failproofai 0.0.8 → 0.0.9-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/.codex/hooks.json +77 -0
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +6 -6
- 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 +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.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 +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.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 +2 -2
- package/.next/standalone/.next/server/app/_not-found.rsc +17 -17
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +17 -17
- 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 +11 -11
- 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/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 +11 -11
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- 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.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 +8 -8
- package/.next/standalone/.next/server/app/policies/page.js +1 -1
- 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 +1 -1
- 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 +1 -1
- 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.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]__0g72weg._.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]__0h3orxc._.js → [root-of-the-server]__0.f_cyx._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0m72uj7._.js → [root-of-the-server]__03rd.z8._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__092s1ta._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__09icjsf._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0dub28-._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0e74wa-._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0g.lg8b._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h..k-e._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0okos0k._.js → [root-of-the-server]__0vu.o-3._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0w6l33k._.js +7 -7
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0of~riu._.js → [root-of-the-server]__0zqcovi._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11pa2ra._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12t-wym._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_07a1g.3._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0uy6m~m._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0zaq1hm._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_10lm7or._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_11rg2a_._.js +3 -0
- 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/node_modules_next_dist_0h9llsw._.js +1 -1
- 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/middleware-build-manifest.js +6 -6
- package/.next/standalone/.next/server/pages/404.html +2 -2
- 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/0.rk1iwdt1d7c.css +1 -0
- package/.next/standalone/.next/static/chunks/00b5h4r1el.6f.js +1 -0
- package/.next/standalone/.next/static/chunks/{01q52wg_amm60.js → 0_c_yox08g_44.js} +2 -2
- package/.next/standalone/.next/static/chunks/0bghqwo4iloy0.js +1 -0
- package/.next/standalone/.next/static/chunks/0fw2h.g66c0h3.js +1 -0
- package/.next/standalone/.next/static/chunks/0gu87mlr5ssnt.js +6 -0
- package/.next/standalone/.next/static/chunks/{0mbc8hyeqe2c4.js → 0igf3xbisp1lx.js} +1 -1
- package/.next/standalone/.next/static/chunks/{175-vim0.ztb2.js → 0jryicwtm9z2g.js} +2 -2
- package/.next/standalone/.next/static/chunks/{12simlrcfk3g2.js → 0kzk5-mh1_x53.js} +1 -1
- package/.next/standalone/.next/static/chunks/0p5zh2diw90a1.js +1 -0
- package/.next/standalone/.next/static/chunks/{0eowehbf5egcz.js → 0ufq8smh~i7wc.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0vlk_pv4somht.js → 0vwqucikost_q.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0t3euwspxi_zg.js → 0w1f.k~gi-y6..js} +1 -1
- package/.next/standalone/.next/static/chunks/{151bdxm9n-pry.js → 0z-jh701rc~j8.js} +1 -1
- package/.next/standalone/.next/static/chunks/{turbopack-0o7k.hakttp4k.js → turbopack-0s36is87fc9r2.js} +1 -1
- package/.next/standalone/app/actions/install-hooks-web.ts +21 -5
- package/.next/standalone/app/policies/hooks-client.tsx +23 -0
- package/.next/standalone/assets/logos/claude.svg +1 -0
- package/.next/standalone/assets/logos/openai-dark.svg +1 -0
- package/.next/standalone/assets/logos/openai-light.svg +1 -0
- package/.next/standalone/package.json +2 -2
- package/.next/standalone/server.js +1 -1
- package/README.md +22 -3
- package/bin/failproofai.mjs +89 -9
- package/dist/cli.mjs +1123 -281
- package/package.json +2 -2
- package/src/hooks/builtin-policies.ts +29 -6
- package/src/hooks/handler.ts +39 -10
- package/src/hooks/hook-activity-store.ts +2 -0
- package/src/hooks/install-prompt.ts +165 -0
- package/src/hooks/integrations.ts +373 -0
- package/src/hooks/manager.ts +96 -171
- package/src/hooks/policy-evaluator.ts +28 -1
- package/src/hooks/policy-types.ts +3 -1
- package/src/hooks/resolve-permission-mode.ts +147 -0
- package/src/hooks/types.ts +30 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0_rr1ty._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0dj-tbi._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0h21oar._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0i~.gk_._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0q3h.2s._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0x..fj-._.js +0 -3
- package/.next/standalone/.next/static/chunks/096~b1zwv69ph.js +0 -1
- package/.next/standalone/.next/static/chunks/0bkir2pd22ski.js +0 -1
- package/.next/standalone/.next/static/chunks/0ksdlt_1hucdm.js +0 -1
- package/.next/standalone/.next/static/chunks/0lua3p__elu_..js +0 -6
- package/.next/standalone/.next/static/chunks/0mir9jdxn35~s.css +0 -1
- package/.next/standalone/.next/static/chunks/0s_18.dox44e9.js +0 -1
- /package/.next/standalone/.next/static/{RYld7TSCDXm2_WhJq20rD → CiVeb_yiVt-O2JYrzGzB7}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{RYld7TSCDXm2_WhJq20rD → CiVeb_yiVt-O2JYrzGzB7}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{RYld7TSCDXm2_WhJq20rD → CiVeb_yiVt-O2JYrzGzB7}/_ssgManifest.js +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "failproofai",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.9-beta.1",
|
|
4
4
|
"description": "The easiest way to manage policies that keep your AI agents reliable, on-task, and running autonomously — for Claude Code & the Agents SDK",
|
|
5
5
|
"bin": {
|
|
6
6
|
"failproofai": "./dist/cli.mjs"
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
"tailwind-merge": "^3.4.0",
|
|
91
91
|
"tailwindcss": "^4.1.18",
|
|
92
92
|
"typescript": "^6.0.2",
|
|
93
|
-
"@anthropic-ai/sdk": "^0.
|
|
93
|
+
"@anthropic-ai/sdk": "^0.91.1",
|
|
94
94
|
"vitest": "^4.0.18"
|
|
95
95
|
},
|
|
96
96
|
"dependencies": {
|
|
@@ -10,15 +10,36 @@ import { allow, deny, instruct } from "./policy-helpers";
|
|
|
10
10
|
import { normalizePolicyName, registerPolicy } from "./policy-registry";
|
|
11
11
|
import { hookLogWarn } from "./hook-logger";
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Whether `resolved` lives under an agent CLI's home directory
|
|
15
|
+
* (~/.claude/ or ~/.codex/). Used to whitelist agent self-reads of their own
|
|
16
|
+
* config and transcripts.
|
|
17
|
+
*/
|
|
18
|
+
function isAgentInternalPath(resolved: string): boolean {
|
|
19
|
+
for (const dir of [".claude", ".codex"]) {
|
|
20
|
+
const root = join(homedir(), dir);
|
|
21
|
+
if (resolved === root || resolved.startsWith(root + "/")) return true;
|
|
22
|
+
}
|
|
23
|
+
return false;
|
|
16
24
|
}
|
|
17
25
|
|
|
18
|
-
|
|
19
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Whether `resolved` is a settings/hooks file for an agent CLI:
|
|
28
|
+
* • Claude Code: `.claude/settings.json`, `.claude/settings.local.json`, etc.
|
|
29
|
+
* • Codex: `.codex/hooks.json`
|
|
30
|
+
* These must NEVER be edited by the agent itself — that would let it disable
|
|
31
|
+
* its own protections.
|
|
32
|
+
*/
|
|
33
|
+
function isAgentSettingsFile(resolved: string): boolean {
|
|
34
|
+
if (/[\\/]\.claude[\\/]settings(?:\.[^/\\]+)?\.json$/.test(resolved)) return true;
|
|
35
|
+
if (/[\\/]\.codex[\\/]hooks\.json$/.test(resolved)) return true;
|
|
36
|
+
return false;
|
|
20
37
|
}
|
|
21
38
|
|
|
39
|
+
// Back-compat aliases — kept for any caller that imports the old names.
|
|
40
|
+
const isClaudeInternalPath = isAgentInternalPath;
|
|
41
|
+
const isClaudeSettingsFile = isAgentSettingsFile;
|
|
42
|
+
|
|
22
43
|
function getCommand(ctx: PolicyContext): string {
|
|
23
44
|
return (ctx.toolInput?.command as string) ?? "";
|
|
24
45
|
}
|
|
@@ -1483,7 +1504,9 @@ export const BUILTIN_POLICIES: BuiltinPolicyDefinition[] = [
|
|
|
1483
1504
|
name: "block-sudo",
|
|
1484
1505
|
description: "Block sudo commands",
|
|
1485
1506
|
fn: blockSudo,
|
|
1486
|
-
|
|
1507
|
+
// PermissionRequest is Codex's escalation-approval event; fire the same
|
|
1508
|
+
// sudo guard there so Codex sandbox bypasses are blocked too.
|
|
1509
|
+
match: { events: ["PreToolUse", "PermissionRequest"], toolNames: ["Bash"] },
|
|
1487
1510
|
defaultEnabled: true,
|
|
1488
1511
|
category: "Dangerous Commands",
|
|
1489
1512
|
params: {
|
package/src/hooks/handler.ts
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* ~/.failproofai/policies-config.json, evaluates matching policies, persists
|
|
6
6
|
* activity to disk, and returns the appropriate exit code + stdout response.
|
|
7
7
|
*/
|
|
8
|
-
import type { HookEventType, SessionMetadata } from "./types";
|
|
8
|
+
import type { HookEventType, IntegrationType, SessionMetadata, CodexHookEventType } from "./types";
|
|
9
|
+
import { CODEX_EVENT_MAP } from "./types";
|
|
9
10
|
import type { PolicyFunction, PolicyResult } from "./policy-types";
|
|
10
11
|
import { readMergedHooksConfig } from "./hooks-config";
|
|
11
12
|
import { registerBuiltinPolicies } from "./builtin-policies";
|
|
@@ -15,10 +16,28 @@ import { loadAllCustomHooks } from "./custom-hooks-loader";
|
|
|
15
16
|
import type { CustomHook } from "./policy-types";
|
|
16
17
|
import { persistHookActivity } from "./hook-activity-store";
|
|
17
18
|
import { trackHookEvent } from "./hook-telemetry";
|
|
19
|
+
import { resolvePermissionMode } from "./resolve-permission-mode";
|
|
18
20
|
import { getInstanceId } from "../../lib/telemetry-id";
|
|
19
21
|
import { hookLogInfo, hookLogWarn } from "./hook-logger";
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Canonicalize an event name to PascalCase. Codex sends snake_case event names
|
|
25
|
+
* on stdin and as the --hook arg; Claude Code sends PascalCase. The internal
|
|
26
|
+
* registry, builtin policies, and policy.match.events all key on PascalCase.
|
|
27
|
+
*/
|
|
28
|
+
function canonicalizeEventType(raw: string, cli: IntegrationType): HookEventType {
|
|
29
|
+
if (cli === "codex") {
|
|
30
|
+
const mapped = CODEX_EVENT_MAP[raw as CodexHookEventType];
|
|
31
|
+
if (mapped) return mapped;
|
|
32
|
+
}
|
|
33
|
+
// Already PascalCase or unknown — pass through; HOOK_EVENT_TYPES type-checks downstream.
|
|
34
|
+
return raw as HookEventType;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function handleHookEvent(
|
|
38
|
+
eventType: string,
|
|
39
|
+
cli: IntegrationType = "claude",
|
|
40
|
+
): Promise<number> {
|
|
22
41
|
const startTime = performance.now();
|
|
23
42
|
|
|
24
43
|
// Read stdin payload (Claude passes JSON)
|
|
@@ -57,13 +76,18 @@ export async function handleHookEvent(eventType: string): Promise<number> {
|
|
|
57
76
|
}
|
|
58
77
|
}
|
|
59
78
|
|
|
79
|
+
// Canonicalize event name (Codex sends snake_case; internals expect PascalCase)
|
|
80
|
+
const canonicalEventType = canonicalizeEventType(eventType, cli);
|
|
81
|
+
|
|
60
82
|
// Extract session metadata from payload
|
|
83
|
+
const sessionId = parsed.session_id as string | undefined;
|
|
61
84
|
const session: SessionMetadata = {
|
|
62
|
-
sessionId
|
|
85
|
+
sessionId,
|
|
63
86
|
transcriptPath: parsed.transcript_path as string | undefined,
|
|
64
87
|
cwd: parsed.cwd as string | undefined,
|
|
65
|
-
permissionMode: parsed
|
|
88
|
+
permissionMode: resolvePermissionMode(cli, parsed, sessionId),
|
|
66
89
|
hookEventName: parsed.hook_event_name as string | undefined,
|
|
90
|
+
cli,
|
|
67
91
|
};
|
|
68
92
|
|
|
69
93
|
// Load enabled policies (merge across project/local/global scopes)
|
|
@@ -98,6 +122,7 @@ export async function handleHookEvent(eventType: string): Promise<number> {
|
|
|
98
122
|
hook_name: hookName,
|
|
99
123
|
error_type: isTimeout ? "timeout" : "exception",
|
|
100
124
|
event_type: eventType,
|
|
125
|
+
cli,
|
|
101
126
|
is_convention_policy: isConvention,
|
|
102
127
|
convention_scope: conventionScope ?? null,
|
|
103
128
|
});
|
|
@@ -116,6 +141,7 @@ export async function handleHookEvent(eventType: string): Promise<number> {
|
|
|
116
141
|
// Fire telemetry once per invocation for custom hook loads
|
|
117
142
|
if (customHooksList.length > 0) {
|
|
118
143
|
void trackHookEvent(getInstanceId(), "custom_hooks_loaded", {
|
|
144
|
+
cli,
|
|
119
145
|
custom_hooks_count: customHooksList.length,
|
|
120
146
|
custom_hook_names: customHooksList.map((h) => h.name),
|
|
121
147
|
event_types_covered: [...new Set(customHooksList.flatMap((h) => h.match?.events ?? []))],
|
|
@@ -125,7 +151,8 @@ export async function handleHookEvent(eventType: string): Promise<number> {
|
|
|
125
151
|
// Fire telemetry for convention-based policy discovery
|
|
126
152
|
if (loadResult.conventionSources.length > 0) {
|
|
127
153
|
void trackHookEvent(getInstanceId(), "convention_policies_loaded", {
|
|
128
|
-
event_type:
|
|
154
|
+
event_type: canonicalEventType,
|
|
155
|
+
cli,
|
|
129
156
|
project_file_count: loadResult.conventionSources.filter((s) => s.scope === "project").length,
|
|
130
157
|
user_file_count: loadResult.conventionSources.filter((s) => s.scope === "user").length,
|
|
131
158
|
convention_hook_count: conventionHookNames.size,
|
|
@@ -133,10 +160,10 @@ export async function handleHookEvent(eventType: string): Promise<number> {
|
|
|
133
160
|
});
|
|
134
161
|
}
|
|
135
162
|
|
|
136
|
-
hookLogInfo(`event=${
|
|
163
|
+
hookLogInfo(`event=${canonicalEventType} cli=${cli} policies=${config.enabledPolicies.length} custom=${customHooksList.length} convention=${conventionHookNames.size}`);
|
|
137
164
|
|
|
138
|
-
// Evaluate policies
|
|
139
|
-
const result = await evaluatePolicies(
|
|
165
|
+
// Evaluate policies (use canonical PascalCase event type)
|
|
166
|
+
const result = await evaluatePolicies(canonicalEventType, parsed, session, config);
|
|
140
167
|
const durationMs = Math.round(performance.now() - startTime);
|
|
141
168
|
hookLogInfo(`result=${result.decision} policy=${result.policyName ?? "none"} duration=${durationMs}ms`);
|
|
142
169
|
|
|
@@ -150,7 +177,8 @@ export async function handleHookEvent(eventType: string): Promise<number> {
|
|
|
150
177
|
// Persist activity to disk (visible in /policies activity tab)
|
|
151
178
|
const activityEntry = {
|
|
152
179
|
timestamp: Date.now(),
|
|
153
|
-
eventType,
|
|
180
|
+
eventType: canonicalEventType,
|
|
181
|
+
integration: cli,
|
|
154
182
|
toolName: (parsed.tool_name as string) ?? null,
|
|
155
183
|
policyName: result.policyName,
|
|
156
184
|
policyNames: result.policyNames,
|
|
@@ -203,7 +231,8 @@ export async function handleHookEvent(eventType: string): Promise<number> {
|
|
|
203
231
|
: [];
|
|
204
232
|
const distinctId = getInstanceId();
|
|
205
233
|
await trackHookEvent(distinctId, "hook_policy_triggered", {
|
|
206
|
-
event_type:
|
|
234
|
+
event_type: canonicalEventType,
|
|
235
|
+
cli,
|
|
207
236
|
tool_name: (parsed.tool_name as string) ?? null,
|
|
208
237
|
policy_name: result.policyName,
|
|
209
238
|
decision: result.decision,
|
|
@@ -41,6 +41,8 @@ let rotateSeq = 0;
|
|
|
41
41
|
export interface HookActivityEntry {
|
|
42
42
|
timestamp: number;
|
|
43
43
|
eventType: string;
|
|
44
|
+
/** Which agent CLI fired the hook (claude | codex). */
|
|
45
|
+
integration?: string;
|
|
44
46
|
toolName: string | null;
|
|
45
47
|
policyName: string | null;
|
|
46
48
|
policyNames?: string[];
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import * as readline from "node:readline";
|
|
13
13
|
import { BUILTIN_POLICIES } from "./builtin-policies";
|
|
14
|
+
import { detectInstalledClis, getIntegration } from "./integrations";
|
|
15
|
+
import type { IntegrationType } from "./types";
|
|
14
16
|
|
|
15
17
|
interface SelectItem {
|
|
16
18
|
name: string;
|
|
@@ -28,6 +30,169 @@ export interface PromptOptions {
|
|
|
28
30
|
includeBeta?: boolean;
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Resolve which agent CLIs to install hooks for.
|
|
35
|
+
*
|
|
36
|
+
* Rules:
|
|
37
|
+
* • If `explicit` is provided (from `--cli`), use it as-is.
|
|
38
|
+
* • Else, detect installed CLIs (PATH probe).
|
|
39
|
+
* • If exactly one detected → use just that one (no prompt).
|
|
40
|
+
* • If multiple detected and stdin is a TTY → arrow-key single-select.
|
|
41
|
+
* • Otherwise → default to all detected (or ["claude"] when none).
|
|
42
|
+
*
|
|
43
|
+
* Returns the selected IntegrationType[] (always non-empty).
|
|
44
|
+
*/
|
|
45
|
+
export async function resolveTargetClis(explicit?: IntegrationType[]): Promise<IntegrationType[]> {
|
|
46
|
+
if (explicit && explicit.length > 0) return [...new Set(explicit)];
|
|
47
|
+
|
|
48
|
+
const detected = detectInstalledClis();
|
|
49
|
+
|
|
50
|
+
if (detected.length === 0) {
|
|
51
|
+
console.log(
|
|
52
|
+
"\x1B[33mWarning: no agent CLI binary found in PATH (claude, codex). " +
|
|
53
|
+
"Defaulting to Claude Code; hooks will activate when an agent is installed.\x1B[0m",
|
|
54
|
+
);
|
|
55
|
+
return ["claude"];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (detected.length === 1) {
|
|
59
|
+
const integration = getIntegration(detected[0]);
|
|
60
|
+
console.log(`Detected ${integration.displayName}; installing hooks for it.`);
|
|
61
|
+
return detected;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Multiple detected. Prompt or default.
|
|
65
|
+
if (!process.stdin.isTTY) return detected; // non-interactive: install for all detected
|
|
66
|
+
|
|
67
|
+
return promptCliTargetSelection(detected);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Interactive arrow-key single-select for "install for which CLI?" when
|
|
72
|
+
* multiple agent CLIs are detected. Visual style mirrors promptPolicySelection.
|
|
73
|
+
*/
|
|
74
|
+
async function promptCliTargetSelection(
|
|
75
|
+
detected: IntegrationType[],
|
|
76
|
+
): Promise<IntegrationType[]> {
|
|
77
|
+
const labels = detected.map((id) => getIntegration(id).displayName).join(" + ");
|
|
78
|
+
const options: Array<{ label: string; description: string; value: IntegrationType[] }> = [
|
|
79
|
+
{ label: "Both", description: labels, value: detected },
|
|
80
|
+
...detected.map((id) => ({
|
|
81
|
+
label: `${getIntegration(id).displayName} only`,
|
|
82
|
+
description: "",
|
|
83
|
+
value: [id] as IntegrationType[],
|
|
84
|
+
})),
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
let cursor = 0;
|
|
88
|
+
let lastLineCount = 0;
|
|
89
|
+
let cursorHidden = false;
|
|
90
|
+
|
|
91
|
+
function hideCursor(): void {
|
|
92
|
+
if (!cursorHidden) {
|
|
93
|
+
process.stdout.write("\x1B[?25l");
|
|
94
|
+
cursorHidden = true;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function showCursor(): void {
|
|
98
|
+
if (cursorHidden) {
|
|
99
|
+
process.stdout.write("\x1B[?25h");
|
|
100
|
+
cursorHidden = false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function truncateLine(line: string, width: number): string {
|
|
105
|
+
let visual = 0;
|
|
106
|
+
let result = "";
|
|
107
|
+
let i = 0;
|
|
108
|
+
while (i < line.length) {
|
|
109
|
+
if (line[i] === "\x1B" && line[i + 1] === "[") {
|
|
110
|
+
let j = i + 2;
|
|
111
|
+
while (j < line.length && !/[A-Za-z]/.test(line[j])) j++;
|
|
112
|
+
j++;
|
|
113
|
+
result += line.slice(i, j);
|
|
114
|
+
i = j;
|
|
115
|
+
} else {
|
|
116
|
+
if (visual >= width) break;
|
|
117
|
+
result += line[i];
|
|
118
|
+
visual++;
|
|
119
|
+
i++;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function render(): void {
|
|
126
|
+
const cols = process.stdout.columns || 120;
|
|
127
|
+
hideCursor();
|
|
128
|
+
|
|
129
|
+
const lines: string[] = [];
|
|
130
|
+
lines.push(" Failproof AI — Install Hooks");
|
|
131
|
+
lines.push("");
|
|
132
|
+
lines.push(` \x1B[2mDetected ${labels}. Choose where to install:\x1B[0m`);
|
|
133
|
+
lines.push("");
|
|
134
|
+
|
|
135
|
+
for (let i = 0; i < options.length; i++) {
|
|
136
|
+
const opt = options[i];
|
|
137
|
+
const isActive = i === cursor;
|
|
138
|
+
const pointer = isActive ? "\x1B[36m❯\x1B[0m" : " ";
|
|
139
|
+
const labelPart = isActive ? `\x1B[1;36m${opt.label}\x1B[0m` : opt.label;
|
|
140
|
+
const pad = opt.description ? " ".repeat(Math.max(2, 22 - opt.label.length)) : "";
|
|
141
|
+
const desc = opt.description ? `\x1B[2m${opt.description}\x1B[0m` : "";
|
|
142
|
+
lines.push(` ${pointer} ${labelPart}${pad}${desc}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
lines.push("");
|
|
146
|
+
lines.push(" \x1B[2m" + "─".repeat(Math.max(2, cols - 2)) + "\x1B[0m");
|
|
147
|
+
lines.push(" [↑↓] Move [Enter] Select [^C] Quit");
|
|
148
|
+
|
|
149
|
+
if (lastLineCount > 0) {
|
|
150
|
+
process.stdout.write(`\x1B[${lastLineCount}A\x1B[J`);
|
|
151
|
+
}
|
|
152
|
+
const output =
|
|
153
|
+
lines.map((l) => (l === "" ? l : truncateLine(l, cols))).join("\n") + "\n";
|
|
154
|
+
process.stdout.write(output);
|
|
155
|
+
lastLineCount = lines.length;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return new Promise<IntegrationType[]>((resolve) => {
|
|
159
|
+
render();
|
|
160
|
+
readline.emitKeypressEvents(process.stdin);
|
|
161
|
+
const wasRaw = process.stdin.isRaw;
|
|
162
|
+
if (process.stdin.setRawMode) process.stdin.setRawMode(true);
|
|
163
|
+
process.stdin.resume();
|
|
164
|
+
|
|
165
|
+
function cleanup(): void {
|
|
166
|
+
showCursor();
|
|
167
|
+
process.stdin.removeListener("keypress", onKey);
|
|
168
|
+
if (process.stdin.setRawMode) process.stdin.setRawMode(wasRaw ?? false);
|
|
169
|
+
process.stdin.pause();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function onKey(_str: string | undefined, key: readline.Key): void {
|
|
173
|
+
if (!key) return;
|
|
174
|
+
if (key.ctrl && (key.name === "c" || key.name === "d")) {
|
|
175
|
+
cleanup();
|
|
176
|
+
process.stdout.write("\n");
|
|
177
|
+
process.exit(130); // SIGINT-equivalent
|
|
178
|
+
}
|
|
179
|
+
if (key.name === "up") {
|
|
180
|
+
cursor = cursor > 0 ? cursor - 1 : options.length - 1;
|
|
181
|
+
render();
|
|
182
|
+
} else if (key.name === "down") {
|
|
183
|
+
cursor = cursor < options.length - 1 ? cursor + 1 : 0;
|
|
184
|
+
render();
|
|
185
|
+
} else if (key.name === "return" || key.name === "space") {
|
|
186
|
+
cleanup();
|
|
187
|
+
process.stdout.write("\n");
|
|
188
|
+
resolve(options[cursor].value);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
process.stdin.on("keypress", onKey);
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
31
196
|
/**
|
|
32
197
|
* Show interactive searchable policy selector.
|
|
33
198
|
* @param preSelected — policy names to pre-check (e.g. from existing config).
|