failproofai 0.0.2-beta.1 → 0.0.2-beta.3

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 (132) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  6. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  7. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  9. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
  10. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  11. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
  12. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
  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/server-reference-manifest.json +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  16. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  17. package/.next/standalone/.next/server/app/_not-found.html +2 -2
  18. package/.next/standalone/.next/server/app/_not-found.rsc +17 -17
  19. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +17 -17
  20. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  21. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +11 -11
  22. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  23. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  24. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  25. package/.next/standalone/.next/server/app/index.html +1 -1
  26. package/.next/standalone/.next/server/app/index.rsc +16 -16
  27. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  28. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +16 -16
  29. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
  30. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +11 -11
  31. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  32. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  33. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  35. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +8 -8
  36. package/.next/standalone/.next/server/app/policies/page.js +1 -1
  37. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  39. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  40. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  41. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  42. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  43. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  44. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  45. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  46. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  47. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  49. package/.next/standalone/.next/server/chunks/[root-of-the-server]__02nt~6d._.js +1 -1
  50. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_0z7w.hh._.js +1 -1
  51. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0a3kr67._.js → [root-of-the-server]__07k6eu-._.js} +2 -2
  52. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__092s1ta._.js +2 -2
  53. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__09icjsf._.js +2 -2
  54. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0g.lg8b._.js +2 -2
  55. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h..k-e._.js +2 -2
  56. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0rbuarm._.js → [root-of-the-server]__0kfv9fw._.js} +2 -2
  57. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0osi8nq._.js → [root-of-the-server]__0okos0k._.js} +3 -3
  58. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0w6l33k._.js +5 -4
  59. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11pa2ra._.js +2 -2
  60. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12t-wym._.js +2 -2
  61. package/.next/standalone/.next/server/chunks/ssr/_10lm7or._.js +1 -1
  62. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_0xerkr6._.js +1 -1
  63. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_0q-m0y-._.js +1 -1
  64. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_0rd0oc-._.js +1 -1
  65. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  66. package/.next/standalone/.next/server/pages/404.html +2 -2
  67. package/.next/standalone/.next/server/pages/500.html +1 -1
  68. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  69. package/.next/standalone/.next/server/server-reference-manifest.json +9 -9
  70. package/.next/standalone/.next/static/chunks/{0a08gn8709y98.js → 0.jo.465b6_k..js} +1 -1
  71. package/.next/standalone/.next/static/chunks/{0jhw8ofx.5g_e.js → 01haq0a3zrx0v.js} +1 -1
  72. package/.next/standalone/.next/static/chunks/08f78tecvx61l.css +1 -0
  73. package/.next/standalone/.next/static/chunks/{0mr-jhx402yci.js → 0a6xi1a8f_qlp.js} +1 -1
  74. package/.next/standalone/.next/static/chunks/{0qvj8bhl661lq.js → 0mq7ze1vkeo1p.js} +1 -1
  75. package/.next/standalone/.next/static/chunks/{0gcz-jqgqz~9m.js → 0p_fpyfmmohnx.js} +1 -1
  76. package/.next/standalone/.next/static/chunks/{0kob_5.phc~sk.js → 0qwyj3m400l_g.js} +1 -1
  77. package/.next/standalone/.next/static/chunks/{0mjc3aq2wxvlt.js → 0t94r_mk0s7e4.js} +1 -1
  78. package/.next/standalone/.next/static/chunks/{0q7z97izctgrw.js → 139~00zc9.u7s.js} +1 -1
  79. package/.next/standalone/Dockerfile.docs +12 -0
  80. package/.next/standalone/README.md +68 -55
  81. package/.next/standalone/bin/failproofai.mjs +221 -128
  82. package/.next/standalone/dist/cli.mjs +415 -106
  83. package/.next/standalone/dist/index.js +2 -2
  84. package/.next/standalone/docs/{architecture.md → architecture.mdx} +40 -23
  85. package/.next/standalone/docs/{built-in-policies.md → built-in-policies.mdx} +134 -12
  86. package/.next/standalone/docs/cli/dashboard.mdx +28 -0
  87. package/.next/standalone/docs/cli/environment-variables.mdx +34 -0
  88. package/.next/standalone/docs/cli/hook.mdx +30 -0
  89. package/.next/standalone/docs/cli/install-policies.mdx +48 -0
  90. package/.next/standalone/docs/cli/list-policies.mdx +31 -0
  91. package/.next/standalone/docs/cli/remove-policies.mdx +44 -0
  92. package/.next/standalone/docs/cli/version.mdx +12 -0
  93. package/.next/standalone/docs/{configuration.md → configuration.mdx} +16 -16
  94. package/.next/standalone/docs/{custom-hooks.md → custom-policies.mdx} +80 -42
  95. package/.next/standalone/docs/{dashboard.md → dashboard.mdx} +26 -29
  96. package/.next/standalone/docs/docs.json +31 -4
  97. package/.next/standalone/docs/examples.mdx +253 -0
  98. package/.next/standalone/docs/for-agents.mdx +38 -0
  99. package/.next/standalone/docs/getting-started.mdx +134 -0
  100. package/.next/standalone/docs/introduction.mdx +57 -0
  101. package/.next/standalone/docs/logo/dark.svg +21 -0
  102. package/.next/standalone/docs/logo/light.svg +21 -0
  103. package/.next/standalone/docs/{package-aliases.md → package-aliases.mdx} +5 -5
  104. package/.next/standalone/docs/{testing.md → testing.mdx} +11 -11
  105. package/.next/standalone/package.json +6 -9
  106. package/.next/standalone/scripts/publish-aliases.mjs +4 -2
  107. package/.next/standalone/skills-lock.json +10 -0
  108. package/.next/standalone/src/cli-error.ts +18 -0
  109. package/.next/standalone/src/hooks/builtin-policies.ts +259 -20
  110. package/.next/standalone/src/hooks/manager.ts +17 -3
  111. package/.next/standalone/src/hooks/policy-evaluator.ts +19 -1
  112. package/.next/standalone/src/hooks/policy-helpers.ts +2 -2
  113. package/.next/standalone/vitest.config.e2e.mts +3 -0
  114. package/.next/standalone/vitest.config.mts +3 -0
  115. package/README.md +68 -55
  116. package/bin/failproofai.mjs +221 -128
  117. package/dist/cli.mjs +415 -106
  118. package/dist/index.js +2 -2
  119. package/package.json +6 -9
  120. package/scripts/publish-aliases.mjs +4 -2
  121. package/src/cli-error.ts +18 -0
  122. package/src/hooks/builtin-policies.ts +259 -20
  123. package/src/hooks/manager.ts +17 -3
  124. package/src/hooks/policy-evaluator.ts +19 -1
  125. package/src/hooks/policy-helpers.ts +2 -2
  126. package/.next/standalone/.next/static/chunks/15jpradyu_531.css +0 -1
  127. package/.next/standalone/docs/cli-reference.md +0 -175
  128. package/.next/standalone/docs/getting-started.md +0 -128
  129. package/.next/standalone/docs/introduction.md +0 -47
  130. /package/.next/standalone/.next/static/{Dnk96sbMPjYOx1pdLdOH0 → 7fR022u1Sj-s5MfKO1q9Y}/_buildManifest.js +0 -0
  131. /package/.next/standalone/.next/static/{Dnk96sbMPjYOx1pdLdOH0 → 7fR022u1Sj-s5MfKO1q9Y}/_clientMiddlewareManifest.js +0 -0
  132. /package/.next/standalone/.next/static/{Dnk96sbMPjYOx1pdLdOH0 → 7fR022u1Sj-s5MfKO1q9Y}/_ssgManifest.js +0 -0
package/dist/cli.mjs CHANGED
@@ -179,8 +179,8 @@ var init_hooks_config = __esm(() => {
179
179
  });
180
180
 
181
181
  // src/hooks/policy-helpers.ts
182
- function allow() {
183
- return { decision: "allow" };
182
+ function allow(reason) {
183
+ return reason ? { decision: "allow", reason } : { decision: "allow" };
184
184
  }
185
185
  function deny(reason) {
186
186
  return { decision: "deny", reason };
@@ -248,7 +248,7 @@ var REGISTRY_KEY = "__FAILPROOFAI_POLICY_REGISTRY__", INDEX_CACHE_KEY = "__FAILP
248
248
  // src/hooks/builtin-policies.ts
249
249
  import { resolve as resolve2, join as join2 } from "node:path";
250
250
  import { readFile, writeFile } from "node:fs/promises";
251
- import { execSync } from "node:child_process";
251
+ import { execSync, execFileSync } from "node:child_process";
252
252
  import { homedir as homedir3 } from "node:os";
253
253
  function isClaudeInternalPath(resolved2) {
254
254
  const claudeDir = join2(homedir3(), ".claude");
@@ -266,6 +266,22 @@ function getFilePath(ctx) {
266
266
  function parseArgvTokens(cmd) {
267
267
  return cmd.trim().split(/\s+/).map((t) => t.replace(/^['"]|['"]$/g, ""));
268
268
  }
269
+ function getCurrentBranch(cwd) {
270
+ try {
271
+ let branch = gitBranchCache.get(cwd);
272
+ if (branch === undefined) {
273
+ branch = execSync("git rev-parse --abbrev-ref HEAD", {
274
+ cwd,
275
+ encoding: "utf8",
276
+ timeout: 3000
277
+ }).trim();
278
+ gitBranchCache.set(cwd, branch);
279
+ }
280
+ return branch || null;
281
+ } catch {
282
+ return null;
283
+ }
284
+ }
269
285
  function matchesAllowedPattern(cmd, pattern) {
270
286
  const cmdTokens = parseArgvTokens(cmd);
271
287
  const patTokens = parseArgvTokens(pattern);
@@ -653,22 +669,12 @@ function blockWorkOnMain(ctx) {
653
669
  const cwd = ctx.session?.cwd;
654
670
  if (!cwd)
655
671
  return allow();
656
- try {
657
- let branch = gitBranchCache.get(cwd);
658
- if (branch === undefined) {
659
- branch = execSync("git rev-parse --abbrev-ref HEAD", {
660
- cwd,
661
- encoding: "utf8",
662
- timeout: 3000
663
- }).trim();
664
- gitBranchCache.set(cwd, branch);
665
- }
666
- const protectedBranches = ctx.params?.protectedBranches ?? ["main", "master"];
667
- if (protectedBranches.includes(branch)) {
668
- return deny(`Git ${cmd.match(/git\s+(\S+)/)?.[1] ?? "operation"} on ${branch} is blocked. Create a feature branch first.`);
669
- }
670
- } catch {
672
+ const branch = getCurrentBranch(cwd);
673
+ if (!branch)
671
674
  return allow();
675
+ const protectedBranches = ctx.params?.protectedBranches ?? ["main", "master"];
676
+ if (protectedBranches.includes(branch)) {
677
+ return deny(`Git ${cmd.match(/git\s+(\S+)/)?.[1] ?? "operation"} on ${branch} is blocked. Create a feature branch first.`);
672
678
  }
673
679
  return allow();
674
680
  }
@@ -767,6 +773,137 @@ function warnBackgroundProcess(ctx) {
767
773
  }
768
774
  return allow();
769
775
  }
776
+ function requireCommitBeforeStop(ctx) {
777
+ const cwd = ctx.session?.cwd;
778
+ if (!cwd)
779
+ return allow("No working directory available, skipping commit check.");
780
+ try {
781
+ const status = execSync("git status --porcelain", {
782
+ cwd,
783
+ encoding: "utf8",
784
+ timeout: 5000
785
+ }).trim();
786
+ if (status.length > 0) {
787
+ return deny("You have uncommitted changes in the working directory. Commit all changes before stopping.");
788
+ }
789
+ return allow("All changes are committed.");
790
+ } catch {
791
+ return allow("Not a git repository, skipping commit check.");
792
+ }
793
+ }
794
+ function requirePushBeforeStop(ctx) {
795
+ const cwd = ctx.session?.cwd;
796
+ if (!cwd)
797
+ return allow("No working directory available, skipping push check.");
798
+ try {
799
+ const remotes = execSync("git remote", {
800
+ cwd,
801
+ encoding: "utf8",
802
+ timeout: 3000
803
+ }).trim();
804
+ if (!remotes)
805
+ return allow("No git remote configured, skipping push check.");
806
+ const remote = ctx.params?.remote ?? "origin";
807
+ const branch = getCurrentBranch(cwd);
808
+ if (!branch || branch === "HEAD")
809
+ return allow("Detached HEAD, skipping push check.");
810
+ let hasTracking = false;
811
+ try {
812
+ execFileSync("git", ["rev-parse", "--verify", `${remote}/${branch}`], {
813
+ cwd,
814
+ encoding: "utf8",
815
+ timeout: 3000
816
+ });
817
+ hasTracking = true;
818
+ } catch {}
819
+ if (!hasTracking) {
820
+ return deny(`Branch "${branch}" has not been pushed to remote "${remote}". ` + `Push your branch with: git push -u ${remote} ${branch}`);
821
+ }
822
+ const unpushed = execFileSync("git", ["log", `${remote}/${branch}..HEAD`, "--oneline"], {
823
+ cwd,
824
+ encoding: "utf8",
825
+ timeout: 5000
826
+ }).trim();
827
+ if (unpushed.length > 0) {
828
+ const commitCount = unpushed.split(`
829
+ `).length;
830
+ return deny(`You have ${commitCount} unpushed commit${commitCount > 1 ? "s" : ""} on branch "${branch}". ` + `Push your changes with: git push`);
831
+ }
832
+ return allow(`All commits pushed to "${remote}".`);
833
+ } catch {
834
+ return allow("Could not check push status, skipping.");
835
+ }
836
+ }
837
+ function requirePrBeforeStop(ctx) {
838
+ const cwd = ctx.session?.cwd;
839
+ if (!cwd)
840
+ return allow("No working directory available, skipping PR check.");
841
+ try {
842
+ try {
843
+ execSync("gh --version", { cwd, encoding: "utf8", timeout: 3000 });
844
+ } catch {
845
+ return allow("GitHub CLI (gh) not installed, skipping PR check.");
846
+ }
847
+ const branch = getCurrentBranch(cwd);
848
+ if (!branch || branch === "HEAD")
849
+ return allow("Detached HEAD, skipping PR check.");
850
+ let prJson;
851
+ try {
852
+ prJson = execSync("gh pr view --json number,url,state", {
853
+ cwd,
854
+ encoding: "utf8",
855
+ timeout: 15000
856
+ }).trim();
857
+ } catch {
858
+ return deny(`No pull request found for branch "${branch}". ` + `Create one with: gh pr create`);
859
+ }
860
+ const pr = JSON.parse(prJson);
861
+ if (pr.state === "OPEN") {
862
+ return allow(`PR #${pr.number} exists: ${pr.url}`);
863
+ }
864
+ return deny(`Pull request for branch "${branch}" is ${pr.state.toLowerCase()}. Create a new PR with: gh pr create`);
865
+ } catch {
866
+ return allow("Could not check PR status, skipping.");
867
+ }
868
+ }
869
+ function requireCiGreenBeforeStop(ctx) {
870
+ const cwd = ctx.session?.cwd;
871
+ if (!cwd)
872
+ return allow("No working directory available, skipping CI check.");
873
+ try {
874
+ try {
875
+ execSync("gh --version", { cwd, encoding: "utf8", timeout: 3000 });
876
+ } catch {
877
+ return allow("GitHub CLI (gh) not installed, skipping CI check.");
878
+ }
879
+ const branch = getCurrentBranch(cwd);
880
+ if (!branch || branch === "HEAD")
881
+ return allow("Detached HEAD, skipping CI check.");
882
+ const runsJson = execFileSync("gh", ["run", "list", "--branch", branch, "--limit", "5", "--json", "status,conclusion,name"], {
883
+ cwd,
884
+ encoding: "utf8",
885
+ timeout: 15000
886
+ }).trim();
887
+ if (!runsJson || runsJson === "[]")
888
+ return allow(`No CI runs found for branch "${branch}".`);
889
+ const runs = JSON.parse(runsJson);
890
+ if (runs.length === 0)
891
+ return allow(`No CI runs found for branch "${branch}".`);
892
+ const failing = runs.filter((r) => r.status === "completed" && r.conclusion !== "success" && r.conclusion !== "skipped");
893
+ if (failing.length > 0) {
894
+ const names = failing.map((r) => `"${r.name}"`).join(", ");
895
+ return deny(`CI checks are failing on branch "${branch}": ${names}. Fix the failing checks before stopping.`);
896
+ }
897
+ const pending = runs.filter((r) => r.status === "in_progress" || r.status === "queued" || r.status === "waiting");
898
+ if (pending.length > 0) {
899
+ const names = pending.map((r) => `"${r.name}"`).join(", ");
900
+ return deny(`CI checks are still running on branch "${branch}": ${names}. Wait for all checks to complete and verify they pass.`);
901
+ }
902
+ return allow(`All CI checks passed on branch "${branch}".`);
903
+ } catch {
904
+ return allow("Could not check CI status, skipping.");
905
+ }
906
+ }
770
907
  function registerBuiltinPolicies(enabledNames) {
771
908
  const enabledSet = new Set(enabledNames);
772
909
  for (const policy of BUILTIN_POLICIES) {
@@ -1102,6 +1239,49 @@ var init_builtin_policies = __esm(() => {
1102
1239
  match: { events: ["PreToolUse"] },
1103
1240
  defaultEnabled: false,
1104
1241
  category: "AI Behavior"
1242
+ },
1243
+ {
1244
+ name: "require-commit-before-stop",
1245
+ description: "Require all changes to be committed before Claude stops",
1246
+ fn: requireCommitBeforeStop,
1247
+ match: { events: ["Stop"] },
1248
+ defaultEnabled: false,
1249
+ category: "Workflow",
1250
+ beta: true
1251
+ },
1252
+ {
1253
+ name: "require-push-before-stop",
1254
+ description: "Require all commits to be pushed to remote before Claude stops",
1255
+ fn: requirePushBeforeStop,
1256
+ match: { events: ["Stop"] },
1257
+ defaultEnabled: false,
1258
+ category: "Workflow",
1259
+ beta: true,
1260
+ params: {
1261
+ remote: {
1262
+ type: "string",
1263
+ description: "Remote name to push to (default: origin)",
1264
+ default: "origin"
1265
+ }
1266
+ }
1267
+ },
1268
+ {
1269
+ name: "require-pr-before-stop",
1270
+ description: "Require a pull request to exist for the current branch before Claude stops",
1271
+ fn: requirePrBeforeStop,
1272
+ match: { events: ["Stop"] },
1273
+ defaultEnabled: false,
1274
+ category: "Workflow",
1275
+ beta: true
1276
+ },
1277
+ {
1278
+ name: "require-ci-green-before-stop",
1279
+ description: "Require CI checks to pass on the current branch before Claude stops",
1280
+ fn: requireCiGreenBeforeStop,
1281
+ match: { events: ["Stop"] },
1282
+ defaultEnabled: false,
1283
+ category: "Workflow",
1284
+ beta: true
1105
1285
  }
1106
1286
  ];
1107
1287
  });
@@ -1124,6 +1304,7 @@ async function evaluatePolicies(eventType, payload, session, config) {
1124
1304
  };
1125
1305
  let instructPolicyName = null;
1126
1306
  let instructReason = null;
1307
+ const allowMessages = [];
1127
1308
  for (const policy of policies) {
1128
1309
  const schema = POLICY_PARAMS_MAP.get(policy.name);
1129
1310
  let ctx;
@@ -1195,6 +1376,9 @@ async function evaluatePolicies(eventType, payload, session, config) {
1195
1376
  instructReason = result.reason ?? `Instruction from policy: ${policy.name}`;
1196
1377
  hookLogInfo(`instruct by "${policy.name}": ${instructReason}`);
1197
1378
  }
1379
+ if (result.decision === "allow" && result.reason) {
1380
+ allowMessages.push(result.reason);
1381
+ }
1198
1382
  }
1199
1383
  if (instructPolicyName && instructReason) {
1200
1384
  if (eventType === "Stop") {
@@ -1222,6 +1406,17 @@ async function evaluatePolicies(eventType, payload, session, config) {
1222
1406
  decision: "instruct"
1223
1407
  };
1224
1408
  }
1409
+ if (allowMessages.length > 0) {
1410
+ const combined = allowMessages.join(`
1411
+ `);
1412
+ const response = {
1413
+ hookSpecificOutput: {
1414
+ hookEventName: eventType,
1415
+ additionalContext: combined
1416
+ }
1417
+ };
1418
+ return { exitCode: 0, stdout: JSON.stringify(response), stderr: "", policyName: null, reason: combined, decision: "allow" };
1419
+ }
1225
1420
  return { exitCode: 0, stdout: "", stderr: "", policyName: null, reason: null, decision: "allow" };
1226
1421
  }
1227
1422
  var POLICY_PARAMS_MAP;
@@ -1536,7 +1731,7 @@ var init_hook_activity_store = __esm(() => {
1536
1731
  });
1537
1732
 
1538
1733
  // package.json
1539
- var version2 = "0.0.2-beta.1";
1734
+ var version2 = "0.0.2-beta.3";
1540
1735
  var init_package = () => {};
1541
1736
 
1542
1737
  // src/posthog-key.ts
@@ -2077,6 +2272,19 @@ var init_install_prompt = __esm(() => {
2077
2272
  init_builtin_policies();
2078
2273
  });
2079
2274
 
2275
+ // src/cli-error.ts
2276
+ var CliError;
2277
+ var init_cli_error = __esm(() => {
2278
+ CliError = class CliError extends Error {
2279
+ exitCode;
2280
+ constructor(message, exitCode = 1) {
2281
+ super(message);
2282
+ this.name = "CliError";
2283
+ this.exitCode = exitCode;
2284
+ }
2285
+ };
2286
+ });
2287
+
2080
2288
  // src/hooks/manager.ts
2081
2289
  var exports_manager = {};
2082
2290
  __export(exports_manager, {
@@ -2130,7 +2338,7 @@ function resolveFailproofaiBinary() {
2130
2338
  return result.split(`
2131
2339
  `)[0].trim();
2132
2340
  } catch {
2133
- throw new Error(`failproofai binary not found in PATH.
2341
+ throw new CliError(`failproofai binary not found in PATH.
2134
2342
  ` + "Install it globally first: npm install -g failproofai");
2135
2343
  }
2136
2344
  }
@@ -2144,7 +2352,7 @@ function validatePolicyNames(names) {
2144
2352
  const invalid = names.filter((n) => !VALID_POLICY_NAMES.has(n));
2145
2353
  if (invalid.length > 0) {
2146
2354
  const validList = [...VALID_POLICY_NAMES].join(", ");
2147
- throw new Error(`Unknown policy name(s): ${invalid.join(", ")}
2355
+ throw new CliError(`Unknown policy name(s): ${invalid.join(", ")}
2148
2356
  ` + `Valid policies: ${validList}`);
2149
2357
  }
2150
2358
  }
@@ -2211,6 +2419,15 @@ function removeHooksFromSettingsFile(settingsPath) {
2211
2419
  return removed;
2212
2420
  }
2213
2421
  async function installHooks(policyNames, scope = "user", cwd, includeBeta = false, source, customPoliciesPath, removeCustomHooks = false) {
2422
+ if (policyNames !== undefined && policyNames.length > 0) {
2423
+ const nonAllNames = policyNames.filter((n) => n !== "all");
2424
+ if (nonAllNames.length > 0)
2425
+ validatePolicyNames(nonAllNames);
2426
+ if (policyNames.includes("all") && nonAllNames.length > 0) {
2427
+ throw new CliError(`"all" cannot be combined with specific policy names.
2428
+ ` + `Use either: --install all or --install block-sudo sanitize-jwt ...`);
2429
+ }
2430
+ }
2214
2431
  const binaryPath = resolveFailproofaiBinary();
2215
2432
  const previousConfig = readHooksConfig();
2216
2433
  const previousEnabled = new Set(previousConfig.enabledPolicies);
@@ -2220,8 +2437,6 @@ async function installHooks(policyNames, scope = "user", cwd, includeBeta = fals
2220
2437
  if (policyNames.length === 1 && policyNames[0] === "all") {
2221
2438
  incoming = BUILTIN_POLICIES.filter((p) => includeBeta || !p.beta).map((p) => p.name);
2222
2439
  } else {
2223
- if (policyNames.length > 0)
2224
- validatePolicyNames(policyNames);
2225
2440
  incoming = policyNames;
2226
2441
  }
2227
2442
  selectedPolicies = [...new Set([...previousConfig.enabledPolicies, ...incoming])];
@@ -2565,6 +2780,7 @@ var init_manager = __esm(() => {
2565
2780
  init_custom_hooks_loader();
2566
2781
  init_hook_telemetry();
2567
2782
  init_telemetry_id();
2783
+ init_cli_error();
2568
2784
  VALID_POLICY_NAMES = new Set(BUILTIN_POLICIES.map((p) => p.name));
2569
2785
  });
2570
2786
 
@@ -2719,12 +2935,29 @@ var init_launch = __esm(() => {
2719
2935
  init_package();
2720
2936
  });
2721
2937
 
2938
+ // src/cli-error.ts
2939
+ var exports_cli_error = {};
2940
+ __export(exports_cli_error, {
2941
+ CliError: () => CliError2
2942
+ });
2943
+ var CliError2;
2944
+ var init_cli_error2 = __esm(() => {
2945
+ CliError2 = class CliError2 extends Error {
2946
+ exitCode;
2947
+ constructor(message, exitCode = 1) {
2948
+ super(message);
2949
+ this.name = "CliError";
2950
+ this.exitCode = exitCode;
2951
+ }
2952
+ };
2953
+ });
2954
+
2722
2955
  // bin/failproofai.mjs
2723
2956
  import { realpathSync as realpathSync2 } from "fs";
2724
2957
  import { dirname as dirname5, resolve as resolve8 } from "path";
2725
2958
  import { fileURLToPath as fileURLToPath2 } from "url";
2726
2959
  // package.json
2727
- var version = "0.0.2-beta.1";
2960
+ var version = "0.0.2-beta.3";
2728
2961
 
2729
2962
  // bin/failproofai.mjs
2730
2963
  if (!process.env.FAILPROOFAI_PACKAGE_ROOT) {
@@ -2736,9 +2969,32 @@ if (!process.env.FAILPROOFAI_DIST_PATH) {
2736
2969
  var args = process.argv.slice(2);
2737
2970
  if (args[0] === "p")
2738
2971
  args[0] = "policies";
2739
- var SUBCOMMANDS = ["policies"];
2740
- if ((args.includes("--help") || args.includes("-h")) && !SUBCOMMANDS.includes(args[0])) {
2741
- console.log(`
2972
+ var hookIdx = args.indexOf("--hook");
2973
+ if (hookIdx >= 0) {
2974
+ if (!args[hookIdx + 1]) {
2975
+ console.error("Error: Missing event type after --hook");
2976
+ console.error("Usage: failproofai --hook <event> (e.g. PreToolUse, PostToolUse)");
2977
+ process.exit(1);
2978
+ }
2979
+ try {
2980
+ const { handleHookEvent: handleHookEvent2 } = await Promise.resolve().then(() => (init_handler(), exports_handler));
2981
+ const exitCode = await handleHookEvent2(args[hookIdx + 1]);
2982
+ process.exit(exitCode);
2983
+ } catch (err) {
2984
+ const msg = err instanceof Error ? err.message : String(err);
2985
+ console.error(`Unexpected error: ${msg}`);
2986
+ process.exit(2);
2987
+ }
2988
+ }
2989
+ async function runCli() {
2990
+ const SUBCOMMANDS = ["policies"];
2991
+ if ((args.includes("--help") || args.includes("-h")) && !SUBCOMMANDS.includes(args[0])) {
2992
+ const extraArgs = args.filter((a) => a !== "--help" && a !== "-h");
2993
+ if (extraArgs.length > 0) {
2994
+ throw new CliError3(`Unexpected argument: ${extraArgs[0]}
2995
+ Run \`failproofai --help\` for usage.`);
2996
+ }
2997
+ console.log(`
2742
2998
  failproofai v${version}
2743
2999
 
2744
3000
  USAGE
@@ -2778,25 +3034,24 @@ LINKS
2778
3034
  \u2B50 Star us: https://github.com/exospherehost/failproofai
2779
3035
  \uD83D\uDCD6 Docs: https://befailproof.ai
2780
3036
  `.trimStart());
2781
- process.exit(0);
2782
- }
2783
- if (args.includes("--version") || args.includes("-v")) {
2784
- console.log(version);
2785
- process.exit(0);
2786
- }
2787
- var hookIdx = args.indexOf("--hook");
2788
- if (hookIdx >= 0 && args[hookIdx + 1]) {
2789
- const { handleHookEvent: handleHookEvent2 } = await Promise.resolve().then(() => (init_handler(), exports_handler));
2790
- const exitCode = await handleHookEvent2(args[hookIdx + 1]);
2791
- process.exit(exitCode);
2792
- }
2793
- if (args[0] === "policies") {
2794
- const subArgs = args.slice(1);
2795
- const isInstall = subArgs.includes("--install") || subArgs.includes("-i");
2796
- const isUninstall = subArgs.includes("--uninstall") || subArgs.includes("-u");
2797
- const isHelp = subArgs.includes("--help") || subArgs.includes("-h");
2798
- if (isHelp) {
2799
- console.log(`
3037
+ process.exit(0);
3038
+ }
3039
+ if ((args.includes("--version") || args.includes("-v")) && !SUBCOMMANDS.includes(args[0])) {
3040
+ const extraArgs = args.filter((a) => a !== "--version" && a !== "-v");
3041
+ if (extraArgs.length > 0) {
3042
+ throw new CliError3(`Unexpected argument: ${extraArgs[0]}
3043
+ Run \`failproofai --help\` for usage.`);
3044
+ }
3045
+ console.log(version);
3046
+ process.exit(0);
3047
+ }
3048
+ if (args[0] === "policies") {
3049
+ const subArgs = args.slice(1);
3050
+ const isInstall = subArgs.includes("--install") || subArgs.includes("-i");
3051
+ const isUninstall = subArgs.includes("--uninstall") || subArgs.includes("-u");
3052
+ const isHelp = subArgs.includes("--help") || subArgs.includes("-h");
3053
+ if (isHelp) {
3054
+ console.log(`
2800
3055
  failproofai policies \u2014 manage Failproof AI policies
2801
3056
 
2802
3057
  USAGE
@@ -2827,65 +3082,119 @@ EXAMPLES
2827
3082
  failproofai policies -u
2828
3083
  failproofai policies --uninstall --custom
2829
3084
  `.trimStart());
3085
+ process.exit(0);
3086
+ }
3087
+ if (isInstall) {
3088
+ const { installHooks: installHooks2 } = await Promise.resolve().then(() => (init_manager(), exports_manager));
3089
+ const scopeIdx = subArgs.indexOf("--scope");
3090
+ const scope = scopeIdx >= 0 ? subArgs[scopeIdx + 1] : "user";
3091
+ if (scopeIdx >= 0 && (!scope || scope.startsWith("-"))) {
3092
+ throw new CliError3("Missing value for --scope. Valid values: user, project, local");
3093
+ }
3094
+ if (scopeIdx >= 0 && !["user", "project", "local"].includes(scope)) {
3095
+ throw new CliError3(`Invalid scope: ${scope}. Valid values: user, project, local`);
3096
+ }
3097
+ const customIdx = subArgs.includes("--custom") ? subArgs.indexOf("--custom") : subArgs.includes("-c") ? subArgs.indexOf("-c") : -1;
3098
+ const customPoliciesPath = customIdx >= 0 ? subArgs[customIdx + 1] : undefined;
3099
+ if (customIdx >= 0 && (!customPoliciesPath || customPoliciesPath.startsWith("-"))) {
3100
+ throw new CliError3(`Missing path after --custom/-c
3101
+ Usage: --custom <path> (e.g. --custom ./my-policies.js)`);
3102
+ }
3103
+ const includeBeta = subArgs.includes("--beta");
3104
+ const consumedIdxs = new Set;
3105
+ if (scopeIdx >= 0)
3106
+ consumedIdxs.add(scopeIdx + 1);
3107
+ if (customIdx >= 0)
3108
+ consumedIdxs.add(customIdx + 1);
3109
+ const flags = new Set(["--install", "-i", "--scope", "--beta", "--custom", "-c"]);
3110
+ const unknownInstallFlag = subArgs.find((a) => a.startsWith("-") && !flags.has(a));
3111
+ if (unknownInstallFlag) {
3112
+ throw new CliError3(`Unknown flag: ${unknownInstallFlag}
3113
+ Run \`failproofai policies --help\` for usage.`);
3114
+ }
3115
+ const explicitPolicyNames = subArgs.filter((a, idx) => !a.startsWith("-") && !consumedIdxs.has(idx));
3116
+ const policyNames = explicitPolicyNames.length > 0 ? explicitPolicyNames : customPoliciesPath !== undefined ? [] : undefined;
3117
+ await installHooks2(policyNames, scope, undefined, includeBeta, undefined, customPoliciesPath);
3118
+ process.exit(0);
3119
+ }
3120
+ if (isUninstall) {
3121
+ const { removeHooks: removeHooks2 } = await Promise.resolve().then(() => (init_manager(), exports_manager));
3122
+ const scopeIdx = subArgs.indexOf("--scope");
3123
+ const scope = scopeIdx >= 0 ? subArgs[scopeIdx + 1] : "user";
3124
+ if (scopeIdx >= 0 && (!scope || scope.startsWith("-"))) {
3125
+ throw new CliError3("Missing value for --scope. Valid values: user, project, local, all");
3126
+ }
3127
+ if (scopeIdx >= 0 && !["user", "project", "local", "all"].includes(scope)) {
3128
+ throw new CliError3(`Invalid scope: ${scope}. Valid values: user, project, local, all`);
3129
+ }
3130
+ const betaOnly = subArgs.includes("--beta");
3131
+ const removeCustomHooks = subArgs.includes("--custom") || subArgs.includes("-c");
3132
+ const consumedIdxs = new Set;
3133
+ if (scopeIdx >= 0)
3134
+ consumedIdxs.add(scopeIdx + 1);
3135
+ const flags = new Set(["--uninstall", "-u", "--scope", "--beta", "--custom", "-c"]);
3136
+ const unknownUninstallFlag = subArgs.find((a) => a.startsWith("-") && !flags.has(a));
3137
+ if (unknownUninstallFlag) {
3138
+ throw new CliError3(`Unknown flag: ${unknownUninstallFlag}
3139
+ Run \`failproofai policies --help\` for usage.`);
3140
+ }
3141
+ const policyNames = subArgs.filter((a, idx) => !a.startsWith("-") && !consumedIdxs.has(idx));
3142
+ await removeHooks2(policyNames.length > 0 ? policyNames : undefined, scope, undefined, { betaOnly, removeCustomHooks });
3143
+ process.exit(0);
3144
+ }
3145
+ const knownListFlags = new Set(["--install", "-i", "--uninstall", "-u", "--help", "-h", "--list"]);
3146
+ const unknownListArg = subArgs.find((a) => a.startsWith("-") && !knownListFlags.has(a));
3147
+ if (unknownListArg) {
3148
+ throw new CliError3(`Unknown flag: ${unknownListArg}
3149
+ Run \`failproofai policies --help\` for usage.`);
3150
+ }
3151
+ const positionalArgs = subArgs.filter((a) => !a.startsWith("-"));
3152
+ if (positionalArgs.length > 0) {
3153
+ throw new CliError3(`Unexpected argument: ${positionalArgs[0]}
3154
+ Run \`failproofai policies --help\` for usage.`);
3155
+ }
3156
+ const { listHooks: listHooks2 } = await Promise.resolve().then(() => (init_manager(), exports_manager));
3157
+ await listHooks2();
2830
3158
  process.exit(0);
2831
3159
  }
2832
- if (isInstall) {
2833
- const { installHooks: installHooks2 } = await Promise.resolve().then(() => (init_manager(), exports_manager));
2834
- const scopeIdx = subArgs.indexOf("--scope");
2835
- const scope = scopeIdx >= 0 ? subArgs[scopeIdx + 1] : "user";
2836
- const customIdx = subArgs.includes("--custom") ? subArgs.indexOf("--custom") : subArgs.includes("-c") ? subArgs.indexOf("-c") : -1;
2837
- const customPoliciesPath = customIdx >= 0 ? subArgs[customIdx + 1] : undefined;
2838
- const includeBeta = subArgs.includes("--beta");
2839
- const consumed = new Set([scope, customPoliciesPath].filter(Boolean));
2840
- const flags = new Set(["--install", "-i", "--scope", "--beta", "--custom", "-c"]);
2841
- const explicitPolicyNames = subArgs.filter((a) => !a.startsWith("-") && !flags.has(a) && !consumed.has(a));
2842
- const policyNames = explicitPolicyNames.length > 0 ? explicitPolicyNames : customPoliciesPath !== undefined ? [] : undefined;
2843
- await installHooks2(policyNames, scope, undefined, includeBeta, undefined, customPoliciesPath);
2844
- process.exit(0);
2845
- }
2846
- if (isUninstall) {
2847
- const { removeHooks: removeHooks2 } = await Promise.resolve().then(() => (init_manager(), exports_manager));
2848
- const scopeIdx = subArgs.indexOf("--scope");
2849
- const scope = scopeIdx >= 0 ? subArgs[scopeIdx + 1] : "user";
2850
- const betaOnly = subArgs.includes("--beta");
2851
- const removeCustomHooks = subArgs.includes("--custom") || subArgs.includes("-c");
2852
- const consumed = new Set([scope].filter(Boolean));
2853
- const flags = new Set(["--uninstall", "-u", "--scope", "--beta", "--custom", "-c"]);
2854
- const policyNames = subArgs.filter((a) => !a.startsWith("-") && !flags.has(a) && !consumed.has(a));
2855
- await removeHooks2(policyNames.length > 0 ? policyNames : undefined, scope, undefined, { betaOnly, removeCustomHooks });
2856
- process.exit(0);
2857
- }
2858
- const { listHooks: listHooks2 } = await Promise.resolve().then(() => (init_manager(), exports_manager));
2859
- await listHooks2();
2860
- process.exit(0);
2861
- }
2862
- var knownFlags = ["--version", "-v", "--help", "-h", "--hook"];
2863
- var unknownFlag = args.find((a) => a.startsWith("-") && !knownFlags.includes(a));
2864
- if (unknownFlag) {
2865
- let levenshtein = function(a, b) {
2866
- const m = a.length, n = b.length;
2867
- const dp = Array.from({ length: m + 1 }, (_, i) => Array.from({ length: n + 1 }, (_2, j) => i === 0 ? j : j === 0 ? i : 0));
2868
- for (let i = 1;i <= m; i++)
2869
- for (let j = 1;j <= n; j++)
2870
- dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
2871
- return dp[m][n];
2872
- };
2873
- const primary = ["--version", "--help", "--hook", "policies"];
2874
- const closest = primary.reduce((best, flag) => {
2875
- const dist = levenshtein(unknownFlag, flag);
2876
- return dist < best.dist ? { flag, dist } : best;
2877
- }, { flag: primary[0], dist: Infinity });
2878
- console.error(`Unknown flag: ${unknownFlag}`);
2879
- console.error(`Did you mean: ${closest.flag}?`);
2880
- console.error(`Run \`failproofai --help\` for usage details.`);
2881
- process.exit(1);
2882
- }
2883
- var unknownSubcommand = args.find((a) => !a.startsWith("-") && a !== "policies");
2884
- if (unknownSubcommand) {
2885
- console.error(`Unknown command: ${unknownSubcommand}`);
2886
- console.error(`Did you mean: failproofai policies?`);
2887
- console.error(`Run \`failproofai --help\` for usage details.`);
2888
- process.exit(1);
2889
- }
2890
- var { launch: launch2 } = await Promise.resolve().then(() => (init_launch(), exports_launch));
2891
- launch2("start");
3160
+ const knownFlags = ["--version", "-v", "--help", "-h", "--hook"];
3161
+ const unknownFlag = args.find((a) => a.startsWith("-") && !knownFlags.includes(a));
3162
+ if (unknownFlag) {
3163
+ let levenshtein = function(a, b) {
3164
+ const m = a.length, n = b.length;
3165
+ const dp = Array.from({ length: m + 1 }, (_, i) => Array.from({ length: n + 1 }, (_2, j) => i === 0 ? j : j === 0 ? i : 0));
3166
+ for (let i = 1;i <= m; i++)
3167
+ for (let j = 1;j <= n; j++)
3168
+ dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
3169
+ return dp[m][n];
3170
+ };
3171
+ const primary = ["--version", "--help", "--hook", "policies"];
3172
+ const closest = primary.reduce((best, flag) => {
3173
+ const dist = levenshtein(unknownFlag, flag);
3174
+ return dist < best.dist ? { flag, dist } : best;
3175
+ }, { flag: primary[0], dist: Infinity });
3176
+ throw new CliError3(`Unknown flag: ${unknownFlag}
3177
+ Did you mean: ${closest.flag}?
3178
+ Run \`failproofai --help\` for usage details.`);
3179
+ }
3180
+ const unknownSubcommand = args.find((a) => !a.startsWith("-") && a !== "policies");
3181
+ if (unknownSubcommand) {
3182
+ throw new CliError3(`Unknown command: ${unknownSubcommand}
3183
+ Did you mean: failproofai policies?
3184
+ Run \`failproofai --help\` for usage details.`);
3185
+ }
3186
+ const { launch: launch2 } = await Promise.resolve().then(() => (init_launch(), exports_launch));
3187
+ launch2("start");
3188
+ }
3189
+ var { CliError: CliError3 } = await Promise.resolve().then(() => (init_cli_error2(), exports_cli_error));
3190
+ try {
3191
+ await runCli();
3192
+ } catch (err) {
3193
+ if (err instanceof CliError3) {
3194
+ console.error(`Error: ${err.message}`);
3195
+ process.exit(err.exitCode);
3196
+ }
3197
+ const msg = err instanceof Error ? err.message : String(err);
3198
+ console.error(`Unexpected error: ${msg}`);
3199
+ process.exit(2);
3200
+ }
package/dist/index.js CHANGED
@@ -69,8 +69,8 @@ function clearCustomHooks() {
69
69
  g[REGISTRY_KEY] = [];
70
70
  }
71
71
  // src/hooks/policy-helpers.ts
72
- function allow() {
73
- return { decision: "allow" };
72
+ function allow(reason) {
73
+ return reason ? { decision: "allow", reason } : { decision: "allow" };
74
74
  }
75
75
  function deny(reason) {
76
76
  return { decision: "deny", reason };