@openviking/dsh-memory-plugin 0.1.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 +129 -0
- package/capture.mjs +104 -0
- package/client.mjs +206 -0
- package/config.mjs +161 -0
- package/cordis.patch.yml +9 -0
- package/index.mjs +56 -0
- package/lifecycle.mjs +8 -0
- package/package.json +60 -0
- package/runtime.mjs +316 -0
- package/shared/capture-utils.mjs +515 -0
- package/shared/credentials.mjs +228 -0
- package/shared/debug-log.mjs +58 -0
- package/shared/pending-queue.mjs +451 -0
- package/shared/plugin-config.mjs +90 -0
- package/shared/profile-inject.mjs +294 -0
- package/shared/recall-compress-core.mjs +195 -0
- package/shared/recall-core.mjs +609 -0
- package/shared/retryable.mjs +9 -0
- package/shared/session-model.mjs +57 -0
- package/shared/setup-wizard.mjs +111 -0
- package/shared/uri-guard.mjs +56 -0
- package/shared/workspace-peer.mjs +16 -0
- package/tools.mjs +246 -0
- package/uri-guard.mjs +51 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// GENERATED FROM examples/memory-plugin-shared/lib. DO NOT EDIT.
|
|
2
|
+
/**
|
|
3
|
+
* Interactive ovcli.conf setup wizard shared by the OpenViking memory plugins.
|
|
4
|
+
*
|
|
5
|
+
* Lets a pure-marketplace install (no installer script) configure the server
|
|
6
|
+
* URL and API key that both the lifecycle hooks and the stdio MCP proxy read:
|
|
7
|
+
*
|
|
8
|
+
* node <plugin>/scripts/setup.mjs
|
|
9
|
+
*
|
|
10
|
+
* Conventions: show current values, keep existing secrets on empty input,
|
|
11
|
+
* merge-write (never drop unknown fields), back up the previous file, 0600.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from "node:fs";
|
|
15
|
+
import { dirname } from "node:path";
|
|
16
|
+
import { createInterface } from "node:readline/promises";
|
|
17
|
+
import { loadCredentialFiles } from "./credentials.mjs";
|
|
18
|
+
|
|
19
|
+
const CLOUD_URL = "https://api.vikingdb.cn-beijing.volces.com/openviking";
|
|
20
|
+
const LOCAL_URL = "http://127.0.0.1:1933";
|
|
21
|
+
|
|
22
|
+
function maskSecret(value) {
|
|
23
|
+
const s = String(value || "");
|
|
24
|
+
if (!s) return "(not set)";
|
|
25
|
+
if (s.length <= 8) return "****";
|
|
26
|
+
return `${s.slice(0, 4)}…${s.slice(-4)} (${s.length} chars)`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function readJsonSafe(path) {
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
32
|
+
} catch {
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function runSetupWizard({
|
|
38
|
+
input = process.stdin,
|
|
39
|
+
output = process.stdout,
|
|
40
|
+
env = process.env,
|
|
41
|
+
} = {}) {
|
|
42
|
+
const { cliPath } = loadCredentialFiles(env);
|
|
43
|
+
const current = readJsonSafe(cliPath);
|
|
44
|
+
const rl = createInterface({ input, output });
|
|
45
|
+
const say = (line = "") => output.write(`${line}\n`);
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
say("OpenViking memory plugin setup");
|
|
49
|
+
say(`Config file: ${cliPath}`);
|
|
50
|
+
say("");
|
|
51
|
+
say("Current values:");
|
|
52
|
+
say(` url: ${current.url || "(not set)"}`);
|
|
53
|
+
say(` api_key: ${maskSecret(current.api_key)}`);
|
|
54
|
+
if (current.account) say(` account: ${current.account}`);
|
|
55
|
+
if (current.user) say(` user: ${current.user}`);
|
|
56
|
+
say("");
|
|
57
|
+
|
|
58
|
+
const defaultUrl = current.url || LOCAL_URL;
|
|
59
|
+
say("Where do you connect to OpenViking?");
|
|
60
|
+
say(` 1) Self-hosted / local [${LOCAL_URL}]`);
|
|
61
|
+
say(` 2) Volcengine OpenViking Cloud [${CLOUD_URL}]`);
|
|
62
|
+
say(` 3) Custom URL / keep current [${defaultUrl}]`);
|
|
63
|
+
const mode = (await rl.question("Choice [1/2/3, default 3]: ")).trim();
|
|
64
|
+
let url = defaultUrl;
|
|
65
|
+
if (mode === "1") {
|
|
66
|
+
url = LOCAL_URL;
|
|
67
|
+
} else if (mode === "2") {
|
|
68
|
+
url = CLOUD_URL;
|
|
69
|
+
} else {
|
|
70
|
+
const answer = (await rl.question(`Server URL [${defaultUrl}]: `)).trim();
|
|
71
|
+
if (answer) url = answer;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const keyPrompt = current.api_key
|
|
75
|
+
? `API key [enter = keep ${maskSecret(current.api_key)}, '-' = clear]: `
|
|
76
|
+
: "API key (leave empty for unauthenticated local mode): ";
|
|
77
|
+
const keyAnswer = await rl.question(keyPrompt);
|
|
78
|
+
let apiKey = current.api_key || "";
|
|
79
|
+
if (keyAnswer === "-") apiKey = "";
|
|
80
|
+
else if (keyAnswer.trim()) apiKey = keyAnswer.trim();
|
|
81
|
+
else if (!current.api_key) apiKey = "";
|
|
82
|
+
|
|
83
|
+
const next = { ...current, url, api_key: apiKey };
|
|
84
|
+
|
|
85
|
+
say("");
|
|
86
|
+
say("Changes:");
|
|
87
|
+
say(` url: ${current.url || "(not set)"} -> ${next.url}`);
|
|
88
|
+
say(` api_key: ${maskSecret(current.api_key)} -> ${maskSecret(next.api_key)}`);
|
|
89
|
+
const confirm = (await rl.question("Write config? [Y/n] ")).trim().toLowerCase();
|
|
90
|
+
if (confirm === "n" || confirm === "no") {
|
|
91
|
+
say("Aborted; nothing written.");
|
|
92
|
+
return { written: false, path: cliPath };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
mkdirSync(dirname(cliPath), { recursive: true });
|
|
96
|
+
if (existsSync(cliPath)) {
|
|
97
|
+
copyFileSync(cliPath, `${cliPath}.bak.${Date.now()}`);
|
|
98
|
+
}
|
|
99
|
+
writeFileSync(cliPath, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 });
|
|
100
|
+
try {
|
|
101
|
+
chmodSync(cliPath, 0o600);
|
|
102
|
+
} catch {
|
|
103
|
+
/* best effort on platforms without chmod semantics */
|
|
104
|
+
}
|
|
105
|
+
say(`Written: ${cliPath}`);
|
|
106
|
+
say("The stdio MCP proxy and hooks pick this up on the next harness start.");
|
|
107
|
+
return { written: true, path: cliPath };
|
|
108
|
+
} finally {
|
|
109
|
+
rl.close();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// GENERATED FROM examples/memory-plugin-shared/lib. DO NOT EDIT.
|
|
2
|
+
const DEFAULT_URI_KEYS = [
|
|
3
|
+
"filePath",
|
|
4
|
+
"file_path",
|
|
5
|
+
"filepath",
|
|
6
|
+
"path",
|
|
7
|
+
"uri",
|
|
8
|
+
"target_uri",
|
|
9
|
+
"targetUri",
|
|
10
|
+
"pattern",
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
export function normalizeToolName(value) {
|
|
14
|
+
return String(value || "").trim().toLowerCase();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function findVikingUri(args = {}, keys = DEFAULT_URI_KEYS) {
|
|
18
|
+
if (!args || typeof args !== "object") return null;
|
|
19
|
+
for (const key of keys) {
|
|
20
|
+
const uri = findVikingUriInValue(args[key]);
|
|
21
|
+
if (uri) return uri;
|
|
22
|
+
}
|
|
23
|
+
return findVikingUriInValue(args);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function findVikingUriInValue(value) {
|
|
27
|
+
if (typeof value === "string") {
|
|
28
|
+
const match = value.match(/\bviking:\/\/[^\s"'`<>)]*/i);
|
|
29
|
+
return match?.[0] || null;
|
|
30
|
+
}
|
|
31
|
+
if (Array.isArray(value)) {
|
|
32
|
+
for (const item of value) {
|
|
33
|
+
const uri = findVikingUriInValue(item);
|
|
34
|
+
if (uri) return uri;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
if (value && typeof value === "object") {
|
|
39
|
+
for (const item of Object.values(value)) {
|
|
40
|
+
const uri = findVikingUriInValue(item);
|
|
41
|
+
if (uri) return uri;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function buildGuardMessage(uri, hint = {}) {
|
|
48
|
+
const tool = hint.tool || "the OpenViking MCP tools";
|
|
49
|
+
const example = typeof hint.example === "function" ? hint.example(uri) : hint.example;
|
|
50
|
+
const lines = [
|
|
51
|
+
"viking:// URIs are OpenViking virtual paths, not local filesystem paths.",
|
|
52
|
+
`Use ${tool} instead.`,
|
|
53
|
+
];
|
|
54
|
+
if (example) lines.push(`Example: ${example}`);
|
|
55
|
+
return lines.join("\n");
|
|
56
|
+
}
|
|
@@ -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/tools.mjs
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
2
|
+
|
|
3
|
+
export function registerOpenVikingTools(ctx, client, runtime) {
|
|
4
|
+
ctx.tools.register(textTool({
|
|
5
|
+
name: "viking_search",
|
|
6
|
+
description:
|
|
7
|
+
"Search OpenViking memories, resources, and skills. Use this for prior decisions, preferences, and project knowledge not present in the current context.",
|
|
8
|
+
parameters: {
|
|
9
|
+
query: { type: "string", required: true, description: "Semantic search query." },
|
|
10
|
+
scope: { type: "string", description: "Optional viking:// URI prefix." },
|
|
11
|
+
limit: { type: "integer", description: "Maximum results, from 1 to 50." },
|
|
12
|
+
},
|
|
13
|
+
async execute(args, exec) {
|
|
14
|
+
const actorPeerId = await peerFor(runtime, exec);
|
|
15
|
+
const results = await client.find(args.query, {
|
|
16
|
+
targetUri: args.scope,
|
|
17
|
+
limit: Math.max(1, Math.min(50, args.limit || 10)),
|
|
18
|
+
actorPeerId,
|
|
19
|
+
});
|
|
20
|
+
if (results.length === 0) return "No results found.";
|
|
21
|
+
return results.map(result => {
|
|
22
|
+
const abstract = result.abstract.length > client.config.recallMaxContentChars
|
|
23
|
+
? `${result.abstract.slice(0, client.config.recallMaxContentChars)}...`
|
|
24
|
+
: result.abstract;
|
|
25
|
+
return `[${result.score.toFixed(2)}] ${result.uri}\n ${abstract}`;
|
|
26
|
+
}).join("\n\n");
|
|
27
|
+
},
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
ctx.tools.register(textTool({
|
|
31
|
+
name: "viking_read",
|
|
32
|
+
description:
|
|
33
|
+
"Read OpenViking content by viking:// URI at abstract, overview, or full detail.",
|
|
34
|
+
parameters: {
|
|
35
|
+
uri: { type: "string", required: true, description: "The viking:// URI to read." },
|
|
36
|
+
level: {
|
|
37
|
+
type: "string",
|
|
38
|
+
required: true,
|
|
39
|
+
enum: ["abstract", "overview", "full"],
|
|
40
|
+
description: "Detail level.",
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
async execute(args, exec) {
|
|
44
|
+
const content = await client.read(
|
|
45
|
+
args.uri,
|
|
46
|
+
args.level,
|
|
47
|
+
await peerFor(runtime, exec),
|
|
48
|
+
);
|
|
49
|
+
return typeof content === "string" && content
|
|
50
|
+
? content
|
|
51
|
+
: `No content at ${args.uri}`;
|
|
52
|
+
},
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
ctx.tools.register(textTool({
|
|
56
|
+
name: "viking_browse",
|
|
57
|
+
description: "List an OpenViking directory or inspect metadata for a viking:// URI.",
|
|
58
|
+
parameters: {
|
|
59
|
+
action: {
|
|
60
|
+
type: "string",
|
|
61
|
+
required: true,
|
|
62
|
+
enum: ["list", "stat"],
|
|
63
|
+
description: "Browse operation.",
|
|
64
|
+
},
|
|
65
|
+
uri: { type: "string", description: "URI to browse. Defaults to viking://." },
|
|
66
|
+
},
|
|
67
|
+
async execute(args, exec) {
|
|
68
|
+
const actorPeerId = await peerFor(runtime, exec);
|
|
69
|
+
const uri = args.uri || "viking://";
|
|
70
|
+
if (args.action === "stat") {
|
|
71
|
+
const info = await client.stat(uri, actorPeerId);
|
|
72
|
+
return info ? JSON.stringify(info, null, 2) : `Not found: ${uri}`;
|
|
73
|
+
}
|
|
74
|
+
const entries = await client.list(uri, actorPeerId);
|
|
75
|
+
if (entries.length === 0) return `Empty directory: ${uri}`;
|
|
76
|
+
return entries
|
|
77
|
+
.map(entry => `${entry.isDir ? "[dir]" : "[file]"} ${entry.name || entry.uri}`)
|
|
78
|
+
.join("\n");
|
|
79
|
+
},
|
|
80
|
+
}));
|
|
81
|
+
|
|
82
|
+
ctx.tools.register(textTool({
|
|
83
|
+
name: "viking_remember",
|
|
84
|
+
description:
|
|
85
|
+
"Store an important fact, preference, decision, or gotcha in the current OpenViking session.",
|
|
86
|
+
parameters: {
|
|
87
|
+
content: { type: "string", required: true, description: "Fact to remember." },
|
|
88
|
+
category: {
|
|
89
|
+
type: "string",
|
|
90
|
+
description: "Optional category such as preference, decision, entity, event, or pattern.",
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
async execute(args, exec) {
|
|
94
|
+
if (!exec.agent) throw new Error("viking_remember requires a calling agent");
|
|
95
|
+
const state = await runtime.initialize(exec.agent);
|
|
96
|
+
if (!state.ready) return "OpenViking server is not reachable.";
|
|
97
|
+
const category = args.category || "general";
|
|
98
|
+
const payload = {
|
|
99
|
+
role: "user",
|
|
100
|
+
content: `[Remember — ${category}] ${args.content}`,
|
|
101
|
+
...(state.config.peerId ? { peer_id: state.config.peerId } : {}),
|
|
102
|
+
};
|
|
103
|
+
const response = await client.addMessage(
|
|
104
|
+
state.ovSessionId,
|
|
105
|
+
payload,
|
|
106
|
+
state.config.peerId,
|
|
107
|
+
);
|
|
108
|
+
return response.ok
|
|
109
|
+
? `Remembered in OpenViking: "${args.content}" (${category})`
|
|
110
|
+
: `Failed to remember: ${response.error?.message || response.error?.code || "unknown error"}`;
|
|
111
|
+
},
|
|
112
|
+
}));
|
|
113
|
+
|
|
114
|
+
ctx.tools.register(textTool({
|
|
115
|
+
name: "viking_forget",
|
|
116
|
+
description:
|
|
117
|
+
"Permanently delete an OpenViking item only after the user explicitly asks to forget or delete it. Use an exact URI, or delete the strongest search match above score 0.8.",
|
|
118
|
+
parameters: {
|
|
119
|
+
uri: { type: "string", description: "Exact viking:// URI to delete." },
|
|
120
|
+
query: { type: "string", description: "Search query used when URI is omitted." },
|
|
121
|
+
},
|
|
122
|
+
async execute(args, exec) {
|
|
123
|
+
const actorPeerId = await peerFor(runtime, exec);
|
|
124
|
+
let uri = args.uri;
|
|
125
|
+
if (!uri && args.query) {
|
|
126
|
+
const results = await client.find(args.query, { limit: 1, actorPeerId });
|
|
127
|
+
if (!results[0] || results[0].score <= 0.8) {
|
|
128
|
+
return "No strong match found (score > 0.8 required).";
|
|
129
|
+
}
|
|
130
|
+
uri = results[0].uri;
|
|
131
|
+
}
|
|
132
|
+
if (!uri) return "Provide either uri or query.";
|
|
133
|
+
return await client.forget(uri, false, actorPeerId)
|
|
134
|
+
? `Deleted: ${uri}`
|
|
135
|
+
: `Failed to delete: ${uri}`;
|
|
136
|
+
},
|
|
137
|
+
}));
|
|
138
|
+
|
|
139
|
+
ctx.tools.register(textTool({
|
|
140
|
+
name: "viking_add_resource",
|
|
141
|
+
description: "Ingest an HTTP(S) or git URL into OpenViking for indexed retrieval.",
|
|
142
|
+
parameters: {
|
|
143
|
+
url: { type: "string", required: true, description: "Remote URL to ingest." },
|
|
144
|
+
reason: { type: "string", description: "Why this resource is relevant." },
|
|
145
|
+
},
|
|
146
|
+
async execute(args, exec) {
|
|
147
|
+
const result = await client.addResource(
|
|
148
|
+
args.url,
|
|
149
|
+
args.reason,
|
|
150
|
+
await peerFor(runtime, exec),
|
|
151
|
+
);
|
|
152
|
+
return result?.root_uri
|
|
153
|
+
? `Ingested: ${result.root_uri}`
|
|
154
|
+
: `Failed to ingest: ${args.url}`;
|
|
155
|
+
},
|
|
156
|
+
}));
|
|
157
|
+
|
|
158
|
+
ctx.tools.register(textTool({
|
|
159
|
+
name: "viking_archive_expand",
|
|
160
|
+
description: "Read original messages from one archive in the current DSH session.",
|
|
161
|
+
parameters: {
|
|
162
|
+
archive_id: { type: "string", required: true, description: "Archive id such as archive_001." },
|
|
163
|
+
},
|
|
164
|
+
async execute(args, exec) {
|
|
165
|
+
if (!exec.agent) throw new Error("viking_archive_expand requires a calling agent");
|
|
166
|
+
const state = await runtime.initialize(exec.agent);
|
|
167
|
+
const archive = await client.getSessionArchive(
|
|
168
|
+
state.ovSessionId,
|
|
169
|
+
args.archive_id,
|
|
170
|
+
state.config.peerId,
|
|
171
|
+
);
|
|
172
|
+
if (!archive) return `Archive not found: ${args.archive_id}`;
|
|
173
|
+
const messages = Array.isArray(archive.messages) ? archive.messages : [];
|
|
174
|
+
const header = [
|
|
175
|
+
`## ${archive.archive_id || args.archive_id}`,
|
|
176
|
+
archive.abstract ? `**Summary**: ${archive.abstract}` : "",
|
|
177
|
+
`**Messages**: ${messages.length}`,
|
|
178
|
+
].filter(Boolean).join("\n");
|
|
179
|
+
const body = messages.map(formatArchiveMessage).join("\n\n");
|
|
180
|
+
return body ? `${header}\n\n${body}` : header;
|
|
181
|
+
},
|
|
182
|
+
}));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Presentation identity per tool: pending-card kind and title verb. */
|
|
186
|
+
const TOOL_PRESENTATION = {
|
|
187
|
+
viking_search: { kind: "read", title: args => `OpenViking search: ${args.query}` },
|
|
188
|
+
viking_read: { kind: "read", title: args => `OpenViking read: ${args.uri}` },
|
|
189
|
+
viking_browse: { kind: "read", title: args => `OpenViking browse: ${args.uri ?? "viking://"}` },
|
|
190
|
+
viking_remember: { kind: "other", title: () => "OpenViking remember" },
|
|
191
|
+
viking_forget: { kind: "other", title: args => `OpenViking forget: ${args.uri ?? args.query ?? ""}` },
|
|
192
|
+
viking_add_resource: { kind: "other", title: args => `OpenViking ingest: ${args.url}` },
|
|
193
|
+
viking_archive_expand: { kind: "read", title: () => "OpenViking archive expand" },
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* All seven tools return model-facing text; `defineTool` owns parameter and
|
|
198
|
+
* output JSON-schema conversion, validation wiring, and reserved-name checks,
|
|
199
|
+
* so the definitions here stay declarative and track dsh's ToolDefinition
|
|
200
|
+
* contract through the pinned peer instead of a hand-built structure.
|
|
201
|
+
*/
|
|
202
|
+
function textTool(definition) {
|
|
203
|
+
const presentation = TOOL_PRESENTATION[definition.name];
|
|
204
|
+
return defineTool({
|
|
205
|
+
...definition,
|
|
206
|
+
output: {
|
|
207
|
+
schema: { type: "string" },
|
|
208
|
+
render: (_args, value) => [{ type: "text", text: value }],
|
|
209
|
+
},
|
|
210
|
+
presentCall: args => ({
|
|
211
|
+
card: "generic",
|
|
212
|
+
kind: presentation.kind,
|
|
213
|
+
title: presentation.title(args),
|
|
214
|
+
rawInput: args,
|
|
215
|
+
}),
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function peerFor(runtime, exec) {
|
|
220
|
+
if (!exec.agent) return undefined;
|
|
221
|
+
return (await runtime.initialize(exec.agent)).config.peerId;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function formatArchiveMessage(message) {
|
|
225
|
+
const role = String(message?.role || "unknown");
|
|
226
|
+
const parts = Array.isArray(message?.parts) ? message.parts : [];
|
|
227
|
+
const body = parts.map(formatArchivePart).filter(Boolean).join("\n");
|
|
228
|
+
return `[${role}]: ${body || "(empty)"}`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function formatArchivePart(part) {
|
|
232
|
+
if (!part || typeof part !== "object") return "";
|
|
233
|
+
if (part.type === "text") return String(part.text || "");
|
|
234
|
+
if (part.type === "context") {
|
|
235
|
+
return `[Context: ${part.uri || "unknown"}]\n${part.abstract || ""}`.trim();
|
|
236
|
+
}
|
|
237
|
+
if (part.type === "tool") {
|
|
238
|
+
const lines = [`[Tool: ${part.tool_name || "unknown"}]`];
|
|
239
|
+
if (part.tool_input !== undefined) {
|
|
240
|
+
lines.push(`Input: ${JSON.stringify(part.tool_input)}`);
|
|
241
|
+
}
|
|
242
|
+
if (part.tool_output) lines.push(`Output: ${part.tool_output}`);
|
|
243
|
+
return lines.join("\n");
|
|
244
|
+
}
|
|
245
|
+
return `[${part.type || "unknown"}]: ${JSON.stringify(part)}`;
|
|
246
|
+
}
|
package/uri-guard.mjs
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { buildGuardMessage, findVikingUri } from "./shared/uri-guard.mjs";
|
|
2
|
+
|
|
3
|
+
const GUARDED_TOOLS = {
|
|
4
|
+
read: {
|
|
5
|
+
tool: "viking_read",
|
|
6
|
+
example: uri => `viking_read(uri="${uri}", level="overview")`,
|
|
7
|
+
},
|
|
8
|
+
glob: {
|
|
9
|
+
tool: "viking_browse",
|
|
10
|
+
example: uri => `viking_browse(action="list", uri="${uri}")`,
|
|
11
|
+
},
|
|
12
|
+
grep: {
|
|
13
|
+
tool: "viking_search",
|
|
14
|
+
example: (uri, args) =>
|
|
15
|
+
`viking_search(query="${escapeText(args?.pattern)}", scope="${uri}")`,
|
|
16
|
+
},
|
|
17
|
+
bash: {
|
|
18
|
+
tool: "viking_read or viking_search",
|
|
19
|
+
example: uri => `viking_read(uri="${uri}", level="overview")`,
|
|
20
|
+
},
|
|
21
|
+
edit: {
|
|
22
|
+
tool: "OpenViking tools",
|
|
23
|
+
example: uri => `viking_read(uri="${uri}", level="full")`,
|
|
24
|
+
},
|
|
25
|
+
write: {
|
|
26
|
+
tool: "OpenViking tools",
|
|
27
|
+
example: uri => `viking_read(uri="${uri}", level="full")`,
|
|
28
|
+
},
|
|
29
|
+
str_replace_editor: {
|
|
30
|
+
tool: "OpenViking tools",
|
|
31
|
+
example: uri => `viking_read(uri="${uri}", level="full")`,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export async function guardVikingUri(exec, next) {
|
|
36
|
+
const hint = GUARDED_TOOLS[exec.name];
|
|
37
|
+
if (!hint) return next();
|
|
38
|
+
const uri = findVikingUri(exec.arguments);
|
|
39
|
+
if (!uri) return next();
|
|
40
|
+
return {
|
|
41
|
+
kind: "deny",
|
|
42
|
+
reason: buildGuardMessage(uri, {
|
|
43
|
+
tool: hint.tool,
|
|
44
|
+
example: hint.example(uri, exec.arguments),
|
|
45
|
+
}),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function escapeText(value) {
|
|
50
|
+
return String(value || "").replaceAll('"', '\\"');
|
|
51
|
+
}
|