failproofai 0.0.9 → 0.0.10-beta.1

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 (202) 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]__0_b7pgn._.js → [root-of-the-server]__0lkkjl_._.js} +2 -2
  79. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__01g_w_e._.js → [root-of-the-server]__0mb9b9d._.js} +2 -2
  80. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0mup1hi._.js +3 -0
  81. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ohb3gc._.js +4 -0
  82. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qbpe_v._.js +3 -0
  83. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s~gy6y._.js +3 -0
  84. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0t5l7a5._.js +3 -0
  85. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ymlddl._.js +152 -6
  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/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0ef3uwk.js +1 -1
  99. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  100. package/.next/standalone/.next/server/pages/404.html +2 -2
  101. package/.next/standalone/.next/server/pages/500.html +1 -1
  102. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  103. package/.next/standalone/.next/server/server-reference-manifest.json +9 -9
  104. package/.next/standalone/.next/static/chunks/{0n-_j_6fo6jex.js → 0-wd3kiz5wrsz.js} +2 -2
  105. package/.next/standalone/.next/static/chunks/{0756i.7omnnl6.js → 0222q~_4u7p6h.js} +1 -1
  106. package/.next/standalone/.next/static/chunks/{0t~iusm_fxoao.js → 02y~6tp1j1wkh.js} +1 -1
  107. package/.next/standalone/.next/static/chunks/{09ose_165ra4d.js → 09qdljea8j.3~.js} +1 -1
  108. package/.next/standalone/.next/static/chunks/0bi2r.m~yokoo.js +1 -0
  109. package/.next/standalone/.next/static/chunks/{11kt_9zaooda3.js → 0pt38lwlsaxvs.js} +1 -1
  110. package/.next/standalone/.next/static/chunks/0q5bmqop--9yk.js +1 -0
  111. package/.next/standalone/.next/static/chunks/{0u-ys71jc4y68.js → 0vl201wjmz17m.js} +2 -2
  112. package/.next/standalone/.next/static/chunks/{0pr7k36o_.du1.js → 0vl~p17i-4qt2.js} +1 -1
  113. package/.next/standalone/.next/static/chunks/0xkzmsj-sniqz.js +1 -0
  114. package/.next/standalone/.next/static/chunks/12po2vpc-4_c1.css +1 -0
  115. package/.next/standalone/.opencode/opencode.json +4 -0
  116. package/.next/standalone/.opencode/plugins/failproofai.mjs +131 -0
  117. package/.next/standalone/.pi/settings.json +5 -0
  118. package/.next/standalone/app/components/cli-badge.tsx +7 -11
  119. package/.next/standalone/app/components/project-list.tsx +32 -4
  120. package/.next/standalone/app/policies/hooks-client.tsx +31 -15
  121. package/.next/standalone/app/project/[name]/page.tsx +52 -16
  122. package/.next/standalone/app/project/[name]/session/[sessionId]/page.tsx +92 -15
  123. package/.next/standalone/assets/logos/copilot-dark.svg +1 -0
  124. package/.next/standalone/assets/logos/copilot-light.svg +1 -0
  125. package/.next/standalone/assets/logos/cursor-dark.svg +1 -0
  126. package/.next/standalone/assets/logos/cursor-light.svg +1 -0
  127. package/.next/standalone/assets/logos/gemini-dark.svg +13 -0
  128. package/.next/standalone/assets/logos/gemini-light.svg +13 -0
  129. package/.next/standalone/assets/logos/opencode-dark.svg +1 -0
  130. package/.next/standalone/assets/logos/opencode-light.svg +1 -0
  131. package/.next/standalone/assets/logos/pi-dark.svg +7 -0
  132. package/.next/standalone/assets/logos/pi-light.svg +7 -0
  133. package/.next/standalone/lib/cli-registry.ts +107 -0
  134. package/.next/standalone/lib/codex-projects.ts +3 -3
  135. package/.next/standalone/lib/copilot-projects.ts +224 -0
  136. package/.next/standalone/lib/copilot-sessions.ts +395 -0
  137. package/.next/standalone/lib/cursor-projects.ts +312 -0
  138. package/.next/standalone/lib/cursor-sessions.ts +467 -0
  139. package/.next/standalone/lib/gemini-projects.ts +203 -0
  140. package/.next/standalone/lib/gemini-sessions.ts +365 -0
  141. package/.next/standalone/lib/opencode-projects.ts +232 -0
  142. package/.next/standalone/lib/opencode-sessions.ts +237 -0
  143. package/.next/standalone/lib/pi-projects.ts +230 -0
  144. package/.next/standalone/lib/pi-sessions.ts +325 -0
  145. package/.next/standalone/lib/projects.ts +67 -31
  146. package/.next/standalone/next.config.ts +5 -4
  147. package/.next/standalone/package.json +2 -1
  148. package/.next/standalone/pi-extension/index.ts +373 -0
  149. package/.next/standalone/pi-extension/package.json +12 -0
  150. package/.next/standalone/server.js +1 -1
  151. package/README.md +37 -3
  152. package/bin/failproofai.mjs +61 -21
  153. package/dist/cli.mjs +2405 -253
  154. package/lib/cli-registry.ts +107 -0
  155. package/lib/codex-projects.ts +3 -3
  156. package/lib/copilot-projects.ts +224 -0
  157. package/lib/copilot-sessions.ts +395 -0
  158. package/lib/cursor-projects.ts +312 -0
  159. package/lib/cursor-sessions.ts +467 -0
  160. package/lib/gemini-projects.ts +203 -0
  161. package/lib/gemini-sessions.ts +365 -0
  162. package/lib/opencode-projects.ts +232 -0
  163. package/lib/opencode-sessions.ts +237 -0
  164. package/lib/pi-projects.ts +230 -0
  165. package/lib/pi-sessions.ts +325 -0
  166. package/lib/projects.ts +67 -31
  167. package/package.json +2 -1
  168. package/pi-extension/index.ts +373 -0
  169. package/pi-extension/package.json +12 -0
  170. package/scripts/install-diagnosis.mjs +190 -0
  171. package/scripts/launch.ts +32 -0
  172. package/scripts/postinstall.mjs +25 -0
  173. package/scripts/translate-docs/mdx-translator.ts +56 -2
  174. package/scripts/translate-docs/translator.ts +1 -1
  175. package/src/hooks/builtin-policies.ts +84 -14
  176. package/src/hooks/handler.ts +67 -5
  177. package/src/hooks/install-prompt.ts +33 -10
  178. package/src/hooks/integrations.ts +1007 -6
  179. package/src/hooks/policy-evaluator.ts +299 -3
  180. package/src/hooks/resolve-permission-mode.ts +23 -0
  181. package/src/hooks/types.ts +307 -3
  182. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0g72weg._.js +0 -3
  183. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0su~k6f._.js +0 -3
  184. package/.next/standalone/.next/server/chunks/lib_codex-projects_ts_07qqk1g._.js +0 -3
  185. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01743wx._.js +0 -3
  186. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__092s1ta._.js +0 -4
  187. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0g.lg8b._.js +0 -4
  188. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0gs6wz4._.js +0 -3
  189. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h..k-e._.js +0 -4
  190. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0it81ys._.js +0 -3
  191. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u4a9jq._.js +0 -4
  192. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11pa2ra._.js +0 -4
  193. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12.h2mg._.js +0 -3
  194. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12t-wym._.js +0 -4
  195. package/.next/standalone/.next/server/chunks/ssr/_04w00cm._.js +0 -3
  196. package/.next/standalone/.next/static/chunks/0.rk1iwdt1d7c.css +0 -1
  197. package/.next/standalone/.next/static/chunks/06x4-d1~o-opr.js +0 -1
  198. package/.next/standalone/.next/static/chunks/095l4hc7-h.~~.js +0 -1
  199. package/.next/standalone/.next/static/chunks/0n~s0gafwnp2y.js +0 -1
  200. /package/.next/standalone/.next/static/{A_Ax17P33facL0OmIwFXj → w0GG7S5UEj1-p5g9hfsh2}/_buildManifest.js +0 -0
  201. /package/.next/standalone/.next/static/{A_Ax17P33facL0OmIwFXj → w0GG7S5UEj1-p5g9hfsh2}/_clientMiddlewareManifest.js +0 -0
  202. /package/.next/standalone/.next/static/{A_Ax17P33facL0OmIwFXj → w0GG7S5UEj1-p5g9hfsh2}/_ssgManifest.js +0 -0
@@ -0,0 +1,131 @@
1
+ // AUTO-GENERATED by failproofai. __failproofai_hook__
2
+ // Re-generate via: failproofai policies --install --cli opencode
3
+ //
4
+ // DEV variant: this repo's contributors run failproofai from source via
5
+ // `bun bin/failproofai.mjs ...` instead of `npx -y failproofai`. This file
6
+ // is the project-scope shim with that dev path baked in. The production
7
+ // shape (npx-based) is generated by `src/hooks/integrations.ts` on install
8
+ // for end users — see `buildOpenCodePluginShim` there.
9
+ //
10
+ // Do NOT install this repo via `failproofai policies --install --cli
11
+ // opencode --scope project` — it would overwrite this dev path with the
12
+ // portable npx form.
13
+ import { spawnSync } from "node:child_process";
14
+ import { resolve } from "node:path";
15
+
16
+ const REPO_ROOT = resolve(import.meta.dirname, "..", "..");
17
+ const FAILPROOFAI_DEV_BIN = resolve(REPO_ROOT, "bin", "failproofai.mjs");
18
+
19
+ const BUS_EVENT_MAP = {
20
+ "session.created": "SessionStart",
21
+ "session.deleted": "SessionEnd",
22
+ "session.idle": "Stop",
23
+ };
24
+
25
+ function runFailproofai(eventName, payload, directory) {
26
+ const r = spawnSync("bun", [FAILPROOFAI_DEV_BIN, "--hook", eventName, "--cli", "opencode"], {
27
+ input: JSON.stringify(payload),
28
+ encoding: "utf8",
29
+ timeout: 60_000,
30
+ cwd: directory,
31
+ });
32
+ return { exitCode: r.status ?? 0, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
33
+ }
34
+
35
+ function applyDecision(result, ctx) {
36
+ if (result.exitCode === 2) {
37
+ throw new Error((result.stderr || "").trim() || "Blocked by failproofai");
38
+ }
39
+ let parsed = null;
40
+ try { parsed = JSON.parse(result.stdout); } catch { /* fail-open allow */ }
41
+ if (!parsed) return;
42
+ const out = parsed.hookSpecificOutput;
43
+ if (out && out.permissionDecision === "deny") {
44
+ throw new Error(out.permissionDecisionReason || "Blocked by failproofai");
45
+ }
46
+ if (out && out.decision && out.decision.behavior === "deny") {
47
+ throw new Error((out.decision.message) || "Blocked by failproofai");
48
+ }
49
+ const ctxText = out && out.additionalContext;
50
+ if (ctxText && ctx && ctx.client && ctx.sessionID) {
51
+ Promise.resolve(ctx.client.session.prompt({
52
+ path: { id: ctx.sessionID },
53
+ body: { parts: [{ type: "text", text: ctxText }] },
54
+ })).catch(() => {});
55
+ }
56
+ }
57
+
58
+ export default async function failproofaiPlugin({ client, directory }) {
59
+ return {
60
+ event: async ({ event }) => {
61
+ if (!event || !event.type) return;
62
+ if (event.type === "message.updated") {
63
+ const props = event.properties || {};
64
+ const info = props.info || props.message || {};
65
+ const role = info.role || props.role;
66
+ if (role !== "user") return;
67
+ const sessionID = info.sessionID || info.sessionId || info.session_id || props.sessionID;
68
+ // Reconstruct the user prompt text so prompt-based policies see it.
69
+ let prompt = "";
70
+ const parts = info.parts || props.parts || [];
71
+ if (Array.isArray(parts)) {
72
+ for (const p of parts) {
73
+ if (p && typeof p === "object" && typeof p.text === "string") prompt += p.text;
74
+ }
75
+ }
76
+ if (!prompt) prompt = (info.text || info.content || props.text || "").toString();
77
+ const r = runFailproofai("UserPromptSubmit", {
78
+ session_id: sessionID, cwd: directory, hook_event_name: "UserPromptSubmit", prompt,
79
+ }, directory);
80
+ applyDecision(r, { client, sessionID });
81
+ return;
82
+ }
83
+ const claudeEvent = BUS_EVENT_MAP[event.type];
84
+ if (!claudeEvent) return;
85
+ const props = event.properties || {};
86
+ const sessionID = props.sessionID || (props.session && props.session.id) || props.id;
87
+ const r = runFailproofai(claudeEvent, {
88
+ session_id: sessionID, cwd: directory, hook_event_name: claudeEvent,
89
+ }, directory);
90
+ applyDecision(r, { client, sessionID });
91
+ },
92
+
93
+ "tool.execute.before": async (input, output) => {
94
+ const r = runFailproofai("PreToolUse", {
95
+ session_id: input.sessionID,
96
+ cwd: directory,
97
+ tool_name: input.tool,
98
+ tool_input: output.args,
99
+ hook_event_name: "PreToolUse",
100
+ }, directory);
101
+ applyDecision(r, { client, sessionID: input.sessionID });
102
+ },
103
+
104
+ "tool.execute.after": async (input, output) => {
105
+ const r = runFailproofai("PostToolUse", {
106
+ session_id: input.sessionID,
107
+ cwd: directory,
108
+ tool_name: input.tool,
109
+ tool_input: input.args,
110
+ tool_response: { title: output.title, output: output.output, metadata: output.metadata },
111
+ hook_event_name: "PostToolUse",
112
+ }, directory);
113
+ applyDecision(r, { client, sessionID: input.sessionID });
114
+ },
115
+
116
+ "permission.ask": async (input, output) => {
117
+ const r = runFailproofai("PermissionRequest", {
118
+ session_id: input.sessionID,
119
+ cwd: directory,
120
+ tool_name: input.tool || input.command || "permission",
121
+ tool_input: input,
122
+ hook_event_name: "PermissionRequest",
123
+ }, directory);
124
+ try {
125
+ applyDecision(r, { client, sessionID: input.sessionID });
126
+ } catch {
127
+ output.status = "deny";
128
+ }
129
+ },
130
+ };
131
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "packages": [
3
+ "../pi-extension"
4
+ ]
5
+ }
@@ -1,21 +1,17 @@
1
1
  /**
2
- * Tiny CLI-origin badge orange for Claude Code, purple for OpenAI Codex.
3
- * Mirrors the IntegrationBadge styling in `app/policies/hooks-client.tsx`,
4
- * extracted here for reuse across the projects listing, project detail page,
5
- * and session viewer.
2
+ * Tiny CLI-origin badge. Visual style and label are sourced from
3
+ * `lib/cli-registry.ts` so adding a new agent CLI = one registry entry, no UI
4
+ * changes here.
6
5
  */
7
6
  import type { ProjectCli } from "@/lib/projects";
7
+ import { getCliLabel, getCliBadgeClasses } from "@/lib/cli-registry";
8
8
 
9
9
  export function CliBadge({ cli }: { cli: ProjectCli }) {
10
- const isCodex = cli === "codex";
11
- const label = isCodex ? "OpenAI Codex" : "Claude Code";
10
+ const label = getCliLabel(cli);
11
+ const classes = getCliBadgeClasses(cli);
12
12
  return (
13
13
  <span
14
- className={`inline-flex items-center rounded px-1.5 py-0.5 text-[0.6rem] font-medium border ${
15
- isCodex
16
- ? "bg-purple-500/10 text-purple-400 border-purple-500/20"
17
- : "bg-orange-500/10 text-orange-400 border-orange-500/20"
18
- }`}
14
+ className={`inline-flex items-center rounded px-1.5 py-0.5 text-[0.6rem] font-medium border ${classes}`}
19
15
  title={`Agent CLI: ${label}`}
20
16
  >
21
17
  {label}
@@ -23,6 +23,7 @@ import {
23
23
  keywordsToParam, paramToKeywords,
24
24
  pageToParam, paramToPage,
25
25
  } from "@/lib/url-filter-serializers";
26
+ import { KNOWN_CLI_IDS, getCliLabel, isKnownCli, type CliId } from "@/lib/cli-registry";
26
27
  import { Folder, Search, X } from "lucide-react";
27
28
  import Link from "next/link";
28
29
  import PaginationControls from "./pagination-controls";
@@ -50,6 +51,10 @@ export default function ProjectList({ folders }: ProjectListProps) {
50
51
  // Read initial state from URL
51
52
  const [keywords, setKeywords] = useState<string[]>(() => paramToKeywords(url.get("q")));
52
53
  const [keywordInput, setKeywordInput] = useState("");
54
+ const [filterCli, setFilterCli] = useState<"" | CliId>(() => {
55
+ const v = url.get("cli");
56
+ return isKnownCli(v) ? v : "";
57
+ });
53
58
 
54
59
  const {
55
60
  filterPreset, dateRange, currentPage, setCurrentPage,
@@ -72,9 +77,10 @@ export default function ProjectList({ folders }: ProjectListProps) {
72
77
  ...dateRangeToParams(dateRange),
73
78
  q: keywordsToParam(keywords),
74
79
  page: pageToParam(currentPage),
80
+ cli: filterCli || undefined,
75
81
  });
76
82
  // eslint-disable-next-line react-hooks/exhaustive-deps
77
- }, [filterPreset, dateRange, keywords, currentPage]);
83
+ }, [filterPreset, dateRange, keywords, currentPage, filterCli]);
78
84
 
79
85
  const addKeyword = (keyword: string) => {
80
86
  const trimmed = keyword.trim();
@@ -96,6 +102,7 @@ export default function ProjectList({ folders }: ProjectListProps) {
96
102
  const clearFilters = () => {
97
103
  clearDateFilters();
98
104
  clearKeywords();
105
+ setFilterCli("");
99
106
  };
100
107
 
101
108
  const normalizedFolders = useMemo(() => rehydrateDates(folders), [folders]);
@@ -113,8 +120,12 @@ export default function ProjectList({ folders }: ProjectListProps) {
113
120
  });
114
121
  }
115
122
 
123
+ if (filterCli) {
124
+ filtered = filtered.filter((folder) => folder.cli.includes(filterCli));
125
+ }
126
+
116
127
  return filtered.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
117
- }, [normalizedFolders, filterPreset, dateRange, keywords]);
128
+ }, [normalizedFolders, filterPreset, dateRange, keywords, filterCli]);
118
129
 
119
130
  const totalPages = Math.max(1, Math.ceil(filteredFolders.length / ITEMS_PER_PAGE));
120
131
  useEffect(() => {
@@ -129,7 +140,7 @@ export default function ProjectList({ folders }: ProjectListProps) {
129
140
  {/* Filter Bar */}
130
141
  <div className="bg-card border border-border rounded-lg p-4">
131
142
  <div className="flex flex-col gap-4">
132
- {/* Preset Filters + Refresh */}
143
+ {/* Preset Filters + CLI filter */}
133
144
  <div className="flex flex-wrap items-center gap-2">
134
145
  <span className="text-sm font-medium text-foreground">Filter by:</span>
135
146
  {FILTER_PRESETS.map((preset) => (
@@ -146,6 +157,23 @@ export default function ProjectList({ folders }: ProjectListProps) {
146
157
  </button>
147
158
  ))}
148
159
 
160
+ <span className="ml-2 text-sm font-medium text-foreground">CLI:</span>
161
+ <select
162
+ aria-label="Filter by CLI"
163
+ value={filterCli}
164
+ onChange={(e) => {
165
+ const v = e.target.value;
166
+ setFilterCli(v === "" || isKnownCli(v) ? v : "");
167
+ }}
168
+ className="px-2 py-1.5 text-sm bg-input border border-border rounded-md text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent"
169
+ >
170
+ <option value="">All CLIs</option>
171
+ {KNOWN_CLI_IDS.map((id) => (
172
+ <option key={id} value={id}>
173
+ {getCliLabel(id)}
174
+ </option>
175
+ ))}
176
+ </select>
149
177
  </div>
150
178
 
151
179
  {/* Keyword Search */}
@@ -227,7 +255,7 @@ export default function ProjectList({ folders }: ProjectListProps) {
227
255
  aria-label="Filter to date"
228
256
  />
229
257
  </div>
230
- {(filterPreset !== "all" || dateRange.from !== null || dateRange.to !== null || keywords.length > 0) && (
258
+ {(filterPreset !== "all" || dateRange.from !== null || dateRange.to !== null || keywords.length > 0 || filterCli !== "") && (
231
259
  <button
232
260
  onClick={clearFilters}
233
261
  className="px-3 py-2 text-sm bg-muted text-muted-foreground hover:bg-muted/80 rounded-md transition-colors"
@@ -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>