failproofai 0.0.7 → 0.0.9-beta.0
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 +3 -3
- 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/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/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/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/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/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/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/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]__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]__0zn7uo6._.js → [root-of-the-server]__0ca1zru._.js} +2 -2
- 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]__0ea22pr._.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 +3 -3
- 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/{01l2mh88iy.ga.js → 03lsndql_yml5.js} +1 -1
- package/.next/standalone/.next/static/chunks/0amfi~vb_gfgo.js +1 -0
- package/.next/standalone/.next/static/chunks/0fw2h.g66c0h3.js +1 -0
- package/.next/standalone/.next/static/chunks/{0f_9854du76y2.js → 0jce49ygr4fdv.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0388wpenm9-a4.js → 0mungg3~jpwe7.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0x0o8~u4jsatb.js → 0uq_5p-p7myfe.js} +2 -2
- package/.next/standalone/.next/static/chunks/0v.xuf4ynzp~~.js +6 -0
- package/.next/standalone/.next/static/chunks/{0kkzzoo.s-t3p.js → 0vb8xxj_v2tz8.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0vlk_pv4somht.js → 0vwqucikost_q.js} +1 -1
- package/.next/standalone/.next/static/chunks/0~mroziiwl1m5.js +1 -0
- 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 +1040 -297
- package/package.json +2 -2
- package/src/hooks/builtin-policies.ts +39 -33
- package/src/hooks/handler.ts +39 -10
- package/src/hooks/hook-activity-store.ts +2 -0
- package/src/hooks/install-prompt.ts +69 -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]__0dj-tbi._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0tjjyb9._.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/0a0lh_a4f_xs-.js +0 -6
- package/.next/standalone/.next/static/chunks/0bkir2pd22ski.js +0 -1
- package/.next/standalone/.next/static/chunks/0j2o20pqkib~d.js +0 -1
- package/.next/standalone/.next/static/chunks/0ksdlt_1hucdm.js +0 -1
- package/.next/standalone/.next/static/chunks/0mir9jdxn35~s.css +0 -1
- package/.next/standalone/.next/static/chunks/12wu.28cbx4dl.js +0 -1
- /package/.next/standalone/.next/static/{9FNjQiktocMN-qDiGqDL5 → oUO8u4z9JvtTzS_2RJoGo}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{9FNjQiktocMN-qDiGqDL5 → oUO8u4z9JvtTzS_2RJoGo}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{9FNjQiktocMN-qDiGqDL5 → oUO8u4z9JvtTzS_2RJoGo}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-CLI hook integration registry.
|
|
3
|
+
*
|
|
4
|
+
* An `Integration` describes how failproofai hooks are installed, detected, and
|
|
5
|
+
* read for a specific agent CLI (Claude Code, OpenAI Codex). The runtime hot
|
|
6
|
+
* path (`handler.ts`, `policy-evaluator.ts`, `BUILTIN_POLICIES`, `policy-helpers`)
|
|
7
|
+
* is agent-agnostic — only install/uninstall plumbing varies.
|
|
8
|
+
*/
|
|
9
|
+
import { execSync } from "node:child_process";
|
|
10
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
11
|
+
import { resolve, dirname } from "node:path";
|
|
12
|
+
import { homedir } from "node:os";
|
|
13
|
+
import {
|
|
14
|
+
HOOK_EVENT_TYPES,
|
|
15
|
+
HOOK_SCOPES,
|
|
16
|
+
CODEX_HOOK_EVENT_TYPES,
|
|
17
|
+
CODEX_HOOK_SCOPES,
|
|
18
|
+
CODEX_EVENT_MAP,
|
|
19
|
+
FAILPROOFAI_HOOK_MARKER,
|
|
20
|
+
INTEGRATION_TYPES,
|
|
21
|
+
type IntegrationType,
|
|
22
|
+
type HookScope,
|
|
23
|
+
type ClaudeSettings,
|
|
24
|
+
type ClaudeHookMatcher,
|
|
25
|
+
type ClaudeHookEntry,
|
|
26
|
+
type CodexHookEventType,
|
|
27
|
+
} from "./types";
|
|
28
|
+
|
|
29
|
+
// ── Generic helpers ─────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
function readJsonFile(path: string): Record<string, unknown> {
|
|
32
|
+
if (!existsSync(path)) return {};
|
|
33
|
+
const raw = readFileSync(path, "utf8");
|
|
34
|
+
return JSON.parse(raw) as Record<string, unknown>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function writeJsonFile(path: string, data: Record<string, unknown>): void {
|
|
38
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
39
|
+
writeFileSync(path, JSON.stringify(data, null, 2) + "\n", "utf8");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isMarkedHook(hook: Record<string, unknown>): boolean {
|
|
43
|
+
if (hook[FAILPROOFAI_HOOK_MARKER] === true) return true;
|
|
44
|
+
// Fallback for legacy installs predating the marker
|
|
45
|
+
const cmd = typeof hook.command === "string" ? hook.command : "";
|
|
46
|
+
return cmd.includes("failproofai") && cmd.includes("--hook");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function binaryExists(name: string): boolean {
|
|
50
|
+
try {
|
|
51
|
+
const cmd = process.platform === "win32" ? `where ${name}` : `which ${name}`;
|
|
52
|
+
execSync(cmd, { encoding: "utf8", stdio: "pipe" });
|
|
53
|
+
return true;
|
|
54
|
+
} catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ── Integration interface ───────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
export interface Integration {
|
|
62
|
+
id: IntegrationType;
|
|
63
|
+
displayName: string;
|
|
64
|
+
/** Settings scopes this integration supports (e.g. claude: user/project/local; codex: user/project). */
|
|
65
|
+
scopes: readonly HookScope[];
|
|
66
|
+
/** Hook events this integration fires (Claude: PascalCase, Codex: snake_case stored as Pascal in settings). */
|
|
67
|
+
eventTypes: readonly string[];
|
|
68
|
+
|
|
69
|
+
/** Resolve the per-scope settings/hooks file path. */
|
|
70
|
+
getSettingsPath(scope: HookScope, cwd?: string): string;
|
|
71
|
+
|
|
72
|
+
/** Read the raw settings/hooks file (returns {} when missing). */
|
|
73
|
+
readSettings(settingsPath: string): Record<string, unknown>;
|
|
74
|
+
|
|
75
|
+
/** Write the settings/hooks file. */
|
|
76
|
+
writeSettings(settingsPath: string, settings: Record<string, unknown>): void;
|
|
77
|
+
|
|
78
|
+
/** Build a single hook entry for a given event. */
|
|
79
|
+
buildHookEntry(binaryPath: string, eventType: string, scope?: HookScope): Record<string, unknown>;
|
|
80
|
+
|
|
81
|
+
/** Whether a hook entry is owned by failproofai. */
|
|
82
|
+
isFailproofaiHook(hook: Record<string, unknown>): boolean;
|
|
83
|
+
|
|
84
|
+
/** Mutate `settings` in place, registering failproofai across all event types. Idempotent. */
|
|
85
|
+
writeHookEntries(settings: Record<string, unknown>, binaryPath: string, scope?: HookScope): void;
|
|
86
|
+
|
|
87
|
+
/** Remove all failproofai hook entries from a settings file. Returns the number removed. */
|
|
88
|
+
removeHooksFromFile(settingsPath: string): number;
|
|
89
|
+
|
|
90
|
+
/** Whether failproofai hooks are present in a given scope. */
|
|
91
|
+
hooksInstalledInSettings(scope: HookScope, cwd?: string): boolean;
|
|
92
|
+
|
|
93
|
+
/** Whether the agent CLI binary is installed (probes PATH). */
|
|
94
|
+
detectInstalled(): boolean;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── Claude Code integration ─────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
export const claudeCode: Integration = {
|
|
100
|
+
id: "claude",
|
|
101
|
+
displayName: "Claude Code",
|
|
102
|
+
scopes: HOOK_SCOPES,
|
|
103
|
+
eventTypes: HOOK_EVENT_TYPES,
|
|
104
|
+
|
|
105
|
+
getSettingsPath(scope, cwd) {
|
|
106
|
+
const base = cwd ? resolve(cwd) : process.cwd();
|
|
107
|
+
switch (scope) {
|
|
108
|
+
case "user":
|
|
109
|
+
return resolve(homedir(), ".claude", "settings.json");
|
|
110
|
+
case "project":
|
|
111
|
+
return resolve(base, ".claude", "settings.json");
|
|
112
|
+
case "local":
|
|
113
|
+
return resolve(base, ".claude", "settings.local.json");
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
readSettings(settingsPath) {
|
|
118
|
+
return readJsonFile(settingsPath);
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
writeSettings(settingsPath, settings) {
|
|
122
|
+
writeJsonFile(settingsPath, settings);
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
buildHookEntry(binaryPath, eventType, scope) {
|
|
126
|
+
// No --cli flag on the Claude command line: the handler defaults to
|
|
127
|
+
// claude when --cli is omitted, preserving back-compat with hooks
|
|
128
|
+
// installed before multi-CLI support was added.
|
|
129
|
+
const command =
|
|
130
|
+
scope === "project"
|
|
131
|
+
? `npx -y failproofai --hook ${eventType}`
|
|
132
|
+
: `"${binaryPath}" --hook ${eventType}`;
|
|
133
|
+
return {
|
|
134
|
+
type: "command",
|
|
135
|
+
command,
|
|
136
|
+
timeout: 60_000,
|
|
137
|
+
[FAILPROOFAI_HOOK_MARKER]: true,
|
|
138
|
+
};
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
isFailproofaiHook: isMarkedHook,
|
|
142
|
+
|
|
143
|
+
writeHookEntries(settings, binaryPath, scope) {
|
|
144
|
+
const s = settings as ClaudeSettings;
|
|
145
|
+
if (!s.hooks) s.hooks = {};
|
|
146
|
+
|
|
147
|
+
for (const eventType of HOOK_EVENT_TYPES) {
|
|
148
|
+
const hookEntry = this.buildHookEntry(binaryPath, eventType, scope) as unknown as ClaudeHookEntry;
|
|
149
|
+
if (!s.hooks[eventType]) s.hooks[eventType] = [];
|
|
150
|
+
const matchers: ClaudeHookMatcher[] = s.hooks[eventType];
|
|
151
|
+
|
|
152
|
+
let found = false;
|
|
153
|
+
for (const matcher of matchers) {
|
|
154
|
+
if (!matcher.hooks) continue;
|
|
155
|
+
const idx = matcher.hooks.findIndex((h) => isMarkedHook(h as Record<string, unknown>));
|
|
156
|
+
if (idx >= 0) {
|
|
157
|
+
matcher.hooks[idx] = hookEntry;
|
|
158
|
+
found = true;
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (!found) matchers.push({ hooks: [hookEntry] });
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
removeHooksFromFile(settingsPath) {
|
|
167
|
+
const settings = this.readSettings(settingsPath) as ClaudeSettings;
|
|
168
|
+
if (!settings.hooks) return 0;
|
|
169
|
+
|
|
170
|
+
let removed = 0;
|
|
171
|
+
for (const eventType of Object.keys(settings.hooks)) {
|
|
172
|
+
const matchers = settings.hooks[eventType];
|
|
173
|
+
if (!Array.isArray(matchers)) continue;
|
|
174
|
+
for (let i = matchers.length - 1; i >= 0; i--) {
|
|
175
|
+
const matcher = matchers[i];
|
|
176
|
+
if (!matcher.hooks) continue;
|
|
177
|
+
const before = matcher.hooks.length;
|
|
178
|
+
matcher.hooks = matcher.hooks.filter((h) => !isMarkedHook(h as Record<string, unknown>));
|
|
179
|
+
removed += before - matcher.hooks.length;
|
|
180
|
+
if (matcher.hooks.length === 0) matchers.splice(i, 1);
|
|
181
|
+
}
|
|
182
|
+
if (matchers.length === 0) delete settings.hooks[eventType];
|
|
183
|
+
}
|
|
184
|
+
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
185
|
+
|
|
186
|
+
this.writeSettings(settingsPath, settings as Record<string, unknown>);
|
|
187
|
+
return removed;
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
hooksInstalledInSettings(scope, cwd) {
|
|
191
|
+
const settingsPath = this.getSettingsPath(scope, cwd);
|
|
192
|
+
if (!existsSync(settingsPath)) return false;
|
|
193
|
+
try {
|
|
194
|
+
const settings = this.readSettings(settingsPath) as ClaudeSettings;
|
|
195
|
+
if (!settings.hooks) return false;
|
|
196
|
+
for (const matchers of Object.values(settings.hooks)) {
|
|
197
|
+
if (!Array.isArray(matchers)) continue;
|
|
198
|
+
for (const matcher of matchers) {
|
|
199
|
+
if (!matcher.hooks) continue;
|
|
200
|
+
if (matcher.hooks.some((h) => isMarkedHook(h as Record<string, unknown>))) return true;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
} catch {
|
|
204
|
+
// Corrupt settings — treat as not installed
|
|
205
|
+
}
|
|
206
|
+
return false;
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
detectInstalled() {
|
|
210
|
+
return binaryExists("claude") || binaryExists("claude-code");
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
// ── OpenAI Codex integration ────────────────────────────────────────────────
|
|
215
|
+
//
|
|
216
|
+
// Codex's hook protocol is Claude-compatible by design (see the parity matrix
|
|
217
|
+
// in plans/great-in-failproofai-i-vectorized-treasure.md). The only material
|
|
218
|
+
// differences are:
|
|
219
|
+
// • Settings paths: ~/.codex/hooks.json (user) and <cwd>/.codex/hooks.json (project)
|
|
220
|
+
// • Stdin event names arrive snake_case (pre_tool_use); we canonicalize to PascalCase before policy lookup
|
|
221
|
+
// • No "local" scope
|
|
222
|
+
// • Settings file carries a top-level "version": 1 marker
|
|
223
|
+
|
|
224
|
+
interface CodexSettingsFile {
|
|
225
|
+
version?: number;
|
|
226
|
+
hooks?: Record<string, ClaudeHookMatcher[]>;
|
|
227
|
+
[key: string]: unknown;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export const codex: Integration = {
|
|
231
|
+
id: "codex",
|
|
232
|
+
displayName: "OpenAI Codex",
|
|
233
|
+
scopes: CODEX_HOOK_SCOPES,
|
|
234
|
+
eventTypes: CODEX_HOOK_EVENT_TYPES,
|
|
235
|
+
|
|
236
|
+
getSettingsPath(scope, cwd) {
|
|
237
|
+
const base = cwd ? resolve(cwd) : process.cwd();
|
|
238
|
+
switch (scope) {
|
|
239
|
+
case "user":
|
|
240
|
+
return resolve(homedir(), ".codex", "hooks.json");
|
|
241
|
+
case "project":
|
|
242
|
+
return resolve(base, ".codex", "hooks.json");
|
|
243
|
+
case "local":
|
|
244
|
+
// Codex has no "local" scope; fall back to project so callers don't crash.
|
|
245
|
+
// The CLI rejects --cli codex --scope local before reaching here.
|
|
246
|
+
return resolve(base, ".codex", "hooks.json");
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
|
|
250
|
+
readSettings(settingsPath) {
|
|
251
|
+
const raw = readJsonFile(settingsPath);
|
|
252
|
+
if (raw.version === undefined) raw.version = 1;
|
|
253
|
+
return raw;
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
writeSettings(settingsPath, settings) {
|
|
257
|
+
writeJsonFile(settingsPath, settings);
|
|
258
|
+
},
|
|
259
|
+
|
|
260
|
+
buildHookEntry(binaryPath, eventType, scope) {
|
|
261
|
+
// `eventType` here is the snake_case Codex event name; Codex stores under
|
|
262
|
+
// PascalCase keys but invokes the command with the snake_case form, which
|
|
263
|
+
// we canonicalize on the way into policy-evaluator.
|
|
264
|
+
const command =
|
|
265
|
+
scope === "project"
|
|
266
|
+
? `npx -y failproofai --hook ${eventType} --cli codex`
|
|
267
|
+
: `"${binaryPath}" --hook ${eventType} --cli codex`;
|
|
268
|
+
return {
|
|
269
|
+
type: "command",
|
|
270
|
+
command,
|
|
271
|
+
timeout: 60_000,
|
|
272
|
+
[FAILPROOFAI_HOOK_MARKER]: true,
|
|
273
|
+
};
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
isFailproofaiHook: isMarkedHook,
|
|
277
|
+
|
|
278
|
+
writeHookEntries(settings, binaryPath, scope) {
|
|
279
|
+
const s = settings as CodexSettingsFile;
|
|
280
|
+
if (s.version === undefined) s.version = 1;
|
|
281
|
+
if (!s.hooks) s.hooks = {};
|
|
282
|
+
|
|
283
|
+
for (const eventType of CODEX_HOOK_EVENT_TYPES) {
|
|
284
|
+
const pascalKey = CODEX_EVENT_MAP[eventType as CodexHookEventType];
|
|
285
|
+
const hookEntry = this.buildHookEntry(binaryPath, eventType, scope) as unknown as ClaudeHookEntry;
|
|
286
|
+
if (!s.hooks[pascalKey]) s.hooks[pascalKey] = [];
|
|
287
|
+
const matchers: ClaudeHookMatcher[] = s.hooks[pascalKey];
|
|
288
|
+
|
|
289
|
+
let found = false;
|
|
290
|
+
for (const matcher of matchers) {
|
|
291
|
+
if (!matcher.hooks) continue;
|
|
292
|
+
const idx = matcher.hooks.findIndex((h) => isMarkedHook(h as Record<string, unknown>));
|
|
293
|
+
if (idx >= 0) {
|
|
294
|
+
matcher.hooks[idx] = hookEntry;
|
|
295
|
+
found = true;
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (!found) matchers.push({ hooks: [hookEntry] });
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
|
|
303
|
+
removeHooksFromFile(settingsPath) {
|
|
304
|
+
const settings = this.readSettings(settingsPath) as CodexSettingsFile;
|
|
305
|
+
if (!settings.hooks) return 0;
|
|
306
|
+
|
|
307
|
+
let removed = 0;
|
|
308
|
+
for (const eventType of Object.keys(settings.hooks)) {
|
|
309
|
+
const matchers = settings.hooks[eventType];
|
|
310
|
+
if (!Array.isArray(matchers)) continue;
|
|
311
|
+
for (let i = matchers.length - 1; i >= 0; i--) {
|
|
312
|
+
const matcher = matchers[i];
|
|
313
|
+
if (!matcher.hooks) continue;
|
|
314
|
+
const before = matcher.hooks.length;
|
|
315
|
+
matcher.hooks = matcher.hooks.filter((h) => !isMarkedHook(h as Record<string, unknown>));
|
|
316
|
+
removed += before - matcher.hooks.length;
|
|
317
|
+
if (matcher.hooks.length === 0) matchers.splice(i, 1);
|
|
318
|
+
}
|
|
319
|
+
if (matchers.length === 0) delete settings.hooks[eventType];
|
|
320
|
+
}
|
|
321
|
+
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
322
|
+
|
|
323
|
+
this.writeSettings(settingsPath, settings as Record<string, unknown>);
|
|
324
|
+
return removed;
|
|
325
|
+
},
|
|
326
|
+
|
|
327
|
+
hooksInstalledInSettings(scope, cwd) {
|
|
328
|
+
const settingsPath = this.getSettingsPath(scope, cwd);
|
|
329
|
+
if (!existsSync(settingsPath)) return false;
|
|
330
|
+
try {
|
|
331
|
+
const settings = this.readSettings(settingsPath) as CodexSettingsFile;
|
|
332
|
+
if (!settings.hooks) return false;
|
|
333
|
+
for (const matchers of Object.values(settings.hooks)) {
|
|
334
|
+
if (!Array.isArray(matchers)) continue;
|
|
335
|
+
for (const matcher of matchers) {
|
|
336
|
+
if (!matcher.hooks) continue;
|
|
337
|
+
if (matcher.hooks.some((h) => isMarkedHook(h as Record<string, unknown>))) return true;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
} catch {
|
|
341
|
+
// Corrupt settings — treat as not installed
|
|
342
|
+
}
|
|
343
|
+
return false;
|
|
344
|
+
},
|
|
345
|
+
|
|
346
|
+
detectInstalled() {
|
|
347
|
+
return binaryExists("codex");
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
// ── Registry ────────────────────────────────────────────────────────────────
|
|
352
|
+
|
|
353
|
+
const INTEGRATIONS: Record<IntegrationType, Integration> = {
|
|
354
|
+
claude: claudeCode,
|
|
355
|
+
codex,
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
export function getIntegration(id: IntegrationType): Integration {
|
|
359
|
+
const integration = INTEGRATIONS[id];
|
|
360
|
+
if (!integration) {
|
|
361
|
+
throw new Error(`Unknown integration: ${id}. Valid: ${INTEGRATION_TYPES.join(", ")}`);
|
|
362
|
+
}
|
|
363
|
+
return integration;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export function listIntegrations(): Integration[] {
|
|
367
|
+
return INTEGRATION_TYPES.map((id) => INTEGRATIONS[id]);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** Detect which agent CLIs are installed on PATH. */
|
|
371
|
+
export function detectInstalledClis(): IntegrationType[] {
|
|
372
|
+
return INTEGRATION_TYPES.filter((id) => INTEGRATIONS[id].detectInstalled());
|
|
373
|
+
}
|