@openviking/dsh-memory-plugin 0.2.0 → 0.3.0
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 +13 -7
- package/config.mjs +3 -1
- package/index.mjs +7 -0
- package/mcp.mjs +4 -1
- package/package.json +4 -4
- package/runtime.mjs +22 -0
- package/shared/credentials.mjs +11 -0
- package/shared/mcp-proxy-config.mjs +24 -0
- package/shared/mcp-proxy-core.mjs +1 -1
- package/shared/recall-core.mjs +8 -4
- package/shared/setup-wizard.mjs +14 -11
- package/shared/uri-guard.mjs +28 -6
package/README.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
An installable DeepSeek Harness bundle that adds OpenViking auto-recall, session capture, `viking://` URI protection, and the OpenViking MCP tool surface.
|
|
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
|
Published as [`@openviking/dsh-memory-plugin`](https://www.npmjs.com/package/@openviking/dsh-memory-plugin).
|
|
6
10
|
|
|
7
11
|
## Install
|
|
@@ -28,19 +32,19 @@ tree's realpath rather than from the profile.
|
|
|
28
32
|
|
|
29
33
|
## Requirements
|
|
30
34
|
|
|
31
|
-
- `@deepseek-ai/dsh` `0.1.0-rc.6`
|
|
35
|
+
- `@deepseek-ai/dsh` `0.1.0-rc.6` or newer in the `0.1.x` release line
|
|
32
36
|
- Node.js `^22.19.0` or `>=24`
|
|
33
37
|
- A reachable OpenViking server
|
|
34
38
|
|
|
35
39
|
The bundle has no runtime npm dependencies. Its message structures come from
|
|
36
40
|
`createUserMessage` (`@deepseek-ai/dsh-llm`), its tool surface from
|
|
37
41
|
`@deepseek-ai/dsh-mcp-client`, and its skill provider from
|
|
38
|
-
`@deepseek-ai/dsh-skill-filesystem` —
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
`overrides`
|
|
43
|
-
|
|
42
|
+
`@deepseek-ai/dsh-skill-filesystem` — peerDependencies that DSH itself already
|
|
43
|
+
installs, so there is nothing extra to add to a profile. The supported peer
|
|
44
|
+
range starts at `0.1.0-rc.6` and stops before `0.2.0`, allowing current DSH
|
|
45
|
+
releases whose core packages resolve to newer release candidates. The local
|
|
46
|
+
devDependencies and `overrides` stay pinned to rc.6 so CI continues to exercise
|
|
47
|
+
the minimum supported contract.
|
|
44
48
|
|
|
45
49
|
Everything under `shared/` and `skills/` is generated by
|
|
46
50
|
`node examples/memory-plugin-shared/sync.mjs` — edit
|
|
@@ -137,6 +141,7 @@ The patch can also carry plugin config:
|
|
|
137
141
|
recallTokenBudget: 2000
|
|
138
142
|
scoreThreshold: 0.35
|
|
139
143
|
captureToolResults: false
|
|
144
|
+
skipSubagentSessions: true
|
|
140
145
|
commitTokenThreshold: 20000
|
|
141
146
|
mcpToolCallTimeoutMs: 60000
|
|
142
147
|
```
|
|
@@ -147,6 +152,7 @@ The patch can also carry plugin config:
|
|
|
147
152
|
- `agent/pre-step` retrieves with the current step input and appends a durable plugin message to that same step.
|
|
148
153
|
- `session/event` captures user, assistant, and optionally tool-result messages without scraping a transcript.
|
|
149
154
|
- `turn/end` checks the OpenViking pending-token threshold and commits when required.
|
|
155
|
+
- `skipSubagentSessions: true` excludes sessions marked with `header.origin: subagent` from automatic profile, recall, capture, and commit; it defaults to `false`.
|
|
150
156
|
- Failed writes enter the shared OpenViking pending queue for replay at the next session start.
|
|
151
157
|
- `tools/pre-execute` blocks DSH filesystem and shell tools from treating `viking://` URIs as local paths, pointing the model at the bridged `mcp__openviking__*` tools instead.
|
|
152
158
|
|
package/config.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { buildUserAgent, resolveOpenVikingCredentials } from "./shared/credentials.mjs";
|
|
2
2
|
import { resolveEffectivePeerId } from "./shared/workspace-peer.mjs";
|
|
3
3
|
|
|
4
|
-
export const PLUGIN_VERSION = "0.
|
|
4
|
+
export const PLUGIN_VERSION = "0.3.0";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Namespace for the bridged OpenViking MCP tools. DSH publishes every MCP tool
|
|
@@ -37,6 +37,7 @@ const DEFAULT_CONFIG = Object.freeze({
|
|
|
37
37
|
captureMaxLength: 24000,
|
|
38
38
|
captureToolMaxChars: 1000000,
|
|
39
39
|
captureAssistantTurns: true,
|
|
40
|
+
skipSubagentSessions: false,
|
|
40
41
|
requestTimeoutMs: 10000,
|
|
41
42
|
mcpToolCallTimeoutMs: 60000,
|
|
42
43
|
});
|
|
@@ -152,6 +153,7 @@ export function resolveConfig(input = {}, env = process.env, cwd = process.cwd()
|
|
|
152
153
|
config.syncTurns = config.syncTurns !== false;
|
|
153
154
|
config.captureAssistantTurns = config.captureAssistantTurns !== false;
|
|
154
155
|
config.captureToolResults = config.captureToolResults === true;
|
|
156
|
+
config.skipSubagentSessions = config.skipSubagentSessions === true;
|
|
155
157
|
return config;
|
|
156
158
|
}
|
|
157
159
|
|
package/index.mjs
CHANGED
|
@@ -13,6 +13,9 @@ export function apply(ctx, input = {}) {
|
|
|
13
13
|
const config = resolveConfig(input);
|
|
14
14
|
const client = new OpenVikingClient(config);
|
|
15
15
|
const runtime = new OpenVikingRuntime(client, config, ctx.logger);
|
|
16
|
+
const skipMemory = session => (
|
|
17
|
+
config.skipSubagentSessions && session?.header?.origin === "subagent"
|
|
18
|
+
);
|
|
16
19
|
ctx.provide("openvikingMemory", runtime);
|
|
17
20
|
ctx.effect(
|
|
18
21
|
() => () => runtime.disposeAll(),
|
|
@@ -20,6 +23,7 @@ export function apply(ctx, input = {}) {
|
|
|
20
23
|
);
|
|
21
24
|
|
|
22
25
|
ctx.on("agent/session-start", ({ agent }) => {
|
|
26
|
+
if (skipMemory(agent.session)) return false;
|
|
23
27
|
agent.ctx.effect(
|
|
24
28
|
() => () => runtime.dispose(agent.session),
|
|
25
29
|
"openvikingMemory.disposeSession()",
|
|
@@ -31,6 +35,7 @@ export function apply(ctx, input = {}) {
|
|
|
31
35
|
// the final claimed batch and appends after every other contributor.
|
|
32
36
|
ctx.on("agent/pre-step", async ({ agent, messages, signal }, next) => {
|
|
33
37
|
const decision = await next();
|
|
38
|
+
if (skipMemory(agent.session)) return decision;
|
|
34
39
|
if (decision.kind !== "enter" || signal.aborted) return decision;
|
|
35
40
|
const profile = await runtime.profileMessage(agent);
|
|
36
41
|
if (signal.aborted) return decision;
|
|
@@ -43,11 +48,13 @@ export function apply(ctx, input = {}) {
|
|
|
43
48
|
}, { prepend: true });
|
|
44
49
|
|
|
45
50
|
ctx.on("session/event", (session, event) => {
|
|
51
|
+
if (skipMemory(session)) return;
|
|
46
52
|
runtime.capture(session, event);
|
|
47
53
|
runtime.maybeCommit(session, event);
|
|
48
54
|
});
|
|
49
55
|
|
|
50
56
|
ctx.on("session/flush", async session => {
|
|
57
|
+
if (skipMemory(session)) return;
|
|
51
58
|
await runtime.flush(session);
|
|
52
59
|
});
|
|
53
60
|
|
package/mcp.mjs
CHANGED
|
@@ -16,7 +16,10 @@ export const PROXY_PATH = fileURLToPath(new URL("./servers/mcp-proxy.mjs", impor
|
|
|
16
16
|
* already resolved have to be passed explicitly.
|
|
17
17
|
*/
|
|
18
18
|
export function buildMcpConfig(config) {
|
|
19
|
-
|
|
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" };
|
|
20
23
|
if (config.endpoint) env.OPENVIKING_URL = config.endpoint;
|
|
21
24
|
if (config.apiKey) env.OPENVIKING_API_KEY = config.apiKey;
|
|
22
25
|
if (config.account) env.OPENVIKING_ACCOUNT = config.account;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openviking/dsh-memory-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "OpenViking memory and context bundle for DeepSeek Harness",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.mjs",
|
|
@@ -35,9 +35,9 @@
|
|
|
35
35
|
"prepublishOnly": "npm run check && npm test"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
|
-
"@deepseek-ai/dsh-llm": "0.1.0-rc.6",
|
|
39
|
-
"@deepseek-ai/dsh-mcp-client": "0.1.0-rc.6",
|
|
40
|
-
"@deepseek-ai/dsh-skill-filesystem": "0.1.0-rc.6"
|
|
38
|
+
"@deepseek-ai/dsh-llm": ">=0.1.0-rc.6 <0.2.0",
|
|
39
|
+
"@deepseek-ai/dsh-mcp-client": ">=0.1.0-rc.6 <0.2.0",
|
|
40
|
+
"@deepseek-ai/dsh-skill-filesystem": ">=0.1.0-rc.6 <0.2.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@deepseek-ai/dsh-llm": "0.1.0-rc.6",
|
package/runtime.mjs
CHANGED
|
@@ -109,6 +109,10 @@ export class OpenVikingRuntime {
|
|
|
109
109
|
async profileMessage(agent) {
|
|
110
110
|
const state = await this.initialize(agent);
|
|
111
111
|
if (!state.ready || !state.profileBlock || state.profileDelivered) return null;
|
|
112
|
+
if (hasStartupProfile(agent)) {
|
|
113
|
+
state.profileDelivered = true;
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
112
116
|
state.profileDelivered = true;
|
|
113
117
|
return pluginMessage(state.profileBlock, "instructions");
|
|
114
118
|
}
|
|
@@ -314,3 +318,21 @@ function pluginMessage(content, form) {
|
|
|
314
318
|
},
|
|
315
319
|
});
|
|
316
320
|
}
|
|
321
|
+
|
|
322
|
+
function hasStartupProfile(agent) {
|
|
323
|
+
const session = agent.session;
|
|
324
|
+
const ownEvents = (session?.events || []).slice(session?.header?.seedLength ?? 0);
|
|
325
|
+
const inHistory = ownEvents.some(event => (
|
|
326
|
+
event?.type === "user/message" && isStartupProfile(event.data)
|
|
327
|
+
));
|
|
328
|
+
if (inHistory) return true;
|
|
329
|
+
return [agent.inbox?.nextTurn, agent.inbox?.nextStep].some(messages => (
|
|
330
|
+
(messages || []).some(isStartupProfile)
|
|
331
|
+
));
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function isStartupProfile(message) {
|
|
335
|
+
return message?.source?.kind === "plugin"
|
|
336
|
+
&& message.source.plugin === OPENVIKING_PLUGIN_SOURCE
|
|
337
|
+
&& message.source.form === "instructions";
|
|
338
|
+
}
|
package/shared/credentials.mjs
CHANGED
|
@@ -192,9 +192,20 @@ export function resolveOpenVikingCredentials(env = process.env) {
|
|
|
192
192
|
const explicitMcpUrl = str(env.OPENVIKING_MCP_URL, "");
|
|
193
193
|
const mcpUrl = (mode !== "cli" && explicitMcpUrl) ? explicitMcpUrl : `${baseUrl.replace(/\/+$/, "")}/mcp`;
|
|
194
194
|
|
|
195
|
+
// The file that actually supplied the api_key, following the same chain —
|
|
196
|
+
// empty when the key came from the environment or was never found.
|
|
197
|
+
let credentialPath = "";
|
|
198
|
+
if (apiKey) {
|
|
199
|
+
if (useCli) credentialPath = files.cliPath;
|
|
200
|
+
else if (str(env.OPENVIKING_BEARER_TOKEN, str(env.OPENVIKING_API_KEY, ""))) credentialPath = "";
|
|
201
|
+
else if (str(files.cliFile.api_key, "")) credentialPath = files.cliPath;
|
|
202
|
+
else credentialPath = files.ovPath;
|
|
203
|
+
}
|
|
204
|
+
|
|
195
205
|
return {
|
|
196
206
|
...files,
|
|
197
207
|
credentialSource: useCli ? "ovcli" : ((mode === "env" || envHasCredentials) ? "env" : "auto"),
|
|
208
|
+
credentialPath,
|
|
198
209
|
baseUrl,
|
|
199
210
|
mcpUrl,
|
|
200
211
|
apiKey,
|
|
@@ -41,6 +41,30 @@ export function defaultCredentialPaths(env = process.env) {
|
|
|
41
41
|
].filter(Boolean);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Resolve the actor peer header for a long-lived MCP proxy process.
|
|
46
|
+
*
|
|
47
|
+
* MCP servers may start in the plugin directory rather than the active
|
|
48
|
+
* workspace, so their process cwd is not a reliable peer identity. Broad
|
|
49
|
+
* recall intentionally spans the authenticated user's peer workspaces and
|
|
50
|
+
* therefore leaves the actor header unset. Actor-scoped recall requires an
|
|
51
|
+
* explicit peer so isolation never depends on the proxy launch directory.
|
|
52
|
+
*/
|
|
53
|
+
export function resolveMcpActorPeerId({
|
|
54
|
+
peerId = "",
|
|
55
|
+
recallPeerScope = "all",
|
|
56
|
+
} = {}) {
|
|
57
|
+
if (recallPeerScope !== "actor") return "";
|
|
58
|
+
|
|
59
|
+
const explicitPeerId = String(peerId || "").trim();
|
|
60
|
+
if (explicitPeerId) return explicitPeerId;
|
|
61
|
+
|
|
62
|
+
throw new Error(
|
|
63
|
+
"OpenViking MCP actor-scoped recall requires an explicit peer ID. "
|
|
64
|
+
+ "Set actor_peer_id in ovcli.conf or OPENVIKING_PEER_ID in the MCP environment.",
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
44
68
|
function uniq(values) {
|
|
45
69
|
return [...new Set(values.filter(Boolean))];
|
|
46
70
|
}
|
|
@@ -295,7 +295,7 @@ export function createOpenVikingMcpProxy({
|
|
|
295
295
|
return errorResponse(
|
|
296
296
|
id,
|
|
297
297
|
-32001,
|
|
298
|
-
`OpenViking MCP request failed. Check the configured URL (${proxyConfig.mcpUrl}) and that
|
|
298
|
+
`OpenViking MCP request failed. Check the configured URL (${proxyConfig.mcpUrl}) and that the OpenViking server (\`openviking-server\`) is reachable.`,
|
|
299
299
|
{ cause: msg },
|
|
300
300
|
);
|
|
301
301
|
}
|
package/shared/recall-core.mjs
CHANGED
|
@@ -14,8 +14,8 @@ const STOPWORDS = new Set([
|
|
|
14
14
|
]);
|
|
15
15
|
const USER_RESERVED_DIRS = new Set(["memories", "skills"]);
|
|
16
16
|
const SOURCES = [
|
|
17
|
-
{ type: "memory", uri: "viking
|
|
18
|
-
{ type: "skill", uri: "viking
|
|
17
|
+
{ type: "memory", uri: "viking://~/memories", bucket: "memories" },
|
|
18
|
+
{ type: "skill", uri: "viking://~/skills", bucket: "skills" },
|
|
19
19
|
];
|
|
20
20
|
const DEFAULT_CONTEXT_LIMIT = 10;
|
|
21
21
|
const DEFAULT_CONTEXT_MAX_TOKENS = 1600;
|
|
@@ -164,10 +164,9 @@ export function contextRequestTimeoutMs(cfg = {}, body = {}) {
|
|
|
164
164
|
// `query_expansion` defaults to "auto" server-side, so only an explicit "off"
|
|
165
165
|
// takes the expansion fuse back out of the budget.
|
|
166
166
|
const wantsExpansion = Boolean(body.session_id) && body.query_expansion !== "off";
|
|
167
|
-
if (!wantsRewrite && !wantsExpansion) return undefined;
|
|
168
|
-
|
|
169
167
|
const configured = Number(cfg.recallContextTimeoutMs);
|
|
170
168
|
if (Number.isFinite(configured) && configured > 0) return Math.max(1000, Math.floor(configured));
|
|
169
|
+
if (!wantsRewrite && !wantsExpansion) return undefined;
|
|
171
170
|
const floor = wantsRewrite ? SERVER_REWRITE_REQUEST_TIMEOUT_MS : EXPANSION_REQUEST_TIMEOUT_MS;
|
|
172
171
|
return Math.max(Number(cfg.timeoutMs) || 0, floor);
|
|
173
172
|
}
|
|
@@ -272,6 +271,11 @@ async function resolveUserSpace(fetchJSON, actorPeerId = "") {
|
|
|
272
271
|
|
|
273
272
|
async function resolveTargetUri(fetchJSON, targetUri, actorPeerId = "") {
|
|
274
273
|
const trimmed = targetUri.trim().replace(/\/+$/, "");
|
|
274
|
+
// viking://~ is the home alias: the server expands it to the caller's own user
|
|
275
|
+
// space, so it needs no client-side rewrite.
|
|
276
|
+
if (trimmed === "viking://~" || trimmed.startsWith("viking://~/")) return trimmed;
|
|
277
|
+
// Legacy compat: uid-less viking://user/<reserved> URIs may still sit in plugin
|
|
278
|
+
// configs. Newer servers reject them, so rewrite to an explicit-uid URI here.
|
|
275
279
|
const m = trimmed.match(/^viking:\/\/user(?:\/(.*))?$/);
|
|
276
280
|
if (!m) return trimmed;
|
|
277
281
|
const rawRest = (m[1] ?? "").trim();
|
package/shared/setup-wizard.mjs
CHANGED
|
@@ -39,14 +39,17 @@ export async function runSetupWizard({
|
|
|
39
39
|
output = process.stdout,
|
|
40
40
|
env = process.env,
|
|
41
41
|
} = {}) {
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
// cliPath is empty until the file exists; first-run writes go to the candidate
|
|
43
|
+
// path (OPENVIKING_CLI_CONFIG_FILE, else ~/.openviking/ovcli.conf).
|
|
44
|
+
const { cliPath, cliPathCandidate } = loadCredentialFiles(env);
|
|
45
|
+
const targetPath = cliPath || cliPathCandidate;
|
|
46
|
+
const current = readJsonSafe(targetPath);
|
|
44
47
|
const rl = createInterface({ input, output });
|
|
45
48
|
const say = (line = "") => output.write(`${line}\n`);
|
|
46
49
|
|
|
47
50
|
try {
|
|
48
51
|
say("OpenViking memory plugin setup");
|
|
49
|
-
say(`Config file: ${
|
|
52
|
+
say(`Config file: ${targetPath}`);
|
|
50
53
|
say("");
|
|
51
54
|
say("Current values:");
|
|
52
55
|
say(` url: ${current.url || "(not set)"}`);
|
|
@@ -89,22 +92,22 @@ export async function runSetupWizard({
|
|
|
89
92
|
const confirm = (await rl.question("Write config? [Y/n] ")).trim().toLowerCase();
|
|
90
93
|
if (confirm === "n" || confirm === "no") {
|
|
91
94
|
say("Aborted; nothing written.");
|
|
92
|
-
return { written: false, path:
|
|
95
|
+
return { written: false, path: targetPath };
|
|
93
96
|
}
|
|
94
97
|
|
|
95
|
-
mkdirSync(dirname(
|
|
96
|
-
if (existsSync(
|
|
97
|
-
copyFileSync(
|
|
98
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
99
|
+
if (existsSync(targetPath)) {
|
|
100
|
+
copyFileSync(targetPath, `${targetPath}.bak.${Date.now()}`);
|
|
98
101
|
}
|
|
99
|
-
writeFileSync(
|
|
102
|
+
writeFileSync(targetPath, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 });
|
|
100
103
|
try {
|
|
101
|
-
chmodSync(
|
|
104
|
+
chmodSync(targetPath, 0o600);
|
|
102
105
|
} catch {
|
|
103
106
|
/* best effort on platforms without chmod semantics */
|
|
104
107
|
}
|
|
105
|
-
say(`Written: ${
|
|
108
|
+
say(`Written: ${targetPath}`);
|
|
106
109
|
say("The stdio MCP proxy and hooks pick this up on the next harness start.");
|
|
107
|
-
return { written: true, path:
|
|
110
|
+
return { written: true, path: targetPath };
|
|
108
111
|
} finally {
|
|
109
112
|
rl.close();
|
|
110
113
|
}
|
package/shared/uri-guard.mjs
CHANGED
|
@@ -14,30 +14,52 @@ export function normalizeToolName(value) {
|
|
|
14
14
|
return String(value || "").trim().toLowerCase();
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
// Arguments that carry file CONTENT rather than a location. The sweep below
|
|
18
|
+
// looks past the known path keys so an unusual one (`paths`, a nested target)
|
|
19
|
+
// is still caught, but text a tool is asked to WRITE is not a path: a local
|
|
20
|
+
// `write` whose body merely mentions viking://user/default/ was denied, and no
|
|
21
|
+
// file was created. Skipped by name at any depth.
|
|
22
|
+
const DEFAULT_CONTENT_KEYS = [
|
|
23
|
+
"content",
|
|
24
|
+
"contents",
|
|
25
|
+
"text",
|
|
26
|
+
"body",
|
|
27
|
+
"old_string",
|
|
28
|
+
"oldString",
|
|
29
|
+
"new_string",
|
|
30
|
+
"newString",
|
|
31
|
+
"old_str",
|
|
32
|
+
"new_str",
|
|
33
|
+
"file_text",
|
|
34
|
+
"insert_line",
|
|
35
|
+
"replacement",
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
export function findVikingUri(args = {}, keys = DEFAULT_URI_KEYS, contentKeys = DEFAULT_CONTENT_KEYS) {
|
|
18
39
|
if (!args || typeof args !== "object") return null;
|
|
19
40
|
for (const key of keys) {
|
|
20
41
|
const uri = findVikingUriInValue(args[key]);
|
|
21
42
|
if (uri) return uri;
|
|
22
43
|
}
|
|
23
|
-
return findVikingUriInValue(args);
|
|
44
|
+
return findVikingUriInValue(args, new Set(contentKeys));
|
|
24
45
|
}
|
|
25
46
|
|
|
26
|
-
export function findVikingUriInValue(value) {
|
|
47
|
+
export function findVikingUriInValue(value, skipKeys) {
|
|
27
48
|
if (typeof value === "string") {
|
|
28
49
|
const match = value.match(/\bviking:\/\/[^\s"'`<>)]*/i);
|
|
29
50
|
return match?.[0] || null;
|
|
30
51
|
}
|
|
31
52
|
if (Array.isArray(value)) {
|
|
32
53
|
for (const item of value) {
|
|
33
|
-
const uri = findVikingUriInValue(item);
|
|
54
|
+
const uri = findVikingUriInValue(item, skipKeys);
|
|
34
55
|
if (uri) return uri;
|
|
35
56
|
}
|
|
36
57
|
return null;
|
|
37
58
|
}
|
|
38
59
|
if (value && typeof value === "object") {
|
|
39
|
-
for (const item of Object.
|
|
40
|
-
|
|
60
|
+
for (const [key, item] of Object.entries(value)) {
|
|
61
|
+
if (skipKeys?.has(key)) continue;
|
|
62
|
+
const uri = findVikingUriInValue(item, skipKeys);
|
|
41
63
|
if (uri) return uri;
|
|
42
64
|
}
|
|
43
65
|
}
|