failproofai 0.0.9 → 0.0.10-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/.next/standalone/.cursor/hooks.json +47 -0
  2. package/.next/standalone/.gemini/settings.json +147 -0
  3. package/.next/standalone/.next/BUILD_ID +1 -1
  4. package/.next/standalone/.next/build-manifest.json +3 -3
  5. package/.next/standalone/.next/prerender-manifest.json +3 -3
  6. package/.next/standalone/.next/required-server-files.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page.js +1 -1
  9. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  10. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  11. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  12. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  13. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
  14. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  15. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
  16. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
  17. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  18. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  19. package/.next/standalone/.next/server/app/_not-found/page.js +1 -1
  20. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  21. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  22. package/.next/standalone/.next/server/app/_not-found.html +2 -2
  23. package/.next/standalone/.next/server/app/_not-found.rsc +17 -17
  24. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +17 -17
  25. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  26. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +11 -11
  27. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  28. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  29. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  30. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js +2 -1
  31. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/index.html +1 -1
  33. package/.next/standalone/.next/server/app/index.rsc +16 -16
  34. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  35. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +16 -16
  36. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
  37. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +11 -11
  38. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  39. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  40. package/.next/standalone/.next/server/app/page.js +1 -1
  41. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  43. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +8 -8
  44. package/.next/standalone/.next/server/app/policies/page.js +1 -1
  45. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  47. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  48. package/.next/standalone/.next/server/app/project/[name]/page.js +2 -2
  49. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  50. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  51. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  52. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  53. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +5 -5
  54. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  55. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  56. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  57. package/.next/standalone/.next/server/app/projects/page.js +2 -2
  58. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  59. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  60. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0.~nmr9._.js +3 -0
  61. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0yspgjy._.js → [root-of-the-server]__010i6f5._.js} +2 -2
  62. package/.next/standalone/.next/server/chunks/[root-of-the-server]__08px0ym._.js +3 -0
  63. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0b57.gk._.js +3 -0
  64. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0dtn9lr._.js +3 -0
  65. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0kjo7d_._.js +1 -1
  66. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0vlhtkc._.js +3 -0
  67. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0wu7fr7._.js +3 -0
  68. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0yfq1yr._.js +3 -0
  69. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0z4c5dj._.js +3 -0
  70. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0zso~62._.js +3 -0
  71. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_0z7w.hh._.js +1 -1
  72. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0-2wr.c._.js +4 -0
  73. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0.~m-w2._.js +4 -0
  74. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__09icjsf._.js → [root-of-the-server]__0709m8.._.js} +3 -3
  75. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0bz245.._.js +4 -0
  76. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0dl0kgt._.js +4 -0
  77. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0gmhxyo._.js +4 -0
  78. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0_b7pgn._.js → [root-of-the-server]__0lkkjl_._.js} +2 -2
  79. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__01g_w_e._.js → [root-of-the-server]__0mb9b9d._.js} +2 -2
  80. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0mup1hi._.js +3 -0
  81. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ohb3gc._.js +4 -0
  82. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qbpe_v._.js +3 -0
  83. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s~gy6y._.js +3 -0
  84. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0t5l7a5._.js +3 -0
  85. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ymlddl._.js +152 -6
  86. package/.next/standalone/.next/server/chunks/ssr/_03d7qyt._.js +3 -0
  87. package/.next/standalone/.next/server/chunks/ssr/{_07a1g.3._.js → _0zx~s__._.js} +2 -2
  88. package/.next/standalone/.next/server/chunks/ssr/_10lm7or._.js +2 -2
  89. package/.next/standalone/.next/server/chunks/ssr/app_0cdqd9w._.js +1 -1
  90. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_0xerkr6._.js +1 -1
  91. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_0q-m0y-._.js +2 -2
  92. package/.next/standalone/.next/server/chunks/ssr/lib_codex-projects_ts_0eosib~._.js +1 -1
  93. package/.next/standalone/.next/server/chunks/ssr/lib_copilot-projects_ts_0r8xkn8._.js +3 -0
  94. package/.next/standalone/.next/server/chunks/ssr/lib_cursor-projects_ts_0qt1scg._.js +3 -0
  95. package/.next/standalone/.next/server/chunks/ssr/lib_gemini-projects_ts_0sl~yqr._.js +3 -0
  96. package/.next/standalone/.next/server/chunks/ssr/lib_opencode-projects_ts_0op9gyp._.js +3 -0
  97. package/.next/standalone/.next/server/chunks/ssr/lib_pi-projects_ts_103tsh1._.js +3 -0
  98. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0ef3uwk.js +1 -1
  99. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  100. package/.next/standalone/.next/server/pages/404.html +2 -2
  101. package/.next/standalone/.next/server/pages/500.html +1 -1
  102. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  103. package/.next/standalone/.next/server/server-reference-manifest.json +9 -9
  104. package/.next/standalone/.next/static/chunks/{0n-_j_6fo6jex.js → 0-wd3kiz5wrsz.js} +2 -2
  105. package/.next/standalone/.next/static/chunks/{0756i.7omnnl6.js → 0222q~_4u7p6h.js} +1 -1
  106. package/.next/standalone/.next/static/chunks/{0t~iusm_fxoao.js → 02y~6tp1j1wkh.js} +1 -1
  107. package/.next/standalone/.next/static/chunks/{09ose_165ra4d.js → 09qdljea8j.3~.js} +1 -1
  108. package/.next/standalone/.next/static/chunks/0bi2r.m~yokoo.js +1 -0
  109. package/.next/standalone/.next/static/chunks/{11kt_9zaooda3.js → 0pt38lwlsaxvs.js} +1 -1
  110. package/.next/standalone/.next/static/chunks/0q5bmqop--9yk.js +1 -0
  111. package/.next/standalone/.next/static/chunks/{0u-ys71jc4y68.js → 0vl201wjmz17m.js} +2 -2
  112. package/.next/standalone/.next/static/chunks/{0pr7k36o_.du1.js → 0vl~p17i-4qt2.js} +1 -1
  113. package/.next/standalone/.next/static/chunks/0xkzmsj-sniqz.js +1 -0
  114. package/.next/standalone/.next/static/chunks/12po2vpc-4_c1.css +1 -0
  115. package/.next/standalone/.opencode/opencode.json +4 -0
  116. package/.next/standalone/.opencode/plugins/failproofai.mjs +131 -0
  117. package/.next/standalone/.pi/settings.json +5 -0
  118. package/.next/standalone/app/components/cli-badge.tsx +7 -11
  119. package/.next/standalone/app/components/project-list.tsx +32 -4
  120. package/.next/standalone/app/policies/hooks-client.tsx +31 -15
  121. package/.next/standalone/app/project/[name]/page.tsx +52 -16
  122. package/.next/standalone/app/project/[name]/session/[sessionId]/page.tsx +92 -15
  123. package/.next/standalone/assets/logos/copilot-dark.svg +1 -0
  124. package/.next/standalone/assets/logos/copilot-light.svg +1 -0
  125. package/.next/standalone/assets/logos/cursor-dark.svg +1 -0
  126. package/.next/standalone/assets/logos/cursor-light.svg +1 -0
  127. package/.next/standalone/assets/logos/gemini-dark.svg +13 -0
  128. package/.next/standalone/assets/logos/gemini-light.svg +13 -0
  129. package/.next/standalone/assets/logos/opencode-dark.svg +1 -0
  130. package/.next/standalone/assets/logos/opencode-light.svg +1 -0
  131. package/.next/standalone/assets/logos/pi-dark.svg +7 -0
  132. package/.next/standalone/assets/logos/pi-light.svg +7 -0
  133. package/.next/standalone/lib/cli-registry.ts +107 -0
  134. package/.next/standalone/lib/codex-projects.ts +3 -3
  135. package/.next/standalone/lib/copilot-projects.ts +224 -0
  136. package/.next/standalone/lib/copilot-sessions.ts +395 -0
  137. package/.next/standalone/lib/cursor-projects.ts +312 -0
  138. package/.next/standalone/lib/cursor-sessions.ts +467 -0
  139. package/.next/standalone/lib/gemini-projects.ts +203 -0
  140. package/.next/standalone/lib/gemini-sessions.ts +365 -0
  141. package/.next/standalone/lib/opencode-projects.ts +232 -0
  142. package/.next/standalone/lib/opencode-sessions.ts +237 -0
  143. package/.next/standalone/lib/pi-projects.ts +230 -0
  144. package/.next/standalone/lib/pi-sessions.ts +325 -0
  145. package/.next/standalone/lib/projects.ts +67 -31
  146. package/.next/standalone/next.config.ts +5 -4
  147. package/.next/standalone/package.json +2 -1
  148. package/.next/standalone/pi-extension/index.ts +373 -0
  149. package/.next/standalone/pi-extension/package.json +12 -0
  150. package/.next/standalone/server.js +1 -1
  151. package/README.md +37 -3
  152. package/bin/failproofai.mjs +61 -21
  153. package/dist/cli.mjs +2405 -253
  154. package/lib/cli-registry.ts +107 -0
  155. package/lib/codex-projects.ts +3 -3
  156. package/lib/copilot-projects.ts +224 -0
  157. package/lib/copilot-sessions.ts +395 -0
  158. package/lib/cursor-projects.ts +312 -0
  159. package/lib/cursor-sessions.ts +467 -0
  160. package/lib/gemini-projects.ts +203 -0
  161. package/lib/gemini-sessions.ts +365 -0
  162. package/lib/opencode-projects.ts +232 -0
  163. package/lib/opencode-sessions.ts +237 -0
  164. package/lib/pi-projects.ts +230 -0
  165. package/lib/pi-sessions.ts +325 -0
  166. package/lib/projects.ts +67 -31
  167. package/package.json +2 -1
  168. package/pi-extension/index.ts +373 -0
  169. package/pi-extension/package.json +12 -0
  170. package/scripts/install-diagnosis.mjs +190 -0
  171. package/scripts/launch.ts +32 -0
  172. package/scripts/postinstall.mjs +25 -0
  173. package/scripts/translate-docs/mdx-translator.ts +56 -2
  174. package/scripts/translate-docs/translator.ts +1 -1
  175. package/src/hooks/builtin-policies.ts +84 -14
  176. package/src/hooks/handler.ts +67 -5
  177. package/src/hooks/install-prompt.ts +33 -10
  178. package/src/hooks/integrations.ts +1007 -6
  179. package/src/hooks/policy-evaluator.ts +299 -3
  180. package/src/hooks/resolve-permission-mode.ts +23 -0
  181. package/src/hooks/types.ts +307 -3
  182. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0g72weg._.js +0 -3
  183. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0su~k6f._.js +0 -3
  184. package/.next/standalone/.next/server/chunks/lib_codex-projects_ts_07qqk1g._.js +0 -3
  185. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01743wx._.js +0 -3
  186. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__092s1ta._.js +0 -4
  187. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0g.lg8b._.js +0 -4
  188. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0gs6wz4._.js +0 -3
  189. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h..k-e._.js +0 -4
  190. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0it81ys._.js +0 -3
  191. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u4a9jq._.js +0 -4
  192. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11pa2ra._.js +0 -4
  193. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12.h2mg._.js +0 -3
  194. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12t-wym._.js +0 -4
  195. package/.next/standalone/.next/server/chunks/ssr/_04w00cm._.js +0 -3
  196. package/.next/standalone/.next/static/chunks/0.rk1iwdt1d7c.css +0 -1
  197. package/.next/standalone/.next/static/chunks/06x4-d1~o-opr.js +0 -1
  198. package/.next/standalone/.next/static/chunks/095l4hc7-h.~~.js +0 -1
  199. package/.next/standalone/.next/static/chunks/0n~s0gafwnp2y.js +0 -1
  200. /package/.next/standalone/.next/static/{A_Ax17P33facL0OmIwFXj → w0GG7S5UEj1-p5g9hfsh2}/_buildManifest.js +0 -0
  201. /package/.next/standalone/.next/static/{A_Ax17P33facL0OmIwFXj → w0GG7S5UEj1-p5g9hfsh2}/_clientMiddlewareManifest.js +0 -0
  202. /package/.next/standalone/.next/static/{A_Ax17P33facL0OmIwFXj → w0GG7S5UEj1-p5g9hfsh2}/_ssgManifest.js +0 -0
@@ -14,6 +14,59 @@ import type { TranslationResult, TranslationCache } from "./types";
14
14
  const __dirname = dirname(fileURLToPath(import.meta.url));
15
15
  const DOCS_DIR = join(__dirname, "..", "..", "docs");
16
16
 
17
+ /**
18
+ * Strip stray ASCII `"` that appear right after a JSX attribute's closing
19
+ * quote — e.g. `<Tab title="Tab „Richtlinien"">`. The translator sometimes
20
+ * wraps an inner phrase in language-specific typographic quotes (`„…"`,
21
+ * `「…」`, etc.) but uses an ASCII `"` for the closing instead of the
22
+ * proper U+201D, which terminates the attribute and leaves the real
23
+ * closing `"` as a stray character that breaks `mintlify validate`.
24
+ *
25
+ * Also drops unmatched typographic opening quotes inside the same attribute
26
+ * value so the rendered title doesn't end with a dangling `„` after we strip
27
+ * the extras.
28
+ */
29
+ export function sanitizeJsxAttributes(content: string): string {
30
+ // Each pair must use an OPENER that is unambiguously an opener — i.e. the
31
+ // codepoint never serves as a CLOSER of a different pair. That's why we
32
+ // skip English curly “…” (U+201C/U+201D): U+201C is also the German
33
+ // closer, so processing English curly after German would strip the very
34
+ // German closer we just preserved.
35
+ const openings: Array<[string, string]> = [
36
+ ["„", "“"], // German „ … "
37
+ ["«", "»"], // French « … »
38
+ ["‹", "›"], // French single ‹ … ›
39
+ ["「", "」"], // Japanese 「 … 」
40
+ ["『", "』"], // Japanese 『 … 』
41
+ ];
42
+ return content.replace(
43
+ /([a-zA-Z_-]+=")([^"\n]*)"+(?=\s|\/|>)/g,
44
+ (match, prefix: string, value: string) => {
45
+ // If the original had exactly one closing " (i.e. no extras),
46
+ // leave it alone — the regex's `"+` would still match a single
47
+ // quote, so we need to re-check the match length to be safe.
48
+ const expectedMinLen = `${prefix}${value}"`.length;
49
+ if (match.length === expectedMinLen) return match;
50
+ let cleaned = value;
51
+ for (const [open, close] of openings) {
52
+ const opens = cleaned.split(open).length - 1;
53
+ const closes = cleaned.split(close).length - 1;
54
+ // Drop only the surplus unmatched openers, removing from the right.
55
+ // A value like `„Foo“ und „Bar` (one matched pair plus one stray
56
+ // opener) keeps the leading `„Foo“` intact and only the dangling
57
+ // `„Bar` opener gets stripped.
58
+ let surplus = opens - closes;
59
+ while (surplus-- > 0) {
60
+ const i = cleaned.lastIndexOf(open);
61
+ if (i < 0) break;
62
+ cleaned = cleaned.slice(0, i) + cleaned.slice(i + open.length);
63
+ }
64
+ }
65
+ return `${prefix}${cleaned}"`;
66
+ },
67
+ );
68
+ }
69
+
17
70
  /**
18
71
  * Rewrite internal doc links to include the language prefix.
19
72
  * e.g. href="/built-in-policies" -> href="/es/built-in-policies"
@@ -94,8 +147,9 @@ export async function translateMdxPage(
94
147
  options.model,
95
148
  );
96
149
 
97
- // Rewrite internal links
98
- const withLinks = rewriteInternalLinks(translated, lang);
150
+ // Strip stray quote artifacts from JSX attribute values, then rewrite links
151
+ const sanitized = sanitizeJsxAttributes(translated);
152
+ const withLinks = rewriteInternalLinks(sanitized, lang);
99
153
 
100
154
  // Write output
101
155
  mkdirSync(dirname(outputPath), { recursive: true });
@@ -15,7 +15,7 @@ const SYSTEM_PROMPT = `You are a professional technical documentation translator
15
15
  ## Rules
16
16
 
17
17
  1. **Preserve all code blocks exactly as-is** — never translate content inside backtick-fenced code blocks (\`\`\`...\`\`\`) or inline code (\`...\`).
18
- 2. **Preserve MDX component syntax** — tags like <Card>, <CardGroup>, <CodeGroup>, <Steps>, <Step>, <Note>, <Tip>, <Tabs>, <Tab>, <Warning> must remain unchanged. Their attribute names (title, icon, href, cols) must remain in English. Only translate the text content of the \`title\` attribute and the text body between tags.
18
+ 2. **Preserve MDX component syntax** — tags like <Card>, <CardGroup>, <CodeGroup>, <Steps>, <Step>, <Note>, <Tip>, <Tabs>, <Tab>, <Warning> must remain unchanged. Their attribute names (title, icon, href, cols) must remain in English. Only translate the text content of the \`title\` attribute and the text body between tags. **Never put an ASCII straight \`"\` inside a \`title="…"\` (or any JSX attribute value)** — it terminates the attribute and breaks MDX parsing. If the target language would normally wrap a word in quotation marks (e.g. German „…", Japanese 「…」), drop the inner quotes inside attribute values and rely on the surrounding tag for emphasis.
19
19
  3. **Preserve YAML frontmatter keys** — only translate the string values of \`title\` and \`description\`. Keep the \`icon\` value unchanged.
20
20
  4. **Preserve all URLs and paths** — never modify href values, image paths, or links.
21
21
  5. **Preserve Markdown structure** — headers (#, ##), lists (-, *), tables (|), bold (**), italic (*), links ([text](url)) must keep their Markdown formatting.
@@ -12,27 +12,71 @@ import { hookLogWarn } from "./hook-logger";
12
12
 
13
13
  /**
14
14
  * Whether `resolved` lives under an agent CLI's home directory
15
- * (~/.claude/ or ~/.codex/). Used to whitelist agent self-reads of their own
16
- * config and transcripts.
15
+ * (~/.claude/, ~/.codex/, ~/.copilot/, ~/.cursor/, ~/.pi/, ~/.gemini/, or any
16
+ * of OpenCode's three home-side dirs). Used to whitelist agent self-reads of
17
+ * their own config and transcripts.
18
+ *
19
+ * OpenCode splits its data across three locations (verified live on
20
+ * opencode v1.14.33 via `opencode debug paths`):
21
+ * • ~/.config/opencode/ — config + plugins
22
+ * • ~/.local/share/opencode/ — sessions, snapshots, opencode.db (SQLite)
23
+ * • ~/.opencode/ — legacy fallback path
17
24
  */
18
25
  function isAgentInternalPath(resolved: string): boolean {
19
- for (const dir of [".claude", ".codex"]) {
20
- const root = join(homedir(), dir);
21
- if (resolved === root || resolved.startsWith(root + "/")) return true;
26
+ // Normalize backslashes to forward slashes so the same `startsWith` check
27
+ // works on Windows. `resolve()` returns forward slashes on POSIX but
28
+ // backslashes on Windows; `join(homedir(), ...)` follows the same OS
29
+ // convention. Comparing both sides under a single forward-slash form
30
+ // avoids per-OS branching.
31
+ const normResolved = resolved.replaceAll("\\", "/");
32
+ for (const dir of [".claude", ".codex", ".copilot", ".cursor", ".opencode", ".pi", ".gemini"]) {
33
+ const root = join(homedir(), dir).replaceAll("\\", "/");
34
+ if (normResolved === root || normResolved.startsWith(root + "/")) return true;
35
+ }
36
+ for (const sub of [join(".config", "opencode"), join(".local", "share", "opencode")]) {
37
+ const root = join(homedir(), sub).replaceAll("\\", "/");
38
+ if (normResolved === root || normResolved.startsWith(root + "/")) return true;
22
39
  }
23
40
  return false;
24
41
  }
25
42
 
26
43
  /**
27
44
  * Whether `resolved` is a settings/hooks file for an agent CLI:
28
- * • Claude Code: `.claude/settings.json`, `.claude/settings.local.json`, etc.
29
- * • Codex: `.codex/hooks.json`
45
+ * • Claude Code: `.claude/settings.json`, `.claude/settings.local.json`, etc.
46
+ * • Codex: `.codex/hooks.json`
47
+ * • Copilot CLI: `.copilot/hooks/*.json`, `.github/hooks/*.json`
48
+ * • Cursor Agent: `.cursor/hooks.json`
49
+ * • OpenCode: `.opencode/opencode.{json,jsonc}`,
50
+ * `.opencode/plugins/*.{mjs,js,ts}`,
51
+ * `~/.config/opencode/{opencode.json,opencode.jsonc,config.json}`,
52
+ * `~/.config/opencode/plugins/*.{mjs,js,ts}`
53
+ * • Pi: `.pi/settings.json` (project) and `.pi/agent/settings.json`
54
+ * (user); also the Pi-managed extension dir
55
+ * `.pi/extensions/` / `.pi/agent/extensions/`.
56
+ * • Gemini CLI: `.gemini/settings.json` (both project and user scope —
57
+ * user is `~/.gemini/settings.json`); also the Gemini-managed
58
+ * hooks scripts dir `.gemini/hooks/`.
30
59
  * These must NEVER be edited by the agent itself — that would let it disable
31
60
  * its own protections.
32
61
  */
33
62
  function isAgentSettingsFile(resolved: string): boolean {
34
63
  if (/[\\/]\.claude[\\/]settings(?:\.[^/\\]+)?\.json$/.test(resolved)) return true;
35
64
  if (/[\\/]\.codex[\\/]hooks\.json$/.test(resolved)) return true;
65
+ if (/[\\/]\.copilot[\\/]hooks[\\/][^/\\]+\.json$/.test(resolved)) return true;
66
+ if (/[\\/]\.github[\\/]hooks[\\/][^/\\]+\.json$/.test(resolved)) return true;
67
+ if (/[\\/]\.cursor[\\/]hooks\.json$/.test(resolved)) return true;
68
+ // OpenCode: project config + plugins, user config + plugins, legacy config.
69
+ if (/[\\/]\.opencode[\\/]opencode\.jsonc?$/.test(resolved)) return true;
70
+ if (/[\\/]\.opencode[\\/]plugins[\\/][^/\\]+\.(?:mjs|js|ts)$/.test(resolved)) return true;
71
+ if (/[\\/]\.config[\\/]opencode[\\/]opencode\.jsonc?$/.test(resolved)) return true;
72
+ if (/[\\/]\.config[\\/]opencode[\\/]config\.json$/.test(resolved)) return true;
73
+ if (/[\\/]\.config[\\/]opencode[\\/]plugins[\\/][^/\\]+\.(?:mjs|js|ts)$/.test(resolved)) return true;
74
+ // Pi: settings + extensions dirs (project and user-scope variants).
75
+ if (/[\\/]\.pi[\\/](?:agent[\\/])?settings\.json$/.test(resolved)) return true;
76
+ if (/[\\/]\.pi[\\/](?:agent[\\/])?extensions[\\/]/.test(resolved)) return true;
77
+ // Gemini: settings.json + hooks dir referenced by `command: $GEMINI_PROJECT_DIR/.gemini/hooks/...`.
78
+ if (/[\\/]\.gemini[\\/]settings\.json$/.test(resolved)) return true;
79
+ if (/[\\/]\.gemini[\\/]hooks[\\/]/.test(resolved)) return true;
36
80
  return false;
37
81
  }
38
82
 
@@ -735,7 +779,7 @@ function blockReadOutsideCwd(ctx: PolicyContext): PolicyResult {
735
779
  for (const p of paths) {
736
780
  const resolved = resolve(cwd, p);
737
781
  if (isClaudeSettingsFile(resolved)) {
738
- return deny(`Reading Claude settings file blocked: ${resolved}`);
782
+ return deny(`Reading agent settings file blocked: ${resolved}`);
739
783
  }
740
784
  if (isClaudeInternalPath(resolved)) continue; // Whitelist ~/.claude/
741
785
  if (resolved === "/dev/null") continue; // Harmless special file
@@ -758,7 +802,7 @@ function blockReadOutsideCwd(ctx: PolicyContext): PolicyResult {
758
802
 
759
803
  // Block settings files in any .claude directory before whitelisting
760
804
  if (isClaudeSettingsFile(resolved)) {
761
- return deny(`Reading Claude settings file blocked: ${resolved}`);
805
+ return deny(`Reading agent settings file blocked: ${resolved}`);
762
806
  }
763
807
 
764
808
  // Whitelist ~/.claude/ — Claude Code's own config, plans, memory, and settings
@@ -1365,17 +1409,44 @@ function requireCiGreenBeforeStop(ctx: PolicyContext): PolicyResult {
1365
1409
  const branch = getCurrentBranch(cwd);
1366
1410
  if (!branch || branch === "HEAD") return allow("Detached HEAD, skipping CI check.");
1367
1411
 
1368
- // 1. GitHub Actions workflow runs
1412
+ // Resolve HEAD up front — the workflow-runs filter below uses it to
1413
+ // ignore runs targeting prior commits on the same branch (otherwise a
1414
+ // stale failure on commit X is still reported after the fix on Y lands).
1415
+ // Third-party checks and commit statuses (queried by SHA below) already
1416
+ // scope to HEAD via getThirdPartyCheckRuns / getCommitStatuses.
1417
+ const sha = getHeadSha(cwd);
1418
+
1419
+ // 1. GitHub Actions workflow runs (filtered to current HEAD, deduped by name)
1369
1420
  let workflowRuns: CiCheck[] = [];
1370
1421
  try {
1422
+ // --limit 20 (was 5): a busy branch can push the latest run for some
1423
+ // workflow out of the top-5 window after the SHA filter. 20 covers
1424
+ // ~4 commits worth of runs for a 5-workflow repo without being slow.
1371
1425
  const runsJson = execFileSync(
1372
1426
  "gh",
1373
- ["run", "list", "--branch", branch, "--limit", "5", "--json", "status,conclusion,name"],
1427
+ ["run", "list", "--branch", branch, "--limit", "20", "--json", "status,conclusion,name,headSha"],
1374
1428
  { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 15000 },
1375
1429
  ).trim();
1376
1430
 
1377
1431
  if (runsJson && runsJson !== "[]") {
1378
- workflowRuns = JSON.parse(runsJson) as CiCheck[];
1432
+ const allWorkflowRuns = JSON.parse(runsJson) as Array<CiCheck & { headSha?: string }>;
1433
+ // Filter to runs targeting the current HEAD commit only — not
1434
+ // historical runs for prior commits on the same branch. When `sha`
1435
+ // is unavailable (e.g. brand-new repo with no commits) fall back
1436
+ // to the unfiltered list so the policy still has something to act on.
1437
+ const headRuns = sha
1438
+ ? allWorkflowRuns.filter((r) => r.headSha === sha)
1439
+ : allWorkflowRuns;
1440
+ // Dedupe by workflow name, keeping the first occurrence (gh run list
1441
+ // returns newest-first). This handles GitHub's "Re-run all jobs" which
1442
+ // creates a fresh run record with the same name + headSha — without
1443
+ // dedupe the older failed record would still trip the deny.
1444
+ const seen = new Set<string>();
1445
+ workflowRuns = headRuns.filter((r) => {
1446
+ if (seen.has(r.name)) return false;
1447
+ seen.add(r.name);
1448
+ return true;
1449
+ });
1379
1450
  }
1380
1451
  } catch {
1381
1452
  // fail-open for workflow runs; continue to check third-party checks
@@ -1384,7 +1455,6 @@ function requireCiGreenBeforeStop(ctx: PolicyContext): PolicyResult {
1384
1455
  // 2. Third-party check runs (CodeRabbit, SonarCloud, Codecov, etc.)
1385
1456
  let thirdPartyChecks: CiCheck[] = [];
1386
1457
  let commitStatuses: CiCheck[] = [];
1387
- const sha = getHeadSha(cwd);
1388
1458
  if (sha) {
1389
1459
  thirdPartyChecks = getThirdPartyCheckRuns(cwd, sha);
1390
1460
  commitStatuses = getCommitStatuses(cwd, sha);
@@ -1879,7 +1949,7 @@ export const BUILTIN_POLICIES: BuiltinPolicyDefinition[] = [
1879
1949
  },
1880
1950
  {
1881
1951
  name: "require-ci-green-before-stop",
1882
- description: "Require CI checks to pass on the current branch before Claude stops",
1952
+ description: "Require CI checks to pass on the current HEAD commit before Claude stops (ignores stale runs on prior commits)",
1883
1953
  fn: requireCiGreenBeforeStop,
1884
1954
  match: { events: ["Stop"] },
1885
1955
  defaultEnabled: false,
@@ -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++) {