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.
- package/CHANGELOG.md +37 -0
- package/README.md +4 -1
- package/README_EN.md +4 -1
- package/docs/update.md +1 -0
- package/lib/client.js +117 -11
- package/lib/compat/capability-specs.js +60 -1
- package/lib/compat/host-contracts.js +72 -0
- package/lib/compat/host-probe.js +59 -0
- package/lib/compat/host-roots.js +1 -1
- package/lib/compat/probe.js +84 -13
- package/lib/context-inject-frame.js +134 -26
- package/lib/context-inject.js +12 -9
- package/lib/http-api.js +3 -2
- package/lib/http-fence.js +26 -1
- package/lib/index.js +5 -2
- package/lib/mcp/manager.js +6 -1
- package/lib/mcp/state-section.js +75 -34
- package/lib/memories/constants.js +39 -7
- package/lib/memories/projection.js +18 -18
- package/lib/memories/service.js +8 -7
- package/lib/memories/snapshot.js +27 -17
- package/lib/ops/compat.js +64 -2
- package/lib/ops/memory.js +2 -2
- package/lib/ops/prompts.js +30 -2
- package/lib/request-gate.js +5 -4
- package/lib/skills/catalog.js +2 -2
- package/lib/subagents/catalog.js +4 -4
- package/lib/subagents/child-preset.js +120 -0
- package/lib/subagents/persona-parse.js +9 -9
- package/lib/subagents/run-provider.js +218 -62
- package/lib/subagents/tool-filter.js +12 -12
- package/lib/subagents/tools.js +29 -22
- package/lib/tools/deps.js +4 -4
- package/lib/tools/mcp.js +34 -34
- package/lib/tools/memory.js +34 -34
- package/lib/tools/prompt.js +5 -5
- package/lib/tools/remove.js +17 -17
- package/lib/tools/scene.js +66 -66
- package/lib/tools/skills.js +21 -21
- package/lib/tools/subagent.js +13 -13
- package/lib/tools/table.js +2 -2
- 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 = '
|
|
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 = '
|
|
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('
|
|
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('
|
|
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('
|
|
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
|
-
// ①
|
|
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
|
|
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
|
|
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
|
|
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) || '
|
|
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:
|
|
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:
|
|
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
|
-
//
|
|
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('
|
|
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) || '
|
|
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('
|
|
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('
|
|
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('
|
|
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:
|
|
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
|
|
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('
|
|
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) + '
|
|
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:
|
|
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
|
|
390
|
+
changed.push(clearsNote ? 'hint (cleared)' : 'hint');
|
|
391
391
|
}
|
|
392
392
|
if (ignored.length) {
|
|
393
|
-
warn += '\nWARN: ' + ignored.join(' / ') + '
|
|
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) + '
|
|
395
|
+
return 'OK: updated ' + String(cur.serverName) + ' (changed ' + changed.join(', ') + ')' + warn;
|
|
396
396
|
},
|
|
397
397
|
}));
|
|
398
398
|
}
|
package/lib/tools/memory.js
CHANGED
|
@@ -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
|
|
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 || '
|
|
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 ? '
|
|
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 ? '
|
|
77
|
-
const header = '
|
|
78
|
-
? rows.length + '
|
|
79
|
-
: shown.length + '
|
|
80
|
-
(shown.length === rows.length ? '' : '
|
|
81
|
-
'\
|
|
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') || '(
|
|
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
|
-
? '\
|
|
110
|
+
? '\nattachments (' + attach.length + '): ' + attach.map((a) => String((a && a.name) || a)).join(', ')
|
|
111
111
|
: '';
|
|
112
|
-
return '# ' + rule.id + '
|
|
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('
|
|
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
|
|
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
|
|
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('
|
|
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) + '
|
|
227
|
-
(rule.id && String(rule.id) !== memory ? ' →
|
|
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('
|
|
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
|
-
? `\
|
|
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 ? '' : '
|
|
271
|
+
const sceneTail = sceneActive ? '' : ', and turn scene "' + group + '" on';
|
|
272
272
|
const enableHint = deps.toolVisible('memory_manager_switch')
|
|
273
|
-
? '
|
|
274
|
-
: '
|
|
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
|
}));
|
package/lib/tools/prompt.js
CHANGED
|
@@ -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
|
|
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('
|
|
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
|
|
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 比对版本,写文件即下一轮重读 —— 不是"下个会话"。说错了
|
package/lib/tools/remove.js
CHANGED
|
@@ -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
|
|
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
|
-
? '
|
|
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('
|
|
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 + '
|
|
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:
|
|
79
|
-
(name === 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
|
};
|