@femtomc/mu-server 26.2.65 → 26.2.67

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.
@@ -1,5 +1,5 @@
1
1
  import { type MessagingOperatorBackend, MessagingOperatorRuntime } from "@femtomc/mu-agent";
2
- import { type Channel, type GenerationTelemetryRecorder, type ReloadableGenerationIdentity } from "@femtomc/mu-control-plane";
2
+ import { type Channel, type CommandPipelineResult, type GenerationTelemetryRecorder, type ReloadableGenerationIdentity } from "@femtomc/mu-control-plane";
3
3
  import { type MuConfig } from "./config.js";
4
4
  import type { ActivityHeartbeatScheduler } from "./heartbeat_scheduler.js";
5
5
  import { type ControlPlaneRunHeartbeatResult, type ControlPlaneRunInterruptResult, type ControlPlaneRunSnapshot, type ControlPlaneRunSupervisorOpts, type ControlPlaneRunTrace } from "./run_supervisor.js";
@@ -76,6 +76,11 @@ export type ControlPlaneHandle = {
76
76
  idOrRoot: string;
77
77
  limit?: number;
78
78
  }): Promise<ControlPlaneRunTrace | null>;
79
+ submitTerminalCommand?(opts: {
80
+ commandText: string;
81
+ repoRoot: string;
82
+ requestId?: string;
83
+ }): Promise<CommandPipelineResult>;
79
84
  stop(): Promise<void>;
80
85
  };
81
86
  export type ControlPlaneConfig = MuConfig["control_plane"];
@@ -96,6 +101,17 @@ export type TelegramGenerationSwapHooks = {
96
101
  timeout_ms: number;
97
102
  }) => void | Promise<void>;
98
103
  };
104
+ export type ControlPlaneSessionMutationAction = "reload" | "update";
105
+ export type ControlPlaneSessionMutationResult = {
106
+ ok: boolean;
107
+ action: ControlPlaneSessionMutationAction;
108
+ message: string;
109
+ details?: Record<string, unknown>;
110
+ };
111
+ export type ControlPlaneSessionMutationHooks = {
112
+ reload?: () => Promise<ControlPlaneSessionMutationResult>;
113
+ update?: () => Promise<ControlPlaneSessionMutationResult>;
114
+ };
99
115
  type DetectedAdapter = {
100
116
  name: "slack";
101
117
  signingSecret: string;
@@ -135,9 +151,11 @@ export type BootstrapControlPlaneOpts = {
135
151
  heartbeatScheduler?: ActivityHeartbeatScheduler;
136
152
  runSupervisorSpawnProcess?: ControlPlaneRunSupervisorOpts["spawnProcess"];
137
153
  runSupervisorHeartbeatIntervalMs?: number;
154
+ sessionMutationHooks?: ControlPlaneSessionMutationHooks;
138
155
  generation?: ControlPlaneGenerationContext;
139
156
  telemetry?: GenerationTelemetryRecorder | null;
140
157
  telegramGenerationHooks?: TelegramGenerationSwapHooks;
158
+ terminalEnabled?: boolean;
141
159
  };
142
160
  export declare function bootstrapControlPlane(opts: BootstrapControlPlaneOpts): Promise<ControlPlaneHandle | null>;
143
161
  export {};
@@ -702,7 +702,7 @@ export async function bootstrapControlPlane(opts) {
702
702
  },
703
703
  }
704
704
  : undefined;
705
- if (detected.length === 0) {
705
+ if (detected.length === 0 && !opts.terminalEnabled) {
706
706
  return null;
707
707
  }
708
708
  const paths = getControlPlanePaths(opts.repoRoot);
@@ -745,6 +745,116 @@ export async function bootstrapControlPlane(opts) {
745
745
  runtime,
746
746
  operator,
747
747
  mutationExecutor: async (record) => {
748
+ if (record.target_type === "reload" || record.target_type === "update") {
749
+ if (record.command_args.length > 0) {
750
+ return {
751
+ terminalState: "failed",
752
+ errorCode: "cli_validation_failed",
753
+ trace: {
754
+ cliCommandKind: record.target_type,
755
+ runRootId: null,
756
+ },
757
+ mutatingEvents: [
758
+ {
759
+ eventType: "session.lifecycle.command.failed",
760
+ payload: {
761
+ action: record.target_type,
762
+ reason: "unexpected_args",
763
+ args: record.command_args,
764
+ },
765
+ },
766
+ ],
767
+ };
768
+ }
769
+ const action = record.target_type;
770
+ const hook = action === "reload"
771
+ ? opts.sessionMutationHooks?.reload
772
+ : opts.sessionMutationHooks?.update;
773
+ if (!hook) {
774
+ return {
775
+ terminalState: "failed",
776
+ errorCode: "session_lifecycle_unavailable",
777
+ trace: {
778
+ cliCommandKind: action,
779
+ runRootId: null,
780
+ },
781
+ mutatingEvents: [
782
+ {
783
+ eventType: "session.lifecycle.command.failed",
784
+ payload: {
785
+ action,
786
+ reason: "hook_missing",
787
+ },
788
+ },
789
+ ],
790
+ };
791
+ }
792
+ try {
793
+ const lifecycle = await hook();
794
+ if (!lifecycle.ok) {
795
+ return {
796
+ terminalState: "failed",
797
+ errorCode: "session_lifecycle_failed",
798
+ trace: {
799
+ cliCommandKind: action,
800
+ runRootId: null,
801
+ },
802
+ mutatingEvents: [
803
+ {
804
+ eventType: "session.lifecycle.command.failed",
805
+ payload: {
806
+ action,
807
+ reason: lifecycle.message,
808
+ details: lifecycle.details ?? null,
809
+ },
810
+ },
811
+ ],
812
+ };
813
+ }
814
+ return {
815
+ terminalState: "completed",
816
+ result: {
817
+ ok: true,
818
+ action,
819
+ message: lifecycle.message,
820
+ details: lifecycle.details ?? null,
821
+ },
822
+ trace: {
823
+ cliCommandKind: action,
824
+ runRootId: null,
825
+ },
826
+ mutatingEvents: [
827
+ {
828
+ eventType: `session.lifecycle.command.${action}`,
829
+ payload: {
830
+ action,
831
+ message: lifecycle.message,
832
+ details: lifecycle.details ?? null,
833
+ },
834
+ },
835
+ ],
836
+ };
837
+ }
838
+ catch (err) {
839
+ return {
840
+ terminalState: "failed",
841
+ errorCode: err instanceof Error && err.message ? err.message : "session_lifecycle_failed",
842
+ trace: {
843
+ cliCommandKind: action,
844
+ runRootId: null,
845
+ },
846
+ mutatingEvents: [
847
+ {
848
+ eventType: "session.lifecycle.command.failed",
849
+ payload: {
850
+ action,
851
+ reason: err instanceof Error && err.message ? err.message : "session_lifecycle_failed",
852
+ },
853
+ },
854
+ ],
855
+ };
856
+ }
857
+ }
748
858
  if (record.target_type === "run start" || record.target_type === "run resume") {
749
859
  try {
750
860
  const launched = await runSupervisor?.startFromCommand(record);
@@ -1050,6 +1160,12 @@ export async function bootstrapControlPlane(opts) {
1050
1160
  async traceRun(traceOpts) {
1051
1161
  return (await runSupervisor?.trace(traceOpts.idOrRoot, { limit: traceOpts.limit })) ?? null;
1052
1162
  },
1163
+ async submitTerminalCommand(terminalOpts) {
1164
+ if (!pipeline) {
1165
+ throw new Error("control_plane_pipeline_unavailable");
1166
+ }
1167
+ return await pipeline.handleTerminalInbound(terminalOpts);
1168
+ },
1053
1169
  async stop() {
1054
1170
  if (drainInterval) {
1055
1171
  clearInterval(drainInterval);
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ export type { ControlPlaneActivityEvent, ControlPlaneActivityEventKind, ControlP
2
2
  export { ControlPlaneActivitySupervisor } from "./activity_supervisor.js";
3
3
  export type { MuConfig, MuConfigPatch, MuConfigPresence } from "./config.js";
4
4
  export { applyMuConfigPatch, DEFAULT_MU_CONFIG, getMuConfigPath, muConfigPresence, normalizeMuConfig, readMuConfigFile, redactMuConfigSecrets, writeMuConfigFile, } from "./config.js";
5
- export type { ActiveAdapter, ControlPlaneConfig, ControlPlaneHandle } from "./control_plane.js";
5
+ export type { ActiveAdapter, ControlPlaneConfig, ControlPlaneHandle, ControlPlaneSessionMutationAction, ControlPlaneSessionMutationHooks, ControlPlaneSessionMutationResult, } from "./control_plane.js";
6
6
  export { bootstrapControlPlane, detectAdapters } from "./control_plane.js";
7
7
  export type { CronProgramLifecycleAction, CronProgramLifecycleEvent, CronProgramOperationResult, CronProgramRegistryOpts, CronProgramSnapshot, CronProgramStatusSnapshot, CronProgramTarget, CronProgramTickEvent, CronProgramWakeMode, } from "./cron_programs.js";
8
8
  export { CronProgramRegistry } from "./cron_programs.js";
package/dist/server.d.ts CHANGED
@@ -5,7 +5,7 @@ import { ForumStore } from "@femtomc/mu-forum";
5
5
  import { IssueStore } from "@femtomc/mu-issue";
6
6
  import { ControlPlaneActivitySupervisor } from "./activity_supervisor.js";
7
7
  import { type MuConfig } from "./config.js";
8
- import { type ControlPlaneConfig, type ControlPlaneHandle } from "./control_plane.js";
8
+ import { type ControlPlaneConfig, type ControlPlaneHandle, type ControlPlaneSessionMutationHooks } from "./control_plane.js";
9
9
  import { CronProgramRegistry } from "./cron_programs.js";
10
10
  import { HeartbeatProgramRegistry } from "./heartbeat_programs.js";
11
11
  import { ActivityHeartbeatScheduler } from "./heartbeat_scheduler.js";
@@ -30,6 +30,7 @@ export type ServerOptions = {
30
30
  config?: MuConfig;
31
31
  configReader?: ConfigReader;
32
32
  configWriter?: ConfigWriter;
33
+ sessionMutationHooks?: ControlPlaneSessionMutationHooks;
33
34
  };
34
35
  export type ServerContext = {
35
36
  repoRoot: string;
package/dist/server.js CHANGED
@@ -46,6 +46,12 @@ function toNonNegativeInt(value, fallback) {
46
46
  }
47
47
  return Math.max(0, Math.trunc(fallback));
48
48
  }
49
+ function shellQuoteArg(value) {
50
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
51
+ }
52
+ function shellJoin(args) {
53
+ return args.map(shellQuoteArg).join(" ");
54
+ }
49
55
  function describeError(err) {
50
56
  if (err instanceof Error)
51
57
  return err.message;
@@ -165,6 +171,144 @@ export function createServer(options = {}) {
165
171
  const autoRunHeartbeatEveryMs = Math.max(1_000, toNonNegativeInt(options.autoRunHeartbeatEveryMs, DEFAULT_AUTO_RUN_HEARTBEAT_EVERY_MS));
166
172
  const operatorWakeLastByKey = new Map();
167
173
  const autoRunHeartbeatProgramByJobId = new Map();
174
+ let sessionMutationScheduled = null;
175
+ const runShellCommand = async (command) => {
176
+ const proc = Bun.spawn({
177
+ cmd: ["bash", "-lc", command],
178
+ cwd: repoRoot,
179
+ env: Bun.env,
180
+ stdin: "ignore",
181
+ stdout: "pipe",
182
+ stderr: "pipe",
183
+ });
184
+ const [exitCode, stdout, stderr] = await Promise.all([
185
+ proc.exited,
186
+ proc.stdout ? new Response(proc.stdout).text() : Promise.resolve(""),
187
+ proc.stderr ? new Response(proc.stderr).text() : Promise.resolve(""),
188
+ ]);
189
+ return {
190
+ exitCode: Number.isFinite(exitCode) ? Number(exitCode) : 1,
191
+ stdout,
192
+ stderr,
193
+ };
194
+ };
195
+ const defaultSessionMutationHooks = {
196
+ reload: async () => {
197
+ if (sessionMutationScheduled) {
198
+ return {
199
+ ok: true,
200
+ action: sessionMutationScheduled.action,
201
+ message: `session ${sessionMutationScheduled.action} already scheduled`,
202
+ details: { scheduled_at_ms: sessionMutationScheduled.at_ms },
203
+ };
204
+ }
205
+ const nowMs = Date.now();
206
+ const restartCommand = Bun.env.MU_RESTART_COMMAND?.trim();
207
+ const inferredArgs = process.argv[0] === process.execPath
208
+ ? [process.execPath, ...process.argv.slice(1)]
209
+ : [process.execPath, ...process.argv];
210
+ const restartShellCommand = restartCommand && restartCommand.length > 0 ? restartCommand : shellJoin(inferredArgs);
211
+ if (!restartShellCommand.trim()) {
212
+ return {
213
+ ok: false,
214
+ action: "reload",
215
+ message: "unable to determine restart command",
216
+ };
217
+ }
218
+ const exitDelayMs = 1_000;
219
+ const launchDelayMs = exitDelayMs + 300;
220
+ const delayedShellCommand = `sleep ${(launchDelayMs / 1_000).toFixed(2)}; ${restartShellCommand}`;
221
+ let spawnedPid = null;
222
+ try {
223
+ const proc = Bun.spawn({
224
+ cmd: ["bash", "-lc", delayedShellCommand],
225
+ cwd: repoRoot,
226
+ env: Bun.env,
227
+ stdin: "ignore",
228
+ stdout: "inherit",
229
+ stderr: "inherit",
230
+ });
231
+ spawnedPid = proc.pid ?? null;
232
+ }
233
+ catch (err) {
234
+ return {
235
+ ok: false,
236
+ action: "reload",
237
+ message: `failed to spawn replacement process: ${describeError(err)}`,
238
+ };
239
+ }
240
+ sessionMutationScheduled = { action: "reload", at_ms: nowMs };
241
+ setTimeout(() => {
242
+ process.exit(0);
243
+ }, exitDelayMs);
244
+ return {
245
+ ok: true,
246
+ action: "reload",
247
+ message: "reload scheduled; restarting process",
248
+ details: {
249
+ restart_command: restartShellCommand,
250
+ restart_launch_command: delayedShellCommand,
251
+ spawned_pid: spawnedPid,
252
+ exit_delay_ms: exitDelayMs,
253
+ launch_delay_ms: launchDelayMs,
254
+ },
255
+ };
256
+ },
257
+ update: async () => {
258
+ if (sessionMutationScheduled) {
259
+ return {
260
+ ok: true,
261
+ action: sessionMutationScheduled.action,
262
+ message: `session ${sessionMutationScheduled.action} already scheduled`,
263
+ details: { scheduled_at_ms: sessionMutationScheduled.at_ms },
264
+ };
265
+ }
266
+ const updateCommand = Bun.env.MU_UPDATE_COMMAND?.trim() || "npm install -g @femtomc/mu@latest";
267
+ const result = await runShellCommand(updateCommand);
268
+ if (result.exitCode !== 0) {
269
+ return {
270
+ ok: false,
271
+ action: "update",
272
+ message: `update command failed (exit ${result.exitCode})`,
273
+ details: {
274
+ update_command: updateCommand,
275
+ stdout: result.stdout.slice(-4_000),
276
+ stderr: result.stderr.slice(-4_000),
277
+ },
278
+ };
279
+ }
280
+ const reloadResult = await defaultSessionMutationHooks.reload?.();
281
+ if (!reloadResult) {
282
+ return {
283
+ ok: false,
284
+ action: "update",
285
+ message: "reload hook unavailable after update",
286
+ };
287
+ }
288
+ if (!reloadResult.ok) {
289
+ return {
290
+ ok: false,
291
+ action: "update",
292
+ message: reloadResult.message,
293
+ details: {
294
+ update_command: updateCommand,
295
+ reload: reloadResult.details ?? null,
296
+ },
297
+ };
298
+ }
299
+ return {
300
+ ok: true,
301
+ action: "update",
302
+ message: "update applied; reload scheduled",
303
+ details: {
304
+ update_command: updateCommand,
305
+ reload: reloadResult.details ?? null,
306
+ update_stdout_tail: result.stdout.slice(-1_000),
307
+ },
308
+ };
309
+ },
310
+ };
311
+ const sessionMutationHooks = options.sessionMutationHooks ?? defaultSessionMutationHooks;
168
312
  const emitOperatorWake = async (opts) => {
169
313
  const dedupeKey = opts.dedupeKey.trim();
170
314
  if (!dedupeKey) {
@@ -214,6 +358,8 @@ export function createServer(options = {}) {
214
358
  heartbeatScheduler,
215
359
  generation,
216
360
  telemetry: generationTelemetry,
361
+ sessionMutationHooks,
362
+ terminalEnabled: true,
217
363
  });
218
364
  });
219
365
  const controlPlaneProxy = {
@@ -272,6 +418,13 @@ export function createServer(options = {}) {
272
418
  return null;
273
419
  return await handle.traceRun(opts);
274
420
  },
421
+ async submitTerminalCommand(opts) {
422
+ const handle = controlPlaneCurrent;
423
+ if (!handle?.submitTerminalCommand) {
424
+ throw new Error("control_plane_unavailable");
425
+ }
426
+ return await handle.submitTerminalCommand(opts);
427
+ },
275
428
  async stop() {
276
429
  const handle = controlPlaneCurrent;
277
430
  controlPlaneCurrent = null;
@@ -1102,6 +1255,100 @@ export function createServer(options = {}) {
1102
1255
  control_plane: controlPlane,
1103
1256
  }, { headers });
1104
1257
  }
1258
+ if (path === "/api/commands/submit") {
1259
+ if (request.method !== "POST") {
1260
+ return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
1261
+ }
1262
+ let body;
1263
+ try {
1264
+ body = (await request.json());
1265
+ }
1266
+ catch {
1267
+ return Response.json({ error: "invalid json body" }, { status: 400, headers });
1268
+ }
1269
+ const kind = typeof body.kind === "string" ? body.kind.trim() : "";
1270
+ if (!kind) {
1271
+ return Response.json({ error: "kind is required" }, { status: 400, headers });
1272
+ }
1273
+ let commandText;
1274
+ switch (kind) {
1275
+ case "run_start": {
1276
+ const prompt = typeof body.prompt === "string" ? body.prompt.trim() : "";
1277
+ if (!prompt) {
1278
+ return Response.json({ error: "prompt is required for run_start" }, { status: 400, headers });
1279
+ }
1280
+ const maxStepsSuffix = typeof body.max_steps === "number" && Number.isFinite(body.max_steps)
1281
+ ? ` --max-steps ${Math.max(1, Math.trunc(body.max_steps))}`
1282
+ : "";
1283
+ commandText = `mu! run start ${prompt}${maxStepsSuffix}`;
1284
+ break;
1285
+ }
1286
+ case "run_resume": {
1287
+ const rootId = typeof body.root_issue_id === "string" ? body.root_issue_id.trim() : "";
1288
+ const maxSteps = typeof body.max_steps === "number" && Number.isFinite(body.max_steps)
1289
+ ? ` ${Math.max(1, Math.trunc(body.max_steps))}`
1290
+ : "";
1291
+ commandText = `mu! run resume${rootId ? ` ${rootId}` : ""}${maxSteps}`;
1292
+ break;
1293
+ }
1294
+ case "run_interrupt": {
1295
+ const rootId = typeof body.root_issue_id === "string" ? body.root_issue_id.trim() : "";
1296
+ commandText = `mu! run interrupt${rootId ? ` ${rootId}` : ""}`;
1297
+ break;
1298
+ }
1299
+ case "reload":
1300
+ commandText = "/mu reload";
1301
+ break;
1302
+ case "update":
1303
+ commandText = "/mu update";
1304
+ break;
1305
+ case "status":
1306
+ commandText = "/mu status";
1307
+ break;
1308
+ case "issue_list":
1309
+ commandText = "/mu issue list";
1310
+ break;
1311
+ case "issue_get": {
1312
+ const issueId = typeof body.issue_id === "string" ? body.issue_id.trim() : "";
1313
+ commandText = `/mu issue get${issueId ? ` ${issueId}` : ""}`;
1314
+ break;
1315
+ }
1316
+ case "forum_read": {
1317
+ const topic = typeof body.topic === "string" ? body.topic.trim() : "";
1318
+ const limit = typeof body.limit === "number" && Number.isFinite(body.limit)
1319
+ ? ` ${Math.max(1, Math.trunc(body.limit))}`
1320
+ : "";
1321
+ commandText = `/mu forum read${topic ? ` ${topic}` : ""}${limit}`;
1322
+ break;
1323
+ }
1324
+ case "run_list":
1325
+ commandText = "/mu run list";
1326
+ break;
1327
+ case "run_status": {
1328
+ const rootId = typeof body.root_issue_id === "string" ? body.root_issue_id.trim() : "";
1329
+ commandText = `/mu run status${rootId ? ` ${rootId}` : ""}`;
1330
+ break;
1331
+ }
1332
+ case "ready":
1333
+ commandText = "/mu ready";
1334
+ break;
1335
+ default:
1336
+ return Response.json({ error: `unknown command kind: ${kind}` }, { status: 400, headers });
1337
+ }
1338
+ try {
1339
+ if (!controlPlaneProxy.submitTerminalCommand) {
1340
+ return Response.json({ error: "control plane not available" }, { status: 503, headers });
1341
+ }
1342
+ const result = await controlPlaneProxy.submitTerminalCommand({
1343
+ commandText,
1344
+ repoRoot: context.repoRoot,
1345
+ });
1346
+ return Response.json({ ok: true, result }, { headers });
1347
+ }
1348
+ catch (err) {
1349
+ return Response.json({ error: `command failed: ${describeError(err)}` }, { status: 500, headers });
1350
+ }
1351
+ }
1105
1352
  if (path === "/api/runs") {
1106
1353
  if (request.method !== "GET") {
1107
1354
  return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
@@ -2016,6 +2263,8 @@ export async function createServerAsync(options = {}) {
2016
2263
  generation_seq: 0,
2017
2264
  },
2018
2265
  telemetry: generationTelemetry,
2266
+ sessionMutationHooks: options.sessionMutationHooks,
2267
+ terminalEnabled: true,
2019
2268
  });
2020
2269
  const serverConfig = createServer({
2021
2270
  ...options,
package/package.json CHANGED
@@ -1,40 +1,40 @@
1
1
  {
2
- "name": "@femtomc/mu-server",
3
- "version": "26.2.65",
4
- "description": "HTTP API server for mu status, work items, messaging setup, and web UI.",
5
- "keywords": [
6
- "mu",
7
- "server",
8
- "api",
9
- "web",
10
- "automation"
11
- ],
12
- "type": "module",
13
- "main": "./dist/index.js",
14
- "types": "./dist/index.d.ts",
15
- "bin": {
16
- "mu-server": "./dist/cli.js"
17
- },
18
- "exports": {
19
- ".": {
20
- "types": "./dist/index.d.ts",
21
- "default": "./dist/index.js"
22
- }
23
- },
24
- "files": [
25
- "dist/**",
26
- "public/**"
27
- ],
28
- "scripts": {
29
- "build": "tsc -p tsconfig.build.json",
30
- "test": "bun test",
31
- "start": "bun run dist/cli.js"
32
- },
33
- "dependencies": {
34
- "@femtomc/mu-agent": "26.2.65",
35
- "@femtomc/mu-control-plane": "26.2.65",
36
- "@femtomc/mu-core": "26.2.65",
37
- "@femtomc/mu-forum": "26.2.65",
38
- "@femtomc/mu-issue": "26.2.65"
39
- }
2
+ "name": "@femtomc/mu-server",
3
+ "version": "26.2.67",
4
+ "description": "HTTP API server for mu status, work items, messaging setup, and web UI.",
5
+ "keywords": [
6
+ "mu",
7
+ "server",
8
+ "api",
9
+ "web",
10
+ "automation"
11
+ ],
12
+ "type": "module",
13
+ "main": "./dist/index.js",
14
+ "types": "./dist/index.d.ts",
15
+ "bin": {
16
+ "mu-server": "./dist/cli.js"
17
+ },
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "default": "./dist/index.js"
22
+ }
23
+ },
24
+ "files": [
25
+ "dist/**",
26
+ "public/**"
27
+ ],
28
+ "scripts": {
29
+ "build": "tsc -p tsconfig.build.json",
30
+ "test": "bun test",
31
+ "start": "bun run dist/cli.js"
32
+ },
33
+ "dependencies": {
34
+ "@femtomc/mu-agent": "26.2.67",
35
+ "@femtomc/mu-control-plane": "26.2.67",
36
+ "@femtomc/mu-core": "26.2.67",
37
+ "@femtomc/mu-forum": "26.2.67",
38
+ "@femtomc/mu-issue": "26.2.67"
39
+ }
40
40
  }