@openviking/opencode-plugin 0.2.3 → 2026.9.9-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INSTALL-ZH.md +25 -7
- package/INSTALL.md +27 -7
- package/README.md +52 -13
- package/index.mjs +10 -3
- package/lib/config.mjs +50 -8
- package/lib/mcp-config.mjs +2 -1
- package/lib/memory-recall.mjs +19 -7
- package/lib/memory-session.mjs +61 -18
- package/lib/runtime.mjs +1 -0
- package/lib/shared/batch-send.mjs +2 -3
- package/lib/shared/capture-utils.mjs +99 -21
- package/lib/shared/credentials.mjs +32 -0
- package/lib/shared/input-filters.mjs +219 -0
- package/lib/shared/mcp-proxy-config.mjs +124 -0
- package/lib/shared/mcp-proxy-core.mjs +60 -2
- package/lib/shared/pending-queue.mjs +13 -3
- package/lib/shared/recall-compress-core.mjs +206 -0
- package/lib/shared/recall-core.mjs +394 -18
- package/lib/shared/retryable.mjs +9 -0
- package/lib/shared/setup-wizard.mjs +14 -11
- package/lib/shared/uri-guard.mjs +28 -6
- package/lib/shared/workspace-identity.mjs +334 -0
- package/lib/shared/workspace-peer.mjs +126 -8
- package/lib/utils.mjs +8 -1
- package/lib/viking-uri-guard.mjs +1 -1
- package/package.json +3 -2
- package/servers/mcp-proxy.mjs +16 -39
- package/servers/mcp-proxy.test.mjs +2 -0
- package/lib/shared/async-writer.mjs +0 -78
package/INSTALL-ZH.md
CHANGED
|
@@ -92,6 +92,7 @@ export { OpenVikingPlugin, default } from "./openviking/index.mjs"
|
|
|
92
92
|
```json
|
|
93
93
|
{
|
|
94
94
|
"enabled": true,
|
|
95
|
+
"mcp": { "enabled": true },
|
|
95
96
|
"timeoutMs": 30000,
|
|
96
97
|
"repoContext": { "enabled": true, "cacheTtlMs": 60000 },
|
|
97
98
|
"autoRecall": {
|
|
@@ -110,6 +111,9 @@ export { OpenVikingPlugin, default } from "./openviking/index.mjs"
|
|
|
110
111
|
}
|
|
111
112
|
```
|
|
112
113
|
|
|
114
|
+
`autoRecall.limit` 是遗留的配额缩放输入,不是最终结果上限。显式设置为
|
|
115
|
+
1 到 5 时,有效总配额仍为 6,因为六个 coding 分类会各保留一个检索槽位。
|
|
116
|
+
|
|
113
117
|
推荐通过环境变量提供 API Key,而不是写入配置文件:
|
|
114
118
|
|
|
115
119
|
```bash
|
|
@@ -127,17 +131,30 @@ user/admin API key 的 API_KEY mode 时应留空。
|
|
|
127
131
|
|
|
128
132
|
高级场景可以用 `OPENVIKING_PLUGIN_CONFIG` 指向其他配置文件路径。
|
|
129
133
|
|
|
134
|
+
### 仅 Hooks 模式
|
|
135
|
+
|
|
136
|
+
如果其他 MCP server 已经提供 OpenViking,可以关闭本插件附带的 MCP 注册,同时保留生命周期 hooks:
|
|
137
|
+
|
|
138
|
+
```json
|
|
139
|
+
{
|
|
140
|
+
"mcp": { "enabled": false }
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
repository context、自动 recall、消息 capture 和生命周期 commit 会继续工作,也不会添加或覆盖
|
|
145
|
+
OpenCode 的 `mcp.openviking` 配置。
|
|
146
|
+
|
|
130
147
|
## 验证
|
|
131
148
|
|
|
132
149
|
修改插件或 OpenViking 配置后,需要重启 OpenCode。
|
|
133
150
|
|
|
134
151
|
进入新的 OpenCode session 后,可以让 agent 浏览 OpenViking memory,或搜索一个已索引的资源。插件应暴露 OpenViking MCP server,OpenCode 中的工具名会带 `openviking_` 前缀:
|
|
135
152
|
|
|
136
|
-
- `
|
|
137
|
-
- `openviking_read`、`openviking_list`、`openviking_grep`、`openviking_glob`
|
|
138
|
-
- `openviking_remember`、`
|
|
153
|
+
- `openviking_search`、`openviking_find`
|
|
154
|
+
- `openviking_read`、`openviking_list`、`openviking_tree`、`openviking_grep`、`openviking_glob`
|
|
155
|
+
- `openviking_remember`、`openviking_write`、`openviking_edit`、`openviking_add_resource`
|
|
156
|
+
- `openviking_list_watches`、`openviking_cancel_watch`、`openviking_forget`、`openviking_health`
|
|
139
157
|
- `openviking_list_watches`、`openviking_cancel_watch`
|
|
140
|
-
- `openviking_code_search`、`openviking_code_outline`、`openviking_code_expand`
|
|
141
158
|
|
|
142
159
|
如果行为异常,先查看运行时文件:
|
|
143
160
|
|
|
@@ -156,18 +173,19 @@ curl http://localhost:1933/health
|
|
|
156
173
|
|
|
157
174
|
插件会通过 OpenCode config 注册 OpenViking stdio MCP proxy。服务端实际返回的 `tools/list` 是最终工具清单;当前 OpenViking server 暴露:
|
|
158
175
|
|
|
159
|
-
- `
|
|
160
|
-
- `openviking_search`:跨 memories/resources/skills 的深度语义检索
|
|
176
|
+
- `openviking_search`:跨 memories/resources/skills 的深度语义检索;使用 `mode="context"` 获取面向当前任务、可直接注入的平衡上下文
|
|
161
177
|
- `openviking_find`:快速语义检索
|
|
162
178
|
- `openviking_remember`:存储重要事实或决策,供记忆提取
|
|
163
179
|
- `openviking_read`:读取一个或多个 `viking://` 文件
|
|
164
180
|
- `openviking_list`:列出 `viking://` 目录
|
|
181
|
+
- `openviking_tree`:展示 `viking://` 目录树
|
|
165
182
|
- `openviking_grep`:精确文本或正则搜索
|
|
166
183
|
- `openviking_glob`:glob 文件匹配
|
|
184
|
+
- `openviking_write`:创建、覆盖或追加 `viking://` 文件
|
|
185
|
+
- `openviking_edit`:对 `viking://` 文件做精确字符串替换
|
|
167
186
|
- `openviking_add_resource`:添加 URL、本地文件、sitemap 或 feed
|
|
168
187
|
- `openviking_forget`:在用户明确确认后删除 `viking://` URI
|
|
169
188
|
- `openviking_list_watches` / `openviking_cancel_watch`:查看或取消资源 watch
|
|
170
|
-
- `openviking_code_search`、`openviking_code_outline`、`openviking_code_expand`:查看已索引代码符号
|
|
171
189
|
- `openviking_health`:检查 OpenViking server 健康状态
|
|
172
190
|
|
|
173
191
|
使用建议:
|
package/INSTALL.md
CHANGED
|
@@ -92,6 +92,7 @@ Example configuration:
|
|
|
92
92
|
```json
|
|
93
93
|
{
|
|
94
94
|
"enabled": true,
|
|
95
|
+
"mcp": { "enabled": true },
|
|
95
96
|
"timeoutMs": 30000,
|
|
96
97
|
"repoContext": { "enabled": true, "cacheTtlMs": 60000 },
|
|
97
98
|
"autoRecall": {
|
|
@@ -110,6 +111,10 @@ Example configuration:
|
|
|
110
111
|
}
|
|
111
112
|
```
|
|
112
113
|
|
|
114
|
+
`autoRecall.limit` is a legacy quota-scaling input, not a final result cap.
|
|
115
|
+
Explicit values from 1 through 5 produce an effective total quota of 6 because
|
|
116
|
+
each coding category keeps one retrieval slot.
|
|
117
|
+
|
|
113
118
|
It is recommended to provide the API key through an environment variable instead of writing it into the configuration file:
|
|
114
119
|
|
|
115
120
|
```bash
|
|
@@ -122,17 +127,31 @@ API keys are resolved from environment variables or `~/.openviking/ovcli.conf` a
|
|
|
122
127
|
|
|
123
128
|
For advanced setups, use `OPENVIKING_PLUGIN_CONFIG` to point to another configuration file path.
|
|
124
129
|
|
|
130
|
+
### Hook-only mode
|
|
131
|
+
|
|
132
|
+
If another MCP server already exposes OpenViking, set the bundled MCP registration to `false` while
|
|
133
|
+
keeping this plugin's lifecycle hooks active:
|
|
134
|
+
|
|
135
|
+
```json
|
|
136
|
+
{
|
|
137
|
+
"mcp": { "enabled": false }
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Repository context, automatic recall, message capture, and lifecycle commits remain enabled. This
|
|
142
|
+
does not add or overwrite OpenCode's `mcp.openviking` entry.
|
|
143
|
+
|
|
125
144
|
## Verify
|
|
126
145
|
|
|
127
146
|
Restart OpenCode after changing plugin or OpenViking configuration.
|
|
128
147
|
|
|
129
148
|
In a new OpenCode session, ask the agent to browse OpenViking memory or search for a known indexed resource. The plugin should expose the OpenViking MCP server, with tools namespaced by OpenCode as `openviking_*`:
|
|
130
149
|
|
|
131
|
-
- `
|
|
132
|
-
- `openviking_read`, `openviking_list`, `openviking_grep`, `openviking_glob`
|
|
133
|
-
- `openviking_remember`, `
|
|
150
|
+
- `openviking_search`, `openviking_find`
|
|
151
|
+
- `openviking_read`, `openviking_list`, `openviking_tree`, `openviking_grep`, `openviking_glob`
|
|
152
|
+
- `openviking_remember`, `openviking_write`, `openviking_edit`, `openviking_add_resource`
|
|
153
|
+
- `openviking_list_watches`, `openviking_cancel_watch`, `openviking_forget`, `openviking_health`
|
|
134
154
|
- `openviking_list_watches`, `openviking_cancel_watch`
|
|
135
|
-
- `openviking_code_search`, `openviking_code_outline`, `openviking_code_expand`
|
|
136
155
|
|
|
137
156
|
If anything looks wrong, check the runtime files:
|
|
138
157
|
|
|
@@ -151,18 +170,19 @@ curl http://localhost:1933/health
|
|
|
151
170
|
|
|
152
171
|
The plugin registers OpenViking's stdio MCP proxy through OpenCode config. The server's real `tools/list` response is the source of truth; current OpenViking servers expose:
|
|
153
172
|
|
|
154
|
-
- `
|
|
155
|
-
- `openviking_search`: deep semantic retrieval across memories, resources, and skills.
|
|
173
|
+
- `openviking_search`: deep semantic retrieval across memories, resources, and skills; use `mode="context"` for balanced, injection-ready context.
|
|
156
174
|
- `openviking_find`: fast semantic retrieval.
|
|
157
175
|
- `openviking_remember`: store important facts or decisions for memory extraction.
|
|
158
176
|
- `openviking_read`: read one or more `viking://` files.
|
|
159
177
|
- `openviking_list`: list a `viking://` directory.
|
|
178
|
+
- `openviking_tree`: show a `viking://` directory tree.
|
|
160
179
|
- `openviking_grep`: exact text or regex search.
|
|
161
180
|
- `openviking_glob`: glob file matching.
|
|
181
|
+
- `openviking_write`: create, overwrite, or append to a `viking://` file.
|
|
182
|
+
- `openviking_edit`: exact string replacement in a `viking://` file.
|
|
162
183
|
- `openviking_add_resource`: add a URL, local file, sitemap, or feed.
|
|
163
184
|
- `openviking_forget`: delete a `viking://` URI after explicit user confirmation.
|
|
164
185
|
- `openviking_list_watches` / `openviking_cancel_watch`: inspect or cancel resource watches.
|
|
165
|
-
- `openviking_code_search`, `openviking_code_outline`, `openviking_code_expand`: inspect indexed code symbols.
|
|
166
186
|
- `openviking_health`: check OpenViking server health.
|
|
167
187
|
|
|
168
188
|
Usage guidance:
|
package/README.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
A unified OpenCode plugin for OpenViking repository retrieval and long-term memory.
|
|
4
4
|
|
|
5
|
+
> **Requires an OpenViking server with `viking://~` home-alias support.** Recall targets the
|
|
6
|
+
> caller's own context space through `viking://~/memories` and `viking://~/skills`; the uid-less
|
|
7
|
+
> `viking://user/memories` shorthand is rejected by newer servers.
|
|
8
|
+
|
|
5
9
|
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
10
|
|
|
7
11
|
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.
|
|
@@ -114,6 +118,7 @@ Create `~/.config/opencode/openviking-config.json`:
|
|
|
114
118
|
```json
|
|
115
119
|
{
|
|
116
120
|
"enabled": true,
|
|
121
|
+
"mcp": { "enabled": true },
|
|
117
122
|
"timeoutMs": 30000,
|
|
118
123
|
"repoContext": { "enabled": true, "cacheTtlMs": 60000 },
|
|
119
124
|
"autoRecall": {
|
|
@@ -132,17 +137,15 @@ Create `~/.config/opencode/openviking-config.json`:
|
|
|
132
137
|
}
|
|
133
138
|
```
|
|
134
139
|
|
|
135
|
-
|
|
140
|
+
`autoRecall.limit` is a legacy quota-scaling input, not a final result cap.
|
|
141
|
+
Explicit values from 1 through 5 produce an effective total quota of 6 because
|
|
142
|
+
each coding category keeps one retrieval slot. Use Context `quotas` directly
|
|
143
|
+
when exact category ceilings are required.
|
|
144
|
+
|
|
145
|
+
API keys are resolved from environment variables or `~/.openviking/ovcli.conf` and sent as `Authorization: Bearer ...` by both hooks and the MCP proxy. Recall goes through the server-side context face (`POST /api/v1/search/search` with `mode="context"`), falling back to the deprecated `/api/v1/search/recall` on older deployments. `account` and `user` are trusted-mode identity
|
|
136
146
|
headers sent as `X-OpenViking-Account` and `X-OpenViking-User`; leave them empty
|
|
137
147
|
when using API-key mode with user/admin API keys.
|
|
138
|
-
By default the plugin derives a peer from the project directory
|
|
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.
|
|
148
|
+
By default the plugin derives a peer from the git identity of the project directory: the normalized `origin` URL, else the repository root path. Outside a git repository no peer is sent at all, and what is remembered there goes to the user-level space `viking://user/<you>/memories`. `git@github.com:volcengine/OpenViking.git` becomes `github.com-volcengine-openviking`; the path fallback keeps the older naming rule where every non-letter-or-digit character becomes `-`, so `/Users/x/Dev/OpenViking` becomes `-Users-x-Dev-OpenViking`. One repository therefore keeps one peer across subdirectories, worktrees, clones and machines, while a fork's different origin keeps it separate. Derivation reads `.git` directly, so no `git` binary is needed. The plugin does not read workspace `.openviking/config.json` files, so a `peer.id` written there has no effect. Data-plane memory/resource requests send the effective peer as `X-OpenViking-Actor-Peer`; captured session messages store it as body `peer_id`. Configure `peerId` or `OPENVIKING_PEER_ID` to override the derived peer, or set `workspacePeer=false` / `OPENVIKING_WORKSPACE_PEER=0` to send no peer at all. Memories written under the older directory-derived peer stay reachable: the default broad recall sweeps every peer under the user, and `recallPeerScope="actor"` asks that previous peer separately.
|
|
146
149
|
|
|
147
150
|
Recall defaults to the broad mode: global memory, the current workspace, and
|
|
148
151
|
other workspace memories can all be recalled, with other workspaces penalized
|
|
@@ -154,10 +157,28 @@ with an explicit actor peer so one person's memories are not recalled into
|
|
|
154
157
|
another person's session.
|
|
155
158
|
|
|
156
159
|
`OPENVIKING_API_KEY`, `OPENVIKING_ACCOUNT`, `OPENVIKING_USER`,
|
|
157
|
-
and `OPENVIKING_PEER_ID` take precedence over values in this file.
|
|
160
|
+
and `OPENVIKING_PEER_ID` take precedence over values in this file. The config
|
|
161
|
+
file's `peerId` still applies whenever shared credentials (ovcli.conf or
|
|
162
|
+
environment variables) do not carry a peer of their own, so an authenticated
|
|
163
|
+
setup keeps writing peer-scoped data instead of dropping into the shared user
|
|
164
|
+
tree.
|
|
158
165
|
|
|
159
166
|
For advanced setups, `OPENVIKING_PLUGIN_CONFIG` can point to another config file path.
|
|
160
167
|
|
|
168
|
+
### Hook-only mode
|
|
169
|
+
|
|
170
|
+
When OpenViking is already exposed through another MCP server, retain the lifecycle hooks while
|
|
171
|
+
skipping this plugin's bundled MCP registration:
|
|
172
|
+
|
|
173
|
+
```json
|
|
174
|
+
{
|
|
175
|
+
"mcp": { "enabled": false }
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
This leaves repository context, automatic recall, message capture, and lifecycle commits enabled.
|
|
180
|
+
It does not add or overwrite OpenCode's `mcp.openviking` entry.
|
|
181
|
+
|
|
161
182
|
OpenCode's local `read`, `glob`, and `grep` tools cannot read `viking://` URIs.
|
|
162
183
|
When the agent accidentally tries that, the plugin blocks the filesystem tool
|
|
163
184
|
call and points it to the OpenViking MCP tools.
|
|
@@ -166,18 +187,19 @@ call and points it to the OpenViking MCP tools.
|
|
|
166
187
|
|
|
167
188
|
OpenCode sees the OpenViking MCP server as `openviking`, so tool names are namespaced with `openviking_`.
|
|
168
189
|
|
|
169
|
-
- `
|
|
170
|
-
- `openviking_search`: deep semantic retrieval across memories, resources, and skills.
|
|
190
|
+
- `openviking_search`: deep semantic retrieval across memories, resources, and skills; use `mode="context"` for balanced, injection-ready context.
|
|
171
191
|
- `openviking_find`: fast semantic retrieval.
|
|
172
192
|
- `openviking_remember`: store important facts or decisions for memory extraction.
|
|
173
193
|
- `openviking_read`: read one or more `viking://` files.
|
|
174
194
|
- `openviking_list`: list a `viking://` directory.
|
|
195
|
+
- `openviking_tree`: show a `viking://` directory tree.
|
|
175
196
|
- `openviking_grep`: exact text or regex search.
|
|
176
197
|
- `openviking_glob`: glob file matching.
|
|
198
|
+
- `openviking_write`: create, overwrite, or append to a `viking://` file.
|
|
199
|
+
- `openviking_edit`: exact string replacement in a `viking://` file.
|
|
177
200
|
- `openviking_add_resource`: add a URL, local file, sitemap, or feed.
|
|
178
201
|
- `openviking_forget`: delete a `viking://` URI after explicit user confirmation.
|
|
179
202
|
- `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
203
|
- `openviking_health`: check OpenViking server health.
|
|
182
204
|
|
|
183
205
|
The proxy forwards the server's real `tools/list` response; the plugin does not maintain a separate native tool list.
|
|
@@ -190,3 +212,20 @@ The plugin writes runtime files to `~/.config/opencode/openviking/` by default:
|
|
|
190
212
|
- `openviking-session-state.json`
|
|
191
213
|
|
|
192
214
|
Set `runtime.dataDir` in config to override this directory.
|
|
215
|
+
|
|
216
|
+
## Automated npm releases
|
|
217
|
+
|
|
218
|
+
Changes merged into upstream `main` under `examples/opencode-plugin/` trigger
|
|
219
|
+
`.github/workflows/opencode-plugin-release.yml`. Releases use the same calendar
|
|
220
|
+
version convention as the OpenClaw plugin: `YYYY.M.D`, then `YYYY.M.D-N` for
|
|
221
|
+
subsequent releases on that date (Asia/Shanghai). No manual source version bump
|
|
222
|
+
is required. The workflow changes the version only in the package being published;
|
|
223
|
+
it does not commit generated version changes back to the repository.
|
|
224
|
+
|
|
225
|
+
Releases are serialized. A queued run checks out current `main`, so several quick
|
|
226
|
+
merges may be included in one package. The npm manifest records
|
|
227
|
+
`openvikingSourceCommit`; rerunning a successfully published commit skips publication.
|
|
228
|
+
Registry failures stop the workflow instead of treating an unavailable registry as
|
|
229
|
+
an unused version. Only upstream `main` can publish the official npm `latest` tag.
|
|
230
|
+
Manual dispatch on `main` retries a failed release using the existing npm credentials
|
|
231
|
+
or Trusted Publishing configuration.
|
package/index.mjs
CHANGED
|
@@ -24,7 +24,7 @@ export async function OpenVikingPlugin({ client, directory }) {
|
|
|
24
24
|
|
|
25
25
|
const repoContext = createRepoContext({ config })
|
|
26
26
|
const sessionManager = createMemorySessionManager({ config, pluginRoot: dataDir })
|
|
27
|
-
const recall = createMemoryRecall({ config })
|
|
27
|
+
const recall = createMemoryRecall({ config, sessionManager })
|
|
28
28
|
const sessionInject = createSessionInject({ config, sessionManager })
|
|
29
29
|
const vikingUriGuard = createVikingUriGuard()
|
|
30
30
|
|
|
@@ -44,8 +44,15 @@ export async function OpenVikingPlugin({ client, directory }) {
|
|
|
44
44
|
|
|
45
45
|
return {
|
|
46
46
|
config: async (opencodeConfig) => {
|
|
47
|
-
const injected = injectOpenVikingMcpConfig(opencodeConfig, pluginRoot)
|
|
48
|
-
|
|
47
|
+
const injected = injectOpenVikingMcpConfig(opencodeConfig, pluginRoot, config.mcp.enabled)
|
|
48
|
+
const hookOnly = !config.mcp.enabled
|
|
49
|
+
log(
|
|
50
|
+
injected || hookOnly ? "INFO" : "WARN",
|
|
51
|
+
"mcp",
|
|
52
|
+
injected ? "Registered OpenViking MCP server" :
|
|
53
|
+
hookOnly ? "Skipped bundled MCP registration in hook-only mode" :
|
|
54
|
+
"OpenViking MCP server was not registered",
|
|
55
|
+
)
|
|
49
56
|
},
|
|
50
57
|
|
|
51
58
|
event: async ({ event }) => {
|
package/lib/config.mjs
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import fs from "fs"
|
|
2
2
|
import path from "path"
|
|
3
3
|
import { homedir } from "os"
|
|
4
|
-
import { resolveOpenVikingCredentials } from "./shared/credentials.mjs"
|
|
4
|
+
import { buildUserAgent, readManifestVersion, resolveOpenVikingCredentials } from "./shared/credentials.mjs"
|
|
5
5
|
import { resolveEffectivePeerId } from "./shared/workspace-peer.mjs"
|
|
6
6
|
|
|
7
|
+
const USER_AGENT = buildUserAgent(
|
|
8
|
+
"opencode",
|
|
9
|
+
readManifestVersion(new URL("../package.json", import.meta.url)),
|
|
10
|
+
)
|
|
11
|
+
|
|
7
12
|
const DEFAULT_CONFIG = {
|
|
8
13
|
endpoint: "http://127.0.0.1:1933",
|
|
9
14
|
apiKey: "",
|
|
@@ -12,7 +17,13 @@ const DEFAULT_CONFIG = {
|
|
|
12
17
|
peerId: "",
|
|
13
18
|
workspacePeer: true,
|
|
14
19
|
recallPeerScope: "all",
|
|
20
|
+
// Server-side query expansion costs a model call before retrieval starts, so
|
|
21
|
+
// it has to be switchable from the client that pays the latency.
|
|
22
|
+
recallQueryExpansion: "auto",
|
|
15
23
|
enabled: true,
|
|
24
|
+
mcp: {
|
|
25
|
+
enabled: true,
|
|
26
|
+
},
|
|
16
27
|
timeoutMs: 30000,
|
|
17
28
|
runtime: {
|
|
18
29
|
dataDir: "",
|
|
@@ -23,7 +34,7 @@ const DEFAULT_CONFIG = {
|
|
|
23
34
|
},
|
|
24
35
|
autoRecall: {
|
|
25
36
|
enabled: true,
|
|
26
|
-
limit:
|
|
37
|
+
limit: 10,
|
|
27
38
|
scoreThreshold: 0.35,
|
|
28
39
|
maxContentChars: 500,
|
|
29
40
|
preferAbstract: true,
|
|
@@ -34,7 +45,7 @@ const DEFAULT_CONFIG = {
|
|
|
34
45
|
captureMode: "semantic",
|
|
35
46
|
captureMaxLength: 24000,
|
|
36
47
|
captureAssistantTurns: true,
|
|
37
|
-
captureToolMaxChars:
|
|
48
|
+
captureToolMaxChars: 1000000,
|
|
38
49
|
commitTokenThreshold: 20000,
|
|
39
50
|
commitKeepRecentCount: 10,
|
|
40
51
|
profileTokenBudget: 10000,
|
|
@@ -112,6 +123,12 @@ function applyLegacyConnection(config, fileConfig) {
|
|
|
112
123
|
|
|
113
124
|
function applyBehaviorConfig(config, fileConfig = {}) {
|
|
114
125
|
if (fileConfig.enabled !== undefined) config.enabled = fileConfig.enabled !== false
|
|
126
|
+
const mcp = fileConfig.mcp && typeof fileConfig.mcp === "object" ? fileConfig.mcp : {}
|
|
127
|
+
config.mcp = {
|
|
128
|
+
...DEFAULT_CONFIG.mcp,
|
|
129
|
+
...mcp,
|
|
130
|
+
enabled: mcp.enabled !== false,
|
|
131
|
+
}
|
|
115
132
|
if (fileConfig.timeoutMs !== undefined) config.timeoutMs = fileConfig.timeoutMs
|
|
116
133
|
config.runtime = {
|
|
117
134
|
...DEFAULT_CONFIG.runtime,
|
|
@@ -123,6 +140,8 @@ function applyBehaviorConfig(config, fileConfig = {}) {
|
|
|
123
140
|
}
|
|
124
141
|
|
|
125
142
|
const autoRecall = fileConfig.autoRecall ?? {}
|
|
143
|
+
config.recallLimitConfigured = autoRecall.limit !== undefined ||
|
|
144
|
+
fileConfig.recallLimit !== undefined
|
|
126
145
|
config.autoRecall = {
|
|
127
146
|
...DEFAULT_CONFIG.autoRecall,
|
|
128
147
|
...autoRecall,
|
|
@@ -152,9 +171,11 @@ function applyBehaviorConfig(config, fileConfig = {}) {
|
|
|
152
171
|
"debugLogPath",
|
|
153
172
|
"workspacePeer",
|
|
154
173
|
"recallPeerScope",
|
|
174
|
+
"recallQueryExpansion",
|
|
155
175
|
]) {
|
|
156
176
|
if (fileConfig[key] !== undefined) config[key] = fileConfig[key]
|
|
157
177
|
}
|
|
178
|
+
config.recallQueryExpansionConfigured = fileConfig.recallQueryExpansion !== undefined
|
|
158
179
|
}
|
|
159
180
|
|
|
160
181
|
function applyEnv(config) {
|
|
@@ -162,7 +183,10 @@ function applyEnv(config) {
|
|
|
162
183
|
if (process.env.OPENVIKING_AUTO_RECALL !== undefined) {
|
|
163
184
|
config.autoRecall.enabled = envBool("OPENVIKING_AUTO_RECALL") ?? config.autoRecall.enabled
|
|
164
185
|
}
|
|
165
|
-
if (process.env.OPENVIKING_RECALL_LIMIT)
|
|
186
|
+
if (process.env.OPENVIKING_RECALL_LIMIT) {
|
|
187
|
+
config.autoRecall.limit = process.env.OPENVIKING_RECALL_LIMIT
|
|
188
|
+
config.recallLimitConfigured = true
|
|
189
|
+
}
|
|
166
190
|
if (process.env.OPENVIKING_SCORE_THRESHOLD) config.autoRecall.scoreThreshold = process.env.OPENVIKING_SCORE_THRESHOLD
|
|
167
191
|
if (process.env.OPENVIKING_RECALL_MAX_CONTENT_CHARS) {
|
|
168
192
|
config.autoRecall.maxContentChars = process.env.OPENVIKING_RECALL_MAX_CONTENT_CHARS
|
|
@@ -172,6 +196,10 @@ function applyEnv(config) {
|
|
|
172
196
|
config.autoRecall.preferAbstract = envBool("OPENVIKING_RECALL_PREFER_ABSTRACT") ?? config.autoRecall.preferAbstract
|
|
173
197
|
}
|
|
174
198
|
if (process.env.OPENVIKING_RECALL_PEER_SCOPE) config.recallPeerScope = process.env.OPENVIKING_RECALL_PEER_SCOPE
|
|
199
|
+
if (process.env.OPENVIKING_RECALL_QUERY_EXPANSION) {
|
|
200
|
+
config.recallQueryExpansion = process.env.OPENVIKING_RECALL_QUERY_EXPANSION
|
|
201
|
+
config.recallQueryExpansionConfigured = true
|
|
202
|
+
}
|
|
175
203
|
if (process.env.OPENVIKING_WORKSPACE_PEER !== undefined) {
|
|
176
204
|
config.workspacePeer = envBool("OPENVIKING_WORKSPACE_PEER") ?? config.workspacePeer
|
|
177
205
|
}
|
|
@@ -216,6 +244,8 @@ function normalizeConfig(config) {
|
|
|
216
244
|
config.baseUrl = config.endpoint
|
|
217
245
|
config.accountId = config.account
|
|
218
246
|
config.userId = config.user
|
|
247
|
+
config.userAgent = USER_AGENT
|
|
248
|
+
config.harness = "opencode"
|
|
219
249
|
config.timeoutMs = normalizeNumber(config.timeoutMs, DEFAULT_CONFIG.timeoutMs, 1000, 300000)
|
|
220
250
|
config.repoContext.cacheTtlMs = normalizeNumber(
|
|
221
251
|
config.repoContext.cacheTtlMs,
|
|
@@ -223,17 +253,25 @@ function normalizeConfig(config) {
|
|
|
223
253
|
1000,
|
|
224
254
|
60 * 60 * 1000,
|
|
225
255
|
)
|
|
226
|
-
config.autoRecall.limit = Math.max(1, Math.min(50, Math.round(Number(config.autoRecall.limit) ||
|
|
256
|
+
config.autoRecall.limit = Math.max(1, Math.min(50, Math.round(Number(config.autoRecall.limit) || 10)))
|
|
227
257
|
config.autoRecall.scoreThreshold = Math.max(0, Math.min(1, Number(config.autoRecall.scoreThreshold) || 0))
|
|
228
258
|
config.autoRecall.maxContentChars = Math.max(100, Math.min(5000, Math.round(Number(config.autoRecall.maxContentChars) || 500)))
|
|
229
259
|
config.autoRecall.tokenBudget = Math.max(200, Math.min(50000, Math.round(Number(config.autoRecall.tokenBudget) || 2000)))
|
|
230
260
|
config.autoRecall.minQueryLength = Math.max(1, Math.min(64, Math.round(Number(config.autoRecall.minQueryLength) || 3)))
|
|
231
261
|
config.captureMode = config.captureMode === "keyword" ? "keyword" : "semantic"
|
|
232
262
|
config.recallPeerScope = config.recallPeerScope === "actor" ? "actor" : "all"
|
|
263
|
+
config.recallQueryExpansion = config.recallQueryExpansion === "off" ? "off" : "auto"
|
|
233
264
|
config.captureMaxLength = Math.max(200, Math.min(100000, Math.round(Number(config.captureMaxLength) || 24000)))
|
|
234
|
-
config.captureToolMaxChars = Math.max(200, Math.min(
|
|
265
|
+
config.captureToolMaxChars = Math.max(200, Math.min(1000000, Math.round(Number(config.captureToolMaxChars) || 1000000)))
|
|
235
266
|
config.commitTokenThreshold = Math.max(1000, Math.round(Number(config.commitTokenThreshold) || 20000))
|
|
236
|
-
|
|
267
|
+
const rawCommitKeepRecentCount = config.commitKeepRecentCount
|
|
268
|
+
const commitKeepRecentCount = rawCommitKeepRecentCount == null ||
|
|
269
|
+
(typeof rawCommitKeepRecentCount === "string" && rawCommitKeepRecentCount.trim() === "")
|
|
270
|
+
? Number.NaN
|
|
271
|
+
: Number(rawCommitKeepRecentCount)
|
|
272
|
+
config.commitKeepRecentCount = Number.isFinite(commitKeepRecentCount)
|
|
273
|
+
? Math.max(0, Math.round(commitKeepRecentCount))
|
|
274
|
+
: DEFAULT_CONFIG.commitKeepRecentCount
|
|
237
275
|
config.profileTokenBudget = Math.max(500, Math.round(Number(config.profileTokenBudget) || 10000))
|
|
238
276
|
config.resumeContextBudget = Math.max(1024, Math.round(Number(config.resumeContextBudget) || 32000))
|
|
239
277
|
if (!Array.isArray(config.bypassSessionPatterns)) config.bypassSessionPatterns = []
|
|
@@ -257,7 +295,11 @@ export function loadConfig(pluginRoot, projectDirectory) {
|
|
|
257
295
|
config.apiKey = creds.apiKey
|
|
258
296
|
config.account = creds.account
|
|
259
297
|
config.user = creds.user
|
|
260
|
-
|
|
298
|
+
// Shared credentials only carry a peer when ovcli.conf sets actor_peer_id
|
|
299
|
+
// (or OPENVIKING_PEER_ID is exported); without one the project config's
|
|
300
|
+
// peerId still applies so authenticated setups keep writing peer-scoped
|
|
301
|
+
// data instead of silently dropping into the shared user tree (#4487).
|
|
302
|
+
config.peerId = creds.peerId || str(fileConfig.peerId, "")
|
|
261
303
|
config.mcpUrl = creds.mcpUrl
|
|
262
304
|
config.credentialSource = creds.credentialSource
|
|
263
305
|
config.credentialPath = creds.cliPath || creds.ovPath || ""
|
package/lib/mcp-config.mjs
CHANGED
|
@@ -11,8 +11,9 @@ export function createOpenVikingMcpConfig(pluginRoot) {
|
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
export function injectOpenVikingMcpConfig(config, pluginRoot) {
|
|
14
|
+
export function injectOpenVikingMcpConfig(config, pluginRoot, enabled = true) {
|
|
15
15
|
if (!config || typeof config !== "object") return false
|
|
16
|
+
if (!enabled) return false
|
|
16
17
|
config.mcp = config.mcp && typeof config.mcp === "object" ? config.mcp : {}
|
|
17
18
|
const current = config.mcp[OPENCODE_MCP_NAME]
|
|
18
19
|
if (current?.enabled === false) return false
|
package/lib/memory-recall.mjs
CHANGED
|
@@ -2,14 +2,15 @@ import { buildRecallBlock } from "./shared/recall-core.mjs"
|
|
|
2
2
|
import { isBypassed } from "./shared/session-model.mjs"
|
|
3
3
|
import { effectivePeerId, fetchJSON, log } from "./utils.mjs"
|
|
4
4
|
|
|
5
|
-
export function createMemoryRecall({ config }) {
|
|
5
|
+
export function createMemoryRecall({ config, sessionManager }) {
|
|
6
6
|
async function injectRelevantMemories(input, output) {
|
|
7
7
|
if (!config.autoRecall?.enabled) return
|
|
8
8
|
const query = extractCurrentUserText(output.parts ?? [])
|
|
9
9
|
if (!query) return
|
|
10
10
|
if (query.length < config.minQueryLength) return
|
|
11
|
+
const sessionID = input.sessionID ?? output.message?.sessionID
|
|
11
12
|
if (isBypassed(config, {
|
|
12
|
-
sessionId:
|
|
13
|
+
sessionId: sessionID,
|
|
13
14
|
cwd: input.directory ?? input.cwd,
|
|
14
15
|
})) return
|
|
15
16
|
|
|
@@ -17,11 +18,19 @@ export function createMemoryRecall({ config }) {
|
|
|
17
18
|
if (!health.ok) return
|
|
18
19
|
|
|
19
20
|
const block = await buildRecallBlock(
|
|
20
|
-
|
|
21
|
+
// 5s is this hook's own budget for a bare retrieval; when the request
|
|
22
|
+
// also spends a server fuse the helper hands down a longer deadline, and
|
|
23
|
+
// overriding it here would abort a request the server was still inside.
|
|
24
|
+
(path, init = {}, options = {}) =>
|
|
25
|
+
fetchJSON(config, path, init, { ...options, timeoutMs: options.timeoutMs ?? 5000 }),
|
|
21
26
|
config,
|
|
22
27
|
query,
|
|
23
28
|
{
|
|
24
29
|
actorPeerId: effectivePeerId(config),
|
|
30
|
+
legacyPeerId: config.effectivePeer?.legacyPeerId ?? "",
|
|
31
|
+
// The mapped OV session is what turns on server-side query expansion
|
|
32
|
+
// and the cross-turn dedup ledger.
|
|
33
|
+
sessionId: sessionID ? sessionManager.getMappedSessionId(sessionID) : "",
|
|
25
34
|
log: (stage, data) => log("DEBUG", "recall", stage, data),
|
|
26
35
|
},
|
|
27
36
|
)
|
|
@@ -35,12 +44,13 @@ export function createMemoryRecall({ config }) {
|
|
|
35
44
|
return { injectRelevantMemories }
|
|
36
45
|
}
|
|
37
46
|
|
|
38
|
-
function extractCurrentUserText(parts) {
|
|
47
|
+
export function extractCurrentUserText(parts) {
|
|
39
48
|
const texts = []
|
|
40
49
|
for (const part of parts) {
|
|
41
50
|
if (part.type !== "text" || typeof part.text !== "string") continue
|
|
51
|
+
if (part.synthetic || part.ignored) continue
|
|
42
52
|
if (part.text.includes("<openviking-context")) return null
|
|
43
|
-
|
|
53
|
+
texts.push(part.text)
|
|
44
54
|
}
|
|
45
55
|
const joined = texts.join(" ").trim()
|
|
46
56
|
return joined || null
|
|
@@ -48,8 +58,10 @@ function extractCurrentUserText(parts) {
|
|
|
48
58
|
|
|
49
59
|
function prependSyntheticRecallPart(input, output, injection) {
|
|
50
60
|
const sessionID = input.sessionID ?? output.message?.sessionID
|
|
51
|
-
|
|
52
|
-
|
|
61
|
+
// messageID is optional in opencode's chat.message API (messageID?: string).
|
|
62
|
+
// When absent, generate a fallback so recall context is always injected.
|
|
63
|
+
const messageID = input.messageID ?? output.message?.id ?? `ov-recall-${Date.now()}`
|
|
64
|
+
if (!sessionID) return false
|
|
53
65
|
|
|
54
66
|
output.parts.unshift({
|
|
55
67
|
id: `prt-ov-recall-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|