failproofai 0.0.9-beta.1 → 0.0.9
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/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +6 -6
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +2 -2
- package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js +1 -2
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +10 -10
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +8 -8
- package/.next/standalone/.next/server/app/policies/page.js +2 -2
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +4 -3
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js +2 -2
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0g72weg._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0su~k6f._.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_codex-projects_ts_07qqk1g._.js +3 -0
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_0z7w.hh._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01743wx._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0.f_cyx._.js → [root-of-the-server]__01g_w_e._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__092s1ta._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__09icjsf._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0dub28-._.js → [root-of-the-server]__0_b7pgn._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0g.lg8b._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0gs6wz4._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h..k-e._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0it81ys._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u4a9jq._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ymlddl._.js +18 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11pa2ra._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12.h2mg._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12t-wym._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_04w00cm._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_10lm7or._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_0cdqd9w._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_0xerkr6._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_0q-m0y-._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/lib_codex-projects_ts_0eosib~._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/lib_utils_ts_068jk73._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{_0zaq1hm._.js → node_modules_0ttbz1~._.js} +2 -2
- package/.next/standalone/.next/server/middleware-build-manifest.js +6 -6
- package/.next/standalone/.next/server/pages/404.html +2 -2
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +9 -9
- package/.next/standalone/.next/static/chunks/{0_c_yox08g_44.js → 01q52wg_amm60.js} +2 -2
- package/.next/standalone/.next/static/chunks/06x4-d1~o-opr.js +1 -0
- package/.next/standalone/.next/static/chunks/{0bghqwo4iloy0.js → 0756i.7omnnl6.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0p5zh2diw90a1.js → 095l4hc7-h.~~.js} +1 -1
- package/.next/standalone/.next/static/chunks/09ose_165ra4d.js +1 -0
- package/.next/standalone/.next/static/chunks/0n-_j_6fo6jex.js +6 -0
- package/.next/standalone/.next/static/chunks/0n~s0gafwnp2y.js +1 -0
- package/.next/standalone/.next/static/chunks/{0ufq8smh~i7wc.js → 0pr7k36o_.du1.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0z-jh701rc~j8.js → 0t~iusm_fxoao.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0jryicwtm9z2g.js → 0u-ys71jc4y68.js} +3 -3
- package/.next/standalone/.next/static/chunks/0zig0fh30t6ou.js +1 -0
- package/.next/standalone/.next/static/chunks/{0w1f.k~gi-y6..js → 11kt_9zaooda3.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0kzk5-mh1_x53.js → 12simlrcfk3g2.js} +1 -1
- package/.next/standalone/.next/static/chunks/{turbopack-0s36is87fc9r2.js → turbopack-0o7k.hakttp4k.js} +1 -1
- package/.next/standalone/app/components/cli-badge.tsx +24 -0
- package/.next/standalone/app/components/project-list.tsx +13 -7
- package/.next/standalone/app/components/sessions-list.tsx +4 -2
- package/.next/standalone/app/policies/hooks-client.tsx +66 -10
- package/.next/standalone/app/project/[name]/page.tsx +49 -22
- package/.next/standalone/app/project/[name]/session/[sessionId]/page.tsx +51 -19
- package/.next/standalone/components/reach-developers.tsx +6 -1
- package/.next/standalone/lib/codex-projects.ts +250 -0
- package/.next/standalone/lib/codex-sessions.ts +414 -0
- package/.next/standalone/lib/format-date.ts +21 -0
- package/.next/standalone/lib/log-entries.ts +3 -3
- package/.next/standalone/lib/paths.ts +13 -0
- package/.next/standalone/lib/projects.ts +57 -3
- package/.next/standalone/lib/utils.ts +6 -22
- package/.next/standalone/package.json +1 -1
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +1 -0
- package/dist/cli.mjs +1042 -122
- package/lib/codex-projects.ts +250 -0
- package/lib/codex-sessions.ts +414 -0
- package/lib/format-date.ts +21 -0
- package/lib/log-entries.ts +3 -3
- package/lib/paths.ts +13 -0
- package/lib/projects.ts +57 -3
- package/lib/utils.ts +6 -22
- package/package.json +1 -1
- package/scripts/launch.ts +2 -1
- package/src/hooks/builtin-policies.ts +7 -1
- package/src/hooks/hook-activity-store.ts +3 -0
- package/src/hooks/manager.ts +1 -1
- package/src/hooks/resolve-permission-mode.ts +6 -91
- package/.next/standalone/.next/server/chunks/[externals]__080wern._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0b57.gk._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__03rd.z8._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0e74wa-._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0vu.o-3._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0w6l33k._.js +0 -17
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0zqcovi._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__105.l_7._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0uy6m~m._.js +0 -3
- package/.next/standalone/.next/static/chunks/00b5h4r1el.6f.js +0 -1
- package/.next/standalone/.next/static/chunks/0fw2h.g66c0h3.js +0 -1
- package/.next/standalone/.next/static/chunks/0gu87mlr5ssnt.js +0 -6
- package/.next/standalone/.next/static/chunks/0igf3xbisp1lx.js +0 -1
- package/.next/standalone/.next/static/chunks/0vwqucikost_q.js +0 -1
- /package/.next/standalone/.next/static/{CiVeb_yiVt-O2JYrzGzB7 → A_Ax17P33facL0OmIwFXj}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{CiVeb_yiVt-O2JYrzGzB7 → A_Ax17P33facL0OmIwFXj}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{CiVeb_yiVt-O2JYrzGzB7 → A_Ax17P33facL0OmIwFXj}/_ssgManifest.js +0 -0
|
@@ -63,10 +63,45 @@ function projectFromTranscriptPath(transcriptPath: string): string | null {
|
|
|
63
63
|
return folder;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
function
|
|
66
|
+
function encodeCwdForUrl(cwd: string): string {
|
|
67
|
+
// Inverse of decodeFolderName, inlined here so this component file stays
|
|
68
|
+
// client-side and doesn't pull lib/paths server imports.
|
|
69
|
+
const driveMatch = /^([A-Za-z]):[\\/](.*)$/.exec(cwd);
|
|
70
|
+
if (driveMatch) return driveMatch[1] + "--" + driveMatch[2].replace(/[\\/]/g, "-");
|
|
71
|
+
return cwd.replace(/[\\/]/g, "-");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function SessionCell({
|
|
75
|
+
sessionId,
|
|
76
|
+
transcriptPath,
|
|
77
|
+
integration,
|
|
78
|
+
cwd,
|
|
79
|
+
}: {
|
|
80
|
+
sessionId?: string;
|
|
81
|
+
transcriptPath?: string;
|
|
82
|
+
integration?: string;
|
|
83
|
+
cwd?: string;
|
|
84
|
+
}) {
|
|
67
85
|
if (!sessionId) return <span className="text-muted-foreground">\u2014</span>;
|
|
68
|
-
const project = transcriptPath ? projectFromTranscriptPath(transcriptPath) : null;
|
|
69
86
|
const short = shortenSession(sessionId);
|
|
87
|
+
|
|
88
|
+
const isCodex = integration === "codex" || (transcriptPath?.includes("/.codex/") ?? false);
|
|
89
|
+
if (isCodex) {
|
|
90
|
+
// The session route auto-detects CLI by file location, so [name] only
|
|
91
|
+
// affects the breadcrumb. Encode the cwd Claude-style when we have it.
|
|
92
|
+
const projectSeg = cwd ? encodeCwdForUrl(cwd) : "codex";
|
|
93
|
+
return (
|
|
94
|
+
<Link
|
|
95
|
+
href={`/project/${encodeURIComponent(projectSeg)}/session/${encodeURIComponent(sessionId)}`}
|
|
96
|
+
className="text-primary hover:underline font-mono"
|
|
97
|
+
onClick={(e) => e.stopPropagation()}
|
|
98
|
+
>
|
|
99
|
+
{short}
|
|
100
|
+
</Link>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const project = transcriptPath ? projectFromTranscriptPath(transcriptPath) : null;
|
|
70
105
|
if (project) {
|
|
71
106
|
return (
|
|
72
107
|
<Link
|
|
@@ -331,9 +366,13 @@ function ActivityTab({
|
|
|
331
366
|
const [filterEventType, setFilterEventType] = useState(() => url.get("event") ?? "");
|
|
332
367
|
const [filterPolicy, setFilterPolicy] = useState(() => url.get("policy") ?? "");
|
|
333
368
|
const [filterSessionId, setFilterSessionId] = useState(() => url.get("session") ?? "");
|
|
369
|
+
const [filterCli, setFilterCli] = useState<"" | "claude" | "codex">(() => {
|
|
370
|
+
const v = url.get("cli");
|
|
371
|
+
return v === "claude" || v === "codex" ? v : "";
|
|
372
|
+
});
|
|
334
373
|
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
335
|
-
const filtersRef = useRef({ filterDecision, filterEventType, filterPolicy, filterSessionId });
|
|
336
|
-
filtersRef.current = { filterDecision, filterEventType, filterPolicy, filterSessionId };
|
|
374
|
+
const filtersRef = useRef({ filterDecision, filterEventType, filterPolicy, filterSessionId, filterCli });
|
|
375
|
+
filtersRef.current = { filterDecision, filterEventType, filterPolicy, filterSessionId, filterCli };
|
|
337
376
|
|
|
338
377
|
useEffect(() => {
|
|
339
378
|
if (!mountedRef.current) {
|
|
@@ -345,17 +384,18 @@ function ActivityTab({
|
|
|
345
384
|
event: filterEventType || undefined,
|
|
346
385
|
policy: filterPolicy || undefined,
|
|
347
386
|
session: filterSessionId || undefined,
|
|
387
|
+
cli: filterCli || undefined,
|
|
348
388
|
page: pageToParam(page),
|
|
349
389
|
});
|
|
350
390
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
351
|
-
}, [filterDecision, filterEventType, filterPolicy, filterSessionId, page]);
|
|
391
|
+
}, [filterDecision, filterEventType, filterPolicy, filterSessionId, filterCli, page]);
|
|
352
392
|
|
|
353
|
-
const hasActiveFilters = filterDecision !== "" || filterEventType !== "" || filterPolicy !== "" || filterSessionId !== "";
|
|
393
|
+
const hasActiveFilters = filterDecision !== "" || filterEventType !== "" || filterPolicy !== "" || filterSessionId !== "" || filterCli !== "";
|
|
354
394
|
|
|
355
395
|
const fetchData = useCallback(async (p: number) => {
|
|
356
396
|
try {
|
|
357
|
-
const { filterDecision: fd, filterEventType: fe, filterPolicy: fp, filterSessionId: fs } = filtersRef.current;
|
|
358
|
-
const active = fd !== "" || fe !== "" || fp !== "" || fs !== "";
|
|
397
|
+
const { filterDecision: fd, filterEventType: fe, filterPolicy: fp, filterSessionId: fs, filterCli: fc } = filtersRef.current;
|
|
398
|
+
const active = fd !== "" || fe !== "" || fp !== "" || fs !== "" || fc !== "";
|
|
359
399
|
let result: HookActivityPayload;
|
|
360
400
|
if (active) {
|
|
361
401
|
result = await searchHookActivityAction(
|
|
@@ -364,6 +404,7 @@ function ActivityTab({
|
|
|
364
404
|
eventType: fe || undefined,
|
|
365
405
|
policyName: fp || undefined,
|
|
366
406
|
sessionId: fs || undefined,
|
|
407
|
+
integration: fc || undefined,
|
|
367
408
|
},
|
|
368
409
|
p,
|
|
369
410
|
);
|
|
@@ -394,7 +435,7 @@ function ActivityTab({
|
|
|
394
435
|
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
395
436
|
};
|
|
396
437
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
397
|
-
}, [filterDecision, filterEventType, filterPolicy, filterSessionId]);
|
|
438
|
+
}, [filterDecision, filterEventType, filterPolicy, filterSessionId, filterCli]);
|
|
398
439
|
|
|
399
440
|
const items = data?.entries ?? [];
|
|
400
441
|
const totalPages = data?.totalPages ?? 1;
|
|
@@ -427,6 +468,16 @@ function ActivityTab({
|
|
|
427
468
|
<option value="UserPromptSubmit">UserPromptSubmit</option>
|
|
428
469
|
<option value="PermissionRequest">PermissionRequest</option>
|
|
429
470
|
</select>
|
|
471
|
+
<select
|
|
472
|
+
value={filterCli}
|
|
473
|
+
onChange={(e) => setFilterCli(e.target.value as "" | "claude" | "codex")}
|
|
474
|
+
className="h-7 rounded-md border border-border bg-background px-2 text-xs text-foreground focus:outline-none focus:ring-2 focus:ring-primary/40 focus:border-primary/40 transition-shadow"
|
|
475
|
+
aria-label="Filter by CLI"
|
|
476
|
+
>
|
|
477
|
+
<option value="">All CLIs</option>
|
|
478
|
+
<option value="claude">Claude Code</option>
|
|
479
|
+
<option value="codex">OpenAI Codex</option>
|
|
480
|
+
</select>
|
|
430
481
|
<div className="relative">
|
|
431
482
|
<input
|
|
432
483
|
type="text"
|
|
@@ -548,7 +599,12 @@ function ActivityTab({
|
|
|
548
599
|
<DurationDisplay ms={item.durationMs} />
|
|
549
600
|
</td>
|
|
550
601
|
<td className="px-3 py-2" title={item.sessionId ?? ""}>
|
|
551
|
-
<SessionCell
|
|
602
|
+
<SessionCell
|
|
603
|
+
sessionId={item.sessionId}
|
|
604
|
+
transcriptPath={item.transcriptPath}
|
|
605
|
+
integration={item.integration}
|
|
606
|
+
cwd={item.cwd}
|
|
607
|
+
/>
|
|
552
608
|
</td>
|
|
553
609
|
<td className="px-3 py-2">
|
|
554
610
|
{item.permissionMode ? (
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** Project page — shows metadata and a filterable sessions list for a single project. */
|
|
2
2
|
import { Suspense } from "react";
|
|
3
|
-
import { resolveProjectPath, getCachedSessionFiles } from "@/lib/projects";
|
|
3
|
+
import { resolveProjectPath, getCachedSessionFiles, type SessionFile } from "@/lib/projects";
|
|
4
|
+
import { getCachedCodexSessionsByEncodedName } from "@/lib/codex-projects";
|
|
4
5
|
import { logWarn } from "@/lib/logger";
|
|
5
6
|
import { decodeFolderName } from "@/lib/paths";
|
|
6
7
|
import { notFound } from "next/navigation";
|
|
@@ -8,7 +9,7 @@ import { existsSync } from "fs";
|
|
|
8
9
|
import { stat } from "fs/promises";
|
|
9
10
|
import Link from "next/link";
|
|
10
11
|
import { ArrowLeft } from "lucide-react";
|
|
11
|
-
import { formatDate } from "@/lib/
|
|
12
|
+
import { formatDate } from "@/lib/format-date";
|
|
12
13
|
import SessionsList from "@/app/components/sessions-list";
|
|
13
14
|
|
|
14
15
|
export const dynamic = "force-dynamic";
|
|
@@ -21,34 +22,61 @@ interface ProjectPageProps {
|
|
|
21
22
|
|
|
22
23
|
export default async function ProjectPage({ params }: ProjectPageProps) {
|
|
23
24
|
const { name } = await params;
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
let
|
|
25
|
+
// Resolve under ~/.claude/projects/. Validation may throw RangeError; on bad input
|
|
26
|
+
// we still want to try Codex, since a Codex-only cwd never escapes this check.
|
|
27
|
+
let claudeProjectPath: string | null = null;
|
|
27
28
|
try {
|
|
28
|
-
|
|
29
|
+
claudeProjectPath = resolveProjectPath(name);
|
|
29
30
|
} catch {
|
|
30
|
-
|
|
31
|
+
claudeProjectPath = null;
|
|
31
32
|
}
|
|
32
33
|
const decodedName = decodeFolderName(name);
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
const claudeExists = claudeProjectPath ? existsSync(claudeProjectPath) : false;
|
|
36
|
+
|
|
37
|
+
let claudeSessions: SessionFile[] = [];
|
|
38
|
+
if (claudeExists && claudeProjectPath) {
|
|
39
|
+
claudeSessions = await getCachedSessionFiles(claudeProjectPath);
|
|
40
|
+
}
|
|
41
|
+
// Note: decodeFolderName is lossy when cwds contain `-` (every `-` becomes `/`),
|
|
42
|
+
// so we look up Codex sessions by re-encoding each session's cwd and matching the slug.
|
|
43
|
+
const codex = await getCachedCodexSessionsByEncodedName(name);
|
|
44
|
+
const codexSessions = codex.sessions;
|
|
45
|
+
|
|
46
|
+
if (!claudeExists && codexSessions.length === 0) {
|
|
36
47
|
notFound();
|
|
37
48
|
}
|
|
38
49
|
|
|
39
|
-
//
|
|
50
|
+
// Prefer the canonical Codex cwd when available — `decodeFolderName(name)` is
|
|
51
|
+
// ambiguous for cwds containing `-` (every `-` becomes `/`). Codex transcripts
|
|
52
|
+
// record the literal cwd, so they round-trip correctly.
|
|
53
|
+
const canonicalRoot = codex.cwd ?? decodedName;
|
|
54
|
+
|
|
55
|
+
// Project header metadata
|
|
40
56
|
let lastModified: Date | null = null;
|
|
41
57
|
let lastModifiedFormatted: string | null = null;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
58
|
+
if (claudeExists && claudeProjectPath) {
|
|
59
|
+
try {
|
|
60
|
+
const stats = await stat(claudeProjectPath);
|
|
61
|
+
lastModified = stats.mtime;
|
|
62
|
+
lastModifiedFormatted = formatDate(stats.mtime);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
logWarn(`Failed to get stats for project ${decodedName}:`, error);
|
|
65
|
+
}
|
|
48
66
|
}
|
|
67
|
+
const newestCodex = codexSessions[0]?.lastModified ?? null;
|
|
68
|
+
if (newestCodex && (!lastModified || newestCodex.getTime() > lastModified.getTime())) {
|
|
69
|
+
lastModified = newestCodex;
|
|
70
|
+
lastModifiedFormatted = formatDate(newestCodex);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const sessionFiles: SessionFile[] = [...claudeSessions, ...codexSessions].sort(
|
|
74
|
+
(a, b) => b.lastModified.getTime() - a.lastModified.getTime(),
|
|
75
|
+
);
|
|
49
76
|
|
|
50
|
-
//
|
|
51
|
-
|
|
77
|
+
// Path line: prefer the Claude storage dir if present (matches existing UX);
|
|
78
|
+
// otherwise show the canonical Codex cwd.
|
|
79
|
+
const displayPath = claudeExists && claudeProjectPath ? claudeProjectPath : canonicalRoot;
|
|
52
80
|
|
|
53
81
|
return (
|
|
54
82
|
<main className="min-h-screen bg-background">
|
|
@@ -63,11 +91,11 @@ export default async function ProjectPage({ params }: ProjectPageProps) {
|
|
|
63
91
|
|
|
64
92
|
<div className="mb-8">
|
|
65
93
|
<h1 className="text-4xl font-bold text-foreground mb-2 break-words break-all">
|
|
66
|
-
{
|
|
94
|
+
{canonicalRoot}
|
|
67
95
|
</h1>
|
|
68
96
|
<div className="space-y-1">
|
|
69
97
|
<p className="text-muted-foreground">
|
|
70
|
-
<span className="font-medium">Path:</span> {
|
|
98
|
+
<span className="font-medium">Path:</span> {displayPath}
|
|
71
99
|
</p>
|
|
72
100
|
{lastModifiedFormatted && (
|
|
73
101
|
<p className="text-muted-foreground">
|
|
@@ -80,7 +108,7 @@ export default async function ProjectPage({ params }: ProjectPageProps) {
|
|
|
80
108
|
{/* Sessions Section */}
|
|
81
109
|
<div className="bg-card text-card-foreground rounded-lg border border-border p-6 shadow-sm">
|
|
82
110
|
<h2 className="text-2xl font-semibold mb-4">Sessions</h2>
|
|
83
|
-
|
|
111
|
+
|
|
84
112
|
{sessionFiles.length === 0 ? (
|
|
85
113
|
<div className="text-center py-8">
|
|
86
114
|
<p className="text-muted-foreground mb-2">
|
|
@@ -98,4 +126,3 @@ export default async function ProjectPage({ params }: ProjectPageProps) {
|
|
|
98
126
|
</main>
|
|
99
127
|
);
|
|
100
128
|
}
|
|
101
|
-
|
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
import Link from "next/link";
|
|
3
3
|
import { ArrowLeft, Download } from "lucide-react";
|
|
4
4
|
import { notFound } from "next/navigation";
|
|
5
|
-
import { getCachedSessionLog } from "@/lib/log-entries";
|
|
5
|
+
import { getCachedSessionLog, type LogEntry } from "@/lib/log-entries";
|
|
6
|
+
import { getCachedCodexSessionLog } from "@/lib/codex-sessions";
|
|
6
7
|
import { decodeFolderName } from "@/lib/paths";
|
|
7
8
|
import { baseSessionId } from "@/lib/utils/session-id";
|
|
8
9
|
import { resolveProjectPath, UUID_RE } from "@/lib/projects";
|
|
9
10
|
import LazyLogViewer from "@/app/components/lazy-log-viewer";
|
|
10
11
|
import { CopyButton } from "@/app/components/copy-button";
|
|
12
|
+
import { CliBadge } from "@/app/components/cli-badge";
|
|
11
13
|
|
|
12
14
|
export const dynamic = "force-dynamic";
|
|
13
15
|
|
|
@@ -30,9 +32,11 @@ export default async function SessionPage({ params }: SessionPageProps) {
|
|
|
30
32
|
const decodedSessionId = baseSessionId(sessionId);
|
|
31
33
|
if (!UUID_RE.test(decodedSessionId)) notFound();
|
|
32
34
|
|
|
33
|
-
let entries = null;
|
|
35
|
+
let entries: LogEntry[] | null = null;
|
|
34
36
|
let rawLines: Record<string, unknown>[] | null = null;
|
|
35
37
|
let error: string | null = null;
|
|
38
|
+
let cli: "claude" | "codex" = "claude";
|
|
39
|
+
let codexCwd: string | undefined;
|
|
36
40
|
|
|
37
41
|
try {
|
|
38
42
|
// Use raw folder name for file operations — decodedName is for display only
|
|
@@ -41,28 +45,50 @@ export default async function SessionPage({ params }: SessionPageProps) {
|
|
|
41
45
|
rawLines = result.rawLines;
|
|
42
46
|
} catch (e) {
|
|
43
47
|
const isNotFound = (e as NodeJS.ErrnoException).code === "ENOENT";
|
|
44
|
-
|
|
48
|
+
if (isNotFound) {
|
|
49
|
+
// Fall back to Codex transcripts. Codex stores files at
|
|
50
|
+
// ~/.codex/sessions/<YYYY>/<MM>/<DD>/<file containing sessionId>.jsonl,
|
|
51
|
+
// so the [name] segment is irrelevant — we look up by sessionId.
|
|
52
|
+
const codex = await getCachedCodexSessionLog(decodedSessionId);
|
|
53
|
+
if (codex) {
|
|
54
|
+
entries = codex.entries;
|
|
55
|
+
rawLines = codex.rawLines;
|
|
56
|
+
codexCwd = codex.cwd;
|
|
57
|
+
cli = "codex";
|
|
58
|
+
} else {
|
|
59
|
+
error = "Session log file not found.";
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
error = "Failed to read session log.";
|
|
63
|
+
}
|
|
45
64
|
}
|
|
46
65
|
|
|
66
|
+
const isCodex = cli === "codex";
|
|
67
|
+
const headerLabel = isCodex ? "CLI" : "Project";
|
|
68
|
+
const headerValue = isCodex ? `OpenAI Codex${codexCwd ? ` · ${codexCwd}` : ""}` : decodedName;
|
|
69
|
+
|
|
47
70
|
return (
|
|
48
71
|
<main className="min-h-screen bg-background">
|
|
49
72
|
<div className="container mx-auto p-8">
|
|
50
73
|
<Link
|
|
51
|
-
href={`/project/${encodeURIComponent(name)}`}
|
|
74
|
+
href={isCodex ? "/policies?tab=activity" : `/project/${encodeURIComponent(name)}`}
|
|
52
75
|
className="inline-flex items-center gap-2 text-muted-foreground hover:text-foreground mb-6 transition-colors"
|
|
53
76
|
>
|
|
54
77
|
<ArrowLeft className="w-4 h-4" />
|
|
55
|
-
<span>Back to Sessions</span>
|
|
78
|
+
<span>{isCodex ? "Back to Activity" : "Back to Sessions"}</span>
|
|
56
79
|
</Link>
|
|
57
80
|
|
|
58
81
|
<div className="mb-8">
|
|
59
|
-
<
|
|
60
|
-
|
|
61
|
-
|
|
82
|
+
<div className="flex flex-wrap items-center gap-3 mb-2">
|
|
83
|
+
<h1 className="text-4xl font-bold text-foreground">
|
|
84
|
+
Session Log
|
|
85
|
+
</h1>
|
|
86
|
+
<CliBadge cli={cli} />
|
|
87
|
+
</div>
|
|
62
88
|
<div className="space-y-1">
|
|
63
89
|
<p className="text-muted-foreground">
|
|
64
|
-
<span className="font-medium">
|
|
65
|
-
{
|
|
90
|
+
<span className="font-medium">{headerLabel}:</span>{" "}
|
|
91
|
+
{headerValue}
|
|
66
92
|
</p>
|
|
67
93
|
<p className="text-muted-foreground break-words break-all inline-flex items-center gap-1">
|
|
68
94
|
<span className="font-medium">Session:</span> {decodedSessionId}
|
|
@@ -73,14 +99,16 @@ export default async function SessionPage({ params }: SessionPageProps) {
|
|
|
73
99
|
<p className="text-muted-foreground">
|
|
74
100
|
<span className="font-medium">{rawLines.length}</span> log lines
|
|
75
101
|
</p>
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
102
|
+
{!isCodex && (
|
|
103
|
+
<a
|
|
104
|
+
href={`/api/download/${encodeURIComponent(name)}/${encodeURIComponent(decodedSessionId)}`}
|
|
105
|
+
download
|
|
106
|
+
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-md bg-muted text-muted-foreground hover:bg-muted/80 hover:text-foreground transition-colors"
|
|
107
|
+
>
|
|
108
|
+
<Download className="w-4 h-4" />
|
|
109
|
+
Download Logs
|
|
110
|
+
</a>
|
|
111
|
+
)}
|
|
84
112
|
</div>
|
|
85
113
|
)}
|
|
86
114
|
</div>
|
|
@@ -92,7 +120,11 @@ export default async function SessionPage({ params }: SessionPageProps) {
|
|
|
92
120
|
</div>
|
|
93
121
|
)}
|
|
94
122
|
{!error && entries && (
|
|
95
|
-
<LazyLogViewer
|
|
123
|
+
<LazyLogViewer
|
|
124
|
+
entries={entries}
|
|
125
|
+
projectName={isCodex ? (codexCwd ?? "OpenAI Codex") : decodedName}
|
|
126
|
+
sessionId={decodedSessionId}
|
|
127
|
+
/>
|
|
96
128
|
)}
|
|
97
129
|
</div>
|
|
98
130
|
</main>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"use client";
|
|
3
3
|
|
|
4
4
|
import React, { useState, useCallback } from "react";
|
|
5
|
-
import { GitBranch, Lightbulb, Bug, MessageSquare, ChevronDown, Star, BookOpen } from "lucide-react";
|
|
5
|
+
import { GitBranch, Lightbulb, Bug, MessageSquare, ChevronDown, Star, BookOpen, Hash } from "lucide-react";
|
|
6
6
|
import { Button } from "@/components/ui/button";
|
|
7
7
|
|
|
8
8
|
const GITHUB_REPO = "https://github.com/exospherehost/failproofai";
|
|
@@ -19,6 +19,11 @@ const options = [
|
|
|
19
19
|
icon: BookOpen,
|
|
20
20
|
href: "https://befailproof.ai",
|
|
21
21
|
},
|
|
22
|
+
{
|
|
23
|
+
label: "Join our Slack",
|
|
24
|
+
icon: Hash,
|
|
25
|
+
href: "https://join.slack.com/t/failproofai/shared_invite/zt-3v63b7k5e-O3NBHmj8X6n9gZSGDx6ggQ",
|
|
26
|
+
},
|
|
22
27
|
{
|
|
23
28
|
label: "Request a Feature",
|
|
24
29
|
icon: Lightbulb,
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex (OpenAI) project discovery.
|
|
3
|
+
*
|
|
4
|
+
* Codex transcripts are stored at `~/.codex/sessions/<YYYY>/<MM>/<DD>/rollout-...jsonl`,
|
|
5
|
+
* keyed by date — not by working directory. To list "projects" we scan every transcript,
|
|
6
|
+
* read only the first record (`session_meta`, which carries `payload.cwd`), and group by cwd.
|
|
7
|
+
*
|
|
8
|
+
* The encoded cwd doubles as the URL slug for `/project/[name]`, matching Claude Code's
|
|
9
|
+
* convention (see `encodeFolderName` in `lib/paths.ts`), so a cwd present in both stores
|
|
10
|
+
* naturally produces the same `name` and can be merged on the Claude side.
|
|
11
|
+
*/
|
|
12
|
+
import { open, readdir } from "fs/promises";
|
|
13
|
+
import { homedir } from "os";
|
|
14
|
+
import { join } from "path";
|
|
15
|
+
import { encodeFolderName } from "./paths";
|
|
16
|
+
import type { ProjectFolder, SessionFile } from "./projects";
|
|
17
|
+
import { runtimeCache } from "./runtime-cache";
|
|
18
|
+
import { batchAll } from "./concurrency";
|
|
19
|
+
import { formatDate } from "./format-date";
|
|
20
|
+
import { logWarn } from "./logger";
|
|
21
|
+
|
|
22
|
+
const CODEX_SESSIONS_ROOT = join(homedir(), ".codex", "sessions");
|
|
23
|
+
const SESSION_ID_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
|
|
24
|
+
// session_meta records can be large (base instructions inlined), but a single record
|
|
25
|
+
// is unlikely to exceed a few hundred KB. 256 KB comfortably covers the first line
|
|
26
|
+
// without slurping a full multi-MB transcript.
|
|
27
|
+
const FIRST_LINE_CHUNK_BYTES = 256 * 1024;
|
|
28
|
+
|
|
29
|
+
interface CodexSessionMeta {
|
|
30
|
+
filePath: string;
|
|
31
|
+
fileName: string;
|
|
32
|
+
cwd: string;
|
|
33
|
+
sessionId: string;
|
|
34
|
+
fileMtime: Date;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function safeReaddir(dir: string) {
|
|
38
|
+
try {
|
|
39
|
+
return await readdir(dir, { withFileTypes: true });
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Read the first line of a file without loading the rest. */
|
|
46
|
+
async function readFirstLine(filePath: string): Promise<string | null> {
|
|
47
|
+
let fh: Awaited<ReturnType<typeof open>> | null = null;
|
|
48
|
+
try {
|
|
49
|
+
fh = await open(filePath, "r");
|
|
50
|
+
const buf = Buffer.alloc(FIRST_LINE_CHUNK_BYTES);
|
|
51
|
+
const { bytesRead } = await fh.read(buf, 0, FIRST_LINE_CHUNK_BYTES, 0);
|
|
52
|
+
if (bytesRead === 0) return null;
|
|
53
|
+
const slice = buf.subarray(0, bytesRead);
|
|
54
|
+
const nl = slice.indexOf(0x0a); // '\n'
|
|
55
|
+
const end = nl === -1 ? bytesRead : nl;
|
|
56
|
+
return slice.subarray(0, end).toString("utf-8");
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
} finally {
|
|
60
|
+
if (fh) await fh.close().catch(() => {});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function extractSessionMeta(line: string): { cwd?: string } {
|
|
65
|
+
try {
|
|
66
|
+
const obj = JSON.parse(line) as { type?: string; payload?: { cwd?: unknown } };
|
|
67
|
+
if (obj.type !== "session_meta") return {};
|
|
68
|
+
const cwd = obj.payload?.cwd;
|
|
69
|
+
if (typeof cwd !== "string" || cwd.length === 0) return {};
|
|
70
|
+
return { cwd };
|
|
71
|
+
} catch {
|
|
72
|
+
return {};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function extractSessionId(filename: string): string | null {
|
|
77
|
+
const m = filename.match(SESSION_ID_RE);
|
|
78
|
+
return m ? m[0] : null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function listJsonlFiles(dir: string): Promise<string[]> {
|
|
82
|
+
const entries = await safeReaddir(dir);
|
|
83
|
+
if (!entries) return [];
|
|
84
|
+
return entries
|
|
85
|
+
.filter((e) => e.isFile() && e.name.endsWith(".jsonl"))
|
|
86
|
+
.map((e) => join(dir, e.name));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Walk `~/.codex/sessions/<Y>/<M>/<D>/` for every `*.jsonl` transcript and read each
|
|
91
|
+
* file's first record to extract `cwd`. Files lacking a parsable `session_meta` or a
|
|
92
|
+
* UUID-looking sessionId in the filename are skipped.
|
|
93
|
+
*/
|
|
94
|
+
async function scanCodexSessions(): Promise<CodexSessionMeta[]> {
|
|
95
|
+
const yearDirs = await safeReaddir(CODEX_SESSIONS_ROOT);
|
|
96
|
+
if (!yearDirs) return [];
|
|
97
|
+
|
|
98
|
+
const filePaths: string[] = [];
|
|
99
|
+
for (const y of yearDirs) {
|
|
100
|
+
if (!y.isDirectory()) continue;
|
|
101
|
+
const monthDirs = await safeReaddir(join(CODEX_SESSIONS_ROOT, y.name));
|
|
102
|
+
if (!monthDirs) continue;
|
|
103
|
+
for (const m of monthDirs) {
|
|
104
|
+
if (!m.isDirectory()) continue;
|
|
105
|
+
const dayDirs = await safeReaddir(join(CODEX_SESSIONS_ROOT, y.name, m.name));
|
|
106
|
+
if (!dayDirs) continue;
|
|
107
|
+
for (const d of dayDirs) {
|
|
108
|
+
if (!d.isDirectory()) continue;
|
|
109
|
+
const dayPath = join(CODEX_SESSIONS_ROOT, y.name, m.name, d.name);
|
|
110
|
+
filePaths.push(...(await listJsonlFiles(dayPath)));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (filePaths.length === 0) return [];
|
|
116
|
+
|
|
117
|
+
const settled = await batchAll(
|
|
118
|
+
filePaths.map((filePath) => async (): Promise<CodexSessionMeta | null> => {
|
|
119
|
+
const sessionId = extractSessionId(filePath.split("/").pop() ?? "");
|
|
120
|
+
if (!sessionId) return null;
|
|
121
|
+
const line = await readFirstLine(filePath);
|
|
122
|
+
if (!line) return null;
|
|
123
|
+
const { cwd } = extractSessionMeta(line);
|
|
124
|
+
if (!cwd) return null;
|
|
125
|
+
let fileMtime: Date;
|
|
126
|
+
try {
|
|
127
|
+
const fh = await open(filePath, "r");
|
|
128
|
+
try {
|
|
129
|
+
const stat = await fh.stat();
|
|
130
|
+
fileMtime = stat.mtime;
|
|
131
|
+
} finally {
|
|
132
|
+
await fh.close().catch(() => {});
|
|
133
|
+
}
|
|
134
|
+
} catch {
|
|
135
|
+
fileMtime = new Date(0);
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
filePath,
|
|
139
|
+
fileName: filePath.split("/").pop() ?? "",
|
|
140
|
+
cwd,
|
|
141
|
+
sessionId,
|
|
142
|
+
fileMtime,
|
|
143
|
+
};
|
|
144
|
+
}),
|
|
145
|
+
16,
|
|
146
|
+
);
|
|
147
|
+
return settled
|
|
148
|
+
.filter((r): r is PromiseFulfilledResult<CodexSessionMeta | null> => r.status === "fulfilled")
|
|
149
|
+
.map((r) => r.value)
|
|
150
|
+
.filter((v): v is CodexSessionMeta => v !== null);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const cachedScan = runtimeCache(scanCodexSessions, 30);
|
|
154
|
+
|
|
155
|
+
/** Returns one ProjectFolder per unique cwd discovered in Codex transcripts. */
|
|
156
|
+
export async function getCodexProjects(): Promise<ProjectFolder[]> {
|
|
157
|
+
let metas: CodexSessionMeta[];
|
|
158
|
+
try {
|
|
159
|
+
metas = await cachedScan();
|
|
160
|
+
} catch (error) {
|
|
161
|
+
logWarn("Failed to scan Codex sessions:", error);
|
|
162
|
+
return [];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const byCwd = new Map<string, { latest: Date; cwd: string }>();
|
|
166
|
+
for (const m of metas) {
|
|
167
|
+
const existing = byCwd.get(m.cwd);
|
|
168
|
+
if (!existing || m.fileMtime.getTime() > existing.latest.getTime()) {
|
|
169
|
+
byCwd.set(m.cwd, { latest: m.fileMtime, cwd: m.cwd });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const folders: ProjectFolder[] = [];
|
|
174
|
+
for (const { cwd, latest } of byCwd.values()) {
|
|
175
|
+
folders.push({
|
|
176
|
+
name: encodeFolderName(cwd),
|
|
177
|
+
path: cwd,
|
|
178
|
+
isDirectory: true,
|
|
179
|
+
lastModified: latest,
|
|
180
|
+
lastModifiedFormatted: formatDate(latest),
|
|
181
|
+
cli: ["codex"],
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
folders.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
|
|
185
|
+
return folders;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function metasToSessionFiles(metas: CodexSessionMeta[]): SessionFile[] {
|
|
189
|
+
const files: SessionFile[] = metas.map((m) => ({
|
|
190
|
+
name: m.fileName.replace(/\.jsonl$/, ""),
|
|
191
|
+
path: m.filePath,
|
|
192
|
+
lastModified: m.fileMtime,
|
|
193
|
+
lastModifiedFormatted: formatDate(m.fileMtime),
|
|
194
|
+
sessionId: m.sessionId,
|
|
195
|
+
cli: "codex",
|
|
196
|
+
}));
|
|
197
|
+
files.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
|
|
198
|
+
return files;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Returns SessionFile entries for every Codex transcript whose cwd matches `cwd` exactly. */
|
|
202
|
+
export async function getCodexSessionsForCwd(cwd: string): Promise<SessionFile[]> {
|
|
203
|
+
let metas: CodexSessionMeta[];
|
|
204
|
+
try {
|
|
205
|
+
metas = await cachedScan();
|
|
206
|
+
} catch (error) {
|
|
207
|
+
logWarn("Failed to scan Codex sessions:", error);
|
|
208
|
+
return [];
|
|
209
|
+
}
|
|
210
|
+
return metasToSessionFiles(metas.filter((m) => m.cwd === cwd));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export interface CodexProjectByName {
|
|
214
|
+
/** Original cwd recovered from the Codex transcripts (canonical, not the lossy decode). */
|
|
215
|
+
cwd: string | null;
|
|
216
|
+
sessions: SessionFile[];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Looks up Codex sessions for a project URL slug. `decodeFolderName` is lossy
|
|
221
|
+
* (every `-` becomes `/`), so we cannot recover the original cwd from the slug —
|
|
222
|
+
* instead we re-encode each session's cwd and match in that direction. Returns
|
|
223
|
+
* both the canonical cwd (first match wins) and the matching sessions.
|
|
224
|
+
*/
|
|
225
|
+
export async function getCodexSessionsByEncodedName(name: string): Promise<CodexProjectByName> {
|
|
226
|
+
let metas: CodexSessionMeta[];
|
|
227
|
+
try {
|
|
228
|
+
metas = await cachedScan();
|
|
229
|
+
} catch (error) {
|
|
230
|
+
logWarn("Failed to scan Codex sessions:", error);
|
|
231
|
+
return { cwd: null, sessions: [] };
|
|
232
|
+
}
|
|
233
|
+
const matches = metas.filter((m) => encodeFolderName(m.cwd) === name);
|
|
234
|
+
return {
|
|
235
|
+
cwd: matches[0]?.cwd ?? null,
|
|
236
|
+
sessions: metasToSessionFiles(matches),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export const getCachedCodexProjects = runtimeCache(getCodexProjects, 30);
|
|
241
|
+
export const getCachedCodexSessionsForCwd = runtimeCache(
|
|
242
|
+
(cwd: string) => getCodexSessionsForCwd(cwd),
|
|
243
|
+
30,
|
|
244
|
+
{ maxSize: 50 },
|
|
245
|
+
);
|
|
246
|
+
export const getCachedCodexSessionsByEncodedName = runtimeCache(
|
|
247
|
+
(name: string) => getCodexSessionsByEncodedName(name),
|
|
248
|
+
30,
|
|
249
|
+
{ maxSize: 50 },
|
|
250
|
+
);
|