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
package/dist/cli.mjs CHANGED
@@ -16,10 +16,10 @@ var __export = (target, all) => {
16
16
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
17
 
18
18
  // src/hooks/types.ts
19
- var HOOK_SCOPES, INTEGRATION_TYPES, CODEX_HOOK_SCOPES, CODEX_HOOK_EVENT_TYPES, CODEX_EVENT_MAP, HOOK_EVENT_TYPES, FAILPROOFAI_HOOK_MARKER = "__failproofai_hook__";
19
+ var HOOK_SCOPES, INTEGRATION_TYPES, CODEX_HOOK_SCOPES, CODEX_HOOK_EVENT_TYPES, CODEX_EVENT_MAP, COPILOT_HOOK_SCOPES, COPILOT_HOOK_EVENT_TYPES, CURSOR_HOOK_SCOPES, CURSOR_HOOK_EVENT_TYPES, CURSOR_EVENT_MAP, OPENCODE_HOOK_SCOPES, OPENCODE_HOOK_EVENT_TYPES, PI_HOOK_SCOPES, PI_HOOK_EVENT_TYPES, PI_EVENT_MAP, GEMINI_HOOK_SCOPES, GEMINI_HOOK_EVENT_TYPES, GEMINI_EVENT_MAP, GEMINI_TOOL_MAP, HOOK_EVENT_TYPES, FAILPROOFAI_HOOK_MARKER = "__failproofai_hook__";
20
20
  var init_types = __esm(() => {
21
21
  HOOK_SCOPES = ["user", "project", "local"];
22
- INTEGRATION_TYPES = ["claude", "codex"];
22
+ INTEGRATION_TYPES = ["claude", "codex", "copilot", "cursor", "opencode", "pi", "gemini"];
23
23
  CODEX_HOOK_SCOPES = ["user", "project"];
24
24
  CODEX_HOOK_EVENT_TYPES = [
25
25
  "session_start",
@@ -37,6 +37,103 @@ var init_types = __esm(() => {
37
37
  user_prompt_submit: "UserPromptSubmit",
38
38
  stop: "Stop"
39
39
  };
40
+ COPILOT_HOOK_SCOPES = ["user", "project"];
41
+ COPILOT_HOOK_EVENT_TYPES = [
42
+ "SessionStart",
43
+ "SessionEnd",
44
+ "UserPromptSubmit",
45
+ "PreToolUse",
46
+ "PostToolUse",
47
+ "Stop"
48
+ ];
49
+ CURSOR_HOOK_SCOPES = ["user", "project"];
50
+ CURSOR_HOOK_EVENT_TYPES = [
51
+ "sessionStart",
52
+ "sessionEnd",
53
+ "beforeSubmitPrompt",
54
+ "preToolUse",
55
+ "postToolUse",
56
+ "stop"
57
+ ];
58
+ CURSOR_EVENT_MAP = {
59
+ sessionStart: "SessionStart",
60
+ sessionEnd: "SessionEnd",
61
+ beforeSubmitPrompt: "UserPromptSubmit",
62
+ preToolUse: "PreToolUse",
63
+ postToolUse: "PostToolUse",
64
+ stop: "Stop"
65
+ };
66
+ OPENCODE_HOOK_SCOPES = ["user", "project"];
67
+ OPENCODE_HOOK_EVENT_TYPES = [
68
+ "tool.execute.before",
69
+ "tool.execute.after",
70
+ "session.created",
71
+ "session.deleted",
72
+ "session.idle",
73
+ "message.updated",
74
+ "permission.ask"
75
+ ];
76
+ PI_HOOK_SCOPES = ["user", "project"];
77
+ PI_HOOK_EVENT_TYPES = [
78
+ "session_start",
79
+ "session_shutdown",
80
+ "input",
81
+ "tool_call",
82
+ "user_bash",
83
+ "tool_result",
84
+ "agent_end"
85
+ ];
86
+ PI_EVENT_MAP = {
87
+ session_start: "SessionStart",
88
+ session_shutdown: "SessionEnd",
89
+ input: "UserPromptSubmit",
90
+ tool_call: "PreToolUse",
91
+ user_bash: "PreToolUse",
92
+ tool_result: "PostToolUse",
93
+ agent_end: "Stop"
94
+ };
95
+ GEMINI_HOOK_SCOPES = ["user", "project"];
96
+ GEMINI_HOOK_EVENT_TYPES = [
97
+ "SessionStart",
98
+ "SessionEnd",
99
+ "BeforeAgent",
100
+ "AfterAgent",
101
+ "BeforeModel",
102
+ "AfterModel",
103
+ "BeforeToolSelection",
104
+ "BeforeTool",
105
+ "AfterTool",
106
+ "PreCompress",
107
+ "Notification"
108
+ ];
109
+ GEMINI_EVENT_MAP = {
110
+ SessionStart: "SessionStart",
111
+ SessionEnd: "SessionEnd",
112
+ BeforeAgent: "UserPromptSubmit",
113
+ AfterAgent: "Stop",
114
+ BeforeTool: "PreToolUse",
115
+ AfterTool: "PostToolUse",
116
+ PreCompress: "PreCompact",
117
+ Notification: "Notification",
118
+ BeforeModel: "BeforeModel",
119
+ AfterModel: "AfterModel",
120
+ BeforeToolSelection: "BeforeToolSelection"
121
+ };
122
+ GEMINI_TOOL_MAP = {
123
+ run_shell_command: "Bash",
124
+ read_file: "Read",
125
+ read_many_files: "Read",
126
+ write_file: "Write",
127
+ replace: "Edit",
128
+ glob: "Glob",
129
+ grep_search: "Grep",
130
+ list_directory: "LS",
131
+ web_fetch: "WebFetch",
132
+ google_web_search: "WebSearch",
133
+ write_todos: "TodoWrite",
134
+ save_memory: "Memory",
135
+ ask_user: "AskUser"
136
+ };
40
137
  HOOK_EVENT_TYPES = [
41
138
  "SessionStart",
42
139
  "SessionEnd",
@@ -332,9 +429,15 @@ import { readFile, writeFile } from "node:fs/promises";
332
429
  import { execSync, execFileSync } from "node:child_process";
333
430
  import { homedir as homedir3 } from "node:os";
334
431
  function isAgentInternalPath(resolved2) {
335
- for (const dir of [".claude", ".codex"]) {
336
- const root = join2(homedir3(), dir);
337
- if (resolved2 === root || resolved2.startsWith(root + "/"))
432
+ const normResolved = resolved2.replaceAll("\\", "/");
433
+ for (const dir of [".claude", ".codex", ".copilot", ".cursor", ".opencode", ".pi", ".gemini"]) {
434
+ const root = join2(homedir3(), dir).replaceAll("\\", "/");
435
+ if (normResolved === root || normResolved.startsWith(root + "/"))
436
+ return true;
437
+ }
438
+ for (const sub of [join2(".config", "opencode"), join2(".local", "share", "opencode")]) {
439
+ const root = join2(homedir3(), sub).replaceAll("\\", "/");
440
+ if (normResolved === root || normResolved.startsWith(root + "/"))
338
441
  return true;
339
442
  }
340
443
  return false;
@@ -344,6 +447,30 @@ function isAgentSettingsFile(resolved2) {
344
447
  return true;
345
448
  if (/[\\/]\.codex[\\/]hooks\.json$/.test(resolved2))
346
449
  return true;
450
+ if (/[\\/]\.copilot[\\/]hooks[\\/][^/\\]+\.json$/.test(resolved2))
451
+ return true;
452
+ if (/[\\/]\.github[\\/]hooks[\\/][^/\\]+\.json$/.test(resolved2))
453
+ return true;
454
+ if (/[\\/]\.cursor[\\/]hooks\.json$/.test(resolved2))
455
+ return true;
456
+ if (/[\\/]\.opencode[\\/]opencode\.jsonc?$/.test(resolved2))
457
+ return true;
458
+ if (/[\\/]\.opencode[\\/]plugins[\\/][^/\\]+\.(?:mjs|js|ts)$/.test(resolved2))
459
+ return true;
460
+ if (/[\\/]\.config[\\/]opencode[\\/]opencode\.jsonc?$/.test(resolved2))
461
+ return true;
462
+ if (/[\\/]\.config[\\/]opencode[\\/]config\.json$/.test(resolved2))
463
+ return true;
464
+ if (/[\\/]\.config[\\/]opencode[\\/]plugins[\\/][^/\\]+\.(?:mjs|js|ts)$/.test(resolved2))
465
+ return true;
466
+ if (/[\\/]\.pi[\\/](?:agent[\\/])?settings\.json$/.test(resolved2))
467
+ return true;
468
+ if (/[\\/]\.pi[\\/](?:agent[\\/])?extensions[\\/]/.test(resolved2))
469
+ return true;
470
+ if (/[\\/]\.gemini[\\/]settings\.json$/.test(resolved2))
471
+ return true;
472
+ if (/[\\/]\.gemini[\\/]hooks[\\/]/.test(resolved2))
473
+ return true;
347
474
  return false;
348
475
  }
349
476
  function getCommand(ctx) {
@@ -780,7 +907,7 @@ function blockReadOutsideCwd(ctx) {
780
907
  for (const p of paths) {
781
908
  const resolved3 = resolve2(cwd, p);
782
909
  if (isClaudeSettingsFile(resolved3)) {
783
- return deny(`Reading Claude settings file blocked: ${resolved3}`);
910
+ return deny(`Reading agent settings file blocked: ${resolved3}`);
784
911
  }
785
912
  if (isClaudeInternalPath(resolved3))
786
913
  continue;
@@ -801,7 +928,7 @@ function blockReadOutsideCwd(ctx) {
801
928
  return allow();
802
929
  const resolved2 = resolve2(cwd, target);
803
930
  if (isClaudeSettingsFile(resolved2)) {
804
- return deny(`Reading Claude settings file blocked: ${resolved2}`);
931
+ return deny(`Reading agent settings file blocked: ${resolved2}`);
805
932
  }
806
933
  if (isClaudeInternalPath(resolved2))
807
934
  return allow();
@@ -1228,16 +1355,24 @@ function requireCiGreenBeforeStop(ctx) {
1228
1355
  const branch = getCurrentBranch(cwd);
1229
1356
  if (!branch || branch === "HEAD")
1230
1357
  return allow("Detached HEAD, skipping CI check.");
1358
+ const sha = getHeadSha(cwd);
1231
1359
  let workflowRuns = [];
1232
1360
  try {
1233
- const runsJson = execFileSync("gh", ["run", "list", "--branch", branch, "--limit", "5", "--json", "status,conclusion,name"], { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 15000 }).trim();
1361
+ const runsJson = execFileSync("gh", ["run", "list", "--branch", branch, "--limit", "20", "--json", "status,conclusion,name,headSha"], { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 15000 }).trim();
1234
1362
  if (runsJson && runsJson !== "[]") {
1235
- workflowRuns = JSON.parse(runsJson);
1363
+ const allWorkflowRuns = JSON.parse(runsJson);
1364
+ const headRuns = sha ? allWorkflowRuns.filter((r) => r.headSha === sha) : allWorkflowRuns;
1365
+ const seen = new Set;
1366
+ workflowRuns = headRuns.filter((r) => {
1367
+ if (seen.has(r.name))
1368
+ return false;
1369
+ seen.add(r.name);
1370
+ return true;
1371
+ });
1236
1372
  }
1237
1373
  } catch {}
1238
1374
  let thirdPartyChecks = [];
1239
1375
  let commitStatuses = [];
1240
- const sha = getHeadSha(cwd);
1241
1376
  if (sha) {
1242
1377
  thirdPartyChecks = getThirdPartyCheckRuns(cwd, sha);
1243
1378
  commitStatuses = getCommitStatuses(cwd, sha);
@@ -1803,7 +1938,7 @@ var init_builtin_policies = __esm(() => {
1803
1938
  },
1804
1939
  {
1805
1940
  name: "require-ci-green-before-stop",
1806
- description: "Require CI checks to pass on the current branch before Claude stops",
1941
+ description: "Require CI checks to pass on the current HEAD commit before Claude stops (ignores stale runs on prior commits)",
1807
1942
  fn: requireCiGreenBeforeStop,
1808
1943
  match: { events: ["Stop"] },
1809
1944
  defaultEnabled: false,
@@ -1874,13 +2009,79 @@ async function evaluatePolicies(eventType, payload, session, config) {
1874
2009
  if (result.decision === "deny") {
1875
2010
  const reason = appendHint(result.reason ?? `Blocked by policy: ${policy.name}`, getConfigParamsFor(config, policy.name)?.hint);
1876
2011
  hookLogInfo(`deny by "${policy.name}": ${reason}`);
1877
- const displayTool = ctx.toolName ?? "unknown tool";
2012
+ let displayTool;
2013
+ if (ctx.toolName) {
2014
+ displayTool = ctx.toolName;
2015
+ } else if (eventType === "UserPromptSubmit") {
2016
+ displayTool = "prompt";
2017
+ } else if (eventType === "SessionStart") {
2018
+ displayTool = "session start";
2019
+ } else if (eventType === "SessionEnd") {
2020
+ displayTool = "session end";
2021
+ } else if (eventType === "Stop") {
2022
+ displayTool = "stop";
2023
+ } else {
2024
+ displayTool = "operation";
2025
+ }
2026
+ const blockedMessage = `Blocked ${displayTool} by failproofai because: ${reason}, as per the policy configured by the user`;
2027
+ if (session?.cli === "cursor") {
2028
+ const response = {
2029
+ permission: "deny",
2030
+ user_message: blockedMessage,
2031
+ agent_message: blockedMessage
2032
+ };
2033
+ return {
2034
+ exitCode: 0,
2035
+ stdout: JSON.stringify(response),
2036
+ stderr: "",
2037
+ policyName: policy.name,
2038
+ reason,
2039
+ decision: "deny"
2040
+ };
2041
+ }
2042
+ if (session?.cli === "pi") {
2043
+ const response = {
2044
+ permission: "deny",
2045
+ reason: blockedMessage
2046
+ };
2047
+ return {
2048
+ exitCode: 0,
2049
+ stdout: JSON.stringify(response),
2050
+ stderr: "",
2051
+ policyName: policy.name,
2052
+ reason,
2053
+ decision: "deny"
2054
+ };
2055
+ }
2056
+ if (session?.cli === "gemini") {
2057
+ if (eventType === "Stop") {
2058
+ const reasonText = `MANDATORY ACTION REQUIRED from failproofai (policy: ${policy.name}): ${reason}
2059
+
2060
+ You MUST complete the above action NOW. Do NOT ask the user for confirmation — execute the required action, then attempt to finish your task again.`;
2061
+ return {
2062
+ exitCode: 0,
2063
+ stdout: JSON.stringify({ decision: "block", reason: reasonText }),
2064
+ stderr: "",
2065
+ policyName: policy.name,
2066
+ reason,
2067
+ decision: "deny"
2068
+ };
2069
+ }
2070
+ return {
2071
+ exitCode: 0,
2072
+ stdout: JSON.stringify({ decision: "deny", reason: blockedMessage }),
2073
+ stderr: "",
2074
+ policyName: policy.name,
2075
+ reason,
2076
+ decision: "deny"
2077
+ };
2078
+ }
1878
2079
  if (eventType === "PreToolUse") {
1879
2080
  const response = {
1880
2081
  hookSpecificOutput: {
1881
2082
  hookEventName: eventType,
1882
2083
  permissionDecision: "deny",
1883
- permissionDecisionReason: `Blocked ${displayTool} by failproofai because: ${reason}, as per the policy configured by the user`
2084
+ permissionDecisionReason: blockedMessage
1884
2085
  }
1885
2086
  };
1886
2087
  return {
@@ -1961,6 +2162,98 @@ You MUST complete the above action NOW. Do NOT ask the user for confirmation —
1961
2162
  const combined = instructEntries.map((e) => e.reason).join(`
1962
2163
  `);
1963
2164
  const policyNames = instructEntries.map((e) => e.policyName);
2165
+ if (session?.cli === "cursor") {
2166
+ if (eventType === "Stop") {
2167
+ const response3 = {
2168
+ followup_message: `Instruction from failproofai: ${combined}`
2169
+ };
2170
+ return {
2171
+ exitCode: 0,
2172
+ stdout: JSON.stringify(response3),
2173
+ stderr: "",
2174
+ policyName: policyNames[0],
2175
+ policyNames,
2176
+ reason: combined,
2177
+ decision: "instruct"
2178
+ };
2179
+ }
2180
+ const response2 = {
2181
+ permission: "allow",
2182
+ additional_context: `Instruction from failproofai: ${combined}`
2183
+ };
2184
+ return {
2185
+ exitCode: 0,
2186
+ stdout: JSON.stringify(response2),
2187
+ stderr: "",
2188
+ policyName: policyNames[0],
2189
+ policyNames,
2190
+ reason: combined,
2191
+ decision: "instruct"
2192
+ };
2193
+ }
2194
+ if (session?.cli === "pi") {
2195
+ const response2 = {
2196
+ permission: "allow",
2197
+ reason: `Instruction from failproofai: ${combined}`
2198
+ };
2199
+ return {
2200
+ exitCode: 0,
2201
+ stdout: JSON.stringify(response2),
2202
+ stderr: "",
2203
+ policyName: policyNames[0],
2204
+ policyNames,
2205
+ reason: combined,
2206
+ decision: "instruct"
2207
+ };
2208
+ }
2209
+ if (session?.cli === "gemini") {
2210
+ if (eventType === "Stop") {
2211
+ const policyAttribution = policyNames.length === 1 ? `policy: ${policyNames[0]}` : `policies: ${policyNames.join(", ")}`;
2212
+ const reasonText = `MANDATORY ACTION REQUIRED from failproofai (${policyAttribution}): ${combined}
2213
+
2214
+ You MUST complete the above action(s) NOW. Do NOT ask the user for confirmation — execute the required action(s), then attempt to finish your task again.`;
2215
+ return {
2216
+ exitCode: 0,
2217
+ stdout: JSON.stringify({ decision: "block", reason: reasonText }),
2218
+ stderr: "",
2219
+ policyName: policyNames[0],
2220
+ policyNames,
2221
+ reason: combined,
2222
+ decision: "instruct"
2223
+ };
2224
+ }
2225
+ const supportsContext = eventType === "UserPromptSubmit" || eventType === "PreToolUse" || eventType === "PostToolUse" || eventType === "SessionStart";
2226
+ if (supportsContext) {
2227
+ const hookEventName = session?.hookEventName ?? session?.rawHookEventName ?? eventType;
2228
+ const response2 = {
2229
+ hookSpecificOutput: {
2230
+ hookEventName,
2231
+ additionalContext: `Instruction from failproofai: ${combined}`
2232
+ }
2233
+ };
2234
+ return {
2235
+ exitCode: 0,
2236
+ stdout: JSON.stringify(response2),
2237
+ stderr: "",
2238
+ policyName: policyNames[0],
2239
+ policyNames,
2240
+ reason: combined,
2241
+ decision: "instruct"
2242
+ };
2243
+ }
2244
+ const stderrMsg = instructEntries.map((e) => `[failproofai] ${e.policyName}: ${e.reason}`).join(`
2245
+ `);
2246
+ return {
2247
+ exitCode: 0,
2248
+ stdout: "",
2249
+ stderr: stderrMsg + `
2250
+ `,
2251
+ policyName: policyNames[0],
2252
+ policyNames,
2253
+ reason: combined,
2254
+ decision: "instruct"
2255
+ };
2256
+ }
1964
2257
  if (eventType === "Stop") {
1965
2258
  const policyAttribution = policyNames.length === 1 ? `policy: ${policyNames[0]}` : `policies: ${policyNames.join(", ")}`;
1966
2259
  return {
@@ -1995,6 +2288,76 @@ You MUST complete the above action(s) NOW. Do NOT ask the user for confirmation
1995
2288
  const combined = allowEntries.map((e) => e.reason).join(`
1996
2289
  `);
1997
2290
  const policyNames = allowEntries.map((e) => e.policyName);
2291
+ if (session?.cli === "cursor") {
2292
+ const response2 = {
2293
+ permission: "allow",
2294
+ additional_context: `Note from failproofai: ${combined}`
2295
+ };
2296
+ const stderrMsg2 = allowEntries.map((e) => `[failproofai] ${e.policyName}: ${e.reason}`).join(`
2297
+ `);
2298
+ return {
2299
+ exitCode: 0,
2300
+ stdout: JSON.stringify(response2),
2301
+ stderr: stderrMsg2 + `
2302
+ `,
2303
+ policyName: policyNames[0],
2304
+ policyNames,
2305
+ reason: combined,
2306
+ decision: "allow"
2307
+ };
2308
+ }
2309
+ if (session?.cli === "pi") {
2310
+ const response2 = {
2311
+ permission: "allow",
2312
+ reason: `Note from failproofai: ${combined}`
2313
+ };
2314
+ const stderrMsg2 = allowEntries.map((e) => `[failproofai] ${e.policyName}: ${e.reason}`).join(`
2315
+ `);
2316
+ return {
2317
+ exitCode: 0,
2318
+ stdout: JSON.stringify(response2),
2319
+ stderr: stderrMsg2 + `
2320
+ `,
2321
+ policyName: policyNames[0],
2322
+ policyNames,
2323
+ reason: combined,
2324
+ decision: "allow"
2325
+ };
2326
+ }
2327
+ if (session?.cli === "gemini") {
2328
+ const supportsContext = eventType === "UserPromptSubmit" || eventType === "PreToolUse" || eventType === "PostToolUse" || eventType === "SessionStart";
2329
+ const stderrMsg2 = allowEntries.map((e) => `[failproofai] ${e.policyName}: ${e.reason}`).join(`
2330
+ `);
2331
+ if (supportsContext) {
2332
+ const hookEventName = session?.hookEventName ?? session?.rawHookEventName ?? eventType;
2333
+ const response2 = {
2334
+ hookSpecificOutput: {
2335
+ hookEventName,
2336
+ additionalContext: `Note from failproofai: ${combined}`
2337
+ }
2338
+ };
2339
+ return {
2340
+ exitCode: 0,
2341
+ stdout: JSON.stringify(response2),
2342
+ stderr: stderrMsg2 + `
2343
+ `,
2344
+ policyName: policyNames[0],
2345
+ policyNames,
2346
+ reason: combined,
2347
+ decision: "allow"
2348
+ };
2349
+ }
2350
+ return {
2351
+ exitCode: 0,
2352
+ stdout: "",
2353
+ stderr: stderrMsg2 + `
2354
+ `,
2355
+ policyName: policyNames[0],
2356
+ policyNames,
2357
+ reason: combined,
2358
+ decision: "allow"
2359
+ };
2360
+ }
1998
2361
  const supportsHookSpecificOutput = eventType === "PreToolUse" || eventType === "PostToolUse" || eventType === "UserPromptSubmit" || eventType === "PermissionRequest";
1999
2362
  const response = supportsHookSpecificOutput ? { hookSpecificOutput: { hookEventName: eventType, additionalContext: `Note from failproofai: ${combined}` } } : { reason: combined };
2000
2363
  const stderrMsg = allowEntries.map((e) => `[failproofai] ${e.policyName}: ${e.reason}`).join(`
@@ -2397,7 +2760,7 @@ var init_hook_activity_store = __esm(() => {
2397
2760
  });
2398
2761
 
2399
2762
  // package.json
2400
- var version2 = "0.0.9";
2763
+ var version2 = "0.0.10-beta.0";
2401
2764
  var init_package = () => {};
2402
2765
 
2403
2766
  // src/posthog-key.ts
@@ -2465,6 +2828,12 @@ import { join as join4 } from "path";
2465
2828
  function getDefaultClaudeProjectsPath() {
2466
2829
  return join4(homedir6(), ".claude", "projects");
2467
2830
  }
2831
+ function decodeFolderName(name) {
2832
+ if (/^[A-Za-z]--/.test(name)) {
2833
+ return name[0] + ":/" + name.slice(3).replace(/-/g, "/");
2834
+ }
2835
+ return name.replace(/-/g, "/");
2836
+ }
2468
2837
  function encodeFolderName(path) {
2469
2838
  const driveMatch = /^([A-Za-z]):[\\/](.*)$/.exec(path);
2470
2839
  if (driveMatch) {
@@ -2550,9 +2919,9 @@ __export(exports_codex_projects, {
2550
2919
  getCachedCodexSessionsByEncodedName: () => getCachedCodexSessionsByEncodedName,
2551
2920
  getCachedCodexProjects: () => getCachedCodexProjects
2552
2921
  });
2553
- import { open as open2, readdir } from "fs/promises";
2554
- import { homedir as homedir7 } from "os";
2555
- import { join as join5 } from "path";
2922
+ import { open as open2, readdir } from "node:fs/promises";
2923
+ import { homedir as homedir7 } from "node:os";
2924
+ import { join as join5 } from "node:path";
2556
2925
  async function safeReaddir(dir) {
2557
2926
  try {
2558
2927
  return await readdir(dir, { withFileTypes: true });
@@ -2739,115 +3108,984 @@ var init_codex_projects = __esm(() => {
2739
3108
  getCachedCodexSessionsByEncodedName = runtimeCache((name) => getCodexSessionsByEncodedName(name), 30, { maxSize: 50 });
2740
3109
  });
2741
3110
 
2742
- // lib/projects.ts
2743
- import { readdir as readdir2, stat as stat2 } from "fs/promises";
2744
- import { join as join6, resolve as resolve5, sep } from "path";
2745
- async function getMtime(path, label) {
3111
+ // lib/copilot-projects.ts
3112
+ var exports_copilot_projects = {};
3113
+ __export(exports_copilot_projects, {
3114
+ getCopilotSessionsForCwd: () => getCopilotSessionsForCwd,
3115
+ getCopilotSessionsByEncodedName: () => getCopilotSessionsByEncodedName,
3116
+ getCopilotProjects: () => getCopilotProjects,
3117
+ getCachedCopilotSessionsForCwd: () => getCachedCopilotSessionsForCwd,
3118
+ getCachedCopilotSessionsByEncodedName: () => getCachedCopilotSessionsByEncodedName,
3119
+ getCachedCopilotProjects: () => getCachedCopilotProjects
3120
+ });
3121
+ import { readdir as readdir2, readFile as readFile3, stat as stat2 } from "node:fs/promises";
3122
+ import { homedir as homedir8 } from "node:os";
3123
+ import { join as join6 } from "node:path";
3124
+ function getCopilotSessionStateRoot() {
3125
+ return join6(process.env.COPILOT_HOME || join6(homedir8(), ".copilot"), "session-state");
3126
+ }
3127
+ async function safeReaddir2(dir) {
2746
3128
  try {
2747
- return (await stat2(path)).mtime;
2748
- } catch (error) {
2749
- logWarn(`Failed to stat ${label}:`, error);
2750
- return new Date(0);
3129
+ return await readdir2(dir, { withFileTypes: true });
3130
+ } catch {
3131
+ return null;
2751
3132
  }
2752
3133
  }
2753
- async function safeReaddir2(dirPath) {
3134
+ function parseCwdFromWorkspace(text) {
3135
+ const m = text.match(/^cwd\s*:\s*(.+?)\s*$/m);
3136
+ if (!m)
3137
+ return;
3138
+ return m[1].replace(/^['"]|['"]$/g, "");
3139
+ }
3140
+ async function statMtime(path) {
2754
3141
  try {
2755
- const s = await stat2(dirPath);
2756
- if (!s.isDirectory())
2757
- return null;
2758
- return await readdir2(dirPath, { withFileTypes: true });
3142
+ return (await stat2(path)).mtime;
2759
3143
  } catch {
2760
3144
  return null;
2761
3145
  }
2762
3146
  }
2763
- async function getClaudeProjectFolders() {
3147
+ async function scanCopilotSessions() {
3148
+ const root = getCopilotSessionStateRoot();
3149
+ const entries = await safeReaddir2(root);
3150
+ if (!entries)
3151
+ return [];
3152
+ const candidates = entries.filter((e) => e.isDirectory()).map((e) => ({
3153
+ sessionId: e.name,
3154
+ workspacePath: join6(root, e.name, "workspace.yaml"),
3155
+ eventsPath: join6(root, e.name, "events.jsonl")
3156
+ }));
3157
+ const settled = await batchAll(candidates.map((c) => async () => {
3158
+ let workspaceText;
3159
+ try {
3160
+ workspaceText = await readFile3(c.workspacePath, "utf-8");
3161
+ } catch {
3162
+ return null;
3163
+ }
3164
+ const cwd = parseCwdFromWorkspace(workspaceText);
3165
+ if (!cwd)
3166
+ return null;
3167
+ const eventsMtime = await statMtime(c.eventsPath);
3168
+ const wsMtime = await statMtime(c.workspacePath);
3169
+ const hasTranscript = eventsMtime !== null;
3170
+ const fileMtime = eventsMtime && wsMtime ? new Date(Math.max(eventsMtime.getTime(), wsMtime.getTime())) : eventsMtime ?? wsMtime ?? new Date(0);
3171
+ return {
3172
+ workspacePath: c.workspacePath,
3173
+ eventsPath: c.eventsPath,
3174
+ sessionId: c.sessionId,
3175
+ cwd,
3176
+ fileMtime,
3177
+ hasTranscript
3178
+ };
3179
+ }), 16);
3180
+ return settled.filter((r) => r.status === "fulfilled").map((r) => r.value).filter((v) => v !== null);
3181
+ }
3182
+ async function getCopilotProjects() {
3183
+ let metas;
2764
3184
  try {
2765
- const projectsPath = getClaudeProjectsPath();
2766
- const entries = await safeReaddir2(projectsPath);
2767
- if (!entries)
2768
- return [];
2769
- const settled = await batchAll(entries.filter((entry) => entry.isDirectory()).map((entry) => async () => {
2770
- const folderPath = join6(projectsPath, entry.name);
2771
- const mtime = await getMtime(folderPath, entry.name);
2772
- return {
2773
- name: entry.name,
2774
- path: folderPath,
2775
- isDirectory: true,
2776
- lastModified: mtime,
2777
- lastModifiedFormatted: formatDate(mtime),
2778
- cli: ["claude"]
2779
- };
2780
- }), 16);
2781
- const folders = settled.filter((r) => r.status === "fulfilled").map((r) => r.value);
2782
- folders.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
2783
- return folders;
3185
+ metas = await cachedScan2();
2784
3186
  } catch (error) {
2785
- logError("Error reading Claude project folders:", error);
3187
+ logWarn("Failed to scan Copilot sessions:", error);
2786
3188
  return [];
2787
3189
  }
2788
- }
2789
- function mergeProjectFolders(claude, codex) {
2790
- const byName = new Map;
2791
- for (const f of claude)
2792
- byName.set(f.name, { ...f, cli: [...f.cli] });
2793
- for (const f of codex) {
2794
- const existing = byName.get(f.name);
2795
- if (!existing) {
2796
- byName.set(f.name, { ...f, cli: [...f.cli] });
3190
+ const byCwd = new Map;
3191
+ for (const m of metas) {
3192
+ if (!m.hasTranscript)
2797
3193
  continue;
3194
+ const existing = byCwd.get(m.cwd);
3195
+ if (!existing || m.fileMtime.getTime() > existing.latest.getTime()) {
3196
+ byCwd.set(m.cwd, { latest: m.fileMtime, cwd: m.cwd });
2798
3197
  }
2799
- const mergedCli = [...existing.cli];
2800
- for (const c of f.cli)
2801
- if (!mergedCli.includes(c))
2802
- mergedCli.push(c);
2803
- const newer = f.lastModified.getTime() > existing.lastModified.getTime() ? f : existing;
2804
- byName.set(f.name, {
2805
- ...existing,
2806
- cli: mergedCli,
2807
- lastModified: newer.lastModified,
2808
- lastModifiedFormatted: newer.lastModifiedFormatted
3198
+ }
3199
+ const folders = [];
3200
+ for (const { cwd, latest } of byCwd.values()) {
3201
+ folders.push({
3202
+ name: encodeFolderName(cwd),
3203
+ path: cwd,
3204
+ isDirectory: true,
3205
+ lastModified: latest,
3206
+ lastModifiedFormatted: formatDate(latest),
3207
+ cli: ["copilot"]
2809
3208
  });
2810
3209
  }
2811
- const merged = Array.from(byName.values());
2812
- merged.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
2813
- return merged;
3210
+ folders.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
3211
+ return folders;
2814
3212
  }
2815
- async function getProjectFolders() {
2816
- const { getCodexProjects: getCodexProjects2 } = await Promise.resolve().then(() => (init_codex_projects(), exports_codex_projects));
2817
- const [claude, codex] = await Promise.all([
2818
- getClaudeProjectFolders(),
2819
- getCodexProjects2().catch((error) => {
2820
- logError("Error reading Codex projects:", error);
2821
- return [];
2822
- })
2823
- ]);
2824
- return mergeProjectFolders(claude, codex);
3213
+ function metasToSessionFiles2(metas) {
3214
+ const files = metas.filter((m) => m.hasTranscript).map((m) => ({
3215
+ name: m.sessionId,
3216
+ path: m.eventsPath,
3217
+ lastModified: m.fileMtime,
3218
+ lastModifiedFormatted: formatDate(m.fileMtime),
3219
+ sessionId: m.sessionId,
3220
+ cli: "copilot"
3221
+ }));
3222
+ files.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
3223
+ return files;
2825
3224
  }
2826
- function resolveProjectPath(projectName) {
2827
- if (!projectName)
2828
- throw new RangeError("Empty project name");
2829
- if (/^[/\\]/.test(projectName))
2830
- throw new RangeError("Absolute project name");
2831
- const projectsPath = resolve5(getClaudeProjectsPath());
2832
- const candidate = resolve5(join6(projectsPath, projectName));
2833
- if (!candidate.startsWith(projectsPath + sep)) {
2834
- throw new RangeError("Project path escapes root");
3225
+ async function getCopilotSessionsForCwd(cwd) {
3226
+ let metas;
3227
+ try {
3228
+ metas = await cachedScan2();
3229
+ } catch (error) {
3230
+ logWarn("Failed to scan Copilot sessions:", error);
3231
+ return [];
2835
3232
  }
2836
- return candidate;
2837
- }
2838
- function extractSessionId2(filename) {
2839
- const uuidPattern = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
2840
- const match = filename.match(uuidPattern);
2841
- return match ? match[0] : undefined;
3233
+ return metasToSessionFiles2(metas.filter((m) => m.cwd === cwd));
3234
+ }
3235
+ async function getCopilotSessionsByEncodedName(name) {
3236
+ let metas;
3237
+ try {
3238
+ metas = await cachedScan2();
3239
+ } catch (error) {
3240
+ logWarn("Failed to scan Copilot sessions:", error);
3241
+ return { cwd: null, sessions: [] };
3242
+ }
3243
+ const matches = metas.filter((m) => m.hasTranscript && encodeFolderName(m.cwd) === name);
3244
+ return {
3245
+ cwd: matches[0]?.cwd ?? null,
3246
+ sessions: metasToSessionFiles2(matches)
3247
+ };
3248
+ }
3249
+ var cachedScan2, getCachedCopilotProjects, getCachedCopilotSessionsForCwd, getCachedCopilotSessionsByEncodedName;
3250
+ var init_copilot_projects = __esm(() => {
3251
+ init_paths();
3252
+ init_logger();
3253
+ cachedScan2 = runtimeCache(scanCopilotSessions, 30);
3254
+ getCachedCopilotProjects = runtimeCache(getCopilotProjects, 30);
3255
+ getCachedCopilotSessionsForCwd = runtimeCache((cwd) => getCopilotSessionsForCwd(cwd), 30, { maxSize: 50 });
3256
+ getCachedCopilotSessionsByEncodedName = runtimeCache((name) => getCopilotSessionsByEncodedName(name), 30, { maxSize: 50 });
3257
+ });
3258
+
3259
+ // lib/cursor-projects.ts
3260
+ var exports_cursor_projects = {};
3261
+ __export(exports_cursor_projects, {
3262
+ getCursorSessionsForCwd: () => getCursorSessionsForCwd,
3263
+ getCursorSessionsByEncodedName: () => getCursorSessionsByEncodedName,
3264
+ getCursorProjects: () => getCursorProjects,
3265
+ getCachedCursorSessionsForCwd: () => getCachedCursorSessionsForCwd,
3266
+ getCachedCursorSessionsByEncodedName: () => getCachedCursorSessionsByEncodedName,
3267
+ getCachedCursorProjects: () => getCachedCursorProjects
3268
+ });
3269
+ import { readdir as readdir3, readFile as readFile4, stat as stat3 } from "node:fs/promises";
3270
+ import { homedir as homedir9 } from "node:os";
3271
+ import { join as join7 } from "node:path";
3272
+ function getCursorHome() {
3273
+ return process.env.CURSOR_HOME || join7(homedir9(), ".cursor");
3274
+ }
3275
+ async function safeReaddir3(dir) {
3276
+ try {
3277
+ return await readdir3(dir, { withFileTypes: true });
3278
+ } catch {
3279
+ return null;
3280
+ }
3281
+ }
3282
+ async function statMtime2(path) {
3283
+ try {
3284
+ return (await stat3(path)).mtime;
3285
+ } catch {
3286
+ return null;
3287
+ }
3288
+ }
3289
+ function parseCwdFromMetaText(text) {
3290
+ try {
3291
+ const parsed = JSON.parse(text);
3292
+ if (typeof parsed.cwd === "string" && parsed.cwd.length > 0)
3293
+ return parsed.cwd;
3294
+ } catch {}
3295
+ const yaml = text.match(/^\s*cwd\s*:\s*(.+?)\s*$/m);
3296
+ if (yaml) {
3297
+ const stripped = yaml[1].replace(/^['"]|['"]$/g, "");
3298
+ if (stripped.length > 0)
3299
+ return stripped;
3300
+ }
3301
+ return;
3302
+ }
3303
+ async function findFirstUsableMeta(dir, candidates) {
3304
+ for (const name of candidates) {
3305
+ const path = join7(dir, name);
3306
+ if (await statMtime2(path) === null)
3307
+ continue;
3308
+ let text;
3309
+ try {
3310
+ text = await readFile4(path, "utf-8");
3311
+ } catch {
3312
+ continue;
3313
+ }
3314
+ const cwd = parseCwdFromMetaText(text);
3315
+ if (cwd)
3316
+ return { path, cwd };
3317
+ }
3318
+ return null;
3319
+ }
3320
+ async function findFirstExistingPath(dir, candidates) {
3321
+ for (const name of candidates) {
3322
+ const path = join7(dir, name);
3323
+ if (await statMtime2(path) !== null)
3324
+ return path;
3325
+ }
3326
+ return null;
3327
+ }
3328
+ async function scanCursorSessions() {
3329
+ const home = getCursorHome();
3330
+ const newCandidates = [];
3331
+ const legacyCandidates = [];
3332
+ const projectsRoot = join7(home, NEW_PROJECTS_DIR);
3333
+ const projectEntries = await safeReaddir3(projectsRoot);
3334
+ if (projectEntries) {
3335
+ for (const proj of projectEntries) {
3336
+ if (!proj.isDirectory())
3337
+ continue;
3338
+ const decoded = decodeFolderName(proj.name);
3339
+ const cwd = decoded.startsWith("/") || /^[A-Za-z]:\//.test(decoded) ? decoded : `/${decoded}`;
3340
+ const transcriptsRoot = join7(projectsRoot, proj.name, NEW_AGENT_TRANSCRIPTS_DIR);
3341
+ const sessionDirs = await safeReaddir3(transcriptsRoot);
3342
+ if (!sessionDirs)
3343
+ continue;
3344
+ for (const sd of sessionDirs) {
3345
+ if (!sd.isDirectory())
3346
+ continue;
3347
+ newCandidates.push({
3348
+ sessionId: sd.name,
3349
+ dir: join7(transcriptsRoot, sd.name),
3350
+ cwd
3351
+ });
3352
+ }
3353
+ }
3354
+ }
3355
+ for (const sub of LEGACY_SESSION_ROOT_CANDIDATES) {
3356
+ const root = join7(home, sub);
3357
+ const entries = await safeReaddir3(root);
3358
+ if (!entries)
3359
+ continue;
3360
+ for (const e of entries) {
3361
+ if (!e.isDirectory())
3362
+ continue;
3363
+ legacyCandidates.push({ sessionId: e.name, dir: join7(root, e.name) });
3364
+ }
3365
+ }
3366
+ if (newCandidates.length === 0 && legacyCandidates.length === 0)
3367
+ return [];
3368
+ const settled = await batchAll([
3369
+ ...newCandidates.map((c) => async () => {
3370
+ const transcriptPath = join7(c.dir, `${c.sessionId}.jsonl`);
3371
+ const transcriptMtime = await statMtime2(transcriptPath);
3372
+ if (!transcriptMtime)
3373
+ return null;
3374
+ return {
3375
+ metaPath: c.dir,
3376
+ transcriptPath,
3377
+ sessionId: c.sessionId,
3378
+ cwd: c.cwd,
3379
+ fileMtime: transcriptMtime,
3380
+ hasTranscript: true
3381
+ };
3382
+ }),
3383
+ ...legacyCandidates.map((c) => async () => {
3384
+ const meta = await findFirstUsableMeta(c.dir, META_FILE_CANDIDATES);
3385
+ if (!meta)
3386
+ return null;
3387
+ const transcriptPath = await findFirstExistingPath(c.dir, LEGACY_TRANSCRIPT_FILE_CANDIDATES);
3388
+ const transcriptMtime = transcriptPath ? await statMtime2(transcriptPath) : null;
3389
+ const metaMtime = await statMtime2(meta.path);
3390
+ const fileMtime = transcriptMtime && metaMtime ? new Date(Math.max(transcriptMtime.getTime(), metaMtime.getTime())) : transcriptMtime ?? metaMtime ?? new Date(0);
3391
+ return {
3392
+ metaPath: meta.path,
3393
+ transcriptPath,
3394
+ sessionId: c.sessionId,
3395
+ cwd: meta.cwd,
3396
+ fileMtime,
3397
+ hasTranscript: transcriptPath !== null
3398
+ };
3399
+ })
3400
+ ], 16);
3401
+ return settled.filter((r) => r.status === "fulfilled").map((r) => r.value).filter((v) => v !== null);
3402
+ }
3403
+ async function getCursorProjects() {
3404
+ let metas;
3405
+ try {
3406
+ metas = await cachedScan3();
3407
+ } catch (error) {
3408
+ logWarn("Failed to scan Cursor sessions:", error);
3409
+ return [];
3410
+ }
3411
+ const byCwd = new Map;
3412
+ for (const m of metas) {
3413
+ if (!m.hasTranscript)
3414
+ continue;
3415
+ const existing = byCwd.get(m.cwd);
3416
+ if (!existing || m.fileMtime.getTime() > existing.latest.getTime()) {
3417
+ byCwd.set(m.cwd, { latest: m.fileMtime, cwd: m.cwd });
3418
+ }
3419
+ }
3420
+ const folders = [];
3421
+ for (const { cwd, latest } of byCwd.values()) {
3422
+ folders.push({
3423
+ name: encodeFolderName(cwd),
3424
+ path: cwd,
3425
+ isDirectory: true,
3426
+ lastModified: latest,
3427
+ lastModifiedFormatted: formatDate(latest),
3428
+ cli: ["cursor"]
3429
+ });
3430
+ }
3431
+ folders.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
3432
+ return folders;
3433
+ }
3434
+ function metasToSessionFiles3(metas) {
3435
+ const files = metas.filter((m) => m.hasTranscript && m.transcriptPath).map((m) => ({
3436
+ name: m.sessionId,
3437
+ path: m.transcriptPath,
3438
+ lastModified: m.fileMtime,
3439
+ lastModifiedFormatted: formatDate(m.fileMtime),
3440
+ sessionId: m.sessionId,
3441
+ cli: "cursor"
3442
+ }));
3443
+ files.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
3444
+ return files;
3445
+ }
3446
+ async function getCursorSessionsForCwd(cwd) {
3447
+ let metas;
3448
+ try {
3449
+ metas = await cachedScan3();
3450
+ } catch (error) {
3451
+ logWarn("Failed to scan Cursor sessions:", error);
3452
+ return [];
3453
+ }
3454
+ return metasToSessionFiles3(metas.filter((m) => m.cwd === cwd));
3455
+ }
3456
+ async function getCursorSessionsByEncodedName(name) {
3457
+ let metas;
3458
+ try {
3459
+ metas = await cachedScan3();
3460
+ } catch (error) {
3461
+ logWarn("Failed to scan Cursor sessions:", error);
3462
+ return { cwd: null, sessions: [] };
3463
+ }
3464
+ const matches = metas.filter((m) => m.hasTranscript && encodeFolderName(m.cwd) === name);
3465
+ return {
3466
+ cwd: matches[0]?.cwd ?? null,
3467
+ sessions: metasToSessionFiles3(matches)
3468
+ };
3469
+ }
3470
+ var LEGACY_SESSION_ROOT_CANDIDATES, META_FILE_CANDIDATES, LEGACY_TRANSCRIPT_FILE_CANDIDATES, NEW_PROJECTS_DIR = "projects", NEW_AGENT_TRANSCRIPTS_DIR = "agent-transcripts", cachedScan3, getCachedCursorProjects, getCachedCursorSessionsForCwd, getCachedCursorSessionsByEncodedName;
3471
+ var init_cursor_projects = __esm(() => {
3472
+ init_paths();
3473
+ init_logger();
3474
+ LEGACY_SESSION_ROOT_CANDIDATES = ["agent-sessions", "conversations", "sessions"];
3475
+ META_FILE_CANDIDATES = ["meta.json", "session.json", "workspace.json", "workspace.yaml"];
3476
+ LEGACY_TRANSCRIPT_FILE_CANDIDATES = ["events.jsonl", "transcript.jsonl", "messages.jsonl"];
3477
+ cachedScan3 = runtimeCache(scanCursorSessions, 30);
3478
+ getCachedCursorProjects = runtimeCache(getCursorProjects, 30);
3479
+ getCachedCursorSessionsForCwd = runtimeCache((cwd) => getCursorSessionsForCwd(cwd), 30, { maxSize: 50 });
3480
+ getCachedCursorSessionsByEncodedName = runtimeCache((name) => getCursorSessionsByEncodedName(name), 30, { maxSize: 50 });
3481
+ });
3482
+
3483
+ // lib/opencode-projects.ts
3484
+ var exports_opencode_projects = {};
3485
+ __export(exports_opencode_projects, {
3486
+ getOpenCodeSessionsForCwd: () => getOpenCodeSessionsForCwd,
3487
+ getOpenCodeSessionsByEncodedName: () => getOpenCodeSessionsByEncodedName,
3488
+ getOpenCodeProjects: () => getOpenCodeProjects,
3489
+ getCachedOpenCodeSessionsForCwd: () => getCachedOpenCodeSessionsForCwd,
3490
+ getCachedOpenCodeSessionsByEncodedName: () => getCachedOpenCodeSessionsByEncodedName,
3491
+ getCachedOpenCodeProjects: () => getCachedOpenCodeProjects
3492
+ });
3493
+ import { execFileSync as execFileSync2 } from "node:child_process";
3494
+ import { basename as basename2 } from "node:path";
3495
+ function runOpenCodeDb(sql) {
3496
+ try {
3497
+ const stdout = execFileSync2("opencode", ["db", "--format", "json", sql], {
3498
+ encoding: "utf8",
3499
+ timeout: 5000,
3500
+ stdio: ["ignore", "pipe", "pipe"]
3501
+ });
3502
+ if (!stdout.trim())
3503
+ return [];
3504
+ const parsed = JSON.parse(stdout);
3505
+ if (!Array.isArray(parsed))
3506
+ return null;
3507
+ return parsed;
3508
+ } catch {
3509
+ return null;
3510
+ }
3511
+ }
3512
+ function readSessionRows() {
3513
+ return runOpenCodeDb(`SELECT id, project_id, slug, directory, title, time_created, time_updated FROM session ORDER BY time_updated DESC LIMIT ${SESSION_LIMIT}`);
3514
+ }
3515
+ function readProjectRows() {
3516
+ return runOpenCodeDb(`SELECT id, worktree, vcs, name, time_created, time_updated FROM project`);
3517
+ }
3518
+ async function getOpenCodeProjects() {
3519
+ const sessions = readSessionRows();
3520
+ const projects = readProjectRows();
3521
+ if (sessions === null && projects === null) {
3522
+ return [];
3523
+ }
3524
+ const projectMap = new Map;
3525
+ for (const p of projects ?? [])
3526
+ projectMap.set(p.id, p);
3527
+ const groups = new Map;
3528
+ for (const s of sessions ?? []) {
3529
+ if (!s.project_id)
3530
+ continue;
3531
+ let g = groups.get(s.project_id);
3532
+ if (!g) {
3533
+ g = { rows: [], latest: 0 };
3534
+ groups.set(s.project_id, g);
3535
+ }
3536
+ g.rows.push(s);
3537
+ if (s.time_updated > g.latest)
3538
+ g.latest = s.time_updated;
3539
+ }
3540
+ const seen = new Set;
3541
+ const out = [];
3542
+ for (const [projectId, group] of groups) {
3543
+ seen.add(projectId);
3544
+ const proj = projectMap.get(projectId);
3545
+ const worktree = proj?.worktree ?? group.rows[0]?.directory ?? null;
3546
+ const name = proj?.name?.trim() || (worktree ? basename2(worktree) : projectId);
3547
+ const path = worktree ?? "";
3548
+ const lastModified = new Date(Math.max(group.latest, proj?.time_updated ?? 0));
3549
+ out.push({
3550
+ name,
3551
+ path,
3552
+ isDirectory: true,
3553
+ lastModified,
3554
+ lastModifiedFormatted: formatDate(lastModified),
3555
+ cli: ["opencode"]
3556
+ });
3557
+ }
3558
+ for (const p of projects ?? []) {
3559
+ if (seen.has(p.id))
3560
+ continue;
3561
+ const worktree = p.worktree ?? "";
3562
+ const name = p.name?.trim() || (worktree ? basename2(worktree) : p.id);
3563
+ const lastModified = new Date(p.time_updated);
3564
+ out.push({
3565
+ name,
3566
+ path: worktree,
3567
+ isDirectory: true,
3568
+ lastModified,
3569
+ lastModifiedFormatted: formatDate(lastModified),
3570
+ cli: ["opencode"]
3571
+ });
3572
+ }
3573
+ out.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
3574
+ return out;
3575
+ }
3576
+ async function getOpenCodeSessionsForCwd(cwd) {
3577
+ const sessions = readSessionRows();
3578
+ if (!sessions)
3579
+ return [];
3580
+ const matches = sessions.filter((s) => s.directory === cwd);
3581
+ return matches.map((s) => {
3582
+ const lastModified = new Date(s.time_updated);
3583
+ return {
3584
+ name: s.title ?? s.slug ?? s.id,
3585
+ path: `opencode://${s.id}`,
3586
+ lastModified,
3587
+ lastModifiedFormatted: formatDate(lastModified),
3588
+ sessionId: s.id,
3589
+ cli: "opencode"
3590
+ };
3591
+ });
3592
+ }
3593
+ async function getOpenCodeSessionsByEncodedName(name) {
3594
+ let projects;
3595
+ let sessions;
3596
+ try {
3597
+ projects = readProjectRows();
3598
+ sessions = readSessionRows();
3599
+ } catch (error) {
3600
+ logWarn("Failed to read OpenCode DB:", error);
3601
+ return { cwd: null, sessions: [] };
3602
+ }
3603
+ if (!projects || !sessions)
3604
+ return { cwd: null, sessions: [] };
3605
+ const matchingProject = projects.find((p) => p.worktree && encodeFolderName(p.worktree) === name);
3606
+ if (!matchingProject || !matchingProject.worktree)
3607
+ return { cwd: null, sessions: [] };
3608
+ const matched = sessions.filter((s) => s.project_id === matchingProject.id);
3609
+ return {
3610
+ cwd: matchingProject.worktree,
3611
+ sessions: matched.map((s) => {
3612
+ const lastModified = new Date(s.time_updated);
3613
+ return {
3614
+ name: s.title ?? s.slug ?? s.id,
3615
+ path: `opencode://${s.id}`,
3616
+ lastModified,
3617
+ lastModifiedFormatted: formatDate(lastModified),
3618
+ sessionId: s.id,
3619
+ cli: "opencode"
3620
+ };
3621
+ })
3622
+ };
3623
+ }
3624
+ var SESSION_LIMIT = 1000, getCachedOpenCodeProjects, getCachedOpenCodeSessionsForCwd, getCachedOpenCodeSessionsByEncodedName;
3625
+ var init_opencode_projects = __esm(() => {
3626
+ init_paths();
3627
+ init_logger();
3628
+ getCachedOpenCodeProjects = runtimeCache(getOpenCodeProjects, 30);
3629
+ getCachedOpenCodeSessionsForCwd = runtimeCache((cwd) => getOpenCodeSessionsForCwd(cwd), 30, { maxSize: 50 });
3630
+ getCachedOpenCodeSessionsByEncodedName = runtimeCache((name) => getOpenCodeSessionsByEncodedName(name), 30, { maxSize: 50 });
3631
+ });
3632
+
3633
+ // lib/pi-projects.ts
3634
+ var exports_pi_projects = {};
3635
+ __export(exports_pi_projects, {
3636
+ getPiSessionsForCwd: () => getPiSessionsForCwd,
3637
+ getPiSessionsByEncodedName: () => getPiSessionsByEncodedName,
3638
+ getPiProjects: () => getPiProjects,
3639
+ getCachedPiSessionsForCwd: () => getCachedPiSessionsForCwd,
3640
+ getCachedPiSessionsByEncodedName: () => getCachedPiSessionsByEncodedName,
3641
+ getCachedPiProjects: () => getCachedPiProjects
3642
+ });
3643
+ import { readdir as readdir4, readFile as readFile5, stat as stat4 } from "node:fs/promises";
3644
+ import { homedir as homedir10 } from "node:os";
3645
+ import { join as join8 } from "node:path";
3646
+ function getPiSessionsRoot() {
3647
+ return process.env.PI_SESSIONS_DIR || join8(homedir10(), ".pi", "agent", "sessions");
3648
+ }
3649
+ async function safeReaddir4(dir) {
3650
+ try {
3651
+ return await readdir4(dir, { withFileTypes: true });
3652
+ } catch {
3653
+ return null;
3654
+ }
3655
+ }
3656
+ async function statMtime3(path) {
3657
+ try {
3658
+ return (await stat4(path)).mtime;
3659
+ } catch {
3660
+ return null;
3661
+ }
3662
+ }
3663
+ async function readSessionCwd(filePath) {
3664
+ let text;
3665
+ try {
3666
+ text = await readFile5(filePath, "utf-8");
3667
+ } catch {
3668
+ return null;
3669
+ }
3670
+ const firstLine = text.indexOf(`
3671
+ `) >= 0 ? text.slice(0, text.indexOf(`
3672
+ `)) : text;
3673
+ if (!firstLine)
3674
+ return null;
3675
+ try {
3676
+ const parsed = JSON.parse(firstLine);
3677
+ if (parsed.type !== "session")
3678
+ return null;
3679
+ if (typeof parsed.cwd !== "string" || parsed.cwd.length === 0)
3680
+ return null;
3681
+ return parsed.cwd;
3682
+ } catch {
3683
+ return null;
3684
+ }
3685
+ }
3686
+ async function scanPiSessions() {
3687
+ const root = getPiSessionsRoot();
3688
+ const cwdDirs = await safeReaddir4(root);
3689
+ if (!cwdDirs)
3690
+ return [];
3691
+ const candidates = [];
3692
+ for (const cwdDir of cwdDirs) {
3693
+ if (!cwdDir.isDirectory())
3694
+ continue;
3695
+ const cwdPath = join8(root, cwdDir.name);
3696
+ const sessionFiles = await safeReaddir4(cwdPath);
3697
+ if (!sessionFiles)
3698
+ continue;
3699
+ for (const f of sessionFiles) {
3700
+ if (!f.isFile())
3701
+ continue;
3702
+ const m = SESSION_FILE_RE.exec(f.name);
3703
+ if (!m)
3704
+ continue;
3705
+ candidates.push({ sessionId: m[1], filePath: join8(cwdPath, f.name) });
3706
+ }
3707
+ }
3708
+ if (candidates.length === 0)
3709
+ return [];
3710
+ const settled = await batchAll(candidates.map((c) => async () => {
3711
+ const cwd = await readSessionCwd(c.filePath);
3712
+ if (!cwd)
3713
+ return null;
3714
+ const mtime = await statMtime3(c.filePath);
3715
+ if (!mtime)
3716
+ return null;
3717
+ return {
3718
+ filePath: c.filePath,
3719
+ sessionId: c.sessionId,
3720
+ cwd,
3721
+ fileMtime: mtime
3722
+ };
3723
+ }), 16);
3724
+ return settled.filter((r) => r.status === "fulfilled").map((r) => r.value).filter((v) => v !== null);
3725
+ }
3726
+ async function getPiProjects() {
3727
+ let metas;
3728
+ try {
3729
+ metas = await cachedScan4();
3730
+ } catch (error) {
3731
+ logWarn("Failed to scan Pi sessions:", error);
3732
+ return [];
3733
+ }
3734
+ const byCwd = new Map;
3735
+ for (const m of metas) {
3736
+ const existing = byCwd.get(m.cwd);
3737
+ if (!existing || m.fileMtime.getTime() > existing.latest.getTime()) {
3738
+ byCwd.set(m.cwd, { latest: m.fileMtime, cwd: m.cwd });
3739
+ }
3740
+ }
3741
+ const folders = [];
3742
+ for (const { cwd, latest } of byCwd.values()) {
3743
+ folders.push({
3744
+ name: encodeFolderName(cwd),
3745
+ path: cwd,
3746
+ isDirectory: true,
3747
+ lastModified: latest,
3748
+ lastModifiedFormatted: formatDate(latest),
3749
+ cli: ["pi"]
3750
+ });
3751
+ }
3752
+ folders.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
3753
+ return folders;
3754
+ }
3755
+ function metasToSessionFiles4(metas) {
3756
+ const files = metas.map((m) => ({
3757
+ name: m.sessionId,
3758
+ path: m.filePath,
3759
+ lastModified: m.fileMtime,
3760
+ lastModifiedFormatted: formatDate(m.fileMtime),
3761
+ sessionId: m.sessionId,
3762
+ cli: "pi"
3763
+ }));
3764
+ files.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
3765
+ return files;
3766
+ }
3767
+ async function getPiSessionsForCwd(cwd) {
3768
+ let metas;
3769
+ try {
3770
+ metas = await cachedScan4();
3771
+ } catch (error) {
3772
+ logWarn("Failed to scan Pi sessions:", error);
3773
+ return [];
3774
+ }
3775
+ return metasToSessionFiles4(metas.filter((m) => m.cwd === cwd));
3776
+ }
3777
+ async function getPiSessionsByEncodedName(name) {
3778
+ let metas;
3779
+ try {
3780
+ metas = await cachedScan4();
3781
+ } catch (error) {
3782
+ logWarn("Failed to scan Pi sessions:", error);
3783
+ return { cwd: null, sessions: [] };
3784
+ }
3785
+ const matches = metas.filter((m) => encodeFolderName(m.cwd) === name);
3786
+ const uniqueCwds = Array.from(new Set(matches.map((m) => m.cwd)));
3787
+ if (uniqueCwds.length !== 1) {
3788
+ return { cwd: null, sessions: [] };
3789
+ }
3790
+ return {
3791
+ cwd: uniqueCwds[0],
3792
+ sessions: metasToSessionFiles4(matches)
3793
+ };
3794
+ }
3795
+ var SESSION_FILE_RE, cachedScan4, getCachedPiProjects, getCachedPiSessionsForCwd, getCachedPiSessionsByEncodedName;
3796
+ var init_pi_projects = __esm(() => {
3797
+ init_paths();
3798
+ init_logger();
3799
+ SESSION_FILE_RE = /^[\d-]+T[\d-]+Z_([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i;
3800
+ cachedScan4 = runtimeCache(scanPiSessions, 30);
3801
+ getCachedPiProjects = runtimeCache(getPiProjects, 30);
3802
+ getCachedPiSessionsForCwd = runtimeCache((cwd) => getPiSessionsForCwd(cwd), 30, { maxSize: 50 });
3803
+ getCachedPiSessionsByEncodedName = runtimeCache((name) => getPiSessionsByEncodedName(name), 30, { maxSize: 50 });
3804
+ });
3805
+
3806
+ // lib/gemini-projects.ts
3807
+ var exports_gemini_projects = {};
3808
+ __export(exports_gemini_projects, {
3809
+ getGeminiSessionsForCwd: () => getGeminiSessionsForCwd,
3810
+ getGeminiSessionsByEncodedName: () => getGeminiSessionsByEncodedName,
3811
+ getGeminiProjects: () => getGeminiProjects,
3812
+ getCachedGeminiSessionsByEncodedName: () => getCachedGeminiSessionsByEncodedName,
3813
+ getCachedGeminiProjects: () => getCachedGeminiProjects
3814
+ });
3815
+ import { readdir as readdir5, readFile as readFile6, stat as stat5 } from "node:fs/promises";
3816
+ import { homedir as homedir11 } from "node:os";
3817
+ import { join as join9 } from "node:path";
3818
+ function getGeminiTmpRoot() {
3819
+ return process.env.GEMINI_SESSIONS_DIR || join9(homedir11(), ".gemini", "tmp");
3820
+ }
3821
+ async function safeReaddir5(dir) {
3822
+ try {
3823
+ return await readdir5(dir, { withFileTypes: true });
3824
+ } catch {
3825
+ return null;
3826
+ }
3827
+ }
3828
+ async function statMtime4(path) {
3829
+ try {
3830
+ return (await stat5(path)).mtime;
3831
+ } catch {
3832
+ return null;
3833
+ }
3834
+ }
3835
+ async function readProjectRoot(projectDir) {
3836
+ try {
3837
+ const text = await readFile6(join9(projectDir, ".project_root"), "utf-8");
3838
+ const trimmed = text.trim();
3839
+ return trimmed.length > 0 ? trimmed : null;
3840
+ } catch {
3841
+ return null;
3842
+ }
3843
+ }
3844
+ async function scanGeminiSessions() {
3845
+ const root = getGeminiTmpRoot();
3846
+ const projectDirs = await safeReaddir5(root);
3847
+ if (!projectDirs)
3848
+ return [];
3849
+ const out = [];
3850
+ await batchAll(projectDirs.filter((d) => d.isDirectory()).map((d) => async () => {
3851
+ const projectDir = join9(root, d.name);
3852
+ const cwd = await readProjectRoot(projectDir);
3853
+ if (!cwd)
3854
+ return;
3855
+ const chatsDir = join9(projectDir, "chats");
3856
+ const files = await safeReaddir5(chatsDir);
3857
+ if (!files)
3858
+ return;
3859
+ for (const f of files) {
3860
+ if (!f.isFile())
3861
+ continue;
3862
+ if (!SESSION_FILE_RE2.test(f.name))
3863
+ continue;
3864
+ const filePath = join9(chatsDir, f.name);
3865
+ const mtime = await statMtime4(filePath);
3866
+ if (!mtime)
3867
+ continue;
3868
+ out.push({ filePath, sessionFilename: f.name, cwd, fileMtime: mtime });
3869
+ }
3870
+ }), 16);
3871
+ return out;
3872
+ }
3873
+ async function getGeminiProjects() {
3874
+ const sessions = await scanGeminiSessions();
3875
+ const byCwd = new Map;
3876
+ for (const s of sessions) {
3877
+ const existing = byCwd.get(s.cwd);
3878
+ if (!existing || s.fileMtime.getTime() > existing.mtime.getTime()) {
3879
+ byCwd.set(s.cwd, { mtime: s.fileMtime });
3880
+ }
3881
+ }
3882
+ const folders = [...byCwd.entries()].map(([cwd, { mtime }]) => ({
3883
+ name: encodeFolderName(cwd),
3884
+ path: cwd,
3885
+ isDirectory: true,
3886
+ lastModified: mtime,
3887
+ lastModifiedFormatted: formatDate(mtime),
3888
+ cli: ["gemini"]
3889
+ }));
3890
+ folders.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
3891
+ return folders;
3892
+ }
3893
+ async function getGeminiSessionsForCwd(cwd) {
3894
+ const sessions = await scanGeminiSessions();
3895
+ const matches = sessions.filter((s) => s.cwd === cwd);
3896
+ const files = matches.map((s) => {
3897
+ const m = s.sessionFilename.match(SESSION_FILE_RE2);
3898
+ return {
3899
+ name: s.sessionFilename,
3900
+ path: s.filePath,
3901
+ lastModified: s.fileMtime,
3902
+ lastModifiedFormatted: formatDate(s.fileMtime),
3903
+ sessionId: m ? m[2] : undefined,
3904
+ cli: "gemini"
3905
+ };
3906
+ });
3907
+ files.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
3908
+ return files;
3909
+ }
3910
+ async function getGeminiSessionsByEncodedName(name) {
3911
+ let metas;
3912
+ try {
3913
+ metas = await scanGeminiSessions();
3914
+ } catch (error) {
3915
+ logWarn("Failed to scan Gemini sessions:", error);
3916
+ return { cwd: null, sessions: [] };
3917
+ }
3918
+ const matches = metas.filter((m) => encodeFolderName(m.cwd) === name);
3919
+ const uniqueCwds = Array.from(new Set(matches.map((m) => m.cwd)));
3920
+ if (uniqueCwds.length !== 1) {
3921
+ return { cwd: null, sessions: [] };
3922
+ }
3923
+ const sessions = matches.map((s) => {
3924
+ const m = s.sessionFilename.match(SESSION_FILE_RE2);
3925
+ return {
3926
+ name: s.sessionFilename,
3927
+ path: s.filePath,
3928
+ lastModified: s.fileMtime,
3929
+ lastModifiedFormatted: formatDate(s.fileMtime),
3930
+ sessionId: m ? m[2] : undefined,
3931
+ cli: "gemini"
3932
+ };
3933
+ });
3934
+ sessions.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
3935
+ return { cwd: uniqueCwds[0], sessions };
3936
+ }
3937
+ var SESSION_FILE_RE2, getCachedGeminiProjects, getCachedGeminiSessionsByEncodedName;
3938
+ var init_gemini_projects = __esm(() => {
3939
+ init_paths();
3940
+ init_logger();
3941
+ SESSION_FILE_RE2 = /^session-(\d{4}-\d{2}-\d{2}T\d{2}-\d{2})-([0-9a-f]{8})\.jsonl$/i;
3942
+ getCachedGeminiProjects = runtimeCache(getGeminiProjects, 30);
3943
+ getCachedGeminiSessionsByEncodedName = runtimeCache((name) => getGeminiSessionsByEncodedName(name), 30, { maxSize: 50 });
3944
+ });
3945
+
3946
+ // lib/projects.ts
3947
+ import { readdir as readdir6, stat as stat6 } from "node:fs/promises";
3948
+ import { join as join10, resolve as resolve5, sep } from "node:path";
3949
+ async function getMtime(path, label) {
3950
+ try {
3951
+ return (await stat6(path)).mtime;
3952
+ } catch (error) {
3953
+ logWarn(`Failed to stat ${label}:`, error);
3954
+ return new Date(0);
3955
+ }
3956
+ }
3957
+ async function safeReaddir6(dirPath) {
3958
+ try {
3959
+ const s = await stat6(dirPath);
3960
+ if (!s.isDirectory())
3961
+ return null;
3962
+ return await readdir6(dirPath, { withFileTypes: true });
3963
+ } catch {
3964
+ return null;
3965
+ }
3966
+ }
3967
+ async function getClaudeProjectFolders() {
3968
+ try {
3969
+ const projectsPath = getClaudeProjectsPath();
3970
+ const entries = await safeReaddir6(projectsPath);
3971
+ if (!entries)
3972
+ return [];
3973
+ const settled = await batchAll(entries.filter((entry) => entry.isDirectory()).map((entry) => async () => {
3974
+ const folderPath = join10(projectsPath, entry.name);
3975
+ const mtime = await getMtime(folderPath, entry.name);
3976
+ return {
3977
+ name: entry.name,
3978
+ path: folderPath,
3979
+ isDirectory: true,
3980
+ lastModified: mtime,
3981
+ lastModifiedFormatted: formatDate(mtime),
3982
+ cli: ["claude"]
3983
+ };
3984
+ }), 16);
3985
+ const folders = settled.filter((r) => r.status === "fulfilled").map((r) => r.value);
3986
+ folders.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
3987
+ return folders;
3988
+ } catch (error) {
3989
+ logError("Error reading Claude project folders:", error);
3990
+ return [];
3991
+ }
3992
+ }
3993
+ function mergeProjectFolders(...sources) {
3994
+ const byName = new Map;
3995
+ for (const list of sources) {
3996
+ for (const f of list) {
3997
+ const existing = byName.get(f.name);
3998
+ if (!existing) {
3999
+ byName.set(f.name, { ...f, cli: [...f.cli] });
4000
+ continue;
4001
+ }
4002
+ const mergedCli = [...existing.cli];
4003
+ for (const c of f.cli)
4004
+ if (!mergedCli.includes(c))
4005
+ mergedCli.push(c);
4006
+ const newer = f.lastModified.getTime() > existing.lastModified.getTime() ? f : existing;
4007
+ byName.set(f.name, {
4008
+ ...existing,
4009
+ cli: mergedCli,
4010
+ lastModified: newer.lastModified,
4011
+ lastModifiedFormatted: newer.lastModifiedFormatted
4012
+ });
4013
+ }
4014
+ }
4015
+ const merged = Array.from(byName.values());
4016
+ merged.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
4017
+ return merged;
4018
+ }
4019
+ async function getProjectFolders() {
4020
+ const [
4021
+ { getCodexProjects: getCodexProjects2 },
4022
+ { getCopilotProjects: getCopilotProjects2 },
4023
+ { getCursorProjects: getCursorProjects2 },
4024
+ { getOpenCodeProjects: getOpenCodeProjects2 },
4025
+ { getPiProjects: getPiProjects2 },
4026
+ { getGeminiProjects: getGeminiProjects2 }
4027
+ ] = await Promise.all([
4028
+ Promise.resolve().then(() => (init_codex_projects(), exports_codex_projects)),
4029
+ Promise.resolve().then(() => (init_copilot_projects(), exports_copilot_projects)),
4030
+ Promise.resolve().then(() => (init_cursor_projects(), exports_cursor_projects)),
4031
+ Promise.resolve().then(() => (init_opencode_projects(), exports_opencode_projects)),
4032
+ Promise.resolve().then(() => (init_pi_projects(), exports_pi_projects)),
4033
+ Promise.resolve().then(() => (init_gemini_projects(), exports_gemini_projects))
4034
+ ]);
4035
+ const [claude, codex, copilot, cursor, opencode, pi, gemini] = await Promise.all([
4036
+ getClaudeProjectFolders(),
4037
+ getCodexProjects2().catch((error) => {
4038
+ logError("Error reading Codex projects:", error);
4039
+ return [];
4040
+ }),
4041
+ getCopilotProjects2().catch((error) => {
4042
+ logError("Error reading Copilot projects:", error);
4043
+ return [];
4044
+ }),
4045
+ getCursorProjects2().catch((error) => {
4046
+ logError("Error reading Cursor projects:", error);
4047
+ return [];
4048
+ }),
4049
+ getOpenCodeProjects2().catch((error) => {
4050
+ logError("Error reading OpenCode projects:", error);
4051
+ return [];
4052
+ }),
4053
+ getPiProjects2().catch((error) => {
4054
+ logError("Error reading Pi projects:", error);
4055
+ return [];
4056
+ }),
4057
+ getGeminiProjects2().catch((error) => {
4058
+ logError("Error reading Gemini projects:", error);
4059
+ return [];
4060
+ })
4061
+ ]);
4062
+ return mergeProjectFolders(claude, codex, copilot, cursor, opencode, pi, gemini);
4063
+ }
4064
+ function resolveProjectPath(projectName) {
4065
+ if (!projectName)
4066
+ throw new RangeError("Empty project name");
4067
+ if (/^[/\\]/.test(projectName))
4068
+ throw new RangeError("Absolute project name");
4069
+ const projectsPath = resolve5(getClaudeProjectsPath());
4070
+ const candidate = resolve5(join10(projectsPath, projectName));
4071
+ if (!candidate.startsWith(projectsPath + sep)) {
4072
+ throw new RangeError("Project path escapes root");
4073
+ }
4074
+ return candidate;
4075
+ }
4076
+ function extractSessionId2(filename) {
4077
+ const uuidPattern = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
4078
+ const match = filename.match(uuidPattern);
4079
+ return match ? match[0] : undefined;
2842
4080
  }
2843
4081
  async function getSessionFiles(projectPath) {
2844
4082
  try {
2845
- const entries = await safeReaddir2(projectPath);
4083
+ const entries = await safeReaddir6(projectPath);
2846
4084
  if (!entries)
2847
4085
  return [];
2848
4086
  const jsonlEntries = entries.filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl") && extractSessionId2(entry.name));
2849
4087
  const settled = await batchAll(jsonlEntries.map((entry) => async () => {
2850
- const filePath = join6(projectPath, entry.name);
4088
+ const filePath = join10(projectPath, entry.name);
2851
4089
  const mtime = await getMtime(filePath, entry.name);
2852
4090
  return {
2853
4091
  name: entry.name,
@@ -2876,13 +4114,13 @@ var init_projects = __esm(() => {
2876
4114
 
2877
4115
  // lib/resolve-subagent-path.ts
2878
4116
  import { access as access2 } from "fs/promises";
2879
- import { join as join7, relative as relative2 } from "path";
4117
+ import { join as join11, relative as relative2 } from "path";
2880
4118
  async function resolveSubagentPath(projectsPath, projectName, sessionId, agentId) {
2881
4119
  const fileName = `agent-${agentId}.jsonl`;
2882
4120
  const candidatePaths = [
2883
- join7(projectsPath, projectName, fileName),
2884
- join7(projectsPath, projectName, sessionId, fileName),
2885
- join7(projectsPath, projectName, sessionId, "subagents", fileName)
4121
+ join11(projectsPath, projectName, fileName),
4122
+ join11(projectsPath, projectName, sessionId, fileName),
4123
+ join11(projectsPath, projectName, sessionId, "subagents", fileName)
2886
4124
  ];
2887
4125
  for (const candidatePath of candidatePaths) {
2888
4126
  if (relative2(projectsPath, candidatePath).startsWith("..")) {
@@ -2920,8 +4158,8 @@ function formatDuration(ms) {
2920
4158
  }
2921
4159
 
2922
4160
  // lib/log-entries.ts
2923
- import { readFile as readFile3 } from "fs/promises";
2924
- import { join as join8 } from "path";
4161
+ import { readFile as readFile7 } from "fs/promises";
4162
+ import { join as join12 } from "path";
2925
4163
  function formatTimestamp(date) {
2926
4164
  const base = formatDate(date);
2927
4165
  const ms = date.getMilliseconds().toString().padStart(3, "0");
@@ -3102,8 +4340,8 @@ async function parseFileContent(fileContent, source) {
3102
4340
  async function parseSessionLog(projectName, sessionId) {
3103
4341
  const projectDir = resolveProjectPath(projectName);
3104
4342
  const projectsPath = getClaudeProjectsPath();
3105
- const filePath = join8(projectDir, `${sessionId}.jsonl`);
3106
- const fileContent = await readFile3(filePath, "utf-8");
4343
+ const filePath = join12(projectDir, `${sessionId}.jsonl`);
4344
+ const fileContent = await readFile7(filePath, "utf-8");
3107
4345
  const { entries: sessionEntries, rawLines: sessionRawLines, subagentIds } = await parseFileContent(fileContent, "session");
3108
4346
  if (subagentIds.length === 0) {
3109
4347
  return { entries: sessionEntries, rawLines: sessionRawLines, subagentIds: [] };
@@ -3113,7 +4351,7 @@ async function parseSessionLog(projectName, sessionId) {
3113
4351
  const agentPath = await resolveSubagentPath(projectsPath, projectName, sessionId, agentId);
3114
4352
  if (!agentPath)
3115
4353
  return null;
3116
- const agentContent = await readFile3(agentPath, "utf-8");
4354
+ const agentContent = await readFile7(agentPath, "utf-8");
3117
4355
  const { entries, rawLines } = await parseFileContent(agentContent, agentSource);
3118
4356
  return { entries, rawLines };
3119
4357
  }), SUBAGENT_CONCURRENCY);
@@ -3140,9 +4378,9 @@ var init_log_entries = __esm(() => {
3140
4378
 
3141
4379
  // lib/codex-sessions.ts
3142
4380
  import { readFileSync as readFileSync3, readdirSync as readdirSync3, existsSync as existsSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4, statSync as statSync4 } from "node:fs";
3143
- import { readFile as readFile4 } from "node:fs/promises";
3144
- import { dirname as dirname3, join as join9 } from "node:path";
3145
- import { homedir as homedir8 } from "node:os";
4381
+ import { readFile as readFile8 } from "node:fs/promises";
4382
+ import { dirname as dirname3, join as join13 } from "node:path";
4383
+ import { homedir as homedir12 } from "node:os";
3146
4384
  function readCache() {
3147
4385
  try {
3148
4386
  if (!existsSync5(CACHE_PATH))
@@ -3164,7 +4402,7 @@ function dirSearch(dir, sessionId) {
3164
4402
  try {
3165
4403
  for (const f of readdirSync3(dir, { withFileTypes: true })) {
3166
4404
  if (f.isFile() && f.name.includes(sessionId) && f.name.endsWith(".jsonl")) {
3167
- return join9(dir, f.name);
4405
+ return join13(dir, f.name);
3168
4406
  }
3169
4407
  }
3170
4408
  } catch {}
@@ -3175,14 +4413,14 @@ function findCodexTranscript(sessionId) {
3175
4413
  const cached = cache[sessionId];
3176
4414
  if (cached && existsSync5(cached))
3177
4415
  return cached;
3178
- const root = join9(homedir8(), ".codex", "sessions");
4416
+ const root = join13(homedir12(), ".codex", "sessions");
3179
4417
  const today = new Date;
3180
4418
  const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000);
3181
4419
  const datedDirs = [today, yesterday].map((d) => {
3182
4420
  const y = String(d.getUTCFullYear());
3183
4421
  const m = String(d.getUTCMonth() + 1).padStart(2, "0");
3184
4422
  const day = String(d.getUTCDate()).padStart(2, "0");
3185
- return join9(root, y, m, day);
4423
+ return join13(root, y, m, day);
3186
4424
  });
3187
4425
  for (const dir of datedDirs) {
3188
4426
  const hit = dirSearch(dir, sessionId);
@@ -3195,13 +4433,13 @@ function findCodexTranscript(sessionId) {
3195
4433
  for (const y of readdirSync3(root, { withFileTypes: true })) {
3196
4434
  if (!y.isDirectory())
3197
4435
  continue;
3198
- for (const m of readdirSync3(join9(root, y.name), { withFileTypes: true })) {
4436
+ for (const m of readdirSync3(join13(root, y.name), { withFileTypes: true })) {
3199
4437
  if (!m.isDirectory())
3200
4438
  continue;
3201
- for (const d of readdirSync3(join9(root, y.name, m.name), { withFileTypes: true })) {
4439
+ for (const d of readdirSync3(join13(root, y.name, m.name), { withFileTypes: true })) {
3202
4440
  if (!d.isDirectory())
3203
4441
  continue;
3204
- const hit = dirSearch(join9(root, y.name, m.name, d.name), sessionId);
4442
+ const hit = dirSearch(join13(root, y.name, m.name, d.name), sessionId);
3205
4443
  if (hit) {
3206
4444
  writeCacheEntry(sessionId, hit);
3207
4445
  return hit;
@@ -3414,14 +4652,14 @@ async function getCodexSessionLog(sessionId) {
3414
4652
  const filePath = findCodexTranscript(sessionId);
3415
4653
  if (!filePath)
3416
4654
  return null;
3417
- const fileContent = await readFile4(filePath, "utf-8");
4655
+ const fileContent = await readFile8(filePath, "utf-8");
3418
4656
  const { entries, rawLines, cwd } = await parseCodexLog(fileContent, "session");
3419
4657
  return { entries, rawLines, cwd, filePath };
3420
4658
  }
3421
4659
  var CACHE_PATH, getCachedCodexSessionLog;
3422
4660
  var init_codex_sessions = __esm(() => {
3423
4661
  init_log_entries();
3424
- CACHE_PATH = join9(homedir8(), ".failproofai", "cache", "codex-session-paths.json");
4662
+ CACHE_PATH = join13(homedir12(), ".failproofai", "cache", "codex-session-paths.json");
3425
4663
  getCachedCodexSessionLog = runtimeCache((sessionId) => getCodexSessionLog(sessionId), 60, { maxSize: 50 });
3426
4664
  });
3427
4665
 
@@ -3557,8 +4795,8 @@ import {
3557
4795
  openSync,
3558
4796
  closeSync
3559
4797
  } from "node:fs";
3560
- import { join as join10 } from "node:path";
3561
- import { homedir as homedir9 } from "node:os";
4798
+ import { join as join14 } from "node:path";
4799
+ import { homedir as homedir13 } from "node:os";
3562
4800
  function ensureAuthDir() {
3563
4801
  if (!existsSync6(AUTH_DIR))
3564
4802
  mkdirSync5(AUTH_DIR, { recursive: true, mode: 448 });
@@ -3593,8 +4831,8 @@ function isLoggedIn() {
3593
4831
  }
3594
4832
  var AUTH_DIR, AUTH_FILE;
3595
4833
  var init_token_store = __esm(() => {
3596
- AUTH_DIR = join10(homedir9(), ".failproofai");
3597
- AUTH_FILE = join10(AUTH_DIR, "auth.json");
4834
+ AUTH_DIR = join14(homedir13(), ".failproofai");
4835
+ AUTH_FILE = join14(AUTH_DIR, "auth.json");
3598
4836
  });
3599
4837
 
3600
4838
  // src/relay/queue.ts
@@ -3618,8 +4856,8 @@ import {
3618
4856
  readdirSync as readdirSync4,
3619
4857
  chmodSync
3620
4858
  } from "node:fs";
3621
- import { join as join11 } from "node:path";
3622
- import { homedir as homedir10 } from "node:os";
4859
+ import { join as join15 } from "node:path";
4860
+ import { homedir as homedir14 } from "node:os";
3623
4861
  import { createHash, randomUUID } from "node:crypto";
3624
4862
  function hashCwd(cwd) {
3625
4863
  if (!cwd)
@@ -3687,7 +4925,7 @@ function claimPendingBatch() {
3687
4925
  return null;
3688
4926
  }
3689
4927
  const seq = `${Date.now()}-${process.pid}`;
3690
- const processingFile = join11(QUEUE_DIR, `${PROCESSING_PREFIX}${seq}.jsonl`);
4928
+ const processingFile = join15(QUEUE_DIR, `${PROCESSING_PREFIX}${seq}.jsonl`);
3691
4929
  try {
3692
4930
  renameSync4(PENDING_FILE, processingFile);
3693
4931
  try {
@@ -3704,7 +4942,7 @@ function claimPendingBatch() {
3704
4942
  function findOrphanProcessingFiles() {
3705
4943
  ensureDir2();
3706
4944
  try {
3707
- return readdirSync4(QUEUE_DIR).filter((n) => n.startsWith(PROCESSING_PREFIX) && n.endsWith(".jsonl")).map((n) => join11(QUEUE_DIR, n)).sort();
4945
+ return readdirSync4(QUEUE_DIR).filter((n) => n.startsWith(PROCESSING_PREFIX) && n.endsWith(".jsonl")).map((n) => join15(QUEUE_DIR, n)).sort();
3708
4946
  } catch {
3709
4947
  return [];
3710
4948
  }
@@ -3733,15 +4971,15 @@ function deleteProcessingFile(path2) {
3733
4971
  var QUEUE_DIR, PENDING_FILE, PROCESSING_PREFIX = "processing-", MAX_QUEUE_BYTES;
3734
4972
  var init_queue = __esm(() => {
3735
4973
  init_token_store();
3736
- QUEUE_DIR = join11(homedir10(), ".failproofai", "cache", "server-queue");
3737
- PENDING_FILE = join11(QUEUE_DIR, "pending.jsonl");
4974
+ QUEUE_DIR = join15(homedir14(), ".failproofai", "cache", "server-queue");
4975
+ PENDING_FILE = join15(QUEUE_DIR, "pending.jsonl");
3738
4976
  MAX_QUEUE_BYTES = 50 * 1024 * 1024;
3739
4977
  });
3740
4978
 
3741
4979
  // src/relay/pid.ts
3742
4980
  import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, existsSync as existsSync8, unlinkSync as unlinkSync4, mkdirSync as mkdirSync7 } from "node:fs";
3743
- import { join as join12, dirname as dirname4 } from "node:path";
3744
- import { homedir as homedir11 } from "node:os";
4981
+ import { join as join16, dirname as dirname4 } from "node:path";
4982
+ import { homedir as homedir15 } from "node:os";
3745
4983
  function readPid() {
3746
4984
  if (!existsSync8(PID_FILE))
3747
4985
  return null;
@@ -3794,7 +5032,7 @@ function stopRelay() {
3794
5032
  }
3795
5033
  var PID_FILE;
3796
5034
  var init_pid = __esm(() => {
3797
- PID_FILE = join12(homedir11(), ".failproofai", "relay.pid");
5035
+ PID_FILE = join16(homedir15(), ".failproofai", "relay.pid");
3798
5036
  });
3799
5037
 
3800
5038
  // src/relay/daemon.ts
@@ -3807,8 +5045,8 @@ __export(exports_daemon, {
3807
5045
  });
3808
5046
  import { spawn } from "node:child_process";
3809
5047
  import { existsSync as existsSync9 } from "node:fs";
3810
- import { join as join13 } from "node:path";
3811
- import { homedir as homedir12 } from "node:os";
5048
+ import { join as join17 } from "node:path";
5049
+ import { homedir as homedir16 } from "node:os";
3812
5050
  import { randomUUID as randomUUID2 } from "node:crypto";
3813
5051
  function ensureRelayRunning() {
3814
5052
  if (!isLoggedIn())
@@ -4075,7 +5313,7 @@ var init_daemon = __esm(() => {
4075
5313
  init_token_store();
4076
5314
  init_pid();
4077
5315
  init_queue();
4078
- QUEUE_DIR2 = join13(homedir12(), ".failproofai", "cache", "server-queue");
5316
+ QUEUE_DIR2 = join17(homedir16(), ".failproofai", "cache", "server-queue");
4079
5317
  });
4080
5318
 
4081
5319
  // src/hooks/handler.ts
@@ -4089,6 +5327,29 @@ function canonicalizeEventType(raw, cli) {
4089
5327
  if (mapped)
4090
5328
  return mapped;
4091
5329
  }
5330
+ if (cli === "cursor") {
5331
+ const mapped = CURSOR_EVENT_MAP[raw];
5332
+ if (mapped)
5333
+ return mapped;
5334
+ }
5335
+ if (cli === "pi") {
5336
+ const mapped = PI_EVENT_MAP[raw];
5337
+ if (mapped)
5338
+ return mapped;
5339
+ }
5340
+ if (cli === "gemini") {
5341
+ const mapped = GEMINI_EVENT_MAP[raw];
5342
+ if (mapped)
5343
+ return mapped;
5344
+ }
5345
+ return raw;
5346
+ }
5347
+ function canonicalizeToolName(raw, cli) {
5348
+ if (!raw)
5349
+ return raw;
5350
+ if (cli === "gemini") {
5351
+ return GEMINI_TOOL_MAP[raw] ?? raw;
5352
+ }
4092
5353
  return raw;
4093
5354
  }
4094
5355
  async function handleHookEvent(eventType, cli = "claude") {
@@ -4127,6 +5388,11 @@ async function handleHookEvent(eventType, cli = "claude") {
4127
5388
  }
4128
5389
  }
4129
5390
  const canonicalEventType = canonicalizeEventType(eventType, cli);
5391
+ const rawToolName = parsed.tool_name;
5392
+ const canonicalToolName = canonicalizeToolName(rawToolName, cli);
5393
+ if (canonicalToolName !== rawToolName) {
5394
+ parsed.tool_name = canonicalToolName;
5395
+ }
4130
5396
  const sessionId = parsed.session_id;
4131
5397
  const session = {
4132
5398
  sessionId,
@@ -4134,6 +5400,7 @@ async function handleHookEvent(eventType, cli = "claude") {
4134
5400
  cwd: parsed.cwd,
4135
5401
  permissionMode: resolvePermissionMode(cli, parsed, sessionId),
4136
5402
  hookEventName: parsed.hook_event_name,
5403
+ rawHookEventName: eventType,
4137
5404
  cli
4138
5405
  };
4139
5406
  const config = readMergedHooksConfig(session.cwd);
@@ -4275,8 +5542,8 @@ __export(exports_daemon2, {
4275
5542
  });
4276
5543
  import { spawn as spawn2 } from "node:child_process";
4277
5544
  import { existsSync as existsSync10 } from "node:fs";
4278
- import { join as join14 } from "node:path";
4279
- import { homedir as homedir13 } from "node:os";
5545
+ import { join as join18 } from "node:path";
5546
+ import { homedir as homedir17 } from "node:os";
4280
5547
  import { randomUUID as randomUUID3 } from "node:crypto";
4281
5548
  function ensureRelayRunning2() {
4282
5549
  if (!isLoggedIn())
@@ -4543,14 +5810,15 @@ var init_daemon2 = __esm(() => {
4543
5810
  init_token_store();
4544
5811
  init_pid();
4545
5812
  init_queue();
4546
- QUEUE_DIR3 = join14(homedir13(), ".failproofai", "cache", "server-queue");
5813
+ QUEUE_DIR3 = join18(homedir17(), ".failproofai", "cache", "server-queue");
4547
5814
  });
4548
5815
 
4549
5816
  // src/hooks/integrations.ts
4550
5817
  import { execSync as execSync3 } from "node:child_process";
4551
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, existsSync as existsSync11, mkdirSync as mkdirSync8 } from "node:fs";
5818
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, existsSync as existsSync11, mkdirSync as mkdirSync8, unlinkSync as unlinkSync5 } from "node:fs";
4552
5819
  import { resolve as resolve6, dirname as dirname5 } from "node:path";
4553
- import { homedir as homedir14 } from "node:os";
5820
+ import { fileURLToPath } from "node:url";
5821
+ import { homedir as homedir18 } from "node:os";
4554
5822
  function readJsonFile(path2) {
4555
5823
  if (!existsSync11(path2))
4556
5824
  return {};
@@ -4563,9 +5831,12 @@ function writeJsonFile(path2, data) {
4563
5831
  `, "utf8");
4564
5832
  }
4565
5833
  function isMarkedHook(hook) {
4566
- if (hook[FAILPROOFAI_HOOK_MARKER] === true)
5834
+ if (!hook || typeof hook !== "object")
5835
+ return false;
5836
+ const h = hook;
5837
+ if (h[FAILPROOFAI_HOOK_MARKER] === true)
4567
5838
  return true;
4568
- const cmd = typeof hook.command === "string" ? hook.command : "";
5839
+ const cmd = typeof h.command === "string" ? h.command : "";
4569
5840
  return cmd.includes("failproofai") && cmd.includes("--hook");
4570
5841
  }
4571
5842
  function binaryExists(name) {
@@ -4577,6 +5848,194 @@ function binaryExists(name) {
4577
5848
  return false;
4578
5849
  }
4579
5850
  }
5851
+ function isMarkedCopilotHook(hook) {
5852
+ if (hook[FAILPROOFAI_HOOK_MARKER] === true)
5853
+ return true;
5854
+ const bash = typeof hook.bash === "string" ? hook.bash : "";
5855
+ const ps = typeof hook.powershell === "string" ? hook.powershell : "";
5856
+ for (const cmd of [bash, ps]) {
5857
+ if (cmd.includes("failproofai") && cmd.includes("--hook"))
5858
+ return true;
5859
+ }
5860
+ return false;
5861
+ }
5862
+ function opencodePluginFilePath(settingsPath) {
5863
+ return resolve6(dirname5(settingsPath), "plugins", "failproofai.mjs");
5864
+ }
5865
+ function buildOpenCodePluginShim(binaryPath, scope) {
5866
+ const useNpx = scope === "project";
5867
+ const escapedBin = useNpx ? '""' : JSON.stringify(binaryPath);
5868
+ return `// AUTO-GENERATED by failproofai. ${FAILPROOFAI_HOOK_MARKER}
5869
+ // Re-generate via: failproofai policies --install --cli opencode
5870
+ // Plugin shim that bridges OpenCode's plugin API to the failproofai binary.
5871
+ // See: https://opencode.ai/docs/plugins/
5872
+ import { spawnSync } from "node:child_process";
5873
+
5874
+ // Map opencode bus-event types → canonical failproofai event names.
5875
+ // (The binary sees PascalCase — the binary's --cli=opencode flag is for
5876
+ // telemetry / activity tagging only; no opencode branch in handler.ts.)
5877
+ const BUS_EVENT_MAP = {
5878
+ "session.created": "SessionStart",
5879
+ "session.deleted": "SessionEnd",
5880
+ "session.idle": "Stop",
5881
+ // message.updated is handled separately (filter to role:user); see below.
5882
+ };
5883
+
5884
+ const FAILPROOFAI_BIN = ${escapedBin};
5885
+ const USE_NPX = ${useNpx};
5886
+
5887
+ function runFailproofai(eventName, payload, directory) {
5888
+ const cmd = USE_NPX ? "npx" : FAILPROOFAI_BIN;
5889
+ const args = USE_NPX
5890
+ ? ["-y", "failproofai", "--hook", eventName, "--cli", "opencode"]
5891
+ : ["--hook", eventName, "--cli", "opencode"];
5892
+ const r = spawnSync(cmd, args, {
5893
+ input: JSON.stringify(payload),
5894
+ encoding: "utf8",
5895
+ timeout: 60_000,
5896
+ cwd: directory,
5897
+ });
5898
+ return { exitCode: r.status ?? 0, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
5899
+ }
5900
+
5901
+ function applyDecision(result, ctx) {
5902
+ // Deny path 1: exit 2 (Claude Stop-style or any non-Pre/Post deny).
5903
+ if (result.exitCode === 2) {
5904
+ throw new Error((result.stderr || "").trim() || "Blocked by failproofai");
5905
+ }
5906
+ // Deny path 2: stdout JSON with hookSpecificOutput.permissionDecision === "deny".
5907
+ let parsed = null;
5908
+ try { parsed = JSON.parse(result.stdout); } catch { /* fail-open allow */ }
5909
+ if (!parsed) return;
5910
+ const out = parsed.hookSpecificOutput;
5911
+ if (out && out.permissionDecision === "deny") {
5912
+ throw new Error(out.permissionDecisionReason || "Blocked by failproofai");
5913
+ }
5914
+ // Codex-shape PermissionRequest deny: hookSpecificOutput.decision.behavior.
5915
+ if (out && out.decision && out.decision.behavior === "deny") {
5916
+ throw new Error((out.decision.message) || "Blocked by failproofai");
5917
+ }
5918
+ // Instruct: forward the additional context as a prompt to the session.
5919
+ const ctxText = out && out.additionalContext;
5920
+ if (ctxText && ctx && ctx.client && ctx.sessionID) {
5921
+ // Fire-and-forget: don't block the tool call on the SDK round-trip.
5922
+ Promise.resolve(ctx.client.session.prompt({
5923
+ path: { id: ctx.sessionID },
5924
+ body: { parts: [{ type: "text", text: ctxText }] },
5925
+ })).catch(() => {});
5926
+ }
5927
+ }
5928
+
5929
+ export default async function failproofaiPlugin({ client, directory }) {
5930
+ return {
5931
+ // Generic bus events: session lifecycle + user-prompt detection.
5932
+ event: async ({ event }) => {
5933
+ if (!event || !event.type) return;
5934
+
5935
+ // UserPromptSubmit — filter message.updated to user role only so we
5936
+ // don't fire on every assistant token. Forward the prompt text so
5937
+ // prompt-based policies (sanitize-* on input, content checks) see it.
5938
+ if (event.type === "message.updated") {
5939
+ const props = event.properties || {};
5940
+ const info = props.info || props.message || {};
5941
+ const role = info.role || props.role;
5942
+ if (role !== "user") return;
5943
+ const sessionID = info.sessionID || info.sessionId || info.session_id || props.sessionID;
5944
+ // OpenCode's message shape: parts is an array of {type, text, ...}.
5945
+ // Concatenate text parts to reconstruct the user-facing prompt.
5946
+ // Fall back to direct text/content fields if a future shape differs.
5947
+ let prompt = "";
5948
+ const parts = info.parts || props.parts || [];
5949
+ if (Array.isArray(parts)) {
5950
+ for (const p of parts) {
5951
+ if (p && typeof p === "object" && typeof p.text === "string") prompt += p.text;
5952
+ }
5953
+ }
5954
+ if (!prompt) prompt = (info.text || info.content || props.text || "").toString();
5955
+ const r = runFailproofai("UserPromptSubmit", {
5956
+ session_id: sessionID, cwd: directory, hook_event_name: "UserPromptSubmit", prompt,
5957
+ }, directory);
5958
+ applyDecision(r, { client, sessionID });
5959
+ return;
5960
+ }
5961
+
5962
+ const claudeEvent = BUS_EVENT_MAP[event.type];
5963
+ if (!claudeEvent) return;
5964
+ const props = event.properties || {};
5965
+ const sessionID = props.sessionID || (props.session && props.session.id) || props.id;
5966
+ const r = runFailproofai(claudeEvent, {
5967
+ session_id: sessionID, cwd: directory, hook_event_name: claudeEvent,
5968
+ }, directory);
5969
+ applyDecision(r, { client, sessionID });
5970
+ },
5971
+
5972
+ // First-class PreToolUse hook. Note: tool args live on output.args (mutable).
5973
+ "tool.execute.before": async (input, output) => {
5974
+ const r = runFailproofai("PreToolUse", {
5975
+ session_id: input.sessionID,
5976
+ cwd: directory,
5977
+ tool_name: input.tool,
5978
+ tool_input: output.args,
5979
+ hook_event_name: "PreToolUse",
5980
+ }, directory);
5981
+ applyDecision(r, { client, sessionID: input.sessionID });
5982
+ },
5983
+
5984
+ // First-class PostToolUse hook. Note: tool args live on input.args here.
5985
+ "tool.execute.after": async (input, output) => {
5986
+ const r = runFailproofai("PostToolUse", {
5987
+ session_id: input.sessionID,
5988
+ cwd: directory,
5989
+ tool_name: input.tool,
5990
+ tool_input: input.args,
5991
+ tool_response: { title: output.title, output: output.output, metadata: output.metadata },
5992
+ hook_event_name: "PostToolUse",
5993
+ }, directory);
5994
+ applyDecision(r, { client, sessionID: input.sessionID });
5995
+ },
5996
+
5997
+ // Cleaner deny UX for prompted tools — mutate output.status instead of throwing.
5998
+ "permission.ask": async (input, output) => {
5999
+ const r = runFailproofai("PermissionRequest", {
6000
+ session_id: input.sessionID,
6001
+ cwd: directory,
6002
+ tool_name: input.tool || input.command || "permission",
6003
+ tool_input: input,
6004
+ hook_event_name: "PermissionRequest",
6005
+ }, directory);
6006
+ try {
6007
+ applyDecision(r, { client, sessionID: input.sessionID });
6008
+ } catch {
6009
+ output.status = "deny";
6010
+ }
6011
+ },
6012
+ };
6013
+ }
6014
+ `;
6015
+ }
6016
+ function getPiExtensionPath() {
6017
+ const fromEnv = process.env.FAILPROOFAI_PACKAGE_ROOT;
6018
+ if (fromEnv)
6019
+ return resolve6(fromEnv, "pi-extension");
6020
+ return resolve6(fileURLToPath(import.meta.url), "..", "..", "..", "pi-extension");
6021
+ }
6022
+ function isFailproofaiPiEntry(source) {
6023
+ if (typeof source !== "string")
6024
+ return false;
6025
+ if (/(?:^|\/)pi-extension\/?$/.test(source))
6026
+ return true;
6027
+ return source.includes("pi-extension") && source.includes("failproofai");
6028
+ }
6029
+ function makePiProjectRelativeEntry(extPath) {
6030
+ const cwd = process.cwd();
6031
+ const cwdResolved = resolve6(cwd);
6032
+ const extResolved = resolve6(extPath);
6033
+ if (extResolved.startsWith(cwdResolved + "/") || extResolved === cwdResolved) {
6034
+ const fromSettingsDir = "../" + extResolved.slice(cwdResolved.length + 1);
6035
+ return fromSettingsDir;
6036
+ }
6037
+ return extResolved;
6038
+ }
4580
6039
  function getIntegration(id) {
4581
6040
  const integration = INTEGRATIONS[id];
4582
6041
  if (!integration) {
@@ -4587,7 +6046,7 @@ function getIntegration(id) {
4587
6046
  function detectInstalledClis() {
4588
6047
  return INTEGRATION_TYPES.filter((id) => INTEGRATIONS[id].detectInstalled());
4589
6048
  }
4590
- var claudeCode, codex, INTEGRATIONS;
6049
+ var claudeCode, codex, copilot, cursor, OPENCODE_PLUGIN_REL_PATH = "./plugins/failproofai.mjs", opencode, pi, gemini, INTEGRATIONS;
4591
6050
  var init_integrations = __esm(() => {
4592
6051
  init_types();
4593
6052
  claudeCode = {
@@ -4599,34 +6058,262 @@ var init_integrations = __esm(() => {
4599
6058
  const base = cwd ? resolve6(cwd) : process.cwd();
4600
6059
  switch (scope) {
4601
6060
  case "user":
4602
- return resolve6(homedir14(), ".claude", "settings.json");
6061
+ return resolve6(homedir18(), ".claude", "settings.json");
6062
+ case "project":
6063
+ return resolve6(base, ".claude", "settings.json");
6064
+ case "local":
6065
+ return resolve6(base, ".claude", "settings.local.json");
6066
+ }
6067
+ },
6068
+ readSettings(settingsPath) {
6069
+ return readJsonFile(settingsPath);
6070
+ },
6071
+ writeSettings(settingsPath, settings) {
6072
+ writeJsonFile(settingsPath, settings);
6073
+ },
6074
+ buildHookEntry(binaryPath, eventType, scope) {
6075
+ const command = scope === "project" ? `npx -y failproofai --hook ${eventType}` : `"${binaryPath}" --hook ${eventType}`;
6076
+ return {
6077
+ type: "command",
6078
+ command,
6079
+ timeout: 60000,
6080
+ [FAILPROOFAI_HOOK_MARKER]: true
6081
+ };
6082
+ },
6083
+ isFailproofaiHook: isMarkedHook,
6084
+ writeHookEntries(settings, binaryPath, scope) {
6085
+ const s = settings;
6086
+ if (!s.hooks)
6087
+ s.hooks = {};
6088
+ for (const eventType of HOOK_EVENT_TYPES) {
6089
+ const hookEntry = this.buildHookEntry(binaryPath, eventType, scope);
6090
+ if (!s.hooks[eventType])
6091
+ s.hooks[eventType] = [];
6092
+ const matchers = s.hooks[eventType];
6093
+ let found = false;
6094
+ for (const matcher of matchers) {
6095
+ if (!matcher.hooks)
6096
+ continue;
6097
+ const idx = matcher.hooks.findIndex((h) => isMarkedHook(h));
6098
+ if (idx >= 0) {
6099
+ matcher.hooks[idx] = hookEntry;
6100
+ found = true;
6101
+ break;
6102
+ }
6103
+ }
6104
+ if (!found)
6105
+ matchers.push({ hooks: [hookEntry] });
6106
+ }
6107
+ },
6108
+ removeHooksFromFile(settingsPath) {
6109
+ const settings = this.readSettings(settingsPath);
6110
+ if (!settings.hooks)
6111
+ return 0;
6112
+ let removed = 0;
6113
+ for (const eventType of Object.keys(settings.hooks)) {
6114
+ const matchers = settings.hooks[eventType];
6115
+ if (!Array.isArray(matchers))
6116
+ continue;
6117
+ for (let i = matchers.length - 1;i >= 0; i--) {
6118
+ const matcher = matchers[i];
6119
+ if (!matcher.hooks)
6120
+ continue;
6121
+ const before = matcher.hooks.length;
6122
+ matcher.hooks = matcher.hooks.filter((h) => !isMarkedHook(h));
6123
+ removed += before - matcher.hooks.length;
6124
+ if (matcher.hooks.length === 0)
6125
+ matchers.splice(i, 1);
6126
+ }
6127
+ if (matchers.length === 0)
6128
+ delete settings.hooks[eventType];
6129
+ }
6130
+ if (Object.keys(settings.hooks).length === 0)
6131
+ delete settings.hooks;
6132
+ this.writeSettings(settingsPath, settings);
6133
+ return removed;
6134
+ },
6135
+ hooksInstalledInSettings(scope, cwd) {
6136
+ const settingsPath = this.getSettingsPath(scope, cwd);
6137
+ if (!existsSync11(settingsPath))
6138
+ return false;
6139
+ try {
6140
+ const settings = this.readSettings(settingsPath);
6141
+ if (!settings.hooks)
6142
+ return false;
6143
+ for (const matchers of Object.values(settings.hooks)) {
6144
+ if (!Array.isArray(matchers))
6145
+ continue;
6146
+ for (const matcher of matchers) {
6147
+ if (!matcher.hooks)
6148
+ continue;
6149
+ if (matcher.hooks.some((h) => isMarkedHook(h)))
6150
+ return true;
6151
+ }
6152
+ }
6153
+ } catch {}
6154
+ return false;
6155
+ },
6156
+ detectInstalled() {
6157
+ return binaryExists("claude") || binaryExists("claude-code");
6158
+ }
6159
+ };
6160
+ codex = {
6161
+ id: "codex",
6162
+ displayName: "OpenAI Codex",
6163
+ scopes: CODEX_HOOK_SCOPES,
6164
+ eventTypes: CODEX_HOOK_EVENT_TYPES,
6165
+ getSettingsPath(scope, cwd) {
6166
+ const base = cwd ? resolve6(cwd) : process.cwd();
6167
+ switch (scope) {
6168
+ case "user":
6169
+ return resolve6(homedir18(), ".codex", "hooks.json");
6170
+ case "project":
6171
+ return resolve6(base, ".codex", "hooks.json");
6172
+ case "local":
6173
+ return resolve6(base, ".codex", "hooks.json");
6174
+ }
6175
+ },
6176
+ readSettings(settingsPath) {
6177
+ const raw = readJsonFile(settingsPath);
6178
+ if (raw.version === undefined)
6179
+ raw.version = 1;
6180
+ return raw;
6181
+ },
6182
+ writeSettings(settingsPath, settings) {
6183
+ writeJsonFile(settingsPath, settings);
6184
+ },
6185
+ buildHookEntry(binaryPath, eventType, scope) {
6186
+ const command = scope === "project" ? `npx -y failproofai --hook ${eventType} --cli codex` : `"${binaryPath}" --hook ${eventType} --cli codex`;
6187
+ return {
6188
+ type: "command",
6189
+ command,
6190
+ timeout: 60000,
6191
+ [FAILPROOFAI_HOOK_MARKER]: true
6192
+ };
6193
+ },
6194
+ isFailproofaiHook: isMarkedHook,
6195
+ writeHookEntries(settings, binaryPath, scope) {
6196
+ const s = settings;
6197
+ if (s.version === undefined)
6198
+ s.version = 1;
6199
+ if (!s.hooks)
6200
+ s.hooks = {};
6201
+ for (const eventType of CODEX_HOOK_EVENT_TYPES) {
6202
+ const pascalKey = CODEX_EVENT_MAP[eventType];
6203
+ const hookEntry = this.buildHookEntry(binaryPath, eventType, scope);
6204
+ if (!s.hooks[pascalKey])
6205
+ s.hooks[pascalKey] = [];
6206
+ const matchers = s.hooks[pascalKey];
6207
+ let found = false;
6208
+ for (const matcher of matchers) {
6209
+ if (!matcher.hooks)
6210
+ continue;
6211
+ const idx = matcher.hooks.findIndex((h) => isMarkedHook(h));
6212
+ if (idx >= 0) {
6213
+ matcher.hooks[idx] = hookEntry;
6214
+ found = true;
6215
+ break;
6216
+ }
6217
+ }
6218
+ if (!found)
6219
+ matchers.push({ hooks: [hookEntry] });
6220
+ }
6221
+ },
6222
+ removeHooksFromFile(settingsPath) {
6223
+ const settings = this.readSettings(settingsPath);
6224
+ if (!settings.hooks)
6225
+ return 0;
6226
+ let removed = 0;
6227
+ for (const eventType of Object.keys(settings.hooks)) {
6228
+ const matchers = settings.hooks[eventType];
6229
+ if (!Array.isArray(matchers))
6230
+ continue;
6231
+ for (let i = matchers.length - 1;i >= 0; i--) {
6232
+ const matcher = matchers[i];
6233
+ if (!matcher.hooks)
6234
+ continue;
6235
+ const before = matcher.hooks.length;
6236
+ matcher.hooks = matcher.hooks.filter((h) => !isMarkedHook(h));
6237
+ removed += before - matcher.hooks.length;
6238
+ if (matcher.hooks.length === 0)
6239
+ matchers.splice(i, 1);
6240
+ }
6241
+ if (matchers.length === 0)
6242
+ delete settings.hooks[eventType];
6243
+ }
6244
+ if (Object.keys(settings.hooks).length === 0)
6245
+ delete settings.hooks;
6246
+ this.writeSettings(settingsPath, settings);
6247
+ return removed;
6248
+ },
6249
+ hooksInstalledInSettings(scope, cwd) {
6250
+ const settingsPath = this.getSettingsPath(scope, cwd);
6251
+ if (!existsSync11(settingsPath))
6252
+ return false;
6253
+ try {
6254
+ const settings = this.readSettings(settingsPath);
6255
+ if (!settings.hooks)
6256
+ return false;
6257
+ for (const matchers of Object.values(settings.hooks)) {
6258
+ if (!Array.isArray(matchers))
6259
+ continue;
6260
+ for (const matcher of matchers) {
6261
+ if (!matcher.hooks)
6262
+ continue;
6263
+ if (matcher.hooks.some((h) => isMarkedHook(h)))
6264
+ return true;
6265
+ }
6266
+ }
6267
+ } catch {}
6268
+ return false;
6269
+ },
6270
+ detectInstalled() {
6271
+ return binaryExists("codex");
6272
+ }
6273
+ };
6274
+ copilot = {
6275
+ id: "copilot",
6276
+ displayName: "GitHub Copilot",
6277
+ scopes: COPILOT_HOOK_SCOPES,
6278
+ eventTypes: COPILOT_HOOK_EVENT_TYPES,
6279
+ getSettingsPath(scope, cwd) {
6280
+ const base = cwd ? resolve6(cwd) : process.cwd();
6281
+ switch (scope) {
6282
+ case "user":
6283
+ return resolve6(homedir18(), ".copilot", "hooks", "failproofai.json");
4603
6284
  case "project":
4604
- return resolve6(base, ".claude", "settings.json");
6285
+ return resolve6(base, ".github", "hooks", "failproofai.json");
4605
6286
  case "local":
4606
- return resolve6(base, ".claude", "settings.local.json");
6287
+ return resolve6(base, ".github", "hooks", "failproofai.json");
4607
6288
  }
4608
6289
  },
4609
6290
  readSettings(settingsPath) {
4610
- return readJsonFile(settingsPath);
6291
+ const raw = readJsonFile(settingsPath);
6292
+ if (raw.version === undefined)
6293
+ raw.version = 1;
6294
+ return raw;
4611
6295
  },
4612
6296
  writeSettings(settingsPath, settings) {
4613
6297
  writeJsonFile(settingsPath, settings);
4614
6298
  },
4615
6299
  buildHookEntry(binaryPath, eventType, scope) {
4616
- const command = scope === "project" ? `npx -y failproofai --hook ${eventType}` : `"${binaryPath}" --hook ${eventType}`;
6300
+ const cmd = scope === "project" ? `npx -y failproofai --hook ${eventType} --cli copilot` : `"${binaryPath}" --hook ${eventType} --cli copilot`;
4617
6301
  return {
4618
6302
  type: "command",
4619
- command,
4620
- timeout: 60000,
6303
+ bash: cmd,
6304
+ powershell: cmd,
6305
+ timeoutSec: 60,
4621
6306
  [FAILPROOFAI_HOOK_MARKER]: true
4622
6307
  };
4623
6308
  },
4624
- isFailproofaiHook: isMarkedHook,
6309
+ isFailproofaiHook: isMarkedCopilotHook,
4625
6310
  writeHookEntries(settings, binaryPath, scope) {
4626
6311
  const s = settings;
6312
+ if (s.version === undefined)
6313
+ s.version = 1;
4627
6314
  if (!s.hooks)
4628
6315
  s.hooks = {};
4629
- for (const eventType of HOOK_EVENT_TYPES) {
6316
+ for (const eventType of COPILOT_HOOK_EVENT_TYPES) {
4630
6317
  const hookEntry = this.buildHookEntry(binaryPath, eventType, scope);
4631
6318
  if (!s.hooks[eventType])
4632
6319
  s.hooks[eventType] = [];
@@ -4635,7 +6322,7 @@ var init_integrations = __esm(() => {
4635
6322
  for (const matcher of matchers) {
4636
6323
  if (!matcher.hooks)
4637
6324
  continue;
4638
- const idx = matcher.hooks.findIndex((h) => isMarkedHook(h));
6325
+ const idx = matcher.hooks.findIndex((h) => isMarkedCopilotHook(h));
4639
6326
  if (idx >= 0) {
4640
6327
  matcher.hooks[idx] = hookEntry;
4641
6328
  found = true;
@@ -4660,7 +6347,7 @@ var init_integrations = __esm(() => {
4660
6347
  if (!matcher.hooks)
4661
6348
  continue;
4662
6349
  const before = matcher.hooks.length;
4663
- matcher.hooks = matcher.hooks.filter((h) => !isMarkedHook(h));
6350
+ matcher.hooks = matcher.hooks.filter((h) => !isMarkedCopilotHook(h));
4664
6351
  removed += before - matcher.hooks.length;
4665
6352
  if (matcher.hooks.length === 0)
4666
6353
  matchers.splice(i, 1);
@@ -4687,7 +6374,7 @@ var init_integrations = __esm(() => {
4687
6374
  for (const matcher of matchers) {
4688
6375
  if (!matcher.hooks)
4689
6376
  continue;
4690
- if (matcher.hooks.some((h) => isMarkedHook(h)))
6377
+ if (matcher.hooks.some((h) => isMarkedCopilotHook(h)))
4691
6378
  return true;
4692
6379
  }
4693
6380
  }
@@ -4695,23 +6382,23 @@ var init_integrations = __esm(() => {
4695
6382
  return false;
4696
6383
  },
4697
6384
  detectInstalled() {
4698
- return binaryExists("claude") || binaryExists("claude-code");
6385
+ return binaryExists("copilot");
4699
6386
  }
4700
6387
  };
4701
- codex = {
4702
- id: "codex",
4703
- displayName: "OpenAI Codex",
4704
- scopes: CODEX_HOOK_SCOPES,
4705
- eventTypes: CODEX_HOOK_EVENT_TYPES,
6388
+ cursor = {
6389
+ id: "cursor",
6390
+ displayName: "Cursor Agent",
6391
+ scopes: CURSOR_HOOK_SCOPES,
6392
+ eventTypes: CURSOR_HOOK_EVENT_TYPES,
4706
6393
  getSettingsPath(scope, cwd) {
4707
6394
  const base = cwd ? resolve6(cwd) : process.cwd();
4708
6395
  switch (scope) {
4709
6396
  case "user":
4710
- return resolve6(homedir14(), ".codex", "hooks.json");
6397
+ return resolve6(homedir18(), ".cursor", "hooks.json");
4711
6398
  case "project":
4712
- return resolve6(base, ".codex", "hooks.json");
6399
+ return resolve6(base, ".cursor", "hooks.json");
4713
6400
  case "local":
4714
- return resolve6(base, ".codex", "hooks.json");
6401
+ return resolve6(base, ".cursor", "hooks.json");
4715
6402
  }
4716
6403
  },
4717
6404
  readSettings(settingsPath) {
@@ -4724,7 +6411,7 @@ var init_integrations = __esm(() => {
4724
6411
  writeJsonFile(settingsPath, settings);
4725
6412
  },
4726
6413
  buildHookEntry(binaryPath, eventType, scope) {
4727
- const command = scope === "project" ? `npx -y failproofai --hook ${eventType} --cli codex` : `"${binaryPath}" --hook ${eventType} --cli codex`;
6414
+ const command = scope === "project" ? `npx -y failproofai --hook ${eventType} --cli cursor` : `"${binaryPath}" --hook ${eventType} --cli cursor`;
4728
6415
  return {
4729
6416
  type: "command",
4730
6417
  command,
@@ -4739,12 +6426,291 @@ var init_integrations = __esm(() => {
4739
6426
  s.version = 1;
4740
6427
  if (!s.hooks)
4741
6428
  s.hooks = {};
4742
- for (const eventType of CODEX_HOOK_EVENT_TYPES) {
4743
- const pascalKey = CODEX_EVENT_MAP[eventType];
6429
+ for (const eventType of CURSOR_HOOK_EVENT_TYPES) {
4744
6430
  const hookEntry = this.buildHookEntry(binaryPath, eventType, scope);
4745
- if (!s.hooks[pascalKey])
4746
- s.hooks[pascalKey] = [];
4747
- const matchers = s.hooks[pascalKey];
6431
+ const existing = s.hooks[eventType];
6432
+ const entries = existing ?? [];
6433
+ if (!existing)
6434
+ s.hooks[eventType] = entries;
6435
+ const idx = entries.findIndex((h) => isMarkedHook(h));
6436
+ if (idx >= 0) {
6437
+ entries[idx] = hookEntry;
6438
+ } else {
6439
+ entries.push(hookEntry);
6440
+ }
6441
+ }
6442
+ },
6443
+ removeHooksFromFile(settingsPath) {
6444
+ const settings = this.readSettings(settingsPath);
6445
+ if (!settings.hooks)
6446
+ return 0;
6447
+ let removed = 0;
6448
+ for (const eventType of Object.keys(settings.hooks)) {
6449
+ const entries = settings.hooks[eventType];
6450
+ if (!Array.isArray(entries))
6451
+ continue;
6452
+ const before = entries.length;
6453
+ const filtered = entries.filter((h) => !isMarkedHook(h));
6454
+ removed += before - filtered.length;
6455
+ if (filtered.length === 0) {
6456
+ delete settings.hooks[eventType];
6457
+ } else {
6458
+ settings.hooks[eventType] = filtered;
6459
+ }
6460
+ }
6461
+ if (Object.keys(settings.hooks).length === 0)
6462
+ delete settings.hooks;
6463
+ this.writeSettings(settingsPath, settings);
6464
+ return removed;
6465
+ },
6466
+ hooksInstalledInSettings(scope, cwd) {
6467
+ const settingsPath = this.getSettingsPath(scope, cwd);
6468
+ if (!existsSync11(settingsPath))
6469
+ return false;
6470
+ try {
6471
+ const settings = this.readSettings(settingsPath);
6472
+ if (!settings.hooks)
6473
+ return false;
6474
+ for (const entries of Object.values(settings.hooks)) {
6475
+ if (!Array.isArray(entries))
6476
+ continue;
6477
+ if (entries.some((h) => isMarkedHook(h)))
6478
+ return true;
6479
+ }
6480
+ } catch {}
6481
+ return false;
6482
+ },
6483
+ detectInstalled() {
6484
+ return binaryExists("cursor-agent") || binaryExists("agent");
6485
+ }
6486
+ };
6487
+ opencode = {
6488
+ id: "opencode",
6489
+ displayName: "OpenCode",
6490
+ scopes: OPENCODE_HOOK_SCOPES,
6491
+ eventTypes: OPENCODE_HOOK_EVENT_TYPES,
6492
+ getSettingsPath(scope, cwd) {
6493
+ const base = cwd ? resolve6(cwd) : process.cwd();
6494
+ switch (scope) {
6495
+ case "user":
6496
+ return resolve6(homedir18(), ".config", "opencode", "opencode.json");
6497
+ case "project":
6498
+ return resolve6(base, ".opencode", "opencode.json");
6499
+ case "local":
6500
+ return resolve6(base, ".opencode", "opencode.json");
6501
+ }
6502
+ },
6503
+ readSettings(settingsPath) {
6504
+ return readJsonFile(settingsPath);
6505
+ },
6506
+ writeSettings(settingsPath, settings) {
6507
+ writeJsonFile(settingsPath, settings);
6508
+ },
6509
+ buildHookEntry(_binaryPath, _eventType, scope) {
6510
+ if (scope === "user") {
6511
+ const abs = resolve6(homedir18(), ".config", "opencode", "plugins", "failproofai.mjs");
6512
+ return { spec: `file://${abs}`, [FAILPROOFAI_HOOK_MARKER]: true };
6513
+ }
6514
+ return { spec: OPENCODE_PLUGIN_REL_PATH, [FAILPROOFAI_HOOK_MARKER]: true };
6515
+ },
6516
+ isFailproofaiHook(hook) {
6517
+ if (typeof hook === "string")
6518
+ return hook.includes("failproofai.mjs");
6519
+ if (Array.isArray(hook))
6520
+ return typeof hook[0] === "string" && hook[0].includes("failproofai.mjs");
6521
+ return false;
6522
+ },
6523
+ writeHookEntries(settings, binaryPath, scope) {
6524
+ const s = settings;
6525
+ const effectiveScope = scope ?? "project";
6526
+ const settingsPath = effectiveScope === "user" ? resolve6(homedir18(), ".config", "opencode", "opencode.json") : resolve6(process.cwd(), ".opencode", "opencode.json");
6527
+ const pluginPath = opencodePluginFilePath(settingsPath);
6528
+ mkdirSync8(dirname5(pluginPath), { recursive: true });
6529
+ writeFileSync6(pluginPath, buildOpenCodePluginShim(binaryPath, effectiveScope), "utf8");
6530
+ if (!Array.isArray(s.plugin))
6531
+ s.plugin = [];
6532
+ const desired = effectiveScope === "user" ? `file://${pluginPath}` : OPENCODE_PLUGIN_REL_PATH;
6533
+ const idx = s.plugin.findIndex((entry) => this.isFailproofaiHook(entry));
6534
+ if (idx >= 0) {
6535
+ s.plugin[idx] = desired;
6536
+ } else {
6537
+ s.plugin.push(desired);
6538
+ }
6539
+ },
6540
+ removeHooksFromFile(settingsPath) {
6541
+ let removed = 0;
6542
+ const settings = this.readSettings(settingsPath);
6543
+ if (Array.isArray(settings.plugin)) {
6544
+ const before = settings.plugin.length;
6545
+ settings.plugin = settings.plugin.filter((entry) => !this.isFailproofaiHook(entry));
6546
+ removed += before - settings.plugin.length;
6547
+ if (settings.plugin.length === 0)
6548
+ delete settings.plugin;
6549
+ }
6550
+ this.writeSettings(settingsPath, settings);
6551
+ const pluginPath = opencodePluginFilePath(settingsPath);
6552
+ if (existsSync11(pluginPath)) {
6553
+ try {
6554
+ const content = readFileSync8(pluginPath, "utf8");
6555
+ if (content.includes(FAILPROOFAI_HOOK_MARKER)) {
6556
+ unlinkSync5(pluginPath);
6557
+ if (removed === 0)
6558
+ removed = 1;
6559
+ }
6560
+ } catch {}
6561
+ }
6562
+ return removed;
6563
+ },
6564
+ hooksInstalledInSettings(scope, cwd) {
6565
+ const settingsPath = this.getSettingsPath(scope, cwd);
6566
+ if (!existsSync11(settingsPath))
6567
+ return false;
6568
+ try {
6569
+ const settings = this.readSettings(settingsPath);
6570
+ if (!Array.isArray(settings.plugin))
6571
+ return false;
6572
+ const hasEntry = settings.plugin.some((entry) => this.isFailproofaiHook(entry));
6573
+ if (!hasEntry)
6574
+ return false;
6575
+ const pluginPath = opencodePluginFilePath(settingsPath);
6576
+ if (!existsSync11(pluginPath))
6577
+ return false;
6578
+ const content = readFileSync8(pluginPath, "utf8");
6579
+ return content.includes(FAILPROOFAI_HOOK_MARKER);
6580
+ } catch {
6581
+ return false;
6582
+ }
6583
+ },
6584
+ detectInstalled() {
6585
+ return binaryExists("opencode");
6586
+ }
6587
+ };
6588
+ pi = {
6589
+ id: "pi",
6590
+ displayName: "Pi",
6591
+ scopes: PI_HOOK_SCOPES,
6592
+ eventTypes: PI_HOOK_EVENT_TYPES,
6593
+ getSettingsPath(scope, cwd) {
6594
+ const base = cwd ? resolve6(cwd) : process.cwd();
6595
+ switch (scope) {
6596
+ case "user":
6597
+ return resolve6(homedir18(), ".pi", "agent", "settings.json");
6598
+ case "project":
6599
+ return resolve6(base, ".pi", "settings.json");
6600
+ case "local":
6601
+ return resolve6(base, ".pi", "settings.json");
6602
+ }
6603
+ },
6604
+ readSettings(settingsPath) {
6605
+ return readJsonFile(settingsPath);
6606
+ },
6607
+ writeSettings(settingsPath, settings) {
6608
+ writeJsonFile(settingsPath, settings);
6609
+ },
6610
+ buildHookEntry(_binaryPath, _eventType, scope) {
6611
+ return {
6612
+ [FAILPROOFAI_HOOK_MARKER]: true,
6613
+ _piPackagePath: getPiExtensionPath(),
6614
+ _piScope: scope
6615
+ };
6616
+ },
6617
+ isFailproofaiHook(hook) {
6618
+ if (typeof hook === "string")
6619
+ return isFailproofaiPiEntry(hook);
6620
+ if (!hook || typeof hook !== "object")
6621
+ return false;
6622
+ const h = hook;
6623
+ if (h[FAILPROOFAI_HOOK_MARKER] === true)
6624
+ return true;
6625
+ if (typeof h.source === "string")
6626
+ return isFailproofaiPiEntry(h.source);
6627
+ return false;
6628
+ },
6629
+ writeHookEntries(settings, _binaryPath, scope) {
6630
+ const s = settings;
6631
+ if (!Array.isArray(s.packages))
6632
+ s.packages = [];
6633
+ const extPath = getPiExtensionPath();
6634
+ const entry = scope === "project" ? makePiProjectRelativeEntry(extPath) : extPath;
6635
+ const idx = s.packages.findIndex((p) => isFailproofaiPiEntry(p));
6636
+ if (idx >= 0) {
6637
+ s.packages[idx] = entry;
6638
+ } else {
6639
+ s.packages.push(entry);
6640
+ }
6641
+ },
6642
+ removeHooksFromFile(settingsPath) {
6643
+ if (!existsSync11(settingsPath))
6644
+ return 0;
6645
+ const settings = this.readSettings(settingsPath);
6646
+ if (!Array.isArray(settings.packages))
6647
+ return 0;
6648
+ const before = settings.packages.length;
6649
+ settings.packages = settings.packages.filter((p) => !isFailproofaiPiEntry(p));
6650
+ const removed = before - settings.packages.length;
6651
+ if (settings.packages.length === 0)
6652
+ delete settings.packages;
6653
+ this.writeSettings(settingsPath, settings);
6654
+ return removed;
6655
+ },
6656
+ hooksInstalledInSettings(scope, cwd) {
6657
+ const settingsPath = this.getSettingsPath(scope, cwd);
6658
+ if (!existsSync11(settingsPath))
6659
+ return false;
6660
+ try {
6661
+ const settings = this.readSettings(settingsPath);
6662
+ if (!Array.isArray(settings.packages))
6663
+ return false;
6664
+ return settings.packages.some((p) => isFailproofaiPiEntry(p));
6665
+ } catch {
6666
+ return false;
6667
+ }
6668
+ },
6669
+ detectInstalled() {
6670
+ return binaryExists("pi");
6671
+ }
6672
+ };
6673
+ gemini = {
6674
+ id: "gemini",
6675
+ displayName: "Gemini CLI",
6676
+ scopes: GEMINI_HOOK_SCOPES,
6677
+ eventTypes: GEMINI_HOOK_EVENT_TYPES,
6678
+ getSettingsPath(scope, cwd) {
6679
+ const base = cwd ? resolve6(cwd) : process.cwd();
6680
+ switch (scope) {
6681
+ case "user":
6682
+ return resolve6(homedir18(), ".gemini", "settings.json");
6683
+ case "project":
6684
+ return resolve6(base, ".gemini", "settings.json");
6685
+ case "local":
6686
+ return resolve6(base, ".gemini", "settings.json");
6687
+ }
6688
+ },
6689
+ readSettings(settingsPath) {
6690
+ return readJsonFile(settingsPath);
6691
+ },
6692
+ writeSettings(settingsPath, settings) {
6693
+ writeJsonFile(settingsPath, settings);
6694
+ },
6695
+ buildHookEntry(binaryPath, eventType, scope) {
6696
+ const command = scope === "project" ? `npx -y failproofai --hook ${eventType} --cli gemini` : `"${binaryPath}" --hook ${eventType} --cli gemini`;
6697
+ return {
6698
+ type: "command",
6699
+ command,
6700
+ timeout: 60000,
6701
+ [FAILPROOFAI_HOOK_MARKER]: true
6702
+ };
6703
+ },
6704
+ isFailproofaiHook: isMarkedHook,
6705
+ writeHookEntries(settings, binaryPath, scope) {
6706
+ const s = settings;
6707
+ if (!s.hooks)
6708
+ s.hooks = {};
6709
+ for (const eventType of GEMINI_HOOK_EVENT_TYPES) {
6710
+ const hookEntry = this.buildHookEntry(binaryPath, eventType, scope);
6711
+ if (!s.hooks[eventType])
6712
+ s.hooks[eventType] = [];
6713
+ const matchers = s.hooks[eventType];
4748
6714
  let found = false;
4749
6715
  for (const matcher of matchers) {
4750
6716
  if (!matcher.hooks)
@@ -4757,7 +6723,7 @@ var init_integrations = __esm(() => {
4757
6723
  }
4758
6724
  }
4759
6725
  if (!found)
4760
- matchers.push({ hooks: [hookEntry] });
6726
+ matchers.push({ matcher: "*", hooks: [hookEntry] });
4761
6727
  }
4762
6728
  },
4763
6729
  removeHooksFromFile(settingsPath) {
@@ -4809,12 +6775,17 @@ var init_integrations = __esm(() => {
4809
6775
  return false;
4810
6776
  },
4811
6777
  detectInstalled() {
4812
- return binaryExists("codex");
6778
+ return binaryExists("gemini");
4813
6779
  }
4814
6780
  };
4815
6781
  INTEGRATIONS = {
4816
6782
  claude: claudeCode,
4817
- codex
6783
+ codex,
6784
+ copilot,
6785
+ cursor,
6786
+ opencode,
6787
+ pi,
6788
+ gemini
4818
6789
  };
4819
6790
  });
4820
6791
 
@@ -4838,7 +6809,7 @@ async function promptPolicySelection(preSelected, options = {}) {
4838
6809
  }));
4839
6810
  const total = items.length;
4840
6811
  const WINDOW_SIZE = 8;
4841
- let cursor = 0;
6812
+ let cursor2 = 0;
4842
6813
  let search = "";
4843
6814
  let lastLineCount = 0;
4844
6815
  let cursorHidden = false;
@@ -4920,8 +6891,8 @@ async function promptPolicySelection(preSelected, options = {}) {
4920
6891
  hideCursor();
4921
6892
  const filtered = getFiltered();
4922
6893
  const shown = filtered.length;
4923
- if (shown > 0 && cursor >= shown)
4924
- cursor = shown - 1;
6894
+ if (shown > 0 && cursor2 >= shown)
6895
+ cursor2 = shown - 1;
4925
6896
  const lines = [];
4926
6897
  lines.push(" Failproof AI — Policy Manager");
4927
6898
  lines.push("");
@@ -4944,7 +6915,7 @@ async function promptPolicySelection(preSelected, options = {}) {
4944
6915
  let cursorDisplayRow = 0;
4945
6916
  for (let i = 0;i < displayRows.length; i++) {
4946
6917
  const row = displayRows[i];
4947
- if (row.kind === "item" && row.filteredIndex === cursor) {
6918
+ if (row.kind === "item" && row.filteredIndex === cursor2) {
4948
6919
  cursorDisplayRow = i;
4949
6920
  break;
4950
6921
  }
@@ -4969,7 +6940,7 @@ async function promptPolicySelection(preSelected, options = {}) {
4969
6940
  lines.push(` \x1B[2m${prefix}${label}${"─".repeat(dashLen)}\x1B[0m`);
4970
6941
  } else {
4971
6942
  const item = row.item;
4972
- const isActive = row.filteredIndex === cursor;
6943
+ const isActive = row.filteredIndex === cursor2;
4973
6944
  const pointer = isActive ? "\x1B[36m❯\x1B[0m" : " ";
4974
6945
  const check = item.selected ? "\x1B[32m[✓]\x1B[0m" : "[ ]";
4975
6946
  const namePart = isActive ? `\x1B[1;36m${item.name}\x1B[0m` : item.name;
@@ -5021,22 +6992,22 @@ async function promptPolicySelection(preSelected, options = {}) {
5021
6992
  const filtered = getFiltered();
5022
6993
  if (key.name === "up") {
5023
6994
  if (filtered.length > 0) {
5024
- cursor = cursor > 0 ? cursor - 1 : filtered.length - 1;
6995
+ cursor2 = cursor2 > 0 ? cursor2 - 1 : filtered.length - 1;
5025
6996
  }
5026
6997
  render();
5027
6998
  } else if (key.name === "down") {
5028
6999
  if (filtered.length > 0) {
5029
- cursor = cursor < filtered.length - 1 ? cursor + 1 : 0;
7000
+ cursor2 = cursor2 < filtered.length - 1 ? cursor2 + 1 : 0;
5030
7001
  }
5031
7002
  render();
5032
7003
  } else if (key.name === "return" || key.name === "space") {
5033
- const item = filtered[cursor];
7004
+ const item = filtered[cursor2];
5034
7005
  if (item)
5035
7006
  item.selected = !item.selected;
5036
7007
  render();
5037
7008
  } else if (key.name === "escape") {
5038
7009
  search = "";
5039
- cursor = 0;
7010
+ cursor2 = 0;
5040
7011
  render();
5041
7012
  } else if (key.ctrl && key.name === "a") {
5042
7013
  const allSelected = filtered.length > 0 && filtered.every((i) => i.selected);
@@ -5052,12 +7023,12 @@ async function promptPolicySelection(preSelected, options = {}) {
5052
7023
  } else if (key.name === "backspace" || key.name === "delete") {
5053
7024
  if (search.length > 0) {
5054
7025
  search = search.slice(0, -1);
5055
- cursor = 0;
7026
+ cursor2 = 0;
5056
7027
  render();
5057
7028
  }
5058
7029
  } else if (_str && _str.length === 1 && !key.ctrl && !key.meta) {
5059
7030
  search += _str;
5060
- cursor = 0;
7031
+ cursor2 = 0;
5061
7032
  render();
5062
7033
  }
5063
7034
  }
@@ -5099,8 +7070,8 @@ __export(exports_manager, {
5099
7070
  });
5100
7071
  import { execSync as execSync4 } from "node:child_process";
5101
7072
  import { existsSync as existsSync12 } from "node:fs";
5102
- import { resolve as resolve7, basename as basename2 } from "node:path";
5103
- import { homedir as homedir15, platform, arch, release, hostname } from "node:os";
7073
+ import { resolve as resolve7, basename as basename3 } from "node:path";
7074
+ import { homedir as homedir19, platform, arch, release, hostname } from "node:os";
5104
7075
  function getSettingsPath(scope, cwd) {
5105
7076
  return claudeCode.getSettingsPath(scope, cwd);
5106
7077
  }
@@ -5518,7 +7489,7 @@ Failproof AI Hook Policies
5518
7489
  const base = cwd ? resolve7(cwd) : process.cwd();
5519
7490
  const conventionDirs = [
5520
7491
  { label: "Project", dir: resolve7(base, ".failproofai", "policies") },
5521
- { label: "User", dir: resolve7(homedir15(), ".failproofai", "policies") }
7492
+ { label: "User", dir: resolve7(homedir19(), ".failproofai", "policies") }
5522
7493
  ];
5523
7494
  for (const { label, dir } of conventionDirs) {
5524
7495
  const files = discoverPolicyFiles(dir);
@@ -5530,15 +7501,15 @@ Failproof AI Hook Policies
5530
7501
  try {
5531
7502
  const hooks = await loadCustomHooks(file);
5532
7503
  if (hooks.length === 0) {
5533
- const filename = basename2(file);
7504
+ const filename = basename3(file);
5534
7505
  console.log(` \x1B[31m✗\x1B[0m ${filename.padEnd(nameColWidth)}\x1B[31mfailed to load\x1B[0m`);
5535
7506
  } else {
5536
- const filename = basename2(file);
7507
+ const filename = basename3(file);
5537
7508
  const hookSummary = hooks.map((h) => h.name).join(", ");
5538
7509
  console.log(` \x1B[32m✓\x1B[0m ${filename.padEnd(nameColWidth)}${hooks.length} hook(s): ${hookSummary}`);
5539
7510
  }
5540
7511
  } catch {
5541
- const filename = basename2(file);
7512
+ const filename = basename3(file);
5542
7513
  console.log(` \x1B[31m✗\x1B[0m ${filename.padEnd(nameColWidth)}\x1B[31merror\x1B[0m`);
5543
7514
  }
5544
7515
  }
@@ -5566,34 +7537,40 @@ __export(exports_install_prompt, {
5566
7537
  promptPolicySelection: () => promptPolicySelection2
5567
7538
  });
5568
7539
  import * as readline2 from "node:readline";
5569
- async function resolveTargetClis(explicit) {
7540
+ async function resolveTargetClis(explicit, action = "install") {
5570
7541
  if (explicit && explicit.length > 0)
5571
7542
  return [...new Set(explicit)];
5572
7543
  const detected = detectInstalledClis();
5573
7544
  if (detected.length === 0) {
5574
- console.log("\x1B[33mWarning: no agent CLI binary found in PATH (claude, codex). " + "Defaulting to Claude Code; hooks will activate when an agent is installed.\x1B[0m");
7545
+ if (action === "uninstall") {
7546
+ console.log("\x1B[33mWarning: no agent CLI binary found in PATH (claude, codex, copilot, cursor-agent, opencode, pi, gemini). " + "Defaulting to Claude Code; nothing will be removed if no settings file exists.\x1B[0m");
7547
+ return ["claude"];
7548
+ }
7549
+ console.log("\x1B[33mWarning: no agent CLI binary found in PATH (claude, codex, copilot, cursor-agent, opencode, pi, gemini). " + "Defaulting to Claude Code; hooks will activate when an agent is installed.\x1B[0m");
5575
7550
  return ["claude"];
5576
7551
  }
5577
7552
  if (detected.length === 1) {
5578
7553
  const integration = getIntegration(detected[0]);
5579
- console.log(`Detected ${integration.displayName}; installing hooks for it.`);
7554
+ const verb = action === "uninstall" ? "removing hooks from" : "installing hooks for";
7555
+ console.log(`Detected ${integration.displayName}; ${verb} it.`);
5580
7556
  return detected;
5581
7557
  }
5582
7558
  if (!process.stdin.isTTY)
5583
7559
  return detected;
5584
- return promptCliTargetSelection(detected);
7560
+ return promptCliTargetSelection(detected, action);
5585
7561
  }
5586
- async function promptCliTargetSelection(detected) {
7562
+ async function promptCliTargetSelection(detected, action = "install") {
5587
7563
  const labels = detected.map((id) => getIntegration(id).displayName).join(" + ");
7564
+ const allLabel = detected.length > 2 ? "All" : "Both";
5588
7565
  const options = [
5589
- { label: "Both", description: labels, value: detected },
7566
+ { label: allLabel, description: labels, value: detected },
5590
7567
  ...detected.map((id) => ({
5591
7568
  label: `${getIntegration(id).displayName} only`,
5592
7569
  description: "",
5593
7570
  value: [id]
5594
7571
  }))
5595
7572
  ];
5596
- let cursor = 0;
7573
+ let cursor2 = 0;
5597
7574
  let lastLineCount = 0;
5598
7575
  let cursorHidden = false;
5599
7576
  function hideCursor() {
@@ -5630,17 +7607,19 @@ async function promptCliTargetSelection(detected) {
5630
7607
  }
5631
7608
  return result;
5632
7609
  }
7610
+ const heading = action === "uninstall" ? "Remove Hooks" : "Install Hooks";
7611
+ const verb = action === "uninstall" ? "remove from" : "install";
5633
7612
  function render() {
5634
7613
  const cols = process.stdout.columns || 120;
5635
7614
  hideCursor();
5636
7615
  const lines = [];
5637
- lines.push(" Failproof AI — Install Hooks");
7616
+ lines.push(` Failproof AI — ${heading}`);
5638
7617
  lines.push("");
5639
- lines.push(` \x1B[2mDetected ${labels}. Choose where to install:\x1B[0m`);
7618
+ lines.push(` \x1B[2mDetected ${labels}. Choose where to ${verb}:\x1B[0m`);
5640
7619
  lines.push("");
5641
7620
  for (let i = 0;i < options.length; i++) {
5642
7621
  const opt = options[i];
5643
- const isActive = i === cursor;
7622
+ const isActive = i === cursor2;
5644
7623
  const pointer = isActive ? "\x1B[36m❯\x1B[0m" : " ";
5645
7624
  const labelPart = isActive ? `\x1B[1;36m${opt.label}\x1B[0m` : opt.label;
5646
7625
  const pad = opt.description ? " ".repeat(Math.max(2, 22 - opt.label.length)) : "";
@@ -5683,16 +7662,16 @@ async function promptCliTargetSelection(detected) {
5683
7662
  process.exit(130);
5684
7663
  }
5685
7664
  if (key.name === "up") {
5686
- cursor = cursor > 0 ? cursor - 1 : options.length - 1;
7665
+ cursor2 = cursor2 > 0 ? cursor2 - 1 : options.length - 1;
5687
7666
  render();
5688
7667
  } else if (key.name === "down") {
5689
- cursor = cursor < options.length - 1 ? cursor + 1 : 0;
7668
+ cursor2 = cursor2 < options.length - 1 ? cursor2 + 1 : 0;
5690
7669
  render();
5691
7670
  } else if (key.name === "return" || key.name === "space") {
5692
7671
  cleanup();
5693
7672
  process.stdout.write(`
5694
7673
  `);
5695
- resolve8(options[cursor].value);
7674
+ resolve8(options[cursor2].value);
5696
7675
  }
5697
7676
  }
5698
7677
  process.stdin.on("keypress", onKey);
@@ -5716,7 +7695,7 @@ async function promptPolicySelection2(preSelected, options = {}) {
5716
7695
  }));
5717
7696
  const total = items.length;
5718
7697
  const WINDOW_SIZE = 8;
5719
- let cursor = 0;
7698
+ let cursor2 = 0;
5720
7699
  let search = "";
5721
7700
  let lastLineCount = 0;
5722
7701
  let cursorHidden = false;
@@ -5798,8 +7777,8 @@ async function promptPolicySelection2(preSelected, options = {}) {
5798
7777
  hideCursor();
5799
7778
  const filtered = getFiltered();
5800
7779
  const shown = filtered.length;
5801
- if (shown > 0 && cursor >= shown)
5802
- cursor = shown - 1;
7780
+ if (shown > 0 && cursor2 >= shown)
7781
+ cursor2 = shown - 1;
5803
7782
  const lines = [];
5804
7783
  lines.push(" Failproof AI — Policy Manager");
5805
7784
  lines.push("");
@@ -5822,7 +7801,7 @@ async function promptPolicySelection2(preSelected, options = {}) {
5822
7801
  let cursorDisplayRow = 0;
5823
7802
  for (let i = 0;i < displayRows.length; i++) {
5824
7803
  const row = displayRows[i];
5825
- if (row.kind === "item" && row.filteredIndex === cursor) {
7804
+ if (row.kind === "item" && row.filteredIndex === cursor2) {
5826
7805
  cursorDisplayRow = i;
5827
7806
  break;
5828
7807
  }
@@ -5847,7 +7826,7 @@ async function promptPolicySelection2(preSelected, options = {}) {
5847
7826
  lines.push(` \x1B[2m${prefix}${label}${"─".repeat(dashLen)}\x1B[0m`);
5848
7827
  } else {
5849
7828
  const item = row.item;
5850
- const isActive = row.filteredIndex === cursor;
7829
+ const isActive = row.filteredIndex === cursor2;
5851
7830
  const pointer = isActive ? "\x1B[36m❯\x1B[0m" : " ";
5852
7831
  const check = item.selected ? "\x1B[32m[✓]\x1B[0m" : "[ ]";
5853
7832
  const namePart = isActive ? `\x1B[1;36m${item.name}\x1B[0m` : item.name;
@@ -5899,22 +7878,22 @@ async function promptPolicySelection2(preSelected, options = {}) {
5899
7878
  const filtered = getFiltered();
5900
7879
  if (key.name === "up") {
5901
7880
  if (filtered.length > 0) {
5902
- cursor = cursor > 0 ? cursor - 1 : filtered.length - 1;
7881
+ cursor2 = cursor2 > 0 ? cursor2 - 1 : filtered.length - 1;
5903
7882
  }
5904
7883
  render();
5905
7884
  } else if (key.name === "down") {
5906
7885
  if (filtered.length > 0) {
5907
- cursor = cursor < filtered.length - 1 ? cursor + 1 : 0;
7886
+ cursor2 = cursor2 < filtered.length - 1 ? cursor2 + 1 : 0;
5908
7887
  }
5909
7888
  render();
5910
7889
  } else if (key.name === "return" || key.name === "space") {
5911
- const item = filtered[cursor];
7890
+ const item = filtered[cursor2];
5912
7891
  if (item)
5913
7892
  item.selected = !item.selected;
5914
7893
  render();
5915
7894
  } else if (key.name === "escape") {
5916
7895
  search = "";
5917
- cursor = 0;
7896
+ cursor2 = 0;
5918
7897
  render();
5919
7898
  } else if (key.ctrl && key.name === "a") {
5920
7899
  const allSelected = filtered.length > 0 && filtered.every((i) => i.selected);
@@ -5930,12 +7909,12 @@ async function promptPolicySelection2(preSelected, options = {}) {
5930
7909
  } else if (key.name === "backspace" || key.name === "delete") {
5931
7910
  if (search.length > 0) {
5932
7911
  search = search.slice(0, -1);
5933
- cursor = 0;
7912
+ cursor2 = 0;
5934
7913
  render();
5935
7914
  }
5936
7915
  } else if (_str && _str.length === 1 && !key.ctrl && !key.meta) {
5937
7916
  search += _str;
5938
- cursor = 0;
7917
+ cursor2 = 0;
5939
7918
  render();
5940
7919
  }
5941
7920
  }
@@ -6088,9 +8067,9 @@ __export(exports_pid, {
6088
8067
  isProcessAlive: () => isProcessAlive2,
6089
8068
  clearPid: () => clearPid2
6090
8069
  });
6091
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync7, existsSync as existsSync13, unlinkSync as unlinkSync5, mkdirSync as mkdirSync9 } from "node:fs";
6092
- import { join as join15, dirname as dirname6 } from "node:path";
6093
- import { homedir as homedir16 } from "node:os";
8070
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync7, existsSync as existsSync13, unlinkSync as unlinkSync6, mkdirSync as mkdirSync9 } from "node:fs";
8071
+ import { join as join19, dirname as dirname6 } from "node:path";
8072
+ import { homedir as homedir20 } from "node:os";
6094
8073
  function readPid2() {
6095
8074
  if (!existsSync13(PID_FILE2))
6096
8075
  return null;
@@ -6112,7 +8091,7 @@ function writePid2(pid) {
6112
8091
  }
6113
8092
  function clearPid2() {
6114
8093
  if (existsSync13(PID_FILE2))
6115
- unlinkSync5(PID_FILE2);
8094
+ unlinkSync6(PID_FILE2);
6116
8095
  }
6117
8096
  function isProcessAlive2(pid) {
6118
8097
  try {
@@ -6149,7 +8128,7 @@ function relayStatus() {
6149
8128
  }
6150
8129
  var PID_FILE2;
6151
8130
  var init_pid2 = __esm(() => {
6152
- PID_FILE2 = join15(homedir16(), ".failproofai", "relay.pid");
8131
+ PID_FILE2 = join19(homedir20(), ".failproofai", "relay.pid");
6153
8132
  });
6154
8133
 
6155
8134
  // scripts/parse-script-args.ts
@@ -6223,7 +8202,7 @@ __export(exports_launch, {
6223
8202
  import { spawn as spawn4 } from "child_process";
6224
8203
  import { realpathSync, existsSync as existsSync14 } from "node:fs";
6225
8204
  import { resolve as resolve9, dirname as dirname7 } from "node:path";
6226
- import { fileURLToPath } from "node:url";
8205
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
6227
8206
  function launch(mode) {
6228
8207
  const { claudeProjectsPath: parsedPath, loggingLevel, disableTelemetry, allowedDevOrigins, remainingArgs } = parseScriptArgs(process.argv.slice(2));
6229
8208
  console.log(`
@@ -6254,7 +8233,7 @@ function launch(mode) {
6254
8233
  process.env.PORT = port;
6255
8234
  process.env.HOSTNAME = "0.0.0.0";
6256
8235
  cmd = "node";
6257
- const packageRoot = process.env.FAILPROOFAI_PACKAGE_ROOT ?? resolve9(dirname7(realpathSync(fileURLToPath(import.meta.url))), "..");
8236
+ const packageRoot = process.env.FAILPROOFAI_PACKAGE_ROOT ?? resolve9(dirname7(realpathSync(fileURLToPath2(import.meta.url))), "..");
6258
8237
  const serverJsPath = resolve9(packageRoot, ".next/standalone/server.js");
6259
8238
  if (!existsSync14(serverJsPath)) {
6260
8239
  console.error(`
@@ -6316,16 +8295,16 @@ var init_cli_error2 = __esm(() => {
6316
8295
  // bin/failproofai.mjs
6317
8296
  import { realpathSync as realpathSync2 } from "fs";
6318
8297
  import { dirname as dirname8, resolve as resolve10 } from "path";
6319
- import { fileURLToPath as fileURLToPath2 } from "url";
8298
+ import { fileURLToPath as fileURLToPath3 } from "url";
6320
8299
  // package.json
6321
- var version = "0.0.9";
8300
+ var version = "0.0.10-beta.0";
6322
8301
 
6323
8302
  // bin/failproofai.mjs
6324
8303
  if (!process.env.FAILPROOFAI_PACKAGE_ROOT) {
6325
- process.env.FAILPROOFAI_PACKAGE_ROOT = resolve10(dirname8(realpathSync2(fileURLToPath2(import.meta.url))), "..");
8304
+ process.env.FAILPROOFAI_PACKAGE_ROOT = resolve10(dirname8(realpathSync2(fileURLToPath3(import.meta.url))), "..");
6326
8305
  }
6327
8306
  if (!process.env.FAILPROOFAI_DIST_PATH) {
6328
- process.env.FAILPROOFAI_DIST_PATH = resolve10(dirname8(realpathSync2(fileURLToPath2(import.meta.url))), "..", "dist");
8307
+ process.env.FAILPROOFAI_DIST_PATH = resolve10(dirname8(realpathSync2(fileURLToPath3(import.meta.url))), "..", "dist");
6329
8308
  }
6330
8309
  var args = process.argv.slice(2);
6331
8310
  if (args[0] === "p")
@@ -6334,13 +8313,13 @@ var hookIdx = args.indexOf("--hook");
6334
8313
  if (hookIdx >= 0) {
6335
8314
  if (!args[hookIdx + 1]) {
6336
8315
  console.error("Error: Missing event type after --hook");
6337
- console.error("Usage: failproofai --hook <event> [--cli <claude|codex>]");
8316
+ console.error("Usage: failproofai --hook <event> [--cli <claude|codex|copilot|cursor|opencode|pi|gemini>]");
6338
8317
  process.exit(1);
6339
8318
  }
6340
8319
  const eventType = args[hookIdx + 1];
6341
8320
  const cliIdx = args.indexOf("--cli");
6342
8321
  const cliArg = cliIdx >= 0 ? args[cliIdx + 1] : undefined;
6343
- const cli = cliArg && (cliArg === "claude" || cliArg === "codex") ? cliArg : "claude";
8322
+ const cli = cliArg && (cliArg === "claude" || cliArg === "codex" || cliArg === "copilot" || cliArg === "cursor" || cliArg === "opencode" || cliArg === "pi" || cliArg === "gemini") ? cliArg : "claude";
6344
8323
  try {
6345
8324
  const { handleHookEvent: handleHookEvent2 } = await Promise.resolve().then(() => (init_handler(), exports_handler));
6346
8325
  const exitCode = await handleHookEvent2(eventType, cli);
@@ -6382,17 +8361,19 @@ COMMANDS
6382
8361
  policies, p List all available policies and their status
6383
8362
  policies --install, -i Enable policies in agent CLI settings
6384
8363
  [names...] Specific policy names to enable
6385
- --cli claude|codex Agent CLI(s) to install for; space-separated
6386
- (e.g. --cli claude codex) or repeated.
8364
+ --cli claude|codex|copilot|cursor|opencode|pi|gemini
8365
+ Agent CLI(s) to install for; space-separated
8366
+ (e.g. --cli claude codex copilot cursor opencode pi gemini) or repeated.
6387
8367
  Default: detect installed CLIs and prompt.
6388
8368
  --scope user|project|local Config scope to write to (default: user)
6389
- (Codex supports user|project only)
8369
+ (Codex / Copilot / Cursor / OpenCode / Pi / Gemini support user|project only)
6390
8370
  --beta Include beta policies
6391
8371
  --custom, -c <path> Path to a JS file of custom policies
6392
8372
 
6393
8373
  policies --uninstall, -u Disable policies or remove hooks
6394
8374
  [names...] Specific policy names to disable
6395
- --cli claude|codex Agent CLI(s) to uninstall from
8375
+ --cli claude|codex|copilot|cursor|opencode|pi|gemini
8376
+ Agent CLI(s) to uninstall from
6396
8377
  --scope user|project|local|all Config scope to remove from (default: user)
6397
8378
  --beta Remove only beta policies
6398
8379
  --custom, -c Clear the customPoliciesPath from config
@@ -6418,11 +8399,21 @@ EXAMPLES
6418
8399
  failproofai policies --install
6419
8400
  failproofai policies --install block-sudo sanitize-api-keys --scope project
6420
8401
  failproofai policies --install --cli codex --scope project
6421
- failproofai policies --install --cli claude codex
8402
+ failproofai policies --install --cli copilot --scope project
8403
+ failproofai policies --install --cli cursor --scope project
8404
+ failproofai policies --install --cli opencode --scope project
8405
+ failproofai policies --install --cli pi --scope project
8406
+ failproofai policies --install --cli gemini --scope project
8407
+ failproofai policies --install --cli claude codex copilot cursor opencode pi gemini
6422
8408
  failproofai policies --install --custom ./my-policies.js
6423
8409
  failproofai policies -i -c ./my-policies.js
6424
8410
  failproofai policies --uninstall block-sudo
6425
8411
  failproofai policies --uninstall --cli codex
8412
+ failproofai policies --uninstall --cli copilot
8413
+ failproofai policies --uninstall --cli cursor
8414
+ failproofai policies --uninstall --cli opencode
8415
+ failproofai policies --uninstall --cli pi
8416
+ failproofai policies --uninstall --cli gemini
6426
8417
  failproofai policies --uninstall --custom
6427
8418
 
6428
8419
  LINKS
@@ -6457,19 +8448,21 @@ USAGE
6457
8448
 
6458
8449
  OPTIONS (install)
6459
8450
  [names...] Specific policy names to enable (omit for interactive)
6460
- --cli claude|codex Agent CLI(s) to install for; space-separated
6461
- (e.g. --cli claude codex) or repeated. Omit to
6462
- detect installed CLIs and prompt (or auto-pick
6463
- if only one is found).
8451
+ --cli claude|codex|copilot|cursor|opencode|pi|gemini
8452
+ Agent CLI(s) to install for; space-separated
8453
+ (e.g. --cli claude codex copilot cursor opencode pi gemini) or repeated.
8454
+ Omit to detect installed CLIs and prompt (or
8455
+ auto-pick if only one is found).
6464
8456
  --scope user|project|local Config scope to write to (default: user)
6465
- (Codex supports user|project only)
8457
+ (Codex / Copilot / Cursor / OpenCode / Pi / Gemini support user|project only)
6466
8458
  --beta Include beta policies
6467
8459
  --custom, -c <path> Path to a JS file of custom policies
6468
8460
  (skips interactive prompt; validates file first)
6469
8461
 
6470
8462
  OPTIONS (uninstall)
6471
8463
  [names...] Specific policy names to disable (omit to remove hooks)
6472
- --cli claude|codex Agent CLI(s) to uninstall from
8464
+ --cli claude|codex|copilot|cursor|opencode|pi|gemini
8465
+ Agent CLI(s) to uninstall from
6473
8466
  --scope user|project|local|all Config scope to remove from (default: user)
6474
8467
  --beta Remove only beta policies
6475
8468
  --custom, -c Clear the customPoliciesPath from config
@@ -6479,11 +8472,20 @@ EXAMPLES
6479
8472
  failproofai policies --install
6480
8473
  failproofai policies --install block-sudo sanitize-api-keys
6481
8474
  failproofai policies --install --cli codex --scope project
6482
- failproofai policies --install --cli claude codex
8475
+ failproofai policies --install --cli copilot --scope project
8476
+ failproofai policies --install --cli cursor --scope project
8477
+ failproofai policies --install --cli opencode --scope project
8478
+ failproofai policies --install --cli pi --scope project
8479
+ failproofai policies --install --cli gemini --scope project
8480
+ failproofai policies --install --cli claude codex copilot cursor opencode pi gemini
6483
8481
  failproofai policies --install --custom ./my-policies.js
6484
8482
  failproofai policies -i -c ./my-policies.js
6485
8483
  failproofai policies --uninstall block-sudo
6486
8484
  failproofai policies --uninstall --cli codex
8485
+ failproofai policies --uninstall --cli copilot
8486
+ failproofai policies --uninstall --cli cursor
8487
+ failproofai policies --uninstall --cli opencode
8488
+ failproofai policies --uninstall --cli pi
6487
8489
  failproofai policies -u
6488
8490
  failproofai policies --uninstall --custom
6489
8491
  `.trimStart());
@@ -6506,7 +8508,7 @@ EXAMPLES
6506
8508
  throw new CliError3(`Missing path after --custom/-c
6507
8509
  Usage: --custom <path> (e.g. --custom ./my-policies.js)`);
6508
8510
  }
6509
- const VALID_CLIS = new Set(["claude", "codex"]);
8511
+ const VALID_CLIS = new Set(["claude", "codex", "copilot", "cursor", "opencode", "pi", "gemini"]);
6510
8512
  const cliFlagValues = [];
6511
8513
  const cliConsumedIdxs = new Set;
6512
8514
  const cliFlagIdxs = subArgs.map((a, i) => a === "--cli" ? i : -1).filter((i) => i >= 0);
@@ -6523,7 +8525,7 @@ Usage: --custom <path> (e.g. --custom ./my-policies.js)`);
6523
8525
  consumed++;
6524
8526
  }
6525
8527
  if (consumed === 0) {
6526
- throw new CliError3("Missing value(s) for --cli. Usage: --cli claude codex (or any subset)");
8528
+ throw new CliError3("Missing value(s) for --cli. Usage: --cli claude codex copilot cursor opencode pi gemini (or any subset)");
6527
8529
  }
6528
8530
  }
6529
8531
  const includeBeta = subArgs.includes("--beta");
@@ -6542,7 +8544,7 @@ Run \`failproofai policies --help\` for usage.`);
6542
8544
  }
6543
8545
  const explicitPolicyNames = subArgs.filter((a, idx) => !a.startsWith("-") && !consumedIdxs.has(idx));
6544
8546
  const policyNames = explicitPolicyNames.length > 0 ? explicitPolicyNames : customPoliciesPath !== undefined ? [] : undefined;
6545
- const cli = await resolveTargetClis2(cliFlagValues.length > 0 ? cliFlagValues : undefined);
8547
+ const cli = await resolveTargetClis2(cliFlagValues.length > 0 ? cliFlagValues : undefined, "install");
6546
8548
  await installHooks2(policyNames, scope, undefined, includeBeta, undefined, customPoliciesPath, false, cli);
6547
8549
  process.exit(0);
6548
8550
  }
@@ -6557,7 +8559,7 @@ Run \`failproofai policies --help\` for usage.`);
6557
8559
  if (scopeIdx >= 0 && !["user", "project", "local", "all"].includes(scope)) {
6558
8560
  throw new CliError3(`Invalid scope: ${scope}. Valid values: user, project, local, all`);
6559
8561
  }
6560
- const VALID_CLIS = new Set(["claude", "codex"]);
8562
+ const VALID_CLIS = new Set(["claude", "codex", "copilot", "cursor", "opencode", "pi", "gemini"]);
6561
8563
  const cliFlagValues = [];
6562
8564
  const cliConsumedIdxs = new Set;
6563
8565
  const cliFlagIdxs = subArgs.map((a, i) => a === "--cli" ? i : -1).filter((i) => i >= 0);
@@ -6574,7 +8576,7 @@ Run \`failproofai policies --help\` for usage.`);
6574
8576
  consumed++;
6575
8577
  }
6576
8578
  if (consumed === 0) {
6577
- throw new CliError3("Missing value(s) for --cli. Usage: --cli claude codex (or any subset)");
8579
+ throw new CliError3("Missing value(s) for --cli. Usage: --cli claude codex copilot cursor opencode pi gemini (or any subset)");
6578
8580
  }
6579
8581
  }
6580
8582
  const betaOnly = subArgs.includes("--beta");
@@ -6591,7 +8593,7 @@ Run \`failproofai policies --help\` for usage.`);
6591
8593
  Run \`failproofai policies --help\` for usage.`);
6592
8594
  }
6593
8595
  const policyNames = subArgs.filter((a, idx) => !a.startsWith("-") && !consumedIdxs.has(idx));
6594
- const cli = await resolveTargetClis2(cliFlagValues.length > 0 ? cliFlagValues : undefined);
8596
+ const cli = await resolveTargetClis2(cliFlagValues.length > 0 ? cliFlagValues : undefined, "uninstall");
6595
8597
  await removeHooks2(policyNames.length > 0 ? policyNames : undefined, scope, undefined, { betaOnly, removeCustomHooks, cli });
6596
8598
  process.exit(0);
6597
8599
  }