dsh-plugin-tool-management 0.1.3 → 0.2.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/lib/index.js CHANGED
@@ -14,6 +14,7 @@ import { defineTool } from '@deepseek-ai/dsh-tools';
14
14
  import { createRequire } from 'node:module';
15
15
  import { createSkillsService } from './skills/service.js';
16
16
  import { createAgentsMdService } from './agents-md/service.js';
17
+ import { createRulesService } from './rules/service.js';
17
18
  import { detectFormat, extractText, parseGenericText, parseJsonlTranscript, parseMarkdownTranscript } from './imports/parsers.js';
18
19
  import { homedir } from 'node:os';
19
20
  import { spawn } from 'node:child_process';
@@ -96,6 +97,11 @@ export default {
96
97
  'history-archive', 'history-unarchive', 'history-delete', 'history-retention-set',
97
98
  'history-unarchive-batch', 'history-delete-batch', 'history-import', 'history-export',
98
99
  'history-archive-batch',
100
+ // rules 写操作(v0.3:create/update/remove/restore 改记忆文件;toggle/set-index/
101
+ // set-active 改侧车索引;create-scene/remove-scene 建删场景目录。
102
+ // set-active = 插件内"启用场景"开关,全局持久化)
103
+ 'rules-create', 'rules-update', 'rules-remove', 'rules-restore', 'rules-toggle', 'rules-set-index', 'rules-set-active',
104
+ 'rules-create-scene', 'rules-remove-scene',
99
105
  ]);
100
106
  const wait = (ms) => ctx.timeout(ms);
101
107
  const message = (e) => String((e && e.message) || e);
@@ -139,6 +145,25 @@ export default {
139
145
  return p.home + sep + 'AGENTS.md';
140
146
  },
141
147
  });
148
+ // ---------- rules(规则/记忆,v0.3)----------
149
+ // 规则真源 $DSH_HOME/rules/<场景>/<name>.md(仅用户级,D1)。**场景 = 一级目录**;
150
+ // 单投影 = 活动场景记忆 → per-agent systemPrompt 段(自动在场,模型无需调用任何工具)。
151
+ // 原"始终层写 ~/.dsh/AGENTS.md"已下线(变更单 01 §4/§10):公共基线由 _shared/ 承担。
152
+ // rulesRoot / rulesStateDir 仅测试注入,生产留空由服务按 DSH_HOME 解析。
153
+ const rulesService = createRulesService(ctx, {
154
+ rulesRoot: String(config?.rulesRoot || ''),
155
+ stateDir: String(config?.rulesStateDir || ''),
156
+ // 场景记忆段预算(字节),默认 65536;仅用于测试与特殊部署调优。
157
+ ...(Number.isFinite(Number(config?.rulesMaxBytes))
158
+ ? { maxBytes: Number(config.rulesMaxBytes) }
159
+ : {}),
160
+ });
161
+ try {
162
+ ctx.effect(() => rulesService.registerProviders(), 'dsh-plugin-tool-management: rules providers');
163
+ }
164
+ catch (e) {
165
+ console.error('[dsh-plugin-tool-management] rules provider setup failed:', message(e));
166
+ }
142
167
  // ---------- history(归档会话管理,折叠自 dsh-archive-manager)----------
143
168
  // cordis.patch.yml 禁用官方 workspace 与 session-projection-cache,插入本插件
144
169
  // 的归档感知子类(lib/history/workspace.js + lib/history/projcache.js)。子类
@@ -899,7 +924,7 @@ export default {
899
924
  return { ok: true, notes: map };
900
925
  });
901
926
  }
902
- const SETTINGS_DEFAULTS = { pollIntervalMs: 5000, toolDescriptionMaxLength: 0 };
927
+ const SETTINGS_DEFAULTS = { pollIntervalMs: 5000, toolDescriptionMaxLength: 0, requireConfirmForModelRuleWrite: true };
903
928
  let pluginSettingsCache = null;
904
929
  function clampInt(value, min, max, fallback) {
905
930
  // Number(null) is 0 — treat missing/empty input as "use the default".
@@ -921,6 +946,10 @@ export default {
921
946
  pollIntervalMs: clampInt(raw && raw.pollIntervalMs, 2000, 60000, SETTINGS_DEFAULTS.pollIntervalMs),
922
947
  // 0 = keep descriptions in full (the UI default); > 0 truncates.
923
948
  toolDescriptionMaxLength: clampInt(raw && raw.toolDescriptionMaxLength, 0, 2000, SETTINGS_DEFAULTS.toolDescriptionMaxLength),
949
+ // 模型写规则需确认(D2):程序化强制,只读设置供 tools/pre-execute 判定。
950
+ requireConfirmForModelRuleWrite: (raw && typeof raw.requireConfirmForModelRuleWrite === 'boolean')
951
+ ? raw.requireConfirmForModelRuleWrite
952
+ : SETTINGS_DEFAULTS.requireConfirmForModelRuleWrite,
924
953
  },
925
954
  };
926
955
  return pluginSettingsCache.value;
@@ -932,6 +961,9 @@ export default {
932
961
  const next = {
933
962
  pollIntervalMs: clampInt(args.pollIntervalMs, 2000, 60000, current.pollIntervalMs),
934
963
  toolDescriptionMaxLength: clampInt(args.toolDescriptionMaxLength, 0, 2000, current.toolDescriptionMaxLength),
964
+ requireConfirmForModelRuleWrite: (args && typeof args.requireConfirmForModelRuleWrite === 'boolean')
965
+ ? args.requireConfirmForModelRuleWrite
966
+ : current.requireConfirmForModelRuleWrite,
935
967
  };
936
968
  const p = await ensurePaths();
937
969
  return withWriteLock(async () => {
@@ -1782,6 +1814,12 @@ export default {
1782
1814
  // skill-delete / skill-trash-restore / skill-trash-delete
1783
1815
  // 成功返回 {ok:true, data};core 业务失败原样透传 {ok:false, error, code?, params?}。
1784
1816
  ...skillsService.ops,
1817
+ // rules ops(由 ./rules/service.js 提供):rules-list / rules-read / rules-budget /
1818
+ // rules-diagnose / rules-create / rules-update / rules-remove / rules-restore /
1819
+ // rules-trash-list / rules-trash-remove / rules-toggle / rules-set-index /
1820
+ // rules-set-active / rules-create-scene / rules-remove-scene。
1821
+ // 成功返回扁平 {ok:true, ...}(不套 data),失败 {ok:false, error, code?}。
1822
+ ...rulesService.ops,
1785
1823
  // AGENTS.md 预设库 ops(由 ./agents-md/service.js 提供):agentsmd-list /
1786
1824
  // agentsmd-read / agentsmd-create / agentsmd-update / agentsmd-remove /
1787
1825
  // agentsmd-apply / agentsmd-get-current
@@ -2349,12 +2387,73 @@ export default {
2349
2387
  return 'OK: preset ' + args.id + ' applied to ~/.dsh/AGENTS.md (next session; current session unchanged' + (r.backedUp ? '; previous backed up to __last-applied__' : '') + ')';
2350
2388
  },
2351
2389
  }));
2390
+ // ---------- rules model tools(v0.3)----------
2391
+ // 活动场景的记忆正文会自动进入系统提示词(无需调用工具读取);这里的工具用于
2392
+ // 查询/编辑规则本身。rule_manager_write 受 tools/pre-execute 审批门禁(D2)。
2393
+ tools.register(defineTool({
2394
+ name: 'rule_manager_list',
2395
+ description: 'List rules/memories under ~/.dsh/scene-memory (id, scene, enabled, description).',
2396
+ parameters: {
2397
+ group: { type: 'string', description: 'Optional group/scene filter.' },
2398
+ },
2399
+ output: { schema: { type: 'string' }, render: (_a, v) => text(v) },
2400
+ async execute(args) {
2401
+ const r = await rulesService.ops['rules-list'](args);
2402
+ if (!r || r.ok === false)
2403
+ throw new Error((r && r.error) || '读取规则失败');
2404
+ const lines = (r.rules || []).map((x) => ('- ' + x.id + ' [' + (x.group || '全局') + '] ' + (x.enabled ? '已启用' : '已停用') +
2405
+ (x.description ? ' — ' + x.description : '')));
2406
+ const scenes = (r.scenes || []).map((s) => s.name + (s.active ? '(启用)' : '(未启用)')).join('、');
2407
+ return '规则(' + (r.rules || []).length + '):\n' + (lines.join('\n') || '(无规则)') +
2408
+ '\n场景:' + (scenes || '(无)') + (r.activeMode === 'all' ? '(默认全部启用)' : '(已收窄)');
2409
+ },
2410
+ }));
2411
+ tools.register(defineTool({
2412
+ name: 'rule_manager_read',
2413
+ description: 'Read the full body of one rule/memory under ~/.dsh/scene-memory.',
2414
+ parameters: {
2415
+ id: { type: 'string', required: true, description: 'Rule id like <scene>/<name>.' },
2416
+ },
2417
+ output: { schema: { type: 'string' }, render: (_a, v) => text(v) },
2418
+ async execute(args) {
2419
+ const r = await rulesService.ops['rules-read'](args);
2420
+ if (!r || r.ok === false)
2421
+ throw new Error((r && r.error) || '读取规则失败');
2422
+ return '# ' + r.rule.id + '\n\n' + (r.rule.body || '');
2423
+ },
2424
+ }));
2425
+ tools.register(defineTool({
2426
+ name: 'rule_manager_write',
2427
+ description: 'Create a new rule/memory as ~/.dsh/scene-memory/<scene>/<name>.md. It becomes active automatically once its scene is enabled. Requires user confirmation (configurable).',
2428
+ parameters: {
2429
+ group: { type: 'string', required: true, description: 'Scene/folder name under ~/.dsh/scene-memory (any Unicode except path separators and < > : " | ? *).' },
2430
+ name: { type: 'string', required: true, description: 'Memory name = the .md file name without the extension; any Unicode is fine (Chinese included), <=64 chars, no path separators or < > : " | ? *, must not start with a dot.' },
2431
+ description: { type: 'string', required: true, description: 'One-sentence description (<=500 chars).' },
2432
+ body: { type: 'string', required: true, description: 'Markdown body (<=256 KiB).' },
2433
+ },
2434
+ output: { schema: { type: 'string' }, render: (_a, v) => text(v) },
2435
+ async execute(args) {
2436
+ const r = await rulesService.ops['rules-create'](args);
2437
+ if (!r || r.ok === false)
2438
+ throw new Error((r && r.error) || '创建规则失败');
2439
+ return 'OK: rule ' + r.rule.id + '(场景「' + (r.rule.group || '全局') + '」启用后自动生效)';
2440
+ },
2441
+ }));
2352
2442
  if (typeof ctx.on === 'function') {
2353
2443
  ;
2354
2444
  ctx.on('tools/pre-execute', (exec, next) => {
2355
2445
  if (exec && exec.name === 'skill_manager_create') {
2356
2446
  return Promise.resolve({ kind: 'ask', reason: 'Create a new skill under DSH_HOME/skills' });
2357
2447
  }
2448
+ if (exec && exec.name === 'rule_manager_write') {
2449
+ // D2:模型写规则默认需确认;设置关闭后直接放行。ask 无应答者时降级为拒绝(fail-closed),
2450
+ // 不在此处做任何兜底放行。
2451
+ return readPluginSettings()
2452
+ .then((s) => (s.requireConfirmForModelRuleWrite
2453
+ ? { kind: 'ask', reason: 'Write a rule under ~/.dsh/scene-memory' }
2454
+ : next()))
2455
+ .catch(() => ({ kind: 'ask', reason: 'Write a rule under ~/.dsh/scene-memory' }));
2456
+ }
2358
2457
  return next();
2359
2458
  });
2360
2459
  }
@@ -2573,6 +2672,23 @@ export default {
2573
2672
  return { kind: 'success', text };
2574
2673
  },
2575
2674
  }), 'dsh-plugin-tool-management: /agents-md command');
2675
+ ctx.effect(() => commands.register({
2676
+ name: 'scene-memory',
2677
+ description: '列出场景记忆及其场景启用状态',
2678
+ handler: async () => {
2679
+ const r = await rulesService.ops['rules-list']({});
2680
+ if (!r || r.ok === false)
2681
+ return { kind: 'error', text: (r && r.error) || '读取记忆失败' };
2682
+ const rules = r.rules || [];
2683
+ if (!rules.length)
2684
+ return { kind: 'success', text: '未发现任何记忆(~/.dsh/scene-memory 为空)。' };
2685
+ const scenes = r.scenes || [];
2686
+ const text = '场景记忆(' + rules.length + '):\n' + rules.map((rule) => ('- ' + rule.name + ' [' + (rule.group || '全局') + '] · ' + (rule.enabled ? '已启用' : '已停用'))).join('\n') + '\n场景:' + (scenes.map((s) => s.name + (s.active ? '(启用)' : '(未启用)')).join('、') || '(无)') +
2687
+ (r.activeMode === 'all' ? '(默认全部启用)' : '(已收窄)') +
2688
+ '\n(启用场景的记忆正文自动进入系统提示词,无需任何工具调用)';
2689
+ return { kind: 'success', text };
2690
+ },
2691
+ }), 'dsh-plugin-tool-management: /scene-memory command');
2576
2692
  }
2577
2693
  }
2578
2694
  catch (e) {
@@ -0,0 +1,121 @@
1
+ // dsh-plugin-tool-management —— 规则「自动在场层」投影(systemPrompt 段注册)。
2
+ //
3
+ // 变更单 01 的核心:把规则从"投影为 skill 目录条目(模型按需 load)"改为
4
+ // **投影为 system prompt 段(自动在场)**。前者即使命中,模型也要自己决定看不看,
5
+ // 正是用户抱怨的"每次都要解释"。
6
+ //
7
+ // 官方接口语义(@deepseek-ai/dsh-system-prompt):
8
+ // - `section({ name, order, text })` 在**调用者的 scope** 注册;scoped 段遮蔽同名全局段。
9
+ // - `text` 可以是**每次装配时求值的 provider 函数** → 切换启用场景后**下一个请求即生效**,
10
+ // 无需重开会话、无需重载插件。
11
+ // - `renderPrompt` 会**删除空段** → 无启用场景/空目录时返回 `''`,不产生空标题。
12
+ //
13
+ // 为什么注册两处(全局 + per-agent):
14
+ // 全局注册覆盖所有 agent(含子 agent);per-agent 注册保证即使插件 ctx 自带 scope
15
+ // (全局段对某些 agent 不可见)也能命中 —— 两者同名,scoped 遮蔽全局,不会重复注入。
16
+ //
17
+ // 缓存契约(§5.2):段文本是**会话级恒定**的(只由启用场景与文件内容决定),
18
+ // 因此前缀逐字节稳定 → 前缀缓存可命中;禁止在段里放时间戳/计数/相对时间。
19
+ const SECTION_NAME = 'tool-management:scene-memory';
20
+ /**
21
+ * §5.1 硬约束 2:官方 `SECTION_ORDERS` 里 2901–4999 是空档(TOOL_REPORT 2900 →
22
+ * TOOLS_SDK 5000),取 **3000** —— 语义上是"工具能力说明之后、输出与 SDK 约定之前",
23
+ * 且远离任何既有段。**不要**用官方已有数值(同号按名称排序,虽确定但难读,日后易撞)。
24
+ */
25
+ const SECTION_ORDER = 3000;
26
+ const GLOBAL_KEY = '@global';
27
+ /** 取 scope 上的 systemPrompt 服务(Cordis 的 ctx.get 可能抛错,全部吞掉)。 */
28
+ function systemPromptOf(scope) {
29
+ if (!scope)
30
+ return undefined;
31
+ try {
32
+ if (typeof scope.get === 'function')
33
+ return scope.get('systemPrompt');
34
+ return scope.systemPrompt;
35
+ }
36
+ catch {
37
+ return undefined;
38
+ }
39
+ }
40
+ /** 注册全局 + agent-scope 的场景记忆段。 */
41
+ export function createRuleProviderRegistrar(ctx, facade) {
42
+ const registrations = new Map();
43
+ const install = (scope, key) => {
44
+ if (!scope || registrations.has(key))
45
+ return;
46
+ const systemPrompt = systemPromptOf(scope);
47
+ if (!systemPrompt || typeof systemPrompt.section !== 'function')
48
+ return;
49
+ try {
50
+ const dispose = systemPrompt.section({
51
+ name: SECTION_NAME,
52
+ order: SECTION_ORDER,
53
+ // 每次装配重读活动场景 → 切换启用状态后下一个请求即生效。
54
+ // ⚠️ 硬约束 1:必须**同步返回 string**(不能是 Promise)——异步读取会破坏 renderPrompt。
55
+ text: () => facade.renderActiveScenes(),
56
+ });
57
+ registrations.set(key, { scope, dispose: typeof dispose === 'function' ? dispose : () => { } });
58
+ }
59
+ catch (e) {
60
+ // 同层重名注册会抛错:降级为"该 scope 不注入",不影响其余 scope。
61
+ console.error('[dsh-plugin-tool-management] scene-memory section registration failed:', String((e && e.message) || e));
62
+ }
63
+ };
64
+ const uninstall = (key) => {
65
+ const entry = registrations.get(key);
66
+ if (!entry)
67
+ return;
68
+ registrations.delete(key);
69
+ try {
70
+ entry.dispose();
71
+ }
72
+ catch { /* ignore */ }
73
+ };
74
+ // ① 全局:插件 ctx 的 scope(覆盖所有 agent)
75
+ install(ctx, GLOBAL_KEY);
76
+ // ② per-agent:与既有 install/uninstall 生命周期一致(agent/created、agent/disposed)
77
+ const on = ctx && ctx.on;
78
+ let stopCreated;
79
+ let stopDisposed;
80
+ if (typeof on === 'function') {
81
+ stopCreated = on('agent/created', (payload) => {
82
+ const agent = payload && payload.agent;
83
+ if (agent && agent.id != null)
84
+ install(agent.ctx, String(agent.id));
85
+ });
86
+ stopDisposed = on('agent/disposed', (payload) => {
87
+ const agent = payload && payload.agent;
88
+ if (agent && agent.id != null)
89
+ uninstall(String(agent.id));
90
+ });
91
+ }
92
+ // 已存在的 agent(插件热加载场景):补注册。
93
+ try {
94
+ const agents = typeof ctx.get === 'function' ? ctx.get('agents') : undefined;
95
+ if (agents && typeof agents.list === 'function') {
96
+ for (const agent of agents.list()) {
97
+ if (agent && agent.id != null)
98
+ install(agent.ctx, String(agent.id));
99
+ }
100
+ }
101
+ }
102
+ catch { /* agents 服务不可用 → 仅靠事件 */ }
103
+ return {
104
+ invalidate: () => {
105
+ for (const entry of registrations.values()) {
106
+ try {
107
+ entry.scope?.emit?.('system-prompt/change');
108
+ }
109
+ catch { /* 宿主无该事件 → 忽略 */ }
110
+ }
111
+ },
112
+ dispose: () => {
113
+ if (typeof stopCreated === 'function')
114
+ stopCreated();
115
+ if (typeof stopDisposed === 'function')
116
+ stopDisposed();
117
+ for (const key of [...registrations.keys()])
118
+ uninstall(key);
119
+ },
120
+ };
121
+ }