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/README.en.md +24 -19
- package/README.md +24 -19
- package/assets/screenshot-session-autoarch.png +0 -0
- package/assets/screenshot-session-details.png +0 -0
- package/assets/screenshot-session-settings.png +0 -0
- package/assets/screenshot-session-starred.png +0 -0
- package/assets/screenshot-session-storage.png +0 -0
- package/assets/screenshot-session-trash.png +0 -0
- package/lib/client.js +258 -7
- package/lib/client.js.map +2 -2
- package/lib/index.js +584 -29
- package/lib/index.js.map +4 -4
- package/package.json +1 -1
- package/src/auto-archive.js +168 -0
- package/src/client/index.jsx +266 -10
- package/src/client/logic.js +6 -0
- package/src/index.js +238 -5
- package/src/markdown.js +175 -0
- package/src/star-index.js +109 -0
- package/src/storage-stats.js +94 -0
- package/assets/screenshot-session-settingsmenu.png +0 -0
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
|
|
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("", ``);
|
|
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 ||
|
|
49
|
-
var TRASH_INDEX =
|
|
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" &&
|
|
451
|
+
for (const v of raw) if (typeof v === "string" && isSafeSessionId2(v)) ids.push(v);
|
|
81
452
|
return ids;
|
|
82
453
|
}
|
|
83
|
-
function
|
|
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 (!
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
231
|
-
const tmp =
|
|
232
|
-
await
|
|
233
|
-
await
|
|
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 =
|
|
420
|
-
if (!isAbsolute(p)) 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
|
|
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
|
|
467
|
-
await
|
|
883
|
+
await mkdir3(dirname(newPath), { recursive: true });
|
|
884
|
+
await rename3(oldPath, backupPath);
|
|
468
885
|
await rewriteFrame0Cwd(backupPath, canonical);
|
|
469
|
-
await
|
|
886
|
+
await rename3(backupPath, newPath);
|
|
470
887
|
} catch (e) {
|
|
471
888
|
try {
|
|
472
|
-
await
|
|
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
|
|
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
|
|
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
|
};
|