dsh-plugin-tool-management 0.5.1 → 0.6.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 (43) hide show
  1. package/README.md +300 -238
  2. package/README_EN.md +300 -304
  3. package/cordis.patch.yml +9 -55
  4. package/docs/Changelog.md +108 -481
  5. package/docs/images/1/345/234/272/346/231/257.png +0 -0
  6. package/docs/images/1/345/234/272/346/231/257_en.png +0 -0
  7. package/docs/images/2MCP.png +0 -0
  8. package/docs/images/2MCP_en.png +0 -0
  9. package/docs/images/3/346/212/200/350/203/275.png +0 -0
  10. package/docs/images/3/346/212/200/350/203/275_en.png +0 -0
  11. package/docs/images/4/345/255/220/346/231/272/350/203/275/344/275/223.png +0 -0
  12. package/docs/images/4/345/255/220/346/231/272/350/203/275/344/275/223_en.png +0 -0
  13. package/docs/images/5/346/217/220/347/244/272/350/257/215.png +0 -0
  14. package/docs/images/5/346/217/220/347/244/272/350/257/215_en.png +0 -0
  15. package/docs/images/6/350/256/260/345/277/206.png +0 -0
  16. package/docs/images/6/350/256/260/345/277/206_en.png +0 -0
  17. package/docs/images/7/344/274/232/350/257/235.png +0 -0
  18. package/docs/images/7/344/274/232/350/257/235_en.png +0 -0
  19. package/docs/images/8/345/205/274/345/256/271.png +0 -0
  20. package/docs/images/8/345/205/274/345/256/271_en.png +0 -0
  21. package/docs/update.md +54 -0
  22. package/lib/agents-md/preset-id.js +49 -0
  23. package/lib/agents-md/service.js +138 -56
  24. package/lib/client.js +4142 -3560
  25. package/lib/compat/probe.js +665 -0
  26. package/lib/history/bridge.js +293 -0
  27. package/lib/history/workspace.js +498 -52
  28. package/lib/http-fence.js +73 -0
  29. package/lib/index.js +511 -126
  30. package/lib/rules/provider.js +3 -3
  31. package/lib/rules/service.js +264 -30
  32. package/lib/scene-prompt-sync.js +112 -0
  33. package/lib/skills/core.js +112 -35
  34. package/lib/skills/service.js +6 -1
  35. package/package.json +5 -3
  36. package/screenshots.json +8 -7
  37. package/docs/images/MCP.png +0 -0
  38. package/docs/images//344/274/232/350/257/235.png +0 -0
  39. package/docs/images//345/234/272/346/231/257.png +0 -0
  40. package/docs/images//345/255/220/346/231/272/350/203/275/344/275/223.png +0 -0
  41. package/docs/images//346/212/200/350/203/275.png +0 -0
  42. package/docs/images//346/217/220/347/244/272/350/257/215.png +0 -0
  43. package/docs/images//350/256/260/345/277/206.png +0 -0
@@ -0,0 +1,293 @@
1
+ import { symbols } from "@deepseek-ai/cordis";
2
+ import { workspaceDomainSpec } from "@deepseek-ai/dsh-workspace";
3
+ import { assessHost, routeFor, refusalsFor, CapabilityRefusalError } from "../compat/probe.js";
4
+ export { CapabilityRefusalError };
5
+ // No Service subclass, no ctx.provide(), no second storage-domain owner.
6
+ // The official API lacks unarchive and a cache deletion barrier, so this file
7
+ // adapts the gaps. WHICH parts of the host it may touch is decided by
8
+ // ../compat/probe.ts from live, observable facts (module identity, member
9
+ // presence, shapes, read-only behaviour), never from comparing source text:
10
+ // text equality only ever proved "same release", and it turned every harmless
11
+ // upstream refactor into a total feature outage.
12
+ const cacheGuards = new WeakMap();
13
+ const raw = (value) => value?.[symbols.original] ?? value;
14
+ const sameLifecycle = (a, b) => a && b && a.createdAt === b.createdAt && (a.cwd ?? null) === (b.cwd ?? null);
15
+ /**
16
+ * Install the write barrier around the host's projection cache when (and only
17
+ * when) the host cannot safely delete a cache row on its own.
18
+ *
19
+ * Guarded on capability, not on implementation text: the cache is wrapped only
20
+ * when the write path and a deletable table are both present, and a host that
21
+ * ships its own `delete`/`whenIdle` barrier never gets wrapped at all.
22
+ *
23
+ * @param cache - host projection cache instance.
24
+ * @returns the barrier handle.
25
+ * @throws {CapabilityRefusalError} when the cache cannot support a safe delete.
26
+ */
27
+ function acquireCacheGuard(cache) {
28
+ let guard = cacheGuards.get(cache);
29
+ if (guard) {
30
+ guard.users += 1;
31
+ return guard;
32
+ }
33
+ const missing = ["put", "write", "requireTable"].filter((name) => typeof cache[name] !== "function");
34
+ if (missing.length > 0) {
35
+ throw new CapabilityRefusalError("delete", refusalsFor({ findings: [{ id: "projection.write", label: "投影缓存写入路径", kind: "write", owner: "projectionCache", fallback: "disable-destructive", state: "missing-member", detail: `宿主投影缓存缺少 ${missing.join(", ")}`, missing }] }, ["projection.write"]));
36
+ }
37
+ const table = cache.requireTable();
38
+ if (typeof table?.delete !== "function") {
39
+ throw new CapabilityRefusalError("delete", [{
40
+ id: "projection.table-delete",
41
+ label: "投影缓存行删除",
42
+ detail: "宿主投影缓存存储不支持安全删除(table.delete 缺失)",
43
+ recovery: "请更新本插件到与本机 DSH 匹配的版本。",
44
+ }]);
45
+ }
46
+ const descriptors = Object.fromEntries(["put", "write"].map((name) => [name, Object.getOwnPropertyDescriptor(cache, name)]));
47
+ const originals = { put: cache.put, write: cache.write };
48
+ const blocked = new Map();
49
+ const pending = new Set();
50
+ const queues = new Map();
51
+ const isBlocked = (id, identity) => blocked.has(id) && (blocked.get(id) === null || sameLifecycle(blocked.get(id), identity));
52
+ function track(task) {
53
+ pending.add(task);
54
+ task.then(() => pending.delete(task), () => pending.delete(task));
55
+ return task;
56
+ }
57
+ function serial(id, fn) {
58
+ const task = (queues.get(id) ?? Promise.resolve()).then(fn);
59
+ const settled = task.then(() => { }, () => { });
60
+ queues.set(id, settled);
61
+ settled.then(() => { if (queues.get(id) === settled)
62
+ queues.delete(id); });
63
+ return track(task);
64
+ }
65
+ const wrappers = {
66
+ put(id, identity, rows) {
67
+ // Preserve the upstream snapshot-at-call boundary even while queued.
68
+ if (isBlocked(id, identity))
69
+ return Promise.resolve();
70
+ const expected = { ...identity };
71
+ let detached;
72
+ try {
73
+ detached = structuredClone(rows);
74
+ }
75
+ catch (error) {
76
+ return Promise.reject(error);
77
+ }
78
+ // Serialize puts with deletion; never erase a new lifecycle reusing id.
79
+ return serial(id, async () => {
80
+ if (isBlocked(id, expected))
81
+ return;
82
+ await originals.put.call(cache, id, expected, detached);
83
+ if (isBlocked(id, expected)) {
84
+ const stored = table.get(id);
85
+ if (stored && sameLifecycle(stored.identity, expected))
86
+ await table.delete(id);
87
+ }
88
+ });
89
+ },
90
+ write(session) {
91
+ // Track the flush await before put as well as put itself. Otherwise a
92
+ // delayed write could outlive disposal and bypass the restored method.
93
+ try {
94
+ return track(Promise.resolve(originals.write.call(cache, session)));
95
+ }
96
+ catch (error) {
97
+ return Promise.reject(error);
98
+ }
99
+ },
100
+ };
101
+ cache.put = wrappers.put;
102
+ cache.write = wrappers.write;
103
+ guard = {
104
+ users: 1,
105
+ begin(id, header) { blocked.set(id, header ?? null); },
106
+ clearTombstone(id) { blocked.delete(id); },
107
+ async whenIdle() { while (pending.size)
108
+ await Promise.allSettled([...pending]); },
109
+ async delete(id) {
110
+ await this.whenIdle();
111
+ return serial(id, async () => {
112
+ const stored = table.get(id);
113
+ const identity = blocked.get(id);
114
+ if (!stored || identity == null || sameLifecycle(stored.identity, identity))
115
+ await table.delete(id);
116
+ });
117
+ },
118
+ async release() {
119
+ if (--this.users > 0)
120
+ return;
121
+ await this.whenIdle();
122
+ if (this.users > 0)
123
+ return;
124
+ let restored = true;
125
+ for (const name of ["put", "write"]) {
126
+ // Never overwrite a later third-party wrapper.
127
+ if (cache[name] !== wrappers[name]) {
128
+ restored = false;
129
+ continue;
130
+ }
131
+ if (descriptors[name])
132
+ Object.defineProperty(cache, name, descriptors[name]);
133
+ else
134
+ delete cache[name];
135
+ }
136
+ if (restored) {
137
+ blocked.clear();
138
+ cacheGuards.delete(cache);
139
+ }
140
+ },
141
+ };
142
+ cacheGuards.set(cache, guard);
143
+ return guard;
144
+ }
145
+ /**
146
+ * Live-session members the delete sequence needs, split by branch.
147
+ *
148
+ * `liveEntryFor`/`detachEntered` are private in the official declarations
149
+ * (dsh-session lib/types/index.d.ts), so they are consumed through the same
150
+ * capability gate as everything else. The point of checking BEFORE any
151
+ * destructive step is unchanged: otherwise the failure lands mid-sequence
152
+ * (after flush, before ledger cleanup) and leaves "transcript gone, archive
153
+ * flag still set" behind.
154
+ *
155
+ * @param sessions - ctx.sessions; the cold branch tolerates it being absent.
156
+ * @param hasLiveSession - whether this session is currently live.
157
+ * @returns the capability ids that must be `ok`, in evaluation order.
158
+ */
159
+ export function requiredSessionCapabilities(hasLiveSession) {
160
+ return hasLiveSession
161
+ ? ["sessions.detach-live"]
162
+ : ["sessions.cold-announce"];
163
+ }
164
+ export function createHistoryBridge(ctx, suppliedRegistry, onArchive) {
165
+ // Cordis returns a fresh traceable method proxy on access. Compare and adapt
166
+ // the original object, not proxies; calls retain the service owner's context.
167
+ const registry = raw(suppliedRegistry);
168
+ const cache = raw(ctx.get("sessionProjectionCache"));
169
+ let guard;
170
+ let stopObserving;
171
+ let assessment;
172
+ let assessmentAt = 0;
173
+ /** Re-probe at most every 5s: probing is cheap but not free on hot paths. */
174
+ const ASSESS_TTL_MS = 5000;
175
+ /**
176
+ * The current host assessment. Refreshed lazily so a host that gains or
177
+ * loses a service is noticed without a plugin reload.
178
+ * @param force - re-probe even when a cached value is still fresh.
179
+ */
180
+ function hostAssessment(force = false) {
181
+ const now = Date.now();
182
+ if (force || assessment === undefined || now - assessmentAt > ASSESS_TTL_MS) {
183
+ try {
184
+ assessment = assessHost(ctx);
185
+ }
186
+ catch (error) {
187
+ // A probe that throws is itself a finding: report it rather than
188
+ // letting it escape into a delete sequence.
189
+ ctx.logger?.warn?.(`host capability probe failed: ${String(error)}`);
190
+ assessment = undefined;
191
+ }
192
+ assessmentAt = now;
193
+ }
194
+ return assessment;
195
+ }
196
+ /** Capability ids not `ok` from the given list. */
197
+ function unavailable(ids) {
198
+ const current = hostAssessment();
199
+ if (current === undefined)
200
+ return ids.map((id) => ({ id, label: id, detail: "宿主能力探测不可用", recovery: "请查看插件日志 dsh-plugin-tool-management.log" }));
201
+ return refusalsFor(current, ids);
202
+ }
203
+ /** Throw when any listed capability is not usable; nothing is written before this runs. */
204
+ function requireCapabilities(operation, ids) {
205
+ const refusals = unavailable(ids);
206
+ if (refusals.length > 0)
207
+ throw new CapabilityRefusalError(operation, refusals);
208
+ }
209
+ const method = (name, ...args) => {
210
+ if (typeof registry[name] !== "function")
211
+ throw new Error(`宿主缺少归档兼容接口 ${name},操作已停止`);
212
+ return registry[name](...args);
213
+ };
214
+ /**
215
+ * The adapter route needs the registry's read/write/delegate members. This is
216
+ * a presence-and-shape check on the live object — the previous implementation
217
+ * compared these methods' source text against this plugin's own copy of the
218
+ * official prototype, which failed on any upstream refactor and, worse,
219
+ * depended on the plugin loading a second copy of the package.
220
+ */
221
+ function checkWorkspace(operation = "archive") {
222
+ const current = hostAssessment();
223
+ if (current !== undefined) {
224
+ const decision = routeFor(current, operation);
225
+ if (decision.via === "none")
226
+ throw new CapabilityRefusalError(operation, decision.refusals);
227
+ return decision;
228
+ }
229
+ requireCapabilities(operation, ["workspace.read-state", "workspace.enqueue"]);
230
+ return { via: "adapter", refusals: [] };
231
+ }
232
+ /** Structural check of the workspace index shape. */
233
+ function checkIndexShape() {
234
+ for (const name of ["headers", "sessionPaths", "invalidSessionPaths", "entities"]) {
235
+ if (!(registry[name] instanceof Map))
236
+ throw new Error(`工作区索引 ${name} 不兼容,未修改数据`);
237
+ }
238
+ }
239
+ return {
240
+ registry,
241
+ /** Latest assessment, for the compatibility surface and the HTTP status op. */
242
+ capabilities(force = false) { return hostAssessment(force); },
243
+ /** Capability ids an operation still needs, without throwing. */
244
+ refusalsFor(operation, ids) { return unavailable(ids); }, observe() {
245
+ if (stopObserving)
246
+ return;
247
+ let previous = new Set(registry.archivedSessionIds);
248
+ // Official durable domain notifications also cover archive operations
249
+ // from the official sidebar or an external archive manager. No monkey
250
+ // patch of archiveSession and no competing writer of its ledger.
251
+ stopObserving = ctx.on("domain/changed", (change) => {
252
+ if (change?.domain !== workspaceDomainSpec.name || change.table !== "" || !Array.isArray(change.value?.archivedSessionIds))
253
+ return;
254
+ const next = new Set(change.value.archivedSessionIds);
255
+ const at = Date.now();
256
+ for (const id of next)
257
+ if (!previous.has(id))
258
+ void onArchive(id, at).catch((error) => ctx.logger.warn(String(error)));
259
+ for (const id of previous)
260
+ if (!next.has(id))
261
+ void onArchive(id, null).catch((error) => ctx.logger.warn(String(error)));
262
+ previous = next;
263
+ });
264
+ },
265
+ checkWorkspace,
266
+ checkIndexShape,
267
+ enqueue(fn) { checkWorkspace("archive"); checkIndexShape(); return registry.enqueueOperation(fn); },
268
+ state: () => method("requireState"),
269
+ table: () => method("requireTable"),
270
+ setState: (state) => method("setState", state),
271
+ readHeader: (id) => method("readSessionHeader", id),
272
+ /**
273
+ * Install the cache delete barrier for one session, before anything is
274
+ * flushed or detached. Prefers a host-native barrier when it exists.
275
+ */
276
+ async beginDelete(id, header) {
277
+ checkWorkspace("delete");
278
+ if (!cache)
279
+ throw new CapabilityRefusalError("delete", [{
280
+ id: "projection.write",
281
+ label: "投影缓存写入路径",
282
+ detail: "宿主未提供 sessionProjectionCache",
283
+ recovery: "请确认 DSH 版本并更新本插件到匹配版本。",
284
+ }]);
285
+ if (typeof cache.delete === "function" && typeof cache.whenIdle === "function")
286
+ return;
287
+ guard ??= acquireCacheGuard(cache);
288
+ guard.begin(id, header);
289
+ },
290
+ cache() { return guard ?? cache; },
291
+ async dispose() { stopObserving?.(); await guard?.release(); },
292
+ };
293
+ }