@femtomc/mu-server 26.2.39 → 26.2.41
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 +13 -1
- package/dist/control_plane.d.ts +17 -0
- package/dist/control_plane.js +189 -100
- package/package.json +13 -7
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
|
-
|
|
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/control_plane.d.ts
CHANGED
|
@@ -24,6 +24,23 @@ type DetectedAdapter = {
|
|
|
24
24
|
botUsername: string | null;
|
|
25
25
|
};
|
|
26
26
|
export declare function detectAdapters(config: ControlPlaneConfig): DetectedAdapter[];
|
|
27
|
+
export type TelegramSendMessagePayload = {
|
|
28
|
+
chat_id: string;
|
|
29
|
+
text: string;
|
|
30
|
+
parse_mode?: "Markdown";
|
|
31
|
+
disable_web_page_preview?: boolean;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Telegram supports a markdown dialect that uses single markers for emphasis.
|
|
35
|
+
* Normalize the most common LLM/GitHub-style markers (`**bold**`, `__italic__`, headings)
|
|
36
|
+
* while preserving fenced code blocks verbatim.
|
|
37
|
+
*/
|
|
38
|
+
export declare function renderTelegramMarkdown(text: string): string;
|
|
39
|
+
export declare function buildTelegramSendMessagePayload(opts: {
|
|
40
|
+
chatId: string;
|
|
41
|
+
text: string;
|
|
42
|
+
richFormatting: boolean;
|
|
43
|
+
}): TelegramSendMessagePayload;
|
|
27
44
|
export type BootstrapControlPlaneOpts = {
|
|
28
45
|
repoRoot: string;
|
|
29
46
|
config?: ControlPlaneConfig;
|
package/dist/control_plane.js
CHANGED
|
@@ -22,6 +22,56 @@ export function detectAdapters(config) {
|
|
|
22
22
|
}
|
|
23
23
|
return adapters;
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Telegram supports a markdown dialect that uses single markers for emphasis.
|
|
27
|
+
* Normalize the most common LLM/GitHub-style markers (`**bold**`, `__italic__`, headings)
|
|
28
|
+
* while preserving fenced code blocks verbatim.
|
|
29
|
+
*/
|
|
30
|
+
export function renderTelegramMarkdown(text) {
|
|
31
|
+
const normalized = text.replaceAll("\r\n", "\n");
|
|
32
|
+
const lines = normalized.split("\n");
|
|
33
|
+
const out = [];
|
|
34
|
+
let inFence = false;
|
|
35
|
+
for (const line of lines) {
|
|
36
|
+
const trimmed = line.trimStart();
|
|
37
|
+
if (trimmed.startsWith("```")) {
|
|
38
|
+
inFence = !inFence;
|
|
39
|
+
out.push(line);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (inFence) {
|
|
43
|
+
out.push(line);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
let next = line;
|
|
47
|
+
next = next.replace(/^#{1,6}\s+(.+)$/, "*$1*");
|
|
48
|
+
next = next.replace(/\*\*(.+?)\*\*/g, "*$1*");
|
|
49
|
+
next = next.replace(/__(.+?)__/g, "_$1_");
|
|
50
|
+
out.push(next);
|
|
51
|
+
}
|
|
52
|
+
return out.join("\n");
|
|
53
|
+
}
|
|
54
|
+
export function buildTelegramSendMessagePayload(opts) {
|
|
55
|
+
if (!opts.richFormatting) {
|
|
56
|
+
return {
|
|
57
|
+
chat_id: opts.chatId,
|
|
58
|
+
text: opts.text,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
chat_id: opts.chatId,
|
|
63
|
+
text: renderTelegramMarkdown(opts.text),
|
|
64
|
+
parse_mode: "Markdown",
|
|
65
|
+
disable_web_page_preview: true,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
async function postTelegramMessage(botToken, payload) {
|
|
69
|
+
return await fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, {
|
|
70
|
+
method: "POST",
|
|
71
|
+
headers: { "Content-Type": "application/json" },
|
|
72
|
+
body: JSON.stringify(payload),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
25
75
|
function buildMessagingOperatorRuntime(opts) {
|
|
26
76
|
if (!opts.config.operator.enabled) {
|
|
27
77
|
return null;
|
|
@@ -49,115 +99,154 @@ export async function bootstrapControlPlane(opts) {
|
|
|
49
99
|
}
|
|
50
100
|
const paths = getControlPlanePaths(opts.repoRoot);
|
|
51
101
|
const runtime = new ControlPlaneRuntime({ repoRoot: opts.repoRoot });
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
}),
|
|
102
|
+
let pipeline = null;
|
|
103
|
+
let drainInterval = null;
|
|
104
|
+
try {
|
|
105
|
+
await runtime.start();
|
|
106
|
+
const operator = opts.operatorRuntime !== undefined
|
|
107
|
+
? opts.operatorRuntime
|
|
108
|
+
: buildMessagingOperatorRuntime({
|
|
109
|
+
repoRoot: opts.repoRoot,
|
|
110
|
+
config: controlPlaneConfig,
|
|
111
|
+
backend: opts.operatorBackend,
|
|
120
112
|
});
|
|
121
|
-
|
|
122
|
-
|
|
113
|
+
pipeline = new ControlPlaneCommandPipeline({ runtime, operator });
|
|
114
|
+
await pipeline.start();
|
|
115
|
+
const outbox = new ControlPlaneOutbox(paths.outboxPath);
|
|
116
|
+
await outbox.load();
|
|
117
|
+
let telegramBotToken = null;
|
|
118
|
+
const adapterMap = new Map();
|
|
119
|
+
for (const d of detected) {
|
|
120
|
+
let adapter;
|
|
121
|
+
switch (d.name) {
|
|
122
|
+
case "slack":
|
|
123
|
+
adapter = new SlackControlPlaneAdapter({
|
|
124
|
+
pipeline,
|
|
125
|
+
outbox,
|
|
126
|
+
signingSecret: d.signingSecret,
|
|
127
|
+
});
|
|
128
|
+
break;
|
|
129
|
+
case "discord":
|
|
130
|
+
adapter = new DiscordControlPlaneAdapter({
|
|
131
|
+
pipeline,
|
|
132
|
+
outbox,
|
|
133
|
+
signingSecret: d.signingSecret,
|
|
134
|
+
});
|
|
135
|
+
break;
|
|
136
|
+
case "telegram":
|
|
137
|
+
adapter = new TelegramControlPlaneAdapter({
|
|
138
|
+
pipeline,
|
|
139
|
+
outbox,
|
|
140
|
+
webhookSecret: d.webhookSecret,
|
|
141
|
+
botUsername: d.botUsername ?? undefined,
|
|
142
|
+
});
|
|
143
|
+
if (d.botToken) {
|
|
144
|
+
telegramBotToken = d.botToken;
|
|
145
|
+
}
|
|
146
|
+
break;
|
|
123
147
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
148
|
+
const route = adapter.spec.route;
|
|
149
|
+
if (adapterMap.has(route)) {
|
|
150
|
+
throw new Error(`duplicate control-plane webhook route: ${route}`);
|
|
151
|
+
}
|
|
152
|
+
adapterMap.set(route, {
|
|
153
|
+
adapter,
|
|
154
|
+
info: {
|
|
155
|
+
name: adapter.spec.channel,
|
|
156
|
+
route,
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
const deliver = async (record) => {
|
|
161
|
+
const { envelope } = record;
|
|
162
|
+
if (envelope.channel === "telegram") {
|
|
163
|
+
if (!telegramBotToken) {
|
|
164
|
+
return { kind: "retry", error: "telegram bot token not configured in .mu/config.json" };
|
|
165
|
+
}
|
|
166
|
+
const richPayload = buildTelegramSendMessagePayload({
|
|
167
|
+
chatId: envelope.channel_conversation_id,
|
|
168
|
+
text: envelope.body,
|
|
169
|
+
richFormatting: true,
|
|
170
|
+
});
|
|
171
|
+
let res = await postTelegramMessage(telegramBotToken, richPayload);
|
|
172
|
+
// Fallback: if Telegram rejects markdown entities, retry as plain text.
|
|
173
|
+
if (!res.ok && res.status === 400 && richPayload.parse_mode) {
|
|
174
|
+
const plainPayload = buildTelegramSendMessagePayload({
|
|
175
|
+
chatId: envelope.channel_conversation_id,
|
|
176
|
+
text: envelope.body,
|
|
177
|
+
richFormatting: false,
|
|
178
|
+
});
|
|
179
|
+
res = await postTelegramMessage(telegramBotToken, plainPayload);
|
|
180
|
+
}
|
|
181
|
+
if (res.ok) {
|
|
182
|
+
return { kind: "delivered" };
|
|
183
|
+
}
|
|
184
|
+
const responseBody = await res.text().catch(() => "");
|
|
185
|
+
if (res.status === 429 || res.status >= 500) {
|
|
186
|
+
const retryAfter = res.headers.get("retry-after");
|
|
187
|
+
const retryDelayMs = retryAfter ? Number.parseInt(retryAfter, 10) * 1000 : undefined;
|
|
188
|
+
return {
|
|
189
|
+
kind: "retry",
|
|
190
|
+
error: `telegram sendMessage ${res.status}: ${responseBody}`,
|
|
191
|
+
retryDelayMs: retryDelayMs && Number.isFinite(retryDelayMs) ? retryDelayMs : undefined,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
127
194
|
return {
|
|
128
195
|
kind: "retry",
|
|
129
|
-
error: `telegram sendMessage ${res.status}: ${
|
|
130
|
-
retryDelayMs: retryDelayMs && Number.isFinite(retryDelayMs) ? retryDelayMs : undefined,
|
|
196
|
+
error: `telegram sendMessage ${res.status}: ${responseBody}`,
|
|
131
197
|
};
|
|
132
198
|
}
|
|
133
|
-
return
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
199
|
+
return undefined;
|
|
200
|
+
};
|
|
201
|
+
const dispatcher = new ControlPlaneOutboxDispatcher({ outbox, deliver });
|
|
202
|
+
drainInterval = setInterval(async () => {
|
|
203
|
+
try {
|
|
204
|
+
await dispatcher.drainDue();
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
// Swallow errors — the dispatcher already handles retries internally.
|
|
208
|
+
}
|
|
209
|
+
}, 2_000);
|
|
210
|
+
return {
|
|
211
|
+
activeAdapters: [...adapterMap.values()].map((v) => v.info),
|
|
212
|
+
async handleWebhook(path, req) {
|
|
213
|
+
const entry = adapterMap.get(path);
|
|
214
|
+
if (!entry)
|
|
215
|
+
return null;
|
|
216
|
+
const result = await entry.adapter.ingest(req);
|
|
217
|
+
return result.response;
|
|
218
|
+
},
|
|
219
|
+
async stop() {
|
|
220
|
+
if (drainInterval) {
|
|
221
|
+
clearInterval(drainInterval);
|
|
222
|
+
drainInterval = null;
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
await pipeline?.stop();
|
|
226
|
+
}
|
|
227
|
+
finally {
|
|
228
|
+
await runtime.stop();
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
if (drainInterval) {
|
|
235
|
+
clearInterval(drainInterval);
|
|
236
|
+
drainInterval = null;
|
|
137
237
|
}
|
|
138
|
-
return undefined;
|
|
139
|
-
};
|
|
140
|
-
const dispatcher = new ControlPlaneOutboxDispatcher({ outbox, deliver });
|
|
141
|
-
const drainInterval = setInterval(async () => {
|
|
142
238
|
try {
|
|
143
|
-
await
|
|
239
|
+
await pipeline?.stop();
|
|
144
240
|
}
|
|
145
241
|
catch {
|
|
146
|
-
//
|
|
242
|
+
// Best effort cleanup.
|
|
147
243
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
return result.response;
|
|
157
|
-
},
|
|
158
|
-
async stop() {
|
|
159
|
-
clearInterval(drainInterval);
|
|
160
|
-
await pipeline.stop();
|
|
161
|
-
},
|
|
162
|
-
};
|
|
244
|
+
try {
|
|
245
|
+
await runtime.stop();
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
// Best effort cleanup.
|
|
249
|
+
}
|
|
250
|
+
throw err;
|
|
251
|
+
}
|
|
163
252
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@femtomc/mu-server",
|
|
3
|
-
"version": "26.2.
|
|
3
|
+
"version": "26.2.41",
|
|
4
4
|
"description": "HTTP API server for mu status, work items, messaging setup, and web UI.",
|
|
5
|
-
"keywords": [
|
|
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.
|
|
29
|
-
"@femtomc/mu-control-plane": "26.2.
|
|
30
|
-
"@femtomc/mu-core": "26.2.
|
|
31
|
-
"@femtomc/mu-forum": "26.2.
|
|
32
|
-
"@femtomc/mu-issue": "26.2.
|
|
34
|
+
"@femtomc/mu-agent": "26.2.41",
|
|
35
|
+
"@femtomc/mu-control-plane": "26.2.41",
|
|
36
|
+
"@femtomc/mu-core": "26.2.41",
|
|
37
|
+
"@femtomc/mu-forum": "26.2.41",
|
|
38
|
+
"@femtomc/mu-issue": "26.2.41"
|
|
33
39
|
}
|
|
34
40
|
}
|