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.
Files changed (3) hide show
  1. package/CLAUDE.md +161 -15
  2. package/dist/index.js +635 -47
  3. 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 + 19个命令(/help /about /provider /model /clear /compact /plan /session /system /context /status /search /undo /export /tools /plugins /mcp /config /exit)
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
- ├── executor.ts # ToolExecutor(确认逻辑 + printToolCall/printToolResult
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
- - [ ] **Agent Skills 系统**:可复用的专业技能包(`.aicli/skills/`)
317
- - [ ] **`/init` 项目初始化**:扫描项目结构自动生成 `AICLI.md`
318
- - [ ] **`/copy` 剪贴板支持**:将最后回答复制到系统剪贴板
319
- - [ ] **多文件编辑预览**:diff preview + 批量 apply/reject
320
- - [ ] **Token 用量统计 `/cost`**:实时显示 input/output token 数,累计 session 用量
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
- - [ ] **Hooks 系统**:pre/post tool execution hooks
324
- - [ ] **Permission Rules**:基于规则的工具权限控制
325
- - [ ] **Checkpointing**:会话检查点,可回退到任意历史点
326
- - [ ] **`/review` 代码审查**:集成 git diff,输出结构化审查意见
327
- - [ ] **Custom Commands**:用户自定义 REPL 命令(`~/.aicli/commands/`)
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
- - [ ] **Token 用量显示**:`ui.showTokenCount` 配置字段已有,但终端展示逻辑未完整实现。
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.23";
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: 65536,
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 existsSync17, readFileSync as readFileSync12 } from "fs";
1568
- import { join as join12, resolve as resolve4, extname as extname3 } from "path";
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\uFF0823\u4E2A\uFF09\uFF1A"));
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 /exit"));
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
- process.stdout.write("\n\n");
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 existsSync15, readFileSync as readFileSync10, writeFileSync as writeFileSync8, unlinkSync as unlinkSync2, mkdirSync as mkdirSync9 } from "fs";
5788
- import { join as join10 } from "path";
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 join10(homedir4(), CONFIG_DIR_NAME, DEV_STATE_FILE_NAME);
6245
+ return join11(homedir4(), CONFIG_DIR_NAME, DEV_STATE_FILE_NAME);
5828
6246
  }
5829
6247
  function saveDevState(content) {
5830
- const configDir = join10(homedir4(), CONFIG_DIR_NAME);
5831
- if (!existsSync15(configDir)) {
5832
- mkdirSync9(configDir, { recursive: true });
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 (!existsSync15(path)) return null;
5848
- const content = readFileSync10(path, "utf-8").trim();
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 (existsSync15(path)) {
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 existsSync16, readdirSync as readdirSync7, mkdirSync as mkdirSync10 } from "fs";
6264
- import { join as join11 } from "path";
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 readFileSync11 } from "fs";
6685
+ import { readFileSync as readFileSync12 } from "fs";
6268
6686
  import { basename as basename4 } from "path";
6269
- function parseSimpleYaml(yaml) {
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 = readFileSync11(filePath, "utf-8");
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 = parseSimpleYaml(yaml);
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 (!existsSync16(this.skillsDir)) {
6747
+ if (!existsSync17(this.skillsDir)) {
6330
6748
  try {
6331
- mkdirSync10(this.skillsDir, { recursive: true });
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 = readdirSync7(this.skillsDir);
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 = join11(this.skillsDir, entry);
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 = extname3(rawPath).toLowerCase();
6827
+ const ext = extname4(rawPath).toLowerCase();
6410
6828
  const mime = IMAGE_MIME[ext];
6411
- if (!existsSync17(absPath)) {
6829
+ if (!existsSync18(absPath)) {
6412
6830
  refs.push({ path: rawPath, type: "notfound" });
6413
6831
  continue;
6414
6832
  }
6415
6833
  if (mime) {
6416
- const data = readFileSync12(absPath).toString("base64");
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 = readFileSync12(absPath, "utf-8");
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 = join12(dir, candidate);
6509
- if (existsSync17(fullPath)) {
6510
- const content = readFileSync12(fullPath, "utf-8").trim();
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 = join12(cwd, setting);
6535
- if (existsSync17(fullPath)) {
6536
- const content = readFileSync12(fullPath, "utf-8").trim();
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 = join12(this.config.getConfigDir(), MEMORY_FILE_NAME);
6594
- if (!existsSync17(memoryPath)) return null;
6595
- let content = readFileSync12(memoryPath, "utf-8").trim();
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 = join12(this.config.getConfigDir(), SKILLS_DIR_NAME);
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 { content, usage } = await this.renderer.renderStream(stream);
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 (this.shouldShowTokens()) {
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 { content: genContent, usage: genUsage } = await this.renderer.renderStream(
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 (this.shouldShowTokens()) {
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jinzd-ai-cli",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "description": "Cross-platform REPL-style AI CLI with multi-provider support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",