failproofai 0.0.9 → 0.0.10-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/.next/standalone/.cursor/hooks.json +47 -0
  2. package/.next/standalone/.gemini/settings.json +147 -0
  3. package/.next/standalone/.next/BUILD_ID +1 -1
  4. package/.next/standalone/.next/build-manifest.json +3 -3
  5. package/.next/standalone/.next/prerender-manifest.json +3 -3
  6. package/.next/standalone/.next/required-server-files.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page.js +1 -1
  9. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  10. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  11. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  12. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  13. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
  14. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  15. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
  16. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
  17. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  18. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  19. package/.next/standalone/.next/server/app/_not-found/page.js +1 -1
  20. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  21. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  22. package/.next/standalone/.next/server/app/_not-found.html +2 -2
  23. package/.next/standalone/.next/server/app/_not-found.rsc +17 -17
  24. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +17 -17
  25. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  26. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +11 -11
  27. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  28. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  29. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  30. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js +2 -1
  31. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/index.html +1 -1
  33. package/.next/standalone/.next/server/app/index.rsc +16 -16
  34. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  35. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +16 -16
  36. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
  37. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +11 -11
  38. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  39. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  40. package/.next/standalone/.next/server/app/page.js +1 -1
  41. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  43. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +8 -8
  44. package/.next/standalone/.next/server/app/policies/page.js +1 -1
  45. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  47. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  48. package/.next/standalone/.next/server/app/project/[name]/page.js +2 -2
  49. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  50. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  51. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  52. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  53. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +5 -5
  54. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  55. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  56. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  57. package/.next/standalone/.next/server/app/projects/page.js +2 -2
  58. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  59. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  60. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0.~nmr9._.js +3 -0
  61. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0yspgjy._.js → [root-of-the-server]__010i6f5._.js} +2 -2
  62. package/.next/standalone/.next/server/chunks/[root-of-the-server]__08px0ym._.js +3 -0
  63. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0b57.gk._.js +3 -0
  64. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0dtn9lr._.js +3 -0
  65. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0kjo7d_._.js +1 -1
  66. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0vlhtkc._.js +3 -0
  67. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0wu7fr7._.js +3 -0
  68. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0yfq1yr._.js +3 -0
  69. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0z4c5dj._.js +3 -0
  70. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0zso~62._.js +3 -0
  71. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_0z7w.hh._.js +1 -1
  72. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0-2wr.c._.js +4 -0
  73. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0.~m-w2._.js +4 -0
  74. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__09icjsf._.js → [root-of-the-server]__0709m8.._.js} +3 -3
  75. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0bz245.._.js +4 -0
  76. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0dl0kgt._.js +4 -0
  77. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0gmhxyo._.js +4 -0
  78. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0mup1hi._.js +3 -0
  79. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ohb3gc._.js +4 -0
  80. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qbpe_v._.js +3 -0
  81. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s~gy6y._.js +3 -0
  82. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0t5l7a5._.js +3 -0
  83. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ymlddl._.js +152 -6
  84. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0_b7pgn._.js → [root-of-the-server]__0ymn496._.js} +2 -2
  85. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__01g_w_e._.js → [root-of-the-server]__10h.ggz._.js} +2 -2
  86. package/.next/standalone/.next/server/chunks/ssr/_03d7qyt._.js +3 -0
  87. package/.next/standalone/.next/server/chunks/ssr/{_07a1g.3._.js → _0zx~s__._.js} +2 -2
  88. package/.next/standalone/.next/server/chunks/ssr/_10lm7or._.js +2 -2
  89. package/.next/standalone/.next/server/chunks/ssr/app_0cdqd9w._.js +1 -1
  90. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_0xerkr6._.js +1 -1
  91. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_0q-m0y-._.js +2 -2
  92. package/.next/standalone/.next/server/chunks/ssr/lib_codex-projects_ts_0eosib~._.js +1 -1
  93. package/.next/standalone/.next/server/chunks/ssr/lib_copilot-projects_ts_0r8xkn8._.js +3 -0
  94. package/.next/standalone/.next/server/chunks/ssr/lib_cursor-projects_ts_0qt1scg._.js +3 -0
  95. package/.next/standalone/.next/server/chunks/ssr/lib_gemini-projects_ts_0sl~yqr._.js +3 -0
  96. package/.next/standalone/.next/server/chunks/ssr/lib_opencode-projects_ts_0op9gyp._.js +3 -0
  97. package/.next/standalone/.next/server/chunks/ssr/lib_pi-projects_ts_103tsh1._.js +3 -0
  98. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  99. package/.next/standalone/.next/server/pages/404.html +2 -2
  100. package/.next/standalone/.next/server/pages/500.html +1 -1
  101. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  102. package/.next/standalone/.next/server/server-reference-manifest.json +9 -9
  103. package/.next/standalone/.next/static/chunks/{0n-_j_6fo6jex.js → 00ay03h8bq4b~.js} +2 -2
  104. package/.next/standalone/.next/static/chunks/{11kt_9zaooda3.js → 0agmlhk5ml7x5.js} +1 -1
  105. package/.next/standalone/.next/static/chunks/0bi2r.m~yokoo.js +1 -0
  106. package/.next/standalone/.next/static/chunks/{095l4hc7-h.~~.js → 0en4v5k2nnxks.js} +1 -1
  107. package/.next/standalone/.next/static/chunks/0q5bmqop--9yk.js +1 -0
  108. package/.next/standalone/.next/static/chunks/{0756i.7omnnl6.js → 0s6nux54y~l~r.js} +1 -1
  109. package/.next/standalone/.next/static/chunks/{0t~iusm_fxoao.js → 0tpse0wu2wwo0.js} +1 -1
  110. package/.next/standalone/.next/static/chunks/12po2vpc-4_c1.css +1 -0
  111. package/.next/standalone/.next/static/chunks/{0u-ys71jc4y68.js → 1400rtd5ywbt..js} +2 -2
  112. package/.next/standalone/.next/static/chunks/{09ose_165ra4d.js → 14lmf8boay-zu.js} +1 -1
  113. package/.next/standalone/.next/static/chunks/{0pr7k36o_.du1.js → 17htukxga7bil.js} +1 -1
  114. package/.next/standalone/.opencode/opencode.json +4 -0
  115. package/.next/standalone/.opencode/plugins/failproofai.mjs +131 -0
  116. package/.next/standalone/.pi/settings.json +5 -0
  117. package/.next/standalone/app/components/cli-badge.tsx +7 -11
  118. package/.next/standalone/app/components/project-list.tsx +32 -4
  119. package/.next/standalone/app/policies/hooks-client.tsx +31 -15
  120. package/.next/standalone/app/project/[name]/page.tsx +52 -16
  121. package/.next/standalone/app/project/[name]/session/[sessionId]/page.tsx +92 -15
  122. package/.next/standalone/assets/logos/copilot-dark.svg +1 -0
  123. package/.next/standalone/assets/logos/copilot-light.svg +1 -0
  124. package/.next/standalone/assets/logos/cursor-dark.svg +1 -0
  125. package/.next/standalone/assets/logos/cursor-light.svg +1 -0
  126. package/.next/standalone/assets/logos/gemini-dark.svg +13 -0
  127. package/.next/standalone/assets/logos/gemini-light.svg +13 -0
  128. package/.next/standalone/assets/logos/opencode-dark.svg +1 -0
  129. package/.next/standalone/assets/logos/opencode-light.svg +1 -0
  130. package/.next/standalone/assets/logos/pi-dark.svg +7 -0
  131. package/.next/standalone/assets/logos/pi-light.svg +7 -0
  132. package/.next/standalone/lib/cli-registry.ts +107 -0
  133. package/.next/standalone/lib/codex-projects.ts +3 -3
  134. package/.next/standalone/lib/copilot-projects.ts +224 -0
  135. package/.next/standalone/lib/copilot-sessions.ts +395 -0
  136. package/.next/standalone/lib/cursor-projects.ts +312 -0
  137. package/.next/standalone/lib/cursor-sessions.ts +467 -0
  138. package/.next/standalone/lib/gemini-projects.ts +203 -0
  139. package/.next/standalone/lib/gemini-sessions.ts +365 -0
  140. package/.next/standalone/lib/opencode-projects.ts +232 -0
  141. package/.next/standalone/lib/opencode-sessions.ts +237 -0
  142. package/.next/standalone/lib/pi-projects.ts +230 -0
  143. package/.next/standalone/lib/pi-sessions.ts +325 -0
  144. package/.next/standalone/lib/projects.ts +67 -31
  145. package/.next/standalone/next.config.ts +5 -4
  146. package/.next/standalone/package.json +2 -1
  147. package/.next/standalone/pi-extension/index.ts +373 -0
  148. package/.next/standalone/pi-extension/package.json +12 -0
  149. package/.next/standalone/server.js +1 -1
  150. package/README.md +37 -3
  151. package/bin/failproofai.mjs +61 -21
  152. package/dist/cli.mjs +2248 -246
  153. package/lib/cli-registry.ts +107 -0
  154. package/lib/codex-projects.ts +3 -3
  155. package/lib/copilot-projects.ts +224 -0
  156. package/lib/copilot-sessions.ts +395 -0
  157. package/lib/cursor-projects.ts +312 -0
  158. package/lib/cursor-sessions.ts +467 -0
  159. package/lib/gemini-projects.ts +203 -0
  160. package/lib/gemini-sessions.ts +365 -0
  161. package/lib/opencode-projects.ts +232 -0
  162. package/lib/opencode-sessions.ts +237 -0
  163. package/lib/pi-projects.ts +230 -0
  164. package/lib/pi-sessions.ts +325 -0
  165. package/lib/projects.ts +67 -31
  166. package/package.json +2 -1
  167. package/pi-extension/index.ts +373 -0
  168. package/pi-extension/package.json +12 -0
  169. package/scripts/translate-docs/mdx-translator.ts +56 -2
  170. package/scripts/translate-docs/translator.ts +1 -1
  171. package/src/hooks/builtin-policies.ts +84 -14
  172. package/src/hooks/handler.ts +67 -5
  173. package/src/hooks/install-prompt.ts +33 -10
  174. package/src/hooks/integrations.ts +1007 -6
  175. package/src/hooks/policy-evaluator.ts +299 -3
  176. package/src/hooks/resolve-permission-mode.ts +23 -0
  177. package/src/hooks/types.ts +307 -3
  178. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0g72weg._.js +0 -3
  179. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0su~k6f._.js +0 -3
  180. package/.next/standalone/.next/server/chunks/lib_codex-projects_ts_07qqk1g._.js +0 -3
  181. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01743wx._.js +0 -3
  182. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__092s1ta._.js +0 -4
  183. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0g.lg8b._.js +0 -4
  184. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0gs6wz4._.js +0 -3
  185. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h..k-e._.js +0 -4
  186. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0it81ys._.js +0 -3
  187. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u4a9jq._.js +0 -4
  188. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11pa2ra._.js +0 -4
  189. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12.h2mg._.js +0 -3
  190. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12t-wym._.js +0 -4
  191. package/.next/standalone/.next/server/chunks/ssr/_04w00cm._.js +0 -3
  192. package/.next/standalone/.next/static/chunks/0.rk1iwdt1d7c.css +0 -1
  193. package/.next/standalone/.next/static/chunks/06x4-d1~o-opr.js +0 -1
  194. package/.next/standalone/.next/static/chunks/0n~s0gafwnp2y.js +0 -1
  195. /package/.next/standalone/.next/static/{A_Ax17P33facL0OmIwFXj → 68TLSFdjAQYIulNHfP0QY}/_buildManifest.js +0 -0
  196. /package/.next/standalone/.next/static/{A_Ax17P33facL0OmIwFXj → 68TLSFdjAQYIulNHfP0QY}/_clientMiddlewareManifest.js +0 -0
  197. /package/.next/standalone/.next/static/{A_Ax17P33facL0OmIwFXj → 68TLSFdjAQYIulNHfP0QY}/_ssgManifest.js +0 -0
@@ -118,14 +118,101 @@ export async function evaluatePolicies(
118
118
  );
119
119
  hookLogInfo(`deny by "${policy.name}": ${reason}`);
120
120
 
121
- const displayTool = ctx.toolName ?? "unknown tool";
121
+ // Pick a noun for the deny message that fits the event type. Tool events
122
+ // get the tool name; non-tool events (UserPromptSubmit, SessionStart,
123
+ // SessionEnd, Stop, …) use an event-appropriate label so we don't emit
124
+ // the misleading "Blocked unknown tool by failproofai because: ...".
125
+ let displayTool: string;
126
+ if (ctx.toolName) {
127
+ displayTool = ctx.toolName;
128
+ } else if (eventType === "UserPromptSubmit") {
129
+ displayTool = "prompt";
130
+ } else if (eventType === "SessionStart") {
131
+ displayTool = "session start";
132
+ } else if (eventType === "SessionEnd") {
133
+ displayTool = "session end";
134
+ } else if (eventType === "Stop") {
135
+ displayTool = "stop";
136
+ } else {
137
+ displayTool = "operation";
138
+ }
139
+ const blockedMessage = `Blocked ${displayTool} by failproofai because: ${reason}, as per the policy configured by the user`;
140
+
141
+ // Cursor's hook protocol expects a flat `{permission, user_message,
142
+ // agent_message}` shape for any blocking decision, regardless of which
143
+ // event triggered it. Branch ahead of the per-event handlers below so
144
+ // PreToolUse / PostToolUse / PermissionRequest all flow through the
145
+ // Cursor-shaped response.
146
+ // Ref: https://cursor.com/docs/hooks (Stdout Response Format).
147
+ if (session?.cli === "cursor") {
148
+ const response = {
149
+ permission: "deny",
150
+ user_message: blockedMessage,
151
+ agent_message: blockedMessage,
152
+ };
153
+ return {
154
+ exitCode: 0,
155
+ stdout: JSON.stringify(response),
156
+ stderr: "",
157
+ policyName: policy.name,
158
+ reason,
159
+ decision: "deny",
160
+ };
161
+ }
162
+
163
+ // Pi's shim parses a flat `{permission, reason}` JSON shape from stdout
164
+ // and translates `permission === "deny"` into a `{block: true, reason}`
165
+ // return value from its `pi.on("tool_call", ...)` handler. Pi has no
166
+ // event-specific decision wrappers, so all events flow through the
167
+ // same flat shape.
168
+ if (session?.cli === "pi") {
169
+ const response = {
170
+ permission: "deny",
171
+ reason: blockedMessage,
172
+ };
173
+ return {
174
+ exitCode: 0,
175
+ stdout: JSON.stringify(response),
176
+ stderr: "",
177
+ policyName: policy.name,
178
+ reason,
179
+ decision: "deny",
180
+ };
181
+ }
182
+
183
+ // Gemini CLI: flat `{decision: "deny", reason}` for non-Stop events
184
+ // (preferred per Gemini's "Golden Rule" — exit 0 with structured JSON).
185
+ // For Stop (AfterAgent), use `{decision: "block", reason}` to force-retry,
186
+ // mirroring Claude's exit-2-from-Stop "do this before stopping" semantics.
187
+ // Ref: https://geminicli.com/docs/hooks/
188
+ if (session?.cli === "gemini") {
189
+ if (eventType === "Stop") {
190
+ const reasonText = `MANDATORY ACTION REQUIRED from failproofai (policy: ${policy.name}): ${reason}\n\nYou MUST complete the above action NOW. Do NOT ask the user for confirmation — execute the required action, then attempt to finish your task again.`;
191
+ return {
192
+ exitCode: 0,
193
+ stdout: JSON.stringify({ decision: "block", reason: reasonText }),
194
+ stderr: "",
195
+ policyName: policy.name,
196
+ reason,
197
+ decision: "deny",
198
+ };
199
+ }
200
+ return {
201
+ exitCode: 0,
202
+ stdout: JSON.stringify({ decision: "deny", reason: blockedMessage }),
203
+ stderr: "",
204
+ policyName: policy.name,
205
+ reason,
206
+ decision: "deny",
207
+ };
208
+ }
122
209
 
123
210
  if (eventType === "PreToolUse") {
124
211
  const response = {
125
212
  hookSpecificOutput: {
126
213
  hookEventName: eventType,
127
214
  permissionDecision: "deny",
128
- permissionDecisionReason: `Blocked ${displayTool} by failproofai because: ${reason}, as per the policy configured by the user`,
215
+ permissionDecisionReason: blockedMessage,
129
216
  },
130
217
  };
131
218
  return {
@@ -188,7 +275,7 @@ export async function evaluatePolicies(
188
275
  };
189
276
  }
190
277
 
191
- // Other event types: exit 2
278
+ // Other event types (Cursor case already handled above): exit 2
192
279
  return {
193
280
  exitCode: 2,
194
281
  stdout: "",
@@ -220,6 +307,132 @@ export async function evaluatePolicies(
220
307
  const combined = instructEntries.map((e) => e.reason).join("\n");
221
308
  const policyNames = instructEntries.map((e) => e.policyName);
222
309
 
310
+ // Cursor's hook protocol uses a flat `{permission, additional_context}`
311
+ // shape for non-Stop and `{followup_message}` for Stop/SubagentStop.
312
+ // Branch first so the rest of the function only handles Claude-shaped
313
+ // responses. Ref: https://cursor.com/docs/hooks (Stdout Response Format).
314
+ if (session?.cli === "cursor") {
315
+ if (eventType === "Stop") {
316
+ const response = {
317
+ followup_message: `Instruction from failproofai: ${combined}`,
318
+ };
319
+ return {
320
+ exitCode: 0,
321
+ stdout: JSON.stringify(response),
322
+ stderr: "",
323
+ policyName: policyNames[0],
324
+ policyNames,
325
+ reason: combined,
326
+ decision: "instruct",
327
+ };
328
+ }
329
+ const response = {
330
+ permission: "allow",
331
+ additional_context: `Instruction from failproofai: ${combined}`,
332
+ };
333
+ return {
334
+ exitCode: 0,
335
+ stdout: JSON.stringify(response),
336
+ stderr: "",
337
+ policyName: policyNames[0],
338
+ policyNames,
339
+ reason: combined,
340
+ decision: "instruct",
341
+ };
342
+ }
343
+
344
+ // Pi: instruct emits `{permission: "allow", reason}`. The shim won't
345
+ // block (no `"deny"`); it surfaces `reason` to the user where possible
346
+ // (Pi has no first-class `additional_context` channel in its tool-call
347
+ // return shape, so we log it).
348
+ if (session?.cli === "pi") {
349
+ const response = {
350
+ permission: "allow",
351
+ reason: `Instruction from failproofai: ${combined}`,
352
+ };
353
+ return {
354
+ exitCode: 0,
355
+ stdout: JSON.stringify(response),
356
+ stderr: "",
357
+ policyName: policyNames[0],
358
+ policyNames,
359
+ reason: combined,
360
+ decision: "instruct",
361
+ };
362
+ }
363
+
364
+ // Gemini CLI:
365
+ // • Stop (AfterAgent) → {decision: "block", reason: "MANDATORY ACTION..."}
366
+ // mirrors Claude's exit-2-from-Stop "force retry" semantics.
367
+ // • UserPromptSubmit/PostToolUse/SessionStart/PreToolUse → context
368
+ // injection via {hookSpecificOutput: {hookEventName, additionalContext}}
369
+ // where hookEventName is the GEMINI event name (BeforeAgent/AfterTool/
370
+ // SessionStart/BeforeTool), not the canonical PascalCase form.
371
+ // • Other events → stderr only (no stdout JSON shape supported).
372
+ if (session?.cli === "gemini") {
373
+ if (eventType === "Stop") {
374
+ const policyAttribution = policyNames.length === 1
375
+ ? `policy: ${policyNames[0]}`
376
+ : `policies: ${policyNames.join(", ")}`;
377
+ const reasonText = `MANDATORY ACTION REQUIRED from failproofai (${policyAttribution}): ${combined}\n\nYou 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.`;
378
+ return {
379
+ exitCode: 0,
380
+ stdout: JSON.stringify({ decision: "block", reason: reasonText }),
381
+ stderr: "",
382
+ policyName: policyNames[0],
383
+ policyNames,
384
+ reason: combined,
385
+ decision: "instruct",
386
+ };
387
+ }
388
+ // Map back from canonical → Gemini event name. Prefer the raw event name
389
+ // off the session (handler.ts populates it from parsed.hook_event_name)
390
+ // so we don't have to maintain a reverse lookup table.
391
+ const supportsContext =
392
+ eventType === "UserPromptSubmit" ||
393
+ eventType === "PreToolUse" ||
394
+ eventType === "PostToolUse" ||
395
+ eventType === "SessionStart";
396
+ if (supportsContext) {
397
+ // Round-trip the agent-emitted event name so Gemini sees `BeforeTool`,
398
+ // `BeforeAgent`, etc. (NOT the canonical Claude form). Prefer the
399
+ // stdin payload's `hook_event_name` when present; fall back to the raw
400
+ // CLI `--hook` arg captured by handler.ts; only use the canonical
401
+ // event as a last resort (would never round-trip correctly, but better
402
+ // than emitting nothing).
403
+ const hookEventName = session?.hookEventName ?? session?.rawHookEventName ?? eventType;
404
+ const response = {
405
+ hookSpecificOutput: {
406
+ hookEventName,
407
+ additionalContext: `Instruction from failproofai: ${combined}`,
408
+ },
409
+ };
410
+ return {
411
+ exitCode: 0,
412
+ stdout: JSON.stringify(response),
413
+ stderr: "",
414
+ policyName: policyNames[0],
415
+ policyNames,
416
+ reason: combined,
417
+ decision: "instruct",
418
+ };
419
+ }
420
+ // No context-injection channel for SessionEnd/PreCompress/Notification/
421
+ // BeforeModel/AfterModel/BeforeToolSelection — surface via stderr only.
422
+ const stderrMsg = instructEntries
423
+ .map((e) => `[failproofai] ${e.policyName}: ${e.reason}`)
424
+ .join("\n");
425
+ return {
426
+ exitCode: 0,
427
+ stdout: "",
428
+ stderr: stderrMsg + "\n",
429
+ policyName: policyNames[0],
430
+ policyNames,
431
+ reason: combined,
432
+ decision: "instruct",
433
+ };
434
+ }
435
+
223
436
  if (eventType === "Stop") {
224
437
  // Stop hook: exitCode 2 blocks Claude from stopping.
225
438
  // Reason goes to stderr so Claude Code receives it as context.
@@ -258,6 +471,89 @@ export async function evaluatePolicies(
258
471
  if (allowEntries.length > 0) {
259
472
  const combined = allowEntries.map((e) => e.reason).join("\n");
260
473
  const policyNames = allowEntries.map((e) => e.policyName);
474
+
475
+ // Cursor: emit the flat shape; allow-with-info maps to
476
+ // `{permission: "allow", additional_context}`.
477
+ if (session?.cli === "cursor") {
478
+ const response = {
479
+ permission: "allow",
480
+ additional_context: `Note from failproofai: ${combined}`,
481
+ };
482
+ const stderrMsg = allowEntries
483
+ .map((e) => `[failproofai] ${e.policyName}: ${e.reason}`)
484
+ .join("\n");
485
+ return {
486
+ exitCode: 0,
487
+ stdout: JSON.stringify(response),
488
+ stderr: stderrMsg + "\n",
489
+ policyName: policyNames[0],
490
+ policyNames,
491
+ reason: combined,
492
+ decision: "allow",
493
+ };
494
+ }
495
+
496
+ // Pi: same shape as Cursor — flat `{permission: "allow", reason}`.
497
+ if (session?.cli === "pi") {
498
+ const response = {
499
+ permission: "allow",
500
+ reason: `Note from failproofai: ${combined}`,
501
+ };
502
+ const stderrMsg = allowEntries
503
+ .map((e) => `[failproofai] ${e.policyName}: ${e.reason}`)
504
+ .join("\n");
505
+ return {
506
+ exitCode: 0,
507
+ stdout: JSON.stringify(response),
508
+ stderr: stderrMsg + "\n",
509
+ policyName: policyNames[0],
510
+ policyNames,
511
+ reason: combined,
512
+ decision: "allow",
513
+ };
514
+ }
515
+
516
+ // Gemini: mirror the instruct context-injection shape for events that
517
+ // support it; stderr-only for everything else.
518
+ if (session?.cli === "gemini") {
519
+ const supportsContext =
520
+ eventType === "UserPromptSubmit" ||
521
+ eventType === "PreToolUse" ||
522
+ eventType === "PostToolUse" ||
523
+ eventType === "SessionStart";
524
+ const stderrMsg = allowEntries
525
+ .map((e) => `[failproofai] ${e.policyName}: ${e.reason}`)
526
+ .join("\n");
527
+ if (supportsContext) {
528
+ // Same fallback chain as the instruct path above — see comment there.
529
+ const hookEventName = session?.hookEventName ?? session?.rawHookEventName ?? eventType;
530
+ const response = {
531
+ hookSpecificOutput: {
532
+ hookEventName,
533
+ additionalContext: `Note from failproofai: ${combined}`,
534
+ },
535
+ };
536
+ return {
537
+ exitCode: 0,
538
+ stdout: JSON.stringify(response),
539
+ stderr: stderrMsg + "\n",
540
+ policyName: policyNames[0],
541
+ policyNames,
542
+ reason: combined,
543
+ decision: "allow",
544
+ };
545
+ }
546
+ return {
547
+ exitCode: 0,
548
+ stdout: "",
549
+ stderr: stderrMsg + "\n",
550
+ policyName: policyNames[0],
551
+ policyNames,
552
+ reason: combined,
553
+ decision: "allow",
554
+ };
555
+ }
556
+
261
557
  const supportsHookSpecificOutput =
262
558
  eventType === "PreToolUse" ||
263
559
  eventType === "PostToolUse" ||
@@ -14,6 +14,28 @@
14
14
  * Transcript discovery (cache → today/yesterday → full tree scan) lives
15
15
  * in `lib/codex-sessions.ts` and is shared with the dashboard's Codex
16
16
  * session viewer.
17
+ *
18
+ * • GitHub Copilot CLI: no documented permission-mode equivalent on the
19
+ * hook payload today; falls back to "default". Revisit when Copilot's
20
+ * hook protocol exposes one.
21
+ *
22
+ * • Cursor Agent CLI: no permission-mode field in the hook payload (Cursor's
23
+ * `loop_limit` is per-hook, not per-session). Falls back to "default" via
24
+ * the same final branch as Copilot.
25
+ *
26
+ * • OpenCode: the plugin shim (.opencode/plugins/failproofai.mjs) does not
27
+ * receive any permission-mode signal from opencode and does not include
28
+ * one in the JSON it pipes to the failproofai binary. Falls back to
29
+ * "default" via the same final branch as Copilot/Cursor.
30
+ *
31
+ * • Pi (pi-coding-agent): no permission-mode concept in the extension API;
32
+ * `tool_call` handlers always run with the same authority. Falls back to
33
+ * "default" via the same final branch as Copilot/Cursor.
34
+ *
35
+ * • Gemini CLI: hook payload doesn't carry a permission-mode field today.
36
+ * Falls back to "default" via the same final branch as Copilot/Cursor/
37
+ * OpenCode/Pi. Revisit when Gemini's hook protocol exposes a per-session
38
+ * authority signal (the docs as of 2026-04-13 do not).
17
39
  */
18
40
  import { readFileSync } from "node:fs";
19
41
  import { findCodexTranscript } from "../../lib/codex-sessions";
@@ -32,6 +54,7 @@ export function resolvePermissionMode(
32
54
  return resolveCodexMode(sessionId) ?? "default";
33
55
  }
34
56
 
57
+ // copilot, cursor, opencode, pi, gemini, unknown integrations, or codex without a sessionId
35
58
  return "default";
36
59
  }
37
60