diff --git a/.env.example b/.env.example index a691eb6..64be8c1 100644 --- a/.env.example +++ b/.env.example @@ -45,10 +45,12 @@ SENTRY_AUTH_TOKEN= # +@users.noreply.github.com). GH_TOKEN= -# === Optional: github-webhooks plugin === -# This image ships a plugin (~/.config/opencode/plugins/github-webhooks.ts) -# that turns inbound GitHub webhooks into OpenCode agent sessions. A -# baseline config file is already baked in at +# === Optional: opencode-webhooks plugin === +# This image ships the opencode-webhooks plugin (resolved into +# ~/.config/opencode/node_modules/opencode-webhooks at build time, with +# source under packages/opencode-webhooks/) which turns inbound GitHub +# webhooks into OpenCode agent sessions. A baseline config file is +# already baked in at # ~/.config/opencode/webhooks.json — it wires 7 triggers covering the # full PR lifecycle (issue assigned → resolve → review → fix CI → # respond to comments). Setting GITHUB_WEBHOOK_SECRET below + having diff --git a/.gitignore b/.gitignore index 2cef73e..1a15b0e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ .env .env.* !.env.example + +# Local-only OpenCode workspace state (plans, scratch notes, session +# data). Anything we want to share publicly belongs in repo-root files +# (README, AGENTS.md, etc.), not here. +.opencode/ diff --git a/.opencode/plans/session-reuse-and-event-buffer.md b/.opencode/plans/session-reuse-and-event-buffer.md deleted file mode 100644 index d193f0e..0000000 --- a/.opencode/plans/session-reuse-and-event-buffer.md +++ /dev/null @@ -1,308 +0,0 @@ -# Plan: per-PR session reuse with event buffering - -## Goal - -Today, every webhook delivery spawns a fresh OpenCode session. Each session -re-explores the codebase from scratch, re-clones the repo, re-reads the -issue. Across the PR lifecycle this means the bot pays the cold-start cost -~10× (open → review → CI fix → comment response → review again → …). - -Replace the one-session-per-event model with a **per-PR session** model so: - -1. Lore, prior reasoning, and codebase context accumulate across events. -2. The agent can correlate "the comment I'm responding to now" with "the - review I posted 20 minutes ago" without re-deriving from GitHub. -3. The cold-start tax amortizes — only the first event on a PR pays full - exploration cost. - -Add an **event buffer** so when an event arrives mid-session, the plugin -queues it as a follow-up prompt rather than spawning a parallel session -or rejecting it. - -## Decisions (proposed; subject to review) - -| Question | Choice | -|---|---| -| Affinity key | PR number (`/#`); fallback to issue number for `issues.assigned`; fallback to `/@` for PR-less `check_suite` | -| Migration | When `issues.assigned` resolves into a PR, the issue's session is renamed/relinked to the PR's key | -| Concurrency | One in-flight `session.prompt` per session; subsequent events queue FIFO | -| Persistence | Reuse existing `bun:sqlite` (the `deliveries` DB); add `session_map` and `event_queue` tables | -| Context bound | Session retired after N consecutive idle hours OR PR merged/closed OR token-budget exceeds threshold (TBD) | -| Agent role isolation | Each enqueued event sets the `agent` field on `session.prompt` per its trigger config; the system prompt switches across roles within one session | -| Failure model | Agent run errors don't drop the session — next event reuses it. Session retired only on explicit retire signal (PR closed, idle expiry, manual purge) | - -## Architecture - -### Components added to `plugins/github-webhooks/` - -``` -session-affinity.ts key resolution: payload → session-key -session-store.ts session_map + event_queue persistence -session-runner.ts replaces dispatch.ts: lookup-or-create + queue + drain -``` - -### Affinity key resolution (`session-affinity.ts`) - -```ts -type SessionKey = string // canonical form - -function resolveSessionKey(event: string, payload: unknown): SessionKey | null -``` - -Logic by event: - -| Event | Key | -|---|---| -| `pull_request.*` | `/#pr` | -| `pull_request_review.*` | `/#pr` (from `payload.pull_request.number`) | -| `pull_request_review_comment.*` | `/#pr` | -| `issue_comment.*` (PR comment) | `/#pr` (from `payload.issue.number`, since on PR-issues these align) | -| `issue_comment.*` (issue-only) | `/#issue` | -| `issues.*` | `/#issue` | -| `check_suite.*` (with PRs) | `/#pr` (from `payload.check_suite.pull_requests[0].number`) | -| `check_suite.*` (no PRs) | `/@` (rare; mostly main-branch CI) | - -Migration: when `pull_request.opened` fires and the body contains -`Closes #N` / `Fixes #N` / `Resolves #N`, the plugin checks whether a -session for `/#issue` exists. If yes, that session's -key is rewritten to the new PR key (single SQLite UPDATE). - -### Session map (`session-store.ts`) - -New tables in the existing `deliveries.db`: - -```sql -CREATE TABLE IF NOT EXISTS session_map ( - key TEXT PRIMARY KEY, -- canonical session key - session_id TEXT NOT NULL UNIQUE, -- OpenCode session id - created_at INTEGER NOT NULL, - last_used_at INTEGER NOT NULL, - retired_at INTEGER -- NULL = active -); - -CREATE INDEX IF NOT EXISTS idx_session_map_last_used - ON session_map(last_used_at DESC); - -CREATE TABLE IF NOT EXISTS event_queue ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - session_key TEXT NOT NULL, - trigger_name TEXT NOT NULL, - agent TEXT NOT NULL, - prompt TEXT NOT NULL, - delivery_id TEXT NOT NULL, - enqueued_at INTEGER NOT NULL, - status TEXT NOT NULL CHECK (status IN ('queued','running','done','failed')) -); - -CREATE INDEX IF NOT EXISTS idx_event_queue_session_status - ON event_queue(session_key, status, id); -``` - -API: - -```ts -type SessionMap = { - lookup(key: SessionKey): { sessionId: string; lastUsedAt: number } | null - create(key: SessionKey, sessionId: string): void - touch(key: SessionKey): void - retire(key: SessionKey): void - retireIdle(olderThanMs: number): SessionKey[] -} - -type EventQueue = { - enqueue(item: QueueItem): number // returns row id - nextFor(key: SessionKey): QueueItem | null // FIFO - markRunning(id: number): void - markDone(id: number, status: 'done' | 'failed'): void - pendingFor(key: SessionKey): number // count -} -``` - -### Runner (`session-runner.ts`) - -Replaces `dispatch.ts` with a queue-aware variant. One in-memory -mutex per session-key to serialize prompts: - -```ts -function makeSessionRunner(opts: {...}): Dispatcher { - const locks = new Map>() - - return async function dispatch(t, prompt, deliveryId, payload) { - const key = resolveSessionKey(t.event, payload) - if (!key) { - // No correlatable key — fall back to old single-shot model. - return legacyDispatch(t, prompt, deliveryId) - } - eventQueue.enqueue({ - sessionKey: key, - triggerName: t.name, - agent: t.agent, - prompt, - deliveryId, - enqueuedAt: Date.now(), - status: 'queued', - }) - // Kick the drainer (no-op if already running for this key). - drainKey(key) - } - - async function drainKey(key: SessionKey) { - if (locks.has(key)) return // already draining - const p = (async () => { - let item = eventQueue.nextFor(key) - while (item) { - eventQueue.markRunning(item.id) - try { - const sessionId = await ensureSession(key, item) - await client.session.prompt({ - path: { id: sessionId }, - body: { agent: item.agent, parts: [{ type: 'text', text: item.prompt }] }, - }) - eventQueue.markDone(item.id, 'done') - sessionMap.touch(key) - } catch (err) { - eventQueue.markDone(item.id, 'failed') - // continue to next event — don't kill the queue on one failure - console.error(`[session-runner] item ${item.id} failed:`, err) - } - item = eventQueue.nextFor(key) - } - })() - locks.set(key, p) - try { await p } finally { locks.delete(key) } - } - - async function ensureSession(key, item) { - const existing = sessionMap.lookup(key) - if (existing && !await isSessionRetiredRemotely(existing.sessionId)) { - return existing.sessionId - } - const session = await client.session.create({ - body: { title: `[${key}] ${item.triggerName}` }, - query: { directory: deriveCwd(key) }, - }) - sessionMap.create(key, session.data.id) - return session.data.id - } -} -``` - -`isSessionRetiredRemotely` defends against the case where the OpenCode -host purges a session out from under us (e.g. user manually deleted it). - -### Idle reaper - -Background timer every hour: - -```ts -const retired = sessionMap.retireIdle(MAX_IDLE_MS) -for (const key of retired) { - console.log(`[session-runner] retired idle session for ${key}`) -} -``` - -`MAX_IDLE_MS` defaults to **72 hours**. Configurable via `SESSION_IDLE_HOURS` -in the webhook config. - -## Migration: issue → PR - -When `pull_request.opened` arrives and body contains `Closes #N`: - -```ts -const issueKey = `${owner}/${repo}#issue${N}` -const prKey = `${owner}/${repo}#pr${number}` -if (sessionMap.lookup(issueKey) && !sessionMap.lookup(prKey)) { - sessionMap.rename(issueKey, prKey) // UPDATE session_map SET key=? WHERE key=? -} -``` - -The session that resolved the issue continues seamlessly into reviewing -the PR it produced. This is the highest-value continuity case. - -## Open questions / risks - -### 1. Context bloat - -A long-lived PR with 10+ review rounds can blow past the model's context -window. Options: - -- **Auto-summarize** at threshold via OpenCode's existing `session.summarize` API call (need to verify availability). -- **Hard cut**: retire the session at N tokens, start fresh on next event. -- **Manual purge**: agent itself emits a `RETIRE` directive when it senses context is stale, plugin retires the session. - -**Proposal**: start with hard-cut at, say, 80% of model max context as -reported by OpenCode. Add summarization as a follow-up if hard-cut proves -disruptive. - -### 2. Cross-agent reasoning bias - -The same session runs `pr-reviewer` then `pr-comment-responder` on the -same PR. The comment-responder's first turn already has the reviewer's -findings in context. This can be: - -- **Good**: the bot doesn't contradict its own prior review. -- **Bad**: the bot agrees with itself even when a human comment surfaces a - blind spot the reviewer missed. - -**Mitigation**: agent prompts already say "this comment may surface things -the prior review missed; treat it on its merits." We rely on the LLM's -own awareness here. If we observe the bias in practice, the escape hatch -is to spawn a fresh sub-agent (via `task` tool) for the comment triage -within the same session, isolating its read pass from the parent's -history. - -### 3. Concurrency on the same PR - -Two near-simultaneous events on the same PR (e.g. comment + check_suite) -both target the same session-key. The mutex serializes them, but: - -- The first event might take 5 minutes (review). The second waits in the - queue. By the time the second runs, the PR state may have moved. -- Consequence: the agent should always re-read PR state at step 0, not - trust queued context. Existing agent prompts already do `gh pr view` - at step 0; adequate. - -### 4. Session storage is on the OpenCode host, not in our SQLite - -We're storing `(key → sessionId)` mappings, but the session's actual -content lives in OpenCode's per-session storage on disk. A redeploy that -wipes `~/.local/share/opencode/` (e.g. ephemeral rootfs without the -volume) would orphan all our mappings. Need to verify session data is -under `~/dev/.opencode/` (the persistent symlink target — yes, per -docker-entrypoint.sh:21) before relying on persistence claims. - -### 5. Manual override - -A maintainer should be able to force a fresh session — e.g. when -historical context has gone bad. Add an `X-OpenCode-Session-Reset: true` -header on the webhook delivery? Or a `RETIRE` magic comment on the PR? - -**Proposal**: `RETIRE` magic comment is more discoverable. The plugin -checks for `pr-retire-session` in the comment body; if present, retires -the session and dispatches the current event as the start of a fresh one. - -## Rollout plan - -1. **Phase 1**: ship the session-map + queue infrastructure with a feature - flag (`SESSION_REUSE_ENABLED=false` by default). Existing single-shot - dispatch path unchanged. -2. **Phase 2**: enable for `pull_request_review_comment` only. Lowest-risk - event class — comment-responder already does step-0 state refresh. -3. **Phase 3**: enable for the full PR lifecycle (`pull_request.*`, - `check_suite.*`, `pull_request_review.*`). -4. **Phase 4**: enable issue→PR migration. - -Each phase observes for: context-bloat reports (sessions > 100k tokens), -cross-agent bias incidents, concurrency stalls. Rollback is one env var. - -## Out of scope - -- Cross-repo session correlation (a PR in repo A that references repo B). -- Sessions surviving across container redeploys *without* the persistent - volume. The `~/dev` mount is the assumption; without it, this whole - feature degrades to single-shot. -- Streaming events into a session that's already mid-prompt. Bun's - `client.session.prompt` is request/response; injecting mid-prompt - would need OpenCode SDK changes upstream. -- Per-event-class agent-bias mitigation (sub-agent isolation). Deferred - to follow-up if observed in Phase 2. diff --git a/Dockerfile b/Dockerfile index 0b49904..a0f8922 100644 --- a/Dockerfile +++ b/Dockerfile @@ -190,11 +190,11 @@ COPY --chown=developer:developer agents \ COPY --chown=developer:developer skills \ /home/developer/.config/opencode/skills -# Bundled plugins (e.g. github-webhooks). OpenCode auto-loads any -# .ts/.js file in this directory at startup. The sibling package.json -# declares the npm deps the plugins import (@opencode-ai/plugin); we -# `bun install` them once at build time so OpenCode doesn't have to do -# it on every container start. +# Bundled plugin packages. The `opencode-webhooks` plugin lives under +# packages/ as a workspace-style file: dep referenced from +# opencode-config-package.json. `bun install` resolves it into +# node_modules/ alongside the npm-published @loreai/opencode plugin; +# both are referenced by absolute file:// URL from opencode.json. # # IMPORTANT: do NOT mount a runtime volume over /home/developer/.config/ # opencode — it would mask the baked-in node_modules and the plugin @@ -202,8 +202,8 @@ COPY --chown=developer:developer skills \ # plugin'`. Persistent state (sessions, auth) lives at ~/dev/.opencode # already via the symlink set up below; that's the only directory you # should attach a volume to. -COPY --chown=developer:developer plugins \ - /home/developer/.config/opencode/plugins +COPY --chown=developer:developer packages \ + /home/developer/.config/opencode/packages COPY --chown=developer:developer opencode-config-package.json \ /home/developer/.config/opencode/package.json COPY --chown=developer:developer opencode-config-bun.lock \ @@ -212,7 +212,7 @@ RUN cd /home/developer/.config/opencode \ && bun install --frozen-lockfile --production \ && rm -rf ~/.bun/install/cache -# Default config for the github-webhooks plugin: one trigger that wires +# Default config for the opencode-webhooks plugin: one trigger that wires # the `issues.assigned` event to the bundled `github-issue-resolver` # agent. The plugin reads this on startup; without it, the listener # stays off (no surprise port). Override per-deploy by setting @@ -237,7 +237,7 @@ EXPOSE 4096 5050 WORKDIR /home/developer/dev # PORT lets PaaS platforms (Railway/Fly/Render) assign a port; falls back -# to 4096 locally. WEBHOOK_PORT (default 5050) is what the github-webhooks +# to 4096 locally. WEBHOOK_PORT (default 5050) is what the opencode-webhooks # plugin binds its listener to. ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/docker-entrypoint.sh"] CMD ["sh", "-c", "exec opencode web --hostname 0.0.0.0 --port ${PORT:-4096}"] diff --git a/README.md b/README.md index 8d44299..b1384ce 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Self-hosted [OpenCode](https://opencode.ai) web UI in a Docker image, ready to d - **OpenCode** built from source from the [`BYK/opencode`](https://github.com/BYK/opencode/tree/byk/cumulative) fork (`byk/cumulative` branch) — carries question-dock UX, plan-mode, and db perf fixes that aren't yet in upstream. Built fresh into the image; auto-update is effectively disabled because the fork has no release feed. - [Sentry CLI](https://cli.sentry.dev), GitHub CLI, **nvm + Node 22 LTS** (`pnpm` / `yarn` via corepack), **Bun**, plus `git`, `ripgrep`, `fd`, `fzf`, `jq`, `yq`, and `build-essential`. - No MCP servers preconfigured — add your own via a project-local `opencode.json` or by editing [`opencode-user-config.json`](./opencode-user-config.json) before building. -- **Bundled OpenCode plugin: [`github-webhooks`](./plugins/github-webhooks.ts)** — turns inbound GitHub webhook deliveries into OpenCode agent sessions running in the same `opencode` process. Ships with [`webhooks.json`](./webhooks.json) baked in (8 triggers covering the full PR lifecycle). Activates on container start once you set `GITHUB_WEBHOOK_SECRET`. See [GitHub webhooks → agent sessions](#github-webhooks--agent-sessions). +- **Bundled OpenCode plugin: [`opencode-webhooks`](./packages/opencode-webhooks)** — turns inbound GitHub webhook deliveries into OpenCode agent sessions running in the same `opencode` process. Ships with [`webhooks.json`](./webhooks.json) baked in (8 triggers covering the full PR lifecycle). Activates on container start once you set `GITHUB_WEBHOOK_SECRET`. The plugin lives as a standalone, publishable npm package under [`packages/opencode-webhooks/`](./packages/opencode-webhooks) — see its [README](./packages/opencode-webhooks/README.md) for the full config schema and how to use it in your own OpenCode setup. See also [GitHub webhooks → agent sessions](#github-webhooks--agent-sessions) below for this image's specific wiring. - **Bundled agents** (permissions pre-broadened so they don't stall on approval prompts): - [`github-issue-resolver`](./agents/github-issue-resolver.md) — issue assigned → branch → plan → implement → draft PR. - [`pr-reviewer`](./agents/pr-reviewer.md) — PR opened / ready-for-review / review-requested / assigned → runs the `review` skill to find issues. On the bot's own PRs it spawns the `pr-fix-applier` subagent to push fixes directly. On others' PRs it posts a structured GitHub review. @@ -52,18 +52,18 @@ See [`.env.example`](./.env.example) for the full template. |---|---| | One of `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`, `GROQ_API_KEY`, `OPENROUTER_API_KEY` | **Required.** LLM provider key. | | `SENTRY_AUTH_TOKEN`, `SENTRY_ORG`, `SENTRY_PROJECT`, `SENTRY_URL` | For the bundled `sentry` CLI. | -| `GH_TOKEN` | For the bundled `gh` CLI **and** the `github-webhooks` plugin's identity-gated triggers. PAT with the scopes you need (typical: `repo`, `read:org`, `workflow`). The plugin runs `gh api user --jq .login` at boot to resolve the bot's GitHub identity; without `GH_TOKEN`, identity-gated triggers fail closed. | -| `GITHUB_WEBHOOK_SECRET` | HMAC secret for the `github-webhooks` plugin. Required to receive webhooks. | +| `GH_TOKEN` | For the bundled `gh` CLI **and** the `opencode-webhooks` plugin's identity-gated triggers. PAT with the scopes you need (typical: `repo`, `read:org`, `workflow`). The plugin runs `gh api user --jq .login` at boot to resolve the bot's GitHub identity; without `GH_TOKEN`, identity-gated triggers fail closed. | +| `GITHUB_WEBHOOK_SECRET` | HMAC secret for the `opencode-webhooks` plugin. Required to receive webhooks. | | `WEBHOOK_PORT`, `WEBHOOKS_CONFIG` | Optional plugin tuning. See [`.env.example`](./.env.example). | | `PORT` | Set automatically by most PaaS providers. Defaults to `4096`. | ## GitHub webhooks → agent sessions -The bundled [`github-webhooks`](./plugins/github-webhooks.ts) plugin runs -**inside** the OpenCode server process — no sidecar, no second process to -supervise, no loopback HTTP. It opens its own listener on port `5050` -(configurable via `WEBHOOK_PORT`) and dispatches verified deliveries -into agent sessions via the in-process SDK client. +The bundled [`opencode-webhooks`](./packages/opencode-webhooks) plugin +runs **inside** the OpenCode server process — no sidecar, no second +process to supervise, no loopback HTTP. It opens its own listener on +port `5050` (configurable via `WEBHOOK_PORT`) and dispatches verified +deliveries into agent sessions via the in-process SDK client. ### Default behavior @@ -238,7 +238,7 @@ afterward — view it in OpenCode's UI like any other session. ### Health check `GET http://:5050/healthz` (the plugin's port, not OpenCode's -4096) returns `{ "ok": true, "plugin": "github-webhooks" }` once the +4096) returns `{ "ok": true, "plugin": "opencode-webhooks" }` once the listener is up. No auth required. ## Local test diff --git a/opencode-config-bun.lock b/opencode-config-bun.lock index 5ec8fa5..302f339 100644 --- a/opencode-config-bun.lock +++ b/opencode-config-bun.lock @@ -7,6 +7,7 @@ "dependencies": { "@loreai/opencode": "latest", "@opencode-ai/plugin": "^1.14.30", + "opencode-webhooks": "file:./packages/opencode-webhooks", }, }, }, @@ -33,16 +34,22 @@ "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="], + "@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="], "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + "@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="], + "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], + "bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="], + "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], @@ -135,6 +142,8 @@ "node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="], + "opencode-webhooks": ["opencode-webhooks@file:packages/opencode-webhooks", { "devDependencies": { "@opencode-ai/plugin": "^1.14.30", "@types/bun": "latest", "typescript": "^5.6.0" }, "peerDependencies": { "@opencode-ai/plugin": ">=1.1.0" } }], + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], "pure-rand": ["pure-rand@8.4.0", "", {}, "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A=="], @@ -153,6 +162,10 @@ "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], + "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], diff --git a/opencode-config-package.json b/opencode-config-package.json index cffa261..45423c7 100644 --- a/opencode-config-package.json +++ b/opencode-config-package.json @@ -2,10 +2,11 @@ "name": "opencode-config", "version": "0.1.0", "private": true, - "description": "Dependencies for plugins shipped under ~/.config/opencode/plugins (loaded by OpenCode at startup)", + "description": "Dependencies for plugins loaded by OpenCode at startup", "type": "module", "dependencies": { "@opencode-ai/plugin": "^1.14.30", - "@loreai/opencode": "latest" + "@loreai/opencode": "latest", + "opencode-webhooks": "file:./packages/opencode-webhooks" } } diff --git a/opencode-user-config.json b/opencode-user-config.json index d840027..0d7aef5 100644 --- a/opencode-user-config.json +++ b/opencode-user-config.json @@ -1,6 +1,7 @@ { "$schema": "https://opencode.ai/config.json", "plugin": [ - "file:///home/developer/.config/opencode/node_modules/@loreai/opencode" + "file:///home/developer/.config/opencode/node_modules/@loreai/opencode", + "file:///home/developer/.config/opencode/node_modules/opencode-webhooks" ] } diff --git a/packages/opencode-webhooks/.gitignore b/packages/opencode-webhooks/.gitignore new file mode 100644 index 0000000..0c059e8 --- /dev/null +++ b/packages/opencode-webhooks/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +bun.lock +*.tgz diff --git a/packages/opencode-webhooks/LICENSE b/packages/opencode-webhooks/LICENSE new file mode 100644 index 0000000..f5bbae9 --- /dev/null +++ b/packages/opencode-webhooks/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Aditya Mathur + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/opencode-webhooks/README.md b/packages/opencode-webhooks/README.md new file mode 100644 index 0000000..85f6c0d --- /dev/null +++ b/packages/opencode-webhooks/README.md @@ -0,0 +1,134 @@ +# opencode-webhooks + +OpenCode plugin: receive GitHub webhooks and dispatch them to OpenCode agent sessions running in the same process. + +When a configured webhook arrives, the plugin verifies the HMAC signature, deduplicates by `X-GitHub-Delivery`, runs identity/payload gating, renders a prompt template against the payload, and starts a new OpenCode agent session via the in-process SDK client. + +> **Runtime: Bun ≥ 1.2.** Uses `Bun.serve`, `Bun.spawn`, and `bun:sqlite`. + +## Install + +Once published to npm, add the package name to your OpenCode config's `plugin` array — OpenCode will install it into `~/.cache/opencode/node_modules/` automatically at startup: + +```jsonc +// ~/.config/opencode/opencode.json +{ + "$schema": "https://opencode.ai/config.json", + "plugin": [ + "opencode-webhooks" + ] +} +``` + +> Until the package is published to npm, install it manually: add `"opencode-webhooks": "file:/path/to/this/repo/packages/opencode-webhooks"` to a `package.json` in your OpenCode config directory (`~/.config/opencode/package.json`), run `bun install` there, and reference the resolved path: +> +> ```jsonc +> { +> "plugin": [ +> "file:///home//.config/opencode/node_modules/opencode-webhooks" +> ] +> } +> ``` + +## Configure + +The plugin reads `webhooks.json` from `~/.config/opencode/webhooks.json` by default. Override with the `WEBHOOKS_CONFIG` env var. + +Minimal config: + +```jsonc +{ + "max_concurrent": 2, + "timeout_ms": 1800000, + "retention": 1000, + "triggers": [ + { + "name": "issue-assigned", + "event": "issues", + "action": "assigned", + "agent": "github-issue-resolver", + "require_bot_match": ["assignee.login"], + "prompt_template": "Issue assigned: {{ payload.issue.html_url }}\n\n{{ payload.issue.body }}" + } + ] +} +``` + +### Top-level fields + +| Field | Default | Description | +|---|---|---| +| `port` | `5050` (or `WEBHOOK_PORT`) | TCP port for the listener. | +| `secret` | `GITHUB_WEBHOOK_SECRET` env | HMAC secret. Without one, every delivery is rejected with 503. | +| `timeout_ms` | `1800000` (30 min) | Per-session abort budget. | +| `max_concurrent` | `2` | Concurrency cap across all triggers. | +| `default_cwd` | OpenCode project root | Fallback session cwd when a trigger doesn't override. | +| `db_path` | `${XDG_DATA_HOME or ~/.local/share}/opencode-webhooks/deliveries.sqlite` | SQLite path for delivery dedup. | +| `retention` | `1000` | Max deliveries kept in dedup DB; oldest pruned. | +| `triggers` | `[]` | Array of trigger objects (see below). | + +### Trigger fields + +| Field | Required | Description | +|---|---|---| +| `name` | yes | Unique per-config; used in logs. | +| `event` | yes | GitHub event header (`issues`, `pull_request`, `*` for any). | +| `action` | no | If set, must match `payload.action` exactly. Omit/`null` = any action. | +| `agent` | yes | OpenCode agent name to invoke. | +| `prompt_template` | yes | Mustache-ish template. `{{ payload.foo.bar }}` looks up paths; missing renders empty. Synthetic `{{ review_state }}` is the lowercased `payload.review.state`. | +| `cwd` | no | Override session cwd. Falls back to `default_cwd`. | +| `enabled` | no | Set `false` to disable a trigger without removing it. | +| `ignore_authors` | no | Skip if `payload.sender.login` matches any entry (case-insensitive). The literal `"$BOT_LOGIN"` is substituted with the resolved bot login. | +| `payload_filter` | no | Object mapping dotted paths → expected values. `"*"` means any non-empty value; other values are scalar equality. AND across keys. | +| `require_bot_match` | no | List of dotted payload paths whose string value must equal the bot's resolved login (case-insensitive). Paths support a `[*]` wildcard (e.g. `requested_reviewers[*].login`). OR across paths. Skips with `bot identity unresolved` if `gh api user` failed at boot (fail-closed). | + +## Bot identity + +The plugin resolves "the bot" via `gh api user --jq .login` at boot. `gh` reads `GH_TOKEN` from the environment. The resolved login is used for: + +- The `require_bot_match` identity gate. +- The `"$BOT_LOGIN"` placeholder substitution in `ignore_authors`. + +If `gh` isn't installed or `GH_TOKEN` isn't set, identity-gated triggers refuse to fire (fail-closed). Triggers without `require_bot_match` are unaffected. + +> **Soft dependency.** `gh` is the GitHub CLI: . Install it on the host running OpenCode. + +## Environment variables + +| Variable | Purpose | +|---|---| +| `GITHUB_WEBHOOK_SECRET` | HMAC secret for `X-Hub-Signature-256` verification. Same value you set in GitHub's webhook config. | +| `GH_TOKEN` | GitHub PAT, read by `gh` CLI for `gh api user`. Required for identity-gated triggers. | +| `WEBHOOK_PORT` | Override listener port (default 5050). | +| `WEBHOOKS_CONFIG` | Path to `webhooks.json` (default `~/.config/opencode/webhooks.json`). | + +## Health check + +``` +GET /healthz → 200 { ok: true, plugin: "opencode-webhooks" } +``` + +## Webhook endpoint + +``` +POST /webhooks/github +``` + +Required headers: + +- `X-GitHub-Event` — event name (e.g. `issues`). +- `X-GitHub-Delivery` — UUID for dedup. +- `X-Hub-Signature-256` — `sha256=` HMAC of the raw body using `GITHUB_WEBHOOK_SECRET`. + +Returns 200 on accept, 401 on bad signature, 409 on duplicate delivery, 404 on path mismatch, 503 if the listener is starting up or the secret is unconfigured. + +## Limitations + +- Single-process plugin; shares the OpenCode server's process. An unhandled rejection inside the dispatcher could crash the host server. The plugin installs a top-level `unhandledRejection` handler, but consumers should still pin OpenCode versions. +- One trigger fires per inbound delivery. If multiple triggers' filters match, only the first (by config order) runs. +- No built-in rate limiting beyond `max_concurrent`. A burst of 200 deliveries in a second will queue at the semaphore but not be dropped. +- `bun:sqlite` is required for delivery dedup. The plugin won't run on Node. + +## License + +MIT — see [LICENSE](./LICENSE). diff --git a/packages/opencode-webhooks/package.json b/packages/opencode-webhooks/package.json new file mode 100644 index 0000000..fd6a817 --- /dev/null +++ b/packages/opencode-webhooks/package.json @@ -0,0 +1,53 @@ +{ + "name": "opencode-webhooks", + "version": "0.1.0", + "type": "module", + "license": "MIT", + "description": "OpenCode plugin: receive GitHub webhooks and dispatch them to OpenCode agent sessions", + "main": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": { + "types": "./src/index.ts", + "bun": "./src/index.ts", + "default": "./src/index.ts" + } + }, + "scripts": { + "typecheck": "tsc --noEmit", + "build": "echo 'opencode-webhooks ships raw TS — no build step needed'" + }, + "peerDependencies": { + "@opencode-ai/plugin": ">=1.1.1" + }, + "devDependencies": { + "@opencode-ai/plugin": "^1.14.30", + "@types/bun": "latest", + "typescript": "^5.6.0" + }, + "files": [ + "src/", + "README.md", + "LICENSE" + ], + "engines": { + "bun": ">=1.2.0" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/MathurAditya724/my-opencode.git", + "directory": "packages/opencode-webhooks" + }, + "publishConfig": { + "access": "public" + }, + "keywords": [ + "opencode", + "plugin", + "github", + "webhook", + "agent", + "automation" + ], + "author": "Aditya Mathur" +} diff --git a/plugins/github-webhooks/bot-identity.ts b/packages/opencode-webhooks/src/bot-identity.ts similarity index 82% rename from plugins/github-webhooks/bot-identity.ts rename to packages/opencode-webhooks/src/bot-identity.ts index a3d0fdd..8b7669c 100644 --- a/plugins/github-webhooks/bot-identity.ts +++ b/packages/opencode-webhooks/src/bot-identity.ts @@ -18,14 +18,14 @@ export async function resolveBotLogin(): Promise { clearTimeout(timer) if (exitCode !== 0) { console.warn( - `[github-webhooks] gh api user exit=${exitCode} stderr=${stderr.trim().slice(0, 200)}`, + `[opencode-webhooks] gh api user exit=${exitCode} stderr=${stderr.trim().slice(0, 200)}`, ) return null } const login = stdout.trim() return login.length > 0 ? login : null } catch (err) { - console.warn("[github-webhooks] resolveBotLogin failed:", err) + console.warn("[opencode-webhooks] resolveBotLogin failed:", err) return null } } diff --git a/plugins/github-webhooks/config.ts b/packages/opencode-webhooks/src/config.ts similarity index 95% rename from plugins/github-webhooks/config.ts rename to packages/opencode-webhooks/src/config.ts index e5962b5..f54aa92 100644 --- a/plugins/github-webhooks/config.ts +++ b/packages/opencode-webhooks/src/config.ts @@ -15,7 +15,7 @@ export async function readWebhookConfig(): Promise { if (!parsed || typeof parsed !== "object") return {} return parsed as WebhookConfig } catch (err) { - console.error(`[github-webhooks] failed to parse config at ${path}:`, err) + console.error(`[opencode-webhooks] failed to parse config at ${path}:`, err) return {} } } diff --git a/plugins/github-webhooks/dispatch.ts b/packages/opencode-webhooks/src/dispatch.ts similarity index 83% rename from plugins/github-webhooks/dispatch.ts rename to packages/opencode-webhooks/src/dispatch.ts index 7e2120a..a332418 100644 --- a/plugins/github-webhooks/dispatch.ts +++ b/packages/opencode-webhooks/src/dispatch.ts @@ -36,12 +36,12 @@ export function makeDispatcher(opts: { const sessionId = session.data?.id if (!sessionId) { console.error( - `[github-webhooks] trigger '${t.name}' (${deliveryId}): session.create returned no id`, + `[opencode-webhooks] trigger '${t.name}' (${deliveryId}): session.create returned no id`, ) return } console.log( - `[github-webhooks] trigger '${t.name}' (${deliveryId}) → session ${sessionId}`, + `[opencode-webhooks] trigger '${t.name}' (${deliveryId}) → session ${sessionId}`, ) await client.session.prompt({ path: { id: sessionId }, @@ -52,11 +52,11 @@ export function makeDispatcher(opts: { signal: abort.signal, }) console.log( - `[github-webhooks] trigger '${t.name}' (${deliveryId}) → session ${sessionId} completed`, + `[opencode-webhooks] trigger '${t.name}' (${deliveryId}) → session ${sessionId} completed`, ) } catch (err) { console.error( - `[github-webhooks] trigger '${t.name}' (${deliveryId}) failed:`, + `[opencode-webhooks] trigger '${t.name}' (${deliveryId}) failed:`, err, ) } finally { diff --git a/plugins/github-webhooks/handler.ts b/packages/opencode-webhooks/src/handler.ts similarity index 98% rename from plugins/github-webhooks/handler.ts rename to packages/opencode-webhooks/src/handler.ts index 305fd39..5a5c849 100644 --- a/plugins/github-webhooks/handler.ts +++ b/packages/opencode-webhooks/src/handler.ts @@ -29,7 +29,7 @@ export function makeFetchHandler(opts: { const url = new URL(req.url) if (req.method === "GET" && url.pathname === "/healthz") { - return Response.json({ ok: true, plugin: "github-webhooks" }) + return Response.json({ ok: true, plugin: "opencode-webhooks" }) } if (req.method !== "POST" || url.pathname !== "/webhooks/github") { return new Response("not found", { status: 404 }) diff --git a/plugins/github-webhooks/hmac.ts b/packages/opencode-webhooks/src/hmac.ts similarity index 100% rename from plugins/github-webhooks/hmac.ts rename to packages/opencode-webhooks/src/hmac.ts diff --git a/plugins/github-webhooks.ts b/packages/opencode-webhooks/src/index.ts similarity index 52% rename from plugins/github-webhooks.ts rename to packages/opencode-webhooks/src/index.ts index eed8a36..e02e45b 100644 --- a/plugins/github-webhooks.ts +++ b/packages/opencode-webhooks/src/index.ts @@ -1,25 +1,34 @@ -// github-webhooks: receives GitHub webhooks, dispatches to OpenCode +// opencode-webhooks: receives GitHub webhooks, dispatches to OpenCode // agents via the in-process SDK client. Listener on WEBHOOK_PORT // (default 5050). Trigger config in webhooks.json. -// -// Implementation modules live in ./github-webhooks/ — this file is the -// thin orchestration layer that opencode loads. import type { Plugin } from "@opencode-ai/plugin" import { homedir } from "node:os" -import { resolveBotLogin } from "./github-webhooks/bot-identity" -import { configPath, normalizeTrigger, readWebhookConfig } from "./github-webhooks/config" -import { makeDispatcher } from "./github-webhooks/dispatch" -import { makeFetchHandler } from "./github-webhooks/handler" -import { makeDrainCounter, makeSemaphore } from "./github-webhooks/semaphore" -import { openDeliveryStore } from "./github-webhooks/storage" +import { resolveBotLogin } from "./bot-identity" +import { configPath, normalizeTrigger, readWebhookConfig } from "./config" +import { makeDispatcher } from "./dispatch" +import { makeFetchHandler } from "./handler" +import { makeDrainCounter, makeSemaphore } from "./semaphore" +import { openDeliveryStore } from "./storage" +export type { Trigger, WebhookConfig, NormalizedTrigger, SkippedDispatch } from "./types" export const GitHubWebhooksPlugin: Plugin = async (ctx) => { + // Bun-only: we use Bun.serve, Bun.spawn, Bun.file, and bun:sqlite. + // OpenCode runs on Bun by default so this is normally a no-op; the + // check exists so consumers running an unofficial Node-based fork + // get a useful error instead of a cryptic ReferenceError on first + // dispatch. + if (typeof Bun === "undefined") { + throw new Error( + "opencode-webhooks requires Bun (uses Bun.serve, Bun.spawn, Bun.file, bun:sqlite). Install Bun >=1.2.0: https://bun.sh", + ) + } + // Don't let a dispatch bug take down the host opencode server. const guard = globalThis as { __ghWebhookGuard?: boolean } if (!guard.__ghWebhookGuard) { process.on("unhandledRejection", (err) => { - console.error("[github-webhooks] unhandledRejection:", err) + console.error("[opencode-webhooks] unhandledRejection:", err) }) guard.__ghWebhookGuard = true } @@ -32,15 +41,18 @@ export const GitHubWebhooksPlugin: Plugin = async (ctx) => { const maxConcurrent = Math.max(1, cfg.max_concurrent ?? 2) const defaultCwd = cfg.default_cwd ?? ctx.directory const retention = cfg.retention ?? 1000 - const dbPath = - cfg.db_path ?? `${homedir()}/dev/.opencode/github-webhooks.sqlite` + // Default DB path follows the XDG data spec; consumers running in + // sandboxed/persistent-volume environments should override `db_path` + // in webhooks.json to point at their persistent location. + const xdgDataHome = process.env.XDG_DATA_HOME || `${homedir()}/.local/share` + const dbPath = cfg.db_path ?? `${xdgDataHome}/opencode-webhooks/deliveries.sqlite` const botLogin = await resolveBotLogin() if (botLogin) { - console.log(`[github-webhooks] bot identity: ${botLogin}`) + console.log(`[opencode-webhooks] bot identity: ${botLogin}`) } else { console.warn( - `[github-webhooks] WARNING: could not resolve bot identity via 'gh api user' — triggers with require_bot_match will be skipped. Set GH_TOKEN to enable identity-gated triggers.`, + `[opencode-webhooks] WARNING: could not resolve bot identity via 'gh api user' — triggers with require_bot_match will be skipped. Set GH_TOKEN to enable identity-gated triggers.`, ) } @@ -48,13 +60,13 @@ export const GitHubWebhooksPlugin: Plugin = async (ctx) => { if (triggers.length === 0) { console.log( - `[github-webhooks] no triggers configured (looked at ${configPath()}) — listener disabled`, + `[opencode-webhooks] no triggers configured (looked at ${configPath()}) — listener disabled`, ) return {} } if (!secret) { console.warn( - `[github-webhooks] WARNING: no HMAC secret configured (set "secret" in ${configPath()} or GITHUB_WEBHOOK_SECRET) — webhooks will be rejected with 503 until you set one`, + `[opencode-webhooks] WARNING: no HMAC secret configured (set "secret" in ${configPath()} or GITHUB_WEBHOOK_SECRET) — webhooks will be rejected with 503 until you set one`, ) } @@ -80,7 +92,7 @@ export const GitHubWebhooksPlugin: Plugin = async (ctx) => { const server = Bun.serve({ port, hostname: "0.0.0.0", fetch }) console.log( - `[github-webhooks] listening on http://0.0.0.0:${port} (db: ${dbPath}, triggers: ${triggers.length})`, + `[opencode-webhooks] listening on http://0.0.0.0:${port} (db: ${dbPath}, triggers: ${triggers.length})`, ) // Graceful shutdown: stop accepting new connections, drain in-flight @@ -90,7 +102,7 @@ export const GitHubWebhooksPlugin: Plugin = async (ctx) => { if (stopping) return stopping = true console.log( - `[github-webhooks] received ${sig}, closing listener (in-flight dispatches: ${drainCounter.inFlight()})`, + `[opencode-webhooks] received ${sig}, closing listener (in-flight dispatches: ${drainCounter.inFlight()})`, ) server.stop(true) const drainTimeoutMs = 25_000 @@ -101,10 +113,10 @@ export const GitHubWebhooksPlugin: Plugin = async (ctx) => { setTimeout(() => reject(new Error("drain timeout")), drainTimeoutMs), ), ]) - console.log(`[github-webhooks] all dispatches drained`) + console.log(`[opencode-webhooks] all dispatches drained`) } catch { console.warn( - `[github-webhooks] drain timeout after ${drainTimeoutMs}ms — ${drainCounter.inFlight()} dispatch(es) still in flight`, + `[opencode-webhooks] drain timeout after ${drainTimeoutMs}ms — ${drainCounter.inFlight()} dispatch(es) still in flight`, ) } } diff --git a/plugins/github-webhooks/matchers.ts b/packages/opencode-webhooks/src/matchers.ts similarity index 100% rename from plugins/github-webhooks/matchers.ts rename to packages/opencode-webhooks/src/matchers.ts diff --git a/plugins/github-webhooks/semaphore.ts b/packages/opencode-webhooks/src/semaphore.ts similarity index 100% rename from plugins/github-webhooks/semaphore.ts rename to packages/opencode-webhooks/src/semaphore.ts diff --git a/plugins/github-webhooks/storage.ts b/packages/opencode-webhooks/src/storage.ts similarity index 100% rename from plugins/github-webhooks/storage.ts rename to packages/opencode-webhooks/src/storage.ts diff --git a/plugins/github-webhooks/template.ts b/packages/opencode-webhooks/src/template.ts similarity index 100% rename from plugins/github-webhooks/template.ts rename to packages/opencode-webhooks/src/template.ts diff --git a/plugins/github-webhooks/types.ts b/packages/opencode-webhooks/src/types.ts similarity index 96% rename from plugins/github-webhooks/types.ts rename to packages/opencode-webhooks/src/types.ts index 639c073..65780d7 100644 --- a/plugins/github-webhooks/types.ts +++ b/packages/opencode-webhooks/src/types.ts @@ -1,4 +1,4 @@ -// Shared types for the github-webhooks plugin. +// Shared types for the opencode-webhooks plugin. export type Trigger = { name: string diff --git a/packages/opencode-webhooks/tsconfig.json b/packages/opencode-webhooks/tsconfig.json new file mode 100644 index 0000000..5613aac --- /dev/null +++ b/packages/opencode-webhooks/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "noEmit": true, + "resolveJsonModule": true, + "isolatedModules": true, + "types": ["bun"] + }, + "include": ["src/**/*"] +} diff --git a/webhooks.json b/webhooks.json index fa2bf95..c5eac32 100644 --- a/webhooks.json +++ b/webhooks.json @@ -2,6 +2,7 @@ "max_concurrent": 2, "timeout_ms": 1800000, "retention": 1000, + "db_path": "/home/developer/dev/.opencode/github-webhooks.sqlite", "triggers": [ { "name": "issue-assigned",