Skip to content

Commit 8f135a3

Browse files
t0sakiZaynJarvis
authored andcommitted
refactor(plugin/codex): align config loading with claude-code plugin
Addresses three review points on PR volcengine#1957: 1. Honor OPENVIKING_CLI_CONFIG_FILE for the ovcli.conf override path (matches the convention used by `ov` CLI and claude-code-memory-plugin). OPENVIKING_CONFIG_FILE stays as the ov.conf override; for backward compat it still works when pointed at an ovcli-shaped file. 2. Strict env-first priority for every connection / identity field (baseUrl, apiKey, account, user, agentId). Env vars now win over ovcli.conf, which wins over ov.conf's codex.* block / server.*, which wins over built-in defaults. 3. Unify hook and MCP-server config loading: src/memory-server.ts now imports loadConfig from scripts/config.mjs (relative path stays valid post-compile because servers/ and scripts/ are siblings), eliminating the divergent account/user/agentId fallback chains the PR-Agent reviewer flagged. Auth header: emit Authorization: Bearer (primary, required by OpenViking Cloud) plus the legacy X-API-Key during the transition window. All six fetch sites updated (4 hook scripts + memory-server.ts + compiled servers/memory-server.js). README: document the new resolution chain, OPENVIKING_CLI_CONFIG_FILE, OPENVIKING_BEARER_TOKEN alias, and the Authorization: Bearer migration.
1 parent 7ea5a1b commit 8f135a3

9 files changed

Lines changed: 287 additions & 225 deletions

File tree

examples/codex-memory-plugin/README.md

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -292,18 +292,38 @@ If step 6 returns no leaf memories, check:
292292
| `autoCommitOnCompact` | `true` | Commit the full transcript on `PreCompact` |
293293
| `debug` | `false` | Write structured debug logs |
294294

295-
Connection settings (URL, account, user, api_key) come from `ovcli.conf` plus standard env overrides:
296-
297-
- `OPENVIKING_CONFIG_FILE`: alternate config path (defaults to `~/.openviking/ovcli.conf`, then `~/.openviking/ov.conf`)
298-
- `OPENVIKING_URL`: override server URL
299-
- `OPENVIKING_API_KEY`: override API key
300-
- `OPENVIKING_ACCOUNT`: override account
301-
- `OPENVIKING_USER`: override user
302-
- `OPENVIKING_AGENT_ID`: override agent identity
295+
Connection settings resolve in this strict priority — env vars always win:
296+
297+
1. **Environment variables** (`OPENVIKING_*`)
298+
2. **`ovcli.conf`** — CLI client config (`url`, `api_key`, `account`, `user`, `agent_id`)
299+
3. **`ov.conf`** — server config (`server.*` + optional `codex.*` tuning block)
300+
4. **Built-in defaults**
301+
302+
Setting `OPENVIKING_URL` alone is enough to run in env-var-only mode (no config files needed) — useful for daemon-spawned agents.
303+
304+
File-path overrides (aligned with `ov` CLI and `claude-code-memory-plugin`):
305+
306+
- `OPENVIKING_CLI_CONFIG_FILE` — alternate `ovcli.conf` path (default `~/.openviking/ovcli.conf`)
307+
- `OPENVIKING_CONFIG_FILE` — alternate `ov.conf` path (default `~/.openviking/ov.conf`). For backward compat, if this points at an ovcli-shaped file (top-level `url`/`api_key`, no `server` section), it is treated as the CLI config.
308+
309+
Connection / identity overrides:
310+
311+
- `OPENVIKING_URL` / `OPENVIKING_BASE_URL` — server URL
312+
- `OPENVIKING_API_KEY` / `OPENVIKING_BEARER_TOKEN` — API key (sent as `Authorization: Bearer` either way)
313+
- `OPENVIKING_ACCOUNT` — account
314+
- `OPENVIKING_USER` — user
315+
- `OPENVIKING_AGENT_ID` — agent identity
316+
317+
State-file / SessionStart tuning:
318+
303319
- `OPENVIKING_CODEX_STATE_DIR`: state file directory (default `~/.openviking/codex-plugin-state`)
304320
- `OPENVIKING_CODEX_ACTIVE_WINDOW_MS`: SessionStart active-window threshold in ms (default `120000` = 2 min)
305321
- `OPENVIKING_CODEX_IDLE_TTL_MS`: SessionStart idle-TTL sweep threshold in ms (default `1800000` = 30 min)
306322

323+
### Auth header
324+
325+
Requests send both `Authorization: Bearer <api_key>` (primary — required by OpenViking Cloud) and `X-API-Key` (legacy — accepted by older self-hosted servers). The legacy header will be dropped once `X-API-Key` is fully retired upstream.
326+
307327
## Hook timeouts
308328

309329
| Hook | Default timeout | Notes |

examples/codex-memory-plugin/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/codex-memory-plugin/scripts/auto-capture.mjs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,10 @@ async function fetchJSON(path, init = {}) {
4545
const timer = setTimeout(() => controller.abort(), cfg.captureTimeoutMs);
4646
try {
4747
const headers = { "Content-Type": "application/json" };
48-
if (cfg.apiKey) headers["X-API-Key"] = cfg.apiKey;
48+
if (cfg.apiKey) {
49+
headers["Authorization"] = `Bearer ${cfg.apiKey}`;
50+
headers["X-API-Key"] = cfg.apiKey;
51+
}
4952
if (cfg.account) headers["X-OpenViking-Account"] = cfg.account;
5053
if (cfg.user) headers["X-OpenViking-User"] = cfg.user;
5154
if (cfg.agentId) headers["X-OpenViking-Agent"] = cfg.agentId;

examples/codex-memory-plugin/scripts/auto-recall.mjs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@ async function fetchJSON(path, init = {}) {
4141
const timer = setTimeout(() => controller.abort(), cfg.timeoutMs);
4242
try {
4343
const headers = { "Content-Type": "application/json" };
44-
if (cfg.apiKey) headers["X-API-Key"] = cfg.apiKey;
44+
if (cfg.apiKey) {
45+
headers["Authorization"] = `Bearer ${cfg.apiKey}`;
46+
headers["X-API-Key"] = cfg.apiKey;
47+
}
4548
if (cfg.account) headers["X-OpenViking-Account"] = cfg.account;
4649
if (cfg.user) headers["X-OpenViking-User"] = cfg.user;
4750
if (cfg.agentId) headers["X-OpenViking-Agent"] = cfg.agentId;

examples/codex-memory-plugin/scripts/config.mjs

Lines changed: 177 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,43 @@
11
/**
22
* Shared configuration loader for the Codex OpenViking memory plugin.
33
*
4-
* Reads connection settings from `~/.openviking/ovcli.conf` (the canonical CLIENT
5-
* config that the `ov` CLI uses), and falls back to the legacy `~/.openviking/ov.conf`
6-
* server config when ovcli.conf is missing.
4+
* Resolution priority (highest → lowest), per-field:
5+
* 1. Environment variables (OPENVIKING_*)
6+
* 2. ovcli.conf — the CLI client config (carries url/api_key/account/user/agent_id)
7+
* 3. ov.conf — the server config (server.* + optional codex.* block for tuning)
8+
* 4. Built-in defaults
79
*
8-
* Plugin-specific overrides go in an optional `codex` section of either file.
10+
* Mirrors examples/claude-code-memory-plugin/scripts/config.mjs so the
11+
* hook surface and the MCP server (src/memory-server.ts imports loadConfig
12+
* from here) resolve identity identically. Aligning the two prevents
13+
* silent identity drift between auto-capture and explicit `remember` calls.
914
*
10-
* Env vars:
11-
* OPENVIKING_CONFIG_FILE alternate ovcli.conf path
12-
* OPENVIKING_URL override server URL
13-
* OPENVIKING_API_KEY override API key
14-
* OPENVIKING_ACCOUNT override account
15-
* OPENVIKING_USER override user
16-
* OPENVIKING_AGENT_ID override agent identity
17-
* OPENVIKING_DEBUG=1 enable debug logging
18-
* OPENVIKING_DEBUG_LOG debug log path
15+
* File-path env vars:
16+
* OPENVIKING_CLI_CONFIG_FILE alternate ovcli.conf path (preferred)
17+
* OPENVIKING_CONFIG_FILE alternate ov.conf path
18+
*
19+
* For backward compat, if only OPENVIKING_CONFIG_FILE is set and the file
20+
* it points at parses as an ovcli.conf (top-level `url`/`api_key`, no
21+
* `server` section), it is treated as ovcli.conf — earlier versions of
22+
* this plugin used OPENVIKING_CONFIG_FILE to mean either file.
23+
*
24+
* Connection / identity env vars:
25+
* OPENVIKING_URL / OPENVIKING_BASE_URL
26+
* OPENVIKING_API_KEY / OPENVIKING_BEARER_TOKEN
27+
* OPENVIKING_ACCOUNT, OPENVIKING_USER, OPENVIKING_AGENT_ID
28+
*
29+
* Misc env vars:
30+
* OPENVIKING_TIMEOUT_MS, OPENVIKING_CAPTURE_TIMEOUT_MS
31+
* OPENVIKING_RECALL_LIMIT, OPENVIKING_SCORE_THRESHOLD
32+
* OPENVIKING_DEBUG=1, OPENVIKING_DEBUG_LOG
1933
*/
2034

21-
import { readFileSync, existsSync } from "node:fs";
35+
import { readFileSync } from "node:fs";
2236
import { homedir } from "node:os";
2337
import { join, resolve as resolvePath } from "node:path";
2438

25-
const DEFAULT_CLI_CONFIG = join(homedir(), ".openviking", "ovcli.conf");
26-
const DEFAULT_SERVER_CONFIG = join(homedir(), ".openviking", "ov.conf");
39+
const DEFAULT_OVCLI_CONF_PATH = join(homedir(), ".openviking", "ovcli.conf");
40+
const DEFAULT_OV_CONF_PATH = join(homedir(), ".openviking", "ov.conf");
2741

2842
function num(val, fallback) {
2943
if (typeof val === "number" && Number.isFinite(val)) return val;
@@ -39,90 +53,184 @@ function str(val, fallback) {
3953
return fallback;
4054
}
4155

42-
function readJson(path) {
56+
function envBool(name) {
57+
const v = process.env[name];
58+
if (v == null || v === "") return undefined;
59+
const lower = v.trim().toLowerCase();
60+
if (lower === "0" || lower === "false" || lower === "no") return false;
61+
if (lower === "1" || lower === "true" || lower === "yes") return true;
62+
return undefined;
63+
}
64+
65+
function tryLoadJson(path) {
66+
let raw;
67+
try {
68+
raw = readFileSync(path, "utf-8");
69+
} catch {
70+
return null;
71+
}
4372
try {
44-
return JSON.parse(readFileSync(path, "utf-8"));
73+
return JSON.parse(raw);
4574
} catch {
75+
process.stderr.write(`[openviking-memory] Invalid config file: ${path}\n`);
4676
return null;
4777
}
4878
}
4979

50-
function deriveBaseUrl(file) {
51-
const direct = str(file?.url, "");
52-
if (direct) return direct.replace(/\/+$/, "");
53-
const server = file?.server || {};
54-
const host = str(server.host, "127.0.0.1").replace("0.0.0.0", "127.0.0.1");
55-
const port = Math.floor(num(server.port, 1933));
56-
return `http://${host}:${port}`;
80+
function looksLikeOvcli(obj) {
81+
if (!obj || typeof obj !== "object") return false;
82+
if (obj.server && typeof obj.server === "object") return false;
83+
return typeof obj.url === "string" || typeof obj.api_key === "string";
5784
}
5885

59-
export function loadConfig() {
60-
const explicitPath = process.env.OPENVIKING_CONFIG_FILE
86+
/**
87+
* Returns { cliFile, cliPath, ovFile, ovPath }. Missing files are
88+
* represented as empty objects, so callers can read fields unconditionally.
89+
*
90+
* OPENVIKING_CLI_CONFIG_FILE overrides the ovcli.conf path.
91+
* OPENVIKING_CONFIG_FILE overrides the ov.conf path; if the file looks
92+
* like an ovcli.conf (no `server` section + has `url`/`api_key`), it is
93+
* also used as the cliFile to support the legacy "pass any conf via
94+
* OPENVIKING_CONFIG_FILE" pattern.
95+
*/
96+
function loadFiles() {
97+
const cliPathEnv = process.env.OPENVIKING_CLI_CONFIG_FILE
98+
? resolvePath(process.env.OPENVIKING_CLI_CONFIG_FILE.replace(/^~/, homedir()))
99+
: null;
100+
const ovPathEnv = process.env.OPENVIKING_CONFIG_FILE
61101
? resolvePath(process.env.OPENVIKING_CONFIG_FILE.replace(/^~/, homedir()))
62102
: null;
63103

64-
const candidates = explicitPath
65-
? [explicitPath]
66-
: [DEFAULT_CLI_CONFIG, DEFAULT_SERVER_CONFIG];
67-
68-
let configPath = null;
69-
let file = null;
70-
for (const candidate of candidates) {
71-
if (existsSync(candidate)) {
72-
configPath = candidate;
73-
file = readJson(candidate) || {};
74-
break;
75-
}
76-
}
77-
if (!file) {
78-
file = {};
79-
configPath = explicitPath || DEFAULT_CLI_CONFIG;
104+
const cliPath = cliPathEnv || DEFAULT_OVCLI_CONF_PATH;
105+
const ovPath = ovPathEnv || DEFAULT_OV_CONF_PATH;
106+
107+
let cliFile = tryLoadJson(cliPath);
108+
let cliLoadedFrom = cliFile ? cliPath : null;
109+
let ovFile = tryLoadJson(ovPath);
110+
let ovLoadedFrom = ovFile ? ovPath : null;
111+
112+
// Backward compat: OPENVIKING_CONFIG_FILE pointing at an ovcli-shaped file.
113+
// Earlier plugin versions had a single OPENVIKING_CONFIG_FILE that could
114+
// point at either ov.conf or ovcli.conf; preserve that by promoting.
115+
if (ovPathEnv && !cliPathEnv && looksLikeOvcli(ovFile)) {
116+
cliFile = ovFile;
117+
cliLoadedFrom = ovLoadedFrom;
118+
ovFile = null;
119+
ovLoadedFrom = null;
80120
}
81121

82-
const baseUrlFromFile = deriveBaseUrl(file);
83-
const baseUrl = (str(process.env.OPENVIKING_URL, baseUrlFromFile) || "http://127.0.0.1:1933").replace(/\/+$/, "");
122+
return {
123+
cliFile: cliFile || {},
124+
cliPath: cliLoadedFrom,
125+
ovFile: ovFile || {},
126+
ovPath: ovLoadedFrom,
127+
};
128+
}
129+
130+
function deriveBaseUrl({ cliFile, ovFile }) {
131+
const envUrl = str(process.env.OPENVIKING_URL, null) || str(process.env.OPENVIKING_BASE_URL, null);
132+
if (envUrl) return envUrl.replace(/\/+$/, "");
84133

85-
const apiKeyFromFile = str(file.api_key, "") || str(file?.server?.root_api_key, "");
86-
const apiKey = str(process.env.OPENVIKING_API_KEY, apiKeyFromFile);
134+
const cliUrl = str(cliFile.url, null);
135+
if (cliUrl) return cliUrl.replace(/\/+$/, "");
87136

88-
const account = str(process.env.OPENVIKING_ACCOUNT, str(file.account, ""));
89-
const user = str(process.env.OPENVIKING_USER, str(file.user, ""));
137+
const server = ovFile.server || {};
138+
const ovUrl = str(server.url, null);
139+
if (ovUrl) return ovUrl.replace(/\/+$/, "");
90140

91-
const cx = file.codex || {};
141+
const host = str(server.host, "127.0.0.1").replace("0.0.0.0", "127.0.0.1");
142+
const port = Math.floor(num(server.port, 1933));
143+
return `http://${host}:${port}`;
144+
}
92145

93-
const debug = cx.debug === true || process.env.OPENVIKING_DEBUG === "1";
146+
export function loadConfig() {
147+
const { cliFile, cliPath, ovFile, ovPath } = loadFiles();
148+
const configPath = cliPath || ovPath || null;
149+
150+
const server = ovFile.server || {};
151+
const cx = ovFile.codex || {};
152+
153+
const baseUrl = deriveBaseUrl({ cliFile, ovFile });
154+
155+
// apiKey: env > cliFile.api_key > codex.apiKey > server.root_api_key
156+
// Accepts OPENVIKING_BEARER_TOKEN or OPENVIKING_API_KEY (sent as Bearer either way).
157+
const apiKey =
158+
str(process.env.OPENVIKING_BEARER_TOKEN, null) ||
159+
str(process.env.OPENVIKING_API_KEY, null) ||
160+
str(cliFile.api_key, null) ||
161+
str(cx.apiKey, null) ||
162+
str(server.root_api_key, "");
163+
164+
// account: env > cliFile.account > codex.accountId > ""
165+
const account =
166+
str(process.env.OPENVIKING_ACCOUNT, null) ||
167+
str(cliFile.account, null) ||
168+
str(cx.accountId, "");
169+
170+
// user: env > cliFile.user > codex.userId > ""
171+
const user =
172+
str(process.env.OPENVIKING_USER, null) ||
173+
str(cliFile.user, null) ||
174+
str(cx.userId, "");
175+
176+
// agentId: env > cliFile.agent_id > codex.agentId > "codex"
177+
const agentId =
178+
str(process.env.OPENVIKING_AGENT_ID, null) ||
179+
str(cliFile.agent_id, null) ||
180+
str(cx.agentId, "codex");
181+
182+
const debug = envBool("OPENVIKING_DEBUG") ?? (cx.debug === true);
94183
const defaultLogPath = join(homedir(), ".openviking", "logs", "codex-hooks.log");
95184
const debugLogPath = str(process.env.OPENVIKING_DEBUG_LOG, defaultLogPath);
96185

97-
const timeoutMs = Math.max(1000, Math.floor(num(cx.timeoutMs, 15000)));
98-
const captureTimeoutMs = Math.max(
99-
1000,
100-
Math.floor(num(cx.captureTimeoutMs, Math.max(timeoutMs * 2, 30000))),
101-
);
186+
const timeoutMs = Math.max(1000, Math.floor(num(
187+
process.env.OPENVIKING_TIMEOUT_MS,
188+
num(cx.timeoutMs, 15000),
189+
)));
190+
const captureTimeoutMs = Math.max(1000, Math.floor(num(
191+
process.env.OPENVIKING_CAPTURE_TIMEOUT_MS,
192+
num(cx.captureTimeoutMs, Math.max(timeoutMs * 2, 30000)),
193+
)));
102194

103195
return {
104196
configPath,
197+
cliConfigPath: cliPath,
198+
ovConfigPath: ovPath,
105199
baseUrl,
106200
apiKey,
107201
account,
108202
user,
109-
agentId: str(process.env.OPENVIKING_AGENT_ID, str(cx.agentId, "codex")),
203+
agentId,
110204
timeoutMs,
111205

112-
autoRecall: cx.autoRecall !== false,
113-
recallLimit: Math.max(1, Math.floor(num(cx.recallLimit, 6))),
114-
scoreThreshold: Math.min(1, Math.max(0, num(cx.scoreThreshold, 0.01))),
115-
minQueryLength: Math.max(1, Math.floor(num(cx.minQueryLength, 3))),
116-
logRankingDetails: cx.logRankingDetails === true,
117-
118-
autoCapture: cx.autoCapture !== false,
119-
captureMode: cx.captureMode === "keyword" ? "keyword" : "semantic",
120-
captureMaxLength: Math.max(200, Math.floor(num(cx.captureMaxLength, 24000))),
206+
autoRecall: envBool("OPENVIKING_AUTO_RECALL") ?? (cx.autoRecall !== false),
207+
recallLimit: Math.max(1, Math.floor(num(
208+
process.env.OPENVIKING_RECALL_LIMIT,
209+
num(cx.recallLimit, 6),
210+
))),
211+
scoreThreshold: Math.min(1, Math.max(0, num(
212+
process.env.OPENVIKING_SCORE_THRESHOLD,
213+
num(cx.scoreThreshold, 0.01),
214+
))),
215+
minQueryLength: Math.max(1, Math.floor(num(
216+
process.env.OPENVIKING_MIN_QUERY_LENGTH,
217+
num(cx.minQueryLength, 3),
218+
))),
219+
logRankingDetails: envBool("OPENVIKING_LOG_RANKING_DETAILS") ?? (cx.logRankingDetails === true),
220+
221+
autoCapture: envBool("OPENVIKING_AUTO_CAPTURE") ?? (cx.autoCapture !== false),
222+
captureMode: (str(process.env.OPENVIKING_CAPTURE_MODE, str(cx.captureMode, "semantic")) === "keyword")
223+
? "keyword"
224+
: "semantic",
225+
captureMaxLength: Math.max(200, Math.floor(num(
226+
process.env.OPENVIKING_CAPTURE_MAX_LENGTH,
227+
num(cx.captureMaxLength, 24000),
228+
))),
121229
captureTimeoutMs,
122-
captureAssistantTurns: cx.captureAssistantTurns === true,
123-
captureLastAssistantOnStop: cx.captureLastAssistantOnStop !== false,
230+
captureAssistantTurns: envBool("OPENVIKING_CAPTURE_ASSISTANT_TURNS") ?? (cx.captureAssistantTurns === true),
231+
captureLastAssistantOnStop: envBool("OPENVIKING_CAPTURE_LAST_ASSISTANT_ON_STOP") ?? (cx.captureLastAssistantOnStop !== false),
124232

125-
autoCommitOnCompact: cx.autoCommitOnCompact !== false,
233+
autoCommitOnCompact: envBool("OPENVIKING_AUTO_COMMIT_ON_COMPACT") ?? (cx.autoCommitOnCompact !== false),
126234

127235
debug,
128236
debugLogPath,

examples/codex-memory-plugin/scripts/pre-compact-capture.mjs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,10 @@ async function fetchJSON(path, init = {}) {
4040
const timer = setTimeout(() => controller.abort(), cfg.captureTimeoutMs);
4141
try {
4242
const headers = { "Content-Type": "application/json" };
43-
if (cfg.apiKey) headers["X-API-Key"] = cfg.apiKey;
43+
if (cfg.apiKey) {
44+
headers["Authorization"] = `Bearer ${cfg.apiKey}`;
45+
headers["X-API-Key"] = cfg.apiKey;
46+
}
4447
if (cfg.account) headers["X-OpenViking-Account"] = cfg.account;
4548
if (cfg.user) headers["X-OpenViking-User"] = cfg.user;
4649
if (cfg.agentId) headers["X-OpenViking-Agent"] = cfg.agentId;

0 commit comments

Comments
 (0)