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
@@ -28,6 +28,8 @@ function loadEnvFiles() {
28
28
  process.env[k] = v;
29
29
  }
30
30
  }
31
+ // 在 loadEnvFiles 回填前捕获:MOCODE_THEME 是否由 shell 设置(决定 /theme 写文件是否下次启动生效)。
32
+ const themeFromShell = process.env.MOCODE_THEME !== undefined;
31
33
  loadEnvFiles();
32
34
  function requireEnv(key) {
33
35
  const v = process.env[key];
@@ -37,42 +39,79 @@ function requireEnv(key) {
37
39
  }
38
40
  return v;
39
41
  }
40
- const SYSTEM_PROMPT = `你是 mocode,一个终端编码 agent。你以"思考 → 调用工具 → 观察结果 → 再思考"的循环完成编程任务,直到问题解决。面向中文用户,回复用中文。
41
-
42
- ## 工作流
43
- - 先理解再动手:不确定需求或代码现状时,先 read_file / grep / glob 探索,不凭空假设。
44
- - 小步推进:任务拆成可验证的子步骤,每步动手前想清楚改什么、为什么。
45
- - 改完即验证:用 run_command 跑 typecheck / 测试 / 构建确认有效,未验证不声称完成。
46
-
47
- ## 工具准则
48
- - 参数与用法见各工具自带说明;这里只讲选择策略与易踩坑。
49
- - 改代码前先 read_file 确认实际内容(带行号),不凭记忆猜。
50
- - 局部改用 edit_file:old_string 须唯一且精确匹配(含缩进/换行),多带上下文行确保唯一;新建或整体重写用 write_file。
51
- - 找路径用 glob,找内容用 grep;不要用 run_command 拼 cat / sed / find / grep。
52
- - 若当前目录存在 .codegraph/(已建代码索引),理解/定位代码、查调用链、看改动影响面时必须先 codegraph 再动手:run_command 跑 codegraph explore "<符号或问题>"(一次拿相关符号源码 + 调用路径)或 codegraph node <符号或文件>(单符号源码 + 调用者)。不要逐文件 glob/read_file/grep 去拼凑理解——那是 codegraph 已替你做完的事。仅当 codegraph 找不到、未索引、要看刚改的最新内容、或改单个已知小文件时才用 read_file/grep/glob。详见 use_skill codegraph。
53
- - run_command 按平台执行(Win 用 cmd、其他用 bash);有副作用的命令(删文件、装包、git push、重置等)执行前先简述意图。
54
- - 需要训练数据之外的最新信息(新版本、新闻、实时数据、最新 API)时用 web_search 联网搜索,不要凭记忆答可能过时的内容。
55
- - 要读取某个具体 URL 的内容(搜索结果里的链接、用户给的 URL)时用 web_fetch 抓取;它只抓静态 HTML,JS 渲染页面拿不到正文时改用 web_search(其结果自带清洗后的正文)。
56
- - 遇到需要用户决策的岔路(多种实现方案、不确定用户意图、需要额外信息才能继续),调 ask_human 列出选项让用户选(用户也可选"自定义输入"自由作答)。不要在任务明确、能自行决定时频繁打扰用户;用户取消后换方案或基于已有信息推进,不原地重复问。
57
-
58
- ## 失败处理
59
- - 工具以字符串返回错误(edit_file 未匹配或不唯一、run_command 非零退出码等)。先分析根因,调整后重试,不要原样重发同一条调用。
60
- - 命令报错时把真实输出读进去再判断,别跳过。
61
-
62
- ## 安全与边界
63
- - 不可逆或外向操作(删除、覆盖既有文件、推送、请求外部服务)执行前向用户确认,除非已获明确授权。
64
- - 只在授权范围内操作;不确定就问,别猜。
65
-
66
- ## 记忆(跨会话长期事实)
67
- - 系统提示已注入「记忆索引」(仅 id/标题/摘要)。需要某条正文时调 memory_search(传 id 或关键词)取;memory_list 看全部索引。
68
- - 遇到非显然、跨会话有用的事实/决策/坑(架构约定、易踩坑、用户偏好、已做决策),用 memory_save 存——只存长期稳定项,不存当前 bug / 临时文件 / 未决 TODO。
69
- - 发现已存记忆过时或与新事实矛盾,用 memory_update(id, …) 原地纠正(别新建重复条);明确失效的用 memory_forget(id) 归档。
70
- - 存前先 memory_search 看是否已有同类条,避免重复。宁可少记,不记正确废话。
71
- - 后台反思 pass 会定期从会话里挖掘并整理记忆(无需你手动),但你主动存的关键事实更可靠。
72
-
73
- ## 终止与汇报
74
- - 无需更多工具时立即停止,直接给结论。
75
- - 如实汇报:成功说成功,失败说卡在哪,跳过的也要说。引用代码用 "path:行号" 格式(如 src/index.ts:42)。保持简洁。`;
42
+ const PLATFORM_NOTE = (() => {
43
+ if (process.platform === 'win32') {
44
+ return `## Environment (Windows)
45
+ - You are on Windows; run_command runs commands via cmd.exe (/c). Unix shell builtins are NOT available here.
46
+ - Windows equivalents: which→where, cat→type, ls→dir, rm→del/rd, cp→copy, mv→move. cmd.exe uses %VAR% (not $VAR); pipes (|) and redirects (>, >>) work, but no $(...) command substitution or backticks.
47
+ - head/tail/find/grep/sed have no cmd.exe equivalent — use the dedicated tools (read_file for head/tail, glob for find, grep for grep), or invoke PowerShell via run_command if you need more.
48
+ - Prefer the dedicated tools (read_file/glob/grep) over shell equivalents — they're cross-platform and already wired in.`;
49
+ }
50
+ if (process.platform === 'darwin') {
51
+ return `## Environment (macOS)
52
+ - You are on macOS; run_command runs via bash -c (user default shell may be zsh). BSD coreutils, not GNU.
53
+ - Pitfalls: sed -i needs an empty backup-ext arg (sed -i '' 's/x/y/' file); grep -P unavailable (use grep -E or the grep tool); find/readlink/date are BSD variants; readlink -f unsupported (use realpath, or greadlink -f if GNU coreutils installed via brew).
54
+ - Prefer the dedicated tools (read_file/glob/grep) over shell equivalents — they sidestep BSD/GNU differences.`;
55
+ }
56
+ return `## Environment (Linux/Unix)
57
+ - You are on ${process.platform}; run_command runs via bash -c. GNU coreutils — standard POSIX/GNU shell syntax is safe.
58
+ - Still prefer the dedicated tools (read_file/glob/grep) over hand-rolled shell where they fit — they avoid quoting pitfalls and are already wired in.`;
59
+ })();
60
+ const SYSTEM_PROMPT = `You are mocode, a terminal coding agent. You complete programming tasks through a "think → call tool → observe result → think again" loop until the problem is solved. Reply to the user in Chinese.
61
+
62
+ ${PLATFORM_NOTE}
63
+
64
+ ## Workflow
65
+ - Understand before acting: when unsure about requirements or code state, explore first; don't assume.
66
+ - Small steps: break tasks into verifiable sub-steps. Before each step, think clearly about what to change and why.
67
+ - Verify after change: run typecheck / tests / build via run_command to confirm it works. Never claim done without verification.
68
+
69
+ ## Tool Guidelines
70
+ - See each tool's own description for parameters and usage; this section covers selection strategy and pitfalls only.
71
+ - **Prefer codegraph for code exploration**: when understanding/locating code, tracing call chains, or assessing impact of changes, if a .codegraph/ index exists, use the codegraph tool first (explore to query by question, node to look up a single symbol) — it returns relevant source + call paths in one shot, more accurate and economical than piecing together via read_file/grep. Fall back to read_file / grep / glob only when codegraph is unavailable (no index), misses, you need to see just-changed content, or you're editing a single known small file. Build the index first with \`codegraph init\` if none exists.
72
+ - Before editing code, read_file to confirm actual content (with line numbers); don't guess from memory.
73
+ - For local edits use edit_file: old_string must be unique and match exactly (including indentation/newlines); include surrounding context lines to ensure uniqueness. Use write_file for new files or full rewrites.
74
+ - Use glob to find file paths, grep to search content; don't use run_command to pipe cat / sed / find / grep.
75
+ - run_command runs per platform (cmd on Windows, bash elsewhere); state intent before running commands with side effects (deleting files, installing packages, git push, resets, etc.).
76
+ - Use web_search for information beyond training data (new versions, news, real-time data, latest APIs); don't answer potentially outdated info from memory.
77
+ - Use web_fetch to read a specific URL (a link from search results, or a URL given by the user); it only fetches static HTML — if a JS-rendered page yields no body, switch to web_search (its results include cleaned body text).
78
+ - Call ask_human when you hit a decision point requiring user input (multiple implementation approaches, unclear intent, or needing extra info to proceed) — list options for the user to pick (they can also choose "custom input" to answer freely). Don't call it frequently when the task is clear and you can decide yourself; if the user cancels, switch approach or proceed with available info — don't re-ask the same question.
79
+
80
+ ## Failure Handling
81
+ - Tools return errors as strings (edit_file no match or non-unique, run_command non-zero exit, etc.). Analyze the root cause, adjust, then retry — don't resend the same call verbatim.
82
+ - When a command errors, read the actual output before judging; don't skip it.
83
+
84
+ ## Safety & Boundaries
85
+ - Confirm with the user before irreversible or outward-facing operations (delete, overwrite existing files, push, request external services), unless explicitly authorized.
86
+ - Operate only within authorized scope; when unsure, ask — don't guess.
87
+
88
+ ## Memory (cross-session long-term facts)
89
+ - A "memory index" (id/title/summary only) is injected into the system prompt. Retrieve full body via memory_search (pass id or keyword); use memory_list to see the entire index.
90
+ - Store non-obvious, cross-session-useful facts/decisions/pitfalls (architecture conventions, gotchas, user preferences, decisions made) with memory_save — only long-term stable items, not current bugs / temp files / undecided TODOs.
91
+ - If an existing memory is outdated or contradicts new facts, correct it in-place with memory_update(id, …) (don't create a duplicate); archive clearly-stale ones with memory_forget(id).
92
+ - Before saving, memory_search to check for an existing similar entry to avoid duplicates. Better to store less than to store trivially correct information.
93
+ - A background reflection pass periodically mines and organizes memories from the session (no manual action needed), but key facts you proactively save are more reliable.
94
+
95
+ ## Plan vs Auto modes
96
+ - Default is AUTO mode: you research and execute with all tools (read/edit/run_command/memory/web/skills).
97
+ - For complex or multi-step tasks, the user may switch to PLAN mode (Shift+Tab): your editing/command/memory-write tools are then removed from your tool list, and you must research with read-only tools only and produce a step-by-step plan (no execution). On approval the session returns to auto mode to execute the plan.
98
+
99
+ ## Termination & Reporting
100
+ - Stop immediately when no more tools are needed; give conclusions directly.
101
+ - Report honestly: say success when successful, say where you're stuck when failing, and mention anything skipped. Reference code in "path:line" format (e.g., src/index.ts:42). Keep it concise.`;
102
+ /**
103
+ * plan 模式追加到系统提示末尾的指令(切到 plan 模式时由 repl 拼进 history[0])。
104
+ * 与 SYSTEM_PROMPT 同语种(英文),指示:只读探查、产出步骤化计划、不执行、审批后回 auto。
105
+ */
106
+ export const PLAN_MODE_SUFFIX = `
107
+
108
+ ## ⛯ PLAN MODE (active now)
109
+ You are in PLAN mode: investigate and design only — do NOT execute or change anything.
110
+ - Your editing / command / memory-write tools (write_file, edit_file, run_command, memory_save, memory_update, memory_forget) have been REMOVED from your tool list. Use only the read-only tools available to you (read_file, glob, grep, codegraph, web_search, web_fetch, use_skill, ask_human, memory_search, memory_list) to investigate.
111
+ - Research thoroughly: locate the relevant code, trace call paths, and understand existing patterns and conventions before designing. Prefer codegraph when a .codegraph/ index exists.
112
+ - Then produce a clear, actionable implementation plan: files to change (with paths), what to change in each and why, the ordered steps, edge cases to handle, and how to verify (typecheck / tests / build). Be specific enough to execute against.
113
+ - Present the plan as your final reply and STOP, unless the user explicitly asked you to "plan first then execute" / "先 plan 再 auto" / autonomous execution: in that case, after presenting the plan, call the switch_mode tool with mode="auto" to switch back to auto mode WITHIN THE SAME TURN and continue implementing the plan yourself (your write/edit/command/memory-write tools become available again immediately). The user will see no approval prompt because you self-switched.
114
+ - If the user entered plan mode manually (via /plan or Shift+Tab) for a safety review and did NOT ask for autonomous execution, do NOT call switch_mode — present the plan and STOP; the user will approve via a prompt and execution happens in a follow-up turn.`;
76
115
  export const config = {
77
116
  baseURL: requireEnv('LLM_BASE_URL'),
78
117
  apiKey: requireEnv('LLM_API_KEY'),
@@ -83,9 +122,15 @@ export const config = {
83
122
  compactThreshold: Number(process.env.COMPACT_THRESHOLD) || 0.85,
84
123
  includeUsage: process.env.LLM_STREAM_USAGE !== 'false',
85
124
  autoCompact: process.env.AUTO_COMPACT !== 'false',
125
+ contextOptimize: process.env.MOCODE_CONTEXT_OPTIMIZE !== 'false',
86
126
  autoReflect: process.env.AUTO_REFLECT !== 'false',
87
127
  reflectEveryN: Number(process.env.REFLECT_EVERY_N) || 5,
128
+ maxSteps: Number(process.env.MAX_STEPS) || 200,
129
+ subAgentMaxSteps: Number(process.env.SUB_AGENT_MAX_STEPS) || 50,
88
130
  sessionDir: path.join(process.cwd(), '.mocode', 'sessions'),
89
131
  searchApiKey: process.env.ANYSEARCH_API_KEY,
132
+ sandboxRoot: process.env.SANDBOX_ROOT || undefined,
90
133
  searchBaseUrl: process.env.ANYSEARCH_BASE_URL || 'https://api.anysearch.com',
134
+ theme: process.env.MOCODE_THEME || 'default',
135
+ themeFromShell,
91
136
  };
@@ -0,0 +1,83 @@
1
+ // Context Classifier:据工具名(强先验)+ 输出形状(启发)+ 兜底,选 ContextKind。
2
+ //
3
+ // 三级信号:
4
+ // 1) 名字强先验(BY_NAME 表,覆盖全部 17 内置工具,确定性强)。
5
+ // 2) 形状启发(为 MCP 工具 / 未来工具 / 未登记工具兜底识别)。
6
+ // 3) 兜底 'passthrough'(不认识 = 不动,零行为变化)。
7
+ //
8
+ // 单一事实源风格(仿 tools/constants.ts 的 READ_TOOL_NAMES / PLAN_DISABLED_TOOLS)。
9
+ // 加新工具:在 BY_NAME 加一行;或靠形状启发自动识别。
10
+ /** 工具名 → ContextKind 的强先验表(覆盖全部 17 内置工具)。 */
11
+ const BY_NAME = {
12
+ // tree:路径列表 → 缩进树
13
+ glob: 'tree',
14
+ // search:file:line 分组
15
+ grep: 'search',
16
+ web_search: 'search',
17
+ // graph:CLI dump → 精炼图
18
+ codegraph: 'graph',
19
+ // log:分级 / 折叠 / 尾偏置
20
+ run_command: 'log',
21
+ // code:保行号(edit_file 依赖,最敏感)
22
+ read_file: 'code',
23
+ // table:列对齐
24
+ memory_list: 'table',
25
+ // memory:紧凑卡片
26
+ memory_search: 'memory',
27
+ // doc:去噪音保正文
28
+ web_fetch: 'doc',
29
+ use_skill: 'doc',
30
+ // status:一行状态(identity,不动)
31
+ edit_file: 'status',
32
+ write_file: 'status',
33
+ ask_human: 'status',
34
+ switch_mode: 'status',
35
+ memory_save: 'status',
36
+ memory_update: 'status',
37
+ memory_forget: 'status',
38
+ // summary:子 agent 摘要(轻量)
39
+ task: 'summary',
40
+ };
41
+ /**
42
+ * 形状启发:对未在 BY_NAME 登记的工具输出做模式识别(为 MCP / 未来工具兜底)。
43
+ * 故意保守:识别不准时回落 passthrough(不动),宁可不少省也不可错改。
44
+ */
45
+ function classifyByShape(output) {
46
+ // file:line: content 形(grep 风格)
47
+ if (/^[^\n:]+:\d+:[^\n]*$/m.test(output))
48
+ return 'search';
49
+ // [退出码 N] 前缀(run_command / codegraph 风格)
50
+ if (/^\[退出码 \d+\]/m.test(output))
51
+ return 'log';
52
+ // 路径列表:多行都是含分隔符的相对路径(glob 风格)
53
+ const lines = output.split('\n').filter((l) => l.trim().length > 0);
54
+ if (lines.length >= 3 &&
55
+ lines.every((l) => /^[\w.\-\\/ ]+$/.test(l.trim()) && /[\\/]/.test(l))) {
56
+ return 'tree';
57
+ }
58
+ // JSON 结构化(web_fetch 的 JSON 响应等)→ doc 渲染
59
+ const trimmed = output.trimStart();
60
+ if (trimmed.startsWith('{') || trimmed.startsWith('['))
61
+ return 'doc';
62
+ return 'passthrough';
63
+ }
64
+ /**
65
+ * 判定 ContextKind。
66
+ * - 有 BY_NAME 强先验 → 用之(内置工具确定性强)。
67
+ * - 否则形状启发(MCP / 未来工具)。
68
+ * - 都不中 → passthrough(不动)。
69
+ *
70
+ * @param toolName 工具名
71
+ * @param output 工具原始输出(形状启发用;有 BY_NAME 时不读)
72
+ * @param _args 已解析参数(预留:未来 read_file 的 offset/limit 可影响 code 编码策略;Phase 1 不用)
73
+ */
74
+ export function classify(toolName, output, _args) {
75
+ const byName = BY_NAME[toolName];
76
+ if (byName)
77
+ return byName;
78
+ return classifyByShape(output);
79
+ }
80
+ /** 暴露 BY_NAME 副本供调试 / 未来 /context 展示(只读视图)。 */
81
+ export function knownToolKinds() {
82
+ return { ...BY_NAME };
83
+ }
@@ -0,0 +1,10 @@
1
+ // 内置 encoder 清单。启动期 pipeline 首次调用时经 registerAll 注册到 registry。
2
+ //
3
+ // Phase 1:仅 passthrough(identity)→ 全链路零行为变化(所有 kind 都回落到它)。
4
+ // Phase 2 起逐步加入:tree / search / log / code / table / memory(见各 encoder 文件)。
5
+ //
6
+ // 加 encoder:新建 encoders/xxx.ts 导出 ContextEncoder,在此数组加一行。无需动 agent / llm / core。
7
+ import { passthroughEncoder } from './passthrough.js';
8
+ export const builtinEncoders = [
9
+ passthroughEncoder,
10
+ ];
@@ -0,0 +1,23 @@
1
+ /**
2
+ * 兜底 encoder:identity,原样返回。
3
+ * - classifier 未命中任何 kind(返回 'passthrough')时用。
4
+ * - pipeline 总开关关闭(MOCODE_CONTEXT_OPTIMIZE=false)时,所有 kind 都走它 → 行为与改造前逐字节一致。
5
+ * - Phase 1 阶段 registry 只注册它 → 全链路零行为变化。
6
+ * - 任何 encoder 报错时,pipeline catch 后回落到它(传原 output)。
7
+ *
8
+ * 永不抛错:output 可能是任意字符串(含 ANSI / 多行 / 非法 UTF-8 片段),identity 直接返回,无解析风险。
9
+ */
10
+ export const passthroughEncoder = {
11
+ kind: 'passthrough',
12
+ encode({ output }) {
13
+ return {
14
+ text: output,
15
+ meta: {
16
+ kind: 'passthrough',
17
+ originalLen: output.length,
18
+ encodedLen: output.length,
19
+ note: 'identity (no encoder registered)',
20
+ },
21
+ };
22
+ },
23
+ };
@@ -0,0 +1,10 @@
1
+ // context/ barrel:Context Optimization Pipeline。
2
+ //
3
+ // 单一入口 optimizeToolResult(agent/core.ts pushToolResult 调)接管"工具结果进 LLM 前"的表示。
4
+ // 不调 LLM、不碰 Tool Calling schema / executeTool / tool_call_id 配对 / TUI 渲染
5
+ // (叶子级:仅 stdlib + tools/constants + session/compact 的 capToolResultForHistory 兜底 + config 开关)。
6
+ //
7
+ // 见 CLAUDE.md「Context Optimization Pipeline」节。
8
+ export { optimizeToolResult } from './pipeline.js';
9
+ export { classify, knownToolKinds } from './classifier.js';
10
+ export { registerEncoder, registerAll, getEncoder, registeredKinds, } from './registry.js';
@@ -0,0 +1,84 @@
1
+ // Context Optimization Pipeline 单一入口。
2
+ //
3
+ // 接管"工具结果进 LLM 前"的表示优化(C1 收口,agent/core.ts pushToolResult 调)。
4
+ // 流程:
5
+ // 1) 解析 argsRaw(失败返 null,encoder 据此降级)。
6
+ // 2) classify(name, output, args) → ContextKind。
7
+ // 3) getEncoder(kind) ?? passthrough → encode(保不变量压缩,纯函数)。
8
+ // 4) capToolResultForHistory(name, text) 作末尾长度裁剪兜底(保 head+标记+tail,与改造前一致)。
9
+ //
10
+ // 不抛错:encoder 报错 → catch 回落原 output + capToolResultForHistory(对齐「调度器永不抛错」)。
11
+ // 兜底零行为变化:未注册 encoder / pipeline 关闭 → passthrough identity → 末尾 cap 与改造前逐字节一致。
12
+ //
13
+ // 兼容:不改 Tool Calling JSON schema、不改 executeTool、不改 tool_call_id 配对、不改 TUI 渲染
14
+ // (hooks.onToolResult 用原始 output,本函数只管进 history 的 content)。
15
+ //
16
+ // 依赖方向:context → {tools/constants, session/compact 的 cap, config};叶子,不反向依赖 llm/agent/tools。
17
+ import { classify } from './classifier.js';
18
+ import { getEncoder, registerAll } from './registry.js';
19
+ import { builtinEncoders } from './encoders/index.js';
20
+ import { passthroughEncoder } from './encoders/passthrough.js';
21
+ import { capToolResultForHistory } from '../session/compact.js';
22
+ import { config } from '../config/index.js';
23
+ import { MAX_HISTORY_RESULT, MAX_SKILL_RESULT, MAX_MEMORY_RESULT, } from '../tools/constants.js';
24
+ let booted = false;
25
+ /** 懒注册内置 encoder(首次调用 optimizeToolResult 时触发,避免模块加载期循环 import)。 */
26
+ function boot() {
27
+ if (booted)
28
+ return;
29
+ registerAll(builtinEncoders);
30
+ booted = true;
31
+ }
32
+ /** 解析工具 arguments JSON;非法或空返 null(同 agent/core.ts parseArgs 语义,独立实现避免循环依赖)。 */
33
+ function tryParseArgs(raw) {
34
+ try {
35
+ return raw.trim() ? JSON.parse(raw) : {};
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ }
41
+ /**
42
+ * 按工具名取软目标 budget(字符)。对齐 capToolResultForHistory 的放宽规则:
43
+ * use_skill / memory_search 走放宽上限(指令 / 记忆正文须完整);其余走 MAX_HISTORY_RESULT。
44
+ * 仅作 encoder 软目标;最终裁剪仍由末尾 capToolResultForHistory 兜底,故两处常量偶有漂移不致命。
45
+ */
46
+ function budgetFor(name) {
47
+ if (name === 'use_skill')
48
+ return MAX_SKILL_RESULT;
49
+ if (name === 'memory_search')
50
+ return MAX_MEMORY_RESULT;
51
+ return MAX_HISTORY_RESULT;
52
+ }
53
+ /**
54
+ * 优化工具结果为进 LLM 的 tool 消息 content。
55
+ *
56
+ * @param name 工具名
57
+ * @param output executeTool 的原始返回字符串
58
+ * @param argsRaw 工具 arguments 原始 JSON 字符串(tc.arguments,可空;未传则 args=null)
59
+ * @returns 进 history 的 content 字符串(永不抛错)
60
+ */
61
+ export function optimizeToolResult(name, output, argsRaw) {
62
+ boot();
63
+ // 总开关关闭:完全走老路径,零行为变化(Phase 1 默认 true,但保留紧急回退开关)。
64
+ if (!config.contextOptimize) {
65
+ return capToolResultForHistory(name, output);
66
+ }
67
+ try {
68
+ const args = argsRaw != null ? tryParseArgs(argsRaw) : null;
69
+ const kind = classify(name, output, args);
70
+ const enc = getEncoder(kind) ?? passthroughEncoder;
71
+ const { text } = enc.encode({
72
+ toolName: name,
73
+ output,
74
+ args,
75
+ budget: budgetFor(name),
76
+ });
77
+ // 末尾长度裁剪兜底(同改造前):encoder 已更短则 no-op;use_skill/memory_search 的放宽 cap 由此保留。
78
+ return capToolResultForHistory(name, text);
79
+ }
80
+ catch {
81
+ // encoder 报错(不应发生,纯函数):回落原 output + cap(永不抛错契约)。
82
+ return capToolResultForHistory(name, output);
83
+ }
84
+ }
@@ -0,0 +1,30 @@
1
+ // kind → encoder 注册表。单一事实源(仿 tools/registry.ts 的 tools[] 风格)。
2
+ //
3
+ // 内置 encoder 在 encoders/index.ts 的 builtinEncoders 数组声明,启动期 pipeline 首次调用时
4
+ // 经 registerAll 注册(懒注册,避免循环 import 在模块加载期触发)。
5
+ // MCP 工具(未来)可在并入 tools/registry.ts 时调 registerEncoder 注册私有 encoder(后注册覆盖默认)。
6
+ //
7
+ // 未注册的 kind → getEncoder 返 undefined → pipeline 回落 passthrough(identity),零行为变化。
8
+ const encoders = new Map();
9
+ let registered = false;
10
+ /** 注册一个 encoder(后注册覆盖先注册,允许 MCP 覆盖默认)。返回 encoder 自身供链式。 */
11
+ export function registerEncoder(enc) {
12
+ encoders.set(enc.kind, enc);
13
+ return enc;
14
+ }
15
+ /** 批量注册(启动期 pipeline 调一次)。幂等:重复调忽略。 */
16
+ export function registerAll(list) {
17
+ if (registered)
18
+ return;
19
+ for (const e of list)
20
+ registerEncoder(e);
21
+ registered = true;
22
+ }
23
+ /** 取某 kind 的 encoder;未注册返 undefined(pipeline 回落 passthrough)。 */
24
+ export function getEncoder(kind) {
25
+ return encoders.get(kind);
26
+ }
27
+ /** 调试:列出已注册 kind。 */
28
+ export function registeredKinds() {
29
+ return [...encoders.keys()];
30
+ }
@@ -0,0 +1,12 @@
1
+ // Context Optimization Pipeline 的类型契约。
2
+ //
3
+ // 设计原则(见 CLAUDE.md context/ 子系统):
4
+ // - Tool Calling 的 JSON schema 与 executeTool 不动;本层只接管"工具结果进 LLM 前"的表示。
5
+ // - 不设计统一 DSL,针对不同数据类型各做最优 encoder。
6
+ // - 所有 encoder 是纯函数(无 LLM 调用 / 无 IO / 无副作用),永不抛错(pipeline 层 try/catch,
7
+ // 失败回落原 output + capToolResultForHistory,对齐 tools/registry.ts「调度器永不抛错」契约)。
8
+ // - 兜底 encoder = passthrough(identity):未注册 encoder 时行为与改造前逐字节一致。
9
+ //
10
+ // 依赖方向:context 是叶子(仅 stdlib + tools/constants + session/compact 的 cap + config 开关),
11
+ // 不反向依赖 llm / agent / tools 业务,无环。
12
+ export {};
package/dist/index.js CHANGED
@@ -36,6 +36,16 @@ process.on('unhandledRejection', (e) => {
36
36
  */
37
37
  async function main() {
38
38
  const args = process.argv.slice(2);
39
+ // --sandbox-root <path>:覆盖沙箱根(文件操作边界)。缺值或以 -- 开头报错退出。
40
+ const sr = args.indexOf('--sandbox-root');
41
+ let sandboxRootOverride;
42
+ if (sr !== -1) {
43
+ sandboxRootOverride = args[sr + 1];
44
+ if (!sandboxRootOverride || sandboxRootOverride.startsWith('--')) {
45
+ console.error('[cli] --sandbox-root 需要一个路径参数');
46
+ process.exit(1);
47
+ }
48
+ }
39
49
  // 首跑配置向导:写 ~/.mocode/config。独立模块,不触发 config 校验,故零配置也能跑。
40
50
  if (args[0] === 'config') {
41
51
  const { runConfigWizard } = await import('./commands/config.js');
@@ -66,12 +76,12 @@ async function main() {
66
76
  }
67
77
  const updateNotice = checkAndMaybeUpdate();
68
78
  const { startRepl } = await import('./repl/index.js');
69
- await startRepl(loaded.history, loaded.id, updateNotice);
79
+ await startRepl(loaded.history, loaded.id, updateNotice, sandboxRootOverride);
70
80
  }
71
81
  else {
72
82
  const updateNotice = checkAndMaybeUpdate();
73
83
  const { startRepl } = await import('./repl/index.js');
74
- await startRepl(undefined, undefined, updateNotice);
84
+ await startRepl(undefined, undefined, updateNotice, sandboxRootOverride);
75
85
  }
76
86
  process.exit(0);
77
87
  }
package/dist/llm/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import OpenAI from 'openai';
2
2
  import { config } from '../config/index.js';
3
3
  import { tools } from '../tools/registry.js';
4
+ import { PLAN_DISABLED_TOOLS } from '../tools/constants.js';
4
5
  const client = new OpenAI({
5
6
  baseURL: config.baseURL,
6
7
  apiKey: config.apiKey,
@@ -16,18 +17,23 @@ export const chatTools = tools.map((t) => ({
16
17
  },
17
18
  }));
18
19
  /**
19
- * 流式调一次 LLM:增量回调文本 / 思考,内部累加 tool_calls 片段。
20
- * 思考内容走 delta.reasoning_content(DeepSeek / GLM / Qwen 等推理模型,
21
- * SDK 类型无此字段,用 as any 取;不支持的模型则无思考,只流文本)。
20
+ * plan 模式用的受限工具 schema:剔除写盘 / 命令 / 记忆写入类(PLAN_DISABLED_TOOLS),
21
+ * 模型在 plan 模式下只看得到只读工具 → 调不到会改文件的工具。runAgent 在 plan 模式传给 chat()。
22
+ */
23
+ export const planChatTools = chatTools.filter((t) => !PLAN_DISABLED_TOOLS.has(t.function.name));
24
+ /**
25
+ * 流式调一次 LLM:增量回调文本,内部累加 tool_calls 片段。
22
26
  * tool_calls 跨 chunk 按 index 累加(id / name / arguments 拼接)。
23
27
  * include_usage 时末尾 chunk 携带 usage,先读再 continue(末尾 chunk 无 delta)。
24
28
  */
25
- export async function chat(messages, handlers = {}, signal) {
29
+ export async function chat(messages, handlers = {}, signal,
30
+ /** 覆盖默认工具 schema;plan 模式传 planChatTools(只读子集),缺省=全量 chatTools。 */
31
+ toolsOverride) {
26
32
  // signal 透传给 SDK 第二参(RequestOptions);abort 后 for await 抛错,chat 不 catch,透传 runAgent 处理。
27
33
  const stream = await client.chat.completions.create({
28
34
  model: config.model,
29
35
  messages,
30
- tools: chatTools,
36
+ tools: toolsOverride ?? chatTools,
31
37
  stream: true,
32
38
  ...(config.maxTokens ? { max_tokens: config.maxTokens } : {}),
33
39
  ...(config.includeUsage ? { stream_options: { include_usage: true } } : {}),
@@ -48,10 +54,6 @@ export async function chat(messages, handlers = {}, signal) {
48
54
  const delta = chunk.choices[0]?.delta;
49
55
  if (!delta)
50
56
  continue; // 末尾 usage-only chunk 等无 delta
51
- // 思考内容(非标准字段,SDK 类型无)
52
- const reasoning = delta.reasoning_content ?? delta.reasoning;
53
- if (reasoning)
54
- handlers.onThinking?.(reasoning);
55
57
  if (delta.content) {
56
58
  content += delta.content;
57
59
  hasContent = true;
@@ -38,8 +38,8 @@ export function buildMemorySection() {
38
38
  return [
39
39
  '',
40
40
  '',
41
- '## 项目记忆(MOCODE.md)',
42
- '以下是项目记忆(架构 / 约定 / 命令等跨会话长期事实),据此行动:',
41
+ '## Project Memory (MOCODE.md)',
42
+ 'The following is project memory (architecture / conventions / commands and other cross-session long-term facts). Act accordingly:',
43
43
  mem,
44
44
  ].join('\n');
45
45
  }
@@ -92,17 +92,17 @@ function buildMemorySample() {
92
92
  .join('\n---\n');
93
93
  }
94
94
  const TYPES = 'decision | fact | pitfall | reference | feedback';
95
- const REFLECT_SYS = `你是 mocode 的记忆反思器。审阅近期会话与现有记忆,产出**仅**值得长期记住的更新。
96
- 严格输出 JSON(无 markdown 代码块、无解释文字):{"saves":[{"type":"...","name":"...","summary":"...","body":"..."}],"updates":[{"id":"...","reason":"...","summary":"...","body":"..."}],"forgets":[{"id":"...","reason":"..."}]}
97
- 空数组合法(无可记则三个数组都空)。
98
- 规则:
99
- ① 只记非显然、跨会话有用的事实/决策/坑;不记当前 bug、临时文件、未决 TODO、易变项;
100
- ② 宁可少记,不记正确废话(如"保持简洁");
101
- ③ updates/forgets 的 id 必须来自下方「现有记忆」列表;不在此列的不要编 id;
102
- ④ saves 的 name 须简洁且与现有不撞;type ∈ {${TYPES}};
103
- ⑤ 若现有记忆与新事实矛盾或过时,update 旧条(改 summary/body)而非新建重复条;
104
- ⑥ forgets 用于明显已失效 / 被新条取代的记忆(归档,非硬删)。`;
105
- const REFLECT_USER = (transcript, sample) => `## 近期会话\n${transcript}\n\n## 现有记忆\n${sample}\n\n产出 JSON:`;
95
+ const REFLECT_SYS = `You are mocode's memory reflector. Review the recent session and existing memories, producing **only** updates worth remembering long-term.
96
+ Output strictly JSON (no markdown code blocks, no explanatory text): {"saves":[{"type":"...","name":"...","summary":"...","body":"..."}],"updates":[{"id":"...","reason":"...","summary":"...","body":"..."}],"forgets":[{"id":"...","reason":"..."}]}
97
+ Empty arrays are valid (if nothing is worth saving, all three arrays are empty).
98
+ Rules:
99
+ ① Only store non-obvious, cross-session-useful facts/decisions/pitfalls; do not store current bugs, temp files, undecided TODOs, or volatile items;
100
+ ② Better to store less than to store trivially correct info (e.g. "keep it concise");
101
+ ③ ids in updates/forgets must come from the "existing memories" list below; do not fabricate ids not listed there;
102
+ ④ names in saves must be concise and not collide with existing ones; type ∈ {${TYPES}};
103
+ ⑤ If an existing memory contradicts new facts or is outdated, update the old entry (modify summary/body) rather than creating a duplicate;
104
+ ⑥ forgets are for memories clearly stale / superseded by a new entry (archive, not hard-delete).`;
105
+ const REFLECT_USER = (transcript, sample) => `## Recent session\n${transcript}\n\n## Existing memories\n${sample}\n\nProduce JSON:`;
106
106
  function parsePlan(content) {
107
107
  if (!content)
108
108
  return null;
@@ -325,14 +325,14 @@ export function buildMemoryIndexSection() {
325
325
  const shown = active.slice(0, MAX_INDEX_ENTRIES);
326
326
  const lines = shown.map((e) => `- ${e.id}: ${e.name} — ${e.summary} (${e.type})`);
327
327
  const tail = active.length > MAX_INDEX_ENTRIES
328
- ? `\n\n…(共 ${active.length} 条,只显前 ${MAX_INDEX_ENTRIES};用 memory_search <id 或关键词> 查更多)`
328
+ ? `\n\n…(${active.length} total, showing first ${MAX_INDEX_ENTRIES}; use memory_search <id or keyword> for more)`
329
329
  : '';
330
330
  return [
331
331
  '',
332
332
  '',
333
- '## 记忆索引(按需 memory_search 取详情)',
334
- '以下是已保存的记忆条目(标题/摘要)。需要正文时调 memory_search(传 id 或关键词);用 memory_list 看全部,'
335
- + 'memory_update 改、memory_forget 忘。本列表为启动快照,会话期间新增的不在此——用 memory_list/memory_search 查最新。',
333
+ '## Memory Index (retrieve full body via memory_search)',
334
+ 'The following are saved memory entries (title/summary only). Retrieve full body via memory_search (pass id or keyword); use memory_list to see all,'
335
+ + ' memory_update to modify, memory_forget to archive. This list is a startup snapshot; entries added during the session are not listed here — use memory_list/memory_search to find them.',
336
336
  ...lines,
337
337
  tail,
338
338
  ].join('\n');