failproofai 1.0.10 → 1.0.11-beta.1
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.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +6 -6
- package/.next/standalone/.next/prerender-manifest.json +4 -4
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js +4 -4
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.meta +8 -0
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js +4 -4
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.meta +8 -0
- package/.next/standalone/.next/server/app/_not-found.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/api/audit/invite/route.js +4 -4
- package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js +4 -4
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/status/route.js +1 -1
- package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js +2 -2
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js +4 -4
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route.js +3 -3
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js +3 -3
- package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page.js +4 -4
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.meta +8 -0
- package/.next/standalone/.next/server/app/index.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js +4 -4
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
- package/.next/standalone/.next/server/app/policies/page.js +8 -8
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js +5 -5
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +5 -5
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js +5 -5
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +8 -8
- package/.next/standalone/.next/server/app/settings/page.js +9 -9
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/{[externals]__0qxhecv._.js → [externals]__0d7i9ea._.js} +2 -2
- package/.next/standalone/.next/server/chunks/{[externals]__0sg9-3p._.js → [externals]__0hjaw70._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[externals]__1_9ze9b._.js +3 -0
- package/.next/standalone/.next/server/chunks/[externals]__1oenfmn._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__08dchfu._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ga9hs7._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0l3yhx4._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0moxohc._.js +22 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0z2ll-a._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1adacul._.js +11 -11
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1autwno._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1v25n-6._.js +3 -0
- package/.next/standalone/.next/server/chunks/_09dz7xv._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1ek68ln._.js +2 -2
- package/.next/standalone/.next/server/chunks/_1fz8m31._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1jrv3hg._.js +3 -0
- package/.next/standalone/.next/server/chunks/_1nx8fim._.js +3 -0
- package/.next/standalone/.next/server/chunks/_1ogwigf._.js +1 -1
- package/.next/standalone/.next/server/chunks/{_1q5i8mb._.js → _1q27azx._.js} +2 -2
- package/.next/standalone/.next/server/chunks/lib_telemetry_ts_0dctyyw._.js +1 -1
- package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_0el6rg8._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_0ol5pa3._.js} +2 -2
- package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_04op_ou._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_1rqmk27._.js} +2 -2
- package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_0dr20uz._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_1w7tiq4._.js} +2 -2
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0oqrl71._.js → [root-of-the-server]__042u-jn._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04usis8._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1-bm5zf._.js → [root-of-the-server]__0502lat._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0_gn5iz._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0f765xr._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__01bmjsj._.js → [root-of-the-server]__0h38gzz._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0hoaz1g._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1ouobn4._.js → [root-of-the-server]__0mxmwtx._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__19evfi8._.js → [root-of-the-server]__0uescqv._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__13d-wb6._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1484zv2._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1mf3zp6._.js → [root-of-the-server]__19dutr7._.js} +4 -4
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1u6sn69._.js → [root-of-the-server]__1ab4ifb._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1fbrk56._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1pprgri._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1um4yxn._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1wb4yt2._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1y3n3cn._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0bqoto4._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_0yq5_3a._.js → _1ffxwxz._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1gb0ifp._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_042cgl1._.js → _1o95rf0._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_1zopuov._.js → _1thr5rf._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_16pa9ht._.js → _1v1o2ya._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +12 -14
- package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9sni._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h4._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_1hl81y4._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_0nkkbfv._.js +6 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589g._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +6 -6
- package/.next/standalone/.next/server/middleware.js +1 -1
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +23 -23
- package/.next/standalone/.next/static/chunks/08tmc_7n-rba1.js +1 -0
- package/.next/standalone/.next/static/chunks/{3mjfvng11xnoo.js → 09tlrmucn6oq3.js} +1 -1
- package/.next/standalone/.next/static/chunks/{078dofaxvjgpd.js → 0i69k917myh7b.js} +1 -1
- package/.next/standalone/.next/static/chunks/{03h3b-uomi7_d.js → 0u_nhsujkactp.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2j92xul6y24_v.js → 0zp51hzx62i-u.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3u1s8dsqiean5.js → 1fz7q0gtnmeod.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3ob2161ap37ul.js → 1j75le8wln-26.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2kj826h1w9r47.js → 1z4km7tw0_50h.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1fqukzzowjflt.js → 239ay_80-xr8z.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3nkc2jlgyygda.js → 2c9pc5xco6rnu.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3i8rg2fv9nd6e.js → 3n46e45atic-m.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0542cuk_va8g1.js → 3sg07wnn969xq.js} +1 -1
- package/.next/standalone/.next/static/chunks/{turbopack-11dn2pegmg9_9.js → turbopack-1opxka4dtjbc5.js} +1 -1
- package/.next/standalone/app/actions/install-hooks-web.ts +10 -2
- package/.next/standalone/hermes-plugin/plugin.yaml +1 -1
- package/.next/standalone/node_modules/@next/env/package.json +1 -1
- package/.next/standalone/node_modules/next/dist/build/swc/index.js +1 -1
- package/.next/standalone/node_modules/next/dist/compiled/hash.js/sha256/256.js +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/hash.js/sha256/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo-experimental.runtime.prod.js +17 -17
- package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo.runtime.prod.js +18 -18
- package/.next/standalone/node_modules/next/dist/compiled/next-server/app-route-turbo.runtime.prod.js +3 -3
- package/.next/standalone/node_modules/next/dist/compiled/next-server/pages-turbo.runtime.prod.js +13 -13
- package/.next/standalone/node_modules/next/dist/lib/patch-incorrect-lockfile.js +3 -3
- package/.next/standalone/node_modules/next/dist/server/base-server.js +1 -2
- package/.next/standalone/node_modules/next/dist/server/config.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-turbopack.js +2 -2
- package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-webpack.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/image-optimizer.js +265 -53
- package/.next/standalone/node_modules/next/dist/server/lib/app-info-log.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/lib/incremental-cache/file-system-cache.js +177 -19
- package/.next/standalone/node_modules/next/dist/server/lib/incremental-cache/index.js +36 -17
- package/.next/standalone/node_modules/next/dist/server/lib/incremental-cache/shared-cache-controls.external.js +14 -6
- package/.next/standalone/node_modules/next/dist/server/lib/match-next-data-pathname.js +3 -1
- package/.next/standalone/node_modules/next/dist/server/lib/node-fs-methods.js +14 -0
- package/.next/standalone/node_modules/next/dist/server/lib/route-cache-key.js +77 -0
- package/.next/standalone/node_modules/next/dist/server/lib/router-utils/block-cross-site-dev.js +9 -11
- package/.next/standalone/node_modules/next/dist/server/lib/start-server.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/mcp/get-mcp-middleware.js +4 -2
- package/.next/standalone/node_modules/next/dist/server/next-server.js +7 -3
- package/.next/standalone/node_modules/next/dist/server/response-cache/index.js +32 -16
- package/.next/standalone/node_modules/next/dist/server/route-matcher-providers/app-page-route-matcher-provider.js +2 -2
- package/.next/standalone/node_modules/next/dist/server/route-modules/pages/pages-handler.js +6 -1
- package/.next/standalone/node_modules/next/dist/server/route-modules/route-module.js +9 -2
- package/.next/standalone/node_modules/next/dist/server/use-cache/use-cache-wrapper.js +30 -29
- package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/app-paths.js +18 -1
- package/.next/standalone/node_modules/next/dist/telemetry/anonymous-meta.js +1 -1
- package/.next/standalone/node_modules/next/dist/telemetry/events/swc-load-failure.js +1 -1
- package/.next/standalone/node_modules/next/dist/telemetry/events/version.js +2 -2
- package/.next/standalone/node_modules/next/package.json +10 -10
- package/.next/standalone/package.json +14 -11
- package/.next/standalone/sdk/python/README.md +41 -0
- package/.next/standalone/sdk/python/docs/README.md +1 -0
- package/.next/standalone/sdk/python/docs/otel/README.md +36 -0
- package/.next/standalone/sdk/python/docs/otel/examples/agent_trace.py +30 -0
- package/.next/standalone/sdk/python/docs/otel/examples/quickstart.py +22 -0
- package/.next/standalone/sdk/python/failproofai_sdk/__init__.py +8 -0
- package/.next/standalone/sdk/python/failproofai_sdk/integrations/__init__.py +3 -0
- package/.next/standalone/sdk/python/failproofai_sdk/integrations/otel.py +189 -0
- package/.next/standalone/sdk/python/pyproject.toml +2 -1
- package/.next/standalone/sdk/python/tests/test_docs.py +16 -3
- package/.next/standalone/sdk/python/tests/test_integrations.py +1 -1
- package/.next/standalone/sdk/python/tests/test_otel.py +147 -0
- package/.next/standalone/sdk/python/tests/test_zero_dependencies.py +4 -1
- package/.next/standalone/sdk/python/uv.lock +258 -147
- package/.next/standalone/sdk/typescript/README.md +43 -0
- package/.next/standalone/sdk/typescript/integration/ai.test.ts +9 -2
- package/.next/standalone/sdk/typescript/package-lock.json +57 -5
- package/.next/standalone/sdk/typescript/package.json +2 -0
- package/.next/standalone/sdk/typescript/src/edge/index.ts +9 -0
- package/.next/standalone/sdk/typescript/src/index.ts +2 -0
- package/.next/standalone/sdk/typescript/src/integrations/index.ts +4 -1
- package/.next/standalone/sdk/typescript/src/integrations/otel.ts +204 -0
- package/.next/standalone/sdk/typescript/test/integrations.test.ts +1 -1
- package/.next/standalone/sdk/typescript/test/otel.test.ts +125 -0
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +653 -1077
- package/dist/cli.mjs +8174 -3930
- package/dist/worker.mjs +170 -62
- package/hermes-plugin/plugin.yaml +1 -1
- package/package.json +14 -11
- package/scripts/launch-facts.ts +157 -0
- package/scripts/launch-ready.ts +228 -0
- package/scripts/launch-screen.ts +202 -0
- package/scripts/launch.ts +106 -49
- package/scripts/skew-log-filter.ts +42 -0
- package/src/audit/cli-login.ts +39 -59
- package/src/audit/cli.ts +308 -228
- package/src/audit/index.ts +45 -2
- package/src/audit/schedule-cli.ts +83 -120
- package/src/audit/types.ts +31 -0
- package/src/hooks/agent-selection.ts +155 -0
- package/src/hooks/backfill-cli.ts +151 -137
- package/src/hooks/backfill-request.ts +149 -0
- package/src/hooks/cloud-connection.ts +1 -1
- package/src/hooks/cloud-enrollment-cli.ts +78 -28
- package/src/hooks/cloud-introspect.ts +3 -0
- package/src/hooks/collector-config.ts +10 -1
- package/src/hooks/config-status.ts +606 -0
- package/src/hooks/configure-wizard.ts +915 -989
- package/src/hooks/daemon-service.ts +16 -6
- package/src/hooks/did-you-mean.ts +185 -0
- package/src/hooks/flush-cli.ts +33 -7
- package/src/hooks/fp-config.ts +96 -0
- package/src/hooks/fp-home.ts +11 -0
- package/src/hooks/fp-reset.ts +102 -95
- package/src/hooks/handler.ts +4 -0
- package/src/hooks/harness-cli.ts +87 -138
- package/src/hooks/install-prompt.ts +38 -477
- package/src/hooks/integrations.ts +1 -1
- package/src/hooks/jev-cli.ts +183 -196
- package/src/hooks/manager.ts +554 -358
- package/src/hooks/onboarding-attempt.ts +11 -9
- package/src/hooks/otel-cli.ts +344 -0
- package/src/hooks/otel-output.ts +24 -0
- package/src/hooks/otel-writers.ts +225 -0
- package/src/hooks/pack-cli.ts +219 -204
- package/src/hooks/report-body.ts +50 -0
- package/src/hooks/safe-config-write.ts +4 -2
- package/src/hooks/session-pause-cli.ts +93 -73
- package/src/hooks/tui.ts +849 -389
- package/src/hooks/uninstall-cli.ts +136 -149
- package/src/hooks/untraced-note.ts +52 -0
- package/.next/standalone/.next/server/chunks/[externals]__19_pzeq._.js +0 -3
- package/.next/standalone/.next/server/chunks/[externals]__20kzpkf._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__01eqny5._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__05ja_05._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0igxt32._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0zyc_nm._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1ttrwnd._.js +0 -22
- package/.next/standalone/.next/server/chunks/_0bs-v8p._.js +0 -3
- package/.next/standalone/.next/server/chunks/_185gyk9._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__056wjo4._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0n0xg95._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0rwtwpm._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0yrsbd_._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11mayhe._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15578wp._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1m_svbe._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1q4p5b8._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_1n3w9lb._.js +0 -6
- package/.next/standalone/.next/static/chunks/1lyg_puttohbc.js +0 -1
- /package/.next/standalone/.next/static/{ECs8__hcNabYV58WfDh-3 → UZKCiP4Q2TeU8zOB-h9Xj}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{ECs8__hcNabYV58WfDh-3 → UZKCiP4Q2TeU8zOB-h9Xj}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{ECs8__hcNabYV58WfDh-3 → UZKCiP4Q2TeU8zOB-h9Xj}/_ssgManifest.js +0 -0
package/bin/failproofai.mjs
CHANGED
|
@@ -120,9 +120,16 @@ async function exitAfterFlush(code) {
|
|
|
120
120
|
const hookIdx = args.indexOf("--hook");
|
|
121
121
|
if (hookIdx >= 0) {
|
|
122
122
|
if (!args[hookIdx + 1]) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
123
|
+
// The notice shape, written out by hand and never coloured: importing the
|
|
124
|
+
// kit here — the first module this file imports — moves `tui.ts` ahead of
|
|
125
|
+
// the builtin policies in the bundle, so its top-level `ctx` renames their
|
|
126
|
+
// `ctx` parameters and changes the audit cache key (`engineVersion`) for
|
|
127
|
+
// every user. It also keeps the hook path an agent spawns per tool call
|
|
128
|
+
// loading nothing new. Written, not `console.error`, which Bun paints red.
|
|
129
|
+
process.stderr.write(
|
|
130
|
+
"✕ Missing event type after --hook.\n Run failproofai help hook for the events and agent ids.\n",
|
|
131
|
+
);
|
|
132
|
+
await exitAfterFlush(1);
|
|
126
133
|
}
|
|
127
134
|
const eventType = args[hookIdx + 1];
|
|
128
135
|
const cliIdx = args.indexOf("--cli");
|
|
@@ -305,8 +312,8 @@ if (hookIdx >= 0) {
|
|
|
305
312
|
* had all three. The words are still each screen's own — this owns the shape,
|
|
306
313
|
* so a screen cannot drift out of the family without editing the family.
|
|
307
314
|
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
315
|
+
* Drawn by `screenKit().helpPage` at the terminal's width; descriptions are
|
|
316
|
+
* prose and may be shortened on a terminal, never in a pipe.
|
|
310
317
|
*/
|
|
311
318
|
/**
|
|
312
319
|
* Lines a module ALREADY laid out with the kit — `harness`, `publish`,
|
|
@@ -334,46 +341,119 @@ async function printLines(lines, ok = true) {
|
|
|
334
341
|
* flatten the structure they were expressing.
|
|
335
342
|
*/
|
|
336
343
|
async function printReport(command, lines, opts = {}) {
|
|
337
|
-
const {
|
|
338
|
-
|
|
344
|
+
const { printBlock, optsFor, screenKit, paint: palette } = await import("../src/hooks/tui");
|
|
345
|
+
const { reportBody } = await import("../src/hooks/report-body");
|
|
339
346
|
const ok = opts.ok !== false;
|
|
340
347
|
const stream = ok ? process.stdout : process.stderr;
|
|
341
348
|
const o = optsFor(stream);
|
|
349
|
+
const kit = screenKit(o);
|
|
350
|
+
const c = palette(o.color);
|
|
342
351
|
// `\`like this\`` becomes pink, and loses the backticks. These messages name
|
|
343
352
|
// the command to run next more often than not, and pink is what you type
|
|
344
|
-
// everywhere
|
|
345
|
-
//
|
|
346
|
-
//
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
353
|
+
// everywhere. Applied AFTER wrapping, because an escape sequence has no
|
|
354
|
+
// width and colouring first would make every wrap measure the wrong length.
|
|
355
|
+
//
|
|
356
|
+
// A line that opens on a state glyph gets that glyph in its role colour, the
|
|
357
|
+
// way a notice draws it: `update` reports a daemon that needs root that way.
|
|
358
|
+
const glyphRoles = { "✓": c.guide, "▲": c.warn, "✕": c.err };
|
|
359
|
+
const paint = (line) => {
|
|
360
|
+
const typed =
|
|
361
|
+
(line.match(/`/g) || []).length % 2 === 0 ? line.replace(/`([^`]+)`/g, (_, cmd) => c.pink(cmd)) : line;
|
|
362
|
+
const lead = /^(\s*)([✓▲✕]) /.exec(typed);
|
|
363
|
+
return lead ? `${lead[1]}${glyphRoles[lead[2]](lead[2])}${typed.slice(lead[1].length + 1)}` : typed;
|
|
364
|
+
};
|
|
365
|
+
// The 2026-10 shape: the header names the command, top-level lines start at
|
|
366
|
+
// column 0, and lines a module indented on purpose keep their indent. A
|
|
367
|
+
// failed run opens with ✕, so it reads as the failure it is — unless its
|
|
368
|
+
// lines already mark their own state; see report-body.ts.
|
|
369
|
+
const name = command.charAt(0).toUpperCase() + command.slice(1);
|
|
370
|
+
const body = reportBody(lines, { ok, width: o.cols - 2 }).map(paint);
|
|
371
|
+
printBlock(stream, [kit.header(opts.meta ? `${name} · ${opts.meta}` : name), "", ...body]);
|
|
362
372
|
}
|
|
363
373
|
|
|
364
374
|
async function printHelp(spec) {
|
|
365
|
-
const {
|
|
366
|
-
|
|
375
|
+
const { screenKit, optsFor, printBlock } = await import("../src/hooks/tui");
|
|
376
|
+
// Descriptions are prose and may be shortened to fit a terminal; nothing is
|
|
377
|
+
// ever cut in a pipe, and a command, flag or path is never cut anywhere.
|
|
378
|
+
const kit = screenKit({ ...optsFor(process.stdout), fit: !!process.stdout.isTTY });
|
|
379
|
+
printBlock(process.stdout, kit.helpPage(spec));
|
|
367
380
|
}
|
|
368
381
|
|
|
369
382
|
async function runCli() {
|
|
383
|
+
// One page for `policies --help` and `policies add|remove|show --help`: it
|
|
384
|
+
// carries every flag of every policies lane, so `help policies add` lands on
|
|
385
|
+
// the right page with no routing of its own. Defined here, inside runCli,
|
|
386
|
+
// so the bundle gains no top-level name.
|
|
387
|
+
const POLICIES_HELP = {
|
|
388
|
+
name: "policies",
|
|
389
|
+
usage: [
|
|
390
|
+
["failproofai policies [--list]", "List every policy and whether it is on"],
|
|
391
|
+
["failproofai policies add [name | owner/repo]", "Turn one on, install a pack, or pick from a list"],
|
|
392
|
+
["failproofai policies remove [name | pack-id]", "Turn one off, uninstall a pack, or pick from a list"],
|
|
393
|
+
["failproofai policies show <owner/repo>", "See what a pack holds"],
|
|
394
|
+
["failproofai policies --install [names...]", "Install hooks and turn on any policies named"],
|
|
395
|
+
["failproofai policies --uninstall [names...]", "Turn named policies off; with none, remove hooks"],
|
|
396
|
+
],
|
|
397
|
+
options: [
|
|
398
|
+
["-i, -u", "Short for --install and --uninstall"],
|
|
399
|
+
["--policy, --only <a,b>", "Install only these policies from a pack"],
|
|
400
|
+
["--category <x,y>", "Install only these categories from a pack"],
|
|
401
|
+
["--all", "List every policy; with a pack, install all of it"],
|
|
402
|
+
["--cli <agent...>", "Apply to these agents only"],
|
|
403
|
+
["", "claude, codex, copilot, cursor, opencode, pi,"],
|
|
404
|
+
["", "hermes, openclaw, factory, devin, antigravity, goose"],
|
|
405
|
+
["--scope <scope>", "user, project, local (default user); all when removing"],
|
|
406
|
+
["--beta", "Include beta policies"],
|
|
407
|
+
["-c, --custom <file>", "With --install, add a custom policy file (repeatable)"],
|
|
408
|
+
["", "--uninstall -c removes every custom policy file"],
|
|
409
|
+
["--releases", "With show: list its releases, and which one is here"],
|
|
410
|
+
],
|
|
411
|
+
examples: [
|
|
412
|
+
"failproofai policies add FailproofAI/policies",
|
|
413
|
+
"failproofai policies add FailproofAI/policies --category git",
|
|
414
|
+
"failproofai policies remove block-sudo",
|
|
415
|
+
],
|
|
416
|
+
};
|
|
417
|
+
// `--cli` in the three policies lanes: one or more space-separated agents,
|
|
418
|
+
// optionally repeated, read until the next flag or the first word that is
|
|
419
|
+
// not an agent, since a policy name may follow the list. One wording for
|
|
420
|
+
// every mistake, built from INTEGRATION_TYPES: the message this replaced
|
|
421
|
+
// named eight of the twelve agents, and said "Missing" for a typo too.
|
|
422
|
+
async function readCliFlag(list) {
|
|
423
|
+
const { INTEGRATION_TYPES } = await import("../src/hooks/types");
|
|
424
|
+
const agents = new Set(INTEGRATION_TYPES);
|
|
425
|
+
const cliFlagValues = [];
|
|
426
|
+
const cliConsumedIdxs = new Set();
|
|
427
|
+
for (let idx = 0; idx < list.length; idx++) {
|
|
428
|
+
if (list[idx] !== "--cli") continue;
|
|
429
|
+
let consumed = 0;
|
|
430
|
+
for (let j = idx + 1; j < list.length && !list[j].startsWith("-") && agents.has(list[j]); j++) {
|
|
431
|
+
cliFlagValues.push(list[j]);
|
|
432
|
+
cliConsumedIdxs.add(j);
|
|
433
|
+
consumed++;
|
|
434
|
+
}
|
|
435
|
+
if (consumed > 0) continue;
|
|
436
|
+
const half = Math.ceil(INTEGRATION_TYPES.length / 2);
|
|
437
|
+
const everyAgent =
|
|
438
|
+
`Use any of: ${INTEGRATION_TYPES.slice(0, half).join(", ")},\n` +
|
|
439
|
+
` ${INTEGRATION_TYPES.slice(half).join(", ")}`;
|
|
440
|
+
const given = list[idx + 1];
|
|
441
|
+
if (given === undefined || given.startsWith("-")) {
|
|
442
|
+
throw new CliError(`Missing agent name after --cli.\n${everyAgent}`);
|
|
443
|
+
}
|
|
444
|
+
const { nearestName } = await import("../src/hooks/did-you-mean");
|
|
445
|
+
const near = nearestName(given, INTEGRATION_TYPES);
|
|
446
|
+
throw new CliError(`Not an agent: ${given}${near ? ` (did you mean ${near}?)` : "."}\n${everyAgent}`);
|
|
447
|
+
}
|
|
448
|
+
return { cliFlagValues, cliConsumedIdxs };
|
|
449
|
+
}
|
|
370
450
|
// --help / -h (only when not inside a subcommand that handles its own --help)
|
|
371
451
|
// `update` and `migrate` were missing here, so `failproofai update --help`
|
|
372
452
|
// exited 1 with "Unexpected argument" — both commands had no reachable help
|
|
373
453
|
// at all, only the paragraph in the top-level dump that this rewrite moved.
|
|
374
454
|
// `help` and `publish` are new. `policy` and `pack` are canonicalized to
|
|
375
455
|
// `policies` above and never reach this list.
|
|
376
|
-
const SUBCOMMANDS = ["policies", "audit", "config", "uninstall", "backfill", "flush", "harness", "jev", "publish", "update", "migrate", "help"];
|
|
456
|
+
const SUBCOMMANDS = ["policies", "audit", "config", "uninstall", "backfill", "flush", "harness", "otel", "jev", "publish", "update", "migrate", "help"];
|
|
377
457
|
// ── help ─────────────────────────────────────────────────────────────────
|
|
378
458
|
//
|
|
379
459
|
// The index and the reference manual used to be the same document: 152 lines,
|
|
@@ -393,33 +473,12 @@ async function runCli() {
|
|
|
393
473
|
// machine-facing flag would only take space from the human-facing commands.
|
|
394
474
|
if (helpTopic === "hook") {
|
|
395
475
|
await printHelp({
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
after: [
|
|
403
|
-
"You do not run this; `failproofai config` writes it into each CLI's",
|
|
404
|
-
"hook configuration for you.",
|
|
405
|
-
],
|
|
406
|
-
},
|
|
407
|
-
{
|
|
408
|
-
label: "options",
|
|
409
|
-
entries: [
|
|
410
|
-
["--hook <event>", "PreToolUse, PostToolUse, UserPromptSubmit, Stop, SubagentStop, SessionStart, SessionEnd, PreCompact, Notification, PermissionRequest"],
|
|
411
|
-
["--cli <name>", "claude, codex, copilot, cursor, opencode, pi, hermes, openclaw, factory, devin, antigravity, goose. Defaults to claude. It selects which payload shape to expect: each CLI names its events and tool arguments differently, and failproofai canonicalizes them."],
|
|
412
|
-
],
|
|
413
|
-
},
|
|
414
|
-
{
|
|
415
|
-
label: "how it answers",
|
|
416
|
-
lines: [
|
|
417
|
-
"It reads the event as JSON on stdin and answers on stdout, in whatever",
|
|
418
|
-
"shape that CLI honours. Exit codes and response shapes differ per CLI by",
|
|
419
|
-
"necessity — see docs.befailproof.ai. Denials are reported to the agent,",
|
|
420
|
-
"never to you.",
|
|
421
|
-
],
|
|
422
|
-
},
|
|
476
|
+
name: "hook",
|
|
477
|
+
usage: [["failproofai --hook <event> [--cli <agent>]", "Called by your agents on every tool call"]],
|
|
478
|
+
options: [
|
|
479
|
+
["--hook <event>", "PreToolUse, PostToolUse, UserPromptSubmit, Stop and more"],
|
|
480
|
+
["--cli <agent>", "claude (default), codex, copilot, cursor, opencode, pi,"],
|
|
481
|
+
["", "hermes, openclaw, factory, devin, antigravity, goose"],
|
|
423
482
|
],
|
|
424
483
|
});
|
|
425
484
|
process.exit(0);
|
|
@@ -435,8 +494,8 @@ async function runCli() {
|
|
|
435
494
|
: helpTopic;
|
|
436
495
|
if (!SUBCOMMANDS.includes(canonical)) {
|
|
437
496
|
throw new CliError(
|
|
438
|
-
`
|
|
439
|
-
`
|
|
497
|
+
`There is no help for '${helpTopic}'.\n` +
|
|
498
|
+
"Run `failproofai help` to see every command.",
|
|
440
499
|
);
|
|
441
500
|
}
|
|
442
501
|
// `policies add|remove|show` has its own help, distinct from the listing's.
|
|
@@ -449,56 +508,69 @@ async function runCli() {
|
|
|
449
508
|
) {
|
|
450
509
|
const extraArgs = args.filter((a) => a !== "--help" && a !== "-h" && a !== "help");
|
|
451
510
|
if (extraArgs.length > 0) {
|
|
452
|
-
throw new CliError(`Unexpected argument: ${extraArgs[0]}
|
|
453
|
-
}
|
|
454
|
-
// The index
|
|
455
|
-
//
|
|
456
|
-
//
|
|
457
|
-
//
|
|
458
|
-
//
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
{
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
511
|
+
throw new CliError(`Unexpected argument: ${extraArgs[0]}.\nRun \`failproofai help\` for usage.`);
|
|
512
|
+
}
|
|
513
|
+
// The index and every `<command> --help` page are built from the same kit,
|
|
514
|
+
// so a row added here cannot end up in a different dialect from the page it
|
|
515
|
+
// points at. Colour is decoration and never meaning: the column already says
|
|
516
|
+
// which half is a command, so this reads the same under NO_COLOR, piped to a
|
|
517
|
+
// file, or on a terminal that has never heard of 24-bit.
|
|
518
|
+
{
|
|
519
|
+
// The index, in the design's shape: what to do first, then every command by
|
|
520
|
+
// what it is for. Composed here from the kit rather than a builder in tui.ts,
|
|
521
|
+
// so the bundle gains no top-level name (see the audit-cache note there).
|
|
522
|
+
const { screenKit, optsFor, printBlock } = await import("../src/hooks/tui");
|
|
523
|
+
const kit = screenKit({ ...optsFor(process.stdout), fit: !!process.stdout.isTTY });
|
|
524
|
+
// The command sits in the SECOND column here, so these rows never go
|
|
525
|
+
// through rows()' description slot, which is the one shortened to fit.
|
|
526
|
+
const steps = [
|
|
527
|
+
["Set up this machine", "failproofai config"],
|
|
528
|
+
["Turn on policies", "failproofai policies add FailproofAI/policies"],
|
|
529
|
+
["See what your agents did", "failproofai"],
|
|
530
|
+
];
|
|
531
|
+
const section = (heading, entries) => [
|
|
532
|
+
"",
|
|
533
|
+
kit.head(heading),
|
|
534
|
+
...kit.rows(entries.map(([name, description]) => [name === "(no args)" ? name : kit.cmd(name), description]), 12),
|
|
535
|
+
];
|
|
536
|
+
await printBlock(process.stdout, [
|
|
537
|
+
kit.header("Guardrails for coding agents on this machine"),
|
|
538
|
+
"",
|
|
539
|
+
kit.head("Get started"),
|
|
540
|
+
...steps.map(([label, command], i) => ` ${i + 1} ${label.padEnd(27)} ${kit.cmd(command)}`),
|
|
541
|
+
...section("Set up", [
|
|
542
|
+
["config", "Connect to cloud or use open source, and pick agents to trace"],
|
|
543
|
+
["update", "Finish an npm upgrade: migrate this machine and match the daemon"],
|
|
544
|
+
["uninstall", "Remove hooks and the daemon from this machine"],
|
|
545
|
+
]),
|
|
546
|
+
...section("Enforce", [
|
|
547
|
+
["policies", "List policies, turn them on or off, install packs"],
|
|
548
|
+
["publish", "Ship your own policies as a pack anyone can install"],
|
|
549
|
+
["jev", "Have Jev judge tool calls on top of the regex policies"],
|
|
550
|
+
]),
|
|
551
|
+
...section("Observe", [
|
|
552
|
+
["(no args)", "Open the policy dashboard on localhost:8020"],
|
|
553
|
+
["audit", "Scan your agents' history for risky and wasteful patterns"],
|
|
554
|
+
["otel", "Opt in to OpenTelemetry from agents and apps"],
|
|
555
|
+
]),
|
|
556
|
+
...section("Less often", [
|
|
557
|
+
["harness", "Collect sessions from extra locations per agent"],
|
|
558
|
+
["flush", "Send queued events to cloud now"],
|
|
559
|
+
["backfill", "Re-send history the collector already read"],
|
|
560
|
+
["migrate", "Bring ~/.failproofai up to this version's layout"],
|
|
561
|
+
]),
|
|
562
|
+
"",
|
|
563
|
+
kit.head("Examples"),
|
|
564
|
+
...kit.rows([
|
|
565
|
+
[kit.cmd("failproofai config --status"), "Check the daemon, cloud and pauses"],
|
|
566
|
+
[kit.cmd("failproofai policies add block-sudo"), "Turn on one policy"],
|
|
567
|
+
[kit.cmd("failproofai policies show acme/guards"), "Preview a pack before installing it"],
|
|
568
|
+
]),
|
|
569
|
+
"",
|
|
570
|
+
`Run ${kit.cmd("failproofai help <command>")} for its options, ${kit.cmd("failproofai -v")} for the version.`,
|
|
571
|
+
"docs.befailproof.ai · discord.befailproof.ai",
|
|
572
|
+
]);
|
|
573
|
+
}
|
|
502
574
|
process.exit(0);
|
|
503
575
|
}
|
|
504
576
|
|
|
@@ -507,7 +579,7 @@ async function runCli() {
|
|
|
507
579
|
if ((args.includes("--version") || args.includes("-v")) && !SUBCOMMANDS.includes(args[0])) {
|
|
508
580
|
const extraArgs = args.filter((a) => a !== "--version" && a !== "-v");
|
|
509
581
|
if (extraArgs.length > 0) {
|
|
510
|
-
throw new CliError(`Unexpected argument: ${extraArgs[0]}
|
|
582
|
+
throw new CliError(`Unexpected argument: ${extraArgs[0]}.\nRun \`failproofai help\` for usage.`);
|
|
511
583
|
}
|
|
512
584
|
console.log(version);
|
|
513
585
|
process.exit(0);
|
|
@@ -582,8 +654,9 @@ async function runCli() {
|
|
|
582
654
|
// entry point can see this ordering.
|
|
583
655
|
const { checkLayoutForCli } = await import("../src/hooks/fp-reset");
|
|
584
656
|
const check = await checkLayoutForCli();
|
|
585
|
-
|
|
586
|
-
if (check.
|
|
657
|
+
// Written rather than `console.error`, which Bun paints red on a terminal.
|
|
658
|
+
if (check.lines.length > 0) process.stderr.write(`${check.lines.join("\n")}\n`);
|
|
659
|
+
if (check.fatal) await exitAfterFlush(1);
|
|
587
660
|
// `check.didReset` is deliberately not read. It used to force the wizard
|
|
588
661
|
// below; see the note at `shouldOfferFirstRun` for why a migrated machine
|
|
589
662
|
// no longer needs setup re-run, and `didReset` in `fp-reset.ts` for what
|
|
@@ -613,6 +686,9 @@ async function runCli() {
|
|
|
613
686
|
// never block a command on reporting
|
|
614
687
|
}
|
|
615
688
|
|
|
689
|
+
// True once the setup wizard has run in this process. It opens with the
|
|
690
|
+
// logomark, so the dashboard launch at the end must not draw a second one.
|
|
691
|
+
let setupWizardRan = false;
|
|
616
692
|
const { shouldOfferFirstRun } = await import("../src/hooks/first-run-gate");
|
|
617
693
|
// `|| layoutWasReset` stood here, with `force: layoutWasReset` below, because a
|
|
618
694
|
// migration used to delete the home's policy config while leaving the agent
|
|
@@ -632,7 +708,7 @@ async function runCli() {
|
|
|
632
708
|
const { maybeFirstRunConfigure } = await import("../src/hooks/configure-wizard");
|
|
633
709
|
// `audit` runs its own scan immediately after this returns; firing the
|
|
634
710
|
// post-setup audit too would scan the whole history twice in a row.
|
|
635
|
-
await maybeFirstRunConfigure({}, { postSetupAudit: args[0] !== "audit" });
|
|
711
|
+
setupWizardRan = await maybeFirstRunConfigure({}, { postSetupAudit: args[0] !== "audit" });
|
|
636
712
|
} catch {
|
|
637
713
|
// Onboarding is never allowed to block the command the user actually typed.
|
|
638
714
|
}
|
|
@@ -649,33 +725,15 @@ async function runCli() {
|
|
|
649
725
|
const subArgs = args.slice(1);
|
|
650
726
|
if (subArgs.includes("--help") || subArgs.includes("-h")) {
|
|
651
727
|
await printHelp({
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
"The collector is unhurried on purpose: a batch is swept once it is older",
|
|
660
|
-
"than two minutes, at most 64 per pass, on a 60-second cadence. That pacing",
|
|
661
|
-
"keeps a backlog from stampeding the server, and it is exactly wrong when",
|
|
662
|
-
"you are standing at a dashboard waiting to see your own events — \"not",
|
|
663
|
-
"delivered yet\" and \"not working\" look identical from there.",
|
|
664
|
-
"",
|
|
665
|
-
"This asks the daemon to make a pass right now, with no minimum age and no",
|
|
666
|
-
"per-pass cap. It re-sends nothing: only batches already spooled and not",
|
|
667
|
-
"yet delivered. For history the collector has already read past, use",
|
|
668
|
-
"`failproofai backfill`.",
|
|
669
|
-
],
|
|
670
|
-
},
|
|
671
|
-
{
|
|
672
|
-
label: "options",
|
|
673
|
-
entries: [
|
|
674
|
-
["--wait", "Block until the spool drains, or --timeout elapses."],
|
|
675
|
-
["--timeout <secs>", "How long --wait waits. Default: 60."],
|
|
676
|
-
],
|
|
677
|
-
},
|
|
728
|
+
name: "flush",
|
|
729
|
+
usage: [["failproofai flush [options]", "Send queued events to cloud now"]],
|
|
730
|
+
options: [
|
|
731
|
+
// The queue on THIS machine: a batch set aside after repeated failures
|
|
732
|
+
// leaves it empty without having been delivered.
|
|
733
|
+
["--wait", "Wait until the queue is empty"],
|
|
734
|
+
["--timeout <secs>", "How long --wait waits (default 60)"],
|
|
678
735
|
],
|
|
736
|
+
optionsCol: 20,
|
|
679
737
|
});
|
|
680
738
|
process.exit(0);
|
|
681
739
|
}
|
|
@@ -685,19 +743,21 @@ async function runCli() {
|
|
|
685
743
|
(a, i) => a.startsWith("-") && !KNOWN.has(a) && subArgs[i - 1] !== "--timeout",
|
|
686
744
|
);
|
|
687
745
|
if (unknown) {
|
|
688
|
-
throw new CliError(`Unexpected argument: ${unknown}
|
|
746
|
+
throw new CliError(`Unexpected argument: ${unknown}.\nRun \`failproofai flush --help\` for usage.`);
|
|
689
747
|
}
|
|
690
748
|
|
|
691
749
|
let timeoutSecs;
|
|
692
750
|
const tIdx = subArgs.indexOf("--timeout");
|
|
693
751
|
if (tIdx >= 0) {
|
|
694
752
|
const raw = subArgs[tIdx + 1];
|
|
695
|
-
if (!raw || raw.startsWith("-"))
|
|
753
|
+
if (!raw || raw.startsWith("-")) {
|
|
754
|
+
throw new CliError("Missing value after --timeout.\nFor example: `failproofai flush --wait --timeout 60`");
|
|
755
|
+
}
|
|
696
756
|
const n = Number(raw);
|
|
697
757
|
// Rejected rather than coerced: NaN would silently become "wait forever
|
|
698
758
|
// or not at all" depending on the comparison, and neither is what was asked.
|
|
699
759
|
if (!Number.isFinite(n) || n <= 0) {
|
|
700
|
-
throw new CliError(`Could not read --timeout ${raw}
|
|
760
|
+
throw new CliError(`Could not read --timeout ${raw}.\nGive a number of seconds, for example \`--timeout 60\``);
|
|
701
761
|
}
|
|
702
762
|
timeoutSecs = n;
|
|
703
763
|
}
|
|
@@ -720,45 +780,38 @@ async function runCli() {
|
|
|
720
780
|
const subArgs = args.slice(1);
|
|
721
781
|
if (subArgs.includes("--help") || subArgs.includes("-h")) {
|
|
722
782
|
await printHelp({
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
lines: [
|
|
730
|
-
"The collector never re-reads a file it has a cursor for, which is right",
|
|
731
|
-
"until the dashboard's data is cleared, a machine is re-enrolled, or",
|
|
732
|
-
"cursors advanced before there was anywhere to send. Then the history",
|
|
733
|
-
"exists on disk and nowhere else, with no way to ask for it again.",
|
|
734
|
-
"",
|
|
735
|
-
"Re-sending is safe: redaction is deterministic, so a re-sent event hashes",
|
|
736
|
-
"identically to its first send and collapses into the row already there.",
|
|
737
|
-
],
|
|
738
|
-
},
|
|
739
|
-
{
|
|
740
|
-
label: "options",
|
|
741
|
-
entries: [
|
|
742
|
-
["--since <when>", "How far back: `30d`, `6m`, `YYYY-MM-DD`. Default 30 days."],
|
|
743
|
-
["--dry-run", "Report what would be re-read and change nothing."],
|
|
744
|
-
],
|
|
745
|
-
},
|
|
746
|
-
{
|
|
747
|
-
label: "note",
|
|
748
|
-
lines: [
|
|
749
|
-
"Which streams are sent follows [collector] in ~/.failproofai/config.toml —",
|
|
750
|
-
"a backfill never sends something your config says you do not want.",
|
|
751
|
-
],
|
|
752
|
-
},
|
|
783
|
+
name: "backfill",
|
|
784
|
+
usage: [["failproofai backfill [options]", "Re-send history the collector already read"]],
|
|
785
|
+
options: [
|
|
786
|
+
["--since <when>", "How far back: 30d, 6m, 1y or YYYY-MM-DD (default 30d)"],
|
|
787
|
+
["--agents <a,b>", "Only these traced agents (default: every traced agent)"],
|
|
788
|
+
["--dry-run", "Show what would be sent, send nothing"],
|
|
753
789
|
],
|
|
790
|
+
examples: ["failproofai backfill --since 6m", "failproofai backfill --agents claude,codex --dry-run"],
|
|
791
|
+
optionsCol: 20,
|
|
754
792
|
});
|
|
755
793
|
process.exit(0);
|
|
756
794
|
}
|
|
757
795
|
|
|
758
|
-
const KNOWN = new Set(["--since", "--dry-run"]);
|
|
759
|
-
const
|
|
796
|
+
const KNOWN = new Set(["--since", "--dry-run", "--agents"]);
|
|
797
|
+
const TAKES_VALUE = new Set(["--since", "--agents"]);
|
|
798
|
+
// Every word is checked, not only the ones that start with "-": a bare
|
|
799
|
+
// `backfill 6m` used to be ignored, and ran the default 30 days instead of
|
|
800
|
+
// the six months that were typed.
|
|
801
|
+
const unknown = subArgs.find((a, i) => !KNOWN.has(a) && !TAKES_VALUE.has(subArgs[i - 1]));
|
|
760
802
|
if (unknown) {
|
|
761
|
-
|
|
803
|
+
const looksLikeWindow = /^\d+[dmy]$/.test(unknown) || !Number.isNaN(Date.parse(unknown));
|
|
804
|
+
throw new CliError(
|
|
805
|
+
`Unexpected argument: ${unknown}.\n` +
|
|
806
|
+
(looksLikeWindow ? `Did you mean \`failproofai backfill --since ${unknown}\`?` : "See its options: `failproofai backfill --help`"),
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
let agents;
|
|
810
|
+
const agentsIdx = subArgs.indexOf("--agents");
|
|
811
|
+
if (agentsIdx >= 0) {
|
|
812
|
+
const raw = subArgs[agentsIdx + 1];
|
|
813
|
+
if (!raw || raw.startsWith("-")) throw new CliError("Missing value after --agents.");
|
|
814
|
+
agents = raw.split(",").map((a) => a.trim()).filter(Boolean);
|
|
762
815
|
}
|
|
763
816
|
|
|
764
817
|
let sinceMs;
|
|
@@ -785,9 +838,23 @@ async function runCli() {
|
|
|
785
838
|
|
|
786
839
|
lastSubcommand = "backfill";
|
|
787
840
|
const { runBackfillCommand } = await import("../src/hooks/backfill-cli");
|
|
788
|
-
const
|
|
789
|
-
|
|
790
|
-
|
|
841
|
+
const { optsFor } = await import("../src/hooks/tui");
|
|
842
|
+
const result = await runBackfillCommand({
|
|
843
|
+
sinceMs,
|
|
844
|
+
since: sinceIdx >= 0 ? subArgs[sinceIdx + 1] : undefined,
|
|
845
|
+
dryRun: subArgs.includes("--dry-run"),
|
|
846
|
+
agents,
|
|
847
|
+
// Drawn for stdout. A refusal goes to stderr, and printBlock drops the
|
|
848
|
+
// colour there when stderr cannot show it.
|
|
849
|
+
render: optsFor(process.stdout),
|
|
850
|
+
});
|
|
851
|
+
await printLines(result.lines, result.exitCode === 0);
|
|
852
|
+
await track("cli_backfill", {
|
|
853
|
+
ok: result.exitCode === 0,
|
|
854
|
+
dry_run: subArgs.includes("--dry-run"),
|
|
855
|
+
explicit_since: sinceIdx >= 0,
|
|
856
|
+
agents_named: agents ? agents.length : 0,
|
|
857
|
+
});
|
|
791
858
|
lastSubcommand = null;
|
|
792
859
|
await exitAfterFlush(result.exitCode);
|
|
793
860
|
return;
|
|
@@ -808,84 +875,71 @@ async function runCli() {
|
|
|
808
875
|
// could reach it to notice. The pack lane is entered from
|
|
809
876
|
// `policies add|remove|show` below, which is the only door it has.
|
|
810
877
|
|
|
878
|
+
if (args[0] === "otel") {
|
|
879
|
+
const subArgs = args.slice(1);
|
|
880
|
+
if (!subArgs.length || subArgs.includes("--help") || subArgs.includes("-h")) {
|
|
881
|
+
const verb = ["status", "enable", "disable", "env"].includes(subArgs[0]) ? subArgs[0] : null;
|
|
882
|
+
const usages = {
|
|
883
|
+
status: "failproofai otel status",
|
|
884
|
+
enable: "failproofai otel enable <agent|all> [--local] [--no-content] [--yes]",
|
|
885
|
+
disable: "failproofai otel disable <agent|all>",
|
|
886
|
+
env: "failproofai otel env [--service <name>] [--local]",
|
|
887
|
+
};
|
|
888
|
+
const options = {
|
|
889
|
+
enable: [
|
|
890
|
+
["--local", "Use the loopback OTLP relay. Automatic for Gemini CLI and Copilot."],
|
|
891
|
+
["--no-content", "Do not capture prompt text or tool details."],
|
|
892
|
+
["--yes", "Accept switching this agent from transcript upload to OTEL."],
|
|
893
|
+
],
|
|
894
|
+
env: [
|
|
895
|
+
["--service <name>", "Service name for env (default: my-agent)."],
|
|
896
|
+
["--local", "Use the loopback OTLP relay."],
|
|
897
|
+
],
|
|
898
|
+
status: [],
|
|
899
|
+
disable: [],
|
|
900
|
+
};
|
|
901
|
+
const acceptedOptions = verb ? options[verb] : [...options.enable, options.env[0]];
|
|
902
|
+
await printHelp({
|
|
903
|
+
name: verb ? `otel ${verb}` : "otel",
|
|
904
|
+
usage: (verb ? [usages[verb]] : Object.values(usages)).map(usage => [
|
|
905
|
+
usage,
|
|
906
|
+
verb === "enable" || verb === "disable" || !verb ? "Agents: claude, codex, gemini, copilot, all" : "",
|
|
907
|
+
]),
|
|
908
|
+
options: acceptedOptions,
|
|
909
|
+
examples: verb === "status" ? ["failproofai otel status"] :
|
|
910
|
+
verb === "disable" ? ["failproofai otel disable all"] :
|
|
911
|
+
verb === "env" ? ["failproofai otel env --service my-agent --local"] :
|
|
912
|
+
["failproofai otel enable claude", "failproofai otel enable codex --local --no-content", "failproofai otel disable all"],
|
|
913
|
+
});
|
|
914
|
+
process.exit(0);
|
|
915
|
+
}
|
|
916
|
+
lastSubcommand = "otel";
|
|
917
|
+
const { runOtelCommand } = await import("../src/hooks/otel-cli");
|
|
918
|
+
const { otelReportLines } = await import("../src/hooks/otel-output");
|
|
919
|
+
const result = await runOtelCommand(subArgs, { onEvent: track });
|
|
920
|
+
// env is shell input and must not be wrapped in a styled report.
|
|
921
|
+
if (subArgs[0] === "env" && result.exitCode === 0) console.log(result.lines.join("\n"));
|
|
922
|
+
else await printReport(`otel ${subArgs[0]}`, otelReportLines(subArgs[0], result), { ok: result.exitCode === 0 });
|
|
923
|
+
lastSubcommand = null;
|
|
924
|
+
await exitAfterFlush(result.exitCode);
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
|
|
811
928
|
if (args[0] === "harness") {
|
|
812
929
|
const subArgs = args.slice(1);
|
|
813
930
|
if (subArgs.length === 0 || subArgs.includes("--help") || subArgs.includes("-h")) {
|
|
814
931
|
await printHelp({
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
{
|
|
827
|
-
label: "why",
|
|
828
|
-
lines: [
|
|
829
|
-
"Each agent CLI is watched wherever its own installer put it —",
|
|
830
|
-
"~/.claude/projects, ~/.hermes/state.db, and so on. That misses every",
|
|
831
|
-
"other arrangement: a second profile, a mounted team share, a container's",
|
|
832
|
-
"home beside the host's, an agent an operator moved. Those hold real",
|
|
833
|
-
"sessions and nothing collects them.",
|
|
834
|
-
],
|
|
835
|
-
},
|
|
836
|
-
{
|
|
837
|
-
label: "the label",
|
|
838
|
-
lines: [
|
|
839
|
-
"An entry is `<path>` or `<label>=<path>`. The label namespaces agent ids",
|
|
840
|
-
"as <label>-<agentId>, and it matters: two locations holding the same",
|
|
841
|
-
"project derive the SAME id (it comes from the cwd inside the transcript,",
|
|
842
|
-
"identical in both copies), so without a label they merge into one agent",
|
|
843
|
-
"whose sessions interleave from two machines' worth of history. Omit it",
|
|
844
|
-
"and one is derived from the folder name.",
|
|
845
|
-
],
|
|
846
|
-
},
|
|
847
|
-
{
|
|
848
|
-
label: "harnesses",
|
|
849
|
-
lines: [
|
|
850
|
-
"claude, codex, copilot, openclaw, pi, factory, antigravity, cursor,",
|
|
851
|
-
"goose, opencode, devin, hermes",
|
|
852
|
-
"",
|
|
853
|
-
"`claude` covers subagent transcripts too — they live under the same root.",
|
|
854
|
-
],
|
|
855
|
-
},
|
|
856
|
-
{
|
|
857
|
-
label: "notes",
|
|
858
|
-
lines: [
|
|
859
|
-
"• Session collection must be on for any of this to be read; extra paths",
|
|
860
|
-
" live under \"collector\" in ~/.failproofai/config.json like the defaults.",
|
|
861
|
-
"• A path overlapping one already captured is REFUSED by the daemon at",
|
|
862
|
-
" startup rather than collected twice under two ids. It says so in the",
|
|
863
|
-
" journal.",
|
|
864
|
-
"• Two entries sharing a LABEL are refused too: they would share one",
|
|
865
|
-
" cursor directory, whose whole map is written at once, so each would",
|
|
866
|
-
" clobber the other's watermark and re-read from zero after a restart.",
|
|
867
|
-
"• Takes effect within seconds — the daemon re-reads config.json on an",
|
|
868
|
-
" interval and cycles its collector. No restart, no sudo.",
|
|
869
|
-
],
|
|
870
|
-
},
|
|
871
|
-
{
|
|
872
|
-
label: "examples",
|
|
873
|
-
lines: [
|
|
874
|
-
"failproofai harness add-path claude work=/srv/team/.claude/projects",
|
|
875
|
-
"failproofai harness add-path hermes prod=/srv/hermes-prod/state.db",
|
|
876
|
-
"failproofai harness add-path codex /mnt/other-home/.codex/sessions",
|
|
877
|
-
"failproofai harness list",
|
|
878
|
-
"failproofai harness remove-path claude work",
|
|
879
|
-
"",
|
|
880
|
-
"One home per person — label them, or both derive the same folder-name",
|
|
881
|
-
"label and the second is refused:",
|
|
882
|
-
"failproofai harness add-path openclaw user1=/srv/.openclaw-user1",
|
|
883
|
-
"failproofai harness add-path openclaw user2=/srv/.openclaw-user2",
|
|
884
|
-
"",
|
|
885
|
-
"Containers: FAILPROOFAI_<HARNESS>_EXTRA_PATHS replaces the file's entries.",
|
|
886
|
-
"FAILPROOFAI_OPENCLAW_EXTRA_PATHS=\"user1=/srv/.a,user2=/srv/.b\"",
|
|
887
|
-
],
|
|
888
|
-
},
|
|
932
|
+
name: "harness",
|
|
933
|
+
usage: [
|
|
934
|
+
["failproofai harness list [agent]", "Show extra capture paths"],
|
|
935
|
+
["failproofai harness add-path <agent> [label=]<path>", "Also capture from here"],
|
|
936
|
+
["failproofai harness remove-path <agent> <path | label>", "Stop capturing from here"],
|
|
937
|
+
],
|
|
938
|
+
// For hermes, goose, opencode and devin the extra path is a database file.
|
|
939
|
+
examples: [
|
|
940
|
+
"failproofai harness add-path claude work=/srv/team/.claude/projects",
|
|
941
|
+
"failproofai harness add-path hermes prod=/srv/hermes-prod/state.db",
|
|
942
|
+
"failproofai harness remove-path claude work",
|
|
889
943
|
],
|
|
890
944
|
});
|
|
891
945
|
process.exit(0);
|
|
@@ -919,102 +973,35 @@ async function runCli() {
|
|
|
919
973
|
const subArgs = args.slice(1);
|
|
920
974
|
if (subArgs.length === 0 || subArgs.includes("--help") || subArgs.includes("-h")) {
|
|
921
975
|
await printHelp({
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
["typesafe", "api.typesafe.ai, model jev-1.13.0"],
|
|
952
|
-
["openrouter", "openrouter.ai, model typesafe/jev-1.13, zero-retention routing"],
|
|
953
|
-
["vercel", "Vercel AI Gateway, model typesafe-ai/jev"],
|
|
954
|
-
["cloudflare", "Workers AI, model typesafe/jev; needs --account-id"],
|
|
955
|
-
["custom", "any TypeSafe-compatible endpoint; needs --base-url"],
|
|
956
|
-
["failproofai", "FailproofAI Cloud, on your org's plan; no key or URL of your own"],
|
|
957
|
-
],
|
|
958
|
-
after: [
|
|
959
|
-
"--url reads the provider off the host, so it needs no --provider;",
|
|
960
|
-
"any other host is custom, with that URL as its base. It is a BASE:",
|
|
961
|
-
"/systemone is appended to it, and a URL that already names an",
|
|
962
|
-
"endpoint (/models, /chat/completions, /systemone, …) is refused",
|
|
963
|
-
"with the base it implies. `failproofai jev models` says which",
|
|
964
|
-
"model ids a base serves, and setup refuses one it does not.",
|
|
965
|
-
"",
|
|
966
|
-
"FailproofAI Cloud needs no setup here: `failproofai config --token",
|
|
967
|
-
"<key>` with a key that carries jev:evaluate (the \"machine\" preset)",
|
|
968
|
-
"turns it on in observe mode when there is no jev.json yet — except",
|
|
969
|
-
"with --no-transcripts, which only stores the key; then",
|
|
970
|
-
"`failproofai jev setup --provider failproofai` switches it on. Its",
|
|
971
|
-
"key stays in credentials.json; no --url ever selects it.",
|
|
972
|
-
],
|
|
973
|
-
},
|
|
974
|
-
{
|
|
975
|
-
label: "setup options",
|
|
976
|
-
entries: [
|
|
977
|
-
["--url <url>", "The endpoint. Picks the provider from its host."],
|
|
978
|
-
["--token <token>", "The key, on the command line — history and `ps` see it."],
|
|
979
|
-
["--provider <kind>", "Required the first time, or to switch providers."],
|
|
980
|
-
["--key-stdin", "Read the key from stdin; on a terminal, a masked prompt."],
|
|
981
|
-
["--key-from-env", "Store no key; read FAILPROOFAI_JEV_API_KEY per session."],
|
|
982
|
-
["--account-id <id>", "Cloudflare account id (32 hex characters)."],
|
|
983
|
-
["--base-url <url>", "Override the API base; `default` clears it."],
|
|
984
|
-
["--model <id>", "Override the model id; `default` clears it."],
|
|
985
|
-
["--mode <m>", "enforce (default), observe (log Jev, enforce regex), or off."],
|
|
986
|
-
["--timeout-ms <n>", "Per-call budget before falling back. Default 3000."],
|
|
987
|
-
],
|
|
988
|
-
},
|
|
989
|
-
{
|
|
990
|
-
label: "notes",
|
|
991
|
-
lines: [
|
|
992
|
-
"• Global only: ~/.failproofai/jev.json, written 0600. A copy anyone",
|
|
993
|
-
" else can read — or one in a directory anyone else can WRITE, who",
|
|
994
|
-
" could replace it — is refused; a repository can never set it.",
|
|
995
|
-
"• Re-running setup for the same provider keeps the key, so",
|
|
996
|
-
" `failproofai jev setup --mode observe` just switches the mode.",
|
|
997
|
-
"• The daemon does not see your shell's environment: keep the key in",
|
|
998
|
-
" the file on a machine set up with `failproofai config`.",
|
|
999
|
-
"• --token is the fast path, not the safe one: your shell history keeps",
|
|
1000
|
-
" it and the process list shows it. Prefer --key-stdin.",
|
|
1001
|
-
],
|
|
1002
|
-
},
|
|
1003
|
-
{
|
|
1004
|
-
label: "examples",
|
|
1005
|
-
lines: [
|
|
1006
|
-
"failproofai jev --url https://api.typesafe.ai/v1 --key-stdin < ~/typesafe.key",
|
|
1007
|
-
"failproofai jev --url https://openrouter.ai/api/v1 --token <token>",
|
|
1008
|
-
"failproofai jev setup --provider typesafe --key-stdin < ~/typesafe.key",
|
|
1009
|
-
"failproofai jev setup --provider cloudflare --account-id <id> --key-stdin",
|
|
1010
|
-
"failproofai config --token <key> (FailproofAI Cloud, observe mode)",
|
|
1011
|
-
"failproofai jev setup --mode enforce (switch the configured route's mode)",
|
|
1012
|
-
"failproofai jev setup --mode off (keep the config, stop asking Jev)",
|
|
1013
|
-
"failproofai jev test",
|
|
1014
|
-
"failproofai jev status",
|
|
1015
|
-
"failproofai jev models",
|
|
1016
|
-
],
|
|
1017
|
-
},
|
|
976
|
+
name: "jev",
|
|
977
|
+
// One row per subcommand: the design's `status | test | models | remove`
|
|
978
|
+
// runs as a shell pipeline if pasted, and the four take different flags.
|
|
979
|
+
usage: [
|
|
980
|
+
["failproofai jev setup --provider <kind> --key-stdin", "Turn Jev on"],
|
|
981
|
+
["failproofai jev --url <url> --key-stdin", "Turn Jev on from a URL"],
|
|
982
|
+
["failproofai jev setup --provider failproofai", "Use FailproofAI Cloud"],
|
|
983
|
+
["failproofai jev status [--json]", "Show config and activity"],
|
|
984
|
+
["failproofai jev test [--json]", "Send one live request"],
|
|
985
|
+
["failproofai jev models [--provider <kind>] [--url <base>] [--json]"],
|
|
986
|
+
["failproofai jev remove", "Delete the config"],
|
|
987
|
+
],
|
|
988
|
+
options: [
|
|
989
|
+
["--provider <kind>", "typesafe openrouter vercel cloudflare custom failproofai"],
|
|
990
|
+
["--key-stdin", "Read the API key from stdin, or a prompt on a terminal"],
|
|
991
|
+
["--key-from-env", "Store no key; read it from FAILPROOFAI_JEV_API_KEY"],
|
|
992
|
+
["--token <token>", "The API key on the command line; prefer --key-stdin"],
|
|
993
|
+
["--url <url>", "The API base URL; picks the provider from its host"],
|
|
994
|
+
["--base-url <url>", "Override the API base; default clears it"],
|
|
995
|
+
["--mode <m>", "enforce, observe (log Jev, enforce regex) or off"],
|
|
996
|
+
["--model <id>", "Override the model id; default clears it"],
|
|
997
|
+
["--account-id <id>", "Cloudflare account id, 32 hex characters"],
|
|
998
|
+
["--timeout-ms <n>", "Time per call before falling back to regex (default 3000)"],
|
|
999
|
+
["--json", "Print JSON (status, test and models)"],
|
|
1000
|
+
],
|
|
1001
|
+
examples: [
|
|
1002
|
+
"failproofai jev setup --provider typesafe --key-stdin < key.txt",
|
|
1003
|
+
"failproofai jev setup --mode observe",
|
|
1004
|
+
"failproofai jev test",
|
|
1018
1005
|
],
|
|
1019
1006
|
});
|
|
1020
1007
|
process.exit(0);
|
|
@@ -1050,36 +1037,10 @@ async function runCli() {
|
|
|
1050
1037
|
const subArgs = args.slice(1);
|
|
1051
1038
|
if (subArgs.includes("--help") || subArgs.includes("-h")) {
|
|
1052
1039
|
await printHelp({
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
{
|
|
1058
|
-
label: "why",
|
|
1059
|
-
lines: [
|
|
1060
|
-
"npm cannot update an installed package on its own, so a machine can sit",
|
|
1061
|
-
"on an old version for months and then jump several layouts at once. This",
|
|
1062
|
-
"runs the steps for that jump in order, keyed on the LAYOUT recorded in",
|
|
1063
|
-
"~/.failproofai/VERSION rather than on the npm version — so skipping",
|
|
1064
|
-
"thirty releases with no layout change runs nothing at all.",
|
|
1065
|
-
"",
|
|
1066
|
-
"It normally happens by itself, on the first command after an upgrade.",
|
|
1067
|
-
"This is for running it deliberately, and for seeing what it would do",
|
|
1068
|
-
"first.",
|
|
1069
|
-
"",
|
|
1070
|
-
"Your settings, cloud enrolment, policy selection, your own policy files,",
|
|
1071
|
-
"decision history and undelivered events are carried across, not removed.",
|
|
1072
|
-
"The irreplaceable files are copied to migrations/backup-layout<n>/ before",
|
|
1073
|
-
"anything runs, and every step is recorded in migrations/applied.json.",
|
|
1074
|
-
],
|
|
1075
|
-
},
|
|
1076
|
-
{
|
|
1077
|
-
label: "options",
|
|
1078
|
-
entries: [
|
|
1079
|
-
["--dry-run", "Print the steps and the files it would save. Change nothing."],
|
|
1080
|
-
],
|
|
1081
|
-
},
|
|
1082
|
-
],
|
|
1040
|
+
name: "migrate",
|
|
1041
|
+
usage: [["failproofai migrate", "Bring ~/.failproofai up to this version's layout"]],
|
|
1042
|
+
options: [["--dry-run", "Show the steps, change nothing"]],
|
|
1043
|
+
optionsCol: 20,
|
|
1083
1044
|
});
|
|
1084
1045
|
process.exit(0);
|
|
1085
1046
|
}
|
|
@@ -1088,7 +1049,7 @@ async function runCli() {
|
|
|
1088
1049
|
const unknown = subArgs.find((a) => a.startsWith("-") && !KNOWN.has(a));
|
|
1089
1050
|
if (unknown) {
|
|
1090
1051
|
throw new CliError(
|
|
1091
|
-
`Unexpected argument: ${unknown}
|
|
1052
|
+
`Unexpected argument: ${unknown}.\nRun \`failproofai migrate --help\` for usage.`,
|
|
1092
1053
|
);
|
|
1093
1054
|
}
|
|
1094
1055
|
|
|
@@ -1136,11 +1097,13 @@ async function runCli() {
|
|
|
1136
1097
|
(step) => ` ${step.ok ? "migrated" : "FAILED "} layout ${step.from} → ${step.to}`,
|
|
1137
1098
|
);
|
|
1138
1099
|
if (run.backedUp.length > 0) report.push("", `Saved first: ${run.backedUp.join(", ")}`);
|
|
1100
|
+
// The failing step is marked where it is: the backup line comes first, and
|
|
1101
|
+
// left to printReport's guess it took the ✕ with the error indented under it.
|
|
1139
1102
|
if (run.failed) {
|
|
1140
1103
|
report.push(
|
|
1141
1104
|
"",
|
|
1142
|
-
|
|
1143
|
-
`The home is still marked layout ${from} and will be retried.`,
|
|
1105
|
+
`✕ Step ${run.failed.from} → ${run.failed.to} did not finish: ${run.failed.error}`,
|
|
1106
|
+
` The home is still marked layout ${from} and will be retried.`,
|
|
1144
1107
|
);
|
|
1145
1108
|
}
|
|
1146
1109
|
await printReport("migrate", report, {
|
|
@@ -1162,36 +1125,10 @@ async function runCli() {
|
|
|
1162
1125
|
const subArgs = args.slice(1);
|
|
1163
1126
|
if (subArgs.includes("--help") || subArgs.includes("-h")) {
|
|
1164
1127
|
await printHelp({
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
label: "usage",
|
|
1170
|
-
entries: [["npm install -g failproofai@latest && failproofai update [--no-daemon]"]],
|
|
1171
|
-
},
|
|
1172
|
-
{
|
|
1173
|
-
label: "why",
|
|
1174
|
-
lines: [
|
|
1175
|
-
"npm replaces the CLI and nothing else. The daemon binary lives at",
|
|
1176
|
-
"~/.failproofai/bin/failproofaid-<version> and stays exactly where it was,",
|
|
1177
|
-
"so after an npm upgrade the two halves are different versions — and",
|
|
1178
|
-
"failproofaid refuses to start against a layout it does not speak, which",
|
|
1179
|
-
"is the loud version of that problem rather than the silent one.",
|
|
1180
|
-
"",
|
|
1181
|
-
"This does the rest of the upgrade: runs any pending layout migrations,",
|
|
1182
|
-
"puts the matching daemon binary in place, and restarts the service.",
|
|
1183
|
-
"Hermes profiles already using failproofai are moved to the linked",
|
|
1184
|
-
"native plugin (legacy shell hooks never checked Hermes cron jobs).",
|
|
1185
|
-
"Exits non-zero when any half could not be brought current.",
|
|
1186
|
-
],
|
|
1187
|
-
},
|
|
1188
|
-
{
|
|
1189
|
-
label: "options",
|
|
1190
|
-
entries: [
|
|
1191
|
-
["--no-daemon", "Migrate the home only. Leaves a version-skewed daemon in place, so prefer letting it run."],
|
|
1192
|
-
],
|
|
1193
|
-
},
|
|
1194
|
-
],
|
|
1128
|
+
name: "update",
|
|
1129
|
+
usage: [["npm i -g failproofai@latest && failproofai update"]],
|
|
1130
|
+
options: [["--no-daemon", "Migrate everything but leave the daemon as is"]],
|
|
1131
|
+
optionsCol: 20,
|
|
1195
1132
|
});
|
|
1196
1133
|
process.exit(0);
|
|
1197
1134
|
}
|
|
@@ -1200,7 +1137,7 @@ async function runCli() {
|
|
|
1200
1137
|
const unknown = subArgs.find((a) => a.startsWith("-") && !KNOWN.has(a));
|
|
1201
1138
|
if (unknown) {
|
|
1202
1139
|
throw new CliError(
|
|
1203
|
-
`Unexpected argument: ${unknown}
|
|
1140
|
+
`Unexpected argument: ${unknown}.\nRun \`failproofai update --help\` for usage.`,
|
|
1204
1141
|
);
|
|
1205
1142
|
}
|
|
1206
1143
|
|
|
@@ -1242,11 +1179,14 @@ async function runCli() {
|
|
|
1242
1179
|
report.push(` ${step.ok ? "migrated" : "FAILED "} layout ${step.from} → ${step.to}`);
|
|
1243
1180
|
}
|
|
1244
1181
|
if (run.backedUp.length > 0) report.push("", `Saved first: ${run.backedUp.join(", ")}`);
|
|
1182
|
+
// Each failing half marks its own line (the daemon's lines arrive with
|
|
1183
|
+
// their ✕ already): the report opens on whatever ran first, often a
|
|
1184
|
+
// success, and printReport's guess would put the ✕ there.
|
|
1245
1185
|
if (run.failed) {
|
|
1246
1186
|
report.push(
|
|
1247
1187
|
"",
|
|
1248
|
-
|
|
1249
|
-
`The home is still marked layout ${state.found} and will be retried.`,
|
|
1188
|
+
`✕ Step ${run.failed.from} → ${run.failed.to} did not finish: ${run.failed.error}`,
|
|
1189
|
+
` The home is still marked layout ${state.found} and will be retried.`,
|
|
1250
1190
|
);
|
|
1251
1191
|
}
|
|
1252
1192
|
} else {
|
|
@@ -1285,19 +1225,22 @@ async function runCli() {
|
|
|
1285
1225
|
const hermesResult = await runHermesUpdateMigration({
|
|
1286
1226
|
daemonSupportsPolicyEvaluation: () => svc.probeDaemonPolicyEvaluation(),
|
|
1287
1227
|
});
|
|
1288
|
-
if (hermesResult.lines.length > 0)
|
|
1228
|
+
if (hermesResult.lines.length > 0) {
|
|
1229
|
+
// Its lines open on a "Hermes:" heading over one row per profile; on a
|
|
1230
|
+
// failure the heading carries the ✕ and the rows say which profile.
|
|
1231
|
+
const [heading, ...rest] = hermesResult.lines;
|
|
1232
|
+
report.push("", hermesResult.ok ? heading : `✕ ${heading}`, ...rest);
|
|
1233
|
+
}
|
|
1289
1234
|
hermesOk = hermesResult.ok;
|
|
1290
1235
|
hermesMigrated = hermesResult.profiles.filter((p) => p.status === "migrated").length;
|
|
1291
1236
|
} catch (err) {
|
|
1292
|
-
report.push("",
|
|
1237
|
+
report.push("", `✕ Hermes migration failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1293
1238
|
hermesOk = false;
|
|
1294
1239
|
}
|
|
1295
1240
|
|
|
1296
1241
|
const updateOk = daemonOk && !migrationFailed && hermesOk;
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
meta: `v${version}`,
|
|
1300
|
-
});
|
|
1242
|
+
// No `v${version}` meta: the header already names the version.
|
|
1243
|
+
await printReport("update", report, { ok: updateOk });
|
|
1301
1244
|
await track("cli_update", {
|
|
1302
1245
|
ok: updateOk,
|
|
1303
1246
|
migrations: migrationsRan,
|
|
@@ -1314,39 +1257,15 @@ async function runCli() {
|
|
|
1314
1257
|
const subArgs = args.slice(1);
|
|
1315
1258
|
if (subArgs.includes("--help") || subArgs.includes("-h")) {
|
|
1316
1259
|
await printHelp({
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
lines: [
|
|
1324
|
-
"• failproofai hook entries from every agent CLI that has them",
|
|
1325
|
-
"• the failproofaid daemon service — ASKED on a plain uninstall (kept",
|
|
1326
|
-
" unless you say yes); always removed with --purge, which prompts for",
|
|
1327
|
-
" your password rather than printing commands to paste",
|
|
1328
|
-
"• the \"require the daemon\" flag — cleared FIRST, so a partial uninstall",
|
|
1329
|
-
" can never leave this machine denying every tool call",
|
|
1330
|
-
],
|
|
1331
|
-
},
|
|
1332
|
-
{
|
|
1333
|
-
label: "options",
|
|
1334
|
-
entries: [
|
|
1335
|
-
["--purge", "Also delete ~/.failproofai — settings, credentials, audit history and the downloaded daemon binary. Off by default so a reinstall keeps your history."],
|
|
1336
|
-
["--dry-run", "Print what would be removed and change nothing."],
|
|
1337
|
-
["--yes, -y", "Skip the confirmation prompt. Required when there is no TTY."],
|
|
1338
|
-
],
|
|
1339
|
-
},
|
|
1340
|
-
{
|
|
1341
|
-
label: "why this exists",
|
|
1342
|
-
lines: [
|
|
1343
|
-
"npm runs no uninstall script, so `npm rm -g failproofai` removes the",
|
|
1344
|
-
"package and leaves the hook entries and the service behind. Run this",
|
|
1345
|
-
"first, then:",
|
|
1346
|
-
" npm rm -g failproofai",
|
|
1347
|
-
],
|
|
1348
|
-
},
|
|
1260
|
+
name: "uninstall",
|
|
1261
|
+
usage: [["failproofai uninstall [options]"]],
|
|
1262
|
+
options: [
|
|
1263
|
+
["--purge", "Also delete ~/.failproofai: settings, keys and history"],
|
|
1264
|
+
["--dry-run", "Show what would be removed, change nothing"],
|
|
1265
|
+
["--yes, -y", "Skip confirmation, remove the daemon; needed without a TTY"],
|
|
1349
1266
|
],
|
|
1267
|
+
examples: ["failproofai uninstall && npm rm -g failproofai"],
|
|
1268
|
+
optionsCol: 20,
|
|
1350
1269
|
});
|
|
1351
1270
|
process.exit(0);
|
|
1352
1271
|
}
|
|
@@ -1355,79 +1274,47 @@ async function runCli() {
|
|
|
1355
1274
|
const unknown = subArgs.find((a) => !KNOWN.has(a));
|
|
1356
1275
|
if (unknown) {
|
|
1357
1276
|
throw new CliError(
|
|
1358
|
-
`Unexpected argument: ${unknown}
|
|
1277
|
+
`Unexpected argument: ${unknown}.\nRun \`failproofai uninstall --help\` for usage.`,
|
|
1359
1278
|
);
|
|
1360
1279
|
}
|
|
1361
1280
|
|
|
1362
1281
|
lastSubcommand = "uninstall_command";
|
|
1363
1282
|
const { runUninstallCommand } = await import("../src/hooks/uninstall-cli");
|
|
1283
|
+
const { screenKit, optsFor, printBlock, stack } = await import("../src/hooks/tui");
|
|
1364
1284
|
const purge = subArgs.includes("--purge");
|
|
1365
1285
|
const dryRun = subArgs.includes("--dry-run");
|
|
1366
1286
|
const yes = subArgs.includes("--yes") || subArgs.includes("-y");
|
|
1287
|
+
const render = optsFor(process.stdout);
|
|
1288
|
+
const kit = screenKit(render);
|
|
1367
1289
|
|
|
1368
|
-
// Set only when the
|
|
1369
|
-
//
|
|
1290
|
+
// Set only when the question was actually asked, which is the one case
|
|
1291
|
+
// where the plan is already on screen and printing it again would
|
|
1292
|
+
// duplicate it.
|
|
1370
1293
|
let planWasShown = false;
|
|
1371
1294
|
const result = await runUninstallCommand({
|
|
1372
1295
|
purge,
|
|
1373
1296
|
dryRun,
|
|
1374
1297
|
yes,
|
|
1375
1298
|
cwd: process.cwd(),
|
|
1376
|
-
|
|
1377
|
-
//
|
|
1299
|
+
render,
|
|
1300
|
+
// ONE question (D17): the plan says exactly what goes, and `y` removes all
|
|
1301
|
+
// of it, the daemon service included — the same thing `--yes` does. Only
|
|
1302
|
+
// offered when a person can actually answer: without a TTY the command
|
|
1303
|
+
// requires --yes rather than assuming consent — see the module.
|
|
1378
1304
|
confirm: process.stdin.isTTY
|
|
1379
1305
|
? async (planLines) => {
|
|
1380
1306
|
planWasShown = true;
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
// non-destructive one.
|
|
1384
|
-
const answer = await selectOne({
|
|
1385
|
-
message: purge
|
|
1386
|
-
? "Remove failproofai and DELETE ~/.failproofai?"
|
|
1387
|
-
: "Remove failproofai from this machine?",
|
|
1388
|
-
body: planLines,
|
|
1389
|
-
choices: [
|
|
1390
|
-
{ label: "No, cancel", value: false },
|
|
1391
|
-
{ label: purge ? "Yes, remove and purge" : "Yes, remove it", value: true },
|
|
1392
|
-
],
|
|
1393
|
-
});
|
|
1394
|
-
return answer === true;
|
|
1307
|
+
process.stdout.write(["", kit.header("Uninstall"), "", ...planLines].join("\n") + "\n");
|
|
1308
|
+
return kit.confirm({ stdin: process.stdin, stdout: process.stdout });
|
|
1395
1309
|
}
|
|
1396
1310
|
: undefined,
|
|
1397
|
-
// Only on a plain uninstall: `--purge` removes the service unconditionally
|
|
1398
|
-
// because it deletes the binary the service points at. Absent without a TTY,
|
|
1399
|
-
// which the module reads as "keep it".
|
|
1400
|
-
confirmDaemon:
|
|
1401
|
-
process.stdin.isTTY && !purge
|
|
1402
|
-
? async () => {
|
|
1403
|
-
const { selectOne } = await import("../src/hooks/tui");
|
|
1404
|
-
const answer = await selectOne({
|
|
1405
|
-
message: "Remove the failproofaid background service too?",
|
|
1406
|
-
body: [
|
|
1407
|
-
"It runs as a system service and needs sudo to remove, so you",
|
|
1408
|
-
"will be asked for your password.",
|
|
1409
|
-
"",
|
|
1410
|
-
"Keeping it is fine if you plan to reinstall — the hooks are",
|
|
1411
|
-
"already gone either way, so nothing is being enforced.",
|
|
1412
|
-
],
|
|
1413
|
-
choices: [
|
|
1414
|
-
{ label: "Yes, remove the service", value: true },
|
|
1415
|
-
{ label: "No, leave it installed", value: false },
|
|
1416
|
-
],
|
|
1417
|
-
});
|
|
1418
|
-
return answer === true;
|
|
1419
|
-
}
|
|
1420
|
-
: undefined,
|
|
1421
1311
|
});
|
|
1422
1312
|
|
|
1423
|
-
// The
|
|
1424
|
-
//
|
|
1425
|
-
//
|
|
1426
|
-
const
|
|
1427
|
-
|
|
1428
|
-
ok: result.exitCode === 0,
|
|
1429
|
-
meta: subArgs.includes("--purge") ? "purge" : subArgs.includes("--dry-run") ? "dry run" : undefined,
|
|
1430
|
-
});
|
|
1313
|
+
// The question already printed the header and the plan; the outcome goes
|
|
1314
|
+
// under it. `planLines` is reported by the command rather than guessed from
|
|
1315
|
+
// the text — see UninstallResult.
|
|
1316
|
+
const outcome = planWasShown ? result.lines.slice(result.planLines) : [kit.header("Uninstall"), "", ...result.lines];
|
|
1317
|
+
printBlock(result.exitCode === 0 ? process.stdout : process.stderr, stack(outcome));
|
|
1431
1318
|
// NOT after a purge. `track` resolves the instance id, and `getInstanceId()`
|
|
1432
1319
|
// lazily WRITES ~/.failproofai/state/telemetry-id — which re-created the
|
|
1433
1320
|
// whole directory seconds after the purge deleted it, leaving a machine the
|
|
@@ -1460,171 +1347,30 @@ async function runCli() {
|
|
|
1460
1347
|
// made the one documented command the only one that did nothing.
|
|
1461
1348
|
if (subArgs.includes("--help") || subArgs.includes("-h")) {
|
|
1462
1349
|
await printHelp({
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
label: "two commands, from nothing",
|
|
1468
|
-
entries: [
|
|
1469
|
-
["failproofai publish --init", "write a policy to start from"],
|
|
1470
|
-
["failproofai publish", "ship it"],
|
|
1471
|
-
],
|
|
1472
|
-
after: [
|
|
1473
|
-
"Write your policies in a git repo, run publish, answer one question,",
|
|
1474
|
-
"done. It asks WHERE only when nothing tells it — no remote to read —",
|
|
1475
|
-
"and works the rest out: which files hold policies, what version is",
|
|
1476
|
-
"next, who you are. Every flag below overrides something it would",
|
|
1477
|
-
"otherwise decide. None is required, and on a pipe or in CI the flags",
|
|
1478
|
-
"are all there is: nothing prompts where nobody can answer.",
|
|
1479
|
-
],
|
|
1480
|
-
},
|
|
1481
|
-
{
|
|
1482
|
-
label: "what --init does",
|
|
1483
|
-
lines: [
|
|
1484
|
-
"Asks what the pack is called, writes <name>.mjs, and stops. No network,",
|
|
1485
|
-
"no git, nothing published. The file is not a template with blanks — it",
|
|
1486
|
-
"is one policy that already blocks git push --force, so the first thing",
|
|
1487
|
-
"you do is edit something that works. It refuses rather than overwriting",
|
|
1488
|
-
"a file that exists.",
|
|
1489
|
-
"",
|
|
1490
|
-
"Then try it on THIS machine, before anyone else can see it:",
|
|
1491
|
-
"",
|
|
1492
|
-
" failproofai policies -i -c ./<name>.mjs",
|
|
1493
|
-
"",
|
|
1494
|
-
"That enforces the file right now — any path, any filename. Ask your",
|
|
1495
|
-
"agent to do the thing you blocked and watch it get refused. Nothing is",
|
|
1496
|
-
"published and nobody else is affected.",
|
|
1497
|
-
],
|
|
1498
|
-
},
|
|
1499
|
-
{
|
|
1500
|
-
label: "what publish does",
|
|
1501
|
-
lines: [
|
|
1502
|
-
"In order, stopping before it touches GitHub if anything is wrong:",
|
|
1503
|
-
"",
|
|
1504
|
-
"1 Finds the policy file here by CONTENT — one that imports failproofai",
|
|
1505
|
-
" and calls customPolicies.add — not by filename, so it finds",
|
|
1506
|
-
" guards.mjs and ignores an unrelated policies.mjs. Not recursive:",
|
|
1507
|
-
" publishing a fixture is worse than being asked. Two candidates and",
|
|
1508
|
-
" it lists them.",
|
|
1509
|
-
"2 Reads the repo from git remote get-url origin, in the FILE's",
|
|
1510
|
-
" directory rather than yours, and decides the version (see below).",
|
|
1511
|
-
"3 Finds your credential: GITHUB_TOKEN, GH_TOKEN, or gh auth login.",
|
|
1512
|
-
" Needs release-write and nothing else. Never printed.",
|
|
1513
|
-
"4 Creates the repository if it does not exist — public, see below.",
|
|
1514
|
-
"5 Builds the three assets, validating with the LOADER's own rules: the",
|
|
1515
|
-
" code that decides what may install on a stranger's machine. A pack",
|
|
1516
|
-
" that could never install fails here, where you can fix it.",
|
|
1517
|
-
"6 Creates or reuses the release and uploads, replacing assets of the",
|
|
1518
|
-
" same name — the install URL is built from fixed names, so a stale",
|
|
1519
|
-
" copy is what somebody would fetch.",
|
|
1520
|
-
],
|
|
1521
|
-
},
|
|
1522
|
-
{
|
|
1523
|
-
label: "how the version is decided",
|
|
1524
|
-
lines: [
|
|
1525
|
-
"The commit you are publishing from — its short sha, twelve",
|
|
1526
|
-
"characters: a1b2c3d4e5f6. Nothing to pick, nothing to count, and it",
|
|
1527
|
-
"names exactly where the bytes came from. Publish the same source",
|
|
1528
|
-
"twice and you get the same version, because there is nothing to",
|
|
1529
|
-
"increment.",
|
|
1530
|
-
"",
|
|
1531
|
-
"Twelve rather than git's seven: seven collides in a repository with",
|
|
1532
|
-
"enough objects, and a version that stops being unique means two",
|
|
1533
|
-
"artifacts claiming one name. The full sha is recorded beside it.",
|
|
1534
|
-
"",
|
|
1535
|
-
"Read from the tree in front of you, never from the repository's",
|
|
1536
|
-
"releases, so a fresh clone and an air-gapped machine compute the",
|
|
1537
|
-
"same answer and neither has to ask GitHub what happened before.",
|
|
1538
|
-
"",
|
|
1539
|
-
"It REFUSES rather than guessing, in two cases, because the version",
|
|
1540
|
-
"claims to name a commit and must not be minted where that is false:",
|
|
1541
|
-
"",
|
|
1542
|
-
" no git checkout there is no commit to name",
|
|
1543
|
-
" uncommitted changes those bytes are not in that commit",
|
|
1544
|
-
"",
|
|
1545
|
-
"--version overrides both, and a tag on HEAD wins over the sha —",
|
|
1546
|
-
"someone who tagged v1.2.0 has SAID what this release is.",
|
|
1547
|
-
"",
|
|
1548
|
-
"A sha does not order. `policies show <owner>/<repo> --releases` is",
|
|
1549
|
-
"where you see which came first, newest at the top.",
|
|
1550
|
-
],
|
|
1551
|
-
},
|
|
1552
|
-
{
|
|
1553
|
-
label: "what a release records",
|
|
1554
|
-
lines: [
|
|
1555
|
-
"The commit you published from, when you are in a git checkout, in the",
|
|
1556
|
-
"manifest and in the release notes. Provenance, not verification —",
|
|
1557
|
-
"the artifact digest is still the only thing that decides whether the",
|
|
1558
|
-
"bytes are the ones that were published. It answers the question a",
|
|
1559
|
-
"digest cannot: which source produced them.",
|
|
1560
|
-
"",
|
|
1561
|
-
"Which is also how anyone installs that exact release:",
|
|
1562
|
-
"",
|
|
1563
|
-
" failproofai policies add <owner>/<repo>@a1b2c3d",
|
|
1564
|
-
"",
|
|
1565
|
-
"and read the whole history with:",
|
|
1566
|
-
"",
|
|
1567
|
-
" failproofai policies show <owner>/<repo> --releases",
|
|
1568
|
-
],
|
|
1569
|
-
},
|
|
1570
|
-
{
|
|
1571
|
-
// This screen said "an existing private one still publishes, and
|
|
1572
|
-
// warns" for as long as that was true. It is REFUSED now — exit 1,
|
|
1573
|
-
// nothing created or uploaded — and `--allow-private` was named
|
|
1574
|
-
// nowhere in this help, so the one documented behaviour pointed a
|
|
1575
|
-
// publisher at a command that exits 1 and gave them no way through.
|
|
1576
|
-
label: "the repo must be public",
|
|
1577
|
-
lines: [
|
|
1578
|
-
"Installs are anonymous HTTPS with no credential to offer, so a private",
|
|
1579
|
-
"repository publishes to nobody. A repo created here is public for that",
|
|
1580
|
-
"reason; an existing private one is REFUSED, before anything is built,",
|
|
1581
|
-
"created or uploaded. --allow-private publishes to one anyway, for",
|
|
1582
|
-
"somebody who will hand the three assets over another way — it still",
|
|
1583
|
-
"says plainly that no `policies add` can reach them.",
|
|
1584
|
-
"",
|
|
1585
|
-
"Only the release matters. Installs read releases/download/<tag>/<asset>",
|
|
1586
|
-
"and never touch your git tree — pushing the source is for humans.",
|
|
1587
|
-
],
|
|
1588
|
-
},
|
|
1589
|
-
{
|
|
1590
|
-
label: "your policy files",
|
|
1591
|
-
lines: [
|
|
1592
|
-
"Write as many as you like — one per category reads well. Every file",
|
|
1593
|
-
"here that registers policies is bundled into the single artifact a pack",
|
|
1594
|
-
"has to be: only the entry is digest-pinned, and a pack importing",
|
|
1595
|
-
"siblings could not honestly claim to be verified. Bundling needs bun;",
|
|
1596
|
-
"without it, name one self-contained file.",
|
|
1597
|
-
"",
|
|
1598
|
-
"Each policy may carry category and defaultEnabled alongside the usual",
|
|
1599
|
-
"fields. category is what --category selects on; defaultEnabled is what",
|
|
1600
|
-
"a bare `policies add` switches on, and it defaults to false.",
|
|
1601
|
-
],
|
|
1602
|
-
},
|
|
1603
|
-
{
|
|
1604
|
-
label: "options",
|
|
1605
|
-
entries: [
|
|
1606
|
-
["--init [file]", "Write a starter policy and stop."],
|
|
1607
|
-
["--repo <owner>/<repo>", "Where to release it, created if missing."],
|
|
1608
|
-
["--version <version>", "Name the version, instead of the commit it was built from."],
|
|
1609
|
-
["--id <publisher/name>", "The pack's id. Defaults to --repo."],
|
|
1610
|
-
["--tag <tag>", "Release tag. Defaults to the version; v prefix ok."],
|
|
1611
|
-
["--notes <text>", "Release notes."],
|
|
1612
|
-
["--out <dir>", "Where to write the assets. Default: dist-pack."],
|
|
1613
|
-
["--effect <effect>", "enforce or observe. observe records and blocks nothing. Default: enforce."],
|
|
1614
|
-
["--dry-run", "Build the assets, publish nothing. No credential."],
|
|
1615
|
-
["--allow-private", "Publish to an already-private repo anyway. Nobody can install it."],
|
|
1616
|
-
],
|
|
1617
|
-
},
|
|
1618
|
-
{
|
|
1619
|
-
label: "examples",
|
|
1620
|
-
lines: [
|
|
1621
|
-
"failproofai publish --init",
|
|
1622
|
-
"failproofai publish",
|
|
1623
|
-
"failproofai publish --dry-run",
|
|
1624
|
-
"failproofai publish ./guards.mjs --repo me/guards --version 2.0.0",
|
|
1625
|
-
],
|
|
1626
|
-
},
|
|
1350
|
+
name: "publish",
|
|
1351
|
+
usage: [
|
|
1352
|
+
["failproofai publish --init [file]", "Write a starter policy file"],
|
|
1353
|
+
["failproofai publish [file]", "Build and release it on GitHub"],
|
|
1627
1354
|
],
|
|
1355
|
+
options: [
|
|
1356
|
+
["--repo <owner/repo>", "Where to release, created if missing (default: origin)"],
|
|
1357
|
+
["--version <v>", "Name the version (default: HEAD's tag, else short sha)"],
|
|
1358
|
+
["--id <publisher/name>", "The pack id (default: the repo)"],
|
|
1359
|
+
["--tag <tag>", "Release tag (default: the version, v prefix ok)"],
|
|
1360
|
+
["--notes <text>", "Release notes (default: policy counts and the commit)"],
|
|
1361
|
+
["--out <dir>", "Where to write the assets (default: dist-pack)"],
|
|
1362
|
+
["--effect <e>", "enforce (default), or observe: record without blocking"],
|
|
1363
|
+
["--min-cli-version <v>", "Oldest failproofai version that may install it"],
|
|
1364
|
+
["--dry-run", "Build the assets and publish nothing"],
|
|
1365
|
+
["--allow-private", "Publish to a private repo anyway; installs will fail"],
|
|
1366
|
+
["--entry <file>", "The policy file, same as [file]"],
|
|
1367
|
+
],
|
|
1368
|
+
examples: [
|
|
1369
|
+
"failproofai publish --init guards",
|
|
1370
|
+
"failproofai policies -i -c ./guards.mjs",
|
|
1371
|
+
"failproofai publish --repo me/guards",
|
|
1372
|
+
],
|
|
1373
|
+
optionsCol: 24,
|
|
1628
1374
|
});
|
|
1629
1375
|
process.exit(0);
|
|
1630
1376
|
}
|
|
@@ -1659,81 +1405,7 @@ async function runCli() {
|
|
|
1659
1405
|
const subArgs = args.slice(1);
|
|
1660
1406
|
|
|
1661
1407
|
if (subArgs.length === 0 || subArgs.includes("--help") || subArgs.includes("-h")) {
|
|
1662
|
-
await printHelp(
|
|
1663
|
-
command: "policies add|remove|show",
|
|
1664
|
-
tagline: "choose what your agents may do",
|
|
1665
|
-
sections: [
|
|
1666
|
-
{
|
|
1667
|
-
label: "usage",
|
|
1668
|
-
// Without the `failproofai policies` prefix, which the heading two
|
|
1669
|
-
// lines up already carries: repeating it costs 21 of the 80 columns
|
|
1670
|
-
// on every row and pushes each description into a second line. The
|
|
1671
|
-
// examples below are the copy-pasteable spelling.
|
|
1672
|
-
entries: [
|
|
1673
|
-
["add", "Pick from what is installed here"],
|
|
1674
|
-
["add <name>", "Turn one policy on"],
|
|
1675
|
-
["add <owner>/<repo>", "Install someone's pack"],
|
|
1676
|
-
["remove <name>", "Turn one policy off"],
|
|
1677
|
-
["remove <pack-id>", "Uninstall a pack"],
|
|
1678
|
-
["show <owner>/<repo>", "What a pack contains, before you take it"],
|
|
1679
|
-
["show <owner>/<repo> --releases", "Every version it has published, and which one is here"],
|
|
1680
|
-
],
|
|
1681
|
-
},
|
|
1682
|
-
{
|
|
1683
|
-
label: "a name or a source",
|
|
1684
|
-
lines: [
|
|
1685
|
-
"Anything with a slash is a pack source; anything without is a policy",
|
|
1686
|
-
"name. Policy names cannot contain a slash, so there is nothing to guess.",
|
|
1687
|
-
"",
|
|
1688
|
-
" block-sudo a policy",
|
|
1689
|
-
" FailproofAI/policies our pack, like any other",
|
|
1690
|
-
" acme/deploy-guard newest release, pinned",
|
|
1691
|
-
" acme/deploy-guard@a1b2c3d4e5f6 that release",
|
|
1692
|
-
" acme/deploy-guard@a1b2c3d the release built from",
|
|
1693
|
-
" that commit",
|
|
1694
|
-
" github:acme/deploy-guard@a1b2c3d4e5f6 same, explicit",
|
|
1695
|
-
" https://github.com/acme/x/releases/tag/v2 the URL you copied",
|
|
1696
|
-
],
|
|
1697
|
-
},
|
|
1698
|
-
{
|
|
1699
|
-
label: "choosing part of a pack",
|
|
1700
|
-
entries: [
|
|
1701
|
-
["--policy a,b", "exactly these (comma-separated or repeated)"],
|
|
1702
|
-
["--category x,y", "whole categories (failproofai policies show <source>)"],
|
|
1703
|
-
["--all", "everything it contains"],
|
|
1704
|
-
],
|
|
1705
|
-
},
|
|
1706
|
-
{
|
|
1707
|
-
label: "options (policy names only)",
|
|
1708
|
-
entries: [
|
|
1709
|
-
["--cli <agent>...", "Agent CLI(s) to apply to; space-separated or repeated. Omit to detect installed CLIs and prompt."],
|
|
1710
|
-
["--scope user|project|local", "Config scope. Default: user."],
|
|
1711
|
-
["--beta", "Allow beta policies"],
|
|
1712
|
-
],
|
|
1713
|
-
},
|
|
1714
|
-
{
|
|
1715
|
-
label: "examples",
|
|
1716
|
-
lines: [
|
|
1717
|
-
"failproofai policies add",
|
|
1718
|
-
"failproofai policies add block-sudo",
|
|
1719
|
-
"failproofai policies add sanitize-api-keys --scope project",
|
|
1720
|
-
"failproofai policies add FailproofAI/policies --category sanitize,git",
|
|
1721
|
-
"failproofai policies add acme/deploy-guard --policy block-prod-deploy",
|
|
1722
|
-
"failproofai policies show acme/deploy-guard",
|
|
1723
|
-
"failproofai policies remove block-sudo",
|
|
1724
|
-
],
|
|
1725
|
-
},
|
|
1726
|
-
],
|
|
1727
|
-
footer: [
|
|
1728
|
-
"With no flags you get the pack's own defaults and are shown the rest.",
|
|
1729
|
-
"Re-adding at a newer version keeps what you chose.",
|
|
1730
|
-
"Agents: claude, codex, copilot, cursor, opencode, pi, hermes, openclaw,",
|
|
1731
|
-
"factory, devin, antigravity, goose.",
|
|
1732
|
-
"Publishing your own: failproofai publish --help",
|
|
1733
|
-
"Offline: FAILPROOFAI_NO_DOWNLOAD=1 refuses to fetch; packs already",
|
|
1734
|
-
"installed keep enforcing. FAILPROOFAI_PACK_BASE_URL points it at a mirror.",
|
|
1735
|
-
],
|
|
1736
|
-
});
|
|
1408
|
+
await printHelp(POLICIES_HELP);
|
|
1737
1409
|
process.exit(0);
|
|
1738
1410
|
}
|
|
1739
1411
|
|
|
@@ -1785,8 +1457,8 @@ async function runCli() {
|
|
|
1785
1457
|
if (action === "show" || looksLikeSource) {
|
|
1786
1458
|
if (action === "show" && !firstPositional) {
|
|
1787
1459
|
throw new CliError(
|
|
1788
|
-
"
|
|
1789
|
-
"
|
|
1460
|
+
"Name the pack to show, as <owner>/<repo>.\n" +
|
|
1461
|
+
"To see what is installed here, run `failproofai policies`",
|
|
1790
1462
|
);
|
|
1791
1463
|
}
|
|
1792
1464
|
// One lane, one implementation. `show` is the pack lane's remote preview,
|
|
@@ -1809,44 +1481,22 @@ async function runCli() {
|
|
|
1809
1481
|
return;
|
|
1810
1482
|
}
|
|
1811
1483
|
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
}
|
|
1818
|
-
|
|
1484
|
+
// `show` left above, so only add and remove reach here. A branch for any
|
|
1485
|
+
// other word stood here, unreachable: this lane is entered for those three.
|
|
1486
|
+
const validScopes = action === "remove"
|
|
1487
|
+
? ["user", "project", "local", "all"]
|
|
1488
|
+
: ["user", "project", "local"];
|
|
1819
1489
|
const scopeIdx = rest.indexOf("--scope");
|
|
1820
1490
|
const scope = scopeIdx >= 0 ? rest[scopeIdx + 1] : "user";
|
|
1821
1491
|
if (scopeIdx >= 0 && (!scope || scope.startsWith("-"))) {
|
|
1822
|
-
throw new CliError(
|
|
1492
|
+
throw new CliError(`Missing value for --scope.\nUse one of: ${validScopes.join(", ")}`);
|
|
1823
1493
|
}
|
|
1824
|
-
const validScopes = action === "remove"
|
|
1825
|
-
? ["user", "project", "local", "all"]
|
|
1826
|
-
: ["user", "project", "local"];
|
|
1827
1494
|
if (scopeIdx >= 0 && !validScopes.includes(scope)) {
|
|
1828
|
-
throw new CliError(`Invalid scope: ${scope}
|
|
1495
|
+
throw new CliError(`Invalid scope: ${scope}.\nUse one of: ${validScopes.join(", ")}`);
|
|
1829
1496
|
}
|
|
1830
1497
|
|
|
1831
1498
|
// --cli accepts one or more space-separated values, optionally repeated.
|
|
1832
|
-
const
|
|
1833
|
-
const cliFlagValues = [];
|
|
1834
|
-
const cliConsumedIdxs = new Set();
|
|
1835
|
-
const cliFlagIdxs = rest.map((a, i) => (a === "--cli" ? i : -1)).filter((i) => i >= 0);
|
|
1836
|
-
for (const idx of cliFlagIdxs) {
|
|
1837
|
-
let consumed = 0;
|
|
1838
|
-
for (let j = idx + 1; j < rest.length; j++) {
|
|
1839
|
-
const v = rest[j];
|
|
1840
|
-
if (v.startsWith("-")) break;
|
|
1841
|
-
if (!VALID_CLIS.has(v)) break;
|
|
1842
|
-
cliFlagValues.push(v);
|
|
1843
|
-
cliConsumedIdxs.add(j);
|
|
1844
|
-
consumed++;
|
|
1845
|
-
}
|
|
1846
|
-
if (consumed === 0) {
|
|
1847
|
-
throw new CliError("Missing value(s) for --cli. Usage: --cli claude codex copilot cursor opencode pi hermes openclaw (or any subset)");
|
|
1848
|
-
}
|
|
1849
|
-
}
|
|
1499
|
+
const { cliFlagValues, cliConsumedIdxs } = await readCliFlag(rest);
|
|
1850
1500
|
|
|
1851
1501
|
const includeBeta = rest.includes("--beta");
|
|
1852
1502
|
|
|
@@ -1854,7 +1504,17 @@ async function runCli() {
|
|
|
1854
1504
|
const knownFlags = new Set(["--scope", "--cli", "--beta"]);
|
|
1855
1505
|
const unknownFlag = rest.find((a) => a.startsWith("-") && !knownFlags.has(a));
|
|
1856
1506
|
if (unknownFlag) {
|
|
1857
|
-
|
|
1507
|
+
// -c is on the shared help page, for --install and --uninstall; pointing
|
|
1508
|
+
// back at that page answered the question with itself.
|
|
1509
|
+
const custom = unknownFlag === "-c" || unknownFlag === "--custom";
|
|
1510
|
+
throw new CliError(
|
|
1511
|
+
`Unknown flag: ${unknownFlag}.\n` +
|
|
1512
|
+
(custom
|
|
1513
|
+
? action === "add"
|
|
1514
|
+
? "Add a custom policy file with `failproofai policies --install -c <file>`"
|
|
1515
|
+
: "Remove custom policy files with `failproofai policies --uninstall -c`"
|
|
1516
|
+
: `Run \`failproofai policies ${action} --help\` for usage.`),
|
|
1517
|
+
);
|
|
1858
1518
|
}
|
|
1859
1519
|
|
|
1860
1520
|
// Positional policy names = anything not consumed by --scope / --cli.
|
|
@@ -1879,8 +1539,8 @@ async function runCli() {
|
|
|
1879
1539
|
}
|
|
1880
1540
|
if (positional.length > 1) {
|
|
1881
1541
|
throw new CliError(
|
|
1882
|
-
|
|
1883
|
-
`For
|
|
1542
|
+
`policies ${action} takes one policy name, not ${positional.length}.\n` +
|
|
1543
|
+
`For several, run \`failproofai policies --${action === "add" ? "install" : "uninstall"} ${positional.join(" ")}\``,
|
|
1884
1544
|
);
|
|
1885
1545
|
}
|
|
1886
1546
|
const policyName = positional[0];
|
|
@@ -1892,6 +1552,9 @@ async function runCli() {
|
|
|
1892
1552
|
);
|
|
1893
1553
|
|
|
1894
1554
|
lastPolicyAction = action;
|
|
1555
|
+
// `syncAgentSelection` on both: an agent hooked here is traced, and one
|
|
1556
|
+
// unhooked at user scope (`policies remove all`) is not, so the next
|
|
1557
|
+
// `failproofai config` neither undoes nor resurrects what this ran.
|
|
1895
1558
|
if (action === "add") {
|
|
1896
1559
|
const { installHooks } = await import("../src/hooks/manager");
|
|
1897
1560
|
await installHooks(
|
|
@@ -1903,6 +1566,7 @@ async function runCli() {
|
|
|
1903
1566
|
undefined,
|
|
1904
1567
|
false,
|
|
1905
1568
|
cli,
|
|
1569
|
+
{ syncAgentSelection: true },
|
|
1906
1570
|
);
|
|
1907
1571
|
await track("cli_policy_add_success", {
|
|
1908
1572
|
scope,
|
|
@@ -1923,7 +1587,7 @@ async function runCli() {
|
|
|
1923
1587
|
[policyName],
|
|
1924
1588
|
scope,
|
|
1925
1589
|
undefined,
|
|
1926
|
-
{ betaOnly: false, removeCustomHooks: false, cli },
|
|
1590
|
+
{ betaOnly: false, removeCustomHooks: false, cli, syncAgentSelection: true },
|
|
1927
1591
|
);
|
|
1928
1592
|
await track("cli_policy_remove_success", {
|
|
1929
1593
|
scope,
|
|
@@ -1945,63 +1609,7 @@ async function runCli() {
|
|
|
1945
1609
|
const isHelp = subArgs.includes("--help") || subArgs.includes("-h");
|
|
1946
1610
|
|
|
1947
1611
|
if (isHelp) {
|
|
1948
|
-
await printHelp(
|
|
1949
|
-
command: "policies",
|
|
1950
|
-
tagline: "manage the policies your agents run under",
|
|
1951
|
-
sections: [
|
|
1952
|
-
{
|
|
1953
|
-
label: "usage",
|
|
1954
|
-
entries: [
|
|
1955
|
-
["(bare)", "List every policy here and whether it is on"],
|
|
1956
|
-
["add [what]", "Turn one on, take a pack, or pick from a list"],
|
|
1957
|
-
["remove <what>", "Turn one off, or uninstall a pack"],
|
|
1958
|
-
["show <owner>/<repo>", "What a pack holds, before you take it"],
|
|
1959
|
-
["--install, -i", "Wire policies into your agent CLIs"],
|
|
1960
|
-
["--uninstall, -u", "Unwire them, or strip the hooks"],
|
|
1961
|
-
],
|
|
1962
|
-
},
|
|
1963
|
-
{
|
|
1964
|
-
label: "options",
|
|
1965
|
-
entries: [
|
|
1966
|
-
["[names...]", "Policy names. Omit for the interactive picker."],
|
|
1967
|
-
["--cli <agent>...", "Agent CLI(s); space-separated or repeated. Omit to detect what is installed and prompt."],
|
|
1968
|
-
["--scope <scope>", "user, project or local. Default: user. --uninstall also takes all."],
|
|
1969
|
-
["--beta", "Include beta policies. On --uninstall, only those."],
|
|
1970
|
-
["--custom, -c <path>", "Custom policy file; repeat for several. Bare on --uninstall, clears every explicit path."],
|
|
1971
|
-
],
|
|
1972
|
-
},
|
|
1973
|
-
{
|
|
1974
|
-
// Enumerated ONCE. It used to be spelled out in full four times on
|
|
1975
|
-
// this screen — twice in the --cli lines, twice more as examples
|
|
1976
|
-
// differing only in which agent they named.
|
|
1977
|
-
label: "agents",
|
|
1978
|
-
lines: [
|
|
1979
|
-
"claude, codex, copilot, cursor, opencode, pi, hermes, openclaw,",
|
|
1980
|
-
"factory, devin, antigravity, goose",
|
|
1981
|
-
"",
|
|
1982
|
-
"Codex, Copilot, Cursor, OpenCode and Pi take user or project scope only.",
|
|
1983
|
-
],
|
|
1984
|
-
},
|
|
1985
|
-
{
|
|
1986
|
-
label: "examples",
|
|
1987
|
-
lines: [
|
|
1988
|
-
"failproofai policies",
|
|
1989
|
-
"failproofai policies --install",
|
|
1990
|
-
"failproofai policies --install block-sudo sanitize-api-keys",
|
|
1991
|
-
"failproofai policies --install --cli codex --scope project",
|
|
1992
|
-
"failproofai policies --install --cli claude codex copilot cursor",
|
|
1993
|
-
"failproofai policies -i -c ./my-policies.js",
|
|
1994
|
-
"failproofai policies --uninstall block-sudo",
|
|
1995
|
-
"failproofai policies -u",
|
|
1996
|
-
"failproofai policies add FailproofAI/policies --category git,database",
|
|
1997
|
-
],
|
|
1998
|
-
},
|
|
1999
|
-
],
|
|
2000
|
-
footer: [
|
|
2001
|
-
"policy, pack and p are all spellings of policies.",
|
|
2002
|
-
"add, remove and show in full: failproofai policies add --help",
|
|
2003
|
-
],
|
|
2004
|
-
});
|
|
1612
|
+
await printHelp(POLICIES_HELP);
|
|
2005
1613
|
process.exit(0);
|
|
2006
1614
|
}
|
|
2007
1615
|
|
|
@@ -2013,10 +1621,10 @@ async function runCli() {
|
|
|
2013
1621
|
const scopeIdx = subArgs.indexOf("--scope");
|
|
2014
1622
|
const scope = scopeIdx >= 0 ? subArgs[scopeIdx + 1] : "user";
|
|
2015
1623
|
if (scopeIdx >= 0 && (!scope || scope.startsWith("-"))) {
|
|
2016
|
-
throw new CliError("Missing value for --scope
|
|
1624
|
+
throw new CliError("Missing value for --scope.\nUse one of: user, project, local");
|
|
2017
1625
|
}
|
|
2018
1626
|
if (scopeIdx >= 0 && !["user", "project", "local"].includes(scope)) {
|
|
2019
|
-
throw new CliError(`Invalid scope: ${scope}
|
|
1627
|
+
throw new CliError(`Invalid scope: ${scope}.\nUse one of: user, project, local`);
|
|
2020
1628
|
}
|
|
2021
1629
|
|
|
2022
1630
|
const customIdxs = subArgs
|
|
@@ -2024,33 +1632,16 @@ async function runCli() {
|
|
|
2024
1632
|
.filter((index) => index >= 0);
|
|
2025
1633
|
const customPoliciesPaths = customIdxs.map((index) => subArgs[index + 1]);
|
|
2026
1634
|
if (customPoliciesPaths.some((path) => !path || path.startsWith("-"))) {
|
|
2027
|
-
throw new CliError("Missing path after --custom/-c
|
|
1635
|
+
throw new CliError("Missing path after --custom/-c.\nFor example: `failproofai policies -i -c ./my-policies.js`");
|
|
2028
1636
|
}
|
|
2029
1637
|
|
|
2030
1638
|
// --cli accepts one or more space-separated values, optionally repeated:
|
|
2031
1639
|
// --cli claude codex copilot
|
|
2032
1640
|
// --cli claude --cli codex
|
|
2033
|
-
// Values are consumed greedily until the next flag or
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
const cliConsumedIdxs =
|
|
2037
|
-
const cliFlagIdxs = subArgs.map((a, i) => (a === "--cli" ? i : -1)).filter((i) => i >= 0);
|
|
2038
|
-
for (const idx of cliFlagIdxs) {
|
|
2039
|
-
let consumed = 0;
|
|
2040
|
-
for (let j = idx + 1; j < subArgs.length; j++) {
|
|
2041
|
-
const v = subArgs[j];
|
|
2042
|
-
if (v.startsWith("-")) break;
|
|
2043
|
-
// Stop at the first non-CLI token so a policy name following --cli
|
|
2044
|
-
// (e.g. `--cli claude block-sudo`) is not mis-consumed as a CLI.
|
|
2045
|
-
if (!VALID_CLIS.has(v)) break;
|
|
2046
|
-
cliFlagValues.push(v);
|
|
2047
|
-
cliConsumedIdxs.add(j);
|
|
2048
|
-
consumed++;
|
|
2049
|
-
}
|
|
2050
|
-
if (consumed === 0) {
|
|
2051
|
-
throw new CliError("Missing value(s) for --cli. Usage: --cli claude codex copilot cursor opencode pi hermes openclaw (or any subset)");
|
|
2052
|
-
}
|
|
2053
|
-
}
|
|
1641
|
+
// Values are consumed greedily until the next flag, or the first word that
|
|
1642
|
+
// is not an agent so a policy name following --cli (e.g. `--cli claude
|
|
1643
|
+
// block-sudo`) is not mis-consumed as one.
|
|
1644
|
+
const { cliFlagValues, cliConsumedIdxs } = await readCliFlag(subArgs);
|
|
2054
1645
|
|
|
2055
1646
|
const includeBeta = subArgs.includes("--beta");
|
|
2056
1647
|
|
|
@@ -2064,7 +1655,7 @@ async function runCli() {
|
|
|
2064
1655
|
const flags = new Set(["--install", "-i", "--scope", "--beta", "--custom", "-c", "--cli"]);
|
|
2065
1656
|
const unknownInstallFlag = subArgs.find((a) => a.startsWith("-") && !flags.has(a));
|
|
2066
1657
|
if (unknownInstallFlag) {
|
|
2067
|
-
throw new CliError(`Unknown flag: ${unknownInstallFlag}
|
|
1658
|
+
throw new CliError(`Unknown flag: ${unknownInstallFlag}.\nRun \`failproofai policies --help\` for usage.`);
|
|
2068
1659
|
}
|
|
2069
1660
|
|
|
2070
1661
|
const explicitPolicyNames = subArgs.filter(
|
|
@@ -2084,6 +1675,8 @@ async function runCli() {
|
|
|
2084
1675
|
"install",
|
|
2085
1676
|
);
|
|
2086
1677
|
|
|
1678
|
+
// An agent hooked here is traced from now on, so the next
|
|
1679
|
+
// `failproofai config` keeps these hooks instead of taking them out.
|
|
2087
1680
|
await installHooks(
|
|
2088
1681
|
policyNames,
|
|
2089
1682
|
scope,
|
|
@@ -2093,6 +1686,7 @@ async function runCli() {
|
|
|
2093
1686
|
customPoliciesPaths.length > 0 ? customPoliciesPaths : undefined,
|
|
2094
1687
|
false,
|
|
2095
1688
|
cli,
|
|
1689
|
+
{ syncAgentSelection: true },
|
|
2096
1690
|
);
|
|
2097
1691
|
await track("cli_install_success", {
|
|
2098
1692
|
scope,
|
|
@@ -2113,33 +1707,14 @@ async function runCli() {
|
|
|
2113
1707
|
const scopeIdx = subArgs.indexOf("--scope");
|
|
2114
1708
|
const scope = scopeIdx >= 0 ? subArgs[scopeIdx + 1] : "user";
|
|
2115
1709
|
if (scopeIdx >= 0 && (!scope || scope.startsWith("-"))) {
|
|
2116
|
-
throw new CliError("Missing value for --scope
|
|
1710
|
+
throw new CliError("Missing value for --scope.\nUse one of: user, project, local, all");
|
|
2117
1711
|
}
|
|
2118
1712
|
if (scopeIdx >= 0 && !["user", "project", "local", "all"].includes(scope)) {
|
|
2119
|
-
throw new CliError(`Invalid scope: ${scope}
|
|
1713
|
+
throw new CliError(`Invalid scope: ${scope}.\nUse one of: user, project, local, all`);
|
|
2120
1714
|
}
|
|
2121
1715
|
|
|
2122
1716
|
// --cli accepts one or more space-separated values; same parser as install.
|
|
2123
|
-
const
|
|
2124
|
-
const cliFlagValues = [];
|
|
2125
|
-
const cliConsumedIdxs = new Set();
|
|
2126
|
-
const cliFlagIdxs = subArgs.map((a, i) => (a === "--cli" ? i : -1)).filter((i) => i >= 0);
|
|
2127
|
-
for (const idx of cliFlagIdxs) {
|
|
2128
|
-
let consumed = 0;
|
|
2129
|
-
for (let j = idx + 1; j < subArgs.length; j++) {
|
|
2130
|
-
const v = subArgs[j];
|
|
2131
|
-
if (v.startsWith("-")) break;
|
|
2132
|
-
// Stop at the first non-CLI token so a policy name following --cli
|
|
2133
|
-
// (e.g. `--cli claude block-sudo`) is not mis-consumed as a CLI.
|
|
2134
|
-
if (!VALID_CLIS.has(v)) break;
|
|
2135
|
-
cliFlagValues.push(v);
|
|
2136
|
-
cliConsumedIdxs.add(j);
|
|
2137
|
-
consumed++;
|
|
2138
|
-
}
|
|
2139
|
-
if (consumed === 0) {
|
|
2140
|
-
throw new CliError("Missing value(s) for --cli. Usage: --cli claude codex copilot cursor opencode pi hermes openclaw (or any subset)");
|
|
2141
|
-
}
|
|
2142
|
-
}
|
|
1717
|
+
const { cliFlagValues, cliConsumedIdxs } = await readCliFlag(subArgs);
|
|
2143
1718
|
|
|
2144
1719
|
const betaOnly = subArgs.includes("--beta");
|
|
2145
1720
|
const removeCustomHooks = subArgs.includes("--custom") || subArgs.includes("-c");
|
|
@@ -2150,7 +1725,7 @@ async function runCli() {
|
|
|
2150
1725
|
const flags = new Set(["--uninstall", "-u", "--scope", "--beta", "--custom", "-c", "--cli"]);
|
|
2151
1726
|
const unknownUninstallFlag = subArgs.find((a) => a.startsWith("-") && !flags.has(a));
|
|
2152
1727
|
if (unknownUninstallFlag) {
|
|
2153
|
-
throw new CliError(`Unknown flag: ${unknownUninstallFlag}
|
|
1728
|
+
throw new CliError(`Unknown flag: ${unknownUninstallFlag}.\nRun \`failproofai policies --help\` for usage.`);
|
|
2154
1729
|
}
|
|
2155
1730
|
|
|
2156
1731
|
const policyNames = subArgs.filter(
|
|
@@ -2162,11 +1737,13 @@ async function runCli() {
|
|
|
2162
1737
|
"uninstall",
|
|
2163
1738
|
);
|
|
2164
1739
|
|
|
1740
|
+
// Hooks taken out at user scope or every scope stop the agent being
|
|
1741
|
+
// traced; a project- or local-only removal leaves the selection alone.
|
|
2165
1742
|
await removeHooks(
|
|
2166
1743
|
policyNames.length > 0 ? policyNames : undefined,
|
|
2167
1744
|
scope,
|
|
2168
1745
|
undefined,
|
|
2169
|
-
{ betaOnly, removeCustomHooks, cli },
|
|
1746
|
+
{ betaOnly, removeCustomHooks, cli, syncAgentSelection: true },
|
|
2170
1747
|
);
|
|
2171
1748
|
await track("cli_uninstall_success", {
|
|
2172
1749
|
scope,
|
|
@@ -2181,26 +1758,27 @@ async function runCli() {
|
|
|
2181
1758
|
|
|
2182
1759
|
// Default: list policies
|
|
2183
1760
|
// Accept --list as a no-op alias (common intuition), reject all other unknown flags
|
|
2184
|
-
// and unexpected positional args (e.g. "hi").
|
|
2185
|
-
|
|
1761
|
+
// and unexpected positional args (e.g. "hi"). --all lists every policy instead of
|
|
1762
|
+
// folding the off ones into one line per category.
|
|
1763
|
+
const knownListFlags = new Set(["--install", "-i", "--uninstall", "-u", "--help", "-h", "--list", "--all"]);
|
|
2186
1764
|
const unknownListArg = subArgs.find((a) => a.startsWith("-") && !knownListFlags.has(a));
|
|
2187
1765
|
if (unknownListArg) {
|
|
2188
1766
|
throw new CliError(
|
|
2189
|
-
`Unknown flag: ${unknownListArg}
|
|
2190
|
-
`
|
|
1767
|
+
`Unknown flag: ${unknownListArg}.\n` +
|
|
1768
|
+
"Run `failproofai policies --help` for usage."
|
|
2191
1769
|
);
|
|
2192
1770
|
}
|
|
2193
1771
|
const positionalArgs = subArgs.filter((a) => !a.startsWith("-"));
|
|
2194
1772
|
if (positionalArgs.length > 0) {
|
|
2195
1773
|
throw new CliError(
|
|
2196
|
-
`Unexpected argument: ${positionalArgs[0]}
|
|
2197
|
-
`
|
|
1774
|
+
`Unexpected argument: ${positionalArgs[0]}.\n` +
|
|
1775
|
+
"Run `failproofai policies --help` for usage."
|
|
2198
1776
|
);
|
|
2199
1777
|
}
|
|
2200
1778
|
|
|
2201
1779
|
lastSubcommand = "list";
|
|
2202
1780
|
const { listHooks } = await import("../src/hooks/manager");
|
|
2203
|
-
await listHooks();
|
|
1781
|
+
await listHooks(undefined, { all: subArgs.includes("--all") });
|
|
2204
1782
|
await track("cli_list_invoked", {});
|
|
2205
1783
|
process.exit(0);
|
|
2206
1784
|
}
|
|
@@ -2224,103 +1802,72 @@ async function runCli() {
|
|
|
2224
1802
|
if (args[0] === "config") {
|
|
2225
1803
|
if (args.includes("--help") || args.includes("-h")) {
|
|
2226
1804
|
await printHelp({
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
],
|
|
2251
|
-
},
|
|
2252
|
-
{
|
|
2253
|
-
label: "with no terminal (CI, containers, an agent driving it)",
|
|
2254
|
-
lines: [
|
|
2255
|
-
"It just runs. There is nothing to confirm when nobody is watching, so it",
|
|
2256
|
-
"applies rather than asking — no flag needed.",
|
|
2257
|
-
"",
|
|
2258
|
-
"Exit 1 if anything it was asked to do did not happen — including a key",
|
|
2259
|
-
"the server refused, and a machine that could not reach root. sudo is",
|
|
2260
|
-
"never prompted for here; it either works without a password, or you are",
|
|
2261
|
-
"told the exact commands to run.",
|
|
2262
|
-
],
|
|
2263
|
-
},
|
|
2264
|
-
{
|
|
2265
|
-
label: "failproof cloud",
|
|
2266
|
-
entries: [
|
|
2267
|
-
["--token <key>", "The API key. Passing one IS the request to connect."],
|
|
2268
|
-
["--url <url>", "Somewhere other than app.befailproof.ai"],
|
|
2269
|
-
["--machine-id <id>", "Defaults to a stable per-machine key"],
|
|
2270
|
-
["--machine-label <n>", "Dashboard name. Alone, renames a connected machine."],
|
|
2271
|
-
["--no-transcripts", "Decisions only, no session transcripts"],
|
|
2272
|
-
["--connect <url>", "Enrol only, on a machine already set up"],
|
|
2273
|
-
["--disconnect", "Stop pulling policy and sending activity"],
|
|
2274
|
-
],
|
|
2275
|
-
},
|
|
2276
|
-
{
|
|
2277
|
-
label: "what connecting means",
|
|
2278
|
-
lines: [
|
|
2279
|
-
"The key can come from FAILPROOFAI_CLOUD_TOKEN instead of the command",
|
|
2280
|
-
"line, and the url from FAILPROOFAI_CLOUD_URL — the same variable the",
|
|
2281
|
-
"daemon reads. Prefer the environment: an argument is readable from `ps`",
|
|
2282
|
-
"by every user on the box, and lands in shell history and CI logs.",
|
|
2283
|
-
"",
|
|
2284
|
-
"One connection, two capabilities: this machine PULLS centrally-managed",
|
|
2285
|
-
"policies and SENDS what its hooks decided. Both are checked against the",
|
|
2286
|
-
"server before anything is written, and reported separately — a key",
|
|
2287
|
-
"carrying policies:pull but not events:add connects for policy and says",
|
|
2288
|
-
"exactly why the dashboard is empty.",
|
|
2289
|
-
"",
|
|
2290
|
-
"Connecting sends BOTH policy decisions and full session transcripts. A",
|
|
2291
|
-
"transcript carries prompts, file contents and whatever was pasted into a",
|
|
2292
|
-
"terminal — that is the point of connecting, and it is stated here rather",
|
|
2293
|
-
"than buried behind a flag nobody finds. Use --no-transcripts for",
|
|
2294
|
-
"decisions only.",
|
|
2295
|
-
"",
|
|
2296
|
-
"Tokens are stored owner-only in ~/.failproofai/, never in the service",
|
|
2297
|
-
"unit — that file is world-readable. Connecting needs no sudo.",
|
|
2298
|
-
],
|
|
2299
|
-
},
|
|
2300
|
-
{
|
|
2301
|
-
label: "pausing enforcement (one session, always time-boxed)",
|
|
2302
|
-
entries: [
|
|
2303
|
-
["--pause", "This directory's newest agent session, for 30m"],
|
|
2304
|
-
["--pause 10m", "A given time (max 8h; s/m/h, bare = minutes)"],
|
|
2305
|
-
["--resume", "End the pause early"],
|
|
2306
|
-
["--resume --all", "End every active pause"],
|
|
2307
|
-
["--session <id>", "Target a specific session"],
|
|
2308
|
-
["--status", "What is paused, and when it lifts"],
|
|
2309
|
-
],
|
|
2310
|
-
after: [
|
|
2311
|
-
"A pause suspends builtin, custom and convention policies for that",
|
|
2312
|
-
"session only, and always expires on its own. Cloud-managed policies",
|
|
2313
|
-
"keep enforcing.",
|
|
2314
|
-
],
|
|
2315
|
-
},
|
|
1805
|
+
name: "config",
|
|
1806
|
+
usage: [["failproofai config [options]"]],
|
|
1807
|
+
options: [
|
|
1808
|
+
["--token <key>", "Connect with this key; prefer FAILPROOFAI_CLOUD_TOKEN"],
|
|
1809
|
+
["--oss", "Use open source; leaves any connection as it is"],
|
|
1810
|
+
["--agents <a,b>", "Trace these agents, or all, without asking"],
|
|
1811
|
+
["--status", "Show the cloud connection, daemon version and pauses"],
|
|
1812
|
+
["--pause [time]", "Pause enforcement for a session (default 30m, max 8h)"],
|
|
1813
|
+
["--resume [--all]", "End a pause early, or every pause with --all"],
|
|
1814
|
+
["--session <id>", "Session to pause or resume (default: newest one here)"],
|
|
1815
|
+
["--connect <url>", "Connect to cloud only (for a machine already set up)"],
|
|
1816
|
+
["--disconnect", "Stop pulling policies and sending activity"],
|
|
1817
|
+
["--no-transcripts", "Send policy decisions only, not session transcripts"],
|
|
1818
|
+
["--url <url>", "Use a self-hosted cloud (or FAILPROOFAI_CLOUD_URL)"],
|
|
1819
|
+
["--machine-label <name>", "Rename this machine; with --token, set up under it"],
|
|
1820
|
+
["--machine-id <id>", "Machine id for --connect (default: a stable id)"],
|
|
1821
|
+
],
|
|
1822
|
+
// The key never goes on the command line: config itself warns that argv
|
|
1823
|
+
// lands in shell history and the process list.
|
|
1824
|
+
examples: [
|
|
1825
|
+
"read -rs FAILPROOFAI_CLOUD_TOKEN && export FAILPROOFAI_CLOUD_TOKEN",
|
|
1826
|
+
"failproofai config",
|
|
1827
|
+
"failproofai config --pause 1h",
|
|
2316
1828
|
],
|
|
2317
|
-
|
|
1829
|
+
optionsCol: 22,
|
|
2318
1830
|
});
|
|
2319
1831
|
process.exit(0);
|
|
2320
1832
|
process.exit(0);
|
|
2321
1833
|
}
|
|
2322
1834
|
lastSubcommand = "config";
|
|
2323
1835
|
|
|
1836
|
+
// Every option config takes. A typo used to fall through to the full setup
|
|
1837
|
+
// run with the option ignored, so `--agent claude` asked every question.
|
|
1838
|
+
//
|
|
1839
|
+
// `--send-transcripts` is retired, not unknown: it was the transcript
|
|
1840
|
+
// opt-in until transcripts became the default (#653), and provisioning
|
|
1841
|
+
// scripts written then still pass it. Accepted, and does nothing.
|
|
1842
|
+
const CONFIG_FLAGS = new Set([
|
|
1843
|
+
"--token", "--oss", "--agents", "--status", "--pause", "--resume", "--all", "--session",
|
|
1844
|
+
"--connect", "--disconnect", "--no-transcripts", "--url", "--machine-label", "--machine-id",
|
|
1845
|
+
"--send-transcripts",
|
|
1846
|
+
]);
|
|
1847
|
+
const CONFIG_VALUE_FLAGS = new Set([
|
|
1848
|
+
"--token", "--agents", "--pause", "--session", "--connect", "--url", "--machine-label", "--machine-id",
|
|
1849
|
+
]);
|
|
1850
|
+
const configRest = args.slice(1);
|
|
1851
|
+
const strayConfig = configRest.find(
|
|
1852
|
+
(a, i) => !CONFIG_FLAGS.has(a) && !(CONFIG_VALUE_FLAGS.has(configRest[i - 1]) && !a.startsWith("-")),
|
|
1853
|
+
);
|
|
1854
|
+
if (strayConfig) {
|
|
1855
|
+
const asFlag = `--${strayConfig.replace(/^-+/, "")}`;
|
|
1856
|
+
throw new CliError(
|
|
1857
|
+
`Unexpected argument: ${strayConfig}.\n` +
|
|
1858
|
+
(CONFIG_FLAGS.has(asFlag) && asFlag !== strayConfig
|
|
1859
|
+
? `Did you mean \`failproofai config ${asFlag}\`?`
|
|
1860
|
+
: "See its options: `failproofai config --help`"),
|
|
1861
|
+
);
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
if (args.includes("--send-transcripts") && args.includes("--no-transcripts")) {
|
|
1865
|
+
throw new CliError(
|
|
1866
|
+
"--send-transcripts and --no-transcripts cannot be combined.\n" +
|
|
1867
|
+
"Transcripts are sent by default; --no-transcripts alone turns them off.",
|
|
1868
|
+
);
|
|
1869
|
+
}
|
|
1870
|
+
|
|
2324
1871
|
// --pause / --resume / --status are non-interactive session actions that
|
|
2325
1872
|
// share `config`'s surface but not the wizard. They write session state,
|
|
2326
1873
|
// never the config file — a pause that reached policies-config.json would
|
|
@@ -2336,14 +1883,33 @@ async function runCli() {
|
|
|
2336
1883
|
// keeps its old meaning (the name to enrol under); on its own it changes the
|
|
2337
1884
|
// name of a machine that is already connected, which previously required
|
|
2338
1885
|
// re-running enrolment with the url and token again just to fix a display name.
|
|
1886
|
+
//
|
|
1887
|
+
// Alongside a setup option (--token, --oss, --agents) it is the name to set up
|
|
1888
|
+
// under: `config --token K --machine-label L` used to run as a rename, so
|
|
1889
|
+
// setup never ran and the key was ignored. Setup applies it when it connects;
|
|
1890
|
+
// otherwise it is refused up front or applied as a rename afterwards (below).
|
|
2339
1891
|
const wantsRename =
|
|
2340
|
-
connectIdx < 0 &&
|
|
1892
|
+
connectIdx < 0 &&
|
|
1893
|
+
!wantsDisconnect &&
|
|
1894
|
+
args.includes("--machine-label") &&
|
|
1895
|
+
!["--token", "--oss", "--agents"].some((flag) => args.includes(flag));
|
|
2341
1896
|
// A key given as --token is in shell history whatever became of the run.
|
|
2342
1897
|
const warnTokenOnArgv = async () => {
|
|
2343
1898
|
if (!args.includes("--token")) return;
|
|
2344
1899
|
const { CONFIG_TOKEN_HISTORY_WARNING } = await import("../src/hooks/cloud-enrollment-cli");
|
|
2345
|
-
|
|
1900
|
+
const { screenKit, colorsEnabled } = await import("../src/hooks/tui");
|
|
1901
|
+
const kit = screenKit({ color: colorsEnabled(process.stderr) });
|
|
1902
|
+
process.stderr.write(`\n${kit.notice("caution", CONFIG_TOKEN_HISTORY_WARNING.join("\n")).join("\n")}\n`);
|
|
2346
1903
|
};
|
|
1904
|
+
// The fix for a flag given no value. Never "put the key after --token": the
|
|
1905
|
+
// whole point of the variable is that a key on argv lands in shell history.
|
|
1906
|
+
const missingValue = (flag) =>
|
|
1907
|
+
new CliError(
|
|
1908
|
+
`Missing value after ${flag}.\n` +
|
|
1909
|
+
(flag === "--token"
|
|
1910
|
+
? "Or leave --token out and set FAILPROOFAI_CLOUD_TOKEN."
|
|
1911
|
+
: "Run `failproofai config --help` for what it takes."),
|
|
1912
|
+
);
|
|
2347
1913
|
if (connectIdx >= 0 || wantsDisconnect || wantsRename) {
|
|
2348
1914
|
if (connectIdx >= 0 && wantsDisconnect) {
|
|
2349
1915
|
throw new CliError("--connect and --disconnect cannot be combined.");
|
|
@@ -2352,7 +1918,7 @@ async function runCli() {
|
|
|
2352
1918
|
const i = args.indexOf(flag);
|
|
2353
1919
|
if (i < 0) return undefined;
|
|
2354
1920
|
const v = args[i + 1];
|
|
2355
|
-
if (!v || v.startsWith("-")) throw
|
|
1921
|
+
if (!v || v.startsWith("-")) throw missingValue(flag);
|
|
2356
1922
|
return v;
|
|
2357
1923
|
};
|
|
2358
1924
|
let result;
|
|
@@ -2382,7 +1948,11 @@ async function runCli() {
|
|
|
2382
1948
|
sessions: !args.includes("--no-transcripts"),
|
|
2383
1949
|
});
|
|
2384
1950
|
}
|
|
2385
|
-
|
|
1951
|
+
// Through the shared presenter: a header naming what ran, and a failure
|
|
1952
|
+
// opening with ✕ and its fix under it, like every other action result.
|
|
1953
|
+
await printReport(wantsRename ? "rename" : wantsDisconnect ? "disconnect" : "connect", result.lines, {
|
|
1954
|
+
ok: result.exitCode === 0,
|
|
1955
|
+
});
|
|
2386
1956
|
await warnTokenOnArgv();
|
|
2387
1957
|
await track("cli_cloud_enrollment", {
|
|
2388
1958
|
action: wantsRename ? "rename" : wantsDisconnect ? "disconnect" : "connect",
|
|
@@ -2398,11 +1968,22 @@ async function runCli() {
|
|
|
2398
1968
|
if (pauseIdx >= 0 || wantsResume || wantsStatus) {
|
|
2399
1969
|
const chosen = [pauseIdx >= 0 && "--pause", wantsResume && "--resume", wantsStatus && "--status"].filter(Boolean);
|
|
2400
1970
|
if (chosen.length > 1) {
|
|
2401
|
-
throw new CliError(`${chosen.join(" and ")} cannot be combined.`);
|
|
1971
|
+
throw new CliError(`${chosen.join(" and ")} cannot be combined.\nRun them one at a time.`);
|
|
1972
|
+
}
|
|
1973
|
+
// `--status` answers "what is this machine's state?": the daemon, the
|
|
1974
|
+
// dashboard and any pauses, the cloud connection and whether data
|
|
1975
|
+
// arrives, and what enforcement is doing — one screen, one column.
|
|
1976
|
+
if (wantsStatus) {
|
|
1977
|
+
const { runStatusCommand } = await import("../src/hooks/config-status");
|
|
1978
|
+
const status = await runStatusCommand({ cwd: process.cwd() });
|
|
1979
|
+
await printLines(status.lines);
|
|
1980
|
+
await track("cli_pause_invoked", { action: "status", ok: true, affected: status.paused });
|
|
1981
|
+
await exitAfterFlush(0);
|
|
1982
|
+
return;
|
|
2402
1983
|
}
|
|
2403
1984
|
const sessionIdx = args.indexOf("--session");
|
|
2404
1985
|
if (sessionIdx >= 0 && !args[sessionIdx + 1]) {
|
|
2405
|
-
throw new CliError("Missing session id after --session.");
|
|
1986
|
+
throw new CliError("Missing session id after --session.\nOr leave --session out to use the newest session here.");
|
|
2406
1987
|
}
|
|
2407
1988
|
// A bare `--pause` takes the default duration, so only treat the next
|
|
2408
1989
|
// token as a duration when it isn't another flag.
|
|
@@ -2411,55 +1992,15 @@ async function runCli() {
|
|
|
2411
1992
|
|
|
2412
1993
|
const { runPauseCommand } = await import("../src/hooks/session-pause-cli");
|
|
2413
1994
|
const result = runPauseCommand({
|
|
2414
|
-
action: pauseIdx >= 0 ? "pause" :
|
|
1995
|
+
action: pauseIdx >= 0 ? "pause" : "resume",
|
|
2415
1996
|
duration,
|
|
2416
1997
|
sessionId: sessionIdx >= 0 ? args[sessionIdx + 1] : undefined,
|
|
2417
1998
|
all: args.includes("--all"),
|
|
2418
1999
|
cwd: process.cwd(),
|
|
2419
2000
|
});
|
|
2420
|
-
|
|
2421
|
-
// halves: whether enforcement is paused AND whether cloud is connected.
|
|
2422
|
-
if (wantsStatus) {
|
|
2423
|
-
const { connectionStatusReport, versionStatusLines } = await import(
|
|
2424
|
-
"../src/hooks/cloud-enrollment-cli"
|
|
2425
|
-
);
|
|
2426
|
-
const { optsFor, printBlock, rows, stack, title, warning } = await import(
|
|
2427
|
-
"../src/hooks/tui"
|
|
2428
|
-
);
|
|
2429
|
-
const opts = optsFor(process.stdout);
|
|
2430
|
-
const report = connectionStatusReport();
|
|
2431
|
-
const { hermesProfileStatusRows } = await import("../src/hooks/integrations");
|
|
2432
|
-
const hermesRows = hermesProfileStatusRows();
|
|
2433
|
-
// The version line was written to be "the only place a user can find out
|
|
2434
|
-
// which daemon they are running" and then never called from anywhere. It
|
|
2435
|
-
// is the right thing for the heading to carry, and it retires a heading
|
|
2436
|
-
// that would otherwise have said the word "status" back to someone who
|
|
2437
|
-
// just typed it.
|
|
2438
|
-
printBlock(
|
|
2439
|
-
process.stdout,
|
|
2440
|
-
stack(
|
|
2441
|
-
title("failproofai config", versionStatusLines()[0], opts),
|
|
2442
|
-
// ONE rows() call over both blocks, so the connection facts and the
|
|
2443
|
-
// enforcement state share a label column. Rendered separately they
|
|
2444
|
-
// computed one column each and the window read as two commands'
|
|
2445
|
-
// output stacked up.
|
|
2446
|
-
//
|
|
2447
|
-
// Always printed, including where reports can never work: "why am I
|
|
2448
|
-
// not getting them?" is the question --status exists to answer, and
|
|
2449
|
-
// an omitted line answers it with silence.
|
|
2450
|
-
rows([...report.rows, ...hermesRows, ...(result.rows ?? [])], opts),
|
|
2451
|
-
report.warnings.length > 0 ? warning(report.warnings, opts) : null,
|
|
2452
|
-
// The trailer is not rows — it is the note and the resume command.
|
|
2453
|
-
// Rendering only `rows` dropped the one line that tells a paused
|
|
2454
|
-
// user how to get unpaused.
|
|
2455
|
-
result.rows ? (result.trailer ?? null) : result.lines,
|
|
2456
|
-
),
|
|
2457
|
-
);
|
|
2458
|
-
} else {
|
|
2459
|
-
await printLines(result.lines, result.exitCode === 0);
|
|
2460
|
-
}
|
|
2001
|
+
await printLines(result.lines, result.exitCode === 0);
|
|
2461
2002
|
await track("cli_pause_invoked", {
|
|
2462
|
-
action: pauseIdx >= 0 ? "pause" :
|
|
2003
|
+
action: pauseIdx >= 0 ? "pause" : "resume",
|
|
2463
2004
|
ok: result.exitCode === 0,
|
|
2464
2005
|
affected: result.affected,
|
|
2465
2006
|
});
|
|
@@ -2480,20 +2021,66 @@ async function runCli() {
|
|
|
2480
2021
|
const i = args.indexOf(flag);
|
|
2481
2022
|
if (i < 0) return undefined;
|
|
2482
2023
|
const v = args[i + 1];
|
|
2483
|
-
if (!v || v.startsWith("-")) throw
|
|
2024
|
+
if (!v || v.startsWith("-")) throw missingValue(flag);
|
|
2484
2025
|
return v;
|
|
2485
2026
|
};
|
|
2027
|
+
// --oss skips the connect step and leaves any connection as it is (Tab's
|
|
2028
|
+
// meaning, decision D4); --disconnect is what removes one. It overrides a
|
|
2029
|
+
// key in the environment, and only an explicit --token contradicts it.
|
|
2030
|
+
const oss = args.includes("--oss");
|
|
2031
|
+
if (oss && args.includes("--token")) {
|
|
2032
|
+
throw new CliError("--oss and --token cannot be combined: one uses open source, the other connects.");
|
|
2033
|
+
}
|
|
2034
|
+
// --agents: any of the twelve, or `all`, validated here so a typo is an
|
|
2035
|
+
// error rather than an agent silently left untraced.
|
|
2036
|
+
let agents;
|
|
2037
|
+
if (args.includes("--agents")) {
|
|
2038
|
+
const raw = valueFor("--agents");
|
|
2039
|
+
const { INTEGRATION_TYPES } = await import("../src/hooks/types");
|
|
2040
|
+
const ids = raw === "all" ? [...INTEGRATION_TYPES] : raw.split(",").map((a) => a.trim()).filter(Boolean);
|
|
2041
|
+
const unknown = ids.filter((id) => !INTEGRATION_TYPES.includes(id));
|
|
2042
|
+
if (ids.length === 0 || unknown.length > 0) {
|
|
2043
|
+
throw new CliError(`Not an agent: ${unknown.join(", ") || raw}.\nAgents: ${INTEGRATION_TYPES.join(", ")}, or all`);
|
|
2044
|
+
}
|
|
2045
|
+
agents = ids;
|
|
2046
|
+
}
|
|
2047
|
+
// `--machine-label` here is the name to connect under. When this run cannot
|
|
2048
|
+
// connect at all it is refused now, before anything is written; on a
|
|
2049
|
+
// machine that stays connected as it was, it is applied after setup.
|
|
2050
|
+
const machineLabel = valueFor("--machine-label");
|
|
2051
|
+
if (machineLabel !== undefined) {
|
|
2052
|
+
const { machineLabelRefusal } = await import("../src/hooks/cloud-enrollment-cli");
|
|
2053
|
+
const refusal = machineLabelRefusal({
|
|
2054
|
+
oss,
|
|
2055
|
+
keyGiven: !oss && Boolean(valueFor("--token") ?? process.env.FAILPROOFAI_CLOUD_TOKEN),
|
|
2056
|
+
interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
2057
|
+
});
|
|
2058
|
+
if (refusal) throw new CliError(refusal);
|
|
2059
|
+
}
|
|
2486
2060
|
const { runConfigureWizard } = await import("../src/hooks/configure-wizard");
|
|
2487
2061
|
const result = await runConfigureWizard(
|
|
2488
2062
|
{},
|
|
2489
2063
|
{
|
|
2490
|
-
token: valueFor("--token") ?? process.env.FAILPROOFAI_CLOUD_TOKEN,
|
|
2064
|
+
token: oss ? undefined : (valueFor("--token") ?? process.env.FAILPROOFAI_CLOUD_TOKEN),
|
|
2491
2065
|
url: valueFor("--url") ?? process.env.FAILPROOFAI_CLOUD_URL,
|
|
2492
2066
|
machineId: valueFor("--machine-id"),
|
|
2493
|
-
machineLabel
|
|
2067
|
+
machineLabel,
|
|
2494
2068
|
noTranscripts: args.includes("--no-transcripts"),
|
|
2069
|
+
oss,
|
|
2070
|
+
agents,
|
|
2495
2071
|
},
|
|
2496
2072
|
);
|
|
2073
|
+
// Setup names the machine only when it connects; when it left a
|
|
2074
|
+
// connection as it was, the label is the rename the flag asked for.
|
|
2075
|
+
let labelExit = 0;
|
|
2076
|
+
if (result.applied && machineLabel !== undefined) {
|
|
2077
|
+
const { applyMachineLabelAfterSetup } = await import("../src/hooks/cloud-enrollment-cli");
|
|
2078
|
+
const renamed = await applyMachineLabelAfterSetup(machineLabel);
|
|
2079
|
+
if (renamed) {
|
|
2080
|
+
await printReport("rename", renamed.lines, { ok: renamed.exitCode === 0 });
|
|
2081
|
+
labelExit = renamed.exitCode;
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2497
2084
|
await warnTokenOnArgv();
|
|
2498
2085
|
await track("cli_configure_invoked", {
|
|
2499
2086
|
applied: result.applied,
|
|
@@ -2505,74 +2092,50 @@ async function runCli() {
|
|
|
2505
2092
|
scopes: result.scopes ?? [],
|
|
2506
2093
|
cli_count: result.clis?.length ?? 0,
|
|
2507
2094
|
abort: result.abort ?? null,
|
|
2095
|
+
oss,
|
|
2096
|
+
agents_named: agents ? agents.length : 0,
|
|
2508
2097
|
});
|
|
2509
2098
|
// `abort` is the field `WizardAbort` exists to expose, and exiting 0
|
|
2510
2099
|
// regardless discarded it: a fleet script could not tell "the user pressed
|
|
2511
2100
|
// Esc" from "this machine could not install the required daemon and is
|
|
2512
2101
|
// unconfigured". Cancelling is not a failure; the other two are.
|
|
2513
|
-
await exitAfterFlush(!result.applied && result.abort && result.abort !== "cancelled" ? 1 :
|
|
2102
|
+
await exitAfterFlush(!result.applied && result.abort && result.abort !== "cancelled" ? 1 : labelExit);
|
|
2514
2103
|
return;
|
|
2515
2104
|
}
|
|
2516
2105
|
|
|
2517
|
-
//
|
|
2518
|
-
//
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
);
|
|
2524
|
-
for (let i = 1; i <= m; i++)
|
|
2525
|
-
for (let j = 1; j <= n; j++)
|
|
2526
|
-
dp[i][j] = a[i - 1] === b[j - 1]
|
|
2527
|
-
? dp[i - 1][j - 1]
|
|
2528
|
-
: 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
2529
|
-
return dp[m][n];
|
|
2530
|
-
}
|
|
2531
|
-
|
|
2532
|
-
// Unknown flag guard — must appear after all known-flag branches
|
|
2106
|
+
// Unknown flag guard — must appear after all known-flag branches.
|
|
2107
|
+
//
|
|
2108
|
+
// Both guards below suggest a whole command, runnable as printed, or nothing
|
|
2109
|
+
// at all. The nearest-spelling guess they replaced always answered, so
|
|
2110
|
+
// `status` got `flush`, `install` got `uninstall` and `-i` got `audit`; see
|
|
2111
|
+
// did-you-mean.ts for the table of words people type and the cut-off.
|
|
2533
2112
|
const knownFlags = ["--version", "-v", "--help", "-h", "--hook"];
|
|
2534
2113
|
const unknownFlag = args.find(a => a.startsWith("-") && !knownFlags.includes(a));
|
|
2535
2114
|
|
|
2536
2115
|
if (unknownFlag) {
|
|
2537
|
-
const
|
|
2538
|
-
const
|
|
2539
|
-
const dist = levenshtein(unknownFlag, flag);
|
|
2540
|
-
return dist < best.dist ? { flag, dist } : best;
|
|
2541
|
-
}, { flag: primary[0], dist: Infinity });
|
|
2542
|
-
|
|
2116
|
+
const { suggestFlag } = await import("../src/hooks/did-you-mean");
|
|
2117
|
+
const suggestion = suggestFlag(unknownFlag);
|
|
2543
2118
|
throw new CliError(
|
|
2544
|
-
`Unknown flag: ${unknownFlag}
|
|
2545
|
-
`Did you mean:
|
|
2546
|
-
`Run \`failproofai --help\` for usage details.`
|
|
2119
|
+
`Unknown flag: ${unknownFlag}.\n` +
|
|
2120
|
+
(suggestion ? `Did you mean \`${suggestion}\`?` : "Run `failproofai help` for usage.")
|
|
2547
2121
|
);
|
|
2548
2122
|
}
|
|
2549
2123
|
|
|
2550
2124
|
// Unknown subcommand guard (non-flag args that aren't a known subcommand)
|
|
2551
2125
|
const unknownSubcommand = args.find(a => !a.startsWith("-") && !SUBCOMMANDS.includes(a));
|
|
2552
2126
|
if (unknownSubcommand) {
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
// a real subcommand until this release, so an old script or plain muscle
|
|
2556
|
-
// memory lands here, and answering "did you mean policies?" sends someone
|
|
2557
|
-
// to the one command that has nothing to do with what they typed.
|
|
2558
|
-
const nearest = SUBCOMMANDS.reduce(
|
|
2559
|
-
(best, name) => {
|
|
2560
|
-
const dist = levenshtein(unknownSubcommand, name);
|
|
2561
|
-
return dist < best.dist ? { name, dist } : best;
|
|
2562
|
-
},
|
|
2563
|
-
{ name: SUBCOMMANDS[0], dist: Infinity },
|
|
2564
|
-
);
|
|
2127
|
+
const { suggestCommand } = await import("../src/hooks/did-you-mean");
|
|
2128
|
+
const suggestion = suggestCommand(unknownSubcommand, SUBCOMMANDS);
|
|
2565
2129
|
throw new CliError(
|
|
2566
|
-
`
|
|
2567
|
-
`Did you mean: failproofai
|
|
2568
|
-
`Run \`failproofai --help\` for usage details.`
|
|
2130
|
+
`There is no command called '${unknownSubcommand}'.\n` +
|
|
2131
|
+
(suggestion ? `Did you mean \`${suggestion}\`?` : "Run `failproofai help` to see every command.")
|
|
2569
2132
|
);
|
|
2570
2133
|
}
|
|
2571
2134
|
|
|
2572
2135
|
// Dashboard launch — always production mode. Runs on every bare `failproofai`
|
|
2573
2136
|
// (first-run onboarding, if any, already ran above).
|
|
2574
2137
|
const { launch } = await import("../scripts/launch");
|
|
2575
|
-
launch("start");
|
|
2138
|
+
launch("start", { logo: !setupWizardRan });
|
|
2576
2139
|
}
|
|
2577
2140
|
|
|
2578
2141
|
// ── Import CliError for use in the guard above ────────────────────────────────
|
|
@@ -2600,8 +2163,19 @@ try {
|
|
|
2600
2163
|
exit_code: err.exitCode,
|
|
2601
2164
|
});
|
|
2602
2165
|
}
|
|
2603
|
-
|
|
2604
|
-
|
|
2166
|
+
// `✕ <what went wrong>`, then each fix on its own line, indented two, with
|
|
2167
|
+
// the command in pink (see `notice` in tui.ts). The exit code is the error's.
|
|
2168
|
+
// Written rather than `console.error`, which Bun paints red on a terminal —
|
|
2169
|
+
// a colour from outside the palette — and drained before exiting.
|
|
2170
|
+
let failureText = `✕ ${err.message}`;
|
|
2171
|
+
try {
|
|
2172
|
+
const { screenKit, colorsEnabled } = await import("../src/hooks/tui");
|
|
2173
|
+
failureText = screenKit({ color: colorsEnabled(process.stderr) }).notice("fail", err.message).join("\n");
|
|
2174
|
+
} catch {
|
|
2175
|
+
// The plain glyph is still the right shape; the kit is decoration.
|
|
2176
|
+
}
|
|
2177
|
+
process.stderr.write(`${failureText}\n`);
|
|
2178
|
+
await exitAfterFlush(err.exitCode);
|
|
2605
2179
|
}
|
|
2606
2180
|
// Unexpected internal error — show message only, no stack trace
|
|
2607
2181
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -2619,6 +2193,8 @@ try {
|
|
|
2619
2193
|
error_type: err instanceof Error ? err.name : "unknown",
|
|
2620
2194
|
});
|
|
2621
2195
|
}
|
|
2622
|
-
|
|
2623
|
-
|
|
2196
|
+
// Plain, never through the kit: this is the path for an install so broken
|
|
2197
|
+
// that anything could be the next thing to fail.
|
|
2198
|
+
process.stderr.write(`✕ Unexpected error: ${msg}\n`);
|
|
2199
|
+
await exitAfterFlush(2);
|
|
2624
2200
|
}
|