mocode-ai 0.1.4 → 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 (46) hide show
  1. package/README.md +196 -188
  2. package/dist/agent/core.js +302 -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 +73 -30
  9. package/dist/llm/index.js +11 -9
  10. package/dist/memory/index.js +2 -2
  11. package/dist/memory/reflect.js +11 -11
  12. package/dist/memory/store.js +4 -4
  13. package/dist/repl/index.js +264 -108
  14. package/dist/session/compact.js +1 -1
  15. package/dist/skills/index.js +2 -2
  16. package/dist/tools/builtins/ask-human.js +7 -7
  17. package/dist/tools/builtins/codegraph.js +113 -0
  18. package/dist/tools/builtins/edit-file.js +3 -3
  19. package/dist/tools/builtins/glob.js +3 -2
  20. package/dist/tools/builtins/grep.js +4 -3
  21. package/dist/tools/builtins/index.js +7 -1
  22. package/dist/tools/builtins/memory-forget.js +2 -2
  23. package/dist/tools/builtins/memory-list.js +2 -2
  24. package/dist/tools/builtins/memory-save.js +7 -7
  25. package/dist/tools/builtins/memory-search.js +4 -4
  26. package/dist/tools/builtins/memory-update.js +6 -6
  27. package/dist/tools/builtins/read-file.js +5 -4
  28. package/dist/tools/builtins/run-command.js +40 -7
  29. package/dist/tools/builtins/switch-mode.js +46 -0
  30. package/dist/tools/builtins/task.js +62 -0
  31. package/dist/tools/builtins/use-skill.js +2 -2
  32. package/dist/tools/builtins/web-fetch.js +16 -3
  33. package/dist/tools/builtins/web-search.js +6 -6
  34. package/dist/tools/builtins/write-file.js +3 -3
  35. package/dist/tools/constants.js +16 -0
  36. package/dist/tools/registry.js +11 -4
  37. package/dist/ui/content.js +25 -17
  38. package/dist/ui/diff.js +28 -26
  39. package/dist/ui/intervention.js +24 -3
  40. package/dist/ui/layout.js +295 -81
  41. package/dist/ui/markdown.js +607 -0
  42. package/dist/ui/mouse.js +93 -0
  43. package/dist/ui/prompt.js +259 -5
  44. package/dist/ui/render.js +1 -1
  45. package/dist/ui/theme.js +146 -13
  46. package/package.json +2 -2
@@ -3,12 +3,12 @@ import { resolve, dirname } from 'node:path';
3
3
  // ---------- write_file ----------
4
4
  export const writeFileTool = {
5
5
  name: 'write_file',
6
- description: '创建或覆盖文件,自动创建父目录。',
6
+ description: 'Create or overwrite a file, creating parent directories as needed.',
7
7
  parameters: {
8
8
  type: 'object',
9
9
  properties: {
10
- path: { type: 'string', description: '文件路径' },
11
- content: { type: 'string', description: '完整文件内容' },
10
+ path: { type: 'string', description: 'File path' },
11
+ content: { type: 'string', description: 'Full file content' },
12
12
  },
13
13
  required: ['path', 'content'],
14
14
  },
@@ -22,3 +22,19 @@ export const GC_DAYS = 90;
22
22
  /** memory_search 结果(召回的记忆正文)的放宽上限:指令性内容,中截破坏语义,对齐 use_skill。 */
23
23
  export const MAX_MEMORY_RESULT = 64000;
24
24
  export const IGNORE = ['**/node_modules/**', '**/.git/**'];
25
+ // ── plan 模式(只读规划,不执行)──────────────────────────────────────────────
26
+ /**
27
+ * plan 模式下从工具 schema 里剔除的工具(模型根本看不到 → 调不到):
28
+ * 写盘 / 命令 / 记忆写入类 + task(派生子 agent,plan 模式只读不可有副作用)。单一事实源,
29
+ * 被 llm(planChatTools)与 agent(防御 backstop)共用。
30
+ * 只读工具(read_file/glob/grep/codegraph/web_search/web_fetch/use_skill/ask_human/memory_search/memory_list)保留。
31
+ */
32
+ export const PLAN_DISABLED_TOOLS = new Set([
33
+ 'write_file',
34
+ 'edit_file',
35
+ 'run_command',
36
+ 'memory_save',
37
+ 'memory_update',
38
+ 'memory_forget',
39
+ 'task',
40
+ ]);
@@ -5,8 +5,13 @@ import { recordMutation } from '../rollback/index.js';
5
5
  * 未来可在此合并 MCP 工具、用户自定义工具等(见 src/mcp/)。
6
6
  */
7
7
  export const tools = builtinTools;
8
- /** 按名调度工具,统一 try/catch + JSON 解析,返回字符串而非抛错。 */
9
- export async function executeTool(name, argsRaw) {
8
+ /**
9
+ * 按名调度工具,统一 try/catch + JSON 解析,返回字符串而非抛错。
10
+ * signal 透传给 tool.execute(经 ctx):长任务工具(run_command/web_fetch)abort 即时取消,
11
+ * 让用户 Ctrl+C 能跟手中断工具执行(而非等命令跑完 / 超时)。
12
+ * opts.skipRollback:子 agent 逻辑隔离用——跳过 recordMutation,子 agent 改动不进主回滚快照链。
13
+ */
14
+ export async function executeTool(name, argsRaw, signal, opts) {
10
15
  const tool = tools.find((t) => t.name === name);
11
16
  if (!tool)
12
17
  return `错误:未知工具 "${name}"`;
@@ -19,12 +24,14 @@ export async function executeTool(name, argsRaw) {
19
24
  }
20
25
  try {
21
26
  // 撤销回滚用:write_file/edit_file 改动前记 before 快照(回滚时恢复到轮末状态)。
22
- if ((name === 'write_file' || name === 'edit_file') &&
27
+ // 子 agent(skipRollback)跳过:其改动不进主回滚链,主 /rollback 不撤销(靠 git 兜底)。
28
+ if (!opts?.skipRollback &&
29
+ (name === 'write_file' || name === 'edit_file') &&
23
30
  typeof args.path === 'string' &&
24
31
  args.path) {
25
32
  recordMutation(args.path);
26
33
  }
27
- return await tool.execute(args);
34
+ return await tool.execute(args, { signal, skipRollback: opts?.skipRollback });
28
35
  }
29
36
  catch (e) {
30
37
  return `错误:工具 ${name} 执行失败: ${e instanceof Error ? e.message : String(e)}`;
@@ -2,7 +2,7 @@
2
2
  * 内容物理行缓冲(Phase 2 滚动回滚用)。
3
3
  *
4
4
  * 存折行后的物理行,每行自洽带色,供 layout.repaintViewport 单行直出、无需继承 SGR 状态。
5
- * contentWrite 逐字符喂入(feedChar / feedSgr / breakRow);eraseSegmentBack 用 popRows 同步删段。
5
+ * contentWrite 逐字符喂入(feedChar / feedSgr / breakRow);setLines 替换当前段(md 流式渲染用)。
6
6
  *
7
7
  * **SGR 自洽模型**:每行存 `rowStartSgr + curRaw + \x1B[0m`:
8
8
  * - rowStartSgr:本行起始时继承的活跃 SGR 前缀(来自上一行末状态);
@@ -23,7 +23,7 @@ let curSgr = ''; // 当前活跃 SGR 状态(行内演进;行末值 = 下行继
23
23
  let rowStartSgr = ''; // 当前行起始继承的 SGR 前缀
24
24
  let curRaw = ''; // 当前行原始字节(可见 + 行内 SGR 码)
25
25
  let hasCurrent = false; // 是否有当前行(feed/breakRow 后;新空行也算)
26
- let segMark = null; // 段起点快照(供 eraseSegment 恢复)
26
+ let segMark = null; // 段起点快照(md 段:setLines 截断用,commitSegment 清)
27
27
  export function reset() {
28
28
  rows = [];
29
29
  curSgr = '';
@@ -55,27 +55,35 @@ export function breakRow() {
55
55
  rowStartSgr = curSgr; // 下行继承本行末状态
56
56
  hasCurrent = true; // 新空行即当前行
57
57
  }
58
- /** 标记段(思考段)起点:快照当前缓冲状态,供 eraseSegment 恢复。 */
58
+ /** 标记段起点:快照当前缓冲状态,供 setLines 截断定位段头(md 流式渲染每 chunk 截断重渲)。 */
59
59
  export function beginSegment() {
60
60
  segMark = { rowIdx: rows.length, rowStartSgr, curSgr, curRaw, hasCurrent };
61
61
  }
62
62
  /**
63
- * 删段并恢复到 beginSegment 时的状态;返回段物理行数(= segLines + 末行部分,与屏幕擦除行数一致)。
64
- * 段足迹 = 自 mark 后提交的行(committedErased)+ 当前行若有内容(currentErased)。
65
- * 恢复后当前行 = 段起点的那一行(折叠标题将写入此处),保证滚动回看只看到标题、不看到原文。
63
+ * 用预渲染的自洽 ANSI 行替换当前段(从 segMark 起),供 markdown 流式渲染:
64
+ * layout.contentWriteMd 每 chunk 把累积 text 经 renderMarkdown 渲成自洽行,调此替换缓冲段。
65
+ * 截断 rows 到 segMark.rowIdx(擦上次渲染)+ push 新行;curSgr 复位为默认(行末 reset);
66
+ * **保留 segMark**(下次 setLines 还要截断),由 commitSegment 清除。
67
+ * 行须各自自洽(行内 SGR 自带、行末 reset)——不经 feedChar/feedSgr,直接入 rows,
68
+ * 故 setLines 后 hasCurrent=false(snapshot 只返 rows;续写位由 layout 跟踪)。
66
69
  */
67
- export function eraseSegment() {
68
- if (!segMark)
69
- return 0;
70
- const committedErased = rows.length - segMark.rowIdx;
71
- const currentErased = hasCurrent && curRaw.length > 0 ? 1 : 0;
72
- rows.length = segMark.rowIdx;
73
- rowStartSgr = segMark.rowStartSgr;
74
- curSgr = segMark.curSgr;
75
- curRaw = segMark.curRaw;
76
- hasCurrent = segMark.hasCurrent;
70
+ export function setLines(lines) {
71
+ if (segMark) {
72
+ rows.length = segMark.rowIdx;
73
+ }
74
+ for (const line of lines) {
75
+ rows.push(line);
76
+ }
77
+ if (rows.length > MAX_ROWS + 512)
78
+ rows.splice(0, rows.length - MAX_ROWS);
79
+ curSgr = '';
80
+ rowStartSgr = '';
81
+ curRaw = '';
82
+ hasCurrent = false;
83
+ }
84
+ /** 提交段:清 segMark,后续写入不再被 setLines 截断(md 段结束、非 md 内容接续时调)。 */
85
+ export function commitSegment() {
77
86
  segMark = null;
78
- return committedErased + currentErased;
79
87
  }
80
88
  /** 快照(已提交行 + 当前行若有)。viewport 取行窗用。 */
81
89
  function snapshot() {
package/dist/ui/diff.js CHANGED
@@ -36,35 +36,37 @@ function langForPath(p) {
36
36
  }
37
37
  return LANG_BY_EXT[base];
38
38
  }
39
- /** 包一层:token 文本 + 颜色 + reset,自成闭区间(防 SGR 跨行泄漏)。 */
40
- const paint = (color) => (s) => `${color}${s}${ui.reset}`;
39
+ /**
40
+ * 包一层:token 文本 + 颜色 + reset,自成闭区间(防 SGR 跨行泄漏)。
41
+ * 取 ColorKey 而非 ANSI 串——闭包调用时才读 `ui[key]`,故 setTheme 后 diff 高亮即跟随。 */
42
+ const paint = (key) => (s) => `${ui[key]}${s}${ui.reset}`;
41
43
  /** 自定义主题:default=plain(未匹配文本不着色,避免默认主题把普通代码染黄),常见 token 用 mocode ui 色板。 */
42
44
  const THEME = {
43
45
  default: plain,
44
- keyword: paint(ui.magenta),
45
- 'built_in': paint(ui.cyan),
46
- type: paint(ui.cyan),
47
- literal: paint(ui.cyan),
48
- number: paint(ui.green),
49
- string: paint(ui.green),
50
- subst: paint(ui.green),
51
- comment: paint(ui.gray),
52
- doctag: paint(ui.gray),
53
- function: paint(ui.blue),
54
- title: paint(ui.blue),
55
- class: paint(ui.brightCyan),
56
- meta: paint(ui.gray),
57
- 'meta-keyword': paint(ui.magenta),
58
- regexp: paint(ui.red),
59
- attr: paint(ui.cyan),
60
- attribute: paint(ui.cyan),
61
- variable: paint(ui.red),
62
- tag: paint(ui.red),
63
- name: paint(ui.cyan),
64
- symbol: paint(ui.cyan),
65
- section: paint(ui.brightMagenta),
66
- addition: paint(ui.green),
67
- deletion: paint(ui.red),
46
+ keyword: paint('magenta'),
47
+ 'built_in': paint('cyan'),
48
+ type: paint('cyan'),
49
+ literal: paint('cyan'),
50
+ number: paint('green'),
51
+ string: paint('green'),
52
+ subst: paint('green'),
53
+ comment: paint('gray'),
54
+ doctag: paint('gray'),
55
+ function: paint('blue'),
56
+ title: paint('blue'),
57
+ class: paint('brightCyan'),
58
+ meta: paint('gray'),
59
+ 'meta-keyword': paint('magenta'),
60
+ regexp: paint('red'),
61
+ attr: paint('cyan'),
62
+ attribute: paint('cyan'),
63
+ variable: paint('red'),
64
+ tag: paint('red'),
65
+ name: paint('cyan'),
66
+ symbol: paint('cyan'),
67
+ section: paint('brightMagenta'),
68
+ addition: paint('green'),
69
+ deletion: paint('red'),
68
70
  };
69
71
  function highlightLine(text, lang) {
70
72
  if (!lang || text === '')
@@ -3,6 +3,7 @@ import { stdin, stderr } from 'node:process';
3
3
  import { ui } from './theme.js';
4
4
  import { displayWidth, truncateDisplay } from './render.js';
5
5
  import * as layout from './layout.js';
6
+ import * as mouse from './mouse.js';
6
7
  import { Spinner } from './spinner.js';
7
8
  const emitter = stdin;
8
9
  /** choice 末尾的"自定义输入"项标签(纯 ASCII,宽度安全)。选中后切到 input 子态。 */
@@ -128,8 +129,9 @@ export async function promptIntervention(req) {
128
129
  emitter.on('keypress', l);
129
130
  savedListeners = [];
130
131
  // 先 paintInput(dim,menu:null):擦菜单(用 lastMenuRows)+ 画 dim 占位底栏 + 复位 lastMenuRows=0;
131
- // 再 repaintViewport:从 content 缓冲重画内容区(恢复被菜单覆盖的 ● ask_human 行)。
132
- // 顺序不可反——repaintViewport 后再 paintInput(menu:null) 会用 stale lastMenuRows 擦掉已恢复的内容。
132
+ // 再处理滚动:面板内可能滚过滚轮(scrollOffset>0)→ resetScroll 回尾让 agent 后续输出可见;
133
+ // 未滚则 repaintViewport 从 content 缓冲重画内容区(恢复被菜单覆盖的 ● ask_human 行)。
134
+ // 顺序不可反——resetScroll/repaintViewport 在菜单还在(lastMenuRows>0)时会用内容覆盖菜单区,擦不掉菜单。
133
135
  layout.paintInput({
134
136
  prompt: '❯ ',
135
137
  lines: [''],
@@ -138,11 +140,24 @@ export async function promptIntervention(req) {
138
140
  menu: null,
139
141
  dim: true,
140
142
  });
141
- layout.repaintViewport();
143
+ if (layout.isScrolled())
144
+ layout.resetScroll();
145
+ else
146
+ layout.repaintViewport();
142
147
  }
143
148
  function onKey(_str, key) {
144
149
  if (resolved || !key)
145
150
  return;
151
+ // SGR 鼠标 fragment:readline 拆碎 \x1B[<…M,经 mouse.consumeMouse 重组;
152
+ // 滚轮 → scrollBy(±5) 滚动查看 agent 输出内容(菜单只靠 ↑↓ 选,滚轮不选菜单);
153
+ // fragment 期间 suppress 吞掉防砸进 onKeyInput 的可打印插入。菜单经 scrollBy 末尾
154
+ // repaint()→paintInput(lastView) 重画,不被 viewport 滚动覆盖。
155
+ const _m = mouse.consumeMouse(key.sequence ?? '');
156
+ if (_m.suppress) {
157
+ if (_m.wheel)
158
+ layout.scrollBy(_m.wheel * 5);
159
+ return;
160
+ }
146
161
  // Ctrl+C → 取消(不 reject SIGINT——否则经 executeTool 的 try/catch 变成 tool 错误串)
147
162
  if (key.ctrl && key.name === 'c') {
148
163
  finish({ action: 'cancelled' });
@@ -158,6 +173,12 @@ export async function promptIntervention(req) {
158
173
  finish({ action: 'cancelled' });
159
174
  return;
160
175
  }
176
+ // PgUp/PgDn:翻页滚动查看 agent 输出内容(与 onRunningKey/prompt.ts 一致;↑↓ 仍归菜单导航,不抢)。
177
+ if (key.name === 'pageup' || key.name === 'pagedown') {
178
+ const pageH = layout.getGeo().contentBottom;
179
+ layout.scrollBy(key.name === 'pageup' ? pageH : -pageH);
180
+ return;
181
+ }
161
182
  if (mode === 'choice') {
162
183
  onKeyChoice(key);
163
184
  }