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
263
.gitignore
vendored
Normal 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
@@ -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
@@ -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.
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
|
||||

|
||||
|
||||
**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.
|
||||
|
||||

|
||||
|
||||
**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.
|
||||
|
||||

|
||||
|
||||
**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 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.
|
||||
|
||||

|
||||
|
||||
**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.
|
||||
|
||||

|
||||
|
||||
**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.
|
||||
BIN
assets/CodetyperAgentMode.png
Normal file
|
After Width: | Height: | Size: 229 KiB |
BIN
assets/CodetyperCopilotModels.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
assets/CodetyperLogin.png
Normal file
|
After Width: | Height: | Size: 104 KiB |
BIN
assets/CodetyperMenu.png
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
assets/CodetyperPermissionView.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
assets/CodetyperThemes.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
assets/CodetyperView.png
Normal file
|
After Width: | Height: | Size: 174 KiB |
1
bin/codetyper
Symbolic link
@@ -0,0 +1 @@
|
||||
../lib/node_modules/codetyper-cli/dist/index.js
|
||||
6
bunfig.toml
Normal 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
@@ -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
@@ -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
@@ -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
100
package.json
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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";
|
||||
23
src/commands/components/callbacks/on-learning-detected.ts
Normal 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);
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
14
src/commands/components/callbacks/on-log.ts
Normal 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,
|
||||
});
|
||||
};
|
||||
5
src/commands/components/callbacks/on-mode-change.ts
Normal 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]);
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
interface PermissionResponse {
|
||||
allowed: boolean;
|
||||
}
|
||||
|
||||
export const onPermissionRequest = async (): Promise<PermissionResponse> => {
|
||||
return { allowed: false };
|
||||
};
|
||||
25
src/commands/components/callbacks/on-tool-call.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
};
|
||||
47
src/commands/components/callbacks/on-tool-result.ts
Normal 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());
|
||||
};
|
||||
35
src/commands/components/chat/agents/show-agents.ts
Normal 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();
|
||||
};
|
||||
60
src/commands/components/chat/agents/switch-agent.ts
Normal 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();
|
||||
};
|
||||
12
src/commands/components/chat/cleanup.ts
Normal 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);
|
||||
};
|
||||
111
src/commands/components/chat/commands/commandsRegistry.ts
Normal 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;
|
||||
28
src/commands/components/chat/commands/handle-command.ts
Normal 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 });
|
||||
}
|
||||
};
|
||||
25
src/commands/components/chat/commands/show-help.ts
Normal 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");
|
||||
};
|
||||
35
src/commands/components/chat/context/add-context-file.ts
Normal 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}`);
|
||||
}
|
||||
};
|
||||
32
src/commands/components/chat/context/load-file.ts
Normal 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}`);
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
22
src/commands/components/chat/context/remove-file.ts
Normal 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}`);
|
||||
};
|
||||
32
src/commands/components/chat/context/show-context-files.ts
Normal 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();
|
||||
};
|
||||
10
src/commands/components/chat/history/clear-conversation.ts
Normal 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");
|
||||
};
|
||||
15
src/commands/components/chat/history/compact-history.ts
Normal 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`);
|
||||
};
|
||||
18
src/commands/components/chat/history/show-context.ts
Normal 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();
|
||||
};
|
||||
18
src/commands/components/chat/history/show-history.ts
Normal 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();
|
||||
};
|
||||
224
src/commands/components/chat/index.ts
Normal 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";
|
||||
143
src/commands/components/chat/mcp/handle-mcp.ts
Normal 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");
|
||||
};
|
||||
6
src/commands/components/chat/mcp/index.ts
Normal 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";
|
||||
76
src/commands/components/chat/mcp/show-mcp-status.ts
Normal 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();
|
||||
};
|
||||
17
src/commands/components/chat/messages/handle-input.ts
Normal 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);
|
||||
};
|
||||
228
src/commands/components/chat/messages/send-message.ts
Normal 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}`);
|
||||
}
|
||||
};
|
||||
31
src/commands/components/chat/models/show-models.ts
Normal 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();
|
||||
};
|
||||
7
src/commands/components/chat/models/show-providers.ts
Normal 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"));
|
||||
};
|
||||
50
src/commands/components/chat/models/switch-model.ts
Normal 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();
|
||||
};
|
||||
51
src/commands/components/chat/models/switch-provider.ts
Normal 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}`);
|
||||
};
|
||||
71
src/commands/components/chat/print-mode.ts
Normal 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);
|
||||
}
|
||||
};
|
||||
38
src/commands/components/chat/session/list-sessions.ts
Normal 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();
|
||||
};
|
||||
20
src/commands/components/chat/session/restore-messages.ts
Normal 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;
|
||||
};
|
||||
20
src/commands/components/chat/session/show-session-info.ts
Normal 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();
|
||||
};
|
||||
34
src/commands/components/chat/state.ts
Normal 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,
|
||||
});
|
||||
129
src/commands/components/chat/usage/show-usage.ts
Normal 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();
|
||||
};
|
||||
25
src/commands/components/dashboard/build-config.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
33
src/commands/components/dashboard/display.ts
Normal 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);
|
||||
};
|
||||
41
src/commands/components/dashboard/render-content.ts
Normal 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");
|
||||
};
|
||||
31
src/commands/components/dashboard/render-footer.ts
Normal 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");
|
||||
};
|
||||
18
src/commands/components/dashboard/render-header.ts
Normal 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}`;
|
||||
};
|
||||
24
src/commands/components/dashboard/render-left-content.ts
Normal 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;
|
||||
};
|
||||
21
src/commands/components/dashboard/render-right-content.ts
Normal 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;
|
||||
};
|
||||
55
src/commands/components/execute/execute-solid.tsx
Normal 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();
|
||||
};
|
||||
104
src/commands/components/execute/execute.tsx
Normal 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 };
|
||||
195
src/commands/components/execute/index.ts
Normal 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;
|
||||
8
src/commands/dashboard.ts
Normal 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
@@ -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;
|
||||
}
|
||||
};
|
||||
10
src/commands/handlers/chat.ts
Normal 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);
|
||||
};
|
||||
123
src/commands/handlers/classify.ts
Normal 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;
|
||||
}
|
||||
};
|
||||
113
src/commands/handlers/config.ts
Normal 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);
|
||||
};
|
||||
62
src/commands/handlers/plan.ts
Normal 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;
|
||||
}
|
||||
};
|
||||
28
src/commands/handlers/registry.ts
Normal 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;
|
||||
};
|
||||
10
src/commands/handlers/run.ts
Normal 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);
|
||||
};
|
||||
15
src/commands/handlers/serve.ts
Normal 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",
|
||||
);
|
||||
};
|
||||
78
src/commands/handlers/validate.ts
Normal 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
@@ -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
@@ -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";
|
||||
45
src/commands/runner/create-plan.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
27
src/commands/runner/display-header.ts
Normal 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();
|
||||
};
|
||||
34
src/commands/runner/display-plan.ts
Normal 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();
|
||||
};
|
||||
35
src/commands/runner/execute-plan.ts
Normal 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 });
|
||||
}
|
||||
};
|
||||
118
src/commands/runner/execute.ts
Normal 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;
|
||||
}
|
||||
};
|
||||
6
src/commands/runner/utils.ts
Normal 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
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Agent constants
|
||||
*/
|
||||
|
||||
export const MAX_ITERATIONS = 50;
|
||||
23
src/constants/auto-scroll.ts
Normal 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
@@ -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
@@ -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`;
|
||||
20
src/constants/bashPatterns.ts
Normal 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/,
|
||||
];
|
||||
106
src/constants/chat-service.ts
Normal 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";
|
||||
85
src/constants/command-suggestion.ts
Normal 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
@@ -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
@@ -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,
|
||||
},
|
||||
];
|
||||
42
src/constants/dashboard.ts
Normal 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
@@ -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
@@ -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`;
|
||||