diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0d5fa7b --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ +build/ +.env +.DS_Store +*.log \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..e163c2b --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,17 @@ +# Project Rules & Architecture + + + +## Architecture + + +## Conventions + + +## Current State + + +- No sessions logged yet. + +## Session Log + diff --git a/README.md b/README.md index 49abc71..fbf1478 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,99 @@ -# OpenCode → OpenWebUI Sync +# opencode-open-webui-sync -A bridge that syncs your OpenCode session state into an OpenWebUI session—so you can review, share, or pick up an opencode work session from the browser. +Syncs context from opencode sessions into Open WebUI. When a session goes idle, it summarizes what happened using a local vLLM model, writes the result to `AGENTS.md` in the project root, and updates an Open WebUI memory so the context is automatically injected into future conversations. -Similar concept to the Obsidian ↔ OpenWebUI bridges, but focused on the OpenCode CLI workflow instead. +No persistent services required. Memory API replaces the need for a Pipelines filter. + +## How it works + +When an opencode session goes idle: + +1. The plugin reads the session transcript via the opencode SDK +2. Sends it to the local vLLM endpoint for summarization +3. Overwrites `## Current State` in `AGENTS.md` and prepends a new `## Session Log` entry +4. Upserts an Open WebUI memory for the project (keyed by git remote repo name, so it works across machines) +5. Shows a toast in opencode when done + +## Files + +| File | Purpose | +|---|---| +| `owui-sync.ts` | opencode plugin — the main implementation | +| `AGENTS.md` | Template to copy into project roots | + +## Setup + +### 1. Get your Open WebUI API key + +Open WebUI → Settings → Account → API Keys → create one. + +### 2. Add the plugin to `~/.config/opencode/config.json` + +```json +"plugin": [ + ["file:///path/to/opencode-open-webui-sync", { + "owui_url": "https://your-owui-host", + "owui_token": "sk-your-api-key" + }] +] +``` + +### 3. Copy AGENTS.md into your project + +```bash +cp /path/to/opencode-open-webui-sync/AGENTS.md /your/project/AGENTS.md +``` + +The plugin writes to `{projectDirectory}/AGENTS.md`, wherever opencode is running from. + +### 4. Verify it works + +Run an opencode session, do some work, let it go idle (~30s). Then: + +```bash +# Check AGENTS.md was updated +cat /your/project/AGENTS.md + +# Watch the plugin log +tail -f ~/.local/share/opencode/owui-sync.log +``` + +## Plugin options + +| Option | Default | Description | +|---|---|---| +| `owui_url` | — | Open WebUI base URL | +| `owui_token` | — | Open WebUI API key | +| `vllm_url` | `http://10.10.0.12:8000/v1` | vLLM base URL | +| `vllm_model` | `qwen3.6` | Model to use for summarization | +| `vllm_key` | `token-abc123` | vLLM API key | + +`owui_url` and `owui_token` are required for memory sync. Without them, only `AGENTS.md` is written. + +The vLLM options have working defaults for this setup. Override them in config if your endpoint differs. + +## Files written by the plugin + +| Path | Purpose | +|---|---| +| `{projectDir}/AGENTS.md` | Session context, updated after each session | +| `~/.local/share/opencode/owui-sync.log` | Plugin log, capped at 500 lines | +| `~/.local/share/opencode/owui-sync-ids.json` | Cache of Open WebUI memory IDs per project | + +## AGENTS.md format + +```markdown +## Current State +- concise bullets describing where the project is right now +- overwritten after every session + +## Session Log +### YYYY-MM-DD HH:MM — Session title (sessionId) +- **Worked on:** ... +- **Decisions:** ... +- **Done:** ... +- **Blockers:** ... +- **Next:** ... +``` + +Anything above `## Current State` (architecture notes, conventions, etc.) is left untouched. diff --git a/owui-sync.ts b/owui-sync.ts new file mode 100644 index 0000000..a8dfc1a --- /dev/null +++ b/owui-sync.ts @@ -0,0 +1,555 @@ +/** + * owui-sync — opencode plugin + * + * On session.idle: + * 1. Reads the session transcript via the opencode SDK client + * 2. Calls the local vLLM endpoint to summarize + * 3. Writes ## Current State + ## Session Log into {projectDir}/AGENTS.md + * 4. Upserts an Open WebUI memory so context appears in all OWUI conversations + * + * Install: add to ~/.config/opencode/config.json: + * "plugin": [ + * ["file:///path/to/opencode-open-webui-sync", { + * "owui_url": "https://your-owui-host", + * "owui_token": "sk-your-api-key" + * }] + * ] + * + * Optional overrides (all have working defaults): + * "vllm_url" — vLLM base URL (default: http://10.10.0.12:8000/v1) + * "vllm_model" — model ID (default: qwen3.6) + * "vllm_key" — vLLM API key (default: token-abc123) + * + * owui_token: Open WebUI → Settings → Account → API Key + */ + +import type { Plugin } from "@opencode-ai/plugin"; +import { appendFileSync, readFileSync, writeFileSync } from "fs"; +import { execFileSync } from "child_process"; + +// --------------------------------------------------------------------------- +// Logging +// --------------------------------------------------------------------------- + +const LOG_FILE = `${process.env.HOME}/.local/share/opencode/owui-sync.log`; +const LOG_MAX_LINES = 600; +const LOG_TRIM_TO = 500; + +function log(...args: unknown[]): void { + const line = `[${new Date().toISOString()}] ${args.map(String).join(" ")}\n`; + try { + appendFileSync(LOG_FILE, line); + rotateLogs(); + } catch { + // ignore — don't let logging break the plugin + } +} + +function rotateLogs(): void { + try { + const content = readFileSync(LOG_FILE, "utf8"); + const lines = content.split("\n"); + if (lines.length > LOG_MAX_LINES) { + writeFileSync(LOG_FILE, lines.slice(-LOG_TRIM_TO).join("\n") + "\n"); + } + } catch { + // file may not exist yet + } +} + +// --------------------------------------------------------------------------- +// Defaults — override via plugin options in config.json +// --------------------------------------------------------------------------- + +const DEFAULT_VLLM_BASE = "http://10.10.0.12:8000/v1"; +const DEFAULT_VLLM_KEY = "token-abc123"; +const DEFAULT_VLLM_MODEL = "qwen3.6"; +const VLLM_TIMEOUT_MS = 60_000; +const TRANSCRIPT_MAX_MESSAGES = 60; + +const OWUI_MEMORY_TAG = "[owui-sync]"; + +const SUMMARIZER_PROMPT = `You are a concise technical note-taker. Given this coding session transcript, extract: + +1. CURRENT STATE: 2-4 bullet points describing where the project is right now. + Focus on what exists, what works, what doesn't. Present tense. + +2. SESSION SUMMARY: What was worked on, key decisions made (with brief rationale), + what was completed, any blockers, and immediate next steps. + +Be specific and technical. No filler. Use the same terminology as the codebase. +Omit tool call details unless the result is architecturally significant. + +Output as JSON: +{ + "current_state": ["bullet 1", "bullet 2"], + "worked_on": "...", + "decisions": ["decision + rationale"], + "done": ["..."], + "blockers": ["..."], + "next": ["..."] +}`; + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +interface SummaryJSON { + current_state: string[]; + worked_on: string; + decisions: string[]; + done: string[]; + blockers: string[]; + next: string[]; +} + +interface OwuiMemory { + id: string; + content: string; +} + +interface Config { + vllmBase: string; + vllmKey: string; + vllmModel: string; + owuiUrl: string; + owuiToken: string; +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/** + * Stable cross-machine project key derived from the git remote origin repo name. + * e.g. https://github.com/user/my-repo.git → my-repo + * git@github.com:user/my-repo.git → my-repo + * Falls back to directory basename if no git remote exists. + */ +function getProjectKey(directory: string): string { + try { + const remote = execFileSync("git", ["-C", directory, "remote", "get-url", "origin"], { + encoding: "utf8", + timeout: 3000, + stdio: ["ignore", "pipe", "ignore"], + }).trim(); + // Strip trailing .git, then take the last path/colon-separated segment + const name = remote.replace(/\.git$/, "").split(/[/:]/g).at(-1); + if (name) return name; + } catch { + // not a git repo or no remote — fall through + } + return directory.split("/").filter(Boolean).at(-1) ?? "unknown"; +} + +function stripThinking(text: string): string { + return text.replace(/[\s\S]*?<\/think>/gi, "").trim(); +} + +function formatDate(d: Date): string { + const pad = (n: number) => String(n).padStart(2, "0"); + return ( + `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ` + + `${pad(d.getHours())}:${pad(d.getMinutes())}` + ); +} + +function sanitizeTitle(raw: string): string { + return raw + .replace(/<[^>]*>/g, "") // strip XML/HTML tags (e.g. ) + .replace(/[^\x20-\x7E]/g, " ") // printable ASCII only + .replace(/\s+/g, " ") + .trim() + .slice(0, 60) || "Untitled session"; +} + +function buildTranscript( + messages: Array<{ info: { role: string; id?: string }; parts: Array<{ type: string; text?: string; synthetic?: boolean; ignored?: boolean }> }> +): string { + const turns = messages.filter((m) => m.info.role === "user" || m.info.role === "assistant"); + const capped = turns.slice(-TRANSCRIPT_MAX_MESSAGES); + + log(`transcript: ${messages.length} total messages, ${turns.length} user/assistant turns, using last ${capped.length}`); + + const lines: string[] = []; + for (const msg of capped) { + const role = msg.info.role; + const allParts = msg.parts.filter((p) => p.type === "text" && p.text); + + // For assistant messages: skip synthetic/ignored parts (injected context, tool descriptions) + // For user messages: keep everything — opencode marks real user input as synthetic too + const kept = role === "assistant" + ? allParts.filter((p) => !p.synthetic && !p.ignored) + : allParts; + + const dropped = allParts.length - kept.length; + if (dropped > 0) { + log(` [${role}] ${msg.info.id ?? "?"}: dropped ${dropped}/${allParts.length} synthetic/ignored parts`); + } + + const text = kept.map((p) => p.text!).join("\n").trim(); + if (!text) continue; + lines.push(`[${role.toUpperCase()}]\n${text}`); + } + + return lines.join("\n\n"); +} + +async function callVLLM(transcript: string, cfg: Config): Promise { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), VLLM_TIMEOUT_MS); + try { + const resp = await fetch(`${cfg.vllmBase}/chat/completions`, { + method: "POST", + signal: controller.signal, + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${cfg.vllmKey}`, + }, + body: JSON.stringify({ + model: cfg.vllmModel, + messages: [ + { role: "system", content: SUMMARIZER_PROMPT }, + { role: "user", content: transcript }, + ], + temperature: 0.2, + max_tokens: 1024, + chat_template_kwargs: { enable_thinking: false }, + }), + }); + + if (!resp.ok) { + log("[ERROR]", `vLLM ${resp.status}:`, await resp.text()); + return null; + } + + const data = await resp.json(); + const raw: string = data?.choices?.[0]?.message?.content ?? ""; + const cleaned = stripThinking(raw); + const jsonMatch = cleaned.match(/\{[\s\S]*\}/); + if (!jsonMatch) { + log("[ERROR]", "no JSON in vLLM response:", cleaned); + return null; + } + const parsed = JSON.parse(jsonMatch[0]); + // Normalize: model occasionally returns worked_on as an array despite the schema + if (Array.isArray(parsed.worked_on)) { + parsed.worked_on = (parsed.worked_on as string[]).join("; "); + } + return parsed as SummaryJSON; + } catch (err) { + if ((err as Error).name === "AbortError") { + log("[WARN]", `vLLM timed out after ${VLLM_TIMEOUT_MS / 1000}s`); + } else { + log("[ERROR]", "vLLM error:", err); + } + return null; + } finally { + clearTimeout(timer); + } +} + +// --------------------------------------------------------------------------- +// AGENTS.md manipulation +// --------------------------------------------------------------------------- + +function parseSections(content: string): Map { + const map = new Map(); + const parts = content.split(/(?=^## )/m); + map.set("preamble", parts[0].startsWith("## ") ? "" : parts[0]); + for (const part of parts) { + if (!part.startsWith("## ")) continue; + const heading = part.match(/^## .+/m)?.[0] ?? ""; + map.set(heading, part); + } + return map; +} + +function serializeSections(map: Map): string { + const preamble = map.get("preamble") ?? ""; + const sections = [...map.entries()] + .filter(([k]) => k !== "preamble") + .map(([, v]) => v); + return preamble + sections.join(""); +} + +function updateAgentsMd( + existing: string, + summary: SummaryJSON, + sessionId: string, + sessionTitle: string +): string { + const sections = parseSections(existing); + + // Dedup: check only within the Session Log section, not the whole file + const logSection = sections.get("## Session Log") ?? ""; + if (logSection.includes(`(${sessionId})`)) { + log(`session ${sessionId} already in AGENTS.md, skipping`); + return existing; + } + + const date = formatDate(new Date()); + + sections.set( + "## Current State", + `## Current State\n` + + `\n` + + `\n` + + summary.current_state.map((b) => `- ${b}`).join("\n") + + "\n" + ); + + const newEntry = + `### ${date} — ${sessionTitle} (${sessionId})\n` + + `- **Worked on:** ${summary.worked_on}\n` + + (summary.decisions.length ? `- **Decisions:** ${summary.decisions.join("; ")}\n` : "") + + (summary.done.length ? `- **Done:**${summary.done.map((d) => `\n - ${d}`).join("")}\n` : "") + + (summary.blockers.length + ? `- **Blockers:**${summary.blockers.map((b) => `\n - ${b}`).join("")}\n` + : "") + + (summary.next.length ? `- **Next:**${summary.next.map((n) => `\n - ${n}`).join("")}\n` : "") + + "\n"; + + const existingLog = logSection || "## Session Log\n\n\n"; + const logLines = existingLog.split("\n"); + const insertIdx = logLines.findIndex((l) => l.startsWith("### ")); + const updatedLog = + insertIdx === -1 + ? existingLog.trimEnd() + "\n" + newEntry + : logLines.slice(0, insertIdx).join("\n") + + "\n" + + newEntry + + logLines.slice(insertIdx).join("\n"); + + sections.set("## Session Log", updatedLog); + return serializeSections(sections); +} + +// --------------------------------------------------------------------------- +// Memory ID cache — avoids the broken GET /api/v1/memories list endpoint +// Keyed by full project directory path to prevent basename collisions. +// --------------------------------------------------------------------------- + +const ID_CACHE_FILE = `${process.env.HOME}/.local/share/opencode/owui-sync-ids.json`; + +function loadIdCache(): Record { + try { + return JSON.parse(readFileSync(ID_CACHE_FILE, "utf8")); + } catch { + return {}; + } +} + +function saveIdCache(cache: Record): void { + try { + writeFileSync(ID_CACHE_FILE, JSON.stringify(cache, null, 2)); + } catch (err) { + log("[ERROR]", "failed to save ID cache:", err); + } +} + +// --------------------------------------------------------------------------- +// Open WebUI memory upsert +// --------------------------------------------------------------------------- + +async function upsertOwuiMemory( + cfg: Config, + projectDirectory: string, + summary: SummaryJSON, + sessionTitle: string +): Promise { + const headers = { + "Content-Type": "application/json", + Authorization: `Bearer ${cfg.owuiToken}`, + }; + const base = cfg.owuiUrl.replace(/\/$/, ""); + const projectName = getProjectKey(projectDirectory); + const projectTag = `${OWUI_MEMORY_TAG} project:${projectName}`; + + const memoryContent = + `${projectTag}\n` + + `Project context as of ${formatDate(new Date())} (session: ${sessionTitle})\n\n` + + `Current state:\n` + + summary.current_state.map((b) => `- ${b}`).join("\n") + + "\n\n" + + `Last session worked on: ${summary.worked_on}\n` + + (summary.next.length + ? `Next steps:\n${summary.next.map((n) => `- ${n}`).join("\n")}` + : ""); + + // Key by git repo name — same repo on any machine maps to the same memory ID + const cache = loadIdCache(); + const cachedId = cache[projectName] ?? null; + + if (cachedId) { + try { + const resp = await fetch(`${base}/api/v1/memories/${cachedId}/update`, { + method: "POST", + headers, + body: JSON.stringify({ content: memoryContent }), + }); + if (resp.ok) { + log(`updated OWUI memory ${cachedId} for project:${projectName}`); + return; + } + log(`cached memory ${cachedId} returned ${resp.status}, recreating`); + } catch (err) { + log("[ERROR]", "OWUI memory update error:", err); + } + } + + try { + const resp = await fetch(`${base}/api/v1/memories/add`, { + method: "POST", + headers, + body: JSON.stringify({ content: memoryContent }), + }); + const body = await resp.text(); + if (resp.ok) { + const created: OwuiMemory = JSON.parse(body); + cache[projectName] = created.id; + saveIdCache(cache); + log(`created OWUI memory ${created.id} for project:${projectName}`); + } else { + log("[ERROR]", `OWUI memory create failed ${resp.status}:`, body.slice(0, 200)); + } + } catch (err) { + log("[ERROR]", "OWUI memory create error:", err); + } +} + +// --------------------------------------------------------------------------- +// Toast helper — mirrors TPS meter's approach: try showToast, fall back to publish +// --------------------------------------------------------------------------- + +function toast( + client: any, + message: string, + variant: "info" | "success" | "warning" | "error" = "info", + duration = 5000 +): void { + const body = { title: "owui-sync", message, variant, duration }; + try { + if (client.tui?.showToast) { + client.tui.showToast({ body }); + return; + } + if (client.tui?.publish) { + client.tui.publish({ body: { type: "tui.toast.show", properties: body } }); + } + } catch { + // TUI not available (e.g. non-interactive mode) — silently skip + } +} + +// --------------------------------------------------------------------------- +// Plugin entry point +// --------------------------------------------------------------------------- + +export const server: Plugin = async (input, options) => { + const cfg: Config = { + vllmBase: (options?.vllm_url as string) || DEFAULT_VLLM_BASE, + vllmKey: (options?.vllm_key as string) || DEFAULT_VLLM_KEY, + vllmModel: (options?.vllm_model as string) || DEFAULT_VLLM_MODEL, + owuiUrl: (options?.owui_url as string) ?? "", + owuiToken: (options?.owui_token as string) ?? "", + }; + + if (!cfg.owuiUrl || !cfg.owuiToken) { + log("[WARN]", "owui_url/owui_token not set — AGENTS.md only, OWUI memory sync disabled"); + } + + return { + event: async ({ event }) => { + if (event.type !== "session.idle") return; + const sessionId = event.properties.sessionID; + toast(input.client, "Summarizing session → AGENTS.md + Open WebUI…"); + summarizeAndWrite(input.client, sessionId, input.directory, cfg).catch( + (err) => log("[ERROR]", "unhandled error:", err) + ); + }, + }; +}; + +async function summarizeAndWrite( + client: Parameters[0]["client"], + sessionId: string, + projectDirectory: string, + cfg: Config +): Promise { + let result: Awaited>; + try { + result = await client.session.messages({ path: { id: sessionId } }); + } catch (err) { + log("[ERROR]", "failed to read session messages:", err); + toast(client, "Failed to read session messages", "error", 8000); + return; + } + + const messages = result.data ?? []; + log(`session ${sessionId}: fetched ${messages.length} messages`); + + const transcript = buildTranscript(messages as any); + if (!transcript) { + log("empty transcript after filtering, skipping"); + return; + } + log(`full transcript (${transcript.length} chars):\n${"─".repeat(40)}\n${transcript}\n${"─".repeat(40)}`); + + // Use opencode's own generated session title + let sessionTitle = "Untitled session"; + try { + const sessionData = await client.session.get({ path: { id: sessionId } }); + const rawTitle = sessionData.data?.title ?? ""; + sessionTitle = sanitizeTitle(rawTitle); + log(`session title from API: ${JSON.stringify(rawTitle)} → ${JSON.stringify(sessionTitle)}`); + } catch (err) { + log("[WARN]", "could not fetch session title:", err); + const firstUser = (messages as any[]).find((m) => m.info?.role === "user"); + const rawTitle = + firstUser?.parts + ?.find((p: any) => p.type === "text" && !p.synthetic && !p.ignored) + ?.text?.slice(0, 120) ?? ""; + sessionTitle = sanitizeTitle(rawTitle); + log(`session title fallback: ${JSON.stringify(sessionTitle)}`); + } + + const summary = await callVLLM(transcript, cfg); + if (!summary) { + toast(client, "Summarization failed — check owui-sync.log", "error", 8000); + return; + } + log(`summary:\n${JSON.stringify(summary, null, 2)}`); + + // Update AGENTS.md + const agentsMdPath = `${projectDirectory}/AGENTS.md`; + let existing = ""; + try { + const file = Bun.file(agentsMdPath); + existing = (await file.exists()) ? await file.text() : ""; + } catch { + existing = ""; + } + + const updated = updateAgentsMd(existing, summary, sessionId, sessionTitle); + if (updated !== existing) { + try { + await Bun.write(agentsMdPath, updated); + log(`updated ${agentsMdPath}`); + } catch (err) { + log("[ERROR]", "failed to write AGENTS.md:", err); + } + } + + // Upsert Open WebUI memory + if (cfg.owuiUrl && cfg.owuiToken) { + await upsertOwuiMemory(cfg, projectDirectory, summary, sessionTitle); + } + + toast( + client, + cfg.owuiUrl ? `Synced: ${sessionTitle}` : `AGENTS.md updated: ${sessionTitle}`, + "success", + 4000 + ); +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..8e15e35 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,445 @@ +{ + "name": "opencode-owui-sync", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "opencode-owui-sync", + "version": "1.0.0", + "devDependencies": { + "bun-types": "^1.3.14" + }, + "peerDependencies": { + "@opencode-ai/plugin": "*" + } + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", + "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", + "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", + "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", + "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", + "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", + "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, + "node_modules/@opencode-ai/plugin": { + "version": "1.14.48", + "resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.14.48.tgz", + "integrity": "sha512-pb2ywByzn4i35WWJquEYyb8lDC/ph1PLXT+heucJN6Y9U/oeSw98JQV93IG7M6BUBks6MKD3DGDJdQfyD6x0rA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@opencode-ai/sdk": "1.14.48", + "effect": "4.0.0-beta.59", + "zod": "4.1.8" + }, + "peerDependencies": { + "@opentui/core": ">=0.2.6", + "@opentui/keymap": ">=0.2.6", + "@opentui/solid": ">=0.2.6" + }, + "peerDependenciesMeta": { + "@opentui/core": { + "optional": true + }, + "@opentui/keymap": { + "optional": true + }, + "@opentui/solid": { + "optional": true + } + } + }, + "node_modules/@opencode-ai/sdk": { + "version": "1.14.48", + "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.14.48.tgz", + "integrity": "sha512-wKM86jCzV/ZApyWrdm3uP8XdWcS0LMbu3FV+OWz1ChiGGg1wiIWNGMJs5CY8/QX2/rUuZrd1Q1DqvdamZ0zLeg==", + "license": "MIT", + "peer": true, + "dependencies": { + "cross-spawn": "7.0.6" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT", + "peer": true + }, + "node_modules/@types/node": { + "version": "25.7.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.7.0.tgz", + "integrity": "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.21.0" + } + }, + "node_modules/bun-types": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.3.14.tgz", + "integrity": "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "peer": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/effect": { + "version": "4.0.0-beta.59", + "resolved": "https://registry.npmjs.org/effect/-/effect-4.0.0-beta.59.tgz", + "integrity": "sha512-xyUDLeHSe8d6lWGOvR6Fgn2HL6gYeTZ/S4Jzk9uc4ZUxMPPsNZlNXrvk0C7/utQFzeX7uAWcVnG2BjbA0SRoAA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "fast-check": "^4.6.0", + "find-my-way-ts": "^0.1.6", + "ini": "^6.0.0", + "kubernetes-types": "^1.30.0", + "msgpackr": "^1.11.9", + "multipasta": "^0.2.7", + "toml": "^4.1.1", + "uuid": "^13.0.0", + "yaml": "^2.8.3" + } + }, + "node_modules/fast-check": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.8.0.tgz", + "integrity": "sha512-GOJ158CUMnN6cSahsv4+ExARvIDuzzinFjkp0E9WtiBa5zcVeLozVkWaE4IzFcc+Y48Wp1EDlUZsXRyAztQcSg==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "pure-rand": "^8.0.0" + }, + "engines": { + "node": ">=12.17.0" + } + }, + "node_modules/find-my-way-ts": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/find-my-way-ts/-/find-my-way-ts-0.1.6.tgz", + "integrity": "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==", + "license": "MIT", + "peer": true + }, + "node_modules/ini": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", + "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", + "license": "ISC", + "peer": true, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC", + "peer": true + }, + "node_modules/kubernetes-types": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/kubernetes-types/-/kubernetes-types-1.30.0.tgz", + "integrity": "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==", + "license": "Apache-2.0", + "peer": true + }, + "node_modules/msgpackr": { + "version": "1.11.12", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.12.tgz", + "integrity": "sha512-RBdJ1Un7yGlXWajrkxcSa93nvQ0w4zBf60c0yYv7YtBelP8H2FA7XsfBbMHtXKXUMUxH7zV3Zuozh+kUQWhHvg==", + "license": "MIT", + "peer": true, + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", + "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" + } + }, + "node_modules/multipasta": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/multipasta/-/multipasta-0.2.7.tgz", + "integrity": "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA==", + "license": "MIT", + "peer": true + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "license": "MIT", + "optional": true, + "peer": true, + "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" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pure-rand": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.0.tgz", + "integrity": "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "peer": true + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "peer": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/toml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/toml/-/toml-4.1.1.tgz", + "integrity": "sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20" + } + }, + "node_modules/undici-types": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.21.0.tgz", + "integrity": "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/uuid": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.2.tgz", + "integrity": "sha512-vzi9uRZ926x4XV73S/4qQaTwPXM2JBj6/6lI/byHH1jOpCzb0zDbfytgA9LcN/hzb2l7WQSQnxITOVx5un/wGw==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "peer": true, + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "peer": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/zod": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.8.tgz", + "integrity": "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..ca5f11a --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "name": "opencode-owui-sync", + "version": "1.0.0", + "type": "module", + "main": "./owui-sync.ts", + "peerDependencies": { + "@opencode-ai/plugin": "*" + }, + "devDependencies": { + "bun-types": "^1.3.14" + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..3b72b43 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "types": ["bun-types"], + "baseUrl": ".", + "paths": { + "@opencode-ai/plugin": ["../../../.config/opencode/node_modules/@opencode-ai/plugin/dist/index"], + "@opencode-ai/plugin/tui": ["../../../.config/opencode/node_modules/@opencode-ai/plugin/dist/tui"] + } + } +}