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.
- package/CHANGELOG.md +73 -0
- package/README.md +78 -22
- package/bin/session-steward-cli.mjs +37 -7
- package/bin/session-steward.mjs +3 -0
- package/dist/assets/index-BQ6SUqXr.css +2 -0
- package/dist/assets/index-QhQbSn0H.js +9 -0
- package/dist/index.html +14 -3
- package/lib/cli.mjs +371 -59
- package/lib/providers/claude-code/index.mjs +7 -0
- package/lib/providers/claude-code/store.mjs +996 -0
- package/lib/providers/codex/database-families.mjs +177 -0
- package/lib/providers/codex/index.mjs +4 -0
- package/lib/providers/codex/store.mjs +685 -333
- package/lib/providers/index.mjs +2 -1
- package/lib/server.mjs +152 -62
- package/lib/settings.mjs +39 -0
- package/lib/storage/files.mjs +39 -0
- package/package.json +10 -2
- package/dist/assets/index-kZ4XDVk-.js +0 -9
- package/dist/assets/index-pzaccjP4.css +0 -2
package/lib/providers/index.mjs
CHANGED
|
@@ -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 = {
|
|
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:
|
|
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
|
|
361
|
+
async function readSessionOverview({ providerId, refresh = false } = {}) {
|
|
362
|
+
const home = settings.getHome(providerId);
|
|
344
363
|
|
|
345
364
|
if (
|
|
346
365
|
!refresh
|
|
347
|
-
&& overviewCache?.
|
|
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({
|
|
373
|
+
const overview = await getProvider(providerId).getSessionOverview({
|
|
374
|
+
...providerOptions(providerId, home),
|
|
375
|
+
refresh,
|
|
376
|
+
});
|
|
354
377
|
overviewCache = {
|
|
355
|
-
|
|
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
|
-
|
|
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.
|
|
431
|
+
if (savedPlan.home !== settings.getHome(savedPlan.providerId)) {
|
|
407
432
|
deletionPlans.delete(planId);
|
|
408
433
|
throw codedError(
|
|
409
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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, {
|
|
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
|
|
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
|
|
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
|
-
|
|
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({
|
|
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
|
|
838
|
+
const home = settings.getHome(providerId);
|
|
839
|
+
const options = providerOptions(providerId, home);
|
|
757
840
|
|
|
758
841
|
if (scope === "deep") {
|
|
759
|
-
await assertDeepCleanupSupported(
|
|
842
|
+
await provider.assertDeepCleanupSupported(options);
|
|
760
843
|
}
|
|
761
844
|
|
|
762
|
-
const store = await loadDeletionStore({
|
|
763
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
4
|
-
"description": "A local Codex session manager for safely reviewing and deleting old sessions
|
|
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",
|