@openviking/opencode-plugin 2026.10.15 → 2026.10.19

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 CHANGED
@@ -178,7 +178,7 @@ OpenCode 的 `mcp.openviking` 配置。
178
178
  进入新的 OpenCode session 后,可以让 agent 浏览 OpenViking memory,或搜索一个已索引的资源。插件应暴露 OpenViking MCP server,OpenCode 中的工具名会带 `openviking_` 前缀:
179
179
 
180
180
  - `openviking_search`、`openviking_find`
181
- - `openviking_read`、`openviking_list`、`openviking_tree`、`openviking_grep`、`openviking_glob`
181
+ - `openviking_read`、`openviking_download`、`openviking_list`、`openviking_tree`、`openviking_grep`、`openviking_glob`
182
182
  - `openviking_remember`、`openviking_write`、`openviking_edit`、`openviking_add_resource`、`openviking_add_skill`
183
183
  - `openviking_list_watches`、`openviking_cancel_watch`、`openviking_forget`、`openviking_health`
184
184
 
@@ -203,6 +203,7 @@ curl http://localhost:1933/health
203
203
  - `openviking_find`:快速语义检索
204
204
  - `openviking_remember`:存储重要事实或决策,供记忆提取
205
205
  - `openviking_read`:读取一个或多个 `viking://` 文件
206
+ - `openviking_download`:获取一次性 URL,用 `curl` 下载文件原始字节
206
207
  - `openviking_list`:列出 `viking://` 目录
207
208
  - `openviking_tree`:展示 `viking://` 目录树
208
209
  - `openviking_grep`:精确文本或正则搜索
package/INSTALL.md CHANGED
@@ -175,7 +175,7 @@ Restart OpenCode after changing plugin or OpenViking configuration.
175
175
  In a new OpenCode session, ask the agent to browse OpenViking memory or search for a known indexed resource. The plugin should expose the OpenViking MCP server, with tools namespaced by OpenCode as `openviking_*`:
176
176
 
177
177
  - `openviking_search`, `openviking_find`
178
- - `openviking_read`, `openviking_list`, `openviking_tree`, `openviking_grep`, `openviking_glob`
178
+ - `openviking_read`, `openviking_download`, `openviking_list`, `openviking_tree`, `openviking_grep`, `openviking_glob`
179
179
  - `openviking_remember`, `openviking_write`, `openviking_edit`, `openviking_add_resource`, `openviking_add_skill`
180
180
  - `openviking_list_watches`, `openviking_cancel_watch`, `openviking_forget`, `openviking_health`
181
181
 
@@ -200,6 +200,7 @@ The plugin registers OpenViking's stdio MCP proxy through OpenCode config. The s
200
200
  - `openviking_find`: fast semantic retrieval.
201
201
  - `openviking_remember`: store important facts or decisions for memory extraction.
202
202
  - `openviking_read`: read one or more `viking://` files.
203
+ - `openviking_download`: get a one-time URL for fetching a file's original bytes with `curl`.
203
204
  - `openviking_list`: list a `viking://` directory.
204
205
  - `openviking_tree`: show a `viking://` directory tree.
205
206
  - `openviking_grep`: exact text or regex search.
package/README.md CHANGED
@@ -226,6 +226,7 @@ OpenCode sees the OpenViking MCP server as `openviking`, so tool names are names
226
226
  - `openviking_find`: fast semantic retrieval.
227
227
  - `openviking_remember`: store important facts or decisions for memory extraction.
228
228
  - `openviking_read`: read one or more `viking://` files.
229
+ - `openviking_download`: get a one-time URL for fetching a file's original bytes with `curl`.
229
230
  - `openviking_list`: list a `viking://` directory.
230
231
  - `openviking_tree`: show a `viking://` directory tree.
231
232
  - `openviking_grep`: exact text or regex search.
@@ -316,7 +316,11 @@ export function createMemorySessionManager({ config, pluginRoot }) {
316
316
  const run = (state.flushChain ?? Promise.resolve()).then(async () => {
317
317
  const added = await flushPendingMessages(opencodeSessionId, state)
318
318
  if (commit && isCaptureEnabled(config)) {
319
- await commitOvSession(state.ovSessionId, { force: true, reason })
319
+ const result = await commitOvSession(state.ovSessionId, { force: true, reason })
320
+ // Server lost the session (reset/reinstall); drop the stale mapping.
321
+ if (result.status === "not_found" && sessions.get(opencodeSessionId) === state) {
322
+ sessions.delete(opencodeSessionId)
323
+ }
320
324
  } else if (added > 0) {
321
325
  await maybeCommitByThreshold(state)
322
326
  }
@@ -346,7 +350,10 @@ export function createMemorySessionManager({ config, pluginRoot }) {
346
350
  }
347
351
 
348
352
  function createSessionState(opencodeSessionId, event = {}) {
349
- const parentId = event?.properties?.info?.parentID ?? event?.properties?.parentID ?? event?.parentID ?? ""
353
+ const isSessionEvent = typeof event?.type === "string" && event.type.startsWith("session.")
354
+ const parentId = isSessionEvent
355
+ ? (event?.properties?.info?.parentID ?? event?.properties?.parentID ?? event?.parentID ?? "")
356
+ : ""
350
357
  const ovSessionId = parentId
351
358
  ? deriveHarnessSessionId("oc-", parentId, `subagent-${opencodeSessionId}`)
352
359
  : deriveHarnessSessionId("oc-", opencodeSessionId)
@@ -515,6 +522,14 @@ export function createMemorySessionManager({ config, pluginRoot }) {
515
522
  })
516
523
  return { status: "queued" }
517
524
  }
525
+ if (res.status === 404) {
526
+ log("WARN", "session", "OpenViking session not found", {
527
+ openviking_session: ovSessionId,
528
+ reason,
529
+ trace_id: res.traceId,
530
+ })
531
+ return { status: "not_found" }
532
+ }
518
533
  log("ERROR", "session", "Failed to commit OpenViking session", {
519
534
  openviking_session: ovSessionId,
520
535
  reason,
@@ -362,14 +362,41 @@ export function extractPartsFromPayload(payload, options = {}) {
362
362
  return parts;
363
363
  }
364
364
 
365
+ function substituteValue(value, rules, role) {
366
+ if (typeof value === "string") {
367
+ let current = value;
368
+ for (const rule of rules) {
369
+ if (rule.op !== "s" || (rule.scope && rule.scope !== role)) continue;
370
+ current = current.replace(rule.re, rule.replacement);
371
+ }
372
+ return current;
373
+ }
374
+ if (Array.isArray(value)) return value.map((item) => substituteValue(item, rules, role));
375
+ if (value && typeof value === "object") {
376
+ return Object.fromEntries(
377
+ Object.entries(value).map(([key, item]) => [key, substituteValue(item, rules, role)]),
378
+ );
379
+ }
380
+ return value;
381
+ }
382
+
383
+ function substituteToolPart(part, rules, role) {
384
+ if (part.type !== "tool") return part;
385
+ const out = { ...part };
386
+ if (part.tool_input !== undefined) out.tool_input = substituteValue(part.tool_input, rules, role);
387
+ if (part.tool_output !== undefined) out.tool_output = substituteValue(part.tool_output, rules, role);
388
+ return out;
389
+ }
390
+
365
391
  /**
366
392
  * Prune blank text parts and run `cfg.captureFilters` over the ones that remain.
367
393
  *
368
394
  * One drop verdict is taken per turn, on the aggregate of its text parts; the
369
395
  * individual parts are then rewritten with the substitutions only, so a rule can
370
- * never both keep and drop the same turn. Tool call/result parts are carried
371
- * through untouched — filters match conversation text, not tool payloads — but a
372
- * turn dropped on its text takes its tool parts with it.
396
+ * never both keep and drop the same turn. Tool call/result parts get the
397
+ * substitutions too, on their input values and output, so a redaction rule also
398
+ * covers a secret that reached a command or its output; only text parts decide
399
+ * the drop verdict, and a turn dropped on its text takes its tool parts with it.
373
400
  */
374
401
  export function filterCaptureParts(parts, role, cfg = {}) {
375
402
  const kept = [];
@@ -389,7 +416,9 @@ export function filterCaptureParts(parts, role, cfg = {}) {
389
416
  if (!compiled.rules.length || kept.length === 0) return blank;
390
417
 
391
418
  const textParts = kept.filter((part) => part.type === "text");
392
- if (textParts.length === 0) return blank;
419
+ if (textParts.length === 0) {
420
+ return { parts: kept.map((part) => substituteToolPart(part, compiled.rules, role)), dropped: false };
421
+ }
393
422
 
394
423
  const verdict = applyInputFilters(textParts.map((part) => part.text).join("\n\n"), compiled.rules, {
395
424
  role,
@@ -399,7 +428,7 @@ export function filterCaptureParts(parts, role, cfg = {}) {
399
428
  const out = [];
400
429
  for (const part of kept) {
401
430
  if (part.type !== "text") {
402
- out.push(part);
431
+ out.push(substituteToolPart(part, compiled.rules, role));
403
432
  continue;
404
433
  }
405
434
  const shaped = applyInputFilters(part.text, compiled.rules, { role, substituteOnly: true });
@@ -431,7 +460,7 @@ function faithfulCaptureDecision(text, cfg) {
431
460
  /**
432
461
  * Shape one host message before an adapter builds its session payload.
433
462
  * Sanitize injected context before evaluating keep/drop rules, so text that
434
- * will not be sent cannot satisfy a keep rule. Tool parts are never filtered.
463
+ * will not be sent cannot satisfy a keep rule. Tool parts never decide keep or drop.
435
464
  */
436
465
  export function shapeCapturePayload(payload, role, cfg = {}, { toolNameById = {}, faithful = false } = {}) {
437
466
  const options = { toolMaxChars: cfg.captureToolMaxChars, toolNameById };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openviking/opencode-plugin",
3
- "version": "2026.10.15",
3
+ "version": "2026.10.19",
4
4
  "author": "@openviking",
5
5
  "description": "Unified OpenCode plugin for OpenViking repository retrieval and long-term memory",
6
6
  "type": "module",
@@ -46,5 +46,5 @@
46
46
  "code-search"
47
47
  ],
48
48
  "license": "Apache-2.0",
49
- "openvikingSourceCommit": "60629f92bfd7d05c67bc50aa452f852acbc77c39"
49
+ "openvikingSourceCommit": "66f528de3ce20770c24335535137e6a1cecfe29f"
50
50
  }