mocode-ai 0.1.9 → 0.2.0

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.
@@ -4,15 +4,17 @@ import path from 'node:path';
4
4
  import dotenv from 'dotenv';
5
5
  /**
6
6
  * 按优先级加载配置文件并回填 process.env:
7
- * 候选(文件内升序、后者覆盖前者):~/.mocode/config(全局)→ <cwd>/.mocode/config → <cwd>/.env(兼容旧用法)。
7
+ * 候选(后者覆盖前者,优先级升序):<cwd>/.env(兼容旧用法,最低)→ ~/.mocode/config(全局)→ <cwd>/.mocode/config(项目级覆盖,最高)。
8
8
  * 合并后只回填 process.env 里**尚未设置**的键——shell 里 export 的环境变量永远优先。
9
- * 故 `mocode` 可在任意目录 / 任意终端启动:全局配置(~/.mocode/config)兜底,项目级文件按需覆盖。
9
+ * 故 `mocode` 可在任意目录 / 任意终端启动:全局配置(~/.mocode/config)兜底(/model 与 mocode config 写此),
10
+ * 项目级 .mocode/config 按需覆盖全局;旧用法 .env 优先级最低,不再盖过全局 config——
11
+ * 否则 /model 写入 ~/.mocode/config 的 LLM 键会被项目 .env 里的同名旧值盖回。
10
12
  */
11
13
  function loadEnvFiles() {
12
14
  const candidates = [
13
- path.join(os.homedir(), '.mocode', 'config'),
14
- path.join(process.cwd(), '.mocode', 'config'),
15
- path.join(process.cwd(), '.env'),
15
+ path.join(process.cwd(), '.env'), // 兼容旧用法,优先级最低
16
+ path.join(os.homedir(), '.mocode', 'config'), // 全局(/model 与 mocode config 写此)
17
+ path.join(process.cwd(), '.mocode', 'config'), // 项目级覆盖,优先级最高
16
18
  ];
17
19
  const fromFiles = {};
18
20
  for (const p of candidates) {
@@ -75,6 +77,7 @@ ${PLATFORM_NOTE}
75
77
 
76
78
  ## Workflow
77
79
  - Understand before acting: when unsure about requirements or code state, explore first; don't assume.
80
+ - **Code exploration first action**: before reading files with read_file or searching with grep, check if a .codegraph/ index exists. If it does, use the codegraph tool (explore for questions/features, node for a specific symbol) as your FIRST step — it returns source + call paths in one shot. Only fall back to read_file/grep when codegraph misses, you need just-changed content, or you're editing a known small file. Build the index with \`codegraph init\` if none exists.
78
81
  - Small steps: break tasks into verifiable sub-steps. Before each step, think clearly about what to change and why.
79
82
  - Verify after change: run typecheck / tests / build via run_command to confirm it works. Never claim done without verification.
80
83
 
@@ -121,7 +124,7 @@ export const PLAN_MODE_SUFFIX = `
121
124
  ## ⛯ PLAN MODE (active now)
122
125
  You are in PLAN mode: investigate and design only — do NOT execute or change anything.
123
126
  - 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.
124
- - Research thoroughly: locate the relevant code, trace call paths, and understand existing patterns and conventions before designing. Prefer codegraph when a .codegraph/ index exists.
127
+ - Research thoroughly: locate the relevant code, trace call paths, and understand existing patterns and conventions before designing. Your FIRST action for code exploration should be the codegraph tool (explore/node) when a .codegraph/ index exists — not read_file/grep. Use read_file/grep only to fill gaps codegraph leaves.
125
128
  - 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.
126
129
  - 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.
127
130
  - 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. Do NOT ask the user for confirmation or approval in your text reply (e.g. "is this plan OK?", "shall I proceed?", "需要你确认") — the REPL automatically shows an approval prompt after you STOP, so asking in text is redundant and forces the user to answer twice. Just present the plan and end your reply.`;
@@ -113,7 +113,7 @@ function renderContextBar(history) {
113
113
  const pct = Math.min(1, est / win);
114
114
  const W = 10;
115
115
  const filled = Math.round(pct * W);
116
- const bar = filled === 0 ? ' '.repeat(W) : '█'.repeat(filled) + '░'.repeat(W - filled);
116
+ const bar = '█'.repeat(filled) + '░'.repeat(W - filled);
117
117
  const src = contextState.lastUsage ? '实测' : '估算';
118
118
  const k = (n) => `${Math.round(n / 1000)}k`;
119
119
  const pctCol = pct >= config.compactThreshold ? ui.yellow : ui.cyan;
@@ -129,7 +129,7 @@ function renderContextBarInline(history) {
129
129
  const pct = Math.min(1, est / win);
130
130
  const W = 10;
131
131
  const filled = Math.round(pct * W);
132
- const bar = filled === 0 ? ' '.repeat(W) : '█'.repeat(filled) + '░'.repeat(W - filled);
132
+ const bar = '█'.repeat(filled) + '░'.repeat(W - filled);
133
133
  const k = (n) => `${Math.round(n / 1000)}k`;
134
134
  const pctCol = pct >= config.compactThreshold ? ui.yellow : ui.cyan;
135
135
  return `${ui.gray}[${pctCol}${bar}${ui.reset}] ${pctCol}${Math.round(pct * 100)}%${ui.reset} ${ui.dim}${k(est)}/${k(win)}${ui.reset}`;
@@ -183,15 +183,10 @@ let pendingPrefill = null; // /rollback 选中后预填的 user 输入(下轮 IN
183
183
  function onRunningKey(_str, key) {
184
184
  if (!key)
185
185
  return;
186
- // SGR 鼠标滚轮:readline \x1B[<btn;col;rowM 拆成多 fragment,经 mouse.consumeMouse 重组;
187
- // 滚轮 → scrollBy(±5)(不调 setUserActive——与键盘滚动一致;流式 flushTimer 在 scrollOffset>0 时不重画,安全)
188
- const _m = mouse.consumeMouse(key.sequence ?? '');
189
- if (_m.suppress) {
190
- if (_m.wheel)
191
- layout.scrollBy(_m.wheel * 5);
186
+ // 鼠标 fragment:重组 + 派发给 layout.handleMouseEvent(滚轮/框选/复制)。
187
+ if (mouse.swallow(key.sequence ?? ''))
192
188
  return;
193
- }
194
- // 滚动回看键(优先;不触发回尾):PgUp/PgDn 翻页,↑/↓ 每次 5 行(键盘;鼠标滚轮已由上方守卫处理)。
189
+ // 滚动回看键(优先;不触发回尾):PgUp/PgDn 翻页,↑/↓ 每次 5 行(键盘)。
195
190
  // 运行态无输入光标,↑/↓ 无其他用途,直接作滚动。
196
191
  if (key.name === 'pageup' ||
197
192
  key.name === 'pagedown' ||
@@ -250,6 +245,11 @@ function onRunningKey(_str, key) {
250
245
  layout.paintRunningInputEcho(runningInput, runningPlaceholder);
251
246
  }
252
247
  }
248
+ /** 鼠标右键单击输入框(未拖动)时 layout 读剪贴板后回调:追加到 typeahead 缓冲(简单粘贴,不分长短)。 */
249
+ function onRunningMousePaste(text) {
250
+ runningInput += text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
251
+ layout.paintRunningInputEcho(runningInput, runningPlaceholder);
252
+ }
253
253
  /** 进入运行态:挂 keypress 监听 + raw mode + 新建 abort 控制器,返回其 signal。在 await runAgent 前、enterRunningMode 后调。 */
254
254
  function startRunningListener(placeholder) {
255
255
  runningPlaceholder = placeholder;
@@ -263,6 +263,7 @@ function startRunningListener(placeholder) {
263
263
  }
264
264
  stdin.resume();
265
265
  emitter.on('keypress', onRunningKey);
266
+ layout.setPasteHandler(onRunningMousePaste); // 鼠标右键单击输入框(未拖动)→ 读剪贴板贴入
266
267
  const ac = new AbortController();
267
268
  currentAbort = ac;
268
269
  return ac.signal;
@@ -270,6 +271,7 @@ function startRunningListener(placeholder) {
270
271
  /** 退出运行态:摘监听 + 清 abort。不 pause / 不 setRawMode(false)——紧接着 promptWithSlashMenu 自己接管 raw。 */
271
272
  function stopRunningListener() {
272
273
  emitter.off('keypress', onRunningKey);
274
+ layout.setPasteHandler(null);
273
275
  currentAbort = null;
274
276
  }
275
277
  /**
@@ -50,9 +50,11 @@ function runCodegraph(args) {
50
50
  }
51
51
  export const codegraphTool = {
52
52
  name: 'codegraph',
53
- description: 'Preferred for understanding/locating code, tracing call chains, assessing impact of changes in repos with a code index (.codegraph/).' +
54
- ' Returns symbol source + call paths in one shot — more accurate/economical than read_file/grep.' +
55
- ' Hints to run `codegraph init` when .codegraph/ is absent.',
53
+ description: 'FIRST CHOICE for understanding/locating code, tracing call chains, or assessing the impact of changes use this before read_file/grep when a .codegraph/ index exists.' +
54
+ ' Returns symbol source + call paths in one shot — more accurate and economical than piecing together read_file/grep.' +
55
+ ' When to use: starting any code exploration; locating a symbol; understanding how a feature works; seeing what calls a function or what a change affects.' +
56
+ ' When NOT to use: no .codegraph/ index (build it first with `codegraph init`); reading a file you just edited; editing a single known small file — in those cases go straight to read_file/edit_file.' +
57
+ ' Fallback: if codegraph misses or the result is incomplete, then use read_file/grep/glob to fill the gaps.',
56
58
  parameters: {
57
59
  type: 'object',
58
60
  properties: {
@@ -0,0 +1,118 @@
1
+ import { stdout, platform } from 'node:process';
2
+ import { spawn } from 'node:child_process';
3
+ /**
4
+ * 系统剪贴板读写(叶子模块,无 UI 依赖)。
5
+ *
6
+ * 写入(copyToClipboard):选区复制(layout 鼠标选择)与 /copy 等命令共用,双通道:
7
+ * 1. OSC 52(`\x1B]52;c;<base64>\x07`):请求终端把文本写入系统剪贴板。经终端转发,SSH / 远程也生效;
8
+ * Windows Terminal ≥1.16、iTerm2、kitty、WezTerm 等支持(部分终端默认关,需开"允许应用访问剪贴板")。
9
+ * 2. 本地原生工具(best-effort,失败静默):win32=clip.exe(UTF-16LE)、darwin=pbcopy、
10
+ * linux=wl-copy / xclip / xsel。覆盖 OSC52 被禁用的场景(如部分 VS Code 集成终端配置)。
11
+ * 两条都发:哪条生效由环境决定,重复写同一内容无副作用。
12
+ *
13
+ * 读取(readClipboard):OSC 52 是单向的(终端不会把剪贴板内容回传给应用,即便发 `\x1B]52;c;?\x07`
14
+ * 请求读取,多数终端出于安全考虑不响应),故读只能靠本地原生工具:win32=PowerShell Get-Clipboard、
15
+ * darwin=pbpaste、linux=wl-paste / xclip -o / xsel -o。供鼠标点击输入框时"贴入"用。
16
+ */
17
+ /** OSC 52:base64(UTF-8) 写系统剪贴板(c=clipboard 选区)。 */
18
+ function osc52(text) {
19
+ const b64 = Buffer.from(text, 'utf8').toString('base64');
20
+ // 长度保护:部分终端对 OSC52 载荷有上限(常见 ~74KB / 100KB),超限直接跳过 OSC52
21
+ // (仍走原生通道),避免半截 base64 污染终端。
22
+ if (b64.length > 100000)
23
+ return;
24
+ stdout.write(`\x1B]52;c;${b64}\x07`);
25
+ }
26
+ /** spawn 一个吃 stdin 的剪贴板工具,把 buf 写入其 stdin;所有错误静默(best-effort)。 */
27
+ function pipeTo(cmd, args, buf) {
28
+ try {
29
+ const p = spawn(cmd, args, {
30
+ stdio: ['pipe', 'ignore', 'ignore'],
31
+ windowsHide: true,
32
+ });
33
+ p.on('error', () => { }); // 命令不存在等:静默(OSC52 兜底)
34
+ p.stdin.on('error', () => { });
35
+ p.stdin.end(buf);
36
+ }
37
+ catch {
38
+ // 忽略
39
+ }
40
+ }
41
+ /** 本地原生剪贴板(best-effort)。 */
42
+ function nativeCopy(text) {
43
+ if (platform === 'win32') {
44
+ // clip.exe 在 Win10+ 接受 UTF-16LE 字节(无 BOM),可正确处理中文/emoji;
45
+ // 直接管 UTF-8 会按控制台代码页解码致乱码,故用 utf16le。
46
+ pipeTo('clip', [], Buffer.from(text, 'utf16le'));
47
+ }
48
+ else if (platform === 'darwin') {
49
+ pipeTo('pbcopy', [], Buffer.from(text, 'utf8'));
50
+ }
51
+ else {
52
+ // linux:Wayland 优先 wl-copy,X11 退 xclip / xsel(装了哪个用哪个;spawn error 静默跳过)
53
+ const b = Buffer.from(text, 'utf8');
54
+ pipeTo('wl-copy', [], b);
55
+ pipeTo('xclip', ['-selection', 'clipboard'], b);
56
+ pipeTo('xsel', ['--clipboard', '--input'], b);
57
+ }
58
+ }
59
+ /** 写入系统剪贴板(OSC52 + 本地原生双通道)。 */
60
+ export function copyToClipboard(text) {
61
+ if (!text)
62
+ return;
63
+ osc52(text);
64
+ nativeCopy(text);
65
+ }
66
+ /** spawn 一个不吃 stdin、把 stdout 收集成字符串的命令;失败返回 null(不抛)。 */
67
+ function captureOutput(cmd, args, encoding) {
68
+ return new Promise((resolve) => {
69
+ try {
70
+ const p = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true });
71
+ const chunks = [];
72
+ let settled = false;
73
+ const finish = (v) => {
74
+ if (settled)
75
+ return;
76
+ settled = true;
77
+ resolve(v);
78
+ };
79
+ p.stdout.on('data', (c) => chunks.push(c));
80
+ p.stdout.on('error', () => finish(null));
81
+ p.on('error', () => finish(null));
82
+ p.on('close', (code) => {
83
+ if (code !== 0)
84
+ return finish(null);
85
+ finish(Buffer.concat(chunks).toString(encoding));
86
+ });
87
+ }
88
+ catch {
89
+ resolve(null);
90
+ }
91
+ });
92
+ }
93
+ /**
94
+ * 读取系统剪贴板(本地原生工具,OSC52 无法读取——终端不回传)。
95
+ * win32 用 PowerShell -NoProfile Get-Clipboard(比 clip.exe 只写无读更可靠,系统自带无需安装);
96
+ * darwin=pbpaste;linux 依次尝试 wl-paste / xclip -o / xsel -o,装了哪个用哪个。
97
+ * 都失败返回空串(不抛),调用方按"无内容可贴"处理。
98
+ */
99
+ export async function readClipboard() {
100
+ if (platform === 'win32') {
101
+ // Get-Clipboard 默认按控制台代码页输出;显式转 UTF8 避免中文乱码。
102
+ const out = await captureOutput('powershell', ['-NoProfile', '-NonInteractive', '-Command',
103
+ '[Console]::OutputEncoding=[System.Text.Encoding]::UTF8; Get-Clipboard -Raw'], 'utf8');
104
+ return out?.replace(/\r\n/g, '\n').replace(/\r$/, '') ?? '';
105
+ }
106
+ if (platform === 'darwin') {
107
+ return (await captureOutput('pbpaste', [], 'utf8')) ?? '';
108
+ }
109
+ // linux:按顺序试,第一个成功(非 null)即用
110
+ const wl = await captureOutput('wl-paste', ['--no-newline'], 'utf8');
111
+ if (wl != null)
112
+ return wl;
113
+ const xc = await captureOutput('xclip', ['-selection', 'clipboard', '-o'], 'utf8');
114
+ if (xc != null)
115
+ return xc;
116
+ const xs = await captureOutput('xsel', ['--clipboard', '--output'], 'utf8');
117
+ return xs ?? '';
118
+ }
@@ -104,3 +104,8 @@ export function sliceFromEnd(offset, count) {
104
104
  export function totalRows() {
105
105
  return rows.length + (hasCurrent ? 1 : 0);
106
106
  }
107
+ /** 取绝对行索引(0-based,含当前行)的原始自洽行;越界返 null。供鼠标选区文本提取。 */
108
+ export function lineAt(abs) {
109
+ const all = snapshot();
110
+ return abs >= 0 && abs < all.length ? all[abs] : null;
111
+ }
@@ -124,6 +124,7 @@ export async function promptIntervention(req) {
124
124
  }
125
125
  /** 退出:摘自己的监听 + 恢复快照监听 + 擦菜单恢复内容区。不 setRawMode(false)/pause stdin(运行态由 repl 接管)。 */
126
126
  function cleanup() {
127
+ layout.setMouseEnabled(true); // 恢复鼠标框选(面板期间禁,防拖拽覆盖菜单)
127
128
  emitter.removeListener('keypress', onKey);
128
129
  for (const l of savedListeners)
129
130
  emitter.on('keypress', l);
@@ -148,16 +149,10 @@ export async function promptIntervention(req) {
148
149
  function onKey(_str, key) {
149
150
  if (resolved || !key)
150
151
  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);
152
+ // 鼠标 fragment:滚轮走 handleMouseEvent(mouseEnabled=true 时仍可滚动查看内容,与之前行为一致);
153
+ // 框选/拖拽在面板期间被 layout.setMouseEnabled(false) 挡掉( viewport 重画覆盖菜单)
154
+ if (mouse.swallow(key.sequence ?? ''))
159
155
  return;
160
- }
161
156
  // Ctrl+C → 取消(不 reject SIGINT——否则经 executeTool 的 try/catch 变成 tool 错误串)
162
157
  if (key.ctrl && key.name === 'c') {
163
158
  finish({ action: 'cancelled' });
@@ -276,8 +271,9 @@ export async function promptIntervention(req) {
276
271
  return new Promise((res, rej) => {
277
272
  resolve = res;
278
273
  try {
279
- // 进入面板:停 spinner(避免 onFrame 覆盖)+ 回尾(若用户正滚动回看)
274
+ // 进入面板:停 spinner(避免 onFrame 覆盖)+ 禁鼠标框选(防拖拽 viewport 重画覆盖菜单)+ 回尾(若用户正滚动回看)
280
275
  Spinner.pauseCurrent();
276
+ layout.setMouseEnabled(false);
281
277
  layout.resetScroll();
282
278
  // 快照现有 keypress 监听(运行态的 onRunningKey)并摘掉,挂自己的 onKey
283
279
  savedListeners = emitter.listeners('keypress').slice();
@@ -297,6 +293,7 @@ export async function promptIntervention(req) {
297
293
  }
298
294
  catch (e) {
299
295
  // 进入失败:必须恢复运行态监听,否则 onRunningKey 残留摘除 → 本 turns 的 Ctrl+C/滚动/typeahead 全废
296
+ layout.setMouseEnabled(true);
300
297
  try {
301
298
  emitter.removeListener('keypress', onKey);
302
299
  }