@femtomc/mu-server 26.2.72 → 26.2.73
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/README.md +17 -0
- package/dist/api/context.d.ts +5 -0
- package/dist/api/context.js +1147 -0
- package/dist/control_plane_bootstrap_helpers.js +4 -1
- package/dist/control_plane_contract.d.ts +2 -5
- package/dist/control_plane_contract.js +1 -1
- package/dist/orchestration_queue.d.ts +44 -0
- package/dist/orchestration_queue.js +111 -0
- package/dist/run_queue.d.ts +1 -1
- package/dist/run_queue.js +78 -79
- package/dist/run_supervisor.d.ts +1 -1
- package/dist/run_supervisor.js +1 -1
- package/dist/server_routing.js +534 -31
- package/package.json +6 -7
package/dist/server_routing.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { extname, join, resolve } from "node:path";
|
|
2
2
|
import { activityRoutes } from "./api/activities.js";
|
|
3
3
|
import { configRoutes } from "./api/config.js";
|
|
4
|
+
import { contextRoutes } from "./api/context.js";
|
|
4
5
|
import { controlPlaneRoutes } from "./api/control_plane.js";
|
|
5
6
|
import { cronRoutes } from "./api/cron.js";
|
|
6
7
|
import { eventRoutes } from "./api/events.js";
|
|
@@ -9,6 +10,8 @@ import { heartbeatRoutes } from "./api/heartbeats.js";
|
|
|
9
10
|
import { identityRoutes } from "./api/identities.js";
|
|
10
11
|
import { issueRoutes } from "./api/issues.js";
|
|
11
12
|
import { runRoutes } from "./api/runs.js";
|
|
13
|
+
import { cronScheduleInputFromBody, hasCronScheduleInput, parseCronTarget } from "./cron_request.js";
|
|
14
|
+
import { normalizeWakeMode } from "./server_types.js";
|
|
12
15
|
const DEFAULT_MIME_TYPES = {
|
|
13
16
|
".html": "text/html; charset=utf-8",
|
|
14
17
|
".js": "text/javascript; charset=utf-8",
|
|
@@ -22,6 +25,109 @@ const DEFAULT_MIME_TYPES = {
|
|
|
22
25
|
".woff2": "font/woff2",
|
|
23
26
|
};
|
|
24
27
|
const DEFAULT_PUBLIC_DIR = join(new URL(".", import.meta.url).pathname, "..", "public");
|
|
28
|
+
function readTrimmedString(value) {
|
|
29
|
+
return typeof value === "string" ? value.trim() : "";
|
|
30
|
+
}
|
|
31
|
+
function readIntOrNull(value) {
|
|
32
|
+
if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value)) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
return Math.trunc(value);
|
|
36
|
+
}
|
|
37
|
+
function readFiniteNumberOrNull(value) {
|
|
38
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
function parseOptionalBoolean(value) {
|
|
44
|
+
if (value == null) {
|
|
45
|
+
return { ok: true, value: null };
|
|
46
|
+
}
|
|
47
|
+
if (typeof value === "boolean") {
|
|
48
|
+
return { ok: true, value };
|
|
49
|
+
}
|
|
50
|
+
return { ok: false, value: null };
|
|
51
|
+
}
|
|
52
|
+
function readCommaList(value) {
|
|
53
|
+
if (Array.isArray(value)) {
|
|
54
|
+
return value
|
|
55
|
+
.map((item) => (typeof item === "string" ? item.trim() : ""))
|
|
56
|
+
.filter((item) => item.length > 0);
|
|
57
|
+
}
|
|
58
|
+
if (typeof value === "string") {
|
|
59
|
+
return value
|
|
60
|
+
.split(",")
|
|
61
|
+
.map((item) => item.trim())
|
|
62
|
+
.filter((item) => item.length > 0);
|
|
63
|
+
}
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
function parseHeartbeatTarget(body) {
|
|
67
|
+
const targetKind = readTrimmedString(body.target_kind).toLowerCase();
|
|
68
|
+
if (targetKind === "run") {
|
|
69
|
+
const jobId = readTrimmedString(body.run_job_id);
|
|
70
|
+
const rootIssueId = readTrimmedString(body.run_root_issue_id);
|
|
71
|
+
if (!jobId && !rootIssueId) {
|
|
72
|
+
return {
|
|
73
|
+
target: null,
|
|
74
|
+
error: "run target requires run_job_id or run_root_issue_id",
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
target: {
|
|
79
|
+
kind: "run",
|
|
80
|
+
job_id: jobId || null,
|
|
81
|
+
root_issue_id: rootIssueId || null,
|
|
82
|
+
},
|
|
83
|
+
error: null,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
if (targetKind === "activity") {
|
|
87
|
+
const activityId = readTrimmedString(body.activity_id);
|
|
88
|
+
if (!activityId) {
|
|
89
|
+
return {
|
|
90
|
+
target: null,
|
|
91
|
+
error: "activity target requires activity_id",
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
target: {
|
|
96
|
+
kind: "activity",
|
|
97
|
+
activity_id: activityId,
|
|
98
|
+
},
|
|
99
|
+
error: null,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
target: null,
|
|
104
|
+
error: "target_kind must be run or activity",
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function commandProgramFailureStatus(reason) {
|
|
108
|
+
if (reason === "not_found") {
|
|
109
|
+
return 404;
|
|
110
|
+
}
|
|
111
|
+
if (reason === "missing_target" || reason === "invalid_target" || reason === "invalid_schedule") {
|
|
112
|
+
return 400;
|
|
113
|
+
}
|
|
114
|
+
if (reason === "not_running" || reason === "failed") {
|
|
115
|
+
return 409;
|
|
116
|
+
}
|
|
117
|
+
return 400;
|
|
118
|
+
}
|
|
119
|
+
function commandCompletedResponse(headers, targetType, result) {
|
|
120
|
+
return Response.json({
|
|
121
|
+
ok: true,
|
|
122
|
+
result: {
|
|
123
|
+
kind: "completed",
|
|
124
|
+
command: {
|
|
125
|
+
target_type: targetType,
|
|
126
|
+
result,
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
}, { headers });
|
|
130
|
+
}
|
|
25
131
|
export function createServerRequestHandler(deps) {
|
|
26
132
|
const publicDir = deps.publicDir ?? DEFAULT_PUBLIC_DIR;
|
|
27
133
|
const mimeTypes = deps.mimeTypes ?? DEFAULT_MIME_TYPES;
|
|
@@ -84,10 +190,10 @@ export function createServerRequestHandler(deps) {
|
|
|
84
190
|
if (!kind) {
|
|
85
191
|
return Response.json({ error: "kind is required" }, { status: 400, headers });
|
|
86
192
|
}
|
|
87
|
-
let commandText;
|
|
193
|
+
let commandText = null;
|
|
88
194
|
switch (kind) {
|
|
89
195
|
case "run_start": {
|
|
90
|
-
const prompt =
|
|
196
|
+
const prompt = readTrimmedString(body.prompt);
|
|
91
197
|
if (!prompt) {
|
|
92
198
|
return Response.json({ error: "prompt is required for run_start" }, { status: 400, headers });
|
|
93
199
|
}
|
|
@@ -98,7 +204,7 @@ export function createServerRequestHandler(deps) {
|
|
|
98
204
|
break;
|
|
99
205
|
}
|
|
100
206
|
case "run_resume": {
|
|
101
|
-
const rootId =
|
|
207
|
+
const rootId = readTrimmedString(body.root_issue_id);
|
|
102
208
|
const maxSteps = typeof body.max_steps === "number" && Number.isFinite(body.max_steps)
|
|
103
209
|
? ` ${Math.max(1, Math.trunc(body.max_steps))}`
|
|
104
210
|
: "";
|
|
@@ -106,7 +212,7 @@ export function createServerRequestHandler(deps) {
|
|
|
106
212
|
break;
|
|
107
213
|
}
|
|
108
214
|
case "run_interrupt": {
|
|
109
|
-
const rootId =
|
|
215
|
+
const rootId = readTrimmedString(body.root_issue_id);
|
|
110
216
|
commandText = `mu! run interrupt${rootId ? ` ${rootId}` : ""}`;
|
|
111
217
|
break;
|
|
112
218
|
}
|
|
@@ -116,40 +222,434 @@ export function createServerRequestHandler(deps) {
|
|
|
116
222
|
case "update":
|
|
117
223
|
commandText = "/mu update";
|
|
118
224
|
break;
|
|
119
|
-
case "
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
225
|
+
case "issue_create": {
|
|
226
|
+
const title = readTrimmedString(body.title);
|
|
227
|
+
if (!title) {
|
|
228
|
+
return Response.json({ error: "title is required for issue_create" }, { status: 400, headers });
|
|
229
|
+
}
|
|
230
|
+
const issueBody = typeof body.body === "string" ? body.body : undefined;
|
|
231
|
+
const tags = readCommaList(body.tags);
|
|
232
|
+
const priority = readIntOrNull(body.priority);
|
|
233
|
+
if (body.priority != null && priority == null) {
|
|
234
|
+
return Response.json({ error: "priority must be an integer" }, { status: 400, headers });
|
|
235
|
+
}
|
|
236
|
+
const created = await deps.context.issueStore.create(title, {
|
|
237
|
+
body: issueBody,
|
|
238
|
+
tags: tags.length > 0 ? tags : undefined,
|
|
239
|
+
priority: priority ?? undefined,
|
|
240
|
+
});
|
|
241
|
+
const parentId = readTrimmedString(body.parent_id);
|
|
242
|
+
if (parentId) {
|
|
243
|
+
await deps.context.issueStore.add_dep(created.id, "parent", parentId);
|
|
244
|
+
}
|
|
245
|
+
const issue = parentId ? ((await deps.context.issueStore.get(created.id)) ?? created) : created;
|
|
246
|
+
return commandCompletedResponse(headers, "issue create", { issue });
|
|
129
247
|
}
|
|
130
|
-
case "
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
248
|
+
case "issue_update": {
|
|
249
|
+
const id = readTrimmedString(body.id);
|
|
250
|
+
if (!id) {
|
|
251
|
+
return Response.json({ error: "id is required for issue_update" }, { status: 400, headers });
|
|
252
|
+
}
|
|
253
|
+
const current = await deps.context.issueStore.get(id);
|
|
254
|
+
if (!current) {
|
|
255
|
+
return Response.json({ error: "issue not found" }, { status: 404, headers });
|
|
256
|
+
}
|
|
257
|
+
const patch = {};
|
|
258
|
+
if (typeof body.title === "string")
|
|
259
|
+
patch.title = body.title;
|
|
260
|
+
if (typeof body.body === "string")
|
|
261
|
+
patch.body = body.body;
|
|
262
|
+
if (typeof body.status === "string")
|
|
263
|
+
patch.status = body.status;
|
|
264
|
+
if (typeof body.outcome === "string")
|
|
265
|
+
patch.outcome = body.outcome;
|
|
266
|
+
if (body.priority != null) {
|
|
267
|
+
const priority = readIntOrNull(body.priority);
|
|
268
|
+
if (priority == null) {
|
|
269
|
+
return Response.json({ error: "priority must be an integer" }, { status: 400, headers });
|
|
270
|
+
}
|
|
271
|
+
patch.priority = priority;
|
|
272
|
+
}
|
|
273
|
+
if (body.tags != null) {
|
|
274
|
+
patch.tags = readCommaList(body.tags);
|
|
275
|
+
}
|
|
276
|
+
const addTags = readCommaList(body.add_tags);
|
|
277
|
+
const removeTags = readCommaList(body.remove_tags);
|
|
278
|
+
if (addTags.length > 0 || removeTags.length > 0) {
|
|
279
|
+
const baseTags = Array.isArray(patch.tags)
|
|
280
|
+
? (patch.tags ?? [])
|
|
281
|
+
: Array.isArray(current.tags)
|
|
282
|
+
? [...current.tags]
|
|
283
|
+
: [];
|
|
284
|
+
const next = new Set(baseTags);
|
|
285
|
+
for (const tag of addTags)
|
|
286
|
+
next.add(tag);
|
|
287
|
+
for (const tag of removeTags)
|
|
288
|
+
next.delete(tag);
|
|
289
|
+
patch.tags = [...next];
|
|
290
|
+
}
|
|
291
|
+
if (Object.keys(patch).length === 0) {
|
|
292
|
+
return Response.json({ error: "issue_update requires at least one patch field" }, { status: 400, headers });
|
|
293
|
+
}
|
|
294
|
+
const issue = await deps.context.issueStore.update(id, patch);
|
|
295
|
+
return commandCompletedResponse(headers, "issue update", { issue });
|
|
137
296
|
}
|
|
138
|
-
case "
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
297
|
+
case "issue_claim": {
|
|
298
|
+
const id = readTrimmedString(body.id);
|
|
299
|
+
if (!id) {
|
|
300
|
+
return Response.json({ error: "id is required for issue_claim" }, { status: 400, headers });
|
|
301
|
+
}
|
|
302
|
+
const claimed = await deps.context.issueStore.claim(id);
|
|
303
|
+
if (!claimed) {
|
|
304
|
+
return Response.json({ error: "failed to claim issue" }, { status: 409, headers });
|
|
305
|
+
}
|
|
306
|
+
const issue = await deps.context.issueStore.get(id);
|
|
307
|
+
if (!issue) {
|
|
308
|
+
return Response.json({ error: "issue not found" }, { status: 404, headers });
|
|
309
|
+
}
|
|
310
|
+
return commandCompletedResponse(headers, "issue claim", { issue });
|
|
311
|
+
}
|
|
312
|
+
case "issue_open": {
|
|
313
|
+
const id = readTrimmedString(body.id);
|
|
314
|
+
if (!id) {
|
|
315
|
+
return Response.json({ error: "id is required for issue_open" }, { status: 400, headers });
|
|
316
|
+
}
|
|
317
|
+
const issue = await deps.context.issueStore.update(id, { status: "open", outcome: null });
|
|
318
|
+
return commandCompletedResponse(headers, "issue open", { issue });
|
|
319
|
+
}
|
|
320
|
+
case "issue_close": {
|
|
321
|
+
const id = readTrimmedString(body.id);
|
|
322
|
+
if (!id) {
|
|
323
|
+
return Response.json({ error: "id is required for issue_close" }, { status: 400, headers });
|
|
324
|
+
}
|
|
325
|
+
const outcome = readTrimmedString(body.outcome) || "success";
|
|
326
|
+
const issue = await deps.context.issueStore.close(id, outcome);
|
|
327
|
+
return commandCompletedResponse(headers, "issue close", { issue });
|
|
328
|
+
}
|
|
329
|
+
case "issue_dep": {
|
|
330
|
+
const srcId = readTrimmedString(body.src_id);
|
|
331
|
+
const dstId = readTrimmedString(body.dst_id);
|
|
332
|
+
const depType = readTrimmedString(body.dep_type) || "blocks";
|
|
333
|
+
if (!srcId || !dstId) {
|
|
334
|
+
return Response.json({ error: "src_id and dst_id are required for issue_dep" }, { status: 400, headers });
|
|
335
|
+
}
|
|
336
|
+
if (depType !== "blocks" && depType !== "parent") {
|
|
337
|
+
return Response.json({ error: "dep_type must be blocks or parent" }, { status: 400, headers });
|
|
338
|
+
}
|
|
339
|
+
await deps.context.issueStore.add_dep(srcId, depType, dstId);
|
|
340
|
+
return commandCompletedResponse(headers, "issue dep", {
|
|
341
|
+
src_id: srcId,
|
|
342
|
+
dst_id: dstId,
|
|
343
|
+
dep_type: depType,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
case "issue_undep": {
|
|
347
|
+
const srcId = readTrimmedString(body.src_id);
|
|
348
|
+
const dstId = readTrimmedString(body.dst_id);
|
|
349
|
+
const depType = readTrimmedString(body.dep_type) || "blocks";
|
|
350
|
+
if (!srcId || !dstId) {
|
|
351
|
+
return Response.json({ error: "src_id and dst_id are required for issue_undep" }, { status: 400, headers });
|
|
352
|
+
}
|
|
353
|
+
if (depType !== "blocks" && depType !== "parent") {
|
|
354
|
+
return Response.json({ error: "dep_type must be blocks or parent" }, { status: 400, headers });
|
|
355
|
+
}
|
|
356
|
+
const ok = await deps.context.issueStore.remove_dep(srcId, depType, dstId);
|
|
357
|
+
return commandCompletedResponse(headers, "issue undep", {
|
|
358
|
+
src_id: srcId,
|
|
359
|
+
dst_id: dstId,
|
|
360
|
+
dep_type: depType,
|
|
361
|
+
ok,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
case "forum_post": {
|
|
365
|
+
const topic = readTrimmedString(body.topic);
|
|
366
|
+
const messageBody = readTrimmedString(body.body);
|
|
367
|
+
if (!topic) {
|
|
368
|
+
return Response.json({ error: "topic is required for forum_post" }, { status: 400, headers });
|
|
369
|
+
}
|
|
370
|
+
if (!messageBody) {
|
|
371
|
+
return Response.json({ error: "body is required for forum_post" }, { status: 400, headers });
|
|
372
|
+
}
|
|
373
|
+
const author = readTrimmedString(body.author) || "operator";
|
|
374
|
+
const message = await deps.context.forumStore.post(topic, messageBody, author);
|
|
375
|
+
return commandCompletedResponse(headers, "forum post", { message });
|
|
376
|
+
}
|
|
377
|
+
case "heartbeat_create": {
|
|
378
|
+
const title = readTrimmedString(body.title);
|
|
379
|
+
if (!title) {
|
|
380
|
+
return Response.json({ error: "title is required for heartbeat_create" }, { status: 400, headers });
|
|
381
|
+
}
|
|
382
|
+
const parsedTarget = parseHeartbeatTarget(body);
|
|
383
|
+
if (!parsedTarget.target) {
|
|
384
|
+
return Response.json({ error: parsedTarget.error ?? "invalid target" }, { status: 400, headers });
|
|
385
|
+
}
|
|
386
|
+
const everyMsRaw = readFiniteNumberOrNull(body.every_ms);
|
|
387
|
+
if (body.every_ms != null && everyMsRaw == null) {
|
|
388
|
+
return Response.json({ error: "every_ms must be a finite number" }, { status: 400, headers });
|
|
389
|
+
}
|
|
390
|
+
if (body.reason != null && typeof body.reason !== "string") {
|
|
391
|
+
return Response.json({ error: "reason must be a string" }, { status: 400, headers });
|
|
392
|
+
}
|
|
393
|
+
if (body.wake_mode != null && typeof body.wake_mode !== "string") {
|
|
394
|
+
return Response.json({ error: "wake_mode must be a string" }, { status: 400, headers });
|
|
395
|
+
}
|
|
396
|
+
const enabled = parseOptionalBoolean(body.enabled);
|
|
397
|
+
if (!enabled.ok) {
|
|
398
|
+
return Response.json({ error: "enabled must be boolean" }, { status: 400, headers });
|
|
399
|
+
}
|
|
400
|
+
if (body.metadata != null &&
|
|
401
|
+
(typeof body.metadata !== "object" || Array.isArray(body.metadata))) {
|
|
402
|
+
return Response.json({ error: "metadata must be an object" }, { status: 400, headers });
|
|
403
|
+
}
|
|
404
|
+
try {
|
|
405
|
+
const program = await deps.heartbeatPrograms.create({
|
|
406
|
+
title,
|
|
407
|
+
target: parsedTarget.target,
|
|
408
|
+
everyMs: everyMsRaw == null ? undefined : Math.max(0, Math.trunc(everyMsRaw)),
|
|
409
|
+
reason: typeof body.reason === "string" ? body.reason : undefined,
|
|
410
|
+
wakeMode: body.wake_mode == null ? undefined : normalizeWakeMode(body.wake_mode),
|
|
411
|
+
enabled: enabled.value ?? undefined,
|
|
412
|
+
metadata: body.metadata,
|
|
413
|
+
});
|
|
414
|
+
return commandCompletedResponse(headers, "heartbeat create", { program });
|
|
415
|
+
}
|
|
416
|
+
catch (err) {
|
|
417
|
+
return Response.json({ error: deps.describeError(err) }, { status: 400, headers });
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
case "heartbeat_update": {
|
|
421
|
+
const programId = readTrimmedString(body.program_id);
|
|
422
|
+
if (!programId) {
|
|
423
|
+
return Response.json({ error: "program_id is required for heartbeat_update" }, { status: 400, headers });
|
|
424
|
+
}
|
|
425
|
+
let target;
|
|
426
|
+
if (body.target_kind != null) {
|
|
427
|
+
const parsedTarget = parseHeartbeatTarget(body);
|
|
428
|
+
if (!parsedTarget.target) {
|
|
429
|
+
return Response.json({ error: parsedTarget.error ?? "invalid target" }, { status: 400, headers });
|
|
430
|
+
}
|
|
431
|
+
target = parsedTarget.target;
|
|
432
|
+
}
|
|
433
|
+
const everyMsRaw = readFiniteNumberOrNull(body.every_ms);
|
|
434
|
+
if (body.every_ms != null && everyMsRaw == null) {
|
|
435
|
+
return Response.json({ error: "every_ms must be a finite number" }, { status: 400, headers });
|
|
436
|
+
}
|
|
437
|
+
if (body.reason != null && typeof body.reason !== "string") {
|
|
438
|
+
return Response.json({ error: "reason must be a string" }, { status: 400, headers });
|
|
439
|
+
}
|
|
440
|
+
if (body.wake_mode != null && typeof body.wake_mode !== "string") {
|
|
441
|
+
return Response.json({ error: "wake_mode must be a string" }, { status: 400, headers });
|
|
442
|
+
}
|
|
443
|
+
const enabled = parseOptionalBoolean(body.enabled);
|
|
444
|
+
if (!enabled.ok) {
|
|
445
|
+
return Response.json({ error: "enabled must be boolean" }, { status: 400, headers });
|
|
446
|
+
}
|
|
447
|
+
if (body.metadata != null &&
|
|
448
|
+
(typeof body.metadata !== "object" || Array.isArray(body.metadata))) {
|
|
449
|
+
return Response.json({ error: "metadata must be an object" }, { status: 400, headers });
|
|
450
|
+
}
|
|
451
|
+
try {
|
|
452
|
+
const result = await deps.heartbeatPrograms.update({
|
|
453
|
+
programId,
|
|
454
|
+
title: typeof body.title === "string" ? body.title : undefined,
|
|
455
|
+
target,
|
|
456
|
+
everyMs: everyMsRaw == null ? undefined : Math.max(0, Math.trunc(everyMsRaw)),
|
|
457
|
+
reason: typeof body.reason === "string" ? body.reason : undefined,
|
|
458
|
+
wakeMode: body.wake_mode == null ? undefined : normalizeWakeMode(body.wake_mode),
|
|
459
|
+
enabled: enabled.value ?? undefined,
|
|
460
|
+
metadata: body.metadata,
|
|
461
|
+
});
|
|
462
|
+
if (!result.ok) {
|
|
463
|
+
return Response.json({ error: `heartbeat update failed: ${result.reason ?? "unknown"}` }, { status: commandProgramFailureStatus(result.reason), headers });
|
|
464
|
+
}
|
|
465
|
+
return commandCompletedResponse(headers, "heartbeat update", { program: result.program });
|
|
466
|
+
}
|
|
467
|
+
catch (err) {
|
|
468
|
+
return Response.json({ error: deps.describeError(err) }, { status: 400, headers });
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
case "heartbeat_delete": {
|
|
472
|
+
const programId = readTrimmedString(body.program_id);
|
|
473
|
+
if (!programId) {
|
|
474
|
+
return Response.json({ error: "program_id is required for heartbeat_delete" }, { status: 400, headers });
|
|
475
|
+
}
|
|
476
|
+
const result = await deps.heartbeatPrograms.remove(programId);
|
|
477
|
+
if (!result.ok) {
|
|
478
|
+
return Response.json({ error: `heartbeat delete failed: ${result.reason ?? "unknown"}` }, { status: commandProgramFailureStatus(result.reason), headers });
|
|
479
|
+
}
|
|
480
|
+
return commandCompletedResponse(headers, "heartbeat delete", { program: result.program });
|
|
481
|
+
}
|
|
482
|
+
case "heartbeat_trigger": {
|
|
483
|
+
const programId = readTrimmedString(body.program_id);
|
|
484
|
+
if (!programId) {
|
|
485
|
+
return Response.json({ error: "program_id is required for heartbeat_trigger" }, { status: 400, headers });
|
|
486
|
+
}
|
|
487
|
+
if (body.reason != null && typeof body.reason !== "string") {
|
|
488
|
+
return Response.json({ error: "reason must be a string" }, { status: 400, headers });
|
|
489
|
+
}
|
|
490
|
+
const result = await deps.heartbeatPrograms.trigger({
|
|
491
|
+
programId,
|
|
492
|
+
reason: typeof body.reason === "string" ? body.reason : null,
|
|
493
|
+
});
|
|
494
|
+
if (!result.ok) {
|
|
495
|
+
return Response.json({ error: `heartbeat trigger failed: ${result.reason ?? "unknown"}` }, { status: commandProgramFailureStatus(result.reason), headers });
|
|
496
|
+
}
|
|
497
|
+
return commandCompletedResponse(headers, "heartbeat trigger", { program: result.program });
|
|
498
|
+
}
|
|
499
|
+
case "heartbeat_enable":
|
|
500
|
+
case "heartbeat_disable": {
|
|
501
|
+
const programId = readTrimmedString(body.program_id);
|
|
502
|
+
if (!programId) {
|
|
503
|
+
return Response.json({ error: `program_id is required for ${kind}` }, { status: 400, headers });
|
|
504
|
+
}
|
|
505
|
+
const result = await deps.heartbeatPrograms.update({
|
|
506
|
+
programId,
|
|
507
|
+
enabled: kind === "heartbeat_enable",
|
|
508
|
+
});
|
|
509
|
+
if (!result.ok) {
|
|
510
|
+
return Response.json({ error: `${kind} failed: ${result.reason ?? "unknown"}` }, { status: commandProgramFailureStatus(result.reason), headers });
|
|
511
|
+
}
|
|
512
|
+
return commandCompletedResponse(headers, kind.replaceAll("_", " "), { program: result.program });
|
|
513
|
+
}
|
|
514
|
+
case "cron_create": {
|
|
515
|
+
const title = readTrimmedString(body.title);
|
|
516
|
+
if (!title) {
|
|
517
|
+
return Response.json({ error: "title is required for cron_create" }, { status: 400, headers });
|
|
518
|
+
}
|
|
519
|
+
const parsedTarget = parseCronTarget(body);
|
|
520
|
+
if (!parsedTarget.target) {
|
|
521
|
+
return Response.json({ error: parsedTarget.error ?? "invalid target" }, { status: 400, headers });
|
|
522
|
+
}
|
|
523
|
+
if (!hasCronScheduleInput(body)) {
|
|
524
|
+
return Response.json({ error: "schedule is required for cron_create" }, { status: 400, headers });
|
|
525
|
+
}
|
|
526
|
+
if (body.reason != null && typeof body.reason !== "string") {
|
|
527
|
+
return Response.json({ error: "reason must be a string" }, { status: 400, headers });
|
|
528
|
+
}
|
|
529
|
+
if (body.wake_mode != null && typeof body.wake_mode !== "string") {
|
|
530
|
+
return Response.json({ error: "wake_mode must be a string" }, { status: 400, headers });
|
|
531
|
+
}
|
|
532
|
+
const enabled = parseOptionalBoolean(body.enabled);
|
|
533
|
+
if (!enabled.ok) {
|
|
534
|
+
return Response.json({ error: "enabled must be boolean" }, { status: 400, headers });
|
|
535
|
+
}
|
|
536
|
+
if (body.metadata != null &&
|
|
537
|
+
(typeof body.metadata !== "object" || Array.isArray(body.metadata))) {
|
|
538
|
+
return Response.json({ error: "metadata must be an object" }, { status: 400, headers });
|
|
539
|
+
}
|
|
540
|
+
try {
|
|
541
|
+
const program = await deps.cronPrograms.create({
|
|
542
|
+
title,
|
|
543
|
+
target: parsedTarget.target,
|
|
544
|
+
schedule: cronScheduleInputFromBody(body),
|
|
545
|
+
reason: typeof body.reason === "string" ? body.reason : undefined,
|
|
546
|
+
wakeMode: body.wake_mode == null ? undefined : normalizeWakeMode(body.wake_mode),
|
|
547
|
+
enabled: enabled.value ?? undefined,
|
|
548
|
+
metadata: body.metadata,
|
|
549
|
+
});
|
|
550
|
+
return commandCompletedResponse(headers, "cron create", { program });
|
|
551
|
+
}
|
|
552
|
+
catch (err) {
|
|
553
|
+
return Response.json({ error: deps.describeError(err) }, { status: 400, headers });
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
case "cron_update": {
|
|
557
|
+
const programId = readTrimmedString(body.program_id);
|
|
558
|
+
if (!programId) {
|
|
559
|
+
return Response.json({ error: "program_id is required for cron_update" }, { status: 400, headers });
|
|
560
|
+
}
|
|
561
|
+
let target;
|
|
562
|
+
if (body.target_kind != null) {
|
|
563
|
+
const parsedTarget = parseCronTarget(body);
|
|
564
|
+
if (!parsedTarget.target) {
|
|
565
|
+
return Response.json({ error: parsedTarget.error ?? "invalid target" }, { status: 400, headers });
|
|
566
|
+
}
|
|
567
|
+
target = parsedTarget.target;
|
|
568
|
+
}
|
|
569
|
+
if (body.reason != null && typeof body.reason !== "string") {
|
|
570
|
+
return Response.json({ error: "reason must be a string" }, { status: 400, headers });
|
|
571
|
+
}
|
|
572
|
+
if (body.wake_mode != null && typeof body.wake_mode !== "string") {
|
|
573
|
+
return Response.json({ error: "wake_mode must be a string" }, { status: 400, headers });
|
|
574
|
+
}
|
|
575
|
+
const enabled = parseOptionalBoolean(body.enabled);
|
|
576
|
+
if (!enabled.ok) {
|
|
577
|
+
return Response.json({ error: "enabled must be boolean" }, { status: 400, headers });
|
|
578
|
+
}
|
|
579
|
+
if (body.metadata != null &&
|
|
580
|
+
(typeof body.metadata !== "object" || Array.isArray(body.metadata))) {
|
|
581
|
+
return Response.json({ error: "metadata must be an object" }, { status: 400, headers });
|
|
582
|
+
}
|
|
583
|
+
try {
|
|
584
|
+
const result = await deps.cronPrograms.update({
|
|
585
|
+
programId,
|
|
586
|
+
title: typeof body.title === "string" ? body.title : undefined,
|
|
587
|
+
reason: typeof body.reason === "string" ? body.reason : undefined,
|
|
588
|
+
wakeMode: body.wake_mode == null ? undefined : normalizeWakeMode(body.wake_mode),
|
|
589
|
+
enabled: enabled.value ?? undefined,
|
|
590
|
+
target,
|
|
591
|
+
schedule: hasCronScheduleInput(body) ? cronScheduleInputFromBody(body) : undefined,
|
|
592
|
+
metadata: body.metadata,
|
|
593
|
+
});
|
|
594
|
+
if (!result.ok) {
|
|
595
|
+
return Response.json({ error: `cron update failed: ${result.reason ?? "unknown"}` }, { status: commandProgramFailureStatus(result.reason), headers });
|
|
596
|
+
}
|
|
597
|
+
return commandCompletedResponse(headers, "cron update", { program: result.program });
|
|
598
|
+
}
|
|
599
|
+
catch (err) {
|
|
600
|
+
return Response.json({ error: deps.describeError(err) }, { status: 400, headers });
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
case "cron_delete": {
|
|
604
|
+
const programId = readTrimmedString(body.program_id);
|
|
605
|
+
if (!programId) {
|
|
606
|
+
return Response.json({ error: "program_id is required for cron_delete" }, { status: 400, headers });
|
|
607
|
+
}
|
|
608
|
+
const result = await deps.cronPrograms.remove(programId);
|
|
609
|
+
if (!result.ok) {
|
|
610
|
+
return Response.json({ error: `cron delete failed: ${result.reason ?? "unknown"}` }, { status: commandProgramFailureStatus(result.reason), headers });
|
|
611
|
+
}
|
|
612
|
+
return commandCompletedResponse(headers, "cron delete", { program: result.program });
|
|
613
|
+
}
|
|
614
|
+
case "cron_trigger": {
|
|
615
|
+
const programId = readTrimmedString(body.program_id);
|
|
616
|
+
if (!programId) {
|
|
617
|
+
return Response.json({ error: "program_id is required for cron_trigger" }, { status: 400, headers });
|
|
618
|
+
}
|
|
619
|
+
if (body.reason != null && typeof body.reason !== "string") {
|
|
620
|
+
return Response.json({ error: "reason must be a string" }, { status: 400, headers });
|
|
621
|
+
}
|
|
622
|
+
const result = await deps.cronPrograms.trigger({
|
|
623
|
+
programId,
|
|
624
|
+
reason: typeof body.reason === "string" ? body.reason : null,
|
|
625
|
+
});
|
|
626
|
+
if (!result.ok) {
|
|
627
|
+
return Response.json({ error: `cron trigger failed: ${result.reason ?? "unknown"}` }, { status: commandProgramFailureStatus(result.reason), headers });
|
|
628
|
+
}
|
|
629
|
+
return commandCompletedResponse(headers, "cron trigger", { program: result.program });
|
|
630
|
+
}
|
|
631
|
+
case "cron_enable":
|
|
632
|
+
case "cron_disable": {
|
|
633
|
+
const programId = readTrimmedString(body.program_id);
|
|
634
|
+
if (!programId) {
|
|
635
|
+
return Response.json({ error: `program_id is required for ${kind}` }, { status: 400, headers });
|
|
636
|
+
}
|
|
637
|
+
const result = await deps.cronPrograms.update({
|
|
638
|
+
programId,
|
|
639
|
+
enabled: kind === "cron_enable",
|
|
640
|
+
});
|
|
641
|
+
if (!result.ok) {
|
|
642
|
+
return Response.json({ error: `${kind} failed: ${result.reason ?? "unknown"}` }, { status: commandProgramFailureStatus(result.reason), headers });
|
|
643
|
+
}
|
|
644
|
+
return commandCompletedResponse(headers, kind.replaceAll("_", " "), { program: result.program });
|
|
145
645
|
}
|
|
146
|
-
case "ready":
|
|
147
|
-
commandText = "/mu ready";
|
|
148
|
-
break;
|
|
149
646
|
default:
|
|
150
647
|
return Response.json({ error: `unknown command kind: ${kind}` }, { status: 400, headers });
|
|
151
648
|
}
|
|
152
649
|
try {
|
|
650
|
+
if (!commandText) {
|
|
651
|
+
return Response.json({ error: `unknown command kind: ${kind}` }, { status: 400, headers });
|
|
652
|
+
}
|
|
153
653
|
if (!deps.controlPlaneProxy.submitTerminalCommand) {
|
|
154
654
|
return Response.json({ error: "control plane not available" }, { status: 503, headers });
|
|
155
655
|
}
|
|
@@ -199,6 +699,9 @@ export function createServerRequestHandler(deps) {
|
|
|
199
699
|
});
|
|
200
700
|
return response;
|
|
201
701
|
}
|
|
702
|
+
if (path === "/api/context" || path.startsWith("/api/context/")) {
|
|
703
|
+
return contextRoutes(request, url, { context: deps.context, describeError: deps.describeError }, headers);
|
|
704
|
+
}
|
|
202
705
|
if (path.startsWith("/webhooks/")) {
|
|
203
706
|
const response = await deps.controlPlaneProxy.handleWebhook(path, request);
|
|
204
707
|
if (response) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@femtomc/mu-server",
|
|
3
|
-
"version": "26.2.
|
|
3
|
+
"version": "26.2.73",
|
|
4
4
|
"description": "HTTP API server for mu status, work items, messaging setup, and web UI.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mu",
|
|
@@ -31,11 +31,10 @@
|
|
|
31
31
|
"start": "bun run dist/cli.js"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@femtomc/mu-agent": "26.2.
|
|
35
|
-
"@femtomc/mu-control-plane": "26.2.
|
|
36
|
-
"@femtomc/mu-core": "26.2.
|
|
37
|
-
"@femtomc/mu-forum": "26.2.
|
|
38
|
-
"@femtomc/mu-issue": "26.2.
|
|
39
|
-
"@femtomc/mu-orchestrator": "26.2.72"
|
|
34
|
+
"@femtomc/mu-agent": "26.2.73",
|
|
35
|
+
"@femtomc/mu-control-plane": "26.2.73",
|
|
36
|
+
"@femtomc/mu-core": "26.2.73",
|
|
37
|
+
"@femtomc/mu-forum": "26.2.73",
|
|
38
|
+
"@femtomc/mu-issue": "26.2.73"
|
|
40
39
|
}
|
|
41
40
|
}
|