failproofai 1.0.2-beta.0 → 1.0.2-beta.2

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 (133) 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 +3 -3
  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.nft.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  11. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
  12. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  13. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +2 -2
  15. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  16. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  17. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  19. package/.next/standalone/.next/server/app/_not-found.rsc +14 -14
  20. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
  21. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +13 -13
  22. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  23. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/audit/page/build-manifest.json +2 -2
  32. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  33. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  35. package/.next/standalone/.next/server/app/index.html +1 -1
  36. package/.next/standalone/.next/server/app/index.rsc +14 -14
  37. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +13 -13
  38. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
  39. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  40. package/.next/standalone/.next/server/app/page/build-manifest.json +2 -2
  41. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  42. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  43. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  44. package/.next/standalone/.next/server/app/policies/page/build-manifest.json +2 -2
  45. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  46. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  47. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  48. package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +2 -2
  49. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  50. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  51. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  52. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +2 -2
  53. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  54. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  55. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  56. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  57. package/.next/standalone/.next/server/app/projects/page/build-manifest.json +2 -2
  58. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  59. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  60. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  61. package/.next/standalone/.next/server/app/settings/page/build-manifest.json +2 -2
  62. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
  63. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  64. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  65. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
  66. package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
  67. package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
  68. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  69. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +2 -2
  70. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +2 -2
  71. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +2 -2
  72. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__07gm3zl._.js +14 -14
  73. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__090sxxw._.js +3 -0
  74. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +2 -2
  75. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +2 -2
  76. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +1 -1
  77. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +2 -2
  78. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s740oi._.js +2 -2
  79. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +2 -2
  80. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  81. package/.next/standalone/.next/server/chunks/ssr/_0wb8osu._.js +23 -0
  82. package/.next/standalone/.next/server/chunks/ssr/{_12l4a_k._.js → _1oggjel._.js} +1 -1
  83. package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +1 -1
  84. package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
  85. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  86. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  87. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
  88. package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
  89. package/.next/standalone/.next/server/middleware-build-manifest.js +5 -5
  90. package/.next/standalone/.next/server/pages/404.html +1 -1
  91. package/.next/standalone/.next/server/pages/500.html +1 -1
  92. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  93. package/.next/standalone/.next/server/server-reference-manifest.json +19 -19
  94. package/.next/standalone/.next/static/chunks/{3y54nw0i7dsjl.js → 0nnbtp0w4sxrr.js} +1 -1
  95. package/.next/standalone/.next/static/chunks/{0sa9v5mhc8861.js → 0ykd_go4oahan.js} +1 -1
  96. package/.next/standalone/.next/static/chunks/{0eqky-7n7dgdm.js → 140ip21rqmelt.js} +1 -1
  97. package/.next/standalone/.next/static/chunks/{2i2yt3hzjredm.js → 1da5a_a-3btas.js} +1 -1
  98. package/.next/standalone/.next/static/chunks/{1oml7c7env4-g.js → 1fac8krph-zgp.js} +1 -1
  99. package/.next/standalone/.next/static/chunks/{2hs7ycvsav7v2.js → 1nzy5jc_vf9e0.js} +1 -1
  100. package/.next/standalone/.next/static/chunks/{1sl2xmn9inx3y.js → 1yw71gn8vqco1.js} +1 -1
  101. package/.next/standalone/.next/static/chunks/{1nuqytbta44af.js → 29eh4k77otcw6.js} +1 -1
  102. package/.next/standalone/.next/static/chunks/{2aawsjld9ied_.js → 2axi5ehjbld9h.js} +1 -1
  103. package/.next/standalone/.next/static/chunks/{2gn7ut4svofm2.js → 2fzxl7n717pbc.js} +1 -1
  104. package/.next/standalone/.next/static/chunks/{35xg6retpe6rp.js → 3jjmdqqzem21x.js} +1 -1
  105. package/.next/standalone/.next/static/chunks/{turbopack-3kjgdgwci4tqf.js → turbopack-2vqpa-dts1gy4.js} +1 -1
  106. package/.next/standalone/app/actions/pack-actions.ts +21 -11
  107. package/.next/standalone/app/policies/hooks-client.tsx +3 -4
  108. package/.next/standalone/fp-cloud-cli/fp_cli/_version.py +1 -1
  109. package/.next/standalone/package.json +10 -11
  110. package/.next/standalone/sdk/python/failproofai_sdk/_version.py +1 -1
  111. package/.next/standalone/server.js +1 -1
  112. package/bin/failproofai.mjs +455 -316
  113. package/dist/cli.mjs +2374 -1873
  114. package/dist/worker.mjs +3 -13
  115. package/package.json +10 -11
  116. package/src/hooks/configure-wizard.ts +67 -444
  117. package/src/hooks/fp-reset.ts +10 -19
  118. package/src/hooks/handler.ts +2 -2
  119. package/src/hooks/manager.ts +58 -24
  120. package/src/hooks/pack-cli.ts +488 -43
  121. package/src/hooks/pack-failclosed.ts +1 -1
  122. package/src/hooks/pack-manifest.ts +14 -6
  123. package/src/hooks/pack-store.ts +89 -128
  124. package/src/hooks/tui.ts +150 -48
  125. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0zig5sv._.js +0 -3
  126. package/.next/standalone/.next/server/chunks/ssr/_00e9tnf._.js +0 -23
  127. package/policy-pack/SHA256SUMS +0 -2
  128. package/policy-pack/failproofai-pack.json +0 -732
  129. package/policy-pack/failproofai-pack.mjs +0 -2017
  130. package/src/hooks/policy-presets.ts +0 -137
  131. /package/.next/standalone/.next/static/{nwbyWT06Ud0XdXOrVJyvS → A7czO1PZdTcNzLGa_H1sm}/_buildManifest.js +0 -0
  132. /package/.next/standalone/.next/static/{nwbyWT06Ud0XdXOrVJyvS → A7czO1PZdTcNzLGa_H1sm}/_clientMiddlewareManifest.js +0 -0
  133. /package/.next/standalone/.next/static/{nwbyWT06Ud0XdXOrVJyvS → A7czO1PZdTcNzLGa_H1sm}/_ssgManifest.js +0 -0
@@ -34,8 +34,35 @@ if (!process.env.FAILPROOFAI_DIST_PATH) {
34
34
 
35
35
  const args = process.argv.slice(2);
36
36
 
37
- // Normalize 'p' 'policies' (shorthand alias)
38
- if (args[0] === "p") args[0] = "policies";
37
+ // ── one noun for policies ──────────────────────────────────────────────────
38
+ // `policies`, `policy` and `pack` were three commands for one idea, two of them
39
+ // a single letter apart and doing unrelated things. They are now three
40
+ // spellings of the same command. Rewritten HERE, above SUBCOMMANDS and every
41
+ // dispatch below, so the rest of this file mentions only the canonical name and
42
+ // no branch has to remember the aliases.
43
+ //
44
+ // Nothing anybody has typed before stops working — the old spellings are
45
+ // translated, not rejected — which matters because they are printed in shipped
46
+ // help output, in this repo's docs, and in the release notes of every pack
47
+ // published so far.
48
+ if (args[0] === "p" || args[0] === "policy") args[0] = "policies";
49
+ if (args[0] === "pack") {
50
+ args[0] = "policies";
51
+ if (args[1] === "list") {
52
+ // `pack list` was two commands wearing one name: bare it listed what is
53
+ // installed here, with an argument it previewed a pack that is not. Those
54
+ // are different questions, so they are different words now — the bare form
55
+ // and `show`.
56
+ const hasSource = args[2] && !args[2].startsWith("-");
57
+ if (hasSource) args.splice(1, 1, "show");
58
+ else args.splice(1, 1);
59
+ } else if (args[1] === "build") {
60
+ // `pack build` produced the release assets and stopped. That is exactly
61
+ // `publish` with nowhere to publish to, so it IS publish — the local half
62
+ // of it. `publish` with no --repo does the same thing and says so.
63
+ args.splice(0, 2, "publish");
64
+ }
65
+ }
39
66
  // Normalize 'configure' / 'setup' → 'config' (aliases), so every later check
40
67
  // (SUBCOMMANDS, dispatch) mentions only the canonical name.
41
68
  if (args[0] === "configure" || args[0] === "setup") args[0] = "config";
@@ -271,168 +298,109 @@ if (hookIdx >= 0) {
271
298
  */
272
299
  async function runCli() {
273
300
  // --help / -h (only when not inside a subcommand that handles its own --help)
274
- const SUBCOMMANDS = ["policies", "policy", "audit", "config", "uninstall", "backfill", "flush", "harness", "pack"];
275
- if ((args.includes("--help") || args.includes("-h")) && !SUBCOMMANDS.includes(args[0])) {
276
- const extraArgs = args.filter((a) => a !== "--help" && a !== "-h");
301
+ // `update` and `migrate` were missing here, so `failproofai update --help`
302
+ // exited 1 with "Unexpected argument" both commands had no reachable help
303
+ // at all, only the paragraph in the top-level dump that this rewrite moved.
304
+ // `help` and `publish` are new. `policy` and `pack` are canonicalized to
305
+ // `policies` above and never reach this list.
306
+ const SUBCOMMANDS = ["policies", "audit", "config", "uninstall", "backfill", "flush", "harness", "publish", "update", "migrate", "help"];
307
+ // ── help ─────────────────────────────────────────────────────────────────
308
+ //
309
+ // The index and the reference manual used to be the same document: 152 lines,
310
+ // six screens at 80x24, with every flag of every command inlined. That is a
311
+ // help tier collapse — the thing you read to find a command was the thing you
312
+ // read to use one — and the cost fell on the person who knew least.
313
+ //
314
+ // Now: ONE screen of what exists, and `help <command>` for everything else.
315
+ // `help <command>` is literally `<command> --help`, dispatched below, so there
316
+ // is exactly one copy of each command's documentation and the two spellings
317
+ // cannot drift.
318
+ const helpTopic = args[0] === "help" ? args[1] : undefined;
319
+ if (args[0] === "help" && helpTopic) {
320
+ // `--hook` is the entry point an agent CLI spawns per tool call. It is
321
+ // documented in NO help output — only a module docblock and one error
322
+ // string — so it gets a topic here rather than a line on the index, where a
323
+ // machine-facing flag would only take space from the human-facing commands.
324
+ if (helpTopic === "hook") {
325
+ console.log(`
326
+ failproofai --hook <event> [--cli <name>]
327
+
328
+ The entry point your agent CLI spawns, once per tool call. You do not run this;
329
+ \`failproofai config\` writes it into each CLI's hook configuration for you.
330
+
331
+ --hook <event> PreToolUse, PostToolUse, UserPromptSubmit, Stop,
332
+ SubagentStop, SessionStart, SessionEnd, PreCompact,
333
+ Notification, PermissionRequest
334
+ --cli <name> claude, codex, copilot, cursor, opencode, pi, hermes,
335
+ openclaw, factory, devin, antigravity, goose.
336
+ Defaults to claude. It selects which payload shape to
337
+ expect: each CLI names its events and tool arguments
338
+ differently, and failproofai canonicalizes them.
339
+
340
+ It reads the event as JSON on stdin and answers on stdout, in whatever shape
341
+ that CLI honours. Exit codes and response shapes differ per CLI by necessity —
342
+ see docs.befailproof.ai. Denials are reported to the agent, never to you.
343
+ `.trimStart());
344
+ process.exit(0);
345
+ }
346
+ // Canonicalize the topic the same way a typed command is canonicalized, so
347
+ // `help pack` and `help policy` answer instead of erroring.
348
+ const canonical =
349
+ helpTopic === "p" || helpTopic === "policy" || helpTopic === "pack"
350
+ ? "policies"
351
+ : helpTopic === "configure" || helpTopic === "setup"
352
+ ? "config"
353
+ : helpTopic;
354
+ if (!SUBCOMMANDS.includes(canonical)) {
355
+ throw new CliError(
356
+ `No help for: ${helpTopic}\n` +
357
+ `Run \`failproofai help\` to see every command.`,
358
+ );
359
+ }
360
+ // `policies add|remove|show` has its own help, distinct from the listing's.
361
+ args.splice(0, 2, canonical, ...(canonical === "policies" && !args[2] ? [] : []), "--help");
362
+ }
363
+
364
+ if (
365
+ args[0] === "help" ||
366
+ ((args.includes("--help") || args.includes("-h")) && !SUBCOMMANDS.includes(args[0]))
367
+ ) {
368
+ const extraArgs = args.filter((a) => a !== "--help" && a !== "-h" && a !== "help");
277
369
  if (extraArgs.length > 0) {
278
- throw new CliError(`Unexpected argument: ${extraArgs[0]}\nRun \`failproofai --help\` for usage.`);
370
+ throw new CliError(`Unexpected argument: ${extraArgs[0]}\nRun \`failproofai help\` for usage.`);
279
371
  }
280
372
  console.log(`
281
- failproofai v${version}
282
-
283
- USAGE
284
- failproofai [command] [options]
285
-
286
- COMMANDS
287
- (no args) Launch the policy dashboard
288
- config Interactive setup pick scope, agents & policies
289
- --connect <url> --token <key> Connect to FailproofAI 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
296
-
297
- policy add <name> Enable a single policy (see \`policy --help\`)
298
- policy remove <name> Disable a single policy
299
-
300
- policies, p List all available policies and their status
301
- policies --install, -i Enable policies in agent CLI settings
302
- [names...] Specific policy names to enable
303
- --cli claude|codex|copilot|cursor|opencode|pi|hermes|openclaw|factory|devin|antigravity|goose
304
- Agent CLI(s) to install for; space-separated
305
- (e.g. --cli claude codex copilot cursor opencode pi hermes openclaw factory devin antigravity goose) or repeated.
306
- Default: detect installed CLIs and prompt.
307
- --scope user|project|local Config scope to write to (default: user)
308
- (Codex / Copilot / Cursor / OpenCode / Pi support user|project only)
309
- --beta Include beta policies
310
- --custom, -c <path> Custom policy file (repeat for multiple files)
311
-
312
- policies --uninstall, -u Disable policies or remove hooks
313
- [names...] Specific policy names to disable
314
- --cli claude|codex|copilot|cursor|opencode|pi|hermes|openclaw|factory|devin|antigravity|goose
315
- Agent CLI(s) to uninstall from
316
- --scope user|project|local|all Config scope to remove from (default: user)
317
- --beta Remove only beta policies
318
- --custom, -c Clear all explicit custom policy paths
319
-
320
- policies --help, -h Show this help for the policies command
321
-
322
- pack add core Install the Failproof AI policies — from this
323
- package, so it needs no network.
324
- --policy <a,b> takes only those, --category
325
- <x,y> takes whole categories, --all takes it all
326
- pack list Show installed policy packs
327
- pack list <source> Show what a pack CONTAINS before installing it.
328
- Reads only its manifest — the code is never
329
- downloaded, so looking is never running
330
- pack add <source> Install someone else's pack from a GitHub release.
331
- <source> is acme/pack, acme/pack@v1.2.0, or a
332
- release URL; no tag takes the newest and pins it
333
- pack remove <publisher/name> Deactivate an installed pack
334
- pack build <entry.mjs> Turn your own policy file into the three assets
335
- a GitHub release needs, so others can install it
336
- pack --help, -h Show this help for the pack command
337
- harness list Show extra capture paths per agent CLI
338
- harness add-path <h> <path> Also capture sessions from <path> for harness
339
- <h>. Accepts \`<label>=<path>\`; the label
340
- namespaces agent ids so two copies of one
341
- project stay distinct.
342
- harness remove-path <h> <p> Stop capturing that path
343
- harness --help, -h Show this help for the harness command
344
-
345
- audit Audit your agent's behavior, then open the
346
- dashboard at http://localhost:8020/audit
347
- audit --schedule [days] Audit on a timer (default 7 days) and email you
348
- what it finds. Signs you in the first time;
349
- --email <address> skips that question
350
- audit --no-schedule Stop auditing on a timer
351
- audit --status Whether scheduling is on, where reports go, and
352
- when the next scan is due
353
- audit --help, -h Show this help for the audit command
354
-
355
- backfill Re-send history the collector already read past
356
- — after clearing the dashboard, re-enrolling a
357
- machine, or connecting later than the work
358
- --since <when> How far back: 30d, 6m, or YYYY-MM-DD
359
- (default: 30 days)
360
- --dry-run Report what would be re-read, change nothing
361
-
362
- flush Deliver everything already spooled, now,
363
- instead of waiting for the next sweep
364
- --wait Block until the spool drains (or --timeout)
365
- --timeout <secs> How long to wait with --wait (default: 60)
366
-
367
- update Finish an upgrade npm cannot: migrate
368
- ~/.failproofai to this version's layout, put the
369
- matching daemon binary in place, restart it.
370
- Run after \`npm install -g failproofai@latest\`.
371
- --no-daemon Migrate the home only
372
-
373
- migrate Run pending layout migrations on their own,
374
- keyed on the layout in ~/.failproofai/VERSION
375
- (not the npm version, so skipping releases with
376
- no layout change runs nothing)
377
- --dry-run Print the steps and change nothing
378
-
379
- uninstall Remove failproofai from this machine: hook
380
- entries from every agent CLI, and the daemon
381
- service. Run this BEFORE \`npm rm -g failproofai\`
382
- — npm runs no uninstall script, so removing the
383
- package alone leaves both behind.
384
- --purge Also delete ~/.failproofai (settings,
385
- credentials, audit history, daemon binary)
386
- --dry-run Show what would be removed, change nothing
387
- --yes, -y Skip the confirmation prompt
388
-
389
- --version, -v Print version and exit
390
- --help, -h Show this help message
391
-
392
- CONVENTION POLICIES
393
- Drop *policies.{js,mjs,ts} files into .failproofai/policies/ for auto-loading.
394
- Works at project level (.failproofai/policies/) and user level (~/.failproofai/policies/).
395
- No --custom flag or config changes needed — just drop files and they're picked up.
396
-
397
- EXAMPLES
398
- failproofai policies
399
- failproofai policies --install
400
- failproofai policies --install block-sudo sanitize-api-keys --scope project
401
- failproofai policies --install --cli codex --scope project
402
- failproofai policies --install --cli copilot --scope project
403
- failproofai policies --install --cli cursor --scope project
404
- failproofai policies --install --cli opencode --scope project
405
- failproofai policies --install --cli pi --scope project
406
- failproofai policies --install --cli factory --scope project
407
- failproofai policies --install --cli devin --scope project
408
- failproofai policies --install --cli claude codex copilot cursor opencode pi hermes openclaw factory devin antigravity goose
409
- failproofai policies --install --custom ./my-policies.js
410
- failproofai policies -i -c ./my-policies.js
411
- failproofai policies --uninstall block-sudo
412
- failproofai policies --uninstall --cli codex
413
- failproofai policies --uninstall --cli copilot
414
- failproofai policies --uninstall --cli cursor
415
- failproofai policies --uninstall --cli opencode
416
- failproofai policies --uninstall --cli pi
417
- failproofai policies --uninstall --custom
418
- failproofai pack add core
419
- failproofai pack add core --category git,database
420
- failproofai pack list acme/support-agent
421
- failproofai pack build ./my-policies.mjs --id acme/support --version 1.0.0
422
- failproofai backfill --since 6m
423
- failproofai backfill --dry-run
424
- failproofai flush --wait
425
- failproofai config --status
426
-
427
- LINKS
428
- ⭐ Star us: https://github.com/failproofai/failproofai
429
- 📖 Docs: https://docs.befailproof.ai/introduction
430
- 💬 Discord: https://discord.befailproof.ai/
431
- 👽 Reddit: https://www.reddit.com/r/failproofai/
373
+ failproofai v${version} -v version -h this screen
374
+ Usage failproofai <command> [options] Detail failproofai help <command>
375
+ ━━ 1 GET IT RUNNING ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
376
+ config, setup Interactive setup: agents, daemon, cloud
377
+ update Finish an npm upgrade: migrate home, match daemon
378
+ ━━ 2 CHOOSE WHAT IT ENFORCES ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
379
+ policies Every policy on this machine, and whether it is on
380
+ policies add Pick from a list, or name one: <policy> or <owner>/<repo>
381
+ policies remove Turn one policy off, or uninstall a whole pack
382
+ policies show What a pack contains, before you install it
383
+ policies -i / -u Wire failproofai into your agent CLIs
384
+ publish Ship your own policies as a pack anyone can install
385
+ ━━ 3 SEE WHAT IT CAUGHT ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
386
+ (no args) Open the policy dashboard on localhost:8020
387
+ audit Scan your agents' history, then open the audit view
388
+ config --status Cloud connection, daemon version, pause state
389
+ harness Extra paths to capture agent sessions from
390
+ ━━ 4 PUT IT RIGHT ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
391
+ config --pause Pause enforcement for one session (30m; max 8h)
392
+ flush Deliver everything already spooled, right now
393
+ backfill Re-send history the collector already read past
394
+ migrate Run pending ~/.failproofai layout migrations
395
+ uninstall Remove hooks and daemon, BEFORE npm rm -g failproofai
396
+
397
+ Docs docs.befailproof.ai Code github.com/failproofai/failproofai
398
+ Chat discord.befailproof.ai Forum reddit.com/r/failproofai
432
399
  `.trimStart());
433
400
  process.exit(0);
434
401
  }
435
402
 
403
+
436
404
  // --version / -v
437
405
  if ((args.includes("--version") || args.includes("-v")) && !SUBCOMMANDS.includes(args[0])) {
438
406
  const extraArgs = args.filter((a) => a !== "--version" && a !== "-v");
@@ -1217,6 +1185,328 @@ WHY THIS EXISTS
1217
1185
  return;
1218
1186
  }
1219
1187
 
1188
+ // publish — build the release assets AND put them on a GitHub release.
1189
+ //
1190
+ // Top level rather than under `policies`, because it is a PRODUCER verb: the
1191
+ // four consumer words are what every user reads, and almost none of them will
1192
+ // ever ship a pack. `pack build` is canonicalized into this above.
1193
+ if (args[0] === "publish") {
1194
+ const subArgs = args.slice(1);
1195
+ if (subArgs.includes("--help") || subArgs.includes("-h") || subArgs.length === 0) {
1196
+ console.log(`
1197
+ failproofai publish — ship your policies as a pack anyone can install
1198
+
1199
+ USAGE
1200
+ failproofai publish <entry.mjs> --repo <owner>/<repo> --version <version>
1201
+
1202
+ WHAT IT DOES
1203
+ Validates the file with the loader's own rules, writes the three release
1204
+ assets, creates (or reuses) the release, and attaches them. One command.
1205
+
1206
+ Only the release matters. Installs read
1207
+ releases/download/<tag>/<asset> and never touch your git tree — pushing the
1208
+ source is for humans reading it, not for the install to work.
1209
+
1210
+ OPTIONS
1211
+ --repo <owner>/<repo> Where to release it. Omit to build the assets only.
1212
+ --version <version> The pack version. Also the tag, unless --tag says otherwise.
1213
+ --id <publisher/name> The pack's id. Defaults to --repo.
1214
+ --tag <tag> Release tag. Defaults to --version; a leading v is fine.
1215
+ --notes <text> Release notes.
1216
+ --out <dir> Where to write the assets (default: dist-pack)
1217
+ --effect enforce|observe observe records and blocks nothing (default: enforce)
1218
+ --dry-run Build the assets, publish nothing.
1219
+
1220
+ CREDENTIAL
1221
+ GITHUB_TOKEN or GH_TOKEN, else whatever \`gh auth login\` stored. It needs
1222
+ write access to releases on that repository and nothing else. The token is
1223
+ never printed.
1224
+
1225
+ THE REPO MUST BE PUBLIC
1226
+ Installs are anonymous HTTPS with no credential to offer, so a private
1227
+ repository publishes to nobody. This warns you if it is.
1228
+
1229
+ YOUR ENTRY FILE
1230
+ One file, no relative imports — only the entry is digest-pinned, so a pack
1231
+ importing siblings could not honestly claim to be verified. Bundle first
1232
+ (esbuild, bun build, rollup) if yours is split. Each policy may carry
1233
+ \`category\` and \`defaultEnabled\` alongside the usual fields.
1234
+
1235
+ EXAMPLES
1236
+ failproofai publish ./deploy-guard.mjs --repo me/deploy-guard --version 1.0.0
1237
+ failproofai publish ./deploy-guard.mjs --version 1.0.1 --dry-run
1238
+ failproofai publish ./x.mjs --repo me/x --version 2.0.0 --effect observe
1239
+ `.trimStart());
1240
+ process.exit(0);
1241
+ }
1242
+
1243
+ lastSubcommand = "publish";
1244
+ const { runPublishCommand } = await import("../src/hooks/pack-cli");
1245
+ const result = await runPublishCommand(subArgs);
1246
+ for (const line of result.lines) {
1247
+ if (result.exitCode === 0) console.log(line);
1248
+ else console.error(line);
1249
+ }
1250
+ // Shape, never value: whether it published and whether it worked. Never the
1251
+ // repo, the pack id or the entry path — all three are things the user typed.
1252
+ await track("cli_publish", { ok: result.exitCode === 0, dry_run: subArgs.includes("--dry-run") });
1253
+ lastSubcommand = null;
1254
+ await exitAfterFlush(result.exitCode);
1255
+ return;
1256
+ }
1257
+
1258
+ // policies add | remove | show — one noun for policies.
1259
+ //
1260
+ // `policies`, `policy` and `pack` were three commands for one idea. They are
1261
+ // one now, and `add`/`remove` take EITHER a policy name or a pack source,
1262
+ // told apart by a SLASH: a policy name matches /^[A-Za-z0-9._-]+$/, so a
1263
+ // slash is already illegal in one and unambiguous in the other. That is the
1264
+ // rule npm and docker use, and it means nobody has to discover a flag before
1265
+ // they can install somebody else's policies.
1266
+ //
1267
+ // Honors the same --cli / --scope / --beta flags as `policies --install`.
1268
+ if (
1269
+ args[0] === "policies" &&
1270
+ (args[1] === "add" || args[1] === "remove" || args[1] === "show")
1271
+ ) {
1272
+ lastSubcommand = "policy";
1273
+ const subArgs = args.slice(1);
1274
+
1275
+ if (subArgs.length === 0 || subArgs.includes("--help") || subArgs.includes("-h")) {
1276
+ console.log(`
1277
+ failproofai policies add|remove|show — choose what your agents may do
1278
+
1279
+ USAGE
1280
+ failproofai policies add Pick from what is installed here
1281
+ failproofai policies add <name> Turn one policy on
1282
+ failproofai policies add <owner>/<repo> Install someone's pack
1283
+ failproofai policies add core Our pack, from this package, offline
1284
+ failproofai policies remove <name> Turn one policy off
1285
+ failproofai policies remove <pack-id> Uninstall a pack
1286
+ failproofai policies show <owner>/<repo> What a pack contains, before you take it
1287
+
1288
+ A NAME OR A SOURCE
1289
+ Anything with a slash is a pack source; anything without is a policy name.
1290
+ Policy names cannot contain a slash, so there is nothing to guess at.
1291
+
1292
+ block-sudo a policy
1293
+ acme/deploy-guard a pack, newest release, pinned
1294
+ acme/deploy-guard@v2.1.0 that release
1295
+ github:acme/deploy-guard@v2.1.0 same, explicit
1296
+ https://github.com/acme/x/releases/tag/v2 the URL you copied
1297
+
1298
+ CHOOSING PART OF A PACK
1299
+ With no flags you get the pack's own defaults and are shown the rest.
1300
+ --policy a,b exactly these
1301
+ --category x,y whole categories (see: failproofai policies show <source>)
1302
+ --all everything it contains
1303
+ Re-adding at a newer version keeps what you chose.
1304
+
1305
+ OPTIONS (policy names only)
1306
+ --cli claude|codex|copilot|cursor|opencode|pi|hermes|openclaw|factory|devin|antigravity|goose
1307
+ Agent CLI(s) to apply to; space-separated or repeated.
1308
+ Omit to detect installed CLIs and prompt.
1309
+ --scope user|project|local Config scope (default: user)
1310
+ --beta Allow beta policies
1311
+
1312
+ EXAMPLES
1313
+ failproofai policies add
1314
+ failproofai policies add block-sudo
1315
+ failproofai policies add sanitize-api-keys --scope project
1316
+ failproofai policies add core --category sanitize,git
1317
+ failproofai policies add acme/deploy-guard --policy block-prod-deploy
1318
+ failproofai policies show acme/deploy-guard
1319
+ failproofai policies remove block-sudo
1320
+
1321
+ Publishing your own: failproofai publish --help
1322
+ Offline: FAILPROOFAI_NO_DOWNLOAD=1 refuses to fetch; packs already installed
1323
+ keep enforcing. FAILPROOFAI_PACK_BASE_URL points it all at a mirror.
1324
+ `.trimStart());
1325
+ process.exit(0);
1326
+ }
1327
+
1328
+ const action = subArgs[0];
1329
+ const rest = subArgs.slice(1);
1330
+
1331
+ // A policy name matches /^[A-Za-z0-9._-]+$/ (pack-manifest.ts owns that
1332
+ // rule), so a slash can only ever mean a pack. `core` is the one name
1333
+ // without a slash that is still a source — it is the short spelling of our
1334
+ // own pack, and is checked by the pack lane itself rather than duplicated
1335
+ // here.
1336
+ const firstPositional = rest.find((a) => !a.startsWith("-"));
1337
+ let looksLikeSource =
1338
+ !!firstPositional &&
1339
+ (firstPositional.includes("/") || firstPositional.startsWith("github:"));
1340
+ if (!looksLikeSource && firstPositional) {
1341
+ // `core` is the one sourcey word with no slash in it. Read the alias set
1342
+ // from the layer that OWNS it rather than restating it here, where the
1343
+ // copy would drift the first time an alias is added — that exact drift
1344
+ // already shipped once, when the dashboard could not resolve a name the
1345
+ // CLI could.
1346
+ const { CORE_ALIASES } = await import("../src/hooks/pack-store");
1347
+ looksLikeSource = CORE_ALIASES.has(firstPositional.toLowerCase());
1348
+ }
1349
+
1350
+ if (action === "show" || looksLikeSource) {
1351
+ if (action === "show" && !firstPositional) {
1352
+ throw new CliError(
1353
+ "Usage: failproofai policies show <owner>/<repo>\n" +
1354
+ "Run `failproofai policies` to see what is already installed here.",
1355
+ );
1356
+ }
1357
+ // One lane, one implementation. `show` is the pack lane's remote preview,
1358
+ // and add/remove of a source are its add/remove — routed by translating
1359
+ // the words rather than by growing a second copy of either.
1360
+ const packArgs =
1361
+ action === "show" ? ["list", ...rest] : [action, ...rest];
1362
+ const { runPackCommand } = await import("../src/hooks/pack-cli");
1363
+ const result = await runPackCommand(packArgs);
1364
+ for (const line of result.lines) {
1365
+ if (result.exitCode === 0) console.log(line);
1366
+ else console.error(line);
1367
+ }
1368
+ await track("cli_pack", {
1369
+ ok: result.exitCode === 0,
1370
+ // The subcommand only — never the pack id, source or policy names. A
1371
+ // pack source is a value the user typed and a third-party pack name is
1372
+ // a publisher-controlled string; we send shape, never value.
1373
+ sub: action,
1374
+ });
1375
+ lastSubcommand = null;
1376
+ await exitAfterFlush(result.exitCode);
1377
+ return;
1378
+ }
1379
+
1380
+ if (action !== "add" && action !== "remove") {
1381
+ throw new CliError(
1382
+ `Unknown policies subcommand: ${action}\n` +
1383
+ `Run \`failproofai policies --help\` for usage.`,
1384
+ );
1385
+ }
1386
+
1387
+ const scopeIdx = rest.indexOf("--scope");
1388
+ const scope = scopeIdx >= 0 ? rest[scopeIdx + 1] : "user";
1389
+ if (scopeIdx >= 0 && (!scope || scope.startsWith("-"))) {
1390
+ throw new CliError("Missing value for --scope. Valid values: user, project, local");
1391
+ }
1392
+ const validScopes = action === "remove"
1393
+ ? ["user", "project", "local", "all"]
1394
+ : ["user", "project", "local"];
1395
+ if (scopeIdx >= 0 && !validScopes.includes(scope)) {
1396
+ throw new CliError(`Invalid scope: ${scope}. Valid values: ${validScopes.join(", ")}`);
1397
+ }
1398
+
1399
+ // --cli accepts one or more space-separated values, optionally repeated.
1400
+ const VALID_CLIS = new Set(["claude", "codex", "copilot", "cursor", "opencode", "pi", "hermes", "openclaw", "factory", "devin", "antigravity", "goose"]);
1401
+ const cliFlagValues = [];
1402
+ const cliConsumedIdxs = new Set();
1403
+ const cliFlagIdxs = rest.map((a, i) => (a === "--cli" ? i : -1)).filter((i) => i >= 0);
1404
+ for (const idx of cliFlagIdxs) {
1405
+ let consumed = 0;
1406
+ for (let j = idx + 1; j < rest.length; j++) {
1407
+ const v = rest[j];
1408
+ if (v.startsWith("-")) break;
1409
+ if (!VALID_CLIS.has(v)) break;
1410
+ cliFlagValues.push(v);
1411
+ cliConsumedIdxs.add(j);
1412
+ consumed++;
1413
+ }
1414
+ if (consumed === 0) {
1415
+ throw new CliError("Missing value(s) for --cli. Usage: --cli claude codex copilot cursor opencode pi hermes openclaw (or any subset)");
1416
+ }
1417
+ }
1418
+
1419
+ const includeBeta = rest.includes("--beta");
1420
+
1421
+ // Reject unknown flags.
1422
+ const knownFlags = new Set(["--scope", "--cli", "--beta"]);
1423
+ const unknownFlag = rest.find((a) => a.startsWith("-") && !knownFlags.has(a));
1424
+ if (unknownFlag) {
1425
+ throw new CliError(`Unknown flag: ${unknownFlag}\nRun \`failproofai policy --help\` for usage.`);
1426
+ }
1427
+
1428
+ // Positional policy names = anything not consumed by --scope / --cli.
1429
+ const consumedIdxs = new Set();
1430
+ if (scopeIdx >= 0) consumedIdxs.add(scopeIdx + 1);
1431
+ for (const i of cliConsumedIdxs) consumedIdxs.add(i);
1432
+ const positional = rest.filter(
1433
+ (a, idx) => !a.startsWith("-") && !consumedIdxs.has(idx),
1434
+ );
1435
+
1436
+ if (positional.length === 0) {
1437
+ // Naming nothing is a question, not a mistake. It used to be an error
1438
+ // telling you to go and read a list somewhere else and come back — so
1439
+ // the answer is to SHOW the list, here, with what is already on ticked.
1440
+ const { runPolicyPicker } = await import("../src/hooks/pack-cli");
1441
+ const result = await runPolicyPicker(action, { stdin: process.stdin, stdout: process.stdout });
1442
+ for (const line of result.lines) {
1443
+ if (result.exitCode === 0) console.log(line);
1444
+ else console.error(line);
1445
+ }
1446
+ await track("cli_policy_picker", { ok: result.exitCode === 0, action });
1447
+ lastSubcommand = null;
1448
+ await exitAfterFlush(result.exitCode);
1449
+ return;
1450
+ }
1451
+ if (positional.length > 1) {
1452
+ throw new CliError(
1453
+ `\`policy ${action}\` takes exactly one policy name (got ${positional.length}).\n` +
1454
+ `For multiple policies use \`failproofai policies --${action === "add" ? "install" : "uninstall"} ${positional.join(" ")}\`.`,
1455
+ );
1456
+ }
1457
+ const policyName = positional[0];
1458
+
1459
+ const { resolveTargetClis } = await import("../src/hooks/install-prompt");
1460
+ const cli = await resolveTargetClis(
1461
+ cliFlagValues.length > 0 ? cliFlagValues : undefined,
1462
+ action === "add" ? "install" : "uninstall",
1463
+ );
1464
+
1465
+ lastPolicyAction = action;
1466
+ if (action === "add") {
1467
+ const { installHooks } = await import("../src/hooks/manager");
1468
+ await installHooks(
1469
+ [policyName],
1470
+ scope,
1471
+ undefined,
1472
+ includeBeta,
1473
+ undefined,
1474
+ undefined,
1475
+ false,
1476
+ cli,
1477
+ );
1478
+ await track("cli_policy_add_success", {
1479
+ scope,
1480
+ cli,
1481
+ cli_count: cli.length,
1482
+ policy_name: policyName,
1483
+ include_beta: includeBeta,
1484
+ });
1485
+ } else {
1486
+ // `policy remove <name>` always removes the named policy regardless
1487
+ // of whether it's beta or not — passing `betaOnly: includeBeta`
1488
+ // here was a mislabel that only affected the telemetry field, not
1489
+ // the actual remove. Drop the `--beta` semantic for remove and
1490
+ // emit beta_only: false unconditionally so dashboards don't see
1491
+ // ghost "beta removal" events.
1492
+ const { removeHooks } = await import("../src/hooks/manager");
1493
+ await removeHooks(
1494
+ [policyName],
1495
+ scope,
1496
+ undefined,
1497
+ { betaOnly: false, removeCustomHooks: false, cli },
1498
+ );
1499
+ await track("cli_policy_remove_success", {
1500
+ scope,
1501
+ cli,
1502
+ cli_count: cli.length,
1503
+ policy_name: policyName,
1504
+ beta_only: false,
1505
+ });
1506
+ }
1507
+ lastPolicyAction = null;
1508
+ process.exit(0);
1509
+ }
1220
1510
  // policies [--install|-i|--uninstall|-u|--help|-h] [names...] [--scope] [--beta] [--custom|-c <path>]
1221
1511
  if (args[0] === "policies") {
1222
1512
  const subArgs = args.slice(1);
@@ -1231,8 +1521,14 @@ failproofai policies — manage Failproof AI policies
1231
1521
 
1232
1522
  USAGE
1233
1523
  failproofai policies List all policies and their status
1234
- failproofai policies --install, -i Enable policies
1235
- failproofai policies --uninstall, -u Disable policies or remove hooks
1524
+ failproofai policies add [what] Turn one on, take a pack, or pick
1525
+ failproofai policies remove <what> Turn one off, or uninstall a pack
1526
+ failproofai policies show <owner>/<repo> What a pack holds, before you take it
1527
+ failproofai policies --install, -i Wire policies into your agent CLIs
1528
+ failproofai policies --uninstall, -u Unwire them, or strip the hooks
1529
+
1530
+ policy, pack and p are all spellings of policies.
1531
+ Detail on the first three: failproofai policies add --help
1236
1532
 
1237
1533
  OPTIONS (install)
1238
1534
  [names...] Specific policy names to enable (omit for interactive)
@@ -1497,163 +1793,6 @@ EXAMPLES
1497
1793
  return;
1498
1794
  }
1499
1795
 
1500
- // policy — single-policy shortcut over `policies --install <name>`.
1501
- // failproofai policy add <name> enable one policy (defaults: claude/user)
1502
- // failproofai policy remove <name> disable one policy
1503
- // Honors the same --cli / --scope / --beta flags as `policies --install`.
1504
- if (args[0] === "policy") {
1505
- lastSubcommand = "policy";
1506
- const subArgs = args.slice(1);
1507
-
1508
- if (subArgs.length === 0 || subArgs.includes("--help") || subArgs.includes("-h")) {
1509
- console.log(`
1510
- failproofai policy — manage a single FailproofAI policy
1511
-
1512
- USAGE
1513
- failproofai policy add <name> Enable one policy
1514
- failproofai policy remove <name> Disable one policy
1515
-
1516
- OPTIONS
1517
- --cli claude|codex|copilot|cursor|opencode|pi|hermes|openclaw|factory|devin|antigravity|goose
1518
- Agent CLI(s) to apply to; space-separated or repeated.
1519
- Omit to detect installed CLIs and prompt.
1520
- --scope user|project|local Config scope (default: user)
1521
- --beta Allow beta policies
1522
-
1523
- EXAMPLES
1524
- failproofai policy add block-sudo
1525
- failproofai policy add sanitize-api-keys --scope project
1526
- failproofai policy add block-force-push --cli claude codex
1527
- failproofai policy remove block-sudo
1528
- `.trimStart());
1529
- process.exit(0);
1530
- }
1531
-
1532
- const action = subArgs[0];
1533
- if (action !== "add" && action !== "remove") {
1534
- throw new CliError(
1535
- `Unknown policy subcommand: ${action}\n` +
1536
- `Run \`failproofai policy --help\` for usage.`,
1537
- );
1538
- }
1539
-
1540
- const rest = subArgs.slice(1);
1541
-
1542
- const scopeIdx = rest.indexOf("--scope");
1543
- const scope = scopeIdx >= 0 ? rest[scopeIdx + 1] : "user";
1544
- if (scopeIdx >= 0 && (!scope || scope.startsWith("-"))) {
1545
- throw new CliError("Missing value for --scope. Valid values: user, project, local");
1546
- }
1547
- const validScopes = action === "remove"
1548
- ? ["user", "project", "local", "all"]
1549
- : ["user", "project", "local"];
1550
- if (scopeIdx >= 0 && !validScopes.includes(scope)) {
1551
- throw new CliError(`Invalid scope: ${scope}. Valid values: ${validScopes.join(", ")}`);
1552
- }
1553
-
1554
- // --cli accepts one or more space-separated values, optionally repeated.
1555
- const VALID_CLIS = new Set(["claude", "codex", "copilot", "cursor", "opencode", "pi", "hermes", "openclaw", "factory", "devin", "antigravity", "goose"]);
1556
- const cliFlagValues = [];
1557
- const cliConsumedIdxs = new Set();
1558
- const cliFlagIdxs = rest.map((a, i) => (a === "--cli" ? i : -1)).filter((i) => i >= 0);
1559
- for (const idx of cliFlagIdxs) {
1560
- let consumed = 0;
1561
- for (let j = idx + 1; j < rest.length; j++) {
1562
- const v = rest[j];
1563
- if (v.startsWith("-")) break;
1564
- if (!VALID_CLIS.has(v)) break;
1565
- cliFlagValues.push(v);
1566
- cliConsumedIdxs.add(j);
1567
- consumed++;
1568
- }
1569
- if (consumed === 0) {
1570
- throw new CliError("Missing value(s) for --cli. Usage: --cli claude codex copilot cursor opencode pi hermes openclaw (or any subset)");
1571
- }
1572
- }
1573
-
1574
- const includeBeta = rest.includes("--beta");
1575
-
1576
- // Reject unknown flags.
1577
- const knownFlags = new Set(["--scope", "--cli", "--beta"]);
1578
- const unknownFlag = rest.find((a) => a.startsWith("-") && !knownFlags.has(a));
1579
- if (unknownFlag) {
1580
- throw new CliError(`Unknown flag: ${unknownFlag}\nRun \`failproofai policy --help\` for usage.`);
1581
- }
1582
-
1583
- // Positional policy names = anything not consumed by --scope / --cli.
1584
- const consumedIdxs = new Set();
1585
- if (scopeIdx >= 0) consumedIdxs.add(scopeIdx + 1);
1586
- for (const i of cliConsumedIdxs) consumedIdxs.add(i);
1587
- const positional = rest.filter(
1588
- (a, idx) => !a.startsWith("-") && !consumedIdxs.has(idx),
1589
- );
1590
-
1591
- if (positional.length === 0) {
1592
- throw new CliError(
1593
- `Missing policy name.\n` +
1594
- `Usage: failproofai policy ${action} <name>\n` +
1595
- `Run \`failproofai policies\` to see available names.`,
1596
- );
1597
- }
1598
- if (positional.length > 1) {
1599
- throw new CliError(
1600
- `\`policy ${action}\` takes exactly one policy name (got ${positional.length}).\n` +
1601
- `For multiple policies use \`failproofai policies --${action === "add" ? "install" : "uninstall"} ${positional.join(" ")}\`.`,
1602
- );
1603
- }
1604
- const policyName = positional[0];
1605
-
1606
- const { resolveTargetClis } = await import("../src/hooks/install-prompt");
1607
- const cli = await resolveTargetClis(
1608
- cliFlagValues.length > 0 ? cliFlagValues : undefined,
1609
- action === "add" ? "install" : "uninstall",
1610
- );
1611
-
1612
- lastPolicyAction = action;
1613
- if (action === "add") {
1614
- const { installHooks } = await import("../src/hooks/manager");
1615
- await installHooks(
1616
- [policyName],
1617
- scope,
1618
- undefined,
1619
- includeBeta,
1620
- undefined,
1621
- undefined,
1622
- false,
1623
- cli,
1624
- );
1625
- await track("cli_policy_add_success", {
1626
- scope,
1627
- cli,
1628
- cli_count: cli.length,
1629
- policy_name: policyName,
1630
- include_beta: includeBeta,
1631
- });
1632
- } else {
1633
- // `policy remove <name>` always removes the named policy regardless
1634
- // of whether it's beta or not — passing `betaOnly: includeBeta`
1635
- // here was a mislabel that only affected the telemetry field, not
1636
- // the actual remove. Drop the `--beta` semantic for remove and
1637
- // emit beta_only: false unconditionally so dashboards don't see
1638
- // ghost "beta removal" events.
1639
- const { removeHooks } = await import("../src/hooks/manager");
1640
- await removeHooks(
1641
- [policyName],
1642
- scope,
1643
- undefined,
1644
- { betaOnly: false, removeCustomHooks: false, cli },
1645
- );
1646
- await track("cli_policy_remove_success", {
1647
- scope,
1648
- cli,
1649
- cli_count: cli.length,
1650
- policy_name: policyName,
1651
- beta_only: false,
1652
- });
1653
- }
1654
- lastPolicyAction = null;
1655
- process.exit(0);
1656
- }
1657
1796
 
1658
1797
  // config — the interactive setup launcher (scope, agents, policies).
1659
1798
  // `configure` and `setup` are canonicalized to "config" up top. Running it