From 133c0cab47da2213a23449be389ed6cd906f21ab Mon Sep 17 00:00:00 2001 From: fez5587 Date: Tue, 31 Mar 2026 19:02:14 -0400 Subject: [PATCH] Viking Updates --- MEMORY_FIRST_POLICY.md | 10 ++ README.md | 13 ++ scripts/install-linux.sh | 3 + scripts/install-windows.ps1 | 2 + templates/scripts/openviking-mcp.js | 206 ++++++++++++++++++++++++++++ 5 files changed, 234 insertions(+) create mode 100644 MEMORY_FIRST_POLICY.md create mode 100755 templates/scripts/openviking-mcp.js diff --git a/MEMORY_FIRST_POLICY.md b/MEMORY_FIRST_POLICY.md new file mode 100644 index 0000000..69ea923 --- /dev/null +++ b/MEMORY_FIRST_POLICY.md @@ -0,0 +1,10 @@ +# Memory-First Policy (OpenViking) + +Use this policy text in any agent `systemPrompt` where you want lower token usage: + +1. For non-trivial tasks, call `openviking_search` first with `top_k` 3-5. +2. If relevant matches exist, answer from retrieved evidence instead of broad context expansion. +3. If needed, call `openviking_read` only on the top hits. +4. Keep injected context compact (summary + small excerpts), avoid large raw dumps. +5. After producing reusable output, call `openviking_upsert` with concise notes. +6. If OpenViking is unavailable, continue using normal model fallbacks without blocking. diff --git a/README.md b/README.md index 79591e9..474d37a 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,12 @@ This folder is safe to commit to GitHub. It contains config and installers, but - `templates/opencode.json` - `templates/oh-my-opencode.json` +- `templates/scripts/openviking-mcp.js` - `scripts/install-linux.sh` - `scripts/install-windows.ps1` - `.env.example` - `OBSIDIAN_KNOWLEDGE_WORKFLOW.md` +- `MEMORY_FIRST_POLICY.md` ## How to use @@ -36,6 +38,17 @@ $env:INSTALL_IMPECCABLE="1" .\scripts\install-windows.ps1 ``` +## OpenViking MCP bridge + +The bundle includes a local OpenViking bridge script (`scripts/openviking-mcp.js`). + +- Default OpenViking server URL: `http://127.0.0.1:1933` +- Override with env var: `OPENVIKING_BASE_URL` +- Tools exposed to OpenCode: + - `openviking_search` + - `openviking_read` + - `openviking_upsert` + ## Important - Do not commit `.env`. diff --git a/scripts/install-linux.sh b/scripts/install-linux.sh index 2ce1a68..2ba0e85 100755 --- a/scripts/install-linux.sh +++ b/scripts/install-linux.sh @@ -14,6 +14,9 @@ echo "Installing OpenCode config to: $TARGET_DIR" mkdir -p "$TARGET_DIR" cp "$TEMPLATES_DIR/opencode.json" "$TARGET_DIR/opencode.json" cp "$TEMPLATES_DIR/oh-my-opencode.json" "$TARGET_DIR/oh-my-opencode.json" +mkdir -p "$TARGET_DIR/scripts" +cp "$TEMPLATES_DIR/scripts/openviking-mcp.js" "$TARGET_DIR/scripts/openviking-mcp.js" +chmod +x "$TARGET_DIR/scripts/openviking-mcp.js" echo "Installing optional components (Node/Bun packages)..." if command -v bun >/dev/null 2>&1; then diff --git a/scripts/install-windows.ps1 b/scripts/install-windows.ps1 index 06fc64a..7b36060 100644 --- a/scripts/install-windows.ps1 +++ b/scripts/install-windows.ps1 @@ -13,6 +13,8 @@ Write-Host "Installing OpenCode config to: $TargetDir" New-Item -ItemType Directory -Path $TargetDir -Force | Out-Null Copy-Item (Join-Path $TemplatesDir "opencode.json") (Join-Path $TargetDir "opencode.json") -Force Copy-Item (Join-Path $TemplatesDir "oh-my-opencode.json") (Join-Path $TargetDir "oh-my-opencode.json") -Force +New-Item -ItemType Directory -Path (Join-Path $TargetDir "scripts") -Force | Out-Null +Copy-Item (Join-Path $TemplatesDir "scripts\openviking-mcp.js") (Join-Path $TargetDir "scripts\openviking-mcp.js") -Force Write-Host "Installing optional components (Node/Bun packages)..." if (Get-Command bun -ErrorAction SilentlyContinue) { diff --git a/templates/scripts/openviking-mcp.js b/templates/scripts/openviking-mcp.js new file mode 100755 index 0000000..ead040d --- /dev/null +++ b/templates/scripts/openviking-mcp.js @@ -0,0 +1,206 @@ +#!/usr/bin/env node +"use strict"; + +const BASE_URL = (process.env.OPENVIKING_BASE_URL || "http://127.0.0.1:1933").replace(/\/+$/, ""); +const SEARCH_PATHS = (process.env.OPENVIKING_SEARCH_PATHS || "/api/v1/search,/find") + .split(",") + .map((s) => s.trim()) + .filter(Boolean); +const READ_PATHS = (process.env.OPENVIKING_READ_PATHS || "/api/v1/read,/read") + .split(",") + .map((s) => s.trim()) + .filter(Boolean); +const UPSERT_PATHS = (process.env.OPENVIKING_UPSERT_PATHS || "/api/v1/add_resource,/add_resource") + .split(",") + .map((s) => s.trim()) + .filter(Boolean); + +function writeMessage(msg) { + const body = Buffer.from(JSON.stringify(msg), "utf8"); + process.stdout.write(`Content-Length: ${body.length}\r\n\r\n`); + process.stdout.write(body); +} + +async function postWithFallback(paths, payload) { + let lastError = null; + for (const path of paths) { + try { + const res = await fetch(`${BASE_URL}${path}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + if (!res.ok) { + const text = await res.text(); + lastError = new Error(`HTTP ${res.status} on ${path}: ${text.slice(0, 400)}`); + continue; + } + return await res.json(); + } catch (err) { + lastError = err; + } + } + throw lastError || new Error("All endpoint attempts failed"); +} + +function makeTools() { + return [ + { + name: "openviking_search", + description: "Search OpenViking memory/context and return top matches.", + inputSchema: { + type: "object", + properties: { + query: { type: "string", description: "Search query." }, + top_k: { type: "integer", minimum: 1, maximum: 20, default: 5 }, + namespace: { type: "string", description: "Optional namespace/project bucket." }, + }, + required: ["query"], + }, + }, + { + name: "openviking_read", + description: "Read a resource from OpenViking by URI or ID.", + inputSchema: { + type: "object", + properties: { + uri: { type: "string" }, + id: { type: "string" }, + namespace: { type: "string" }, + }, + }, + }, + { + name: "openviking_upsert", + description: "Add or update a memory resource in OpenViking.", + inputSchema: { + type: "object", + properties: { + id: { type: "string" }, + uri: { type: "string" }, + title: { type: "string" }, + content: { type: "string" }, + namespace: { type: "string" }, + metadata: { type: "object" }, + }, + required: ["content"], + }, + }, + ]; +} + +async function handleToolCall(name, args) { + if (name === "openviking_search") { + const payload = { + query: args.query, + top_k: args.top_k ?? 5, + namespace: args.namespace, + }; + const data = await postWithFallback(SEARCH_PATHS, payload); + return { + content: [{ type: "text", text: JSON.stringify(data, null, 2) }], + }; + } + if (name === "openviking_read") { + const payload = { + uri: args.uri, + id: args.id, + namespace: args.namespace, + }; + const data = await postWithFallback(READ_PATHS, payload); + return { + content: [{ type: "text", text: JSON.stringify(data, null, 2) }], + }; + } + if (name === "openviking_upsert") { + const payload = { + id: args.id, + uri: args.uri, + title: args.title, + content: args.content, + namespace: args.namespace, + metadata: args.metadata, + }; + const data = await postWithFallback(UPSERT_PATHS, payload); + return { + content: [{ type: "text", text: JSON.stringify(data, null, 2) }], + }; + } + throw new Error(`Unknown tool: ${name}`); +} + +async function onRequest(msg) { + const { id, method, params } = msg; + try { + if (method === "initialize") { + writeMessage({ + jsonrpc: "2.0", + id, + result: { + protocolVersion: "2024-11-05", + serverInfo: { name: "openviking-mcp-bridge", version: "0.1.0" }, + capabilities: { tools: {} }, + }, + }); + return; + } + if (method === "notifications/initialized") { + return; + } + if (method === "tools/list") { + writeMessage({ + jsonrpc: "2.0", + id, + result: { tools: makeTools() }, + }); + return; + } + if (method === "tools/call") { + const toolName = params?.name; + const args = params?.arguments || {}; + const result = await handleToolCall(toolName, args); + writeMessage({ jsonrpc: "2.0", id, result }); + return; + } + writeMessage({ + jsonrpc: "2.0", + id, + error: { code: -32601, message: `Method not found: ${method}` }, + }); + } catch (err) { + writeMessage({ + jsonrpc: "2.0", + id, + error: { code: -32000, message: err && err.message ? err.message : String(err) }, + }); + } +} + +let buffer = Buffer.alloc(0); +process.stdin.on("data", (chunk) => { + buffer = Buffer.concat([buffer, chunk]); + while (true) { + const headerEnd = buffer.indexOf("\r\n\r\n"); + if (headerEnd === -1) break; + const headerText = buffer.slice(0, headerEnd).toString("utf8"); + const match = headerText.match(/Content-Length:\s*(\d+)/i); + if (!match) { + buffer = buffer.slice(headerEnd + 4); + continue; + } + const length = Number(match[1]); + const total = headerEnd + 4 + length; + if (buffer.length < total) break; + const body = buffer.slice(headerEnd + 4, total).toString("utf8"); + buffer = buffer.slice(total); + let msg; + try { + msg = JSON.parse(body); + } catch { + continue; + } + if (msg && msg.jsonrpc === "2.0" && msg.method) { + onRequest(msg); + } + } +});