failproofai 0.0.10-beta.8 → 0.0.10

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 (174) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +7 -7
  3. package/.next/standalone/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +4 -4
  6. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  11. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
  12. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  13. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
  14. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
  15. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  16. package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +4 -4
  17. package/.next/standalone/.next/server/app/_not-found/page/next-font-manifest.json +6 -2
  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 +2 -2
  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 +1 -30
  23. package/.next/standalone/.next/server/app/_not-found.rsc +21 -26
  24. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +21 -26
  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 +10 -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 +1 -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 +21 -21
  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 +21 -21
  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 +10 -11
  38. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +3 -2
  39. package/.next/standalone/.next/server/app/page/build-manifest.json +4 -4
  40. package/.next/standalone/.next/server/app/page/next-font-manifest.json +6 -2
  41. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  42. package/.next/standalone/.next/server/app/page.js +2 -2
  43. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  45. package/.next/standalone/.next/server/app/policies/page/build-manifest.json +4 -4
  46. package/.next/standalone/.next/server/app/policies/page/next-font-manifest.json +6 -2
  47. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +8 -8
  48. package/.next/standalone/.next/server/app/policies/page.js +2 -2
  49. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  50. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  51. package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +4 -4
  52. package/.next/standalone/.next/server/app/project/[name]/page/next-font-manifest.json +6 -2
  53. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  54. package/.next/standalone/.next/server/app/project/[name]/page.js +3 -3
  55. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  56. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  57. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +4 -4
  58. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/next-font-manifest.json +6 -2
  59. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  60. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  61. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +3 -3
  62. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  63. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  64. package/.next/standalone/.next/server/app/projects/page/build-manifest.json +4 -4
  65. package/.next/standalone/.next/server/app/projects/page/next-font-manifest.json +6 -2
  66. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  67. package/.next/standalone/.next/server/app/projects/page.js +3 -3
  68. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  69. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  70. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0fjhqi9._.js → [root-of-the-server]__044xt9.._.js} +2 -2
  71. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0d_ob4n._.js +1 -1
  72. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0fe7_q_._.js +1 -1
  73. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0vlhtkc._.js +1 -1
  74. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0yfq1yr._.js +1 -1
  75. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_0z7w.hh._.js +1 -1
  76. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0370~qj._.js +3 -0
  77. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0mup1hi._.js → [root-of-the-server]__0609ezh._.js} +1 -1
  78. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__07_-mkc._.js +3 -0
  79. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0e9o9ri._.js +4 -0
  80. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0og6yo7._.js → [root-of-the-server]__0l6swv1._.js} +2 -2
  81. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0logebz._.js +3 -0
  82. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0mi5ejy._.js +2 -2
  83. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0joez.y._.js → [root-of-the-server]__0podumr._.js} +3 -3
  84. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0rkxer-._.js +3 -0
  85. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0rl2kwi._.js +4 -0
  86. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0t5l7a5._.js +1 -1
  87. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0vg0uey._.js +4 -0
  88. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ye1w50._.js +4 -0
  89. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ymlddl._.js +32 -7
  90. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__10._f0s._.js +4 -0
  91. package/.next/standalone/.next/server/chunks/ssr/_03d7qyt._.js +1 -1
  92. package/.next/standalone/.next/server/chunks/ssr/_0xb8ngh._.js +3 -0
  93. package/.next/standalone/.next/server/chunks/ssr/app_0cdqd9w._.js +1 -1
  94. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_0xerkr6._.js +1 -1
  95. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_0q-m0y-._.js +2 -2
  96. package/.next/standalone/.next/server/chunks/ssr/lib_gemini-projects_ts_0sl~yqr._.js +1 -1
  97. package/.next/standalone/.next/server/chunks/ssr/lib_opencode-projects_ts_0op9gyp._.js +1 -1
  98. package/.next/standalone/.next/server/middleware-build-manifest.js +7 -7
  99. package/.next/standalone/.next/server/next-font-manifest.js +1 -1
  100. package/.next/standalone/.next/server/next-font-manifest.json +21 -2
  101. package/.next/standalone/.next/server/pages/404.html +1 -30
  102. package/.next/standalone/.next/server/pages/500.html +1 -1
  103. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  104. package/.next/standalone/.next/server/server-reference-manifest.json +9 -9
  105. package/.next/standalone/.next/static/chunks/{0k2yq8zevk9bl.js → 0j171xiqge4rv.js} +1 -1
  106. package/.next/standalone/.next/static/chunks/0kqar56yl~41o.js +6 -0
  107. package/.next/standalone/.next/static/chunks/{07i9r0t6n4cpy.js → 0lt8ko3lw.5yt.js} +1 -1
  108. package/.next/standalone/.next/static/chunks/0ml1.ck_5t36i.js +1 -0
  109. package/.next/standalone/.next/static/chunks/{0km4.rc8uvt_t.js → 0pkl..xgo-qox.js} +1 -1
  110. package/.next/standalone/.next/static/chunks/{12simlrcfk3g2.js → 0rnqmir4cd5p9.js} +2 -2
  111. package/.next/standalone/.next/static/chunks/{0bi2r.m~yokoo.js → 0w6fzf.07a24u.js} +1 -1
  112. package/.next/standalone/.next/static/chunks/0xbo5nl6w4lka.js +1 -0
  113. package/.next/standalone/.next/static/chunks/12l2t63hkyo2q.js +1 -0
  114. package/.next/standalone/.next/static/chunks/{0tyw4u3~2isbh.js → 12pt~2f.c1sha.js} +1 -1
  115. package/.next/standalone/.next/static/chunks/{0by8zx1no6kt4.js → 14lii11wmo450.js} +1 -1
  116. package/.next/standalone/.next/static/chunks/179yytvmam0ug.js +1 -0
  117. package/.next/standalone/.next/static/chunks/17rm86uz2nd5a.css +2 -0
  118. package/.next/standalone/.next/static/chunks/{turbopack-0o7k.hakttp4k.js → turbopack-05z7a19q43zfq.js} +1 -1
  119. package/.next/standalone/.next/static/media/4fa387ec64143e14-s.0q3udbd2bu5yp.woff2 +0 -0
  120. package/.next/standalone/.next/static/media/797e433ab948586e-s.p.0.q-h669a_dqa.woff2 +0 -0
  121. package/.next/standalone/.next/static/media/bbc41e54d2fcbd21-s.0gw~uztddq1df.woff2 +0 -0
  122. package/.next/standalone/.opencode/plugins/failproofai.mjs +75 -15
  123. package/.next/standalone/app/actions/get-hooks-config.ts +25 -1
  124. package/.next/standalone/app/components/cli-badge.tsx +1 -1
  125. package/.next/standalone/app/globals.css +68 -111
  126. package/.next/standalone/app/layout.tsx +16 -56
  127. package/.next/standalone/app/policies/hooks-client.tsx +228 -44
  128. package/.next/standalone/components/navbar.tsx +16 -15
  129. package/.next/standalone/components/ui/button.tsx +4 -4
  130. package/.next/standalone/lib/gemini-projects.ts +64 -24
  131. package/.next/standalone/lib/opencode-projects.ts +9 -7
  132. package/.next/standalone/package.json +2 -2
  133. package/.next/standalone/pi-extension/index.ts +113 -12
  134. package/.next/standalone/readme-arch-hq.gif +0 -0
  135. package/.next/standalone/server.js +1 -1
  136. package/README.md +54 -241
  137. package/dist/cli.mjs +195 -75
  138. package/lib/gemini-projects.ts +64 -24
  139. package/lib/opencode-projects.ts +9 -7
  140. package/package.json +2 -2
  141. package/pi-extension/index.ts +113 -12
  142. package/scripts/launch.ts +6 -22
  143. package/scripts/parse-script-args.ts +1 -11
  144. package/scripts/translate-docs/config.ts +0 -1
  145. package/src/hooks/handler.ts +63 -6
  146. package/src/hooks/integrations.ts +31 -6
  147. package/src/hooks/policy-evaluator.ts +34 -2
  148. package/src/hooks/types.ts +52 -0
  149. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01hj~sd._.js +0 -4
  150. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02r6nu-._.js +0 -3
  151. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04dywib._.js +0 -4
  152. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__06sb2gn._.js +0 -4
  153. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__09jpajs._.js +0 -4
  154. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0jm6jnh._.js +0 -3
  155. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0t2k4c5._.js +0 -3
  156. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u1i~9~._.js +0 -4
  157. package/.next/standalone/.next/server/chunks/ssr/_02_tcps._.js +0 -32
  158. package/.next/standalone/.next/server/chunks/ssr/_10lm7or._.js +0 -5
  159. package/.next/standalone/.next/server/chunks/ssr/_11rg2a_._.js +0 -3
  160. package/.next/standalone/.next/static/chunks/0gq8kvc3blri~.js +0 -1
  161. package/.next/standalone/.next/static/chunks/0q5bmqop--9yk.js +0 -1
  162. package/.next/standalone/.next/static/chunks/0s41ggdsb2alw.js +0 -3
  163. package/.next/standalone/.next/static/chunks/0t_7i~pqwbcww.js +0 -6
  164. package/.next/standalone/.next/static/chunks/0xr8w5io1-kb9.css +0 -1
  165. package/.next/standalone/.next/static/chunks/164g0yuhpb2pi.js +0 -1
  166. package/.next/standalone/components/logo.tsx +0 -36
  167. package/.next/standalone/components/theme-toggle.tsx +0 -37
  168. package/.next/standalone/contexts/ThemeContext.tsx +0 -69
  169. package/.next/standalone/failproofai-hq.gif +0 -0
  170. package/.next/standalone/public/exospheresmall-dark.png +0 -0
  171. package/.next/standalone/public/exospheresmall.png +0 -0
  172. /package/.next/standalone/.next/static/{0PSH56_4bbPBaHiyPkthl → dAuQps6jUwCz9X1Q5FFOO}/_buildManifest.js +0 -0
  173. /package/.next/standalone/.next/static/{0PSH56_4bbPBaHiyPkthl → dAuQps6jUwCz9X1Q5FFOO}/_clientMiddlewareManifest.js +0 -0
  174. /package/.next/standalone/.next/static/{0PSH56_4bbPBaHiyPkthl → dAuQps6jUwCz9X1Q5FFOO}/_ssgManifest.js +0 -0
package/dist/cli.mjs CHANGED
@@ -16,7 +16,7 @@ var __export = (target, all) => {
16
16
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
17
 
18
18
  // src/hooks/types.ts
19
- var HOOK_SCOPES, INTEGRATION_TYPES, CODEX_HOOK_SCOPES, CODEX_HOOK_EVENT_TYPES, CODEX_EVENT_MAP, CODEX_TOOL_MAP, COPILOT_HOOK_SCOPES, COPILOT_HOOK_EVENT_TYPES, COPILOT_TOOL_MAP, CURSOR_HOOK_SCOPES, CURSOR_HOOK_EVENT_TYPES, CURSOR_EVENT_MAP, CURSOR_TOOL_MAP, OPENCODE_HOOK_SCOPES, OPENCODE_HOOK_EVENT_TYPES, PI_HOOK_SCOPES, PI_HOOK_EVENT_TYPES, PI_EVENT_MAP, GEMINI_HOOK_SCOPES, GEMINI_HOOK_EVENT_TYPES, GEMINI_EVENT_MAP, GEMINI_TOOL_MAP, HOOK_EVENT_TYPES, FAILPROOFAI_HOOK_MARKER = "__failproofai_hook__";
19
+ var HOOK_SCOPES, INTEGRATION_TYPES, CODEX_HOOK_SCOPES, CODEX_HOOK_EVENT_TYPES, CODEX_EVENT_MAP, CODEX_TOOL_MAP, COPILOT_HOOK_SCOPES, COPILOT_HOOK_EVENT_TYPES, COPILOT_TOOL_MAP, CURSOR_HOOK_SCOPES, CURSOR_HOOK_EVENT_TYPES, CURSOR_EVENT_MAP, CURSOR_TOOL_MAP, OPENCODE_HOOK_SCOPES, OPENCODE_HOOK_EVENT_TYPES, OPENCODE_TOOL_MAP, OPENCODE_TOOL_INPUT_MAP, PI_HOOK_SCOPES, PI_HOOK_EVENT_TYPES, PI_EVENT_MAP, PI_TOOL_MAP, PI_TOOL_INPUT_MAP, GEMINI_HOOK_SCOPES, GEMINI_HOOK_EVENT_TYPES, GEMINI_EVENT_MAP, GEMINI_TOOL_MAP, HOOK_EVENT_TYPES, FAILPROOFAI_HOOK_MARKER = "__failproofai_hook__";
20
20
  var init_types = __esm(() => {
21
21
  HOOK_SCOPES = ["user", "project", "local"];
22
22
  INTEGRATION_TYPES = ["claude", "codex", "copilot", "cursor", "opencode", "pi", "gemini"];
@@ -108,6 +108,30 @@ var init_types = __esm(() => {
108
108
  "message.updated",
109
109
  "permission.ask"
110
110
  ];
111
+ OPENCODE_TOOL_MAP = {
112
+ bash: "Bash",
113
+ read: "Read",
114
+ write: "Write",
115
+ edit: "Edit",
116
+ apply_patch: "Edit",
117
+ glob: "Glob",
118
+ grep: "Grep",
119
+ list: "LS",
120
+ webfetch: "WebFetch",
121
+ websearch: "WebSearch",
122
+ todowrite: "TodoWrite",
123
+ todoread: "TodoRead"
124
+ };
125
+ OPENCODE_TOOL_INPUT_MAP = {
126
+ Read: { filePath: "file_path" },
127
+ Write: { filePath: "file_path" },
128
+ Edit: {
129
+ filePath: "file_path",
130
+ oldString: "old_string",
131
+ newString: "new_string",
132
+ replaceAll: "replace_all"
133
+ }
134
+ };
111
135
  PI_HOOK_SCOPES = ["user", "project"];
112
136
  PI_HOOK_EVENT_TYPES = [
113
137
  "session_start",
@@ -127,6 +151,19 @@ var init_types = __esm(() => {
127
151
  tool_result: "PostToolUse",
128
152
  agent_end: "Stop"
129
153
  };
154
+ PI_TOOL_MAP = {
155
+ bash: "Bash",
156
+ read: "Read",
157
+ write: "Write",
158
+ edit: "Edit",
159
+ glob: "Glob",
160
+ grep: "Grep"
161
+ };
162
+ PI_TOOL_INPUT_MAP = {
163
+ Read: { path: "file_path" },
164
+ Write: { path: "file_path" },
165
+ Edit: { path: "file_path" }
166
+ };
130
167
  GEMINI_HOOK_SCOPES = ["user", "project"];
131
168
  GEMINI_HOOK_EVENT_TYPES = [
132
169
  "SessionStart",
@@ -2089,6 +2126,19 @@ You MUST complete the above action NOW. Do NOT ask the user for confirmation —
2089
2126
  };
2090
2127
  }
2091
2128
  if (session?.cli === "pi") {
2129
+ if (eventType === "Stop") {
2130
+ const reasonText = `MANDATORY ACTION REQUIRED from failproofai (policy: ${policy.name}): ${reason}
2131
+
2132
+ You MUST complete the above action NOW. Do NOT ask the user for confirmation — execute the required action, then attempt to finish your task again.`;
2133
+ return {
2134
+ exitCode: 0,
2135
+ stdout: JSON.stringify({ permission: "deny", reason: reasonText }),
2136
+ stderr: "",
2137
+ policyName: policy.name,
2138
+ reason,
2139
+ decision: "deny"
2140
+ };
2141
+ }
2092
2142
  const response = {
2093
2143
  permission: "deny",
2094
2144
  reason: blockedMessage
@@ -2267,6 +2317,21 @@ You MUST complete the above action NOW. Do NOT ask the user for confirmation —
2267
2317
  };
2268
2318
  }
2269
2319
  if (session?.cli === "pi") {
2320
+ if (eventType === "Stop") {
2321
+ const policyAttribution = policyNames.length === 1 ? `policy: ${policyNames[0]}` : `policies: ${policyNames.join(", ")}`;
2322
+ const reasonText = `MANDATORY ACTION REQUIRED from failproofai (${policyAttribution}): ${combined}
2323
+
2324
+ You MUST complete the above action(s) NOW. Do NOT ask the user for confirmation — execute the required action(s), then attempt to finish your task again.`;
2325
+ return {
2326
+ exitCode: 0,
2327
+ stdout: JSON.stringify({ permission: "deny", reason: reasonText }),
2328
+ stderr: "",
2329
+ policyName: policyNames[0],
2330
+ policyNames,
2331
+ reason: combined,
2332
+ decision: "instruct"
2333
+ };
2334
+ }
2270
2335
  const response2 = {
2271
2336
  permission: "allow",
2272
2337
  reason: `Instruction from failproofai: ${combined}`
@@ -2864,7 +2929,7 @@ var init_hook_activity_store = __esm(() => {
2864
2929
  });
2865
2930
 
2866
2931
  // package.json
2867
- var version2 = "0.0.10-beta.8";
2932
+ var version2 = "0.0.10";
2868
2933
  var init_package = () => {};
2869
2934
 
2870
2935
  // src/posthog-key.ts
@@ -3609,7 +3674,6 @@ __export(exports_opencode_projects, {
3609
3674
  getCachedOpenCodeProjects: () => getCachedOpenCodeProjects
3610
3675
  });
3611
3676
  import { execFileSync as execFileSync2 } from "node:child_process";
3612
- import { basename as basename2 } from "node:path";
3613
3677
  function runOpenCodeDb(sql) {
3614
3678
  try {
3615
3679
  const stdout = execFileSync2("opencode", ["db", "--format", "json", sql], {
@@ -3661,7 +3725,7 @@ async function getOpenCodeProjects() {
3661
3725
  seen.add(projectId);
3662
3726
  const proj = projectMap.get(projectId);
3663
3727
  const worktree = proj?.worktree ?? group.rows[0]?.directory ?? null;
3664
- const name = proj?.name?.trim() || (worktree ? basename2(worktree) : projectId);
3728
+ const name = worktree ? encodeFolderName(worktree) : projectId;
3665
3729
  const path = worktree ?? "";
3666
3730
  const lastModified = new Date(Math.max(group.latest, proj?.time_updated ?? 0));
3667
3731
  out.push({
@@ -3677,7 +3741,7 @@ async function getOpenCodeProjects() {
3677
3741
  if (seen.has(p.id))
3678
3742
  continue;
3679
3743
  const worktree = p.worktree ?? "";
3680
- const name = p.name?.trim() || (worktree ? basename2(worktree) : p.id);
3744
+ const name = worktree ? encodeFolderName(worktree) : p.id;
3681
3745
  const lastModified = new Date(p.time_updated);
3682
3746
  out.push({
3683
3747
  name,
@@ -3930,7 +3994,7 @@ __export(exports_gemini_projects, {
3930
3994
  getCachedGeminiSessionsByEncodedName: () => getCachedGeminiSessionsByEncodedName,
3931
3995
  getCachedGeminiProjects: () => getCachedGeminiProjects
3932
3996
  });
3933
- import { readdir as readdir5, readFile as readFile6, stat as stat5 } from "node:fs/promises";
3997
+ import { open as open3, readdir as readdir5, readFile as readFile6, stat as stat5 } from "node:fs/promises";
3934
3998
  import { homedir as homedir11 } from "node:os";
3935
3999
  import { join as join9 } from "node:path";
3936
4000
  function getGeminiTmpRoot() {
@@ -3950,6 +4014,37 @@ async function statMtime4(path) {
3950
4014
  return null;
3951
4015
  }
3952
4016
  }
4017
+ async function readFirstLine2(filePath) {
4018
+ let fh = null;
4019
+ try {
4020
+ fh = await open3(filePath, "r");
4021
+ const buf = Buffer.alloc(FIRST_LINE_CHUNK_BYTES2);
4022
+ const { bytesRead } = await fh.read(buf, 0, FIRST_LINE_CHUNK_BYTES2, 0);
4023
+ if (bytesRead === 0)
4024
+ return null;
4025
+ const slice = buf.subarray(0, bytesRead);
4026
+ const nl = slice.indexOf(10);
4027
+ const end = nl === -1 ? bytesRead : nl;
4028
+ return slice.subarray(0, end).toString("utf-8");
4029
+ } catch {
4030
+ return null;
4031
+ } finally {
4032
+ if (fh)
4033
+ await fh.close().catch(() => {});
4034
+ }
4035
+ }
4036
+ function extractFullSessionId(line) {
4037
+ if (!line)
4038
+ return;
4039
+ try {
4040
+ const meta = JSON.parse(line);
4041
+ if (typeof meta.sessionId !== "string")
4042
+ return;
4043
+ return UUID_RE.test(meta.sessionId) ? meta.sessionId : undefined;
4044
+ } catch {
4045
+ return;
4046
+ }
4047
+ }
3953
4048
  async function readProjectRoot(projectDir) {
3954
4049
  try {
3955
4050
  const text = await readFile6(join9(projectDir, ".project_root"), "utf-8");
@@ -3983,7 +4078,8 @@ async function scanGeminiSessions() {
3983
4078
  const mtime = await statMtime4(filePath);
3984
4079
  if (!mtime)
3985
4080
  continue;
3986
- out.push({ filePath, sessionFilename: f.name, cwd, fileMtime: mtime });
4081
+ const sessionId = extractFullSessionId(await readFirstLine2(filePath));
4082
+ out.push({ filePath, sessionFilename: f.name, cwd, fileMtime: mtime, sessionId });
3987
4083
  }
3988
4084
  }), 16);
3989
4085
  return out;
@@ -4011,17 +4107,14 @@ async function getGeminiProjects() {
4011
4107
  async function getGeminiSessionsForCwd(cwd) {
4012
4108
  const sessions = await scanGeminiSessions();
4013
4109
  const matches = sessions.filter((s) => s.cwd === cwd);
4014
- const files = matches.map((s) => {
4015
- const m = s.sessionFilename.match(SESSION_FILE_RE2);
4016
- return {
4017
- name: s.sessionFilename,
4018
- path: s.filePath,
4019
- lastModified: s.fileMtime,
4020
- lastModifiedFormatted: formatDate(s.fileMtime),
4021
- sessionId: m ? m[2] : undefined,
4022
- cli: "gemini"
4023
- };
4024
- });
4110
+ const files = matches.map((s) => ({
4111
+ name: s.sessionFilename,
4112
+ path: s.filePath,
4113
+ lastModified: s.fileMtime,
4114
+ lastModifiedFormatted: formatDate(s.fileMtime),
4115
+ sessionId: s.sessionId,
4116
+ cli: "gemini"
4117
+ }));
4025
4118
  files.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
4026
4119
  return files;
4027
4120
  }
@@ -4038,25 +4131,24 @@ async function getGeminiSessionsByEncodedName(name) {
4038
4131
  if (uniqueCwds.length !== 1) {
4039
4132
  return { cwd: null, sessions: [] };
4040
4133
  }
4041
- const sessions = matches.map((s) => {
4042
- const m = s.sessionFilename.match(SESSION_FILE_RE2);
4043
- return {
4044
- name: s.sessionFilename,
4045
- path: s.filePath,
4046
- lastModified: s.fileMtime,
4047
- lastModifiedFormatted: formatDate(s.fileMtime),
4048
- sessionId: m ? m[2] : undefined,
4049
- cli: "gemini"
4050
- };
4051
- });
4134
+ const sessions = matches.map((s) => ({
4135
+ name: s.sessionFilename,
4136
+ path: s.filePath,
4137
+ lastModified: s.fileMtime,
4138
+ lastModifiedFormatted: formatDate(s.fileMtime),
4139
+ sessionId: s.sessionId,
4140
+ cli: "gemini"
4141
+ }));
4052
4142
  sessions.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
4053
4143
  return { cwd: uniqueCwds[0], sessions };
4054
4144
  }
4055
- var SESSION_FILE_RE2, getCachedGeminiProjects, getCachedGeminiSessionsByEncodedName;
4145
+ var SESSION_FILE_RE2, UUID_RE, FIRST_LINE_CHUNK_BYTES2, getCachedGeminiProjects, getCachedGeminiSessionsByEncodedName;
4056
4146
  var init_gemini_projects = __esm(() => {
4057
4147
  init_paths();
4058
4148
  init_logger();
4059
4149
  SESSION_FILE_RE2 = /^session-(\d{4}-\d{2}-\d{2}T\d{2}-\d{2})-([0-9a-f]{8})\.jsonl$/i;
4150
+ UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
4151
+ FIRST_LINE_CHUNK_BYTES2 = 4 * 1024;
4060
4152
  getCachedGeminiProjects = runtimeCache(getGeminiProjects, 30);
4061
4153
  getCachedGeminiSessionsByEncodedName = runtimeCache((name) => getGeminiSessionsByEncodedName(name), 30, { maxSize: 50 });
4062
4154
  });
@@ -5041,7 +5133,7 @@ var init_copilot_sessions = __esm(() => {
5041
5133
  // lib/cursor-sessions.ts
5042
5134
  import { readFileSync as readFileSync6, readdirSync as readdirSync5, existsSync as existsSync7, statSync as statSync6 } from "node:fs";
5043
5135
  import { readFile as readFile10 } from "node:fs/promises";
5044
- import { basename as basename3, join as join15, resolve as resolve7, sep as sep3 } from "node:path";
5136
+ import { basename as basename2, join as join15, resolve as resolve7, sep as sep3 } from "node:path";
5045
5137
  import { homedir as homedir14 } from "node:os";
5046
5138
  function getCursorHome2() {
5047
5139
  return process.env.CURSOR_HOME || join15(homedir14(), ".cursor");
@@ -5079,7 +5171,7 @@ function findCursorTranscript(sessionId) {
5079
5171
  const dir = getCursorSessionDir(sessionId);
5080
5172
  if (!dir)
5081
5173
  return null;
5082
- const newCandidate = join15(dir, `${basename3(dir)}.jsonl`);
5174
+ const newCandidate = join15(dir, `${basename2(dir)}.jsonl`);
5083
5175
  if (existsSync7(newCandidate))
5084
5176
  return newCandidate;
5085
5177
  for (const name of LEGACY_TRANSCRIPT_FILE_CANDIDATES2) {
@@ -5303,7 +5395,7 @@ function getPiSessionStateRoot() {
5303
5395
  return process.env.PI_SESSIONS_DIR || join16(homedir15(), ".pi", "agent", "sessions");
5304
5396
  }
5305
5397
  function isSafeSessionId(sessionId) {
5306
- return UUID_RE.test(sessionId);
5398
+ return UUID_RE2.test(sessionId);
5307
5399
  }
5308
5400
  function findPiTranscript(sessionId) {
5309
5401
  if (!isSafeSessionId(sessionId))
@@ -5475,10 +5567,10 @@ async function getPiSessionLog(sessionId) {
5475
5567
  filePath
5476
5568
  };
5477
5569
  }
5478
- var UUID_RE, SESSION_FILE_RE3, getCachedPiSessionLog;
5570
+ var UUID_RE2, SESSION_FILE_RE3, getCachedPiSessionLog;
5479
5571
  var init_pi_sessions = __esm(() => {
5480
5572
  init_log_entries();
5481
- UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
5573
+ UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
5482
5574
  SESSION_FILE_RE3 = /^[\d-]+T[\d-]+Z_([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i;
5483
5575
  getCachedPiSessionLog = runtimeCache((sessionId) => getPiSessionLog(sessionId), 60, { maxSize: 50 });
5484
5576
  });
@@ -5492,7 +5584,7 @@ function getGeminiSessionStateRoot() {
5492
5584
  return process.env.GEMINI_SESSIONS_DIR || join17(homedir16(), ".gemini", "tmp");
5493
5585
  }
5494
5586
  function isSafeSessionId2(sessionId) {
5495
- return UUID_RE2.test(sessionId);
5587
+ return UUID_RE3.test(sessionId);
5496
5588
  }
5497
5589
  function readFirstLineSync(path) {
5498
5590
  let fd;
@@ -5703,10 +5795,10 @@ async function getGeminiSessionLog(sessionId) {
5703
5795
  filePath
5704
5796
  };
5705
5797
  }
5706
- var UUID_RE2, SESSION_FILE_RE4, getCachedGeminiSessionLog;
5798
+ var UUID_RE3, SESSION_FILE_RE4, getCachedGeminiSessionLog;
5707
5799
  var init_gemini_sessions = __esm(() => {
5708
5800
  init_log_entries();
5709
- UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
5801
+ UUID_RE3 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
5710
5802
  SESSION_FILE_RE4 = /^session-(.+)-([0-9a-f]{8})\.jsonl$/i;
5711
5803
  getCachedGeminiSessionLog = runtimeCache((sessionId) => getGeminiSessionLog(sessionId), 60, { maxSize: 50 });
5712
5804
  });
@@ -6397,8 +6489,28 @@ function canonicalizeToolName(raw, cli) {
6397
6489
  return CODEX_TOOL_MAP[raw] ?? raw;
6398
6490
  if (cli === "gemini")
6399
6491
  return GEMINI_TOOL_MAP[raw] ?? raw;
6492
+ if (cli === "opencode")
6493
+ return OPENCODE_TOOL_MAP[raw] ?? raw;
6494
+ if (cli === "pi")
6495
+ return PI_TOOL_MAP[raw] ?? raw;
6400
6496
  return raw;
6401
6497
  }
6498
+ function canonicalizeToolInput(toolName, rawInput, cli) {
6499
+ if (!toolName || !rawInput || typeof rawInput !== "object")
6500
+ return rawInput;
6501
+ let perToolMap;
6502
+ if (cli === "opencode")
6503
+ perToolMap = OPENCODE_TOOL_INPUT_MAP[toolName];
6504
+ else if (cli === "pi")
6505
+ perToolMap = PI_TOOL_INPUT_MAP[toolName];
6506
+ if (!perToolMap)
6507
+ return rawInput;
6508
+ const out = {};
6509
+ for (const [k, v] of Object.entries(rawInput)) {
6510
+ out[perToolMap[k] ?? k] = v;
6511
+ }
6512
+ return out;
6513
+ }
6402
6514
  async function handleHookEvent(eventType, cli = "claude") {
6403
6515
  const startTime = performance.now();
6404
6516
  const MAX_STDIN_BYTES = 1048576;
@@ -6440,6 +6552,11 @@ async function handleHookEvent(eventType, cli = "claude") {
6440
6552
  if (canonicalToolName !== rawToolName) {
6441
6553
  parsed.tool_name = canonicalToolName;
6442
6554
  }
6555
+ const rawInput = parsed.tool_input;
6556
+ const canonicalInput = canonicalizeToolInput(canonicalToolName, rawInput, cli);
6557
+ if (canonicalInput !== rawInput) {
6558
+ parsed.tool_input = canonicalInput;
6559
+ }
6443
6560
  const sessionId = parsed.session_id;
6444
6561
  const session = {
6445
6562
  sessionId,
@@ -6955,6 +7072,28 @@ function canonicalizeTool(raw) {
6955
7072
  return TOOL_NAME_MAP[raw] != null ? TOOL_NAME_MAP[raw] : raw;
6956
7073
  }
6957
7074
 
7075
+ // Per-tool input-key translation: opencode native tools deliver args as
7076
+ // camelCase (\`filePath\`, \`oldString\`, …) but failproofai builtin policies
7077
+ // (\`block-read-outside-cwd\`, \`block-env-files\`, \`block-secrets-write\`)
7078
+ // read \`ctx.toolInput.file_path\` etc. Without this map every Read/Write/Edit
7079
+ // path-check silently no-ops on opencode. Keys are PascalCase canonical tool
7080
+ // names so the lookup pairs with canonicalizeTool's output. Tools outside the
7081
+ // map (MCP \`mcp_*\`, plugins) pass through unchanged. Keep in sync with
7082
+ // OPENCODE_TOOL_INPUT_MAP in failproofai/src/hooks/types.ts.
7083
+ const TOOL_INPUT_MAP = {
7084
+ Read: { filePath: "file_path" },
7085
+ Write: { filePath: "file_path" },
7086
+ Edit: { filePath: "file_path", oldString: "old_string", newString: "new_string", replaceAll: "replace_all" },
7087
+ };
7088
+ function canonicalizeToolInput(canonicalToolName, args) {
7089
+ if (!args || typeof args !== "object") return args;
7090
+ const map = TOOL_INPUT_MAP[canonicalToolName];
7091
+ if (!map) return args;
7092
+ const out = {};
7093
+ for (const k of Object.keys(args)) out[map[k] != null ? map[k] : k] = args[k];
7094
+ return out;
7095
+ }
7096
+
6958
7097
  const FAILPROOFAI_BIN = ${escapedBin};
6959
7098
  const USE_NPX = ${useNpx};
6960
7099
 
@@ -7053,11 +7192,12 @@ export default async function failproofaiPlugin({ client, directory }) {
7053
7192
 
7054
7193
  // First-class PreToolUse hook. Note: tool args live on output.args (mutable).
7055
7194
  "tool.execute.before": async (input, output) => {
7195
+ const canonicalTool = canonicalizeTool(input.tool);
7056
7196
  const r = runFailproofai("PreToolUse", {
7057
7197
  session_id: input.sessionID,
7058
7198
  cwd: directory,
7059
- tool_name: canonicalizeTool(input.tool),
7060
- tool_input: output.args,
7199
+ tool_name: canonicalTool,
7200
+ tool_input: canonicalizeToolInput(canonicalTool, output.args),
7061
7201
  hook_event_name: "PreToolUse",
7062
7202
  }, directory);
7063
7203
  await applyDecision(r, { client, sessionID: input.sessionID }, "PreToolUse");
@@ -7065,11 +7205,12 @@ export default async function failproofaiPlugin({ client, directory }) {
7065
7205
 
7066
7206
  // First-class PostToolUse hook. Note: tool args live on input.args here.
7067
7207
  "tool.execute.after": async (input, output) => {
7208
+ const canonicalTool = canonicalizeTool(input.tool);
7068
7209
  const r = runFailproofai("PostToolUse", {
7069
7210
  session_id: input.sessionID,
7070
7211
  cwd: directory,
7071
- tool_name: canonicalizeTool(input.tool),
7072
- tool_input: input.args,
7212
+ tool_name: canonicalTool,
7213
+ tool_input: canonicalizeToolInput(canonicalTool, input.args),
7073
7214
  tool_response: { title: output.title, output: output.output, metadata: output.metadata },
7074
7215
  hook_event_name: "PostToolUse",
7075
7216
  }, directory);
@@ -7078,11 +7219,12 @@ export default async function failproofaiPlugin({ client, directory }) {
7078
7219
 
7079
7220
  // Cleaner deny UX for prompted tools — mutate output.status instead of throwing.
7080
7221
  "permission.ask": async (input, output) => {
7222
+ const canonicalTool = canonicalizeTool(input.tool);
7081
7223
  const r = runFailproofai("PermissionRequest", {
7082
7224
  session_id: input.sessionID,
7083
7225
  cwd: directory,
7084
- tool_name: canonicalizeTool(input.tool) || input.command || "permission",
7085
- tool_input: input,
7226
+ tool_name: canonicalTool || input.command || "permission",
7227
+ tool_input: canonicalizeToolInput(canonicalTool, input),
7086
7228
  hook_event_name: "PermissionRequest",
7087
7229
  }, directory);
7088
7230
  try {
@@ -8153,7 +8295,7 @@ __export(exports_manager, {
8153
8295
  });
8154
8296
  import { execSync as execSync4 } from "node:child_process";
8155
8297
  import { existsSync as existsSync16 } from "node:fs";
8156
- import { resolve as resolve11, basename as basename4 } from "node:path";
8298
+ import { resolve as resolve11, basename as basename3 } from "node:path";
8157
8299
  import { homedir as homedir23, platform, arch, release, hostname } from "node:os";
8158
8300
  function getSettingsPath(scope, cwd) {
8159
8301
  return claudeCode.getSettingsPath(scope, cwd);
@@ -8584,15 +8726,15 @@ Failproof AI Hook Policies
8584
8726
  try {
8585
8727
  const hooks = await loadCustomHooks(file);
8586
8728
  if (hooks.length === 0) {
8587
- const filename = basename4(file);
8729
+ const filename = basename3(file);
8588
8730
  console.log(` \x1B[31m✗\x1B[0m ${filename.padEnd(nameColWidth)}\x1B[31mfailed to load\x1B[0m`);
8589
8731
  } else {
8590
- const filename = basename4(file);
8732
+ const filename = basename3(file);
8591
8733
  const hookSummary = hooks.map((h) => h.name).join(", ");
8592
8734
  console.log(` \x1B[32m✓\x1B[0m ${filename.padEnd(nameColWidth)}${hooks.length} hook(s): ${hookSummary}`);
8593
8735
  }
8594
8736
  } catch {
8595
- const filename = basename4(file);
8737
+ const filename = basename3(file);
8596
8738
  console.log(` \x1B[31m✗\x1B[0m ${filename.padEnd(nameColWidth)}\x1B[31merror\x1B[0m`);
8597
8739
  }
8598
8740
  }
@@ -9285,7 +9427,6 @@ function parseStringFlag(flagName, errorLabel, inlineValue, args, index, options
9285
9427
  }
9286
9428
  function parseScriptArgs(argv) {
9287
9429
  const args = [...argv];
9288
- let claudeProjectsPath;
9289
9430
  let loggingLevel;
9290
9431
  let disableTelemetry = false;
9291
9432
  let allowedDevOrigins;
@@ -9294,13 +9435,6 @@ function parseScriptArgs(argv) {
9294
9435
  const eqIdx = arg.indexOf("=");
9295
9436
  const flag = eqIdx >= 0 ? arg.slice(0, eqIdx) : arg;
9296
9437
  const inlineValue = eqIdx >= 0 ? arg.slice(eqIdx + 1) : null;
9297
- if (flag === "--projects-path" || flag === "-p") {
9298
- const { value, spliceCount } = parseStringFlag(flag, "a path argument", inlineValue, args, i);
9299
- claudeProjectsPath = value;
9300
- args.splice(i, spliceCount);
9301
- i--;
9302
- continue;
9303
- }
9304
9438
  if (flag === "--logging") {
9305
9439
  const raw = inlineValue ?? args[i + 1];
9306
9440
  if (raw === undefined || inlineValue === null && raw.startsWith("-")) {
@@ -9331,7 +9465,7 @@ function parseScriptArgs(argv) {
9331
9465
  continue;
9332
9466
  }
9333
9467
  }
9334
- return { claudeProjectsPath, loggingLevel, disableTelemetry, allowedDevOrigins, remainingArgs: args };
9468
+ return { loggingLevel, disableTelemetry, allowedDevOrigins, remainingArgs: args };
9335
9469
  }
9336
9470
  var init_parse_script_args = () => {};
9337
9471
 
@@ -9477,27 +9611,15 @@ import { realpathSync as realpathSync2, existsSync as existsSync19 } from "node:
9477
9611
  import { resolve as resolve14, dirname as dirname8 } from "node:path";
9478
9612
  import { fileURLToPath as fileURLToPath2 } from "node:url";
9479
9613
  function launch(mode) {
9480
- const { claudeProjectsPath: parsedPath, loggingLevel, disableTelemetry, allowedDevOrigins, remainingArgs } = parseScriptArgs(process.argv.slice(2));
9614
+ const { loggingLevel, disableTelemetry, allowedDevOrigins, remainingArgs } = parseScriptArgs(process.argv.slice(2));
9481
9615
  console.log(`
9482
- ______ _ __ ____ ___ ____
9483
- / ____/___ _(_) /___ _________ ____ / __/ / | / _/
9484
- / /_ / __ \`/ / / __ \\/ ___/ __ \\/ __ \\/ /_ / /| | / /
9485
- / __/ / /_/ / / / /_/ / / / /_/ / /_/ / __/ / ___ |_/ /
9486
- /_/ \\__,_/_/_/ .___/_/ \\____/\\____/_/ /_/ |_/___/
9487
- /_/ v${version2}
9616
+ failproof ai
9488
9617
  `);
9618
+ console.log(` \uD83D\uDCE6 Version: ${version2}`);
9489
9619
  console.log(` ⭐ Star us: https://github.com/exospherehost/failproofai`);
9490
9620
  console.log(` \uD83D\uDCD6 Docs: https://befailproof.ai`);
9491
9621
  console.log(` \uD83D\uDCAC Slack: https://join.slack.com/t/failproofai/shared_invite/zt-3v63b7k5e-O3NBHmj8X6n9gZSGDx6ggQ
9492
9622
  `);
9493
- let claudeProjectsPath = parsedPath;
9494
- if (!claudeProjectsPath) {
9495
- claudeProjectsPath = getDefaultClaudeProjectsPath();
9496
- console.log(`Using default .claude projects path: ${claudeProjectsPath}`);
9497
- } else {
9498
- console.log(`Using custom .claude projects path: ${claudeProjectsPath}`);
9499
- }
9500
- process.env.CLAUDE_PROJECTS_PATH = claudeProjectsPath;
9501
9623
  let cmd;
9502
9624
  let cmdArgs;
9503
9625
  if (mode === "start") {
@@ -9545,7 +9667,6 @@ Error: Cannot find server.js at:
9545
9667
  stdio: "inherit",
9546
9668
  env: {
9547
9669
  ...process.env,
9548
- CLAUDE_PROJECTS_PATH: claudeProjectsPath,
9549
9670
  ...loggingLevel ? { FAILPROOFAI_LOG_LEVEL: loggingLevel } : {},
9550
9671
  ...disableTelemetry ? { FAILPROOFAI_TELEMETRY_DISABLED: "1" } : {},
9551
9672
  ...allowedDevOrigins ? { FAILPROOFAI_ALLOWED_DEV_ORIGINS: allowedDevOrigins.join(",") } : {}
@@ -9560,7 +9681,6 @@ Error: Cannot find server.js at:
9560
9681
  });
9561
9682
  }
9562
9683
  var init_launch = __esm(() => {
9563
- init_paths();
9564
9684
  init_parse_script_args();
9565
9685
  init_install_diagnosis();
9566
9686
  init_package();
@@ -9588,7 +9708,7 @@ import { realpathSync as realpathSync3 } from "fs";
9588
9708
  import { dirname as dirname9, resolve as resolve15 } from "path";
9589
9709
  import { fileURLToPath as fileURLToPath3 } from "url";
9590
9710
  // package.json
9591
- var version = "0.0.10-beta.8";
9711
+ var version = "0.0.10";
9592
9712
 
9593
9713
  // bin/failproofai.mjs
9594
9714
  if (!process.env.FAILPROOFAI_PACKAGE_ROOT) {
@@ -21,7 +21,7 @@
21
21
  * missing `~/.gemini/` returns `[]`, malformed JSONL falls open without
22
22
  * surfacing the session.
23
23
  */
24
- import { readdir, readFile, stat } from "node:fs/promises";
24
+ import { open, readdir, readFile, stat } from "node:fs/promises";
25
25
  import { homedir } from "node:os";
26
26
  import { join } from "node:path";
27
27
  import type { ProjectFolder, SessionFile } from "./projects";
@@ -34,6 +34,13 @@ import { logWarn } from "./logger";
34
34
  /** Filename pattern for a Gemini session JSONL:
35
35
  * `session-<ISO-timestamp-with-dashes>-<8-hex-uuid-prefix>.jsonl`. */
36
36
  const SESSION_FILE_RE = /^session-(\d{4}-\d{2}-\d{2}T\d{2}-\d{2})-([0-9a-f]{8})\.jsonl$/i;
37
+ /** Full UUID — the filename only embeds the first 8 hex chars; the rest is on
38
+ * the JSONL metadata header line. The session detail route requires a full
39
+ * UUID, so links built from the truncated filename prefix 404. */
40
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
41
+ /** Metadata header sits on line 1 and is well under 1 KB; 4 KB covers it
42
+ * comfortably without slurping a multi-MB transcript. */
43
+ const FIRST_LINE_CHUNK_BYTES = 4 * 1024;
37
44
 
38
45
  /** Override for tests. Defaults to the live Gemini session-state root. */
39
46
  function getGeminiTmpRoot(): string {
@@ -46,6 +53,10 @@ interface GeminiSessionMeta {
46
53
  sessionFilename: string;
47
54
  cwd: string;
48
55
  fileMtime: Date;
56
+ /** Full UUID parsed from the JSONL metadata header (line 1). Undefined when
57
+ * the header is missing, malformed, or carries a non-UUID `sessionId`;
58
+ * callers fall through to rendering an un-linked row. */
59
+ sessionId?: string;
49
60
  }
50
61
 
51
62
  async function safeReaddir(dir: string) {
@@ -64,6 +75,40 @@ async function statMtime(path: string): Promise<Date | null> {
64
75
  }
65
76
  }
66
77
 
78
+ /** Read the first newline-delimited line of `filePath` without slurping the
79
+ * rest. Mirrors `lib/codex-projects.ts`'s readFirstLine; the Gemini metadata
80
+ * header is always on line 1. */
81
+ async function readFirstLine(filePath: string): Promise<string | null> {
82
+ let fh: Awaited<ReturnType<typeof open>> | null = null;
83
+ try {
84
+ fh = await open(filePath, "r");
85
+ const buf = Buffer.alloc(FIRST_LINE_CHUNK_BYTES);
86
+ const { bytesRead } = await fh.read(buf, 0, FIRST_LINE_CHUNK_BYTES, 0);
87
+ if (bytesRead === 0) return null;
88
+ const slice = buf.subarray(0, bytesRead);
89
+ const nl = slice.indexOf(0x0a); // '\n'
90
+ const end = nl === -1 ? bytesRead : nl;
91
+ return slice.subarray(0, end).toString("utf-8");
92
+ } catch {
93
+ return null;
94
+ } finally {
95
+ if (fh) await fh.close().catch(() => {});
96
+ }
97
+ }
98
+
99
+ /** Extract a full-UUID `sessionId` from a JSONL metadata header line. Returns
100
+ * undefined on parse failure, missing field, or a non-UUID value. */
101
+ function extractFullSessionId(line: string | null): string | undefined {
102
+ if (!line) return undefined;
103
+ try {
104
+ const meta = JSON.parse(line) as { sessionId?: unknown };
105
+ if (typeof meta.sessionId !== "string") return undefined;
106
+ return UUID_RE.test(meta.sessionId) ? meta.sessionId : undefined;
107
+ } catch {
108
+ return undefined;
109
+ }
110
+ }
111
+
67
112
  /** Read `.project_root` to recover the absolute cwd for a basename folder.
68
113
  * Returns null if missing or empty (caller treats the folder as un-mappable). */
69
114
  async function readProjectRoot(projectDir: string): Promise<string | null> {
@@ -101,7 +146,8 @@ async function scanGeminiSessions(): Promise<GeminiSessionMeta[]> {
101
146
  const filePath = join(chatsDir, f.name);
102
147
  const mtime = await statMtime(filePath);
103
148
  if (!mtime) continue;
104
- out.push({ filePath, sessionFilename: f.name, cwd, fileMtime: mtime });
149
+ const sessionId = extractFullSessionId(await readFirstLine(filePath));
150
+ out.push({ filePath, sessionFilename: f.name, cwd, fileMtime: mtime, sessionId });
105
151
  }
106
152
  }),
107
153
  16,
@@ -140,17 +186,14 @@ export async function getGeminiProjects(): Promise<ProjectFolder[]> {
140
186
  export async function getGeminiSessionsForCwd(cwd: string): Promise<SessionFile[]> {
141
187
  const sessions = await scanGeminiSessions();
142
188
  const matches = sessions.filter((s) => s.cwd === cwd);
143
- const files: SessionFile[] = matches.map((s) => {
144
- const m = s.sessionFilename.match(SESSION_FILE_RE);
145
- return {
146
- name: s.sessionFilename,
147
- path: s.filePath,
148
- lastModified: s.fileMtime,
149
- lastModifiedFormatted: formatDate(s.fileMtime),
150
- sessionId: m ? m[2] : undefined,
151
- cli: "gemini",
152
- };
153
- });
189
+ const files: SessionFile[] = matches.map((s) => ({
190
+ name: s.sessionFilename,
191
+ path: s.filePath,
192
+ lastModified: s.fileMtime,
193
+ lastModifiedFormatted: formatDate(s.fileMtime),
194
+ sessionId: s.sessionId,
195
+ cli: "gemini",
196
+ }));
154
197
  files.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
155
198
  return files;
156
199
  }
@@ -180,17 +223,14 @@ export async function getGeminiSessionsByEncodedName(name: string): Promise<Gemi
180
223
  if (uniqueCwds.length !== 1) {
181
224
  return { cwd: null, sessions: [] };
182
225
  }
183
- const sessions = matches.map((s) => {
184
- const m = s.sessionFilename.match(SESSION_FILE_RE);
185
- return {
186
- name: s.sessionFilename,
187
- path: s.filePath,
188
- lastModified: s.fileMtime,
189
- lastModifiedFormatted: formatDate(s.fileMtime),
190
- sessionId: m ? m[2] : undefined,
191
- cli: "gemini" as const,
192
- };
193
- });
226
+ const sessions = matches.map((s) => ({
227
+ name: s.sessionFilename,
228
+ path: s.filePath,
229
+ lastModified: s.fileMtime,
230
+ lastModifiedFormatted: formatDate(s.fileMtime),
231
+ sessionId: s.sessionId,
232
+ cli: "gemini" as const,
233
+ }));
194
234
  sessions.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
195
235
  return { cwd: uniqueCwds[0], sessions };
196
236
  }