failproofai 0.0.10 → 0.0.11-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +7 -7
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +4 -4
- 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.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
- 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 +4 -4
- package/.next/standalone/.next/server/app/_not-found/page/next-font-manifest.json +1 -1
- 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.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- 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/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +16 -16
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +16 -16
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +10 -10
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/page/build-manifest.json +4 -4
- package/.next/standalone/.next/server/app/page/next-font-manifest.json +1 -1
- 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 +4 -4
- package/.next/standalone/.next/server/app/policies/page/next-font-manifest.json +1 -1
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +8 -8
- package/.next/standalone/.next/server/app/policies/page.js +4 -4
- 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 +4 -4
- package/.next/standalone/.next/server/app/project/[name]/page/next-font-manifest.json +1 -1
- 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 +4 -4
- 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 +4 -4
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/next-font-manifest.json +1 -1
- 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 +4 -4
- 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 +4 -4
- package/.next/standalone/.next/server/app/projects/page/next-font-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js +4 -4
- 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/chunks/[root-of-the-server]__0d_ob4n._.js +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__044xt9.._.js → [root-of-the-server]__0fwb7ao._.js} +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0g48iv.._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0j8-xkl._.js +1 -1
- package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_0bdfoky.js +1 -1
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_05pz9._._.js +1 -1
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_0z7w.hh._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0-wn51s._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01as125._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__098zro9._.js +19 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__02r.cjq._.js → [root-of-the-server]__09v.ljl._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0agrcb8._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0b7hkr~._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ehh6vp._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0g8l0tu._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0j4l6hl._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0ye1w50._.js → [root-of-the-server]__0k5n2kz._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0lp08ll._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0n0yaqw._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0o21f.o._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0t8juvy._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__10xgshr._.js → [root-of-the-server]__0tcyn68._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ts150~._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0podumr._.js → [root-of-the-server]__0uylufv._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ymlddl._.js +5 -5
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0~03grs._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_0cdqd9w._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_0xerkr6._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_0q-m0y-._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/lib_utils_ts_068jk73._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_0ttbz1~._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_06u0kr8._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_0h9llsw._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0a_7sdg.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0ef3uwk.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0j79~gv.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0pbja1x.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0r6o0i2.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_11y81~_.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_12or2kf.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_0mebn66._.js +1 -1
- package/.next/standalone/.next/server/functions-config-manifest.json +2 -2
- package/.next/standalone/.next/server/middleware-build-manifest.js +7 -7
- package/.next/standalone/.next/server/next-font-manifest.js +1 -1
- package/.next/standalone/.next/server/next-font-manifest.json +6 -6
- 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 +9 -9
- package/.next/standalone/.next/static/chunks/07kpqoo7kuckx.js +6 -0
- package/.next/standalone/.next/static/chunks/0a40sy4tk8ioe.js +1 -0
- package/.next/standalone/.next/static/chunks/{12l2t63hkyo2q.js → 0azb~vy9ds_uy.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0j171xiqge4rv.js → 0bke.~atnsbeb.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0lt8ko3lw.5yt.js → 0bv1oyxspkpkb.js} +1 -1
- package/.next/standalone/.next/static/chunks/{179yytvmam0ug.js → 0dvhi-prcsh3~.js} +1 -1
- package/.next/standalone/.next/static/chunks/0f5p9plm.aqlp.css +2 -0
- package/.next/standalone/.next/static/chunks/0ffvlbgzgnlw7.js +2 -0
- package/.next/standalone/.next/static/chunks/{150i0n26fnvso.js → 0n1n67imq.udf.js} +1 -1
- package/.next/standalone/.next/static/chunks/0spktq7xqab9h.js +1 -0
- package/.next/standalone/.next/static/chunks/{14lii11wmo450.js → 118q3uljozd5z.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0pkl..xgo-qox.js → 11w14gnqzprir.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0rnqmir4cd5p9.js → 17mubwtqwijpu.js} +1 -1
- package/.next/standalone/.next/static/chunks/{turbopack-05z7a19q43zfq.js → turbopack-0nh.aopesgj~5.js} +1 -1
- package/.next/standalone/.next/static/media/4fa387ec64143e14-s.0.qu-9752pffj.woff2 +0 -0
- package/.next/standalone/.next/static/media/5ce348bf30bf5439-s.0ee55_hj9qcer.woff2 +0 -0
- package/.next/standalone/.next/static/media/6306c77e7c8268e4-s.0mao5jbfbduzp.woff2 +0 -0
- package/.next/standalone/.next/static/media/797e433ab948586e-s.p.09zddjkbdep5a.woff2 +0 -0
- package/.next/standalone/.next/static/media/7d817b4c03b0c5f1-s.0uzt.a6d44yda.woff2 +0 -0
- package/.next/standalone/.next/static/media/bbc41e54d2fcbd21-s.0mvwgmnhv29no.woff2 +0 -0
- package/.next/standalone/.next/static/{dAuQps6jUwCz9X1Q5FFOO → tGVQM5SE3NvbVu0gbAJm7}/_clientMiddlewareManifest.js +2 -2
- package/.next/standalone/app/policies/hooks-client.tsx +111 -14
- package/.next/standalone/components/navbar.tsx +1 -1
- package/.next/standalone/components/reach-developers.tsx +2 -2
- package/.next/standalone/lib/claude-sessions.ts +181 -0
- package/.next/standalone/node_modules/@next/env/package.json +1 -1
- package/.next/standalone/node_modules/next/dist/build/static-paths/app.js +2 -1
- package/.next/standalone/node_modules/next/dist/build/swc/index.js +1 -1
- package/.next/standalone/node_modules/next/dist/build/utils.js +2 -1
- package/.next/standalone/node_modules/next/dist/client/components/router-reducer/fetch-server-response.js +2 -2
- package/.next/standalone/node_modules/next/dist/client/components/router-reducer/set-cache-busting-search-param.js +8 -2
- package/.next/standalone/node_modules/next/dist/client/route-params.js +23 -6
- package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo-experimental.runtime.prod.js +13 -13
- package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo.runtime.prod.js +11 -11
- package/.next/standalone/node_modules/next/dist/compiled/next-server/app-route-turbo.runtime.prod.js +2 -2
- package/.next/standalone/node_modules/next/dist/compiled/next-server/pages-turbo.runtime.prod.js +10 -10
- package/.next/standalone/node_modules/next/dist/lib/patch-incorrect-lockfile.js +3 -3
- package/.next/standalone/node_modules/next/dist/server/app-render/action-handler.js +3 -6
- package/.next/standalone/node_modules/next/dist/server/app-render/app-render.js +62 -9
- package/.next/standalone/node_modules/next/dist/server/app-render/collect-segment-data.js +16 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/create-component-tree.js +49 -19
- package/.next/standalone/node_modules/next/dist/server/app-render/get-script-nonce-from-header.js +8 -20
- package/.next/standalone/node_modules/next/dist/server/app-render/metadata-insertion/create-server-inserted-metadata.js +8 -7
- package/.next/standalone/node_modules/next/dist/server/app-render/use-flight-response.js +2 -2
- package/.next/standalone/node_modules/next/dist/server/async-storage/work-store.js +2 -1
- package/.next/standalone/node_modules/next/dist/server/base-server.js +13 -5
- 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/dev/static-paths-worker.js +2 -1
- package/.next/standalone/node_modules/next/dist/server/image-optimizer.js +22 -2
- package/.next/standalone/node_modules/next/dist/server/lib/app-info-log.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/lib/is-rsc-request.js +18 -0
- package/.next/standalone/node_modules/next/dist/server/lib/mock-request.js +30 -5
- package/.next/standalone/node_modules/next/dist/server/lib/patch-set-header.js +7 -0
- package/.next/standalone/node_modules/next/dist/server/lib/router-server.js +6 -3
- package/.next/standalone/node_modules/next/dist/server/lib/router-utils/resolve-routes.js +18 -4
- package/.next/standalone/node_modules/next/dist/server/lib/server-ipc/utils.js +3 -1
- package/.next/standalone/node_modules/next/dist/server/lib/start-server.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/next-server.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/request/fallback-params.js +27 -1
- package/.next/standalone/node_modules/next/dist/server/route-modules/app-route/module.js +1 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/route-module.js +11 -1
- package/.next/standalone/node_modules/next/dist/server/server-utils.js +19 -2
- package/.next/standalone/node_modules/next/dist/server/stream-utils/node-web-streams-helper.js +5 -5
- package/.next/standalone/node_modules/next/dist/server/use-cache/use-cache-wrapper.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/web/adapter.js +4 -1
- package/.next/standalone/node_modules/next/dist/server/web/edge-route-module-wrapper.js +2 -1
- package/.next/standalone/node_modules/next/dist/shared/lib/errors/canary-only-config-error.js +1 -1
- package/.next/standalone/node_modules/next/dist/{server → shared/lib}/htmlescape.js +15 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/router/routes/app.js +13 -1
- package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/cache-busting-search-param.js +56 -10
- 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 +15 -15
- package/.next/standalone/node_modules/react/cjs/react.development.js +1 -1
- package/.next/standalone/node_modules/react/cjs/react.production.js +1 -1
- package/.next/standalone/node_modules/react/package.json +1 -1
- package/.next/standalone/node_modules/react-dom/cjs/react-dom-server-legacy.browser.production.js +1 -1
- package/.next/standalone/node_modules/react-dom/cjs/react-dom-server-legacy.node.production.js +1 -1
- package/.next/standalone/node_modules/react-dom/cjs/react-dom-server.browser.production.js +3 -3
- package/.next/standalone/node_modules/react-dom/cjs/react-dom-server.edge.production.js +3 -3
- package/.next/standalone/node_modules/react-dom/cjs/react-dom-server.node.production.js +3 -3
- package/.next/standalone/node_modules/react-dom/cjs/react-dom.production.js +1 -1
- package/.next/standalone/node_modules/react-dom/package.json +2 -2
- package/.next/standalone/package.json +5 -5
- package/.next/standalone/proxy.ts +1 -1
- package/.next/standalone/server.js +1 -1
- package/README.md +4 -4
- package/bin/failproofai.mjs +230 -73
- package/dist/cli.mjs +3028 -1453
- package/lib/claude-sessions.ts +181 -0
- package/package.json +5 -5
- package/scripts/launch.ts +1 -1
- package/scripts/postinstall.mjs +89 -1
- package/src/audit/cache.ts +113 -0
- package/src/audit/cli-adapters/claude.ts +97 -0
- package/src/audit/cli-adapters/codex.ts +56 -0
- package/src/audit/cli-adapters/copilot.ts +51 -0
- package/src/audit/cli-adapters/cursor.ts +51 -0
- package/src/audit/cli-adapters/gemini.ts +51 -0
- package/src/audit/cli-adapters/index.ts +70 -0
- package/src/audit/cli-adapters/opencode.ts +52 -0
- package/src/audit/cli-adapters/pi.ts +51 -0
- package/src/audit/cli-adapters/shared.ts +85 -0
- package/src/audit/detectors/find-from-root.ts +27 -0
- package/src/audit/detectors/git-commit-no-verify.ts +22 -0
- package/src/audit/detectors/index.ts +33 -0
- package/src/audit/detectors/prefer-edit-over-read-cat.ts +31 -0
- package/src/audit/detectors/prefer-edit-over-sed-awk.ts +27 -0
- package/src/audit/detectors/prefer-write-over-heredoc.ts +36 -0
- package/src/audit/detectors/redundant-cd-cwd.ts +28 -0
- package/src/audit/detectors/reread-after-edit.ts +58 -0
- package/src/audit/detectors/sleep-polling-loop.ts +34 -0
- package/src/audit/index.ts +369 -0
- package/src/audit/replay.ts +121 -0
- package/src/audit/report.ts +349 -0
- package/src/audit/telemetry.ts +113 -0
- package/src/audit/types.ts +193 -0
- package/src/hooks/builtin-policies.ts +79 -1
- package/src/hooks/custom-hooks-loader.ts +19 -3
- package/src/hooks/first-run-nudge.ts +146 -0
- package/src/hooks/handler.ts +21 -102
- package/src/hooks/install-prompt.ts +34 -4
- package/src/hooks/manager.ts +72 -5
- package/src/hooks/policy-evaluator.ts +19 -4
- package/src/hooks/policy-registry.ts +1 -1
- package/src/hooks/policy-types.ts +9 -0
- package/src/hooks/tool-name-canonicalize.ts +65 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0609ezh._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__07_-mkc._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__09z7o2x._.js +0 -19
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0_sh2n0._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0e9o9ri._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0l6swv1._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0logebz._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0mi5ejy._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0odijkc._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0rkxer-._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0rl2kwi._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0vg0uey._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0x5limi._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__10._f0s._.js +0 -4
- package/.next/standalone/.next/static/chunks/01q52wg_amm60.js +0 -2
- package/.next/standalone/.next/static/chunks/0kqar56yl~41o.js +0 -6
- package/.next/standalone/.next/static/chunks/0ml1.ck_5t36i.js +0 -1
- package/.next/standalone/.next/static/chunks/0zig0fh30t6ou.js +0 -1
- package/.next/standalone/.next/static/chunks/17rm86uz2nd5a.css +0 -2
- package/.next/standalone/.next/static/media/4fa387ec64143e14-s.0q3udbd2bu5yp.woff2 +0 -0
- package/.next/standalone/.next/static/media/797e433ab948586e-s.p.0.q-h669a_dqa.woff2 +0 -0
- package/.next/standalone/.next/static/media/bbc41e54d2fcbd21-s.0gw~uztddq1df.woff2 +0 -0
- package/src/auth/login.ts +0 -104
- package/src/auth/logout.ts +0 -50
- package/src/auth/token-store.ts +0 -64
- package/src/relay/daemon.ts +0 -362
- package/src/relay/pid.ts +0 -76
- package/src/relay/queue.ts +0 -225
- /package/.next/standalone/.next/static/{dAuQps6jUwCz9X1Q5FFOO → tGVQM5SE3NvbVu0gbAJm7}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{dAuQps6jUwCz9X1Q5FFOO → tGVQM5SE3NvbVu0gbAJm7}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor Agent CLI transcript adapter.
|
|
3
|
+
*/
|
|
4
|
+
import { statSync } from "node:fs";
|
|
5
|
+
import { getCursorProjects, getCursorSessionsByEncodedName } from "../../../lib/cursor-projects";
|
|
6
|
+
import { getCursorSessionLog } from "../../../lib/cursor-sessions";
|
|
7
|
+
import type { NormalizedToolEvent, TranscriptMetadata } from "../types";
|
|
8
|
+
import type { ListOpts } from "./claude";
|
|
9
|
+
import { logEntriesToEvents } from "./shared";
|
|
10
|
+
|
|
11
|
+
export async function listCursorTranscriptMetadata(
|
|
12
|
+
opts: ListOpts = {},
|
|
13
|
+
): Promise<TranscriptMetadata[]> {
|
|
14
|
+
const projectFilter = opts.projects ? new Set(opts.projects) : null;
|
|
15
|
+
const sinceMs = opts.sinceMs ?? 0;
|
|
16
|
+
const out: TranscriptMetadata[] = [];
|
|
17
|
+
|
|
18
|
+
const projects = await getCursorProjects();
|
|
19
|
+
for (const project of projects) {
|
|
20
|
+
const { cwd, sessions } = await getCursorSessionsByEncodedName(project.name);
|
|
21
|
+
const effectiveCwd = cwd ?? "";
|
|
22
|
+
if (projectFilter && !projectFilter.has(effectiveCwd)) continue;
|
|
23
|
+
for (const s of sessions) {
|
|
24
|
+
const mtimeMs = s.lastModified.getTime();
|
|
25
|
+
if (mtimeMs < sinceMs) continue;
|
|
26
|
+
let sizeBytes = 0;
|
|
27
|
+
try { sizeBytes = statSync(s.path).size; } catch { /* unreadable */ }
|
|
28
|
+
if (!s.sessionId) continue;
|
|
29
|
+
out.push({
|
|
30
|
+
cli: "cursor",
|
|
31
|
+
projectName: project.name,
|
|
32
|
+
sessionId: s.sessionId,
|
|
33
|
+
transcriptPath: s.path,
|
|
34
|
+
mtimeMs,
|
|
35
|
+
sizeBytes,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function streamCursorEvents(meta: TranscriptMetadata): Promise<NormalizedToolEvent[]> {
|
|
43
|
+
const log = await getCursorSessionLog(meta.sessionId);
|
|
44
|
+
if (!log) return [];
|
|
45
|
+
return logEntriesToEvents(log.entries, {
|
|
46
|
+
cli: "cursor",
|
|
47
|
+
sessionId: meta.sessionId,
|
|
48
|
+
transcriptPath: meta.transcriptPath,
|
|
49
|
+
cwd: log.cwd ?? "",
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gemini CLI transcript adapter.
|
|
3
|
+
*/
|
|
4
|
+
import { statSync } from "node:fs";
|
|
5
|
+
import { getGeminiProjects, getGeminiSessionsByEncodedName } from "../../../lib/gemini-projects";
|
|
6
|
+
import { getGeminiSessionLog } from "../../../lib/gemini-sessions";
|
|
7
|
+
import type { NormalizedToolEvent, TranscriptMetadata } from "../types";
|
|
8
|
+
import type { ListOpts } from "./claude";
|
|
9
|
+
import { logEntriesToEvents } from "./shared";
|
|
10
|
+
|
|
11
|
+
export async function listGeminiTranscriptMetadata(
|
|
12
|
+
opts: ListOpts = {},
|
|
13
|
+
): Promise<TranscriptMetadata[]> {
|
|
14
|
+
const projectFilter = opts.projects ? new Set(opts.projects) : null;
|
|
15
|
+
const sinceMs = opts.sinceMs ?? 0;
|
|
16
|
+
const out: TranscriptMetadata[] = [];
|
|
17
|
+
|
|
18
|
+
const projects = await getGeminiProjects();
|
|
19
|
+
for (const project of projects) {
|
|
20
|
+
const { cwd, sessions } = await getGeminiSessionsByEncodedName(project.name);
|
|
21
|
+
const effectiveCwd = cwd ?? "";
|
|
22
|
+
if (projectFilter && !projectFilter.has(effectiveCwd)) continue;
|
|
23
|
+
for (const s of sessions) {
|
|
24
|
+
const mtimeMs = s.lastModified.getTime();
|
|
25
|
+
if (mtimeMs < sinceMs) continue;
|
|
26
|
+
let sizeBytes = 0;
|
|
27
|
+
try { sizeBytes = statSync(s.path).size; } catch { /* unreadable */ }
|
|
28
|
+
if (!s.sessionId) continue;
|
|
29
|
+
out.push({
|
|
30
|
+
cli: "gemini",
|
|
31
|
+
projectName: project.name,
|
|
32
|
+
sessionId: s.sessionId,
|
|
33
|
+
transcriptPath: s.path,
|
|
34
|
+
mtimeMs,
|
|
35
|
+
sizeBytes,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function streamGeminiEvents(meta: TranscriptMetadata): Promise<NormalizedToolEvent[]> {
|
|
43
|
+
const log = await getGeminiSessionLog(meta.sessionId);
|
|
44
|
+
if (!log) return [];
|
|
45
|
+
return logEntriesToEvents(log.entries, {
|
|
46
|
+
cli: "gemini",
|
|
47
|
+
sessionId: meta.sessionId,
|
|
48
|
+
transcriptPath: meta.transcriptPath,
|
|
49
|
+
cwd: log.cwd ?? "",
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter registry — maps each IntegrationType to its list+stream functions.
|
|
3
|
+
*
|
|
4
|
+
* Each adapter exposes:
|
|
5
|
+
* • listTranscripts(opts) → Promise<TranscriptMetadata[]>
|
|
6
|
+
* • streamEvents(meta) → Promise<NormalizedToolEvent[]>
|
|
7
|
+
*
|
|
8
|
+
* Add a new CLI by writing a sibling module and registering it here.
|
|
9
|
+
*/
|
|
10
|
+
import type { IntegrationType } from "../../hooks/types";
|
|
11
|
+
import type { NormalizedToolEvent, TranscriptMetadata } from "../types";
|
|
12
|
+
import type { ListOpts } from "./claude";
|
|
13
|
+
|
|
14
|
+
import { listClaudeTranscriptMetadata, streamClaudeEvents } from "./claude";
|
|
15
|
+
import { listCodexTranscriptMetadata, streamCodexEvents } from "./codex";
|
|
16
|
+
import { listCopilotTranscriptMetadata, streamCopilotEvents } from "./copilot";
|
|
17
|
+
import { listCursorTranscriptMetadata, streamCursorEvents } from "./cursor";
|
|
18
|
+
import { listOpenCodeTranscriptMetadata, streamOpenCodeEvents } from "./opencode";
|
|
19
|
+
import { listPiTranscriptMetadata, streamPiEvents } from "./pi";
|
|
20
|
+
import { listGeminiTranscriptMetadata, streamGeminiEvents } from "./gemini";
|
|
21
|
+
|
|
22
|
+
export type { ListOpts };
|
|
23
|
+
|
|
24
|
+
export interface CliAdapter {
|
|
25
|
+
cli: IntegrationType;
|
|
26
|
+
listTranscripts: (opts?: ListOpts) => Promise<TranscriptMetadata[]>;
|
|
27
|
+
streamEvents: (meta: TranscriptMetadata) => Promise<NormalizedToolEvent[]>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const ADAPTERS: Record<IntegrationType, CliAdapter> = {
|
|
31
|
+
claude: {
|
|
32
|
+
cli: "claude",
|
|
33
|
+
listTranscripts: listClaudeTranscriptMetadata,
|
|
34
|
+
streamEvents: streamClaudeEvents,
|
|
35
|
+
},
|
|
36
|
+
codex: {
|
|
37
|
+
cli: "codex",
|
|
38
|
+
listTranscripts: listCodexTranscriptMetadata,
|
|
39
|
+
streamEvents: streamCodexEvents,
|
|
40
|
+
},
|
|
41
|
+
copilot: {
|
|
42
|
+
cli: "copilot",
|
|
43
|
+
listTranscripts: listCopilotTranscriptMetadata,
|
|
44
|
+
streamEvents: streamCopilotEvents,
|
|
45
|
+
},
|
|
46
|
+
cursor: {
|
|
47
|
+
cli: "cursor",
|
|
48
|
+
listTranscripts: listCursorTranscriptMetadata,
|
|
49
|
+
streamEvents: streamCursorEvents,
|
|
50
|
+
},
|
|
51
|
+
opencode: {
|
|
52
|
+
cli: "opencode",
|
|
53
|
+
listTranscripts: listOpenCodeTranscriptMetadata,
|
|
54
|
+
streamEvents: streamOpenCodeEvents,
|
|
55
|
+
},
|
|
56
|
+
pi: {
|
|
57
|
+
cli: "pi",
|
|
58
|
+
listTranscripts: listPiTranscriptMetadata,
|
|
59
|
+
streamEvents: streamPiEvents,
|
|
60
|
+
},
|
|
61
|
+
gemini: {
|
|
62
|
+
cli: "gemini",
|
|
63
|
+
listTranscripts: listGeminiTranscriptMetadata,
|
|
64
|
+
streamEvents: streamGeminiEvents,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export function getAdapter(cli: IntegrationType): CliAdapter {
|
|
69
|
+
return ADAPTERS[cli];
|
|
70
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode (sst/opencode) transcript adapter.
|
|
3
|
+
*
|
|
4
|
+
* OpenCode is the outlier — sessions live in a SQLite database, not on disk
|
|
5
|
+
* as JSONL files. The `transcriptPath` is therefore a virtual `opencode://<id>`
|
|
6
|
+
* URI and `sizeBytes` is 0 (the file cache layer treats it as uncacheable).
|
|
7
|
+
*/
|
|
8
|
+
import { getOpenCodeProjects, getOpenCodeSessionsByEncodedName } from "../../../lib/opencode-projects";
|
|
9
|
+
import { getOpenCodeSessionLog } from "../../../lib/opencode-sessions";
|
|
10
|
+
import type { NormalizedToolEvent, TranscriptMetadata } from "../types";
|
|
11
|
+
import type { ListOpts } from "./claude";
|
|
12
|
+
import { logEntriesToEvents } from "./shared";
|
|
13
|
+
|
|
14
|
+
export async function listOpenCodeTranscriptMetadata(
|
|
15
|
+
opts: ListOpts = {},
|
|
16
|
+
): Promise<TranscriptMetadata[]> {
|
|
17
|
+
const projectFilter = opts.projects ? new Set(opts.projects) : null;
|
|
18
|
+
const sinceMs = opts.sinceMs ?? 0;
|
|
19
|
+
const out: TranscriptMetadata[] = [];
|
|
20
|
+
|
|
21
|
+
const projects = await getOpenCodeProjects();
|
|
22
|
+
for (const project of projects) {
|
|
23
|
+
const { cwd, sessions } = await getOpenCodeSessionsByEncodedName(project.name);
|
|
24
|
+
const effectiveCwd = cwd ?? "";
|
|
25
|
+
if (projectFilter && !projectFilter.has(effectiveCwd)) continue;
|
|
26
|
+
for (const s of sessions) {
|
|
27
|
+
const mtimeMs = s.lastModified.getTime();
|
|
28
|
+
if (mtimeMs < sinceMs) continue;
|
|
29
|
+
if (!s.sessionId) continue;
|
|
30
|
+
out.push({
|
|
31
|
+
cli: "opencode",
|
|
32
|
+
projectName: project.name,
|
|
33
|
+
sessionId: s.sessionId,
|
|
34
|
+
transcriptPath: s.path,
|
|
35
|
+
mtimeMs,
|
|
36
|
+
sizeBytes: 0,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function streamOpenCodeEvents(meta: TranscriptMetadata): Promise<NormalizedToolEvent[]> {
|
|
44
|
+
const log = await getOpenCodeSessionLog(meta.sessionId);
|
|
45
|
+
if (!log) return [];
|
|
46
|
+
return logEntriesToEvents(log.entries, {
|
|
47
|
+
cli: "opencode",
|
|
48
|
+
sessionId: meta.sessionId,
|
|
49
|
+
transcriptPath: meta.transcriptPath,
|
|
50
|
+
cwd: log.cwd ?? "",
|
|
51
|
+
});
|
|
52
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi (pi-coding-agent) transcript adapter.
|
|
3
|
+
*/
|
|
4
|
+
import { statSync } from "node:fs";
|
|
5
|
+
import { getPiProjects, getPiSessionsByEncodedName } from "../../../lib/pi-projects";
|
|
6
|
+
import { getPiSessionLog } from "../../../lib/pi-sessions";
|
|
7
|
+
import type { NormalizedToolEvent, TranscriptMetadata } from "../types";
|
|
8
|
+
import type { ListOpts } from "./claude";
|
|
9
|
+
import { logEntriesToEvents } from "./shared";
|
|
10
|
+
|
|
11
|
+
export async function listPiTranscriptMetadata(
|
|
12
|
+
opts: ListOpts = {},
|
|
13
|
+
): Promise<TranscriptMetadata[]> {
|
|
14
|
+
const projectFilter = opts.projects ? new Set(opts.projects) : null;
|
|
15
|
+
const sinceMs = opts.sinceMs ?? 0;
|
|
16
|
+
const out: TranscriptMetadata[] = [];
|
|
17
|
+
|
|
18
|
+
const projects = await getPiProjects();
|
|
19
|
+
for (const project of projects) {
|
|
20
|
+
const { cwd, sessions } = await getPiSessionsByEncodedName(project.name);
|
|
21
|
+
const effectiveCwd = cwd ?? "";
|
|
22
|
+
if (projectFilter && !projectFilter.has(effectiveCwd)) continue;
|
|
23
|
+
for (const s of sessions) {
|
|
24
|
+
const mtimeMs = s.lastModified.getTime();
|
|
25
|
+
if (mtimeMs < sinceMs) continue;
|
|
26
|
+
let sizeBytes = 0;
|
|
27
|
+
try { sizeBytes = statSync(s.path).size; } catch { /* unreadable */ }
|
|
28
|
+
if (!s.sessionId) continue;
|
|
29
|
+
out.push({
|
|
30
|
+
cli: "pi",
|
|
31
|
+
projectName: project.name,
|
|
32
|
+
sessionId: s.sessionId,
|
|
33
|
+
transcriptPath: s.path,
|
|
34
|
+
mtimeMs,
|
|
35
|
+
sizeBytes,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function streamPiEvents(meta: TranscriptMetadata): Promise<NormalizedToolEvent[]> {
|
|
43
|
+
const log = await getPiSessionLog(meta.sessionId);
|
|
44
|
+
if (!log) return [];
|
|
45
|
+
return logEntriesToEvents(log.entries, {
|
|
46
|
+
cli: "pi",
|
|
47
|
+
sessionId: meta.sessionId,
|
|
48
|
+
transcriptPath: meta.transcriptPath,
|
|
49
|
+
cwd: log.cwd ?? "",
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers used by every per-CLI adapter.
|
|
3
|
+
*
|
|
4
|
+
* The lib/<cli>-sessions.ts parsers all produce the same `LogEntry[]` shape
|
|
5
|
+
* (defined in lib/log-entries.ts), so the conversion from LogEntry[] to
|
|
6
|
+
* NormalizedToolEvent[] is uniform across CLIs. The only per-CLI difference is
|
|
7
|
+
* the canonicalization function, which we delegate to
|
|
8
|
+
* `src/hooks/tool-name-canonicalize.ts`.
|
|
9
|
+
*/
|
|
10
|
+
import type { LogEntry } from "../../../lib/log-entries";
|
|
11
|
+
import type { IntegrationType } from "../../hooks/types";
|
|
12
|
+
import {
|
|
13
|
+
canonicalizeToolName,
|
|
14
|
+
canonicalizeToolInput,
|
|
15
|
+
} from "../../hooks/tool-name-canonicalize";
|
|
16
|
+
import {
|
|
17
|
+
AUDIT_TOOL_RESULT_MAX_BYTES,
|
|
18
|
+
type NormalizedToolEvent,
|
|
19
|
+
} from "../types";
|
|
20
|
+
|
|
21
|
+
/** Truncate a string to at most `maxBytes` UTF-8 bytes, preserving valid
|
|
22
|
+
* encoding (never splits a multi-byte sequence). `String.prototype.length`
|
|
23
|
+
* counts UTF-16 code units, not bytes — using it to "cap memory" would let
|
|
24
|
+
* through up to 4× the intended byte budget for non-ASCII text. */
|
|
25
|
+
function truncateToUtf8Bytes(s: string, maxBytes: number): string {
|
|
26
|
+
const buf = Buffer.from(s, "utf-8");
|
|
27
|
+
if (buf.byteLength <= maxBytes) return s;
|
|
28
|
+
// Walk back at most 3 bytes to land on a UTF-8 boundary (a leading byte is
|
|
29
|
+
// 0xxxxxxx or 11xxxxxx; continuation bytes are 10xxxxxx).
|
|
30
|
+
let end = maxBytes;
|
|
31
|
+
while (end > 0 && (buf[end] & 0xc0) === 0x80) end--;
|
|
32
|
+
return buf.subarray(0, end).toString("utf-8");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ConvertContext {
|
|
36
|
+
cli: IntegrationType;
|
|
37
|
+
sessionId: string;
|
|
38
|
+
transcriptPath: string;
|
|
39
|
+
/** Cwd resolved by the per-CLI parser. May be empty if the transcript had no
|
|
40
|
+
* session-start record. The audit falls back to the decoded project name. */
|
|
41
|
+
cwd: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Walks the LogEntry[] in timestamp order and yields one NormalizedToolEvent
|
|
45
|
+
* per `tool_use` content block, with the matching `tool_result.content` text
|
|
46
|
+
* attached (truncated). Returns events in chronological order. */
|
|
47
|
+
export function logEntriesToEvents(
|
|
48
|
+
entries: LogEntry[],
|
|
49
|
+
ctx: ConvertContext,
|
|
50
|
+
): NormalizedToolEvent[] {
|
|
51
|
+
const events: NormalizedToolEvent[] = [];
|
|
52
|
+
|
|
53
|
+
for (const entry of entries) {
|
|
54
|
+
if (entry.type !== "assistant") continue;
|
|
55
|
+
for (const block of entry.message.content) {
|
|
56
|
+
if (block.type !== "tool_use") continue;
|
|
57
|
+
const rawName = block.name;
|
|
58
|
+
const canonicalName = canonicalizeToolName(rawName, ctx.cli) ?? rawName;
|
|
59
|
+
const canonicalInput = canonicalizeToolInput(
|
|
60
|
+
canonicalName,
|
|
61
|
+
block.input,
|
|
62
|
+
ctx.cli,
|
|
63
|
+
) as Record<string, unknown>;
|
|
64
|
+
|
|
65
|
+
let toolResultText: string | undefined;
|
|
66
|
+
if (block.result?.content) {
|
|
67
|
+
toolResultText = truncateToUtf8Bytes(block.result.content, AUDIT_TOOL_RESULT_MAX_BYTES);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
events.push({
|
|
71
|
+
cli: ctx.cli,
|
|
72
|
+
sessionId: ctx.sessionId,
|
|
73
|
+
transcriptPath: ctx.transcriptPath,
|
|
74
|
+
cwd: ctx.cwd,
|
|
75
|
+
timestamp: entry.timestamp,
|
|
76
|
+
toolName: canonicalName,
|
|
77
|
+
rawToolName: rawName,
|
|
78
|
+
toolInput: canonicalInput ?? {},
|
|
79
|
+
toolResultText,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return events;
|
|
85
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Detector } from "../types";
|
|
2
|
+
|
|
3
|
+
const RISKY_ROOTS = ["/", "/home", "/usr", "/etc", "/var", "/opt", "/Users"];
|
|
4
|
+
|
|
5
|
+
/** Bash `find` invoked against the filesystem root or another high-level
|
|
6
|
+
* directory — tends to exhaust resources and rarely returns useful results. */
|
|
7
|
+
export const findFromRoot: Detector = {
|
|
8
|
+
name: "find-from-root",
|
|
9
|
+
description: "Bash `find` against `/`, `/home`, `/usr`, etc. — scope to cwd instead.",
|
|
10
|
+
category: "Risky",
|
|
11
|
+
severity: "warn",
|
|
12
|
+
displayTitle: "Ran find from /, /home, /usr, etc.",
|
|
13
|
+
impact: "Filesystem-wide finds exhaust resources and rarely return useful results.",
|
|
14
|
+
detect(event) {
|
|
15
|
+
if (event.toolName !== "Bash") return null;
|
|
16
|
+
const command = (event.toolInput as { command?: unknown }).command;
|
|
17
|
+
if (typeof command !== "string") return null;
|
|
18
|
+
const cmd = command.trim();
|
|
19
|
+
// find / OR find /home OR find "/etc" … — first non-flag arg
|
|
20
|
+
const match = /(?:^|[\s;|&])find\s+(?:-\S+\s+)*("[^"]+"|'[^']+'|\S+)/.exec(cmd);
|
|
21
|
+
if (!match) return null;
|
|
22
|
+
const raw = match[1].replace(/^["']|["']$/g, "");
|
|
23
|
+
const stripped = raw.replace(/\/+$/, "") || "/";
|
|
24
|
+
if (!RISKY_ROOTS.includes(stripped)) return null;
|
|
25
|
+
return { example: cmd.slice(0, 160) };
|
|
26
|
+
},
|
|
27
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Detector } from "../types";
|
|
2
|
+
|
|
3
|
+
/** `git commit ... --no-verify` (or short `-n`) — skipping pre-commit hooks. */
|
|
4
|
+
export const gitCommitNoVerify: Detector = {
|
|
5
|
+
name: "git-commit-no-verify",
|
|
6
|
+
description: "git commit invoked with --no-verify / -n, skipping hooks.",
|
|
7
|
+
category: "Risky",
|
|
8
|
+
severity: "warn",
|
|
9
|
+
displayTitle: "Committed with --no-verify",
|
|
10
|
+
impact: "Skips pre-commit hooks that exist to catch broken or unsafe code.",
|
|
11
|
+
detect(event) {
|
|
12
|
+
if (event.toolName !== "Bash") return null;
|
|
13
|
+
const command = (event.toolInput as { command?: unknown }).command;
|
|
14
|
+
if (typeof command !== "string") return null;
|
|
15
|
+
const cmd = command;
|
|
16
|
+
if (!/\bgit\s+commit\b/.test(cmd)) return null;
|
|
17
|
+
if (/\s--no-verify\b/.test(cmd) || /\s-n\b/.test(cmd)) {
|
|
18
|
+
return { example: cmd.replace(/\s+/g, " ").trim().slice(0, 160) };
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
},
|
|
22
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry of audit-only detectors.
|
|
3
|
+
*
|
|
4
|
+
* Detectors are pure functions over a NormalizedToolEvent (plus optional
|
|
5
|
+
* per-session state). They detect "stupid behaviors" not currently covered by
|
|
6
|
+
* the runtime builtin policies, with no real-time enforcement — counting only.
|
|
7
|
+
*
|
|
8
|
+
* Add a new detector by writing a sibling file and registering it here.
|
|
9
|
+
*/
|
|
10
|
+
import type { Detector } from "../types";
|
|
11
|
+
import { redundantCdCwd } from "./redundant-cd-cwd";
|
|
12
|
+
import { preferEditOverReadCat } from "./prefer-edit-over-read-cat";
|
|
13
|
+
import { preferEditOverSedAwk } from "./prefer-edit-over-sed-awk";
|
|
14
|
+
import { preferWriteOverHeredoc } from "./prefer-write-over-heredoc";
|
|
15
|
+
import { sleepPollingLoop } from "./sleep-polling-loop";
|
|
16
|
+
import { findFromRoot } from "./find-from-root";
|
|
17
|
+
import { gitCommitNoVerify } from "./git-commit-no-verify";
|
|
18
|
+
import { rereadAfterEdit } from "./reread-after-edit";
|
|
19
|
+
|
|
20
|
+
export const AUDIT_DETECTORS: Detector[] = [
|
|
21
|
+
redundantCdCwd,
|
|
22
|
+
preferEditOverReadCat,
|
|
23
|
+
preferEditOverSedAwk,
|
|
24
|
+
preferWriteOverHeredoc,
|
|
25
|
+
sleepPollingLoop,
|
|
26
|
+
findFromRoot,
|
|
27
|
+
gitCommitNoVerify,
|
|
28
|
+
rereadAfterEdit,
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
export function getDetectorByName(name: string): Detector | undefined {
|
|
32
|
+
return AUDIT_DETECTORS.find((d) => d.name === name);
|
|
33
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Detector } from "../types";
|
|
2
|
+
|
|
3
|
+
const SOURCE_EXT_RE = /\.(?:ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|kt|swift|rb|php|c|h|cc|cpp|hpp|cs|scala|sh|bash|zsh|json|yaml|yml|toml|md|txt|sql|html|css|scss|sass)$/i;
|
|
4
|
+
|
|
5
|
+
/** `cat | head | tail | less | more` invoked on a single source file with no
|
|
6
|
+
* shell pipeline / redirection. The Read tool is the right answer. .env files
|
|
7
|
+
* are intentionally excluded (covered by `block-env-files`). */
|
|
8
|
+
export const preferEditOverReadCat: Detector = {
|
|
9
|
+
name: "prefer-edit-over-read-cat",
|
|
10
|
+
description: "Bash `cat`/`head`/`tail`/`less`/`more` on a single source file — use Read.",
|
|
11
|
+
category: "Wasteful",
|
|
12
|
+
severity: "info",
|
|
13
|
+
displayTitle: "Used `cat`/`head`/`tail` on a source file",
|
|
14
|
+
impact: "Burns tokens; the Read tool returns content directly without going through Bash output.",
|
|
15
|
+
detect(event) {
|
|
16
|
+
if (event.toolName !== "Bash") return null;
|
|
17
|
+
const command = (event.toolInput as { command?: unknown }).command;
|
|
18
|
+
if (typeof command !== "string") return null;
|
|
19
|
+
const cmd = command.trim();
|
|
20
|
+
// Reject any shell pipeline, redirection, command chaining or substitution.
|
|
21
|
+
if (/[|<>;&`$()]/.test(cmd)) return null;
|
|
22
|
+
const match = /^(cat|head|tail|less|more)\s+(?:-\S+\s+)*(?:"([^"]+)"|'([^']+)'|(\S+))\s*$/.exec(cmd);
|
|
23
|
+
if (!match) return null;
|
|
24
|
+
const path = match[2] ?? match[3] ?? match[4] ?? "";
|
|
25
|
+
if (!path) return null;
|
|
26
|
+
// .env is covered by block-env-files; skip to avoid double-counting.
|
|
27
|
+
if (/(?:^|\/)\.env(?:\..+)?$/.test(path)) return null;
|
|
28
|
+
if (!SOURCE_EXT_RE.test(path)) return null;
|
|
29
|
+
return { example: cmd };
|
|
30
|
+
},
|
|
31
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Detector } from "../types";
|
|
2
|
+
|
|
3
|
+
/** In-place edits with `sed -i` or `awk … > file` that should have been done
|
|
4
|
+
* with the Edit tool. */
|
|
5
|
+
export const preferEditOverSedAwk: Detector = {
|
|
6
|
+
name: "prefer-edit-over-sed-awk",
|
|
7
|
+
description: "Bash `sed -i`/`awk` in-place edits — use Edit.",
|
|
8
|
+
category: "Wasteful",
|
|
9
|
+
severity: "info",
|
|
10
|
+
displayTitle: "Used sed -i or awk for an in-place edit",
|
|
11
|
+
impact: "Edit tool is safer and produces a diff the agent can verify.",
|
|
12
|
+
detect(event) {
|
|
13
|
+
if (event.toolName !== "Bash") return null;
|
|
14
|
+
const command = (event.toolInput as { command?: unknown }).command;
|
|
15
|
+
if (typeof command !== "string") return null;
|
|
16
|
+
const cmd = command.trim();
|
|
17
|
+
// `sed -i ...` (GNU/macOS) or `sed -i'.bak' ...` (BSD-style)
|
|
18
|
+
if (/(?:^|\s|;|&&|\|\|)sed\b[^|]*\s-i(?=\b|['"])/.test(cmd)) {
|
|
19
|
+
return { example: cmd };
|
|
20
|
+
}
|
|
21
|
+
// `awk '...' file > out` or `awk '...' file > file` (in-place via redirection)
|
|
22
|
+
if (/(?:^|\s|;|&&|\|\|)awk\b[^|]*\s>\s*\S+/.test(cmd) && !/\|/.test(cmd)) {
|
|
23
|
+
return { example: cmd };
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
},
|
|
27
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Detector } from "../types";
|
|
2
|
+
|
|
3
|
+
/** `cat << EOF > file` heredoc patterns or `echo … > file` writing multi-line
|
|
4
|
+
* content. The Write tool is the right answer. */
|
|
5
|
+
export const preferWriteOverHeredoc: Detector = {
|
|
6
|
+
name: "prefer-write-over-heredoc",
|
|
7
|
+
description: "Bash heredoc / `echo > file` writing multi-line content — use Write.",
|
|
8
|
+
category: "Wasteful",
|
|
9
|
+
severity: "info",
|
|
10
|
+
displayTitle: "Used heredoc / `echo > file` to write a multi-line file",
|
|
11
|
+
impact: "Write tool handles escaping and is verifiable.",
|
|
12
|
+
detect(event) {
|
|
13
|
+
if (event.toolName !== "Bash") return null;
|
|
14
|
+
const command = (event.toolInput as { command?: unknown }).command;
|
|
15
|
+
if (typeof command !== "string") return null;
|
|
16
|
+
const cmd = command;
|
|
17
|
+
// Heredoc redirected to a file IMMEDIATELY after the delimiter:
|
|
18
|
+
// `cat <<'EOF' > path` ← match (heredoc opens AND redirects in one step)
|
|
19
|
+
// `cat <<EOF` inside `$(...)` ← skip (heredoc captured by command substitution,
|
|
20
|
+
// later `> file` is unrelated)
|
|
21
|
+
// The redirect must appear before the next whitespace/newline that ends the
|
|
22
|
+
// heredoc opener line — otherwise it's a body or downstream redirect.
|
|
23
|
+
// Heredoc delimiters are case-sensitive but `EOF`, `eof`, `Eof`, `MARKER`
|
|
24
|
+
// and digits-after-letter are all valid; match any letter/digit/underscore.
|
|
25
|
+
if (/<<-?\s*['"]?[A-Za-z_][A-Za-z0-9_]*['"]?\s*>\s*\S/.test(cmd)) {
|
|
26
|
+
const summary = cmd.replace(/\s+/g, " ").trim().slice(0, 160);
|
|
27
|
+
return { example: summary };
|
|
28
|
+
}
|
|
29
|
+
// `echo "multi\nline" > file` or `printf "..." > file` with embedded newlines.
|
|
30
|
+
if (/(?:^|\s|;|&&|\|\|)(?:echo|printf)\s+["'][^"']*\n[^"']*["']\s*>\s*\S/.test(cmd)) {
|
|
31
|
+
const summary = cmd.replace(/\s+/g, " ").trim().slice(0, 160);
|
|
32
|
+
return { example: summary };
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
},
|
|
36
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Detector } from "../types";
|
|
2
|
+
|
|
3
|
+
/** Bash command starting with `cd <absolute-path> && …` where the absolute
|
|
4
|
+
* path equals (or is the same realpath as) the session's cwd. The agent's
|
|
5
|
+
* shell already runs in cwd — the prefix is pure waste. Explicitly called
|
|
6
|
+
* out in the Claude Code system prompt for git commands. */
|
|
7
|
+
export const redundantCdCwd: Detector = {
|
|
8
|
+
name: "redundant-cd-cwd",
|
|
9
|
+
description:
|
|
10
|
+
"Bash commands prefixed with `cd <cwd> && …` even though commands already run in cwd.",
|
|
11
|
+
category: "Wasteful",
|
|
12
|
+
severity: "info",
|
|
13
|
+
displayTitle: "Prepended cd <cwd> before commands",
|
|
14
|
+
impact: "Pure waste — your agent's shell already runs in `cwd`.",
|
|
15
|
+
detect(event) {
|
|
16
|
+
if (event.toolName !== "Bash") return null;
|
|
17
|
+
const command = (event.toolInput as { command?: unknown }).command;
|
|
18
|
+
if (typeof command !== "string" || !event.cwd) return null;
|
|
19
|
+
const trimmed = command.trimStart();
|
|
20
|
+
const match = /^cd\s+(?:"([^"]+)"|'([^']+)'|(\S+))\s*&&\s*([\s\S]+)$/.exec(trimmed);
|
|
21
|
+
if (!match) return null;
|
|
22
|
+
const path = (match[1] ?? match[2] ?? match[3] ?? "").replace(/\/+$/, "");
|
|
23
|
+
const cwd = event.cwd.replace(/\/+$/, "");
|
|
24
|
+
if (path !== cwd) return null;
|
|
25
|
+
const rest = match[4].trim();
|
|
26
|
+
return { example: `cd ${path} && ${rest}` };
|
|
27
|
+
},
|
|
28
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Detector, DetectorSessionState } from "../types";
|
|
2
|
+
|
|
3
|
+
const STATE_KEY = "rereadAfterEdit";
|
|
4
|
+
const WINDOW = 5;
|
|
5
|
+
|
|
6
|
+
interface RereadState {
|
|
7
|
+
/** Map of file_path → number of remaining tool-calls in which a Read should
|
|
8
|
+
* trigger this detector. Decremented every tool event; deleted at 0. */
|
|
9
|
+
countdown: Map<string, number>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function getState(state: DetectorSessionState): RereadState {
|
|
13
|
+
let s = state[STATE_KEY] as RereadState | undefined;
|
|
14
|
+
if (!s) {
|
|
15
|
+
s = { countdown: new Map() };
|
|
16
|
+
state[STATE_KEY] = s;
|
|
17
|
+
}
|
|
18
|
+
return s;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** When Edit or Write lands on file_path, then a subsequent Read of the same
|
|
22
|
+
* file_path within N tool calls is wasteful — the editor already returned the
|
|
23
|
+
* updated content. Explicitly called out in the Claude system prompt. */
|
|
24
|
+
export const rereadAfterEdit: Detector = {
|
|
25
|
+
name: "reread-after-edit",
|
|
26
|
+
description: "Read of a file that was just Edit'd or Write'n in the same session.",
|
|
27
|
+
category: "Wasteful",
|
|
28
|
+
severity: "info",
|
|
29
|
+
displayTitle: "Re-read a file it just edited",
|
|
30
|
+
impact: "Edit/Write already returned the updated content; the second Read is wasted tokens.",
|
|
31
|
+
detect(event, sessionState) {
|
|
32
|
+
const state = getState(sessionState);
|
|
33
|
+
const filePath = (event.toolInput as { file_path?: unknown }).file_path;
|
|
34
|
+
const pathStr = typeof filePath === "string" ? filePath : null;
|
|
35
|
+
|
|
36
|
+
// Tick down every existing countdown.
|
|
37
|
+
for (const [key, n] of state.countdown) {
|
|
38
|
+
if (n <= 1) state.countdown.delete(key);
|
|
39
|
+
else state.countdown.set(key, n - 1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!pathStr) return null;
|
|
43
|
+
|
|
44
|
+
if (event.toolName === "Edit" || event.toolName === "Write") {
|
|
45
|
+
state.countdown.set(pathStr, WINDOW);
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (event.toolName === "Read") {
|
|
50
|
+
if (state.countdown.has(pathStr)) {
|
|
51
|
+
state.countdown.delete(pathStr); // count once per edit-then-read pair
|
|
52
|
+
return { example: `Read ${pathStr} immediately after Edit/Write` };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return null;
|
|
57
|
+
},
|
|
58
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Detector } from "../types";
|
|
2
|
+
|
|
3
|
+
const SLEEP_THRESHOLD_SECONDS = 30;
|
|
4
|
+
|
|
5
|
+
/** Bash `sleep N` where N ≥ 30 (busy polling), or `while …; sleep …; done`. */
|
|
6
|
+
export const sleepPollingLoop: Detector = {
|
|
7
|
+
name: "sleep-polling-loop",
|
|
8
|
+
description: "Bash long `sleep` or while-sleep polling loops.",
|
|
9
|
+
category: "Wasteful",
|
|
10
|
+
severity: "info",
|
|
11
|
+
displayTitle: "Used a long sleep or while-sleep polling loop",
|
|
12
|
+
impact: "Burns wall-clock; better to wait for an explicit signal.",
|
|
13
|
+
detect(event) {
|
|
14
|
+
if (event.toolName !== "Bash") return null;
|
|
15
|
+
const command = (event.toolInput as { command?: unknown }).command;
|
|
16
|
+
if (typeof command !== "string") return null;
|
|
17
|
+
const cmd = command;
|
|
18
|
+
// while-sleep loop
|
|
19
|
+
if (/\bwhile\b[\s\S]*?\bsleep\b[\s\S]*?\bdone\b/.test(cmd)) {
|
|
20
|
+
return { example: cmd.replace(/\s+/g, " ").trim().slice(0, 160) };
|
|
21
|
+
}
|
|
22
|
+
// Standalone long sleep. parseFloat so `sleep 0.5m` (= 30s) isn't dropped.
|
|
23
|
+
const match = /\bsleep\s+(\d+(?:\.\d+)?)(m|h|d)?\b/.exec(cmd);
|
|
24
|
+
if (match) {
|
|
25
|
+
const n = parseFloat(match[1]);
|
|
26
|
+
const unit = match[2] ?? "s";
|
|
27
|
+
const seconds = unit === "m" ? n * 60 : unit === "h" ? n * 3600 : unit === "d" ? n * 86400 : n;
|
|
28
|
+
if (seconds >= SLEEP_THRESHOLD_SECONDS) {
|
|
29
|
+
return { example: cmd.replace(/\s+/g, " ").trim().slice(0, 160) };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
},
|
|
34
|
+
};
|