Terminal-based AI coding agent with interactive TUI for autonomous code generation.

Features:
  - Interactive TUI with React/Ink
  - Autonomous agent with tool calls (bash, read, write, edit, glob, grep)
  - Permission system with pattern-based rules
  - Session management with auto-compaction
  - Dual providers: GitHub Copilot and Ollama
  - MCP server integration
  - Todo panel and theme system
  - Streaming responses
  - GitHub-compatible project context
This commit is contained in:
2026-01-27 23:33:06 -05:00
commit 0062e5d9d9
521 changed files with 66418 additions and 0 deletions

263
.gitignore vendored Normal file
View File

@@ -0,0 +1,263 @@
# Codetyper.nvim - AI coding partner files
*.coder.*
.coder/
.codetyper/
.claude/
# Node + TypeScript (generated via gitignore.io)
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
pnpm-debug.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory
coverage/
*.lcov
.nyc_output
# Grunt intermediate storage
.grunt
# Bower dependency directory
bower_components/
# Compiled binary addons
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript build info
*.tsbuildinfo
# Compiled output
dist/
out/
build/
lib/
# Next.js, Nuxt.js, SvelteKit, Vercel, etc.
.next/
.nuxt/
.svelte-kit/
.vercel/
.cache/
.parcel-cache/
.output/
.turbo/
# Serverless directories
.serverless/
# Environment files
.env
.env.*.local
.env.local
# IDEs and editors
.vscode/
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.idea/
*.sublime-project
*.sublime-workspace
# Misc
.DS_Store
Thumbs.db
*.swp
# OS generated files
Desktop.ini
*/Thumbs.db
# Optional npm cache
.npm
# Yarn Integrity file
.yarn-integrity
# Yarn v2
.pnp.*
.yarn/*
!.yarn/releases
!.yarn/plugins
!.yarn/sdks
!.yarn/versions
# Coverage and test output
coverage/
test-results/
# Temporary files
tmp/
temp/
# eslint cache
.eslintcache
# Node + TypeScript (generated via gitignore.io)
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
pnpm-debug.log*
lerna-debug.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory
coverage/
*.lcov
.nyc_output
# Grunt intermediate storage
.grunt
# Bower dependency directory
bower_components/
# Compiled binary addons
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript build info
*.tsbuildinfo
# Compiled output
dist/
out/
build/
lib/
# Next.js, Nuxt.js, SvelteKit, Vercel, etc.
.next/
.nuxt/
.svelte-kit/
.vercel/
.cache/
.parcel-cache/
.output/
.turbo/
# Serverless directories
.serverless/
# Environment files
.env
.env.*.local
.env.local
.env.test
.env.production.local
# dotenv environment variables for direnv
.envrc
# Local env files
.local/
# IDEs and editors
.vscode/
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.idea/
*.sublime-project
*.sublime-workspace
# Misc
.DS_Store
Thumbs.db
*.swp
# OS generated files
Desktop.ini
*/Thumbs.db
# Optional npm cache
.npm
# Yarn Integrity file
.yarn-integrity
# Yarn v2 / Plug'n'Play
.pnp.*
.yarn/*
!.yarn/releases
!.yarn/plugins
!.yarn/sdks
!.yarn/versions
# pnpm
.pnpm-store/
.pnpm-debug.log
# Coverage and test output
coverage/
test-results/
jest-test-results.json
# Temporary files
tmp/
temp/
# eslint cache
.eslintcache
# Parcel cache
.cache/
.parcel-cache/
# Turborepo
.turbo/
# Storybook build outputs
out-storybook/
storybook-static/
# Build artifacts from tools
*.tgz
*.snapshot
# Generated files
*.log.*
npm-debug.log*
# Lockfiles (if you prefer to ignore; typically keep them)
# package-lock.json
# yarn.lock
# pnpm-lock.yaml
# Local build files
.cache-loader
!.env.example
# Other
.vscode-test/
coverage-final.json

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Carlos Gutierrez <carlos.gutierrez@carg.dev>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

284
README.md Normal file
View File

@@ -0,0 +1,284 @@
# CodeTyper CLI
An AI-powered terminal coding agent with an interactive TUI. CodeTyper autonomously executes coding tasks using tool calls with granular permission controls and intelligent provider routing.
![CodeTyper Welcome Screen](assets/CodetyperLogin.png)
## How It Works
CodeTyper is an autonomous coding agent that runs in your terminal. You describe what you want to build or fix, and CodeTyper:
1. **Analyzes** your request and breaks it into steps
2. **Executes** tools (bash, read, write, edit) to accomplish the task
3. **Asks permission** before modifying files or running commands
4. **Learns** from your project to provide context-aware assistance
### Cascading Provider System
CodeTyper uses an intelligent provider routing system:
```
User Request
|
v
[Detect Task Type] --> code_generation, bug_fix, refactoring, etc.
|
v
[Check Ollama Score] --> Quality score from past interactions
|
v
[Route Decision]
|
+-- High Score (85%+) --> Ollama Only (trusted)
|
+-- Low Score (40%-) --> Copilot Only (needs improvement)
|
+-- Medium Score --> Cascade Mode
|
v
[1. Ollama generates response]
|
v
[2. Copilot audits for issues]
|
v
[3. Update quality scores]
|
v
[Return best response]
```
Over time, CodeTyper learns which provider performs best for different task types.
## Installation
```bash
# Clone and install
git clone https://github.com/your-username/codetyper-cli.git
cd codetyper-cli
bun install && bun run build && bun link
# Login to a provider
codetyper login copilot
# Start interactive chat
codetyper
```
## Features
### Interactive TUI
Full-screen terminal interface with real-time streaming responses.
![CodeTyper Status View](assets/CodetyperView.png)
**Key bindings:**
- `Enter` - Send message
- `Shift+Enter` - New line
- `/` - Open command menu
- `Ctrl+Tab` - Toggle interaction mode
- `Ctrl+T` - Toggle todo panel
- `Shift+Up/Down` - Scroll log panel
- `Ctrl+C` (twice) - Exit
### Command Menu
Press `/` to access all commands organized by category.
![Command Menu](assets/CodetyperMenu.png)
**Available Commands:**
| Category | Command | Description |
|----------|---------|-------------|
| General | `/help` | Show available commands |
| General | `/clear` | Clear conversation history |
| General | `/exit` | Exit the chat |
| Session | `/save` | Save current session |
| Session | `/context` | Show context information |
| Session | `/usage` | Show token usage statistics |
| Session | `/remember` | Save a learning about the project |
| Session | `/learnings` | Show saved learnings |
| Settings | `/model` | Select AI model |
| Settings | `/agent` | Select agent |
| Settings | `/mode` | Switch interaction mode |
| Settings | `/provider` | Switch LLM provider |
| Settings | `/status` | Show provider status |
| Settings | `/theme` | Change color theme |
| Settings | `/mcp` | Manage MCP servers |
| Account | `/whoami` | Show logged in account |
| Account | `/login` | Authenticate with provider |
| Account | `/logout` | Sign out from provider |
### Agent Mode with Diff View
When CodeTyper modifies files, you see a clear diff view of changes.
![Agent Mode with Diffs](assets/CodetyperAgentMode.png)
**Interaction Modes:**
- **Agent** - Full access, can modify files
- **Ask** - Read-only, answers questions
- **Code Review** - Review PRs and diffs
### Permission System
Granular control over what CodeTyper can do. Every file operation requires approval.
![Permission Modal](assets/CodetyperPermissionView.png)
**Permission Scopes:**
- `[y]` Yes, this once
- `[s]` Yes, for this session
- `[a]` Always allow for this project
- `[g]` Always allow globally
- `[n]` No, deny this request
### Model Selection
Access to multiple AI models through GitHub Copilot.
![Model Selection](assets/CodetyperCopilotModels.png)
**Available Models:**
- GPT-5, GPT-5-mini (Unlimited)
- GPT-5.2-codex, GPT-5.1-codex
- Grok-code-fast-1
- And more...
### Theme System
14+ built-in themes to customize your experience.
![Theme Selection](assets/CodetyperThemes.png)
**Available Themes:**
default, dracula, nord, tokyo-night, gruvbox, monokai, catppuccin, one-dark, solarized-dark, github-dark, rose-pine, kanagawa, ayu-dark, cargdev-cyberpunk
## Providers
| Provider | Models | Auth Method | Use Case |
|----------|--------|-------------|----------|
| **GitHub Copilot** | GPT-5, Claude, Gemini | OAuth (device flow) | Cloud-based, high quality |
| **Ollama** | Llama, DeepSeek, Qwen, etc. | Local server | Private, offline, zero-cost |
### Cascade Mode
When both providers are available, CodeTyper can use them together:
1. **Ollama** processes the request first (fast, local)
2. **Copilot** audits the response for issues
3. Quality scores update based on audit results
4. Future requests route based on learned performance
Check provider status with `/status`:
```
═══ Provider Status ═══
Current Provider: copilot
Cascade Mode: Enabled
Ollama:
Status: ● Available
Quality Score: 72%
Copilot:
Status: ● Available
```
## Configuration
Settings are stored in `~/.config/codetyper/config.json`:
```json
{
"provider": "copilot",
"model": "auto",
"theme": "default",
"cascadeEnabled": true,
"maxIterations": 20,
"timeout": 30000
}
```
### Project Context
CodeTyper reads project-specific context from:
- `.github/` - GitHub workflows and templates
- `.codetyper/` - Project-specific rules and learnings
- `rules.md` - Custom instructions for the AI
## CLI Usage
```bash
# Start interactive TUI
codetyper
# Start with a prompt
codetyper "Create a REST API with Express"
# Continue last session
codetyper --continue
# Resume specific session
codetyper --resume <session-id>
# Use specific provider
codetyper --provider ollama
# Print mode (non-interactive)
codetyper --print "Explain this codebase"
```
## Tools
CodeTyper has access to these built-in tools:
| Tool | Description |
|------|-------------|
| `bash` | Execute shell commands |
| `read` | Read file contents |
| `write` | Create or overwrite files |
| `edit` | Find and replace in files |
| `todo-read` | Read current todo list |
| `todo-write` | Update todo list |
### MCP Integration
Connect external MCP (Model Context Protocol) servers for extended capabilities:
```bash
# In the TUI
/mcp
# Then add a new server
```
## Development
```bash
# Watch mode
bun run dev
# Type check
bun run typecheck
# Build
bun run build
# Run tests
bun test
# Lint
bun run lint
```
## Documentation
- [Changelog](docs/CHANGELOG.md) - Version history and changes
- [Contributing](docs/CONTRIBUTING.md) - How to contribute
## License
MIT - See [LICENSE](LICENSE) for details.

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

BIN
assets/CodetyperLogin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

BIN
assets/CodetyperMenu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
assets/CodetyperThemes.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
assets/CodetyperView.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

1
bin/codetyper Symbolic link
View File

@@ -0,0 +1 @@
../lib/node_modules/codetyper-cli/dist/index.js

1228
bun.lock Normal file

File diff suppressed because it is too large Load Diff

6
bunfig.toml Normal file
View File

@@ -0,0 +1,6 @@
# Bun configuration for @opentui/solid JSX transformation
preload = ["@opentui/solid/preload"]
[run]
# Enable browser export conditions for @opentui/solid
conditions = ["browser"]

132
docs/CHANGELOG.md Normal file
View File

@@ -0,0 +1,132 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- **Home Screen**: New welcome screen with centered gradient logo
- Displays version, provider, and model info
- Transitions to session view on first message
- Clean, centered layout
- **MCP Integration**: Model Context Protocol server support
- Connect to external MCP servers
- Use tools from connected servers
- `/mcp` command for server management
- Status display in UI
- **Reasoning System**: Advanced agent orchestration
- Memory selection for context optimization
- Quality evaluation of responses
- Termination detection for agent loops
- Context compression for long conversations
- Retry policies with exponential backoff
- **Todo Panel**: Task tracking during sessions
- Toggle visibility with `Ctrl+T`
- `todo-read` and `todo-write` tools
- Zustand-based state management
- **Theme System**: Customizable color themes
- `/theme` command to switch themes
- Dark, Light, Tokyo Night, Dracula themes
- Persistent theme preference
- **Agent Selection**: Switch between agent modes
- `/agent` command for selection
- Coder, Architect, Reviewer agents
- **Learning System**: Knowledge persistence
- Vector store for embeddings
- Semantic search capabilities
- Project learnings storage
- **Streaming Responses**: Real-time message display
- Faster feedback from LLM
- Progress indicators
- **Enhanced Navigation**:
- `PageUp/PageDown` for fast scrolling
- `Shift+Up/Down` for line-by-line scroll
- `Ctrl+Home/End` to jump to top/bottom
- **Optimized Permissions**: Performance improvements
- Pattern caching
- Indexed pattern matching
- Faster permission checks
- **Auto-Compaction**: Context management
- Automatic conversation compression
- Maintains context within limits
### Changed
- Improved session header with token count and context percentage
- Enhanced status bar with MCP connection info
- Better command menu with more commands
## [0.1.0] - 2025-01-16
### Added
- **Interactive TUI**: Full terminal UI using Ink (React for CLIs)
- Message-based input (Enter to send, Alt+Enter for newlines)
- Log panel showing conversation history
- Status bar with session info
- ASCII banner header
- **Permission System**: Granular control over tool execution
- Interactive permission modal with keyboard navigation
- Scoped permissions: once, session, project, global
- Pattern-based matching: `Bash(command:args)`, `Read(*)`, `Write(path)`, `Edit(*.ext)`
- Persistent storage in `~/.codetyper/settings.json` and `.codetyper/settings.json`
- **Agent System**: Autonomous task execution
- Multi-turn conversation with tool calls
- Automatic retry with exponential backoff for rate limits
- Configurable max iterations
- **Tools**:
- `bash` - Execute shell commands
- `read` - Read file contents
- `write` - Create or overwrite files
- `edit` - Find and replace in files
- **Provider Support**:
- GitHub Copilot (default) - OAuth device flow authentication, access to GPT-4o, GPT-5, Claude, Gemini via Copilot API
- Ollama - Local server (no auth), run any local model
- **Session Management**:
- Persistent session storage
- Continue previous sessions with `--continue`
- Resume specific sessions with `--resume <id>`
- **CLI Commands**:
- `codetyper` - Start interactive TUI
- `codetyper <prompt>` - Start with initial prompt
- `codetyper login <provider>` - Authenticate with provider
- `codetyper status` - Show provider status
- `codetyper config` - Manage configuration
### Changed
- Migrated from readline-based input to Ink TUI
- Removed classic mode in favor of TUI-only interface
- Tool output now captured and displayed in log panel (not streamed to stdout)
### Fixed
- Permission modal not showing in TUI mode
- Input area blocking during command execution
- Rate limit handling for Copilot provider (429 errors)
---
## Version History
- **0.1.0** - Initial release with TUI, agent system, and multi-provider support

202
docs/CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,202 @@
# Contributing to CodeTyper CLI
Thank you for your interest in contributing to CodeTyper CLI! This document provides guidelines and instructions for contributing.
## Code of Conduct
Please be respectful and constructive in all interactions. We welcome contributors of all experience levels.
## Getting Started
### Prerequisites
- Node.js >= 18.0.0
- npm or yarn
- Git
### Setup
1. Fork the repository
2. Clone your fork:
```bash
git clone https://github.com/YOUR_USERNAME/codetyper-cli.git
cd codetyper-cli
```
3. Install dependencies:
```bash
npm install
```
4. Build the project:
```bash
npm run build
```
5. Link for local testing:
```bash
npm link
```
### Development Workflow
```bash
# Start TypeScript watch mode
npm run dev
# Run tests
npm test
# Lint code
npm run lint
# Format code
npm run format
```
## How to Contribute
### Reporting Bugs
1. Check existing issues to avoid duplicates
2. Create a new issue with:
- Clear, descriptive title
- Steps to reproduce
- Expected vs actual behavior
- Environment details (OS, Node version, provider)
- Relevant logs or screenshots
### Suggesting Features
1. Check existing issues for similar requests
2. Create a feature request with:
- Clear description of the feature
- Use cases and motivation
- Proposed implementation (optional)
### Submitting Pull Requests
1. Create a branch from `main`:
```bash
git checkout -b feature/your-feature-name
```
2. Make your changes following our coding standards
3. Write/update tests if applicable
4. Ensure all tests pass:
```bash
npm test
```
5. Commit with clear messages:
```bash
git commit -m "feat: add new feature description"
```
6. Push and create a Pull Request
### Commit Message Format
We follow [Conventional Commits](https://www.conventionalcommits.org/):
- `feat:` - New feature
- `fix:` - Bug fix
- `docs:` - Documentation changes
- `style:` - Code style changes (formatting, etc.)
- `refactor:` - Code refactoring
- `test:` - Adding or updating tests
- `chore:` - Maintenance tasks
Examples:
```
feat: add permission caching for faster lookups
fix: resolve race condition in agent loop
docs: update README with new CLI options
```
## Coding Standards
### TypeScript
- Use TypeScript strict mode
- Define explicit types (avoid `any` when possible)
- Use interfaces for object shapes
- Export types that are part of the public API
### Code Style
- Use 2 spaces for indentation
- Use single quotes for strings
- Add trailing commas in multi-line structures
- Keep lines under 100 characters when reasonable
### File Organization
```
src/
├── index.ts # Entry point only
├── commands/ # CLI command implementations
├── providers/ # LLM provider integrations
├── tools/ # Agent tools (bash, read, write, edit)
├── tui/ # Terminal UI components
│ └── components/ # Reusable UI components
└── types.ts # Shared type definitions
```
### Testing
- Write tests for non-UI logic
- Place tests in `tests/` directory
- Name test files `*.test.ts`
- Use descriptive test names
```typescript
describe('PermissionManager', () => {
it('should match wildcard patterns correctly', () => {
// ...
});
});
```
### Documentation
- Add JSDoc comments for public APIs
- Update README for user-facing changes
- Update CHANGELOG for notable changes
## Project Structure
### Key Files
| File | Purpose |
|------|---------|
| `src/index.ts` | CLI entry point, command registration |
| `src/agent.ts` | Agent loop, tool orchestration |
| `src/permissions.ts` | Permission system |
| `src/commands/chat-tui.tsx` | Main TUI command |
| `src/tui/App.tsx` | Root TUI component |
| `src/tui/store.ts` | Zustand state management |
### Adding a New Provider
1. Create `src/providers/yourprovider.ts`
2. Implement the `Provider` interface
3. Register in `src/providers/index.ts`
4. Add authentication in `src/commands/login.ts`
5. Update documentation
### Adding a New Tool
1. Create `src/tools/yourtool.ts`
2. Define parameters with Zod schema
3. Implement `execute` function
4. Register in `src/tools/index.ts`
5. Add permission handling if needed
## Questions?
- Open a GitHub issue for questions
- Tag with `question` label
## License
By contributing, you agree that your contributions will be licensed under the MIT License.

16
eslint.config.js Normal file
View File

@@ -0,0 +1,16 @@
import globals from "globals";
import tseslint from "typescript-eslint";
import pluginJs from "@eslint/js";
export default tseslint.config(
pluginJs.configs.recommended,
...tseslint.configs.recommended,
{
languageOptions: {
globals: {
...globals.browser,
...globals.node,
},
},
},
);

9224
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

100
package.json Normal file
View File

@@ -0,0 +1,100 @@
{
"name": "codetyper-cli",
"version": "0.1.74",
"description": "CodeTyper AI Agent - Standalone CLI for autonomous code generation",
"main": "dist/index.js",
"bin": {
"codetyper": "./dist/index.js"
},
"type": "module",
"scripts": {
"dev": "bun src/index.ts",
"dev:nobump": "bun scripts/build.ts && npm link",
"dev:watch": "bun scripts/dev-watch.ts",
"build": "bun scripts/build.ts",
"sync-version": "bun scripts/sync-version.ts",
"start": "bun src/index.ts",
"test": "bun test",
"lint": "bun eslint src/**/*.ts",
"format": "npx prettier --write \"src/**/*.ts\"",
"prettier": "npx prettier --write \"src/**/*.ts\" \"src/**/*.tsx\" \"scripts/**/*.ts\"",
"typecheck": "bun tsc --noEmit"
},
"keywords": [
"ai",
"coding",
"assistant",
"cli",
"agent",
"typescript"
],
"author": {
"name": "Carlos Gutierrez",
"email": "carlos.gutierrez@carg.dev",
"url": "https://github.com/CarGDev"
},
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/CarGDev/codetyper.nvim.git"
},
"homepage": "https://github.com/CarGDev/codetyper.nvim#readme",
"bugs": {
"url": "https://github.com/CarGDev/codetyper.nvim/issues"
},
"files": [
"dist/**/*.js",
"dist/**/*.wasm",
"dist/**/*.scm",
"src/version.json"
],
"dependencies": {
"@opentui/core": "^0.1.74",
"@opentui/solid": "^0.1.74",
"@solid-primitives/event-bus": "^1.0.11",
"@solid-primitives/scheduled": "^1.4.3",
"boxen": "^8.0.1",
"chalk": "^5.3.0",
"chokidar": "^5.0.0",
"cli-highlight": "^2.1.11",
"commander": "^14.0.2",
"fast-glob": "^3.3.2",
"got": "^14.0.0",
"inquirer": "^13.2.1",
"mimic-function": "^5.0.1",
"opentui-spinner": "^0.0.6",
"ora": "^9.1.0",
"solid-js": "^1.9.10",
"uuid": "^13.0.0",
"zustand": "^5.0.10",
"zod": "^4.3.5",
"zod-to-json-schema": "^3.25.1"
},
"overrides": {
"string-width": "^5.1.2",
"strip-ansi": "^6.0.1"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.3",
"@eslint/js": "^9.39.2",
"@types/inquirer": "^9.0.7",
"@types/node": "^25.0.10",
"@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^8.53.1",
"@typescript-eslint/parser": "^8.53.1",
"eslint": "^9.39.2",
"eslint-config-standard": "^17.1.0",
"eslint-config-standard-with-typescript": "^43.0.1",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-n": "^17.23.2",
"eslint-plugin-promise": "^7.2.1",
"globals": "^17.0.0",
"prettier": "^3.1.1",
"typescript": "^5.9.3",
"typescript-eslint": "^8.53.0",
"vitest": "^4.0.17"
},
"engines": {
"node": ">=18.0.0"
}
}

62
scripts/build.ts Normal file
View File

@@ -0,0 +1,62 @@
#!/usr/bin/env bun
/**
* Build script for codetyper-cli
*
* Uses the @opentui/solid plugin for JSX transformation during bundling.
*/
import solidPlugin from "../node_modules/@opentui/solid/scripts/solid-plugin";
import { join, dirname } from "path";
import { fileURLToPath } from "url";
import { readFile, writeFile } from "fs/promises";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const ROOT_DIR = join(__dirname, "..");
process.chdir(ROOT_DIR);
// Sync version before building
const syncVersion = async (): Promise<void> => {
const packageJson = JSON.parse(
await readFile(join(ROOT_DIR, "package.json"), "utf-8"),
);
const { version } = packageJson;
await writeFile(
join(ROOT_DIR, "src/version.json"),
JSON.stringify({ version }, null, 2) + "\n",
);
console.log(`Synced version: ${version}`);
};
await syncVersion();
// Build the application
console.log("Building codetyper-cli...");
const result = await Bun.build({
entrypoints: ["./src/index.ts"],
outdir: "./dist",
target: "node",
conditions: ["node"],
plugins: [solidPlugin],
sourcemap: "external",
});
if (!result.success) {
console.error("Build failed:");
for (const log of result.logs) {
console.error(log);
}
process.exit(1);
}
// Update shebang to use node
const distPath = join(ROOT_DIR, "dist/index.js");
let distContent = await readFile(distPath, "utf-8");
distContent = distContent.replace(/^#!.*\n/, "#!/usr/bin/env node\n");
await writeFile(distPath, distContent);
console.log("Build completed successfully!");

138
scripts/dev-watch.ts Normal file
View File

@@ -0,0 +1,138 @@
#!/usr/bin/env bun
/**
* Development watch script for codetyper-cli
*
* Watches for file changes and restarts the TUI application properly,
* handling terminal cleanup between restarts.
*/
import { spawn, type Subprocess } from "bun";
import { watch } from "chokidar";
import { join, dirname } from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const ROOT_DIR = join(__dirname, "..");
const WATCH_PATHS = ["src/**/*.ts", "src/**/*.tsx"];
const IGNORE_PATTERNS = [
"**/node_modules/**",
"**/dist/**",
"**/*.test.ts",
"**/*.spec.ts",
];
const DEBOUNCE_MS = 300;
let currentProcess: Subprocess | null = null;
let restartTimeout: ReturnType<typeof setTimeout> | null = null;
const clearTerminal = (): void => {
// Reset terminal and clear screen
process.stdout.write("\x1b[2J\x1b[H\x1b[3J");
};
const killProcess = async (): Promise<void> => {
if (currentProcess) {
try {
currentProcess.kill("SIGTERM");
// Wait a bit for graceful shutdown
await new Promise((resolve) => setTimeout(resolve, 100));
if (currentProcess.exitCode === null) {
currentProcess.kill("SIGKILL");
}
} catch {
// Process might already be dead
}
currentProcess = null;
}
};
const startProcess = (): void => {
clearTerminal();
console.log("\x1b[36m[dev-watch]\x1b[0m Starting codetyper...");
console.log("\x1b[90m─────────────────────────────────────\x1b[0m\n");
currentProcess = spawn({
cmd: ["bun", "src/index.ts"],
cwd: ROOT_DIR,
stdio: ["inherit", "inherit", "inherit"],
env: {
...process.env,
NODE_ENV: "development",
},
});
currentProcess.exited.then((code) => {
if (code !== 0 && code !== null) {
console.log(
`\n\x1b[33m[dev-watch]\x1b[0m Process exited with code ${code}`,
);
}
});
};
const scheduleRestart = (path: string): void => {
if (restartTimeout) {
clearTimeout(restartTimeout);
}
restartTimeout = setTimeout(async () => {
console.log(`\n\x1b[33m[dev-watch]\x1b[0m Change detected: ${path}`);
console.log("\x1b[33m[dev-watch]\x1b[0m Restarting...\n");
await killProcess();
startProcess();
}, DEBOUNCE_MS);
};
const main = async (): Promise<void> => {
console.log("\x1b[36m[dev-watch]\x1b[0m Watching for changes...");
console.log(`\x1b[90mRoot: ${ROOT_DIR}\x1b[0m`);
console.log(`\x1b[90mPaths: ${WATCH_PATHS.join(", ")}\x1b[0m`);
const watcher = watch(WATCH_PATHS, {
ignored: IGNORE_PATTERNS,
persistent: true,
ignoreInitial: true,
cwd: ROOT_DIR,
usePolling: false,
awaitWriteFinish: {
stabilityThreshold: 100,
pollInterval: 100,
},
});
watcher.on("ready", () => {
console.log("\x1b[32m[dev-watch]\x1b[0m Watcher ready\n");
});
watcher.on("error", (error) => {
console.error("\x1b[31m[dev-watch]\x1b[0m Watcher error:", error);
});
watcher.on("change", scheduleRestart);
watcher.on("add", scheduleRestart);
watcher.on("unlink", scheduleRestart);
// Handle exit signals
const cleanup = async (): Promise<void> => {
console.log("\n\x1b[36m[dev-watch]\x1b[0m Shutting down...");
await watcher.close();
await killProcess();
process.exit(0);
};
process.on("SIGINT", cleanup);
process.on("SIGTERM", cleanup);
// Start the initial process
startProcess();
};
main().catch((err) => {
console.error("Error:", err);
process.exit(1);
});

29
scripts/sync-version.ts Normal file
View File

@@ -0,0 +1,29 @@
import { readFile, writeFile } from "fs/promises";
import { join, dirname } from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const ROOT_DIR = join(__dirname, "..");
const PACKAGE_JSON_PATH = join(ROOT_DIR, "package.json");
const VERSION_JSON_PATH = join(ROOT_DIR, "src/version.json");
const syncVersion = async (): Promise<void> => {
const packageJson = JSON.parse(await readFile(PACKAGE_JSON_PATH, "utf-8"));
const { version } = packageJson;
const versionJson = { version };
await writeFile(
VERSION_JSON_PATH,
JSON.stringify(versionJson, null, 2) + "\n",
);
console.log(`Synced version: ${version}`);
};
syncVersion().catch((error) => {
console.error("Failed to sync version:", error);
process.exit(1);
});

36
src/commands/chat-tui.ts Normal file
View File

@@ -0,0 +1,36 @@
/**
* TUI-based Chat Command for CodeTyper CLI (Presentation Layer)
*
* This file is the main entry point for the chat TUI.
* It assembles callbacks and re-exports the execute function.
* All business logic is delegated to chat-tui-service.ts
*/
import type { ChatServiceCallbacks } from "@services/chat-tui-service.ts";
import { onModeChange } from "@commands/components/callbacks/on-mode-change.ts";
import { onLog } from "@commands/components/callbacks/on-log.ts";
import { onToolCall } from "@commands/components/callbacks/on-tool-call.ts";
import { onToolResult } from "@commands/components/callbacks/on-tool-result.ts";
import { onPermissionRequest } from "@commands/components/callbacks/on-permission-request.ts";
import { onLearningDetected } from "@commands/components/callbacks/on-learning-detected.ts";
import executeCommand from "@commands/components/execute/index.ts";
export const createCallbacks = (): ChatServiceCallbacks => ({
onModeChange,
onLog,
onToolCall,
onToolResult,
onPermissionRequest,
onLearningDetected,
});
export const execute = executeCommand;
export {
onModeChange,
onLog,
onToolCall,
onToolResult,
onPermissionRequest,
onLearningDetected,
};

8
src/commands/chat.ts Normal file
View File

@@ -0,0 +1,8 @@
/**
* Interactive chat mode for CodeTyper CLI
*
* This file re-exports the modular chat implementation.
*/
export { execute, createInitialState } from "@commands/components/chat/index";
export type { ChatState } from "@commands/components/chat/index";

View File

@@ -0,0 +1,23 @@
import { v4 as uuidv4 } from "uuid";
import { appStore } from "@tui/index.ts";
import type { LearningResponse } from "@tui/types.ts";
import type { LearningCandidate } from "@services/learning-service.ts";
export const onLearningDetected = async (
candidate: LearningCandidate,
): Promise<LearningResponse> => {
return new Promise((resolve) => {
appStore.setMode("learning_prompt");
appStore.setLearningPrompt({
id: uuidv4(),
content: candidate.content,
context: candidate.context,
category: candidate.category,
resolve: (response: LearningResponse) => {
appStore.setLearningPrompt(null);
appStore.setMode("idle");
resolve(response);
},
});
});
};

View File

@@ -0,0 +1,14 @@
import { appStore } from "@tui/index.ts";
import type { LogType } from "@/types/log";
export const onLog = (
type: string,
content: string,
metadata?: Record<string, unknown>,
): void => {
appStore.addLog({
type: type as LogType,
content,
metadata,
});
};

View File

@@ -0,0 +1,5 @@
import { appStore } from "@tui/index.ts";
export const onModeChange = (mode: string): void => {
appStore.setMode(mode as Parameters<typeof appStore.setMode>[0]);
};

View File

@@ -0,0 +1,7 @@
interface PermissionResponse {
allowed: boolean;
}
export const onPermissionRequest = async (): Promise<PermissionResponse> => {
return { allowed: false };
};

View File

@@ -0,0 +1,25 @@
import { appStore } from "@tui/index.ts";
import { isQuietTool } from "@utils/tools.ts";
import type { ToolCallParams } from "@interfaces/ToolCallParams.ts";
export const onToolCall = (call: ToolCallParams): void => {
appStore.setCurrentToolCall({
id: call.id,
name: call.name,
description: call.description,
status: "running",
});
const isQuiet = isQuietTool(call.name, call.args);
appStore.addLog({
type: "tool",
content: call.description,
metadata: {
toolName: call.name,
toolStatus: "running",
toolDescription: call.description,
quiet: isQuiet,
},
});
};

View File

@@ -0,0 +1,47 @@
import { appStore } from "@tui/index.ts";
import {
truncateOutput,
detectDiffContent,
} from "@services/chat-tui-service.ts";
import { getThinkingMessage } from "@constants/status-messages.ts";
export const onToolResult = (
success: boolean,
title: string,
output?: string,
error?: string,
): void => {
appStore.updateToolCall({
status: success ? "success" : "error",
result: success ? output : undefined,
error: error,
});
const state = appStore.getState();
const logEntry = state.logs.find(
(log) => log.type === "tool" && log.metadata?.toolStatus === "running",
);
if (logEntry) {
const diffData = output ? detectDiffContent(output) : undefined;
const displayContent = diffData?.isDiff
? output
: output
? truncateOutput(output)
: "";
appStore.updateLog(logEntry.id, {
content: success
? `${title}${displayContent ? "\n" + displayContent : ""}`
: `${title}: ${error}`,
metadata: {
...logEntry.metadata,
toolStatus: success ? "success" : "error",
toolDescription: title,
diffData: diffData,
},
});
}
appStore.setThinkingMessage(getThinkingMessage());
};

View File

@@ -0,0 +1,35 @@
/**
* Show available agents command
*/
import chalk from "chalk";
import { agentLoader } from "@services/agent-loader";
import type { ChatState } from "@commands/components/chat/state";
export const showAgents = async (state: ChatState): Promise<void> => {
const agents = await agentLoader.getAvailableAgents(process.cwd());
const currentAgent = state.currentAgent ?? "coder";
console.log("\n" + chalk.bold.underline("Available Agents") + "\n");
for (const agent of agents) {
const isCurrent = agent.id === currentAgent;
const marker = isCurrent ? chalk.cyan("→") : " ";
const nameStyle = isCurrent ? chalk.cyan.bold : chalk.white;
console.log(`${marker} ${nameStyle(agent.name)}`);
if (agent.description) {
console.log(` ${chalk.gray(agent.description)}`);
}
if (agent.model) {
console.log(` ${chalk.gray(`Model: ${agent.model}`)}`);
}
console.log();
}
console.log(chalk.gray("Use /agent <name> to switch agents"));
console.log();
};

View File

@@ -0,0 +1,60 @@
/**
* Switch agent command
*/
import chalk from "chalk";
import { errorMessage, infoMessage, warningMessage } from "@utils/terminal";
import { agentLoader } from "@services/agent-loader";
import type { ChatState } from "@commands/components/chat/state";
export const switchAgent = async (
agentName: string,
state: ChatState,
): Promise<void> => {
if (!agentName.trim()) {
warningMessage("Usage: /agent <name>");
infoMessage("Use /agents to see available agents");
return;
}
const normalizedName = agentName.toLowerCase().trim();
const agents = await agentLoader.getAvailableAgents(process.cwd());
// Find agent by id or partial name match
const agent = agents.find(
(a) =>
a.id === normalizedName ||
a.name.toLowerCase() === normalizedName ||
a.id.includes(normalizedName) ||
a.name.toLowerCase().includes(normalizedName),
);
if (!agent) {
errorMessage(`Agent not found: ${agentName}`);
infoMessage("Use /agents to see available agents");
return;
}
state.currentAgent = agent.id;
// Update system prompt with agent prompt
if (agent.prompt) {
// Prepend agent prompt to system prompt
const basePrompt = state.systemPrompt;
state.systemPrompt = `${agent.prompt}\n\n${basePrompt}`;
// Update the system message in messages array
if (state.messages.length > 0 && state.messages[0].role === "system") {
state.messages[0].content = state.systemPrompt;
}
}
console.log();
console.log(chalk.green(`✓ Switched to agent: ${chalk.bold(agent.name)}`));
if (agent.description) {
console.log(chalk.gray(` ${agent.description}`));
}
console.log();
};

View File

@@ -0,0 +1,12 @@
import chalk from "chalk";
import type { ChatState } from "./state.ts";
export const createCleanup = (state: ChatState) => (): void => {
state.isRunning = false;
if (state.inputEditor) {
state.inputEditor.stop();
state.inputEditor = null;
}
console.log("\n" + chalk.cyan("Goodbye!"));
process.exit(0);
};

View File

@@ -0,0 +1,111 @@
import { saveSession } from "@services/session";
import { showHelp } from "@commands/components/chat/commands/show-help";
import { clearConversation } from "@commands/components/chat/history/clear-conversation";
import { showContextFiles } from "@commands/components/chat/context/show-context-files";
import { removeFile } from "@commands/components/chat/context/remove-file";
import { showContext } from "@commands/components/chat/history/show-context";
import { compactHistory } from "@commands/components/chat/history/compact-history";
import { showHistory } from "@commands/components/chat/history/show-history";
import { showModels } from "@commands/components/chat/models/show-models";
import { showProviders } from "@commands/components/chat/models/show-providers";
import { switchProvider } from "@commands/components/chat/models/switch-provider";
import { switchModel } from "@commands/components/chat/models/switch-model";
import { showSessionInfo } from "@commands/components/chat/session/show-session-info";
import { listSessions } from "@commands/components/chat/session/list-sessions";
import { showUsage } from "@commands/components/chat/usage/show-usage";
import { showAgents } from "@commands/components/chat/agents/show-agents";
import { switchAgent } from "@commands/components/chat/agents/switch-agent";
import { handleMCP } from "@commands/components/chat/mcp/handle-mcp";
import { CommandContext } from "@interfaces/commandContext";
import type { CommandHandler } from "@/types/commandHandler";
import { successMessage } from "@utils/terminal";
const COMMAND_REGISTRY: Map<string, CommandHandler> = new Map<
string,
CommandHandler
>([
["help", () => showHelp()],
["h", () => showHelp()],
["clear", (ctx: CommandContext) => clearConversation(ctx.state)],
["c", (ctx: CommandContext) => clearConversation(ctx.state)],
["files", (ctx: CommandContext) => showContextFiles(ctx.state.contextFiles)],
["f", (ctx: CommandContext) => showContextFiles(ctx.state.contextFiles)],
["exit", (ctx: CommandContext) => ctx.cleanup()],
["quit", (ctx: CommandContext) => ctx.cleanup()],
["q", (ctx: CommandContext) => ctx.cleanup()],
[
"save",
async () => {
await saveSession();
successMessage("Session saved");
},
],
[
"s",
async () => {
await saveSession();
successMessage("Session saved");
},
],
[
"models",
async (ctx: CommandContext) =>
showModels(ctx.state.currentProvider, ctx.state.currentModel),
],
[
"m",
async (ctx: CommandContext) =>
showModels(ctx.state.currentProvider, ctx.state.currentModel),
],
["providers", async () => showProviders()],
["p", async () => showProviders()],
[
"provider",
async (ctx: CommandContext) =>
switchProvider(ctx.args.join(" "), ctx.state),
],
[
"model",
async (ctx: CommandContext) => switchModel(ctx.args.join(" "), ctx.state),
],
["context", (ctx: CommandContext) => showContext(ctx.state)],
["compact", (ctx: CommandContext) => compactHistory(ctx.state)],
["history", (ctx: CommandContext) => showHistory(ctx.state)],
[
"remove",
(ctx: CommandContext) =>
removeFile(ctx.args.join(" "), ctx.state.contextFiles),
],
[
"rm",
(ctx: CommandContext) =>
removeFile(ctx.args.join(" "), ctx.state.contextFiles),
],
["session", async () => showSessionInfo()],
["sessions", async () => listSessions()],
["usage", async (ctx: CommandContext) => showUsage(ctx.state)],
["u", async (ctx: CommandContext) => showUsage(ctx.state)],
[
"agent",
async (ctx: CommandContext) => {
if (ctx.args.length === 0) {
await showAgents(ctx.state);
} else {
await switchAgent(ctx.args.join(" "), ctx.state);
}
},
],
[
"a",
async (ctx: CommandContext) => {
if (ctx.args.length === 0) {
await showAgents(ctx.state);
} else {
await switchAgent(ctx.args.join(" "), ctx.state);
}
},
],
["mcp", async (ctx: CommandContext) => handleMCP(ctx.args)],
]);
export default COMMAND_REGISTRY;

View File

@@ -0,0 +1,28 @@
import { warningMessage, infoMessage } from "@utils/terminal";
import type { ChatState } from "@commands/components/chat/state";
import COMMAND_REGISTRY from "@commands/components/chat/commands/commandsRegistry";
const isValidCommand = (cmd: string): boolean => {
return COMMAND_REGISTRY.has(cmd);
};
export const handleCommand = async (
command: string,
state: ChatState,
cleanup: () => void,
): Promise<void> => {
const parts = command.slice(1).split(/\s+/);
const cmd = parts[0].toLowerCase();
const args = parts.slice(1);
if (!isValidCommand(cmd)) {
warningMessage(`Unknown command: /${cmd}`);
infoMessage("Type /help for available commands");
return;
}
const handler = COMMAND_REGISTRY.get(cmd);
if (handler) {
await handler({ state, args, cleanup });
}
};

View File

@@ -0,0 +1,25 @@
import chalk from "chalk";
import { HELP_COMMANDS } from "@constants/help-commands.ts";
export const showHelp = (): void => {
console.log("\n" + chalk.bold.underline("Commands") + "\n");
for (const [cmd, desc] of HELP_COMMANDS) {
console.log(` ${chalk.yellow(cmd.padEnd(20))} ${desc}`);
}
console.log("\n" + chalk.bold.underline("File References") + "\n");
console.log(` ${chalk.yellow("@<file>")} Add a file to context`);
console.log(
` ${chalk.yellow('@"file with spaces"')} Add file with spaces in name`,
);
console.log(
` ${chalk.yellow("@src/*.ts")} Add files matching glob pattern`,
);
console.log("\n" + chalk.bold.underline("Examples") + "\n");
console.log(" @src/app.ts explain this code");
console.log(" @src/utils.ts @src/types.ts refactor these files");
console.log(" /model gpt-4o");
console.log(" /provider copilot\n");
};

View File

@@ -0,0 +1,35 @@
import { resolve } from "path";
import { existsSync } from "fs";
import fg from "fast-glob";
import { errorMessage, warningMessage } from "@utils/terminal";
import { loadFile } from "@commands/components/chat/context/load-file";
import { IGNORE_FOLDERS } from "@constants/paths";
export const addContextFile = async (
pattern: string,
contextFiles: Map<string, string>,
): Promise<void> => {
try {
const paths = await fg(pattern, {
cwd: process.cwd(),
absolute: true,
ignore: IGNORE_FOLDERS,
});
if (paths.length === 0) {
const absolutePath = resolve(process.cwd(), pattern);
if (existsSync(absolutePath)) {
await loadFile(absolutePath, contextFiles);
} else {
warningMessage(`File not found: ${pattern}`);
}
return;
}
for (const filePath of paths) {
await loadFile(filePath, contextFiles);
}
} catch (error) {
errorMessage(`Failed to add file: ${error}`);
}
};

View File

@@ -0,0 +1,32 @@
import { readFile, stat } from "fs/promises";
import { basename } from "path";
import { warningMessage, successMessage, errorMessage } from "@utils/terminal";
import { addContextFile } from "@services/session";
export const loadFile = async (
filePath: string,
contextFiles: Map<string, string>,
): Promise<void> => {
try {
const stats = await stat(filePath);
if (stats.isDirectory()) {
warningMessage(`Skipping directory: ${filePath}`);
return;
}
if (stats.size > 100 * 1024) {
warningMessage(`File too large (>100KB): ${basename(filePath)}`);
return;
}
const content = await readFile(filePath, "utf-8");
contextFiles.set(filePath, content);
successMessage(
`Added: ${basename(filePath)} (${content.split("\n").length} lines)`,
);
await addContextFile(filePath);
} catch (error) {
errorMessage(`Failed to read file: ${error}`);
}
};

View File

@@ -0,0 +1,27 @@
import { FILE_REFERENCE_PATTERN } from "@constants/patterns";
import { addContextFile } from "@commands/components/chat/context/add-context-file";
export const processFileReferences = async (
input: string,
contextFiles: Map<string, string>,
): Promise<string> => {
const pattern = new RegExp(FILE_REFERENCE_PATTERN.source, "g");
let match;
const filesToAdd: string[] = [];
while ((match = pattern.exec(input)) !== null) {
const filePath = match[1] || match[2] || match[3];
filesToAdd.push(filePath);
}
for (const filePath of filesToAdd) {
await addContextFile(filePath, contextFiles);
}
const textOnly = input.replace(pattern, "").trim();
if (!textOnly && filesToAdd.length > 0) {
return `Analyze the files I've added to the context.`;
}
return input;
};

View File

@@ -0,0 +1,22 @@
import { basename } from "path";
import { warningMessage, successMessage } from "@utils/terminal";
export const removeFile = (
filename: string,
contextFiles: Map<string, string>,
): void => {
if (!filename) {
warningMessage("Please specify a file to remove");
return;
}
for (const [path] of contextFiles) {
if (path.includes(filename) || basename(path) === filename) {
contextFiles.delete(path);
successMessage(`Removed: ${basename(path)}`);
return;
}
}
warningMessage(`File not found in context: ${filename}`);
};

View File

@@ -0,0 +1,32 @@
import chalk from "chalk";
import { basename } from "path";
import { getCurrentSession } from "@services/session";
import { infoMessage, filePath } from "@utils/terminal";
export const showContextFiles = (contextFiles: Map<string, string>): void => {
const session = getCurrentSession();
const files = session?.contextFiles || [];
if (files.length === 0 && contextFiles.size === 0) {
infoMessage("No context files loaded");
return;
}
console.log("\n" + chalk.bold("Context Files:"));
if (contextFiles.size > 0) {
console.log(chalk.gray(" Pending (will be included in next message):"));
for (const [path] of contextFiles) {
console.log(` - ${filePath(basename(path))}`);
}
}
if (files.length > 0) {
console.log(chalk.gray(" In session:"));
files.forEach((file, index) => {
console.log(` ${index + 1}. ${filePath(file)}`);
});
}
console.log();
};

View File

@@ -0,0 +1,10 @@
import { clearMessages } from "@services/session";
import { successMessage } from "@utils/terminal";
import type { ChatState } from "@commands/components/chat/state";
export const clearConversation = (state: ChatState): void => {
state.messages = [{ role: "system", content: state.systemPrompt }];
state.contextFiles.clear();
clearMessages();
successMessage("Conversation cleared");
};

View File

@@ -0,0 +1,15 @@
import { successMessage, infoMessage } from "@utils/terminal";
import type { ChatState } from "@commands/components/chat/state";
export const compactHistory = (state: ChatState): void => {
if (state.messages.length <= 11) {
infoMessage("History is already compact");
return;
}
const systemPrompt = state.messages[0];
const recentMessages = state.messages.slice(-10);
state.messages = [systemPrompt, ...recentMessages];
successMessage(`Compacted to ${state.messages.length - 1} messages`);
};

View File

@@ -0,0 +1,18 @@
import chalk from "chalk";
import type { ChatState } from "@commands/components/chat/state";
export const showContext = (state: ChatState): void => {
const messageCount = state.messages.length - 1;
const totalChars = state.messages.reduce(
(acc, m) => acc + m.content.length,
0,
);
const estimatedTokens = Math.round(totalChars / 4);
console.log("\n" + chalk.bold("Context Information:"));
console.log(` Messages: ${messageCount}`);
console.log(` Characters: ${totalChars.toLocaleString()}`);
console.log(` Estimated tokens: ~${estimatedTokens.toLocaleString()}`);
console.log(` Pending files: ${state.contextFiles.size}`);
console.log();
};

View File

@@ -0,0 +1,18 @@
import chalk from "chalk";
import type { ChatState } from "@commands/components/chat/state";
export const showHistory = (state: ChatState): void => {
console.log("\n" + chalk.bold("Conversation History:") + "\n");
for (let i = 1; i < state.messages.length; i++) {
const msg = state.messages[i];
const role =
msg.role === "user" ? chalk.cyan("You") : chalk.green("Assistant");
const preview = msg.content.slice(0, 100).replace(/\n/g, " ");
console.log(
` ${i}. ${role}: ${preview}${msg.content.length > 100 ? "..." : ""}`,
);
}
console.log();
};

View File

@@ -0,0 +1,224 @@
import chalk from "chalk";
import { infoMessage, errorMessage, warningMessage } from "@utils/terminal";
import {
createSession,
loadSession,
getMostRecentSession,
findSession,
setWorkingDirectory,
} from "@services/session";
import { getConfig } from "@services/config";
import type { Provider as ProviderName, ChatSession } from "@/types/index";
import { getProvider, getProviderStatus } from "@providers/index.ts";
import {
printWelcome,
formatTipLine,
Style,
Theme,
createInputEditor,
} from "@ui/index";
import {
DEFAULT_SYSTEM_PROMPT,
buildSystemPromptWithRules,
} from "@prompts/index.ts";
import type { ChatOptions } from "@interfaces/ChatOptions.ts";
import { createInitialState, type ChatState } from "./state.ts";
import { restoreMessagesFromSession } from "./session/restore-messages.ts";
import { addContextFile } from "./context/add-context-file.ts";
import { handleCommand } from "./commands/handle-command.ts";
import { handleInput } from "./messages/handle-input.ts";
import { executePrintMode } from "./print-mode.ts";
import { createCleanup } from "./cleanup.ts";
export const execute = async (options: ChatOptions): Promise<void> => {
const config = await getConfig();
const state = createInitialState(
(options.provider || config.get("provider")) as ProviderName,
);
state.verbose = options.verbose || false;
state.autoApprove = options.autoApprove || false;
state.currentModel = options.model || config.get("model") || "auto";
const status = await getProviderStatus(state.currentProvider);
if (!status.valid) {
errorMessage(`Provider ${state.currentProvider} is not configured.`);
infoMessage(`Run: codetyper login ${state.currentProvider}`);
process.exit(1);
}
if (options.systemPrompt) {
state.systemPrompt = options.systemPrompt;
} else {
const { prompt: promptWithRules, rulesPaths } =
await buildSystemPromptWithRules(DEFAULT_SYSTEM_PROMPT, process.cwd());
state.systemPrompt = promptWithRules;
if (rulesPaths.length > 0 && state.verbose) {
infoMessage(`Loaded ${rulesPaths.length} rule file(s):`);
for (const rulePath of rulesPaths) {
infoMessage(` - ${rulePath}`);
}
}
if (options.appendSystemPrompt) {
state.systemPrompt =
state.systemPrompt + "\n\n" + options.appendSystemPrompt;
}
}
let session: ChatSession;
if (options.continueSession) {
const recent = await getMostRecentSession(process.cwd());
if (recent) {
session = recent;
await loadSession(session.id);
state.messages = restoreMessagesFromSession(session, state.systemPrompt);
if (state.verbose) {
infoMessage(`Continuing session: ${session.id}`);
}
} else {
warningMessage(
"No previous session found in this directory. Starting new session.",
);
session = await createSession("coder");
}
} else if (options.resumeSession) {
const found = await findSession(options.resumeSession);
if (found) {
session = found;
await loadSession(session.id);
state.messages = restoreMessagesFromSession(session, state.systemPrompt);
if (state.verbose) {
infoMessage(`Resumed session: ${session.id}`);
}
} else {
errorMessage(`Session not found: ${options.resumeSession}`);
process.exit(1);
}
} else {
session = await createSession("coder");
await setWorkingDirectory(process.cwd());
}
if (state.messages.length === 0) {
state.messages = [{ role: "system", content: state.systemPrompt }];
}
if (options.files && options.files.length > 0) {
for (const file of options.files) {
await addContextFile(file, state.contextFiles);
}
}
if (options.printMode && options.initialPrompt) {
await executePrintMode(options.initialPrompt, state);
return;
}
const hasInitialPrompt =
options.initialPrompt && options.initialPrompt.trim().length > 0;
const provider = getProvider(state.currentProvider);
const model = state.currentModel || "auto";
printWelcome("0.1.0", provider.displayName, model);
console.log(
Theme.textMuted +
" Session: " +
Style.RESET +
chalk.gray(session.id.slice(0, 16) + "..."),
);
console.log("");
console.log(
Theme.textMuted +
" Commands: " +
Style.RESET +
chalk.cyan("@file") +
" " +
chalk.cyan("/help") +
" " +
chalk.cyan("/clear") +
" " +
chalk.cyan("/exit"),
);
console.log(
Theme.textMuted +
" Input: " +
Style.RESET +
chalk.cyan("Enter") +
" to send, " +
chalk.cyan("Alt+Enter") +
" for newline",
);
console.log("");
console.log(" " + formatTipLine());
console.log("");
state.inputEditor = createInputEditor({
prompt: "\x1b[36m> \x1b[0m",
continuationPrompt: "\x1b[90m│ \x1b[0m",
});
state.isRunning = true;
const cleanup = createCleanup(state);
const commandHandler = async (command: string, st: ChatState) => {
await handleCommand(command, st, cleanup);
};
state.inputEditor.on("submit", async (input: string) => {
if (state.isProcessing) return;
state.isProcessing = true;
state.inputEditor?.lock();
try {
await handleInput(input, state, commandHandler);
} catch (error) {
errorMessage(`Error: ${error}`);
}
state.isProcessing = false;
if (state.isRunning && state.inputEditor) {
state.inputEditor.unlock();
}
});
state.inputEditor.on("interrupt", () => {
if (state.isProcessing) {
console.log("\n" + chalk.yellow("Interrupted"));
state.isProcessing = false;
state.inputEditor?.unlock();
} else {
cleanup();
}
});
state.inputEditor.on("close", () => {
cleanup();
});
state.inputEditor.start();
if (hasInitialPrompt) {
state.isProcessing = true;
state.inputEditor.lock();
console.log(chalk.cyan("> ") + options.initialPrompt);
try {
await handleInput(options.initialPrompt!, state, commandHandler);
} catch (error) {
errorMessage(`Error: ${error}`);
}
state.isProcessing = false;
if (state.isRunning && state.inputEditor) {
state.inputEditor.unlock();
}
}
};
export { createInitialState, type ChatState } from "./state.ts";

View File

@@ -0,0 +1,143 @@
/**
* Handle MCP commands in chat
*/
import chalk from "chalk";
import {
initializeMCP,
connectServer,
disconnectServer,
connectAllServers,
disconnectAllServers,
getAllTools,
} from "@services/mcp/index";
import { showMCPStatus } from "@commands/components/chat/mcp/show-mcp-status";
import { appStore } from "@tui-solid/context/app";
/**
* Handle MCP subcommands
*/
export const handleMCP = async (args: string[]): Promise<void> => {
const subcommand = args[0] || "status";
const handlers: Record<string, (args: string[]) => Promise<void>> = {
status: handleStatus,
connect: handleConnect,
disconnect: handleDisconnect,
tools: handleTools,
add: handleAdd,
};
const handler = handlers[subcommand];
if (!handler) {
console.log(chalk.yellow(`Unknown MCP command: ${subcommand}`));
console.log(
chalk.gray("Available: status, connect, disconnect, tools, add"),
);
return;
}
await handler(args.slice(1));
};
/**
* Show MCP status
*/
const handleStatus = async (_args: string[]): Promise<void> => {
await showMCPStatus();
};
/**
* Connect to MCP servers
*/
const handleConnect = async (args: string[]): Promise<void> => {
await initializeMCP();
const name = args[0];
if (name) {
try {
console.log(chalk.gray(`Connecting to ${name}...`));
const instance = await connectServer(name);
console.log(chalk.green(`✓ Connected to ${name}`));
console.log(chalk.gray(` Tools: ${instance.tools.length}`));
} catch (err) {
console.log(chalk.red(`✗ Failed to connect: ${err}`));
}
} else {
console.log(chalk.gray("Connecting to all servers..."));
const results = await connectAllServers();
for (const [serverName, instance] of results) {
if (instance.state === "connected") {
console.log(
chalk.green(`${serverName}: ${instance.tools.length} tools`),
);
} else {
console.log(
chalk.red(`${serverName}: ${instance.error || "Failed"}`),
);
}
}
}
console.log();
};
/**
* Disconnect from MCP servers
*/
const handleDisconnect = async (args: string[]): Promise<void> => {
const name = args[0];
if (name) {
await disconnectServer(name);
console.log(chalk.green(`✓ Disconnected from ${name}`));
} else {
await disconnectAllServers();
console.log(chalk.green("✓ Disconnected from all servers"));
}
console.log();
};
/**
* List available MCP tools
*/
const handleTools = async (_args: string[]): Promise<void> => {
await connectAllServers();
const tools = getAllTools();
if (tools.length === 0) {
console.log(chalk.yellow("\nNo tools available."));
console.log(chalk.gray("Connect to MCP servers first with /mcp connect"));
console.log();
return;
}
console.log(chalk.bold("\nMCP Tools\n"));
// Group by server
const byServer = new Map<string, typeof tools>();
for (const item of tools) {
const existing = byServer.get(item.server) || [];
existing.push(item);
byServer.set(item.server, existing);
}
for (const [server, serverTools] of byServer) {
console.log(chalk.cyan(`${server}:`));
for (const { tool } of serverTools) {
console.log(` ${chalk.white(tool.name)}`);
if (tool.description) {
console.log(` ${chalk.gray(tool.description)}`);
}
}
console.log();
}
};
/**
* Open the MCP add form
*/
const handleAdd = async (_args: string[]): Promise<void> => {
appStore.setMode("mcp_add");
};

View File

@@ -0,0 +1,6 @@
/**
* MCP chat commands
*/
export { showMCPStatus } from "@commands/components/chat/mcp/show-mcp-status";
export { handleMCP } from "@commands/components/chat/mcp/handle-mcp";

View File

@@ -0,0 +1,76 @@
/**
* Show MCP server status in chat
*/
import chalk from "chalk";
import {
initializeMCP,
getServerInstances,
getAllTools,
isMCPAvailable,
} from "@services/mcp/index";
/**
* Display MCP server status
*/
export const showMCPStatus = async (): Promise<void> => {
await initializeMCP();
const hasServers = await isMCPAvailable();
if (!hasServers) {
console.log(chalk.yellow("\nNo MCP servers configured."));
console.log(chalk.gray("Add a server with: codetyper mcp add <name>"));
console.log();
return;
}
const instances = getServerInstances();
const tools = getAllTools();
console.log(chalk.bold("\nMCP Status\n"));
// Server status
console.log(chalk.cyan("Servers:"));
for (const [name, instance] of instances) {
const stateColors: Record<string, (s: string) => string> = {
connected: chalk.green,
connecting: chalk.yellow,
disconnected: chalk.gray,
error: chalk.red,
};
const colorFn = stateColors[instance.state] || chalk.white;
const status = colorFn(instance.state);
const toolCount =
instance.state === "connected" ? ` (${instance.tools.length} tools)` : "";
console.log(` ${chalk.white(name)}: ${status}${chalk.gray(toolCount)}`);
if (instance.error) {
console.log(` ${chalk.red(instance.error)}`);
}
}
// Tool summary
if (tools.length > 0) {
console.log();
console.log(chalk.cyan(`Available Tools: ${chalk.white(tools.length)}`));
// Group by server
const byServer = new Map<string, string[]>();
for (const { server, tool } of tools) {
const existing = byServer.get(server) || [];
existing.push(tool.name);
byServer.set(server, existing);
}
for (const [server, toolNames] of byServer) {
console.log(` ${chalk.gray(server)}: ${toolNames.join(", ")}`);
}
}
console.log();
console.log(chalk.gray("Use /mcp connect to connect servers"));
console.log(chalk.gray("Use /mcp tools for detailed tool info"));
console.log();
};

View File

@@ -0,0 +1,17 @@
import { processFileReferences } from "@commands/components/chat/context/process-file-references";
import { sendMessage } from "@commands/components/chat/messages/send-message";
import type { ChatState } from "@commands/components/chat/state";
export const handleInput = async (
input: string,
state: ChatState,
handleCommand: (command: string, state: ChatState) => Promise<void>,
): Promise<void> => {
if (input.startsWith("/")) {
await handleCommand(input, state);
return;
}
const processedInput = await processFileReferences(input, state.contextFiles);
await sendMessage(processedInput, state);
};

View File

@@ -0,0 +1,228 @@
import chalk from "chalk";
import { basename, extname } from "path";
import { addMessage } from "@services/session";
import { initializePermissions } from "@services/permissions";
import { createAgent } from "@services/agent";
import { infoMessage, errorMessage, warningMessage } from "@utils/terminal";
import { getThinkingMessage } from "@constants/status-messages";
import {
detectDebuggingRequest,
buildDebuggingContext,
getDebuggingPrompt,
} from "@services/debugging-service";
import {
detectCodeReviewRequest,
buildCodeReviewContext,
getCodeReviewPrompt,
} from "@services/code-review-service";
import {
detectRefactoringRequest,
buildRefactoringContext,
getRefactoringPrompt,
} from "@services/refactoring-service";
import {
detectMemoryCommand,
processMemoryCommand,
buildRelevantMemoryPrompt,
} from "@services/memory-service";
import type { ChatState } from "@commands/components/chat/state";
export const sendMessage = async (
content: string,
state: ChatState,
): Promise<void> => {
let userMessage = content;
if (state.contextFiles.size > 0) {
const contextParts: string[] = [];
for (const [path, fileContent] of state.contextFiles) {
const ext = extname(path).slice(1) || "txt";
contextParts.push(
`File: ${basename(path)}\n\`\`\`${ext}\n${fileContent}\n\`\`\``,
);
}
userMessage = contextParts.join("\n\n") + "\n\n" + content;
state.contextFiles.clear();
}
// Detect debugging requests and enhance message with context
const debugContext = detectDebuggingRequest(userMessage);
if (debugContext.isDebugging) {
const debugPrompt = getDebuggingPrompt();
const contextInfo = buildDebuggingContext(debugContext);
// Inject debugging system message before user message if not already present
const hasDebuggingPrompt = state.messages.some(
(msg) => msg.role === "system" && msg.content.includes("debugging mode"),
);
if (!hasDebuggingPrompt) {
state.messages.push({ role: "system", content: debugPrompt });
}
// Append debug context to user message if extracted
if (contextInfo) {
userMessage = userMessage + "\n\n" + contextInfo;
}
if (state.verbose) {
infoMessage(`Debugging mode activated: ${debugContext.debugType}`);
}
}
// Detect code review requests and enhance message with context
const reviewContext = detectCodeReviewRequest(userMessage);
if (reviewContext.isReview) {
const reviewPrompt = getCodeReviewPrompt();
const contextInfo = buildCodeReviewContext(reviewContext);
// Inject code review system message before user message if not already present
const hasReviewPrompt = state.messages.some(
(msg) =>
msg.role === "system" && msg.content.includes("code review mode"),
);
if (!hasReviewPrompt) {
state.messages.push({ role: "system", content: reviewPrompt });
}
// Append review context to user message if extracted
if (contextInfo) {
userMessage = userMessage + "\n\n" + contextInfo;
}
if (state.verbose) {
infoMessage(`Code review mode activated: ${reviewContext.reviewType}`);
}
}
// Detect refactoring requests and enhance message with context
const refactorContext = detectRefactoringRequest(userMessage);
if (refactorContext.isRefactoring) {
const refactorPrompt = getRefactoringPrompt();
const contextInfo = buildRefactoringContext(refactorContext);
// Inject refactoring system message before user message if not already present
const hasRefactoringPrompt = state.messages.some(
(msg) =>
msg.role === "system" && msg.content.includes("refactoring mode"),
);
if (!hasRefactoringPrompt) {
state.messages.push({ role: "system", content: refactorPrompt });
}
// Append refactoring context to user message if extracted
if (contextInfo) {
userMessage = userMessage + "\n\n" + contextInfo;
}
if (state.verbose) {
infoMessage(
`Refactoring mode activated: ${refactorContext.refactoringType}`,
);
}
}
// Detect memory commands
const memoryContext = detectMemoryCommand(userMessage);
if (memoryContext.isMemoryCommand) {
const result = await processMemoryCommand(memoryContext);
console.log(chalk.cyan("\n[Memory System]"));
console.log(
result.success
? chalk.green(result.message)
: chalk.yellow(result.message),
);
// For store/forget commands, still send to agent for confirmation response
if (
memoryContext.commandType === "list" ||
memoryContext.commandType === "query"
) {
// Just display results, don't send to agent
return;
}
if (state.verbose) {
infoMessage(`Memory command: ${memoryContext.commandType}`);
}
}
// Auto-retrieve relevant memories for context
const relevantMemoryPrompt = await buildRelevantMemoryPrompt(userMessage);
if (relevantMemoryPrompt) {
userMessage = userMessage + "\n\n" + relevantMemoryPrompt;
if (state.verbose) {
infoMessage("Relevant memories retrieved");
}
}
state.messages.push({ role: "user", content: userMessage });
await addMessage("user", content);
await initializePermissions();
const agent = createAgent(process.cwd(), {
provider: state.currentProvider,
model: state.currentModel,
verbose: state.verbose,
autoApprove: state.autoApprove,
onToolCall: (call) => {
console.log(chalk.cyan(`\n[Tool: ${call.name}]`));
if (state.verbose) {
console.log(chalk.gray(JSON.stringify(call.arguments, null, 2)));
}
},
onToolResult: (_callId, result) => {
if (result.success) {
console.log(chalk.green(`${result.title}`));
} else {
console.log(chalk.red(`${result.title}: ${result.error}`));
}
},
onText: (_text) => {},
});
process.stdout.write(chalk.gray(getThinkingMessage() + "\n"));
try {
const result = await agent.run(state.messages);
if (result.finalResponse) {
state.messages.push({
role: "assistant",
content: result.finalResponse,
});
await addMessage("assistant", result.finalResponse);
console.log(chalk.bold("\nAssistant:"));
console.log(result.finalResponse);
console.log();
if (result.toolCalls.length > 0) {
const successful = result.toolCalls.filter(
(tc) => tc.result.success,
).length;
infoMessage(
chalk.gray(
`Tools: ${successful}/${result.toolCalls.length} successful, ${result.iterations} iteration(s)`,
),
);
}
} else if (result.toolCalls.length > 0) {
const successful = result.toolCalls.filter(
(tc) => tc.result.success,
).length;
infoMessage(
chalk.gray(
`Completed: ${successful}/${result.toolCalls.length} tools successful`,
),
);
} else {
warningMessage("No response received");
}
} catch (error) {
errorMessage(`Failed: ${error}`);
}
};

View File

@@ -0,0 +1,31 @@
import chalk from "chalk";
import type { Provider as ProviderName } from "@/types/index";
import { getProvider } from "@providers/index.ts";
export const showModels = async (
currentProvider: ProviderName,
currentModel: string | undefined,
): Promise<void> => {
const provider = getProvider(currentProvider);
const models = await provider.getModels();
const activeModel = currentModel || "auto";
const isAutoSelected = activeModel === "auto";
console.log(`\n${chalk.bold(provider.displayName + " Models")}\n`);
// Show "auto" option first
const autoMarker = isAutoSelected ? chalk.cyan("→") : " ";
console.log(
`${autoMarker} ${chalk.cyan("auto")} - Let API choose the best model`,
);
for (const model of models) {
const isCurrent = model.id === activeModel;
const marker = isCurrent ? chalk.cyan("→") : " ";
console.log(`${marker} ${chalk.cyan(model.id)} - ${model.name}`);
}
console.log("\n" + chalk.gray("Use /model <name> to switch"));
console.log();
};

View File

@@ -0,0 +1,7 @@
import { getConfig } from "@services/config";
import { displayProvidersStatus } from "@providers/index.ts";
export const showProviders = async (): Promise<void> => {
const config = await getConfig();
await displayProvidersStatus(config.get("provider"));
};

View File

@@ -0,0 +1,50 @@
import {
infoMessage,
warningMessage,
successMessage,
errorMessage,
} from "@utils/terminal";
import { getConfig } from "@services/config";
import { getProvider } from "@providers/index.ts";
import { showModels } from "./show-models.ts";
import type { ChatState } from "../state.ts";
export const switchModel = async (
modelName: string,
state: ChatState,
): Promise<void> => {
if (!modelName) {
warningMessage("Please specify a model name");
await showModels(state.currentProvider, state.currentModel);
return;
}
// Handle "auto" as a special case
if (modelName.toLowerCase() === "auto") {
state.currentModel = "auto";
successMessage("Switched to model: auto (API will choose)");
const config = await getConfig();
config.set("model", "auto");
await config.save();
return;
}
const provider = getProvider(state.currentProvider);
const models = await provider.getModels();
const model = models.find((m) => m.id === modelName || m.name === modelName);
if (!model) {
errorMessage(`Model not found: ${modelName}`);
infoMessage("Use /models to see available models, or use 'auto'");
return;
}
state.currentModel = model.id;
successMessage(`Switched to model: ${model.name}`);
// Persist model selection to config
const config = await getConfig();
config.set("model", model.id);
await config.save();
};

View File

@@ -0,0 +1,51 @@
import type { Provider as ProviderName } from "@/types/index";
import {
errorMessage,
warningMessage,
infoMessage,
successMessage,
} from "@utils/terminal";
import { getConfig } from "@services/config";
import {
getProvider,
getProviderStatus,
getDefaultModel,
} from "@providers/index.ts";
import type { ChatState } from "../state.ts";
export const switchProvider = async (
providerName: string,
state: ChatState,
): Promise<void> => {
if (!providerName) {
warningMessage("Please specify a provider: copilot, or ollama");
return;
}
const validProviders = ["copilot", "ollama"];
if (!validProviders.includes(providerName)) {
errorMessage(`Invalid provider: ${providerName}`);
infoMessage("Valid providers: " + validProviders.join(", "));
return;
}
const status = await getProviderStatus(providerName as ProviderName);
if (!status.valid) {
errorMessage(`Provider ${providerName} is not configured`);
infoMessage(`Run: codetyper login ${providerName}`);
return;
}
state.currentProvider = providerName as ProviderName;
state.currentModel = undefined;
const config = await getConfig();
config.set("provider", providerName as ProviderName);
await config.save();
const provider = getProvider(state.currentProvider);
const model = getDefaultModel(state.currentProvider);
successMessage(`Switched to ${provider.displayName}`);
infoMessage(`Using model: ${model}`);
};

View File

@@ -0,0 +1,71 @@
import chalk from "chalk";
import { basename, extname } from "path";
import { initializePermissions } from "@services/permissions";
import { createAgent } from "@services/agent";
import type { ChatState } from "@commands/components/chat/state";
import { processFileReferences } from "@commands/components/chat/context/process-file-references";
export const executePrintMode = async (
prompt: string,
state: ChatState,
): Promise<void> => {
const processedPrompt = await processFileReferences(
prompt,
state.contextFiles,
);
let userMessage = processedPrompt;
if (state.contextFiles.size > 0) {
const contextParts: string[] = [];
for (const [path, fileContent] of state.contextFiles) {
const ext = extname(path).slice(1) || "txt";
contextParts.push(
`File: ${basename(path)}\n\`\`\`${ext}\n${fileContent}\n\`\`\``,
);
}
userMessage = contextParts.join("\n\n") + "\n\n" + processedPrompt;
}
state.messages.push({ role: "user", content: userMessage });
await initializePermissions();
const agent = createAgent(process.cwd(), {
provider: state.currentProvider,
model: state.currentModel,
verbose: state.verbose,
autoApprove: state.autoApprove,
onToolCall: (call) => {
console.error(chalk.cyan(`[Tool: ${call.name}]`));
},
onToolResult: (_callId, result) => {
if (result.success) {
console.error(chalk.green(`${result.title}`));
} else {
console.error(chalk.red(`${result.title}: ${result.error}`));
}
},
});
try {
const result = await agent.run(state.messages);
if (result.finalResponse) {
console.log(result.finalResponse);
}
if (state.verbose && result.toolCalls.length > 0) {
const successful = result.toolCalls.filter(
(tc) => tc.result.success,
).length;
console.error(
chalk.gray(
`[Tools: ${successful}/${result.toolCalls.length} successful, ${result.iterations} iteration(s)]`,
),
);
}
} catch (error) {
console.error(chalk.red(`Error: ${error}`));
process.exit(1);
}
};

View File

@@ -0,0 +1,38 @@
import chalk from "chalk";
import { getSessionSummaries } from "@services/session";
import { infoMessage } from "@utils/terminal";
export const listSessions = async (): Promise<void> => {
const summaries = await getSessionSummaries();
if (summaries.length === 0) {
infoMessage("No saved sessions");
return;
}
console.log("\n" + chalk.bold("Saved Sessions:") + "\n");
for (const session of summaries.slice(0, 10)) {
const date = new Date(session.updatedAt).toLocaleDateString();
const time = new Date(session.updatedAt).toLocaleTimeString();
const preview = session.lastMessage
? session.lastMessage.slice(0, 50).replace(/\n/g, " ")
: "(no messages)";
console.log(` ${chalk.cyan(session.id.slice(0, 20))}...`);
console.log(
` ${chalk.gray(`${date} ${time}`)} - ${session.messageCount} messages`,
);
console.log(
` ${chalk.gray(preview)}${preview.length >= 50 ? "..." : ""}`,
);
console.log();
}
if (summaries.length > 10) {
infoMessage(`... and ${summaries.length - 10} more sessions`);
}
console.log(chalk.gray("Resume with: codetyper -r <session-id>"));
console.log();
};

View File

@@ -0,0 +1,20 @@
import type { ChatSession } from "@/types/index";
import type { Message } from "@providers/index.ts";
export const restoreMessagesFromSession = (
session: ChatSession,
systemPrompt: string,
): Message[] => {
const messages: Message[] = [{ role: "system", content: systemPrompt }];
for (const msg of session.messages) {
if (msg.role !== "system") {
messages.push({
role: msg.role as "user" | "assistant",
content: msg.content,
});
}
}
return messages;
};

View File

@@ -0,0 +1,20 @@
import chalk from "chalk";
import { getCurrentSession } from "@services/session";
import { warningMessage } from "@utils/terminal";
export const showSessionInfo = async (): Promise<void> => {
const session = getCurrentSession();
if (!session) {
warningMessage("No active session");
return;
}
console.log("\n" + chalk.bold("Session Information:"));
console.log(` ID: ${chalk.cyan(session.id)}`);
console.log(` Agent: ${session.agent}`);
console.log(` Messages: ${session.messages.length}`);
console.log(` Context files: ${session.contextFiles.length}`);
console.log(` Created: ${new Date(session.createdAt).toLocaleString()}`);
console.log(` Updated: ${new Date(session.updatedAt).toLocaleString()}`);
console.log();
};

View File

@@ -0,0 +1,34 @@
import type { Provider as ProviderName } from "@/types/index";
import type { Message } from "@providers/index";
import type { InputEditorInstance } from "@ui/index";
import { DEFAULT_SYSTEM_PROMPT } from "@prompts/index";
export interface ChatState {
inputEditor: InputEditorInstance | null;
isRunning: boolean;
isProcessing: boolean;
currentProvider: ProviderName;
currentModel: string | undefined;
currentAgent: string | undefined;
messages: Message[];
contextFiles: Map<string, string>;
systemPrompt: string;
verbose: boolean;
autoApprove: boolean;
}
export const createInitialState = (
provider: ProviderName = "copilot",
): ChatState => ({
inputEditor: null,
isRunning: false,
isProcessing: false,
currentProvider: provider,
currentModel: undefined,
currentAgent: "coder",
messages: [],
contextFiles: new Map(),
systemPrompt: DEFAULT_SYSTEM_PROMPT,
verbose: false,
autoApprove: false,
});

View File

@@ -0,0 +1,129 @@
/**
* Show usage statistics command
*/
import chalk from "chalk";
import { usageStore } from "@stores/usage-store";
import { getUserInfo } from "@providers/copilot/credentials";
import { getCopilotUsage } from "@providers/copilot/usage";
import { getProvider } from "@providers/index";
import { renderUsageBar, renderUnlimitedBar } from "@utils/progress-bar";
import type { ChatState } from "@commands/components/chat/state";
import type { CopilotQuotaDetail } from "@/types/copilot-usage";
const formatNumber = (num: number): string => {
return num.toLocaleString();
};
const formatDuration = (ms: number): string => {
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
if (hours > 0) {
return `${hours}h ${minutes % 60}m`;
}
if (minutes > 0) {
return `${minutes}m ${seconds % 60}s`;
}
return `${seconds}s`;
};
const printQuotaBar = (
name: string,
quota: CopilotQuotaDetail | undefined,
resetInfo?: string,
): void => {
if (!quota) {
console.log(chalk.bold(name));
console.log(chalk.gray("N/A"));
console.log();
return;
}
if (quota.unlimited) {
renderUnlimitedBar(name).forEach((line) => console.log(line));
console.log();
return;
}
const used = quota.entitlement - quota.remaining;
renderUsageBar(name, used, quota.entitlement, resetInfo).forEach((line) =>
console.log(line),
);
console.log();
};
export const showUsage = async (state: ChatState): Promise<void> => {
const stats = usageStore.getStats();
const provider = getProvider(state.currentProvider);
const sessionDuration = Date.now() - stats.sessionStartTime;
console.log();
// User info and quota for Copilot
if (state.currentProvider === "copilot") {
const userInfo = await getUserInfo();
const copilotUsage = await getCopilotUsage();
if (copilotUsage) {
const resetDate = copilotUsage.quota_reset_date;
printQuotaBar(
"Premium Requests",
copilotUsage.quota_snapshots.premium_interactions,
`Resets ${resetDate}`,
);
printQuotaBar(
"Chat",
copilotUsage.quota_snapshots.chat,
`Resets ${resetDate}`,
);
printQuotaBar(
"Completions",
copilotUsage.quota_snapshots.completions,
`Resets ${resetDate}`,
);
}
console.log(chalk.bold("Account"));
console.log(`${chalk.gray("Provider:")} ${provider.displayName}`);
console.log(`${chalk.gray("Model:")} ${state.currentModel ?? "auto"}`);
if (userInfo) {
console.log(`${chalk.gray("User:")} ${userInfo.login}`);
}
if (copilotUsage) {
console.log(`${chalk.gray("Plan:")} ${copilotUsage.copilot_plan}`);
}
console.log();
} else {
console.log(chalk.bold("Provider"));
console.log(`${chalk.gray("Name:")} ${provider.displayName}`);
console.log(`${chalk.gray("Model:")} ${state.currentModel ?? "auto"}`);
console.log();
}
// Session stats with bar
console.log(chalk.bold("Current Session"));
renderUsageBar(
"Tokens",
stats.totalTokens,
stats.totalTokens || 1,
`${formatNumber(stats.promptTokens)} prompt + ${formatNumber(stats.completionTokens)} completion`,
)
.slice(1)
.forEach((line) => console.log(line));
console.log(`${chalk.gray("Requests:")} ${formatNumber(stats.requestCount)}`);
console.log(`${chalk.gray("Duration:")} ${formatDuration(sessionDuration)}`);
if (stats.requestCount > 0) {
const avgTokensPerRequest = Math.round(
stats.totalTokens / stats.requestCount,
);
console.log(
`${chalk.gray("Avg tokens/request:")} ${formatNumber(avgTokensPerRequest)}`,
);
}
console.log();
};

View File

@@ -0,0 +1,25 @@
/**
* Dashboard Config Builder
*/
import os from "os";
import { getConfig } from "@services/config";
import { DASHBOARD_TITLE } from "@constants/dashboard";
import type { DashboardConfig } from "@/types/dashboard";
export const buildDashboardConfig = async (
version: string,
): Promise<DashboardConfig> => {
const configMgr = await getConfig();
const username = os.userInfo().username;
const cwd = process.cwd();
const provider = configMgr.get("provider") as string;
return {
title: DASHBOARD_TITLE,
version,
user: username,
cwd,
provider,
};
};

View File

@@ -0,0 +1,33 @@
/**
* Dashboard Display
*
* Renders and displays the main dashboard UI.
*/
import { DASHBOARD_LAYOUT } from "@constants/dashboard";
import { buildDashboardConfig } from "@commands/components/dashboard/build-config";
import { renderHeader } from "@commands/components/dashboard/render-header";
import { renderContent } from "@commands/components/dashboard/render-content";
import { renderFooter } from "@commands/components/dashboard/render-footer";
const getTerminalWidth = (): number => {
return process.stdout.columns || DASHBOARD_LAYOUT.DEFAULT_WIDTH;
};
const renderDashboard = async (version: string): Promise<string> => {
const config = await buildDashboardConfig(version);
const width = getTerminalWidth();
const header = renderHeader(config, width);
const content = renderContent(config, width, DASHBOARD_LAYOUT.CONTENT_HEIGHT);
const footer = renderFooter(width);
return [header, content, footer].join("\n");
};
export const displayDashboard = async (version: string): Promise<void> => {
console.clear();
const dashboard = await renderDashboard(version);
console.log(dashboard);
process.exit(0);
};

View File

@@ -0,0 +1,41 @@
/**
* Dashboard Content Renderer
*/
import { DASHBOARD_LAYOUT, DASHBOARD_BORDER } from "@constants/dashboard";
import { renderLeftContent } from "@commands/components/dashboard/render-left-content";
import { renderRightContent } from "@commands/components/dashboard/render-right-content";
import type { DashboardConfig } from "@/types/dashboard";
const padContent = (content: string[], height: number): string[] => {
const padded = [...content];
while (padded.length < height) {
padded.push("");
}
return padded;
};
export const renderContent = (
config: DashboardConfig,
width: number,
height: number,
): string => {
const dividerPos = Math.floor(width * DASHBOARD_LAYOUT.LEFT_COLUMN_RATIO);
const leftWidth = dividerPos - DASHBOARD_LAYOUT.PADDING;
const rightWidth = width - dividerPos - DASHBOARD_LAYOUT.PADDING;
const leftContent = padContent(renderLeftContent(config), height);
const rightContent = padContent(renderRightContent(), height);
const lines: string[] = [];
for (let i = 0; i < height; i++) {
const left = (leftContent[i] || "").padEnd(leftWidth);
const right = (rightContent[i] || "").padEnd(rightWidth);
lines.push(
`${DASHBOARD_BORDER.VERTICAL} ${left} ${DASHBOARD_BORDER.VERTICAL} ${right} ${DASHBOARD_BORDER.VERTICAL}`,
);
}
return lines.join("\n");
};

View File

@@ -0,0 +1,31 @@
/**
* Dashboard Footer Renderer
*/
import chalk from "chalk";
import {
DASHBOARD_BORDER,
DASHBOARD_QUICK_COMMANDS,
} from "@constants/dashboard";
export const renderFooter = (width: number): string => {
const dashCount = Math.max(0, width - 2);
const dashes = DASHBOARD_BORDER.HORIZONTAL.repeat(dashCount);
const borderLine = `${DASHBOARD_BORDER.BOTTOM_LEFT}${dashes}${DASHBOARD_BORDER.BOTTOM_RIGHT}`;
const commandLines = DASHBOARD_QUICK_COMMANDS.map(
({ command, description }) =>
` ${chalk.cyan(command.padEnd(18))} ${description}`,
);
const lines = [
borderLine,
"",
chalk.dim("Quick Commands:"),
...commandLines,
"",
chalk.dim("Press Ctrl+C to exit • Type 'codetyper chat' to begin"),
];
return lines.join("\n");
};

View File

@@ -0,0 +1,18 @@
/**
* Dashboard Header Renderer
*/
import chalk from "chalk";
import { DASHBOARD_BORDER } from "@constants/dashboard";
import type { DashboardConfig } from "@/types/dashboard";
export const renderHeader = (
config: DashboardConfig,
width: number,
): string => {
const title = ` ${config.title} ${config.version} `;
const dashCount = Math.max(0, width - title.length - 2);
const dashes = DASHBOARD_BORDER.HORIZONTAL.repeat(dashCount);
return `${DASHBOARD_BORDER.TOP_LEFT}${DASHBOARD_BORDER.HORIZONTAL}${DASHBOARD_BORDER.HORIZONTAL}${DASHBOARD_BORDER.HORIZONTAL} ${chalk.cyan.bold(title)}${dashes}${DASHBOARD_BORDER.TOP_RIGHT}`;
};

View File

@@ -0,0 +1,24 @@
/**
* Dashboard Left Content Renderer
*/
import chalk from "chalk";
import { DASHBOARD_LOGO } from "@constants/dashboard";
import type { DashboardConfig } from "@/types/dashboard";
export const renderLeftContent = (config: DashboardConfig): string[] => {
const lines: string[] = [];
lines.push("");
lines.push(chalk.green(`Welcome back ${config.user}!`));
lines.push("");
const coloredLogo = DASHBOARD_LOGO.map((line) => chalk.cyan.bold(line));
lines.push(...coloredLogo);
lines.push("");
lines.push(chalk.cyan.bold(`${config.provider.toUpperCase()}`));
lines.push(chalk.dim(`${config.user}@codetyper`));
return lines;
};

View File

@@ -0,0 +1,21 @@
/**
* Dashboard Right Content Renderer
*/
import chalk from "chalk";
import { DASHBOARD_COMMANDS } from "@constants/dashboard";
export const renderRightContent = (): string[] => {
const lines: string[] = [];
lines.push(chalk.bold("Ready to code"));
lines.push("");
for (const { command, description } of DASHBOARD_COMMANDS) {
lines.push(chalk.cyan(command));
lines.push(` ${description}`);
lines.push("");
}
return lines;
};

View File

@@ -0,0 +1,55 @@
import { tui } from "@tui-solid/index";
import { getProviderInfo } from "@services/chat-tui-service";
import type { ChatServiceState } from "@services/chat-tui-service";
import type { AgentConfig } from "@/types/agent-config";
import type { PermissionScope, LearningScope } from "@/types/tui";
export interface RenderAppSolidProps {
sessionId: string;
handleSubmit: (message: string) => Promise<void>;
handleCommand: (command: string) => Promise<void>;
handleModelSelect: (model: string) => Promise<void>;
handleAgentSelect: (agentId: string, agent: AgentConfig) => Promise<void>;
handleThemeSelect: (theme: string) => void;
handlePermissionResponse?: (
allowed: boolean,
scope?: PermissionScope,
) => void;
handleLearningResponse?: (
save: boolean,
scope?: LearningScope,
editedContent?: string,
) => void;
handleExit: () => void;
showBanner: boolean;
state: ChatServiceState;
plan?: {
id: string;
title: string;
items: Array<{ id: string; text: string; completed: boolean }>;
} | null;
}
export const renderAppSolid = async (
props: RenderAppSolidProps,
): Promise<void> => {
const { displayName, model: defaultModel } = getProviderInfo(
props.state.provider,
);
const currentModel = props.state.model ?? defaultModel;
await tui({
sessionId: props.sessionId,
provider: displayName,
model: currentModel,
onSubmit: props.handleSubmit,
onCommand: props.handleCommand,
onModelSelect: props.handleModelSelect,
onThemeSelect: props.handleThemeSelect,
onPermissionResponse: props.handlePermissionResponse ?? (() => {}),
onLearningResponse: props.handleLearningResponse ?? (() => {}),
plan: props.plan,
});
props.handleExit();
};

View File

@@ -0,0 +1,104 @@
import { tui, appStore } from "@tui/index";
import { getProviderInfo } from "@services/chat-tui-service";
import { addServer, connectServer } from "@services/mcp/index";
import type { ChatServiceState } from "@services/chat-tui-service";
import type { AgentConfig } from "@/types/agent-config";
import type { PermissionScope, LearningScope } from "@/types/tui";
import type { ProviderModel } from "@/types/providers";
import type { MCPAddFormData } from "@/types/mcp";
interface AgentOption {
id: string;
name: string;
description?: string;
}
export interface RenderAppProps {
sessionId?: string;
handleSubmit: (message: string) => Promise<void>;
handleCommand: (command: string) => Promise<void>;
handleModelSelect: (model: string) => Promise<void>;
handleAgentSelect: (agentId: string, agent: AgentConfig) => Promise<void>;
handleThemeSelect: (theme: string) => void;
handleProviderSelect?: (providerId: string) => Promise<void>;
handleCascadeToggle?: (enabled: boolean) => Promise<void>;
handleMCPAdd?: (data: MCPAddFormData) => Promise<void>;
handlePermissionResponse?: (
allowed: boolean,
scope?: PermissionScope,
) => void;
handleLearningResponse?: (
save: boolean,
scope?: LearningScope,
editedContent?: string,
) => void;
handleExit: () => void;
showBanner: boolean;
state: ChatServiceState;
availableModels?: ProviderModel[];
agents?: AgentOption[];
initialPrompt?: string;
theme?: string;
cascadeEnabled?: boolean;
plan?: {
id: string;
title: string;
items: Array<{ id: string; text: string; completed: boolean }>;
} | null;
}
const defaultHandleMCPAdd = async (data: MCPAddFormData): Promise<void> => {
const serverArgs = data.args.trim()
? data.args.trim().split(/\s+/)
: undefined;
await addServer(
data.name,
{
command: data.command,
args: serverArgs,
enabled: true,
},
data.isGlobal,
);
await connectServer(data.name);
};
export const renderApp = async (props: RenderAppProps): Promise<void> => {
const { displayName, model: defaultModel } = getProviderInfo(
props.state.provider,
);
const currentModel = props.state.model ?? defaultModel;
await tui({
sessionId: props.sessionId,
provider: displayName,
model: currentModel,
theme: props.theme,
cascadeEnabled: props.cascadeEnabled,
availableModels: props.availableModels,
agents: props.agents,
initialPrompt: props.initialPrompt,
onSubmit: props.handleSubmit,
onCommand: props.handleCommand,
onModelSelect: props.handleModelSelect,
onThemeSelect: props.handleThemeSelect,
onProviderSelect: props.handleProviderSelect,
onCascadeToggle: props.handleCascadeToggle,
onAgentSelect: async (agentId: string) => {
const agent = props.agents?.find((a) => a.id === agentId);
if (agent) {
await props.handleAgentSelect(agentId, agent as AgentConfig);
}
},
onMCPAdd: props.handleMCPAdd ?? defaultHandleMCPAdd,
onPermissionResponse: props.handlePermissionResponse ?? (() => {}),
onLearningResponse: props.handleLearningResponse ?? (() => {}),
plan: props.plan,
});
props.handleExit();
};
export { appStore };

View File

@@ -0,0 +1,195 @@
import { renderApp, appStore } from "@commands/components/execute/execute";
import type { RenderAppProps } from "@commands/components/execute/execute";
import {
initializeChatService,
loadModels,
handleModelSelect as serviceHandleModelSelect,
executePrintMode,
setupPermissionHandler,
cleanupPermissionHandler,
executeCommand,
handleMessage,
} from "@services/chat-tui-service";
import type { ChatServiceState } from "@services/chat-tui-service";
import type { ChatTUIOptions } from "@interfaces/ChatTUIOptions";
import type { AgentConfig } from "@/types/agent-config";
import { getConfig } from "@services/config";
import { getThinkingMessage } from "@constants/status-messages";
import {
enterFullscreen,
registerExitHandlers,
exitFullscreen,
clearScreen,
} from "@utils/terminal";
import { createCallbacks } from "@commands/chat-tui";
import { agentLoader } from "@services/agent-loader";
interface ExecuteContext {
state: ChatServiceState | null;
}
const createHandleExit = (): (() => void) => (): void => {
cleanupPermissionHandler();
exitFullscreen();
clearScreen();
console.log("Goodbye!");
process.exit(0);
};
const createHandleModelSelect =
(ctx: ExecuteContext) =>
async (model: string): Promise<void> => {
if (!ctx.state) return;
await serviceHandleModelSelect(ctx.state, model, createCallbacks());
};
const createHandleAgentSelect =
(ctx: ExecuteContext) =>
async (agentId: string, agent: AgentConfig): Promise<void> => {
if (!ctx.state) return;
(ctx.state as ChatServiceState & { currentAgent?: string }).currentAgent =
agentId;
if (agent.prompt) {
const basePrompt = ctx.state.systemPrompt;
ctx.state.systemPrompt = `${agent.prompt}\n\n${basePrompt}`;
if (
ctx.state.messages.length > 0 &&
ctx.state.messages[0].role === "system"
) {
ctx.state.messages[0].content = ctx.state.systemPrompt;
}
}
};
const createHandleThemeSelect =
() =>
(themeName: string): void => {
getConfig().then((config) => {
config.set("theme", themeName);
config.save();
});
};
const createHandleProviderSelect =
(ctx: ExecuteContext) =>
async (providerId: string): Promise<void> => {
if (!ctx.state) return;
ctx.state.provider = providerId as "copilot" | "ollama";
const config = await getConfig();
config.set("provider", providerId as "copilot" | "ollama");
await config.save();
};
const createHandleCascadeToggle =
() =>
async (enabled: boolean): Promise<void> => {
const config = await getConfig();
config.set("cascadeEnabled", enabled);
await config.save();
};
const createHandleCommand =
(ctx: ExecuteContext, handleExit: () => void) =>
async (command: string): Promise<void> => {
if (!ctx.state) return;
if (["exit", "quit", "q"].includes(command.toLowerCase())) {
handleExit();
return;
}
await executeCommand(ctx.state, command, createCallbacks());
};
const createHandleSubmit =
(ctx: ExecuteContext, handleCommand: (command: string) => Promise<void>) =>
async (message: string): Promise<void> => {
if (!ctx.state) return;
if (message.startsWith("/")) {
const [command] = message.slice(1).split(/\s+/);
await handleCommand(command);
return;
}
// Set initial thinking message (streaming will update this)
appStore.setThinkingMessage(getThinkingMessage());
try {
await handleMessage(ctx.state, message, createCallbacks());
} finally {
// Clean up any remaining state after message handling
appStore.setThinkingMessage(null);
appStore.setCurrentToolCall(null);
appStore.setMode("idle");
}
};
const execute = async (options: ChatTUIOptions): Promise<void> => {
const ctx: ExecuteContext = {
state: null,
};
const { state, session } = await initializeChatService(options);
ctx.state = state;
if (options.printMode && options.initialPrompt) {
await executePrintMode(state, options.initialPrompt);
return;
}
setupPermissionHandler();
const models = await loadModels(state.provider);
const agents = await agentLoader.getAvailableAgents(process.cwd());
const config = await getConfig();
const savedTheme = config.get("theme");
// Register exit handlers to ensure terminal cleanup on abrupt termination
registerExitHandlers();
enterFullscreen();
const handleExit = createHandleExit();
const handleModelSelectFn = createHandleModelSelect(ctx);
const handleAgentSelectFn = createHandleAgentSelect(ctx);
const handleThemeSelectFn = createHandleThemeSelect();
const handleProviderSelectFn = createHandleProviderSelect(ctx);
const handleCascadeToggleFn = createHandleCascadeToggle();
const handleCommand = createHandleCommand(ctx, handleExit);
const handleSubmit = createHandleSubmit(ctx, handleCommand);
// Only pass sessionId if resuming/continuing - otherwise show Home view first
const isResuming = options.continueSession || options.resumeSession;
const savedCascadeEnabled = config.get("cascadeEnabled");
const renderProps: RenderAppProps = {
sessionId: isResuming ? session.id : undefined,
handleSubmit,
handleCommand,
handleModelSelect: handleModelSelectFn,
handleAgentSelect: handleAgentSelectFn,
handleThemeSelect: handleThemeSelectFn,
handleProviderSelect: handleProviderSelectFn,
handleCascadeToggle: handleCascadeToggleFn,
handleExit,
showBanner: true,
state,
availableModels: models,
agents: agents.map((a) => ({
id: a.id,
name: a.name,
description: a.description,
})),
initialPrompt: options.initialPrompt,
theme: savedTheme,
cascadeEnabled: savedCascadeEnabled ?? true,
};
await renderApp(renderProps);
};
export default execute;

View File

@@ -0,0 +1,8 @@
/**
* Dashboard Command
*
* Re-exports the modular dashboard implementation.
*/
export { displayDashboard } from "@commands/components/dashboard/display";
export type { DashboardConfig } from "@/types/dashboard";

25
src/commands/handlers.ts Normal file
View File

@@ -0,0 +1,25 @@
/**
* Command handlers - Route commands to appropriate implementations
*/
import { errorMessage } from "@utils/terminal";
import { COMMAND_REGISTRY, isValidCommand } from "@commands/handlers/registry";
import type { CommandOptions } from "@/types/index";
export const handleCommand = async (
command: string,
options: CommandOptions,
): Promise<void> => {
try {
if (!isValidCommand(command)) {
errorMessage(`Unknown command: ${command}`);
process.exit(1);
}
const handler = COMMAND_REGISTRY[command];
await handler(options);
} catch (error) {
errorMessage(`Command failed: ${error}`);
throw error;
}
};

View File

@@ -0,0 +1,10 @@
/**
* Chat command handler
*/
import { execute as executeChat } from "@commands/chat";
import type { CommandOptions } from "@/types/index";
export const handleChat = async (options: CommandOptions): Promise<void> => {
await executeChat(options);
};

View File

@@ -0,0 +1,123 @@
/**
* Classify command handler
*/
import chalk from "chalk";
import {
succeedSpinner,
startSpinner,
errorMessage,
failSpinner,
headerMessage,
} from "@utils/terminal";
import {
INTENT_KEYWORDS,
CLASSIFICATION_CONFIDENCE,
} from "@constants/handlers";
import type {
CommandOptions,
IntentRequest,
IntentResponse,
} from "@/types/index";
const classifyIntent = async (
request: IntentRequest,
): Promise<IntentResponse> => {
await new Promise((resolve) => setTimeout(resolve, 1000));
const prompt = request.prompt.toLowerCase();
let intent: IntentResponse["intent"] = "ask";
let confidence: number = CLASSIFICATION_CONFIDENCE.DEFAULT;
const intentMatchers: Record<string, () => void> = {
fix: () => {
intent = "fix";
confidence = CLASSIFICATION_CONFIDENCE.HIGH;
},
test: () => {
intent = "test";
confidence = CLASSIFICATION_CONFIDENCE.MEDIUM;
},
refactor: () => {
intent = "refactor";
confidence = CLASSIFICATION_CONFIDENCE.LOW;
},
code: () => {
intent = "code";
confidence = CLASSIFICATION_CONFIDENCE.DEFAULT;
},
document: () => {
intent = "document";
confidence = CLASSIFICATION_CONFIDENCE.HIGH;
},
};
for (const [intentKey, keywords] of Object.entries(INTENT_KEYWORDS)) {
const hasMatch = keywords.some((keyword) => prompt.includes(keyword));
if (hasMatch) {
intentMatchers[intentKey]?.();
break;
}
}
return {
intent,
confidence,
reasoning: `Based on keywords in the prompt, this appears to be a ${intent} request.`,
needsClarification: confidence < CLASSIFICATION_CONFIDENCE.THRESHOLD,
clarificationQuestions:
confidence < CLASSIFICATION_CONFIDENCE.THRESHOLD
? [
"Which specific files should I focus on?",
"What is the expected outcome?",
]
: undefined,
};
};
export const handleClassify = async (
options: CommandOptions,
): Promise<void> => {
const { prompt, context, files = [] } = options;
if (!prompt) {
errorMessage("Prompt is required");
return;
}
headerMessage("Classifying Intent");
console.log(chalk.bold("Prompt:") + ` ${prompt}`);
if (context) {
console.log(chalk.bold("Context:") + ` ${context}`);
}
if (files.length > 0) {
console.log(chalk.bold("Files:") + ` ${files.join(", ")}`);
}
console.log();
startSpinner("Analyzing prompt...");
try {
const result = await classifyIntent({ prompt, context, files });
succeedSpinner("Analysis complete");
console.log();
console.log(chalk.bold("Intent:") + ` ${chalk.cyan(result.intent)}`);
console.log(
chalk.bold("Confidence:") +
` ${chalk.green((result.confidence * 100).toFixed(1) + "%")}`,
);
console.log(chalk.bold("Reasoning:") + ` ${result.reasoning}`);
if (result.needsClarification && result.clarificationQuestions) {
console.log();
console.log(chalk.yellow.bold("Clarification needed:"));
result.clarificationQuestions.forEach((q, i) => {
console.log(` ${i + 1}. ${q}`);
});
}
} catch (error) {
failSpinner("Classification failed");
throw error;
}
};

View File

@@ -0,0 +1,113 @@
/**
* Config command handler
*/
import {
errorMessage,
filePath,
successMessage,
hightLigthedJson,
headerMessage,
infoMessage,
} from "@utils/terminal";
import { getConfig } from "@services/config";
import {
VALID_CONFIG_KEYS,
VALID_PROVIDERS,
CONFIG_VALIDATION,
} from "@constants/handlers";
import type { CommandOptions, Provider } from "@/types/index";
import type { ConfigAction, ConfigKey } from "@/types/handlers";
type ConfigActionHandler = (key?: string, value?: string) => Promise<void>;
const showConfig = async (): Promise<void> => {
const config = await getConfig();
headerMessage("Configuration");
const allConfig = config.getAll();
hightLigthedJson(allConfig);
};
const showPath = async (): Promise<void> => {
const config = await getConfig();
const configPath = config.getConfigPath();
console.log(filePath(configPath));
};
const setConfigValue = async (key?: string, value?: string): Promise<void> => {
if (!key || value === undefined) {
errorMessage("Key and value are required");
return;
}
if (!VALID_CONFIG_KEYS.includes(key as ConfigKey)) {
errorMessage(`Invalid config key: ${key}`);
infoMessage(`Valid keys: ${VALID_CONFIG_KEYS.join(", ")}`);
return;
}
const config = await getConfig();
const keySetters: Record<ConfigKey, () => boolean> = {
provider: () => {
if (!VALID_PROVIDERS.includes(value as Provider)) {
errorMessage(`Invalid provider: ${value}`);
infoMessage(`Valid providers: ${VALID_PROVIDERS.join(", ")}`);
return false;
}
config.set("provider", value as Provider);
return true;
},
model: () => {
config.set("model", value);
return true;
},
maxIterations: () => {
const num = parseInt(value, 10);
if (isNaN(num) || num < CONFIG_VALIDATION.MIN_ITERATIONS) {
errorMessage("maxIterations must be a positive number");
return false;
}
config.set("maxIterations", num);
return true;
},
timeout: () => {
const num = parseInt(value, 10);
if (isNaN(num) || num < CONFIG_VALIDATION.MIN_TIMEOUT_MS) {
errorMessage(
`timeout must be at least ${CONFIG_VALIDATION.MIN_TIMEOUT_MS}ms`,
);
return false;
}
config.set("timeout", num);
return true;
},
};
const setter = keySetters[key as ConfigKey];
const success = setter();
if (success) {
await config.save();
successMessage(`Set ${key} = ${value}`);
}
};
const CONFIG_ACTION_HANDLERS: Record<ConfigAction, ConfigActionHandler> = {
show: showConfig,
path: showPath,
set: setConfigValue,
};
export const handleConfig = async (options: CommandOptions): Promise<void> => {
const { action, key, value } = options;
const handler = CONFIG_ACTION_HANDLERS[action as ConfigAction];
if (!handler) {
errorMessage(`Unknown config action: ${action}`);
return;
}
await handler(key, value);
};

View File

@@ -0,0 +1,62 @@
/**
* Plan command handler
*/
import chalk from "chalk";
import {
hightLigthedJson,
filePath,
errorMessage,
failSpinner,
headerMessage,
startSpinner,
succeedSpinner,
successMessage,
} from "@utils/terminal";
import type { CommandOptions } from "@/types/index";
export const handlePlan = async (options: CommandOptions): Promise<void> => {
const { intent, task, files = [], output } = options;
if (!task) {
errorMessage("Task description is required");
return;
}
headerMessage("Generating Plan");
console.log(chalk.bold("Intent:") + ` ${chalk.cyan(intent || "unknown")}`);
console.log(chalk.bold("Task:") + ` ${task}`);
if (files.length > 0) {
console.log(chalk.bold("Files:") + ` ${files.join(", ")}`);
}
console.log();
startSpinner("Generating execution plan...");
try {
await new Promise((resolve) => setTimeout(resolve, 1500));
succeedSpinner("Plan generated");
const plan = {
intent,
task,
files,
steps: [
{ id: "step_1", type: "read", description: "Analyze existing code" },
{ id: "step_2", type: "edit", description: "Apply changes" },
{ id: "step_3", type: "execute", description: "Run tests" },
],
};
if (output) {
const fs = await import("fs/promises");
await fs.writeFile(output, JSON.stringify(plan, null, 2));
successMessage(`Plan saved to ${filePath(output)}`);
} else {
hightLigthedJson(plan);
}
} catch (error) {
failSpinner("Plan generation failed");
throw error;
}
};

View File

@@ -0,0 +1,28 @@
/**
* Command handler registry - object-based routing
*/
import { handleChat } from "@commands/handlers/chat";
import { handleRun } from "@commands/handlers/run";
import { handleClassify } from "@commands/handlers/classify";
import { handlePlan } from "@commands/handlers/plan";
import { handleValidate } from "@commands/handlers/validate";
import { handleConfig } from "@commands/handlers/config";
import { handleServe } from "@commands/handlers/serve";
import type { CommandRegistry } from "@/types/handlers";
export const COMMAND_REGISTRY: CommandRegistry = {
chat: handleChat,
run: handleRun,
classify: handleClassify,
plan: handlePlan,
validate: handleValidate,
config: handleConfig,
serve: handleServe,
};
export const isValidCommand = (
command: string,
): command is keyof CommandRegistry => {
return command in COMMAND_REGISTRY;
};

View File

@@ -0,0 +1,10 @@
/**
* Run command handler
*/
import { execute } from "@commands/runner";
import type { CommandOptions } from "@/types/index";
export const handleRun = async (options: CommandOptions): Promise<void> => {
await execute(options);
};

View File

@@ -0,0 +1,15 @@
/**
* Serve command handler
*/
import { boxMessage, warningMessage, infoMessage } from "@utils/terminal";
import type { CommandOptions } from "@/types/index";
import { SERVER_INFO } from "@constants/serve";
export const handleServe = async (_options: CommandOptions): Promise<void> => {
boxMessage(SERVER_INFO, "Server Mode");
warningMessage("Server mode not yet implemented");
infoMessage(
"This will integrate with the existing agent/main.py JSON-RPC server",
);
};

View File

@@ -0,0 +1,78 @@
/**
* Validate command handler
*/
import chalk from "chalk";
import {
failSpinner,
warningMessage,
successMessage,
succeedSpinner,
startSpinner,
errorMessage,
headerMessage,
filePath,
} from "@utils/terminal";
import { getConfig } from "@services/config";
import type { CommandOptions } from "@/types/index";
export const handleValidate = async (
options: CommandOptions,
): Promise<void> => {
const { planFile } = options;
if (!planFile) {
errorMessage("Plan file is required");
return;
}
headerMessage("Validating Plan");
console.log(chalk.bold("Plan file:") + ` ${filePath(planFile)}`);
console.log();
startSpinner("Validating plan...");
try {
const fs = await import("fs/promises");
const planData = await fs.readFile(planFile, "utf-8");
const plan = JSON.parse(planData);
await new Promise((resolve) => setTimeout(resolve, 1000));
const config = await getConfig();
const warnings: string[] = [];
const errors: string[] = [];
plan.files?.forEach((file: string) => {
if (config.isProtectedPath(file)) {
warnings.push(`Protected path: ${file}`);
}
});
succeedSpinner("Validation complete");
console.log();
if (errors.length > 0) {
console.log(chalk.red.bold("Errors:"));
errors.forEach((err) => console.log(` - ${err}`));
}
if (warnings.length > 0) {
console.log(chalk.yellow.bold("Warnings:"));
warnings.forEach((warn) => console.log(` - ${warn}`));
}
if (errors.length === 0 && warnings.length === 0) {
successMessage("Plan is valid and safe to execute");
} else if (errors.length > 0) {
errorMessage("Plan has errors and cannot be executed");
process.exit(1);
} else {
warningMessage("Plan has warnings - proceed with caution");
}
} catch (error) {
failSpinner("Validation failed");
throw error;
}
};

319
src/commands/mcp.ts Normal file
View File

@@ -0,0 +1,319 @@
/**
* MCP Command - Manage MCP servers
*
* Usage:
* codetyper mcp list - List configured servers
* codetyper mcp add <name> - Add a new server
* codetyper mcp remove <name> - Remove a server
* codetyper mcp connect [name] - Connect to server(s)
* codetyper mcp disconnect [name] - Disconnect from server(s)
* codetyper mcp status - Show connection status
* codetyper mcp tools - List available tools
*/
import chalk from "chalk";
import { errorMessage, infoMessage, successMessage } from "@utils/terminal";
import {
initializeMCP,
getMCPConfig,
addServer,
removeServer,
connectServer,
disconnectServer,
connectAllServers,
disconnectAllServers,
getServerInstances,
getAllTools,
} from "@services/mcp/index";
/**
* MCP command handler
*/
export const mcpCommand = async (args: string[]): Promise<void> => {
const subcommand = args[0] || "status";
const handlers: Record<string, (args: string[]) => Promise<void>> = {
list: handleList,
add: handleAdd,
remove: handleRemove,
connect: handleConnect,
disconnect: handleDisconnect,
status: handleStatus,
tools: handleTools,
help: handleHelp,
};
const handler = handlers[subcommand];
if (!handler) {
errorMessage(`Unknown subcommand: ${subcommand}`);
await handleHelp([]);
return;
}
await handler(args.slice(1));
};
/**
* List configured servers
*/
const handleList = async (_args: string[]): Promise<void> => {
await initializeMCP();
const config = await getMCPConfig();
const servers = Object.entries(config.servers);
if (servers.length === 0) {
infoMessage("No MCP servers configured.");
infoMessage("Add a server with: codetyper mcp add <name>");
return;
}
console.log(chalk.bold("\nConfigured MCP Servers:\n"));
for (const [name, server] of servers) {
const enabled =
server.enabled !== false ? chalk.green("✓") : chalk.gray("○");
console.log(` ${enabled} ${chalk.cyan(name)}`);
console.log(
` Command: ${server.command} ${(server.args || []).join(" ")}`,
);
if (server.transport && server.transport !== "stdio") {
console.log(` Transport: ${server.transport}`);
}
console.log();
}
};
/**
* Add a new server
*/
const handleAdd = async (args: string[]): Promise<void> => {
const name = args[0];
if (!name) {
errorMessage("Server name required");
infoMessage(
"Usage: codetyper mcp add <name> --command <cmd> [--args <args>]",
);
return;
}
// Parse options
let command = "";
const serverArgs: string[] = [];
let isGlobal = false;
for (let i = 1; i < args.length; i++) {
const arg = args[i];
if (arg === "--command" || arg === "-c") {
command = args[++i] || "";
} else if (arg === "--args" || arg === "-a") {
// Collect remaining args
while (args[i + 1] && !args[i + 1].startsWith("--")) {
serverArgs.push(args[++i]);
}
} else if (arg === "--global" || arg === "-g") {
isGlobal = true;
}
}
if (!command) {
// Interactive mode - ask for command
infoMessage("Adding MCP server interactively...");
infoMessage("Example: npx @modelcontextprotocol/server-sqlite");
// For now, require command flag
errorMessage("Command required. Use --command <cmd>");
return;
}
try {
await addServer(
name,
{
command,
args: serverArgs.length > 0 ? serverArgs : undefined,
enabled: true,
},
isGlobal,
);
successMessage(`Added MCP server: ${name}`);
infoMessage(`Connect with: codetyper mcp connect ${name}`);
} catch (err) {
errorMessage(`Failed to add server: ${err}`);
}
};
/**
* Remove a server
*/
const handleRemove = async (args: string[]): Promise<void> => {
const name = args[0];
if (!name) {
errorMessage("Server name required");
return;
}
const isGlobal = args.includes("--global") || args.includes("-g");
try {
await removeServer(name, isGlobal);
successMessage(`Removed MCP server: ${name}`);
} catch (err) {
errorMessage(`Failed to remove server: ${err}`);
}
};
/**
* Connect to server(s)
*/
const handleConnect = async (args: string[]): Promise<void> => {
const name = args[0];
if (name) {
// Connect to specific server
try {
infoMessage(`Connecting to ${name}...`);
const instance = await connectServer(name);
successMessage(`Connected to ${name}`);
console.log(` Tools: ${instance.tools.length}`);
console.log(` Resources: ${instance.resources.length}`);
} catch (err) {
errorMessage(`Failed to connect: ${err}`);
}
} else {
// Connect to all servers
infoMessage("Connecting to all servers...");
const results = await connectAllServers();
for (const [serverName, instance] of results) {
if (instance.state === "connected") {
successMessage(
`${serverName}: Connected (${instance.tools.length} tools)`,
);
} else {
errorMessage(`${serverName}: ${instance.error || "Failed"}`);
}
}
}
};
/**
* Disconnect from server(s)
*/
const handleDisconnect = async (args: string[]): Promise<void> => {
const name = args[0];
if (name) {
await disconnectServer(name);
successMessage(`Disconnected from ${name}`);
} else {
await disconnectAllServers();
successMessage("Disconnected from all servers");
}
};
/**
* Show connection status
*/
const handleStatus = async (_args: string[]): Promise<void> => {
await initializeMCP();
const instances = getServerInstances();
if (instances.size === 0) {
infoMessage("No MCP servers configured.");
return;
}
console.log(chalk.bold("\nMCP Server Status:\n"));
for (const [name, instance] of instances) {
const stateColors: Record<string, (s: string) => string> = {
connected: chalk.green,
connecting: chalk.yellow,
disconnected: chalk.gray,
error: chalk.red,
};
const colorFn = stateColors[instance.state] || chalk.white;
const status = colorFn(instance.state.toUpperCase());
console.log(` ${chalk.cyan(name)}: ${status}`);
if (instance.state === "connected") {
console.log(` Tools: ${instance.tools.length}`);
console.log(` Resources: ${instance.resources.length}`);
}
if (instance.error) {
console.log(` Error: ${chalk.red(instance.error)}`);
}
console.log();
}
};
/**
* List available tools
*/
const handleTools = async (_args: string[]): Promise<void> => {
await connectAllServers();
const tools = getAllTools();
if (tools.length === 0) {
infoMessage("No tools available. Connect to MCP servers first.");
return;
}
console.log(chalk.bold("\nAvailable MCP Tools:\n"));
// Group by server
const byServer = new Map<string, typeof tools>();
for (const item of tools) {
const existing = byServer.get(item.server) || [];
existing.push(item);
byServer.set(item.server, existing);
}
for (const [server, serverTools] of byServer) {
console.log(chalk.cyan(` ${server}:`));
for (const { tool } of serverTools) {
console.log(` - ${chalk.white(tool.name)}`);
if (tool.description) {
console.log(` ${chalk.gray(tool.description)}`);
}
}
console.log();
}
};
/**
* Show help
*/
const handleHelp = async (_args: string[]): Promise<void> => {
console.log(`
${chalk.bold("MCP (Model Context Protocol) Management")}
${chalk.cyan("Usage:")}
codetyper mcp <command> [options]
${chalk.cyan("Commands:")}
list List configured servers
add <name> Add a new server
--command, -c <cmd> Command to run
--args, -a <args> Arguments for command
--global, -g Add to global config
remove <name> Remove a server
--global, -g Remove from global config
connect [name] Connect to server(s)
disconnect [name] Disconnect from server(s)
status Show connection status
tools List available tools from connected servers
${chalk.cyan("Examples:")}
codetyper mcp add sqlite -c npx -a @modelcontextprotocol/server-sqlite
codetyper mcp connect sqlite
codetyper mcp tools
`);
};
export default mcpCommand;

10
src/commands/runner.ts Normal file
View File

@@ -0,0 +1,10 @@
/**
* Autonomous task runner - executes agent tasks
*/
export { execute } from "@commands/runner/execute";
export { createPlan } from "@commands/runner/create-plan";
export { executePlan } from "@commands/runner/execute-plan";
export { displayPlan, getStepIcon } from "@commands/runner/display-plan";
export { displayHeader } from "@commands/runner/display-header";
export { delay } from "@commands/runner/utils";

View File

@@ -0,0 +1,45 @@
/**
* Plan creation utilities
*/
import {
RUNNER_DELAYS,
MOCK_STEPS,
DEFAULT_FILE,
ESTIMATED_TIME_PER_STEP,
} from "@constants/runner";
import { delay } from "@commands/runner/utils";
import type { AgentType, ExecutionPlan, PlanStep } from "@/types/index";
export const createPlan = async (
task: string,
_agent: AgentType,
files: string[],
): Promise<ExecutionPlan> => {
await delay(RUNNER_DELAYS.PLANNING);
const targetFile = files[0] ?? DEFAULT_FILE;
const steps: PlanStep[] = [
{
...MOCK_STEPS.READ,
file: targetFile,
},
{
...MOCK_STEPS.EDIT,
file: targetFile,
dependencies: [...MOCK_STEPS.EDIT.dependencies],
},
{
...MOCK_STEPS.EXECUTE,
dependencies: [...MOCK_STEPS.EXECUTE.dependencies],
},
];
return {
steps,
intent: "code",
summary: task,
estimatedTime: steps.length * ESTIMATED_TIME_PER_STEP,
};
};

View File

@@ -0,0 +1,27 @@
/**
* Runner header display utilities
*/
import chalk from "chalk";
import { headerMessage, filePath } from "@utils/terminal";
import type { RunnerOptions } from "@/types/runner";
export const displayHeader = (options: RunnerOptions): void => {
const { task, agent, files, dryRun } = options;
headerMessage("Running Task");
console.log(chalk.bold("Agent:") + ` ${chalk.cyan(agent)}`);
console.log(chalk.bold("Task:") + ` ${task}`);
if (files.length > 0) {
console.log(
chalk.bold("Files:") + ` ${files.map((f) => filePath(f)).join(", ")}`,
);
}
console.log(
chalk.bold("Mode:") +
` ${dryRun ? chalk.yellow("Dry Run") : chalk.green("Execute")}`,
);
console.log();
};

View File

@@ -0,0 +1,34 @@
/**
* Plan display utilities
*/
import chalk from "chalk";
import { filePath } from "@utils/terminal";
import { STEP_ICONS, DEFAULT_STEP_ICON } from "@constants/runner";
import type { ExecutionPlan, PlanStep } from "@/types/index";
export const getStepIcon = (type: PlanStep["type"]): string =>
STEP_ICONS[type] ?? DEFAULT_STEP_ICON;
export const displayPlan = (plan: ExecutionPlan): void => {
console.log("\n" + chalk.bold.underline("Execution Plan:"));
console.log(chalk.gray(`${plan.summary}`));
console.log();
plan.steps.forEach((step, index) => {
const icon = getStepIcon(step.type);
const deps = step.dependencies
? chalk.gray(` (depends on: ${step.dependencies.join(", ")})`)
: "";
console.log(
`${icon} ${chalk.bold(`Step ${index + 1}:`)} ${step.description}${deps}`,
);
if (step.file) {
console.log(` ${filePath(step.file)}`);
}
if (step.tool) {
console.log(` ${chalk.gray(`Tool: ${step.tool}`)}`);
}
});
console.log();
};

View File

@@ -0,0 +1,35 @@
/**
* Plan execution utilities
*/
import { failSpinner, succeedSpinner, startSpinner } from "@utils/terminal";
import { RUNNER_DELAYS } from "@constants/runner";
import { getStepIcon } from "@commands/runner/display-plan";
import { delay } from "@commands/runner/utils";
import type { ExecutionPlan, PlanStep } from "@/types/index";
import type { StepContext } from "@/types/runner";
const executeStep = async (context: StepContext): Promise<void> => {
const { step, current, total } = context;
const icon = getStepIcon(step.type);
const message = `${icon} Step ${current}/${total}: ${step.description}`;
startSpinner(message);
try {
await delay(RUNNER_DELAYS.STEP_EXECUTION);
succeedSpinner(message);
} catch (error) {
failSpinner(message);
throw error;
}
};
export const executePlan = async (plan: ExecutionPlan): Promise<void> => {
const total = plan.steps.length;
for (let i = 0; i < plan.steps.length; i++) {
const step: PlanStep = plan.steps[i];
await executeStep({ step, current: i + 1, total });
}
};

View File

@@ -0,0 +1,118 @@
/**
* Main runner execution function
*/
import {
askConfirm,
failSpinner,
successMessage,
succeedSpinner,
headerMessage,
startSpinner,
infoMessage,
errorMessage,
warningMessage,
} from "@utils/terminal";
import { RUNNER_DELAYS, RUNNER_MESSAGES } from "@constants/runner";
import { displayHeader } from "@commands/runner/display-header";
import { displayPlan } from "@commands/runner/display-plan";
import { createPlan } from "@commands/runner/create-plan";
import { executePlan } from "@commands/runner/execute-plan";
import { delay } from "@commands/runner/utils";
import type { CommandOptions, AgentType } from "@/types/index";
import type { RunnerOptions } from "@/types/runner";
const parseOptions = (options: CommandOptions): RunnerOptions | null => {
const {
task,
agent = "coder",
files = [],
dryRun = false,
autoApprove = false,
} = options;
if (!task) {
errorMessage(RUNNER_MESSAGES.TASK_REQUIRED);
return null;
}
return {
task,
agent: agent as AgentType,
files,
dryRun,
autoApprove,
};
};
const runDiscoveryPhase = async (): Promise<void> => {
startSpinner(RUNNER_MESSAGES.DISCOVERY_START);
await delay(RUNNER_DELAYS.DISCOVERY);
succeedSpinner(RUNNER_MESSAGES.DISCOVERY_COMPLETE);
};
const runPlanningPhase = async (
task: string,
agent: AgentType,
files: string[],
) => {
startSpinner(RUNNER_MESSAGES.PLANNING_START);
const plan = await createPlan(task, agent, files);
succeedSpinner(`Plan created with ${plan.steps.length} steps`);
return plan;
};
const confirmExecution = async (autoApprove: boolean): Promise<boolean> => {
if (autoApprove) {
return true;
}
const approved = await askConfirm(RUNNER_MESSAGES.CONFIRM_EXECUTE);
if (!approved) {
warningMessage(RUNNER_MESSAGES.EXECUTION_CANCELLED);
return false;
}
return true;
};
export const execute = async (options: CommandOptions): Promise<void> => {
const runnerOptions = parseOptions(options);
if (!runnerOptions) {
return;
}
const { task, agent, files, dryRun, autoApprove } = runnerOptions;
displayHeader(runnerOptions);
try {
await runDiscoveryPhase();
const plan = await runPlanningPhase(task, agent, files);
displayPlan(plan);
if (dryRun) {
infoMessage(RUNNER_MESSAGES.DRY_RUN_INFO);
return;
}
const shouldExecute = await confirmExecution(autoApprove);
if (!shouldExecute) {
return;
}
headerMessage("Executing Plan");
await executePlan(plan);
successMessage(`\n${RUNNER_MESSAGES.TASK_COMPLETE}`);
} catch (error) {
failSpinner(RUNNER_MESSAGES.TASK_FAILED);
errorMessage(`Error: ${error}`);
throw error;
}
};

View File

@@ -0,0 +1,6 @@
/**
* Runner utility functions
*/
export const delay = (ms: number): Promise<void> =>
new Promise((resolve) => setTimeout(resolve, ms));

5
src/constants/agent.ts Normal file
View File

@@ -0,0 +1,5 @@
/**
* Agent constants
*/
export const MAX_ITERATIONS = 50;

View File

@@ -0,0 +1,23 @@
/**
* Auto-Scroll Constants
*
* Constants for auto-scroll behavior in the TUI
*/
/** Distance from bottom (in lines) to consider "at bottom" */
export const BOTTOM_THRESHOLD = 3;
/** Settling time after operations complete (ms) */
export const SETTLE_TIMEOUT_MS = 300;
/** Timeout for marking auto-scroll events (ms) */
export const AUTO_SCROLL_MARK_TIMEOUT_MS = 250;
/** Default scroll lines per keyboard event */
export const KEYBOARD_SCROLL_LINES = 3;
/** Default scroll lines per page event */
export const PAGE_SCROLL_LINES = 10;
/** Mouse scroll lines per wheel event */
export const MOUSE_SCROLL_LINES = 3;

103
src/constants/banner.ts Normal file
View File

@@ -0,0 +1,103 @@
/**
* Banner constants for CodeTyper CLI
*/
// ASCII art for "codetyper" using block characters
export const BANNER_LINES = [
" __ __ ",
" _______ _____/ /__ / /___ ______ ___ _____ ",
" / ___/ / / / _ \\/ _ \\/ __/ / / / __ \\/ _ \\/ ___/ ",
"/ /__/ /_/ / __/ __/ /_/ /_/ / /_/ / __/ / ",
"\\___/\\____/\\___/\\___/\\__/\\__, / .___/\\___/_/ ",
" /____/_/ ",
] as const;
// Alternative minimal banner
export const BANNER_MINIMAL = [
"╭───────────────────────────────────────╮",
"│ ▄▀▀ ▄▀▄ █▀▄ ██▀ ▀█▀ ▀▄▀ █▀▄ ██▀ █▀▄ │",
"│ ▀▄▄ ▀▄▀ █▄▀ █▄▄ █ █ █▀ █▄▄ █▀▄ │",
"╰───────────────────────────────────────╯",
] as const;
// Block-style banner (similar to opencode)
export const BANNER_BLOCKS = [
"█▀▀ █▀█ █▀▄ █▀▀ ▀█▀ █▄█ █▀█ █▀▀ █▀█",
"█ █ █ █ █ █▀▀ █ █ █▀▀ █▀▀ █▀▄",
"▀▀▀ ▀▀▀ ▀▀ ▀▀▀ ▀ ▀ ▀ ▀▀▀ ▀ ▀",
] as const;
// Gradient colors for banner (cyan to blue)
export const GRADIENT_COLORS = [
"\x1b[96m", // Bright cyan
"\x1b[36m", // Cyan
"\x1b[94m", // Bright blue
"\x1b[34m", // Blue
"\x1b[95m", // Bright magenta
"\x1b[35m", // Magenta
] as const;
// Banner style to lines mapping
export const BANNER_STYLE_MAP: Record<string, readonly string[]> = {
default: BANNER_LINES,
minimal: BANNER_MINIMAL,
blocks: BANNER_BLOCKS,
} as const;
// Large ASCII art banner
export const BANNER = `
,gggg, _,gggggg,_ ,gggggggggggg, ,ggggggg, ,ggggggggggggggg ,ggg, gg ,ggggggggggg, ,ggggggg, ,ggggggggggg,
,88"""Y8b, ,d8P""d8P"Y8b, dP"""88""""""Y8b, ,dP"""""""Y8bdP""""""88"""""""dP""Y8a 88 dP"""88""""""Y8, ,dP"""""""Y8bdP"""88""""""Y8,
d8" \`Y8,d8' Y8 "8b,dPYb, 88 \`8b, d8' a Y8Yb,_ 88 Yb, \`88 88 Yb, 88 \`8b d8' a Y8Yb, 88 \`8b
d8' 8b d8d8' \`Ybaaad88P' \`" 88 \`8b 88 "Y8P' \`"" 88 \`" 88 88 \`" 88 ,8P 88 "Y8P' \`" 88 ,8P
,8I "Y88P'8P \`"""Y8 88 Y8 \`8baaaa 88 88 88 88aaaad8P" \`8baaaa 88aaaad8P"
I8' 8b d8 88 d8,d8P"""" 88 88 88 88""""" ,d8P"""" 88""""Yb,
d8 Y8, ,8P 88 ,8Pd8" 88 88 ,88 88 d8" 88 "8b
Y8, \`Y8, ,8P' 88 ,8P'Y8, gg, 88 Y8b,___,d888 88 Y8, 88 \`8i
\`Yba,,_____, \`Y8b,,__,,d8P' 88______,dP' \`Yba,,_____, "Yb,,8P "Y88888P"88, 88 \`Yba,,_____, 88 Yb,
\`"Y8888888 \`"Y8888P"' 888888888P" \`"Y8888888 "Y8P' ,ad8888 88 \`"Y8888888 88 Y8
d8P" 88
,d8' 88
d8' 88
88 88
Y8,_ _,88
"Y888P"
`;
// Welcome message with help information
export const WELCOME_MESSAGE = `
🤖 CodeTyper AI Agent - Autonomous Code Generation Assistant
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Default Provider: GitHub Copilot (gpt-4)
Getting Started:
codetyper chat Start interactive chat
codetyper run "your task" Execute autonomous task
codetyper classify "prompt" Analyze intent
codetyper config show View configuration
Commands:
chat Interactive REPL session
run <task> Execute task autonomously
classify <prompt> Classify user intent
plan <intent> Generate execution plan
validate <plan> Validate plan safety
config Manage configuration
serve Start JSON-RPC server
Options:
--help, -h Show help
--version, -V Show version
Chat Commands:
/help Show help
/models View available LLM providers
/provider Switch LLM provider
/files List context files
/clear Clear conversation
/exit Exit chat
💡 Tip: Use 'codetyper chat' then '/models' to see all available providers
📖 Docs: Run 'codetyper --help <command>' for detailed information
`;

30
src/constants/bash.ts Normal file
View File

@@ -0,0 +1,30 @@
/**
* Bash tool constants
*/
export const BASH_DEFAULTS = {
MAX_OUTPUT_LENGTH: 30000,
TIMEOUT: 120000,
KILL_DELAY: 1000,
} as const;
export const BASH_SIGNALS = {
TERMINATE: "SIGTERM",
KILL: "SIGKILL",
} as const;
export const BASH_MESSAGES = {
PERMISSION_DENIED: "Permission denied by user",
TIMED_OUT: (timeout: number) => `Command timed out after ${timeout}ms`,
ABORTED: "Command aborted",
EXIT_CODE: (code: number) => `Command exited with code ${code}`,
TRUNCATED: "\n\n... (truncated)",
} as const;
export const BASH_DESCRIPTION = `Execute a shell command. Use this tool to run commands like git, npm, mkdir, etc.
Guidelines:
- Always provide a clear description of what the command does
- Use absolute paths when possible
- Be careful with destructive commands (rm, etc.)
- Commands that modify the filesystem will require user approval`;

View File

@@ -0,0 +1,20 @@
/** Quiet bash commands - read-only exploration operations */
export const QUIET_BASH_PATTERNS = [
/^ls\b/,
/^cat\b/,
/^head\b/,
/^tail\b/,
/^find\b/,
/^grep\b/,
/^rg\b/,
/^fd\b/,
/^tree\b/,
/^pwd\b/,
/^echo\b/,
/^which\b/,
/^file\b/,
/^stat\b/,
/^wc\b/,
/^du\b/,
/^df\b/,
];

View File

@@ -0,0 +1,106 @@
/**
* Chat service constants
*/
export const CHAT_TRUNCATE_DEFAULTS = {
MAX_LINES: 10,
MAX_LENGTH: 500,
} as const;
export const FILE_SIZE_LIMITS = {
MAX_CONTEXT_FILE_SIZE: 100000,
} as const;
export const DIFF_PATTERNS = [
/@@\s*-\d+/m,
/---\s+[ab]?\//m,
/\+\+\+\s+[ab]?\//m,
] as const;
export const GLOB_IGNORE_PATTERNS = [
"**/node_modules/**",
"**/.git/**",
] as const;
export const CHAT_MESSAGES = {
CONVERSATION_CLEARED: "Conversation cleared",
SESSION_SAVED: "Session saved",
LEARNING_SAVED: (scope: string) => `Learning saved (${scope})`,
LEARNING_SKIPPED: "Learning skipped",
NO_LEARNINGS:
"No learnings saved yet. Use /remember to save learnings about your project.",
NO_CONVERSATION:
"No conversation to create learning from. Start a conversation first.",
NO_LEARNINGS_DETECTED:
'No learnings detected from the last exchange. Try being more explicit about preferences (e.g., "always use TypeScript", "prefer functional style").',
UNKNOWN_COMMAND: (cmd: string) => `Unknown command: ${cmd}`,
FILE_NOT_FOUND: (pattern: string) => `File not found: ${pattern}`,
FILE_TOO_LARGE: (name: string, size: number) =>
`File too large: ${name} (${Math.round(size / 1024)}KB)`,
FILE_IS_BINARY: (name: string) => `Cannot add binary file: ${name}`,
FILE_ADDED: (name: string) => `Added to context: ${name}`,
FILE_ADD_FAILED: (error: unknown) => `Failed to add file: ${error}`,
FILE_READ_FAILED: (error: unknown) => `Failed to read file: ${error}`,
ANALYZE_FILES: "Analyze the files I've added to the context.",
GITHUB_ISSUES_FOUND: (count: number, issues: string) =>
`Found ${count} GitHub issue(s): ${issues}`,
COMPACTION_STARTING: "Summarizing conversation history...",
COMPACTION_CONTINUING: "Continuing with your request...",
} as const;
export const AUTH_MESSAGES = {
ALREADY_LOGGED_IN: "Already logged in. Use /logout first to re-authenticate.",
AUTH_SUCCESS: "Successfully authenticated with GitHub Copilot!",
AUTH_FAILED: (error: string) => `Authentication failed: ${error}`,
AUTH_START_FAILED: (error: string) =>
`Failed to start authentication: ${error}`,
LOGGED_OUT:
"Logged out from GitHub Copilot. Run /login to authenticate again.",
NOT_LOGGED_IN: "Not logged in. Run /login to authenticate.",
NO_LOGIN_REQUIRED: (provider: string) =>
`Provider ${provider} doesn't require login.`,
NO_LOGOUT_SUPPORT: (provider: string) =>
`Provider ${provider} doesn't support logout.`,
OLLAMA_NO_AUTH: "Ollama is a local provider - no authentication required.",
COPILOT_AUTH_INSTRUCTIONS: (uri: string, code: string) =>
`To authenticate with GitHub Copilot:\n\n1. Open: ${uri}\n2. Enter code: ${code}\n\nWaiting for authentication...`,
LOGGED_IN_AS: (login: string, name?: string) =>
`Logged in as: ${login}${name ? ` (${name})` : ""}`,
} as const;
export const MODEL_MESSAGES = {
MODEL_AUTO: "Model set to auto - the provider will choose the best model.",
MODEL_CHANGED: (model: string) => `Model changed to: ${model}`,
} as const;
// Re-export HELP_TEXT from prompts for backward compatibility
export { HELP_TEXT } from "@prompts/ui/help";
export const LEARNING_CONFIDENCE_THRESHOLD = 0.7;
export const MAX_LEARNINGS_DISPLAY = 20;
export type CommandName =
| "help"
| "h"
| "clear"
| "c"
| "save"
| "s"
| "context"
| "usage"
| "u"
| "model"
| "models"
| "agent"
| "a"
| "theme"
| "mcp"
| "mode"
| "whoami"
| "login"
| "logout"
| "provider"
| "p"
| "status"
| "remember"
| "learnings";

View File

@@ -0,0 +1,85 @@
/**
* Command suggestion constants
*/
import type { SuggestionPriority } from "@/types/command-suggestion";
export const PROJECT_FILES = {
PACKAGE_JSON: "package.json",
YARN_LOCK: "yarn.lock",
PNPM_LOCK: "pnpm-lock.yaml",
BUN_LOCK: "bun.lockb",
CARGO_TOML: "Cargo.toml",
GO_MOD: "go.mod",
PYPROJECT: "pyproject.toml",
REQUIREMENTS: "requirements.txt",
MAKEFILE: "Makefile",
DOCKERFILE: "Dockerfile",
} as const;
export const PRIORITY_ORDER: Record<SuggestionPriority, number> = {
high: 0,
medium: 1,
low: 2,
};
export const PRIORITY_ICONS: Record<SuggestionPriority, string> = {
high: "⚡",
medium: "→",
low: "·",
};
export const FILE_PATTERNS = {
PACKAGE_JSON: /package\.json$/,
TSCONFIG: /tsconfig.*\.json$/,
SOURCE_FILES: /\.(ts|tsx|js|jsx)$/,
CARGO_TOML: /Cargo\.toml$/,
GO_MOD: /go\.mod$/,
PYTHON_DEPS: /requirements.*\.txt$|pyproject\.toml$/,
DOCKER: /Dockerfile$|docker-compose.*\.ya?ml$/,
MAKEFILE: /Makefile$/,
MIGRATIONS: /migrations?\/.*\.(sql|ts|js)$/,
ENV_EXAMPLE: /\.env\.example$|\.env\.sample$/,
LINTER_CONFIG: /\.eslintrc|\.prettierrc|eslint\.config|prettier\.config/,
TEST_FILE: /\.test\.|\.spec\.|__tests__/,
} as const;
export const CONTENT_PATTERNS = {
DEPENDENCIES: /\"dependencies\"/,
DEV_DEPENDENCIES: /\"devDependencies\"/,
PEER_DEPENDENCIES: /\"peerDependencies\"/,
} as const;
export const SUGGESTION_MESSAGES = {
INSTALL_DEPS: "Install dependencies",
REBUILD_PROJECT: "Rebuild the project",
RUN_TESTS: "Run tests",
START_DEV: "Start development server",
BUILD_RUST: "Build the Rust project",
TIDY_GO: "Tidy Go modules",
INSTALL_PYTHON_EDITABLE: "Install Python package in editable mode",
INSTALL_PYTHON_DEPS: "Install Python dependencies",
DOCKER_COMPOSE_BUILD: "Rebuild and start Docker containers",
DOCKER_BUILD: "Rebuild Docker image",
RUN_MAKE: "Run make",
RUN_MIGRATE: "Run database migrations",
CREATE_ENV: "Create local .env file",
RUN_LINT: "Run linter to check for issues",
} as const;
export const SUGGESTION_REASONS = {
PACKAGE_JSON_MODIFIED: "package.json was modified",
TSCONFIG_CHANGED: "TypeScript configuration changed",
TEST_FILE_MODIFIED: "Test file was modified",
SOURCE_FILE_MODIFIED: "Source file was modified",
CARGO_MODIFIED: "Cargo.toml was modified",
GO_MOD_MODIFIED: "go.mod was modified",
PYTHON_DEPS_CHANGED: "Python dependencies changed",
REQUIREMENTS_MODIFIED: "requirements.txt was modified",
DOCKER_COMPOSE_CHANGED: "Docker Compose configuration changed",
DOCKERFILE_MODIFIED: "Dockerfile was modified",
MAKEFILE_MODIFIED: "Makefile was modified",
MIGRATION_MODIFIED: "Migration file was added or modified",
ENV_TEMPLATE_MODIFIED: "Environment template was modified",
LINTER_CONFIG_CHANGED: "Linter configuration changed",
} as const;

114
src/constants/components.ts Normal file
View File

@@ -0,0 +1,114 @@
/**
* UI component constants
*/
// Box drawing characters
export const BoxChars = {
// Single line
single: {
topLeft: "┌",
topRight: "┐",
bottomLeft: "└",
bottomRight: "┘",
horizontal: "─",
vertical: "│",
leftT: "├",
rightT: "┤",
topT: "┬",
bottomT: "┴",
cross: "┼",
},
// Double line
double: {
topLeft: "╔",
topRight: "╗",
bottomLeft: "╚",
bottomRight: "╝",
horizontal: "═",
vertical: "║",
leftT: "╠",
rightT: "╣",
topT: "╦",
bottomT: "╩",
cross: "╬",
},
// Rounded
rounded: {
topLeft: "╭",
topRight: "╮",
bottomLeft: "╰",
bottomRight: "╯",
horizontal: "─",
vertical: "│",
leftT: "├",
rightT: "┤",
topT: "┬",
bottomT: "┴",
cross: "┼",
},
// Bold
bold: {
topLeft: "┏",
topRight: "┓",
bottomLeft: "┗",
bottomRight: "┛",
horizontal: "━",
vertical: "┃",
leftT: "┣",
rightT: "┫",
topT: "┳",
bottomT: "┻",
cross: "╋",
},
} as const;
// Default box options
export const BOX_DEFAULTS = {
style: "rounded" as const,
padding: 1,
align: "left" as const,
} as const;
// Tool icon mapping
export const TOOL_ICONS = {
bash: "bash",
read: "read",
write: "write",
edit: "edit",
default: "default",
} as const;
// State color mapping
export const STATE_COLORS = {
pending: "DIM",
running: "primary",
success: "success",
error: "error",
} as const;
// Role configuration for message display
export const ROLE_CONFIG = {
user: { label: "You", colorKey: "primary" },
assistant: { label: "CodeTyper", colorKey: "success" },
system: { label: "System", colorKey: "textMuted" },
tool: { label: "Tool", colorKey: "warning" },
} as const;
// Status indicator configuration
export const STATUS_INDICATORS = {
success: { iconKey: "success", colorKey: "success" },
error: { iconKey: "error", colorKey: "error" },
warning: { iconKey: "warning", colorKey: "warning" },
info: { iconKey: "info", colorKey: "info" },
pending: { iconKey: "pending", colorKey: "textMuted" },
running: { iconKey: "running", colorKey: "primary" },
} as const;
// Tool call icon configuration
export const TOOL_CALL_ICONS = {
bash: { iconKey: "bash", colorKey: "warning" },
read: { iconKey: "read", colorKey: "info" },
write: { iconKey: "write", colorKey: "success" },
edit: { iconKey: "edit", colorKey: "primary" },
default: { iconKey: "gear", colorKey: "textMuted" },
} as const;

213
src/constants/copilot.ts Normal file
View File

@@ -0,0 +1,213 @@
import type { ProviderModel, ProviderName } from "@/types/providers";
// Provider identification
export const COPILOT_PROVIDER_NAME: ProviderName = "copilot";
export const COPILOT_DISPLAY_NAME = "GitHub Copilot";
// GitHub Copilot API endpoints
export const COPILOT_AUTH_URL =
"https://api.github.com/copilot_internal/v2/token";
export const COPILOT_MODELS_URL = "https://api.githubcopilot.com/models";
// GitHub OAuth endpoints for device flow
export const GITHUB_CLIENT_ID = "Iv1.b507a08c87ecfe98";
export const GITHUB_DEVICE_CODE_URL = "https://github.com/login/device/code";
export const GITHUB_ACCESS_TOKEN_URL =
"https://github.com/login/oauth/access_token";
// Cache and retry configuration
export const COPILOT_MODELS_CACHE_TTL = 5 * 60 * 1000; // 5 minutes
export const COPILOT_MAX_RETRIES = 3;
export const COPILOT_INITIAL_RETRY_DELAY = 1000; // 1 second
// Default model
export const COPILOT_DEFAULT_MODEL = "gpt-5-mini";
// Unlimited fallback model (used when quota is exceeded)
export const COPILOT_UNLIMITED_MODEL = "gpt-4o";
// Copilot messages
export const COPILOT_MESSAGES = {
QUOTA_EXCEEDED_SWITCHING: (from: string, to: string) =>
`Quota exceeded for ${from}. Switching to unlimited model: ${to}`,
MODEL_SWITCHED: (from: string, to: string) =>
`Model switched: ${from}${to} (quota exceeded)`,
FORMAT_MULTIPLIER: (multiplier: number) =>
multiplier === 0 ? "Unlimited" : `${multiplier}x`,
} as const;
// Model cost multipliers from GitHub Copilot
// 0x = Unlimited (no premium request usage)
// Lower multiplier = cheaper, Higher multiplier = more expensive
export const MODEL_COST_MULTIPLIERS: Record<string, number> = {
// Unlimited models (0x)
"gpt-4o": 0,
"gpt-4o-mini": 0,
"gpt-5-mini": 0,
"grok-code-fast-1": 0,
"raptor-mini": 0,
// Low cost models (0.33x)
"claude-haiku-4.5": 0.33,
"gemini-3-flash-preview": 0.33,
"gpt-5.1-codex-mini-preview": 0.33,
// Standard cost models (1.0x)
"claude-sonnet-4": 1.0,
"claude-sonnet-4.5": 1.0,
"gemini-2.5-pro": 1.0,
"gemini-3-pro-preview": 1.0,
"gpt-4.1": 1.0,
"gpt-5": 1.0,
"gpt-5-codex-preview": 1.0,
"gpt-5.1": 1.0,
"gpt-5.1-codex": 1.0,
"gpt-5.1-codex-max": 1.0,
"gpt-5.2": 1.0,
"gpt-5.2-codex": 1.0,
// Premium models (3.0x)
"claude-opus-4.5": 3.0,
};
// Models that are unlimited (0x cost multiplier)
export const UNLIMITED_MODELS = new Set([
"gpt-4o",
"gpt-4o-mini",
"gpt-5-mini",
"grok-code-fast-1",
"raptor-mini",
]);
// Model context sizes (input tokens, output tokens)
export interface ModelContextSize {
input: number;
output: number;
}
export const MODEL_CONTEXT_SIZES: Record<string, ModelContextSize> = {
// Claude models
"claude-haiku-4.5": { input: 128000, output: 16000 },
"claude-opus-4.5": { input: 128000, output: 16000 },
"claude-sonnet-4": { input: 128000, output: 16000 },
"claude-sonnet-4.5": { input: 128000, output: 16000 },
// Gemini models
"gemini-2.5-pro": { input: 109000, output: 64000 },
"gemini-3-flash-preview": { input: 109000, output: 64000 },
"gemini-3-pro-preview": { input: 109000, output: 64000 },
// GPT-4 models
"gpt-4.1": { input: 111000, output: 16000 },
"gpt-4o": { input: 64000, output: 4000 },
// GPT-5 models
"gpt-5": { input: 128000, output: 128000 },
"gpt-5-mini": { input: 128000, output: 64000 },
"gpt-5-codex-preview": { input: 128000, output: 128000 },
"gpt-5.1": { input: 128000, output: 64000 },
"gpt-5.1-codex": { input: 128000, output: 128000 },
"gpt-5.1-codex-max": { input: 128000, output: 128000 },
"gpt-5.1-codex-mini-preview": { input: 128000, output: 128000 },
"gpt-5.2": { input: 128000, output: 64000 },
"gpt-5.2-codex": { input: 272000, output: 128000 },
// Other models
"grok-code-fast-1": { input: 109000, output: 64000 },
"raptor-mini": { input: 200000, output: 64000 },
};
// Default context size for unknown models
export const DEFAULT_CONTEXT_SIZE: ModelContextSize = {
input: 128000,
output: 16000,
};
// Get context size for a model
export const getModelContextSize = (modelId: string): ModelContextSize =>
MODEL_CONTEXT_SIZES[modelId] ?? DEFAULT_CONTEXT_SIZE;
// Fallback models when API is unavailable
export const COPILOT_FALLBACK_MODELS: ProviderModel[] = [
{
id: "gpt-4o",
name: "GPT-4o",
maxTokens: 4000,
supportsTools: true,
supportsStreaming: true,
costMultiplier: 0,
isUnlimited: true,
},
{
id: "gpt-5-mini",
name: "GPT-5 mini",
maxTokens: 64000,
supportsTools: true,
supportsStreaming: true,
costMultiplier: 0,
isUnlimited: true,
},
{
id: "claude-sonnet-4",
name: "Claude Sonnet 4",
maxTokens: 16000,
supportsTools: true,
supportsStreaming: true,
costMultiplier: 1.0,
isUnlimited: false,
},
{
id: "claude-sonnet-4.5",
name: "Claude Sonnet 4.5",
maxTokens: 16000,
supportsTools: true,
supportsStreaming: true,
costMultiplier: 1.0,
isUnlimited: false,
},
{
id: "claude-opus-4.5",
name: "Claude Opus 4.5",
maxTokens: 16000,
supportsTools: true,
supportsStreaming: true,
costMultiplier: 3.0,
isUnlimited: false,
},
{
id: "gpt-4.1",
name: "GPT-4.1",
maxTokens: 16000,
supportsTools: true,
supportsStreaming: true,
costMultiplier: 1.0,
isUnlimited: false,
},
{
id: "gpt-5",
name: "GPT-5",
maxTokens: 128000,
supportsTools: true,
supportsStreaming: true,
costMultiplier: 1.0,
isUnlimited: false,
},
{
id: "gemini-2.5-pro",
name: "Gemini 2.5 Pro",
maxTokens: 64000,
supportsTools: true,
supportsStreaming: true,
costMultiplier: 1.0,
isUnlimited: false,
},
{
id: "grok-code-fast-1",
name: "Grok Code Fast 1",
maxTokens: 64000,
supportsTools: true,
supportsStreaming: true,
costMultiplier: 0,
isUnlimited: true,
},
];

View File

@@ -0,0 +1,42 @@
/**
* Dashboard Constants
*/
export const DASHBOARD_TITLE = "CodeTyper";
export const DASHBOARD_LAYOUT = {
DEFAULT_WIDTH: 120,
CONTENT_HEIGHT: 15,
LEFT_COLUMN_RATIO: 0.35,
PADDING: 3,
} as const;
export const DASHBOARD_LOGO = [
" ██████╗███████╗",
" ██╔════╝██╔════╝",
" ██║ ███████╗",
" ██║ ╚════██║",
" ╚██████╗███████║",
" ╚═════╝╚══════╝",
] as const;
export const DASHBOARD_COMMANDS = [
{ command: "codetyper chat", description: "Start interactive chat" },
{ command: "codetyper run <task>", description: "Execute autonomous task" },
{ command: "/help", description: "Show all commands in chat" },
] as const;
export const DASHBOARD_QUICK_COMMANDS = [
{ command: "codetyper chat", description: "Start interactive chat" },
{ command: "codetyper run", description: "Execute autonomous task" },
{ command: "codetyper --help", description: "Show all commands" },
] as const;
export const DASHBOARD_BORDER = {
TOP_LEFT: "╭",
TOP_RIGHT: "╮",
BOTTOM_LEFT: "╰",
BOTTOM_RIGHT: "╯",
HORIZONTAL: "─",
VERTICAL: "│",
} as const;

13
src/constants/diff.ts Normal file
View File

@@ -0,0 +1,13 @@
/**
* Diff utility constants
*/
// Default context lines for hunks
export const DIFF_CONTEXT_LINES = 3;
// Line type prefixes
export const LINE_PREFIXES = {
add: "+",
remove: "-",
context: " ",
} as const;

25
src/constants/edit.ts Normal file
View File

@@ -0,0 +1,25 @@
/**
* Edit tool constants
*/
export const EDIT_MESSAGES = {
NOT_FOUND:
"Could not find the text to replace. Make sure old_string matches exactly.",
MULTIPLE_OCCURRENCES: (count: number) =>
`old_string appears ${count} times. Use replace_all=true or provide more context to make it unique.`,
PERMISSION_DENIED: "Permission denied by user",
} as const;
export const EDIT_TITLES = {
FAILED: (path: string) => `Edit failed: ${path}`,
CANCELLED: (path: string) => `Edit cancelled: ${path}`,
SUCCESS: (path: string) => `Edited: ${path}`,
EDITING: (name: string) => `Editing ${name}`,
} as const;
export const EDIT_DESCRIPTION = `Edit a file by replacing specific text. The old_string must match exactly.
Guidelines:
- old_string must be unique in the file (or use replace_all)
- Preserve indentation exactly as it appears in the file
- Requires user approval for edits`;

Some files were not shown because too many files have changed in this diff Show More