@openviking/opencode-plugin 0.2.4 → 2026.9.9-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.
@@ -1,16 +1,134 @@
1
1
  // GENERATED FROM examples/memory-plugin-shared/lib. DO NOT EDIT.
2
+ /**
3
+ * Which peer a workspace writes its memories under.
4
+ *
5
+ * The peer used to be the working directory with every non-alphanumeric byte
6
+ * turned into a dash, which made the identity an accident of where the
7
+ * repository happened to sit: a clone on another machine, a rename, a worktree
8
+ * or simply `cd examples/` each minted a separate, empty namespace. The default
9
+ * is now git's own idea of the repository, so one project keeps one memory
10
+ * wherever it is checked out.
11
+ *
12
+ * `peer.source` decides the rule. Presets cover the three answers most people
13
+ * want; a template — or a list of templates tried in order — covers the rest.
14
+ * The old behaviour is still one word away, byte for byte.
15
+ */
16
+
17
+ import { legacySanitize, resolveWorkspaceIdentity, sanitizePeerId } from "./workspace-identity.mjs";
18
+
19
+ /**
20
+ * `git` is the default, and it resolves only inside a repository: the remote
21
+ * first, because that is the one name every clone agrees on, then the
22
+ * repository root for a repo that has none. Anywhere else it resolves to
23
+ * nothing and no peer is sent — a scratch folder or an app's per-task
24
+ * directory would otherwise mint a fresh, empty namespace each time, so those
25
+ * memories go to the user-level space instead. Naming such a directory is what
26
+ * `peer.id` is for, and deriving one from a bare path is what `cwd` is for;
27
+ * both are opt-in.
28
+ *
29
+ * No preset adds a prefix. A path-derived id starts with `-` on POSIX, so it
30
+ * cannot collide with a remote-derived one; anyone who wants a prefix writes
31
+ * their own template.
32
+ */
33
+ export const PEER_SOURCE_PRESETS = {
34
+ git: ["{git_remote}", "{git_root}"],
35
+ cwd: ["{cwd}"],
36
+ none: [],
37
+ };
38
+
39
+ export const DEFAULT_PEER_SOURCE = "git";
40
+
41
+ const VARIABLE_RE = /\{([a-z_]+)\}/g;
42
+
2
43
  export function deriveWorkspacePeerId(cwd) {
3
- return String(cwd || "").replace(/[^A-Za-z0-9]/g, "-");
44
+ return legacySanitize(cwd);
4
45
  }
5
46
 
6
- export function resolveEffectivePeerId({ cfg = {}, cwd = "" } = {}) {
47
+ /**
48
+ * Normalize `peer.source` — a preset name, a template, or a list — to templates.
49
+ *
50
+ * A bare string with no `{` is a misspelled preset, not a template: `Git` or
51
+ * `gti` would otherwise become a peer literally named `Git`, quietly stranding
52
+ * the workspace's memories in a namespace nobody meant to create. Such a value
53
+ * warns and falls back to the default chain. An array is left alone — writing a
54
+ * list is explicit enough to mean the constant it contains.
55
+ */
56
+ export function peerSourceTemplates(source, onWarn = null) {
57
+ if (Array.isArray(source)) return source.map(String).filter(Boolean);
58
+ const raw = String(source ?? "").trim();
59
+ if (!raw) return PEER_SOURCE_PRESETS[DEFAULT_PEER_SOURCE];
60
+ if (Object.hasOwn(PEER_SOURCE_PRESETS, raw)) return PEER_SOURCE_PRESETS[raw];
61
+ if (raw.includes("{")) return [raw];
62
+
63
+ const message = `OpenViking: ignored peer.source ${JSON.stringify(raw)}: it is neither a preset `
64
+ + `(${Object.keys(PEER_SOURCE_PRESETS).join(", ")}) nor a template such as "team-{dir}". `
65
+ + `Falling back to ${DEFAULT_PEER_SOURCE}.`;
66
+ if (typeof onWarn === "function") onWarn(message);
67
+ else process.stderr.write(`${message}\n`);
68
+ return PEER_SOURCE_PRESETS[DEFAULT_PEER_SOURCE];
69
+ }
70
+
71
+ /**
72
+ * Substitute one template, or return "" when any variable it names is empty.
73
+ *
74
+ * All-or-nothing on purpose: a half-resolved template like `git-` would be a
75
+ * silently shared identity, so an empty variable falls through to the next
76
+ * template instead.
77
+ */
78
+ export function renderPeerTemplate(template, vars) {
79
+ const text = String(template || "");
80
+ if (!text) return "";
81
+ let empty = false;
82
+ const rendered = text.replace(VARIABLE_RE, (match, name) => {
83
+ if (!Object.hasOwn(vars, name)) {
84
+ empty = true;
85
+ return "";
86
+ }
87
+ const value = String(vars[name] ?? "");
88
+ if (!value) empty = true;
89
+ return value;
90
+ });
91
+ return empty ? "" : rendered;
92
+ }
93
+
94
+ /**
95
+ * The peer this process should send, and where it came from.
96
+ *
97
+ * `source` keeps its three values — call sites compare it against the literal
98
+ * `"workspace"` to decide whether a session pin may be reused — while `origin`
99
+ * names the template that actually produced the id, and `legacyPeerId` carries
100
+ * the pre-git id whenever it differs, so recall can still reach memories
101
+ * written under it.
102
+ */
103
+ export function resolveEffectivePeerId({ cfg = {}, cwd = "", identity = null, env = process.env, onWarn = null } = {}) {
7
104
  const explicit = String(cfg.peerId || "").trim();
8
- if (explicit) return { peerId: explicit, source: "explicit" };
105
+ if (explicit) return { peerId: explicit, source: "explicit", origin: "explicit", legacyPeerId: "" };
9
106
 
10
- if (cfg.workspacePeer !== false) {
11
- const peerId = deriveWorkspacePeerId(cwd);
12
- if (peerId) return { peerId, source: "workspace" };
13
- }
107
+ // `OPENVIKING_WORKSPACE_PEER=0` predates `peer.source` and still means "none".
108
+ if (cfg.workspacePeer === false) return { peerId: "", source: "none", origin: "disabled", legacyPeerId: "" };
109
+
110
+ const templates = peerSourceTemplates(cfg.peerSource, onWarn);
111
+ if (!templates.length) return { peerId: "", source: "none", origin: "none", legacyPeerId: "" };
14
112
 
15
- return { peerId: "", source: "none" };
113
+ // `harness` is composed here rather than in the identity, whose result is
114
+ // cached on disk under a cwd-only key — two harnesses in one directory would
115
+ // otherwise read each other's peer back out of that cache.
116
+ const vars = {
117
+ ...((identity || resolveWorkspaceIdentity({ cwd, env })).vars || {}),
118
+ harness: sanitizePeerId(cfg.harness || cfg.clientId || ""),
119
+ };
120
+ const legacyPeerId = deriveWorkspacePeerId(cwd);
121
+ for (const template of templates) {
122
+ const peerId = renderPeerTemplate(template, vars);
123
+ if (!peerId) continue;
124
+ return {
125
+ peerId,
126
+ source: "workspace",
127
+ origin: template,
128
+ legacyPeerId: peerId === legacyPeerId ? "" : legacyPeerId,
129
+ };
130
+ }
131
+ // No peer, but the pre-git id is still what earlier sessions in this
132
+ // directory wrote under, so recall keeps reaching it.
133
+ return { peerId: "", source: "none", origin: "unresolved", legacyPeerId };
16
134
  }
package/lib/utils.mjs CHANGED
@@ -68,6 +68,10 @@ export function effectivePeerId(config) {
68
68
  return String(config.effectivePeer?.peerId || config.peerId || "").trim() || null
69
69
  }
70
70
 
71
+ function responseTraceId(payload) {
72
+ return payload?.result?.trace_id || payload?.error?.trace_id || payload?.trace_id || undefined
73
+ }
74
+
71
75
  export async function fetchJSON(config, endpoint, init = {}, options = {}) {
72
76
  const url = `${normalizeEndpoint(config.endpoint)}${endpoint}`
73
77
  const headers = makeAuthHeaders(
@@ -85,14 +89,16 @@ export async function fetchJSON(config, endpoint, init = {}, options = {}) {
85
89
  })
86
90
  const text = await response.text()
87
91
  const payload = text ? parseJsonOrText(text) : {}
92
+ const traceId = responseTraceId(payload)
88
93
  if (!response.ok || payload?.status === "error") {
89
94
  return {
90
95
  ok: false,
91
96
  status: response.status,
92
97
  error: payload?.error || payload?.message || { message: `HTTP ${response.status}` },
98
+ traceId,
93
99
  }
94
100
  }
95
- return { ok: true, status: response.status, result: payload?.result ?? payload }
101
+ return { ok: true, status: response.status, result: payload?.result ?? payload, traceId }
96
102
  } catch (error) {
97
103
  return { ok: false, status: 0, error: { message: error?.message ?? String(error) } }
98
104
  } finally {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openviking/opencode-plugin",
3
- "version": "0.2.4",
3
+ "version": "2026.9.9-1",
4
4
  "author": "@openviking",
5
5
  "description": "Unified OpenCode plugin for OpenViking repository retrieval and long-term memory",
6
6
  "type": "module",
@@ -43,5 +43,6 @@
43
43
  "rag",
44
44
  "code-search"
45
45
  ],
46
- "license": "Apache-2.0"
46
+ "license": "Apache-2.0",
47
+ "openvikingSourceCommit": "708dba60bd2484a9ecb69da27383b5203d8daee2"
47
48
  }
@@ -13,56 +13,32 @@ import { join, resolve as resolvePath } from "node:path"
13
13
  import { fileURLToPath } from "node:url"
14
14
  import { loadConfig } from "../lib/config.mjs"
15
15
  import { createLogger } from "../lib/shared/debug-log.mjs"
16
+ import { buildMcpProxyConfig, resolveMcpActorPeerId } from "../lib/shared/mcp-proxy-config.mjs"
16
17
  import { createOpenVikingMcpProxy } from "../lib/shared/mcp-proxy-core.mjs"
17
- import { resolveEffectivePeerId } from "../lib/shared/workspace-peer.mjs"
18
18
 
19
19
  export { createOpenVikingMcpProxy } from "../lib/shared/mcp-proxy-core.mjs"
20
20
 
21
- const DEFAULT_TIMEOUT_MS = 15000
22
-
23
- function trimSlash(value) {
24
- return String(value || "").replace(/\/+$/, "")
25
- }
26
-
27
- function normalizePath(value) {
28
- const raw = String(value || "").trim()
29
- if (!raw) return ""
30
- if (raw === "~") return homedir()
31
- if (raw.startsWith("~/")) return resolvePath(join(homedir(), raw.slice(2)))
32
- return resolvePath(raw)
33
- }
34
-
35
- function uniq(values) {
36
- return [...new Set(values.filter(Boolean))]
37
- }
38
-
39
21
  function readProxyConfig() {
40
22
  const cfg = loadConfig(resolvePath(fileURLToPath(import.meta.url), "..", ".."))
41
- const effectivePeer = resolveEffectivePeerId({ cfg, cwd: process.cwd() })
42
- const mcpUrl = cfg.mcpUrl || `${trimSlash(cfg.endpoint)}/mcp`
43
- return {
44
- mcpUrl,
45
- apiKey: cfg.apiKey || "",
46
- account: cfg.account || "",
47
- user: cfg.user || "",
48
- peerId: effectivePeer.peerId,
49
- userAgent: cfg.userAgent || "",
50
- timeoutMs: Math.max(1000, Number(cfg.timeoutMs) || DEFAULT_TIMEOUT_MS),
51
- debug: cfg.debug === true,
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,
52
33
  debugLogPath: cfg.debugLogPath,
53
- credentialSource: cfg.credentialSource || "auto",
54
- credentialPath: cfg.credentialPath || cfg.configPath || "",
55
- watchedPaths: uniq([
34
+ credentialSource: cfg.credentialSource,
35
+ credentialPath: cfg.credentialPath || cfg.configPath,
36
+ watchedPaths: [
56
37
  cfg.credentialPath,
57
38
  cfg.configPath,
58
- normalizePath(process.env.OPENVIKING_CLI_CONFIG_FILE),
59
- normalizePath(process.env.OPENVIKING_CONFIG_FILE),
60
- normalizePath(process.env.OPENVIKING_PLUGIN_CONFIG),
61
- join(homedir(), ".openviking", "ovcli.conf"),
62
- join(homedir(), ".openviking", "ov.conf"),
63
39
  join(homedir(), ".config", "opencode", "openviking-config.json"),
64
- ]),
65
- }
40
+ ],
41
+ })
66
42
  }
67
43
 
68
44
  if (process.argv[1] && fileURLToPath(import.meta.url) === resolvePath(process.argv[1])) {
@@ -1,78 +0,0 @@
1
- // GENERATED FROM examples/memory-plugin-shared/lib. DO NOT EDIT.
2
- /**
3
- * Async write-path helper for hook-based memory plugins.
4
- *
5
- * When `cfg.writePathAsync` is true and we're not already the worker, we:
6
- * 1. Drain the parent's stdin (the hook payload)
7
- * 2. Emit the harness-specific approve/no-op response on stdout immediately
8
- * 3. Spawn a detached clone of ourselves with env OV_HOOK_WORKER=1
9
- * 4. Feed the drained payload to the worker's stdin, unref, and exit
10
- *
11
- * The worker re-enters the same script, maybeDetach returns false (because
12
- * OV_HOOK_WORKER=1), and the hook's normal synchronous code path reads its
13
- * own stdin and runs the OV HTTP work with nobody waiting.
14
- *
15
- * Call this BEFORE the hook reads stdin itself. When it returns true, the
16
- * caller must `return` out of main() immediately.
17
- */
18
-
19
- import { spawn } from "node:child_process";
20
-
21
- const WORKER_ENV = "OV_HOOK_WORKER";
22
-
23
- export async function maybeDetach(cfg, { approve }) {
24
- if (!cfg.writePathAsync) return false;
25
- if (process.env[WORKER_ENV] === "1") return false;
26
-
27
- // Drain parent stdin so we can forward to the worker.
28
- let raw;
29
- try {
30
- const chunks = [];
31
- for await (const chunk of process.stdin) chunks.push(chunk);
32
- raw = Buffer.concat(chunks);
33
- } catch {
34
- // stdin read failed - let the synchronous path handle it.
35
- return false;
36
- }
37
-
38
- let child;
39
- try {
40
- child = spawn(process.execPath, [process.argv[1]], {
41
- detached: true,
42
- stdio: ["pipe", "ignore", "ignore"],
43
- env: { ...process.env, [WORKER_ENV]: "1" },
44
- });
45
- } catch {
46
- // spawn failed - fall through to sync mode, but we've already drained
47
- // stdin so hand the payload back via env var path below.
48
- process.env.OPENVIKING_HOOK_STDIN_CACHE = raw.toString();
49
- return false;
50
- }
51
-
52
- // Approve first, then write to the detached child. Ordering matters for
53
- // harnesses that read stdout before waiting for the hook process to exit.
54
- approve();
55
-
56
- try {
57
- child.stdin.write(raw);
58
- child.stdin.end();
59
- } catch { /* worker may have already exited; nothing useful to recover */ }
60
- child.unref();
61
- return true;
62
- }
63
-
64
- /**
65
- * Fallback stdin reader used when the async path drained stdin but spawn
66
- * failed; hooks should call this instead of reading stdin directly when
67
- * possible so the sync fallback still works after a failed detach.
68
- */
69
- export async function readHookStdin() {
70
- if (process.env.OPENVIKING_HOOK_STDIN_CACHE) {
71
- const cached = process.env.OPENVIKING_HOOK_STDIN_CACHE;
72
- delete process.env.OPENVIKING_HOOK_STDIN_CACHE;
73
- return cached;
74
- }
75
- const chunks = [];
76
- for await (const chunk of process.stdin) chunks.push(chunk);
77
- return Buffer.concat(chunks).toString();
78
- }
@@ -1,90 +0,0 @@
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
- * Consumers: Claude Code and Codex only. The other harnesses ship this module
23
- * through `sync.mjs` but still read their knobs from the environment, so a
24
- * `plugin` entry named after them is inert. `HARNESS_KEYS` lists what a harness
25
- * loader actually consumes today — add a key here as its loader starts calling
26
- * `loadPluginSettings`, not before, so the section never promises a knob that
27
- * silently does nothing.
28
- */
29
-
30
- import { readFileSync } from "node:fs";
31
- import { homedir } from "node:os";
32
- import { join, resolve as resolvePath } from "node:path";
33
-
34
- const DEFAULT_OVCLI_CONF_PATH = join(homedir(), ".openviking", "ovcli.conf");
35
-
36
- export const HARNESS_KEYS = {
37
- claudeCode: "claude_code",
38
- codex: "codex",
39
- };
40
-
41
- function tryLoadJson(path) {
42
- try {
43
- return JSON.parse(readFileSync(path, "utf-8"));
44
- } catch {
45
- return null;
46
- }
47
- }
48
-
49
- /**
50
- * Merge the shared plugin section with the harness-specific override.
51
- * Returns a flat settings object; unknown keys pass through untouched so a
52
- * harness can add its own knobs without touching this module.
53
- */
54
- export function loadPluginSettings(harness, env = process.env) {
55
- const path = resolvePath(
56
- (env.OPENVIKING_CLI_CONFIG_FILE || DEFAULT_OVCLI_CONF_PATH).replace(/^~/, homedir()),
57
- );
58
- const file = tryLoadJson(path);
59
- const plugin = file && typeof file.plugin === "object" && file.plugin ? file.plugin : {};
60
-
61
- const shared = {};
62
- for (const [key, value] of Object.entries(plugin)) {
63
- if (value && typeof value === "object" && !Array.isArray(value)) continue;
64
- shared[key] = value;
65
- }
66
- const scoped = harness && plugin[harness] && typeof plugin[harness] === "object"
67
- ? plugin[harness]
68
- : {};
69
-
70
- return { ...shared, ...scoped };
71
- }
72
-
73
- const REWRITE_MODES = new Set(["off", "client", "server", "auto"]);
74
-
75
- /**
76
- * Normalize the tri-state rewrite knob.
77
- *
78
- * off — never compress
79
- * client — always compress locally through the host CLI
80
- * server — always ask the server for a digest
81
- * auto — prefer local (cost lands on the user's own subscription), fall back
82
- * to the server when no healthy host CLI is available
83
- */
84
- export function normalizeRewriteMode(value, fallback = "off") {
85
- const raw = String(value ?? "").trim().toLowerCase();
86
- if (REWRITE_MODES.has(raw)) return raw;
87
- if (raw === "1" || raw === "true" || raw === "yes") return "auto";
88
- if (raw === "0" || raw === "false" || raw === "no") return "off";
89
- return fallback;
90
- }