jinzd-ai-cli 0.1.47 → 0.1.49
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
CHANGED
|
@@ -38,8 +38,8 @@ src/
|
|
|
38
38
|
│ ├── config-manager.ts # 读写 ~/.aicli/config.json,三层优先级:env > file > default
|
|
39
39
|
│ └── env-loader.ts # AICLI_API_KEY_* 等环境变量映射
|
|
40
40
|
├── session/
|
|
41
|
-
│ ├── session.ts # Session(addMessage / clear / compact / toJSON / fromJSON)
|
|
42
|
-
│ └── session-manager.ts # CRUD for ~/.aicli/history/*.json
|
|
41
|
+
│ ├── session.ts # Session(addMessage / clear / compact / fork / toJSON / fromJSON)
|
|
42
|
+
│ └── session-manager.ts # CRUD + forkSession for ~/.aicli/history/*.json
|
|
43
43
|
├── repl/
|
|
44
44
|
│ ├── repl.ts # 主 REPL 循环(MAX_TOOL_ROUNDS=20,handleChatWithTools agentic loop)
|
|
45
45
|
│ ├── renderer.ts # 终端输出(renderStream / renderResponse 均不加前置 \n)
|
|
@@ -47,7 +47,7 @@ src/
|
|
|
47
47
|
│ ├── dev-state.ts # 开发状态交接(provider/model 切换时快照生成、save/load/clear)
|
|
48
48
|
│ ├── setup-wizard.ts # @inquirer/prompts 首次运行交互式设置
|
|
49
49
|
│ └── commands/
|
|
50
|
-
│ └── index.ts # CommandRegistry +
|
|
50
|
+
│ └── index.ts # CommandRegistry + 35个命令(/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 /test /scaffold /add-dir /memory /doctor /bug /think /diff /fork /exit)
|
|
51
51
|
│ ├── custom-commands.ts # CustomCommandManager(~/.aicli/commands/*.md 用户自定义命令)
|
|
52
52
|
├── skills/
|
|
53
53
|
│ ├── types.ts # Skill/SkillMeta 接口、parseSkillFile(YAML frontmatter 解析)
|
|
@@ -350,6 +350,81 @@ const stdinLines = Array.isArray(rawStdin) ? rawStdin.map(String)
|
|
|
350
350
|
- [x] **web_fetch DNS 解析时 SSRF 防护**(v0.1.25):新增 `resolveAndCheck()` 函数,用 `dns.promises.lookup()` 预解析域名,检查结果 IP 是否为私有地址。初始 URL 和 redirect 目标均校验。
|
|
351
351
|
- [ ] **`persistentCwd` 全局状态**:bash 工具的当前工作目录是模块级全局变量,多 session 并发时可能串扰。现阶段单 session REPL 无影响,GUI 多会话扩展时需重构为 per-session 状态。
|
|
352
352
|
|
|
353
|
+
## 本轮开发完成记录(2026-03-08,v0.1.47 → v0.1.48)
|
|
354
|
+
|
|
355
|
+
### Tier 2 体验增强:/undo 增强 + /fork 对话分支
|
|
356
|
+
|
|
357
|
+
**Feature 1:/undo 增强 — bash 工具文件追踪 + 命令增强**
|
|
358
|
+
|
|
359
|
+
| 文件 | 变更 |
|
|
360
|
+
|------|------|
|
|
361
|
+
| `src/tools/undo-stack.ts` | `UndoEntry` 新增 `isDirectory?: boolean` + `pushNewFile()` / `pushNewDir()` 方法 + `undo()` 目录分支 |
|
|
362
|
+
| `src/tools/builtin/bash.ts` | 三辅助函数 + `execute()` 集成 |
|
|
363
|
+
| `src/repl/commands/index.ts` | `/undo` 重写为 `/undo [list\|<n>]` |
|
|
364
|
+
|
|
365
|
+
- **UndoStack 扩展**:新增 `pushNewFile(filePath, desc)` 和 `pushNewDir(dirPath, desc)` 方法,previousContent=null 表示新建(undo 时删除)。`undo()` 新增 `isDirectory` 分支,用 `rmdirSync` 尝试删除空目录,非空给出提示。
|
|
366
|
+
- **Bash 工具文件追踪**(浅层 CWD 快照 + 命令模式解析):
|
|
367
|
+
- `snapshotDir(dir)` — `readdirSync` 获取目录下所有条目的绝对路径 Set
|
|
368
|
+
- `parseCreationTargets(command, cwd)` — 正则解析 touch/mkdir/echo>/cp/New-Item 等常见创建命令的目标路径
|
|
369
|
+
- `pushBashUndoEntries(beforeSnapshot, parsedTargetsBefore, cwd)` — 对比前后快照 + 解析目标,为新建文件/目录推入 undo 条目
|
|
370
|
+
- 集成到 `execute()`:执行前快照 + 构建目标存在状态 Map;执行后(成功/失败均)调用 pushBashUndoEntries
|
|
371
|
+
- **/undo 命令增强**:
|
|
372
|
+
- `/undo` — 撤销最近 1 次(向后兼容)
|
|
373
|
+
- `/undo list` — 显示完整 undo 栈(编号、类型标签 `[new]`/`[mod]`/`[dir]`、描述、时间)
|
|
374
|
+
- `/undo <n>` — 连续撤销最近 N 次操作,逐条显示进度
|
|
375
|
+
|
|
376
|
+
**Feature 2:/fork 对话分支**
|
|
377
|
+
|
|
378
|
+
| 文件 | 变更 |
|
|
379
|
+
|------|------|
|
|
380
|
+
| `src/session/session.ts` | 静态 `fork(original, newId, messageCount, newTitle?)` 方法 |
|
|
381
|
+
| `src/session/session-manager.ts` | `forkSession(messageCount, title?)` 方法 |
|
|
382
|
+
| `src/repl/commands/index.ts` | `/fork` 命令 + `CommandContext.forkSession` |
|
|
383
|
+
| `src/repl/repl.ts` | ctx.forkSession 注入 + tab 补全(/undo list, /fork checkpoint 名称) |
|
|
384
|
+
|
|
385
|
+
- **Session.fork()**:复制 messages[0..messageCount],保留范围内 checkpoints(深拷贝),新 UUID,title 默认 "Fork of <原标题>"
|
|
386
|
+
- **SessionManager.forkSession()**:先保存原始 session → Session.fork 创建分叉 → 设为当前 → 保存并返回
|
|
387
|
+
- **/fork 命令**:
|
|
388
|
+
- `/fork` — 从当前位置分叉(复制全部消息)
|
|
389
|
+
- `/fork <checkpoint-name>` — 从指定 checkpoint 分叉(仅复制到该 checkpoint 的消息)
|
|
390
|
+
- 显示原始/新 session ID、title、消息数、保留 checkpoint 数
|
|
391
|
+
- 提示用户 `/session load <id>` 切回原始
|
|
392
|
+
- **Tab 补全**:`/undo` 提供 `list`;`/fork` 提供当前 session 的 checkpoint 名称列表
|
|
393
|
+
|
|
394
|
+
### 版本与收尾
|
|
395
|
+
- `src/core/constants.ts`:VERSION `0.1.47` → `0.1.48`
|
|
396
|
+
- `package.json`:version 同步
|
|
397
|
+
- `src/repl/renderer.ts`:命令计数 34 → 35,命令列表新增 `/fork`,新增 1 条特性(/fork 对话分支),更新 /undo 描述
|
|
398
|
+
- 构建验证:`npm run build` 零错误(ESM + CJS 双产物)
|
|
399
|
+
- 发布:`npm publish` → `jinzd-ai-cli@0.1.48`
|
|
400
|
+
|
|
401
|
+
### 本轮变更文件汇总
|
|
402
|
+
|
|
403
|
+
| 文件 | 变更类型 | 说明 |
|
|
404
|
+
|------|---------|------|
|
|
405
|
+
| `src/core/constants.ts` | 修改 | VERSION 0.1.47 → 0.1.48 |
|
|
406
|
+
| `src/tools/undo-stack.ts` | 修改 | isDirectory 字段 + pushNewFile/pushNewDir + undo 目录支持 |
|
|
407
|
+
| `src/tools/builtin/bash.ts` | 修改 | snapshotDir + parseCreationTargets + pushBashUndoEntries + execute 集成 |
|
|
408
|
+
| `src/session/session.ts` | 修改 | 静态 fork() 方法 |
|
|
409
|
+
| `src/session/session-manager.ts` | 修改 | forkSession() 方法 |
|
|
410
|
+
| `src/repl/commands/index.ts` | 修改 | /undo 增强 + /fork 命令 + CommandContext.forkSession + /help 更新 |
|
|
411
|
+
| `src/repl/repl.ts` | 修改 | ctx.forkSession 注入 + tab 补全(/undo, /fork) |
|
|
412
|
+
| `src/repl/renderer.ts` | 修改 | /about 35 命令 + 特性更新 |
|
|
413
|
+
| `package.json` | 修改 | version 0.1.47 → 0.1.48 |
|
|
414
|
+
|
|
415
|
+
### 下一步建议
|
|
416
|
+
|
|
417
|
+
#### Tier 2 — 体验增强(剩余)
|
|
418
|
+
1. **L1 低危**:`run-tests.ts` 的 `JSON.parse(package.json)` 细粒度错误处理
|
|
419
|
+
2. **IDE 集成**:VS Code 扩展(架构已准备就绪,core/providers/tools 无终端依赖)
|
|
420
|
+
3. **OAuth/浏览器登录**:无需手动填 API Key,打开浏览器完成 OAuth 流程自动保存 token
|
|
421
|
+
|
|
422
|
+
#### Tier 3 — 长远方向
|
|
423
|
+
4. **Web UI**:基于 EventBus + WebSocket 的浏览器界面,复用 core/providers/tools 层
|
|
424
|
+
5. **GitHub 仓库迁移**:从 gitee 迁移到 GitHub,npm 包受众更广
|
|
425
|
+
|
|
426
|
+
---
|
|
427
|
+
|
|
353
428
|
## 本轮开发完成记录(2026-03-07,v0.1.40 → v0.1.41)
|
|
354
429
|
|
|
355
430
|
### Bug 修复:Kimi 虚假完成声明(方案 C)
|
|
@@ -536,8 +611,8 @@ const stdinLines = Array.isArray(rawStdin) ? rawStdin.map(String)
|
|
|
536
611
|
4. **L1 低危**:`run-tests.ts` 的 `JSON.parse(package.json)` 细粒度错误处理
|
|
537
612
|
5. **IDE 集成**:VS Code 扩展(架构已准备就绪,core/providers/tools 无终端依赖)
|
|
538
613
|
6. **OAuth/浏览器登录**:无需手动填 API Key,打开浏览器完成 OAuth 流程自动保存 token
|
|
539
|
-
7.
|
|
540
|
-
8.
|
|
614
|
+
7. ~~**`/undo` 增强**~~:✅ 已在 v0.1.48 实现(bash 文件追踪 + /undo list + /undo <n>)
|
|
615
|
+
8. ~~**对话分支(fork)**~~:✅ 已在 v0.1.48 实现(/fork [checkpoint-name])
|
|
541
616
|
|
|
542
617
|
#### Tier 3 — 长远方向
|
|
543
618
|
9. **Web UI**:基于 EventBus + WebSocket 的浏览器界面,复用 core/providers/tools 层
|
package/dist/index.js
CHANGED
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
SUBAGENT_MAX_ROUNDS_LIMIT,
|
|
30
30
|
VERSION,
|
|
31
31
|
runTestsTool
|
|
32
|
-
} from "./chunk-
|
|
32
|
+
} from "./chunk-P4XKHPXU.js";
|
|
33
33
|
|
|
34
34
|
// src/index.ts
|
|
35
35
|
import { program } from "commander";
|
|
@@ -1846,6 +1846,26 @@ var Session = class _Session {
|
|
|
1846
1846
|
}))
|
|
1847
1847
|
};
|
|
1848
1848
|
}
|
|
1849
|
+
/**
|
|
1850
|
+
* 从现有 session 分叉创建新 session。
|
|
1851
|
+
*
|
|
1852
|
+
* 复制 messages[0..messageCount],保留范围内的 checkpoints。
|
|
1853
|
+
* 新 session 拥有独立 UUID,title 默认 "Fork of <原标题>"。
|
|
1854
|
+
*
|
|
1855
|
+
* @param original 原始 session
|
|
1856
|
+
* @param newId 新 session 的 UUID
|
|
1857
|
+
* @param messageCount 复制的消息数量(0 = 空 session,> messages.length 则取全部)
|
|
1858
|
+
* @param newTitle 可选的新标题
|
|
1859
|
+
*/
|
|
1860
|
+
static fork(original, newId, messageCount, newTitle) {
|
|
1861
|
+
const forked = new _Session(newId, original.provider, original.model);
|
|
1862
|
+
forked.title = newTitle ?? (original.title ? `Fork of ${original.title}` : void 0);
|
|
1863
|
+
const clampedCount = Math.min(Math.max(messageCount, 0), original.messages.length);
|
|
1864
|
+
forked.messages = original.messages.slice(0, clampedCount).map((m) => ({ ...m }));
|
|
1865
|
+
forked.checkpoints = original.checkpoints.filter((c) => c.messageIndex <= clampedCount).map((c) => ({ ...c, timestamp: new Date(c.timestamp.getTime()) }));
|
|
1866
|
+
forked.updated = /* @__PURE__ */ new Date();
|
|
1867
|
+
return forked;
|
|
1868
|
+
}
|
|
1849
1869
|
/**
|
|
1850
1870
|
* 从磁盘 JSON 数据恢复 Session 实例。
|
|
1851
1871
|
* 添加运行时校验:损坏或不兼容的历史文件会抛出明确错误,而非 TypeError 崩溃。
|
|
@@ -1961,6 +1981,26 @@ var SessionManager = class {
|
|
|
1961
1981
|
}
|
|
1962
1982
|
return metas.sort((a, b) => b.updated.getTime() - a.updated.getTime());
|
|
1963
1983
|
}
|
|
1984
|
+
/**
|
|
1985
|
+
* 从当前 session 分叉创建新 session。
|
|
1986
|
+
*
|
|
1987
|
+
* 先保存原始 session(保留完整历史),然后创建分叉(截取到 messageCount),
|
|
1988
|
+
* 将分叉设为当前 session 并保存。
|
|
1989
|
+
*
|
|
1990
|
+
* @param messageCount 复制的消息数量
|
|
1991
|
+
* @param title 可选的新标题
|
|
1992
|
+
* @returns 新的分叉 session
|
|
1993
|
+
*/
|
|
1994
|
+
async forkSession(messageCount, title) {
|
|
1995
|
+
if (!this._current) {
|
|
1996
|
+
throw new Error("No active session to fork");
|
|
1997
|
+
}
|
|
1998
|
+
await this.save();
|
|
1999
|
+
const forked = Session.fork(this._current, uuidv4(), messageCount, title);
|
|
2000
|
+
this._current = forked;
|
|
2001
|
+
await this.save();
|
|
2002
|
+
return forked;
|
|
2003
|
+
}
|
|
1964
2004
|
/**
|
|
1965
2005
|
* 跨 session 全文搜索。
|
|
1966
2006
|
* 遍历所有历史 JSON 文件,逐条匹配消息内容(不区分大小写),
|
|
@@ -2021,8 +2061,8 @@ var SessionManager = class {
|
|
|
2021
2061
|
|
|
2022
2062
|
// src/repl/repl.ts
|
|
2023
2063
|
import * as readline from "readline";
|
|
2024
|
-
import { existsSync as existsSync19, readFileSync as readFileSync13, readdirSync as
|
|
2025
|
-
import { join as join14, resolve as
|
|
2064
|
+
import { existsSync as existsSync19, readFileSync as readFileSync13, readdirSync as readdirSync11, statSync as statSync8 } from "fs";
|
|
2065
|
+
import { join as join14, resolve as resolve5, extname as extname4, dirname as dirname6, basename as basename6 } from "path";
|
|
2026
2066
|
import chalk10 from "chalk";
|
|
2027
2067
|
|
|
2028
2068
|
// src/repl/renderer.ts
|
|
@@ -2231,12 +2271,12 @@ var Renderer = class {
|
|
|
2231
2271
|
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"));
|
|
2232
2272
|
console.log(tool("run_tests", "\u8FD0\u884C\u9879\u76EE\u6D4B\u8BD5\u5E76\u8FD4\u56DE\u7ED3\u6784\u5316\u62A5\u544A\uFF08\u81EA\u52A8\u68C0\u6D4B Maven/npm/pytest \u7B49\uFF09"));
|
|
2233
2273
|
console.log(HR);
|
|
2234
|
-
console.log(theme.dim(" REPL \u547D\u4EE4\
|
|
2274
|
+
console.log(theme.dim(" REPL \u547D\u4EE4\uFF0835\u4E2A\uFF09\uFF1A"));
|
|
2235
2275
|
console.log(theme.dim(" /help /about /provider /model /clear /compact /plan /session"));
|
|
2236
2276
|
console.log(theme.dim(" /system /context /status /search /undo /export /copy /cost"));
|
|
2237
2277
|
console.log(theme.dim(" /init /skill /tools /plugins /mcp /config /checkpoint /review"));
|
|
2238
2278
|
console.log(theme.dim(" /commands /test /scaffold /add-dir /memory /doctor /bug /think"));
|
|
2239
|
-
console.log(theme.dim(" /diff /exit"));
|
|
2279
|
+
console.log(theme.dim(" /diff /fork /exit"));
|
|
2240
2280
|
console.log(HR);
|
|
2241
2281
|
console.log(theme.dim(" \u4E3B\u8981\u7279\u6027\uFF1A"));
|
|
2242
2282
|
console.log(feat("Agentic \u5FAA\u73AF\uFF08\u6700\u591A 25 \u8F6E\u5DE5\u5177\u8C03\u7528\uFF0C\u6700\u7EC8\u56DE\u7B54\u6D41\u5F0F\u8F93\u51FA\uFF09"));
|
|
@@ -2244,7 +2284,7 @@ var Renderer = class {
|
|
|
2244
2284
|
console.log(feat("Git \u4E0A\u4E0B\u6587\u611F\u77E5\uFF1A\u542F\u52A8\u81EA\u52A8\u6CE8\u5165\u5206\u652F\u540D\u4E0E\u6587\u4EF6\u53D8\u66F4\u72B6\u6001"));
|
|
2245
2285
|
console.log(feat("\u9879\u76EE\u4E0A\u4E0B\u6587\u6587\u4EF6\uFF1A\u81EA\u52A8\u52A0\u8F7D AICLI.md / CLAUDE.md\uFF08\u4E09\u5C42\u7EA7\uFF1A\u5168\u5C40/\u9879\u76EE/\u5B50\u76EE\u5F55\uFF09"));
|
|
2246
2286
|
console.log(feat("\u8DE8 session \u5386\u53F2\u5168\u6587\u641C\u7D22\uFF08/search <\u5173\u952E\u8BCD>\uFF09"));
|
|
2247
|
-
console.log(feat("\u6587\u4EF6\u64CD\u4F5C\u64A4\u9500\uFF08/undo\uFF0C\u652F\u6301 write_file / edit_file\uFF09"));
|
|
2287
|
+
console.log(feat("\u6587\u4EF6\u64CD\u4F5C\u64A4\u9500\uFF08/undo [list|<n>]\uFF0C\u652F\u6301 write_file / edit_file / bash \u521B\u5EFA\u7684\u6587\u4EF6/\u76EE\u5F55\uFF09"));
|
|
2248
2288
|
console.log(feat("Thinking \u6A21\u5F0F\u6298\u53E0\uFF08<think> \u5757\u81EA\u52A8\u6298\u53E0\uFF0CGLM-5 \u7B49\uFF09"));
|
|
2249
2289
|
console.log(feat("Token \u7528\u91CF\u8FFD\u8E2A\uFF08\u6BCF\u6B21\u56DE\u590D + session \u7D2F\u8BA1\uFF0C\u652F\u6301 Gemini/Claude/DeepSeek \u7B49\uFF09"));
|
|
2250
2290
|
console.log(feat("MCP \u534F\u8BAE\u652F\u6301\uFF1A\u63A5\u5165\u5916\u90E8 MCP \u670D\u52A1\u5668\u5DE5\u5177\uFF08config.json mcpServers \u914D\u7F6E\uFF09"));
|
|
@@ -2286,6 +2326,7 @@ var Renderer = class {
|
|
|
2286
2326
|
console.log(feat("/config set|get|show\uFF1AREPL \u5185\u5FEB\u6377\u8BFB\u5199\u914D\u7F6E\uFF0C\u65E0\u9700\u8FDB\u5165\u5411\u5BFC\uFF08\u70B9\u5206\u8DEF\u5F84 + \u81EA\u52A8\u7C7B\u578B\u8F6C\u6362\uFF09"));
|
|
2287
2327
|
console.log(feat("\u5DE5\u5177\u8C03\u7528\u6700\u7EC8\u56DE\u7B54\u6D41\u5F0F\u5316\uFF1A\u6A21\u62DF\u6253\u5B57\u673A\u6548\u679C\u9010\u5757\u8F93\u51FA\uFF0C\u96F6\u989D\u5916 API \u8C03\u7528\uFF0C\u652F\u6301 Escape \u4E2D\u65AD"));
|
|
2288
2328
|
console.log(feat("/diff \u547D\u4EE4\uFF1A\u663E\u793A\u5F53\u524D session \u5185\u6240\u6709\u6587\u4EF6\u4FEE\u6539\u7684\u6C47\u603B diff\uFF08\u5408\u5E76\u540C\u6587\u4EF6\u591A\u6B21\u4FEE\u6539\uFF09"));
|
|
2329
|
+
console.log(feat("/fork \u5BF9\u8BDD\u5206\u652F\uFF1A\u4ECE\u5F53\u524D\u4F4D\u7F6E\u6216\u6307\u5B9A checkpoint \u5206\u53C9\u4E3A\u65B0 session\uFF0C\u63A2\u7D22\u4E0D\u540C\u65B9\u6848"));
|
|
2289
2330
|
console.log();
|
|
2290
2331
|
}
|
|
2291
2332
|
printPrompt(provider, _model) {
|
|
@@ -2385,8 +2426,8 @@ var Renderer = class {
|
|
|
2385
2426
|
process.stdout.write("\n\n");
|
|
2386
2427
|
}
|
|
2387
2428
|
if (fileStream) {
|
|
2388
|
-
await new Promise((
|
|
2389
|
-
fileStream.end((err) => err ? reject(err) :
|
|
2429
|
+
await new Promise((resolve6, reject) => {
|
|
2430
|
+
fileStream.end((err) => err ? reject(err) : resolve6());
|
|
2390
2431
|
});
|
|
2391
2432
|
const kb = (Buffer.byteLength(fullContent, "utf-8") / 1024).toFixed(1);
|
|
2392
2433
|
process.stdout.write(theme.success(` \u2705 \u5DF2\u4FDD\u5B58: ${options.saveToFile} (${kb} KB)
|
|
@@ -2441,7 +2482,7 @@ var Renderer = class {
|
|
|
2441
2482
|
process.stdout.write(displayed.slice(pos, end));
|
|
2442
2483
|
pos = end;
|
|
2443
2484
|
if (pos < displayed.length) {
|
|
2444
|
-
await new Promise((
|
|
2485
|
+
await new Promise((resolve6) => setTimeout(resolve6, DELAY_MS));
|
|
2445
2486
|
}
|
|
2446
2487
|
}
|
|
2447
2488
|
process.stdout.write("\n\n");
|
|
@@ -2595,7 +2636,7 @@ function formatGitContextForPrompt(ctx) {
|
|
|
2595
2636
|
}
|
|
2596
2637
|
|
|
2597
2638
|
// src/tools/undo-stack.ts
|
|
2598
|
-
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, unlinkSync, existsSync as existsSync4 } from "fs";
|
|
2639
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, unlinkSync, rmdirSync, existsSync as existsSync4 } from "fs";
|
|
2599
2640
|
var MAX_UNDO_DEPTH = 20;
|
|
2600
2641
|
var UndoStack = class {
|
|
2601
2642
|
stack = [];
|
|
@@ -2623,6 +2664,37 @@ var UndoStack = class {
|
|
|
2623
2664
|
this.stack.shift();
|
|
2624
2665
|
}
|
|
2625
2666
|
}
|
|
2667
|
+
/**
|
|
2668
|
+
* 推入一个新建文件的条目(previousContent=null),undo 时删除该文件。
|
|
2669
|
+
* 用于 bash 工具执行后检测到的新建文件。
|
|
2670
|
+
*/
|
|
2671
|
+
pushNewFile(filePath, description) {
|
|
2672
|
+
this.stack.push({
|
|
2673
|
+
filePath,
|
|
2674
|
+
previousContent: null,
|
|
2675
|
+
description,
|
|
2676
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
2677
|
+
});
|
|
2678
|
+
if (this.stack.length > MAX_UNDO_DEPTH) {
|
|
2679
|
+
this.stack.shift();
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
/**
|
|
2683
|
+
* 推入一个新建目录的条目(previousContent=null, isDirectory=true),
|
|
2684
|
+
* undo 时尝试 rmdir(仅空目录可删)。
|
|
2685
|
+
*/
|
|
2686
|
+
pushNewDir(dirPath, description) {
|
|
2687
|
+
this.stack.push({
|
|
2688
|
+
filePath: dirPath,
|
|
2689
|
+
previousContent: null,
|
|
2690
|
+
description,
|
|
2691
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
2692
|
+
isDirectory: true
|
|
2693
|
+
});
|
|
2694
|
+
if (this.stack.length > MAX_UNDO_DEPTH) {
|
|
2695
|
+
this.stack.shift();
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2626
2698
|
/**
|
|
2627
2699
|
* 弹出并执行最近一次撤销操作。
|
|
2628
2700
|
* @returns 撤销结果描述,或 null(栈为空时)
|
|
@@ -2632,10 +2704,22 @@ var UndoStack = class {
|
|
|
2632
2704
|
if (!entry) return null;
|
|
2633
2705
|
try {
|
|
2634
2706
|
if (entry.previousContent === null) {
|
|
2635
|
-
if (
|
|
2636
|
-
|
|
2707
|
+
if (entry.isDirectory) {
|
|
2708
|
+
if (existsSync4(entry.filePath)) {
|
|
2709
|
+
try {
|
|
2710
|
+
rmdirSync(entry.filePath);
|
|
2711
|
+
return { entry, result: `Removed newly created directory: ${entry.filePath}` };
|
|
2712
|
+
} catch {
|
|
2713
|
+
return { entry, result: `Cannot remove directory (not empty): ${entry.filePath}` };
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
return { entry, result: `Directory already removed: ${entry.filePath}` };
|
|
2717
|
+
} else {
|
|
2718
|
+
if (existsSync4(entry.filePath)) {
|
|
2719
|
+
unlinkSync(entry.filePath);
|
|
2720
|
+
}
|
|
2721
|
+
return { entry, result: `Deleted newly created file: ${entry.filePath}` };
|
|
2637
2722
|
}
|
|
2638
|
-
return { entry, result: `Deleted newly created file: ${entry.filePath}` };
|
|
2639
2723
|
} else {
|
|
2640
2724
|
writeFileSync3(entry.filePath, entry.previousContent, "utf-8");
|
|
2641
2725
|
const lines = entry.previousContent.split("\n").length;
|
|
@@ -3173,7 +3257,7 @@ function createDefaultCommands() {
|
|
|
3173
3257
|
" /context - Show or reload hierarchical context layers",
|
|
3174
3258
|
" /status - Show current status",
|
|
3175
3259
|
" /search <keyword> - Search across all session histories",
|
|
3176
|
-
" /undo
|
|
3260
|
+
" /undo [list|<n>] - Undo file ops (list stack, undo N times)",
|
|
3177
3261
|
" /export [md|json] [file] - Export session to file (default: auto-named .md)",
|
|
3178
3262
|
" /tools - List all AI tools available",
|
|
3179
3263
|
" /plugins - Show plugin directory and loaded plugins",
|
|
@@ -3193,6 +3277,7 @@ function createDefaultCommands() {
|
|
|
3193
3277
|
" /doctor - Health check (API keys, config, MCP status)",
|
|
3194
3278
|
" /bug [--copy] - Generate bug report template (--copy to clipboard)",
|
|
3195
3279
|
" /diff [--stats] - Show all file modifications in this session",
|
|
3280
|
+
" /fork [checkpoint] - Fork session from checkpoint or current position",
|
|
3196
3281
|
" /exit - Exit"
|
|
3197
3282
|
] : [];
|
|
3198
3283
|
console.log("\nAvailable commands:");
|
|
@@ -3581,23 +3666,53 @@ ${text}
|
|
|
3581
3666
|
},
|
|
3582
3667
|
{
|
|
3583
3668
|
name: "undo",
|
|
3584
|
-
description: "Undo
|
|
3585
|
-
usage: "/undo",
|
|
3586
|
-
execute(
|
|
3587
|
-
const
|
|
3588
|
-
if (
|
|
3669
|
+
description: "Undo file operations (supports: /undo, /undo list, /undo <n>)",
|
|
3670
|
+
usage: "/undo [list | <n>]",
|
|
3671
|
+
execute(args, ctx) {
|
|
3672
|
+
const sub = args.trim();
|
|
3673
|
+
if (sub === "list") {
|
|
3674
|
+
const history = undoStack.getHistory();
|
|
3675
|
+
if (history.length === 0) {
|
|
3676
|
+
ctx.renderer.printInfo("Undo stack is empty.");
|
|
3677
|
+
return;
|
|
3678
|
+
}
|
|
3679
|
+
console.log(theme.heading("\n Undo Stack (" + history.length + " entries, newest last):\n"));
|
|
3680
|
+
history.forEach((entry, i) => {
|
|
3681
|
+
const timeStr = entry.timestamp.toLocaleTimeString();
|
|
3682
|
+
const typeTag = entry.isDirectory ? theme.accent("[dir]") : entry.previousContent === null ? theme.accent("[new]") : theme.dim("[mod]");
|
|
3683
|
+
console.log(` ${theme.dim(String(i + 1).padStart(3))} ${typeTag} ${entry.description} ${theme.dim(timeStr)}`);
|
|
3684
|
+
});
|
|
3685
|
+
console.log("");
|
|
3686
|
+
return;
|
|
3687
|
+
}
|
|
3688
|
+
const n = sub ? parseInt(sub, 10) : 1;
|
|
3689
|
+
if (isNaN(n) || n < 1) {
|
|
3690
|
+
ctx.renderer.printInfo("Usage: /undo [list | <n>] \u2014 n must be a positive integer.");
|
|
3691
|
+
return;
|
|
3692
|
+
}
|
|
3693
|
+
if (undoStack.depth === 0) {
|
|
3589
3694
|
ctx.renderer.printInfo("Nothing to undo.");
|
|
3590
3695
|
return;
|
|
3591
3696
|
}
|
|
3592
|
-
const
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
ctx.renderer.
|
|
3599
|
-
|
|
3697
|
+
const count = Math.min(n, undoStack.depth);
|
|
3698
|
+
const results = [];
|
|
3699
|
+
for (let i = 0; i < count; i++) {
|
|
3700
|
+
const top = undoStack.peek();
|
|
3701
|
+
if (!top) break;
|
|
3702
|
+
const timeStr = top.timestamp.toLocaleTimeString();
|
|
3703
|
+
ctx.renderer.printInfo(`Undoing [${i + 1}/${count}]: ${top.description} (${timeStr})`);
|
|
3704
|
+
const undoResult = undoStack.undo();
|
|
3705
|
+
if (undoResult) {
|
|
3706
|
+
results.push(undoResult.result);
|
|
3707
|
+
ctx.renderer.printSuccess(" " + undoResult.result);
|
|
3708
|
+
}
|
|
3709
|
+
}
|
|
3710
|
+
if (results.length === 0) {
|
|
3600
3711
|
ctx.renderer.printInfo("Nothing to undo.");
|
|
3712
|
+
} else if (results.length > 1) {
|
|
3713
|
+
console.log(theme.success(`
|
|
3714
|
+
\u2713 ${results.length} operations undone.
|
|
3715
|
+
`));
|
|
3601
3716
|
}
|
|
3602
3717
|
}
|
|
3603
3718
|
},
|
|
@@ -4173,7 +4288,7 @@ ${hint}` : "")
|
|
|
4173
4288
|
description: "Run project tests and show structured report",
|
|
4174
4289
|
usage: "/test [command|filter]",
|
|
4175
4290
|
async execute(args, _ctx) {
|
|
4176
|
-
const { executeTests } = await import("./run-tests-
|
|
4291
|
+
const { executeTests } = await import("./run-tests-JJVZWQKI.js");
|
|
4177
4292
|
const argStr = args.join(" ").trim();
|
|
4178
4293
|
let testArgs = {};
|
|
4179
4294
|
if (argStr) {
|
|
@@ -4500,6 +4615,57 @@ Summary: ${fileMap.size} file(s) \u2014 ${newFiles} new, ${modifiedFiles} modifi
|
|
|
4500
4615
|
console.log();
|
|
4501
4616
|
}
|
|
4502
4617
|
},
|
|
4618
|
+
{
|
|
4619
|
+
name: "fork",
|
|
4620
|
+
description: "Fork current session from a checkpoint or current position",
|
|
4621
|
+
usage: "/fork [checkpoint-name]",
|
|
4622
|
+
async execute(args, ctx) {
|
|
4623
|
+
const session = ctx.sessions.current;
|
|
4624
|
+
if (!session) {
|
|
4625
|
+
ctx.renderer.printInfo("No active session to fork.");
|
|
4626
|
+
return;
|
|
4627
|
+
}
|
|
4628
|
+
const sub = args.join(" ").trim();
|
|
4629
|
+
let messageCount = session.messages.length;
|
|
4630
|
+
let fromLabel = "current position";
|
|
4631
|
+
if (sub) {
|
|
4632
|
+
const cp = session.checkpoints.find((c) => c.name === sub);
|
|
4633
|
+
if (!cp) {
|
|
4634
|
+
const available = session.checkpoints.map((c) => c.name);
|
|
4635
|
+
if (available.length > 0) {
|
|
4636
|
+
ctx.renderer.printInfo(
|
|
4637
|
+
`Checkpoint "${sub}" not found. Available: ${available.join(", ")}`
|
|
4638
|
+
);
|
|
4639
|
+
} else {
|
|
4640
|
+
ctx.renderer.printInfo(
|
|
4641
|
+
`Checkpoint "${sub}" not found. No checkpoints saved. Use /checkpoint save <name> first.`
|
|
4642
|
+
);
|
|
4643
|
+
}
|
|
4644
|
+
return;
|
|
4645
|
+
}
|
|
4646
|
+
messageCount = cp.messageIndex;
|
|
4647
|
+
fromLabel = `checkpoint "${cp.name}" (${cp.messageIndex} messages)`;
|
|
4648
|
+
}
|
|
4649
|
+
try {
|
|
4650
|
+
const originalId = session.id.slice(0, 8);
|
|
4651
|
+
const originalTitle = session.title ?? "(untitled)";
|
|
4652
|
+
const forked = await ctx.forkSession(messageCount);
|
|
4653
|
+
console.log(theme.heading("\n Session Forked\n"));
|
|
4654
|
+
console.log(` ${theme.dim("Original:")} ${theme.accent(originalId)} "${originalTitle}"`);
|
|
4655
|
+
console.log(` ${theme.dim("Forked: ")} ${theme.accent(forked.id.slice(0, 8))} "${forked.title ?? "(untitled)"}"`);
|
|
4656
|
+
console.log(` ${theme.dim("From: ")} ${fromLabel}`);
|
|
4657
|
+
console.log(` ${theme.dim("Messages:")} ${forked.messages.length} copied, ${forked.checkpoints.length} checkpoint(s) preserved`);
|
|
4658
|
+
console.log("");
|
|
4659
|
+
console.log(theme.dim(` Use /session load ${originalId} to switch back to original.`));
|
|
4660
|
+
console.log("");
|
|
4661
|
+
ctx.refreshPrompt();
|
|
4662
|
+
} catch (err) {
|
|
4663
|
+
ctx.renderer.printError(
|
|
4664
|
+
`Fork failed: ${err instanceof Error ? err.message : String(err)}`
|
|
4665
|
+
);
|
|
4666
|
+
}
|
|
4667
|
+
}
|
|
4668
|
+
},
|
|
4503
4669
|
{
|
|
4504
4670
|
name: "exit",
|
|
4505
4671
|
description: "Exit the REPL",
|
|
@@ -4519,7 +4685,7 @@ var IGNORE_ENTER_MS = 80;
|
|
|
4519
4685
|
function selectFromList(prompt, items, initialIndex = 0) {
|
|
4520
4686
|
if (items.length === 0) return Promise.resolve(null);
|
|
4521
4687
|
const PAGE = 12;
|
|
4522
|
-
return new Promise((
|
|
4688
|
+
return new Promise((resolve6) => {
|
|
4523
4689
|
let selected = Math.max(0, Math.min(initialIndex, items.length - 1));
|
|
4524
4690
|
let windowStart = Math.max(0, selected - Math.floor(PAGE / 2));
|
|
4525
4691
|
let lastRenderedLines = 0;
|
|
@@ -4594,7 +4760,7 @@ function selectFromList(prompt, items, initialIndex = 0) {
|
|
|
4594
4760
|
process.stdout.write(chalk5.dim(` \u2714 ${result}
|
|
4595
4761
|
`));
|
|
4596
4762
|
}
|
|
4597
|
-
|
|
4763
|
+
resolve6(result);
|
|
4598
4764
|
};
|
|
4599
4765
|
const handleSequence = (seq) => {
|
|
4600
4766
|
if (seq === "\x1B") {
|
|
@@ -4658,13 +4824,13 @@ function selectFromList(prompt, items, initialIndex = 0) {
|
|
|
4658
4824
|
}
|
|
4659
4825
|
};
|
|
4660
4826
|
if (!process.stdin.isTTY) {
|
|
4661
|
-
|
|
4827
|
+
resolve6(items[0]?.value ?? null);
|
|
4662
4828
|
return;
|
|
4663
4829
|
}
|
|
4664
4830
|
try {
|
|
4665
4831
|
process.stdin.setRawMode(true);
|
|
4666
4832
|
} catch {
|
|
4667
|
-
|
|
4833
|
+
resolve6(items[0]?.value ?? null);
|
|
4668
4834
|
return;
|
|
4669
4835
|
}
|
|
4670
4836
|
savedDataListeners = process.stdin.rawListeners("data");
|
|
@@ -4677,7 +4843,7 @@ function selectFromList(prompt, items, initialIndex = 0) {
|
|
|
4677
4843
|
|
|
4678
4844
|
// src/tools/builtin/bash.ts
|
|
4679
4845
|
import { execSync as execSync4 } from "child_process";
|
|
4680
|
-
import { existsSync as existsSync7 } from "fs";
|
|
4846
|
+
import { existsSync as existsSync7, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
|
|
4681
4847
|
import { platform as platform2 } from "os";
|
|
4682
4848
|
import { resolve as resolve2 } from "path";
|
|
4683
4849
|
var IS_WINDOWS = platform2() === "win32";
|
|
@@ -4749,6 +4915,12 @@ var bashTool = {
|
|
|
4749
4915
|
} else {
|
|
4750
4916
|
actualCommand = command;
|
|
4751
4917
|
}
|
|
4918
|
+
const beforeSnapshot = snapshotDir(effectiveCwd);
|
|
4919
|
+
const parsedTargets = parseCreationTargets(command, effectiveCwd);
|
|
4920
|
+
const parsedTargetsBefore = /* @__PURE__ */ new Map();
|
|
4921
|
+
for (const t of parsedTargets) {
|
|
4922
|
+
parsedTargetsBefore.set(t, existsSync7(t));
|
|
4923
|
+
}
|
|
4752
4924
|
try {
|
|
4753
4925
|
const output = execSync4(actualCommand, {
|
|
4754
4926
|
timeout,
|
|
@@ -4763,9 +4935,11 @@ var bashTool = {
|
|
|
4763
4935
|
}
|
|
4764
4936
|
});
|
|
4765
4937
|
updateCwdFromCommand(command, effectiveCwd);
|
|
4938
|
+
pushBashUndoEntries(beforeSnapshot, parsedTargetsBefore, effectiveCwd);
|
|
4766
4939
|
const result = IS_WINDOWS && Buffer.isBuffer(output) ? output.toString("utf-8") : output;
|
|
4767
4940
|
return result || "(command completed with no output)";
|
|
4768
4941
|
} catch (err) {
|
|
4942
|
+
pushBashUndoEntries(beforeSnapshot, parsedTargetsBefore, effectiveCwd);
|
|
4769
4943
|
if (err && typeof err === "object" && "status" in err) {
|
|
4770
4944
|
const execErr = err;
|
|
4771
4945
|
const stderr = IS_WINDOWS && Buffer.isBuffer(execErr.stderr) ? execErr.stderr.toString("utf-8").trim() : execErr.stderr?.toString().trim() ?? "";
|
|
@@ -4798,6 +4972,69 @@ function fixWindowsDeleteCommand(command) {
|
|
|
4798
4972
|
}
|
|
4799
4973
|
);
|
|
4800
4974
|
}
|
|
4975
|
+
function snapshotDir(dir) {
|
|
4976
|
+
try {
|
|
4977
|
+
return new Set(readdirSync3(dir).map((name) => resolve2(dir, name)));
|
|
4978
|
+
} catch {
|
|
4979
|
+
return /* @__PURE__ */ new Set();
|
|
4980
|
+
}
|
|
4981
|
+
}
|
|
4982
|
+
function parseCreationTargets(command, cwd) {
|
|
4983
|
+
const targets = [];
|
|
4984
|
+
for (const m of command.matchAll(/(?:echo|cat|printf)\s+[^>]*>\s*(['"]?)([^\s;&|'"]+)\1/g)) {
|
|
4985
|
+
if (m[2]) targets.push(resolve2(cwd, m[2]));
|
|
4986
|
+
}
|
|
4987
|
+
for (const m of command.matchAll(/\btouch\s+((?:['"]?[^\s;&|'"]+['"]?\s*)+)/g)) {
|
|
4988
|
+
for (const f of m[1].trim().split(/\s+/)) {
|
|
4989
|
+
const clean = f.replace(/^['"]|['"]$/g, "");
|
|
4990
|
+
if (clean && !clean.startsWith("-")) targets.push(resolve2(cwd, clean));
|
|
4991
|
+
}
|
|
4992
|
+
}
|
|
4993
|
+
for (const m of command.matchAll(/\bmkdir\s+(?:-\w+\s+)*((?:['"]?[^\s;&|'"]+['"]?\s*)+)/g)) {
|
|
4994
|
+
for (const d of m[1].trim().split(/\s+/)) {
|
|
4995
|
+
const clean = d.replace(/^['"]|['"]$/g, "");
|
|
4996
|
+
if (clean && !clean.startsWith("-")) targets.push(resolve2(cwd, clean));
|
|
4997
|
+
}
|
|
4998
|
+
}
|
|
4999
|
+
for (const m of command.matchAll(/\bcp\s+(?:-\w+\s+)*['"]?[^\s;&|'"]+['"]?\s+(['"]?)([^\s;&|'"]+)\1/g)) {
|
|
5000
|
+
if (m[2]) targets.push(resolve2(cwd, m[2]));
|
|
5001
|
+
}
|
|
5002
|
+
for (const m of command.matchAll(/\bNew-Item\s+(?:-(?:Path|ItemType)\s+\w+\s+)*['"]?([^\s;&|'"]+)['"]?/gi)) {
|
|
5003
|
+
if (m[1] && !m[1].startsWith("-")) targets.push(resolve2(cwd, m[1]));
|
|
5004
|
+
}
|
|
5005
|
+
return [...new Set(targets)];
|
|
5006
|
+
}
|
|
5007
|
+
function pushBashUndoEntries(beforeSnapshot, parsedTargetsBefore, cwd) {
|
|
5008
|
+
const tracked = /* @__PURE__ */ new Set();
|
|
5009
|
+
const afterSnapshot = snapshotDir(cwd);
|
|
5010
|
+
for (const absPath of afterSnapshot) {
|
|
5011
|
+
if (!beforeSnapshot.has(absPath)) {
|
|
5012
|
+
try {
|
|
5013
|
+
const st = statSync3(absPath);
|
|
5014
|
+
if (st.isDirectory()) {
|
|
5015
|
+
undoStack.pushNewDir(absPath, `bash (new dir): ${absPath}`);
|
|
5016
|
+
} else {
|
|
5017
|
+
undoStack.pushNewFile(absPath, `bash (new file): ${absPath}`);
|
|
5018
|
+
}
|
|
5019
|
+
tracked.add(absPath);
|
|
5020
|
+
} catch {
|
|
5021
|
+
}
|
|
5022
|
+
}
|
|
5023
|
+
}
|
|
5024
|
+
for (const [target, existedBefore] of parsedTargetsBefore) {
|
|
5025
|
+
if (!existedBefore && !tracked.has(target) && existsSync7(target)) {
|
|
5026
|
+
try {
|
|
5027
|
+
const st = statSync3(target);
|
|
5028
|
+
if (st.isDirectory()) {
|
|
5029
|
+
undoStack.pushNewDir(target, `bash (new dir): ${target}`);
|
|
5030
|
+
} else {
|
|
5031
|
+
undoStack.pushNewFile(target, `bash (new file): ${target}`);
|
|
5032
|
+
}
|
|
5033
|
+
} catch {
|
|
5034
|
+
}
|
|
5035
|
+
}
|
|
5036
|
+
}
|
|
5037
|
+
}
|
|
4801
5038
|
function updateCwdFromCommand(command, baseCwd) {
|
|
4802
5039
|
const cdMatches = [...command.matchAll(/(?:^|[;&|])\s*cd\s+(['"]?)([^\s;&|'"]+)\1/g)];
|
|
4803
5040
|
if (cdMatches.length === 0) return;
|
|
@@ -4814,8 +5051,9 @@ function updateCwdFromCommand(command, baseCwd) {
|
|
|
4814
5051
|
}
|
|
4815
5052
|
|
|
4816
5053
|
// src/tools/builtin/read-file.ts
|
|
4817
|
-
import { readFileSync as readFileSync5, existsSync as existsSync8, statSync as
|
|
4818
|
-
import {
|
|
5054
|
+
import { readFileSync as readFileSync5, existsSync as existsSync8, statSync as statSync4, readdirSync as readdirSync4 } from "fs";
|
|
5055
|
+
import { execSync as execSync5 } from "child_process";
|
|
5056
|
+
import { extname, resolve as resolve3, basename as basename2, sep, dirname as dirname3 } from "path";
|
|
4819
5057
|
import { homedir as homedir2 } from "os";
|
|
4820
5058
|
var MAX_FILE_BYTES = 10 * 1024 * 1024;
|
|
4821
5059
|
function getSensitiveWarning(normalizedPath) {
|
|
@@ -4883,6 +5121,55 @@ function isBinaryBuffer(buf) {
|
|
|
4883
5121
|
}
|
|
4884
5122
|
return nullCount / sample.length > 0.1;
|
|
4885
5123
|
}
|
|
5124
|
+
function findSimilarFiles(filePath) {
|
|
5125
|
+
const targetName = basename2(filePath).toLowerCase();
|
|
5126
|
+
if (!targetName) return [];
|
|
5127
|
+
const cwd = process.cwd();
|
|
5128
|
+
const candidates = [];
|
|
5129
|
+
try {
|
|
5130
|
+
for (const entry of readdirSync4(cwd, { withFileTypes: true })) {
|
|
5131
|
+
if (entry.isFile() && entry.name.toLowerCase() === targetName) {
|
|
5132
|
+
candidates.push(entry.name);
|
|
5133
|
+
}
|
|
5134
|
+
if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
|
|
5135
|
+
try {
|
|
5136
|
+
const subDir = resolve3(cwd, entry.name);
|
|
5137
|
+
for (const sub of readdirSync4(subDir, { withFileTypes: true })) {
|
|
5138
|
+
if (sub.isFile() && sub.name.toLowerCase() === targetName) {
|
|
5139
|
+
candidates.push(`${entry.name}/${sub.name}`);
|
|
5140
|
+
}
|
|
5141
|
+
}
|
|
5142
|
+
} catch {
|
|
5143
|
+
}
|
|
5144
|
+
}
|
|
5145
|
+
}
|
|
5146
|
+
} catch {
|
|
5147
|
+
}
|
|
5148
|
+
return candidates.slice(0, 5);
|
|
5149
|
+
}
|
|
5150
|
+
function tryExtractPdfText(absPath) {
|
|
5151
|
+
try {
|
|
5152
|
+
const output = execSync5(`pdftotext "${absPath}" -`, {
|
|
5153
|
+
timeout: 15e3,
|
|
5154
|
+
encoding: "utf-8",
|
|
5155
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
5156
|
+
});
|
|
5157
|
+
if (output.trim().length > 0) return output;
|
|
5158
|
+
} catch {
|
|
5159
|
+
}
|
|
5160
|
+
try {
|
|
5161
|
+
const pyScript = `import sys; exec("try:\\n from pdfminer.high_level import extract_text\\n print(extract_text(sys.argv[1]))\\nexcept: pass")`;
|
|
5162
|
+
const output = execSync5(`python -c "${pyScript}" "${absPath}"`, {
|
|
5163
|
+
timeout: 15e3,
|
|
5164
|
+
encoding: "utf-8",
|
|
5165
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
5166
|
+
env: { ...process.env, PYTHONUTF8: "1", PYTHONIOENCODING: "utf-8" }
|
|
5167
|
+
});
|
|
5168
|
+
if (output.trim().length > 0) return output;
|
|
5169
|
+
} catch {
|
|
5170
|
+
}
|
|
5171
|
+
return null;
|
|
5172
|
+
}
|
|
4886
5173
|
var readFileTool = {
|
|
4887
5174
|
definition: {
|
|
4888
5175
|
name: "read_file",
|
|
@@ -4907,8 +5194,24 @@ var readFileTool = {
|
|
|
4907
5194
|
const encoding = args["encoding"] ?? "utf-8";
|
|
4908
5195
|
if (!filePath) throw new Error("path is required");
|
|
4909
5196
|
const normalizedPath = resolve3(filePath);
|
|
4910
|
-
if (!existsSync8(normalizedPath))
|
|
4911
|
-
|
|
5197
|
+
if (!existsSync8(normalizedPath)) {
|
|
5198
|
+
const suggestions = findSimilarFiles(filePath);
|
|
5199
|
+
if (suggestions.length > 0) {
|
|
5200
|
+
throw new Error(
|
|
5201
|
+
`File not found: ${filePath}
|
|
5202
|
+
\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55: ${process.cwd()}
|
|
5203
|
+
\u627E\u5230\u540C\u540D\u6587\u4EF6\uFF0C\u4F60\u662F\u5426\u8981\u627E\uFF1A
|
|
5204
|
+
` + suggestions.map((s) => ` \u2192 ${s}`).join("\n") + `
|
|
5205
|
+
\u8BF7\u4F7F\u7528\u6B63\u786E\u7684\u76F8\u5BF9\u8DEF\u5F84\u91CD\u8BD5\u3002`
|
|
5206
|
+
);
|
|
5207
|
+
}
|
|
5208
|
+
throw new Error(
|
|
5209
|
+
`File not found: ${filePath}
|
|
5210
|
+
\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55: ${process.cwd()}
|
|
5211
|
+
\u8BF7\u4F7F\u7528 list_dir \u786E\u8BA4\u6587\u4EF6\u8DEF\u5F84\u540E\u91CD\u8BD5\u3002`
|
|
5212
|
+
);
|
|
5213
|
+
}
|
|
5214
|
+
const { size } = statSync4(normalizedPath);
|
|
4912
5215
|
if (size > MAX_FILE_BYTES) {
|
|
4913
5216
|
const mb = (size / 1024 / 1024).toFixed(1);
|
|
4914
5217
|
return `[File too large: ${filePath} (${mb} MB)]
|
|
@@ -4920,12 +5223,31 @@ var readFileTool = {
|
|
|
4920
5223
|
}
|
|
4921
5224
|
const sensitiveWarning = getSensitiveWarning(normalizedPath);
|
|
4922
5225
|
const ext = extname(normalizedPath).toLowerCase();
|
|
5226
|
+
if (ext === ".pdf") {
|
|
5227
|
+
const pdfText = tryExtractPdfText(normalizedPath);
|
|
5228
|
+
if (pdfText) {
|
|
5229
|
+
const lines2 = pdfText.split("\n").length;
|
|
5230
|
+
return `[PDF extracted: ${filePath} | ${lines2} lines]
|
|
5231
|
+
|
|
5232
|
+
${pdfText}`;
|
|
5233
|
+
}
|
|
5234
|
+
const dir = dirname3(normalizedPath);
|
|
5235
|
+
const nameNoExt = basename2(normalizedPath, ext);
|
|
5236
|
+
const textAlts = [".md", ".txt", ".html"].map((e) => resolve3(dir, nameNoExt + e)).filter(existsSync8);
|
|
5237
|
+
return `[Binary file: ${filePath}]
|
|
5238
|
+
PDF \u6587\u672C\u63D0\u53D6\u5931\u8D25\uFF08pdftotext \u548C pdfminer \u5747\u4E0D\u53EF\u7528\uFF09\u3002
|
|
5239
|
+
` + (textAlts.length > 0 ? `\u627E\u5230\u53EF\u66FF\u4EE3\u7684\u6587\u672C\u7248\u672C\uFF1A
|
|
5240
|
+
${textAlts.map((p) => ` \u2192 ${basename2(p)}`).join("\n")}
|
|
5241
|
+
\u8BF7\u4F7F\u7528 read_file \u8BFB\u53D6\u4E0A\u8FF0\u6587\u4EF6\u3002` : `\u5982\u9700\u4F7F\u7528\u5176\u5185\u5BB9\uFF0C\u8BF7\u4F7F\u7528 bash \u5DE5\u5177\u5B89\u88C5\u5E76\u8C03\u7528 pdftotext\uFF1A
|
|
5242
|
+
pip install pdfminer.six # \u5B89\u88C5 Python PDF \u5E93
|
|
5243
|
+
\u6216\u5C06 PDF \u624B\u52A8\u8F6C\u6362\u4E3A .md / .txt \u6587\u4EF6\u540E\u518D\u8BFB\u53D6\u3002`);
|
|
5244
|
+
}
|
|
4923
5245
|
if (BINARY_EXTENSIONS.has(ext)) {
|
|
4924
5246
|
return `[Binary file: ${filePath}]
|
|
4925
5247
|
\u6B64\u6587\u4EF6\u4E3A\u4E8C\u8FDB\u5236\u683C\u5F0F\uFF08${ext}\uFF09\uFF0C\u65E0\u6CD5\u4F5C\u4E3A\u6587\u672C\u8BFB\u53D6\u3002
|
|
4926
5248
|
\u5982\u9700\u4F7F\u7528\u5176\u5185\u5BB9\uFF0C\u8BF7\u8003\u8651\uFF1A
|
|
4927
5249
|
1. \u5C06\u6587\u4EF6\u8F6C\u6362\u4E3A\u6587\u672C\u683C\u5F0F\u540E\u518D\u8BFB\u53D6
|
|
4928
|
-
2. \u4F7F\u7528 bash \u5DE5\u5177\u8C03\u7528\u5916\u90E8\u8F6C\u6362\u7A0B\u5E8F\uFF08\u5982
|
|
5250
|
+
2. \u4F7F\u7528 bash \u5DE5\u5177\u8C03\u7528\u5916\u90E8\u8F6C\u6362\u7A0B\u5E8F\uFF08\u5982 pandoc \u7B49\uFF09
|
|
4929
5251
|
3. \u82E5\u6709\u5BF9\u5E94\u7684\u7EAF\u6587\u672C\u7248\u672C\uFF08.md / .txt\uFF09\uFF0C\u8BF7\u76F4\u63A5\u8BFB\u53D6\u90A3\u4E2A\u6587\u4EF6`;
|
|
4930
5252
|
}
|
|
4931
5253
|
const buf = readFileSync5(normalizedPath);
|
|
@@ -4949,7 +5271,7 @@ ${content}`;
|
|
|
4949
5271
|
|
|
4950
5272
|
// src/tools/builtin/write-file.ts
|
|
4951
5273
|
import { writeFileSync as writeFileSync5, appendFileSync as appendFileSync2, mkdirSync as mkdirSync5 } from "fs";
|
|
4952
|
-
import { dirname as
|
|
5274
|
+
import { dirname as dirname4 } from "path";
|
|
4953
5275
|
var writeFileTool = {
|
|
4954
5276
|
definition: {
|
|
4955
5277
|
name: "write_file",
|
|
@@ -4989,7 +5311,7 @@ var writeFileTool = {
|
|
|
4989
5311
|
if (!appendMode) {
|
|
4990
5312
|
undoStack.push(filePath, `write_file: ${filePath}`);
|
|
4991
5313
|
}
|
|
4992
|
-
mkdirSync5(
|
|
5314
|
+
mkdirSync5(dirname4(filePath), { recursive: true });
|
|
4993
5315
|
if (appendMode) {
|
|
4994
5316
|
appendFileSync2(filePath, content, encoding);
|
|
4995
5317
|
} else {
|
|
@@ -5221,8 +5543,8 @@ function truncatePreview(str, maxLen = 80) {
|
|
|
5221
5543
|
}
|
|
5222
5544
|
|
|
5223
5545
|
// src/tools/builtin/list-dir.ts
|
|
5224
|
-
import { readdirSync as
|
|
5225
|
-
import { join as join6 } from "path";
|
|
5546
|
+
import { readdirSync as readdirSync5, statSync as statSync5, existsSync as existsSync10 } from "fs";
|
|
5547
|
+
import { join as join6, basename as basename3 } from "path";
|
|
5226
5548
|
var listDirTool = {
|
|
5227
5549
|
definition: {
|
|
5228
5550
|
name: "list_dir",
|
|
@@ -5244,7 +5566,33 @@ var listDirTool = {
|
|
|
5244
5566
|
async execute(args) {
|
|
5245
5567
|
const dirPath = String(args["path"] ?? process.cwd());
|
|
5246
5568
|
const recursive = Boolean(args["recursive"] ?? false);
|
|
5247
|
-
if (!existsSync10(dirPath))
|
|
5569
|
+
if (!existsSync10(dirPath)) {
|
|
5570
|
+
const targetName = basename3(dirPath).toLowerCase();
|
|
5571
|
+
const cwd = process.cwd();
|
|
5572
|
+
const suggestions = [];
|
|
5573
|
+
try {
|
|
5574
|
+
for (const entry of readdirSync5(cwd, { withFileTypes: true })) {
|
|
5575
|
+
if (entry.isDirectory() && entry.name.toLowerCase() === targetName) {
|
|
5576
|
+
suggestions.push(entry.name);
|
|
5577
|
+
}
|
|
5578
|
+
}
|
|
5579
|
+
} catch {
|
|
5580
|
+
}
|
|
5581
|
+
if (suggestions.length > 0) {
|
|
5582
|
+
throw new Error(
|
|
5583
|
+
`Directory not found: ${dirPath}
|
|
5584
|
+
\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55: ${cwd}
|
|
5585
|
+
\u627E\u5230\u540C\u540D\u76EE\u5F55\uFF1A
|
|
5586
|
+
` + suggestions.map((s) => ` \u2192 ${s}`).join("\n") + `
|
|
5587
|
+
\u8BF7\u4F7F\u7528\u6B63\u786E\u7684\u76F8\u5BF9\u8DEF\u5F84\u91CD\u8BD5\u3002`
|
|
5588
|
+
);
|
|
5589
|
+
}
|
|
5590
|
+
throw new Error(
|
|
5591
|
+
`Directory not found: ${dirPath}
|
|
5592
|
+
\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55: ${cwd}
|
|
5593
|
+
\u8BF7\u5148\u4F7F\u7528 list_dir\uFF08\u4E0D\u4F20 path\uFF09\u67E5\u770B\u5F53\u524D\u76EE\u5F55\u7ED3\u6784\u3002`
|
|
5594
|
+
);
|
|
5595
|
+
}
|
|
5248
5596
|
const lines = [`Directory: ${dirPath}
|
|
5249
5597
|
`];
|
|
5250
5598
|
listRecursive(dirPath, "", recursive, lines);
|
|
@@ -5254,7 +5602,7 @@ var listDirTool = {
|
|
|
5254
5602
|
function listRecursive(basePath, indent, recursive, lines) {
|
|
5255
5603
|
let entries;
|
|
5256
5604
|
try {
|
|
5257
|
-
entries =
|
|
5605
|
+
entries = readdirSync5(basePath, { withFileTypes: true });
|
|
5258
5606
|
} catch {
|
|
5259
5607
|
lines.push(`${indent}(permission denied)`);
|
|
5260
5608
|
return;
|
|
@@ -5277,7 +5625,7 @@ function listRecursive(basePath, indent, recursive, lines) {
|
|
|
5277
5625
|
}
|
|
5278
5626
|
} else {
|
|
5279
5627
|
try {
|
|
5280
|
-
const stat =
|
|
5628
|
+
const stat = statSync5(join6(basePath, entry.name));
|
|
5281
5629
|
const size = formatSize(stat.size);
|
|
5282
5630
|
lines.push(`${indent}\u{1F4C4} ${entry.name} (${size})`);
|
|
5283
5631
|
} catch {
|
|
@@ -5293,7 +5641,7 @@ function formatSize(bytes) {
|
|
|
5293
5641
|
}
|
|
5294
5642
|
|
|
5295
5643
|
// src/tools/builtin/grep-files.ts
|
|
5296
|
-
import { readdirSync as
|
|
5644
|
+
import { readdirSync as readdirSync6, readFileSync as readFileSync7, statSync as statSync6, existsSync as existsSync11 } from "fs";
|
|
5297
5645
|
import { join as join7, relative } from "path";
|
|
5298
5646
|
var grepFilesTool = {
|
|
5299
5647
|
definition: {
|
|
@@ -5354,7 +5702,7 @@ var grepFilesTool = {
|
|
|
5354
5702
|
regex = new RegExp(escaped, ignoreCase ? "gi" : "g");
|
|
5355
5703
|
}
|
|
5356
5704
|
const results = [];
|
|
5357
|
-
const stat =
|
|
5705
|
+
const stat = statSync6(rootPath);
|
|
5358
5706
|
if (stat.isFile()) {
|
|
5359
5707
|
searchInFile(rootPath, rootPath, regex, contextLines, maxResults, results);
|
|
5360
5708
|
} else {
|
|
@@ -5410,7 +5758,7 @@ function collectFiles(dirPath, filePattern, results, regex, contextLines, maxRes
|
|
|
5410
5758
|
if (results.length >= maxResults) return;
|
|
5411
5759
|
let entries;
|
|
5412
5760
|
try {
|
|
5413
|
-
entries =
|
|
5761
|
+
entries = readdirSync6(dirPath, { withFileTypes: true });
|
|
5414
5762
|
} catch {
|
|
5415
5763
|
return;
|
|
5416
5764
|
}
|
|
@@ -5463,8 +5811,8 @@ function searchInFile(fullPath, displayPath, regex, contextLines, maxResults, re
|
|
|
5463
5811
|
}
|
|
5464
5812
|
|
|
5465
5813
|
// src/tools/builtin/glob-files.ts
|
|
5466
|
-
import { readdirSync as
|
|
5467
|
-
import { join as join8, relative as relative2, basename as
|
|
5814
|
+
import { readdirSync as readdirSync7, statSync as statSync7, existsSync as existsSync12 } from "fs";
|
|
5815
|
+
import { join as join8, relative as relative2, basename as basename4 } from "path";
|
|
5468
5816
|
var globFilesTool = {
|
|
5469
5817
|
definition: {
|
|
5470
5818
|
name: "glob_files",
|
|
@@ -5568,7 +5916,7 @@ function collectMatchingFiles(dirPath, rootPath, regex, results, maxResults) {
|
|
|
5568
5916
|
if (results.length >= maxResults) return;
|
|
5569
5917
|
let entries;
|
|
5570
5918
|
try {
|
|
5571
|
-
entries =
|
|
5919
|
+
entries = readdirSync7(dirPath, { withFileTypes: true });
|
|
5572
5920
|
} catch {
|
|
5573
5921
|
return;
|
|
5574
5922
|
}
|
|
@@ -5580,9 +5928,9 @@ function collectMatchingFiles(dirPath, rootPath, regex, results, maxResults) {
|
|
|
5580
5928
|
collectMatchingFiles(fullPath, rootPath, regex, results, maxResults);
|
|
5581
5929
|
} else if (entry.isFile()) {
|
|
5582
5930
|
const relPath = relative2(rootPath, fullPath).replace(/\\/g, "/");
|
|
5583
|
-
if (regex.test(relPath) || regex.test(
|
|
5931
|
+
if (regex.test(relPath) || regex.test(basename4(relPath))) {
|
|
5584
5932
|
try {
|
|
5585
|
-
const stat =
|
|
5933
|
+
const stat = statSync7(fullPath);
|
|
5586
5934
|
results.push({ relPath, absPath: fullPath, mtime: stat.mtimeMs });
|
|
5587
5935
|
} catch {
|
|
5588
5936
|
results.push({ relPath, absPath: fullPath, mtime: 0 });
|
|
@@ -5656,7 +6004,7 @@ var runInteractiveTool = {
|
|
|
5656
6004
|
PYTHONDONTWRITEBYTECODE: "1"
|
|
5657
6005
|
};
|
|
5658
6006
|
const prefixWarnings = [argsTypeWarning, stdinTypeWarning].filter(Boolean).join("");
|
|
5659
|
-
return new Promise((
|
|
6007
|
+
return new Promise((resolve6) => {
|
|
5660
6008
|
const child = spawn(executable, cmdArgs.map(String), {
|
|
5661
6009
|
cwd: process.cwd(),
|
|
5662
6010
|
env,
|
|
@@ -5689,22 +6037,22 @@ var runInteractiveTool = {
|
|
|
5689
6037
|
setTimeout(writeNextLine, 400);
|
|
5690
6038
|
const timer = setTimeout(() => {
|
|
5691
6039
|
child.kill();
|
|
5692
|
-
|
|
6040
|
+
resolve6(`${prefixWarnings}[Timeout after ${timeout}ms]
|
|
5693
6041
|
${buildOutput(stdout, stderr)}`);
|
|
5694
6042
|
}, timeout);
|
|
5695
6043
|
child.on("close", (code) => {
|
|
5696
6044
|
clearTimeout(timer);
|
|
5697
6045
|
const output = buildOutput(stdout, stderr);
|
|
5698
6046
|
if (code !== 0 && code !== null) {
|
|
5699
|
-
|
|
6047
|
+
resolve6(`${prefixWarnings}Exit code ${code}:
|
|
5700
6048
|
${output}`);
|
|
5701
6049
|
} else {
|
|
5702
|
-
|
|
6050
|
+
resolve6(`${prefixWarnings}${output || "(no output)"}`);
|
|
5703
6051
|
}
|
|
5704
6052
|
});
|
|
5705
6053
|
child.on("error", (err) => {
|
|
5706
6054
|
clearTimeout(timer);
|
|
5707
|
-
|
|
6055
|
+
resolve6(
|
|
5708
6056
|
`${prefixWarnings}Failed to start process "${executable}": ${err.message}
|
|
5709
6057
|
Hint: On Windows, use the full path to the executable, e.g.:
|
|
5710
6058
|
C:\\Users\\Jinzd\\anaconda3\\envs\\python312\\python.exe`
|
|
@@ -5925,7 +6273,7 @@ var webFetchTool = {
|
|
|
5925
6273
|
|
|
5926
6274
|
// src/tools/builtin/save-last-response.ts
|
|
5927
6275
|
import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync6 } from "fs";
|
|
5928
|
-
import { dirname as
|
|
6276
|
+
import { dirname as dirname5 } from "path";
|
|
5929
6277
|
var lastResponseStore = { content: "" };
|
|
5930
6278
|
var saveLastResponseTool = {
|
|
5931
6279
|
definition: {
|
|
@@ -5958,7 +6306,7 @@ var saveLastResponseTool = {
|
|
|
5958
6306
|
throw new Error("\u6CA1\u6709\u53EF\u4FDD\u5B58\u7684\u5185\u5BB9\uFF1AAI \u5C1A\u672A\u4EA7\u751F\u4EFB\u4F55\u56DE\u590D\uFF0C\u6216\u4E0A\u6B21\u56DE\u590D\u4E3A\u7A7A\u3002");
|
|
5959
6307
|
}
|
|
5960
6308
|
undoStack.push(filePath, `save_last_response: ${filePath}`);
|
|
5961
|
-
mkdirSync6(
|
|
6309
|
+
mkdirSync6(dirname5(filePath), { recursive: true });
|
|
5962
6310
|
writeFileSync7(filePath, content, "utf-8");
|
|
5963
6311
|
const lines = content.split("\n").length;
|
|
5964
6312
|
return `File saved: ${filePath} (${lines} lines, ${content.length} bytes)`;
|
|
@@ -6051,7 +6399,7 @@ function promptUser(rl, question) {
|
|
|
6051
6399
|
console.log();
|
|
6052
6400
|
console.log(chalk6.cyan("\u2753 ") + chalk6.bold(question));
|
|
6053
6401
|
process.stdout.write(chalk6.cyan("> "));
|
|
6054
|
-
return new Promise((
|
|
6402
|
+
return new Promise((resolve6) => {
|
|
6055
6403
|
let completed = false;
|
|
6056
6404
|
const cleanup = (answer) => {
|
|
6057
6405
|
if (completed) return;
|
|
@@ -6061,7 +6409,7 @@ function promptUser(rl, question) {
|
|
|
6061
6409
|
rl.pause();
|
|
6062
6410
|
rlAny.output = savedOutput;
|
|
6063
6411
|
askUserContext.prompting = false;
|
|
6064
|
-
|
|
6412
|
+
resolve6(answer);
|
|
6065
6413
|
};
|
|
6066
6414
|
const onLine = (line) => {
|
|
6067
6415
|
cleanup(line);
|
|
@@ -6553,7 +6901,7 @@ var spawnAgentTool = {
|
|
|
6553
6901
|
|
|
6554
6902
|
// src/tools/registry.ts
|
|
6555
6903
|
import { pathToFileURL } from "url";
|
|
6556
|
-
import { existsSync as existsSync14, mkdirSync as mkdirSync8, readdirSync as
|
|
6904
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync8, readdirSync as readdirSync8 } from "fs";
|
|
6557
6905
|
import { join as join10 } from "path";
|
|
6558
6906
|
var ToolRegistry = class {
|
|
6559
6907
|
tools = /* @__PURE__ */ new Map();
|
|
@@ -6636,7 +6984,7 @@ var ToolRegistry = class {
|
|
|
6636
6984
|
}
|
|
6637
6985
|
let files;
|
|
6638
6986
|
try {
|
|
6639
|
-
files =
|
|
6987
|
+
files = readdirSync8(pluginsDir).filter((f) => f.endsWith(".js"));
|
|
6640
6988
|
} catch {
|
|
6641
6989
|
return 0;
|
|
6642
6990
|
}
|
|
@@ -6690,7 +7038,7 @@ import chalk9 from "chalk";
|
|
|
6690
7038
|
import { existsSync as existsSync15, readFileSync as readFileSync9 } from "fs";
|
|
6691
7039
|
|
|
6692
7040
|
// src/tools/hooks.ts
|
|
6693
|
-
import { execSync as
|
|
7041
|
+
import { execSync as execSync6 } from "child_process";
|
|
6694
7042
|
function runHook(template, vars) {
|
|
6695
7043
|
if (!template) return;
|
|
6696
7044
|
let cmd = template;
|
|
@@ -6699,7 +7047,7 @@ function runHook(template, vars) {
|
|
|
6699
7047
|
cmd = cmd.replace(/\{args\}/g, vars.args ?? "");
|
|
6700
7048
|
cmd = cmd.replace(/\{status\}/g, vars.status ?? "");
|
|
6701
7049
|
try {
|
|
6702
|
-
|
|
7050
|
+
execSync6(cmd, {
|
|
6703
7051
|
timeout: 5e3,
|
|
6704
7052
|
stdio: ["pipe", "pipe", "pipe"],
|
|
6705
7053
|
encoding: "utf-8"
|
|
@@ -6960,7 +7308,7 @@ var ToolExecutor = class {
|
|
|
6960
7308
|
rl.resume();
|
|
6961
7309
|
process.stdout.write(prompt);
|
|
6962
7310
|
this.confirming = true;
|
|
6963
|
-
return new Promise((
|
|
7311
|
+
return new Promise((resolve6) => {
|
|
6964
7312
|
let completed = false;
|
|
6965
7313
|
const cleanup = (result) => {
|
|
6966
7314
|
if (completed) return;
|
|
@@ -6970,7 +7318,7 @@ var ToolExecutor = class {
|
|
|
6970
7318
|
rl.pause();
|
|
6971
7319
|
rlAny.output = savedOutput;
|
|
6972
7320
|
this.confirming = false;
|
|
6973
|
-
|
|
7321
|
+
resolve6(result);
|
|
6974
7322
|
};
|
|
6975
7323
|
const onLine = (line) => {
|
|
6976
7324
|
const input2 = line.trim().toLowerCase();
|
|
@@ -7131,7 +7479,7 @@ var ToolExecutor = class {
|
|
|
7131
7479
|
rl.resume();
|
|
7132
7480
|
process.stdout.write(color("Proceed? [y/N] (type y + Enter to confirm) "));
|
|
7133
7481
|
this.confirming = true;
|
|
7134
|
-
return new Promise((
|
|
7482
|
+
return new Promise((resolve6) => {
|
|
7135
7483
|
let completed = false;
|
|
7136
7484
|
const cleanup = (answer) => {
|
|
7137
7485
|
if (completed) return;
|
|
@@ -7141,7 +7489,7 @@ var ToolExecutor = class {
|
|
|
7141
7489
|
rl.pause();
|
|
7142
7490
|
rlAny.output = savedOutput;
|
|
7143
7491
|
this.confirming = false;
|
|
7144
|
-
|
|
7492
|
+
resolve6(answer === "y");
|
|
7145
7493
|
};
|
|
7146
7494
|
const onLine = (line) => {
|
|
7147
7495
|
cleanup(line.trim().toLowerCase());
|
|
@@ -7396,9 +7744,9 @@ Managing ${displayName} API Key`);
|
|
|
7396
7744
|
};
|
|
7397
7745
|
|
|
7398
7746
|
// src/repl/custom-commands.ts
|
|
7399
|
-
import { existsSync as existsSync16, readFileSync as readFileSync10, readdirSync as
|
|
7747
|
+
import { existsSync as existsSync16, readFileSync as readFileSync10, readdirSync as readdirSync9, mkdirSync as mkdirSync9 } from "fs";
|
|
7400
7748
|
import { join as join11, extname as extname3 } from "path";
|
|
7401
|
-
import { execSync as
|
|
7749
|
+
import { execSync as execSync7 } from "child_process";
|
|
7402
7750
|
function parseSimpleYaml(text) {
|
|
7403
7751
|
const result = {};
|
|
7404
7752
|
for (const line of text.split("\n")) {
|
|
@@ -7443,7 +7791,7 @@ function expandTemplate(template, args) {
|
|
|
7443
7791
|
});
|
|
7444
7792
|
result = result.replace(/\{\{git-diff\}\}/g, () => {
|
|
7445
7793
|
try {
|
|
7446
|
-
return
|
|
7794
|
+
return execSync7("git diff", { encoding: "utf-8", timeout: 1e4 }).trim();
|
|
7447
7795
|
} catch {
|
|
7448
7796
|
return "[No git diff available]";
|
|
7449
7797
|
}
|
|
@@ -7466,7 +7814,7 @@ var CustomCommandManager = class {
|
|
|
7466
7814
|
return 0;
|
|
7467
7815
|
}
|
|
7468
7816
|
let count = 0;
|
|
7469
|
-
for (const file of
|
|
7817
|
+
for (const file of readdirSync9(this.commandsDir)) {
|
|
7470
7818
|
if (extname3(file) !== ".md") continue;
|
|
7471
7819
|
const cmd = parseCommandFile(join11(this.commandsDir, file));
|
|
7472
7820
|
if (cmd) {
|
|
@@ -7693,7 +8041,7 @@ var McpClient = class {
|
|
|
7693
8041
|
// 内部方法:JSON-RPC 通信
|
|
7694
8042
|
// ══════════════════════════════════════════════════════════════════
|
|
7695
8043
|
sendRequest(method, params) {
|
|
7696
|
-
return new Promise((
|
|
8044
|
+
return new Promise((resolve6, reject) => {
|
|
7697
8045
|
if (!this.process?.stdin?.writable) {
|
|
7698
8046
|
return reject(new Error(`MCP server [${this.serverId}] stdin not writable`));
|
|
7699
8047
|
}
|
|
@@ -7716,7 +8064,7 @@ var McpClient = class {
|
|
|
7716
8064
|
this.pendingRequests.set(id, {
|
|
7717
8065
|
resolve: (result) => {
|
|
7718
8066
|
cleanup();
|
|
7719
|
-
|
|
8067
|
+
resolve6(result);
|
|
7720
8068
|
},
|
|
7721
8069
|
reject: (error) => {
|
|
7722
8070
|
cleanup();
|
|
@@ -7787,13 +8135,13 @@ var McpClient = class {
|
|
|
7787
8135
|
}
|
|
7788
8136
|
/** Promise 超时包装 */
|
|
7789
8137
|
withTimeout(promise, ms, label) {
|
|
7790
|
-
return new Promise((
|
|
8138
|
+
return new Promise((resolve6, reject) => {
|
|
7791
8139
|
const timer = setTimeout(() => {
|
|
7792
8140
|
reject(new Error(`MCP [${this.serverId}] ${label} timed out after ${ms}ms`));
|
|
7793
8141
|
}, ms);
|
|
7794
8142
|
promise.then((val) => {
|
|
7795
8143
|
clearTimeout(timer);
|
|
7796
|
-
|
|
8144
|
+
resolve6(val);
|
|
7797
8145
|
}).catch((err) => {
|
|
7798
8146
|
clearTimeout(timer);
|
|
7799
8147
|
reject(err);
|
|
@@ -8058,12 +8406,12 @@ var McpManager = class {
|
|
|
8058
8406
|
};
|
|
8059
8407
|
|
|
8060
8408
|
// src/skills/manager.ts
|
|
8061
|
-
import { existsSync as existsSync18, readdirSync as
|
|
8409
|
+
import { existsSync as existsSync18, readdirSync as readdirSync10, mkdirSync as mkdirSync11 } from "fs";
|
|
8062
8410
|
import { join as join13 } from "path";
|
|
8063
8411
|
|
|
8064
8412
|
// src/skills/types.ts
|
|
8065
8413
|
import { readFileSync as readFileSync12 } from "fs";
|
|
8066
|
-
import { basename as
|
|
8414
|
+
import { basename as basename5 } from "path";
|
|
8067
8415
|
function parseSimpleYaml2(yaml) {
|
|
8068
8416
|
const result = {};
|
|
8069
8417
|
for (const line of yaml.split("\n")) {
|
|
@@ -8092,7 +8440,7 @@ function parseSkillFile(filePath) {
|
|
|
8092
8440
|
if (!frontmatterMatch) {
|
|
8093
8441
|
return {
|
|
8094
8442
|
meta: {
|
|
8095
|
-
name:
|
|
8443
|
+
name: basename5(filePath, ".md"),
|
|
8096
8444
|
description: ""
|
|
8097
8445
|
},
|
|
8098
8446
|
content: raw.trim(),
|
|
@@ -8103,7 +8451,7 @@ function parseSkillFile(filePath) {
|
|
|
8103
8451
|
const parsed = parseSimpleYaml2(yaml);
|
|
8104
8452
|
return {
|
|
8105
8453
|
meta: {
|
|
8106
|
-
name: parsed["name"] ??
|
|
8454
|
+
name: parsed["name"] ?? basename5(filePath, ".md"),
|
|
8107
8455
|
description: parsed["description"] ?? "",
|
|
8108
8456
|
tools: parsed["tools"] ? parseYamlArray(parsed["tools"]) : void 0
|
|
8109
8457
|
},
|
|
@@ -8133,7 +8481,7 @@ var SkillManager = class {
|
|
|
8133
8481
|
}
|
|
8134
8482
|
let entries;
|
|
8135
8483
|
try {
|
|
8136
|
-
entries =
|
|
8484
|
+
entries = readdirSync10(this.skillsDir);
|
|
8137
8485
|
} catch {
|
|
8138
8486
|
return 0;
|
|
8139
8487
|
}
|
|
@@ -8244,7 +8592,7 @@ function parseAtReferences(input2, cwd) {
|
|
|
8244
8592
|
let match;
|
|
8245
8593
|
while ((match = atPattern.exec(input2)) !== null) {
|
|
8246
8594
|
const rawPath = match[1] ?? match[2] ?? match[3] ?? "";
|
|
8247
|
-
const absPath =
|
|
8595
|
+
const absPath = resolve5(cwd, rawPath);
|
|
8248
8596
|
const ext = extname4(rawPath).toLowerCase();
|
|
8249
8597
|
const mime = IMAGE_MIME[ext];
|
|
8250
8598
|
if (!existsSync19(absPath)) {
|
|
@@ -8252,7 +8600,7 @@ function parseAtReferences(input2, cwd) {
|
|
|
8252
8600
|
continue;
|
|
8253
8601
|
}
|
|
8254
8602
|
if (mime) {
|
|
8255
|
-
const fileSize =
|
|
8603
|
+
const fileSize = statSync8(absPath).size;
|
|
8256
8604
|
if (fileSize > MAX_IMAGE_BYTES) {
|
|
8257
8605
|
refs.push({ path: rawPath, type: "toolarge" });
|
|
8258
8606
|
continue;
|
|
@@ -8450,7 +8798,7 @@ var Repl = class {
|
|
|
8450
8798
|
if (depth > 2 || entryCount >= MAX_TREE_ENTRIES) return;
|
|
8451
8799
|
let entries;
|
|
8452
8800
|
try {
|
|
8453
|
-
entries =
|
|
8801
|
+
entries = readdirSync11(dir);
|
|
8454
8802
|
} catch {
|
|
8455
8803
|
return;
|
|
8456
8804
|
}
|
|
@@ -8462,7 +8810,7 @@ var Repl = class {
|
|
|
8462
8810
|
const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
|
|
8463
8811
|
let isDir;
|
|
8464
8812
|
try {
|
|
8465
|
-
isDir =
|
|
8813
|
+
isDir = statSync8(fullPath).isDirectory();
|
|
8466
8814
|
} catch {
|
|
8467
8815
|
continue;
|
|
8468
8816
|
}
|
|
@@ -8484,7 +8832,7 @@ ${treeLines.join("\n")}`
|
|
|
8484
8832
|
if (totalChars >= MAX_TOTAL_CHARS) return;
|
|
8485
8833
|
let entries;
|
|
8486
8834
|
try {
|
|
8487
|
-
entries =
|
|
8835
|
+
entries = readdirSync11(dir);
|
|
8488
8836
|
} catch {
|
|
8489
8837
|
return;
|
|
8490
8838
|
}
|
|
@@ -8494,7 +8842,7 @@ ${treeLines.join("\n")}`
|
|
|
8494
8842
|
const fullPath = join14(dir, name);
|
|
8495
8843
|
let st;
|
|
8496
8844
|
try {
|
|
8497
|
-
st =
|
|
8845
|
+
st = statSync8(fullPath);
|
|
8498
8846
|
} catch {
|
|
8499
8847
|
continue;
|
|
8500
8848
|
}
|
|
@@ -8535,13 +8883,13 @@ ${content}
|
|
|
8535
8883
|
* 已添加时返回 added=false(无重复添加)。
|
|
8536
8884
|
*/
|
|
8537
8885
|
addExtraContextDir(dirPath) {
|
|
8538
|
-
const absPath =
|
|
8886
|
+
const absPath = resolve5(dirPath);
|
|
8539
8887
|
if (!existsSync19(absPath)) {
|
|
8540
8888
|
return { success: false, charCount: 0, added: false, error: `Directory not found: ${dirPath}` };
|
|
8541
8889
|
}
|
|
8542
8890
|
let isDir;
|
|
8543
8891
|
try {
|
|
8544
|
-
isDir =
|
|
8892
|
+
isDir = statSync8(absPath).isDirectory();
|
|
8545
8893
|
} catch {
|
|
8546
8894
|
return { success: false, charCount: 0, added: false, error: `Cannot access: ${dirPath}` };
|
|
8547
8895
|
}
|
|
@@ -8558,7 +8906,7 @@ ${content}
|
|
|
8558
8906
|
}
|
|
8559
8907
|
/** 从额外上下文中移除目录。返回 true 表示成功移除,false 表示未找到。 */
|
|
8560
8908
|
removeExtraContextDir(dirPath) {
|
|
8561
|
-
const absPath =
|
|
8909
|
+
const absPath = resolve5(dirPath);
|
|
8562
8910
|
const idx = this.extraContextDirs.findIndex((d) => d.dir === absPath);
|
|
8563
8911
|
if (idx === -1) return false;
|
|
8564
8912
|
this.extraContextDirs.splice(idx, 1);
|
|
@@ -8641,8 +8989,8 @@ ${content}
|
|
|
8641
8989
|
if (setting === false) return { layers: [], mergedContent: "" };
|
|
8642
8990
|
const cwd = process.cwd();
|
|
8643
8991
|
if (setting !== "auto") {
|
|
8644
|
-
const fullPath =
|
|
8645
|
-
if (!fullPath.startsWith(
|
|
8992
|
+
const fullPath = resolve5(cwd, String(setting));
|
|
8993
|
+
if (!fullPath.startsWith(resolve5(cwd))) {
|
|
8646
8994
|
process.stderr.write(
|
|
8647
8995
|
`[Warning] contextFile path "${setting}" is outside current directory, ignoring.
|
|
8648
8996
|
`
|
|
@@ -8685,8 +9033,8 @@ ${content}
|
|
|
8685
9033
|
charCount: projectCtx.content.length
|
|
8686
9034
|
});
|
|
8687
9035
|
}
|
|
8688
|
-
const normalizedCwd =
|
|
8689
|
-
const normalizedRoot =
|
|
9036
|
+
const normalizedCwd = resolve5(cwd);
|
|
9037
|
+
const normalizedRoot = resolve5(projectRoot);
|
|
8690
9038
|
if (normalizedCwd !== normalizedRoot) {
|
|
8691
9039
|
const localCtx = this.findContextFile(cwd);
|
|
8692
9040
|
if (localCtx) {
|
|
@@ -9092,7 +9440,7 @@ Session '${this.resumeSessionId}' not found.
|
|
|
9092
9440
|
this.handleExit();
|
|
9093
9441
|
});
|
|
9094
9442
|
this.showPrompt();
|
|
9095
|
-
await new Promise((
|
|
9443
|
+
await new Promise((resolve6) => {
|
|
9096
9444
|
let processing = false;
|
|
9097
9445
|
this.rl.on("line", async (line) => {
|
|
9098
9446
|
if (this.toolExecutor.confirming) return;
|
|
@@ -9127,13 +9475,13 @@ Session '${this.resumeSessionId}' not found.
|
|
|
9127
9475
|
process.stdin.resume();
|
|
9128
9476
|
this.showPrompt();
|
|
9129
9477
|
} else {
|
|
9130
|
-
|
|
9478
|
+
resolve6();
|
|
9131
9479
|
}
|
|
9132
9480
|
}
|
|
9133
9481
|
});
|
|
9134
9482
|
this.rl.on("close", () => {
|
|
9135
9483
|
if (!processing) {
|
|
9136
|
-
|
|
9484
|
+
resolve6();
|
|
9137
9485
|
}
|
|
9138
9486
|
});
|
|
9139
9487
|
});
|
|
@@ -9386,6 +9734,12 @@ Session '${this.resumeSessionId}' not found.
|
|
|
9386
9734
|
return ["reload"];
|
|
9387
9735
|
case "mcp":
|
|
9388
9736
|
return ["reconnect"];
|
|
9737
|
+
case "undo":
|
|
9738
|
+
return ["list"];
|
|
9739
|
+
case "fork": {
|
|
9740
|
+
const cps = this.sessions.current?.checkpoints.map((c) => c.name) ?? [];
|
|
9741
|
+
return cps;
|
|
9742
|
+
}
|
|
9389
9743
|
case "compact":
|
|
9390
9744
|
case "review":
|
|
9391
9745
|
case "init":
|
|
@@ -9402,18 +9756,18 @@ Session '${this.resumeSessionId}' not found.
|
|
|
9402
9756
|
completeFilePath(partial) {
|
|
9403
9757
|
try {
|
|
9404
9758
|
const normalized = partial.replace(/\\/g, "/");
|
|
9405
|
-
const dir = normalized.includes("/") ?
|
|
9406
|
-
const prefix = normalized.includes("/") ?
|
|
9407
|
-
const absDir =
|
|
9759
|
+
const dir = normalized.includes("/") ? dirname6(normalized) : ".";
|
|
9760
|
+
const prefix = normalized.includes("/") ? basename6(normalized) : normalized;
|
|
9761
|
+
const absDir = resolve5(process.cwd(), dir);
|
|
9408
9762
|
if (!existsSync19(absDir)) return [];
|
|
9409
|
-
const entries =
|
|
9763
|
+
const entries = readdirSync11(absDir);
|
|
9410
9764
|
const results = [];
|
|
9411
9765
|
for (const entry of entries) {
|
|
9412
9766
|
if (entry.startsWith(".")) continue;
|
|
9413
9767
|
if (!entry.toLowerCase().startsWith(prefix.toLowerCase())) continue;
|
|
9414
9768
|
try {
|
|
9415
9769
|
const fullPath = join14(absDir, entry);
|
|
9416
|
-
const stat =
|
|
9770
|
+
const stat = statSync8(fullPath);
|
|
9417
9771
|
const rel = dir === "." ? entry : `${dir}/${entry}`;
|
|
9418
9772
|
results.push(stat.isDirectory() ? `${rel}/` : rel);
|
|
9419
9773
|
} catch {
|
|
@@ -9943,6 +10297,7 @@ Tip: You can continue the conversation by asking the AI to proceed.`
|
|
|
9943
10297
|
addContextDir: (dirPath) => this.addExtraContextDir(dirPath),
|
|
9944
10298
|
removeContextDir: (dirPath) => this.removeExtraContextDir(dirPath),
|
|
9945
10299
|
listContextDirs: () => [...this.extraContextDirs],
|
|
10300
|
+
forkSession: (messageCount, title) => this.sessions.forkSession(messageCount, title),
|
|
9946
10301
|
exit: () => this.handleExit()
|
|
9947
10302
|
};
|
|
9948
10303
|
await cmd.execute(args, ctx);
|
|
@@ -10071,10 +10426,10 @@ async function setupProxy(configProxy) {
|
|
|
10071
10426
|
}
|
|
10072
10427
|
async function readStdin() {
|
|
10073
10428
|
if (process.stdin.isTTY) return "";
|
|
10074
|
-
return new Promise((
|
|
10429
|
+
return new Promise((resolve6, reject) => {
|
|
10075
10430
|
const chunks = [];
|
|
10076
10431
|
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
10077
|
-
process.stdin.on("end", () =>
|
|
10432
|
+
process.stdin.on("end", () => resolve6(Buffer.concat(chunks).toString("utf-8").trimEnd()));
|
|
10078
10433
|
process.stdin.on("error", reject);
|
|
10079
10434
|
});
|
|
10080
10435
|
}
|