mingdao-harness 0.2.4 → 0.2.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 (61) hide show
  1. package/docs/ROADMAP-NEXT.md +5 -5
  2. package/package.json +1 -1
  3. package/src/agent.js +19 -19
  4. package/src/audit.js +2 -2
  5. package/src/batch.js +7 -7
  6. package/src/cachestats.js +9 -9
  7. package/src/cli.js +29 -679
  8. package/src/commands/desktop.js +1 -1
  9. package/src/commands/key.js +1 -1
  10. package/src/commands/repl.js +711 -0
  11. package/src/commands/schedule.js +7 -7
  12. package/src/commands/skill.js +18 -16
  13. package/src/commands/sync.js +5 -5
  14. package/src/commands/update.js +7 -6
  15. package/src/commands/workspace.js +2 -2
  16. package/src/compact.js +6 -6
  17. package/src/config.js +7 -4
  18. package/src/context.js +11 -11
  19. package/src/credentials.js +6 -0
  20. package/src/hooks.js +11 -11
  21. package/src/mcp-presets.js +5 -5
  22. package/src/mcp.js +19 -19
  23. package/src/memory.js +16 -16
  24. package/src/model-discovery.js +16 -15
  25. package/src/models.js +4 -4
  26. package/src/notify.js +2 -2
  27. package/src/permissions.js +7 -3
  28. package/src/pricing.js +35 -1
  29. package/src/prompts.js +1 -1
  30. package/src/providers/index.js +7 -7
  31. package/src/providers/openai-compatible.js +8 -8
  32. package/src/routing.js +5 -5
  33. package/src/schedule.js +28 -28
  34. package/src/session-index.js +5 -1
  35. package/src/session.js +11 -2
  36. package/src/skill-lib.js +62 -3
  37. package/src/skill-registry.js +10 -9
  38. package/src/skills.js +10 -10
  39. package/src/sync-server.js +86 -3
  40. package/src/sync.js +53 -20
  41. package/src/tasks/worker.js +126 -0
  42. package/src/tasks.js +13 -13
  43. package/src/titles.js +5 -5
  44. package/src/tokenizer.js +22 -4
  45. package/src/tools/bash.js +12 -12
  46. package/src/tools/fs-tools.js +66 -9
  47. package/src/tools/index.js +7 -7
  48. package/src/ui.js +83 -6
  49. package/src/update.js +8 -8
  50. package/src/web/attachments.js +1 -1
  51. package/src/web/routes/api.js +38 -786
  52. package/src/web/routes/domains/config.js +280 -0
  53. package/src/web/routes/domains/misc.js +128 -0
  54. package/src/web/routes/domains/schedule.js +127 -0
  55. package/src/web/routes/domains/sessions.js +105 -0
  56. package/src/web/routes/domains/skills.js +79 -0
  57. package/src/web/routes/domains/sync.js +101 -0
  58. package/src/web/routes/domains/workspace.js +113 -0
  59. package/src/web/server.js +29 -14
  60. package/src/web/web-io.js +54 -12
  61. package/src/workspace.js +14 -14
@@ -6,9 +6,9 @@
6
6
  * @typedef {Error & { status?: number, headers?: Headers }} ApiError
7
7
  */
8
8
 
9
- export async function chat({ baseUrl, apiKey, model, messages, tools, temperature, maxTokens, signal, onDelta, includeUsage = true, responseFormat, reasoningEffort }) {
9
+ export async function chat(/** @type {any} */ { baseUrl, apiKey, model, messages, tools, temperature, maxTokens, signal, onDelta, includeUsage = true, responseFormat, reasoningEffort }) {
10
10
  const url = String(baseUrl).replace(/\/+$/, '') + '/chat/completions';
11
- const payload = { model, messages };
11
+ const payload = /** @type {Record<string, any>} */ ({ model, messages });
12
12
  if (temperature != null) payload.temperature = temperature;
13
13
  if (tools && tools.length) {
14
14
  payload.tools = tools;
@@ -36,7 +36,7 @@ export async function chat({ baseUrl, apiKey, model, messages, tools, temperatur
36
36
  });
37
37
  } catch (err) {
38
38
  /** @type {ApiError} */
39
- const e = new Error(`网络请求失败:${err?.message || err}`);
39
+ const e = new Error(`网络请求失败:${(/** @type {any} */ (err))?.message || err}`);
40
40
  e.status = 0;
41
41
  throw e;
42
42
  }
@@ -64,7 +64,7 @@ export async function chat({ baseUrl, apiKey, model, messages, tools, temperatur
64
64
  return parseStream(res.body, onDelta);
65
65
  }
66
66
 
67
- export function parseNonStream(json, onDelta) {
67
+ export function parseNonStream(/** @type {any} */ json, /** @type {any} */ onDelta) {
68
68
  const choice = json?.choices?.[0];
69
69
  const msg = choice?.message ?? {};
70
70
  if (msg.content) onDelta?.({ text: msg.content });
@@ -80,7 +80,7 @@ export function parseNonStream(json, onDelta) {
80
80
  }
81
81
 
82
82
  // 解析 SSE 流:处理跨 chunk 断行、增量 content / reasoning_content / tool_calls。
83
- export async function parseStream(body, onDelta) {
83
+ export async function parseStream(/** @type {any} */ body, /** @type {any} */ onDelta) {
84
84
  const reader = body.getReader();
85
85
  const decoder = new TextDecoder();
86
86
  let buf = '';
@@ -90,15 +90,15 @@ export async function parseStream(body, onDelta) {
90
90
  const indexKeys = new Map(); // index -> key(id 只在首片出现,后续分片按 index 找回同一条目)
91
91
  let autoKey = 0;
92
92
  let usage = null;
93
- let finish = null;
93
+ let /** @type {any} */ finish = null;
94
94
 
95
95
  let doneFlag = false;
96
- const handleLine = (lineRaw) => {
96
+ const handleLine = (/** @type {any} */ lineRaw) => {
97
97
  const line = lineRaw.trim();
98
98
  if (!line.startsWith('data:')) return;
99
99
  const data = line.slice(5).trim();
100
100
  if (data === '[DONE]') {
101
- finish = finish || 'stop';
101
+ finish = /** @type {any} */ finish || 'stop';
102
102
  doneFlag = true;
103
103
  return true; // 通知外层终止读取(审计 P2-4:[DONE] 后不再消费残帧、不挂到超时)
104
104
  }
package/src/routing.js CHANGED
@@ -9,7 +9,7 @@
9
9
  import crypto from 'node:crypto';
10
10
  import { modelPreset } from './models.js';
11
11
 
12
- export function routingConfig(cfg) {
12
+ export function routingConfig(/** @type {any} */ cfg) {
13
13
  const r = cfg?.routing;
14
14
  if (!r || r.enabled === false) return null;
15
15
  const planner = r.planner || 'deepseek-v4-pro';
@@ -25,7 +25,7 @@ const PLAN_HINTS =
25
25
  const GENERATION_HINTS =
26
26
  /(生成|制作|编写|创建|写|开发|实现|做).{0,30}(游戏|网页|页面|网站|应用|程序|文档|报告|简历|PPT|演示|完整|详细|小工具)|网页版|游戏/;
27
27
 
28
- export function heuristicRoute(text, rc) {
28
+ export function heuristicRoute(/** @type {any} */ text, /** @type {any} */ rc) {
29
29
  const s = String(text ?? '');
30
30
  if (GENERATION_HINTS.test(s)) return rc.planner; // 生成类:大输出优先
31
31
  if (PLAN_HINTS.test(s)) {
@@ -41,7 +41,7 @@ export function heuristicRoute(text, rc) {
41
41
  const ROUTE_CACHE_MAX = 100;
42
42
  const routeCache = new Map();
43
43
 
44
- export async function routeTask({ cfg, provider, currentModel, text, sticky = null, sessionStats = null }) {
44
+ export async function routeTask(/** @type {any} */ { cfg, provider, currentModel, text, sticky = null, sessionStats = null }) {
45
45
  const rc = routingConfig(cfg);
46
46
  if (!rc) return { model: currentModel, reason: null };
47
47
  // 当前模型已在路由池外(用户手动指定):不干预
@@ -59,7 +59,7 @@ export async function routeTask({ cfg, provider, currentModel, text, sticky = nu
59
59
  // 会话粘滞 + 升级检测(Hermes C2):执行类会话不再逐轮分类(生成类关键词已在启发式拦截),
60
60
  // 但会话内复杂度信号累积(工具步数/截断次数)时允许升级 planner——「开头简单、中途复杂」不再被 flash 硬扛。
61
61
  if (sticky === rc.executor) {
62
- const stats = sessionStats || {};
62
+ const stats = /** @type {any} */ (sessionStats || {});
63
63
  const steps = Number(stats.steps) || 0;
64
64
  const truncated = Number(stats.truncated) || 0;
65
65
  const UPGRADE_STEPS = Number(cfg?.routing?.upgradeSteps) || 10;
@@ -140,7 +140,7 @@ export async function routeTask({ cfg, provider, currentModel, text, sticky = nu
140
140
  }
141
141
 
142
142
  // 子代理模型选择:路由开启时固定 executor
143
- export function subagentModel(cfg, currentModel) {
143
+ export function subagentModel(/** @type {any} */ cfg, /** @type {any} */ currentModel) {
144
144
  const rc = routingConfig(cfg);
145
145
  if (!rc) return currentModel;
146
146
  return modelPreset(rc.executor) ? rc.executor : currentModel;
package/src/schedule.js CHANGED
@@ -11,7 +11,7 @@ import { spawn } from 'node:child_process';
11
11
  import { fileURLToPath } from 'node:url';
12
12
  import { startTask, readTask, killTask } from './tasks.js';
13
13
 
14
- function isRunningTask(home, taskId) {
14
+ function isRunningTask(/** @type {any} */ home, /** @type {any} */ taskId) {
15
15
  const t = readTask(home, taskId);
16
16
  return Boolean(t && t.status === 'running');
17
17
  }
@@ -21,21 +21,21 @@ const CLI_PATH = fileURLToPath(new URL('./cli.js', import.meta.url));
21
21
  import { isPeakHour, deferToOffpeak } from './pricing.js';
22
22
  import { atomicWriteFileSync, withFileLockSync } from './atomic-write.js';
23
23
 
24
- export function scheduleDir(home) {
24
+ export function scheduleDir(/** @type {any} */ home) {
25
25
  return path.join(home, 'schedule');
26
26
  }
27
27
 
28
- export function parseInterval(s) {
28
+ export function parseInterval(/** @type {any} */ s) {
29
29
  const m = /^(\d+)(s|m|h|d)$/.exec(String(s).trim());
30
30
  if (!m) return null;
31
31
  const n = Number(m[1]);
32
- const unit = { s: 1000, m: 60000, h: 3600000, d: 86400000 }[m[2]];
32
+ const unit = /** @type {Record<string, number>} */ ({ s: 1000, m: 60000, h: 3600000, d: 86400000 })[m[2]];
33
33
  if (!(n > 0)) return null;
34
34
  return n * unit;
35
35
  }
36
36
 
37
37
  // 解析 --at:'YYYY-MM-DD HH:MM' 或 'HH:MM'(今天;已过则明天)
38
- export function parseAt(s) {
38
+ export function parseAt(/** @type {any} */ s) {
39
39
  const t = String(s).trim();
40
40
  const full = /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})$/.exec(t);
41
41
  if (full) {
@@ -56,7 +56,7 @@ export function parseAt(s) {
56
56
  return null;
57
57
  }
58
58
 
59
- export function listSchedules(home) {
59
+ export function listSchedules(/** @type {any} */ home) {
60
60
  const dir = scheduleDir(home);
61
61
  let files;
62
62
  try {
@@ -74,11 +74,11 @@ export function listSchedules(home) {
74
74
  return out.sort((a, b) => (a.nextRunAt || 0) - (b.nextRunAt || 0));
75
75
  }
76
76
 
77
- function isValidScheduleId(id) {
77
+ function isValidScheduleId(/** @type {any} */ id) {
78
78
  return typeof id === 'string' && /^[a-z0-9]+$/.test(id) && id.length >= 2 && id.length <= 40;
79
79
  }
80
80
 
81
- export function readSchedule(home, id) {
81
+ export function readSchedule(/** @type {any} */ home, /** @type {any} */ id) {
82
82
  if (!isValidScheduleId(id)) return null; // 防 id 路径穿越
83
83
  try {
84
84
  return JSON.parse(fs.readFileSync(path.join(scheduleDir(home), id + '.json'), 'utf8'));
@@ -87,7 +87,7 @@ export function readSchedule(home, id) {
87
87
  }
88
88
  }
89
89
 
90
- export function writeSchedule(home, job) {
90
+ export function writeSchedule(/** @type {any} */ home, /** @type {any} */ job) {
91
91
  if (!isValidScheduleId(job?.id)) return null;
92
92
  fs.mkdirSync(scheduleDir(home), { recursive: true });
93
93
  const target = path.join(scheduleDir(home), job.id + '.json');
@@ -96,7 +96,7 @@ export function writeSchedule(home, job) {
96
96
  }
97
97
 
98
98
  /** 新建调度任务;after: 依赖的任务 ID(全部成功后才启动,任一失败则跳过) */
99
- export function addSchedule(home, question, /** @type {any} */ { at, every, after, permission, model, cwd, anchor, offpeak }) {
99
+ export function addSchedule(/** @type {any} */ home, /** @type {any} */ question, /** @type {any} */ { at, every, after, permission, model, cwd, anchor, offpeak }) {
100
100
  const id = 'sc' + Date.now().toString(36) + Math.random().toString(36).slice(2, 5);
101
101
  const interval = every != null ? parseInterval(every) : null;
102
102
  const afterList = Array.isArray(after) ? after.filter(Boolean).map(String) : after ? String(after).split(',').map((x) => x.trim()).filter(Boolean) : [];
@@ -147,7 +147,7 @@ export function addSchedule(home, question, /** @type {any} */ { at, every, afte
147
147
  // 质检 H2(pause 失效确定性回归):runOnce 返回后的状态决策抽为纯函数——
148
148
  // 用户在执行期间 pause/remove 的指令(paused/failed/文件已删)绝不被 pending 覆盖;
149
149
  // 连续失败 3 次熔断;否则排下一次(锚点对齐避免逐日漂移)。
150
- export function postRunStatus(cur2, result) {
150
+ export function postRunStatus(/** @type {any} */ cur2, /** @type {any} */ result) {
151
151
  if (!cur2) return null;
152
152
  if (cur2.status === 'paused' || cur2.status === 'failed') return null;
153
153
  if (result !== 'done' && (cur2.consecutiveFailures || 0) >= 3) {
@@ -157,7 +157,7 @@ export function postRunStatus(cur2, result) {
157
157
  return { status: 'pending', nextRunAt: next };
158
158
  }
159
159
 
160
- export function removeSchedule(home, id) {
160
+ export function removeSchedule(/** @type {any} */ home, /** @type {any} */ id) {
161
161
  // 质检 H3:读-改-写序列加锁,与 sleeper 循环的状态写互斥(防丢更新)
162
162
  return withFileLockSync(path.join(scheduleDir(home), '.lock'), () => {
163
163
  const job = readSchedule(home, id);
@@ -179,7 +179,7 @@ export function removeSchedule(home, id) {
179
179
  });
180
180
  }
181
181
 
182
- export function pauseSchedule(home, id) {
182
+ export function pauseSchedule(/** @type {any} */ home, /** @type {any} */ id) {
183
183
  // 质检 H3:读-改-写序列加锁(pause 与 sleeper 的 postRunStatus 写互斥,杜绝 pause 被覆盖)
184
184
  return withFileLockSync(path.join(scheduleDir(home), '.lock'), () => {
185
185
  const job = readSchedule(home, id);
@@ -198,7 +198,7 @@ export function pauseSchedule(home, id) {
198
198
  });
199
199
  }
200
200
 
201
- export function resumeSchedule(home, id) {
201
+ export function resumeSchedule(/** @type {any} */ home, /** @type {any} */ id) {
202
202
  const job = readSchedule(home, id);
203
203
  if (!job || job.status !== 'paused') return false;
204
204
  let next = job.nextRunAt;
@@ -215,7 +215,7 @@ export function resumeSchedule(home, id) {
215
215
  }
216
216
 
217
217
  // 每日锚点:锚点时刻(HH:MM)对齐到 now 之后的最近一次
218
- function nextAnchorAfter(anchor, interval) {
218
+ function nextAnchorAfter(/** @type {any} */ anchor, /** @type {any} */ interval) {
219
219
  const a = parseAt(anchor);
220
220
  if (a == null) return null;
221
221
  let n = a;
@@ -224,7 +224,7 @@ function nextAnchorAfter(anchor, interval) {
224
224
  }
225
225
 
226
226
  // 链式编排:A→B→C,后者依赖前者成功
227
- export function chainSchedules(home, questions, opts = {}) {
227
+ export function chainSchedules(/** @type {any} */ home, /** @type {any} */ questions, opts = {}) {
228
228
  const ids = [];
229
229
  let prev = null;
230
230
  for (const q of questions) {
@@ -237,7 +237,7 @@ export function chainSchedules(home, questions, opts = {}) {
237
237
  return { ids };
238
238
  }
239
239
 
240
- export function sleeperAlive(pid) {
240
+ export function sleeperAlive(/** @type {any} */ pid) {
241
241
  if (!pid) return false;
242
242
  try {
243
243
  process.kill(pid, 0);
@@ -247,7 +247,7 @@ export function sleeperAlive(pid) {
247
247
  }
248
248
  }
249
249
 
250
- function spawnSleeper(home, job) {
250
+ function spawnSleeper(/** @type {any} */ home, /** @type {any} */ job) {
251
251
  const child = spawn(process.execPath, [CLI_PATH, 'schedule-worker', job.id], {
252
252
  detached: true,
253
253
  stdio: 'ignore',
@@ -260,12 +260,12 @@ function spawnSleeper(home, job) {
260
260
  }
261
261
 
262
262
  // —— 单守护进程调度器(评估 P3-5):一进程监督全部任务 ——
263
- export function daemonPidFile(home) {
263
+ export function daemonPidFile(/** @type {any} */ home) {
264
264
  return path.join(scheduleDir(home), 'daemon.pid');
265
265
  }
266
266
  // pid 文件内容 "pid nonce":校验进程存在 + 命令行含同一 nonce,防止陈旧 PID 被无关进程复用而误判(审计 P2-7)
267
267
  // 质检 M11:kill 前校验 PID 归属(/proc/<pid>/cmdline 含 needle 才动手,防 PID 复用误杀)
268
- function pidOwnedBy(pid, needle) {
268
+ function pidOwnedBy(/** @type {any} */ pid, /** @type {any} */ needle) {
269
269
  try {
270
270
  const cmdline = fs.readFileSync(`/proc/${pid}/cmdline`, 'utf8');
271
271
  return cmdline.includes(needle);
@@ -274,7 +274,7 @@ function pidOwnedBy(pid, needle) {
274
274
  }
275
275
  }
276
276
 
277
- export function daemonAlive(home) {
277
+ export function daemonAlive(/** @type {any} */ home) {
278
278
  try {
279
279
  const [pidStr, nonce] = fs.readFileSync(daemonPidFile(home), 'utf8').trim().split(/\s+/);
280
280
  const pid = Number(pidStr);
@@ -291,7 +291,7 @@ export function daemonAlive(home) {
291
291
  return false;
292
292
  }
293
293
  }
294
- export function stopDaemon(home) {
294
+ export function stopDaemon(/** @type {any} */ home) {
295
295
  try {
296
296
  const pid = Number(fs.readFileSync(daemonPidFile(home), 'utf8'));
297
297
  if (pid) {
@@ -305,7 +305,7 @@ export function stopDaemon(home) {
305
305
  } catch {}
306
306
  return true;
307
307
  }
308
- export function spawnDaemon(home) {
308
+ export function spawnDaemon(/** @type {any} */ home) {
309
309
  if (daemonAlive(home)) return true;
310
310
  const nonce = Math.random().toString(36).slice(2, 10);
311
311
  const child = spawn(process.execPath, [CLI_PATH, 'schedule-daemon', nonce], {
@@ -324,7 +324,7 @@ export function spawnDaemon(home) {
324
324
  }
325
325
 
326
326
  // 重启自愈:有非终态任务且 daemon 不在 → 拉起单守护(失败才回退旧式逐任务 sleeper)
327
- export function reconcileSchedules(home) {
327
+ export function reconcileSchedules(/** @type {any} */ home) {
328
328
  const jobs = listSchedules(home);
329
329
  const hasPending = jobs.some((j) => j.status === 'pending' || j.status === 'running');
330
330
  if (!hasPending) return;
@@ -343,14 +343,14 @@ export function reconcileSchedules(home) {
343
343
  spawnDaemon(home);
344
344
  }
345
345
  // —— sleeper 主循环(schedule-worker 进程内运行)——
346
- export async function runSleeper(home, id) {
346
+ export async function runSleeper(/** @type {any} */ home, /** @type {any} */ id) {
347
347
  const job = readSchedule(home, id);
348
348
  if (!job) return;
349
349
 
350
- const wait = (ms) => new Promise((r) => setTimeout(r, ms));
350
+ const wait = (/** @type {any} */ ms) => new Promise((r) => setTimeout(r, ms));
351
351
 
352
352
  // 依赖检查:支持两种依赖——调度任务 id(chain 编排)或后台任务 id(mingdao run 输出)
353
- async function depsSatisfied(deps) {
353
+ async function depsSatisfied(/** @type {any} */ deps) {
354
354
  for (const dep of deps) {
355
355
  const sch = readSchedule(home, dep);
356
356
  if (sch) {
@@ -476,7 +476,7 @@ export async function runSleeper(home, id) {
476
476
  }
477
477
  }
478
478
 
479
- export function formatScheduleRow(j) {
479
+ export function formatScheduleRow(/** @type {any} */ j) {
480
480
  const mark = j.status === 'pending' ? '⏳' : j.status === 'running' ? '▶' : j.status === 'paused' ? '⏸' : j.status === 'done' ? '✓' : '✖';
481
481
  const when =
482
482
  j.kind === 'every'
@@ -25,6 +25,7 @@ export function loadSessionIndex() {
25
25
  }
26
26
  }
27
27
 
28
+ /** @param {any} idx */
28
29
  export function saveSessionIndex(idx) {
29
30
  try {
30
31
  ensureHome();
@@ -34,9 +35,10 @@ export function saveSessionIndex(idx) {
34
35
  }
35
36
 
36
37
  // 分词:ASCII 词(小写)+ 中文 bigram + 中文单字;返回 Map<词, 次数>
38
+ /** @param {any} text */
37
39
  export function tokenize(text) {
38
40
  const terms = new Map();
39
- const add = (t) => {
41
+ const add = (/** @type {any} */ t) => {
40
42
  if (t) terms.set(t, (terms.get(t) || 0) + 1);
41
43
  };
42
44
  const s = String(text ?? '');
@@ -61,6 +63,7 @@ export function tokenize(text) {
61
63
  }
62
64
 
63
65
  // 从会话文件提取可检索文本(user/assistant 内容拼接)
66
+ /** @param {any} file */
64
67
  export function extractSessionText(file) {
65
68
  const parts = [];
66
69
  for (const line of fs.readFileSync(file, 'utf8').split('\n')) {
@@ -79,6 +82,7 @@ export function extractSessionText(file) {
79
82
  }
80
83
 
81
84
  // 增量同步索引(对比 mtime/size,只重算变化的文件;清理已删除条目)
85
+ /** @param {any} home @param {any} sessions */
82
86
  export function syncSessionIndex(home, sessions) {
83
87
  const idx = loadSessionIndex();
84
88
  const dir = path.join(home, 'sessions');
package/src/session.js CHANGED
@@ -4,6 +4,7 @@
4
4
  import fs from 'node:fs';
5
5
  import path from 'node:path';
6
6
 
7
+ /** @param {any} home */
7
8
  export function listSessions(home) {
8
9
  const dir = path.join(home, 'sessions');
9
10
  try {
@@ -20,10 +21,12 @@ export function listSessions(home) {
20
21
  }
21
22
  }
22
23
 
24
+ /** @param {any} home */
23
25
  export function latestSession(home) {
24
26
  return listSessions(home)[0] || null;
25
27
  }
26
28
 
29
+ /** @param {any} home */
27
30
  export function createSession(home) {
28
31
  const dir = path.join(home, 'sessions');
29
32
  fs.mkdirSync(dir, { recursive: true });
@@ -33,18 +36,21 @@ export function createSession(home) {
33
36
  return { file, name: path.basename(file) };
34
37
  }
35
38
 
39
+ /** @param {any} file @param {any} messages */
36
40
  export function appendMessages(file, messages) {
37
41
  if (!messages?.length) return;
38
- const lines = messages.map((m) => JSON.stringify(m)).join('\n') + '\n';
42
+ const lines = messages.map((/** @type {any} */ m) => JSON.stringify(m)).join('\n') + '\n';
39
43
  fs.appendFileSync(file, lines);
40
44
  }
41
45
 
42
46
  // 整文件原子重写:自动压缩后把会话文件同步为压缩形态(否则每次加载历史都会重新触发压缩)
47
+ /** @param {any} file @param {any} messages */
43
48
  export function rewriteSession(file, messages) {
44
- const lines = messages.map((m) => JSON.stringify(m)).join('\n');
49
+ const lines = messages.map((/** @type {any} */ m) => JSON.stringify(m)).join('\n');
45
50
  atomicWriteFileSync(file, lines + (lines ? '\n' : ''), { mode: 0o600 }); // 质检 H4:会话文件原子写
46
51
  }
47
52
 
53
+ /** @param {any} file */
48
54
  export function loadSession(file) {
49
55
  const raw = fs.readFileSync(file, 'utf8');
50
56
  const messages = [];
@@ -61,6 +67,7 @@ export function loadSession(file) {
61
67
  }
62
68
 
63
69
  // 会话预览:第一条用户消息(用于会话选择器)
70
+ /** @param {any} file */
64
71
  export function sessionPreview(file) {
65
72
  try {
66
73
  const raw = fs.readFileSync(file, 'utf8');
@@ -79,6 +86,7 @@ export function sessionPreview(file) {
79
86
  }
80
87
  }
81
88
 
89
+ /** @param {any} mtime */
82
90
  export function relativeTime(mtime) {
83
91
  const diff = Date.now() - mtime;
84
92
  if (diff < 60000) return '刚刚';
@@ -92,6 +100,7 @@ export function relativeTime(mtime) {
92
100
  import { syncSessionIndex, tokenize, extractSessionText } from './session-index.js';
93
101
  import { atomicWriteFileSync } from './atomic-write.js';
94
102
 
103
+ /** @param {any} home @param {any} keyword */
95
104
  export function searchSessions(home, keyword, { limit = 20 } = {}) {
96
105
  const kw = String(keyword ?? '').trim();
97
106
  if (!kw) return listSessions(home).slice(0, limit);
package/src/skill-lib.js CHANGED
@@ -22,8 +22,13 @@ const LIB_DIR = fileURLToPath(new URL('../skills-lib', import.meta.url));
22
22
  // ---------- 完整性(P3-3):技能目录联合哈希 ----------
23
23
  // 对目录内全部文件(排除 .mingdao-source.json 自身)按相对路径排序,
24
24
  // 逐文件 sha256 后拼成 "rel:hash\n" 再取一次 sha256,作为目录内容指纹。
25
+ /**
26
+ * @param {any} dir
27
+ */
25
28
  export function skillDirHash(dir) {
29
+ /** @type {any[]} */
26
30
  const out = [];
31
+ /** @type {(d: any, base: any) => void} */
27
32
  const walk = (d, base) => {
28
33
  let entries;
29
34
  try {
@@ -47,6 +52,9 @@ export function skillDirHash(dir) {
47
52
  .digest('hex');
48
53
  }
49
54
 
55
+ /**
56
+ * @param {any} dir
57
+ */
50
58
  export function readSourceMeta(dir) {
51
59
  try {
52
60
  return JSON.parse(fs.readFileSync(path.join(dir, '.mingdao-source.json'), 'utf8'));
@@ -55,11 +63,18 @@ export function readSourceMeta(dir) {
55
63
  }
56
64
  }
57
65
 
66
+ /**
67
+ * @param {any} dir
68
+ * @param {any} meta
69
+ */
58
70
  export function writeSourceMeta(dir, meta) {
59
71
  fs.writeFileSync(path.join(dir, '.mingdao-source.json'), JSON.stringify(meta, null, 2) + '\n', { mode: 0o600 });
60
72
  }
61
73
 
62
74
  // 显式信任当前内容(用户改过 registry/library 安装的技能后重新记录指纹)
75
+ /**
76
+ * @param {any} name
77
+ */
63
78
  export function trustSkill(name) {
64
79
  const target = path.join(userSkillsDir(), name);
65
80
  const meta = readSourceMeta(target);
@@ -79,6 +94,10 @@ export function userSkillsDir() {
79
94
  }
80
95
 
81
96
  // 从技能目录读取 name / description(frontmatter 优先,回退标题/目录名)
97
+ /**
98
+ * @param {any} dir
99
+ * @returns {any}
100
+ */
82
101
  function readSkillMeta(dir) {
83
102
  try {
84
103
  const text = fs.readFileSync(path.join(dir, 'SKILL.md'), 'utf8');
@@ -116,6 +135,7 @@ export function installedUserSkillNames() {
116
135
 
117
136
  export function libraryList() {
118
137
  const installed = installedUserSkillNames();
138
+ /** @type {any[]} */
119
139
  const out = [];
120
140
  let entries;
121
141
  try {
@@ -132,6 +152,9 @@ export function libraryList() {
132
152
  return out.sort((a, b) => a.name.localeCompare(b.name));
133
153
  }
134
154
 
155
+ /**
156
+ * @param {any} kw
157
+ */
135
158
  export function searchLibrary(kw) {
136
159
  const k = String(kw || '').trim().toLowerCase();
137
160
  return libraryList().filter(
@@ -144,6 +167,10 @@ export function searchLibrary(kw) {
144
167
  // - name:字母/数字/连字符/点/下划线,1–64 位
145
168
  // - description:非空且 ≤ 200 字
146
169
  // 校验失败返回具体错误,绝不装入技能目录。
170
+ /**
171
+ * @param {any} text
172
+ * @param {any} hint
173
+ */
147
174
  export function validateSkillMarkdown(text, hint) {
148
175
  const src = hint ? `(${hint})` : '';
149
176
  if (!text || !String(text).trim()) return { error: `SKILL.md 为空${src}` };
@@ -159,6 +186,10 @@ export function validateSkillMarkdown(text, hint) {
159
186
  return { ok: true, name, description: desc };
160
187
  }
161
188
 
189
+ /**
190
+ * @param {any} dir
191
+ * @param {any} hint
192
+ */
162
193
  export function validateSkillDir(dir, hint) {
163
194
  const skillMd = path.join(dir, 'SKILL.md');
164
195
  let text;
@@ -170,6 +201,12 @@ export function validateSkillDir(dir, hint) {
170
201
  return validateSkillMarkdown(text, hint);
171
202
  }
172
203
 
204
+ /**
205
+ * @param {any} dir
206
+ * @param {any} name
207
+ * @param {any} source
208
+ * @param {any} extra
209
+ */
173
210
  function copySkillIntoUser(dir, name, source, extra) {
174
211
  ensureHome();
175
212
  const target = path.join(userSkillsDir(), name);
@@ -188,12 +225,18 @@ function copySkillIntoUser(dir, name, source, extra) {
188
225
  return { name, dir: target };
189
226
  }
190
227
 
228
+ /**
229
+ * @param {any} name
230
+ */
191
231
  export function installFromLibrary(name) {
192
232
  const found = libraryList().find((s) => s.name === name);
193
233
  if (!found) return { error: `技能库中没有 ${name}(mingdao skill search 查看全部)` };
194
234
  return copySkillIntoUser(found.dir, found.name, 'library', {});
195
235
  }
196
236
 
237
+ /**
238
+ * @param {any} dir
239
+ */
197
240
  export function installFromDir(dir) {
198
241
  const abs = path.resolve(dir);
199
242
  if (!fs.existsSync(path.join(abs, 'SKILL.md'))) {
@@ -204,6 +247,9 @@ export function installFromDir(dir) {
204
247
  return copySkillIntoUser(abs, meta.name, 'dir', { from: abs });
205
248
  }
206
249
 
250
+ /**
251
+ * @param {any} url
252
+ */
207
253
  export async function installFromUrl(url) {
208
254
  let u;
209
255
  try {
@@ -222,7 +268,7 @@ export async function installFromUrl(url) {
222
268
  if (!res.ok) return { error: `下载失败:HTTP ${res.status}` };
223
269
  text = await res.text();
224
270
  if (text.length > 512 * 1024) return { error: 'SKILL.md 超过 512KB 上限' };
225
- } catch (e) {
271
+ } catch (/** @type {any} */ e) {
226
272
  return { error: `下载失败:${e.name === 'AbortError' ? '30 秒超时' : e.message}` };
227
273
  } finally {
228
274
  clearTimeout(timer);
@@ -244,6 +290,9 @@ export async function installFromUrl(url) {
244
290
  return r;
245
291
  }
246
292
 
293
+ /**
294
+ * @param {any} gitUrl
295
+ */
247
296
  export function installFromGit(gitUrl) {
248
297
  if (typeof gitUrl !== 'string' || gitUrl.trim().startsWith('-')) {
249
298
  return { error: 'git 地址不能以 - 开头(防选项注入)' };
@@ -262,7 +311,8 @@ export function installFromGit(gitUrl) {
262
311
  const found = [];
263
312
  const stack = [tmp];
264
313
  while (stack.length && found.length < 20) {
265
- const dir = stack.pop();
314
+ const dir = /** @type {any} */ (stack.pop());
315
+ /** @type {any[]} */
266
316
  let entries = [];
267
317
  try {
268
318
  entries = fs.readdirSync(dir, { withFileTypes: true });
@@ -297,6 +347,9 @@ export function installFromGit(gitUrl) {
297
347
  return { names: installed.map((i) => i.name), dirs: installed.map((i) => i.dir), skipped };
298
348
  }
299
349
 
350
+ /**
351
+ * @param {any} name
352
+ */
300
353
  export function uninstallSkill(name) {
301
354
  const key = String(name).trim();
302
355
  // '.'/'..' 会拼出上级目录,必须硬拒绝(正则 [A-Za-z0-9_.-]+ 会放行纯点号名)
@@ -310,9 +363,12 @@ export function uninstallSkill(name) {
310
363
  }
311
364
 
312
365
  // 按元数据来源重新安装(库:重新复制;url:重新下载;git:重新克隆)
366
+ /**
367
+ * @param {any} name
368
+ */
313
369
  export async function reinstallSkill(name) {
314
370
  const target = path.join(userSkillsDir(), name);
315
- let meta = {};
371
+ let meta = /** @type {any} */ ({});
316
372
  try {
317
373
  meta = JSON.parse(fs.readFileSync(path.join(target, '.mingdao-source.json'), 'utf8'));
318
374
  } catch {
@@ -330,6 +386,9 @@ export async function reinstallSkill(name) {
330
386
  }
331
387
 
332
388
  // 统一入口:自动识别 库名 | 本地目录 | SKILL.md URL | git 仓库
389
+ /**
390
+ * @param {any} arg
391
+ */
333
392
  export async function installSkill(arg) {
334
393
  const a = String(arg || '').trim();
335
394
  if (!a) return { error: '缺少参数:mingdao skill install <库名|目录|SKILL.md URL|git 仓库地址>' };
@@ -42,14 +42,14 @@ function loadCache() {
42
42
  }
43
43
  }
44
44
 
45
- function saveCache(data, host) {
45
+ function saveCache(/** @type {any} */ data, /** @type {any} */ host) {
46
46
  try {
47
47
  ensureHome();
48
48
  fs.writeFileSync(cacheFile(), JSON.stringify({ fetchedAt: Date.now(), host, data }, null, 2) + '\n', { mode: 0o600 });
49
49
  } catch {}
50
50
  }
51
51
 
52
- async function fetchText(url, timeoutMs = 20000, maxBytes = 2 * 1024 * 1024) {
52
+ async function fetchText(/** @type {any} */ url, timeoutMs = 20000, maxBytes = 2 * 1024 * 1024) {
53
53
  const ctrl = new AbortController();
54
54
  const timer = setTimeout(() => ctrl.abort(), timeoutMs);
55
55
  try {
@@ -91,26 +91,26 @@ export async function fetchRegistryIndex({ force = false, allowNetwork = true }
91
91
  }
92
92
  // 网络失败时回退旧缓存(过期也可用)
93
93
  if (cached?.data?.skills) return { data: cached.data, host: cached.host, fromCache: true, stale: true };
94
- return { error: `无法获取线上技能库:${lastErr?.message || '网络不可达'}(可用 MINGDAO_REGISTRY_URL 指向自建 registry)` };
94
+ return { error: `无法获取线上技能库:${(/** @type {any} */ (lastErr))?.message || '网络不可达'}(可用 MINGDAO_REGISTRY_URL 指向自建 registry)` };
95
95
  }
96
96
 
97
97
  // 远端搜索(合并展示:name/description/source/installed)
98
- export async function searchRegistry(kw, { force = false, allowNetwork = true } = {}) {
98
+ export async function searchRegistry(/** @type {any} */ kw, { force = false, allowNetwork = true } = {}) {
99
99
  const r = await fetchRegistryIndex({ force, allowNetwork });
100
100
  if (r.error) return { error: r.error };
101
101
  const k = String(kw || '').trim().toLowerCase();
102
102
  const installed = installedUserSkillNames();
103
103
  const skills = r.data.skills
104
- .filter((s) => !k || s.name.toLowerCase().includes(k) || (s.description || '').toLowerCase().includes(k))
105
- .map((s) => ({ name: s.name, description: s.description, source: 'registry', installed: installed.has(s.name) }));
104
+ .filter((/** @type {any} */ s) => !k || s.name.toLowerCase().includes(k) || (s.description || '').toLowerCase().includes(k))
105
+ .map((/** @type {any} */ s) => ({ name: s.name, description: s.description, source: 'registry', installed: installed.has(s.name) }));
106
106
  return { skills, host: r.host, updatedAt: r.data.updatedAt, fromCache: r.fromCache, stale: r.stale || false };
107
107
  }
108
108
 
109
109
  // 按索引安装(逐文件下载 + dry-run 校验)
110
- export async function installFromRegistry(name) {
110
+ export async function installFromRegistry(/** @type {any} */ name) {
111
111
  const r = await fetchRegistryIndex();
112
112
  if (r.error) return { error: r.error };
113
- const entry = r.data.skills.find((s) => s.name === name);
113
+ const entry = r.data.skills.find((/** @type {any} */ s) => s.name === name);
114
114
  if (!entry) return { error: `线上技能库中没有 ${name}(mingdao skill search 查看)` };
115
115
  if (!Array.isArray(entry.files) || !entry.files.length) return { error: `技能 ${name} 的索引缺少文件清单` };
116
116
 
@@ -134,7 +134,8 @@ export async function installFromRegistry(name) {
134
134
  text = await fetchText(`${host}/skills-lib/${encodeURI(name)}/${rel.split('/').map(encodeURIComponent).join('/')}`, 30000, MAX_FILE);
135
135
  break;
136
136
  } catch (e) {
137
- lastErr = e?.name === 'AbortError' ? '下载超时' : String(e?.message || e);
137
+ const ee = /** @type {any} */ (e);
138
+ lastErr = ee?.name === 'AbortError' ? '下载超时' : String(ee?.message || ee);
138
139
  }
139
140
  }
140
141
  if (text === null) return { error: `下载 ${name}/${rel} 失败:${lastErr}(已尝试全部镜像)` };