mocode-ai 1.2.8 → 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.
- package/dist/agent/core.js +126 -43
- package/dist/config/index.js +31 -6
- package/dist/config/presets.js +36 -1
- package/dist/context/budget.js +0 -15
- package/dist/llm/index.js +33 -0
- package/dist/repl/index.js +57 -13
- package/dist/session/compact.js +237 -13
- package/dist/session/notes.js +63 -0
- package/dist/session/scheduler.js +5 -9
- package/dist/tools/constants.js +7 -0
- package/package.json +1 -1
- package/dist/__trace_manual_test__.js +0 -1
- package/dist/agent/core.d.ts +0 -112
- package/dist/agent/index.d.ts +0 -19
- package/dist/agent/mode.d.ts +0 -29
- package/dist/agent/retry-classifier.js +0 -64
- package/dist/agent/spawn.d.ts +0 -36
- package/dist/attachments/image.d.ts +0 -22
- package/dist/commands/config.d.ts +0 -8
- package/dist/commands/skill.js +0 -230
- package/dist/config/file.d.ts +0 -23
- package/dist/config/index.d.ts +0 -114
- package/dist/config/presets.d.ts +0 -57
- package/dist/context/budget.d.ts +0 -79
- package/dist/context/classifier.d.ts +0 -14
- package/dist/context/encoders/_util.d.ts +0 -11
- package/dist/context/encoders/code.d.ts +0 -2
- package/dist/context/encoders/doc.d.ts +0 -14
- package/dist/context/encoders/graph.d.ts +0 -11
- package/dist/context/encoders/graph.js +0 -81
- package/dist/context/encoders/index.d.ts +0 -2
- package/dist/context/encoders/log.d.ts +0 -2
- package/dist/context/encoders/log.js +0 -2
- package/dist/context/encoders/memory.d.ts +0 -2
- package/dist/context/encoders/passthrough.d.ts +0 -11
- package/dist/context/encoders/search.d.ts +0 -2
- package/dist/context/encoders/summary.d.ts +0 -12
- package/dist/context/encoders/table.d.ts +0 -2
- package/dist/context/encoders/tree.d.ts +0 -2
- package/dist/context/index.d.ts +0 -6
- package/dist/context/lifecycle.d.ts +0 -51
- package/dist/context/pipeline.d.ts +0 -9
- package/dist/context/registry.d.ts +0 -9
- package/dist/context/relevance.d.ts +0 -65
- package/dist/context/types.d.ts +0 -36
- package/dist/foo.js +0 -28
- package/dist/foo.test.js +0 -12
- package/dist/index.d.ts +0 -1
- package/dist/llm/capabilities.d.ts +0 -19
- package/dist/llm/index.d.ts +0 -111
- package/dist/memory/discover.d.ts +0 -15
- package/dist/memory/index.d.ts +0 -13
- package/dist/memory/reflect.d.ts +0 -34
- package/dist/memory/store.d.ts +0 -124
- package/dist/pet/bridge.d.ts +0 -73
- package/dist/pet/protocol.d.ts +0 -83
- package/dist/pet/state.d.ts +0 -28
- package/dist/plan/active.d.ts +0 -23
- package/dist/plan/active.js +0 -45
- package/dist/plan/index.d.ts +0 -13
- package/dist/plan/index.js +0 -33
- package/dist/plan/state.d.ts +0 -2
- package/dist/plan/state.js +0 -11
- package/dist/plan/store.d.ts +0 -78
- package/dist/plan/store.js +0 -429
- package/dist/project-skill/index.js +0 -95
- package/dist/project-skill/initializer.js +0 -178
- package/dist/project-snapshot/index.js +0 -85
- package/dist/project-snapshot/llm-snapshot.js +0 -150
- package/dist/project-snapshot/static-files.js +0 -87
- package/dist/repl/index.d.ts +0 -22
- package/dist/rollback/index.d.ts +0 -79
- package/dist/sandbox/command.d.ts +0 -3
- package/dist/sandbox/index.d.ts +0 -4
- package/dist/sandbox/jail.d.ts +0 -17
- package/dist/sandbox/policy.d.ts +0 -21
- package/dist/sandbox/root.d.ts +0 -5
- package/dist/session/compact.d.ts +0 -92
- package/dist/session/index.d.ts +0 -13
- package/dist/session/notes-plan.js +0 -41
- package/dist/session/persist.d.ts +0 -30
- package/dist/session/scheduler.d.ts +0 -53
- package/dist/skills/discover.d.ts +0 -33
- package/dist/skills/index.d.ts +0 -13
- package/dist/skills/skill-eval.js +0 -345
- package/dist/skills/skill-improve.js +0 -221
- package/dist/skills/stats.js +0 -102
- package/dist/tools/builtins/apply-patch.js +0 -174
- package/dist/tools/builtins/ask-human.d.ts +0 -9
- package/dist/tools/builtins/codegraph.d.ts +0 -2
- package/dist/tools/builtins/codegraph.js +0 -128
- package/dist/tools/builtins/edit-file.d.ts +0 -2
- package/dist/tools/builtins/glob.d.ts +0 -2
- package/dist/tools/builtins/grep.d.ts +0 -2
- package/dist/tools/builtins/index.d.ts +0 -2
- package/dist/tools/builtins/memory-forget.d.ts +0 -2
- package/dist/tools/builtins/memory-list.d.ts +0 -2
- package/dist/tools/builtins/memory-save.d.ts +0 -2
- package/dist/tools/builtins/memory-search.d.ts +0 -2
- package/dist/tools/builtins/memory-update.d.ts +0 -2
- package/dist/tools/builtins/project-skill-update.js +0 -102
- package/dist/tools/builtins/read-file.d.ts +0 -2
- package/dist/tools/builtins/run-command.d.ts +0 -2
- package/dist/tools/builtins/switch-mode.d.ts +0 -2
- package/dist/tools/builtins/switch-mode.js +0 -44
- package/dist/tools/builtins/task.d.ts +0 -2
- package/dist/tools/builtins/todolist.d.ts +0 -2
- package/dist/tools/builtins/todolist.js +0 -411
- package/dist/tools/builtins/use-skill.d.ts +0 -2
- package/dist/tools/builtins/web-fetch.d.ts +0 -2
- package/dist/tools/builtins/web-search.d.ts +0 -2
- package/dist/tools/builtins/write-file.d.ts +0 -2
- package/dist/tools/constants.d.ts +0 -52
- package/dist/tools/registry.d.ts +0 -18
- package/dist/tools/retry.js +0 -137
- package/dist/tools/types.d.ts +0 -42
- package/dist/ui/batch.d.ts +0 -67
- package/dist/ui/clipboard.d.ts +0 -9
- package/dist/ui/content.d.ts +0 -55
- package/dist/ui/diff.d.ts +0 -34
- package/dist/ui/intervention.d.ts +0 -46
- package/dist/ui/layout.d.ts +0 -225
- package/dist/ui/markdown.d.ts +0 -46
- package/dist/ui/mouse.d.ts +0 -29
- package/dist/ui/prompt.d.ts +0 -61
- package/dist/ui/render.d.ts +0 -60
- package/dist/ui/spinner.d.ts +0 -26
- package/dist/ui/theme.d.ts +0 -47
- package/dist/ui/thinking.js +0 -80
package/dist/skills/stats.js
DELETED
|
@@ -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,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,102 +0,0 @@
|
|
|
1
|
-
import { readProjectSkill, writeProjectSkillWithCompression, appendProjectSkillWithCompression, } from '../../project-skill/index.js';
|
|
2
|
-
/**
|
|
3
|
-
* 项目专属 Skill 更新工具。
|
|
4
|
-
* 支持三种操作:
|
|
5
|
-
* - read: 读取当前 skill 内容
|
|
6
|
-
* - write: 全量覆盖(适合重构整个 skill)
|
|
7
|
-
* - append: 追加到末尾(适合发现新坑点/约定时增量更新)
|
|
8
|
-
*
|
|
9
|
-
* 使用场景:
|
|
10
|
-
* - 开发过程中发现项目特有的架构模式、命名约定
|
|
11
|
-
* - 踩坑后记录避坑指南
|
|
12
|
-
* - 总结项目关键决策和设计理由
|
|
13
|
-
* - 随着项目开发,skill 越来越了解项目,开发效率越来越高
|
|
14
|
-
*/
|
|
15
|
-
export const projectSkillUpdateTool = {
|
|
16
|
-
name: 'project_skill_update',
|
|
17
|
-
description: 'Update the project-specific skill file (.mocode/project-skill.md). ' +
|
|
18
|
-
'Use this to record architectural patterns, naming conventions, common pitfalls, ' +
|
|
19
|
-
'and key decisions specific to this project. The skill persists across sessions and ' +
|
|
20
|
-
'is injected into the system prompt when MOCODE_PROJECT_SKILL=true. ' +
|
|
21
|
-
'Actions: "read" (view current content), "write" (replace all), "append" (add to end). ' +
|
|
22
|
-
'\n\n' +
|
|
23
|
-
'## Skill 维护指南\n' +
|
|
24
|
-
'Skill 专注 Snapshot 无法自动提供的洞察知识(why/how/gotchas)。Snapshot 已提供文件结构和静态文件内容——Skill 不要重复这些信息。\n' +
|
|
25
|
-
'\n' +
|
|
26
|
-
'**Skill 该写什么(Snapshot 不能替代的)**:\n' +
|
|
27
|
-
'- 设计决策的 why(为什么这样设计、取舍是什么)\n' +
|
|
28
|
-
'- 模块行为和职责描述(怎么工作、数据流、调用链)\n' +
|
|
29
|
-
'- 常见坑点和解决方案(踩过的坑、非直觉行为、边界条件)\n' +
|
|
30
|
-
'- 项目约定(命名规范、代码风格、测试策略)\n' +
|
|
31
|
-
'- 开发流程(构建/测试/部署命令及注意事项)\n' +
|
|
32
|
-
'- 关键 API 的使用限制和特殊行为\n' +
|
|
33
|
-
'\n' +
|
|
34
|
-
'**Skill 不该写什么(交给 Snapshot)**:\n' +
|
|
35
|
-
'- 文件列表和目录结构(Snapshot 自动扫描 src 树)\n' +
|
|
36
|
-
'- 静态文件内容摘要(依赖、编译器选项等 → package.json / tsconfig.json)\n' +
|
|
37
|
-
'\n' +
|
|
38
|
-
'**何时更新**:发现新架构模式、踩坑后总结、学到新约定、完成重要重构、用户纠正理解时。\n' +
|
|
39
|
-
'\n' +
|
|
40
|
-
'**注意事项**:\n' +
|
|
41
|
-
'- 内容长度不设硬上限,按项目实际复杂度完整记录有价值的信息\n' +
|
|
42
|
-
'- 写可操作的内容,避免空泛描述\n' +
|
|
43
|
-
'- 用具体路径和例子(不要写"有多个模块",要写"src/agent 负责 agent 循环")\n' +
|
|
44
|
-
'- 定期整理,删除过时和重复信息\n' +
|
|
45
|
-
'- 更新前建议先 `read` 看一下现有内容,避免重复。',
|
|
46
|
-
risk: 'safe',
|
|
47
|
-
parameters: {
|
|
48
|
-
type: 'object',
|
|
49
|
-
properties: {
|
|
50
|
-
action: {
|
|
51
|
-
type: 'string',
|
|
52
|
-
enum: ['read', 'write', 'append'],
|
|
53
|
-
description: 'Action to perform: "read" to view current content, "write" to replace all, "append" to add to end',
|
|
54
|
-
},
|
|
55
|
-
content: {
|
|
56
|
-
type: 'string',
|
|
57
|
-
description: 'Content to write or append (required for "write" and "append" actions, ignored for "read")',
|
|
58
|
-
},
|
|
59
|
-
},
|
|
60
|
-
required: ['action'],
|
|
61
|
-
},
|
|
62
|
-
execute: async (args, ctx) => {
|
|
63
|
-
const action = String(args.action ?? '').trim();
|
|
64
|
-
const content = String(args.content ?? '');
|
|
65
|
-
const signal = ctx?.signal;
|
|
66
|
-
switch (action) {
|
|
67
|
-
case 'read': {
|
|
68
|
-
const current = readProjectSkill();
|
|
69
|
-
if (!current) {
|
|
70
|
-
return 'Project skill is empty or does not exist yet.';
|
|
71
|
-
}
|
|
72
|
-
return `Current project skill:\n\n${current}`;
|
|
73
|
-
}
|
|
74
|
-
case 'write': {
|
|
75
|
-
if (!content.trim()) {
|
|
76
|
-
return 'Error: "write" action requires non-empty content parameter.';
|
|
77
|
-
}
|
|
78
|
-
const result = await writeProjectSkillWithCompression(content, 3, signal);
|
|
79
|
-
if (!result.ok) {
|
|
80
|
-
return `Error: ${result.error}`;
|
|
81
|
-
}
|
|
82
|
-
return result.compressed
|
|
83
|
-
? 'Project skill updated successfully (full replacement, auto-compressed to fit limit).'
|
|
84
|
-
: 'Project skill updated successfully (full replacement).';
|
|
85
|
-
}
|
|
86
|
-
case 'append': {
|
|
87
|
-
if (!content.trim()) {
|
|
88
|
-
return 'Error: "append" action requires non-empty content parameter.';
|
|
89
|
-
}
|
|
90
|
-
const result = await appendProjectSkillWithCompression(content, 3, signal);
|
|
91
|
-
if (!result.ok) {
|
|
92
|
-
return `Error: ${result.error}`;
|
|
93
|
-
}
|
|
94
|
-
return result.compressed
|
|
95
|
-
? 'Project skill updated successfully (appended, auto-compressed to fit limit).'
|
|
96
|
-
: 'Project skill updated successfully (appended).';
|
|
97
|
-
}
|
|
98
|
-
default:
|
|
99
|
-
return `Error: Unknown action "${action}". Use "read", "write", or "append".`;
|
|
100
|
-
}
|
|
101
|
-
},
|
|
102
|
-
};
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { setAgentMode, getAgentMode } from '../../agent/mode.js';
|
|
2
|
-
// ---------- switch_mode ----------
|
|
3
|
-
export const switchModeTool = {
|
|
4
|
-
name: 'switch_mode',
|
|
5
|
-
description: [
|
|
6
|
-
'Switch agent mode between "plan" (read-only investigation) and "auto" (full tool execution).',
|
|
7
|
-
'Use to transition from planning to execution WITHIN THE SAME TURN: in plan, after presenting a plan, call switch_mode("auto") and continue — ONLY when the user asked for autonomous execution ("先 plan 再 auto" etc.).',
|
|
8
|
-
'If the user entered plan mode manually (/plan or Shift+Tab) for review, do NOT switch to auto — present the plan and STOP for approval.',
|
|
9
|
-
].join(' '),
|
|
10
|
-
parameters: {
|
|
11
|
-
type: 'object',
|
|
12
|
-
properties: {
|
|
13
|
-
mode: {
|
|
14
|
-
type: 'string',
|
|
15
|
-
enum: ['auto', 'plan'],
|
|
16
|
-
description: 'Target mode: "plan" (read-only) or "auto" (full tools)',
|
|
17
|
-
},
|
|
18
|
-
},
|
|
19
|
-
required: ['mode'],
|
|
20
|
-
},
|
|
21
|
-
async execute(args) {
|
|
22
|
-
const target = String(args.mode);
|
|
23
|
-
if (target !== 'auto' && target !== 'plan') {
|
|
24
|
-
return `错误:mode 必须是 "auto" 或 "plan",收到 "${target}"`;
|
|
25
|
-
}
|
|
26
|
-
const prev = getAgentMode();
|
|
27
|
-
if (prev === target) {
|
|
28
|
-
return `已在 ${target} 模式,无需切换。`;
|
|
29
|
-
}
|
|
30
|
-
setAgentMode(target); // 触发 listener:repl 重写 history[0] 系统提示 + 刷状态行 modeTag
|
|
31
|
-
if (target === 'auto') {
|
|
32
|
-
return [
|
|
33
|
-
`已从 plan 切换到 auto 模式。`,
|
|
34
|
-
`写盘 / 命令 / 记忆写入工具(write_file / edit_file / run_command / memory_save / memory_update / memory_forget)现已恢复可用。`,
|
|
35
|
-
`请按你刚才产出的计划继续执行(同一轮内,无需重述计划)。`,
|
|
36
|
-
].join('\n');
|
|
37
|
-
}
|
|
38
|
-
return [
|
|
39
|
-
`已从 auto 切换到 plan 模式(只读探查)。`,
|
|
40
|
-
`写盘 / 命令 / 记忆写入工具已禁用;只用 read_file / glob / grep / codegraph / web_search / web_fetch / use_skill / ask_human / memory_search / memory_list 探查。`,
|
|
41
|
-
`产出计划后 STOP 等用户审批,或(若用户要求自主执行)再调 switch_mode("auto") 续跑。`,
|
|
42
|
-
].join('\n');
|
|
43
|
-
},
|
|
44
|
-
};
|