dsh-session-toolbox 0.0.0-stage → 0.1.1

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/host.js ADDED
@@ -0,0 +1,327 @@
1
+ import {
2
+ JsonDocument,
3
+ MoveEngine,
4
+ TrashEngine,
5
+ apiHandler,
6
+ dispatch,
7
+ guardJournal,
8
+ guardLedger,
9
+ guardManifest,
10
+ guardSettings
11
+ } from "./chunk-4UQB3MPY.js";
12
+
13
+ // src/index.mjs
14
+ import { mkdir, realpath, stat } from "node:fs/promises";
15
+ import { join, resolve } from "node:path";
16
+ import z from "@deepseek-ai/schemastery";
17
+ import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
18
+
19
+ // package.json
20
+ var package_default = {
21
+ name: "dsh-session-toolbox",
22
+ version: "0.1.1",
23
+ repository: {
24
+ type: "git",
25
+ url: "https://github.com/AndroidSix/dsh-session-toolbox.git"
26
+ },
27
+ description: "Draft vault, session-to-project move and a trash bin for DeepSeek Harness",
28
+ type: "module",
29
+ main: "lib/index.js",
30
+ bin: {
31
+ "dsh-session-toolbox": "lib/cli.js"
32
+ },
33
+ exports: {
34
+ ".": "./lib/index.js",
35
+ "./host": "./lib/host.js",
36
+ "./client": "./lib/client.js",
37
+ "./cordis.patch.yml": "./cordis.patch.yml",
38
+ "./package.json": "./package.json"
39
+ },
40
+ engines: {
41
+ node: "^22.19.0 || >=24.0.0"
42
+ },
43
+ files: [
44
+ "lib",
45
+ "cordis.patch.yml",
46
+ "README.md",
47
+ "LICENSE",
48
+ "THIRD_PARTY_NOTICES.md",
49
+ "session-toolbox.example.json",
50
+ "CHANGELOG.md"
51
+ ],
52
+ dsh: {
53
+ bundle: {
54
+ patch: "./cordis.patch.yml"
55
+ },
56
+ client: {
57
+ inject: [
58
+ "@deepseek-ai/dsh-client-ui-renderer"
59
+ ],
60
+ platform: "web"
61
+ }
62
+ },
63
+ scripts: {
64
+ build: "node scripts/build.mjs",
65
+ test: "node --test tests/*.test.mjs",
66
+ check: "npm run build && npm test && node scripts/check-package.mjs",
67
+ "check:tarball": "npm run build && node scripts/check-package.mjs --tarball",
68
+ "pack:release": "npm run build && npm pack --ignore-scripts"
69
+ },
70
+ peerDependencies: {
71
+ "@deepseek-ai/cordis": "^4.0.4",
72
+ "@deepseek-ai/dsh-atomic-write": "0.2.0-rc.2",
73
+ "@deepseek-ai/dsh-home-paths": "0.2.0-rc.2",
74
+ "@deepseek-ai/schemastery": "^3.18.4",
75
+ react: "^18.2.0"
76
+ },
77
+ devDependencies: {
78
+ "@deepseek-ai/cordis": "^4.0.4",
79
+ "@deepseek-ai/dsh-atomic-write": "0.2.0-rc.2",
80
+ "@deepseek-ai/dsh-home-paths": "0.2.0-rc.2",
81
+ "@deepseek-ai/schemastery": "^3.18.4",
82
+ esbuild: "^0.25.0",
83
+ react: "^18.2.0",
84
+ "react-dom": "^18.2.0"
85
+ },
86
+ license: "MIT",
87
+ keywords: [
88
+ "dsh-plugin",
89
+ "deepseek-harness",
90
+ "draft-vault",
91
+ "session-move",
92
+ "trash-bin"
93
+ ]
94
+ };
95
+
96
+ // src/index.mjs
97
+ var pluginVersion = package_default.version;
98
+ var name = "dsh-session-toolbox";
99
+ var inject = [];
100
+ var Config = z.object({
101
+ configFile: z.string().description("Directory for session-toolbox data (defaults to <DSH_HOME>/session-toolbox)").default(""),
102
+ dshHome: z.string().description("Override the DSH home whose sessions/ storage the tool manages (advanced)").default("")
103
+ });
104
+ function defaultKitRoot() {
105
+ return join(resolveDshHome(), "session-toolbox");
106
+ }
107
+ async function apply(ctx, options = {}) {
108
+ const dshHome = resolve(options.dshHome || resolveDshHome());
109
+ const root = resolve(options.configFile || join(dshHome, "session-toolbox"));
110
+ await mkdir(root, { recursive: true, mode: 448 });
111
+ const capabilities = {
112
+ workspaceRegistry: ctx.get("workspaceRegistry") !== void 0,
113
+ sessions: ctx.get("sessions") !== void 0,
114
+ sessionPersistence: ctx.get("sessionPersistence") !== void 0,
115
+ webServer: false
116
+ // flips true when the optional webServer inject fires
117
+ };
118
+ const settingsDoc = new JsonDocument(root, "session-toolbox.json", { version: 1 }, guardSettings);
119
+ const journalDoc = new JsonDocument(root, "journal.json", { version: 1, active: null }, guardJournal);
120
+ const ledgerDoc = new JsonDocument(root, "trash.json", { version: 1, items: [] }, guardLedger);
121
+ const manifestDoc = new JsonDocument(root, "vault-manifest.json", { version: 1, records: [] }, guardManifest);
122
+ let settingsCache = await settingsDoc.read().catch(() => ({ trash: {}, draftVault: {} }));
123
+ let journalCache = null;
124
+ try {
125
+ journalCache = (await journalDoc.read()).active;
126
+ } catch (error) {
127
+ ctx.logger?.warn?.(`session-toolbox: journal unreadable \u2014 ${error.message}`);
128
+ }
129
+ if (journalCache !== null) ctx.logger?.warn?.(`session-toolbox: unresolved move journal for ${journalCache.sessionId}; complete or roll it back from the panel or CLI`);
130
+ const move = new MoveEngine({
131
+ root,
132
+ sessionsRoot: join(dshHome, "sessions"),
133
+ projcacheRoot: join(dshHome, "storages", "session_projcache", "sessions"),
134
+ journal: journalDoc,
135
+ capabilities: hostCapabilities(ctx)
136
+ });
137
+ const trash = new TrashEngine({
138
+ adapter: registryAdapter(ctx),
139
+ sessions: () => (ctx.get("sessions")?.list() ?? []).map((session) => session.header?.id).filter((id) => typeof id === "string"),
140
+ sessionsRoot: join(dshHome, "sessions"),
141
+ projcacheRoot: join(dshHome, "storages", "session_projcache", "sessions"),
142
+ kitRoot: root,
143
+ ledger: ledgerDoc,
144
+ vault: { dropSession: (sessionId) => manifestDoc.update((current) => ({ ...current, records: current.records.filter((record) => record.sessionId !== sessionId) })) },
145
+ journal: () => journalCache,
146
+ settings: () => settingsCache.trash ?? {}
147
+ });
148
+ const kit = {
149
+ settings: settingsDoc,
150
+ manifest: manifestDoc,
151
+ journal: journalDoc,
152
+ move,
153
+ trash,
154
+ updateSettings: async (patch) => {
155
+ const next = await settingsDoc.update((current) => deepMerge(current, patch));
156
+ settingsCache = next;
157
+ return next;
158
+ },
159
+ vaultSync: async (records) => {
160
+ const manifest = await manifestDoc.update(() => ({
161
+ version: 1,
162
+ records: (Array.isArray(records) ? records : []).filter((record) => typeof record.sessionId === "string" && typeof record.attachmentId === "string").slice(-2e3).map((record) => ({
163
+ sessionId: record.sessionId,
164
+ attachmentId: record.attachmentId,
165
+ name: String(record.name ?? ""),
166
+ size: Number(record.size) || 0,
167
+ hash: String(record.hash ?? ""),
168
+ createdAt: Number(record.createdAt) || Date.now()
169
+ }))
170
+ }));
171
+ return manifest;
172
+ },
173
+ state: async () => {
174
+ const settings = await settingsDoc.read();
175
+ settingsCache = settings;
176
+ const journal = (await journalDoc.read().catch(() => ({ active: journalCache }))).active;
177
+ journalCache = journal;
178
+ const manifest = await manifestDoc.read();
179
+ return {
180
+ pluginVersion,
181
+ capabilities,
182
+ settings,
183
+ journal,
184
+ trash: await trash.items(),
185
+ // Bytes live in browser IndexedDB; the manifest mirrors their metadata only.
186
+ vault: { records: manifest.records.length, bytes: manifest.records.reduce((total, record) => total + (record.size ?? 0), 0) },
187
+ recovery: journal === null ? null : `\u672A\u5B8C\u6210\u8FC1\u79FB\uFF1A${journal.sessionId} \u2192 ${journal.toCwd}\uFF08\u5DF2\u5B8C\u6210 ${journal.done.join("/") || "\u65E0"}\uFF09`
188
+ };
189
+ },
190
+ action: (input) => dispatch(kit, input)
191
+ };
192
+ ctx.inject?.(["webServer"], (webCtx) => {
193
+ capabilities.webServer = true;
194
+ webCtx.effect(() => webCtx.webServer.register({ kind: "exact", path: "/.well-known/dsh-session-toolbox", handler: apiHandler(kit) }));
195
+ });
196
+ ctx.provide("sessionToolbox", kit);
197
+ ctx.effect(() => () => {
198
+ });
199
+ ctx.logger?.info?.("dsh-session-toolbox ready", JSON.stringify({ version: pluginVersion, capabilities }));
200
+ }
201
+ function hostCapabilities(ctx) {
202
+ return {
203
+ sessionLive: (sessionId) => {
204
+ const sessions = ctx.get("sessions");
205
+ return sessions ? sessions.list().some((session) => session.header?.id === sessionId) : false;
206
+ },
207
+ listHeaders: async () => {
208
+ const persistence = ctx.get("sessionPersistence");
209
+ if (persistence?.list !== void 0) return (await persistence.list()).map((snapshot) => snapshot.header);
210
+ const registry = ctx.get("workspaceRegistry");
211
+ if (registry?.listStoredHeaders !== void 0) return await registry.listStoredHeaders();
212
+ throw new Error("no session-persistence service in this DSH build");
213
+ },
214
+ /** Drop stale header entries so `attachSession` validates the NEW on-disk cwd (§10 MR4). */
215
+ refreshHeaderCache: async () => {
216
+ const registry = ctx.get("workspaceRegistry");
217
+ if (typeof registry?.replaceHeaderIndex !== "function") return;
218
+ await registry.replaceHeaderIndex(await registry.listStoredHeaders());
219
+ },
220
+ /** Ledger step (§10 MR2 step 2): source detach → target create → attach. */
221
+ moveLedger: async (sessionId, fromCwd, toCwd) => {
222
+ const registry = ctx.get("workspaceRegistry");
223
+ if (registry === void 0) throw new Error("workspaceRegistry unavailable in this DSH build");
224
+ const canonicalFrom = fromCwd === void 0 ? void 0 : await realpath(fromCwd).catch(() => fromCwd);
225
+ const source = canonicalFrom === void 0 ? void 0 : await registry.resolveByPath(canonicalFrom);
226
+ if (source !== void 0) await source.detachSession(sessionId);
227
+ let target = await registry.resolveByPath(toCwd);
228
+ if (target === void 0) {
229
+ if (!await stat(toCwd).then((s) => s.isDirectory(), () => false)) throw new Error(`target directory ${toCwd} does not exist`);
230
+ target = await registry.create(toCwd);
231
+ }
232
+ await target.attachSession(sessionId);
233
+ },
234
+ resyncIndex: async () => {
235
+ return ctx.get("sessionQuery") !== void 0 ? "lazy" : "unavailable";
236
+ }
237
+ };
238
+ }
239
+ function registryAdapter(ctx) {
240
+ const registry = () => {
241
+ const value = ctx.get("workspaceRegistry");
242
+ if (value === void 0) throw Object.assign(new Error("workspaceRegistry unavailable"), { code: "no-capability" });
243
+ return value;
244
+ };
245
+ return {
246
+ sessionKnown: async (sessionId) => {
247
+ const rows = registry();
248
+ return typeof rows.sessionKnown === "function" ? await rows.sessionKnown(sessionId) : false;
249
+ },
250
+ archivedSet: async () => new Set(registry().archivedSessionIds ?? []),
251
+ pinnedSet: async () => new Set(registry().pinnedSessionIds ?? []),
252
+ locateSession: async (sessionId) => {
253
+ for (const entity of registry().list()) {
254
+ const ids = entity.sessionIds ?? [];
255
+ if (!ids.includes(sessionId)) continue;
256
+ const at = ids.indexOf(sessionId);
257
+ return { workspaceId: entity.id, beforeId: at + 1 < ids.length ? ids[at + 1] : null, title: null };
258
+ }
259
+ return { workspaceId: null, beforeId: null, title: null };
260
+ },
261
+ workspaceSnapshot: async (workspaceId) => {
262
+ const rows = registry();
263
+ const entity = rows.get(workspaceId);
264
+ if (entity === void 0) return void 0;
265
+ const ids = entity.sessionIds ?? [];
266
+ const archived = new Set(rows.archivedSessionIds ?? []);
267
+ const pinned = new Set(rows.pinnedSessionIds ?? []);
268
+ const order = rows.list().map((row) => row.id);
269
+ const at = order.indexOf(entity.id);
270
+ return {
271
+ workspaceId: entity.id,
272
+ path: entity.path,
273
+ title: entity.title,
274
+ beforeId: at > 0 ? order[at - 1] : null,
275
+ sessions: ids.map((sessionId, index) => ({
276
+ sessionId,
277
+ previouslyArchived: archived.has(sessionId),
278
+ previouslyPinned: pinned.has(sessionId),
279
+ beforeId: index + 1 < ids.length ? ids[index + 1] : null
280
+ }))
281
+ };
282
+ },
283
+ archiveSession: (sessionId, options) => registry().archiveSession(sessionId, options),
284
+ unarchiveSession: (sessionId) => registry().unarchiveSession(sessionId),
285
+ pinSession: (sessionId) => registry().pinSession(sessionId),
286
+ unpinSession: (sessionId) => registry().unpinSession(sessionId),
287
+ deleteWorkspace: (workspaceId) => registry().delete(workspaceId),
288
+ repairSlot: async (sessionId, workspaceId, beforeId) => {
289
+ const entity = registry().get(workspaceId);
290
+ if (entity === void 0) return;
291
+ if (!entity.sessionIds?.includes(sessionId)) await entity.attachSession(sessionId);
292
+ if (beforeId !== null && entity.sessionIds?.includes(beforeId)) await entity.insertSessionBefore(sessionId, beforeId);
293
+ },
294
+ restoreWorkspace: async (snapshot) => {
295
+ const rows = registry();
296
+ let entity = rows.list().find((row) => row.path === snapshot.path);
297
+ if (entity === void 0) {
298
+ await mkdir(snapshot.path, { recursive: true }).catch(() => {
299
+ });
300
+ entity = await rows.create(snapshot.path, snapshot.title);
301
+ }
302
+ for (const row of [...snapshot.sessions].reverse()) await entity.attachSession(row.sessionId).catch(() => {
303
+ });
304
+ if (snapshot.beforeId !== null && entity.id !== snapshot.beforeId) await rows.insertBefore(entity.id, snapshot.beforeId).catch(() => {
305
+ });
306
+ return entity;
307
+ },
308
+ purgeSession: async () => ctx.get("sessionPersistence")?.delete === void 0 ? "unavailable" : "present"
309
+ };
310
+ }
311
+ function deepMerge(base, patch) {
312
+ const next = { ...base };
313
+ for (const [key, value] of Object.entries(patch ?? {})) {
314
+ next[key] = value !== null && typeof value === "object" && !Array.isArray(value) ? deepMerge(base[key] ?? {}, value) : value;
315
+ }
316
+ return next;
317
+ }
318
+ export {
319
+ Config,
320
+ apply,
321
+ defaultKitRoot,
322
+ hostCapabilities,
323
+ inject,
324
+ name,
325
+ registryAdapter
326
+ };
327
+ //# sourceMappingURL=index.js.map
package/lib/index.js ADDED
@@ -0,0 +1,327 @@
1
+ import {
2
+ JsonDocument,
3
+ MoveEngine,
4
+ TrashEngine,
5
+ apiHandler,
6
+ dispatch,
7
+ guardJournal,
8
+ guardLedger,
9
+ guardManifest,
10
+ guardSettings
11
+ } from "./chunk-4UQB3MPY.js";
12
+
13
+ // src/index.mjs
14
+ import { mkdir, realpath, stat } from "node:fs/promises";
15
+ import { join, resolve } from "node:path";
16
+ import z from "@deepseek-ai/schemastery";
17
+ import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
18
+
19
+ // package.json
20
+ var package_default = {
21
+ name: "dsh-session-toolbox",
22
+ version: "0.1.1",
23
+ repository: {
24
+ type: "git",
25
+ url: "https://github.com/AndroidSix/dsh-session-toolbox.git"
26
+ },
27
+ description: "Draft vault, session-to-project move and a trash bin for DeepSeek Harness",
28
+ type: "module",
29
+ main: "lib/index.js",
30
+ bin: {
31
+ "dsh-session-toolbox": "lib/cli.js"
32
+ },
33
+ exports: {
34
+ ".": "./lib/index.js",
35
+ "./host": "./lib/host.js",
36
+ "./client": "./lib/client.js",
37
+ "./cordis.patch.yml": "./cordis.patch.yml",
38
+ "./package.json": "./package.json"
39
+ },
40
+ engines: {
41
+ node: "^22.19.0 || >=24.0.0"
42
+ },
43
+ files: [
44
+ "lib",
45
+ "cordis.patch.yml",
46
+ "README.md",
47
+ "LICENSE",
48
+ "THIRD_PARTY_NOTICES.md",
49
+ "session-toolbox.example.json",
50
+ "CHANGELOG.md"
51
+ ],
52
+ dsh: {
53
+ bundle: {
54
+ patch: "./cordis.patch.yml"
55
+ },
56
+ client: {
57
+ inject: [
58
+ "@deepseek-ai/dsh-client-ui-renderer"
59
+ ],
60
+ platform: "web"
61
+ }
62
+ },
63
+ scripts: {
64
+ build: "node scripts/build.mjs",
65
+ test: "node --test tests/*.test.mjs",
66
+ check: "npm run build && npm test && node scripts/check-package.mjs",
67
+ "check:tarball": "npm run build && node scripts/check-package.mjs --tarball",
68
+ "pack:release": "npm run build && npm pack --ignore-scripts"
69
+ },
70
+ peerDependencies: {
71
+ "@deepseek-ai/cordis": "^4.0.4",
72
+ "@deepseek-ai/dsh-atomic-write": "0.2.0-rc.2",
73
+ "@deepseek-ai/dsh-home-paths": "0.2.0-rc.2",
74
+ "@deepseek-ai/schemastery": "^3.18.4",
75
+ react: "^18.2.0"
76
+ },
77
+ devDependencies: {
78
+ "@deepseek-ai/cordis": "^4.0.4",
79
+ "@deepseek-ai/dsh-atomic-write": "0.2.0-rc.2",
80
+ "@deepseek-ai/dsh-home-paths": "0.2.0-rc.2",
81
+ "@deepseek-ai/schemastery": "^3.18.4",
82
+ esbuild: "^0.25.0",
83
+ react: "^18.2.0",
84
+ "react-dom": "^18.2.0"
85
+ },
86
+ license: "MIT",
87
+ keywords: [
88
+ "dsh-plugin",
89
+ "deepseek-harness",
90
+ "draft-vault",
91
+ "session-move",
92
+ "trash-bin"
93
+ ]
94
+ };
95
+
96
+ // src/index.mjs
97
+ var pluginVersion = package_default.version;
98
+ var name = "dsh-session-toolbox";
99
+ var inject = [];
100
+ var Config = z.object({
101
+ configFile: z.string().description("Directory for session-toolbox data (defaults to <DSH_HOME>/session-toolbox)").default(""),
102
+ dshHome: z.string().description("Override the DSH home whose sessions/ storage the tool manages (advanced)").default("")
103
+ });
104
+ function defaultKitRoot() {
105
+ return join(resolveDshHome(), "session-toolbox");
106
+ }
107
+ async function apply(ctx, options = {}) {
108
+ const dshHome = resolve(options.dshHome || resolveDshHome());
109
+ const root = resolve(options.configFile || join(dshHome, "session-toolbox"));
110
+ await mkdir(root, { recursive: true, mode: 448 });
111
+ const capabilities = {
112
+ workspaceRegistry: ctx.get("workspaceRegistry") !== void 0,
113
+ sessions: ctx.get("sessions") !== void 0,
114
+ sessionPersistence: ctx.get("sessionPersistence") !== void 0,
115
+ webServer: false
116
+ // flips true when the optional webServer inject fires
117
+ };
118
+ const settingsDoc = new JsonDocument(root, "session-toolbox.json", { version: 1 }, guardSettings);
119
+ const journalDoc = new JsonDocument(root, "journal.json", { version: 1, active: null }, guardJournal);
120
+ const ledgerDoc = new JsonDocument(root, "trash.json", { version: 1, items: [] }, guardLedger);
121
+ const manifestDoc = new JsonDocument(root, "vault-manifest.json", { version: 1, records: [] }, guardManifest);
122
+ let settingsCache = await settingsDoc.read().catch(() => ({ trash: {}, draftVault: {} }));
123
+ let journalCache = null;
124
+ try {
125
+ journalCache = (await journalDoc.read()).active;
126
+ } catch (error) {
127
+ ctx.logger?.warn?.(`session-toolbox: journal unreadable \u2014 ${error.message}`);
128
+ }
129
+ if (journalCache !== null) ctx.logger?.warn?.(`session-toolbox: unresolved move journal for ${journalCache.sessionId}; complete or roll it back from the panel or CLI`);
130
+ const move = new MoveEngine({
131
+ root,
132
+ sessionsRoot: join(dshHome, "sessions"),
133
+ projcacheRoot: join(dshHome, "storages", "session_projcache", "sessions"),
134
+ journal: journalDoc,
135
+ capabilities: hostCapabilities(ctx)
136
+ });
137
+ const trash = new TrashEngine({
138
+ adapter: registryAdapter(ctx),
139
+ sessions: () => (ctx.get("sessions")?.list() ?? []).map((session) => session.header?.id).filter((id) => typeof id === "string"),
140
+ sessionsRoot: join(dshHome, "sessions"),
141
+ projcacheRoot: join(dshHome, "storages", "session_projcache", "sessions"),
142
+ kitRoot: root,
143
+ ledger: ledgerDoc,
144
+ vault: { dropSession: (sessionId) => manifestDoc.update((current) => ({ ...current, records: current.records.filter((record) => record.sessionId !== sessionId) })) },
145
+ journal: () => journalCache,
146
+ settings: () => settingsCache.trash ?? {}
147
+ });
148
+ const kit = {
149
+ settings: settingsDoc,
150
+ manifest: manifestDoc,
151
+ journal: journalDoc,
152
+ move,
153
+ trash,
154
+ updateSettings: async (patch) => {
155
+ const next = await settingsDoc.update((current) => deepMerge(current, patch));
156
+ settingsCache = next;
157
+ return next;
158
+ },
159
+ vaultSync: async (records) => {
160
+ const manifest = await manifestDoc.update(() => ({
161
+ version: 1,
162
+ records: (Array.isArray(records) ? records : []).filter((record) => typeof record.sessionId === "string" && typeof record.attachmentId === "string").slice(-2e3).map((record) => ({
163
+ sessionId: record.sessionId,
164
+ attachmentId: record.attachmentId,
165
+ name: String(record.name ?? ""),
166
+ size: Number(record.size) || 0,
167
+ hash: String(record.hash ?? ""),
168
+ createdAt: Number(record.createdAt) || Date.now()
169
+ }))
170
+ }));
171
+ return manifest;
172
+ },
173
+ state: async () => {
174
+ const settings = await settingsDoc.read();
175
+ settingsCache = settings;
176
+ const journal = (await journalDoc.read().catch(() => ({ active: journalCache }))).active;
177
+ journalCache = journal;
178
+ const manifest = await manifestDoc.read();
179
+ return {
180
+ pluginVersion,
181
+ capabilities,
182
+ settings,
183
+ journal,
184
+ trash: await trash.items(),
185
+ // Bytes live in browser IndexedDB; the manifest mirrors their metadata only.
186
+ vault: { records: manifest.records.length, bytes: manifest.records.reduce((total, record) => total + (record.size ?? 0), 0) },
187
+ recovery: journal === null ? null : `\u672A\u5B8C\u6210\u8FC1\u79FB\uFF1A${journal.sessionId} \u2192 ${journal.toCwd}\uFF08\u5DF2\u5B8C\u6210 ${journal.done.join("/") || "\u65E0"}\uFF09`
188
+ };
189
+ },
190
+ action: (input) => dispatch(kit, input)
191
+ };
192
+ ctx.inject?.(["webServer"], (webCtx) => {
193
+ capabilities.webServer = true;
194
+ webCtx.effect(() => webCtx.webServer.register({ kind: "exact", path: "/.well-known/dsh-session-toolbox", handler: apiHandler(kit) }));
195
+ });
196
+ ctx.provide("sessionToolbox", kit);
197
+ ctx.effect(() => () => {
198
+ });
199
+ ctx.logger?.info?.("dsh-session-toolbox ready", JSON.stringify({ version: pluginVersion, capabilities }));
200
+ }
201
+ function hostCapabilities(ctx) {
202
+ return {
203
+ sessionLive: (sessionId) => {
204
+ const sessions = ctx.get("sessions");
205
+ return sessions ? sessions.list().some((session) => session.header?.id === sessionId) : false;
206
+ },
207
+ listHeaders: async () => {
208
+ const persistence = ctx.get("sessionPersistence");
209
+ if (persistence?.list !== void 0) return (await persistence.list()).map((snapshot) => snapshot.header);
210
+ const registry = ctx.get("workspaceRegistry");
211
+ if (registry?.listStoredHeaders !== void 0) return await registry.listStoredHeaders();
212
+ throw new Error("no session-persistence service in this DSH build");
213
+ },
214
+ /** Drop stale header entries so `attachSession` validates the NEW on-disk cwd (§10 MR4). */
215
+ refreshHeaderCache: async () => {
216
+ const registry = ctx.get("workspaceRegistry");
217
+ if (typeof registry?.replaceHeaderIndex !== "function") return;
218
+ await registry.replaceHeaderIndex(await registry.listStoredHeaders());
219
+ },
220
+ /** Ledger step (§10 MR2 step 2): source detach → target create → attach. */
221
+ moveLedger: async (sessionId, fromCwd, toCwd) => {
222
+ const registry = ctx.get("workspaceRegistry");
223
+ if (registry === void 0) throw new Error("workspaceRegistry unavailable in this DSH build");
224
+ const canonicalFrom = fromCwd === void 0 ? void 0 : await realpath(fromCwd).catch(() => fromCwd);
225
+ const source = canonicalFrom === void 0 ? void 0 : await registry.resolveByPath(canonicalFrom);
226
+ if (source !== void 0) await source.detachSession(sessionId);
227
+ let target = await registry.resolveByPath(toCwd);
228
+ if (target === void 0) {
229
+ if (!await stat(toCwd).then((s) => s.isDirectory(), () => false)) throw new Error(`target directory ${toCwd} does not exist`);
230
+ target = await registry.create(toCwd);
231
+ }
232
+ await target.attachSession(sessionId);
233
+ },
234
+ resyncIndex: async () => {
235
+ return ctx.get("sessionQuery") !== void 0 ? "lazy" : "unavailable";
236
+ }
237
+ };
238
+ }
239
+ function registryAdapter(ctx) {
240
+ const registry = () => {
241
+ const value = ctx.get("workspaceRegistry");
242
+ if (value === void 0) throw Object.assign(new Error("workspaceRegistry unavailable"), { code: "no-capability" });
243
+ return value;
244
+ };
245
+ return {
246
+ sessionKnown: async (sessionId) => {
247
+ const rows = registry();
248
+ return typeof rows.sessionKnown === "function" ? await rows.sessionKnown(sessionId) : false;
249
+ },
250
+ archivedSet: async () => new Set(registry().archivedSessionIds ?? []),
251
+ pinnedSet: async () => new Set(registry().pinnedSessionIds ?? []),
252
+ locateSession: async (sessionId) => {
253
+ for (const entity of registry().list()) {
254
+ const ids = entity.sessionIds ?? [];
255
+ if (!ids.includes(sessionId)) continue;
256
+ const at = ids.indexOf(sessionId);
257
+ return { workspaceId: entity.id, beforeId: at + 1 < ids.length ? ids[at + 1] : null, title: null };
258
+ }
259
+ return { workspaceId: null, beforeId: null, title: null };
260
+ },
261
+ workspaceSnapshot: async (workspaceId) => {
262
+ const rows = registry();
263
+ const entity = rows.get(workspaceId);
264
+ if (entity === void 0) return void 0;
265
+ const ids = entity.sessionIds ?? [];
266
+ const archived = new Set(rows.archivedSessionIds ?? []);
267
+ const pinned = new Set(rows.pinnedSessionIds ?? []);
268
+ const order = rows.list().map((row) => row.id);
269
+ const at = order.indexOf(entity.id);
270
+ return {
271
+ workspaceId: entity.id,
272
+ path: entity.path,
273
+ title: entity.title,
274
+ beforeId: at > 0 ? order[at - 1] : null,
275
+ sessions: ids.map((sessionId, index) => ({
276
+ sessionId,
277
+ previouslyArchived: archived.has(sessionId),
278
+ previouslyPinned: pinned.has(sessionId),
279
+ beforeId: index + 1 < ids.length ? ids[index + 1] : null
280
+ }))
281
+ };
282
+ },
283
+ archiveSession: (sessionId, options) => registry().archiveSession(sessionId, options),
284
+ unarchiveSession: (sessionId) => registry().unarchiveSession(sessionId),
285
+ pinSession: (sessionId) => registry().pinSession(sessionId),
286
+ unpinSession: (sessionId) => registry().unpinSession(sessionId),
287
+ deleteWorkspace: (workspaceId) => registry().delete(workspaceId),
288
+ repairSlot: async (sessionId, workspaceId, beforeId) => {
289
+ const entity = registry().get(workspaceId);
290
+ if (entity === void 0) return;
291
+ if (!entity.sessionIds?.includes(sessionId)) await entity.attachSession(sessionId);
292
+ if (beforeId !== null && entity.sessionIds?.includes(beforeId)) await entity.insertSessionBefore(sessionId, beforeId);
293
+ },
294
+ restoreWorkspace: async (snapshot) => {
295
+ const rows = registry();
296
+ let entity = rows.list().find((row) => row.path === snapshot.path);
297
+ if (entity === void 0) {
298
+ await mkdir(snapshot.path, { recursive: true }).catch(() => {
299
+ });
300
+ entity = await rows.create(snapshot.path, snapshot.title);
301
+ }
302
+ for (const row of [...snapshot.sessions].reverse()) await entity.attachSession(row.sessionId).catch(() => {
303
+ });
304
+ if (snapshot.beforeId !== null && entity.id !== snapshot.beforeId) await rows.insertBefore(entity.id, snapshot.beforeId).catch(() => {
305
+ });
306
+ return entity;
307
+ },
308
+ purgeSession: async () => ctx.get("sessionPersistence")?.delete === void 0 ? "unavailable" : "present"
309
+ };
310
+ }
311
+ function deepMerge(base, patch) {
312
+ const next = { ...base };
313
+ for (const [key, value] of Object.entries(patch ?? {})) {
314
+ next[key] = value !== null && typeof value === "object" && !Array.isArray(value) ? deepMerge(base[key] ?? {}, value) : value;
315
+ }
316
+ return next;
317
+ }
318
+ export {
319
+ Config,
320
+ apply,
321
+ defaultKitRoot,
322
+ hostCapabilities,
323
+ inject,
324
+ name,
325
+ registryAdapter
326
+ };
327
+ //# sourceMappingURL=index.js.map