@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,375 @@
1
+ import type { OVMessage } from "../client.js";
2
+ import { sanitizeToolUseResultPairing } from "../session-transcript-repair.js";
3
+
4
+ export type AgentMessage = {
5
+ role?: string;
6
+ content?: unknown;
7
+ timestamp?: unknown;
8
+ };
9
+
10
+ export function toRoleId(senderId: string | undefined): string | undefined {
11
+ if (!senderId) {
12
+ return undefined;
13
+ }
14
+ const normalized = senderId
15
+ .trim()
16
+ .replace(/[^a-zA-Z0-9_-]+/g, "_")
17
+ .replace(/^_+|_+$/g, "")
18
+ .replace(/_+/g, "_");
19
+ return normalized || undefined;
20
+ }
21
+
22
+ /**
23
+ * Convert an OpenViking stored message (parts-based format) into one or more
24
+ * OpenClaw AgentMessages (content-blocks format).
25
+ *
26
+ * For assistant messages with ToolParts, this produces:
27
+ * 1. The assistant message with canonical toolCall blocks in its content array
28
+ * 2. A separate toolResult message per ToolPart (carrying tool_output)
29
+ */
30
+ export function convertToAgentMessages(msg: { role: string; parts: unknown[] }): AgentMessage[] {
31
+ const parts = msg.parts ?? [];
32
+ const contentBlocks: Record<string, unknown>[] = [];
33
+ const toolCallBlocks: Record<string, unknown>[] = [];
34
+ const toolResults: AgentMessage[] = [];
35
+
36
+ for (const part of parts) {
37
+ if (!part || typeof part !== "object") continue;
38
+ const p = part as Record<string, unknown>;
39
+
40
+ if (p.type === "text" && typeof p.text === "string") {
41
+ contentBlocks.push({ type: "text", text: p.text });
42
+ } else if (p.type === "context") {
43
+ if (typeof p.abstract === "string" && p.abstract) {
44
+ contentBlocks.push({ type: "text", text: p.abstract });
45
+ }
46
+ } else if (p.type === "tool") {
47
+ const toolId = typeof p.tool_id === "string" ? p.tool_id : "";
48
+ const toolName = typeof p.tool_name === "string" ? p.tool_name : undefined;
49
+ const status = typeof p.tool_status === "string" ? p.tool_status : "unknown";
50
+ const output = typeof p.tool_output === "string" ? p.tool_output : "";
51
+ const ref = typeof p.tool_output_ref === "string" ? p.tool_output_ref : "";
52
+ const originalChars = typeof p.tool_output_original_chars === "number"
53
+ ? p.tool_output_original_chars
54
+ : undefined;
55
+
56
+ if (toolId) {
57
+ toolCallBlocks.push({
58
+ type: "toolCall",
59
+ id: toolId,
60
+ name: toolName ?? "unknown",
61
+ arguments: p.tool_input ?? {},
62
+ });
63
+
64
+ let resultText = (status === "completed" || status === "error")
65
+ ? (output || "(no output)")
66
+ : "(interrupted — tool did not complete)";
67
+ if (ref && !resultText.includes(ref)) {
68
+ const suffix = originalChars !== undefined
69
+ ? `\n[tool-result-ref] ${ref} original_chars=${originalChars}`
70
+ : `\n[tool-result-ref] ${ref}`;
71
+ resultText += suffix;
72
+ }
73
+ const resultPayload: Record<string, unknown> = {
74
+ role: "toolResult",
75
+ toolCallId: toolId,
76
+ content: [{ type: "text", text: resultText }],
77
+ isError: status === "error",
78
+ };
79
+ if (toolName) {
80
+ resultPayload.toolName = toolName;
81
+ }
82
+ toolResults.push(resultPayload as unknown as AgentMessage);
83
+ } else {
84
+ const fallbackName = toolName ?? "unknown";
85
+ const segments = [`[${fallbackName}] (${status})`];
86
+ if (p.tool_input) {
87
+ try {
88
+ segments.push(`Input: ${JSON.stringify(p.tool_input)}`);
89
+ } catch {
90
+ // non-serializable input, skip
91
+ }
92
+ }
93
+ if (output) {
94
+ segments.push(`Output: ${output}`);
95
+ }
96
+ contentBlocks.push({ type: "text", text: segments.join("\n") });
97
+ }
98
+ }
99
+ }
100
+
101
+ const result: AgentMessage[] = [];
102
+
103
+ if (msg.role === "assistant") {
104
+ result.push({ role: "assistant", content: [...contentBlocks, ...toolCallBlocks] });
105
+ result.push(...toolResults);
106
+ } else {
107
+ const texts = contentBlocks
108
+ .filter((b) => b.type === "text")
109
+ .map((b) => b.text as string);
110
+ if (texts.length > 0) {
111
+ result.push({ role: msg.role, content: texts.join("\n") });
112
+ } else if (toolCallBlocks.length === 0) {
113
+ result.push({ role: msg.role, content: "" });
114
+ }
115
+ if (toolCallBlocks.length > 0) {
116
+ result.push({ role: "assistant", content: toolCallBlocks });
117
+ result.push(...toolResults);
118
+ }
119
+ }
120
+
121
+ return result;
122
+ }
123
+
124
+ export function formatMessageFaithful(msg: OVMessage): string {
125
+ const roleTag = `[${msg.role}]`;
126
+ if (!msg.parts || msg.parts.length === 0) {
127
+ return `${roleTag}: (empty)`;
128
+ }
129
+
130
+ const sections: string[] = [];
131
+ for (const part of msg.parts) {
132
+ if (!part || typeof part !== "object") continue;
133
+ switch (part.type) {
134
+ case "text":
135
+ if (part.text) sections.push(part.text);
136
+ break;
137
+ case "tool": {
138
+ const status = part.tool_status ?? "unknown";
139
+ const header = `[Tool: ${part.tool_name ?? "unknown"}] (${status})`;
140
+ const inputStr = part.tool_input
141
+ ? `Input: ${JSON.stringify(part.tool_input, null, 2)}`
142
+ : "";
143
+ const outputStr = part.tool_output ? `Output:\n${part.tool_output}` : "";
144
+ sections.push([header, inputStr, outputStr].filter(Boolean).join("\n"));
145
+ break;
146
+ }
147
+ case "context":
148
+ sections.push(
149
+ `[Context: ${part.uri ?? "?"}]${part.abstract ? ` ${part.abstract}` : ""}`,
150
+ );
151
+ break;
152
+ default:
153
+ sections.push(`[${part.type}]: ${JSON.stringify(part)}`);
154
+ }
155
+ }
156
+
157
+ return `${roleTag}:\n${sections.join("\n\n")}`;
158
+ }
159
+
160
+ /** Merge consecutive assistant messages by concatenating their content arrays. */
161
+ export function mergeConsecutiveAssistants(messages: AgentMessage[]): AgentMessage[] {
162
+ const result: AgentMessage[] = [];
163
+ for (const msg of messages) {
164
+ const prev = result[result.length - 1];
165
+ if (msg.role === "assistant" && prev?.role === "assistant") {
166
+ const prevContent = Array.isArray(prev.content) ? prev.content : [{ type: "text", text: prev.content }];
167
+ const currContent = Array.isArray(msg.content) ? msg.content : [{ type: "text", text: msg.content }];
168
+ prev.content = [...prevContent, ...currContent] as typeof prev.content;
169
+ } else {
170
+ result.push({ ...msg });
171
+ }
172
+ }
173
+ return result;
174
+ }
175
+
176
+ /**
177
+ * Hoist tool_result blocks to the front of a content array.
178
+ *
179
+ * The Anthropic / Bedrock / Gemini APIs require tool_result blocks to appear
180
+ * at the START of a user message's content array (a tool_result must follow
181
+ * the assistant tool_use that produced it). When mergeConsecutiveUsers
182
+ * merges two user messages, the previous content's text blocks may end up
183
+ * before tool_results from the second message — this function fixes the order.
184
+ *
185
+ * Same pattern as Claude Code's hoistToolResults in src/utils/messages.ts.
186
+ */
187
+ function hoistToolResults<T>(content: T[]): T[] {
188
+ const toolResults: T[] = [];
189
+ const others: T[] = [];
190
+ for (const block of content) {
191
+ if (
192
+ block &&
193
+ typeof block === "object" &&
194
+ "type" in block &&
195
+ (block as { type?: string }).type === "tool_result"
196
+ ) {
197
+ toolResults.push(block);
198
+ } else {
199
+ others.push(block);
200
+ }
201
+ }
202
+ return [...toolResults, ...others];
203
+ }
204
+
205
+ /**
206
+ * Merge consecutive user messages by concatenating their content arrays.
207
+ *
208
+ * Mirror of mergeConsecutiveAssistants. Required because Gemini and Anthropic
209
+ * APIs reject consecutive same-role messages with stopReason=stop payloads=0
210
+ * (empty response). Three independent sources can inject role: "user":
211
+ *
212
+ * 1. Archive commit: "[Session History Summary]" via buildArchiveMemory
213
+ * 2. OpenClaw yield events: "[sessions_yield interrupt]" / "Turn yielded. ..."
214
+ * 3. Audio/Telegram metadata: "[Audio] User text: [Telegram <name>...]"
215
+ *
216
+ * Without merging, these can stack into 2-5 consecutive user turns. The
217
+ * 1P Anthropic API would merge server-side, but Bedrock/Gemini won't —
218
+ * we merge client-side for wire-format consistency.
219
+ *
220
+ * Note: this MUST run AFTER sanitizeToolUseResultPairing because that pass
221
+ * may strip orphaned tool_use / tool_result blocks and thereby create new
222
+ * user-user adjacencies that didn't exist in the input.
223
+ *
224
+ * Tracks issue #1724.
225
+ */
226
+ export function mergeConsecutiveUsers(messages: AgentMessage[]): AgentMessage[] {
227
+ const result: AgentMessage[] = [];
228
+ for (const msg of messages) {
229
+ const prev = result[result.length - 1];
230
+ if (msg.role === "user" && prev?.role === "user") {
231
+ const prevContent = Array.isArray(prev.content) ? prev.content : [{ type: "text", text: prev.content }];
232
+ const currContent = Array.isArray(msg.content) ? msg.content : [{ type: "text", text: msg.content }];
233
+ prev.content = hoistToolResults([...prevContent, ...currContent]) as typeof prev.content;
234
+ } else {
235
+ result.push({ ...msg });
236
+ }
237
+ }
238
+ return result;
239
+ }
240
+
241
+ /**
242
+ * Defensive role-alternation invariant check.
243
+ *
244
+ * After mergeConsecutiveUsers + mergeConsecutiveAssistants, the message stream
245
+ * should already alternate user/assistant. But sanitizeToolUseResultPairing
246
+ * can in rare cases strip a user_with_tool_result message that was the only
247
+ * thing separating two assistant messages, leaving an assistant-assistant
248
+ * adjacency that upstream merge passes can't fix.
249
+ *
250
+ * When detected, we insert a placeholder user message — matching Claude Code's
251
+ * NO_CONTENT_MESSAGE pattern (see CC src/utils/messages.ts:5375-5388) — to
252
+ * preserve the alternation contract that Gemini / Anthropic require.
253
+ */
254
+ export function ensureAlternation(messages: AgentMessage[]): AgentMessage[] {
255
+ const result: AgentMessage[] = [];
256
+ for (const msg of messages) {
257
+ const prev = result[result.length - 1];
258
+ if (prev && prev.role === "assistant" && msg.role === "assistant") {
259
+ result.push({
260
+ role: "user",
261
+ content: "(no content)",
262
+ });
263
+ }
264
+ result.push(msg);
265
+ }
266
+ return result;
267
+ }
268
+
269
+ function normalizeAssistantContent(messages: AgentMessage[]): void {
270
+ for (let i = 0; i < messages.length; i++) {
271
+ const msg = messages[i];
272
+ if (msg?.role === "assistant" && typeof msg.content === "string") {
273
+ messages[i] = {
274
+ ...msg,
275
+ content: [{ type: "text", text: msg.content }],
276
+ };
277
+ }
278
+ }
279
+ }
280
+
281
+ function canonicalizeAssistantBlock(block: unknown): unknown {
282
+ if (!block || typeof block !== "object") {
283
+ return block;
284
+ }
285
+
286
+ const rec = block as Record<string, unknown>;
287
+ const type = typeof rec.type === "string" ? rec.type : "";
288
+ if (type === "toolCall") {
289
+ if (rec.arguments !== undefined) {
290
+ return rec;
291
+ }
292
+ return {
293
+ ...rec,
294
+ arguments: rec.input ?? rec.toolInput ?? {},
295
+ };
296
+ }
297
+
298
+ if (type === "toolUse" || type === "functionCall" || type === "tool_call") {
299
+ return {
300
+ type: "toolCall",
301
+ id: rec.id ?? rec.toolCallId ?? rec.toolUseId,
302
+ name: rec.name,
303
+ arguments: rec.arguments ?? rec.input ?? rec.toolInput ?? {},
304
+ };
305
+ }
306
+
307
+ return rec;
308
+ }
309
+
310
+ function canonicalizeAgentMessages(messages: AgentMessage[]): AgentMessage[] {
311
+ let changed = false;
312
+ const next = messages.map((msg) => {
313
+ if (!msg || typeof msg !== "object") {
314
+ return msg;
315
+ }
316
+
317
+ if (msg.role === "assistant") {
318
+ const content = Array.isArray(msg.content)
319
+ ? msg.content.map((block) => canonicalizeAssistantBlock(block))
320
+ : typeof msg.content === "string"
321
+ ? [{ type: "text", text: msg.content }]
322
+ : msg.content;
323
+
324
+ if (content !== msg.content) {
325
+ changed = true;
326
+ return { ...msg, content };
327
+ }
328
+ return msg;
329
+ }
330
+
331
+ if (msg.role === "toolResult") {
332
+ const raw = msg as Record<string, unknown>;
333
+ const toolCallId =
334
+ (typeof raw.toolCallId === "string" && raw.toolCallId) ||
335
+ (typeof raw.toolUseId === "string" && raw.toolUseId) ||
336
+ undefined;
337
+ const toolName =
338
+ typeof raw.toolName === "string" && raw.toolName.trim()
339
+ ? raw.toolName.trim()
340
+ : undefined;
341
+
342
+ const nextMsg = {
343
+ ...msg,
344
+ ...(toolCallId ? { toolCallId } : {}),
345
+ ...(toolName ? { toolName } : {}),
346
+ } as AgentMessage;
347
+
348
+ if (nextMsg !== msg) {
349
+ changed = true;
350
+ }
351
+ return nextMsg;
352
+ }
353
+
354
+ return msg;
355
+ });
356
+
357
+ return changed ? next : messages;
358
+ }
359
+
360
+ export function sanitizeAgentMessagesForProvider(messages: AgentMessage[]): AgentMessage[] {
361
+ normalizeAssistantContent(messages);
362
+ const canonical = canonicalizeAgentMessages(messages);
363
+ // Defense in depth (issue #1724):
364
+ // 1) sanitizeToolUseResultPairing may strip orphaned tool_use/tool_result,
365
+ // potentially creating new user-user or assistant-assistant adjacencies.
366
+ // 2) mergeConsecutiveUsers fixes user-user (mirror of mergeConsecutiveAssistants
367
+ // already running inside buildSessionContext).
368
+ // 3) ensureAlternation is a final invariant check for the rare
369
+ // assistant-assistant case that the merges can't reach.
370
+ return ensureAlternation(
371
+ mergeConsecutiveUsers(
372
+ sanitizeToolUseResultPairing(canonical as never[]) as AgentMessage[],
373
+ ),
374
+ );
375
+ }
@@ -0,0 +1,125 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+
4
+ export type SlotActivationResult = {
5
+ activated: boolean;
6
+ previousOwner?: string;
7
+ replaced: boolean;
8
+ };
9
+
10
+ export type SetupIO = {
11
+ readConfig: (configPath: string) => Record<string, unknown>;
12
+ writeConfig: (configPath: string, config: Record<string, unknown>) => void;
13
+ backupConfig: (configPath: string) => string | null;
14
+ };
15
+
16
+ export const defaultSetupIO: SetupIO = {
17
+ readConfig: readOpenClawConfig,
18
+ writeConfig: writeOpenClawConfigFile,
19
+ backupConfig,
20
+ };
21
+
22
+ export function readOpenClawConfig(configPath: string): Record<string, unknown> {
23
+ if (!fs.existsSync(configPath)) return {};
24
+ try {
25
+ return JSON.parse(fs.readFileSync(configPath, "utf-8"));
26
+ } catch {
27
+ return {};
28
+ }
29
+ }
30
+
31
+ function writeOpenClawConfigFile(configPath: string, config: Record<string, unknown>): void {
32
+ const configDir = path.dirname(configPath);
33
+ if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true });
34
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
35
+ }
36
+
37
+ export function backupConfig(configPath: string): string | null {
38
+ if (!fs.existsSync(configPath)) return null;
39
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
40
+ const backupPath = `${configPath}.bak.${timestamp}`;
41
+ try {
42
+ fs.copyFileSync(configPath, backupPath);
43
+ return backupPath;
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+
49
+ export function getExistingPluginConfig(config: Record<string, unknown>): Record<string, unknown> | null {
50
+ const plugins = config.plugins as Record<string, unknown> | undefined;
51
+ if (!plugins) return null;
52
+ const entries = plugins.entries as Record<string, unknown> | undefined;
53
+ if (!entries) return null;
54
+ const entry = entries.openviking as Record<string, unknown> | undefined;
55
+ if (!entry) return null;
56
+ const cfg = entry.config as Record<string, unknown> | undefined;
57
+ return cfg && cfg.mode ? cfg : null;
58
+ }
59
+
60
+ export function ensureInstallRecord(plugins: Record<string, unknown>): void {
61
+ const installs = plugins.installs as Record<string, unknown> | undefined;
62
+ if (installs && typeof installs === "object") {
63
+ delete installs.openviking;
64
+ }
65
+
66
+ if (!plugins.allow) plugins.allow = [];
67
+ const allow = plugins.allow as string[];
68
+ if (!allow.includes("openviking")) {
69
+ allow.push("openviking");
70
+ }
71
+ }
72
+
73
+ export function writeOpenVikingConfig(
74
+ configPath: string,
75
+ pluginCfg: Record<string, unknown>,
76
+ io: SetupIO = defaultSetupIO,
77
+ ): void {
78
+ io.backupConfig(configPath);
79
+
80
+ const config = io.readConfig(configPath);
81
+
82
+ if (!config.plugins) config.plugins = {};
83
+ const plugins = config.plugins as Record<string, unknown>;
84
+ if (!plugins.entries) plugins.entries = {};
85
+ const entries = plugins.entries as Record<string, unknown>;
86
+
87
+ const existingEntry = (entries.openviking as Record<string, unknown>) ?? {};
88
+ entries.openviking = { ...existingEntry, config: pluginCfg };
89
+
90
+ ensureInstallRecord(plugins);
91
+
92
+ io.writeConfig(configPath, config);
93
+ }
94
+
95
+ export function activateContextEngineSlot(
96
+ configPath: string,
97
+ force: boolean,
98
+ io: SetupIO = defaultSetupIO,
99
+ ): SlotActivationResult {
100
+ const config = io.readConfig(configPath);
101
+ if (!config.plugins) config.plugins = {};
102
+ const plugins = config.plugins as Record<string, unknown>;
103
+ if (!plugins.slots) plugins.slots = {};
104
+ const slots = plugins.slots as Record<string, unknown>;
105
+
106
+ const current = slots.contextEngine as string | undefined;
107
+
108
+ if (current === "openviking") return { activated: false, replaced: false };
109
+
110
+ if (current && current !== "openviking" && !force) {
111
+ return { activated: false, previousOwner: current, replaced: false };
112
+ }
113
+
114
+ slots.contextEngine = "openviking";
115
+ io.writeConfig(configPath, config);
116
+ return { activated: true, previousOwner: current || undefined, replaced: !!current };
117
+ }
118
+
119
+ export function isContextEngineSlotActive(configPath: string, io: SetupIO = defaultSetupIO): boolean {
120
+ const config = io.readConfig(configPath);
121
+ const plugins = config.plugins as Record<string, unknown> | undefined;
122
+ if (!plugins) return false;
123
+ const slots = plugins.slots as Record<string, unknown> | undefined;
124
+ return slots?.contextEngine === "openviking";
125
+ }
@@ -0,0 +1,5 @@
1
+ export function setExitCodeOnFailure(result: { success: boolean }): void {
2
+ if (!result.success) {
3
+ process.exitCode = 1;
4
+ }
5
+ }
@@ -0,0 +1,56 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ export type SetupCompatibilityRange = {
6
+ min: string;
7
+ max: string;
8
+ };
9
+
10
+ export function findPluginPackageRoot(fromDir = path.dirname(fileURLToPath(import.meta.url))): string | null {
11
+ let current = path.resolve(fromDir);
12
+ for (let depth = 0; depth < 5; depth += 1) {
13
+ if (
14
+ fs.existsSync(path.join(current, "package.json")) &&
15
+ fs.existsSync(path.join(current, "openclaw.plugin.json"))
16
+ ) {
17
+ return current;
18
+ }
19
+
20
+ const parent = path.dirname(current);
21
+ if (parent === current) break;
22
+ current = parent;
23
+ }
24
+ return null;
25
+ }
26
+
27
+ export function readPluginVersion(): string {
28
+ try {
29
+ const packageRoot = findPluginPackageRoot();
30
+ if (!packageRoot) return "unknown";
31
+ const pkgPath = path.join(packageRoot, "package.json");
32
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
33
+ return String(pkg.version ?? "unknown");
34
+ } catch {
35
+ return "unknown";
36
+ }
37
+ }
38
+
39
+ export function readCompatRangeFromManifest(): SetupCompatibilityRange {
40
+ try {
41
+ const packageRoot = findPluginPackageRoot();
42
+ if (!packageRoot) return { min: "", max: "" };
43
+ const manifestPath = path.join(packageRoot, "install-manifest.json");
44
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
45
+ const compat = manifest?.compatibility ?? {};
46
+ return {
47
+ min: String(compat.minOpenvikingVersion ?? ""),
48
+ max: String(compat.maxOpenvikingVersion ?? ""),
49
+ };
50
+ } catch {
51
+ return { min: "", max: "" };
52
+ }
53
+ }
54
+
55
+ export const PLUGIN_VERSION = readPluginVersion();
56
+ export const { min: COMPATIBLE_SERVER_MIN, max: COMPATIBLE_SERVER_MAX } = readCompatRangeFromManifest();