failproofai 0.0.10-beta.0 → 0.0.10-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 (97) 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/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  6. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  10. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
  11. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  12. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
  13. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
  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/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 +2 -2
  19. package/.next/standalone/.next/server/app/_not-found.rsc +17 -17
  20. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +17 -17
  21. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  22. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +11 -11
  23. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  24. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  25. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  26. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/index.html +1 -1
  28. package/.next/standalone/.next/server/app/index.rsc +16 -16
  29. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  30. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +16 -16
  31. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
  32. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +11 -11
  33. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  34. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  35. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  37. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +8 -8
  38. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  40. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  41. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  43. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  44. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  45. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  47. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  48. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  49. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  50. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0dtn9lr._.js +1 -1
  51. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_05pz9._._.js +1 -1
  52. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_0z7w.hh._.js +1 -1
  53. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0-2wr.c._.js +2 -2
  54. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0.~m-w2._.js +2 -2
  55. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0709m8.._.js +2 -2
  56. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0bz245.._.js +2 -2
  57. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0dl0kgt._.js +2 -2
  58. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0gmhxyo._.js +2 -2
  59. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0ymn496._.js → [root-of-the-server]__0ils1oq._.js} +2 -2
  60. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ohb3gc._.js +2 -2
  61. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__10h.ggz._.js → [root-of-the-server]__0tt8-uq._.js} +2 -2
  62. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ymlddl._.js +28 -4
  63. package/.next/standalone/.next/server/chunks/ssr/_10lm7or._.js +2 -2
  64. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_0xerkr6._.js +1 -1
  65. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_0q-m0y-._.js +1 -1
  66. package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_0mebn66._.js +1 -1
  67. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  68. package/.next/standalone/.next/server/pages/404.html +2 -2
  69. package/.next/standalone/.next/server/pages/500.html +1 -1
  70. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  71. package/.next/standalone/.next/server/server-reference-manifest.json +9 -9
  72. package/.next/standalone/.next/static/chunks/{0agmlhk5ml7x5.js → 0-dltnti0ew4y.js} +1 -1
  73. package/.next/standalone/.next/static/chunks/{14lmf8boay-zu.js → 02h_cfxpk4x9..js} +1 -1
  74. package/.next/standalone/.next/static/chunks/{0s6nux54y~l~r.js → 0a_w-lcg.0tl7.js} +1 -1
  75. package/.next/standalone/.next/static/chunks/{1400rtd5ywbt..js → 0lscbfo_2_h14.js} +2 -2
  76. package/.next/standalone/.next/static/chunks/0xr8w5io1-kb9.css +1 -0
  77. package/.next/standalone/.next/static/chunks/{0tpse0wu2wwo0.js → 0y8oyen~jnjl3.js} +1 -1
  78. package/.next/standalone/.next/static/chunks/{00ay03h8bq4b~.js → 0yzl~f-qji0...js} +1 -1
  79. package/.next/standalone/.next/static/chunks/{0en4v5k2nnxks.js → 16k5y9ruha2v4.js} +1 -1
  80. package/.next/standalone/.next/static/chunks/{17htukxga7bil.js → 17o7-pn_xzt-t.js} +1 -1
  81. package/.next/standalone/app/components/raw-log-viewer.tsx +25 -5
  82. package/.next/standalone/package.json +2 -2
  83. package/.next/standalone/pi-extension/index.ts +21 -4
  84. package/.next/standalone/server.js +1 -1
  85. package/dist/cli.mjs +203 -16
  86. package/package.json +2 -2
  87. package/pi-extension/index.ts +21 -4
  88. package/scripts/install-diagnosis.mjs +190 -0
  89. package/scripts/launch.ts +32 -0
  90. package/scripts/postinstall.mjs +25 -0
  91. package/src/hooks/handler.ts +24 -11
  92. package/src/hooks/integrations.ts +27 -3
  93. package/src/hooks/types.ts +73 -2
  94. package/.next/standalone/.next/static/chunks/12po2vpc-4_c1.css +0 -1
  95. /package/.next/standalone/.next/static/{68TLSFdjAQYIulNHfP0QY → EBbXdrT2rHib3zGLSuY7Q}/_buildManifest.js +0 -0
  96. /package/.next/standalone/.next/static/{68TLSFdjAQYIulNHfP0QY → EBbXdrT2rHib3zGLSuY7Q}/_clientMiddlewareManifest.js +0 -0
  97. /package/.next/standalone/.next/static/{68TLSFdjAQYIulNHfP0QY → EBbXdrT2rHib3zGLSuY7Q}/_ssgManifest.js +0 -0
package/dist/cli.mjs CHANGED
@@ -16,7 +16,7 @@ var __export = (target, all) => {
16
16
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
17
 
18
18
  // src/hooks/types.ts
19
- var HOOK_SCOPES, INTEGRATION_TYPES, CODEX_HOOK_SCOPES, CODEX_HOOK_EVENT_TYPES, CODEX_EVENT_MAP, COPILOT_HOOK_SCOPES, COPILOT_HOOK_EVENT_TYPES, CURSOR_HOOK_SCOPES, CURSOR_HOOK_EVENT_TYPES, CURSOR_EVENT_MAP, OPENCODE_HOOK_SCOPES, OPENCODE_HOOK_EVENT_TYPES, PI_HOOK_SCOPES, PI_HOOK_EVENT_TYPES, PI_EVENT_MAP, GEMINI_HOOK_SCOPES, GEMINI_HOOK_EVENT_TYPES, GEMINI_EVENT_MAP, GEMINI_TOOL_MAP, HOOK_EVENT_TYPES, FAILPROOFAI_HOOK_MARKER = "__failproofai_hook__";
19
+ var HOOK_SCOPES, INTEGRATION_TYPES, CODEX_HOOK_SCOPES, CODEX_HOOK_EVENT_TYPES, CODEX_EVENT_MAP, COPILOT_HOOK_SCOPES, COPILOT_HOOK_EVENT_TYPES, COPILOT_TOOL_MAP, CURSOR_HOOK_SCOPES, CURSOR_HOOK_EVENT_TYPES, CURSOR_EVENT_MAP, OPENCODE_HOOK_SCOPES, OPENCODE_HOOK_EVENT_TYPES, PI_HOOK_SCOPES, PI_HOOK_EVENT_TYPES, PI_EVENT_MAP, GEMINI_HOOK_SCOPES, GEMINI_HOOK_EVENT_TYPES, GEMINI_EVENT_MAP, GEMINI_TOOL_MAP, HOOK_EVENT_TYPES, FAILPROOFAI_HOOK_MARKER = "__failproofai_hook__";
20
20
  var init_types = __esm(() => {
21
21
  HOOK_SCOPES = ["user", "project", "local"];
22
22
  INTEGRATION_TYPES = ["claude", "codex", "copilot", "cursor", "opencode", "pi", "gemini"];
@@ -46,6 +46,16 @@ var init_types = __esm(() => {
46
46
  "PostToolUse",
47
47
  "Stop"
48
48
  ];
49
+ COPILOT_TOOL_MAP = {
50
+ bash: "Bash",
51
+ read: "Read",
52
+ write: "Write",
53
+ edit: "Edit",
54
+ str_replace_editor: "Edit",
55
+ glob: "Glob",
56
+ grep: "Grep",
57
+ ls: "LS"
58
+ };
49
59
  CURSOR_HOOK_SCOPES = ["user", "project"];
50
60
  CURSOR_HOOK_EVENT_TYPES = [
51
61
  "sessionStart",
@@ -2760,7 +2770,7 @@ var init_hook_activity_store = __esm(() => {
2760
2770
  });
2761
2771
 
2762
2772
  // package.json
2763
- var version2 = "0.0.10-beta.0";
2773
+ var version2 = "0.0.10-beta.2";
2764
2774
  var init_package = () => {};
2765
2775
 
2766
2776
  // src/posthog-key.ts
@@ -5347,6 +5357,9 @@ function canonicalizeEventType(raw, cli) {
5347
5357
  function canonicalizeToolName(raw, cli) {
5348
5358
  if (!raw)
5349
5359
  return raw;
5360
+ if (cli === "copilot") {
5361
+ return COPILOT_TOOL_MAP[raw] ?? raw;
5362
+ }
5350
5363
  if (cli === "gemini") {
5351
5364
  return GEMINI_TOOL_MAP[raw] ?? raw;
5352
5365
  }
@@ -5881,6 +5894,30 @@ const BUS_EVENT_MAP = {
5881
5894
  // message.updated is handled separately (filter to role:user); see below.
5882
5895
  };
5883
5896
 
5897
+ // Map opencode lowercase tool IDs (\`input.tool\`) → Claude PascalCase canonical
5898
+ // names. Builtin failproofai policies match on PascalCase via case-sensitive
5899
+ // \`Array.includes\`, so without this every Bash/Read/Write/Edit builtin
5900
+ // silently no-ops under opencode. Keep in sync with OPENCODE_TOOL_MAP in
5901
+ // failproofai/src/hooks/types.ts (this shim is loaded in-process by opencode
5902
+ // and must be self-contained — no imports from the failproofai package).
5903
+ // Unknown tools pass through unchanged via \`?? raw\`.
5904
+ const TOOL_NAME_MAP = {
5905
+ bash: "Bash",
5906
+ read: "Read",
5907
+ write: "Write",
5908
+ edit: "Edit",
5909
+ glob: "Glob",
5910
+ grep: "Grep",
5911
+ list: "LS",
5912
+ webfetch: "WebFetch",
5913
+ todowrite: "TodoWrite",
5914
+ todoread: "TodoRead",
5915
+ };
5916
+ function canonicalizeTool(raw) {
5917
+ if (!raw) return raw;
5918
+ return TOOL_NAME_MAP[raw] != null ? TOOL_NAME_MAP[raw] : raw;
5919
+ }
5920
+
5884
5921
  const FAILPROOFAI_BIN = ${escapedBin};
5885
5922
  const USE_NPX = ${useNpx};
5886
5923
 
@@ -5974,7 +6011,7 @@ export default async function failproofaiPlugin({ client, directory }) {
5974
6011
  const r = runFailproofai("PreToolUse", {
5975
6012
  session_id: input.sessionID,
5976
6013
  cwd: directory,
5977
- tool_name: input.tool,
6014
+ tool_name: canonicalizeTool(input.tool),
5978
6015
  tool_input: output.args,
5979
6016
  hook_event_name: "PreToolUse",
5980
6017
  }, directory);
@@ -5986,7 +6023,7 @@ export default async function failproofaiPlugin({ client, directory }) {
5986
6023
  const r = runFailproofai("PostToolUse", {
5987
6024
  session_id: input.sessionID,
5988
6025
  cwd: directory,
5989
- tool_name: input.tool,
6026
+ tool_name: canonicalizeTool(input.tool),
5990
6027
  tool_input: input.args,
5991
6028
  tool_response: { title: output.title, output: output.output, metadata: output.metadata },
5992
6029
  hook_event_name: "PostToolUse",
@@ -5999,7 +6036,7 @@ export default async function failproofaiPlugin({ client, directory }) {
5999
6036
  const r = runFailproofai("PermissionRequest", {
6000
6037
  session_id: input.sessionID,
6001
6038
  cwd: directory,
6002
- tool_name: input.tool || input.command || "permission",
6039
+ tool_name: canonicalizeTool(input.tool) || input.command || "permission",
6003
6040
  tool_input: input,
6004
6041
  hook_event_name: "PermissionRequest",
6005
6042
  }, directory);
@@ -8194,14 +8231,146 @@ function parseScriptArgs(argv) {
8194
8231
  }
8195
8232
  var init_parse_script_args = () => {};
8196
8233
 
8234
+ // scripts/install-diagnosis.mjs
8235
+ import { existsSync as existsSync14, readFileSync as readFileSync10, realpathSync } from "node:fs";
8236
+ import { dirname as dirname7, resolve as resolve9 } from "node:path";
8237
+ import { homedir as homedir21, platform as platform3 } from "node:os";
8238
+ import { spawnSync } from "node:child_process";
8239
+ function findPackageRoot(start) {
8240
+ try {
8241
+ let dir = realpathSync(start);
8242
+ if (existsSync14(dir) && !existsSync14(resolve9(dir, "package.json"))) {
8243
+ dir = dirname7(dir);
8244
+ }
8245
+ while (true) {
8246
+ const pkgPath = resolve9(dir, "package.json");
8247
+ if (existsSync14(pkgPath)) {
8248
+ try {
8249
+ const pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
8250
+ if (pkg.name === PKG_NAME)
8251
+ return dir;
8252
+ } catch {}
8253
+ }
8254
+ const parent = dirname7(dir);
8255
+ if (parent === dir)
8256
+ return null;
8257
+ dir = parent;
8258
+ }
8259
+ } catch {
8260
+ return null;
8261
+ }
8262
+ }
8263
+ function readPackageVersion(packageRoot) {
8264
+ if (!packageRoot)
8265
+ return null;
8266
+ try {
8267
+ const pkg = JSON.parse(readFileSync10(resolve9(packageRoot, "package.json"), "utf8"));
8268
+ return typeof pkg.version === "string" ? pkg.version : null;
8269
+ } catch {
8270
+ return null;
8271
+ }
8272
+ }
8273
+ function resolvePathFirstBinary() {
8274
+ try {
8275
+ const isWin = platform3() === "win32";
8276
+ const res = isWin ? spawnSync("where", [PKG_NAME], { encoding: "utf8" }) : spawnSync("sh", ["-c", `command -v ${PKG_NAME}`], { encoding: "utf8" });
8277
+ if (res.status !== 0)
8278
+ return null;
8279
+ const first = (res.stdout || "").split(/\r?\n/).find((l) => l.trim().length > 0);
8280
+ return first ? first.trim() : null;
8281
+ } catch {
8282
+ return null;
8283
+ }
8284
+ }
8285
+ function locateNpmGlobal() {
8286
+ try {
8287
+ const res = spawnSync("npm", ["root", "-g"], { encoding: "utf8" });
8288
+ if (res.status !== 0)
8289
+ return null;
8290
+ const root = (res.stdout || "").trim();
8291
+ if (!root)
8292
+ return null;
8293
+ const candidate = resolve9(root, PKG_NAME);
8294
+ return existsSync14(resolve9(candidate, "package.json")) ? candidate : null;
8295
+ } catch {
8296
+ return null;
8297
+ }
8298
+ }
8299
+ function locateBunGlobal() {
8300
+ try {
8301
+ const candidate = resolve9(homedir21(), ".bun", "install", "global", "node_modules", PKG_NAME);
8302
+ return existsSync14(resolve9(candidate, "package.json")) ? candidate : null;
8303
+ } catch {
8304
+ return null;
8305
+ }
8306
+ }
8307
+ function buildRecommendation(pathFirstBin) {
8308
+ if (!pathFirstBin)
8309
+ return null;
8310
+ const bunBinPrefix = resolve9(homedir21(), ".bun", "bin") + "/";
8311
+ const bunGlobalPrefix = resolve9(homedir21(), ".bun", "install", "global") + "/";
8312
+ const isBun = pathFirstBin.startsWith(bunBinPrefix) || pathFirstBin.startsWith(bunGlobalPrefix);
8313
+ if (isBun) {
8314
+ return `rm -f ~/.bun/bin/${PKG_NAME} && rm -rf ~/.bun/install/global/node_modules/${PKG_NAME}`;
8315
+ }
8316
+ return `npm rm -g ${PKG_NAME}`;
8317
+ }
8318
+ function diagnoseShadow(self) {
8319
+ const selfPackageRoot = (() => {
8320
+ try {
8321
+ return self?.selfPackageRoot ? realpathSync(self.selfPackageRoot) : null;
8322
+ } catch {
8323
+ return self?.selfPackageRoot ?? null;
8324
+ }
8325
+ })();
8326
+ const selfVersion = self?.selfVersion ?? null;
8327
+ const pathFirstBin = resolvePathFirstBinary();
8328
+ const pathFirstPackageRoot = pathFirstBin ? findPackageRoot(pathFirstBin) : null;
8329
+ const pathFirstVersion = readPackageVersion(pathFirstPackageRoot);
8330
+ const npmGlobalPath = locateNpmGlobal();
8331
+ const npmGlobalVersion = readPackageVersion(npmGlobalPath);
8332
+ const bunGlobalPath = locateBunGlobal();
8333
+ const bunGlobalVersion = readPackageVersion(bunGlobalPath);
8334
+ let shadowed = false;
8335
+ if (selfPackageRoot && pathFirstPackageRoot && pathFirstPackageRoot !== selfPackageRoot) {
8336
+ shadowed = true;
8337
+ } else if (pathFirstPackageRoot) {
8338
+ if (npmGlobalPath && npmGlobalPath !== pathFirstPackageRoot)
8339
+ shadowed = true;
8340
+ else if (bunGlobalPath && bunGlobalPath !== pathFirstPackageRoot)
8341
+ shadowed = true;
8342
+ }
8343
+ const recommendation = shadowed ? buildRecommendation(pathFirstBin) : null;
8344
+ let shadowDescription = null;
8345
+ if (shadowed) {
8346
+ shadowDescription = `PATH resolves to ${pathFirstPackageRoot}` + (pathFirstVersion ? ` (v${pathFirstVersion})` : "") + `, but you just installed ${selfPackageRoot}` + (selfVersion ? ` (v${selfVersion})` : "") + ".";
8347
+ }
8348
+ return {
8349
+ selfPackageRoot,
8350
+ selfVersion,
8351
+ pathFirstBin,
8352
+ pathFirstPath: pathFirstPackageRoot,
8353
+ pathFirstVersion,
8354
+ npmGlobalPath,
8355
+ npmGlobalVersion,
8356
+ bunGlobalPath,
8357
+ bunGlobalVersion,
8358
+ shadowed,
8359
+ shadowDescription,
8360
+ recommendation
8361
+ };
8362
+ }
8363
+ var PKG_NAME = "failproofai";
8364
+ var init_install_diagnosis = () => {};
8365
+
8197
8366
  // scripts/launch.ts
8198
8367
  var exports_launch = {};
8199
8368
  __export(exports_launch, {
8200
8369
  launch: () => launch
8201
8370
  });
8202
8371
  import { spawn as spawn4 } from "child_process";
8203
- import { realpathSync, existsSync as existsSync14 } from "node:fs";
8204
- import { resolve as resolve9, dirname as dirname7 } from "node:path";
8372
+ import { realpathSync as realpathSync2, existsSync as existsSync15 } from "node:fs";
8373
+ import { resolve as resolve10, dirname as dirname8 } from "node:path";
8205
8374
  import { fileURLToPath as fileURLToPath2 } from "node:url";
8206
8375
  function launch(mode) {
8207
8376
  const { claudeProjectsPath: parsedPath, loggingLevel, disableTelemetry, allowedDevOrigins, remainingArgs } = parseScriptArgs(process.argv.slice(2));
@@ -8233,10 +8402,27 @@ function launch(mode) {
8233
8402
  process.env.PORT = port;
8234
8403
  process.env.HOSTNAME = "0.0.0.0";
8235
8404
  cmd = "node";
8236
- const packageRoot = process.env.FAILPROOFAI_PACKAGE_ROOT ?? resolve9(dirname7(realpathSync(fileURLToPath2(import.meta.url))), "..");
8237
- const serverJsPath = resolve9(packageRoot, ".next/standalone/server.js");
8238
- if (!existsSync14(serverJsPath)) {
8239
- console.error(`
8405
+ const packageRoot = process.env.FAILPROOFAI_PACKAGE_ROOT ?? resolve10(dirname8(realpathSync2(fileURLToPath2(import.meta.url))), "..");
8406
+ const serverJsPath = resolve10(packageRoot, ".next/standalone/server.js");
8407
+ if (!existsSync15(serverJsPath)) {
8408
+ let shadowMessage = null;
8409
+ try {
8410
+ const diag = diagnoseShadow({ selfPackageRoot: packageRoot, selfVersion: version2 });
8411
+ if (diag.shadowed) {
8412
+ const alt = (diag.npmGlobalPath && diag.npmGlobalPath !== diag.pathFirstPath ? { path: diag.npmGlobalPath, version: diag.npmGlobalVersion } : null) ?? (diag.bunGlobalPath && diag.bunGlobalPath !== diag.pathFirstPath ? { path: diag.bunGlobalPath, version: diag.bunGlobalVersion } : null);
8413
+ const newer = alt?.path ?? "(unknown)";
8414
+ const newerVer = alt?.version ?? "?";
8415
+ shadowMessage = `
8416
+ Error: failproofai on your PATH is a stale install that no longer has its build output.
8417
+ ` + ` Running: ${diag.pathFirstPath}` + (diag.pathFirstVersion ? ` (v${diag.pathFirstVersion})` : "") + `
8418
+ ` + ` Newer copy: ${newer} (v${newerVer})
8419
+
8420
+ ` + `Remove the shadow with:
8421
+ ${diag.recommendation}
8422
+ `;
8423
+ }
8424
+ } catch {}
8425
+ console.error(shadowMessage ?? `
8240
8426
  Error: Cannot find server.js at:
8241
8427
  ${serverJsPath}
8242
8428
 
@@ -8272,6 +8458,7 @@ Error: Cannot find server.js at:
8272
8458
  var init_launch = __esm(() => {
8273
8459
  init_paths();
8274
8460
  init_parse_script_args();
8461
+ init_install_diagnosis();
8275
8462
  init_package();
8276
8463
  });
8277
8464
 
@@ -8293,18 +8480,18 @@ var init_cli_error2 = __esm(() => {
8293
8480
  });
8294
8481
 
8295
8482
  // bin/failproofai.mjs
8296
- import { realpathSync as realpathSync2 } from "fs";
8297
- import { dirname as dirname8, resolve as resolve10 } from "path";
8483
+ import { realpathSync as realpathSync3 } from "fs";
8484
+ import { dirname as dirname9, resolve as resolve11 } from "path";
8298
8485
  import { fileURLToPath as fileURLToPath3 } from "url";
8299
8486
  // package.json
8300
- var version = "0.0.10-beta.0";
8487
+ var version = "0.0.10-beta.2";
8301
8488
 
8302
8489
  // bin/failproofai.mjs
8303
8490
  if (!process.env.FAILPROOFAI_PACKAGE_ROOT) {
8304
- process.env.FAILPROOFAI_PACKAGE_ROOT = resolve10(dirname8(realpathSync2(fileURLToPath3(import.meta.url))), "..");
8491
+ process.env.FAILPROOFAI_PACKAGE_ROOT = resolve11(dirname9(realpathSync3(fileURLToPath3(import.meta.url))), "..");
8305
8492
  }
8306
8493
  if (!process.env.FAILPROOFAI_DIST_PATH) {
8307
- process.env.FAILPROOFAI_DIST_PATH = resolve10(dirname8(realpathSync2(fileURLToPath3(import.meta.url))), "..", "dist");
8494
+ process.env.FAILPROOFAI_DIST_PATH = resolve11(dirname9(realpathSync3(fileURLToPath3(import.meta.url))), "..", "dist");
8308
8495
  }
8309
8496
  var args = process.argv.slice(2);
8310
8497
  if (args[0] === "p")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "failproofai",
3
- "version": "0.0.10-beta.0",
3
+ "version": "0.0.10-beta.2",
4
4
  "description": "The easiest way to manage policies that keep your AI agents reliable, on-task, and running autonomously — for Claude Code & the Agents SDK",
5
5
  "bin": {
6
6
  "failproofai": "./dist/cli.mjs"
@@ -91,7 +91,7 @@
91
91
  "tailwind-merge": "^3.4.0",
92
92
  "tailwindcss": "^4.1.18",
93
93
  "typescript": "^6.0.2",
94
- "@anthropic-ai/sdk": "^0.91.1",
94
+ "@anthropic-ai/sdk": "^0.93.0",
95
95
  "vitest": "^4.0.18"
96
96
  },
97
97
  "dependencies": {
@@ -102,14 +102,31 @@ interface PiToolCallEvent {
102
102
  }
103
103
 
104
104
  /**
105
- * Pi emits tool names in lowercase (`bash`, `read`, `edit`, `write`).
106
- * failproofai's builtin policies match on Claude-shaped capitalized names
107
- * (`Bash`, `Read`, `Edit`, `Write`). Map between the two so existing
105
+ * Pi emits tool names in lowercase (`bash`, `read`, `edit`, `write`, `glob`,
106
+ * `grep`). failproofai's builtin policies match on Claude PascalCase
107
+ * (`Bash`, `Read`, `Edit`, `Write`, `Glob`, `Grep`) via case-sensitive
108
+ * `Array.includes` in policy-registry.ts. Map between the two so existing
108
109
  * tool-name match clauses fire on Pi sessions.
110
+ *
111
+ * Keys must stay in sync with PI_TOOL_MAP in src/hooks/types.ts (this shim is
112
+ * loaded in-process by Pi and must be self-contained — no imports from the
113
+ * failproofai package). Update both whenever Pi adds a tool ID we care about.
114
+ *
115
+ * Unknown tools (anything not in this map) pass through unchanged so custom
116
+ * policies that match on raw Pi tool IDs still work.
109
117
  */
118
+ const PI_TOOL_MAP: Record<string, string> = {
119
+ bash: "Bash",
120
+ read: "Read",
121
+ write: "Write",
122
+ edit: "Edit",
123
+ glob: "Glob",
124
+ grep: "Grep",
125
+ };
126
+
110
127
  function canonicalizeToolName(piToolName: string | undefined): string | undefined {
111
128
  if (!piToolName) return undefined;
112
- return piToolName.charAt(0).toUpperCase() + piToolName.slice(1);
129
+ return PI_TOOL_MAP[piToolName] ?? piToolName;
113
130
  }
114
131
 
115
132
  /** Resolve the cwd for the policy payload. Pi events don't include cwd, so
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Detects when `failproofai` on the user's PATH is shadowed by a different,
3
+ * older install — typically a leftover `bun link` from a prior dev session, or
4
+ * a `bun install -g failproofai` whose prefix sorts ahead of npm's on PATH.
5
+ *
6
+ * Used by:
7
+ * - scripts/postinstall.mjs — warn at install time so the customer never sees
8
+ * the misleading "missing build output" runtime error.
9
+ * - scripts/launch.ts — when .next/standalone/server.js is missing,
10
+ * produce a shadow-shaped error if the cause is a shadow rather than a
11
+ * genuinely broken build.
12
+ *
13
+ * Pure Node.js built-ins, no external dependencies. Every probe is wrapped in
14
+ * try/catch — diagnoseShadow() is guaranteed not to throw.
15
+ */
16
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
17
+ import { dirname, resolve } from "node:path";
18
+ import { homedir, platform } from "node:os";
19
+ import { spawnSync } from "node:child_process";
20
+
21
+ const PKG_NAME = "failproofai";
22
+
23
+ /**
24
+ * Walk up from `start` looking for a package.json whose name === "failproofai".
25
+ * Returns its directory, or null when no such package.json is reachable.
26
+ */
27
+ function findPackageRoot(start) {
28
+ try {
29
+ let dir = realpathSync(start);
30
+ // If `start` was a file (e.g. /usr/local/bin/failproofai), step up to its dir.
31
+ if (existsSync(dir) && !existsSync(resolve(dir, "package.json"))) {
32
+ dir = dirname(dir);
33
+ }
34
+ while (true) {
35
+ const pkgPath = resolve(dir, "package.json");
36
+ if (existsSync(pkgPath)) {
37
+ try {
38
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
39
+ if (pkg.name === PKG_NAME) return dir;
40
+ } catch {
41
+ // unreadable or non-JSON — fall through to parent
42
+ }
43
+ }
44
+ const parent = dirname(dir);
45
+ if (parent === dir) return null;
46
+ dir = parent;
47
+ }
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+
53
+ /** Read `version` from a package.json; null on any error. */
54
+ function readPackageVersion(packageRoot) {
55
+ if (!packageRoot) return null;
56
+ try {
57
+ const pkg = JSON.parse(readFileSync(resolve(packageRoot, "package.json"), "utf8"));
58
+ return typeof pkg.version === "string" ? pkg.version : null;
59
+ } catch {
60
+ return null;
61
+ }
62
+ }
63
+
64
+ /** Find which `failproofai` PATH would resolve. POSIX: `command -v`; Win32: `where`. */
65
+ function resolvePathFirstBinary() {
66
+ try {
67
+ const isWin = platform() === "win32";
68
+ const res = isWin
69
+ ? spawnSync("where", [PKG_NAME], { encoding: "utf8" })
70
+ : spawnSync("sh", ["-c", `command -v ${PKG_NAME}`], { encoding: "utf8" });
71
+ if (res.status !== 0) return null;
72
+ const first = (res.stdout || "").split(/\r?\n/).find((l) => l.trim().length > 0);
73
+ return first ? first.trim() : null;
74
+ } catch {
75
+ return null;
76
+ }
77
+ }
78
+
79
+ /** Locate the npm global install of failproofai, if any. */
80
+ function locateNpmGlobal() {
81
+ try {
82
+ const res = spawnSync("npm", ["root", "-g"], { encoding: "utf8" });
83
+ if (res.status !== 0) return null;
84
+ const root = (res.stdout || "").trim();
85
+ if (!root) return null;
86
+ const candidate = resolve(root, PKG_NAME);
87
+ return existsSync(resolve(candidate, "package.json")) ? candidate : null;
88
+ } catch {
89
+ return null;
90
+ }
91
+ }
92
+
93
+ /** Locate the bun global install of failproofai, if any. */
94
+ function locateBunGlobal() {
95
+ try {
96
+ const candidate = resolve(homedir(), ".bun", "install", "global", "node_modules", PKG_NAME);
97
+ return existsSync(resolve(candidate, "package.json")) ? candidate : null;
98
+ } catch {
99
+ return null;
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Build a copy-pasteable cleanup command for the offending install.
105
+ *
106
+ * The signal we trust is `pathFirstBin` — the un-resolved binary location PATH
107
+ * pointed to. For bun-link shadows the realpath'd package root is the dev tree
108
+ * (not under ~/.bun/), so checking the package root would mis-classify those
109
+ * shadows as npm and recommend the wrong cleanup.
110
+ */
111
+ function buildRecommendation(pathFirstBin) {
112
+ if (!pathFirstBin) return null;
113
+ const bunBinPrefix = resolve(homedir(), ".bun", "bin") + "/";
114
+ const bunGlobalPrefix = resolve(homedir(), ".bun", "install", "global") + "/";
115
+ const isBun = pathFirstBin.startsWith(bunBinPrefix) || pathFirstBin.startsWith(bunGlobalPrefix);
116
+ if (isBun) {
117
+ return `rm -f ~/.bun/bin/${PKG_NAME} && rm -rf ~/.bun/install/global/node_modules/${PKG_NAME}`;
118
+ }
119
+ return `npm rm -g ${PKG_NAME}`;
120
+ }
121
+
122
+ /**
123
+ * Diagnose whether the running binary is being shadowed on PATH by a different
124
+ * failproofai install.
125
+ *
126
+ * @param {{ selfPackageRoot: string, selfVersion: string | null }} self
127
+ * The package root and version of the binary calling diagnoseShadow().
128
+ * Callers (bin/failproofai.mjs, scripts/postinstall.mjs) already have these
129
+ * values; passing them in keeps the helper deterministic and free of
130
+ * import.meta.url assumptions.
131
+ */
132
+ export function diagnoseShadow(self) {
133
+ const selfPackageRoot = (() => {
134
+ try { return self?.selfPackageRoot ? realpathSync(self.selfPackageRoot) : null; }
135
+ catch { return self?.selfPackageRoot ?? null; }
136
+ })();
137
+ const selfVersion = self?.selfVersion ?? null;
138
+
139
+ const pathFirstBin = resolvePathFirstBinary();
140
+ const pathFirstPackageRoot = pathFirstBin ? findPackageRoot(pathFirstBin) : null;
141
+ const pathFirstVersion = readPackageVersion(pathFirstPackageRoot);
142
+
143
+ const npmGlobalPath = locateNpmGlobal();
144
+ const npmGlobalVersion = readPackageVersion(npmGlobalPath);
145
+
146
+ const bunGlobalPath = locateBunGlobal();
147
+ const bunGlobalVersion = readPackageVersion(bunGlobalPath);
148
+
149
+ // "Shadow" covers two scenarios:
150
+ // 1. Postinstall case — `selfPackageRoot` is the just-installed copy and
151
+ // PATH resolves elsewhere. Flag when the two roots differ.
152
+ // 2. Runtime case — the running binary IS the shadow (so selfPackageRoot
153
+ // === pathFirstPackageRoot), but a *different* failproofai install
154
+ // exists at the npm or bun global. Flag when one of those differs from
155
+ // pathFirstPackageRoot.
156
+ let shadowed = false;
157
+ if (selfPackageRoot && pathFirstPackageRoot && pathFirstPackageRoot !== selfPackageRoot) {
158
+ shadowed = true;
159
+ } else if (pathFirstPackageRoot) {
160
+ if (npmGlobalPath && npmGlobalPath !== pathFirstPackageRoot) shadowed = true;
161
+ else if (bunGlobalPath && bunGlobalPath !== pathFirstPackageRoot) shadowed = true;
162
+ }
163
+
164
+ const recommendation = shadowed ? buildRecommendation(pathFirstBin) : null;
165
+
166
+ // A short human-readable summary used by callers that want a one-liner.
167
+ let shadowDescription = null;
168
+ if (shadowed) {
169
+ shadowDescription =
170
+ `PATH resolves to ${pathFirstPackageRoot}` +
171
+ (pathFirstVersion ? ` (v${pathFirstVersion})` : "") +
172
+ `, but you just installed ${selfPackageRoot}` +
173
+ (selfVersion ? ` (v${selfVersion})` : "") + ".";
174
+ }
175
+
176
+ return {
177
+ selfPackageRoot,
178
+ selfVersion,
179
+ pathFirstBin,
180
+ pathFirstPath: pathFirstPackageRoot,
181
+ pathFirstVersion,
182
+ npmGlobalPath,
183
+ npmGlobalVersion,
184
+ bunGlobalPath,
185
+ bunGlobalVersion,
186
+ shadowed,
187
+ shadowDescription,
188
+ recommendation,
189
+ };
190
+ }
package/scripts/launch.ts CHANGED
@@ -7,6 +7,7 @@ import { realpathSync, existsSync } from "node:fs";
7
7
  import { resolve, dirname } from "node:path";
8
8
  import { fileURLToPath } from "node:url";
9
9
  import { parseScriptArgs } from "./parse-script-args";
10
+ import { diagnoseShadow } from "./install-diagnosis.mjs";
10
11
  import { version } from "../package.json";
11
12
 
12
13
  export function launch(mode: "dev" | "start"): void {
@@ -49,7 +50,38 @@ export function launch(mode: "dev" | "start"): void {
49
50
  ?? resolve(dirname(realpathSync(fileURLToPath(import.meta.url))), "..");
50
51
  const serverJsPath = resolve(packageRoot, ".next/standalone/server.js");
51
52
  if (!existsSync(serverJsPath)) {
53
+ // Most "missing server.js" reports come from a PATH shadow (an older
54
+ // `bun link` or a `bun install -g` whose prefix wins over npm), not from
55
+ // a genuinely broken build. Diagnose first so the error message names
56
+ // the actual cause when that's what's going on.
57
+ let shadowMessage: string | null = null;
58
+ try {
59
+ const diag = diagnoseShadow({ selfPackageRoot: packageRoot, selfVersion: version });
60
+ if (diag.shadowed) {
61
+ // Pick whichever alternate install exists at npm/bun globals AND
62
+ // differs from PATH-first. In the runtime stale-binary scenario the
63
+ // running install IS the PATH-first one, so we'd otherwise point the
64
+ // user back at themselves.
65
+ const alt =
66
+ (diag.npmGlobalPath && diag.npmGlobalPath !== diag.pathFirstPath
67
+ ? { path: diag.npmGlobalPath, version: diag.npmGlobalVersion }
68
+ : null)
69
+ ?? (diag.bunGlobalPath && diag.bunGlobalPath !== diag.pathFirstPath
70
+ ? { path: diag.bunGlobalPath, version: diag.bunGlobalVersion }
71
+ : null);
72
+ const newer = alt?.path ?? "(unknown)";
73
+ const newerVer = alt?.version ?? "?";
74
+ shadowMessage =
75
+ `\nError: failproofai on your PATH is a stale install that no longer has its build output.\n` +
76
+ ` Running: ${diag.pathFirstPath}` + (diag.pathFirstVersion ? ` (v${diag.pathFirstVersion})` : "") + `\n` +
77
+ ` Newer copy: ${newer} (v${newerVer})\n\n` +
78
+ `Remove the shadow with:\n ${diag.recommendation}\n`;
79
+ }
80
+ } catch {
81
+ // Diagnosis is best-effort; fall back to the original message.
82
+ }
52
83
  console.error(
84
+ shadowMessage ??
53
85
  `\nError: Cannot find server.js at:\n ${serverJsPath}\n\n` +
54
86
  `The package may be missing its build output.\n` +
55
87
  `Try reinstalling:\n npm install -g failproofai@latest\n`
@@ -12,6 +12,7 @@ import { resolve } from "node:path";
12
12
  import { platform, arch, release, homedir, hostname } from "node:os";
13
13
  import { createHmac } from "node:crypto";
14
14
  import { trackInstallEvent } from "./install-telemetry.mjs";
15
+ import { diagnoseShadow } from "./install-diagnosis.mjs";
15
16
 
16
17
  // Skip when running in development context (e.g. `bun install` in the source repo).
17
18
  // INIT_CWD is set by npm/bun to the directory where install was invoked; it differs
@@ -29,6 +30,30 @@ if (!existsSync(serverJsPath)) {
29
30
  process.exit(1);
30
31
  }
31
32
 
33
+ // Detect when an older `failproofai` is shadowing this fresh install on PATH —
34
+ // classic case is a leftover `bun link` from a prior dev session, or a
35
+ // `bun install -g` whose ~/.bun/bin sorts ahead of npm's prefix. Without this
36
+ // warning the user only finds out later via a confusing runtime error from
37
+ // scripts/launch.ts pointing at the *old* install's missing build output.
38
+ try {
39
+ let selfVersion = null;
40
+ try {
41
+ selfVersion = JSON.parse(readFileSync(resolve(process.cwd(), "package.json"), "utf8")).version ?? null;
42
+ } catch {}
43
+ const diag = diagnoseShadow({ selfPackageRoot: process.cwd(), selfVersion });
44
+ if (diag.shadowed) {
45
+ console.warn(
46
+ `\n[failproofai] Warning: another failproofai install is earlier on your PATH.\n` +
47
+ ` Just installed: ${diag.selfPackageRoot}` + (diag.selfVersion ? ` (v${diag.selfVersion})` : "") + `\n` +
48
+ ` PATH resolves : ${diag.pathFirstPath}` + (diag.pathFirstVersion ? ` (v${diag.pathFirstVersion})` : "") + `\n\n` +
49
+ ` Your shell will run the older copy. Remove the shadow with:\n` +
50
+ ` ${diag.recommendation}\n`
51
+ );
52
+ }
53
+ } catch {
54
+ // Diagnosis is best-effort — never fail the install over a warning.
55
+ }
56
+
32
57
  const FAILPROOFAI_HOOK_MARKER = "__failproofai_hook__";
33
58
  const NAMESPACE = "failproofai-telemetry-v1";
34
59