feat: initialize opencode-owui-sync project

This commit is contained in:
2026-05-14 21:20:17 +02:00
parent 8daf5ee414
commit c488122290
7 changed files with 1146 additions and 3 deletions
+6
View File
@@ -0,0 +1,6 @@
node_modules/
dist/
build/
.env
.DS_Store
*.log
+17
View File
@@ -0,0 +1,17 @@
# Project Rules & Architecture
<!-- Static sections below are for humans and AI agents to read. -->
<!-- ## Current State and ## Session Log are managed by owui-sync plugin. -->
## Architecture
<!-- Add project-specific architecture notes here. -->
## Conventions
<!-- Add coding conventions, naming rules, etc. here. -->
## Current State
<!-- Updated by owui-sync plugin on session end -->
<!-- Single section, overwritten each time -->
- No sessions logged yet.
## Session Log
<!-- Append-only, newest first -->
+97 -3
View File
@@ -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.
+555
View File
@@ -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(/<think>[\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. <system-...>)
.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<SummaryJSON | null> {
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<string, string> {
const map = new Map<string, string>();
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, string>): 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` +
`<!-- Updated by owui-sync on session end -->\n` +
`<!-- Single section, overwritten each time -->\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<!-- Append-only, newest first -->\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<string, string> {
try {
return JSON.parse(readFileSync(ID_CACHE_FILE, "utf8"));
} catch {
return {};
}
}
function saveIdCache(cache: Record<string, string>): 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<void> {
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<Plugin>[0]["client"],
sessionId: string,
projectDirectory: string,
cfg: Config
): Promise<void> {
let result: Awaited<ReturnType<typeof client.session.messages>>;
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
);
}
+445
View File
@@ -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"
}
}
}
}
+12
View File
@@ -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"
}
}
+14
View File
@@ -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"]
}
}
}