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/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
- const {
9
- assertDeepCleanupSupported,
10
- executeSessionDeletion,
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, codexHome, inactiveDays, result, search, showInternals, sort, workspace }) {
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
- `Home: ${codexHome} | Sessions: ${result.total} | Page: ${result.page}/${result.pageCount}\n`,
204
+ `${providerName} home: ${providerHome} | Sessions: ${result.total} | Page: ${result.page}/${result.pageCount}\n`,
174
205
  );
175
206
  output.write(
176
- `Sort: ${sort} | Search: ${search || "-"} | Internals: ${showInternals ? "shown" : "hidden"}\n`,
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 | page | next | prev | internals | refresh | help | quit\n\n",
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", 64)} ${pad("Updated", 8)} ${pad("Cwd", 20)} Markers\n`,
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(64)} ${"-".repeat(8)} ${"-".repeat(20)} ${"-".repeat(24)}\n`);
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, 64), 64)} ${pad(relativeTime(record.updatedAtMs), 8)} ${pad(truncate(getCwdDisplay(record), 20), 20)} ${truncate(getMarkerText(record), 24)}\n`,
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 (!record) {
234
- throw new Error(`No session exists at row ${selector}.`);
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 (start > end) {
247
- throw new Error(`Invalid range: ${selector}.`);
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
- if (!record) {
254
- throw new Error(`No session exists at row ${index}.`);
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
- resolvedIds.add(record.id);
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
- codexHome: state.codexHome,
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
- codexHome: options.codexHome,
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
- codexHome: state.codexHome,
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({ codexHome: state.codexHome, id: sessionIds[0] });
532
- const deletionStore = await loadDeletionStore({
533
- codexHome: state.codexHome,
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
- await assertDeepCleanupSupported({ codexHome: state.codexHome });
554
- const deletionStore = await loadDeletionStore({
555
- codexHome: state.codexHome,
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
- "\nClose the selected Codex sessions before continuing. Active-session detection is unavailable.\n",
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: "deep",
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} transcripts.\n`,
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
- codexHome: path.resolve(options.codexHome),
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
  };
@@ -0,0 +1,7 @@
1
+ import * as store from "./store.mjs";
2
+
3
+ export const claudeCodeProvider = Object.freeze({
4
+ id: "claude-code",
5
+ displayName: "Claude Code",
6
+ ...store,
7
+ });