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.
Files changed (154) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +6 -6
  3. package/.next/standalone/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +3 -3
  6. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  11. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
  12. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  13. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
  14. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
  15. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  16. package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +3 -3
  17. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  18. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  19. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  20. package/.next/standalone/.next/server/app/_not-found.html +2 -2
  21. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  22. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  23. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  24. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
  25. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  26. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  27. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  28. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js +1 -2
  29. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/index.html +1 -1
  31. package/.next/standalone/.next/server/app/index.rsc +15 -15
  32. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  33. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  34. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
  35. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +10 -10
  36. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  37. package/.next/standalone/.next/server/app/page/build-manifest.json +3 -3
  38. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  39. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  41. package/.next/standalone/.next/server/app/policies/page/build-manifest.json +3 -3
  42. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +8 -8
  43. package/.next/standalone/.next/server/app/policies/page.js +2 -2
  44. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  45. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  46. package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +3 -3
  47. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  48. package/.next/standalone/.next/server/app/project/[name]/page.js +1 -1
  49. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  50. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  51. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +3 -3
  52. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  53. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  54. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +4 -3
  55. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  56. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  57. package/.next/standalone/.next/server/app/projects/page/build-manifest.json +3 -3
  58. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  59. package/.next/standalone/.next/server/app/projects/page.js +2 -2
  60. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  61. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  62. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0g72weg._.js +1 -1
  63. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0su~k6f._.js +3 -0
  64. package/.next/standalone/.next/server/chunks/lib_codex-projects_ts_07qqk1g._.js +3 -0
  65. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_0z7w.hh._.js +1 -1
  66. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01743wx._.js +3 -0
  67. 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
  68. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__092s1ta._.js +2 -2
  69. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__09icjsf._.js +2 -2
  70. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0dub28-._.js → [root-of-the-server]__0_b7pgn._.js} +2 -2
  71. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0g.lg8b._.js +2 -2
  72. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0gs6wz4._.js +3 -0
  73. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h..k-e._.js +2 -2
  74. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0it81ys._.js +3 -0
  75. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u4a9jq._.js +4 -0
  76. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ymlddl._.js +18 -0
  77. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11pa2ra._.js +2 -2
  78. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12.h2mg._.js +3 -0
  79. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12t-wym._.js +2 -2
  80. package/.next/standalone/.next/server/chunks/ssr/_04w00cm._.js +3 -0
  81. package/.next/standalone/.next/server/chunks/ssr/_10lm7or._.js +2 -2
  82. package/.next/standalone/.next/server/chunks/ssr/app_0cdqd9w._.js +3 -0
  83. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_0xerkr6._.js +1 -1
  84. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_0q-m0y-._.js +1 -1
  85. package/.next/standalone/.next/server/chunks/ssr/lib_codex-projects_ts_0eosib~._.js +3 -0
  86. package/.next/standalone/.next/server/chunks/ssr/lib_utils_ts_068jk73._.js +3 -0
  87. package/.next/standalone/.next/server/chunks/ssr/{_0zaq1hm._.js → node_modules_0ttbz1~._.js} +2 -2
  88. package/.next/standalone/.next/server/middleware-build-manifest.js +6 -6
  89. package/.next/standalone/.next/server/pages/404.html +2 -2
  90. package/.next/standalone/.next/server/pages/500.html +1 -1
  91. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  92. package/.next/standalone/.next/server/server-reference-manifest.json +9 -9
  93. package/.next/standalone/.next/static/chunks/{0_c_yox08g_44.js → 01q52wg_amm60.js} +2 -2
  94. package/.next/standalone/.next/static/chunks/06x4-d1~o-opr.js +1 -0
  95. package/.next/standalone/.next/static/chunks/{0bghqwo4iloy0.js → 0756i.7omnnl6.js} +1 -1
  96. package/.next/standalone/.next/static/chunks/{0p5zh2diw90a1.js → 095l4hc7-h.~~.js} +1 -1
  97. package/.next/standalone/.next/static/chunks/09ose_165ra4d.js +1 -0
  98. package/.next/standalone/.next/static/chunks/0n-_j_6fo6jex.js +6 -0
  99. package/.next/standalone/.next/static/chunks/0n~s0gafwnp2y.js +1 -0
  100. package/.next/standalone/.next/static/chunks/{0ufq8smh~i7wc.js → 0pr7k36o_.du1.js} +1 -1
  101. package/.next/standalone/.next/static/chunks/{0z-jh701rc~j8.js → 0t~iusm_fxoao.js} +1 -1
  102. package/.next/standalone/.next/static/chunks/{0jryicwtm9z2g.js → 0u-ys71jc4y68.js} +3 -3
  103. package/.next/standalone/.next/static/chunks/0zig0fh30t6ou.js +1 -0
  104. package/.next/standalone/.next/static/chunks/{0w1f.k~gi-y6..js → 11kt_9zaooda3.js} +1 -1
  105. package/.next/standalone/.next/static/chunks/{0kzk5-mh1_x53.js → 12simlrcfk3g2.js} +1 -1
  106. package/.next/standalone/.next/static/chunks/{turbopack-0s36is87fc9r2.js → turbopack-0o7k.hakttp4k.js} +1 -1
  107. package/.next/standalone/app/components/cli-badge.tsx +24 -0
  108. package/.next/standalone/app/components/project-list.tsx +13 -7
  109. package/.next/standalone/app/components/sessions-list.tsx +4 -2
  110. package/.next/standalone/app/policies/hooks-client.tsx +66 -10
  111. package/.next/standalone/app/project/[name]/page.tsx +49 -22
  112. package/.next/standalone/app/project/[name]/session/[sessionId]/page.tsx +51 -19
  113. package/.next/standalone/components/reach-developers.tsx +6 -1
  114. package/.next/standalone/lib/codex-projects.ts +250 -0
  115. package/.next/standalone/lib/codex-sessions.ts +414 -0
  116. package/.next/standalone/lib/format-date.ts +21 -0
  117. package/.next/standalone/lib/log-entries.ts +3 -3
  118. package/.next/standalone/lib/paths.ts +13 -0
  119. package/.next/standalone/lib/projects.ts +57 -3
  120. package/.next/standalone/lib/utils.ts +6 -22
  121. package/.next/standalone/package.json +1 -1
  122. package/.next/standalone/server.js +1 -1
  123. package/bin/failproofai.mjs +1 -0
  124. package/dist/cli.mjs +1042 -122
  125. package/lib/codex-projects.ts +250 -0
  126. package/lib/codex-sessions.ts +414 -0
  127. package/lib/format-date.ts +21 -0
  128. package/lib/log-entries.ts +3 -3
  129. package/lib/paths.ts +13 -0
  130. package/lib/projects.ts +57 -3
  131. package/lib/utils.ts +6 -22
  132. package/package.json +1 -1
  133. package/scripts/launch.ts +2 -1
  134. package/src/hooks/builtin-policies.ts +7 -1
  135. package/src/hooks/hook-activity-store.ts +3 -0
  136. package/src/hooks/manager.ts +1 -1
  137. package/src/hooks/resolve-permission-mode.ts +6 -91
  138. package/.next/standalone/.next/server/chunks/[externals]__080wern._.js +0 -3
  139. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0b57.gk._.js +0 -3
  140. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__03rd.z8._.js +0 -3
  141. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0e74wa-._.js +0 -3
  142. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0vu.o-3._.js +0 -4
  143. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0w6l33k._.js +0 -17
  144. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0zqcovi._.js +0 -3
  145. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__105.l_7._.js +0 -3
  146. package/.next/standalone/.next/server/chunks/ssr/_0uy6m~m._.js +0 -3
  147. package/.next/standalone/.next/static/chunks/00b5h4r1el.6f.js +0 -1
  148. package/.next/standalone/.next/static/chunks/0fw2h.g66c0h3.js +0 -1
  149. package/.next/standalone/.next/static/chunks/0gu87mlr5ssnt.js +0 -6
  150. package/.next/standalone/.next/static/chunks/0igf3xbisp1lx.js +0 -1
  151. package/.next/standalone/.next/static/chunks/0vwqucikost_q.js +0 -1
  152. /package/.next/standalone/.next/static/{CiVeb_yiVt-O2JYrzGzB7 → A_Ax17P33facL0OmIwFXj}/_buildManifest.js +0 -0
  153. /package/.next/standalone/.next/static/{CiVeb_yiVt-O2JYrzGzB7 → A_Ax17P33facL0OmIwFXj}/_clientMiddlewareManifest.js +0 -0
  154. /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 SessionCell({ sessionId, transcriptPath }: { sessionId?: string; transcriptPath?: string }) {
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 sessionId={item.sessionId} transcriptPath={item.transcriptPath} />
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/utils";
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
- // Next.js already decodes route params once; resolveProjectPath validates and
25
- // canonicalizes, throwing RangeError if the path escapes the projects root.
26
- let projectPath: string;
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
- projectPath = resolveProjectPath(name);
29
+ claudeProjectPath = resolveProjectPath(name);
29
30
  } catch {
30
- notFound();
31
+ claudeProjectPath = null;
31
32
  }
32
33
  const decodedName = decodeFolderName(name);
33
34
 
34
- // Check if project exists
35
- if (!existsSync(projectPath)) {
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
- // Get project stats for last modified date
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
- try {
43
- const stats = await stat(projectPath);
44
- lastModified = stats.mtime;
45
- lastModifiedFormatted = formatDate(stats.mtime);
46
- } catch (error) {
47
- logWarn(`Failed to get stats for project ${decodedName}:`, error);
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
- // Get session files
51
- const sessionFiles = await getCachedSessionFiles(projectPath);
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
- {decodedName}
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> {projectPath}
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
- error = isNotFound ? "Session log file not found." : "Failed to read session log.";
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
- <h1 className="text-4xl font-bold text-foreground mb-2">
60
- Session Log
61
- </h1>
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">Project:</span>{" "}
65
- {decodedName}
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
- <a
77
- href={`/api/download/${encodeURIComponent(name)}/${encodeURIComponent(decodedSessionId)}`}
78
- download
79
- 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"
80
- >
81
- <Download className="w-4 h-4" />
82
- Download Logs
83
- </a>
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 entries={entries} projectName={decodedName} sessionId={decodedSessionId} />
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
+ );