dsh-plugin-tool-management 0.20.3 → 0.21.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 (42) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +4 -1
  3. package/README_EN.md +4 -1
  4. package/docs/update.md +1 -0
  5. package/lib/client.js +117 -11
  6. package/lib/compat/capability-specs.js +60 -1
  7. package/lib/compat/host-contracts.js +72 -0
  8. package/lib/compat/host-probe.js +59 -0
  9. package/lib/compat/host-roots.js +1 -1
  10. package/lib/compat/probe.js +84 -13
  11. package/lib/context-inject-frame.js +134 -26
  12. package/lib/context-inject.js +12 -9
  13. package/lib/http-api.js +3 -2
  14. package/lib/http-fence.js +26 -1
  15. package/lib/index.js +5 -2
  16. package/lib/mcp/manager.js +6 -1
  17. package/lib/mcp/state-section.js +75 -34
  18. package/lib/memories/constants.js +39 -7
  19. package/lib/memories/projection.js +18 -18
  20. package/lib/memories/service.js +8 -7
  21. package/lib/memories/snapshot.js +27 -17
  22. package/lib/ops/compat.js +64 -2
  23. package/lib/ops/memory.js +2 -2
  24. package/lib/ops/prompts.js +30 -2
  25. package/lib/request-gate.js +5 -4
  26. package/lib/skills/catalog.js +2 -2
  27. package/lib/subagents/catalog.js +4 -4
  28. package/lib/subagents/child-preset.js +120 -0
  29. package/lib/subagents/persona-parse.js +9 -9
  30. package/lib/subagents/run-provider.js +218 -62
  31. package/lib/subagents/tool-filter.js +12 -12
  32. package/lib/subagents/tools.js +29 -22
  33. package/lib/tools/deps.js +4 -4
  34. package/lib/tools/mcp.js +34 -34
  35. package/lib/tools/memory.js +34 -34
  36. package/lib/tools/prompt.js +5 -5
  37. package/lib/tools/remove.js +17 -17
  38. package/lib/tools/scene.js +66 -66
  39. package/lib/tools/skills.js +21 -21
  40. package/lib/tools/subagent.js +13 -13
  41. package/lib/tools/table.js +2 -2
  42. package/package.json +25 -24
package/lib/tools/mcp.js CHANGED
@@ -28,7 +28,7 @@
28
28
  import { DEFAULT_MCP_NOTE_MAX_LENGTH, normalizeMcpNote } from '../mcp/state-section.js';
29
29
  import { text } from './deps.js';
30
30
  /** 「去看清单」那句:`mcp_manager_list` 不在模型手里时不能点名它(调用方按 `toolVisibleFor` 决定给不给)。 */
31
- const MCP_LIST_HINT = '(用 mcp_manager_list 看清单,all=true 连停用的也列)';
31
+ const MCP_LIST_HINT = ' (see mcp_manager_list — all=true also lists disabled ones)';
32
32
  /**
33
33
  * 指不到 `mcp_manager_list` 时的**中性出口**:不点名任何工具,两条常驻来源都成立
34
34
  * —— 模型自己的 `mcp__<server>__<tool>` 工具名(就在它的 schema 里),以及注入段。
@@ -37,22 +37,22 @@ const MCP_LIST_HINT = '(用 mcp_manager_list 看清单,all=true 连停用的
37
37
  * 的 MCP 工具,猜错 serverName 时若报错里只有「没有这台服务器:X」,模型拿不到任何补救线索
38
38
  * —— 它只能猜第二次。出口要么指向工具(那条工具得在它手里),要么指向这两条常驻来源。
39
39
  */
40
- const MCP_NEUTRAL_HINT = '(服务器名照抄你自己 `mcp__<server>__<tool>` 工具名里的那一段,或「当前 MCP 服务器」段里列出的名字)';
40
+ const MCP_NEUTRAL_HINT = ' (copy the server name from your own `mcp__<server>__<tool>` tool names, or take one listed in the "Current MCP servers" reminder)';
41
41
  /** 按 serverName 定位条目;同名(project + global 各一台)时要求 level 明确指定。 */
42
42
  function locate(rows, server, level, listHint = MCP_LIST_HINT) {
43
43
  const hits = (rows || []).filter((r) => String(r.serverName) === server);
44
44
  if (!hits.length) {
45
- throw new Error('没有这台服务器:' + server + (listHint || MCP_NEUTRAL_HINT));
45
+ throw new Error('no such server: ' + server + (listHint || MCP_NEUTRAL_HINT));
46
46
  }
47
47
  if (level) {
48
48
  const picked = hits.filter((r) => r.level === level);
49
49
  if (!picked.length) {
50
- throw new Error('「' + server + '」没有 level=' + level + ' 的那一台(现有:' + hits.map((r) => r.level).join(' / ') + ')');
50
+ throw new Error('"' + server + '" has no level=' + level + ' entry (existing: ' + hits.map((r) => r.level).join(' / ') + ')');
51
51
  }
52
52
  return picked[0];
53
53
  }
54
54
  if (hits.length > 1) {
55
- throw new Error('「' + server + '」在 ' + hits.map((r) => r.level).join(' 与 ') + ' 各有一台,无法确定动哪一台 —— 传 level 指定');
55
+ throw new Error('"' + server + '" exists at both ' + hits.map((r) => r.level).join(' and ') + ' — pass level to say which one to touch');
56
56
  }
57
57
  return hits[0];
58
58
  }
@@ -89,11 +89,11 @@ export function buildMcpTools(deps) {
89
89
  // 是行动依据 —— 模型要知道"停用的那台去哪查"就得靠这句(all=true)。
90
90
  //
91
91
  // 2026-10-15 注入文本审查:删掉两处白付的 token ——
92
- // ① 括号里那句把「当前 MCP 服务器」reminder 的**内容**逐项列了一遍(可用 / 曾连上 /
92
+ // ① 括号里那句把 "Current MCP servers" reminder 的**内容**逐项列了一遍(可用 / 曾连上 /
93
93
  // 备注 / 停用不进),而那段 reminder 每轮就在模型上下文里,它自己看得到;
94
94
  // ② `where loader:on:<phase> is the loader's current phase` —— 用自己解释自己
95
95
  // (phase 就是 phase),零信息。字段名本身留在输出里,模型照抄即可。
96
- description: 'List configured MCP servers (level project|global; enabled state; live loader status; tool count excluding switched-off tools; user hint). The「当前 MCP 服务器」reminder is a filtered view; this tool is the raw one — defaults to enabled servers, all=true for every configured one. Pass tools=true to list each server\'s tool names.',
96
+ description: 'List configured MCP servers (level project|global; enabled state; live loader status; tool count excluding switched-off tools; user hint). The "Current MCP servers" reminder is a filtered view; this tool is the raw one — defaults to enabled servers, all=true for every configured one. Pass tools=true to list each server\'s tool names.',
97
97
  parameters: {
98
98
  all: { type: 'boolean', description: 'Include disabled servers (default false).' },
99
99
  tools: { type: 'boolean', description: 'Also list each server\'s tool names, marking the switched-off ones (default false).' },
@@ -162,7 +162,7 @@ export function buildMcpTools(deps) {
162
162
  // 报错退化成「没有这台服务器:<你猜的>」—— 一个字都不告诉它还能去哪看。
163
163
  // 指向模型自己的工具表(`mcp__<server>__<tool>` 就在它的 schema 里)与常驻注入段,
164
164
  // 这两条**不需要任何工具可见**就成立。
165
- server: { type: 'string', required: true, description: 'Server name, as it appears in your own `mcp__<server>__<tool>` tool names (the「当前 MCP 服务器」reminder lists the usable ones).' },
165
+ server: { type: 'string', required: true, description: 'Server name, as it appears in your own `mcp__<server>__<tool>` tool names (the "Current MCP servers" reminder lists the usable ones).' },
166
166
  action: { type: 'string', required: true, enum: ['on', 'off', 'restart'], description: 'on = enable, off = disable, restart = reconnect and re-sync tools.' },
167
167
  level: { type: 'string', description: 'project or global — only to disambiguate same-name servers.' },
168
168
  tool: { type: 'string', description: 'One tool name on that server (no mcp__ prefix). Only for on/off.' },
@@ -171,12 +171,12 @@ export function buildMcpTools(deps) {
171
171
  async execute(args, exec) {
172
172
  const action = String((args && args.action) || '');
173
173
  if (action !== 'on' && action !== 'off' && action !== 'restart') {
174
- throw new Error('action 只能是 on / off / restart(收到:' + (action || '(空)') + ')');
174
+ throw new Error('action must be on / off / restart (got: ' + (action || '(empty)') + ')');
175
175
  }
176
176
  const server = String((args && args.server) || '').trim();
177
177
  const tool = args && args.tool !== undefined ? String(args.tool).trim() : '';
178
178
  if (tool !== '' && action === 'restart') {
179
- throw new Error('重启是整台服务器的动作(先停用、等断开、再按原状态启用),不能只重启一个工具');
179
+ throw new Error('restart is a whole-server action (off, wait for it to drop, then back on at its previous state); a single tool cannot be restarted');
180
180
  }
181
181
  // on|off 会写补丁(`mcpm-set-enabled` / `mcpm-tool-enabled` 在登记表里是 frozen),
182
182
  // 所以过场景冻结守卫;restart 不过 —— 它是锁定期间唯一的恢复入口(见文件头)。
@@ -187,36 +187,36 @@ export function buildMcpTools(deps) {
187
187
  }
188
188
  const list = await deps.mcpmListView();
189
189
  if (!list || list.ok === false)
190
- throw new Error((list && list.error) || '读取 MCP 配置失败');
190
+ throw new Error((list && list.error) || 'failed to read the MCP config');
191
191
  const level = args && args.level !== undefined ? String(args.level) : undefined;
192
192
  const row = locate(list.rows || [], server, level, listHint(exec && exec.agent));
193
193
  const enabled = action === 'on';
194
194
  if (tool !== '') {
195
195
  const r = await deps.mcpmToolEnabled({ serverName: String(row.serverName), tool, enabled });
196
196
  if (!r || r.ok === false)
197
- throw new Error((r && r.error) || '切换工具启停失败');
197
+ throw new Error((r && r.error) || 'failed to switch this tool');
198
198
  // 与面板同一条同步:`mcpm-tool-enabled` 在登记表里带 syncsArchive,漏了就是
199
199
  // "页面拨的进档案、模型拨的不进"。
200
200
  const syncErr = await deps.syncSwitchToScene('mcpm-tool-enabled', { serverName: String(row.serverName), tool, enabled });
201
201
  // 整台停用时,工具级开关在运行时本来就不生效(那个工具压根不存在)—— 不说的话
202
202
  // 模型会以为已经生效。与记忆族"启用了一条不在启用场景里的记忆"同一条理由。
203
- const offNote = row.disabled ? '(注意:整台服务器当前是停用的,这个工具级开关要等它启用才生效)' : '';
203
+ const offNote = row.disabled ? ' (note: the whole server is off right now, so this tool-level switch only takes effect once the server is on)' : '';
204
204
  return 'OK: ' + String(row.serverName) + ' tool ' + tool + ' now ' + (enabled ? 'enabled' : 'disabled') + offNote +
205
- (syncErr ? '\nWARN: 当前场景档案未同步(' + syncErr + ')' : '');
205
+ (syncErr ? '\nWARN: the current scene archive was not synced (' + syncErr + ')' : '');
206
206
  }
207
207
  if (action === 'restart') {
208
208
  const r = await deps.mcpmRestart({ id: row.id, level: row.level });
209
209
  if (!r || r.ok === false)
210
- throw new Error((r && r.error) || '重启失败');
210
+ throw new Error((r && r.error) || 'restart failed');
211
211
  return 'OK: ' + String(row.serverName) + ' (' + row.level + ') restarted';
212
212
  }
213
213
  const r = await deps.mcpmSetEnabled({ id: row.id, level: row.level, enabled });
214
214
  if (!r || r.ok === false)
215
- throw new Error((r && r.error) || '切换服务器启停失败');
215
+ throw new Error((r && r.error) || 'failed to switch this server');
216
216
  // 场景未锁定时,页面 / 模型改的开关都要落进当前场景的档案(与 handlers 层同一套同步)。
217
217
  const syncErr = await deps.syncSwitchToScene('mcpm-set-enabled', { id: row.id, level: row.level, enabled });
218
218
  return 'OK: ' + String(row.serverName) + ' (' + row.level + ') now ' + (enabled ? 'enabled' : 'disabled') +
219
- (syncErr ? '\nWARN: 当前场景档案未同步(' + syncErr + ')' : '');
219
+ (syncErr ? '\nWARN: the current scene archive was not synced (' + syncErr + ')' : '');
220
220
  },
221
221
  }));
222
222
  register(defineTool({
@@ -232,7 +232,7 @@ export function buildMcpTools(deps) {
232
232
  env: { type: 'string', description: 'Extra env vars, key=value per line (stdio). Omit = keep.' },
233
233
  level: { type: 'string', description: 'project or global (project when creating); only needed for same-name servers.' },
234
234
  // 名字与注入段对齐(2026-10-14 对抗性审查 P1-2):注入段与工具输出都写 `user-hint`
235
- // (对应中文那句「用户提示:」),参数名跟着叫 `hint`,一份数据三个面一个词。
235
+ // (对应注入行里那句 `user hint:`),参数名跟着叫 `hint`,一份数据三个面一个词。
236
236
  // 「写什么」那半句已经由注入框架的 `how` 行逐字说过("选服务器之前先看一眼"),
237
237
  // 这里只留参数独有的信息:长度上限与截断行为(2026-10-14 审查 P2-4)。
238
238
  hint: { type: 'string', description: "Short hint for later (≤300 chars; longer is stored whole but truncated in listings). '' clears the existing hint; omit = keep." },
@@ -244,7 +244,7 @@ export function buildMcpTools(deps) {
244
244
  throw new Error(blocked);
245
245
  const server = String((args && args.server) || '').trim();
246
246
  if (!server)
247
- throw new Error('缺少参数:server(服务器名)');
247
+ throw new Error('missing argument: server (the server name)');
248
248
  const levelArg = args && args.level !== undefined ? String(args.level) : undefined;
249
249
  // `hint` 的三态(2026-10-15 修 P1-5):省略 = 保持;`''` = **清空**;非空 = 设置。
250
250
  // 此前用 `hintText.trim() !== ''` 判"要不要写备注",把"清空"与"省略"混成一件事 ——
@@ -255,20 +255,20 @@ export function buildMcpTools(deps) {
255
255
  const clearsNote = hintGiven && hintText.trim() === '';
256
256
  const list = await deps.mcpmListView();
257
257
  if (!list || list.ok === false)
258
- throw new Error((list && list.error) || '读取 MCP 配置失败');
258
+ throw new Error((list && list.error) || 'failed to read the MCP config');
259
259
  const rows = list.rows || [];
260
260
  const hits = rows.filter((r) => String(r.serverName) === server);
261
261
  // ── 建:transport 与它那一侧的必填项都得给 ─────────────────────────────
262
262
  if (!hits.length) {
263
263
  const transport = String((args && args.transport) || '').trim();
264
264
  if (transport !== 'stdio' && transport !== 'streamable-http') {
265
- throw new Error('新建服务器必须给 transport:stdio 或 streamable-http');
265
+ throw new Error('creating a server needs transport: stdio or streamable-http');
266
266
  }
267
267
  if (transport === 'stdio' && String((args && args.command) || '').trim() === '') {
268
- throw new Error('新建 stdio 服务器必须给 command(宿主会按它 spawn 进程)');
268
+ throw new Error('a new stdio server needs command (the host spawns that process)');
269
269
  }
270
270
  if (transport === 'streamable-http' && String((args && args.url) || '').trim() === '') {
271
- throw new Error('新建 streamable-http 服务器必须给 url');
271
+ throw new Error('a new streamable-http server needs url');
272
272
  }
273
273
  const payload = {
274
274
  serverName: server,
@@ -283,14 +283,14 @@ export function buildMcpTools(deps) {
283
283
  payload.level = levelArg;
284
284
  const r = await deps.mcpmAdd(payload);
285
285
  if (!r || r.ok === false)
286
- throw new Error((r && r.error) || '新增服务器失败');
286
+ throw new Error((r && r.error) || 'failed to add the server');
287
287
  // 备注单独走 `mcpm-note`(`mcpm-add` 不认这个字段)。失败**不回滚**主写入:
288
288
  // 服务器已经建好了,"备注没写上"是 warning,不是失败(与 0.13.0 界面同规格)。
289
289
  let warn = '';
290
290
  if (hintGiven && !clearsNote) {
291
291
  const n = await deps.mcpmNote({ id: String(r.row.id), note: hintText });
292
292
  if (!n || n.ok === false)
293
- warn = '\nWARN: 服务器已建好,但备注没写进去(' + ((n && n.error) || '未知原因') + ')';
293
+ warn = '\nWARN: the server was created, but the hint was not written (' + ((n && n.error) || 'unknown reason') + ')';
294
294
  }
295
295
  return 'OK: created ' + r.row.id + ' at ' + r.row.level + warn;
296
296
  }
@@ -298,7 +298,7 @@ export function buildMcpTools(deps) {
298
298
  const cur = locate(hits, server, levelArg, listHint(exec && exec.agent));
299
299
  const transport = args && args.transport !== undefined ? String(args.transport) : String(cur.transport);
300
300
  if (transport !== 'stdio' && transport !== 'streamable-http') {
301
- throw new Error('transport 只能是 stdio 或 streamable-http(这台现在是:' + String(cur.transport) + ')');
301
+ throw new Error('transport must be stdio or streamable-http (this one is: ' + String(cur.transport) + ')');
302
302
  }
303
303
  const touched = [];
304
304
  const ignored = [];
@@ -365,15 +365,15 @@ export function buildMcpTools(deps) {
365
365
  // 直接写备注。这与"三个可改字段一个都没给就拒"是同一条理由的两面。
366
366
  if (!touched.length && !levelChanged) {
367
367
  if (!hintGiven)
368
- throw new Error('没有要改的东西:transport / url / command / args / headers / env / level / hint 至少给一个');
368
+ throw new Error('nothing to change: give at least one of transport / url / command / args / headers / env / level / hint');
369
369
  const n = await deps.mcpmNote({ id: cur.id, note: hintText });
370
370
  if (!n || n.ok === false)
371
- throw new Error((n && n.error) || '写备注失败');
372
- return 'OK: updated ' + String(cur.serverName) + '(' + (clearsNote ? 'hint 已清空' : '改了 hint') + ')';
371
+ throw new Error((n && n.error) || 'failed to write the hint');
372
+ return 'OK: updated ' + String(cur.serverName) + ' (' + (clearsNote ? 'hint cleared' : 'hint changed') + ')';
373
373
  }
374
374
  const r = await deps.mcpmEdit(payload);
375
375
  if (!r || r.ok === false)
376
- throw new Error((r && r.error) || '修改服务器失败');
376
+ throw new Error((r && r.error) || 'failed to update the server');
377
377
  // `hint` 进 `changed` 必须**等写成功之后**(2026-10-15 修 P1-6):此前按意图先算进去,
378
378
  // mcpmNote 失败时回执一边说"改了 hint"、一边 WARN"备注没写进去",模型读不出到底改没改。
379
379
  const changed = touched.concat(levelChanged ? ['level'] : []);
@@ -385,14 +385,14 @@ export function buildMcpTools(deps) {
385
385
  if (hintGiven) {
386
386
  const n = await deps.mcpmNote({ id: cur.id, note: hintText });
387
387
  if (!n || n.ok === false)
388
- warn += '\nWARN: 服务器已保存,但备注没写进去(' + ((n && n.error) || '未知原因') + ')';
388
+ warn += '\nWARN: the server was saved, but the hint was not written (' + ((n && n.error) || 'unknown reason') + ')';
389
389
  else
390
- changed.push(clearsNote ? 'hint(已清空)' : 'hint');
390
+ changed.push(clearsNote ? 'hint (cleared)' : 'hint');
391
391
  }
392
392
  if (ignored.length) {
393
- warn += '\nWARN: ' + ignored.join(' / ') + ' 与当前 transport(' + transport + ')无关,已忽略';
393
+ warn += '\nWARN: ' + ignored.join(' / ') + ' do not apply to the current transport (' + transport + ') and were ignored';
394
394
  }
395
- return 'OK: updated ' + String(cur.serverName) + '(改了 ' + changed.join('、') + ')' + warn;
395
+ return 'OK: updated ' + String(cur.serverName) + ' (changed ' + changed.join(', ') + ')' + warn;
396
396
  },
397
397
  }));
398
398
  }
@@ -26,7 +26,7 @@ export function buildMemoryTools(deps) {
26
26
  name: 'memory_manager_list',
27
27
  // 末尾**不点名** `scene_manager_list`:场景一族在出厂默认里就是关着的,指向一条模型没有的
28
28
  // 工具只会让它去猜名字(用户 2026-09-23 把默认名单扩到十五条时去掉的这句)。
29
- description: 'List memories (id, scene, enabled, form flat|bundle, description) plus the scene injection currently follows. What is injected each turn is in the「当前记忆」reminder; use this for ids and for entries that are off. Pass a `scene` to filter by scene; all=true lists everything.',
29
+ description: 'List memories (id, scene, enabled, form flat|bundle, description) plus the scene injection currently follows. What is injected each turn is in the "Current memories" reminder; use this for ids and for entries that are off. Pass a `scene` to filter by scene; all=true lists everything.',
30
30
  parameters: {
31
31
  // 参数名与其余五个域同词(2026-10-14 对抗性审查 P1-3):同一个概念在工具面只该有一个名字,
32
32
  // 而 `scene` 是它在 `scene_manager_*`、`<scene>/<name>` 与注入段标题里的叫法。
@@ -44,7 +44,7 @@ export function buildMemoryTools(deps) {
44
44
  opArgs.group = args.scene;
45
45
  const r = await deps.rulesOps['rules-list'](opArgs);
46
46
  if (!r || r.ok === false)
47
- throw new Error((r && r.error) || '读取规则失败');
47
+ throw new Error((r && r.error) || 'failed to read memories');
48
48
  // 「会注入」的判定与记忆段同源(renderSceneMemory):global 场景恒常注入,其余场景要
49
49
  // 在 index.active 里,再叠加单条 enabled 与同名 bundle 的 shadowed。默认只列这些,
50
50
  // 全列一次会把没启用的条目也算进上下文(用户裁定 2026-09-16,与技能列表同口径)。
@@ -52,37 +52,37 @@ export function buildMemoryTools(deps) {
52
52
  (group !== '' && (r.activeMode === 'all' || String(r.activeScene || '') === group));
53
53
  const stateOf = (x) => {
54
54
  if (x.shadowed === true)
55
- return { injects: false, label: '被同名覆盖' };
55
+ return { injects: false, label: 'shadowed' };
56
56
  if (x.enabled === false)
57
- return { injects: false, label: '已停用' };
57
+ return { injects: false, label: 'disabled' };
58
58
  if (!sceneOn(String(x.group || '')))
59
- return { injects: false, label: '场景未启用' };
60
- return { injects: true, label: '已启用' };
59
+ return { injects: false, label: 'scene off' };
60
+ return { injects: true, label: 'injects' };
61
61
  };
62
62
  const rows = (r.rules || []).map((x) => ({ x, ...stateOf(x) }));
63
63
  const shown = showAll ? rows : rows.filter((row) => row.injects);
64
- const lines = shown.map(({ x, label }) => ('- ' + x.id + ' [' + (x.group || '未归属场景') + '] ' + label +
64
+ const lines = shown.map(({ x, label }) => ('- ' + x.id + ' [' + (x.group || 'no scene') + '] ' + label +
65
65
  // 形态与附件(2026-10-15):此前不报,模型分不清哪条是 bundle、更不知道它带了几个附件
66
66
  // (`rules-list` 早就把 `form` 与 bundle 的附件摘要给了出来,只是这里没渲染)。
67
67
  (x.form === 'bundle'
68
- ? ' · bundle' + (x.attach && x.attach.count ? '(' + x.attach.count + ' 个附件)' : '')
68
+ ? ' · bundle' + (x.attach && x.attach.count ? ' (' + x.attach.count + ' attachment(s))' : '')
69
69
  : '') +
70
70
  (x.description ? ' — ' + x.description : '')));
71
71
  // 注入按哪个场景走 —— 上面那句「场景未启用」只有配上它才可解读:少了这一行,
72
72
  // 模型看到一片"未启用"会以为是条目自己的问题。(场景本身的清单在 `scene_manager_list`,
73
73
  // 2026-09-23 拆过去了;这里只留"记忆按哪一集注入",因为那是记忆域自己的现状。)
74
74
  const scope = r.activeMode === 'all'
75
- ? '全部场景(历史默认,未收窄)'
76
- : (r.activeScene ? '「' + String(r.activeScene) + '」' : '未启用任何场景');
77
- const header = '记忆:' + (showAll
78
- ? rows.length + ' 条'
79
- : shown.length + ' 条会注入 / 共 ' + rows.length + ' 条' +
80
- (shown.length === rows.length ? '' : '(传 all=true 看全部)')) +
81
- '\n注入范围:' + scope + ' + global(global 恒常注入)\n';
75
+ ? 'every scene (historical default, not narrowed)'
76
+ : (r.activeScene ? '"' + String(r.activeScene) + '"' : 'no scene is on');
77
+ const header = 'memories: ' + (showAll
78
+ ? rows.length + ' total'
79
+ : shown.length + ' injecting / ' + rows.length + ' total' +
80
+ (shown.length === rows.length ? '' : ' (pass all=true for all)')) +
81
+ '\ninjects from: ' + scope + ' + global (global always injects)\n';
82
82
  // 注入边界:压制型预设(persona complete / 关闭运行时上下文)下本插件默认不注入,
83
83
  // 此时列出的记忆**不在**模型上下文里。必须说出来,否则模型会假设自己已经看到正文。
84
84
  const notice = await deps.reachNoticeForAgent(deps.presetRoster(), exec && exec.agent && exec.agent.ctx, deps.injectNoticeOptions());
85
- return header + (lines.join('\n') || '(无记忆)') + notice;
85
+ return header + (lines.join('\n') || '(none)') + notice;
86
86
  },
87
87
  }));
88
88
  register(defineTool({
@@ -99,7 +99,7 @@ export function buildMemoryTools(deps) {
99
99
  // op 的字段名是 `id`,不改 op:这里显式转换(键名对模型是 `<场景>/<名字>`,对 op 是 `id`)。
100
100
  const r = await deps.rulesOps['rules-read']({ id: String((args && args.memory) || '') });
101
101
  if (!r || r.ok === false)
102
- throw new Error((r && r.error) || '读取规则失败');
102
+ throw new Error((r && r.error) || 'failed to read memories');
103
103
  const rule = r.rule || {};
104
104
  // 形态与附件名(2026-10-15):op 一直把 `attachments` 给了出来,这里此前**整个丢掉**,
105
105
  // 于是模型连"这条记忆是 bundle、带了 3 个附件"都看不到。只给名字不给内容 ——
@@ -107,9 +107,9 @@ export function buildMemoryTools(deps) {
107
107
  const attach = Array.isArray(rule.attachments) ? rule.attachments : [];
108
108
  const form = rule.form === 'bundle' ? 'bundle' : 'flat';
109
109
  const attLine = attach.length
110
- ? '\n附件(' + attach.length + ' 个):' + attach.map((a) => String((a && a.name) || a)).join('、')
110
+ ? '\nattachments (' + attach.length + '): ' + attach.map((a) => String((a && a.name) || a)).join(', ')
111
111
  : '';
112
- return '# ' + rule.id + '(' + form + ')' + attLine + '\n\n' + (rule.body || '');
112
+ return '# ' + rule.id + ' (' + form + ')' + attLine + '\n\n' + (rule.body || '');
113
113
  },
114
114
  }));
115
115
  register(defineTool({
@@ -143,10 +143,10 @@ export function buildMemoryTools(deps) {
143
143
  // `enabled` 必须是布尔:`rules-toggle` 缺了它直接拒(ops/memory.ts),但那是给用户看的
144
144
  // 中文 op 错;这里先挡一句说清"这条工具要什么"。
145
145
  if (typeof (args && args.enabled) !== 'boolean')
146
- throw new Error('缺少参数:enabled 必须是 true 或 false');
146
+ throw new Error('missing argument: enabled must be true or false');
147
147
  const r = await deps.rulesOps['rules-toggle']({ id: memory, enabled: args.enabled });
148
148
  if (!r || r.ok === false)
149
- throw new Error((r && r.error) || '切换记忆启停失败');
149
+ throw new Error((r && r.error) || 'failed to switch this memory');
150
150
  const rule = r.rule || {};
151
151
  // 「启用了一条不在启用场景里的记忆」是一次静默无效:单条开关拨上去了,注入集里却没有它。
152
152
  // 这一句不说,模型会以为已经生效 —— 与 skill_manager_switch 说清影子副本同一条理由。
@@ -157,7 +157,7 @@ export function buildMemoryTools(deps) {
157
157
  const group = String(rule.group || '');
158
158
  const on = group === 'global' || (group !== '' && (all.activeMode === 'all' || String(all.activeScene || '') === group));
159
159
  if (!on)
160
- sceneNote = ' — BUT its scene「' + group + '」is not the active one, so it still will not be injected; enable that scene (or move it to global) first';
160
+ sceneNote = ' — BUT its scene "' + group + '" is not the one injecting, so it still will not be injected; turn that scene on (or move it to global) first';
161
161
  }
162
162
  }
163
163
  catch {
@@ -193,7 +193,7 @@ export function buildMemoryTools(deps) {
193
193
  // 清单里打印的就是 `<场景>/<名字>`,照抄比拼装准(§2 规则一)。
194
194
  const slash = memory.indexOf('/');
195
195
  if (slash <= 0 || slash === memory.length - 1) {
196
- throw new Error('memory 必须是 <场景>/<名字> 两段(照抄清单里那一行;全局场景写 global/<名字>):' + (memory || '(空)'));
196
+ throw new Error('memory must be <scene>/<name> (copy that from the listing; the always-on scene is written global/<name>): ' + (memory || '(empty)'));
197
197
  }
198
198
  const group = memory.slice(0, slash);
199
199
  const name = memory.slice(slash + 1);
@@ -201,7 +201,7 @@ export function buildMemoryTools(deps) {
201
201
  // 这一侧 —— `rules-create` 对同名是**拒绝**的(`error.rules.exists`),op 不会 upsert。
202
202
  const all = await deps.rulesOps['rules-list']({});
203
203
  if (!all || all.ok === false)
204
- throw new Error((all && all.error) || '读取规则失败');
204
+ throw new Error((all && all.error) || 'failed to read memories');
205
205
  const hit = (all.rules || []).find((x) => String(x.id) === memory);
206
206
  if (hit) {
207
207
  // 改的分支一律 read-modify-write(省略 = 保持),因为 `rules-update` 是整份重写。
@@ -209,7 +209,7 @@ export function buildMemoryTools(deps) {
209
209
  // 模型以为改了点什么,其实只是把文件重写了一遍。
210
210
  const touched = ['body', 'description', 'nextName'].filter((k) => args[k] !== undefined);
211
211
  if (!touched.length)
212
- throw new Error('没有要改的东西:body / description / nextName 至少给一个');
212
+ throw new Error('nothing to change: give at least one of body / description / nextName');
213
213
  const payload = { id: memory };
214
214
  // 只带模型真给过的键:`description` 传空串在 op 里是"删除并重新派生",与"未给"是两种
215
215
  // 意图,都必须原样透传 —— 所以用 `!== undefined` 而不是真值判断。
@@ -221,10 +221,10 @@ export function buildMemoryTools(deps) {
221
221
  payload.name = args.nextName;
222
222
  const r = await deps.rulesOps['rules-update'](payload);
223
223
  if (!r || r.ok === false)
224
- throw new Error((r && r.error) || '更新记忆失败');
224
+ throw new Error((r && r.error) || 'failed to update the memory');
225
225
  const rule = r.rule || {};
226
- return 'OK: updated ' + String(rule.id || memory) + '(改了 ' + touched.join('、') + ')' +
227
- (rule.id && String(rule.id) !== memory ? ' → 现在是 ' + rule.id : '');
226
+ return 'OK: updated ' + String(rule.id || memory) + ' (changed ' + touched.join(', ') + ')' +
227
+ (rule.id && String(rule.id) !== memory ? ' → now ' + rule.id : '');
228
228
  }
229
229
  // 建的分支:正文与描述都必填。`rules-create` 自己会拒空正文与非法名(ops/memory.ts),
230
230
  // 但那是给用户看的中文 op 错;模型需要的是"这条工具要什么",所以在工具侧先挡、点名缺哪个。
@@ -235,7 +235,7 @@ export function buildMemoryTools(deps) {
235
235
  description.trim() === '' ? 'description' : '',
236
236
  ].filter((x) => x !== '');
237
237
  if (missing.length)
238
- throw new Error('新建记忆必须给 ' + missing.join(' 与 ') + '(' + memory + ' 还不存在)');
238
+ throw new Error('creating a memory needs ' + missing.join(' and ') + ' (' + memory + ' does not exist yet)');
239
239
  // 查重只**说出来**,不拒绝 —— 猜错一次就把该记的东西记不下去,比多记一条代价大得多;
240
240
  // 而且"像不像同一条"本来就是人判断的事。只在建的分支提示:改的分支没有"再记一条"的风险。
241
241
  const norm = (value) => String(value ?? '').toLowerCase().replace(/\s+/g, ' ').trim();
@@ -256,11 +256,11 @@ export function buildMemoryTools(deps) {
256
256
  .slice(0, 3);
257
257
  const r = await deps.rulesOps['rules-create']({ group, name, description, body });
258
258
  if (!r || r.ok === false)
259
- throw new Error((r && r.error) || '创建规则失败');
259
+ throw new Error((r && r.error) || 'failed to create the memory');
260
260
  // 刚建好的这条自己会在列表里,从名单里去掉。
261
261
  const dup = similar.filter((id) => id !== String(r.rule.id));
262
262
  const warn = dup.length
263
- ? `\n注意:已有 ${dup.length} 条名字或描述与它很像(${dup.join('、')})—— 若是同一件事,改那一条而不是留着两条。`
263
+ ? `\nnote: ${dup.length} existing memories have a similar name or description (${dup.join(', ')}) — if it is the same thing, update that one instead of keeping two.`
264
264
  : '';
265
265
  // 回执必须同时说清**两个**条件(2026-10-15 smoke-test P1-3):新建的记忆在 op 层是
266
266
  // `enabled: false`(ops/memory.ts 的 rulesCreate),而原先这句只提场景 —— 读起来像
@@ -268,10 +268,10 @@ export function buildMemoryTools(deps) {
268
268
  // 的 enableHint 同一形状:点名的工具在出厂默认名单里是关着的,关着就改说"用户开启后"。
269
269
  const sceneActive = group === 'global' ||
270
270
  (group !== '' && (all.activeMode === 'all' || String(all.activeScene || '') === group));
271
- const sceneTail = sceneActive ? '' : ',并启用场景「' + group + '」';
271
+ const sceneTail = sceneActive ? '' : ', and turn scene "' + group + '" on';
272
272
  const enableHint = deps.toolVisible('memory_manager_switch')
273
- ? '(新建默认停用,要生效先 memory_manager_switch 开它' + sceneTail + ')'
274
- : '(新建默认停用,用户开启后才能生效' + sceneTail + ')';
273
+ ? ' (created disabled — turn it on first with memory_manager_switch' + sceneTail + ')'
274
+ : ' (created disabled — the user has to turn it on first' + sceneTail + ')';
275
275
  return 'OK: created ' + r.rule.id + enableHint + warn;
276
276
  },
277
277
  }));
@@ -19,7 +19,7 @@ export function buildPromptTools(deps) {
19
19
  // 那句 `(that reminder is absent when the preset carries the official instructions line)`
20
20
  // 删掉(2026-10-15 注入文本审查):模型**观测不到**"official instructions line"这个条件,
21
21
  // 拿它做不了任何决定 —— reminder 在不在,它抬头就能看见,不必由描述来预告。
22
- description: 'List AGENTS.md presets (id, description, active state, file path); read that file for a preset body. The one in effect is in the「当前提示词」reminder, whose「来源:」line names the file. Defaults to the one in effect; all=true for the whole library.',
22
+ description: 'List AGENTS.md presets (id, description, active state, file path); read that file for a preset body. The one in effect is in the "Current prompt" reminder, whose "Instructions from:" line names the file. Defaults to the one in effect; all=true for the whole library.',
23
23
  parameters: {
24
24
  all: { type: 'boolean', description: 'Include inactive presets (default false).' },
25
25
  },
@@ -28,7 +28,7 @@ export function buildPromptTools(deps) {
28
28
  const showAll = Boolean(args && args.all === true);
29
29
  const r = await deps.promptOps['agentsmd-list']({});
30
30
  if (!r || r.ok === false)
31
- throw new Error((r && r.error) || '读取提示词预设失败');
31
+ throw new Error((r && r.error) || 'failed to read the AGENTS.md presets');
32
32
  const all = r.presets || [];
33
33
  // 默认只列「这份 AGENTS.md 来自哪个预设」(用户裁定 2026-09-16)。
34
34
  // 判定分两级:`active`(场景驱动时 = 场景绑的那份;否则 = 内容逐字节相同)优先,
@@ -88,21 +88,21 @@ export function buildPromptTools(deps) {
88
88
  // 不说的话用户以为 AGENTS.md 还在(0.19.1 界面弹窗报的就是这两种)。
89
89
  const r = await deps.unapplyPreset();
90
90
  if (!r.ok)
91
- throw new Error(r.error || '取消应用失败');
91
+ throw new Error(r.error || 'failed to cancel the application');
92
92
  return r.removed === true
93
93
  ? 'OK: application cancelled — ~/.dsh/AGENTS.md did not exist before the run started, so the file was removed (effective next turn).'
94
94
  : 'OK: application cancelled — ~/.dsh/AGENTS.md is back to what it was before the run started (effective next turn).';
95
95
  }
96
96
  const id = String((args && args.id) || '').trim();
97
97
  if (id === '')
98
- throw new Error('缺少参数:id(只有 apply=false 取消时才不用给)');
98
+ throw new Error('missing argument: id (only optional when apply=false cancels)');
99
99
  const r = await deps.applyPresetGuarded(id);
100
100
  if (!r.ok)
101
101
  throw new Error(r.error);
102
102
  // 场景驱动时写的是「场景绑定」,回执必须说清改的是哪个场景 —— 只说「已应用到
103
103
  // AGENTS.md」会让模型对用户谎报(用户不知道自己的场景绑定被换掉了)。
104
104
  if (r.viaScene) {
105
- return 'OK: preset ' + id + ' is now the prompt binding of scene 「' + (r.scene || '') + '」 and the scene was re-synced (effective next turn; ~/.dsh/AGENTS.md now carries that scene\'s binding).';
105
+ return 'OK: preset ' + id + ' is now the prompt binding of scene "' + (r.scene || '') + '" and the scene was re-synced (effective next turn; ~/.dsh/AGENTS.md now carries that scene\'s binding).';
106
106
  }
107
107
  // 生效时机与宿主行为对齐(index.ts 提示词服务一节考证过):dsh-agent-instructions
108
108
  // 每个 pre-step 都 stat 比对版本,写文件即下一轮重读 —— 不是"下个会话"。说错了
@@ -50,13 +50,13 @@ function restoredName(r) {
50
50
  async function runRemove(deps, cfg, args) {
51
51
  const action = String((args && args.action) || '');
52
52
  if (action !== 'trash' && action !== 'restore' && action !== 'list') {
53
- throw new Error('action 只能是 trash / restore / list(收到:' + (action || '(空)') + ')');
53
+ throw new Error('action must be trash / restore / list (got: ' + (action || '(empty)') + ')');
54
54
  }
55
55
  if (action === 'list') {
56
56
  const entries = await cfg.list();
57
57
  return entries.length
58
- ? '回收站(' + entries.length + ' 条):\n' + entries.map((e) => '- ' + e.id + ' · ' + e.name + (e.deletedAt ? ' · ' + e.deletedAt : '')).join('\n')
59
- : '回收站是空的。';
58
+ ? 'trash: ' + entries.length + ' item(s)\n' + entries.map((e) => '- ' + e.id + ' · ' + e.name + (e.deletedAt ? ' · ' + e.deletedAt : '')).join('\n')
59
+ : 'the trash is empty.';
60
60
  }
61
61
  // 进 / 出回收站都是写:过场景冻结守卫(与面板的删除按钮同一档)。
62
62
  const blocked = await deps.lockedSceneGuard();
@@ -64,19 +64,19 @@ async function runRemove(deps, cfg, args) {
64
64
  throw new Error(blocked);
65
65
  const id = String((args && args.id) || '').trim();
66
66
  if (id === '')
67
- throw new Error('缺少参数:id(action=' + action + ' 需要)');
67
+ throw new Error('missing argument: id (required by action=' + action + ')');
68
68
  const r = action === 'trash' ? await cfg.trash(args) : await cfg.restore(args);
69
69
  if (!r || r.ok === false)
70
- throw new Error((r && r.error) || (action === 'trash' ? '移入回收站失败' : '恢复失败'));
70
+ throw new Error((r && r.error) || (action === 'trash' ? 'failed to move it to the trash' : 'restore failed'));
71
71
  if (action === 'trash') {
72
- return 'OK: ' + cfg.label + '「' + id + '」已移入回收站(可恢复;永久删除在面板的回收站里)';
72
+ return 'OK: ' + cfg.label + ' "' + id + '" moved to the trash (recoverable; permanent deletion lives in the panel trash)';
73
73
  }
74
74
  // 恢复的回执要说**恢复成了什么**,而不是回收站条目 id(2026-10-15 smoke-test P1-6):
75
75
  // `id` 是 `action=list` 给的那一串(记忆 `mv…`、技能 `<时间戳>-<uuid>`),模型看到它
76
76
  // 认不出恢复了哪一条;退号恢复时最终落盘的名字还与回收站里的原名不同,更要说准。
77
77
  const name = restoredName(r) || id;
78
- return 'OK: 已从回收站恢复 ' + cfg.label + '「' + name + '」' +
79
- (name === id ? '' : '(回收站条目 ' + id + ')');
78
+ return 'OK: restored ' + cfg.label + ' "' + name + '" from the trash' +
79
+ (name === id ? '' : ' (trash entry ' + id + ')');
80
80
  }
81
81
  export function buildRemoveTools(deps) {
82
82
  const { defineTool, register } = deps;
@@ -86,18 +86,18 @@ export function buildRemoveTools(deps) {
86
86
  const pr = deps.promptOps;
87
87
  const sc = deps.sceneOps;
88
88
  const memoryCfg = {
89
- noun: 'memory', label: '记忆',
89
+ noun: 'memory', label: 'memory',
90
90
  trash: (a) => mem['rules-remove']({ id: String(a.id || '') }),
91
91
  restore: (a) => mem['rules-restore']({ trashId: String(a.id || '') }),
92
92
  list: async () => {
93
93
  const r = await mem['rules-trash-list']({});
94
94
  if (!r || r.ok === false)
95
- throw new Error((r && r.error) || '读取回收站失败');
95
+ throw new Error((r && r.error) || 'failed to read the trash');
96
96
  return (r.entries || []).map((e) => ({ id: String(e.trashId), name: String(e.name), deletedAt: e.deletedAt }));
97
97
  },
98
98
  };
99
99
  const skillCfg = {
100
- noun: 'skill', label: '技能',
100
+ noun: 'skill', label: 'skill',
101
101
  // 与 `skill_manager_switch` 同一条(smoke-test P0-1):不给 root 就按名字在全部来源里
102
102
  // 找胜出的那一份。默认值原本是 dsh,而 `skill_manager_save` 落 hub —— 「建完就删」
103
103
  // 必然报「技能不存在」。删一个真的不存在的技能时退回 dsh,交给 op 报它自己的错。
@@ -117,30 +117,30 @@ export function buildRemoveTools(deps) {
117
117
  },
118
118
  };
119
119
  const subagentCfg = {
120
- noun: 'persona', label: '人设',
120
+ noun: 'persona', label: 'persona',
121
121
  trash: (a) => sub['subagent-delete']({ name: String(a.id || '') }),
122
122
  restore: (a) => sub['subagent-trash-restore']({ id: String(a.id || '') }),
123
123
  list: async () => {
124
124
  const r = await sub['subagent-trash-list']({});
125
125
  if (!r || r.ok === false)
126
- throw new Error((r && r.error) || '读取回收站失败');
126
+ throw new Error((r && r.error) || 'failed to read the trash');
127
127
  return (r.trash || []).map((e) => ({ id: String(e.id), name: String(e.name), deletedAt: e.deletedAt }));
128
128
  },
129
129
  };
130
130
  const promptCfg = {
131
- noun: 'preset', label: '提示词预设',
131
+ noun: 'preset', label: 'preset',
132
132
  trash: (a) => pr['agentsmd-remove']({ id: String(a.id || '') }),
133
133
  restore: (a) => pr['agentsmd-trash-restore']({ id: String(a.id || '') }),
134
134
  list: async () => {
135
135
  const r = await pr['agentsmd-trash-list']({});
136
136
  if (!r || r.ok === false)
137
- throw new Error((r && r.error) || '读取回收站失败');
137
+ throw new Error((r && r.error) || 'failed to read the trash');
138
138
  const arr = Array.isArray(r) ? r : (r.presets || r.trash || r.entries || []);
139
139
  return arr.map((e) => ({ id: String(e.id), name: String(e.name || e.id), deletedAt: e.deletedAt }));
140
140
  },
141
141
  };
142
142
  const sceneCfg = {
143
- noun: 'scene', label: '场景',
143
+ noun: 'scene', label: 'scene',
144
144
  // 删场景走 handlers 里那份(`withAgentsMdSync` 包过):直调 `memoriesService.ops` 会漏掉
145
145
  // 「场景绑的预设曾写进 AGENTS.md,删了要跟着处理」那一步。
146
146
  trash: (a) => deps.sceneRemove({ name: String(a.id || '') }),
@@ -148,7 +148,7 @@ export function buildRemoveTools(deps) {
148
148
  list: async () => {
149
149
  const r = await sc['scene-trash-list']({});
150
150
  if (!r || r.ok === false)
151
- throw new Error((r && r.error) || '读取回收站失败');
151
+ throw new Error((r && r.error) || 'failed to read the trash');
152
152
  return (r.trash || []).map((e) => ({ id: String(e.id), name: String(e.name), deletedAt: e.deletedAt }));
153
153
  },
154
154
  };