session-steward 0.1.1 → 0.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/CHANGELOG.md +57 -0
- package/README.md +158 -104
- package/bin/session-steward-cli.mjs +49 -7
- package/bin/session-steward.mjs +3 -0
- package/dist/assets/index-CN9iax_v.css +2 -0
- package/dist/assets/index-fUX3qen0.js +9 -0
- package/dist/index.html +14 -3
- package/lib/cli.mjs +464 -61
- package/lib/providers/claude-code/index.mjs +7 -0
- package/lib/providers/claude-code/store.mjs +951 -0
- package/lib/providers/codex/index.mjs +8 -0
- package/lib/providers/codex/store.mjs +477 -64
- package/lib/providers/index.mjs +2 -1
- package/lib/server.mjs +316 -59
- package/lib/settings.mjs +39 -0
- package/lib/storage/files.mjs +39 -0
- package/package.json +19 -10
- package/dist/assets/index-DGVNvKX8.js +0 -9
- package/dist/assets/index-DTDZWQb9.css +0 -2
- package/docs/session-steward-overview.jpg +0 -0
package/lib/cli.mjs
CHANGED
|
@@ -5,23 +5,25 @@ import process, { stdin as input, stdout as output } from "node:process";
|
|
|
5
5
|
|
|
6
6
|
import { getProvider } from "./providers/index.mjs";
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
formatSessionForJson,
|
|
12
|
-
getSessionRecord,
|
|
13
|
-
listSessions,
|
|
14
|
-
loadDeletionStore,
|
|
15
|
-
planSessionDeletion,
|
|
16
|
-
verifySessionDeletion,
|
|
17
|
-
} = getProvider("codex");
|
|
8
|
+
function providerOptions(providerId, home) {
|
|
9
|
+
return providerId === "codex" ? { codexHome: home } : { claudeHome: home };
|
|
10
|
+
}
|
|
18
11
|
|
|
19
12
|
const PAGE_SIZE = 20;
|
|
13
|
+
const ALLOWED_INACTIVE_DAYS = new Set([30, 60, 90]);
|
|
14
|
+
const ALLOWED_ARCHIVE_STATUSES = new Set(["all", "active", "archived"]);
|
|
15
|
+
const ALLOWED_CLEANUP_MODES = new Set(["standard", "thorough"]);
|
|
20
16
|
const HELP_TEXT = `
|
|
21
17
|
Commands
|
|
22
18
|
search <text> Set the active search filter
|
|
23
19
|
search Clear the active search filter
|
|
24
|
-
|
|
20
|
+
workspace <path> Show one exact workspace
|
|
21
|
+
workspace Clear the workspace filter
|
|
22
|
+
inactive <30|60|90> Show sessions last active that many days ago
|
|
23
|
+
inactive Clear the inactivity filter
|
|
24
|
+
archive <all|active|archived> Filter sessions by archive status
|
|
25
|
+
archive Clear the archive filter
|
|
26
|
+
sort <updated|created|name|cwd|size>
|
|
25
27
|
Change sort order
|
|
26
28
|
inspect <index|id-prefix> Show session details
|
|
27
29
|
delete <selector> [...] Delete one or more sessions
|
|
@@ -29,6 +31,12 @@ Commands
|
|
|
29
31
|
next Next page
|
|
30
32
|
prev Previous page
|
|
31
33
|
internals Toggle subagent visibility
|
|
34
|
+
supporting Toggle supporting-session visibility
|
|
35
|
+
cleanup <standard|thorough> Choose the cleanup level
|
|
36
|
+
overview Show storage and workspace totals
|
|
37
|
+
backups Show retained recovery backups
|
|
38
|
+
restore <index|backup-id> Restore a recovery backup
|
|
39
|
+
delete-backup <index|id> Permanently remove a recovery backup
|
|
32
40
|
refresh Reload sessions from sqlite and disk
|
|
33
41
|
help Show this help
|
|
34
42
|
quit Exit
|
|
@@ -40,6 +48,29 @@ Selectors
|
|
|
40
48
|
delete 1 4-6 019dd26c
|
|
41
49
|
`.trim();
|
|
42
50
|
|
|
51
|
+
const CLI_HELP_TEXT = `
|
|
52
|
+
Usage: session-steward-cli [options]
|
|
53
|
+
|
|
54
|
+
Options
|
|
55
|
+
--provider <codex|claude-code> Choose the session provider
|
|
56
|
+
--codex-home <path> Use another Codex session folder for this run
|
|
57
|
+
--claude-home <path> Use another Claude session folder for this run
|
|
58
|
+
--json Print sessions as JSON
|
|
59
|
+
--include-internals Include subagent sessions
|
|
60
|
+
--include-supporting Include supporting sessions
|
|
61
|
+
--cleanup <standard|thorough> Choose the interactive cleanup level
|
|
62
|
+
--overview Print storage and workspace totals
|
|
63
|
+
--backups Print retained recovery backups
|
|
64
|
+
--search <text> Search names, workspaces, and session IDs
|
|
65
|
+
--workspace <path> Show one exact workspace
|
|
66
|
+
--inactive-days <30|60|90> Show sessions last active at least this long ago
|
|
67
|
+
--archive-status <status> Show all, active, or archived sessions
|
|
68
|
+
--sort <updated|created|name|cwd|size>
|
|
69
|
+
Choose the session order
|
|
70
|
+
--limit <number> Limit JSON results
|
|
71
|
+
-h, --help Show this help
|
|
72
|
+
`.trim();
|
|
73
|
+
|
|
43
74
|
function truncate(value, maxLength) {
|
|
44
75
|
if (value.length <= maxLength) {
|
|
45
76
|
return value;
|
|
@@ -101,6 +132,31 @@ function absoluteTime(timestampMs) {
|
|
|
101
132
|
return new Date(timestampMs).toLocaleString();
|
|
102
133
|
}
|
|
103
134
|
|
|
135
|
+
function formatBytes(bytes) {
|
|
136
|
+
if (!Number.isFinite(bytes) || bytes < 0) return "-";
|
|
137
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
138
|
+
|
|
139
|
+
const units = ["KB", "MB", "GB", "TB"];
|
|
140
|
+
let value = bytes;
|
|
141
|
+
let unit = "B";
|
|
142
|
+
|
|
143
|
+
for (const nextUnit of units) {
|
|
144
|
+
value /= 1024;
|
|
145
|
+
unit = nextUnit;
|
|
146
|
+
if (value < 1024) break;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return `${value.toFixed(value < 10 ? 1 : 0)} ${unit}`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function cleanupScope(cleanupMode) {
|
|
153
|
+
return cleanupMode === "thorough" ? "deep" : "core";
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function cleanupLabel(scope) {
|
|
157
|
+
return scope === "deep" ? "Thorough" : scope === "core" ? "Standard" : "Unknown";
|
|
158
|
+
}
|
|
159
|
+
|
|
104
160
|
function getCwdDisplay(record) {
|
|
105
161
|
if (!record.cwd) {
|
|
106
162
|
return "-";
|
|
@@ -138,26 +194,32 @@ function getMarkerText(record) {
|
|
|
138
194
|
return markers.join(", ");
|
|
139
195
|
}
|
|
140
196
|
|
|
141
|
-
function printScreen({
|
|
197
|
+
function printScreen({ archiveStatus, cleanupMode, inactiveDays, providerHome, providerName, result, search, showInternals, showSupporting, sort, workspace }) {
|
|
142
198
|
if (output.isTTY) {
|
|
143
199
|
output.write("\x1Bc");
|
|
144
200
|
}
|
|
145
201
|
|
|
146
202
|
output.write("Session Steward\n");
|
|
147
203
|
output.write(
|
|
148
|
-
|
|
204
|
+
`${providerName} home: ${providerHome} | Sessions: ${result.total} | Page: ${result.page}/${result.pageCount}\n`,
|
|
205
|
+
);
|
|
206
|
+
output.write(
|
|
207
|
+
`Sort: ${sort} | Search: ${search || "-"} | Cleanup: ${cleanupLabel(cleanupScope(cleanupMode))}\n`,
|
|
149
208
|
);
|
|
150
209
|
output.write(
|
|
151
|
-
`
|
|
210
|
+
`Subagents: ${showInternals ? "shown" : "hidden"} | Supporting: ${showSupporting ? "shown" : "hidden"}\n`,
|
|
152
211
|
);
|
|
153
212
|
output.write(
|
|
154
|
-
|
|
213
|
+
`Last active: ${inactiveDays ? `${inactiveDays}+ days ago` : "any time"} | Workspace: ${workspace || "all"} | Status: ${archiveStatus}\n`,
|
|
214
|
+
);
|
|
215
|
+
output.write(
|
|
216
|
+
"Commands: search | workspace | inactive | archive | sort | inspect | delete | overview | backups | help | quit\n\n",
|
|
155
217
|
);
|
|
156
218
|
|
|
157
219
|
output.write(
|
|
158
|
-
`${pad("#", 4)} ${pad("Name",
|
|
220
|
+
`${pad("#", 4)} ${pad("Name", 52)} ${pad("Updated", 8)} ${pad("Size", 10)} ${pad("Cwd", 18)} Markers\n`,
|
|
159
221
|
);
|
|
160
|
-
output.write(`${"-".repeat(4)} ${"-".repeat(
|
|
222
|
+
output.write(`${"-".repeat(4)} ${"-".repeat(52)} ${"-".repeat(8)} ${"-".repeat(10)} ${"-".repeat(18)} ${"-".repeat(20)}\n`);
|
|
161
223
|
|
|
162
224
|
if (result.records.length === 0) {
|
|
163
225
|
output.write("No sessions match the current view.\n");
|
|
@@ -167,7 +229,7 @@ function printScreen({ codexHome, result, search, showInternals, sort }) {
|
|
|
167
229
|
result.records.forEach((record, index) => {
|
|
168
230
|
const rowNumber = index + 1;
|
|
169
231
|
output.write(
|
|
170
|
-
`${pad(String(rowNumber), 4)} ${pad(truncate(record.displayName,
|
|
232
|
+
`${pad(String(rowNumber), 4)} ${pad(truncate(record.displayName, 52), 52)} ${pad(relativeTime(record.updatedAtMs), 8)} ${pad(formatBytes(record.transcriptBytes), 10)} ${pad(truncate(getCwdDisplay(record), 18), 18)} ${truncate(getMarkerText(record), 20)}\n`,
|
|
171
233
|
);
|
|
172
234
|
});
|
|
173
235
|
}
|
|
@@ -202,12 +264,10 @@ function parseSelectors(selectors, records) {
|
|
|
202
264
|
const rowIndex = Number.parseInt(selector, 10) - 1;
|
|
203
265
|
const record = records[rowIndex];
|
|
204
266
|
|
|
205
|
-
if (
|
|
206
|
-
|
|
267
|
+
if (record) {
|
|
268
|
+
resolvedIds.add(record.id);
|
|
269
|
+
continue;
|
|
207
270
|
}
|
|
208
|
-
|
|
209
|
-
resolvedIds.add(record.id);
|
|
210
|
-
continue;
|
|
211
271
|
}
|
|
212
272
|
|
|
213
273
|
if (/^\d+-\d+$/u.test(selector)) {
|
|
@@ -215,21 +275,19 @@ function parseSelectors(selectors, records) {
|
|
|
215
275
|
const start = Number.parseInt(startText, 10);
|
|
216
276
|
const end = Number.parseInt(endText, 10);
|
|
217
277
|
|
|
218
|
-
if (
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
for (let index = start; index <= end; index += 1) {
|
|
223
|
-
const record = records[index - 1];
|
|
278
|
+
if (startText.length <= 4 && endText.length <= 4) {
|
|
279
|
+
if (start > end) {
|
|
280
|
+
throw new Error(`Invalid range: ${selector}.`);
|
|
281
|
+
}
|
|
224
282
|
|
|
225
|
-
|
|
226
|
-
|
|
283
|
+
for (let index = start; index <= end; index += 1) {
|
|
284
|
+
const record = records[index - 1];
|
|
285
|
+
if (!record) throw new Error(`No session exists at row ${index}.`);
|
|
286
|
+
resolvedIds.add(record.id);
|
|
227
287
|
}
|
|
228
288
|
|
|
229
|
-
|
|
289
|
+
continue;
|
|
230
290
|
}
|
|
231
|
-
|
|
232
|
-
continue;
|
|
233
291
|
}
|
|
234
292
|
|
|
235
293
|
const matches = records.filter((record) => record.id.startsWith(selector));
|
|
@@ -257,6 +315,7 @@ function printInspect(record, deletionPlan) {
|
|
|
257
315
|
output.write(`Created: ${absoluteTime(record.createdAtMs)}\n`);
|
|
258
316
|
output.write(`Cwd: ${record.cwd || "-"}\n`);
|
|
259
317
|
output.write(`Transcript: ${record.rolloutPath || "-"}\n`);
|
|
318
|
+
output.write(`Transcript size: ${formatBytes(record.transcriptBytes)}\n`);
|
|
260
319
|
output.write(`Title source: ${record.titleSource}\n`);
|
|
261
320
|
output.write(`Parent: ${record.parentThreadId || "-"}\n`);
|
|
262
321
|
output.write(`Children: ${record.childThreadIds.length}\n`);
|
|
@@ -272,7 +331,9 @@ function printInspect(record, deletionPlan) {
|
|
|
272
331
|
output.write(`Delete log rows: ${deletionPlan.logRowCount}\n`);
|
|
273
332
|
}
|
|
274
333
|
|
|
275
|
-
function printDeletionPreview(plan) {
|
|
334
|
+
function printDeletionPreview(plan, preflight, scope) {
|
|
335
|
+
const fileCount = preflight.transcriptFileCount ?? plan.transcriptFileCount;
|
|
336
|
+
const sessionBytes = preflight.transcriptBytes ?? plan.transcriptBytes;
|
|
276
337
|
output.write("\nDelete preview\n");
|
|
277
338
|
output.write("--------------\n");
|
|
278
339
|
output.write(`Sessions: ${plan.ids.length}\n`);
|
|
@@ -282,6 +343,10 @@ function printDeletionPreview(plan) {
|
|
|
282
343
|
output.write(`History rows: ${plan.historyMatchCount}\n`);
|
|
283
344
|
output.write(`Spawn edges: ${plan.spawnEdgeCount}\n`);
|
|
284
345
|
output.write(`Log rows: ${plan.logRowCount}\n`);
|
|
346
|
+
output.write(`Cleanup: ${cleanupLabel(scope)}\n`);
|
|
347
|
+
output.write(`Files: ${fileCount}\n`);
|
|
348
|
+
output.write(`Session data: ${formatBytes(sessionBytes)}\n`);
|
|
349
|
+
output.write(`Temporary backup space needed: ${formatBytes(preflight.estimatedBackupBytes)}\n`);
|
|
285
350
|
|
|
286
351
|
for (const record of plan.records.slice(0, 20)) {
|
|
287
352
|
output.write(`- ${record.displayName} (${record.id})\n`);
|
|
@@ -301,20 +366,146 @@ function printHelp() {
|
|
|
301
366
|
}
|
|
302
367
|
|
|
303
368
|
function validateSort(value) {
|
|
304
|
-
return ["updated", "created", "name", "cwd"].includes(value)
|
|
369
|
+
return ["updated", "created", "name", "cwd", "size"].includes(value)
|
|
305
370
|
? value
|
|
306
371
|
: "updated";
|
|
307
372
|
}
|
|
308
373
|
|
|
374
|
+
function validateInactiveDays(value) {
|
|
375
|
+
if (value === undefined || value === null || value === "") {
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const days = Number(value);
|
|
380
|
+
|
|
381
|
+
if (!ALLOWED_INACTIVE_DAYS.has(days)) {
|
|
382
|
+
throw new Error("Inactive days must be 30, 60, or 90.");
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return days;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function validateArchiveStatus(value) {
|
|
389
|
+
const status = value || "all";
|
|
390
|
+
|
|
391
|
+
if (!ALLOWED_ARCHIVE_STATUSES.has(status)) {
|
|
392
|
+
throw new Error("Archive status must be all, active, or archived.");
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return status;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function validateCleanupMode(value) {
|
|
399
|
+
const cleanupMode = value || "standard";
|
|
400
|
+
|
|
401
|
+
if (!ALLOWED_CLEANUP_MODES.has(cleanupMode)) {
|
|
402
|
+
throw new Error("Cleanup must be standard or thorough.");
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return cleanupMode;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async function writeJsonValue(value) {
|
|
409
|
+
const content = `${JSON.stringify(value, null, 2)}\n`;
|
|
410
|
+
if (!output.write(content)) await once(output, "drain");
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async function loadOverview(state) {
|
|
414
|
+
return state.provider.getSessionOverview({
|
|
415
|
+
...providerOptions(state.provider.id, state.providerHome),
|
|
416
|
+
refresh: true,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function printOverview(overview, providerName) {
|
|
421
|
+
output.write(`\n${providerName} overview\n`);
|
|
422
|
+
output.write(`${"-".repeat(providerName.length + 9)}\n`);
|
|
423
|
+
output.write(`On disk: ${formatBytes(overview.transcriptBytes)} in ${overview.transcriptFileCount} files\n`);
|
|
424
|
+
output.write(`All sessions: ${overview.sessionCount}\n`);
|
|
425
|
+
output.write(`Primary sessions: ${overview.primarySessionCount}\n`);
|
|
426
|
+
output.write(`Subagent sessions: ${overview.subagentCount}\n`);
|
|
427
|
+
output.write(`Supporting sessions: ${overview.supportingCount}\n`);
|
|
428
|
+
|
|
429
|
+
if (Number.isFinite(overview.cliSessionCount)) {
|
|
430
|
+
output.write(`CLI sessions: ${overview.cliSessionCount}\n`);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (Number.isFinite(overview.desktopSessionCount)) {
|
|
434
|
+
output.write(`Desktop sessions: ${overview.desktopSessionCount}\n`);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (overview.workspaces.length === 0) return;
|
|
438
|
+
output.write("\nWorkspaces\n");
|
|
439
|
+
output.write(`${pad("Sessions", 10)} ${pad("Size", 10)} Workspace\n`);
|
|
440
|
+
|
|
441
|
+
for (const workspace of overview.workspaces) {
|
|
442
|
+
output.write(
|
|
443
|
+
`${pad(String(workspace.sessionCount), 10)} ${pad(formatBytes(workspace.transcriptBytes), 10)} ${workspace.path || "Unknown workspace"}\n`,
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
async function loadBackups(state) {
|
|
449
|
+
return state.provider.listSessionDeletionBackups(
|
|
450
|
+
providerOptions(state.provider.id, state.providerHome),
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function printBackups(backups) {
|
|
455
|
+
output.write("\nRecovery backups\n");
|
|
456
|
+
output.write("----------------\n");
|
|
457
|
+
|
|
458
|
+
if (backups.length === 0) {
|
|
459
|
+
output.write("No recovery backups are stored.\n");
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
output.write(`${pad("#", 4)} ${pad("Created", 22)} ${pad("Cleanup", 10)} ${pad("Size", 10)} ${pad("Files", 7)} Status\n`);
|
|
464
|
+
|
|
465
|
+
backups.forEach((backup, index) => {
|
|
466
|
+
output.write(
|
|
467
|
+
`${pad(String(index + 1), 4)} ${pad(truncate(absoluteTime(backup.createdAtMs), 22), 22)} ${pad(cleanupLabel(backup.scope), 10)} ${pad(formatBytes(backup.bytes), 10)} ${pad(String(backup.fileCount), 7)} ${backup.restorable ? "Ready to restore" : "Manual recovery only"}\n`,
|
|
468
|
+
);
|
|
469
|
+
output.write(` ${backup.id}\n`);
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function resolveBackupSelector(selector, backups) {
|
|
474
|
+
if (!selector) throw new Error("Choose a recovery backup by number or backup id.");
|
|
475
|
+
|
|
476
|
+
if (/^\d+$/u.test(selector)) {
|
|
477
|
+
const backup = backups[Number.parseInt(selector, 10) - 1];
|
|
478
|
+
if (!backup) throw new Error(`No recovery backup exists at row ${selector}.`);
|
|
479
|
+
return backup;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const exactMatch = backups.find((backup) => backup.id === selector);
|
|
483
|
+
if (exactMatch) return exactMatch;
|
|
484
|
+
const prefixMatches = backups.filter((backup) => backup.id.startsWith(selector));
|
|
485
|
+
if (prefixMatches.length === 0) throw new Error(`No recovery backup id starts with "${selector}".`);
|
|
486
|
+
if (prefixMatches.length > 1) throw new Error(`Recovery backup id prefix "${selector}" is ambiguous.`);
|
|
487
|
+
return prefixMatches[0];
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function getInactiveBeforeMs(days) {
|
|
491
|
+
return days === null ? null : Date.now() - days * 24 * 60 * 60 * 1000;
|
|
492
|
+
}
|
|
493
|
+
|
|
309
494
|
async function refreshPage(state) {
|
|
310
|
-
state.result = await listSessions({
|
|
311
|
-
|
|
495
|
+
state.result = await state.provider.listSessions({
|
|
496
|
+
archiveStatus: state.archiveStatus,
|
|
497
|
+
...providerOptions(state.provider.id, state.providerHome),
|
|
498
|
+
inactiveBeforeMs: getInactiveBeforeMs(state.inactiveDays),
|
|
312
499
|
includeInternals: state.showInternals,
|
|
500
|
+
includeSupporting: state.showSupporting,
|
|
313
501
|
page: state.page,
|
|
314
502
|
pageSize: PAGE_SIZE,
|
|
503
|
+
refresh: state.forceRefresh,
|
|
315
504
|
search: state.search,
|
|
316
505
|
sort: state.sort,
|
|
506
|
+
workspace: state.workspace,
|
|
317
507
|
});
|
|
508
|
+
state.forceRefresh = false;
|
|
318
509
|
state.page = state.result.page;
|
|
319
510
|
}
|
|
320
511
|
|
|
@@ -323,24 +514,30 @@ async function printJson(options) {
|
|
|
323
514
|
if (!output.write(chunk)) await once(output, "drain");
|
|
324
515
|
};
|
|
325
516
|
const limit = Number.isFinite(options.limit) && options.limit > 0 ? options.limit : Infinity;
|
|
517
|
+
const inactiveBeforeMs = getInactiveBeforeMs(validateInactiveDays(options.inactiveDays));
|
|
518
|
+
const archiveStatus = validateArchiveStatus(options.archiveStatus);
|
|
326
519
|
let page = 1;
|
|
327
520
|
let written = 0;
|
|
328
521
|
let first = true;
|
|
329
522
|
await writeChunk("[\n");
|
|
330
523
|
|
|
331
524
|
while (written < limit) {
|
|
332
|
-
const result = await listSessions({
|
|
333
|
-
|
|
525
|
+
const result = await options.provider.listSessions({
|
|
526
|
+
archiveStatus,
|
|
527
|
+
...providerOptions(options.provider.id, options.providerHome),
|
|
528
|
+
inactiveBeforeMs,
|
|
334
529
|
includeInternals: Boolean(options.includeInternals),
|
|
530
|
+
includeSupporting: Boolean(options.includeSupporting),
|
|
335
531
|
page,
|
|
336
532
|
pageSize: Math.min(100, limit - written),
|
|
337
533
|
search: options.search || "",
|
|
338
534
|
sort: validateSort(options.sort || "updated"),
|
|
535
|
+
workspace: options.workspace,
|
|
339
536
|
});
|
|
340
537
|
|
|
341
538
|
for (const record of result.records) {
|
|
342
539
|
if (written >= limit) break;
|
|
343
|
-
await writeChunk(`${first ? "" : ",\n"}${JSON.stringify(formatSessionForJson(record), null, 2)}`);
|
|
540
|
+
await writeChunk(`${first ? "" : ",\n"}${JSON.stringify(options.provider.formatSessionForJson(record), null, 2)}`);
|
|
344
541
|
first = false;
|
|
345
542
|
written += 1;
|
|
346
543
|
}
|
|
@@ -362,11 +559,17 @@ async function runInteractive(state) {
|
|
|
362
559
|
while (true) {
|
|
363
560
|
await refreshPage(state);
|
|
364
561
|
printScreen({
|
|
365
|
-
|
|
562
|
+
archiveStatus: state.archiveStatus,
|
|
563
|
+
cleanupMode: state.cleanupMode,
|
|
564
|
+
inactiveDays: state.inactiveDays,
|
|
565
|
+
providerHome: state.providerHome,
|
|
566
|
+
providerName: state.provider.displayName,
|
|
366
567
|
result: state.result,
|
|
367
568
|
search: state.search,
|
|
368
569
|
showInternals: state.showInternals,
|
|
570
|
+
showSupporting: state.showSupporting,
|
|
369
571
|
sort: state.sort,
|
|
572
|
+
workspace: state.workspace,
|
|
370
573
|
});
|
|
371
574
|
|
|
372
575
|
const commandInput = await rl.question("\nsession-steward> ");
|
|
@@ -387,6 +590,7 @@ async function runInteractive(state) {
|
|
|
387
590
|
}
|
|
388
591
|
|
|
389
592
|
if (["refresh", "r"].includes(command.name)) {
|
|
593
|
+
state.forceRefresh = true;
|
|
390
594
|
state.page = 1;
|
|
391
595
|
continue;
|
|
392
596
|
}
|
|
@@ -397,12 +601,161 @@ async function runInteractive(state) {
|
|
|
397
601
|
continue;
|
|
398
602
|
}
|
|
399
603
|
|
|
604
|
+
if (["supporting", "toggle-supporting"].includes(command.name)) {
|
|
605
|
+
state.showSupporting = !state.showSupporting;
|
|
606
|
+
state.page = 1;
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (command.name === "cleanup") {
|
|
611
|
+
try {
|
|
612
|
+
state.cleanupMode = validateCleanupMode(command.args[0]);
|
|
613
|
+
} catch (error) {
|
|
614
|
+
output.write(`\n${error instanceof Error ? error.message : String(error)}\n`);
|
|
615
|
+
await pause(rl);
|
|
616
|
+
}
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (command.name === "overview") {
|
|
621
|
+
try {
|
|
622
|
+
printOverview(await loadOverview(state), state.provider.displayName);
|
|
623
|
+
} catch (error) {
|
|
624
|
+
output.write(`\n${error instanceof Error ? error.message : String(error)}\n`);
|
|
625
|
+
}
|
|
626
|
+
await pause(rl);
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
if (command.name === "backups") {
|
|
631
|
+
try {
|
|
632
|
+
printBackups(await loadBackups(state));
|
|
633
|
+
} catch (error) {
|
|
634
|
+
output.write(`\n${error instanceof Error ? error.message : String(error)}\n`);
|
|
635
|
+
}
|
|
636
|
+
await pause(rl);
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if (command.name === "restore") {
|
|
641
|
+
try {
|
|
642
|
+
const backups = await loadBackups(state);
|
|
643
|
+
const backup = resolveBackupSelector(command.args[0], backups);
|
|
644
|
+
|
|
645
|
+
if (!backup.restorable) {
|
|
646
|
+
throw new Error("This backup cannot be restored automatically. Its files are still available for manual recovery.");
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
output.write(`\nRestore backup from ${absoluteTime(backup.createdAtMs)} (${formatBytes(backup.bytes)}).\n`);
|
|
650
|
+
output.write("Current files are saved before the restore begins.\n");
|
|
651
|
+
const response = await rl.question('\nType "RESTORE" to confirm: ');
|
|
652
|
+
|
|
653
|
+
if (response.trim() !== "RESTORE") {
|
|
654
|
+
output.write("Restore cancelled.\n");
|
|
655
|
+
await pause(rl);
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const restoreResult = await state.provider.restoreSessionDeletionBackup({
|
|
660
|
+
backupDirectory: backup.backupDirectory,
|
|
661
|
+
...providerOptions(state.provider.id, state.providerHome),
|
|
662
|
+
onProgress: ({ message }) => {
|
|
663
|
+
if (message) output.write(`${message}...\n`);
|
|
664
|
+
},
|
|
665
|
+
});
|
|
666
|
+
const cleanupDirectories = [
|
|
667
|
+
restoreResult.safetyBackupDirectory,
|
|
668
|
+
backup.backupDirectory,
|
|
669
|
+
].filter(Boolean);
|
|
670
|
+
const retainedDirectories = [];
|
|
671
|
+
|
|
672
|
+
for (const backupDirectory of cleanupDirectories) {
|
|
673
|
+
try {
|
|
674
|
+
await state.provider.deleteSessionDeletionBackup({
|
|
675
|
+
backupDirectory,
|
|
676
|
+
...providerOptions(state.provider.id, state.providerHome),
|
|
677
|
+
});
|
|
678
|
+
} catch {
|
|
679
|
+
retainedDirectories.push(backupDirectory);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
state.provider.invalidateSessionCache?.(providerOptions(state.provider.id, state.providerHome));
|
|
684
|
+
const restoredCount = restoreResult.restoredFileCount ?? restoreResult.restoredEntryCount ?? 0;
|
|
685
|
+
output.write(`Restored and verified ${restoredCount} session data files.\n`);
|
|
686
|
+
if (retainedDirectories.length > 0) {
|
|
687
|
+
output.write(`Restore completed, but recovery files remain at ${retainedDirectories.join(", ")}.\n`);
|
|
688
|
+
}
|
|
689
|
+
state.page = 1;
|
|
690
|
+
state.forceRefresh = true;
|
|
691
|
+
} catch (error) {
|
|
692
|
+
output.write(`\n${error instanceof Error ? error.message : String(error)}\n`);
|
|
693
|
+
}
|
|
694
|
+
await pause(rl);
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (command.name === "delete-backup") {
|
|
699
|
+
try {
|
|
700
|
+
const backups = await loadBackups(state);
|
|
701
|
+
const backup = resolveBackupSelector(command.args[0], backups);
|
|
702
|
+
output.write(`\nDelete recovery backup from ${absoluteTime(backup.createdAtMs)} (${formatBytes(backup.bytes)}).\n`);
|
|
703
|
+
output.write("You will no longer be able to restore from it.\n");
|
|
704
|
+
const response = await rl.question('\nType "DELETE BACKUP" to confirm: ');
|
|
705
|
+
|
|
706
|
+
if (response.trim() !== "DELETE BACKUP") {
|
|
707
|
+
output.write("Backup deletion cancelled.\n");
|
|
708
|
+
await pause(rl);
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
await state.provider.deleteSessionDeletionBackup({
|
|
713
|
+
backupDirectory: backup.backupDirectory,
|
|
714
|
+
...providerOptions(state.provider.id, state.providerHome),
|
|
715
|
+
});
|
|
716
|
+
output.write(`Deleted recovery backup ${backup.id}.\n`);
|
|
717
|
+
} catch (error) {
|
|
718
|
+
output.write(`\n${error instanceof Error ? error.message : String(error)}\n`);
|
|
719
|
+
}
|
|
720
|
+
await pause(rl);
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
|
|
400
724
|
if (["search", "s"].includes(command.name)) {
|
|
401
725
|
state.search = command.args.join(" ");
|
|
402
726
|
state.page = 1;
|
|
403
727
|
continue;
|
|
404
728
|
}
|
|
405
729
|
|
|
730
|
+
if (["workspace", "cwd"].includes(command.name)) {
|
|
731
|
+
state.workspace = command.args.length > 0 ? command.args.join(" ") : undefined;
|
|
732
|
+
state.page = 1;
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
if (["inactive", "inactive-days"].includes(command.name)) {
|
|
737
|
+
try {
|
|
738
|
+
state.inactiveDays = validateInactiveDays(command.args[0]);
|
|
739
|
+
state.page = 1;
|
|
740
|
+
state.forceRefresh = true;
|
|
741
|
+
} catch (error) {
|
|
742
|
+
output.write(`\n${error instanceof Error ? error.message : String(error)}\n`);
|
|
743
|
+
await pause(rl);
|
|
744
|
+
}
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
if (["archive", "archive-status"].includes(command.name)) {
|
|
749
|
+
try {
|
|
750
|
+
state.archiveStatus = validateArchiveStatus(command.args[0]);
|
|
751
|
+
state.page = 1;
|
|
752
|
+
} catch (error) {
|
|
753
|
+
output.write(`\n${error instanceof Error ? error.message : String(error)}\n`);
|
|
754
|
+
await pause(rl);
|
|
755
|
+
}
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
758
|
+
|
|
406
759
|
if (command.name === "sort") {
|
|
407
760
|
state.sort = validateSort(command.args[0] ?? "updated");
|
|
408
761
|
state.page = 1;
|
|
@@ -433,12 +786,12 @@ async function runInteractive(state) {
|
|
|
433
786
|
throw new Error("`inspect` accepts exactly one selector.");
|
|
434
787
|
}
|
|
435
788
|
|
|
436
|
-
const record = await getSessionRecord({
|
|
437
|
-
const deletionStore = await loadDeletionStore({
|
|
438
|
-
|
|
789
|
+
const record = await state.provider.getSessionRecord({ ...providerOptions(state.provider.id, state.providerHome), id: sessionIds[0] });
|
|
790
|
+
const deletionStore = await state.provider.loadDeletionStore({
|
|
791
|
+
...providerOptions(state.provider.id, state.providerHome),
|
|
439
792
|
recordIds: sessionIds,
|
|
440
793
|
});
|
|
441
|
-
const deletionPlan = await planSessionDeletion({
|
|
794
|
+
const deletionPlan = await state.provider.planSessionDeletion({
|
|
442
795
|
recordIds: sessionIds,
|
|
443
796
|
store: deletionStore,
|
|
444
797
|
});
|
|
@@ -455,19 +808,30 @@ async function runInteractive(state) {
|
|
|
455
808
|
if (command.name === "delete") {
|
|
456
809
|
try {
|
|
457
810
|
const sessionIds = parseSelectors(command.args, state.result.records);
|
|
458
|
-
|
|
459
|
-
const
|
|
460
|
-
|
|
811
|
+
if (sessionIds.length === 0) throw new Error("Choose at least one session to delete.");
|
|
812
|
+
const scope = cleanupScope(state.cleanupMode);
|
|
813
|
+
if (scope === "deep") {
|
|
814
|
+
await state.provider.assertDeepCleanupSupported(
|
|
815
|
+
providerOptions(state.provider.id, state.providerHome),
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
const deletionStore = await state.provider.loadDeletionStore({
|
|
819
|
+
...providerOptions(state.provider.id, state.providerHome),
|
|
461
820
|
recordIds: sessionIds,
|
|
462
821
|
});
|
|
463
|
-
const deletionPlan = await planSessionDeletion({
|
|
822
|
+
const deletionPlan = await state.provider.planSessionDeletion({
|
|
464
823
|
recordIds: sessionIds,
|
|
465
824
|
store: deletionStore,
|
|
466
825
|
});
|
|
826
|
+
const preflight = await state.provider.preflightSessionDeletion({
|
|
827
|
+
plan: deletionPlan,
|
|
828
|
+
scope,
|
|
829
|
+
store: deletionStore,
|
|
830
|
+
});
|
|
467
831
|
|
|
468
|
-
printDeletionPreview(deletionPlan);
|
|
832
|
+
printDeletionPreview(deletionPlan, preflight, scope);
|
|
469
833
|
output.write(
|
|
470
|
-
|
|
834
|
+
`\nClose the selected ${state.provider.displayName} sessions before continuing.\n`,
|
|
471
835
|
);
|
|
472
836
|
|
|
473
837
|
const confirmationToken =
|
|
@@ -499,7 +863,7 @@ async function runInteractive(state) {
|
|
|
499
863
|
let result;
|
|
500
864
|
|
|
501
865
|
try {
|
|
502
|
-
result = await executeSessionDeletion({
|
|
866
|
+
result = await state.provider.executeSessionDeletion({
|
|
503
867
|
onProgress: ({ canCancel: nextCanCancel, message }) => {
|
|
504
868
|
canCancel = nextCanCancel;
|
|
505
869
|
if (message !== lastMessage) {
|
|
@@ -508,21 +872,30 @@ async function runInteractive(state) {
|
|
|
508
872
|
}
|
|
509
873
|
},
|
|
510
874
|
plan: deletionPlan,
|
|
875
|
+
scope,
|
|
511
876
|
shouldCancel: () => cancelRequested,
|
|
512
877
|
store: deletionStore,
|
|
513
878
|
});
|
|
514
879
|
} finally {
|
|
515
880
|
process.off("SIGINT", handleInterrupt);
|
|
516
881
|
}
|
|
517
|
-
const verification = await verifySessionDeletion({
|
|
882
|
+
const verification = await state.provider.verifySessionDeletion({
|
|
518
883
|
plan: deletionPlan,
|
|
519
|
-
scope
|
|
884
|
+
scope,
|
|
520
885
|
store: deletionStore,
|
|
521
886
|
});
|
|
522
887
|
|
|
523
888
|
if (verification.complete) {
|
|
889
|
+
try {
|
|
890
|
+
await state.provider.deleteSessionDeletionBackup({
|
|
891
|
+
backupDirectory: result.backupDirectory,
|
|
892
|
+
...providerOptions(state.provider.id, state.providerHome),
|
|
893
|
+
});
|
|
894
|
+
} catch {
|
|
895
|
+
output.write(`Cleanup completed, but its recovery backup remains at ${result.backupDirectory}.\n`);
|
|
896
|
+
}
|
|
524
897
|
output.write(
|
|
525
|
-
`Deleted and verified ${result.deletedIds.length} sessions and ${result.deletedTranscriptPaths.length}
|
|
898
|
+
`Deleted and verified ${result.deletedIds.length} sessions and ${result.deletedTranscriptPaths.length} session paths.\n`,
|
|
526
899
|
);
|
|
527
900
|
} else {
|
|
528
901
|
output.write(
|
|
@@ -536,7 +909,11 @@ async function runInteractive(state) {
|
|
|
536
909
|
);
|
|
537
910
|
}
|
|
538
911
|
|
|
912
|
+
state.provider.invalidateSessionCache?.(
|
|
913
|
+
providerOptions(state.provider.id, state.providerHome),
|
|
914
|
+
);
|
|
539
915
|
state.page = 1;
|
|
916
|
+
state.forceRefresh = true;
|
|
540
917
|
} catch (error) {
|
|
541
918
|
output.write(`\n${error instanceof Error ? error.message : String(error)}\n`);
|
|
542
919
|
await pause(rl);
|
|
@@ -555,24 +932,50 @@ async function runInteractive(state) {
|
|
|
555
932
|
|
|
556
933
|
export async function runCli(options) {
|
|
557
934
|
if (options.help) {
|
|
558
|
-
output.write(
|
|
559
|
-
|
|
560
|
-
|
|
935
|
+
output.write(`${CLI_HELP_TEXT}\n`);
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
const provider = options.provider ?? getProvider(options.providerId || "codex");
|
|
940
|
+
const providerHome = path.resolve(options.providerHome);
|
|
941
|
+
|
|
942
|
+
if (options.overview && options.backups) {
|
|
943
|
+
throw new Error("Choose either overview or backups.");
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
if (options.overview) {
|
|
947
|
+
const overview = await loadOverview({ provider, providerHome });
|
|
948
|
+
if (options.json) await writeJsonValue(overview);
|
|
949
|
+
else printOverview(overview, provider.displayName);
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
if (options.backups) {
|
|
954
|
+
const backups = await loadBackups({ provider, providerHome });
|
|
955
|
+
if (options.json) await writeJsonValue(backups);
|
|
956
|
+
else printBackups(backups);
|
|
561
957
|
return;
|
|
562
958
|
}
|
|
563
959
|
|
|
564
960
|
if (options.json) {
|
|
565
|
-
await printJson(options);
|
|
961
|
+
await printJson({ ...options, provider, providerHome });
|
|
566
962
|
return;
|
|
567
963
|
}
|
|
568
964
|
|
|
569
965
|
const state = {
|
|
570
|
-
|
|
966
|
+
archiveStatus: validateArchiveStatus(options.archiveStatus),
|
|
967
|
+
cleanupMode: validateCleanupMode(options.cleanup),
|
|
968
|
+
provider,
|
|
969
|
+
providerHome,
|
|
970
|
+
inactiveDays: validateInactiveDays(options.inactiveDays),
|
|
971
|
+
forceRefresh: false,
|
|
571
972
|
page: 1,
|
|
572
973
|
result: null,
|
|
573
974
|
search: options.search || "",
|
|
574
975
|
showInternals: Boolean(options.includeInternals),
|
|
976
|
+
showSupporting: Boolean(options.includeSupporting),
|
|
575
977
|
sort: validateSort(options.sort || "updated"),
|
|
978
|
+
workspace: options.workspace,
|
|
576
979
|
};
|
|
577
980
|
|
|
578
981
|
await runInteractive(state);
|