@openviking/dsh-memory-plugin 0.3.2 → 0.4.4
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 +11 -6
- package/capture.mjs +14 -0
- package/client.mjs +6 -155
- package/config.mjs +36 -170
- package/index.mjs +2 -1
- package/mcp-env.mjs +33 -0
- package/mcp.mjs +2 -33
- package/package.json +4 -2
- package/runtime.mjs +10 -22
- package/servers/mcp-proxy.mjs +11 -21
- package/shared/capture-utils.mjs +77 -1
- package/shared/config-schema.mjs +359 -0
- package/shared/credentials.mjs +238 -102
- package/shared/mcp-proxy-config.mjs +154 -2
- package/shared/mcp-proxy-core.mjs +23 -15
- package/shared/ov-http.mjs +112 -0
- package/shared/plugin-config.mjs +306 -0
- package/shared/recall-core.mjs +190 -96
- package/shared/uri-guard.mjs +173 -1
- package/shared/workspace-config.mjs +433 -0
- package/shared/workspace-peer.mjs +63 -0
- package/shared/workspace-registry.mjs +87 -0
- package/skills.mjs +2 -0
- package/uri-guard.mjs +20 -10
package/README.md
CHANGED
|
@@ -29,6 +29,8 @@ be a real package. Linking a source checkout (`dsh plugin --profile web add
|
|
|
29
29
|
./examples/dsh-memory-plugin`) only works when that checkout has its own
|
|
30
30
|
`node_modules`, because Node resolves the bundle's dsh peers from the source
|
|
31
31
|
tree's realpath rather than from the profile.
|
|
32
|
+
It also needs `node examples/memory-plugin-shared/sync.mjs` run first: the
|
|
33
|
+
`shared/` modules the bundle imports are generated, not committed.
|
|
32
34
|
|
|
33
35
|
## Requirements
|
|
34
36
|
|
|
@@ -122,11 +124,13 @@ SDK client opens that standalone stream it stops resolving POST responses, so
|
|
|
122
124
|
`tools/list` never returns. The proxy owns the transport itself and is
|
|
123
125
|
unaffected.
|
|
124
126
|
|
|
125
|
-
The bundle's resolved
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
127
|
+
The bundle's resolved connection — url, MCP url, key, account, user and auth
|
|
128
|
+
mode — travels to the proxy through the child environment, because DSH scrubs
|
|
129
|
+
credential-shaped names out of the inherited env and a subprocess cannot see the
|
|
130
|
+
Cordis patch. It goes with `OPENVIKING_CREDENTIAL_SOURCE=env`, so the proxy reads
|
|
131
|
+
no config file and reaches the server exactly as the runtime does, including
|
|
132
|
+
with no key when the runtime has none. A changed `ovcli.conf` therefore takes
|
|
133
|
+
effect for the tools when DSH restarts the bundle, as it does for the runtime.
|
|
130
134
|
|
|
131
135
|
Two consequences follow from the proxy being one process per profile:
|
|
132
136
|
|
|
@@ -199,7 +203,8 @@ The patch can also carry plugin config:
|
|
|
199
203
|
- `skipSubagentSessions: true` excludes sessions marked with `header.origin: subagent` from automatic profile, recall, capture, and commit; it defaults to `false`.
|
|
200
204
|
- `syncTurns: false` stops every new write: no captured messages, no threshold or shutdown commit. Writes queued while the toggle was on are still replayed by the background drainer once the server recovers — they were captured with the toggle on. Profile injection and recall are unaffected; it defaults to `true`.
|
|
201
205
|
- Failed writes enter the shared OpenViking pending queue. A background drainer (default every 60s, `OPENVIKING_PENDING_DRAIN_INTERVAL_MS`) probes the server health and replays the queue in-process, so a transient write failure recovers without restarting dsh; it does not consume the session-start retry budget. Session-start replays keep consuming retries as before.
|
|
202
|
-
- `tools/pre-execute`
|
|
206
|
+
- `tools/pre-execute` denies a DSH filesystem tool (`read`, `glob`, `grep`, `edit`, `write`, `str_replace_editor`) whose path argument is a `viking://` URI, pointing the model at the bridged `mcp__openviking__*` tools instead. A `grep` whose pattern is `viking://` text still runs.
|
|
207
|
+
- `tools/post-execute` lets a `bash` command that carries a `viking://` URI run unchanged and attaches a notice for the model: use the bridged tools if it meant OpenViking content, or ignore the notice when the URI is intentional data such as an `ov` argument or an HTTP payload.
|
|
203
208
|
|
|
204
209
|
Each DSH session maps to `dsh-<session-id>` in OpenViking. Workspace-derived actor peers are resolved per session and sent on every session-specific request: the peer is the git identity of the session's workspace — the normalized `origin` URL (`git@github.com:volcengine/OpenViking.git` becomes `github.com-volcengine-openviking`), else the repository root path, that fallback keeping the older rule where every non-letter-or-digit character becomes `-`. 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`. One repository therefore keeps one peer across subdirectories, worktrees, clones and machines, while a fork's different origin keeps it separate. DSH does not read workspace `.openviking/config.json` files, so a `peer.id` written there has no effect; pin a peer with `OPENVIKING_PEER_ID` instead. Memories written under the older path-derived peer stay reachable: the default `recallPeerScope: all` sweeps every peer under the user.
|
|
205
210
|
|
package/capture.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
1
2
|
import {
|
|
2
3
|
extractPartsFromPayload,
|
|
3
4
|
extractTextFromPayload,
|
|
@@ -6,6 +7,19 @@ import {
|
|
|
6
7
|
|
|
7
8
|
export const OPENVIKING_PLUGIN_SOURCE = "openviking-memory";
|
|
8
9
|
|
|
10
|
+
export function pluginMessage(content, source) {
|
|
11
|
+
// dsh's own constructor: identity, normalization, and any future Message
|
|
12
|
+
// invariants come from the pinned peer instead of a hand-built object.
|
|
13
|
+
return createUserMessage({
|
|
14
|
+
content: [{ type: "text", text: content }],
|
|
15
|
+
source: {
|
|
16
|
+
kind: "plugin",
|
|
17
|
+
plugin: OPENVIKING_PLUGIN_SOURCE,
|
|
18
|
+
...source,
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
9
23
|
export function captureEvent(event, config, toolNames = new Map()) {
|
|
10
24
|
if (!event || typeof event !== "object") return null;
|
|
11
25
|
if (event.type === "tool/call") {
|
package/client.mjs
CHANGED
|
@@ -1,67 +1,13 @@
|
|
|
1
|
+
import { createOvHttp } from "./shared/ov-http.mjs";
|
|
2
|
+
|
|
1
3
|
export class OpenVikingClient {
|
|
2
4
|
constructor(config) {
|
|
3
5
|
this.config = config;
|
|
4
6
|
this.connected = false;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
if (this.config.apiKey) headers.Authorization = `Bearer ${this.config.apiKey}`;
|
|
10
|
-
if (this.config.account) headers["X-OpenViking-Account"] = this.config.account;
|
|
11
|
-
if (this.config.user) headers["X-OpenViking-User"] = this.config.user;
|
|
12
|
-
const actorPeerId = options.actorPeerId ?? this.config.peerId;
|
|
13
|
-
if (actorPeerId) headers["X-OpenViking-Actor-Peer"] = actorPeerId;
|
|
14
|
-
if (this.config.userAgent) headers["User-Agent"] = this.config.userAgent;
|
|
15
|
-
return headers;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
async fetchJSON(path, init = {}, options = {}) {
|
|
19
|
-
const timeoutMs = options.timeoutMs ?? this.config.requestTimeoutMs;
|
|
20
|
-
const controller = new AbortController();
|
|
21
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
22
|
-
try {
|
|
23
|
-
const response = await fetch(`${this.config.endpoint}${path}`, {
|
|
24
|
-
...init,
|
|
25
|
-
headers: {
|
|
26
|
-
...this.headers(options),
|
|
27
|
-
...(init.headers || {}),
|
|
28
|
-
},
|
|
29
|
-
signal: controller.signal,
|
|
30
|
-
});
|
|
31
|
-
const body = await response.json().catch(() => ({}));
|
|
32
|
-
const traceId = body?.result?.trace_id
|
|
33
|
-
|| body?.error?.trace_id
|
|
34
|
-
|| body?.trace_id
|
|
35
|
-
|| undefined;
|
|
36
|
-
if (!response.ok || body?.status === "error") {
|
|
37
|
-
return {
|
|
38
|
-
ok: false,
|
|
39
|
-
result: null,
|
|
40
|
-
status: response.status,
|
|
41
|
-
error: body?.error || { message: `HTTP ${response.status}` },
|
|
42
|
-
traceId,
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
return {
|
|
46
|
-
ok: true,
|
|
47
|
-
result: body?.result ?? body,
|
|
48
|
-
status: response.status,
|
|
49
|
-
traceId,
|
|
50
|
-
};
|
|
51
|
-
} catch (error) {
|
|
52
|
-
return {
|
|
53
|
-
ok: false,
|
|
54
|
-
result: null,
|
|
55
|
-
status: 0,
|
|
56
|
-
error: { message: error instanceof Error ? error.message : String(error) },
|
|
57
|
-
};
|
|
58
|
-
} finally {
|
|
59
|
-
clearTimeout(timer);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async health() {
|
|
64
|
-
return (await this.healthResult()).ok;
|
|
7
|
+
this.fetchJSON = createOvHttp(config, {
|
|
8
|
+
defaultTimeoutMs: config.requestTimeoutMs,
|
|
9
|
+
resolveActorPeerId: () => this.config.peerId,
|
|
10
|
+
});
|
|
65
11
|
}
|
|
66
12
|
|
|
67
13
|
async healthResult() {
|
|
@@ -70,12 +16,6 @@ export class OpenVikingClient {
|
|
|
70
16
|
return response;
|
|
71
17
|
}
|
|
72
18
|
|
|
73
|
-
async ensureSession(sessionId, actorPeerId) {
|
|
74
|
-
const response = await this.ensureSessionResult(sessionId, actorPeerId);
|
|
75
|
-
return response.ok
|
|
76
|
-
|| (response.status === 409 && response.error?.code === "ALREADY_EXISTS");
|
|
77
|
-
}
|
|
78
|
-
|
|
79
19
|
async ensureSessionResult(sessionId, actorPeerId) {
|
|
80
20
|
const response = await this.fetchJSON("/api/v1/sessions", {
|
|
81
21
|
method: "POST",
|
|
@@ -93,15 +33,6 @@ export class OpenVikingClient {
|
|
|
93
33
|
return response.ok ? response.result : null;
|
|
94
34
|
}
|
|
95
35
|
|
|
96
|
-
async getSessionArchive(sessionId, archiveId, actorPeerId) {
|
|
97
|
-
const response = await this.fetchJSON(
|
|
98
|
-
`/api/v1/sessions/${encodeURIComponent(sessionId)}/archives/${encodeURIComponent(archiveId)}`,
|
|
99
|
-
{},
|
|
100
|
-
{ actorPeerId },
|
|
101
|
-
);
|
|
102
|
-
return response.ok ? response.result : null;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
36
|
async addMessage(sessionId, payload, actorPeerId) {
|
|
106
37
|
return this.fetchJSON(
|
|
107
38
|
`/api/v1/sessions/${encodeURIComponent(sessionId)}/messages`,
|
|
@@ -123,84 +54,4 @@ export class OpenVikingClient {
|
|
|
123
54
|
{ timeoutMs: options.timeoutMs ?? 30000, actorPeerId },
|
|
124
55
|
);
|
|
125
56
|
}
|
|
126
|
-
|
|
127
|
-
async find(query, options = {}) {
|
|
128
|
-
const body = { query };
|
|
129
|
-
if (options.targetUri) body.target_uri = options.targetUri;
|
|
130
|
-
if (options.limit) body.limit = options.limit;
|
|
131
|
-
if (options.scoreThreshold !== undefined) body.score_threshold = options.scoreThreshold;
|
|
132
|
-
const response = await this.fetchJSON("/api/v1/search/find", {
|
|
133
|
-
method: "POST",
|
|
134
|
-
body: JSON.stringify(body),
|
|
135
|
-
}, { actorPeerId: options.actorPeerId });
|
|
136
|
-
if (!response.ok || !response.result) return [];
|
|
137
|
-
|
|
138
|
-
const results = [];
|
|
139
|
-
for (const bucket of ["memories", "resources", "skills"]) {
|
|
140
|
-
const entries = response.result[bucket];
|
|
141
|
-
if (!Array.isArray(entries)) continue;
|
|
142
|
-
for (const entry of entries) {
|
|
143
|
-
results.push({
|
|
144
|
-
uri: entry?.uri || "",
|
|
145
|
-
contextType: entry?.context_type
|
|
146
|
-
|| (bucket === "memories" ? "memory" : bucket === "skills" ? "skill" : "resource"),
|
|
147
|
-
score: Number(entry?.score || 0),
|
|
148
|
-
abstract: entry?.abstract || "",
|
|
149
|
-
overview: entry?.overview || null,
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
return results;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
async read(uri, level, actorPeerId) {
|
|
157
|
-
const endpoint = level === "abstract"
|
|
158
|
-
? "abstract"
|
|
159
|
-
: level === "overview"
|
|
160
|
-
? "overview"
|
|
161
|
-
: "read";
|
|
162
|
-
const response = await this.fetchJSON(
|
|
163
|
-
`/api/v1/content/${endpoint}?uri=${encodeURIComponent(uri)}`,
|
|
164
|
-
{},
|
|
165
|
-
{ actorPeerId },
|
|
166
|
-
);
|
|
167
|
-
return response.ok ? response.result : null;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
async list(uri, actorPeerId) {
|
|
171
|
-
const response = await this.fetchJSON(
|
|
172
|
-
`/api/v1/fs/ls?uri=${encodeURIComponent(uri)}&output=original`,
|
|
173
|
-
{},
|
|
174
|
-
{ actorPeerId },
|
|
175
|
-
);
|
|
176
|
-
return response.ok && Array.isArray(response.result) ? response.result : [];
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
async stat(uri, actorPeerId) {
|
|
180
|
-
const response = await this.fetchJSON(
|
|
181
|
-
`/api/v1/fs/stat?uri=${encodeURIComponent(uri)}`,
|
|
182
|
-
{},
|
|
183
|
-
{ actorPeerId },
|
|
184
|
-
);
|
|
185
|
-
return response.ok ? response.result : null;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
async forget(uri, recursive = false, actorPeerId) {
|
|
189
|
-
const response = await this.fetchJSON(
|
|
190
|
-
`/api/v1/fs?uri=${encodeURIComponent(uri)}&recursive=${recursive}`,
|
|
191
|
-
{ method: "DELETE" },
|
|
192
|
-
{ actorPeerId },
|
|
193
|
-
);
|
|
194
|
-
return response.ok;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
async addResource(path, reason, actorPeerId) {
|
|
198
|
-
const body = { path };
|
|
199
|
-
if (reason) body.reason = reason;
|
|
200
|
-
const response = await this.fetchJSON("/api/v1/resources", {
|
|
201
|
-
method: "POST",
|
|
202
|
-
body: JSON.stringify(body),
|
|
203
|
-
}, { timeoutMs: 30000, actorPeerId });
|
|
204
|
-
return response.ok ? response.result : null;
|
|
205
|
-
}
|
|
206
57
|
}
|
package/config.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { buildPluginConfig } from "./shared/plugin-config.mjs";
|
|
2
|
+
import { loadCredentialFiles } from "./shared/credentials.mjs";
|
|
3
3
|
|
|
4
|
-
export const PLUGIN_VERSION = "0.
|
|
4
|
+
export const PLUGIN_VERSION = "0.4.4";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Namespace for the bridged OpenViking MCP tools. DSH publishes every MCP tool
|
|
@@ -10,172 +10,38 @@ export const PLUGIN_VERSION = "0.3.2";
|
|
|
10
10
|
*/
|
|
11
11
|
export const MCP_SERVER_NAME = "openviking";
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
recallQueryExpansionConfigured: false,
|
|
25
|
-
syncTurns: true,
|
|
26
|
-
recallTokenBudget: 2000,
|
|
27
|
-
recallMaxContentChars: 500,
|
|
28
|
-
recallPreferAbstract: true,
|
|
29
|
-
recallLimit: 10,
|
|
30
|
-
recallLimitConfigured: false,
|
|
31
|
-
scoreThreshold: 0.35,
|
|
32
|
-
minQueryLength: 3,
|
|
33
|
-
profileTokenBudget: 10000,
|
|
34
|
-
commitTokenThreshold: 20000,
|
|
35
|
-
commitKeepRecentCount: 10,
|
|
36
|
-
captureToolResults: false,
|
|
37
|
-
captureMode: "semantic",
|
|
38
|
-
captureMaxLength: 24000,
|
|
39
|
-
captureToolMaxChars: 1000000,
|
|
40
|
-
captureAssistantTurns: true,
|
|
41
|
-
skipSubagentSessions: false,
|
|
42
|
-
requestTimeoutMs: 10000,
|
|
43
|
-
mcpToolCallTimeoutMs: 60000,
|
|
44
|
-
});
|
|
45
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Resolve the plugin's configuration.
|
|
15
|
+
*
|
|
16
|
+
* `input` is what the cordis host hands the plugin — this harness has no config
|
|
17
|
+
* file of its own. Every knob is declared in `shared/config-schema.mjs`, and
|
|
18
|
+
* `ovcli.conf`'s `plugin` section outranks the host's input so one file
|
|
19
|
+
* configures every harness and `ov config switch` moves them together.
|
|
20
|
+
* Connection fields are the exception: an endpoint, key, identity or auth mode
|
|
21
|
+
* named by the host is the more specific answer and stays ahead of the
|
|
22
|
+
* credential chain.
|
|
23
|
+
*/
|
|
46
24
|
export function resolveConfig(input = {}, env = process.env, cwd = process.cwd()) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
if (env.OPENVIKING_RECALL_PEER_SCOPE) {
|
|
71
|
-
config.recallPeerScope = env.OPENVIKING_RECALL_PEER_SCOPE;
|
|
72
|
-
}
|
|
73
|
-
if (env.OPENVIKING_RECALL_QUERY_EXPANSION) {
|
|
74
|
-
config.recallQueryExpansion = env.OPENVIKING_RECALL_QUERY_EXPANSION;
|
|
75
|
-
config.recallQueryExpansionConfigured = true;
|
|
76
|
-
}
|
|
77
|
-
if (env.OPENVIKING_RECALL_LIMIT) {
|
|
78
|
-
config.recallLimit = env.OPENVIKING_RECALL_LIMIT;
|
|
79
|
-
config.recallLimitConfigured = true;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
config.endpoint = String(config.endpoint || DEFAULT_CONFIG.endpoint).replace(/\/+$/, "");
|
|
83
|
-
config.workspacePeer = config.workspacePeer !== false;
|
|
84
|
-
const effectivePeer = resolveEffectivePeerId({ cfg: config, cwd });
|
|
85
|
-
config.peerId = effectivePeer.peerId;
|
|
86
|
-
config.legacyPeerId = effectivePeer.legacyPeerId;
|
|
87
|
-
config.recallPeerScope = config.recallPeerScope === "actor" ? "actor" : "all";
|
|
88
|
-
config.recallQueryExpansion = config.recallQueryExpansion === "off" ? "off" : "auto";
|
|
89
|
-
config.recallLimit = clampInteger(config.recallLimit, 1, 50, DEFAULT_CONFIG.recallLimit);
|
|
90
|
-
config.recallMaxContentChars = clampInteger(
|
|
91
|
-
config.recallMaxContentChars,
|
|
92
|
-
100,
|
|
93
|
-
5000,
|
|
94
|
-
DEFAULT_CONFIG.recallMaxContentChars,
|
|
95
|
-
);
|
|
96
|
-
config.recallTokenBudget = clampInteger(
|
|
97
|
-
config.recallTokenBudget,
|
|
98
|
-
200,
|
|
99
|
-
50000,
|
|
100
|
-
DEFAULT_CONFIG.recallTokenBudget,
|
|
101
|
-
);
|
|
102
|
-
config.scoreThreshold = clampNumber(
|
|
103
|
-
config.scoreThreshold,
|
|
104
|
-
0,
|
|
105
|
-
1,
|
|
106
|
-
DEFAULT_CONFIG.scoreThreshold,
|
|
107
|
-
);
|
|
108
|
-
config.minQueryLength = clampInteger(
|
|
109
|
-
config.minQueryLength,
|
|
110
|
-
1,
|
|
111
|
-
64,
|
|
112
|
-
DEFAULT_CONFIG.minQueryLength,
|
|
113
|
-
);
|
|
114
|
-
config.profileTokenBudget = clampInteger(
|
|
115
|
-
config.profileTokenBudget,
|
|
116
|
-
500,
|
|
117
|
-
50000,
|
|
118
|
-
DEFAULT_CONFIG.profileTokenBudget,
|
|
119
|
-
);
|
|
120
|
-
config.commitTokenThreshold = clampInteger(
|
|
121
|
-
config.commitTokenThreshold,
|
|
122
|
-
1000,
|
|
123
|
-
1000000,
|
|
124
|
-
DEFAULT_CONFIG.commitTokenThreshold,
|
|
125
|
-
);
|
|
126
|
-
config.commitKeepRecentCount = clampInteger(
|
|
127
|
-
config.commitKeepRecentCount,
|
|
128
|
-
0,
|
|
129
|
-
1000,
|
|
130
|
-
DEFAULT_CONFIG.commitKeepRecentCount,
|
|
131
|
-
);
|
|
132
|
-
config.captureMaxLength = clampInteger(
|
|
133
|
-
config.captureMaxLength,
|
|
134
|
-
200,
|
|
135
|
-
100000,
|
|
136
|
-
DEFAULT_CONFIG.captureMaxLength,
|
|
137
|
-
);
|
|
138
|
-
config.captureToolMaxChars = clampInteger(
|
|
139
|
-
config.captureToolMaxChars,
|
|
140
|
-
200,
|
|
141
|
-
1000000,
|
|
142
|
-
DEFAULT_CONFIG.captureToolMaxChars,
|
|
143
|
-
);
|
|
144
|
-
config.requestTimeoutMs = clampInteger(
|
|
145
|
-
config.requestTimeoutMs,
|
|
146
|
-
1000,
|
|
147
|
-
120000,
|
|
148
|
-
DEFAULT_CONFIG.requestTimeoutMs,
|
|
149
|
-
);
|
|
150
|
-
config.mcpToolCallTimeoutMs = clampInteger(
|
|
151
|
-
config.mcpToolCallTimeoutMs,
|
|
152
|
-
1000,
|
|
153
|
-
600000,
|
|
154
|
-
DEFAULT_CONFIG.mcpToolCallTimeoutMs,
|
|
155
|
-
);
|
|
156
|
-
config.captureMode = config.captureMode === "keyword" ? "keyword" : "semantic";
|
|
157
|
-
config.syncTurns = config.syncTurns !== false;
|
|
158
|
-
config.captureAssistantTurns = config.captureAssistantTurns !== false;
|
|
159
|
-
config.captureToolResults = config.captureToolResults === true;
|
|
160
|
-
config.skipSubagentSessions = config.skipSubagentSessions === true;
|
|
161
|
-
return config;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function environmentBoolean(value, fallback) {
|
|
165
|
-
const normalized = String(value || "").trim().toLowerCase();
|
|
166
|
-
if (["0", "false", "no", "off"].includes(normalized)) return false;
|
|
167
|
-
if (["1", "true", "yes", "on"].includes(normalized)) return true;
|
|
168
|
-
return fallback;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function clampInteger(value, minimum, maximum, fallback) {
|
|
172
|
-
const number = Math.round(Number(value));
|
|
173
|
-
if (!Number.isFinite(number)) return fallback;
|
|
174
|
-
return Math.max(minimum, Math.min(maximum, number));
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function clampNumber(value, minimum, maximum, fallback) {
|
|
178
|
-
const number = Number(value);
|
|
179
|
-
if (!Number.isFinite(number)) return fallback;
|
|
180
|
-
return Math.max(minimum, Math.min(maximum, number));
|
|
25
|
+
// ov.conf's `dsh` section is the legacy layer here as everywhere else, but
|
|
26
|
+
// the cordis patch shares that slot; the host named this process's settings,
|
|
27
|
+
// so it wins the overlap.
|
|
28
|
+
const ovSection = loadCredentialFiles(env).ovFile.dsh;
|
|
29
|
+
const config = buildPluginConfig("dsh", {
|
|
30
|
+
env,
|
|
31
|
+
cwd,
|
|
32
|
+
legacy: { ...(ovSection && typeof ovSection === "object" ? ovSection : {}), ...input },
|
|
33
|
+
version: PLUGIN_VERSION,
|
|
34
|
+
hostInput: {
|
|
35
|
+
peerId: input.peerId,
|
|
36
|
+
account: input.account,
|
|
37
|
+
user: input.user,
|
|
38
|
+
apiKey: input.apiKey,
|
|
39
|
+
baseUrl: input.endpoint,
|
|
40
|
+
// The legacy knob layer this used to ride accepted both spellings.
|
|
41
|
+
authMode: input.authMode || input.auth_mode,
|
|
42
|
+
},
|
|
43
|
+
deriveEffectivePeer: true,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return { ...config, peerId: config.effectivePeer.peerId };
|
|
181
47
|
}
|
package/index.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { injectStartupProfile } from "./lifecycle.mjs";
|
|
|
4
4
|
import { mountOpenVikingMcp } from "./mcp.mjs";
|
|
5
5
|
import { OpenVikingRuntime } from "./runtime.mjs";
|
|
6
6
|
import { mountOpenVikingSkills } from "./skills.mjs";
|
|
7
|
-
import { guardVikingUri } from "./uri-guard.mjs";
|
|
7
|
+
import { guardVikingUri, noticeVikingUri } from "./uri-guard.mjs";
|
|
8
8
|
|
|
9
9
|
export const name = "openviking-memory";
|
|
10
10
|
export const inject = ["agents", "sessions", "tools"];
|
|
@@ -71,6 +71,7 @@ export function apply(ctx, input = {}) {
|
|
|
71
71
|
});
|
|
72
72
|
|
|
73
73
|
ctx.on("tools/pre-execute", guardVikingUri);
|
|
74
|
+
ctx.on("tools/post-execute", noticeVikingUri);
|
|
74
75
|
|
|
75
76
|
// Mounted last, and deliberately not awaited: the bridge's apply blocks on
|
|
76
77
|
// its first tools/list, so a server that accepts the connection but never
|
package/mcp-env.mjs
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
|
|
3
|
+
import { MCP_SERVER_NAME } from "./config.mjs";
|
|
4
|
+
import { forwardConnectionEnv } from "./shared/mcp-proxy-config.mjs";
|
|
5
|
+
|
|
6
|
+
/** The same stdio proxy every other OpenViking memory integration starts. */
|
|
7
|
+
export const PROXY_PATH = fileURLToPath(new URL("./servers/mcp-proxy.mjs", import.meta.url));
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Build the dsh-mcp-client config for the OpenViking stdio proxy.
|
|
11
|
+
*
|
|
12
|
+
* DSH scrubs credential-shaped names out of the environment a subprocess
|
|
13
|
+
* inherits, and the Cordis patch is invisible to one, so the proxy cannot
|
|
14
|
+
* resolve the connection the way this process did. It gets the answer instead:
|
|
15
|
+
* the resolved connection and this session's peer, forwarded with the forced
|
|
16
|
+
* `env` source so the child reads no file and lands on exactly this url, key,
|
|
17
|
+
* identity, auth mode and peer — the empty ones included.
|
|
18
|
+
*/
|
|
19
|
+
export function buildMcpConfig(config) {
|
|
20
|
+
// In DSH Desktop, process.execPath is Electron's executable rather than a
|
|
21
|
+
// standalone Node binary. This tells Electron to run the proxy script as
|
|
22
|
+
// Node instead of attempting to launch a second Desktop instance.
|
|
23
|
+
const env = { ELECTRON_RUN_AS_NODE: "1", ...forwardConnectionEnv(config) };
|
|
24
|
+
if (config.timeoutMs) env.OPENVIKING_TIMEOUT_MS = String(config.timeoutMs);
|
|
25
|
+
return {
|
|
26
|
+
transport: "stdio",
|
|
27
|
+
serverName: MCP_SERVER_NAME,
|
|
28
|
+
command: process.execPath,
|
|
29
|
+
args: [PROXY_PATH],
|
|
30
|
+
env,
|
|
31
|
+
toolCallTimeoutMs: config.mcpToolCallTimeoutMs,
|
|
32
|
+
};
|
|
33
|
+
}
|
package/mcp.mjs
CHANGED
|
@@ -1,39 +1,8 @@
|
|
|
1
|
-
import { fileURLToPath } from "node:url";
|
|
2
1
|
import * as mcpClient from "@deepseek-ai/dsh-mcp-client";
|
|
3
2
|
|
|
4
|
-
import {
|
|
3
|
+
import { buildMcpConfig } from "./mcp-env.mjs";
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
export const PROXY_PATH = fileURLToPath(new URL("./servers/mcp-proxy.mjs", import.meta.url));
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Build the dsh-mcp-client config for the OpenViking stdio proxy.
|
|
11
|
-
*
|
|
12
|
-
* The bundle's own credential resolution (`OPENVIKING_*` → `ovcli.conf` →
|
|
13
|
-
* `ov.conf`, plus anything set in the Cordis patch) is forwarded through the
|
|
14
|
-
* child environment: DSH scrubs credential-shaped names out of the inherited
|
|
15
|
-
* env, and the patch is invisible to a subprocess, so values the runtime
|
|
16
|
-
* already resolved have to be passed explicitly.
|
|
17
|
-
*/
|
|
18
|
-
export function buildMcpConfig(config) {
|
|
19
|
-
// In DSH Desktop, process.execPath is Electron's executable rather than a
|
|
20
|
-
// standalone Node binary. This tells Electron to run the proxy script as
|
|
21
|
-
// Node instead of attempting to launch a second Desktop instance.
|
|
22
|
-
const env = { ELECTRON_RUN_AS_NODE: "1" };
|
|
23
|
-
if (config.endpoint) env.OPENVIKING_URL = config.endpoint;
|
|
24
|
-
if (config.apiKey) env.OPENVIKING_API_KEY = config.apiKey;
|
|
25
|
-
if (config.account) env.OPENVIKING_ACCOUNT = config.account;
|
|
26
|
-
if (config.user) env.OPENVIKING_USER = config.user;
|
|
27
|
-
if (config.peerId) env.OPENVIKING_PEER_ID = config.peerId;
|
|
28
|
-
return {
|
|
29
|
-
transport: "stdio",
|
|
30
|
-
serverName: MCP_SERVER_NAME,
|
|
31
|
-
command: process.execPath,
|
|
32
|
-
args: [PROXY_PATH],
|
|
33
|
-
env,
|
|
34
|
-
toolCallTimeoutMs: config.mcpToolCallTimeoutMs,
|
|
35
|
-
};
|
|
36
|
-
}
|
|
5
|
+
export { buildMcpConfig, PROXY_PATH } from "./mcp-env.mjs";
|
|
37
6
|
|
|
38
7
|
/**
|
|
39
8
|
* Mount the tool surface. The bridge ships with dsh itself, so it resolves
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openviking/dsh-memory-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"description": "OpenViking memory and context bundle for DeepSeek Harness",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.mjs",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"capture.mjs",
|
|
15
15
|
"lifecycle.mjs",
|
|
16
16
|
"mcp.mjs",
|
|
17
|
+
"mcp-env.mjs",
|
|
17
18
|
"runtime.mjs",
|
|
18
19
|
"skills.mjs",
|
|
19
20
|
"uri-guard.mjs",
|
|
@@ -29,10 +30,11 @@
|
|
|
29
30
|
}
|
|
30
31
|
},
|
|
31
32
|
"scripts": {
|
|
33
|
+
"prepack": "node ../memory-plugin-shared/sync.mjs",
|
|
32
34
|
"check": "for f in *.mjs shared/*.mjs; do node --check \"$f\" || exit 1; done && npm run check:version",
|
|
33
35
|
"check:version": "node --input-type=module -e \"import {readFileSync} from 'node:fs'; const {PLUGIN_VERSION} = await import('./config.mjs'); const v = JSON.parse(readFileSync('package.json','utf8')).version; if (PLUGIN_VERSION !== v) throw new Error('PLUGIN_VERSION ' + PLUGIN_VERSION + ' does not match package.json version ' + v);\"",
|
|
34
36
|
"test": "node --test *.test.mjs",
|
|
35
|
-
"prepublishOnly": "npm run check && npm test"
|
|
37
|
+
"prepublishOnly": "npm run prepack && npm run check && npm test"
|
|
36
38
|
},
|
|
37
39
|
"peerDependencies": {
|
|
38
40
|
"@deepseek-ai/dsh-llm": ">=0.1.0-rc.6 <0.2.0 || ^0.1.5-rc.1",
|