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
@@ -5,8 +5,16 @@
5
5
  * ~/.failproofai/policies-config.json, evaluates matching policies, persists
6
6
  * activity to disk, and returns the appropriate exit code + stdout response.
7
7
  */
8
- import type { HookEventType, IntegrationType, SessionMetadata, CodexHookEventType } from "./types";
9
- import { CODEX_EVENT_MAP } from "./types";
8
+ import type {
9
+ HookEventType,
10
+ IntegrationType,
11
+ SessionMetadata,
12
+ CodexHookEventType,
13
+ CursorHookEventType,
14
+ PiHookEventType,
15
+ GeminiHookEventType,
16
+ } from "./types";
17
+ import { CODEX_EVENT_MAP, CURSOR_EVENT_MAP, PI_EVENT_MAP, GEMINI_EVENT_MAP, GEMINI_TOOL_MAP } from "./types";
10
18
  import type { PolicyFunction, PolicyResult } from "./policy-types";
11
19
  import { readMergedHooksConfig } from "./hooks-config";
12
20
  import { registerBuiltinPolicies } from "./builtin-policies";
@@ -22,18 +30,56 @@ import { hookLogInfo, hookLogWarn } from "./hook-logger";
22
30
 
23
31
  /**
24
32
  * Canonicalize an event name to PascalCase. Codex sends snake_case event names
25
- * on stdin and as the --hook arg; Claude Code sends PascalCase. The internal
26
- * registry, builtin policies, and policy.match.events all key on PascalCase.
33
+ * on stdin and as the --hook arg; Cursor sends camelCase (`preToolUse`,
34
+ * `beforeSubmitPrompt`); Pi sends underscore_lower_snake_case (`tool_call`,
35
+ * `session_start`); Claude Code sends PascalCase. Copilot CLI is installed
36
+ * in "VS Code compatible" PascalCase mode (see integrations.ts), so its events
37
+ * arrive PascalCase already. Gemini also sends PascalCase but with different
38
+ * names (`BeforeTool`, `BeforeAgent`, `AfterAgent`); we map via GEMINI_EVENT_MAP.
39
+ * The internal registry, builtin policies, and policy.match.events all key on
40
+ * PascalCase.
27
41
  */
28
42
  function canonicalizeEventType(raw: string, cli: IntegrationType): HookEventType {
29
43
  if (cli === "codex") {
30
44
  const mapped = CODEX_EVENT_MAP[raw as CodexHookEventType];
31
45
  if (mapped) return mapped;
32
46
  }
33
- // Already PascalCase or unknown — pass through; HOOK_EVENT_TYPES type-checks downstream.
47
+ if (cli === "cursor") {
48
+ const mapped = CURSOR_EVENT_MAP[raw as CursorHookEventType];
49
+ if (mapped) return mapped;
50
+ }
51
+ if (cli === "pi") {
52
+ const mapped = PI_EVENT_MAP[raw as PiHookEventType];
53
+ if (mapped) return mapped;
54
+ }
55
+ if (cli === "gemini") {
56
+ const mapped = GEMINI_EVENT_MAP[raw as GeminiHookEventType];
57
+ if (mapped) return mapped;
58
+ }
59
+ // claude / copilot / unknown — already PascalCase, pass through.
60
+ // HOOK_EVENT_TYPES type-checks downstream.
34
61
  return raw as HookEventType;
35
62
  }
36
63
 
64
+ /**
65
+ * Canonicalize a per-CLI tool name to the Claude PascalCase form that builtin
66
+ * policies match on (e.g. `Bash`, `Read`, `Write`, `Edit`). Today only Gemini
67
+ * needs this — its tools are snake_case (`run_shell_command`, `read_file`,
68
+ * `write_file`, `replace`, …). Other CLIs pass through unchanged: Claude /
69
+ * Codex / Copilot already use PascalCase, and Cursor / Pi pre-canonicalize
70
+ * inside their own plugin shims before the payload reaches this binary.
71
+ *
72
+ * Unknown tool names (MCP `mcp_*`, third-party extensions, Skills) pass
73
+ * through unchanged so non-builtin tooling isn't lost.
74
+ */
75
+ function canonicalizeToolName(raw: string | undefined, cli: IntegrationType): string | undefined {
76
+ if (!raw) return raw;
77
+ if (cli === "gemini") {
78
+ return GEMINI_TOOL_MAP[raw] ?? raw;
79
+ }
80
+ return raw;
81
+ }
82
+
37
83
  export async function handleHookEvent(
38
84
  eventType: string,
39
85
  cli: IntegrationType = "claude",
@@ -79,6 +125,17 @@ export async function handleHookEvent(
79
125
  // Canonicalize event name (Codex sends snake_case; internals expect PascalCase)
80
126
  const canonicalEventType = canonicalizeEventType(eventType, cli);
81
127
 
128
+ // Canonicalize tool name in place so both the policy-registry tool-name
129
+ // filter and policy bodies (`ctx.toolName === "Bash"`) see the canonical
130
+ // form. Today only Gemini's snake_case names need translation; other CLIs
131
+ // are no-ops here. Mutating `parsed.tool_name` keeps the activity store +
132
+ // telemetry tagging consistent (they read from `parsed.tool_name`).
133
+ const rawToolName = parsed.tool_name as string | undefined;
134
+ const canonicalToolName = canonicalizeToolName(rawToolName, cli);
135
+ if (canonicalToolName !== rawToolName) {
136
+ parsed.tool_name = canonicalToolName;
137
+ }
138
+
82
139
  // Extract session metadata from payload
83
140
  const sessionId = parsed.session_id as string | undefined;
84
141
  const session: SessionMetadata = {
@@ -87,6 +144,11 @@ export async function handleHookEvent(
87
144
  cwd: parsed.cwd as string | undefined,
88
145
  permissionMode: resolvePermissionMode(cli, parsed, sessionId),
89
146
  hookEventName: parsed.hook_event_name as string | undefined,
147
+ // Preserve the raw CLI-side event name (eventType arg) before
148
+ // canonicalization. Response shapes that round-trip the agent-emitted
149
+ // event name (e.g. Gemini's `hookSpecificOutput.hookEventName`) prefer
150
+ // this over the canonicalized form when stdin omits hook_event_name.
151
+ rawHookEventName: eventType,
90
152
  cli,
91
153
  };
92
154
 
@@ -30,8 +30,13 @@ export interface PromptOptions {
30
30
  includeBeta?: boolean;
31
31
  }
32
32
 
33
+ /** Whether the prompt is being shown for an install or an uninstall flow.
34
+ * Drives heading + hint text so `policies --uninstall` no longer says
35
+ * "Install Hooks". */
36
+ export type CliPromptAction = "install" | "uninstall";
37
+
33
38
  /**
34
- * Resolve which agent CLIs to install hooks for.
39
+ * Resolve which agent CLIs to install/uninstall hooks for.
35
40
  *
36
41
  * Rules:
37
42
  * • If `explicit` is provided (from `--cli`), use it as-is.
@@ -42,14 +47,26 @@ export interface PromptOptions {
42
47
  *
43
48
  * Returns the selected IntegrationType[] (always non-empty).
44
49
  */
45
- export async function resolveTargetClis(explicit?: IntegrationType[]): Promise<IntegrationType[]> {
50
+ export async function resolveTargetClis(
51
+ explicit?: IntegrationType[],
52
+ action: CliPromptAction = "install",
53
+ ): Promise<IntegrationType[]> {
46
54
  if (explicit && explicit.length > 0) return [...new Set(explicit)];
47
55
 
48
56
  const detected = detectInstalledClis();
49
57
 
50
58
  if (detected.length === 0) {
59
+ if (action === "uninstall") {
60
+ // Uninstall flow: no agent CLIs detected — nothing to remove from. Default to
61
+ // claude so removeHooks operates over Claude's scopes (no-op if no settings file).
62
+ console.log(
63
+ "\x1B[33mWarning: no agent CLI binary found in PATH (claude, codex, copilot, cursor-agent, opencode, pi, gemini). " +
64
+ "Defaulting to Claude Code; nothing will be removed if no settings file exists.\x1B[0m",
65
+ );
66
+ return ["claude"];
67
+ }
51
68
  console.log(
52
- "\x1B[33mWarning: no agent CLI binary found in PATH (claude, codex). " +
69
+ "\x1B[33mWarning: no agent CLI binary found in PATH (claude, codex, copilot, cursor-agent, opencode, pi, gemini). " +
53
70
  "Defaulting to Claude Code; hooks will activate when an agent is installed.\x1B[0m",
54
71
  );
55
72
  return ["claude"];
@@ -57,26 +74,29 @@ export async function resolveTargetClis(explicit?: IntegrationType[]): Promise<I
57
74
 
58
75
  if (detected.length === 1) {
59
76
  const integration = getIntegration(detected[0]);
60
- console.log(`Detected ${integration.displayName}; installing hooks for it.`);
77
+ const verb = action === "uninstall" ? "removing hooks from" : "installing hooks for";
78
+ console.log(`Detected ${integration.displayName}; ${verb} it.`);
61
79
  return detected;
62
80
  }
63
81
 
64
82
  // Multiple detected. Prompt or default.
65
- if (!process.stdin.isTTY) return detected; // non-interactive: install for all detected
83
+ if (!process.stdin.isTTY) return detected; // non-interactive: install/remove for all detected
66
84
 
67
- return promptCliTargetSelection(detected);
85
+ return promptCliTargetSelection(detected, action);
68
86
  }
69
87
 
70
88
  /**
71
- * Interactive arrow-key single-select for "install for which CLI?" when
89
+ * Interactive arrow-key single-select for "install/remove for which CLI?" when
72
90
  * multiple agent CLIs are detected. Visual style mirrors promptPolicySelection.
73
91
  */
74
92
  async function promptCliTargetSelection(
75
93
  detected: IntegrationType[],
94
+ action: CliPromptAction = "install",
76
95
  ): Promise<IntegrationType[]> {
77
96
  const labels = detected.map((id) => getIntegration(id).displayName).join(" + ");
97
+ const allLabel = detected.length > 2 ? "All" : "Both";
78
98
  const options: Array<{ label: string; description: string; value: IntegrationType[] }> = [
79
- { label: "Both", description: labels, value: detected },
99
+ { label: allLabel, description: labels, value: detected },
80
100
  ...detected.map((id) => ({
81
101
  label: `${getIntegration(id).displayName} only`,
82
102
  description: "",
@@ -122,14 +142,17 @@ async function promptCliTargetSelection(
122
142
  return result;
123
143
  }
124
144
 
145
+ const heading = action === "uninstall" ? "Remove Hooks" : "Install Hooks";
146
+ const verb = action === "uninstall" ? "remove from" : "install";
147
+
125
148
  function render(): void {
126
149
  const cols = process.stdout.columns || 120;
127
150
  hideCursor();
128
151
 
129
152
  const lines: string[] = [];
130
- lines.push(" Failproof AI — Install Hooks");
153
+ lines.push(` Failproof AI — ${heading}`);
131
154
  lines.push("");
132
- lines.push(` \x1B[2mDetected ${labels}. Choose where to install:\x1B[0m`);
155
+ lines.push(` \x1B[2mDetected ${labels}. Choose where to ${verb}:\x1B[0m`);
133
156
  lines.push("");
134
157
 
135
158
  for (let i = 0; i < options.length; i++) {