@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.
- package/INSTALL-ZH.md +22 -5
- package/INSTALL.md +23 -5
- package/README.md +46 -11
- package/index.mjs +9 -2
- package/lib/config.mjs +17 -3
- package/lib/mcp-config.mjs +2 -1
- package/lib/memory-recall.mjs +5 -2
- package/lib/memory-session.mjs +53 -9
- package/lib/shared/capture-utils.mjs +98 -19
- package/lib/shared/credentials.mjs +11 -0
- package/lib/shared/input-filters.mjs +219 -0
- package/lib/shared/mcp-proxy-config.mjs +124 -0
- package/lib/shared/mcp-proxy-core.mjs +13 -1
- package/lib/shared/pending-queue.mjs +11 -0
- package/lib/shared/recall-compress-core.mjs +17 -6
- package/lib/shared/recall-core.mjs +74 -5
- package/lib/shared/setup-wizard.mjs +14 -11
- package/lib/shared/uri-guard.mjs +28 -6
- package/lib/shared/workspace-identity.mjs +334 -0
- package/lib/shared/workspace-peer.mjs +126 -8
- package/lib/utils.mjs +7 -1
- package/package.json +3 -2
- package/servers/mcp-proxy.mjs +16 -40
- package/lib/shared/async-writer.mjs +0 -78
- package/lib/shared/plugin-config.mjs +0 -90
|
@@ -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
|
|
44
|
+
return legacySanitize(cwd);
|
|
4
45
|
}
|
|
5
46
|
|
|
6
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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": "
|
|
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
|
}
|
package/servers/mcp-proxy.mjs
CHANGED
|
@@ -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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
54
|
-
credentialPath: cfg.credentialPath || cfg.configPath
|
|
55
|
-
watchedPaths:
|
|
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
|
-
}
|