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/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
 
@@ -318,14 +333,17 @@ function removeExpiredEntries(entries, ttlMs, maximum) {
318
333
  }
319
334
  }
320
335
 
321
- export async function startLocalServer({ codexHome, configDirectory, port = 0 }) {
336
+ export async function startLocalServer({ claudeHome, codexHome, configDirectory, port = 0 }) {
322
337
  if (!Number.isInteger(port) || port < 0 || port > 65535) {
323
338
  throw new Error("port must be an integer between 0 and 65535.");
324
339
  }
325
340
 
326
341
  const settings = await createProviderSettings({
327
342
  configDirectory,
328
- providerHomeOverrides: codexHome === undefined ? {} : { codex: codexHome },
343
+ providerHomeOverrides: {
344
+ ...(codexHome === undefined ? {} : { codex: codexHome }),
345
+ ...(claudeHome === undefined ? {} : { "claude-code": claudeHome }),
346
+ },
329
347
  });
330
348
  const mutationToken = randomBytes(32).toString("base64url");
331
349
  let mutationInProgress = false;
@@ -339,20 +357,25 @@ export async function startLocalServer({ codexHome, configDirectory, port = 0 })
339
357
  overviewCache = null;
340
358
  }
341
359
 
342
- async function readSessionOverview({ refresh = false } = {}) {
343
- const activeCodexHome = settings.getHome("codex");
360
+ async function readSessionOverview({ providerId, refresh = false } = {}) {
361
+ const home = settings.getHome(providerId);
344
362
 
345
363
  if (
346
364
  !refresh
347
- && overviewCache?.codexHome === activeCodexHome
365
+ && overviewCache?.providerId === providerId
366
+ && overviewCache?.home === home
348
367
  && overviewCache.expiresAtMs > Date.now()
349
368
  ) {
350
369
  return overviewCache.overview;
351
370
  }
352
371
 
353
- const overview = await getSessionOverview({ codexHome: activeCodexHome });
372
+ const overview = await getProvider(providerId).getSessionOverview({
373
+ ...providerOptions(providerId, home),
374
+ refresh,
375
+ });
354
376
  overviewCache = {
355
- codexHome: activeCodexHome,
377
+ home,
378
+ providerId,
356
379
  expiresAtMs: Date.now() + SESSION_OVERVIEW_TTL_MS,
357
380
  overview,
358
381
  };
@@ -360,6 +383,7 @@ export async function startLocalServer({ codexHome, configDirectory, port = 0 })
360
383
  }
361
384
 
362
385
  async function removeOperationBackups(operation, additionalDirectories = []) {
386
+ const provider = getProvider(operation.providerId);
363
387
  const candidates = [...new Set([
364
388
  ...(operation.backupDirectories ?? []),
365
389
  operation.backupDirectory,
@@ -370,9 +394,9 @@ export async function startLocalServer({ codexHome, configDirectory, port = 0 })
370
394
 
371
395
  for (const backupDirectory of candidates) {
372
396
  try {
373
- await deleteSessionDeletionBackup({
397
+ await provider.deleteSessionDeletionBackup({
374
398
  backupDirectory,
375
- codexHome: operation.codexHome,
399
+ ...providerOptions(operation.providerId, operation.home),
376
400
  });
377
401
  } catch {
378
402
  remaining.push(backupDirectory);
@@ -403,10 +427,10 @@ export async function startLocalServer({ codexHome, configDirectory, port = 0 })
403
427
  );
404
428
  }
405
429
 
406
- if (savedPlan.codexHome !== settings.getHome("codex")) {
430
+ if (savedPlan.home !== settings.getHome(savedPlan.providerId)) {
407
431
  deletionPlans.delete(planId);
408
432
  throw codedError(
409
- "The Codex session folder changed. Review the selection again.",
433
+ `The ${getProvider(savedPlan.providerId).displayName} session folder changed. Review the selection again.`,
410
434
  PLAN_REVIEW_REQUIRED,
411
435
  );
412
436
  }
@@ -427,11 +451,13 @@ export async function startLocalServer({ codexHome, configDirectory, port = 0 })
427
451
  operation.progress = 2;
428
452
 
429
453
  try {
454
+ const provider = getProvider(savedPlan.providerId);
455
+ const options = providerOptions(savedPlan.providerId, savedPlan.home);
430
456
  let currentStore;
431
457
 
432
458
  try {
433
- currentStore = await loadDeletionStore({
434
- codexHome: savedPlan.codexHome,
459
+ currentStore = await provider.loadDeletionStore({
460
+ ...options,
435
461
  recordIds: savedPlan.requestedIds,
436
462
  });
437
463
  } catch (error) {
@@ -444,11 +470,11 @@ export async function startLocalServer({ codexHome, configDirectory, port = 0 })
444
470
  throw error;
445
471
  }
446
472
 
447
- const currentPlan = await planSessionDeletion({
473
+ const currentPlan = await provider.planSessionDeletion({
448
474
  recordIds: savedPlan.requestedIds,
449
475
  store: currentStore,
450
476
  });
451
- const currentFingerprint = await fingerprintSessionDeletion({
477
+ const currentFingerprint = await provider.fingerprintSessionDeletion({
452
478
  plan: currentPlan,
453
479
  scope: savedPlan.scope,
454
480
  store: currentStore,
@@ -461,11 +487,17 @@ export async function startLocalServer({ codexHome, configDirectory, port = 0 })
461
487
  );
462
488
  }
463
489
 
490
+ await provider.preflightSessionDeletion({
491
+ plan: currentPlan,
492
+ scope: savedPlan.scope,
493
+ store: currentStore,
494
+ });
495
+
464
496
  if (savedPlan.scope === "deep") {
465
- await assertDeepCleanupSupported({ codexHome: savedPlan.codexHome });
497
+ await provider.assertDeepCleanupSupported(options);
466
498
  }
467
499
 
468
- const result = await executeSessionDeletion({
500
+ const result = await provider.executeSessionDeletion({
469
501
  onProgress: (update) => Object.assign(operation, update),
470
502
  plan: currentPlan,
471
503
  scope: savedPlan.scope,
@@ -479,7 +511,7 @@ export async function startLocalServer({ codexHome, configDirectory, port = 0 })
479
511
  operation.message = "Checking that cleanup completed";
480
512
  operation.phase = "verification";
481
513
  operation.progress = 94;
482
- const verification = await verifySessionDeletion({
514
+ const verification = await provider.verifySessionDeletion({
483
515
  plan: currentPlan,
484
516
  scope: savedPlan.scope,
485
517
  store: currentStore,
@@ -524,6 +556,9 @@ export async function startLocalServer({ codexHome, configDirectory, port = 0 })
524
556
  operation.status = "failed";
525
557
  }
526
558
  } finally {
559
+ getProvider(savedPlan.providerId).invalidateSessionCache?.(
560
+ providerOptions(savedPlan.providerId, savedPlan.home),
561
+ );
527
562
  invalidateSessionOverview();
528
563
  savedPlan.consumed = true;
529
564
  deletionPlans.delete(savedPlan.id);
@@ -543,7 +578,8 @@ export async function startLocalServer({ codexHome, configDirectory, port = 0 })
543
578
  canDeleteBackup: false,
544
579
  canRestore: false,
545
580
  cancelRequested: false,
546
- codexHome: savedPlan.codexHome,
581
+ home: savedPlan.home,
582
+ providerId: savedPlan.providerId,
547
583
  createdAtMs: Date.now(),
548
584
  error: null,
549
585
  errorCode: null,
@@ -578,9 +614,9 @@ export async function startLocalServer({ codexHome, configDirectory, port = 0 })
578
614
  activeOperationId = operation.id;
579
615
  const task = (async () => {
580
616
  try {
581
- const restoreResult = await restoreSessionDeletionBackup({
617
+ const restoreResult = await getProvider(operation.providerId).restoreSessionDeletionBackup({
582
618
  backupDirectory: operation.backupDirectory,
583
- codexHome: operation.codexHome,
619
+ ...providerOptions(operation.providerId, operation.home),
584
620
  onProgress: (update) => Object.assign(operation, update),
585
621
  });
586
622
  operation.restoreResult = restoreResult;
@@ -609,6 +645,9 @@ export async function startLocalServer({ codexHome, configDirectory, port = 0 })
609
645
  operation.message = "Restore could not be completed";
610
646
  operation.status = "restore-failed";
611
647
  } finally {
648
+ getProvider(operation.providerId).invalidateSessionCache?.(
649
+ providerOptions(operation.providerId, operation.home),
650
+ );
612
651
  invalidateSessionOverview();
613
652
  operation.finishedAtMs = Date.now();
614
653
  if (activeOperationId === operation.id) activeOperationId = null;
@@ -656,7 +695,34 @@ export async function startLocalServer({ codexHome, configDirectory, port = 0 })
656
695
  }
657
696
 
658
697
  if (request.method === "GET" && requestUrl.pathname === "/api/config") {
659
- sendJson(response, 200, { mutationToken, providers: settings.getAll() });
698
+ sendJson(response, 200, {
699
+ activeProviderId: settings.getActiveProviderId(),
700
+ mutationToken,
701
+ providerOrder: listProviders().map(({ id }) => id),
702
+ providers: settings.getAll(),
703
+ });
704
+ return;
705
+ }
706
+
707
+ if (request.method === "PUT" && requestUrl.pathname === "/api/settings/active-provider") {
708
+ requireMutationAuthorization({ request, requestUrl, token: mutationToken });
709
+
710
+ if (mutationInProgress || activeOperationId) {
711
+ sendJson(response, 409, { error: "Wait for the current change to finish before switching providers." });
712
+ return;
713
+ }
714
+
715
+ mutationInProgress = true;
716
+
717
+ try {
718
+ const activeProviderId = await settings.setActiveProviderId(
719
+ (await readJsonBody(request)).providerId,
720
+ );
721
+ sendJson(response, 200, { activeProviderId });
722
+ } finally {
723
+ mutationInProgress = false;
724
+ }
725
+
660
726
  return;
661
727
  }
662
728
 
@@ -691,7 +757,10 @@ export async function startLocalServer({ codexHome, configDirectory, port = 0 })
691
757
  }
692
758
 
693
759
  if (request.method === "GET" && requestUrl.pathname === "/api/compatibility") {
694
- const diagnostic = await diagnoseStorageCompatibility({ codexHome: settings.getHome("codex") });
760
+ const providerId = resolveProviderId(requestUrl.searchParams.get("provider"));
761
+ const provider = getProvider(providerId);
762
+ const home = settings.getHome(providerId);
763
+ const diagnostic = await provider.diagnoseStorageCompatibility(providerOptions(providerId, home));
695
764
  const currentVersions = await getInstalledProductVersions();
696
765
  const versionSupport = Object.fromEntries(
697
766
  Object.entries(diagnostic.builtFor).map(([product, supportedVersions]) => [
@@ -702,12 +771,14 @@ export async function startLocalServer({ codexHome, configDirectory, port = 0 })
702
771
  }),
703
772
  ]),
704
773
  );
705
- sendJson(response, 200, { ...diagnostic, currentVersions, versionSupport });
774
+ sendJson(response, 200, { ...diagnostic, currentVersions, providerId, versionSupport });
706
775
  return;
707
776
  }
708
777
 
709
778
  if (request.method === "GET" && requestUrl.pathname === "/api/session-overview") {
779
+ const providerId = resolveProviderId(requestUrl.searchParams.get("provider"));
710
780
  const overview = await readSessionOverview({
781
+ providerId,
711
782
  refresh: requestUrl.searchParams.get("refresh") === "true",
712
783
  });
713
784
  sendJson(response, 200, { overview });
@@ -715,64 +786,76 @@ export async function startLocalServer({ codexHome, configDirectory, port = 0 })
715
786
  }
716
787
 
717
788
  if (request.method === "GET" && requestUrl.pathname === "/api/sessions") {
718
- const result = await listSessions({
789
+ const providerId = resolveProviderId(requestUrl.searchParams.get("provider"));
790
+ const provider = getProvider(providerId);
791
+ const result = await provider.listSessions({
719
792
  archiveStatus: getArchiveStatus(requestUrl.searchParams.get("archiveStatus")),
720
- codexHome: settings.getHome("codex"),
793
+ ...providerOptions(providerId, settings.getHome(providerId)),
721
794
  inactiveBeforeMs: getInactiveBeforeMs(requestUrl.searchParams.get("inactiveDays")),
722
795
  includeInternals: requestUrl.searchParams.get("includeInternals") === "true",
723
796
  includeSupporting: requestUrl.searchParams.get("includeSupporting") === "true",
724
797
  page: getPositiveInteger(requestUrl.searchParams.get("page"), 1),
725
798
  pageSize: getPositiveInteger(requestUrl.searchParams.get("pageSize"), 25, 100),
799
+ refresh: requestUrl.searchParams.get("refresh") === "true",
726
800
  search: requestUrl.searchParams.get("search"),
727
- sort: requestUrl.searchParams.get("sort"),
801
+ sort: getSort(requestUrl.searchParams.get("sort")),
728
802
  workspace: requestUrl.searchParams.has("workspace")
729
803
  ? requestUrl.searchParams.get("workspace")
730
804
  : undefined,
731
805
  });
732
806
  sendJson(response, 200, {
733
807
  ...result,
734
- records: result.records.map(formatSessionForJson),
808
+ records: result.records.map(provider.formatSessionForJson),
735
809
  });
736
810
  return;
737
811
  }
738
812
 
739
813
  if (request.method === "GET" && requestUrl.pathname.startsWith("/api/sessions/")) {
814
+ const providerId = resolveProviderId(requestUrl.searchParams.get("provider"));
815
+ const provider = getProvider(providerId);
740
816
  const id = decodeURIComponent(requestUrl.pathname.slice("/api/sessions/".length));
741
- const record = await getSessionRecord({ codexHome: settings.getHome("codex"), id });
817
+ const record = await provider.getSessionRecord({
818
+ ...providerOptions(providerId, settings.getHome(providerId)),
819
+ id,
820
+ });
742
821
 
743
822
  if (!record) {
744
823
  sendJson(response, 404, { error: "Session not found." });
745
824
  return;
746
825
  }
747
826
 
748
- sendJson(response, 200, { record: formatSessionForJson(record) });
827
+ sendJson(response, 200, { record: provider.formatSessionForJson(record) });
749
828
  return;
750
829
  }
751
830
 
752
831
  if (request.method === "POST" && requestUrl.pathname === "/api/deletion-plans") {
753
832
  const body = await readJsonBody(request);
833
+ const providerId = resolveProviderId(body.providerId);
834
+ const provider = getProvider(providerId);
754
835
  const ids = normalizeIds(body.ids);
755
836
  const scope = getScope(body.scope);
756
- const activeCodexHome = settings.getHome("codex");
837
+ const home = settings.getHome(providerId);
838
+ const options = providerOptions(providerId, home);
757
839
 
758
840
  if (scope === "deep") {
759
- await assertDeepCleanupSupported({ codexHome: activeCodexHome });
841
+ await provider.assertDeepCleanupSupported(options);
760
842
  }
761
843
 
762
- const store = await loadDeletionStore({
763
- codexHome: activeCodexHome,
844
+ const store = await provider.loadDeletionStore({
845
+ ...options,
764
846
  recordIds: ids,
765
847
  });
766
- const plan = await planSessionDeletion({ recordIds: ids, store });
767
- const preflight = await preflightSessionDeletion({ plan, store });
848
+ const plan = await provider.planSessionDeletion({ recordIds: ids, store });
849
+ const preflight = await provider.preflightSessionDeletion({ plan, scope, store });
768
850
  const id = randomBytes(18).toString("base64url");
769
851
  const expiresAtMs = Date.now() + PLAN_TTL_MS;
770
852
  const savedPlan = {
771
- codexHome: activeCodexHome,
853
+ home,
772
854
  consumed: false,
773
855
  expiresAtMs,
774
- fingerprint: await fingerprintSessionDeletion({ plan, scope, store }),
856
+ fingerprint: await provider.fingerprintSessionDeletion({ plan, scope, store }),
775
857
  id,
858
+ providerId,
776
859
  requestedIds: ids,
777
860
  scope,
778
861
  };
@@ -781,16 +864,18 @@ export async function startLocalServer({ codexHome, configDirectory, port = 0 })
781
864
  deletionPlans.delete(deletionPlans.keys().next().value);
782
865
  }
783
866
  deletionPlans.set(id, savedPlan);
867
+ const warnings = preflight.activeThreadDetection === "unavailable"
868
+ ? [`The current ${provider.displayName} runtime cannot identify an active session. Confirm it is safe to delete the selected sessions.`]
869
+ : [];
784
870
  sendJson(response, 200, {
785
871
  plan: {
786
872
  ...summarizePlan(plan, preflight, scope),
787
873
  expiresAtMs,
788
874
  id,
875
+ warnings,
789
876
  },
790
877
  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
- : [],
878
+ warnings,
794
879
  });
795
880
  return;
796
881
  }
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.3.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,10 @@
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",
55
61
  "prepack": "npm run build",
62
+ "pretest": "npm run build",
56
63
  "start": "node ./bin/session-steward.mjs",
57
64
  "serve": "node ./bin/session-steward.mjs",
58
65
  "check": "node ./bin/session-steward-cli.mjs --json --limit 5",