@femtomc/mu-server 26.2.38 → 26.2.40

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @femtomc/mu-server
2
2
 
3
- HTTP JSON API server for mu issue and forum stores. Provides the backend for the mu web UI and can also be used standalone for programmatic access.
3
+ HTTP API server for mu. Powers `mu serve`, the web UI, and programmatic status/control endpoints.
4
4
 
5
5
  ## Installation
6
6
 
@@ -115,11 +115,11 @@ Bun.serve(server);
115
115
 
116
116
  ### With Web UI (Recommended)
117
117
 
118
- The easiest way to run the server with the bundled web interface (and default terminal meta-agent chat):
118
+ The easiest way to run the server with the bundled web interface (and default terminal operator chat):
119
119
 
120
120
  ```bash
121
121
  # From any mu repository
122
- mu serve # API + web UI + terminal meta-agent session
122
+ mu serve # API + web UI + terminal operator session
123
123
  mu serve --no-open # Skip browser auto-open (headless/SSH)
124
124
  mu serve --port 8080 # Custom shared API/web UI port
125
125
  ```
package/dist/cli.js CHANGED
@@ -13,7 +13,19 @@ catch {
13
13
  console.log(`Starting mu-server on port ${port}...`);
14
14
  console.log(`Repository root: ${repoRoot}`);
15
15
  const { serverConfig, controlPlane } = await createServerAsync({ repoRoot, port });
16
- const server = Bun.serve(serverConfig);
16
+ let server;
17
+ try {
18
+ server = Bun.serve(serverConfig);
19
+ }
20
+ catch (err) {
21
+ try {
22
+ await controlPlane?.stop();
23
+ }
24
+ catch {
25
+ // Best effort cleanup. Preserve the startup error.
26
+ }
27
+ throw err;
28
+ }
17
29
  console.log(`Server running at http://localhost:${port}`);
18
30
  if (controlPlane && controlPlane.activeAdapters.length > 0) {
19
31
  console.log("Control plane: active");
package/dist/config.d.ts CHANGED
@@ -21,7 +21,7 @@ export type MuConfig = {
21
21
  refresh_token: string | null;
22
22
  };
23
23
  };
24
- meta_agent: {
24
+ operator: {
25
25
  enabled: boolean;
26
26
  run_triggers_enabled: boolean;
27
27
  provider: string | null;
@@ -51,7 +51,7 @@ export type MuConfigPatch = {
51
51
  refresh_token?: string | null;
52
52
  };
53
53
  };
54
- meta_agent?: {
54
+ operator?: {
55
55
  enabled?: boolean;
56
56
  run_triggers_enabled?: boolean;
57
57
  provider?: string | null;
@@ -81,7 +81,7 @@ export type MuConfigPresence = {
81
81
  refresh_token: boolean;
82
82
  };
83
83
  };
84
- meta_agent: {
84
+ operator: {
85
85
  enabled: boolean;
86
86
  run_triggers_enabled: boolean;
87
87
  provider: boolean;
package/dist/config.js CHANGED
@@ -23,7 +23,7 @@ export const DEFAULT_MU_CONFIG = {
23
23
  refresh_token: null,
24
24
  },
25
25
  },
26
- meta_agent: {
26
+ operator: {
27
27
  enabled: true,
28
28
  run_triggers_enabled: true,
29
29
  provider: null,
@@ -109,19 +109,19 @@ export function normalizeMuConfig(input) {
109
109
  }
110
110
  }
111
111
  }
112
- const metaAgent = asRecord(controlPlane.meta_agent);
113
- if (metaAgent) {
114
- if ("enabled" in metaAgent) {
115
- next.control_plane.meta_agent.enabled = normalizeBoolean(metaAgent.enabled, next.control_plane.meta_agent.enabled);
112
+ const operator = asRecord(controlPlane.operator);
113
+ if (operator) {
114
+ if ("enabled" in operator) {
115
+ next.control_plane.operator.enabled = normalizeBoolean(operator.enabled, next.control_plane.operator.enabled);
116
116
  }
117
- if ("run_triggers_enabled" in metaAgent) {
118
- next.control_plane.meta_agent.run_triggers_enabled = normalizeBoolean(metaAgent.run_triggers_enabled, next.control_plane.meta_agent.run_triggers_enabled);
117
+ if ("run_triggers_enabled" in operator) {
118
+ next.control_plane.operator.run_triggers_enabled = normalizeBoolean(operator.run_triggers_enabled, next.control_plane.operator.run_triggers_enabled);
119
119
  }
120
- if ("provider" in metaAgent) {
121
- next.control_plane.meta_agent.provider = normalizeNullableString(metaAgent.provider);
120
+ if ("provider" in operator) {
121
+ next.control_plane.operator.provider = normalizeNullableString(operator.provider);
122
122
  }
123
- if ("model" in metaAgent) {
124
- next.control_plane.meta_agent.model = normalizeNullableString(metaAgent.model);
123
+ if ("model" in operator) {
124
+ next.control_plane.operator.model = normalizeNullableString(operator.model);
125
125
  }
126
126
  }
127
127
  return next;
@@ -189,23 +189,23 @@ function normalizeMuConfigPatch(input) {
189
189
  }
190
190
  }
191
191
  }
192
- const metaAgent = asRecord(controlPlane.meta_agent);
193
- if (metaAgent) {
194
- patch.control_plane.meta_agent = {};
195
- if ("enabled" in metaAgent) {
196
- patch.control_plane.meta_agent.enabled = normalizeBoolean(metaAgent.enabled, DEFAULT_MU_CONFIG.control_plane.meta_agent.enabled);
192
+ const operator = asRecord(controlPlane.operator);
193
+ if (operator) {
194
+ patch.control_plane.operator = {};
195
+ if ("enabled" in operator) {
196
+ patch.control_plane.operator.enabled = normalizeBoolean(operator.enabled, DEFAULT_MU_CONFIG.control_plane.operator.enabled);
197
197
  }
198
- if ("run_triggers_enabled" in metaAgent) {
199
- patch.control_plane.meta_agent.run_triggers_enabled = normalizeBoolean(metaAgent.run_triggers_enabled, DEFAULT_MU_CONFIG.control_plane.meta_agent.run_triggers_enabled);
198
+ if ("run_triggers_enabled" in operator) {
199
+ patch.control_plane.operator.run_triggers_enabled = normalizeBoolean(operator.run_triggers_enabled, DEFAULT_MU_CONFIG.control_plane.operator.run_triggers_enabled);
200
200
  }
201
- if ("provider" in metaAgent) {
202
- patch.control_plane.meta_agent.provider = normalizeNullableString(metaAgent.provider);
201
+ if ("provider" in operator) {
202
+ patch.control_plane.operator.provider = normalizeNullableString(operator.provider);
203
203
  }
204
- if ("model" in metaAgent) {
205
- patch.control_plane.meta_agent.model = normalizeNullableString(metaAgent.model);
204
+ if ("model" in operator) {
205
+ patch.control_plane.operator.model = normalizeNullableString(operator.model);
206
206
  }
207
- if (Object.keys(patch.control_plane.meta_agent).length === 0) {
208
- delete patch.control_plane.meta_agent;
207
+ if (Object.keys(patch.control_plane.operator).length === 0) {
208
+ delete patch.control_plane.operator;
209
209
  }
210
210
  }
211
211
  if (patch.control_plane.adapters && Object.keys(patch.control_plane.adapters).length === 0) {
@@ -259,19 +259,19 @@ export function applyMuConfigPatch(base, patchInput) {
259
259
  }
260
260
  }
261
261
  }
262
- const metaAgent = patch.control_plane.meta_agent;
263
- if (metaAgent) {
264
- if ("enabled" in metaAgent && typeof metaAgent.enabled === "boolean") {
265
- next.control_plane.meta_agent.enabled = metaAgent.enabled;
262
+ const operator = patch.control_plane.operator;
263
+ if (operator) {
264
+ if ("enabled" in operator && typeof operator.enabled === "boolean") {
265
+ next.control_plane.operator.enabled = operator.enabled;
266
266
  }
267
- if ("run_triggers_enabled" in metaAgent && typeof metaAgent.run_triggers_enabled === "boolean") {
268
- next.control_plane.meta_agent.run_triggers_enabled = metaAgent.run_triggers_enabled;
267
+ if ("run_triggers_enabled" in operator && typeof operator.run_triggers_enabled === "boolean") {
268
+ next.control_plane.operator.run_triggers_enabled = operator.run_triggers_enabled;
269
269
  }
270
- if ("provider" in metaAgent) {
271
- next.control_plane.meta_agent.provider = metaAgent.provider ?? null;
270
+ if ("provider" in operator) {
271
+ next.control_plane.operator.provider = operator.provider ?? null;
272
272
  }
273
- if ("model" in metaAgent) {
274
- next.control_plane.meta_agent.model = metaAgent.model ?? null;
273
+ if ("model" in operator) {
274
+ next.control_plane.operator.model = operator.model ?? null;
275
275
  }
276
276
  }
277
277
  return next;
@@ -349,11 +349,11 @@ export function muConfigPresence(config) {
349
349
  refresh_token: isPresent(config.control_plane.adapters.gmail.refresh_token),
350
350
  },
351
351
  },
352
- meta_agent: {
353
- enabled: config.control_plane.meta_agent.enabled,
354
- run_triggers_enabled: config.control_plane.meta_agent.run_triggers_enabled,
355
- provider: isPresent(config.control_plane.meta_agent.provider),
356
- model: isPresent(config.control_plane.meta_agent.model),
352
+ operator: {
353
+ enabled: config.control_plane.operator.enabled,
354
+ run_triggers_enabled: config.control_plane.operator.run_triggers_enabled,
355
+ provider: isPresent(config.control_plane.operator.provider),
356
+ model: isPresent(config.control_plane.operator.model),
357
357
  },
358
358
  },
359
359
  };
@@ -1,4 +1,4 @@
1
- import { type MessagingMetaAgentBackend, MessagingMetaAgentRuntime } from "@femtomc/mu-agent";
1
+ import { type MessagingOperatorBackend, MessagingOperatorRuntime } from "@femtomc/mu-agent";
2
2
  import { type Channel } from "@femtomc/mu-control-plane";
3
3
  import { type MuConfig } from "./config.js";
4
4
  export type ActiveAdapter = {
@@ -27,8 +27,8 @@ export declare function detectAdapters(config: ControlPlaneConfig): DetectedAdap
27
27
  export type BootstrapControlPlaneOpts = {
28
28
  repoRoot: string;
29
29
  config?: ControlPlaneConfig;
30
- metaAgentRuntime?: MessagingMetaAgentRuntime | null;
31
- metaAgentBackend?: MessagingMetaAgentBackend;
30
+ operatorRuntime?: MessagingOperatorRuntime | null;
31
+ operatorBackend?: MessagingOperatorBackend;
32
32
  };
33
33
  export declare function bootstrapControlPlane(opts: BootstrapControlPlaneOpts): Promise<ControlPlaneHandle | null>;
34
34
  export {};
@@ -1,4 +1,4 @@
1
- import { ApprovedCommandBroker, CommandContextResolver, MessagingMetaAgentRuntime, PiMessagingMetaAgentBackend, serveExtensionPaths, } from "@femtomc/mu-agent";
1
+ import { ApprovedCommandBroker, CommandContextResolver, MessagingOperatorRuntime, PiMessagingOperatorBackend, serveExtensionPaths, } from "@femtomc/mu-agent";
2
2
  import { ControlPlaneCommandPipeline, ControlPlaneOutbox, ControlPlaneOutboxDispatcher, ControlPlaneRuntime, DiscordControlPlaneAdapter, getControlPlanePaths, SlackControlPlaneAdapter, TelegramControlPlaneAdapter, } from "@femtomc/mu-control-plane";
3
3
  import { DEFAULT_MU_CONFIG } from "./config.js";
4
4
  export function detectAdapters(config) {
@@ -22,20 +22,20 @@ export function detectAdapters(config) {
22
22
  }
23
23
  return adapters;
24
24
  }
25
- function buildMessagingMetaAgentRuntime(opts) {
26
- if (!opts.config.meta_agent.enabled) {
25
+ function buildMessagingOperatorRuntime(opts) {
26
+ if (!opts.config.operator.enabled) {
27
27
  return null;
28
28
  }
29
29
  const backend = opts.backend ??
30
- new PiMessagingMetaAgentBackend({
31
- provider: opts.config.meta_agent.provider ?? undefined,
32
- model: opts.config.meta_agent.model ?? undefined,
30
+ new PiMessagingOperatorBackend({
31
+ provider: opts.config.operator.provider ?? undefined,
32
+ model: opts.config.operator.model ?? undefined,
33
33
  extensionPaths: serveExtensionPaths,
34
34
  });
35
- return new MessagingMetaAgentRuntime({
35
+ return new MessagingOperatorRuntime({
36
36
  backend,
37
37
  broker: new ApprovedCommandBroker({
38
- runTriggersEnabled: opts.config.meta_agent.run_triggers_enabled,
38
+ runTriggersEnabled: opts.config.operator.run_triggers_enabled,
39
39
  contextResolver: new CommandContextResolver({ allowedRepoRoots: [opts.repoRoot] }),
40
40
  }),
41
41
  enabled: true,
@@ -49,115 +49,146 @@ export async function bootstrapControlPlane(opts) {
49
49
  }
50
50
  const paths = getControlPlanePaths(opts.repoRoot);
51
51
  const runtime = new ControlPlaneRuntime({ repoRoot: opts.repoRoot });
52
- await runtime.start();
53
- const metaAgent = opts.metaAgentRuntime !== undefined
54
- ? opts.metaAgentRuntime
55
- : buildMessagingMetaAgentRuntime({
56
- repoRoot: opts.repoRoot,
57
- config: controlPlaneConfig,
58
- backend: opts.metaAgentBackend,
59
- });
60
- const pipeline = new ControlPlaneCommandPipeline({ runtime, metaAgent });
61
- await pipeline.start();
62
- const outbox = new ControlPlaneOutbox(paths.outboxPath);
63
- await outbox.load();
64
- let telegramBotToken = null;
65
- const adapterMap = new Map();
66
- for (const d of detected) {
67
- let adapter;
68
- switch (d.name) {
69
- case "slack":
70
- adapter = new SlackControlPlaneAdapter({
71
- pipeline,
72
- outbox,
73
- signingSecret: d.signingSecret,
74
- });
75
- break;
76
- case "discord":
77
- adapter = new DiscordControlPlaneAdapter({
78
- pipeline,
79
- outbox,
80
- signingSecret: d.signingSecret,
81
- });
82
- break;
83
- case "telegram":
84
- adapter = new TelegramControlPlaneAdapter({
85
- pipeline,
86
- outbox,
87
- webhookSecret: d.webhookSecret,
88
- botUsername: d.botUsername ?? undefined,
89
- });
90
- if (d.botToken) {
91
- telegramBotToken = d.botToken;
92
- }
93
- break;
94
- }
95
- const route = adapter.spec.route;
96
- if (adapterMap.has(route)) {
97
- throw new Error(`duplicate control-plane webhook route: ${route}`);
98
- }
99
- adapterMap.set(route, {
100
- adapter,
101
- info: {
102
- name: adapter.spec.channel,
103
- route,
104
- },
105
- });
106
- }
107
- const deliver = async (record) => {
108
- const { envelope } = record;
109
- if (envelope.channel === "telegram") {
110
- if (!telegramBotToken) {
111
- return { kind: "retry", error: "telegram bot token not configured in .mu/config.json" };
112
- }
113
- const res = await fetch(`https://api.telegram.org/bot${telegramBotToken}/sendMessage`, {
114
- method: "POST",
115
- headers: { "Content-Type": "application/json" },
116
- body: JSON.stringify({
117
- chat_id: envelope.channel_conversation_id,
118
- text: envelope.body,
119
- }),
52
+ let pipeline = null;
53
+ let drainInterval = null;
54
+ try {
55
+ await runtime.start();
56
+ const operator = opts.operatorRuntime !== undefined
57
+ ? opts.operatorRuntime
58
+ : buildMessagingOperatorRuntime({
59
+ repoRoot: opts.repoRoot,
60
+ config: controlPlaneConfig,
61
+ backend: opts.operatorBackend,
120
62
  });
121
- if (res.ok) {
122
- return { kind: "delivered" };
63
+ pipeline = new ControlPlaneCommandPipeline({ runtime, operator });
64
+ await pipeline.start();
65
+ const outbox = new ControlPlaneOutbox(paths.outboxPath);
66
+ await outbox.load();
67
+ let telegramBotToken = null;
68
+ const adapterMap = new Map();
69
+ for (const d of detected) {
70
+ let adapter;
71
+ switch (d.name) {
72
+ case "slack":
73
+ adapter = new SlackControlPlaneAdapter({
74
+ pipeline,
75
+ outbox,
76
+ signingSecret: d.signingSecret,
77
+ });
78
+ break;
79
+ case "discord":
80
+ adapter = new DiscordControlPlaneAdapter({
81
+ pipeline,
82
+ outbox,
83
+ signingSecret: d.signingSecret,
84
+ });
85
+ break;
86
+ case "telegram":
87
+ adapter = new TelegramControlPlaneAdapter({
88
+ pipeline,
89
+ outbox,
90
+ webhookSecret: d.webhookSecret,
91
+ botUsername: d.botUsername ?? undefined,
92
+ });
93
+ if (d.botToken) {
94
+ telegramBotToken = d.botToken;
95
+ }
96
+ break;
123
97
  }
124
- if (res.status === 429 || res.status >= 500) {
125
- const retryAfter = res.headers.get("retry-after");
126
- const retryDelayMs = retryAfter ? Number.parseInt(retryAfter, 10) * 1000 : undefined;
98
+ const route = adapter.spec.route;
99
+ if (adapterMap.has(route)) {
100
+ throw new Error(`duplicate control-plane webhook route: ${route}`);
101
+ }
102
+ adapterMap.set(route, {
103
+ adapter,
104
+ info: {
105
+ name: adapter.spec.channel,
106
+ route,
107
+ },
108
+ });
109
+ }
110
+ const deliver = async (record) => {
111
+ const { envelope } = record;
112
+ if (envelope.channel === "telegram") {
113
+ if (!telegramBotToken) {
114
+ return { kind: "retry", error: "telegram bot token not configured in .mu/config.json" };
115
+ }
116
+ const res = await fetch(`https://api.telegram.org/bot${telegramBotToken}/sendMessage`, {
117
+ method: "POST",
118
+ headers: { "Content-Type": "application/json" },
119
+ body: JSON.stringify({
120
+ chat_id: envelope.channel_conversation_id,
121
+ text: envelope.body,
122
+ }),
123
+ });
124
+ if (res.ok) {
125
+ return { kind: "delivered" };
126
+ }
127
+ if (res.status === 429 || res.status >= 500) {
128
+ const retryAfter = res.headers.get("retry-after");
129
+ const retryDelayMs = retryAfter ? Number.parseInt(retryAfter, 10) * 1000 : undefined;
130
+ return {
131
+ kind: "retry",
132
+ error: `telegram sendMessage ${res.status}: ${await res.text().catch(() => "")}`,
133
+ retryDelayMs: retryDelayMs && Number.isFinite(retryDelayMs) ? retryDelayMs : undefined,
134
+ };
135
+ }
127
136
  return {
128
137
  kind: "retry",
129
138
  error: `telegram sendMessage ${res.status}: ${await res.text().catch(() => "")}`,
130
- retryDelayMs: retryDelayMs && Number.isFinite(retryDelayMs) ? retryDelayMs : undefined,
131
139
  };
132
140
  }
133
- return {
134
- kind: "retry",
135
- error: `telegram sendMessage ${res.status}: ${await res.text().catch(() => "")}`,
136
- };
141
+ return undefined;
142
+ };
143
+ const dispatcher = new ControlPlaneOutboxDispatcher({ outbox, deliver });
144
+ drainInterval = setInterval(async () => {
145
+ try {
146
+ await dispatcher.drainDue();
147
+ }
148
+ catch {
149
+ // Swallow errors — the dispatcher already handles retries internally.
150
+ }
151
+ }, 2_000);
152
+ return {
153
+ activeAdapters: [...adapterMap.values()].map((v) => v.info),
154
+ async handleWebhook(path, req) {
155
+ const entry = adapterMap.get(path);
156
+ if (!entry)
157
+ return null;
158
+ const result = await entry.adapter.ingest(req);
159
+ return result.response;
160
+ },
161
+ async stop() {
162
+ if (drainInterval) {
163
+ clearInterval(drainInterval);
164
+ drainInterval = null;
165
+ }
166
+ try {
167
+ await pipeline?.stop();
168
+ }
169
+ finally {
170
+ await runtime.stop();
171
+ }
172
+ },
173
+ };
174
+ }
175
+ catch (err) {
176
+ if (drainInterval) {
177
+ clearInterval(drainInterval);
178
+ drainInterval = null;
137
179
  }
138
- return undefined;
139
- };
140
- const dispatcher = new ControlPlaneOutboxDispatcher({ outbox, deliver });
141
- const drainInterval = setInterval(async () => {
142
180
  try {
143
- await dispatcher.drainDue();
181
+ await pipeline?.stop();
144
182
  }
145
183
  catch {
146
- // Swallow errors — the dispatcher already handles retries internally.
184
+ // Best effort cleanup.
147
185
  }
148
- }, 2_000);
149
- return {
150
- activeAdapters: [...adapterMap.values()].map((v) => v.info),
151
- async handleWebhook(path, req) {
152
- const entry = adapterMap.get(path);
153
- if (!entry)
154
- return null;
155
- const result = await entry.adapter.ingest(req);
156
- return result.response;
157
- },
158
- async stop() {
159
- clearInterval(drainInterval);
160
- await pipeline.stop();
161
- },
162
- };
186
+ try {
187
+ await runtime.stop();
188
+ }
189
+ catch {
190
+ // Best effort cleanup.
191
+ }
192
+ throw err;
193
+ }
163
194
  }
package/package.json CHANGED
@@ -1,6 +1,14 @@
1
1
  {
2
2
  "name": "@femtomc/mu-server",
3
- "version": "26.2.38",
3
+ "version": "26.2.40",
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
+ ],
4
12
  "type": "module",
5
13
  "main": "./dist/index.js",
6
14
  "types": "./dist/index.d.ts",
@@ -23,10 +31,10 @@
23
31
  "start": "bun run dist/cli.js"
24
32
  },
25
33
  "dependencies": {
26
- "@femtomc/mu-agent": "26.2.38",
27
- "@femtomc/mu-control-plane": "26.2.38",
28
- "@femtomc/mu-core": "26.2.38",
29
- "@femtomc/mu-forum": "26.2.38",
30
- "@femtomc/mu-issue": "26.2.38"
34
+ "@femtomc/mu-agent": "26.2.40",
35
+ "@femtomc/mu-control-plane": "26.2.40",
36
+ "@femtomc/mu-core": "26.2.40",
37
+ "@femtomc/mu-forum": "26.2.40",
38
+ "@femtomc/mu-issue": "26.2.40"
31
39
  }
32
40
  }