@openviking/opencode-plugin 2026.9.10 → 2026.9.21-1

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,87 @@
1
+ // GENERATED FROM examples/memory-plugin-shared/lib. DO NOT EDIT.
2
+ /**
3
+ * The per-machine workspace registry, `~/.openviking/workspaces/`.
4
+ *
5
+ * One file per workspace rather than one file listing them all: hooks are many
6
+ * short-lived processes, and a shared JSON file loses writes whenever any two
7
+ * of them touch unrelated workspaces at once.
8
+ *
9
+ * The plugins only read this layer; nothing writes it today. A user who wants
10
+ * an entry creates the file by hand, and it then outranks both workspace files
11
+ * — so it is where they keep the last word over any repository they clone.
12
+ */
13
+
14
+ import { createHash } from "node:crypto";
15
+ import { homedir } from "node:os";
16
+ import { basename, join } from "node:path";
17
+
18
+ import { readWorkspaceFile } from "./workspace-config.mjs";
19
+
20
+ export function registryDir(env = process.env) {
21
+ const home = String(env.OPENVIKING_HOME || "").trim();
22
+ const base = home ? home.replace(/^~(?=$|\/)/, homedir()) : join(homedir(), ".openviking");
23
+ return join(base, "workspaces");
24
+ }
25
+
26
+ /**
27
+ * A readable name plus a hash, keyed on the workspace's identity rather than
28
+ * its path wherever git supplies one.
29
+ *
30
+ * Two linked worktrees of one repository are one workspace — the same peer and
31
+ * the same settings — and keying on the checkout path would silently split
32
+ * them in two. Outside a repository there is no
33
+ * identity but the path, so two `~/src/api` clones still get separate entries.
34
+ */
35
+ export function slotName(root, identity = null) {
36
+ const path = String(root || "");
37
+ const key = identity ? identityKey(identity) : "path";
38
+ const source = key === "path" ? path : key;
39
+ const label = key.startsWith("remote:") ? key.split("/").pop() : basename(path);
40
+ const readable = String(label || "").replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
41
+ const digest = createHash("sha256").update(source).digest("hex").slice(0, 12);
42
+ return `${readable ? `${readable}-` : ""}${digest}.json`;
43
+ }
44
+
45
+ export function entryPath(root, env = process.env, identity = null) {
46
+ return join(registryDir(env), slotName(root, identity));
47
+ }
48
+
49
+ /**
50
+ * The identity a stored entry is checked against. Path alone is not enough:
51
+ * a directory can be deleted and a different repository cloned in its place,
52
+ * and inheriting the old entry's peer would silently cross two projects.
53
+ */
54
+ export function identityKey(identity) {
55
+ // The normalized remote, so re-spelling origin (ssh ↔ https, or rotating an
56
+ // embedded token) is not mistaken for a different repository.
57
+ const remote = String(identity?.remote || "").trim();
58
+ if (remote) return `remote:${remote}`;
59
+ if (identity?.isGit) return `git:${identity.gitCommonDir || ""}`;
60
+ return "path";
61
+ }
62
+
63
+ /**
64
+ * Read this workspace's entry, or null.
65
+ *
66
+ * A stored entry whose identity contradicts the current one is treated as a
67
+ * miss — negative evidence. Nothing is inherited from it.
68
+ */
69
+ export function readEntry(root, { identity = null, env = process.env } = {}) {
70
+ const path = entryPath(root, env, identity);
71
+ const file = readWorkspaceFile(path, { layer: "registry" });
72
+ if (!file.data) return { path, entry: null, warnings: file.warnings, conflict: false };
73
+
74
+ const entry = file.data;
75
+ const warnings = [...file.warnings];
76
+ if (identity) {
77
+ const expected = identityKey(identity);
78
+ const stored = String(entry.identity || "");
79
+ if (stored && stored !== expected) {
80
+ warnings.push(
81
+ `${path} was recorded for a different repository (${stored}); starting a fresh entry for ${expected}`,
82
+ );
83
+ return { path, entry: null, warnings, conflict: true };
84
+ }
85
+ }
86
+ return { path, entry, warnings, conflict: false };
87
+ }
package/lib/utils.mjs CHANGED
@@ -1,6 +1,8 @@
1
1
  import fs from "fs"
2
2
  import path from "path"
3
3
 
4
+ import { createOvHttp } from "./shared/ov-http.mjs"
5
+
4
6
  let logFilePath = null
5
7
 
6
8
  export function initLogger(dataDir) {
@@ -68,167 +70,49 @@ export function effectivePeerId(config) {
68
70
  return String(config.effectivePeer?.peerId || config.peerId || "").trim() || null
69
71
  }
70
72
 
71
- function responseTraceId(payload) {
72
- return payload?.result?.trace_id || payload?.error?.trace_id || payload?.trace_id || undefined
73
+ // The config travels with every call here, so the client is built per call —
74
+ // a closure next to a network round-trip costs nothing.
75
+ function ovHttp(config) {
76
+ return createOvHttp(
77
+ { ...config, baseUrl: normalizeEndpoint(config.endpoint) },
78
+ { defaultTimeoutMs: config.timeoutMs },
79
+ )
73
80
  }
74
81
 
75
82
  export async function fetchJSON(config, endpoint, init = {}, options = {}) {
76
- const url = `${normalizeEndpoint(config.endpoint)}${endpoint}`
77
- const headers = makeAuthHeaders(
78
- config,
79
- { "Content-Type": "application/json", ...(init.headers ?? {}) },
80
- options.actorPeerId,
81
- )
82
- const controller = new AbortController()
83
- const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? config.timeoutMs)
84
- try {
85
- const response = await fetch(url, {
86
- ...init,
87
- headers,
88
- signal: controller.signal,
89
- })
90
- const text = await response.text()
91
- const payload = text ? parseJsonOrText(text) : {}
92
- const traceId = responseTraceId(payload)
93
- if (!response.ok || payload?.status === "error") {
94
- return {
95
- ok: false,
96
- status: response.status,
97
- error: payload?.error || payload?.message || { message: `HTTP ${response.status}` },
98
- traceId,
99
- }
100
- }
101
- return { ok: true, status: response.status, result: payload?.result ?? payload, traceId }
102
- } catch (error) {
103
- return { ok: false, status: 0, error: { message: error?.message ?? String(error) } }
104
- } finally {
105
- clearTimeout(timeout)
106
- }
83
+ return ovHttp(config)(endpoint, init, options)
107
84
  }
108
85
 
86
+ /**
87
+ * `fetchJSON` for the callers that would rather catch than branch: it resolves
88
+ * to the unwrapped result and turns every failure into an Error whose message
89
+ * is what the user can act on.
90
+ */
109
91
  export async function makeRequest(config, options) {
110
- const url = `${normalizeEndpoint(config.endpoint)}${options.endpoint}`
111
- const headers = makeAuthHeaders(
112
- config,
113
- { "Content-Type": "application/json", ...(options.headers ?? {}) },
114
- options.actorPeerId,
115
- )
116
-
117
- const controller = new AbortController()
118
- const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? config.timeoutMs)
119
- let onAbort = null
120
-
121
- if (options.abortSignal) {
122
- if (options.abortSignal.aborted) controller.abort()
123
- onAbort = () => controller.abort()
124
- options.abortSignal.addEventListener("abort", onAbort, { once: true })
125
- }
126
-
127
- try {
128
- const response = await fetch(url, {
129
- method: options.method,
130
- headers,
131
- body: options.body === undefined ? undefined : JSON.stringify(options.body),
132
- signal: controller.signal,
133
- })
134
-
135
- const text = await response.text()
136
- const payload = text ? parseJsonOrText(text) : {}
137
-
138
- if (!response.ok) {
139
- const rawError = typeof payload === "object" ? payload.error ?? payload.message : payload
140
- const errorMessage = typeof rawError === "string" ? rawError : JSON.stringify(rawError)
141
- if (response.status === 401 || response.status === 403) {
142
- throw new Error("Authentication failed. Please check apiKey/account/user in openviking-config.json or OPENVIKING_* environment variables.")
143
- }
144
- throw new Error(`Request failed (${response.status}): ${errorMessage}`)
145
- }
146
-
147
- return payload
148
- } catch (error) {
149
- if (error?.name === "AbortError") {
150
- throw new Error(`Request timeout after ${options.timeoutMs ?? config.timeoutMs}ms`)
151
- }
152
- if (error?.message?.includes("fetch failed") || error?.code === "ECONNREFUSED") {
153
- throw new Error(`OpenViking service unavailable at ${config.endpoint}. Start it with: openviking-server --config ~/.openviking/ov.conf`)
154
- }
155
- throw error
156
- } finally {
157
- clearTimeout(timeout)
158
- if (options.abortSignal && onAbort) {
159
- options.abortSignal.removeEventListener("abort", onAbort)
160
- }
161
- }
162
- }
163
-
164
- export async function makeMultipartRequest(config, options) {
165
- const url = `${normalizeEndpoint(config.endpoint)}${options.endpoint}`
166
- const headers = makeAuthHeaders(config, options.headers ?? {}, options.actorPeerId)
167
-
168
- const controller = new AbortController()
169
- const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? config.timeoutMs)
170
- let onAbort = null
171
-
172
- if (options.abortSignal) {
173
- if (options.abortSignal.aborted) controller.abort()
174
- onAbort = () => controller.abort()
175
- options.abortSignal.addEventListener("abort", onAbort, { once: true })
176
- }
177
-
178
- try {
179
- const response = await fetch(url, {
180
- method: options.method,
181
- headers,
182
- body: options.body,
183
- signal: controller.signal,
184
- })
185
-
186
- const text = await response.text()
187
- const payload = text ? parseJsonOrText(text) : {}
188
-
189
- if (!response.ok) {
190
- const rawError = typeof payload === "object" ? payload.error ?? payload.message : payload
191
- const errorMessage = typeof rawError === "string" ? rawError : JSON.stringify(rawError)
192
- if (response.status === 401 || response.status === 403) {
193
- throw new Error("Authentication failed. Please check apiKey/account/user in openviking-config.json or OPENVIKING_* environment variables.")
194
- }
195
- throw new Error(`Request failed (${response.status}): ${errorMessage}`)
92
+ const timeoutMs = options.timeoutMs ?? config.timeoutMs
93
+ const response = await fetchJSON(config, options.endpoint, {
94
+ method: options.method,
95
+ headers: options.headers,
96
+ body: options.body === undefined ? undefined : JSON.stringify(options.body),
97
+ }, { timeoutMs, actorPeerId: options.actorPeerId })
98
+ if (response.ok) return response.result
99
+
100
+ const message = getResponseErrorMessage(response.error)
101
+ // Status 0 is the transport: nothing answered, so the message is ours, not
102
+ // the server's, and it says what the user has to go fix.
103
+ if (response.status === 0) {
104
+ if (response.error?.aborted) {
105
+ throw new Error(`Request timeout after ${timeoutMs}ms`)
196
106
  }
197
-
198
- return payload
199
- } catch (error) {
200
- if (error?.name === "AbortError") {
201
- throw new Error(`Request timeout after ${options.timeoutMs ?? config.timeoutMs}ms`)
202
- }
203
- if (error?.message?.includes("fetch failed") || error?.code === "ECONNREFUSED") {
107
+ if (message.includes("fetch failed") || message.includes("ECONNREFUSED")) {
204
108
  throw new Error(`OpenViking service unavailable at ${config.endpoint}. Start it with: openviking-server --config ~/.openviking/ov.conf`)
205
109
  }
206
- throw error
207
- } finally {
208
- clearTimeout(timeout)
209
- if (options.abortSignal && onAbort) {
210
- options.abortSignal.removeEventListener("abort", onAbort)
211
- }
110
+ throw new Error(message)
212
111
  }
213
- }
214
-
215
- function makeAuthHeaders(config, headers = {}, actorPeerId = "") {
216
- const result = { ...headers }
217
- if (config.apiKey) result["Authorization"] = `Bearer ${config.apiKey}`
218
- if (config.account) result["X-OpenViking-Account"] = config.account
219
- if (config.user) result["X-OpenViking-User"] = config.user
220
- const peerId = String(actorPeerId || "").trim()
221
- if (peerId) result["X-OpenViking-Actor-Peer"] = peerId
222
- if (config.userAgent) result["User-Agent"] = config.userAgent
223
- return result
224
- }
225
-
226
- function parseJsonOrText(text) {
227
- try {
228
- return JSON.parse(text)
229
- } catch {
230
- return text
112
+ if (response.status === 401 || response.status === 403) {
113
+ throw new Error("Authentication failed. Check api_key/account/user in ~/.openviking/ovcli.conf, or the OPENVIKING_* environment variables.")
231
114
  }
115
+ throw new Error(`Request failed (${response.status}): ${message}`)
232
116
  }
233
117
 
234
118
  export function getResponseErrorMessage(error) {
@@ -236,30 +120,3 @@ export function getResponseErrorMessage(error) {
236
120
  if (typeof error === "string") return error
237
121
  return error.message || error.code || "Unknown OpenViking error"
238
122
  }
239
-
240
- export function unwrapResponse(response) {
241
- if (!response || typeof response !== "object") {
242
- throw new Error("OpenViking returned an invalid response")
243
- }
244
- if (response.status && response.status !== "ok") {
245
- throw new Error(getResponseErrorMessage(response.error))
246
- }
247
- return response.result
248
- }
249
-
250
- export function validateVikingUri(uri, toolName = "tool") {
251
- if (typeof uri !== "string" || !uri.startsWith("viking://")) {
252
- log("ERROR", toolName, "Invalid Viking URI", { uri })
253
- return 'Error: Invalid URI format. Must start with "viking://".'
254
- }
255
- return null
256
- }
257
-
258
- export function ensureRemoteUrl(value) {
259
- try {
260
- const url = new URL(value)
261
- return url.protocol === "http:" || url.protocol === "https:"
262
- } catch {
263
- return false
264
- }
265
- }
@@ -1,5 +1,5 @@
1
1
  import { log } from "./utils.mjs"
2
- import { buildGuardMessage, findVikingUri, normalizeToolName } from "./shared/uri-guard.mjs"
2
+ import { evaluateUriGuard, evaluateUriNotice, findVikingUri, normalizeToolName } from "./shared/uri-guard.mjs"
3
3
 
4
4
  const FILESYSTEM_TOOL_HINTS = {
5
5
  read: {
@@ -14,23 +14,38 @@ const FILESYSTEM_TOOL_HINTS = {
14
14
  tool: "openviking_search",
15
15
  example: (uri, args = {}) => `openviking_search(query="${String(args.pattern ?? "").replaceAll('"', '\\"')}", target_uri="${uri}")`,
16
16
  },
17
+ bash: {
18
+ tool: "openviking_read or openviking_search",
19
+ example: (uri) => `openviking_read(uris=["${uri}"])`,
20
+ },
17
21
  }
18
22
 
19
23
  export function createVikingUriGuard() {
20
24
  return async (input, output) => {
21
25
  const toolName = normalizeToolName(input?.tool ?? input?.name)
22
- const hint = FILESYSTEM_TOOL_HINTS[toolName]
23
- if (!hint) return
24
-
25
26
  const args = output?.args ?? input?.args ?? {}
26
- const uri = findVikingUri(args)
27
- if (!uri) return
27
+ const decision = evaluateUriGuard(toolName, args, { hints: FILESYSTEM_TOOL_HINTS })
28
+ if (!decision) return
28
29
 
29
30
  log("INFO", "viking-uri-guard", "Blocked filesystem tool for viking URI", {
30
31
  tool: toolName,
31
- uri,
32
+ uri: decision.uri,
33
+ })
34
+ throw new Error(decision.reason)
35
+ }
36
+ }
37
+
38
+ export function createVikingUriNotice() {
39
+ return async (input, output) => {
40
+ const toolName = normalizeToolName(input?.tool ?? input?.name)
41
+ const notice = evaluateUriNotice(toolName, input?.args ?? {}, { hints: FILESYSTEM_TOOL_HINTS })
42
+ if (!notice || !output) return
43
+
44
+ log("INFO", "viking-uri-guard", "Attached viking URI notice to shell tool output", {
45
+ tool: toolName,
46
+ uri: notice.uri,
32
47
  })
33
- throw new Error(buildGuardMessage(uri, { ...hint, example: hint.example(uri, args) }))
48
+ output.output = output.output ? `${output.output}\n\n${notice.reason}` : notice.reason
34
49
  }
35
50
  }
36
51
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openviking/opencode-plugin",
3
- "version": "2026.9.10",
3
+ "version": "2026.9.21-1",
4
4
  "author": "@openviking",
5
5
  "description": "Unified OpenCode plugin for OpenViking repository retrieval and long-term memory",
6
6
  "type": "module",
@@ -23,8 +23,9 @@
23
23
  "INSTALL-ZH.md"
24
24
  ],
25
25
  "scripts": {
26
+ "prepack": "node ../memory-plugin-shared/sync.mjs",
26
27
  "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
+ "test": "node --test tests/*.test.mjs"
28
29
  },
29
30
  "dependencies": {},
30
31
  "publishConfig": {
@@ -44,5 +45,5 @@
44
45
  "code-search"
45
46
  ],
46
47
  "license": "Apache-2.0",
47
- "openvikingSourceCommit": "3841e6f2992bc2299142f5ec50143e6b30e55ac0"
48
+ "openvikingSourceCommit": "9b0ce3dea5d8874fc373dbf40d8314945dc10234"
48
49
  }
@@ -3,42 +3,22 @@
3
3
  /**
4
4
  * stdio -> streamable-HTTP MCP proxy for the OpenViking OpenCode plugin.
5
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
6
+ * OpenCode starts this process as a local MCP server. The proxy resolves its
7
+ * connection through the plugin's own `loadConfig()`, forwards JSON-RPC
8
8
  * requests to the server's /mcp endpoint, and keeps stdout protocol-clean.
9
9
  */
10
10
 
11
- import { homedir } from "node:os"
12
- import { join, resolve as resolvePath } from "node:path"
11
+ import { resolve as resolvePath } from "node:path"
13
12
  import { fileURLToPath } from "node:url"
14
13
  import { loadConfig } from "../lib/config.mjs"
15
14
  import { createLogger } from "../lib/shared/debug-log.mjs"
16
- import { buildMcpProxyConfig, resolveMcpActorPeerId } from "../lib/shared/mcp-proxy-config.mjs"
15
+ import { toMcpProxyConfig } from "../lib/shared/mcp-proxy-config.mjs"
17
16
  import { createOpenVikingMcpProxy } from "../lib/shared/mcp-proxy-core.mjs"
18
17
 
19
- export { createOpenVikingMcpProxy } from "../lib/shared/mcp-proxy-core.mjs"
18
+ const PLUGIN_ROOT = resolvePath(fileURLToPath(import.meta.url), "..", "..")
20
19
 
21
- function readProxyConfig() {
22
- const cfg = loadConfig(resolvePath(fileURLToPath(import.meta.url), "..", ".."))
23
- return buildMcpProxyConfig({
24
- baseUrl: cfg.endpoint,
25
- mcpUrl: cfg.mcpUrl,
26
- apiKey: cfg.apiKey,
27
- account: cfg.account,
28
- user: cfg.user,
29
- peerId: resolveMcpActorPeerId(cfg),
30
- userAgent: cfg.userAgent,
31
- timeoutMs: cfg.timeoutMs,
32
- debug: cfg.debug,
33
- debugLogPath: cfg.debugLogPath,
34
- credentialSource: cfg.credentialSource,
35
- credentialPath: cfg.credentialPath || cfg.configPath,
36
- watchedPaths: [
37
- cfg.credentialPath,
38
- cfg.configPath,
39
- join(homedir(), ".config", "opencode", "openviking-config.json"),
40
- ],
41
- })
20
+ export function readProxyConfig(env = process.env) {
21
+ return toMcpProxyConfig(loadConfig(PLUGIN_ROOT, undefined, { env }), { env })
42
22
  }
43
23
 
44
24
  if (process.argv[1] && fileURLToPath(import.meta.url) === resolvePath(process.argv[1])) {
@@ -1,102 +0,0 @@
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
- userAgent: "openviking-memory-opencode/9.9.9",
60
- timeoutMs: 5000,
61
- debug: false,
62
- debugLogPath: "",
63
- credentialSource: "test",
64
- credentialPath: "",
65
- watchedPaths: [],
66
- }),
67
- loggerFactory: () => ({ log() {}, logError() {} }),
68
- })
69
- return {
70
- proxy,
71
- async messages() {
72
- await new Promise((resolve) => setImmediate(resolve))
73
- return out.join("").trim().split("\n").filter(Boolean).map((line) => JSON.parse(line))
74
- },
75
- }
76
- }
77
-
78
- test("OpenCode MCP proxy forwards initialize with auth and identity headers", async () => {
79
- await withServer((_req, res, entry) => {
80
- assert.equal(entry.method, "POST")
81
- assert.equal(entry.headers.authorization, "Bearer test-key")
82
- assert.equal(entry.headers["x-openviking-account"], "acct")
83
- assert.equal(entry.headers["x-openviking-user"], "user")
84
- assert.equal(entry.headers["x-openviking-actor-peer"], "peer")
85
- assert.equal(entry.headers["user-agent"], "openviking-memory-opencode/9.9.9")
86
- res.writeHead(200, {
87
- "content-type": "text/event-stream",
88
- "mcp-session-id": "sid-oc",
89
- })
90
- res.end(sseMessage(jsonRpc(1, { protocolVersion: "2025-06-18" })))
91
- }, async ({ url, requests }) => {
92
- const { proxy, messages } = makeProxy(url)
93
- await proxy.handleMessage({
94
- jsonrpc: "2.0",
95
- id: 1,
96
- method: "initialize",
97
- params: { protocolVersion: "2025-06-18" },
98
- })
99
- assert.deepEqual(await messages(), [jsonRpc(1, { protocolVersion: "2025-06-18" })])
100
- assert.equal(requests.length, 1)
101
- })
102
- })