failproofai 0.0.9 → 0.0.10-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/.next/standalone/.cursor/hooks.json +47 -0
  2. package/.next/standalone/.gemini/settings.json +147 -0
  3. package/.next/standalone/.next/BUILD_ID +1 -1
  4. package/.next/standalone/.next/build-manifest.json +3 -3
  5. package/.next/standalone/.next/prerender-manifest.json +3 -3
  6. package/.next/standalone/.next/required-server-files.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page.js +1 -1
  9. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  10. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  11. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  12. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  13. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
  14. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  15. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
  16. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
  17. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  18. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  19. package/.next/standalone/.next/server/app/_not-found/page.js +1 -1
  20. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  21. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  22. package/.next/standalone/.next/server/app/_not-found.html +2 -2
  23. package/.next/standalone/.next/server/app/_not-found.rsc +17 -17
  24. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +17 -17
  25. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  26. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +11 -11
  27. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  28. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  29. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  30. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js +2 -1
  31. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/index.html +1 -1
  33. package/.next/standalone/.next/server/app/index.rsc +16 -16
  34. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  35. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +16 -16
  36. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
  37. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +11 -11
  38. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  39. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  40. package/.next/standalone/.next/server/app/page.js +1 -1
  41. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  43. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +8 -8
  44. package/.next/standalone/.next/server/app/policies/page.js +1 -1
  45. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  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 +2 -2
  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/react-loadable-manifest.json +2 -2
  52. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  53. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +5 -5
  54. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  55. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  56. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  57. package/.next/standalone/.next/server/app/projects/page.js +2 -2
  58. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  59. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  60. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0.~nmr9._.js +3 -0
  61. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0yspgjy._.js → [root-of-the-server]__010i6f5._.js} +2 -2
  62. package/.next/standalone/.next/server/chunks/[root-of-the-server]__08px0ym._.js +3 -0
  63. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0b57.gk._.js +3 -0
  64. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0dtn9lr._.js +3 -0
  65. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0kjo7d_._.js +1 -1
  66. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0vlhtkc._.js +3 -0
  67. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0wu7fr7._.js +3 -0
  68. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0yfq1yr._.js +3 -0
  69. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0z4c5dj._.js +3 -0
  70. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0zso~62._.js +3 -0
  71. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_0z7w.hh._.js +1 -1
  72. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0-2wr.c._.js +4 -0
  73. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0.~m-w2._.js +4 -0
  74. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__09icjsf._.js → [root-of-the-server]__0709m8.._.js} +3 -3
  75. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0bz245.._.js +4 -0
  76. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0dl0kgt._.js +4 -0
  77. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0gmhxyo._.js +4 -0
  78. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0mup1hi._.js +3 -0
  79. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ohb3gc._.js +4 -0
  80. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qbpe_v._.js +3 -0
  81. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s~gy6y._.js +3 -0
  82. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0t5l7a5._.js +3 -0
  83. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ymlddl._.js +152 -6
  84. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0_b7pgn._.js → [root-of-the-server]__0ymn496._.js} +2 -2
  85. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__01g_w_e._.js → [root-of-the-server]__10h.ggz._.js} +2 -2
  86. package/.next/standalone/.next/server/chunks/ssr/_03d7qyt._.js +3 -0
  87. package/.next/standalone/.next/server/chunks/ssr/{_07a1g.3._.js → _0zx~s__._.js} +2 -2
  88. package/.next/standalone/.next/server/chunks/ssr/_10lm7or._.js +2 -2
  89. package/.next/standalone/.next/server/chunks/ssr/app_0cdqd9w._.js +1 -1
  90. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_0xerkr6._.js +1 -1
  91. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_0q-m0y-._.js +2 -2
  92. package/.next/standalone/.next/server/chunks/ssr/lib_codex-projects_ts_0eosib~._.js +1 -1
  93. package/.next/standalone/.next/server/chunks/ssr/lib_copilot-projects_ts_0r8xkn8._.js +3 -0
  94. package/.next/standalone/.next/server/chunks/ssr/lib_cursor-projects_ts_0qt1scg._.js +3 -0
  95. package/.next/standalone/.next/server/chunks/ssr/lib_gemini-projects_ts_0sl~yqr._.js +3 -0
  96. package/.next/standalone/.next/server/chunks/ssr/lib_opencode-projects_ts_0op9gyp._.js +3 -0
  97. package/.next/standalone/.next/server/chunks/ssr/lib_pi-projects_ts_103tsh1._.js +3 -0
  98. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  99. package/.next/standalone/.next/server/pages/404.html +2 -2
  100. package/.next/standalone/.next/server/pages/500.html +1 -1
  101. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  102. package/.next/standalone/.next/server/server-reference-manifest.json +9 -9
  103. package/.next/standalone/.next/static/chunks/{0n-_j_6fo6jex.js → 00ay03h8bq4b~.js} +2 -2
  104. package/.next/standalone/.next/static/chunks/{11kt_9zaooda3.js → 0agmlhk5ml7x5.js} +1 -1
  105. package/.next/standalone/.next/static/chunks/0bi2r.m~yokoo.js +1 -0
  106. package/.next/standalone/.next/static/chunks/{095l4hc7-h.~~.js → 0en4v5k2nnxks.js} +1 -1
  107. package/.next/standalone/.next/static/chunks/0q5bmqop--9yk.js +1 -0
  108. package/.next/standalone/.next/static/chunks/{0756i.7omnnl6.js → 0s6nux54y~l~r.js} +1 -1
  109. package/.next/standalone/.next/static/chunks/{0t~iusm_fxoao.js → 0tpse0wu2wwo0.js} +1 -1
  110. package/.next/standalone/.next/static/chunks/12po2vpc-4_c1.css +1 -0
  111. package/.next/standalone/.next/static/chunks/{0u-ys71jc4y68.js → 1400rtd5ywbt..js} +2 -2
  112. package/.next/standalone/.next/static/chunks/{09ose_165ra4d.js → 14lmf8boay-zu.js} +1 -1
  113. package/.next/standalone/.next/static/chunks/{0pr7k36o_.du1.js → 17htukxga7bil.js} +1 -1
  114. package/.next/standalone/.opencode/opencode.json +4 -0
  115. package/.next/standalone/.opencode/plugins/failproofai.mjs +131 -0
  116. package/.next/standalone/.pi/settings.json +5 -0
  117. package/.next/standalone/app/components/cli-badge.tsx +7 -11
  118. package/.next/standalone/app/components/project-list.tsx +32 -4
  119. package/.next/standalone/app/policies/hooks-client.tsx +31 -15
  120. package/.next/standalone/app/project/[name]/page.tsx +52 -16
  121. package/.next/standalone/app/project/[name]/session/[sessionId]/page.tsx +92 -15
  122. package/.next/standalone/assets/logos/copilot-dark.svg +1 -0
  123. package/.next/standalone/assets/logos/copilot-light.svg +1 -0
  124. package/.next/standalone/assets/logos/cursor-dark.svg +1 -0
  125. package/.next/standalone/assets/logos/cursor-light.svg +1 -0
  126. package/.next/standalone/assets/logos/gemini-dark.svg +13 -0
  127. package/.next/standalone/assets/logos/gemini-light.svg +13 -0
  128. package/.next/standalone/assets/logos/opencode-dark.svg +1 -0
  129. package/.next/standalone/assets/logos/opencode-light.svg +1 -0
  130. package/.next/standalone/assets/logos/pi-dark.svg +7 -0
  131. package/.next/standalone/assets/logos/pi-light.svg +7 -0
  132. package/.next/standalone/lib/cli-registry.ts +107 -0
  133. package/.next/standalone/lib/codex-projects.ts +3 -3
  134. package/.next/standalone/lib/copilot-projects.ts +224 -0
  135. package/.next/standalone/lib/copilot-sessions.ts +395 -0
  136. package/.next/standalone/lib/cursor-projects.ts +312 -0
  137. package/.next/standalone/lib/cursor-sessions.ts +467 -0
  138. package/.next/standalone/lib/gemini-projects.ts +203 -0
  139. package/.next/standalone/lib/gemini-sessions.ts +365 -0
  140. package/.next/standalone/lib/opencode-projects.ts +232 -0
  141. package/.next/standalone/lib/opencode-sessions.ts +237 -0
  142. package/.next/standalone/lib/pi-projects.ts +230 -0
  143. package/.next/standalone/lib/pi-sessions.ts +325 -0
  144. package/.next/standalone/lib/projects.ts +67 -31
  145. package/.next/standalone/next.config.ts +5 -4
  146. package/.next/standalone/package.json +2 -1
  147. package/.next/standalone/pi-extension/index.ts +373 -0
  148. package/.next/standalone/pi-extension/package.json +12 -0
  149. package/.next/standalone/server.js +1 -1
  150. package/README.md +37 -3
  151. package/bin/failproofai.mjs +61 -21
  152. package/dist/cli.mjs +2248 -246
  153. package/lib/cli-registry.ts +107 -0
  154. package/lib/codex-projects.ts +3 -3
  155. package/lib/copilot-projects.ts +224 -0
  156. package/lib/copilot-sessions.ts +395 -0
  157. package/lib/cursor-projects.ts +312 -0
  158. package/lib/cursor-sessions.ts +467 -0
  159. package/lib/gemini-projects.ts +203 -0
  160. package/lib/gemini-sessions.ts +365 -0
  161. package/lib/opencode-projects.ts +232 -0
  162. package/lib/opencode-sessions.ts +237 -0
  163. package/lib/pi-projects.ts +230 -0
  164. package/lib/pi-sessions.ts +325 -0
  165. package/lib/projects.ts +67 -31
  166. package/package.json +2 -1
  167. package/pi-extension/index.ts +373 -0
  168. package/pi-extension/package.json +12 -0
  169. package/scripts/translate-docs/mdx-translator.ts +56 -2
  170. package/scripts/translate-docs/translator.ts +1 -1
  171. package/src/hooks/builtin-policies.ts +84 -14
  172. package/src/hooks/handler.ts +67 -5
  173. package/src/hooks/install-prompt.ts +33 -10
  174. package/src/hooks/integrations.ts +1007 -6
  175. package/src/hooks/policy-evaluator.ts +299 -3
  176. package/src/hooks/resolve-permission-mode.ts +23 -0
  177. package/src/hooks/types.ts +307 -3
  178. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0g72weg._.js +0 -3
  179. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0su~k6f._.js +0 -3
  180. package/.next/standalone/.next/server/chunks/lib_codex-projects_ts_07qqk1g._.js +0 -3
  181. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01743wx._.js +0 -3
  182. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__092s1ta._.js +0 -4
  183. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0g.lg8b._.js +0 -4
  184. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0gs6wz4._.js +0 -3
  185. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h..k-e._.js +0 -4
  186. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0it81ys._.js +0 -3
  187. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u4a9jq._.js +0 -4
  188. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11pa2ra._.js +0 -4
  189. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12.h2mg._.js +0 -3
  190. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12t-wym._.js +0 -4
  191. package/.next/standalone/.next/server/chunks/ssr/_04w00cm._.js +0 -3
  192. package/.next/standalone/.next/static/chunks/0.rk1iwdt1d7c.css +0 -1
  193. package/.next/standalone/.next/static/chunks/06x4-d1~o-opr.js +0 -1
  194. package/.next/standalone/.next/static/chunks/0n~s0gafwnp2y.js +0 -1
  195. /package/.next/standalone/.next/static/{A_Ax17P33facL0OmIwFXj → 68TLSFdjAQYIulNHfP0QY}/_buildManifest.js +0 -0
  196. /package/.next/standalone/.next/static/{A_Ax17P33facL0OmIwFXj → 68TLSFdjAQYIulNHfP0QY}/_clientMiddlewareManifest.js +0 -0
  197. /package/.next/standalone/.next/static/{A_Ax17P33facL0OmIwFXj → 68TLSFdjAQYIulNHfP0QY}/_ssgManifest.js +0 -0
@@ -27,6 +27,7 @@ import { updatePolicyParamsAction } from "@/app/actions/update-policy-params";
27
27
  import { useAutoRefresh } from "@/contexts/AutoRefreshContext";
28
28
  import { useUrlParams } from "@/lib/use-url-params";
29
29
  import { pageToParam, paramToPage } from "@/lib/url-filter-serializers";
30
+ import { getCliLabel, getCliBadgeClasses, KNOWN_CLI_IDS, isKnownCli, type CliId } from "@/lib/cli-registry";
30
31
  import { formatRelativeTime } from "@/lib/format-duration";
31
32
  import { Button } from "@/components/ui/button";
32
33
 
@@ -86,10 +87,24 @@ function SessionCell({
86
87
  const short = shortenSession(sessionId);
87
88
 
88
89
  const isCodex = integration === "codex" || (transcriptPath?.includes("/.codex/") ?? false);
89
- if (isCodex) {
90
+ const isCopilot =
91
+ integration === "copilot" ||
92
+ (transcriptPath?.includes("/.copilot/session-state/") ?? false);
93
+ const isCursor =
94
+ integration === "cursor" || (transcriptPath?.includes("/.cursor/") ?? false);
95
+ const isOpenCode =
96
+ integration === "opencode" ||
97
+ (transcriptPath?.includes("/.local/share/opencode/") ?? false) ||
98
+ (transcriptPath?.includes("/.opencode/") ?? false);
99
+ const isPi =
100
+ integration === "pi" || (transcriptPath?.includes("/.pi/") ?? false);
101
+ const isGemini =
102
+ integration === "gemini" || (transcriptPath?.includes("/.gemini/") ?? false);
103
+ if (isCodex || isCopilot || isCursor || isOpenCode || isPi || isGemini) {
90
104
  // The session route auto-detects CLI by file location, so [name] only
91
105
  // affects the breadcrumb. Encode the cwd Claude-style when we have it.
92
- const projectSeg = cwd ? encodeCwdForUrl(cwd) : "codex";
106
+ const fallbackSeg = isCodex ? "codex" : isCopilot ? "copilot" : isCursor ? "cursor" : isOpenCode ? "opencode" : isPi ? "pi" : "gemini";
107
+ const projectSeg = cwd ? encodeCwdForUrl(cwd) : fallbackSeg;
93
108
  return (
94
109
  <Link
95
110
  href={`/project/${encodeURIComponent(projectSeg)}/session/${encodeURIComponent(sessionId)}`}
@@ -153,16 +168,11 @@ function EventTypeBadge({ eventType }: { eventType: string }) {
153
168
 
154
169
  function IntegrationBadge({ integration }: { integration?: string }) {
155
170
  if (!integration) return null;
156
- const label =
157
- integration === "claude" ? "Claude Code" : integration === "codex" ? "OpenAI Codex" : integration;
158
- const isCodex = integration === "codex";
171
+ const label = getCliLabel(integration);
172
+ const classes = getCliBadgeClasses(integration);
159
173
  return (
160
174
  <span
161
- className={`inline-flex items-center rounded px-1.5 py-0.5 text-[0.6rem] font-medium border ${
162
- isCodex
163
- ? "bg-purple-500/10 text-purple-400 border-purple-500/20"
164
- : "bg-orange-500/10 text-orange-400 border-orange-500/20"
165
- }`}
175
+ className={`inline-flex items-center rounded px-1.5 py-0.5 text-[0.6rem] font-medium border ${classes}`}
166
176
  title={`Agent CLI: ${label}`}
167
177
  >
168
178
  {label}
@@ -366,9 +376,9 @@ function ActivityTab({
366
376
  const [filterEventType, setFilterEventType] = useState(() => url.get("event") ?? "");
367
377
  const [filterPolicy, setFilterPolicy] = useState(() => url.get("policy") ?? "");
368
378
  const [filterSessionId, setFilterSessionId] = useState(() => url.get("session") ?? "");
369
- const [filterCli, setFilterCli] = useState<"" | "claude" | "codex">(() => {
379
+ const [filterCli, setFilterCli] = useState<"" | CliId>(() => {
370
380
  const v = url.get("cli");
371
- return v === "claude" || v === "codex" ? v : "";
381
+ return isKnownCli(v) ? v : "";
372
382
  });
373
383
  const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
374
384
  const filtersRef = useRef({ filterDecision, filterEventType, filterPolicy, filterSessionId, filterCli });
@@ -470,13 +480,19 @@ function ActivityTab({
470
480
  </select>
471
481
  <select
472
482
  value={filterCli}
473
- onChange={(e) => setFilterCli(e.target.value as "" | "claude" | "codex")}
483
+ onChange={(e) => {
484
+ const v = e.target.value;
485
+ setFilterCli(v === "" || isKnownCli(v) ? v : "");
486
+ }}
474
487
  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
488
  aria-label="Filter by CLI"
476
489
  >
477
490
  <option value="">All CLIs</option>
478
- <option value="claude">Claude Code</option>
479
- <option value="codex">OpenAI Codex</option>
491
+ {KNOWN_CLI_IDS.map((id) => (
492
+ <option key={id} value={id}>
493
+ {getCliLabel(id)}
494
+ </option>
495
+ ))}
480
496
  </select>
481
497
  <div className="relative">
482
498
  <input
@@ -2,6 +2,11 @@
2
2
  import { Suspense } from "react";
3
3
  import { resolveProjectPath, getCachedSessionFiles, type SessionFile } from "@/lib/projects";
4
4
  import { getCachedCodexSessionsByEncodedName } from "@/lib/codex-projects";
5
+ import { getCachedCopilotSessionsByEncodedName } from "@/lib/copilot-projects";
6
+ import { getCachedCursorSessionsByEncodedName } from "@/lib/cursor-projects";
7
+ import { getCachedOpenCodeSessionsByEncodedName } from "@/lib/opencode-projects";
8
+ import { getCachedPiSessionsByEncodedName } from "@/lib/pi-projects";
9
+ import { getCachedGeminiSessionsByEncodedName } from "@/lib/gemini-projects";
5
10
  import { logWarn } from "@/lib/logger";
6
11
  import { decodeFolderName } from "@/lib/paths";
7
12
  import { notFound } from "next/navigation";
@@ -23,7 +28,8 @@ interface ProjectPageProps {
23
28
  export default async function ProjectPage({ params }: ProjectPageProps) {
24
29
  const { name } = await params;
25
30
  // 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.
31
+ // we still want to try the external CLIs, since a non-Claude-only cwd never
32
+ // escapes this check.
27
33
  let claudeProjectPath: string | null = null;
28
34
  try {
29
35
  claudeProjectPath = resolveProjectPath(name);
@@ -39,18 +45,39 @@ export default async function ProjectPage({ params }: ProjectPageProps) {
39
45
  claudeSessions = await getCachedSessionFiles(claudeProjectPath);
40
46
  }
41
47
  // 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);
48
+ // so each external CLI looks up sessions by re-encoding cwd and matching the slug.
49
+ const [codex, copilot, cursor, opencode, pi, gemini] = await Promise.all([
50
+ getCachedCodexSessionsByEncodedName(name),
51
+ getCachedCopilotSessionsByEncodedName(name),
52
+ getCachedCursorSessionsByEncodedName(name),
53
+ getCachedOpenCodeSessionsByEncodedName(name),
54
+ getCachedPiSessionsByEncodedName(name),
55
+ getCachedGeminiSessionsByEncodedName(name),
56
+ ]);
44
57
  const codexSessions = codex.sessions;
58
+ const copilotSessions = copilot.sessions;
59
+ const cursorSessions = cursor.sessions;
60
+ const opencodeSessions = opencode.sessions;
61
+ const piSessions = pi.sessions;
62
+ const geminiSessions = gemini.sessions;
45
63
 
46
- if (!claudeExists && codexSessions.length === 0) {
64
+ if (
65
+ !claudeExists &&
66
+ codexSessions.length === 0 &&
67
+ copilotSessions.length === 0 &&
68
+ cursorSessions.length === 0 &&
69
+ opencodeSessions.length === 0 &&
70
+ piSessions.length === 0 &&
71
+ geminiSessions.length === 0
72
+ ) {
47
73
  notFound();
48
74
  }
49
75
 
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;
76
+ // Prefer a canonical cwd recovered from any external store when available —
77
+ // `decodeFolderName(name)` is ambiguous for cwds containing `-` (every `-`
78
+ // becomes `/`). Each external transcript records the literal cwd, so they
79
+ // round-trip correctly. First non-null wins (Codex → Copilot → Cursor → OpenCode → Pi → Gemini).
80
+ const canonicalRoot = codex.cwd ?? copilot.cwd ?? cursor.cwd ?? opencode.cwd ?? pi.cwd ?? gemini.cwd ?? decodedName;
54
81
 
55
82
  // Project header metadata
56
83
  let lastModified: Date | null = null;
@@ -64,18 +91,27 @@ export default async function ProjectPage({ params }: ProjectPageProps) {
64
91
  logWarn(`Failed to get stats for project ${decodedName}:`, error);
65
92
  }
66
93
  }
67
- const newestCodex = codexSessions[0]?.lastModified ?? null;
68
- if (newestCodex && (!lastModified || newestCodex.getTime() > lastModified.getTime())) {
69
- lastModified = newestCodex;
70
- lastModifiedFormatted = formatDate(newestCodex);
94
+ const newestExternal = [codexSessions[0], copilotSessions[0], cursorSessions[0], opencodeSessions[0], piSessions[0], geminiSessions[0]]
95
+ .filter((s): s is SessionFile => !!s)
96
+ .map((s) => s.lastModified)
97
+ .reduce<Date | null>((acc, d) => (!acc || d.getTime() > acc.getTime() ? d : acc), null);
98
+ if (newestExternal && (!lastModified || newestExternal.getTime() > lastModified.getTime())) {
99
+ lastModified = newestExternal;
100
+ lastModifiedFormatted = formatDate(newestExternal);
71
101
  }
72
102
 
73
- const sessionFiles: SessionFile[] = [...claudeSessions, ...codexSessions].sort(
74
- (a, b) => b.lastModified.getTime() - a.lastModified.getTime(),
75
- );
103
+ const sessionFiles: SessionFile[] = [
104
+ ...claudeSessions,
105
+ ...codexSessions,
106
+ ...copilotSessions,
107
+ ...cursorSessions,
108
+ ...opencodeSessions,
109
+ ...piSessions,
110
+ ...geminiSessions,
111
+ ].sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
76
112
 
77
113
  // Path line: prefer the Claude storage dir if present (matches existing UX);
78
- // otherwise show the canonical Codex cwd.
114
+ // otherwise show the canonical cwd recovered from the first external store.
79
115
  const displayPath = claudeExists && claudeProjectPath ? claudeProjectPath : canonicalRoot;
80
116
 
81
117
  return (
@@ -4,6 +4,11 @@ import { ArrowLeft, Download } from "lucide-react";
4
4
  import { notFound } from "next/navigation";
5
5
  import { getCachedSessionLog, type LogEntry } from "@/lib/log-entries";
6
6
  import { getCachedCodexSessionLog } from "@/lib/codex-sessions";
7
+ import { getCachedCopilotSessionLog } from "@/lib/copilot-sessions";
8
+ import { getCachedCursorSessionLog } from "@/lib/cursor-sessions";
9
+ import { getCachedOpenCodeSessionLog } from "@/lib/opencode-sessions";
10
+ import { getCachedPiSessionLog } from "@/lib/pi-sessions";
11
+ import { getCachedGeminiSessionLog } from "@/lib/gemini-sessions";
7
12
  import { decodeFolderName } from "@/lib/paths";
8
13
  import { baseSessionId } from "@/lib/utils/session-id";
9
14
  import { resolveProjectPath, UUID_RE } from "@/lib/projects";
@@ -30,13 +35,17 @@ export default async function SessionPage({ params }: SessionPageProps) {
30
35
  }
31
36
  const decodedName = decodeFolderName(name);
32
37
  const decodedSessionId = baseSessionId(sessionId);
33
- if (!UUID_RE.test(decodedSessionId)) notFound();
38
+ // OpenCode session IDs are not UUIDs — they use `ses_*` prefixes (e.g.
39
+ // `ses_21ad60d14ffewMeRRKMLdS7vOI`). The other four CLIs use UUIDs. Accept
40
+ // either; the per-CLI loader returns null for unknown IDs anyway.
41
+ const OPENCODE_SESSION_RE = /^ses_[A-Za-z0-9]+$/;
42
+ if (!UUID_RE.test(decodedSessionId) && !OPENCODE_SESSION_RE.test(decodedSessionId)) notFound();
34
43
 
35
44
  let entries: LogEntry[] | null = null;
36
45
  let rawLines: Record<string, unknown>[] | null = null;
37
46
  let error: string | null = null;
38
- let cli: "claude" | "codex" = "claude";
39
- let codexCwd: string | undefined;
47
+ let cli: "claude" | "codex" | "copilot" | "cursor" | "opencode" | "pi" | "gemini" = "claude";
48
+ let externalCwd: string | undefined;
40
49
 
41
50
  try {
42
51
  // Use raw folder name for file operations — decodedName is for display only
@@ -46,36 +55,89 @@ export default async function SessionPage({ params }: SessionPageProps) {
46
55
  } catch (e) {
47
56
  const isNotFound = (e as NodeJS.ErrnoException).code === "ENOENT";
48
57
  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.
58
+ // Fall back through external stores in order: Codex Copilot → Cursor → OpenCode → Pi.
59
+ // Each store keys by sessionId rather than the project slug, so the
60
+ // [name] segment is irrelevant on these branches.
52
61
  const codex = await getCachedCodexSessionLog(decodedSessionId);
53
62
  if (codex) {
54
63
  entries = codex.entries;
55
64
  rawLines = codex.rawLines;
56
- codexCwd = codex.cwd;
65
+ externalCwd = codex.cwd;
57
66
  cli = "codex";
58
67
  } else {
59
- error = "Session log file not found.";
68
+ const copilot = await getCachedCopilotSessionLog(decodedSessionId);
69
+ if (copilot) {
70
+ entries = copilot.entries;
71
+ rawLines = copilot.rawLines;
72
+ externalCwd = copilot.cwd;
73
+ cli = "copilot";
74
+ } else {
75
+ const cursor = await getCachedCursorSessionLog(decodedSessionId);
76
+ if (cursor) {
77
+ entries = cursor.entries;
78
+ rawLines = cursor.rawLines;
79
+ externalCwd = cursor.cwd;
80
+ cli = "cursor";
81
+ } else {
82
+ const opencode = await getCachedOpenCodeSessionLog(decodedSessionId);
83
+ if (opencode) {
84
+ entries = opencode.entries;
85
+ rawLines = opencode.rawLines;
86
+ externalCwd = opencode.cwd;
87
+ cli = "opencode";
88
+ } else {
89
+ const pi = await getCachedPiSessionLog(decodedSessionId);
90
+ if (pi) {
91
+ entries = pi.entries;
92
+ rawLines = pi.rawLines;
93
+ externalCwd = pi.cwd;
94
+ cli = "pi";
95
+ } else {
96
+ const gemini = await getCachedGeminiSessionLog(decodedSessionId);
97
+ if (gemini) {
98
+ entries = gemini.entries;
99
+ rawLines = gemini.rawLines;
100
+ externalCwd = gemini.cwd;
101
+ cli = "gemini";
102
+ } else {
103
+ error = "Session log file not found.";
104
+ }
105
+ }
106
+ }
107
+ }
108
+ }
60
109
  }
61
110
  } else {
62
111
  error = "Failed to read session log.";
63
112
  }
64
113
  }
65
114
 
66
- const isCodex = cli === "codex";
67
- const headerLabel = isCodex ? "CLI" : "Project";
68
- const headerValue = isCodex ? `OpenAI Codex${codexCwd ? ` · ${codexCwd}` : ""}` : decodedName;
115
+ const isExternal = cli !== "claude";
116
+ const headerLabel = isExternal ? "CLI" : "Project";
117
+ const headerValue =
118
+ cli === "codex"
119
+ ? `OpenAI Codex${externalCwd ? ` · ${externalCwd}` : ""}`
120
+ : cli === "copilot"
121
+ ? `GitHub Copilot${externalCwd ? ` · ${externalCwd}` : ""}`
122
+ : cli === "cursor"
123
+ ? `Cursor Agent${externalCwd ? ` · ${externalCwd}` : ""}`
124
+ : cli === "opencode"
125
+ ? `OpenCode${externalCwd ? ` · ${externalCwd}` : ""}`
126
+ : cli === "pi"
127
+ ? `Pi${externalCwd ? ` · ${externalCwd}` : ""}`
128
+ : cli === "gemini"
129
+ ? `Gemini CLI${externalCwd ? ` · ${externalCwd}` : ""}`
130
+ : decodedName;
69
131
 
70
132
  return (
71
133
  <main className="min-h-screen bg-background">
72
134
  <div className="container mx-auto p-8">
73
135
  <Link
74
- href={isCodex ? "/policies?tab=activity" : `/project/${encodeURIComponent(name)}`}
136
+ href={isExternal ? "/policies?tab=activity" : `/project/${encodeURIComponent(name)}`}
75
137
  className="inline-flex items-center gap-2 text-muted-foreground hover:text-foreground mb-6 transition-colors"
76
138
  >
77
139
  <ArrowLeft className="w-4 h-4" />
78
- <span>{isCodex ? "Back to Activity" : "Back to Sessions"}</span>
140
+ <span>{isExternal ? "Back to Activity" : "Back to Sessions"}</span>
79
141
  </Link>
80
142
 
81
143
  <div className="mb-8">
@@ -99,7 +161,7 @@ export default async function SessionPage({ params }: SessionPageProps) {
99
161
  <p className="text-muted-foreground">
100
162
  <span className="font-medium">{rawLines.length}</span> log lines
101
163
  </p>
102
- {!isCodex && (
164
+ {!isExternal && (
103
165
  <a
104
166
  href={`/api/download/${encodeURIComponent(name)}/${encodeURIComponent(decodedSessionId)}`}
105
167
  download
@@ -122,7 +184,22 @@ export default async function SessionPage({ params }: SessionPageProps) {
122
184
  {!error && entries && (
123
185
  <LazyLogViewer
124
186
  entries={entries}
125
- projectName={isCodex ? (codexCwd ?? "OpenAI Codex") : decodedName}
187
+ projectName={
188
+ isExternal
189
+ ? (externalCwd ??
190
+ (cli === "codex"
191
+ ? "OpenAI Codex"
192
+ : cli === "copilot"
193
+ ? "GitHub Copilot"
194
+ : cli === "cursor"
195
+ ? "Cursor Agent"
196
+ : cli === "opencode"
197
+ ? "OpenCode"
198
+ : cli === "pi"
199
+ ? "Pi"
200
+ : "Gemini CLI"))
201
+ : decodedName
202
+ }
126
203
  sessionId={decodedSessionId}
127
204
  />
128
205
  )}
@@ -0,0 +1 @@
1
+ <svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="#FFFFFF"><title>GitHub Copilot</title><path d="M23.922 16.997C23.061 18.492 18.063 22.02 12 22.02 5.937 22.02.939 18.492.078 16.997A.641.641 0 0 1 0 16.741v-2.869a.883.883 0 0 1 .053-.22c.372-.935 1.347-2.292 2.605-2.656.167-.429.414-1.055.644-1.517a10.098 10.098 0 0 1-.052-1.086c0-1.331.282-2.499 1.132-3.368.397-.406.89-.717 1.474-.952C7.255 2.937 9.248 1.98 11.978 1.98c2.731 0 4.767.957 6.166 2.093.584.235 1.077.546 1.474.952.85.869 1.132 2.037 1.132 3.368 0 .368-.014.733-.052 1.086.23.462.477 1.088.644 1.517 1.258.364 2.233 1.721 2.605 2.656a.841.841 0 0 1 .053.22v2.869a.641.641 0 0 1-.078.256Zm-11.75-5.992h-.344a4.359 4.359 0 0 1-.355.508c-.77.947-1.918 1.492-3.508 1.492-1.725 0-2.989-.359-3.782-1.259a2.137 2.137 0 0 1-.085-.104L4 11.746v6.585c1.435.779 4.514 2.179 8 2.179 3.486 0 6.565-1.4 8-2.179v-6.585l-.098-.104s-.033.045-.085.104c-.793.9-2.057 1.259-3.782 1.259-1.59 0-2.738-.545-3.508-1.492a4.359 4.359 0 0 1-.355-.508Zm2.328 3.25c.549 0 1 .451 1 1v2c0 .549-.451 1-1 1-.549 0-1-.451-1-1v-2c0-.549.451-1 1-1Zm-5 0c.549 0 1 .451 1 1v2c0 .549-.451 1-1 1-.549 0-1-.451-1-1v-2c0-.549.451-1 1-1Zm3.313-6.185c.136 1.057.403 1.913.878 2.497.442.544 1.134.938 2.344.938 1.573 0 2.292-.337 2.657-.751.384-.435.558-1.15.558-2.361 0-1.14-.243-1.847-.705-2.319-.477-.488-1.319-.862-2.824-1.025-1.487-.161-2.192.138-2.533.529-.269.307-.437.808-.438 1.578v.021c0 .265.021.562.063.893Zm-1.626 0c.042-.331.063-.628.063-.894v-.02c-.001-.77-.169-1.271-.438-1.578-.341-.391-1.046-.69-2.533-.529-1.505.163-2.347.537-2.824 1.025-.462.472-.705 1.179-.705 2.319 0 1.211.175 1.926.558 2.361.365.414 1.084.751 2.657.751 1.21 0 1.902-.394 2.344-.938.475-.584.742-1.44.878-2.497Z"/></svg>
@@ -0,0 +1 @@
1
+ <svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="#000000"><title>GitHub Copilot</title><path d="M23.922 16.997C23.061 18.492 18.063 22.02 12 22.02 5.937 22.02.939 18.492.078 16.997A.641.641 0 0 1 0 16.741v-2.869a.883.883 0 0 1 .053-.22c.372-.935 1.347-2.292 2.605-2.656.167-.429.414-1.055.644-1.517a10.098 10.098 0 0 1-.052-1.086c0-1.331.282-2.499 1.132-3.368.397-.406.89-.717 1.474-.952C7.255 2.937 9.248 1.98 11.978 1.98c2.731 0 4.767.957 6.166 2.093.584.235 1.077.546 1.474.952.85.869 1.132 2.037 1.132 3.368 0 .368-.014.733-.052 1.086.23.462.477 1.088.644 1.517 1.258.364 2.233 1.721 2.605 2.656a.841.841 0 0 1 .053.22v2.869a.641.641 0 0 1-.078.256Zm-11.75-5.992h-.344a4.359 4.359 0 0 1-.355.508c-.77.947-1.918 1.492-3.508 1.492-1.725 0-2.989-.359-3.782-1.259a2.137 2.137 0 0 1-.085-.104L4 11.746v6.585c1.435.779 4.514 2.179 8 2.179 3.486 0 6.565-1.4 8-2.179v-6.585l-.098-.104s-.033.045-.085.104c-.793.9-2.057 1.259-3.782 1.259-1.59 0-2.738-.545-3.508-1.492a4.359 4.359 0 0 1-.355-.508Zm2.328 3.25c.549 0 1 .451 1 1v2c0 .549-.451 1-1 1-.549 0-1-.451-1-1v-2c0-.549.451-1 1-1Zm-5 0c.549 0 1 .451 1 1v2c0 .549-.451 1-1 1-.549 0-1-.451-1-1v-2c0-.549.451-1 1-1Zm3.313-6.185c.136 1.057.403 1.913.878 2.497.442.544 1.134.938 2.344.938 1.573 0 2.292-.337 2.657-.751.384-.435.558-1.15.558-2.361 0-1.14-.243-1.847-.705-2.319-.477-.488-1.319-.862-2.824-1.025-1.487-.161-2.192.138-2.533.529-.269.307-.437.808-.438 1.578v.021c0 .265.021.562.063.893Zm-1.626 0c.042-.331.063-.628.063-.894v-.02c-.001-.77-.169-1.271-.438-1.578-.341-.391-1.046-.69-2.533-.529-1.505.163-2.347.537-2.824 1.025-.462.472-.705 1.179-.705 2.319 0 1.211.175 1.926.558 2.361.365.414 1.084.751 2.657.751 1.21 0 1.902-.394 2.344-.938.475-.584.742-1.44.878-2.497Z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="395 393 175 197" role="img"><title>Cursor Agent</title><path fill="#72716D" d="M483.395 490.5L566 538.297C565.493 539.178 564.757 539.93 563.845 540.456L486.636 585.13C484.632 586.29 482.159 586.29 480.154 585.13L402.945 540.456C402.034 539.93 401.297 539.178 400.79 538.297L483.395 490.5Z"/><path fill="#55544F" d="M483.395 395V490.5L400.79 538.297C400.282 537.416 400 536.398 400 535.346V445.654C400 443.545 401.122 441.6 402.945 440.544L480.15 395.87C481.154 395.29 482.273 395 483.391 395H483.395Z"/><path fill="#43413C" d="M565.996 442.703C565.489 441.822 564.752 441.07 563.841 440.544L486.632 395.87C485.632 395.29 484.513 395 483.395 395V490.5L566 538.297C566.507 537.416 566.789 536.398 566.789 535.346V445.654C566.789 444.598 566.511 443.588 566 442.703H565.996Z"/><path fill="#D6D5D2" d="M560.218 446.049C560.686 446.858 560.751 447.896 560.218 448.82L485.235 578.974C484.732 579.855 483.392 579.493 483.392 578.479V492.713C483.392 492.029 483.209 491.37 482.877 490.794L560.215 446.045H560.218V446.049Z"/><path fill="#FFFFFF" d="M560.218 446.049L482.88 490.797C482.552 490.224 482.073 489.737 481.48 489.394L407.369 446.511C406.49 446.006 406.851 444.663 407.862 444.663H557.824C558.889 444.663 559.754 445.239 560.218 446.049Z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="395 393 175 197" role="img"><title>Cursor Agent</title><path fill="#72716D" d="M483.395 490.5L566 538.297C565.493 539.178 564.757 539.93 563.845 540.456L486.636 585.13C484.632 586.29 482.159 586.29 480.154 585.13L402.945 540.456C402.034 539.93 401.297 539.178 400.79 538.297L483.395 490.5Z"/><path fill="#55544F" d="M483.395 395V490.5L400.79 538.297C400.282 537.416 400 536.398 400 535.346V445.654C400 443.545 401.122 441.6 402.945 440.544L480.15 395.87C481.154 395.29 482.273 395 483.391 395H483.395Z"/><path fill="#43413C" d="M565.996 442.703C565.489 441.822 564.752 441.07 563.841 440.544L486.632 395.87C485.632 395.29 484.513 395 483.395 395V490.5L566 538.297C566.507 537.416 566.789 536.398 566.789 535.346V445.654C566.789 444.598 566.511 443.588 566 442.703H565.996Z"/><path fill="#D6D5D2" d="M560.218 446.049C560.686 446.858 560.751 447.896 560.218 448.82L485.235 578.974C484.732 579.855 483.392 579.493 483.392 578.479V492.713C483.392 492.029 483.209 491.37 482.877 490.794L560.215 446.045H560.218V446.049Z"/><path fill="#FFFFFF" d="M560.218 446.049L482.88 490.797C482.552 490.224 482.073 489.737 481.48 489.394L407.369 446.511C406.49 446.006 406.851 444.663 407.862 444.663H557.824C558.889 444.663 559.754 445.239 560.218 446.049Z"/></svg>
@@ -0,0 +1,13 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64" aria-label="Gemini CLI">
2
+ <defs>
3
+ <linearGradient id="gemini-grad-dark" x1="0" y1="0" x2="24" y2="24" gradientUnits="userSpaceOnUse">
4
+ <stop offset="0" stop-color="#74c0fc" />
5
+ <stop offset="0.5" stop-color="#9775fa" />
6
+ <stop offset="1" stop-color="#b197fc" />
7
+ </linearGradient>
8
+ </defs>
9
+ <path
10
+ fill="url(#gemini-grad-dark)"
11
+ d="M12 1c.4 5.6 4.4 9.6 10 10-5.6.4-9.6 4.4-10 10-.4-5.6-4.4-9.6-10-10C7.6 10.6 11.6 6.6 12 1z"
12
+ />
13
+ </svg>
@@ -0,0 +1,13 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64" aria-label="Gemini CLI">
2
+ <defs>
3
+ <linearGradient id="gemini-grad-light" x1="0" y1="0" x2="24" y2="24" gradientUnits="userSpaceOnUse">
4
+ <stop offset="0" stop-color="#1c7ed6" />
5
+ <stop offset="0.5" stop-color="#5e60ce" />
6
+ <stop offset="1" stop-color="#7048e8" />
7
+ </linearGradient>
8
+ </defs>
9
+ <path
10
+ fill="url(#gemini-grad-light)"
11
+ d="M12 1c.4 5.6 4.4 9.6 10 10-5.6.4-9.6 4.4-10 10-.4-5.6-4.4-9.6-10-10C7.6 10.6 11.6 6.6 12 1z"
12
+ />
13
+ </svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512" fill="none" role="img"><title>OpenCode</title><path d="M320 224V352H192V224H320Z" fill="#5A5858"/><path fill-rule="evenodd" clip-rule="evenodd" d="M384 416H128V96H384V416ZM320 160H192V352H320V160Z" fill="white"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512" fill="none" role="img"><title>OpenCode</title><path d="M320 224V352H192V224H320Z" fill="#A5A2A2"/><path fill-rule="evenodd" clip-rule="evenodd" d="M384 416H128V96H384V416ZM320 160H192V352H320V160Z" fill="#131010"/></svg>
@@ -0,0 +1,7 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 800">
3
+ <!-- P shape: outer boundary clockwise, inner hole counter-clockwise -->
4
+ <path fill="#fff" fill-rule="evenodd" d="M165.29 165.29 H517.36 V400 H400 V517.36 H282.65 V634.72 H165.29 Z M282.65 282.65 V400 H400 V282.65 Z"/>
5
+ <!-- i dot -->
6
+ <path fill="#fff" d="M517.36 400 H634.72 V634.72 H517.36 Z"/>
7
+ </svg>
@@ -0,0 +1,7 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 800">
3
+ <!-- P shape: outer boundary clockwise, inner hole counter-clockwise -->
4
+ <path fill="#000" fill-rule="evenodd" d="M165.29 165.29 H517.36 V400 H400 V517.36 H282.65 V634.72 H165.29 Z M282.65 282.65 V400 H400 V282.65 Z"/>
5
+ <!-- i dot -->
6
+ <path fill="#000" d="M517.36 400 H634.72 V634.72 H517.36 Z"/>
7
+ </svg>
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Single source of truth for agent-CLI metadata used by the dashboard, the
3
+ * `bin/failproofai.mjs` argv parser, the install prompt, and the badge UI.
4
+ *
5
+ * This module is **client-safe** — it only exports plain string metadata. The
6
+ * server-side project / session providers live in their own files
7
+ * (`lib/codex-projects.ts`, `lib/codex-sessions.ts`, `lib/copilot-projects.ts`,
8
+ * `lib/copilot-sessions.ts`, `lib/cursor-projects.ts`, `lib/cursor-sessions.ts`,
9
+ * `lib/opencode-projects.ts`, `lib/opencode-sessions.ts`,
10
+ * `lib/pi-projects.ts`, `lib/pi-sessions.ts`)
11
+ * and are imported lazily by `lib/projects.ts` and the session viewer page so
12
+ * Turbopack doesn't drag Node-only deps (`fs/promises`, `os`) into client
13
+ * bundles.
14
+ *
15
+ * Adding a new agent CLI = three steps:
16
+ * 1. Extend `INTEGRATION_TYPES` in `src/hooks/types.ts` (server-side hook contract).
17
+ * 2. Add an `Integration` impl in `src/hooks/integrations.ts` (install/uninstall plumbing).
18
+ * 3. Add an entry to `CLI_REGISTRY` below — display label and badge classes.
19
+ * Optionally add a project provider (`lib/<cli>-projects.ts`) and a
20
+ * session loader (`lib/<cli>-sessions.ts`); wire them into
21
+ * `lib/projects.ts#getProjectFolders` and the session viewer page's
22
+ * fallback chain (both already iterate over per-CLI providers).
23
+ *
24
+ * Filter dropdown, badge component, and project-list filter all read from
25
+ * this registry and pick up new CLIs without further code changes.
26
+ */
27
+ import type { IntegrationType } from "@/src/hooks/types";
28
+
29
+ /** Canonical CLI ids the registry knows about. Mirrors `INTEGRATION_TYPES`. */
30
+ export const KNOWN_CLI_IDS = ["claude", "codex", "copilot", "cursor", "opencode", "pi", "gemini"] as const satisfies readonly IntegrationType[];
31
+ export type CliId = (typeof KNOWN_CLI_IDS)[number];
32
+
33
+ /** Per-CLI metadata consumed by the dashboard. */
34
+ export interface CliEntry {
35
+ id: CliId;
36
+ label: string;
37
+ /** Tailwind utility classes for the small CLI badge (background + text + border). */
38
+ badgeClasses: string;
39
+ }
40
+
41
+ const CLI_ENTRIES: Record<CliId, CliEntry> = {
42
+ claude: {
43
+ id: "claude",
44
+ label: "Claude Code",
45
+ badgeClasses: "bg-orange-500/10 text-orange-400 border-orange-500/20",
46
+ },
47
+ codex: {
48
+ id: "codex",
49
+ label: "OpenAI Codex",
50
+ badgeClasses: "bg-purple-500/10 text-purple-400 border-purple-500/20",
51
+ },
52
+ copilot: {
53
+ id: "copilot",
54
+ label: "GitHub Copilot",
55
+ badgeClasses: "bg-blue-500/10 text-blue-400 border-blue-500/20",
56
+ },
57
+ cursor: {
58
+ id: "cursor",
59
+ label: "Cursor Agent",
60
+ badgeClasses: "bg-emerald-500/10 text-emerald-400 border-emerald-500/20",
61
+ },
62
+ opencode: {
63
+ id: "opencode",
64
+ label: "OpenCode",
65
+ badgeClasses: "bg-amber-500/10 text-amber-400 border-amber-500/20",
66
+ },
67
+ pi: {
68
+ id: "pi",
69
+ label: "Pi",
70
+ badgeClasses: "bg-pink-500/10 text-pink-400 border-pink-500/20",
71
+ },
72
+ gemini: {
73
+ id: "gemini",
74
+ label: "Gemini CLI",
75
+ badgeClasses: "bg-sky-500/10 text-sky-400 border-sky-500/20",
76
+ },
77
+ };
78
+
79
+ export function getCliEntry(id: string): CliEntry | undefined {
80
+ return CLI_ENTRIES[id as CliId];
81
+ }
82
+
83
+ export function listCliEntries(): CliEntry[] {
84
+ return KNOWN_CLI_IDS.map((id) => CLI_ENTRIES[id]);
85
+ }
86
+
87
+ /** External CLIs (everything except Claude) that contribute project listings. */
88
+ export function listExternalCliEntries(): CliEntry[] {
89
+ return listCliEntries().filter((c) => c.id !== "claude");
90
+ }
91
+
92
+ /** Display label for a CLI id. Returns the id itself if unknown. */
93
+ export function getCliLabel(id: string): string {
94
+ return getCliEntry(id)?.label ?? id;
95
+ }
96
+
97
+ /** Badge classes for a CLI id. Falls back to Claude's classes if unknown. */
98
+ export function getCliBadgeClasses(id: string): string {
99
+ return getCliEntry(id)?.badgeClasses ?? CLI_ENTRIES.claude.badgeClasses;
100
+ }
101
+
102
+ /** Predicate: is this id a known CLI? Useful when validating user input. */
103
+ export function isKnownCli(id: string | null | undefined): id is CliId {
104
+ // hasOwnProperty.call (not `id in CLI_ENTRIES`) so inherited Object.prototype
105
+ // keys like "toString" / "constructor" / "hasOwnProperty" don't pass.
106
+ return typeof id === "string" && Object.prototype.hasOwnProperty.call(CLI_ENTRIES, id);
107
+ }
@@ -9,9 +9,9 @@
9
9
  * convention (see `encodeFolderName` in `lib/paths.ts`), so a cwd present in both stores
10
10
  * naturally produces the same `name` and can be merged on the Claude side.
11
11
  */
12
- import { open, readdir } from "fs/promises";
13
- import { homedir } from "os";
14
- import { join } from "path";
12
+ import { open, readdir } from "node:fs/promises";
13
+ import { homedir } from "node:os";
14
+ import { join } from "node:path";
15
15
  import { encodeFolderName } from "./paths";
16
16
  import type { ProjectFolder, SessionFile } from "./projects";
17
17
  import { runtimeCache } from "./runtime-cache";