@openviking/openclaw-plugin 2026.6.2 → 2026.6.17-dev.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.
Files changed (108) hide show
  1. package/INSTALL-AGENT.md +4 -4
  2. package/INSTALL-ZH.md +68 -8
  3. package/INSTALL.md +12 -8
  4. package/README.md +45 -35
  5. package/adapters/http-transport.ts +3 -0
  6. package/adapters/resource-packager.ts +141 -0
  7. package/auto-recall.ts +258 -27
  8. package/client.ts +201 -177
  9. package/commands/setup.ts +200 -33
  10. package/config/feature-gates.json +10 -0
  11. package/config.ts +428 -85
  12. package/context-engine.ts +71 -1402
  13. package/dist/adapters/http-transport.js +1 -0
  14. package/dist/adapters/resource-packager.js +114 -0
  15. package/dist/auto-recall.js +201 -25
  16. package/dist/client.js +125 -161
  17. package/dist/commands/setup.js +170 -32
  18. package/dist/config.js +285 -64
  19. package/dist/context-engine.js +62 -1173
  20. package/dist/index.js +209 -1489
  21. package/dist/memory-ranking.js +35 -12
  22. package/dist/plugin/command-registration.js +5 -0
  23. package/dist/plugin/openviking-archive-tools.js +210 -0
  24. package/dist/plugin/openviking-bypass-runtime.js +8 -0
  25. package/dist/plugin/openviking-client-runtime.js +28 -0
  26. package/dist/plugin/openviking-command-args.js +146 -0
  27. package/dist/plugin/openviking-command-definitions.js +206 -0
  28. package/dist/plugin/openviking-context-engine-ref.js +11 -0
  29. package/dist/plugin/openviking-context-engine-registration.js +23 -0
  30. package/dist/plugin/openviking-feature-gates.js +250 -0
  31. package/dist/plugin/openviking-import-runtime.js +50 -0
  32. package/dist/plugin/openviking-import-tools.js +86 -0
  33. package/dist/plugin/openviking-lifecycle-hooks.js +33 -0
  34. package/dist/plugin/openviking-memory-recall-tools.js +233 -0
  35. package/dist/plugin/openviking-memory-tools.js +199 -0
  36. package/dist/plugin/openviking-query-config-command.js +203 -0
  37. package/dist/plugin/openviking-query-runtime.js +367 -0
  38. package/dist/plugin/openviking-query-tools.js +91 -0
  39. package/dist/plugin/openviking-recall-trace-runtime.js +387 -0
  40. package/dist/plugin/openviking-recall-trace-tools.js +38 -0
  41. package/dist/plugin/openviking-runtime-state.js +26 -0
  42. package/dist/plugin/openviking-runtime-utils.js +62 -0
  43. package/dist/plugin/openviking-services.js +20 -0
  44. package/dist/plugin/openviking-session-routing-runtime.js +61 -0
  45. package/dist/plugin/openviking-tool-result-tools.js +268 -0
  46. package/dist/plugin/recall-trace-routes.js +77 -0
  47. package/dist/plugin/tool-registration.js +23 -0
  48. package/dist/process-manager.js +1 -1
  49. package/dist/query-config.js +336 -0
  50. package/dist/recall-trace.js +309 -0
  51. package/dist/registries/recall-resource-types.js +58 -0
  52. package/dist/routing/identity-routing.js +152 -0
  53. package/dist/routing/memory-uri.js +7 -0
  54. package/dist/services/context-lifecycle-service.js +847 -0
  55. package/dist/services/context-message-adapter.js +340 -0
  56. package/dist/session-transcript-repair.js +0 -1
  57. package/dist/text-utils.js +14 -31
  58. package/index.ts +271 -1873
  59. package/install-manifest.json +10 -1
  60. package/memory-ranking.ts +46 -10
  61. package/openclaw.plugin.json +198 -36
  62. package/package.json +14 -4
  63. package/plugin/command-registration.ts +34 -0
  64. package/plugin/openviking-archive-tools.ts +301 -0
  65. package/plugin/openviking-bypass-runtime.ts +26 -0
  66. package/plugin/openviking-client-runtime.ts +74 -0
  67. package/plugin/openviking-command-args.ts +187 -0
  68. package/plugin/openviking-command-definitions.ts +294 -0
  69. package/plugin/openviking-context-engine-ref.ts +16 -0
  70. package/plugin/openviking-context-engine-registration.ts +97 -0
  71. package/plugin/openviking-feature-gates.ts +335 -0
  72. package/plugin/openviking-import-runtime.ts +105 -0
  73. package/plugin/openviking-import-tools.ts +133 -0
  74. package/plugin/openviking-lifecycle-hooks.ts +65 -0
  75. package/plugin/openviking-memory-recall-tools.ts +368 -0
  76. package/plugin/openviking-memory-tools.ts +299 -0
  77. package/plugin/openviking-query-config-command.ts +230 -0
  78. package/plugin/openviking-query-formatters.ts +108 -0
  79. package/plugin/openviking-query-runtime.ts +476 -0
  80. package/plugin/openviking-query-tools.ts +161 -0
  81. package/plugin/openviking-recall-trace-runtime.ts +482 -0
  82. package/plugin/openviking-recall-trace-tools.ts +99 -0
  83. package/plugin/openviking-runtime-state.ts +52 -0
  84. package/plugin/openviking-runtime-utils.ts +67 -0
  85. package/plugin/openviking-services.ts +49 -0
  86. package/plugin/openviking-session-routing-runtime.ts +105 -0
  87. package/plugin/openviking-tool-result-tools.ts +397 -0
  88. package/plugin/recall-trace-routes.ts +133 -0
  89. package/plugin/tool-registration.ts +51 -0
  90. package/process-manager.ts +1 -1
  91. package/query-config.ts +400 -0
  92. package/recall-trace.ts +442 -0
  93. package/registries/openviking-tools.ts +84 -0
  94. package/registries/recall-resource-types.ts +74 -0
  95. package/routing/identity-routing.ts +210 -0
  96. package/routing/memory-uri.ts +8 -0
  97. package/services/context-lifecycle-service.ts +1209 -0
  98. package/services/context-message-adapter.ts +375 -0
  99. package/services/setup/config-writer.ts +125 -0
  100. package/services/setup/exit-utils.ts +5 -0
  101. package/services/setup/package-metadata.ts +56 -0
  102. package/services/setup/probe-service.ts +143 -0
  103. package/services/setup/setup-flow.ts +306 -0
  104. package/services/setup/version-compatibility.ts +41 -0
  105. package/session-transcript-repair.ts +0 -2
  106. package/skills/install-openviking-memory/SKILL.md +123 -50
  107. package/skills/openviking-context-database/SKILL.md +286 -0
  108. package/text-utils.ts +15 -34
@@ -146,6 +146,13 @@ const QUERY_TOKEN_STOPWORDS = new Set([
146
146
  "your",
147
147
  "you",
148
148
  ]);
149
+ const DEFAULT_RANKING_WEIGHTS = {
150
+ baseScore: 1,
151
+ leaf: 0.12,
152
+ event: 0.1,
153
+ preference: 0.08,
154
+ lexicalOverlapMax: 0.2,
155
+ };
149
156
  function buildRecallQueryProfile(query) {
150
157
  const text = query.trim();
151
158
  const allTokens = text.toLowerCase().match(QUERY_TOKEN_RE) ?? [];
@@ -156,7 +163,7 @@ function buildRecallQueryProfile(query) {
156
163
  wantsTemporal: TEMPORAL_QUERY_RE.test(text),
157
164
  };
158
165
  }
159
- function lexicalOverlapBoost(tokens, text) {
166
+ function lexicalOverlapBoost(tokens, text, maxBoost = DEFAULT_RANKING_WEIGHTS.lexicalOverlapMax) {
160
167
  if (tokens.length === 0 || !text) {
161
168
  return 0;
162
169
  }
@@ -167,24 +174,40 @@ function lexicalOverlapBoost(tokens, text) {
167
174
  matched += 1;
168
175
  }
169
176
  }
170
- return Math.min(0.2, (matched / Math.min(tokens.length, 4)) * 0.2);
171
- }
172
- function rankForInjection(item, query) {
177
+ return Math.min(maxBoost, (matched / Math.min(tokens.length, 4)) * maxBoost);
178
+ }
179
+ function inferResourceType(item) {
180
+ if (item.uri.startsWith("viking://resources"))
181
+ return "resource";
182
+ if (item.uri.startsWith("viking://session/"))
183
+ return "session";
184
+ if (item.uri.startsWith("viking://user/skills") || item.uri.startsWith("viking://skills"))
185
+ return "agent";
186
+ if (item.uri.startsWith("viking://user/"))
187
+ return "user";
188
+ return undefined;
189
+ }
190
+ function rankForInjection(item, query, options) {
173
191
  // Keep ranking simple and stable: semantic score + light query-aware boosts.
174
- const baseScore = clampScore(item.score);
192
+ const weights = { ...DEFAULT_RANKING_WEIGHTS, ...(options?.weights ?? {}) };
193
+ const baseScore = clampScore(item.score) * weights.baseScore;
175
194
  const abstract = (item.abstract ?? item.overview ?? "").trim();
176
- const leafBoost = isLeafLikeMemory(item) ? 0.12 : 0;
177
- const eventBoost = query.wantsTemporal && isEventMemory(item) ? 0.1 : 0;
178
- const preferenceBoost = query.wantsPreference && isPreferencesMemory(item) ? 0.08 : 0;
179
- const overlapBoost = lexicalOverlapBoost(query.tokens, `${item.uri} ${abstract}`);
180
- return baseScore + leafBoost + eventBoost + preferenceBoost + overlapBoost;
195
+ const leafBoost = isLeafLikeMemory(item) ? weights.leaf : 0;
196
+ const eventBoost = query.wantsTemporal && isEventMemory(item) ? weights.event : 0;
197
+ const preferenceBoost = query.wantsPreference && isPreferencesMemory(item) ? weights.preference : 0;
198
+ const overlapBoost = lexicalOverlapBoost(query.tokens, `${item.uri} ${abstract}`, weights.lexicalOverlapMax);
199
+ const category = (item.category ?? "").toLowerCase();
200
+ const categoryBoost = category ? (options?.categoryWeights?.[category] ?? options?.categoryWeights?.[item.category ?? ""] ?? 0) : 0;
201
+ const resourceType = inferResourceType(item);
202
+ const resourceTypeBoost = resourceType ? (options?.resourceTypeWeights?.[resourceType] ?? 0) : 0;
203
+ return baseScore + leafBoost + eventBoost + preferenceBoost + overlapBoost + categoryBoost + resourceTypeBoost;
181
204
  }
182
- export function pickMemoriesForInjection(items, limit, queryText, scoreThreshold = 0) {
205
+ export function pickMemoriesForInjection(items, limit, queryText, scoreThreshold = 0, rankingOptions) {
183
206
  if (items.length === 0 || limit <= 0) {
184
207
  return [];
185
208
  }
186
209
  const query = buildRecallQueryProfile(queryText);
187
- const sorted = [...items].sort((a, b) => rankForInjection(b, query) - rankForInjection(a, query));
210
+ const sorted = [...items].sort((a, b) => rankForInjection(b, query, rankingOptions) - rankForInjection(a, query, rankingOptions));
188
211
  const deduped = [];
189
212
  const seen = new Set();
190
213
  for (const item of sorted) {
@@ -0,0 +1,5 @@
1
+ export function registerOpenVikingCommands(api, commands) {
2
+ for (const command of commands) {
3
+ api.registerCommand?.(command);
4
+ }
5
+ }
@@ -0,0 +1,210 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ function previewText(value, maxChars) {
3
+ const text = typeof value === "string" ? value.replace(/\s+/g, " ").trim() : "";
4
+ if (!text) {
5
+ return undefined;
6
+ }
7
+ return text.length <= maxChars ? text : `${text.slice(0, Math.max(0, maxChars - 1))}…`;
8
+ }
9
+ export function registerOpenVikingArchiveTools(deps) {
10
+ deps.registerTool((ctx) => ({
11
+ name: "ov_archive_search",
12
+ label: "Archive Search (OpenViking)",
13
+ description: "Keyword-grep across all archived original conversation messages of the current session. " +
14
+ "Use this whenever the [Session History Summary] does not contain the specific detail " +
15
+ "the user is asking about. Extract 2-3 concrete entity words from the question " +
16
+ "(names, places, objects, dates) and search each separately. " +
17
+ "Only conclude information is unavailable after trying at least 2 different keyword variations.",
18
+ parameters: Type.Object({
19
+ query: Type.String({
20
+ description: "A single keyword or short phrase to grep. Use concrete nouns, names, dates, " +
21
+ "or distinctive phrases. Case-insensitive. Prefer entity words over full sentences.",
22
+ }),
23
+ archiveId: Type.Optional(Type.String({
24
+ description: 'Optional: limit search to one archive (e.g. "archive_005")',
25
+ })),
26
+ }),
27
+ async execute(_toolCallId, params) {
28
+ if (deps.isBypassedSession(ctx)) {
29
+ return deps.makeBypassedToolResult("ov_archive_search");
30
+ }
31
+ deps.rememberSessionAgentId(ctx);
32
+ const sessionId = ctx.sessionId ?? "";
33
+ const sessionKey = ctx.sessionKey ?? "";
34
+ if (!sessionId && !sessionKey) {
35
+ return {
36
+ content: [{ type: "text", text: "Error: no active session." }],
37
+ details: { error: "no_session" },
38
+ };
39
+ }
40
+ const ovSessionId = deps.toOvSessionId(ctx.sessionId, ctx.sessionKey);
41
+ const query = String(params.query ?? "").trim();
42
+ const archiveId = params.archiveId;
43
+ if (!query) {
44
+ return {
45
+ content: [{ type: "text", text: "Error: query is required." }],
46
+ details: { error: "missing_param", param: "query" },
47
+ };
48
+ }
49
+ const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
50
+ deps.logger?.info?.(`openviking: ov_archive_search query="${query}" escaped="${escapedQuery}" archive=${archiveId ?? "all"} session=${ovSessionId}`);
51
+ try {
52
+ const client = await deps.getClient();
53
+ const agentId = deps.resolveAgentId(ctx.sessionId, ctx.sessionKey);
54
+ const started = Date.now();
55
+ const result = await client.grepSessionArchives(ovSessionId, escapedQuery, {
56
+ archiveId,
57
+ caseInsensitive: true,
58
+ agentId,
59
+ });
60
+ const traceResults = (result.matches ?? []).slice(0, deps.traceRecallMaxResultsPerSearch).map((match) => ({
61
+ uri: match.uri,
62
+ resourceType: "archive",
63
+ abstractPreview: previewText(match.content, deps.traceRecallPreviewChars),
64
+ resultType: "archive_match",
65
+ }));
66
+ const recordArchiveTrace = async (displayed) => {
67
+ await deps.traceRecorder?.recordAndFlush({
68
+ schemaVersion: "1.0",
69
+ traceId: deps.createTraceId("ov_archive_search"),
70
+ ts: Date.now(),
71
+ sessionId: ctx.sessionId,
72
+ sessionKey: ctx.sessionKey,
73
+ ovSessionId,
74
+ agentId,
75
+ source: "ov_archive_search",
76
+ operationType: "archive_grep",
77
+ resourceTypes: ["session"],
78
+ trigger: { query, derivedKeywords: [query] },
79
+ searches: [{
80
+ resourceType: "archive",
81
+ targetUriResolved: archiveId ? `viking://session/${ovSessionId}/history/${archiveId}` : `viking://session/${ovSessionId}/history`,
82
+ limit: deps.traceRecallMaxResultsPerSearch,
83
+ durationMs: Date.now() - started,
84
+ total: result.matches?.length ?? result.count ?? 0,
85
+ results: traceResults,
86
+ archiveId,
87
+ caseInsensitive: true,
88
+ }],
89
+ selected: displayed.map((match) => ({
90
+ uri: match.uri,
91
+ resourceType: "archive",
92
+ line: match.line,
93
+ abstractPreview: previewText(match.content, deps.traceRecallPreviewChars),
94
+ displayed: true,
95
+ })),
96
+ stats: {
97
+ candidateCount: result.matches?.length ?? result.count ?? 0,
98
+ selectedCount: displayed.length,
99
+ injectedCount: 0,
100
+ },
101
+ });
102
+ };
103
+ if (!result.matches || result.matches.length === 0) {
104
+ await recordArchiveTrace([]);
105
+ return {
106
+ content: [{
107
+ type: "text",
108
+ text: `No matches found for "${query}". Try a different keyword — ` +
109
+ "the original conversation may use different wording than the question. " +
110
+ "Try synonyms, related terms, or shorter fragments.",
111
+ }],
112
+ details: { query, matchCount: 0 },
113
+ };
114
+ }
115
+ const MAX_MATCHES = 12;
116
+ const MAX_LINE_LEN = 1500;
117
+ const shown = result.matches.slice(0, MAX_MATCHES);
118
+ await recordArchiveTrace(shown);
119
+ const blocks = shown.map((m, i) => {
120
+ const archiveTag = m.uri.match(/archive_\d+/)?.[0] ?? "unknown";
121
+ const truncated = m.content.length > MAX_LINE_LEN
122
+ ? m.content.slice(0, MAX_LINE_LEN) + "…(truncated)"
123
+ : m.content;
124
+ return `## Match ${i + 1}: ${archiveTag} (line ${m.line})\n${truncated}`;
125
+ });
126
+ const header = `Found ${result.matches.length} match(es) for "${query}"` +
127
+ (result.matches.length > MAX_MATCHES ? ` (showing first ${MAX_MATCHES})` : "") + ":";
128
+ return {
129
+ content: [{ type: "text", text: header + "\n\n" + blocks.join("\n\n") }],
130
+ details: { query, matchCount: result.matches.length },
131
+ };
132
+ }
133
+ catch (err) {
134
+ const msg = err instanceof Error ? err.message : String(err);
135
+ deps.logger?.error?.(`openviking: ov_archive_search error: ${msg}`);
136
+ return {
137
+ content: [{ type: "text", text: `Archive search failed: ${msg}` }],
138
+ details: { error: msg },
139
+ };
140
+ }
141
+ },
142
+ }), { name: "ov_archive_search" });
143
+ deps.registerTool((ctx) => ({
144
+ name: "ov_archive_expand",
145
+ label: "Archive Expand (OpenViking)",
146
+ description: "Retrieve original messages from a compressed session archive. " +
147
+ "Use when a session summary lacks specific details " +
148
+ "such as exact commands, file paths, code snippets, or config values. " +
149
+ "Check [Archive Index] to find the right archive ID.",
150
+ parameters: Type.Object({
151
+ archiveId: Type.String({
152
+ description: 'Archive ID from [Archive Index] (e.g. "archive_002")',
153
+ }),
154
+ }),
155
+ async execute(_toolCallId, params) {
156
+ if (deps.isBypassedSession(ctx)) {
157
+ return deps.makeBypassedToolResult("ov_archive_expand");
158
+ }
159
+ const session = deps.resolvePluginSessionRouting(ctx);
160
+ const archiveId = String(params.archiveId ?? "").trim();
161
+ const sessionId = session.sessionId ?? "";
162
+ deps.logger?.info?.(`openviking: ov_archive_expand invoked (archiveId=${archiveId || "(empty)"}, sessionId=${sessionId || "(empty)"})`);
163
+ if (!archiveId) {
164
+ deps.logger?.warn?.("openviking: ov_archive_expand missing archiveId");
165
+ return {
166
+ content: [{ type: "text", text: "Error: archiveId is required." }],
167
+ details: { error: "missing_param", param: "archiveId" },
168
+ };
169
+ }
170
+ if (!session.ovSessionId) {
171
+ return {
172
+ content: [{ type: "text", text: "Error: no active session." }],
173
+ details: { error: "no_session" },
174
+ };
175
+ }
176
+ try {
177
+ const client = await deps.getClient();
178
+ const detail = await client.getSessionArchive(session.ovSessionId, archiveId, session.agentId);
179
+ const header = [
180
+ `## ${detail.archive_id}`,
181
+ detail.abstract ? `**Summary**: ${detail.abstract}` : "",
182
+ `**Messages**: ${detail.messages.length}`,
183
+ "",
184
+ ].filter(Boolean).join("\n");
185
+ const body = detail.messages
186
+ .map((message) => deps.formatMessage(message))
187
+ .join("\n\n");
188
+ deps.logger?.info?.(`openviking: ov_archive_expand expanded ${detail.archive_id}, messages=${detail.messages.length}, chars=${body.length}, sessionId=${sessionId}`);
189
+ return {
190
+ content: [{ type: "text", text: `${header}\n${body}` }],
191
+ details: {
192
+ action: "expanded",
193
+ archiveId: detail.archive_id,
194
+ messageCount: detail.messages.length,
195
+ sessionId,
196
+ ovSessionId: session.ovSessionId,
197
+ },
198
+ };
199
+ }
200
+ catch (err) {
201
+ const msg = err instanceof Error ? err.message : String(err);
202
+ deps.logger?.warn?.(`openviking: ov_archive_expand failed (archiveId=${archiveId}, sessionId=${sessionId}): ${msg}`);
203
+ return {
204
+ content: [{ type: "text", text: `Failed to expand ${archiveId}: ${msg}` }],
205
+ details: { error: msg, archiveId, sessionId, ovSessionId: session.ovSessionId },
206
+ };
207
+ }
208
+ },
209
+ }), { name: "ov_archive_expand" });
210
+ }
@@ -0,0 +1,8 @@
1
+ import { compileSessionPatterns, shouldBypassSession, } from "../text-utils.js";
2
+ export function createOpenVikingBypassRuntime(options) {
3
+ const bypassSessionPatterns = compileSessionPatterns(options.cfg.bypassSessionPatterns);
4
+ const isBypassedSession = (ctx) => shouldBypassSession(ctx ?? {}, bypassSessionPatterns);
5
+ return {
6
+ isBypassedSession,
7
+ };
8
+ }
@@ -0,0 +1,28 @@
1
+ import { OpenVikingClient } from "../client.js";
2
+ export function createOpenVikingClientRuntime(options) {
3
+ const { cfg, logger } = options;
4
+ if (cfg.logFindRequests) {
5
+ logger.info("openviking: routing debug logging enabled (config logFindRequests, or env OPENVIKING_LOG_ROUTING=1 / OPENVIKING_DEBUG=1)");
6
+ }
7
+ const verboseRoutingInfo = (message) => {
8
+ if (cfg.logFindRequests) {
9
+ logger.info(message);
10
+ }
11
+ };
12
+ verboseRoutingInfo(`openviking: loaded plugin config peer_role="${cfg.peer_role}" peer_prefix="${cfg.peer_prefix}" ` +
13
+ `(raw peer_prefix=${JSON.stringify(options.rawPeerPrefix ?? "(missing)")}; ` +
14
+ `${cfg.peer_prefix
15
+ ? 'non-empty → assistant peer_id is <peer_prefix>_<ctx.agentId> when peer_role="assistant", or <peer_prefix>_main when ctx.agentId is unknown'
16
+ : 'empty → assistant peer_id follows OpenClaw ctx.agentId when peer_role="assistant", or "main" when ctx.agentId is unknown'})`);
17
+ const routingDebugLog = cfg.logFindRequests
18
+ ? (msg) => {
19
+ logger.info(msg);
20
+ }
21
+ : undefined;
22
+ const clientPromise = Promise.resolve(new OpenVikingClient(cfg.baseUrl, cfg.apiKey, cfg.peer_prefix, cfg.timeoutMs, cfg.accountId, cfg.userId, routingDebugLog, { transport: options.transport }));
23
+ const getClient = () => clientPromise;
24
+ return {
25
+ getClient,
26
+ verboseRoutingInfo,
27
+ };
28
+ }
@@ -0,0 +1,146 @@
1
+ export function tokenizeCommandArgs(args) {
2
+ const tokens = [];
3
+ let current = "";
4
+ let quote = null;
5
+ let escaping = false;
6
+ for (let i = 0; i < args.length; i += 1) {
7
+ const ch = args[i];
8
+ const next = args[i + 1];
9
+ if (escaping) {
10
+ current += ch;
11
+ escaping = false;
12
+ continue;
13
+ }
14
+ if (ch === "\\") {
15
+ const shouldEscape = quote === '"'
16
+ ? next === '"' || next === "\\"
17
+ : !quote && Boolean(next && (/\s/.test(next) || next === '"' || next === "'"));
18
+ if (shouldEscape) {
19
+ escaping = true;
20
+ continue;
21
+ }
22
+ current += ch;
23
+ continue;
24
+ }
25
+ if ((ch === '"' || ch === "'") && (!quote || quote === ch)) {
26
+ quote = quote ? null : ch;
27
+ continue;
28
+ }
29
+ if (!quote && /\s/.test(ch)) {
30
+ if (current) {
31
+ tokens.push(current);
32
+ current = "";
33
+ }
34
+ continue;
35
+ }
36
+ current += ch;
37
+ }
38
+ if (escaping) {
39
+ current += "\\";
40
+ }
41
+ if (quote) {
42
+ throw new Error("Unterminated quoted argument");
43
+ }
44
+ if (current) {
45
+ tokens.push(current);
46
+ }
47
+ return tokens;
48
+ }
49
+ function parseFlagArgs(args) {
50
+ const tokens = tokenizeCommandArgs(args);
51
+ const positionals = [];
52
+ const flags = new Map();
53
+ for (let i = 0; i < tokens.length; i += 1) {
54
+ const token = tokens[i];
55
+ if (!token.startsWith("--")) {
56
+ positionals.push(token);
57
+ continue;
58
+ }
59
+ const raw = token.slice(2);
60
+ if (!raw) {
61
+ continue;
62
+ }
63
+ const eqIndex = raw.indexOf("=");
64
+ if (eqIndex >= 0) {
65
+ flags.set(raw.slice(0, eqIndex), raw.slice(eqIndex + 1));
66
+ continue;
67
+ }
68
+ const next = tokens[i + 1];
69
+ if (next && !next.startsWith("--")) {
70
+ flags.set(raw, next);
71
+ i += 1;
72
+ }
73
+ else {
74
+ flags.set(raw, true);
75
+ }
76
+ }
77
+ return { positionals, flags };
78
+ }
79
+ function getStringFlag(flags, name) {
80
+ const value = flags.get(name);
81
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
82
+ }
83
+ function getNumberFlag(flags, name) {
84
+ const raw = getStringFlag(flags, name);
85
+ if (!raw) {
86
+ return undefined;
87
+ }
88
+ const value = Number(raw);
89
+ if (!Number.isFinite(value)) {
90
+ throw new Error(`--${name} must be a number`);
91
+ }
92
+ return value;
93
+ }
94
+ function getBoolFlag(flags, name) {
95
+ return flags.get(name) === true;
96
+ }
97
+ export function parseAddResourceCommandArgs(args) {
98
+ const parsed = parseFlagArgs(args);
99
+ const source = parsed.positionals.length <= 1 ? parsed.positionals[0] : parsed.positionals.join(" ").trim();
100
+ if (!source) {
101
+ throw new Error("Usage: /add-resource <source> [--to URI] [--parent URI] [--reason TEXT] [--instruction TEXT] [--wait] [--timeout SEC]");
102
+ }
103
+ const to = getStringFlag(parsed.flags, "to");
104
+ const parent = getStringFlag(parsed.flags, "parent");
105
+ if (to && parent) {
106
+ throw new Error("Cannot specify both --to and --parent.");
107
+ }
108
+ return {
109
+ source,
110
+ to,
111
+ parent,
112
+ reason: getStringFlag(parsed.flags, "reason"),
113
+ instruction: getStringFlag(parsed.flags, "instruction"),
114
+ wait: getBoolFlag(parsed.flags, "wait"),
115
+ timeout: getNumberFlag(parsed.flags, "timeout"),
116
+ };
117
+ }
118
+ export function parseAddSkillCommandArgs(args) {
119
+ const parsed = parseFlagArgs(args);
120
+ const source = parsed.positionals.length <= 1 ? parsed.positionals[0] : parsed.positionals.join(" ").trim();
121
+ if (!source) {
122
+ throw new Error("Usage: /add-skill <source> [--wait] [--timeout SEC]");
123
+ }
124
+ if (parsed.flags.has("to") || parsed.flags.has("parent") || parsed.flags.has("reason") || parsed.flags.has("instruction")) {
125
+ throw new Error("--to, --parent, --reason, and --instruction are resource-only options.");
126
+ }
127
+ return {
128
+ source,
129
+ wait: getBoolFlag(parsed.flags, "wait"),
130
+ timeout: getNumberFlag(parsed.flags, "timeout"),
131
+ };
132
+ }
133
+ export function parseOVSearchCommandArgs(args) {
134
+ const parsed = parseFlagArgs(args);
135
+ // `/ov-search` only accepts a single query string, so positional segments are
136
+ // always re-joined to preserve unquoted multi-word searches.
137
+ const query = parsed.positionals.join(" ").trim();
138
+ if (!query) {
139
+ throw new Error('Usage: /ov-search "<query>" [--uri URI] [--limit N]');
140
+ }
141
+ return {
142
+ query,
143
+ uri: getStringFlag(parsed.flags, "uri"),
144
+ limit: getNumberFlag(parsed.flags, "limit"),
145
+ };
146
+ }
@@ -0,0 +1,206 @@
1
+ function tokenizeCommandArgs(args) {
2
+ const tokens = [];
3
+ let current = "";
4
+ let quote = null;
5
+ let escaping = false;
6
+ for (let i = 0; i < args.length; i += 1) {
7
+ const ch = args[i];
8
+ const next = args[i + 1];
9
+ if (escaping) {
10
+ current += ch;
11
+ escaping = false;
12
+ continue;
13
+ }
14
+ if (ch === "\\") {
15
+ const shouldEscape = quote === '"'
16
+ ? next === '"' || next === "\\"
17
+ : !quote && Boolean(next && (/\s/.test(next) || next === '"' || next === "'"));
18
+ if (shouldEscape) {
19
+ escaping = true;
20
+ continue;
21
+ }
22
+ current += ch;
23
+ continue;
24
+ }
25
+ if ((ch === '"' || ch === "'") && (!quote || quote === ch)) {
26
+ quote = quote ? null : ch;
27
+ continue;
28
+ }
29
+ if (!quote && /\s/.test(ch)) {
30
+ if (current) {
31
+ tokens.push(current);
32
+ current = "";
33
+ }
34
+ continue;
35
+ }
36
+ current += ch;
37
+ }
38
+ if (escaping) {
39
+ current += "\\";
40
+ }
41
+ if (quote) {
42
+ throw new Error("Unterminated quoted argument");
43
+ }
44
+ if (current) {
45
+ tokens.push(current);
46
+ }
47
+ return tokens;
48
+ }
49
+ function parseFlagArgs(args) {
50
+ const tokens = tokenizeCommandArgs(args);
51
+ const flags = new Map();
52
+ for (let i = 0; i < tokens.length; i += 1) {
53
+ const token = tokens[i];
54
+ if (!token.startsWith("--")) {
55
+ continue;
56
+ }
57
+ const raw = token.slice(2);
58
+ if (!raw) {
59
+ continue;
60
+ }
61
+ const eqIndex = raw.indexOf("=");
62
+ if (eqIndex >= 0) {
63
+ flags.set(raw.slice(0, eqIndex), raw.slice(eqIndex + 1));
64
+ continue;
65
+ }
66
+ const next = tokens[i + 1];
67
+ if (next && !next.startsWith("--")) {
68
+ flags.set(raw, next);
69
+ i += 1;
70
+ }
71
+ else {
72
+ flags.set(raw, true);
73
+ }
74
+ }
75
+ return { flags };
76
+ }
77
+ function getStringFlag(flags, name) {
78
+ const value = flags.get(name);
79
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
80
+ }
81
+ function getNumberFlag(flags, name) {
82
+ const raw = getStringFlag(flags, name);
83
+ if (!raw) {
84
+ return undefined;
85
+ }
86
+ const value = Number(raw);
87
+ if (!Number.isFinite(value)) {
88
+ throw new Error(`--${name} must be a number`);
89
+ }
90
+ return value;
91
+ }
92
+ function getBoolFlag(flags, name) {
93
+ return flags.get(name) === true;
94
+ }
95
+ function parseRecallTraceCommandArgs(args) {
96
+ const flags = parseFlagArgs(args).flags;
97
+ return {
98
+ turn: getStringFlag(flags, "turn"),
99
+ traceId: getStringFlag(flags, "trace-id"),
100
+ sessionId: getStringFlag(flags, "session-id"),
101
+ sessionKey: getStringFlag(flags, "session-key"),
102
+ ovSessionId: getStringFlag(flags, "ov-session-id"),
103
+ source: getStringFlag(flags, "source"),
104
+ resourceTypes: getStringFlag(flags, "resource-types"),
105
+ since: getNumberFlag(flags, "since"),
106
+ until: getNumberFlag(flags, "until"),
107
+ includeContent: getBoolFlag(flags, "include-content"),
108
+ limit: getNumberFlag(flags, "limit"),
109
+ };
110
+ }
111
+ function toCommandResult(result) {
112
+ return { text: result.content[0].text, details: result.details };
113
+ }
114
+ export function createOpenVikingCommandDefinitions(deps) {
115
+ return [
116
+ {
117
+ name: "add-resource",
118
+ description: "Add a resource into OpenViking.",
119
+ acceptsArgs: true,
120
+ handler: async (ctx) => {
121
+ try {
122
+ if (deps.isBypassedSession(ctx)) {
123
+ return toCommandResult(deps.makeBypassedToolResult("add_resource"));
124
+ }
125
+ const session = deps.resolvePluginSessionRouting(ctx);
126
+ const input = deps.parseAddResourceCommandArgs(ctx.args ?? "");
127
+ return toCommandResult(await deps.addResourceOpenViking(input, session.agentId));
128
+ }
129
+ catch (err) {
130
+ return { text: `OpenViking add resource failed: ${err instanceof Error ? err.message : String(err)}` };
131
+ }
132
+ },
133
+ },
134
+ {
135
+ name: "add-skill",
136
+ description: "Add a skill into OpenViking.",
137
+ acceptsArgs: true,
138
+ handler: async (ctx) => {
139
+ try {
140
+ if (deps.isBypassedSession(ctx)) {
141
+ return toCommandResult(deps.makeBypassedToolResult("add_skill"));
142
+ }
143
+ const session = deps.resolvePluginSessionRouting(ctx);
144
+ const input = deps.parseAddSkillCommandArgs(ctx.args ?? "");
145
+ return toCommandResult(await deps.addSkillOpenViking(input, session.agentId));
146
+ }
147
+ catch (err) {
148
+ return { text: `OpenViking add skill failed: ${err instanceof Error ? err.message : String(err)}` };
149
+ }
150
+ },
151
+ },
152
+ {
153
+ name: "ov-search",
154
+ description: "Search OpenViking resources and skills.",
155
+ acceptsArgs: true,
156
+ handler: async (ctx) => {
157
+ try {
158
+ if (deps.isBypassedSession(ctx)) {
159
+ return toCommandResult(deps.makeBypassedToolResult("ov_search"));
160
+ }
161
+ const session = deps.resolvePluginSessionRouting(ctx);
162
+ const input = deps.parseOVSearchCommandArgs(ctx.args ?? "");
163
+ return toCommandResult(await deps.searchOpenViking(input, session.agentId, session));
164
+ }
165
+ catch (err) {
166
+ return { text: `OpenViking search failed: ${err instanceof Error ? err.message : String(err)}` };
167
+ }
168
+ },
169
+ },
170
+ {
171
+ name: "ov-query-config",
172
+ description: "Get or set runtime OpenViking query parameters for the current claw/session.",
173
+ acceptsArgs: true,
174
+ handler: async (ctx) => {
175
+ try {
176
+ return await deps.handleQueryConfigCommand(ctx);
177
+ }
178
+ catch (err) {
179
+ return { text: `OpenViking query config failed: ${err instanceof Error ? err.message : String(err)}` };
180
+ }
181
+ },
182
+ },
183
+ {
184
+ name: "ov-recall-trace",
185
+ description: "Query OpenViking recall trace records.",
186
+ acceptsArgs: true,
187
+ handler: async (ctx) => {
188
+ try {
189
+ if (deps.isBypassedSession(ctx)) {
190
+ return toCommandResult(deps.makeBypassedToolResult("ov_recall_trace"));
191
+ }
192
+ const session = deps.resolvePluginSessionRouting(ctx);
193
+ const input = parseRecallTraceCommandArgs(ctx.args ?? "");
194
+ const result = await deps.queryRecallTraces(input, session);
195
+ return {
196
+ text: deps.formatRecallTraceText(result),
197
+ details: { count: result.entries.length, lookupLayer: result.lookupLayer, warnings: result.warnings, entries: result.entries },
198
+ };
199
+ }
200
+ catch (err) {
201
+ return { text: `OpenViking recall trace query failed: ${err instanceof Error ? err.message : String(err)}` };
202
+ }
203
+ },
204
+ },
205
+ ];
206
+ }