failproofai 1.0.0-beta.2 → 1.0.0-beta.21

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 (381) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/app-path-routes-manifest.json +2 -1
  3. package/.next/standalone/.next/build-manifest.json +6 -6
  4. package/.next/standalone/.next/prerender-manifest.json +3 -3
  5. package/.next/standalone/.next/required-server-files.json +1 -1
  6. package/.next/standalone/.next/routes-manifest.json +6 -0
  7. package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +3 -3
  8. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  9. package/.next/standalone/.next/server/app/_global-error/page.js +3 -1
  10. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  11. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  12. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  13. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  14. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
  15. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  16. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
  17. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
  18. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  19. package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +3 -3
  20. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  21. package/.next/standalone/.next/server/app/_not-found/page.js +3 -1
  22. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  23. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  24. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  25. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  26. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  27. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  28. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
  29. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  30. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  31. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  32. package/.next/standalone/.next/server/app/api/audit/invite/route.js +3 -2
  33. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/audit/run/route.js +4 -3
  35. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/api/audit/status/route.js +1 -1
  37. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/api/auth/login-request/route.js +3 -1
  39. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js +3 -2
  41. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/api/auth/logout/route.js +3 -2
  43. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/api/auth/reminder/route.js +3 -2
  45. package/.next/standalone/.next/server/app/api/auth/reminder/route.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/api/auth/status/route.js +3 -2
  47. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js +1 -1
  49. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  50. package/.next/standalone/.next/server/app/audit/page/build-manifest.json +3 -3
  51. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  52. package/.next/standalone/.next/server/app/audit/page.js +5 -3
  53. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  54. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  55. package/.next/standalone/.next/server/app/index.html +1 -1
  56. package/.next/standalone/.next/server/app/index.rsc +15 -15
  57. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  58. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  59. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
  60. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +10 -10
  61. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  62. package/.next/standalone/.next/server/app/page/build-manifest.json +3 -3
  63. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  64. package/.next/standalone/.next/server/app/page.js +3 -1
  65. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  66. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  67. package/.next/standalone/.next/server/app/policies/page/build-manifest.json +3 -3
  68. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +30 -18
  69. package/.next/standalone/.next/server/app/policies/page.js +5 -3
  70. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  71. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  72. package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +3 -3
  73. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  74. package/.next/standalone/.next/server/app/project/[name]/page.js +4 -2
  75. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  76. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  77. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +3 -3
  78. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  79. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  80. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +6 -4
  81. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  82. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  83. package/.next/standalone/.next/server/app/projects/page/build-manifest.json +3 -3
  84. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  85. package/.next/standalone/.next/server/app/projects/page.js +4 -2
  86. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  87. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  88. package/.next/standalone/.next/server/app/settings/page/app-paths-manifest.json +3 -0
  89. package/.next/standalone/.next/server/app/settings/page/build-manifest.json +18 -0
  90. package/.next/standalone/.next/server/app/settings/page/next-font-manifest.json +6 -0
  91. package/.next/standalone/.next/server/app/settings/page/react-loadable-manifest.json +1 -0
  92. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +53 -0
  93. package/.next/standalone/.next/server/app/settings/page.js +18 -0
  94. package/.next/standalone/.next/server/app/settings/page.js.map +5 -0
  95. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -0
  96. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +3 -0
  97. package/.next/standalone/.next/server/app-paths-manifest.json +2 -1
  98. package/.next/standalone/.next/server/chunks/[externals]__0h9_83k._.js +3 -0
  99. package/.next/standalone/.next/server/chunks/{[externals]__1nl3dvw._.js → [externals]__0r-zamh._.js} +2 -2
  100. package/.next/standalone/.next/server/chunks/[externals]__1ifpkx-._.js +3 -0
  101. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0l4vuqm._.js → [root-of-the-server]__00ni9za._.js} +2 -2
  102. package/.next/standalone/.next/server/chunks/[root-of-the-server]__06hexd0._.js +3 -0
  103. package/.next/standalone/.next/server/chunks/[root-of-the-server]__08y94xv._.js +3 -0
  104. package/.next/standalone/.next/server/chunks/[root-of-the-server]__094p1f1._.js +3 -0
  105. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_0xu5z._.js +1 -1
  106. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ed01_d._.js +3 -0
  107. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0-dh5jy._.js → [root-of-the-server]__0effm9m._.js} +2 -2
  108. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0g0vcwt._.js +3 -0
  109. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__18d-9oi._.js → [root-of-the-server]__0ix8gm7._.js} +2 -2
  110. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0h3lfmp._.js → [root-of-the-server]__0ji11a-._.js} +2 -2
  111. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__10oa-v9._.js → [root-of-the-server]__0ojwjxl._.js} +2 -2
  112. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__16le-kd._.js → [root-of-the-server]__0pbp9fe._.js} +2 -2
  113. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q-v9z2._.js +1 -1
  114. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0vb5fl2._.js +3 -0
  115. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0sb_5m8._.js → [root-of-the-server]__131ebum._.js} +4 -4
  116. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1ibjy5x._.js +3 -0
  117. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1tvn4wf._.js +1 -1
  118. package/.next/standalone/.next/server/chunks/[root-of-the-server]__20d06rh._.js +3 -0
  119. package/.next/standalone/.next/server/chunks/_0lxbzdq._.js +3 -0
  120. package/.next/standalone/.next/server/chunks/_1zuiiy3._.js +3 -0
  121. package/.next/standalone/.next/server/chunks/lib_telemetry_ts_0dctyyw._.js +3 -0
  122. package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_17k9e3w.js +5 -5
  123. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_01r25oi._.js +2 -1
  124. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +2 -1
  125. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  126. package/.next/standalone/.next/server/chunks/ssr/[externals]__1aq139v._.js +3 -0
  127. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0-wfhrp._.js +3 -0
  128. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__06bp_fg._.js +5 -0
  129. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__074qmkp._.js +3 -0
  130. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1apb7k_._.js → [root-of-the-server]__0_1tiya._.js} +2 -2
  131. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__170799-._.js → [root-of-the-server]__0spkm68._.js} +11 -11
  132. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0t281d4._.js +3 -0
  133. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__17itmx-._.js +5 -0
  134. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__01od0m6._.js → [root-of-the-server]__1n2vcv4._.js} +2 -2
  135. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1qapotl._.js +1 -1
  136. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1rv-sc9._.js +3 -0
  137. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1tlvv9a._.js +3 -0
  138. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1u-wx42._.js +4 -0
  139. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1v185mo._.js +3 -0
  140. package/.next/standalone/.next/server/chunks/ssr/_0-yi74u._.js +3 -0
  141. package/.next/standalone/.next/server/chunks/ssr/_0rjgfln._.js +3 -0
  142. package/.next/standalone/.next/server/chunks/ssr/_0vo2qyt._.js +3 -0
  143. package/.next/standalone/.next/server/chunks/ssr/_0xej-0e._.js +3 -0
  144. package/.next/standalone/.next/server/chunks/ssr/_10399ge._.js +3 -0
  145. package/.next/standalone/.next/server/chunks/ssr/_11_p9y8._.js +1 -1
  146. package/.next/standalone/.next/server/chunks/ssr/_149k9oy._.js +3 -0
  147. package/.next/standalone/.next/server/chunks/ssr/_1_hwp06._.js +3 -0
  148. package/.next/standalone/.next/server/chunks/ssr/_1ctse6u._.js +3 -0
  149. package/.next/standalone/.next/server/chunks/ssr/_1e19atl._.js +1 -1
  150. package/.next/standalone/.next/server/chunks/ssr/_1mnhi_r._.js +3 -0
  151. package/.next/standalone/.next/server/chunks/ssr/_1ror7k9._.js +3 -0
  152. package/.next/standalone/.next/server/chunks/ssr/_1sdgprl._.js +5 -0
  153. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +2 -2
  154. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  155. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
  156. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0dwo94s.js +4 -0
  157. package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js +2 -1
  158. package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +3 -0
  159. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589g._.js +4 -0
  160. package/.next/standalone/.next/server/middleware-build-manifest.js +6 -6
  161. package/.next/standalone/.next/server/middleware.js +2 -2
  162. package/.next/standalone/.next/server/pages/404.html +1 -1
  163. package/.next/standalone/.next/server/pages/500.html +1 -1
  164. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  165. package/.next/standalone/.next/server/server-reference-manifest.json +74 -20
  166. package/.next/standalone/.next/static/chunks/{03fmihek9n986.js → 06zgzyh3wv1ig.js} +1 -1
  167. package/.next/standalone/.next/static/chunks/0km-jn9o5xyqs.css +2 -0
  168. package/.next/standalone/.next/static/chunks/{1hdhteui932bq.js → 0qmo2t50kpb30.js} +1 -1
  169. package/.next/standalone/.next/static/chunks/{2sbelanzvxvv0.js → 113g8wtal6_c4.js} +22 -22
  170. package/.next/standalone/.next/static/chunks/16p68cybetr31.js +1 -0
  171. package/.next/standalone/.next/static/chunks/18wqn0_3mzylf.js +1 -0
  172. package/.next/standalone/.next/static/chunks/1v1gqvpjk62jm.js +1 -0
  173. package/.next/standalone/.next/static/chunks/{0fi645si-uiiq.js → 1xq1m8h14tnlj.js} +1 -1
  174. package/.next/standalone/.next/static/chunks/{3hp8ulqknybil.js → 1y3vwogqb88dx.js} +1 -1
  175. package/.next/standalone/.next/static/chunks/{3vs4ay36tjds_.js → 1zmh-b0ulrqu7.js} +1 -1
  176. package/.next/standalone/.next/static/chunks/23kev26by1e4f.js +1 -0
  177. package/.next/standalone/.next/static/chunks/299lhw60e8gr6.js +1 -0
  178. package/.next/standalone/.next/static/chunks/{18rvdw5v72364.js → 2adbpdaxp5ygq.js} +1 -1
  179. package/.next/standalone/.next/static/chunks/{2j94uwkb4s3qr.js → 2kbqcw-6swmav.js} +1 -1
  180. package/.next/standalone/.next/static/chunks/2n9n5m0963pdg.js +6 -0
  181. package/.next/standalone/.next/static/chunks/2uvhw57mf9lg_.css +2 -0
  182. package/.next/standalone/.next/static/chunks/{31feojxhjyjpl.js → 30nrrpwymht5m.js} +1 -1
  183. package/.next/standalone/.next/static/chunks/31coz11ia5vc4.js +1 -0
  184. package/.next/standalone/.next/static/chunks/{turbopack-1lsv92x7klwrn.js → turbopack-3diub4ctvn1n0.js} +1 -1
  185. package/.next/standalone/SECURITY.md +8 -2
  186. package/.next/standalone/app/actions/get-active-pauses.ts +16 -0
  187. package/.next/standalone/app/actions/get-hooks-config.ts +2 -2
  188. package/.next/standalone/app/actions/get-scheduled-audit.ts +70 -0
  189. package/.next/standalone/app/actions/update-scheduled-audit.ts +44 -0
  190. package/.next/standalone/app/api/audit/run/route.ts +28 -0
  191. package/.next/standalone/app/api/audit/status/route.ts +13 -1
  192. package/.next/standalone/app/api/auth/status/route.ts +1 -1
  193. package/.next/standalone/app/audit/_components/audit-dashboard.tsx +1 -3
  194. package/.next/standalone/app/audit/_components/audit-progress-strip.tsx +1 -1
  195. package/.next/standalone/app/audit/_components/auth-dialog.css +159 -0
  196. package/.next/standalone/app/audit/_components/auth-dialog.tsx +3 -0
  197. package/.next/standalone/app/audit/_components/come-back-better-section.tsx +1 -1
  198. package/.next/standalone/app/audit/_components/empty-state.tsx +0 -1
  199. package/.next/standalone/app/audit/_components/how-to-improve-section.tsx +1 -1
  200. package/.next/standalone/app/audit/_components/quirks-section.tsx +0 -1
  201. package/.next/standalone/app/audit/_components/report-footer.tsx +0 -2
  202. package/.next/standalone/app/audit/_components/run-progress.tsx +1 -1
  203. package/.next/standalone/app/audit/_components/strengths-section.tsx +0 -1
  204. package/.next/standalone/app/audit/audit-styles.css +6 -149
  205. package/.next/standalone/app/components/pause-notices.tsx +111 -0
  206. package/.next/standalone/app/components/raw-log-viewer.tsx +1 -1
  207. package/.next/standalone/app/components/toast.tsx +1 -1
  208. package/.next/standalone/app/policies/hooks-client.tsx +84 -8
  209. package/.next/standalone/app/settings/page.tsx +31 -0
  210. package/.next/standalone/app/settings/settings-client.tsx +487 -0
  211. package/.next/standalone/components/navbar.tsx +2 -0
  212. package/.next/standalone/lib/auth/api-server-client.ts +2 -3
  213. package/.next/standalone/lib/auth/auth-store.ts +4 -5
  214. package/.next/standalone/lib/codex-sessions.ts +2 -1
  215. package/.next/standalone/lib/dashboard-host.ts +101 -0
  216. package/.next/standalone/lib/install-check.ts +22 -7
  217. package/.next/standalone/lib/pi-sessions.ts +99 -14
  218. package/.next/standalone/lib/telemetry-enabled.ts +54 -0
  219. package/.next/standalone/lib/telemetry-id.ts +52 -1
  220. package/.next/standalone/lib/telemetry.ts +8 -4
  221. package/.next/standalone/node_modules/react/cjs/react.development.js +1 -1
  222. package/.next/standalone/node_modules/react/cjs/react.production.js +1 -1
  223. package/.next/standalone/node_modules/react/package.json +3 -3
  224. package/.next/standalone/node_modules/react-dom/cjs/react-dom-server-legacy.browser.production.js +1 -1
  225. package/.next/standalone/node_modules/react-dom/cjs/react-dom-server-legacy.node.production.js +1 -1
  226. package/.next/standalone/node_modules/react-dom/cjs/react-dom-server.browser.production.js +3 -3
  227. package/.next/standalone/node_modules/react-dom/cjs/react-dom-server.edge.production.js +3 -3
  228. package/.next/standalone/node_modules/react-dom/cjs/react-dom-server.node.production.js +3 -3
  229. package/.next/standalone/node_modules/react-dom/cjs/react-dom.production.js +1 -1
  230. package/.next/standalone/node_modules/react-dom/package.json +4 -4
  231. package/.next/standalone/package.json +23 -12
  232. package/.next/standalone/proxy.ts +111 -1
  233. package/.next/standalone/server.js +1 -1
  234. package/bin/failproofai-worker.mjs +1 -1
  235. package/bin/failproofai.mjs +1041 -81
  236. package/bin/failproofaid-shim.mjs +4 -4
  237. package/dist/cli.mjs +18513 -10474
  238. package/dist/worker.mjs +1223 -299
  239. package/lib/auth/api-server-client.ts +2 -3
  240. package/lib/auth/auth-store.ts +4 -5
  241. package/lib/codex-sessions.ts +2 -1
  242. package/lib/dashboard-host.ts +101 -0
  243. package/lib/install-check.ts +22 -7
  244. package/lib/pi-sessions.ts +99 -14
  245. package/lib/telemetry-enabled.ts +54 -0
  246. package/lib/telemetry-id.ts +52 -1
  247. package/lib/telemetry.ts +8 -4
  248. package/package.json +23 -12
  249. package/scripts/build-daemon-packages.mjs +247 -0
  250. package/scripts/daemon-platforms.mjs +42 -0
  251. package/scripts/dev-hook.mjs +46 -5
  252. package/scripts/install-telemetry.mjs +8 -1
  253. package/scripts/launch.ts +25 -3
  254. package/scripts/parse-script-args.ts +27 -1
  255. package/scripts/prune-standalone.mjs +26 -0
  256. package/scripts/publish-aliases.mjs +62 -41
  257. package/scripts/repro-npm-install.sh +155 -0
  258. package/scripts/translate-docs/cache.ts +1 -1
  259. package/scripts/translate-docs/cli.ts +2 -2
  260. package/scripts/translate-docs/mintlify-nav.ts +1 -1
  261. package/scripts/translate-docs/readme-translator.ts +163 -4
  262. package/scripts/validate-mdx.ts +153 -13
  263. package/src/audit/audit-lock.ts +285 -0
  264. package/src/audit/audit-schedule.ts +111 -0
  265. package/src/audit/cache.ts +18 -5
  266. package/src/audit/cli-adapters/goose.ts +2 -1
  267. package/src/audit/cli-adapters/hermes.ts +28 -10
  268. package/src/audit/cli.ts +191 -35
  269. package/src/audit/dashboard-cache.ts +2 -3
  270. package/src/audit/report.ts +0 -1
  271. package/src/hooks/backfill-cli.ts +247 -0
  272. package/src/hooks/builtin-policies.ts +391 -3
  273. package/src/hooks/cloud-connection.ts +354 -0
  274. package/src/hooks/cloud-enrollment-cli.ts +439 -0
  275. package/src/hooks/cloud-enrollment.ts +284 -0
  276. package/src/hooks/cloud-introspect.ts +147 -0
  277. package/src/hooks/cloud-managed-policies.ts +206 -0
  278. package/src/hooks/collector-config.ts +218 -0
  279. package/src/hooks/configure-wizard.ts +1211 -233
  280. package/src/hooks/custom-hooks-loader.ts +243 -37
  281. package/src/hooks/daemon-client.ts +136 -33
  282. package/src/hooks/daemon-download.ts +264 -31
  283. package/src/hooks/daemon-service.ts +1124 -76
  284. package/src/hooks/first-run-gate.ts +50 -0
  285. package/src/hooks/flush-cli.ts +180 -0
  286. package/src/hooks/fp-config.ts +835 -0
  287. package/src/hooks/fp-home.ts +714 -0
  288. package/src/hooks/fp-reset.ts +1363 -0
  289. package/src/hooks/handler.ts +185 -6
  290. package/src/hooks/harness-cli.ts +386 -0
  291. package/src/hooks/hook-activity-store.ts +171 -24
  292. package/src/hooks/hook-logger.ts +2 -2
  293. package/src/hooks/hook-telemetry.ts +5 -1
  294. package/src/hooks/hooks-config.ts +4 -3
  295. package/src/hooks/integrations.ts +0 -1
  296. package/src/hooks/loader-utils.ts +172 -9
  297. package/src/hooks/manager.ts +41 -5
  298. package/src/hooks/migrations.ts +450 -0
  299. package/src/hooks/onboarding-attempt.ts +195 -0
  300. package/src/hooks/onboarding-lock.ts +118 -0
  301. package/src/hooks/policy-types.ts +28 -0
  302. package/src/hooks/session-pause-cli.ts +159 -0
  303. package/src/hooks/session-pause.ts +267 -0
  304. package/src/hooks/setup-state.ts +256 -0
  305. package/src/hooks/tui.ts +209 -9
  306. package/src/hooks/uninstall-cli.ts +418 -0
  307. package/src/hooks/worker-server.ts +134 -15
  308. package/.next/standalone/.next/server/chunks/[externals]__14odj07._.js +0 -3
  309. package/.next/standalone/.next/server/chunks/[externals]__1s61mel._.js +0 -3
  310. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0id_xf7._.js +0 -3
  311. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0longh-._.js +0 -3
  312. package/.next/standalone/.next/server/chunks/[root-of-the-server]__17g9wh7._.js +0 -3
  313. package/.next/standalone/.next/server/chunks/[root-of-the-server]__19120tr._.js +0 -3
  314. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1fwl2mz._.js +0 -3
  315. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1myjm-d._.js +0 -3
  316. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1rhmvod._.js +0 -3
  317. package/.next/standalone/.next/server/chunks/ssr/[externals]__1_g_b3t._.js +0 -3
  318. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0114ewg._.js +0 -4
  319. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02r5bgf._.js +0 -4
  320. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0e8sjqm._.js +0 -4
  321. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0lkzqax._.js +0 -4
  322. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0nb0syg._.js +0 -3
  323. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qucxyj._.js +0 -4
  324. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u02miy._.js +0 -4
  325. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0uno39i._.js +0 -3
  326. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__14dd6h8._.js +0 -3
  327. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ath6v_._.js +0 -4
  328. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1psjybo._.js +0 -3
  329. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1zqz4v8._.js +0 -4
  330. package/.next/standalone/.next/server/chunks/ssr/_1m_p7pi._.js +0 -3
  331. package/.next/standalone/.next/server/chunks/ssr/node_modules_html-to-image_es_index_0oy_-36.js +0 -3
  332. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1ezd2jf._.js +0 -5
  333. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1tnuifj._.js +0 -5
  334. package/.next/standalone/.next/static/chunks/0h7auy7hzjyhw.js +0 -1
  335. package/.next/standalone/.next/static/chunks/0zbxssxh53n-3.js +0 -1
  336. package/.next/standalone/.next/static/chunks/2mazwb1qmur1p.js +0 -1
  337. package/.next/standalone/.next/static/chunks/2pujpnfx4pws6.css +0 -2
  338. package/.next/standalone/.next/static/chunks/33u59vf_8xpd-.js +0 -1
  339. package/.next/standalone/.next/static/chunks/3i27c3hcriawq.css +0 -1
  340. package/.next/standalone/.next/static/chunks/3s7myk_co0gbh.js +0 -1
  341. package/.next/standalone/.next/static/chunks/40k1_v4os4cw6.js +0 -6
  342. package/.next/standalone/Cargo.lock +0 -428
  343. package/.next/standalone/Cargo.toml +0 -9
  344. package/.next/standalone/crates/PROTOCOL.md +0 -122
  345. package/.next/standalone/crates/failproofaid/Cargo.toml +0 -18
  346. package/.next/standalone/crates/failproofaid/src/lock.rs +0 -81
  347. package/.next/standalone/crates/failproofaid/src/main.rs +0 -83
  348. package/.next/standalone/crates/failproofaid/src/paths.rs +0 -172
  349. package/.next/standalone/crates/failproofaid/src/server.rs +0 -502
  350. package/.next/standalone/crates/failproofaid/src/worker.rs +0 -400
  351. package/.next/standalone/crates/failproofaid/tests/daemon_e2e.rs +0 -241
  352. package/.next/standalone/crates/fpai-ipc/Cargo.toml +0 -16
  353. package/.next/standalone/crates/fpai-ipc/src/envelope.rs +0 -175
  354. package/.next/standalone/crates/fpai-ipc/src/framing.rs +0 -177
  355. package/.next/standalone/crates/fpai-ipc/src/lib.rs +0 -10
  356. package/.next/standalone/crates/fpai-ipc/src/peer.rs +0 -80
  357. package/.next/standalone/docker-hook-sync/Dockerfile +0 -79
  358. package/.next/standalone/docker-hook-sync/entrypoint.sh +0 -109
  359. package/.next/standalone/integration-suite/Dockerfile +0 -47
  360. package/.next/standalone/integration-suite/README.md +0 -129
  361. package/.next/standalone/integration-suite/canary-policies.mjs +0 -45
  362. package/.next/standalone/integration-suite/capture-tokens.sh +0 -48
  363. package/.next/standalone/integration-suite/ci-entrypoint.sh +0 -179
  364. package/.next/standalone/integration-suite/inject-tokens.sh +0 -44
  365. package/.next/standalone/integration-suite/install-clis.sh +0 -136
  366. package/.next/standalone/integration-suite/probe-cli.sh +0 -248
  367. package/.next/standalone/integration-suite/report.js +0 -120
  368. package/.next/standalone/integration-suite/run.sh +0 -159
  369. package/.next/standalone/openclaw-plugin/index.js +0 -223
  370. package/.next/standalone/openclaw-plugin/openclaw.plugin.json +0 -6
  371. package/.next/standalone/openclaw-plugin/package.json +0 -12
  372. package/.next/standalone/osv-scanner.toml +0 -17
  373. package/.next/standalone/pi-extension/index.ts +0 -524
  374. package/.next/standalone/pi-extension/package.json +0 -12
  375. package/.next/standalone/postcss.config.mjs +0 -8
  376. package/.next/standalone/readme-arch-hq.gif +0 -0
  377. package/.next/standalone/rust-toolchain.toml +0 -4
  378. package/src/auth/cli.ts +0 -371
  379. /package/.next/standalone/.next/static/{NWAjs0tHFyzk97gRGG09r → VjEdIL6W5uBsvEzknXqV9}/_buildManifest.js +0 -0
  380. /package/.next/standalone/.next/static/{NWAjs0tHFyzk97gRGG09r → VjEdIL6W5uBsvEzknXqV9}/_clientMiddlewareManifest.js +0 -0
  381. /package/.next/standalone/.next/static/{NWAjs0tHFyzk97gRGG09r → VjEdIL6W5uBsvEzknXqV9}/_ssgManifest.js +0 -0
@@ -124,13 +124,13 @@ if (hookIdx >= 0) {
124
124
  // machine until `failproofai config` has installed failproofaid AND
125
125
  // written the daemonConfigured marker (Stage 4). Until then this is
126
126
  // byte-for-byte the same handleHookEvent(...) call below.
127
- const { isDaemonConfigured, tryDaemonHook } = await import("../src/hooks/daemon-client");
127
+ const { isDaemonConfigured, attemptDaemonHook } = await import("../src/hooks/daemon-client");
128
128
  if (isDaemonConfigured()) {
129
129
  const { readStdinPayload } = await import("../src/hooks/read-stdin");
130
130
  const { evaluateHookEvent } = await import("../src/hooks/handler");
131
131
  const stdinRead = await readStdinPayload();
132
132
 
133
- const daemonResult = await tryDaemonHook({
133
+ const attempt = await attemptDaemonHook({
134
134
  hookEvent: eventType,
135
135
  cli,
136
136
  stdin: stdinRead.payload,
@@ -140,21 +140,40 @@ if (hookIdx >= 0) {
140
140
  cwd: process.cwd(),
141
141
  });
142
142
 
143
- // No silent fallback to in-process evaluation here: this machine was
144
- // explicitly configured to run through the daemon, so an unreachable
145
- // daemon fails closed insteada loud, correctly-shaped deny (real
146
- // per-CLI response shaping, not a generic denial) rather than a
147
- // silent downgrade. See the plan's "Confirmed scope decisions".
148
- const result =
149
- daemonResult ??
150
- (await evaluateHookEvent(eventType, cli, stdinRead.payload, {
151
- forceDecision: {
152
- decision: "deny",
153
- reason:
154
- "failproofaid could not be reached. This machine is configured to run hooks through it " +
155
- "— check the daemon (see `failproofai config`) rather than retrying blindly.",
156
- },
157
- }));
143
+ // On a daemon-configured machine the daemon is the ONLY evaluator. Every
144
+ // way of not getting an answer from it denies, and in-process evaluation
145
+ // is never reached from this branch that is what "all enforcement is
146
+ // routed through the daemon" means, and a fallback here would be a second
147
+ // policy engine reachable by breaking the first.
148
+ //
149
+ // The two failures still differ in what the USER has to do, so they are
150
+ // told apart in the message and nowhere else:
151
+ //
152
+ // protocol-mismatch: a daemon answered, so it is alive; the CLI and the
153
+ // daemon are different versions, which is what an `npm update` that
154
+ // has not been followed by `failproofai config` looks like. The remedy
155
+ // is an upgrade, and naming it is the difference between a one-command
156
+ // fix and a support ticket. `daemonVersionSkew()` has already been
157
+ // hinting this on every CLI command.
158
+ //
159
+ // unreachable: nothing answered. A stopped service, a deleted socket and
160
+ // deliberate tampering are indistinguishable from here — and a machine
161
+ // where stopping one service silently disables every guardrail is not
162
+ // a guarded machine.
163
+ let result;
164
+ if (attempt.ok) {
165
+ result = attempt.response;
166
+ } else {
167
+ const reason =
168
+ attempt.failure === "protocol-mismatch"
169
+ ? "failproofaid is running a different protocol version than this CLI, so it " +
170
+ "cannot evaluate this call. Run `failproofai config` to update the daemon."
171
+ : "failproofaid could not be reached. This machine is configured to run hooks through it " +
172
+ "— check the daemon (see `failproofai config`) rather than retrying blindly.";
173
+ result = await evaluateHookEvent(eventType, cli, stdinRead.payload, {
174
+ forceDecision: { decision: "deny", reason },
175
+ });
176
+ }
158
177
 
159
178
  if (result.stdout) process.stdout.write(result.stdout);
160
179
  if (result.stderr) process.stderr.write(result.stderr);
@@ -168,6 +187,64 @@ if (hookIdx >= 0) {
168
187
  await exitAfterFlush(exitCode);
169
188
  } catch (err) {
170
189
  const msg = err instanceof Error ? err.message : String(err);
190
+
191
+ // The outer fail-closed boundary. This wrote NOTHING to stdout and exited 2,
192
+ // which is a deny for Claude and Factory's non-Stop events and a silent
193
+ // ALLOW for the eight CLIs that read their verdict from stdout JSON —
194
+ // Cursor, Pi, Hermes, OpenClaw, Devin, Antigravity, Goose, and Factory's
195
+ // Stop. Everything above can land here, INCLUDING the forced-deny call that
196
+ // handles an unreachable daemon, so the one path whose entire job is to fail
197
+ // closed failed open instead.
198
+ //
199
+ // Emitted BEFORE any telemetry: `flushHookTelemetry` loops until its queue
200
+ // drains and is not bounded, so draining first meant a stuck send could hold
201
+ // the verdict back indefinitely — and a verdict that arrives after the agent
202
+ // has moved on is the same as no verdict.
203
+ const reason =
204
+ "failproofai could not evaluate this call and is failing closed. " +
205
+ `Check the failproofai installation (\`failproofai config\`). Underlying error: ${msg}`;
206
+ let emitted = false;
207
+ let denyExitCode = 2;
208
+ try {
209
+ // The real evaluator does the shaping, exactly as the unreachable-daemon
210
+ // path above does — that is what keeps this deny from being inert on some
211
+ // CLI whose contract nobody remembered here. It can only work if the
212
+ // handler module is loadable, which is why the fallback below exists.
213
+ const { evaluateHookEvent } = await import("../src/hooks/handler");
214
+ const forced = await evaluateHookEvent(eventType, cli, "", {
215
+ forceDecision: { decision: "deny", reason },
216
+ });
217
+ if (forced.stdout) process.stdout.write(forced.stdout);
218
+ if (forced.stderr) process.stderr.write(forced.stderr);
219
+ emitted = true;
220
+ denyExitCode = forced.exitCode;
221
+ } catch {
222
+ // Last ditch: the module that knows each CLI's exact deny shape is itself
223
+ // the thing that just failed, so this emits the union of them — every CLI
224
+ // reads only the keys it knows and ignores the rest. Deliberately NOT a
225
+ // per-CLI table: a second copy of those twelve contracts would drift from
226
+ // the real one, and this runs only when the install is already broken.
227
+ // Imperfect enforcement beats the zero bytes that were written before.
228
+ try {
229
+ process.stdout.write(
230
+ JSON.stringify({
231
+ decision: "block",
232
+ reason,
233
+ permission: "deny",
234
+ followup_message: reason,
235
+ hookSpecificOutput: {
236
+ hookEventName: eventType,
237
+ permissionDecision: "deny",
238
+ permissionDecisionReason: reason,
239
+ },
240
+ }),
241
+ );
242
+ emitted = true;
243
+ } catch {}
244
+ }
245
+ if (!emitted) console.error(`Unexpected error: ${msg}`);
246
+ else console.error(`[failproofai] failing closed: ${msg}`);
247
+
171
248
  await track("hook_dispatch_error", {
172
249
  event_type: eventType,
173
250
  cli,
@@ -180,8 +257,10 @@ if (hookIdx >= 0) {
180
257
  const { flushHookTelemetry } = await import("../src/hooks/hook-telemetry");
181
258
  await flushHookTelemetry();
182
259
  } catch {}
183
- console.error(`Unexpected error: ${msg}`);
184
- process.exit(2);
260
+ // `exitAfterFlush`, not a bare `process.exit`: this was the one exit in
261
+ // `--hook` handling that skipped it, so under load `process.exit` could
262
+ // truncate the very bytes carrying the deny.
263
+ await exitAfterFlush(denyExitCode);
185
264
  }
186
265
  }
187
266
 
@@ -191,18 +270,8 @@ if (hookIdx >= 0) {
191
270
  * Error → unexpected; shows message only, exits 2
192
271
  */
193
272
  async function runCli() {
194
- // Report a fresh install / upgrade. Deliberately here rather than at module
195
- // scope: everything above this point is the --hook fast path, which runs on
196
- // every tool call. No-ops after the first run on a given version.
197
- try {
198
- const { maybeReportInstall } = await import("../lib/install-check");
199
- await maybeReportInstall(version);
200
- } catch {
201
- // never block a command on reporting
202
- }
203
-
204
273
  // --help / -h (only when not inside a subcommand that handles its own --help)
205
- const SUBCOMMANDS = ["policies", "policy", "auth", "audit", "config"];
274
+ const SUBCOMMANDS = ["policies", "policy", "audit", "config", "uninstall", "backfill", "flush", "harness"];
206
275
  if ((args.includes("--help") || args.includes("-h")) && !SUBCOMMANDS.includes(args[0])) {
207
276
  const extraArgs = args.filter((a) => a !== "--help" && a !== "-h");
208
277
  if (extraArgs.length > 0) {
@@ -217,6 +286,13 @@ USAGE
217
286
  COMMANDS
218
287
  (no args) Launch the policy dashboard
219
288
  config Interactive setup — pick scope, agents & policies
289
+ --connect <url> --token <key> Connect to Failproof Cloud non-interactively
290
+ --machine-id <id> Stable id for this machine
291
+ --machine-label <name> Human-readable name in the dashboard
292
+ --no-transcripts Report decisions only, never transcripts
293
+ --disconnect Stop pulling policy and sending activity
294
+ --status Show connection, daemon and pause state
295
+ --pause / --resume Pause or resume enforcement
220
296
 
221
297
  policy add <name> Enable a single policy (see \`policy --help\`)
222
298
  policy remove <name> Disable a single policy
@@ -243,16 +319,52 @@ COMMANDS
243
319
 
244
320
  policies --help, -h Show this help for the policies command
245
321
 
246
- auth Sign in / out of FailproofAI from the CLI.
247
- login Email + OTP flow; writes ~/.failproofai/auth.json
248
- logout Revoke this session and remove auth.json
249
- whoami Print the currently authenticated identity
250
- auth --help, -h Show this help for the auth command
322
+ harness list Show extra capture paths per agent CLI
323
+ harness add-path <h> <path> Also capture sessions from <path> for harness
324
+ <h>. Accepts \`<label>=<path>\`; the label
325
+ namespaces agent ids so two copies of one
326
+ project stay distinct.
327
+ harness remove-path <h> <p> Stop capturing that path
328
+ harness --help, -h Show this help for the harness command
251
329
 
252
330
  audit Audit your agent's behavior, then open the
253
331
  dashboard at http://localhost:8020/audit
254
332
  audit --help, -h Show this help for the audit command
255
333
 
334
+ backfill Re-send history the collector already read past
335
+ — after clearing the dashboard, re-enrolling a
336
+ machine, or connecting later than the work
337
+ --since <when> How far back: 30d, 6m, or YYYY-MM-DD
338
+ (default: 30 days)
339
+ --dry-run Report what would be re-read, change nothing
340
+
341
+ flush Deliver everything already spooled, now,
342
+ instead of waiting for the next sweep
343
+ --wait Block until the spool drains (or --timeout)
344
+ --timeout <secs> How long to wait with --wait (default: 60)
345
+
346
+ update Finish an upgrade npm cannot: migrate
347
+ ~/.failproofai to this version's layout, put the
348
+ matching daemon binary in place, restart it.
349
+ Run after \`npm install -g failproofai@latest\`.
350
+ --no-daemon Migrate the home only
351
+
352
+ migrate Run pending layout migrations on their own,
353
+ keyed on the layout in ~/.failproofai/VERSION
354
+ (not the npm version, so skipping releases with
355
+ no layout change runs nothing)
356
+ --dry-run Print the steps and change nothing
357
+
358
+ uninstall Remove failproofai from this machine: hook
359
+ entries from every agent CLI, and the daemon
360
+ service. Run this BEFORE \`npm rm -g failproofai\`
361
+ — npm runs no uninstall script, so removing the
362
+ package alone leaves both behind.
363
+ --purge Also delete ~/.failproofai (settings,
364
+ credentials, audit history, daemon binary)
365
+ --dry-run Show what would be removed, change nothing
366
+ --yes, -y Skip the confirmation prompt
367
+
256
368
  --version, -v Print version and exit
257
369
  --help, -h Show this help message
258
370
 
@@ -282,6 +394,10 @@ EXAMPLES
282
394
  failproofai policies --uninstall --cli opencode
283
395
  failproofai policies --uninstall --cli pi
284
396
  failproofai policies --uninstall --custom
397
+ failproofai backfill --since 6m
398
+ failproofai backfill --dry-run
399
+ failproofai flush --wait
400
+ failproofai config --status
285
401
 
286
402
  LINKS
287
403
  ⭐ Star us: https://github.com/failproofai/failproofai
@@ -301,6 +417,698 @@ LINKS
301
417
  process.exit(0);
302
418
  }
303
419
 
420
+ // First-run onboarding — before any subcommand runs its own work.
421
+ //
422
+ // On a machine that has never been set up, the first thing the user typed is
423
+ // almost never the thing they need first, so we run the wizard and then let
424
+ // their original command proceed. Exemptions matter more than the rule:
425
+ //
426
+ // --hook never reaches here (it exits above) — it runs on every
427
+ // tool call, and a wizard on that path would hang an agent
428
+ // --version/--help answering "what is this" must not require setup
429
+ // config IS the wizard
430
+ // policies/policy explicit configuration actions. Intercepting these would
431
+ // fight the intent the user just stated, and would break
432
+ // non-interactive scripts that call them to do setup.
433
+ //
434
+ // `maybeFirstRunConfigure` is itself a no-op on a configured machine, on a
435
+ // non-TTY, and under sudo — so this is a cheap check, not a second gate.
436
+ // Layout check, before onboarding decides anything. A home written by an
437
+ // older layout is reset here — visibly, in a real command the user typed —
438
+ // rather than from a hook, which runs unattended once per tool call. A home
439
+ // written by a NEWER layout stops the command instead: that data is fine and
440
+ // an upgrade would read it, so deleting it would destroy something
441
+ // recoverable.
442
+ //
443
+ // `audit --scheduled` is the exception, and it takes the HOOK's branch: it is
444
+ // spawned by failproofaid on a timer, so "visibly, in a real command the user
445
+ // typed" is exactly what it is not. `checkLayoutForCli()` deletes
446
+ // config.toml and credentials.toml (see `resettablePaths`), so letting a
447
+ // background process reach it would silently revoke a user's
448
+ // `[telemetry] enabled = false`, erase their cloud enrolment, and switch off
449
+ // `[audit] auto` — the setting that scheduled the run — with the explanation
450
+ // going only to the service journal. Reachable on every machine at the next
451
+ // LAYOUT_VERSION bump, when a home carrying `auto = true` is by definition
452
+ // stale. Verified live: one scheduled tick took a home's whole config.
453
+ //
454
+ // `--help` / `--version` take the same exemption `first-run-gate.ts` gives
455
+ // them, and for a stronger reason. Those two answer "what is this / how do I
456
+ // use it"; the subcommands that parse their own help (`policies --help`) fall
457
+ // past the help block above and reach here, so without this a user typing
458
+ // `failproofai policies --help` had their home reset by a question. The
459
+ // adjacent, far less destructive first-run gate exempted help from the start.
460
+ {
461
+ const isHelpOrVersion =
462
+ args.includes("--help") || args.includes("-h") || args.includes("--version") || args.includes("-v");
463
+ if (args[0] === "audit" && args.includes("--scheduled")) {
464
+ // NOT `layoutWarningForHook` — that one answers the hook's question
465
+ // ("are policies unenforced?") and deliberately stays quiet when the
466
+ // global config is readable. This gate is about the LAYOUT, and must
467
+ // fire whenever completing the run would reset the home.
468
+ const { layoutBlockerForScheduledRun } = await import("../src/hooks/fp-reset");
469
+ const warning = layoutBlockerForScheduledRun();
470
+ if (warning) {
471
+ // Exit 1, not 75: 75 means "another audit holds the lock" and is retried
472
+ // in fifteen minutes, which here would just re-warn four times an hour
473
+ // forever. A stale home is a real failure that `failproofai config`
474
+ // fixes, so it is reported and retried at the ordinary cadence.
475
+ console.error(warning);
476
+ process.exit(1);
477
+ }
478
+ } else if (!isHelpOrVersion && args[0] !== "migrate" && args[0] !== "update") {
479
+ // `migrate` and `update` are exempt because they ARE this step, and letting
480
+ // it run first breaks them both. `migrate --dry-run` promises to change
481
+ // nothing and print what would happen — with the check ahead of it, the home
482
+ // was already migrated by the time the subcommand looked, so it reported
483
+ // "nothing to migrate" on every stale machine it was ever pointed at, which
484
+ // is the one answer a dry run must never give wrongly. Caught by a smoke
485
+ // test on a seeded layout-2 home, not by a unit test: nothing below the CLI
486
+ // entry point can see this ordering.
487
+ const { checkLayoutForCli } = await import("../src/hooks/fp-reset");
488
+ const check = await checkLayoutForCli();
489
+ for (const line of check.lines) console.error(line);
490
+ if (check.fatal) process.exit(1);
491
+ // `check.didReset` is deliberately not read. It used to force the wizard
492
+ // below; see the note at `shouldOfferFirstRun` for why a migrated machine
493
+ // no longer needs setup re-run, and `didReset` in `fp-reset.ts` for what
494
+ // the field means now.
495
+ }
496
+ }
497
+
498
+ // Report a fresh install / upgrade. AFTER the layout check above, never
499
+ // before: this writes the `last-version` marker, and while that file lived at
500
+ // the root of the home it was one of the landmarks `detectLayout()` reads as
501
+ // "layout 1". Running first meant the CLI created the file and then read it
502
+ // back as evidence of an old layout, so every genuinely fresh machine had its
503
+ // very first command open with "failproofai reorganised … Removed 1 item(s)
504
+ // from the old layout." Nothing was lost — there was nothing there — but
505
+ // training every new user to ignore that banner is expensive given what it
506
+ // says on a real layout-1 home. The file has also moved under `state/`, so
507
+ // the two are independent now; the order is kept because it is the correct
508
+ // one regardless.
509
+ //
510
+ // Deliberately not at module scope: everything above the CLI entry is the
511
+ // --hook fast path, which runs on every tool call. No-ops after the first run
512
+ // on a given version.
513
+ try {
514
+ const { maybeReportInstall } = await import("../lib/install-check");
515
+ await maybeReportInstall(version);
516
+ } catch {
517
+ // never block a command on reporting
518
+ }
519
+
520
+ const { shouldOfferFirstRun } = await import("../src/hooks/first-run-gate");
521
+ // `|| layoutWasReset` stood here, with `force: layoutWasReset` below, because a
522
+ // migration used to delete the home's policy config while leaving the agent
523
+ // CLIs' settings files alone — so `isConfigured()` read true off
524
+ // `hasGlobalHooks` and setup was skipped, leaving hooks firing against no
525
+ // policies, silently and permanently.
526
+ //
527
+ // The migration keeps that file now (`HOME_CLASSES` classes it `user-typed`),
528
+ // along with `config.json` and `credentials.json`, so a migrated machine is
529
+ // configured in fact and not merely in appearance. Forcing the wizard here
530
+ // would open an interactive prompt with nothing to answer — and on a fleet box,
531
+ // a CI runner or a headless gateway, nobody to answer it. A home that never
532
+ // finished setup still gets here on its own: `isConfigured()` is false for it.
533
+ // See `didReset` in `fp-reset.ts`.
534
+ if (shouldOfferFirstRun(args)) {
535
+ try {
536
+ const { maybeFirstRunConfigure } = await import("../src/hooks/configure-wizard");
537
+ // `audit` runs its own scan immediately after this returns; firing the
538
+ // post-setup audit too would scan the whole history twice in a row.
539
+ await maybeFirstRunConfigure({}, { postSetupAudit: args[0] !== "audit" });
540
+ } catch {
541
+ // Onboarding is never allowed to block the command the user actually typed.
542
+ }
543
+ }
544
+
545
+ // backfill [--since <YYYY-MM-DD|Nd>] [--dry-run]
546
+ //
547
+ // Hands off to the daemon rather than doing the work: the cursors it rewinds
548
+ // are held in memory by the RUNNING collector, which would write them back
549
+ // over. Every precondition a person can get wrong is still checked HERE,
550
+ // synchronously, because reporting success and leaving the real failure in the
551
+ // journal is what already cost twenty minutes on a live machine.
552
+ if (args[0] === "flush") {
553
+ const subArgs = args.slice(1);
554
+ if (subArgs.includes("--help") || subArgs.includes("-h")) {
555
+ console.log(`
556
+ failproofai flush — deliver what is already spooled, now
557
+
558
+ USAGE
559
+ failproofai flush [--wait] [--timeout <secs>]
560
+
561
+ WHY
562
+ The collector is unhurried on purpose: a batch is swept once it is older than
563
+ two minutes, at most 64 per pass, on a 60-second cadence. That pacing keeps a
564
+ backlog from stampeding the server, and it is exactly wrong when you are
565
+ standing at a dashboard waiting to see your own events — "not delivered yet"
566
+ and "not working" look identical from there.
567
+
568
+ This asks the daemon to make a pass right now, with no minimum age and no
569
+ per-pass cap. It re-sends nothing: only batches already spooled and not yet
570
+ delivered. For history the collector has already read past, use \`backfill\`.
571
+
572
+ OPTIONS
573
+ --wait Block until the spool drains, or --timeout elapses.
574
+ --timeout <secs> How long --wait waits. Default: 60.
575
+ `);
576
+ process.exit(0);
577
+ }
578
+
579
+ const KNOWN = new Set(["--wait", "--timeout"]);
580
+ const unknown = subArgs.find(
581
+ (a, i) => a.startsWith("-") && !KNOWN.has(a) && subArgs[i - 1] !== "--timeout",
582
+ );
583
+ if (unknown) {
584
+ throw new CliError(`Unexpected argument: ${unknown}\nRun \`failproofai flush --help\` for usage.`);
585
+ }
586
+
587
+ let timeoutSecs;
588
+ const tIdx = subArgs.indexOf("--timeout");
589
+ if (tIdx >= 0) {
590
+ const raw = subArgs[tIdx + 1];
591
+ if (!raw || raw.startsWith("-")) throw new CliError("Missing value after --timeout.");
592
+ const n = Number(raw);
593
+ // Rejected rather than coerced: NaN would silently become "wait forever
594
+ // or not at all" depending on the comparison, and neither is what was asked.
595
+ if (!Number.isFinite(n) || n <= 0) {
596
+ throw new CliError(`Could not read --timeout ${raw}. Give a number of seconds.`);
597
+ }
598
+ timeoutSecs = n;
599
+ }
600
+
601
+ lastSubcommand = "flush";
602
+ const { runFlushCommand } = await import("../src/hooks/flush-cli");
603
+ const result = await runFlushCommand({ wait: subArgs.includes("--wait"), timeoutSecs });
604
+ for (const line of result.lines) {
605
+ if (result.exitCode === 0) console.log(line);
606
+ else console.error(line);
607
+ }
608
+ await track("cli_flush", {
609
+ ok: result.exitCode === 0,
610
+ waited: subArgs.includes("--wait"),
611
+ pending: result.pending,
612
+ });
613
+ lastSubcommand = null;
614
+ await exitAfterFlush(result.exitCode);
615
+ return;
616
+ }
617
+
618
+ if (args[0] === "backfill") {
619
+ const subArgs = args.slice(1);
620
+ if (subArgs.includes("--help") || subArgs.includes("-h")) {
621
+ console.log(`
622
+ failproofai backfill — re-send history the collector has already read past
623
+
624
+ USAGE
625
+ failproofai backfill [--since <when>] [--dry-run]
626
+
627
+ WHY
628
+ The collector never re-reads a file it has a cursor for, which is right until
629
+ the dashboard's data is cleared, a machine is re-enrolled, or cursors advanced
630
+ before there was anywhere to send. Then the history exists on disk and nowhere
631
+ else, with no way to ask for it again.
632
+
633
+ Re-sending is safe: redaction is deterministic, so a re-sent event hashes
634
+ identically to its first send and collapses into the row already there.
635
+
636
+ OPTIONS
637
+ --since <when> How far back. \`30d\`, \`6m\`, or \`YYYY-MM-DD\`.
638
+ Default: 30 days.
639
+ --dry-run Report what would be re-read and change nothing.
640
+
641
+ Which streams are sent follows [collector] in ~/.failproofai/config.toml —
642
+ a backfill never sends something your config says you do not want.
643
+ `);
644
+ process.exit(0);
645
+ }
646
+
647
+ const KNOWN = new Set(["--since", "--dry-run"]);
648
+ const unknown = subArgs.find((a, i) => a.startsWith("-") && !KNOWN.has(a) && subArgs[i - 1] !== "--since");
649
+ if (unknown) {
650
+ throw new CliError(`Unexpected argument: ${unknown}\nRun \`failproofai backfill --help\` for usage.`);
651
+ }
652
+
653
+ let sinceMs;
654
+ const sinceIdx = subArgs.indexOf("--since");
655
+ if (sinceIdx >= 0) {
656
+ const raw = subArgs[sinceIdx + 1];
657
+ if (!raw || raw.startsWith("-")) throw new CliError("Missing value after --since.");
658
+ // `30d` / `6m` / an ISO date. Rejected rather than guessed at: silently
659
+ // reading an unparseable window as "the default" would send a different
660
+ // amount of history than was asked for, and nothing would say so.
661
+ const rel = /^(\d+)([dmy])$/.exec(raw);
662
+ if (rel) {
663
+ const n = Number(rel[1]);
664
+ const days = rel[2] === "d" ? n : rel[2] === "m" ? n * 30 : n * 365;
665
+ sinceMs = Date.now() - days * 24 * 60 * 60 * 1000;
666
+ } else {
667
+ const t = Date.parse(raw);
668
+ if (Number.isNaN(t)) {
669
+ throw new CliError(`Could not read --since ${raw}. Use 30d, 6m, or YYYY-MM-DD.`);
670
+ }
671
+ sinceMs = t;
672
+ }
673
+ }
674
+
675
+ lastSubcommand = "backfill";
676
+ const { runBackfillCommand } = await import("../src/hooks/backfill-cli");
677
+ const result = runBackfillCommand({ sinceMs, dryRun: subArgs.includes("--dry-run") });
678
+ for (const line of result.lines) {
679
+ if (result.exitCode === 0) console.log(line);
680
+ else console.error(line);
681
+ }
682
+ await track("cli_backfill", { ok: result.exitCode === 0, dry_run: subArgs.includes("--dry-run"), explicit_since: sinceIdx >= 0 });
683
+ lastSubcommand = null;
684
+ await exitAfterFlush(result.exitCode);
685
+ return;
686
+ }
687
+
688
+ // harness list | add-path | remove-path
689
+ //
690
+ // Extra capture paths per harness. Writes ~/.failproofai/config.toml and
691
+ // nothing else — no root, no daemon call — so it works on a machine whose
692
+ // daemon is stopped, which is when someone is most likely to be fixing what
693
+ // it captures.
694
+ if (args[0] === "harness") {
695
+ const subArgs = args.slice(1);
696
+ if (subArgs.length === 0 || subArgs.includes("--help") || subArgs.includes("-h")) {
697
+ console.log(`
698
+ failproofai harness — capture sessions from more than one location per agent CLI
699
+
700
+ USAGE
701
+ failproofai harness list [<harness>]
702
+ failproofai harness add-path <harness> [<label>=]<path>
703
+ failproofai harness remove-path <harness> <path|label>
704
+
705
+ WHY
706
+ Each agent CLI is watched wherever its own installer put it — ~/.claude/projects,
707
+ ~/.hermes/state.db, and so on. That misses every other arrangement: a second
708
+ profile, a mounted team share, a container's home beside the host's, an agent
709
+ an operator moved. Those hold real sessions and nothing collects them.
710
+
711
+ THE LABEL
712
+ An entry is \`<path>\` or \`<label>=<path>\`. The label namespaces agent ids as
713
+ <label>-<agentId>, and it matters: two locations holding the same project
714
+ derive the SAME id (it comes from the cwd inside the transcript, identical in
715
+ both copies), so without a label they merge into one agent whose sessions
716
+ interleave from two machines' worth of history. Omit it and one is derived
717
+ from the folder name.
718
+
719
+ HARNESSES
720
+ claude, codex, copilot, openclaw, pi, factory, antigravity, cursor,
721
+ goose, opencode, devin, hermes
722
+
723
+ \`claude\` covers subagent transcripts too — they live under the same root.
724
+
725
+ NOTES
726
+ • Session collection must be on for any of this to be read; extra paths live
727
+ under "collector" in ~/.failproofai/config.json like the default ones do.
728
+ • A path overlapping one already captured is REFUSED by the daemon at startup
729
+ rather than collected twice under two ids. It says so in the journal.
730
+ • Two entries sharing a LABEL are refused too: they would share one cursor
731
+ directory, whose whole map is written at once, so each would clobber the
732
+ other's watermark and re-read from zero after every restart.
733
+ • Takes effect within seconds — the daemon re-reads config.json on an interval
734
+ and cycles its collector. No restart, no sudo.
735
+
736
+ EXAMPLES
737
+ failproofai harness add-path claude work=/srv/team/.claude/projects
738
+ failproofai harness add-path hermes prod=/srv/hermes-prod/state.db
739
+ failproofai harness add-path codex /mnt/other-home/.codex/sessions
740
+ failproofai harness list
741
+ failproofai harness remove-path claude work
742
+
743
+ One home per person — label them, or both derive the same folder-name label
744
+ and the second is refused:
745
+ failproofai harness add-path openclaw user1=/srv/.openclaw-user1
746
+ failproofai harness add-path openclaw user2=/srv/.openclaw-user2
747
+
748
+ Containers: FAILPROOFAI_<HARNESS>_EXTRA_PATHS replaces the file's entries.
749
+ FAILPROOFAI_OPENCLAW_EXTRA_PATHS="user1=/srv/.openclaw-user1,user2=/srv/.openclaw-user2"
750
+
751
+ `.trimStart());
752
+ process.exit(0);
753
+ }
754
+
755
+ lastSubcommand = "harness";
756
+ const { runHarnessCommand } = await import("../src/hooks/harness-cli");
757
+ const result = runHarnessCommand(subArgs);
758
+ for (const line of result.lines) {
759
+ if (result.exitCode === 0) console.log(line);
760
+ else console.error(line);
761
+ }
762
+ await track("cli_harness", {
763
+ ok: result.exitCode === 0,
764
+ // The subcommand only — never the harness name, the label or the path.
765
+ // A path is a value the user typed, and `enterprise-docs/product-analytics.md`
766
+ // promises we send shape, never value.
767
+ sub: ["list", "add-path", "remove-path"].includes(subArgs[0]) ? subArgs[0] : "unknown",
768
+ });
769
+ lastSubcommand = null;
770
+ await exitAfterFlush(result.exitCode);
771
+ return;
772
+ }
773
+
774
+ // uninstall [--purge] [--dry-run] [--yes|-y]
775
+ //
776
+ // Top-level, and deliberately NOT a flag on `policies`. `policies --uninstall`
777
+ // disables policies; this removes the product — hook entries across every
778
+ // agent CLI plus the root-owned daemon service — and the two must not be one
779
+ // keystroke apart.
780
+ if (args[0] === "migrate") {
781
+ const subArgs = args.slice(1);
782
+ if (subArgs.includes("--help") || subArgs.includes("-h")) {
783
+ console.log(`
784
+ failproofai migrate — bring ~/.failproofai up to the layout this version speaks
785
+
786
+ USAGE
787
+ failproofai migrate [--dry-run]
788
+
789
+ WHY
790
+ npm cannot update an installed package on its own, so a machine can sit on an
791
+ old version for months and then jump several layouts at once. This runs the
792
+ steps for that jump in order, keyed on the LAYOUT recorded in
793
+ ~/.failproofai/VERSION rather than on the npm version — so skipping thirty
794
+ releases with no layout change runs nothing at all.
795
+
796
+ It normally happens by itself, on the first command after an upgrade. This is
797
+ for running it deliberately, and for seeing what it would do first.
798
+
799
+ Your settings, cloud enrolment, policy selection, your own policy files,
800
+ decision history and undelivered events are carried across, not removed. The
801
+ irreplaceable files are copied to migrations/backup-layout<n>/ before anything
802
+ runs, and every step is recorded in migrations/applied.json.
803
+
804
+ OPTIONS
805
+ --dry-run Print the steps and the files that would be saved. Change nothing.
806
+ `);
807
+ process.exit(0);
808
+ }
809
+
810
+ const KNOWN = new Set(["--dry-run"]);
811
+ const unknown = subArgs.find((a) => a.startsWith("-") && !KNOWN.has(a));
812
+ if (unknown) {
813
+ throw new CliError(
814
+ `Unexpected argument: ${unknown}\nRun \`failproofai migrate --help\` for usage.`,
815
+ );
816
+ }
817
+
818
+ lastSubcommand = "migrate";
819
+ const { detectLayout } = await import("../src/hooks/fp-config");
820
+ const { LAYOUT_VERSION } = await import("../src/hooks/fp-home");
821
+ const { describePlan, runMigrations } = await import("../src/hooks/migrations");
822
+ const state = detectLayout();
823
+
824
+ // A NEWER home is refused here exactly as `checkLayoutForCli` refuses it: the
825
+ // data is fine and an upgrade would read it, so migrating "forward" from it
826
+ // is not a thing that exists.
827
+ if (state.kind === "future") {
828
+ console.error(
829
+ `This machine's failproofai directory was written by a newer version (layout ${state.found};`,
830
+ );
831
+ console.error(`this build speaks ${LAYOUT_VERSION}). Upgrade rather than migrate:`);
832
+ console.error(` npm install -g failproofai@latest`);
833
+ await track("cli_migrate", { ok: false, reason: "future_layout" });
834
+ lastSubcommand = null;
835
+ await exitAfterFlush(1);
836
+ return;
837
+ }
838
+
839
+ const from = state.kind === "stale" ? state.found : LAYOUT_VERSION;
840
+ if (subArgs.includes("--dry-run")) {
841
+ for (const line of describePlan(from)) console.log(line);
842
+ await track("cli_migrate", { ok: true, dry_run: true, from });
843
+ lastSubcommand = null;
844
+ await exitAfterFlush(0);
845
+ return;
846
+ }
847
+
848
+ if (state.kind !== "stale") {
849
+ console.log(`Already at layout ${LAYOUT_VERSION}. Nothing to migrate.`);
850
+ await track("cli_migrate", { ok: true, from, steps: 0 });
851
+ lastSubcommand = null;
852
+ await exitAfterFlush(0);
853
+ return;
854
+ }
855
+
856
+ const run = runMigrations(from);
857
+ for (const step of run.steps) {
858
+ console.log(`${step.ok ? "migrated" : "FAILED "} layout ${step.from} → ${step.to}`);
859
+ }
860
+ if (run.backedUp.length > 0) {
861
+ console.log(`Saved first: ${run.backedUp.join(", ")}`);
862
+ }
863
+ if (run.failed) {
864
+ console.error(`Step ${run.failed.from} → ${run.failed.to} did not finish: ${run.failed.error}`);
865
+ console.error(`The home is still marked layout ${from} and will be retried.`);
866
+ }
867
+ await track("cli_migrate", {
868
+ ok: !run.failed,
869
+ from,
870
+ steps: run.steps.length,
871
+ removed: run.outcome.removed.length,
872
+ });
873
+ lastSubcommand = null;
874
+ await exitAfterFlush(run.failed ? 1 : 0);
875
+ return;
876
+ }
877
+
878
+ if (args[0] === "update") {
879
+ const subArgs = args.slice(1);
880
+ if (subArgs.includes("--help") || subArgs.includes("-h")) {
881
+ console.log(`
882
+ failproofai update — finish an upgrade: migrate the home, match the daemon
883
+
884
+ USAGE
885
+ npm install -g failproofai@latest && failproofai update [--no-daemon]
886
+
887
+ WHY
888
+ npm replaces the CLI and nothing else. The daemon binary lives at
889
+ ~/.failproofai/bin/failproofaid-<version> and stays exactly where it was, so
890
+ after an npm upgrade the two halves are different versions — and failproofaid
891
+ refuses to start against a layout it does not speak, which is the loud version
892
+ of that problem rather than the silent one.
893
+
894
+ This does the rest of the upgrade: runs any pending layout migrations, puts the
895
+ matching daemon binary in place, and restarts the service.
896
+
897
+ OPTIONS
898
+ --no-daemon Migrate the home only. Leaves a version-skewed daemon in place,
899
+ so prefer letting it run.
900
+ `);
901
+ process.exit(0);
902
+ }
903
+
904
+ const KNOWN = new Set(["--no-daemon"]);
905
+ const unknown = subArgs.find((a) => a.startsWith("-") && !KNOWN.has(a));
906
+ if (unknown) {
907
+ throw new CliError(
908
+ `Unexpected argument: ${unknown}\nRun \`failproofai update --help\` for usage.`,
909
+ );
910
+ }
911
+
912
+ lastSubcommand = "update";
913
+ // Runs the migration itself rather than leaning on the check at the top of
914
+ // this file, which this command is exempt from. Reading the ledger and
915
+ // inferring "the top-level check must have done it" was the first version, and
916
+ // it is the kind of indirection that reads fine and reports the wrong thing
917
+ // the moment either half moves.
918
+ const { detectLayout } = await import("../src/hooks/fp-config");
919
+ const { LAYOUT_VERSION } = await import("../src/hooks/fp-home");
920
+ const { runMigrations } = await import("../src/hooks/migrations");
921
+ const state = detectLayout();
922
+
923
+ if (state.kind === "future") {
924
+ console.error(
925
+ `This machine's failproofai directory was written by a NEWER version (layout ${state.found};`,
926
+ );
927
+ console.error(`this build speaks ${LAYOUT_VERSION}). This CLI is the stale half:`);
928
+ console.error(` npm install -g failproofai@latest`);
929
+ await track("cli_update", { ok: false, reason: "future_layout" });
930
+ lastSubcommand = null;
931
+ await exitAfterFlush(1);
932
+ return;
933
+ }
934
+
935
+ let migrationsRan = 0;
936
+ let migrationFailed = false;
937
+ if (state.kind === "stale") {
938
+ const run = runMigrations(state.found);
939
+ migrationsRan = run.steps.length;
940
+ migrationFailed = Boolean(run.failed);
941
+ for (const step of run.steps) {
942
+ console.log(`${step.ok ? "migrated" : "FAILED "} layout ${step.from} → ${step.to}`);
943
+ }
944
+ if (run.backedUp.length > 0) console.log(`Saved first: ${run.backedUp.join(", ")}`);
945
+ if (run.failed) {
946
+ console.error(
947
+ `Step ${run.failed.from} → ${run.failed.to} did not finish: ${run.failed.error}`,
948
+ );
949
+ console.error(`The home is still marked layout ${state.found} and will be retried.`);
950
+ }
951
+ } else {
952
+ console.log(`Home is at layout ${LAYOUT_VERSION}; no migration was needed.`);
953
+ }
954
+
955
+ // The daemon is refreshed even when a step failed, and deliberately: the
956
+ // binary and the layout are independent halves, and leaving a version-skewed
957
+ // daemon behind on top of a failed migration is strictly worse than fixing
958
+ // the half that can be fixed. The exit code still reports the failure.
959
+ let daemonOk = true;
960
+ if (subArgs.includes("--no-daemon")) {
961
+ console.log("Skipped the daemon (--no-daemon). Its version may not match this CLI.");
962
+ } else {
963
+ const svc = await import("../src/hooks/daemon-service");
964
+ if (!svc.isDaemonSupportedPlatform()) {
965
+ console.log(`failproofaid does not run on ${process.platform}; nothing to update.`);
966
+ } else {
967
+ // `refreshDaemonToCliVersion` answers "is there a service at all" itself,
968
+ // so there is no separate installed check to get out of step with it.
969
+ const result = await svc.refreshDaemonToCliVersion();
970
+ for (const line of result.lines) console.log(line);
971
+ daemonOk = result.ok;
972
+ }
973
+ }
974
+
975
+ await track("cli_update", {
976
+ ok: daemonOk && !migrationFailed,
977
+ migrations: migrationsRan,
978
+ migration_failed: migrationFailed,
979
+ });
980
+ lastSubcommand = null;
981
+ await exitAfterFlush(daemonOk && !migrationFailed ? 0 : 1);
982
+ return;
983
+ }
984
+
985
+ if (args[0] === "uninstall") {
986
+ const subArgs = args.slice(1);
987
+ if (subArgs.includes("--help") || subArgs.includes("-h")) {
988
+ console.log(`
989
+ failproofai uninstall — remove failproofai from this machine
990
+
991
+ USAGE
992
+ failproofai uninstall [--purge] [--dry-run] [--yes]
993
+
994
+ WHAT IT REMOVES
995
+ • failproofai hook entries from every agent CLI that has them
996
+ • the failproofaid daemon service — ASKED on a plain uninstall (kept unless
997
+ you say yes); always removed with --purge, which prompts for your password
998
+ rather than printing commands to paste
999
+ • the "require the daemon" flag — cleared FIRST, so a partial uninstall can
1000
+ never leave this machine denying every tool call
1001
+
1002
+ OPTIONS
1003
+ --purge Also delete ~/.failproofai — settings, credentials, audit
1004
+ history and the downloaded daemon binary. Off by default so a
1005
+ reinstall keeps your history.
1006
+ --dry-run Print what would be removed and change nothing.
1007
+ --yes, -y Skip the confirmation prompt. Required when there is no TTY.
1008
+
1009
+ WHY THIS EXISTS
1010
+ npm runs no uninstall script, so \`npm rm -g failproofai\` removes the package
1011
+ and leaves the hook entries and the service behind. Run this first, then:
1012
+ npm rm -g failproofai
1013
+ `);
1014
+ process.exit(0);
1015
+ }
1016
+
1017
+ const KNOWN = new Set(["--purge", "--dry-run", "--yes", "-y"]);
1018
+ const unknown = subArgs.find((a) => !KNOWN.has(a));
1019
+ if (unknown) {
1020
+ throw new CliError(
1021
+ `Unexpected argument: ${unknown}\nRun \`failproofai uninstall --help\` for usage.`,
1022
+ );
1023
+ }
1024
+
1025
+ lastSubcommand = "uninstall_command";
1026
+ const { runUninstallCommand } = await import("../src/hooks/uninstall-cli");
1027
+ const purge = subArgs.includes("--purge");
1028
+ const dryRun = subArgs.includes("--dry-run");
1029
+ const yes = subArgs.includes("--yes") || subArgs.includes("-y");
1030
+
1031
+ // Set only when the prompt actually rendered the plan, which is the one
1032
+ // case where printing it again would duplicate it.
1033
+ let planWasShown = false;
1034
+ const result = await runUninstallCommand({
1035
+ purge,
1036
+ dryRun,
1037
+ yes,
1038
+ cwd: process.cwd(),
1039
+ // Only offered when a person can actually answer. Without a TTY the
1040
+ // command requires --yes rather than assuming consent — see the module.
1041
+ confirm: process.stdin.isTTY
1042
+ ? async (planLines) => {
1043
+ planWasShown = true;
1044
+ const { selectOne } = await import("../src/hooks/tui");
1045
+ // "No" first, so the default landing position on Enter is the
1046
+ // non-destructive one.
1047
+ const answer = await selectOne({
1048
+ message: purge
1049
+ ? "Remove failproofai and DELETE ~/.failproofai?"
1050
+ : "Remove failproofai from this machine?",
1051
+ body: planLines,
1052
+ choices: [
1053
+ { label: "No, cancel", value: false },
1054
+ { label: purge ? "Yes, remove and purge" : "Yes, remove it", value: true },
1055
+ ],
1056
+ });
1057
+ return answer === true;
1058
+ }
1059
+ : undefined,
1060
+ // Only on a plain uninstall: `--purge` removes the service unconditionally
1061
+ // because it deletes the binary the service points at. Absent without a TTY,
1062
+ // which the module reads as "keep it".
1063
+ confirmDaemon:
1064
+ process.stdin.isTTY && !purge
1065
+ ? async () => {
1066
+ const { selectOne } = await import("../src/hooks/tui");
1067
+ const answer = await selectOne({
1068
+ message: "Remove the failproofaid background service too?",
1069
+ body: [
1070
+ "It runs as a system service and needs sudo to remove, so you",
1071
+ "will be asked for your password.",
1072
+ "",
1073
+ "Keeping it is fine if you plan to reinstall — the hooks are",
1074
+ "already gone either way, so nothing is being enforced.",
1075
+ ],
1076
+ choices: [
1077
+ { label: "Yes, remove the service", value: true },
1078
+ { label: "No, leave it installed", value: false },
1079
+ ],
1080
+ });
1081
+ return answer === true;
1082
+ }
1083
+ : undefined,
1084
+ });
1085
+
1086
+ // The prompt already rendered the plan as its body; re-printing it would
1087
+ // show the same block twice. `planLines` is reported by the command rather
1088
+ // than guessed from the text — see UninstallResult.
1089
+ const skip = planWasShown ? result.planLines : 0;
1090
+ for (const line of result.lines.slice(skip)) {
1091
+ if (result.exitCode === 0) console.log(line);
1092
+ else console.error(line);
1093
+ }
1094
+ // NOT after a purge. `track` resolves the instance id, and `getInstanceId()`
1095
+ // lazily WRITES ~/.failproofai/state/telemetry-id — which re-created the
1096
+ // whole directory seconds after the purge deleted it, leaving a machine the
1097
+ // user had just wiped holding a brand-new tracking identifier and making
1098
+ // the command's own "✓ deleted" line false. A purge means gone; nothing
1099
+ // gets to touch the home afterwards, least of all telemetry.
1100
+ if (!result.purged) {
1101
+ await track("cli_uninstall_command", {
1102
+ ok: result.exitCode === 0,
1103
+ purge,
1104
+ dry_run: dryRun,
1105
+ });
1106
+ }
1107
+ lastSubcommand = null;
1108
+ await exitAfterFlush(result.exitCode);
1109
+ return;
1110
+ }
1111
+
304
1112
  // policies [--install|-i|--uninstall|-u|--help|-h] [names...] [--scope] [--beta] [--custom|-c <path>]
305
1113
  if (args[0] === "policies") {
306
1114
  const subArgs = args.slice(1);
@@ -566,19 +1374,6 @@ EXAMPLES
566
1374
  process.exit(0);
567
1375
  }
568
1376
 
569
- // auth — email-OTP login flow against the FailproofAI api-server.
570
- if (args[0] === "auth") {
571
- lastSubcommand = "auth";
572
- const { runAuthCli } = await import("../src/auth/cli");
573
- await runAuthCli(args.slice(1));
574
- await track("cli_auth_invoked", {
575
- args_count: args.length - 1,
576
- subcommand: args[1] ?? "help",
577
- exit_code: process.exitCode ?? 0,
578
- });
579
- process.exit(process.exitCode ?? 0);
580
- }
581
-
582
1377
  // audit — scan local agent-CLI history, then launch the dashboard at /audit.
583
1378
  if (args[0] === "audit") {
584
1379
  lastSubcommand = "audit";
@@ -765,23 +1560,203 @@ USAGE
765
1560
  WHAT IT DOES
766
1561
  Walks you through 4 quick steps and writes everything for you:
767
1562
  1. Where — global (all projects) or just this project
768
- 2. Assistants — which agent CLIs to protect (Claude, Codex, ...)
1563
+ 2. Harnesses — which agent CLIs to protect (Claude, Codex, ...)
769
1564
  3. Policies — presets (combine any), Everything, or a custom pick
770
1565
  4. Review — confirms the exact files it will change, then applies
771
1566
 
1567
+ FAILPROOF CLOUD
1568
+ failproofai config --connect <url> --token <key> [--machine-id <id>]
1569
+ Connect this machine to Failproof Cloud
1570
+ [--no-transcripts] decisions only, no transcripts
1571
+ failproofai config --disconnect Stop pulling policy and sending activity
1572
+ failproofai config --status Show connection, daemon and pause state
1573
+ failproofai config --pause [--session <id>]
1574
+ Pause enforcement, time-boxed
1575
+ failproofai config --resume [--all]
1576
+ Resume enforcement
1577
+
1578
+ --machine-label <name> Human-readable name in the dashboard
1579
+ (use alone to rename an already-connected machine)
1580
+
1581
+ One connection, two capabilities: this machine PULLS centrally-managed
1582
+ policies and SENDS what its hooks decided, so the dashboard shows the fleet
1583
+ it is enforcing on. Both are checked against the server before anything is
1584
+ written, and reported separately — a key carrying policies:pull but not
1585
+ events:add connects for policy and says exactly why the dashboard is empty.
1586
+
1587
+ Tokens are stored owner-only in ~/.failproofai/, never in the service unit —
1588
+ that file is world-readable. Connecting needs no sudo, and the machine id
1589
+ defaults to this host's name.
1590
+
1591
+ Connecting sends BOTH policy decisions and full session transcripts. A
1592
+ transcript carries prompts, file contents and whatever was pasted into a
1593
+ terminal — that is the point of connecting, and it is stated here rather than
1594
+ buried behind a flag nobody finds. Use --no-transcripts for decisions only.
1595
+
1596
+ PAUSING ENFORCEMENT (one session, always time-boxed)
1597
+ failproofai config --pause Pause this directory's newest agent session (30m)
1598
+ failproofai config --pause 10m Pause for a given time (max 8h; s/m/h, bare = minutes)
1599
+ failproofai config --resume End the pause early
1600
+ failproofai config --status Show what is paused and when it lifts
1601
+ --session <id> Target a specific session
1602
+ --all With --resume, end every active pause
1603
+
1604
+ A pause suspends builtin, custom and convention policies for that session
1605
+ only, and always expires on its own. Cloud-managed policies keep enforcing.
1606
+
772
1607
  Prefer flags? See \`failproofai policies --help\`.
773
1608
  `.trimStart());
774
1609
  process.exit(0);
775
1610
  }
776
1611
  lastSubcommand = "config";
1612
+
1613
+ // --pause / --resume / --status are non-interactive session actions that
1614
+ // share `config`'s surface but not the wizard. They write session state,
1615
+ // never the config file — a pause that reached policies-config.json would
1616
+ // be committed and outlive the session that asked for it.
1617
+ // Cloud enrolment. Deliberately writes a credential file the daemon reads
1618
+ // rather than an Environment= line in the service unit: that unit is
1619
+ // installed world-readable (0644, /etc/systemd/system), so a token there
1620
+ // would be readable by every local user. Keeping it out also means no
1621
+ // sudo, and lets an already-installed daemon be connected.
1622
+ const connectIdx = args.indexOf("--connect");
1623
+ const wantsDisconnect = args.includes("--disconnect");
1624
+ // `--machine-label` ALONE is a rename, not an enrolment. Alongside --connect it
1625
+ // keeps its old meaning (the name to enrol under); on its own it changes the
1626
+ // name of a machine that is already connected, which previously required
1627
+ // re-running enrolment with the url and token again just to fix a display name.
1628
+ const wantsRename =
1629
+ connectIdx < 0 && !wantsDisconnect && args.includes("--machine-label");
1630
+ if (connectIdx >= 0 || wantsDisconnect || wantsRename) {
1631
+ if (connectIdx >= 0 && wantsDisconnect) {
1632
+ throw new CliError("--connect and --disconnect cannot be combined.");
1633
+ }
1634
+ const valueAfter = (flag) => {
1635
+ const i = args.indexOf(flag);
1636
+ if (i < 0) return undefined;
1637
+ const v = args[i + 1];
1638
+ if (!v || v.startsWith("-")) throw new CliError(`Missing value after ${flag}.`);
1639
+ return v;
1640
+ };
1641
+ let result;
1642
+ if (wantsRename) {
1643
+ const { runRenameCommand } = await import("../src/hooks/cloud-enrollment-cli");
1644
+ result = await runRenameCommand(valueAfter("--machine-label"));
1645
+ } else if (wantsDisconnect) {
1646
+ const { runDisconnectCommand } = await import("../src/hooks/cloud-enrollment-cli");
1647
+ result = runDisconnectCommand();
1648
+ } else {
1649
+ const { hostname } = await import("node:os");
1650
+ const { runConnectCommand } = await import("../src/hooks/cloud-enrollment-cli");
1651
+ result = await runConnectCommand({
1652
+ url: valueAfter("--connect"),
1653
+ token: valueAfter("--token"),
1654
+ machineId: valueAfter("--machine-id"),
1655
+ machineLabel: valueAfter("--machine-label"),
1656
+ defaultMachineId: hostname(),
1657
+ // Transcripts are what connecting is FOR, so they default on and the
1658
+ // disclosure is made at the point of connection rather than hidden
1659
+ // behind an opt-in flag most people never discover — a dashboard
1660
+ // showing only decisions is the empty-dashboard problem in a
1661
+ // different costume. --no-transcripts is the explicit way out, and
1662
+ // `failproofai config --status` always says which is in effect.
1663
+ sessions: !args.includes("--no-transcripts"),
1664
+ });
1665
+ }
1666
+ for (const line of result.lines) {
1667
+ if (result.exitCode === 0) console.log(line);
1668
+ else console.error(line);
1669
+ }
1670
+ await track("cli_cloud_enrollment", {
1671
+ action: wantsRename ? "rename" : wantsDisconnect ? "disconnect" : "connect",
1672
+ ok: result.exitCode === 0,
1673
+ });
1674
+ await exitAfterFlush(result.exitCode);
1675
+ return;
1676
+ }
1677
+
1678
+ const pauseIdx = args.indexOf("--pause");
1679
+ const wantsResume = args.includes("--resume");
1680
+ const wantsStatus = args.includes("--status");
1681
+ if (pauseIdx >= 0 || wantsResume || wantsStatus) {
1682
+ const chosen = [pauseIdx >= 0 && "--pause", wantsResume && "--resume", wantsStatus && "--status"].filter(Boolean);
1683
+ if (chosen.length > 1) {
1684
+ throw new CliError(`${chosen.join(" and ")} cannot be combined.`);
1685
+ }
1686
+ const sessionIdx = args.indexOf("--session");
1687
+ if (sessionIdx >= 0 && !args[sessionIdx + 1]) {
1688
+ throw new CliError("Missing session id after --session.");
1689
+ }
1690
+ // A bare `--pause` takes the default duration, so only treat the next
1691
+ // token as a duration when it isn't another flag.
1692
+ const next = pauseIdx >= 0 ? args[pauseIdx + 1] : undefined;
1693
+ const duration = next && !next.startsWith("-") ? next : undefined;
1694
+
1695
+ const { runPauseCommand } = await import("../src/hooks/session-pause-cli");
1696
+ const result = runPauseCommand({
1697
+ action: pauseIdx >= 0 ? "pause" : wantsResume ? "resume" : "status",
1698
+ duration,
1699
+ sessionId: sessionIdx >= 0 ? args[sessionIdx + 1] : undefined,
1700
+ all: args.includes("--all"),
1701
+ cwd: process.cwd(),
1702
+ });
1703
+ // `--status` answers "what is this machine's state?", which is both
1704
+ // halves: whether enforcement is paused AND whether cloud is connected.
1705
+ if (wantsStatus) {
1706
+ const { connectionStatusLines } = await import("../src/hooks/cloud-enrollment-cli");
1707
+ for (const line of connectionStatusLines()) console.log(line);
1708
+ // Always printed, including where reports can never work: "why am I
1709
+ // not getting them?" is the question --status exists to answer, and an
1710
+ // omitted line answers it with silence.
1711
+ console.log("");
1712
+ }
1713
+ for (const line of result.lines) {
1714
+ if (result.exitCode === 0) console.log(line);
1715
+ else console.error(line);
1716
+ }
1717
+ await track("cli_pause_invoked", {
1718
+ action: pauseIdx >= 0 ? "pause" : wantsResume ? "resume" : "status",
1719
+ ok: result.exitCode === 0,
1720
+ affected: result.affected,
1721
+ });
1722
+ await exitAfterFlush(result.exitCode);
1723
+ return;
1724
+ }
1725
+
777
1726
  const { runConfigureWizard } = await import("../src/hooks/configure-wizard");
778
1727
  const result = await runConfigureWizard();
779
1728
  await track("cli_configure_invoked", {
780
1729
  applied: result.applied,
781
- scope: result.scope ?? null,
1730
+ // `target` and `scopes`, not `scope`: the wizard rework replaced that
1731
+ // field and nothing caught it, because `.mjs` is outside the tsconfig
1732
+ // include so `tsc --noEmit` never type-checks this file. Every
1733
+ // `cli_configure_invoked` since has reported `scope: null`.
1734
+ target: result.target ?? null,
1735
+ scopes: result.scopes ?? [],
782
1736
  cli_count: result.clis?.length ?? 0,
1737
+ abort: result.abort ?? null,
783
1738
  });
784
- process.exit(0);
1739
+ // `abort` is the field `WizardAbort` exists to expose, and exiting 0
1740
+ // regardless discarded it: a fleet script could not tell "the user pressed
1741
+ // Esc" from "this machine could not install the required daemon and is
1742
+ // unconfigured". Cancelling is not a failure; the other two are.
1743
+ await exitAfterFlush(!result.applied && result.abort && result.abort !== "cancelled" ? 1 : 0);
1744
+ return;
1745
+ }
1746
+
1747
+ // Shared by both "unknown thing" guards below, so a mistyped SUBCOMMAND gets
1748
+ // the same nearest-match treatment a mistyped flag already got.
1749
+ function levenshtein(a, b) {
1750
+ const m = a.length, n = b.length;
1751
+ const dp = Array.from({ length: m + 1 }, (_, i) =>
1752
+ Array.from({ length: n + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0))
1753
+ );
1754
+ for (let i = 1; i <= m; i++)
1755
+ for (let j = 1; j <= n; j++)
1756
+ dp[i][j] = a[i - 1] === b[j - 1]
1757
+ ? dp[i - 1][j - 1]
1758
+ : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
1759
+ return dp[m][n];
785
1760
  }
786
1761
 
787
1762
  // Unknown flag guard — must appear after all known-flag branches
@@ -789,20 +1764,7 @@ WHAT IT DOES
789
1764
  const unknownFlag = args.find(a => a.startsWith("-") && !knownFlags.includes(a));
790
1765
 
791
1766
  if (unknownFlag) {
792
- function levenshtein(a, b) {
793
- const m = a.length, n = b.length;
794
- const dp = Array.from({ length: m + 1 }, (_, i) =>
795
- Array.from({ length: n + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0))
796
- );
797
- for (let i = 1; i <= m; i++)
798
- for (let j = 1; j <= n; j++)
799
- dp[i][j] = a[i - 1] === b[j - 1]
800
- ? dp[i - 1][j - 1]
801
- : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
802
- return dp[m][n];
803
- }
804
-
805
- const primary = ["--version", "--help", "--hook", "policies", "policy", "auth", "audit"];
1767
+ const primary = ["--version", "--help", "--hook", "policies", "policy", "audit"];
806
1768
  const closest = primary.reduce((best, flag) => {
807
1769
  const dist = levenshtein(unknownFlag, flag);
808
1770
  return dist < best.dist ? { flag, dist } : best;
@@ -818,29 +1780,27 @@ WHAT IT DOES
818
1780
  // Unknown subcommand guard (non-flag args that aren't a known subcommand)
819
1781
  const unknownSubcommand = args.find(a => !a.startsWith("-") && !SUBCOMMANDS.includes(a));
820
1782
  if (unknownSubcommand) {
1783
+ // Nearest match rather than a hardcoded "policies", which was wrong for
1784
+ // every input that was not a typo of it. `auth` made that concrete: it was
1785
+ // a real subcommand until this release, so an old script or plain muscle
1786
+ // memory lands here, and answering "did you mean policies?" sends someone
1787
+ // to the one command that has nothing to do with what they typed.
1788
+ const nearest = SUBCOMMANDS.reduce(
1789
+ (best, name) => {
1790
+ const dist = levenshtein(unknownSubcommand, name);
1791
+ return dist < best.dist ? { name, dist } : best;
1792
+ },
1793
+ { name: SUBCOMMANDS[0], dist: Infinity },
1794
+ );
821
1795
  throw new CliError(
822
1796
  `Unknown command: ${unknownSubcommand}\n` +
823
- `Did you mean: failproofai policies?\n` +
1797
+ `Did you mean: failproofai ${nearest.name}?\n` +
824
1798
  `Run \`failproofai --help\` for usage details.`
825
1799
  );
826
1800
  }
827
1801
 
828
- // First-run onboarding — on the first bare `failproofai` invocation, run the
829
- // configure wizard (which also fires the post-setup audit) BEFORE the
830
- // dashboard. Unlike before, we then fall through to launch the dashboard, so a
831
- // fresh user gets: setup → audit → dashboard, and every later `failproofai`
832
- // goes straight to the dashboard. Best-effort: any error must not block launch.
833
- if (args.length === 0) {
834
- try {
835
- const { maybeFirstRunConfigure } = await import("../src/hooks/configure-wizard");
836
- await maybeFirstRunConfigure();
837
- } catch {
838
- // First-run onboarding is non-critical; fall through to the dashboard.
839
- }
840
- }
841
-
842
1802
  // Dashboard launch — always production mode. Runs on every bare `failproofai`
843
- // (after first-run onboarding, if any).
1803
+ // (first-run onboarding, if any, already ran above).
844
1804
  const { launch } = await import("../scripts/launch");
845
1805
  launch("start");
846
1806
  }