dsh-sessions-manager 3.2.2 → 3.3.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 mkdir2, realpath, rename as rename2, stat, unlink, writeFile as writeFile2 } from "node:fs/promises";
3
+ import { basename, dirname, isAbsolute, join as join2 } from "node:path";
4
+ import { readFileSync as readFileSync3 } from "node:fs";
5
+ import { homedir as homedir2 } from "node:os";
6
6
 
7
7
  // src/zstd-frame.js
8
8
  import zlib from "node:zlib";
@@ -41,12 +41,221 @@ 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
+
44
253
  // src/index.js
45
254
  var name = "dsh-sessions-manager";
46
255
  var inject = ["webServer", "workspaceRegistry", "sessionPersistence", "sessionQuery", "storageDomain"];
47
256
  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");
257
+ var TRASH_DIR = process.env.DSH_SESSIONS_MANAGER_TRASH_DIR || join2(homedir2(), ".dsh", "sessions-manager-trash");
258
+ var TRASH_INDEX = join2(TRASH_DIR, "index.json");
50
259
  var TRASH_SCHEMA_VERSION = 2;
51
260
  var DEFAULT_TRASH_SETTINGS = Object.freeze({ retentionDays: 0 });
52
261
  var FETCH_TOOL_RE = /search|fetch|download|browse/i;
@@ -77,14 +286,14 @@ function parseIds(body) {
77
286
  const raw = body && body.sessionIds;
78
287
  if (!Array.isArray(raw)) return null;
79
288
  const ids = [];
80
- for (const v of raw) if (typeof v === "string" && isSafeSessionId(v)) ids.push(v);
289
+ for (const v of raw) if (typeof v === "string" && isSafeSessionId2(v)) ids.push(v);
81
290
  return ids;
82
291
  }
83
- function isSafeSessionId(value) {
292
+ function isSafeSessionId2(value) {
84
293
  return typeof value === "string" && value.length > 0 && value.length <= 200 && !/[\\/\0]/.test(value) && value !== "." && value !== "..";
85
294
  }
86
295
  function requireSessionId(value) {
87
- if (!isSafeSessionId(value)) {
296
+ if (!isSafeSessionId2(value)) {
88
297
  const error = new Error("\u65E0\u6548\u7684 sessionId");
89
298
  error.status = 400;
90
299
  throw error;
@@ -213,12 +422,12 @@ function apply(ctx) {
213
422
  schemaVersion: TRASH_SCHEMA_VERSION,
214
423
  settings: { retentionDays },
215
424
  items: raw && Array.isArray(raw.items) ? raw.items : [],
216
- purgedSessionIds: raw && Array.isArray(raw.purgedSessionIds) ? [...new Set(raw.purgedSessionIds.filter(isSafeSessionId).map(String))] : []
425
+ purgedSessionIds: raw && Array.isArray(raw.purgedSessionIds) ? [...new Set(raw.purgedSessionIds.filter(isSafeSessionId2).map(String))] : []
217
426
  };
218
427
  }
219
428
  async function readTrashStore() {
220
429
  try {
221
- return normalizeTrashStore(JSON.parse(readFileSync2(TRASH_INDEX, "utf8")));
430
+ return normalizeTrashStore(JSON.parse(readFileSync3(TRASH_INDEX, "utf8")));
222
431
  } catch (e) {
223
432
  return normalizeTrashStore(null);
224
433
  }
@@ -227,10 +436,10 @@ function apply(ctx) {
227
436
  return (await readTrashStore()).items;
228
437
  }
229
438
  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);
439
+ await mkdir2(TRASH_DIR, { recursive: true });
440
+ const tmp = join2(TRASH_DIR, `.index-${process.pid}-${Date.now()}.tmp`);
441
+ await writeFile2(tmp, JSON.stringify(normalizeTrashStore(store), null, 2), { encoding: "utf8", mode: 384 });
442
+ await rename2(tmp, TRASH_INDEX);
234
443
  }
235
444
  function mutateTrash(mutator) {
236
445
  const operation = trashMutation.then(async () => {
@@ -243,6 +452,16 @@ function apply(ctx) {
243
452
  });
244
453
  return operation;
245
454
  }
455
+ const stars = createStarIndex();
456
+ async function gcStars(validIds) {
457
+ try {
458
+ const store = await stars.read();
459
+ const valid = new Set(validIds.map(String));
460
+ const gone = store.starredSessionIds.filter((id) => !valid.has(id));
461
+ if (gone.length) await stars.removeIds(gone);
462
+ } catch (e) {
463
+ }
464
+ }
246
465
  async function deleteOne(sid) {
247
466
  requireSessionId(sid);
248
467
  let header = null;
@@ -382,6 +601,8 @@ function apply(ctx) {
382
601
  purged = true;
383
602
  });
384
603
  if (!purged) throw new Error("\u5F7B\u5E95\u5220\u9664\u5931\u8D25");
604
+ stars.removeIds([sid]).catch(() => {
605
+ });
385
606
  return { ok: true, purged: true };
386
607
  }
387
608
  async function trashSettings(next) {
@@ -416,8 +637,8 @@ function apply(ctx) {
416
637
  async function moveTargetWorkspace(rawPath) {
417
638
  if (typeof rawPath !== "string" || !rawPath.trim()) throw new Error("\u7F3A\u5C11\u76EE\u6807\u5DE5\u4F5C\u533A\u8DEF\u5F84");
418
639
  let p = String(rawPath).trim();
419
- if (p.startsWith("~/")) p = join(homedir(), p.slice(2));
420
- if (!isAbsolute(p)) p = join(homedir(), p);
640
+ if (p.startsWith("~/")) p = join2(homedir2(), p.slice(2));
641
+ if (!isAbsolute(p)) p = join2(homedir2(), p);
421
642
  let canonical = null;
422
643
  try {
423
644
  canonical = await realpath(p);
@@ -425,7 +646,7 @@ function apply(ctx) {
425
646
  canonical = null;
426
647
  }
427
648
  if (canonical === null) {
428
- await mkdir(p, { recursive: true });
649
+ await mkdir2(p, { recursive: true });
429
650
  canonical = await realpath(p);
430
651
  }
431
652
  return { canonical, entity: await w.create(canonical, basename(canonical) || "workspace") };
@@ -463,13 +684,13 @@ function apply(ctx) {
463
684
  if (!oldPath || !newPath || oldPath === newPath) return false;
464
685
  const backupPath = `${oldPath}.move-backup-${Date.now()}`;
465
686
  try {
466
- await mkdir(dirname(newPath), { recursive: true });
467
- await rename(oldPath, backupPath);
687
+ await mkdir2(dirname(newPath), { recursive: true });
688
+ await rename2(oldPath, backupPath);
468
689
  await rewriteFrame0Cwd(backupPath, canonical);
469
- await rename(backupPath, newPath);
690
+ await rename2(backupPath, newPath);
470
691
  } catch (e) {
471
692
  try {
472
- await rename(backupPath, oldPath);
693
+ await rename2(backupPath, oldPath);
473
694
  } catch (_) {
474
695
  }
475
696
  if (e && e.code !== "ENOENT") throw e;
@@ -515,7 +736,7 @@ function apply(ctx) {
515
736
  const backupPath = oldPath ? `${oldPath}.move-backup-${Date.now()}` : null;
516
737
  if (backupPath) {
517
738
  try {
518
- await rename(oldPath, backupPath);
739
+ await rename2(oldPath, backupPath);
519
740
  } catch (e) {
520
741
  if (e && e.code !== "ENOENT") throw new Error("\u79FB\u52A8\u5931\u8D25\uFF1A\u65E0\u6CD5\u5907\u4EFD\u65E7\u7684\u4F1A\u8BDD\u65E5\u5FD7");
521
742
  }
@@ -523,7 +744,7 @@ function apply(ctx) {
523
744
  const restore = async () => {
524
745
  if (backupPath) {
525
746
  try {
526
- await rename(backupPath, oldPath);
747
+ await rename2(backupPath, oldPath);
527
748
  } catch (_) {
528
749
  }
529
750
  }
@@ -620,9 +841,11 @@ function apply(ctx) {
620
841
  async function allSessionItems() {
621
842
  let materialized = /* @__PURE__ */ new Set();
622
843
  let live = ctx.get("sessions");
844
+ let headersOk = false;
623
845
  try {
624
846
  const headers = await sp.list();
625
847
  materialized = new Set(headers.map((h) => String(h.id)));
848
+ headersOk = true;
626
849
  } catch (e) {
627
850
  }
628
851
  const ids = [];
@@ -658,6 +881,13 @@ function apply(ctx) {
658
881
  const res2 = await Promise.all(visibleIds.slice(i, i + CHUNK).map(resolveOne));
659
882
  for (const it of res2) items.push({ ...it, archived: currentArchived.has(it.sessionId) });
660
883
  }
884
+ let starredSet = /* @__PURE__ */ new Set();
885
+ try {
886
+ starredSet = new Set((await stars.read()).starredSessionIds);
887
+ } catch (e) {
888
+ }
889
+ for (const it of items) it.starred = starredSet.has(String(it.sessionId));
890
+ if (headersOk) await gcStars(ids);
661
891
  return items;
662
892
  }
663
893
  async function sidebarAuthority() {
@@ -1053,6 +1283,51 @@ function apply(ctx) {
1053
1283
  }
1054
1284
  }
1055
1285
  }));
1286
+ disposers.push(ctx.webServer.register({
1287
+ kind: "exact",
1288
+ path: "/archived-sessions/star/set",
1289
+ handler: async (req, res) => {
1290
+ try {
1291
+ const body = await readJsonBody(req);
1292
+ const starred = !!(body && body.starred);
1293
+ let ids = parseIds(body);
1294
+ if ((!ids || ids.length === 0) && body && typeof body.sessionId === "string") {
1295
+ ids = isSafeSessionId2(body.sessionId) ? [body.sessionId] : null;
1296
+ }
1297
+ if (!ids || ids.length === 0) return json(res, { ok: false, error: "missing sessionId" }, 400);
1298
+ const starredSessionIds = await stars.setStarred(ids, starred);
1299
+ json(res, { ok: true, starredSessionIds });
1300
+ } catch (e) {
1301
+ json(res, { ok: false, error: String(e && e.message || e) }, errorStatus(e));
1302
+ }
1303
+ }
1304
+ }));
1305
+ disposers.push(ctx.webServer.register({
1306
+ kind: "exact",
1307
+ path: "/archived-sessions/export-md",
1308
+ handler: async (req, res) => {
1309
+ try {
1310
+ const url = new URL(req.url, "http://localhost");
1311
+ const sid = url.searchParams.get("sessionId");
1312
+ requireSessionId(sid);
1313
+ const r = await sp.readFrom(sid, 0);
1314
+ if (!r || !r.meta) {
1315
+ const error = new Error("\u65E0\u6CD5\u8BFB\u53D6\u8BE5\u4F1A\u8BDD\u7684\u65E5\u5FD7");
1316
+ error.status = 404;
1317
+ throw error;
1318
+ }
1319
+ const md = renderSessionMarkdown({ ...r.meta, id: sid }, r.events || []);
1320
+ res.writeHead(200, {
1321
+ "content-type": "text/markdown; charset=utf-8",
1322
+ "content-disposition": `attachment; filename="dsh-session-${sid}.md"`,
1323
+ "cache-control": "no-store"
1324
+ });
1325
+ res.end(md);
1326
+ } catch (e) {
1327
+ json(res, { error: String(e && e.message || e) }, errorStatus(e));
1328
+ }
1329
+ }
1330
+ }));
1056
1331
  disposers.push(ctx.webServer.register({
1057
1332
  kind: "exact",
1058
1333
  path: "/archived-sessions/sidebar-state",