dsh-plugin-tool-management 0.17.0 → 0.18.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 (83) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +18 -3
  3. package/README_EN.md +18 -3
  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 +16 -0
  21. package/lib/client.js +1573 -133
  22. package/lib/compat/capability-specs.js +321 -0
  23. package/lib/compat/host-probe.js +261 -0
  24. package/lib/compat/host-roots.js +168 -0
  25. package/lib/compat/probe-types.js +6 -0
  26. package/lib/compat/probe.js +9 -720
  27. package/lib/context-inject-contract.js +114 -0
  28. package/lib/context-inject-frame.js +147 -0
  29. package/lib/context-inject-select.js +127 -0
  30. package/lib/context-inject-surface.js +97 -0
  31. package/lib/context-inject.js +10 -478
  32. package/lib/http-api.js +229 -0
  33. package/lib/hub.js +2 -0
  34. package/lib/index.js +134 -836
  35. package/lib/mcp/inspect-probe.js +199 -0
  36. package/lib/mcp/manager.js +30 -548
  37. package/lib/mcp/tool-visibility.js +388 -0
  38. package/lib/memories/archive-engine.js +142 -5
  39. package/lib/memories/archive.js +10 -1
  40. package/lib/memories/discovery.js +261 -0
  41. package/lib/memories/export-plan.js +42 -0
  42. package/lib/memories/memories-types.js +1 -0
  43. package/lib/memories/service.js +4 -38
  44. package/lib/memories/snapshot.js +10 -252
  45. package/lib/op-registry.js +21 -0
  46. package/lib/ops/compat.js +11 -0
  47. package/lib/ops/quick-prompts.js +26 -0
  48. package/lib/ops/scene-records.js +40 -0
  49. package/lib/ops/scene-sync.js +71 -1
  50. package/lib/ops/sessions.js +13 -9
  51. package/lib/ops/snapshot.js +18 -12
  52. package/lib/prompts/quick-service.js +211 -0
  53. package/lib/scene-linkage.js +184 -0
  54. package/lib/sessions/archive-schemas.js +121 -0
  55. package/lib/sessions/path-key.js +46 -0
  56. package/lib/sessions/workspace.js +5 -163
  57. package/lib/settings-sidecars.js +143 -0
  58. package/lib/skills/core-types.js +8 -0
  59. package/lib/skills/core-util.js +81 -0
  60. package/lib/skills/core.js +56 -2326
  61. package/lib/skills/dir-watch.js +71 -0
  62. package/lib/skills/frontmatter.js +96 -0
  63. package/lib/skills/homes.js +14 -0
  64. package/lib/skills/import-pipeline.js +231 -0
  65. package/lib/skills/manager-state.js +800 -0
  66. package/lib/skills/official.js +269 -0
  67. package/lib/skills/provider-registration.js +89 -0
  68. package/lib/skills/service.js +5 -240
  69. package/lib/skills/skill-roots.js +287 -0
  70. package/lib/skills/skill-trash.js +265 -0
  71. package/lib/skills/system-trash.js +97 -0
  72. package/lib/skills/upload-pipeline.js +374 -0
  73. package/lib/slash-settings.js +25 -0
  74. package/lib/subagents/persona-parse.js +312 -0
  75. package/lib/subagents/persona-types.js +5 -0
  76. package/lib/subagents/persona-write.js +93 -0
  77. package/lib/subagents/run-provider.js +159 -0
  78. package/lib/subagents/service.js +14 -638
  79. package/lib/subagents/tool-filter.js +98 -0
  80. package/lib/token-config.js +240 -0
  81. package/lib/tool-table-switch.js +163 -0
  82. package/lib/tools/scene.js +40 -9
  83. package/package.json +3 -2
@@ -0,0 +1,321 @@
1
+ // 宿主能力表:每一项写明「插件要用宿主的哪个成员、缺了走哪条降级路」。
2
+ //
3
+ // 这张表就是与宿主的契约本体 —— 纯字面量,判定逻辑在 probe.ts 的 inspectCapability,
4
+ // 取数原语在 host-probe.ts。缺一个成员时界面给出的是这一行点名的降级,而不是删除序列
5
+ // 中途抛出的一次异常。
6
+ import { callableWithSentinel, isFn, PROBE_SENTINEL } from './host-probe.js';
7
+ /**
8
+ * The capability table. This IS the contract with the host: everything the
9
+ * adapters reach for is listed here with what happens when it is absent, so a
10
+ * missing member produces a named, visible degradation instead of a throw from
11
+ * somewhere in the middle of a delete sequence.
12
+ */
13
+ export const CAPABILITY_SPECS = [
14
+ // ---- workspace registry: read side -------------------------------------
15
+ {
16
+ id: 'workspace.read-state',
17
+ label: '读取工作区状态',
18
+ kind: 'read',
19
+ owner: 'workspace',
20
+ fallback: 'refuse-operation',
21
+ methods: ['requireState'],
22
+ probe: (t) => {
23
+ try {
24
+ const state = t.requireState();
25
+ if (state === null || typeof state !== 'object')
26
+ return 'requireState() 未返回对象';
27
+ const value = state;
28
+ if (!Array.isArray(value.archivedSessionIds))
29
+ return 'requireState().archivedSessionIds 不是数组';
30
+ if (!Array.isArray(value.workspaceIds))
31
+ return 'requireState().workspaceIds 不是数组';
32
+ return undefined;
33
+ }
34
+ catch (error) {
35
+ return `requireState() 抛错:${String(error?.message ?? error)}`;
36
+ }
37
+ },
38
+ },
39
+ {
40
+ id: 'workspace.read-table',
41
+ label: '读取工作区表',
42
+ kind: 'read',
43
+ owner: 'workspace',
44
+ fallback: 'refuse-operation',
45
+ methods: ['requireTable'],
46
+ probe: (t) => {
47
+ try {
48
+ const table = t.requireTable();
49
+ if (table === null || typeof table !== 'object' || !isFn(table.get))
50
+ return 'requireTable() 未返回可 get 的表';
51
+ return undefined;
52
+ }
53
+ catch (error) {
54
+ return `requireTable() 抛错:${String(error?.message ?? error)}`;
55
+ }
56
+ },
57
+ },
58
+ {
59
+ id: 'workspace.index-shape',
60
+ label: '工作区索引结构',
61
+ kind: 'read',
62
+ owner: 'workspace',
63
+ fallback: 'refuse-operation',
64
+ fields: [
65
+ { name: 'headers', instanceOf: 'Map' },
66
+ { name: 'sessionPaths', instanceOf: 'Map' },
67
+ { name: 'invalidSessionPaths', instanceOf: 'Map' },
68
+ { name: 'entities', instanceOf: 'Map' },
69
+ ],
70
+ },
71
+ {
72
+ id: 'workspace.read-header',
73
+ label: '读取会话头部',
74
+ kind: 'read',
75
+ owner: 'workspace',
76
+ fallback: 'refuse-operation',
77
+ methods: ['readSessionHeader'],
78
+ },
79
+ // ---- workspace registry: write side ------------------------------------
80
+ {
81
+ id: 'workspace.enqueue',
82
+ label: '串行写事务',
83
+ kind: 'write',
84
+ owner: 'workspace',
85
+ fallback: 'disable-destructive',
86
+ methods: ['enqueueOperation'],
87
+ },
88
+ {
89
+ id: 'workspace.set-state',
90
+ label: '写工作区状态',
91
+ kind: 'write',
92
+ owner: 'workspace',
93
+ fallback: 'disable-destructive',
94
+ methods: ['setState'],
95
+ },
96
+ {
97
+ id: 'workspace.index-header',
98
+ label: '索引会话头部',
99
+ kind: 'write',
100
+ owner: 'workspace',
101
+ fallback: 'disable-destructive',
102
+ methods: ['indexHeader'],
103
+ },
104
+ {
105
+ id: 'workspace.archive-native',
106
+ label: '宿主原生归档入口',
107
+ kind: 'write',
108
+ owner: 'workspace',
109
+ fallback: 'native-entry',
110
+ methods: ['archiveSession'],
111
+ optional: true,
112
+ // 分层探针第二层(哨兵真调):官方实现对未知 id 走读路径校验后受控拒绝
113
+ //(WorkspaceUnknownSessionError);同步 TypeError 才是「不该路由信任」的信号。
114
+ probe: (t) => callableWithSentinel(t, 'archiveSession', PROBE_SENTINEL),
115
+ },
116
+ {
117
+ id: 'workspace.unarchive-native',
118
+ label: '宿主原生恢复入口',
119
+ kind: 'write',
120
+ owner: 'workspace',
121
+ fallback: 'native-entry',
122
+ methods: ['unarchiveSession'],
123
+ optional: true,
124
+ probe: (t) => callableWithSentinel(t, 'unarchiveSession', PROBE_SENTINEL),
125
+ },
126
+ {
127
+ id: 'workspace.batch-native',
128
+ label: '宿主原生批量入口',
129
+ kind: 'write',
130
+ owner: 'workspace',
131
+ fallback: 'native-entry',
132
+ methods: ['archiveWorkspaceSessions'],
133
+ optional: true,
134
+ probe: (t) => callableWithSentinel(t, 'archiveWorkspaceSessions', [PROBE_SENTINEL]),
135
+ },
136
+ {
137
+ id: 'workspace.delete-native',
138
+ label: '宿主原生删除入口',
139
+ kind: 'delete',
140
+ owner: 'workspace',
141
+ // NOT `disable-destructive`: when this slot is empty the plugin performs the
142
+ // whole delete itself, so nothing is disabled. `native-entry` describes what
143
+ // actually happens (the adapter takes over).
144
+ fallback: 'native-entry',
145
+ methods: ['deleteSession'],
146
+ optional: true,
147
+ // 删除路由的 native 判定此前只看方法存在(07 审查五档问题 2):宿主升级把同名
148
+ // 方法换成别的签名,路由仍会走 native 裸调。哨兵真调把口径提到「可调用」,
149
+ // 文本比对收紧(见 inspectCapability 的 delete 类分支)负责「实现漂移」那一层。
150
+ probe: (t) => callableWithSentinel(t, 'deleteSession', PROBE_SENTINEL),
151
+ },
152
+ // ---- sessions runtime (private members, used only on the live branch) ---
153
+ {
154
+ id: 'sessions.detach-live',
155
+ label: '实时会话落盘与分离',
156
+ kind: 'delete',
157
+ owner: 'sessions',
158
+ fallback: 'disable-destructive',
159
+ methods: ['flush', 'liveEntryFor', 'detachEntered'],
160
+ // 零副作用真调(按官方源码核对):liveEntryFor/flush 对哨兵在查表处受控抛错;
161
+ // detachEntered 传普通对象在未知 id 处早退(entry 形状 {id} 即可)。
162
+ probe: (t) => callableWithSentinel(t, 'liveEntryFor', PROBE_SENTINEL)
163
+ ?? callableWithSentinel(t, 'flush', PROBE_SENTINEL)
164
+ ?? callableWithSentinel(t, 'detachEntered', { id: PROBE_SENTINEL }),
165
+ },
166
+ {
167
+ id: 'sessions.cold-announce',
168
+ label: '冷会话移除广播',
169
+ kind: 'delete',
170
+ owner: 'sessions',
171
+ fallback: 'disable-destructive',
172
+ methods: ['enter', 'announce'],
173
+ // announce 哨兵真调:内部先 liveEntryFor 查表,哨兵输入在查表处受控抛错。
174
+ // **enter 不真调**(官方实现对任意输入都会往 store 写入条目),它只有存在性 + 文本比对。
175
+ probe: (t) => callableWithSentinel(t, 'announce', PROBE_SENTINEL),
176
+ },
177
+ // ---- projection cache ---------------------------------------------------
178
+ {
179
+ id: 'projection.write',
180
+ label: '投影缓存写入路径',
181
+ kind: 'write',
182
+ owner: 'projectionCache',
183
+ fallback: 'disable-destructive',
184
+ methods: ['write', 'put', 'requireTable'],
185
+ },
186
+ {
187
+ // B2:此前它只是 bridge 运行时那句拒绝里的**临时 id**(`acquireCacheGuard` 里现场拼的),
188
+ // 于是客户端压根不知道它 —— 宿主表不可删时,"路由说可用、点下去必拒"(V13)。提升为
189
+ // 能力表的正式条目后,路由判定与运行时前提同一份依据。
190
+ id: 'projection.table-delete',
191
+ label: '投影缓存行删除',
192
+ kind: 'delete',
193
+ owner: 'projectionCache',
194
+ fallback: 'disable-destructive',
195
+ // 探测内容就是运行时那句硬前提:`requireTable().delete` 在不在。
196
+ probe: (target) => {
197
+ if (typeof target?.requireTable !== 'function')
198
+ return '宿主投影缓存缺少 requireTable';
199
+ let table;
200
+ try {
201
+ table = target.requireTable();
202
+ }
203
+ catch (error) {
204
+ return `宿主投影缓存 requireTable() 抛错:${String(error)}`;
205
+ }
206
+ if (typeof table?.delete !== 'function')
207
+ return '宿主投影缓存存储不支持安全删除(table.delete 缺失)';
208
+ return undefined;
209
+ },
210
+ },
211
+ {
212
+ id: 'projection.delete-native',
213
+ label: '投影缓存删除屏障',
214
+ kind: 'delete',
215
+ owner: 'projectionCache',
216
+ // Absent on rc.2: `sessions/bridge.ts` installs a checked write barrier
217
+ // instead, so absence is a routing fact, not a failure. Only a cache whose
218
+ // write path cannot be wrapped at all is a real problem.
219
+ fallback: 'native-entry',
220
+ optional: true,
221
+ probe: (t) => {
222
+ if (isFn(t?.delete) && isFn(t?.whenIdle))
223
+ return undefined;
224
+ const wrappable = ['write', 'put'].filter((name) => !isFn(t?.[name]));
225
+ if (wrappable.length > 0)
226
+ return `宿主缓存无法安全包裹(缺少 ${wrappable.join(', ')})`;
227
+ let table;
228
+ try {
229
+ table = t.requireTable();
230
+ }
231
+ catch (error) {
232
+ return `requireTable() 抛错:${String(error?.message ?? error)}`;
233
+ }
234
+ if (!isFn(table?.delete))
235
+ return '宿主缓存存储不支持行删除(table.delete 缺失)';
236
+ return undefined;
237
+ },
238
+ },
239
+ // ---- settings document path(0.1.7 的两处适配面之一)----------------------
240
+ // `prepareDocument()` 在 0.1.7 把返回值从「主目录下的设置文档路径」改成「profile 补丁
241
+ // 路径」(`configEditor.documentPath`)—— 方法一直在、语义变了,方法存在性探测抓不住
242
+ // (0.15.0 的 MCP 页事故就是这么静默发生的)。这一条能拦的是「方法消失 / 签名漂移到
243
+ // 同步抛 TypeError」;**返回值形状**是异步结果,同步探针看不到,那一半由 ensurePaths
244
+ // 的 await 后自检负责(src/index.ts:认 profile 形状上溯 + 主目录不变量检查,异常走
245
+ // noteRuntime)。两半合起来才是这个契约的完整探测。
246
+ {
247
+ id: 'settings.document-path',
248
+ label: '设置文档路径(主目录推导源)',
249
+ kind: 'read',
250
+ owner: 'settings',
251
+ fallback: 'refuse-operation',
252
+ methods: ['prepareDocument'],
253
+ // 零副作用真调:官方实现就是 `Promise.resolve(this.documentPath)`,纯读。拿不到
254
+ // 异步结果没关系 —— 同步 TypeError(签名漂移)才是这一层要拦的。
255
+ probe: (t) => callableWithSentinel(t, 'prepareDocument'),
256
+ // 健康行也常显观测路径(官方 SettingsForms 有同步的 `documentPath` getter,
257
+ // prepareDocument 就是它的 Promise 包装):语义再变,页面上这行字一眼见底。
258
+ describe: (t) => {
259
+ try {
260
+ const p = t.documentPath;
261
+ return typeof p === 'string' && p !== '' ? `观测路径:${p}` : undefined;
262
+ }
263
+ catch {
264
+ return undefined;
265
+ }
266
+ },
267
+ },
268
+ // ---- ctx.fs:MCP 域补丁写路径的唯一出口(审查 §5 F5)----------------------
269
+ // 为什么值得单列:补丁文件在插件自己的数据目录**之外**(`~/.dsh/cordis.patch.yml` 与
270
+ // `profiles/<名>/cordis.patch.yml`),读写都得经 `ctx.fs` 显式升级沙箱策略。五个方法里
271
+ // 任何一个改名,19 处补丁写路径会**一起**报错 —— 报错是可见的(不是静默),但要等到用户
272
+ // 点下去才发现;钉在兼容页上就能在升级后第一眼看见。
273
+ //
274
+ // 能探到 / 探不到(如实标注):成员存在性可探;`writeText` 的**实参个数**可观测(健康行
275
+ // 附注)。「五个位置参数的语义变了」**探不到** —— 真调它会写文件(有副作用),而本探针
276
+ // 的纪律是零副作用(见 callableWithSentinel 的副作用口径)。那半条只能靠宿主升级清单人工核对。
277
+ {
278
+ id: 'fs.patch-io',
279
+ label: '宿主文件读写(补丁文件出口)',
280
+ kind: 'write',
281
+ owner: 'fs',
282
+ fallback: 'refuse-operation',
283
+ methods: ['resolve', 'stat', 'readText', 'writeText', 'listDir'],
284
+ describe: (t) => {
285
+ const w = t.writeText;
286
+ return typeof w === 'function' ? `观测 writeText 实参个数=${w.length}` : undefined;
287
+ },
288
+ },
289
+ // ---- agent preset roster(0.1.7 的两处适配面之二)-------------------------
290
+ // 0.1.7 把 `read(id)`(直返组合文本)改名成 `readDocument(id)`(返回文档对象,组合
291
+ // YAML 在 `.content`)。读取口是「read 优先、readDocument 兜底」双入口
292
+ // (preset-reach.ts 的 readCompositionText),两个名字**任一在场即可** —— 这正是
293
+ // methods 列表表达不了的 either-or,用 probe 写。list / composedPreset 缺一个,
294
+ // 注入边界矩阵与边界提示就瞎一半,同为必需。
295
+ {
296
+ id: 'preset.roster-surface',
297
+ label: '预设名册读取面',
298
+ kind: 'read',
299
+ owner: 'presetRoster',
300
+ fallback: 'inform-only',
301
+ probe: (t) => {
302
+ const o = t;
303
+ const hasRead = isFn(o.read);
304
+ const hasDoc = isFn(o.readDocument);
305
+ if (!hasRead && !hasDoc)
306
+ return 'read 与 readDocument 都缺失:组合文本读不到,注入边界矩阵与极简兜底注入失明';
307
+ const missing = ['list', 'composedPreset'].filter((name) => !isFn(o[name]));
308
+ if (missing.length > 0)
309
+ return `名册缺少 ${missing.join(', ')}:注入边界矩阵不完整`;
310
+ return undefined;
311
+ },
312
+ // 健康行附注实际走的读取路。0.1.7 起 `read` 改名 `readDocument`,兜底成功是**正常形态**
313
+ // 而非降级 —— 这句话只出现在绿色行上(运行时上报会渲染成问题行,那里不放)。
314
+ describe: (t) => {
315
+ const o = t;
316
+ if (!isFn(o.read) && isFn(o.readDocument))
317
+ return '读取走 readDocument(宿主 0.1.7 起的形态)';
318
+ return undefined;
319
+ },
320
+ },
321
+ ];
@@ -0,0 +1,261 @@
1
+ // 宿主能力探测的取数原语:解析包路径、读版本、比对实现文本、取原型、用哨兵实调。
2
+ //
3
+ // 与 probe.ts 的分工:这里只管「怎么探」(怎么把说明符变成一个磁盘路径、怎么读出版本、
4
+ // 怎么真调一个方法而不制造副作用),判定与降级口径全在 probe.ts。搬移段的每一行都
5
+ // 是从 probe.ts 整段剪切而来,未作任何改写;`safeResolve`/`classPrototypeOf` 用的
6
+ // `import.meta.url` 解析基准与本文件原先同在 lib/compat/ 一层,换文件名不改目录,
7
+ // 解析链才没有变(本插件按产物目录深度判形态,见 probe.ts 的 SOURCE_INSTALL)。
8
+ import { createRequire } from 'node:module';
9
+ import { readFileSync, realpathSync } from 'node:fs';
10
+ import { dirname, join } from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
12
+ export const isFn = (value) => typeof value === 'function';
13
+ /**
14
+ * Resolve a package the way this plugin resolves it, without throwing.
15
+ *
16
+ * `import.meta.resolve` 给的是**URL**:目录里带空格的路径(桌面版装在
17
+ * `D:\DeepSeek Harness\...`)会以 `%20` 形式回来,直接当路径用会得到一个
18
+ * 永远打不开的名字 —— 版本号读不出来(显示 `unknown`)、asar 路径也匹配不上。
19
+ * 用 `fileURLToPath` 老实解码(它同时处理盘符与百分号转义)。
20
+ */
21
+ export function safeResolve(specifier) {
22
+ try {
23
+ return fileURLToPath(import.meta.resolve(specifier));
24
+ }
25
+ catch {
26
+ try {
27
+ return createRequire(import.meta.url).resolve(specifier);
28
+ }
29
+ catch {
30
+ return null;
31
+ }
32
+ }
33
+ }
34
+ /**
35
+ * Whether a path lives inside an electron `app.asar` archive.
36
+ *
37
+ * 桌面版把整个 DSH 装进 `resources/app.asar`:宿主的 `@deepseek-ai/*` 物理上就在
38
+ * 归档里,插件(装在 `$DSH_HOME/profiles/<name>/node_modules`)在外面的磁盘上。
39
+ * 归档里的模块**不可能**与外面的目录是同一个物理文件,`@deepseek-ai/*` 又是
40
+ * peer(官方包由宿主提供),所以「不同路径」是这种打包方式的必然结果,不是安装
41
+ * 故障。判据取 `.asar` 这个路径段(`join` 出来的分隔符在 Windows 上是 `\`)。
42
+ */
43
+ export function isAsarPath(value) {
44
+ if (value === null)
45
+ return false;
46
+ return /[\\/][^\\/]+\.asar[\\/]/i.test(value);
47
+ }
48
+ /**
49
+ * Resolve a path through every junction/symlink to its physical file.
50
+ * Node loads modules by real path, so two different-looking paths that
51
+ * realpath to one file ARE the same module — which is exactly the property
52
+ * this plugin depends on, and the reason `@deepseek-ai/*` is junctioned into
53
+ * the host installation instead of being copied.
54
+ *
55
+ * 取不到真实形态时**不再拿输入去比**(issue #1 要求):那会把「路径不可解析」
56
+ * 伪装成「两份不同拷贝」,给出一个编出来的结论。改由调用方把原因报成说明。
57
+ */
58
+ export function realPathWithReason(value) {
59
+ if (value === null)
60
+ return { reason: '路径为空' };
61
+ try {
62
+ return { path: realpathSync.native !== undefined ? realpathSync.native(value) : realpathSync(value) };
63
+ }
64
+ catch (error) {
65
+ const detail = error;
66
+ const code = typeof detail.code === 'string' ? detail.code : '';
67
+ const message = typeof detail.message === 'string' ? detail.message : String(error);
68
+ return { reason: code === '' ? message : `${code} ${message}` };
69
+ }
70
+ }
71
+ /** Read a package version from a resolvable manifest. */
72
+ export function versionOf(specifier) {
73
+ const entry = safeResolve(specifier);
74
+ if (entry === null)
75
+ return undefined;
76
+ let dir = dirname(entry);
77
+ for (let i = 0; i < 4; i += 1) {
78
+ try {
79
+ const parsed = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'));
80
+ if (parsed.name === specifier && typeof parsed.version === 'string')
81
+ return parsed.version;
82
+ }
83
+ catch {
84
+ /* keep walking up */
85
+ }
86
+ const parent = dirname(dir);
87
+ if (parent === dir)
88
+ break;
89
+ dir = parent;
90
+ }
91
+ return undefined;
92
+ }
93
+ /**
94
+ * Version of the package a resolved entry path belongs to: walk up until a
95
+ * `package.json` whose `name` matches the package directory we came from.
96
+ *
97
+ * 与 `versionOf` 的分工:那个从**说明符**出发(要能解析),这个从**已经拿到的路径**
98
+ * 出发。归档宿主那种「路径不同但版本相同」的判断只能用后者 —— 前者在 `.asar` 里
99
+ * 读得动,但两边路径不同时它无法回答「这两个文件各属于哪个版本」。
100
+ */
101
+ export function versionOfPackageAt(entry) {
102
+ const expected = packageNameOfPath(entry);
103
+ let dir = dirname(entry);
104
+ for (let i = 0; i < 5; i += 1) {
105
+ try {
106
+ const parsed = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'));
107
+ if (typeof parsed.version === 'string' && (expected === null || parsed.name === expected))
108
+ return parsed.version;
109
+ }
110
+ catch { /* keep walking up */ }
111
+ const parent = dirname(dir);
112
+ if (parent === dir)
113
+ break;
114
+ dir = parent;
115
+ }
116
+ return null;
117
+ }
118
+ /** `.../node_modules/@scope/name/lib/index.js` -> `@scope/name` (null when the path has no such segment). */
119
+ function packageNameOfPath(entry) {
120
+ const parts = entry.split(/[\\/]/);
121
+ const at = parts.lastIndexOf('node_modules');
122
+ if (at === -1 || at + 1 >= parts.length)
123
+ return null;
124
+ const first = parts[at + 1];
125
+ if (first === undefined)
126
+ return null;
127
+ if (first.startsWith('@')) {
128
+ const second = parts[at + 2];
129
+ return second === undefined ? null : `${first}/${second}`;
130
+ }
131
+ return first;
132
+ }
133
+ /**
134
+ * Compare a live member with this plugin's copy of the implementation.
135
+ * `undefined` means "not comparable" — the member is absent on the live object
136
+ * or the reference copy does not have it either.
137
+ */
138
+ export function textMatchOf(live, reference) {
139
+ if (!isFn(live) || !isFn(reference))
140
+ return undefined;
141
+ if (live === reference)
142
+ return true;
143
+ try {
144
+ return Function.prototype.toString.call(live) === Function.prototype.toString.call(reference);
145
+ }
146
+ catch {
147
+ return undefined;
148
+ }
149
+ }
150
+ /**
151
+ * The prototype that defines a live instance's members, following the prototype
152
+ * chain so proxies, subclass instances and cross-realm objects all answer
153
+ * honestly.
154
+ */
155
+ function prototypeOf(instance) {
156
+ if (instance === null || typeof instance !== 'object')
157
+ return undefined;
158
+ if (typeof instance === 'function')
159
+ return instance;
160
+ const proto = Object.getPrototypeOf(instance);
161
+ return proto !== null && typeof proto === 'object' ? proto : undefined;
162
+ }
163
+ // The official package + class each capability owner's members live on. This is
164
+ // the single answer to "which class defines this owner" — both the live
165
+ // reference-prototype lookup below and the exported static table read it, so a
166
+ // host drift is described in exactly one place.
167
+ const WORKSPACE_CLASS = { pkg: '@deepseek-ai/dsh-workspace', target: 'WorkspaceRegistry' };
168
+ const PROJECTION_CLASS = { pkg: '@deepseek-ai/dsh-session-projection-cache', target: 'SessionProjectionCache' };
169
+ // `sessions.*` members are host-private members, but they are still declared on
170
+ // an official class: `@deepseek-ai/dsh-session` is a peer this plugin declares,
171
+ // and `SessionStore` is where `flush`/`liveEntryFor`/`detachEntered`/`enter`/
172
+ // `announce` live. Without this entry the doctor could not see the two
173
+ // capabilities the delete route depends on.
174
+ const SESSIONS_CLASS = { pkg: '@deepseek-ai/dsh-session', target: 'SessionStore' };
175
+ // 0.15.0 新增的两块契约面 —— 恰是 0.1.7 升级咬人的两处(prepareDocument 返回语义变了、
176
+ // read 改名 readDocument),此前不在表里,故障只能靠用户实测发现。类名按官方安装源码核对
177
+ // (dsh-settings 导出 SettingsForms;dsh-agent-preset-registry 导出 AgentPresetRegistry)。
178
+ const SETTINGS_CLASS = { pkg: '@deepseek-ai/dsh-settings', target: 'SettingsForms' };
179
+ const ROSTER_CLASS = { pkg: '@deepseek-ai/dsh-agent-preset-registry', target: 'AgentPresetRegistry' };
180
+ export const OWNER_CLASSES = {
181
+ workspace: WORKSPACE_CLASS,
182
+ projectionCache: PROJECTION_CLASS,
183
+ sessions: SESSIONS_CLASS,
184
+ settings: SETTINGS_CLASS,
185
+ presetRoster: ROSTER_CLASS,
186
+ };
187
+ /** The named export's prototype, or undefined when the package is absent/trimmed. */
188
+ function classPrototypeOf(ref) {
189
+ try {
190
+ const mod = createRequire(import.meta.url)(ref.pkg);
191
+ const klass = mod[ref.target];
192
+ return klass?.prototype;
193
+ }
194
+ catch {
195
+ return undefined; // optional in tests and in trimmed deployments
196
+ }
197
+ }
198
+ /**
199
+ * 哨兵值:不可能命中任何真实会话/工作区(会话 id 由宿主生成,不含此串)。
200
+ * 行为探针真调宿主方法时一律传它 —— 最坏情形是宿主受控拒绝(not found),
201
+ * 而「哨兵输入都能产生意外效果」本身就是要探出来的「不该路由信任」。
202
+ */
203
+ export const PROBE_SENTINEL = '__dshm_probe_never_exists__';
204
+ /**
205
+ * 用哨兵实参**真调**一个宿主方法(2026-09-19 分层探针第二层),验证「存在且可调用」:
206
+ * - 同步抛 `TypeError` → 实现坏了/签名漂移,返回失败详情(shape-mismatch);
207
+ * - 受控同步拒绝(普通 `Error`,如 not found)或返回值(含 rejected Promise)→ 通过。
208
+ *
209
+ * 只判同步段:探针框架是同步的(`assessHost` 不能 await),异步结果分类留待
210
+ * 异步化改造(见审查 07 跟进项)。返回的 Promise 一律挂 `.catch` 吞掉,探针
211
+ * 不制造 unhandled rejection。调用必须以 `target` 为 receiver —— 私有方法全靠
212
+ * `this.*` 拿内部状态,脱离 receiver 调用会把好实现误判成 TypeError。
213
+ *
214
+ * 副作用口径(按官方源码逐个核过):liveEntryFor/announce/flush 哨兵输入在
215
+ * 查表处受控抛错,零副作用;detachEntered 传普通对象在未知 id 处早退;
216
+ * deleteSession/archiveSession 等原生入口对未知 id 是读路径校验后受控拒绝。
217
+ * `enter` 不在此列(会往 store 里写哨兵条目),它只做存在性 + 文本比对。
218
+ */
219
+ export function callableWithSentinel(target, name, ...args) {
220
+ try {
221
+ const method = target[name];
222
+ if (!isFn(method))
223
+ return `${name} 不是函数(methods 检查应已拦下,此处兜底)`;
224
+ const returned = method.call(target, ...args);
225
+ if (returned !== null && typeof returned === 'object' && isFn(returned.then)) {
226
+ const settled = returned;
227
+ if (isFn(settled.catch))
228
+ settled.catch(() => { });
229
+ }
230
+ return undefined;
231
+ }
232
+ catch (error) {
233
+ if (error instanceof TypeError)
234
+ return `${name} 哨兵真调抛 TypeError:${String(error?.message ?? error)}`;
235
+ return undefined;
236
+ }
237
+ }
238
+ /** Official prototypes this plugin's adapters mirror, when importable. */
239
+ export function referencePrototypes() {
240
+ const out = {};
241
+ const workspace = classPrototypeOf(WORKSPACE_CLASS);
242
+ if (workspace !== undefined)
243
+ out.workspace = workspace;
244
+ const cache = classPrototypeOf(PROJECTION_CLASS);
245
+ if (cache !== undefined)
246
+ out.cache = cache;
247
+ // sessions 的五个私有方法全声明在官方 SessionStore.prototype 上(本插件 peer 依赖
248
+ // 同包同版本),与 workspace/cache 同机制接入文本比对 —— 此前该域只有存在性检查。
249
+ const sessions = classPrototypeOf(SESSIONS_CLASS);
250
+ if (sessions !== undefined)
251
+ out.sessions = sessions;
252
+ // 两块新契约面的参考副本:包不在 devDeps/宿主锚点里时优雅缺省(textMatch 记 undefined),
253
+ // 存在性探测不依赖它们。
254
+ const settings = classPrototypeOf(SETTINGS_CLASS);
255
+ if (settings !== undefined)
256
+ out.settings = settings;
257
+ const roster = classPrototypeOf(ROSTER_CLASS);
258
+ if (roster !== undefined)
259
+ out.roster = roster;
260
+ return out;
261
+ }