Compare commits
6 Commits
main
...
v1.1.16-de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14b1e004f6 | ||
|
|
fdf27ed0b7 | ||
|
|
a13bc7a0c6 | ||
|
|
487aef295d | ||
|
|
8ae7cb47ce | ||
|
|
be7b5b485b |
22
.eslintrc.cjs
Normal file
22
.eslintrc.cjs
Normal file
@ -0,0 +1,22 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: { node: true, es2022: true },
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: { ecmaVersion: 'latest', sourceType: 'module', ecmaFeatures: { jsx: true } },
|
||||
plugins: ['@typescript-eslint', 'react', 'react-hooks'],
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
'prettier',
|
||||
],
|
||||
settings: { react: { version: 'detect' } },
|
||||
ignorePatterns: ['dist/**', 'node_modules/**'],
|
||||
rules: {
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
},
|
||||
}
|
||||
|
||||
8
.husky/pre-commit
Normal file
8
.husky/pre-commit
Normal file
@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
# Temporarily disabled - no lint-staged configuration
|
||||
# npx lint-staged
|
||||
|
||||
echo "Pre-commit hook: skipping lint-staged (not configured)"
|
||||
|
||||
7
.prettierrc.json
Normal file
7
.prettierrc.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"semi": false,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 100
|
||||
}
|
||||
|
||||
137
.sub_task/README.md
Normal file
137
.sub_task/README.md
Normal file
@ -0,0 +1,137 @@
|
||||
# TypeScript 错误修复 - 快速参考指南
|
||||
|
||||
## 🚨 重要:必须先完成 Step 0!
|
||||
|
||||
在开始任何并行任务之前,**必须完成 `step_0_foundation_serial.md`**。这是所有其他修复的基础。
|
||||
|
||||
## 📁 文件结构
|
||||
|
||||
```
|
||||
.sub_task/
|
||||
├── README.md # 本文件
|
||||
├── execution_plan.md # 总体执行计划和依赖关系
|
||||
├── step_0_foundation_serial.md # ⚠️ 必须首先完成(串行)
|
||||
├── step_1_parallel_worker_0.md # 工具修复:ArchitectTool, FileReadTool
|
||||
├── step_1_parallel_worker_1.md # 工具修复:FileWriteTool, FileEditTool
|
||||
├── step_1_parallel_worker_2.md # 工具修复:TaskTool, MultiEditTool
|
||||
├── step_1_parallel_worker_3.md # 工具修复:其他工具
|
||||
├── step_2_parallel_worker_0.md # React 组件修复
|
||||
├── step_2_parallel_worker_1.md # Hook 系统修复
|
||||
└── step_2_parallel_worker_2.md # Service 层修复
|
||||
```
|
||||
|
||||
## 🎯 快速开始
|
||||
|
||||
### 单人执行
|
||||
```bash
|
||||
# 1. 完成基础修复
|
||||
# 打开 step_0_foundation_serial.md 并按步骤执行
|
||||
|
||||
# 2. 检查进度
|
||||
npx tsc --noEmit 2>&1 | wc -l
|
||||
|
||||
# 3. 依次完成 Step 1 的 4 个任务
|
||||
|
||||
# 4. 依次完成 Step 2 的 3 个任务
|
||||
|
||||
# 5. 最终验证
|
||||
bun run dev
|
||||
```
|
||||
|
||||
### 多人协作
|
||||
```bash
|
||||
# 人员 A:负责 Step 0(独自完成)
|
||||
# 完成后通知其他人
|
||||
|
||||
# Step 0 完成后,分配任务:
|
||||
# 人员 B:step_1_parallel_worker_0.md
|
||||
# 人员 C:step_1_parallel_worker_1.md
|
||||
# 人员 D:step_1_parallel_worker_2.md
|
||||
# 人员 E:step_1_parallel_worker_3.md
|
||||
# 人员 F:step_2_parallel_worker_0.md
|
||||
# 人员 G:step_2_parallel_worker_1.md
|
||||
# 人员 H:step_2_parallel_worker_2.md
|
||||
```
|
||||
|
||||
## 📊 进度监控
|
||||
|
||||
### 实时错误计数
|
||||
```bash
|
||||
# 查看当前错误总数
|
||||
npx tsc --noEmit 2>&1 | wc -l
|
||||
|
||||
# 查看错误分布
|
||||
npx tsc --noEmit 2>&1 | grep -oE "src/[^(]*" | cut -d: -f1 | xargs -I {} dirname {} | sort | uniq -c | sort -rn
|
||||
```
|
||||
|
||||
### 特定文件检查
|
||||
```bash
|
||||
# 检查特定工具的错误
|
||||
npx tsc --noEmit 2>&1 | grep "FileWriteTool"
|
||||
|
||||
# 检查特定目录的错误
|
||||
npx tsc --noEmit 2>&1 | grep "src/tools/"
|
||||
```
|
||||
|
||||
## ✅ 完成标准
|
||||
|
||||
每个任务文档都有详细的"完成标志"部分。确保:
|
||||
1. 所有复选框都已勾选
|
||||
2. TypeScript 错误减少到预期数量
|
||||
3. 功能测试通过
|
||||
|
||||
## 🔧 常用命令
|
||||
|
||||
```bash
|
||||
# 开发模式运行
|
||||
bun run dev
|
||||
|
||||
# TypeScript 检查
|
||||
npx tsc --noEmit
|
||||
|
||||
# 查看具体错误
|
||||
npx tsc --noEmit --pretty
|
||||
|
||||
# 测试特定功能
|
||||
bun test
|
||||
|
||||
# 格式化代码
|
||||
bun run format
|
||||
```
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
1. **不要跳过 Step 0** - 这会导致后续所有任务失败
|
||||
2. **保持原有功能** - 只修复类型,不改变业务逻辑
|
||||
3. **频繁保存** - 使用 git 定期提交进度
|
||||
4. **遇到问题** - 参考每个文档的"常见问题"部分
|
||||
|
||||
## 📈 预期成果
|
||||
|
||||
| 里程碑 | 错误数 | 完成百分比 |
|
||||
|--------|--------|-----------|
|
||||
| 初始状态 | 127 | 0% |
|
||||
| Step 0 完成 | ~80 | 37% |
|
||||
| Step 1 完成 | ~40 | 69% |
|
||||
| Step 2 完成 | ~15 | 88% |
|
||||
| 最终清理 | 0 | 100% |
|
||||
|
||||
## 🆘 获取帮助
|
||||
|
||||
如果遇到无法解决的问题:
|
||||
1. 检查对应任务文档的"常见问题"部分
|
||||
2. 查看 `execution_plan.md` 的风险部分
|
||||
3. 使用文档中提供的调试技巧
|
||||
4. 记录问题供高级开发者处理
|
||||
|
||||
## 🎉 完成后
|
||||
|
||||
所有任务完成后:
|
||||
1. 运行完整测试套件
|
||||
2. 检查没有运行时错误
|
||||
3. 更新 `tasks.md` 标记所有任务完成
|
||||
4. 庆祝成功!🎊
|
||||
|
||||
---
|
||||
|
||||
**记住:质量比速度更重要。宁可慢一点,也要确保每个修复都正确。**
|
||||
171
.sub_task/execution_plan.md
Normal file
171
.sub_task/execution_plan.md
Normal file
@ -0,0 +1,171 @@
|
||||
# TypeScript 错误修复执行计划
|
||||
|
||||
## 任务依赖关系图
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Step 0: Foundation - SERIAL<br/>必须首先完成] --> B1[Step 1 Worker 0<br/>ArchitectTool & FileReadTool]
|
||||
A --> B2[Step 1 Worker 1<br/>FileWriteTool & FileEditTool]
|
||||
A --> B3[Step 1 Worker 2<br/>TaskTool & MultiEditTool]
|
||||
A --> B4[Step 1 Worker 3<br/>Other Tools]
|
||||
|
||||
A --> C1[Step 2 Worker 0<br/>React Components]
|
||||
A --> C2[Step 2 Worker 1<br/>Hook System]
|
||||
A --> C3[Step 2 Worker 2<br/>Service Layer]
|
||||
|
||||
B1 --> D[Step 3: Final Validation]
|
||||
B2 --> D
|
||||
B3 --> D
|
||||
B4 --> D
|
||||
C1 --> D
|
||||
C2 --> D
|
||||
C3 --> D
|
||||
```
|
||||
|
||||
## 执行顺序
|
||||
|
||||
### 🔴 Phase 0: 串行任务 (必须先完成)
|
||||
**时间**: 1-2 小时
|
||||
**文件**: `step_0_foundation_serial.md`
|
||||
|
||||
此任务建立所有其他修复的基础:
|
||||
- 安装缺失依赖 (sharp)
|
||||
- 创建类型增强文件
|
||||
- 修复核心 Message 和 Tool 类型
|
||||
- 修复 Key 类型扩展
|
||||
|
||||
**重要**: 在此任务完成前,不要开始任何其他任务!
|
||||
|
||||
### 🟢 Phase 1: 并行任务组 A (Step 1)
|
||||
**时间**: 2-3 小时(并行执行)
|
||||
**可同时分配给 4 个工作者**
|
||||
|
||||
| Worker | 文件 | 负责内容 | 预计时间 |
|
||||
|--------|------|---------|----------|
|
||||
| 0 | `step_1_parallel_worker_0.md` | ArchitectTool, FileReadTool | 60分钟 |
|
||||
| 1 | `step_1_parallel_worker_1.md` | FileWriteTool, FileEditTool | 60分钟 |
|
||||
| 2 | `step_1_parallel_worker_2.md` | TaskTool, MultiEditTool | 85分钟 |
|
||||
| 3 | `step_1_parallel_worker_3.md` | StickerRequestTool, NotebookReadTool, AskExpertModelTool | 70分钟 |
|
||||
|
||||
### 🟢 Phase 2: 并行任务组 B (Step 2)
|
||||
**时间**: 1-2 小时(并行执行)
|
||||
**可同时分配给 3 个工作者**
|
||||
|
||||
| Worker | 文件 | 负责内容 | 预计时间 |
|
||||
|--------|------|---------|----------|
|
||||
| 0 | `step_2_parallel_worker_0.md` | React 19/Ink 6 组件修复 | 80分钟 |
|
||||
| 1 | `step_2_parallel_worker_1.md` | Hook 系统修复 | 60分钟 |
|
||||
| 2 | `step_2_parallel_worker_2.md` | Service 层和入口点修复 | 65分钟 |
|
||||
|
||||
### 🔵 Phase 3: 最终验证
|
||||
**时间**: 30分钟
|
||||
**所有并行任务完成后执行**
|
||||
|
||||
1. 运行完整的 TypeScript 检查
|
||||
2. 测试所有主要功能
|
||||
3. 记录剩余问题(如果有)
|
||||
|
||||
## 任务分配建议
|
||||
|
||||
### 如果有 1 个开发者
|
||||
1. 按顺序执行:Step 0 → Step 1 (worker 0-3) → Step 2 (worker 0-2)
|
||||
2. 总时间:约 6-8 小时
|
||||
|
||||
### 如果有 2 个开发者
|
||||
1. 开发者 A:Step 0 → Step 1 Worker 0 & 1 → Step 2 Worker 0
|
||||
2. 开发者 B:等待 Step 0 → Step 1 Worker 2 & 3 → Step 2 Worker 1 & 2
|
||||
3. 总时间:约 4-5 小时
|
||||
|
||||
### 如果有 4+ 个开发者
|
||||
1. 开发者 A:Step 0(独自完成)
|
||||
2. 其他开发者:等待 Step 0 完成
|
||||
3. Step 0 完成后:
|
||||
- 开发者 B-E:各自领取 Step 1 的一个 worker 任务
|
||||
- 开发者 A, F, G:各自领取 Step 2 的一个 worker 任务
|
||||
4. 总时间:约 2-3 小时
|
||||
|
||||
## 进度跟踪
|
||||
|
||||
使用以下命令跟踪进度:
|
||||
|
||||
```bash
|
||||
# 检查当前错误数量
|
||||
npx tsc --noEmit 2>&1 | wc -l
|
||||
|
||||
# 检查特定步骤的错误
|
||||
npx tsc --noEmit 2>&1 | grep "FileWriteTool" # 示例
|
||||
|
||||
# 运行测试
|
||||
bun run dev
|
||||
```
|
||||
|
||||
## 预期结果
|
||||
|
||||
| 阶段 | 预期错误减少 | 剩余错误 |
|
||||
|------|------------|----------|
|
||||
| 初始状态 | - | 127 |
|
||||
| Step 0 完成 | 40-50 | 77-87 |
|
||||
| Step 1 完成 | 35-45 | 32-52 |
|
||||
| Step 2 完成 | 25-35 | 7-27 |
|
||||
| 最终清理 | 7-27 | 0 |
|
||||
|
||||
## 风险和缓解措施
|
||||
|
||||
### 风险 1: Step 0 未正确完成
|
||||
**影响**: 所有后续任务都会遇到类型错误
|
||||
**缓解**: 严格验证 Step 0 的完成标志
|
||||
|
||||
### 风险 2: 并行任务冲突
|
||||
**影响**: 同时修改相同文件导致冲突
|
||||
**缓解**: 每个 worker 负责独立的文件集
|
||||
|
||||
### 风险 3: 运行时错误
|
||||
**影响**: 类型修复可能引入运行时问题
|
||||
**缓解**: 每个阶段后进行功能测试
|
||||
|
||||
## 通信协议
|
||||
|
||||
### 任务开始
|
||||
```
|
||||
Worker X 开始 Step Y Worker Z
|
||||
预计完成时间:HH:MM
|
||||
```
|
||||
|
||||
### 遇到问题
|
||||
```
|
||||
Worker X 遇到阻塞问题:
|
||||
- 问题描述
|
||||
- 尝试的解决方案
|
||||
- 需要的帮助
|
||||
```
|
||||
|
||||
### 任务完成
|
||||
```
|
||||
Worker X 完成 Step Y Worker Z
|
||||
- 修复错误数:N
|
||||
- 剩余问题:[列表]
|
||||
- 测试结果:[通过/失败]
|
||||
```
|
||||
|
||||
## 质量检查清单
|
||||
|
||||
每个任务完成后检查:
|
||||
- [ ] TypeScript 编译无错误(针对负责的文件)
|
||||
- [ ] 功能测试通过
|
||||
- [ ] 没有引入新的错误
|
||||
- [ ] 代码格式正确
|
||||
- [ ] 没有遗留的 TODO 或临时代码
|
||||
|
||||
## 最终交付标准
|
||||
|
||||
1. **零 TypeScript 错误**
|
||||
2. **所有功能正常工作**
|
||||
3. **没有运行时警告**
|
||||
4. **代码可维护性未降低**
|
||||
5. **性能无明显影响**
|
||||
|
||||
## 备注
|
||||
|
||||
- 每个 worker 文档都是自包含的,可以独立执行
|
||||
- 如果某个任务比预期复杂,记录问题供后续优化
|
||||
- 保持 git commits 小而频繁,便于回滚
|
||||
239
.sub_task/step_0_foundation_serial.md
Normal file
239
.sub_task/step_0_foundation_serial.md
Normal file
@ -0,0 +1,239 @@
|
||||
# Step 0: Foundation Type System Fix (MUST COMPLETE FIRST - SERIAL)
|
||||
|
||||
## 项目背景
|
||||
本项目是 Kode CLI 工具,基于 TypeScript + React (Ink 6) 构建的命令行界面。最近升级到 React 19 和 Ink 6 后出现了 127 个 TypeScript 编译错误。本任务是修复基础类型系统,为后续并行修复打下基础。
|
||||
|
||||
## 任务目标
|
||||
修复核心类型定义,使得其他所有模块可以基于正确的类型定义进行修复。
|
||||
|
||||
## 系统架构概览
|
||||
```
|
||||
Kode CLI
|
||||
├── src/
|
||||
│ ├── messages.ts - 消息类型定义
|
||||
│ ├── Tool.ts - 工具基类接口
|
||||
│ ├── types/ - 类型定义目录
|
||||
│ ├── query.ts - 查询系统
|
||||
│ ├── utils/ - 工具函数
|
||||
│ │ └── messageContextManager.ts
|
||||
│ └── hooks/ - React hooks
|
||||
│ └── useTextInput.ts
|
||||
```
|
||||
|
||||
## 施工步骤
|
||||
|
||||
### Phase 1: 安装缺失依赖 (5分钟)
|
||||
|
||||
#### Step 1.1: 安装 sharp 图像处理库
|
||||
**文件**: package.json
|
||||
**执行命令**:
|
||||
```bash
|
||||
bun add sharp
|
||||
bun add -d @types/sharp
|
||||
```
|
||||
**验证**: 运行 `bun install` 确认无错误
|
||||
|
||||
### Phase 2: 创建类型增强文件 (10分钟)
|
||||
|
||||
#### Step 2.1: 创建 Ink 类型增强
|
||||
**创建文件**: `src/types/ink-augmentation.d.ts`
|
||||
**精确内容**:
|
||||
```typescript
|
||||
// Ink Key 类型增强 - 添加缺失的键盘属性
|
||||
declare module 'ink' {
|
||||
interface Key {
|
||||
fn?: boolean;
|
||||
home?: boolean;
|
||||
end?: boolean;
|
||||
space?: boolean;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 2.2: 创建通用类型定义
|
||||
**创建文件**: `src/types/common.d.ts`
|
||||
**精确内容**:
|
||||
```typescript
|
||||
// UUID 类型定义
|
||||
export type UUID = `${string}-${string}-${string}-${string}-${string}`;
|
||||
|
||||
// 扩展的工具上下文
|
||||
export interface ExtendedToolUseContext extends ToolUseContext {
|
||||
setToolJSX: (jsx: React.ReactElement) => void;
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 3: 修复消息类型系统 (20分钟)
|
||||
|
||||
#### Step 3.1: 修复 Message 类型定义
|
||||
**修改文件**: `src/messages.ts`
|
||||
**查找内容** (大约在 50-100 行之间):
|
||||
```typescript
|
||||
export interface ProgressMessage {
|
||||
type: 'progress'
|
||||
// ... existing properties
|
||||
}
|
||||
```
|
||||
**替换为**:
|
||||
```typescript
|
||||
export interface ProgressMessage {
|
||||
type: 'progress'
|
||||
message?: any // 添加可选的 message 属性以兼容旧代码
|
||||
content: AssistantMessage
|
||||
normalizedMessages: NormalizedMessage[]
|
||||
siblingToolUseIDs: Set<string>
|
||||
tools: Tool<any, any>[]
|
||||
toolUseID: string
|
||||
uuid: UUID
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 3.2: 修复 query.ts 中的消息访问
|
||||
**修改文件**: `src/query.ts`
|
||||
**查找内容** (第 203-210 行):
|
||||
```typescript
|
||||
message: msg.message
|
||||
```
|
||||
**替换为**:
|
||||
```typescript
|
||||
// 使用类型守卫安全访问
|
||||
...(msg.type !== 'progress' && 'message' in msg ? { message: msg.message } : {})
|
||||
```
|
||||
|
||||
#### Step 3.3: 修复 messageContextManager.ts
|
||||
**修改文件**: `src/utils/messageContextManager.ts`
|
||||
**查找内容** (第 136 行附近):
|
||||
```typescript
|
||||
return {
|
||||
type: "assistant",
|
||||
message: { role: "assistant", content: [...] }
|
||||
}
|
||||
```
|
||||
**替换为**:
|
||||
```typescript
|
||||
return {
|
||||
type: "assistant",
|
||||
message: { role: "assistant", content: [...] },
|
||||
costUSD: 0,
|
||||
durationMs: 0,
|
||||
uuid: crypto.randomUUID() as UUID
|
||||
}
|
||||
```
|
||||
**注意**: 需要在文件顶部添加导入:
|
||||
```typescript
|
||||
import type { UUID } from '../types/common';
|
||||
```
|
||||
|
||||
### Phase 4: 修复 Tool 接口定义 (15分钟)
|
||||
|
||||
#### Step 4.1: 更新 Tool 基础接口
|
||||
**修改文件**: `src/Tool.ts`
|
||||
**查找内容**:
|
||||
```typescript
|
||||
export interface Tool<TInput, TOutput> {
|
||||
renderResultForAssistant(output: TOutput): string;
|
||||
renderToolUseRejectedMessage(): React.ReactElement;
|
||||
// ...
|
||||
}
|
||||
```
|
||||
**替换为**:
|
||||
```typescript
|
||||
export interface Tool<TInput, TOutput> {
|
||||
renderResultForAssistant(output: TOutput): string | any[];
|
||||
renderToolUseRejectedMessage?(...args: any[]): React.ReactElement;
|
||||
// ...其他属性保持不变
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 4.2: 更新 ToolUseContext
|
||||
**修改文件**: `src/Tool.ts` (或者在同一文件中查找 ToolUseContext)
|
||||
**查找内容**:
|
||||
```typescript
|
||||
export interface ToolUseContext {
|
||||
// existing properties
|
||||
}
|
||||
```
|
||||
**替换为**:
|
||||
```typescript
|
||||
export interface ToolUseContext {
|
||||
// ...existing properties
|
||||
setToolJSX?: (jsx: React.ReactElement) => void;
|
||||
messageId?: string;
|
||||
agentId?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 5: 修复其他基础类型问题 (10分钟)
|
||||
|
||||
#### Step 5.1: 修复 thinking.ts 枚举值
|
||||
**修改文件**: `src/utils/thinking.ts`
|
||||
**查找内容** (第 115 行):
|
||||
```typescript
|
||||
"low" | "medium" | "high" | "minimal"
|
||||
```
|
||||
**替换为**:
|
||||
```typescript
|
||||
"low" | "medium" | "high"
|
||||
```
|
||||
|
||||
#### Step 5.2: 移除无用的 @ts-expect-error
|
||||
**修改文件列表及行号**:
|
||||
1. `src/entrypoints/cli.tsx` - 删除第 318 行
|
||||
2. `src/hooks/useDoublePress.ts` - 删除第 33 行
|
||||
3. `src/hooks/useTextInput.ts` - 删除第 143 行
|
||||
4. `src/utils/messages.tsx` - 删除第 301 行
|
||||
|
||||
**操作**: 直接删除包含 `// @ts-expect-error` 的整行
|
||||
|
||||
### Phase 6: 验证基础修复 (5分钟)
|
||||
|
||||
#### Step 6.1: 运行类型检查
|
||||
```bash
|
||||
npx tsc --noEmit 2>&1 | wc -l
|
||||
```
|
||||
**预期结果**: 错误数量应该从 127 减少到 70-80 左右
|
||||
|
||||
#### Step 6.2: 检查特定文件错误
|
||||
```bash
|
||||
npx tsc --noEmit 2>&1 | grep "src/messages.ts"
|
||||
npx tsc --noEmit 2>&1 | grep "src/query.ts"
|
||||
npx tsc --noEmit 2>&1 | grep "src/Tool.ts"
|
||||
```
|
||||
**预期结果**: 这些文件不应再有错误
|
||||
|
||||
#### Step 6.3: 测试运行时
|
||||
```bash
|
||||
bun run dev
|
||||
# 输入 /help 测试基础功能
|
||||
```
|
||||
**预期结果**: CLI 应该能启动,基础命令能运行
|
||||
|
||||
## 完成标志
|
||||
- [ ] sharp 依赖已安装
|
||||
- [ ] ink-augmentation.d.ts 已创建
|
||||
- [ ] Message 类型错误已修复
|
||||
- [ ] Tool 接口已更新
|
||||
- [ ] 无用的 @ts-expect-error 已移除
|
||||
- [ ] TypeScript 错误减少 40% 以上
|
||||
- [ ] 基础 CLI 功能可运行
|
||||
|
||||
## 注意事项
|
||||
1. **不要修改功能逻辑**,只修复类型定义
|
||||
2. **保留原有注释**,只添加必要的类型注释
|
||||
3. **使用 git diff 检查改动**,确保没有意外修改
|
||||
4. **每个文件修改后立即保存**,避免丢失工作
|
||||
|
||||
## 如果遇到问题
|
||||
1. 检查文件路径是否正确
|
||||
2. 确认 bun 和 npm 都已安装
|
||||
3. 如果找不到指定代码,使用 grep 搜索:
|
||||
```bash
|
||||
grep -n "ProgressMessage" src/messages.ts
|
||||
```
|
||||
4. 保存修改前的文件备份:
|
||||
```bash
|
||||
cp src/messages.ts src/messages.ts.backup
|
||||
```
|
||||
|
||||
## 完成后
|
||||
将此文档标记为完成,然后可以开始 Step 1 的并行任务。
|
||||
237
.sub_task/step_1_parallel_worker_0.md
Normal file
237
.sub_task/step_1_parallel_worker_0.md
Normal file
@ -0,0 +1,237 @@
|
||||
# Step 1 - Worker 0: ArchitectTool & FileReadTool 修复
|
||||
|
||||
## 前置条件
|
||||
**必须先完成 step_0_foundation_serial.md 的所有任务**
|
||||
|
||||
## 项目背景
|
||||
Kode CLI 的工具系统采用插件架构,每个工具都实现 Tool 接口。本任务负责修复 ArchitectTool 和 FileReadTool 的类型错误。
|
||||
|
||||
## 系统架构上下文
|
||||
```
|
||||
src/tools/
|
||||
├── ArchitectTool/
|
||||
│ ├── ArchitectTool.tsx - 架构分析工具
|
||||
│ └── prompt.ts - 工具提示词
|
||||
├── FileReadTool/
|
||||
│ ├── FileReadTool.tsx - 文件读取工具
|
||||
│ └── prompt.ts - 工具提示词
|
||||
```
|
||||
|
||||
## 任务目标
|
||||
1. 修复 ArchitectTool 的返回类型不匹配问题
|
||||
2. 修复 FileReadTool 的图像处理返回类型问题
|
||||
3. 确保两个工具都能正确编译和运行
|
||||
|
||||
## 详细施工步骤
|
||||
|
||||
### Phase 1: 修复 ArchitectTool (30分钟)
|
||||
|
||||
#### Step 1.1: 修复 renderResultForAssistant 返回类型
|
||||
**文件**: `src/tools/ArchitectTool/ArchitectTool.tsx`
|
||||
**定位方法**: 搜索 `renderResultForAssistant`
|
||||
**当前问题**: 返回 string | array,但接口期望只返回 string
|
||||
**查找代码** (大约在第 100-150 行):
|
||||
```typescript
|
||||
renderResultForAssistant(data: { type: "text"; file: {...} } | { type: "image"; file: {...} }) {
|
||||
if (data.type === "image") {
|
||||
return [{
|
||||
type: "image",
|
||||
source: {
|
||||
type: "base64",
|
||||
data: data.file.base64,
|
||||
media_type: data.file.type,
|
||||
},
|
||||
}];
|
||||
}
|
||||
return `File content...`;
|
||||
}
|
||||
```
|
||||
**修复方案**: 由于 Tool 接口已在 Step 0 中更新为允许 string | any[],这里不需要修改,只需确认导入正确。
|
||||
|
||||
#### Step 1.2: 修复 renderToolUseRejectedMessage 签名
|
||||
**文件**: `src/tools/ArchitectTool/ArchitectTool.tsx`
|
||||
**定位**: 搜索第二个工具定义(FileWriteTool 部分)
|
||||
**查找代码** (大约在第 200-250 行):
|
||||
```typescript
|
||||
renderToolUseRejectedMessage({ file_path, content }, { columns, verbose }) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
**替换为**:
|
||||
```typescript
|
||||
renderToolUseRejectedMessage({ file_path, content }: any = {}, { columns, verbose }: any = {}) {
|
||||
// 如果函数体使用了这些参数,保持不变
|
||||
// 如果没使用,可以简化为:
|
||||
// renderToolUseRejectedMessage() {
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 1.3: 修复 call 方法签名
|
||||
**文件**: `src/tools/ArchitectTool/ArchitectTool.tsx`
|
||||
**定位**: 搜索第三个工具定义(通常在文件末尾)
|
||||
**查找代码** (大约在第 60 行):
|
||||
```typescript
|
||||
call: async function* ({ prompt, context }, toolUseContext, canUseTool) {
|
||||
```
|
||||
**替换为**:
|
||||
```typescript
|
||||
call: async function* ({ prompt, context }: any, toolUseContext: any) {
|
||||
// 移除第三个参数 canUseTool,如果函数体内使用了它,需要调整逻辑
|
||||
```
|
||||
|
||||
#### Step 1.4: 修复第三个工具的 renderResultForAssistant
|
||||
**文件**: `src/tools/ArchitectTool/ArchitectTool.tsx`
|
||||
**查找代码** (大约在第 101 行):
|
||||
```typescript
|
||||
renderResultForAssistant: (data: TextBlock[]) => data,
|
||||
```
|
||||
**替换为**:
|
||||
```typescript
|
||||
renderResultForAssistant: (data: TextBlock[]) => JSON.stringify(data),
|
||||
```
|
||||
|
||||
### Phase 2: 修复 FileReadTool (30分钟)
|
||||
|
||||
#### Step 2.1: 修复图像返回类型
|
||||
**文件**: `src/tools/FileReadTool/FileReadTool.tsx`
|
||||
**定位**: 搜索 `renderResultForAssistant`
|
||||
**查找代码** (大约在第 255 行):
|
||||
```typescript
|
||||
renderResultForAssistant(data) {
|
||||
if (data.type === "image") {
|
||||
return [{
|
||||
type: "image",
|
||||
source: {
|
||||
type: "base64",
|
||||
data: data.file.base64,
|
||||
media_type: data.file.type,
|
||||
},
|
||||
}];
|
||||
}
|
||||
// ... text handling
|
||||
}
|
||||
```
|
||||
**修复**: 由于 Tool 接口已支持 string | any[],此处不需要修改
|
||||
|
||||
#### Step 2.2: 处理 sharp 模块导入
|
||||
**文件**: `src/tools/FileReadTool/FileReadTool.tsx`
|
||||
**定位**: 搜索 `import.*sharp` (大约在第 319 行)
|
||||
**查找代码**:
|
||||
```typescript
|
||||
import sharp from 'sharp';
|
||||
```
|
||||
**修复方案 1 - 动态导入** (推荐):
|
||||
```typescript
|
||||
// 删除顶部的 import sharp from 'sharp';
|
||||
// 在使用处改为动态导入
|
||||
const sharp = await import('sharp').catch(() => null);
|
||||
if (!sharp) {
|
||||
throw new Error('Sharp module not available');
|
||||
}
|
||||
```
|
||||
|
||||
**修复方案 2 - 条件导入**:
|
||||
```typescript
|
||||
let sharp: any;
|
||||
try {
|
||||
sharp = require('sharp');
|
||||
} catch {
|
||||
sharp = null;
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 2.3: 添加类型声明
|
||||
**文件**: `src/tools/FileReadTool/FileReadTool.tsx`
|
||||
**在文件顶部添加**:
|
||||
```typescript
|
||||
import type { ImageBlockParam } from '@anthropic-ai/sdk';
|
||||
// 如果 ImageBlockParam 不存在,使用:
|
||||
type ImageBlockParam = {
|
||||
Source: 'image/jpeg' | 'image/png' | 'image/gif' | 'image/webp';
|
||||
};
|
||||
```
|
||||
|
||||
### Phase 3: 验证修复 (10分钟)
|
||||
|
||||
#### Step 3.1: 单独检查 ArchitectTool 错误
|
||||
```bash
|
||||
npx tsc --noEmit 2>&1 | grep "ArchitectTool"
|
||||
```
|
||||
**预期结果**: 无输出或错误数量显著减少
|
||||
|
||||
#### Step 3.2: 单独检查 FileReadTool 错误
|
||||
```bash
|
||||
npx tsc --noEmit 2>&1 | grep "FileReadTool"
|
||||
```
|
||||
**预期结果**: 无输出或只有 sharp 相关警告
|
||||
|
||||
#### Step 3.3: 测试工具功能
|
||||
```bash
|
||||
# 启动 CLI
|
||||
bun run dev
|
||||
|
||||
# 测试文件读取
|
||||
# 输入: read package.json
|
||||
|
||||
# 测试架构分析(如果有此命令)
|
||||
# 输入: analyze src/Tool.ts
|
||||
```
|
||||
|
||||
### Phase 4: 处理遗留问题 (10分钟)
|
||||
|
||||
#### Step 4.1: 如果还有类型错误
|
||||
1. 检查是否正确导入了更新后的 Tool 接口:
|
||||
```typescript
|
||||
import { Tool } from '../../Tool';
|
||||
```
|
||||
|
||||
2. 确认 ToolUseContext 类型正确:
|
||||
```typescript
|
||||
import type { ToolUseContext } from '../../Tool';
|
||||
```
|
||||
|
||||
3. 对于复杂的类型错误,可以临时使用 any:
|
||||
```typescript
|
||||
// 临时解决方案,标记 TODO
|
||||
// TODO: 正确类型化此处
|
||||
const result = someComplexOperation() as any;
|
||||
```
|
||||
|
||||
## 完成标志
|
||||
- [ ] ArchitectTool 编译无错误
|
||||
- [ ] FileReadTool 编译无错误
|
||||
- [ ] sharp 导入问题已解决
|
||||
- [ ] 两个工具的基础功能可运行
|
||||
- [ ] TypeScript 错误减少至少 10 个
|
||||
|
||||
## 注意事项
|
||||
1. **保持功能不变** - 只修复类型,不改变业务逻辑
|
||||
2. **保留原有注释** - 不要删除现有的代码注释
|
||||
3. **测试每个修改** - 每次修改后运行 tsc 检查
|
||||
4. **使用版本控制** - 定期 git add 保存进度
|
||||
|
||||
## 常见问题解决
|
||||
|
||||
### Q: 找不到 Tool 接口定义?
|
||||
```bash
|
||||
find src -name "*.ts" -o -name "*.tsx" | xargs grep "export interface Tool"
|
||||
```
|
||||
|
||||
### Q: ImageBlockParam 类型不存在?
|
||||
创建本地类型定义:
|
||||
```typescript
|
||||
// 在文件顶部添加
|
||||
interface ImageBlockParam {
|
||||
Source: string;
|
||||
}
|
||||
```
|
||||
|
||||
### Q: sharp 模块一直报错?
|
||||
确认已安装:
|
||||
```bash
|
||||
bun add sharp
|
||||
bun add -d @types/sharp
|
||||
```
|
||||
|
||||
## 完成后
|
||||
标记此任务完成,可以继续其他 worker 的并行任务。
|
||||
286
.sub_task/step_1_parallel_worker_1.md
Normal file
286
.sub_task/step_1_parallel_worker_1.md
Normal file
@ -0,0 +1,286 @@
|
||||
# Step 1 - Worker 1: FileWriteTool & FileEditTool 修复
|
||||
|
||||
## 前置条件
|
||||
**必须先完成 step_0_foundation_serial.md 的所有任务**
|
||||
|
||||
## 项目背景
|
||||
这两个工具负责文件的写入和编辑操作,是 Kode CLI 的核心功能。它们都有相似的权限请求界面和错误处理逻辑。
|
||||
|
||||
## 系统架构上下文
|
||||
```
|
||||
src/tools/
|
||||
├── FileWriteTool/
|
||||
│ ├── FileWriteTool.tsx - 文件写入工具
|
||||
│ └── prompt.ts - 工具提示词
|
||||
├── FileEditTool/
|
||||
│ ├── FileEditTool.tsx - 文件编辑工具
|
||||
│ ├── prompt.ts - 工具提示词
|
||||
│ └── utils.ts - 工具函数
|
||||
```
|
||||
|
||||
## 任务目标
|
||||
1. 修复两个工具的 renderToolUseRejectedMessage 签名问题
|
||||
2. 修复 renderToolResultMessage 签名问题
|
||||
3. 确保文件操作权限流程正常
|
||||
|
||||
## 详细施工步骤
|
||||
|
||||
### Phase 1: 修复 FileWriteTool (25分钟)
|
||||
|
||||
#### Step 1.1: 修复 renderToolUseRejectedMessage 签名
|
||||
**文件**: `src/tools/FileWriteTool/FileWriteTool.tsx`
|
||||
**定位**: 搜索 `renderToolUseRejectedMessage` (大约第 70 行)
|
||||
**当前代码**:
|
||||
```typescript
|
||||
renderToolUseRejectedMessage({ file_path, content }, { columns, verbose }) {
|
||||
return <FileWritePermissionRejected ... />;
|
||||
}
|
||||
```
|
||||
**修复为**:
|
||||
```typescript
|
||||
renderToolUseRejectedMessage(input?: any, options?: any) {
|
||||
// 如果函数体需要这些参数
|
||||
const { file_path, content } = input || {};
|
||||
const { columns, verbose } = options || {};
|
||||
return <FileWritePermissionRejected ... />;
|
||||
}
|
||||
```
|
||||
**或者如果接口允许可选**:
|
||||
```typescript
|
||||
renderToolUseRejectedMessage() {
|
||||
// 简化版本,如果不需要参数
|
||||
return <FileWritePermissionRejected />;
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 1.2: 修复 renderToolResultMessage 签名
|
||||
**文件**: `src/tools/FileWriteTool/FileWriteTool.tsx`
|
||||
**定位**: 搜索 `renderToolResultMessage` (大约第 122 行)
|
||||
**当前代码**:
|
||||
```typescript
|
||||
renderToolResultMessage({ filePath, content, structuredPatch, type }, { verbose }) {
|
||||
return <FileWriteResultMessage ... />;
|
||||
}
|
||||
```
|
||||
**修复为**:
|
||||
```typescript
|
||||
renderToolResultMessage(output: any) {
|
||||
const { filePath, content, structuredPatch, type } = output;
|
||||
// 注意:第二个参数 verbose 可能需要从其他地方获取
|
||||
return <FileWriteResultMessage ... />;
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 1.3: 导入必要的类型
|
||||
**文件**: `src/tools/FileWriteTool/FileWriteTool.tsx`
|
||||
**在文件顶部添加**:
|
||||
```typescript
|
||||
import type { Tool, ToolUseContext } from '../../Tool';
|
||||
import type { Hunk } from 'diff';
|
||||
```
|
||||
|
||||
#### Step 1.4: 修复组件导入
|
||||
**检查导入部分**:
|
||||
```typescript
|
||||
// 确保这些组件存在并正确导入
|
||||
import { FileWritePermissionRejected } from '../../components/permissions/FileWritePermissionRequest';
|
||||
import { FileWriteResultMessage } from '../../components/messages/FileWriteResultMessage';
|
||||
```
|
||||
|
||||
### Phase 2: 修复 FileEditTool (25分钟)
|
||||
|
||||
#### Step 2.1: 修复 renderToolUseRejectedMessage 签名
|
||||
**文件**: `src/tools/FileEditTool/FileEditTool.tsx`
|
||||
**定位**: 搜索 `renderToolUseRejectedMessage` (大约第 78 行)
|
||||
**当前代码**:
|
||||
```typescript
|
||||
renderToolUseRejectedMessage({ file_path, old_string, new_string }, { columns, verbose }) {
|
||||
return <FileEditPermissionRejected ... />;
|
||||
}
|
||||
```
|
||||
**修复为**:
|
||||
```typescript
|
||||
renderToolUseRejectedMessage(input?: any, options?: any) {
|
||||
const { file_path, old_string, new_string } = input || {};
|
||||
const { columns, verbose } = options || {};
|
||||
return <FileEditPermissionRejected ... />;
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 2.2: 检查 validateInput 方法
|
||||
**文件**: `src/tools/FileEditTool/FileEditTool.tsx`
|
||||
**定位**: 搜索 `validateInput`
|
||||
**确保签名正确**:
|
||||
```typescript
|
||||
async validateInput(
|
||||
{ file_path, old_string, new_string }: any,
|
||||
{ readFileTimestamps }: ToolUseContext
|
||||
): Promise<{ result: boolean; message?: string }> {
|
||||
// 实现...
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 2.3: 检查 call 方法
|
||||
**文件**: `src/tools/FileEditTool/FileEditTool.tsx`
|
||||
**定位**: 搜索 `call:`
|
||||
**确保异步生成器签名正确**:
|
||||
```typescript
|
||||
async *call(
|
||||
{ file_path, old_string, new_string }: any,
|
||||
context: ToolUseContext
|
||||
): AsyncGenerator<{ type: "result"; data: any; resultForAssistant: string }> {
|
||||
// 实现...
|
||||
yield {
|
||||
type: "result",
|
||||
data: result,
|
||||
resultForAssistant: `File ${file_path} updated successfully`
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 2.4: 修复工具导出
|
||||
**文件**: `src/tools/FileEditTool/FileEditTool.tsx`
|
||||
**在文件末尾确认**:
|
||||
```typescript
|
||||
export const FileEditTool: Tool<any, any> = {
|
||||
name: "file_edit",
|
||||
description: async () => "Edit files",
|
||||
// ... 所有必需的方法
|
||||
};
|
||||
```
|
||||
|
||||
### Phase 3: 修复共享组件和工具函数 (15分钟)
|
||||
|
||||
#### Step 3.1: 检查权限组件
|
||||
**文件**: `src/components/permissions/FileWritePermissionRequest/index.tsx`
|
||||
**确保导出了**:
|
||||
```typescript
|
||||
export { FileWritePermissionRejected } from './FileWritePermissionRejected';
|
||||
```
|
||||
|
||||
**文件**: `src/components/permissions/FileEditPermissionRequest/index.tsx`
|
||||
**确保导出了**:
|
||||
```typescript
|
||||
export { FileEditPermissionRejected } from './FileEditPermissionRejected';
|
||||
```
|
||||
|
||||
#### Step 3.2: 修复 utils.ts (如果存在)
|
||||
**文件**: `src/tools/FileEditTool/utils.ts`
|
||||
**检查函数签名**:
|
||||
```typescript
|
||||
export function applyEdit(
|
||||
content: string,
|
||||
oldString: string,
|
||||
newString: string,
|
||||
replaceAll: boolean = false
|
||||
): { updatedContent: string; occurrences: number } {
|
||||
// 确保返回值包含所需属性
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 4: 验证修复 (10分钟)
|
||||
|
||||
#### Step 4.1: 检查 FileWriteTool 错误
|
||||
```bash
|
||||
npx tsc --noEmit 2>&1 | grep "FileWriteTool"
|
||||
```
|
||||
**预期**: 无错误或错误显著减少
|
||||
|
||||
#### Step 4.2: 检查 FileEditTool 错误
|
||||
```bash
|
||||
npx tsc --noEmit 2>&1 | grep "FileEditTool"
|
||||
```
|
||||
**预期**: 无错误或错误显著减少
|
||||
|
||||
#### Step 4.3: 功能测试
|
||||
```bash
|
||||
# 启动 CLI
|
||||
bun run dev
|
||||
|
||||
# 测试文件写入(创建测试文件)
|
||||
# 输入: write test.txt "Hello World"
|
||||
|
||||
# 测试文件编辑(如果上面创建成功)
|
||||
# 输入: edit test.txt "Hello" "Hi"
|
||||
|
||||
# 清理测试文件
|
||||
rm test.txt
|
||||
```
|
||||
|
||||
### Phase 5: 处理边缘情况 (10分钟)
|
||||
|
||||
#### Step 5.1: 如果组件不存在
|
||||
创建临时占位组件:
|
||||
```typescript
|
||||
// 临时解决方案
|
||||
const FileWritePermissionRejected = () => <Text>Permission rejected</Text>;
|
||||
const FileEditPermissionRejected = () => <Text>Permission rejected</Text>;
|
||||
```
|
||||
|
||||
#### Step 5.2: 如果类型仍然不匹配
|
||||
使用类型断言:
|
||||
```typescript
|
||||
const tool = {
|
||||
// ... tool implementation
|
||||
} as Tool<any, any>;
|
||||
|
||||
export const FileWriteTool = tool;
|
||||
```
|
||||
|
||||
## 完成标志
|
||||
- [ ] FileWriteTool 编译无错误
|
||||
- [ ] FileEditTool 编译无错误
|
||||
- [ ] 权限拒绝消息正确显示
|
||||
- [ ] 文件写入功能正常
|
||||
- [ ] 文件编辑功能正常
|
||||
- [ ] TypeScript 错误减少至少 8 个
|
||||
|
||||
## 注意事项
|
||||
1. **保持权限检查逻辑** - 不要跳过权限验证
|
||||
2. **保留错误处理** - 确保所有错误情况都有处理
|
||||
3. **测试文件操作** - 使用临时文件测试,避免修改重要文件
|
||||
4. **备份修改** - 定期 git add 保存进度
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 找不到权限组件?
|
||||
```bash
|
||||
find src -name "*Permission*" -type f | grep -E "(Write|Edit)"
|
||||
```
|
||||
|
||||
### Q: Hunk 类型不存在?
|
||||
```typescript
|
||||
// 添加类型定义
|
||||
type Hunk = {
|
||||
oldStart: number;
|
||||
oldLines: number;
|
||||
newStart: number;
|
||||
newLines: number;
|
||||
lines: string[];
|
||||
};
|
||||
```
|
||||
|
||||
### Q: 组件导入路径错误?
|
||||
检查实际路径:
|
||||
```bash
|
||||
ls -la src/components/permissions/
|
||||
```
|
||||
|
||||
## 调试技巧
|
||||
1. 使用 `console.log` 临时调试:
|
||||
```typescript
|
||||
console.log('FileWriteTool input:', input);
|
||||
```
|
||||
|
||||
2. 检查运行时类型:
|
||||
```typescript
|
||||
console.log('Type of input:', typeof input);
|
||||
```
|
||||
|
||||
3. 使用 TypeScript 编译器获取详细错误:
|
||||
```bash
|
||||
npx tsc --noEmit --pretty 2>&1 | less
|
||||
```
|
||||
|
||||
## 完成后
|
||||
标记此任务完成,继续其他并行任务。记录任何未解决的问题供后续处理。
|
||||
355
.sub_task/step_1_parallel_worker_2.md
Normal file
355
.sub_task/step_1_parallel_worker_2.md
Normal file
@ -0,0 +1,355 @@
|
||||
# Step 1 - Worker 2: TaskTool & MultiEditTool 修复
|
||||
|
||||
## 前置条件
|
||||
**必须先完成 step_0_foundation_serial.md 的所有任务**
|
||||
|
||||
## 项目背景
|
||||
TaskTool 是 Kode CLI 的核心调度工具,负责创建子任务和代理。MultiEditTool 允许批量编辑文件。这两个工具都涉及复杂的异步操作。
|
||||
|
||||
## 系统架构上下文
|
||||
```
|
||||
src/tools/
|
||||
├── TaskTool/
|
||||
│ ├── TaskTool.tsx - 任务调度工具
|
||||
│ └── prompt.ts - 工具提示词
|
||||
├── MultiEditTool/
|
||||
│ ├── MultiEditTool.tsx - 批量编辑工具
|
||||
│ └── prompt.ts - 工具提示词
|
||||
```
|
||||
|
||||
## 任务目标
|
||||
1. 修复 TaskTool 的 AsyncGenerator 类型不匹配
|
||||
2. 修复 ExtendedToolUseContext 缺失属性
|
||||
3. 修复 MultiEditTool 的参数和返回值问题
|
||||
|
||||
## 详细施工步骤
|
||||
|
||||
### Phase 1: 修复 TaskTool (40分钟)
|
||||
|
||||
#### Step 1.1: 修复 AsyncGenerator 返回类型
|
||||
**文件**: `src/tools/TaskTool/TaskTool.tsx`
|
||||
**定位**: 搜索 `call:` 方法 (大约第 68 行)
|
||||
**当前问题**: AsyncGenerator 返回类型包含 progress 和 result,但接口只期望 result
|
||||
|
||||
**当前代码结构**:
|
||||
```typescript
|
||||
async *call(
|
||||
{ description, prompt, model_name, subagent_type },
|
||||
context
|
||||
): AsyncGenerator<
|
||||
| { type: "result"; data: { error: string }; resultForAssistant: string }
|
||||
| { type: "progress"; content: any; normalizedMessages: any[]; tools: any[] }
|
||||
| { type: "result"; data: any; normalizedMessages: any[]; resultForAssistant: any }
|
||||
> {
|
||||
// 实现
|
||||
}
|
||||
```
|
||||
|
||||
**修复方案 1 - 简化返回类型**:
|
||||
```typescript
|
||||
async *call(
|
||||
{ description, prompt, model_name, subagent_type }: any,
|
||||
context: ToolUseContext
|
||||
): AsyncGenerator<{ type: "result"; data: any; resultForAssistant?: string }> {
|
||||
// 修改实现,只 yield result 类型
|
||||
// 将 progress 类型改为内部处理或日志
|
||||
|
||||
try {
|
||||
// ... 执行逻辑
|
||||
|
||||
// 不再 yield progress,改为:
|
||||
// console.log('Progress:', progressData);
|
||||
|
||||
// 只 yield result
|
||||
yield {
|
||||
type: "result",
|
||||
data: resultData,
|
||||
resultForAssistant: "Task completed"
|
||||
};
|
||||
} catch (error) {
|
||||
yield {
|
||||
type: "result",
|
||||
data: { error: error.message },
|
||||
resultForAssistant: `Error: ${error.message}`
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**修复方案 2 - 使用类型断言**:
|
||||
```typescript
|
||||
async *call(
|
||||
input: any,
|
||||
context: ToolUseContext
|
||||
) {
|
||||
// 保持原有逻辑,但在导出时断言
|
||||
} as Tool<any, any>['call']
|
||||
```
|
||||
|
||||
#### Step 1.2: 修复 ExtendedToolUseContext 问题
|
||||
**文件**: `src/tools/TaskTool/TaskTool.tsx`
|
||||
**定位**: 搜索使用 ExtendedToolUseContext 的地方 (大约第 191 行)
|
||||
**错误**: 缺少 setToolJSX 属性
|
||||
|
||||
**查找代码**:
|
||||
```typescript
|
||||
const extendedContext: ExtendedToolUseContext = {
|
||||
abortController: ...,
|
||||
options: ...,
|
||||
messageId: ...,
|
||||
agentId: ...,
|
||||
readFileTimestamps: ...
|
||||
// 缺少 setToolJSX
|
||||
};
|
||||
```
|
||||
|
||||
**修复为**:
|
||||
```typescript
|
||||
const extendedContext: ExtendedToolUseContext = {
|
||||
abortController: context.abortController,
|
||||
options: {
|
||||
...context.options,
|
||||
// 确保包含所需属性
|
||||
},
|
||||
messageId: context.messageId || '',
|
||||
agentId: context.agentId || '',
|
||||
readFileTimestamps: context.readFileTimestamps || {},
|
||||
setToolJSX: context.setToolJSX || (() => {}) // 添加默认实现
|
||||
};
|
||||
```
|
||||
|
||||
#### Step 1.3: 导入 ExtendedToolUseContext 类型
|
||||
**文件**: `src/tools/TaskTool/TaskTool.tsx`
|
||||
**在文件顶部添加**:
|
||||
```typescript
|
||||
import type { Tool, ToolUseContext } from '../../Tool';
|
||||
import type { ExtendedToolUseContext } from '../../types/common';
|
||||
```
|
||||
|
||||
#### Step 1.4: 修复 TextBlock 类型
|
||||
**文件**: `src/tools/TaskTool/TaskTool.tsx`
|
||||
**添加类型定义**:
|
||||
```typescript
|
||||
// 在文件顶部
|
||||
type TextBlock = {
|
||||
type: 'text';
|
||||
text: string;
|
||||
};
|
||||
```
|
||||
|
||||
### Phase 2: 修复 MultiEditTool (30分钟)
|
||||
|
||||
#### Step 2.1: 修复 applyEdit 调用参数
|
||||
**文件**: `src/tools/MultiEditTool/MultiEditTool.tsx`
|
||||
**定位**: 搜索 `applyEdit` 调用 (大约第 281 行)
|
||||
**错误**: 期望 3 个参数,但传了 4 个
|
||||
|
||||
**查找代码**:
|
||||
```typescript
|
||||
const result = applyEdit(currentContent, old_string, new_string, replace_all);
|
||||
```
|
||||
|
||||
**修复方案 1 - 检查 applyEdit 函数签名**:
|
||||
```typescript
|
||||
// 查找 applyEdit 的定义
|
||||
// 如果它确实只接受 3 个参数,修改调用:
|
||||
const result = applyEdit(currentContent, old_string, new_string);
|
||||
// 单独处理 replace_all 逻辑
|
||||
```
|
||||
|
||||
**修复方案 2 - 更新 applyEdit 函数**:
|
||||
```typescript
|
||||
// 如果 applyEdit 应该接受 4 个参数,更新其定义:
|
||||
function applyEdit(
|
||||
content: string,
|
||||
oldString: string,
|
||||
newString: string,
|
||||
replaceAll: boolean = false
|
||||
) {
|
||||
// 实现
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 2.2: 修复返回值属性
|
||||
**文件**: `src/tools/MultiEditTool/MultiEditTool.tsx`
|
||||
**定位**: 使用 result.newContent 和 result.occurrences 的地方 (第 283, 289 行)
|
||||
**错误**: 属性不存在
|
||||
|
||||
**查找代码**:
|
||||
```typescript
|
||||
currentContent = result.newContent;
|
||||
// 和
|
||||
if (result.occurrences === 0) {
|
||||
```
|
||||
|
||||
**修复为**:
|
||||
```typescript
|
||||
// 确保 applyEdit 返回正确的结构
|
||||
interface EditResult {
|
||||
updatedFile: string; // 或 newContent
|
||||
patch: any[];
|
||||
occurrences?: number; // 添加此属性
|
||||
}
|
||||
|
||||
// 使用时:
|
||||
currentContent = result.updatedFile || result.newContent;
|
||||
if ((result.occurrences || 0) === 0) {
|
||||
```
|
||||
|
||||
#### Step 2.3: 修复函数参数数量
|
||||
**文件**: `src/tools/MultiEditTool/MultiEditTool.tsx`
|
||||
**定位**: 第 306 行的函数调用
|
||||
**错误**: 期望 4 个参数,提供了 3 个
|
||||
|
||||
**可能的修复**:
|
||||
```typescript
|
||||
// 查找函数定义,添加缺失的参数
|
||||
// 或者提供默认值
|
||||
someFunction(arg1, arg2, arg3, undefined);
|
||||
```
|
||||
|
||||
### Phase 3: 创建或修复辅助类型 (15分钟)
|
||||
|
||||
#### Step 3.1: 创建 types 文件(如果不存在)
|
||||
**创建文件**: `src/tools/types.ts`
|
||||
```typescript
|
||||
// 工具系统的共享类型定义
|
||||
|
||||
export interface EditResult {
|
||||
updatedFile: string;
|
||||
newContent?: string;
|
||||
patch: any[];
|
||||
occurrences: number;
|
||||
}
|
||||
|
||||
export interface TaskProgress {
|
||||
type: 'progress';
|
||||
content: any;
|
||||
normalizedMessages: any[];
|
||||
tools: any[];
|
||||
}
|
||||
|
||||
export interface TaskResult {
|
||||
type: 'result';
|
||||
data: any;
|
||||
resultForAssistant?: string;
|
||||
}
|
||||
|
||||
export type TaskOutput = TaskProgress | TaskResult;
|
||||
```
|
||||
|
||||
#### Step 3.2: 更新工具导入
|
||||
**在两个工具文件中添加**:
|
||||
```typescript
|
||||
import type { EditResult, TaskResult } from '../types';
|
||||
```
|
||||
|
||||
### Phase 4: 验证和测试 (15分钟)
|
||||
|
||||
#### Step 4.1: 检查 TaskTool 错误
|
||||
```bash
|
||||
npx tsc --noEmit 2>&1 | grep "TaskTool"
|
||||
```
|
||||
|
||||
#### Step 4.2: 检查 MultiEditTool 错误
|
||||
```bash
|
||||
npx tsc --noEmit 2>&1 | grep "MultiEditTool"
|
||||
```
|
||||
|
||||
#### Step 4.3: 功能测试
|
||||
```bash
|
||||
# 启动 CLI
|
||||
bun run dev
|
||||
|
||||
# 测试任务创建(如果有相关命令)
|
||||
# 输入: task "Create a simple function"
|
||||
|
||||
# 测试批量编辑(创建测试文件)
|
||||
echo "old text\nold text\nold text" > test.txt
|
||||
# 输入: multiedit test.txt "old" "new"
|
||||
rm test.txt
|
||||
```
|
||||
|
||||
### Phase 5: 处理复杂情况 (10分钟)
|
||||
|
||||
#### Step 5.1: 如果 AsyncGenerator 太复杂
|
||||
使用包装函数:
|
||||
```typescript
|
||||
const taskToolCall = async function* (input: any, context: any) {
|
||||
// 原始复杂逻辑
|
||||
} as any;
|
||||
|
||||
export const TaskTool: Tool<any, any> = {
|
||||
name: "task",
|
||||
call: taskToolCall,
|
||||
// ... 其他属性
|
||||
};
|
||||
```
|
||||
|
||||
#### Step 5.2: 如果类型冲突无法解决
|
||||
创建适配器:
|
||||
```typescript
|
||||
class TaskToolAdapter {
|
||||
static adaptOutput(output: any): { type: "result"; data: any } {
|
||||
if (output.type === "progress") {
|
||||
// 转换 progress 为某种 result 格式
|
||||
return {
|
||||
type: "result",
|
||||
data: { progress: output }
|
||||
};
|
||||
}
|
||||
return output;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 完成标志
|
||||
- [ ] TaskTool AsyncGenerator 类型匹配
|
||||
- [ ] ExtendedToolUseContext 完整
|
||||
- [ ] MultiEditTool 参数正确
|
||||
- [ ] 返回值属性存在
|
||||
- [ ] 两个工具都能编译
|
||||
- [ ] TypeScript 错误减少至少 15 个
|
||||
|
||||
## 注意事项
|
||||
1. **保持异步逻辑** - 不要改变 async/await 模式
|
||||
2. **保留错误处理** - 确保 try/catch 完整
|
||||
3. **测试并发场景** - TaskTool 可能同时运行多个任务
|
||||
4. **注意内存泄漏** - AsyncGenerator 需要正确清理
|
||||
|
||||
## 调试建议
|
||||
|
||||
### 追踪 AsyncGenerator 问题
|
||||
```typescript
|
||||
async *debugGenerator() {
|
||||
console.log('Generator started');
|
||||
try {
|
||||
yield { type: "result", data: "test" };
|
||||
console.log('Yielded result');
|
||||
} finally {
|
||||
console.log('Generator cleanup');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 类型检查技巧
|
||||
```typescript
|
||||
// 获取函数的返回类型
|
||||
type ReturnTypeOf<T> = T extends (...args: any[]) => infer R ? R : never;
|
||||
type CallReturnType = ReturnTypeOf<typeof TaskTool.call>;
|
||||
```
|
||||
|
||||
## 常见错误解决
|
||||
|
||||
### AsyncGenerator 类型不兼容
|
||||
1. 检查所有 yield 语句
|
||||
2. 确保都返回相同的类型结构
|
||||
3. 使用联合类型时要一致
|
||||
|
||||
### 属性不存在
|
||||
1. 检查对象的实际结构
|
||||
2. 添加可选链操作符 `?.`
|
||||
3. 使用类型守卫
|
||||
|
||||
## 完成后
|
||||
记录任何未解决的复杂问题,特别是关于 AsyncGenerator 的类型问题,供高级开发者后续优化。
|
||||
347
.sub_task/step_1_parallel_worker_3.md
Normal file
347
.sub_task/step_1_parallel_worker_3.md
Normal file
@ -0,0 +1,347 @@
|
||||
# Step 1 - Worker 3: Other Tools 修复 (StickerRequestTool, NotebookReadTool, AskExpertModelTool)
|
||||
|
||||
## 前置条件
|
||||
**必须先完成 step_0_foundation_serial.md 的所有任务**
|
||||
|
||||
## 项目背景
|
||||
这组工具包含特殊功能:StickerRequestTool 处理贴纸请求,NotebookReadTool 读取 Jupyter 笔记本,AskExpertModelTool 调用专家模型。
|
||||
|
||||
## 系统架构上下文
|
||||
```
|
||||
src/tools/
|
||||
├── StickerRequestTool/
|
||||
│ └── StickerRequestTool.tsx
|
||||
├── NotebookReadTool/
|
||||
│ └── NotebookReadTool.tsx
|
||||
├── AskExpertModelTool/
|
||||
│ └── AskExpertModelTool.tsx
|
||||
```
|
||||
|
||||
## 任务目标
|
||||
1. 修复 StickerRequestTool 的 setToolJSX 属性问题
|
||||
2. 修复 NotebookReadTool 的类型转换问题
|
||||
3. 修复 AskExpertModelTool 的 debugLogger 调用问题
|
||||
|
||||
## 详细施工步骤
|
||||
|
||||
### Phase 1: 修复 StickerRequestTool (20分钟)
|
||||
|
||||
#### Step 1.1: 处理 setToolJSX 缺失问题
|
||||
**文件**: `src/tools/StickerRequestTool/StickerRequestTool.tsx`
|
||||
**定位**: 搜索 `setToolJSX` (第 41, 51, 57 行)
|
||||
**问题**: ToolUseContext 不包含 setToolJSX
|
||||
|
||||
**查找代码**:
|
||||
```typescript
|
||||
context.setToolJSX(<StickerUI .../>);
|
||||
```
|
||||
|
||||
**修复方案 1 - 条件调用**:
|
||||
```typescript
|
||||
// 检查属性是否存在
|
||||
if (context.setToolJSX) {
|
||||
context.setToolJSX(<StickerUI .../>);
|
||||
} else {
|
||||
// 备用方案:记录日志或使用其他方式
|
||||
console.log('Sticker UI would be displayed here');
|
||||
}
|
||||
```
|
||||
|
||||
**修复方案 2 - 类型守卫**:
|
||||
```typescript
|
||||
// 在文件顶部添加类型守卫
|
||||
function hasSetToolJSX(ctx: any): ctx is ExtendedToolUseContext {
|
||||
return 'setToolJSX' in ctx && typeof ctx.setToolJSX === 'function';
|
||||
}
|
||||
|
||||
// 使用时
|
||||
if (hasSetToolJSX(context)) {
|
||||
context.setToolJSX(<StickerUI .../>);
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 1.2: 修复 renderToolUseRejectedMessage
|
||||
**文件**: `src/tools/StickerRequestTool/StickerRequestTool.tsx`
|
||||
**定位**: 第 85 行
|
||||
**问题**: 签名不匹配
|
||||
|
||||
**查找代码**:
|
||||
```typescript
|
||||
renderToolUseRejectedMessage(_input: any) {
|
||||
return <Text>...</Text>;
|
||||
}
|
||||
```
|
||||
|
||||
**修复为**:
|
||||
```typescript
|
||||
renderToolUseRejectedMessage() {
|
||||
// 移除参数或设为可选
|
||||
return <Text color="red">Sticker request rejected</Text>;
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 1.3: 确保组件导入正确
|
||||
**文件**: `src/tools/StickerRequestTool/StickerRequestTool.tsx`
|
||||
**检查导入**:
|
||||
```typescript
|
||||
import React from 'react';
|
||||
import { Text, Box } from 'ink';
|
||||
import type { Tool, ToolUseContext } from '../../Tool';
|
||||
```
|
||||
|
||||
### Phase 2: 修复 NotebookReadTool (20分钟)
|
||||
|
||||
#### Step 2.1: 修复类型转换问题
|
||||
**文件**: `src/tools/NotebookReadTool/NotebookReadTool.tsx`
|
||||
**定位**: 第 179 行
|
||||
**问题**: unknown 不能赋值给 string | string[]
|
||||
|
||||
**查找代码**:
|
||||
```typescript
|
||||
someFunction(unknownValue);
|
||||
```
|
||||
|
||||
**修复方案 1 - 类型断言**:
|
||||
```typescript
|
||||
someFunction(unknownValue as string);
|
||||
// 或者如果可能是数组
|
||||
someFunction(unknownValue as string | string[]);
|
||||
```
|
||||
|
||||
**修复方案 2 - 类型检查**:
|
||||
```typescript
|
||||
// 安全的类型检查
|
||||
if (typeof unknownValue === 'string') {
|
||||
someFunction(unknownValue);
|
||||
} else if (Array.isArray(unknownValue)) {
|
||||
someFunction(unknownValue);
|
||||
} else {
|
||||
// 处理其他情况
|
||||
someFunction(String(unknownValue));
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 2.2: 处理 Jupyter 笔记本类型
|
||||
**文件**: `src/tools/NotebookReadTool/NotebookReadTool.tsx`
|
||||
**添加类型定义**:
|
||||
```typescript
|
||||
// 在文件顶部
|
||||
interface NotebookCell {
|
||||
cell_type: 'code' | 'markdown';
|
||||
source: string | string[];
|
||||
outputs?: any[];
|
||||
}
|
||||
|
||||
interface NotebookData {
|
||||
cells: NotebookCell[];
|
||||
metadata?: any;
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 2.3: 修复解析逻辑
|
||||
**确保正确处理 source 字段**:
|
||||
```typescript
|
||||
function parseSource(source: unknown): string {
|
||||
if (typeof source === 'string') {
|
||||
return source;
|
||||
}
|
||||
if (Array.isArray(source)) {
|
||||
return source.join('');
|
||||
}
|
||||
return String(source);
|
||||
}
|
||||
|
||||
// 使用
|
||||
const content = parseSource(cell.source);
|
||||
```
|
||||
|
||||
### Phase 3: 修复 AskExpertModelTool (25分钟)
|
||||
|
||||
#### Step 3.1: 修复 debugLogger 调用
|
||||
**文件**: `src/tools/AskExpertModelTool/AskExpertModelTool.tsx`
|
||||
**定位**: 第 149, 172, 306, 319, 327, 344, 358, 417, 499, 508, 533 行
|
||||
**问题**: debugLogger 不是函数或参数数量错误
|
||||
|
||||
**查找 debugLogger 的使用**:
|
||||
```typescript
|
||||
debugLogger('phase', 'data', 'requestId');
|
||||
```
|
||||
|
||||
**修复方案 1 - 检查导入**:
|
||||
```typescript
|
||||
// 确保正确导入
|
||||
import { debugLogger } from '../../utils/debugLogger';
|
||||
|
||||
// 如果 debugLogger 是对象,使用正确的方法
|
||||
debugLogger.log('phase', 'data');
|
||||
// 或
|
||||
debugLogger.info('phase', { data, requestId });
|
||||
```
|
||||
|
||||
**修复方案 2 - 创建包装函数**:
|
||||
```typescript
|
||||
// 如果 debugLogger 结构复杂
|
||||
const log = (phase: string, data: any, requestId?: string) => {
|
||||
if (typeof debugLogger === 'function') {
|
||||
debugLogger(phase, data, requestId);
|
||||
} else if (debugLogger && debugLogger.log) {
|
||||
debugLogger.log(phase, { data, requestId });
|
||||
} else {
|
||||
console.log(`[${phase}]`, data, requestId);
|
||||
}
|
||||
};
|
||||
|
||||
// 替换所有 debugLogger 调用为 log
|
||||
log('api-call', responseData, requestId);
|
||||
```
|
||||
|
||||
#### Step 3.2: 修复每个 debugLogger 调用
|
||||
**系统性替换所有出错的行**:
|
||||
|
||||
1. **第 149 行**:
|
||||
```typescript
|
||||
// 原始:debugLogger(...)
|
||||
// 修改为:
|
||||
log('expert-model-start', { input }, requestId);
|
||||
```
|
||||
|
||||
2. **第 172 行** (2 参数变 1 参数):
|
||||
```typescript
|
||||
// 原始:debugLogger.something(arg1, arg2)
|
||||
// 修改为:
|
||||
debugLogger.api?.('phase', data) || console.log('API:', data);
|
||||
```
|
||||
|
||||
3. **继续修复其他行**,使用相同模式
|
||||
|
||||
#### Step 3.3: 处理模型调用逻辑
|
||||
**确保异步调用正确**:
|
||||
```typescript
|
||||
async function callExpertModel(prompt: string, model: string) {
|
||||
try {
|
||||
log('model-call-start', { prompt, model });
|
||||
|
||||
const response = await modelService.complete({
|
||||
prompt,
|
||||
model,
|
||||
});
|
||||
|
||||
log('model-call-success', response);
|
||||
return response;
|
||||
} catch (error) {
|
||||
log('model-call-error', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 4: 通用修复和优化 (10分钟)
|
||||
|
||||
#### Step 4.1: 添加通用类型定义
|
||||
**创建文件**: `src/tools/utils/types.ts`
|
||||
```typescript
|
||||
// 工具系统的辅助类型
|
||||
export type DebugLogger = {
|
||||
log: (phase: string, data: any) => void;
|
||||
info: (phase: string, data: any) => void;
|
||||
warn: (phase: string, data: any) => void;
|
||||
error: (phase: string, data: any) => void;
|
||||
api?: (phase: string, data: any, requestId?: string) => void;
|
||||
flow?: (phase: string, data: any, requestId?: string) => void;
|
||||
};
|
||||
|
||||
export interface ToolContext extends ToolUseContext {
|
||||
setToolJSX?: (jsx: React.ReactElement) => void;
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 4.2: 统一导入语句
|
||||
**在所有三个工具文件的顶部**:
|
||||
```typescript
|
||||
import type { Tool } from '../../Tool';
|
||||
import type { ToolContext } from '../utils/types';
|
||||
```
|
||||
|
||||
### Phase 5: 验证和测试 (15分钟)
|
||||
|
||||
#### Step 5.1: 检查各工具错误
|
||||
```bash
|
||||
# StickerRequestTool
|
||||
npx tsc --noEmit 2>&1 | grep "StickerRequestTool"
|
||||
|
||||
# NotebookReadTool
|
||||
npx tsc --noEmit 2>&1 | grep "NotebookReadTool"
|
||||
|
||||
# AskExpertModelTool
|
||||
npx tsc --noEmit 2>&1 | grep "AskExpertModelTool"
|
||||
```
|
||||
|
||||
#### Step 5.2: 功能测试
|
||||
```bash
|
||||
# 启动 CLI
|
||||
bun run dev
|
||||
|
||||
# 测试笔记本读取(如果有 .ipynb 文件)
|
||||
# 输入: read notebook.ipynb
|
||||
|
||||
# 测试专家模型(如果配置了)
|
||||
# 输入: ask "What is TypeScript?"
|
||||
```
|
||||
|
||||
## 完成标志
|
||||
- [ ] StickerRequestTool 编译无错误
|
||||
- [ ] NotebookReadTool 类型转换正确
|
||||
- [ ] AskExpertModelTool debugLogger 调用修复
|
||||
- [ ] 所有工具都能加载
|
||||
- [ ] TypeScript 错误减少至少 20 个
|
||||
|
||||
## 注意事项
|
||||
1. **保持功能完整** - 不要删除功能代码
|
||||
2. **日志很重要** - 保留或改进日志记录
|
||||
3. **处理边缘情况** - 考虑 undefined/null 值
|
||||
4. **测试特殊文件** - 如 .ipynb 文件的解析
|
||||
|
||||
## 调试技巧
|
||||
|
||||
### 检查对象结构
|
||||
```typescript
|
||||
console.log('debugLogger type:', typeof debugLogger);
|
||||
console.log('debugLogger keys:', Object.keys(debugLogger || {}));
|
||||
```
|
||||
|
||||
### 类型调试
|
||||
```typescript
|
||||
// 临时添加以查看类型
|
||||
type DebugType = typeof debugLogger;
|
||||
const checkType: DebugType = null!;
|
||||
```
|
||||
|
||||
### 运行时检查
|
||||
```typescript
|
||||
if (!context.setToolJSX) {
|
||||
console.warn('setToolJSX not available in context');
|
||||
}
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: debugLogger 的正确用法?
|
||||
```bash
|
||||
# 查找其他使用示例
|
||||
grep -r "debugLogger" src --include="*.ts" --include="*.tsx" | head -20
|
||||
```
|
||||
|
||||
### Q: setToolJSX 在哪里定义?
|
||||
```bash
|
||||
# 查找定义
|
||||
grep -r "setToolJSX" src --include="*.ts" --include="*.tsx"
|
||||
```
|
||||
|
||||
### Q: Notebook 类型定义?
|
||||
```bash
|
||||
# 查找 Jupyter 相关类型
|
||||
find src -name "*.d.ts" | xargs grep -l "notebook\|jupyter"
|
||||
```
|
||||
|
||||
## 完成后
|
||||
这是 Step 1 的最后一个并行任务。完成后,可以进入 Step 2 的并行任务(React 组件、Hooks、Services 的修复)。
|
||||
331
.sub_task/step_2_parallel_worker_0.md
Normal file
331
.sub_task/step_2_parallel_worker_0.md
Normal file
@ -0,0 +1,331 @@
|
||||
# Step 2 - Worker 0: React 19 / Ink 6 组件修复
|
||||
|
||||
## 前置条件
|
||||
**必须先完成 step_0_foundation_serial.md 的所有任务**
|
||||
|
||||
## 项目背景
|
||||
React 19 和 Ink 6 引入了破坏性更改,特别是关于 props 的处理。主要问题是 `children` prop 现在是必需的,以及 `key` prop 不能作为组件 props 传递。
|
||||
|
||||
## 系统架构上下文
|
||||
```
|
||||
src/
|
||||
├── commands/
|
||||
│ └── agents.tsx - 代理命令界面
|
||||
├── components/
|
||||
│ └── messages/
|
||||
│ └── AssistantToolUseMessage.tsx
|
||||
├── screens/
|
||||
│ ├── REPL.tsx - 主交互界面
|
||||
│ └── Doctor.tsx - 诊断界面
|
||||
```
|
||||
|
||||
## 任务目标
|
||||
1. 修复所有 React 19 的 children prop 问题
|
||||
2. 修复 key prop 传递问题
|
||||
3. 修复导入路径问题
|
||||
4. 确保所有组件正确渲染
|
||||
|
||||
## 详细施工步骤
|
||||
|
||||
### Phase 1: 修复 key prop 问题 (20分钟)
|
||||
|
||||
#### Step 1.1: 修复 agents.tsx 中的 key prop
|
||||
**文件**: `src/commands/agents.tsx`
|
||||
**定位**: 第 2357, 2832, 3137, 3266 行
|
||||
**问题**: key 作为 props 传递而不是 JSX 属性
|
||||
|
||||
**查找模式**:
|
||||
```typescript
|
||||
<Text {...{key: index, color: 'gray'}}>
|
||||
```
|
||||
|
||||
**修复为**:
|
||||
```typescript
|
||||
<Text key={index} color="gray">
|
||||
```
|
||||
|
||||
**具体修复**:
|
||||
|
||||
1. **第 2357 行**:
|
||||
```typescript
|
||||
// 原始
|
||||
<Text {...{key: index, color: 'gray'}}>
|
||||
// 修复
|
||||
<Text key={index} color="gray">
|
||||
```
|
||||
|
||||
2. **第 3137 行**:
|
||||
```typescript
|
||||
// 原始
|
||||
<Text {...{key: someKey, color: 'blue'}}>
|
||||
// 修复
|
||||
<Text key={someKey} color="blue">
|
||||
```
|
||||
|
||||
3. **第 3266 行**:
|
||||
```typescript
|
||||
// 原始
|
||||
<Text {...{key: i, color: 'green'}}>
|
||||
// 修复
|
||||
<Text key={i} color="green">
|
||||
```
|
||||
|
||||
#### Step 1.2: 修复 isContinue 属性访问
|
||||
**文件**: `src/commands/agents.tsx`
|
||||
**定位**: 第 2832 行
|
||||
**问题**: 属性在某些类型上不存在
|
||||
|
||||
**查找代码**:
|
||||
```typescript
|
||||
if (option.isContinue) {
|
||||
```
|
||||
|
||||
**修复为**:
|
||||
```typescript
|
||||
if ('isContinue' in option && option.isContinue) {
|
||||
// 处理 continue 选项
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 2: 修复 children prop 问题 (25分钟)
|
||||
|
||||
#### Step 2.1: AssistantToolUseMessage 组件
|
||||
**文件**: `src/components/messages/AssistantToolUseMessage.tsx`
|
||||
**定位**: 第 65, 91 行
|
||||
|
||||
**第 65 行 - 函数调用参数**:
|
||||
```typescript
|
||||
// 查找
|
||||
someFunction(argument)
|
||||
// 如果函数不期望参数,修改为
|
||||
someFunction()
|
||||
```
|
||||
|
||||
**第 91 行 - 缺少 children**:
|
||||
```typescript
|
||||
// 原始
|
||||
<Text agentType={agentType} bold />
|
||||
// 修复
|
||||
<Text bold>
|
||||
{agentType ? `[${agentType}]` : 'Processing...'}
|
||||
</Text>
|
||||
```
|
||||
|
||||
#### Step 2.2: REPL.tsx 组件
|
||||
**文件**: `src/screens/REPL.tsx`
|
||||
**定位**: 第 526, 621, 625 行
|
||||
|
||||
**第 526 行 - TodoProvider**:
|
||||
```typescript
|
||||
// 原始
|
||||
<TodoProvider />
|
||||
// 修复
|
||||
<TodoProvider>
|
||||
{/* 子组件内容 */}
|
||||
<Box>{/* 实际的 TODO 界面 */}</Box>
|
||||
</TodoProvider>
|
||||
```
|
||||
|
||||
**第 621 行 - PermissionProvider**:
|
||||
```typescript
|
||||
// 原始
|
||||
<PermissionProvider isBypassPermissionsModeAvailable={...} />
|
||||
// 修复
|
||||
<PermissionProvider isBypassPermissionsModeAvailable={...}>
|
||||
{children}
|
||||
</PermissionProvider>
|
||||
```
|
||||
|
||||
**第 625 行 - Generic Provider**:
|
||||
```typescript
|
||||
// 原始
|
||||
<SomeProvider items={items} />
|
||||
// 修复
|
||||
<SomeProvider items={items}>
|
||||
{/* 查找原始代码中应该包含的子元素 */}
|
||||
{renderContent()}
|
||||
</SomeProvider>
|
||||
```
|
||||
|
||||
### Phase 3: 修复导入路径问题 (10分钟)
|
||||
|
||||
#### Step 3.1: Doctor.tsx 导入路径
|
||||
**文件**: `src/screens/Doctor.tsx`
|
||||
**定位**: 第 5 行
|
||||
**问题**: TypeScript 不允许 .tsx 扩展名
|
||||
|
||||
**查找代码**:
|
||||
```typescript
|
||||
import Something from '../path/to/file.tsx'
|
||||
```
|
||||
|
||||
**修复为**:
|
||||
```typescript
|
||||
import Something from '../path/to/file'
|
||||
```
|
||||
|
||||
### Phase 4: 修复 React 19 特定问题 (15分钟)
|
||||
|
||||
#### Step 4.1: 检查所有 Text 组件
|
||||
**全局搜索并修复**:
|
||||
```bash
|
||||
# 查找所有可能缺少 children 的 Text 组件
|
||||
grep -n "<Text.*\/>" src/commands/agents.tsx
|
||||
```
|
||||
|
||||
**修复模式**:
|
||||
```typescript
|
||||
// 如果发现自闭合的 Text 没有内容
|
||||
<Text color="gray" />
|
||||
// 修复为
|
||||
<Text color="gray">{' '}</Text>
|
||||
// 或
|
||||
<Text color="gray"></Text> // 零宽空格
|
||||
```
|
||||
|
||||
#### Step 4.2: 检查所有 Box 组件
|
||||
```typescript
|
||||
// 确保 Box 组件有内容或明确表示为空
|
||||
<Box />
|
||||
// 修复为
|
||||
<Box>{/* intentionally empty */}</Box>
|
||||
```
|
||||
|
||||
### Phase 5: 处理 React 19 严格模式 (10分钟)
|
||||
|
||||
#### Step 5.1: 添加类型声明(如果需要)
|
||||
**创建文件**: `src/types/react-overrides.d.ts`
|
||||
```typescript
|
||||
import 'react';
|
||||
|
||||
declare module 'react' {
|
||||
interface PropsWithChildren {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 5.2: 更新组件接口
|
||||
**对于自定义组件,确保 props 类型正确**:
|
||||
```typescript
|
||||
interface MyComponentProps {
|
||||
children: React.ReactNode; // 如果需要 children
|
||||
// 或
|
||||
children?: React.ReactNode; // 如果 children 可选
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 6: 验证和测试 (20分钟)
|
||||
|
||||
#### Step 6.1: 检查组件错误
|
||||
```bash
|
||||
# 检查 agents.tsx
|
||||
npx tsc --noEmit 2>&1 | grep "agents.tsx"
|
||||
|
||||
# 检查消息组件
|
||||
npx tsc --noEmit 2>&1 | grep "AssistantToolUseMessage"
|
||||
|
||||
# 检查 REPL
|
||||
npx tsc --noEmit 2>&1 | grep "REPL.tsx"
|
||||
|
||||
# 检查 Doctor
|
||||
npx tsc --noEmit 2>&1 | grep "Doctor.tsx"
|
||||
```
|
||||
|
||||
#### Step 6.2: 运行时测试
|
||||
```bash
|
||||
# 启动 CLI
|
||||
bun run dev
|
||||
|
||||
# 测试代理命令
|
||||
/agents
|
||||
|
||||
# 测试帮助
|
||||
/help
|
||||
|
||||
# 测试诊断
|
||||
/doctor
|
||||
```
|
||||
|
||||
#### Step 6.3: 视觉检查
|
||||
确保:
|
||||
- 文本正确显示
|
||||
- 列表项有正确的 key
|
||||
- 没有 React 警告在控制台
|
||||
|
||||
## 完成标志
|
||||
- [ ] 所有 key prop 正确传递
|
||||
- [ ] 所有组件都有必需的 children
|
||||
- [ ] 导入路径没有 .tsx 扩展名
|
||||
- [ ] agents.tsx 无类型错误
|
||||
- [ ] REPL.tsx 无类型错误
|
||||
- [ ] 所有组件正确渲染
|
||||
- [ ] TypeScript 错误减少至少 15 个
|
||||
|
||||
## 注意事项
|
||||
1. **保持布局** - 不要改变组件的视觉布局
|
||||
2. **保留功能** - 确保交互功能正常
|
||||
3. **React 19 兼容** - 遵循新的严格规则
|
||||
4. **Ink 6 特性** - 利用新的 Ink 特性如果适用
|
||||
|
||||
## 调试技巧
|
||||
|
||||
### 查找缺少 children 的组件
|
||||
```typescript
|
||||
// 添加临时 linter 规则
|
||||
/* eslint-disable react/no-children-prop */
|
||||
```
|
||||
|
||||
### 调试 key prop
|
||||
```typescript
|
||||
// 在开发模式下查看 key
|
||||
{items.map((item, index) => {
|
||||
console.log('Rendering item with key:', index);
|
||||
return <Text key={index}>{item}</Text>;
|
||||
})}
|
||||
```
|
||||
|
||||
### 检查组件 props
|
||||
```typescript
|
||||
// 临时添加 props 日志
|
||||
const MyComponent: React.FC<Props> = (props) => {
|
||||
console.log('Component props:', props);
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: children 应该是什么类型?
|
||||
```typescript
|
||||
// 对于 Ink 组件
|
||||
children: React.ReactNode
|
||||
|
||||
// 对于文本组件
|
||||
children: string | number
|
||||
|
||||
// 对于容器
|
||||
children: React.ReactElement | React.ReactElement[]
|
||||
```
|
||||
|
||||
### Q: key 应该如何传递?
|
||||
```typescript
|
||||
// 正确 ✅
|
||||
<Component key={id} otherProp="value" />
|
||||
|
||||
// 错误 ❌
|
||||
<Component {...{key: id, otherProp: "value"}} />
|
||||
<Component key={id} {...props} /> // 如果 props 包含 key
|
||||
```
|
||||
|
||||
### Q: 如何处理条件渲染的 children?
|
||||
```typescript
|
||||
<Container>
|
||||
{condition ? <Child /> : null}
|
||||
{/* 或 */}
|
||||
{condition && <Child />}
|
||||
</Container>
|
||||
```
|
||||
|
||||
## 完成后
|
||||
此任务完成后,React 组件相关的错误应该大幅减少。可以继续进行其他 Step 2 的并行任务。
|
||||
399
.sub_task/step_2_parallel_worker_1.md
Normal file
399
.sub_task/step_2_parallel_worker_1.md
Normal file
@ -0,0 +1,399 @@
|
||||
# Step 2 - Worker 1: Hook 系统修复
|
||||
|
||||
## 前置条件
|
||||
**必须先完成 step_0_foundation_serial.md 的所有任务**
|
||||
|
||||
## 项目背景
|
||||
Kode CLI 使用自定义 React Hooks 处理终端输入和用户交互。主要问题是 Ink 的 Key 类型缺少某些属性,以及一些未使用的 @ts-expect-error 指令。
|
||||
|
||||
## 系统架构上下文
|
||||
```
|
||||
src/hooks/
|
||||
├── useDoublePress.ts - 双击检测
|
||||
├── useTextInput.ts - 文本输入处理
|
||||
├── useUnifiedCompletion.ts - 自动完成功能
|
||||
└── ...其他 hooks
|
||||
```
|
||||
|
||||
## 任务目标
|
||||
1. 修复 Key 类型属性缺失问题
|
||||
2. 移除未使用的 @ts-expect-error 指令
|
||||
3. 确保所有输入处理正常工作
|
||||
|
||||
## 详细施工步骤
|
||||
|
||||
### Phase 1: 修复 Key 类型问题 (25分钟)
|
||||
|
||||
#### Step 1.1: 修复 useTextInput.ts
|
||||
**文件**: `src/hooks/useTextInput.ts`
|
||||
**问题位置**: 第 143, 266, 268, 272, 274 行
|
||||
|
||||
**第 143 行 - 移除未使用的指令**:
|
||||
```typescript
|
||||
// 删除这一行
|
||||
// @ts-expect-error
|
||||
```
|
||||
|
||||
**第 266, 268 行 - fn 属性不存在**:
|
||||
```typescript
|
||||
// 原始代码
|
||||
if (input.fn) {
|
||||
// 处理功能键
|
||||
}
|
||||
|
||||
// 修复方案 1 - 类型断言
|
||||
if ((input as any).fn) {
|
||||
// 处理功能键
|
||||
}
|
||||
|
||||
// 修复方案 2 - 属性检查
|
||||
if ('fn' in input && input.fn) {
|
||||
// 处理功能键
|
||||
}
|
||||
|
||||
// 修复方案 3 - 扩展类型(如果 step_0 已创建类型增强)
|
||||
// 确保导入了增强类型
|
||||
import type { Key } from 'ink';
|
||||
// Key 类型应该已经包含 fn 属性
|
||||
```
|
||||
|
||||
**第 272, 274 行 - home 和 end 属性**:
|
||||
```typescript
|
||||
// 原始代码
|
||||
if (input.home) {
|
||||
// 光标移到开始
|
||||
}
|
||||
if (input.end) {
|
||||
// 光标移到结束
|
||||
}
|
||||
|
||||
// 修复 - 使用扩展的 Key 类型或类型守卫
|
||||
if ('home' in input && input.home) {
|
||||
setCursorPosition(0);
|
||||
}
|
||||
if ('end' in input && input.end) {
|
||||
setCursorPosition(value.length);
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 1.2: 创建 Key 类型辅助函数
|
||||
**在 useTextInput.ts 顶部添加**:
|
||||
```typescript
|
||||
// Key 类型辅助函数
|
||||
interface ExtendedKey extends Key {
|
||||
fn?: boolean;
|
||||
home?: boolean;
|
||||
end?: boolean;
|
||||
}
|
||||
|
||||
function isExtendedKey(key: Key): key is ExtendedKey {
|
||||
return true; // 因为我们已经扩展了类型
|
||||
}
|
||||
|
||||
// 或者更安全的检查
|
||||
function hasProperty<T extends object, K extends PropertyKey>(
|
||||
obj: T,
|
||||
key: K
|
||||
): obj is T & Record<K, unknown> {
|
||||
return key in obj;
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 1.3: 重构键盘处理逻辑
|
||||
**优化键盘输入处理**:
|
||||
```typescript
|
||||
const handleKeyPress = (input: string, key: Key) => {
|
||||
// 功能键处理
|
||||
if (hasProperty(key, 'fn') && key.fn) {
|
||||
handleFunctionKey(input);
|
||||
return;
|
||||
}
|
||||
|
||||
// Home 键
|
||||
if (hasProperty(key, 'home') && key.home) {
|
||||
setCursorPosition(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// End 键
|
||||
if (hasProperty(key, 'end') && key.end) {
|
||||
setCursorPosition(value.length);
|
||||
return;
|
||||
}
|
||||
|
||||
// 普通按键处理
|
||||
if (key.return) {
|
||||
handleSubmit();
|
||||
} else if (key.backspace) {
|
||||
handleBackspace();
|
||||
} else if (key.delete) {
|
||||
handleDelete();
|
||||
} else if (key.leftArrow) {
|
||||
moveCursorLeft();
|
||||
} else if (key.rightArrow) {
|
||||
moveCursorRight();
|
||||
} else {
|
||||
insertText(input);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Phase 2: 修复 useUnifiedCompletion.ts (20分钟)
|
||||
|
||||
#### Step 2.1: 修复 space 属性
|
||||
**文件**: `src/hooks/useUnifiedCompletion.ts`
|
||||
**定位**: 第 1151 行
|
||||
**问题**: space 属性不存在
|
||||
|
||||
**查找代码**:
|
||||
```typescript
|
||||
if (key.space) {
|
||||
// 处理空格键
|
||||
}
|
||||
```
|
||||
|
||||
**修复为**:
|
||||
```typescript
|
||||
// 方案 1 - 检查输入字符
|
||||
if (input === ' ') {
|
||||
// 处理空格键
|
||||
}
|
||||
|
||||
// 方案 2 - 扩展属性检查
|
||||
if ('space' in key && key.space) {
|
||||
// 处理空格键
|
||||
}
|
||||
|
||||
// 方案 3 - 组合检查
|
||||
if (input === ' ' || (hasProperty(key, 'space') && key.space)) {
|
||||
// 处理空格键
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 2.2: 优化自动完成逻辑
|
||||
**改进类型安全**:
|
||||
```typescript
|
||||
interface CompletionKey extends Key {
|
||||
space?: boolean;
|
||||
tab?: boolean;
|
||||
}
|
||||
|
||||
const handleCompletionKey = (input: string, key: Key) => {
|
||||
const extKey = key as CompletionKey;
|
||||
|
||||
// Tab 完成
|
||||
if (key.tab) {
|
||||
return performCompletion();
|
||||
}
|
||||
|
||||
// 空格触发
|
||||
if (input === ' ' || extKey.space) {
|
||||
return checkForCompletion();
|
||||
}
|
||||
|
||||
// Escape 取消
|
||||
if (key.escape) {
|
||||
return cancelCompletion();
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Phase 3: 修复 useDoublePress.ts (10分钟)
|
||||
|
||||
#### Step 3.1: 移除未使用的指令
|
||||
**文件**: `src/hooks/useDoublePress.ts`
|
||||
**定位**: 第 33 行
|
||||
|
||||
**操作**:
|
||||
```typescript
|
||||
// 删除这一行
|
||||
// @ts-expect-error
|
||||
```
|
||||
|
||||
#### Step 3.2: 检查相关代码
|
||||
**确保删除指令后代码仍然正确**:
|
||||
```typescript
|
||||
// 检查第 33 行附近的代码
|
||||
// 如果有类型问题,正确修复而不是使用 @ts-expect-error
|
||||
```
|
||||
|
||||
### Phase 4: 创建通用 Hook 工具函数 (15分钟)
|
||||
|
||||
#### Step 4.1: 创建 hook 工具文件
|
||||
**创建文件**: `src/hooks/utils.ts`
|
||||
```typescript
|
||||
import type { Key } from 'ink';
|
||||
|
||||
// 扩展的 Key 类型
|
||||
export interface ExtendedKey extends Key {
|
||||
fn?: boolean;
|
||||
home?: boolean;
|
||||
end?: boolean;
|
||||
space?: boolean;
|
||||
pageUp?: boolean;
|
||||
pageDown?: boolean;
|
||||
}
|
||||
|
||||
// 类型守卫
|
||||
export function isExtendedKey(key: Key): key is ExtendedKey {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 属性检查
|
||||
export function hasKeyProperty<K extends keyof ExtendedKey>(
|
||||
key: Key,
|
||||
property: K
|
||||
): key is Key & Record<K, ExtendedKey[K]> {
|
||||
return property in key;
|
||||
}
|
||||
|
||||
// 键盘事件标准化
|
||||
export function normalizeKey(input: string, key: Key): ExtendedKey {
|
||||
return {
|
||||
...key,
|
||||
space: input === ' ',
|
||||
// 添加其他标准化逻辑
|
||||
} as ExtendedKey;
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 4.2: 更新 hooks 使用工具函数
|
||||
**在需要的 hooks 中导入**:
|
||||
```typescript
|
||||
import { hasKeyProperty, normalizeKey } from './utils';
|
||||
|
||||
// 使用
|
||||
const normalizedKey = normalizeKey(input, key);
|
||||
if (normalizedKey.space) {
|
||||
// 处理空格
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 5: 验证和测试 (15分钟)
|
||||
|
||||
#### Step 5.1: 检查 Hook 错误
|
||||
```bash
|
||||
# useTextInput
|
||||
npx tsc --noEmit 2>&1 | grep "useTextInput"
|
||||
|
||||
# useUnifiedCompletion
|
||||
npx tsc --noEmit 2>&1 | grep "useUnifiedCompletion"
|
||||
|
||||
# useDoublePress
|
||||
npx tsc --noEmit 2>&1 | grep "useDoublePress"
|
||||
|
||||
# 所有 hooks
|
||||
npx tsc --noEmit 2>&1 | grep "src/hooks/"
|
||||
```
|
||||
|
||||
#### Step 5.2: 功能测试
|
||||
```bash
|
||||
# 启动 CLI
|
||||
bun run dev
|
||||
|
||||
# 测试文本输入
|
||||
# 输入一些文本,测试:
|
||||
# - 光标移动 (箭头键)
|
||||
# - Home/End 键
|
||||
# - 退格/删除
|
||||
# - 自动完成 (Tab)
|
||||
|
||||
# 测试双击
|
||||
# 快速按两次相同的键
|
||||
```
|
||||
|
||||
#### Step 5.3: 创建测试脚本
|
||||
**创建文件**: `test-hooks.js`
|
||||
```javascript
|
||||
// 简单的键盘输入测试
|
||||
const readline = require('readline');
|
||||
|
||||
readline.emitKeypressEvents(process.stdin);
|
||||
process.stdin.setRawMode(true);
|
||||
|
||||
console.log('Press keys to test (Ctrl+C to exit):');
|
||||
|
||||
process.stdin.on('keypress', (str, key) => {
|
||||
console.log('Input:', str, 'Key:', key);
|
||||
|
||||
if (key && key.ctrl && key.name === 'c') {
|
||||
process.exit();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## 完成标志
|
||||
- [ ] useTextInput.ts 无类型错误
|
||||
- [ ] useUnifiedCompletion.ts 无类型错误
|
||||
- [ ] useDoublePress.ts 无未使用指令
|
||||
- [ ] 所有键盘输入正常工作
|
||||
- [ ] 自动完成功能正常
|
||||
- [ ] TypeScript 错误减少至少 10 个
|
||||
|
||||
## 注意事项
|
||||
1. **保持输入响应** - 不要引入延迟
|
||||
2. **处理边缘情况** - 考虑特殊键组合
|
||||
3. **保留快捷键** - 确保所有快捷键仍然工作
|
||||
4. **浏览器兼容性** - 如果有 web 版本,考虑兼容性
|
||||
|
||||
## 调试技巧
|
||||
|
||||
### 监控键盘输入
|
||||
```typescript
|
||||
useInput((input, key) => {
|
||||
console.log('Raw input:', { input, key });
|
||||
console.log('Key properties:', Object.keys(key));
|
||||
});
|
||||
```
|
||||
|
||||
### 测试特殊键
|
||||
```typescript
|
||||
const testKeys = {
|
||||
'Ctrl+C': { ctrl: true, name: 'c' },
|
||||
'Home': { home: true },
|
||||
'End': { end: true },
|
||||
'F1': { fn: true, name: 'f1' },
|
||||
};
|
||||
```
|
||||
|
||||
### 性能监控
|
||||
```typescript
|
||||
const handleInput = (input: string, key: Key) => {
|
||||
const start = performance.now();
|
||||
// 处理逻辑
|
||||
const end = performance.now();
|
||||
if (end - start > 16) { // 超过一帧
|
||||
console.warn('Slow input handling:', end - start);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: Key 类型从哪里来?
|
||||
```bash
|
||||
# 查看 ink 的类型定义
|
||||
cat node_modules/ink/build/index.d.ts | grep "interface Key"
|
||||
```
|
||||
|
||||
### Q: 如何处理组合键?
|
||||
```typescript
|
||||
if (key.ctrl && key.name === 'a') {
|
||||
// Ctrl+A: 全选
|
||||
}
|
||||
if (key.meta && key.name === 'v') {
|
||||
// Cmd+V (Mac) / Win+V: 粘贴
|
||||
}
|
||||
```
|
||||
|
||||
### Q: 输入延迟问题?
|
||||
```typescript
|
||||
// 使用防抖
|
||||
const debouncedHandler = useMemo(
|
||||
() => debounce(handleInput, 50),
|
||||
[]
|
||||
);
|
||||
```
|
||||
|
||||
## 完成后
|
||||
Hook 系统修复完成后,用户输入处理应该完全正常。这是用户体验的关键部分,确保充分测试。
|
||||
496
.sub_task/step_2_parallel_worker_2.md
Normal file
496
.sub_task/step_2_parallel_worker_2.md
Normal file
@ -0,0 +1,496 @@
|
||||
# Step 2 - Worker 2: Service 层修复
|
||||
|
||||
## 前置条件
|
||||
**必须先完成 step_0_foundation_serial.md 的所有任务**
|
||||
|
||||
## 项目背景
|
||||
Service 层处理与外部 API 的通信,包括 OpenAI、Claude 等模型服务。主要问题是对 unknown 类型的不安全访问。
|
||||
|
||||
## 系统架构上下文
|
||||
```
|
||||
src/services/
|
||||
├── openai.ts - OpenAI API 集成
|
||||
├── claude.ts - Claude API 集成
|
||||
├── modelAdapterFactory.ts - 模型适配器工厂
|
||||
└── mcpClient.ts - MCP 客户端
|
||||
src/entrypoints/
|
||||
├── cli.tsx - CLI 入口
|
||||
└── mcp.ts - MCP 入口
|
||||
```
|
||||
|
||||
## 任务目标
|
||||
1. 修复 openai.ts 中的类型安全问题
|
||||
2. 修复 entrypoints 中的函数调用问题
|
||||
3. 添加适当的错误处理和类型守卫
|
||||
|
||||
## 详细施工步骤
|
||||
|
||||
### Phase 1: 修复 OpenAI Service (30分钟)
|
||||
|
||||
#### Step 1.1: 添加响应类型定义
|
||||
**文件**: `src/services/openai.ts`
|
||||
**在文件顶部添加类型定义**:
|
||||
```typescript
|
||||
// OpenAI API 响应类型
|
||||
interface OpenAIErrorResponse {
|
||||
error?: {
|
||||
message: string;
|
||||
type: string;
|
||||
code?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface OpenAIModel {
|
||||
id: string;
|
||||
object: string;
|
||||
created: number;
|
||||
owned_by: string;
|
||||
}
|
||||
|
||||
interface OpenAIModelsResponse {
|
||||
data?: OpenAIModel[];
|
||||
models?: OpenAIModel[]; // 某些 API 兼容服务使用此字段
|
||||
object?: string;
|
||||
}
|
||||
|
||||
interface OpenAIChatResponse {
|
||||
id: string;
|
||||
object: string;
|
||||
created: number;
|
||||
choices: Array<{
|
||||
message: {
|
||||
role: string;
|
||||
content: string;
|
||||
};
|
||||
finish_reason: string;
|
||||
}>;
|
||||
usage?: {
|
||||
prompt_tokens: number;
|
||||
completion_tokens: number;
|
||||
total_tokens: number;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 1.2: 修复错误处理 (第 611, 743 行)
|
||||
**文件**: `src/services/openai.ts`
|
||||
**定位**: 第 611, 743 行
|
||||
|
||||
**修复错误访问**:
|
||||
```typescript
|
||||
// 原始代码
|
||||
if (error.error && error.message) {
|
||||
// 处理错误
|
||||
}
|
||||
|
||||
// 修复为 - 添加类型守卫
|
||||
function isOpenAIError(error: unknown): error is OpenAIErrorResponse {
|
||||
return (
|
||||
typeof error === 'object' &&
|
||||
error !== null &&
|
||||
'error' in error &&
|
||||
typeof (error as any).error === 'object'
|
||||
);
|
||||
}
|
||||
|
||||
// 使用类型守卫
|
||||
try {
|
||||
// API 调用
|
||||
} catch (error) {
|
||||
if (isOpenAIError(error) && error.error) {
|
||||
const errorMessage = error.error.message || 'Unknown error';
|
||||
console.error('OpenAI API error:', errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
} else if (error instanceof Error) {
|
||||
throw error;
|
||||
} else {
|
||||
throw new Error('Unknown error occurred');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 1.3: 修复模型列表处理 (第 1291-1299 行)
|
||||
**文件**: `src/services/openai.ts`
|
||||
**定位**: 第 1291-1299 行
|
||||
|
||||
**修复数据访问**:
|
||||
```typescript
|
||||
// 原始代码
|
||||
if (response.data || response.models) {
|
||||
const models = response.data || response.models;
|
||||
}
|
||||
|
||||
// 修复为 - 添加类型安全
|
||||
async function fetchModels(): Promise<OpenAIModel[]> {
|
||||
try {
|
||||
const response = await fetch('/v1/models');
|
||||
const data: unknown = await response.json();
|
||||
|
||||
// 类型验证
|
||||
if (!isModelsResponse(data)) {
|
||||
throw new Error('Invalid models response');
|
||||
}
|
||||
|
||||
// 安全访问
|
||||
const models = data.data || data.models || [];
|
||||
return models;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch models:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// 类型守卫
|
||||
function isModelsResponse(data: unknown): data is OpenAIModelsResponse {
|
||||
return (
|
||||
typeof data === 'object' &&
|
||||
data !== null &&
|
||||
(Array.isArray((data as any).data) || Array.isArray((data as any).models))
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 1.4: 创建通用 API 调用包装器
|
||||
**添加安全的 API 调用函数**:
|
||||
```typescript
|
||||
class OpenAIService {
|
||||
private async safeApiCall<T>(
|
||||
endpoint: string,
|
||||
options?: RequestInit
|
||||
): Promise<T> {
|
||||
try {
|
||||
const response = await fetch(endpoint, options);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
if (isOpenAIError(errorData)) {
|
||||
throw new Error(errorData.error?.message || 'API request failed');
|
||||
}
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data as T;
|
||||
} catch (error) {
|
||||
// 统一错误处理
|
||||
if (error instanceof Error) {
|
||||
throw error;
|
||||
}
|
||||
throw new Error('Unknown error in API call');
|
||||
}
|
||||
}
|
||||
|
||||
async listModels(): Promise<OpenAIModel[]> {
|
||||
const response = await this.safeApiCall<OpenAIModelsResponse>('/v1/models');
|
||||
return response.data || response.models || [];
|
||||
}
|
||||
|
||||
async createChatCompletion(params: any): Promise<OpenAIChatResponse> {
|
||||
return this.safeApiCall<OpenAIChatResponse>('/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 2: 修复 CLI 入口点 (20分钟)
|
||||
|
||||
#### Step 2.1: 修复 cli.tsx
|
||||
**文件**: `src/entrypoints/cli.tsx`
|
||||
|
||||
**第 318 行 - 移除未使用的 @ts-expect-error**:
|
||||
```typescript
|
||||
// 删除这一行
|
||||
// @ts-expect-error
|
||||
```
|
||||
|
||||
**第 543 行 - 修复 getConfig 重载**:
|
||||
```typescript
|
||||
// 原始代码
|
||||
const config = getConfig(isGlobal);
|
||||
|
||||
// 修复方案 1 - 明确类型
|
||||
const config = isGlobal ? getConfig(true) : getConfig(false);
|
||||
|
||||
// 修复方案 2 - 类型断言
|
||||
const config = getConfig(isGlobal as true);
|
||||
|
||||
// 修复方案 3 - 修改函数签名(如果可以)
|
||||
function getConfig(global?: boolean): Config {
|
||||
// 统一处理
|
||||
}
|
||||
```
|
||||
|
||||
**第 1042 行 - 修复无类型函数调用**:
|
||||
```typescript
|
||||
// 原始代码
|
||||
someFunction<Type>(args);
|
||||
|
||||
// 如果函数不是泛型,移除类型参数
|
||||
someFunction(args);
|
||||
|
||||
// 或者确保函数是泛型
|
||||
const someFunction = <T,>(arg: T): T => {
|
||||
return arg;
|
||||
};
|
||||
```
|
||||
|
||||
### Phase 3: 修复 MCP 入口点 (15分钟)
|
||||
|
||||
#### Step 3.1: 修复 mcp.ts
|
||||
**文件**: `src/entrypoints/mcp.ts`
|
||||
|
||||
**第 70 行 - 修复参数数量**:
|
||||
```typescript
|
||||
// 查找函数定义,确认期望的参数数量
|
||||
// 如果函数不期望参数
|
||||
someFunction();
|
||||
|
||||
// 如果参数是可选的
|
||||
someFunction(undefined);
|
||||
```
|
||||
|
||||
**第 130 行 - 修复参数数量**:
|
||||
```typescript
|
||||
// 原始:3 个参数,期望 2 个
|
||||
someFunction(arg1, arg2, arg3);
|
||||
|
||||
// 修复方案 1 - 移除多余参数
|
||||
someFunction(arg1, arg2);
|
||||
|
||||
// 修复方案 2 - 合并参数
|
||||
someFunction(arg1, { arg2, arg3 });
|
||||
|
||||
// 修复方案 3 - 检查函数签名是否正确
|
||||
// 可能函数签名已更改,需要更新调用
|
||||
```
|
||||
|
||||
### Phase 4: 创建类型安全的服务层 (15分钟)
|
||||
|
||||
#### Step 4.1: 创建服务基类
|
||||
**创建文件**: `src/services/base.ts`
|
||||
```typescript
|
||||
// 基础服务类,提供通用功能
|
||||
export abstract class BaseService {
|
||||
protected apiKey?: string;
|
||||
protected baseUrl: string;
|
||||
|
||||
constructor(config: { apiKey?: string; baseUrl: string }) {
|
||||
this.apiKey = config.apiKey;
|
||||
this.baseUrl = config.baseUrl;
|
||||
}
|
||||
|
||||
protected async request<T>(
|
||||
endpoint: string,
|
||||
options?: RequestInit
|
||||
): Promise<T> {
|
||||
const url = `${this.baseUrl}${endpoint}`;
|
||||
const headers = {
|
||||
...options?.headers,
|
||||
...(this.apiKey && { 'Authorization': `Bearer ${this.apiKey}` }),
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(url, { ...options, headers });
|
||||
|
||||
if (!response.ok) {
|
||||
await this.handleErrorResponse(response);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
this.handleError(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
protected async handleErrorResponse(response: Response): Promise<never> {
|
||||
const error = await response.json().catch(() => ({
|
||||
message: `HTTP ${response.status}`
|
||||
}));
|
||||
throw new Error(error.message || 'Request failed');
|
||||
}
|
||||
|
||||
protected handleError(error: unknown): void {
|
||||
console.error('Service error:', error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 4.2: 更新服务使用基类
|
||||
```typescript
|
||||
// 在 openai.ts 中
|
||||
import { BaseService } from './base';
|
||||
|
||||
export class OpenAIService extends BaseService {
|
||||
constructor(apiKey?: string) {
|
||||
super({
|
||||
apiKey,
|
||||
baseUrl: process.env.OPENAI_BASE_URL || 'https://api.openai.com',
|
||||
});
|
||||
}
|
||||
|
||||
async listModels(): Promise<OpenAIModel[]> {
|
||||
const response = await this.request<OpenAIModelsResponse>('/v1/models');
|
||||
return response.data || [];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 5: 验证和测试 (20分钟)
|
||||
|
||||
#### Step 5.1: 检查服务层错误
|
||||
```bash
|
||||
# OpenAI service
|
||||
npx tsc --noEmit 2>&1 | grep "openai.ts"
|
||||
|
||||
# CLI entrypoint
|
||||
npx tsc --noEmit 2>&1 | grep "cli.tsx"
|
||||
|
||||
# MCP entrypoint
|
||||
npx tsc --noEmit 2>&1 | grep "mcp.ts"
|
||||
|
||||
# 所有服务
|
||||
npx tsc --noEmit 2>&1 | grep "src/services/"
|
||||
```
|
||||
|
||||
#### Step 5.2: 测试 API 调用
|
||||
```bash
|
||||
# 启动 CLI
|
||||
bun run dev
|
||||
|
||||
# 如果配置了 OpenAI
|
||||
export OPENAI_API_KEY="your-key"
|
||||
|
||||
# 测试模型列表(如果有此功能)
|
||||
/models
|
||||
|
||||
# 测试基本功能
|
||||
/help
|
||||
```
|
||||
|
||||
#### Step 5.3: 创建测试脚本
|
||||
**创建文件**: `test-services.ts`
|
||||
```typescript
|
||||
import { OpenAIService } from './src/services/openai';
|
||||
|
||||
async function testServices() {
|
||||
console.log('Testing services...');
|
||||
|
||||
// 测试 OpenAI
|
||||
try {
|
||||
const openai = new OpenAIService(process.env.OPENAI_API_KEY);
|
||||
const models = await openai.listModels();
|
||||
console.log('OpenAI models:', models.length);
|
||||
} catch (error) {
|
||||
console.error('OpenAI test failed:', error);
|
||||
}
|
||||
|
||||
console.log('Tests complete');
|
||||
}
|
||||
|
||||
testServices();
|
||||
```
|
||||
|
||||
## 完成标志
|
||||
- [ ] OpenAI service 类型安全
|
||||
- [ ] 所有 unknown 类型正确处理
|
||||
- [ ] CLI 入口点无错误
|
||||
- [ ] MCP 入口点无错误
|
||||
- [ ] API 调用有错误处理
|
||||
- [ ] TypeScript 错误减少至少 12 个
|
||||
|
||||
## 注意事项
|
||||
1. **保护 API 密钥** - 不要记录敏感信息
|
||||
2. **处理网络错误** - 考虑超时和重试
|
||||
3. **向后兼容** - 保持现有 API 接口
|
||||
4. **性能考虑** - 避免不必要的 API 调用
|
||||
|
||||
## 调试技巧
|
||||
|
||||
### API 响应调试
|
||||
```typescript
|
||||
const debugResponse = async (response: Response) => {
|
||||
const text = await response.text();
|
||||
console.log('Response:', {
|
||||
status: response.status,
|
||||
headers: Object.fromEntries(response.headers),
|
||||
body: text.substring(0, 500),
|
||||
});
|
||||
return JSON.parse(text);
|
||||
};
|
||||
```
|
||||
|
||||
### 类型检查
|
||||
```typescript
|
||||
// 运行时类型检查
|
||||
console.log('Type of response:', typeof response);
|
||||
console.log('Response keys:', Object.keys(response || {}));
|
||||
```
|
||||
|
||||
### 网络请求监控
|
||||
```typescript
|
||||
const fetchWithLogging = async (url: string, options?: RequestInit) => {
|
||||
console.log(`[FETCH] ${options?.method || 'GET'} ${url}`);
|
||||
const start = Date.now();
|
||||
try {
|
||||
const response = await fetch(url, options);
|
||||
console.log(`[FETCH] ${response.status} in ${Date.now() - start}ms`);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error(`[FETCH] Error after ${Date.now() - start}ms:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 如何处理不同的 API 响应格式?
|
||||
```typescript
|
||||
// 使用联合类型
|
||||
type APIResponse = OpenAIResponse | AnthropicResponse | CustomResponse;
|
||||
|
||||
// 使用类型守卫区分
|
||||
function isOpenAIResponse(r: APIResponse): r is OpenAIResponse {
|
||||
return 'choices' in r;
|
||||
}
|
||||
```
|
||||
|
||||
### Q: 如何处理 API 版本差异?
|
||||
```typescript
|
||||
class VersionedAPI {
|
||||
private version: string;
|
||||
|
||||
constructor(version: string = 'v1') {
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
getEndpoint(path: string): string {
|
||||
return `/${this.version}${path}`;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Q: 重试逻辑?
|
||||
```typescript
|
||||
async function retryableRequest<T>(
|
||||
fn: () => Promise<T>,
|
||||
retries: number = 3
|
||||
): Promise<T> {
|
||||
for (let i = 0; i < retries; i++) {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (error) {
|
||||
if (i === retries - 1) throw error;
|
||||
await new Promise(r => setTimeout(r, 1000 * Math.pow(2, i)));
|
||||
}
|
||||
}
|
||||
throw new Error('Max retries exceeded');
|
||||
}
|
||||
```
|
||||
|
||||
## 完成后
|
||||
Service 层修复完成后,API 通信应该类型安全且稳定。这是系统可靠性的关键部分。
|
||||
169
PUBLISH_GUIDE.md
Normal file
169
PUBLISH_GUIDE.md
Normal file
@ -0,0 +1,169 @@
|
||||
# 发包脚本使用指南
|
||||
|
||||
Kode 项目提供了两套发包流程,用于不同的发布场景:
|
||||
|
||||
## 🚀 快速使用
|
||||
|
||||
### 开发版本发布 (测试用)
|
||||
```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 版本号
|
||||
- 不影响正式版本的用户
|
||||
- 可以快速迭代测试
|
||||
|
||||
### 2. 正式版本 (`latest` tag)
|
||||
- **目的**: 面向最终用户的稳定版本
|
||||
- **版本格式**: `1.1.16`, `1.1.17`, `1.2.0`
|
||||
- **安装方式**: `npm install -g @shareai-lab/kode` (默认)
|
||||
- **特点**:
|
||||
- 语义化版本控制
|
||||
- 严格的发布流程
|
||||
- 包含完整的测试和检查
|
||||
|
||||
## 🛠️ 脚本功能详解
|
||||
|
||||
### 开发版本发布 (`scripts/publish-dev.js`)
|
||||
|
||||
**自动化流程**:
|
||||
1. ✅ 检查当前分支和工作区状态
|
||||
2. 🔢 自动生成递增的 dev 版本号
|
||||
3. 🔨 构建项目
|
||||
4. 🔍 运行预发布检查
|
||||
5. 📤 发布到 npm 的 `dev` tag
|
||||
6. 🏷️ 创建 git tag
|
||||
7. 🔄 恢复 package.json (不提交版本变更)
|
||||
|
||||
**使用场景**:
|
||||
- 功能开发完成,需要内部测试
|
||||
- PR 合并前的最终验证
|
||||
- 快速修复验证
|
||||
|
||||
**安全特性**:
|
||||
- 临时修改 package.json,发布后自动恢复
|
||||
- 失败时自动回滚
|
||||
- 不污染主分支版本号
|
||||
|
||||
### 正式版本发布 (`scripts/publish-release.js`)
|
||||
|
||||
**交互式流程**:
|
||||
1. 🔍 检查分支 (建议在 main/master)
|
||||
2. 🧹 确保工作区干净
|
||||
3. 📡 拉取最新代码
|
||||
4. 🔢 选择版本升级类型:
|
||||
- **patch** (1.1.16 → 1.1.17): 修复 bug
|
||||
- **minor** (1.1.16 → 1.2.0): 新功能
|
||||
- **major** (1.1.16 → 2.0.0): 破坏性变更
|
||||
- **custom**: 自定义版本号
|
||||
5. ✅ 确认发布信息
|
||||
6. 🧪 运行测试和类型检查
|
||||
7. 🔨 构建项目
|
||||
8. 📝 提交版本更新
|
||||
9. 🏷️ 创建 git tag
|
||||
10. 📤 发布到 npm (默认 `latest` tag)
|
||||
11. 📡 推送到 git 仓库
|
||||
|
||||
**安全特性**:
|
||||
- 交互式确认,避免误发布
|
||||
- 测试失败时自动回滚版本号
|
||||
- 完整的 git 历史记录
|
||||
|
||||
## 🎯 最佳实践
|
||||
|
||||
### 开发流程建议
|
||||
```bash
|
||||
# 1. 开发功能
|
||||
git checkout -b feature/new-feature
|
||||
# ... 开发代码 ...
|
||||
git commit -am "feat: add new feature"
|
||||
|
||||
# 2. 发布开发版本测试
|
||||
npm run publish:dev
|
||||
# 安装测试: npm install -g @shareai-lab/kode@dev
|
||||
|
||||
# 3. 测试通过后合并到主分支
|
||||
git checkout main
|
||||
git merge feature/new-feature
|
||||
|
||||
# 4. 发布正式版本
|
||||
npm run publish:release
|
||||
```
|
||||
|
||||
### 版本号管理
|
||||
- **开发版**: 基于当前正式版本自动递增
|
||||
- **正式版**: 遵循 [语义化版本](https://semver.org/lang/zh-CN/) 规范
|
||||
- **Git 标签**: 自动创建,格式 `v1.1.16`
|
||||
|
||||
### 标签管理
|
||||
```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`
|
||||
- 确保有包的发布权限
|
||||
|
||||
**Git 相关错误?**
|
||||
- 确保有 git 推送权限
|
||||
- 检查远程仓库配置: `git remote -v`
|
||||
|
||||
### 手动清理
|
||||
```bash
|
||||
# 如果发布过程中断,可能需要手动清理
|
||||
git tag -d v1.1.16-dev.1 # 删除本地标签
|
||||
git push origin :v1.1.16-dev.1 # 删除远程标签
|
||||
```
|
||||
|
||||
## 📊 监控和分析
|
||||
|
||||
```bash
|
||||
# 查看包下载统计
|
||||
npm view @shareai-lab/kode
|
||||
|
||||
# 查看所有版本的详细信息
|
||||
npm view @shareai-lab/kode versions --json
|
||||
|
||||
# 测试安装
|
||||
npm install -g @shareai-lab/kode@dev
|
||||
kode --version
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
通过这套双发包系统,你可以:
|
||||
- 🚀 快速发布开发版本进行内部测试
|
||||
- 🛡️ 安全发布正式版本给最终用户
|
||||
- 📈 保持清晰的版本管理和发布历史
|
||||
- ⚡ 自动化大部分重复操作,减少人为错误
|
||||
451
fire-snake-game.html
Normal file
451
fire-snake-game.html
Normal file
@ -0,0 +1,451 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>喷火蛇游戏</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(45deg, #1a1a2e, #16213e);
|
||||
font-family: 'Courier New', monospace;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.game-container {
|
||||
text-align: center;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 15px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 10px 30px rgba(255, 100, 100, 0.3);
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #ff6b6b;
|
||||
margin-bottom: 10px;
|
||||
text-shadow: 2px 2px 4px rgba(255, 100, 100, 0.5);
|
||||
}
|
||||
|
||||
#gameCanvas {
|
||||
border: 3px solid #ff6b6b;
|
||||
border-radius: 10px;
|
||||
background: #000;
|
||||
box-shadow: 0 0 20px rgba(255, 100, 100, 0.4);
|
||||
}
|
||||
|
||||
.controls {
|
||||
margin: 15px 0;
|
||||
color: #ffa500;
|
||||
}
|
||||
|
||||
.score {
|
||||
font-size: 18px;
|
||||
color: #00ff00;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.game-over {
|
||||
color: #ff4444;
|
||||
font-size: 24px;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
button {
|
||||
background: linear-gradient(45deg, #ff6b6b, #ff8e53);
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
border-radius: 25px;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
font-size: 16px;
|
||||
margin: 5px;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="game-container">
|
||||
<h1>🐍 喷火蛇游戏 🔥</h1>
|
||||
<div class="controls">
|
||||
使用方向键移动 | 空格键喷火 | R键重新开始
|
||||
</div>
|
||||
<div class="score" id="score">分数: 0</div>
|
||||
<canvas id="gameCanvas" width="600" height="400"></canvas>
|
||||
<div id="gameOver" class="game-over" style="display: none;">
|
||||
游戏结束!按R键重新开始
|
||||
</div>
|
||||
<button onclick="startGame()">开始游戏</button>
|
||||
<button onclick="togglePause()">暂停/继续</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const canvas = document.getElementById('gameCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const scoreElement = document.getElementById('score');
|
||||
const gameOverElement = document.getElementById('gameOver');
|
||||
|
||||
// 游戏状态
|
||||
let gameRunning = false;
|
||||
let gamePaused = false;
|
||||
let score = 0;
|
||||
|
||||
// 网格大小
|
||||
const gridSize = 20;
|
||||
const canvasWidth = canvas.width;
|
||||
const canvasHeight = canvas.height;
|
||||
|
||||
// 蛇的初始状态
|
||||
let snake = [
|
||||
{x: 10, y: 10}
|
||||
];
|
||||
let direction = {x: 0, y: 0};
|
||||
|
||||
// 食物
|
||||
let food = generateFood();
|
||||
|
||||
// 火焰系统
|
||||
let flames = [];
|
||||
let fireBreathing = false;
|
||||
|
||||
// 粒子效果
|
||||
let particles = [];
|
||||
|
||||
// 生成食物
|
||||
function generateFood() {
|
||||
return {
|
||||
x: Math.floor(Math.random() * (canvasWidth / gridSize)),
|
||||
y: Math.floor(Math.random() * (canvasHeight / gridSize))
|
||||
};
|
||||
}
|
||||
|
||||
// 火焰粒子类
|
||||
class Flame {
|
||||
constructor(x, y, angle, speed) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.vx = Math.cos(angle) * speed;
|
||||
this.vy = Math.sin(angle) * speed;
|
||||
this.life = 30;
|
||||
this.maxLife = 30;
|
||||
this.size = Math.random() * 8 + 4;
|
||||
}
|
||||
|
||||
update() {
|
||||
this.x += this.vx;
|
||||
this.y += this.vy;
|
||||
this.life--;
|
||||
this.size *= 0.96;
|
||||
}
|
||||
|
||||
draw() {
|
||||
if (this.life <= 0) return;
|
||||
|
||||
const alpha = this.life / this.maxLife;
|
||||
const hue = 60 - (1 - alpha) * 60; // 从黄色到红色
|
||||
|
||||
ctx.save();
|
||||
ctx.globalAlpha = alpha;
|
||||
ctx.fillStyle = `hsl(${hue}, 100%, 50%)`;
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
// 粒子类(用于特效)
|
||||
class Particle {
|
||||
constructor(x, y, color) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.vx = (Math.random() - 0.5) * 4;
|
||||
this.vy = (Math.random() - 0.5) * 4;
|
||||
this.life = 20;
|
||||
this.color = color;
|
||||
}
|
||||
|
||||
update() {
|
||||
this.x += this.vx;
|
||||
this.y += this.vy;
|
||||
this.life--;
|
||||
}
|
||||
|
||||
draw() {
|
||||
if (this.life <= 0) return;
|
||||
|
||||
ctx.save();
|
||||
ctx.globalAlpha = this.life / 20;
|
||||
ctx.fillStyle = this.color;
|
||||
ctx.fillRect(this.x, this.y, 4, 4);
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
// 键盘事件处理
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (!gameRunning || gamePaused) {
|
||||
if (e.code === 'KeyR') {
|
||||
startGame();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
switch(e.code) {
|
||||
case 'ArrowUp':
|
||||
if (direction.y === 0) direction = {x: 0, y: -1};
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
if (direction.y === 0) direction = {x: 0, y: 1};
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
if (direction.x === 0) direction = {x: -1, y: 0};
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
if (direction.x === 0) direction = {x: 1, y: 0};
|
||||
break;
|
||||
case 'Space':
|
||||
e.preventDefault();
|
||||
breatheFire();
|
||||
break;
|
||||
case 'KeyR':
|
||||
startGame();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// 喷火功能
|
||||
function breatheFire() {
|
||||
if (!gameRunning || fireBreathing) return;
|
||||
|
||||
fireBreathing = true;
|
||||
setTimeout(() => fireBreathing = false, 200);
|
||||
|
||||
const head = snake[0];
|
||||
const headX = head.x * gridSize + gridSize / 2;
|
||||
const headY = head.y * gridSize + gridSize / 2;
|
||||
|
||||
// 计算喷火方向
|
||||
let fireAngle = 0;
|
||||
if (direction.x === 1) fireAngle = 0;
|
||||
else if (direction.x === -1) fireAngle = Math.PI;
|
||||
else if (direction.y === -1) fireAngle = -Math.PI / 2;
|
||||
else if (direction.y === 1) fireAngle = Math.PI / 2;
|
||||
|
||||
// 创建火焰粒子
|
||||
for (let i = 0; i < 15; i++) {
|
||||
const angle = fireAngle + (Math.random() - 0.5) * 0.8;
|
||||
const speed = Math.random() * 8 + 4;
|
||||
flames.push(new Flame(headX, headY, angle, speed));
|
||||
}
|
||||
}
|
||||
|
||||
// 检查火焰碰撞
|
||||
function checkFlameCollisions() {
|
||||
flames.forEach(flame => {
|
||||
// 检查是否击中食物
|
||||
const foodX = food.x * gridSize + gridSize / 2;
|
||||
const foodY = food.y * gridSize + gridSize / 2;
|
||||
const distance = Math.sqrt((flame.x - foodX) ** 2 + (flame.y - foodY) ** 2);
|
||||
|
||||
if (distance < gridSize / 2 + flame.size) {
|
||||
// 火焰击中食物,获得额外分数
|
||||
score += 5;
|
||||
food = generateFood();
|
||||
|
||||
// 添加特效
|
||||
for (let i = 0; i < 10; i++) {
|
||||
particles.push(new Particle(foodX, foodY, '#ffff00'));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 绘制蛇
|
||||
function drawSnake() {
|
||||
snake.forEach((segment, index) => {
|
||||
ctx.fillStyle = index === 0 ? '#ff6b6b' : '#ff8e53'; // 头部更红
|
||||
ctx.fillRect(segment.x * gridSize, segment.y * gridSize, gridSize - 2, gridSize - 2);
|
||||
|
||||
// 蛇头添加眼睛
|
||||
if (index === 0) {
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.fillRect(segment.x * gridSize + 4, segment.y * gridSize + 4, 4, 4);
|
||||
ctx.fillRect(segment.x * gridSize + 12, segment.y * gridSize + 4, 4, 4);
|
||||
|
||||
ctx.fillStyle = '#000';
|
||||
ctx.fillRect(segment.x * gridSize + 5, segment.y * gridSize + 5, 2, 2);
|
||||
ctx.fillRect(segment.x * gridSize + 13, segment.y * gridSize + 5, 2, 2);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 绘制食物
|
||||
function drawFood() {
|
||||
ctx.fillStyle = '#00ff00';
|
||||
ctx.fillRect(food.x * gridSize, food.y * gridSize, gridSize - 2, gridSize - 2);
|
||||
|
||||
// 食物闪烁效果
|
||||
if (Math.floor(Date.now() / 200) % 2) {
|
||||
ctx.fillStyle = '#88ff88';
|
||||
ctx.fillRect(food.x * gridSize + 4, food.y * gridSize + 4, gridSize - 10, gridSize - 10);
|
||||
}
|
||||
}
|
||||
|
||||
// 移动蛇
|
||||
function moveSnake() {
|
||||
if (direction.x === 0 && direction.y === 0) return;
|
||||
|
||||
const head = {
|
||||
x: snake[0].x + direction.x,
|
||||
y: snake[0].y + direction.y
|
||||
};
|
||||
|
||||
snake.unshift(head);
|
||||
|
||||
// 检查是否吃到食物
|
||||
if (head.x === food.x && head.y === food.y) {
|
||||
score += 10;
|
||||
food = generateFood();
|
||||
|
||||
// 添加粒子特效
|
||||
for (let i = 0; i < 8; i++) {
|
||||
particles.push(new Particle(
|
||||
head.x * gridSize + gridSize / 2,
|
||||
head.y * gridSize + gridSize / 2,
|
||||
'#00ff00'
|
||||
));
|
||||
}
|
||||
} else {
|
||||
snake.pop();
|
||||
}
|
||||
}
|
||||
|
||||
// 检查碰撞
|
||||
function checkCollisions() {
|
||||
const head = snake[0];
|
||||
|
||||
// 墙壁碰撞
|
||||
if (head.x < 0 || head.x >= canvasWidth / gridSize ||
|
||||
head.y < 0 || head.y >= canvasHeight / gridSize) {
|
||||
gameOver();
|
||||
return;
|
||||
}
|
||||
|
||||
// 自身碰撞
|
||||
for (let i = 1; i < snake.length; i++) {
|
||||
if (head.x === snake[i].x && head.y === snake[i].y) {
|
||||
gameOver();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 游戏结束
|
||||
function gameOver() {
|
||||
gameRunning = false;
|
||||
gameOverElement.style.display = 'block';
|
||||
}
|
||||
|
||||
// 更新游戏
|
||||
function update() {
|
||||
if (!gameRunning || gamePaused) return;
|
||||
|
||||
moveSnake();
|
||||
checkCollisions();
|
||||
checkFlameCollisions();
|
||||
|
||||
// 更新火焰
|
||||
flames = flames.filter(flame => {
|
||||
flame.update();
|
||||
return flame.life > 0;
|
||||
});
|
||||
|
||||
// 更新粒子
|
||||
particles = particles.filter(particle => {
|
||||
particle.update();
|
||||
return particle.life > 0;
|
||||
});
|
||||
|
||||
scoreElement.textContent = `分数: ${score}`;
|
||||
}
|
||||
|
||||
// 绘制游戏
|
||||
function draw() {
|
||||
// 清空画布
|
||||
ctx.fillStyle = '#000';
|
||||
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
|
||||
|
||||
// 绘制网格(可选)
|
||||
ctx.strokeStyle = '#111';
|
||||
for (let i = 0; i < canvasWidth; i += gridSize) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(i, 0);
|
||||
ctx.lineTo(i, canvasHeight);
|
||||
ctx.stroke();
|
||||
}
|
||||
for (let i = 0; i < canvasHeight; i += gridSize) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, i);
|
||||
ctx.lineTo(canvasWidth, i);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
drawFood();
|
||||
drawSnake();
|
||||
|
||||
// 绘制火焰
|
||||
flames.forEach(flame => flame.draw());
|
||||
|
||||
// 绘制粒子
|
||||
particles.forEach(particle => particle.draw());
|
||||
}
|
||||
|
||||
// 游戏主循环
|
||||
function gameLoop() {
|
||||
update();
|
||||
draw();
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
|
||||
// 开始游戏
|
||||
function startGame() {
|
||||
gameRunning = true;
|
||||
gamePaused = false;
|
||||
score = 0;
|
||||
snake = [{x: 10, y: 10}];
|
||||
direction = {x: 0, y: 0};
|
||||
food = generateFood();
|
||||
flames = [];
|
||||
particles = [];
|
||||
gameOverElement.style.display = 'none';
|
||||
}
|
||||
|
||||
// 暂停/继续游戏
|
||||
function togglePause() {
|
||||
if (gameRunning) {
|
||||
gamePaused = !gamePaused;
|
||||
}
|
||||
}
|
||||
|
||||
// 启动游戏循环
|
||||
gameLoop();
|
||||
|
||||
// 自动开始游戏
|
||||
setTimeout(() => {
|
||||
if (!gameRunning) {
|
||||
startGame();
|
||||
}
|
||||
}, 1000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
114
fix-typescript-errors.sh
Normal file
114
fix-typescript-errors.sh
Normal file
@ -0,0 +1,114 @@
|
||||
#!/bin/bash
|
||||
|
||||
# TypeScript Error Fix Execution Script
|
||||
# This script helps track progress through the fix phases
|
||||
|
||||
set -e
|
||||
|
||||
echo "TypeScript Error Fix Script - 100% Confidence Plan"
|
||||
echo "=================================================="
|
||||
echo ""
|
||||
|
||||
# Color codes for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Function to run TypeScript check
|
||||
check_typescript() {
|
||||
echo -e "${YELLOW}Running TypeScript compilation check...${NC}"
|
||||
if npx tsc --noEmit 2>&1 | tee typescript-errors.log; then
|
||||
echo -e "${GREEN}✓ No TypeScript errors found!${NC}"
|
||||
return 0
|
||||
else
|
||||
ERROR_COUNT=$(npx tsc --noEmit 2>&1 | wc -l)
|
||||
echo -e "${RED}✗ Found $ERROR_COUNT lines of errors${NC}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to show current phase
|
||||
show_phase() {
|
||||
echo ""
|
||||
echo -e "${GREEN}════════════════════════════════════════${NC}"
|
||||
echo -e "${GREEN} PHASE $1: $2${NC}"
|
||||
echo -e "${GREEN}════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Initial check
|
||||
echo "Initial TypeScript Error Count:"
|
||||
check_typescript || true
|
||||
INITIAL_ERRORS=$(wc -l < typescript-errors.log)
|
||||
echo ""
|
||||
|
||||
# Phase tracking
|
||||
CURRENT_PHASE=1
|
||||
PHASES_COMPLETED=0
|
||||
|
||||
while true; do
|
||||
echo -e "${YELLOW}Current Phase: $CURRENT_PHASE${NC}"
|
||||
echo "Select an action:"
|
||||
echo "1) Check current TypeScript errors"
|
||||
echo "2) Mark current phase as complete"
|
||||
echo "3) View specific error category"
|
||||
echo "4) Generate error summary"
|
||||
echo "5) Exit"
|
||||
|
||||
read -p "Choice: " choice
|
||||
|
||||
case $choice in
|
||||
1)
|
||||
check_typescript || true
|
||||
CURRENT_ERRORS=$(wc -l < typescript-errors.log)
|
||||
FIXED=$((INITIAL_ERRORS - CURRENT_ERRORS))
|
||||
echo ""
|
||||
echo -e "${GREEN}Progress: Fixed $FIXED errors (from $INITIAL_ERRORS to $CURRENT_ERRORS)${NC}"
|
||||
;;
|
||||
2)
|
||||
PHASES_COMPLETED=$((PHASES_COMPLETED + 1))
|
||||
echo -e "${GREEN}✓ Phase $CURRENT_PHASE completed!${NC}"
|
||||
CURRENT_PHASE=$((CURRENT_PHASE + 1))
|
||||
|
||||
case $CURRENT_PHASE in
|
||||
2) show_phase 2 "Tool System Implementation" ;;
|
||||
3) show_phase 3 "React 19 / Ink 6 Components" ;;
|
||||
4) show_phase 4 "Service Layer Fixes" ;;
|
||||
5) show_phase 5 "Hook System Updates" ;;
|
||||
6) show_phase 6 "Utility Functions" ;;
|
||||
7) show_phase 7 "Dependency Management" ;;
|
||||
8) show_phase 8 "Validation & Testing" ;;
|
||||
*)
|
||||
echo -e "${GREEN}🎉 All phases completed!${NC}"
|
||||
check_typescript && echo -e "${GREEN}✨ TypeScript compilation successful!${NC}"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
3)
|
||||
echo "Error categories:"
|
||||
echo "1) Tool errors"
|
||||
echo "2) Component errors"
|
||||
echo "3) Hook errors"
|
||||
echo "4) Service errors"
|
||||
read -p "Select category: " cat
|
||||
case $cat in
|
||||
1) grep -E "src/tools/" typescript-errors.log | head -20 ;;
|
||||
2) grep -E "src/components/|src/screens/" typescript-errors.log | head -20 ;;
|
||||
3) grep -E "src/hooks/" typescript-errors.log | head -20 ;;
|
||||
4) grep -E "src/services/" typescript-errors.log | head -20 ;;
|
||||
esac
|
||||
;;
|
||||
4)
|
||||
echo "Error Summary by Directory:"
|
||||
echo "----------------------------"
|
||||
npx tsc --noEmit 2>&1 | grep -oE "src/[^(]*" | cut -d: -f1 | xargs -I {} dirname {} | sort | uniq -c | sort -rn
|
||||
;;
|
||||
5)
|
||||
echo "Exiting..."
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
echo ""
|
||||
done
|
||||
96
package.json
96
package.json
@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "@shareai-lab/kode",
|
||||
"version": "1.0.80",
|
||||
"version": "1.1.16",
|
||||
"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,96 +24,24 @@
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/bedrock-sdk": "^0.12.6",
|
||||
"@anthropic-ai/sdk": "^0.39.0",
|
||||
"@anthropic-ai/vertex-sdk": "^0.7.0",
|
||||
"@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",
|
||||
"chalk": "^5.4.1",
|
||||
"cli-highlight": "^2.1.11",
|
||||
"cli-table3": "^0.6.5",
|
||||
"commander": "^13.1.0",
|
||||
"debug": "^4.4.1",
|
||||
"diff": "^7.0.0",
|
||||
"dotenv": "^16.6.1",
|
||||
"env-paths": "^3.0.0",
|
||||
"figures": "^6.1.0",
|
||||
"glob": "^11.0.3",
|
||||
"gray-matter": "^4.0.3",
|
||||
"highlight.js": "^11.11.1",
|
||||
"ink": "^5.2.1",
|
||||
"ink-link": "^4.1.0",
|
||||
"ink-select-input": "^6.2.0",
|
||||
"ink-text-input": "^6.0.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lru-cache": "^11.1.0",
|
||||
"marked": "^15.0.12",
|
||||
"nanoid": "^5.1.5",
|
||||
"node-fetch": "^3.3.2",
|
||||
"node-html-parser": "^7.0.1",
|
||||
"openai": "^4.104.0",
|
||||
"react": "18.3.1",
|
||||
"semver": "^7.7.2",
|
||||
"shell-quote": "^1.8.3",
|
||||
"spawn-rx": "^5.1.2",
|
||||
"tsx": "^4.20.3",
|
||||
"turndown": "^7.2.1",
|
||||
"undici": "^7.11.0",
|
||||
"wrap-ansi": "^9.0.0",
|
||||
"zod": "^3.25.76",
|
||||
"zod-to-json-schema": "^3.24.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^24.1.0",
|
||||
"bun-types": "latest",
|
||||
"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"
|
||||
]
|
||||
"typecheck": "tsc --noEmit",
|
||||
"prepare": "",
|
||||
"publish:dev": "node scripts/publish-dev.js",
|
||||
"publish:release": "node scripts/publish-release.js"
|
||||
}
|
||||
}
|
||||
146
phase1-fixes.md
Normal file
146
phase1-fixes.md
Normal file
@ -0,0 +1,146 @@
|
||||
# Phase 1: Core Type System Foundation - Detailed Fix Guide
|
||||
|
||||
## 1.1 Message Type System Fix
|
||||
|
||||
### Problem Analysis
|
||||
The `Message` type union has inconsistent property access. Some code expects a `message` property that doesn't exist on all union members.
|
||||
|
||||
### Fix Location: `src/messages.ts`
|
||||
```typescript
|
||||
// Current problematic union
|
||||
export type Message = AssistantMessage | UserMessage | ProgressMessage
|
||||
|
||||
// The issue: Code in query.ts accesses .message property which doesn't exist on ProgressMessage
|
||||
```
|
||||
|
||||
### Solution
|
||||
```typescript
|
||||
// Option 1: Add message property to ProgressMessage
|
||||
export interface ProgressMessage {
|
||||
type: 'progress'
|
||||
message?: any // Add this
|
||||
// ... existing properties
|
||||
}
|
||||
|
||||
// Option 2: Fix access pattern in query.ts
|
||||
// Instead of directly accessing .message, use type guards:
|
||||
if (msg.type !== 'progress' && 'message' in msg) {
|
||||
// Safe to access msg.message
|
||||
}
|
||||
```
|
||||
|
||||
### Fix in `src/utils/messageContextManager.ts` (line 136)
|
||||
```typescript
|
||||
// Current
|
||||
return {
|
||||
type: "assistant",
|
||||
message: { role: "assistant", content: [...] }
|
||||
}
|
||||
|
||||
// Fixed
|
||||
return {
|
||||
type: "assistant",
|
||||
message: { role: "assistant", content: [...] },
|
||||
costUSD: 0, // Add required property
|
||||
durationMs: 0, // Add required property
|
||||
uuid: crypto.randomUUID() as UUID // Add required property
|
||||
}
|
||||
```
|
||||
|
||||
## 1.2 Tool Interface Alignment
|
||||
|
||||
### Problem Analysis
|
||||
The Tool interface expects specific return types, but implementations return different types.
|
||||
|
||||
### Fix Location: `src/Tool.ts`
|
||||
```typescript
|
||||
// Current interface (approximate)
|
||||
export interface Tool<TInput, TOutput> {
|
||||
renderResultForAssistant(output: TOutput): string
|
||||
renderToolUseRejectedMessage(): React.ReactElement
|
||||
// ...
|
||||
}
|
||||
|
||||
// Fixed interface
|
||||
export interface Tool<TInput, TOutput> {
|
||||
renderResultForAssistant(output: TOutput): string | any[] // Allow arrays
|
||||
renderToolUseRejectedMessage(...args: any[]): React.ReactElement // Allow optional params
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Add to ToolUseContext
|
||||
```typescript
|
||||
// In src/types.ts or wherever ToolUseContext is defined
|
||||
export interface ToolUseContext {
|
||||
// ... existing properties
|
||||
setToolJSX?: (jsx: React.ReactElement) => void // Add as optional
|
||||
}
|
||||
|
||||
export interface ExtendedToolUseContext extends ToolUseContext {
|
||||
setToolJSX: (jsx: React.ReactElement) => void // Required in extended version
|
||||
}
|
||||
```
|
||||
|
||||
## 1.3 Key Type Extensions
|
||||
|
||||
### Problem Analysis
|
||||
The Key type from Ink doesn't have all properties that the code expects.
|
||||
|
||||
### Fix Location: Create `src/types/ink-augmentation.d.ts`
|
||||
```typescript
|
||||
// Type augmentation for ink
|
||||
declare module 'ink' {
|
||||
interface Key {
|
||||
fn?: boolean
|
||||
home?: boolean
|
||||
end?: boolean
|
||||
space?: boolean
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Alternative: Create wrapper type
|
||||
```typescript
|
||||
// In src/types/input.ts
|
||||
import { Key as InkKey } from 'ink'
|
||||
|
||||
export interface ExtendedKey extends InkKey {
|
||||
fn?: boolean
|
||||
home?: boolean
|
||||
end?: boolean
|
||||
space?: boolean
|
||||
}
|
||||
|
||||
// Then update all usages from Key to ExtendedKey
|
||||
```
|
||||
|
||||
## Verification Steps
|
||||
|
||||
After each fix:
|
||||
1. Run `npx tsc --noEmit` to check error count
|
||||
2. Verify no runtime errors with `bun run dev`
|
||||
3. Test affected functionality
|
||||
|
||||
## Expected Outcome
|
||||
|
||||
After Phase 1 completion:
|
||||
- Message type errors in query.ts resolved
|
||||
- Tool interface matches all implementations
|
||||
- Key type has all required properties
|
||||
- Error count reduced by approximately 40-50 errors
|
||||
|
||||
## Commands to Run
|
||||
|
||||
```bash
|
||||
# After each file change
|
||||
npx tsc --noEmit 2>&1 | grep -c "error TS"
|
||||
|
||||
# Check specific file errors
|
||||
npx tsc --noEmit 2>&1 | grep "src/query.ts"
|
||||
npx tsc --noEmit 2>&1 | grep "src/messages.ts"
|
||||
npx tsc --noEmit 2>&1 | grep "Tool.ts"
|
||||
|
||||
# Test runtime
|
||||
bun run dev
|
||||
```
|
||||
94
quick-fix-checklist.md
Normal file
94
quick-fix-checklist.md
Normal file
@ -0,0 +1,94 @@
|
||||
# Quick Fix Checklist - Start Here! 🚀
|
||||
|
||||
## Immediate Actions (Fix These First)
|
||||
|
||||
### 1. Install Missing Dependencies (2 min)
|
||||
```bash
|
||||
bun add sharp
|
||||
bun add -d @types/sharp
|
||||
```
|
||||
|
||||
### 2. Create Type Augmentation File (5 min)
|
||||
Create `src/types/ink-augmentation.d.ts`:
|
||||
```typescript
|
||||
declare module 'ink' {
|
||||
interface Key {
|
||||
fn?: boolean
|
||||
home?: boolean
|
||||
end?: boolean
|
||||
space?: boolean
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Fix Critical Type Errors (15 min)
|
||||
|
||||
#### A. Fix src/query.ts (lines 203-210)
|
||||
Replace direct `.message` access with type guard:
|
||||
```typescript
|
||||
// Before: msg.message
|
||||
// After:
|
||||
if (msg.type !== 'progress' && 'message' in msg) {
|
||||
// use msg.message
|
||||
}
|
||||
```
|
||||
|
||||
#### B. Fix src/utils/messageContextManager.ts (line 136)
|
||||
Add missing properties:
|
||||
```typescript
|
||||
return {
|
||||
type: "assistant",
|
||||
message: { role: "assistant", content: [...] },
|
||||
costUSD: 0,
|
||||
durationMs: 0,
|
||||
uuid: crypto.randomUUID() as UUID
|
||||
}
|
||||
```
|
||||
|
||||
#### C. Fix src/utils/thinking.ts (line 115)
|
||||
Remove 'minimal' from type:
|
||||
```typescript
|
||||
// Change from: "low" | "medium" | "high" | "minimal"
|
||||
// To: "low" | "medium" | "high"
|
||||
```
|
||||
|
||||
### 4. Quick Component Fixes (10 min)
|
||||
|
||||
#### A. Fix key prop issues in src/commands/agents.tsx
|
||||
```typescript
|
||||
// Instead of: <Text {...{key: index, color: 'gray'}}>
|
||||
// Use: <Text key={index} color="gray">
|
||||
```
|
||||
|
||||
#### B. Add children to components
|
||||
```typescript
|
||||
// src/components/messages/AssistantToolUseMessage.tsx (line 91)
|
||||
<Text agentType={agentType} bold>{/* Add content here */}</Text>
|
||||
|
||||
// src/screens/REPL.tsx (line 526)
|
||||
<TodoProvider>{/* Add children */}</TodoProvider>
|
||||
```
|
||||
|
||||
### 5. Remove Unused Directives (5 min)
|
||||
Remove these lines:
|
||||
- src/entrypoints/cli.tsx:318
|
||||
- src/hooks/useDoublePress.ts:33
|
||||
- src/hooks/useTextInput.ts:143
|
||||
- src/utils/messages.tsx:301
|
||||
|
||||
## Verify Progress
|
||||
```bash
|
||||
# Check error count
|
||||
npx tsc --noEmit 2>&1 | wc -l
|
||||
|
||||
# Should see significant reduction after these fixes
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
Once these quick fixes are done:
|
||||
1. Run full TypeScript check
|
||||
2. Move to Phase 2 (Tool implementations)
|
||||
3. Use tasks.md for detailed tracking
|
||||
|
||||
## Expected Result
|
||||
These quick fixes should eliminate ~40-50% of errors, making the remaining issues much clearer.
|
||||
112
scripts/build.mjs
Normal file
112
scripts/build.mjs
Normal file
@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env node
|
||||
import { build } from 'esbuild'
|
||||
import { existsSync, mkdirSync, writeFileSync, cpSync, readFileSync, readdirSync, statSync } from 'node:fs'
|
||||
import { join, extname, dirname } 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' })
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
console.log('✅ Build completed for cross-platform compatibility!')
|
||||
console.log('📋 Generated files:')
|
||||
console.log(' - dist/ (CommonJS modules)')
|
||||
console.log(' - dist/index.js (main entrypoint)')
|
||||
console.log(' - dist/entrypoints/cli.js (CLI main)')
|
||||
console.log(' - cli.js (cross-platform wrapper)')
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error('❌ Build failed:', err)
|
||||
process.exit(1)
|
||||
})
|
||||
139
scripts/build.ts
139
scripts/build.ts
@ -12,70 +12,119 @@ async function build() {
|
||||
rmSync(file, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
// Ensure dist folder exists
|
||||
if (!existsSync('dist')) {
|
||||
// @ts-ignore
|
||||
await import('node:fs/promises').then(m => m.mkdir('dist', { recursive: true }))
|
||||
}
|
||||
|
||||
// Create the CLI wrapper
|
||||
// Create the CLI wrapper (prefer dist when available, then bun, then node+tsx)
|
||||
const wrapper = `#!/usr/bin/env node
|
||||
|
||||
const { spawn } = require('child_process');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
// Prefer bun if available, otherwise use node with loader
|
||||
// Prefer dist (pure Node) if available, otherwise try bun, then node+tsx
|
||||
const args = process.argv.slice(2);
|
||||
const cliPath = path.join(__dirname, 'src', 'entrypoints', 'cli.tsx');
|
||||
const distEntrypoint = path.join(__dirname, 'dist', 'entrypoints', 'cli.js');
|
||||
|
||||
// Try bun first
|
||||
// 1) Run compiled dist with Node if present (Windows-friendly, no bun/tsx needed)
|
||||
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();
|
||||
if (fs.existsSync(distEntrypoint)) {
|
||||
const child = spawn(process.execPath, [distEntrypoint, ...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', () => runWithBunOrTsx());
|
||||
return;
|
||||
}
|
||||
} catch (_) {
|
||||
// fallthrough to bun/tsx
|
||||
}
|
||||
|
||||
function runWithNode() {
|
||||
// Use local tsx installation
|
||||
const tsxPath = path.join(__dirname, 'node_modules', '.bin', 'tsx');
|
||||
// 2) Otherwise, try bun first, then fall back to node+tsx
|
||||
runWithBunOrTsx();
|
||||
|
||||
function runWithBunOrTsx() {
|
||||
// Try bun first
|
||||
try {
|
||||
const { execSync } = require('child_process');
|
||||
execSync('bun --version', { stdio: 'ignore' });
|
||||
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', () => runWithNodeTsx());
|
||||
return;
|
||||
} catch {
|
||||
// ignore and try tsx path
|
||||
}
|
||||
|
||||
runWithNodeTsx();
|
||||
}
|
||||
|
||||
function runWithNodeTsx() {
|
||||
// Use local tsx installation; if missing, try PATH-resolved tsx
|
||||
const binDir = path.join(__dirname, 'node_modules', '.bin')
|
||||
const tsxPath = process.platform === 'win32'
|
||||
? path.join(binDir, 'tsx.cmd')
|
||||
: path.join(binDir, 'tsx')
|
||||
|
||||
const runPathTsx = () => {
|
||||
const child2 = spawn('tsx', [cliPath, ...args], {
|
||||
stdio: 'inherit',
|
||||
shell: process.platform === 'win32',
|
||||
env: {
|
||||
...process.env,
|
||||
YOGA_WASM_PATH: path.join(__dirname, 'yoga.wasm'),
|
||||
TSX_TSCONFIG_PATH: process.platform === 'win32' ? 'noop' : undefined
|
||||
},
|
||||
})
|
||||
child2.on('error', () => {
|
||||
console.error('\\nError: tsx is required but not found.')
|
||||
console.error('Please install tsx globally: npm install -g tsx')
|
||||
process.exit(1)
|
||||
})
|
||||
child2.on('exit', (code2) => process.exit(code2 || 0))
|
||||
}
|
||||
|
||||
const child = spawn(tsxPath, [cliPath, ...args], {
|
||||
stdio: 'inherit',
|
||||
env: {
|
||||
...process.env,
|
||||
YOGA_WASM_PATH: path.join(__dirname, 'yoga.wasm')
|
||||
}
|
||||
});
|
||||
shell: process.platform === 'win32',
|
||||
env: {
|
||||
...process.env,
|
||||
YOGA_WASM_PATH: path.join(__dirname, 'yoga.wasm'),
|
||||
TSX_TSCONFIG_PATH: process.platform === 'win32' ? 'noop' : undefined
|
||||
},
|
||||
})
|
||||
|
||||
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));
|
||||
child.on('error', () => runPathTsx())
|
||||
child.on('exit', (code) => {
|
||||
if (code && code !== 0) return runPathTsx()
|
||||
process.exit(code || 0)
|
||||
})
|
||||
}
|
||||
`;
|
||||
|
||||
writeFileSync('cli.js', wrapper);
|
||||
chmodSync('cli.js', 0o755);
|
||||
|
||||
|
||||
// Create a slim dist/index.js that imports the real entrypoint
|
||||
const distIndex = `#!/usr/bin/env node
|
||||
import './entrypoints/cli.js';
|
||||
`;
|
||||
writeFileSync('dist/index.js', distIndex);
|
||||
chmodSync('dist/index.js', 0o755);
|
||||
// Create .npmrc
|
||||
const npmrc = `# Ensure tsx is installed
|
||||
auto-install-peers=true
|
||||
@ -100,4 +149,4 @@ if (import.meta.main) {
|
||||
build();
|
||||
}
|
||||
|
||||
export { 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');
|
||||
|
||||
104
scripts/publish-dev.js
Executable file
104
scripts/publish-dev.js
Executable file
@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const { execSync } = require('child_process');
|
||||
const { readFileSync, writeFileSync } = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
/**
|
||||
* 发布开发版本到 npm
|
||||
* 使用 -dev tag,版本号自动递增 dev 后缀
|
||||
*/
|
||||
async function publishDev() {
|
||||
try {
|
||||
console.log('🚀 Starting dev version publish process...\n');
|
||||
|
||||
// 1. 确保在正确的分支
|
||||
const currentBranch = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8' }).trim();
|
||||
console.log(`📍 Current branch: ${currentBranch}`);
|
||||
|
||||
// 2. 检查工作区是否干净
|
||||
try {
|
||||
execSync('git diff --exit-code', { stdio: 'ignore' });
|
||||
execSync('git diff --cached --exit-code', { stdio: 'ignore' });
|
||||
} catch {
|
||||
console.log('⚠️ Working directory has uncommitted changes, committing...');
|
||||
execSync('git add .');
|
||||
execSync('git commit -m "chore: prepare dev release"');
|
||||
}
|
||||
|
||||
// 3. 读取当前版本
|
||||
const packagePath = path.join(process.cwd(), 'package.json');
|
||||
const packageJson = JSON.parse(readFileSync(packagePath, 'utf8'));
|
||||
const baseVersion = packageJson.version;
|
||||
|
||||
// 4. 生成开发版本号
|
||||
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'`);
|
||||
|
||||
// 5. 临时更新 package.json 版本号
|
||||
const originalPackageJson = { ...packageJson };
|
||||
packageJson.version = devVersion;
|
||||
writeFileSync(packagePath, JSON.stringify(packageJson, null, 2));
|
||||
|
||||
// 6. 构建项目
|
||||
console.log('🔨 Building project...');
|
||||
execSync('npm run build', { stdio: 'inherit' });
|
||||
|
||||
// 7. 运行预发布检查
|
||||
console.log('🔍 Running pre-publish checks...');
|
||||
execSync('node scripts/prepublish-check.js', { stdio: 'inherit' });
|
||||
|
||||
// 8. 发布到 npm 的 dev tag
|
||||
console.log('📤 Publishing to npm...');
|
||||
execSync(`npm publish --tag dev --access public`, { stdio: 'inherit' });
|
||||
|
||||
// 9. 恢复原始 package.json
|
||||
writeFileSync(packagePath, JSON.stringify(originalPackageJson, null, 2));
|
||||
|
||||
// 10. 创建 git tag
|
||||
console.log('🏷️ Creating git tag...');
|
||||
execSync(`git tag -a v${devVersion} -m "Dev release ${devVersion}"`);
|
||||
execSync(`git push origin v${devVersion}`);
|
||||
|
||||
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}`);
|
||||
|
||||
} 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();
|
||||
159
scripts/publish-release.js
Executable file
159
scripts/publish-release.js
Executable file
@ -0,0 +1,159 @@
|
||||
#!/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,支持语义化版本升级
|
||||
*/
|
||||
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 currentBranch = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8' }).trim();
|
||||
if (currentBranch !== 'main' && currentBranch !== 'master') {
|
||||
console.log('⚠️ Not on main/master branch. Current branch:', currentBranch);
|
||||
const proceed = await question('Continue anyway? (y/N): ');
|
||||
if (proceed.toLowerCase() !== 'y') {
|
||||
console.log('❌ Cancelled');
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 检查工作区是否干净
|
||||
try {
|
||||
execSync('git diff --exit-code', { stdio: 'ignore' });
|
||||
execSync('git diff --cached --exit-code', { stdio: 'ignore' });
|
||||
console.log('✅ Working directory is clean');
|
||||
} catch {
|
||||
console.log('❌ Working directory has uncommitted changes');
|
||||
console.log('Please commit or stash your changes before releasing');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 3. 拉取最新代码
|
||||
console.log('📡 Pulling latest changes...');
|
||||
execSync('git pull origin ' + currentBranch, { stdio: 'inherit' });
|
||||
|
||||
// 4. 读取当前版本
|
||||
const packagePath = path.join(process.cwd(), 'package.json');
|
||||
const packageJson = JSON.parse(readFileSync(packagePath, 'utf8'));
|
||||
const currentVersion = packageJson.version;
|
||||
|
||||
console.log(`📦 Current version: ${currentVersion}`);
|
||||
|
||||
// 5. 选择版本升级类型
|
||||
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);
|
||||
}
|
||||
|
||||
// 6. 确认发布
|
||||
console.log(`\n📋 Release Summary:`);
|
||||
console.log(` Current: ${currentVersion}`);
|
||||
console.log(` New: ${newVersion}`);
|
||||
console.log(` Branch: ${currentBranch}`);
|
||||
console.log(` Tag: latest`);
|
||||
|
||||
const confirm = await question('\n🤔 Proceed with release? (y/N): ');
|
||||
if (confirm.toLowerCase() !== 'y') {
|
||||
console.log('❌ Cancelled');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// 7. 更新版本号
|
||||
console.log('📝 Updating version...');
|
||||
packageJson.version = newVersion;
|
||||
writeFileSync(packagePath, JSON.stringify(packageJson, null, 2));
|
||||
|
||||
// 8. 运行测试
|
||||
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...');
|
||||
packageJson.version = currentVersion;
|
||||
writeFileSync(packagePath, JSON.stringify(packageJson, null, 2));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 9. 构建项目
|
||||
console.log('🔨 Building project...');
|
||||
execSync('npm run build', { stdio: 'inherit' });
|
||||
|
||||
// 10. 运行预发布检查
|
||||
console.log('🔍 Running pre-publish checks...');
|
||||
execSync('node scripts/prepublish-check.js', { stdio: 'inherit' });
|
||||
|
||||
// 11. 提交版本更新
|
||||
console.log('📝 Committing version update...');
|
||||
execSync('git add package.json');
|
||||
execSync(`git commit -m "chore: bump version to ${newVersion}"`);
|
||||
|
||||
// 12. 创建 git tag
|
||||
console.log('🏷️ Creating git tag...');
|
||||
execSync(`git tag -a v${newVersion} -m "Release ${newVersion}"`);
|
||||
|
||||
// 13. 发布到 npm
|
||||
console.log('📤 Publishing to npm...');
|
||||
execSync('npm publish --access public', { stdio: 'inherit' });
|
||||
|
||||
// 14. 推送到 git
|
||||
console.log('📡 Pushing to git...');
|
||||
execSync(`git push origin ${currentBranch}`);
|
||||
execSync(`git push origin v${newVersion}`);
|
||||
|
||||
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`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Production release failed:', error.message);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
rl.close();
|
||||
}
|
||||
}
|
||||
|
||||
publishRelease();
|
||||
@ -64,18 +64,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
|
||||
>
|
||||
|
||||
@ -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')
|
||||
}
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -7,7 +7,7 @@ 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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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,
|
||||
}),
|
||||
|
||||
@ -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}>
|
||||
|
||||
@ -13,9 +13,13 @@ export const MIN_LOGO_WIDTH = 50
|
||||
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 +39,36 @@ 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}</Text>
|
||||
<Text>Run the following command to update:</Text>
|
||||
<Text>
|
||||
{' '}
|
||||
{updateBannerCommands?.[0] ?? 'bun add -g @shareai-lab/kode@latest'}
|
||||
</Text>
|
||||
<Text>Or:</Text>
|
||||
<Text>
|
||||
{' '}
|
||||
{updateBannerCommands?.[1] ?? 'npm install -g @shareai-lab/kode@latest'}
|
||||
</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 /> */}
|
||||
|
||||
@ -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>{' '}
|
||||
→{' '}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -596,7 +596,7 @@ function PromptInput({
|
||||
mode === 'bash'
|
||||
? theme.bashBorder
|
||||
: mode === 'koding'
|
||||
? theme.koding
|
||||
? theme.noting
|
||||
: theme.secondaryBorder
|
||||
}
|
||||
borderDimColor
|
||||
@ -614,7 +614,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 +668,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
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -62,7 +62,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 +89,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}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
import { version } from '../../package.json'
|
||||
import { createRequire } from 'module'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const pkg = require('../../package.json')
|
||||
|
||||
export const MACRO = {
|
||||
VERSION: version,
|
||||
VERSION: pkg.version,
|
||||
README_URL: 'https://docs.anthropic.com/s/claude-code',
|
||||
PACKAGE_URL: '@shareai-lab/kode',
|
||||
ISSUES_EXPLAINER: 'report the issue at https://github.com/shareAI-lab/kode/issues',
|
||||
|
||||
@ -1,8 +1,30 @@
|
||||
#!/usr/bin/env -S node --no-warnings=ExperimentalWarning --enable-source-maps
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { initSentry } from '../services/sentry'
|
||||
import { PRODUCT_COMMAND, PRODUCT_NAME } from '../constants/product'
|
||||
initSentry() // Initialize Sentry as early as possible
|
||||
|
||||
// Ensure YOGA_WASM_PATH is set for Ink across run modes (wrapper/dev)
|
||||
// Resolve yoga.wasm relative to this file when missing using ESM-friendly APIs
|
||||
try {
|
||||
if (!process.env.YOGA_WASM_PATH) {
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = dirname(__filename)
|
||||
const devCandidate = join(__dirname, '../../yoga.wasm')
|
||||
const distCandidate = join(__dirname, './yoga.wasm')
|
||||
const resolved = existsSync(distCandidate)
|
||||
? distCandidate
|
||||
: existsSync(devCandidate)
|
||||
? devCandidate
|
||||
: undefined
|
||||
if (resolved) {
|
||||
process.env.YOGA_WASM_PATH = resolved
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// XXX: Without this line (and the Object.keys, even though it seems like it does nothing!),
|
||||
// there is a bug in Bun only on Win32 that causes this import to be removed, even though
|
||||
// its use is solely because of its side-effects.
|
||||
@ -11,9 +33,9 @@ Object.keys(dontcare)
|
||||
|
||||
import React from 'react'
|
||||
import { ReadStream } from 'tty'
|
||||
import { openSync, existsSync } from 'fs'
|
||||
import { render, RenderOptions } from 'ink'
|
||||
import { REPL } from '../screens/REPL'
|
||||
import { openSync } from 'fs'
|
||||
// ink and REPL are imported lazily to avoid top-level awaits during module init
|
||||
import type { RenderOptions } from 'ink'
|
||||
import { addToHistory } from '../history'
|
||||
import { getContext, setContext, removeContext } from '../context'
|
||||
import { Command } from '@commander-js/extra-typings'
|
||||
@ -74,8 +96,11 @@ import {
|
||||
getLatestVersion,
|
||||
installGlobalPackage,
|
||||
assertMinVersion,
|
||||
getUpdateCommandSuggestions,
|
||||
} from '../utils/autoUpdater'
|
||||
import { gt } from 'semver'
|
||||
import { CACHE_PATHS } from '../utils/log'
|
||||
// import { checkAndNotifyUpdate } from '../utils/autoUpdater'
|
||||
import { PersistentShell } from '../utils/PersistentShell'
|
||||
import { GATE_USE_EXTERNAL_UPDATER } from '../constants/betas'
|
||||
import { clearTerminal } from '../utils/terminal'
|
||||
@ -106,6 +131,7 @@ async function showSetupScreens(
|
||||
!config.hasCompletedOnboarding // always show onboarding at least once
|
||||
) {
|
||||
await clearTerminal()
|
||||
const { render } = await import('ink')
|
||||
await new Promise<void>(resolve => {
|
||||
render(
|
||||
<Onboarding
|
||||
@ -155,9 +181,12 @@ async function showSetupScreens(
|
||||
grantReadPermissionForOriginalDir()
|
||||
resolve()
|
||||
}
|
||||
render(<TrustDialog onDone={onDone} />, {
|
||||
exitOnCtrlC: false,
|
||||
})
|
||||
;(async () => {
|
||||
const { render } = await import('ink')
|
||||
render(<TrustDialog onDone={onDone} />, {
|
||||
exitOnCtrlC: false,
|
||||
})
|
||||
})()
|
||||
})
|
||||
}
|
||||
|
||||
@ -187,7 +216,14 @@ async function setup(cwd: string, safeMode?: boolean): Promise<void> {
|
||||
grantReadPermissionForOriginalDir()
|
||||
|
||||
// Start watching agent configuration files for changes
|
||||
const { startAgentWatcher, clearAgentCache } = await import('../utils/agentLoader')
|
||||
// Try ESM-friendly path first (compiled dist), then fall back to extensionless (dev/tsx)
|
||||
let agentLoader: any
|
||||
try {
|
||||
agentLoader = await import('../utils/agentLoader.js')
|
||||
} catch {
|
||||
agentLoader = await import('../utils/agentLoader')
|
||||
}
|
||||
const { startAgentWatcher, clearAgentCache } = agentLoader
|
||||
await startAgentWatcher(() => {
|
||||
// Cache is already cleared in the watcher, just log
|
||||
console.log('✅ Agent configurations hot-reloaded')
|
||||
@ -259,7 +295,10 @@ async function setup(cwd: string, safeMode?: boolean): Promise<void> {
|
||||
if (autoUpdaterStatus === 'not_configured') {
|
||||
logEvent('tengu_setup_auto_updater_not_configured', {})
|
||||
await new Promise<void>(resolve => {
|
||||
render(<Doctor onDone={() => resolve()} />)
|
||||
;(async () => {
|
||||
const { render } = await import('ink')
|
||||
render(<Doctor onDone={() => resolve()} />)
|
||||
})()
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -290,10 +329,12 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
// Disabled background notifier to avoid mid-screen logs during REPL
|
||||
|
||||
let inputPrompt = ''
|
||||
let renderContext: RenderOptions | undefined = {
|
||||
exitOnCtrlC: false,
|
||||
// @ts-expect-error - onFlicker not in RenderOptions interface
|
||||
|
||||
onFlicker() {
|
||||
logEvent('tengu_flicker', {})
|
||||
},
|
||||
@ -417,8 +458,23 @@ ${commandList}`,
|
||||
} else {
|
||||
const isDefaultModel = await isDefaultSlowAndCapableModel()
|
||||
|
||||
render(
|
||||
<REPL
|
||||
// Prefetch update info before first render to place banner at top
|
||||
const updateInfo = await (async () => {
|
||||
try {
|
||||
const latest = await getLatestVersion()
|
||||
if (latest && gt(latest, MACRO.VERSION)) {
|
||||
const cmds = await getUpdateCommandSuggestions()
|
||||
return { version: latest as string, commands: cmds as string[] }
|
||||
}
|
||||
} catch {}
|
||||
return { version: null as string | null, commands: null as string[] | null }
|
||||
})()
|
||||
|
||||
{
|
||||
const { render } = await import('ink')
|
||||
const { REPL } = await import('../screens/REPL')
|
||||
render(
|
||||
<REPL
|
||||
commands={commands}
|
||||
debug={debug}
|
||||
initialPrompt={inputPrompt}
|
||||
@ -429,9 +485,12 @@ ${commandList}`,
|
||||
safeMode={safe}
|
||||
mcpClients={mcpClients}
|
||||
isDefaultModel={isDefaultModel}
|
||||
initialUpdateVersion={updateInfo.version}
|
||||
initialUpdateCommands={updateInfo.commands}
|
||||
/>,
|
||||
renderContext,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
@ -504,7 +563,7 @@ ${commandList}`,
|
||||
.action(async ({ cwd, global }) => {
|
||||
await setup(cwd, false)
|
||||
console.log(
|
||||
JSON.stringify(listConfigForCLI(global ? (true as const) : (false as const)), null, 2),
|
||||
JSON.stringify(global ? listConfigForCLI(true) : listConfigForCLI(false), null, 2),
|
||||
)
|
||||
process.exit(0)
|
||||
})
|
||||
@ -1003,9 +1062,7 @@ ${commandList}`,
|
||||
function ClaudeDesktopImport() {
|
||||
const { useState } = reactModule
|
||||
const [isFinished, setIsFinished] = useState(false)
|
||||
const [importResults, setImportResults] = useState<
|
||||
{ name: string; success: boolean }[]
|
||||
>([])
|
||||
const [importResults, setImportResults] = useState([] as { name: string; success: boolean }[])
|
||||
const [isImporting, setIsImporting] = useState(false)
|
||||
const theme = getTheme()
|
||||
|
||||
@ -1098,11 +1155,11 @@ ${commandList}`,
|
||||
<Box
|
||||
flexDirection="column"
|
||||
borderStyle="round"
|
||||
borderColor={theme.claude}
|
||||
borderColor={theme.kode}
|
||||
padding={1}
|
||||
width={'100%'}
|
||||
>
|
||||
<Text bold color={theme.claude}>
|
||||
<Text bold color={theme.kode}>
|
||||
Import MCP Servers from Claude Desktop
|
||||
</Text>
|
||||
|
||||
@ -1209,7 +1266,10 @@ ${commandList}`,
|
||||
logEvent('tengu_doctor_command', {})
|
||||
|
||||
await new Promise<void>(resolve => {
|
||||
render(<Doctor onDone={() => resolve()} doctorMode={true} />)
|
||||
;(async () => {
|
||||
const { render } = await import('ink')
|
||||
render(<Doctor onDone={() => resolve()} doctorMode={true} />)
|
||||
})()
|
||||
})
|
||||
process.exit(0)
|
||||
})
|
||||
@ -1219,16 +1279,8 @@ ${commandList}`,
|
||||
// claude update
|
||||
program
|
||||
.command('update')
|
||||
.description('Check for updates and install if available')
|
||||
.description('Show manual upgrade commands (no auto-install)')
|
||||
.action(async () => {
|
||||
const useExternalUpdater = await checkGate(GATE_USE_EXTERNAL_UPDATER)
|
||||
if (useExternalUpdater) {
|
||||
// The external updater intercepts calls to "claude update", which means if we have received
|
||||
// this command at all, the extenral updater isn't installed on this machine.
|
||||
console.log(`This version of ${PRODUCT_NAME} is no longer supported.`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
logEvent('tengu_update_check', {})
|
||||
console.log(`Current version: ${MACRO.VERSION}`)
|
||||
console.log('Checking for updates...')
|
||||
@ -1246,30 +1298,12 @@ ${commandList}`,
|
||||
}
|
||||
|
||||
console.log(`New version available: ${latestVersion}`)
|
||||
console.log('Installing update...')
|
||||
|
||||
const status = await installGlobalPackage()
|
||||
|
||||
switch (status) {
|
||||
case 'success':
|
||||
console.log(`Successfully updated to version ${latestVersion}`)
|
||||
break
|
||||
case 'no_permissions':
|
||||
console.error('Error: Insufficient permissions to install update')
|
||||
console.error('Try running with sudo or fix npm permissions')
|
||||
process.exit(1)
|
||||
break
|
||||
case 'install_failed':
|
||||
console.error('Error: Failed to install update')
|
||||
process.exit(1)
|
||||
break
|
||||
case 'in_progress':
|
||||
console.error(
|
||||
'Error: Another instance is currently performing an update',
|
||||
)
|
||||
console.error('Please wait and try again later')
|
||||
process.exit(1)
|
||||
break
|
||||
const { getUpdateCommandSuggestions } = await import('../utils/autoUpdater')
|
||||
const cmds = await getUpdateCommandSuggestions()
|
||||
console.log('\nRun one of the following commands to update:')
|
||||
for (const c of cmds) console.log(` ${c}`)
|
||||
if (process.platform !== 'win32') {
|
||||
console.log('\nNote: you may need to prefix with "sudo" on macOS/Linux.')
|
||||
}
|
||||
process.exit(0)
|
||||
})
|
||||
@ -1288,11 +1322,14 @@ ${commandList}`,
|
||||
await setup(cwd, false)
|
||||
logEvent('tengu_view_logs', { number: number?.toString() ?? '' })
|
||||
const context: { unmount?: () => void } = {}
|
||||
const { unmount } = render(
|
||||
<LogList context={context} type="messages" logNumber={number} />,
|
||||
renderContextWithExitOnCtrlC,
|
||||
)
|
||||
context.unmount = unmount
|
||||
;(async () => {
|
||||
const { render } = await import('ink')
|
||||
const { unmount } = render(
|
||||
<LogList context={context} type="messages" logNumber={number} />,
|
||||
renderContextWithExitOnCtrlC,
|
||||
)
|
||||
context.unmount = unmount
|
||||
})()
|
||||
})
|
||||
|
||||
// claude resume
|
||||
@ -1357,8 +1394,11 @@ ${commandList}`,
|
||||
}
|
||||
const fork = getNextAvailableLogForkNumber(date, forkNumber ?? 1, 0)
|
||||
const isDefaultModel = await isDefaultSlowAndCapableModel()
|
||||
render(
|
||||
<REPL
|
||||
{
|
||||
const { render } = await import('ink')
|
||||
const { REPL } = await import('../screens/REPL')
|
||||
render(
|
||||
<REPL
|
||||
initialPrompt=""
|
||||
messageLogName={date}
|
||||
initialForkNumber={fork}
|
||||
@ -1372,7 +1412,8 @@ ${commandList}`,
|
||||
isDefaultModel={isDefaultModel}
|
||||
/>,
|
||||
{ exitOnCtrlC: false },
|
||||
)
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
logError(`Failed to load conversation: ${error}`)
|
||||
process.exit(1)
|
||||
@ -1380,17 +1421,20 @@ ${commandList}`,
|
||||
} else {
|
||||
// Show the conversation selector UI
|
||||
const context: { unmount?: () => void } = {}
|
||||
const { unmount } = render(
|
||||
<ResumeConversation
|
||||
context={context}
|
||||
commands={commands}
|
||||
logs={logs}
|
||||
tools={tools}
|
||||
verbose={verbose}
|
||||
/>,
|
||||
renderContextWithExitOnCtrlC,
|
||||
)
|
||||
context.unmount = unmount
|
||||
;(async () => {
|
||||
const { render } = await import('ink')
|
||||
const { unmount } = render(
|
||||
<ResumeConversation
|
||||
context={context}
|
||||
commands={commands}
|
||||
logs={logs}
|
||||
tools={tools}
|
||||
verbose={verbose}
|
||||
/>,
|
||||
renderContextWithExitOnCtrlC,
|
||||
)
|
||||
context.unmount = unmount
|
||||
})()
|
||||
}
|
||||
})
|
||||
|
||||
@ -1410,11 +1454,14 @@ ${commandList}`,
|
||||
await setup(cwd, false)
|
||||
logEvent('tengu_view_errors', { number: number?.toString() ?? '' })
|
||||
const context: { unmount?: () => void } = {}
|
||||
const { unmount } = render(
|
||||
<LogList context={context} type="errors" logNumber={number} />,
|
||||
renderContextWithExitOnCtrlC,
|
||||
)
|
||||
context.unmount = unmount
|
||||
;(async () => {
|
||||
const { render } = await import('ink')
|
||||
const { unmount } = render(
|
||||
<LogList context={context} type="errors" logNumber={number} />,
|
||||
renderContextWithExitOnCtrlC,
|
||||
)
|
||||
context.unmount = unmount
|
||||
})()
|
||||
})
|
||||
|
||||
// claude context (TODO: deprecate)
|
||||
@ -1501,9 +1548,23 @@ process.on('exit', () => {
|
||||
PersistentShell.getInstance().close()
|
||||
})
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
console.log('SIGINT')
|
||||
process.exit(0)
|
||||
function gracefulExit(code = 0) {
|
||||
try { resetCursor() } catch {}
|
||||
try { PersistentShell.getInstance().close() } catch {}
|
||||
process.exit(code)
|
||||
}
|
||||
|
||||
process.on('SIGINT', () => gracefulExit(0))
|
||||
process.on('SIGTERM', () => gracefulExit(0))
|
||||
// Windows CTRL+BREAK
|
||||
process.on('SIGBREAK', () => gracefulExit(0))
|
||||
process.on('unhandledRejection', err => {
|
||||
console.error('Unhandled rejection:', err)
|
||||
gracefulExit(1)
|
||||
})
|
||||
process.on('uncaughtException', err => {
|
||||
console.error('Uncaught exception:', err)
|
||||
gracefulExit(1)
|
||||
})
|
||||
|
||||
function resetCursor() {
|
||||
|
||||
@ -67,7 +67,7 @@ export async function startMCPServer(cwd: string): Promise<void> {
|
||||
const tools = await Promise.all(
|
||||
MCP_TOOLS.map(async tool => ({
|
||||
...tool,
|
||||
description: await tool.description(z.object({})),
|
||||
description: await tool.description(),
|
||||
inputSchema: zodToJsonSchema(tool.inputSchema) as ToolInput,
|
||||
})),
|
||||
)
|
||||
@ -127,7 +127,6 @@ export async function startMCPServer(cwd: string): Promise<void> {
|
||||
},
|
||||
readFileTimestamps: state.readFileTimestamps,
|
||||
},
|
||||
hasPermissionsToUseTool,
|
||||
)
|
||||
|
||||
const finalResult = await lastX(result)
|
||||
|
||||
@ -30,7 +30,6 @@ export function useDoublePress(
|
||||
} else {
|
||||
onFirstPress?.()
|
||||
setPending(true)
|
||||
// @ts-expect-error: Bun is overloading types here, but we're using the NodeJS runtime
|
||||
timeoutRef.current = setTimeout(
|
||||
() => setPending(false),
|
||||
DOUBLE_PRESS_TIMEOUT_MS,
|
||||
|
||||
@ -140,7 +140,6 @@ export function useTextInput({
|
||||
onMessage?.(true, CLIPBOARD_ERROR_MESSAGE)
|
||||
maybeClearImagePasteErrorTimeout()
|
||||
setImagePasteErrorTimeout(
|
||||
// @ts-expect-error: Bun is overloading types here, but we're using the NodeJS runtime
|
||||
setTimeout(() => {
|
||||
onMessage?.(false)
|
||||
}, 4000),
|
||||
@ -263,15 +262,15 @@ export function useTextInput({
|
||||
switch (true) {
|
||||
case key.escape:
|
||||
return handleEscape
|
||||
case key.leftArrow && (key.ctrl || key.meta || key.fn):
|
||||
case key.leftArrow && (key.ctrl || key.meta || ('fn' in key && key.fn)):
|
||||
return () => cursor.prevWord()
|
||||
case key.rightArrow && (key.ctrl || key.meta || key.fn):
|
||||
case key.rightArrow && (key.ctrl || key.meta || ('fn' in key && key.fn)):
|
||||
return () => cursor.nextWord()
|
||||
case key.ctrl:
|
||||
return handleCtrl
|
||||
case key.home:
|
||||
case 'home' in key && key.home:
|
||||
return () => cursor.startOfLine()
|
||||
case key.end:
|
||||
case 'end' in key && key.end:
|
||||
return () => cursor.endOfLine()
|
||||
case key.pageDown:
|
||||
return () => cursor.endOfLine()
|
||||
|
||||
@ -1067,7 +1067,7 @@ export function useUnifiedCompletion({
|
||||
})
|
||||
|
||||
// Handle navigation keys - simplified and unified
|
||||
useInput((_, key) => {
|
||||
useInput((inputChar, key) => {
|
||||
// Enter key - confirm selection and end completion (always add space)
|
||||
if (key.return && state.isActive && state.suggestions.length > 0) {
|
||||
const selectedSuggestion = state.suggestions[state.selectedIndex]
|
||||
@ -1148,7 +1148,7 @@ export function useUnifiedCompletion({
|
||||
}
|
||||
|
||||
// Space key - complete and potentially continue for directories
|
||||
if (key.space && state.isActive && state.suggestions.length > 0) {
|
||||
if (inputChar === ' ' && state.isActive && state.suggestions.length > 0) {
|
||||
const selectedSuggestion = state.suggestions[state.selectedIndex]
|
||||
const isDirectory = selectedSuggestion.value.endsWith('/')
|
||||
|
||||
|
||||
34
src/index.ts
Normal file
34
src/index.ts
Normal file
@ -0,0 +1,34 @@
|
||||
// Unified CLI entry (lightweight)
|
||||
// - Development: use `bun run src/entrypoints/cli.tsx`
|
||||
// - Production: transpiled to `dist/index.js` and used as bin/main
|
||||
|
||||
import { createRequire } from 'module'
|
||||
const require = createRequire(import.meta.url)
|
||||
|
||||
function hasFlag(...flags: string[]): boolean {
|
||||
return process.argv.some(arg => flags.includes(arg))
|
||||
}
|
||||
|
||||
// Minimal pre-parse: handle version/help early without loading heavy UI modules
|
||||
if (hasFlag('--version', '-v')) {
|
||||
try {
|
||||
const pkg = require('../package.json')
|
||||
console.log(pkg.version || '')
|
||||
} catch {
|
||||
console.log('')
|
||||
}
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
if (hasFlag('--help-lite')) {
|
||||
console.log(`Usage: kode [options] [command] [prompt]\n\n` +
|
||||
`Common options:\n` +
|
||||
` -h, --help Show full help\n` +
|
||||
` -v, --version Show version\n` +
|
||||
` -p, --print Print response and exit (non-interactive)\n` +
|
||||
` -c, --cwd <cwd> Set working directory`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
// For compatibility, --help loads full CLI help
|
||||
await import('./entrypoints/cli.js')
|
||||
21
src/query.ts
21
src/query.ts
@ -3,7 +3,7 @@ import {
|
||||
MessageParam,
|
||||
ToolUseBlock,
|
||||
} from '@anthropic-ai/sdk/resources/index.mjs'
|
||||
import { UUID } from 'crypto'
|
||||
import type { UUID } from './types/common'
|
||||
import type { Tool, ToolUseContext } from './Tool'
|
||||
import {
|
||||
messagePairValidForBinaryFeedback,
|
||||
@ -70,6 +70,9 @@ export type UserMessage = {
|
||||
options?: {
|
||||
isKodingRequest?: boolean
|
||||
kodingContext?: string
|
||||
isCustomCommand?: boolean
|
||||
commandName?: string
|
||||
commandArgs?: string
|
||||
}
|
||||
}
|
||||
|
||||
@ -196,8 +199,9 @@ export async function* query(
|
||||
if (reminders && messages.length > 0) {
|
||||
// Find the last user message
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
if (messages[i]?.type === 'user') {
|
||||
const lastUserMessage = messages[i]
|
||||
const msg = messages[i]
|
||||
if (msg?.type === 'user') {
|
||||
const lastUserMessage = msg as UserMessage
|
||||
messages[i] = {
|
||||
...lastUserMessage,
|
||||
message: {
|
||||
@ -637,7 +641,7 @@ async function* checkPermissionsAndCallTool(
|
||||
|
||||
// Call the tool
|
||||
try {
|
||||
const generator = tool.call(normalizedInput as never, context, canUseTool)
|
||||
const generator = tool.call(normalizedInput as never, context)
|
||||
for await (const result of generator) {
|
||||
switch (result.type) {
|
||||
case 'result':
|
||||
@ -649,13 +653,13 @@ async function* checkPermissionsAndCallTool(
|
||||
[
|
||||
{
|
||||
type: 'tool_result',
|
||||
content: result.resultForAssistant,
|
||||
content: result.resultForAssistant || String(result.data),
|
||||
tool_use_id: toolUseID,
|
||||
},
|
||||
],
|
||||
{
|
||||
data: result.data,
|
||||
resultForAssistant: result.resultForAssistant,
|
||||
resultForAssistant: result.resultForAssistant || String(result.data),
|
||||
},
|
||||
)
|
||||
return
|
||||
@ -668,9 +672,10 @@ async function* checkPermissionsAndCallTool(
|
||||
toolUseID,
|
||||
siblingToolUseIDs,
|
||||
result.content,
|
||||
result.normalizedMessages,
|
||||
result.tools,
|
||||
result.normalizedMessages || [],
|
||||
result.tools || [],
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { Box, Text, useInput } from 'ink'
|
||||
import { Select } from '../components/CustomSelect/select'
|
||||
import { getTheme } from '../utils/theme'
|
||||
import { ConfigureNpmPrefix } from './ConfigureNpmPrefix.tsx'
|
||||
import { ConfigureNpmPrefix } from './ConfigureNpmPrefix'
|
||||
import { platform } from 'process'
|
||||
import {
|
||||
checkNpmPermissions,
|
||||
|
||||
@ -44,6 +44,7 @@ import type { WrappedClient } from '../services/mcpClient'
|
||||
import type { Tool } from '../Tool'
|
||||
import { AutoUpdaterResult } from '../utils/autoUpdater'
|
||||
import { getGlobalConfig, saveGlobalConfig } from '../utils/config'
|
||||
import { MACRO } from '../constants/macros'
|
||||
import { logEvent } from '../services/statsig'
|
||||
import { getNextAvailableLogForkNumber } from '../utils/log'
|
||||
import {
|
||||
@ -87,6 +88,9 @@ type Props = {
|
||||
mcpClients?: WrappedClient[]
|
||||
// Flag to indicate if current model is default
|
||||
isDefaultModel?: boolean
|
||||
// Update banner info passed from CLI before first render
|
||||
initialUpdateVersion?: string | null
|
||||
initialUpdateCommands?: string[] | null
|
||||
}
|
||||
|
||||
export type BinaryFeedbackContext = {
|
||||
@ -108,6 +112,8 @@ export function REPL({
|
||||
initialMessages,
|
||||
mcpClients = [],
|
||||
isDefaultModel = true,
|
||||
initialUpdateVersion,
|
||||
initialUpdateCommands,
|
||||
}: Props): React.ReactNode {
|
||||
// TODO: probably shouldn't re-read config from file synchronously on every keystroke
|
||||
const verbose = verboseFromCLI ?? getGlobalConfig().verbose
|
||||
@ -149,6 +155,10 @@ export function REPL({
|
||||
|
||||
const [binaryFeedbackContext, setBinaryFeedbackContext] =
|
||||
useState<BinaryFeedbackContext | null>(null)
|
||||
// New version banner: passed in from CLI to guarantee top placement
|
||||
const updateAvailableVersion = initialUpdateVersion ?? null
|
||||
const updateCommands = initialUpdateCommands ?? null
|
||||
// No separate Static for banner; it renders inside Logo
|
||||
|
||||
const getBinaryFeedbackResponse = useCallback(
|
||||
(
|
||||
@ -209,6 +219,8 @@ export function REPL({
|
||||
}
|
||||
}, [messages, showCostDialog, haveShownCostDialog])
|
||||
|
||||
// Update banner is provided by CLI at startup; no async check here.
|
||||
|
||||
const canUseTool = useCanUseTool(setToolUseConfirm)
|
||||
|
||||
async function onInit() {
|
||||
@ -478,7 +490,12 @@ export function REPL({
|
||||
type: 'static',
|
||||
jsx: (
|
||||
<Box flexDirection="column" key={`logo${forkNumber}`}>
|
||||
<Logo mcpClients={mcpClients} isDefaultModel={isDefaultModel} />
|
||||
<Logo
|
||||
mcpClients={mcpClients}
|
||||
isDefaultModel={isDefaultModel}
|
||||
updateBannerVersion={updateAvailableVersion}
|
||||
updateBannerCommands={updateCommands}
|
||||
/>
|
||||
<ProjectOnboarding workspaceDir={getOriginalCwd()} />
|
||||
</Box>
|
||||
),
|
||||
@ -506,7 +523,7 @@ export function REPL({
|
||||
shouldShowDot={false}
|
||||
/>
|
||||
) : (
|
||||
<MessageResponse>
|
||||
<MessageResponse children={
|
||||
<Message
|
||||
message={_.content}
|
||||
messages={_.normalizedMessages}
|
||||
@ -524,7 +541,7 @@ export function REPL({
|
||||
shouldAnimate={false}
|
||||
shouldShowDot={false}
|
||||
/>
|
||||
</MessageResponse>
|
||||
} />
|
||||
)
|
||||
) : (
|
||||
<Message
|
||||
@ -601,14 +618,17 @@ export function REPL({
|
||||
const showingCostDialog = !isLoading && showCostDialog
|
||||
|
||||
return (
|
||||
<PermissionProvider isBypassPermissionsModeAvailable={!safeMode}>
|
||||
<ModeIndicator />
|
||||
<PermissionProvider
|
||||
isBypassPermissionsModeAvailable={!safeMode}
|
||||
children={
|
||||
<React.Fragment>
|
||||
{/* Update banner now renders inside Logo for stable placement */}
|
||||
<ModeIndicator />
|
||||
<React.Fragment key={`static-messages-${forkNumber}`}>
|
||||
<Static
|
||||
items={messagesJSX.filter(_ => _.type === 'static')}
|
||||
>
|
||||
{_ => _.jsx}
|
||||
</Static>
|
||||
children={_ => ((_ as any).jsx)}
|
||||
/>
|
||||
</React.Fragment>
|
||||
{messagesJSX.filter(_ => _.type === 'transient').map(_ => _.jsx)}
|
||||
<Box
|
||||
@ -749,7 +769,9 @@ export function REPL({
|
||||
)}
|
||||
{/** Fix occasional rendering artifact */}
|
||||
<Newline />
|
||||
</PermissionProvider>
|
||||
</React.Fragment>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -518,7 +518,7 @@ export async function getCompletionWithProfile(
|
||||
messageCount: opts.messages?.length || 0,
|
||||
streamMode: opts.stream,
|
||||
timestamp: new Date().toISOString(),
|
||||
modelProfileName: modelProfile?.modelName,
|
||||
modelProfileModelName: modelProfile?.modelName,
|
||||
modelProfileName: modelProfile?.name,
|
||||
})
|
||||
|
||||
@ -608,7 +608,13 @@ export async function getCompletionWithProfile(
|
||||
// 🔥 NEW: Parse error message to detect and handle specific API errors
|
||||
try {
|
||||
const errorData = await response.json()
|
||||
const errorMessage = errorData?.error?.message || errorData?.message || `HTTP ${response.status}`
|
||||
// Type guard for error data structure
|
||||
const hasError = (data: unknown): data is { error?: { message?: string }; message?: string } => {
|
||||
return typeof data === 'object' && data !== null
|
||||
}
|
||||
const errorMessage = hasError(errorData)
|
||||
? (errorData.error?.message || errorData.message || `HTTP ${response.status}`)
|
||||
: `HTTP ${response.status}`
|
||||
|
||||
// Check if this is a parameter error that we can fix
|
||||
const isGPT5 = opts.model.startsWith('gpt-5')
|
||||
@ -740,7 +746,13 @@ export async function getCompletionWithProfile(
|
||||
// 🔥 NEW: Parse error message to detect and handle specific API errors
|
||||
try {
|
||||
const errorData = await response.json()
|
||||
const errorMessage = errorData?.error?.message || errorData?.message || `HTTP ${response.status}`
|
||||
// Type guard for error data structure
|
||||
const hasError = (data: unknown): data is { error?: { message?: string }; message?: string } => {
|
||||
return typeof data === 'object' && data !== null
|
||||
}
|
||||
const errorMessage = hasError(errorData)
|
||||
? (errorData.error?.message || errorData.message || `HTTP ${response.status}`)
|
||||
: `HTTP ${response.status}`
|
||||
|
||||
// Check if this is a parameter error that we can fix
|
||||
const isGPT5 = opts.model.startsWith('gpt-5')
|
||||
@ -1285,16 +1297,25 @@ export async function fetchCustomModels(
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
// Type guards for different API response formats
|
||||
const hasDataArray = (obj: unknown): obj is { data: unknown[] } => {
|
||||
return typeof obj === 'object' && obj !== null && 'data' in obj && Array.isArray((obj as any).data)
|
||||
}
|
||||
|
||||
const hasModelsArray = (obj: unknown): obj is { models: unknown[] } => {
|
||||
return typeof obj === 'object' && obj !== null && 'models' in obj && Array.isArray((obj as any).models)
|
||||
}
|
||||
|
||||
// Validate response format and extract models array
|
||||
let models = []
|
||||
|
||||
if (data && data.data && Array.isArray(data.data)) {
|
||||
if (hasDataArray(data)) {
|
||||
// Standard OpenAI format: { data: [...] }
|
||||
models = data.data
|
||||
} else if (Array.isArray(data)) {
|
||||
// Direct array format
|
||||
models = data
|
||||
} else if (data && data.models && Array.isArray(data.models)) {
|
||||
} else if (hasModelsArray(data)) {
|
||||
// Alternative format: { models: [...] }
|
||||
models = data.models
|
||||
} else {
|
||||
|
||||
@ -107,7 +107,8 @@ export function logEvent(
|
||||
}
|
||||
|
||||
export const checkGate = memoize(async (gateName: string): Promise<boolean> => {
|
||||
return true
|
||||
// Default to disabled gates when Statsig is not active
|
||||
return false
|
||||
// if (env.isCI || process.env.NODE_ENV === 'test') {
|
||||
// return false
|
||||
// }
|
||||
@ -120,7 +121,7 @@ export const checkGate = memoize(async (gateName: string): Promise<boolean> => {
|
||||
})
|
||||
|
||||
export const useStatsigGate = (gateName: string, defaultValue = false) => {
|
||||
return true
|
||||
return false
|
||||
// const [gateValue, setGateValue] = React.useState(defaultValue)
|
||||
// React.useEffect(() => {
|
||||
// checkGate(gateName).then(setGateValue)
|
||||
|
||||
@ -57,7 +57,7 @@ export const ArchitectTool = {
|
||||
needsPermissions() {
|
||||
return false
|
||||
},
|
||||
async *call({ prompt, context }, toolUseContext, canUseTool) {
|
||||
async *call({ prompt, context }, toolUseContext) {
|
||||
const content = context
|
||||
? `<context>${context}</context>\n\n${prompt}`
|
||||
: prompt
|
||||
@ -67,10 +67,13 @@ export const ArchitectTool = {
|
||||
const messages: Message[] = [userMessage]
|
||||
|
||||
// We only allow the file exploration tools to be used in the architect tool
|
||||
const allowedTools = (toolUseContext.options.tools ?? []).filter(_ =>
|
||||
const allowedTools = (toolUseContext.options?.tools ?? []).filter(_ =>
|
||||
FS_EXPLORATION_TOOLS.map(_ => _.name).includes(_.name),
|
||||
)
|
||||
|
||||
// Create a dummy canUseTool function since this tool controls its own tool usage
|
||||
const canUseTool = async () => ({ result: true as const })
|
||||
|
||||
const lastResponse = await lastX(
|
||||
query(
|
||||
messages,
|
||||
@ -79,7 +82,17 @@ export const ArchitectTool = {
|
||||
canUseTool,
|
||||
{
|
||||
...toolUseContext,
|
||||
options: { ...toolUseContext.options, tools: allowedTools },
|
||||
setToolJSX: () => {}, // Dummy function since ArchitectTool doesn't use UI
|
||||
options: {
|
||||
commands: toolUseContext.options?.commands || [],
|
||||
forkNumber: toolUseContext.options?.forkNumber || 0,
|
||||
messageLogName: toolUseContext.options?.messageLogName || 'default',
|
||||
verbose: toolUseContext.options?.verbose || false,
|
||||
safeMode: toolUseContext.options?.safeMode || false,
|
||||
maxThinkingTokens: toolUseContext.options?.maxThinkingTokens || 0,
|
||||
...toolUseContext.options,
|
||||
tools: allowedTools
|
||||
},
|
||||
},
|
||||
),
|
||||
)
|
||||
@ -98,8 +111,8 @@ export const ArchitectTool = {
|
||||
async prompt() {
|
||||
return DESCRIPTION
|
||||
},
|
||||
renderResultForAssistant(data) {
|
||||
return data
|
||||
renderResultForAssistant(data: TextBlock[]): string {
|
||||
return data.map(block => block.text).join('\n')
|
||||
},
|
||||
renderToolUseMessage(input) {
|
||||
return Object.entries(input)
|
||||
|
||||
@ -146,7 +146,7 @@ Question: What are the most effective React optimization techniques for handling
|
||||
}
|
||||
} catch (e) {
|
||||
// If we can't determine current model, allow the request
|
||||
debugLogger('AskExpertModel', 'Could not determine current model:', e)
|
||||
debugLogger.error('AskExpertModel', { message: 'Could not determine current model', error: e })
|
||||
}
|
||||
|
||||
// Validate that the model exists and is available
|
||||
@ -169,7 +169,8 @@ Question: What are the most effective React optimization techniques for handling
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logError('Model validation error in AskExpertModelTool', error)
|
||||
console.error('Model validation error in AskExpertModelTool:', error)
|
||||
logError(error)
|
||||
return {
|
||||
result: false,
|
||||
message: `Failed to validate expert model '${expert_model}'. Please check your model configuration.`,
|
||||
@ -303,7 +304,8 @@ ${output.expertAnswer}`
|
||||
const session = createExpertChatSession(expertModel)
|
||||
sessionId = session.sessionId
|
||||
} catch (error) {
|
||||
logError('Failed to create new expert chat session', error)
|
||||
console.error('Failed to create new expert chat session:', error)
|
||||
logError(error)
|
||||
throw new Error('Failed to create new chat session')
|
||||
}
|
||||
} else {
|
||||
@ -316,16 +318,15 @@ ${output.expertAnswer}`
|
||||
sessionId = newSession.sessionId
|
||||
}
|
||||
} catch (error) {
|
||||
logError('Failed to load expert chat session', error)
|
||||
console.error('Failed to load expert chat session:', error)
|
||||
logError(error)
|
||||
// Fallback: create new session
|
||||
try {
|
||||
const newSession = createExpertChatSession(expertModel)
|
||||
sessionId = newSession.sessionId
|
||||
} catch (createError) {
|
||||
logError(
|
||||
'Failed to create fallback expert chat session',
|
||||
createError,
|
||||
)
|
||||
console.error('Failed to create fallback expert chat session:', createError)
|
||||
logError(createError)
|
||||
throw new Error('Unable to create or load chat session')
|
||||
}
|
||||
}
|
||||
@ -341,7 +342,8 @@ ${output.expertAnswer}`
|
||||
try {
|
||||
historyMessages = getSessionMessages(sessionId)
|
||||
} catch (error) {
|
||||
logError('Failed to load session messages', error)
|
||||
console.error('Failed to load session messages:', error)
|
||||
logError(error)
|
||||
historyMessages = [] // Fallback to empty history
|
||||
}
|
||||
|
||||
@ -355,7 +357,8 @@ ${output.expertAnswer}`
|
||||
: createAssistantMessage(msg.content),
|
||||
)
|
||||
} catch (error) {
|
||||
logError('Failed to create system messages', error)
|
||||
console.error('Failed to create system messages:', error)
|
||||
logError(error)
|
||||
throw new Error('Failed to prepare conversation messages')
|
||||
}
|
||||
|
||||
@ -414,7 +417,8 @@ ${output.expertAnswer}`
|
||||
timeoutPromise
|
||||
])
|
||||
} catch (error: any) {
|
||||
logError('Expert model query failed', error)
|
||||
console.error('Expert model query failed:', error)
|
||||
logError(error)
|
||||
|
||||
// Check for specific error types
|
||||
if (
|
||||
@ -496,7 +500,8 @@ ${output.expertAnswer}`
|
||||
throw new Error('Expert response was empty')
|
||||
}
|
||||
} catch (error) {
|
||||
logError('Failed to extract expert answer', error)
|
||||
console.error('Failed to extract expert answer:', error)
|
||||
logError(error)
|
||||
throw new Error('Failed to process expert response')
|
||||
}
|
||||
|
||||
@ -505,7 +510,8 @@ ${output.expertAnswer}`
|
||||
addMessageToSession(sessionId, 'user', question)
|
||||
addMessageToSession(sessionId, 'assistant', expertAnswer)
|
||||
} catch (error) {
|
||||
logError('Failed to save conversation to session', error)
|
||||
console.error('Failed to save conversation to session:', error)
|
||||
logError(error)
|
||||
// Don't throw here - we got a valid response, saving is non-critical
|
||||
}
|
||||
|
||||
@ -530,7 +536,8 @@ ${output.expertAnswer}`
|
||||
return yield* this.handleInterrupt()
|
||||
}
|
||||
|
||||
logError('AskExpertModelTool execution failed', error)
|
||||
console.error('AskExpertModelTool execution failed:', error)
|
||||
logError(error)
|
||||
|
||||
// Ensure we have a valid sessionId for error response
|
||||
const errorSessionId = sessionId || 'error-session'
|
||||
|
||||
@ -6,6 +6,7 @@ import * as React from 'react'
|
||||
import { z } from 'zod'
|
||||
import { FileEditToolUpdatedMessage } from '../../components/FileEditToolUpdatedMessage'
|
||||
import { StructuredDiff } from '../../components/StructuredDiff'
|
||||
import { FallbackToolUseRejectedMessage } from '../../components/FallbackToolUseRejectedMessage'
|
||||
import { logEvent } from '../../services/statsig'
|
||||
import { Tool, ValidationResult } from '../../Tool'
|
||||
import { intersperse } from '../../utils/array'
|
||||
@ -76,10 +77,13 @@ export const FileEditTool = {
|
||||
)
|
||||
},
|
||||
renderToolUseRejectedMessage(
|
||||
{ file_path, old_string, new_string },
|
||||
{ columns, verbose },
|
||||
{ file_path, old_string, new_string }: any = {},
|
||||
{ columns, verbose }: any = {},
|
||||
) {
|
||||
try {
|
||||
if (!file_path) {
|
||||
return <FallbackToolUseRejectedMessage />
|
||||
}
|
||||
const { patch } = applyEdit(file_path, old_string, new_string)
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
|
||||
@ -8,6 +8,7 @@ import { z } from 'zod'
|
||||
import { FileEditToolUpdatedMessage } from '../../components/FileEditToolUpdatedMessage'
|
||||
import { HighlightedCode } from '../../components/HighlightedCode'
|
||||
import { StructuredDiff } from '../../components/StructuredDiff'
|
||||
import { FallbackToolUseRejectedMessage } from '../../components/FallbackToolUseRejectedMessage'
|
||||
import { logEvent } from '../../services/statsig'
|
||||
import type { Tool } from '../../Tool'
|
||||
import { intersperse } from '../../utils/array'
|
||||
@ -67,8 +68,11 @@ export const FileWriteTool = {
|
||||
renderToolUseMessage(input, { verbose }) {
|
||||
return `file_path: ${verbose ? input.file_path : relative(getCwd(), input.file_path)}`
|
||||
},
|
||||
renderToolUseRejectedMessage({ file_path, content }, { columns, verbose }) {
|
||||
renderToolUseRejectedMessage({ file_path, content }: any = {}, { columns, verbose }: any = {}) {
|
||||
try {
|
||||
if (!file_path) {
|
||||
return <FallbackToolUseRejectedMessage />
|
||||
}
|
||||
const fullFilePath = isAbsolute(file_path)
|
||||
? file_path
|
||||
: resolve(getCwd(), file_path)
|
||||
@ -120,9 +124,9 @@ export const FileWriteTool = {
|
||||
}
|
||||
},
|
||||
renderToolResultMessage(
|
||||
{ filePath, content, structuredPatch, type },
|
||||
{ verbose },
|
||||
{ filePath, content, structuredPatch, type }
|
||||
) {
|
||||
const verbose = false // Default to false since verbose is no longer passed
|
||||
switch (type) {
|
||||
case 'create': {
|
||||
const contentWithFallback = content || '(No content)'
|
||||
|
||||
@ -19,7 +19,28 @@ import { logError } from '../../utils/log'
|
||||
import { getCwd } from '../../utils/state'
|
||||
import { getTheme } from '../../utils/theme'
|
||||
import { NotebookEditTool } from '../NotebookEditTool/NotebookEditTool'
|
||||
import { applyEdit } from '../FileEditTool/utils'
|
||||
// Local content-based edit function for MultiEditTool
|
||||
function applyContentEdit(
|
||||
content: string,
|
||||
oldString: string,
|
||||
newString: string,
|
||||
replaceAll: boolean = false
|
||||
): { newContent: string; occurrences: number } {
|
||||
if (replaceAll) {
|
||||
const regex = new RegExp(oldString.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')
|
||||
const matches = content.match(regex)
|
||||
const occurrences = matches ? matches.length : 0
|
||||
const newContent = content.replace(regex, newString)
|
||||
return { newContent, occurrences }
|
||||
} else {
|
||||
if (content.includes(oldString)) {
|
||||
const newContent = content.replace(oldString, newString)
|
||||
return { newContent, occurrences: 1 }
|
||||
} else {
|
||||
throw new Error(`String not found: ${oldString.substring(0, 50)}...`)
|
||||
}
|
||||
}
|
||||
}
|
||||
import { hasWritePermission } from '../../utils/permissions/filesystem'
|
||||
import { PROJECT_FILE } from '../../constants/product'
|
||||
import { DESCRIPTION, PROMPT } from './prompt'
|
||||
@ -274,7 +295,7 @@ export const MultiEditTool = {
|
||||
const { old_string, new_string, replace_all } = edit
|
||||
|
||||
try {
|
||||
const result = applyEdit(
|
||||
const result = applyContentEdit(
|
||||
modifiedContent,
|
||||
old_string,
|
||||
new_string,
|
||||
@ -302,8 +323,9 @@ export const MultiEditTool = {
|
||||
}
|
||||
|
||||
// Write the modified content
|
||||
const lineEndings = fileExists ? detectLineEndings(currentContent) : '\n'
|
||||
writeTextContent(filePath, modifiedContent, lineEndings)
|
||||
const lineEndings = fileExists ? detectLineEndings(currentContent) : 'LF'
|
||||
const encoding = fileExists ? detectFileEncoding(filePath) : 'utf8'
|
||||
writeTextContent(filePath, modifiedContent, encoding, lineEndings)
|
||||
|
||||
// Record Agent edit operation for file freshness tracking
|
||||
recordFileEdit(filePath, modifiedContent)
|
||||
|
||||
@ -176,7 +176,7 @@ function processOutput(output: NotebookCellOutput) {
|
||||
case 'display_data':
|
||||
return {
|
||||
output_type: output.output_type,
|
||||
text: processOutputText(output.data?.['text/plain']),
|
||||
text: processOutputText(output.data?.['text/plain'] as string | string[] | undefined),
|
||||
image: output.data && extractImage(output.data),
|
||||
}
|
||||
case 'error':
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { z } from 'zod'
|
||||
import React from 'react'
|
||||
import { Text } from 'ink'
|
||||
import { Tool, ToolUseContext } from '../../Tool'
|
||||
import { Tool, ToolUseContext, ExtendedToolUseContext } from '../../Tool'
|
||||
import { DESCRIPTION, PROMPT } from './prompt'
|
||||
import {
|
||||
StickerRequestForm,
|
||||
@ -38,8 +38,11 @@ export const StickerRequestTool: Tool = {
|
||||
resolveForm = success => resolve(success)
|
||||
})
|
||||
|
||||
context.setToolJSX?.({
|
||||
jsx: (
|
||||
// Check if setToolJSX is available (cast context if needed)
|
||||
const extendedContext = context as ExtendedToolUseContext
|
||||
if (extendedContext.setToolJSX) {
|
||||
extendedContext.setToolJSX({
|
||||
jsx: (
|
||||
<StickerRequestForm
|
||||
onSubmit={(formData: FormData) => {
|
||||
// Log successful completion with form data
|
||||
@ -48,18 +51,27 @@ export const StickerRequestTool: Tool = {
|
||||
has_optional_address: Boolean(formData.address2).toString(),
|
||||
})
|
||||
resolveForm(true)
|
||||
context.setToolJSX?.(null) // Clear the JSX
|
||||
if (extendedContext.setToolJSX) {
|
||||
extendedContext.setToolJSX(null) // Clear the JSX
|
||||
}
|
||||
}}
|
||||
onClose={() => {
|
||||
// Log form cancellation
|
||||
logEvent('sticker_request_form_cancelled', {})
|
||||
resolveForm(false)
|
||||
context.setToolJSX?.(null) // Clear the JSX
|
||||
if (extendedContext.setToolJSX) {
|
||||
extendedContext.setToolJSX(null) // Clear the JSX
|
||||
}
|
||||
}}
|
||||
/>
|
||||
),
|
||||
shouldHidePromptInput: true,
|
||||
})
|
||||
),
|
||||
shouldHidePromptInput: true,
|
||||
})
|
||||
} else {
|
||||
// Fallback if setToolJSX is not available
|
||||
console.log('Sticker form would be displayed here, but setToolJSX is not available')
|
||||
resolveForm(false)
|
||||
}
|
||||
|
||||
// Wait for form completion and get status
|
||||
const success = await formComplete
|
||||
@ -82,12 +94,14 @@ export const StickerRequestTool: Tool = {
|
||||
return ''
|
||||
},
|
||||
|
||||
renderToolUseRejectedMessage: _input => (
|
||||
<Text>
|
||||
⎿
|
||||
<Text color={getTheme().error}>No (Sticker request cancelled)</Text>
|
||||
</Text>
|
||||
),
|
||||
renderToolUseRejectedMessage() {
|
||||
return (
|
||||
<Text>
|
||||
⎿
|
||||
<Text color={getTheme().error}>No (Sticker request cancelled)</Text>
|
||||
</Text>
|
||||
)
|
||||
},
|
||||
|
||||
renderResultForAssistant: (content: string) => content,
|
||||
}
|
||||
|
||||
@ -72,7 +72,7 @@ export const TaskTool = {
|
||||
options: { safeMode = false, forkNumber, messageLogName, verbose },
|
||||
readFileTimestamps,
|
||||
},
|
||||
) {
|
||||
): AsyncGenerator<{ type: 'result'; data: TextBlock[]; resultForAssistant?: string }, void, unknown> {
|
||||
const startTime = Date.now()
|
||||
|
||||
// Default to general-purpose if no subagent_type specified
|
||||
@ -95,7 +95,7 @@ export const TaskTool = {
|
||||
|
||||
yield {
|
||||
type: 'result',
|
||||
data: { error: helpMessage },
|
||||
data: [{ type: 'text', text: helpMessage }] as TextBlock[],
|
||||
resultForAssistant: helpMessage,
|
||||
}
|
||||
return
|
||||
@ -135,14 +135,7 @@ export const TaskTool = {
|
||||
}
|
||||
}
|
||||
|
||||
// We yield an initial message immediately so the UI
|
||||
// doesn't move around when messages start streaming back.
|
||||
yield {
|
||||
type: 'progress',
|
||||
content: createAssistantMessage(chalk.dim(`[${agentType}] ${description}`)),
|
||||
normalizedMessages: normalizeMessages(messages),
|
||||
tools,
|
||||
}
|
||||
// Skip initial progress yield - only yield results for Tool interface
|
||||
|
||||
const [taskPrompt, context, maxThinkingTokens] = await Promise.all([
|
||||
getAgentPrompt(),
|
||||
@ -194,6 +187,7 @@ export const TaskTool = {
|
||||
messageId: getLastAssistantMessageId(messages),
|
||||
agentId: taskId,
|
||||
readFileTimestamps,
|
||||
setToolJSX: () => {}, // No-op implementation for TaskTool
|
||||
},
|
||||
)) {
|
||||
messages.push(message)
|
||||
@ -214,12 +208,7 @@ export const TaskTool = {
|
||||
if (content.type === 'text' && content.text && content.text !== INTERRUPT_MESSAGE) {
|
||||
// Show agent's reasoning/responses
|
||||
const preview = content.text.length > 200 ? content.text.substring(0, 200) + '...' : content.text
|
||||
yield {
|
||||
type: 'progress',
|
||||
content: createAssistantMessage(`[${agentType}] ${preview}`),
|
||||
normalizedMessages,
|
||||
tools,
|
||||
}
|
||||
// Skip progress yield - only yield results for Tool interface
|
||||
} else if (content.type === 'tool_use') {
|
||||
toolUseCount++
|
||||
|
||||
@ -250,12 +239,7 @@ export const TaskTool = {
|
||||
}
|
||||
}
|
||||
|
||||
yield {
|
||||
type: 'progress',
|
||||
content: modifiedMessage,
|
||||
normalizedMessages,
|
||||
tools,
|
||||
}
|
||||
// Skip progress yield - only yield results for Tool interface
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -273,12 +257,7 @@ export const TaskTool = {
|
||||
_ => _.type === 'text' && _.text === INTERRUPT_MESSAGE,
|
||||
)
|
||||
) {
|
||||
yield {
|
||||
type: 'progress',
|
||||
content: lastMessage,
|
||||
normalizedMessages,
|
||||
tools,
|
||||
}
|
||||
// Skip progress yield - only yield final result
|
||||
} else {
|
||||
const result = [
|
||||
toolUseCount === 1 ? '1 tool use' : `${toolUseCount} tool uses`,
|
||||
@ -290,12 +269,7 @@ export const TaskTool = {
|
||||
) + ' tokens',
|
||||
formatDuration(Date.now() - startTime),
|
||||
]
|
||||
yield {
|
||||
type: 'progress',
|
||||
content: createAssistantMessage(`[${agentType}] Completed (${result.join(' · ')})`),
|
||||
normalizedMessages,
|
||||
tools,
|
||||
}
|
||||
// Skip progress yield - only yield final result
|
||||
}
|
||||
|
||||
// Output is an AssistantMessage, but since TaskTool is a tool, it needs
|
||||
@ -304,9 +278,7 @@ export const TaskTool = {
|
||||
yield {
|
||||
type: 'result',
|
||||
data,
|
||||
normalizedMessages,
|
||||
resultForAssistant: this.renderResultForAssistant(data),
|
||||
tools,
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
2
src/types/common.d.ts
vendored
Normal file
2
src/types/common.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
// UUID 类型定义
|
||||
export type UUID = `${string}-${string}-${string}-${string}-${string}`;
|
||||
@ -263,22 +263,16 @@ export async function startAgentWatcher(onChange?: () => void): Promise<void> {
|
||||
* Stop watching agent configuration directories
|
||||
*/
|
||||
export async function stopAgentWatcher(): Promise<void> {
|
||||
const closePromises = watchers.map(watcher =>
|
||||
new Promise<void>((resolve) => {
|
||||
// FSWatcher.close() is synchronous and does not accept a callback on Node 18/20
|
||||
try {
|
||||
for (const watcher of watchers) {
|
||||
try {
|
||||
watcher.close((err) => {
|
||||
if (err) {
|
||||
console.error('Failed to close file watcher:', err)
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error closing watcher:', error)
|
||||
resolve()
|
||||
watcher.close()
|
||||
} catch (err) {
|
||||
console.error('Failed to close file watcher:', err)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
await Promise.allSettled(closePromises)
|
||||
watchers = []
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
watchers = []
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,13 +12,16 @@ import {
|
||||
} from 'fs'
|
||||
import { platform } from 'process'
|
||||
import { execFileNoThrow } from './execFileNoThrow'
|
||||
import { spawn } from 'child_process'
|
||||
import { logError } from './log'
|
||||
import { accessSync } from 'fs'
|
||||
import { CLAUDE_BASE_DIR } from './env'
|
||||
import { logEvent, getDynamicConfig } from '../services/statsig'
|
||||
import { lt } from 'semver'
|
||||
import { lt, gt } from 'semver'
|
||||
import { MACRO } from '../constants/macros'
|
||||
import { PRODUCT_COMMAND, PRODUCT_NAME } from '../constants/product'
|
||||
import { getGlobalConfig, saveGlobalConfig, isAutoUpdaterDisabled } from './config'
|
||||
import { env } from './env'
|
||||
export type InstallStatus =
|
||||
| 'success'
|
||||
| 'no_permissions'
|
||||
@ -49,14 +52,12 @@ export async function assertMinVersion(): Promise<void> {
|
||||
versionConfig.minVersion &&
|
||||
lt(MACRO.VERSION, versionConfig.minVersion)
|
||||
) {
|
||||
const suggestions = await getUpdateCommandSuggestions()
|
||||
const cmdLines = suggestions.map(c => ` ${c}`).join('\n')
|
||||
console.error(`
|
||||
It looks like your version of ${PRODUCT_NAME} (${MACRO.VERSION}) needs an update.
|
||||
A newer version (${versionConfig.minVersion} or higher) is required to continue.
|
||||
|
||||
To update, please run:
|
||||
${PRODUCT_COMMAND} update
|
||||
|
||||
This will ensure you have access to the latest features and improvements.
|
||||
您的 ${PRODUCT_NAME} 版本 (${MACRO.VERSION}) 过低,需要升级到 ${versionConfig.minVersion} 或更高版本。
|
||||
请手动执行以下任一命令进行升级:
|
||||
${cmdLines}
|
||||
`)
|
||||
process.exit(1)
|
||||
}
|
||||
@ -267,21 +268,48 @@ export function getPermissionsCommand(npmPrefix: string): string {
|
||||
}
|
||||
|
||||
export async function getLatestVersion(): Promise<string | null> {
|
||||
const abortController = new AbortController()
|
||||
setTimeout(() => abortController.abort(), 5000)
|
||||
// 1) Try npm CLI (fast when available)
|
||||
try {
|
||||
const abortController = new AbortController()
|
||||
setTimeout(() => abortController.abort(), 5000)
|
||||
const result = await execFileNoThrow(
|
||||
'npm',
|
||||
['view', MACRO.PACKAGE_URL, 'version'],
|
||||
abortController.signal,
|
||||
)
|
||||
if (result.code === 0) {
|
||||
const v = result.stdout.trim()
|
||||
if (v) return v
|
||||
}
|
||||
} catch {}
|
||||
|
||||
const result = await execFileNoThrow(
|
||||
'npm',
|
||||
['view', MACRO.PACKAGE_URL, 'version'],
|
||||
abortController.signal,
|
||||
)
|
||||
if (result.code !== 0) {
|
||||
// 2) Fallback: fetch npm registry (works in Bun/Node without npm)
|
||||
try {
|
||||
const controller = new AbortController()
|
||||
const timer = setTimeout(() => controller.abort(), 5000)
|
||||
const res = await fetch(
|
||||
`https://registry.npmjs.org/${encodeURIComponent(MACRO.PACKAGE_URL)}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/vnd.npm.install-v1+json',
|
||||
'User-Agent': `${PRODUCT_NAME}/${MACRO.VERSION}`,
|
||||
},
|
||||
signal: controller.signal,
|
||||
},
|
||||
)
|
||||
clearTimeout(timer)
|
||||
if (!res.ok) return null
|
||||
const json: any = await res.json().catch(() => null)
|
||||
const latest = json && json['dist-tags'] && json['dist-tags'].latest
|
||||
return typeof latest === 'string' ? latest : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
return result.stdout.trim()
|
||||
}
|
||||
|
||||
export async function installGlobalPackage(): Promise<InstallStatus> {
|
||||
// Detect preferred package manager and install accordingly
|
||||
if (!acquireLock()) {
|
||||
logError('Another process is currently installing an update')
|
||||
// Log the lock contention to statsig
|
||||
@ -293,26 +321,138 @@ export async function installGlobalPackage(): Promise<InstallStatus> {
|
||||
}
|
||||
|
||||
try {
|
||||
const manager = await detectPackageManager()
|
||||
if (manager === 'npm') {
|
||||
const { hasPermissions } = await checkNpmPermissions()
|
||||
if (!hasPermissions) {
|
||||
return 'no_permissions'
|
||||
}
|
||||
// Stream实时输出,减少用户等待感
|
||||
const code = await runStreaming('npm', ['install', '-g', MACRO.PACKAGE_URL])
|
||||
if (code !== 0) {
|
||||
logError(`Failed to install new version via npm (exit ${code})`)
|
||||
return 'install_failed'
|
||||
}
|
||||
return 'success'
|
||||
}
|
||||
|
||||
if (manager === 'bun') {
|
||||
const code = await runStreaming('bun', ['add', '-g', `${MACRO.PACKAGE_URL}@latest`])
|
||||
if (code !== 0) {
|
||||
logError(`Failed to install new version via bun (exit ${code})`)
|
||||
return 'install_failed'
|
||||
}
|
||||
return 'success'
|
||||
}
|
||||
|
||||
// Fallback to npm if unknown
|
||||
const { hasPermissions } = await checkNpmPermissions()
|
||||
if (!hasPermissions) {
|
||||
return 'no_permissions'
|
||||
}
|
||||
|
||||
const installResult = await execFileNoThrow('npm', [
|
||||
'install',
|
||||
'-g',
|
||||
MACRO.PACKAGE_URL,
|
||||
])
|
||||
if (installResult.code !== 0) {
|
||||
logError(
|
||||
`Failed to install new version of claude: ${installResult.stdout} ${installResult.stderr}`,
|
||||
)
|
||||
return 'install_failed'
|
||||
}
|
||||
|
||||
if (!hasPermissions) return 'no_permissions'
|
||||
const code = await runStreaming('npm', ['install', '-g', MACRO.PACKAGE_URL])
|
||||
if (code !== 0) return 'install_failed'
|
||||
return 'success'
|
||||
} finally {
|
||||
// Ensure we always release the lock
|
||||
releaseLock()
|
||||
}
|
||||
}
|
||||
|
||||
export type PackageManager = 'npm' | 'bun'
|
||||
|
||||
export async function detectPackageManager(): Promise<PackageManager> {
|
||||
// Respect explicit override if provided later via config/env (future-proof)
|
||||
try {
|
||||
// Heuristic 1: npm available and global root resolvable
|
||||
const npmRoot = await execFileNoThrow('npm', ['-g', 'root'])
|
||||
if (npmRoot.code === 0 && npmRoot.stdout.trim()) {
|
||||
return 'npm'
|
||||
}
|
||||
} catch {}
|
||||
|
||||
try {
|
||||
// Heuristic 2: running on a system with bun and installed path hints bun
|
||||
const bunVer = await execFileNoThrow('bun', ['--version'])
|
||||
if (bunVer.code === 0) {
|
||||
// BUN_INSTALL defaults to ~/.bun; if our package lives under that tree, prefer bun
|
||||
// If npm not detected but bun is available, choose bun
|
||||
return 'bun'
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// Default to npm when uncertain
|
||||
return 'npm'
|
||||
}
|
||||
|
||||
function runStreaming(cmd: string, args: string[]): Promise<number> {
|
||||
return new Promise(resolve => {
|
||||
// 打印正在使用的包管理器与命令,增强透明度
|
||||
try {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`> ${cmd} ${args.join(' ')}`)
|
||||
} catch {}
|
||||
|
||||
const child = spawn(cmd, args, {
|
||||
stdio: 'inherit',
|
||||
env: process.env,
|
||||
})
|
||||
child.on('close', code => resolve(code ?? 0))
|
||||
child.on('error', () => resolve(1))
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate human-friendly update commands for the detected package manager.
|
||||
* Also includes an alternative manager command as fallback for users.
|
||||
*/
|
||||
export async function getUpdateCommandSuggestions(): Promise<string[]> {
|
||||
// Prefer Bun first, then npm (consistent, simple UX). Include @latest.
|
||||
return [
|
||||
`bun add -g ${MACRO.PACKAGE_URL}@latest`,
|
||||
`npm install -g ${MACRO.PACKAGE_URL}@latest`,
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Non-blocking update notifier (daily)
|
||||
* - Respects CI and disabled auto-updater
|
||||
* - Uses env.hasInternetAccess() to quickly skip offline cases
|
||||
* - Stores last check timestamp + last suggested version in global config
|
||||
*/
|
||||
export async function checkAndNotifyUpdate(): Promise<void> {
|
||||
try {
|
||||
if (process.env.NODE_ENV === 'test') return
|
||||
if (await isAutoUpdaterDisabled()) return
|
||||
if (await env.getIsDocker()) return
|
||||
if (!(await env.hasInternetAccess())) return
|
||||
|
||||
const config: any = getGlobalConfig()
|
||||
const now = Date.now()
|
||||
const DAY_MS = 24 * 60 * 60 * 1000
|
||||
const lastCheck = Number(config.lastUpdateCheckAt || 0)
|
||||
if (lastCheck && now - lastCheck < DAY_MS) return
|
||||
|
||||
const latest = await getLatestVersion()
|
||||
if (!latest) {
|
||||
// Still record the check to avoid spamming
|
||||
saveGlobalConfig({ ...config, lastUpdateCheckAt: now })
|
||||
return
|
||||
}
|
||||
|
||||
if (gt(latest, MACRO.VERSION)) {
|
||||
// Update stored state and print a low-noise hint
|
||||
saveGlobalConfig({
|
||||
...config,
|
||||
lastUpdateCheckAt: now,
|
||||
lastSuggestedVersion: latest,
|
||||
})
|
||||
const suggestions = await getUpdateCommandSuggestions()
|
||||
const first = suggestions[0]
|
||||
console.log(`New version available: ${latest}. Recommended: ${first}`)
|
||||
} else {
|
||||
saveGlobalConfig({ ...config, lastUpdateCheckAt: now })
|
||||
}
|
||||
} catch (error) {
|
||||
// Never block or throw; just log and move on
|
||||
logError(`update-notify: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
@ -173,6 +173,8 @@ export type GlobalConfig = {
|
||||
modelProfiles?: ModelProfile[] // Model configuration list
|
||||
modelPointers?: ModelPointers // Model pointer system
|
||||
defaultModelName?: string // Default model
|
||||
// Update notifications
|
||||
lastDismissedUpdateVersion?: string
|
||||
}
|
||||
|
||||
export const DEFAULT_GLOBAL_CONFIG: GlobalConfig = {
|
||||
@ -196,6 +198,7 @@ export const DEFAULT_GLOBAL_CONFIG: GlobalConfig = {
|
||||
reasoning: '',
|
||||
quick: '',
|
||||
},
|
||||
lastDismissedUpdateVersion: undefined,
|
||||
}
|
||||
|
||||
export const GLOBAL_CONFIG_KEYS = [
|
||||
|
||||
@ -98,12 +98,15 @@ export function isInDirectory(
|
||||
: normalizedCwd + sep
|
||||
|
||||
// Join with a base directory to make them absolute-like for comparison
|
||||
// Using 'dummy' as base to avoid any actual file system dependencies
|
||||
const fullPath = resolvePath(cwd(), normalizedCwd, normalizedPath)
|
||||
const fullCwd = resolvePath(cwd(), normalizedCwd)
|
||||
|
||||
// Check if the path starts with the cwd
|
||||
return fullPath.startsWith(fullCwd)
|
||||
// Robust subpath check using path.relative (case-insensitive on Windows)
|
||||
const rel = relative(fullCwd, fullPath)
|
||||
if (!rel || rel === '') return true
|
||||
if (rel.startsWith('..')) return false
|
||||
if (isAbsolute(rel)) return false
|
||||
return true
|
||||
}
|
||||
|
||||
export function readTextContent(
|
||||
|
||||
@ -51,7 +51,7 @@ export async function* all<A>(
|
||||
promises.add(next(generator))
|
||||
// TODO: Clean this up
|
||||
if (value !== undefined) {
|
||||
yield value
|
||||
yield value as A
|
||||
}
|
||||
} else if (waiting.length > 0) {
|
||||
// Start a new generator when one finishes
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { Message } from '../query'
|
||||
import type { UUID } from '../types/common'
|
||||
import { countTokens } from './tokens'
|
||||
import crypto from 'crypto'
|
||||
|
||||
export interface MessageRetentionStrategy {
|
||||
type:
|
||||
@ -144,6 +146,9 @@ export class MessageContextManager {
|
||||
},
|
||||
],
|
||||
},
|
||||
costUSD: 0,
|
||||
durationMs: 0,
|
||||
uuid: crypto.randomUUID() as UUID
|
||||
}
|
||||
|
||||
const truncatedMessages = [summaryMessage, ...recentMessages]
|
||||
|
||||
@ -298,7 +298,6 @@ export async function processUserInput(
|
||||
newMessages[0]!.type === 'user' &&
|
||||
newMessages[1]!.type === 'assistant' &&
|
||||
typeof newMessages[1]!.message.content === 'string' &&
|
||||
// @ts-expect-error: TODO: this is probably a bug
|
||||
newMessages[1]!.message.content.startsWith('Unknown command:')
|
||||
) {
|
||||
logEvent('tengu_input_slash_invalid', { input })
|
||||
@ -436,7 +435,14 @@ async function getMessagesForSlashCommand(
|
||||
|
||||
try {
|
||||
// Use the context's abortController for local commands
|
||||
const result = await command.call(args, context)
|
||||
const result = await command.call(args, {
|
||||
...context,
|
||||
options: {
|
||||
commands: context.options.commands || [],
|
||||
tools: context.options.tools || [],
|
||||
slowAndCapableModel: context.options.slowAndCapableModel || 'main'
|
||||
}
|
||||
})
|
||||
|
||||
return [
|
||||
userMessage,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { isAbsolute, resolve } from 'path'
|
||||
import { isAbsolute, resolve, relative, sep } from 'path'
|
||||
import { getCwd, getOriginalCwd } from '../state'
|
||||
|
||||
// In-memory storage for file permissions that resets each session
|
||||
@ -12,7 +12,26 @@ const writeFileAllowedDirectories: Set<string> = new Set()
|
||||
* @returns Absolute path
|
||||
*/
|
||||
export function toAbsolutePath(path: string): string {
|
||||
return isAbsolute(path) ? resolve(path) : resolve(getCwd(), path)
|
||||
const abs = isAbsolute(path) ? resolve(path) : resolve(getCwd(), path)
|
||||
return normalizeForCompare(abs)
|
||||
}
|
||||
|
||||
function normalizeForCompare(p: string): string {
|
||||
// Normalize separators and resolve .. and . segments
|
||||
const norm = resolve(p)
|
||||
// On Windows, comparisons should be case-insensitive
|
||||
return process.platform === 'win32' ? norm.toLowerCase() : norm
|
||||
}
|
||||
|
||||
function isSubpath(base: string, target: string): boolean {
|
||||
const rel = relative(base, target)
|
||||
// If different drive letters on Windows, relative returns the target path
|
||||
if (!rel || rel === '') return true
|
||||
// Not a subpath if it goes up to parent
|
||||
if (rel.startsWith('..')) return false
|
||||
// Not a subpath if absolute
|
||||
if (isAbsolute(rel)) return false
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
@ -22,7 +41,8 @@ export function toAbsolutePath(path: string): string {
|
||||
*/
|
||||
export function pathInOriginalCwd(path: string): boolean {
|
||||
const absolutePath = toAbsolutePath(path)
|
||||
return absolutePath.startsWith(toAbsolutePath(getOriginalCwd()))
|
||||
const base = toAbsolutePath(getOriginalCwd())
|
||||
return isSubpath(base, absolutePath)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -32,12 +52,8 @@ export function pathInOriginalCwd(path: string): boolean {
|
||||
*/
|
||||
export function hasReadPermission(directory: string): boolean {
|
||||
const absolutePath = toAbsolutePath(directory)
|
||||
|
||||
for (const allowedPath of readFileAllowedDirectories) {
|
||||
// Permission exists for this directory or a path prefix
|
||||
if (absolutePath.startsWith(allowedPath)) {
|
||||
return true
|
||||
}
|
||||
if (isSubpath(allowedPath, absolutePath)) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@ -49,12 +65,8 @@ export function hasReadPermission(directory: string): boolean {
|
||||
*/
|
||||
export function hasWritePermission(directory: string): boolean {
|
||||
const absolutePath = toAbsolutePath(directory)
|
||||
|
||||
for (const allowedPath of writeFileAllowedDirectories) {
|
||||
// Permission exists for this directory or a path prefix
|
||||
if (absolutePath.startsWith(allowedPath)) {
|
||||
return true
|
||||
}
|
||||
if (isSubpath(allowedPath, absolutePath)) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@ -65,10 +77,9 @@ export function hasWritePermission(directory: string): boolean {
|
||||
*/
|
||||
function saveReadPermission(directory: string): void {
|
||||
const absolutePath = toAbsolutePath(directory)
|
||||
|
||||
// Clean up any existing subdirectories of this path
|
||||
for (const allowedPath of readFileAllowedDirectories) {
|
||||
if (allowedPath.startsWith(absolutePath)) {
|
||||
// Remove any existing subpaths contained by this new path
|
||||
for (const allowedPath of Array.from(readFileAllowedDirectories)) {
|
||||
if (isSubpath(absolutePath, allowedPath)) {
|
||||
readFileAllowedDirectories.delete(allowedPath)
|
||||
}
|
||||
}
|
||||
@ -92,10 +103,8 @@ export function grantReadPermissionForOriginalDir(): void {
|
||||
*/
|
||||
function saveWritePermission(directory: string): void {
|
||||
const absolutePath = toAbsolutePath(directory)
|
||||
|
||||
// Clean up any existing subdirectories of this path
|
||||
for (const allowedPath of writeFileAllowedDirectories) {
|
||||
if (allowedPath.startsWith(absolutePath)) {
|
||||
for (const allowedPath of Array.from(writeFileAllowedDirectories)) {
|
||||
if (isSubpath(absolutePath, allowedPath)) {
|
||||
writeFileAllowedDirectories.delete(allowedPath)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync, unlinkSync, renameSync } from 'node:fs'
|
||||
import { join, dirname, normalize, resolve, extname } from 'node:path'
|
||||
import { join, dirname, normalize, resolve, extname, relative, isAbsolute } from 'node:path'
|
||||
import { homedir } from 'node:os'
|
||||
|
||||
/**
|
||||
@ -98,7 +98,12 @@ export class SecureFileService {
|
||||
|
||||
// 检查是否在允许的基础路径中
|
||||
const isInAllowedPath = Array.from(this.allowedBasePaths).some(basePath => {
|
||||
return absolutePath.startsWith(basePath)
|
||||
const base = resolve(basePath)
|
||||
const rel = relative(base, absolutePath)
|
||||
if (!rel || rel === '') return true
|
||||
if (rel.startsWith('..')) return false
|
||||
if (isAbsolute(rel)) return false
|
||||
return true
|
||||
})
|
||||
|
||||
if (!isInAllowedPath) {
|
||||
@ -556,4 +561,4 @@ export class SecureFileService {
|
||||
}
|
||||
|
||||
// 导出单例实例
|
||||
export const secureFileService = SecureFileService.getInstance()
|
||||
export const secureFileService = SecureFileService.getInstance()
|
||||
|
||||
@ -2,18 +2,16 @@ import { getGlobalConfig } from './config'
|
||||
|
||||
export interface Theme {
|
||||
bashBorder: string
|
||||
claude: string
|
||||
koding: string
|
||||
kode: string
|
||||
noting: string
|
||||
permission: string
|
||||
secondaryBorder: string
|
||||
text: string
|
||||
secondaryText: string
|
||||
suggestion: string
|
||||
// Semantic colors
|
||||
success: string
|
||||
error: string
|
||||
warning: string
|
||||
// UI colors
|
||||
primary: string
|
||||
secondary: string
|
||||
diff: {
|
||||
@ -25,9 +23,9 @@ export interface Theme {
|
||||
}
|
||||
|
||||
const lightTheme: Theme = {
|
||||
bashBorder: '#ff0087',
|
||||
claude: '#7aff59ff',
|
||||
koding: '#9dff00ff',
|
||||
bashBorder: '#FF6E57',
|
||||
kode: '#FFC233',
|
||||
noting: '#222222',
|
||||
permission: '#e9c61aff',
|
||||
secondaryBorder: '#999',
|
||||
text: '#000',
|
||||
@ -47,31 +45,31 @@ const lightTheme: Theme = {
|
||||
}
|
||||
|
||||
const lightDaltonizedTheme: Theme = {
|
||||
bashBorder: '#0066cc', // Blue instead of pink for better contrast
|
||||
claude: '#5f97cd', // Orange adjusted for deuteranopia
|
||||
koding: '#0000ff',
|
||||
permission: '#3366ff', // Brighter blue for better visibility
|
||||
bashBorder: '#FF6E57',
|
||||
kode: '#FFC233',
|
||||
noting: '#222222',
|
||||
permission: '#3366ff',
|
||||
secondaryBorder: '#999',
|
||||
text: '#000',
|
||||
secondaryText: '#666',
|
||||
suggestion: '#3366ff',
|
||||
success: '#006699', // Blue instead of green
|
||||
error: '#cc0000', // Pure red for better distinction
|
||||
warning: '#ff9900', // Orange adjusted for deuteranopia
|
||||
success: '#006699',
|
||||
error: '#cc0000',
|
||||
warning: '#ff9900',
|
||||
primary: '#000',
|
||||
secondary: '#666',
|
||||
diff: {
|
||||
added: '#99ccff', // Light blue instead of green
|
||||
removed: '#ffcccc', // Light red for better contrast
|
||||
added: '#99ccff',
|
||||
removed: '#ffcccc',
|
||||
addedDimmed: '#d1e7fd',
|
||||
removedDimmed: '#ffe9e9',
|
||||
},
|
||||
}
|
||||
|
||||
const darkTheme: Theme = {
|
||||
bashBorder: '#fd5db1',
|
||||
claude: '#5f97cd',
|
||||
koding: '#0000ff',
|
||||
bashBorder: '#FF6E57',
|
||||
kode: '#FFC233',
|
||||
noting: '#222222',
|
||||
permission: '#b1b9f9',
|
||||
secondaryBorder: '#888',
|
||||
text: '#fff',
|
||||
@ -91,32 +89,28 @@ const darkTheme: Theme = {
|
||||
}
|
||||
|
||||
const darkDaltonizedTheme: Theme = {
|
||||
bashBorder: '#3399ff', // Bright blue instead of pink
|
||||
claude: '#5f97cd', // Orange adjusted for deuteranopia
|
||||
koding: '#0000ff',
|
||||
permission: '#99ccff', // Light blue for better contrast
|
||||
bashBorder: '#FF6E57',
|
||||
kode: '#FFC233',
|
||||
noting: '#222222',
|
||||
permission: '#99ccff',
|
||||
secondaryBorder: '#888',
|
||||
text: '#fff',
|
||||
secondaryText: '#999',
|
||||
suggestion: '#99ccff',
|
||||
success: '#3399ff', // Bright blue instead of green
|
||||
error: '#ff6666', // Bright red for better visibility
|
||||
warning: '#ffcc00', // Yellow-orange for deuteranopia
|
||||
success: '#3399ff',
|
||||
error: '#ff6666',
|
||||
warning: '#ffcc00',
|
||||
primary: '#fff',
|
||||
secondary: '#999',
|
||||
diff: {
|
||||
added: '#004466', // Dark blue instead of green
|
||||
removed: '#660000', // Dark red for better contrast
|
||||
added: '#004466',
|
||||
removed: '#660000',
|
||||
addedDimmed: '#3e515b',
|
||||
removedDimmed: '#3e2c2c',
|
||||
},
|
||||
}
|
||||
|
||||
export type ThemeNames =
|
||||
| 'dark'
|
||||
| 'light'
|
||||
| 'light-daltonized'
|
||||
| 'dark-daltonized'
|
||||
export type ThemeNames = 'dark' | 'light' | 'light-daltonized' | 'dark-daltonized'
|
||||
|
||||
export function getTheme(overrideTheme?: ThemeNames): Theme {
|
||||
const config = getGlobalConfig()
|
||||
|
||||
@ -112,7 +112,7 @@ export async function getReasoningEffort(
|
||||
// 🔧 Fix: Use ModelManager fallback instead of legacy config
|
||||
const modelManager = getModelManager()
|
||||
const fallbackProfile = modelManager.getModel('main')
|
||||
reasoningEffort = fallbackProfile?.reasoningEffort || 'medium'
|
||||
reasoningEffort = (fallbackProfile?.reasoningEffort === 'minimal' ? 'low' : fallbackProfile?.reasoningEffort) || 'medium'
|
||||
}
|
||||
|
||||
const maxEffort =
|
||||
|
||||
135
tasks.md
Normal file
135
tasks.md
Normal file
@ -0,0 +1,135 @@
|
||||
# TypeScript Error Fix Plan - 100% Confidence Strategy
|
||||
|
||||
## Overview
|
||||
Fix all 127 TypeScript compilation errors systematically, starting from core type definitions to implementation details.
|
||||
|
||||
## Phase 1: Core Type System Foundation (Critical - Block Everything)
|
||||
|
||||
### 1.1 Message Type System Fix
|
||||
- [ ] Fix Message type union in `src/messages.ts` - Add missing 'message' property to ProgressMessage or remove from usage
|
||||
- [ ] Add required properties (costUSD, durationMs, uuid) to message type factories in `src/utils/messageContextManager.ts`
|
||||
- [ ] Fix query.ts message property access patterns (lines 203-210)
|
||||
- [ ] Validate all Message type consumers after changes
|
||||
|
||||
### 1.2 Tool Interface Alignment
|
||||
- [ ] Update Tool base interface in `src/Tool.ts` to match actual implementations
|
||||
- [ ] Fix renderResultForAssistant return type to allow string | array
|
||||
- [ ] Fix renderToolUseRejectedMessage signature to be consistent (0 args vs 2 args)
|
||||
- [ ] Add optional setToolJSX to ToolUseContext interface
|
||||
- [ ] Update ExtendedToolUseContext type definition
|
||||
|
||||
### 1.3 Key Type Extensions
|
||||
- [ ] Add missing properties to Key type: `fn`, `home`, `end`, `space`
|
||||
- [ ] Update ink types or create proper type augmentation file
|
||||
- [ ] Verify all keyboard event handlers after Key type update
|
||||
|
||||
## Phase 2: Tool System Implementation (High Priority)
|
||||
|
||||
### 2.1 Fix Tool Implementations
|
||||
- [ ] Fix ArchitectTool - Align call() and renderResultForAssistant signatures
|
||||
- [ ] Fix FileReadTool - Handle string | array return type, add sharp dependency
|
||||
- [ ] Fix FileWriteTool - Fix renderToolUseRejectedMessage signature
|
||||
- [ ] Fix FileEditTool - Fix renderToolUseRejectedMessage signature
|
||||
- [ ] Fix MultiEditTool - Fix applyEdit parameters and return properties
|
||||
- [ ] Fix TaskTool - Align AsyncGenerator types with Tool interface
|
||||
- [ ] Fix StickerRequestTool - Handle optional setToolJSX property
|
||||
- [ ] Fix NotebookReadTool - Type assertion for unknown to string conversion
|
||||
- [ ] Fix AskExpertModelTool - Fix debugLogger call signatures
|
||||
|
||||
### 2.2 Tool Prompt System
|
||||
- [ ] Update all tool prompt.ts files to match new signatures
|
||||
- [ ] Ensure async description functions are properly typed
|
||||
|
||||
## Phase 3: React 19 / Ink 6 Component Updates (Medium Priority)
|
||||
|
||||
### 3.1 Component Props Fix
|
||||
- [ ] Fix agents.tsx - Remove 'key' from component props, pass as JSX attribute
|
||||
- [ ] Fix AssistantToolUseMessage - Add required children prop
|
||||
- [ ] Fix REPL.tsx - Add children to PermissionProvider and TodoProvider
|
||||
- [ ] Fix all Text components missing children prop
|
||||
|
||||
### 3.2 Import Path Corrections
|
||||
- [ ] Remove .tsx extensions from imports in Doctor.tsx
|
||||
- [ ] Verify all import paths follow TypeScript conventions
|
||||
|
||||
## Phase 4: Service Layer Fixes (Medium Priority)
|
||||
|
||||
### 4.1 OpenAI Service Type Safety
|
||||
- [ ] Add proper error type guards in openai.ts (lines 611, 743)
|
||||
- [ ] Type API responses properly (lines 1291-1299)
|
||||
- [ ] Create response type interfaces for OpenAI API
|
||||
|
||||
### 4.2 Config Service Overloads
|
||||
- [ ] Fix getConfig overload in cli.tsx line 543
|
||||
- [ ] Ensure boolean parameter properly narrows to true/false
|
||||
|
||||
## Phase 5: Hook System Updates (Low Priority)
|
||||
|
||||
### 5.1 Input Hook Fixes
|
||||
- [ ] Remove unused @ts-expect-error in useDoublePress.ts
|
||||
- [ ] Remove unused @ts-expect-error in useTextInput.ts
|
||||
- [ ] Fix Key type usage in useTextInput.ts
|
||||
- [ ] Fix Key type usage in useUnifiedCompletion.ts
|
||||
|
||||
### 5.2 Message Hook Updates
|
||||
- [ ] Fix useUnifiedCompletion optional vs required properties
|
||||
- [ ] Update messages.tsx type assertions
|
||||
|
||||
## Phase 6: Utility Functions (Low Priority)
|
||||
|
||||
### 6.1 Type Utilities
|
||||
- [ ] Fix generators.ts void | Awaited<A> issue
|
||||
- [ ] Fix thinking.ts enum value 'minimal'
|
||||
- [ ] Clean up type assertions
|
||||
|
||||
### 6.2 Clean-up Tasks
|
||||
- [ ] Remove all unused @ts-expect-error directives
|
||||
- [ ] Fix entrypoints parameter counts
|
||||
- [ ] Add isCustomCommand to proper type definition
|
||||
|
||||
## Phase 7: Dependency Management
|
||||
|
||||
### 7.1 Missing Dependencies
|
||||
- [ ] Add sharp package for image processing
|
||||
- [ ] Verify all package.json dependencies are installed
|
||||
- [ ] Update @types packages if needed
|
||||
|
||||
## Phase 8: Validation & Testing
|
||||
|
||||
### 8.1 Compilation Verification
|
||||
- [ ] Run `npx tsc --noEmit` after each phase
|
||||
- [ ] Document remaining errors if any
|
||||
- [ ] Ensure zero TypeScript errors
|
||||
|
||||
### 8.2 Runtime Testing
|
||||
- [ ] Test basic CLI functionality
|
||||
- [ ] Test each tool individually
|
||||
- [ ] Test React components render correctly
|
||||
- [ ] Verify no runtime regressions
|
||||
|
||||
## Execution Order & Time Estimates
|
||||
|
||||
1. **Phase 1**: 2 hours - Must complete first, blocks everything
|
||||
2. **Phase 2**: 3 hours - Can parallelize tool fixes
|
||||
3. **Phase 3**: 1 hour - Independent, can do in parallel with Phase 4
|
||||
4. **Phase 4**: 1 hour - Independent service fixes
|
||||
5. **Phase 5**: 30 minutes - Quick fixes
|
||||
6. **Phase 6**: 30 minutes - Simple clean-up
|
||||
7. **Phase 7**: 15 minutes - Package installation
|
||||
8. **Phase 8**: 1 hour - Final validation
|
||||
|
||||
**Total Estimated Time**: 9 hours 15 minutes
|
||||
|
||||
## Success Criteria
|
||||
- [ ] Zero TypeScript compilation errors
|
||||
- [ ] All tools functioning correctly
|
||||
- [ ] React components rendering without warnings
|
||||
- [ ] No runtime regressions
|
||||
- [ ] Clean git diff with minimal changes
|
||||
|
||||
## Risk Mitigation
|
||||
- Create backup branch before starting
|
||||
- Test each phase independently
|
||||
- Use `git add -p` for selective staging
|
||||
- Document any breaking changes
|
||||
- Keep fixes minimal and focused
|
||||
53
test-cli.js
Normal file
53
test-cli.js
Normal file
@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const { spawn } = require('child_process');
|
||||
const path = require('path');
|
||||
|
||||
console.log('Testing Kode CLI...\n');
|
||||
|
||||
// Test 1: Version
|
||||
console.log('1. Testing version command:');
|
||||
const versionProcess = spawn('node', ['cli.js', '--version'], { cwd: __dirname });
|
||||
versionProcess.stdout.on('data', (data) => {
|
||||
console.log(` Version: ${data.toString().trim()}`);
|
||||
});
|
||||
|
||||
versionProcess.on('close', () => {
|
||||
// Test 2: Help
|
||||
console.log('\n2. Testing help command:');
|
||||
const helpProcess = spawn('node', ['cli.js', '--help'], { cwd: __dirname });
|
||||
|
||||
helpProcess.stdout.on('data', (data) => {
|
||||
console.log(` ${data.toString()}`);
|
||||
});
|
||||
|
||||
helpProcess.stderr.on('data', (data) => {
|
||||
console.log(` Error: ${data.toString()}`);
|
||||
});
|
||||
|
||||
helpProcess.on('close', (code) => {
|
||||
console.log(`\n3. CLI help exited with code ${code}`);
|
||||
|
||||
// Test 3: Quick interaction test
|
||||
console.log('\n4. Testing interactive mode (sending exit):');
|
||||
const interactiveProcess = spawn('node', ['cli.js'], { cwd: __dirname });
|
||||
|
||||
interactiveProcess.stdout.on('data', (data) => {
|
||||
const output = data.toString();
|
||||
if (output.includes('Kode') || output.includes('Welcome') || output.includes('>')) {
|
||||
console.log(' ✓ CLI started successfully');
|
||||
interactiveProcess.stdin.write('exit\n');
|
||||
}
|
||||
});
|
||||
|
||||
interactiveProcess.stderr.on('data', (data) => {
|
||||
console.log(` Stderr: ${data.toString()}`);
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
interactiveProcess.kill();
|
||||
console.log('\n✅ All basic CLI tests completed');
|
||||
process.exit(0);
|
||||
}, 2000);
|
||||
});
|
||||
});
|
||||
85
typescript_errors_analysis.md
Normal file
85
typescript_errors_analysis.md
Normal file
@ -0,0 +1,85 @@
|
||||
# TypeScript Compilation Error Analysis
|
||||
|
||||
## Summary
|
||||
Total errors: 127 lines of error output
|
||||
Total files affected: 34 files
|
||||
|
||||
## Error Categories
|
||||
|
||||
### 1. React 19 / Ink 6 Migration Issues (Most Common)
|
||||
**Error Type**: Missing `children` prop, incorrect prop types
|
||||
**Affected Files**:
|
||||
- `src/commands/agents.tsx` - `key` prop not allowed on Props
|
||||
- `src/components/messages/AssistantToolUseMessage.tsx` - Missing `children` prop
|
||||
- `src/screens/REPL.tsx` - Missing `children` prop in multiple components
|
||||
- `src/screens/Doctor.tsx` - Import path issue with `.tsx` extension
|
||||
|
||||
### 2. Type Incompatibility Issues
|
||||
**Error Type**: Type assignments, missing properties, incorrect return types
|
||||
**Affected Folders & Files**:
|
||||
|
||||
#### `/src/tools/` (Tool System Issues)
|
||||
- `ArchitectTool/ArchitectTool.tsx` - Return type incompatibilities, incorrect signatures
|
||||
- `AskExpertModelTool/AskExpertModelTool.tsx` - Function call argument mismatch
|
||||
- `FileEditTool/FileEditTool.tsx` - Function signature mismatch
|
||||
- `FileReadTool/FileReadTool.tsx` - Return type string vs array incompatibility, missing 'sharp' module
|
||||
- `FileWriteTool/FileWriteTool.tsx` - Function signature mismatch
|
||||
- `MultiEditTool/MultiEditTool.tsx` - Missing properties, wrong argument counts
|
||||
- `NotebookReadTool/NotebookReadTool.tsx` - Type 'unknown' assignment issue
|
||||
- `StickerRequestTool/StickerRequestTool.tsx` - Missing `setToolJSX` property
|
||||
- `TaskTool/TaskTool.tsx` - Complex AsyncGenerator return type mismatch
|
||||
|
||||
#### `/src/hooks/` (Hook Issues)
|
||||
- `useDoublePress.ts` - Unused @ts-expect-error directive
|
||||
- `useTextInput.ts` - Missing properties on Key type (`fn`, `home`, `end`)
|
||||
- `useUnifiedCompletion.ts` - Missing `space` property on Key type
|
||||
|
||||
#### `/src/services/` (Service Layer Issues)
|
||||
- `openai.ts` - Unknown type property access (`error`, `message`, `data`, `models`)
|
||||
|
||||
#### `/src/utils/` (Utility Issues)
|
||||
- `generators.ts` - Type 'void' not assignable to generic type
|
||||
- `messageContextManager.ts` - Missing required properties (costUSD, durationMs, uuid)
|
||||
- `messages.tsx` - Property mismatch, optional vs required properties
|
||||
- `thinking.ts` - Invalid enum value 'minimal'
|
||||
|
||||
#### `/src/entrypoints/` (Entry Point Issues)
|
||||
- `cli.tsx` - Unused @ts-expect-error, overload mismatch, untyped function call
|
||||
- `mcp.ts` - Wrong argument counts
|
||||
|
||||
### 3. Query System Issues
|
||||
**File**: `src/query.ts`
|
||||
**Errors**:
|
||||
- Property 'message' does not exist on ProgressMessage type
|
||||
- Type comparisons between 'progress' and 'result'
|
||||
- Missing properties on result types
|
||||
|
||||
## Priority Fix Areas
|
||||
|
||||
### High Priority (Core functionality)
|
||||
1. **Tool System** - Most tools have signature mismatches affecting core functionality
|
||||
2. **Query System** - Message type definitions are broken
|
||||
3. **Entry Points** - CLI and MCP entry points have critical errors
|
||||
|
||||
### Medium Priority (User interaction)
|
||||
1. **React Components** - Props issues with React 19/Ink 6
|
||||
2. **Hooks** - Key handling for user input
|
||||
|
||||
### Low Priority (Clean-up)
|
||||
1. **Unused @ts-expect-error directives**
|
||||
2. **Import path extensions**
|
||||
|
||||
## Root Causes
|
||||
|
||||
1. **React 19 / Ink 6 Upgrade** - Breaking changes in component prop requirements
|
||||
2. **Tool Interface Changes** - Mismatch between tool implementations and base Tool interface
|
||||
3. **Type Definition Drift** - Types have evolved but implementations haven't been updated
|
||||
4. **Missing Dependencies** - 'sharp' module for image processing
|
||||
|
||||
## Recommended Fix Strategy
|
||||
|
||||
1. Fix Tool base interface to align with implementations
|
||||
2. Update React component props for React 19/Ink 6
|
||||
3. Resolve Message type definitions in query system
|
||||
4. Add missing type properties to Key interface
|
||||
5. Clean up unused directives and type assertions
|
||||
Loading…
x
Reference in New Issue
Block a user