@openviking/opencode-plugin 0.2.3 → 2026.9.9-1
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 +25 -7
- package/INSTALL.md +27 -7
- package/README.md +52 -13
- package/index.mjs +10 -3
- package/lib/config.mjs +50 -8
- package/lib/mcp-config.mjs +2 -1
- package/lib/memory-recall.mjs +19 -7
- package/lib/memory-session.mjs +61 -18
- package/lib/runtime.mjs +1 -0
- package/lib/shared/batch-send.mjs +2 -3
- package/lib/shared/capture-utils.mjs +99 -21
- package/lib/shared/credentials.mjs +32 -0
- package/lib/shared/input-filters.mjs +219 -0
- package/lib/shared/mcp-proxy-config.mjs +124 -0
- package/lib/shared/mcp-proxy-core.mjs +60 -2
- package/lib/shared/pending-queue.mjs +13 -3
- package/lib/shared/recall-compress-core.mjs +206 -0
- package/lib/shared/recall-core.mjs +394 -18
- package/lib/shared/retryable.mjs +9 -0
- package/lib/shared/setup-wizard.mjs +14 -11
- package/lib/shared/uri-guard.mjs +28 -6
- package/lib/shared/workspace-identity.mjs +334 -0
- package/lib/shared/workspace-peer.mjs +126 -8
- package/lib/utils.mjs +8 -1
- package/lib/viking-uri-guard.mjs +1 -1
- package/package.json +3 -2
- package/servers/mcp-proxy.mjs +16 -39
- package/servers/mcp-proxy.test.mjs +2 -0
- package/lib/shared/async-writer.mjs +0 -78
package/lib/memory-session.mjs
CHANGED
|
@@ -15,6 +15,9 @@ import {
|
|
|
15
15
|
import {
|
|
16
16
|
sendSessionMessages,
|
|
17
17
|
} from "./shared/batch-send.mjs"
|
|
18
|
+
import {
|
|
19
|
+
isRetryableFailure,
|
|
20
|
+
} from "./shared/retryable.mjs"
|
|
18
21
|
import {
|
|
19
22
|
log,
|
|
20
23
|
effectivePeerId,
|
|
@@ -27,9 +30,23 @@ export function createMemorySessionManager({ config, pluginRoot }) {
|
|
|
27
30
|
const statePath = path.join(pluginRoot, "openviking-session-state.json")
|
|
28
31
|
const oldSessionMapPath = path.join(pluginRoot, "openviking-session-map.json")
|
|
29
32
|
let saveTimer = null
|
|
33
|
+
// Serialize saves: concurrent saveState() calls (a debounced save racing
|
|
34
|
+
// with flushAll / flushSession / session deletion) all share the same
|
|
35
|
+
// `${statePath}.tmp` temp file, so one rename can fail with ENOENT after
|
|
36
|
+
// another already moved it. Chaining through a promise queue keeps at most
|
|
37
|
+
// one write+rename in flight. Each queued save re-serializes the in-memory
|
|
38
|
+
// `sessions` map at execution time, so the last save always persists the
|
|
39
|
+
// latest state.
|
|
40
|
+
let saveChain = Promise.resolve()
|
|
41
|
+
|
|
42
|
+
function enqueueSave() {
|
|
43
|
+
const run = saveChain.then(() => saveState())
|
|
44
|
+
saveChain = run.catch(() => {})
|
|
45
|
+
return run
|
|
46
|
+
}
|
|
30
47
|
|
|
31
48
|
async function init() {
|
|
32
|
-
await migrateLegacySessionMap()
|
|
49
|
+
if (config.autoCapture) await migrateLegacySessionMap()
|
|
33
50
|
await loadState()
|
|
34
51
|
const health = await fetchJSON(config, "/health", {}, { timeoutMs: 5000 })
|
|
35
52
|
if (health.ok) {
|
|
@@ -81,7 +98,7 @@ export function createMemorySessionManager({ config, pluginRoot }) {
|
|
|
81
98
|
function debouncedSaveState() {
|
|
82
99
|
if (saveTimer) clearTimeout(saveTimer)
|
|
83
100
|
saveTimer = setTimeout(() => {
|
|
84
|
-
|
|
101
|
+
enqueueSave().catch((error) => {
|
|
85
102
|
log("ERROR", "persistence", "Debounced save failed", { error: error?.message })
|
|
86
103
|
})
|
|
87
104
|
}, 300)
|
|
@@ -140,9 +157,9 @@ export function createMemorySessionManager({ config, pluginRoot }) {
|
|
|
140
157
|
await handleSessionCompacted(event)
|
|
141
158
|
} else if (event.type === "session.idle") {
|
|
142
159
|
await handleSessionIdle(event)
|
|
143
|
-
} else if (event.type === "message.updated") {
|
|
160
|
+
} else if (event.type === "message.updated" && config.autoCapture) {
|
|
144
161
|
await handleMessageUpdated(event)
|
|
145
|
-
} else if (event.type === "message.part.updated") {
|
|
162
|
+
} else if (event.type === "message.part.updated" && config.autoCapture) {
|
|
146
163
|
await handleMessagePartUpdated(event)
|
|
147
164
|
}
|
|
148
165
|
}
|
|
@@ -173,7 +190,7 @@ export function createMemorySessionManager({ config, pluginRoot }) {
|
|
|
173
190
|
if (!sessionId) return
|
|
174
191
|
await flushSession(sessionId, { commit: true, reason: event.type })
|
|
175
192
|
sessions.delete(sessionId)
|
|
176
|
-
await
|
|
193
|
+
await enqueueSave()
|
|
177
194
|
}
|
|
178
195
|
|
|
179
196
|
async function handleSessionError(event) {
|
|
@@ -247,10 +264,20 @@ export function createMemorySessionManager({ config, pluginRoot }) {
|
|
|
247
264
|
clearTimeout(saveTimer)
|
|
248
265
|
saveTimer = null
|
|
249
266
|
}
|
|
267
|
+
// Do not abort the whole dispose flush when one session fails (e.g. server
|
|
268
|
+
// already GC'd a stale session). Continue so later sessions still commit.
|
|
269
|
+
// See https://github.com/volcengine/OpenViking/issues/4490
|
|
250
270
|
for (const sessionId of sessions.keys()) {
|
|
251
|
-
|
|
271
|
+
try {
|
|
272
|
+
await flushSession(sessionId, { commit, reason: "flushAll" })
|
|
273
|
+
} catch (err) {
|
|
274
|
+
console.warn(
|
|
275
|
+
`[opencode-plugin] flushAll skipped session ${sessionId}:`,
|
|
276
|
+
err?.message || err,
|
|
277
|
+
)
|
|
278
|
+
}
|
|
252
279
|
}
|
|
253
|
-
await
|
|
280
|
+
await enqueueSave()
|
|
254
281
|
}
|
|
255
282
|
|
|
256
283
|
async function flushSession(opencodeSessionId, { commit = false, reason = "manual" } = {}) {
|
|
@@ -259,12 +286,12 @@ export function createMemorySessionManager({ config, pluginRoot }) {
|
|
|
259
286
|
if (!state) return false
|
|
260
287
|
|
|
261
288
|
const added = await flushPendingMessages(opencodeSessionId, state)
|
|
262
|
-
if (commit) {
|
|
289
|
+
if (commit && config.autoCapture) {
|
|
263
290
|
await commitOvSession(state.ovSessionId, { force: true, reason })
|
|
264
291
|
} else if (added > 0) {
|
|
265
292
|
await maybeCommitByThreshold(state)
|
|
266
293
|
}
|
|
267
|
-
await
|
|
294
|
+
await enqueueSave()
|
|
268
295
|
return true
|
|
269
296
|
}
|
|
270
297
|
|
|
@@ -361,6 +388,7 @@ export function createMemorySessionManager({ config, pluginRoot }) {
|
|
|
361
388
|
}
|
|
362
389
|
|
|
363
390
|
async function flushPendingMessages(opencodeSessionId, state) {
|
|
391
|
+
if (!config.autoCapture) return 0
|
|
364
392
|
const toSend = []
|
|
365
393
|
for (const [messageId, message] of state.messages.entries()) {
|
|
366
394
|
if (message.captured) continue
|
|
@@ -437,15 +465,35 @@ export function createMemorySessionManager({ config, pluginRoot }) {
|
|
|
437
465
|
for (const state of sessions.values()) {
|
|
438
466
|
if (state.ovSessionId === ovSessionId) state.lastCommitTime = Date.now()
|
|
439
467
|
}
|
|
440
|
-
|
|
441
|
-
|
|
468
|
+
const traceId = res.traceId || res.result?.trace_id
|
|
469
|
+
log("INFO", "session", "Committed OpenViking session", {
|
|
470
|
+
openviking_session: ovSessionId,
|
|
471
|
+
reason,
|
|
472
|
+
trace_id: traceId,
|
|
473
|
+
})
|
|
474
|
+
return { status: "accepted", result: res.result, traceId }
|
|
442
475
|
}
|
|
443
476
|
if (isRetryableFailure(res)) {
|
|
444
477
|
await enqueue("commitSession", ovSessionId, body)
|
|
445
|
-
log("WARN", "session", "Queued OpenViking session commit", {
|
|
478
|
+
log("WARN", "session", "Queued OpenViking session commit", {
|
|
479
|
+
openviking_session: ovSessionId,
|
|
480
|
+
reason,
|
|
481
|
+
trace_id: res.traceId,
|
|
482
|
+
status: res.status,
|
|
483
|
+
})
|
|
446
484
|
return { status: "queued" }
|
|
447
485
|
}
|
|
448
|
-
|
|
486
|
+
log("ERROR", "session", "Failed to commit OpenViking session", {
|
|
487
|
+
openviking_session: ovSessionId,
|
|
488
|
+
reason,
|
|
489
|
+
trace_id: res.traceId,
|
|
490
|
+
status: res.status,
|
|
491
|
+
error: res.error?.message || res.error?.code,
|
|
492
|
+
})
|
|
493
|
+
throw new Error(
|
|
494
|
+
`Failed to commit OpenViking session ${ovSessionId}: ${res.error?.message || res.status}` +
|
|
495
|
+
(res.traceId ? ` (trace_id=${res.traceId})` : ""),
|
|
496
|
+
)
|
|
449
497
|
}
|
|
450
498
|
|
|
451
499
|
async function migrateLegacySessionMap() {
|
|
@@ -474,9 +522,4 @@ export function createMemorySessionManager({ config, pluginRoot }) {
|
|
|
474
522
|
}
|
|
475
523
|
}
|
|
476
524
|
|
|
477
|
-
function isRetryableFailure(res) {
|
|
478
|
-
if (!res || res.ok) return false
|
|
479
|
-
const status = Number(res.status || 0)
|
|
480
|
-
return !status || status >= 500 || status === 408 || status === 429
|
|
481
|
-
}
|
|
482
525
|
}
|
package/lib/runtime.mjs
CHANGED
|
@@ -6,6 +6,7 @@ export async function checkServiceHealth(config, timeoutMs = 3000) {
|
|
|
6
6
|
try {
|
|
7
7
|
const response = await fetch(`${normalizeEndpoint(config.endpoint)}/health`, {
|
|
8
8
|
method: "GET",
|
|
9
|
+
headers: config.userAgent ? { "User-Agent": config.userAgent } : undefined,
|
|
9
10
|
signal: controller.signal,
|
|
10
11
|
})
|
|
11
12
|
return response.ok
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
// GENERATED FROM examples/memory-plugin-shared/lib. DO NOT EDIT.
|
|
2
2
|
import { enqueue } from "./pending-queue.mjs";
|
|
3
|
+
import { isRetryableFailure } from "./retryable.mjs";
|
|
3
4
|
|
|
4
5
|
export const BATCH_LIMIT = 100;
|
|
5
6
|
|
|
6
7
|
export function isRetryableSendFailure(res) {
|
|
7
|
-
|
|
8
|
-
const status = Number(res.status || 0);
|
|
9
|
-
return !status || status === 408 || status === 429 || status >= 500;
|
|
8
|
+
return isRetryableFailure(res);
|
|
10
9
|
}
|
|
11
10
|
|
|
12
11
|
function makeResult() {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
// GENERATED FROM examples/memory-plugin-shared/lib. DO NOT EDIT.
|
|
2
|
+
import { applyInputFilters, compileInputFilters } from "./input-filters.mjs";
|
|
3
|
+
|
|
2
4
|
const TEXT_BLOCK_TYPES = new Set(["text", "input_text", "output_text"]);
|
|
3
5
|
const TOOL_CALL_TYPES = new Set([
|
|
4
6
|
"tool_call",
|
|
@@ -17,6 +19,11 @@ const TOOL_RESULT_TYPES = new Set([
|
|
|
17
19
|
"functioncalloutput",
|
|
18
20
|
]);
|
|
19
21
|
|
|
22
|
+
// Tool output is reported verbatim; the server owns truncation via
|
|
23
|
+
// tool_output_externalization (threshold_chars, default 20000). This cap only
|
|
24
|
+
// guards against pathological payloads.
|
|
25
|
+
const DEFAULT_TOOL_MAX_CHARS = 1000000;
|
|
26
|
+
|
|
20
27
|
const ACK_RE = /^(?:ok|okay|k|yes|yep|no|nope|thanks|thank you|thx|done|收到|好的|好|嗯|可以|继续|不用|不需要|没了|好了)[.!?。!?\s]*$/i;
|
|
21
28
|
const SLASH_COMMAND_RE = /^\/[a-z0-9_-]{1,64}\b/i;
|
|
22
29
|
const METADATA_KEYS = [
|
|
@@ -97,9 +104,9 @@ function toolName(block) {
|
|
|
97
104
|
block?.name ||
|
|
98
105
|
block?.tool_name ||
|
|
99
106
|
block?.toolName ||
|
|
107
|
+
block?.tool ||
|
|
100
108
|
block?.function?.name ||
|
|
101
109
|
block?.call?.name ||
|
|
102
|
-
block?.id ||
|
|
103
110
|
"",
|
|
104
111
|
);
|
|
105
112
|
}
|
|
@@ -108,6 +115,7 @@ function toolPayload(block, kind) {
|
|
|
108
115
|
if (!block || typeof block !== "object") return "";
|
|
109
116
|
if (kind === "call") {
|
|
110
117
|
return block.input ??
|
|
118
|
+
block.state?.input ??
|
|
111
119
|
block.arguments ??
|
|
112
120
|
block.args ??
|
|
113
121
|
block.params ??
|
|
@@ -118,8 +126,10 @@ function toolPayload(block, kind) {
|
|
|
118
126
|
"";
|
|
119
127
|
}
|
|
120
128
|
return block.output ??
|
|
129
|
+
block.state?.output ??
|
|
121
130
|
block.result ??
|
|
122
131
|
block.error ??
|
|
132
|
+
block.state?.error ??
|
|
123
133
|
block.data ??
|
|
124
134
|
block.content ??
|
|
125
135
|
block.text ??
|
|
@@ -128,27 +138,36 @@ function toolPayload(block, kind) {
|
|
|
128
138
|
|
|
129
139
|
function toolId(block) {
|
|
130
140
|
return oneLine(
|
|
131
|
-
block?.id ||
|
|
132
141
|
block?.call_id ||
|
|
133
142
|
block?.callId ||
|
|
143
|
+
block?.callID ||
|
|
134
144
|
block?.tool_call_id ||
|
|
135
145
|
block?.toolCallId ||
|
|
136
146
|
block?.tool_use_id ||
|
|
137
147
|
block?.toolUseId ||
|
|
138
148
|
block?.function_call_id ||
|
|
139
149
|
block?.functionCallId ||
|
|
150
|
+
block?.id ||
|
|
140
151
|
"",
|
|
141
152
|
);
|
|
142
153
|
}
|
|
143
154
|
|
|
144
155
|
function toolStatus(block, kind) {
|
|
145
156
|
if (kind === "call") return "running";
|
|
146
|
-
if (block?.is_error || block?.error) return "error";
|
|
147
|
-
const status = oneLine(block?.status || "");
|
|
157
|
+
if (block?.is_error || block?.isError || block?.state?.isError || block?.error || block?.state?.error) return "error";
|
|
158
|
+
const status = oneLine(block?.status || block?.state?.status || "");
|
|
148
159
|
return status || "completed";
|
|
149
160
|
}
|
|
150
161
|
|
|
151
|
-
function
|
|
162
|
+
function setToolInput(part, payload) {
|
|
163
|
+
const input = parseMaybeJson(payload);
|
|
164
|
+
if (input === "" || input == null) return;
|
|
165
|
+
part.tool_input = typeof input === "object" && !Array.isArray(input)
|
|
166
|
+
? input
|
|
167
|
+
: { value: input };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function buildToolPart(block, kind, { toolMaxChars = DEFAULT_TOOL_MAX_CHARS, toolNameById = {} } = {}) {
|
|
152
171
|
const id = toolId(block);
|
|
153
172
|
const name = toolName(block) || (id ? toolNameById[id] : "");
|
|
154
173
|
const payload = toolPayload(block, kind);
|
|
@@ -159,13 +178,11 @@ function buildToolPart(block, kind, { toolMaxChars = 2000, toolNameById = {} } =
|
|
|
159
178
|
tool_status: toolStatus(block, kind),
|
|
160
179
|
};
|
|
161
180
|
if (kind === "call") {
|
|
162
|
-
|
|
163
|
-
if (input !== "" && input != null) {
|
|
164
|
-
part.tool_input = typeof input === "object" && !Array.isArray(input)
|
|
165
|
-
? input
|
|
166
|
-
: { value: input };
|
|
167
|
-
}
|
|
181
|
+
setToolInput(part, payload);
|
|
168
182
|
} else {
|
|
183
|
+
if (block?.state?.input !== undefined) {
|
|
184
|
+
setToolInput(part, block.state.input);
|
|
185
|
+
}
|
|
169
186
|
part.tool_output = stringifyCompact(payload, toolMaxChars);
|
|
170
187
|
}
|
|
171
188
|
return part;
|
|
@@ -202,7 +219,7 @@ function blockToText(block, options) {
|
|
|
202
219
|
}
|
|
203
220
|
|
|
204
221
|
export function extractTextFromContent(content, options = {}) {
|
|
205
|
-
const opts = { toolMaxChars:
|
|
222
|
+
const opts = { toolMaxChars: DEFAULT_TOOL_MAX_CHARS, ...options };
|
|
206
223
|
if (!content) return "";
|
|
207
224
|
if (typeof content === "string") return content;
|
|
208
225
|
if (Array.isArray(content)) {
|
|
@@ -222,7 +239,7 @@ export function extractTextFromPayload(payload, options = {}) {
|
|
|
222
239
|
const chunks = [];
|
|
223
240
|
const directType = normalizeType(payload.type || payload.kind || payload.role);
|
|
224
241
|
if (TOOL_RESULT_TYPES.has(directType) || directType === "tool" || TOOL_CALL_TYPES.has(directType)) {
|
|
225
|
-
const direct = blockToText(payload, { toolMaxChars:
|
|
242
|
+
const direct = blockToText(payload, { toolMaxChars: DEFAULT_TOOL_MAX_CHARS, ...options });
|
|
226
243
|
if (direct) return direct;
|
|
227
244
|
}
|
|
228
245
|
|
|
@@ -242,7 +259,7 @@ export function extractTextFromPayload(payload, options = {}) {
|
|
|
242
259
|
}
|
|
243
260
|
|
|
244
261
|
if (chunks.length === 0) {
|
|
245
|
-
const direct = blockToText(payload, { toolMaxChars:
|
|
262
|
+
const direct = blockToText(payload, { toolMaxChars: DEFAULT_TOOL_MAX_CHARS, ...options });
|
|
246
263
|
if (direct) chunks.push(direct);
|
|
247
264
|
}
|
|
248
265
|
|
|
@@ -282,7 +299,7 @@ function collectToolNamesByIdFromPayload(payload, out) {
|
|
|
282
299
|
}
|
|
283
300
|
|
|
284
301
|
function extractPartsFromContent(content, options = {}) {
|
|
285
|
-
const opts = { toolMaxChars:
|
|
302
|
+
const opts = { toolMaxChars: DEFAULT_TOOL_MAX_CHARS, toolNameById: {}, ...options };
|
|
286
303
|
const parts = [];
|
|
287
304
|
if (!content) return parts;
|
|
288
305
|
if (typeof content === "string") {
|
|
@@ -310,7 +327,7 @@ function extractPartsFromContent(content, options = {}) {
|
|
|
310
327
|
|
|
311
328
|
export function extractPartsFromPayload(payload, options = {}) {
|
|
312
329
|
if (!payload || typeof payload !== "object") return [];
|
|
313
|
-
const opts = { toolMaxChars:
|
|
330
|
+
const opts = { toolMaxChars: DEFAULT_TOOL_MAX_CHARS, toolNameById: {}, ...options };
|
|
314
331
|
if (payload.message && typeof payload.message === "object") {
|
|
315
332
|
return extractPartsFromPayload(payload.message, opts);
|
|
316
333
|
}
|
|
@@ -339,6 +356,53 @@ export function extractPartsFromPayload(payload, options = {}) {
|
|
|
339
356
|
return parts;
|
|
340
357
|
}
|
|
341
358
|
|
|
359
|
+
/**
|
|
360
|
+
* Prune blank text parts and run `cfg.captureFilters` over the ones that remain.
|
|
361
|
+
*
|
|
362
|
+
* One drop verdict is taken per turn, on the aggregate of its text parts; the
|
|
363
|
+
* individual parts are then rewritten with the substitutions only, so a rule can
|
|
364
|
+
* never both keep and drop the same turn. Tool call/result parts are carried
|
|
365
|
+
* through untouched — filters match conversation text, not tool payloads — but a
|
|
366
|
+
* turn dropped on its text takes its tool parts with it.
|
|
367
|
+
*/
|
|
368
|
+
export function filterCaptureParts(parts, role, cfg = {}) {
|
|
369
|
+
const kept = [];
|
|
370
|
+
for (const part of parts || []) {
|
|
371
|
+
if (!part) continue;
|
|
372
|
+
if (part.type !== "text") {
|
|
373
|
+
kept.push(part);
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
const text = typeof part.text === "string" ? part.text.trim() : "";
|
|
377
|
+
if (!text) continue;
|
|
378
|
+
kept.push(text === part.text ? part : { ...part, text });
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const blank = { parts: kept, dropped: false };
|
|
382
|
+
const compiled = compileInputFilters(cfg?.captureFilters);
|
|
383
|
+
if (!compiled.rules.length || kept.length === 0) return blank;
|
|
384
|
+
|
|
385
|
+
const textParts = kept.filter((part) => part.type === "text");
|
|
386
|
+
if (textParts.length === 0) return blank;
|
|
387
|
+
|
|
388
|
+
const verdict = applyInputFilters(textParts.map((part) => part.text).join("\n\n"), compiled.rules, {
|
|
389
|
+
role,
|
|
390
|
+
});
|
|
391
|
+
if (verdict.dropped) return { parts: [], dropped: true };
|
|
392
|
+
|
|
393
|
+
const out = [];
|
|
394
|
+
for (const part of kept) {
|
|
395
|
+
if (part.type !== "text") {
|
|
396
|
+
out.push(part);
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
const shaped = applyInputFilters(part.text, compiled.rules, { role, substituteOnly: true });
|
|
400
|
+
if (!shaped.text) continue;
|
|
401
|
+
out.push(shaped.text === part.text ? part : { ...part, text: shaped.text });
|
|
402
|
+
}
|
|
403
|
+
return { parts: out, dropped: out.length === 0 };
|
|
404
|
+
}
|
|
405
|
+
|
|
342
406
|
export function extractCaptureTurns(rolloutEntries, cfg = {}) {
|
|
343
407
|
const toolNameById = collectToolNamesByIdFromEntries(rolloutEntries);
|
|
344
408
|
const turns = [];
|
|
@@ -356,10 +420,15 @@ export function extractCaptureTurns(rolloutEntries, cfg = {}) {
|
|
|
356
420
|
toolMaxChars: cfg.captureToolMaxChars,
|
|
357
421
|
toolNameById,
|
|
358
422
|
});
|
|
359
|
-
const
|
|
360
|
-
if (
|
|
423
|
+
const shaped = filterCaptureParts(parts, role, cfg);
|
|
424
|
+
if (shaped.dropped) continue;
|
|
425
|
+
// With parts on the wire the drop decision was already taken above, so the
|
|
426
|
+
// text path only runs the filters for the `content` fallback. That also
|
|
427
|
+
// keeps `turn.text` faithful for callers that scan it for trigger words.
|
|
428
|
+
const decision = shouldCaptureText(rawText, role, cfg, { filters: shaped.parts.length === 0 });
|
|
429
|
+
if (!decision.shouldCapture && shaped.parts.length === 0) continue;
|
|
361
430
|
const text = decision.shouldCapture ? decision.text : "";
|
|
362
|
-
turns.push({ role, text, parts });
|
|
431
|
+
turns.push({ role, text, parts: shaped.parts });
|
|
363
432
|
}
|
|
364
433
|
return turns;
|
|
365
434
|
}
|
|
@@ -471,12 +540,21 @@ function isPunctuationOnly(text) {
|
|
|
471
540
|
return !/[a-z0-9\u3400-\u9fff]/i.test(text);
|
|
472
541
|
}
|
|
473
542
|
|
|
474
|
-
export function shouldCaptureText(text, role, cfg = {}) {
|
|
543
|
+
export function shouldCaptureText(text, role, cfg = {}, { filters = true } = {}) {
|
|
475
544
|
const maxLength = cfg.captureMaxLength || 24000;
|
|
476
545
|
const sanitized = sanitizeCapturedText(text);
|
|
477
546
|
if (!sanitized) return { shouldCapture: false, reason: "empty", text: "" };
|
|
478
547
|
|
|
479
|
-
|
|
548
|
+
let capped = truncateCaptureText(sanitized, maxLength);
|
|
549
|
+
if (filters) {
|
|
550
|
+
const compiled = compileInputFilters(cfg?.captureFilters);
|
|
551
|
+
if (compiled.rules.length) {
|
|
552
|
+
const verdict = applyInputFilters(capped, compiled.rules, { role });
|
|
553
|
+
if (verdict.dropped) return { shouldCapture: false, reason: "filtered", text: "" };
|
|
554
|
+
capped = verdict.text;
|
|
555
|
+
if (!capped) return { shouldCapture: false, reason: "empty", text: "" };
|
|
556
|
+
}
|
|
557
|
+
}
|
|
480
558
|
const compact = oneLine(capped);
|
|
481
559
|
const isToolSummary = /^\[tool-(?:call|result)\b/i.test(compact);
|
|
482
560
|
|
|
@@ -30,6 +30,27 @@ function tryLoadJson(path) {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Build the User-Agent every harness plugin sends on OpenViking-bound requests.
|
|
35
|
+
* Shape is `name/semver` so downstream stats layers can parse it as one token.
|
|
36
|
+
*/
|
|
37
|
+
export function buildUserAgent(harness, version) {
|
|
38
|
+
return `openviking-memory-${harness}/${str(version, "") || "0.0.0"}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Read a plugin manifest's `version` field. Accepts a path or a URL (so callers
|
|
43
|
+
* can resolve relative to import.meta.url). Returns "" when unreadable so the
|
|
44
|
+
* User-Agent falls back to 0.0.0 instead of throwing inside a short-lived hook.
|
|
45
|
+
*/
|
|
46
|
+
export function readManifestVersion(manifest) {
|
|
47
|
+
try {
|
|
48
|
+
return str(JSON.parse(readFileSync(manifest, "utf-8")).version, "");
|
|
49
|
+
} catch {
|
|
50
|
+
return "";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
33
54
|
function looksLikeOvcli(obj) {
|
|
34
55
|
if (!obj || typeof obj !== "object") return false;
|
|
35
56
|
if (obj.server && typeof obj.server === "object") return false;
|
|
@@ -171,9 +192,20 @@ export function resolveOpenVikingCredentials(env = process.env) {
|
|
|
171
192
|
const explicitMcpUrl = str(env.OPENVIKING_MCP_URL, "");
|
|
172
193
|
const mcpUrl = (mode !== "cli" && explicitMcpUrl) ? explicitMcpUrl : `${baseUrl.replace(/\/+$/, "")}/mcp`;
|
|
173
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
|
+
|
|
174
205
|
return {
|
|
175
206
|
...files,
|
|
176
207
|
credentialSource: useCli ? "ovcli" : ((mode === "env" || envHasCredentials) ? "env" : "auto"),
|
|
208
|
+
credentialPath,
|
|
177
209
|
baseUrl,
|
|
178
210
|
mcpUrl,
|
|
179
211
|
apiKey,
|