dsh-plugin-tool-management 0.8.0 → 0.9.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.
Files changed (60) hide show
  1. package/CHANGELOG.md +67 -1
  2. package/README.md +53 -48
  3. package/README_EN.md +47 -44
  4. package/docs/images/1-EN.png +0 -0
  5. package/docs/images/1.png +0 -0
  6. package/docs/images/2-EN.png +0 -0
  7. package/docs/images/2.png +0 -0
  8. package/docs/images/3-EN.png +0 -0
  9. package/docs/images/3.png +0 -0
  10. package/docs/images/4-EN.png +0 -0
  11. package/docs/images/4.png +0 -0
  12. package/docs/images/5-EN.png +0 -0
  13. package/docs/images/5.png +0 -0
  14. package/docs/images/6-EN.png +0 -0
  15. package/docs/images/6.png +0 -0
  16. package/docs/images/7-EN.png +0 -0
  17. package/docs/images/7.png +0 -0
  18. package/docs/images/8-EN.png +0 -0
  19. package/docs/images/8.png +0 -0
  20. package/docs/update.md +39 -0
  21. package/lib/agents-md/preset-id.js +1 -1
  22. package/lib/agents-md/service.js +46 -9
  23. package/lib/approval-policy.js +1 -1
  24. package/lib/client.js +431 -135
  25. package/lib/compat/preset-reach.js +155 -93
  26. package/lib/compat/probe.js +6 -4
  27. package/lib/context-inject.js +304 -0
  28. package/lib/hub.js +155 -13
  29. package/lib/imports/upload.js +34 -14
  30. package/lib/index.js +870 -226
  31. package/lib/mcp/state-section.js +46 -15
  32. package/lib/rules/archive-engine.js +182 -57
  33. package/lib/rules/archive.js +57 -1
  34. package/lib/rules/service.js +386 -156
  35. package/lib/scene-prompt-sync.js +101 -12
  36. package/lib/skills/catalog.js +125 -0
  37. package/lib/skills/core.js +38 -4
  38. package/lib/skills/service.js +36 -10
  39. package/lib/subagents/catalog.js +15 -5
  40. package/lib/subagents/service.js +39 -28
  41. package/lib/subagents/tools.js +18 -13
  42. package/package.json +14 -9
  43. package/screenshots.json +8 -8
  44. package/docs/images/1/345/234/272/346/231/257.png +0 -0
  45. package/docs/images/1/345/234/272/346/231/257_en.png +0 -0
  46. package/docs/images/2MCP.png +0 -0
  47. package/docs/images/2MCP_en.png +0 -0
  48. package/docs/images/3/346/212/200/350/203/275.png +0 -0
  49. package/docs/images/3/346/212/200/350/203/275_en.png +0 -0
  50. package/docs/images/4/345/255/220/346/231/272/350/203/275/344/275/223.png +0 -0
  51. package/docs/images/4/345/255/220/346/231/272/350/203/275/344/275/223_en.png +0 -0
  52. package/docs/images/5/346/217/220/347/244/272/350/257/215.png +0 -0
  53. package/docs/images/5/346/217/220/347/244/272/350/257/215_en.png +0 -0
  54. package/docs/images/6/350/256/260/345/277/206.png +0 -0
  55. package/docs/images/6/350/256/260/345/277/206_en.png +0 -0
  56. package/docs/images/7/344/274/232/350/257/235.png +0 -0
  57. package/docs/images/7/344/274/232/350/257/235_en.png +0 -0
  58. package/docs/images/8/345/205/274/345/256/271.png +0 -0
  59. package/docs/images/8/345/205/274/345/256/271_en.png +0 -0
  60. package/lib/prompt-sections.js +0 -148
@@ -7,8 +7,11 @@
7
7
  //
8
8
  // id 即目录名:字符集口径见 `./preset-id.ts`(用户裁定「什么都能写」,只留
9
9
  // 文件系统安全约束);`__last-applied__` 是备份槽,不算用户预设。
10
- // 「当前生效」靠比对 ~/.dsh/AGENTS.md 的 sha256 与各预设 sha256 推断,
11
- // 无状态文件——用户手改全局文件也能如实反映。
10
+ // 「当前生效」(`active`)靠比对 ~/.dsh/AGENTS.md 的 sha256 与各预设 sha256 推断 ——
11
+ // 用户手改全局文件也能如实反映。但那只能回答「内容一不一样」,回答不了「这份文件是
12
+ // 从哪个预设来的」:手改一次之后 `active` 全为 false,模型侧列表就只剩空(或被迫
13
+ // 全列,白烧上下文)。所以另记一份 `__applied__.json`(只记「最近一次应用的是谁」),
14
+ // 两个事实分开报:`active` = 内容逐字节相同;`lastApplied` = 最近一次应用写入的是它。
12
15
  import { mkdir, readdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
13
16
  import { join } from 'node:path';
14
17
  import { createHash } from 'node:crypto';
@@ -18,6 +21,11 @@ const FILENAME = 'AGENTS.md';
18
21
  /** 描述侧车:与 AGENTS.md 同目录,避免把「只给使用者看」的文字注入提示词。 */
19
22
  const META_FILE = 'meta.json';
20
23
  const LAST_APPLIED_ID = LAST_APPLIED_PRESET_ID;
24
+ /**
25
+ * 「最近一次应用」的记录侧车。是**文件**不是目录,所以 `list()` / `ensureInit()` 的
26
+ * `isDirectory()` 过滤天然把它排除在预设之外。
27
+ */
28
+ const APPLIED_FILE = '__applied__.json';
21
29
  /** 新建预设的初始正文(空模板);用户也可以直接粘贴自己的内容。 */
22
30
  const BLANK_TEMPLATE = '# AGENTS.md\n\n(DSH 全局指令基线预设,待编辑)\n';
23
31
  /** 全局 AGENTS.md 的备份代际上限:与 mcpm patch 的 KEEP_PATCH_BACKUPS 同纪律。 */
@@ -110,6 +118,7 @@ export function createAgentsMdService(_ctx, deps) {
110
118
  // 库目录不存在视作空(首次运行尚未创建)
111
119
  }
112
120
  const globalHash = await sha256OfFile(globalPath);
121
+ const appliedId = await readAppliedId();
113
122
  const ids = entries
114
123
  .filter((e) => e.isDirectory() && isValidPresetId(e.name) && e.name !== LAST_APPLIED_ID)
115
124
  .map((e) => e.name);
@@ -118,6 +127,7 @@ export function createAgentsMdService(_ctx, deps) {
118
127
  return {
119
128
  id,
120
129
  active: globalHash !== null && contentHash !== null && globalHash === contentHash,
130
+ ...(appliedId === id ? { lastApplied: true } : {}),
121
131
  description: await readDescription(id),
122
132
  };
123
133
  }));
@@ -217,6 +227,11 @@ export function createAgentsMdService(_ctx, deps) {
217
227
  }
218
228
  await writeFile(join(deps.presetsDir, targetId, FILENAME), String(content ?? ''), 'utf8');
219
229
  await writeDescription(targetId, description);
230
+ if (targetId !== safeId && await readAppliedId() === safeId) {
231
+ // 改名的正好是「最近一次应用」的那个预设:记录跟着改,否则它指向一个不存在的 id,
232
+ // 手改过全局文件的用户就再也看不到「这份文件从哪来」。
233
+ await writeAppliedId(targetId);
234
+ }
220
235
  return { ok: true, id: targetId, ...(targetId === safeId ? {} : { renamedFrom: safeId }) };
221
236
  }
222
237
  /**
@@ -250,6 +265,25 @@ export function createAgentsMdService(_ctx, deps) {
250
265
  return null;
251
266
  }
252
267
  }
268
+ /** 「最近一次应用」的预设 id;无记录 / 记录不可读 / id 已不合法 → `null`(一律不猜)。 */
269
+ async function readAppliedId() {
270
+ try {
271
+ const raw = JSON.parse(await readFile(join(deps.presetsDir, APPLIED_FILE), 'utf8'));
272
+ const id = raw && typeof raw.id === 'string' ? raw.id : null;
273
+ return id !== null && isValidPresetId(id) && id !== LAST_APPLIED_ID ? id : null;
274
+ }
275
+ catch {
276
+ return null;
277
+ }
278
+ }
279
+ /** 记下本次应用。**best-effort**:记录失败绝不能影响「应用」本身。 */
280
+ async function writeAppliedId(id) {
281
+ try {
282
+ await mkdir(deps.presetsDir, { recursive: true });
283
+ await writeFile(join(deps.presetsDir, APPLIED_FILE), JSON.stringify({ id, at: new Date().toISOString() }, null, 2) + '\n', 'utf8');
284
+ }
285
+ catch { /* ignore */ }
286
+ }
253
287
  async function apply(id) {
254
288
  const bad = idError(id);
255
289
  if (bad)
@@ -265,6 +299,7 @@ export function createAgentsMdService(_ctx, deps) {
265
299
  const prev = await readGlobal();
266
300
  const backedUp = await backupGlobal(prev);
267
301
  await writeFile(await deps.getGlobalAgentsMdPath(), content, 'utf8');
302
+ await writeAppliedId(safeId);
268
303
  return { ok: true, id: safeId, backedUp };
269
304
  }
270
305
  /**
@@ -282,6 +317,8 @@ export function createAgentsMdService(_ctx, deps) {
282
317
  catch (e) {
283
318
  return { ok: false, error: '写入全局 AGENTS.md 失败:' + message(e) };
284
319
  }
320
+ // 基线内容可能来自用户手写(场景退出恢复)→ 不绑定任何预设。
321
+ await writeAppliedId(null);
285
322
  return { ok: true, backedUp };
286
323
  }
287
324
  async function getCurrent() {
@@ -324,7 +361,7 @@ export function createAgentsMdService(_ctx, deps) {
324
361
  }
325
362
  }
326
363
  /**
327
- * 删除预设 = **移入回收站**(整个 `agents-md/<id>/` 目录搬走)。
364
+ * 删除预设 = **移入回收站**(整个 `prompts/<id>/` 目录搬走)。
328
365
  * 备份槽 `__last-applied__` 不可删;正在生效的预设由调用方先拦(见 index.ts)。
329
366
  */
330
367
  async function remove(id) {
@@ -341,17 +378,17 @@ export function createAgentsMdService(_ctx, deps) {
341
378
  catch {
342
379
  return { ok: false, error: '预设不存在:' + safeId };
343
380
  }
344
- const moved = await moveToTrash('agents-md', safeId, [{ from: dir, dest: 'preset' }]);
381
+ const moved = await moveToTrash('prompts', safeId, [{ from: dir, dest: 'preset' }]);
345
382
  if (moved.ok === false)
346
383
  return { ok: false, error: '移入回收站失败:' + moved.error };
347
384
  return { ok: true, id: safeId };
348
385
  }
349
386
  async function trashList() {
350
- return { ok: true, trash: await listTrashEntries('agents-md') };
387
+ return { ok: true, trash: await listTrashEntries('prompts') };
351
388
  }
352
389
  /** 从回收站恢复预设:同 id 已存在时**拒绝**(绝不覆盖)。 */
353
390
  async function trashRestore(id) {
354
- const entry = await readTrashEntry('agents-md', String(id ?? '').trim());
391
+ const entry = await readTrashEntry('prompts', String(id ?? '').trim());
355
392
  if (!entry)
356
393
  return { ok: false, error: '回收站条目不存在:' + String(id ?? '').trim() };
357
394
  const bad = idError(entry.name);
@@ -365,17 +402,17 @@ export function createAgentsMdService(_ctx, deps) {
365
402
  catch { /* 可用 */ }
366
403
  try {
367
404
  await mkdir(deps.presetsDir, { recursive: true });
368
- await moveOutOfTrash('agents-md', entry.id, 'preset', target);
405
+ await moveOutOfTrash('prompts', entry.id, 'preset', target);
369
406
  }
370
407
  catch (e) {
371
408
  return { ok: false, error: '恢复失败:' + message(e) };
372
409
  }
373
- await purgeTrashEntry('agents-md', entry.id);
410
+ await purgeTrashEntry('prompts', entry.id);
374
411
  return { ok: true, id: entry.name };
375
412
  }
376
413
  async function trashDelete(id) {
377
414
  const clean = String(id ?? '').trim();
378
- const gone = await purgeTrashEntry('agents-md', clean);
415
+ const gone = await purgeTrashEntry('prompts', clean);
379
416
  if (!gone)
380
417
  return { ok: false, error: '回收站条目不存在:' + clean };
381
418
  return { ok: true, id: clean };
@@ -19,7 +19,7 @@
19
19
  // try/catch 里,异常被吞成 false,于是 never 会话里永远走 ask 分支(2026-09-13
20
20
  // 19:52 会话实测到的 bug)。dsh-tools 自己拿这条缝也是 `this.ctx.get("approval")`
21
21
  // (dsh-tools/lib/index.js `serviceAsk`),且它的 inject 同样只有 systemPrompt
22
- // ——可作对照。回归测试见 test/approval-policy.test.mjs。
22
+ // ——可作对照。
23
23
  /**
24
24
  * 判断本次工具调用的会话是否处于 never 审批策略(= 完全权限,确认门应直接放行)。
25
25
  *