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/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 = {
|
|
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:
|
|
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
|
|
360
|
+
async function readSessionOverview({ providerId, refresh = false } = {}) {
|
|
361
|
+
const home = settings.getHome(providerId);
|
|
344
362
|
|
|
345
363
|
if (
|
|
346
364
|
!refresh
|
|
347
|
-
&& overviewCache?.
|
|
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({
|
|
372
|
+
const overview = await getProvider(providerId).getSessionOverview({
|
|
373
|
+
...providerOptions(providerId, home),
|
|
374
|
+
refresh,
|
|
375
|
+
});
|
|
354
376
|
overviewCache = {
|
|
355
|
-
|
|
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
|
-
|
|
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.
|
|
430
|
+
if (savedPlan.home !== settings.getHome(savedPlan.providerId)) {
|
|
407
431
|
deletionPlans.delete(planId);
|
|
408
432
|
throw codedError(
|
|
409
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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, {
|
|
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
|
|
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
|
|
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
|
-
|
|
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({
|
|
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
|
|
837
|
+
const home = settings.getHome(providerId);
|
|
838
|
+
const options = providerOptions(providerId, home);
|
|
757
839
|
|
|
758
840
|
if (scope === "deep") {
|
|
759
|
-
await assertDeepCleanupSupported(
|
|
841
|
+
await provider.assertDeepCleanupSupported(options);
|
|
760
842
|
}
|
|
761
843
|
|
|
762
|
-
const store = await loadDeletionStore({
|
|
763
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
4
|
-
"description": "A local Codex session manager for safely reviewing and deleting old sessions
|
|
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",
|