@openviking/opencode-plugin 0.2.0 → 0.2.2
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 +17 -3
- package/lib/config.mjs +13 -1
- package/lib/shared/mcp-proxy-core.mjs +11 -4
- package/lib/shared/recall-core.mjs +25 -5
- package/lib/shared/workspace-peer.mjs +16 -0
- package/lib/utils.mjs +1 -1
- package/package.json +1 -1
- package/servers/mcp-proxy.mjs +3 -1
package/README.md
CHANGED
|
@@ -135,9 +135,23 @@ Create `~/.config/opencode/openviking-config.json`:
|
|
|
135
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
|
|
136
136
|
headers sent as `X-OpenViking-Account` and `X-OpenViking-User`; leave them empty
|
|
137
137
|
when using API-key mode with user/admin API keys.
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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.
|
|
141
155
|
|
|
142
156
|
`OPENVIKING_API_KEY`, `OPENVIKING_ACCOUNT`, `OPENVIKING_USER`,
|
|
143
157
|
and `OPENVIKING_PEER_ID` take precedence over values in this file.
|
package/lib/config.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import fs from "fs"
|
|
|
2
2
|
import path from "path"
|
|
3
3
|
import { homedir } from "os"
|
|
4
4
|
import { resolveOpenVikingCredentials } from "./shared/credentials.mjs"
|
|
5
|
+
import { resolveEffectivePeerId } from "./shared/workspace-peer.mjs"
|
|
5
6
|
|
|
6
7
|
const DEFAULT_CONFIG = {
|
|
7
8
|
endpoint: "http://127.0.0.1:1933",
|
|
@@ -9,6 +10,8 @@ const DEFAULT_CONFIG = {
|
|
|
9
10
|
account: "",
|
|
10
11
|
user: "",
|
|
11
12
|
peerId: "",
|
|
13
|
+
workspacePeer: true,
|
|
14
|
+
recallPeerScope: "all",
|
|
12
15
|
enabled: true,
|
|
13
16
|
timeoutMs: 30000,
|
|
14
17
|
runtime: {
|
|
@@ -147,6 +150,8 @@ function applyBehaviorConfig(config, fileConfig = {}) {
|
|
|
147
150
|
"bypassSessionPatterns",
|
|
148
151
|
"debug",
|
|
149
152
|
"debugLogPath",
|
|
153
|
+
"workspacePeer",
|
|
154
|
+
"recallPeerScope",
|
|
150
155
|
]) {
|
|
151
156
|
if (fileConfig[key] !== undefined) config[key] = fileConfig[key]
|
|
152
157
|
}
|
|
@@ -166,6 +171,10 @@ function applyEnv(config) {
|
|
|
166
171
|
if (process.env.OPENVIKING_RECALL_PREFER_ABSTRACT !== undefined) {
|
|
167
172
|
config.autoRecall.preferAbstract = envBool("OPENVIKING_RECALL_PREFER_ABSTRACT") ?? config.autoRecall.preferAbstract
|
|
168
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
|
+
}
|
|
169
178
|
if (process.env.OPENVIKING_MIN_QUERY_LENGTH) config.autoRecall.minQueryLength = process.env.OPENVIKING_MIN_QUERY_LENGTH
|
|
170
179
|
if (process.env.OPENVIKING_AUTO_CAPTURE !== undefined) {
|
|
171
180
|
config.autoCapture = envBool("OPENVIKING_AUTO_CAPTURE") ?? config.autoCapture
|
|
@@ -220,6 +229,7 @@ function normalizeConfig(config) {
|
|
|
220
229
|
config.autoRecall.tokenBudget = Math.max(200, Math.min(50000, Math.round(Number(config.autoRecall.tokenBudget) || 2000)))
|
|
221
230
|
config.autoRecall.minQueryLength = Math.max(1, Math.min(64, Math.round(Number(config.autoRecall.minQueryLength) || 3)))
|
|
222
231
|
config.captureMode = config.captureMode === "keyword" ? "keyword" : "semantic"
|
|
232
|
+
config.recallPeerScope = config.recallPeerScope === "actor" ? "actor" : "all"
|
|
223
233
|
config.captureMaxLength = Math.max(200, Math.min(100000, Math.round(Number(config.captureMaxLength) || 24000)))
|
|
224
234
|
config.captureToolMaxChars = Math.max(200, Math.min(20000, Math.round(Number(config.captureToolMaxChars) || 2000)))
|
|
225
235
|
config.commitTokenThreshold = Math.max(1000, Math.round(Number(config.commitTokenThreshold) || 20000))
|
|
@@ -262,7 +272,9 @@ export function loadConfig(pluginRoot, projectDirectory) {
|
|
|
262
272
|
applyEnv(config)
|
|
263
273
|
config.configPath = configPath
|
|
264
274
|
config.legacyCredentialsUsed = legacyUsed && creds.credentialSource !== "env"
|
|
265
|
-
|
|
275
|
+
const normalized = normalizeConfig(config)
|
|
276
|
+
normalized.effectivePeer = resolveEffectivePeerId({ cfg: normalized, cwd: projectDirectory })
|
|
277
|
+
return normalized
|
|
266
278
|
}
|
|
267
279
|
|
|
268
280
|
export function resolveDataDir(pluginRoot, config) {
|
|
@@ -201,11 +201,14 @@ export function createOpenVikingMcpProxy({
|
|
|
201
201
|
return true;
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
-
function headersForRequest(includeSession = true
|
|
204
|
+
function headersForRequest(includeSession = true) {
|
|
205
205
|
const headers = {
|
|
206
206
|
"Content-Type": "application/json",
|
|
207
207
|
"Accept": "application/json, text/event-stream",
|
|
208
|
-
|
|
208
|
+
// Always the proxy's current version (default, then server-negotiated) —
|
|
209
|
+
// never the client's un-negotiated ask, which strict upstreams reject
|
|
210
|
+
// with HTTP 400 before initialize negotiation can run.
|
|
211
|
+
"MCP-Protocol-Version": protocolVersion,
|
|
209
212
|
};
|
|
210
213
|
if (includeSession && sessionId) headers["Mcp-Session-Id"] = sessionId;
|
|
211
214
|
if (proxyConfig.apiKey) headers.Authorization = `Bearer ${proxyConfig.apiKey}`;
|
|
@@ -265,7 +268,7 @@ export function createOpenVikingMcpProxy({
|
|
|
265
268
|
try {
|
|
266
269
|
const res = await fetchImpl(proxyConfig.mcpUrl, {
|
|
267
270
|
method: "POST",
|
|
268
|
-
headers: headersForRequest(includeSession
|
|
271
|
+
headers: headersForRequest(includeSession),
|
|
269
272
|
body: JSON.stringify(message),
|
|
270
273
|
signal: controller.signal,
|
|
271
274
|
});
|
|
@@ -273,6 +276,10 @@ export function createOpenVikingMcpProxy({
|
|
|
273
276
|
const messages = parseHttpBody(res.headers.get("content-type"), text);
|
|
274
277
|
const nextSessionId = res.headers.get("mcp-session-id");
|
|
275
278
|
if (nextSessionId) sessionId = nextSessionId;
|
|
279
|
+
if (message?.method === "initialize" && res.ok) {
|
|
280
|
+
const negotiated = messages.find((m) => typeof m?.result?.protocolVersion === "string")?.result.protocolVersion;
|
|
281
|
+
if (negotiated) protocolVersion = negotiated;
|
|
282
|
+
}
|
|
276
283
|
if (!res.ok) {
|
|
277
284
|
throw new HttpStatusError(res.status, res.statusText, text, messages);
|
|
278
285
|
}
|
|
@@ -366,7 +373,7 @@ export function createOpenVikingMcpProxy({
|
|
|
366
373
|
const expectsResponse = isRequest(message);
|
|
367
374
|
if (message.method === "initialize") {
|
|
368
375
|
initializeRequest = cloneMessage(message);
|
|
369
|
-
protocolVersion =
|
|
376
|
+
protocolVersion = DEFAULT_PROTOCOL_VERSION;
|
|
370
377
|
sessionId = "";
|
|
371
378
|
}
|
|
372
379
|
if (message.method === "notifications/initialized") {
|
|
@@ -20,7 +20,7 @@ export function estimateTokens(text) {
|
|
|
20
20
|
|
|
21
21
|
export function buildRecallEndpointBody(cfg = {}) {
|
|
22
22
|
const limit = Math.max(Number(cfg.recallLimit || 0), 1);
|
|
23
|
-
|
|
23
|
+
const body = {
|
|
24
24
|
query: "",
|
|
25
25
|
quotas: {
|
|
26
26
|
events: limit,
|
|
@@ -32,6 +32,8 @@ export function buildRecallEndpointBody(cfg = {}) {
|
|
|
32
32
|
min_score: Number.isFinite(Number(cfg.scoreThreshold)) ? Number(cfg.scoreThreshold) : 0.35,
|
|
33
33
|
render: true,
|
|
34
34
|
};
|
|
35
|
+
if (cfg.recallPeerScope === "actor") body.peer_scope = "actor";
|
|
36
|
+
return body;
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
function clampScore(v) {
|
|
@@ -233,10 +235,7 @@ async function buildFallbackInjectionBlock(fetchJSON, items, cfg, actorPeerId =
|
|
|
233
235
|
async function recallViaEndpoint(fetchJSON, cfg, query, actorPeerId = "", log = () => {}) {
|
|
234
236
|
const body = buildRecallEndpointBody(cfg);
|
|
235
237
|
body.query = query;
|
|
236
|
-
const res = await fetchJSON
|
|
237
|
-
method: "POST",
|
|
238
|
-
body: JSON.stringify(body),
|
|
239
|
-
}, { actorPeerId });
|
|
238
|
+
const res = await postRecall(fetchJSON, body, { actorPeerId, log });
|
|
240
239
|
if (!res.ok) {
|
|
241
240
|
log("recall_endpoint_fallback", { status: res.status || 0 });
|
|
242
241
|
return null;
|
|
@@ -251,6 +250,27 @@ async function recallViaEndpoint(fetchJSON, cfg, query, actorPeerId = "", log =
|
|
|
251
250
|
].join("\n");
|
|
252
251
|
}
|
|
253
252
|
|
|
253
|
+
export async function postRecall(fetchJSON, body, opts = {}) {
|
|
254
|
+
const actorPeerId = opts.actorPeerId || "";
|
|
255
|
+
const log = opts.log || (() => {});
|
|
256
|
+
const request = { ...body };
|
|
257
|
+
const res = await fetchJSON("/api/v1/search/recall", {
|
|
258
|
+
method: "POST",
|
|
259
|
+
body: JSON.stringify(request),
|
|
260
|
+
}, { actorPeerId });
|
|
261
|
+
if (!request.peer_scope || (res.status !== 400 && res.status !== 422)) {
|
|
262
|
+
return res;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const downgraded = { ...request };
|
|
266
|
+
delete downgraded.peer_scope;
|
|
267
|
+
log("recall_peer_scope_downgrade", { status: res.status || 0 });
|
|
268
|
+
return fetchJSON("/api/v1/search/recall", {
|
|
269
|
+
method: "POST",
|
|
270
|
+
body: JSON.stringify(downgraded),
|
|
271
|
+
}, { actorPeerId });
|
|
272
|
+
}
|
|
273
|
+
|
|
254
274
|
export async function buildRecallBlock(fetchJSON, cfg, query, options = {}) {
|
|
255
275
|
const actorPeerId = options.actorPeerId ?? cfg.peerId ?? "";
|
|
256
276
|
const log = options.log || (() => {});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// GENERATED FROM examples/memory-plugin-shared/lib. DO NOT EDIT.
|
|
2
|
+
export function deriveWorkspacePeerId(cwd) {
|
|
3
|
+
return String(cwd || "").replace(/[^A-Za-z0-9]/g, "-");
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function resolveEffectivePeerId({ cfg = {}, cwd = "" } = {}) {
|
|
7
|
+
const explicit = String(cfg.peerId || "").trim();
|
|
8
|
+
if (explicit) return { peerId: explicit, source: "explicit" };
|
|
9
|
+
|
|
10
|
+
if (cfg.workspacePeer !== false) {
|
|
11
|
+
const peerId = deriveWorkspacePeerId(cwd);
|
|
12
|
+
if (peerId) return { peerId, source: "workspace" };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return { peerId: "", source: "none" };
|
|
16
|
+
}
|
package/lib/utils.mjs
CHANGED
|
@@ -65,7 +65,7 @@ export function normalizeEndpoint(endpoint) {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
export function effectivePeerId(config) {
|
|
68
|
-
return String(config.peerId || "").trim() || null
|
|
68
|
+
return String(config.effectivePeer?.peerId || config.peerId || "").trim() || null
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
export async function fetchJSON(config, endpoint, init = {}, options = {}) {
|
package/package.json
CHANGED
package/servers/mcp-proxy.mjs
CHANGED
|
@@ -14,6 +14,7 @@ import { fileURLToPath } from "node:url"
|
|
|
14
14
|
import { loadConfig } from "../lib/config.mjs"
|
|
15
15
|
import { createLogger } from "../lib/shared/debug-log.mjs"
|
|
16
16
|
import { createOpenVikingMcpProxy } from "../lib/shared/mcp-proxy-core.mjs"
|
|
17
|
+
import { resolveEffectivePeerId } from "../lib/shared/workspace-peer.mjs"
|
|
17
18
|
|
|
18
19
|
export { createOpenVikingMcpProxy } from "../lib/shared/mcp-proxy-core.mjs"
|
|
19
20
|
|
|
@@ -37,13 +38,14 @@ function uniq(values) {
|
|
|
37
38
|
|
|
38
39
|
function readProxyConfig() {
|
|
39
40
|
const cfg = loadConfig(resolvePath(fileURLToPath(import.meta.url), "..", ".."))
|
|
41
|
+
const effectivePeer = resolveEffectivePeerId({ cfg, cwd: process.cwd() })
|
|
40
42
|
const mcpUrl = cfg.mcpUrl || `${trimSlash(cfg.endpoint)}/mcp`
|
|
41
43
|
return {
|
|
42
44
|
mcpUrl,
|
|
43
45
|
apiKey: cfg.apiKey || "",
|
|
44
46
|
account: cfg.account || "",
|
|
45
47
|
user: cfg.user || "",
|
|
46
|
-
peerId:
|
|
48
|
+
peerId: effectivePeer.peerId,
|
|
47
49
|
timeoutMs: Math.max(1000, Number(cfg.timeoutMs) || DEFAULT_TIMEOUT_MS),
|
|
48
50
|
debug: cfg.debug === true,
|
|
49
51
|
debugLogPath: cfg.debugLogPath,
|