Compare commits

...

40 Commits

Author SHA1 Message Date
Xinlu Lai
52cb635512
Merge pull request #135 from RadonX/responses-api-adaptation
feat(openai): Responses API integration with adapter pattern
2025-11-15 20:32:17 +08:00
Radon Co
3d7f81242b test(responses-api): restructure test suite layout 2025-11-11 23:03:31 -08:00
Radon Co
14f9892bb5 feat(responses-api-adapter): Enhanced Tool Call Conversion
WHAT: Enhanced tool call handling in Responses API adapter with better validation, error handling, and test coverage

WHY: The adapter lacked robust tool call parsing and validation, leading to potential issues with malformed tool calls and incomplete test coverage. We needed to improve error handling and add comprehensive tests for real tool call scenarios.

HOW: Enhanced tool call result parsing with defensive null checking; improved assistant tool call parsing with proper validation; enhanced response tool call parsing with better structure and support for multiple tool call types; added validation for streaming tool call handling; updated tests to validate real tool call parsing from API; added multi-turn conversation test with tool result injection

Testing: All 3 integration tests pass with real API calls. Validated tool call parsing and tool result conversion working correctly. Real tool call detected and parsed successfully.
2025-11-11 12:58:30 -08:00
Radon Co
25adc80161 prompt(queryOpenAI): Separate adapter context from API execution
WHAT: Refactored queryOpenAI to prepare adapter context outside withRetry and execute API calls inside withRetry

WHY: The previous implementation mixed adapter preparation and execution, causing type confusion and state management issues

HOW: Created AdapterExecutionContext and QueryResult types, moved adapter context creation before withRetry block, wrapped all API calls (Responses API, Chat Completions, and legacy) inside withRetry with unified return structure, added normalizeUsage() helper to handle token field variations, ensured responseId and content are properly preserved through the unified return path
2025-11-11 00:49:01 -08:00
Radon Co
8288378dbd refactor(claude.ts): Extract adapter path before withRetry for clean separation
Problem: Mixed return types from withRetry callback caused content loss when
adapter returned AssistantMessage but outer code expected ChatCompletion.

Solution: Restructured queryOpenAI to separate adapter and legacy paths:
- Adapter path (responsesAPI): Direct execution, early return, no withRetry
- Legacy path (chat_completions): Uses withRetry for retry logic

Benefits:
 No type confusion - adapter path never enters withRetry
 Clean separation of concerns - adapters handle format, legacy handles retry
 Streaming-ready architecture for future async generator implementation
 Content displays correctly in CLI (fixed empty content bug)
 All 14 tests pass (52 assertions)

Additional changes:
- Added StreamingEvent type to base adapter for future async generators
- Updated UnifiedResponse to support both string and array content
- Added comments explaining architectural decisions and future improvements
- Fixed content loss bug in responses API path
2025-11-10 23:51:09 -08:00
Radon Co
c8ecba04d8 fix: Return AssistantMessage early to prevent content loss
Prevents adapter responses from being overwritten with empty content.
Adds early return check when response.type === 'assistant' to preserve
correctly formatted content from the adapter path.

All tests pass, CLI content now displays correctly.
2025-11-09 23:47:53 -08:00
Radon Co
34cd4e250d feat(responsesAPI): Implement async generator streaming for real-time UI updates
WHAT:
- Refactored ResponsesAPIAdapter to support async generator streaming pattern
- Added parseStreamingResponse() method that yields StreamingEvent incrementally
- Maintained backward compatibility with parseStreamingResponseBuffered() method
- Updated UnifiedResponse type to support both string and array content formats

WHY:
- Aligns Responses API adapter with Kode's three-level streaming architecture (Provider → Query → REPL)
- Enables real-time UI updates with text appearing progressively instead of all at once
- Supports TTFT (Time-To-First-Token) tracking for performance monitoring
- Matches Chat Completions streaming implementation pattern for consistency
- Resolves architectural mismatch between adapter pattern and streaming requirements

HOW:
- responsesAPI.ts: Implemented async *parseStreamingResponse() yielding events (message_start, text_delta, tool_request, usage, message_stop)
- base.ts: Added StreamingEvent type definition and optional parseStreamingResponse() to base class
- modelCapabilities.ts: Updated UnifiedResponse.content to accept string | Array<{type, text?, [key]: any}>
- parseResponse() maintains backward compatibility by calling buffered version
- All 14 tests pass with no regressions
2025-11-09 23:14:16 -08:00
Radon Co
be6477cca7 feat: Fix CLI crash and add OpenAI Responses API integration
WHAT: Fix critical CLI crash with content.filter() error and implement OpenAI Responses API integration with comprehensive testing

WHY: CLI was crashing with 'TypeError: undefined is not an object (evaluating "content.filter")' when using OpenAI models, preventing users from making API calls. Additionally needed proper Responses API support with reasoning tokens.

HOW:
• Fix content extraction from OpenAI response structure in legacy path
• Add JSON/Zod schema detection in responsesAPI adapter
• Create comprehensive test suite for both integration and production scenarios
• Document the new adapter architecture and usage

CRITICAL FIXES:
• claude.ts: Extract content from response.choices[0].message.content instead of undefined response.content
• responsesAPI.ts: Detect if schema is already JSON (has 'type' property) vs Zod schema before conversion

FILES:
• src/services/claude.ts - Critical bug fix for OpenAI response content extraction
• src/services/adapters/responsesAPI.ts - Robust schema detection for tool parameters
• src/test/integration-cli-flow.test.ts - Integration tests for full flow
• src/test/chat-completions-e2e.test.ts - End-to-end Chat Completions compatibility tests
• src/test/production-api-tests.test.ts - Production API tests with environment configuration
• docs/develop/modules/openai-adapters.md - New adapter system documentation
• docs/develop/README.md - Updated development documentation
2025-11-09 18:41:29 -08:00
Radon Co
7069893d14 feat(responses-api): Support OpenAI Responses API with proper parameter mapping
WHAT: Add support for OpenAI Responses API in Kode CLI adapter
WHY: Enable GPT-5 and similar models that require Responses API instead of Chat Completions; fix HTTP 400 errors and schema conversion failures
HOW: Fixed tool format to use flat structure matching API spec; added missing critical parameters (include array, parallel_tool_calls, store, tool_choice); implemented robust schema conversion handling both Zod and pre-built JSON schemas; added array-based content parsing for Anthropic compatibility; created comprehensive integration tests exercising the full claude.ts flow

AFFECTED FILES:
- src/services/adapters/responsesAPI.ts: Complete adapter implementation
- src/services/openai.ts: Simplified request handling
- src/test/integration-cli-flow.test.ts: New integration test suite
- src/test/responses-api-e2e.test.ts: Enhanced with production test capability

VERIFICATION:
- Integration tests pass: bun test src/test/integration-cli-flow.test.ts
- Production tests: PRODUCTION_TEST_MODE=true bun test src/test/responses-api-e2e.test.ts
2025-11-09 14:22:43 -08:00
Radon Co
3c9b0ec9d1 prompt(api): Add OpenAI Responses API support with SSE streaming
WHAT: Implement Responses API adapter with full SSE streaming support to enable Kode CLI working with GPT-5 and other models that require OpenAI Responses API format

WHY: GPT-5 and newer models use OpenAI Responses API (different from Chat Completions) which returns streaming SSE responses. Kode CLI needed a conversion layer to translate between Anthropic API format and OpenAI Responses API format for seamless model integration

HOW: Created ResponsesAPIAdapter that converts Anthropic UnifiedRequestParams to Responses API format (instructions, input array, max_output_tokens, stream=true), added SSE parser to collect streaming chunks and convert back to UnifiedResponse format. Fixed ModelAdapterFactory to properly select Responses API for GPT-5 models. Updated parseResponse to async across all adapters. Added production tests validating end-to-end conversion with actual API calls
2025-11-09 01:29:04 -08:00
Xinlu Lai
a4c3f16c2b
not coding, kode is everything 2025-11-05 03:09:11 +08:00
Xinlu Lai
e50c6f52f6
Merge pull request #107 from majiang213/main
fix: Fixed the issue where the build script would not create cli.js
2025-10-10 01:28:02 +08:00
Xinlu Lai
893112e43c
Merge pull request #110 from Mriris/main
fix: Ollama on Windows
2025-10-10 01:26:37 +08:00
若辰
f934cfa62e
Merge branch 'shareAI-lab:main' into main 2025-10-08 13:14:03 +08:00
Xinlu Lai
1b3b0786ca
Merge pull request #106 from wxhzzsf/wxh/qeury-anthropic-native-mcp-input-schema-fix
fix: add mcp support for anthropic native tool input schema
2025-10-07 13:48:52 +08:00
若辰
f486925c06 fix(PersistentShell): remove login shell arguments for MSYS and ensure correct CWD updates 2025-10-06 14:05:14 +08:00
若辰
ab0d3f26f3 fix(PersistentShell): improve command execution for WSL and MSYS environments 2025-10-06 13:47:28 +08:00
若辰
70f0d6b109 feat(ModelSelector): model context 2025-10-06 13:18:05 +08:00
若辰
451362256c fix(ModelSelector): ollama 2025-10-05 23:59:44 +08:00
MJ
fd1d6e385d fix: Fixed the issue where the build script would not create cli.js 2025-09-29 18:28:28 +08:00
Xiaohan Wang
ce8c8dad63 fix: add mcp support for anthropic native tool input schema 2025-09-29 16:55:43 +08:00
CrazyBoyM
b847352101 refactor: use tsconfig aliases throughout 2025-09-20 15:14:39 +08:00
CrazyBoyM
61a8ce0d22 clean code 2025-09-20 15:14:38 +08:00
CrazyBoyM
59dce97350 fix & remove ugly code 2025-09-20 15:14:38 +08:00
CrazyBoyM
d4abb2abee clean code 2025-09-20 15:14:38 +08:00
Xinlu Lai
78b49355cd
Merge pull request #89 from glide-the/update_build_docker
chore(docker): update Dockerfile to copy built application from dist …
2025-09-14 15:02:03 +08:00
glide-the
fbb2db6963 chore(docker): update Dockerfile to copy built application from dist and set entrypoint correctly\n\n- Adjusted COPY command to reference the new dist directory\n- Updated entrypoint to use the correct path for cli.js 2025-09-13 22:52:39 +08:00
CrazyBoyM
d0d1dca009 upgrade for win & remove some un-use code 2025-09-12 00:44:15 +08:00
CrazyBoyM
6bbaa6c559 fix win 2025-09-11 09:57:38 +08:00
CrazyBoyM
b0d9f58d76 fix(multi-edit): pass structuredPatch to update UI; harden FileEditToolUpdatedMessage against undefined patches\n\n- MultiEditTool: compute diff via getPatch and render standard update message\n- FileEditToolUpdatedMessage: treat missing structuredPatch as empty to avoid crash\n- Preserves existing feature and UI while eliminating reduce-on-undefined 2025-09-10 15:33:58 +08:00
CrazyBoyM
a7af9834ef chore(update): disable auto-update flow; keep version-check banner only\n\n- Remove AutoUpdater UI and NPM prefix/permissions flows\n- Simplify Doctor to passive health-check\n- Keep only getLatestVersion/assertMinVersion/update suggestions\n- Clean REPL/PromptInput to avoid extra renders and flicker\n- No hardcoding; no auto-install; docstrings tidy 2025-09-10 14:22:33 +08:00
Xinlu Lai
53234ba2d9
Merge pull request #67 from xiaomao87/fix-dockerfile
Dockerfile: fix runtime error `Module not found "/app/src/entrypoints/cli.tsx"`
2025-09-03 00:49:12 +08:00
Xinlu Lai
20111a0a26
Update README.zh-CN.md 2025-09-03 00:48:03 +08:00
Xinlu Lai
f857fb9577
Update README.md 2025-09-03 00:47:22 +08:00
dev
da15ca5a5f Dockerfile: fix runtime error Module not found "/app/src/entrypoints/cli.tsx" 2025-09-02 16:22:22 +08:00
Xinlu Lai
f23787f50d
Merge pull request #60 from burncloud/feat/burncloud
 feat: add BurnCloud as new AI model provider
2025-09-01 02:59:33 +08:00
Wei
32a9badecd feat: add new model provider BurnCloud fix default models 2025-08-30 16:01:14 +08:00
Xinlu Lai
5471b2d5fc
Update README.md 2025-08-30 01:38:51 +08:00
Xinlu Lai
97684a3208
Update README.md 2025-08-30 01:29:04 +08:00
Wei
efe00eee3b feat: add new model provider BurnCloud 2025-08-28 17:04:34 +08:00
249 changed files with 5305 additions and 4850 deletions

18
.env.example Normal file
View File

@ -0,0 +1,18 @@
# Environment Variables for Production API Tests
# Copy this file to .env and fill in your actual API keys
# Enable production test mode
PRODUCTION_TEST_MODE=true
# GPT-5 Codex Test Configuration
TEST_GPT5_API_KEY=your_gpt5_api_key_here
TEST_GPT5_BASE_URL=http://127.0.0.1:3000/openai
# MiniMax Codex Test Configuration
TEST_MINIMAX_API_KEY=your_minimax_api_key_here
TEST_MINIMAX_BASE_URL=https://api.minimaxi.com/v1
# WARNING:
# - Never commit .env files to version control!
# - The .env file is already in .gitignore
# - API keys should be kept secret and secure

19
.eslintrc.cjs Normal file
View File

@ -0,0 +1,19 @@
module.exports = {
extends: [
'eslint:recommended',
'@typescript-eslint/recommended',
],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
root: true,
env: {
node: true,
es2022: true,
},
rules: {
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-non-null-assertion': 'off',
},
ignorePatterns: ['dist/', 'node_modules/', 'cli.js'],
}

5
.husky/pre-commit Executable file
View File

@ -0,0 +1,5 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
bun run format:check
bun run typecheck

View File

@ -1,6 +1,6 @@
# AGENTS.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
This file provides guidance to Kode automation agents (including those compatible with Claude Code's `.claude` ecosystem) when working with code in this repository.
## Development Commands
@ -46,7 +46,7 @@ SKIP_BUNDLED_CHECK=true npm publish
## High-Level Architecture
### Core System Design
Kode implements a **three-layer parallel architecture** inspired by Claude Code:
Kode implements a **three-layer parallel architecture** refined for fast iteration across terminal workflows while remaining compatible with the Claude Code agent ecosystem:
1. **User Interaction Layer** (`src/screens/REPL.tsx`)
- Interactive terminal interface using Ink (React for CLI)
@ -74,9 +74,9 @@ Kode implements a **three-layer parallel architecture** inspired by Claude Code:
### Agent System (`src/utils/agentLoader.ts`)
**Dynamic Agent Configuration Loading** with 5-tier priority system:
1. Built-in (code-embedded)
2. `~/.claude/agents/` (Claude user)
2. `~/.claude/agents/` (Claude Code user directory compatibility)
3. `~/.kode/agents/` (Kode user)
4. `./.claude/agents/` (Claude project)
4. `./.claude/agents/` (Claude Code project directory compatibility)
5. `./.kode/agents/` (Kode project)
Agents are defined as markdown files with YAML frontmatter:
@ -99,7 +99,7 @@ Each tool follows a consistent pattern in `src/tools/[ToolName]/`:
- Permission-aware execution
### Service Layer
- **Claude Service** (`src/services/claude.ts`): Primary AI model interface
- **Anthropic Service** (`src/services/claude.ts`): Claude API integration
- **OpenAI Service** (`src/services/openai.ts`): OpenAI-compatible models
- **Model Adapter Factory** (`src/services/modelAdapterFactory.ts`): Unified model interface
- **MCP Client** (`src/services/mcpClient.ts`): Model Context Protocol for tool extensions
@ -204,4 +204,4 @@ const description = typeof tool.description === 'function'
1. Create `.md` file with proper YAML frontmatter
2. Place in appropriate directory based on scope
3. Test with `/agents` command
4. Verify tool permissions work correctly
4. Verify tool permissions work correctly

View File

@ -1,10 +1,11 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
This file provides guidance to Kode automation agents (including those compatible with Claude Code's `.claude` ecosystem) when working with code in this repository.
## Development Commands
### Essential Development Workflow
```bash
# Install dependencies
bun install
@ -15,6 +16,9 @@ bun run dev
# Build the CLI wrapper for distribution
bun run build
# Pre-release integration testing
bun link
# Clean build artifacts
bun run clean
@ -30,12 +34,17 @@ bun run format:check
```
### Build System Details
- **Primary Build Tool**: Bun (required for development)
- **Distribution**: Smart CLI wrapper (`cli.js`) that prefers Bun but falls back to Node.js with tsx loader
- **Entry Point**: `src/entrypoints/cli.tsx`
- **Build Output**: `cli.js` (executable wrapper) and `.npmrc` (npm configuration)
- **Build Output**:
- `cli.js` - Cross-platform executable wrapper that uses `process.cwd()` as working directory
- `.npmrc` - npm configuration file with `package-lock=false` and `save-exact=true`
- `dist/` - ESM modules compiled from TypeScript sources
### Publishing
```bash
# Publish to npm (requires build first)
npm publish
@ -46,7 +55,7 @@ SKIP_BUNDLED_CHECK=true npm publish
## High-Level Architecture
### Core System Design
Kode implements a **three-layer parallel architecture** inspired by Claude Code:
Kode implements a **three-layer parallel architecture** refined for fast iteration across terminal workflows while remaining compatible with the Claude Code agent ecosystem:
1. **User Interaction Layer** (`src/screens/REPL.tsx`)
- Interactive terminal interface using Ink (React for CLI)
@ -74,9 +83,9 @@ Kode implements a **three-layer parallel architecture** inspired by Claude Code:
### Agent System (`src/utils/agentLoader.ts`)
**Dynamic Agent Configuration Loading** with 5-tier priority system:
1. Built-in (code-embedded)
2. `~/.claude/agents/` (Claude user)
2. `~/.claude/agents/` (Claude Code user directory compatibility)
3. `~/.kode/agents/` (Kode user)
4. `./.claude/agents/` (Claude project)
4. `./.claude/agents/` (Claude Code project directory compatibility)
5. `./.kode/agents/` (Kode project)
Agents are defined as markdown files with YAML frontmatter:
@ -99,7 +108,7 @@ Each tool follows a consistent pattern in `src/tools/[ToolName]/`:
- Permission-aware execution
### Service Layer
- **Claude Service** (`src/services/claude.ts`): Primary AI model interface
- **Anthropic Service** (`src/services/claude.ts`): Claude API integration
- **OpenAI Service** (`src/services/openai.ts`): OpenAI-compatible models
- **Model Adapter Factory** (`src/services/modelAdapterFactory.ts`): Unified model interface
- **MCP Client** (`src/services/mcpClient.ts`): Model Context Protocol for tool extensions
@ -112,6 +121,7 @@ Each tool follows a consistent pattern in `src/tools/[ToolName]/`:
- CLI parameter overrides
- Multi-model profile management
### Context Management
- **Message Context Manager** (`src/utils/messageContextManager.ts`): Intelligent context window handling
- **Memory Tools** (`src/tools/MemoryReadTool/`, `src/tools/MemoryWriteTool/`): Persistent memory across sessions
@ -204,4 +214,24 @@ const description = typeof tool.description === 'function'
1. Create `.md` file with proper YAML frontmatter
2. Place in appropriate directory based on scope
3. Test with `/agents` command
4. Verify tool permissions work correctly
4. Verify tool permissions work correctly
### Testing in Other Projects
After making changes, test the CLI in different environments:
1. **Development Testing**:
```bash
bun run build # Build with updated cli.js wrapper
bun link # Link globally for testing
```
2. **Test in External Project**:
```bash
cd /path/to/other/project
kode --help # Verify CLI works and uses correct working directory
```
3. **Verify Working Directory**:
- CLI wrapper uses `process.cwd()` to ensure commands run in user's current directory
- Not in Kode's installation directory
- Essential for file operations and project context

View File

@ -69,18 +69,19 @@ RUN npm install -g tsx
WORKDIR /workspace
# Copy built application from builder stage
COPY --from=builder /app/cli.js /app/cli.js
COPY --from=builder /app/dist /app/dist
COPY --from=builder /app/package.json /app/package.json
COPY --from=builder /app/node_modules /app/node_modules
COPY --from=builder /app/src /app/src
# Create the entrypoint script
RUN cat << 'EOF' > /entrypoint.sh
#!/bin/sh
# RUN cat << 'EOF' > /entrypoint.sh
# #!/bin/sh
/root/.bun/bin/bun /app/cli.js -c /workspace "$@"
EOF
# /root/.bun/bin/bun /app/dist/entrypoints/cli.js -c /workspace "$@"
# EOF
RUN chmod +x /entrypoint.sh
# RUN chmod +x /entrypoint.sh
# Set the entrypoint
ENTRYPOINT ["/entrypoint.sh"]
ENTRYPOINT ["/root/.bun/bin/bun", "/app/dist/entrypoints/cli.js", "-c", "/workspace"]

View File

@ -1,30 +1,26 @@
# Kode - AI Assistant for Your Terminal
# Kode - AI Coding
<img width="991" height="479" alt="image" src="https://github.com/user-attachments/assets/c1751e92-94dc-4e4a-9558-8cd2d058c1a1" /> <br>
[![npm version](https://badge.fury.io/js/@shareai-lab%2Fkode.svg)](https://www.npmjs.com/package/@shareai-lab/kode)
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
[![AGENTS.md](https://img.shields.io/badge/AGENTS.md-Compatible-brightgreen)](https://agents.md)
[中文文档](README.zh-CN.md) | [Contributing](CONTRIBUTING.md) | [Documentation](docs/)
## 🎉 Big Announcement: We're Now Apache 2.0 Licensed!
<img width="90%" alt="image" src="https://github.com/user-attachments/assets/fdce7017-8095-429d-b74e-07f43a6919e1" />
**Great news for the developer community!** In our commitment to democratizing AI agent technology and fostering a vibrant ecosystem of innovation, we're thrilled to announce that Kode has transitioned from AGPLv3 to the **Apache 2.0 license**.
<img width="90%" alt="2c0ad8540f2872d197c7b17ae23d74f5" src="https://github.com/user-attachments/assets/f220cc27-084d-468e-a3f4-d5bc44d84fac" />
### What This Means for You:
- ✅ **Complete Freedom**: Use Kode in any project - personal, commercial, or enterprise
- ✅ **Build Without Barriers**: Create proprietary solutions without open-sourcing requirements
- ✅ **Simple Attribution**: Just maintain copyright notices and license info
- ✅ **Join a Movement**: Be part of accelerating the world's transition to AI-powered development
<img width="90%" alt="f266d316d90ddd0db5a3d640c1126930" src="https://github.com/user-attachments/assets/90ec7399-1349-4607-b689-96613b3dc3e2" />
<img width="90%" alt="image" src="https://github.com/user-attachments/assets/b30696ce-5ab1-40a0-b741-c7ef3945dba0" />
This change reflects our belief that the future of software development is collaborative, open, and augmented by AI. By removing licensing barriers, we're empowering developers worldwide to build the next generation of AI-assisted tools and workflows. Let's build the future together! 🚀
## 📢 Update Log
**2025-08-29**: We've added Windows support! All Windows users can now run Kode using Git Bash, Unix subsystems, or WSL (Windows Subsystem for Linux) on their computers.
<img width="606" height="303" alt="image" src="https://github.com/user-attachments/assets/6cf50553-aacd-4241-a579-6e935b6c62b5" />
## 🤝 AGENTS.md Standard Support
**Kode proudly supports the [AGENTS.md standard protocol](https://agents.md) initiated by OpenAI** - a simple, open format for guiding programming agents that's used by 20k+ open source projects.
@ -32,17 +28,17 @@ This change reflects our belief that the future of software development is colla
### Full Compatibility with Multiple Standards
- ✅ **AGENTS.md** - Native support for the OpenAI-initiated standard format
- ✅ **CLAUDE.md** - Full backward compatibility with Claude Code configurations
- ✅ **CLAUDE.md** - Full backward compatibility with Claude Code `.claude` configurations
- ✅ **Subagent System** - Advanced agent delegation and task orchestration
- ✅ **Cross-platform** - Works with 20+ AI models and providers
Use `# Your documentation request` to generate and maintain your AGENTS.md file automatically, while maintaining full compatibility with existing Claude Code workflows.
Use `# Your documentation request` to generate and maintain your AGENTS.md file automatically, while preserving compatibility with existing `.claude` workflows.
## Overview
Kode is a powerful AI assistant that lives in your terminal. It can understand your codebase, edit files, run commands, and handle entire workflows for you.
> **⚠️ Security Notice**: Kode runs in YOLO mode by default (equivalent to Claude's `--dangerously-skip-permissions` flag), bypassing all permission checks for maximum productivity. YOLO mode is recommended only for trusted, secure environments when working on non-critical projects. If you're working with important files or using models of questionable capability, we strongly recommend using `kode --safe` to enable permission checks and manual approval for all operations.
> **⚠️ Security Notice**: Kode runs in YOLO mode by default (equivalent to Claude Code's `--dangerously-skip-permissions` flag), bypassing all permission checks for maximum productivity. YOLO mode is recommended only for trusted, secure environments when working on non-critical projects. If you're working with important files or using models of questionable capability, we strongly recommend using `kode --safe` to enable permission checks and manual approval for all operations.
>
> **📊 Model Performance**: For optimal performance, we recommend using newer, more capable models designed for autonomous task completion. Avoid older Q&A-focused models like GPT-4o or Gemini 2.5 Pro, which are optimized for answering questions rather than sustained independent task execution. Choose models specifically trained for agentic workflows and extended reasoning capabilities.
@ -89,20 +85,6 @@ Our state-of-the-art completion system provides unparalleled coding assistance:
## Installation
### Recommended: Using Bun (Fastest)
First install Bun if you haven't already:
```bash
curl -fsSL https://bun.sh/install | bash
```
Then install Kode:
```bash
bun add -g @shareai-lab/kode
```
### Alternative: Using npm
```bash
npm install -g @shareai-lab/kode
```
@ -112,6 +94,18 @@ After installation, you can use any of these commands:
- `kwa` - Kode With Agent (alternative)
- `kd` - Ultra-short alias
### Windows Notes
- Install Git for Windows to provide a Bash (Unixlike) environment: https://git-scm.com/download/win
- Kode automatically prefers Git Bash/MSYS or WSL Bash when available.
- If neither is available, it will fall back to your default shell, but many features work best with Bash.
- Use VS Codes integrated terminal rather than legacy Command Prompt (cmd):
- Better font rendering and icon support.
- Fewer path and encoding quirks compared to cmd.
- Select “Git Bash” as the VS Code terminal shell when possible.
- Optional: If you install globally via npm, avoid spaces in the global prefix path to prevent shim issues.
- Example: `npm config set prefix "C:\\npm"` and reinstall global packages.
## Usage
### Interactive Mode
@ -127,9 +121,9 @@ kd
### Non-Interactive Mode
Get a quick response:
```bash
kode -p "explain this function" main.js
kode -p "explain this function" path/to/file.js
# or
kwa -p "explain this function" main.js
kwa -p "explain this function" path/to/file.js
```
### Using the @ Mention System

View File

@ -23,7 +23,7 @@
Kode 是一个强大的 AI 助手,运行在你的终端中。它能理解你的代码库、编辑文件、运行命令,并为你处理整个开发工作流。
> **⚠️ 安全提示**Kode 默认以 YOLO 模式运行(等同于 Claude 的 `--dangerously-skip-permissions` 标志跳过所有权限检查以获得最大生产力。YOLO 模式仅建议在安全可信的环境中处理非重要项目时使用。如果您正在处理重要文件或使用能力存疑的模型,我们强烈建议使用 `kode --safe` 启用权限检查和手动审批所有操作。
> **⚠️ 安全提示**Kode 默认以 YOLO 模式运行(等同于 Claude Code `--dangerously-skip-permissions` 标志跳过所有权限检查以获得最大生产力。YOLO 模式仅建议在安全可信的环境中处理非重要项目时使用。如果您正在处理重要文件或使用能力存疑的模型,我们强烈建议使用 `kode --safe` 启用权限检查和手动审批所有操作。
>
> **📊 模型性能建议**:为获得最佳体验,建议使用专为自主任务完成设计的新一代强大模型。避免使用 GPT-4o、Gemini 2.5 Pro 等较老的问答型模型,它们主要针对回答问题进行优化,而非持续的独立任务执行。请选择专门训练用于智能体工作流和扩展推理能力的模型。
@ -41,20 +41,6 @@ Kode 是一个强大的 AI 助手,运行在你的终端中。它能理解你
## 安装
### 推荐方式:使用 Bun最快
首先安装 Bun如果尚未安装
```bash
curl -fsSL https://bun.sh/install | bash
```
然后安装 Kode
```bash
bun add -g @shareai-lab/kode
```
### 备选方式:使用 npm
```bash
npm install -g @shareai-lab/kode
```
@ -64,6 +50,17 @@ npm install -g @shareai-lab/kode
- `kwa` - Kode With Agent备选
- `kd` - 超短别名
### Windows 提示
- 请安装 Git for Windows包含 Git Bash 类 Unix 终端https://git-scm.com/download/win
- Kode 会优先使用 Git Bash/MSYS 或 WSL Bash没有时会回退到默认终端但在 Bash 下体验更佳。
- 推荐在 VS Code 的集成终端中运行(而非系统默认的 cmd
- 字体与图标显示更稳定UI 体验更好。
- 相比 cmd 路径/编码等兼容性问题更少。
- 在 VS Code 终端中选择 “Git Bash” 作为默认 Shell。
- 可选:若通过 npm 全局安装,建议避免将 npm 全局 prefix 设置在含空格的路径,以免生成的可执行 shim 出现路径解析问题。
- 示例:`npm config set prefix "C:\\npm"`,然后重新安装全局包。
## 使用方法
### 交互模式
@ -79,9 +76,9 @@ kd
### 非交互模式
获取快速响应:
```bash
kode -p "解释这个函数" main.js
kode -p "解释这个函数" 路径/到/文件.js
# 或
kwa -p "解释这个函数" main.js
kwa -p "解释这个函数" 路径/到/文件.js
```
### Docker 使用说明

202
docs/PUBLISH_GUIDE.md Normal file
View File

@ -0,0 +1,202 @@
# 发包脚本使用指南
Kode 项目提供了两套发包流程专注于npm发布不涉及git操作
## 🚀 快速使用
### 开发版本发布 (测试用)
```bash
npm run publish:dev
```
### 正式版本发布
```bash
npm run publish:release
```
## 📦 发包策略
### 1. 开发版本 (`dev` tag)
- **目的**: 内部测试和预发布验证
- **版本格式**: `1.1.16-dev.1`, `1.1.16-dev.2`
- **安装方式**: `npm install -g @shareai-lab/kode@dev`
- **特点**:
- 自动递增 dev 版本号
- 不影响正式版本的用户
- 可以快速迭代测试
- 临时修改package.json发布后自动恢复
### 2. 正式版本 (`latest` tag)
- **目的**: 面向最终用户的稳定版本
- **版本格式**: `1.1.16`, `1.1.17`, `1.2.0`
- **安装方式**: `npm install -g @shareai-lab/kode` (默认)
- **特点**:
- 语义化版本控制
- 严格的发布流程
- 包含完整的测试和检查
- 永久更新package.json版本号
## 🛠️ 脚本功能详解
### 开发版本发布 (`scripts/publish-dev.js`)
**自动化流程**:
1. 🔢 读取当前基础版本
2. 📊 查询npm上现有的dev版本自动递增
3. 📝 临时更新package.json版本号
4. 🔨 构建项目
5. 🔍 运行预发布检查
6. 📤 发布到npm的`dev` tag
7. 🔄 恢复package.json到原始版本
**使用场景**:
- 功能开发完成,需要内部测试
- PR验证前的最终测试
- 快速修复验证
**安全特性**:
- 临时修改package.json发布后自动恢复
- 失败时自动回滚
- 不会改变本地版本状态
### 正式版本发布 (`scripts/publish-release.js`)
**交互式流程**:
1. 📦 显示当前版本
2. 🔢 选择版本升级类型:
- **patch** (1.1.16 → 1.1.17): 修复 bug
- **minor** (1.1.16 → 1.2.0): 新功能
- **major** (1.1.16 → 2.0.0): 破坏性变更
- **custom**: 自定义版本号
3. ✅ 检查版本是否已在npm上存在
4. 🤔 确认发布信息
5. 📝 更新package.json版本号永久
6. 🧪 运行测试和类型检查
7. 🔨 构建项目
8. 🔍 运行预发布检查
9. 📤 发布到npm (默认`latest` tag)
10. 💡 提示后续git操作建议
**安全特性**:
- 交互式确认,避免误发布
- 检查版本冲突
- 测试失败时自动回滚版本号
- 永久保留版本更改供git提交
## 🎯 最佳实践
### 开发流程建议
```bash
# 1. 开发功能
# ... 开发代码 ...
# 2. 发布开发版本测试
npm run publish:dev
# 安装测试: npm install -g @shareai-lab/kode@dev
# 3. 测试通过后发布正式版本
npm run publish:release
# 4. 手动处理git操作如需要
git add package.json
git commit -m "chore: bump version to x.x.x"
git tag vx.x.x
git push origin main
git push origin vx.x.x
```
### 版本号管理
- **开发版**: 基于当前正式版本自动递增 (1.1.16-dev.1 → 1.1.16-dev.2)
- **正式版**: 遵循 [语义化版本](https://semver.org/lang/zh-CN/) 规范
- **版本检查**: 自动检查npm上是否已存在相同版本
### 标签管理
```bash
# 查看所有版本
npm view @shareai-lab/kode versions --json
# 查看 dev 版本
npm view @shareai-lab/kode@dev version
# 查看最新正式版本
npm view @shareai-lab/kode@latest version
```
## 🔧 故障排除
### 常见问题
**发布失败怎么办?**
- 开发版本脚本会自动回滚package.json
- 正式版本:检查错误信息,修复后重新运行
**版本号冲突?**
- 开发版本会自动递增,不会冲突
- 正式版本发布前会检查是否已存在
**权限问题?**
- 确保已登录npm: `npm whoami`
- 确保有包的发布权限: `npm owner ls @shareai-lab/kode`
**测试失败?**
- 正式版本发布会自动回滚版本号
- 修复测试问题后重新运行
### 手动操作
```bash
# 查看当前登录用户
npm whoami
# 登录npm
npm login
# 查看包信息
npm view @shareai-lab/kode
# 手动发布(不推荐)
npm publish --tag dev # 开发版本
npm publish # 正式版本
```
## 📊 监控和分析
```bash
# 查看包下载统计
npm view @shareai-lab/kode
# 查看所有版本的详细信息
npm view @shareai-lab/kode versions --json
# 测试安装
npm install -g @shareai-lab/kode@dev
kode --version
```
## ⚙️ Git集成建议
虽然发包脚本不包含git操作但建议的git工作流程
**开发版本发布后**:
```bash
# 开发版本不需要git操作package.json已自动恢复
# 继续开发即可
```
**正式版本发布后**:
```bash
# package.json已更新版本号建议提交
git add package.json
git commit -m "chore: bump version to $(node -p "require('./package.json').version")"
git tag "v$(node -p "require('./package.json').version")"
git push origin main
git push origin --tags
```
---
通过这套纯npm发包系统你可以
- 🚀 快速发布开发版本进行内部测试
- 🛡️ 安全发布正式版本给最终用户
- 📈 保持清晰的版本管理
- ⚡ 专注于包发布git操作完全可控
- 🔄 灵活的版本回滚和恢复机制

View File

@ -74,8 +74,8 @@ Agents can be defined at five levels with priority order (later overrides earlie
- Provided with Kode
- Cannot be modified
2. **Claude User** (`~/.claude/agents/`)
- Claude Code compatible user-level agents
2. **.claude User (Claude Code)** (`~/.claude/agents/`)
- Compatibility with Claude Code user directories
- Personal agents available across all projects
3. **Kode User** (`~/.kode/agents/`)
@ -83,8 +83,8 @@ Agents can be defined at five levels with priority order (later overrides earlie
- Overrides Claude user agents with same name
- Create with `/agents` command or manually
4. **Claude Project** (`./.claude/agents/`)
- Claude Code compatible project-specific agents
4. **.claude Project (Claude Code)** (`./.claude/agents/`)
- Compatibility with Claude Code project directories
- Overrides user-level agents
5. **Kode Project** (`./.kode/agents/`)
@ -227,4 +227,4 @@ Planned improvements:
- Performance metrics per agent
- Agent composition (agents using other agents)
- Cloud-based agent sharing
- Agent versioning and rollback
- Agent versioning and rollback

View File

@ -17,6 +17,7 @@ This comprehensive documentation provides a complete understanding of the Kode c
- **[Model Management](./modules/model-management.md)** - Multi-provider AI model integration and intelligent switching
- **[MCP Integration](./modules/mcp-integration.md)** - Model Context Protocol for third-party tool integration
- **[Custom Commands](./modules/custom-commands.md)** - Markdown-based extensible command system
- **[OpenAI Adapter Layer](./modules/openai-adapters.md)** - Anthropic-to-OpenAI request translation for Chat Completions and Responses API
### Core Modules
@ -216,4 +217,4 @@ For questions or issues:
---
This documentation represents the complete technical understanding of the Kode system as of the current version. It serves as the authoritative reference for developers working on or with the Kode codebase.
This documentation represents the complete technical understanding of the Kode system as of the current version. It serves as the authoritative reference for developers working on or with the Kode codebase.

View File

@ -0,0 +1,63 @@
# OpenAI Adapter Layer
This module explains how Kodes Anthropic-first conversation engine can selectively route requests through OpenAI Chat Completions or the new Responses API without exposing that complexity to the rest of the system. The adapter layer only runs when `USE_NEW_ADAPTERS !== 'false'` and a `ModelProfile` is available.
## Goals
- Preserve Anthropic-native data structures (`AssistantMessage`, `MessageParam`, tool blocks) everywhere outside the adapter layer.
- Translate those structures into a provider-neutral `UnifiedRequestParams` shape so different adapters can share logic.
- Map the unified format onto each providers transport (Chat Completions vs Responses API) and back into Anthropic-style `AssistantMessage` objects.
## Request Flow
1. **Anthropic Messages → Unified Params**
`queryOpenAI` (`src/services/claude.ts`) converts the existing Anthropic message history into OpenAI-style role/content pairs via `convertAnthropicMessagesToOpenAIMessages`, flattens system prompts, and builds a `UnifiedRequestParams` bundle (see `src/types/modelCapabilities.ts`). This bundle captures:
- `messages`: already normalized to OpenAI format but still provider-neutral inside the adapters.
- `systemPrompt`: array of strings, preserving multi-block Anthropic system prompts.
- `tools`: tool metadata (names, descriptions, JSON schema) fetched once so adapters can reshape it.
- `maxTokens`, `stream`, `reasoningEffort`, `verbosity`, `previousResponseId`, and `temperature` flags.
2. **Adapter Selection**
`ModelAdapterFactory` inspects the `ModelProfile` and capability table (`src/constants/modelCapabilities.ts`) to choose either:
- `ChatCompletionsAdapter` for classic `/chat/completions` style providers.
- `ResponsesAPIAdapter` when the provider natively supports `/responses`.
3. **Adapter-Specific Request Construction**
- **Chat Completions (`src/services/adapters/chatCompletions.ts`)**
- Reassembles a single message list including system prompts.
- Picks the correct max-token field (`max_tokens` vs `max_completion_tokens`).
- Attaches OpenAI function-calling tool descriptors, optional `stream_options`, reasoning effort, and verbosity when supported.
- Handles model quirks (e.g., removes unsupported fields for `o1` models).
- **Responses API (`src/services/adapters/responsesAPI.ts`)**
- Converts chat-style messages into `input` items (message blocks, function-call outputs, images).
- Moves system prompts into the `instructions` string.
- Uses `max_output_tokens`, always enables streaming, and adds `include` entries for reasoning envelopes.
- Emits the flat `tools` array expected by `/responses`, `tool_choice`, `parallel_tool_calls`, state IDs, verbosity controls, etc.
4. **Transport**
Both adapters delegate the actual network call to helpers in `src/services/openai.ts`:
- Chat Completions requests use `getCompletionWithProfile` (legacy path) or the same helper `queryOpenAI` previously relied on.
- Responses API requests go through `callGPT5ResponsesAPI`, which POSTs the adapter-built payload and returns the raw `Response` object for streaming support.
## Response Flow
1. **Raw Response → Unified Response**
- `ChatCompletionsAdapter.parseResponse` pulls the first `choice`, extracts tool calls, and normalizes usage counts.
- `ResponsesAPIAdapter.parseResponse` distinguishes between streaming vs JSON responses:
- Streaming: incrementally decode SSE chunks, concatenate `response.output_text.delta`, and capture completed tool calls.
- JSON: fold `output` message items into text blocks, gather tool-call items, and preserve `usage`/`response.id` for stateful follow-ups.
- Both return a `UnifiedResponse` containing `content`, `toolCalls`, token usage, and optional `responseId`.
2. **Unified Response → Anthropic AssistantMessage**
Back in `queryOpenAI`, the unified response is wrapped in Anthropics schema: `content` becomes Ink-ready blocks, tool calls become `tool_use` entries, and usage numbers flow into `AssistantMessage.message.usage`. Consumers (UI, TaskTool, etc.) continue to see only Anthropic-style messages.
## Legacy Fallbacks
- If `USE_NEW_ADAPTERS === 'false'` or no `ModelProfile` is available, the system bypasses adapters entirely and hits `getCompletionWithProfile` / `getGPT5CompletionWithProfile`. These paths still rely on helper utilities in `src/services/openai.ts`.
- `ResponsesAPIAdapter` also carries compatibility flags (e.g., `previousResponseId`, `parallel_tool_calls`) so a single unified params structure works across official OpenAI and third-party providers.
## When to Extend This Layer
- **New OpenAI-style providers**: add capability metadata and, if necessary, a specialized adapter that extends `ModelAPIAdapter`.
- **Model-specific quirks**: keep conversions inside the adapter so upstream Anthropic abstractions stay untouched.
- **Stateful Responses**: leverage the `responseId` surfaced by `UnifiedResponse` to support follow-up calls that require `previous_response_id`.

View File

@ -1,13 +1,13 @@
{
"name": "@shareai-lab/kode",
"version": "1.0.80",
"version": "1.1.23",
"bin": {
"kode": "cli.js",
"kwa": "cli.js",
"kd": "cli.js"
},
"engines": {
"node": ">=18.0.0"
"node": ">=20.18.1"
},
"main": "cli.js",
"author": "ShareAI-lab <ai-lab@foxmail.com>",
@ -24,26 +24,25 @@
"files": [
"cli.js",
"yoga.wasm",
"src/**/*",
"dist/**/*",
"scripts/postinstall.js",
".npmrc"
],
"scripts": {
"dev": "bun run ./src/entrypoints/cli.tsx --verbose",
"build": "bun run scripts/build.ts",
"build": "node scripts/build.mjs",
"clean": "rm -rf cli.js",
"prepublishOnly": "bun run build && node scripts/prepublish-check.js",
"prepublishOnly": "node scripts/build.mjs && node scripts/prepublish-check.js",
"postinstall": "node scripts/postinstall.js || true",
"format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json}\"",
"format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,json}\"",
"lint": "eslint . --ext .ts,.tsx,.js --max-warnings 0",
"lint:fix": "eslint . --ext .ts,.tsx,.js --fix",
"test": "bun test",
"typecheck": "tsc --noEmit"
},
"optionalDependencies": {
"@img/sharp-darwin-arm64": "^0.33.5",
"@img/sharp-linux-arm": "^0.33.5",
"@img/sharp-linux-x64": "^0.33.5",
"@img/sharp-win32-x64": "^0.33.5"
"typecheck": "tsc --noEmit",
"prepare": "",
"publish:dev": "node scripts/publish-dev.js",
"publish:release": "node scripts/publish-release.js"
},
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.12.6",
@ -52,7 +51,6 @@
"@commander-js/extra-typings": "^13.1.0",
"@inkjs/ui": "^2.0.0",
"@modelcontextprotocol/sdk": "^1.15.1",
"@statsig/js-client": "^3.18.2",
"@types/lodash-es": "^4.17.12",
"@types/react": "^19.1.8",
"ansi-escapes": "^7.0.0",
@ -83,8 +81,10 @@
"semver": "^7.7.2",
"shell-quote": "^1.8.3",
"spawn-rx": "^5.1.2",
"string-width": "^7.2.0",
"strip-ansi": "^7.1.0",
"tsx": "^4.20.3",
"turndown": "^7.2.1",
"turndown": "^7.2.0",
"undici": "^7.11.0",
"wrap-ansi": "^9.0.0",
"zod": "^3.25.76",
@ -95,25 +95,8 @@
"@types/jest": "^30.0.0",
"@types/node": "^24.1.0",
"bun-types": "latest",
"esbuild": "^0.25.9",
"prettier": "^3.6.2",
"typescript": "^5.9.2"
},
"overrides": {
"string-width": "^7.2.0",
"strip-ansi": "^7.1.0"
},
"directories": {
"doc": "docs",
"test": "test"
},
"keywords": [
"cli",
"ai",
"assistant",
"agent",
"kode",
"shareai",
"terminal",
"command-line"
]
}
}
}

195
scripts/build.mjs Normal file
View File

@ -0,0 +1,195 @@
#!/usr/bin/env node
import { build } from 'esbuild'
import { existsSync, mkdirSync, writeFileSync, cpSync, readFileSync, readdirSync, statSync, chmodSync } from 'node:fs'
import { join } from 'node:path'
const SRC_DIR = 'src'
const OUT_DIR = 'dist'
function collectEntries(dir, acc = []) {
const items = readdirSync(dir)
for (const name of items) {
const p = join(dir, name)
const st = statSync(p)
if (st.isDirectory()) {
// skip tests and storybook or similar folders if any, adjust as needed
if (name === 'test' || name === '__tests__') continue
collectEntries(p, acc)
} else if (st.isFile()) {
if (p.endsWith('.ts') || p.endsWith('.tsx')) acc.push(p)
}
}
return acc
}
function fixRelativeImports(dir) {
const items = readdirSync(dir)
for (const name of items) {
const p = join(dir, name)
const st = statSync(p)
if (st.isDirectory()) {
fixRelativeImports(p)
continue
}
if (!p.endsWith('.js')) continue
let text = readFileSync(p, 'utf8')
// Handle: from '...'
text = text.replace(/(from\s+['"])(\.{1,2}\/[^'"\n]+)(['"])/gm, (m, a, spec, c) => {
if (/\.(js|json|node|mjs|cjs)$/.test(spec)) return m
return a + spec + '.js' + c
})
// Handle: export ... from '...'
text = text.replace(/(export\s+[^;]*?from\s+['"])(\.{1,2}\/[^'"\n]+)(['"])/gm, (m, a, spec, c) => {
if (/\.(js|json|node|mjs|cjs)$/.test(spec)) return m
return a + spec + '.js' + c
})
// Handle: dynamic import('...')
text = text.replace(/(import\(\s*['"])(\.{1,2}\/[^'"\n]+)(['"]\s*\))/gm, (m, a, spec, c) => {
if (/\.(js|json|node|mjs|cjs)$/.test(spec)) return m
return a + spec + '.js' + c
})
writeFileSync(p, text)
}
}
async function main() {
console.log('🚀 Building Kode CLI for cross-platform compatibility...')
if (!existsSync(OUT_DIR)) mkdirSync(OUT_DIR, { recursive: true })
const entries = collectEntries(SRC_DIR)
// Build ESM format but ensure Node.js compatibility
await build({
entryPoints: entries,
outdir: OUT_DIR,
outbase: SRC_DIR,
bundle: false,
platform: 'node',
format: 'esm',
target: ['node20'],
sourcemap: true,
legalComments: 'none',
logLevel: 'info',
tsconfig: 'tsconfig.json',
})
// Fix relative import specifiers to include .js extension for ESM
fixRelativeImports(OUT_DIR)
// Mark dist as ES module
writeFileSync(join(OUT_DIR, 'package.json'), JSON.stringify({
type: 'module',
main: './entrypoints/cli.js'
}, null, 2))
// Create a proper entrypoint - ESM with async handling
const mainEntrypoint = join(OUT_DIR, 'index.js')
writeFileSync(mainEntrypoint, `#!/usr/bin/env node
import('./entrypoints/cli.js').catch(err => {
console.error('❌ Failed to load CLI:', err.message);
process.exit(1);
});
`)
// Copy yoga.wasm alongside outputs
try {
cpSync('yoga.wasm', join(OUT_DIR, 'yoga.wasm'))
console.log('✅ yoga.wasm copied to dist')
} catch (err) {
console.warn('⚠️ Could not copy yoga.wasm:', err.message)
}
// Create cross-platform CLI wrapper
const cliWrapper = `#!/usr/bin/env node
// Cross-platform CLI wrapper for Kode
// Prefers Bun but falls back to Node.js with tsx loader
const { spawn } = require('child_process');
const { existsSync } = require('fs');
const path = require('path');
// Get the directory where this CLI script is installed
const kodeDir = __dirname;
const distPath = path.join(kodeDir, 'dist', 'index.js');
// Check if we have a built version
if (!existsSync(distPath)) {
console.error('❌ Built files not found. Run "bun run build" first.');
process.exit(1);
}
// Try to use Bun first, then fallback to Node.js with tsx
const runWithBun = () => {
const proc = spawn('bun', ['run', distPath, ...process.argv.slice(2)], {
stdio: 'inherit',
cwd: process.cwd() // Use current working directory, not kode installation directory
});
proc.on('error', (err) => {
if (err.code === 'ENOENT') {
// Bun not found, try Node.js
runWithNode();
} else {
console.error('❌ Failed to start with Bun:', err.message);
process.exit(1);
}
});
proc.on('close', (code) => {
process.exit(code);
});
};
const runWithNode = () => {
const proc = spawn('node', [distPath, ...process.argv.slice(2)], {
stdio: 'inherit',
cwd: process.cwd() // Use current working directory, not kode installation directory
});
proc.on('error', (err) => {
console.error('❌ Failed to start with Node.js:', err.message);
process.exit(1);
});
proc.on('close', (code) => {
process.exit(code);
});
};
// Start with Bun preference
runWithBun();
`;
writeFileSync('cli.js', cliWrapper);
// Make cli.js executable
try {
chmodSync('cli.js', 0o755);
console.log('✅ cli.js made executable');
} catch (err) {
console.warn('⚠️ Could not make cli.js executable:', err.message);
}
// Create .npmrc file
const npmrcContent = `# Kode npm configuration
package-lock=false
save-exact=true
`;
writeFileSync('.npmrc', npmrcContent);
console.log('✅ Build completed for cross-platform compatibility!')
console.log('📋 Generated files:')
console.log(' - dist/ (ESM modules)')
console.log(' - dist/index.js (main entrypoint)')
console.log(' - dist/entrypoints/cli.js (CLI main)')
console.log(' - cli.js (cross-platform wrapper)')
console.log(' - .npmrc (npm configuration)')
}
main().catch(err => {
console.error('❌ Build failed:', err)
process.exit(1)
})

View File

@ -1,103 +0,0 @@
#!/usr/bin/env bun
import { existsSync, rmSync, writeFileSync, chmodSync } from 'fs';
async function build() {
console.log('🚀 Building Kode CLI...\n');
try {
// Clean previous builds
console.log('🧹 Cleaning previous builds...');
['cli.js', '.npmrc'].forEach(file => {
if (existsSync(file)) {
rmSync(file, { recursive: true, force: true });
}
});
// Create the CLI wrapper
const wrapper = `#!/usr/bin/env node
const { spawn } = require('child_process');
const path = require('path');
// Prefer bun if available, otherwise use node with loader
const args = process.argv.slice(2);
const cliPath = path.join(__dirname, 'src', 'entrypoints', 'cli.tsx');
// Try bun first
try {
const { execSync } = require('child_process');
execSync('bun --version', { stdio: 'ignore' });
// Bun is available
const child = spawn('bun', ['run', cliPath, ...args], {
stdio: 'inherit',
env: {
...process.env,
YOGA_WASM_PATH: path.join(__dirname, 'yoga.wasm')
}
});
child.on('exit', (code) => process.exit(code || 0));
child.on('error', () => {
// Fallback to node if bun fails
runWithNode();
});
} catch {
// Bun not available, use node
runWithNode();
}
function runWithNode() {
// Use local tsx installation
const tsxPath = path.join(__dirname, 'node_modules', '.bin', 'tsx');
const child = spawn(tsxPath, [cliPath, ...args], {
stdio: 'inherit',
env: {
...process.env,
YOGA_WASM_PATH: path.join(__dirname, 'yoga.wasm')
}
});
child.on('error', (err) => {
if (err.code === 'ENOENT') {
console.error('\\nError: tsx is required but not found.');
console.error('Please run: npm install');
process.exit(1);
} else {
console.error('Failed to start Kode:', err.message);
process.exit(1);
}
});
child.on('exit', (code) => process.exit(code || 0));
}
`;
writeFileSync('cli.js', wrapper);
chmodSync('cli.js', 0o755);
// Create .npmrc
const npmrc = `# Ensure tsx is installed
auto-install-peers=true
`;
writeFileSync('.npmrc', npmrc);
console.log('✅ Build completed successfully!\n');
console.log('📋 Generated files:');
console.log(' - cli.js (Smart CLI wrapper)');
console.log(' - .npmrc (NPM configuration)');
console.log('\n🚀 Ready to publish!');
} catch (error) {
console.error('❌ Build failed:', error);
process.exit(1);
}
}
// Run build if called directly
if (import.meta.main) {
build();
}
export { build };

View File

@ -1,56 +1,18 @@
#!/usr/bin/env node
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
// This postinstall is intentionally minimal and cross-platform safe.
// npm/pnpm/yarn already create shims from package.json "bin" fields.
// We avoid attempting to create symlinks or relying on platform-specific tools like `which`/`where`.
const primaryCommand = 'kode';
const alternativeCommands = ['kwa', 'kd'];
function commandExists(cmd) {
function postinstallNotice() {
// Only print informational hints; never fail install.
try {
execSync(`which ${cmd}`, { stdio: 'ignore' });
return true;
} catch {
return false;
}
console.log('✅ @shareai-lab/kode installed. Commands available: kode, kwa, kd');
console.log(' If shell cannot find them, try reloading your terminal or reinstall globally:');
console.log(' npm i -g @shareai-lab/kode (or use: npx @shareai-lab/kode)');
} catch {}
}
function setupCommand() {
// Check if primary command exists
if (!commandExists(primaryCommand)) {
console.log(`✅ '${primaryCommand}' command is available and has been set up.`);
return;
}
console.log(`⚠️ '${primaryCommand}' command already exists on your system.`);
// Find an available alternative
for (const alt of alternativeCommands) {
if (!commandExists(alt)) {
// Create alternative command
const binPath = path.join(__dirname, '..', 'cli.js');
const altBinPath = path.join(__dirname, '..', '..', '..', '.bin', alt);
try {
fs.symlinkSync(binPath, altBinPath);
console.log(`✅ Created alternative command '${alt}' instead.`);
console.log(` You can run the tool using: ${alt}`);
return;
} catch (err) {
// Continue to next alternative
}
}
}
console.log(`
All common command names are taken. You can still run the tool using:
- npx @shareai-lab/kode
- Or create your own alias: alias myai='npx @shareai-lab/kode'
`);
}
// Only run in postinstall, not in development
if (process.env.npm_lifecycle_event === 'postinstall') {
setupCommand();
}
postinstallNotice();
}

View File

@ -39,4 +39,4 @@ console.log(` Version: ${pkg.version}`);
console.log(` Main: ${pkg.main}`);
console.log(` Bin: kode -> ${pkg.bin.kode}`);
console.log('\n🚀 Ready to publish!');
console.log(' Run: npm publish');
console.log(' Run: npm publish');

89
scripts/publish-dev.js Executable file
View File

@ -0,0 +1,89 @@
#!/usr/bin/env node
const { execSync } = require('child_process');
const { readFileSync, writeFileSync } = require('fs');
const path = require('path');
/**
* 发布开发版本到 npm
* 使用 -dev tag版本号自动递增 dev 后缀
* 不涉及 git 操作专注于 npm 发布
*/
async function publishDev() {
try {
console.log('🚀 Starting dev version publish process...\n');
// 1. 读取当前版本
const packagePath = path.join(process.cwd(), 'package.json');
const packageJson = JSON.parse(readFileSync(packagePath, 'utf8'));
const baseVersion = packageJson.version;
console.log(`📦 Current base version: ${baseVersion}`);
// 2. 生成开发版本号
let devVersion;
try {
// 获取当前 dev tag 的最新版本
const npmResult = execSync(`npm view @shareai-lab/kode@dev version`, { encoding: 'utf8' }).trim();
const currentDevVersion = npmResult;
if (currentDevVersion.startsWith(baseVersion + '-dev.')) {
const devNumber = parseInt(currentDevVersion.split('-dev.')[1]) + 1;
devVersion = `${baseVersion}-dev.${devNumber}`;
} else {
devVersion = `${baseVersion}-dev.1`;
}
} catch {
// 如果没有找到现有的 dev 版本,从 1 开始
devVersion = `${baseVersion}-dev.1`;
}
console.log(`📦 Publishing version: ${devVersion} with tag 'dev'`);
// 3. 临时更新 package.json 版本号
const originalPackageJson = { ...packageJson };
packageJson.version = devVersion;
writeFileSync(packagePath, JSON.stringify(packageJson, null, 2));
// 4. 构建项目
console.log('🔨 Building project...');
execSync('npm run build', { stdio: 'inherit' });
// 5. 运行预发布检查
console.log('🔍 Running pre-publish checks...');
execSync('node scripts/prepublish-check.js', { stdio: 'inherit' });
// 6. 发布到 npm 的 dev tag
console.log('📤 Publishing to npm...');
execSync(`npm publish --tag dev --access public`, { stdio: 'inherit' });
// 7. 恢复原始 package.json
writeFileSync(packagePath, JSON.stringify(originalPackageJson, null, 2));
console.log('\n✅ Dev version published successfully!');
console.log(`📦 Version: ${devVersion}`);
console.log(`🔗 Install with: npm install -g @shareai-lab/kode@dev`);
console.log(`🔗 Or: npm install -g @shareai-lab/kode@${devVersion}`);
console.log(`📊 View on npm: https://www.npmjs.com/package/@shareai-lab/kode/v/${devVersion}`);
} catch (error) {
console.error('❌ Dev publish failed:', error.message);
// 尝试恢复 package.json
try {
const packagePath = path.join(process.cwd(), 'package.json');
const packageJson = JSON.parse(readFileSync(packagePath, 'utf8'));
if (packageJson.version.includes('-dev.')) {
// 恢复到基础版本
const baseVersion = packageJson.version.split('-dev.')[0];
packageJson.version = baseVersion;
writeFileSync(packagePath, JSON.stringify(packageJson, null, 2));
console.log('🔄 Restored package.json version');
}
} catch {}
process.exit(1);
}
}
publishDev();

142
scripts/publish-release.js Executable file
View File

@ -0,0 +1,142 @@
#!/usr/bin/env node
const { execSync } = require('child_process');
const { readFileSync, writeFileSync } = require('fs');
const path = require('path');
const readline = require('readline');
/**
* 发布正式版本到 npm
* 使用 latest tag支持语义化版本升级
* 不涉及 git 操作专注于 npm 发布
*/
async function publishRelease() {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const question = (query) => new Promise(resolve => rl.question(query, resolve));
try {
console.log('🚀 Starting production release process...\n');
// 1. 读取当前版本
const packagePath = path.join(process.cwd(), 'package.json');
const packageJson = JSON.parse(readFileSync(packagePath, 'utf8'));
const currentVersion = packageJson.version;
console.log(`📦 Current version: ${currentVersion}`);
// 2. 选择版本升级类型
console.log('\n🔢 Version bump options:');
const versionParts = currentVersion.split('.');
const major = parseInt(versionParts[0]);
const minor = parseInt(versionParts[1]);
const patch = parseInt(versionParts[2]);
console.log(` 1. patch → ${major}.${minor}.${patch + 1} (bug fixes)`);
console.log(` 2. minor → ${major}.${minor + 1}.0 (new features)`);
console.log(` 3. major → ${major + 1}.0.0 (breaking changes)`);
console.log(` 4. custom → enter custom version`);
const choice = await question('\nSelect version bump (1-4): ');
let newVersion;
switch (choice) {
case '1':
newVersion = `${major}.${minor}.${patch + 1}`;
break;
case '2':
newVersion = `${major}.${minor + 1}.0`;
break;
case '3':
newVersion = `${major + 1}.0.0`;
break;
case '4':
newVersion = await question('Enter custom version: ');
break;
default:
console.log('❌ Invalid choice');
process.exit(1);
}
// 3. 检查版本是否已存在
try {
execSync(`npm view @shareai-lab/kode@${newVersion} version`, { stdio: 'ignore' });
console.log(`❌ Version ${newVersion} already exists on npm`);
process.exit(1);
} catch {
// 版本不存在,可以继续
}
// 4. 确认发布
console.log(`\n📋 Release Summary:`);
console.log(` Current: ${currentVersion}`);
console.log(` New: ${newVersion}`);
console.log(` Tag: latest`);
const confirm = await question('\n🤔 Proceed with release? (y/N): ');
if (confirm.toLowerCase() !== 'y') {
console.log('❌ Cancelled');
process.exit(0);
}
// 5. 更新版本号
console.log('📝 Updating version...');
const originalPackageJson = { ...packageJson };
packageJson.version = newVersion;
writeFileSync(packagePath, JSON.stringify(packageJson, null, 2));
// 6. 运行测试
console.log('🧪 Running tests...');
try {
execSync('npm run typecheck', { stdio: 'inherit' });
execSync('npm test', { stdio: 'inherit' });
} catch (error) {
console.log('❌ Tests failed, rolling back version...');
writeFileSync(packagePath, JSON.stringify(originalPackageJson, null, 2));
process.exit(1);
}
// 7. 构建项目
console.log('🔨 Building project...');
execSync('npm run build', { stdio: 'inherit' });
// 8. 运行预发布检查
console.log('🔍 Running pre-publish checks...');
execSync('node scripts/prepublish-check.js', { stdio: 'inherit' });
// 9. 发布到 npm
console.log('📤 Publishing to npm...');
execSync('npm publish --access public', { stdio: 'inherit' });
console.log('\n🎉 Production release published successfully!');
console.log(`📦 Version: ${newVersion}`);
console.log(`🔗 Install with: npm install -g @shareai-lab/kode`);
console.log(`🔗 Or: npm install -g @shareai-lab/kode@${newVersion}`);
console.log(`📊 View on npm: https://www.npmjs.com/package/@shareai-lab/kode`);
console.log('\n💡 Next steps:');
console.log(' - Commit the version change to git');
console.log(' - Create a git tag for this release');
console.log(' - Push changes to the repository');
} catch (error) {
console.error('❌ Production release failed:', error.message);
// 尝试恢复 package.json
try {
const packagePath = path.join(process.cwd(), 'package.json');
const originalContent = readFileSync(packagePath, 'utf8');
// 如果版本被修改了,尝试恢复(这里简化处理)
console.log('🔄 Please manually restore package.json if needed');
} catch {}
process.exit(1);
} finally {
rl.close();
}
}
publishRelease();

View File

@ -1,57 +0,0 @@
#!/usr/bin/env node
const fs = require('fs');
const { execSync } = require('child_process');
const path = require('path');
async function publish() {
console.log('🚀 Starting publish workaround...\n');
const packagePath = path.join(__dirname, '..', 'package.json');
try {
// Read package.json
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
const originalBundled = packageJson.bundledDependencies;
// Remove bundledDependencies temporarily
console.log('📦 Removing bundledDependencies temporarily...');
delete packageJson.bundledDependencies;
fs.writeFileSync(packagePath, JSON.stringify(packageJson, null, 2));
// Set proxy and publish
console.log('🌍 Setting proxy and publishing...');
process.env.https_proxy = 'http://127.0.0.1:7890';
process.env.http_proxy = 'http://127.0.0.1:7890';
process.env.all_proxy = 'socks5://127.0.0.1:7890';
process.env.SKIP_BUNDLED_CHECK = 'true';
execSync('npm publish --access public', {
stdio: 'inherit',
env: process.env
});
// Restore bundledDependencies
console.log('✅ Restoring bundledDependencies...');
packageJson.bundledDependencies = originalBundled;
fs.writeFileSync(packagePath, JSON.stringify(packageJson, null, 2));
console.log('🎉 Published successfully!');
} catch (error) {
console.error('❌ Publish failed:', error.message);
// Restore package.json on error
try {
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
packageJson.bundledDependencies = ["tsx"];
fs.writeFileSync(packagePath, JSON.stringify(packageJson, null, 2));
} catch (e) {
console.error('Failed to restore package.json');
}
process.exit(1);
}
}
publish();

View File

@ -1,8 +1,10 @@
import { z } from 'zod'
import * as React from 'react'
// DEPRECATED: Use domain/tool/Tool.interface.ts for new implementations
// This interface will be maintained for compatibility during transition
/**
* Core Tool interface for Kode's extensible tool system
* Provides standardized contract for all tool implementations
*/
export type SetToolJSXFn = (jsx: {
jsx: React.ReactNode | null
@ -64,18 +66,19 @@ export interface Tool<
input: z.infer<TInput>,
context?: ToolUseContext,
) => Promise<ValidationResult>
renderResultForAssistant: (output: TOutput) => string
renderResultForAssistant: (output: TOutput) => string | any[]
renderToolUseMessage: (
input: z.infer<TInput>,
options: { verbose: boolean },
) => string
renderToolUseRejectedMessage: () => React.ReactElement
renderToolUseRejectedMessage?: (...args: any[]) => React.ReactElement
renderToolResultMessage?: (output: TOutput) => React.ReactElement
call: (
input: z.infer<TInput>,
context: ToolUseContext,
) => AsyncGenerator<
{ type: 'result'; data: TOutput; resultForAssistant?: string },
| { type: 'result'; data: TOutput; resultForAssistant?: string }
| { type: 'progress'; content: any; normalizedMessages?: any[]; tools?: any[] },
void,
unknown
>

View File

@ -1,26 +1,26 @@
import React, { useState, useEffect, useMemo, useCallback, useReducer, Fragment } from 'react'
import { Box, Text, useInput } from 'ink'
import InkTextInput from 'ink-text-input'
import { getActiveAgents, clearAgentCache } from '../utils/agentLoader'
import { AgentConfig } from '../utils/agentLoader'
import { getActiveAgents, clearAgentCache } from '@utils/agentLoader'
import { AgentConfig } from '@utils/agentLoader'
import { writeFileSync, unlinkSync, mkdirSync, existsSync, readFileSync, renameSync } from 'fs'
import { join } from 'path'
import * as path from 'path'
import { homedir } from 'os'
import * as os from 'os'
import { getCwd } from '../utils/state'
import { getTheme } from '../utils/theme'
import { getCwd } from '@utils/state'
import { getTheme } from '@utils/theme'
import matter from 'gray-matter'
import { exec, spawn } from 'child_process'
import { promisify } from 'util'
import { watch, FSWatcher } from 'fs'
import { getMCPTools } from '../services/mcpClient'
import { getModelManager } from '../utils/model'
import { getMCPTools } from '@services/mcpClient'
import { getModelManager } from '@utils/model'
import { randomUUID } from 'crypto'
const execAsync = promisify(exec)
// Core constants aligned with Claude Code architecture
// Core constants aligned with the Claude Code agent architecture
const AGENT_LOCATIONS = {
USER: "user",
PROJECT: "project",
@ -119,7 +119,7 @@ type GeneratedAgent = {
// AI generation function (use main pointer model)
async function generateAgentWithClaude(prompt: string): Promise<GeneratedAgent> {
// Import Claude service dynamically to avoid circular dependencies
const { queryModel } = await import('../services/claude')
const { queryModel } = await import('@services/claude')
const systemPrompt = `You are an expert at creating AI agent configurations. Based on the user's description, generate a specialized agent configuration.
@ -324,7 +324,7 @@ function validateAgentConfig(config: Partial<CreateState>, existingAgents: Agent
}
}
// File system operations with Claude Code alignment
// File system operations retained for Claude Code parity
function getAgentDirectory(location: AgentLocation): string {
if (location === AGENT_LOCATIONS.BUILT_IN || location === AGENT_LOCATIONS.ALL) {
throw new Error(`Cannot get directory path for ${location} agents`)
@ -462,7 +462,16 @@ async function openInEditor(filePath: string): Promise<void> {
const projectDir = process.cwd()
const homeDir = os.homedir()
if (!resolvedPath.startsWith(projectDir) && !resolvedPath.startsWith(homeDir)) {
const isSub = (base: string, target: string) => {
const path = require('path')
const rel = path.relative(path.resolve(base), path.resolve(target))
if (!rel || rel === '') return true
if (rel.startsWith('..')) return false
if (path.isAbsolute(rel)) return false
return true
}
if (!isSub(projectDir, resolvedPath) && !isSub(homeDir, resolvedPath)) {
throw new Error('Access denied: File path outside allowed directories')
}
@ -536,7 +545,7 @@ async function updateAgent(
writeFileSync(filePath, content, { encoding: 'utf-8', flag: 'w' })
}
// Enhanced UI Components with Claude Code alignment
// Enhanced UI components retained for Claude Code parity
interface HeaderProps {
title: string
@ -1565,7 +1574,7 @@ function AgentListView({
<Box marginBottom={1}>
<Text bold color={theme.primary}>💭 What are agents?</Text>
</Box>
<Text>Specialized AI assistants that Claude can delegate to for specific tasks.</Text>
<Text>Specialized AI assistants that Kode can delegate to for specific tasks, compatible with Claude Code `.claude` agent packs.</Text>
<Text>Each agent has its own context, prompt, and tools.</Text>
<Box marginTop={1} marginBottom={1}>
@ -2345,7 +2354,9 @@ function ConfirmStep({ createState, setCreateState, setModeState, tools, onAgent
<Box marginTop={1}>
<Text><Text bold>Warnings:</Text></Text>
{validation.warnings.map((warning, idx) => (
<Text key={idx} color={theme.warning}> {warning}</Text>
<Fragment key={idx}>
<Text color={theme.warning}> {warning}</Text>
</Fragment>
))}
</Box>
)}
@ -2820,7 +2831,7 @@ function EditToolsStep({ agent, tools, setModeState, onAgentUpdated }: EditTools
<Box flexDirection="column" marginTop={1}>
{options.map((option, idx) => {
const isSelected = idx === selectedIndex
const isContinue = option.isContinue
const isContinue = 'isContinue' in option && option.isContinue
const isAdvancedToggle = (option as any).isAdvancedToggle
const isSeparator = (option as any).isSeparator
@ -3125,7 +3136,9 @@ function ViewAgent({ agent, tools, setModeState }: ViewAgentProps) {
) : (
<Box flexDirection="column" paddingLeft={2}>
{allowedTools.map(tool => (
<Text key={tool.name} color={theme.secondary}> {tool.name}</Text>
<Fragment key={tool.name}>
<Text color={theme.secondary}> {tool.name}</Text>
</Fragment>
))}
</Box>
)}
@ -3254,7 +3267,9 @@ function EditAgent({ agent, tools, setModeState, onAgentUpdated }: EditAgentProp
{validation.warnings.length > 0 && (
<Box marginTop={1}>
{validation.warnings.map((warning, idx) => (
<Text key={idx} color={theme.warning}> {warning}</Text>
<Fragment key={idx}>
<Text color={theme.warning}> {warning}</Text>
</Fragment>
))}
</Box>
)}

View File

@ -2,7 +2,7 @@ import {
ProjectConfig,
getCurrentProjectConfig as getCurrentProjectConfigDefault,
saveCurrentProjectConfig as saveCurrentProjectConfigDefault,
} from '../utils/config.js'
} from '@utils/config'
export type ProjectConfigHandler = {
getCurrentProjectConfig: () => ProjectConfig

View File

@ -1,7 +1,7 @@
import { Command } from '../commands'
import { Bug } from '../components/Bug'
import { Command } from '@commands'
import { Bug } from '@components/Bug'
import * as React from 'react'
import { PRODUCT_NAME } from '../constants/product'
import { PRODUCT_NAME } from '@constants/product'
const bug = {
type: 'local-jsx',

View File

@ -1,12 +1,12 @@
import { Command } from '../commands'
import { getMessagesSetter } from '../messages'
import { getContext } from '../context'
import { getCodeStyle } from '../utils/style'
import { clearTerminal } from '../utils/terminal'
import { getOriginalCwd, setCwd } from '../utils/state'
import { Message } from '../query'
import { resetReminderSession } from '../services/systemReminder'
import { resetFileFreshnessSession } from '../services/fileFreshness'
import { Command } from '@commands'
import { getMessagesSetter } from '@messages'
import { getContext } from '@context'
import { getCodeStyle } from '@utils/style'
import { clearTerminal } from '@utils/terminal'
import { getOriginalCwd, setCwd } from '@utils/state'
import { Message } from '@query'
import { resetReminderSession } from '@services/systemReminder'
import { resetFileFreshnessSession } from '@services/fileFreshness'
export async function clearConversation(context: {
setForkConvoWithMessagesOnTheNextRender: (

View File

@ -1,15 +1,15 @@
import { Command } from '../commands'
import { getContext } from '../context'
import { getMessagesGetter, getMessagesSetter } from '../messages'
import { API_ERROR_MESSAGE_PREFIX, queryLLM } from '../services/claude'
import { Command } from '@commands'
import { getContext } from '@context'
import { getMessagesGetter, getMessagesSetter } from '@messages'
import { API_ERROR_MESSAGE_PREFIX, queryLLM } from '@services/claude'
import {
createUserMessage,
normalizeMessagesForAPI,
} from '../utils/messages.js'
import { getCodeStyle } from '../utils/style'
import { clearTerminal } from '../utils/terminal'
import { resetReminderSession } from '../services/systemReminder'
import { resetFileFreshnessSession } from '../services/fileFreshness'
} from '@utils/messages'
import { getCodeStyle } from '@utils/style'
import { clearTerminal } from '@utils/terminal'
import { resetReminderSession } from '@services/systemReminder'
import { resetFileFreshnessSession } from '@services/fileFreshness'
const COMPRESSION_PROMPT = `Please provide a comprehensive summary of our conversation structured as follows:

View File

@ -1,5 +1,5 @@
import { Command } from '../commands'
import { Config } from '../components/Config'
import { Command } from '@commands'
import { Config } from '@components/Config'
import * as React from 'react'
const config = {

View File

@ -1,5 +1,5 @@
import type { Command } from '../commands'
import { formatTotalCost } from '../cost-tracker'
import type { Command } from '@commands'
import { formatTotalCost } from '@costTracker'
const cost = {
type: 'local',

View File

@ -1,11 +1,11 @@
import type { Command } from '../commands'
import type { Tool } from '../Tool'
import type { Command } from '@commands'
import type { Tool } from '@tool'
import Table from 'cli-table3'
import { getSystemPrompt } from '../constants/prompts'
import { getContext } from '../context'
import { getSystemPrompt } from '@constants/prompts'
import { getContext } from '@context'
import { zodToJsonSchema } from 'zod-to-json-schema'
import { getMessagesGetter } from '../messages'
import { PROJECT_FILE } from '../constants/product'
import { getMessagesGetter } from '@messages'
import { PROJECT_FILE } from '@constants/product'
// Quick and dirty estimate of bytes per token for rough token counts
const BYTES_PER_TOKEN = 4

View File

@ -1,7 +1,7 @@
import React from 'react'
import type { Command } from '../commands'
import { Doctor } from '../screens/Doctor'
import { PRODUCT_NAME } from '../constants/product'
import type { Command } from '@commands'
import { Doctor } from '@screens/Doctor'
import { PRODUCT_NAME } from '@constants/product'
const doctor: Command = {
name: 'doctor',

View File

@ -1,5 +1,5 @@
import { Command } from '../commands'
import { Help } from '../components/Help'
import { Command } from '@commands'
import { Help } from '@components/Help'
import * as React from 'react'
const help = {

View File

@ -1,6 +1,6 @@
import type { Command } from '../commands'
import { markProjectOnboardingComplete } from '../ProjectOnboarding'
import { PROJECT_FILE } from '../constants/product'
import type { Command } from '@commands'
import { markProjectOnboardingComplete } from '@components/ProjectOnboarding'
import { PROJECT_FILE } from '@constants/product'
const command = {
type: 'prompt',
name: 'init',

View File

@ -1,6 +1,6 @@
import { Command } from '../commands'
import { logError } from '../utils/log'
import { execFileNoThrow } from '../utils/execFileNoThrow'
import { Command } from '@commands'
import { logError } from '@utils/log'
import { execFileNoThrow } from '@utils/execFileNoThrow'
const isEnabled =
process.platform === 'darwin' &&

View File

@ -1,9 +1,9 @@
import * as React from 'react'
import type { Command } from '../commands'
import { ConsoleOAuthFlow } from '../components/ConsoleOAuthFlow'
import { clearTerminal } from '../utils/terminal'
import { isLoggedInToAnthropic } from '../utils/auth'
import { useExitOnCtrlCD } from '../hooks/useExitOnCtrlCD'
import type { Command } from '@commands'
import { ConsoleOAuthFlow } from '@components/ConsoleOAuthFlow'
import { clearTerminal } from '@utils/terminal'
import { isLoggedInToAnthropic } from '@utils/auth'
import { useExitOnCtrlCD } from '@hooks/useExitOnCtrlCD'
import { Box, Text } from 'ink'
import { clearConversation } from './clear'

View File

@ -1,7 +1,7 @@
import * as React from 'react'
import type { Command } from '../commands'
import { getGlobalConfig, saveGlobalConfig } from '../utils/config'
import { clearTerminal } from '../utils/terminal'
import type { Command } from '@commands'
import { getGlobalConfig, saveGlobalConfig } from '@utils/config'
import { clearTerminal } from '@utils/terminal'
import { Text } from 'ink'
export default {

View File

@ -1,8 +1,8 @@
import type { Command } from '../commands'
import { listMCPServers, getClients } from '../services/mcpClient'
import { PRODUCT_COMMAND } from '../constants/product'
import type { Command } from '@commands'
import { listMCPServers, getClients } from '@services/mcpClient'
import { PRODUCT_COMMAND } from '@constants/product'
import chalk from 'chalk'
import { getTheme } from '../utils/theme'
import { getTheme } from '@utils/theme'
const mcp = {
type: 'local',

View File

@ -1,8 +1,8 @@
import React from 'react'
import { render } from 'ink'
import { ModelConfig } from '../components/ModelConfig'
import { enableConfigs } from '../utils/config'
import { triggerModelConfigChange } from '../messages'
import { ModelConfig } from '@components/ModelConfig'
import { enableConfigs } from '@utils/config'
import { triggerModelConfigChange } from '@messages'
export const help = 'Change your AI provider and model settings'
export const description = 'Change your AI provider and model settings'
@ -26,7 +26,7 @@ export async function call(
<ModelConfig
onClose={() => {
// Force ModelManager reload to ensure UI sync - wait for completion before closing
import('../utils/model').then(({ reloadModelManager }) => {
import('@utils/model').then(({ reloadModelManager }) => {
reloadModelManager()
// 🔧 Critical fix: Trigger global UI refresh after model config changes
// This ensures PromptInput component detects ModelManager singleton state changes

View File

@ -1,6 +1,6 @@
import React from 'react'
import type { Command } from '../commands'
import { ModelStatusDisplay } from '../components/ModelStatusDisplay'
import type { Command } from '@commands'
import { ModelStatusDisplay } from '@components/ModelStatusDisplay'
const modelstatus: Command = {
name: 'modelstatus',

View File

@ -1,8 +1,8 @@
import * as React from 'react'
import type { Command } from '../commands'
import { Onboarding } from '../components/Onboarding'
import { clearTerminal } from '../utils/terminal'
import { getGlobalConfig, saveGlobalConfig } from '../utils/config'
import type { Command } from '@commands'
import { Onboarding } from '@components/Onboarding'
import { clearTerminal } from '@utils/terminal'
import { getGlobalConfig, saveGlobalConfig } from '@utils/config'
import { clearConversation } from './clear'
export default {

View File

@ -1,4 +1,4 @@
import { Command } from '../commands'
import { Command } from '@commands'
export default {
type: 'prompt',

View File

@ -1,6 +1,6 @@
import { Command } from '../commands'
import { reloadCustomCommands } from '../services/customCommands'
import { getCommands } from '../commands'
import { Command } from '@commands'
import { reloadCustomCommands } from '@services/customCommands'
import { getCommands } from '@commands'
/**
* Refresh Commands - Reload custom commands from filesystem

View File

@ -1,6 +1,6 @@
import { MACRO } from '../constants/macros.js'
import type { Command } from '../commands'
import { RELEASE_NOTES } from '../constants/releaseNotes'
import { MACRO } from '@constants/macros'
import type { Command } from '@commands'
import { RELEASE_NOTES } from '@constants/releaseNotes'
const releaseNotes: Command = {
description: 'Show release notes for the current or specified version',

View File

@ -1,8 +1,8 @@
import * as React from 'react'
import type { Command } from '../commands'
import { ResumeConversation } from '../screens/ResumeConversation'
import type { Command } from '@commands'
import { ResumeConversation } from '@screens/ResumeConversation'
import { render } from 'ink'
import { CACHE_PATHS, loadLogList } from '../utils/log'
import { CACHE_PATHS, loadLogList } from '@utils/log'
export default {
type: 'local-jsx',

View File

@ -1,5 +1,5 @@
import { Command } from '../commands'
import { BashTool } from '../tools/BashTool/BashTool'
import { Command } from '@commands'
import { BashTool } from '@tools/BashTool/BashTool'
export default {
type: 'prompt',

View File

@ -1,15 +1,15 @@
import { Command } from '../commands'
import { Command } from '@commands'
import { EOL, platform, homedir } from 'os'
import { execFileNoThrow } from '../utils/execFileNoThrow'
import { execFileNoThrow } from '@utils/execFileNoThrow'
import chalk from 'chalk'
import { getTheme } from '../utils/theme'
import { env } from '../utils/env'
import { getGlobalConfig, saveGlobalConfig } from '../utils/config'
import { markProjectOnboardingComplete } from '../ProjectOnboarding'
import { getTheme } from '@utils/theme'
import { env } from '@utils/env'
import { getGlobalConfig, saveGlobalConfig } from '@utils/config'
import { markProjectOnboardingComplete } from '@components/ProjectOnboarding'
import { readFileSync, writeFileSync } from 'fs'
import { join } from 'path'
import { safeParseJSON } from '../utils/json'
import { logError } from '../utils/log'
import { safeParseJSON } from '@utils/json'
import { logError } from '@utils/log'
const terminalSetup: Command = {
type: 'local',

View File

@ -1,93 +0,0 @@
import React from 'react'
import { Box, Text } from 'ink'
import { getGlobalConfig, saveGlobalConfig } from '../utils/config'
import { getTheme } from '../utils/theme'
import { Select } from './CustomSelect/select'
import { useExitOnCtrlCD } from '../hooks/useExitOnCtrlCD'
import chalk from 'chalk'
type Props = {
customApiKeyTruncated: string
onDone(): void
}
export function ApproveApiKey({
customApiKeyTruncated,
onDone,
}: Props): React.ReactNode {
const theme = getTheme()
function onChange(value: 'yes' | 'no') {
const config = getGlobalConfig()
switch (value) {
case 'yes': {
saveGlobalConfig({
...config,
customApiKeyResponses: {
...config.customApiKeyResponses,
approved: [
...(config.customApiKeyResponses?.approved ?? []),
customApiKeyTruncated,
],
},
})
onDone()
break
}
case 'no': {
saveGlobalConfig({
...config,
customApiKeyResponses: {
...config.customApiKeyResponses,
rejected: [
...(config.customApiKeyResponses?.rejected ?? []),
customApiKeyTruncated,
],
},
})
onDone()
break
}
}
}
const exitState = useExitOnCtrlCD(() => process.exit(0))
return (
<>
<Box
flexDirection="column"
gap={1}
padding={1}
borderStyle="round"
borderColor={theme.warning}
>
<Text bold color={theme.warning}>
Detected a custom API key in your environment
</Text>
<Text>
Your environment sets{' '}
<Text color={theme.warning}>ANTHROPIC_API_KEY</Text>:{' '}
<Text bold>sk-ant-...{customApiKeyTruncated}</Text>
</Text>
<Text>Do you want to use this API key?</Text>
<Select
options={[
{ label: `No (${chalk.bold('recommended')})`, value: 'no' },
{ label: 'Yes', value: 'yes' },
]}
onChange={value => onChange(value as 'yes' | 'no')}
/>
</Box>
<Box marginLeft={3}>
<Text dimColor>
{exitState.pending ? (
<>Press {exitState.keyName} again to exit</>
) : (
<>Enter to confirm</>
)}
</Text>
</Box>
</>
)
}

View File

@ -1,13 +1,13 @@
import { Box, Text } from 'ink'
import React from 'react'
import { getTheme } from '../utils/theme'
import { ASCII_LOGO } from '../constants/product'
import { getTheme } from '@utils/theme'
import { ASCII_LOGO } from '@constants/product'
export function AsciiLogo(): React.ReactNode {
const theme = getTheme()
return (
<Box flexDirection="column" alignItems="flex-start">
<Text color={theme.claude}>{ASCII_LOGO}</Text>
<Text color={theme.kode}>{ASCII_LOGO}</Text>
</Box>
)
}

View File

@ -1,148 +0,0 @@
import { Box, Text } from 'ink'
import * as React from 'react'
import { getTheme } from '../utils/theme'
import { gte } from 'semver'
import { useEffect, useState } from 'react'
import { isAutoUpdaterDisabled } from '../utils/config'
import {
AutoUpdaterResult,
getLatestVersion,
installGlobalPackage,
} from '../utils/autoUpdater.js'
import { useInterval } from '../hooks/useInterval'
import { logEvent } from '../services/statsig'
import { MACRO } from '../constants/macros'
import { PRODUCT_COMMAND } from '../constants/product'
type Props = {
debug: boolean
isUpdating: boolean
onChangeIsUpdating: (isUpdating: boolean) => void
onAutoUpdaterResult: (autoUpdaterResult: AutoUpdaterResult) => void
autoUpdaterResult: AutoUpdaterResult | null
}
export function AutoUpdater({
debug,
isUpdating,
onChangeIsUpdating,
onAutoUpdaterResult,
autoUpdaterResult,
}: Props): React.ReactNode {
const theme = getTheme()
const [versions, setVersions] = useState<{
global?: string | null
latest?: string | null
}>({})
const checkForUpdates = React.useCallback(async () => {
if (process.env.NODE_ENV === 'test' || process.env.NODE_ENV === 'dev') {
return
}
if (isUpdating) {
return
}
// Get versions
const globalVersion = MACRO.VERSION
const latestVersion = await getLatestVersion()
const isDisabled = true //await isAutoUpdaterDisabled()
setVersions({ global: globalVersion, latest: latestVersion })
// Check if update needed and perform update
if (
!isDisabled &&
globalVersion &&
latestVersion &&
!gte(globalVersion, latestVersion)
) {
const startTime = Date.now()
onChangeIsUpdating(true)
const installStatus = await installGlobalPackage()
onChangeIsUpdating(false)
if (installStatus === 'success') {
logEvent('tengu_auto_updater_success', {
fromVersion: globalVersion,
toVersion: latestVersion,
durationMs: String(Date.now() - startTime),
})
} else {
logEvent('tengu_auto_updater_fail', {
fromVersion: globalVersion,
attemptedVersion: latestVersion,
status: installStatus,
durationMs: String(Date.now() - startTime),
})
}
onAutoUpdaterResult({
version: latestVersion!,
status: installStatus,
})
}
// Don't re-render when isUpdating changes
// TODO: Find a cleaner way to do this
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [onAutoUpdaterResult])
// Initial check
useEffect(() => {
// checkForUpdates()
}, [checkForUpdates])
// Check every 30 minutes
// useInterval(checkForUpdates, 30 * 60 * 1000)
if (debug) {
return (
<Box flexDirection="row">
<Text dimColor>
globalVersion: {versions.global} &middot; latestVersion:{' '}
{versions.latest}
</Text>
</Box>
)
}
if (!autoUpdaterResult?.version && (!versions.global || !versions.latest)) {
return null
}
if (!autoUpdaterResult?.version && !isUpdating) {
return null
}
return (
<Box flexDirection="row">
{debug && (
<Text dimColor>
globalVersion: {versions.global} &middot; latestVersion:{' '}
{versions.latest}
</Text>
)}
{isUpdating && (
<>
<Box>
<Text color={theme.secondaryText} dimColor wrap="end">
Auto-updating to v{versions.latest}
</Text>
</Box>
</>
)}
{autoUpdaterResult?.status === 'success' && autoUpdaterResult?.version ? (
<Text color={theme.success}>
Update installed &middot; Restart to apply
</Text>
) : null}
{(autoUpdaterResult?.status === 'install_failed' ||
autoUpdaterResult?.status === 'no_permissions') && (
<Text color={theme.error}>
Auto-update failed &middot; Try{' '}
<Text bold>{PRODUCT_COMMAND} doctor</Text> or{' '}
<Text bold>npm i -g {MACRO.PACKAGE_URL}</Text>
</Text>
)}
</Box>
)
}

View File

@ -1,23 +1,22 @@
import { Box, Text, useInput } from 'ink'
import * as React from 'react'
import { useState, useCallback, useEffect } from 'react'
import { getTheme } from '../utils/theme'
import { getMessagesGetter } from '../messages'
import type { Message } from '../query'
import { getTheme } from '@utils/theme'
import { getMessagesGetter } from '@messages'
import type { Message } from '@query'
import TextInput from './TextInput'
import { logError, getInMemoryErrors } from '../utils/log'
import { env } from '../utils/env'
import { getGitState, getIsGit, GitRepoState } from '../utils/git'
import { useTerminalSize } from '../hooks/useTerminalSize'
import { getAnthropicApiKey, getGlobalConfig } from '../utils/config'
import { USER_AGENT } from '../utils/http'
import { logEvent } from '../services/statsig'
import { PRODUCT_NAME } from '../constants/product'
import { API_ERROR_MESSAGE_PREFIX, queryQuick } from '../services/claude'
import { openBrowser } from '../utils/browser'
import { useExitOnCtrlCD } from '../hooks/useExitOnCtrlCD'
import { MACRO } from '../constants/macros'
import { GITHUB_ISSUES_REPO_URL } from '../constants/product'
import { logError, getInMemoryErrors } from '@utils/log'
import { env } from '@utils/env'
import { getGitState, getIsGit, GitRepoState } from '@utils/git'
import { useTerminalSize } from '@hooks/useTerminalSize'
import { getAnthropicApiKey, getGlobalConfig } from '@utils/config'
import { USER_AGENT } from '@utils/http'
import { PRODUCT_NAME } from '@constants/product'
import { API_ERROR_MESSAGE_PREFIX, queryQuick } from '@services/claude'
import { openBrowser } from '@utils/browser'
import { useExitOnCtrlCD } from '@hooks/useExitOnCtrlCD'
import { MACRO } from '@constants/macros'
import { GITHUB_ISSUES_REPO_URL } from '@constants/product'
type Props = {
onDone(result: string): void

View File

@ -2,15 +2,15 @@ import { Box, Text, useInput } from 'ink'
import * as React from 'react'
import { useState } from 'react'
import figures from 'figures'
import { getTheme } from '../utils/theme'
import { getTheme } from '@utils/theme'
import {
GlobalConfig,
saveGlobalConfig,
getGlobalConfig,
} from '../utils/config.js'
} from '@utils/config'
import chalk from 'chalk'
import { useExitOnCtrlCD } from '../hooks/useExitOnCtrlCD'
import { getModelManager } from '../utils/model'
import { useExitOnCtrlCD } from '@hooks/useExitOnCtrlCD'
import { getModelManager } from '@utils/model'
type Props = {
onClose: () => void

View File

@ -1,17 +1,16 @@
import React, { useEffect, useState, useCallback } from 'react'
import { Static, Box, Text, useInput } from 'ink'
import TextInput from './TextInput'
import { OAuthService, createAndStoreApiKey } from '../services/oauth'
import { getTheme } from '../utils/theme'
import { logEvent } from '../services/statsig'
import { OAuthService, createAndStoreApiKey } from '@services/oauth'
import { getTheme } from '@utils/theme'
import { AsciiLogo } from './AsciiLogo'
import { useTerminalSize } from '../hooks/useTerminalSize'
import { logError } from '../utils/log'
import { clearTerminal } from '../utils/terminal'
import { useTerminalSize } from '@hooks/useTerminalSize'
import { logError } from '@utils/log'
import { clearTerminal } from '@utils/terminal'
import { SimpleSpinner } from './Spinner'
import { WelcomeBox } from './Onboarding'
import { PRODUCT_NAME } from '../constants/product'
import { sendNotification } from '../services/notifier'
import { PRODUCT_NAME } from '@constants/product'
import { sendNotification } from '@services/notifier'
type Props = {
onDone(): void
@ -70,10 +69,10 @@ export function ConsoleOAuthFlow({ onDone }: Props): React.ReactNode {
useInput(async (_, key) => {
if (key.return) {
if (oauthStatus.state === 'idle') {
logEvent('tengu_oauth_start', {})
setOAuthStatus({ state: 'ready_to_start' })
} else if (oauthStatus.state === 'success') {
logEvent('tengu_oauth_success', {})
await clearTerminal() // needed to clear out Static components
onDone()
} else if (oauthStatus.state === 'error' && oauthStatus.toRetry) {
@ -101,7 +100,7 @@ export function ConsoleOAuthFlow({ onDone }: Props): React.ReactNode {
}
// Track which path the user is taking (manual code entry)
logEvent('tengu_oauth_manual_entry', {})
oauthService.processCallback({
authorizationCode,
state,
@ -133,7 +132,7 @@ export function ConsoleOAuthFlow({ onDone }: Props): React.ReactNode {
'Failed to exchange authorization code for access token. Please try again.',
toRetry: { state: 'ready_to_start' },
})
logEvent('tengu_oauth_token_exchange_error', { error: err.message })
} else {
// Handle other errors
setOAuthStatus({
@ -154,7 +153,7 @@ export function ConsoleOAuthFlow({ onDone }: Props): React.ReactNode {
message: 'Failed to create API key: ' + err.message,
toRetry: { state: 'ready_to_start' },
})
logEvent('tengu_oauth_api_key_error', { error: err.message })
throw err
},
)
@ -169,13 +168,10 @@ export function ConsoleOAuthFlow({ onDone }: Props): React.ReactNode {
"Unable to create API key. The server accepted the request but didn't return a key.",
toRetry: { state: 'ready_to_start' },
})
logEvent('tengu_oauth_api_key_error', {
error: 'server_returned_no_key',
})
}
} catch (err) {
const errorMessage = (err as Error).message
logEvent('tengu_oauth_error', { error: errorMessage })
}
}, [oauthService, setShowPastePrompt])

View File

@ -1,7 +1,7 @@
import { Box, Text, useInput } from 'ink'
import React from 'react'
import { Select } from './CustomSelect/select'
import { getTheme } from '../utils/theme'
import { getTheme } from '@utils/theme'
import Link from './Link'
interface Props {

View File

@ -2,7 +2,7 @@ import figures from 'figures'
import { Box, Text } from 'ink'
import React, { type ReactNode } from 'react'
import { type Theme } from './theme'
import { getTheme } from '../../utils/theme'
import { getTheme } from '@utils/theme'
export type SelectOptionProps = {
/**
@ -45,13 +45,13 @@ export function SelectOption({
paddingRight: 1,
}),
focusIndicator: () => ({
color: appTheme.claude,
color: appTheme.kode,
}),
label: ({ isFocused, isSelected }: { isFocused: boolean; isSelected: boolean }) => ({
color: isSelected
? appTheme.success
: isFocused
? appTheme.claude
? appTheme.kode
: appTheme.text,
bold: isSelected,
}),

View File

@ -5,7 +5,7 @@ import { type Theme } from './theme'
import { useSelectState } from './use-select-state'
import { useSelect } from './use-select'
import { Option } from '@inkjs/ui'
import { getTheme } from '../../utils/theme'
import { getTheme } from '@utils/theme'
export type OptionSubtree = {
/**

View File

@ -1,7 +1,7 @@
import * as React from 'react'
import { getTheme } from '../utils/theme'
import { getTheme } from '@utils/theme'
import { Text } from 'ink'
import { PRODUCT_NAME } from '../constants/product'
import { PRODUCT_NAME } from '@constants/product'
export function FallbackToolUseRejectedMessage(): React.ReactNode {
return (

View File

@ -1,16 +1,16 @@
import { Hunk } from 'diff'
import { Box, Text } from 'ink'
import * as React from 'react'
import { intersperse } from '../utils/array'
import { intersperse } from '@utils/array'
import { StructuredDiff } from './StructuredDiff'
import { getTheme } from '../utils/theme'
import { getCwd } from '../utils/state'
import { getTheme } from '@utils/theme'
import { getCwd } from '@utils/state'
import { relative } from 'path'
import { useTerminalSize } from '../hooks/useTerminalSize'
import { useTerminalSize } from '@hooks/useTerminalSize'
type Props = {
filePath: string
structuredPatch: Hunk[]
structuredPatch?: Hunk[]
verbose: boolean
}
@ -20,11 +20,12 @@ export function FileEditToolUpdatedMessage({
verbose,
}: Props): React.ReactNode {
const { columns } = useTerminalSize()
const numAdditions = structuredPatch.reduce(
const patches = Array.isArray(structuredPatch) ? structuredPatch : []
const numAdditions = patches.reduce(
(count, hunk) => count + hunk.lines.filter(_ => _.startsWith('+')).length,
0,
)
const numRemovals = structuredPatch.reduce(
const numRemovals = patches.reduce(
(count, hunk) => count + hunk.lines.filter(_ => _.startsWith('-')).length,
0,
)
@ -49,18 +50,19 @@ export function FileEditToolUpdatedMessage({
</>
) : null}
</Text>
{intersperse(
structuredPatch.map(_ => (
<Box flexDirection="column" paddingLeft={5} key={_.newStart}>
<StructuredDiff patch={_} dim={false} width={columns - 12} />
</Box>
)),
i => (
<Box paddingLeft={5} key={`ellipsis-${i}`}>
<Text color={getTheme().secondaryText}>...</Text>
</Box>
),
)}
{patches.length > 0 &&
intersperse(
patches.map(_ => (
<Box flexDirection="column" paddingLeft={5} key={_.newStart}>
<StructuredDiff patch={_} dim={false} width={columns - 12} />
</Box>
)),
i => (
<Box paddingLeft={5} key={`ellipsis-${i}`}>
<Text color={getTheme().secondaryText}>...</Text>
</Box>
),
)}
</Box>
)
}

View File

@ -1,15 +1,15 @@
import { Command } from '../commands'
import { PRODUCT_COMMAND, PRODUCT_NAME } from '../constants/product'
import { Command } from '@commands'
import { PRODUCT_COMMAND, PRODUCT_NAME } from '@constants/product'
import {
getCustomCommandDirectories,
hasCustomCommands,
type CustomCommandWithScope,
} from '../services/customCommands'
} from '@services/customCommands'
import * as React from 'react'
import { Box, Text, useInput } from 'ink'
import { getTheme } from '../utils/theme'
import { getTheme } from '@utils/theme'
import { PressEnterToContinue } from './PressEnterToContinue'
import { MACRO } from '../constants/macros'
import { MACRO } from '@constants/macros'
/**
* Help Component - Interactive help system with progressive disclosure
@ -66,7 +66,7 @@ export function Help({
return (
<Box flexDirection="column" padding={1}>
<Text bold color={theme.claude}>
<Text bold color={theme.kode}>
{`${PRODUCT_NAME} v${MACRO.VERSION}`}
</Text>
@ -150,7 +150,7 @@ export function Help({
<Box flexDirection="column">
{customCommands.map((cmd, i) => (
<Box key={i} marginLeft={1}>
<Text bold color={theme.claude}>{`/${cmd.name}`}</Text>
<Text bold color={theme.kode}>{`/${cmd.name}`}</Text>
<Text> - {cmd.description}</Text>
{cmd.aliases && cmd.aliases.length > 0 && (
<Text color={theme.secondaryText}>
@ -175,10 +175,10 @@ export function Help({
Custom commands loaded from:
</Text>
<Text color={theme.secondaryText}>
{getCustomCommandDirectories().userClaude} (user: prefix)
{getCustomCommandDirectories().userClaude} (Claude `.claude` user scope)
</Text>
<Text color={theme.secondaryText}>
{getCustomCommandDirectories().projectClaude} (project: prefix)
{getCustomCommandDirectories().projectClaude} (Claude `.claude` project scope)
</Text>
<Text color={theme.secondaryText}>
Use /refresh-commands to reload after changes
@ -190,10 +190,10 @@ export function Help({
Create custom commands by adding .md files to:
</Text>
<Text color={theme.secondaryText}>
{getCustomCommandDirectories().userClaude} (user: prefix)
{getCustomCommandDirectories().userClaude} (Claude `.claude` user scope)
</Text>
<Text color={theme.secondaryText}>
{getCustomCommandDirectories().projectClaude} (project: prefix)
{getCustomCommandDirectories().projectClaude} (Claude `.claude` project scope)
</Text>
<Text color={theme.secondaryText}>
Use /refresh-commands to reload after creation

View File

@ -1,7 +1,7 @@
import { highlight, supportsLanguage } from 'cli-highlight'
import { Text } from 'ink'
import React, { useMemo } from 'react'
import { logError } from '../utils/log'
import { logError } from '@utils/log'
type Props = {
code: string

View File

@ -1,11 +1,11 @@
import React from 'react'
import { Box, Newline, Text, useInput } from 'ink'
import { getTheme } from '../utils/theme'
import { getTheme } from '@utils/theme'
import { Select } from './CustomSelect/select'
import { render } from 'ink'
import { writeFileSync } from 'fs'
import { ConfigParseError } from '../utils/errors'
import { useExitOnCtrlCD } from '../hooks/useExitOnCtrlCD'
import { ConfigParseError } from '@utils/errors'
import { useExitOnCtrlCD } from '@hooks/useExitOnCtrlCD'
interface InvalidConfigHandlerProps {
error: ConfigParseError
}
@ -18,7 +18,7 @@ interface InvalidConfigDialogProps {
}
/**
* Dialog shown when the Claude config file contains invalid JSON
* Dialog shown when the Kode config file contains invalid JSON
*/
function InvalidConfigDialog({
filePath,

View File

@ -1,7 +1,7 @@
import InkLink from 'ink-link'
import { Text } from 'ink'
import React from 'react'
import { env } from '../utils/env'
import { env } from '@utils/env'
type LinkProps = {
url: string

View File

@ -1,10 +1,10 @@
import React from 'react'
import { Box, Text } from 'ink'
import { Select } from './CustomSelect/select'
import type { LogOption } from '../types/logs'
import { getTheme } from '../utils/theme'
import { useTerminalSize } from '../hooks/useTerminalSize'
import { formatDate } from '../utils/log'
import type { LogOption } from '@kode-types/logs'
import { getTheme } from '@utils/theme'
import { useTerminalSize } from '@hooks/useTerminalSize'
import { formatDate } from '@utils/log'
type LogSelectorProps = {
logs: LogOption[]

View File

@ -1,21 +1,31 @@
import { Box, Text, Newline } from 'ink'
import * as React from 'react'
import { getTheme } from '../utils/theme'
import { PRODUCT_NAME } from '../constants/product'
import { getAnthropicApiKey, getGlobalConfig } from '../utils/config'
import { getCwd } from '../utils/state'
import { getTheme } from '@utils/theme'
import { PRODUCT_NAME } from '@constants/product'
import { getAnthropicApiKey, getGlobalConfig } from '@utils/config'
import { getCwd } from '@utils/state'
import { AsciiLogo } from './AsciiLogo'
import type { WrappedClient } from '../services/mcpClient'
import { getModelManager } from '../utils/model'
import type { WrappedClient } from '@services/mcpClient'
import { getModelManager } from '@utils/model'
import { MACRO } from '@constants/macros'
export const MIN_LOGO_WIDTH = 50
const DEFAULT_UPDATE_COMMANDS = [
'bun add -g @shareai-lab/kode@latest',
'npm install -g @shareai-lab/kode@latest',
] as const
export function Logo({
mcpClients,
isDefaultModel = false,
updateBannerVersion,
updateBannerCommands,
}: {
mcpClients: WrappedClient[]
isDefaultModel?: boolean
updateBannerVersion?: string | null
updateBannerCommands?: string[] | null
}): React.ReactNode {
const width = Math.max(MIN_LOGO_WIDTH, getCwd().length + 12)
const theme = getTheme()
@ -35,15 +45,31 @@ export function Logo({
return (
<Box flexDirection="column">
<Box
borderColor={theme.claude}
borderColor={theme.kode}
borderStyle="round"
flexDirection="column"
gap={1}
paddingLeft={1}
marginRight={2}
width={width}
>
{updateBannerVersion ? (
<Box flexDirection="column">
<Text color="yellow">New version available: {updateBannerVersion} (current: {MACRO.VERSION})</Text>
<Text>Run the following command to update:</Text>
<Text>
{' '}
{updateBannerCommands?.[1] ?? DEFAULT_UPDATE_COMMANDS[1]}
</Text>
{process.platform !== 'win32' && (
<Text dimColor>
Note: you may need to prefix with "sudo" on macOS/Linux.
</Text>
)}
</Box>
) : null}
<Text>
<Text color={theme.claude}></Text> Welcome to{' '}
<Text color={theme.kode}></Text> Welcome to{' '}
<Text bold>{PRODUCT_NAME}</Text> <Text>research preview!</Text>
</Text>
{/* <AsciiLogo /> */}

View File

@ -1,13 +1,13 @@
import React from 'react'
import { Box, Text, useInput } from 'ink'
import { getTheme } from '../utils/theme'
import { getTheme } from '@utils/theme'
import { Select } from './CustomSelect/select'
import {
saveCurrentProjectConfig,
getCurrentProjectConfig,
} from '../utils/config.js'
} from '@utils/config'
import { MCPServerDialogCopy } from './MCPServerDialogCopy'
import { useExitOnCtrlCD } from '../hooks/useExitOnCtrlCD'
import { useExitOnCtrlCD } from '@hooks/useExitOnCtrlCD'
type Props = {
serverName: string

View File

@ -1,7 +1,7 @@
import React from 'react'
import { Text } from 'ink'
import Link from 'ink-link'
import { PRODUCT_NAME, PRODUCT_COMMAND } from '../constants/product'
import { PRODUCT_NAME, PRODUCT_COMMAND } from '@constants/product'
export function MCPServerDialogCopy(): React.ReactNode {
return (

View File

@ -1,14 +1,14 @@
import React from 'react'
import { Box, Text, useInput } from 'ink'
import { getTheme } from '../utils/theme'
import { getTheme } from '@utils/theme'
import { MultiSelect } from '@inkjs/ui'
import {
saveCurrentProjectConfig,
getCurrentProjectConfig,
} from '../utils/config.js'
} from '@utils/config'
import { partition } from 'lodash-es'
import { MCPServerDialogCopy } from './MCPServerDialogCopy'
import { useExitOnCtrlCD } from '../hooks/useExitOnCtrlCD'
import { useExitOnCtrlCD } from '@hooks/useExitOnCtrlCD'
type Props = {
serverNames: string[]

View File

@ -1,6 +1,6 @@
import { Box } from 'ink'
import * as React from 'react'
import type { AssistantMessage, Message, UserMessage } from '../query'
import type { AssistantMessage, Message, UserMessage } from '@query'
import type {
ContentBlock,
DocumentBlockParam,
@ -10,16 +10,16 @@ import type {
ToolResultBlockParam,
ToolUseBlockParam,
} from '@anthropic-ai/sdk/resources/index.mjs'
import { Tool } from '../Tool'
import { logError } from '../utils/log'
import { Tool } from '@tool'
import { logError } from '@utils/log'
import { UserToolResultMessage } from './messages/UserToolResultMessage/UserToolResultMessage'
import { AssistantToolUseMessage } from './messages/AssistantToolUseMessage'
import { AssistantTextMessage } from './messages/AssistantTextMessage'
import { UserTextMessage } from './messages/UserTextMessage'
import { NormalizedMessage } from '../utils/messages'
import { NormalizedMessage } from '@utils/messages'
import { AssistantThinkingMessage } from './messages/AssistantThinkingMessage'
import { AssistantRedactedThinkingMessage } from './messages/AssistantRedactedThinkingMessage'
import { useTerminalSize } from '../hooks/useTerminalSize'
import { useTerminalSize } from '@hooks/useTerminalSize'
type Props = {
message: UserMessage | AssistantMessage

View File

@ -2,19 +2,18 @@ import { Box, Text, useInput } from 'ink'
import * as React from 'react'
import { useMemo, useState, useEffect } from 'react'
import figures from 'figures'
import { getTheme } from '../utils/theme'
import { getTheme } from '@utils/theme'
import { Message as MessageComponent } from './Message'
import { randomUUID } from 'crypto'
import { type Tool } from '../Tool'
import { type Tool } from '@tool'
import {
createUserMessage,
isEmptyMessageText,
isNotEmptyMessage,
normalizeMessages,
} from '../utils/messages.js'
import { logEvent } from '../services/statsig'
import type { AssistantMessage, UserMessage } from '../query'
import { useExitOnCtrlCD } from '../hooks/useExitOnCtrlCD'
} from '@utils/messages'
import type { AssistantMessage, UserMessage } from '@query'
import { useExitOnCtrlCD } from '@hooks/useExitOnCtrlCD'
type Props = {
erroredToolUseIDs: Set<string>
@ -37,23 +36,14 @@ export function MessageSelector({
}: Props): React.ReactNode {
const currentUUID = useMemo(randomUUID, [])
// Log when selector is opened
useEffect(() => {
logEvent('tengu_message_selector_opened', {})
}, [])
useEffect(() => {}, [])
function handleSelect(message: UserMessage) {
const indexFromEnd = messages.length - 1 - messages.indexOf(message)
logEvent('tengu_message_selector_selected', {
index_from_end: indexFromEnd.toString(),
message_type: message.type,
is_current_prompt: (message.uuid === currentUUID).toString(),
})
onSelect(message)
}
function handleEscape() {
logEvent('tengu_message_selector_cancelled', {})
onEscape()
}

View File

@ -1,7 +1,7 @@
import React from 'react'
import { Box, Text } from 'ink'
import { usePermissionContext } from '../context/PermissionContext'
import { getTheme } from '../utils/theme'
import { usePermissionContext } from '@context/PermissionContext'
import { getTheme } from '@utils/theme'
interface ModeIndicatorProps {
showTransitionCount?: boolean

View File

@ -2,15 +2,15 @@ import { Box, Text, useInput } from 'ink'
import * as React from 'react'
import { useState, useCallback, useEffect, useRef } from 'react'
import figures from 'figures'
import { getTheme } from '../utils/theme'
import { getTheme } from '@utils/theme'
import {
getGlobalConfig,
saveGlobalConfig,
ModelPointerType,
setModelPointer,
} from '../utils/config.js'
import { getModelManager } from '../utils/model'
import { useExitOnCtrlCD } from '../hooks/useExitOnCtrlCD'
} from '@utils/config'
import { getModelManager } from '@utils/model'
import { useExitOnCtrlCD } from '@hooks/useExitOnCtrlCD'
import { ModelSelector } from './ModelSelector'
import { ModelListManager } from './ModelListManager'

View File

@ -2,10 +2,10 @@ import { Box, Text, useInput } from 'ink'
import * as React from 'react'
import { useState, useCallback } from 'react'
import figures from 'figures'
import { getTheme } from '../utils/theme'
import { getGlobalConfig, ModelPointerType } from '../utils/config.js'
import { getModelManager } from '../utils/model'
import { useExitOnCtrlCD } from '../hooks/useExitOnCtrlCD'
import { getTheme } from '@utils/theme'
import { getGlobalConfig, ModelPointerType } from '@utils/config'
import { getModelManager } from '@utils/model'
import { useExitOnCtrlCD } from '@hooks/useExitOnCtrlCD'
import { ModelSelector } from './ModelSelector'
type Props = {

View File

@ -1,9 +1,9 @@
import React, { useState, useEffect, useCallback, useRef } from 'react'
import { Box, Text, useInput } from 'ink'
import { getTheme } from '../utils/theme'
import { getTheme } from '@utils/theme'
import { Select } from './CustomSelect/select'
import { Newline } from 'ink'
import { getModelManager } from '../utils/model'
import { getModelManager } from '@utils/model'
// 共享的屏幕容器组件,避免重复边框
function ScreenContainer({
@ -33,8 +33,8 @@ function ScreenContainer({
</Box>
)
}
import { PRODUCT_NAME } from '../constants/product'
import { useExitOnCtrlCD } from '../hooks/useExitOnCtrlCD'
import { PRODUCT_NAME } from '@constants/product'
import { useExitOnCtrlCD } from '@hooks/useExitOnCtrlCD'
import {
getGlobalConfig,
saveGlobalConfig,
@ -42,14 +42,14 @@ import {
ModelPointerType,
setAllPointersToModel,
setModelPointer,
} from '../utils/config.js'
import models, { providers } from '../constants/models'
} from '@utils/config'
import models, { providers } from '@constants/models'
import TextInput from './TextInput'
import OpenAI from 'openai'
import chalk from 'chalk'
import { fetchAnthropicModels, verifyApiKey } from '../services/claude'
import { fetchCustomModels, getModelFeatures } from '../services/openai'
import { testGPT5Connection, validateGPT5Config } from '../services/gpt5ConnectionTest'
import { fetchAnthropicModels, verifyApiKey } from '@services/claude'
import { fetchCustomModels, getModelFeatures } from '@services/openai'
import { testGPT5Connection, validateGPT5Config } from '@services/gpt5ConnectionTest'
type Props = {
onDone: () => void
abortController?: AbortController
@ -419,7 +419,10 @@ export function ModelSelector({
function getModelDetails(model: ModelInfo): string {
const details = []
if (model.max_tokens) {
// Show context_length if available (Ollama models), otherwise max_tokens
if (model.context_length) {
details.push(`${formatNumber(model.context_length)} tokens`)
} else if (model.max_tokens) {
details.push(`${formatNumber(model.max_tokens)} tokens`)
}
@ -1040,13 +1043,15 @@ export function ModelSelector({
}
// Transform Ollama models to our format
// Note: max_tokens here is for OUTPUT tokens, not context length
const ollamaModels = models.map((model: any) => ({
model:
model.id ??
model.name ??
model.modelName ??
(typeof model === 'string' ? model : ''),
provider: 'ollama',
max_tokens: 4096, // Default value
max_tokens: DEFAULT_MAX_TOKENS, // Default output tokens (8K is reasonable)
supports_vision: false,
supports_function_calling: true,
supports_reasoning_effort: false,
@ -1055,16 +1060,102 @@ export function ModelSelector({
// Filter out models with empty names
const validModels = ollamaModels.filter(model => model.model)
setAvailableModels(validModels)
// Helper: normalize Ollama server root for /api/show (strip trailing /v1)
const normalizeOllamaRoot = (url: string): string => {
try {
const u = new URL(url)
let pathname = u.pathname.replace(/\/+$|^$/, '')
if (pathname.endsWith('/v1')) {
pathname = pathname.slice(0, -3)
}
u.pathname = pathname
return u.toString().replace(/\/+$/, '')
} catch {
return url.replace(/\/v1\/?$/, '')
}
}
// Helper: extract num_ctx/context_length from /api/show response
const extractContextTokens = (data: any): number | null => {
if (!data || typeof data !== 'object') return null
// First check model_info for architecture-specific context_length fields
// Example: qwen2.context_length, llama.context_length, etc.
if (data.model_info && typeof data.model_info === 'object') {
const modelInfo = data.model_info
for (const key of Object.keys(modelInfo)) {
if (key.endsWith('.context_length') || key.endsWith('_context_length')) {
const val = modelInfo[key]
if (typeof val === 'number' && isFinite(val) && val > 0) {
return val
}
}
}
}
// Fallback to other common fields
const candidates = [
(data as any)?.parameters?.num_ctx,
(data as any)?.model_info?.num_ctx,
(data as any)?.config?.num_ctx,
(data as any)?.details?.context_length,
(data as any)?.context_length,
(data as any)?.num_ctx,
(data as any)?.max_tokens,
(data as any)?.max_new_tokens
].filter((v: any) => typeof v === 'number' && isFinite(v) && v > 0)
if (candidates.length > 0) {
return Math.max(...candidates)
}
// parameters may be a string like "num_ctx=4096 ..."
if (typeof (data as any)?.parameters === 'string') {
const m = (data as any).parameters.match(/num_ctx\s*[:=]\s*(\d+)/i)
if (m) {
const n = parseInt(m[1], 10)
if (Number.isFinite(n) && n > 0) return n
}
}
return null
}
// Enrich each model via /api/show to get accurate context length
// Store context length separately from max_tokens (output limit)
const ollamaRoot = normalizeOllamaRoot(ollamaBaseUrl)
const enrichedModels = await Promise.all(
validModels.map(async (m: any) => {
try {
const showResp = await fetch(`${ollamaRoot}/api/show`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: m.model })
})
if (showResp.ok) {
const showData = await showResp.json()
const ctx = extractContextTokens(showData)
if (typeof ctx === 'number' && isFinite(ctx) && ctx > 0) {
// Store context_length separately, don't override max_tokens
return { ...m, context_length: ctx }
}
}
// Fallback to default if missing
return m
} catch {
return m
}
})
)
setAvailableModels(enrichedModels)
// Only navigate if we have models
if (validModels.length > 0) {
if (enrichedModels.length > 0) {
navigateTo('model')
} else {
setModelLoadError('No models found in your Ollama installation')
}
return validModels
return enrichedModels
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error)
@ -1403,7 +1494,15 @@ export function ModelSelector({
setReasoningEffort(null)
}
// Set context length if available (from Ollama /api/show)
if (modelInfo?.context_length) {
setContextLength(modelInfo.context_length)
} else {
setContextLength(DEFAULT_CONTEXT_LENGTH)
}
// Set max tokens based on model info or default
// Note: max_tokens is for OUTPUT, not context window
if (modelInfo?.max_tokens) {
const modelMaxTokens = modelInfo.max_tokens
// Check if the model's max tokens matches any of our presets

View File

@ -1,9 +1,9 @@
import React from 'react'
import { Text, Box } from 'ink'
import { getModelManager } from '../utils/model'
import { getGlobalConfig } from '../utils/config'
import { useExitOnCtrlCD } from '../hooks/useExitOnCtrlCD'
import { getTheme } from '../utils/theme'
import { getModelManager } from '@utils/model'
import { getGlobalConfig } from '@utils/config'
import { useExitOnCtrlCD } from '@hooks/useExitOnCtrlCD'
import { getTheme } from '@utils/theme'
type Props = {
onClose: () => void
@ -44,7 +44,7 @@ export function ModelStatusDisplay({ onClose }: Props): React.ReactNode {
<Box key={pointer} flexDirection="column" marginBottom={1}>
<Text>
🎯{' '}
<Text bold color={theme.claude}>
<Text bold color={theme.kode}>
{pointer.toUpperCase()}
</Text>{' '}
{model.name}
@ -76,7 +76,7 @@ export function ModelStatusDisplay({ onClose }: Props): React.ReactNode {
<Box key={pointer} flexDirection="column" marginBottom={1}>
<Text>
🎯{' '}
<Text bold color={theme.claude}>
<Text bold color={theme.kode}>
{pointer.toUpperCase()}
</Text>{' '}
<Text color={theme.error}> Not configured</Text>
@ -89,7 +89,7 @@ export function ModelStatusDisplay({ onClose }: Props): React.ReactNode {
<Box key={pointer} flexDirection="column" marginBottom={1}>
<Text>
🎯{' '}
<Text bold color={theme.claude}>
<Text bold color={theme.kode}>
{pointer.toUpperCase()}
</Text>{' '}
{' '}

View File

@ -1,19 +1,19 @@
import React, { useState } from 'react'
import { PRODUCT_NAME } from '../constants/product'
import { PRODUCT_NAME } from '@constants/product'
import { Box, Newline, Text, useInput } from 'ink'
import {
getGlobalConfig,
saveGlobalConfig,
DEFAULT_GLOBAL_CONFIG,
ProviderType,
} from '../utils/config.js'
} from '@utils/config'
import { OrderedList } from '@inkjs/ui'
import { useExitOnCtrlCD } from '../hooks/useExitOnCtrlCD'
import { useExitOnCtrlCD } from '@hooks/useExitOnCtrlCD'
import { MIN_LOGO_WIDTH } from './Logo'
import { Select } from './CustomSelect/select'
import { StructuredDiff } from './StructuredDiff'
import { getTheme, type ThemeNames } from '../utils/theme'
import { clearTerminal } from '../utils/terminal'
import { getTheme, type ThemeNames } from '@utils/theme'
import { clearTerminal } from '@utils/terminal'
import { PressEnterToContinue } from './PressEnterToContinue'
import { ModelSelector } from './ModelSelector'
type StepId = 'theme' | 'usage' | 'providers' | 'model'
@ -260,13 +260,13 @@ export function WelcomeBox(): React.ReactNode {
const theme = getTheme()
return (
<Box
borderColor={theme.claude}
borderColor={theme.kode}
borderStyle="round"
paddingX={1}
width={MIN_LOGO_WIDTH}
>
<Text>
<Text color={theme.claude}></Text> Welcome to{' '}
<Text color={theme.kode}></Text> Welcome to{' '}
<Text bold>{PRODUCT_NAME}</Text> research preview!
</Text>
</Box>

View File

@ -1,5 +1,5 @@
import * as React from 'react'
import { getTheme } from '../utils/theme'
import { getTheme } from '@utils/theme'
import { Text } from 'ink'
export function PressEnterToContinue(): React.ReactNode {

View File

@ -6,17 +6,17 @@ import {
getGlobalConfig,
saveCurrentProjectConfig,
saveGlobalConfig,
} from './utils/config.js'
} from '@utils/config'
import { existsSync } from 'fs'
import { join } from 'path'
import { homedir } from 'os'
import terminalSetup from './commands/terminalSetup'
import { getTheme } from './utils/theme'
import { RELEASE_NOTES } from './constants/releaseNotes'
import terminalSetup from '@commands/terminalSetup'
import { getTheme } from '@utils/theme'
import { RELEASE_NOTES } from '@constants/releaseNotes'
import { gt } from 'semver'
import { isDirEmpty } from './utils/file'
import { MACRO } from './constants/macros'
import { PROJECT_FILE, PRODUCT_NAME } from './constants/product'
import { isDirEmpty } from '@utils/file'
import { MACRO } from '@constants/macros'
import { PROJECT_FILE, PRODUCT_NAME } from '@constants/product'
// Function to mark onboarding as complete
export function markProjectOnboardingComplete(): void {
@ -74,9 +74,9 @@ export default function ProjectOnboarding({
// Load what we need for onboarding
// NOTE: This whole component is statically rendered Once
const hasClaudeMd = existsSync(join(workspaceDir, PROJECT_FILE))
const workspaceHasProjectGuide = existsSync(join(workspaceDir, PROJECT_FILE))
const isWorkspaceDirEmpty = isDirEmpty(workspaceDir)
const needsClaudeMd = !hasClaudeMd && !isWorkspaceDirEmpty
const shouldRecommendProjectGuide = !workspaceHasProjectGuide && !isWorkspaceDirEmpty
const showTerminalTip =
terminalSetup.isEnabled && !getGlobalConfig().shiftEnterKeyBindingInstalled
@ -106,9 +106,9 @@ export default function ProjectOnboarding({
</React.Fragment>,
)
}
if (needsClaudeMd) {
if (shouldRecommendProjectGuide) {
items.push(
<React.Fragment key="claudemd">
<React.Fragment key="projectGuide">
{/* @ts-expect-error - OrderedList.Item children prop issue */}
<OrderedList.Item>
<Text color={theme.secondaryText}>

View File

@ -1,37 +1,34 @@
import { Box, Text, useInput } from 'ink'
import { sample } from 'lodash-es'
import { getExampleCommands } from '../utils/exampleCommands'
import * as React from 'react'
import { type Message } from '../query'
import { processUserInput } from '../utils/messages'
import { useArrowKeyHistory } from '../hooks/useArrowKeyHistory'
import { useUnifiedCompletion } from '../hooks/useUnifiedCompletion'
import { addToHistory } from '../history'
import { type Message } from '@query'
import { processUserInput } from '@utils/messages'
import { useArrowKeyHistory } from '@hooks/useArrowKeyHistory'
import { useUnifiedCompletion } from '@hooks/useUnifiedCompletion'
import { addToHistory } from '@history'
import TextInput from './TextInput'
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { countTokens } from '../utils/tokens'
import { countTokens } from '@utils/tokens'
import { SentryErrorBoundary } from './SentryErrorBoundary'
import { AutoUpdater } from './AutoUpdater'
import type { AutoUpdaterResult } from '../utils/autoUpdater'
import type { Command } from '../commands'
import type { SetToolJSXFn, Tool } from '../Tool'
import type { Command } from '@commands'
import type { SetToolJSXFn, Tool } from '@tool'
import { TokenWarning, WARNING_THRESHOLD } from './TokenWarning'
import { useTerminalSize } from '../hooks/useTerminalSize'
import { getTheme } from '../utils/theme'
import { getModelManager, reloadModelManager } from '../utils/model'
import { saveGlobalConfig } from '../utils/config'
import { setTerminalTitle } from '../utils/terminal'
import { useTerminalSize } from '@hooks/useTerminalSize'
import { getTheme } from '@utils/theme'
import { getModelManager, reloadModelManager } from '@utils/model'
import { saveGlobalConfig } from '@utils/config'
import { setTerminalTitle } from '@utils/terminal'
import terminalSetup, {
isShiftEnterKeyBindingInstalled,
handleHashCommand,
} from '../commands/terminalSetup'
import { usePermissionContext } from '../context/PermissionContext'
} from '@commands/terminalSetup'
import { usePermissionContext } from '@context/PermissionContext'
// Async function to interpret the '#' command input using AI
async function interpretHashCommand(input: string): Promise<string> {
// Use the AI to interpret the input
try {
const { queryQuick } = await import('../services/claude')
const { queryQuick } = await import('@services/claude')
// Create a prompt for the model to interpret the hash command
const systemPrompt = [
@ -79,8 +76,6 @@ type Props = {
verbose: boolean
messages: Message[]
setToolJSX: SetToolJSXFn
onAutoUpdaterResult: (result: AutoUpdaterResult) => void
autoUpdaterResult: AutoUpdaterResult | null
tools: Tool[]
input: string
onInputChange: (value: string) => void
@ -114,8 +109,6 @@ function PromptInput({
verbose,
messages,
setToolJSX,
onAutoUpdaterResult,
autoUpdaterResult,
tools,
input,
onInputChange,
@ -131,7 +124,6 @@ function PromptInput({
readFileTimestamps,
onModelChange,
}: Props): React.ReactNode {
const [isAutoUpdating, setIsAutoUpdating] = useState(false)
const [exitMessage, setExitMessage] = useState<{
show: boolean
key?: string
@ -316,7 +308,7 @@ function PromptInput({
addToHistory(mode === 'koding' ? `#${input}` : input)
onInputChange('')
// Create additional context to inform Claude this is for KODING.md
// Create additional context to inform the assistant this is for KODING.md
const kodingContext =
'The user is using Koding mode. Format your response as a comprehensive, well-structured document suitable for adding to AGENTS.md. Use proper markdown formatting with headings, lists, code blocks, etc. The response should be complete and ready to add to AGENTS.md documentation.'
@ -362,8 +354,8 @@ function PromptInput({
if (messages.length) {
await onQuery(messages)
// After query completes, the last message should be Claude's response
// We'll set up a one-time listener to capture and save Claude's response
// After query completes, the last message should be the assistant's response
// We'll set up a one-time listener to capture and save that response
// This will be handled by the REPL component or message handler
}
@ -532,7 +524,7 @@ function PromptInput({
onShowMessageSelector()
}
// Shift+Tab for mode cycling (matching original Claude Code implementation)
// Shift+Tab for mode cycling (retains legacy keyboard behavior)
if (key.shift && key.tab) {
cycleMode()
return true // Explicitly handled
@ -596,7 +588,7 @@ function PromptInput({
mode === 'bash'
? theme.bashBorder
: mode === 'koding'
? theme.koding
? theme.noting
: theme.secondaryBorder
}
borderDimColor
@ -614,7 +606,7 @@ function PromptInput({
{mode === 'bash' ? (
<Text color={theme.bashBorder}>&nbsp;!&nbsp;</Text>
) : mode === 'koding' ? (
<Text color={theme.koding}>&nbsp;#&nbsp;</Text>
<Text color={theme.noting}>&nbsp;#&nbsp;</Text>
) : (
<Text color={isLoading ? theme.secondaryText : undefined}>
&nbsp;&gt;&nbsp;
@ -668,7 +660,7 @@ function PromptInput({
! for bash mode
</Text>
<Text
color={mode === 'koding' ? theme.koding : undefined}
color={mode === 'koding' ? theme.noting : undefined}
dimColor={mode !== 'koding'}
>
· # for AGENTS.md
@ -681,9 +673,7 @@ function PromptInput({
</Box>
<SentryErrorBoundary children={
<Box justifyContent="flex-end" gap={1}>
{!autoUpdaterResult &&
!isAutoUpdating &&
!debug &&
{!debug &&
tokenUsage < WARNING_THRESHOLD && (
<Text dimColor>
{terminalSetup.isEnabled &&
@ -693,13 +683,6 @@ function PromptInput({
</Text>
)}
<TokenWarning tokenUsage={tokenUsage} />
{/* <AutoUpdater
debug={debug}
onAutoUpdaterResult={onAutoUpdaterResult}
autoUpdaterResult={autoUpdaterResult}
isUpdating={isAutoUpdating}
onChangeIsUpdating={setIsAutoUpdating}
/> */}
</Box>
} />
</Box>
@ -737,13 +720,6 @@ function PromptInput({
<SentryErrorBoundary children={
<Box justifyContent="flex-end" gap={1}>
<TokenWarning tokenUsage={countTokens(messages)} />
<AutoUpdater
debug={debug}
onAutoUpdaterResult={onAutoUpdaterResult}
autoUpdaterResult={autoUpdaterResult}
isUpdating={isAutoUpdating}
onChangeIsUpdating={setIsAutoUpdating}
/>
</Box>
} />
</Box>

View File

@ -1,5 +1,5 @@
import * as React from 'react'
import { captureException } from '../services/sentry'
import { captureException } from '@services/sentry'
interface Props {
children: React.ReactNode

View File

@ -1,9 +1,9 @@
import { Box, Text } from 'ink'
import * as React from 'react'
import { useEffect, useRef, useState } from 'react'
import { getTheme } from '../utils/theme'
import { getTheme } from '@utils/theme'
import { sample } from 'lodash-es'
import { getSessionState } from '../utils/sessionState'
import { getSessionState } from '@utils/sessionState'
// NB: The third character in this string is an emoji that
// renders on Windows consoles with a green background
const CHARACTERS =
@ -96,9 +96,9 @@ export function Spinner(): React.ReactNode {
return (
<Box flexDirection="row" marginTop={1}>
<Box flexWrap="nowrap" height={1} width={2}>
<Text color={getTheme().claude}>{frames[frame]}</Text>
<Text color={getTheme().kode}>{frames[frame]}</Text>
</Box>
<Text color={getTheme().claude}>{message.current} </Text>
<Text color={getTheme().kode}>{message.current} </Text>
<Text color={getTheme().secondaryText}>
({elapsedTime}s · <Text bold>esc</Text> to interrupt)
</Text>
@ -123,7 +123,7 @@ export function SimpleSpinner(): React.ReactNode {
return (
<Box flexWrap="nowrap" height={1} width={2}>
<Text color={getTheme().claude}>{frames[frame]}</Text>
<Text color={getTheme().kode}>{frames[frame]}</Text>
</Box>
)
}

View File

@ -1,16 +0,0 @@
import React from 'react'
export interface FormData {
// Define form data structure as needed
[key: string]: any
}
export interface StickerRequestFormProps {
// Define props as needed
onSubmit?: (data: FormData) => void
}
export const StickerRequestForm: React.FC<StickerRequestFormProps> = () => {
// Minimal component implementation
return null
}

View File

@ -1,9 +1,9 @@
import { Box, Text } from 'ink'
import * as React from 'react'
import { Hunk } from 'diff'
import { getTheme, ThemeNames } from '../utils/theme'
import { getTheme, ThemeNames } from '@utils/theme'
import { useMemo } from 'react'
import { wrapText } from '../utils/format'
import { wrapText } from '@utils/format'
type Props = {
patch: Hunk

View File

@ -1,8 +1,8 @@
import React from 'react'
import { Text, useInput } from 'ink'
import chalk from 'chalk'
import { useTextInput } from '../hooks/useTextInput'
import { getTheme } from '../utils/theme'
import { useTextInput } from '@hooks/useTextInput'
import { getTheme } from '@utils/theme'
import { type Key } from 'ink'
export type Props = {

View File

@ -1,6 +1,6 @@
import React from 'react'
import { Box, Text } from 'ink'
import type { TodoItem as TodoItemType } from '../utils/todoStorage'
import type { TodoItem as TodoItemType } from '@utils/todoStorage'
export interface TodoItemProps {
todo: TodoItemType

View File

@ -1,6 +1,6 @@
import { Box, Text } from 'ink'
import * as React from 'react'
import { getTheme } from '../utils/theme'
import { getTheme } from '@utils/theme'
type Props = {
tokenUsage: number

View File

@ -1,8 +1,8 @@
import { Box, Text } from 'ink'
import React from 'react'
import { useInterval } from '../hooks/useInterval'
import { getTheme } from '../utils/theme'
import { BLACK_CIRCLE } from '../constants/figures'
import { useInterval } from '@hooks/useInterval'
import { getTheme } from '@utils/theme'
import { BLACK_CIRCLE } from '@constants/figures'
type Props = {
isError: boolean

View File

@ -1,16 +1,15 @@
import React from 'react'
import { Box, Text, useInput } from 'ink'
import { getTheme } from '../utils/theme'
import { getTheme } from '@utils/theme'
import { Select } from './CustomSelect/select'
import {
saveCurrentProjectConfig,
getCurrentProjectConfig,
} from '../utils/config.js'
import { PRODUCT_NAME } from '../constants/product'
import { logEvent } from '../services/statsig'
import { useExitOnCtrlCD } from '../hooks/useExitOnCtrlCD'
} from '@utils/config'
import { PRODUCT_NAME } from '@constants/product'
import { useExitOnCtrlCD } from '@hooks/useExitOnCtrlCD'
import { homedir } from 'os'
import { getCwd } from '../utils/state'
import { getCwd } from '@utils/state'
import Link from './Link'
type Props = {
@ -19,20 +18,13 @@ type Props = {
export function TrustDialog({ onDone }: Props): React.ReactNode {
const theme = getTheme()
React.useEffect(() => {
// Log when dialog is shown
logEvent('trust_dialog_shown', {})
}, [])
React.useEffect(() => {}, [])
function onChange(value: 'yes' | 'no') {
const config = getCurrentProjectConfig()
switch (value) {
case 'yes': {
// Log when user accepts
const isHomeDir = homedir() === getCwd()
logEvent('trust_dialog_accept', {
isHomeDir: String(isHomeDir),
})
if (!isHomeDir) {
saveCurrentProjectConfig({

View File

@ -1,15 +1,15 @@
import { default as React, useCallback } from 'react'
import { useNotifyAfterTimeout } from '../../hooks/useNotifyAfterTimeout'
import { AssistantMessage, BinaryFeedbackResult } from '../../query'
import type { Tool } from '../../Tool'
import type { NormalizedMessage } from '../../utils/messages'
import { useNotifyAfterTimeout } from '@hooks/useNotifyAfterTimeout'
import { AssistantMessage, BinaryFeedbackResult } from '@query'
import type { Tool } from '@tool'
import type { NormalizedMessage } from '@utils/messages'
import { BinaryFeedbackView } from './BinaryFeedbackView'
import {
type BinaryFeedbackChoose,
getBinaryFeedbackResultForChoice,
logBinaryFeedbackEvent,
} from './utils.js'
import { PRODUCT_NAME } from '../../constants/product'
} from './utils'
import { PRODUCT_NAME } from '@constants/product'
type Props = {
m1: AssistantMessage

View File

@ -1,15 +1,15 @@
import { FileEditTool } from '../../tools/FileEditTool/FileEditTool'
import { FileEditToolDiff } from '../permissions/FileEditPermissionRequest/FileEditToolDiff'
import { Message } from '../Message'
import { FileEditTool } from '@tools/FileEditTool/FileEditTool'
import { FileEditToolDiff } from '@components/permissions/FileEditPermissionRequest/FileEditToolDiff'
import { Message } from '@components/Message'
import {
normalizeMessages,
type NormalizedMessage,
} from '../../utils/messages.js'
import type { Tool } from '../../Tool'
import { useTerminalSize } from '../../hooks/useTerminalSize'
import { FileWriteTool } from '../../tools/FileWriteTool/FileWriteTool'
import { FileWriteToolDiff } from '../permissions/FileWritePermissionRequest/FileWriteToolDiff'
import type { AssistantMessage } from '../../query'
} from '@utils/messages'
import type { Tool } from '@tool'
import { useTerminalSize } from '@hooks/useTerminalSize'
import { FileWriteTool } from '@tools/FileWriteTool/FileWriteTool'
import { FileWriteToolDiff } from '@components/permissions/FileWritePermissionRequest/FileWriteToolDiff'
import type { AssistantMessage } from '@query'
import * as React from 'react'
import { Box } from 'ink'

View File

@ -3,16 +3,16 @@ import chalk from 'chalk'
import { Box, Text, useInput } from 'ink'
import Link from 'ink-link'
import React, { useState } from 'react'
import { getTheme } from '../../utils/theme'
import { Select } from '../CustomSelect/select'
import type { Tool } from '../../Tool'
import type { NormalizedMessage } from '../../utils/messages'
import { getTheme } from '@utils/theme'
import { Select } from '@components/CustomSelect/select'
import type { Tool } from '@tool'
import type { NormalizedMessage } from '@utils/messages'
import { BinaryFeedbackOption } from './BinaryFeedbackOption'
import type { AssistantMessage } from '../../query'
import type { AssistantMessage } from '@query'
import type { BinaryFeedbackChoose } from './utils'
import { useExitOnCtrlCD } from '../../hooks/useExitOnCtrlCD'
import { useExitOnCtrlCD } from '@hooks/useExitOnCtrlCD'
import { BinaryFeedbackChoice } from './utils'
import { PRODUCT_NAME } from '../../constants/product'
import { PRODUCT_NAME } from '@constants/product'
const HELP_URL = 'https://go/cli-feedback'

View File

@ -1,10 +1,9 @@
import { TextBlock, ToolUseBlock } from '@anthropic-ai/sdk/resources/index.mjs'
import { AssistantMessage, BinaryFeedbackResult } from '../../query'
import { MAIN_QUERY_TEMPERATURE } from '../../services/claude'
import { getDynamicConfig, logEvent } from '../../services/statsig'
import { AssistantMessage, BinaryFeedbackResult } from '@query'
import { MAIN_QUERY_TEMPERATURE } from '@services/claude'
import { isEqual, zip } from 'lodash-es'
import { getGitState } from '../../utils/git'
import { getGitState } from '@utils/git'
export type BinaryFeedbackChoice =
| 'prefer-left'
@ -18,10 +17,8 @@ type BinaryFeedbackConfig = {
sampleFrequency: number
}
async function getBinaryFeedbackStatsigConfig(): Promise<BinaryFeedbackConfig> {
return await getDynamicConfig('tengu-binary-feedback-config', {
sampleFrequency: 0,
})
async function getBinaryFeedbackConfig(): Promise<BinaryFeedbackConfig> {
return { sampleFrequency: 0 }
}
function getMessageBlockSequence(m: AssistantMessage) {
@ -32,63 +29,7 @@ function getMessageBlockSequence(m: AssistantMessage) {
})
}
export async function logBinaryFeedbackEvent(
m1: AssistantMessage,
m2: AssistantMessage,
choice: BinaryFeedbackChoice,
): Promise<void> {
const modelA = m1.message.model
const modelB = m2.message.model
const gitState = await getGitState()
logEvent('tengu_binary_feedback', {
msg_id_A: m1.message.id,
msg_id_B: m2.message.id,
choice: {
'prefer-left': m1.message.id,
'prefer-right': m2.message.id,
neither: undefined,
'no-preference': undefined,
}[choice],
choiceStr: choice,
gitHead: gitState?.commitHash,
gitBranch: gitState?.branchName,
gitRepoRemoteUrl: gitState?.remoteUrl || undefined,
gitRepoIsHeadOnRemote: gitState?.isHeadOnRemote?.toString(),
gitRepoIsClean: gitState?.isClean?.toString(),
modelA,
modelB,
temperatureA: String(MAIN_QUERY_TEMPERATURE),
temperatureB: String(MAIN_QUERY_TEMPERATURE),
seqA: String(getMessageBlockSequence(m1)),
seqB: String(getMessageBlockSequence(m2)),
})
}
export async function logBinaryFeedbackSamplingDecision(
decision: boolean,
reason?: string,
): Promise<void> {
logEvent('tengu_binary_feedback_sampling_decision', {
decision: decision.toString(),
reason,
})
}
export async function logBinaryFeedbackDisplayDecision(
decision: boolean,
m1: AssistantMessage,
m2: AssistantMessage,
reason?: string,
): Promise<void> {
logEvent('tengu_binary_feedback_display_decision', {
decision: decision.toString(),
reason,
msg_id_A: m1.message.id,
msg_id_B: m2.message.id,
seqA: String(getMessageBlockSequence(m1)),
seqB: String(getMessageBlockSequence(m2)),
})
}
// Logging removed to minimize runtime surface area; behavior unaffected
function textContentBlocksEqual(cb1: TextBlock, cb2: TextBlock): boolean {
return cb1.text === cb2.text
@ -122,34 +63,27 @@ function allContentBlocksEqual(
export async function shouldUseBinaryFeedback(): Promise<boolean> {
if (process.env.DISABLE_BINARY_FEEDBACK) {
logBinaryFeedbackSamplingDecision(false, 'disabled_by_env_var')
return false
}
if (process.env.FORCE_BINARY_FEEDBACK) {
logBinaryFeedbackSamplingDecision(true, 'forced_by_env_var')
return true
}
if (process.env.USER_TYPE !== 'ant') {
logBinaryFeedbackSamplingDecision(false, 'not_ant')
return false
}
if (process.env.NODE_ENV === 'test') {
// Binary feedback breaks a couple tests related to checking for permission,
// so we have to disable it in tests at the risk of hiding bugs
logBinaryFeedbackSamplingDecision(false, 'test')
return false
}
const config = await getBinaryFeedbackStatsigConfig()
const config = await getBinaryFeedbackConfig()
if (config.sampleFrequency === 0) {
logBinaryFeedbackSamplingDecision(false, 'top_level_frequency_zero')
return false
}
if (Math.random() > config.sampleFrequency) {
logBinaryFeedbackSamplingDecision(false, 'top_level_frequency_rng')
return false
}
logBinaryFeedbackSamplingDecision(true)
return true
}
@ -157,9 +91,8 @@ export function messagePairValidForBinaryFeedback(
m1: AssistantMessage,
m2: AssistantMessage,
): boolean {
const logPass = () => logBinaryFeedbackDisplayDecision(true, m1, m2)
const logFail = (reason: string) =>
logBinaryFeedbackDisplayDecision(false, m1, m2, reason)
const logPass = () => {}
const logFail = (_reason: string) => {}
// Ignore thinking blocks, on the assumption that users don't find them very relevant
// compared to other content types
@ -218,3 +151,9 @@ export function getBinaryFeedbackResultForChoice(
return { message: null, shouldSkipPermissionCheck: false }
}
}
// Keep a minimal exported stub to satisfy imports without side effects
export async function logBinaryFeedbackEvent(
_m1: AssistantMessage,
_m2: AssistantMessage,
_choice: BinaryFeedbackChoice,
): Promise<void> {}

View File

@ -1,6 +1,6 @@
import * as React from 'react'
import BashToolResultMessage from '../../tools/BashTool/BashToolResultMessage'
import { extractTag } from '../../utils/messages'
import BashToolResultMessage from '@tools/BashTool/BashToolResultMessage'
import { extractTag } from '@utils/messages'
export function AssistantBashOutputMessage({
content,

View File

@ -1,6 +1,6 @@
import * as React from 'react'
import { extractTag } from '../../utils/messages'
import { getTheme } from '../../utils/theme'
import { extractTag } from '@utils/messages'
import { getTheme } from '@utils/theme'
import { Box, Text } from 'ink'
export function AssistantLocalCommandOutputMessage({

View File

@ -1,6 +1,6 @@
import React from 'react'
import { Box, Text } from 'ink'
import { getTheme } from '../../utils/theme'
import { getTheme } from '@utils/theme'
type Props = {
addMargin: boolean

View File

@ -2,25 +2,25 @@ import { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
import React from 'react'
import { AssistantBashOutputMessage } from './AssistantBashOutputMessage'
import { AssistantLocalCommandOutputMessage } from './AssistantLocalCommandOutputMessage'
import { getTheme } from '../../utils/theme'
import { getTheme } from '@utils/theme'
import { Box, Text } from 'ink'
import { Cost } from '../Cost'
import { Cost } from '@components/Cost'
import {
API_ERROR_MESSAGE_PREFIX,
CREDIT_BALANCE_TOO_LOW_ERROR_MESSAGE,
INVALID_API_KEY_ERROR_MESSAGE,
PROMPT_TOO_LONG_ERROR_MESSAGE,
} from '../../services/claude.js'
} from '@services/claude'
import {
CANCEL_MESSAGE,
INTERRUPT_MESSAGE,
INTERRUPT_MESSAGE_FOR_TOOL_USE,
isEmptyMessageText,
NO_RESPONSE_REQUESTED,
} from '../../utils/messages.js'
import { BLACK_CIRCLE } from '../../constants/figures'
import { applyMarkdown } from '../../utils/markdown'
import { useTerminalSize } from '../../hooks/useTerminalSize'
} from '@utils/messages'
import { BLACK_CIRCLE } from '@constants/figures'
import { applyMarkdown } from '@utils/markdown'
import { useTerminalSize } from '@hooks/useTerminalSize'
type Props = {
param: TextBlockParam
@ -74,7 +74,7 @@ export function AssistantTextMessage({
}
switch (text) {
// Local JSX commands don't need a response, but we still want Claude to see them
// Local JSX commands don't need a response, but we still want the assistant to see them
// Tool results render their own interrupt messages
case NO_RESPONSE_REQUESTED:
case INTERRUPT_MESSAGE_FOR_TOOL_USE:

View File

@ -1,7 +1,7 @@
import React from 'react'
import { Box, Text } from 'ink'
import { getTheme } from '../../utils/theme'
import { applyMarkdown } from '../../utils/markdown'
import { getTheme } from '@utils/theme'
import { applyMarkdown } from '@utils/markdown'
import {
ThinkingBlock,
ThinkingBlockParam,

View File

@ -1,13 +1,13 @@
import { Box, Text } from 'ink'
import React from 'react'
import { logError } from '../../utils/log'
import { logError } from '@utils/log'
import { ToolUseBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
import { Tool } from '../../Tool'
import { Cost } from '../Cost'
import { ToolUseLoader } from '../ToolUseLoader'
import { getTheme } from '../../utils/theme'
import { BLACK_CIRCLE } from '../../constants/figures'
import { ThinkTool } from '../../tools/ThinkTool/ThinkTool'
import { Tool } from '@tool'
import { Cost } from '@components/Cost'
import { ToolUseLoader } from '@components/ToolUseLoader'
import { getTheme } from '@utils/theme'
import { BLACK_CIRCLE } from '@constants/figures'
import { ThinkTool } from '@tools/ThinkTool/ThinkTool'
import { AssistantThinkingMessage } from './AssistantThinkingMessage'
import { TaskToolMessage } from './TaskToolMessage'
@ -50,9 +50,8 @@ export function AssistantToolUseMessage({
// Keeping color undefined makes the OS use the default color regardless of appearance
const color = isQueued ? getTheme().secondaryText : undefined
// TODO: Avoid this special case
// Handle thinking tool with specialized rendering
if (tool === ThinkTool) {
// params were already validated in query(), so this won't throe
const { thought } = ThinkTool.inputSchema.parse(param.input)
return (
<AssistantThinkingMessage
@ -62,7 +61,7 @@ export function AssistantToolUseMessage({
)
}
const userFacingToolName = tool.userFacingName ? tool.userFacingName(param.input) : tool.name
const userFacingToolName = tool.userFacingName ? tool.userFacingName() : tool.name
return (
<Box
flexDirection="row"
@ -89,11 +88,10 @@ export function AssistantToolUseMessage({
))}
{tool.name === 'Task' && param.input ? (
<TaskToolMessage
agentType={(param.input as any).subagent_type || 'general-purpose'}
bold={!isQueued}
>
{userFacingToolName}
</TaskToolMessage>
agentType={String((param.input as any).subagent_type || 'general-purpose')}
bold={Boolean(!isQueued)}
children={String(userFacingToolName || '')}
/>
) : (
<Text color={color} bold={!isQueued}>
{userFacingToolName}

View File

@ -1,6 +1,6 @@
import React from 'react'
import { Box, Text } from 'ink'
import { getTheme } from '../../utils/theme'
import { getTheme } from '@utils/theme'
interface Props {
agentType: string
@ -14,7 +14,7 @@ export function TaskProgressMessage({ agentType, status, toolCount }: Props) {
return (
<Box flexDirection="column" marginTop={1}>
<Box flexDirection="row">
<Text color={theme.claude}> </Text>
<Text color={theme.kode}> </Text>
<Text color={theme.text} bold>
[{agentType}]
</Text>
@ -29,4 +29,4 @@ export function TaskProgressMessage({ agentType, status, toolCount }: Props) {
)}
</Box>
)
}
}

View File

@ -1,7 +1,7 @@
import React, { useEffect, useState, useMemo } from 'react'
import { Text } from 'ink'
import { getAgentByType } from '../../utils/agentLoader'
import { getTheme } from '../../utils/theme'
import { getAgentByType } from '@utils/agentLoader'
import { getTheme } from '@utils/theme'
interface Props {
agentType: string

View File

@ -1,7 +1,7 @@
import { Box, Text } from 'ink'
import * as React from 'react'
import { extractTag } from '../../utils/messages'
import { getTheme } from '../../utils/theme'
import { extractTag } from '@utils/messages'
import { getTheme } from '@utils/theme'
import { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
type Props = {

View File

@ -1,7 +1,7 @@
import { Box, Text } from 'ink'
import * as React from 'react'
import { getTheme } from '../../utils/theme'
import { extractTag } from '../../utils/messages'
import { getTheme } from '@utils/theme'
import { extractTag } from '@utils/messages'
import { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
type Props = {

View File

@ -1,7 +1,7 @@
import { Box, Text } from 'ink'
import * as React from 'react'
import { extractTag } from '../../utils/messages'
import { getTheme } from '../../utils/theme'
import { extractTag } from '@utils/messages'
import { getTheme } from '@utils/theme'
import { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
type Props = {
@ -20,7 +20,7 @@ export function UserKodingInputMessage({
return (
<Box flexDirection="column" marginTop={addMargin ? 1 : 0} width="100%">
<Box>
<Text color={getTheme().koding}>#</Text>
<Text color={getTheme().noting}>#</Text>
<Text color={getTheme().secondaryText}> {input}</Text>
</Box>
</Box>

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