@goodfoot/agent-hooks 1.0.0 → 1.0.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/README.md CHANGED
@@ -1,27 +1,190 @@
1
1
  # `@goodfoot/agent-hooks`
2
2
 
3
- Unified agent hooks library for AI coding CLIs. One package consolidating the
4
- runtime previously split across `@goodfoot/claude-code-hooks` and
5
- `@goodfoot/codex-hooks`, with per-agent entry points.
3
+ **Build AI coding agent hooks in TypeScript — one package, any supported agent.**
6
4
 
7
- **Status: `0.1.0` — core only.** This release ships the agent-neutral core:
8
- the shared driver, transport abstraction, hook factory primitive, `Logger`,
9
- and env utilities. Per-agent entry points (`@goodfoot/agent-hooks/claude-code`,
10
- `/codex`, `/antigravity`) do not exist yet; they are added to the package
11
- `exports` map only in the version that lands their implementation — never
12
- pre-declared.
5
+ `@goodfoot/agent-hooks` is the unified successor to `@goodfoot/claude-code-hooks` and
6
+ `@goodfoot/codex-hooks`: a shared agent-neutral core (hook factory primitive, transport
7
+ driver, `Logger`, env/stdin utilities) with a per-agent entry point on top. Write
8
+ TypeScript hooks, compile them with the bundled CLI, and run the resulting
9
+ self-contained executables under Claude Code or Codex.
13
10
 
14
- ## Exports map policy
11
+ **Status: `1.0.0`.** Three entry points ship and are published: `@goodfoot/agent-hooks/claude-code`
12
+ (full parity with `@goodfoot/claude-code-hooks`), `@goodfoot/agent-hooks/codex` (full
13
+ parity with `@goodfoot/codex-hooks`), and `@goodfoot/agent-hooks/opencode` (native support
14
+ for OpenCode's in-process plugin model — see [OpenCode](#opencode) below). Antigravity CLI
15
+ support exists internally as typed factories, output builders, and a conformance matrix,
16
+ but is **not** part of this release's public surface — there is no
17
+ `@goodfoot/agent-hooks/antigravity` export yet, and `--agent antigravity` is rejected by
18
+ the CLI. It ships once a non-interactive, authenticated Antigravity CLI exists to
19
+ validate against.
15
20
 
16
- The `exports` map is an explicit, closed list of subpaths. It deliberately
17
- does **not** include a `./*` wildcard subpath (unlike the source packages):
18
- a wildcard would make every internal module independently importable,
19
- defeating the root-export lint. This is a permanent rule enforced by the
20
- package's own test suite (`tests/export-surface.test.ts`), which also carries
21
- a fail-case fixture proving the check rejects a wildcard entry.
21
+ ## Install
22
+
23
+ ```bash
24
+ yarn add @goodfoot/agent-hooks
25
+ # or npm install, pnpm, etc.
26
+ ```
27
+
28
+ ## Quick Start
29
+
30
+ ### 1. Write a hook
31
+
32
+ Import factories from your target agent's entry point. **Note:** you must use
33
+ `export default` and the factory function — the CLI's build step relies on it.
34
+
35
+ Claude Code (`@goodfoot/agent-hooks/claude-code`):
36
+
37
+ ```typescript
38
+ import { preToolUseHook, preToolUseOutput } from '@goodfoot/agent-hooks/claude-code';
39
+
40
+ export default preToolUseHook({ matcher: 'Bash' }, async (input, { logger }) => {
41
+ const { command } = input.tool_input as { command: string };
42
+
43
+ // Use logger, NEVER console.log
44
+ logger.info('Checking command', { command });
45
+
46
+ if (command.trim() === 'ls') {
47
+ return preToolUseOutput({
48
+ systemMessage: 'Auto-approved: ls command is safe.',
49
+ hookSpecificOutput: { permissionDecision: 'allow' },
50
+ });
51
+ }
52
+
53
+ return preToolUseOutput({ systemMessage: 'Command passed through for review.' });
54
+ });
55
+ ```
56
+
57
+ Codex (`@goodfoot/agent-hooks/codex`):
58
+
59
+ ```typescript
60
+ import { preToolUseHook, preToolUseOutput } from '@goodfoot/agent-hooks/codex';
61
+
62
+ export default preToolUseHook(async (input, { logger }) => {
63
+ logger.info('Checking tool call', { tool: input.tool_name });
64
+ return preToolUseOutput({});
65
+ });
66
+ ```
67
+
68
+ ### 2. Compile
69
+
70
+ The `--agent` flag is required and never inferred — always pass it explicitly:
71
+
72
+ ```bash
73
+ npx -y @goodfoot/agent-hooks --agent claude-code -i "hooks/**/*.ts" -o "./dist/hooks.json"
74
+ npx -y @goodfoot/agent-hooks --agent codex -i "src/**/*.ts" -o ".codex/hooks.json"
75
+ ```
76
+
77
+ Or scaffold a starter project:
78
+
79
+ ```bash
80
+ npx -y @goodfoot/agent-hooks --agent claude-code --scaffold ./my-hooks --hooks Stop,SubagentStop -o dist/hooks.json
81
+ ```
82
+
83
+ Run `npx -y @goodfoot/agent-hooks --help` for the full flag reference, including
84
+ Codex-specific options (e.g. `--plugin-root`), `--log`/`--log-env-var`, `--executable`,
85
+ and `--loader`.
86
+
87
+ ### 3. Run
88
+
89
+ The compiled output is a self-contained executable per hook; point your agent's hook
90
+ configuration (Claude Code's `hooks.json`, Codex's plugin `hooks.json`) at the compiled
91
+ files, same as with the deprecated per-agent packages.
92
+
93
+ ## Hook events
94
+
95
+ Each entry point exports one factory function and one matching output builder per
96
+ supported event. Import both from the same entry point (`@goodfoot/agent-hooks/claude-code`
97
+ or `@goodfoot/agent-hooks/codex`) — do not mix events or output builders across agents.
98
+
99
+ - **Claude Code** (`src/agents/claude-code`): the full 30-factory surface ported from
100
+ `@goodfoot/claude-code-hooks`, including `preToolUseHook`, `postToolUseHook`,
101
+ `sessionStartHook`, `notificationHook`, `permissionRequestHook`, `elicitationHook`,
102
+ task/teammate hooks, `worktreeCreateHook`, and the `tool-helpers` type guards
103
+ (`isBashTool`, `isEditTool`, …).
104
+ - **Codex** (`src/agents/codex`): the 10-factory surface ported from
105
+ `@goodfoot/codex-hooks`, including `preToolUseHook`, `postToolUseHook`,
106
+ `sessionStartHook`, `stopHook`, `subagentStartHook`/`subagentStopHook`,
107
+ `userPromptSubmitHook`, `preCompactHook`/`postCompactHook`, and `permissionRequestHook`.
108
+
109
+ ## OpenCode
110
+
111
+ OpenCode's plugin model is a long-lived, in-process JS module with a stable callback map
112
+ (`Hooks`) — not the one-shot stdin/stdout command-hook model Claude Code and Codex use, so
113
+ `@goodfoot/agent-hooks/opencode` does **not** route through `defineHook`/`Transport`/`drive`.
114
+ It ships its own primitives instead:
115
+
116
+ ```typescript
117
+ import { defineOpenCodePlugin, guardAdvisory, createRootSessionRegistry } from '@goodfoot/agent-hooks/opencode';
118
+
119
+ const sessions = createRootSessionRegistry();
120
+
121
+ export default defineOpenCodePlugin({
122
+ id: 'my-plugin',
123
+ server: async ({ client }) => ({
124
+ event: guardAdvisory('event', async ({ event }) => {
125
+ if (event.type === 'session.created') {
126
+ sessions.observe(event.properties.info.id, event.properties.info.parentID);
127
+ } else {
128
+ sessions.observeResumed(event.properties.info?.id ?? '');
129
+ }
130
+ }, 'continue'),
131
+
132
+ 'tool.execute.before': async (input, output) => {
133
+ if (input.tool === 'bash' && sessions.isRoot(input.sessionID)) {
134
+ output.args = { ...output.args, timeout: 30_000 };
135
+ }
136
+ },
137
+ }),
138
+ });
139
+ ```
140
+
141
+ - **`defineOpenCodePlugin({ id, server })`** validates and returns the `{ id, server }`
142
+ module shape OpenCode's real loader (`getServerPlugin`) accepts as a default export —
143
+ a bare `server` function also satisfies the loader and needs no wrapper.
144
+ - **`guardAdvisory(name, handler, policy, onError?)`** applies the same opt-in
145
+ `"continue"` fail-open policy described below, restricted at the type level to
146
+ *advisory* callback names — `permission.ask` is the one policy-enforcing callback
147
+ (a real allow/deny security decision) and is rejected at compile time, since silently
148
+ swallowing its failure would skip that decision instead of making it.
149
+ - **`createRootSessionRegistry()`** tracks session parentage and resumption across the
150
+ plugin's lifetime: a resumed session does not necessarily re-emit `session.created`, so
151
+ `observe`/`observeResumed` record first-seen-event distinctly and `isRoot`/`isResumed`
152
+ answer from that record.
153
+
154
+ Compile with the same unified CLI, but `--agent opencode` treats `-o`/`--output` as a
155
+ plugin-artifact **directory**, not a manifest path — OpenCode config references built
156
+ files directly, so there is no `hooks.json`-equivalent manifest or shebang wrapper:
157
+
158
+ ```bash
159
+ npx -y @goodfoot/agent-hooks --agent opencode -i "src/**/*.ts" -o "./.opencode/plugin"
160
+ ```
161
+
162
+ Point `opencode.json`'s plugin list (or the equivalent config in your OpenCode host) at
163
+ the compiled `.mjs` file(s) under that output directory.
164
+
165
+ `@opencode-ai/plugin` is an **optional peer dependency** — only required if you import
166
+ `@goodfoot/agent-hooks/opencode`; Claude Code and Codex hook authors never need it
167
+ installed.
168
+
169
+ ## Fail-open advisory hooks
170
+
171
+ Both entry points support the same opt-in `unexpectedError: "continue"` hook config used
172
+ by the deprecated packages: unexpected runtime failures (stdin read, parse, handler
173
+ execution, output serialization, stdout write, logger cleanup) are swallowed into the
174
+ empty response instead of surfacing a failed-hook error. It is rejected — at the type
175
+ level and at runtime — for events where fail-open would be unsafe (`PreToolUse`,
176
+ `PermissionRequest`, blocking `Stop`/`SubagentStop`, `WorktreeCreate`/`WorktreeRemove` on
177
+ Claude Code; permission/blocking events on Codex). A thrown `HookBlockError` always wins
178
+ over this policy and still blocks.
22
179
 
23
180
  ## Core API
24
181
 
182
+ The root export (`@goodfoot/agent-hooks`, no subpath) exposes only the agent-neutral
183
+ core that each entry point is built on. It has no knowledge of exit codes or stdout
184
+ policy — that's owned entirely by each agent's transport. Use this if you're extending
185
+ an existing entry point or adding support for a new agent; hook authors targeting
186
+ Claude Code or Codex should import from the per-agent entry point instead.
187
+
25
188
  ### `defineHook(eventName, config, handler, policyGate?)`
26
189
 
27
190
  Factory primitive every agent's per-event factory builds on:
@@ -127,6 +290,27 @@ Structured logging ported behavior-identically from the source packages:
127
290
  silent by default, never writes stdout/stderr, optional JSON Lines file output
128
291
  via `AGENT_HOOKS_LOG_FILE`, event subscription via `logger.on(level, handler)`.
129
292
 
293
+ ## Exports map policy
294
+
295
+ The `exports` map is an explicit, closed list of subpaths. It deliberately
296
+ does **not** include a `./*` wildcard subpath (unlike the deprecated source
297
+ packages): a wildcard would make every internal module independently importable,
298
+ defeating the root-export lint. This is a permanent rule enforced by the
299
+ package's own test suite (`tests/export-surface.test.ts`), which also carries
300
+ a fail-case fixture proving the check rejects a wildcard entry.
301
+
302
+ ## Migrating from `@goodfoot/claude-code-hooks` / `@goodfoot/codex-hooks`
303
+
304
+ Both source packages are deprecated in favor of this one. Migration is a dependency
305
+ swap, not a behavior change:
306
+
307
+ 1. Replace the dependency: `@goodfoot/claude-code-hooks` → `@goodfoot/agent-hooks`, or
308
+ `@goodfoot/codex-hooks` → `@goodfoot/agent-hooks`.
309
+ 2. Update imports to the matching entry point: `@goodfoot/agent-hooks/claude-code` or
310
+ `@goodfoot/agent-hooks/codex`. Factory and output-builder names are unchanged.
311
+ 3. Update `build` scripts and any committed `hooks.json` to invoke the `agent-hooks` CLI
312
+ binary with the required `--agent <claude-code|codex>` flag.
313
+
130
314
  ## Development
131
315
 
132
316
  ```bash
@@ -54,7 +54,7 @@ function generatePackageJson(projectName, outputPath) {
54
54
  typecheck: "tsc --noEmit",
55
55
  },
56
56
  dependencies: {
57
- [PACKAGE_NAME]: "^1.0.0",
57
+ [PACKAGE_NAME]: "^1.0.2",
58
58
  },
59
59
  devDependencies: {
60
60
  "@biomejs/biome": "2.4.9",
@@ -0,0 +1,119 @@
1
+ /**
2
+ * OpenCode-specific CLI internals: bundling each selected plugin entry to a
3
+ * standalone ESM artifact and validating its `{ id, server }` (or bare
4
+ * function) default-export contract against a real dynamic import of the
5
+ * built output — not a hand-rolled AST check, which could pass a shape the
6
+ * actual OpenCode loader would still reject.
7
+ *
8
+ * Unlike Codex's `compileCodexHook`, there is no wrapper content and no
9
+ * `execute()` runtime module to link: OpenCode imports the plugin author's
10
+ * own default export directly, so the bundle *is* the artifact — no
11
+ * stdin/stdout driver, no `hooks.json` manifest, no shebang.
12
+ *
13
+ * Driver-level primitives (loader parsing, glob discovery) are imported from
14
+ * the shared CLI module — never re-derived here.
15
+ * @module
16
+ */
17
+ import * as fs from "node:fs";
18
+ import * as path from "node:path";
19
+ import { pathToFileURL } from "node:url";
20
+ import * as esbuild from "esbuild";
21
+ import { buildEsbuildLoaderMap, discoverHookSourceFiles, parseEsbuildLoaderFlag } from "../../cli.js";
22
+ /**
23
+ * Bundles one OpenCode plugin entry to a self-contained ESM artifact. The
24
+ * source file's own default export is preserved unchanged — no wrapper.
25
+ */
26
+ export async function compileOpenCodePlugin(sourcePath, loaders, sourcemap = true) {
27
+ const result = await esbuild.build({
28
+ entryPoints: [sourcePath],
29
+ loader: loaders,
30
+ format: "esm",
31
+ platform: "node",
32
+ target: "node20",
33
+ bundle: true,
34
+ sourcemap: sourcemap ? "inline" : false,
35
+ write: false,
36
+ external: ["node:*", "fs", "path", "os", "crypto", "module", "url"],
37
+ mainFields: ["module", "main"],
38
+ conditions: ["import", "node"],
39
+ preserveSymlinks: true,
40
+ });
41
+ const content = result.outputFiles?.[0]?.text;
42
+ if (content === undefined) {
43
+ throw new Error(`esbuild produced no output for ${sourcePath}`);
44
+ }
45
+ return content;
46
+ }
47
+ /**
48
+ * Validates that `outputPath`'s default export matches OpenCode's real
49
+ * loader contract (`getServerPlugin`): either a bare function, or an object
50
+ * with a `server` function property. Imports the built artifact itself
51
+ * rather than statically analyzing source, so the check exercises exactly
52
+ * what OpenCode's loader will see.
53
+ * @throws Error when the default export matches neither shape.
54
+ */
55
+ export async function validateOpenCodePluginModule(outputPath) {
56
+ const imported = await import(pathToFileURL(outputPath).href);
57
+ const defaultExport = imported.default;
58
+ const isBareFunction = typeof defaultExport === "function";
59
+ const isServerModule = typeof defaultExport === "object" &&
60
+ defaultExport !== null &&
61
+ typeof defaultExport.server === "function";
62
+ if (!isBareFunction && !isServerModule) {
63
+ throw new Error(`${outputPath}: default export must be a function or a { server } object (OpenCode's loader contract), got ${typeof defaultExport}`);
64
+ }
65
+ }
66
+ async function compileAllOpenCodePlugins(pluginFiles, outputDir, loaders, sourcemap) {
67
+ if (!fs.existsSync(outputDir)) {
68
+ fs.mkdirSync(outputDir, { recursive: true });
69
+ }
70
+ const compiled = [];
71
+ for (const sourcePath of pluginFiles) {
72
+ const content = await compileOpenCodePlugin(sourcePath, loaders, sourcemap);
73
+ const baseName = path.basename(sourcePath, path.extname(sourcePath));
74
+ const outputPath = path.join(outputDir, `${baseName}.mjs`);
75
+ fs.writeFileSync(outputPath, content, { encoding: "utf-8" });
76
+ try {
77
+ await validateOpenCodePluginModule(outputPath);
78
+ }
79
+ catch (error) {
80
+ fs.rmSync(outputPath);
81
+ throw error;
82
+ }
83
+ compiled.push({ sourcePath, outputPath });
84
+ }
85
+ return compiled;
86
+ }
87
+ /**
88
+ * Validates OpenCode build arguments beyond the shared `--agent` gate: `-i`
89
+ * and `-o` required, loader flags well-formed.
90
+ */
91
+ export function validateOpenCodeArgs(args) {
92
+ if (args.input === "") {
93
+ return "Missing required argument: -i/--input";
94
+ }
95
+ if (args.output === "") {
96
+ return "Missing required argument: -o/--output";
97
+ }
98
+ for (const loaderFlag of args.loaderFlags) {
99
+ if (parseEsbuildLoaderFlag(loaderFlag) === undefined) {
100
+ return `Invalid --loader value: ${loaderFlag}. Expected .ext=type`;
101
+ }
102
+ }
103
+ return undefined;
104
+ }
105
+ /**
106
+ * Runs the OpenCode build branch of the unified CLI: discovers plugin entry
107
+ * files matching `--input`, bundles each to a standalone ESM artifact under
108
+ * `--output` (treated as a directory — OpenCode plugin config references
109
+ * these files directly, there is no manifest), and validates each artifact's
110
+ * default export against OpenCode's real loader contract.
111
+ */
112
+ export async function runOpenCodeCli(args) {
113
+ const cwd = process.cwd();
114
+ const pluginFiles = await discoverHookSourceFiles(args.input, cwd);
115
+ const outputDir = path.resolve(cwd, args.output);
116
+ const loaders = buildEsbuildLoaderMap(args.loaderFlags);
117
+ const sourcemap = args.sourcemap ?? true;
118
+ return compileAllOpenCodePlugins(pluginFiles, outputDir, loaders, sourcemap);
119
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * OpenCode callback policy split: which lifecycle callbacks may opt into
3
+ * guarded fail-open (`"continue"`) behavior, and which must always surface
4
+ * failures.
5
+ *
6
+ * Unlike Codex's advisory allow-list (transcribed from `codex-hooks/README.md`,
7
+ * an external source-of-truth document), OpenCode has no prior source package
8
+ * to transcribe from — this package *is* the source of truth for its own
9
+ * OpenCode surface. The split below is derived directly from CARD.md's
10
+ * Desired Functionality section (committed to the card repository, itself
11
+ * the authorization for this contract): "Advisory callbacks can opt into
12
+ * guarded fail-open behavior with observable diagnostics; policy-enforcing
13
+ * callbacks default to surfacing failures."
14
+ *
15
+ * `permission.ask` is the only callback whose `output.status` is an
16
+ * allow/deny/ask *security decision* — swallowing a failure there means the
17
+ * decision was silently skipped, exactly the Codex precedent
18
+ * (`PreToolUse`/`PermissionRequest`) this mirrors. Every other lifecycle
19
+ * callback only observes or additively enriches (message/tool mutation,
20
+ * shell env, event notification) and is therefore advisory.
21
+ * @module
22
+ */
23
+ import { OPENCODE_HOOK_NAMES } from "./types.js";
24
+ /** Callbacks that must always surface failures — never eligible for `"continue"`. */
25
+ export const POLICY_ENFORCING_EVENTS = ["permission.ask"];
26
+ /**
27
+ * The advisory allow-list: every OpenCode lifecycle callback except
28
+ * {@link POLICY_ENFORCING_EVENTS}. Only these accept
29
+ * `unexpectedError: "continue"`, enforced at compile time by
30
+ * {@link AllowedUnexpectedErrorPolicy} and at call time by
31
+ * {@link ../../core/define-hook.ts|the policy-gate pattern}.
32
+ */
33
+ export const ADVISORY_EVENTS = OPENCODE_HOOK_NAMES.filter((name) => !POLICY_ENFORCING_EVENTS.includes(name));
@@ -0,0 +1,15 @@
1
+ /**
2
+ * `@goodfoot/agent-hooks/opencode` — the OpenCode agent surface.
3
+ *
4
+ * Re-exports OpenCode-native types (narrowed from `@opencode-ai/plugin`),
5
+ * the advisory/policy-enforcing callback split, the shared error-policy
6
+ * vocabulary, and the plugin authoring primitives. No default export at this
7
+ * subpath — `defineOpenCodePlugin`'s *return value* is the `{ id, server }`
8
+ * default export a plugin author's own module produces, not something this
9
+ * barrel exports itself.
10
+ * @module
11
+ */
12
+ export { ADVISORY_EVENTS, POLICY_ENFORCING_EVENTS } from "./events.js";
13
+ export { createRootSessionRegistry, defineOpenCodePlugin, guardAdvisory } from "./plugin.js";
14
+ export { applyOpenCodeErrorPolicy } from "./policy.js";
15
+ export { OPENCODE_HOOK_NAMES } from "./types.js";
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Plugin authoring primitives for `@goodfoot/agent-hooks/opencode`.
3
+ *
4
+ * `defineOpenCodePlugin` validates the `{ id, server }` module contract that
5
+ * OpenCode's real loader (`getServerPlugin`, verified live against
6
+ * `sst/opencode`'s `packages/opencode/src/plugin/index.ts`) accepts as a
7
+ * default export. `createRootSessionRegistry` and `guardAdvisory` are
8
+ * optional composition helpers a `server` factory may call — not a required
9
+ * framework, since a plugin author can also return a raw {@link Hooks}
10
+ * object built directly from `./types.js`.
11
+ * @module
12
+ */
13
+ import { applyOpenCodeErrorPolicy } from "./policy.js";
14
+ /**
15
+ * Validates and returns an OpenCode plugin module definition.
16
+ *
17
+ * @param definition - `id` must be a non-empty string; `server` must be a
18
+ * function matching upstream's {@link Plugin} signature.
19
+ * @returns `definition`, unchanged, once validated.
20
+ * @throws Error when `id` is missing/empty or `server` is not a function.
21
+ */
22
+ export function defineOpenCodePlugin(definition) {
23
+ if (typeof definition.id !== "string" || definition.id.length === 0) {
24
+ throw new Error(`defineOpenCodePlugin: "id" must be a non-empty string, got ${JSON.stringify(definition.id)}`);
25
+ }
26
+ if (typeof definition.server !== "function") {
27
+ throw new Error(`defineOpenCodePlugin: "server" must be a function, got ${typeof definition.server}`);
28
+ }
29
+ return { id: definition.id, server: definition.server };
30
+ }
31
+ /** Creates a fresh, empty {@link RootSessionRegistry}. */
32
+ export function createRootSessionRegistry() {
33
+ const parentBySessionId = new Map();
34
+ const resumedSessionIds = new Set();
35
+ const recordIfUnseen = (sessionId, parentId, resumed) => {
36
+ if (parentBySessionId.has(sessionId)) {
37
+ return;
38
+ }
39
+ parentBySessionId.set(sessionId, parentId);
40
+ if (resumed) {
41
+ resumedSessionIds.add(sessionId);
42
+ }
43
+ };
44
+ return {
45
+ observe(sessionId, parentId) {
46
+ recordIfUnseen(sessionId, parentId, false);
47
+ },
48
+ observeResumed(sessionId, parentId) {
49
+ recordIfUnseen(sessionId, parentId, true);
50
+ },
51
+ isRoot(sessionId) {
52
+ return parentBySessionId.get(sessionId) === undefined;
53
+ },
54
+ isResumed(sessionId) {
55
+ return resumedSessionIds.has(sessionId);
56
+ },
57
+ };
58
+ }
59
+ /**
60
+ * Wraps an advisory OpenCode callback with {@link applyOpenCodeErrorPolicy},
61
+ * restricted at the type level to {@link AdvisoryEventName}s — a
62
+ * policy-enforcing callback name is a compile-time error here, since
63
+ * swallowing its failure would silently skip a security decision.
64
+ *
65
+ * @param name - An advisory callback name from `./types.js`'s `Hooks` map.
66
+ * @param handler - The callback implementation to guard.
67
+ * @param policy - The configured policy; `undefined` behaves as `"error"`.
68
+ * @param onError - Best-effort diagnostic sink for a swallowed `"continue"` error.
69
+ */
70
+ export function guardAdvisory(name, handler, policy, onError) {
71
+ return applyOpenCodeErrorPolicy(name, handler, policy, onError);
72
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * OpenCode error-policy application.
3
+ *
4
+ * Reuses only the host-neutral policy vocabulary from
5
+ * {@link ../../core/types.ts} — not {@link ../../core/define-hook.ts|defineHook}
6
+ * itself, which is built around a single one-shot handler call and doesn't
7
+ * fit OpenCode's long-lived, heterogeneous callback map.
8
+ * @module
9
+ */
10
+ /**
11
+ * Wraps an advisory OpenCode callback so an unexpected throw is handled
12
+ * per `policy` instead of propagating to the OpenCode server process.
13
+ *
14
+ * - `"error"` (default): rethrows unchanged.
15
+ * - `"continue"`: swallows the error, invokes `onError` (if provided) as a
16
+ * best-effort diagnostic sink, and resolves instead of rejecting.
17
+ *
18
+ * Never call this for a policy-enforcing callback (see
19
+ * {@link ./events.ts|POLICY_ENFORCING_EVENTS}) — {@link ./plugin.ts|guardAdvisory}
20
+ * is the only intended caller and restricts callback names accordingly.
21
+ *
22
+ * @param name - The callback name, used only for the diagnostic sink.
23
+ * @param handler - The callback implementation to guard.
24
+ * @param policy - The configured policy; `undefined` behaves as `"error"`.
25
+ * @param onError - Best-effort diagnostic sink for a swallowed `"continue"` error.
26
+ * @throws Error the handler threw is thrown when `policy` is `"error"`.
27
+ */
28
+ export function applyOpenCodeErrorPolicy(_name, handler, policy, onError) {
29
+ if (policy !== "continue") {
30
+ return handler;
31
+ }
32
+ return async (...args) => {
33
+ try {
34
+ await handler(...args);
35
+ }
36
+ catch (error) {
37
+ onError?.(error, "handler");
38
+ }
39
+ };
40
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * OpenCode-native types for `@goodfoot/agent-hooks/opencode`.
3
+ *
4
+ * OpenCode loads a long-lived, in-process plugin module rather than spawning
5
+ * a one-shot command-hook process: {@link PluginModule} (`{ id?, server }`)
6
+ * or a bare {@link Plugin} function is what OpenCode's real loader accepts —
7
+ * verified directly against the installed `@opencode-ai/plugin@1.18.23`
8
+ * `.d.ts`, not the (looser) public docs page. This module re-exports and
9
+ * narrows those upstream types; it never redeclares OpenCode's callback
10
+ * input/output shapes by hand, so this surface can't drift from the SDK it
11
+ * wraps.
12
+ * @module
13
+ */
14
+ /**
15
+ * The stable, lifecycle-callback keys of {@link Hooks} — excludes `tool`
16
+ * (custom tool definitions, not a lifecycle callback), `auth` and `provider`
17
+ * (static registration objects), and `dispose` (a no-argument cleanup
18
+ * callback with no policy semantics of its own).
19
+ *
20
+ * Order matches declaration order in the upstream `Hooks` interface.
21
+ */
22
+ export const OPENCODE_HOOK_NAMES = [
23
+ "event",
24
+ "config",
25
+ "chat.message",
26
+ "chat.params",
27
+ "chat.headers",
28
+ "permission.ask",
29
+ "command.execute.before",
30
+ "tool.execute.before",
31
+ "shell.env",
32
+ "tool.execute.after",
33
+ "experimental.chat.messages.transform",
34
+ "experimental.chat.system.transform",
35
+ "experimental.provider.small_model",
36
+ "experimental.session.compacting",
37
+ "experimental.compaction.autocontinue",
38
+ "experimental.text.complete",
39
+ "tool.definition",
40
+ ];
package/dist/cli.js CHANGED
@@ -26,7 +26,7 @@ import { scaffoldProject } from "./scaffold.js";
26
26
  // ============================================================================
27
27
  // Constants
28
28
  // ============================================================================
29
- const VERSION = "1.0.0";
29
+ const VERSION = "1.0.2";
30
30
  const DEFAULT_ESBUILD_LOADERS = {
31
31
  ".md": "text",
32
32
  };
@@ -65,10 +65,12 @@ Usage:
65
65
  under a .codex/ directory use git-toplevel-relative commands.
66
66
 
67
67
  Required Arguments:
68
- --agent <claude-code|codex|antigravity>
68
+ --agent <claude-code|codex|antigravity|opencode>
69
69
  The agent runtime the hooks are built for. Determines the hook surface
70
70
  validated at build time and the runtime module linked into each compiled
71
71
  bundle. There is no default: omitting this flag is an error.
72
+ With --agent opencode, -o/--output is treated as a plugin artifact
73
+ directory (no hooks.json manifest is generated).
72
74
  Example: --agent claude-code
73
75
 
74
76
  Build Mode (compile existing hooks):
@@ -325,11 +327,11 @@ function validateArgs(args) {
325
327
  // --agent is required and never inferred (plan step 2.3). Validate the
326
328
  // value against the agents this release knows, so a typo fails closed
327
329
  // instead of silently building for the wrong runtime.
328
- const VALID_AGENTS = ["claude-code", "codex", "antigravity"];
330
+ const VALID_AGENTS = ["claude-code", "codex", "antigravity", "opencode"];
329
331
  const isKnownAgent = (value) => VALID_AGENTS.includes(value);
330
332
  if (!isKnownAgent(args.agent)) {
331
333
  return args.agent === ""
332
- ? "Missing required argument: --agent <claude-code|codex|antigravity>"
334
+ ? "Missing required argument: --agent <claude-code|codex|antigravity|opencode>"
333
335
  : `Invalid --agent value: ${args.agent}. Valid agents: ${VALID_AGENTS.join(", ")}`;
334
336
  }
335
337
  // Scaffold mode validation
@@ -1217,9 +1219,42 @@ async function main() {
1217
1219
  closeLog();
1218
1220
  }
1219
1221
  }
1222
+ if (args.agent === "opencode") {
1223
+ const { runOpenCodeCli, validateOpenCodeArgs } = await import("./agents/opencode/cli-support.js");
1224
+ const openCodeValidationError = validateOpenCodeArgs({
1225
+ input: args.input,
1226
+ output: args.output,
1227
+ loaderFlags: args.loaderFlags ?? [],
1228
+ sourcemap: args.sourcemap,
1229
+ });
1230
+ if (openCodeValidationError !== undefined) {
1231
+ process.stderr.write(`Error: ${openCodeValidationError}\n\n`);
1232
+ process.stdout.write(HELP_TEXT);
1233
+ process.exit(1);
1234
+ }
1235
+ try {
1236
+ const compiled = await runOpenCodeCli({
1237
+ input: args.input,
1238
+ output: args.output,
1239
+ loaderFlags: args.loaderFlags ?? [],
1240
+ sourcemap: args.sourcemap,
1241
+ });
1242
+ process.stdout.write(`Generated ${compiled.length} plugin artifact(s) in ${path.resolve(process.cwd(), args.output)}\n`);
1243
+ process.exit(0);
1244
+ }
1245
+ catch (error) {
1246
+ const message = error instanceof Error ? error.message : String(error);
1247
+ log("error", "OpenCode build failed", { error: message });
1248
+ process.stderr.write(`Error: ${message}\n`);
1249
+ process.exit(1);
1250
+ }
1251
+ finally {
1252
+ closeLog();
1253
+ }
1254
+ }
1220
1255
  // Antigravity ships in step 5; fail closed until then.
1221
1256
  if (args.agent !== "claude-code") {
1222
- process.stderr.write(`Error: --agent ${args.agent} is not implemented in this release; only "claude-code" and "codex" build today (antigravity ships in step 5).\n`);
1257
+ process.stderr.write(`Error: --agent ${args.agent} is not implemented in this release; only "claude-code", "codex", and "opencode" build today (antigravity ships in step 5).\n`);
1223
1258
  process.exit(1);
1224
1259
  }
1225
1260
  // Handle scaffold mode
package/dist/scaffold.js CHANGED
@@ -131,7 +131,7 @@ function generatePackageJson(projectName, outputPath) {
131
131
  typecheck: "tsc --noEmit",
132
132
  },
133
133
  dependencies: {
134
- "@goodfoot/agent-hooks": "^1.0.0",
134
+ "@goodfoot/agent-hooks": "^1.0.2",
135
135
  },
136
136
  devDependencies: {
137
137
  "@biomejs/biome": "2.4.16",
@@ -427,7 +427,9 @@ describe("${eventName} Hook", () => {
427
427
  const result = await hook(mockInput, context);
428
428
 
429
429
  // Verify output has expected structure
430
- expect(result).toBeDefined();
430
+ if (result == null) {
431
+ throw new Error("hook returned null");
432
+ }
431
433
  expect(result).toHaveProperty("_type", "${eventName}");
432
434
  expect(result).toHaveProperty("stdout");
433
435
  expect(typeof result.stdout).toBe("object");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodfoot/agent-hooks",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Unified agent hooks library: shared core runtime with per-agent entry points for Claude Code, Codex, and Antigravity",
5
5
  "homepage": "https://github.com/goodfoot-io/marketplace/tree/main/packages/agent-hooks",
6
6
  "repository": {
@@ -38,6 +38,10 @@
38
38
  "./codex": {
39
39
  "import": "./dist/agents/codex/index.js",
40
40
  "types": "./types/agents/codex/index.d.ts"
41
+ },
42
+ "./opencode": {
43
+ "import": "./dist/agents/opencode/index.js",
44
+ "types": "./types/agents/opencode/index.d.ts"
41
45
  }
42
46
  },
43
47
  "bin": "./dist/cli.js",
@@ -51,9 +55,18 @@
51
55
  "glob": "^11.0.0",
52
56
  "typescript": "^5.9.3"
53
57
  },
58
+ "peerDependencies": {
59
+ "@opencode-ai/plugin": "^1.18.23"
60
+ },
61
+ "peerDependenciesMeta": {
62
+ "@opencode-ai/plugin": {
63
+ "optional": true
64
+ }
65
+ },
54
66
  "devDependencies": {
55
67
  "@anthropic-ai/claude-agent-sdk": "^0.3.162",
56
68
  "@biomejs/biome": "2.4.16",
69
+ "@opencode-ai/plugin": "1.18.23",
57
70
  "@types/node": "^24",
58
71
  "tsx": "^4.20.6",
59
72
  "vitest": "4.0.13"
@@ -0,0 +1,57 @@
1
+ /**
2
+ * OpenCode-specific CLI internals: bundling each selected plugin entry to a
3
+ * standalone ESM artifact and validating its `{ id, server }` (or bare
4
+ * function) default-export contract against a real dynamic import of the
5
+ * built output — not a hand-rolled AST check, which could pass a shape the
6
+ * actual OpenCode loader would still reject.
7
+ *
8
+ * Unlike Codex's `compileCodexHook`, there is no wrapper content and no
9
+ * `execute()` runtime module to link: OpenCode imports the plugin author's
10
+ * own default export directly, so the bundle *is* the artifact — no
11
+ * stdin/stdout driver, no `hooks.json` manifest, no shebang.
12
+ *
13
+ * Driver-level primitives (loader parsing, glob discovery) are imported from
14
+ * the shared CLI module — never re-derived here.
15
+ * @module
16
+ */
17
+ import * as esbuild from "esbuild";
18
+ /** OpenCode CLI arguments accepted on top of the shared `--agent opencode` gate. */
19
+ export interface OpenCodeCliArgs {
20
+ input: string;
21
+ output: string;
22
+ loaderFlags: string[];
23
+ sourcemap?: boolean;
24
+ }
25
+ type HookLoaderMap = Record<string, esbuild.Loader>;
26
+ interface CompiledOpenCodePlugin {
27
+ sourcePath: string;
28
+ outputPath: string;
29
+ }
30
+ /**
31
+ * Bundles one OpenCode plugin entry to a self-contained ESM artifact. The
32
+ * source file's own default export is preserved unchanged — no wrapper.
33
+ */
34
+ export declare function compileOpenCodePlugin(sourcePath: string, loaders: HookLoaderMap, sourcemap?: boolean): Promise<string>;
35
+ /**
36
+ * Validates that `outputPath`'s default export matches OpenCode's real
37
+ * loader contract (`getServerPlugin`): either a bare function, or an object
38
+ * with a `server` function property. Imports the built artifact itself
39
+ * rather than statically analyzing source, so the check exercises exactly
40
+ * what OpenCode's loader will see.
41
+ * @throws Error when the default export matches neither shape.
42
+ */
43
+ export declare function validateOpenCodePluginModule(outputPath: string): Promise<void>;
44
+ /**
45
+ * Validates OpenCode build arguments beyond the shared `--agent` gate: `-i`
46
+ * and `-o` required, loader flags well-formed.
47
+ */
48
+ export declare function validateOpenCodeArgs(args: OpenCodeCliArgs): string | undefined;
49
+ /**
50
+ * Runs the OpenCode build branch of the unified CLI: discovers plugin entry
51
+ * files matching `--input`, bundles each to a standalone ESM artifact under
52
+ * `--output` (treated as a directory — OpenCode plugin config references
53
+ * these files directly, there is no manifest), and validates each artifact's
54
+ * default export against OpenCode's real loader contract.
55
+ */
56
+ export declare function runOpenCodeCli(args: OpenCodeCliArgs): Promise<CompiledOpenCodePlugin[]>;
57
+ export {};
@@ -0,0 +1,46 @@
1
+ /**
2
+ * OpenCode callback policy split: which lifecycle callbacks may opt into
3
+ * guarded fail-open (`"continue"`) behavior, and which must always surface
4
+ * failures.
5
+ *
6
+ * Unlike Codex's advisory allow-list (transcribed from `codex-hooks/README.md`,
7
+ * an external source-of-truth document), OpenCode has no prior source package
8
+ * to transcribe from — this package *is* the source of truth for its own
9
+ * OpenCode surface. The split below is derived directly from CARD.md's
10
+ * Desired Functionality section (committed to the card repository, itself
11
+ * the authorization for this contract): "Advisory callbacks can opt into
12
+ * guarded fail-open behavior with observable diagnostics; policy-enforcing
13
+ * callbacks default to surfacing failures."
14
+ *
15
+ * `permission.ask` is the only callback whose `output.status` is an
16
+ * allow/deny/ask *security decision* — swallowing a failure there means the
17
+ * decision was silently skipped, exactly the Codex precedent
18
+ * (`PreToolUse`/`PermissionRequest`) this mirrors. Every other lifecycle
19
+ * callback only observes or additively enriches (message/tool mutation,
20
+ * shell env, event notification) and is therefore advisory.
21
+ * @module
22
+ */
23
+ import type { UnexpectedErrorPolicy } from "../../core/types.js";
24
+ import { type OpenCodeHookName } from "./types.js";
25
+ /** Callbacks that must always surface failures — never eligible for `"continue"`. */
26
+ export declare const POLICY_ENFORCING_EVENTS: readonly ["permission.ask"];
27
+ /** The policy-enforcing set as a literal union. */
28
+ export type PolicyEnforcingEventName = (typeof POLICY_ENFORCING_EVENTS)[number];
29
+ /** Type-level membership check: `true` only for policy-enforcing callback names. */
30
+ export type IsPolicyEnforcingEvent<TName extends OpenCodeHookName> = TName extends PolicyEnforcingEventName ? true : false;
31
+ /** The advisory allow-list as a literal union: every callback except {@link PolicyEnforcingEventName}. */
32
+ export type AdvisoryEventName = Exclude<OpenCodeHookName, PolicyEnforcingEventName>;
33
+ /**
34
+ * The advisory allow-list: every OpenCode lifecycle callback except
35
+ * {@link POLICY_ENFORCING_EVENTS}. Only these accept
36
+ * `unexpectedError: "continue"`, enforced at compile time by
37
+ * {@link AllowedUnexpectedErrorPolicy} and at call time by
38
+ * {@link ../../core/define-hook.ts|the policy-gate pattern}.
39
+ */
40
+ export declare const ADVISORY_EVENTS: readonly AdvisoryEventName[];
41
+ /**
42
+ * The policy type a callback may carry, narrowed by allow-list membership:
43
+ * advisory callbacks keep `"error" | "continue"`; every policy-enforcing
44
+ * callback collapses to `"error"`.
45
+ */
46
+ export type AllowedUnexpectedErrorPolicy<TName extends OpenCodeHookName> = IsPolicyEnforcingEvent<TName> extends true ? Exclude<UnexpectedErrorPolicy, "continue"> : UnexpectedErrorPolicy;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * `@goodfoot/agent-hooks/opencode` — the OpenCode agent surface.
3
+ *
4
+ * Re-exports OpenCode-native types (narrowed from `@opencode-ai/plugin`),
5
+ * the advisory/policy-enforcing callback split, the shared error-policy
6
+ * vocabulary, and the plugin authoring primitives. No default export at this
7
+ * subpath — `defineOpenCodePlugin`'s *return value* is the `{ id, server }`
8
+ * default export a plugin author's own module produces, not something this
9
+ * barrel exports itself.
10
+ * @module
11
+ */
12
+ export type { AdvisoryEventName, AllowedUnexpectedErrorPolicy, IsPolicyEnforcingEvent, PolicyEnforcingEventName, } from "./events.js";
13
+ export { ADVISORY_EVENTS, POLICY_ENFORCING_EVENTS } from "./events.js";
14
+ export type { RootSessionRegistry } from "./plugin.js";
15
+ export { createRootSessionRegistry, defineOpenCodePlugin, guardAdvisory } from "./plugin.js";
16
+ export type { UnexpectedErrorHandler, UnexpectedErrorPolicy } from "./policy.js";
17
+ export { applyOpenCodeErrorPolicy } from "./policy.js";
18
+ export type { Hooks, OpenCodeHookHandler, OpenCodeHookName, Plugin, PluginInput, PluginModule, PluginOptions, } from "./types.js";
19
+ export { OPENCODE_HOOK_NAMES } from "./types.js";
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Plugin authoring primitives for `@goodfoot/agent-hooks/opencode`.
3
+ *
4
+ * `defineOpenCodePlugin` validates the `{ id, server }` module contract that
5
+ * OpenCode's real loader (`getServerPlugin`, verified live against
6
+ * `sst/opencode`'s `packages/opencode/src/plugin/index.ts`) accepts as a
7
+ * default export. `createRootSessionRegistry` and `guardAdvisory` are
8
+ * optional composition helpers a `server` factory may call — not a required
9
+ * framework, since a plugin author can also return a raw {@link Hooks}
10
+ * object built directly from `./types.js`.
11
+ * @module
12
+ */
13
+ import type { AdvisoryEventName } from "./events.js";
14
+ import { type UnexpectedErrorHandler, type UnexpectedErrorPolicy } from "./policy.js";
15
+ import type { OpenCodeHookHandler, Plugin, PluginInput, PluginModule } from "./types.js";
16
+ /**
17
+ * Validates and returns an OpenCode plugin module definition.
18
+ *
19
+ * @param definition - `id` must be a non-empty string; `server` must be a
20
+ * function matching upstream's {@link Plugin} signature.
21
+ * @returns `definition`, unchanged, once validated.
22
+ * @throws Error when `id` is missing/empty or `server` is not a function.
23
+ */
24
+ export declare function defineOpenCodePlugin(definition: {
25
+ id: string;
26
+ server: Plugin;
27
+ }): PluginModule;
28
+ /**
29
+ * Tracks OpenCode session parentage and resumption across a plugin's
30
+ * lifetime.
31
+ *
32
+ * Live testing of a prior implementation of this pattern found that a
33
+ * *resumed* session does not necessarily re-emit `session.created` — it may
34
+ * first appear only through later activity (e.g. `session.updated`,
35
+ * `message.updated`) for a session id the registry has never observed. This
36
+ * registry treats that as the resumed-session signal `isResumed` reports.
37
+ */
38
+ export interface RootSessionRegistry {
39
+ /**
40
+ * Records that `sessionId` was observed via a `session.created` event,
41
+ * optionally as a child of `parentId`. Idempotent: observing the same
42
+ * `sessionId` twice does not change its recorded parentage or resumed
43
+ * status.
44
+ */
45
+ observe(sessionId: string, parentId?: string): void;
46
+ /**
47
+ * Records that `sessionId` was observed via any event *other than*
48
+ * `session.created` — the resumed-session signal. Idempotent the same way
49
+ * as {@link observe}; calling either method after the other for the same
50
+ * `sessionId` does not change its already-recorded resumed status.
51
+ */
52
+ observeResumed(sessionId: string, parentId?: string): void;
53
+ /** `true` when `sessionId` has no recorded parent. */
54
+ isRoot(sessionId: string): boolean;
55
+ /**
56
+ * `true` when `sessionId` was first observed through a non-`session.created`
57
+ * event name rather than through `session.created` itself.
58
+ */
59
+ isResumed(sessionId: string): boolean;
60
+ }
61
+ /** Creates a fresh, empty {@link RootSessionRegistry}. */
62
+ export declare function createRootSessionRegistry(): RootSessionRegistry;
63
+ /**
64
+ * Wraps an advisory OpenCode callback with {@link applyOpenCodeErrorPolicy},
65
+ * restricted at the type level to {@link AdvisoryEventName}s — a
66
+ * policy-enforcing callback name is a compile-time error here, since
67
+ * swallowing its failure would silently skip a security decision.
68
+ *
69
+ * @param name - An advisory callback name from `./types.js`'s `Hooks` map.
70
+ * @param handler - The callback implementation to guard.
71
+ * @param policy - The configured policy; `undefined` behaves as `"error"`.
72
+ * @param onError - Best-effort diagnostic sink for a swallowed `"continue"` error.
73
+ */
74
+ export declare function guardAdvisory<TName extends AdvisoryEventName>(name: TName, handler: OpenCodeHookHandler<TName>, policy: UnexpectedErrorPolicy | undefined, onError?: UnexpectedErrorHandler): OpenCodeHookHandler<TName>;
75
+ export type { PluginInput };
@@ -0,0 +1,30 @@
1
+ /**
2
+ * OpenCode error-policy application.
3
+ *
4
+ * Reuses only the host-neutral policy vocabulary from
5
+ * {@link ../../core/types.ts} — not {@link ../../core/define-hook.ts|defineHook}
6
+ * itself, which is built around a single one-shot handler call and doesn't
7
+ * fit OpenCode's long-lived, heterogeneous callback map.
8
+ * @module
9
+ */
10
+ import type { UnexpectedErrorHandler, UnexpectedErrorPolicy } from "../../core/types.js";
11
+ export type { UnexpectedErrorHandler, UnexpectedErrorPolicy };
12
+ /**
13
+ * Wraps an advisory OpenCode callback so an unexpected throw is handled
14
+ * per `policy` instead of propagating to the OpenCode server process.
15
+ *
16
+ * - `"error"` (default): rethrows unchanged.
17
+ * - `"continue"`: swallows the error, invokes `onError` (if provided) as a
18
+ * best-effort diagnostic sink, and resolves instead of rejecting.
19
+ *
20
+ * Never call this for a policy-enforcing callback (see
21
+ * {@link ./events.ts|POLICY_ENFORCING_EVENTS}) — {@link ./plugin.ts|guardAdvisory}
22
+ * is the only intended caller and restricts callback names accordingly.
23
+ *
24
+ * @param name - The callback name, used only for the diagnostic sink.
25
+ * @param handler - The callback implementation to guard.
26
+ * @param policy - The configured policy; `undefined` behaves as `"error"`.
27
+ * @param onError - Best-effort diagnostic sink for a swallowed `"continue"` error.
28
+ * @throws Error the handler threw is thrown when `policy` is `"error"`.
29
+ */
30
+ export declare function applyOpenCodeErrorPolicy<TArgs extends unknown[]>(_name: string, handler: (...args: TArgs) => Promise<void>, policy: UnexpectedErrorPolicy | undefined, onError?: UnexpectedErrorHandler): (...args: TArgs) => Promise<void>;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * OpenCode-native types for `@goodfoot/agent-hooks/opencode`.
3
+ *
4
+ * OpenCode loads a long-lived, in-process plugin module rather than spawning
5
+ * a one-shot command-hook process: {@link PluginModule} (`{ id?, server }`)
6
+ * or a bare {@link Plugin} function is what OpenCode's real loader accepts —
7
+ * verified directly against the installed `@opencode-ai/plugin@1.18.23`
8
+ * `.d.ts`, not the (looser) public docs page. This module re-exports and
9
+ * narrows those upstream types; it never redeclares OpenCode's callback
10
+ * input/output shapes by hand, so this surface can't drift from the SDK it
11
+ * wraps.
12
+ * @module
13
+ */
14
+ import type { Hooks, Plugin, PluginInput, PluginModule, PluginOptions } from "@opencode-ai/plugin";
15
+ export type { Hooks, Plugin, PluginInput, PluginModule, PluginOptions };
16
+ /**
17
+ * The stable, lifecycle-callback keys of {@link Hooks} — excludes `tool`
18
+ * (custom tool definitions, not a lifecycle callback), `auth` and `provider`
19
+ * (static registration objects), and `dispose` (a no-argument cleanup
20
+ * callback with no policy semantics of its own).
21
+ *
22
+ * Order matches declaration order in the upstream `Hooks` interface.
23
+ */
24
+ export declare const OPENCODE_HOOK_NAMES: readonly ["event", "config", "chat.message", "chat.params", "chat.headers", "permission.ask", "command.execute.before", "tool.execute.before", "shell.env", "tool.execute.after", "experimental.chat.messages.transform", "experimental.chat.system.transform", "experimental.provider.small_model", "experimental.session.compacting", "experimental.compaction.autocontinue", "experimental.text.complete", "tool.definition"];
25
+ /** Literal union of {@link OPENCODE_HOOK_NAMES}. */
26
+ export type OpenCodeHookName = (typeof OPENCODE_HOOK_NAMES)[number];
27
+ /** The handler type for a given lifecycle callback name, narrowed from {@link Hooks}. */
28
+ export type OpenCodeHookHandler<TName extends OpenCodeHookName> = NonNullable<Hooks[TName]>;