session-steward 0.2.0 → 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 +77 -21
- package/bin/session-steward-cli.mjs +37 -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 +366 -59
- package/lib/providers/claude-code/index.mjs +7 -0
- package/lib/providers/claude-code/store.mjs +951 -0
- package/lib/providers/codex/index.mjs +4 -0
- package/lib/providers/codex/store.mjs +256 -41
- package/lib/providers/index.mjs +2 -1
- package/lib/server.mjs +147 -62
- package/lib/settings.mjs +39 -0
- package/lib/storage/files.mjs +39 -0
- package/package.json +9 -2
- package/dist/assets/index-kZ4XDVk-.js +0 -9
- package/dist/assets/index-pzaccjP4.css +0 -2
package/lib/cli.mjs
CHANGED
|
@@ -5,20 +5,14 @@ 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;
|
|
20
13
|
const ALLOWED_INACTIVE_DAYS = new Set([30, 60, 90]);
|
|
21
14
|
const ALLOWED_ARCHIVE_STATUSES = new Set(["all", "active", "archived"]);
|
|
15
|
+
const ALLOWED_CLEANUP_MODES = new Set(["standard", "thorough"]);
|
|
22
16
|
const HELP_TEXT = `
|
|
23
17
|
Commands
|
|
24
18
|
search <text> Set the active search filter
|
|
@@ -29,7 +23,7 @@ Commands
|
|
|
29
23
|
inactive Clear the inactivity filter
|
|
30
24
|
archive <all|active|archived> Filter sessions by archive status
|
|
31
25
|
archive Clear the archive filter
|
|
32
|
-
sort <updated|created|name|cwd>
|
|
26
|
+
sort <updated|created|name|cwd|size>
|
|
33
27
|
Change sort order
|
|
34
28
|
inspect <index|id-prefix> Show session details
|
|
35
29
|
delete <selector> [...] Delete one or more sessions
|
|
@@ -37,6 +31,12 @@ Commands
|
|
|
37
31
|
next Next page
|
|
38
32
|
prev Previous page
|
|
39
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
|
|
40
40
|
refresh Reload sessions from sqlite and disk
|
|
41
41
|
help Show this help
|
|
42
42
|
quit Exit
|
|
@@ -52,14 +52,20 @@ const CLI_HELP_TEXT = `
|
|
|
52
52
|
Usage: session-steward-cli [options]
|
|
53
53
|
|
|
54
54
|
Options
|
|
55
|
+
--provider <codex|claude-code> Choose the session provider
|
|
55
56
|
--codex-home <path> Use another Codex session folder for this run
|
|
57
|
+
--claude-home <path> Use another Claude session folder for this run
|
|
56
58
|
--json Print sessions as JSON
|
|
57
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
|
|
58
64
|
--search <text> Search names, workspaces, and session IDs
|
|
59
65
|
--workspace <path> Show one exact workspace
|
|
60
66
|
--inactive-days <30|60|90> Show sessions last active at least this long ago
|
|
61
67
|
--archive-status <status> Show all, active, or archived sessions
|
|
62
|
-
--sort <updated|created|name|cwd>
|
|
68
|
+
--sort <updated|created|name|cwd|size>
|
|
63
69
|
Choose the session order
|
|
64
70
|
--limit <number> Limit JSON results
|
|
65
71
|
-h, --help Show this help
|
|
@@ -126,6 +132,31 @@ function absoluteTime(timestampMs) {
|
|
|
126
132
|
return new Date(timestampMs).toLocaleString();
|
|
127
133
|
}
|
|
128
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
|
+
|
|
129
160
|
function getCwdDisplay(record) {
|
|
130
161
|
if (!record.cwd) {
|
|
131
162
|
return "-";
|
|
@@ -163,29 +194,32 @@ function getMarkerText(record) {
|
|
|
163
194
|
return markers.join(", ");
|
|
164
195
|
}
|
|
165
196
|
|
|
166
|
-
function printScreen({ archiveStatus,
|
|
197
|
+
function printScreen({ archiveStatus, cleanupMode, inactiveDays, providerHome, providerName, result, search, showInternals, showSupporting, sort, workspace }) {
|
|
167
198
|
if (output.isTTY) {
|
|
168
199
|
output.write("\x1Bc");
|
|
169
200
|
}
|
|
170
201
|
|
|
171
202
|
output.write("Session Steward\n");
|
|
172
203
|
output.write(
|
|
173
|
-
|
|
204
|
+
`${providerName} home: ${providerHome} | Sessions: ${result.total} | Page: ${result.page}/${result.pageCount}\n`,
|
|
174
205
|
);
|
|
175
206
|
output.write(
|
|
176
|
-
`Sort: ${sort} | Search: ${search || "-"} |
|
|
207
|
+
`Sort: ${sort} | Search: ${search || "-"} | Cleanup: ${cleanupLabel(cleanupScope(cleanupMode))}\n`,
|
|
208
|
+
);
|
|
209
|
+
output.write(
|
|
210
|
+
`Subagents: ${showInternals ? "shown" : "hidden"} | Supporting: ${showSupporting ? "shown" : "hidden"}\n`,
|
|
177
211
|
);
|
|
178
212
|
output.write(
|
|
179
213
|
`Last active: ${inactiveDays ? `${inactiveDays}+ days ago` : "any time"} | Workspace: ${workspace || "all"} | Status: ${archiveStatus}\n`,
|
|
180
214
|
);
|
|
181
215
|
output.write(
|
|
182
|
-
"Commands: search | workspace | inactive | archive | sort | inspect | delete |
|
|
216
|
+
"Commands: search | workspace | inactive | archive | sort | inspect | delete | overview | backups | help | quit\n\n",
|
|
183
217
|
);
|
|
184
218
|
|
|
185
219
|
output.write(
|
|
186
|
-
`${pad("#", 4)} ${pad("Name",
|
|
220
|
+
`${pad("#", 4)} ${pad("Name", 52)} ${pad("Updated", 8)} ${pad("Size", 10)} ${pad("Cwd", 18)} Markers\n`,
|
|
187
221
|
);
|
|
188
|
-
output.write(`${"-".repeat(4)} ${"-".repeat(
|
|
222
|
+
output.write(`${"-".repeat(4)} ${"-".repeat(52)} ${"-".repeat(8)} ${"-".repeat(10)} ${"-".repeat(18)} ${"-".repeat(20)}\n`);
|
|
189
223
|
|
|
190
224
|
if (result.records.length === 0) {
|
|
191
225
|
output.write("No sessions match the current view.\n");
|
|
@@ -195,7 +229,7 @@ function printScreen({ archiveStatus, codexHome, inactiveDays, result, search, s
|
|
|
195
229
|
result.records.forEach((record, index) => {
|
|
196
230
|
const rowNumber = index + 1;
|
|
197
231
|
output.write(
|
|
198
|
-
`${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`,
|
|
199
233
|
);
|
|
200
234
|
});
|
|
201
235
|
}
|
|
@@ -230,12 +264,10 @@ function parseSelectors(selectors, records) {
|
|
|
230
264
|
const rowIndex = Number.parseInt(selector, 10) - 1;
|
|
231
265
|
const record = records[rowIndex];
|
|
232
266
|
|
|
233
|
-
if (
|
|
234
|
-
|
|
267
|
+
if (record) {
|
|
268
|
+
resolvedIds.add(record.id);
|
|
269
|
+
continue;
|
|
235
270
|
}
|
|
236
|
-
|
|
237
|
-
resolvedIds.add(record.id);
|
|
238
|
-
continue;
|
|
239
271
|
}
|
|
240
272
|
|
|
241
273
|
if (/^\d+-\d+$/u.test(selector)) {
|
|
@@ -243,21 +275,19 @@ function parseSelectors(selectors, records) {
|
|
|
243
275
|
const start = Number.parseInt(startText, 10);
|
|
244
276
|
const end = Number.parseInt(endText, 10);
|
|
245
277
|
|
|
246
|
-
if (
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
for (let index = start; index <= end; index += 1) {
|
|
251
|
-
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
|
+
}
|
|
252
282
|
|
|
253
|
-
|
|
254
|
-
|
|
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);
|
|
255
287
|
}
|
|
256
288
|
|
|
257
|
-
|
|
289
|
+
continue;
|
|
258
290
|
}
|
|
259
|
-
|
|
260
|
-
continue;
|
|
261
291
|
}
|
|
262
292
|
|
|
263
293
|
const matches = records.filter((record) => record.id.startsWith(selector));
|
|
@@ -285,6 +315,7 @@ function printInspect(record, deletionPlan) {
|
|
|
285
315
|
output.write(`Created: ${absoluteTime(record.createdAtMs)}\n`);
|
|
286
316
|
output.write(`Cwd: ${record.cwd || "-"}\n`);
|
|
287
317
|
output.write(`Transcript: ${record.rolloutPath || "-"}\n`);
|
|
318
|
+
output.write(`Transcript size: ${formatBytes(record.transcriptBytes)}\n`);
|
|
288
319
|
output.write(`Title source: ${record.titleSource}\n`);
|
|
289
320
|
output.write(`Parent: ${record.parentThreadId || "-"}\n`);
|
|
290
321
|
output.write(`Children: ${record.childThreadIds.length}\n`);
|
|
@@ -300,7 +331,9 @@ function printInspect(record, deletionPlan) {
|
|
|
300
331
|
output.write(`Delete log rows: ${deletionPlan.logRowCount}\n`);
|
|
301
332
|
}
|
|
302
333
|
|
|
303
|
-
function printDeletionPreview(plan) {
|
|
334
|
+
function printDeletionPreview(plan, preflight, scope) {
|
|
335
|
+
const fileCount = preflight.transcriptFileCount ?? plan.transcriptFileCount;
|
|
336
|
+
const sessionBytes = preflight.transcriptBytes ?? plan.transcriptBytes;
|
|
304
337
|
output.write("\nDelete preview\n");
|
|
305
338
|
output.write("--------------\n");
|
|
306
339
|
output.write(`Sessions: ${plan.ids.length}\n`);
|
|
@@ -310,6 +343,10 @@ function printDeletionPreview(plan) {
|
|
|
310
343
|
output.write(`History rows: ${plan.historyMatchCount}\n`);
|
|
311
344
|
output.write(`Spawn edges: ${plan.spawnEdgeCount}\n`);
|
|
312
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`);
|
|
313
350
|
|
|
314
351
|
for (const record of plan.records.slice(0, 20)) {
|
|
315
352
|
output.write(`- ${record.displayName} (${record.id})\n`);
|
|
@@ -329,7 +366,7 @@ function printHelp() {
|
|
|
329
366
|
}
|
|
330
367
|
|
|
331
368
|
function validateSort(value) {
|
|
332
|
-
return ["updated", "created", "name", "cwd"].includes(value)
|
|
369
|
+
return ["updated", "created", "name", "cwd", "size"].includes(value)
|
|
333
370
|
? value
|
|
334
371
|
: "updated";
|
|
335
372
|
}
|
|
@@ -358,22 +395,117 @@ function validateArchiveStatus(value) {
|
|
|
358
395
|
return status;
|
|
359
396
|
}
|
|
360
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
|
+
|
|
361
490
|
function getInactiveBeforeMs(days) {
|
|
362
491
|
return days === null ? null : Date.now() - days * 24 * 60 * 60 * 1000;
|
|
363
492
|
}
|
|
364
493
|
|
|
365
494
|
async function refreshPage(state) {
|
|
366
|
-
state.result = await listSessions({
|
|
495
|
+
state.result = await state.provider.listSessions({
|
|
367
496
|
archiveStatus: state.archiveStatus,
|
|
368
|
-
|
|
497
|
+
...providerOptions(state.provider.id, state.providerHome),
|
|
369
498
|
inactiveBeforeMs: getInactiveBeforeMs(state.inactiveDays),
|
|
370
499
|
includeInternals: state.showInternals,
|
|
500
|
+
includeSupporting: state.showSupporting,
|
|
371
501
|
page: state.page,
|
|
372
502
|
pageSize: PAGE_SIZE,
|
|
503
|
+
refresh: state.forceRefresh,
|
|
373
504
|
search: state.search,
|
|
374
505
|
sort: state.sort,
|
|
375
506
|
workspace: state.workspace,
|
|
376
507
|
});
|
|
508
|
+
state.forceRefresh = false;
|
|
377
509
|
state.page = state.result.page;
|
|
378
510
|
}
|
|
379
511
|
|
|
@@ -390,11 +522,12 @@ async function printJson(options) {
|
|
|
390
522
|
await writeChunk("[\n");
|
|
391
523
|
|
|
392
524
|
while (written < limit) {
|
|
393
|
-
const result = await listSessions({
|
|
525
|
+
const result = await options.provider.listSessions({
|
|
394
526
|
archiveStatus,
|
|
395
|
-
|
|
527
|
+
...providerOptions(options.provider.id, options.providerHome),
|
|
396
528
|
inactiveBeforeMs,
|
|
397
529
|
includeInternals: Boolean(options.includeInternals),
|
|
530
|
+
includeSupporting: Boolean(options.includeSupporting),
|
|
398
531
|
page,
|
|
399
532
|
pageSize: Math.min(100, limit - written),
|
|
400
533
|
search: options.search || "",
|
|
@@ -404,7 +537,7 @@ async function printJson(options) {
|
|
|
404
537
|
|
|
405
538
|
for (const record of result.records) {
|
|
406
539
|
if (written >= limit) break;
|
|
407
|
-
await writeChunk(`${first ? "" : ",\n"}${JSON.stringify(formatSessionForJson(record), null, 2)}`);
|
|
540
|
+
await writeChunk(`${first ? "" : ",\n"}${JSON.stringify(options.provider.formatSessionForJson(record), null, 2)}`);
|
|
408
541
|
first = false;
|
|
409
542
|
written += 1;
|
|
410
543
|
}
|
|
@@ -427,11 +560,14 @@ async function runInteractive(state) {
|
|
|
427
560
|
await refreshPage(state);
|
|
428
561
|
printScreen({
|
|
429
562
|
archiveStatus: state.archiveStatus,
|
|
430
|
-
|
|
563
|
+
cleanupMode: state.cleanupMode,
|
|
431
564
|
inactiveDays: state.inactiveDays,
|
|
565
|
+
providerHome: state.providerHome,
|
|
566
|
+
providerName: state.provider.displayName,
|
|
432
567
|
result: state.result,
|
|
433
568
|
search: state.search,
|
|
434
569
|
showInternals: state.showInternals,
|
|
570
|
+
showSupporting: state.showSupporting,
|
|
435
571
|
sort: state.sort,
|
|
436
572
|
workspace: state.workspace,
|
|
437
573
|
});
|
|
@@ -454,6 +590,7 @@ async function runInteractive(state) {
|
|
|
454
590
|
}
|
|
455
591
|
|
|
456
592
|
if (["refresh", "r"].includes(command.name)) {
|
|
593
|
+
state.forceRefresh = true;
|
|
457
594
|
state.page = 1;
|
|
458
595
|
continue;
|
|
459
596
|
}
|
|
@@ -464,6 +601,126 @@ async function runInteractive(state) {
|
|
|
464
601
|
continue;
|
|
465
602
|
}
|
|
466
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
|
+
|
|
467
724
|
if (["search", "s"].includes(command.name)) {
|
|
468
725
|
state.search = command.args.join(" ");
|
|
469
726
|
state.page = 1;
|
|
@@ -480,6 +737,7 @@ async function runInteractive(state) {
|
|
|
480
737
|
try {
|
|
481
738
|
state.inactiveDays = validateInactiveDays(command.args[0]);
|
|
482
739
|
state.page = 1;
|
|
740
|
+
state.forceRefresh = true;
|
|
483
741
|
} catch (error) {
|
|
484
742
|
output.write(`\n${error instanceof Error ? error.message : String(error)}\n`);
|
|
485
743
|
await pause(rl);
|
|
@@ -528,12 +786,12 @@ async function runInteractive(state) {
|
|
|
528
786
|
throw new Error("`inspect` accepts exactly one selector.");
|
|
529
787
|
}
|
|
530
788
|
|
|
531
|
-
const record = await getSessionRecord({
|
|
532
|
-
const deletionStore = await loadDeletionStore({
|
|
533
|
-
|
|
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),
|
|
534
792
|
recordIds: sessionIds,
|
|
535
793
|
});
|
|
536
|
-
const deletionPlan = await planSessionDeletion({
|
|
794
|
+
const deletionPlan = await state.provider.planSessionDeletion({
|
|
537
795
|
recordIds: sessionIds,
|
|
538
796
|
store: deletionStore,
|
|
539
797
|
});
|
|
@@ -550,19 +808,30 @@ async function runInteractive(state) {
|
|
|
550
808
|
if (command.name === "delete") {
|
|
551
809
|
try {
|
|
552
810
|
const sessionIds = parseSelectors(command.args, state.result.records);
|
|
553
|
-
|
|
554
|
-
const
|
|
555
|
-
|
|
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),
|
|
556
820
|
recordIds: sessionIds,
|
|
557
821
|
});
|
|
558
|
-
const deletionPlan = await planSessionDeletion({
|
|
822
|
+
const deletionPlan = await state.provider.planSessionDeletion({
|
|
559
823
|
recordIds: sessionIds,
|
|
560
824
|
store: deletionStore,
|
|
561
825
|
});
|
|
826
|
+
const preflight = await state.provider.preflightSessionDeletion({
|
|
827
|
+
plan: deletionPlan,
|
|
828
|
+
scope,
|
|
829
|
+
store: deletionStore,
|
|
830
|
+
});
|
|
562
831
|
|
|
563
|
-
printDeletionPreview(deletionPlan);
|
|
832
|
+
printDeletionPreview(deletionPlan, preflight, scope);
|
|
564
833
|
output.write(
|
|
565
|
-
|
|
834
|
+
`\nClose the selected ${state.provider.displayName} sessions before continuing.\n`,
|
|
566
835
|
);
|
|
567
836
|
|
|
568
837
|
const confirmationToken =
|
|
@@ -594,7 +863,7 @@ async function runInteractive(state) {
|
|
|
594
863
|
let result;
|
|
595
864
|
|
|
596
865
|
try {
|
|
597
|
-
result = await executeSessionDeletion({
|
|
866
|
+
result = await state.provider.executeSessionDeletion({
|
|
598
867
|
onProgress: ({ canCancel: nextCanCancel, message }) => {
|
|
599
868
|
canCancel = nextCanCancel;
|
|
600
869
|
if (message !== lastMessage) {
|
|
@@ -603,21 +872,30 @@ async function runInteractive(state) {
|
|
|
603
872
|
}
|
|
604
873
|
},
|
|
605
874
|
plan: deletionPlan,
|
|
875
|
+
scope,
|
|
606
876
|
shouldCancel: () => cancelRequested,
|
|
607
877
|
store: deletionStore,
|
|
608
878
|
});
|
|
609
879
|
} finally {
|
|
610
880
|
process.off("SIGINT", handleInterrupt);
|
|
611
881
|
}
|
|
612
|
-
const verification = await verifySessionDeletion({
|
|
882
|
+
const verification = await state.provider.verifySessionDeletion({
|
|
613
883
|
plan: deletionPlan,
|
|
614
|
-
scope
|
|
884
|
+
scope,
|
|
615
885
|
store: deletionStore,
|
|
616
886
|
});
|
|
617
887
|
|
|
618
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
|
+
}
|
|
619
897
|
output.write(
|
|
620
|
-
`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`,
|
|
621
899
|
);
|
|
622
900
|
} else {
|
|
623
901
|
output.write(
|
|
@@ -631,7 +909,11 @@ async function runInteractive(state) {
|
|
|
631
909
|
);
|
|
632
910
|
}
|
|
633
911
|
|
|
912
|
+
state.provider.invalidateSessionCache?.(
|
|
913
|
+
providerOptions(state.provider.id, state.providerHome),
|
|
914
|
+
);
|
|
634
915
|
state.page = 1;
|
|
916
|
+
state.forceRefresh = true;
|
|
635
917
|
} catch (error) {
|
|
636
918
|
output.write(`\n${error instanceof Error ? error.message : String(error)}\n`);
|
|
637
919
|
await pause(rl);
|
|
@@ -654,19 +936,44 @@ export async function runCli(options) {
|
|
|
654
936
|
return;
|
|
655
937
|
}
|
|
656
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);
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
|
|
657
960
|
if (options.json) {
|
|
658
|
-
await printJson(options);
|
|
961
|
+
await printJson({ ...options, provider, providerHome });
|
|
659
962
|
return;
|
|
660
963
|
}
|
|
661
964
|
|
|
662
965
|
const state = {
|
|
663
966
|
archiveStatus: validateArchiveStatus(options.archiveStatus),
|
|
664
|
-
|
|
967
|
+
cleanupMode: validateCleanupMode(options.cleanup),
|
|
968
|
+
provider,
|
|
969
|
+
providerHome,
|
|
665
970
|
inactiveDays: validateInactiveDays(options.inactiveDays),
|
|
971
|
+
forceRefresh: false,
|
|
666
972
|
page: 1,
|
|
667
973
|
result: null,
|
|
668
974
|
search: options.search || "",
|
|
669
975
|
showInternals: Boolean(options.includeInternals),
|
|
976
|
+
showSupporting: Boolean(options.includeSupporting),
|
|
670
977
|
sort: validateSort(options.sort || "updated"),
|
|
671
978
|
workspace: options.workspace,
|
|
672
979
|
};
|