@openviking/opencode-plugin 2026.9.24 → 2026.9.25-2

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
@@ -11,7 +11,7 @@
11
11
 
12
12
  需要先准备:
13
13
 
14
- - OpenCode
14
+ - OpenCode 1.15.7+ 或 OpenCode 2.0.15+
15
15
  - OpenViking HTTP Server
16
16
  - Node.js 18+
17
17
  - 如果服务端启用了认证,需要可用的 OpenViking API Key
@@ -38,6 +38,8 @@ curl http://localhost:1933/health
38
38
  }
39
39
  ```
40
40
 
41
+ 这份 `plugin` 配置同时兼容 v1 和 v2;v2 会在加载时规范化为原生的 `plugins`,无需手工迁移配置。
42
+
41
43
  ## 安装方式二:源码安装
42
44
 
43
45
  用于开发调试或 PR 测试。OpenCode 推荐插件目录:
package/INSTALL.md CHANGED
@@ -11,7 +11,7 @@ This is the only OpenCode plugin example maintained in this repository. It does
11
11
 
12
12
  Prepare the following first:
13
13
 
14
- - OpenCode
14
+ - OpenCode 1.15.7+ or OpenCode 2.0.15+
15
15
  - OpenViking HTTP Server
16
16
  - Node.js 18+
17
17
  - A valid OpenViking API key if authentication is enabled on the server
@@ -38,6 +38,8 @@ Normal users are recommended to enable it through OpenCode's package plugin mech
38
38
  }
39
39
  ```
40
40
 
41
+ This `plugin` entry works on both v1 and v2. OpenCode v2 normalizes it to the native `plugins` form at load time, so no manual config migration is required.
42
+
41
43
  ## Installation Method 2: Source Install
42
44
 
43
45
  Use this method for development, debugging, or PR testing. OpenCode's recommended plugin directory is:
package/README.md CHANGED
@@ -17,6 +17,7 @@ The plugin uses OpenCode hooks for lifecycle behavior and registers OpenViking's
17
17
  - Exposes the same OpenViking MCP tools used by the Claude Code and Codex memory plugins.
18
18
  - Maps each OpenCode session to an OpenViking session.
19
19
  - Captures user and assistant text messages into OpenViking.
20
+ - On OpenCode v2, reads the structured session context at execution boundaries so tool calls and results are captured too.
20
21
  - Commits sessions at lifecycle boundaries for memory extraction.
21
22
  - Automatically recalls relevant memories and injects them as hidden synthetic context for the current user message.
22
23
  - Blocks accidental local filesystem reads of `viking://` URIs and points the agent back to `openviking_read`, `openviking_glob`, or `openviking_search`. Shell commands that carry a `viking://` URI still run, with a notice appended to their output.
@@ -50,11 +51,13 @@ There is intentionally no `skills/openviking/SKILL.md`. The tool surface comes f
50
51
 
51
52
  ## Requirements
52
53
 
53
- - OpenCode
54
+ - OpenCode 1.15.7+ or OpenCode 2.0.15+
54
55
  - OpenViking HTTP server
55
56
  - Node.js 18+
56
57
  - An OpenViking API key if your server requires authentication
57
58
 
59
+ The same package serves both plugin APIs. OpenCode 1 calls `server()` (or the named `OpenVikingPlugin` export), while OpenCode 2 calls `setup()`. The v2 entrypoint does not import `@opencode/plugin`, so a source install on OpenCode 1 still loads. OpenCode 2 normalizes the installer's `"plugin"` key to `"plugins"`; keep `"plugin"` in shared v1/v2 configuration. On v2, the bundled MCP server uses `codemode: false`, preserving the documented `openviking_*` tool names.
60
+
58
61
  Start OpenViking first:
59
62
 
60
63
  ```bash
@@ -202,6 +205,8 @@ call and points it to the OpenViking MCP tools. A `bash` command that contains a
202
205
  an HTTP payload); the command runs and the plugin appends a notice naming the
203
206
  MCP tools to its output.
204
207
 
208
+ OpenCode v2 has no plugin toast API. Service availability and other non-fatal notices are written to `openviking-memory.log`. The plugin captures the transcript before compaction discards it, checks the commit threshold after every execution (succeeded, failed, or interrupted), and forces a commit after completed compaction, session deletion, and cleanup. Each plugin instance handles only the sessions of its own location, and keeps its capture cursor in OpenCode plugin storage so a reloaded instance does not resend earlier turns.
209
+
205
210
  ## MCP Tools
206
211
 
207
212
  OpenCode sees the OpenViking MCP server as `openviking`, so tool names are namespaced with `openviking_`.
package/index.mjs CHANGED
@@ -1,41 +1,30 @@
1
1
  import { dirname } from "path"
2
2
  import { fileURLToPath } from "url"
3
- import { initializeRuntime } from "./lib/runtime.mjs"
4
- import { createRepoContext } from "./lib/repo-context.mjs"
5
- import { createMemorySessionManager } from "./lib/memory-session.mjs"
6
- import { createMemoryRecall } from "./lib/memory-recall.mjs"
7
- import { createSessionInject } from "./lib/session-inject.mjs"
8
- import { createVikingUriGuard, createVikingUriNotice } from "./lib/viking-uri-guard.mjs"
3
+ import { createOpenVikingRuntime } from "./lib/plugin-runtime.mjs"
4
+ import { startV2Plugin } from "./lib/v2-plugin.mjs"
9
5
  import { injectOpenVikingMcpConfig } from "./lib/mcp-config.mjs"
10
- import { loadConfig, resolveDataDir } from "./lib/config.mjs"
11
6
  import { isRecallEnabled } from "./lib/shared/recall-core.mjs"
12
- import { initLogger, log, makeToast } from "./lib/utils.mjs"
7
+ import { log } from "./lib/utils.mjs"
13
8
 
14
9
  const pluginRoot = dirname(fileURLToPath(import.meta.url))
15
10
 
16
- export async function OpenVikingPlugin({ client, directory }) {
17
- const config = loadConfig(pluginRoot, directory)
18
- const dataDir = resolveDataDir(pluginRoot, config)
19
- initLogger(dataDir)
20
-
21
- if (!config.enabled) {
22
- log("INFO", "plugin", "OpenViking plugin is disabled in configuration")
23
- return {}
24
- }
25
-
26
- const repoContext = createRepoContext({ config })
27
- const sessionManager = createMemorySessionManager({ config, pluginRoot: dataDir })
28
- const recall = createMemoryRecall({ config, sessionManager })
29
- const sessionInject = createSessionInject({ config, sessionManager })
30
- const vikingUriGuard = createVikingUriGuard()
31
- const vikingUriNotice = createVikingUriNotice()
11
+ export async function OpenVikingPlugin({ client, directory } = {}) {
12
+ const runtime = createOpenVikingRuntime({ client, directory, pluginRoot })
13
+ if (!runtime) return {}
14
+ await runtime.ready
15
+ return v1Hooks(runtime)
16
+ }
32
17
 
33
- await sessionManager.init()
34
- const toast = makeToast(client)
35
- Promise.resolve().then(async () => {
36
- const ready = await initializeRuntime(config, client)
37
- if (ready) await repoContext.refreshRepos({ force: true })
38
- })
18
+ function v1Hooks(runtime) {
19
+ const {
20
+ config,
21
+ sessionManager,
22
+ repoContext,
23
+ recall,
24
+ sessionInject,
25
+ vikingUriGuard,
26
+ vikingUriNotice,
27
+ } = runtime
39
28
 
40
29
  return {
41
30
  config: async (opencodeConfig) => {
@@ -67,9 +56,18 @@ export async function OpenVikingPlugin({ client, directory }) {
67
56
 
68
57
  "chat.message": async (input, output) => {
69
58
  try {
70
- await sessionInject.injectSessionContext(input, output)
71
- if (!isRecallEnabled(config)) return
72
- await recall.injectRelevantMemories(input, output)
59
+ // opencode awaits this hook before persisting/broadcasting the user
60
+ // message, so serial awaits here stack remote latency onto message
61
+ // display (#5148). Session inject and recall are independent after
62
+ // entry; overlap them the way the dsh plugin does (#4643).
63
+ if (!isRecallEnabled(config)) {
64
+ await sessionInject.injectSessionContext(input, output)
65
+ return
66
+ }
67
+ await Promise.all([
68
+ sessionInject.injectSessionContext(input, output),
69
+ recall.injectRelevantMemories(input, output),
70
+ ])
73
71
  } catch (error) {
74
72
  log("WARN", "recall", "Auto recall failed", { error: error?.message ?? String(error) })
75
73
  }
@@ -92,4 +90,17 @@ export async function OpenVikingPlugin({ client, directory }) {
92
90
  }
93
91
  }
94
92
 
95
- export default OpenVikingPlugin
93
+ const OpenVikingV2Plugin = {
94
+ id: "openviking",
95
+ async setup(ctx) {
96
+ const directory = ctx?.location?.project?.directory || ctx?.location?.directory
97
+ const runtime = createOpenVikingRuntime({ directory, pluginRoot })
98
+ if (!runtime) return
99
+ return startV2Plugin(ctx, runtime, { pluginRoot })
100
+ },
101
+ async server(input) {
102
+ return OpenVikingPlugin(input)
103
+ },
104
+ }
105
+
106
+ export default OpenVikingV2Plugin
@@ -11,6 +11,19 @@ export function createOpenVikingMcpConfig(pluginRoot) {
11
11
  }
12
12
  }
13
13
 
14
+ export function createOpenVikingV2McpConfig(pluginRoot) {
15
+ return {
16
+ type: "local",
17
+ command: ["node", resolvePath(pluginRoot, "servers", "mcp-proxy.mjs")],
18
+ codemode: false,
19
+ timeout: {
20
+ startup: 15000,
21
+ catalog: 15000,
22
+ execution: 15000,
23
+ },
24
+ }
25
+ }
26
+
14
27
  export function injectOpenVikingMcpConfig(config, pluginRoot, enabled = true) {
15
28
  if (!config || typeof config !== "object") return false
16
29
  if (!enabled) return false
@@ -3,12 +3,12 @@ import { isBypassed } from "./shared/session-model.mjs"
3
3
  import { effectivePeerId, fetchJSON, log } from "./utils.mjs"
4
4
 
5
5
  export function createMemoryRecall({ config, sessionManager }) {
6
- async function injectRelevantMemories(input, output) {
6
+ async function buildRelevantMemories(input, parts) {
7
7
  if (!isRecallEnabled(config)) return
8
- const query = extractCurrentUserText(output.parts ?? [])
8
+ const query = extractCurrentUserText(parts ?? [])
9
9
  if (!query) return
10
10
  if (query.length < config.minQueryLength) return
11
- const sessionID = input.sessionID ?? output.message?.sessionID
11
+ const sessionID = input.sessionID
12
12
  if (isBypassed(config, {
13
13
  sessionId: sessionID,
14
14
  cwd: input.directory ?? input.cwd,
@@ -34,14 +34,20 @@ export function createMemoryRecall({ config, sessionManager }) {
34
34
  log: (stage, data) => log("DEBUG", "recall", stage, data),
35
35
  },
36
36
  )
37
- if (!block) return
37
+ return block || undefined
38
+ }
38
39
 
39
- if (prependSyntheticRecallPart(input, output, block)) {
40
+ async function injectRelevantMemories(input, output) {
41
+ const block = await buildRelevantMemories({
42
+ ...input,
43
+ sessionID: input.sessionID ?? output.message?.sessionID,
44
+ }, output.parts)
45
+ if (block && prependSyntheticRecallPart(input, output, block)) {
40
46
  log("INFO", "recall", "Injected OpenViking context")
41
47
  }
42
48
  }
43
49
 
44
- return { injectRelevantMemories }
50
+ return { buildRelevantMemories, injectRelevantMemories }
45
51
  }
46
52
 
47
53
  export function extractCurrentUserText(parts) {
@@ -1,10 +1,9 @@
1
1
  import fs from "fs"
2
2
  import path from "path"
3
3
  import {
4
- extractPartsFromPayload,
5
4
  extractTextFromPayload,
6
5
  isCaptureEnabled,
7
- shouldCaptureText,
6
+ shapeCapturePayload,
8
7
  } from "./shared/capture-utils.mjs"
9
8
  import {
10
9
  deriveHarnessSessionId,
@@ -31,6 +30,7 @@ export function createMemorySessionManager({ config, pluginRoot }) {
31
30
  const statePath = path.join(pluginRoot, "openviking-session-state.json")
32
31
  const oldSessionMapPath = path.join(pluginRoot, "openviking-session-map.json")
33
32
  let saveTimer = null
33
+ let initBackground = Promise.resolve()
34
34
  // Serialize saves: concurrent saveState() calls (a debounced save racing
35
35
  // with flushAll / flushSession / session deletion) all share the same
36
36
  // `${statePath}.tmp` temp file, so one rename can fail with ENOENT after
@@ -46,16 +46,24 @@ export function createMemorySessionManager({ config, pluginRoot }) {
46
46
  return run
47
47
  }
48
48
 
49
- async function init() {
50
- if (isCaptureEnabled(config)) await migrateLegacySessionMap()
49
+ async function init({ deferNetwork = false } = {}) {
51
50
  await loadState()
52
- const health = await fetchJSON(config, "/health", {}, { timeoutMs: 5000 })
53
- if (health.ok) {
51
+ initBackground = Promise.resolve().then(async () => {
52
+ if (isCaptureEnabled(config)) await migrateLegacySessionMap()
53
+ const health = await fetchJSON(config, "/health", {}, { timeoutMs: 5000 })
54
+ if (!health.ok) return
54
55
  await replayPending(
55
56
  (endpoint, init = {}, options = {}) => fetchJSON(config, endpoint, init, options),
56
57
  (stage, data) => log("DEBUG", "pending", stage, data),
57
58
  )
58
- }
59
+ }).catch((error) => {
60
+ log("WARN", "pending", "Pending replay failed during initialization", { error: error?.message })
61
+ })
62
+ if (!deferNetwork) await initBackground
63
+ }
64
+
65
+ async function waitForBackground() {
66
+ await initBackground
59
67
  }
60
68
 
61
69
  async function loadState() {
@@ -117,7 +125,9 @@ export function createMemorySessionManager({ config, pluginRoot }) {
117
125
  {
118
126
  role: message.role,
119
127
  captured: message.captured,
120
- parts: Array.from(message.parts.entries()),
128
+ // Captured messages are never read by flushPendingMessages again; retain
129
+ // only their metadata so completed payloads cannot grow the state file.
130
+ parts: message.captured ? [] : Array.from(message.parts.entries()),
121
131
  },
122
132
  ])),
123
133
  }
@@ -306,6 +316,7 @@ export function createMemorySessionManager({ config, pluginRoot }) {
306
316
 
307
317
  return {
308
318
  init,
319
+ waitForBackground,
309
320
  handleEvent,
310
321
  getMappedSessionId,
311
322
  commitSession,
@@ -371,18 +382,17 @@ export function createMemorySessionManager({ config, pluginRoot }) {
371
382
  if (!role) return null
372
383
  if (role === "assistant" && !config.captureAssistantTurns) return null
373
384
 
374
- const rawText = partsRaw
375
- .map((part) => extractTextFromPayload(part, { toolMaxChars: config.captureToolMaxChars }))
376
- .filter(Boolean)
377
- .join("\n\n")
378
- const captureParts = partsRaw.flatMap((part) => extractPartsFromPayload(part, {
379
- toolMaxChars: config.captureToolMaxChars,
380
- }))
381
- const decision = shouldCaptureText(rawText, role, config)
382
- if (!decision.shouldCapture && captureParts.length === 0) return null
383
- const body = captureParts.length > 0
384
- ? { role, parts: captureParts }
385
- : { role, content: decision.text }
385
+ const shaped = shapeCapturePayload({ role, content: partsRaw }, role, config)
386
+ const toolParts = shaped.parts.filter((part) => part.type !== "text")
387
+ if (shaped.dropped || (!shaped.text && toolParts.length === 0)) return null
388
+ const body = toolParts.length > 0
389
+ ? { role, parts: [
390
+ ...(shaped.text && shaped.parts.some((part) => part.type === "text")
391
+ ? [{ type: "text", text: shaped.text }]
392
+ : []),
393
+ ...toolParts,
394
+ ] }
395
+ : { role, content: shaped.text }
386
396
  const peerId = effectivePeerId(config)
387
397
  if (peerId) body.peer_id = peerId
388
398
  return body
@@ -0,0 +1,51 @@
1
+ import { createRepoContext } from "./repo-context.mjs"
2
+ import { createMemorySessionManager } from "./memory-session.mjs"
3
+ import { createMemoryRecall } from "./memory-recall.mjs"
4
+ import { createSessionInject } from "./session-inject.mjs"
5
+ import { createVikingUriGuard, createVikingUriNotice } from "./viking-uri-guard.mjs"
6
+ import { loadConfig, resolveDataDir } from "./config.mjs"
7
+ import { initializeRuntime } from "./runtime.mjs"
8
+ import { initLogger, log } from "./utils.mjs"
9
+
10
+ export function createOpenVikingRuntime({ client, directory, pluginRoot }) {
11
+ const config = loadConfig(pluginRoot, directory)
12
+ const dataDir = resolveDataDir(pluginRoot, config)
13
+ initLogger(dataDir)
14
+
15
+ if (!config.enabled) {
16
+ log("INFO", "plugin", "OpenViking plugin is disabled in configuration")
17
+ return null
18
+ }
19
+
20
+ const repoContext = createRepoContext({ config })
21
+ const sessionManager = createMemorySessionManager({ config, pluginRoot: dataDir })
22
+ const recall = createMemoryRecall({ config, sessionManager })
23
+ const sessionInject = createSessionInject({ config, sessionManager })
24
+ const vikingUriGuard = createVikingUriGuard()
25
+ const vikingUriNotice = createVikingUriNotice()
26
+
27
+ // OpenCode v2 has no v1 client object and waits for setup before accepting
28
+ // the first prompt. Load local state before hooks run, but keep health and
29
+ // pending replay off that activation path.
30
+ const ready = sessionManager.init({ deferNetwork: !client })
31
+ const background = ready.then(async () => {
32
+ const healthy = await initializeRuntime(config, client)
33
+ if (healthy) await repoContext.refreshRepos({ force: true })
34
+ }).catch((error) => {
35
+ log("WARN", "plugin", "OpenViking runtime initialization failed", {
36
+ error: error?.message ?? String(error),
37
+ })
38
+ })
39
+
40
+ return {
41
+ config,
42
+ repoContext,
43
+ sessionManager,
44
+ recall,
45
+ sessionInject,
46
+ vikingUriGuard,
47
+ vikingUriNotice,
48
+ ready,
49
+ background,
50
+ }
51
+ }
@@ -4,10 +4,10 @@ import { effectivePeerId, fetchJSON, log } from "./utils.mjs"
4
4
  export function createSessionInject({ config, sessionManager }) {
5
5
  const injectedSessions = new Set()
6
6
 
7
- async function injectSessionContext(input, output) {
7
+ async function buildSessionContext(input) {
8
8
  if (config.noAutoInject) return false
9
- const sessionID = input.sessionID ?? output.message?.sessionID
10
- const messageID = input.messageID ?? output.message?.id
9
+ const sessionID = input.sessionID
10
+ const messageID = input.messageID
11
11
  if (!sessionID || !messageID || injectedSessions.has(sessionID)) return false
12
12
 
13
13
  const ovSessionId = sessionManager.getMappedSessionId(sessionID)
@@ -26,7 +26,7 @@ export function createSessionInject({ config, sessionManager }) {
26
26
 
27
27
  if (parts.length === 0) {
28
28
  injectedSessions.add(sessionID)
29
- return false
29
+ return undefined
30
30
  }
31
31
 
32
32
  const block = [
@@ -35,14 +35,6 @@ export function createSessionInject({ config, sessionManager }) {
35
35
  "</openviking-context>",
36
36
  ].join("\n")
37
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
38
  injectedSessions.add(sessionID)
47
39
  log("INFO", "session-inject", "Injected OpenViking session context", {
48
40
  opencode_session: sessionID,
@@ -50,10 +42,28 @@ export function createSessionInject({ config, sessionManager }) {
50
42
  hasProfile: Boolean(profile?.block),
51
43
  hasArchive: Boolean(archive),
52
44
  })
45
+ return block
46
+ }
47
+
48
+ async function injectSessionContext(input, output) {
49
+ const block = await buildSessionContext({
50
+ ...input,
51
+ sessionID: input.sessionID ?? output.message?.sessionID,
52
+ messageID: input.messageID ?? output.message?.id,
53
+ })
54
+ if (!block) return false
55
+ output.parts.unshift({
56
+ id: `prt-ov-session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
57
+ type: "text",
58
+ text: block,
59
+ synthetic: true,
60
+ sessionID: input.sessionID ?? output.message?.sessionID,
61
+ messageID: input.messageID ?? output.message?.id,
62
+ })
53
63
  return true
54
64
  }
55
65
 
56
- return { injectSessionContext }
66
+ return { buildSessionContext, injectSessionContext }
57
67
  }
58
68
 
59
69
  async function fetchArchiveBlock(fetcher, ovSessionId, tokenBudget) {
@@ -409,6 +409,61 @@ export function filterCaptureParts(parts, role, cfg = {}) {
409
409
  return { parts: out, dropped: out.length === 0 };
410
410
  }
411
411
 
412
+ /** Sanitize already-extracted message parts before applying capture rules. */
413
+ export function shapeCaptureParts(parts, role, cfg = {}) {
414
+ const sanitized = (parts || []).map((part) => part?.type === "text"
415
+ ? { ...part, text: sanitizeCapturedText(part.text) }
416
+ : part);
417
+ return filterCaptureParts(sanitized, role, cfg);
418
+ }
419
+
420
+ function faithfulCaptureDecision(text, cfg) {
421
+ const sanitized = sanitizeCapturedText(text);
422
+ if (!sanitized) return { shouldCapture: false, text: "" };
423
+ const capped = truncateCaptureText(sanitized, cfg.captureMaxLength || 24000);
424
+ const compact = oneLine(capped);
425
+ if (/^\[openviking-memory\]/i.test(compact) || SLASH_COMMAND_RE.test(compact)) {
426
+ return { shouldCapture: false, text: "" };
427
+ }
428
+ return { shouldCapture: true, text: capped };
429
+ }
430
+
431
+ /**
432
+ * Shape one host message before an adapter builds its session payload.
433
+ * 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.
435
+ */
436
+ export function shapeCapturePayload(payload, role, cfg = {}, { toolNameById = {}, faithful = false } = {}) {
437
+ const options = { toolMaxChars: cfg.captureToolMaxChars, toolNameById };
438
+ const rawText = extractTextFromPayload(payload, options);
439
+ const sourceParts = extractPartsFromPayload(payload, options);
440
+ const hasTextPart = sourceParts.some((part) => part?.type === "text");
441
+ const sanitizedText = sanitizeCapturedText(rawText);
442
+ // Some hosts supply an array of plain strings. It has text but no structured
443
+ // parts, so use a temporary part for the same filter verdict.
444
+ const fallback = sourceParts.length === 0 && sanitizedText
445
+ ? [{ type: "text", text: rawText }]
446
+ : [];
447
+ const shaped = shapeCaptureParts(sourceParts.length ? sourceParts : fallback, role, cfg);
448
+ if (shaped.dropped) return { parts: [], text: "", signalText: "", dropped: true };
449
+
450
+ const parts = sourceParts.length ? shaped.parts : [];
451
+ const decisionText = hasTextPart || fallback.length
452
+ ? shaped.parts.filter((part) => part.type === "text").map((part) => part.text).join("\n\n")
453
+ : sanitizedText;
454
+ const decision = faithful
455
+ ? faithfulCaptureDecision(decisionText, cfg)
456
+ : shouldCaptureText(decisionText, role, cfg, { filters: false });
457
+ // Codex scans turn.text for explicit-memory keywords; keep that scan based
458
+ // on the user's original words while the filtered parts go on the wire.
459
+ const signalText = decision.shouldCapture && parts.length
460
+ ? (faithful
461
+ ? faithfulCaptureDecision(sanitizedText, cfg)
462
+ : shouldCaptureText(sanitizedText, role, cfg, { filters: false })).text
463
+ : decision.text;
464
+ return { parts, text: decision.shouldCapture ? decision.text : "", signalText: signalText || "", dropped: false };
465
+ }
466
+
412
467
  export function extractCaptureTurns(rolloutEntries, cfg = {}) {
413
468
  const toolNameById = collectToolNamesByIdFromEntries(rolloutEntries);
414
469
  const turns = [];
@@ -421,20 +476,9 @@ export function extractCaptureTurns(rolloutEntries, cfg = {}) {
421
476
  if (!role) continue;
422
477
  if (isAssistantSideCaptureRole(rawRole) && !cfg.captureAssistantTurns) continue;
423
478
 
424
- const rawText = extractTextFromPayload(payload, { toolMaxChars: cfg.captureToolMaxChars });
425
- const parts = extractPartsFromPayload(payload, {
426
- toolMaxChars: cfg.captureToolMaxChars,
427
- toolNameById,
428
- });
429
- const shaped = filterCaptureParts(parts, role, cfg);
430
- if (shaped.dropped) continue;
431
- // With parts on the wire the drop decision was already taken above, so the
432
- // text path only runs the filters for the `content` fallback. That also
433
- // keeps `turn.text` faithful for callers that scan it for trigger words.
434
- const decision = shouldCaptureText(rawText, role, cfg, { filters: shaped.parts.length === 0 });
435
- if (!decision.shouldCapture && shaped.parts.length === 0) continue;
436
- const text = decision.shouldCapture ? decision.text : "";
437
- turns.push({ role, text, parts: shaped.parts });
479
+ const shaped = shapeCapturePayload(payload, role, cfg, { toolNameById });
480
+ if (shaped.dropped || (!shaped.text && shaped.parts.length === 0)) continue;
481
+ turns.push({ role, text: shaped.parts.length ? shaped.signalText : shaped.text, parts: shaped.parts });
438
482
  }
439
483
  return turns;
440
484
  }
@@ -555,7 +599,7 @@ export function sanitizeCapturedText(text) {
555
599
  .replace(/^[ \t]*\[Subagent Context\][^\n]*$/gim, " ")
556
600
  .replace(/^\s*Sender\s*\([^)]+\)\s*```[\s\S]*?```\s*/gim, " ")
557
601
  .replace(/^\s*Conversation (?:metadata|info):\s*```[\s\S]*?```\s*/gim, " ")
558
- .replace(/^\s*\[?\d{4}-\d{2}-\d{2}[T ][^\]\n]{3,80}\]?\s*/gm, "")
602
+ .replace(/^[ \t]*\[\d{4}-\d{2}-\d{2}[T ][-+0-9:.Z]{5,35}\][ \t]*/gm, "")
559
603
  .replace(/^\s*\d{10,13}\s+/gm, "");
560
604
  value = stripMetadataFences(value);
561
605
  value = stripInjectedDigestBlocks(value);
@@ -598,16 +642,18 @@ export function shouldCaptureText(text, role, cfg = {}, { filters = true } = {})
598
642
  const sanitized = sanitizeCapturedText(text);
599
643
  if (!sanitized) return { shouldCapture: false, reason: "empty", text: "" };
600
644
 
601
- let capped = truncateCaptureText(sanitized, maxLength);
645
+ // Rules judge the full sanitized turn; the wire cap applies afterward.
646
+ let filtered = sanitized;
602
647
  if (filters) {
603
648
  const compiled = compileInputFilters(cfg?.captureFilters);
604
649
  if (compiled.rules.length) {
605
- const verdict = applyInputFilters(capped, compiled.rules, { role });
650
+ const verdict = applyInputFilters(filtered, compiled.rules, { role });
606
651
  if (verdict.dropped) return { shouldCapture: false, reason: "filtered", text: "" };
607
- capped = verdict.text;
608
- if (!capped) return { shouldCapture: false, reason: "empty", text: "" };
652
+ filtered = verdict.text;
653
+ if (!filtered) return { shouldCapture: false, reason: "empty", text: "" };
609
654
  }
610
655
  }
656
+ const capped = truncateCaptureText(filtered, maxLength);
611
657
  const compact = oneLine(capped);
612
658
  const isToolSummary = /^\[tool-(?:call|result)\b/i.test(compact);
613
659
 
package/lib/utils.mjs CHANGED
@@ -56,10 +56,12 @@ export function log(level, toolName, message, data) {
56
56
  }
57
57
 
58
58
  export function makeToast(client) {
59
- return (message, variant = "warning") =>
60
- client?.tui?.showToast?.({
59
+ return (message, variant = "warning") => {
60
+ const request = client?.tui?.showToast?.({
61
61
  body: { title: "OpenViking", message, variant, duration: 8000 },
62
- }).catch(() => {})
62
+ })
63
+ return request?.catch?.(() => {})
64
+ }
63
65
  }
64
66
 
65
67
  export function normalizeEndpoint(endpoint) {
@@ -0,0 +1,102 @@
1
+ export function normalizeV2LifecycleEvent(event) {
2
+ const type = event?.type
3
+ const data = event?.data && typeof event.data === "object" ? event.data : {}
4
+ const sessionID = data.sessionID ?? data.sessionId
5
+ if (!type || !sessionID) return []
6
+
7
+ if (type === "session.created") {
8
+ return [sessionEvent("session.created", sessionID, { parentID: data.parentID })]
9
+ }
10
+ if (type === "session.deleted") {
11
+ return [sessionEvent("session.deleted", sessionID)]
12
+ }
13
+ if (type === "session.compaction.ended") {
14
+ return [sessionEvent("session.compacted", sessionID)]
15
+ }
16
+ // A failed execution ends one turn, not the session, so it keeps state like
17
+ // the other terminal outcomes instead of taking v1's session.error path.
18
+ if (
19
+ type === "session.execution.succeeded" ||
20
+ type === "session.execution.interrupted" ||
21
+ type === "session.execution.failed"
22
+ ) {
23
+ return [sessionEvent("session.idle", sessionID)]
24
+ }
25
+ return []
26
+ }
27
+
28
+ export function contextMessageEvents(sessionID, message) {
29
+ if (!sessionID || !message?.id) return []
30
+ if (message.type === "user") {
31
+ if (typeof message.text !== "string" || !message.text.trim()) return []
32
+ return [
33
+ messageUpdated(sessionID, message.id, "user"),
34
+ partUpdated(sessionID, message.id, `${message.id}:text`, { type: "text", text: message.text }),
35
+ ]
36
+ }
37
+ if (message.type !== "assistant") return []
38
+
39
+ const events = [messageUpdated(sessionID, message.id, "assistant")]
40
+ for (const [index, block] of (message.content ?? []).entries()) {
41
+ const part = contentPart(block, index)
42
+ if (part) events.push(partUpdated(sessionID, message.id, part.id, part))
43
+ }
44
+ return events.length > 1 ? events : []
45
+ }
46
+
47
+ function sessionEvent(type, sessionID, info = {}) {
48
+ return {
49
+ type,
50
+ properties: { info: { id: sessionID, sessionID, ...info } },
51
+ }
52
+ }
53
+
54
+ function contentPart(block, index) {
55
+ if (!block || typeof block !== "object") return null
56
+ // Reasoning is dropped, matching v1 parts and the shared capture filter.
57
+ if (block.type === "text") {
58
+ return {
59
+ id: `text:${index}`,
60
+ type: "text",
61
+ text: typeof block.text === "string" ? block.text : "",
62
+ }
63
+ }
64
+ if (block.type !== "tool") return null
65
+ const output = toolContentText(block.state?.content) || errorText(block.state?.error)
66
+ return {
67
+ id: block.id || `tool:${index}`,
68
+ type: "tool",
69
+ callID: block.id,
70
+ tool: block.name,
71
+ state: block.state,
72
+ output,
73
+ }
74
+ }
75
+
76
+ function toolContentText(content) {
77
+ if (!Array.isArray(content)) return ""
78
+ return content
79
+ .filter((item) => item?.type === "text" && typeof item.text === "string")
80
+ .map((item) => item.text)
81
+ .join("\n\n")
82
+ }
83
+
84
+ function errorText(error) {
85
+ if (typeof error === "string") return error
86
+ if (typeof error?.message === "string") return error.message
87
+ return ""
88
+ }
89
+
90
+ function messageUpdated(sessionID, messageID, role) {
91
+ return {
92
+ type: "message.updated",
93
+ properties: { info: { sessionID, id: messageID, role } },
94
+ }
95
+ }
96
+
97
+ function partUpdated(sessionID, messageID, partId, part) {
98
+ return {
99
+ type: "message.part.updated",
100
+ properties: { part: { ...part, id: partId, sessionID, messageID } },
101
+ }
102
+ }
@@ -0,0 +1,352 @@
1
+ import { isRecallEnabled } from "./shared/recall-core.mjs"
2
+ import { createOpenVikingV2McpConfig } from "./mcp-config.mjs"
3
+ import { contextMessageEvents, normalizeV2LifecycleEvent } from "./v2-events.mjs"
4
+ import { log } from "./utils.mjs"
5
+
6
+ const METADATA_KEY = "openviking"
7
+ const CURSOR_KEY_PREFIX = "capture-cursor/"
8
+ const LIFECYCLE_EVENTS = new Set([
9
+ "session.created",
10
+ "session.deleted",
11
+ "session.execution.succeeded",
12
+ "session.execution.failed",
13
+ "session.execution.interrupted",
14
+ "session.compaction.ended",
15
+ ])
16
+
17
+ export async function startV2Plugin(ctx, runtime, { pluginRoot }) {
18
+ const {
19
+ config,
20
+ sessionManager,
21
+ repoContext,
22
+ recall,
23
+ sessionInject,
24
+ vikingUriGuard,
25
+ vikingUriNotice,
26
+ } = runtime
27
+ const captureCursors = new Map()
28
+ const sessionOwnership = new Map()
29
+ // Compaction hooks can run while lifecycle events are still flushing. Keep
30
+ // capture and flush ordered so the same pending message is never sent twice.
31
+ let sessionTask = Promise.resolve()
32
+ const serializeSession = (operation) => {
33
+ const task = sessionTask.then(operation)
34
+ sessionTask = task.catch(() => {})
35
+ return task
36
+ }
37
+ const directory = ctx?.location?.project?.directory || ctx?.location?.directory
38
+
39
+ if (config.mcp.enabled && ctx?.mcp?.transform) {
40
+ await ctx.mcp.transform((editor) => {
41
+ const current = editor.get?.("openviking")
42
+ if (current?.disabled === true) return
43
+ editor.set("openviking", createOpenVikingV2McpConfig(pluginRoot))
44
+ })
45
+ log("INFO", "mcp", "Registered OpenViking MCP server for OpenCode v2")
46
+ } else if (!config.mcp.enabled) {
47
+ log("INFO", "mcp", "Skipped bundled MCP registration in hook-only mode")
48
+ }
49
+
50
+ if (ctx?.tool?.hook) {
51
+ await ctx.tool.hook("execute.before", async (event) => {
52
+ await runtime.ready
53
+ await vikingUriGuard(
54
+ { tool: event.tool, args: event.input },
55
+ { args: event.input },
56
+ )
57
+ })
58
+ await ctx.tool.hook("execute.after", async (event) => {
59
+ try {
60
+ await runtime.ready
61
+ await noticeV2Tool(event, vikingUriNotice)
62
+ } catch (error) {
63
+ logHookError("tool.execute.after", error)
64
+ }
65
+ })
66
+ }
67
+
68
+ if (ctx?.session?.hook) {
69
+ await ctx.session.hook("prompt", async (event) => {
70
+ try {
71
+ await runtime.ready
72
+ await prepareV2Prompt(event, {
73
+ directory,
74
+ recall,
75
+ sessionInject,
76
+ recallEnabled: isRecallEnabled(config),
77
+ })
78
+ } catch (error) {
79
+ logHookError("session.prompt", error)
80
+ }
81
+ })
82
+
83
+ await ctx.session.hook("context", async (event) => {
84
+ try {
85
+ await runtime.ready
86
+ injectV2Context(event, repoContext)
87
+ } catch (error) {
88
+ logHookError("session.context", error)
89
+ }
90
+ })
91
+
92
+ await ctx.session.hook("compaction", async (event) => {
93
+ try {
94
+ await runtime.ready
95
+ // context() drops the old transcript as soon as compaction completes.
96
+ // Capture it now; the ended event below owns the commit.
97
+ await serializeSession(() => captureV2Context(ctx, sessionManager, captureCursors, event.sessionID))
98
+ } catch (error) {
99
+ logHookError("session.compaction.capture", error)
100
+ }
101
+ })
102
+ }
103
+
104
+ const controller = new AbortController()
105
+ let eventTask = Promise.resolve()
106
+ if (ctx?.event?.subscribe) {
107
+ eventTask = consumeEvents(ctx, controller.signal, async (event) => {
108
+ const sessionID = event?.data?.sessionID ?? event?.data?.sessionId
109
+ if (!LIFECYCLE_EVENTS.has(event?.type) || !sessionID) return
110
+ if (!(await ownsSession(ctx, sessionOwnership, event, sessionID))) return
111
+ await serializeSession(async () => {
112
+ await runtime.ready
113
+ if (event.type === "session.execution.failed") {
114
+ log("WARN", "v2", "OpenCode v2 execution failed", {
115
+ opencode_session: sessionID,
116
+ error: event.data?.error?.message ?? event.data?.error,
117
+ })
118
+ }
119
+ if (isExecutionBoundary(event.type)) {
120
+ try {
121
+ await captureV2Context(ctx, sessionManager, captureCursors, sessionID)
122
+ } catch (error) {
123
+ logHookError("session.context.capture", error)
124
+ }
125
+ }
126
+ for (const normalized of normalizeV2LifecycleEvent(event)) {
127
+ await sessionManager.handleEvent(normalized)
128
+ if (normalized.type === "session.created") {
129
+ await repoContext.refreshRepos({ force: true })
130
+ }
131
+ }
132
+ if (event.type === "session.deleted") {
133
+ sessionOwnership.delete(sessionID)
134
+ await forgetCursor(ctx, captureCursors, sessionID)
135
+ }
136
+ })
137
+ })
138
+ }
139
+
140
+ return async () => {
141
+ controller.abort()
142
+ try {
143
+ await eventTask
144
+ await sessionTask
145
+ await runtime.ready
146
+ await runtime.background
147
+ await sessionManager.waitForBackground?.()
148
+ await sessionManager.flushAll({ commit: true })
149
+ } catch (error) {
150
+ logHookError("plugin.cleanup", error)
151
+ }
152
+ log("INFO", "plugin", "OpenViking plugin disposed")
153
+ }
154
+ }
155
+
156
+ async function consumeEvents(ctx, signal, handle) {
157
+ try {
158
+ for await (const event of ctx.event.subscribe({ signal })) {
159
+ try {
160
+ await handle(event)
161
+ } catch (error) {
162
+ logHookError(`event.${event?.type || "unknown"}`, error)
163
+ }
164
+ }
165
+ } catch (error) {
166
+ if (signal.aborted) return
167
+ logHookError("event.subscribe", error)
168
+ }
169
+ }
170
+
171
+ export async function prepareV2Prompt(event, {
172
+ directory,
173
+ recall,
174
+ sessionInject,
175
+ recallEnabled,
176
+ }) {
177
+ const sessionID = event?.sessionID
178
+ const messageID = event?.messageID
179
+ if (!sessionID || !messageID) return
180
+ const text = event?.prompt?.text
181
+ const parts = typeof text === "string" && text.trim() ? [{ type: "text", text }] : []
182
+ const input = { sessionID, messageID, directory }
183
+ const blocks = []
184
+ try {
185
+ const sessionBlock = await sessionInject.buildSessionContext(input)
186
+ if (sessionBlock) blocks.push(sessionBlock)
187
+ } catch (error) {
188
+ logHookError("session.prompt.profile", error)
189
+ }
190
+ if (recallEnabled) {
191
+ try {
192
+ const recallBlock = await recall.buildRelevantMemories(input, parts)
193
+ if (recallBlock) blocks.push(recallBlock)
194
+ } catch (error) {
195
+ logHookError("session.prompt.recall", error)
196
+ }
197
+ }
198
+ if (blocks.length === 0) return
199
+
200
+ event.metadata = event.metadata && typeof event.metadata === "object" ? event.metadata : {}
201
+ event.metadata[METADATA_KEY] = { context: blocks }
202
+ }
203
+
204
+ export function injectV2Context(event, repoContext) {
205
+ const repoPrompt = repoContext.getRepoSystemPrompt()
206
+ if (repoPrompt) pushSystem(event, repoPrompt)
207
+ if (!Array.isArray(event?.messages)) return
208
+
209
+ for (const message of event.messages) {
210
+ const blocks = message?.metadata?.[METADATA_KEY]?.context
211
+ if (message?.role !== "user" || !Array.isArray(blocks) || blocks.length === 0) continue
212
+ if (!Array.isArray(message.content)) message.content = []
213
+ if (message.content.some((part) => part?.metadata?.[METADATA_KEY] === true)) continue
214
+ message.content.unshift({
215
+ type: "text",
216
+ text: blocks.filter((block) => typeof block === "string" && block).join("\n\n"),
217
+ metadata: { [METADATA_KEY]: true },
218
+ })
219
+ }
220
+ }
221
+
222
+ // The plugin event stream carries every location served by the process, and
223
+ // execution events have no envelope location, so ownership comes from the
224
+ // session itself. An unresolvable session is handled rather than dropped.
225
+ async function ownsSession(ctx, ownership, event, sessionID) {
226
+ if (event.type === "session.created") {
227
+ const owned = sameLocation(event.location ?? event.data?.location, ctx.location)
228
+ ownership.set(sessionID, owned)
229
+ return owned
230
+ }
231
+ if (event.location) return sameLocation(event.location, ctx.location)
232
+ if (ownership.has(sessionID)) return ownership.get(sessionID)
233
+ if (event.type === "session.deleted" || !ctx.session?.get) return true
234
+ try {
235
+ const result = await ctx.session.get({ sessionID })
236
+ const location = (result?.data ?? result)?.location
237
+ if (location?.directory) {
238
+ const owned = sameLocation(location, ctx.location)
239
+ ownership.set(sessionID, owned)
240
+ return owned
241
+ }
242
+ } catch (error) {
243
+ logHookError("session.get", error)
244
+ }
245
+ return true
246
+ }
247
+
248
+ function sameLocation(ref, location) {
249
+ if (!ref?.directory || !location?.directory) return true
250
+ if (ref.directory !== location.directory) return false
251
+ return !ref.workspaceID || !location.workspaceID || ref.workspaceID === location.workspaceID
252
+ }
253
+
254
+ export async function captureV2Context(ctx, sessionManager, cursors, sessionID) {
255
+ const messages = await ctx.session.context({ sessionID })
256
+ if (!Array.isArray(messages)) return
257
+ // The cursor lives in host storage because the session state file is shared
258
+ // by every location instance and cannot prove what was already captured.
259
+ const cursor = cursors.get(sessionID) ?? await readCursor(ctx, sessionID)
260
+ const cursorIndex = cursor ? messages.findIndex((message) => message?.id === cursor) : -1
261
+ const pending = cursorIndex >= 0 ? messages.slice(cursorIndex + 1) : messages
262
+ for (const message of pending) {
263
+ for (const normalized of contextMessageEvents(sessionID, message)) {
264
+ await sessionManager.handleEvent(normalized)
265
+ }
266
+ }
267
+ const last = messages.at(-1)?.id
268
+ if (!last) return
269
+ cursors.set(sessionID, last)
270
+ if (last !== cursor) await writeCursor(ctx, sessionID, last)
271
+ }
272
+
273
+ async function readCursor(ctx, sessionID) {
274
+ if (!ctx?.storage?.get) return undefined
275
+ try {
276
+ const value = await ctx.storage.get(CURSOR_KEY_PREFIX + sessionID)
277
+ return typeof value === "string" ? value : undefined
278
+ } catch (error) {
279
+ logHookError("storage.get", error)
280
+ return undefined
281
+ }
282
+ }
283
+
284
+ async function writeCursor(ctx, sessionID, cursor) {
285
+ if (!ctx?.storage?.set) return
286
+ try {
287
+ await ctx.storage.set(CURSOR_KEY_PREFIX + sessionID, cursor)
288
+ } catch (error) {
289
+ logHookError("storage.set", error)
290
+ }
291
+ }
292
+
293
+ async function forgetCursor(ctx, cursors, sessionID) {
294
+ cursors.delete(sessionID)
295
+ if (!ctx?.storage?.remove) return
296
+ try {
297
+ await ctx.storage.remove(CURSOR_KEY_PREFIX + sessionID)
298
+ } catch (error) {
299
+ logHookError("storage.remove", error)
300
+ }
301
+ }
302
+
303
+ function isExecutionBoundary(type) {
304
+ return type === "session.execution.succeeded" ||
305
+ type === "session.execution.failed" ||
306
+ type === "session.execution.interrupted"
307
+ }
308
+
309
+ function pushSystem(event, text) {
310
+ if (!text) return
311
+ if (!Array.isArray(event.system)) event.system = []
312
+ if (event.system.some((part) => part?.type === "text" && part.text === text)) return
313
+ event.system.push({ type: "text", text })
314
+ }
315
+
316
+ function logHookError(hook, error) {
317
+ log("WARN", "v2", `OpenCode v2 ${hook} failed`, {
318
+ error: error?.message ?? String(error),
319
+ })
320
+ }
321
+
322
+ async function noticeV2Tool(event, vikingUriNotice) {
323
+ if (event?.status !== "completed") return
324
+ const before = toolResultText(event.result)
325
+ const output = { output: before }
326
+ await vikingUriNotice({ tool: event.tool, args: event.input }, output)
327
+ if (!output.output || output.output === before) return
328
+ writeToolResultText(event.result, output.output)
329
+ }
330
+
331
+ function toolResultText(result) {
332
+ if (!result || typeof result !== "object") return ""
333
+ if (typeof result.content === "string") return result.content
334
+ if (!Array.isArray(result.content)) return ""
335
+ return result.content
336
+ .filter((item) => item?.type === "text" && typeof item.text === "string")
337
+ .map((item) => item.text)
338
+ .join("\n\n")
339
+ }
340
+
341
+ function writeToolResultText(result, text) {
342
+ if (!result || typeof result !== "object") return
343
+ if (typeof result.content === "string" || result.content == null) {
344
+ result.content = text
345
+ return
346
+ }
347
+ if (!Array.isArray(result.content)) return
348
+ // Preserve the original text/media blocks and append only the new notice.
349
+ const before = toolResultText(result)
350
+ const notice = text.startsWith(before) ? text.slice(before.length).trimStart() : text
351
+ if (notice) result.content.push({ type: "text", text: notice })
352
+ }
@@ -18,6 +18,10 @@ const FILESYSTEM_TOOL_HINTS = {
18
18
  tool: "openviking_read or openviking_search",
19
19
  example: (uri) => `openviking_read(uris=["${uri}"])`,
20
20
  },
21
+ shell: {
22
+ tool: "openviking_read or openviking_search",
23
+ example: (uri) => `openviking_read(uris=["${uri}"])`,
24
+ },
21
25
  }
22
26
 
23
27
  export function createVikingUriGuard() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openviking/opencode-plugin",
3
- "version": "2026.9.24",
3
+ "version": "2026.9.25-2",
4
4
  "author": "@openviking",
5
5
  "description": "Unified OpenCode plugin for OpenViking repository retrieval and long-term memory",
6
6
  "type": "module",
@@ -24,7 +24,7 @@
24
24
  ],
25
25
  "scripts": {
26
26
  "prepack": "node ../memory-plugin-shared/sync.mjs",
27
- "check": "node --check index.mjs && node --check lib/config.mjs && node --check lib/mcp-config.mjs && node --check lib/runtime.mjs && node --check lib/repo-context.mjs && node --check lib/memory-session.mjs && node --check lib/memory-recall.mjs && node --check lib/session-inject.mjs && node --check lib/viking-uri-guard.mjs && node --check lib/utils.mjs && node --check lib/shared/*.mjs && node --check servers/mcp-proxy.mjs && node --check scripts/setup.mjs && node --check tests/*.test.mjs",
27
+ "check": "node --check index.mjs && node --check lib/config.mjs && node --check lib/mcp-config.mjs && node --check lib/runtime.mjs && node --check lib/plugin-runtime.mjs && node --check lib/v2-events.mjs && node --check lib/v2-plugin.mjs && node --check lib/repo-context.mjs && node --check lib/memory-session.mjs && node --check lib/memory-recall.mjs && node --check lib/session-inject.mjs && node --check lib/viking-uri-guard.mjs && node --check lib/utils.mjs && node --check lib/shared/*.mjs && node --check servers/mcp-proxy.mjs && node --check scripts/setup.mjs && node --check tests/*.test.mjs",
28
28
  "test": "node --test tests/*.test.mjs"
29
29
  },
30
30
  "dependencies": {},
@@ -45,5 +45,5 @@
45
45
  "code-search"
46
46
  ],
47
47
  "license": "Apache-2.0",
48
- "openvikingSourceCommit": "20a97022d52277f2000b9014b8c10817192ad020"
48
+ "openvikingSourceCommit": "3e02c6ac29842be214fa1560dfaf6ea39a69322c"
49
49
  }