Compare commits
40 Commits
v1.1.16-de
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52cb635512 | ||
|
|
3d7f81242b | ||
|
|
14f9892bb5 | ||
|
|
25adc80161 | ||
|
|
8288378dbd | ||
|
|
c8ecba04d8 | ||
|
|
34cd4e250d | ||
|
|
be6477cca7 | ||
|
|
7069893d14 | ||
|
|
3c9b0ec9d1 | ||
|
|
a4c3f16c2b | ||
|
|
e50c6f52f6 | ||
|
|
893112e43c | ||
|
|
f934cfa62e | ||
|
|
1b3b0786ca | ||
|
|
f486925c06 | ||
|
|
ab0d3f26f3 | ||
|
|
70f0d6b109 | ||
|
|
451362256c | ||
|
|
fd1d6e385d | ||
|
|
ce8c8dad63 | ||
|
|
b847352101 | ||
|
|
61a8ce0d22 | ||
|
|
59dce97350 | ||
|
|
d4abb2abee | ||
|
|
78b49355cd | ||
|
|
fbb2db6963 | ||
|
|
d0d1dca009 | ||
|
|
6bbaa6c559 | ||
|
|
b0d9f58d76 | ||
|
|
a7af9834ef | ||
|
|
53234ba2d9 | ||
|
|
20111a0a26 | ||
|
|
f857fb9577 | ||
|
|
da15ca5a5f | ||
|
|
f23787f50d | ||
|
|
32a9badecd | ||
|
|
5471b2d5fc | ||
|
|
97684a3208 | ||
|
|
efe00eee3b |
18
.env.example
Normal file
18
.env.example
Normal 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
19
.eslintrc.cjs
Normal 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
5
.husky/pre-commit
Executable file
@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
bun run format:check
|
||||
bun run typecheck
|
||||
12
AGENTS.md
12
AGENTS.md
@ -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
|
||||
|
||||
44
CLAUDE.md
44
CLAUDE.md
@ -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
|
||||
|
||||
15
Dockerfile
15
Dockerfile
@ -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"]
|
||||
|
||||
56
README.md
56
README.md
@ -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>
|
||||
[](https://www.npmjs.com/package/@shareai-lab/kode)
|
||||
[](https://opensource.org/licenses/Apache-2.0)
|
||||
[](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 (Unix‑like) 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 Code’s 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
|
||||
|
||||
@ -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
202
docs/PUBLISH_GUIDE.md
Normal 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操作完全可控
|
||||
- 🔄 灵活的版本回滚和恢复机制
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
63
docs/develop/modules/openai-adapters.md
Normal file
63
docs/develop/modules/openai-adapters.md
Normal file
@ -0,0 +1,63 @@
|
||||
# OpenAI Adapter Layer
|
||||
|
||||
This module explains how Kode’s 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 provider’s 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 Anthropic’s 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`.
|
||||
51
package.json
51
package.json
@ -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
195
scripts/build.mjs
Normal 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)
|
||||
})
|
||||
103
scripts/build.ts
103
scripts/build.ts
@ -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 };
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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
89
scripts/publish-dev.js
Executable 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
142
scripts/publish-release.js
Executable 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();
|
||||
@ -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();
|
||||
13
src/Tool.ts
13
src/Tool.ts
@ -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
|
||||
>
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -2,7 +2,7 @@ import {
|
||||
ProjectConfig,
|
||||
getCurrentProjectConfig as getCurrentProjectConfigDefault,
|
||||
saveCurrentProjectConfig as saveCurrentProjectConfigDefault,
|
||||
} from '../utils/config.js'
|
||||
} from '@utils/config'
|
||||
|
||||
export type ProjectConfigHandler = {
|
||||
getCurrentProjectConfig: () => ProjectConfig
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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: (
|
||||
|
||||
@ -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:
|
||||
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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' &&
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Command } from '../commands'
|
||||
import { Command } from '@commands'
|
||||
|
||||
export default {
|
||||
type: 'prompt',
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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} · 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} · 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 · Restart to apply
|
||||
</Text>
|
||||
) : null}
|
||||
{(autoUpdaterResult?.status === 'install_failed' ||
|
||||
autoUpdaterResult?.status === 'no_permissions') && (
|
||||
<Text color={theme.error}>
|
||||
✗ Auto-update failed · Try{' '}
|
||||
<Text bold>{PRODUCT_COMMAND} doctor</Text> or{' '}
|
||||
<Text bold>npm i -g {MACRO.PACKAGE_URL}</Text>
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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])
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
}),
|
||||
|
||||
@ -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 = {
|
||||
/**
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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[]
|
||||
|
||||
@ -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 /> */}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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[]
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>{' '}
|
||||
→{' '}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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}>
|
||||
@ -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}> ! </Text>
|
||||
) : mode === 'koding' ? (
|
||||
<Text color={theme.koding}> # </Text>
|
||||
<Text color={theme.noting}> # </Text>
|
||||
) : (
|
||||
<Text color={isLoading ? theme.secondaryText : undefined}>
|
||||
>
|
||||
@ -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>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import * as React from 'react'
|
||||
import { captureException } from '../services/sentry'
|
||||
import { captureException } from '@services/sentry'
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
@ -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> {}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user