@femtomc/mu-server 26.2.100 → 26.2.102
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 +6 -0
- package/dist/control_plane.d.ts +2 -0
- package/dist/control_plane.js +239 -7
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -185,6 +185,12 @@ Operational fallbacks:
|
|
|
185
185
|
- `telegram_reply_to_message_id` metadata anchors replies when parseable.
|
|
186
186
|
- Missing Slack/Telegram bot tokens surface capability reason codes (`*_bot_token_missing`) and retry behavior.
|
|
187
187
|
|
|
188
|
+
Server channel renderers consume canonical `hud_docs` metadata (`HudDoc`) for Slack/Telegram HUD
|
|
189
|
+
rendering + actions. Optional HUD presentation hints (`title_style`, `snapshot_style`, chip/item styles)
|
|
190
|
+
and metadata presets (`metadata.style_preset`) may be used by richer renderers and safely ignored by
|
|
191
|
+
plain-text channels. New features should extend the shared HUD contract path instead of bespoke
|
|
192
|
+
channel-specific HUD payload formats.
|
|
193
|
+
|
|
188
194
|
## Running the Server
|
|
189
195
|
|
|
190
196
|
### With terminal operator session (recommended)
|
package/dist/control_plane.d.ts
CHANGED
|
@@ -53,9 +53,11 @@ export declare function buildTelegramSendMessagePayload(opts: {
|
|
|
53
53
|
}): TelegramSendMessagePayload;
|
|
54
54
|
export declare function splitTelegramMessageText(text: string, maxLen?: number): string[];
|
|
55
55
|
export declare function splitSlackMessageText(text: string, maxLen?: number): string[];
|
|
56
|
+
type TelegramCallbackDataEncoder = (commandText: string) => Promise<string>;
|
|
56
57
|
export declare function deliverTelegramOutboxRecord(opts: {
|
|
57
58
|
botToken: string;
|
|
58
59
|
record: OutboxRecord;
|
|
60
|
+
encodeCallbackData?: TelegramCallbackDataEncoder;
|
|
59
61
|
}): Promise<OutboxDeliveryHandlerResult>;
|
|
60
62
|
export declare function deliverSlackOutboxRecord(opts: {
|
|
61
63
|
botToken: string;
|
package/dist/control_plane.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { applyHudStylePreset, normalizeHudDocs } from "@femtomc/mu-core";
|
|
2
|
+
import { ControlPlaneCommandPipeline, ControlPlaneOutbox, ControlPlaneRuntime, buildSlackHudActionId, getControlPlanePaths, TelegramControlPlaneAdapterSpec, } from "@femtomc/mu-control-plane";
|
|
2
3
|
import { DEFAULT_MU_CONFIG } from "./config.js";
|
|
3
4
|
import { buildMessagingOperatorRuntime, createOutboxDrainLoop } from "./control_plane_bootstrap_helpers.js";
|
|
4
5
|
import { buildWakeOutboundEnvelope, resolveWakeFanoutCapability, wakeDeliveryMetadataFromOutboxRecord, wakeDispatchReasonCode, wakeFanoutDedupeKey, } from "./control_plane_wake_delivery.js";
|
|
@@ -141,6 +142,132 @@ export function splitTelegramMessageText(text, maxLen = TELEGRAM_MESSAGE_MAX_LEN
|
|
|
141
142
|
return chunks;
|
|
142
143
|
}
|
|
143
144
|
const SLACK_MESSAGE_MAX_LEN = 3_500;
|
|
145
|
+
const SLACK_BLOCK_TEXT_MAX_LEN = 3_000;
|
|
146
|
+
const SLACK_BLOCKS_MAX = 50;
|
|
147
|
+
const SLACK_ACTIONS_MAX_PER_BLOCK = 5;
|
|
148
|
+
const SLACK_ACTIONS_MAX_TOTAL = 20;
|
|
149
|
+
const SLACK_DOCS_MAX = 3;
|
|
150
|
+
const SLACK_SECTION_LINE_MAX = 8;
|
|
151
|
+
function hudDocsForPresentation(input, maxDocs) {
|
|
152
|
+
return normalizeHudDocs(input, { maxDocs }).map((doc) => applyHudStylePreset(doc) ?? doc);
|
|
153
|
+
}
|
|
154
|
+
function truncateSlackText(text, maxLen = SLACK_BLOCK_TEXT_MAX_LEN) {
|
|
155
|
+
if (text.length <= maxLen) {
|
|
156
|
+
return text;
|
|
157
|
+
}
|
|
158
|
+
if (maxLen <= 1) {
|
|
159
|
+
return text.slice(0, maxLen);
|
|
160
|
+
}
|
|
161
|
+
return `${text.slice(0, maxLen - 1)}…`;
|
|
162
|
+
}
|
|
163
|
+
function hudTonePrefix(tone) {
|
|
164
|
+
switch (tone) {
|
|
165
|
+
case "success":
|
|
166
|
+
return "✅";
|
|
167
|
+
case "warning":
|
|
168
|
+
return "⚠️";
|
|
169
|
+
case "error":
|
|
170
|
+
return "⛔";
|
|
171
|
+
case "accent":
|
|
172
|
+
return "🔹";
|
|
173
|
+
case "muted":
|
|
174
|
+
return "▫️";
|
|
175
|
+
case "dim":
|
|
176
|
+
return "·";
|
|
177
|
+
case "info":
|
|
178
|
+
default:
|
|
179
|
+
return "ℹ️";
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function applySlackHudTextStyle(text, style, opts = {}) {
|
|
183
|
+
const weight = style?.weight ?? opts.defaultWeight;
|
|
184
|
+
const italic = style?.italic ?? opts.defaultItalic ?? false;
|
|
185
|
+
const code = style?.code ?? opts.defaultCode ?? false;
|
|
186
|
+
let out = text;
|
|
187
|
+
if (code) {
|
|
188
|
+
out = `\`${out}\``;
|
|
189
|
+
}
|
|
190
|
+
if (italic) {
|
|
191
|
+
out = `_${out}_`;
|
|
192
|
+
}
|
|
193
|
+
if (weight === "strong") {
|
|
194
|
+
out = `*${out}*`;
|
|
195
|
+
}
|
|
196
|
+
return out;
|
|
197
|
+
}
|
|
198
|
+
function stripSlackMrkdwn(text) {
|
|
199
|
+
return text.replace(/[*_`~]/g, "");
|
|
200
|
+
}
|
|
201
|
+
function hudDocSectionLines(doc) {
|
|
202
|
+
const lines = [];
|
|
203
|
+
const chipsLine = doc.chips
|
|
204
|
+
.slice(0, 8)
|
|
205
|
+
.map((chip) => {
|
|
206
|
+
const label = applySlackHudTextStyle(chip.label, chip.style, { defaultWeight: "strong" });
|
|
207
|
+
return `${hudTonePrefix(chip.tone)} ${label}`;
|
|
208
|
+
})
|
|
209
|
+
.join(" · ");
|
|
210
|
+
if (chipsLine.length > 0) {
|
|
211
|
+
lines.push(chipsLine);
|
|
212
|
+
}
|
|
213
|
+
for (const section of doc.sections) {
|
|
214
|
+
switch (section.kind) {
|
|
215
|
+
case "kv": {
|
|
216
|
+
const title = applySlackHudTextStyle(section.title ?? "Details", section.title_style, { defaultWeight: "strong" });
|
|
217
|
+
const items = section.items.slice(0, SLACK_SECTION_LINE_MAX).map((item) => {
|
|
218
|
+
const value = applySlackHudTextStyle(item.value, item.value_style);
|
|
219
|
+
return `• *${item.label}:* ${value}`;
|
|
220
|
+
});
|
|
221
|
+
if (items.length > 0) {
|
|
222
|
+
lines.push([title, ...items].join("\n"));
|
|
223
|
+
}
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
case "checklist": {
|
|
227
|
+
const title = applySlackHudTextStyle(section.title ?? "Checklist", section.title_style, { defaultWeight: "strong" });
|
|
228
|
+
const items = section.items
|
|
229
|
+
.slice(0, SLACK_SECTION_LINE_MAX)
|
|
230
|
+
.map((item) => `${item.done ? "✅" : "⬜"} ${applySlackHudTextStyle(item.label, item.style)}`);
|
|
231
|
+
if (items.length > 0) {
|
|
232
|
+
lines.push([title, ...items].join("\n"));
|
|
233
|
+
}
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
case "activity": {
|
|
237
|
+
const title = applySlackHudTextStyle(section.title ?? "Activity", section.title_style, { defaultWeight: "strong" });
|
|
238
|
+
const items = section.lines.slice(0, SLACK_SECTION_LINE_MAX).map((line) => `• ${line}`);
|
|
239
|
+
if (items.length > 0) {
|
|
240
|
+
lines.push([title, ...items].join("\n"));
|
|
241
|
+
}
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
case "text": {
|
|
245
|
+
const title = section.title
|
|
246
|
+
? `${applySlackHudTextStyle(section.title, section.title_style, { defaultWeight: "strong" })}\n`
|
|
247
|
+
: "";
|
|
248
|
+
const text = applySlackHudTextStyle(section.text, section.style);
|
|
249
|
+
lines.push(`${title}${text}`);
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (lines.length === 0) {
|
|
255
|
+
const snapshot = applySlackHudTextStyle(doc.snapshot_compact, doc.snapshot_style);
|
|
256
|
+
lines.push(`${applySlackHudTextStyle("Snapshot", undefined, { defaultWeight: "strong" })}\n${snapshot}`);
|
|
257
|
+
}
|
|
258
|
+
return lines;
|
|
259
|
+
}
|
|
260
|
+
function hudActionButtons(doc) {
|
|
261
|
+
return doc.actions.slice(0, SLACK_ACTIONS_MAX_TOTAL).map((action) => ({
|
|
262
|
+
type: "button",
|
|
263
|
+
text: {
|
|
264
|
+
type: "plain_text",
|
|
265
|
+
text: truncateSlackText(action.label, 75),
|
|
266
|
+
},
|
|
267
|
+
value: truncateSlackText(action.command_text, 2_000),
|
|
268
|
+
action_id: buildSlackHudActionId(action.id),
|
|
269
|
+
}));
|
|
270
|
+
}
|
|
144
271
|
export function splitSlackMessageText(text, maxLen = SLACK_MESSAGE_MAX_LEN) {
|
|
145
272
|
if (text.length <= maxLen) {
|
|
146
273
|
return [text];
|
|
@@ -161,8 +288,46 @@ export function splitSlackMessageText(text, maxLen = SLACK_MESSAGE_MAX_LEN) {
|
|
|
161
288
|
}
|
|
162
289
|
return chunks;
|
|
163
290
|
}
|
|
164
|
-
function slackBlocksForOutboxRecord(
|
|
165
|
-
|
|
291
|
+
function slackBlocksForOutboxRecord(record, body) {
|
|
292
|
+
const hudDocs = hudDocsForPresentation(record.envelope.metadata?.hud_docs, SLACK_DOCS_MAX);
|
|
293
|
+
if (hudDocs.length === 0) {
|
|
294
|
+
return undefined;
|
|
295
|
+
}
|
|
296
|
+
const blocks = [];
|
|
297
|
+
blocks.push({
|
|
298
|
+
type: "section",
|
|
299
|
+
text: { type: "mrkdwn", text: truncateSlackText(body) },
|
|
300
|
+
});
|
|
301
|
+
for (const doc of hudDocs) {
|
|
302
|
+
if (blocks.length >= SLACK_BLOCKS_MAX) {
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
const styledTitle = applySlackHudTextStyle(doc.title, doc.title_style, { defaultWeight: "strong" });
|
|
306
|
+
blocks.push({
|
|
307
|
+
type: "context",
|
|
308
|
+
elements: [{ type: "mrkdwn", text: truncateSlackText(`*HUD* · ${styledTitle}`) }],
|
|
309
|
+
});
|
|
310
|
+
for (const line of hudDocSectionLines(doc)) {
|
|
311
|
+
if (blocks.length >= SLACK_BLOCKS_MAX) {
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
blocks.push({
|
|
315
|
+
type: "section",
|
|
316
|
+
text: { type: "mrkdwn", text: truncateSlackText(line) },
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
const buttons = hudActionButtons(doc);
|
|
320
|
+
for (let idx = 0; idx < buttons.length; idx += SLACK_ACTIONS_MAX_PER_BLOCK) {
|
|
321
|
+
if (blocks.length >= SLACK_BLOCKS_MAX) {
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
blocks.push({
|
|
325
|
+
type: "actions",
|
|
326
|
+
elements: buttons.slice(idx, idx + SLACK_ACTIONS_MAX_PER_BLOCK),
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return blocks.slice(0, SLACK_BLOCKS_MAX);
|
|
166
331
|
}
|
|
167
332
|
function slackThreadTsFromMetadata(metadata) {
|
|
168
333
|
const candidates = [metadata?.slack_thread_ts, metadata?.slack_message_ts, metadata?.thread_ts];
|
|
@@ -181,8 +346,63 @@ function slackStatusMessageTsFromMetadata(metadata) {
|
|
|
181
346
|
const trimmed = value.trim();
|
|
182
347
|
return trimmed.length > 0 ? trimmed : undefined;
|
|
183
348
|
}
|
|
184
|
-
|
|
185
|
-
|
|
349
|
+
const TELEGRAM_CALLBACK_DATA_MAX_BYTES = 64;
|
|
350
|
+
const TELEGRAM_ACTIONS_MAX_TOTAL = 20;
|
|
351
|
+
const TELEGRAM_ACTIONS_PER_ROW = 3;
|
|
352
|
+
const TELEGRAM_DOCS_MAX = 2;
|
|
353
|
+
function utf8ByteLength(value) {
|
|
354
|
+
return new TextEncoder().encode(value).length;
|
|
355
|
+
}
|
|
356
|
+
function telegramTextForOutboxRecord(record, body) {
|
|
357
|
+
const hudDocs = hudDocsForPresentation(record.envelope.metadata?.hud_docs, TELEGRAM_DOCS_MAX);
|
|
358
|
+
if (hudDocs.length === 0) {
|
|
359
|
+
return body;
|
|
360
|
+
}
|
|
361
|
+
const lines = [body.trim()];
|
|
362
|
+
for (const doc of hudDocs) {
|
|
363
|
+
lines.push("", `HUD · ${doc.title}`);
|
|
364
|
+
for (const sectionLine of hudDocSectionLines(doc)) {
|
|
365
|
+
lines.push(stripSlackMrkdwn(sectionLine));
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return lines.join("\n").trim();
|
|
369
|
+
}
|
|
370
|
+
async function compileTelegramHudActions(opts) {
|
|
371
|
+
const hudDocs = hudDocsForPresentation(opts.record.envelope.metadata?.hud_docs, TELEGRAM_DOCS_MAX);
|
|
372
|
+
if (hudDocs.length === 0) {
|
|
373
|
+
return { overflowText: "" };
|
|
374
|
+
}
|
|
375
|
+
const buttons = [];
|
|
376
|
+
const overflowLines = [];
|
|
377
|
+
const actions = hudDocs.flatMap((doc) => doc.actions).slice(0, TELEGRAM_ACTIONS_MAX_TOTAL);
|
|
378
|
+
for (const action of actions) {
|
|
379
|
+
let callbackData = action.command_text;
|
|
380
|
+
if (opts.encodeCallbackData) {
|
|
381
|
+
try {
|
|
382
|
+
callbackData = await opts.encodeCallbackData(action.command_text);
|
|
383
|
+
}
|
|
384
|
+
catch {
|
|
385
|
+
overflowLines.push(`• ${action.label}: ${action.command_text}`);
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (utf8ByteLength(callbackData) > TELEGRAM_CALLBACK_DATA_MAX_BYTES) {
|
|
390
|
+
overflowLines.push(`• ${action.label}: ${action.command_text}`);
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
buttons.push({
|
|
394
|
+
text: action.label.slice(0, 64),
|
|
395
|
+
callback_data: callbackData,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
const inline_keyboard = [];
|
|
399
|
+
for (let idx = 0; idx < buttons.length; idx += TELEGRAM_ACTIONS_PER_ROW) {
|
|
400
|
+
inline_keyboard.push(buttons.slice(idx, idx + TELEGRAM_ACTIONS_PER_ROW));
|
|
401
|
+
}
|
|
402
|
+
return {
|
|
403
|
+
replyMarkup: inline_keyboard.length > 0 ? { inline_keyboard } : undefined,
|
|
404
|
+
overflowText: overflowLines.length > 0 ? `\n\nActions:\n${overflowLines.join("\n")}` : "",
|
|
405
|
+
};
|
|
186
406
|
}
|
|
187
407
|
async function postTelegramMessage(botToken, payload) {
|
|
188
408
|
return await fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, {
|
|
@@ -272,12 +492,17 @@ async function sendTelegramTextChunks(opts) {
|
|
|
272
492
|
}
|
|
273
493
|
export async function deliverTelegramOutboxRecord(opts) {
|
|
274
494
|
const { botToken, record } = opts;
|
|
275
|
-
const
|
|
495
|
+
const hudActions = await compileTelegramHudActions({
|
|
496
|
+
record,
|
|
497
|
+
encodeCallbackData: opts.encodeCallbackData,
|
|
498
|
+
});
|
|
499
|
+
const replyMarkup = hudActions.replyMarkup;
|
|
276
500
|
const replyToMessageId = maybeParseTelegramMessageId(record.envelope.metadata?.telegram_reply_to_message_id);
|
|
501
|
+
const telegramText = `${telegramTextForOutboxRecord(record, record.envelope.body)}${hudActions.overflowText}`.trim();
|
|
277
502
|
const fallbackMessagePayload = {
|
|
278
503
|
...buildTelegramSendMessagePayload({
|
|
279
504
|
chatId: record.envelope.channel_conversation_id,
|
|
280
|
-
text:
|
|
505
|
+
text: telegramText,
|
|
281
506
|
richFormatting: true,
|
|
282
507
|
}),
|
|
283
508
|
...(replyMarkup ? { reply_markup: replyMarkup } : {}),
|
|
@@ -727,6 +952,13 @@ export async function bootstrapControlPlane(opts) {
|
|
|
727
952
|
return await deliverTelegramOutboxRecord({
|
|
728
953
|
botToken: telegramBotToken,
|
|
729
954
|
record,
|
|
955
|
+
encodeCallbackData: async (commandText) => {
|
|
956
|
+
const active = telegramManager.activeAdapter();
|
|
957
|
+
if (!active) {
|
|
958
|
+
return commandText;
|
|
959
|
+
}
|
|
960
|
+
return await active.issueCallbackToken({ commandText });
|
|
961
|
+
},
|
|
730
962
|
});
|
|
731
963
|
},
|
|
732
964
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@femtomc/mu-server",
|
|
3
|
-
"version": "26.2.
|
|
3
|
+
"version": "26.2.102",
|
|
4
4
|
"description": "HTTP API server for mu control-plane transport/session plus run/activity scheduling coordination.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mu",
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
"start": "bun run dist/cli.js"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@femtomc/mu-agent": "26.2.
|
|
34
|
-
"@femtomc/mu-control-plane": "26.2.
|
|
35
|
-
"@femtomc/mu-core": "26.2.
|
|
33
|
+
"@femtomc/mu-agent": "26.2.102",
|
|
34
|
+
"@femtomc/mu-control-plane": "26.2.102",
|
|
35
|
+
"@femtomc/mu-core": "26.2.102"
|
|
36
36
|
}
|
|
37
37
|
}
|