mingdao-harness 0.2.4 → 0.2.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 (66) hide show
  1. package/docs/EVALUATION-ROUND2.md +58 -0
  2. package/docs/ROADMAP-NEXT.md +5 -5
  3. package/package.json +2 -2
  4. package/src/agent.js +74 -36
  5. package/src/atomic-write.js +8 -2
  6. package/src/audit.js +2 -2
  7. package/src/batch.js +7 -7
  8. package/src/cachestats.js +9 -9
  9. package/src/cli.js +42 -688
  10. package/src/commands/desktop.js +1 -1
  11. package/src/commands/key.js +1 -1
  12. package/src/commands/repl.js +711 -0
  13. package/src/commands/schedule.js +7 -7
  14. package/src/commands/skill.js +18 -16
  15. package/src/commands/sync.js +5 -5
  16. package/src/commands/update.js +7 -6
  17. package/src/commands/workspace.js +2 -2
  18. package/src/compact.js +6 -6
  19. package/src/config.js +7 -4
  20. package/src/context.js +37 -21
  21. package/src/cost-guard.js +14 -6
  22. package/src/credentials.js +6 -0
  23. package/src/hooks.js +11 -11
  24. package/src/mcp-presets.js +5 -5
  25. package/src/mcp.js +19 -19
  26. package/src/memory.js +16 -16
  27. package/src/model-discovery.js +16 -15
  28. package/src/models.js +4 -4
  29. package/src/notify.js +2 -2
  30. package/src/permissions.js +7 -3
  31. package/src/pricing.js +35 -1
  32. package/src/prompts.js +1 -1
  33. package/src/providers/index.js +7 -7
  34. package/src/providers/openai-compatible.js +13 -11
  35. package/src/routing.js +8 -8
  36. package/src/schedule.js +30 -30
  37. package/src/session-index.js +5 -1
  38. package/src/session.js +11 -2
  39. package/src/skill-lib.js +62 -3
  40. package/src/skill-registry.js +10 -9
  41. package/src/skills.js +14 -12
  42. package/src/sync-server.js +86 -3
  43. package/src/sync.js +53 -20
  44. package/src/tasks/worker.js +126 -0
  45. package/src/tasks.js +14 -14
  46. package/src/titles.js +5 -5
  47. package/src/tokenizer.js +23 -5
  48. package/src/tools/bash.js +12 -12
  49. package/src/tools/fs-tools.js +66 -9
  50. package/src/tools/index.js +7 -7
  51. package/src/ui.js +83 -6
  52. package/src/update.js +8 -8
  53. package/src/web/app.js +16 -5
  54. package/src/web/attachments.js +1 -1
  55. package/src/web/index.html +21 -1
  56. package/src/web/routes/api.js +38 -786
  57. package/src/web/routes/domains/config.js +280 -0
  58. package/src/web/routes/domains/misc.js +129 -0
  59. package/src/web/routes/domains/schedule.js +127 -0
  60. package/src/web/routes/domains/sessions.js +105 -0
  61. package/src/web/routes/domains/skills.js +79 -0
  62. package/src/web/routes/domains/sync.js +101 -0
  63. package/src/web/routes/domains/workspace.js +119 -0
  64. package/src/web/server.js +73 -31
  65. package/src/web/web-io.js +54 -12
  66. package/src/workspace.js +14 -14
package/src/tools/bash.js CHANGED
@@ -15,22 +15,22 @@ const MAX_TIMEOUT_SECONDS = 600;
15
15
  // 整体关闭(回到完全透传)。
16
16
  const SENSITIVE_ENV_PAIR = /(api[_-]?key|access[_-]?key|client[_-]?secret|private[_-]?key)/i;
17
17
  const SENSITIVE_ENV_SEGMENT = /(^|_)(token|secret|password|passwd|credential|authorization|auth)(_|$)/i;
18
- const isSensitiveEnv = (k) => SENSITIVE_ENV_PAIR.test(k) || SENSITIVE_ENV_SEGMENT.test(k);
18
+ const isSensitiveEnv = (/** @type {any} */ k) => SENSITIVE_ENV_PAIR.test(k) || SENSITIVE_ENV_SEGMENT.test(k);
19
19
 
20
- function buildChildEnv(ctx, filterSensitive) {
20
+ function buildChildEnv(/** @type {any} */ ctx, /** @type {any} */ filterSensitive) {
21
21
  if (!filterSensitive) return process.env;
22
22
  const keep = new Set((ctx?.cfg?.bashEnvKeep || []).map(String));
23
23
  const env = {};
24
24
  for (const [k, v] of Object.entries(process.env)) {
25
- if (!isSensitiveEnv(k) || keep.has(k)) env[k] = v;
25
+ if (!isSensitiveEnv(k) || keep.has(k)) (/** @type {any} */ (env))[k] = v;
26
26
  }
27
27
  return env;
28
28
  }
29
29
 
30
- let sandboxSupport = null;
30
+ let /** @type {any} */ sandboxSupport = null;
31
31
 
32
32
  export function detectSandbox() {
33
- if (sandboxSupport !== null) return sandboxSupport;
33
+ if (/** @type {any} */ sandboxSupport !== null) return /** @type {any} */ sandboxSupport;
34
34
  sandboxSupport = 'none';
35
35
  if (process.platform === 'linux') {
36
36
  try {
@@ -46,12 +46,12 @@ export function detectSandbox() {
46
46
  // 输出折叠(审计 MiniMax §3.3-E / v0.1.48 P1-G):模型回填的 bash 输出先折叠再截断——
47
47
  // 1) 剥离 ANSI 转义序列(CSI/OSC);2) 连续重复行(>3 行相同)折叠为「首行 + 重复标记」。
48
48
  // npm install 类输出通常 30-50KB → 折叠后 5-10KB,单次工具回填省 60-70% prompt token。
49
- function stripAnsi(s) {
49
+ function stripAnsi(/** @type {any} */ s) {
50
50
  return s
51
51
  .replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, '')
52
52
  .replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, '');
53
53
  }
54
- function foldRepeats(s) {
54
+ function foldRepeats(/** @type {any} */ s) {
55
55
  const lines = s.split('\n');
56
56
  const out = [];
57
57
  let i = 0;
@@ -69,12 +69,12 @@ function foldRepeats(s) {
69
69
  }
70
70
  return out.join('\n');
71
71
  }
72
- function tail(s, n) {
72
+ function tail(/** @type {any} */ s, /** @type {any} */ n) {
73
73
  const folded = foldRepeats(stripAnsi(s));
74
74
  return folded.length > n ? `…[输出过长,已截断头部]\n${folded.slice(-n)}` : folded;
75
75
  }
76
76
 
77
- export function runBash(args, ctx) {
77
+ export function runBash(/** @type {any} */ args, /** @type {any} */ ctx) {
78
78
  const command = String(args.command ?? '');
79
79
  if (!command.trim()) return { ok: false, error: 'command 参数为空。' };
80
80
  const timeoutSec = Math.min(Number(args.timeout) || 120, MAX_TIMEOUT_SECONDS);
@@ -125,13 +125,13 @@ export function runBash(args, ctx) {
125
125
  let done = false;
126
126
  let timedOut = false;
127
127
  // 输出增量截断:超长输出只保留尾部,避免内存无限累积
128
- const cap = (s, d) => {
128
+ const cap = (/** @type {any} */ s, /** @type {any} */ d) => {
129
129
  const t = s + d;
130
130
  return t.length > MAX_OUTPUT * 2 ? t.slice(-MAX_OUTPUT * 2) : t;
131
131
  };
132
- const killGroup = (sig) => {
132
+ const killGroup = (/** @type {any} */ sig) => {
133
133
  try {
134
- process.kill(-child.pid, sig);
134
+ process.kill(-(/** @type {any} */ (child)).pid, sig);
135
135
  } catch {
136
136
  try {
137
137
  child.kill(sig);
@@ -9,11 +9,18 @@ const MAX_GLOB_RESULTS = 1000;
9
9
  const MAX_GREP_MATCHES = 250;
10
10
  const MAX_FILE_BYTES = 5 * 1024 * 1024;
11
11
 
12
+ /**
13
+ * @param {any} cwd
14
+ * @param {any} p
15
+ */
12
16
  function resolvePath(cwd, p) {
13
17
  if (path.isAbsolute(p)) return path.normalize(p);
14
18
  return path.resolve(cwd, p);
15
19
  }
16
20
 
21
+ /**
22
+ * @param {any} buf
23
+ */
17
24
  function isProbablyBinary(buf) {
18
25
  const head = buf.subarray(0, 8192);
19
26
  for (let i = 0; i < head.length; i++) {
@@ -25,6 +32,10 @@ function isProbablyBinary(buf) {
25
32
  // —— undo 备份(会话级,每文件最多 10 份 + 全局上限,质检 M9) ——
26
33
  const UNDO_MAX_FILES = 64;
27
34
  const UNDO_MAX_BYTES = 20 * 1024 * 1024;
35
+ /**
36
+ * @param {any} ctx
37
+ * @param {any} p
38
+ */
28
39
  function backup(ctx, p) {
29
40
  try {
30
41
  const store = ctx?.undoStore?.backups;
@@ -57,6 +68,10 @@ function backup(ctx, p) {
57
68
  } catch {}
58
69
  }
59
70
 
71
+ /**
72
+ * @param {any} args
73
+ * @param {any} ctx
74
+ */
60
75
  export function undo(args, ctx) {
61
76
  const store = ctx?.undoStore?.backups;
62
77
  if (!(store instanceof Map) || !store.size) return { ok: false, error: '没有可撤销的修改。' };
@@ -68,7 +83,7 @@ export function undo(args, ctx) {
68
83
  try {
69
84
  fs.writeFileSync(p, last.content);
70
85
  return { ok: true, output: `已撤销 ${p} 的最近一次修改(该文件剩余备份 ${list.length} 个)。` };
71
- } catch (err) {
86
+ } catch (/** @type {any} */ err) {
72
87
  return { ok: false, error: `撤销失败:${err?.message || err}` };
73
88
  }
74
89
  }
@@ -87,7 +102,7 @@ export function undo(args, ctx) {
87
102
  try {
88
103
  fs.writeFileSync(latestFile, last.content);
89
104
  return { ok: true, output: `已撤销 ${latestFile} 的最近一次修改。` };
90
- } catch (err) {
105
+ } catch (/** @type {any} */ err) {
91
106
  return { ok: false, error: `撤销失败:${err?.message || err}` };
92
107
  }
93
108
  }
@@ -95,10 +110,17 @@ export function undo(args, ctx) {
95
110
  const READ_CACHE_MAX = 200; // 会话级 read 缓存条数上限(超出清最旧,防止无界增长)
96
111
  const readCache = new Map(); // 绝对路径 → { mtimeMs, size, lines }
97
112
 
113
+ /**
114
+ * @param {any} p
115
+ */
98
116
  export function invalidateReadCache(p) {
99
117
  readCache.delete(p);
100
118
  }
101
119
 
120
+ /**
121
+ * @param {any} args
122
+ * @param {any} ctx
123
+ */
102
124
  export function read(args, ctx) {
103
125
  try {
104
126
  const p = resolvePath(ctx.cwd, args.path ?? '');
@@ -145,13 +167,17 @@ export function read(args, ctx) {
145
167
  out.join('\n') + (truncated ? `\n…[共 ${lines.length} 行,已显示第 ${offset}-${end} 行]` : ''),
146
168
  totalLines: lines.length,
147
169
  };
148
- } catch (err) {
170
+ } catch (/** @type {any} */ err) {
149
171
  return { ok: false, error: `读取失败:${err?.message || err}` };
150
172
  }
151
173
  }
152
174
 
153
175
  const MAX_WRITE_BYTES = 2 * 1024 * 1024; // write 单次内容上限(防模型输出超大文件打爆磁盘)
154
176
 
177
+ /**
178
+ * @param {any} args
179
+ * @param {any} ctx
180
+ */
155
181
  export function write(args, ctx) {
156
182
  try {
157
183
  const p = resolvePath(ctx.cwd, args.path ?? '');
@@ -171,12 +197,17 @@ export function write(args, ctx) {
171
197
  fs.writeFileSync(p, content);
172
198
  invalidateReadCache(p);
173
199
  return { ok: true, output: `已写入 ${p}(${Buffer.byteLength(content)} 字节)。` };
174
- } catch (err) {
200
+ } catch (/** @type {any} */ err) {
175
201
  return { ok: false, error: `写入失败:${err?.message || err}` };
176
202
  }
177
203
  }
178
204
 
179
205
  // 提取替换区域(含上下文),供 TUI 渲染变更预览
206
+ /**
207
+ * @param {any} text
208
+ * @param {any} lineStart
209
+ * @param {any} lineCount
210
+ */
180
211
  function regionAround(text, lineStart, lineCount, context = 2) {
181
212
  const lines = text.split('\n');
182
213
  const from = Math.max(0, lineStart - context);
@@ -184,6 +215,10 @@ function regionAround(text, lineStart, lineCount, context = 2) {
184
215
  return lines.slice(from, to).join('\n');
185
216
  }
186
217
 
218
+ /**
219
+ * @param {any} args
220
+ * @param {any} ctx
221
+ */
187
222
  export function edit(args, ctx) {
188
223
  try {
189
224
  const p = resolvePath(ctx.cwd, args.path ?? '');
@@ -218,11 +253,15 @@ export function edit(args, ctx) {
218
253
  output: `已编辑 ${p}(替换 ${count} 处)。`,
219
254
  diff: { before, after },
220
255
  };
221
- } catch (err) {
256
+ } catch (/** @type {any} */ err) {
222
257
  return { ok: false, error: `编辑失败:${err?.message || err}` };
223
258
  }
224
259
  }
225
260
 
261
+ /**
262
+ * @param {any} args
263
+ * @param {any} ctx
264
+ */
226
265
  export function ls(args, ctx) {
227
266
  try {
228
267
  const p = resolvePath(ctx.cwd, args.path || '.');
@@ -243,11 +282,14 @@ export function ls(args, ctx) {
243
282
  return ad - bd || a.localeCompare(b);
244
283
  });
245
284
  return { ok: true, output: rows.join('\n') || '(空目录)' };
246
- } catch (err) {
285
+ } catch (/** @type {any} */ err) {
247
286
  return { ok: false, error: `列目录失败:${err?.message || err}` };
248
287
  }
249
288
  }
250
289
 
290
+ /**
291
+ * @param {any} pattern
292
+ */
251
293
  function globToRegExp(pattern) {
252
294
  let re = '';
253
295
  for (let i = 0; i < pattern.length; i++) {
@@ -275,6 +317,10 @@ function globToRegExp(pattern) {
275
317
  return new RegExp('^' + re + '$');
276
318
  }
277
319
 
320
+ /**
321
+ * @param {any} root
322
+ * @param {(full: any) => any} visitor
323
+ */
278
324
  function walkFiles(root, visitor) {
279
325
  const stack = [root];
280
326
  let scanned = 0;
@@ -300,6 +346,10 @@ function walkFiles(root, visitor) {
300
346
  return scanned;
301
347
  }
302
348
 
349
+ /**
350
+ * @param {any} args
351
+ * @param {any} ctx
352
+ */
303
353
  export function glob(args, ctx) {
304
354
  try {
305
355
  const pattern = String(args.pattern ?? '*');
@@ -307,6 +357,7 @@ export function glob(args, ctx) {
307
357
  if (!fs.existsSync(root)) return { ok: false, error: `目录不存在:${root}` };
308
358
  const re = globToRegExp(pattern);
309
359
  const matchRel = pattern.includes('/');
360
+ /** @type {any[]} */
310
361
  const results = [];
311
362
  let truncated = false;
312
363
  walkFiles(root, (full) => {
@@ -321,11 +372,15 @@ export function glob(args, ctx) {
321
372
  if (!results.length) output = '(无匹配)';
322
373
  if (truncated) output += `\n…[结果超过 ${MAX_GLOB_RESULTS} 条,已截断]`;
323
374
  return { ok: true, output };
324
- } catch (err) {
375
+ } catch (/** @type {any} */ err) {
325
376
  return { ok: false, error: `glob 失败:${err?.message || err}` };
326
377
  }
327
378
  }
328
379
 
380
+ /**
381
+ * @param {any} args
382
+ * @param {any} ctx
383
+ */
329
384
  export function grep(args, ctx) {
330
385
  try {
331
386
  const pattern = String(args.pattern ?? '');
@@ -338,14 +393,16 @@ export function grep(args, ctx) {
338
393
  let re;
339
394
  try {
340
395
  re = new RegExp(pattern);
341
- } catch (e) {
396
+ } catch (/** @type {any} */ e) {
342
397
  return { ok: false, error: `无效的正则表达式:${e.message}` };
343
398
  }
344
399
  // 单行截断:超长行(如压缩的 JS/日志)截到 20KB 再匹配,防止正则开销失控
345
400
  const MAX_LINE = 20 * 1024;
401
+ /** @type {(line: any) => any} */
346
402
  const testLine = (line) => (line.length > MAX_LINE ? re.test(line.slice(0, MAX_LINE)) : re.test(line));
347
403
  const root = resolvePath(ctx.cwd, args.path || '.');
348
404
  const include = args.include ? globToRegExp(String(args.include)) : null;
405
+ /** @type {any[]} */
349
406
  const matches = [];
350
407
  let truncated = false;
351
408
  let scannedFiles = 0;
@@ -386,7 +443,7 @@ export function grep(args, ctx) {
386
443
  ? `\n…[匹配超过 ${MAX_GREP_MATCHES} 条,已截断;已扫描 ${scannedFiles} 个文件]`
387
444
  : `\n[已扫描 ${scannedFiles} 个文件]`;
388
445
  return { ok: true, output };
389
- } catch (err) {
446
+ } catch (/** @type {any} */ err) {
390
447
  return { ok: false, error: `grep 失败:${err?.message || err}` };
391
448
  }
392
449
  }
@@ -252,10 +252,10 @@ export function buildToolSchemas(usedNames, extra = []) {
252
252
  return [...strip(TOOLS), ...strip(extra)];
253
253
  }
254
254
 
255
- function runSkill(args, ctx) {
255
+ function runSkill(/** @type {any} */ args, /** @type {any} */ ctx) {
256
256
  const name = String(args.name ?? '').trim();
257
257
  const skills = listSkills(ctx.workingDir);
258
- const label = (s) => `${s.name}${s.source === 'user' ? '(用户级)' : s.source === 'builtin' ? '(内置)' : ''}`;
258
+ const label = (/** @type {any} */ s) => `${s.name}${s.source === 'user' ? '(用户级)' : s.source === 'builtin' ? '(内置)' : ''}`;
259
259
  if (!name) {
260
260
  return {
261
261
  ok: true,
@@ -271,7 +271,7 @@ function runSkill(args, ctx) {
271
271
  return { ok: true, output: `技能 ${name}${s.source === 'builtin' ? '(内置)' : ''}(${s.path}):\n\n${s.content}` };
272
272
  }
273
273
 
274
- async function runTask(args, ctx) {
274
+ async function runTask(/** @type {any} */ args, /** @type {any} */ ctx) {
275
275
  const prompt = String(args.prompt ?? '');
276
276
  const description = String(args.description ?? '');
277
277
  if (!prompt) return { ok: false, error: '缺少 prompt 参数。' };
@@ -280,11 +280,11 @@ async function runTask(args, ctx) {
280
280
  const out = await ctx.spawnTask(prompt, { description, readOnly: args.readOnly === true });
281
281
  return { ok: true, output: out };
282
282
  } catch (err) {
283
- return { ok: false, error: `子任务失败:${err?.message || err}` };
283
+ return { ok: false, error: `子任务失败:${(/** @type {any} */ (err))?.message || err}` };
284
284
  }
285
285
  }
286
286
 
287
- function runTodo(args, ctx) {
287
+ function runTodo(/** @type {any} */ args, /** @type {any} */ ctx) {
288
288
  const list = Array.isArray(args.todos) ? args.todos : [];
289
289
  if (Array.isArray(ctx.todos)) {
290
290
  ctx.todos.splice(0, ctx.todos.length, ...list);
@@ -293,12 +293,12 @@ function runTodo(args, ctx) {
293
293
  ok: true,
294
294
  output:
295
295
  `任务清单已更新(${list.length} 项):\n` +
296
- list.map((t) => `- [${t.status || 'pending'}] ${t.content}`).join('\n'),
296
+ list.map((/** @type {any} */ t) => `- [${t.status || 'pending'}] ${t.content}`).join('\n'),
297
297
  todos: list,
298
298
  };
299
299
  }
300
300
 
301
- export async function dispatch(name, args, ctx) {
301
+ export async function dispatch(/** @type {any} */ name, /** @type {any} */ args, /** @type {any} */ ctx) {
302
302
  switch (name) {
303
303
  case 'read':
304
304
  return read(args, ctx);
package/src/ui.js CHANGED
@@ -26,6 +26,10 @@ export const C = {
26
26
 
27
27
  const stdoutTTY = () => Boolean(process.stdout.isTTY);
28
28
 
29
+ /**
30
+ * @param {any} text
31
+ * @param {any} code
32
+ */
29
33
  export function style(text, code) {
30
34
  if (!stdoutTTY()) {
31
35
  // 非 TTY(管道/重定向):剥离所有内嵌 ANSI 码,保证输出干净可解析
@@ -35,10 +39,11 @@ export function style(text, code) {
35
39
  }
36
40
 
37
41
  // ---------- 终端显示宽度(CJK 占 2 列) ----------
42
+ /** @param {any} s */
38
43
  function displayWidth(s) {
39
44
  let w = 0;
40
45
  for (const ch of String(s)) {
41
- const c = ch.codePointAt(0);
46
+ const c = /** @type {any} */ (ch.codePointAt(0));
42
47
  const wide =
43
48
  c >= 0x1100 &&
44
49
  (c <= 0x115f ||
@@ -57,10 +62,18 @@ function displayWidth(s) {
57
62
  return w;
58
63
  }
59
64
 
65
+ /**
66
+ * @param {any} s
67
+ * @param {any} w
68
+ */
60
69
  function padTo(s, w) {
61
70
  return String(s) + ' '.repeat(Math.max(0, w - displayWidth(s)));
62
71
  }
63
72
 
73
+ /**
74
+ * @param {any} s
75
+ * @param {any} prefix
76
+ */
64
77
  function indent(s, prefix) {
65
78
  return String(s)
66
79
  .split('\n')
@@ -70,6 +83,7 @@ function indent(s, prefix) {
70
83
 
71
84
  // ---------- 轻量代码高亮 ----------
72
85
  const HASH_LANGS = new Set(['py', 'python', 'sh', 'bash', 'yaml', 'yml', 'toml', 'markdown', 'md']);
86
+ /** @type {Record<string, string[]>} */
73
87
  const LANG_KEYWORDS = {
74
88
  js: ['const', 'let', 'var', 'function', 'return', 'if', 'else', 'for', 'while', 'import', 'from', 'export', 'default', 'class', 'extends', 'new', 'await', 'async', 'try', 'catch', 'finally', 'throw', 'typeof', 'instanceof', 'null', 'undefined', 'true', 'false', 'this', 'super', 'switch', 'case', 'break', 'continue', 'do', 'of', 'in', 'static', 'get', 'set', 'delete', 'void', 'yield'],
75
89
  ts: ['interface', 'type', 'enum', 'implements', 'readonly', 'private', 'public', 'protected', 'namespace', 'declare', 'as', 'any', 'unknown', 'never', 'string', 'number', 'boolean'],
@@ -83,6 +97,10 @@ const LANG_KEYWORDS = {
83
97
  yaml: [],
84
98
  };
85
99
 
100
+ /**
101
+ * @param {any} code
102
+ * @param {any} lang
103
+ */
86
104
  export function highlightCode(code, lang = '') {
87
105
  if (!stdoutTTY()) return code;
88
106
  const kws = new Set([...(LANG_KEYWORDS[lang] || []), ...(LANG_KEYWORDS[lang.replace(/-.*/, '')] || [])]);
@@ -117,6 +135,7 @@ export function highlightCode(code, lang = '') {
117
135
  }
118
136
 
119
137
  // ---------- Markdown 行内样式 ----------
138
+ /** @param {any} s */
120
139
  function inline(s) {
121
140
  const parts = [];
122
141
  const re = /(`[^`]+`|\*\*[^*]+\*\*|\*[^*\s][^*]*\*)/g;
@@ -134,6 +153,7 @@ function inline(s) {
134
153
  return parts.join('');
135
154
  }
136
155
 
156
+ /** @param {any} line */
137
157
  function renderInline(line) {
138
158
  const trimmed = line.trim();
139
159
  if (/^#{1,6}\s/.test(trimmed)) return style(inline(line), C.bold + C.cyan);
@@ -145,13 +165,16 @@ function renderInline(line) {
145
165
  }
146
166
 
147
167
  // ---------- 流式渲染器 ----------
168
+ /** @param {any} io */
148
169
  function createStreamRenderer(io) {
149
170
  let buf = '';
150
171
  let inCode = false;
151
172
  let lang = '';
173
+ /** @type {any[]} */
152
174
  let codeLines = [];
153
175
  let streamed = false;
154
176
 
177
+ /** @param {any} line */
155
178
  function renderLine(line) {
156
179
  const trimmed = line.trim();
157
180
  if (trimmed.startsWith('```')) {
@@ -174,6 +197,7 @@ function createStreamRenderer(io) {
174
197
  }
175
198
 
176
199
  return {
200
+ /** @param {any} text */
177
201
  push(text) {
178
202
  streamed = true;
179
203
  buf += String(text);
@@ -202,6 +226,10 @@ function createStreamRenderer(io) {
202
226
  }
203
227
 
204
228
  // ---------- 行级 diff(编辑预览用,仅小区域) ----------
229
+ /**
230
+ * @param {any} a
231
+ * @param {any} b
232
+ */
205
233
  function diffLines(a, b) {
206
234
  const n = a.length;
207
235
  const m = b.length;
@@ -253,6 +281,7 @@ const COMMANDS = [
253
281
  '/quit',
254
282
  ];
255
283
 
284
+ /** @param {any} line */
256
285
  function completeLine(line) {
257
286
  if (line.startsWith('/model ') && line.length >= 7) {
258
287
  const pre = line.slice(7);
@@ -261,6 +290,10 @@ function completeLine(line) {
261
290
  return [COMMANDS.filter((c) => c.startsWith(line)), line];
262
291
  }
263
292
 
293
+ /**
294
+ * @param {any} name
295
+ * @param {any} args
296
+ */
264
297
  function summarizeArgs(name, args) {
265
298
  try {
266
299
  if (name === 'bash') return ` ${args.command ?? ''}`;
@@ -274,12 +307,19 @@ function summarizeArgs(name, args) {
274
307
 
275
308
  // ---------- io 工厂 ----------
276
309
  export function createIO({ quiet = false } = {}) {
310
+ /** @type {any} */
277
311
  let rl = null;
312
+ /** @type {any} */
278
313
  let spinnerTimer = null;
314
+ /** @type {any} */
279
315
  let renderer = null;
316
+ /** @type {any[]} */
280
317
  let askQueue = []; // 非 TTY:挂起的输入请求
318
+ /** @type {any[]} */
281
319
  let lineQueue = []; // 非 TTY:已缓冲但未被消费的行
320
+ /** @type {any} */
282
321
  let sigintHandler = null; // TTY 下 rl 'SIGINT' 事件转发目标
322
+ /** @type {any} */
283
323
  let ttyPending = null;
284
324
  let closed = false;
285
325
 
@@ -291,6 +331,7 @@ export function createIO({ quiet = false } = {}) {
291
331
  isTTY: Boolean(process.stdin.isTTY),
292
332
  showReasoning: true,
293
333
 
334
+ /** @param {any} v */
294
335
  setShowReasoning(v) {
295
336
  io.showReasoning = !!v;
296
337
  },
@@ -304,7 +345,7 @@ export function createIO({ quiet = false } = {}) {
304
345
  output: process.stdout,
305
346
  terminal,
306
347
  historySize: 200,
307
- completer: (line) => completeLine(line),
348
+ completer: (/** @type {any} */ line) => completeLine(line),
308
349
  });
309
350
  if (terminal) {
310
351
  // 关键:TTY 下 Ctrl+C 由 readline 捕获(raw 模式),需要在这里转发给中断处理器;
@@ -321,7 +362,7 @@ export function createIO({ quiet = false } = {}) {
321
362
  }
322
363
  if (!terminal) {
323
364
  // 非 TTY(管道输入):持久 line 监听 + 队列,避免 rl.question 丢弃首行之后的输入
324
- rl.on('line', (line) => {
365
+ rl.on('line', (/** @type {any} */ line) => {
325
366
  const trimmed = line.trim();
326
367
  const pending = askQueue.shift();
327
368
  if (pending) pending.resolve(trimmed);
@@ -344,6 +385,7 @@ export function createIO({ quiet = false } = {}) {
344
385
  return rl;
345
386
  },
346
387
 
388
+ /** @param {any} lines */
347
389
  setHistory(lines) {
348
390
  try {
349
391
  const r = io.ensureRl();
@@ -357,6 +399,10 @@ export function createIO({ quiet = false } = {}) {
357
399
  if (!quiet) console.log(text);
358
400
  },
359
401
 
402
+ /**
403
+ * @param {any} title
404
+ * @param {any} lines
405
+ */
360
406
  box(title, lines) {
361
407
  if (quiet) return;
362
408
  if (!stdoutTTY()) {
@@ -389,6 +435,7 @@ export function createIO({ quiet = false } = {}) {
389
435
  io.stopSpinner();
390
436
  },
391
437
 
438
+ /** @param {any} label */
392
439
  startSpinner(label) {
393
440
  if (quiet || !stdoutTTY()) return;
394
441
  io.stopSpinner();
@@ -408,6 +455,7 @@ export function createIO({ quiet = false } = {}) {
408
455
  }
409
456
  },
410
457
 
458
+ /** @param {any} text */
411
459
  writeText(text) {
412
460
  if (quiet) return;
413
461
  if (io._reasoningStarted && !io._answerStarted && stdoutTTY()) {
@@ -417,6 +465,7 @@ export function createIO({ quiet = false } = {}) {
417
465
  renderer?.push(String(text));
418
466
  },
419
467
 
468
+ /** @param {any} text */
420
469
  writeReasoning(text) {
421
470
  if (quiet || !io.showReasoning) return;
422
471
  if (!io._reasoningStarted) {
@@ -426,6 +475,7 @@ export function createIO({ quiet = false } = {}) {
426
475
  process.stdout.write(style(String(text), C.dim));
427
476
  },
428
477
 
478
+ /** @param {any} todos */
429
479
  renderTodo(todos) {
430
480
  if (quiet || !todos?.length) return;
431
481
  io.print(style(' ☑ 任务清单', C.bold));
@@ -436,6 +486,10 @@ export function createIO({ quiet = false } = {}) {
436
486
  }
437
487
  },
438
488
 
489
+ /**
490
+ * @param {any} code
491
+ * @param {any} lang
492
+ */
439
493
  printCodeBlock(code, lang) {
440
494
  if (quiet) return;
441
495
  const label = lang || 'code';
@@ -446,6 +500,12 @@ export function createIO({ quiet = false } = {}) {
446
500
  },
447
501
 
448
502
  // —— 工具执行可视化 ——
503
+ /**
504
+ * @param {any} name
505
+ * @param {any} args
506
+ * @param {any} result
507
+ * @param {any} durationMs
508
+ */
449
509
  renderTool(name, args, result, durationMs) {
450
510
  if (quiet) return;
451
511
  const r = result && typeof result === 'object' ? result : { ok: false, error: String(result ?? '') };
@@ -525,6 +585,11 @@ export function createIO({ quiet = false } = {}) {
525
585
  io.print(style(` ✓ 输出 ${out.length} 字符${ms}`, C.green));
526
586
  },
527
587
 
588
+ /**
589
+ * @param {any} name
590
+ * @param {any} args
591
+ * @param {any} reason
592
+ */
528
593
  renderToolDenied(name, args, reason) {
529
594
  if (quiet) return;
530
595
  const label = reason === '未授权' || !reason ? '未授权,已跳过' : reason;
@@ -532,11 +597,12 @@ export function createIO({ quiet = false } = {}) {
532
597
  },
533
598
 
534
599
  // —— 用量与费用状态行 ——
600
+ /** @param {{ modelName: any, usage: any, durationMs: any }} param0 */
535
601
  printUsageLine({ modelName, usage, durationMs }) {
536
602
  if (quiet) return;
537
603
  const p = usage?.prompt_tokens ?? 0;
538
604
  const c = usage?.completion_tokens ?? 0;
539
- const fmt = (n) => (n >= 1000 ? (n / 1000).toFixed(1) + 'k' : String(n));
605
+ const fmt = (/** @type {any} */ n) => (n >= 1000 ? (n / 1000).toFixed(1) + 'k' : String(n));
540
606
  const secs = ((durationMs ?? 0) / 1000).toFixed(1) + 's';
541
607
  const costPart = estimateCostLabel(modelName, p, c, usage);
542
608
  io.print(
@@ -550,6 +616,7 @@ export function createIO({ quiet = false } = {}) {
550
616
  // —— SIGINT(Ctrl+C 中断生成,不退出程序) ——
551
617
  // TTY 下 readline raw 模式会把 Ctrl+C 转成 rl 的 'SIGINT' 事件(ensureRl 转发到 sigintHandler),
552
618
  // 非 TTY/其他来源走 process 级 SIGINT;两层都注册,保证任何路径都能中断
619
+ /** @param {any} fn */
553
620
  onSigint(fn) {
554
621
  const h = () => {
555
622
  try {
@@ -564,6 +631,10 @@ export function createIO({ quiet = false } = {}) {
564
631
  };
565
632
  },
566
633
 
634
+ /**
635
+ * @param {any} question
636
+ * @param {any} opts
637
+ */
567
638
  ask(question, opts = {}) {
568
639
  if (!process.stdin.isTTY) {
569
640
  // 非 TTY:优先消费已缓冲的行(接口关闭后仍可能有剩余行),不打印提示符
@@ -592,7 +663,7 @@ export function createIO({ quiet = false } = {}) {
592
663
  }
593
664
  const entry = { resolve, restore };
594
665
  ttyPending = entry;
595
- r.question(question, (answer) => {
666
+ r.question(question, (/** @type {any} */ answer) => {
596
667
  restore();
597
668
  if (ttyPending === entry) ttyPending = null;
598
669
  resolve(answer.trim());
@@ -601,6 +672,7 @@ export function createIO({ quiet = false } = {}) {
601
672
  },
602
673
 
603
674
  // 行尾反斜杠续行,支持多行输入
675
+ /** @param {any} prompt */
604
676
  async askMultiline(prompt, continuation = '…> ') {
605
677
  const lines = [];
606
678
  for (;;) {
@@ -615,14 +687,19 @@ export function createIO({ quiet = false } = {}) {
615
687
  }
616
688
  },
617
689
 
690
+ /** @param {any} question */
618
691
  async confirm(question) {
619
692
  const a = await io.ask(question + ' ');
620
693
  return /^y(es)?$/i.test(a);
621
694
  },
622
695
 
696
+ /**
697
+ * @param {any} label
698
+ * @param {any} options
699
+ */
623
700
  async choose(label, options) {
624
701
  io.print(style(label, C.bold));
625
- options.forEach((o, i) => io.print(` ${style(String(i + 1), C.cyan)}. ${o.label}`));
702
+ options.forEach((/** @type {any} */ o, /** @type {any} */ i) => io.print(` ${style(String(i + 1), C.cyan)}. ${o.label}`));
626
703
  for (;;) {
627
704
  const ans = await io.ask('请输入序号:');
628
705
  const n = Number(ans);