@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
@@ -0,0 +1,301 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import type { OVMessage } from "../client.js";
3
+ import type { RecallTraceEntry, RecallTraceResult } from "../recall-trace.js";
4
+
5
+ export type OpenVikingArchiveToolContext = {
6
+ sessionKey?: string;
7
+ sessionId?: string;
8
+ agentId?: string;
9
+ senderId?: string;
10
+ };
11
+
12
+ export type OpenVikingArchiveSession = {
13
+ sessionId?: string;
14
+ sessionKey?: string;
15
+ ovSessionId?: string;
16
+ agentId: string;
17
+ };
18
+
19
+ type OpenVikingArchiveMatch = {
20
+ uri: string;
21
+ line: number;
22
+ content: string;
23
+ };
24
+
25
+ export type OpenVikingArchiveClient = {
26
+ grepSessionArchives: (
27
+ sessionId: string,
28
+ pattern: string,
29
+ options: { archiveId?: string; caseInsensitive: boolean; agentId?: string },
30
+ ) => Promise<{
31
+ count?: number;
32
+ matches?: OpenVikingArchiveMatch[];
33
+ }>;
34
+ getSessionArchive: (
35
+ sessionId: string,
36
+ archiveId: string,
37
+ agentId?: string,
38
+ ) => Promise<{
39
+ archive_id: string;
40
+ abstract?: string;
41
+ messages: OVMessage[];
42
+ }>;
43
+ };
44
+
45
+ export type OpenVikingArchiveToolsDeps = {
46
+ registerTool: (toolOrFactory: unknown, opts: { name: string }) => void;
47
+ getClient: () => Promise<OpenVikingArchiveClient>;
48
+ rememberSessionAgentId: (ctx: OpenVikingArchiveToolContext) => void;
49
+ toOvSessionId: (sessionId?: string, sessionKey?: string) => string;
50
+ resolveAgentId: (sessionId?: string, sessionKey?: string, ovSessionId?: string) => string;
51
+ resolvePluginSessionRouting: (ctx?: OpenVikingArchiveToolContext) => OpenVikingArchiveSession;
52
+ isBypassedSession: (ctx?: OpenVikingArchiveToolContext) => boolean;
53
+ makeBypassedToolResult: (toolName: string) => unknown;
54
+ formatMessage: (message: OVMessage) => string;
55
+ traceRecorder?: { recordAndFlush: (trace: RecallTraceEntry) => Promise<unknown> | unknown };
56
+ traceRecallMaxResultsPerSearch: number;
57
+ traceRecallPreviewChars: number;
58
+ createTraceId: (source: string) => string;
59
+ logger?: {
60
+ info?: (message: string) => void;
61
+ warn?: (message: string) => void;
62
+ error?: (message: string) => void;
63
+ };
64
+ };
65
+
66
+ function previewText(value: unknown, maxChars: number): string | undefined {
67
+ const text = typeof value === "string" ? value.replace(/\s+/g, " ").trim() : "";
68
+ if (!text) {
69
+ return undefined;
70
+ }
71
+ return text.length <= maxChars ? text : `${text.slice(0, Math.max(0, maxChars - 1))}…`;
72
+ }
73
+
74
+ export function registerOpenVikingArchiveTools(deps: OpenVikingArchiveToolsDeps): void {
75
+ deps.registerTool(
76
+ (ctx: OpenVikingArchiveToolContext) => ({
77
+ name: "ov_archive_search",
78
+ label: "Archive Search (OpenViking)",
79
+ description:
80
+ "Keyword-grep across all archived original conversation messages of the current session. " +
81
+ "Use this whenever the [Session History Summary] does not contain the specific detail " +
82
+ "the user is asking about. Extract 2-3 concrete entity words from the question " +
83
+ "(names, places, objects, dates) and search each separately. " +
84
+ "Only conclude information is unavailable after trying at least 2 different keyword variations.",
85
+ parameters: Type.Object({
86
+ query: Type.String({
87
+ description:
88
+ "A single keyword or short phrase to grep. Use concrete nouns, names, dates, " +
89
+ "or distinctive phrases. Case-insensitive. Prefer entity words over full sentences.",
90
+ }),
91
+ archiveId: Type.Optional(
92
+ Type.String({
93
+ description: 'Optional: limit search to one archive (e.g. "archive_005")',
94
+ }),
95
+ ),
96
+ }),
97
+ async execute(_toolCallId: string, params: Record<string, unknown>) {
98
+ if (deps.isBypassedSession(ctx)) {
99
+ return deps.makeBypassedToolResult("ov_archive_search");
100
+ }
101
+ deps.rememberSessionAgentId(ctx);
102
+ const sessionId = ctx.sessionId ?? "";
103
+ const sessionKey = ctx.sessionKey ?? "";
104
+ if (!sessionId && !sessionKey) {
105
+ return {
106
+ content: [{ type: "text", text: "Error: no active session." }],
107
+ details: { error: "no_session" },
108
+ };
109
+ }
110
+ const ovSessionId = deps.toOvSessionId(ctx.sessionId, ctx.sessionKey);
111
+ const query = String((params as { query?: string }).query ?? "").trim();
112
+ const archiveId = (params as { archiveId?: string }).archiveId;
113
+
114
+ if (!query) {
115
+ return {
116
+ content: [{ type: "text", text: "Error: query is required." }],
117
+ details: { error: "missing_param", param: "query" },
118
+ };
119
+ }
120
+
121
+ const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
122
+ deps.logger?.info?.(`openviking: ov_archive_search query="${query}" escaped="${escapedQuery}" archive=${archiveId ?? "all"} session=${ovSessionId}`);
123
+
124
+ try {
125
+ const client = await deps.getClient();
126
+ const agentId = deps.resolveAgentId(ctx.sessionId, ctx.sessionKey);
127
+ const started = Date.now();
128
+ const result = await client.grepSessionArchives(ovSessionId, escapedQuery, {
129
+ archiveId,
130
+ caseInsensitive: true,
131
+ agentId,
132
+ });
133
+ const traceResults: RecallTraceResult[] = (result.matches ?? []).slice(0, deps.traceRecallMaxResultsPerSearch).map((match) => ({
134
+ uri: match.uri,
135
+ resourceType: "archive",
136
+ abstractPreview: previewText(match.content, deps.traceRecallPreviewChars),
137
+ resultType: "archive_match",
138
+ }));
139
+
140
+ const recordArchiveTrace = async (displayed: OpenVikingArchiveMatch[]) => {
141
+ await deps.traceRecorder?.recordAndFlush({
142
+ schemaVersion: "1.0",
143
+ traceId: deps.createTraceId("ov_archive_search"),
144
+ ts: Date.now(),
145
+ sessionId: ctx.sessionId,
146
+ sessionKey: ctx.sessionKey,
147
+ ovSessionId,
148
+ agentId,
149
+ source: "ov_archive_search",
150
+ operationType: "archive_grep",
151
+ resourceTypes: ["session"],
152
+ trigger: { query, derivedKeywords: [query] },
153
+ searches: [{
154
+ resourceType: "archive",
155
+ targetUriResolved: archiveId ? `viking://session/${ovSessionId}/history/${archiveId}` : `viking://session/${ovSessionId}/history`,
156
+ limit: deps.traceRecallMaxResultsPerSearch,
157
+ durationMs: Date.now() - started,
158
+ total: result.matches?.length ?? result.count ?? 0,
159
+ results: traceResults,
160
+ archiveId,
161
+ caseInsensitive: true,
162
+ }],
163
+ selected: displayed.map((match) => ({
164
+ uri: match.uri,
165
+ resourceType: "archive",
166
+ line: match.line,
167
+ abstractPreview: previewText(match.content, deps.traceRecallPreviewChars),
168
+ displayed: true,
169
+ })),
170
+ stats: {
171
+ candidateCount: result.matches?.length ?? result.count ?? 0,
172
+ selectedCount: displayed.length,
173
+ injectedCount: 0,
174
+ },
175
+ });
176
+ };
177
+
178
+ if (!result.matches || result.matches.length === 0) {
179
+ await recordArchiveTrace([]);
180
+ return {
181
+ content: [{
182
+ type: "text",
183
+ text: `No matches found for "${query}". Try a different keyword — ` +
184
+ "the original conversation may use different wording than the question. " +
185
+ "Try synonyms, related terms, or shorter fragments.",
186
+ }],
187
+ details: { query, matchCount: 0 },
188
+ };
189
+ }
190
+
191
+ const MAX_MATCHES = 12;
192
+ const MAX_LINE_LEN = 1500;
193
+ const shown = result.matches.slice(0, MAX_MATCHES);
194
+ await recordArchiveTrace(shown);
195
+ const blocks = shown.map((m, i) => {
196
+ const archiveTag = m.uri.match(/archive_\d+/)?.[0] ?? "unknown";
197
+ const truncated = m.content.length > MAX_LINE_LEN
198
+ ? m.content.slice(0, MAX_LINE_LEN) + "…(truncated)"
199
+ : m.content;
200
+ return `## Match ${i + 1}: ${archiveTag} (line ${m.line})\n${truncated}`;
201
+ });
202
+
203
+ const header = `Found ${result.matches.length} match(es) for "${query}"` +
204
+ (result.matches.length > MAX_MATCHES ? ` (showing first ${MAX_MATCHES})` : "") + ":";
205
+
206
+ return {
207
+ content: [{ type: "text", text: header + "\n\n" + blocks.join("\n\n") }],
208
+ details: { query, matchCount: result.matches.length },
209
+ };
210
+ } catch (err: unknown) {
211
+ const msg = err instanceof Error ? err.message : String(err);
212
+ deps.logger?.error?.(`openviking: ov_archive_search error: ${msg}`);
213
+ return {
214
+ content: [{ type: "text", text: `Archive search failed: ${msg}` }],
215
+ details: { error: msg },
216
+ };
217
+ }
218
+ },
219
+ }),
220
+ { name: "ov_archive_search" },
221
+ );
222
+
223
+ deps.registerTool((ctx: OpenVikingArchiveToolContext) => ({
224
+ name: "ov_archive_expand",
225
+ label: "Archive Expand (OpenViking)",
226
+ description:
227
+ "Retrieve original messages from a compressed session archive. " +
228
+ "Use when a session summary lacks specific details " +
229
+ "such as exact commands, file paths, code snippets, or config values. " +
230
+ "Check [Archive Index] to find the right archive ID.",
231
+ parameters: Type.Object({
232
+ archiveId: Type.String({
233
+ description:
234
+ 'Archive ID from [Archive Index] (e.g. "archive_002")',
235
+ }),
236
+ }),
237
+ async execute(_toolCallId: string, params: Record<string, unknown>) {
238
+ if (deps.isBypassedSession(ctx)) {
239
+ return deps.makeBypassedToolResult("ov_archive_expand");
240
+ }
241
+ const session = deps.resolvePluginSessionRouting(ctx);
242
+ const archiveId = String((params as { archiveId?: string }).archiveId ?? "").trim();
243
+ const sessionId = session.sessionId ?? "";
244
+ deps.logger?.info?.(`openviking: ov_archive_expand invoked (archiveId=${archiveId || "(empty)"}, sessionId=${sessionId || "(empty)"})`);
245
+
246
+ if (!archiveId) {
247
+ deps.logger?.warn?.("openviking: ov_archive_expand missing archiveId");
248
+ return {
249
+ content: [{ type: "text", text: "Error: archiveId is required." }],
250
+ details: { error: "missing_param", param: "archiveId" },
251
+ };
252
+ }
253
+
254
+ if (!session.ovSessionId) {
255
+ return {
256
+ content: [{ type: "text", text: "Error: no active session." }],
257
+ details: { error: "no_session" },
258
+ };
259
+ }
260
+
261
+ try {
262
+ const client = await deps.getClient();
263
+ const detail = await client.getSessionArchive(
264
+ session.ovSessionId,
265
+ archiveId,
266
+ session.agentId,
267
+ );
268
+
269
+ const header = [
270
+ `## ${detail.archive_id}`,
271
+ detail.abstract ? `**Summary**: ${detail.abstract}` : "",
272
+ `**Messages**: ${detail.messages.length}`,
273
+ "",
274
+ ].filter(Boolean).join("\n");
275
+
276
+ const body = detail.messages
277
+ .map((message) => deps.formatMessage(message))
278
+ .join("\n\n");
279
+
280
+ deps.logger?.info?.(`openviking: ov_archive_expand expanded ${detail.archive_id}, messages=${detail.messages.length}, chars=${body.length}, sessionId=${sessionId}`);
281
+ return {
282
+ content: [{ type: "text", text: `${header}\n${body}` }],
283
+ details: {
284
+ action: "expanded",
285
+ archiveId: detail.archive_id,
286
+ messageCount: detail.messages.length,
287
+ sessionId,
288
+ ovSessionId: session.ovSessionId,
289
+ },
290
+ };
291
+ } catch (err) {
292
+ const msg = err instanceof Error ? err.message : String(err);
293
+ deps.logger?.warn?.(`openviking: ov_archive_expand failed (archiveId=${archiveId}, sessionId=${sessionId}): ${msg}`);
294
+ return {
295
+ content: [{ type: "text", text: `Failed to expand ${archiveId}: ${msg}` }],
296
+ details: { error: msg, archiveId, sessionId, ovSessionId: session.ovSessionId },
297
+ };
298
+ }
299
+ },
300
+ }), { name: "ov_archive_expand" });
301
+ }
@@ -0,0 +1,26 @@
1
+ import {
2
+ compileSessionPatterns,
3
+ shouldBypassSession,
4
+ } from "../text-utils.js";
5
+
6
+ type BypassRuntimeConfig = {
7
+ bypassSessionPatterns: string[];
8
+ };
9
+
10
+ type SessionBypassContext = {
11
+ sessionId?: string;
12
+ sessionKey?: string;
13
+ };
14
+
15
+ export function createOpenVikingBypassRuntime<TConfig extends BypassRuntimeConfig>(options: {
16
+ cfg: TConfig;
17
+ }) {
18
+ const bypassSessionPatterns = compileSessionPatterns(options.cfg.bypassSessionPatterns);
19
+
20
+ const isBypassedSession = (ctx?: SessionBypassContext): boolean =>
21
+ shouldBypassSession(ctx ?? {}, bypassSessionPatterns);
22
+
23
+ return {
24
+ isBypassedSession,
25
+ };
26
+ }
@@ -0,0 +1,74 @@
1
+ import { OpenVikingClient } from "../client.js";
2
+ import type { HttpTransport } from "../adapters/http-transport.js";
3
+
4
+ type Logger = {
5
+ info: (message: string) => void;
6
+ };
7
+
8
+ type ClientRuntimeConfig = {
9
+ baseUrl: string;
10
+ apiKey: string;
11
+ peer_role: "none" | "assistant" | "person";
12
+ peer_prefix: string;
13
+ timeoutMs: number;
14
+ accountId?: string;
15
+ userId?: string;
16
+ logFindRequests: boolean;
17
+ };
18
+
19
+ export function createOpenVikingClientRuntime(options: {
20
+ cfg: ClientRuntimeConfig;
21
+ rawPeerPrefix: unknown;
22
+ logger: Logger;
23
+ transport?: HttpTransport;
24
+ }) {
25
+ const { cfg, logger } = options;
26
+
27
+ if (cfg.logFindRequests) {
28
+ logger.info(
29
+ "openviking: routing debug logging enabled (config logFindRequests, or env OPENVIKING_LOG_ROUTING=1 / OPENVIKING_DEBUG=1)",
30
+ );
31
+ }
32
+
33
+ const verboseRoutingInfo = (message: string) => {
34
+ if (cfg.logFindRequests) {
35
+ logger.info(message);
36
+ }
37
+ };
38
+
39
+ verboseRoutingInfo(
40
+ `openviking: loaded plugin config peer_role="${cfg.peer_role}" peer_prefix="${cfg.peer_prefix}" ` +
41
+ `(raw peer_prefix=${JSON.stringify(options.rawPeerPrefix ?? "(missing)")}; ` +
42
+ `${
43
+ cfg.peer_prefix
44
+ ? 'non-empty → assistant peer_id is <peer_prefix>_<ctx.agentId> when peer_role="assistant", or <peer_prefix>_main when ctx.agentId is unknown'
45
+ : 'empty → assistant peer_id follows OpenClaw ctx.agentId when peer_role="assistant", or "main" when ctx.agentId is unknown'
46
+ })`,
47
+ );
48
+
49
+ const routingDebugLog = cfg.logFindRequests
50
+ ? (msg: string) => {
51
+ logger.info(msg);
52
+ }
53
+ : undefined;
54
+
55
+ const clientPromise = Promise.resolve(
56
+ new OpenVikingClient(
57
+ cfg.baseUrl,
58
+ cfg.apiKey,
59
+ cfg.peer_prefix,
60
+ cfg.timeoutMs,
61
+ cfg.accountId,
62
+ cfg.userId,
63
+ routingDebugLog,
64
+ { transport: options.transport },
65
+ ),
66
+ );
67
+
68
+ const getClient = (): Promise<OpenVikingClient> => clientPromise;
69
+
70
+ return {
71
+ getClient,
72
+ verboseRoutingInfo,
73
+ };
74
+ }
@@ -0,0 +1,187 @@
1
+ export type AddResourceCommandArgs = {
2
+ source?: string;
3
+ to?: string;
4
+ parent?: string;
5
+ reason?: string;
6
+ instruction?: string;
7
+ wait?: boolean;
8
+ timeout?: number;
9
+ };
10
+
11
+ export type AddSkillCommandArgs = {
12
+ source?: string;
13
+ data?: unknown;
14
+ wait?: boolean;
15
+ timeout?: number;
16
+ };
17
+
18
+ export type OVSearchCommandArgs = {
19
+ query: string;
20
+ uri?: string;
21
+ limit?: number;
22
+ };
23
+
24
+ export function tokenizeCommandArgs(args: string): string[] {
25
+ const tokens: string[] = [];
26
+ let current = "";
27
+ let quote: "'" | '"' | null = null;
28
+ let escaping = false;
29
+
30
+ for (let i = 0; i < args.length; i += 1) {
31
+ const ch = args[i]!;
32
+ const next = args[i + 1];
33
+ if (escaping) {
34
+ current += ch;
35
+ escaping = false;
36
+ continue;
37
+ }
38
+ if (ch === "\\") {
39
+ const shouldEscape =
40
+ quote === '"'
41
+ ? next === '"' || next === "\\"
42
+ : !quote && Boolean(next && (/\s/.test(next) || next === '"' || next === "'"));
43
+ if (shouldEscape) {
44
+ escaping = true;
45
+ continue;
46
+ }
47
+ current += ch;
48
+ continue;
49
+ }
50
+ if ((ch === '"' || ch === "'") && (!quote || quote === ch)) {
51
+ quote = quote ? null : ch;
52
+ continue;
53
+ }
54
+ if (!quote && /\s/.test(ch)) {
55
+ if (current) {
56
+ tokens.push(current);
57
+ current = "";
58
+ }
59
+ continue;
60
+ }
61
+ current += ch;
62
+ }
63
+
64
+ if (escaping) {
65
+ current += "\\";
66
+ }
67
+ if (quote) {
68
+ throw new Error("Unterminated quoted argument");
69
+ }
70
+ if (current) {
71
+ tokens.push(current);
72
+ }
73
+ return tokens;
74
+ }
75
+
76
+ type ParsedFlagArgs = {
77
+ positionals: string[];
78
+ flags: Map<string, string | boolean>;
79
+ };
80
+
81
+ function parseFlagArgs(args: string): ParsedFlagArgs {
82
+ const tokens = tokenizeCommandArgs(args);
83
+ const positionals: string[] = [];
84
+ const flags = new Map<string, string | boolean>();
85
+
86
+ for (let i = 0; i < tokens.length; i += 1) {
87
+ const token = tokens[i]!;
88
+ if (!token.startsWith("--")) {
89
+ positionals.push(token);
90
+ continue;
91
+ }
92
+ const raw = token.slice(2);
93
+ if (!raw) {
94
+ continue;
95
+ }
96
+ const eqIndex = raw.indexOf("=");
97
+ if (eqIndex >= 0) {
98
+ flags.set(raw.slice(0, eqIndex), raw.slice(eqIndex + 1));
99
+ continue;
100
+ }
101
+ const next = tokens[i + 1];
102
+ if (next && !next.startsWith("--")) {
103
+ flags.set(raw, next);
104
+ i += 1;
105
+ } else {
106
+ flags.set(raw, true);
107
+ }
108
+ }
109
+
110
+ return { positionals, flags };
111
+ }
112
+
113
+ function getStringFlag(flags: Map<string, string | boolean>, name: string): string | undefined {
114
+ const value = flags.get(name);
115
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
116
+ }
117
+
118
+ function getNumberFlag(flags: Map<string, string | boolean>, name: string): number | undefined {
119
+ const raw = getStringFlag(flags, name);
120
+ if (!raw) {
121
+ return undefined;
122
+ }
123
+ const value = Number(raw);
124
+ if (!Number.isFinite(value)) {
125
+ throw new Error(`--${name} must be a number`);
126
+ }
127
+ return value;
128
+ }
129
+
130
+ function getBoolFlag(flags: Map<string, string | boolean>, name: string): boolean {
131
+ return flags.get(name) === true;
132
+ }
133
+
134
+ export function parseAddResourceCommandArgs(args: string): AddResourceCommandArgs {
135
+ const parsed = parseFlagArgs(args);
136
+ const source =
137
+ parsed.positionals.length <= 1 ? parsed.positionals[0] : parsed.positionals.join(" ").trim();
138
+ if (!source) {
139
+ throw new Error("Usage: /add-resource <source> [--to URI] [--parent URI] [--reason TEXT] [--instruction TEXT] [--wait] [--timeout SEC]");
140
+ }
141
+ const to = getStringFlag(parsed.flags, "to");
142
+ const parent = getStringFlag(parsed.flags, "parent");
143
+ if (to && parent) {
144
+ throw new Error("Cannot specify both --to and --parent.");
145
+ }
146
+ return {
147
+ source,
148
+ to,
149
+ parent,
150
+ reason: getStringFlag(parsed.flags, "reason"),
151
+ instruction: getStringFlag(parsed.flags, "instruction"),
152
+ wait: getBoolFlag(parsed.flags, "wait"),
153
+ timeout: getNumberFlag(parsed.flags, "timeout"),
154
+ };
155
+ }
156
+
157
+ export function parseAddSkillCommandArgs(args: string): AddSkillCommandArgs {
158
+ const parsed = parseFlagArgs(args);
159
+ const source =
160
+ parsed.positionals.length <= 1 ? parsed.positionals[0] : parsed.positionals.join(" ").trim();
161
+ if (!source) {
162
+ throw new Error("Usage: /add-skill <source> [--wait] [--timeout SEC]");
163
+ }
164
+ if (parsed.flags.has("to") || parsed.flags.has("parent") || parsed.flags.has("reason") || parsed.flags.has("instruction")) {
165
+ throw new Error("--to, --parent, --reason, and --instruction are resource-only options.");
166
+ }
167
+ return {
168
+ source,
169
+ wait: getBoolFlag(parsed.flags, "wait"),
170
+ timeout: getNumberFlag(parsed.flags, "timeout"),
171
+ };
172
+ }
173
+
174
+ export function parseOVSearchCommandArgs(args: string): OVSearchCommandArgs {
175
+ const parsed = parseFlagArgs(args);
176
+ // `/ov-search` only accepts a single query string, so positional segments are
177
+ // always re-joined to preserve unquoted multi-word searches.
178
+ const query = parsed.positionals.join(" ").trim();
179
+ if (!query) {
180
+ throw new Error('Usage: /ov-search "<query>" [--uri URI] [--limit N]');
181
+ }
182
+ return {
183
+ query,
184
+ uri: getStringFlag(parsed.flags, "uri"),
185
+ limit: getNumberFlag(parsed.flags, "limit"),
186
+ };
187
+ }