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" ;
2236import { homedir } from "node:os" ;
2337import { 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
2842function 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,
0 commit comments