dsh-plugin-tool-management 0.13.0 → 0.14.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 (51) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +91 -123
  3. package/README_EN.md +99 -126
  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 +28 -0
  21. package/lib/client.js +578 -124
  22. package/lib/compat/preset-reach.js +6 -3
  23. package/lib/context-inject.js +86 -32
  24. package/lib/index.js +251 -92
  25. package/lib/mcp/manager.js +3 -14
  26. package/lib/mcp/secret-guard.js +21 -0
  27. package/lib/mcp/state-section.js +13 -11
  28. package/lib/memories/constants.js +72 -8
  29. package/lib/memories/index-io.js +1 -1
  30. package/lib/memories/projection.js +67 -31
  31. package/lib/memories/service.js +17 -2
  32. package/lib/memories/snapshot.js +113 -22
  33. package/lib/op-registry.js +8 -2
  34. package/lib/ops/candidates.js +11 -0
  35. package/lib/ops/compat.js +10 -2
  36. package/lib/ops/sessions.js +1 -1
  37. package/lib/prompts/service.js +25 -2
  38. package/lib/scenes/candidates.js +56 -0
  39. package/lib/skills/core.js +93 -0
  40. package/lib/skills/service.js +34 -2
  41. package/lib/subagents/service.js +37 -4
  42. package/lib/subagents/tools.js +11 -4
  43. package/lib/tools/deps.js +15 -0
  44. package/lib/tools/mcp.js +291 -50
  45. package/lib/tools/memory.js +131 -80
  46. package/lib/tools/prompt.js +27 -13
  47. package/lib/tools/scene.js +351 -0
  48. package/lib/tools/skills.js +93 -17
  49. package/lib/tools/subagent.js +56 -45
  50. package/lib/tools/table.js +204 -15
  51. package/package.json +108 -108
@@ -1,5 +1,5 @@
1
- // 子智能体域的 model 工具(2026-09-19 从 index.ts 的注册区抽出):
2
- // subagent_manager_list / subagent_manager_run / _set_enabled / _create / _update。
1
+ // 子智能体域的 model 工具(2026-09-19 从 index.ts 的注册区抽出;2026-09-23 合并):
2
+ // subagent_manager_list / subagent_manager_run / _switch / _save。
3
3
  //
4
4
  // exec.agent / exec.signal 由工具运行时提供(parent 与取消信号的官方通道)。
5
5
  //
@@ -49,8 +49,8 @@ export function buildSubagentTools(deps) {
49
49
  const ops = () => deps.subagentService.ops;
50
50
  try {
51
51
  register(deps.defineTool({
52
- name: 'subagent_manager_set_enabled',
53
- description: 'Enable or disable one persona. Only enabled personas appear in the「可委派的子智能体」catalog and can be given work by subagent_manager_run. Reversible, and the file is untouched. Only when the user asks or approves.',
52
+ name: 'subagent_manager_switch',
53
+ description: 'Enable or disable one persona. Only enabled personas appear in the「可委派的子智能体」catalog and can be given work. Reversible, and the file is untouched. Only when the user asks or approves.',
54
54
  parameters: {
55
55
  name: { type: 'string', required: true, description: 'Persona name.' },
56
56
  enabled: { type: 'boolean', required: true, description: 'true = enable, false = disable; omission is refused.' },
@@ -73,47 +73,25 @@ export function buildSubagentTools(deps) {
73
73
  }));
74
74
  }
75
75
  catch (e) {
76
- recordFailure('subagent_manager_set_enabled', e);
76
+ recordFailure('subagent_manager_switch', e);
77
77
  }
78
78
  try {
79
79
  register(deps.defineTool({
80
- name: 'subagent_manager_create',
81
- description: 'Create a persona at ~/.dsh/tool-management/subagents/<name>.md. Only on the user\'s explicit request. Write `body` as who this is and how it judges its own work — not the steps or paths, those belong to the task; put hard output requirements in `output` (one per line), not buried in prose. New personas start disabled: enable with subagent_manager_set_enabled before delegating.',
80
+ name: 'subagent_manager_save',
81
+ // 为什么不是直通 `subagent-update`:那个 op 走 serializePersona **整份重写**文件,
82
+ // 缺席的字段一律按空处理。界面每次都带全量表单所以没事,模型只改一句 description
83
+ // 却直通过去,就会把人设正文与工具限制一起冲掉。这里先读现状再合并 —— 合并发生在
84
+ // 工具这一侧,op 的语义不动。
85
+ //
86
+ // 0.14.0:create 与 update 并成一条 upsert。判定也只能在这里做 —— `subagent-create`
87
+ // 对同名是拒绝的、`subagent-update` 对不存在的名字报错,而模型手里的目录可能已经过期。
88
+ description: 'Create a persona at ~/.dsh/tool-management/subagents/<name>.md, or update the one that already has this name — the receipt says which. Omitted fields keep their current values. Write `body` as who this is and how it judges its own work — not the steps or paths, those belong to the task; put hard output requirements in `output` (one per line), not buried in prose. Only on the user\'s explicit request.',
82
89
  parameters: {
83
- name: { type: 'string', required: true, description: 'Persona name (= file name); ≤64 chars, no path separators or < > : " | ? *, must not start with a dot.' },
84
- description: { type: 'string', required: true, description: 'One-line routing description: what it is for and when to pick it. This is what shows up in the delegation catalog.' },
85
- body: { type: 'string', required: true, description: 'Markdown role definition.' },
86
- output: { type: 'string', description: 'Output contract, one requirement per line (rendered to the persona as its own 「输出要求」 section). Omit for no hard requirements.' },
87
- },
88
- output: { schema: { type: 'string' }, render: (_a, v) => text(String(v)) },
89
- async execute(args) {
90
- const blocked = await deps.lockedSceneGuard();
91
- if (blocked)
92
- throw new Error(blocked);
93
- const r = await ops()['subagent-create'](args);
94
- if (!r || r.ok === false)
95
- throw new Error((r && r.error) || '创建人设失败');
96
- return 'OK: persona ' + String(r.name || args.name) + ' created(默认未启用,要委派它先 subagent_manager_set_enabled)';
97
- },
98
- }));
99
- }
100
- catch (e) {
101
- recordFailure('subagent_manager_create', e);
102
- }
103
- try {
104
- register(deps.defineTool({
105
- name: 'subagent_manager_update',
106
- // 为什么不是直通 `subagent-update`:那个 op 走 serializePersona **整份重写**文件,
107
- // 缺席的字段一律按空处理。界面每次都带全量表单所以没事,模型只改一句 description
108
- // 却直通过去,就会把人设正文与工具限制一起冲掉。这里先读现状再合并 —— 合并发生在
109
- // 工具这一侧,op 的语义不动。
110
- description: 'Update a persona: body, description, output contract, or name (renaming re-points scene-profile bindings). Omit a field to keep it — this merges, so you can change one part without restating the role. Only on the user\'s instruction.',
111
- parameters: {
112
- name: { type: 'string', required: true, description: 'Current persona name.' },
113
- nextName: { type: 'string', description: 'New name (rename). Omit to keep it. Refused if a persona already has that name — nothing is overwritten.' },
114
- description: { type: 'string', description: 'New one-line routing description. Omit to keep it.' },
115
- body: { type: 'string', description: 'New Markdown role definition. Omit to keep it.' },
116
- output: { type: 'string', description: 'New output contract, one requirement per line. Empty string clears it. Omit to keep it.' },
90
+ name: { type: 'string', required: true, description: 'Persona name (= file name); ≤64 chars, no path separators or < > : " | ? *.' },
91
+ description: { type: 'string', description: 'One-line routing description — this is what shows up in the delegation catalog. Required when creating; omit = keep.' },
92
+ body: { type: 'string', description: 'Markdown role definition. Required when creating; omit = keep.' },
93
+ output: { type: 'string', description: 'Output contract, one requirement per line. Empty string clears it; omit = keep.' },
94
+ nextName: { type: 'string', description: 'New name (rename); re-points scene-profile bindings. Omit = keep. Refused if a persona already has that name.' },
117
95
  },
118
96
  output: { schema: { type: 'string' }, render: (_a, v) => text(String(v)) },
119
97
  async execute(args) {
@@ -122,10 +100,39 @@ export function buildSubagentTools(deps) {
122
100
  throw new Error(blocked);
123
101
  const name = String((args && args.name) || '').trim();
124
102
  const current = await ops()['subagent-get']({ name });
125
- if (!current || current.ok === false)
126
- throw new Error((current && current.error) || '人设不存在: ' + name);
103
+ if (!current || current.ok === false) {
104
+ // 建的分支:description 与 body 都必填。`subagent-create` 自己会拒(人设名不合法 /
105
+ // 已存在),但那两条是给用户看的 op 错;模型需要的是"这条工具要什么"。
106
+ const description = String((args && args.description) || '').trim();
107
+ const body = String((args && args.body) || '').trim();
108
+ const missing = [
109
+ description === '' ? 'description' : '',
110
+ body === '' ? 'body' : '',
111
+ ].filter((x) => x !== '');
112
+ if (missing.length)
113
+ throw new Error('新建人设必须给 ' + missing.join(' 与 ') + '(' + name + ' 还不存在)');
114
+ const r = await ops()['subagent-create']({ name, description: args.description, body: args.body, output: args.output });
115
+ if (!r || r.ok === false)
116
+ throw new Error((r && r.error) || '创建人设失败');
117
+ // 新建的人设默认停用(与 MCP / 技能同口径)。这句必须留:不说的话模型会直接去委派,
118
+ // 然后收到"人设不可用",而它并不知道自己少拨了一个开关。点名的 subagent_manager_switch
119
+ // 在出厂默认名单里就是关着的 —— 关着就改说"用户启用后才能委派",别指一条模型没有的
120
+ // 工具(与 mcp/skill 的 listHint 同一条纪律)。
121
+ const enableHint = deps.toolVisible('subagent_manager_switch')
122
+ ? '(默认未启用,要委派它先 subagent_manager_switch)'
123
+ : '(默认未启用,用户启用后才能委派)';
124
+ return 'OK: persona ' + String(r.name || name) + ' created' + enableHint;
125
+ }
127
126
  const keep = current.persona || {};
128
- const touched = ['nextName', 'description', 'body', 'output'].filter((k) => args && args[k] !== undefined);
127
+ const touched = [];
128
+ if (args.nextName !== undefined)
129
+ touched.push('nextName');
130
+ if (args.description !== undefined)
131
+ touched.push('description');
132
+ if (args.body !== undefined)
133
+ touched.push('body');
134
+ if (args.output !== undefined)
135
+ touched.push('output');
129
136
  if (!touched.length)
130
137
  throw new Error('没有要改的东西:nextName / description / body / output 至少给一个');
131
138
  const merged = {
@@ -136,6 +143,10 @@ export function buildSubagentTools(deps) {
136
143
  output: args.output !== undefined ? String(args.output) : (Array.isArray(keep.output) ? keep.output.join('\n') : String(keep.output ?? '')),
137
144
  provider: keep.provider ?? '',
138
145
  model: keep.model ?? '',
146
+ // ⚠️ 必须带回来:`subagent-update` 走 `serializePersona` **整份重写**,少带一个字段
147
+ // 就是静默清掉它。`reasoningEffort` 是 0.14.0 新增的,最容易在这里被漏掉 ——
148
+ // 模型只改一句 body,用户配好的思考强度就没了,而且没有任何提示。
149
+ reasoningEffort: keep.reasoningEffort ?? '',
139
150
  tools: keep.tools ?? [],
140
151
  toolsDeny: keep.toolsDeny ?? [],
141
152
  toolsByPreset: keep.toolsByPreset ?? {},
@@ -150,6 +161,6 @@ export function buildSubagentTools(deps) {
150
161
  }));
151
162
  }
152
163
  catch (e) {
153
- recordFailure('subagent_manager_update', e);
164
+ recordFailure('subagent_manager_save', e);
154
165
  }
155
166
  }
@@ -1,8 +1,22 @@
1
1
  // 「模型工具表」开关(2026-09-23)。
2
2
  //
3
- // 为什么要有它:工具表是按**每个请求**付钱的 —— 本插件 20 个工具的 schema 合计 ≈3,100 tok,
4
- // 每一轮都随请求发出,哪怕这一轮根本用不上(实测口径见 review/后续方向.md)。关掉某几个工具,
5
- // 它们整份不进请求(不是"把描述写短点"那种省);面板(114 个 op)与注入通道完全不受影响。
3
+ // 为什么要有它:工具表是按**每个请求**付钱的 —— 本插件 20 个工具的 schema 合计 ≈3,769 tok
4
+ // (口径:整份 `JSON.stringify(definition).length / 4`,与宿主记录的一致),每一轮都随请求
5
+ // 发出,哪怕这一轮根本用不上。关掉某几个工具,它们整份不进请求(不是"把描述写短点"那种省);
6
+ // 面板(118 个 op)与注入通道完全不受影响。
7
+ //
8
+ // 出厂**默认就关着十五条**(20 条里实发只剩 5 条)—— 名单、判据与代价见下面
9
+ // `DEFAULT_HIDDEN_TOOLS`;用户自己额外关的与出厂关的在合计那句里分开报。
10
+ //
11
+ // ⚠️ **那个口径不是 token 数,别拿它跟别的数直接比**(2026-09-23 用真 BPE 词表实测):
12
+ // 工具描述是英文,`字符数 / 4` 对英文**高估约 10%** —— 18 个工具那一版真实是 ≈3,137 tok(o200k)。
13
+ // 反方向也一样:注入段是中文,同一个口径对它**低估约 13%**(四段 716 → 真 823)。
14
+ // 所以"工具表 3,769 / 注入 716"这两个数不是同一把尺子,跨语言比大小会得出错的结论。
15
+ // 口径本身是**宿主的**(`recordToolSize`),我们改不了;能做的只是知道它偏在哪。
16
+ //
17
+ // 20 条这一版**没有重测 BPE**:开发环境里没有分词器(`node_modules` 里只有 `js-tokens`,
18
+ // 那是词法分析器、不是 BPE)。按 18 条时 `3,137 / 3,432 ≈ 0.914` 的比例外推约 3,457 ——
19
+ // 那是**推算不是实测**,别把它当数字用;要真值得在有分词器的环境里重跑一遍。
6
20
  //
7
21
  // 本文件只放纯逻辑(normalize / 分域分组 / 体积汇总)。落盘与 TTL 缓存在 index.ts,与
8
22
  // 注入设置(`inject-settings.json`)走同一套写法。
@@ -15,22 +29,189 @@
15
29
  // · 工具执行门禁(关掉的工具即便被点到也不执行 —— 可见性半边失败时仍有这一半);
16
30
  // · 注入通道与两个目录:目录里「用 `skill_manager_list` 查」这类句子在工具关掉后就是假的。
17
31
  import { INJECT_DOMAIN_KEYS, domainOfTool } from '../context-inject.js';
18
- /** 默认一个都不关:装完即用,省钱的开关由用户自己按需打开。 */
19
- export const DEFAULT_TOOL_TABLE_SETTINGS = { hidden: [] };
20
- /** 把任意输入夹成合法设置(只收非空字符串;重复的只留一份)。 */
21
- export function normalizeToolTableSettings(raw) {
22
- const obj = (raw && typeof raw === 'object' ? raw : {});
23
- const list = Array.isArray(obj.hidden) ? obj.hidden : [];
24
- const hidden = [];
32
+ /**
33
+ * 出厂默认关掉的十五条(0.14.0):整张表 20 条 ≈3,769 tok,关掉后**每轮实发 5 条 ≈1,090**(省 ≈2,679)。
34
+ *
35
+ * 判据是用户 2026-09-23 的三轮裁定,同一条:**这些信息或动作常态下由注入段与面板承担,
36
+ * 平时也用不到模型自己伸手**。三组:
37
+ * · **写侧五条** —— 三个 `*_save`(MCP / 技能 / 人设:配置本体在面板上改)+ 提示词两条
38
+ * (这一族只有两条,而当前生效的那份本来就在「本机提示词」注入段里)。
39
+ * · **读侧四条** —— 三个 `*_list` + `memory_manager_read`。会话开头注入的那几段目录已经写了
40
+ * 当前有什么,模型不必每轮再带一份查询工具。
41
+ * · **场景三条 + 开关三条** —— 场景一族(建 / 勾档案 / 进入退出)与 `skill` / `memory` /
42
+ * `subagent` 三个 `_switch`(用户:「平时使用也很少」)。启停在面板上都有同款,注入段也
43
+ * 写着当前开着哪些。**`mcp_manager_switch` 刻意留着**:它是"这台先给我关了"这条最常用的
44
+ * 临时动作的唯一入口,也是 MCP 域留下来的一条腿。
45
+ *
46
+ * ⚠️ 关掉的是**能力**,不是"冗余",代价如实记在这里(要拿回来就在「兼容」页勾,或整批换:
47
+ * 「恢复方案」):注入的目录是**筛过的视图**(MCP 段只列可用/曾可用的、人设目录只列本场景
48
+ * 绑定且深度够的、技能目录只列可调用的),所以"没开的那几台 / 被同名覆盖的那份技能 / 没进
49
+ * 上下文的记忆正文"在会话里查不到;`*_switch` 关掉后模型也不能再替你拨启停。三处配套改动为
50
+ * 的就是不把模型往墙上推:目录被条数上限截断时那句「用 `X_manager_list` 查」自动退回「另有
51
+ * N 个未列出。」(`listToolVisible`);`mcp_manager_list` / `skill_manager_read` 里"没找到 →
52
+ * 去调 list"那两句先问 `deps.toolVisible`;`memory_manager_list` 的描述不再点名已关的工具。
53
+ *
54
+ * 只在**用户从没记过选择**时生效(见 index.ts 的 `toolTableSettingsFrom`:侧车文件读不出来
55
+ * 才用这份)。已经存过盘的一律照原样,包括显式的 `hidden: []` —— 那是"我全都要",
56
+ * 不是"还没配过"。存过盘的人要拿回这份默认,走界面上的「恢复方案 → 出厂默认」。
57
+ */
58
+ export const DEFAULT_HIDDEN_TOOLS = Object.freeze([
59
+ 'scene_manager_list',
60
+ 'scene_manager_switch',
61
+ 'scene_manager_save',
62
+ 'memory_manager_read',
63
+ 'memory_manager_switch',
64
+ 'mcp_manager_list',
65
+ 'mcp_manager_save',
66
+ 'skill_manager_list',
67
+ 'skill_manager_switch',
68
+ 'skill_manager_save',
69
+ 'subagent_manager_list',
70
+ 'subagent_manager_switch',
71
+ 'subagent_manager_save',
72
+ 'prompt_manager_list',
73
+ 'prompt_manager_apply',
74
+ ]);
75
+ /** 出厂默认那份设置:十五条不发(名单与理由见上面),方案清单是空的。 */
76
+ export const DEFAULT_TOOL_TABLE_SETTINGS = { hidden: [...DEFAULT_HIDDEN_TOOLS], presets: [] };
77
+ /** 方案名上限(与 MCP 备注那类输入同风格:短到能在一行里读,长了也没人看)。 */
78
+ export const PRESET_NAME_MAX_LENGTH = 40;
79
+ /** 存得下的方案数上限 —— 侧车是人手能审的文件,不放开。 */
80
+ export const PRESET_MAX_COUNT = 20;
81
+ const DEFAULT_HIDDEN_SET = new Set(DEFAULT_HIDDEN_TOOLS);
82
+ /** 夹一份工具名名单(只收非空字符串;重复的只留一份)。 */
83
+ function normalizeNameList(list) {
84
+ const out = [];
85
+ if (!Array.isArray(list))
86
+ return out;
25
87
  for (const item of list) {
26
88
  // 只认字符串:`[7]` 这种手改产物不该被 String() 变成一个看着像工具名的东西。
27
89
  if (typeof item !== 'string')
28
90
  continue;
29
91
  const name = item.trim();
30
- if (name !== '' && hidden.indexOf(name) < 0)
31
- hidden.push(name);
92
+ if (name !== '' && out.indexOf(name) < 0)
93
+ out.push(name);
32
94
  }
33
- return { hidden };
95
+ return out;
96
+ }
97
+ /** 夹一份方案清单:名字非空且截到上限、同名只留第一个、总数封顶(超出的是手改产物,丢掉不如不认)。 */
98
+ function normalizePresetList(list) {
99
+ const out = [];
100
+ if (!Array.isArray(list))
101
+ return out;
102
+ for (const item of list) {
103
+ if (!item || typeof item !== 'object')
104
+ continue;
105
+ const raw = item;
106
+ if (typeof raw.name !== 'string')
107
+ continue;
108
+ const name = raw.name.trim().slice(0, PRESET_NAME_MAX_LENGTH);
109
+ if (name === '' || !Array.isArray(raw.hidden))
110
+ continue;
111
+ if (out.some((p) => p.name === name))
112
+ continue;
113
+ out.push({ name, hidden: normalizeNameList(raw.hidden) });
114
+ if (out.length >= PRESET_MAX_COUNT)
115
+ break;
116
+ }
117
+ return out;
118
+ }
119
+ /** 把任意输入夹成合法设置(缺字段 / 类型不对的条目一律不认,绝不凭空造方案)。 */
120
+ export function normalizeToolTableSettings(raw) {
121
+ const obj = (raw && typeof raw === 'object' ? raw : {});
122
+ return { hidden: normalizeNameList(obj.hidden), presets: normalizePresetList(obj.presets) };
123
+ }
124
+ /**
125
+ * 读侧入口:**从没记过选择**(侧车不存在 / 读不出,`readJsonFile` 给 `null`)时给出厂默认,
126
+ * 其余一律按盘上那份夹一遍。
127
+ *
128
+ * 为什么必须分这一叉:默认值不能合成在 `normalize` 里,否则用户把五条全打开、存下
129
+ * `hidden: []` 之后,下一次读又会把它们关回去 —— 那是把"我全都要"读成"还没配过"。
130
+ */
131
+ export function toolTableSettingsFrom(raw) {
132
+ return raw === null || raw === undefined ? DEFAULT_TOOL_TABLE_SETTINGS : normalizeToolTableSettings(raw);
133
+ }
134
+ /**
135
+ * 旧工具名 → 新工具名迁移表(0.14.0 与 0.14.x 两轮改名累积)。
136
+ *
137
+ * 为什么必须有它:`tool-table.json` 存的是**用户点名关掉的那些工具**。0.14.0 把五族并成 17 条
138
+ * 之后,旧名在新表里根本不存在 —— 不迁移的话,用户"关掉了某条"的意图会在新名字上**静默失效**
139
+ * (那条工具照旧每轮发出去,而他以为早就关了)。
140
+ *
141
+ * 映射规则不是字符串替换而是**语义合并**:两条旧工具并成一条 `save` 时,只要原来关掉了其中
142
+ * 任意一条,就应当关掉新的那条 —— 用户当时的意图是"别让模型碰这件事"。
143
+ *
144
+ * 只翻译**认识的名字**,其余原样保留:我们无法区分"用户的旧名"与"别的插件的工具名",
145
+ * 误删别人的条目比留着一条死名更糟。
146
+ */
147
+ export const LEGACY_TOOL_NAME_MAP = Object.freeze({
148
+ // 记忆族:0.14.0 只把 write + update 并成一条 `save`,名字本身没动 —— 所以只有这两条要迁移。
149
+ // (0.14.0 开发中途曾把整族改叫 `scene_memory_manager_*`,但那个名字**从未发布**,用户侧
150
+ // 不可能有它,因此不存在需要从它迁回来的数据。)
151
+ memory_manager_write: 'memory_manager_save',
152
+ memory_manager_update: 'memory_manager_save',
153
+ mcp_manager_set_enabled: 'mcp_manager_switch',
154
+ mcp_manager_restart: 'mcp_manager_switch',
155
+ mcp_manager_add: 'mcp_manager_save',
156
+ skill_manager_create: 'skill_manager_save',
157
+ subagent_manager_create: 'subagent_manager_save',
158
+ subagent_manager_update: 'subagent_manager_save',
159
+ // 0.14.x:「拨一个开关」六域同形 —— `_set_enabled` → `_switch`(与 `mcp_manager_set_enabled`
160
+ // → `mcp_manager_switch` 同一条路)。**改的只是名字,参数不动**(仍是 `enabled` 布尔),
161
+ // 所以这里是一对一翻译、不涉及语义合并。
162
+ memory_manager_set_enabled: 'memory_manager_switch',
163
+ skill_manager_set_enabled: 'skill_manager_switch',
164
+ subagent_manager_set_enabled: 'subagent_manager_switch',
165
+ });
166
+ /**
167
+ * 把一份设置里的旧工具名翻译成新名(**幂等**:已经全是新名时 `changed` 为 false、原样返回)。
168
+ * 翻译后按新名去重(两条旧名并成同一条 save 时只留一份)。
169
+ *
170
+ * 命名方案**一起翻译**:方案里存的就是工具名,只翻当前那份的话,用户恢复旧方案时会把
171
+ * 已经改名的条目原样铺回当前设置 —— 那条"静默失效"的毛病只是晚一步发生。
172
+ */
173
+ export function migrateLegacyToolNames(settings) {
174
+ let changed = false;
175
+ const translate = (list) => {
176
+ const out = [];
177
+ for (const raw of list) {
178
+ const name = LEGACY_TOOL_NAME_MAP[raw] ?? raw;
179
+ if (name !== raw)
180
+ changed = true;
181
+ if (out.indexOf(name) < 0)
182
+ out.push(name);
183
+ }
184
+ return out;
185
+ };
186
+ const hidden = translate(settings.hidden);
187
+ const presets = settings.presets.map((p) => {
188
+ const next = translate(p.hidden);
189
+ // 逐字比对,不能只比长度:`skill_manager_create → skill_manager_save` 是**一对一**,
190
+ // 长度不变 —— 拿长度当"有没有变"的判据会把翻译结果整份丢掉(方案里留的还是旧名)。
191
+ const same = next.length === p.hidden.length && next.every((n, i) => n === p.hidden[i]);
192
+ return same ? p : { name: p.name, hidden: next };
193
+ });
194
+ return changed ? { settings: { hidden, presets }, changed: true } : { settings, changed: false };
195
+ }
196
+ /**
197
+ * 新存一份命名方案:**同名不覆盖**(用户 2026-09-23:「保存方案不能同名」)。"保存"这颗按钮
198
+ * 不该悄悄改掉一份已有的记录 —— 那是破坏性动作,而它连一句确认都没有。撞名时 `exists=true`
199
+ * 且原样返回,由调用方在写盘之前挡下来并如实报错。封顶同样由调用方挡(超了要报错,不是静默丢)。
200
+ */
201
+ export function addPreset(settings, name, hidden) {
202
+ if (settings.presets.some((p) => p.name === name))
203
+ return { settings, exists: true };
204
+ return {
205
+ settings: { hidden: settings.hidden, presets: settings.presets.concat([{ name, hidden: normalizeNameList(hidden) }]) },
206
+ exists: false,
207
+ };
208
+ }
209
+ /** 删一份方案。名字不认识时 `changed=false` —— 由调用方如实报,不静默成功。 */
210
+ export function dropPreset(settings, name) {
211
+ const presets = settings.presets.filter((p) => p.name !== name);
212
+ if (presets.length === settings.presets.length)
213
+ return { settings, changed: false };
214
+ return { settings: { hidden: settings.hidden, presets }, changed: true };
34
215
  }
35
216
  /**
36
217
  * 宿主与界面共用的字符/词比(`dsh-token-meter` 的 `CHARS_PER_TOKEN`,其
@@ -69,13 +250,15 @@ export function buildToolTableReport(rows, hidden) {
69
250
  list = [];
70
251
  buckets.set(key, list);
71
252
  }
72
- list.push({ name, tok: estimateTok(row.bytes), hidden: off.has(name) });
253
+ list.push({ name, tok: estimateTok(row.bytes), hidden: off.has(name), defaultHidden: DEFAULT_HIDDEN_SET.has(name) });
73
254
  }
74
255
  const groups = [];
75
256
  let visibleTok = 0;
76
257
  let hiddenTok = 0;
77
258
  let visibleCount = 0;
78
259
  let totalCount = 0;
260
+ let defaultHiddenCount = 0;
261
+ let defaultHiddenTok = 0;
79
262
  for (const [key, tools] of buckets) {
80
263
  if (!tools.length)
81
264
  continue;
@@ -84,8 +267,13 @@ export function buildToolTableReport(rows, hidden) {
84
267
  for (const tool of tools) {
85
268
  tok += tool.tok;
86
269
  totalCount += 1;
87
- if (tool.hidden)
270
+ if (tool.hidden) {
88
271
  offTok += tool.tok;
272
+ if (tool.defaultHidden) {
273
+ defaultHiddenCount += 1;
274
+ defaultHiddenTok += tool.tok;
275
+ }
276
+ }
89
277
  else {
90
278
  visibleTok += tool.tok;
91
279
  visibleCount += 1;
@@ -97,5 +285,6 @@ export function buildToolTableReport(rows, hidden) {
97
285
  return {
98
286
  groups, visibleTok, hiddenTok, totalTok: visibleTok + hiddenTok,
99
287
  visibleCount, hiddenCount: totalCount - visibleCount, totalCount,
288
+ defaultHiddenCount, defaultHiddenTok,
100
289
  };
101
290
  }
package/package.json CHANGED
@@ -1,108 +1,108 @@
1
- {
2
- "name": "dsh-plugin-tool-management",
3
- "version": "0.13.0",
4
- "description": "DSH plugin: one settings panel for MCP servers, skills, scenes, memories, subagents, AGENTS.md presets and archived sessions — scene memory injects into the system prompt, scene lock freezes all five domains, deleted workspaces re-register with one click. Settings UI + HTTP API + 20 model tools (mcp_manager_*, skill_manager_*, prompt_manager_*, memory_manager_*, subagent_*). Install with one command: dsh plugin --profile web add dsh-plugin-tool-management@latest",
5
- "repository": {
6
- "type": "git",
7
- "url": "https://github.com/ouli-1242/dsh-plugin-tool-management"
8
- },
9
- "type": "module",
10
- "main": "lib/index.js",
11
- "exports": {
12
- ".": "./lib/index.js",
13
- "./client": "./lib/client.js",
14
- "./workspace": "./lib/sessions/workspace.js",
15
- "./package.json": "./package.json"
16
- },
17
- "files": [
18
- "lib",
19
- "cordis.patch.yml",
20
- "README.md",
21
- "README_EN.md",
22
- "CHANGELOG.md",
23
- "LICENSE",
24
- "screenshots.json",
25
- "docs"
26
- ],
27
- "engines": {
28
- "node": ">=18"
29
- },
30
- "publishConfig": {
31
- "registry": "https://registry.npmjs.org",
32
- "access": "public"
33
- },
34
- "scripts": {
35
- "build": "tsc -p tsconfig.json && node scripts/sync-client.mjs && node scripts/sync-profile.mjs",
36
- "prepublishOnly": "npm run build && npm run lint && npm run typecheck:client && node --test test/client-exports.test.mjs",
37
- "lint": "node --check lib/client.js && node --check lib/index.js",
38
- "typecheck:client": "node scripts/sync-client.mjs && tsc -p tsconfig.client.json",
39
- "check:i18n": "node scripts/check-i18n.mjs lib/client.js",
40
- "check:host": "node scripts/host-deps.mjs && node scripts/doctor.mjs",
41
- "host-deps": "node scripts/host-deps.mjs --fix",
42
- "doctor": "node scripts/doctor.mjs",
43
- "test": "npm run build && npm run check:i18n && npm run typecheck:client && node --test test/contracts.test.mjs test/client-exports.test.mjs",
44
- "sync:profile": "node scripts/sync-profile.mjs"
45
- },
46
- "keywords": [
47
- "dsh",
48
- "deepseek-harness",
49
- "skill",
50
- "skills",
51
- "mcp",
52
- "mcp-server",
53
- "plugin",
54
- "cordis"
55
- ],
56
- "dsh": {
57
- "bundle": {
58
- "patch": "./cordis.patch.yml"
59
- },
60
- "client": {
61
- "platform": "web",
62
- "inject": [
63
- "@deepseek-ai/dsh-client-locale"
64
- ]
65
- }
66
- },
67
- "peerDependencies": {
68
- "@deepseek-ai/cordis": "^4.0.2",
69
- "@deepseek-ai/dsh-llm": ">=0.1.5-rc.2",
70
- "@deepseek-ai/dsh-session": ">=0.1.5-rc.2",
71
- "@deepseek-ai/dsh-session-projection-cache": ">=0.1.5-rc.2",
72
- "@deepseek-ai/dsh-spill-local": ">=0.1.5-rc.2",
73
- "@deepseek-ai/dsh-storage-domain": ">=0.1.5-rc.2",
74
- "@deepseek-ai/dsh-subagent-fork-in-process": ">=0.1.5-rc.2",
75
- "@deepseek-ai/dsh-subagent-spawn-in-process": ">=0.1.5-rc.2",
76
- "@deepseek-ai/dsh-tools": ">=0.1.5-rc.2",
77
- "@deepseek-ai/dsh-workspace": ">=0.1.5-rc.2",
78
- "react": "^18.2.0"
79
- },
80
- "peerDependenciesMeta": {
81
- "@deepseek-ai/dsh-subagent-fork-in-process": {
82
- "optional": true
83
- },
84
- "@deepseek-ai/dsh-subagent-spawn-in-process": {
85
- "optional": true
86
- }
87
- },
88
- "devDependencies": {
89
- "@deepseek-ai/cordis": "^4.0.1",
90
- "@deepseek-ai/dsh-llm": "^0.1.5-rc.2",
91
- "@deepseek-ai/dsh-session": "^0.1.5-rc.2",
92
- "@deepseek-ai/dsh-session-projection-cache": "^0.1.5-rc.2",
93
- "@deepseek-ai/dsh-spill-local": "^0.1.5-rc.2",
94
- "@deepseek-ai/dsh-storage-domain": "^0.1.5-rc.2",
95
- "@deepseek-ai/dsh-subagent-fork-in-process": "^0.1.5-rc.2",
96
- "@deepseek-ai/dsh-subagent-spawn-in-process": "^0.1.5-rc.2",
97
- "@deepseek-ai/dsh-tools": "^0.1.5-rc.2",
98
- "@deepseek-ai/dsh-workspace": "^0.1.5-rc.2",
99
- "@types/js-yaml": "^4.0.9",
100
- "@types/node": "^24.0.0",
101
- "typescript": "^6.0.0"
102
- },
103
- "license": "MIT",
104
- "dependencies": {
105
- "fflate": "^0.8.3",
106
- "js-yaml": "^4.3.2"
107
- }
108
- }
1
+ {
2
+ "name": "dsh-plugin-tool-management",
3
+ "version": "0.14.0",
4
+ "description": "DSH plugin: one settings panel for MCP servers, skills, scenes, memories, subagent personas and AGENTS.md presets — a scene switches the whole environment, and the model can drive it from chat too.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/ouli-1242/dsh-plugin-tool-management"
8
+ },
9
+ "type": "module",
10
+ "main": "lib/index.js",
11
+ "exports": {
12
+ ".": "./lib/index.js",
13
+ "./client": "./lib/client.js",
14
+ "./workspace": "./lib/sessions/workspace.js",
15
+ "./package.json": "./package.json"
16
+ },
17
+ "files": [
18
+ "lib",
19
+ "cordis.patch.yml",
20
+ "README.md",
21
+ "README_EN.md",
22
+ "CHANGELOG.md",
23
+ "LICENSE",
24
+ "screenshots.json",
25
+ "docs"
26
+ ],
27
+ "engines": {
28
+ "node": ">=18"
29
+ },
30
+ "publishConfig": {
31
+ "registry": "https://registry.npmjs.org",
32
+ "access": "public"
33
+ },
34
+ "scripts": {
35
+ "build": "tsc -p tsconfig.json && node scripts/sync-client.mjs && node scripts/sync-profile.mjs",
36
+ "prepublishOnly": "npm run build && npm run lint && npm run typecheck:client && node --test test/client-exports.test.mjs",
37
+ "lint": "node --check lib/client.js && node --check lib/index.js",
38
+ "typecheck:client": "node scripts/sync-client.mjs && tsc -p tsconfig.client.json",
39
+ "check:i18n": "node scripts/check-i18n.mjs lib/client.js",
40
+ "check:host": "node scripts/host-deps.mjs && node scripts/doctor.mjs",
41
+ "host-deps": "node scripts/host-deps.mjs --fix",
42
+ "doctor": "node scripts/doctor.mjs",
43
+ "test": "npm run build && npm run check:i18n && npm run typecheck:client && node --test test/contracts.test.mjs test/client-exports.test.mjs",
44
+ "sync:profile": "node scripts/sync-profile.mjs"
45
+ },
46
+ "keywords": [
47
+ "dsh",
48
+ "deepseek-harness",
49
+ "skill",
50
+ "skills",
51
+ "mcp",
52
+ "mcp-server",
53
+ "plugin",
54
+ "cordis"
55
+ ],
56
+ "dsh": {
57
+ "bundle": {
58
+ "patch": "./cordis.patch.yml"
59
+ },
60
+ "client": {
61
+ "platform": "web",
62
+ "inject": [
63
+ "@deepseek-ai/dsh-client-locale"
64
+ ]
65
+ }
66
+ },
67
+ "peerDependencies": {
68
+ "@deepseek-ai/cordis": "^4.0.2",
69
+ "@deepseek-ai/dsh-llm": ">=0.1.5-rc.2",
70
+ "@deepseek-ai/dsh-session": ">=0.1.5-rc.2",
71
+ "@deepseek-ai/dsh-session-projection-cache": ">=0.1.5-rc.2",
72
+ "@deepseek-ai/dsh-spill-local": ">=0.1.5-rc.2",
73
+ "@deepseek-ai/dsh-storage-domain": ">=0.1.5-rc.2",
74
+ "@deepseek-ai/dsh-subagent-fork-in-process": ">=0.1.5-rc.2",
75
+ "@deepseek-ai/dsh-subagent-spawn-in-process": ">=0.1.5-rc.2",
76
+ "@deepseek-ai/dsh-tools": ">=0.1.5-rc.2",
77
+ "@deepseek-ai/dsh-workspace": ">=0.1.5-rc.2",
78
+ "react": "^18.2.0"
79
+ },
80
+ "peerDependenciesMeta": {
81
+ "@deepseek-ai/dsh-subagent-fork-in-process": {
82
+ "optional": true
83
+ },
84
+ "@deepseek-ai/dsh-subagent-spawn-in-process": {
85
+ "optional": true
86
+ }
87
+ },
88
+ "devDependencies": {
89
+ "@deepseek-ai/cordis": "^4.0.1",
90
+ "@deepseek-ai/dsh-llm": "^0.1.5-rc.2",
91
+ "@deepseek-ai/dsh-session": "^0.1.5-rc.2",
92
+ "@deepseek-ai/dsh-session-projection-cache": "^0.1.5-rc.2",
93
+ "@deepseek-ai/dsh-spill-local": "^0.1.5-rc.2",
94
+ "@deepseek-ai/dsh-storage-domain": "^0.1.5-rc.2",
95
+ "@deepseek-ai/dsh-subagent-fork-in-process": "^0.1.5-rc.2",
96
+ "@deepseek-ai/dsh-subagent-spawn-in-process": "^0.1.5-rc.2",
97
+ "@deepseek-ai/dsh-tools": "^0.1.5-rc.2",
98
+ "@deepseek-ai/dsh-workspace": "^0.1.5-rc.2",
99
+ "@types/js-yaml": "^4.0.9",
100
+ "@types/node": "^24.0.0",
101
+ "typescript": "^6.0.0"
102
+ },
103
+ "license": "MIT",
104
+ "dependencies": {
105
+ "fflate": "^0.8.3",
106
+ "js-yaml": "^4.3.2"
107
+ }
108
+ }