@goodfoot/agent-hooks 1.0.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/README.md +137 -0
- package/dist/agents/antigravity/events.js +42 -0
- package/dist/agents/antigravity/hooks.js +59 -0
- package/dist/agents/antigravity/index.js +28 -0
- package/dist/agents/antigravity/outputs.js +81 -0
- package/dist/agents/antigravity/transport.js +84 -0
- package/dist/agents/antigravity/types.js +11 -0
- package/dist/agents/claude-code/env.js +27 -0
- package/dist/agents/claude-code/events.js +87 -0
- package/dist/agents/claude-code/hooks.js +1000 -0
- package/dist/agents/claude-code/index.js +57 -0
- package/dist/agents/claude-code/outputs.js +579 -0
- package/dist/agents/claude-code/tool-helpers.js +840 -0
- package/dist/agents/claude-code/transport.js +254 -0
- package/dist/agents/claude-code/types.js +11 -0
- package/dist/agents/codex/cli-support.js +318 -0
- package/dist/agents/codex/constants.js +29 -0
- package/dist/agents/codex/events.js +68 -0
- package/dist/agents/codex/hooks.js +93 -0
- package/dist/agents/codex/index.js +21 -0
- package/dist/agents/codex/outputs.js +189 -0
- package/dist/agents/codex/scaffold.js +273 -0
- package/dist/agents/codex/transport.js +139 -0
- package/dist/agents/codex/types.js +1 -0
- package/dist/cli.js +1357 -0
- package/dist/constants.js +39 -0
- package/dist/core/define-hook.js +61 -0
- package/dist/core/env.js +186 -0
- package/dist/core/logger.js +508 -0
- package/dist/core/stdin.js +38 -0
- package/dist/core/transport.js +231 -0
- package/dist/core/types.js +10 -0
- package/dist/index.js +17 -0
- package/dist/scaffold.js +512 -0
- package/package.json +61 -0
- package/types/agents/antigravity/events.d.ts +50 -0
- package/types/agents/antigravity/hooks.d.ts +76 -0
- package/types/agents/antigravity/index.d.ts +27 -0
- package/types/agents/antigravity/outputs.d.ts +107 -0
- package/types/agents/antigravity/transport.d.ts +47 -0
- package/types/agents/antigravity/types.d.ts +58 -0
- package/types/agents/claude-code/env.d.ts +27 -0
- package/types/agents/claude-code/events.d.ts +69 -0
- package/types/agents/claude-code/hooks.d.ts +1182 -0
- package/types/agents/claude-code/index.d.ts +23 -0
- package/types/agents/claude-code/outputs.d.ts +1160 -0
- package/types/agents/claude-code/tool-helpers.d.ts +807 -0
- package/types/agents/claude-code/transport.d.ts +119 -0
- package/types/agents/claude-code/types.d.ts +557 -0
- package/types/agents/codex/cli-support.d.ts +105 -0
- package/types/agents/codex/constants.d.ts +10 -0
- package/types/agents/codex/events.d.ts +63 -0
- package/types/agents/codex/hooks.d.ts +135 -0
- package/types/agents/codex/index.d.ts +19 -0
- package/types/agents/codex/outputs.d.ts +190 -0
- package/types/agents/codex/scaffold.d.ts +14 -0
- package/types/agents/codex/transport.d.ts +74 -0
- package/types/agents/codex/types.d.ts +131 -0
- package/types/cli.d.ts +315 -0
- package/types/constants.d.ts +9 -0
- package/types/core/define-hook.d.ts +39 -0
- package/types/core/env.d.ts +150 -0
- package/types/core/logger.d.ts +476 -0
- package/types/core/stdin.d.ts +21 -0
- package/types/core/transport.d.ts +131 -0
- package/types/core/types.d.ts +137 -0
- package/types/index.d.ts +18 -0
- package/types/scaffold.d.ts +46 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport abstraction and the shared driver.
|
|
3
|
+
*
|
|
4
|
+
* Core owns the try/catch skeleton — stdin read, JSON parse, handler
|
|
5
|
+
* invocation, outcome classification, buffered single-write emission, process
|
|
6
|
+
* exit — and has **no knowledge of exit codes or stdout policy**: every wire
|
|
7
|
+
* decision is delegated to the agent's {@link Transport.finalize}, a pure
|
|
8
|
+
* mapping from {@link HookOutcome} to `{ stdout?, stderr?, exitCode }`. Agent
|
|
9
|
+
* transports are data-heavy adapters; they must not re-implement driver logic.
|
|
10
|
+
* @module
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* const transport: Transport<MyOutput> = {
|
|
14
|
+
* finalize: (outcome) => {
|
|
15
|
+
* if (outcome.kind === "response") {
|
|
16
|
+
* return { stdout: JSON.stringify(outcome.output ?? {}), exitCode: 0 };
|
|
17
|
+
* }
|
|
18
|
+
* return { stderr: "blocked", exitCode: 2 };
|
|
19
|
+
* },
|
|
20
|
+
* };
|
|
21
|
+
*
|
|
22
|
+
* await drive(transport, myHook);
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
import { logger } from "./logger.js";
|
|
26
|
+
import { parseStdinJson, readStdin } from "./stdin.js";
|
|
27
|
+
/**
|
|
28
|
+
* Error thrown by a hook handler to signal an intentional block.
|
|
29
|
+
*
|
|
30
|
+
* `drive()` classifies a caught {@link HookBlockError} **before** consulting
|
|
31
|
+
* the `unexpectedError` policy: it always routes to the `block` outcome,
|
|
32
|
+
* under every policy value, so an advisory event's in-flight block decision
|
|
33
|
+
* survives a crash mid-computation. Any other throw is subject to policy as
|
|
34
|
+
* usual. Agents may subclass or re-export this class; each agent's transport
|
|
35
|
+
* owns the wire translation of the `block` outcome.
|
|
36
|
+
*/
|
|
37
|
+
export class HookBlockError extends Error {
|
|
38
|
+
/**
|
|
39
|
+
* Optional structured fields carried alongside the block reason (e.g.
|
|
40
|
+
* extra wire fields the agent's translation may forward).
|
|
41
|
+
*/
|
|
42
|
+
fields;
|
|
43
|
+
/**
|
|
44
|
+
* @param message - The block reason; becomes the error `message`.
|
|
45
|
+
* @param fields - Optional additional structured fields.
|
|
46
|
+
*/
|
|
47
|
+
constructor(message, fields) {
|
|
48
|
+
super(message);
|
|
49
|
+
this.name = "HookBlockError";
|
|
50
|
+
this.fields = fields;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Exit code used only if a transport's finalize itself throws under the default policy. */
|
|
54
|
+
const FALLBACK_EXIT_ERROR = 1;
|
|
55
|
+
/** Exit code used only if a transport's finalize itself throws under the "continue" policy. */
|
|
56
|
+
const FALLBACK_EXIT_SUCCESS = 0;
|
|
57
|
+
/**
|
|
58
|
+
* Reports an unexpected error through the caller's diagnostic sink and the
|
|
59
|
+
* runtime logger. Both are best-effort: this function never throws, so a
|
|
60
|
+
* broken sink or logger cannot itself fail the "continue" policy it backs.
|
|
61
|
+
* @param onUnexpectedError - The caller's optional diagnostic callback
|
|
62
|
+
* @param error - The unexpected error
|
|
63
|
+
* @param phase - The runtime phase in which the error occurred
|
|
64
|
+
*/
|
|
65
|
+
function reportUnexpectedError(onUnexpectedError, error, phase) {
|
|
66
|
+
try {
|
|
67
|
+
onUnexpectedError?.(error, phase);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// Diagnostic sinks must not recursively fail the invocation.
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
logger.logError(error, `Unexpected error in ${phase} phase (fail-open)`, { phase });
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// The logger itself may be the thing that failed (e.g. log file write).
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Clears logger context and closes any open log file, surfacing cleanup
|
|
81
|
+
* failures through the same policy path as both source runtimes.
|
|
82
|
+
*/
|
|
83
|
+
function cleanup(policy, onUnexpectedError) {
|
|
84
|
+
try {
|
|
85
|
+
logger.clearContext();
|
|
86
|
+
logger.close();
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
if (policy !== "continue") {
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
reportUnexpectedError(onUnexpectedError, error, "cleanup");
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Classifies a caught value into an outcome.
|
|
97
|
+
*
|
|
98
|
+
* Precedence is the mechanism of record: `error instanceof HookBlockError`
|
|
99
|
+
* routes to the `block` outcome **before** policy is consulted, mirroring the
|
|
100
|
+
* precedence codex-hooks' runtime already gives `BlockError`. Every other
|
|
101
|
+
* throw is subject to policy: `"continue"` swallows it into the empty
|
|
102
|
+
* response; the default `"error"` policy yields `handlerError`.
|
|
103
|
+
*/
|
|
104
|
+
function classify(error, phase, policy, onUnexpectedError) {
|
|
105
|
+
if (error instanceof HookBlockError) {
|
|
106
|
+
return { kind: "block", error };
|
|
107
|
+
}
|
|
108
|
+
if (policy === "continue") {
|
|
109
|
+
reportUnexpectedError(onUnexpectedError, error, phase);
|
|
110
|
+
return { kind: "response", output: undefined };
|
|
111
|
+
}
|
|
112
|
+
return { kind: "handlerError", error, phase };
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Writes an unexpected (non-handler, runtime-owned) error's stacktrace to
|
|
116
|
+
* stderr. Used for cleanup/finalize failures under the default `"error"`
|
|
117
|
+
* policy, which exit with code 1 — these are not the handler's own
|
|
118
|
+
* intentional signal.
|
|
119
|
+
*/
|
|
120
|
+
function writeUnexpectedErrorStderr(error) {
|
|
121
|
+
if (error instanceof Error) {
|
|
122
|
+
process.stderr.write(`${error.stack ?? error.message}\n`);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
process.stderr.write(`${String(error)}\n`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Last-resort logger shutdown used only when the transport's finalize itself
|
|
130
|
+
* threw — a broken mapping must not mask the original failure.
|
|
131
|
+
*/
|
|
132
|
+
function cleanupQuietly() {
|
|
133
|
+
try {
|
|
134
|
+
logger.clearContext();
|
|
135
|
+
logger.close();
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// Nothing left to do; exiting immediately after.
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Runs one hook invocation end to end: reads stdin, parses JSON, invokes the
|
|
143
|
+
* handler, classifies the outcome, maps it once through the transport, writes
|
|
144
|
+
* the resulting streams buffered (at most one write per stream), and exits
|
|
145
|
+
* with the transport's exit code.
|
|
146
|
+
*
|
|
147
|
+
* Malformed/unreadable stdin is classified at its phase (`read`/`parse`) like
|
|
148
|
+
* any other unexpected failure and logged identically to both source
|
|
149
|
+
* runtimes; each agent's transport decides the wire shape for that
|
|
150
|
+
* classification.
|
|
151
|
+
*
|
|
152
|
+
* @param transport - The agent's wire adapter.
|
|
153
|
+
* @param hookFn - The hook function to execute (from a factory built on
|
|
154
|
+
* {@link defineHook}).
|
|
155
|
+
* @returns Never resolves normally: the process exits inside this function.
|
|
156
|
+
*/
|
|
157
|
+
export async function drive(transport, hookFn) {
|
|
158
|
+
const policy = hookFn.unexpectedError ?? "error";
|
|
159
|
+
const onUnexpectedError = hookFn.onUnexpectedError;
|
|
160
|
+
const outcome = await (async () => {
|
|
161
|
+
let stdinContent;
|
|
162
|
+
try {
|
|
163
|
+
stdinContent = await readStdin();
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
logger.logError(error, "Failed to read stdin");
|
|
167
|
+
return classify(error, "read", policy, onUnexpectedError);
|
|
168
|
+
}
|
|
169
|
+
let input;
|
|
170
|
+
try {
|
|
171
|
+
input = parseStdinJson(stdinContent);
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
logger.logError(error, "Failed to parse stdin JSON");
|
|
175
|
+
return classify(error, "parse", policy, onUnexpectedError);
|
|
176
|
+
}
|
|
177
|
+
logger.setContext(hookFn.eventName, input);
|
|
178
|
+
const context = hookFn.createContext?.(input) ?? { logger };
|
|
179
|
+
try {
|
|
180
|
+
const result = await hookFn(input, context);
|
|
181
|
+
if (result === null || result === undefined) {
|
|
182
|
+
return { kind: "response", output: undefined };
|
|
183
|
+
}
|
|
184
|
+
const raw = transport.rawStdout?.(result);
|
|
185
|
+
return raw !== undefined ? { kind: "rawStdout", stdout: raw } : { kind: "response", output: result };
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
return classify(error, "handler", policy, onUnexpectedError);
|
|
189
|
+
}
|
|
190
|
+
})();
|
|
191
|
+
let finalized;
|
|
192
|
+
try {
|
|
193
|
+
finalized = transport.finalize(outcome);
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
if (policy === "continue") {
|
|
197
|
+
reportUnexpectedError(onUnexpectedError, error, "serialize");
|
|
198
|
+
cleanupQuietly();
|
|
199
|
+
process.exit(FALLBACK_EXIT_SUCCESS);
|
|
200
|
+
}
|
|
201
|
+
writeUnexpectedErrorStderr(error);
|
|
202
|
+
cleanupQuietly();
|
|
203
|
+
process.exit(FALLBACK_EXIT_ERROR);
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
cleanup(policy, onUnexpectedError);
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
writeUnexpectedErrorStderr(error);
|
|
210
|
+
process.exit(FALLBACK_EXIT_ERROR);
|
|
211
|
+
}
|
|
212
|
+
if (finalized.stderr !== undefined) {
|
|
213
|
+
process.stderr.write(finalized.stderr);
|
|
214
|
+
}
|
|
215
|
+
if (finalized.stdout !== undefined) {
|
|
216
|
+
try {
|
|
217
|
+
process.stdout.write(finalized.stdout);
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
if (policy === "continue") {
|
|
221
|
+
reportUnexpectedError(onUnexpectedError, error, "write");
|
|
222
|
+
cleanupQuietly();
|
|
223
|
+
process.exit(FALLBACK_EXIT_SUCCESS);
|
|
224
|
+
}
|
|
225
|
+
writeUnexpectedErrorStderr(error);
|
|
226
|
+
cleanupQuietly();
|
|
227
|
+
process.exit(FALLBACK_EXIT_ERROR);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
process.exit(finalized.exitCode);
|
|
231
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared core types for `@goodfoot/agent-hooks`.
|
|
3
|
+
*
|
|
4
|
+
* Core is agent-neutral: it owns the policy vocabulary, the hook-function
|
|
5
|
+
* shape, and the context contract, but never an event list — each agent
|
|
6
|
+
* (Claude Code, Codex, Antigravity) injects its own event names and its own
|
|
7
|
+
* advisory allow-list through {@link HookPolicyGate}.
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@goodfoot/agent-hooks` root export.
|
|
3
|
+
*
|
|
4
|
+
* Exposes the agent-neutral core only: the {@link defineHook} factory
|
|
5
|
+
* primitive, the transport/driver pair ({@link Transport}, {@link drive}),
|
|
6
|
+
* {@link HookBlockError}, the Logger, env utilities, and stdin helpers.
|
|
7
|
+
*
|
|
8
|
+
* Per-agent entry points (`@goodfoot/agent-hooks/claude-code`, `/codex`,
|
|
9
|
+
* `/antigravity`) are added to the package `exports` map only in the step
|
|
10
|
+
* that lands their implementation — never pre-declared.
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
export { defineHook } from "./core/define-hook.js";
|
|
14
|
+
export { CLAUDE_ENV_VARS, getEnvFilePath, getProjectDir, isRemoteEnvironment, persistEnvVar, persistEnvVars, } from "./core/env.js";
|
|
15
|
+
export { LOG_LEVELS, Logger, logger, } from "./core/logger.js";
|
|
16
|
+
export { parseStdinJson, readStdin } from "./core/stdin.js";
|
|
17
|
+
export { HookBlockError, } from "./core/transport.js";
|