failproofai 1.0.4-beta.7 → 1.0.4

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 (221) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +5 -5
  3. package/.next/standalone/.next/prerender-manifest.json +4 -4
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +2 -2
  6. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page.js +2 -2
  8. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  9. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  11. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  12. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
  13. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  14. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +2 -2
  16. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  17. package/.next/standalone/.next/server/app/_not-found/page.js +2 -2
  18. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  19. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  20. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  21. package/.next/standalone/.next/server/app/_not-found.rsc +14 -14
  22. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
  23. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +13 -13
  24. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  25. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/audit/run/route.js +4 -4
  27. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/audit/page/build-manifest.json +2 -2
  35. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +4 -37
  36. package/.next/standalone/.next/server/app/audit/page.js +2 -2
  37. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  39. package/.next/standalone/.next/server/app/index.html +1 -1
  40. package/.next/standalone/.next/server/app/index.rsc +14 -14
  41. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +13 -13
  42. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
  43. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  44. package/.next/standalone/.next/server/app/page/build-manifest.json +2 -2
  45. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  46. package/.next/standalone/.next/server/app/page.js +2 -2
  47. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  49. package/.next/standalone/.next/server/app/policies/page/build-manifest.json +2 -2
  50. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  51. package/.next/standalone/.next/server/app/policies/page.js +4 -4
  52. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  53. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  54. package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +2 -2
  55. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  56. package/.next/standalone/.next/server/app/project/[name]/page.js +3 -3
  57. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  58. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  59. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +2 -2
  60. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  61. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  62. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +4 -4
  63. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  64. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  65. package/.next/standalone/.next/server/app/projects/page/build-manifest.json +2 -2
  66. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  67. package/.next/standalone/.next/server/app/projects/page.js +3 -3
  68. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  69. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  70. package/.next/standalone/.next/server/app/settings/page/build-manifest.json +2 -2
  71. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -15
  72. package/.next/standalone/.next/server/app/settings/page.js +4 -4
  73. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  74. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  75. package/.next/standalone/.next/server/chunks/{[externals]__0v1hz15._.js → [externals]__0-9ti7z._.js} +1 -1
  76. package/.next/standalone/.next/server/chunks/{[externals]__1nb206a._.js → [externals]__0u3wrvq._.js} +1 -1
  77. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
  78. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0rv7m0k._.js +1 -1
  79. package/.next/standalone/.next/server/chunks/[root-of-the-server]__17g9wh7._.js +1 -1
  80. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1legmza._.js +1 -1
  81. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1p-qi2t._.js +1 -1
  82. package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
  83. package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
  84. package/.next/standalone/.next/server/chunks/{_0otft92._.js → _1-akg6w._.js} +2 -2
  85. package/.next/standalone/.next/server/chunks/_1ek68ln._.js +10 -10
  86. package/.next/standalone/.next/server/chunks/_1vslkoe._.js +1 -1
  87. package/.next/standalone/.next/server/chunks/lib_telemetry_ts_0dctyyw._.js +1 -1
  88. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  89. package/.next/standalone/.next/server/chunks/src_hooks_fp-home_ts_09kv0bn._.js +1 -1
  90. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1fvfw59._.js → [root-of-the-server]__00-s7h8._.js} +1 -1
  91. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +4 -0
  92. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +4 -0
  93. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +4 -0
  94. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__12hbpha._.js → [root-of-the-server]__09flfsx._.js} +2 -2
  95. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +4 -0
  96. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +4 -0
  97. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0kokc8z._.js → [root-of-the-server]__0n_lxhg._.js} +1 -1
  98. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +1 -1
  99. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +4 -0
  100. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s740oi._.js +6 -0
  101. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0dxf928._.js → [root-of-the-server]__190won8._.js} +1 -1
  102. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0n2txhk._.js → [root-of-the-server]__1ctpynv._.js} +1 -1
  103. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +4 -0
  104. package/.next/standalone/.next/server/chunks/ssr/_02yoeej._.js +3 -0
  105. package/.next/standalone/.next/server/chunks/ssr/{_0a0zlpr._.js → _04rbnaq._.js} +2 -2
  106. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  107. package/.next/standalone/.next/server/chunks/ssr/{_0s-77v5._.js → _0h_douw._.js} +1 -1
  108. package/.next/standalone/.next/server/chunks/ssr/_1_ozgif._.js +1 -1
  109. package/.next/standalone/.next/server/chunks/ssr/_1ddpc5g._.js +1 -1
  110. package/.next/standalone/.next/server/chunks/ssr/_1es2j7i._.js +14 -51
  111. package/.next/standalone/.next/server/chunks/ssr/{_07t3jol._.js → _1u8-lu2._.js} +2 -2
  112. package/.next/standalone/.next/server/chunks/ssr/_1ylae7o._.js +3 -0
  113. package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +3 -0
  114. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +2 -2
  115. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +69 -1
  116. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  117. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
  118. package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
  119. package/.next/standalone/.next/server/chunks/ssr/node_modules_13n822a._.js +1 -1
  120. package/.next/standalone/.next/server/chunks/ssr/node_modules_html-to-image_es_index_1ao30b1.js +3 -0
  121. package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +1 -1
  122. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589g._.js +1 -1
  123. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
  124. package/.next/standalone/.next/server/middleware-build-manifest.js +5 -5
  125. package/.next/standalone/.next/server/pages/404.html +1 -1
  126. package/.next/standalone/.next/server/pages/500.html +1 -1
  127. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  128. package/.next/standalone/.next/server/server-reference-manifest.json +21 -65
  129. package/.next/standalone/.next/static/chunks/{0f1pznnc7zlub.js → 0-tyrpwuc3lla.js} +1 -1
  130. package/.next/standalone/.next/static/chunks/0ca7octhod0g2.js +69 -0
  131. package/.next/standalone/.next/static/chunks/0u64yey446k4f.css +1 -0
  132. package/.next/standalone/.next/static/chunks/{1csubx9gwhsf6.js → 1j6o9min_6qa9.js} +1 -1
  133. package/.next/standalone/.next/static/chunks/27cl8h-c2w7cd.js +1 -0
  134. package/.next/standalone/.next/static/chunks/{0jpd8dv2vk930.css → 29-iqnp9v_axt.css} +1 -1
  135. package/.next/standalone/.next/static/chunks/{3uyhmm01g64k4.css → 2k9f4tyv04809.css} +0 -1
  136. package/.next/standalone/.next/static/chunks/{0t6ea7d3bkmdv.js → 2lwipmv5etc8m.js} +1 -1
  137. package/.next/standalone/.next/static/chunks/2pd16tn5qmx13.js +1 -0
  138. package/.next/standalone/.next/static/chunks/{0ntdfr2552x-h.js → 2u8vh73tpw28_.js} +1 -1
  139. package/.next/standalone/.next/static/chunks/{15uon66hpwbgg.js → 31u94yukw9fz-.js} +1 -1
  140. package/.next/standalone/.next/static/chunks/{2pwfrn6_mw-1u.js → 3p9_8x5ezce-9.js} +1 -1
  141. package/.next/standalone/.next/static/chunks/3qfoeqhy5l10x.js +1 -0
  142. package/.next/standalone/.next/static/chunks/{083fk73jtv9a0.js → 3tjji451hrgc0.js} +1 -1
  143. package/.next/standalone/.next/static/chunks/3zkg2s2vzxc3d.js +1 -0
  144. package/.next/standalone/.next/static/chunks/{turbopack-3s3c-u_u_l0u9.js → turbopack-3p73obe0xmrxm.js} +1 -1
  145. package/.next/standalone/.opencode/plugins/failproofai.mjs +27 -51
  146. package/.next/standalone/app/actions/get-scheduled-audit.ts +0 -5
  147. package/.next/standalone/app/actions/update-scheduled-audit.ts +57 -18
  148. package/.next/standalone/app/api/auth/status/route.ts +2 -3
  149. package/.next/standalone/app/audit/_components/audit-dashboard.tsx +34 -140
  150. package/.next/standalone/app/audit/_components/audit-poster.tsx +10 -29
  151. package/.next/standalone/app/audit/_components/come-back-better-section.tsx +1 -6
  152. package/.next/standalone/app/audit/_components/empty-state.tsx +1 -4
  153. package/.next/standalone/app/audit/_components/how-to-improve-section.tsx +5 -19
  154. package/.next/standalone/app/audit/_components/share-templates.ts +44 -110
  155. package/.next/standalone/app/audit/audit-styles.css +1 -132
  156. package/.next/standalone/app/settings/page.tsx +1 -1
  157. package/.next/standalone/app/settings/settings-client.tsx +45 -50
  158. package/.next/standalone/lib/auth/api-server-client.ts +5 -33
  159. package/.next/standalone/lib/claude-sessions.ts +27 -101
  160. package/.next/standalone/lib/codex-sessions.ts +4 -20
  161. package/.next/standalone/lib/copilot-sessions.ts +1 -32
  162. package/.next/standalone/lib/cursor-sessions.ts +5 -62
  163. package/.next/standalone/lib/sqlite-reader.ts +1 -78
  164. package/.next/standalone/package.json +9 -9
  165. package/.next/standalone/server.js +1 -1
  166. package/bin/failproofai.mjs +1 -1
  167. package/dist/cli.mjs +500 -1762
  168. package/dist/worker.mjs +16 -138
  169. package/lib/auth/api-server-client.ts +5 -33
  170. package/lib/claude-sessions.ts +27 -101
  171. package/lib/codex-sessions.ts +4 -20
  172. package/lib/copilot-sessions.ts +1 -32
  173. package/lib/cursor-sessions.ts +5 -62
  174. package/lib/sqlite-reader.ts +1 -78
  175. package/package.json +9 -9
  176. package/pi-extension/index.ts +5 -50
  177. package/src/audit/cache.ts +0 -6
  178. package/src/audit/cli-adapters/claude.ts +0 -13
  179. package/src/audit/cli-login.ts +1 -4
  180. package/src/audit/cli.ts +7 -157
  181. package/src/audit/harm-report.ts +4 -111
  182. package/src/audit/index.ts +15 -286
  183. package/src/audit/machine-store.ts +9 -8
  184. package/src/audit/redact-example.ts +15 -324
  185. package/src/audit/report-harm.ts +37 -33
  186. package/src/audit/report.ts +3 -6
  187. package/src/audit/schedule-cli.ts +33 -54
  188. package/src/audit/scoring.ts +0 -49
  189. package/src/audit/types.ts +0 -52
  190. package/src/hooks/builtin-policies.ts +2 -67
  191. package/src/hooks/configure-wizard.ts +0 -55
  192. package/src/hooks/fp-config.ts +48 -83
  193. package/src/hooks/fp-home.ts +0 -27
  194. package/src/hooks/integrations.ts +14 -51
  195. package/src/hooks/uninstall-cli.ts +2 -17
  196. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0-4ja6q._.js +0 -4
  197. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__00cr3xu._.js +0 -4
  198. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04px77e._.js +0 -4
  199. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0egr5s1._.js +0 -6
  200. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0i0l1di._.js +0 -4
  201. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0r3c1_0._.js +0 -4
  202. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__17xnxlx._.js +0 -4
  203. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ckpmkk._.js +0 -4
  204. package/.next/standalone/.next/server/chunks/ssr/_04ng_6x._.js +0 -3
  205. package/.next/standalone/.next/server/chunks/ssr/_0l2hi_d._.js +0 -3
  206. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_rerun-button_tsx_0blihgw._.js +0 -3
  207. package/.next/standalone/.next/static/chunks/0divvxng208xo.js +0 -1
  208. package/.next/standalone/.next/static/chunks/27du9n7ssja7l.js +0 -1
  209. package/.next/standalone/.next/static/chunks/2h_pcb1y8_jde.js +0 -1
  210. package/.next/standalone/app/actions/get-leaks.ts +0 -104
  211. package/.next/standalone/app/audit/_components/leak-section.tsx +0 -192
  212. package/src/audit/desktop-notify.ts +0 -420
  213. package/src/audit/leak-fingerprint.ts +0 -198
  214. package/src/audit/leak-record.ts +0 -248
  215. package/src/audit/leak-scan.ts +0 -268
  216. package/src/audit/leak-store.ts +0 -220
  217. package/src/audit/macos-notifier.ts +0 -298
  218. package/src/hooks/notice.ts +0 -24
  219. /package/.next/standalone/.next/static/{YiPsVpjrc-pEQoN7XvhZ0 → dg8VQ7Ejlo0Qsh_x92oIt}/_buildManifest.js +0 -0
  220. /package/.next/standalone/.next/static/{YiPsVpjrc-pEQoN7XvhZ0 → dg8VQ7Ejlo0Qsh_x92oIt}/_clientMiddlewareManifest.js +0 -0
  221. /package/.next/standalone/.next/static/{YiPsVpjrc-pEQoN7XvhZ0 → dg8VQ7Ejlo0Qsh_x92oIt}/_ssgManifest.js +0 -0
@@ -127,15 +127,7 @@ interface CursorRecord {
127
127
  /** Cursor 2026-04+ transcript shape: `{role, message: {content: [...]}}`. */
128
128
  role?: "user" | "assistant" | "system" | string;
129
129
  message?: {
130
- content?: Array<{
131
- type?: string;
132
- text?: string;
133
- id?: string;
134
- name?: string;
135
- input?: Record<string, unknown>;
136
- tool_use_id?: string;
137
- content?: unknown;
138
- }>;
130
+ content?: Array<{ type?: string; text?: string }>;
139
131
  };
140
132
  }
141
133
 
@@ -191,36 +183,11 @@ export async function parseCursorLog(
191
183
  if (!raw.type && raw.role && raw.message?.content) {
192
184
  const synthDate = new Date(SYNTH_T0 + i);
193
185
  const synthTs = synthDate.toISOString();
194
- const messageContent = raw.message.content;
195
- const textParts = messageContent
186
+ const textParts = raw.message.content
196
187
  .filter((c) => c?.type === "text" && typeof c.text === "string")
197
188
  .map((c) => c.text!)
198
189
  .join("");
199
190
  if (raw.role === "user") {
200
- // Some Cursor builds emit Claude-style tool_result blocks in the next
201
- // user record. Pair them before treating any remaining text as a user
202
- // turn so command output is available to the credential scanner.
203
- let attachedResult = false;
204
- for (const block of messageContent) {
205
- if (block?.type !== "tool_result") continue;
206
- const callId = typeof block.tool_use_id === "string" ? block.tool_use_id : undefined;
207
- const toolUse = callId ? toolUseById.get(callId) : undefined;
208
- if (!toolUse) continue;
209
- const durationMs = Math.max(
210
- 0,
211
- synthDate.getTime() - (toolUseStartMs.get(callId!) ?? synthDate.getTime()),
212
- );
213
- toolUse.result = {
214
- timestamp: synthTs,
215
- timestampFormatted: formatTimestamp(synthDate),
216
- content: typeof block.content === "string"
217
- ? block.content
218
- : block.content == null ? "" : JSON.stringify(block.content),
219
- durationMs,
220
- durationFormatted: formatDuration(durationMs),
221
- };
222
- attachedResult = true;
223
- }
224
191
  // Strip the synthesized `<timestamp>...</timestamp>\n<user_query>...\n</user_query>`
225
192
  // wrapper Cursor adds for context — keep just the user_query body.
226
193
  const queryMatch = /<user_query>\s*([\s\S]*?)\s*<\/user_query>/.exec(textParts);
@@ -231,37 +198,13 @@ export async function parseCursorLog(
231
198
  ...baseEntry(rawCopy, synthTs, synthDate, source),
232
199
  message: { role: "user", content: text },
233
200
  } satisfies UserEntry);
234
- } else if (!attachedResult) {
235
- entries.push({
236
- type: "system",
237
- ...baseEntry(rawCopy, synthTs, synthDate, source),
238
- raw: rawCopy,
239
- } satisfies GenericEntry);
240
201
  }
241
202
  continue;
242
203
  }
243
204
  if (raw.role === "assistant") {
244
- const blocks: ContentBlock[] = [];
245
- if (textParts) blocks.push({ type: "text", text: textParts });
246
- for (let j = 0; j < messageContent.length; j++) {
247
- const contentBlock = messageContent[j];
248
- if (contentBlock?.type !== "tool_use") continue;
249
- const name = typeof contentBlock.name === "string" ? contentBlock.name : "tool";
250
- const id = typeof contentBlock.id === "string" && contentBlock.id
251
- ? contentBlock.id
252
- : `cursor-${i}-${name}-${j}`;
253
- const toolUse: ToolUseBlock = {
254
- type: "tool_use",
255
- id,
256
- name,
257
- input: contentBlock.input && typeof contentBlock.input === "object"
258
- ? contentBlock.input
259
- : {},
260
- };
261
- blocks.push(toolUse);
262
- toolUseById.set(id, toolUse);
263
- toolUseStartMs.set(id, synthDate.getTime());
264
- }
205
+ const blocks: ContentBlock[] = textParts
206
+ ? [{ type: "text", text: textParts }]
207
+ : [];
265
208
  if (blocks.length === 0) {
266
209
  entries.push({
267
210
  type: "system",
@@ -39,42 +39,8 @@ interface NodeSqliteModule {
39
39
  DatabaseSync: new (path: string, options?: { readOnly?: boolean }) => NodeSqliteDb;
40
40
  }
41
41
 
42
- /**
43
- * Swallow Node's "SQLite is an experimental feature" warning, once.
44
- *
45
- * It is not information the user can act on — we chose `node:sqlite`
46
- * deliberately, and the alternative tier is a WASM build that leaks. But it is
47
- * emitted on first use, which lands it in the MIDDLE of whatever is on screen,
48
- * and the audit's progress display redraws itself by moving the cursor up a
49
- * fixed number of lines. Two unexpected lines of stderr push the cursor down
50
- * two rows, so the next redraw repaints the block two rows lower and strands
51
- * the top of the previous frame above it. The user sees every stage twice and
52
- * reasonably concludes the audit ran twice.
53
- *
54
- * Filtered by message rather than silenced wholesale: `process.emitWarning` is
55
- * how deprecations and real problems surface too, and a blanket
56
- * `NODE_NO_WARNINGS` in a tool that installs into other people's machines would
57
- * hide those as well.
58
- */
59
- let warningPatched = false;
60
- function silenceSqliteExperimentalWarning(): void {
61
- if (warningPatched) return;
62
- warningPatched = true;
63
- const original = process.emitWarning.bind(process);
64
- // The overloads differ across Node versions; the shape we care about is the
65
- // first argument, and everything else is passed straight through.
66
- process.emitWarning = ((warning: unknown, ...rest: unknown[]) => {
67
- const text = typeof warning === "string" ? warning : String((warning as Error)?.message ?? "");
68
- if (text.includes("SQLite is an experimental feature")) return;
69
- return (original as (...a: unknown[]) => void)(warning, ...rest);
70
- }) as typeof process.emitWarning;
71
- }
72
-
73
42
  async function tryNodeSqlite(dbPath: string): Promise<SqliteReader | null> {
74
43
  try {
75
- // Before the import, not after: the warning fires on first use of the
76
- // module, which is this line.
77
- silenceSqliteExperimentalWarning();
78
44
  // Dynamic import: on Node < 22.5 this rejects and we fall through to sql.js.
79
45
  const mod = (await import("node:sqlite")) as unknown as NodeSqliteModule;
80
46
  const db = new mod.DatabaseSync(dbPath, { readOnly: true });
@@ -100,53 +66,10 @@ function loadSqlJs(): Promise<SqlJsStatic> {
100
66
  return sqlPromise;
101
67
  }
102
68
 
103
- const SQLITE_MAGIC = "SQLite format 3\0";
104
-
105
- /**
106
- * Clear the WAL flag on OUR OWN in-memory copy of the database image.
107
- *
108
- * Header bytes 18 and 19 are the file-format write and read versions; `2`
109
- * means WAL. sql.js never sees the `-wal` sidecar — it is handed the main
110
- * file's bytes and nothing else, which is exactly the snapshot caveat this
111
- * module already documents — so the flag buys us nothing, and it costs a great
112
- * deal: opening a WAL-flagged image makes SQLite build a wal-index
113
- * shared-memory region that sql.js's MEMFS-backed VFS never reclaims on
114
- * `close()` (close unlinks only the db file it created).
115
- *
116
- * The asm.js build's heap is a FIXED 22,151,168-byte ArrayBuffer with
117
- * ALLOW_MEMORY_GROWTH off, so `emscripten_resize_heap` is compiled to
118
- * `abort("OOM")`. Measured: the 128th WAL-mode open exhausts it — the same
119
- * threshold for a 319 KB database and a 10 MB one, because the leaked region
120
- * is a fixed per-connection allocation rather than data.
121
- *
122
- * And the abort is not confined to that open. `initSqlJs` memoizes one
123
- * process-wide module, so once it aborts every later `openSqliteReadonly`
124
- * silently returns null. A full audit opens ~150 databases — devin, goose and
125
- * opencode each open theirs once per SESSION — so it crossed 128 and then
126
- * dropped whatever it had not yet read, with no error anywhere. The
127
- * `Aborted(OOM)` lines on stderr were the only symptom, and they read as
128
- * harmless because the run still completed.
129
- *
130
- * Only under bun and Node < 22.5: tier 1 is `node:sqlite`, which bun does not
131
- * provide, and `engines.node` allows 20.9. Both fall through to here.
132
- */
133
- function clearWalFlag(bytes: Buffer): Buffer {
134
- // Too short to carry a header, or not a SQLite image at all — hand it back
135
- // untouched and let sql.js produce its own error.
136
- if (bytes.length < 20) return bytes;
137
- if (bytes.toString("latin1", 0, 16) !== SQLITE_MAGIC) return bytes;
138
- if (bytes[18] === 1 && bytes[19] === 1) return bytes; // already rollback-journal
139
- bytes[18] = 1;
140
- bytes[19] = 1;
141
- return bytes;
142
- }
143
-
144
69
  async function trySqlJs(dbPath: string): Promise<SqliteReader | null> {
145
70
  try {
146
71
  const SQL = await loadSqlJs();
147
- // `readFileSync` already returns a private copy, so mutating the two header
148
- // bytes cannot touch the file on disk.
149
- const db = new SQL.Database(clearWalFlag(readFileSync(dbPath)));
72
+ const db = new SQL.Database(readFileSync(dbPath));
150
73
  return {
151
74
  query<T = Record<string, unknown>>(sql: string, params?: unknown[]): T[] {
152
75
  const stmt = db.prepare(sql);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "failproofai",
3
- "version": "1.0.4-beta.7",
3
+ "version": "1.0.4",
4
4
  "description": "Observability and enforcement for AI agent harnesses. 39 built-in policies hooked into 12 of them — Claude Code, Codex, Cursor, Hermes, OpenClaw and more — blocking the tool call before it runs. Local dashboard included, no account needed.",
5
5
  "bin": {
6
6
  "failproofai": "./dist/cli.mjs",
@@ -121,15 +121,15 @@
121
121
  "browserslist": "4.28.8"
122
122
  },
123
123
  "optionalDependencies": {
124
- "@failproofai/failproofaid-linux-x64": "1.0.4-beta.7",
125
- "@failproofai/failproofaid-linux-arm64": "1.0.4-beta.7",
126
- "@failproofai/failproofaid-darwin-x64": "1.0.4-beta.7",
127
- "@failproofai/failproofaid-darwin-arm64": "1.0.4-beta.7"
124
+ "@failproofai/failproofaid-linux-x64": "1.0.4",
125
+ "@failproofai/failproofaid-linux-arm64": "1.0.4",
126
+ "@failproofai/failproofaid-darwin-x64": "1.0.4",
127
+ "@failproofai/failproofaid-darwin-arm64": "1.0.4"
128
128
  },
129
129
  "failproofaidBinaries": {
130
- "linux-x64": "e59735a8f94cd8ac697b2bd67aafdd8d76a8394d53db229876980cc13a18f07e",
131
- "linux-arm64": "b9ce50dd2f79b55f4f7112add265a776a4a6d002c2a56f971e666fbb796f3906",
132
- "darwin-x64": "b46efc8ec33945a5cc26b7bb288fb87cea65b5983bd83094b5e607d53b8eda4c",
133
- "darwin-arm64": "a8cc6112cee5ca8a7d517900e9ce32118855135980f22979fa0ff7aa6f68d954"
130
+ "linux-x64": "cca1d41ff951784ba084bf2d72cfad0fe8d988f8226e0e7a4538b1fb8d76a5bd",
131
+ "linux-arm64": "55e9c1e2864cbf6057fd0c27e4bc573ca8fba2cd35d480a3db3a55d398e7bec0",
132
+ "darwin-x64": "2a46ec77d2cac14a036f35e832f92f16895239814a5f10f02121f3c313e70574",
133
+ "darwin-arm64": "a0e7ccc6fe24e1017592637192ba8a26c9d05ca839c98bc72a983094c863b39b"
134
134
  }
135
135
  }
@@ -25,7 +25,7 @@
25
25
  * spawns extensions with an undefined cwd contract, so paths are resolved
26
26
  * relative to this file via `import.meta.url`, NOT process.cwd().
27
27
  */
28
- import { spawn, spawnSync } from "node:child_process";
28
+ import { spawnSync } from "node:child_process";
29
29
  import { resolve, dirname, join } from "node:path";
30
30
  import { fileURLToPath } from "node:url";
31
31
  import { existsSync, readdirSync, statSync } from "node:fs";
@@ -64,47 +64,6 @@ function debug(msg: string): void {
64
64
  }
65
65
  }
66
66
 
67
- /**
68
- * Forward an event whose VERDICT NOBODY READS, without blocking Pi.
69
- *
70
- * `callPolicy` is `spawnSync`, and Pi awaits its handlers serially on the
71
- * startup path — so a `session_start` forward froze the whole TUI for as long
72
- * as the subprocess took (measured floor 1.0-1.6s just to boot the binary,
73
- * ceiling the full 60s timeout) and then threw the answer away. Three call
74
- * sites are like that by design: `session_start`, `tool_result` and
75
- * `session_shutdown` forward for the activity feed and for session-id
76
- * continuity, and Pi cannot act on their return in any case
77
- * (`ToolResultEventResult` has no `block`; the other two have no Result type
78
- * at all).
79
- *
80
- * So they get a detached `spawn` instead. The event still reaches failproofai,
81
- * the policies still run and still log, and Pi keeps rendering. Deliberately
82
- * NOT used for `tool_call`, `user_bash`, `input` or `agent_end`: those four
83
- * consume the decision, and blocking is what enforcement means.
84
- */
85
- function forwardPolicy(
86
- eventName: string,
87
- payload: unknown,
88
- ): void {
89
- const { cmd, args } = resolveSpawn();
90
- debug(`forwardPolicy (detached) event=${eventName} cmd=${cmd}`);
91
- try {
92
- const child = spawn(cmd, [...args, "--hook", eventName, "--cli", "pi"], {
93
- stdio: ["pipe", "ignore", "ignore"],
94
- detached: false,
95
- });
96
- // A child that outlives its parent's interest must not keep the event loop
97
- // alive, or Pi cannot exit until the subprocess does.
98
- child.unref();
99
- // EPIPE if the child died before stdin was consumed — fail-open, silently.
100
- child.on("error", (err) => debug(`forward error ${err.message}`));
101
- child.stdin?.on("error", () => {});
102
- child.stdin?.end(JSON.stringify(payload));
103
- } catch (err) {
104
- debug(`FORWARD EXCEPTION ${err instanceof Error ? err.message : String(err)}`);
105
- }
106
- }
107
-
108
67
  function callPolicy(eventName: string, payload: unknown): { block: boolean; reason: string } {
109
68
  const { cmd, args } = resolveSpawn();
110
69
  debug(`callPolicy event=${eventName} cmd=${cmd}`);
@@ -124,12 +83,8 @@ function callPolicy(eventName: string, payload: unknown): { block: boolean; reas
124
83
  const parsed = JSON.parse(stdout) as PolicyDecision;
125
84
  if (parsed.permission === "deny") {
126
85
  debug(`DENY reason=${parsed.reason}`);
127
- return {
128
- block: true,
129
- reason: parsed.reason ?? "Blocked by failproofai",
130
- };
86
+ return { block: true, reason: parsed.reason ?? "Blocked by failproofai" };
131
87
  }
132
- return { block: false, reason: "" };
133
88
  } catch (err) {
134
89
  debug(`EXCEPTION ${err instanceof Error ? err.message : String(err)}`);
135
90
  // Fail-open: never block tool execution because of an infra failure.
@@ -462,7 +417,7 @@ export default function failproofaiBridge(pi: PiExtensionApi) {
462
417
  // need session_id continuity see the metadata.
463
418
  pi.on("session_start", (event: unknown): unknown => {
464
419
  const e = event as PiSessionStartEvent;
465
- forwardPolicy("session_start", {
420
+ callPolicy("session_start", {
466
421
  session_id: resolveSessionId(e.sessionId, resolveCwd(e.cwd)),
467
422
  cwd: resolveCwd(e.cwd),
468
423
  reason: e.reason,
@@ -480,7 +435,7 @@ export default function failproofaiBridge(pi: PiExtensionApi) {
480
435
  pi.on("tool_result", (event: unknown): unknown => {
481
436
  const e = event as PiToolResultEvent;
482
437
  const canonicalTool = canonicalizeToolName(e.toolName);
483
- forwardPolicy("tool_result", {
438
+ callPolicy("tool_result", {
484
439
  tool_name: canonicalTool,
485
440
  tool_input: canonicalizeToolInput(canonicalTool, e.input) ?? {},
486
441
  tool_response: { content: e.content, isError: e.isError },
@@ -545,7 +500,7 @@ export default function failproofaiBridge(pi: PiExtensionApi) {
545
500
  pi.on("session_shutdown", (event: unknown): unknown => {
546
501
  const e = event as PiSessionShutdownEvent;
547
502
  const cwd = resolveCwd(e.cwd);
548
- forwardPolicy("session_shutdown", {
503
+ callPolicy("session_shutdown", {
549
504
  session_id: resolveSessionId(e.sessionId, cwd),
550
505
  cwd,
551
506
  reason: e.reason,
@@ -14,7 +14,6 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync, openSync
14
14
  import { join } from "node:path";
15
15
  import { BUILTIN_POLICIES } from "../hooks/builtin-policies";
16
16
  import { AUDIT_DETECTORS } from "./detectors";
17
- import { LEAK_SCAN_VERSION } from "./leak-scan";
18
17
  import type { TranscriptAuditResult, DetectorSessionState } from "./types";
19
18
  import { auditCacheDir } from "../hooks/fp-home";
20
19
 
@@ -163,9 +162,6 @@ interface CacheEntry {
163
162
  sizeBytes: number;
164
163
  engineVersion: string;
165
164
  detectorVersion: string;
166
- /** Separate because leak detection is neither a builtin policy nor one of
167
- * the behavioural AUDIT_DETECTORS hashed above. */
168
- leakScanVersion: number;
169
165
  result: TranscriptAuditResult;
170
166
  /**
171
167
  * How far into the file the recorded result actually accounts for, at a LINE
@@ -273,7 +269,6 @@ export function readCachedTranscript(
273
269
  if (entry.schemaVersion !== CACHE_SCHEMA_VERSION) return null;
274
270
  if (entry.engineVersion !== getEngineVersion()) return null;
275
271
  if (entry.detectorVersion !== getDetectorVersion()) return null;
276
- if (entry.leakScanVersion !== LEAK_SCAN_VERSION) return null;
277
272
  // Number.isFinite (not typeof) so a malformed JSON `Infinity` /
278
273
  // `NaN` is rejected — those would otherwise bypass the TTL check
279
274
  // and pin a stale entry as valid forever.
@@ -337,7 +332,6 @@ export function writeCachedTranscriptResult(
337
332
  sizeBytes,
338
333
  engineVersion: getEngineVersion(),
339
334
  detectorVersion: getDetectorVersion(),
340
- leakScanVersion: LEAK_SCAN_VERSION,
341
335
  result,
342
336
  };
343
337
  if (resume && resume.bytesScanned > 0) {
@@ -6,7 +6,6 @@
6
6
  * parses each via lib/log-entries.ts.
7
7
  */
8
8
  import { readFile, open, type FileHandle } from "node:fs/promises";
9
- import { readFileSync } from "node:fs";
10
9
  import {
11
10
  listClaudeProjects,
12
11
  listClaudeTranscripts,
@@ -41,17 +40,6 @@ export async function listClaudeTranscriptMetadata(
41
40
  }
42
41
  for (const t of transcripts) {
43
42
  if (t.mtimeMs < sinceMs) continue;
44
- let sessionDescription: string | undefined;
45
- if (t.isSubagent) {
46
- try {
47
- const meta = JSON.parse(
48
- readFileSync(t.transcriptPath.replace(/\.jsonl$/, ".meta.json"), "utf8"),
49
- ) as { description?: unknown };
50
- if (typeof meta.description === "string") sessionDescription = meta.description;
51
- } catch {
52
- // Sidecars are optional (journal transcripts do not have one).
53
- }
54
- }
55
43
  out.push({
56
44
  cli: "claude",
57
45
  projectName: project.name,
@@ -59,7 +47,6 @@ export async function listClaudeTranscriptMetadata(
59
47
  transcriptPath: t.transcriptPath,
60
48
  mtimeMs: t.mtimeMs,
61
49
  sizeBytes: t.sizeBytes,
62
- sessionDescription,
63
50
  });
64
51
  }
65
52
  }
@@ -173,10 +173,7 @@ export async function ensureSignedIn(preset?: string): Promise<EnsureSignedIn> {
173
173
  throw new LoginError(
174
174
  "Signing in needs an interactive terminal, and this one is not.\n" +
175
175
  "Run `failproofai audit --schedule` from a shell you are sitting at,\n" +
176
- "or sign in from the dashboard — both write the same session file.\n" +
177
- "\n" +
178
- "`--email` does not avoid this: it fills in the address so you are not\n" +
179
- "asked for it, but a one-time code is still sent there for you to paste.",
176
+ "or sign in from the dashboard — both write the same session file.",
180
177
  );
181
178
  }
182
179
 
package/src/audit/cli.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * the dashboard to view it.
4
4
  *
5
5
  * failproofai audit Scan, then launch the dashboard at /audit.
6
- * failproofai audit --schedule Put local scans and notifications on a timer.
6
+ * failproofai audit --schedule Put scans on a timer and mail the findings.
7
7
  * failproofai audit --no-schedule Stop the timer.
8
8
  * failproofai audit --status What this machine is scheduled to do.
9
9
  * failproofai audit -h, --help Show usage.
@@ -35,11 +35,6 @@ import { getInstanceId } from "../../lib/telemetry-id";
35
35
  import { sanitizeErrorMessage } from "../../lib/telemetry-sanitize";
36
36
  import { openWhenReady } from "./open-browser";
37
37
  import { describeOutcome, reportHarm } from "./report-harm";
38
- import { notifyDesktop } from "./desktop-notify";
39
- import { macNotifierInstalled, pruneMacNotifyQueue, queueMacNotification } from "./macos-notifier";
40
- import { desktopLeakNotice, type DesktopLeakEmailState } from "../hooks/notice";
41
- import { readConfig } from "../hooks/fp-config";
42
- import { readAuth } from "../../lib/auth/auth-store";
43
38
  import { brandAnsi, ANSI_RESET, ANSI_BOLD, ANSI_DIM, helpScreen, helpOptsFor } from "../hooks/tui";
44
39
  import { version } from "../../package.json";
45
40
 
@@ -97,36 +92,26 @@ export function helpText(): string {
97
92
  {
98
93
  command: "audit",
99
94
  version,
100
- tagline: "find credentials your agents leaked into their own transcripts",
95
+ tagline: "review your agent CLIs for risky and wasteful patterns",
101
96
  sections: [
102
97
  {
103
98
  label: "usage",
104
99
  entries: [
105
- ["(bare)", `Scan your session history for leaked keys, then open http://localhost:${DASHBOARD_PORT}/audit`],
106
- ["--schedule [days]", "Scan on a timer and notify you on this machine. Default 7 days, range 1-90. No account required."],
100
+ ["(bare)", `Scan your session history, then open http://localhost:${DASHBOARD_PORT}/audit`],
101
+ ["--schedule [days]", "Scan on a timer and email the findings. Default 7 days, range 1-90. Signs you in the first time."],
107
102
  // Its own row now, rather than a clause inside --schedule's. It
108
103
  // does not stand alone, which is why it used to be a clause — but
109
104
  // a clause wraps, and `--email <address>` landing with the flag at
110
105
  // the end of one line and its placeholder at the start of the next
111
106
  // is not a flag anybody can read or copy.
112
- ["--email <address>", "With --schedule, add email alerts for the most recent masked credential leak. A sign-in code is emailed to you."],
107
+ ["--email <address>", "With --schedule, skips the sign-in prompt."],
113
108
  ["--no-schedule", "Stop the timer. Leaves you signed in."],
114
- // Its own pair of flags rather than a clause on --schedule: whether
115
- // this machine scans and whether it may interrupt you are separate
116
- // decisions, and nobody silencing a banner should have to re-state
117
- // their schedule to do it.
118
- ["--notify", "Show a desktop notification when a scan finds a credential. On by default."],
119
- ["--no-notify", "Stop desktop notifications. Scheduled scans and optional email alerts are unaffected."],
120
109
  ["--status", "Whether scheduling is on, where reports go, the daemon's state, and when the next scan is due."],
121
110
  ["-h, --help", "Show this help."],
122
111
  ],
123
112
  },
124
113
  ],
125
- footer: [
126
- "Every scan runs on this machine. Leak alerts appear as desktop system",
127
- "notifications, never inside agent sessions. If you add email, only the",
128
- "newest masked credential exposure and its metadata leave the machine.",
129
- ],
114
+ footer: ["Everything runs on this machine; only a scheduled digest ever leaves it."],
130
115
  },
131
116
  helpOptsFor(process.stdout),
132
117
  );
@@ -214,18 +199,6 @@ function startProgress(): Progress {
214
199
  const lines = Array.from({ length: n }, (_, i) => lineFor(i));
215
200
  // Move the cursor back up over the previously-drawn block, then clear and
216
201
  // rewrite each line in place.
217
- //
218
- // INVARIANT: nothing else may write to the terminal while this is running.
219
- // The cursor-up is a fixed count, so any stray line pushes the cursor down
220
- // and the next redraw repaints the block lower — stranding the top of the
221
- // old frame above it, which reads as the audit having run twice. That is
222
- // not hypothetical: Node's "SQLite is an experimental feature" warning did
223
- // exactly this on every first run, two lines of it, and it is why
224
- // `lib/sqlite-reader.ts` now filters that one warning at the source.
225
- //
226
- // There is no defensive fix from inside here — erasing to end of screen
227
- // still leaves the stranded lines ABOVE the cursor. Keep the terminal
228
- // quiet instead.
229
202
  if (printed) process.stdout.write(`\x1b[${n}A`);
230
203
  process.stdout.write(lines.map((l) => `\x1b[2K${l}`).join("\n") + "\n");
231
204
  printed = true;
@@ -361,116 +334,6 @@ function heldByLine(held: AuditLockInfo | null): string {
361
334
  return `another audit is already running (pid ${held.pid}, started by ${held.source} ${ageS}s ago)`;
362
335
  }
363
336
 
364
- /**
365
- * Raise a desktop banner for credentials this scan found and nothing has
366
- * announced yet.
367
- *
368
- * Scheduled runs only, and the reason is the same one that keeps `reportHarm`
369
- * here: an interactive `failproofai audit` prints its findings to a person who
370
- * is already looking at them, so a banner on top is noise. The scheduled run is
371
- * the one with nobody watching, which is exactly when a notification is the
372
- * only thing that reaches anyone.
373
- *
374
- * Every failure is swallowed on purpose. This is an announcement about a
375
- * completed scan, not part of the scan: a machine with no desktop, a locked
376
- * session, a bus that hung — none of those make the audit itself a failure, and
377
- * turning a good scan into exit 1 would make the scheduler back off from
378
- * running the thing that actually matters.
379
- */
380
- async function announceLeaksOnDesktop(
381
- result: AuditResult,
382
- report: Awaited<ReturnType<typeof reportHarm>>,
383
- ): Promise<void> {
384
- try {
385
- await announceLeaksOrThrow(result, report);
386
- } catch (err) {
387
- // The catch is not decoration. `notifyDesktop` documents that it never
388
- // throws, but it is doing socket I/O, and so are the config read and the
389
- // marker write beside it — depending on three modules' promises to keep a
390
- // completed scan from reporting failure is a dependency this does not need
391
- // to have.
392
- process.stderr.write(
393
- `failproofai: could not announce a leak on the desktop: ` +
394
- `${err instanceof Error ? err.message : String(err)}\n`,
395
- );
396
- }
397
- }
398
-
399
- async function announceLeaksOrThrow(
400
- result: AuditResult,
401
- report: Awaited<ReturnType<typeof reportHarm>>,
402
- ): Promise<void> {
403
- // `leakIds` includes credentials this scan encountered before. A scheduled
404
- // run is a fresh safety check, so it must raise a fresh system notification
405
- // when the leak is still present; the old per-finding marker is why only the
406
- // first test run ever produced a banner. Older cached results fall back to
407
- // first-seen ids.
408
- const ids = result.leakIds ?? result.newLeakIds ?? [];
409
- if (ids.length === 0) return;
410
- // Read at fire time, not at scan time: the switch answers "should this
411
- // interrupt me", and it is the user's most recent answer that counts.
412
- if (!readConfig().audit.notify) return;
413
-
414
- const n = ids.length === 1 ? "a possible credential exposure" : `${ids.length} possible credential exposures`;
415
- // Fixed text with no scanned content and no count. A finding's own text comes
416
- // from a repository this machine cloned, and notification bodies render
417
- // markup on several Linux desktops. The count is intentionally left to the
418
- // report too: a banner must not turn detector candidates into a claim that
419
- // the user leaked N real credentials.
420
- //
421
- // No action buttons, deliberately. `Notify` supports them, but a server
422
- // delivers the click back as an `ActionInvoked` signal to the sender — and
423
- // this process exits as soon as the scan finishes, so the button would be
424
- // dead. A plain command the user can copy beats a button that does nothing.
425
- const emailState: DesktopLeakEmailState =
426
- report.kind === "sent"
427
- ? "sent"
428
- : report.kind === "signed-out" || !readAuth()
429
- ? "missing"
430
- : report.kind === "failed"
431
- ? "failed"
432
- : "held";
433
- const { title, body } = desktopLeakNotice(emailState);
434
-
435
- // The two platforms need opposite things, and the split is not cosmetic. On
436
- // Linux this process can reach the session bus itself. On macOS it cannot
437
- // reach Notification Center at all — it is a child of a LaunchDaemon, outside
438
- // the GUI session — so it hands the message to an agent that lives inside
439
- // one. See `macos-notifier.ts` for why that agent exists.
440
- if (process.platform === "darwin") {
441
- pruneMacNotifyQueue();
442
- if (!queueMacNotification(ids[0], title, body)) {
443
- process.stderr.write(`failproofai: found ${n} but could not queue a notification\n`);
444
- } else if (!macNotifierInstalled()) {
445
- // Queued, but nothing currently collects it. The payload is durable and
446
- // will be consumed when the LaunchAgent returns.
447
- // Worth a line, because the immediate symptom is otherwise silence.
448
- process.stderr.write(
449
- `failproofai: found ${n}; the notifier is not installed, so nothing will show it ` +
450
- `(run \`failproofai config\`)\n`,
451
- );
452
- }
453
- return;
454
- }
455
-
456
- const outcome = await notifyDesktop(
457
- title,
458
- body,
459
- // `0` asks the notification server for a fresh id. Each scheduled scan that
460
- // still sees a leak is a new reminder, rather than replacing or suppressing
461
- // the first banner forever.
462
- 0,
463
- );
464
- if (!outcome.ok) {
465
- // stderr, so it lands in the journal beside the run it belongs to. Not a
466
- // failure of the audit — but "we found a key and could not tell you" is
467
- // exactly the sentence someone debugging this needs to find.
468
- process.stderr.write(
469
- `failproofai: found ${n} but could not raise a desktop notification (${outcome.reason})\n`,
470
- );
471
- }
472
- }
473
-
474
337
  // ── Headless (scheduled) audit ───────────────────────────────────────────────
475
338
 
476
339
  /**
@@ -541,9 +404,7 @@ export async function runScheduledAudit(): Promise<number> {
541
404
  `${num(result.transcripts.scanned)} sessions, ${num(result.totals.hits)} hits\n`,
542
405
  );
543
406
 
544
- // Email is optional and never controls the local scan. Run it first only so
545
- // the desktop notification can truthfully say whether this particular
546
- // alert was also emailed; every failure remains a non-fatal outcome.
407
+ // Report harmful findings upstream, if the user switched emailed reports on.
547
408
  //
548
409
  // AFTER the dashboard cache is written and AFTER the success line, because
549
410
  // the scan is the product and this is an optional extra on top of it.
@@ -557,7 +418,6 @@ export async function runScheduledAudit(): Promise<number> {
557
418
  // would also make the manual command do a network call that
558
419
  // `audit --help` promises it does not.
559
420
  const outcome = await reportHarm(result);
560
- await announceLeaksOnDesktop(result, outcome);
561
421
  const line = describeOutcome(outcome);
562
422
  if (line) {
563
423
  // Anything other than a successful send goes to stderr: on a scheduled run
@@ -692,16 +552,6 @@ export async function runAuditCli(args: string[]): Promise<void> {
692
552
  process.exit(0);
693
553
  }
694
554
 
695
- for (const [flag, enable] of [["--notify", true], ["--no-notify", false]] as const) {
696
- if (args.includes(flag)) {
697
- const extra = args.find((a) => a !== flag);
698
- if (extra) die(`\`audit ${flag}\` takes no other arguments (got: ${extra}).`);
699
- const { runNotifyToggle } = await import("./schedule-cli");
700
- runNotifyToggle(enable);
701
- process.exit(0);
702
- }
703
- }
704
-
705
555
  if (args.includes("--no-schedule")) {
706
556
  const extra = args.find((a) => a !== "--no-schedule");
707
557
  if (extra) die(`\`audit --no-schedule\` takes no other arguments (got: ${extra}).`);