mocode-ai 1.2.7 → 1.2.9

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 (129) hide show
  1. package/dist/agent/core.js +126 -43
  2. package/dist/config/index.js +31 -6
  3. package/dist/config/presets.js +36 -1
  4. package/dist/context/budget.js +5 -16
  5. package/dist/llm/index.js +42 -4
  6. package/dist/repl/index.js +57 -13
  7. package/dist/session/compact.js +329 -40
  8. package/dist/session/notes.js +63 -0
  9. package/dist/session/scheduler.js +5 -9
  10. package/dist/tools/constants.js +21 -0
  11. package/package.json +1 -1
  12. package/dist/__trace_manual_test__.js +0 -1
  13. package/dist/agent/core.d.ts +0 -112
  14. package/dist/agent/index.d.ts +0 -19
  15. package/dist/agent/mode.d.ts +0 -29
  16. package/dist/agent/retry-classifier.js +0 -64
  17. package/dist/agent/spawn.d.ts +0 -36
  18. package/dist/attachments/image.d.ts +0 -22
  19. package/dist/commands/config.d.ts +0 -8
  20. package/dist/commands/skill.js +0 -230
  21. package/dist/config/file.d.ts +0 -23
  22. package/dist/config/index.d.ts +0 -114
  23. package/dist/config/presets.d.ts +0 -57
  24. package/dist/context/budget.d.ts +0 -79
  25. package/dist/context/classifier.d.ts +0 -14
  26. package/dist/context/encoders/_util.d.ts +0 -11
  27. package/dist/context/encoders/code.d.ts +0 -2
  28. package/dist/context/encoders/doc.d.ts +0 -14
  29. package/dist/context/encoders/graph.d.ts +0 -11
  30. package/dist/context/encoders/graph.js +0 -81
  31. package/dist/context/encoders/index.d.ts +0 -2
  32. package/dist/context/encoders/log.d.ts +0 -2
  33. package/dist/context/encoders/log.js +0 -2
  34. package/dist/context/encoders/memory.d.ts +0 -2
  35. package/dist/context/encoders/passthrough.d.ts +0 -11
  36. package/dist/context/encoders/search.d.ts +0 -2
  37. package/dist/context/encoders/summary.d.ts +0 -12
  38. package/dist/context/encoders/table.d.ts +0 -2
  39. package/dist/context/encoders/tree.d.ts +0 -2
  40. package/dist/context/index.d.ts +0 -6
  41. package/dist/context/lifecycle.d.ts +0 -51
  42. package/dist/context/pipeline.d.ts +0 -9
  43. package/dist/context/registry.d.ts +0 -9
  44. package/dist/context/relevance.d.ts +0 -65
  45. package/dist/context/types.d.ts +0 -36
  46. package/dist/foo.js +0 -28
  47. package/dist/foo.test.js +0 -12
  48. package/dist/index.d.ts +0 -1
  49. package/dist/llm/capabilities.d.ts +0 -19
  50. package/dist/llm/index.d.ts +0 -111
  51. package/dist/memory/discover.d.ts +0 -15
  52. package/dist/memory/index.d.ts +0 -13
  53. package/dist/memory/reflect.d.ts +0 -34
  54. package/dist/memory/store.d.ts +0 -124
  55. package/dist/pet/bridge.d.ts +0 -73
  56. package/dist/pet/protocol.d.ts +0 -83
  57. package/dist/pet/state.d.ts +0 -28
  58. package/dist/plan/active.d.ts +0 -23
  59. package/dist/plan/active.js +0 -45
  60. package/dist/plan/index.d.ts +0 -13
  61. package/dist/plan/index.js +0 -33
  62. package/dist/plan/state.d.ts +0 -2
  63. package/dist/plan/state.js +0 -11
  64. package/dist/plan/store.d.ts +0 -78
  65. package/dist/plan/store.js +0 -429
  66. package/dist/project-skill/index.js +0 -95
  67. package/dist/project-skill/initializer.js +0 -178
  68. package/dist/project-snapshot/index.js +0 -85
  69. package/dist/project-snapshot/llm-snapshot.js +0 -150
  70. package/dist/project-snapshot/static-files.js +0 -87
  71. package/dist/repl/index.d.ts +0 -22
  72. package/dist/rollback/index.d.ts +0 -79
  73. package/dist/sandbox/command.d.ts +0 -3
  74. package/dist/sandbox/index.d.ts +0 -4
  75. package/dist/sandbox/jail.d.ts +0 -17
  76. package/dist/sandbox/policy.d.ts +0 -21
  77. package/dist/sandbox/root.d.ts +0 -5
  78. package/dist/session/compact.d.ts +0 -92
  79. package/dist/session/index.d.ts +0 -13
  80. package/dist/session/notes-plan.js +0 -41
  81. package/dist/session/persist.d.ts +0 -30
  82. package/dist/session/scheduler.d.ts +0 -53
  83. package/dist/skills/discover.d.ts +0 -33
  84. package/dist/skills/index.d.ts +0 -13
  85. package/dist/skills/skill-eval.js +0 -345
  86. package/dist/skills/skill-improve.js +0 -221
  87. package/dist/skills/stats.js +0 -102
  88. package/dist/tools/builtins/apply-patch.js +0 -174
  89. package/dist/tools/builtins/ask-human.d.ts +0 -9
  90. package/dist/tools/builtins/codegraph.d.ts +0 -2
  91. package/dist/tools/builtins/codegraph.js +0 -128
  92. package/dist/tools/builtins/edit-file.d.ts +0 -2
  93. package/dist/tools/builtins/glob.d.ts +0 -2
  94. package/dist/tools/builtins/grep.d.ts +0 -2
  95. package/dist/tools/builtins/index.d.ts +0 -2
  96. package/dist/tools/builtins/memory-forget.d.ts +0 -2
  97. package/dist/tools/builtins/memory-list.d.ts +0 -2
  98. package/dist/tools/builtins/memory-save.d.ts +0 -2
  99. package/dist/tools/builtins/memory-search.d.ts +0 -2
  100. package/dist/tools/builtins/memory-update.d.ts +0 -2
  101. package/dist/tools/builtins/project-skill-update.js +0 -102
  102. package/dist/tools/builtins/read-file.d.ts +0 -2
  103. package/dist/tools/builtins/run-command.d.ts +0 -2
  104. package/dist/tools/builtins/switch-mode.d.ts +0 -2
  105. package/dist/tools/builtins/switch-mode.js +0 -44
  106. package/dist/tools/builtins/task.d.ts +0 -2
  107. package/dist/tools/builtins/todolist.d.ts +0 -2
  108. package/dist/tools/builtins/todolist.js +0 -411
  109. package/dist/tools/builtins/use-skill.d.ts +0 -2
  110. package/dist/tools/builtins/web-fetch.d.ts +0 -2
  111. package/dist/tools/builtins/web-search.d.ts +0 -2
  112. package/dist/tools/builtins/write-file.d.ts +0 -2
  113. package/dist/tools/constants.d.ts +0 -52
  114. package/dist/tools/registry.d.ts +0 -18
  115. package/dist/tools/retry.js +0 -137
  116. package/dist/tools/types.d.ts +0 -42
  117. package/dist/ui/batch.d.ts +0 -67
  118. package/dist/ui/clipboard.d.ts +0 -9
  119. package/dist/ui/content.d.ts +0 -55
  120. package/dist/ui/diff.d.ts +0 -34
  121. package/dist/ui/intervention.d.ts +0 -46
  122. package/dist/ui/layout.d.ts +0 -225
  123. package/dist/ui/markdown.d.ts +0 -46
  124. package/dist/ui/mouse.d.ts +0 -29
  125. package/dist/ui/prompt.d.ts +0 -61
  126. package/dist/ui/render.d.ts +0 -60
  127. package/dist/ui/spinner.d.ts +0 -26
  128. package/dist/ui/theme.d.ts +0 -47
  129. package/dist/ui/thinking.js +0 -80
@@ -1,221 +0,0 @@
1
- // skill description 进化循环(自进化 Phase 1b)。
2
- //
3
- // 协议移植自 skill-creator 的 run_loop.py(调研见 docs/skill-self-evolution-research.md Part 1.2):
4
- // - 分层切 train/holdout(默认 0.4)防过拟合
5
- // - 每轮:LLM 读失败案例 → 提案新 description → 评测 → 接受门
6
- // - 接受门(保守双条件):train 严格提升 且 holdout 不降;holdout 为空时退化为全集严格提升
7
- // - 拒绝缓冲:被拒的候选记下来,后续提案明确告知「这些已试过且更差」,防重复打转
8
- // - 迭代上限(默认 5);全程记录 history,返回最佳 description
9
- //
10
- // 安全边界(调研 Part 3):
11
- // - 默认 dry-run(只打印不落盘);--apply 才写回 SKILL.md,且写前再显式确认一次
12
- // - builtin skill 无磁盘载体,拒绝(入口在 skill-eval.loadSkillForEval)
13
- // - 进化对象仅 description 一行;正文/脚本/执行面字段不在自动编辑范围
14
- //
15
- // 依赖:skill-eval.ts(评测引擎 + 纯函数)+ llm.chat(提案,单轮无工具)。
16
- import { readFileSync, writeFileSync } from 'node:fs';
17
- import { chat } from '../llm/index.js';
18
- import { runTriggerEval, splitTriggerSet, applyDescription, MAX_DESCRIPTION_CHARS, } from './skill-eval.js';
19
- /** 提案 LLM 的系统指令:确定性模板(不用 t(),保证跨语言可比、可单测断言)。 */
20
- export function buildImprovePrompt(skill, currentDescription, failingCases, rejected) {
21
- const body = (skill.body ?? '').slice(0, 1500);
22
- const failLines = failingCases
23
- .map((c) => {
24
- const expect = c.should_trigger ? 'SHOULD trigger' : 'should NOT trigger';
25
- return `- "${c.query}" → ${expect}, actual trigger rate ${c.trigger_rate.toFixed(2)}`;
26
- })
27
- .join('\n');
28
- const rejectedBlock = rejected.length
29
- ? `\nThese descriptions were already tried and performed WORSE. Do not propose any of them or near-duplicates:\n${rejected
30
- .map((d) => `- ${d}`)
31
- .join('\n')}\n`
32
- : '';
33
- return [
34
- `You are optimizing the frontmatter "description" field of an agent skill named "${skill.name}".`,
35
- 'The description is the ONLY text the model sees when deciding whether to trigger the skill.',
36
- `The skill body (what it actually does) begins with:\n---\n${body}\n---`,
37
- `Current description:\n${currentDescription}`,
38
- 'Test results with the current description (failures to fix):',
39
- failLines,
40
- rejectedBlock,
41
- 'Propose ONE improved description that makes the skill trigger for the SHOULD-trigger cases and stay silent for the should-NOT-trigger cases.',
42
- 'Rules:',
43
- '- Output ONLY the description text, nothing else (no quotes, no "description:", no markdown).',
44
- '- One line only, 100-400 characters.',
45
- '- Write it as a routing rule: state WHAT it does and WHEN to use it (concrete triggers/phrases), not as documentation.',
46
- '- Preserve the language of the current description (English stays English, Chinese stays Chinese).',
47
- ].join('\n');
48
- }
49
- /**
50
- * 从 LLM 输出里提取候选 description(纯函数):
51
- * 去首尾空白 → 若以 "description:" 开头则剥掉 → 只取第一行 → 去配对引号 → 校验长度。
52
- * 非法(空 / 超长)返 null。
53
- */
54
- export function extractDescriptionProposal(text, maxChars = MAX_DESCRIPTION_CHARS) {
55
- let t = (text ?? '').trim();
56
- if (!t)
57
- return null;
58
- t = t.replace(/^description\s*[::]\s*/i, '');
59
- let first = t.split(/\r?\n/)[0].trim();
60
- if (!first)
61
- return null;
62
- if (first.length >= 2 && (first[0] === '"' || first[0] === "'") && first[first.length - 1] === first[0]) {
63
- first = first.slice(1, -1).trim();
64
- }
65
- if (!first || first.length > maxChars)
66
- return null;
67
- return first;
68
- }
69
- /** 接受门(纯函数):train 严格提升 且 holdout 不降;holdout 为空时全集严格提升。 */
70
- export function shouldAcceptCandidate(base, cand) {
71
- const trainUp = cand.trainPass > base.trainPass;
72
- if (!trainUp) {
73
- return { accept: false, reason: `train 未提升 (${cand.trainPass}/${cand.trainTotal} ≤ ${base.trainPass}/${base.trainTotal})` };
74
- }
75
- if (base.holdoutPass === null || cand.holdoutPass === null) {
76
- return { accept: true, reason: `train 提升且无 holdout(${base.trainPass}→${cand.trainPass})` };
77
- }
78
- if (cand.holdoutPass < base.holdoutPass) {
79
- return { accept: false, reason: `holdout 回退 (${cand.holdoutPass} < ${base.holdoutPass}),过拟合 train` };
80
- }
81
- return { accept: true, reason: `train ${base.trainPass}→${cand.trainPass},holdout ${base.holdoutPass}→${cand.holdoutPass}(不降)` };
82
- }
83
- /** 把报告按 query 集合拆成 train / holdout 的分项(纯函数)。 */
84
- function partitionReport(report, trainQueries) {
85
- const train = report.results.filter((r) => trainQueries.has(r.query));
86
- const holdout = report.results.filter((r) => !trainQueries.has(r.query));
87
- return {
88
- trainPass: train.filter((r) => r.pass).length,
89
- trainTotal: train.length,
90
- holdoutPass: holdout.length ? holdout.filter((r) => r.pass).length : null,
91
- holdoutTotal: holdout.length,
92
- };
93
- }
94
- function failingCases(report) {
95
- return report.results
96
- .filter((r) => !r.pass)
97
- .map((r) => ({ query: r.query, should_trigger: r.should_trigger, trigger_rate: r.trigger_rate }));
98
- }
99
- /** 调 LLM 提案一次(单轮、无工具、超时 abort)。失败抛错由调用方捕获。 */
100
- async function proposeDescription(prompt, timeoutMs, signal) {
101
- const controller = new AbortController();
102
- const timer = setTimeout(() => controller.abort(), timeoutMs);
103
- // AbortSignal.any 需 Node ≥20;engines>=18,手动桥接外部 signal。
104
- const externalAbort = () => controller.abort();
105
- if (signal) {
106
- if (signal.aborted)
107
- controller.abort();
108
- else
109
- signal.addEventListener('abort', externalAbort, { once: true });
110
- }
111
- try {
112
- const messages = [{ role: 'user', content: prompt }];
113
- const res = await chat(messages, {}, controller.signal);
114
- return extractDescriptionProposal(res.content ?? '');
115
- }
116
- finally {
117
- clearTimeout(timer);
118
- signal?.removeEventListener('abort', externalAbort);
119
- }
120
- }
121
- /**
122
- * 进化主循环。evalSet 必须非空(调用方保证;无 evals 的 skill 在 CLI 层拒绝——
123
- * 「没验证门的 skill 禁止自动落盘」,调研共识 1)。
124
- */
125
- export async function runImproveLoop(skill, evalSet, opts = {}) {
126
- const maxIterations = opts.maxIterations ?? 5;
127
- const runsPerQuery = opts.runsPerQuery ?? 3;
128
- const threshold = opts.threshold ?? 0.5;
129
- const { train, holdout } = splitTriggerSet(evalSet, opts.holdout ?? 0.4, opts.seed ?? 42);
130
- if (train.length === 0)
131
- throw new Error('train 集为空:eval 集太小,无法进化(至少需要 2 条且覆盖触发/不触发)');
132
- const trainQueries = new Set(train.map((t) => t.query));
133
- const evalOpts = { runsPerQuery, threshold, timeoutMs: opts.timeoutMs, signal: opts.signal };
134
- let baseDescription = skill.description;
135
- let baseReport = await runTriggerEval(skill, baseDescription, evalSet, runsPerQuery, threshold, evalOpts);
136
- let baseParts = partitionReport(baseReport, trainQueries);
137
- const iterations = [];
138
- const rejected = [];
139
- let bestDescription = baseDescription;
140
- let bestKey = [baseParts.trainPass, baseParts.holdoutPass ?? 0];
141
- const earlyExit = () => baseParts.trainTotal > 0 &&
142
- baseParts.trainPass === baseParts.trainTotal &&
143
- (baseParts.holdoutPass === null || (baseParts.holdoutTotal > 0 && baseParts.holdoutPass === baseParts.holdoutTotal));
144
- if (earlyExit()) {
145
- opts.onIteration?.(0, maxIterations, '当前 description 在 train + holdout 全通过,无需进化。');
146
- }
147
- for (let iter = 1; iter <= maxIterations && !earlyExit(); iter++) {
148
- opts.onIteration?.(iter, maxIterations, `第 ${iter}/${maxIterations} 轮:提案新 description…`);
149
- const prompt = buildImprovePrompt(skill, baseDescription, failingCases(baseReport), rejected);
150
- let candidate;
151
- try {
152
- candidate = await proposeDescription(prompt, opts.proposeTimeoutMs ?? 120_000, opts.signal);
153
- }
154
- catch (e) {
155
- const msg = e instanceof Error ? e.message : String(e);
156
- opts.onIteration?.(iter, maxIterations, `提案失败(${msg.slice(0, 120)}),终止。`);
157
- break;
158
- }
159
- if (!candidate || candidate === baseDescription) {
160
- opts.onIteration?.(iter, maxIterations, '提案无效(空/超长/与当前相同),终止。');
161
- break;
162
- }
163
- const candReport = await runTriggerEval(skill, candidate, evalSet, runsPerQuery, threshold, evalOpts);
164
- const candParts = partitionReport(candReport, trainQueries);
165
- const gate = shouldAcceptCandidate(baseParts, candParts);
166
- const iteration = {
167
- iteration: iter,
168
- description: candidate,
169
- trainPass: candParts.trainPass,
170
- trainTotal: candParts.trainTotal,
171
- holdoutPass: candParts.holdoutPass,
172
- holdoutTotal: candParts.holdoutTotal,
173
- accepted: gate.accept,
174
- reason: gate.reason,
175
- };
176
- iterations.push(iteration);
177
- opts.onIteration?.(iter, maxIterations, `${gate.accept ? '✓ 接受' : '· 拒绝'}: train ${candParts.trainPass}/${candParts.trainTotal}, holdout ${candParts.holdoutPass ?? '-'}/${candParts.holdoutTotal} — ${gate.reason}`);
178
- if (gate.accept) {
179
- baseDescription = candidate;
180
- baseReport = candReport;
181
- baseParts = candParts;
182
- const key = [candParts.trainPass, candParts.holdoutPass ?? 0];
183
- if (key[0] > bestKey[0] || (key[0] === bestKey[0] && key[1] > bestKey[1])) {
184
- bestKey = key;
185
- bestDescription = candidate;
186
- }
187
- }
188
- else {
189
- rejected.push(candidate);
190
- if (rejected.length > 5)
191
- rejected.shift();
192
- }
193
- }
194
- return {
195
- skill: skill.name,
196
- originalDescription: skill.description,
197
- bestDescription,
198
- improved: bestDescription !== skill.description,
199
- iterations,
200
- };
201
- }
202
- /**
203
- * 把进化结果写回 SKILL.md 的 description(唯一的落盘点;CLI --apply 时才调)。
204
- * 返回写后的文件内容;frontmatter 无法安全替换时抛错(不落盘)。
205
- */
206
- export function applyImprovedDescription(skill, description) {
207
- if (skill.dir === 'builtin')
208
- throw new Error('内置 skill 不可修改');
209
- if (description.length > MAX_DESCRIPTION_CHARS)
210
- throw new Error(`description 超长(>${MAX_DESCRIPTION_CHARS})`);
211
- const original = readFileSync(skill.skillMdPath, 'utf8');
212
- const next = applyDescription(original, description);
213
- if (next === null) {
214
- throw new Error('无法定位 SKILL.md frontmatter 的 description 字段,拒绝写回(请手工编辑)');
215
- }
216
- if (next === original) {
217
- throw new Error('写回无变化(description 与当前一致)');
218
- }
219
- writeFileSync(skill.skillMdPath, next, 'utf8');
220
- return next;
221
- }
@@ -1,102 +0,0 @@
1
- // skill 使用台账(自进化 Phase 0)。
2
- // 为什么不在 trace.jsonl 上做:工具事件出于隐私只存参数指纹(sha256/keys,
3
- // trace-sanitize.ts 刻意不存值),拿不到 skill name。台账因此在工具层直接记录——
4
- // use_skill / run_skill 是唯一知道真实 skill name 的落点。
5
- //
6
- // 落盘:<cwd>/.mocode/skill-stats.jsonl(append-only JSONL,每行一次使用)。
7
- // 纯观测:任何写失败静默吞掉,绝不阻断 agent 主流程(风格对齐 session/trace.ts)。
8
- // 聚合是纯函数(aggregateSkillStats),吃记录数组吐按 skill 的计数,便于单测。
9
- import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
10
- import path from 'node:path';
11
- /**
12
- * 台账路径(项目级,与 sessions 同根;使用是项目上下文相关的,不写全局)。
13
- * baseDir 可选:测试指向临时目录用;缺省 process.cwd()。
14
- */
15
- export function skillStatsPath(baseDir = process.cwd()) {
16
- return path.join(baseDir, '.mocode', 'skill-stats.jsonl');
17
- }
18
- /**
19
- * 追加一条台账记录;任何失败静默(观测不得阻断主流程)。
20
- * MOCODE_SKILL_EVAL=1(触发评测进程内设置)时跳过:评测里的人工构造调用
21
- * 是测量手段不是真实使用,记入会污染自进化的输入信号。
22
- */
23
- export function recordSkillUsage(rec, baseDir = process.cwd()) {
24
- if (process.env.MOCODE_SKILL_EVAL === '1')
25
- return;
26
- try {
27
- const p = skillStatsPath(baseDir);
28
- const dir = path.dirname(p);
29
- if (!existsSync(dir))
30
- mkdirSync(dir, { recursive: true });
31
- appendFileSync(p, JSON.stringify(rec) + '\n', 'utf8');
32
- }
33
- catch {
34
- // 观测失败静默
35
- }
36
- }
37
- /** 读取台账原始记录;文件不存在 / 单行损坏 → 跳过(不抛)。 */
38
- export function loadSkillUsage(baseDir = process.cwd()) {
39
- try {
40
- const content = readFileSync(skillStatsPath(baseDir), 'utf8');
41
- const out = [];
42
- for (const line of content.split('\n')) {
43
- const s = line.trim();
44
- if (!s)
45
- continue;
46
- try {
47
- const v = JSON.parse(s);
48
- if (v && typeof v.skill === 'string' && (v.kind === 'use' || v.kind === 'run')) {
49
- out.push(v);
50
- }
51
- }
52
- catch {
53
- // 损坏行跳过
54
- }
55
- }
56
- return out;
57
- }
58
- catch {
59
- return [];
60
- }
61
- }
62
- /**
63
- * 纯聚合:记录 → 按 skill 的计数视图。按 skill 名分组(大小写敏感),
64
- * lastUsedAt 取 ts 字符串字典序最大(ISO 时间戳字典序 == 时间序)。
65
- * 输入乱序也安全。空输入返回 []。
66
- */
67
- export function aggregateSkillStats(records) {
68
- const bySkill = new Map();
69
- for (const r of records) {
70
- const list = bySkill.get(r.skill);
71
- if (list)
72
- list.push(r);
73
- else
74
- bySkill.set(r.skill, [r]);
75
- }
76
- const out = [];
77
- for (const [skill, list] of bySkill) {
78
- const runs = list.filter((r) => r.kind === 'run');
79
- const runSuccess = runs.filter((r) => r.status === 'success').length;
80
- let lastFailure = null;
81
- let lastUsedAt = '';
82
- for (const r of list) {
83
- if (typeof r.ts === 'string' && r.ts > lastUsedAt)
84
- lastUsedAt = r.ts;
85
- if (r.status !== 'success' && (!lastFailure || r.ts > lastFailure.ts)) {
86
- lastFailure = { ts: r.ts, status: r.status, code: r.code };
87
- }
88
- }
89
- out.push({
90
- skill,
91
- total: list.length,
92
- uses: list.length - runs.length,
93
- runs: runs.length,
94
- runSuccessRate: runs.length ? runSuccess / runs.length : null,
95
- lastFailure,
96
- lastUsedAt,
97
- });
98
- }
99
- // 按最近使用倒序,让 /skills 徽标与人工浏览都「最活跃的在前」。
100
- out.sort((a, b) => (a.lastUsedAt < b.lastUsedAt ? 1 : a.lastUsedAt > b.lastUsedAt ? -1 : 0));
101
- return out;
102
- }
@@ -1,174 +0,0 @@
1
- import { readFile } from 'node:fs/promises';
2
- import { jailResolve } from '../../sandbox/index.js';
3
- import { commitChangeSet, contentHash, createChangeSet, summarizeChangeSet, } from '../../changeset/index.js';
4
- function invalid(message) {
5
- const error = new Error(message);
6
- error.name = 'PatchError';
7
- return error;
8
- }
9
- function parsePatch(input) {
10
- const lines = input.replace(/\r\n/g, '\n').split('\n');
11
- if (lines[0] !== '*** Begin Patch')
12
- throw invalid('patch 必须以 *** Begin Patch 开始。');
13
- const operations = [];
14
- let i = 1;
15
- while (i < lines.length && lines[i] !== '*** End Patch') {
16
- const header = lines[i++];
17
- let match = /^\*\*\* Add File: (.+)$/.exec(header);
18
- if (match) {
19
- const body = [];
20
- while (i < lines.length && !lines[i].startsWith('*** ')) {
21
- const line = lines[i++];
22
- if (!line.startsWith('+'))
23
- throw invalid(`Add File ${match[1]} 的内容行必须以 + 开头。`);
24
- body.push(line.slice(1));
25
- }
26
- operations.push({ kind: 'add', path: match[1], lines: body });
27
- continue;
28
- }
29
- match = /^\*\*\* Delete File: (.+)$/.exec(header);
30
- if (match) {
31
- operations.push({ kind: 'delete', path: match[1] });
32
- continue;
33
- }
34
- match = /^\*\*\* Update File: (.+)$/.exec(header);
35
- if (!match)
36
- throw invalid(`未知 patch 指令: ${header}`);
37
- const hunks = [];
38
- while (i < lines.length && !lines[i].startsWith('*** ')) {
39
- if (!lines[i].startsWith('@@'))
40
- throw invalid(`Update File ${match[1]} 缺少 @@ hunk。`);
41
- i++;
42
- const hunk = [];
43
- while (i < lines.length && !lines[i].startsWith('@@') && !lines[i].startsWith('*** ')) {
44
- hunk.push(lines[i++]);
45
- }
46
- hunks.push(hunk);
47
- }
48
- operations.push({ kind: 'update', path: match[1], hunks });
49
- }
50
- if (lines[i] !== '*** End Patch')
51
- throw invalid('patch 缺少 *** End Patch。');
52
- if (operations.length === 0)
53
- throw invalid('patch 不包含文件操作。');
54
- return operations;
55
- }
56
- function applyHunks(source, hunks, file) {
57
- let current = source.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
58
- for (const hunk of hunks) {
59
- const before = [];
60
- const after = [];
61
- for (const line of hunk) {
62
- if (line.startsWith(' ')) {
63
- before.push(line.slice(1));
64
- after.push(line.slice(1));
65
- }
66
- else if (line.startsWith('-')) {
67
- before.push(line.slice(1));
68
- }
69
- else if (line.startsWith('+')) {
70
- after.push(line.slice(1));
71
- }
72
- else if (line === '\') {
73
- continue;
74
- }
75
- else {
76
- throw invalid(`Update File ${file} 的 hunk 行必须以空格、+ 或 - 开头。`);
77
- }
78
- }
79
- const oldText = before.join('\n');
80
- const newText = after.join('\n');
81
- if (!oldText)
82
- throw invalid(`Update File ${file} 的 hunk 缺少上下文或删除行。`);
83
- const first = current.indexOf(oldText);
84
- if (first < 0)
85
- throw invalid(`Update File ${file} 的 hunk 与当前文件不匹配。`);
86
- if (current.indexOf(oldText, first + 1) >= 0) {
87
- throw invalid(`Update File ${file} 的 hunk 上下文不唯一,请增加上下文。`);
88
- }
89
- current = current.slice(0, first) + newText + current.slice(first + oldText.length);
90
- }
91
- return source.includes('\r\n') ? current.replace(/\n/g, '\r\n') : current;
92
- }
93
- async function buildChanges(operations) {
94
- const changes = [];
95
- for (const operation of operations) {
96
- if (operation.kind === 'add') {
97
- changes.push({
98
- path: operation.path,
99
- operation: 'create',
100
- expectedHash: null,
101
- replacement: operation.lines.join('\n'),
102
- });
103
- continue;
104
- }
105
- const raw = await readFile(jailResolve(operation.path));
106
- const expectedHash = contentHash(raw);
107
- if (operation.kind === 'delete') {
108
- changes.push({ path: operation.path, operation: 'delete', expectedHash });
109
- continue;
110
- }
111
- const source = raw.toString('utf8');
112
- changes.push({
113
- path: operation.path,
114
- operation: 'update',
115
- expectedHash,
116
- replacement: applyHunks(source, operation.hunks, operation.path),
117
- });
118
- }
119
- return changes;
120
- }
121
- function conflictOutcome(result) {
122
- return {
123
- status: 'error',
124
- code: 'CHANGE_CONFLICT',
125
- retryable: false,
126
- changedFiles: [],
127
- staleFiles: result.conflicts.map((item) => item.path),
128
- output: [
129
- '错误:apply_patch 检测到内容冲突,磁盘未发生变化。',
130
- ...result.conflicts.map((item) => `- ${item.path}: expected=${item.expectedHash ?? 'missing'}, actual=${item.actualHash ?? 'missing'} (${item.reason})`),
131
- ].join('\n'),
132
- };
133
- }
134
- export const applyPatchTool = {
135
- name: 'apply_patch',
136
- description: 'Apply a multi-file patch transactionally. Format: *** Begin Patch, then *** Add/Update/Delete File sections, then *** End Patch. All files are dry-run and hash-checked before any file is committed; failure leaves disk unchanged.',
137
- risk: 'confirm',
138
- parameters: {
139
- type: 'object',
140
- properties: {
141
- patch: { type: 'string', description: 'The complete *** Begin Patch ... *** End Patch document.' },
142
- },
143
- required: ['patch'],
144
- },
145
- async execute(args, ctx) {
146
- try {
147
- const changes = await buildChanges(parsePatch(String(args.patch)));
148
- const result = await commitChangeSet(createChangeSet(changes), ctx?.signal);
149
- if (result.status === 'conflict')
150
- return conflictOutcome(result);
151
- if (result.status === 'failed') {
152
- return { status: 'error', code: 'PATCH_INVALID', retryable: false, changedFiles: [], output: `错误:apply_patch 提交失败,已恢复磁盘: ${result.error}` };
153
- }
154
- const summary = summarizeChangeSet(result.changeSet);
155
- return {
156
- status: 'success',
157
- code: 'OK',
158
- retryable: false,
159
- changedFiles: result.changedFiles,
160
- changeSet: summary,
161
- output: `已事务化应用 ChangeSet ${summary.id}:\n${summary.changes.map((item) => `- ${item.operation} ${item.path} (${item.beforeHash ?? 'missing'} -> ${item.afterHash ?? 'missing'})`).join('\n')}`,
162
- };
163
- }
164
- catch (error) {
165
- return {
166
- status: 'error',
167
- code: 'PATCH_INVALID',
168
- retryable: false,
169
- changedFiles: [],
170
- output: `错误:apply_patch 无效,磁盘未发生变化: ${error instanceof Error ? error.message : String(error)}`,
171
- };
172
- }
173
- },
174
- };
@@ -1,9 +0,0 @@
1
- import type { Tool } from '../types.js';
2
- /** 单个选项:label 是选项本身,detail 是选它意味着什么/取舍(可选)。 */
3
- export interface ChoiceOption {
4
- label: string;
5
- detail?: string;
6
- }
7
- /** 公开以便 check-ask-human-options.ts 单元测试。 */
8
- export declare function coerceOptions(raw: unknown): ChoiceOption[];
9
- export declare const askHumanTool: Tool;
@@ -1,2 +0,0 @@
1
- import type { Tool } from '../types.js';
2
- export declare const codegraphTool: Tool;
@@ -1,128 +0,0 @@
1
- import { spawn } from 'node:child_process';
2
- import { existsSync } from 'node:fs';
3
- import path from 'node:path';
4
- import { MAX_OUTPUT } from '../constants.js';
5
- // ---------- codegraph ----------
6
- // 已建代码索引(.codegraph/)的仓库里,理解/定位代码、查调用链、看改动影响面时
7
- // 的首选工具。比逐文件 read_file/grep 拼凑更准更省。
8
- // 检查 .codegraph/ 是否存在:不存在则提示建索引,不盲目调 CLI(避免报错噪音)。
9
- /** 同步判断当前 cwd 是否已建 codegraph 索引。 */
10
- function hasCodegraphIndex() {
11
- return existsSync(path.join(process.cwd(), '.codegraph'));
12
- }
13
- /** 跑 codegraph CLI 子进程,返回合并 stdout+stderr(带退出码),超时 60s。
14
- * signal 透传(用户 Ctrl+C)→ 即时杀子进程,不等 60s 超时。 */
15
- function runCodegraph(args, signal) {
16
- return new Promise((done) => {
17
- const isWin = process.platform === 'win32';
18
- // win32 必须走 cmd.exe /c:Node 自 CVE-2024-27980 修复后,shell:false 直接 spawn
19
- // .cmd/.bat 会抛 EINVAL;cmd.exe 是真二进制,直接 spawn 安全(与 run_command 同约定)。
20
- const child = isWin
21
- ? spawn('cmd.exe', ['/c', 'codegraph', ...args], { cwd: process.cwd() })
22
- : spawn('codegraph', args, { cwd: process.cwd() });
23
- let out = '';
24
- let finished = false;
25
- const finish = (s) => {
26
- if (finished)
27
- return;
28
- finished = true;
29
- clearTimeout(timer);
30
- signal?.removeEventListener('abort', onAbort);
31
- done(s);
32
- };
33
- const onChunk = (chunk) => {
34
- if (out.length < MAX_OUTPUT)
35
- out += chunk.toString('utf8');
36
- };
37
- child.stdout.on('data', onChunk);
38
- child.stderr.on('data', onChunk);
39
- child.on('error', (e) => finish(`codegraph 执行失败(可能未安装或不在 PATH): ${e.message}\n` +
40
- '安装: npm i -g @colbymchenry/codegraph'));
41
- child.on('close', (code) => {
42
- let r = out.trim();
43
- if (out.length >= MAX_OUTPUT)
44
- r += '\n...(输出已截断)';
45
- finish(`[退出码 ${code}]\n${r || '(无输出)'}`);
46
- });
47
- const timer = setTimeout(() => {
48
- child.kill();
49
- finish(`[超时,已终止]\n${out.trim()}`);
50
- }, 60000);
51
- // abort(用户 Ctrl+C,经 executeTool ctx.signal 透传)→ 杀子进程 + 返[已中断]
52
- const onAbort = () => {
53
- child.kill();
54
- finish(`[已中断]\n${out.trim()}`);
55
- };
56
- if (signal) {
57
- if (signal.aborted)
58
- onAbort();
59
- else
60
- signal.addEventListener('abort', onAbort, { once: true });
61
- }
62
- });
63
- }
64
- export const codegraphTool = {
65
- name: 'codegraph',
66
- description: 'FIRST CHOICE for understanding/locating code, tracing call chains, or assessing the impact of changes — use this before read_file/grep when a .codegraph/ index exists.' +
67
- ' Returns symbol source + call paths in one shot — more accurate and economical than piecing together read_file/grep.' +
68
- ' When to use: starting any code exploration; locating a symbol; understanding how a feature works; seeing what calls a function or what a change affects.' +
69
- ' When NOT to use: no .codegraph/ index (build it first with `codegraph init`); reading a file you just edited; editing a single known small file — in those cases go straight to read_file/edit_file; or the symbol/area was already retrieved earlier in this session — reuse that result instead of calling again.' +
70
- ' Fallback: if codegraph misses or the result is incomplete, then use read_file/grep/glob to fill the gaps.',
71
- parameters: {
72
- type: 'object',
73
- properties: {
74
- action: {
75
- type: 'string',
76
- enum: ['explore', 'node'],
77
- description: "explore=fetch relevant source + call paths by question/symbol (use when understanding a feature/architecture);" +
78
- "node=look up a single symbol's source + callers/callees, or read a file + dependencies (use when locating a specific symbol)",
79
- },
80
- query: {
81
- type: 'string',
82
- description: 'explore: the question or symbol name to investigate (e.g. "runAgent" or "how history is compacted").' +
83
- 'node: symbol name (e.g. runAgent) or file path (used with file mode)',
84
- },
85
- file: {
86
- type: 'string',
87
- description: 'node only: treat query as a file path to read (file mode), or disambiguate to a specific file',
88
- },
89
- offset: {
90
- type: 'integer',
91
- description: 'node file mode only: start line (1-based)',
92
- },
93
- limit: {
94
- type: 'integer',
95
- description: 'node file mode only: max number of lines',
96
- },
97
- },
98
- required: ['action', 'query'],
99
- },
100
- async execute(args, ctx) {
101
- if (!hasCodegraphIndex()) {
102
- return ('当前目录无 .codegraph/ 索引。codegraph 工具不可用。\n' +
103
- '建索引:运行 `codegraph init`(需要 codegraph CLI:`npm i -g @colbymchenry/codegraph`)。\n' +
104
- '在此之前,可改用 read_file / grep / glob 探索代码。');
105
- }
106
- const action = String(args.action);
107
- const query = String(args.query);
108
- const cliArgs = [];
109
- if (action === 'explore') {
110
- // explore 接 <query...> 多 token,拆分后逐个 push
111
- cliArgs.push('explore', ...query.split(/\s+/).filter(Boolean));
112
- }
113
- else if (action === 'node') {
114
- cliArgs.push('node', query);
115
- const file = args.file ? String(args.file) : '';
116
- if (file)
117
- cliArgs.push('--file', file);
118
- if (args.offset !== undefined)
119
- cliArgs.push('--offset', String(args.offset));
120
- if (args.limit !== undefined)
121
- cliArgs.push('--limit', String(args.limit));
122
- }
123
- else {
124
- return `错误:未知 action "${action}",可选 explore 或 node。`;
125
- }
126
- return runCodegraph(cliArgs, ctx?.signal);
127
- },
128
- };
@@ -1,2 +0,0 @@
1
- import type { Tool } from '../types.js';
2
- export declare const editFileTool: Tool;
@@ -1,2 +0,0 @@
1
- import type { Tool } from '../types.js';
2
- export declare const globTool: Tool;
@@ -1,2 +0,0 @@
1
- import type { Tool } from '../types.js';
2
- export declare const grepTool: Tool;
@@ -1,2 +0,0 @@
1
- import type { Tool } from '../types.js';
2
- export declare const builtinTools: Tool[];
@@ -1,2 +0,0 @@
1
- import type { Tool } from '../types.js';
2
- export declare const memoryForgetTool: Tool;
@@ -1,2 +0,0 @@
1
- import type { Tool } from '../types.js';
2
- export declare const memoryListTool: Tool;
@@ -1,2 +0,0 @@
1
- import type { Tool } from '../types.js';
2
- export declare const memorySaveTool: Tool;
@@ -1,2 +0,0 @@
1
- import type { Tool } from '../types.js';
2
- export declare const memorySearchTool: Tool;