@openviking/openclaw-plugin 2026.5.8 → 2026.5.21

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/dist/index.js CHANGED
@@ -4,10 +4,39 @@ import { registerSetupCli } from "./commands/setup.js";
4
4
  import { OpenVikingClient, isMemoryUri } from "./client.js";
5
5
  import { formatMessageFaithful, toRoleId } from "./context-engine.js";
6
6
  import { compileSessionPatterns, shouldBypassSession, } from "./text-utils.js";
7
- import { clampScore, postProcessMemories, formatMemoryLines, } from "./memory-ranking.js";
7
+ import { clampScore, postProcessMemories, pickMemoriesForInjection, } from "./memory-ranking.js";
8
8
  import { createMemoryOpenVikingContextEngine, openClawSessionToOvStorageId, openClawSessionRefToOvStorageId, } from "./context-engine.js";
9
9
  import { buildMemoryLines, buildMemoryLinesWithBudget, estimateTokenCount, prepareRecallQuery, } from "./auto-recall.js";
10
10
  export { buildMemoryLines, buildMemoryLinesWithBudget, estimateTokenCount, prepareRecallQuery, };
11
+ function parseToolResultRef(value) {
12
+ const raw = typeof value === "string" ? value.trim() : "";
13
+ if (!raw) {
14
+ return null;
15
+ }
16
+ const match = raw.match(/^viking:\/\/session\/([^/]+)\/tool-results\/([^/?#]+)(?:[?#].*)?$/);
17
+ if (!match) {
18
+ return null;
19
+ }
20
+ const sessionId = decodeURIComponent(match[1]);
21
+ const toolResultId = decodeURIComponent(match[2]);
22
+ if (!sessionId || !toolResultId) {
23
+ return null;
24
+ }
25
+ return {
26
+ sessionId,
27
+ toolResultId,
28
+ ref: `viking://session/${encodeURIComponent(sessionId)}/tool-results/${encodeURIComponent(toolResultId)}`,
29
+ };
30
+ }
31
+ function getOptionalInteger(value, fallback) {
32
+ if (typeof value !== "number" || !Number.isFinite(value)) {
33
+ return fallback;
34
+ }
35
+ return Math.floor(value);
36
+ }
37
+ function getPositiveInteger(value, fallback) {
38
+ return Math.max(1, getOptionalInteger(value, fallback));
39
+ }
11
40
  const DEFAULT_OPENCLAW_AGENT_ID = "main";
12
41
  /**
13
42
  * OpenViking `UserIdentifier` allows only [a-zA-Z0-9_-] for agent_id
@@ -121,15 +150,6 @@ function getNumberFlag(flags, name) {
121
150
  function getBoolFlag(flags, name) {
122
151
  return flags.get(name) === true;
123
152
  }
124
- function parseImportKind(value) {
125
- if (!value) {
126
- return "resource";
127
- }
128
- if (value === "resource" || value === "skill") {
129
- return value;
130
- }
131
- throw new Error("--kind must be resource or skill");
132
- }
133
153
  function extractToolSenderId(ctx) {
134
154
  if (!ctx || typeof ctx !== "object") {
135
155
  return undefined;
@@ -149,23 +169,18 @@ function extractToolSenderId(ctx) {
149
169
  }
150
170
  return undefined;
151
171
  }
152
- export function parseOvImportCommandArgs(args) {
172
+ export function parseAddResourceCommandArgs(args) {
153
173
  const parsed = parseFlagArgs(args);
154
- const kind = parseImportKind(getStringFlag(parsed.flags, "kind"));
155
174
  const source = parsed.positionals.length <= 1 ? parsed.positionals[0] : parsed.positionals.join(" ").trim();
156
175
  if (!source) {
157
- throw new Error("Usage: /ov-import <source> [--kind resource|skill] [--to URI] [--parent URI] [--wait]");
176
+ throw new Error("Usage: /add-resource <source> [--to URI] [--parent URI] [--reason TEXT] [--instruction TEXT] [--wait] [--timeout SEC]");
158
177
  }
159
178
  const to = getStringFlag(parsed.flags, "to");
160
179
  const parent = getStringFlag(parsed.flags, "parent");
161
180
  if (to && parent) {
162
181
  throw new Error("Cannot specify both --to and --parent.");
163
182
  }
164
- if (kind === "skill" && (to || parent || parsed.flags.has("reason") || parsed.flags.has("instruction"))) {
165
- throw new Error("--to, --parent, --reason, and --instruction are resource-only options.");
166
- }
167
183
  return {
168
- kind,
169
184
  source,
170
185
  to,
171
186
  parent,
@@ -175,6 +190,21 @@ export function parseOvImportCommandArgs(args) {
175
190
  timeout: getNumberFlag(parsed.flags, "timeout"),
176
191
  };
177
192
  }
193
+ export function parseAddSkillCommandArgs(args) {
194
+ const parsed = parseFlagArgs(args);
195
+ const source = parsed.positionals.length <= 1 ? parsed.positionals[0] : parsed.positionals.join(" ").trim();
196
+ if (!source) {
197
+ throw new Error("Usage: /add-skill <source> [--wait] [--timeout SEC]");
198
+ }
199
+ if (parsed.flags.has("to") || parsed.flags.has("parent") || parsed.flags.has("reason") || parsed.flags.has("instruction")) {
200
+ throw new Error("--to, --parent, --reason, and --instruction are resource-only options.");
201
+ }
202
+ return {
203
+ source,
204
+ wait: getBoolFlag(parsed.flags, "wait"),
205
+ timeout: getNumberFlag(parsed.flags, "timeout"),
206
+ };
207
+ }
178
208
  export function parseMemorySearchCommandArgs(args) {
179
209
  const parsed = parseFlagArgs(args);
180
210
  // `/memory-search` only accepts a single query string, so positional segments are
@@ -417,32 +447,21 @@ const contextEnginePlugin = {
417
447
  },
418
448
  };
419
449
  };
420
- const executeImport = async (input, agentId) => {
421
- const kind = input.kind ?? "resource";
422
- if (kind === "skill") {
423
- if (input.to || input.parent || input.reason || input.instruction) {
424
- throw new Error("to, parent, reason, and instruction are resource-only options.");
425
- }
426
- return importSkill({
427
- path: input.source,
428
- data: input.data,
429
- wait: input.wait,
430
- timeout: input.timeout,
431
- }, agentId);
432
- }
433
- if (input.data !== undefined && input.data !== null) {
434
- throw new Error("data is only supported for skill imports.");
435
- }
436
- return importResource({
437
- pathOrUrl: input.source ?? "",
438
- to: input.to,
439
- parent: input.parent,
440
- reason: input.reason,
441
- instruction: input.instruction,
442
- wait: input.wait,
443
- timeout: input.timeout,
444
- }, agentId);
445
- };
450
+ const addResourceOpenViking = (input, agentId) => importResource({
451
+ pathOrUrl: input.source ?? "",
452
+ to: input.to,
453
+ parent: input.parent,
454
+ reason: input.reason,
455
+ instruction: input.instruction,
456
+ wait: input.wait,
457
+ timeout: input.timeout,
458
+ }, agentId);
459
+ const addSkillOpenViking = (input, agentId) => importSkill({
460
+ path: input.source,
461
+ data: input.data,
462
+ wait: input.wait,
463
+ timeout: input.timeout,
464
+ }, agentId);
446
465
  const mergeFindResults = (results) => {
447
466
  const deduplicate = (items) => {
448
467
  const seen = new Map();
@@ -566,30 +585,26 @@ const contextEnginePlugin = {
566
585
  };
567
586
  };
568
587
  api.registerTool((ctx) => ({
569
- name: "ov_import",
570
- label: "Import (OpenViking)",
571
- description: "Import an OpenViking resource or skill only when the user explicitly asks to import, add, or index one. " +
572
- "Defaults to resource; set kind=skill for SKILL.md, skill directories, raw skill content, or MCP tool dicts.",
588
+ name: "add_resource",
589
+ label: "Add Resource (OpenViking)",
590
+ description: "Use only when the user explicitly asks to import, add, upload, save, or index a document, directory, URL, Git repository, or OpenClaw media attachment into OpenViking resources. " +
591
+ "For a '[media attached: /path ...]' document, set source to that exact local media path. Do not invent OpenViking upload REST endpoints.",
573
592
  parameters: Type.Object({
574
- kind: Type.Optional(Type.Union([Type.Literal("resource"), Type.Literal("skill")], { description: "Import kind. Default: resource" })),
575
- source: Type.Optional(Type.String({ description: "Local path, directory path, public URL, or Git URL" })),
576
- data: Type.Optional(Type.Any({ description: "Skill only: raw SKILL.md content or MCP tool dict" })),
577
- to: Type.Optional(Type.String({ description: "Resource only: exact target URI, e.g. viking://resources/project-docs" })),
578
- parent: Type.Optional(Type.String({ description: "Resource only: parent URI under viking://resources" })),
579
- reason: Type.Optional(Type.String({ description: "Resource only: reason or note for adding this resource" })),
580
- instruction: Type.Optional(Type.String({ description: "Resource only: processing instruction for semantic extraction" })),
593
+ source: Type.String({ description: "Local path, OpenClaw media attachment path, directory path, public URL, or Git URL" }),
594
+ to: Type.Optional(Type.String({ description: "Exact target URI, e.g. viking://resources/project-docs" })),
595
+ parent: Type.Optional(Type.String({ description: "Parent URI under viking://resources" })),
596
+ reason: Type.Optional(Type.String({ description: "Reason or note for adding this resource" })),
597
+ instruction: Type.Optional(Type.String({ description: "Processing instruction for semantic extraction" })),
581
598
  wait: Type.Optional(Type.Boolean({ description: "Wait for processing to complete" })),
582
599
  timeout: Type.Optional(Type.Number({ description: "Timeout in seconds when wait is true" })),
583
600
  }),
584
601
  async execute(_toolCallId, params) {
585
602
  if (isBypassedSession(ctx)) {
586
- return makeBypassedToolResult("ov_import");
603
+ return makeBypassedToolResult("add_resource");
587
604
  }
588
605
  const session = resolvePluginSessionRouting(ctx);
589
- return executeImport({
590
- kind: params.kind === "skill" ? "skill" : "resource",
606
+ return addResourceOpenViking({
591
607
  source: typeof params.source === "string" ? params.source : undefined,
592
- data: params.data,
593
608
  to: typeof params.to === "string" ? params.to : undefined,
594
609
  parent: typeof params.parent === "string" ? params.parent : undefined,
595
610
  reason: typeof params.reason === "string" ? params.reason : undefined,
@@ -598,7 +613,31 @@ const contextEnginePlugin = {
598
613
  timeout: typeof params.timeout === "number" ? params.timeout : undefined,
599
614
  }, session.agentId);
600
615
  },
601
- }), { name: "ov_import" });
616
+ }), { name: "add_resource" });
617
+ api.registerTool((ctx) => ({
618
+ name: "add_skill",
619
+ label: "Add Skill (OpenViking)",
620
+ description: "Use only when the user explicitly asks to import, add, install, or register a skill into OpenViking. " +
621
+ "Set source to a local SKILL.md file or skill directory, or data to raw SKILL.md content or an MCP tool dict.",
622
+ parameters: Type.Object({
623
+ source: Type.Optional(Type.String({ description: "Local SKILL.md path or skill directory path" })),
624
+ data: Type.Optional(Type.Any({ description: "Raw SKILL.md content or MCP tool dict" })),
625
+ wait: Type.Optional(Type.Boolean({ description: "Wait for processing to complete" })),
626
+ timeout: Type.Optional(Type.Number({ description: "Timeout in seconds when wait is true" })),
627
+ }),
628
+ async execute(_toolCallId, params) {
629
+ if (isBypassedSession(ctx)) {
630
+ return makeBypassedToolResult("add_skill");
631
+ }
632
+ const session = resolvePluginSessionRouting(ctx);
633
+ return addSkillOpenViking({
634
+ source: typeof params.source === "string" ? params.source : undefined,
635
+ data: params.data,
636
+ wait: typeof params.wait === "boolean" ? params.wait : undefined,
637
+ timeout: typeof params.timeout === "number" ? params.timeout : undefined,
638
+ }, session.agentId);
639
+ },
640
+ }), { name: "add_skill" });
602
641
  api.registerTool((ctx) => ({
603
642
  name: "memory_search",
604
643
  label: "Memory Search (OpenViking)",
@@ -621,22 +660,42 @@ const contextEnginePlugin = {
621
660
  },
622
661
  }), { name: "memory_search" });
623
662
  api.registerCommand?.({
624
- name: "ov-import",
625
- description: "Import a resource or skill into OpenViking.",
663
+ name: "add-resource",
664
+ description: "Add a resource into OpenViking.",
626
665
  acceptsArgs: true,
627
666
  handler: async (ctx) => {
628
667
  try {
629
668
  if (isBypassedSession(ctx)) {
630
- const bypassed = makeBypassedToolResult("ov_import");
669
+ const bypassed = makeBypassedToolResult("add_resource");
631
670
  return { text: bypassed.content[0].text, details: bypassed.details };
632
671
  }
633
672
  const session = resolvePluginSessionRouting(ctx);
634
- const input = parseOvImportCommandArgs(ctx.args ?? "");
635
- const result = await executeImport(input, session.agentId);
673
+ const input = parseAddResourceCommandArgs(ctx.args ?? "");
674
+ const result = await addResourceOpenViking(input, session.agentId);
636
675
  return { text: result.content[0].text, details: result.details };
637
676
  }
638
677
  catch (err) {
639
- return { text: `OpenViking import failed: ${err instanceof Error ? err.message : String(err)}` };
678
+ return { text: `OpenViking add resource failed: ${err instanceof Error ? err.message : String(err)}` };
679
+ }
680
+ },
681
+ });
682
+ api.registerCommand?.({
683
+ name: "add-skill",
684
+ description: "Add a skill into OpenViking.",
685
+ acceptsArgs: true,
686
+ handler: async (ctx) => {
687
+ try {
688
+ if (isBypassedSession(ctx)) {
689
+ const bypassed = makeBypassedToolResult("add_skill");
690
+ return { text: bypassed.content[0].text, details: bypassed.details };
691
+ }
692
+ const session = resolvePluginSessionRouting(ctx);
693
+ const input = parseAddSkillCommandArgs(ctx.args ?? "");
694
+ const result = await addSkillOpenViking(input, session.agentId);
695
+ return { text: result.content[0].text, details: result.details };
696
+ }
697
+ catch (err) {
698
+ return { text: `OpenViking add skill failed: ${err instanceof Error ? err.message : String(err)}` };
640
699
  }
641
700
  },
642
701
  });
@@ -734,29 +793,54 @@ const contextEnginePlugin = {
734
793
  total: leafOnly.length,
735
794
  };
736
795
  }
737
- const memories = postProcessMemories(result.memories ?? [], {
738
- limit,
796
+ const leafOnly = (result.memories ?? []).filter((m) => !m.level || m.level === 2);
797
+ const processed = postProcessMemories(leafOnly, {
798
+ limit: requestLimit,
739
799
  scoreThreshold,
740
800
  });
801
+ const memories = pickMemoriesForInjection(processed, limit, query);
741
802
  if (memories.length === 0) {
742
803
  return {
743
804
  content: [{ type: "text", text: "No relevant OpenViking memories found." }],
744
805
  details: { count: 0, total: result.total ?? 0, scoreThreshold },
745
806
  };
746
807
  }
808
+ const { lines: memoryLines } = await buildMemoryLinesWithBudget(memories, (uri) => recallClient.read(uri, session.agentId), {
809
+ recallPreferAbstract: false,
810
+ recallMaxInjectedChars: cfg.recallMaxInjectedChars,
811
+ });
812
+ if (memoryLines.length === 0) {
813
+ return {
814
+ content: [
815
+ {
816
+ type: "text",
817
+ text: `No complete OpenViking memories fit recallMaxInjectedChars=${cfg.recallMaxInjectedChars}.`,
818
+ },
819
+ ],
820
+ details: {
821
+ count: 0,
822
+ memories,
823
+ total: result.total ?? memories.length,
824
+ scoreThreshold,
825
+ requestLimit,
826
+ recallMaxInjectedChars: cfg.recallMaxInjectedChars,
827
+ },
828
+ };
829
+ }
747
830
  return {
748
831
  content: [
749
832
  {
750
833
  type: "text",
751
- text: `Found ${memories.length} memories:\n\n${formatMemoryLines(memories)}`,
834
+ text: `Found ${memoryLines.length} memories:\n\n${memoryLines.join("\n")}`,
752
835
  },
753
836
  ],
754
837
  details: {
755
- count: memories.length,
838
+ count: memoryLines.length,
756
839
  memories,
757
840
  total: result.total ?? memories.length,
758
841
  scoreThreshold,
759
842
  requestLimit,
843
+ recallMaxInjectedChars: cfg.recallMaxInjectedChars,
760
844
  },
761
845
  };
762
846
  },
@@ -1103,6 +1187,242 @@ const contextEnginePlugin = {
1103
1187
  }
1104
1188
  },
1105
1189
  }), { name: "ov_archive_expand" });
1190
+ api.registerTool((ctx) => ({
1191
+ name: "openviking_tool_result_read",
1192
+ label: "Tool Result Read (OpenViking)",
1193
+ description: "Restore the full original content of a tool result that was externalized by OpenViking. " +
1194
+ "Use when a previous tool result was externalized and only a preview is visible — " +
1195
+ "the preview contains a [tool-result-ref] or viking://session/.../tool-results/... URI. " +
1196
+ "\"Read\" tool returns the same truncated preview; this tool returns the complete content. " +
1197
+ "To read all content: pass offset=0 and a limit large enough to cover the whole result " +
1198
+ "(e.g. limit=100000). Use offset/limit for paging only when you need a specific section.",
1199
+ parameters: Type.Object({
1200
+ tool_output_ref: Type.String({
1201
+ description: "Exact OV URI from the preview, e.g. viking://session/<session_id>/tool-results/<tool_result_id>",
1202
+ }),
1203
+ offset: Type.Optional(Type.Number({ description: "Unicode character offset. Default: 0" })),
1204
+ limit: Type.Optional(Type.Number({ description: "Maximum Unicode characters to read. Default: 20000" })),
1205
+ }),
1206
+ async execute(_toolCallId, params) {
1207
+ if (isBypassedSession(ctx)) {
1208
+ return makeBypassedToolResult("openviking_tool_result_read");
1209
+ }
1210
+ const session = resolvePluginSessionRouting(ctx);
1211
+ if (!session.ovSessionId) {
1212
+ return {
1213
+ content: [{ type: "text", text: "Error: no active session." }],
1214
+ details: { error: "no_session" },
1215
+ };
1216
+ }
1217
+ const parsed = parseToolResultRef(params.tool_output_ref ?? params.ref ?? params.uri);
1218
+ if (!parsed) {
1219
+ return {
1220
+ content: [{ type: "text", text: "Error: tool_output_ref must be a viking://session/.../tool-results/... URI." }],
1221
+ details: { error: "invalid_tool_output_ref" },
1222
+ };
1223
+ }
1224
+ if (parsed.sessionId !== session.ovSessionId) {
1225
+ return {
1226
+ content: [{ type: "text", text: "Error: refusing to read a tool result from another session." }],
1227
+ details: {
1228
+ error: "session_mismatch",
1229
+ requestedSessionId: parsed.sessionId,
1230
+ currentSessionId: session.ovSessionId,
1231
+ },
1232
+ };
1233
+ }
1234
+ const offset = Math.max(0, getOptionalInteger(params.offset, 0));
1235
+ const limit = getOptionalInteger(params.limit, 20_000);
1236
+ if (limit < -1) {
1237
+ return {
1238
+ content: [{ type: "text", text: "Error: limit must be -1 or greater than or equal to 0." }],
1239
+ details: { error: "invalid_limit", limit },
1240
+ };
1241
+ }
1242
+ try {
1243
+ const client = await getClient();
1244
+ const result = await client.readToolResult(session.ovSessionId, parsed.toolResultId, { offset, limit, includeMetadata: true }, session.agentId);
1245
+ const returnedChars = result.content.length;
1246
+ const nextOffset = result.offset + returnedChars;
1247
+ const text = result.content || "(empty tool result chunk)";
1248
+ return {
1249
+ content: [{ type: "text", text }],
1250
+ details: {
1251
+ action: "read",
1252
+ tool_output_ref: parsed.ref,
1253
+ tool_result_id: result.tool_result_id,
1254
+ offset: result.offset,
1255
+ limit: result.limit,
1256
+ returned_chars: returnedChars,
1257
+ total_chars: result.total_chars,
1258
+ has_more: result.has_more,
1259
+ next_offset: result.has_more ? nextOffset : null,
1260
+ metadata: result.metadata ?? null,
1261
+ },
1262
+ };
1263
+ }
1264
+ catch (err) {
1265
+ const msg = err instanceof Error ? err.message : String(err);
1266
+ api.logger.warn?.(`openviking: openviking_tool_result_read failed: ${msg}`);
1267
+ return {
1268
+ content: [{ type: "text", text: `Failed to read tool result: ${msg}` }],
1269
+ details: { error: msg, tool_output_ref: parsed.ref },
1270
+ };
1271
+ }
1272
+ },
1273
+ }), { name: "openviking_tool_result_read" });
1274
+ api.registerTool((ctx) => ({
1275
+ name: "openviking_tool_result_search",
1276
+ label: "Tool Result Search (OpenViking)",
1277
+ description: "Search inside an externalized tool result for a keyword. " +
1278
+ "Use when you need to find specific content in a large externalized result, " +
1279
+ "before reading it with openviking_tool_result_read. " +
1280
+ "Returns matching snippets with their character offsets.",
1281
+ parameters: Type.Object({
1282
+ tool_output_ref: Type.String({
1283
+ description: "Exact OV URI from the preview, e.g. viking://session/<session_id>/tool-results/<tool_result_id>",
1284
+ }),
1285
+ query: Type.String({ description: "Keyword or exact text to search for" }),
1286
+ limit: Type.Optional(Type.Number({ description: "Maximum matches. Default: 20" })),
1287
+ context_chars: Type.Optional(Type.Number({ description: "Characters around each match. Default: 300" })),
1288
+ }),
1289
+ async execute(_toolCallId, params) {
1290
+ if (isBypassedSession(ctx)) {
1291
+ return makeBypassedToolResult("openviking_tool_result_search");
1292
+ }
1293
+ const session = resolvePluginSessionRouting(ctx);
1294
+ if (!session.ovSessionId) {
1295
+ return {
1296
+ content: [{ type: "text", text: "Error: no active session." }],
1297
+ details: { error: "no_session" },
1298
+ };
1299
+ }
1300
+ const parsed = parseToolResultRef(params.tool_output_ref ?? params.ref ?? params.uri);
1301
+ if (!parsed) {
1302
+ return {
1303
+ content: [{ type: "text", text: "Error: tool_output_ref must be a viking://session/.../tool-results/... URI." }],
1304
+ details: { error: "invalid_tool_output_ref" },
1305
+ };
1306
+ }
1307
+ if (parsed.sessionId !== session.ovSessionId) {
1308
+ return {
1309
+ content: [{ type: "text", text: "Error: refusing to search a tool result from another session." }],
1310
+ details: {
1311
+ error: "session_mismatch",
1312
+ requestedSessionId: parsed.sessionId,
1313
+ currentSessionId: session.ovSessionId,
1314
+ },
1315
+ };
1316
+ }
1317
+ const query = String(params.query ?? "").trim();
1318
+ if (!query) {
1319
+ return {
1320
+ content: [{ type: "text", text: "Error: query is required." }],
1321
+ details: { error: "missing_param", param: "query" },
1322
+ };
1323
+ }
1324
+ const limit = getPositiveInteger(params.limit, 20);
1325
+ const contextChars = Math.max(0, getOptionalInteger(params.context_chars ?? params.contextChars, 300));
1326
+ try {
1327
+ const client = await getClient();
1328
+ const result = await client.searchToolResult(session.ovSessionId, parsed.toolResultId, query, { limit, contextChars }, session.agentId);
1329
+ const matches = result.matches ?? [];
1330
+ const text = matches.length
1331
+ ? [
1332
+ `Found ${matches.length} match(es) for "${query}" in ${parsed.ref}:`,
1333
+ "",
1334
+ ...matches.map((match, index) => `## Match ${index + 1} (offset ${match.offset})\n${match.snippet}`),
1335
+ ].join("\n")
1336
+ : `No matches found for "${query}" in ${parsed.ref}.`;
1337
+ return {
1338
+ content: [{ type: "text", text }],
1339
+ details: {
1340
+ action: "searched",
1341
+ tool_output_ref: parsed.ref,
1342
+ tool_result_id: result.tool_result_id,
1343
+ query,
1344
+ match_count: matches.length,
1345
+ matches,
1346
+ },
1347
+ };
1348
+ }
1349
+ catch (err) {
1350
+ const msg = err instanceof Error ? err.message : String(err);
1351
+ api.logger.warn?.(`openviking: openviking_tool_result_search failed: ${msg}`);
1352
+ return {
1353
+ content: [{ type: "text", text: `Failed to search tool result: ${msg}` }],
1354
+ details: { error: msg, tool_output_ref: parsed.ref, query },
1355
+ };
1356
+ }
1357
+ },
1358
+ }), { name: "openviking_tool_result_search" });
1359
+ api.registerTool((ctx) => ({
1360
+ name: "openviking_tool_result_list",
1361
+ label: "Tool Result List (OpenViking)",
1362
+ description: "List externalized tool results for the current session. " +
1363
+ "Use to discover available refs before calling openviking_tool_result_read. " +
1364
+ "Optionally filter by tool_name to narrow down results.",
1365
+ parameters: Type.Object({
1366
+ tool_name: Type.Optional(Type.String({ description: "Optional exact tool name filter" })),
1367
+ limit: Type.Optional(Type.Number({ description: "Maximum results. Default: 50" })),
1368
+ }),
1369
+ async execute(_toolCallId, params) {
1370
+ if (isBypassedSession(ctx)) {
1371
+ return makeBypassedToolResult("openviking_tool_result_list");
1372
+ }
1373
+ const session = resolvePluginSessionRouting(ctx);
1374
+ if (!session.ovSessionId) {
1375
+ return {
1376
+ content: [{ type: "text", text: "Error: no active session." }],
1377
+ details: { error: "no_session" },
1378
+ };
1379
+ }
1380
+ const toolName = typeof params.tool_name === "string" && params.tool_name.trim()
1381
+ ? params.tool_name.trim()
1382
+ : typeof params.toolName === "string" && params.toolName.trim()
1383
+ ? params.toolName.trim()
1384
+ : undefined;
1385
+ const limit = getPositiveInteger(params.limit, 50);
1386
+ try {
1387
+ const client = await getClient();
1388
+ const result = await client.listToolResults(session.ovSessionId, { toolName, limit }, session.agentId);
1389
+ const items = result.tool_results ?? [];
1390
+ const text = items.length
1391
+ ? [
1392
+ `Found ${items.length} externalized tool result(s) in current session:`,
1393
+ "",
1394
+ ...items.map((item, index) => {
1395
+ const ref = typeof item.storage_uri === "string" ? item.storage_uri : "(missing ref)";
1396
+ const name = typeof item.tool_name === "string" ? item.tool_name : "tool";
1397
+ const chars = typeof item.original_chars === "number" ? item.original_chars : "unknown";
1398
+ const created = typeof item.created_at === "string" ? ` created_at=${item.created_at}` : "";
1399
+ return `${index + 1}. ${name} original_chars=${chars}${created}\nref: ${ref}`;
1400
+ }),
1401
+ ].join("\n")
1402
+ : toolName
1403
+ ? `No externalized tool results found for tool "${toolName}" in current session.`
1404
+ : "No externalized tool results found in current session.";
1405
+ return {
1406
+ content: [{ type: "text", text }],
1407
+ details: {
1408
+ action: "listed",
1409
+ session_id: session.ovSessionId,
1410
+ tool_name: toolName ?? null,
1411
+ count: items.length,
1412
+ tool_results: items,
1413
+ },
1414
+ };
1415
+ }
1416
+ catch (err) {
1417
+ const msg = err instanceof Error ? err.message : String(err);
1418
+ api.logger.warn?.(`openviking: openviking_tool_result_list failed: ${msg}`);
1419
+ return {
1420
+ content: [{ type: "text", text: `Failed to list tool results: ${msg}` }],
1421
+ details: { error: msg, session_id: session.ovSessionId, tool_name: toolName ?? null },
1422
+ };
1423
+ }
1424
+ },
1425
+ }), { name: "openviking_tool_result_list" });
1106
1426
  let contextEngineRef = null;
1107
1427
  const sessionAgentResolver = createSessionAgentResolver(cfg.agent_prefix);
1108
1428
  const rememberSessionAgentId = (ctx) => {