@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,143 @@
1
+ export type SetupNetworkTransport = (url: string, init: RequestInit) => Promise<Response>;
2
+
3
+ export type SetupVersionCompatibility = "compatible" | "server_too_old" | "server_too_new" | "unknown";
4
+
5
+ export type SetupHealthResult = {
6
+ ok: boolean;
7
+ version: string;
8
+ error: string;
9
+ compatibility: SetupVersionCompatibility;
10
+ pluginVersion: string;
11
+ compatRange: string;
12
+ };
13
+
14
+ export type SetupApiKeyProbeResult = {
15
+ keyType: "user_key" | "root_key" | "no_key" | "unknown";
16
+ needsAccountId: boolean;
17
+ needsUserId: boolean;
18
+ detail: string;
19
+ };
20
+
21
+ export type SetupNetworkProbeOptions = {
22
+ pluginVersion: string;
23
+ compatRange: string;
24
+ checkVersionCompatibility: (serverVersion: string) => SetupVersionCompatibility;
25
+ transport?: SetupNetworkTransport;
26
+ timeoutMs?: number;
27
+ };
28
+
29
+ export type SetupNetworkProbes = {
30
+ probeApiKeyType: (baseUrl: string, apiKey?: string) => Promise<SetupApiKeyProbeResult>;
31
+ checkServiceHealth: (baseUrl: string, apiKey?: string) => Promise<SetupHealthResult>;
32
+ };
33
+
34
+ export const defaultSetupNetworkTransport: SetupNetworkTransport = (url, init) => fetch(url, init);
35
+
36
+ export function createSetupNetworkProbes({
37
+ pluginVersion,
38
+ compatRange,
39
+ checkVersionCompatibility,
40
+ transport = defaultSetupNetworkTransport,
41
+ timeoutMs = 10_000,
42
+ }: SetupNetworkProbeOptions): SetupNetworkProbes {
43
+ return {
44
+ async probeApiKeyType(baseUrl: string, apiKey?: string): Promise<SetupApiKeyProbeResult> {
45
+ if (!apiKey) {
46
+ return { keyType: "no_key", needsAccountId: false, needsUserId: false, detail: "No API key configured" };
47
+ }
48
+
49
+ const controller = new AbortController();
50
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
51
+ const sessionsUrl = `${baseUrl.replace(/\/+$/, "")}/api/v1/sessions?limit=1`;
52
+ try {
53
+ const headers: Record<string, string> = { "X-API-Key": apiKey };
54
+ const response = await transport(sessionsUrl, {
55
+ headers,
56
+ signal: controller.signal,
57
+ });
58
+
59
+ if (response.ok) {
60
+ return { keyType: "user_key", needsAccountId: false, needsUserId: false, detail: "API key has full user context" };
61
+ }
62
+
63
+ if ([400, 401, 403, 422].includes(response.status)) {
64
+ let body = "";
65
+ try {
66
+ body = await response.text();
67
+ } catch { /* ignore parse errors */ }
68
+ const lower = body.toLowerCase();
69
+ const needsAccount = /x-openviking-account|account[_ ]?id|account context|tenant/.test(lower);
70
+ const needsUser = /x-openviking-user|user[_ ]?id|user context|user key/.test(lower);
71
+ if (needsAccount || needsUser) {
72
+ return {
73
+ keyType: "root_key",
74
+ needsAccountId: needsAccount,
75
+ needsUserId: needsUser,
76
+ detail: body.slice(0, 200),
77
+ };
78
+ }
79
+
80
+ try {
81
+ const probeHeaders: Record<string, string> = {
82
+ "X-API-Key": apiKey,
83
+ "X-OpenViking-Account": "__probe__",
84
+ "X-OpenViking-User": "__probe__",
85
+ };
86
+ const probe2 = await transport(sessionsUrl, {
87
+ headers: probeHeaders,
88
+ signal: controller.signal,
89
+ });
90
+ if (probe2.status !== response.status) {
91
+ return {
92
+ keyType: "root_key",
93
+ needsAccountId: true,
94
+ needsUserId: true,
95
+ detail: body.slice(0, 200) || `HTTP ${response.status} -> ${probe2.status} after adding tenant headers`,
96
+ };
97
+ }
98
+ } catch { /* ignore probe errors, fall through to unknown */ }
99
+
100
+ if (response.status === 401 || response.status === 403) {
101
+ return { keyType: "unknown", needsAccountId: false, needsUserId: false, detail: `HTTP ${response.status} - authentication failed, verify your API key` };
102
+ }
103
+ return { keyType: "unknown", needsAccountId: false, needsUserId: false, detail: `HTTP ${response.status}${body ? ` - ${body.slice(0, 160)}` : ""}` };
104
+ }
105
+
106
+ return { keyType: "unknown", needsAccountId: false, needsUserId: false, detail: `HTTP ${response.status}` };
107
+ } catch (err) {
108
+ return { keyType: "unknown", needsAccountId: false, needsUserId: false, detail: String(err instanceof Error ? err.message : err) };
109
+ } finally {
110
+ clearTimeout(timeoutId);
111
+ }
112
+ },
113
+
114
+ async checkServiceHealth(baseUrl: string, apiKey?: string): Promise<SetupHealthResult> {
115
+ const controller = new AbortController();
116
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
117
+ try {
118
+ const headers: Record<string, string> = {};
119
+ if (apiKey) headers["X-API-Key"] = apiKey;
120
+ const response = await transport(`${baseUrl.replace(/\/+$/, "")}/health`, {
121
+ headers,
122
+ signal: controller.signal,
123
+ });
124
+ if (response.ok) {
125
+ try {
126
+ const data = await response.json() as Record<string, unknown>;
127
+ const result = (data.result ?? data) as Record<string, unknown>;
128
+ const version = String(result.version ?? data.version ?? "");
129
+ const compatibility = checkVersionCompatibility(version);
130
+ return { ok: true, version, error: "", compatibility, pluginVersion, compatRange };
131
+ } catch {
132
+ return { ok: true, version: "", error: "", compatibility: "unknown", pluginVersion, compatRange };
133
+ }
134
+ }
135
+ return { ok: false, version: "", error: `HTTP ${response.status}`, compatibility: "unknown", pluginVersion, compatRange };
136
+ } catch (err) {
137
+ return { ok: false, version: "", error: String(err instanceof Error ? err.message : err), compatibility: "unknown", pluginVersion, compatRange };
138
+ } finally {
139
+ clearTimeout(timeoutId);
140
+ }
141
+ },
142
+ };
143
+ }
@@ -0,0 +1,306 @@
1
+ import {
2
+ activateContextEngineSlot,
3
+ getExistingPluginConfig,
4
+ isContextEngineSlotActive,
5
+ writeOpenVikingConfig,
6
+ type SetupIO,
7
+ type SlotActivationResult,
8
+ } from "./config-writer.js";
9
+
10
+ export type { SetupIO, SlotActivationResult };
11
+
12
+ export type VersionCompatibility = "compatible" | "server_too_old" | "server_too_new" | "unknown";
13
+
14
+ export type HealthResult = {
15
+ ok: boolean;
16
+ version: string;
17
+ error: string;
18
+ compatibility: VersionCompatibility;
19
+ pluginVersion: string;
20
+ compatRange: string;
21
+ };
22
+
23
+ export type ApiKeyProbeResult = {
24
+ keyType: "user_key" | "root_key" | "no_key" | "unknown";
25
+ needsAccountId: boolean;
26
+ needsUserId: boolean;
27
+ detail: string;
28
+ };
29
+
30
+ export type SetupResult = {
31
+ success: boolean;
32
+ action: "configured" | "existing" | "error" | "slot_blocked";
33
+ config?: {
34
+ mode: string;
35
+ baseUrl: string;
36
+ apiKey?: string;
37
+ peer_role?: "none" | "assistant" | "person";
38
+ peer_prefix?: string;
39
+ accountId?: string;
40
+ userId?: string;
41
+ recallTargetTypes?: string[];
42
+ };
43
+ health?: HealthResult;
44
+ keyProbe?: ApiKeyProbeResult;
45
+ slot: SlotActivationResult;
46
+ error?: string;
47
+ };
48
+
49
+ export type StatusResult = {
50
+ configured: boolean;
51
+ config?: {
52
+ mode: string;
53
+ baseUrl: string;
54
+ hasApiKey: boolean;
55
+ peer_role?: "none" | "assistant" | "person";
56
+ peer_prefix?: string;
57
+ hasAccountId: boolean;
58
+ hasUserId: boolean;
59
+ };
60
+ health?: HealthResult;
61
+ keyProbe?: ApiKeyProbeResult;
62
+ slotActive: boolean;
63
+ };
64
+
65
+ export type SetupParams = {
66
+ baseUrl: string;
67
+ apiKey?: string;
68
+ peerRole?: "none" | "assistant" | "person";
69
+ peerPrefix?: string;
70
+ accountId?: string;
71
+ userId?: string;
72
+ recallTargetTypes?: string[];
73
+ allowOffline?: boolean;
74
+ forceSlot?: boolean;
75
+ };
76
+
77
+ export type InteractiveRemoteConfigParams = {
78
+ existing?: Record<string, unknown> | null;
79
+ baseUrl: string;
80
+ apiKey?: string;
81
+ peerRole?: "none" | "assistant" | "person";
82
+ peerPrefix?: string;
83
+ accountId?: string;
84
+ userId?: string;
85
+ forceSlot?: boolean;
86
+ };
87
+
88
+ export type InteractiveRemoteConfigResult = {
89
+ config: Record<string, unknown>;
90
+ slot: SlotActivationResult;
91
+ };
92
+
93
+ export type OpenVikingSetupService = {
94
+ setupNonInteractive: (configPath: string, params: SetupParams) => Promise<SetupResult>;
95
+ saveInteractiveRemoteConfig: (configPath: string, params: InteractiveRemoteConfigParams) => Promise<InteractiveRemoteConfigResult>;
96
+ useExistingRemoteConfig: (configPath: string, existing: Record<string, unknown>) => Promise<SetupResult>;
97
+ getStatus: (configPath: string) => Promise<StatusResult>;
98
+ };
99
+
100
+ export type OpenVikingSetupServiceDependencies = {
101
+ io: SetupIO;
102
+ defaultRemoteUrl?: string;
103
+ checkServiceHealth: (baseUrl: string, apiKey?: string) => Promise<HealthResult>;
104
+ probeApiKeyType: (baseUrl: string, apiKey?: string) => Promise<ApiKeyProbeResult>;
105
+ };
106
+
107
+ export function maskKey(key: string): string {
108
+ if (key.length <= 8) return "****";
109
+ return `${key.slice(0, 4)}...${key.slice(-4)}`;
110
+ }
111
+
112
+ export function isLegacyLocalMode(existing: Record<string, unknown>): boolean {
113
+ const mode = existing.mode;
114
+ return mode !== "remote";
115
+ }
116
+
117
+ export function buildInteractiveRemotePluginConfig({
118
+ existing,
119
+ baseUrl,
120
+ apiKey,
121
+ peerRole,
122
+ peerPrefix,
123
+ accountId,
124
+ userId,
125
+ }: InteractiveRemoteConfigParams): Record<string, unknown> {
126
+ const pluginCfg: Record<string, unknown> = {
127
+ ...(existing ?? {}),
128
+ mode: "remote",
129
+ baseUrl,
130
+ };
131
+ if (apiKey) pluginCfg.apiKey = apiKey;
132
+ else delete pluginCfg.apiKey;
133
+ if (peerRole) pluginCfg.peer_role = peerRole;
134
+ else delete pluginCfg.peer_role;
135
+ if (peerPrefix) pluginCfg.peer_prefix = peerPrefix;
136
+ else delete pluginCfg.peer_prefix;
137
+ if (accountId) pluginCfg.accountId = accountId;
138
+ else delete pluginCfg.accountId;
139
+ if (userId) pluginCfg.userId = userId;
140
+ else delete pluginCfg.userId;
141
+ delete pluginCfg.configPath;
142
+ delete pluginCfg.port;
143
+ return pluginCfg;
144
+ }
145
+
146
+ export function createOpenVikingSetupService({
147
+ io,
148
+ defaultRemoteUrl = "http://127.0.0.1:1933",
149
+ checkServiceHealth,
150
+ probeApiKeyType,
151
+ }: OpenVikingSetupServiceDependencies): OpenVikingSetupService {
152
+ return {
153
+ async setupNonInteractive(configPath: string, params: SetupParams): Promise<SetupResult> {
154
+ try {
155
+ const { baseUrl, apiKey, peerRole, peerPrefix, accountId, userId, recallTargetTypes, allowOffline, forceSlot } = params;
156
+
157
+ const health = await checkServiceHealth(baseUrl, apiKey);
158
+
159
+ if (!health.ok && !allowOffline) {
160
+ return {
161
+ success: false,
162
+ action: "error",
163
+ config: { mode: "remote", baseUrl },
164
+ health,
165
+ slot: { activated: false, replaced: false },
166
+ error: `Server unreachable: ${health.error}. Use --allow-offline to save config anyway.`,
167
+ };
168
+ }
169
+
170
+ const keyProbe = health.ok ? await probeApiKeyType(baseUrl, apiKey) : undefined;
171
+
172
+ if (keyProbe?.keyType === "root_key" && (!accountId || !userId)) {
173
+ const missing: string[] = [];
174
+ if (!accountId) missing.push("--account-id");
175
+ if (!userId) missing.push("--user-id");
176
+ return {
177
+ success: false,
178
+ action: "error",
179
+ config: {
180
+ mode: "remote",
181
+ baseUrl,
182
+ ...(apiKey ? { apiKey: maskKey(apiKey) } : {}),
183
+ },
184
+ health,
185
+ keyProbe,
186
+ slot: { activated: false, replaced: false },
187
+ error: `Root API key detected. Missing: ${missing.join(", ")}. Re-run with: ${missing.map(f => `${f} <value>`).join(" ")}`,
188
+ };
189
+ }
190
+
191
+ const pluginCfg: Record<string, unknown> = { mode: "remote", baseUrl };
192
+ if (apiKey) pluginCfg.apiKey = apiKey;
193
+ if (peerRole) pluginCfg.peer_role = peerRole;
194
+ if (peerPrefix) pluginCfg.peer_prefix = peerPrefix;
195
+ if (accountId) pluginCfg.accountId = accountId;
196
+ if (userId) pluginCfg.userId = userId;
197
+ if (recallTargetTypes && recallTargetTypes.length > 0) pluginCfg.recallTargetTypes = recallTargetTypes;
198
+
199
+ writeOpenVikingConfig(configPath, pluginCfg, io);
200
+ const slot = activateContextEngineSlot(configPath, !!forceSlot, io);
201
+
202
+ const resultConfig = {
203
+ mode: "remote",
204
+ baseUrl,
205
+ ...(apiKey ? { apiKey: maskKey(apiKey) } : {}),
206
+ ...(peerRole ? { peer_role: peerRole } : {}),
207
+ ...(peerPrefix ? { peer_prefix: peerPrefix } : {}),
208
+ ...(accountId ? { accountId } : {}),
209
+ ...(userId ? { userId } : {}),
210
+ ...(recallTargetTypes && recallTargetTypes.length > 0 ? { recallTargetTypes } : {}),
211
+ };
212
+
213
+ if (!slot.activated && slot.previousOwner) {
214
+ return {
215
+ success: false,
216
+ action: "slot_blocked",
217
+ config: resultConfig,
218
+ health,
219
+ keyProbe,
220
+ slot,
221
+ error: `contextEngine slot is owned by "${slot.previousOwner}". Config was saved but slot was NOT changed. Use --force-slot to replace.`,
222
+ };
223
+ }
224
+
225
+ return {
226
+ success: true,
227
+ action: "configured",
228
+ config: resultConfig,
229
+ health,
230
+ keyProbe,
231
+ slot,
232
+ };
233
+ } catch (err) {
234
+ return {
235
+ success: false,
236
+ action: "error",
237
+ slot: { activated: false, replaced: false },
238
+ error: String(err instanceof Error ? err.message : err),
239
+ };
240
+ }
241
+ },
242
+
243
+ async saveInteractiveRemoteConfig(
244
+ configPath: string,
245
+ params: InteractiveRemoteConfigParams,
246
+ ): Promise<InteractiveRemoteConfigResult> {
247
+ const pluginCfg = buildInteractiveRemotePluginConfig(params);
248
+ writeOpenVikingConfig(configPath, pluginCfg, io);
249
+ const slot = activateContextEngineSlot(configPath, !!params.forceSlot, io);
250
+ return { config: pluginCfg, slot };
251
+ },
252
+
253
+ async useExistingRemoteConfig(configPath: string, existing: Record<string, unknown>): Promise<SetupResult> {
254
+ const baseUrl = String(existing.baseUrl ?? defaultRemoteUrl);
255
+ const apiKey = existing.apiKey ? String(existing.apiKey) : undefined;
256
+ const health = await checkServiceHealth(baseUrl, apiKey);
257
+ const slot = activateContextEngineSlot(configPath, false, io);
258
+ return {
259
+ success: true,
260
+ action: "existing",
261
+ config: {
262
+ mode: String(existing.mode ?? "remote"),
263
+ baseUrl,
264
+ ...(apiKey ? { apiKey: maskKey(apiKey) } : {}),
265
+ ...(existing.peer_role ? { peer_role: String(existing.peer_role) as "none" | "assistant" | "person" } : {}),
266
+ ...(existing.peer_prefix ? { peer_prefix: String(existing.peer_prefix) } : {}),
267
+ ...(existing.accountId ? { accountId: String(existing.accountId) } : {}),
268
+ ...(existing.userId ? { userId: String(existing.userId) } : {}),
269
+ },
270
+ health,
271
+ slot,
272
+ };
273
+ },
274
+
275
+ async getStatus(configPath: string): Promise<StatusResult> {
276
+ const config = io.readConfig(configPath);
277
+ const existing = getExistingPluginConfig(config);
278
+ const slotActive = isContextEngineSlotActive(configPath, io);
279
+
280
+ if (!existing) {
281
+ return { configured: false, slotActive };
282
+ }
283
+
284
+ const baseUrl = String(existing.baseUrl ?? defaultRemoteUrl);
285
+ const apiKey = existing.apiKey ? String(existing.apiKey) : undefined;
286
+ const health = await checkServiceHealth(baseUrl, apiKey);
287
+ const keyProbe = health.ok ? await probeApiKeyType(baseUrl, apiKey) : undefined;
288
+
289
+ return {
290
+ configured: true,
291
+ config: {
292
+ mode: String(existing.mode ?? "remote"),
293
+ baseUrl,
294
+ hasApiKey: !!existing.apiKey,
295
+ ...(existing.peer_role ? { peer_role: String(existing.peer_role) as "none" | "assistant" | "person" } : {}),
296
+ ...(existing.peer_prefix ? { peer_prefix: String(existing.peer_prefix) } : {}),
297
+ hasAccountId: !!existing.accountId,
298
+ hasUserId: !!existing.userId,
299
+ },
300
+ health,
301
+ keyProbe,
302
+ slotActive,
303
+ };
304
+ },
305
+ };
306
+ }
@@ -0,0 +1,41 @@
1
+ export type VersionCompatibility = "compatible" | "server_too_old" | "server_too_new" | "unknown";
2
+
3
+ export type VersionCompatibilityRange = {
4
+ min?: string;
5
+ max?: string;
6
+ };
7
+
8
+ export function parseVersionTuple(v: string): number[] | null {
9
+ const cleaned = v.replace(/^v/i, "").split("-")[0];
10
+ const parts = cleaned.split(".").map(Number);
11
+ if (parts.some(isNaN)) return null;
12
+ return parts;
13
+ }
14
+
15
+ export function compareVersions(a: number[], b: number[]): number {
16
+ const len = Math.max(a.length, b.length);
17
+ for (let i = 0; i < len; i++) {
18
+ const diff = (a[i] ?? 0) - (b[i] ?? 0);
19
+ if (diff !== 0) return diff;
20
+ }
21
+ return 0;
22
+ }
23
+
24
+ export function checkVersionCompatibility(
25
+ serverVersion: string,
26
+ { min, max }: VersionCompatibilityRange,
27
+ ): VersionCompatibility {
28
+ if (!serverVersion) return "unknown";
29
+ const sv = parseVersionTuple(serverVersion);
30
+ if (!sv) return "unknown";
31
+
32
+ if (min) {
33
+ const minV = parseVersionTuple(min);
34
+ if (minV && compareVersions(sv, minV) < 0) return "server_too_old";
35
+ }
36
+ if (max) {
37
+ const maxV = parseVersionTuple(max);
38
+ if (maxV && compareVersions(sv, maxV) > 0) return "server_too_new";
39
+ }
40
+ return "compatible";
41
+ }
@@ -191,8 +191,6 @@ function normalizeToolResultName(
191
191
  return message;
192
192
  }
193
193
 
194
- export { makeMissingToolResult };
195
-
196
194
  export type ToolCallInputRepairReport = {
197
195
  messages: AgentMessage[];
198
196
  droppedToolCalls: number;