session-steward 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
1
  import { codexProvider } from "./codex/index.mjs";
2
+ import { claudeCodeProvider } from "./claude-code/index.mjs";
2
3
 
3
- const providers = Object.freeze([codexProvider]);
4
+ const providers = Object.freeze([codexProvider, claudeCodeProvider]);
4
5
  const providersById = new Map(providers.map((provider) => [provider.id, provider]));
5
6
 
6
7
  export function getProvider(providerId) {
package/lib/server.mjs CHANGED
@@ -5,27 +5,10 @@ import { promises as fs } from "node:fs";
5
5
  import path from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
 
8
- import { getProvider } from "./providers/index.mjs";
8
+ import { getProvider, listProviders } from "./providers/index.mjs";
9
9
  import { createProviderSettings } from "./settings.mjs";
10
10
  import { classifyInstalledVersion } from "./version-support.mjs";
11
11
 
12
- const {
13
- assertDeepCleanupSupported,
14
- deleteSessionDeletionBackup,
15
- formatSessionForJson,
16
- executeSessionDeletion,
17
- fingerprintSessionDeletion,
18
- diagnoseStorageCompatibility,
19
- getSessionOverview,
20
- getSessionRecord,
21
- listSessions,
22
- loadDeletionStore,
23
- planSessionDeletion,
24
- preflightSessionDeletion,
25
- restoreSessionDeletionBackup,
26
- verifySessionDeletion,
27
- } = getProvider("codex");
28
-
29
12
  const MAX_BODY_BYTES = 64 * 1024;
30
13
  const ALLOWED_SCOPES = new Set(["core", "deep"]);
31
14
  const PLAN_TTL_MS = 10 * 60 * 1000;
@@ -37,6 +20,7 @@ const PLAN_REVIEW_REQUIRED = "DELETION_PLAN_REVIEW_REQUIRED";
37
20
  const SESSION_OVERVIEW_TTL_MS = 45 * 1000;
38
21
  const ALLOWED_INACTIVE_DAYS = new Set([30, 60, 90]);
39
22
  const ALLOWED_ARCHIVE_STATUSES = new Set(["all", "active", "archived"]);
23
+ const ALLOWED_SORTS = new Set(["created", "cwd", "name", "size", "updated"]);
40
24
  const publicDirectory = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "dist");
41
25
  const staticAssets = new Map([
42
26
  ["/", { fileName: "index.html", contentType: "text/html; charset=utf-8" }],
@@ -51,22 +35,43 @@ function readCommandVersion(command, args) {
51
35
  }
52
36
 
53
37
  async function getInstalledProductVersions() {
54
- const versions = { chatgptDesktop: null, codexCli: readCommandVersion("codex", ["--version"]) };
38
+ const versions = {
39
+ chatgptDesktop: null,
40
+ claudeCli: readCommandVersion("claude", ["--version"]),
41
+ claudeDesktop: null,
42
+ codexCli: readCommandVersion("codex", ["--version"]),
43
+ };
55
44
 
56
45
  if (process.platform !== "darwin") {
57
46
  return versions;
58
47
  }
59
48
 
60
49
  const chatGptInfoPath = "/Applications/ChatGPT.app/Contents/Info.plist";
50
+ const claudeInfoPath = "/Applications/Claude.app/Contents/Info.plist";
61
51
  try {
62
52
  await fs.access(chatGptInfoPath);
63
53
  versions.chatgptDesktop = readCommandVersion("/usr/libexec/PlistBuddy", ["-c", "Print :CFBundleShortVersionString", chatGptInfoPath]);
64
54
  } catch {
65
55
  }
56
+ try {
57
+ await fs.access(claudeInfoPath);
58
+ versions.claudeDesktop = readCommandVersion("/usr/libexec/PlistBuddy", ["-c", "Print :CFBundleShortVersionString", claudeInfoPath]);
59
+ } catch {
60
+ }
66
61
 
67
62
  return versions;
68
63
  }
69
64
 
65
+ function providerOptions(providerId, home) {
66
+ return providerId === "codex" ? { codexHome: home } : { claudeHome: home };
67
+ }
68
+
69
+ function resolveProviderId(value) {
70
+ const providerId = value || "codex";
71
+ getProvider(providerId);
72
+ return providerId;
73
+ }
74
+
70
75
  function getStaticAsset(requestPath) {
71
76
  const knownAsset = staticAssets.get(requestPath);
72
77
 
@@ -193,6 +198,16 @@ function getArchiveStatus(value) {
193
198
  return status;
194
199
  }
195
200
 
201
+ function getSort(value) {
202
+ const sort = value || "updated";
203
+
204
+ if (!ALLOWED_SORTS.has(sort)) {
205
+ throw new Error("Session order is not available.");
206
+ }
207
+
208
+ return sort;
209
+ }
210
+
196
211
  function getLocalRequestOrigin({ hostHeader, server }) {
197
212
  if (typeof hostHeader !== "string") {
198
213
  return null;
@@ -254,8 +269,8 @@ function summarizePlan(plan, preflight, scope) {
254
269
  sessionCount: plan.ids.length,
255
270
  spawnEdgeCount: plan.spawnEdgeCount,
256
271
  relatedRecordCount,
257
- transcriptBytes: plan.transcriptBytes,
258
- transcriptCount: plan.transcriptFileCount,
272
+ transcriptBytes: preflight.transcriptBytes ?? plan.transcriptBytes,
273
+ transcriptCount: preflight.transcriptFileCount ?? plan.transcriptFileCount,
259
274
  };
260
275
  }
261
276
 
@@ -279,6 +294,7 @@ function summarizeDeletionResult(result) {
279
294
  deletedTranscriptCount: result.deletedTranscriptPaths.length,
280
295
  recoveryBackupDeleted: false,
281
296
  skippedTranscriptCount: result.skippedTranscriptPaths.length,
297
+ unrecognizedLocationCount: result.unrecognizedLocationCount ?? 0,
282
298
  };
283
299
  }
284
300
 
@@ -318,14 +334,17 @@ function removeExpiredEntries(entries, ttlMs, maximum) {
318
334
  }
319
335
  }
320
336
 
321
- export async function startLocalServer({ codexHome, configDirectory, port = 0 }) {
337
+ export async function startLocalServer({ claudeHome, codexHome, configDirectory, port = 0 }) {
322
338
  if (!Number.isInteger(port) || port < 0 || port > 65535) {
323
339
  throw new Error("port must be an integer between 0 and 65535.");
324
340
  }
325
341
 
326
342
  const settings = await createProviderSettings({
327
343
  configDirectory,
328
- providerHomeOverrides: codexHome === undefined ? {} : { codex: codexHome },
344
+ providerHomeOverrides: {
345
+ ...(codexHome === undefined ? {} : { codex: codexHome }),
346
+ ...(claudeHome === undefined ? {} : { "claude-code": claudeHome }),
347
+ },
329
348
  });
330
349
  const mutationToken = randomBytes(32).toString("base64url");
331
350
  let mutationInProgress = false;
@@ -339,20 +358,25 @@ export async function startLocalServer({ codexHome, configDirectory, port = 0 })
339
358
  overviewCache = null;
340
359
  }
341
360
 
342
- async function readSessionOverview({ refresh = false } = {}) {
343
- const activeCodexHome = settings.getHome("codex");
361
+ async function readSessionOverview({ providerId, refresh = false } = {}) {
362
+ const home = settings.getHome(providerId);
344
363
 
345
364
  if (
346
365
  !refresh
347
- && overviewCache?.codexHome === activeCodexHome
366
+ && overviewCache?.providerId === providerId
367
+ && overviewCache?.home === home
348
368
  && overviewCache.expiresAtMs > Date.now()
349
369
  ) {
350
370
  return overviewCache.overview;
351
371
  }
352
372
 
353
- const overview = await getSessionOverview({ codexHome: activeCodexHome });
373
+ const overview = await getProvider(providerId).getSessionOverview({
374
+ ...providerOptions(providerId, home),
375
+ refresh,
376
+ });
354
377
  overviewCache = {
355
- codexHome: activeCodexHome,
378
+ home,
379
+ providerId,
356
380
  expiresAtMs: Date.now() + SESSION_OVERVIEW_TTL_MS,
357
381
  overview,
358
382
  };
@@ -360,6 +384,7 @@ export async function startLocalServer({ codexHome, configDirectory, port = 0 })
360
384
  }
361
385
 
362
386
  async function removeOperationBackups(operation, additionalDirectories = []) {
387
+ const provider = getProvider(operation.providerId);
363
388
  const candidates = [...new Set([
364
389
  ...(operation.backupDirectories ?? []),
365
390
  operation.backupDirectory,
@@ -370,9 +395,9 @@ export async function startLocalServer({ codexHome, configDirectory, port = 0 })
370
395
 
371
396
  for (const backupDirectory of candidates) {
372
397
  try {
373
- await deleteSessionDeletionBackup({
398
+ await provider.deleteSessionDeletionBackup({
374
399
  backupDirectory,
375
- codexHome: operation.codexHome,
400
+ ...providerOptions(operation.providerId, operation.home),
376
401
  });
377
402
  } catch {
378
403
  remaining.push(backupDirectory);
@@ -403,10 +428,10 @@ export async function startLocalServer({ codexHome, configDirectory, port = 0 })
403
428
  );
404
429
  }
405
430
 
406
- if (savedPlan.codexHome !== settings.getHome("codex")) {
431
+ if (savedPlan.home !== settings.getHome(savedPlan.providerId)) {
407
432
  deletionPlans.delete(planId);
408
433
  throw codedError(
409
- "The Codex session folder changed. Review the selection again.",
434
+ `The ${getProvider(savedPlan.providerId).displayName} session folder changed. Review the selection again.`,
410
435
  PLAN_REVIEW_REQUIRED,
411
436
  );
412
437
  }
@@ -427,11 +452,13 @@ export async function startLocalServer({ codexHome, configDirectory, port = 0 })
427
452
  operation.progress = 2;
428
453
 
429
454
  try {
455
+ const provider = getProvider(savedPlan.providerId);
456
+ const options = providerOptions(savedPlan.providerId, savedPlan.home);
430
457
  let currentStore;
431
458
 
432
459
  try {
433
- currentStore = await loadDeletionStore({
434
- codexHome: savedPlan.codexHome,
460
+ currentStore = await provider.loadDeletionStore({
461
+ ...options,
435
462
  recordIds: savedPlan.requestedIds,
436
463
  });
437
464
  } catch (error) {
@@ -444,11 +471,11 @@ export async function startLocalServer({ codexHome, configDirectory, port = 0 })
444
471
  throw error;
445
472
  }
446
473
 
447
- const currentPlan = await planSessionDeletion({
474
+ const currentPlan = await provider.planSessionDeletion({
448
475
  recordIds: savedPlan.requestedIds,
449
476
  store: currentStore,
450
477
  });
451
- const currentFingerprint = await fingerprintSessionDeletion({
478
+ const currentFingerprint = await provider.fingerprintSessionDeletion({
452
479
  plan: currentPlan,
453
480
  scope: savedPlan.scope,
454
481
  store: currentStore,
@@ -461,11 +488,17 @@ export async function startLocalServer({ codexHome, configDirectory, port = 0 })
461
488
  );
462
489
  }
463
490
 
491
+ await provider.preflightSessionDeletion({
492
+ plan: currentPlan,
493
+ scope: savedPlan.scope,
494
+ store: currentStore,
495
+ });
496
+
464
497
  if (savedPlan.scope === "deep") {
465
- await assertDeepCleanupSupported({ codexHome: savedPlan.codexHome });
498
+ await provider.assertDeepCleanupSupported(options);
466
499
  }
467
500
 
468
- const result = await executeSessionDeletion({
501
+ const result = await provider.executeSessionDeletion({
469
502
  onProgress: (update) => Object.assign(operation, update),
470
503
  plan: currentPlan,
471
504
  scope: savedPlan.scope,
@@ -479,7 +512,7 @@ export async function startLocalServer({ codexHome, configDirectory, port = 0 })
479
512
  operation.message = "Checking that cleanup completed";
480
513
  operation.phase = "verification";
481
514
  operation.progress = 94;
482
- const verification = await verifySessionDeletion({
515
+ const verification = await provider.verifySessionDeletion({
483
516
  plan: currentPlan,
484
517
  scope: savedPlan.scope,
485
518
  store: currentStore,
@@ -524,6 +557,9 @@ export async function startLocalServer({ codexHome, configDirectory, port = 0 })
524
557
  operation.status = "failed";
525
558
  }
526
559
  } finally {
560
+ getProvider(savedPlan.providerId).invalidateSessionCache?.(
561
+ providerOptions(savedPlan.providerId, savedPlan.home),
562
+ );
527
563
  invalidateSessionOverview();
528
564
  savedPlan.consumed = true;
529
565
  deletionPlans.delete(savedPlan.id);
@@ -543,7 +579,8 @@ export async function startLocalServer({ codexHome, configDirectory, port = 0 })
543
579
  canDeleteBackup: false,
544
580
  canRestore: false,
545
581
  cancelRequested: false,
546
- codexHome: savedPlan.codexHome,
582
+ home: savedPlan.home,
583
+ providerId: savedPlan.providerId,
547
584
  createdAtMs: Date.now(),
548
585
  error: null,
549
586
  errorCode: null,
@@ -578,9 +615,9 @@ export async function startLocalServer({ codexHome, configDirectory, port = 0 })
578
615
  activeOperationId = operation.id;
579
616
  const task = (async () => {
580
617
  try {
581
- const restoreResult = await restoreSessionDeletionBackup({
618
+ const restoreResult = await getProvider(operation.providerId).restoreSessionDeletionBackup({
582
619
  backupDirectory: operation.backupDirectory,
583
- codexHome: operation.codexHome,
620
+ ...providerOptions(operation.providerId, operation.home),
584
621
  onProgress: (update) => Object.assign(operation, update),
585
622
  });
586
623
  operation.restoreResult = restoreResult;
@@ -609,6 +646,9 @@ export async function startLocalServer({ codexHome, configDirectory, port = 0 })
609
646
  operation.message = "Restore could not be completed";
610
647
  operation.status = "restore-failed";
611
648
  } finally {
649
+ getProvider(operation.providerId).invalidateSessionCache?.(
650
+ providerOptions(operation.providerId, operation.home),
651
+ );
612
652
  invalidateSessionOverview();
613
653
  operation.finishedAtMs = Date.now();
614
654
  if (activeOperationId === operation.id) activeOperationId = null;
@@ -656,7 +696,34 @@ export async function startLocalServer({ codexHome, configDirectory, port = 0 })
656
696
  }
657
697
 
658
698
  if (request.method === "GET" && requestUrl.pathname === "/api/config") {
659
- sendJson(response, 200, { mutationToken, providers: settings.getAll() });
699
+ sendJson(response, 200, {
700
+ activeProviderId: settings.getActiveProviderId(),
701
+ mutationToken,
702
+ providerOrder: listProviders().map(({ id }) => id),
703
+ providers: settings.getAll(),
704
+ });
705
+ return;
706
+ }
707
+
708
+ if (request.method === "PUT" && requestUrl.pathname === "/api/settings/active-provider") {
709
+ requireMutationAuthorization({ request, requestUrl, token: mutationToken });
710
+
711
+ if (mutationInProgress || activeOperationId) {
712
+ sendJson(response, 409, { error: "Wait for the current change to finish before switching providers." });
713
+ return;
714
+ }
715
+
716
+ mutationInProgress = true;
717
+
718
+ try {
719
+ const activeProviderId = await settings.setActiveProviderId(
720
+ (await readJsonBody(request)).providerId,
721
+ );
722
+ sendJson(response, 200, { activeProviderId });
723
+ } finally {
724
+ mutationInProgress = false;
725
+ }
726
+
660
727
  return;
661
728
  }
662
729
 
@@ -691,7 +758,10 @@ export async function startLocalServer({ codexHome, configDirectory, port = 0 })
691
758
  }
692
759
 
693
760
  if (request.method === "GET" && requestUrl.pathname === "/api/compatibility") {
694
- const diagnostic = await diagnoseStorageCompatibility({ codexHome: settings.getHome("codex") });
761
+ const providerId = resolveProviderId(requestUrl.searchParams.get("provider"));
762
+ const provider = getProvider(providerId);
763
+ const home = settings.getHome(providerId);
764
+ const diagnostic = await provider.diagnoseStorageCompatibility(providerOptions(providerId, home));
695
765
  const currentVersions = await getInstalledProductVersions();
696
766
  const versionSupport = Object.fromEntries(
697
767
  Object.entries(diagnostic.builtFor).map(([product, supportedVersions]) => [
@@ -702,12 +772,14 @@ export async function startLocalServer({ codexHome, configDirectory, port = 0 })
702
772
  }),
703
773
  ]),
704
774
  );
705
- sendJson(response, 200, { ...diagnostic, currentVersions, versionSupport });
775
+ sendJson(response, 200, { ...diagnostic, currentVersions, providerId, versionSupport });
706
776
  return;
707
777
  }
708
778
 
709
779
  if (request.method === "GET" && requestUrl.pathname === "/api/session-overview") {
780
+ const providerId = resolveProviderId(requestUrl.searchParams.get("provider"));
710
781
  const overview = await readSessionOverview({
782
+ providerId,
711
783
  refresh: requestUrl.searchParams.get("refresh") === "true",
712
784
  });
713
785
  sendJson(response, 200, { overview });
@@ -715,64 +787,76 @@ export async function startLocalServer({ codexHome, configDirectory, port = 0 })
715
787
  }
716
788
 
717
789
  if (request.method === "GET" && requestUrl.pathname === "/api/sessions") {
718
- const result = await listSessions({
790
+ const providerId = resolveProviderId(requestUrl.searchParams.get("provider"));
791
+ const provider = getProvider(providerId);
792
+ const result = await provider.listSessions({
719
793
  archiveStatus: getArchiveStatus(requestUrl.searchParams.get("archiveStatus")),
720
- codexHome: settings.getHome("codex"),
794
+ ...providerOptions(providerId, settings.getHome(providerId)),
721
795
  inactiveBeforeMs: getInactiveBeforeMs(requestUrl.searchParams.get("inactiveDays")),
722
796
  includeInternals: requestUrl.searchParams.get("includeInternals") === "true",
723
797
  includeSupporting: requestUrl.searchParams.get("includeSupporting") === "true",
724
798
  page: getPositiveInteger(requestUrl.searchParams.get("page"), 1),
725
799
  pageSize: getPositiveInteger(requestUrl.searchParams.get("pageSize"), 25, 100),
800
+ refresh: requestUrl.searchParams.get("refresh") === "true",
726
801
  search: requestUrl.searchParams.get("search"),
727
- sort: requestUrl.searchParams.get("sort"),
802
+ sort: getSort(requestUrl.searchParams.get("sort")),
728
803
  workspace: requestUrl.searchParams.has("workspace")
729
804
  ? requestUrl.searchParams.get("workspace")
730
805
  : undefined,
731
806
  });
732
807
  sendJson(response, 200, {
733
808
  ...result,
734
- records: result.records.map(formatSessionForJson),
809
+ records: result.records.map(provider.formatSessionForJson),
735
810
  });
736
811
  return;
737
812
  }
738
813
 
739
814
  if (request.method === "GET" && requestUrl.pathname.startsWith("/api/sessions/")) {
815
+ const providerId = resolveProviderId(requestUrl.searchParams.get("provider"));
816
+ const provider = getProvider(providerId);
740
817
  const id = decodeURIComponent(requestUrl.pathname.slice("/api/sessions/".length));
741
- const record = await getSessionRecord({ codexHome: settings.getHome("codex"), id });
818
+ const record = await provider.getSessionRecord({
819
+ ...providerOptions(providerId, settings.getHome(providerId)),
820
+ id,
821
+ });
742
822
 
743
823
  if (!record) {
744
824
  sendJson(response, 404, { error: "Session not found." });
745
825
  return;
746
826
  }
747
827
 
748
- sendJson(response, 200, { record: formatSessionForJson(record) });
828
+ sendJson(response, 200, { record: provider.formatSessionForJson(record) });
749
829
  return;
750
830
  }
751
831
 
752
832
  if (request.method === "POST" && requestUrl.pathname === "/api/deletion-plans") {
753
833
  const body = await readJsonBody(request);
834
+ const providerId = resolveProviderId(body.providerId);
835
+ const provider = getProvider(providerId);
754
836
  const ids = normalizeIds(body.ids);
755
837
  const scope = getScope(body.scope);
756
- const activeCodexHome = settings.getHome("codex");
838
+ const home = settings.getHome(providerId);
839
+ const options = providerOptions(providerId, home);
757
840
 
758
841
  if (scope === "deep") {
759
- await assertDeepCleanupSupported({ codexHome: activeCodexHome });
842
+ await provider.assertDeepCleanupSupported(options);
760
843
  }
761
844
 
762
- const store = await loadDeletionStore({
763
- codexHome: activeCodexHome,
845
+ const store = await provider.loadDeletionStore({
846
+ ...options,
764
847
  recordIds: ids,
765
848
  });
766
- const plan = await planSessionDeletion({ recordIds: ids, store });
767
- const preflight = await preflightSessionDeletion({ plan, store });
849
+ const plan = await provider.planSessionDeletion({ recordIds: ids, store });
850
+ const preflight = await provider.preflightSessionDeletion({ plan, scope, store });
768
851
  const id = randomBytes(18).toString("base64url");
769
852
  const expiresAtMs = Date.now() + PLAN_TTL_MS;
770
853
  const savedPlan = {
771
- codexHome: activeCodexHome,
854
+ home,
772
855
  consumed: false,
773
856
  expiresAtMs,
774
- fingerprint: await fingerprintSessionDeletion({ plan, scope, store }),
857
+ fingerprint: await provider.fingerprintSessionDeletion({ plan, scope, store }),
775
858
  id,
859
+ providerId,
776
860
  requestedIds: ids,
777
861
  scope,
778
862
  };
@@ -781,16 +865,22 @@ export async function startLocalServer({ codexHome, configDirectory, port = 0 })
781
865
  deletionPlans.delete(deletionPlans.keys().next().value);
782
866
  }
783
867
  deletionPlans.set(id, savedPlan);
868
+ const warnings = [];
869
+ if (preflight.activeThreadDetection === "unavailable") {
870
+ warnings.push(`The current ${provider.displayName} runtime cannot identify an active session. Confirm it is safe to delete the selected sessions.`);
871
+ }
872
+ if (plan.unrecognizedLocationCount > 0) {
873
+ warnings.push(`${plan.unrecognizedLocationCount} ${plan.unrecognizedLocationCount === 1 ? "location" : "locations"} in your Claude folder ${plan.unrecognizedLocationCount === 1 ? "was" : "were"} not recognized and will not be examined.`);
874
+ }
784
875
  sendJson(response, 200, {
785
876
  plan: {
786
877
  ...summarizePlan(plan, preflight, scope),
787
878
  expiresAtMs,
788
879
  id,
880
+ warnings,
789
881
  },
790
882
  scope,
791
- warnings: preflight.activeThreadDetection === "unavailable"
792
- ? ["The current Codex runtime cannot identify an active session. Confirm it is safe to delete the selected sessions."]
793
- : [],
883
+ warnings,
794
884
  });
795
885
  return;
796
886
  }
package/lib/settings.mjs CHANGED
@@ -7,6 +7,17 @@ const CONFIG_VERSION = 1;
7
7
  const PROVIDERS = {
8
8
  codex: {
9
9
  defaultHome: () => path.join(os.homedir(), ".codex"),
10
+ displayName: "Codex",
11
+ homeLabel: "Codex home folder",
12
+ homePlaceholder: "~/.codex",
13
+ },
14
+ "claude-code": {
15
+ defaultHome: () => process.env.CLAUDE_CONFIG_DIR && path.isAbsolute(process.env.CLAUDE_CONFIG_DIR)
16
+ ? path.resolve(process.env.CLAUDE_CONFIG_DIR)
17
+ : path.join(os.homedir(), ".claude"),
18
+ displayName: "Claude Code",
19
+ homeLabel: "Claude home folder",
20
+ homePlaceholder: "~/.claude",
10
21
  },
11
22
  };
12
23
 
@@ -165,7 +176,10 @@ export async function createProviderSettings({ configDirectory, providerHomeOver
165
176
 
166
177
  return {
167
178
  defaultHome,
179
+ displayName: definition.displayName,
168
180
  home,
181
+ homeLabel: definition.homeLabel,
182
+ homePlaceholder: definition.homePlaceholder,
169
183
  isDefault: home === defaultHome,
170
184
  source: startupHomes[providerId] && home === startupHomes[providerId]
171
185
  ? "startup"
@@ -175,6 +189,29 @@ export async function createProviderSettings({ configDirectory, providerHomeOver
175
189
  };
176
190
  }
177
191
 
192
+ function getActiveProviderId() {
193
+ return typeof config.activeProviderId === "string" && PROVIDERS[config.activeProviderId]
194
+ ? config.activeProviderId
195
+ : "codex";
196
+ }
197
+
198
+ async function setActiveProviderId(providerId) {
199
+ getProviderDefinition(providerId);
200
+ const nextConfig = {
201
+ ...config,
202
+ activeProviderId: providerId,
203
+ providers: config.providers,
204
+ version: CONFIG_VERSION,
205
+ };
206
+ try {
207
+ await writeConfig(configPath, nextConfig);
208
+ } catch (error) {
209
+ throw new Error("Session Steward could not remember this provider.", { cause: error });
210
+ }
211
+ config = nextConfig;
212
+ return providerId;
213
+ }
214
+
178
215
  async function setProviderHome(providerId, value) {
179
216
  const definition = getProviderDefinition(providerId);
180
217
  const home = await requireExistingDirectory(value);
@@ -221,9 +258,11 @@ export async function createProviderSettings({ configDirectory, providerHomeOver
221
258
  }
222
259
 
223
260
  return {
261
+ getActiveProviderId,
224
262
  getAll: () => Object.fromEntries(Object.keys(PROVIDERS).map((providerId) => [providerId, getProvider(providerId)])),
225
263
  getHome: (providerId) => getProvider(providerId).home,
226
264
  resetProviderHome,
265
+ setActiveProviderId,
227
266
  setProviderHome,
228
267
  };
229
268
  }
@@ -0,0 +1,39 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+
4
+ export async function measurePath(targetPath) {
5
+ let bytes = 0;
6
+ let fileCount = 0;
7
+ const pending = [path.resolve(targetPath)];
8
+
9
+ while (pending.length > 0) {
10
+ const currentPath = pending.pop();
11
+ let stats;
12
+
13
+ try {
14
+ stats = await fs.lstat(currentPath);
15
+ } catch (error) {
16
+ if (error?.code === "ENOENT") continue;
17
+ throw error;
18
+ }
19
+
20
+ if (stats.isSymbolicLink()) continue;
21
+
22
+ if (stats.isDirectory()) {
23
+ const entries = await fs.readdir(currentPath);
24
+
25
+ for (const entry of entries) {
26
+ pending.push(path.join(currentPath, entry));
27
+ }
28
+
29
+ continue;
30
+ }
31
+
32
+ if (stats.isFile()) {
33
+ bytes += stats.size;
34
+ fileCount += 1;
35
+ }
36
+ }
37
+
38
+ return { bytes, fileCount };
39
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "session-steward",
3
- "version": "0.2.0",
4
- "description": "A local Codex session manager for safely reviewing and deleting old sessions with a browser UI or terminal CLI.",
3
+ "version": "0.4.0",
4
+ "description": "A local Codex and Claude Code session manager for safely reviewing and deleting old sessions.",
5
5
  "license": "MIT",
6
6
  "author": "Mallik Cheripally",
7
7
  "type": "module",
@@ -17,6 +17,9 @@
17
17
  "bugs": "https://github.com/mallikcheripally/session-steward/issues",
18
18
  "keywords": [
19
19
  "codex",
20
+ "claude-code",
21
+ "claude-code-sessions",
22
+ "claude-desktop",
20
23
  "codex-cli",
21
24
  "openai-codex",
22
25
  "codex-sessions",
@@ -27,6 +30,7 @@
27
30
  "delete-codex-sessions",
28
31
  "session-manager",
29
32
  "session-cleanup",
33
+ "ai-session-manager",
30
34
  "chatgpt-desktop",
31
35
  "chatgpt-history",
32
36
  "ai-coding-agent",
@@ -37,6 +41,7 @@
37
41
  "bin",
38
42
  "dist",
39
43
  "lib",
44
+ "CHANGELOG.md",
40
45
  "LICENSE",
41
46
  "README.md"
42
47
  ],
@@ -51,8 +56,11 @@
51
56
  "benchmark:discovery": "node --expose-gc test/benchmarks/codex-discovery.mjs",
52
57
  "benchmark:overview": "node --expose-gc test/benchmarks/codex-overview.mjs",
53
58
  "benchmark:scale": "node --expose-gc test/benchmarks/codex-list.mjs",
59
+ "benchmark:size": "node --expose-gc test/benchmarks/codex-size.mjs",
54
60
  "benchmark:transcripts": "node --expose-gc test/benchmarks/codex-transcripts.mjs",
61
+ "benchmark:versioned-stores": "node --expose-gc test/benchmarks/codex-versioned-stores.mjs",
55
62
  "prepack": "npm run build",
63
+ "pretest": "npm run build",
56
64
  "start": "node ./bin/session-steward.mjs",
57
65
  "serve": "node ./bin/session-steward.mjs",
58
66
  "check": "node ./bin/session-steward-cli.mjs --json --limit 5",