@klhapp/skillmux 0.6.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -0
- package/README.md +9 -3
- package/docs/configuration.md +13 -10
- package/package.json +1 -1
- package/src/adapters.ts +11 -5
- package/src/calibrate.ts +7 -59
- package/src/cli.ts +521 -869
- package/src/commands/config.ts +202 -0
- package/src/commands/core.ts +52 -0
- package/src/commands/project.ts +412 -0
- package/src/commands/shared.ts +45 -0
- package/src/commands/target.ts +110 -0
- package/src/completions.ts +69 -55
- package/src/config-mutation.ts +65 -0
- package/src/config-watcher.ts +63 -2
- package/src/doctor.ts +19 -1
- package/src/output.ts +21 -23
- package/src/server.ts +228 -63
package/src/server.ts
CHANGED
|
@@ -4,8 +4,15 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
4
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { createClients } from "./clients";
|
|
7
|
-
import { loadConfig } from "./config";
|
|
8
|
-
import {
|
|
7
|
+
import { loadConfig, resolveConfigPath } from "./config";
|
|
8
|
+
import { ConfigWatcher, type ReloadStatus } from "./config-watcher";
|
|
9
|
+
import { RuntimeSnapshotManager } from "./snapshot";
|
|
10
|
+
import {
|
|
11
|
+
backfillEmbeddings,
|
|
12
|
+
configure,
|
|
13
|
+
fetchSkill,
|
|
14
|
+
resolveSkill,
|
|
15
|
+
} from "./router-core";
|
|
9
16
|
import { closeRuntime, getRuntime, startVaultWatcher } from "./router-core";
|
|
10
17
|
import { getStats, SINCE_PATTERN } from "./stats";
|
|
11
18
|
import { SKILL_ID_PATTERN } from "./vault";
|
|
@@ -21,13 +28,18 @@ import {
|
|
|
21
28
|
RELOADABLE_KEYS,
|
|
22
29
|
RESTART_REQUIRED_KEYS,
|
|
23
30
|
} from "./config-service";
|
|
24
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
applyCalibrationRun,
|
|
33
|
+
getCalibrationRun,
|
|
34
|
+
listCalibrationRuns,
|
|
35
|
+
} from "./calibrate";
|
|
25
36
|
|
|
26
37
|
export const metricsRegistry = new MetricsRegistry();
|
|
27
38
|
export const readinessState = new ReadinessState();
|
|
28
39
|
|
|
29
40
|
export interface ServerHandle {
|
|
30
41
|
port?: number;
|
|
42
|
+
reloadStatus(): ReloadStatus;
|
|
31
43
|
stop(): Promise<void>;
|
|
32
44
|
}
|
|
33
45
|
|
|
@@ -35,10 +47,15 @@ let warnedAuthToken = false;
|
|
|
35
47
|
function resolveAuthToken(envName: string): string {
|
|
36
48
|
const value = process.env[envName];
|
|
37
49
|
if (value) return value;
|
|
38
|
-
if (
|
|
50
|
+
if (
|
|
51
|
+
envName === "SKILLMUX_AUTH_TOKEN" &&
|
|
52
|
+
process.env.SKILL_ROUTER_AUTH_TOKEN
|
|
53
|
+
) {
|
|
39
54
|
if (!warnedAuthToken) {
|
|
40
55
|
warnedAuthToken = true;
|
|
41
|
-
console.error(
|
|
56
|
+
console.error(
|
|
57
|
+
"skillmux: SKILL_ROUTER_AUTH_TOKEN is deprecated, set SKILLMUX_AUTH_TOKEN instead",
|
|
58
|
+
);
|
|
42
59
|
}
|
|
43
60
|
return process.env.SKILL_ROUTER_AUTH_TOKEN;
|
|
44
61
|
}
|
|
@@ -57,9 +74,36 @@ export async function startServer(opts?: {
|
|
|
57
74
|
port?: number;
|
|
58
75
|
config?: Config;
|
|
59
76
|
clients?: Partial<Clients>;
|
|
77
|
+
configPath?: string;
|
|
60
78
|
}): Promise<ServerHandle> {
|
|
61
|
-
const
|
|
62
|
-
|
|
79
|
+
const configPath = resolveConfigPath(opts?.configPath);
|
|
80
|
+
const config = opts?.config ?? (await loadConfig(configPath));
|
|
81
|
+
const initialClients = { ...createClients(config), ...opts?.clients };
|
|
82
|
+
const snapshots = RuntimeSnapshotManager.create(config, initialClients);
|
|
83
|
+
const inactiveReloadStatus: ReloadStatus = {
|
|
84
|
+
last_successful_reload_at: null,
|
|
85
|
+
last_reload_error: null,
|
|
86
|
+
restart_required_keys: [],
|
|
87
|
+
};
|
|
88
|
+
configure({ config, clients: initialClients });
|
|
89
|
+
// An injected config has no guaranteed file source. Watch it only when the
|
|
90
|
+
// caller explicitly supplies that source; normal server startup always watches.
|
|
91
|
+
const watcherPath =
|
|
92
|
+
opts?.configPath ?? (opts?.config ? undefined : configPath);
|
|
93
|
+
const configWatcher = watcherPath
|
|
94
|
+
? await ConfigWatcher.start(watcherPath, {
|
|
95
|
+
onReload: (nextConfig) => {
|
|
96
|
+
const nextClients = {
|
|
97
|
+
...createClients(nextConfig),
|
|
98
|
+
...opts?.clients,
|
|
99
|
+
};
|
|
100
|
+
snapshots.replace(nextConfig, nextClients);
|
|
101
|
+
configure({ config: nextConfig, clients: nextClients });
|
|
102
|
+
},
|
|
103
|
+
onError: (error) =>
|
|
104
|
+
console.error("skillmux config reload error:", error),
|
|
105
|
+
})
|
|
106
|
+
: undefined;
|
|
63
107
|
const stopWatcher = await startVaultWatcher();
|
|
64
108
|
const initPromise = initializeRuntime(readinessState)
|
|
65
109
|
.then(() => metricsRegistry.setReadiness(readinessState.get()))
|
|
@@ -88,7 +132,10 @@ export async function startServer(opts?: {
|
|
|
88
132
|
|
|
89
133
|
if (result.outcome === "matched") {
|
|
90
134
|
const { body, ...meta } = result;
|
|
91
|
-
return {
|
|
135
|
+
return {
|
|
136
|
+
content: [{ type: "text" as const, text: body }],
|
|
137
|
+
structuredContent: { ...meta },
|
|
138
|
+
};
|
|
92
139
|
}
|
|
93
140
|
return {
|
|
94
141
|
content: [{ type: "text" as const, text: JSON.stringify(result) }],
|
|
@@ -113,7 +160,10 @@ export async function startServer(opts?: {
|
|
|
113
160
|
try {
|
|
114
161
|
const result = await fetchSkill({ skill_id });
|
|
115
162
|
const { body, ...meta } = result;
|
|
116
|
-
return {
|
|
163
|
+
return {
|
|
164
|
+
content: [{ type: "text" as const, text: body }],
|
|
165
|
+
structuredContent: { ...meta },
|
|
166
|
+
};
|
|
117
167
|
} catch (err) {
|
|
118
168
|
metricsRegistry.recordError();
|
|
119
169
|
throw err;
|
|
@@ -123,9 +173,8 @@ export async function startServer(opts?: {
|
|
|
123
173
|
|
|
124
174
|
const transportType = opts?.transport ?? "stdio";
|
|
125
175
|
if (transportType === "http") {
|
|
126
|
-
const { WebStandardStreamableHTTPServerTransport } =
|
|
127
|
-
"@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js"
|
|
128
|
-
);
|
|
176
|
+
const { WebStandardStreamableHTTPServerTransport } =
|
|
177
|
+
await import("@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js");
|
|
129
178
|
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
130
179
|
sessionIdGenerator: () => crypto.randomUUID(),
|
|
131
180
|
});
|
|
@@ -133,7 +182,7 @@ export async function startServer(opts?: {
|
|
|
133
182
|
|
|
134
183
|
const { RateLimiter } = await import("./rate-limiter");
|
|
135
184
|
const rateLimiter = new RateLimiter(
|
|
136
|
-
config.server?.rate_limit || { enabled: false, requests_per_minute: 60 }
|
|
185
|
+
config.server?.rate_limit || { enabled: false, requests_per_minute: 60 },
|
|
137
186
|
);
|
|
138
187
|
|
|
139
188
|
const port = opts?.port ?? Number(process.env.PORT || 3000);
|
|
@@ -149,8 +198,13 @@ export async function startServer(opts?: {
|
|
|
149
198
|
};
|
|
150
199
|
const origin = req.headers.get("origin") || "";
|
|
151
200
|
const allowedOrigins = serverConfig.allowed_origins;
|
|
152
|
-
const isAllowed =
|
|
153
|
-
|
|
201
|
+
const isAllowed =
|
|
202
|
+
allowedOrigins.includes("*") || allowedOrigins.includes(origin);
|
|
203
|
+
const allowOriginHeader = isAllowed
|
|
204
|
+
? allowedOrigins.includes("*")
|
|
205
|
+
? "*"
|
|
206
|
+
: origin
|
|
207
|
+
: "";
|
|
154
208
|
|
|
155
209
|
if (origin && !isAllowed) {
|
|
156
210
|
return new Response("CORS origin not allowed", { status: 403 });
|
|
@@ -161,7 +215,8 @@ export async function startServer(opts?: {
|
|
|
161
215
|
headers: {
|
|
162
216
|
"Access-Control-Allow-Origin": allowOriginHeader,
|
|
163
217
|
"Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
|
|
164
|
-
"Access-Control-Allow-Headers":
|
|
218
|
+
"Access-Control-Allow-Headers":
|
|
219
|
+
"Content-Type, Authorization, MCP-Protocol-Version",
|
|
165
220
|
},
|
|
166
221
|
});
|
|
167
222
|
}
|
|
@@ -208,29 +263,42 @@ export async function startServer(opts?: {
|
|
|
208
263
|
if (allowOriginHeader) {
|
|
209
264
|
headers.set("Access-Control-Allow-Origin", allowOriginHeader);
|
|
210
265
|
}
|
|
211
|
-
for (const [key, value] of Object.entries(
|
|
266
|
+
for (const [key, value] of Object.entries(
|
|
267
|
+
rateLimitResult.headers,
|
|
268
|
+
)) {
|
|
212
269
|
headers.set(key, value);
|
|
213
270
|
}
|
|
214
|
-
return new Response(JSON.stringify({ status: "ok" }), {
|
|
271
|
+
return new Response(JSON.stringify({ status: "ok" }), {
|
|
272
|
+
status: 200,
|
|
273
|
+
headers,
|
|
274
|
+
});
|
|
215
275
|
}
|
|
216
276
|
if (url.pathname === "/health/ready") {
|
|
217
277
|
const readiness = readinessState.get();
|
|
218
278
|
const headers = new Headers({ "Content-Type": "application/json" });
|
|
219
|
-
if (allowOriginHeader)
|
|
279
|
+
if (allowOriginHeader)
|
|
280
|
+
headers.set("Access-Control-Allow-Origin", allowOriginHeader);
|
|
220
281
|
return new Response(JSON.stringify(readiness), {
|
|
221
282
|
status: readiness.status === "ready" ? 200 : 503,
|
|
222
283
|
headers,
|
|
223
284
|
});
|
|
224
285
|
}
|
|
225
286
|
if (url.pathname === "/metrics") {
|
|
226
|
-
const headers = new Headers({
|
|
287
|
+
const headers = new Headers({
|
|
288
|
+
"Content-Type": "text/plain; version=0.0.4",
|
|
289
|
+
});
|
|
227
290
|
if (allowOriginHeader) {
|
|
228
291
|
headers.set("Access-Control-Allow-Origin", allowOriginHeader);
|
|
229
292
|
}
|
|
230
|
-
for (const [key, value] of Object.entries(
|
|
293
|
+
for (const [key, value] of Object.entries(
|
|
294
|
+
rateLimitResult.headers,
|
|
295
|
+
)) {
|
|
231
296
|
headers.set(key, value);
|
|
232
297
|
}
|
|
233
|
-
return new Response(metricsRegistry.render(), {
|
|
298
|
+
return new Response(metricsRegistry.render(), {
|
|
299
|
+
status: 200,
|
|
300
|
+
headers,
|
|
301
|
+
});
|
|
234
302
|
}
|
|
235
303
|
}
|
|
236
304
|
|
|
@@ -238,10 +306,15 @@ export async function startServer(opts?: {
|
|
|
238
306
|
if (serverConfig.auth_enabled) {
|
|
239
307
|
const expectedToken = resolveAuthToken(serverConfig.auth_token_env);
|
|
240
308
|
if (!expectedToken) {
|
|
241
|
-
return new Response(
|
|
309
|
+
return new Response(
|
|
310
|
+
"Server authentication configured but token environment variable is empty",
|
|
311
|
+
{ status: 500 },
|
|
312
|
+
);
|
|
242
313
|
}
|
|
243
314
|
const authHeader = req.headers.get("authorization") || "";
|
|
244
|
-
const token = authHeader.startsWith("Bearer ")
|
|
315
|
+
const token = authHeader.startsWith("Bearer ")
|
|
316
|
+
? authHeader.slice(7)
|
|
317
|
+
: authHeader;
|
|
245
318
|
if (!token || !safeTokenEquals(token, expectedToken)) {
|
|
246
319
|
return new Response("Unauthorized", { status: 401 });
|
|
247
320
|
}
|
|
@@ -253,15 +326,23 @@ export async function startServer(opts?: {
|
|
|
253
326
|
const since = url.searchParams.get("since") ?? "";
|
|
254
327
|
if (!SINCE_PATTERN.test(since)) {
|
|
255
328
|
return new Response(
|
|
256
|
-
JSON.stringify({
|
|
329
|
+
JSON.stringify({
|
|
330
|
+
error:
|
|
331
|
+
"since must be a relative window (e.g. 30d) or an absolute ISO-8601 date",
|
|
332
|
+
}),
|
|
257
333
|
{ status: 400, headers: { "Content-Type": "application/json" } },
|
|
258
334
|
);
|
|
259
335
|
}
|
|
260
336
|
const { db } = await getRuntime();
|
|
261
337
|
const headers = new Headers({ "Content-Type": "application/json" });
|
|
262
|
-
if (allowOriginHeader)
|
|
263
|
-
|
|
264
|
-
|
|
338
|
+
if (allowOriginHeader)
|
|
339
|
+
headers.set("Access-Control-Allow-Origin", allowOriginHeader);
|
|
340
|
+
for (const [key, value] of Object.entries(rateLimitResult.headers))
|
|
341
|
+
headers.set(key, value);
|
|
342
|
+
return new Response(JSON.stringify(getStats(db, since)), {
|
|
343
|
+
status: 200,
|
|
344
|
+
headers,
|
|
345
|
+
});
|
|
265
346
|
}
|
|
266
347
|
|
|
267
348
|
// Admin HTTP API (/admin/v1/*)
|
|
@@ -270,55 +351,85 @@ export async function startServer(opts?: {
|
|
|
270
351
|
return new Response("Admin endpoints disabled", { status: 403 });
|
|
271
352
|
}
|
|
272
353
|
|
|
273
|
-
const adminTokenEnv =
|
|
354
|
+
const adminTokenEnv =
|
|
355
|
+
serverConfig.admin.token_env || "SKILLMUX_ADMIN_TOKEN";
|
|
274
356
|
const expectedAdminToken = process.env[adminTokenEnv] || "";
|
|
275
357
|
const authHeader = req.headers.get("authorization") || "";
|
|
276
|
-
const token = authHeader.startsWith("Bearer ")
|
|
277
|
-
|
|
278
|
-
|
|
358
|
+
const token = authHeader.startsWith("Bearer ")
|
|
359
|
+
? authHeader.slice(7)
|
|
360
|
+
: authHeader;
|
|
361
|
+
|
|
362
|
+
if (
|
|
363
|
+
!expectedAdminToken ||
|
|
364
|
+
!token ||
|
|
365
|
+
!safeTokenEquals(token, expectedAdminToken)
|
|
366
|
+
) {
|
|
279
367
|
return new Response("Unauthorized", { status: 401 });
|
|
280
368
|
}
|
|
281
369
|
|
|
282
370
|
const headers = new Headers({ "Content-Type": "application/json" });
|
|
283
|
-
if (allowOriginHeader)
|
|
371
|
+
if (allowOriginHeader)
|
|
372
|
+
headers.set("Access-Control-Allow-Origin", allowOriginHeader);
|
|
284
373
|
|
|
285
|
-
if (
|
|
286
|
-
|
|
374
|
+
if (
|
|
375
|
+
req.method === "GET" &&
|
|
376
|
+
url.pathname === "/admin/v1/capabilities"
|
|
377
|
+
) {
|
|
378
|
+
const isExternallyManaged =
|
|
379
|
+
process.env.SKILLMUX_CONFIG_READONLY === "true";
|
|
287
380
|
return new Response(
|
|
288
381
|
JSON.stringify({
|
|
289
382
|
config_read: true,
|
|
290
383
|
config_write: !isExternallyManaged,
|
|
291
384
|
calibration: true,
|
|
292
|
-
persistence: isExternallyManaged
|
|
385
|
+
persistence: isExternallyManaged
|
|
386
|
+
? "externally_managed"
|
|
387
|
+
: "writable",
|
|
293
388
|
reloadable_keys: RELOADABLE_KEYS,
|
|
294
389
|
restart_required_keys: RESTART_REQUIRED_KEYS,
|
|
295
390
|
}),
|
|
296
|
-
{ status: 200, headers }
|
|
391
|
+
{ status: 200, headers },
|
|
297
392
|
);
|
|
298
393
|
}
|
|
299
394
|
|
|
300
395
|
if (req.method === "GET" && url.pathname === "/admin/v1/config") {
|
|
301
|
-
const { effective, sources } = await getEffectiveConfig();
|
|
302
|
-
const
|
|
303
|
-
const
|
|
304
|
-
|
|
396
|
+
const { effective, sources } = await getEffectiveConfig(configPath);
|
|
397
|
+
const desiredHash = computeHash(effective);
|
|
398
|
+
const snapshot = snapshots.acquire();
|
|
399
|
+
const activeRevision = computeHash(snapshot.snapshot.config);
|
|
400
|
+
snapshot.release();
|
|
401
|
+
const status =
|
|
402
|
+
configWatcher?.reloadStatus() ?? inactiveReloadStatus;
|
|
403
|
+
headers.set("ETag", `"${desiredHash}"`);
|
|
305
404
|
return new Response(
|
|
306
405
|
JSON.stringify({
|
|
307
406
|
desired: effective,
|
|
308
407
|
effective,
|
|
309
408
|
sources,
|
|
310
|
-
active_revision:
|
|
311
|
-
runtime:
|
|
409
|
+
active_revision: activeRevision,
|
|
410
|
+
runtime: {
|
|
411
|
+
target: "local",
|
|
412
|
+
desired_source: configPath,
|
|
413
|
+
desired_source_hash: desiredHash,
|
|
414
|
+
active_revision: activeRevision,
|
|
415
|
+
active_source_hash: activeRevision,
|
|
416
|
+
...status,
|
|
417
|
+
readiness: readinessState.get(),
|
|
418
|
+
runtime: "running",
|
|
419
|
+
},
|
|
312
420
|
}),
|
|
313
|
-
{ status: 200, headers }
|
|
421
|
+
{ status: 200, headers },
|
|
314
422
|
);
|
|
315
423
|
}
|
|
316
424
|
|
|
317
425
|
if (req.method === "PATCH" && url.pathname === "/admin/v1/config") {
|
|
318
426
|
if (process.env.SKILLMUX_CONFIG_READONLY === "true") {
|
|
319
427
|
return new Response(
|
|
320
|
-
JSON.stringify({
|
|
321
|
-
|
|
428
|
+
JSON.stringify({
|
|
429
|
+
error: "CONFIG_EXTERNALLY_MANAGED",
|
|
430
|
+
message: "Configuration is externally managed",
|
|
431
|
+
}),
|
|
432
|
+
{ status: 409, headers },
|
|
322
433
|
);
|
|
323
434
|
}
|
|
324
435
|
|
|
@@ -329,45 +440,91 @@ export async function startServer(opts?: {
|
|
|
329
440
|
|
|
330
441
|
if (!ifMatch || cleanIfMatch !== currentHash) {
|
|
331
442
|
return new Response(
|
|
332
|
-
JSON.stringify({
|
|
333
|
-
|
|
443
|
+
JSON.stringify({
|
|
444
|
+
error: "CONFIG_REVISION_CONFLICT",
|
|
445
|
+
message: "Revision conflict",
|
|
446
|
+
}),
|
|
447
|
+
{ status: 409, headers },
|
|
334
448
|
);
|
|
335
449
|
}
|
|
336
450
|
|
|
337
|
-
const body = (await req.json()) as {
|
|
451
|
+
const body = (await req.json()) as {
|
|
452
|
+
changes: Record<string, string | number | boolean>;
|
|
453
|
+
};
|
|
338
454
|
let lastResult: any = null;
|
|
339
455
|
for (const [k, v] of Object.entries(body.changes ?? {})) {
|
|
340
|
-
lastResult = await setDottedKey(k, String(v), {
|
|
456
|
+
lastResult = await setDottedKey(k, String(v), {
|
|
457
|
+
targetName: "remote",
|
|
458
|
+
});
|
|
341
459
|
}
|
|
342
460
|
|
|
343
|
-
return new Response(JSON.stringify(lastResult ?? { ok: true }), {
|
|
461
|
+
return new Response(JSON.stringify(lastResult ?? { ok: true }), {
|
|
462
|
+
status: 200,
|
|
463
|
+
headers,
|
|
464
|
+
});
|
|
344
465
|
}
|
|
345
466
|
|
|
346
467
|
if (url.pathname.startsWith("/admin/v1/calibrations")) {
|
|
347
468
|
const { db } = await getRuntime();
|
|
348
|
-
if (
|
|
469
|
+
if (
|
|
470
|
+
req.method === "GET" &&
|
|
471
|
+
url.pathname === "/admin/v1/calibrations"
|
|
472
|
+
) {
|
|
349
473
|
const runs = listCalibrationRuns(db);
|
|
350
|
-
return new Response(JSON.stringify(runs), {
|
|
474
|
+
return new Response(JSON.stringify(runs), {
|
|
475
|
+
status: 200,
|
|
476
|
+
headers,
|
|
477
|
+
});
|
|
351
478
|
}
|
|
352
|
-
const runIdMatch = url.pathname.match(
|
|
479
|
+
const runIdMatch = url.pathname.match(
|
|
480
|
+
/^\/admin\/v1\/calibrations\/([^\/]+)$/,
|
|
481
|
+
);
|
|
353
482
|
if (req.method === "GET" && runIdMatch && runIdMatch[1]) {
|
|
354
483
|
const runId = runIdMatch[1];
|
|
355
484
|
const run = getCalibrationRun(db, runId);
|
|
356
|
-
if (!run)
|
|
357
|
-
|
|
485
|
+
if (!run)
|
|
486
|
+
return new Response(
|
|
487
|
+
JSON.stringify({ error: "Calibration run not found" }),
|
|
488
|
+
{ status: 404, headers },
|
|
489
|
+
);
|
|
490
|
+
return new Response(JSON.stringify(run), {
|
|
491
|
+
status: 200,
|
|
492
|
+
headers,
|
|
493
|
+
});
|
|
358
494
|
}
|
|
359
|
-
if (
|
|
495
|
+
if (
|
|
496
|
+
req.method === "POST" &&
|
|
497
|
+
url.pathname === "/admin/v1/calibrations"
|
|
498
|
+
) {
|
|
360
499
|
const runId = "run_" + Math.random().toString(36).slice(2, 10);
|
|
361
|
-
return new Response(
|
|
500
|
+
return new Response(
|
|
501
|
+
JSON.stringify({ run_id: runId, status: "running" }),
|
|
502
|
+
{ status: 202, headers },
|
|
503
|
+
);
|
|
362
504
|
}
|
|
363
|
-
const applyMatch = url.pathname.match(
|
|
505
|
+
const applyMatch = url.pathname.match(
|
|
506
|
+
/^\/admin\/v1\/calibrations\/([^\/]+)\/apply$/,
|
|
507
|
+
);
|
|
364
508
|
if (req.method === "POST" && applyMatch && applyMatch[1]) {
|
|
365
509
|
const runId = applyMatch[1];
|
|
366
510
|
const run = getCalibrationRun(db, runId);
|
|
367
|
-
if (!run)
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
511
|
+
if (!run)
|
|
512
|
+
return new Response(
|
|
513
|
+
JSON.stringify({ error: "Calibration run not found" }),
|
|
514
|
+
{ status: 404, headers },
|
|
515
|
+
);
|
|
516
|
+
const { DEFAULT_CONFIG_PATH, expandHome } =
|
|
517
|
+
await import("./config");
|
|
518
|
+
await applyCalibrationRun(
|
|
519
|
+
db,
|
|
520
|
+
runId,
|
|
521
|
+
expandHome(DEFAULT_CONFIG_PATH),
|
|
522
|
+
{},
|
|
523
|
+
);
|
|
524
|
+
return new Response(JSON.stringify({ ok: true, run_id: runId }), {
|
|
525
|
+
status: 200,
|
|
526
|
+
headers,
|
|
527
|
+
});
|
|
371
528
|
}
|
|
372
529
|
}
|
|
373
530
|
|
|
@@ -408,13 +565,17 @@ export async function startServer(opts?: {
|
|
|
408
565
|
console.log(`skillmux serving over HTTP on ${hostname}:${bunServer.port}`);
|
|
409
566
|
return {
|
|
410
567
|
port: bunServer.port,
|
|
568
|
+
reloadStatus: () =>
|
|
569
|
+
configWatcher?.reloadStatus() ?? { ...inactiveReloadStatus },
|
|
411
570
|
async stop() {
|
|
412
571
|
if (stopped) return;
|
|
413
572
|
stopped = true;
|
|
414
573
|
readinessState.set({ ...readinessState.get(), status: "stopping" });
|
|
415
574
|
metricsRegistry.setReadiness(readinessState.get());
|
|
416
575
|
bunServer.stop(true);
|
|
576
|
+
configWatcher?.stop();
|
|
417
577
|
stopWatcher();
|
|
578
|
+
snapshots.dispose();
|
|
418
579
|
await server.close();
|
|
419
580
|
closeRuntime();
|
|
420
581
|
},
|
|
@@ -423,12 +584,16 @@ export async function startServer(opts?: {
|
|
|
423
584
|
await server.connect(new StdioServerTransport());
|
|
424
585
|
let stopped = false;
|
|
425
586
|
return {
|
|
587
|
+
reloadStatus: () =>
|
|
588
|
+
configWatcher?.reloadStatus() ?? { ...inactiveReloadStatus },
|
|
426
589
|
async stop() {
|
|
427
590
|
if (stopped) return;
|
|
428
591
|
stopped = true;
|
|
429
592
|
readinessState.set({ ...readinessState.get(), status: "stopping" });
|
|
430
593
|
metricsRegistry.setReadiness(readinessState.get());
|
|
594
|
+
configWatcher?.stop();
|
|
431
595
|
stopWatcher();
|
|
596
|
+
snapshots.dispose();
|
|
432
597
|
await server.close();
|
|
433
598
|
closeRuntime();
|
|
434
599
|
},
|