mocode-ai 0.1.3 → 0.1.5

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 (48) hide show
  1. package/README.md +196 -161
  2. package/dist/agent/core.js +302 -0
  3. package/dist/agent/index.js +85 -202
  4. package/dist/agent/mode.js +46 -0
  5. package/dist/agent/spawn.js +123 -0
  6. package/dist/commands/config.js +8 -26
  7. package/dist/config/file.js +48 -0
  8. package/dist/config/index.js +76 -22
  9. package/dist/llm/index.js +11 -9
  10. package/dist/memory/discover.js +49 -0
  11. package/dist/memory/index.js +45 -0
  12. package/dist/memory/reflect.js +265 -0
  13. package/dist/memory/store.js +339 -0
  14. package/dist/repl/index.js +334 -87
  15. package/dist/session/compact.js +51 -3
  16. package/dist/skills/index.js +2 -2
  17. package/dist/tools/builtins/ask-human.js +50 -0
  18. package/dist/tools/builtins/codegraph.js +113 -0
  19. package/dist/tools/builtins/edit-file.js +3 -3
  20. package/dist/tools/builtins/glob.js +3 -2
  21. package/dist/tools/builtins/grep.js +4 -3
  22. package/dist/tools/builtins/index.js +19 -1
  23. package/dist/tools/builtins/memory-forget.js +32 -0
  24. package/dist/tools/builtins/memory-list.js +34 -0
  25. package/dist/tools/builtins/memory-save.js +51 -0
  26. package/dist/tools/builtins/memory-search.js +42 -0
  27. package/dist/tools/builtins/memory-update.js +41 -0
  28. package/dist/tools/builtins/read-file.js +5 -4
  29. package/dist/tools/builtins/run-command.js +40 -7
  30. package/dist/tools/builtins/switch-mode.js +46 -0
  31. package/dist/tools/builtins/task.js +62 -0
  32. package/dist/tools/builtins/use-skill.js +2 -2
  33. package/dist/tools/builtins/web-fetch.js +16 -3
  34. package/dist/tools/builtins/web-search.js +6 -6
  35. package/dist/tools/builtins/write-file.js +3 -3
  36. package/dist/tools/constants.js +29 -0
  37. package/dist/tools/registry.js +11 -4
  38. package/dist/ui/content.js +25 -17
  39. package/dist/ui/diff.js +28 -26
  40. package/dist/ui/intervention.js +318 -0
  41. package/dist/ui/layout.js +402 -103
  42. package/dist/ui/markdown.js +607 -0
  43. package/dist/ui/mouse.js +93 -0
  44. package/dist/ui/prompt.js +469 -27
  45. package/dist/ui/render.js +19 -1
  46. package/dist/ui/spinner.js +15 -0
  47. package/dist/ui/theme.js +147 -13
  48. package/package.json +2 -2
@@ -1,6 +1,6 @@
1
1
  import { chat, estimateMessagesTokens, estimateToolSchemaTokens, estimateTokens, } from '../llm/index.js';
2
2
  import { config } from '../config/index.js';
3
- import { MAX_HISTORY_RESULT, MAX_OLD_TOOL_STUB, MAX_SKILL_RESULT } from '../tools/constants.js';
3
+ import { MAX_HISTORY_RESULT, MAX_MEMORY_RESULT, MAX_OLD_TOOL_STUB, MAX_SKILL_RESULT } from '../tools/constants.js';
4
4
  import { ui } from '../ui/theme.js';
5
5
  import { Spinner } from '../ui/spinner.js';
6
6
  import * as layout from '../ui/layout.js';
@@ -40,6 +40,17 @@ export function capToolResultForHistory(name, output) {
40
40
  return marker.slice(0, MAX_SKILL_RESULT);
41
41
  return output.slice(0, remain) + marker;
42
42
  }
43
+ if (name === 'memory_search') {
44
+ // 召回的记忆正文是高价值事实/指令,中截会劈断语义;同 use_skill 走尾截(保头部、弃尾部)。
45
+ if (output.length <= MAX_MEMORY_RESULT)
46
+ return output;
47
+ const removed = output.length - MAX_MEMORY_RESULT;
48
+ const marker = `…[记忆结果过长,已截断尾部 ${removed} 字符]…`;
49
+ const remain = MAX_MEMORY_RESULT - marker.length;
50
+ if (remain <= 0)
51
+ return marker.slice(0, MAX_MEMORY_RESULT);
52
+ return output.slice(0, remain) + marker;
53
+ }
43
54
  if (output.length <= MAX_HISTORY_RESULT)
44
55
  return output;
45
56
  return truncateMid(output, MAX_HISTORY_RESULT);
@@ -136,7 +147,7 @@ async function defaultSummarize(older, focus) {
136
147
  }
137
148
  const sysMsg = {
138
149
  role: 'system',
139
- content: '你是会话摘要器。只输出摘要正文,不超过 300 字,保留:用户核心请求、已读写/改动的文件及关键变更、执行过的关键命令及结果要点、已做决策、当前任务进度与下一步、未决问题。不要复述全部细节。',
150
+ content: 'You are a session summarizer. Output only the summary body, max 300 words, preserving: the user\'s core request; files read/written/modified and key changes; key commands run and their result highlights; decisions made; current task progress and next step; open questions. Do not recap every detail.',
140
151
  };
141
152
  const userMsg = {
142
153
  role: 'user',
@@ -194,7 +205,12 @@ export async function compactHistory(history, opts) {
194
205
  }
195
206
  return noop;
196
207
  }
197
- // 第一层:微压缩——旧区 tool 结果原地截短(保 tool_call_id,无 LLM 调用)
208
+ // 第一层:微压缩——旧区原地截短(保 tool_call_id,无 LLM 调用)
209
+ // 覆盖三类大字段,均只裁模型/工具产物,不动 user 原话与 system(摘要):
210
+ // ① tool 结果 content;
211
+ // ② 旧 assistant 的 tool_calls.arguments —— 保 JSON 合法:整体超长才进,
212
+ // 逐字段值裁中截后重新 stringify(不裸中截会劈断 JSON、严格后端 400);parse 失败跳过;
213
+ // ③ 旧 assistant 正文 content(模型长解释,回看价值低)。
198
214
  let microcompactDone = false;
199
215
  for (const g of oldGroups) {
200
216
  for (const t of g.tools) {
@@ -204,6 +220,38 @@ export async function compactHistory(history, opts) {
204
220
  microcompactDone = true;
205
221
  }
206
222
  }
223
+ const as = g.assistant;
224
+ if (as && as.role === 'assistant') {
225
+ // ③ 旧正文 content
226
+ if (typeof as.content === 'string' && as.content.length > MAX_OLD_TOOL_STUB) {
227
+ as.content = truncateMid(as.content, MAX_OLD_TOOL_STUB);
228
+ microcompactDone = true;
229
+ }
230
+ // ② tool_calls 参数:整体超长才进,逐字段裁值,保 JSON 合法
231
+ if (Array.isArray(as.tool_calls)) {
232
+ for (const tc of as.tool_calls) {
233
+ const args = tc?.function?.arguments;
234
+ if (typeof args !== 'string' || args.length <= MAX_OLD_TOOL_STUB)
235
+ continue;
236
+ try {
237
+ const parsed = JSON.parse(args);
238
+ if (parsed && typeof parsed === 'object') {
239
+ for (const k of Object.keys(parsed)) {
240
+ const v = parsed[k];
241
+ if (typeof v === 'string' && v.length > MAX_OLD_TOOL_STUB) {
242
+ parsed[k] = truncateMid(v, MAX_OLD_TOOL_STUB);
243
+ }
244
+ }
245
+ tc.function.arguments = JSON.stringify(parsed);
246
+ microcompactDone = true;
247
+ }
248
+ }
249
+ catch {
250
+ // arguments 非合法 JSON(模型偶发):不裁,沿用「调度器永不抛错」
251
+ }
252
+ }
253
+ }
254
+ }
207
255
  }
208
256
  // 第二层:摘要——把旧区(微压缩后)压成一条 system 摘要
209
257
  const older = flattenGroups(oldGroups);
@@ -39,8 +39,8 @@ export function buildSkillsSection() {
39
39
  return [
40
40
  '',
41
41
  '',
42
- '## Skills(按需加载)',
43
- '以下 skill 可用。只在任务相关时调用 use_skill 工具(传 skill 的 name)加载其完整指令,据此行动;不要无脑批量加载。',
42
+ '## Skills (load on demand)',
43
+ 'The following skills are available. Call the use_skill tool (passing the skill name) only when relevant to the current task, to load its full instructions and act on them; do not load them all blindly.',
44
44
  ...lines,
45
45
  ].join('\n');
46
46
  }
@@ -0,0 +1,50 @@
1
+ import { promptIntervention } from '../../ui/intervention.js';
2
+ // ---------- ask_human ----------
3
+ export const askHumanTool = {
4
+ name: 'ask_human',
5
+ description: [
6
+ 'Call this tool when you hit a decision point during a task that requires human input — it pops up a question panel in the terminal for the user to choose.',
7
+ 'Use when: multiple implementation approaches need a user decision, user intent is unclear and needs clarification, or extra info is needed to proceed.',
8
+ 'Blocks until the user responds; the user can pick a preset option or choose "custom input" to answer freely; the result is returned as the tool result.',
9
+ 'Do not call frequently when the task is clear and you can decide yourself — it interrupts the user. When options is omitted or empty, it becomes free-text input instead.',
10
+ ].join(''),
11
+ parameters: {
12
+ type: 'object',
13
+ properties: {
14
+ question: {
15
+ type: 'string',
16
+ description: 'The question to ask the user; keep it concise (shown as the panel title)',
17
+ },
18
+ options: {
19
+ type: 'array',
20
+ items: { type: 'string' },
21
+ description: 'Options for the user to choose from (2~6). May be omitted — when omitted or empty, it becomes free-text input instead',
22
+ },
23
+ context: {
24
+ type: 'string',
25
+ description: 'Background explanation for the question (optional; helps the user understand why their decision is needed; shown under the title, may be multiline)',
26
+ },
27
+ },
28
+ required: ['question'],
29
+ },
30
+ async execute(args) {
31
+ const question = String(args.question ?? '');
32
+ const options = Array.isArray(args.options)
33
+ ? args.options.map((o) => String(o))
34
+ : [];
35
+ const context = args.context ? String(args.context) : undefined;
36
+ const result = await promptIntervention({
37
+ type: options.length > 0 ? 'choice' : 'input',
38
+ title: question,
39
+ options: options.length > 0 ? options : undefined,
40
+ detail: context,
41
+ });
42
+ if (result.action === 'cancelled') {
43
+ return '用户取消了选择。请考虑是否有不依赖用户输入的替代方案,或换个角度重新提问。';
44
+ }
45
+ if (result.action === 'submitted') {
46
+ return `用户回答:${result.value ?? ''}`;
47
+ }
48
+ return `用户选择:${result.value ?? ''}`;
49
+ },
50
+ };
@@ -0,0 +1,113 @@
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
+ function runCodegraph(args) {
15
+ return new Promise((done) => {
16
+ const isWin = process.platform === 'win32';
17
+ // win32 必须走 cmd.exe /c:Node 自 CVE-2024-27980 修复后,shell:false 直接 spawn
18
+ // .cmd/.bat 会抛 EINVAL;cmd.exe 是真二进制,直接 spawn 安全(与 run_command 同约定)。
19
+ const child = isWin
20
+ ? spawn('cmd.exe', ['/c', 'codegraph', ...args], { cwd: process.cwd() })
21
+ : spawn('codegraph', args, { cwd: process.cwd() });
22
+ let out = '';
23
+ let finished = false;
24
+ const finish = (s) => {
25
+ if (finished)
26
+ return;
27
+ finished = true;
28
+ clearTimeout(timer);
29
+ done(s);
30
+ };
31
+ const onChunk = (chunk) => {
32
+ if (out.length < MAX_OUTPUT)
33
+ out += chunk.toString('utf8');
34
+ };
35
+ child.stdout.on('data', onChunk);
36
+ child.stderr.on('data', onChunk);
37
+ child.on('error', (e) => finish(`codegraph 执行失败(可能未安装或不在 PATH): ${e.message}\n` +
38
+ '安装: npm i -g @colbymchenry/codegraph'));
39
+ child.on('close', (code) => {
40
+ let r = out.trim();
41
+ if (out.length >= MAX_OUTPUT)
42
+ r += '\n...(输出已截断)';
43
+ finish(`[退出码 ${code}]\n${r || '(无输出)'}`);
44
+ });
45
+ const timer = setTimeout(() => {
46
+ child.kill();
47
+ finish(`[超时,已终止]\n${out.trim()}`);
48
+ }, 60000);
49
+ });
50
+ }
51
+ export const codegraphTool = {
52
+ name: 'codegraph',
53
+ description: 'Preferred tool for understanding/locating code, tracing call chains, and assessing the impact of changes in repos with a code index (.codegraph/).' +
54
+ ' Returns relevant symbol source + call paths in one shot — more accurate and economical than piecing together via read_file/grep.' +
55
+ ' Returns a hint when .codegraph/ is absent (build it first with `codegraph init`).',
56
+ parameters: {
57
+ type: 'object',
58
+ properties: {
59
+ action: {
60
+ type: 'string',
61
+ enum: ['explore', 'node'],
62
+ description: "explore=fetch relevant source + call paths by question/symbol (use when understanding a feature/architecture);" +
63
+ "node=look up a single symbol's source + callers/callees, or read a file + dependencies (use when locating a specific symbol)",
64
+ },
65
+ query: {
66
+ type: 'string',
67
+ description: 'explore: the question or symbol name to investigate (e.g. "runAgent" or "how history is compacted").' +
68
+ 'node: symbol name (e.g. runAgent) or file path (used with file mode)',
69
+ },
70
+ file: {
71
+ type: 'string',
72
+ description: 'node only: treat query as a file path to read (file mode), or disambiguate to a specific file',
73
+ },
74
+ offset: {
75
+ type: 'integer',
76
+ description: 'node file mode only: start line (1-based)',
77
+ },
78
+ limit: {
79
+ type: 'integer',
80
+ description: 'node file mode only: max number of lines',
81
+ },
82
+ },
83
+ required: ['action', 'query'],
84
+ },
85
+ async execute(args) {
86
+ if (!hasCodegraphIndex()) {
87
+ return ('当前目录无 .codegraph/ 索引。codegraph 工具不可用。\n' +
88
+ '建索引:运行 `codegraph init`(需要 codegraph CLI:`npm i -g @colbymchenry/codegraph`)。\n' +
89
+ '在此之前,可改用 read_file / grep / glob 探索代码。');
90
+ }
91
+ const action = String(args.action);
92
+ const query = String(args.query);
93
+ const cliArgs = [];
94
+ if (action === 'explore') {
95
+ // explore 接 <query...> 多 token,拆分后逐个 push
96
+ cliArgs.push('explore', ...query.split(/\s+/).filter(Boolean));
97
+ }
98
+ else if (action === 'node') {
99
+ cliArgs.push('node', query);
100
+ const file = args.file ? String(args.file) : '';
101
+ if (file)
102
+ cliArgs.push('--file', file);
103
+ if (args.offset !== undefined)
104
+ cliArgs.push('--offset', String(args.offset));
105
+ if (args.limit !== undefined)
106
+ cliArgs.push('--limit', String(args.limit));
107
+ }
108
+ else {
109
+ return `错误:未知 action "${action}",可选 explore 或 node。`;
110
+ }
111
+ return runCodegraph(cliArgs);
112
+ },
113
+ };
@@ -3,13 +3,13 @@ import { resolve } from 'node:path';
3
3
  // ---------- edit_file ----------
4
4
  export const editFileTool = {
5
5
  name: 'edit_file',
6
- description: '对文件做精确字符串替换。old_string 必须在文件中唯一出现且完全匹配(含缩进/换行)。新建文件请用 write_file。',
6
+ description: 'Make a precise string replacement in a file. old_string must occur exactly once in the file and match exactly (including indentation/newlines). Use write_file for new files.',
7
7
  parameters: {
8
8
  type: 'object',
9
9
  properties: {
10
10
  path: { type: 'string' },
11
- old_string: { type: 'string', description: '要被替换的原文,须精确匹配' },
12
- new_string: { type: 'string', description: '替换后的新文本' },
11
+ old_string: { type: 'string', description: 'The original text to be replaced; must match exactly' },
12
+ new_string: { type: 'string', description: 'The new text to replace it with' },
13
13
  },
14
14
  required: ['path', 'old_string', 'new_string'],
15
15
  },
@@ -3,11 +3,12 @@ import { IGNORE } from '../constants.js';
3
3
  // ---------- glob ----------
4
4
  export const globTool = {
5
5
  name: 'glob',
6
- description: '按 glob 模式查找文件路径(如 **/*.ts)。返回匹配列表(自动排除 node_modules / .git)。',
6
+ description: 'Find file paths matching a glob pattern (e.g. **/*.ts). Returns a list of matches (auto-excludes node_modules / .git).' +
7
+ ' Note: when understanding code architecture/call chains, if a .codegraph/ index exists, prefer the codegraph tool over piecing together via glob one file at a time.',
7
8
  parameters: {
8
9
  type: 'object',
9
10
  properties: {
10
- pattern: { type: 'string', description: 'glob 模式,如 **/*.ts 或 src/**/*.json' },
11
+ pattern: { type: 'string', description: 'Glob pattern, e.g. **/*.ts or src/**/*.json' },
11
12
  },
12
13
  required: ['pattern'],
13
14
  },
@@ -5,12 +5,13 @@ import { MAX_RESULTS, IGNORE } from '../constants.js';
5
5
  // ---------- grep ----------
6
6
  export const grepTool = {
7
7
  name: 'grep',
8
- description: '在文件内容里按正则搜索,返回 file:line: 匹配行。默认递归搜索当前目录(排除 node_modules/.git)。可选 glob 限定文件类型。',
8
+ description: 'Search file contents by regex, returning file:line: matched lines. Recursively searches the current directory by default (excluding node_modules/.git). Optional glob to restrict file types.' +
9
+ ' Note: when understanding code architecture/call chains, if a .codegraph/ index exists, prefer the codegraph tool over piecing together via grep one file at a time.',
9
10
  parameters: {
10
11
  type: 'object',
11
12
  properties: {
12
- pattern: { type: 'string', description: '正则表达式' },
13
- glob: { type: 'string', description: '可选,限定文件 glob,如 *.ts' },
13
+ pattern: { type: 'string', description: 'Regular expression' },
14
+ glob: { type: 'string', description: 'Optional, restrict to a file glob, e.g. *.ts' },
14
15
  },
15
16
  required: ['pattern'],
16
17
  },
@@ -7,9 +7,18 @@ import { grepTool } from './grep.js';
7
7
  import { webSearchTool } from './web-search.js';
8
8
  import { webFetchTool } from './web-fetch.js';
9
9
  import { useSkillTool } from './use-skill.js';
10
+ import { askHumanTool } from './ask-human.js';
11
+ import { codegraphTool } from './codegraph.js';
12
+ import { switchModeTool } from './switch-mode.js';
13
+ import { memorySaveTool } from './memory-save.js';
14
+ import { memorySearchTool } from './memory-search.js';
15
+ import { memoryListTool } from './memory-list.js';
16
+ import { memoryUpdateTool } from './memory-update.js';
17
+ import { memoryForgetTool } from './memory-forget.js';
18
+ import { taskTool } from './task.js';
10
19
  /**
11
20
  * 所有内置工具,按注册顺序排列。
12
- * 加新工具:在本目录新建 `xxx.ts` 导出一个 Tool,再在下面数组里加一行。
21
+ * 加新工具:在本目录新建 `xxx.ts` 导出一个 Tool,再在下面数组里加一行。无需改 agent / llm。
13
22
  */
14
23
  export const builtinTools = [
15
24
  readFileTool,
@@ -18,7 +27,16 @@ export const builtinTools = [
18
27
  runCommandTool,
19
28
  globTool,
20
29
  grepTool,
30
+ codegraphTool,
21
31
  webSearchTool,
22
32
  webFetchTool,
23
33
  useSkillTool,
34
+ askHumanTool,
35
+ switchModeTool, // plan↔auto 自切(两模式都可见,不进 PLAN_DISABLED_TOOLS;副作用控制工具→串行分支)
36
+ memorySaveTool,
37
+ memorySearchTool,
38
+ memoryListTool,
39
+ memoryUpdateTool,
40
+ memoryForgetTool,
41
+ taskTool, // 派生子 agent(独立 history + 可受限工具集);plan 模式禁用(见 PLAN_DISABLED_TOOLS)
24
42
  ];
@@ -0,0 +1,32 @@
1
+ import { forgetEntry } from '../../memory/store.js';
2
+ // ---------- memory_forget ----------
3
+ // 遗忘:默认归档(archived,从索引/默认搜索隐藏,可复活);mode=delete 硬删。pinned 拒删。
4
+ export const memoryForgetTool = {
5
+ name: 'memory_forget',
6
+ description: 'Forget a memory entry: by default archives (archived, hidden from index and default search, can be revived via memory_update); mode=delete hard-deletes. Pinned entries cannot be deleted (first memory_update pinned=false to unpin).',
7
+ parameters: {
8
+ type: 'object',
9
+ properties: {
10
+ id: { type: 'string' },
11
+ mode: {
12
+ type: 'string',
13
+ enum: ['archive', 'delete'],
14
+ description: 'Default archive',
15
+ },
16
+ },
17
+ required: ['id'],
18
+ },
19
+ async execute(args) {
20
+ const id = String(args.id ?? '').trim();
21
+ if (!id)
22
+ return '错误:缺少 id。';
23
+ const mode = args.mode === 'delete' ? 'delete' : 'archive';
24
+ const r = forgetEntry(id, mode);
25
+ if (r.ok)
26
+ return `已${mode === 'delete' ? '硬删' : '归档'}记忆 [${id}]。`;
27
+ if ('notFound' in r)
28
+ return `错误:找不到记忆 id="${id}"。`;
29
+ // pinned
30
+ return `错误:[${id}] 已钉住,拒绝遗忘。先 memory_update(id="${id}", pinned=false) 解钉。`;
31
+ },
32
+ };
@@ -0,0 +1,34 @@
1
+ import { listEntries } from '../../memory/store.js';
2
+ // ---------- memory_list ----------
3
+ // 列索引(id/name/summary,无正文、不 bump recall)。用于浏览有哪些、拿 id 再 memory_search 取正文。
4
+ export const memoryListTool = {
5
+ name: 'memory_list',
6
+ description: 'List the memory index (id/name/summary, no body). Defaults to active. Get an id, then use memory_search to retrieve the full body.',
7
+ parameters: {
8
+ type: 'object',
9
+ properties: {
10
+ type: {
11
+ type: 'string',
12
+ enum: ['decision', 'fact', 'pitfall', 'reference', 'feedback'],
13
+ },
14
+ status: {
15
+ type: 'string',
16
+ enum: ['active', 'superseded', 'archived', 'any'],
17
+ description: 'Default active',
18
+ },
19
+ },
20
+ },
21
+ async execute(args) {
22
+ const items = listEntries({
23
+ type: typeof args.type === 'string' ? args.type : undefined,
24
+ status: typeof args.status === 'string'
25
+ ? args.status
26
+ : undefined,
27
+ });
28
+ if (items.length === 0)
29
+ return '(无记忆条目)';
30
+ return items
31
+ .map((i) => `- ${i.id}: ${i.name} — ${i.summary} (${i.type}, ${i.status})`)
32
+ .join('\n');
33
+ },
34
+ };
@@ -0,0 +1,51 @@
1
+ import { saveEntry } from '../../memory/store.js';
2
+ // ---------- memory_save ----------
3
+ // 存一条长期记忆(跨会话)。启动只把标题/摘要注入索引(几百 token);详情按需 memory_search 取。
4
+ // 撞库(name→id 已存在)拒绝,引导用 memory_update。
5
+ export const memorySaveTool = {
6
+ name: 'memory_save',
7
+ description: 'Save a long-term memory entry (cross-session). Store only non-obvious, long-term-useful facts/decisions/pitfalls. The title goes into the startup index; retrieve full body on demand via memory_search.',
8
+ parameters: {
9
+ type: 'object',
10
+ properties: {
11
+ name: { type: 'string', description: 'Short unique title (converted to id; rejected if it collides with an existing one)' },
12
+ summary: { type: 'string', description: 'One-line summary (goes into the index; keep it short)' },
13
+ body: { type: 'string', description: 'Full content (details/context/evidence)' },
14
+ type: {
15
+ type: 'string',
16
+ enum: ['decision', 'fact', 'pitfall', 'reference', 'feedback'],
17
+ description: 'Category, default fact',
18
+ },
19
+ pinned: { type: 'boolean', description: 'Pin (exempt from auto-forgetting decay), default false' },
20
+ scope: {
21
+ type: 'string',
22
+ enum: ['project', 'global'],
23
+ description: 'Store at project level (<cwd>/.mocode/) or global (~/.mocode/), default project',
24
+ },
25
+ },
26
+ required: ['name', 'summary', 'body'],
27
+ },
28
+ async execute(args) {
29
+ const name = String(args.name ?? '').trim();
30
+ const summary = String(args.summary ?? '').trim();
31
+ const body = String(args.body ?? '').trim();
32
+ if (!name)
33
+ return '错误:缺少 name。';
34
+ if (!summary)
35
+ return '错误:缺少 summary。';
36
+ if (!body)
37
+ return '错误:缺少 body。';
38
+ const type = typeof args.type === 'string' ? args.type : undefined;
39
+ const r = saveEntry({
40
+ name,
41
+ summary,
42
+ body,
43
+ type,
44
+ pinned: args.pinned === true,
45
+ scope: args.scope === 'global' ? 'global' : 'project',
46
+ });
47
+ if (r.ok)
48
+ return `已保存记忆 [${r.id}] "${name}"(下次启动进索引)。`;
49
+ return `已存在同名记忆 [${r.exists}]。改用 memory_update(id="${r.exists}", …) 更新,或换一个 name。`;
50
+ },
51
+ };
@@ -0,0 +1,42 @@
1
+ import { searchEntries } from '../../memory/store.js';
2
+ // ---------- memory_search ----------
3
+ // 关键词搜记忆正文(多词子串匹配,name 权重最高)。命中即 bump recallCount(遗忘衰减依据)。
4
+ // 结果走 capToolResultForHistory 的放宽上限(同 use_skill,保正文完整)。
5
+ export const memorySearchTool = {
6
+ name: 'memory_search',
7
+ description: 'Search memory entries by keyword (multi-word substring match, ranked by relevance), returning full body of matching entries. A hit bumps recall count (affects forgetting decay).',
8
+ parameters: {
9
+ type: 'object',
10
+ properties: {
11
+ query: { type: 'string', description: 'Keywords (words in id or name work best)' },
12
+ type: {
13
+ type: 'string',
14
+ enum: ['decision', 'fact', 'pitfall', 'reference', 'feedback'],
15
+ },
16
+ status: {
17
+ type: 'string',
18
+ enum: ['active', 'superseded', 'archived', 'any'],
19
+ description: 'Default active (only search live entries)',
20
+ },
21
+ limit: { type: 'integer', description: 'Number of entries to return, default 5, max 20' },
22
+ },
23
+ required: ['query'],
24
+ },
25
+ async execute(args) {
26
+ const query = String(args.query ?? '').trim();
27
+ if (!query)
28
+ return '错误:缺少 query。';
29
+ const r = searchEntries(query, {
30
+ type: typeof args.type === 'string' ? args.type : undefined,
31
+ status: typeof args.status === 'string'
32
+ ? args.status
33
+ : undefined,
34
+ limit: typeof args.limit === 'number' ? args.limit : undefined,
35
+ });
36
+ if (r.length === 0)
37
+ return `(无匹配记忆:query="${query}")`;
38
+ return r
39
+ .map((e) => `# [${e.id}] ${e.name} (${e.type}, recalled ${e.recallCount})\nsummary: ${e.summary}\n\n${e.body}`)
40
+ .join('\n\n---\n\n');
41
+ },
42
+ };
@@ -0,0 +1,41 @@
1
+ import { updateEntry } from '../../memory/store.js';
2
+ // ---------- memory_update ----------
3
+ // 原地改一条记忆(id 不变)。反思的弱意义:干活时发现事实变了/过时即纠正。
4
+ export const memoryUpdateTool = {
5
+ name: 'memory_update',
6
+ description: 'Update a memory entry in place (id unchanged). Use when facts changed / correcting outdated info / updating summary or body / toggling pinned. Get id from memory_list or memory_search.',
7
+ parameters: {
8
+ type: 'object',
9
+ properties: {
10
+ id: { type: 'string' },
11
+ summary: { type: 'string', description: 'New summary (only pass fields to change)' },
12
+ body: { type: 'string', description: 'New body (only pass fields to change)' },
13
+ name: { type: 'string', description: 'New title (id does not change with name)' },
14
+ pinned: { type: 'boolean', description: 'Toggle pin status (true pins and exempts decay / false unpins)' },
15
+ reason: { type: 'string', description: 'Update reason (recorded in lastUpdateReason for traceability)' },
16
+ },
17
+ required: ['id'],
18
+ },
19
+ async execute(args) {
20
+ const id = String(args.id ?? '').trim();
21
+ if (!id)
22
+ return '错误:缺少 id。';
23
+ const patch = {};
24
+ if (typeof args.summary === 'string')
25
+ patch.summary = args.summary;
26
+ if (typeof args.body === 'string')
27
+ patch.body = args.body;
28
+ if (typeof args.name === 'string')
29
+ patch.name = args.name;
30
+ if (typeof args.reason === 'string')
31
+ patch.reason = args.reason;
32
+ if (typeof args.pinned === 'boolean')
33
+ patch.pinned = args.pinned;
34
+ if (Object.keys(patch).length === 0)
35
+ return '错误:至少传一个要改的字段(summary/body/name/pinned/reason)。';
36
+ const r = updateEntry(id, patch);
37
+ if (r.ok)
38
+ return `已更新记忆 [${id}]。`;
39
+ return `错误:找不到记忆 id="${id}"。用 memory_list 查可用 id。`;
40
+ },
41
+ };
@@ -4,13 +4,14 @@ import { MAX_FILE_LINES } from '../constants.js';
4
4
  // ---------- read_file ----------
5
5
  export const readFileTool = {
6
6
  name: 'read_file',
7
- description: '读取文件内容,返回带行号的文本。改代码前先读。可选 offset(起始行,1-based,默认1)和 limit(行数,默认2000)。',
7
+ description: 'Read file content, returning text with line numbers. Read before editing code. Optional offset (start line, 1-based, default 1) and limit (number of lines, default 2000).' +
8
+ ' Note: when understanding code architecture/call chains, if a .codegraph/ index exists, prefer the codegraph tool over piecing together via read_file one file at a time.',
8
9
  parameters: {
9
10
  type: 'object',
10
11
  properties: {
11
- path: { type: 'string', description: '文件路径,相对工作目录' },
12
- offset: { type: 'integer', description: '起始行号(1-based),默认1' },
13
- limit: { type: 'integer', description: '最大读取行数,默认2000' },
12
+ path: { type: 'string', description: 'File path, relative to the working directory' },
13
+ offset: { type: 'integer', description: 'Start line (1-based), default 1' },
14
+ limit: { type: 'integer', description: 'Max number of lines to read, default 2000' },
14
15
  },
15
16
  required: ['path'],
16
17
  },