@femtomc/mu-server 26.2.39 → 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/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");
@@ -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 operator = opts.operatorRuntime !== undefined
54
- ? opts.operatorRuntime
55
- : buildMessagingOperatorRuntime({
56
- repoRoot: opts.repoRoot,
57
- config: controlPlaneConfig,
58
- backend: opts.operatorBackend,
59
- });
60
- const pipeline = new ControlPlaneCommandPipeline({ runtime, operator });
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,8 +1,14 @@
1
1
  {
2
2
  "name": "@femtomc/mu-server",
3
- "version": "26.2.39",
3
+ "version": "26.2.40",
4
4
  "description": "HTTP API server for mu status, work items, messaging setup, and web UI.",
5
- "keywords": ["mu", "server", "api", "web", "automation"],
5
+ "keywords": [
6
+ "mu",
7
+ "server",
8
+ "api",
9
+ "web",
10
+ "automation"
11
+ ],
6
12
  "type": "module",
7
13
  "main": "./dist/index.js",
8
14
  "types": "./dist/index.d.ts",
@@ -25,10 +31,10 @@
25
31
  "start": "bun run dist/cli.js"
26
32
  },
27
33
  "dependencies": {
28
- "@femtomc/mu-agent": "26.2.39",
29
- "@femtomc/mu-control-plane": "26.2.39",
30
- "@femtomc/mu-core": "26.2.39",
31
- "@femtomc/mu-forum": "26.2.39",
32
- "@femtomc/mu-issue": "26.2.39"
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"
33
39
  }
34
40
  }