mocode-ai 0.1.4 → 0.1.6

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 (59) hide show
  1. package/README.md +196 -188
  2. package/dist/agent/core.js +309 -0
  3. package/dist/agent/index.js +62 -249
  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 +81 -36
  9. package/dist/context/classifier.js +83 -0
  10. package/dist/context/encoders/index.js +10 -0
  11. package/dist/context/encoders/passthrough.js +23 -0
  12. package/dist/context/index.js +10 -0
  13. package/dist/context/pipeline.js +84 -0
  14. package/dist/context/registry.js +30 -0
  15. package/dist/context/types.js +12 -0
  16. package/dist/index.js +12 -2
  17. package/dist/llm/index.js +11 -9
  18. package/dist/memory/index.js +2 -2
  19. package/dist/memory/reflect.js +11 -11
  20. package/dist/memory/store.js +4 -4
  21. package/dist/repl/index.js +269 -109
  22. package/dist/sandbox/command.js +41 -0
  23. package/dist/sandbox/index.js +5 -0
  24. package/dist/sandbox/jail.js +75 -0
  25. package/dist/sandbox/policy.js +58 -0
  26. package/dist/sandbox/root.js +19 -0
  27. package/dist/session/compact.js +1 -1
  28. package/dist/skills/index.js +2 -2
  29. package/dist/tools/builtins/ask-human.js +7 -7
  30. package/dist/tools/builtins/codegraph.js +113 -0
  31. package/dist/tools/builtins/edit-file.js +3 -3
  32. package/dist/tools/builtins/glob.js +10 -5
  33. package/dist/tools/builtins/grep.js +14 -9
  34. package/dist/tools/builtins/index.js +7 -1
  35. package/dist/tools/builtins/memory-forget.js +2 -2
  36. package/dist/tools/builtins/memory-list.js +2 -2
  37. package/dist/tools/builtins/memory-save.js +7 -7
  38. package/dist/tools/builtins/memory-search.js +4 -4
  39. package/dist/tools/builtins/memory-update.js +6 -6
  40. package/dist/tools/builtins/read-file.js +5 -4
  41. package/dist/tools/builtins/run-command.js +47 -8
  42. package/dist/tools/builtins/switch-mode.js +46 -0
  43. package/dist/tools/builtins/task.js +62 -0
  44. package/dist/tools/builtins/use-skill.js +2 -2
  45. package/dist/tools/builtins/web-fetch.js +16 -3
  46. package/dist/tools/builtins/web-search.js +6 -6
  47. package/dist/tools/builtins/write-file.js +3 -3
  48. package/dist/tools/constants.js +16 -0
  49. package/dist/tools/registry.js +18 -4
  50. package/dist/ui/content.js +25 -17
  51. package/dist/ui/diff.js +28 -26
  52. package/dist/ui/intervention.js +24 -3
  53. package/dist/ui/layout.js +295 -81
  54. package/dist/ui/markdown.js +607 -0
  55. package/dist/ui/mouse.js +93 -0
  56. package/dist/ui/prompt.js +259 -5
  57. package/dist/ui/render.js +1 -1
  58. package/dist/ui/theme.js +146 -13
  59. package/package.json +2 -2
@@ -0,0 +1,75 @@
1
+ // 路径牢笼原语:把任意输入路径解析到沙箱根之内,拒绝越界(../、绝对路径外圈、符号链接出圈)。
2
+ // 核心:resolve(root, input) → realpath(解软链)→ 前缀校验。
3
+ // 新文件(目标不存在)走「最近存在祖先 realpath 再拼回剩余分量」,同样挡住「祖先是出圈软链」。
4
+ import { realpathSync, existsSync } from 'node:fs';
5
+ import { resolve, relative, isAbsolute, dirname, basename, join } from 'node:path';
6
+ import { getSandboxRoot } from './root.js';
7
+ const isWin = process.platform === 'win32';
8
+ /** 当前沙箱根(绝对),未初始化回退 process.cwd()。 */
9
+ function root() {
10
+ return resolve(getSandboxRoot() ?? process.cwd());
11
+ }
12
+ /**
13
+ * abs 是否在沙箱根内(含 == 根)。win32 下两边 toLowerCase 再比(node 的 path.relative 不做
14
+ * 大小写无关比较,盘符大小写差异会误判出圈)。**不做 realpath**——用于 glob/grep 结果后置过滤;
15
+ * 越界软链的拦挡靠 jailResolve 的 realpath。
16
+ */
17
+ export function isInsideRoot(abs) {
18
+ const r = root();
19
+ // resolve 相对 r(sandbox root):glob/grep 结果相对 sandbox root,需对齐;
20
+ // 传绝对路径时 resolve(r, abs) 返 abs 不变,兼容 jailResolve 的 realpath 结果。
21
+ const a = isWin ? resolve(r, abs).toLowerCase() : resolve(r, abs);
22
+ const rr = isWin ? r.toLowerCase() : r;
23
+ const rel = relative(rr, a);
24
+ return !rel.startsWith('..') && !isAbsolute(rel);
25
+ }
26
+ /**
27
+ * 把输入路径解析为沙箱根内的绝对路径;越界(../、绝对外圈、符号链接出圈)抛错。
28
+ * 供 enforceSandbox 重写 args.path(读/写/改)、readDiffContext 预读旧内容用。
29
+ * 同步:realpathSync 仅做 fs 元数据查询(微秒级),与既有 readFileSync/existsSync 用法一致。
30
+ */
31
+ export function jailResolve(input) {
32
+ const r = root();
33
+ let abs = resolve(r, input);
34
+ try {
35
+ abs = realpathSync(abs);
36
+ }
37
+ catch {
38
+ // 目标或中间段不存在(写新文件 / 路径中间段未建):realpath 最近存在祖先,拼回剩余分量。
39
+ let dir = dirname(abs);
40
+ const rest = [basename(abs)];
41
+ while (!existsSync(dir)) {
42
+ const parent = dirname(dir);
43
+ if (parent === dir)
44
+ break; // 已到 FS 根(C:\ 或 /),无法再上
45
+ rest.unshift(basename(dir));
46
+ dir = parent;
47
+ }
48
+ try {
49
+ abs = join(realpathSync(dir), ...rest);
50
+ }
51
+ catch {
52
+ // 连祖先都 realpath 失败(不该发生,除非 root 本身不可达):用 resolve 值,交由包含校验兜底
53
+ abs = resolve(r, input);
54
+ }
55
+ }
56
+ if (!isInsideRoot(abs)) {
57
+ throw new Error(`路径越界,已被沙箱拒绝: ${input}`);
58
+ }
59
+ return abs;
60
+ }
61
+ /**
62
+ * glob pattern 校验:拒绝对路径(平台相关的 isAbsolute)与含 `..` 段的 pattern。
63
+ * 返 null = 通过;返 string = 拒绝原因。形如 *.ts 的正常 pattern 放行。
64
+ */
65
+ export function jailGlobPattern(pattern) {
66
+ const p = String(pattern ?? '').trim();
67
+ if (!p)
68
+ return '空 pattern';
69
+ if (isAbsolute(p))
70
+ return `不得为绝对路径: ${pattern}`;
71
+ const segs = p.split(/[\\/]/);
72
+ if (segs.includes('..'))
73
+ return `不得含 .. 段: ${pattern}`;
74
+ return null;
75
+ }
@@ -0,0 +1,58 @@
1
+ // 沙箱策略分类集 + 集中执行 enforceSandbox。单一事实源,仿 tools/constants.ts 的
2
+ // PLAN_DISABLED_TOOLS / READ_TOOL_NAMES 风格。
3
+ import { jailResolve, jailGlobPattern } from './jail.js';
4
+ /**
5
+ * 豁免 cwd 牢笼的工具:
6
+ * - memory_*:操作 ~/.mocode 与 <cwd>/.mocode(CLAUDE.md 明确不进回滚、在外圈),本就该在外圈
7
+ * - use_skill:读 ~/.claude/skills、~/.mocode/skills、<cwd>/.mocode/skills,部分在外圈
8
+ * - web_*:跨网络,非文件路径
9
+ * - ask_human / switch_mode:无文件路径
10
+ * - codegraph:只读 cwd 下 .codegraph/ 索引(只读、不写盘)
11
+ * - task:派生子 agent,继承全局 root(子 agent 同进程天然共享 getSandboxRoot)
12
+ */
13
+ export const SANDBOX_EXEMPT_TOOLS = new Set([
14
+ 'memory_save', 'memory_update', 'memory_forget', 'memory_search', 'memory_list',
15
+ 'use_skill',
16
+ 'web_search', 'web_fetch',
17
+ 'ask_human', 'switch_mode',
18
+ 'codegraph',
19
+ 'task',
20
+ ]);
21
+ /**
22
+ * 路径类工具:enforceSandbox 集中把 args.path 重写为牢内绝对路径(默认安全;工具内
23
+ * resolve(absolutePath) 原样返回)。**新加带 path 参数的工具须列入此集**,否则不会被牢笼挡。
24
+ */
25
+ export const SANDBOX_PATH_TOOLS = new Set(['read_file', 'write_file', 'edit_file']);
26
+ /**
27
+ * 工具执行前的沙箱校验。返 string = 拒绝(直接喂 LLM,不执行);返 null = 放行(可能已重写 args.path)。
28
+ * **不抛**——契约对齐「调度器永不抛错、永远返回字符串」(tools/registry.ts executeTool)。
29
+ * run_command 不在此处理(cwd / env / denylist 在其站点)。
30
+ */
31
+ export function enforceSandbox(name, args) {
32
+ if (SANDBOX_EXEMPT_TOOLS.has(name))
33
+ return null;
34
+ if (SANDBOX_PATH_TOOLS.has(name)) {
35
+ const p = args.path;
36
+ if (typeof p === 'string' && p) {
37
+ try {
38
+ args.path = jailResolve(p);
39
+ }
40
+ catch (e) {
41
+ const why = e instanceof Error ? e.message : String(e);
42
+ return `错误:路径越界,已被沙箱拒绝: ${p} (${why})`;
43
+ }
44
+ }
45
+ return null;
46
+ }
47
+ if (name === 'glob') {
48
+ const pat = String(args.pattern ?? '');
49
+ const err = jailGlobPattern(pat);
50
+ return err ? `错误:glob ${err}` : null;
51
+ }
52
+ if (name === 'grep') {
53
+ const pat = String(args.glob ?? '**/*');
54
+ const err = jailGlobPattern(pat);
55
+ return err ? `错误:grep glob ${err}` : null;
56
+ }
57
+ return null;
58
+ }
@@ -0,0 +1,19 @@
1
+ // 共享叶子:沙箱根目录(文件操作边界)。零依赖、不反向引用业务、不落盘。
2
+ // 依赖方向无环:tools/registry → sandbox、tools/builtins/* → sandbox、agent/core → sandbox、repl → sandbox
3
+ // —— 全是「业务 → 叶子」,同 src/agent/mode.ts。
4
+ //
5
+ // 设计:sandboxRoot 是纯边界记录(默认 = process.cwd(),不 chdir),避免与 config.sessionDir 等
6
+ // 模块加载期计算的值产生错位(若 chdir 会令那些值变陈旧)。子 agent 同进程天然继承全局 root
7
+ // (未来 agents/ 做 worktree 隔离时再改为沿 opts 透传,照 signal 同形链路,7 处改动)。
8
+ let currentRoot = null;
9
+ /** 当前沙箱根(绝对路径)。未初始化返 null,调用方 ?? process.cwd() 兜底(防御)。 */
10
+ export function getSandboxRoot() {
11
+ return currentRoot;
12
+ }
13
+ /** 设置沙箱根。repl startRepl 启动时调一次(默认 process.cwd();--sandbox-root / SANDBOX_ROOT 可覆盖)。
14
+ * 返回之前的值,供未来 save/restore(子 agent worktree 隔离)用。 */
15
+ export function setSandboxRoot(root) {
16
+ const prev = currentRoot;
17
+ currentRoot = root;
18
+ return prev;
19
+ }
@@ -147,7 +147,7 @@ async function defaultSummarize(older, focus) {
147
147
  }
148
148
  const sysMsg = {
149
149
  role: 'system',
150
- 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.',
151
151
  };
152
152
  const userMsg = {
153
153
  role: 'user',
@@ -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
  }
@@ -3,26 +3,26 @@ import { promptIntervention } from '../../ui/intervention.js';
3
3
  export const askHumanTool = {
4
4
  name: 'ask_human',
5
5
  description: [
6
- '当你在执行任务中遇到需要人类做决策的岔路时调用此工具,在终端弹出问题面板让用户选择。',
7
- '适用:多种实现方案需要用户拍板、不确定用户意图需要澄清、需要用户提供额外信息才能继续。',
8
- '调用后阻塞等待用户响应;用户可挑预设选项,也可选"自定义输入"自由作答,结果作为工具返回值返回。',
9
- '不要在任务明确、可自行决定时频繁调用——会打断用户。options 省略或为空时改为自由文本输入。',
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
10
  ].join(''),
11
11
  parameters: {
12
12
  type: 'object',
13
13
  properties: {
14
14
  question: {
15
15
  type: 'string',
16
- description: '要问用户的问题,简明扼要(显示为面板标题)',
16
+ description: 'The question to ask the user; keep it concise (shown as the panel title)',
17
17
  },
18
18
  options: {
19
19
  type: 'array',
20
20
  items: { type: 'string' },
21
- description: '供用户选择的选项(2~6 个)。可省略——省略或为空时改为自由文本输入',
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
22
  },
23
23
  context: {
24
24
  type: 'string',
25
- description: '问题的背景说明(可选,帮助用户理解为何需要他决策;显示在标题下,可多行)',
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
26
  },
27
27
  },
28
28
  required: ['question'],
@@ -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
  },
@@ -1,24 +1,29 @@
1
1
  import fg from 'fast-glob';
2
2
  import { IGNORE } from '../constants.js';
3
+ import { getSandboxRoot, isInsideRoot } from '../../sandbox/index.js';
3
4
  // ---------- glob ----------
4
5
  export const globTool = {
5
6
  name: 'glob',
6
- description: '按 glob 模式查找文件路径(如 **/*.ts)。返回匹配列表(自动排除 node_modules / .git)。',
7
+ description: 'Find file paths matching a glob pattern (e.g. **/*.ts). Returns a list of matches (auto-excludes node_modules / .git).' +
8
+ ' 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
9
  parameters: {
8
10
  type: 'object',
9
11
  properties: {
10
- pattern: { type: 'string', description: 'glob 模式,如 **/*.ts 或 src/**/*.json' },
12
+ pattern: { type: 'string', description: 'Glob pattern, e.g. **/*.ts or src/**/*.json' },
11
13
  },
12
14
  required: ['pattern'],
13
15
  },
14
16
  async execute(args) {
15
17
  const pattern = String(args.pattern);
16
- const files = await fg(pattern, {
17
- cwd: process.cwd(),
18
+ const cwd = getSandboxRoot() ?? process.cwd();
19
+ const files = (await fg(pattern, {
20
+ cwd,
18
21
  onlyFiles: true,
19
22
  dot: true,
20
23
  ignore: IGNORE,
21
- });
24
+ followSymbolicLinks: false, // 不跟随软链目录,防经软链列出牢外文件
25
+ throwErrorOnBrokenSymbolicLink: false,
26
+ })).filter((f) => isInsideRoot(f)); // 后置兜底:仅留牢内
22
27
  if (files.length === 0)
23
28
  return '无匹配文件';
24
29
  const shown = files.slice(0, 200);
@@ -1,16 +1,17 @@
1
1
  import { readFile } from 'node:fs/promises';
2
- import { resolve } from 'node:path';
3
2
  import fg from 'fast-glob';
4
3
  import { MAX_RESULTS, IGNORE } from '../constants.js';
4
+ import { getSandboxRoot, isInsideRoot, jailResolve } from '../../sandbox/index.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
  },
@@ -24,12 +25,15 @@ export const grepTool = {
24
25
  catch (e) {
25
26
  return `错误:非法正则 ${pattern}: ${e instanceof Error ? e.message : String(e)}`;
26
27
  }
27
- const files = await fg(g, {
28
- cwd: process.cwd(),
28
+ const cwd = getSandboxRoot() ?? process.cwd();
29
+ const files = (await fg(g, {
30
+ cwd,
29
31
  onlyFiles: true,
30
32
  dot: true,
31
33
  ignore: IGNORE,
32
- });
34
+ followSymbolicLinks: false, // 不跟随软链目录,防经软链扫到牢外文件
35
+ throwErrorOnBrokenSymbolicLink: false,
36
+ })).filter((f) => isInsideRoot(f)); // 后置兜底:仅留牢内
33
37
  const results = [];
34
38
  let scanned = 0;
35
39
  for (const f of files) {
@@ -37,10 +41,11 @@ export const grepTool = {
37
41
  break;
38
42
  let content;
39
43
  try {
40
- content = await readFile(resolve(f), 'utf8');
44
+ // jailResolve:realpath 化,防「牢内文件软链→牢外」的内容泄露;越界/不可读均 catch 跳过
45
+ content = await readFile(jailResolve(f), 'utf8');
41
46
  }
42
47
  catch {
43
- continue; // 跳过无法读的文件(二进制/权限)
48
+ continue; // 跳过无法读的文件(二进制/权限/沙箱越界)
44
49
  }
45
50
  scanned++;
46
51
  const lines = content.split(/\r?\n/);
@@ -8,14 +8,17 @@ import { webSearchTool } from './web-search.js';
8
8
  import { webFetchTool } from './web-fetch.js';
9
9
  import { useSkillTool } from './use-skill.js';
10
10
  import { askHumanTool } from './ask-human.js';
11
+ import { codegraphTool } from './codegraph.js';
12
+ import { switchModeTool } from './switch-mode.js';
11
13
  import { memorySaveTool } from './memory-save.js';
12
14
  import { memorySearchTool } from './memory-search.js';
13
15
  import { memoryListTool } from './memory-list.js';
14
16
  import { memoryUpdateTool } from './memory-update.js';
15
17
  import { memoryForgetTool } from './memory-forget.js';
18
+ import { taskTool } from './task.js';
16
19
  /**
17
20
  * 所有内置工具,按注册顺序排列。
18
- * 加新工具:在本目录新建 `xxx.ts` 导出一个 Tool,再在下面数组里加一行。
21
+ * 加新工具:在本目录新建 `xxx.ts` 导出一个 Tool,再在下面数组里加一行。无需改 agent / llm。
19
22
  */
20
23
  export const builtinTools = [
21
24
  readFileTool,
@@ -24,13 +27,16 @@ export const builtinTools = [
24
27
  runCommandTool,
25
28
  globTool,
26
29
  grepTool,
30
+ codegraphTool,
27
31
  webSearchTool,
28
32
  webFetchTool,
29
33
  useSkillTool,
30
34
  askHumanTool,
35
+ switchModeTool, // plan↔auto 自切(两模式都可见,不进 PLAN_DISABLED_TOOLS;副作用控制工具→串行分支)
31
36
  memorySaveTool,
32
37
  memorySearchTool,
33
38
  memoryListTool,
34
39
  memoryUpdateTool,
35
40
  memoryForgetTool,
41
+ taskTool, // 派生子 agent(独立 history + 可受限工具集);plan 模式禁用(见 PLAN_DISABLED_TOOLS)
36
42
  ];
@@ -3,7 +3,7 @@ import { forgetEntry } from '../../memory/store.js';
3
3
  // 遗忘:默认归档(archived,从索引/默认搜索隐藏,可复活);mode=delete 硬删。pinned 拒删。
4
4
  export const memoryForgetTool = {
5
5
  name: 'memory_forget',
6
- description: '遗忘一条记忆:默认归档(archived,从索引/默认搜索隐藏,可经 memory_update 复活);mode=delete 硬删。pinned 条目拒删(先 memory_update pinned=false 解钉)。',
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
7
  parameters: {
8
8
  type: 'object',
9
9
  properties: {
@@ -11,7 +11,7 @@ export const memoryForgetTool = {
11
11
  mode: {
12
12
  type: 'string',
13
13
  enum: ['archive', 'delete'],
14
- description: '默认 archive',
14
+ description: 'Default archive',
15
15
  },
16
16
  },
17
17
  required: ['id'],
@@ -3,7 +3,7 @@ import { listEntries } from '../../memory/store.js';
3
3
  // 列索引(id/name/summary,无正文、不 bump recall)。用于浏览有哪些、拿 id 再 memory_search 取正文。
4
4
  export const memoryListTool = {
5
5
  name: 'memory_list',
6
- description: '列出记忆索引(id/name/summary,不含正文)。默认列 active。拿 id 再 memory_search 取正文。',
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
7
  parameters: {
8
8
  type: 'object',
9
9
  properties: {
@@ -14,7 +14,7 @@ export const memoryListTool = {
14
14
  status: {
15
15
  type: 'string',
16
16
  enum: ['active', 'superseded', 'archived', 'any'],
17
- description: '默认 active',
17
+ description: 'Default active',
18
18
  },
19
19
  },
20
20
  },
@@ -4,23 +4,23 @@ import { saveEntry } from '../../memory/store.js';
4
4
  // 撞库(name→id 已存在)拒绝,引导用 memory_update。
5
5
  export const memorySaveTool = {
6
6
  name: 'memory_save',
7
- description: '保存一条长期记忆(跨会话)。只存非显然、长期有用的事实/决策/坑。启动时标题进索引,详情按需 memory_search 取。',
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
8
  parameters: {
9
9
  type: 'object',
10
10
  properties: {
11
- name: { type: 'string', description: '简短唯一标题(转成 id,撞库会拒绝)' },
12
- summary: { type: 'string', description: '一句话摘要(进索引,尽量短)' },
13
- body: { type: 'string', description: '完整内容(细节/上下文/证据)' },
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
14
  type: {
15
15
  type: 'string',
16
16
  enum: ['decision', 'fact', 'pitfall', 'reference', 'feedback'],
17
- description: '分类,默认 fact',
17
+ description: 'Category, default fact',
18
18
  },
19
- pinned: { type: 'boolean', description: '钉住(豁免自动遗忘衰减),默认 false' },
19
+ pinned: { type: 'boolean', description: 'Pin (exempt from auto-forgetting decay), default false' },
20
20
  scope: {
21
21
  type: 'string',
22
22
  enum: ['project', 'global'],
23
- description: '存到项目级(<cwd>/.mocode/)还是全局(~/.mocode/),默认 project',
23
+ description: 'Store at project level (<cwd>/.mocode/) or global (~/.mocode/), default project',
24
24
  },
25
25
  },
26
26
  required: ['name', 'summary', 'body'],
@@ -4,11 +4,11 @@ import { searchEntries } from '../../memory/store.js';
4
4
  // 结果走 capToolResultForHistory 的放宽上限(同 use_skill,保正文完整)。
5
5
  export const memorySearchTool = {
6
6
  name: 'memory_search',
7
- description: '按关键词搜索记忆正文(多词子串匹配,按相关度排),返回匹配条目全文。命中记一次召回(影响遗忘衰减)。',
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
8
  parameters: {
9
9
  type: 'object',
10
10
  properties: {
11
- query: { type: 'string', description: '关键词(id 或 name 里的词效果最好)' },
11
+ query: { type: 'string', description: 'Keywords (words in id or name work best)' },
12
12
  type: {
13
13
  type: 'string',
14
14
  enum: ['decision', 'fact', 'pitfall', 'reference', 'feedback'],
@@ -16,9 +16,9 @@ export const memorySearchTool = {
16
16
  status: {
17
17
  type: 'string',
18
18
  enum: ['active', 'superseded', 'archived', 'any'],
19
- description: '默认 active(只搜活的)',
19
+ description: 'Default active (only search live entries)',
20
20
  },
21
- limit: { type: 'integer', description: '返回条数,默认 5,上限 20' },
21
+ limit: { type: 'integer', description: 'Number of entries to return, default 5, max 20' },
22
22
  },
23
23
  required: ['query'],
24
24
  },
@@ -3,16 +3,16 @@ import { updateEntry } from '../../memory/store.js';
3
3
  // 原地改一条记忆(id 不变)。反思的弱意义:干活时发现事实变了/过时即纠正。
4
4
  export const memoryUpdateTool = {
5
5
  name: 'memory_update',
6
- description: '更新一条记忆(id 不变)。用于事实变了/纠正过时/补 summary 或 body/切换 pinned。id 从 memory_list 或 memory_search 拿。',
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
7
  parameters: {
8
8
  type: 'object',
9
9
  properties: {
10
10
  id: { type: 'string' },
11
- summary: { type: 'string', description: '新摘要(只传要改的字段)' },
12
- body: { type: 'string', description: '新正文(只传要改的字段)' },
13
- name: { type: 'string', description: '新标题(id 不随 name 变)' },
14
- pinned: { type: 'boolean', description: '切换钉住状态(true 钉住豁免衰减 / false 解钉)' },
15
- reason: { type: 'string', description: '更新理由(记入 lastUpdateReason,便于追溯)' },
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
16
  },
17
17
  required: ['id'],
18
18
  },
@@ -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
  },