@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.
Files changed (68) hide show
  1. package/README.md +137 -0
  2. package/dist/agents/antigravity/events.js +42 -0
  3. package/dist/agents/antigravity/hooks.js +59 -0
  4. package/dist/agents/antigravity/index.js +28 -0
  5. package/dist/agents/antigravity/outputs.js +81 -0
  6. package/dist/agents/antigravity/transport.js +84 -0
  7. package/dist/agents/antigravity/types.js +11 -0
  8. package/dist/agents/claude-code/env.js +27 -0
  9. package/dist/agents/claude-code/events.js +87 -0
  10. package/dist/agents/claude-code/hooks.js +1000 -0
  11. package/dist/agents/claude-code/index.js +57 -0
  12. package/dist/agents/claude-code/outputs.js +579 -0
  13. package/dist/agents/claude-code/tool-helpers.js +840 -0
  14. package/dist/agents/claude-code/transport.js +254 -0
  15. package/dist/agents/claude-code/types.js +11 -0
  16. package/dist/agents/codex/cli-support.js +318 -0
  17. package/dist/agents/codex/constants.js +29 -0
  18. package/dist/agents/codex/events.js +68 -0
  19. package/dist/agents/codex/hooks.js +93 -0
  20. package/dist/agents/codex/index.js +21 -0
  21. package/dist/agents/codex/outputs.js +189 -0
  22. package/dist/agents/codex/scaffold.js +273 -0
  23. package/dist/agents/codex/transport.js +139 -0
  24. package/dist/agents/codex/types.js +1 -0
  25. package/dist/cli.js +1357 -0
  26. package/dist/constants.js +39 -0
  27. package/dist/core/define-hook.js +61 -0
  28. package/dist/core/env.js +186 -0
  29. package/dist/core/logger.js +508 -0
  30. package/dist/core/stdin.js +38 -0
  31. package/dist/core/transport.js +231 -0
  32. package/dist/core/types.js +10 -0
  33. package/dist/index.js +17 -0
  34. package/dist/scaffold.js +512 -0
  35. package/package.json +61 -0
  36. package/types/agents/antigravity/events.d.ts +50 -0
  37. package/types/agents/antigravity/hooks.d.ts +76 -0
  38. package/types/agents/antigravity/index.d.ts +27 -0
  39. package/types/agents/antigravity/outputs.d.ts +107 -0
  40. package/types/agents/antigravity/transport.d.ts +47 -0
  41. package/types/agents/antigravity/types.d.ts +58 -0
  42. package/types/agents/claude-code/env.d.ts +27 -0
  43. package/types/agents/claude-code/events.d.ts +69 -0
  44. package/types/agents/claude-code/hooks.d.ts +1182 -0
  45. package/types/agents/claude-code/index.d.ts +23 -0
  46. package/types/agents/claude-code/outputs.d.ts +1160 -0
  47. package/types/agents/claude-code/tool-helpers.d.ts +807 -0
  48. package/types/agents/claude-code/transport.d.ts +119 -0
  49. package/types/agents/claude-code/types.d.ts +557 -0
  50. package/types/agents/codex/cli-support.d.ts +105 -0
  51. package/types/agents/codex/constants.d.ts +10 -0
  52. package/types/agents/codex/events.d.ts +63 -0
  53. package/types/agents/codex/hooks.d.ts +135 -0
  54. package/types/agents/codex/index.d.ts +19 -0
  55. package/types/agents/codex/outputs.d.ts +190 -0
  56. package/types/agents/codex/scaffold.d.ts +14 -0
  57. package/types/agents/codex/transport.d.ts +74 -0
  58. package/types/agents/codex/types.d.ts +131 -0
  59. package/types/cli.d.ts +315 -0
  60. package/types/constants.d.ts +9 -0
  61. package/types/core/define-hook.d.ts +39 -0
  62. package/types/core/env.d.ts +150 -0
  63. package/types/core/logger.d.ts +476 -0
  64. package/types/core/stdin.d.ts +21 -0
  65. package/types/core/transport.d.ts +131 -0
  66. package/types/core/types.d.ts +137 -0
  67. package/types/index.d.ts +18 -0
  68. 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";