feat: initialize opencode-owui-sync project
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
node_modules/
|
||||
dist/
|
||||
build/
|
||||
.env
|
||||
.DS_Store
|
||||
*.log
|
||||
@@ -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 -->
|
||||
@@ -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
@@ -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
|
||||
);
|
||||
}
|
||||
Generated
+445
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user