@dragonwize/opencode-event-push 0.5.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/LICENSE +21 -0
- package/README.md +190 -0
- package/event-push.example.json +30 -0
- package/package.json +43 -0
- package/src/index.ts +135 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alan Doucette
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# opencode-event-push
|
|
2
|
+
|
|
3
|
+
An [OpenCode](https://opencode.ai) plugin that forwards a configurable set of events to one or more URLs via HTTP POST.
|
|
4
|
+
|
|
5
|
+
Use it to integrate OpenCode with webhooks, logging pipelines, monitoring systems, or any custom backend.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Add the plugin to your `opencode.json`:
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"$schema": "https://opencode.ai/config.json",
|
|
14
|
+
"plugin": ["opencode-event-push"]
|
|
15
|
+
}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
OpenCode installs the package automatically at startup using Bun. The package lands in:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
~/.cache/opencode/node_modules/opencode-event-push/
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Configuration
|
|
25
|
+
|
|
26
|
+
The plugin reads its config from `event-push.json` in the **same directory as the installed plugin package** (i.e. next to `package.json`). Copy the bundled example and edit it:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
cp ~/.cache/opencode/node_modules/opencode-event-push/event-push.example.json \
|
|
30
|
+
~/.cache/opencode/node_modules/opencode-event-push/event-push.json
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Then open `event-push.json` and replace the placeholder URLs, events, and credentials.
|
|
34
|
+
|
|
35
|
+
### Config schema
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"targets": [
|
|
40
|
+
{
|
|
41
|
+
"url": "https://your-endpoint.example.com/events",
|
|
42
|
+
"events": ["session.idle", "session.error"],
|
|
43
|
+
"retry": {
|
|
44
|
+
"attempts": 3,
|
|
45
|
+
"delayMs": 500
|
|
46
|
+
},
|
|
47
|
+
"headers": {
|
|
48
|
+
"Authorization": "Bearer your-token"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
#### `targets` (required)
|
|
56
|
+
|
|
57
|
+
An array of target objects. Each target is independent — it gets its own URL, event filter, retry policy, and headers.
|
|
58
|
+
|
|
59
|
+
| Field | Required | Default | Description |
|
|
60
|
+
|---|---|---|---|
|
|
61
|
+
| `url` | yes | — | URL to `POST` events to |
|
|
62
|
+
| `events` | no | all events | Allowlist of event types to forward to this target. Omit or leave empty to receive all events. |
|
|
63
|
+
| `retry.attempts` | no | `3` | Maximum number of attempts (including the first try) |
|
|
64
|
+
| `retry.delayMs` | no | `500` | Base delay in ms between retries. Uses exponential backoff: `delayMs * 2^attempt` |
|
|
65
|
+
| `headers` | no | `{}` | Extra HTTP headers sent with every request to this target |
|
|
66
|
+
|
|
67
|
+
### Multiple targets
|
|
68
|
+
|
|
69
|
+
Each target filters and delivers events independently. One slow or failing target does not block the others — all targets for a given event are pushed in parallel.
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"targets": [
|
|
74
|
+
{
|
|
75
|
+
"url": "https://webhook.example.com/opencode",
|
|
76
|
+
"events": ["session.created", "session.idle", "session.error"],
|
|
77
|
+
"headers": { "Authorization": "Bearer secret1" }
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"url": "https://logging.example.com/ingest",
|
|
81
|
+
"events": ["tool.execute.after", "file.edited"],
|
|
82
|
+
"headers": { "X-API-Key": "secret2" }
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"url": "https://catch-all.example.com/events"
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Available events
|
|
92
|
+
|
|
93
|
+
Any event from the OpenCode plugin API can be forwarded. Common ones:
|
|
94
|
+
|
|
95
|
+
**Session:** `session.created` `session.updated` `session.deleted` `session.idle` `session.error` `session.status` `session.compacted` `session.diff`
|
|
96
|
+
|
|
97
|
+
**Message:** `message.updated` `message.removed` `message.part.updated` `message.part.removed`
|
|
98
|
+
|
|
99
|
+
**Tool:** `tool.execute.before` `tool.execute.after`
|
|
100
|
+
|
|
101
|
+
**File:** `file.edited` `file.watcher.updated`
|
|
102
|
+
|
|
103
|
+
**Permission:** `permission.asked` `permission.replied`
|
|
104
|
+
|
|
105
|
+
**Other:** `command.executed` `server.connected` `todo.updated` `lsp.updated` `lsp.client.diagnostics` `installation.updated` `tui.prompt.append` `tui.command.execute` `tui.toast.show` `shell.env`
|
|
106
|
+
|
|
107
|
+
## Payload format
|
|
108
|
+
|
|
109
|
+
Each event is POSTed as JSON with `Content-Type: application/json`. The payload is the raw event object from OpenCode, e.g.:
|
|
110
|
+
|
|
111
|
+
```json
|
|
112
|
+
{
|
|
113
|
+
"type": "session.idle",
|
|
114
|
+
"sessionID": "abc123",
|
|
115
|
+
"properties": { ... }
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Testing
|
|
120
|
+
|
|
121
|
+
A test server is included that accepts incoming events and prints them live to the terminal with color-coded output.
|
|
122
|
+
|
|
123
|
+
### Start the test server
|
|
124
|
+
|
|
125
|
+
```sh
|
|
126
|
+
bun run test-server.ts
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
By default it listens on port `34567`. Pass an alternative port as the first argument:
|
|
130
|
+
|
|
131
|
+
```sh
|
|
132
|
+
bun run test-server.ts 9000
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Point the plugin at it
|
|
136
|
+
|
|
137
|
+
Add a target to your `event-push.json` (the catch-all form with no `events` filter is most useful for testing):
|
|
138
|
+
|
|
139
|
+
```json
|
|
140
|
+
{
|
|
141
|
+
"targets": [
|
|
142
|
+
{ "url": "http://localhost:34567" }
|
|
143
|
+
]
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Then start OpenCode normally. Events will appear in the test server terminal as they fire.
|
|
148
|
+
|
|
149
|
+
### Send a test event manually
|
|
150
|
+
|
|
151
|
+
You can also POST events directly with `curl` to verify the server is running before starting OpenCode:
|
|
152
|
+
|
|
153
|
+
```sh
|
|
154
|
+
curl -X POST http://localhost:34567 \
|
|
155
|
+
-H "Content-Type: application/json" \
|
|
156
|
+
-d '{"type":"session.idle","sessionID":"test","properties":{"title":"hello"}}'
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Sample output
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
opencode-event-push test server
|
|
163
|
+
Listening on http://localhost:34567
|
|
164
|
+
Waiting for events…
|
|
165
|
+
|
|
166
|
+
────────────────────────────────────────────────────────────
|
|
167
|
+
[2026-02-27 10:00:01.234] #1 session.idle
|
|
168
|
+
sessionID="abc123" properties={"title":"My session"}
|
|
169
|
+
{
|
|
170
|
+
"type": "session.idle",
|
|
171
|
+
"sessionID": "abc123",
|
|
172
|
+
"properties": {
|
|
173
|
+
"title": "My session"
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Event types are color-coded by namespace: `session.*` (cyan), `tool.*` (yellow), `message.*` (magenta), `file.*` (green), `permission.*` (red).
|
|
179
|
+
|
|
180
|
+
## Error handling
|
|
181
|
+
|
|
182
|
+
On a failed request the plugin retries with exponential backoff (500ms, 1000ms, 2000ms, …). After exhausting all attempts it logs a `warn`-level message via OpenCode's structured log — it does not throw or block the session in any way.
|
|
183
|
+
|
|
184
|
+
## Local plugin usage
|
|
185
|
+
|
|
186
|
+
If you prefer to use this as a local file plugin rather than via npm, drop `src/index.ts` into `.opencode/plugins/` and place `event-push.json` in the same `.opencode/plugins/` directory.
|
|
187
|
+
|
|
188
|
+
## License
|
|
189
|
+
|
|
190
|
+
MIT
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"targets": [
|
|
3
|
+
{ "url": "http://localhost:34567" },
|
|
4
|
+
{
|
|
5
|
+
"url": "https://webhook.example.com/opencode",
|
|
6
|
+
"events": [
|
|
7
|
+
"session.created",
|
|
8
|
+
"session.idle",
|
|
9
|
+
"session.error"
|
|
10
|
+
],
|
|
11
|
+
"retry": {
|
|
12
|
+
"attempts": 3,
|
|
13
|
+
"delayMs": 500
|
|
14
|
+
},
|
|
15
|
+
"headers": {
|
|
16
|
+
"Authorization": "Bearer your-secret-token"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"url": "https://logging.example.com/ingest",
|
|
21
|
+
"events": [
|
|
22
|
+
"tool.execute.after",
|
|
23
|
+
"file.edited"
|
|
24
|
+
],
|
|
25
|
+
"headers": {
|
|
26
|
+
"X-API-Key": "your-api-key"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dragonwize/opencode-event-push",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "OpenCode plugin that forwards a configurable set of events to one or more URLs via HTTP POST.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"opencode",
|
|
7
|
+
"opencode-plugin",
|
|
8
|
+
"webhook",
|
|
9
|
+
"events"
|
|
10
|
+
],
|
|
11
|
+
"homepage": "https://github.com/dragonwize/opencode-event-push",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/dragonwize/opencode-event-push/issues"
|
|
14
|
+
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/dragonwize/opencode-event-push.git"
|
|
18
|
+
},
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"author": "dragonwize",
|
|
21
|
+
"type": "module",
|
|
22
|
+
"main": "src/index.ts",
|
|
23
|
+
"files": [
|
|
24
|
+
"src/",
|
|
25
|
+
"event-push.example.json"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"test-server": "bun run test-server.ts",
|
|
29
|
+
"typecheck": "tsc --noEmit"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@opencode-ai/plugin": "latest",
|
|
33
|
+
"@types/bun": "^1.3.9",
|
|
34
|
+
"@types/node": "^25.3.2",
|
|
35
|
+
"bun-types": "^1.3.9",
|
|
36
|
+
"typescript": "^5",
|
|
37
|
+
"undici-types": "^7.18.2",
|
|
38
|
+
"zod": "^4.1.8"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"@opencode-ai/plugin": "*"
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs"
|
|
2
|
+
import { dirname, join } from "node:path"
|
|
3
|
+
import { fileURLToPath } from "node:url"
|
|
4
|
+
import type { Plugin } from "@opencode-ai/plugin"
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
7
|
+
|
|
8
|
+
// ── Config types ──────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
interface RetryConfig {
|
|
11
|
+
/** Maximum number of attempts (including the first). Default: 3 */
|
|
12
|
+
attempts?: number
|
|
13
|
+
/** Base delay in milliseconds for exponential backoff. Default: 500 */
|
|
14
|
+
delayMs?: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface TargetConfig {
|
|
18
|
+
/** Required. URL to POST events to. */
|
|
19
|
+
url: string
|
|
20
|
+
/**
|
|
21
|
+
* Optional allowlist of event types to forward to this target.
|
|
22
|
+
* Omit (or set to an empty array) to forward all events.
|
|
23
|
+
*/
|
|
24
|
+
events?: string[]
|
|
25
|
+
/** Optional retry policy. */
|
|
26
|
+
retry?: RetryConfig
|
|
27
|
+
/** Optional extra HTTP headers (e.g. Authorization, X-API-Key). */
|
|
28
|
+
headers?: Record<string, string>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface PluginConfig {
|
|
32
|
+
targets: TargetConfig[]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ── Config loading ────────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
function loadConfig(): PluginConfig {
|
|
38
|
+
// The config file lives alongside the plugin (one level up from src/)
|
|
39
|
+
const configPath = join(__dirname, "..", "event-push.json")
|
|
40
|
+
try {
|
|
41
|
+
const raw = readFileSync(configPath, "utf-8")
|
|
42
|
+
const parsed = JSON.parse(raw) as PluginConfig
|
|
43
|
+
if (!Array.isArray(parsed.targets)) {
|
|
44
|
+
console.warn(
|
|
45
|
+
"[opencode-event-push] event-push.json is missing a 'targets' array — plugin is a no-op",
|
|
46
|
+
)
|
|
47
|
+
return { targets: [] }
|
|
48
|
+
}
|
|
49
|
+
return parsed
|
|
50
|
+
} catch (err: unknown) {
|
|
51
|
+
const isNotFound =
|
|
52
|
+
err instanceof Error && "code" in err && (err as NodeJS.ErrnoException).code === "ENOENT"
|
|
53
|
+
if (!isNotFound) {
|
|
54
|
+
console.warn(
|
|
55
|
+
`[opencode-event-push] Could not read event-push.json: ${String(err)}`,
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
return { targets: [] }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── HTTP push with retry ──────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
async function pushToTarget(
|
|
65
|
+
target: TargetConfig,
|
|
66
|
+
payload: unknown,
|
|
67
|
+
log: (msg: string, extra?: Record<string, unknown>) => Promise<void>,
|
|
68
|
+
): Promise<void> {
|
|
69
|
+
const { url, retry = {}, headers = {} } = target
|
|
70
|
+
const maxAttempts = retry.attempts ?? 3
|
|
71
|
+
const baseDelay = retry.delayMs ?? 500
|
|
72
|
+
|
|
73
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
74
|
+
try {
|
|
75
|
+
const res = await fetch(url, {
|
|
76
|
+
method: "POST",
|
|
77
|
+
headers: { "Content-Type": "application/json", ...headers },
|
|
78
|
+
body: JSON.stringify(payload),
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
if (res.ok) return
|
|
82
|
+
|
|
83
|
+
throw new Error(`HTTP ${res.status} ${res.statusText}`)
|
|
84
|
+
} catch (err) {
|
|
85
|
+
const isLastAttempt = attempt === maxAttempts - 1
|
|
86
|
+
if (isLastAttempt) {
|
|
87
|
+
await log(
|
|
88
|
+
`Failed to push event to ${url} after ${maxAttempts} attempt(s): ${String(err)}`,
|
|
89
|
+
{ url, error: String(err), attempts: maxAttempts },
|
|
90
|
+
)
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Exponential backoff: 500ms, 1000ms, 2000ms, …
|
|
95
|
+
const delay = baseDelay * 2 ** attempt
|
|
96
|
+
await new Promise((resolve) => setTimeout(resolve, delay))
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── Plugin ────────────────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
export const EventPushPlugin: Plugin = async ({ client }) => {
|
|
104
|
+
const { targets } = loadConfig()
|
|
105
|
+
|
|
106
|
+
if (targets.length === 0) return {}
|
|
107
|
+
|
|
108
|
+
async function log(message: string, extra?: Record<string, unknown>) {
|
|
109
|
+
await client.app.log({
|
|
110
|
+
body: {
|
|
111
|
+
service: "opencode-event-push",
|
|
112
|
+
level: "warn",
|
|
113
|
+
message,
|
|
114
|
+
extra,
|
|
115
|
+
},
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
event: async ({ event }) => {
|
|
121
|
+
const matchingTargets = targets.filter(
|
|
122
|
+
(t) =>
|
|
123
|
+
!t.events || t.events.length === 0 || t.events.includes(event.type),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
if (matchingTargets.length === 0) return
|
|
127
|
+
|
|
128
|
+
await Promise.allSettled(
|
|
129
|
+
matchingTargets.map((target) => pushToTarget(target, event, log)),
|
|
130
|
+
)
|
|
131
|
+
},
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export default EventPushPlugin
|