@openviking/opencode-plugin 2026.9.10 → 2026.9.21-2

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,112 @@
1
+ // GENERATED FROM examples/memory-plugin-shared/lib. DO NOT EDIT.
2
+ /**
3
+ * The one path from a hook to the OpenViking server.
4
+ *
5
+ * Every harness used to own its own AbortController, header block and envelope
6
+ * parser, and they drifted: one sent the api key twice, one named the operator
7
+ * to every proxy on the path whether or not the server was trusted with it, one
8
+ * read a different field for the same account. A server-side change now lands
9
+ * in one place instead of eight.
10
+ *
11
+ * `X-API-Key` is deliberately absent. The server takes `Authorization: Bearer`,
12
+ * and the identity headers go out only when the resolved config says this
13
+ * server is trusted with the operator's name.
14
+ */
15
+
16
+ // A request that aborts the instant it starts reads as a dead server. A config
17
+ // that asks for less than a second gets a second.
18
+ const MIN_TIMEOUT_MS = 1000;
19
+
20
+ /**
21
+ * The headers every harness puts on every OpenViking request.
22
+ *
23
+ * `identityHeaders` overrides `cfg.sendIdentityHeaders` for the callers that
24
+ * decide per request rather than per config — the doctor probes both ways.
25
+ */
26
+ export function buildOvHeaders(cfg = {}, { actorPeerId = "", identityHeaders, extraHeaders } = {}) {
27
+ const identity = identityHeaders === undefined ? Boolean(cfg.sendIdentityHeaders) : Boolean(identityHeaders);
28
+ const headers = { "Content-Type": "application/json" };
29
+ if (cfg.apiKey) headers["Authorization"] = `Bearer ${cfg.apiKey}`;
30
+ if (identity && cfg.account) headers["X-OpenViking-Account"] = cfg.account;
31
+ if (identity && cfg.user) headers["X-OpenViking-User"] = cfg.user;
32
+ if (actorPeerId) headers["X-OpenViking-Actor-Peer"] = actorPeerId;
33
+ if (cfg.userAgent) headers["User-Agent"] = cfg.userAgent;
34
+ return { ...headers, ...extraHeaders };
35
+ }
36
+
37
+ function responseTraceId(body) {
38
+ return body?.result?.trace_id || body?.error?.trace_id || body?.trace_id || undefined;
39
+ }
40
+
41
+ /**
42
+ * Build the `fetchJSON(path, init, { timeoutMs, actorPeerId })` a harness talks
43
+ * through. It resolves to `{ ok, status, result, error, traceId }` and never
44
+ * throws: a hook that crashes on a closed port would take its host's turn with
45
+ * it, so a network error is `{ ok: false, status: 0 }` and the retry rules in
46
+ * `retryable.mjs` decide what happens next.
47
+ *
48
+ * `resolveActorPeerId` is a callback because Codex only knows its peer after
49
+ * loading session state under the lock, well after the client is built.
50
+ * `requireJsonBody` is for the capture hooks, which would rather retry a body
51
+ * they cannot parse than record the turn as sent.
52
+ */
53
+ export function createOvHttp(cfg = {}, {
54
+ defaultTimeoutMs = 0,
55
+ resolveActorPeerId = () => "",
56
+ identityHeaders,
57
+ extraHeaders,
58
+ requireJsonBody = false,
59
+ } = {}) {
60
+ // dsh and pi name this field `endpoint`; every other harness names it `baseUrl`.
61
+ const baseUrl = cfg.baseUrl || cfg.endpoint || "";
62
+ return async function fetchJSON(path, init = {}, options = {}) {
63
+ const timeoutMs = Math.max(MIN_TIMEOUT_MS, Number(options.timeoutMs) || Number(defaultTimeoutMs) || 0);
64
+ const controller = new AbortController();
65
+ let timedOut = false;
66
+ const timer = setTimeout(() => {
67
+ timedOut = true;
68
+ controller.abort();
69
+ }, timeoutMs);
70
+ try {
71
+ const headers = {
72
+ ...buildOvHeaders(cfg, {
73
+ actorPeerId: options.actorPeerId ?? resolveActorPeerId(),
74
+ identityHeaders,
75
+ extraHeaders,
76
+ }),
77
+ ...(init.headers || {}),
78
+ };
79
+ const response = await fetch(`${baseUrl}${path}`, { ...init, headers, signal: controller.signal });
80
+ const body = await response.json().catch(() => null);
81
+ if (requireJsonBody && !body) {
82
+ return {
83
+ ok: false,
84
+ status: response.status,
85
+ result: null,
86
+ error: { message: "empty or invalid JSON response" },
87
+ };
88
+ }
89
+ const traceId = responseTraceId(body);
90
+ if (!response.ok || body?.status === "error") {
91
+ return {
92
+ ok: false,
93
+ status: response.status,
94
+ result: null,
95
+ error: body?.error || { message: `HTTP ${response.status}` },
96
+ traceId,
97
+ };
98
+ }
99
+ return { ok: true, status: response.status, result: body?.result ?? body ?? {}, traceId };
100
+ } catch (error) {
101
+ const message = error?.message || String(error);
102
+ // This module owns the controller, so it says a timeout was a timeout
103
+ // rather than leaving callers to guess it from whatever fetch threw.
104
+ const failure = timedOut
105
+ ? { name: "AbortError", aborted: true, message }
106
+ : { message };
107
+ return { ok: false, status: 0, result: null, error: failure };
108
+ } finally {
109
+ clearTimeout(timer);
110
+ }
111
+ };
112
+ }
@@ -0,0 +1,306 @@
1
+ // GENERATED FROM examples/memory-plugin-shared/lib. DO NOT EDIT.
2
+ /**
3
+ * Per-harness plugin settings from ovcli.conf.
4
+ *
5
+ * ovcli.conf carried connection fields only, so every harness had to keep its
6
+ * tuning knobs in ov.conf's harness section — a server-side file that a
7
+ * client-side plugin has no business editing. The `plugin` section fixes that:
8
+ * shared keys apply to every harness that reads them, and a per-harness object
9
+ * overrides them.
10
+ *
11
+ * {
12
+ * "url": "...", "api_key": "...",
13
+ * "plugin": {
14
+ * "recallQueryExpansion": "off",
15
+ * "recallCompress": "auto"
16
+ * }
17
+ * }
18
+ *
19
+ * Resolution stays env → ovcli.conf plugin.<harness> → ovcli.conf plugin →
20
+ * ov.conf harness section (legacy) → defaults.
21
+ *
22
+ * Every harness resolves through `resolveSettings()` below, and assembles its
23
+ * whole configuration through `buildPluginConfig()`, so one `plugin` section
24
+ * configures all of them and `ov config switch` moves behaviour along with
25
+ * credentials. The knobs themselves are declared once in `config-schema.mjs`;
26
+ * this module decides which files supply them and what is derived from them.
27
+ */
28
+
29
+ import { homedir } from "node:os";
30
+ import { join } from "node:path";
31
+
32
+ import { HARNESS_KEYS, KNOBS, harnessKey, knobDefault, resolveKnobs } from "./config-schema.mjs";
33
+ import {
34
+ buildUserAgent,
35
+ loadCredentialFiles,
36
+ readManifestVersion,
37
+ resolveConnection,
38
+ } from "./credentials.mjs";
39
+ import {
40
+ announcedOverrides,
41
+ loadWorkspaceLayers,
42
+ mergeConfigLayers,
43
+ normalizeWorkspaceConfig,
44
+ projectWorkspaceSettings,
45
+ } from "./workspace-config.mjs";
46
+ import { findWorkspaceRoot, resolveWorkspaceIdentity } from "./workspace-identity.mjs";
47
+ import { resolveEffectivePeerId, resolvePluginPeerId } from "./workspace-peer.mjs";
48
+ import { readEntry } from "./workspace-registry.mjs";
49
+
50
+ const OVCLI_LAYER = "ovcli.conf";
51
+ const OV_CONF_LAYER = "ov.conf";
52
+
53
+ export { HARNESS_KEYS, harnessKey };
54
+
55
+ /**
56
+ * Merge the shared plugin section with the harness-specific override.
57
+ * Returns a flat settings object; unknown keys pass through untouched so a
58
+ * harness can add its own knobs without touching this module.
59
+ *
60
+ * `cliFile` is ovcli.conf already parsed. Callers that have it hand it over so
61
+ * the file is read once per build and, more importantly, so the `plugin`
62
+ * section comes out of the same file the credential chain read — an
63
+ * `OPENVIKING_CONFIG_FILE` that is ovcli-shaped is ovcli.conf, and only
64
+ * `loadCredentialFiles` knows that.
65
+ */
66
+ export function loadPluginSettings(harness, env = process.env, options = {}) {
67
+ const file = options.cliFile || loadCredentialFiles(env).cliFile;
68
+ const plugin = file && typeof file.plugin === "object" && file.plugin ? file.plugin : {};
69
+
70
+ const shared = {};
71
+ for (const [key, value] of Object.entries(plugin)) {
72
+ if (value && typeof value === "object" && !Array.isArray(value)) continue;
73
+ shared[key] = value;
74
+ }
75
+ // Both spellings of the harness name find the same override, so copying
76
+ // `claude-code` out of the installer is not a silent no-op.
77
+ const key = harnessKey(harness);
78
+ const scoped = {};
79
+ for (const candidate of [key, key.replace(/_/g, "-")]) {
80
+ const value = candidate && plugin[candidate];
81
+ if (value && typeof value === "object" && !Array.isArray(value)) Object.assign(scoped, value);
82
+ }
83
+
84
+ const settings = { ...shared, ...scoped };
85
+ const cwd = String(options.cwd || "").trim();
86
+ if (!cwd) return settings;
87
+ return { ...settings, ...resolveWorkspaceSettings(cwd, env, options).settings };
88
+ }
89
+
90
+ /**
91
+ * Every knob for one harness, resolved through the whole layer stack.
92
+ *
93
+ * `legacy` is that harness's section in ov.conf, which is where tuning used to
94
+ * live: it stays the lowest configured layer so an existing deployment keeps
95
+ * working, and everything above it comes from files a client may edit.
96
+ *
97
+ * Returns the settings, the names some layer actually supplied (what the
98
+ * `*Configured` flags report), and the raw ovcli.conf merge — a couple of
99
+ * callers still need to know a value came from ovcli.conf rather than ov.conf.
100
+ */
101
+ export function resolveSettings(harness, options = {}) {
102
+ const { env = process.env, cwd = "", legacy = {}, clientVersion = "", cliFile = null } = options;
103
+ const key = harnessKey(harness);
104
+ const plugin = loadPluginSettings(key, env, { cwd, clientVersion, cliFile });
105
+ const { settings, configured, sources } = resolveKnobs({
106
+ harness: key,
107
+ layers: [
108
+ { name: OV_CONF_LAYER, data: legacy },
109
+ { name: OVCLI_LAYER, data: plugin },
110
+ ],
111
+ env,
112
+ });
113
+ return { settings, configured, sources, plugin };
114
+ }
115
+
116
+ /**
117
+ * The workspace layers for one cwd, flattened into harness knobs.
118
+ *
119
+ * Takes the cwd explicitly because a hook's `loadConfig()` runs at module top
120
+ * level, before the payload on stdin has said which directory the session is
121
+ * actually in — the caller resolves this again once it knows.
122
+ */
123
+ export function resolveWorkspaceSettings(cwd, env = process.env, { clientVersion = "" } = {}) {
124
+ const empty = { settings: {}, root: "", provenance: {}, warnings: [], announced: [] };
125
+ try {
126
+ const { root } = findWorkspaceRoot(cwd, env);
127
+ if (!root) return empty;
128
+
129
+ const { layers, warnings } = loadWorkspaceLayers(root, { clientVersion });
130
+ // The identity is what makes the registry's negative evidence work: without
131
+ // it a directory reused by a different repository inherits the old peer.
132
+ const identity = resolveWorkspaceIdentity({ cwd, env });
133
+ const registry = readEntry(root, { identity, env });
134
+ warnings.push(...registry.warnings);
135
+ if (registry.entry?.settings) layers.push({ layer: "registry", data: registry.entry.settings });
136
+ if (registry.entry?.peer) layers.push({ layer: "registry", data: { peer: registry.entry.peer } });
137
+ if (!layers.length) return { ...empty, root, warnings };
138
+
139
+ const { value, provenance } = mergeConfigLayers(layers, warnings);
140
+ normalizeWorkspaceConfig(value, warnings);
141
+ return {
142
+ settings: projectWorkspaceSettings(value),
143
+ root,
144
+ provenance,
145
+ warnings,
146
+ announced: announcedOverrides(provenance),
147
+ value,
148
+ };
149
+ } catch {
150
+ // A hook must never die over a config file. An unreadable layer is no layer.
151
+ return empty;
152
+ }
153
+ }
154
+
155
+ const SEND_ONLY_WHEN_CONFIGURED = KNOBS.filter((knob) => knob.sendOnlyWhenConfigured);
156
+
157
+ function str(value) {
158
+ return typeof value === "string" && value.trim() ? value.trim() : "";
159
+ }
160
+
161
+ /** The ov.conf block named after this harness, the lowest layer of every chain. */
162
+ function ovConfSection(ovFile, key) {
163
+ const section = key ? ovFile?.[key] : null;
164
+ return section && typeof section === "object" && !Array.isArray(section) ? section : {};
165
+ }
166
+
167
+ /**
168
+ * One harness's whole configuration: the files, the knobs, the peer, and the
169
+ * handful of fields derived from them.
170
+ *
171
+ * Every loader used to write this sequence out by hand and each wrote a
172
+ * slightly different subset, which is how ov.conf's `<harness>.authMode` came
173
+ * to apply on three harnesses only, and why two of the four knobs the server
174
+ * defaults for itself were reported as configured on two harnesses and nowhere
175
+ * else. What is left in a loader after this call is what only that harness
176
+ * knows.
177
+ *
178
+ * The connection — server, key, identity, auth mode — is not a knob: it comes
179
+ * from `resolveConnection`, the same call an MCP proxy's connection comes
180
+ * from, and the `plugin` keys and ov.conf block that name it are read there.
181
+ *
182
+ * `legacy` replaces the ov.conf block this harness would otherwise get for its
183
+ * knobs (dsh's host hands it one of its own); `hostInput` is what an embedding
184
+ * host named for this process, which is the more specific answer and outranks
185
+ * every file; `logFile` is the log basename this harness has always used, and
186
+ * a harness that never had a default log path does not get one invented for
187
+ * it; `rootKeyFallback` is for the harnesses whose chain predates the ovcli.conf
188
+ * pinning and has always ended at `server.root_api_key`.
189
+ */
190
+ export function buildPluginConfig(harness, {
191
+ cwd = "",
192
+ env = process.env,
193
+ legacy = null,
194
+ manifestUrl = "",
195
+ version = "",
196
+ hostInput = {},
197
+ logFile = "",
198
+ rootKeyFallback = false,
199
+ deriveEffectivePeer = false,
200
+ } = {}) {
201
+ const name = String(harness || "");
202
+ const key = harnessKey(name);
203
+ const workspaceCwd = str(cwd) || process.cwd();
204
+ const files = loadCredentialFiles(env);
205
+ // The version answers two questions: what the User-Agent reports, and whether
206
+ // a workspace file asking for a newer client gets to say so.
207
+ const clientVersion = str(version) || (manifestUrl ? readManifestVersion(manifestUrl) : "");
208
+ const { settings, configured, sources } = resolveSettings(key, {
209
+ env,
210
+ cwd: workspaceCwd,
211
+ legacy: legacy || ovConfSection(files.ovFile, key),
212
+ cliFile: files.cliFile,
213
+ clientVersion,
214
+ });
215
+ const connection = resolveConnection(key, { env, files, hostInput, rootKeyFallback });
216
+ const { baseUrl, account, user } = connection;
217
+ const timeoutMs = settings.timeoutMs;
218
+ const peerId = resolvePluginPeerId({
219
+ settings,
220
+ configured,
221
+ sources,
222
+ credentials: connection,
223
+ hostInput: hostInput?.peerId,
224
+ env,
225
+ });
226
+
227
+ const recallRewrite = normalizeRewriteMode(
228
+ env.OPENVIKING_RECALL_COMPRESS ?? env.OPENVIKING_RECALL_REWRITE ?? settings.recallCompress,
229
+ knobDefault(KNOBS.find((knob) => knob.name === "recallCompress"), harnessKey(name)),
230
+ );
231
+ const config = {
232
+ ...settings,
233
+ harness: name,
234
+ recallCompress: recallRewrite,
235
+ recallRewrite,
236
+ clientVersion,
237
+ userAgent: buildUserAgent(name, clientVersion),
238
+
239
+ baseUrl,
240
+ mcpUrl: connection.mcpUrl,
241
+ apiKey: connection.apiKey,
242
+ account,
243
+ user,
244
+ authMode: connection.authMode,
245
+ sendIdentityHeaders: connection.sendIdentityHeaders,
246
+
247
+ credentialSource: connection.credentialSource,
248
+ apiKeySource: connection.apiKeySource,
249
+ credentialPath: connection.credentialPath,
250
+ hasApiKey: connection.hasApiKey,
251
+ configPath: connection.cliPath || connection.ovPath || null,
252
+ cliConfigPath: connection.cliPath,
253
+ ovConfigPath: connection.ovPath,
254
+ cliPath: connection.cliPath,
255
+ ovPath: connection.ovPath,
256
+
257
+ peerId,
258
+ explicitPeerId: peerId,
259
+
260
+ // A write gets a longer budget than a read, and a digest has to finish
261
+ // inside the recall request it belongs to, so both fall back to a knob.
262
+ captureTimeoutMs: settings.captureTimeoutMs || Math.max(timeoutMs * 2, 30000),
263
+ recallCompressTimeoutMs: settings.recallCompressTimeoutMs
264
+ || Math.max(1000, settings.recallTimeoutMs - 10000),
265
+ debugLogPath: settings.debugLogPath
266
+ || (logFile ? join(homedir(), ".openviking", "logs", logFile) : ""),
267
+
268
+ // The spellings these fields carried before every harness shared a loader.
269
+ endpoint: baseUrl,
270
+ accountId: account,
271
+ userId: user,
272
+ requestTimeoutMs: timeoutMs,
273
+ };
274
+ for (const knob of SEND_ONLY_WHEN_CONFIGURED) {
275
+ config[`${knob.name}Configured`] = configured.has(knob.name);
276
+ }
277
+
278
+ // Only for the harnesses that resolve the peer once at load. The rest derive
279
+ // it per session, against the directory the payload names, and an MCP proxy
280
+ // must not derive one from wherever it happened to be launched.
281
+ if (deriveEffectivePeer) {
282
+ const effectivePeer = resolveEffectivePeerId({ cfg: config, cwd: workspaceCwd, env });
283
+ config.effectivePeer = effectivePeer;
284
+ config.legacyPeerId = effectivePeer.legacyPeerId;
285
+ }
286
+ return config;
287
+ }
288
+
289
+ const REWRITE_MODES = new Set(["off", "client", "server", "auto"]);
290
+
291
+ /**
292
+ * Normalize the tri-state rewrite knob.
293
+ *
294
+ * off — never compress
295
+ * client — always compress locally through the host CLI
296
+ * server — always ask the server for a digest
297
+ * auto — prefer local (cost lands on the user's own subscription), fall back
298
+ * to the server when no healthy host CLI is available
299
+ */
300
+ export function normalizeRewriteMode(value, fallback = "off") {
301
+ const raw = String(value ?? "").trim().toLowerCase();
302
+ if (REWRITE_MODES.has(raw)) return raw;
303
+ if (raw === "1" || raw === "true" || raw === "yes") return "auto";
304
+ if (raw === "0" || raw === "false" || raw === "no") return "off";
305
+ return fallback;
306
+ }