@openviking/opencode-plugin 0.1.6 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,111 @@
1
+ // GENERATED FROM examples/memory-plugin-shared/lib. DO NOT EDIT.
2
+ /**
3
+ * Interactive ovcli.conf setup wizard shared by the OpenViking memory plugins.
4
+ *
5
+ * Lets a pure-marketplace install (no installer script) configure the server
6
+ * URL and API key that both the lifecycle hooks and the stdio MCP proxy read:
7
+ *
8
+ * node <plugin>/scripts/setup.mjs
9
+ *
10
+ * Conventions: show current values, keep existing secrets on empty input,
11
+ * merge-write (never drop unknown fields), back up the previous file, 0600.
12
+ */
13
+
14
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from "node:fs";
15
+ import { dirname } from "node:path";
16
+ import { createInterface } from "node:readline/promises";
17
+ import { loadCredentialFiles } from "./credentials.mjs";
18
+
19
+ const CLOUD_URL = "https://api.vikingdb.cn-beijing.volces.com/openviking";
20
+ const LOCAL_URL = "http://127.0.0.1:1933";
21
+
22
+ function maskSecret(value) {
23
+ const s = String(value || "");
24
+ if (!s) return "(not set)";
25
+ if (s.length <= 8) return "****";
26
+ return `${s.slice(0, 4)}…${s.slice(-4)} (${s.length} chars)`;
27
+ }
28
+
29
+ function readJsonSafe(path) {
30
+ try {
31
+ return JSON.parse(readFileSync(path, "utf-8"));
32
+ } catch {
33
+ return {};
34
+ }
35
+ }
36
+
37
+ export async function runSetupWizard({
38
+ input = process.stdin,
39
+ output = process.stdout,
40
+ env = process.env,
41
+ } = {}) {
42
+ const { cliPath } = loadCredentialFiles(env);
43
+ const current = readJsonSafe(cliPath);
44
+ const rl = createInterface({ input, output });
45
+ const say = (line = "") => output.write(`${line}\n`);
46
+
47
+ try {
48
+ say("OpenViking memory plugin setup");
49
+ say(`Config file: ${cliPath}`);
50
+ say("");
51
+ say("Current values:");
52
+ say(` url: ${current.url || "(not set)"}`);
53
+ say(` api_key: ${maskSecret(current.api_key)}`);
54
+ if (current.account) say(` account: ${current.account}`);
55
+ if (current.user) say(` user: ${current.user}`);
56
+ say("");
57
+
58
+ const defaultUrl = current.url || LOCAL_URL;
59
+ say("Where do you connect to OpenViking?");
60
+ say(` 1) Self-hosted / local [${LOCAL_URL}]`);
61
+ say(` 2) Volcengine OpenViking Cloud [${CLOUD_URL}]`);
62
+ say(` 3) Custom URL / keep current [${defaultUrl}]`);
63
+ const mode = (await rl.question("Choice [1/2/3, default 3]: ")).trim();
64
+ let url = defaultUrl;
65
+ if (mode === "1") {
66
+ url = LOCAL_URL;
67
+ } else if (mode === "2") {
68
+ url = CLOUD_URL;
69
+ } else {
70
+ const answer = (await rl.question(`Server URL [${defaultUrl}]: `)).trim();
71
+ if (answer) url = answer;
72
+ }
73
+
74
+ const keyPrompt = current.api_key
75
+ ? `API key [enter = keep ${maskSecret(current.api_key)}, '-' = clear]: `
76
+ : "API key (leave empty for unauthenticated local mode): ";
77
+ const keyAnswer = await rl.question(keyPrompt);
78
+ let apiKey = current.api_key || "";
79
+ if (keyAnswer === "-") apiKey = "";
80
+ else if (keyAnswer.trim()) apiKey = keyAnswer.trim();
81
+ else if (!current.api_key) apiKey = "";
82
+
83
+ const next = { ...current, url, api_key: apiKey };
84
+
85
+ say("");
86
+ say("Changes:");
87
+ say(` url: ${current.url || "(not set)"} -> ${next.url}`);
88
+ say(` api_key: ${maskSecret(current.api_key)} -> ${maskSecret(next.api_key)}`);
89
+ const confirm = (await rl.question("Write config? [Y/n] ")).trim().toLowerCase();
90
+ if (confirm === "n" || confirm === "no") {
91
+ say("Aborted; nothing written.");
92
+ return { written: false, path: cliPath };
93
+ }
94
+
95
+ mkdirSync(dirname(cliPath), { recursive: true });
96
+ if (existsSync(cliPath)) {
97
+ copyFileSync(cliPath, `${cliPath}.bak.${Date.now()}`);
98
+ }
99
+ writeFileSync(cliPath, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 });
100
+ try {
101
+ chmodSync(cliPath, 0o600);
102
+ } catch {
103
+ /* best effort on platforms without chmod semantics */
104
+ }
105
+ say(`Written: ${cliPath}`);
106
+ say("The stdio MCP proxy and hooks pick this up on the next harness start.");
107
+ return { written: true, path: cliPath };
108
+ } finally {
109
+ rl.close();
110
+ }
111
+ }
@@ -0,0 +1,56 @@
1
+ // GENERATED FROM examples/memory-plugin-shared/lib. DO NOT EDIT.
2
+ const DEFAULT_URI_KEYS = [
3
+ "filePath",
4
+ "file_path",
5
+ "filepath",
6
+ "path",
7
+ "uri",
8
+ "target_uri",
9
+ "targetUri",
10
+ "pattern",
11
+ ];
12
+
13
+ export function normalizeToolName(value) {
14
+ return String(value || "").trim().toLowerCase();
15
+ }
16
+
17
+ export function findVikingUri(args = {}, keys = DEFAULT_URI_KEYS) {
18
+ if (!args || typeof args !== "object") return null;
19
+ for (const key of keys) {
20
+ const uri = findVikingUriInValue(args[key]);
21
+ if (uri) return uri;
22
+ }
23
+ return findVikingUriInValue(args);
24
+ }
25
+
26
+ export function findVikingUriInValue(value) {
27
+ if (typeof value === "string") {
28
+ const match = value.match(/\bviking:\/\/[^\s"'`<>)]*/i);
29
+ return match?.[0] || null;
30
+ }
31
+ if (Array.isArray(value)) {
32
+ for (const item of value) {
33
+ const uri = findVikingUriInValue(item);
34
+ if (uri) return uri;
35
+ }
36
+ return null;
37
+ }
38
+ if (value && typeof value === "object") {
39
+ for (const item of Object.values(value)) {
40
+ const uri = findVikingUriInValue(item);
41
+ if (uri) return uri;
42
+ }
43
+ }
44
+ return null;
45
+ }
46
+
47
+ export function buildGuardMessage(uri, hint = {}) {
48
+ const tool = hint.tool || "the OpenViking MCP tools";
49
+ const example = typeof hint.example === "function" ? hint.example(uri) : hint.example;
50
+ const lines = [
51
+ "viking:// URIs are OpenViking virtual paths, not local filesystem paths.",
52
+ `Use ${tool} instead.`,
53
+ ];
54
+ if (example) lines.push(`Example: ${example}`);
55
+ return lines.join("\n");
56
+ }
package/lib/utils.mjs CHANGED
@@ -1,123 +1,8 @@
1
1
  import fs from "fs"
2
2
  import path from "path"
3
- import { homedir } from "os"
4
-
5
- export const DEFAULT_CONFIG = {
6
- endpoint: "http://localhost:1933",
7
- apiKey: "",
8
- account: "",
9
- user: "",
10
- peerId: "",
11
- enabled: true,
12
- timeoutMs: 30000,
13
- runtime: {
14
- dataDir: "",
15
- },
16
- repoContext: {
17
- enabled: true,
18
- cacheTtlMs: 60000,
19
- },
20
- autoRecall: {
21
- enabled: true,
22
- limit: 6,
23
- scoreThreshold: 0.15,
24
- maxContentChars: 500,
25
- preferAbstract: true,
26
- tokenBudget: 2000,
27
- },
28
- }
29
3
 
30
4
  let logFilePath = null
31
5
 
32
- function cloneDefaultConfig() {
33
- return JSON.parse(JSON.stringify(DEFAULT_CONFIG))
34
- }
35
-
36
- function mergeConfig(fileConfig = {}) {
37
- const config = cloneDefaultConfig()
38
- for (const key of ["endpoint", "apiKey", "account", "user", "peerId", "enabled", "timeoutMs"]) {
39
- if (fileConfig[key] !== undefined) config[key] = fileConfig[key]
40
- }
41
- config.runtime = {
42
- ...DEFAULT_CONFIG.runtime,
43
- dataDir: fileConfig.runtime?.dataDir ?? DEFAULT_CONFIG.runtime.dataDir,
44
- }
45
- config.repoContext = { ...DEFAULT_CONFIG.repoContext, ...(fileConfig.repoContext ?? {}) }
46
- config.autoRecall = { ...DEFAULT_CONFIG.autoRecall, ...(fileConfig.autoRecall ?? {}) }
47
-
48
- if (process.env.OPENVIKING_API_KEY) {
49
- config.apiKey = process.env.OPENVIKING_API_KEY
50
- }
51
- if (process.env.OPENVIKING_ACCOUNT) {
52
- config.account = process.env.OPENVIKING_ACCOUNT
53
- }
54
- if (process.env.OPENVIKING_USER) {
55
- config.user = process.env.OPENVIKING_USER
56
- }
57
- if (process.env.OPENVIKING_PEER_ID) {
58
- config.peerId = process.env.OPENVIKING_PEER_ID
59
- }
60
-
61
- config.timeoutMs = normalizeNumber(config.timeoutMs, DEFAULT_CONFIG.timeoutMs, 1000, 300000)
62
- config.repoContext.cacheTtlMs = normalizeNumber(
63
- config.repoContext.cacheTtlMs,
64
- DEFAULT_CONFIG.repoContext.cacheTtlMs,
65
- 1000,
66
- 60 * 60 * 1000,
67
- )
68
- clampRecallConfig(config.autoRecall)
69
- return config
70
- }
71
-
72
- function normalizeNumber(value, fallback, min, max) {
73
- const next = Number(value)
74
- if (!Number.isFinite(next)) return fallback
75
- return Math.max(min, Math.min(max, next))
76
- }
77
-
78
- function clampRecallConfig(recall) {
79
- recall.limit = Math.max(1, Math.min(50, Math.round(Number(recall.limit) || 6)))
80
- recall.scoreThreshold = Math.max(0, Math.min(1, Number(recall.scoreThreshold) || 0))
81
- recall.maxContentChars = Math.max(100, Math.min(5000, Math.round(Number(recall.maxContentChars) || 500)))
82
- recall.tokenBudget = Math.max(100, Math.min(10000, Math.round(Number(recall.tokenBudget) || 2000)))
83
- }
84
-
85
- export function loadConfig(pluginRoot, projectDirectory) {
86
- for (const configPath of getConfigPaths(pluginRoot, projectDirectory)) {
87
- try {
88
- if (fs.existsSync(configPath)) {
89
- const fileConfig = JSON.parse(fs.readFileSync(configPath, "utf8"))
90
- return mergeConfig(fileConfig)
91
- }
92
- } catch (error) {
93
- console.warn(`Failed to load OpenViking config from ${configPath}:`, error)
94
- }
95
- }
96
- return mergeConfig()
97
- }
98
-
99
- function getConfigPaths(pluginRoot, projectDirectory) {
100
- const paths = []
101
- if (process.env.OPENVIKING_PLUGIN_CONFIG) paths.push(expandHome(process.env.OPENVIKING_PLUGIN_CONFIG))
102
- if (projectDirectory) paths.push(path.join(projectDirectory, ".opencode", "openviking-config.json"))
103
- paths.push(path.join(homedir(), ".config", "opencode", "openviking-config.json"))
104
- paths.push(path.join(pluginRoot, "openviking-config.json"))
105
- return paths
106
- }
107
-
108
- export function resolveDataDir(pluginRoot, config) {
109
- const configured = config.runtime?.dataDir
110
- if (configured) return expandHome(configured)
111
- return path.join(homedir(), ".config", "opencode", "openviking")
112
- }
113
-
114
- function expandHome(value) {
115
- if (!value || typeof value !== "string") return value
116
- if (value === "~") return homedir()
117
- if (value.startsWith("~/") || value.startsWith("~\\")) return path.join(homedir(), value.slice(2))
118
- return value
119
- }
120
-
121
6
  export function initLogger(dataDir) {
122
7
  fs.mkdirSync(dataDir, { recursive: true })
123
8
  logFilePath = path.join(dataDir, "openviking-memory.log")
@@ -183,6 +68,38 @@ export function effectivePeerId(config) {
183
68
  return String(config.peerId || "").trim() || null
184
69
  }
185
70
 
71
+ export async function fetchJSON(config, endpoint, init = {}, options = {}) {
72
+ const url = `${normalizeEndpoint(config.endpoint)}${endpoint}`
73
+ const headers = makeAuthHeaders(
74
+ config,
75
+ { "Content-Type": "application/json", ...(init.headers ?? {}) },
76
+ options.actorPeerId,
77
+ )
78
+ const controller = new AbortController()
79
+ const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? config.timeoutMs)
80
+ try {
81
+ const response = await fetch(url, {
82
+ ...init,
83
+ headers,
84
+ signal: controller.signal,
85
+ })
86
+ const text = await response.text()
87
+ const payload = text ? parseJsonOrText(text) : {}
88
+ if (!response.ok || payload?.status === "error") {
89
+ return {
90
+ ok: false,
91
+ status: response.status,
92
+ error: payload?.error || payload?.message || { message: `HTTP ${response.status}` },
93
+ }
94
+ }
95
+ return { ok: true, status: response.status, result: payload?.result ?? payload }
96
+ } catch (error) {
97
+ return { ok: false, status: 0, error: { message: error?.message ?? String(error) } }
98
+ } finally {
99
+ clearTimeout(timeout)
100
+ }
101
+ }
102
+
186
103
  export async function makeRequest(config, options) {
187
104
  const url = `${normalizeEndpoint(config.endpoint)}${options.endpoint}`
188
105
  const headers = makeAuthHeaders(
@@ -291,7 +208,7 @@ export async function makeMultipartRequest(config, options) {
291
208
 
292
209
  function makeAuthHeaders(config, headers = {}, actorPeerId = "") {
293
210
  const result = { ...headers }
294
- if (config.apiKey) result["X-API-Key"] = config.apiKey
211
+ if (config.apiKey) result["Authorization"] = `Bearer ${config.apiKey}`
295
212
  if (config.account) result["X-OpenViking-Account"] = config.account
296
213
  if (config.user) result["X-OpenViking-User"] = config.user
297
214
  const peerId = String(actorPeerId || "").trim()
@@ -1,17 +1,18 @@
1
1
  import { log } from "./utils.mjs"
2
+ import { buildGuardMessage, findVikingUri, normalizeToolName } from "./shared/uri-guard.mjs"
2
3
 
3
4
  const FILESYSTEM_TOOL_HINTS = {
4
5
  read: {
5
- replacement: "memread",
6
- example: (uri) => `memread(uri="${uri}", level="auto")`,
6
+ tool: "openviking_read",
7
+ example: (uri) => `openviking_read(uri="${uri}")`,
7
8
  },
8
9
  glob: {
9
- replacement: "membrowse",
10
- example: (uri) => `membrowse(uri="${uri}", view="list", recursive=true)`,
10
+ tool: "openviking_glob",
11
+ example: (uri) => `openviking_glob(uri="${uri}", pattern="**/*")`,
11
12
  },
12
13
  grep: {
13
- replacement: "memsearch",
14
- example: (uri, args) => `memsearch(query="${String(args.pattern ?? "").replaceAll('"', '\\"')}", target_uri="${uri}")`,
14
+ tool: "openviking_search",
15
+ example: (uri, args = {}) => `openviking_search(query="${String(args.pattern ?? "").replaceAll('"', '\\"')}", target_uri="${uri}")`,
15
16
  },
16
17
  }
17
18
 
@@ -29,24 +30,8 @@ export function createVikingUriGuard() {
29
30
  tool: toolName,
30
31
  uri,
31
32
  })
32
- throw new Error(
33
- [
34
- "viking:// URIs are OpenViking virtual paths, not local filesystem paths.",
35
- `Use ${hint.replacement} instead.`,
36
- `Example: ${hint.example(uri, args)}`,
37
- ].join("\n"),
38
- )
33
+ throw new Error(buildGuardMessage(uri, { ...hint, example: hint.example(uri, args) }))
39
34
  }
40
35
  }
41
36
 
42
- export function findVikingUri(args = {}) {
43
- for (const key of ["filePath", "path", "uri"]) {
44
- const value = args[key]
45
- if (typeof value === "string" && value.startsWith("viking://")) return value
46
- }
47
- return null
48
- }
49
-
50
- function normalizeToolName(value) {
51
- return String(value || "").trim().toLowerCase()
52
- }
37
+ export { findVikingUri, normalizeToolName }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openviking/opencode-plugin",
3
- "version": "0.1.6",
3
+ "version": "0.2.0",
4
4
  "author": "@openviking",
5
5
  "description": "Unified OpenCode plugin for OpenViking repository retrieval and long-term memory",
6
6
  "type": "module",
@@ -10,22 +10,23 @@
10
10
  "tui"
11
11
  ],
12
12
  "exports": {
13
- ".": "./index.mjs"
13
+ ".": "./index.mjs",
14
+ "./servers/mcp-proxy.mjs": "./servers/mcp-proxy.mjs"
14
15
  },
15
16
  "files": [
16
17
  "index.mjs",
17
18
  "lib/",
19
+ "servers/",
20
+ "scripts/",
18
21
  "README.md",
19
22
  "INSTALL.md",
20
23
  "INSTALL-ZH.md"
21
24
  ],
22
25
  "scripts": {
23
- "check": "node --check index.mjs && node --check lib/runtime.mjs && node --check lib/repo-context.mjs && node --check lib/memory-session.mjs && node --check lib/memadd-local.mjs && node --check lib/memory-tools.mjs && node --check lib/code-tools.mjs && node --check lib/memory-recall.mjs && node --check lib/viking-uri-guard.mjs && node --check lib/utils.mjs && node --check tests/*.test.mjs",
24
- "test": "node --test tests/*.test.mjs"
25
- },
26
- "dependencies": {
27
- "@opencode-ai/plugin": "^1.14.37"
26
+ "check": "node --check index.mjs && node --check lib/config.mjs && node --check lib/mcp-config.mjs && node --check lib/runtime.mjs && node --check lib/repo-context.mjs && node --check lib/memory-session.mjs && node --check lib/memory-recall.mjs && node --check lib/session-inject.mjs && node --check lib/viking-uri-guard.mjs && node --check lib/utils.mjs && node --check lib/shared/*.mjs && node --check servers/mcp-proxy.mjs && node --check scripts/setup.mjs && node --check tests/*.test.mjs",
27
+ "test": "node --test tests/*.test.mjs servers/*.test.mjs"
28
28
  },
29
+ "dependencies": {},
29
30
  "publishConfig": {
30
31
  "access": "public"
31
32
  },
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runSetupWizard } from "../lib/shared/setup-wizard.mjs"
4
+
5
+ runSetupWizard().catch((err) => {
6
+ process.stderr.write(`${err?.stack || err}\n`)
7
+ process.exit(1)
8
+ })
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * stdio -> streamable-HTTP MCP proxy for the OpenViking OpenCode plugin.
5
+ *
6
+ * OpenCode starts this process as a local MCP server. The proxy reads the same
7
+ * OpenViking credential sources as the lifecycle hooks, forwards JSON-RPC
8
+ * requests to the server's /mcp endpoint, and keeps stdout protocol-clean.
9
+ */
10
+
11
+ import { homedir } from "node:os"
12
+ import { join, resolve as resolvePath } from "node:path"
13
+ import { fileURLToPath } from "node:url"
14
+ import { loadConfig } from "../lib/config.mjs"
15
+ import { createLogger } from "../lib/shared/debug-log.mjs"
16
+ import { createOpenVikingMcpProxy } from "../lib/shared/mcp-proxy-core.mjs"
17
+
18
+ export { createOpenVikingMcpProxy } from "../lib/shared/mcp-proxy-core.mjs"
19
+
20
+ const DEFAULT_TIMEOUT_MS = 15000
21
+
22
+ function trimSlash(value) {
23
+ return String(value || "").replace(/\/+$/, "")
24
+ }
25
+
26
+ function normalizePath(value) {
27
+ const raw = String(value || "").trim()
28
+ if (!raw) return ""
29
+ if (raw === "~") return homedir()
30
+ if (raw.startsWith("~/")) return resolvePath(join(homedir(), raw.slice(2)))
31
+ return resolvePath(raw)
32
+ }
33
+
34
+ function uniq(values) {
35
+ return [...new Set(values.filter(Boolean))]
36
+ }
37
+
38
+ function readProxyConfig() {
39
+ const cfg = loadConfig(resolvePath(fileURLToPath(import.meta.url), "..", ".."))
40
+ const mcpUrl = cfg.mcpUrl || `${trimSlash(cfg.endpoint)}/mcp`
41
+ return {
42
+ mcpUrl,
43
+ apiKey: cfg.apiKey || "",
44
+ account: cfg.account || "",
45
+ user: cfg.user || "",
46
+ peerId: cfg.peerId || "",
47
+ timeoutMs: Math.max(1000, Number(cfg.timeoutMs) || DEFAULT_TIMEOUT_MS),
48
+ debug: cfg.debug === true,
49
+ debugLogPath: cfg.debugLogPath,
50
+ credentialSource: cfg.credentialSource || "auto",
51
+ credentialPath: cfg.credentialPath || cfg.configPath || "",
52
+ watchedPaths: uniq([
53
+ cfg.credentialPath,
54
+ cfg.configPath,
55
+ normalizePath(process.env.OPENVIKING_CLI_CONFIG_FILE),
56
+ normalizePath(process.env.OPENVIKING_CONFIG_FILE),
57
+ normalizePath(process.env.OPENVIKING_PLUGIN_CONFIG),
58
+ join(homedir(), ".openviking", "ovcli.conf"),
59
+ join(homedir(), ".openviking", "ov.conf"),
60
+ join(homedir(), ".config", "opencode", "openviking-config.json"),
61
+ ]),
62
+ }
63
+ }
64
+
65
+ if (process.argv[1] && fileURLToPath(import.meta.url) === resolvePath(process.argv[1])) {
66
+ createOpenVikingMcpProxy({ readConfig: readProxyConfig, loggerFactory: createLogger }).start()
67
+ }
@@ -0,0 +1,100 @@
1
+ import test from "node:test"
2
+ import assert from "node:assert/strict"
3
+ import { createServer } from "node:http"
4
+ import { Writable } from "node:stream"
5
+ import { createOpenVikingMcpProxy } from "./mcp-proxy.mjs"
6
+
7
+ function jsonRpc(id, result = {}) {
8
+ return { jsonrpc: "2.0", id, result }
9
+ }
10
+
11
+ function sseMessage(obj) {
12
+ return `event: message\r\ndata: ${JSON.stringify(obj)}\r\n\r\n`
13
+ }
14
+
15
+ async function readBody(req) {
16
+ const chunks = []
17
+ for await (const chunk of req) chunks.push(chunk)
18
+ return Buffer.concat(chunks).toString("utf-8")
19
+ }
20
+
21
+ async function withServer(handler, fn) {
22
+ const requests = []
23
+ const server = createServer(async (req, res) => {
24
+ const body = await readBody(req)
25
+ const entry = {
26
+ method: req.method,
27
+ url: req.url,
28
+ headers: req.headers,
29
+ body: body ? JSON.parse(body) : null,
30
+ }
31
+ requests.push(entry)
32
+ await handler(req, res, entry)
33
+ })
34
+ await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve))
35
+ try {
36
+ const { port } = server.address()
37
+ return await fn({ url: `http://127.0.0.1:${port}/mcp`, requests })
38
+ } finally {
39
+ await new Promise((resolve) => server.close(resolve))
40
+ }
41
+ }
42
+
43
+ function makeProxy(url) {
44
+ const out = []
45
+ const stdout = new Writable({
46
+ write(chunk, _encoding, callback) {
47
+ out.push(chunk.toString("utf-8"))
48
+ callback()
49
+ },
50
+ })
51
+ const proxy = createOpenVikingMcpProxy({
52
+ stdout,
53
+ readConfig: () => ({
54
+ mcpUrl: url,
55
+ apiKey: "test-key",
56
+ account: "acct",
57
+ user: "user",
58
+ peerId: "peer",
59
+ timeoutMs: 5000,
60
+ debug: false,
61
+ debugLogPath: "",
62
+ credentialSource: "test",
63
+ credentialPath: "",
64
+ watchedPaths: [],
65
+ }),
66
+ loggerFactory: () => ({ log() {}, logError() {} }),
67
+ })
68
+ return {
69
+ proxy,
70
+ async messages() {
71
+ await new Promise((resolve) => setImmediate(resolve))
72
+ return out.join("").trim().split("\n").filter(Boolean).map((line) => JSON.parse(line))
73
+ },
74
+ }
75
+ }
76
+
77
+ test("OpenCode MCP proxy forwards initialize with auth and identity headers", async () => {
78
+ await withServer((_req, res, entry) => {
79
+ assert.equal(entry.method, "POST")
80
+ assert.equal(entry.headers.authorization, "Bearer test-key")
81
+ assert.equal(entry.headers["x-openviking-account"], "acct")
82
+ assert.equal(entry.headers["x-openviking-user"], "user")
83
+ assert.equal(entry.headers["x-openviking-actor-peer"], "peer")
84
+ res.writeHead(200, {
85
+ "content-type": "text/event-stream",
86
+ "mcp-session-id": "sid-oc",
87
+ })
88
+ res.end(sseMessage(jsonRpc(1, { protocolVersion: "2025-06-18" })))
89
+ }, async ({ url, requests }) => {
90
+ const { proxy, messages } = makeProxy(url)
91
+ await proxy.handleMessage({
92
+ jsonrpc: "2.0",
93
+ id: 1,
94
+ method: "initialize",
95
+ params: { protocolVersion: "2025-06-18" },
96
+ })
97
+ assert.deepEqual(await messages(), [jsonRpc(1, { protocolVersion: "2025-06-18" })])
98
+ assert.equal(requests.length, 1)
99
+ })
100
+ })