@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/INSTALL-ZH.md +50 -49
- package/INSTALL.md +50 -49
- package/README.md +57 -126
- package/index.mjs +20 -12
- package/lib/config.mjs +284 -0
- package/lib/mcp-config.mjs +21 -0
- package/lib/memory-recall.mjs +21 -172
- package/lib/memory-session.mjs +301 -478
- package/lib/repo-context.mjs +5 -5
- package/lib/session-inject.mjs +71 -0
- package/lib/shared/capture-utils.mjs +500 -0
- package/lib/shared/credentials.mjs +207 -0
- package/lib/shared/debug-log.mjs +58 -0
- package/lib/shared/mcp-proxy-core.mjs +451 -0
- package/lib/shared/pending-queue.mjs +439 -0
- package/lib/shared/profile-inject.mjs +294 -0
- package/lib/shared/recall-core.mjs +302 -0
- package/lib/shared/session-model.mjs +57 -0
- package/lib/shared/setup-wizard.mjs +111 -0
- package/lib/shared/uri-guard.mjs +56 -0
- package/lib/shared/workspace-peer.mjs +16 -0
- package/lib/utils.mjs +34 -117
- package/lib/viking-uri-guard.mjs +9 -24
- package/package.json +8 -7
- package/scripts/setup.mjs +8 -0
- package/servers/mcp-proxy.mjs +69 -0
- package/servers/mcp-proxy.test.mjs +100 -0
- package/lib/code-tools.mjs +0 -134
- package/lib/memadd-local.mjs +0 -118
- package/lib/memory-tools.mjs +0 -410
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
|
|
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
|
|
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 `
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
└──
|
|
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.
|
|
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
|
-
`
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
|
163
|
+
call and points it to the OpenViking MCP tools.
|
|
149
164
|
|
|
150
|
-
## Tools
|
|
165
|
+
## MCP Tools
|
|
151
166
|
|
|
152
|
-
|
|
167
|
+
OpenCode sees the OpenViking MCP server as `openviking`, so tool names are namespaced with `openviking_`.
|
|
153
168
|
|
|
154
|
-
|
|
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
|
-
|
|
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-
|
|
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 {
|
|
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
|
-
|
|
85
|
+
dispose: async () => {
|
|
78
86
|
await sessionManager.flushAll({ commit: true })
|
|
79
|
-
log("INFO", "plugin", "OpenViking plugin
|
|
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
|
+
}
|