@openviking/opencode-plugin 0.1.6 → 0.2.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/README.md CHANGED
@@ -4,17 +4,17 @@ A unified OpenCode plugin for OpenViking repository retrieval and long-term memo
4
4
 
5
5
  This is the only OpenCode plugin example maintained in this repository. It supersedes the former split examples for indexed repository prompt injection and long-term memory.
6
6
 
7
- The new plugin exposes everything through OpenCode tool hooks and talks to OpenViking through HTTP APIs. It does not install or require an OpenCode skill, and agents do not need to run `ov` shell commands.
7
+ The plugin uses OpenCode hooks for lifecycle behavior and registers OpenViking's standard stdio MCP proxy for model tools. It does not install or require an OpenCode skill, and agents do not need to run `ov` shell commands.
8
8
 
9
9
  ## What It Does
10
10
 
11
11
  - Injects indexed `viking://resources/` repositories into the system prompt.
12
- - Exposes repository search, grep, glob, read, browse, add, write, remove, and queue status as tools.
12
+ - Exposes the same OpenViking MCP tools used by the Claude Code and Codex memory plugins.
13
13
  - Maps each OpenCode session to an OpenViking session.
14
14
  - Captures user and assistant text messages into OpenViking.
15
15
  - Commits sessions at lifecycle boundaries for memory extraction.
16
16
  - Automatically recalls relevant memories and injects them as hidden synthetic context for the current user message.
17
- - Blocks accidental local filesystem reads of `viking://` URIs and points the agent back to `memread`, `membrowse`, or `memsearch`.
17
+ - Blocks accidental local filesystem reads of `viking://` URIs and points the agent back to `openviking_read`, `openviking_glob`, or `openviking_search`.
18
18
 
19
19
  ## Files
20
20
 
@@ -25,26 +25,29 @@ examples/opencode-plugin/
25
25
  ├── README.md
26
26
  ├── INSTALL-ZH.md
27
27
  ├── lib/
28
+ │ ├── config.mjs
29
+ │ ├── mcp-config.mjs
28
30
  │ ├── runtime.mjs
29
31
  │ ├── repo-context.mjs
30
32
  │ ├── memory-session.mjs
31
- │ ├── memadd-local.mjs
32
- │ ├── memory-tools.mjs
33
33
  │ ├── memory-recall.mjs
34
+ │ ├── session-inject.mjs
34
35
  │ ├── viking-uri-guard.mjs
35
36
  │ └── utils.mjs
37
+ ├── servers/
38
+ │ └── mcp-proxy.mjs
36
39
  ├── tests/
37
40
  └── wrappers/
38
41
  └── openviking.js
39
42
  ```
40
43
 
41
- There is intentionally no `skills/openviking/SKILL.md`. The former skill behavior is implemented as tools.
44
+ There is intentionally no `skills/openviking/SKILL.md`. The tool surface comes from OpenViking's MCP endpoint.
42
45
 
43
46
  ## Requirements
44
47
 
45
48
  - OpenCode
46
49
  - OpenViking HTTP server
47
- - Node.js / npm for installing the plugin dependency
50
+ - Node.js 18+
48
51
  - An OpenViking API key if your server requires authentication
49
52
 
50
53
  Start OpenViking first:
@@ -80,8 +83,7 @@ mkdir -p ~/.config/opencode/plugins/openviking
80
83
  cp examples/opencode-plugin/wrappers/openviking.js ~/.config/opencode/plugins/openviking.js
81
84
  cp examples/opencode-plugin/index.mjs examples/opencode-plugin/package.json ~/.config/opencode/plugins/openviking/
82
85
  cp -r examples/opencode-plugin/lib ~/.config/opencode/plugins/openviking/
83
- cd ~/.config/opencode/plugins/openviking
84
- npm install
86
+ cp -r examples/opencode-plugin/servers ~/.config/opencode/plugins/openviking/
85
87
  ```
86
88
 
87
89
  This creates a stable OpenCode plugin layout:
@@ -93,7 +95,7 @@ This creates a stable OpenCode plugin layout:
93
95
  ├── index.mjs
94
96
  ├── package.json
95
97
  ├── lib/
96
- └── node_modules/
98
+ └── servers/
97
99
  ```
98
100
 
99
101
  The top-level `openviking.js` is only a wrapper:
@@ -111,151 +113,80 @@ Create `~/.config/opencode/openviking-config.json`:
111
113
 
112
114
  ```json
113
115
  {
114
- "endpoint": "http://localhost:1933",
115
- "apiKey": "",
116
- "account": "",
117
- "user": "",
118
- "peerId": "",
119
116
  "enabled": true,
120
117
  "timeoutMs": 30000,
121
118
  "repoContext": { "enabled": true, "cacheTtlMs": 60000 },
122
119
  "autoRecall": {
123
120
  "enabled": true,
124
121
  "limit": 6,
125
- "scoreThreshold": 0.15,
122
+ "scoreThreshold": 0.35,
126
123
  "maxContentChars": 500,
127
124
  "preferAbstract": true,
128
- "tokenBudget": 2000
129
- }
125
+ "tokenBudget": 2000,
126
+ "minQueryLength": 3
127
+ },
128
+ "commitTokenThreshold": 20000,
129
+ "commitKeepRecentCount": 10,
130
+ "profileTokenBudget": 10000,
131
+ "resumeContextBudget": 32000
130
132
  }
131
133
  ```
132
134
 
133
- `apiKey` is sent as `X-API-Key`. `account` and `user` are trusted-mode identity
135
+ API keys are resolved from environment variables or `~/.openviking/ovcli.conf` and sent as `Authorization: Bearer ...` by both hooks and the MCP proxy. `account` and `user` are trusted-mode identity
134
136
  headers sent as `X-OpenViking-Account` and `X-OpenViking-User`; leave them empty
135
137
  when using API-key mode with user/admin API keys.
136
- `peerId` is sent as `X-OpenViking-Actor-Peer` on data-plane memory/resource
137
- requests; captured session messages store it as body `peer_id`. Configure
138
- `peerId` explicitly when peer-scoped memory routing is needed.
138
+ By default the plugin derives a peer from the project directory using Claude's
139
+ project-directory naming rule: every non-letter-or-digit character becomes `-`,
140
+ with no path normalization. For example, `/Users/x/Dev/OpenViking` becomes
141
+ `-Users-x-Dev-OpenViking`. Data-plane memory/resource requests send the
142
+ effective peer as `X-OpenViking-Actor-Peer`; captured session messages store it
143
+ as body `peer_id`. Configure `peerId` or `OPENVIKING_PEER_ID` to override the
144
+ workspace-derived peer, or set `workspacePeer=false` /
145
+ `OPENVIKING_WORKSPACE_PEER=0` to turn workspace-derived peers off.
146
+
147
+ Recall defaults to the broad mode: global memory, the current workspace, and
148
+ other workspace memories can all be recalled, with other workspaces penalized
149
+ and rendered later. Set `recallPeerScope="actor"` or
150
+ `OPENVIKING_RECALL_PEER_SCOPE=actor` for the isolation mode, which only sees
151
+ global memory plus the current workspace. In deployments where one bot serves
152
+ multiple real people, such as zouk, vikingbot, or AstrBot, use the isolation mode
153
+ with an explicit actor peer so one person's memories are not recalled into
154
+ another person's session.
139
155
 
140
156
  `OPENVIKING_API_KEY`, `OPENVIKING_ACCOUNT`, `OPENVIKING_USER`,
141
- and `OPENVIKING_PEER_ID` take
142
- precedence over values in this file.
157
+ and `OPENVIKING_PEER_ID` take precedence over values in this file.
143
158
 
144
159
  For advanced setups, `OPENVIKING_PLUGIN_CONFIG` can point to another config file path.
145
160
 
146
161
  OpenCode's local `read`, `glob`, and `grep` tools cannot read `viking://` URIs.
147
162
  When the agent accidentally tries that, the plugin blocks the filesystem tool
148
- call and points it to `memread`, `membrowse`, or `memsearch`.
163
+ call and points it to the OpenViking MCP tools.
149
164
 
150
- ## Tools
165
+ ## MCP Tools
151
166
 
152
- ### `memsearch`
167
+ OpenCode sees the OpenViking MCP server as `openviking`, so tool names are namespaced with `openviking_`.
153
168
 
154
- Semantic search across memories, resources, and skills.
169
+ - `openviking_recall`: balanced current-task recall using OpenViking's `/recall` endpoint.
170
+ - `openviking_search`: deep semantic retrieval across memories, resources, and skills.
171
+ - `openviking_find`: fast semantic retrieval.
172
+ - `openviking_remember`: store important facts or decisions for memory extraction.
173
+ - `openviking_read`: read one or more `viking://` files.
174
+ - `openviking_list`: list a `viking://` directory.
175
+ - `openviking_grep`: exact text or regex search.
176
+ - `openviking_glob`: glob file matching.
177
+ - `openviking_add_resource`: add a URL, local file, sitemap, or feed.
178
+ - `openviking_forget`: delete a `viking://` URI after explicit user confirmation.
179
+ - `openviking_list_watches` / `openviking_cancel_watch`: inspect or cancel resource watches.
180
+ - `openviking_code_search`, `openviking_code_outline`, `openviking_code_expand`: inspect indexed code symbols.
181
+ - `openviking_health`: check OpenViking server health.
155
182
 
156
- Use for conceptual questions, repository internals, user preferences, and context-aware retrieval. Use `target_uri` to narrow scope, for example `viking://resources/fastapi/`.
157
-
158
- ### `memread`
159
-
160
- Read a specific `viking://` URI using `abstract`, `overview`, `read`, or `auto`.
161
-
162
- Use after `memsearch`, `memgrep`, `memglob`, or `membrowse` returns a URI.
163
-
164
- ### `membrowse`
165
-
166
- Browse OpenViking filesystem structure with `list`, `tree`, or `stat`.
167
-
168
- Use to discover exact URIs before reading content.
169
-
170
- ### `memcommit`
171
-
172
- Commit the current OpenCode session to OpenViking and trigger memory extraction.
173
-
174
- The plugin also commits at session deletion, session error, compaction, and plugin shutdown boundaries.
175
-
176
- ### `memgrep`
177
-
178
- Pattern search through OpenViking content.
179
-
180
- Use for exact symbols, class names, function names, error strings, or known keywords.
181
-
182
- ### `memglob`
183
-
184
- Glob file matching through OpenViking content.
185
-
186
- Use to enumerate files such as `**/*.py`, `**/test_*.ts`, or `**/*.md`.
187
-
188
- ### `memadd`
189
-
190
- Add a remote URL or local file resource to OpenViking.
191
-
192
- Remote `http(s)` URLs go directly through `POST /api/v1/resources`.
193
- Local files use the safer two-step server flow: upload the file to
194
- `POST /api/v1/resources/temp_upload`, then add it through
195
- `POST /api/v1/resources` with the returned `temp_file_id`.
196
-
197
- Local paths may be absolute, relative to the OpenCode project directory, or
198
- `file://` URLs. Local directory upload is not supported yet.
199
-
200
- Examples:
201
-
202
- ```text
203
- memadd path="https://example.com/spec.md" to="viking://resources/spec"
204
- memadd path="./docs/notes.md" parent="viking://resources/"
205
- memadd path="file:///home/alice/project/notes.md" reason="project notes"
206
- ```
207
-
208
- After adding a resource, the tool also returns `GET /api/v1/observer/queue` status.
209
-
210
- ### `memwrite`
211
-
212
- Write text content to a `viking://` file through `POST /api/v1/content/write`.
213
-
214
- Use this for durable notes, small project memory files, or resource text that
215
- should be updated directly. The default mode is `create` to avoid accidental
216
- overwrites. Use `append` to extend an existing file and `replace` only when the
217
- user explicitly wants to overwrite the file.
218
-
219
- Examples:
220
-
221
- ```text
222
- memwrite uri="viking://user/memories/project-notes.md" content="# Decision\n\nUse PostgreSQL." wait=true
223
- memwrite uri="viking://resources/docs/api.md" content="\n\n## New endpoint" mode="append"
224
- ```
225
-
226
- ### `memremove`
227
-
228
- Remove a `viking://` URI through `DELETE /api/v1/fs`.
229
-
230
- This tool requires `confirm: true`. The user must explicitly confirm deletion before the agent calls it.
231
-
232
- ### `memqueue`
233
-
234
- Return OpenViking observer queue status for embedding and semantic processing.
235
-
236
- ### `codesearch`
237
-
238
- Search AST-supported symbol names across a confirmed ingested code repository or source subtree.
239
-
240
- Use this after you have evidence that a `viking://` URI contains supported source files.
241
-
242
- ### `codeoutline`
243
-
244
- Show symbol structure for a confirmed `viking://` source file.
245
-
246
- Use this after `memglob`, `membrowse`, or `codesearch` finds an exact source file URI.
247
-
248
- ### `codeexpand`
249
-
250
- Return the full source for one named symbol from a confirmed `viking://` source file.
251
-
252
- Use this after `codeoutline` or other evidence shows the symbol exists in that file.
183
+ The proxy forwards the server's real `tools/list` response; the plugin does not maintain a separate native tool list.
253
184
 
254
185
  ## Runtime Files
255
186
 
256
187
  The plugin writes runtime files to `~/.config/opencode/openviking/` by default:
257
188
 
258
189
  - `openviking-memory.log`
259
- - `openviking-session-map.json`
190
+ - `openviking-session-state.json`
260
191
 
261
192
  Set `runtime.dataDir` in config to override this directory.
package/index.mjs CHANGED
@@ -3,17 +3,15 @@ import { fileURLToPath } from "url"
3
3
  import { initializeRuntime } from "./lib/runtime.mjs"
4
4
  import { createRepoContext } from "./lib/repo-context.mjs"
5
5
  import { createMemorySessionManager } from "./lib/memory-session.mjs"
6
- import { createCodeTools } from "./lib/code-tools.mjs"
7
- import { createMemoryTools } from "./lib/memory-tools.mjs"
8
6
  import { createMemoryRecall } from "./lib/memory-recall.mjs"
7
+ import { createSessionInject } from "./lib/session-inject.mjs"
9
8
  import { createVikingUriGuard } from "./lib/viking-uri-guard.mjs"
10
- import { initLogger, loadConfig, log, resolveDataDir } from "./lib/utils.mjs"
9
+ import { injectOpenVikingMcpConfig } from "./lib/mcp-config.mjs"
10
+ import { loadConfig, resolveDataDir } from "./lib/config.mjs"
11
+ import { initLogger, log, makeToast } from "./lib/utils.mjs"
11
12
 
12
13
  const pluginRoot = dirname(fileURLToPath(import.meta.url))
13
14
 
14
- /**
15
- * @type {import('@opencode-ai/plugin').Plugin}
16
- */
17
15
  export async function OpenVikingPlugin({ client, directory }) {
18
16
  const config = loadConfig(pluginRoot, directory)
19
17
  const dataDir = resolveDataDir(pluginRoot, config)
@@ -27,11 +25,17 @@ export async function OpenVikingPlugin({ client, directory }) {
27
25
  const repoContext = createRepoContext({ config })
28
26
  const sessionManager = createMemorySessionManager({ config, pluginRoot: dataDir })
29
27
  const recall = createMemoryRecall({ config })
28
+ const sessionInject = createSessionInject({ config, sessionManager })
30
29
  const vikingUriGuard = createVikingUriGuard()
31
- const tools = createMemoryTools({ config, sessionManager, projectDirectory: directory })
32
- const codeTools = createCodeTools({ config })
33
30
 
34
31
  await sessionManager.init()
32
+ const toast = makeToast(client)
33
+ if (config.legacyCredentialsUsed) {
34
+ log("WARN", "config", "Legacy OpenCode credential fields are still in use; run node scripts/setup.mjs to migrate to ovcli.conf", {
35
+ configPath: config.configPath,
36
+ })
37
+ await toast("OpenViking credentials in openviking-config.json are deprecated. Run node scripts/setup.mjs to migrate.", "warning")
38
+ }
35
39
 
36
40
  Promise.resolve().then(async () => {
37
41
  const ready = await initializeRuntime(config, client)
@@ -39,6 +43,11 @@ export async function OpenVikingPlugin({ client, directory }) {
39
43
  })
40
44
 
41
45
  return {
46
+ config: async (opencodeConfig) => {
47
+ const injected = injectOpenVikingMcpConfig(opencodeConfig, pluginRoot)
48
+ log(injected ? "INFO" : "WARN", "mcp", injected ? "Registered OpenViking MCP server" : "OpenViking MCP server was not registered")
49
+ },
50
+
42
51
  event: async ({ event }) => {
43
52
  await sessionManager.handleEvent(event)
44
53
  if (event?.type === "session.created") {
@@ -48,8 +57,6 @@ export async function OpenVikingPlugin({ client, directory }) {
48
57
 
49
58
  "tool.execute.before": vikingUriGuard,
50
59
 
51
- tool: { ...tools, ...codeTools },
52
-
53
60
  "experimental.chat.system.transform": (_input, output) => {
54
61
  const prompt = repoContext.getRepoSystemPrompt()
55
62
  if (prompt) output.system.push(prompt)
@@ -57,6 +64,7 @@ export async function OpenVikingPlugin({ client, directory }) {
57
64
 
58
65
  "chat.message": async (input, output) => {
59
66
  try {
67
+ await sessionInject.injectSessionContext(input, output)
60
68
  if (!config.autoRecall?.enabled) return
61
69
  await recall.injectRelevantMemories(input, output)
62
70
  } catch (error) {
@@ -74,9 +82,9 @@ export async function OpenVikingPlugin({ client, directory }) {
74
82
  })
75
83
  },
76
84
 
77
- stop: async () => {
85
+ dispose: async () => {
78
86
  await sessionManager.flushAll({ commit: true })
79
- log("INFO", "plugin", "OpenViking plugin stopped")
87
+ log("INFO", "plugin", "OpenViking plugin disposed")
80
88
  },
81
89
  }
82
90
  }
package/lib/config.mjs ADDED
@@ -0,0 +1,284 @@
1
+ import fs from "fs"
2
+ import path from "path"
3
+ import { homedir } from "os"
4
+ import { resolveOpenVikingCredentials } from "./shared/credentials.mjs"
5
+ import { resolveEffectivePeerId } from "./shared/workspace-peer.mjs"
6
+
7
+ const DEFAULT_CONFIG = {
8
+ endpoint: "http://127.0.0.1:1933",
9
+ apiKey: "",
10
+ account: "",
11
+ user: "",
12
+ peerId: "",
13
+ workspacePeer: true,
14
+ recallPeerScope: "all",
15
+ enabled: true,
16
+ timeoutMs: 30000,
17
+ runtime: {
18
+ dataDir: "",
19
+ },
20
+ repoContext: {
21
+ enabled: true,
22
+ cacheTtlMs: 60000,
23
+ },
24
+ autoRecall: {
25
+ enabled: true,
26
+ limit: 6,
27
+ scoreThreshold: 0.35,
28
+ maxContentChars: 500,
29
+ preferAbstract: true,
30
+ tokenBudget: 2000,
31
+ minQueryLength: 3,
32
+ },
33
+ autoCapture: true,
34
+ captureMode: "semantic",
35
+ captureMaxLength: 24000,
36
+ captureAssistantTurns: true,
37
+ captureToolMaxChars: 2000,
38
+ commitTokenThreshold: 20000,
39
+ commitKeepRecentCount: 10,
40
+ profileTokenBudget: 10000,
41
+ resumeContextBudget: 32000,
42
+ noAutoInject: false,
43
+ bypassSession: false,
44
+ bypassSessionPatterns: [],
45
+ debug: false,
46
+ debugLogPath: path.join(homedir(), ".openviking", "logs", "opencode-plugin.log"),
47
+ }
48
+
49
+ function cloneDefaultConfig() {
50
+ return JSON.parse(JSON.stringify(DEFAULT_CONFIG))
51
+ }
52
+
53
+ function normalizeNumber(value, fallback, min, max) {
54
+ const next = Number(value)
55
+ if (!Number.isFinite(next)) return fallback
56
+ return Math.max(min, Math.min(max, next))
57
+ }
58
+
59
+ function envBool(name) {
60
+ const value = process.env[name]
61
+ if (value == null || value === "") return undefined
62
+ const lower = String(value).trim().toLowerCase()
63
+ if (lower === "0" || lower === "false" || lower === "no" || lower === "off") return false
64
+ if (lower === "1" || lower === "true" || lower === "yes" || lower === "on") return true
65
+ return undefined
66
+ }
67
+
68
+ function str(value, fallback = "") {
69
+ return typeof value === "string" && value.trim() ? value.trim() : fallback
70
+ }
71
+
72
+ function expandHome(value) {
73
+ if (!value || typeof value !== "string") return value
74
+ if (value === "~") return homedir()
75
+ if (value.startsWith("~/") || value.startsWith("~\\")) return path.join(homedir(), value.slice(2))
76
+ return value
77
+ }
78
+
79
+ function readConfigFile(pluginRoot, projectDirectory) {
80
+ for (const configPath of getConfigPaths(pluginRoot, projectDirectory)) {
81
+ try {
82
+ if (!fs.existsSync(configPath)) continue
83
+ return { path: configPath, data: JSON.parse(fs.readFileSync(configPath, "utf8")) }
84
+ } catch (error) {
85
+ console.warn(`Failed to load OpenViking config from ${configPath}:`, error)
86
+ }
87
+ }
88
+ return { path: "", data: {} }
89
+ }
90
+
91
+ function getConfigPaths(pluginRoot, projectDirectory) {
92
+ const paths = []
93
+ if (process.env.OPENVIKING_PLUGIN_CONFIG) paths.push(expandHome(process.env.OPENVIKING_PLUGIN_CONFIG))
94
+ if (projectDirectory) paths.push(path.join(projectDirectory, ".opencode", "openviking-config.json"))
95
+ paths.push(path.join(homedir(), ".config", "opencode", "openviking-config.json"))
96
+ paths.push(path.join(pluginRoot, "openviking-config.json"))
97
+ return paths
98
+ }
99
+
100
+ function applyLegacyConnection(config, fileConfig) {
101
+ const hasLegacyCredentials = ["endpoint", "apiKey", "account", "user", "peerId"]
102
+ .some((key) => fileConfig[key] !== undefined && fileConfig[key] !== "")
103
+ if (!hasLegacyCredentials) return false
104
+
105
+ if (fileConfig.endpoint !== undefined) config.endpoint = fileConfig.endpoint
106
+ if (fileConfig.apiKey !== undefined) config.apiKey = fileConfig.apiKey
107
+ if (fileConfig.account !== undefined) config.account = fileConfig.account
108
+ if (fileConfig.user !== undefined) config.user = fileConfig.user
109
+ if (fileConfig.peerId !== undefined) config.peerId = fileConfig.peerId
110
+ return true
111
+ }
112
+
113
+ function applyBehaviorConfig(config, fileConfig = {}) {
114
+ if (fileConfig.enabled !== undefined) config.enabled = fileConfig.enabled !== false
115
+ if (fileConfig.timeoutMs !== undefined) config.timeoutMs = fileConfig.timeoutMs
116
+ config.runtime = {
117
+ ...DEFAULT_CONFIG.runtime,
118
+ ...(fileConfig.runtime ?? {}),
119
+ }
120
+ config.repoContext = {
121
+ ...DEFAULT_CONFIG.repoContext,
122
+ ...(fileConfig.repoContext ?? {}),
123
+ }
124
+
125
+ const autoRecall = fileConfig.autoRecall ?? {}
126
+ config.autoRecall = {
127
+ ...DEFAULT_CONFIG.autoRecall,
128
+ ...autoRecall,
129
+ enabled: autoRecall.enabled !== false,
130
+ limit: autoRecall.limit ?? fileConfig.recallLimit ?? DEFAULT_CONFIG.autoRecall.limit,
131
+ scoreThreshold: autoRecall.scoreThreshold ?? fileConfig.scoreThreshold ?? DEFAULT_CONFIG.autoRecall.scoreThreshold,
132
+ maxContentChars: autoRecall.maxContentChars ?? fileConfig.recallMaxContentChars ?? DEFAULT_CONFIG.autoRecall.maxContentChars,
133
+ preferAbstract: autoRecall.preferAbstract ?? fileConfig.recallPreferAbstract ?? DEFAULT_CONFIG.autoRecall.preferAbstract,
134
+ tokenBudget: autoRecall.tokenBudget ?? fileConfig.recallTokenBudget ?? DEFAULT_CONFIG.autoRecall.tokenBudget,
135
+ minQueryLength: autoRecall.minQueryLength ?? fileConfig.minQueryLength ?? DEFAULT_CONFIG.autoRecall.minQueryLength,
136
+ }
137
+
138
+ for (const key of [
139
+ "autoCapture",
140
+ "captureMode",
141
+ "captureMaxLength",
142
+ "captureAssistantTurns",
143
+ "captureToolMaxChars",
144
+ "commitTokenThreshold",
145
+ "commitKeepRecentCount",
146
+ "profileTokenBudget",
147
+ "resumeContextBudget",
148
+ "noAutoInject",
149
+ "bypassSession",
150
+ "bypassSessionPatterns",
151
+ "debug",
152
+ "debugLogPath",
153
+ "workspacePeer",
154
+ "recallPeerScope",
155
+ ]) {
156
+ if (fileConfig[key] !== undefined) config[key] = fileConfig[key]
157
+ }
158
+ }
159
+
160
+ function applyEnv(config) {
161
+ if (process.env.OPENVIKING_TIMEOUT_MS) config.timeoutMs = process.env.OPENVIKING_TIMEOUT_MS
162
+ if (process.env.OPENVIKING_AUTO_RECALL !== undefined) {
163
+ config.autoRecall.enabled = envBool("OPENVIKING_AUTO_RECALL") ?? config.autoRecall.enabled
164
+ }
165
+ if (process.env.OPENVIKING_RECALL_LIMIT) config.autoRecall.limit = process.env.OPENVIKING_RECALL_LIMIT
166
+ if (process.env.OPENVIKING_SCORE_THRESHOLD) config.autoRecall.scoreThreshold = process.env.OPENVIKING_SCORE_THRESHOLD
167
+ if (process.env.OPENVIKING_RECALL_MAX_CONTENT_CHARS) {
168
+ config.autoRecall.maxContentChars = process.env.OPENVIKING_RECALL_MAX_CONTENT_CHARS
169
+ }
170
+ if (process.env.OPENVIKING_RECALL_TOKEN_BUDGET) config.autoRecall.tokenBudget = process.env.OPENVIKING_RECALL_TOKEN_BUDGET
171
+ if (process.env.OPENVIKING_RECALL_PREFER_ABSTRACT !== undefined) {
172
+ config.autoRecall.preferAbstract = envBool("OPENVIKING_RECALL_PREFER_ABSTRACT") ?? config.autoRecall.preferAbstract
173
+ }
174
+ if (process.env.OPENVIKING_RECALL_PEER_SCOPE) config.recallPeerScope = process.env.OPENVIKING_RECALL_PEER_SCOPE
175
+ if (process.env.OPENVIKING_WORKSPACE_PEER !== undefined) {
176
+ config.workspacePeer = envBool("OPENVIKING_WORKSPACE_PEER") ?? config.workspacePeer
177
+ }
178
+ if (process.env.OPENVIKING_MIN_QUERY_LENGTH) config.autoRecall.minQueryLength = process.env.OPENVIKING_MIN_QUERY_LENGTH
179
+ if (process.env.OPENVIKING_AUTO_CAPTURE !== undefined) {
180
+ config.autoCapture = envBool("OPENVIKING_AUTO_CAPTURE") ?? config.autoCapture
181
+ }
182
+ if (process.env.OPENVIKING_CAPTURE_MODE) config.captureMode = process.env.OPENVIKING_CAPTURE_MODE
183
+ if (process.env.OPENVIKING_CAPTURE_MAX_LENGTH) config.captureMaxLength = process.env.OPENVIKING_CAPTURE_MAX_LENGTH
184
+ if (process.env.OPENVIKING_CAPTURE_ASSISTANT_TURNS !== undefined) {
185
+ config.captureAssistantTurns = envBool("OPENVIKING_CAPTURE_ASSISTANT_TURNS") ?? config.captureAssistantTurns
186
+ }
187
+ if (process.env.OPENVIKING_CAPTURE_TOOL_MAX_CHARS) {
188
+ config.captureToolMaxChars = process.env.OPENVIKING_CAPTURE_TOOL_MAX_CHARS
189
+ }
190
+ if (process.env.OPENVIKING_COMMIT_TOKEN_THRESHOLD) {
191
+ config.commitTokenThreshold = process.env.OPENVIKING_COMMIT_TOKEN_THRESHOLD
192
+ }
193
+ if (process.env.OPENVIKING_COMMIT_KEEP_RECENT_COUNT) {
194
+ config.commitKeepRecentCount = process.env.OPENVIKING_COMMIT_KEEP_RECENT_COUNT
195
+ }
196
+ if (process.env.OPENVIKING_PROFILE_TOKEN_BUDGET) config.profileTokenBudget = process.env.OPENVIKING_PROFILE_TOKEN_BUDGET
197
+ if (process.env.OPENVIKING_RESUME_CONTEXT_BUDGET) config.resumeContextBudget = process.env.OPENVIKING_RESUME_CONTEXT_BUDGET
198
+ if (process.env.OPENVIKING_NO_AUTO_INJECT !== undefined) {
199
+ config.noAutoInject = envBool("OPENVIKING_NO_AUTO_INJECT") ?? config.noAutoInject
200
+ }
201
+ if (process.env.OPENVIKING_BYPASS_SESSION !== undefined) {
202
+ config.bypassSession = envBool("OPENVIKING_BYPASS_SESSION") ?? config.bypassSession
203
+ }
204
+ if (process.env.OPENVIKING_BYPASS_SESSION_PATTERNS) {
205
+ config.bypassSessionPatterns = process.env.OPENVIKING_BYPASS_SESSION_PATTERNS
206
+ .split(",")
207
+ .map((item) => item.trim())
208
+ .filter(Boolean)
209
+ }
210
+ if (process.env.OPENVIKING_DEBUG !== undefined) config.debug = envBool("OPENVIKING_DEBUG") ?? config.debug
211
+ if (process.env.OPENVIKING_DEBUG_LOG) config.debugLogPath = process.env.OPENVIKING_DEBUG_LOG
212
+ }
213
+
214
+ function normalizeConfig(config) {
215
+ config.endpoint = str(config.endpoint, DEFAULT_CONFIG.endpoint).replace(/\/+$/, "")
216
+ config.baseUrl = config.endpoint
217
+ config.accountId = config.account
218
+ config.userId = config.user
219
+ config.timeoutMs = normalizeNumber(config.timeoutMs, DEFAULT_CONFIG.timeoutMs, 1000, 300000)
220
+ config.repoContext.cacheTtlMs = normalizeNumber(
221
+ config.repoContext.cacheTtlMs,
222
+ DEFAULT_CONFIG.repoContext.cacheTtlMs,
223
+ 1000,
224
+ 60 * 60 * 1000,
225
+ )
226
+ config.autoRecall.limit = Math.max(1, Math.min(50, Math.round(Number(config.autoRecall.limit) || 6)))
227
+ config.autoRecall.scoreThreshold = Math.max(0, Math.min(1, Number(config.autoRecall.scoreThreshold) || 0))
228
+ config.autoRecall.maxContentChars = Math.max(100, Math.min(5000, Math.round(Number(config.autoRecall.maxContentChars) || 500)))
229
+ config.autoRecall.tokenBudget = Math.max(200, Math.min(50000, Math.round(Number(config.autoRecall.tokenBudget) || 2000)))
230
+ config.autoRecall.minQueryLength = Math.max(1, Math.min(64, Math.round(Number(config.autoRecall.minQueryLength) || 3)))
231
+ config.captureMode = config.captureMode === "keyword" ? "keyword" : "semantic"
232
+ config.recallPeerScope = config.recallPeerScope === "actor" ? "actor" : "all"
233
+ config.captureMaxLength = Math.max(200, Math.min(100000, Math.round(Number(config.captureMaxLength) || 24000)))
234
+ config.captureToolMaxChars = Math.max(200, Math.min(20000, Math.round(Number(config.captureToolMaxChars) || 2000)))
235
+ config.commitTokenThreshold = Math.max(1000, Math.round(Number(config.commitTokenThreshold) || 20000))
236
+ config.commitKeepRecentCount = Math.max(0, Math.round(Number(config.commitKeepRecentCount) || 10))
237
+ config.profileTokenBudget = Math.max(500, Math.round(Number(config.profileTokenBudget) || 10000))
238
+ config.resumeContextBudget = Math.max(1024, Math.round(Number(config.resumeContextBudget) || 32000))
239
+ if (!Array.isArray(config.bypassSessionPatterns)) config.bypassSessionPatterns = []
240
+
241
+ config.recallLimit = config.autoRecall.limit
242
+ config.scoreThreshold = config.autoRecall.scoreThreshold
243
+ config.recallMaxContentChars = config.autoRecall.maxContentChars
244
+ config.recallPreferAbstract = config.autoRecall.preferAbstract !== false
245
+ config.recallTokenBudget = config.autoRecall.tokenBudget
246
+ config.minQueryLength = config.autoRecall.minQueryLength
247
+ return config
248
+ }
249
+
250
+ export function loadConfig(pluginRoot, projectDirectory) {
251
+ const config = cloneDefaultConfig()
252
+ const { path: configPath, data: fileConfig } = readConfigFile(pluginRoot, projectDirectory)
253
+ applyBehaviorConfig(config, fileConfig)
254
+
255
+ const creds = resolveOpenVikingCredentials()
256
+ config.endpoint = creds.baseUrl
257
+ config.apiKey = creds.apiKey
258
+ config.account = creds.account
259
+ config.user = creds.user
260
+ config.peerId = creds.peerId
261
+ config.mcpUrl = creds.mcpUrl
262
+ config.credentialSource = creds.credentialSource
263
+ config.credentialPath = creds.cliPath || creds.ovPath || ""
264
+
265
+ const mayUseLegacyCredentials = creds.credentialSource !== "env" &&
266
+ creds.credentialSource !== "ovcli" &&
267
+ !creds.apiKey &&
268
+ !creds.account &&
269
+ !creds.user &&
270
+ !creds.peerId
271
+ const legacyUsed = mayUseLegacyCredentials ? applyLegacyConnection(config, fileConfig) : false
272
+ applyEnv(config)
273
+ config.configPath = configPath
274
+ config.legacyCredentialsUsed = legacyUsed && creds.credentialSource !== "env"
275
+ const normalized = normalizeConfig(config)
276
+ normalized.effectivePeer = resolveEffectivePeerId({ cfg: normalized, cwd: projectDirectory })
277
+ return normalized
278
+ }
279
+
280
+ export function resolveDataDir(pluginRoot, config) {
281
+ const configured = config.runtime?.dataDir
282
+ if (configured) return expandHome(configured)
283
+ return path.join(homedir(), ".config", "opencode", "openviking")
284
+ }
@@ -0,0 +1,21 @@
1
+ import { resolve as resolvePath } from "node:path"
2
+
3
+ export const OPENCODE_MCP_NAME = "openviking"
4
+
5
+ export function createOpenVikingMcpConfig(pluginRoot) {
6
+ return {
7
+ type: "local",
8
+ command: ["node", resolvePath(pluginRoot, "servers", "mcp-proxy.mjs")],
9
+ enabled: true,
10
+ timeout: 15000,
11
+ }
12
+ }
13
+
14
+ export function injectOpenVikingMcpConfig(config, pluginRoot) {
15
+ if (!config || typeof config !== "object") return false
16
+ config.mcp = config.mcp && typeof config.mcp === "object" ? config.mcp : {}
17
+ const current = config.mcp[OPENCODE_MCP_NAME]
18
+ if (current?.enabled === false) return false
19
+ config.mcp[OPENCODE_MCP_NAME] = createOpenVikingMcpConfig(pluginRoot)
20
+ return true
21
+ }