@expnn/opencode-yaml-hooks 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +352 -0
- package/dist/adapter/opencode.d.ts +2 -0
- package/dist/core/bash-executor.d.ts +4 -0
- package/dist/core/bash-types.d.ts +33 -0
- package/dist/core/config-paths.d.ts +13 -0
- package/dist/core/load-hooks.d.ts +26 -0
- package/dist/core/runtime.d.ts +12 -0
- package/dist/core/session-state.d.ts +25 -0
- package/dist/core/tool-paths.d.ts +13 -0
- package/dist/core/types.d.ts +102 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +8735 -0
- package/package.json +54 -0
package/README.md
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
# opencode-yaml-hooks
|
|
2
|
+
|
|
3
|
+
`opencode-yaml-hooks` is an OpenCode plugin that loads hook definitions from `hooks.yaml` and runs command, tool, or bash actions on session and tool lifecycle events.
|
|
4
|
+
|
|
5
|
+
Use it to run tests after edits, lint changed files, block risky commands before they run, or trigger local automation without another LLM step.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
Register it in your `opencode.json`:
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"$schema": "https://opencode.ai/config.json",
|
|
14
|
+
"plugin": ["opencode-yaml-hooks"]
|
|
15
|
+
}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Quick start
|
|
19
|
+
|
|
20
|
+
Create one of:
|
|
21
|
+
|
|
22
|
+
- `~/.config/opencode/hook/hooks.yaml`
|
|
23
|
+
- `<project>/.opencode/hook/hooks.yaml`
|
|
24
|
+
|
|
25
|
+
Then add a minimal hook:
|
|
26
|
+
|
|
27
|
+
```yaml
|
|
28
|
+
hooks:
|
|
29
|
+
- name: npm-test
|
|
30
|
+
event: file.changed
|
|
31
|
+
conditions: [matchesCodeFiles]
|
|
32
|
+
actions:
|
|
33
|
+
- bash: "npm test"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
This runs `npm test` after a supported file mutation tool changes at least one tracked code file.
|
|
37
|
+
|
|
38
|
+
## Current config locations
|
|
39
|
+
|
|
40
|
+
Hooks are merged from global and project locations.
|
|
41
|
+
|
|
42
|
+
| Platform | Global config | Project config |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| macOS / Linux | `~/.config/opencode/hook/hooks.yaml` | `<project>/.opencode/hook/hooks.yaml` |
|
|
45
|
+
| Windows | `~/.config/opencode/hook/hooks.yaml` preferred, otherwise `%APPDATA%/opencode/hook/hooks.yaml` | `<project>/.opencode/hook/hooks.yaml` |
|
|
46
|
+
|
|
47
|
+
## Start with these defaults
|
|
48
|
+
|
|
49
|
+
Unless you need something more specific:
|
|
50
|
+
|
|
51
|
+
- prefer `file.changed` for file-oriented automation
|
|
52
|
+
- leave `scope` unset unless you need `main` or `child`
|
|
53
|
+
- leave `runIn` unset unless you need actions to execute in the root session
|
|
54
|
+
- treat `tool.after.*` and `tool.after.<name>` as advanced hooks for observability or non-file workflows
|
|
55
|
+
|
|
56
|
+
Explicit defaults in the current runtime:
|
|
57
|
+
|
|
58
|
+
- `scope` defaults to `all`
|
|
59
|
+
- `runIn` defaults to `current`
|
|
60
|
+
- `conditions` are optional
|
|
61
|
+
- bash `timeout` defaults to `60000` milliseconds
|
|
62
|
+
|
|
63
|
+
## Schema overview
|
|
64
|
+
|
|
65
|
+
```yaml
|
|
66
|
+
hooks:
|
|
67
|
+
- name: <hook-name>
|
|
68
|
+
event: <hook-event>
|
|
69
|
+
action: <stop> # optional, only for tool.before.* hooks
|
|
70
|
+
scope: <all|main|child> # optional, defaults to all
|
|
71
|
+
runIn: <current|main> # optional, defaults to current
|
|
72
|
+
async: <boolean> # optional, fire-and-forget execution
|
|
73
|
+
conditions: # optional
|
|
74
|
+
- matchesCodeFiles
|
|
75
|
+
- matchesAnyPath: src/**/*.ts
|
|
76
|
+
- matchesAllPaths:
|
|
77
|
+
- package.json
|
|
78
|
+
- apps/*/package.json
|
|
79
|
+
actions: # required, non-empty
|
|
80
|
+
- command: <string>
|
|
81
|
+
- command:
|
|
82
|
+
name: <string>
|
|
83
|
+
args: <string>
|
|
84
|
+
- tool:
|
|
85
|
+
name: <string>
|
|
86
|
+
args: <object>
|
|
87
|
+
- bash: <string>
|
|
88
|
+
- bash:
|
|
89
|
+
command: <string>
|
|
90
|
+
timeout: <positive integer milliseconds>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Validation rules enforced by the runtime:
|
|
94
|
+
|
|
95
|
+
- `hooks` must exist and be an array
|
|
96
|
+
- each hook must be an object with a supported `event`
|
|
97
|
+
- `scope`, if present, must be `all`, `main`, or `child`
|
|
98
|
+
- `runIn`, if present, must be `current` or `main`
|
|
99
|
+
- `action`, if present, must be `stop` and is only supported on `tool.before.*` and `tool.before.<name>` hooks
|
|
100
|
+
- `async`, if present, must be a boolean; cannot be `true` on `tool.before` or `session.idle` events; async hooks must use only `bash` actions
|
|
101
|
+
- `conditions`, if present, must be an array of supported condition entries
|
|
102
|
+
- `actions` must be a non-empty array
|
|
103
|
+
- each action must define exactly one of `command`, `tool`, or `bash`
|
|
104
|
+
|
|
105
|
+
## Supported events
|
|
106
|
+
|
|
107
|
+
### Session events
|
|
108
|
+
|
|
109
|
+
| Event | When it fires |
|
|
110
|
+
|---|---|
|
|
111
|
+
| `session.created` | When OpenCode creates a session |
|
|
112
|
+
| `session.deleted` | When OpenCode deletes a session |
|
|
113
|
+
| `session.idle` | When a session becomes idle |
|
|
114
|
+
| `file.changed` | After a supported mutation tool reports file changes |
|
|
115
|
+
|
|
116
|
+
### Tool events
|
|
117
|
+
|
|
118
|
+
| Event | When it fires |
|
|
119
|
+
|---|---|
|
|
120
|
+
| `tool.before.*` | Before every tool execution |
|
|
121
|
+
| `tool.before.<name>` | Before a specific tool, such as `tool.before.write` |
|
|
122
|
+
| `tool.after.*` | Advanced: after every tool execution |
|
|
123
|
+
| `tool.after.<name>` | Advanced: after a specific tool, such as `tool.after.edit` |
|
|
124
|
+
|
|
125
|
+
Tool hook order for a tool named `write`:
|
|
126
|
+
|
|
127
|
+
1. `tool.before.*`
|
|
128
|
+
2. `tool.before.write`
|
|
129
|
+
3. tool executes
|
|
130
|
+
4. `file.changed` if the tool changed tracked files
|
|
131
|
+
5. `tool.after.*`
|
|
132
|
+
6. `tool.after.write`
|
|
133
|
+
|
|
134
|
+
## Public API versus advanced hooks
|
|
135
|
+
|
|
136
|
+
### Preferred public API: `file.changed`
|
|
137
|
+
|
|
138
|
+
Use `file.changed` when your automation depends on changed files.
|
|
139
|
+
|
|
140
|
+
Why it is preferred:
|
|
141
|
+
|
|
142
|
+
- it only fires for supported mutation tools: `write`, `edit`, `multiedit`, `patch`, and `apply_patch`
|
|
143
|
+
- it includes `files` and structured `changes` metadata
|
|
144
|
+
- it avoids catch-all after-hook ambiguity
|
|
145
|
+
- it is the recommended path for linting, formatting, indexing, and atomic commit workflows
|
|
146
|
+
|
|
147
|
+
### Advanced escape hatches: `tool.after.*` and `tool.after.<name>`
|
|
148
|
+
|
|
149
|
+
Keep using low-level tool hooks only when you need:
|
|
150
|
+
|
|
151
|
+
- observability for every tool call, including non-file tools
|
|
152
|
+
- tool-specific post-processing unrelated to changed files
|
|
153
|
+
- compatibility with workflows that truly depend on raw tool arguments instead of normalized file changes
|
|
154
|
+
|
|
155
|
+
## Conditions
|
|
156
|
+
|
|
157
|
+
All configured conditions must pass for a hook to run.
|
|
158
|
+
|
|
159
|
+
| Condition | Meaning |
|
|
160
|
+
|---|---|
|
|
161
|
+
| `matchesCodeFiles` | Run only when tracked modified files include at least one supported code extension |
|
|
162
|
+
| `matchesAnyPath` | Run only when at least one final changed file path matches one or more glob patterns |
|
|
163
|
+
| `matchesAllPaths` | Run only when every final changed file path matches at least one glob pattern |
|
|
164
|
+
|
|
165
|
+
`matchesCodeFiles` is extension-based. Extensionless files such as `Dockerfile` do not currently count as code changes.
|
|
166
|
+
|
|
167
|
+
`matchesAnyPath` and `matchesAllPaths` only work on `file.changed` and `session.idle`. Both accept either a single string or a string array, and both fail when there are no changed files to evaluate.
|
|
168
|
+
|
|
169
|
+
Examples:
|
|
170
|
+
|
|
171
|
+
```yaml
|
|
172
|
+
hooks:
|
|
173
|
+
- event: file.changed
|
|
174
|
+
conditions:
|
|
175
|
+
- matchesAnyPath: src/**/*.ts
|
|
176
|
+
actions:
|
|
177
|
+
- bash: "npm run lint -- --fix"
|
|
178
|
+
|
|
179
|
+
- event: session.idle
|
|
180
|
+
scope: main
|
|
181
|
+
conditions:
|
|
182
|
+
- matchesAllPaths:
|
|
183
|
+
- package.json
|
|
184
|
+
- apps/*/package.json
|
|
185
|
+
actions:
|
|
186
|
+
- bash: "npm test"
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Invalid usage example:
|
|
190
|
+
|
|
191
|
+
```yaml
|
|
192
|
+
hooks:
|
|
193
|
+
- event: tool.after.write
|
|
194
|
+
conditions:
|
|
195
|
+
- matchesAnyPath: src/**/*.ts # invalid: path conditions are file.changed/session.idle only
|
|
196
|
+
actions:
|
|
197
|
+
- bash: "echo nope"
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Actions
|
|
201
|
+
|
|
202
|
+
### Command action
|
|
203
|
+
|
|
204
|
+
Runs an OpenCode command in the same session, unless `runIn: main` redirects it to the root session.
|
|
205
|
+
|
|
206
|
+
```yaml
|
|
207
|
+
actions:
|
|
208
|
+
- command: simplify-changes
|
|
209
|
+
- command:
|
|
210
|
+
name: review-pr
|
|
211
|
+
args: "main feature"
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Tool action
|
|
215
|
+
|
|
216
|
+
Prompts the session to use a tool with specific arguments.
|
|
217
|
+
|
|
218
|
+
```yaml
|
|
219
|
+
actions:
|
|
220
|
+
- tool:
|
|
221
|
+
name: bash
|
|
222
|
+
args:
|
|
223
|
+
command: "echo done"
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Bash action
|
|
227
|
+
|
|
228
|
+
Runs a bash command directly without another LLM step.
|
|
229
|
+
|
|
230
|
+
```yaml
|
|
231
|
+
actions:
|
|
232
|
+
- bash: "npm run lint"
|
|
233
|
+
- bash:
|
|
234
|
+
command: "$OPENCODE_PROJECT_DIR/.opencode/hooks/init.sh"
|
|
235
|
+
timeout: 30000
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
If `timeout` is omitted, bash actions use the runtime default of `60000` milliseconds.
|
|
239
|
+
|
|
240
|
+
`OPENCODE_PROJECT_DIR` remains the action cwd / project directory that triggered the hook.
|
|
241
|
+
|
|
242
|
+
## Bash payloads
|
|
243
|
+
|
|
244
|
+
Every bash action receives:
|
|
245
|
+
|
|
246
|
+
- inherited `process.env`
|
|
247
|
+
- `OPENCODE_PROJECT_DIR` for the action cwd / project directory
|
|
248
|
+
- `OPENCODE_SESSION_ID`
|
|
249
|
+
- `OPENCODE_GIT_COMMON_DIR` when available
|
|
250
|
+
- JSON over stdin
|
|
251
|
+
|
|
252
|
+
Example `file.changed` payload:
|
|
253
|
+
|
|
254
|
+
```json
|
|
255
|
+
{
|
|
256
|
+
"session_id": "abc123",
|
|
257
|
+
"event": "file.changed",
|
|
258
|
+
"cwd": "/path/to/project",
|
|
259
|
+
"files": ["src/index.ts", "src/renamed.ts"],
|
|
260
|
+
"changes": [
|
|
261
|
+
{ "operation": "modify", "path": "src/index.ts" },
|
|
262
|
+
{ "operation": "rename", "fromPath": "src/old.ts", "toPath": "src/renamed.ts" }
|
|
263
|
+
],
|
|
264
|
+
"tool_name": "apply_patch",
|
|
265
|
+
"tool_args": {
|
|
266
|
+
"patchText": "*** Begin Patch\\n...\\n*** End Patch"
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## Blocking behavior
|
|
272
|
+
|
|
273
|
+
Only `tool.before.*` and `tool.before.<name>` hooks can block execution.
|
|
274
|
+
|
|
275
|
+
- a bash action that exits with `2` blocks the tool
|
|
276
|
+
- `action: stop` escalates a blocking pre-tool hook into a best-effort `session.abort(...)` for the active session
|
|
277
|
+
- `tool.after.*`, `tool.after.<name>`, `file.changed`, and session hooks do not block execution
|
|
278
|
+
- non-blocking failures are logged and later actions continue
|
|
279
|
+
|
|
280
|
+
Example:
|
|
281
|
+
|
|
282
|
+
```yaml
|
|
283
|
+
hooks:
|
|
284
|
+
- event: tool.before.bash
|
|
285
|
+
action: stop
|
|
286
|
+
actions:
|
|
287
|
+
- bash: |
|
|
288
|
+
payload=$(cat)
|
|
289
|
+
cmd=$(printf '%s' "$payload" | jq -r '.tool_args.command // empty')
|
|
290
|
+
|
|
291
|
+
case "$cmd" in
|
|
292
|
+
"git push"|git\ push\ *)
|
|
293
|
+
echo "Blocked and stopping session: git push is not allowed." >&2
|
|
294
|
+
exit 2
|
|
295
|
+
;;
|
|
296
|
+
esac
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## Execution behavior on this branch
|
|
300
|
+
|
|
301
|
+
- hooks for the same event run in declaration order
|
|
302
|
+
- global hooks load before project hooks
|
|
303
|
+
- the runtime reloads discovered `hooks.yaml` files at each hook entrypoint
|
|
304
|
+
- invalid reloads are rejected and the last known good config stays active
|
|
305
|
+
- `session.idle` clears tracked changes only after successful dispatch
|
|
306
|
+
- if idle dispatch fails, tracked changes are preserved for retry
|
|
307
|
+
- reentrant `file.changed` and `tool.after.*` dispatches are queued and replayed after the active dispatch finishes
|
|
308
|
+
- `async: true` hooks return immediately without blocking the tool pipeline; their actions run in the background as best-effort work
|
|
309
|
+
- async actions for the same event and source session are serialized to prevent overlapping executions; note that serialization is per source session, not per target — `runIn: main` hooks from different child sessions are not serialized against each other
|
|
310
|
+
|
|
311
|
+
## Copy-paste examples
|
|
312
|
+
|
|
313
|
+
See [`examples/hooks.yaml`](examples/hooks.yaml) for:
|
|
314
|
+
|
|
315
|
+
- main-session only examples
|
|
316
|
+
- child-to-main `runIn: main` routing
|
|
317
|
+
- recommended `file.changed` automation
|
|
318
|
+
- advanced `tool.after.*` observability
|
|
319
|
+
- conservative atomic commit wiring
|
|
320
|
+
|
|
321
|
+
## See also
|
|
322
|
+
|
|
323
|
+
- [`docs/hooks-v2-reference.md`](docs/hooks-v2-reference.md) for the current public config shape
|
|
324
|
+
- [`docs/comparison-with-claude-code-hooks.md`](docs/comparison-with-claude-code-hooks.md) for how this compares to Claude Code's hook system
|
|
325
|
+
|
|
326
|
+
## Known limitations
|
|
327
|
+
|
|
328
|
+
- file tracking is limited to supported OpenCode mutation tools, not arbitrary filesystem changes
|
|
329
|
+
- `matchesCodeFiles` is extension-based and ignores extensionless code-like files
|
|
330
|
+
- tool hooks depend on actual emitted OpenCode tool names
|
|
331
|
+
- Windows discovery is supported, but bash actions still require a working shell runtime
|
|
332
|
+
- `async: true` is not allowed on `tool.before.*` or `session.idle` events; async hooks cannot block tool execution or idle dispatch
|
|
333
|
+
- async hooks must use only `bash` actions; `command` and `tool` actions have no timeout and can stall the queue
|
|
334
|
+
- async hook failures are logged but not retried; async execution is best-effort and not guaranteed to complete if the host process exits
|
|
335
|
+
|
|
336
|
+
## Explicit non-goals for v1/v2 runtime scope
|
|
337
|
+
|
|
338
|
+
This package does **not** currently try to:
|
|
339
|
+
|
|
340
|
+
- define custom hook events beyond session, file, and tool lifecycle events
|
|
341
|
+
- provide config inheritance or override priority beyond global-then-project merging
|
|
342
|
+
- provide retries, scheduling, or concurrency controls per hook
|
|
343
|
+
- track arbitrary filesystem changes outside OpenCode mutation tools
|
|
344
|
+
- make command or tool actions blocking
|
|
345
|
+
|
|
346
|
+
## Development
|
|
347
|
+
|
|
348
|
+
```bash
|
|
349
|
+
npm install
|
|
350
|
+
npm run build
|
|
351
|
+
npm test
|
|
352
|
+
```
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type BashExecutionRequest, type BashHookContext, type BashHookResult, type BashProcessResult } from "./bash-types.js";
|
|
2
|
+
export declare function executeBashHook(request: BashExecutionRequest): Promise<BashHookResult>;
|
|
3
|
+
export declare function mapBashProcessResultToHookResult(result: BashProcessResult, context: BashHookContext): BashHookResult;
|
|
4
|
+
export declare function isBlockingToolBeforeEvent(event: string): boolean;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { PluginInput } from "@opencode-ai/plugin";
|
|
2
|
+
import type { FileChange } from "./types.js";
|
|
3
|
+
export declare const DEFAULT_BASH_TIMEOUT = 60000;
|
|
4
|
+
export interface BashHookContext {
|
|
5
|
+
readonly session_id: string;
|
|
6
|
+
readonly event: string;
|
|
7
|
+
readonly cwd: string;
|
|
8
|
+
readonly files?: readonly string[];
|
|
9
|
+
readonly changes?: readonly FileChange[];
|
|
10
|
+
readonly tool_name?: string;
|
|
11
|
+
readonly tool_args?: Record<string, unknown>;
|
|
12
|
+
}
|
|
13
|
+
export interface BashExecutionRequest {
|
|
14
|
+
readonly command: string;
|
|
15
|
+
readonly context: BashHookContext;
|
|
16
|
+
readonly projectDir: string;
|
|
17
|
+
readonly timeout?: number;
|
|
18
|
+
readonly client?: PluginInput["client"];
|
|
19
|
+
}
|
|
20
|
+
export interface BashProcessResult {
|
|
21
|
+
readonly command: string;
|
|
22
|
+
readonly stdout: string;
|
|
23
|
+
readonly stderr: string;
|
|
24
|
+
readonly durationMs: number;
|
|
25
|
+
readonly exitCode: number;
|
|
26
|
+
readonly signal: NodeJS.Signals | null;
|
|
27
|
+
readonly timedOut: boolean;
|
|
28
|
+
}
|
|
29
|
+
export type BashHookResultStatus = "success" | "failed" | "blocked" | "timed_out";
|
|
30
|
+
export interface BashHookResult extends BashProcessResult {
|
|
31
|
+
readonly status: BashHookResultStatus;
|
|
32
|
+
readonly blocking: boolean;
|
|
33
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface HookConfigDiscoveryOptions {
|
|
2
|
+
readonly projectDir?: string;
|
|
3
|
+
readonly platform?: string;
|
|
4
|
+
readonly homeDir?: string;
|
|
5
|
+
readonly appDataDir?: string;
|
|
6
|
+
readonly exists?: (filePath: string) => boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface HookConfigPaths {
|
|
9
|
+
readonly global?: string;
|
|
10
|
+
readonly project?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function resolveHookConfigPaths(options?: HookConfigDiscoveryOptions): HookConfigPaths;
|
|
13
|
+
export declare function discoverHookConfigPaths(options?: HookConfigDiscoveryOptions): string[];
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type HookOverrideEntry, type HookMap, type HookValidationError, type ParsedHooksFile } from "./types.js";
|
|
2
|
+
import { type HookConfigDiscoveryOptions } from "./config-paths.js";
|
|
3
|
+
export interface HookDiscoveryResult {
|
|
4
|
+
readonly hooks: HookMap;
|
|
5
|
+
readonly errors: HookValidationError[];
|
|
6
|
+
readonly files: string[];
|
|
7
|
+
}
|
|
8
|
+
export interface HookLoadOptions extends HookConfigDiscoveryOptions {
|
|
9
|
+
readonly readFile?: (filePath: string) => string;
|
|
10
|
+
}
|
|
11
|
+
export interface HookLoadSnapshot extends HookDiscoveryResult {
|
|
12
|
+
readonly signature: string;
|
|
13
|
+
}
|
|
14
|
+
type ParsedHooksFileResult = ParsedHooksFile & {
|
|
15
|
+
readonly files: string[];
|
|
16
|
+
};
|
|
17
|
+
export declare function parseHooksFile(filePath: string, content: string): ParsedHooksFileResult;
|
|
18
|
+
export declare function loadHooksFile(filePath: string, readFile?: (filePath: string) => string): ParsedHooksFileResult;
|
|
19
|
+
export declare function loadDiscoveredHooks(options?: HookLoadOptions): HookDiscoveryResult;
|
|
20
|
+
export declare function loadDiscoveredHooksSnapshot(options?: HookLoadOptions): HookLoadSnapshot;
|
|
21
|
+
export declare function mergeHookMaps(...hookMaps: HookMap[]): HookMap;
|
|
22
|
+
export declare function resolveOverrides(hooks: HookMap, overrides: HookOverrideEntry[]): {
|
|
23
|
+
hooks: HookMap;
|
|
24
|
+
errors: HookValidationError[];
|
|
25
|
+
};
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Hooks, PluginInput } from "@opencode-ai/plugin";
|
|
2
|
+
import { executeBashHook } from "./bash-executor.js";
|
|
3
|
+
import type { BashExecutionRequest } from "./bash-types.js";
|
|
4
|
+
import type { HookMap } from "./types.js";
|
|
5
|
+
type ExecuteBashHook = (request: BashExecutionRequest) => ReturnType<typeof executeBashHook>;
|
|
6
|
+
export interface CreateHooksRuntimeOptions {
|
|
7
|
+
readonly hooks?: HookMap;
|
|
8
|
+
readonly executeBash?: ExecuteBashHook;
|
|
9
|
+
readonly client?: PluginInput["client"];
|
|
10
|
+
}
|
|
11
|
+
export declare function createHooksRuntime(input: PluginInput, options?: CreateHooksRuntimeOptions): Hooks;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { FileChange } from "./types.js";
|
|
2
|
+
export interface PendingToolCall {
|
|
3
|
+
readonly sessionID: string;
|
|
4
|
+
readonly toolArgs: Record<string, unknown>;
|
|
5
|
+
}
|
|
6
|
+
export type SessionScope = "all" | "main" | "child";
|
|
7
|
+
export declare class SessionStateStore {
|
|
8
|
+
private readonly sessions;
|
|
9
|
+
private readonly pendingToolCalls;
|
|
10
|
+
rememberSession(sessionID: string, parentID?: string | null): void;
|
|
11
|
+
evaluateScope(sessionID: string, scope: SessionScope, resolveParentID: (sessionID: string) => Promise<string | null | undefined>): Promise<boolean>;
|
|
12
|
+
getRootSessionID(sessionID: string, resolveParentID: (sessionID: string) => Promise<string | null | undefined>): Promise<string>;
|
|
13
|
+
isDeleted(sessionID: string): boolean;
|
|
14
|
+
deleteSession(sessionID: string): void;
|
|
15
|
+
setPendingToolCall(callID: string, sessionID: string, toolArgs: Record<string, unknown>): void;
|
|
16
|
+
consumePendingToolCall(callID: string): PendingToolCall | undefined;
|
|
17
|
+
addFileChanges(sessionID: string, changes: Iterable<FileChange>): void;
|
|
18
|
+
getFileChanges(sessionID: string): FileChange[];
|
|
19
|
+
getModifiedPaths(sessionID: string): string[];
|
|
20
|
+
beginIdleDispatch(sessionID: string, changes: readonly FileChange[]): void;
|
|
21
|
+
consumeFileChanges(sessionID: string, changes: readonly FileChange[]): void;
|
|
22
|
+
cancelIdleDispatch(sessionID: string): void;
|
|
23
|
+
private getOrCreateSession;
|
|
24
|
+
private resolveRootSessionID;
|
|
25
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { FileChange } from "./types.js";
|
|
2
|
+
declare const DIRECT_MUTATION_TOOL_NAMES: readonly ["write", "edit", "multiedit"];
|
|
3
|
+
declare const PATCH_MUTATION_TOOL_NAMES: readonly ["patch", "apply_patch"];
|
|
4
|
+
declare const BASH_TOOL_NAME: "bash";
|
|
5
|
+
export declare const MUTATION_TOOL_NAMES: Set<"bash" | "write" | "edit" | "multiedit" | "patch" | "apply_patch">;
|
|
6
|
+
export type MutationToolName = (typeof DIRECT_MUTATION_TOOL_NAMES)[number] | (typeof PATCH_MUTATION_TOOL_NAMES)[number] | typeof BASH_TOOL_NAME;
|
|
7
|
+
export type NormalizedMutationToolName = "write" | "edit" | "multiedit" | "apply_patch" | "bash";
|
|
8
|
+
export declare function normalizeMutationToolName(toolName: string): NormalizedMutationToolName | undefined;
|
|
9
|
+
export declare function getMutationToolHookNames(toolName: string): string[];
|
|
10
|
+
export declare function getToolAffectedPaths(toolName: string, args: Record<string, unknown>): string[];
|
|
11
|
+
export declare function getToolFileChanges(toolName: string, args: Record<string, unknown>): FileChange[];
|
|
12
|
+
export declare function getChangedPaths(changes: readonly FileChange[]): string[];
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
export declare const SESSION_HOOK_EVENTS: readonly ["session.idle", "session.created", "session.deleted", "file.changed"];
|
|
2
|
+
export declare const LEGACY_HOOK_CONDITIONS: readonly ["matchesCodeFiles"];
|
|
3
|
+
export declare const PATH_HOOK_CONDITION_KEYS: readonly ["matchesAnyPath", "matchesAllPaths"];
|
|
4
|
+
export declare const HOOK_SCOPES: readonly ["all", "main", "child"];
|
|
5
|
+
export declare const HOOK_RUN_IN: readonly ["current", "main"];
|
|
6
|
+
export declare const HOOK_BEHAVIORS: readonly ["stop"];
|
|
7
|
+
export type SessionHookEvent = (typeof SESSION_HOOK_EVENTS)[number];
|
|
8
|
+
export type ToolHookPhase = "before" | "after";
|
|
9
|
+
export type ToolHookEvent = `tool.${ToolHookPhase}.*` | `tool.${ToolHookPhase}.${string}`;
|
|
10
|
+
export type HookEvent = SessionHookEvent | ToolHookEvent;
|
|
11
|
+
export type HookLegacyCondition = (typeof LEGACY_HOOK_CONDITIONS)[number];
|
|
12
|
+
export type HookPathConditionKey = (typeof PATH_HOOK_CONDITION_KEYS)[number];
|
|
13
|
+
export type HookPathCondition = {
|
|
14
|
+
readonly matchesAnyPath: readonly string[];
|
|
15
|
+
} | {
|
|
16
|
+
readonly matchesAllPaths: readonly string[];
|
|
17
|
+
};
|
|
18
|
+
export type HookCondition = HookLegacyCondition | HookPathCondition;
|
|
19
|
+
export type HookScope = (typeof HOOK_SCOPES)[number];
|
|
20
|
+
export type HookRunIn = (typeof HOOK_RUN_IN)[number];
|
|
21
|
+
export type HookBehavior = (typeof HOOK_BEHAVIORS)[number];
|
|
22
|
+
export interface CreateFileChange {
|
|
23
|
+
readonly operation: "create";
|
|
24
|
+
readonly path: string;
|
|
25
|
+
}
|
|
26
|
+
export interface ModifyFileChange {
|
|
27
|
+
readonly operation: "modify";
|
|
28
|
+
readonly path: string;
|
|
29
|
+
}
|
|
30
|
+
export interface DeleteFileChange {
|
|
31
|
+
readonly operation: "delete";
|
|
32
|
+
readonly path: string;
|
|
33
|
+
}
|
|
34
|
+
export interface RenameFileChange {
|
|
35
|
+
readonly operation: "rename";
|
|
36
|
+
readonly fromPath: string;
|
|
37
|
+
readonly toPath: string;
|
|
38
|
+
}
|
|
39
|
+
export type FileChange = CreateFileChange | ModifyFileChange | DeleteFileChange | RenameFileChange;
|
|
40
|
+
export interface HookCommandActionConfig {
|
|
41
|
+
readonly name: string;
|
|
42
|
+
readonly args?: string;
|
|
43
|
+
}
|
|
44
|
+
export interface HookToolActionConfig {
|
|
45
|
+
readonly name: string;
|
|
46
|
+
readonly args?: Record<string, unknown>;
|
|
47
|
+
}
|
|
48
|
+
export interface HookBashActionConfig {
|
|
49
|
+
readonly command: string;
|
|
50
|
+
readonly timeout?: number;
|
|
51
|
+
}
|
|
52
|
+
export interface HookCommandAction {
|
|
53
|
+
readonly command: string | HookCommandActionConfig;
|
|
54
|
+
}
|
|
55
|
+
export interface HookToolAction {
|
|
56
|
+
readonly tool: HookToolActionConfig;
|
|
57
|
+
}
|
|
58
|
+
export interface HookBashAction {
|
|
59
|
+
readonly bash: string | HookBashActionConfig;
|
|
60
|
+
}
|
|
61
|
+
export type HookAction = HookCommandAction | HookToolAction | HookBashAction;
|
|
62
|
+
export interface HookConfigSource {
|
|
63
|
+
readonly filePath: string;
|
|
64
|
+
readonly index: number;
|
|
65
|
+
}
|
|
66
|
+
export interface HookConfig {
|
|
67
|
+
readonly id?: string;
|
|
68
|
+
readonly event: HookEvent;
|
|
69
|
+
readonly name?: string;
|
|
70
|
+
readonly action?: HookBehavior;
|
|
71
|
+
readonly actions: HookAction[];
|
|
72
|
+
readonly scope: HookScope;
|
|
73
|
+
readonly runIn: HookRunIn;
|
|
74
|
+
readonly async?: boolean;
|
|
75
|
+
readonly conditions?: HookCondition[];
|
|
76
|
+
readonly source: HookConfigSource;
|
|
77
|
+
}
|
|
78
|
+
export interface HookOverrideEntry {
|
|
79
|
+
readonly targetId: string;
|
|
80
|
+
readonly disable: boolean;
|
|
81
|
+
readonly replacement?: HookConfig;
|
|
82
|
+
readonly source: HookConfigSource;
|
|
83
|
+
}
|
|
84
|
+
export type HookMap = Map<HookEvent, HookConfig[]>;
|
|
85
|
+
export type HookValidationErrorCode = "invalid_frontmatter" | "missing_hooks" | "invalid_hooks" | "invalid_hook" | "invalid_event" | "invalid_scope" | "invalid_run_in" | "invalid_hook_action" | "invalid_conditions" | "invalid_actions" | "invalid_action" | "duplicate_hook_id" | "override_target_not_found" | "invalid_override" | "invalid_async";
|
|
86
|
+
export interface HookValidationError {
|
|
87
|
+
readonly code: HookValidationErrorCode;
|
|
88
|
+
readonly filePath: string;
|
|
89
|
+
readonly message: string;
|
|
90
|
+
readonly path?: string;
|
|
91
|
+
}
|
|
92
|
+
export interface ParsedHooksFile {
|
|
93
|
+
readonly hooks: HookMap;
|
|
94
|
+
readonly overrides: HookOverrideEntry[];
|
|
95
|
+
readonly errors: HookValidationError[];
|
|
96
|
+
}
|
|
97
|
+
export declare function isHookEvent(value: unknown): value is HookEvent;
|
|
98
|
+
export declare function isHookLegacyCondition(value: unknown): value is HookLegacyCondition;
|
|
99
|
+
export declare function isHookPathConditionKey(value: unknown): value is HookPathConditionKey;
|
|
100
|
+
export declare function isHookScope(value: unknown): value is HookScope;
|
|
101
|
+
export declare function isHookRunIn(value: unknown): value is HookRunIn;
|
|
102
|
+
export declare function isHookBehavior(value: unknown): value is HookBehavior;
|
package/dist/index.d.ts
ADDED