@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 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