jinzd-ai-cli 0.1.23 → 0.1.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +161 -15
- package/dist/index.js +635 -47
- package/package.json +1 -1
package/CLAUDE.md
CHANGED
|
@@ -45,15 +45,21 @@ src/
|
|
|
45
45
|
│ ├── dev-state.ts # 开发状态交接(provider/model 切换时快照生成、save/load/clear)
|
|
46
46
|
│ ├── setup-wizard.ts # @inquirer/prompts 首次运行交互式设置
|
|
47
47
|
│ └── commands/
|
|
48
|
-
│ └── index.ts # CommandRegistry +
|
|
48
|
+
│ └── index.ts # CommandRegistry + 26个命令(/help /about /provider /model /clear /compact /plan /session /system /context /status /search /undo /export /copy /cost /init /skill /tools /plugins /mcp /config /checkpoint /review /commands /exit)
|
|
49
|
+
│ ├── custom-commands.ts # CustomCommandManager(~/.aicli/commands/*.md 用户自定义命令)
|
|
50
|
+
├── skills/
|
|
51
|
+
│ ├── types.ts # Skill/SkillMeta 接口、parseSkillFile(YAML frontmatter 解析)
|
|
52
|
+
│ └── manager.ts # SkillManager(加载/激活/停用技能,工具白名单过滤)
|
|
49
53
|
├── mcp/
|
|
50
54
|
│ ├── types.ts # MCP 协议类型定义(JSON-RPC、工具 schema、服务器配置)
|
|
51
55
|
│ ├── client.ts # McpClient(单个 MCP 服务器 STDIO 连接,JSON-RPC 通信)
|
|
52
56
|
│ └── manager.ts # McpManager(多服务器管理,工具发现与注册,MCP→Tool 转换)
|
|
53
57
|
└── tools/
|
|
54
|
-
├── types.ts # ToolDefinition / ToolCall / ToolResult / DangerLevel / getDangerLevel
|
|
58
|
+
├── types.ts # ToolDefinition / ToolCall / ToolResult / DangerLevel / getDangerLevel / isFileWriteTool
|
|
55
59
|
├── registry.ts # ToolRegistry(注册全部内置工具,共15个)
|
|
56
|
-
├──
|
|
60
|
+
├── hooks.ts # 工具执行钩子(runHook,shell 命令模板替换 + execSync)
|
|
61
|
+
├── permissions.ts # 基于规则的工具权限控制(checkPermission,首匹配规则)
|
|
62
|
+
├── executor.ts # ToolExecutor(确认逻辑 + 批量文件写入预览 + batchConfirm + hooks + permissions)
|
|
57
63
|
└── builtin/
|
|
58
64
|
├── bash.ts # bash 工具(Windows: PowerShell + Buffer→UTF-8;Unix: $SHELL)
|
|
59
65
|
├── read-file.ts # read_file 工具
|
|
@@ -312,29 +318,169 @@ const stdinLines = Array.isArray(rawStdin) ? rawStdin.map(String)
|
|
|
312
318
|
- [x] **Plan Mode 规划模式**(2026-02-25):`/plan` 进入只读规划(AI 只能用只读工具白名单),提示符显示 `[PLAN]` 黄色标记,`/plan execute` 切回正常模式,`/plan exit` 放弃计划。
|
|
313
319
|
- [x] **Sub-agents 系统**(2026-02-27):`spawn_agent` 工具在同进程中运行独立 agentic 循环。子代理拥有隔离对话、过滤后的工具集(SUBAGENT_ALLOWED_TOOLS)、自动确认 write 操作、阻止 destructive 操作。SubAgentExecutor 带前缀终端输出。
|
|
314
320
|
|
|
315
|
-
### P1 —
|
|
316
|
-
- [
|
|
317
|
-
- [
|
|
318
|
-
- [
|
|
319
|
-
- [
|
|
320
|
-
- [
|
|
321
|
+
### P1 — 重要差距(已全部完成 2026-02-28)
|
|
322
|
+
- [x] **Agent Skills 系统**(2026-02-28):`~/.aicli/skills/*.md` 可复用技能包,YAML frontmatter 声明 name/description/tools 白名单。`/skill list|<name>|off|reload` 命令。激活时注入 system prompt + 过滤工具集(Plan Mode 优先)。
|
|
323
|
+
- [x] **`/init` 项目初始化**(2026-02-28):扫描项目类型(Node/Rust/Python/Go/Java 等)+ 目录结构树,调用当前 AI 生成结构化 AICLI.md。已存在时需 `--force` 覆盖。
|
|
324
|
+
- [x] **`/copy` 剪贴板支持**(2026-02-28):跨平台复制最后 AI 回答到系统剪贴板(Windows: clip / macOS: pbcopy / Linux: xclip 或 xsel),无外部 npm 依赖。
|
|
325
|
+
- [x] **多文件编辑预览**(2026-02-28):`executeAll()` 重构为三组分流(safe→fileWrite→other)。文件写入 2+ 个时走 `executeBatchFileWrites()` 批量 diff 预览 + `batchConfirm()`(`[a]pprove all / [r]eject all / [1,3,5]` 选择性 approve)。
|
|
326
|
+
- [x] **Token 用量统计 `/cost`**(2026-02-28):显示 session 累计 input/output/total tokens + provider/model/消息数。`/cost reset` 重置计数器。
|
|
321
327
|
|
|
322
328
|
### P2 — 增强功能
|
|
323
|
-
- [
|
|
324
|
-
- [
|
|
325
|
-
- [
|
|
326
|
-
- [
|
|
327
|
-
- [
|
|
329
|
+
- [x] **Hooks 系统**(2026-02-28):pre/post tool execution shell 命令钩子(`config.hooks`),模板变量 `{tool}/{dangerLevel}/{args}/{status}`
|
|
330
|
+
- [x] **Permission Rules**(2026-02-28):基于规则的工具权限控制(`config.permissionRules`),auto-approve/deny/confirm,首匹配规则
|
|
331
|
+
- [x] **Checkpointing**(2026-02-28):`/checkpoint save/restore/list/delete`,检查点元数据随 session JSON 持久化
|
|
332
|
+
- [x] **`/review` 代码审查**(2026-02-28):读取 git diff + 上下文,AI 生成结构化审查意见(`--staged`/`--detailed`)
|
|
333
|
+
- [x] **Custom Commands**(2026-02-28):`~/.aicli/commands/*.md` 用户自定义命令,YAML frontmatter + `{{input}}/{{git-diff}}/{{git-context}}/{{file:path}}` 模板变量
|
|
328
334
|
|
|
329
335
|
## 已知待改进项
|
|
330
336
|
|
|
331
337
|
### 低优先级
|
|
332
338
|
- [ ] **macOS/Linux 完整测试**:跨平台逻辑已处理(`$SHELL` fallback、UTF-8 env vars),但尚未在非 Windows 环境实际运行验证。
|
|
333
|
-
- [
|
|
339
|
+
- [x] **Token 用量显示**:已通过 `/cost` 命令实现(v0.1.23),显示 session 累计 input/output/total tokens。
|
|
334
340
|
- [ ] **GitHub 仓库迁移**:当前在 gitee,npm 包受众需要 GitHub 托管。
|
|
335
341
|
- [ ] **web_fetch DNS 解析时 SSRF 防护**:当前只检查 URL hostname 字符串,若 hostname 是域名(非裸 IP)则未检查其解析后的 IP 是否为私有地址。需在发起请求后对实际连接 IP 做二次校验(复杂,低频风险)。
|
|
336
342
|
- [ ] **`persistentCwd` 全局状态**:bash 工具的当前工作目录是模块级全局变量,多 session 并发时可能串扰。现阶段单 session REPL 无影响,GUI 多会话扩展时需重构为 per-session 状态。
|
|
337
343
|
|
|
344
|
+
## 本轮开发完成记录(2026-02-28,v0.1.24 → v0.1.25)
|
|
345
|
+
|
|
346
|
+
### 新增功能:Tab 自动补全 + 流式 Token 计数
|
|
347
|
+
|
|
348
|
+
**功能 1:Tab 自动补全**
|
|
349
|
+
- `src/repl/repl.ts`:`readline.createInterface` 注入 `completer` 回调
|
|
350
|
+
- 新增 `completeInput(line)` 方法:根据输入上下文分发补全逻辑
|
|
351
|
+
- 命令名补全:`/pro<TAB>` → `/provider`(内置 + 自定义命令)
|
|
352
|
+
- 子命令参数:`/provider <TAB>` → provider 列表;`/model <TAB>` → 模型列表;`/checkpoint <TAB>` → save/restore/list/delete 等
|
|
353
|
+
- `@` 文件路径补全:`@src/re<TAB>` → `@src/repl/`(递进目录补全,跳过隐藏文件)
|
|
354
|
+
- 新增 `completeFilePath(partial)` 辅助方法:`readdirSync` + `statSync` 扫描目录,目录追加 `/` 后缀
|
|
355
|
+
|
|
356
|
+
**功能 2:流式 Token 计数(内联显示)**
|
|
357
|
+
- `src/repl/renderer.ts`:`renderStream()` 签名扩展 `showTokens?` + `sessionTotal?`
|
|
358
|
+
- 流结束后在 `\n\n` 之前立即内联显示 token 计数
|
|
359
|
+
- 有精确 usage(OpenAI/Gemini)→ 显示完整 `📊 in X + out Y = Z tokens │ session total: N`
|
|
360
|
+
- 无精确 usage(Claude streaming)→ 基于字符数估算 `📊 ~X output tokens (estimated)`
|
|
361
|
+
- 返回值新增 `tokensShown: boolean` 标志,防止 REPL 重复调用 `renderUsage()`
|
|
362
|
+
- `src/repl/repl.ts`:`handleChatSimple()` + tee 流式路径传入 `showTokens`/`sessionTotal`,条件跳过后续 `renderUsage()`
|
|
363
|
+
|
|
364
|
+
**版本与收尾**
|
|
365
|
+
- `src/core/constants.ts`:VERSION `0.1.24` → `0.1.25`
|
|
366
|
+
- `package.json`:version 同步
|
|
367
|
+
- `src/repl/renderer.ts`:/about 新增 2 条特性条目
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
## 本轮开发完成记录(2026-02-28,v0.1.22 → v0.1.23)
|
|
372
|
+
|
|
373
|
+
### P1 全部 5 个功能实现
|
|
374
|
+
|
|
375
|
+
**背景**:P0 全部完成后,进入 P1 重要差距功能开发。一次性实现全部 5 项。
|
|
376
|
+
|
|
377
|
+
### 功能 1:`/copy` 剪贴板支持
|
|
378
|
+
|
|
379
|
+
**实现**:`src/repl/commands/index.ts`
|
|
380
|
+
- `copyToClipboard()` 辅助函数:跨平台调用原生命令(Windows: `clip` / macOS: `pbcopy` / Linux: `xclip -selection clipboard`,fallback `xsel --clipboard --input`)
|
|
381
|
+
- 通过 `execSync` 管道写入,无外部 npm 依赖
|
|
382
|
+
- `CommandContext` 新增 `getLastResponse: () => string`
|
|
383
|
+
- `repl.ts` ctx 注入:`getLastResponse: () => lastResponseStore.content`
|
|
384
|
+
|
|
385
|
+
### 功能 2:`/cost` Token 用量统计
|
|
386
|
+
|
|
387
|
+
**实现**:`src/repl/commands/index.ts`
|
|
388
|
+
- 显示 session 累计 input/output/total tokens + provider/model/消息数
|
|
389
|
+
- `/cost reset` 重置计数器
|
|
390
|
+
- 使用已有 `ctx.getSessionTokenUsage()` 和 `ctx.resetSessionTokenUsage()`
|
|
391
|
+
|
|
392
|
+
### 功能 3:`/init` 项目初始化
|
|
393
|
+
|
|
394
|
+
**实现**:`src/repl/commands/index.ts`
|
|
395
|
+
- `SCAN_SKIP_DIRS` Set(node_modules、.git、dist 等 20+ 目录)
|
|
396
|
+
- `ProjectInfo` 接口 + `scanDirTree()` 递归目录树(深度 4,每层 30 项)
|
|
397
|
+
- `scanProject()` 检测项目类型(package.json/Cargo.toml/pyproject.toml/go.mod 等)
|
|
398
|
+
- `buildInitPrompt()` 构造 AI 提示词,要求生成结构化 AICLI.md
|
|
399
|
+
- `CommandContext` 新增 `chatOnce(prompt, options?)` 方法
|
|
400
|
+
- `repl.ts` ctx 实现:调用 provider.chat() 非流式,temperature=0.3
|
|
401
|
+
- 已存在 AICLI.md 时需 `/init --force` 覆盖
|
|
402
|
+
|
|
403
|
+
### 功能 4:Agent Skills 系统
|
|
404
|
+
|
|
405
|
+
**新文件**:
|
|
406
|
+
- `src/skills/types.ts`:`SkillMeta`(name/description/tools?)、`Skill`(meta/content/filePath)接口;`parseSimpleYaml()`(regex key:value)、`parseYamlArray()`(`[a, b, c]` 格式)、`parseSkillFile()`(读取 .md → 提取 YAML frontmatter → 返回 Skill)
|
|
407
|
+
- `src/skills/manager.ts`:`SkillManager` 类——`loadSkills()`(扫描 skillsDir,自动创建目录)、`activate(name)` / `deactivate()`、`getActivePromptContent()` / `getActiveToolFilter()`(返回工具名 Set)
|
|
408
|
+
|
|
409
|
+
**集成**(`src/repl/repl.ts`):
|
|
410
|
+
- `private skillManager: SkillManager | null` 字段
|
|
411
|
+
- `start()` 中初始化 SkillManager、加载技能、显示数量
|
|
412
|
+
- `buildCurrentSystemPrompt()` 注入激活技能的 content(在项目上下文之后,plan mode 之前)
|
|
413
|
+
- `handleChatWithTools()` 工具过滤:Plan Mode > Skill 白名单 > 全部工具
|
|
414
|
+
- `refreshPrompt()` 显示 `[skill:name]` 洋红色标记
|
|
415
|
+
|
|
416
|
+
**命令**(`/skill`):
|
|
417
|
+
- `/skill` 或 `/skill list`:列出所有技能
|
|
418
|
+
- `/skill <name>`:激活指定技能
|
|
419
|
+
- `/skill off`:停用当前技能
|
|
420
|
+
- `/skill reload`:重新扫描技能目录
|
|
421
|
+
|
|
422
|
+
**常量**:`src/core/constants.ts` 新增 `SKILLS_DIR_NAME = 'skills'`
|
|
423
|
+
|
|
424
|
+
### 功能 5:多文件编辑预览(批量确认)
|
|
425
|
+
|
|
426
|
+
**修改**:
|
|
427
|
+
- `src/tools/types.ts`:新增 `isFileWriteTool(name)` 判断 write_file / edit_file
|
|
428
|
+
- `src/tools/executor.ts`:`executeAll()` 重构为三组分流:
|
|
429
|
+
1. `safeCalls`(safe 级别)→ 先执行(保证 mkdir 等前置操作完成)
|
|
430
|
+
2. `fileWriteCalls`(isFileWriteTool && write)→ 单个走原有确认,2+ 个走批量
|
|
431
|
+
3. `otherCalls`(剩余 write/destructive)→ 逐个原有确认流程
|
|
432
|
+
- 新增 `executeBatchFileWrites(calls)`:展示编号列表 + diff 预览 → `batchConfirm()`
|
|
433
|
+
- 新增 `batchConfirm(count)`:`[a]pprove all / [r]eject all / [1,3,5] approve specific`
|
|
434
|
+
- 使用 `rl.once('line')` 读整行(与 `confirm()` 模式一致)
|
|
435
|
+
- 解析逗号分隔数字编号,返回 `'all' | 'none' | Set<number>`
|
|
436
|
+
- 支持 Ctrl+C 取消(复用 `cancelConfirmFn`)
|
|
437
|
+
|
|
438
|
+
### 架构变更
|
|
439
|
+
- `src/repl/commands/index.ts`:CommandContext 新增 `getLastResponse` / `chatOnce` / `refreshPrompt` / `getSkillManager`;新增 /copy /cost /init /skill 共 4 个命令;/help 更新
|
|
440
|
+
- `src/repl/repl.ts`:SkillManager 集成 + 4 个新 ctx 方法
|
|
441
|
+
- `src/repl/renderer.ts`:`/about` REPL 命令 19 → 23,新增 3 条特性(Agent Skills / /init / 多文件编辑预览)
|
|
442
|
+
|
|
443
|
+
### 版本与发布
|
|
444
|
+
- `src/core/constants.ts`:VERSION `0.1.22` → `0.1.23`
|
|
445
|
+
- `package.json`:version `0.1.22` → `0.1.23`
|
|
446
|
+
|
|
447
|
+
---
|
|
448
|
+
|
|
449
|
+
## 本轮开发完成记录(2026-02-28,v0.1.23 → v0.1.24)
|
|
450
|
+
|
|
451
|
+
### P2 全部 5 功能实现
|
|
452
|
+
|
|
453
|
+
**功能 1:Hooks 系统(pre/post tool execution)**
|
|
454
|
+
- 新增 `src/tools/hooks.ts`:`ToolHookConfig` 接口 + `runHook(template, vars)` 函数(模板变量替换 → execSync 5s 超时,失败 stderr 警告)
|
|
455
|
+
- `src/config/schema.ts`:新增 `hooks` 可选字段(`preToolExecution`/`postToolExecution`)
|
|
456
|
+
- `src/tools/executor.ts`:新增 `setConfig()` 方法;`execute()` 中 getDangerLevel 后调 pre hook,工具完成/失败后调 post hook
|
|
457
|
+
|
|
458
|
+
**功能 2:Permission Rules(基于规则的工具权限)**
|
|
459
|
+
- 新增 `src/tools/permissions.ts`:`PermissionRule` 接口 + `checkPermission()` 纯函数(首匹配规则,tool 支持 `*` 通配,when 条件含 dangerLevel/pathPattern)
|
|
460
|
+
- `src/config/schema.ts`:新增 `permissionRules` 数组 + `defaultPermission`(默认 `confirm`)
|
|
461
|
+
- `src/tools/executor.ts`:execute() 中 getDangerLevel 后调 checkPermission → deny 直接拒绝 / auto-approve 跳过 confirm / confirm 走原有流程
|
|
462
|
+
|
|
463
|
+
**功能 3:Checkpointing(会话检查点)**
|
|
464
|
+
- `src/session/session.ts`:新增 `CheckpointMeta` 接口(name/messageIndex/timestamp)、`checkpoints` 数组、4 个方法(createCheckpoint/restoreCheckpoint/listCheckpoints/deleteCheckpoint)、toJSON/fromJSON 更新
|
|
465
|
+
- `src/repl/commands/index.ts`:新增 `/checkpoint` 命令(save/restore/list/delete 子命令)
|
|
466
|
+
|
|
467
|
+
**功能 4:`/review` 代码审查**
|
|
468
|
+
- `src/repl/commands/index.ts`:新增 `buildReviewPrompt()` 辅助函数(中文结构化审查 prompt)+ `/review` 命令(`--staged`/`--detailed`,diff 超 8000 字截断保头尾)
|
|
469
|
+
|
|
470
|
+
**功能 5:Custom Commands(用户自定义命令)**
|
|
471
|
+
- 新增 `src/repl/custom-commands.ts`:`CustomCommandManager` 类(loadCommands/listCommands/getCommand)+ `expandTemplate()` 模板变量展开(`{{input}}/{{file:path}}/{{git-diff}}/{{git-context}}`)
|
|
472
|
+
- `src/core/constants.ts`:新增 `CUSTOM_COMMANDS_DIR_NAME = 'commands'`
|
|
473
|
+
- `src/repl/repl.ts`:CustomCommandManager 集成(初始化 + handleCommand fallback + ctx 方法)
|
|
474
|
+
- `src/repl/commands/index.ts`:新增 `/commands` 命令(list/reload)
|
|
475
|
+
|
|
476
|
+
**版本与收尾**
|
|
477
|
+
- `src/core/constants.ts`:VERSION `0.1.23` → `0.1.24`
|
|
478
|
+
- `package.json`:version 同步
|
|
479
|
+
- `src/repl/renderer.ts`:/about 命令计数 23 → 26,新增 5 条 P2 特性条目
|
|
480
|
+
- 所有 P2 路线图条目标记为已完成
|
|
481
|
+
|
|
482
|
+
---
|
|
483
|
+
|
|
338
484
|
## 本轮开发完成记录(2026-02-27,v0.1.21 → v0.1.22)
|
|
339
485
|
|
|
340
486
|
### 新增功能:Sub-Agent 子代理系统(`spawn_agent` 工具)
|
package/dist/index.js
CHANGED
|
@@ -84,6 +84,22 @@ var ConfigSchema = z.object({
|
|
|
84
84
|
env: z.record(z.string()).optional(),
|
|
85
85
|
timeout: z.number().default(3e4)
|
|
86
86
|
})).default({}),
|
|
87
|
+
// 工具执行钩子(shell 命令,模板变量:{tool} {dangerLevel} {args} {status})
|
|
88
|
+
hooks: z.object({
|
|
89
|
+
preToolExecution: z.string().optional(),
|
|
90
|
+
postToolExecution: z.string().optional()
|
|
91
|
+
}).optional(),
|
|
92
|
+
// 工具权限规则(按顺序匹配第一个生效)
|
|
93
|
+
permissionRules: z.array(z.object({
|
|
94
|
+
tool: z.string(),
|
|
95
|
+
action: z.enum(["auto-approve", "deny", "confirm"]),
|
|
96
|
+
when: z.object({
|
|
97
|
+
dangerLevel: z.enum(["safe", "write", "destructive"]).optional(),
|
|
98
|
+
pathPattern: z.string().optional()
|
|
99
|
+
}).optional()
|
|
100
|
+
})).default([]),
|
|
101
|
+
// 无规则匹配时的默认权限动作
|
|
102
|
+
defaultPermission: z.enum(["auto-approve", "deny", "confirm"]).default("confirm"),
|
|
87
103
|
// 插件加载开关(安全控制)
|
|
88
104
|
// 默认 false:不自动加载 ~/.aicli/plugins/ 中的插件文件。
|
|
89
105
|
// 插件以完整 Node.js 权限在主进程中执行(可读写文件、访问网络、执行命令),
|
|
@@ -129,13 +145,14 @@ var EnvLoader = class {
|
|
|
129
145
|
};
|
|
130
146
|
|
|
131
147
|
// src/core/constants.ts
|
|
132
|
-
var VERSION = "0.1.
|
|
148
|
+
var VERSION = "0.1.25";
|
|
133
149
|
var APP_NAME = "ai-cli";
|
|
134
150
|
var CONFIG_DIR_NAME = ".aicli";
|
|
135
151
|
var CONFIG_FILE_NAME = "config.json";
|
|
136
152
|
var HISTORY_DIR_NAME = "history";
|
|
137
153
|
var PLUGINS_DIR_NAME = "plugins";
|
|
138
154
|
var SKILLS_DIR_NAME = "skills";
|
|
155
|
+
var CUSTOM_COMMANDS_DIR_NAME = "commands";
|
|
139
156
|
var CONTEXT_FILE_CANDIDATES = ["AICLI.md", "CLAUDE.md"];
|
|
140
157
|
var MEMORY_FILE_NAME = "memory.md";
|
|
141
158
|
var MEMORY_MAX_CHARS = 1e4;
|
|
@@ -988,7 +1005,7 @@ var DeepSeekProvider = class extends OpenAICompatibleProvider {
|
|
|
988
1005
|
{
|
|
989
1006
|
id: "deepseek-chat",
|
|
990
1007
|
displayName: "DeepSeek Chat (V3)",
|
|
991
|
-
contextWindow:
|
|
1008
|
+
contextWindow: 131072,
|
|
992
1009
|
supportsStreaming: true
|
|
993
1010
|
},
|
|
994
1011
|
{
|
|
@@ -1348,6 +1365,7 @@ var Session = class _Session {
|
|
|
1348
1365
|
messages = [];
|
|
1349
1366
|
title;
|
|
1350
1367
|
tokenUsage = { inputTokens: 0, outputTokens: 0 };
|
|
1368
|
+
checkpoints = [];
|
|
1351
1369
|
constructor(id, provider, model) {
|
|
1352
1370
|
this.id = id;
|
|
1353
1371
|
this.provider = provider;
|
|
@@ -1387,6 +1405,32 @@ var Session = class _Session {
|
|
|
1387
1405
|
this.updated = /* @__PURE__ */ new Date();
|
|
1388
1406
|
return removedCount;
|
|
1389
1407
|
}
|
|
1408
|
+
/** 在当前消息位置创建检查点 */
|
|
1409
|
+
createCheckpoint(name) {
|
|
1410
|
+
this.checkpoints = this.checkpoints.filter((c) => c.name !== name);
|
|
1411
|
+
this.checkpoints.push({
|
|
1412
|
+
name,
|
|
1413
|
+
messageIndex: this.messages.length,
|
|
1414
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
1415
|
+
});
|
|
1416
|
+
}
|
|
1417
|
+
/** 恢复到指定检查点:截断消息到检查点位置,移除后续检查点 */
|
|
1418
|
+
restoreCheckpoint(name) {
|
|
1419
|
+
const cp = this.checkpoints.find((c) => c.name === name);
|
|
1420
|
+
if (!cp) return false;
|
|
1421
|
+
this.messages = this.messages.slice(0, cp.messageIndex);
|
|
1422
|
+
this.checkpoints = this.checkpoints.filter((c) => c.messageIndex <= cp.messageIndex);
|
|
1423
|
+
this.updated = /* @__PURE__ */ new Date();
|
|
1424
|
+
return true;
|
|
1425
|
+
}
|
|
1426
|
+
listCheckpoints() {
|
|
1427
|
+
return [...this.checkpoints];
|
|
1428
|
+
}
|
|
1429
|
+
deleteCheckpoint(name) {
|
|
1430
|
+
const len = this.checkpoints.length;
|
|
1431
|
+
this.checkpoints = this.checkpoints.filter((c) => c.name !== name);
|
|
1432
|
+
return this.checkpoints.length < len;
|
|
1433
|
+
}
|
|
1390
1434
|
getMeta() {
|
|
1391
1435
|
return {
|
|
1392
1436
|
id: this.id,
|
|
@@ -1407,6 +1451,11 @@ var Session = class _Session {
|
|
|
1407
1451
|
updated: this.updated.toISOString(),
|
|
1408
1452
|
title: this.title,
|
|
1409
1453
|
tokenUsage: { ...this.tokenUsage },
|
|
1454
|
+
checkpoints: this.checkpoints.map((c) => ({
|
|
1455
|
+
name: c.name,
|
|
1456
|
+
messageIndex: c.messageIndex,
|
|
1457
|
+
timestamp: c.timestamp.toISOString()
|
|
1458
|
+
})),
|
|
1410
1459
|
messages: this.messages.map((m) => ({
|
|
1411
1460
|
...m,
|
|
1412
1461
|
timestamp: m.timestamp.toISOString()
|
|
@@ -1441,6 +1490,13 @@ var Session = class _Session {
|
|
|
1441
1490
|
outputTokens: typeof tu.outputTokens === "number" ? tu.outputTokens : 0
|
|
1442
1491
|
};
|
|
1443
1492
|
}
|
|
1493
|
+
if (Array.isArray(d.checkpoints)) {
|
|
1494
|
+
session.checkpoints = d.checkpoints.map((c) => ({
|
|
1495
|
+
name: String(c.name ?? ""),
|
|
1496
|
+
messageIndex: typeof c.messageIndex === "number" ? c.messageIndex : 0,
|
|
1497
|
+
timestamp: new Date(c.timestamp)
|
|
1498
|
+
}));
|
|
1499
|
+
}
|
|
1444
1500
|
session.messages = d.messages.map((m) => {
|
|
1445
1501
|
const ts = new Date(m.timestamp);
|
|
1446
1502
|
return {
|
|
@@ -1564,8 +1620,8 @@ var SessionManager = class {
|
|
|
1564
1620
|
|
|
1565
1621
|
// src/repl/repl.ts
|
|
1566
1622
|
import * as readline from "readline";
|
|
1567
|
-
import { existsSync as
|
|
1568
|
-
import { join as
|
|
1623
|
+
import { existsSync as existsSync18, readFileSync as readFileSync13, readdirSync as readdirSync9, statSync as statSync6 } from "fs";
|
|
1624
|
+
import { join as join13, resolve as resolve4, extname as extname4, dirname as dirname5, basename as basename5 } from "path";
|
|
1569
1625
|
import chalk10 from "chalk";
|
|
1570
1626
|
|
|
1571
1627
|
// src/repl/renderer.ts
|
|
@@ -1669,10 +1725,11 @@ var Renderer = class {
|
|
|
1669
1725
|
console.log(tool("write_todos", "\u62C6\u89E3\u4EFB\u52A1\u4E3A\u5B50\u4EFB\u52A1\u5217\u8868\uFF0C\u5B9E\u65F6\u663E\u793A\u8FDB\u5EA6"));
|
|
1670
1726
|
console.log(tool("spawn_agent", "\u59D4\u6D3E\u72EC\u7ACB\u5B50\u4EE3\u7406\u6267\u884C\u7279\u5B9A\u4EFB\u52A1\uFF08\u9694\u79BB\u5BF9\u8BDD + \u81EA\u52A8\u5DE5\u5177\u8C03\u7528\u5FAA\u73AF\uFF09"));
|
|
1671
1727
|
console.log(HR);
|
|
1672
|
-
console.log(chalk.gray(" REPL \u547D\u4EE4\
|
|
1728
|
+
console.log(chalk.gray(" REPL \u547D\u4EE4\uFF0826\u4E2A\uFF09\uFF1A"));
|
|
1673
1729
|
console.log(chalk.dim(" /help /about /provider /model /clear /compact /plan /session"));
|
|
1674
1730
|
console.log(chalk.dim(" /system /context /status /search /undo /export /copy /cost"));
|
|
1675
|
-
console.log(chalk.dim(" /init /skill /tools /plugins /mcp /config /
|
|
1731
|
+
console.log(chalk.dim(" /init /skill /tools /plugins /mcp /config /checkpoint /review"));
|
|
1732
|
+
console.log(chalk.dim(" /commands /exit"));
|
|
1676
1733
|
console.log(HR);
|
|
1677
1734
|
console.log(chalk.gray(" \u4E3B\u8981\u7279\u6027\uFF1A"));
|
|
1678
1735
|
console.log(feat("Agentic \u5FAA\u73AF\uFF08\u6700\u591A 20 \u8F6E\u5DE5\u5177\u8C03\u7528\uFF0C\u6700\u7EC8\u56DE\u7B54\u6D41\u5F0F\u8F93\u51FA\uFF09"));
|
|
@@ -1693,6 +1750,13 @@ var Renderer = class {
|
|
|
1693
1750
|
console.log(feat("Agent Skills\uFF1A~/.aicli/skills/ \u53EF\u590D\u7528\u6280\u80FD\u5305\uFF0C\u6CE8\u5165 system prompt + \u5DE5\u5177\u767D\u540D\u5355"));
|
|
1694
1751
|
console.log(feat("/init \u9879\u76EE\u521D\u59CB\u5316\uFF1A\u626B\u63CF\u9879\u76EE\u7ED3\u6784\uFF0CAI \u751F\u6210 AICLI.md \u4E0A\u4E0B\u6587\u6587\u4EF6"));
|
|
1695
1752
|
console.log(feat("\u591A\u6587\u4EF6\u7F16\u8F91\u9884\u89C8\uFF1A\u6279\u91CF diff preview + \u9009\u62E9\u6027 approve/reject"));
|
|
1753
|
+
console.log(feat("Hooks \u7CFB\u7EDF\uFF1Apre/post \u5DE5\u5177\u6267\u884C\u94A9\u5B50\uFF08shell \u547D\u4EE4\uFF0C\u6A21\u677F\u53D8\u91CF\u66FF\u6362\uFF09"));
|
|
1754
|
+
console.log(feat("Permission Rules\uFF1A\u57FA\u4E8E\u89C4\u5219\u7684\u5DE5\u5177\u6743\u9650\u63A7\u5236\uFF08auto-approve/deny/confirm\uFF09"));
|
|
1755
|
+
console.log(feat("Checkpointing\uFF1A/checkpoint \u4F1A\u8BDD\u68C0\u67E5\u70B9\u4FDD\u5B58/\u6062\u590D/\u5217\u8868/\u5220\u9664"));
|
|
1756
|
+
console.log(feat("/review \u4EE3\u7801\u5BA1\u67E5\uFF1A\u8BFB\u53D6 git diff\uFF0CAI \u751F\u6210\u7ED3\u6784\u5316\u5BA1\u67E5\u610F\u89C1"));
|
|
1757
|
+
console.log(feat("Custom Commands\uFF1A~/.aicli/commands/*.md \u7528\u6237\u81EA\u5B9A\u4E49 REPL \u547D\u4EE4"));
|
|
1758
|
+
console.log(feat("Tab \u81EA\u52A8\u8865\u5168\uFF1A\u547D\u4EE4\u540D/\u5B50\u547D\u4EE4\u53C2\u6570/@\u6587\u4EF6\u8DEF\u5F84\uFF0C\u6309 Tab \u89E6\u53D1"));
|
|
1759
|
+
console.log(feat("\u6D41\u5F0F Token \u8BA1\u6570\uFF1A\u6D41\u5F0F\u8F93\u51FA\u7ED3\u675F\u540E\u7ACB\u5373\u5185\u8054\u663E\u793A\u7CBE\u786E/\u4F30\u7B97 token \u6570"));
|
|
1696
1760
|
console.log(feat("\u72EC\u7ACB\u53EF\u6267\u884C\u6587\u4EF6\u6253\u5305\uFF08~56MB\uFF0C\u65E0\u9700 Node.js \u73AF\u5883\uFF09"));
|
|
1697
1761
|
console.log();
|
|
1698
1762
|
}
|
|
@@ -1755,7 +1819,26 @@ var Renderer = class {
|
|
|
1755
1819
|
if (fileStream) fileStream.write(chunk.delta);
|
|
1756
1820
|
flushBuf();
|
|
1757
1821
|
}
|
|
1758
|
-
|
|
1822
|
+
let tokensShown = false;
|
|
1823
|
+
if (options?.showTokens) {
|
|
1824
|
+
process.stdout.write("\n");
|
|
1825
|
+
if (usage) {
|
|
1826
|
+
const sessionTotal = options.sessionTotal;
|
|
1827
|
+
const updatedTotal = sessionTotal ? { inputTokens: sessionTotal.inputTokens + usage.inputTokens, outputTokens: sessionTotal.outputTokens + usage.outputTokens } : void 0;
|
|
1828
|
+
this.renderUsage(usage, updatedTotal);
|
|
1829
|
+
tokensShown = true;
|
|
1830
|
+
} else if (fullContent.length > 0) {
|
|
1831
|
+
const est = Math.ceil(fullContent.length / 2.5);
|
|
1832
|
+
process.stdout.write(chalk.dim(`\u{1F4CA} ~${est.toLocaleString()} output tokens (estimated)
|
|
1833
|
+
|
|
1834
|
+
`));
|
|
1835
|
+
tokensShown = true;
|
|
1836
|
+
} else {
|
|
1837
|
+
process.stdout.write("\n");
|
|
1838
|
+
}
|
|
1839
|
+
} else {
|
|
1840
|
+
process.stdout.write("\n\n");
|
|
1841
|
+
}
|
|
1759
1842
|
if (fileStream) {
|
|
1760
1843
|
await new Promise((resolve5, reject) => {
|
|
1761
1844
|
fileStream.end((err) => err ? reject(err) : resolve5());
|
|
@@ -1765,7 +1848,7 @@ var Renderer = class {
|
|
|
1765
1848
|
|
|
1766
1849
|
`));
|
|
1767
1850
|
}
|
|
1768
|
-
return { content: fullContent, usage };
|
|
1851
|
+
return { content: fullContent, usage, tokensShown };
|
|
1769
1852
|
}
|
|
1770
1853
|
renderResponse(content) {
|
|
1771
1854
|
process.stdout.write(chalk.cyan("Assistant: "));
|
|
@@ -2201,6 +2284,29 @@ function copyToClipboard(text) {
|
|
|
2201
2284
|
}
|
|
2202
2285
|
}
|
|
2203
2286
|
}
|
|
2287
|
+
function buildReviewPrompt(diff, gitContextStr, detailed) {
|
|
2288
|
+
const level = detailed ? "\u8BF7\u8FDB\u884C\u8BE6\u7EC6\u6DF1\u5EA6\u5BA1\u67E5\uFF0C\u5305\u62EC\uFF1A\u5B89\u5168\u6027\u3001\u6027\u80FD\u3001\u53EF\u7EF4\u62A4\u6027\u3001\u9519\u8BEF\u5904\u7406\u3001\u547D\u540D\u89C4\u8303\u3001\u4EE3\u7801\u91CD\u590D\u3002" : "\u8BF7\u8FDB\u884C\u7B80\u660E\u4EE3\u7801\u5BA1\u67E5\uFF0C\u805A\u7126\u4E8E bug\u3001\u5B89\u5168\u95EE\u9898\u548C\u5173\u952E\u6539\u8FDB\u5EFA\u8BAE\u3002";
|
|
2289
|
+
return `# \u4EE3\u7801\u5BA1\u67E5\u8BF7\u6C42
|
|
2290
|
+
|
|
2291
|
+
${level}
|
|
2292
|
+
|
|
2293
|
+
## Git \u72B6\u6001
|
|
2294
|
+
${gitContextStr}
|
|
2295
|
+
|
|
2296
|
+
## \u4EE3\u7801\u53D8\u66F4\uFF08diff\uFF09
|
|
2297
|
+
\`\`\`diff
|
|
2298
|
+
${diff}
|
|
2299
|
+
\`\`\`
|
|
2300
|
+
|
|
2301
|
+
## \u8F93\u51FA\u683C\u5F0F
|
|
2302
|
+
\u8BF7\u6309\u4EE5\u4E0B\u7ED3\u6784\u8F93\u51FA\u5BA1\u67E5\u610F\u89C1\uFF1A
|
|
2303
|
+
1. **\u603B\u8BC4**\uFF1A\u4E00\u53E5\u8BDD\u6982\u62EC\u53D8\u66F4\u8D28\u91CF
|
|
2304
|
+
2. **\u95EE\u9898\u5217\u8868**\uFF08\u5982\u6709\uFF09\uFF1A\u6BCF\u4E2A\u95EE\u9898\u5305\u542B [\u4E25\u91CD\u7A0B\u5EA6] \u6587\u4EF6:\u884C\u53F7 \u2014 \u95EE\u9898\u63CF\u8FF0 + \u5EFA\u8BAE\u4FEE\u590D
|
|
2305
|
+
3. **\u6539\u8FDB\u5EFA\u8BAE**\uFF08\u5982\u6709\uFF09\uFF1A\u975E\u5FC5\u987B\u4F46\u63A8\u8350\u7684\u4F18\u5316
|
|
2306
|
+
4. **\u4EAE\u70B9**\uFF08\u5982\u6709\uFF09\uFF1A\u503C\u5F97\u80AF\u5B9A\u7684\u597D\u5B9E\u8DF5
|
|
2307
|
+
|
|
2308
|
+
\u4E25\u91CD\u7A0B\u5EA6\u5206\u7EA7\uFF1A\u{1F534} Critical / \u{1F7E1} Warning / \u{1F535} Info`;
|
|
2309
|
+
}
|
|
2204
2310
|
var CommandRegistry = class {
|
|
2205
2311
|
commands = /* @__PURE__ */ new Map();
|
|
2206
2312
|
register(command) {
|
|
@@ -2245,6 +2351,9 @@ function createDefaultCommands() {
|
|
|
2245
2351
|
" /cost [reset] - Show session token usage (or reset counters)",
|
|
2246
2352
|
" /init [--force] - Generate AICLI.md by scanning project structure",
|
|
2247
2353
|
" /skill [name|off|list] - Manage agent skills (reusable prompt packs)",
|
|
2354
|
+
" /checkpoint [save|restore|delete] <name> - Session checkpoints",
|
|
2355
|
+
" /review [--staged] [--detailed] - AI code review from git diff",
|
|
2356
|
+
" /commands [reload] - List/reload custom commands (~/.aicli/commands/)",
|
|
2248
2357
|
" /exit - Exit"
|
|
2249
2358
|
] : [];
|
|
2250
2359
|
console.log("\nAvailable commands:");
|
|
@@ -2943,6 +3052,149 @@ ${text}
|
|
|
2943
3052
|
console.log();
|
|
2944
3053
|
}
|
|
2945
3054
|
},
|
|
3055
|
+
// ── /checkpoint ────────────────────────────────────────────
|
|
3056
|
+
{
|
|
3057
|
+
name: "checkpoint",
|
|
3058
|
+
description: "Manage session checkpoints (save/restore/list/delete)",
|
|
3059
|
+
usage: "/checkpoint [save <name> | restore <name> | delete <name>]",
|
|
3060
|
+
execute(args, ctx) {
|
|
3061
|
+
const session = ctx.sessions.current;
|
|
3062
|
+
if (!session) {
|
|
3063
|
+
ctx.renderer.renderError("No active session.");
|
|
3064
|
+
return;
|
|
3065
|
+
}
|
|
3066
|
+
const sub = args[0]?.toLowerCase();
|
|
3067
|
+
if (!sub || sub === "list") {
|
|
3068
|
+
const cps = ctx.listCheckpoints();
|
|
3069
|
+
if (cps.length === 0) {
|
|
3070
|
+
console.log(chalk2.dim(" No checkpoints. Use /checkpoint save <name> to create one."));
|
|
3071
|
+
return;
|
|
3072
|
+
}
|
|
3073
|
+
console.log(chalk2.gray(` Checkpoints (${cps.length}):`));
|
|
3074
|
+
for (const cp of cps) {
|
|
3075
|
+
const ts = cp.timestamp.toLocaleString();
|
|
3076
|
+
console.log(chalk2.cyan(` ${cp.name.padEnd(20)} `) + chalk2.dim(`msg #${cp.messageIndex} ${ts}`));
|
|
3077
|
+
}
|
|
3078
|
+
console.log();
|
|
3079
|
+
return;
|
|
3080
|
+
}
|
|
3081
|
+
const name = args.slice(1).join(" ").trim();
|
|
3082
|
+
if (sub === "save") {
|
|
3083
|
+
if (!name) {
|
|
3084
|
+
ctx.renderer.renderError("Usage: /checkpoint save <name>");
|
|
3085
|
+
return;
|
|
3086
|
+
}
|
|
3087
|
+
ctx.createCheckpoint(name);
|
|
3088
|
+
console.log(chalk2.green(` \u2713 Checkpoint "${name}" saved at message #${session.messages.length}`));
|
|
3089
|
+
return;
|
|
3090
|
+
}
|
|
3091
|
+
if (sub === "restore") {
|
|
3092
|
+
if (!name) {
|
|
3093
|
+
ctx.renderer.renderError("Usage: /checkpoint restore <name>");
|
|
3094
|
+
return;
|
|
3095
|
+
}
|
|
3096
|
+
const ok = ctx.restoreCheckpoint(name);
|
|
3097
|
+
if (ok) {
|
|
3098
|
+
console.log(chalk2.green(` \u2713 Restored to checkpoint "${name}" (${session.messages.length} messages)`));
|
|
3099
|
+
} else {
|
|
3100
|
+
ctx.renderer.renderError(`Checkpoint "${name}" not found.`);
|
|
3101
|
+
}
|
|
3102
|
+
return;
|
|
3103
|
+
}
|
|
3104
|
+
if (sub === "delete") {
|
|
3105
|
+
if (!name) {
|
|
3106
|
+
ctx.renderer.renderError("Usage: /checkpoint delete <name>");
|
|
3107
|
+
return;
|
|
3108
|
+
}
|
|
3109
|
+
const ok = ctx.deleteCheckpoint(name);
|
|
3110
|
+
if (ok) {
|
|
3111
|
+
console.log(chalk2.green(` \u2713 Deleted checkpoint "${name}"`));
|
|
3112
|
+
} else {
|
|
3113
|
+
ctx.renderer.renderError(`Checkpoint "${name}" not found.`);
|
|
3114
|
+
}
|
|
3115
|
+
return;
|
|
3116
|
+
}
|
|
3117
|
+
ctx.renderer.renderError(`Unknown subcommand: ${sub}. Use save/restore/list/delete.`);
|
|
3118
|
+
}
|
|
3119
|
+
},
|
|
3120
|
+
// ── /review ──────────────────────────────────────────────────
|
|
3121
|
+
{
|
|
3122
|
+
name: "review",
|
|
3123
|
+
description: "AI code review based on git diff",
|
|
3124
|
+
usage: "/review [--staged] [--detailed]",
|
|
3125
|
+
async execute(args, ctx) {
|
|
3126
|
+
const gitCtx = getGitContext();
|
|
3127
|
+
if (!gitCtx) {
|
|
3128
|
+
ctx.renderer.renderError("Not a git repository.");
|
|
3129
|
+
return;
|
|
3130
|
+
}
|
|
3131
|
+
const staged = args.includes("--staged");
|
|
3132
|
+
const detailed = args.includes("--detailed");
|
|
3133
|
+
let diff;
|
|
3134
|
+
try {
|
|
3135
|
+
const cmd = staged ? "git diff --staged" : "git diff";
|
|
3136
|
+
diff = execSync2(cmd, { encoding: "utf-8", timeout: 1e4 }).trim();
|
|
3137
|
+
} catch {
|
|
3138
|
+
ctx.renderer.renderError("Failed to run git diff.");
|
|
3139
|
+
return;
|
|
3140
|
+
}
|
|
3141
|
+
if (!diff) {
|
|
3142
|
+
console.log(chalk2.dim(" No changes to review." + (staged ? "" : " Try --staged for staged changes.")));
|
|
3143
|
+
return;
|
|
3144
|
+
}
|
|
3145
|
+
const MAX_DIFF = 8e3;
|
|
3146
|
+
let truncated = false;
|
|
3147
|
+
if (diff.length > MAX_DIFF) {
|
|
3148
|
+
const head = diff.slice(0, Math.floor(MAX_DIFF * 0.7));
|
|
3149
|
+
const tail = diff.slice(diff.length - Math.floor(MAX_DIFF * 0.2));
|
|
3150
|
+
diff = head + "\n\n... [diff \u5DF2\u622A\u65AD\uFF0C\u5171 " + diff.length + " \u5B57\u7B26] ...\n\n" + tail;
|
|
3151
|
+
truncated = true;
|
|
3152
|
+
}
|
|
3153
|
+
const prompt = buildReviewPrompt(diff, formatGitContextForPrompt(gitCtx), detailed);
|
|
3154
|
+
console.log(chalk2.dim(" Analyzing changes..."));
|
|
3155
|
+
try {
|
|
3156
|
+
const review = await ctx.chatOnce(prompt, { temperature: 0.3, maxTokens: 8192 });
|
|
3157
|
+
console.log();
|
|
3158
|
+
console.log(review);
|
|
3159
|
+
console.log();
|
|
3160
|
+
if (truncated) {
|
|
3161
|
+
console.log(chalk2.yellow(" \u26A0 Diff was truncated. Consider reviewing smaller changesets."));
|
|
3162
|
+
}
|
|
3163
|
+
} catch (err) {
|
|
3164
|
+
ctx.renderer.renderError(`Review failed: ${err.message}`);
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
},
|
|
3168
|
+
// ── /commands ─────────────────────────────────────────────────
|
|
3169
|
+
{
|
|
3170
|
+
name: "commands",
|
|
3171
|
+
description: "List or reload custom commands from ~/.aicli/commands/",
|
|
3172
|
+
usage: "/commands [reload]",
|
|
3173
|
+
execute(args, ctx) {
|
|
3174
|
+
const mgr = ctx.getCustomCommandManager();
|
|
3175
|
+
if (!mgr) {
|
|
3176
|
+
console.log(chalk2.dim(" Custom commands not initialized."));
|
|
3177
|
+
return;
|
|
3178
|
+
}
|
|
3179
|
+
const sub = args[0]?.toLowerCase();
|
|
3180
|
+
if (sub === "reload") {
|
|
3181
|
+
const count = mgr.loadCommands();
|
|
3182
|
+
console.log(chalk2.green(` \u2713 Reloaded ${count} custom command(s).`));
|
|
3183
|
+
return;
|
|
3184
|
+
}
|
|
3185
|
+
const cmds = mgr.listCommands();
|
|
3186
|
+
if (cmds.length === 0) {
|
|
3187
|
+
console.log(chalk2.dim(" No custom commands found in ~/.aicli/commands/"));
|
|
3188
|
+
console.log(chalk2.dim(" Create .md files with YAML frontmatter (name/description/usage)."));
|
|
3189
|
+
return;
|
|
3190
|
+
}
|
|
3191
|
+
console.log(chalk2.gray(` Custom commands (${cmds.length}):`));
|
|
3192
|
+
for (const cmd of cmds) {
|
|
3193
|
+
console.log(chalk2.cyan(` /${cmd.meta.name.padEnd(18)} `) + chalk2.dim(cmd.meta.description));
|
|
3194
|
+
}
|
|
3195
|
+
console.log();
|
|
3196
|
+
}
|
|
3197
|
+
},
|
|
2946
3198
|
{
|
|
2947
3199
|
name: "exit",
|
|
2948
3200
|
description: "Exit the REPL",
|
|
@@ -5172,6 +5424,43 @@ function simpleDiff(oldLines, newLines) {
|
|
|
5172
5424
|
return result;
|
|
5173
5425
|
}
|
|
5174
5426
|
|
|
5427
|
+
// src/tools/hooks.ts
|
|
5428
|
+
import { execSync as execSync4 } from "child_process";
|
|
5429
|
+
function runHook(template, vars) {
|
|
5430
|
+
if (!template) return;
|
|
5431
|
+
let cmd = template;
|
|
5432
|
+
cmd = cmd.replace(/\{tool\}/g, vars.tool);
|
|
5433
|
+
cmd = cmd.replace(/\{dangerLevel\}/g, vars.dangerLevel ?? "");
|
|
5434
|
+
cmd = cmd.replace(/\{args\}/g, vars.args ?? "");
|
|
5435
|
+
cmd = cmd.replace(/\{status\}/g, vars.status ?? "");
|
|
5436
|
+
try {
|
|
5437
|
+
execSync4(cmd, {
|
|
5438
|
+
timeout: 5e3,
|
|
5439
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
5440
|
+
encoding: "utf-8"
|
|
5441
|
+
});
|
|
5442
|
+
} catch {
|
|
5443
|
+
process.stderr.write(`\u26A0 Hook failed: ${cmd.slice(0, 100)}
|
|
5444
|
+
`);
|
|
5445
|
+
}
|
|
5446
|
+
}
|
|
5447
|
+
|
|
5448
|
+
// src/tools/permissions.ts
|
|
5449
|
+
function checkPermission(toolName, args, dangerLevel, rules, defaultAction = "confirm") {
|
|
5450
|
+
for (const rule of rules) {
|
|
5451
|
+
if (rule.tool !== "*" && rule.tool !== toolName) continue;
|
|
5452
|
+
if (rule.when) {
|
|
5453
|
+
if (rule.when.dangerLevel && rule.when.dangerLevel !== dangerLevel) continue;
|
|
5454
|
+
if (rule.when.pathPattern) {
|
|
5455
|
+
const path = String(args["path"] ?? args["command"] ?? "");
|
|
5456
|
+
if (!path.includes(rule.when.pathPattern)) continue;
|
|
5457
|
+
}
|
|
5458
|
+
}
|
|
5459
|
+
return rule.action;
|
|
5460
|
+
}
|
|
5461
|
+
return defaultAction;
|
|
5462
|
+
}
|
|
5463
|
+
|
|
5175
5464
|
// src/tools/executor.ts
|
|
5176
5465
|
var MAX_TOOL_OUTPUT_CHARS2 = 12e3;
|
|
5177
5466
|
function truncateOutput2(content, toolName) {
|
|
@@ -5224,6 +5513,17 @@ var ToolExecutor = class {
|
|
|
5224
5513
|
setReadline(rl) {
|
|
5225
5514
|
this.rl = rl;
|
|
5226
5515
|
}
|
|
5516
|
+
/** 钩子配置(可选) */
|
|
5517
|
+
hookConfig;
|
|
5518
|
+
/** 权限规则(可选) */
|
|
5519
|
+
permissionRules = [];
|
|
5520
|
+
defaultPermission = "confirm";
|
|
5521
|
+
/** 注入 hooks 和 permission rules 配置 */
|
|
5522
|
+
setConfig(opts) {
|
|
5523
|
+
this.hookConfig = opts.hookConfig;
|
|
5524
|
+
if (opts.permissionRules) this.permissionRules = opts.permissionRules;
|
|
5525
|
+
if (opts.defaultPermission) this.defaultPermission = opts.defaultPermission;
|
|
5526
|
+
}
|
|
5227
5527
|
async execute(call) {
|
|
5228
5528
|
const tool = this.registry.get(call.name);
|
|
5229
5529
|
if (!tool) {
|
|
@@ -5234,6 +5534,33 @@ var ToolExecutor = class {
|
|
|
5234
5534
|
};
|
|
5235
5535
|
}
|
|
5236
5536
|
const dangerLevel = getDangerLevel(call.name, call.arguments);
|
|
5537
|
+
runHook(this.hookConfig?.preToolExecution, {
|
|
5538
|
+
tool: call.name,
|
|
5539
|
+
dangerLevel,
|
|
5540
|
+
args: JSON.stringify(call.arguments).slice(0, 200)
|
|
5541
|
+
});
|
|
5542
|
+
if (this.permissionRules.length > 0) {
|
|
5543
|
+
const action = checkPermission(call.name, call.arguments, dangerLevel, this.permissionRules, this.defaultPermission);
|
|
5544
|
+
if (action === "deny") {
|
|
5545
|
+
return { callId: call.id, content: `Permission denied by rule for tool: ${call.name}`, isError: false };
|
|
5546
|
+
}
|
|
5547
|
+
if (action === "auto-approve") {
|
|
5548
|
+
this.printToolCall(call);
|
|
5549
|
+
try {
|
|
5550
|
+
const rawContent = await tool.execute(call.arguments);
|
|
5551
|
+
const content = truncateOutput2(rawContent, call.name);
|
|
5552
|
+
const wasTruncated = content !== rawContent;
|
|
5553
|
+
this.printToolResult(call.name, rawContent, false, wasTruncated);
|
|
5554
|
+
runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
|
|
5555
|
+
return { callId: call.id, content, isError: false };
|
|
5556
|
+
} catch (err) {
|
|
5557
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
5558
|
+
this.printToolResult(call.name, message, true, false);
|
|
5559
|
+
runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "error" });
|
|
5560
|
+
return { callId: call.id, content: message, isError: true };
|
|
5561
|
+
}
|
|
5562
|
+
}
|
|
5563
|
+
}
|
|
5237
5564
|
if (dangerLevel === "write") {
|
|
5238
5565
|
this.printToolCall(call);
|
|
5239
5566
|
this.printDiffPreview(call);
|
|
@@ -5263,10 +5590,12 @@ var ToolExecutor = class {
|
|
|
5263
5590
|
const content = truncateOutput2(rawContent, call.name);
|
|
5264
5591
|
const wasTruncated = content !== rawContent;
|
|
5265
5592
|
this.printToolResult(call.name, rawContent, false, wasTruncated);
|
|
5593
|
+
runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
|
|
5266
5594
|
return { callId: call.id, content, isError: false };
|
|
5267
5595
|
} catch (err) {
|
|
5268
5596
|
const message = err instanceof Error ? err.message : String(err);
|
|
5269
5597
|
this.printToolResult(call.name, message, true, false);
|
|
5598
|
+
runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "error" });
|
|
5270
5599
|
return { callId: call.id, content: message, isError: true };
|
|
5271
5600
|
}
|
|
5272
5601
|
}
|
|
@@ -5783,9 +6112,98 @@ Managing ${displayName} API Key`);
|
|
|
5783
6112
|
}
|
|
5784
6113
|
};
|
|
5785
6114
|
|
|
6115
|
+
// src/repl/custom-commands.ts
|
|
6116
|
+
import { existsSync as existsSync15, readFileSync as readFileSync10, readdirSync as readdirSync7, mkdirSync as mkdirSync9 } from "fs";
|
|
6117
|
+
import { join as join10, extname as extname3 } from "path";
|
|
6118
|
+
import { execSync as execSync5 } from "child_process";
|
|
6119
|
+
function parseSimpleYaml(text) {
|
|
6120
|
+
const result = {};
|
|
6121
|
+
for (const line of text.split("\n")) {
|
|
6122
|
+
const match = line.match(/^(\w+)\s*:\s*(.+)$/);
|
|
6123
|
+
if (match) {
|
|
6124
|
+
result[match[1]] = match[2].trim().replace(/^['"]|['"]$/g, "");
|
|
6125
|
+
}
|
|
6126
|
+
}
|
|
6127
|
+
return result;
|
|
6128
|
+
}
|
|
6129
|
+
function parseCommandFile(filePath) {
|
|
6130
|
+
let content;
|
|
6131
|
+
try {
|
|
6132
|
+
content = readFileSync10(filePath, "utf-8");
|
|
6133
|
+
} catch {
|
|
6134
|
+
return null;
|
|
6135
|
+
}
|
|
6136
|
+
const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
|
|
6137
|
+
if (!fmMatch) return null;
|
|
6138
|
+
const yaml = parseSimpleYaml(fmMatch[1]);
|
|
6139
|
+
const name = yaml["name"];
|
|
6140
|
+
if (!name) return null;
|
|
6141
|
+
return {
|
|
6142
|
+
meta: {
|
|
6143
|
+
name,
|
|
6144
|
+
description: yaml["description"] ?? "",
|
|
6145
|
+
usage: yaml["usage"] ?? `/${name}`
|
|
6146
|
+
},
|
|
6147
|
+
promptTemplate: fmMatch[2].trim(),
|
|
6148
|
+
filePath
|
|
6149
|
+
};
|
|
6150
|
+
}
|
|
6151
|
+
function expandTemplate(template, args) {
|
|
6152
|
+
let result = template;
|
|
6153
|
+
result = result.replace(/\{\{input\}\}/g, args.join(" "));
|
|
6154
|
+
result = result.replace(/\{\{file:([^}]+)\}\}/g, (_m, p) => {
|
|
6155
|
+
try {
|
|
6156
|
+
return readFileSync10(p.trim(), "utf-8");
|
|
6157
|
+
} catch {
|
|
6158
|
+
return `[Error: cannot read ${p.trim()}]`;
|
|
6159
|
+
}
|
|
6160
|
+
});
|
|
6161
|
+
result = result.replace(/\{\{git-diff\}\}/g, () => {
|
|
6162
|
+
try {
|
|
6163
|
+
return execSync5("git diff", { encoding: "utf-8", timeout: 1e4 }).trim();
|
|
6164
|
+
} catch {
|
|
6165
|
+
return "[No git diff available]";
|
|
6166
|
+
}
|
|
6167
|
+
});
|
|
6168
|
+
result = result.replace(/\{\{git-context\}\}/g, () => {
|
|
6169
|
+
const ctx = getGitContext();
|
|
6170
|
+
return ctx ? formatGitContextForPrompt(ctx) : "[Not a git repository]";
|
|
6171
|
+
});
|
|
6172
|
+
return result;
|
|
6173
|
+
}
|
|
6174
|
+
var CustomCommandManager = class {
|
|
6175
|
+
constructor(commandsDir) {
|
|
6176
|
+
this.commandsDir = commandsDir;
|
|
6177
|
+
}
|
|
6178
|
+
commands = /* @__PURE__ */ new Map();
|
|
6179
|
+
loadCommands() {
|
|
6180
|
+
this.commands.clear();
|
|
6181
|
+
if (!existsSync15(this.commandsDir)) {
|
|
6182
|
+
mkdirSync9(this.commandsDir, { recursive: true });
|
|
6183
|
+
return 0;
|
|
6184
|
+
}
|
|
6185
|
+
let count = 0;
|
|
6186
|
+
for (const file of readdirSync7(this.commandsDir)) {
|
|
6187
|
+
if (extname3(file) !== ".md") continue;
|
|
6188
|
+
const cmd = parseCommandFile(join10(this.commandsDir, file));
|
|
6189
|
+
if (cmd) {
|
|
6190
|
+
this.commands.set(cmd.meta.name, cmd);
|
|
6191
|
+
count++;
|
|
6192
|
+
}
|
|
6193
|
+
}
|
|
6194
|
+
return count;
|
|
6195
|
+
}
|
|
6196
|
+
listCommands() {
|
|
6197
|
+
return [...this.commands.values()];
|
|
6198
|
+
}
|
|
6199
|
+
getCommand(name) {
|
|
6200
|
+
return this.commands.get(name);
|
|
6201
|
+
}
|
|
6202
|
+
};
|
|
6203
|
+
|
|
5786
6204
|
// src/repl/dev-state.ts
|
|
5787
|
-
import { existsSync as
|
|
5788
|
-
import { join as
|
|
6205
|
+
import { existsSync as existsSync16, readFileSync as readFileSync11, writeFileSync as writeFileSync8, unlinkSync as unlinkSync2, mkdirSync as mkdirSync10 } from "fs";
|
|
6206
|
+
import { join as join11 } from "path";
|
|
5789
6207
|
import { homedir as homedir4 } from "os";
|
|
5790
6208
|
var DEV_STATE_MAX_CHARS = 4e3;
|
|
5791
6209
|
var SNAPSHOT_PROMPT = `You are about to be replaced by a different AI model. Please generate a structured development state snapshot so the next model can continue seamlessly.
|
|
@@ -5824,12 +6242,12 @@ function sessionHasMeaningfulContent(messages) {
|
|
|
5824
6242
|
return hasUser && hasAssistant;
|
|
5825
6243
|
}
|
|
5826
6244
|
function getDevStatePath() {
|
|
5827
|
-
return
|
|
6245
|
+
return join11(homedir4(), CONFIG_DIR_NAME, DEV_STATE_FILE_NAME);
|
|
5828
6246
|
}
|
|
5829
6247
|
function saveDevState(content) {
|
|
5830
|
-
const configDir =
|
|
5831
|
-
if (!
|
|
5832
|
-
|
|
6248
|
+
const configDir = join11(homedir4(), CONFIG_DIR_NAME);
|
|
6249
|
+
if (!existsSync16(configDir)) {
|
|
6250
|
+
mkdirSync10(configDir, { recursive: true });
|
|
5833
6251
|
}
|
|
5834
6252
|
let trimmed = content.trim();
|
|
5835
6253
|
if (trimmed.length > DEV_STATE_MAX_CHARS) {
|
|
@@ -5844,13 +6262,13 @@ function saveDevState(content) {
|
|
|
5844
6262
|
}
|
|
5845
6263
|
function loadDevState() {
|
|
5846
6264
|
const path = getDevStatePath();
|
|
5847
|
-
if (!
|
|
5848
|
-
const content =
|
|
6265
|
+
if (!existsSync16(path)) return null;
|
|
6266
|
+
const content = readFileSync11(path, "utf-8").trim();
|
|
5849
6267
|
return content || null;
|
|
5850
6268
|
}
|
|
5851
6269
|
function clearDevState() {
|
|
5852
6270
|
const path = getDevStatePath();
|
|
5853
|
-
if (
|
|
6271
|
+
if (existsSync16(path)) {
|
|
5854
6272
|
try {
|
|
5855
6273
|
unlinkSync2(path);
|
|
5856
6274
|
} catch {
|
|
@@ -6260,13 +6678,13 @@ var McpManager = class {
|
|
|
6260
6678
|
};
|
|
6261
6679
|
|
|
6262
6680
|
// src/skills/manager.ts
|
|
6263
|
-
import { existsSync as
|
|
6264
|
-
import { join as
|
|
6681
|
+
import { existsSync as existsSync17, readdirSync as readdirSync8, mkdirSync as mkdirSync11 } from "fs";
|
|
6682
|
+
import { join as join12 } from "path";
|
|
6265
6683
|
|
|
6266
6684
|
// src/skills/types.ts
|
|
6267
|
-
import { readFileSync as
|
|
6685
|
+
import { readFileSync as readFileSync12 } from "fs";
|
|
6268
6686
|
import { basename as basename4 } from "path";
|
|
6269
|
-
function
|
|
6687
|
+
function parseSimpleYaml2(yaml) {
|
|
6270
6688
|
const result = {};
|
|
6271
6689
|
for (const line of yaml.split("\n")) {
|
|
6272
6690
|
const match = line.match(/^(\w+)\s*:\s*(.+)$/);
|
|
@@ -6286,7 +6704,7 @@ function parseYamlArray(value) {
|
|
|
6286
6704
|
function parseSkillFile(filePath) {
|
|
6287
6705
|
let raw;
|
|
6288
6706
|
try {
|
|
6289
|
-
raw =
|
|
6707
|
+
raw = readFileSync12(filePath, "utf-8");
|
|
6290
6708
|
} catch {
|
|
6291
6709
|
return null;
|
|
6292
6710
|
}
|
|
@@ -6302,7 +6720,7 @@ function parseSkillFile(filePath) {
|
|
|
6302
6720
|
};
|
|
6303
6721
|
}
|
|
6304
6722
|
const [, yaml, content] = frontmatterMatch;
|
|
6305
|
-
const parsed =
|
|
6723
|
+
const parsed = parseSimpleYaml2(yaml);
|
|
6306
6724
|
return {
|
|
6307
6725
|
meta: {
|
|
6308
6726
|
name: parsed["name"] ?? basename4(filePath, ".md"),
|
|
@@ -6326,22 +6744,22 @@ var SkillManager = class {
|
|
|
6326
6744
|
/** 发现并加载 skillsDir 下所有 .md 文件,返回加载数量 */
|
|
6327
6745
|
loadSkills() {
|
|
6328
6746
|
this.skills.clear();
|
|
6329
|
-
if (!
|
|
6747
|
+
if (!existsSync17(this.skillsDir)) {
|
|
6330
6748
|
try {
|
|
6331
|
-
|
|
6749
|
+
mkdirSync11(this.skillsDir, { recursive: true });
|
|
6332
6750
|
} catch {
|
|
6333
6751
|
}
|
|
6334
6752
|
return 0;
|
|
6335
6753
|
}
|
|
6336
6754
|
let entries;
|
|
6337
6755
|
try {
|
|
6338
|
-
entries =
|
|
6756
|
+
entries = readdirSync8(this.skillsDir);
|
|
6339
6757
|
} catch {
|
|
6340
6758
|
return 0;
|
|
6341
6759
|
}
|
|
6342
6760
|
for (const entry of entries) {
|
|
6343
6761
|
if (!entry.endsWith(".md")) continue;
|
|
6344
|
-
const filePath =
|
|
6762
|
+
const filePath = join12(this.skillsDir, entry);
|
|
6345
6763
|
const skill = parseSkillFile(filePath);
|
|
6346
6764
|
if (skill) {
|
|
6347
6765
|
this.skills.set(skill.meta.name, skill);
|
|
@@ -6406,14 +6824,14 @@ function parseAtReferences(input2, cwd) {
|
|
|
6406
6824
|
while ((match = atPattern.exec(input2)) !== null) {
|
|
6407
6825
|
const rawPath = match[1] ?? match[2] ?? match[3] ?? "";
|
|
6408
6826
|
const absPath = resolve4(cwd, rawPath);
|
|
6409
|
-
const ext =
|
|
6827
|
+
const ext = extname4(rawPath).toLowerCase();
|
|
6410
6828
|
const mime = IMAGE_MIME[ext];
|
|
6411
|
-
if (!
|
|
6829
|
+
if (!existsSync18(absPath)) {
|
|
6412
6830
|
refs.push({ path: rawPath, type: "notfound" });
|
|
6413
6831
|
continue;
|
|
6414
6832
|
}
|
|
6415
6833
|
if (mime) {
|
|
6416
|
-
const data =
|
|
6834
|
+
const data = readFileSync13(absPath).toString("base64");
|
|
6417
6835
|
imageParts.push({
|
|
6418
6836
|
type: "image_url",
|
|
6419
6837
|
image_url: { url: `data:${mime};base64,${data}` }
|
|
@@ -6421,7 +6839,7 @@ function parseAtReferences(input2, cwd) {
|
|
|
6421
6839
|
refs.push({ path: rawPath, type: "image" });
|
|
6422
6840
|
textBody = textBody.replace(match[0], "").trim();
|
|
6423
6841
|
} else {
|
|
6424
|
-
const content =
|
|
6842
|
+
const content = readFileSync13(absPath, "utf-8");
|
|
6425
6843
|
const inlined = `
|
|
6426
6844
|
|
|
6427
6845
|
[File: ${rawPath}]
|
|
@@ -6462,11 +6880,17 @@ var Repl = class {
|
|
|
6462
6880
|
this.rl = readline.createInterface({
|
|
6463
6881
|
input: process.stdin,
|
|
6464
6882
|
output: process.stdout,
|
|
6465
|
-
terminal: true
|
|
6883
|
+
terminal: true,
|
|
6884
|
+
completer: (line) => this.completeInput(line)
|
|
6466
6885
|
});
|
|
6467
6886
|
this.toolRegistry = new ToolRegistry();
|
|
6468
6887
|
this.toolExecutor = new ToolExecutor(this.toolRegistry);
|
|
6469
6888
|
this.toolExecutor.setReadline(this.rl);
|
|
6889
|
+
this.toolExecutor.setConfig({
|
|
6890
|
+
hookConfig: this.config.get("hooks") ?? void 0,
|
|
6891
|
+
permissionRules: this.config.get("permissionRules") ?? [],
|
|
6892
|
+
defaultPermission: this.config.get("defaultPermission") ?? "confirm"
|
|
6893
|
+
});
|
|
6470
6894
|
}
|
|
6471
6895
|
rl;
|
|
6472
6896
|
currentProvider;
|
|
@@ -6493,6 +6917,7 @@ var Repl = class {
|
|
|
6493
6917
|
planMode = false;
|
|
6494
6918
|
/** 技能管理器 */
|
|
6495
6919
|
skillManager = null;
|
|
6920
|
+
customCommandManager = null;
|
|
6496
6921
|
/**
|
|
6497
6922
|
* 交互式列表选择器进行中标志。
|
|
6498
6923
|
* 与 toolExecutor.confirming 类似:主循环 line handler 在此为 true 时忽略 line 事件,
|
|
@@ -6505,9 +6930,9 @@ var Repl = class {
|
|
|
6505
6930
|
*/
|
|
6506
6931
|
findContextFile(dir, candidates = CONTEXT_FILE_CANDIDATES) {
|
|
6507
6932
|
for (const candidate of candidates) {
|
|
6508
|
-
const fullPath =
|
|
6509
|
-
if (
|
|
6510
|
-
const content =
|
|
6933
|
+
const fullPath = join13(dir, candidate);
|
|
6934
|
+
if (existsSync18(fullPath)) {
|
|
6935
|
+
const content = readFileSync13(fullPath, "utf-8").trim();
|
|
6511
6936
|
if (content) return { filePath: fullPath, content };
|
|
6512
6937
|
}
|
|
6513
6938
|
}
|
|
@@ -6531,9 +6956,9 @@ var Repl = class {
|
|
|
6531
6956
|
if (setting === false) return { layers: [], mergedContent: "" };
|
|
6532
6957
|
const cwd = process.cwd();
|
|
6533
6958
|
if (setting !== "auto") {
|
|
6534
|
-
const fullPath =
|
|
6535
|
-
if (
|
|
6536
|
-
const content =
|
|
6959
|
+
const fullPath = join13(cwd, setting);
|
|
6960
|
+
if (existsSync18(fullPath)) {
|
|
6961
|
+
const content = readFileSync13(fullPath, "utf-8").trim();
|
|
6537
6962
|
if (content) {
|
|
6538
6963
|
const layer = {
|
|
6539
6964
|
level: "project",
|
|
@@ -6590,9 +7015,9 @@ var Repl = class {
|
|
|
6590
7015
|
* 超过 MEMORY_MAX_CHARS 时只取末尾最新部分。
|
|
6591
7016
|
*/
|
|
6592
7017
|
loadMemoryContent() {
|
|
6593
|
-
const memoryPath =
|
|
6594
|
-
if (!
|
|
6595
|
-
let content =
|
|
7018
|
+
const memoryPath = join13(this.config.getConfigDir(), MEMORY_FILE_NAME);
|
|
7019
|
+
if (!existsSync18(memoryPath)) return null;
|
|
7020
|
+
let content = readFileSync13(memoryPath, "utf-8").trim();
|
|
6596
7021
|
if (!content) return null;
|
|
6597
7022
|
if (content.length > MEMORY_MAX_CHARS) {
|
|
6598
7023
|
content = content.slice(-MEMORY_MAX_CHARS);
|
|
@@ -6868,11 +7293,18 @@ ${response.content.trim()}
|
|
|
6868
7293
|
process.stdout.write(chalk10.dim(` \u{1F50C} Plugins loaded: ${pluginCount} tool(s) from plugins/
|
|
6869
7294
|
`));
|
|
6870
7295
|
}
|
|
6871
|
-
const skillsDir =
|
|
7296
|
+
const skillsDir = join13(this.config.getConfigDir(), SKILLS_DIR_NAME);
|
|
6872
7297
|
this.skillManager = new SkillManager(skillsDir);
|
|
6873
7298
|
const skillCount = this.skillManager.loadSkills();
|
|
6874
7299
|
if (skillCount > 0) {
|
|
6875
7300
|
process.stdout.write(chalk10.dim(` \u{1F3AF} Skills: ${skillCount} available (use /skill to manage)
|
|
7301
|
+
`));
|
|
7302
|
+
}
|
|
7303
|
+
const commandsDir = join13(this.config.getConfigDir(), CUSTOM_COMMANDS_DIR_NAME);
|
|
7304
|
+
this.customCommandManager = new CustomCommandManager(commandsDir);
|
|
7305
|
+
const customCmdCount = this.customCommandManager.loadCommands();
|
|
7306
|
+
if (customCmdCount > 0) {
|
|
7307
|
+
process.stdout.write(chalk10.dim(` \u{1F4CB} Custom commands: ${customCmdCount} loaded (use /commands to list)
|
|
6876
7308
|
`));
|
|
6877
7309
|
}
|
|
6878
7310
|
const mcpServers = this.config.get("mcpServers");
|
|
@@ -7038,6 +7470,126 @@ ${response.content.trim()}
|
|
|
7038
7470
|
maxTokens: params.maxTokens ?? DEFAULT_MAX_TOKENS
|
|
7039
7471
|
};
|
|
7040
7472
|
}
|
|
7473
|
+
// ─── Tab 自动补全 ──────────────────────────────────────────────────────
|
|
7474
|
+
/**
|
|
7475
|
+
* readline completer 回调。根据输入上下文返回候选补全列表。
|
|
7476
|
+
* 返回格式:[candidates, matchedPrefix]
|
|
7477
|
+
*/
|
|
7478
|
+
completeInput(line) {
|
|
7479
|
+
if (line.startsWith("/") && !line.includes(" ")) {
|
|
7480
|
+
const prefix = line.toLowerCase();
|
|
7481
|
+
const names = [];
|
|
7482
|
+
for (const cmd of this.commands.listAll()) {
|
|
7483
|
+
const full = `/${cmd.name}`;
|
|
7484
|
+
if (full.startsWith(prefix)) names.push(full);
|
|
7485
|
+
}
|
|
7486
|
+
if (this.customCommandManager) {
|
|
7487
|
+
for (const cmd of this.customCommandManager.listCommands()) {
|
|
7488
|
+
const full = `/${cmd.meta.name}`;
|
|
7489
|
+
if (full.startsWith(prefix) && !names.includes(full)) names.push(full);
|
|
7490
|
+
}
|
|
7491
|
+
}
|
|
7492
|
+
return [names.sort(), line];
|
|
7493
|
+
}
|
|
7494
|
+
if (line.startsWith("/") && line.includes(" ")) {
|
|
7495
|
+
const spaceIdx = line.indexOf(" ");
|
|
7496
|
+
const cmdName = line.slice(1, spaceIdx).toLowerCase();
|
|
7497
|
+
const argPart = line.slice(spaceIdx + 1);
|
|
7498
|
+
const candidates = this.getSubcommandCandidates(cmdName, argPart);
|
|
7499
|
+
if (candidates.length > 0) {
|
|
7500
|
+
const prefix = argPart.toLowerCase();
|
|
7501
|
+
const filtered = candidates.filter((c) => c.toLowerCase().startsWith(prefix));
|
|
7502
|
+
const fullCandidates = filtered.map((c) => `/${cmdName} ${c}`);
|
|
7503
|
+
return [fullCandidates, line];
|
|
7504
|
+
}
|
|
7505
|
+
return [[], line];
|
|
7506
|
+
}
|
|
7507
|
+
const atIdx = line.lastIndexOf("@");
|
|
7508
|
+
if (atIdx !== -1) {
|
|
7509
|
+
const partial = line.slice(atIdx + 1);
|
|
7510
|
+
if (!partial.startsWith('"') && !partial.startsWith("'")) {
|
|
7511
|
+
const fileCandidates = this.completeFilePath(partial);
|
|
7512
|
+
if (fileCandidates.length > 0) {
|
|
7513
|
+
const fullCandidates = fileCandidates.map((f) => line.slice(0, atIdx + 1) + f);
|
|
7514
|
+
return [fullCandidates, line];
|
|
7515
|
+
}
|
|
7516
|
+
}
|
|
7517
|
+
}
|
|
7518
|
+
return [[], line];
|
|
7519
|
+
}
|
|
7520
|
+
/** 根据命令名返回子命令/参数候选列表 */
|
|
7521
|
+
getSubcommandCandidates(cmdName, _argPart) {
|
|
7522
|
+
switch (cmdName) {
|
|
7523
|
+
case "provider": {
|
|
7524
|
+
return this.providers.listAvailable().map((p) => p.info.id);
|
|
7525
|
+
}
|
|
7526
|
+
case "model": {
|
|
7527
|
+
try {
|
|
7528
|
+
const provider = this.providers.get(this.currentProvider);
|
|
7529
|
+
return provider.info.models.map((m) => m.id);
|
|
7530
|
+
} catch {
|
|
7531
|
+
return [];
|
|
7532
|
+
}
|
|
7533
|
+
}
|
|
7534
|
+
case "session":
|
|
7535
|
+
return ["new", "list", "load"];
|
|
7536
|
+
case "checkpoint":
|
|
7537
|
+
return ["save", "restore", "list", "delete"];
|
|
7538
|
+
case "skill": {
|
|
7539
|
+
const skills = this.skillManager?.listSkills().map((s) => s.meta.name) ?? [];
|
|
7540
|
+
return [...skills, "off", "list", "reload"];
|
|
7541
|
+
}
|
|
7542
|
+
case "plan":
|
|
7543
|
+
return ["enter", "execute", "exit", "cancel", "status"];
|
|
7544
|
+
case "commands":
|
|
7545
|
+
return ["list", "reload"];
|
|
7546
|
+
case "export":
|
|
7547
|
+
return ["md", "json"];
|
|
7548
|
+
case "cost":
|
|
7549
|
+
return ["reset"];
|
|
7550
|
+
case "context":
|
|
7551
|
+
return ["reload"];
|
|
7552
|
+
case "mcp":
|
|
7553
|
+
return ["reconnect"];
|
|
7554
|
+
case "compact":
|
|
7555
|
+
case "review":
|
|
7556
|
+
case "init":
|
|
7557
|
+
return [];
|
|
7558
|
+
// 自由文本参数,不补全
|
|
7559
|
+
default:
|
|
7560
|
+
return [];
|
|
7561
|
+
}
|
|
7562
|
+
}
|
|
7563
|
+
/**
|
|
7564
|
+
* 补全 @ 后的文件路径。
|
|
7565
|
+
* partial 为 @ 后的部分(如 "src/re"),返回匹配的路径列表(目录带 / 后缀)。
|
|
7566
|
+
*/
|
|
7567
|
+
completeFilePath(partial) {
|
|
7568
|
+
try {
|
|
7569
|
+
const normalized = partial.replace(/\\/g, "/");
|
|
7570
|
+
const dir = normalized.includes("/") ? dirname5(normalized) : ".";
|
|
7571
|
+
const prefix = normalized.includes("/") ? basename5(normalized) : normalized;
|
|
7572
|
+
const absDir = resolve4(process.cwd(), dir);
|
|
7573
|
+
if (!existsSync18(absDir)) return [];
|
|
7574
|
+
const entries = readdirSync9(absDir);
|
|
7575
|
+
const results = [];
|
|
7576
|
+
for (const entry of entries) {
|
|
7577
|
+
if (entry.startsWith(".")) continue;
|
|
7578
|
+
if (!entry.toLowerCase().startsWith(prefix.toLowerCase())) continue;
|
|
7579
|
+
try {
|
|
7580
|
+
const fullPath = join13(absDir, entry);
|
|
7581
|
+
const stat = statSync6(fullPath);
|
|
7582
|
+
const rel = dir === "." ? entry : `${dir}/${entry}`;
|
|
7583
|
+
results.push(stat.isDirectory() ? `${rel}/` : rel);
|
|
7584
|
+
} catch {
|
|
7585
|
+
}
|
|
7586
|
+
}
|
|
7587
|
+
return results.sort();
|
|
7588
|
+
} catch {
|
|
7589
|
+
return [];
|
|
7590
|
+
}
|
|
7591
|
+
}
|
|
7592
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
7041
7593
|
shouldShowTokens() {
|
|
7042
7594
|
return this.config.get("ui").showTokenCount;
|
|
7043
7595
|
}
|
|
@@ -7056,7 +7608,11 @@ ${response.content.trim()}
|
|
|
7056
7608
|
timeout: modelParams.timeout,
|
|
7057
7609
|
thinking: modelParams.thinking
|
|
7058
7610
|
});
|
|
7059
|
-
const
|
|
7611
|
+
const showTokens = this.shouldShowTokens();
|
|
7612
|
+
const { content, usage, tokensShown } = await this.renderer.renderStream(stream, {
|
|
7613
|
+
showTokens,
|
|
7614
|
+
sessionTotal: showTokens ? { ...this.sessionTokenUsage } : void 0
|
|
7615
|
+
});
|
|
7060
7616
|
lastResponseStore.content = content;
|
|
7061
7617
|
session.addMessage({ role: "assistant", content, timestamp: /* @__PURE__ */ new Date() });
|
|
7062
7618
|
this.events.emit("message.after", { content });
|
|
@@ -7064,7 +7620,7 @@ ${response.content.trim()}
|
|
|
7064
7620
|
this.sessionTokenUsage.inputTokens += usage.inputTokens;
|
|
7065
7621
|
this.sessionTokenUsage.outputTokens += usage.outputTokens;
|
|
7066
7622
|
session.addTokenUsage(usage);
|
|
7067
|
-
if (
|
|
7623
|
+
if (showTokens && !tokensShown) {
|
|
7068
7624
|
this.renderer.renderUsage(usage, this.sessionTokenUsage);
|
|
7069
7625
|
}
|
|
7070
7626
|
}
|
|
@@ -7178,9 +7734,10 @@ ${response.content.trim()}
|
|
|
7178
7734
|
thinking: modelParams.thinking,
|
|
7179
7735
|
...extraMessages.length > 0 ? { _extraMessages: extraMessages } : {}
|
|
7180
7736
|
});
|
|
7181
|
-
const
|
|
7737
|
+
const teeShowTokens = this.shouldShowTokens();
|
|
7738
|
+
const { content: genContent, usage: genUsage, tokensShown: teeTokShown } = await this.renderer.renderStream(
|
|
7182
7739
|
genStream,
|
|
7183
|
-
{ saveToFile }
|
|
7740
|
+
{ saveToFile, showTokens: teeShowTokens, sessionTotal: teeShowTokens ? { ...this.sessionTokenUsage } : void 0 }
|
|
7184
7741
|
);
|
|
7185
7742
|
lastResponseStore.content = genContent;
|
|
7186
7743
|
if (genUsage) {
|
|
@@ -7203,7 +7760,7 @@ ${response.content.trim()}
|
|
|
7203
7760
|
this.sessionTokenUsage.inputTokens += roundUsage.inputTokens;
|
|
7204
7761
|
this.sessionTokenUsage.outputTokens += roundUsage.outputTokens;
|
|
7205
7762
|
session.addTokenUsage(roundUsage);
|
|
7206
|
-
if (
|
|
7763
|
+
if (teeShowTokens && !teeTokShown) {
|
|
7207
7764
|
this.renderer.renderUsage(roundUsage, this.sessionTokenUsage);
|
|
7208
7765
|
}
|
|
7209
7766
|
}
|
|
@@ -7247,6 +7804,30 @@ ${response.content.trim()}
|
|
|
7247
7804
|
const args = parts.slice(1);
|
|
7248
7805
|
const cmd = this.commands.get(cmdName);
|
|
7249
7806
|
if (!cmd) {
|
|
7807
|
+
const customCmd = this.customCommandManager?.getCommand(cmdName);
|
|
7808
|
+
if (customCmd) {
|
|
7809
|
+
const prompt = expandTemplate(customCmd.promptTemplate, args);
|
|
7810
|
+
console.log(chalk10.dim(` Running custom command: /${cmdName}...`));
|
|
7811
|
+
try {
|
|
7812
|
+
const provider = this.providers.get(this.currentProvider);
|
|
7813
|
+
const modelParams = this.getModelParams();
|
|
7814
|
+
const response = await provider.chat({
|
|
7815
|
+
messages: [{ role: "user", content: prompt, timestamp: /* @__PURE__ */ new Date() }],
|
|
7816
|
+
model: this.currentModel,
|
|
7817
|
+
systemPrompt: this.buildCurrentSystemPrompt(),
|
|
7818
|
+
stream: false,
|
|
7819
|
+
temperature: 0.3,
|
|
7820
|
+
maxTokens: modelParams.maxTokens ?? 4096,
|
|
7821
|
+
timeout: modelParams.timeout ?? 6e4
|
|
7822
|
+
});
|
|
7823
|
+
console.log();
|
|
7824
|
+
console.log(response.content);
|
|
7825
|
+
console.log();
|
|
7826
|
+
} catch (err) {
|
|
7827
|
+
this.renderer.renderError(`Custom command failed: ${err.message}`);
|
|
7828
|
+
}
|
|
7829
|
+
return;
|
|
7830
|
+
}
|
|
7250
7831
|
this.renderer.renderError(
|
|
7251
7832
|
`Unknown command: /${cmdName}. Type /help for available commands.`
|
|
7252
7833
|
);
|
|
@@ -7341,6 +7922,13 @@ ${response.content.trim()}
|
|
|
7341
7922
|
},
|
|
7342
7923
|
refreshPrompt: () => this.refreshPrompt(),
|
|
7343
7924
|
getSkillManager: () => this.skillManager ?? null,
|
|
7925
|
+
createCheckpoint: (name) => {
|
|
7926
|
+
this.sessions.current?.createCheckpoint(name);
|
|
7927
|
+
},
|
|
7928
|
+
listCheckpoints: () => this.sessions.current?.listCheckpoints() ?? [],
|
|
7929
|
+
restoreCheckpoint: (name) => this.sessions.current?.restoreCheckpoint(name) ?? false,
|
|
7930
|
+
deleteCheckpoint: (name) => this.sessions.current?.deleteCheckpoint(name) ?? false,
|
|
7931
|
+
getCustomCommandManager: () => this.customCommandManager ?? null,
|
|
7344
7932
|
exit: () => this.handleExit()
|
|
7345
7933
|
};
|
|
7346
7934
|
await cmd.execute(args, ctx);
|