dsh-sessions-manager 3.2.2 → 3.4.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
@@ -1,8 +1,8 @@
1
1
  // src/index.js
2
- import { mkdir, realpath, rename, stat, unlink, writeFile } from "node:fs/promises";
3
- import { basename, dirname, isAbsolute, join } from "node:path";
4
- import { readFileSync as readFileSync2 } from "node:fs";
5
- import { homedir } from "node:os";
2
+ import { mkdir as mkdir3, realpath, rename as rename3, stat, unlink, writeFile as writeFile3 } from "node:fs/promises";
3
+ import { basename, dirname, isAbsolute, join as join3 } from "node:path";
4
+ import { readFileSync as readFileSync4 } from "node:fs";
5
+ import { homedir as homedir3 } from "node:os";
6
6
 
7
7
  // src/zstd-frame.js
8
8
  import zlib from "node:zlib";
@@ -41,17 +41,388 @@ function rewriteFrame0Cwd(filePath, newCwd) {
41
41
  writeFileSync(filePath, Buffer.concat([newFrame0, rest]));
42
42
  }
43
43
 
44
+ // src/markdown.js
45
+ var MAX_TOOL_ARG = 200;
46
+ function isoTime(value) {
47
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return null;
48
+ try {
49
+ return new Date(value).toISOString();
50
+ } catch {
51
+ return null;
52
+ }
53
+ }
54
+ function yamlString(value) {
55
+ return `"${String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\r?\n/g, "\\n")}"`;
56
+ }
57
+ function blocksOf(value) {
58
+ return Array.isArray(value) ? value.filter((b) => b && typeof b === "object") : [];
59
+ }
60
+ function textFromBlocks(blocks) {
61
+ const parts = [];
62
+ for (const block of blocks) {
63
+ if (block.type === "text" && typeof block.text === "string") parts.push(block.text);
64
+ }
65
+ return parts.join("\n\n").trim();
66
+ }
67
+ function imageCountOf(blocks) {
68
+ let count = 0;
69
+ for (const block of blocks) if (block.type === "image") count++;
70
+ return count;
71
+ }
72
+ function reasoningFromBlocks(blocks) {
73
+ const parts = [];
74
+ for (const block of blocks) {
75
+ if (block.type === "reasoning" && typeof block.text === "string" && block.text.trim()) parts.push(block.text.trim());
76
+ }
77
+ return parts.join("\n\n");
78
+ }
79
+ function summarizeToolArguments(name2, rawArguments) {
80
+ let parsed = null;
81
+ if (typeof rawArguments === "string") {
82
+ try {
83
+ parsed = JSON.parse(rawArguments);
84
+ } catch {
85
+ parsed = null;
86
+ }
87
+ } else if (rawArguments && typeof rawArguments === "object") {
88
+ parsed = rawArguments;
89
+ }
90
+ if (parsed === null) return typeof rawArguments === "string" ? rawArguments.slice(0, MAX_TOOL_ARG) : "";
91
+ if (typeof parsed !== "object") return String(parsed).slice(0, MAX_TOOL_ARG);
92
+ const preferred = ["command", "file_path", "path", "query", "url", "pattern"];
93
+ for (const key of preferred) {
94
+ if (typeof parsed[key] === "string" && parsed[key].trim()) return parsed[key];
95
+ }
96
+ const keys = Object.keys(parsed);
97
+ if (keys.length === 0) return "";
98
+ const rest = {};
99
+ for (const key of keys.slice(0, 6)) {
100
+ const value = parsed[key];
101
+ rest[key] = typeof value === "string" ? value : JSON.stringify(value);
102
+ }
103
+ return JSON.stringify(rest).slice(0, MAX_TOOL_ARG);
104
+ }
105
+ function renderSessionMarkdown(meta, events, options = {}) {
106
+ const includeReasoning = options.includeReasoning === true;
107
+ const includeToolResults = options.includeToolResults === true;
108
+ const header = meta && typeof meta === "object" ? meta : {};
109
+ const list = Array.isArray(events) ? events : [];
110
+ let title = typeof header.title === "string" && header.title.trim() ? header.title.trim() : null;
111
+ for (const ev of list) {
112
+ const data = ev && ev.data;
113
+ if (ev && ev.type === "session/title" && data && typeof data.title === "string" && data.title.trim()) {
114
+ title = data.title.trim();
115
+ }
116
+ }
117
+ const front = ["---"];
118
+ if (title) front.push(`title: ${yamlString(title)}`);
119
+ if (typeof header.id === "string" && header.id) front.push(`sessionId: ${yamlString(header.id)}`);
120
+ if (typeof header.cwd === "string" && header.cwd) front.push(`cwd: ${yamlString(header.cwd)}`);
121
+ const created = isoTime(header.createdAt);
122
+ if (created) front.push(`createdAt: ${created}`);
123
+ const exported = isoTime(options.exportedAt);
124
+ if (exported) front.push(`exportedAt: ${exported}`);
125
+ front.push("---");
126
+ const out = [front.join("\n")];
127
+ if (title) out.push("", `# ${title}`);
128
+ let turn = null;
129
+ for (const ev of list) {
130
+ if (!ev || typeof ev !== "object") continue;
131
+ const data = ev.data && typeof ev.data === "object" ? ev.data : {};
132
+ const type = ev.type;
133
+ if (type === "turn/start") {
134
+ const next = Number.isInteger(data.turn) ? data.turn : null;
135
+ if (next !== null && next !== turn) {
136
+ turn = next;
137
+ out.push("", `## \u7B2C ${turn} \u8F6E`);
138
+ }
139
+ continue;
140
+ }
141
+ if (type === "user/message") {
142
+ const blocks = blocksOf(data.content);
143
+ const text = textFromBlocks(blocks);
144
+ const images = imageCountOf(blocks);
145
+ if (!text && images === 0) continue;
146
+ out.push("", "### \u7528\u6237", "");
147
+ if (text) out.push(text);
148
+ for (let i = 0; i < images; i++) out.push("", `![\u56FE\u7247 ${i + 1}](attachment)`);
149
+ continue;
150
+ }
151
+ if (type === "assistant/message") {
152
+ const message = data.message && typeof data.message === "object" ? data.message : {};
153
+ const blocks = blocksOf(message.content);
154
+ const text = textFromBlocks(blocks);
155
+ const reasoning = includeReasoning ? reasoningFromBlocks(blocks) : "";
156
+ if (!text && !reasoning) continue;
157
+ out.push("", "### \u52A9\u624B", "");
158
+ if (reasoning) out.push("> \u601D\u8003\uFF1A" + reasoning.split("\n").join("\n> "), "");
159
+ if (text) out.push(text);
160
+ continue;
161
+ }
162
+ if (type === "tool/call") {
163
+ const name2 = typeof data.name === "string" && data.name ? data.name : "tool";
164
+ const summary = summarizeToolArguments(name2, data.arguments);
165
+ out.push("", `### \u5DE5\u5177\u8C03\u7528\uFF1A\`${name2}\``, "");
166
+ out.push(summary ? "```\n" + summary + "\n```" : "\uFF08\u65E0\u53C2\u6570\uFF09");
167
+ continue;
168
+ }
169
+ if (type === "tool/result" && includeToolResults) {
170
+ const message = data.message && typeof data.message === "object" ? data.message : {};
171
+ const blocks = blocksOf(message.content);
172
+ let text = "";
173
+ for (const block of blocks) {
174
+ if (block.type === "tool-result") text = textFromBlocks(blocksOf(block.content));
175
+ }
176
+ if (text) out.push("", "<details><summary>\u5DE5\u5177\u7ED3\u679C</summary>", "", "```\n" + text.slice(0, 2e3) + "\n```", "", "</details>");
177
+ }
178
+ }
179
+ out.push("");
180
+ return out.join("\n");
181
+ }
182
+
183
+ // src/star-index.js
184
+ import { mkdir, rename, writeFile } from "node:fs/promises";
185
+ import { readFileSync as readFileSync2 } from "node:fs";
186
+ import { homedir } from "node:os";
187
+ import { join } from "node:path";
188
+ var STAR_SCHEMA_VERSION = 3;
189
+ var DEFAULT_STAR_DIR = join(homedir(), ".dsh", "sessions-manager");
190
+ function isSafeSessionId(value) {
191
+ return typeof value === "string" && value.length > 0 && value.length <= 200 && !/[\\/\0]/.test(value) && value !== "." && value !== "..";
192
+ }
193
+ function normalizeStarStore(raw) {
194
+ const legacy = Array.isArray(raw) ? raw : null;
195
+ const source = legacy || (raw && typeof raw === "object" ? raw : null);
196
+ const ids = source && Array.isArray(source.starredSessionIds) ? source.starredSessionIds : legacy || [];
197
+ const clean = [];
198
+ const seen = /* @__PURE__ */ new Set();
199
+ for (const id of ids) {
200
+ if (!isSafeSessionId(id)) continue;
201
+ if (seen.has(id)) continue;
202
+ seen.add(id);
203
+ clean.push(id);
204
+ }
205
+ return { schemaVersion: STAR_SCHEMA_VERSION, starredSessionIds: clean };
206
+ }
207
+ function createStarIndex(options = {}) {
208
+ const dir = options.dir || process.env.DSH_SESSIONS_MANAGER_STAR_DIR || DEFAULT_STAR_DIR;
209
+ const indexPath = options.indexPath || join(dir, "star.json");
210
+ let mutation = Promise.resolve();
211
+ async function read() {
212
+ try {
213
+ return normalizeStarStore(JSON.parse(readFileSync2(indexPath, "utf8")));
214
+ } catch {
215
+ return normalizeStarStore(null);
216
+ }
217
+ }
218
+ async function write(store) {
219
+ await mkdir(dir, { recursive: true });
220
+ const tmp = join(dir, `.star-${process.pid}-${Date.now()}.tmp`);
221
+ await writeFile(tmp, JSON.stringify(normalizeStarStore(store), null, 2), { encoding: "utf8", mode: 384 });
222
+ await rename(tmp, indexPath);
223
+ }
224
+ function mutate(mutator) {
225
+ const operation = mutation.then(async () => {
226
+ const store = await read();
227
+ const result = await mutator(store);
228
+ await write(store);
229
+ return result;
230
+ });
231
+ mutation = operation.catch(() => {
232
+ });
233
+ return operation;
234
+ }
235
+ function setStarred(ids, starred) {
236
+ const wanted = (Array.isArray(ids) ? ids : []).filter(isSafeSessionId).map(String);
237
+ return mutate((store) => {
238
+ const set = new Set(store.starredSessionIds);
239
+ for (const id of wanted) {
240
+ if (starred) set.add(id);
241
+ else set.delete(id);
242
+ }
243
+ store.starredSessionIds = [...set];
244
+ return store.starredSessionIds;
245
+ });
246
+ }
247
+ function removeIds(ids) {
248
+ return setStarred(ids, false);
249
+ }
250
+ return { read, write, mutate, setStarred, removeIds, indexPath, dir };
251
+ }
252
+
253
+ // src/storage-stats.js
254
+ var UNGROUPED_KEY = "__ungrouped__";
255
+ function isFiniteSize(value) {
256
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
257
+ }
258
+ function aggregateStorage(items, options = {}) {
259
+ const topN = Number.isInteger(options.topN) && options.topN > 0 ? options.topN : 10;
260
+ const list = Array.isArray(items) ? items : [];
261
+ const buckets = /* @__PURE__ */ new Map();
262
+ const sized = [];
263
+ let totalBytes = 0;
264
+ let unknownSessions = 0;
265
+ let counted = 0;
266
+ for (const item of list) {
267
+ if (!item || item.sessionId == null) continue;
268
+ counted++;
269
+ const id = String(item.sessionId);
270
+ const path = item.workspacePath ? String(item.workspacePath) : null;
271
+ const key = path || UNGROUPED_KEY;
272
+ let bucket = buckets.get(key);
273
+ if (!bucket) {
274
+ bucket = { key, path, title: item.workspaceTitle ? String(item.workspaceTitle) : null, bytes: 0, sessions: 0 };
275
+ buckets.set(key, bucket);
276
+ }
277
+ bucket.sessions++;
278
+ if (isFiniteSize(item.sizeBytes)) {
279
+ bucket.bytes += item.sizeBytes;
280
+ totalBytes += item.sizeBytes;
281
+ sized.push({
282
+ sessionId: id,
283
+ title: item.title || null,
284
+ workspacePath: path,
285
+ workspaceTitle: bucket.title,
286
+ sizeBytes: item.sizeBytes
287
+ });
288
+ } else {
289
+ unknownSessions++;
290
+ }
291
+ }
292
+ const workspaces = [...buckets.values()].sort((a, b) => b.bytes - a.bytes || b.sessions - a.sessions || a.key.localeCompare(b.key)).map((bucket) => ({ ...bucket, share: totalBytes > 0 ? bucket.bytes / totalBytes : 0 }));
293
+ const top = sized.sort((a, b) => b.sizeBytes - a.sizeBytes || a.sessionId.localeCompare(b.sessionId)).slice(0, topN);
294
+ return {
295
+ totalBytes,
296
+ sessionCount: counted,
297
+ sizedSessions: sized.length,
298
+ unknownSessions,
299
+ workspaces,
300
+ top
301
+ };
302
+ }
303
+
304
+ // src/auto-archive.js
305
+ import { mkdir as mkdir2, rename as rename2, writeFile as writeFile2 } from "node:fs/promises";
306
+ import { readFileSync as readFileSync3 } from "node:fs";
307
+ import { homedir as homedir2 } from "node:os";
308
+ import { join as join2 } from "node:path";
309
+ var AUTO_ARCHIVE_SCHEMA_VERSION = 4;
310
+ var INACTIVE_DAY_OPTIONS = Object.freeze([0, 30, 60, 90]);
311
+ var DAY_MS = 864e5;
312
+ var RUN_INTERVAL_MS = DAY_MS;
313
+ var DEFAULT_DIR = join2(homedir2(), ".dsh", "sessions-manager");
314
+ function normalizeAutoArchiveStore(raw) {
315
+ const source = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
316
+ const settings = source.settings && typeof source.settings === "object" ? source.settings : {};
317
+ const inactiveDays = INACTIVE_DAY_OPTIONS.includes(settings.inactiveDays) ? settings.inactiveDays : 0;
318
+ return {
319
+ schemaVersion: AUTO_ARCHIVE_SCHEMA_VERSION,
320
+ settings: {
321
+ inactiveDays,
322
+ // Starred sessions are an explicit "keep" mark, so they are skipped
323
+ // unless the user opts out.
324
+ skipStarred: settings.skipStarred !== false
325
+ },
326
+ lastRunAt: Number.isFinite(source.lastRunAt) ? source.lastRunAt : null,
327
+ lastArchivedCount: Number.isInteger(source.lastArchivedCount) && source.lastArchivedCount >= 0 ? source.lastArchivedCount : 0
328
+ };
329
+ }
330
+ function pickInactiveCandidates(items, options = {}) {
331
+ const days = options.inactiveDays;
332
+ if (!INACTIVE_DAY_OPTIONS.includes(days) || days === 0) return [];
333
+ const now = Number.isFinite(options.now) ? options.now : Date.now();
334
+ const cutoff = now - days * DAY_MS;
335
+ const skipStarred = options.skipStarred !== false;
336
+ const activeId = options.activeSessionId != null ? String(options.activeSessionId) : null;
337
+ const list = Array.isArray(items) ? items : [];
338
+ const out = [];
339
+ const seen = /* @__PURE__ */ new Set();
340
+ for (const item of list) {
341
+ if (!item || item.sessionId == null) continue;
342
+ const id = String(item.sessionId);
343
+ if (seen.has(id)) continue;
344
+ if (item.archived) continue;
345
+ if (skipStarred && item.starred) continue;
346
+ if (activeId !== null && id === activeId) continue;
347
+ const updatedAt = Number(item.updatedAt);
348
+ if (!Number.isFinite(updatedAt) || updatedAt <= 0) continue;
349
+ if (updatedAt < cutoff) {
350
+ seen.add(id);
351
+ out.push(id);
352
+ }
353
+ }
354
+ return out;
355
+ }
356
+ function createAutoArchiveStore(options = {}) {
357
+ const dir = options.dir || process.env.DSH_SESSIONS_MANAGER_AUTO_ARCHIVE_DIR || DEFAULT_DIR;
358
+ const indexPath = options.indexPath || join2(dir, "auto-archive.json");
359
+ let mutation = Promise.resolve();
360
+ async function read() {
361
+ try {
362
+ return normalizeAutoArchiveStore(JSON.parse(readFileSync3(indexPath, "utf8")));
363
+ } catch {
364
+ return normalizeAutoArchiveStore(null);
365
+ }
366
+ }
367
+ async function write(store) {
368
+ await mkdir2(dir, { recursive: true });
369
+ const tmp = join2(dir, `.auto-archive-${process.pid}-${Date.now()}.tmp`);
370
+ await writeFile2(tmp, JSON.stringify(normalizeAutoArchiveStore(store), null, 2), { encoding: "utf8", mode: 384 });
371
+ await rename2(tmp, indexPath);
372
+ }
373
+ function mutate(mutator) {
374
+ const operation = mutation.then(async () => {
375
+ const store = await read();
376
+ const result = await mutator(store);
377
+ await write(store);
378
+ return result;
379
+ });
380
+ mutation = operation.catch(() => {
381
+ });
382
+ return operation;
383
+ }
384
+ function update(patch = {}) {
385
+ return mutate((store) => {
386
+ if (Object.prototype.hasOwnProperty.call(patch, "inactiveDays")) {
387
+ const days = Number(patch.inactiveDays);
388
+ if (!INACTIVE_DAY_OPTIONS.includes(days)) {
389
+ const error = new Error(`inactiveDays \u4EC5\u652F\u6301 ${INACTIVE_DAY_OPTIONS.join("\u3001")}`);
390
+ error.status = 400;
391
+ throw error;
392
+ }
393
+ store.settings.inactiveDays = days;
394
+ }
395
+ if (Object.prototype.hasOwnProperty.call(patch, "skipStarred")) {
396
+ store.settings.skipStarred = !!patch.skipStarred;
397
+ }
398
+ return store.settings;
399
+ });
400
+ }
401
+ function recordRun(count, at = Date.now()) {
402
+ return mutate((store) => {
403
+ store.lastRunAt = at;
404
+ store.lastArchivedCount = Number.isInteger(count) && count >= 0 ? count : 0;
405
+ return store;
406
+ });
407
+ }
408
+ function isFresh(store, now = Date.now()) {
409
+ return Number.isFinite(store && store.lastRunAt) && now - store.lastRunAt < RUN_INTERVAL_MS;
410
+ }
411
+ return { read, write, mutate, update, recordRun, isFresh, indexPath, dir };
412
+ }
413
+
44
414
  // src/index.js
45
415
  var name = "dsh-sessions-manager";
46
416
  var inject = ["webServer", "workspaceRegistry", "sessionPersistence", "sessionQuery", "storageDomain"];
47
417
  var MAX_TITLE = 80;
48
- var TRASH_DIR = process.env.DSH_SESSIONS_MANAGER_TRASH_DIR || join(homedir(), ".dsh", "sessions-manager-trash");
49
- var TRASH_INDEX = join(TRASH_DIR, "index.json");
418
+ var TRASH_DIR = process.env.DSH_SESSIONS_MANAGER_TRASH_DIR || join3(homedir3(), ".dsh", "sessions-manager-trash");
419
+ var TRASH_INDEX = join3(TRASH_DIR, "index.json");
50
420
  var TRASH_SCHEMA_VERSION = 2;
51
421
  var DEFAULT_TRASH_SETTINGS = Object.freeze({ retentionDays: 0 });
52
422
  var FETCH_TOOL_RE = /search|fetch|download|browse/i;
53
423
  var MAX_FETCHES = 12;
54
424
  var MAX_FILES = 20;
425
+ var MAX_STORAGE_TOP = 50;
55
426
  function json(res, value, status = 200) {
56
427
  res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
57
428
  res.end(JSON.stringify(value));
@@ -77,14 +448,14 @@ function parseIds(body) {
77
448
  const raw = body && body.sessionIds;
78
449
  if (!Array.isArray(raw)) return null;
79
450
  const ids = [];
80
- for (const v of raw) if (typeof v === "string" && isSafeSessionId(v)) ids.push(v);
451
+ for (const v of raw) if (typeof v === "string" && isSafeSessionId2(v)) ids.push(v);
81
452
  return ids;
82
453
  }
83
- function isSafeSessionId(value) {
454
+ function isSafeSessionId2(value) {
84
455
  return typeof value === "string" && value.length > 0 && value.length <= 200 && !/[\\/\0]/.test(value) && value !== "." && value !== "..";
85
456
  }
86
457
  function requireSessionId(value) {
87
- if (!isSafeSessionId(value)) {
458
+ if (!isSafeSessionId2(value)) {
88
459
  const error = new Error("\u65E0\u6548\u7684 sessionId");
89
460
  error.status = 400;
90
461
  throw error;
@@ -163,7 +534,7 @@ function apply(ctx) {
163
534
  return operation;
164
535
  }
165
536
  let wsByPath = {};
166
- async function resolveOne(id) {
537
+ async function resolveOne(id, usage) {
167
538
  let title = null, createdAt = null, cwd = null;
168
539
  try {
169
540
  const o = await sq.readTitleSnapshot(id);
@@ -190,7 +561,7 @@ function apply(ctx) {
190
561
  const ws = cwd ? wsByPath[cwd] : void 0;
191
562
  const workspaceGone = !!(cwd && !ws);
192
563
  const display = title ? String(title).length > MAX_TITLE ? String(title).slice(0, MAX_TITLE) + "\u2026" : String(title) : null;
193
- return {
564
+ const base = {
194
565
  sessionId: id,
195
566
  title: display,
196
567
  createdAt: createdAt || null,
@@ -199,6 +570,39 @@ function apply(ctx) {
199
570
  workspaceGone: workspaceGone ? true : false,
200
571
  hasWorkspace: !!cwd
201
572
  };
573
+ if (usage) {
574
+ if (usage.sizeById && usage.sizeById.has(id)) base.sizeBytes = usage.sizeById.get(id);
575
+ if (usage.mtimeById && usage.mtimeById.has(id)) base.updatedAt = usage.mtimeById.get(id);
576
+ }
577
+ return base;
578
+ }
579
+ async function collectUsage() {
580
+ const sizeById = /* @__PURE__ */ new Map();
581
+ const mtimeById = /* @__PURE__ */ new Map();
582
+ let headers = [];
583
+ try {
584
+ headers = await sp.list();
585
+ } catch (e) {
586
+ headers = [];
587
+ }
588
+ if (!Array.isArray(headers)) headers = [];
589
+ const CHUNK = 8;
590
+ for (let i = 0; i < headers.length; i += CHUNK) {
591
+ await Promise.all(headers.slice(i, i + CHUNK).map(async (header) => {
592
+ const id = header && header.id != null ? String(header.id) : null;
593
+ if (!id) return;
594
+ try {
595
+ const loc = sp.locate(header);
596
+ if (!loc || typeof loc.path !== "string" || !loc.path) return;
597
+ const st = await stat(loc.path);
598
+ if (!st) return;
599
+ if (typeof st.size === "number") sizeById.set(id, st.size);
600
+ if (typeof st.mtimeMs === "number" && st.mtimeMs > 0) mtimeById.set(id, Math.floor(st.mtimeMs));
601
+ } catch (e) {
602
+ }
603
+ }));
604
+ }
605
+ return { sizeById, mtimeById };
202
606
  }
203
607
  async function restoreOne(sid) {
204
608
  requireSessionId(sid);
@@ -213,12 +617,12 @@ function apply(ctx) {
213
617
  schemaVersion: TRASH_SCHEMA_VERSION,
214
618
  settings: { retentionDays },
215
619
  items: raw && Array.isArray(raw.items) ? raw.items : [],
216
- purgedSessionIds: raw && Array.isArray(raw.purgedSessionIds) ? [...new Set(raw.purgedSessionIds.filter(isSafeSessionId).map(String))] : []
620
+ purgedSessionIds: raw && Array.isArray(raw.purgedSessionIds) ? [...new Set(raw.purgedSessionIds.filter(isSafeSessionId2).map(String))] : []
217
621
  };
218
622
  }
219
623
  async function readTrashStore() {
220
624
  try {
221
- return normalizeTrashStore(JSON.parse(readFileSync2(TRASH_INDEX, "utf8")));
625
+ return normalizeTrashStore(JSON.parse(readFileSync4(TRASH_INDEX, "utf8")));
222
626
  } catch (e) {
223
627
  return normalizeTrashStore(null);
224
628
  }
@@ -227,10 +631,10 @@ function apply(ctx) {
227
631
  return (await readTrashStore()).items;
228
632
  }
229
633
  async function writeTrashStore(store) {
230
- await mkdir(TRASH_DIR, { recursive: true });
231
- const tmp = join(TRASH_DIR, `.index-${process.pid}-${Date.now()}.tmp`);
232
- await writeFile(tmp, JSON.stringify(normalizeTrashStore(store), null, 2), { encoding: "utf8", mode: 384 });
233
- await rename(tmp, TRASH_INDEX);
634
+ await mkdir3(TRASH_DIR, { recursive: true });
635
+ const tmp = join3(TRASH_DIR, `.index-${process.pid}-${Date.now()}.tmp`);
636
+ await writeFile3(tmp, JSON.stringify(normalizeTrashStore(store), null, 2), { encoding: "utf8", mode: 384 });
637
+ await rename3(tmp, TRASH_INDEX);
234
638
  }
235
639
  function mutateTrash(mutator) {
236
640
  const operation = trashMutation.then(async () => {
@@ -243,6 +647,17 @@ function apply(ctx) {
243
647
  });
244
648
  return operation;
245
649
  }
650
+ const stars = createStarIndex();
651
+ const autoArchive = createAutoArchiveStore();
652
+ async function gcStars(validIds) {
653
+ try {
654
+ const store = await stars.read();
655
+ const valid = new Set(validIds.map(String));
656
+ const gone = store.starredSessionIds.filter((id) => !valid.has(id));
657
+ if (gone.length) await stars.removeIds(gone);
658
+ } catch (e) {
659
+ }
660
+ }
246
661
  async function deleteOne(sid) {
247
662
  requireSessionId(sid);
248
663
  let header = null;
@@ -382,6 +797,8 @@ function apply(ctx) {
382
797
  purged = true;
383
798
  });
384
799
  if (!purged) throw new Error("\u5F7B\u5E95\u5220\u9664\u5931\u8D25");
800
+ stars.removeIds([sid]).catch(() => {
801
+ });
385
802
  return { ok: true, purged: true };
386
803
  }
387
804
  async function trashSettings(next) {
@@ -416,8 +833,8 @@ function apply(ctx) {
416
833
  async function moveTargetWorkspace(rawPath) {
417
834
  if (typeof rawPath !== "string" || !rawPath.trim()) throw new Error("\u7F3A\u5C11\u76EE\u6807\u5DE5\u4F5C\u533A\u8DEF\u5F84");
418
835
  let p = String(rawPath).trim();
419
- if (p.startsWith("~/")) p = join(homedir(), p.slice(2));
420
- if (!isAbsolute(p)) p = join(homedir(), p);
836
+ if (p.startsWith("~/")) p = join3(homedir3(), p.slice(2));
837
+ if (!isAbsolute(p)) p = join3(homedir3(), p);
421
838
  let canonical = null;
422
839
  try {
423
840
  canonical = await realpath(p);
@@ -425,7 +842,7 @@ function apply(ctx) {
425
842
  canonical = null;
426
843
  }
427
844
  if (canonical === null) {
428
- await mkdir(p, { recursive: true });
845
+ await mkdir3(p, { recursive: true });
429
846
  canonical = await realpath(p);
430
847
  }
431
848
  return { canonical, entity: await w.create(canonical, basename(canonical) || "workspace") };
@@ -463,13 +880,13 @@ function apply(ctx) {
463
880
  if (!oldPath || !newPath || oldPath === newPath) return false;
464
881
  const backupPath = `${oldPath}.move-backup-${Date.now()}`;
465
882
  try {
466
- await mkdir(dirname(newPath), { recursive: true });
467
- await rename(oldPath, backupPath);
883
+ await mkdir3(dirname(newPath), { recursive: true });
884
+ await rename3(oldPath, backupPath);
468
885
  await rewriteFrame0Cwd(backupPath, canonical);
469
- await rename(backupPath, newPath);
886
+ await rename3(backupPath, newPath);
470
887
  } catch (e) {
471
888
  try {
472
- await rename(backupPath, oldPath);
889
+ await rename3(backupPath, oldPath);
473
890
  } catch (_) {
474
891
  }
475
892
  if (e && e.code !== "ENOENT") throw e;
@@ -515,7 +932,7 @@ function apply(ctx) {
515
932
  const backupPath = oldPath ? `${oldPath}.move-backup-${Date.now()}` : null;
516
933
  if (backupPath) {
517
934
  try {
518
- await rename(oldPath, backupPath);
935
+ await rename3(oldPath, backupPath);
519
936
  } catch (e) {
520
937
  if (e && e.code !== "ENOENT") throw new Error("\u79FB\u52A8\u5931\u8D25\uFF1A\u65E0\u6CD5\u5907\u4EFD\u65E7\u7684\u4F1A\u8BDD\u65E5\u5FD7");
521
938
  }
@@ -523,7 +940,7 @@ function apply(ctx) {
523
940
  const restore = async () => {
524
941
  if (backupPath) {
525
942
  try {
526
- await rename(backupPath, oldPath);
943
+ await rename3(backupPath, oldPath);
527
944
  } catch (_) {
528
945
  }
529
946
  }
@@ -617,12 +1034,14 @@ function apply(ctx) {
617
1034
  return { next: null, value: { ok: true, archived: true } };
618
1035
  });
619
1036
  }
620
- async function allSessionItems() {
1037
+ async function allSessionItems(opts = {}) {
621
1038
  let materialized = /* @__PURE__ */ new Set();
622
1039
  let live = ctx.get("sessions");
1040
+ let headersOk = false;
623
1041
  try {
624
1042
  const headers = await sp.list();
625
1043
  materialized = new Set(headers.map((h) => String(h.id)));
1044
+ headersOk = true;
626
1045
  } catch (e) {
627
1046
  }
628
1047
  const ids = [];
@@ -653,13 +1072,55 @@ function apply(ctx) {
653
1072
  }
654
1073
  const currentArchived = new Set((await archivedState().catch(() => ({ archivedSessionIds: [] }))).archivedSessionIds || []);
655
1074
  const items = [];
1075
+ const usage = opts && opts.usage ? await collectUsage() : null;
656
1076
  const CHUNK = 6;
657
1077
  for (let i = 0; i < visibleIds.length; i += CHUNK) {
658
- const res2 = await Promise.all(visibleIds.slice(i, i + CHUNK).map(resolveOne));
1078
+ const res2 = await Promise.all(visibleIds.slice(i, i + CHUNK).map((id) => resolveOne(id, usage)));
659
1079
  for (const it of res2) items.push({ ...it, archived: currentArchived.has(it.sessionId) });
660
1080
  }
1081
+ let starredSet = /* @__PURE__ */ new Set();
1082
+ try {
1083
+ starredSet = new Set((await stars.read()).starredSessionIds);
1084
+ } catch (e) {
1085
+ }
1086
+ for (const it of items) it.starred = starredSet.has(String(it.sessionId));
1087
+ if (headersOk) await gcStars(ids);
661
1088
  return items;
662
1089
  }
1090
+ async function buildStorage(opts = {}) {
1091
+ const items = await allSessionItems({ usage: true });
1092
+ const raw = Number(opts && opts.topN);
1093
+ const topN = Number.isInteger(raw) && raw > 0 ? Math.min(raw, MAX_STORAGE_TOP) : 10;
1094
+ return aggregateStorage(items, { topN });
1095
+ }
1096
+ async function autoArchiveSweep(opts = {}) {
1097
+ const store = await autoArchive.read();
1098
+ const days = store.settings.inactiveDays;
1099
+ if (!days) return { ok: true, skipped: "disabled", archived: 0 };
1100
+ const now = Date.now();
1101
+ if (!(opts && opts.force) && autoArchive.isFresh(store, now)) {
1102
+ return { ok: true, skipped: "throttled", archived: 0, lastRunAt: store.lastRunAt, lastArchivedCount: store.lastArchivedCount };
1103
+ }
1104
+ const items = await allSessionItems({ usage: true });
1105
+ const candidates = pickInactiveCandidates(items, {
1106
+ inactiveDays: days,
1107
+ skipStarred: store.settings.skipStarred,
1108
+ activeSessionId: getActiveSessionId(ctx),
1109
+ now
1110
+ });
1111
+ let archived = 0;
1112
+ const failed = [];
1113
+ for (const sid of candidates) {
1114
+ try {
1115
+ const result = await archiveOne(sid);
1116
+ if (result && result.archived) archived++;
1117
+ } catch (e) {
1118
+ failed.push({ sessionId: sid, error: String(e && e.message || e) });
1119
+ }
1120
+ }
1121
+ await autoArchive.recordRun(archived, now);
1122
+ return { ok: true, archived, candidates: candidates.length, failed, lastRunAt: now };
1123
+ }
663
1124
  async function sidebarAuthority() {
664
1125
  const ids = [];
665
1126
  try {
@@ -866,7 +1327,7 @@ function apply(ctx) {
866
1327
  const items = [];
867
1328
  const CHUNK = 6;
868
1329
  for (let i = 0; i < idStrs.length; i += CHUNK) {
869
- const res2 = await Promise.all(idStrs.slice(i, i + CHUNK).map(resolveOne));
1330
+ const res2 = await Promise.all(idStrs.slice(i, i + CHUNK).map((id) => resolveOne(id)));
870
1331
  items.push.apply(items, res2);
871
1332
  }
872
1333
  json(res, { items });
@@ -1053,6 +1514,51 @@ function apply(ctx) {
1053
1514
  }
1054
1515
  }
1055
1516
  }));
1517
+ disposers.push(ctx.webServer.register({
1518
+ kind: "exact",
1519
+ path: "/archived-sessions/star/set",
1520
+ handler: async (req, res) => {
1521
+ try {
1522
+ const body = await readJsonBody(req);
1523
+ const starred = !!(body && body.starred);
1524
+ let ids = parseIds(body);
1525
+ if ((!ids || ids.length === 0) && body && typeof body.sessionId === "string") {
1526
+ ids = isSafeSessionId2(body.sessionId) ? [body.sessionId] : null;
1527
+ }
1528
+ if (!ids || ids.length === 0) return json(res, { ok: false, error: "missing sessionId" }, 400);
1529
+ const starredSessionIds = await stars.setStarred(ids, starred);
1530
+ json(res, { ok: true, starredSessionIds });
1531
+ } catch (e) {
1532
+ json(res, { ok: false, error: String(e && e.message || e) }, errorStatus(e));
1533
+ }
1534
+ }
1535
+ }));
1536
+ disposers.push(ctx.webServer.register({
1537
+ kind: "exact",
1538
+ path: "/archived-sessions/export-md",
1539
+ handler: async (req, res) => {
1540
+ try {
1541
+ const url = new URL(req.url, "http://localhost");
1542
+ const sid = url.searchParams.get("sessionId");
1543
+ requireSessionId(sid);
1544
+ const r = await sp.readFrom(sid, 0);
1545
+ if (!r || !r.meta) {
1546
+ const error = new Error("\u65E0\u6CD5\u8BFB\u53D6\u8BE5\u4F1A\u8BDD\u7684\u65E5\u5FD7");
1547
+ error.status = 404;
1548
+ throw error;
1549
+ }
1550
+ const md = renderSessionMarkdown({ ...r.meta, id: sid }, r.events || []);
1551
+ res.writeHead(200, {
1552
+ "content-type": "text/markdown; charset=utf-8",
1553
+ "content-disposition": `attachment; filename="dsh-session-${sid}.md"`,
1554
+ "cache-control": "no-store"
1555
+ });
1556
+ res.end(md);
1557
+ } catch (e) {
1558
+ json(res, { error: String(e && e.message || e) }, errorStatus(e));
1559
+ }
1560
+ }
1561
+ }));
1056
1562
  disposers.push(ctx.webServer.register({
1057
1563
  kind: "exact",
1058
1564
  path: "/archived-sessions/sidebar-state",
@@ -1146,6 +1652,55 @@ function apply(ctx) {
1146
1652
  }
1147
1653
  }
1148
1654
  }));
1655
+ disposers.push(ctx.webServer.register({
1656
+ kind: "exact",
1657
+ path: "/archived-sessions/storage",
1658
+ handler: async (req, res) => {
1659
+ try {
1660
+ const body = await readJsonBody(req);
1661
+ json(res, await buildStorage({ topN: body && body.topN }));
1662
+ } catch (e) {
1663
+ json(res, { error: String(e && e.message || e) }, 500);
1664
+ }
1665
+ }
1666
+ }));
1667
+ disposers.push(ctx.webServer.register({
1668
+ kind: "exact",
1669
+ path: "/archived-sessions/auto-archive/settings",
1670
+ handler: async (req, res) => {
1671
+ try {
1672
+ const body = await readJsonBody(req);
1673
+ const patch = {};
1674
+ if (body && Object.prototype.hasOwnProperty.call(body, "inactiveDays")) patch.inactiveDays = body.inactiveDays;
1675
+ if (body && Object.prototype.hasOwnProperty.call(body, "skipStarred")) patch.skipStarred = body.skipStarred;
1676
+ const settings = Object.keys(patch).length ? await autoArchive.update(patch) : (await autoArchive.read()).settings;
1677
+ const sweep = await autoArchiveSweep();
1678
+ const store = await autoArchive.read();
1679
+ json(res, {
1680
+ ok: true,
1681
+ settings,
1682
+ lastRunAt: store.lastRunAt,
1683
+ lastArchivedCount: store.lastArchivedCount,
1684
+ sweep
1685
+ });
1686
+ } catch (e) {
1687
+ json(res, { ok: false, error: String(e && e.message || e) }, errorStatus(e));
1688
+ }
1689
+ }
1690
+ }));
1691
+ disposers.push(ctx.webServer.register({
1692
+ kind: "exact",
1693
+ path: "/archived-sessions/auto-archive/run",
1694
+ handler: async (req, res) => {
1695
+ try {
1696
+ const sweep = await autoArchiveSweep({ force: true });
1697
+ const store = await autoArchive.read();
1698
+ json(res, { ok: true, ...sweep, settings: store.settings, lastRunAt: store.lastRunAt, lastArchivedCount: store.lastArchivedCount });
1699
+ } catch (e) {
1700
+ json(res, { ok: false, error: String(e && e.message || e) }, 500);
1701
+ }
1702
+ }
1703
+ }));
1149
1704
  return () => {
1150
1705
  for (const d of disposers) d();
1151
1706
  };