@openviking/opencode-plugin 0.1.6 → 0.2.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/INSTALL-ZH.md +50 -49
- package/INSTALL.md +50 -49
- package/README.md +40 -123
- package/index.mjs +20 -12
- package/lib/config.mjs +272 -0
- package/lib/mcp-config.mjs +21 -0
- package/lib/memory-recall.mjs +21 -172
- package/lib/memory-session.mjs +301 -478
- package/lib/repo-context.mjs +5 -5
- package/lib/session-inject.mjs +71 -0
- package/lib/shared/capture-utils.mjs +500 -0
- package/lib/shared/credentials.mjs +207 -0
- package/lib/shared/debug-log.mjs +58 -0
- package/lib/shared/mcp-proxy-core.mjs +451 -0
- package/lib/shared/pending-queue.mjs +439 -0
- package/lib/shared/profile-inject.mjs +294 -0
- package/lib/shared/recall-core.mjs +282 -0
- package/lib/shared/session-model.mjs +57 -0
- package/lib/shared/setup-wizard.mjs +111 -0
- package/lib/shared/uri-guard.mjs +56 -0
- package/lib/utils.mjs +33 -116
- package/lib/viking-uri-guard.mjs +9 -24
- package/package.json +8 -7
- package/scripts/setup.mjs +8 -0
- package/servers/mcp-proxy.mjs +67 -0
- package/servers/mcp-proxy.test.mjs +100 -0
- package/lib/code-tools.mjs +0 -134
- package/lib/memadd-local.mjs +0 -118
- package/lib/memory-tools.mjs +0 -410
package/lib/repo-context.mjs
CHANGED
|
@@ -45,11 +45,11 @@ export function createRepoContext({ config }) {
|
|
|
45
45
|
"When the user asks about these projects or their internals, use the OpenViking tools before answering.",
|
|
46
46
|
"",
|
|
47
47
|
"Tool guidance:",
|
|
48
|
-
"- Use `
|
|
49
|
-
"- Use `
|
|
50
|
-
"- Use `
|
|
51
|
-
"- Use `
|
|
52
|
-
"- Use `
|
|
48
|
+
"- Use the `openviking_search` MCP tool for semantic or conceptual repository questions.",
|
|
49
|
+
"- Use `openviking_grep` for exact symbols, error strings, class names, function names, and regex-like searches.",
|
|
50
|
+
"- Use `openviking_glob` to enumerate files by pattern.",
|
|
51
|
+
"- Use `openviking_list` to inspect directory structure and `openviking_read` to read specific URIs.",
|
|
52
|
+
"- Use `openviking_add_resource` and `openviking_forget` for repository resource management when explicitly requested.",
|
|
53
53
|
"",
|
|
54
54
|
cachedRepos,
|
|
55
55
|
].join("\n")
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { buildProfileBlock } from "./shared/profile-inject.mjs"
|
|
2
|
+
import { effectivePeerId, fetchJSON, log } from "./utils.mjs"
|
|
3
|
+
|
|
4
|
+
export function createSessionInject({ config, sessionManager }) {
|
|
5
|
+
const injectedSessions = new Set()
|
|
6
|
+
|
|
7
|
+
async function injectSessionContext(input, output) {
|
|
8
|
+
if (config.noAutoInject) return false
|
|
9
|
+
const sessionID = input.sessionID ?? output.message?.sessionID
|
|
10
|
+
const messageID = input.messageID ?? output.message?.id
|
|
11
|
+
if (!sessionID || !messageID || injectedSessions.has(sessionID)) return false
|
|
12
|
+
|
|
13
|
+
const ovSessionId = sessionManager.getMappedSessionId(sessionID)
|
|
14
|
+
if (ovSessionId.includes("__subagent-")) return false
|
|
15
|
+
|
|
16
|
+
const actorPeerId = effectivePeerId(config)
|
|
17
|
+
const clientFetch = (endpoint, init = {}, options = {}) =>
|
|
18
|
+
fetchJSON(config, endpoint, init, { ...options, actorPeerId, timeoutMs: 10000 })
|
|
19
|
+
|
|
20
|
+
const parts = []
|
|
21
|
+
const profile = await buildProfileBlock(clientFetch, config.profileTokenBudget, actorPeerId)
|
|
22
|
+
if (profile?.block) parts.push(profile.block)
|
|
23
|
+
|
|
24
|
+
const archive = await fetchArchiveBlock(clientFetch, ovSessionId, config.resumeContextBudget)
|
|
25
|
+
if (archive) parts.push(archive)
|
|
26
|
+
|
|
27
|
+
if (parts.length === 0) {
|
|
28
|
+
injectedSessions.add(sessionID)
|
|
29
|
+
return false
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const block = [
|
|
33
|
+
'<openviking-context source="session-start">',
|
|
34
|
+
...parts,
|
|
35
|
+
"</openviking-context>",
|
|
36
|
+
].join("\n")
|
|
37
|
+
|
|
38
|
+
output.parts.unshift({
|
|
39
|
+
id: `prt-ov-session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
40
|
+
type: "text",
|
|
41
|
+
text: block,
|
|
42
|
+
synthetic: true,
|
|
43
|
+
sessionID,
|
|
44
|
+
messageID,
|
|
45
|
+
})
|
|
46
|
+
injectedSessions.add(sessionID)
|
|
47
|
+
log("INFO", "session-inject", "Injected OpenViking session context", {
|
|
48
|
+
opencode_session: sessionID,
|
|
49
|
+
openviking_session: ovSessionId,
|
|
50
|
+
hasProfile: Boolean(profile?.block),
|
|
51
|
+
hasArchive: Boolean(archive),
|
|
52
|
+
})
|
|
53
|
+
return true
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { injectSessionContext }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function fetchArchiveBlock(fetcher, ovSessionId, tokenBudget) {
|
|
60
|
+
const res = await fetcher(
|
|
61
|
+
`/api/v1/sessions/${encodeURIComponent(ovSessionId)}/context?token_budget=${Math.max(1024, tokenBudget)}`,
|
|
62
|
+
)
|
|
63
|
+
if (!res.ok) return ""
|
|
64
|
+
const overview = String(res.result?.latest_archive_overview || "").trim()
|
|
65
|
+
if (!overview) return ""
|
|
66
|
+
return [
|
|
67
|
+
`<session-archive session="${ovSessionId}">`,
|
|
68
|
+
overview,
|
|
69
|
+
"</session-archive>",
|
|
70
|
+
].join("\n")
|
|
71
|
+
}
|
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
// GENERATED FROM examples/memory-plugin-shared/lib. DO NOT EDIT.
|
|
2
|
+
const TEXT_BLOCK_TYPES = new Set(["text", "input_text", "output_text"]);
|
|
3
|
+
const TOOL_CALL_TYPES = new Set([
|
|
4
|
+
"tool_call",
|
|
5
|
+
"toolcall",
|
|
6
|
+
"tool_use",
|
|
7
|
+
"tooluse",
|
|
8
|
+
"function_call",
|
|
9
|
+
"functioncall",
|
|
10
|
+
]);
|
|
11
|
+
const TOOL_RESULT_TYPES = new Set([
|
|
12
|
+
"tool_result",
|
|
13
|
+
"toolresult",
|
|
14
|
+
"tool_output",
|
|
15
|
+
"tooloutput",
|
|
16
|
+
"function_call_output",
|
|
17
|
+
"functioncalloutput",
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
const ACK_RE = /^(?:ok|okay|k|yes|yep|no|nope|thanks|thank you|thx|done|收到|好的|好|嗯|可以|继续|不用|不需要|没了|好了)[.!?。!?\s]*$/i;
|
|
21
|
+
const SLASH_COMMAND_RE = /^\/[a-z0-9_-]{1,64}\b/i;
|
|
22
|
+
const METADATA_KEYS = [
|
|
23
|
+
"session_id",
|
|
24
|
+
"sessionid",
|
|
25
|
+
"sessionkey",
|
|
26
|
+
"conversation_id",
|
|
27
|
+
"conversationid",
|
|
28
|
+
"channel",
|
|
29
|
+
"sender",
|
|
30
|
+
"user_id",
|
|
31
|
+
"userid",
|
|
32
|
+
"agent_id",
|
|
33
|
+
"agentid",
|
|
34
|
+
"timestamp",
|
|
35
|
+
"timezone",
|
|
36
|
+
"cwd",
|
|
37
|
+
"model",
|
|
38
|
+
"permission_mode",
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
function normalizeType(value) {
|
|
42
|
+
return String(value || "").toLowerCase().replace(/[-\s]/g, "_");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isToolCallBlock(block) {
|
|
46
|
+
const type = normalizeType(block?.type || block?.kind || block?.role);
|
|
47
|
+
return TOOL_CALL_TYPES.has(type) || Boolean(block?.tool_calls) || Boolean(block?.function?.name);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isToolResultBlock(block) {
|
|
51
|
+
const type = normalizeType(block?.type || block?.kind || block?.role);
|
|
52
|
+
return TOOL_RESULT_TYPES.has(type) || type === "tool" || type === "function";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function oneLine(text) {
|
|
56
|
+
return String(text || "").replace(/\s+/g, " ").trim();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function truncateCaptureText(text, maxChars = 2000) {
|
|
60
|
+
const value = String(text || "").trim();
|
|
61
|
+
if (!Number.isFinite(maxChars) || maxChars <= 0 || value.length <= maxChars) return value;
|
|
62
|
+
return `${value.slice(0, Math.max(0, maxChars - 20)).trimEnd()}\n[truncated]`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function stringifyCompact(value, maxChars) {
|
|
66
|
+
if (value == null) return "";
|
|
67
|
+
if (typeof value === "string") return truncateCaptureText(value, maxChars);
|
|
68
|
+
try {
|
|
69
|
+
return truncateCaptureText(JSON.stringify(value), maxChars);
|
|
70
|
+
} catch {
|
|
71
|
+
return truncateCaptureText(String(value), maxChars);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function parseMaybeJson(value) {
|
|
76
|
+
if (value == null || typeof value !== "string") return value;
|
|
77
|
+
const trimmed = value.trim();
|
|
78
|
+
if (!trimmed) return value;
|
|
79
|
+
try {
|
|
80
|
+
return JSON.parse(trimmed);
|
|
81
|
+
} catch {
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function blockText(block) {
|
|
87
|
+
if (!block || typeof block !== "object") return "";
|
|
88
|
+
if (typeof block.text === "string") return block.text;
|
|
89
|
+
if (typeof block.output_text === "string") return block.output_text;
|
|
90
|
+
if (typeof block.input_text === "string") return block.input_text;
|
|
91
|
+
if (typeof block.content === "string") return block.content;
|
|
92
|
+
return "";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function toolName(block) {
|
|
96
|
+
return oneLine(
|
|
97
|
+
block?.name ||
|
|
98
|
+
block?.tool_name ||
|
|
99
|
+
block?.toolName ||
|
|
100
|
+
block?.function?.name ||
|
|
101
|
+
block?.call?.name ||
|
|
102
|
+
block?.id ||
|
|
103
|
+
"",
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function toolPayload(block, kind) {
|
|
108
|
+
if (!block || typeof block !== "object") return "";
|
|
109
|
+
if (kind === "call") {
|
|
110
|
+
return block.input ??
|
|
111
|
+
block.arguments ??
|
|
112
|
+
block.args ??
|
|
113
|
+
block.params ??
|
|
114
|
+
block.function?.arguments ??
|
|
115
|
+
block.command ??
|
|
116
|
+
block.call?.input ??
|
|
117
|
+
block.call?.arguments ??
|
|
118
|
+
"";
|
|
119
|
+
}
|
|
120
|
+
return block.output ??
|
|
121
|
+
block.result ??
|
|
122
|
+
block.error ??
|
|
123
|
+
block.data ??
|
|
124
|
+
block.content ??
|
|
125
|
+
block.text ??
|
|
126
|
+
"";
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function toolId(block) {
|
|
130
|
+
return oneLine(
|
|
131
|
+
block?.id ||
|
|
132
|
+
block?.call_id ||
|
|
133
|
+
block?.callId ||
|
|
134
|
+
block?.tool_call_id ||
|
|
135
|
+
block?.toolCallId ||
|
|
136
|
+
block?.tool_use_id ||
|
|
137
|
+
block?.toolUseId ||
|
|
138
|
+
block?.function_call_id ||
|
|
139
|
+
block?.functionCallId ||
|
|
140
|
+
"",
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function toolStatus(block, kind) {
|
|
145
|
+
if (kind === "call") return "running";
|
|
146
|
+
if (block?.is_error || block?.error) return "error";
|
|
147
|
+
const status = oneLine(block?.status || "");
|
|
148
|
+
return status || "completed";
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function buildToolPart(block, kind, { toolMaxChars = 2000, toolNameById = {} } = {}) {
|
|
152
|
+
const id = toolId(block);
|
|
153
|
+
const name = toolName(block) || (id ? toolNameById[id] : "");
|
|
154
|
+
const payload = toolPayload(block, kind);
|
|
155
|
+
const part = {
|
|
156
|
+
type: "tool",
|
|
157
|
+
tool_id: id || undefined,
|
|
158
|
+
tool_name: name || undefined,
|
|
159
|
+
tool_status: toolStatus(block, kind),
|
|
160
|
+
};
|
|
161
|
+
if (kind === "call") {
|
|
162
|
+
const input = parseMaybeJson(payload);
|
|
163
|
+
if (input !== "" && input != null) {
|
|
164
|
+
part.tool_input = typeof input === "object" && !Array.isArray(input)
|
|
165
|
+
? input
|
|
166
|
+
: { value: input };
|
|
167
|
+
}
|
|
168
|
+
} else {
|
|
169
|
+
part.tool_output = stringifyCompact(payload, toolMaxChars);
|
|
170
|
+
}
|
|
171
|
+
return part;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function formatToolBlock(block, kind, maxChars) {
|
|
175
|
+
const name = toolName(block);
|
|
176
|
+
const payload = toolPayload(block, kind);
|
|
177
|
+
const body = oneLine(stringifyCompact(payload, maxChars));
|
|
178
|
+
const label = kind === "call" ? "tool-call" : "tool-result";
|
|
179
|
+
return body
|
|
180
|
+
? `[${label}${name ? ` ${name}` : ""}] ${body}`
|
|
181
|
+
: `[${label}${name ? ` ${name}` : ""}]`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function blockToText(block, options) {
|
|
185
|
+
if (!block) return "";
|
|
186
|
+
if (typeof block === "string") return block;
|
|
187
|
+
if (Array.isArray(block)) return extractTextFromContent(block, options);
|
|
188
|
+
if (typeof block !== "object") return "";
|
|
189
|
+
|
|
190
|
+
if (block.item && typeof block.item === "object") {
|
|
191
|
+
const itemText = blockToText(block.item, options);
|
|
192
|
+
if (itemText) return itemText;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const type = normalizeType(block.type || block.kind || block.role);
|
|
196
|
+
if (TEXT_BLOCK_TYPES.has(type)) return blockText(block);
|
|
197
|
+
if (isToolCallBlock(block)) return formatToolBlock(block, "call", options.toolMaxChars);
|
|
198
|
+
if (isToolResultBlock(block)) return formatToolBlock(block, "result", options.toolMaxChars);
|
|
199
|
+
if (Array.isArray(block.content)) return extractTextFromContent(block.content, options);
|
|
200
|
+
if (!type) return blockText(block);
|
|
201
|
+
return "";
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function extractTextFromContent(content, options = {}) {
|
|
205
|
+
const opts = { toolMaxChars: 2000, ...options };
|
|
206
|
+
if (!content) return "";
|
|
207
|
+
if (typeof content === "string") return content;
|
|
208
|
+
if (Array.isArray(content)) {
|
|
209
|
+
return content
|
|
210
|
+
.map((block) => blockToText(block, opts))
|
|
211
|
+
.filter(Boolean)
|
|
212
|
+
.join("\n\n");
|
|
213
|
+
}
|
|
214
|
+
if (typeof content === "object") {
|
|
215
|
+
return blockToText(content, opts) || stringifyCompact(content, opts.toolMaxChars);
|
|
216
|
+
}
|
|
217
|
+
return "";
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function extractTextFromPayload(payload, options = {}) {
|
|
221
|
+
if (!payload || typeof payload !== "object") return "";
|
|
222
|
+
const chunks = [];
|
|
223
|
+
const directType = normalizeType(payload.type || payload.kind || payload.role);
|
|
224
|
+
if (TOOL_RESULT_TYPES.has(directType) || directType === "tool" || TOOL_CALL_TYPES.has(directType)) {
|
|
225
|
+
const direct = blockToText(payload, { toolMaxChars: 2000, ...options });
|
|
226
|
+
if (direct) return direct;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (payload.message && typeof payload.message === "object") {
|
|
230
|
+
const messageText = extractTextFromPayload(payload.message, options);
|
|
231
|
+
if (messageText) chunks.push(messageText);
|
|
232
|
+
} else if (payload.content !== undefined) {
|
|
233
|
+
const contentText = extractTextFromContent(payload.content, options);
|
|
234
|
+
if (contentText) chunks.push(contentText);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
for (const key of ["tool_calls", "toolCalls", "function_call", "functionCall", "tool_call", "toolCall"]) {
|
|
238
|
+
const value = payload[key];
|
|
239
|
+
if (!value) continue;
|
|
240
|
+
const toolText = extractTextFromContent(value, options);
|
|
241
|
+
if (toolText) chunks.push(toolText);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (chunks.length === 0) {
|
|
245
|
+
const direct = blockToText(payload, { toolMaxChars: 2000, ...options });
|
|
246
|
+
if (direct) chunks.push(direct);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return chunks.join("\n\n");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function collectToolNamesByIdFromEntries(entries) {
|
|
253
|
+
const map = {};
|
|
254
|
+
for (const entry of entries || []) {
|
|
255
|
+
const payload = entry?.payload && typeof entry.payload === "object" ? entry.payload : entry;
|
|
256
|
+
collectToolNamesByIdFromPayload(payload, map);
|
|
257
|
+
}
|
|
258
|
+
return map;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function collectToolNamesByIdFromPayload(payload, out) {
|
|
262
|
+
if (!payload || typeof payload !== "object") return;
|
|
263
|
+
if (payload.message && typeof payload.message === "object") {
|
|
264
|
+
collectToolNamesByIdFromPayload(payload.message, out);
|
|
265
|
+
}
|
|
266
|
+
const candidates = [];
|
|
267
|
+
if (Array.isArray(payload.content)) candidates.push(...payload.content);
|
|
268
|
+
for (const key of ["tool_calls", "toolCalls", "function_call", "functionCall", "tool_call", "toolCall"]) {
|
|
269
|
+
const value = payload[key];
|
|
270
|
+
if (!value) continue;
|
|
271
|
+
if (Array.isArray(value)) candidates.push(...value);
|
|
272
|
+
else candidates.push(value);
|
|
273
|
+
}
|
|
274
|
+
if (isToolCallBlock(payload)) candidates.push(payload);
|
|
275
|
+
for (const block of candidates) {
|
|
276
|
+
if (!block || typeof block !== "object") continue;
|
|
277
|
+
if (!isToolCallBlock(block)) continue;
|
|
278
|
+
const id = toolId(block);
|
|
279
|
+
const name = toolName(block);
|
|
280
|
+
if (id && name) out[id] = name;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function extractPartsFromContent(content, options = {}) {
|
|
285
|
+
const opts = { toolMaxChars: 2000, toolNameById: {}, ...options };
|
|
286
|
+
const parts = [];
|
|
287
|
+
if (!content) return parts;
|
|
288
|
+
if (typeof content === "string") {
|
|
289
|
+
if (content.trim()) parts.push({ type: "text", text: content });
|
|
290
|
+
return parts;
|
|
291
|
+
}
|
|
292
|
+
if (!Array.isArray(content)) {
|
|
293
|
+
const text = blockToText(content, opts);
|
|
294
|
+
if (text.trim()) parts.push({ type: "text", text });
|
|
295
|
+
return parts;
|
|
296
|
+
}
|
|
297
|
+
for (const block of content) {
|
|
298
|
+
if (!block || typeof block !== "object") continue;
|
|
299
|
+
if (isToolCallBlock(block)) {
|
|
300
|
+
parts.push(buildToolPart(block, "call", opts));
|
|
301
|
+
} else if (isToolResultBlock(block)) {
|
|
302
|
+
parts.push(buildToolPart(block, "result", opts));
|
|
303
|
+
} else {
|
|
304
|
+
const text = blockToText(block, opts);
|
|
305
|
+
if (text.trim()) parts.push({ type: "text", text });
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return parts;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export function extractPartsFromPayload(payload, options = {}) {
|
|
312
|
+
if (!payload || typeof payload !== "object") return [];
|
|
313
|
+
const opts = { toolMaxChars: 2000, toolNameById: {}, ...options };
|
|
314
|
+
if (payload.message && typeof payload.message === "object") {
|
|
315
|
+
return extractPartsFromPayload(payload.message, opts);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const directType = normalizeType(payload.type || payload.kind || payload.role);
|
|
319
|
+
if (TOOL_CALL_TYPES.has(directType) || isToolCallBlock(payload)) {
|
|
320
|
+
return [buildToolPart(payload, "call", opts)];
|
|
321
|
+
}
|
|
322
|
+
if (TOOL_RESULT_TYPES.has(directType) || directType === "tool" || directType === "function") {
|
|
323
|
+
return [buildToolPart(payload, "result", opts)];
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const parts = [];
|
|
327
|
+
if (payload.content !== undefined) {
|
|
328
|
+
parts.push(...extractPartsFromContent(payload.content, opts));
|
|
329
|
+
}
|
|
330
|
+
for (const key of ["tool_calls", "toolCalls", "function_call", "functionCall", "tool_call", "toolCall"]) {
|
|
331
|
+
const value = payload[key];
|
|
332
|
+
if (!value) continue;
|
|
333
|
+
if (Array.isArray(value)) {
|
|
334
|
+
for (const block of value) parts.push(...extractPartsFromPayload(block, opts));
|
|
335
|
+
} else {
|
|
336
|
+
parts.push(...extractPartsFromPayload(value, opts));
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return parts;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function extractCaptureTurns(rolloutEntries, cfg = {}) {
|
|
343
|
+
const toolNameById = collectToolNamesByIdFromEntries(rolloutEntries);
|
|
344
|
+
const turns = [];
|
|
345
|
+
for (const entry of rolloutEntries || []) {
|
|
346
|
+
if (!entry || typeof entry !== "object") continue;
|
|
347
|
+
const payload = entry.payload && typeof entry.payload === "object" ? entry.payload : entry;
|
|
348
|
+
const message = payload.message && typeof payload.message === "object" ? payload.message : null;
|
|
349
|
+
const rawRole = message?.role || payload.role || payload.type || payload.kind;
|
|
350
|
+
const role = normalizeCaptureRole(rawRole);
|
|
351
|
+
if (!role) continue;
|
|
352
|
+
if (isAssistantSideCaptureRole(rawRole) && !cfg.captureAssistantTurns) continue;
|
|
353
|
+
|
|
354
|
+
const rawText = extractTextFromPayload(payload, { toolMaxChars: cfg.captureToolMaxChars });
|
|
355
|
+
const parts = extractPartsFromPayload(payload, {
|
|
356
|
+
toolMaxChars: cfg.captureToolMaxChars,
|
|
357
|
+
toolNameById,
|
|
358
|
+
});
|
|
359
|
+
const decision = shouldCaptureText(rawText, role, cfg);
|
|
360
|
+
if (!decision.shouldCapture && parts.length === 0) continue;
|
|
361
|
+
const text = decision.shouldCapture ? decision.text : "";
|
|
362
|
+
turns.push({ role, text, parts });
|
|
363
|
+
}
|
|
364
|
+
return turns;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export function normalizeCaptureRole(role) {
|
|
368
|
+
const value = normalizeType(role);
|
|
369
|
+
if (value === "user") return "user";
|
|
370
|
+
if (value === "assistant") return "assistant";
|
|
371
|
+
if (value === "tool" || value === "tool_result" || value === "function" || value === "function_call_output") {
|
|
372
|
+
return "user";
|
|
373
|
+
}
|
|
374
|
+
if (value === "tool_call" || value === "function_call") return "assistant";
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export function isAssistantSideCaptureRole(role) {
|
|
379
|
+
const value = normalizeType(role);
|
|
380
|
+
return value === "assistant" ||
|
|
381
|
+
value === "tool" ||
|
|
382
|
+
value === "tool_result" ||
|
|
383
|
+
value === "tool_call" ||
|
|
384
|
+
value === "function" ||
|
|
385
|
+
value === "function_call" ||
|
|
386
|
+
value === "function_call_output";
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function stripMetadataFences(text) {
|
|
390
|
+
return String(text || "").replace(/```(?:json)?\s*([\s\S]*?)```/gi, (match, body) => {
|
|
391
|
+
const lower = body.toLowerCase();
|
|
392
|
+
let hits = 0;
|
|
393
|
+
for (const key of METADATA_KEYS) {
|
|
394
|
+
const re = new RegExp(`["']?${key}["']?\\s*:`, "i");
|
|
395
|
+
if (re.test(lower)) hits += 1;
|
|
396
|
+
}
|
|
397
|
+
return hits >= 3 ? "" : match;
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function stripInjectedDigestBlocks(text) {
|
|
402
|
+
const lines = String(text || "").split(/\r?\n/);
|
|
403
|
+
const out = [];
|
|
404
|
+
let skipping = false;
|
|
405
|
+
let skipUntilMcpHint = false;
|
|
406
|
+
|
|
407
|
+
for (const line of lines) {
|
|
408
|
+
const trimmed = line.trim();
|
|
409
|
+
if (/^OpenViking session archive digest:/i.test(trimmed)) {
|
|
410
|
+
skipping = true;
|
|
411
|
+
skipUntilMcpHint = true;
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
if (/^OpenViking memory digest:/i.test(trimmed)) {
|
|
415
|
+
skipping = true;
|
|
416
|
+
skipUntilMcpHint = false;
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
if (skipping) {
|
|
420
|
+
if (skipUntilMcpHint) {
|
|
421
|
+
if (/^More detail: use the OpenViking MCP /i.test(trimmed)) {
|
|
422
|
+
skipping = false;
|
|
423
|
+
skipUntilMcpHint = false;
|
|
424
|
+
}
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
if (!trimmed) {
|
|
428
|
+
skipping = false;
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
if (
|
|
432
|
+
/^(?:[-*]\s+|#{1,6}\s+|More detail:|Use OpenViking MCP|Latest committed archive|Resume continuity|viking:\/\/)/i.test(trimmed) ||
|
|
433
|
+
/^\s{2,}\S/.test(line)
|
|
434
|
+
) {
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
skipping = false;
|
|
438
|
+
}
|
|
439
|
+
out.push(line);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return out.join("\n");
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export function sanitizeCapturedText(text) {
|
|
446
|
+
let value = String(text || "");
|
|
447
|
+
value = value
|
|
448
|
+
.replace(/\u0000/g, "")
|
|
449
|
+
.replace(/<openviking-context\b[^>]*>[\s\S]*?<\/openviking-context>/gi, " ")
|
|
450
|
+
.replace(/<relevant-memor(?:y|ies)\b[^>]*>[\s\S]*?<\/relevant-memor(?:y|ies)>/gi, " ")
|
|
451
|
+
.replace(/^\s*Sender\s*\([^)]+\)\s*```[\s\S]*?```\s*/gim, " ")
|
|
452
|
+
.replace(/^\s*Conversation (?:metadata|info):\s*```[\s\S]*?```\s*/gim, " ")
|
|
453
|
+
.replace(/^\s*\[?\d{4}-\d{2}-\d{2}[T ][^\]\n]{3,80}\]?\s*/gm, "")
|
|
454
|
+
.replace(/^\s*\d{10,13}\s+/gm, "");
|
|
455
|
+
value = stripMetadataFences(value);
|
|
456
|
+
value = stripInjectedDigestBlocks(value);
|
|
457
|
+
value = value.replace(/^\s*More detail: use the OpenViking MCP .*$/gim, " ");
|
|
458
|
+
return value
|
|
459
|
+
.replace(/[ \t]+\n/g, "\n")
|
|
460
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
461
|
+
.trim();
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function hasEnoughSignal(text) {
|
|
465
|
+
const cjk = text.match(/[\u3400-\u9fff]/g)?.length || 0;
|
|
466
|
+
const alnum = text.match(/[a-z0-9]/gi)?.length || 0;
|
|
467
|
+
return cjk >= 4 || alnum >= 6 || text.length >= 12;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function isPunctuationOnly(text) {
|
|
471
|
+
return !/[a-z0-9\u3400-\u9fff]/i.test(text);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
export function shouldCaptureText(text, role, cfg = {}) {
|
|
475
|
+
const maxLength = cfg.captureMaxLength || 24000;
|
|
476
|
+
const sanitized = sanitizeCapturedText(text);
|
|
477
|
+
if (!sanitized) return { shouldCapture: false, reason: "empty", text: "" };
|
|
478
|
+
|
|
479
|
+
const capped = truncateCaptureText(sanitized, maxLength);
|
|
480
|
+
const compact = oneLine(capped);
|
|
481
|
+
const isToolSummary = /^\[tool-(?:call|result)\b/i.test(compact);
|
|
482
|
+
|
|
483
|
+
if (!isToolSummary && role === "user" && SLASH_COMMAND_RE.test(compact)) {
|
|
484
|
+
return { shouldCapture: false, reason: "slash_command", text: "" };
|
|
485
|
+
}
|
|
486
|
+
if (!isToolSummary && ACK_RE.test(compact)) {
|
|
487
|
+
return { shouldCapture: false, reason: "ack", text: "" };
|
|
488
|
+
}
|
|
489
|
+
if (!isToolSummary && isPunctuationOnly(compact)) {
|
|
490
|
+
return { shouldCapture: false, reason: "punctuation", text: "" };
|
|
491
|
+
}
|
|
492
|
+
if (!isToolSummary && !hasEnoughSignal(compact)) {
|
|
493
|
+
return { shouldCapture: false, reason: "too_short", text: "" };
|
|
494
|
+
}
|
|
495
|
+
if (/^\[openviking-memory\]/i.test(compact)) {
|
|
496
|
+
return { shouldCapture: false, reason: "plugin_status", text: "" };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
return { shouldCapture: true, reason: "ok", text: capped };
|
|
500
|
+
}
|