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