failproofai 0.0.9-beta.1 → 0.0.9-beta.2

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 (155) 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]__092s1ta._.js +2 -2
  68. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__09icjsf._.js +2 -2
  69. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0.f_cyx._.js → [root-of-the-server]__0eu4j_n._.js} +2 -2
  70. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0g.lg8b._.js +2 -2
  71. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0gs6wz4._.js +3 -0
  72. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h..k-e._.js +2 -2
  73. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0it81ys._.js +3 -0
  74. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u4a9jq._.js +4 -0
  75. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0dub28-._.js → [root-of-the-server]__0vrlxf2._.js} +2 -2
  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/03egp37o1l629.js +1 -0
  95. package/.next/standalone/.next/static/chunks/06j6c0ofqjy0v.js +6 -0
  96. package/.next/standalone/.next/static/chunks/06x4-d1~o-opr.js +1 -0
  97. package/.next/standalone/.next/static/chunks/{0jryicwtm9z2g.js → 0e8c_1f7-8e7t.js} +3 -3
  98. package/.next/standalone/.next/static/chunks/0mumk7h5i.1xd.js +1 -0
  99. package/.next/standalone/.next/static/chunks/0n~s0gafwnp2y.js +1 -0
  100. package/.next/standalone/.next/static/chunks/{0bghqwo4iloy0.js → 0zdn~84f58hvf.js} +1 -1
  101. package/.next/standalone/.next/static/chunks/0zig0fh30t6ou.js +1 -0
  102. package/.next/standalone/.next/static/chunks/{0z-jh701rc~j8.js → 10mlwc4y_kqo2.js} +1 -1
  103. package/.next/standalone/.next/static/chunks/{0kzk5-mh1_x53.js → 12simlrcfk3g2.js} +1 -1
  104. package/.next/standalone/.next/static/chunks/{0w1f.k~gi-y6..js → 15_mi91qaeieu.js} +1 -1
  105. package/.next/standalone/.next/static/chunks/{0ufq8smh~i7wc.js → 18a9xv2p3~x.9.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/0p5zh2diw90a1.js +0 -1
  152. package/.next/standalone/.next/static/chunks/0vwqucikost_q.js +0 -1
  153. /package/.next/standalone/.next/static/{CiVeb_yiVt-O2JYrzGzB7 → SyaO-J1hupjAiRCG-Syzg}/_buildManifest.js +0 -0
  154. /package/.next/standalone/.next/static/{CiVeb_yiVt-O2JYrzGzB7 → SyaO-J1hupjAiRCG-Syzg}/_clientMiddlewareManifest.js +0 -0
  155. /package/.next/standalone/.next/static/{CiVeb_yiVt-O2JYrzGzB7 → SyaO-J1hupjAiRCG-Syzg}/_ssgManifest.js +0 -0
@@ -6,7 +6,7 @@
6
6
 
7
7
  import { useState, useMemo, useEffect, useRef } from "react";
8
8
  import { SessionFile } from "@/lib/projects";
9
- import { formatDate } from "@/lib/utils";
9
+ import { formatDate } from "@/lib/format-date";
10
10
  import {
11
11
  FILTER_PRESETS,
12
12
  ITEMS_PER_PAGE,
@@ -25,6 +25,7 @@ import Link from "next/link";
25
25
  import PaginationControls from "./pagination-controls";
26
26
  import DatePickerInput from "./date-picker-input";
27
27
  import { CopyButton } from "./copy-button";
28
+ import { CliBadge } from "./cli-badge";
28
29
 
29
30
 
30
31
  interface SessionsListProps {
@@ -190,7 +191,7 @@ export default function SessionsList({ files, projectName }: SessionsListProps)
190
191
  <File className="w-5 h-5 text-primary" />
191
192
  </td>
192
193
  <td className="px-4 py-3 max-w-md">
193
- <div className="flex items-center gap-1">
194
+ <div className="flex flex-wrap items-center gap-2">
194
195
  {file.sessionId ? (
195
196
  <>
196
197
  <Link
@@ -206,6 +207,7 @@ export default function SessionsList({ files, projectName }: SessionsListProps)
206
207
  {file.name.replace(/\.jsonl$/, "")}
207
208
  </span>
208
209
  )}
210
+ {file.cli && <CliBadge cli={file.cli} />}
209
211
  </div>
210
212
  </td>
211
213
  <td className="px-4 py-3 text-sm text-muted-foreground">
@@ -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,