@femtomc/mu-server 26.2.99 → 26.2.101

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
@@ -185,6 +185,10 @@ 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. New features should extend the shared HUD contract path instead of bespoke
190
+ channel-specific HUD payload formats.
191
+
188
192
  ## Running the Server
189
193
 
190
194
  ### With terminal operator session (recommended)
@@ -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;
@@ -1,4 +1,5 @@
1
- import { ControlPlaneCommandPipeline, ControlPlaneOutbox, ControlPlaneRuntime, getControlPlanePaths, TelegramControlPlaneAdapterSpec, } from "@femtomc/mu-control-plane";
1
+ import { 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,100 @@ 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 truncateSlackText(text, maxLen = SLACK_BLOCK_TEXT_MAX_LEN) {
152
+ if (text.length <= maxLen) {
153
+ return text;
154
+ }
155
+ if (maxLen <= 1) {
156
+ return text.slice(0, maxLen);
157
+ }
158
+ return `${text.slice(0, maxLen - 1)}…`;
159
+ }
160
+ function hudTonePrefix(tone) {
161
+ switch (tone) {
162
+ case "success":
163
+ return "✅";
164
+ case "warning":
165
+ return "⚠️";
166
+ case "error":
167
+ return "⛔";
168
+ case "accent":
169
+ return "🔹";
170
+ case "muted":
171
+ return "▫️";
172
+ case "dim":
173
+ return "·";
174
+ case "info":
175
+ default:
176
+ return "ℹ️";
177
+ }
178
+ }
179
+ function hudDocSectionLines(doc) {
180
+ const lines = [];
181
+ const chipsLine = doc.chips
182
+ .slice(0, 8)
183
+ .map((chip) => `${hudTonePrefix(chip.tone)} *${chip.label}*`)
184
+ .join(" · ");
185
+ if (chipsLine.length > 0) {
186
+ lines.push(chipsLine);
187
+ }
188
+ for (const section of doc.sections) {
189
+ switch (section.kind) {
190
+ case "kv": {
191
+ const title = section.title ? `*${section.title}*` : "*Details*";
192
+ const items = section.items.slice(0, SLACK_SECTION_LINE_MAX).map((item) => `• *${item.label}:* ${item.value}`);
193
+ if (items.length > 0) {
194
+ lines.push([title, ...items].join("\n"));
195
+ }
196
+ break;
197
+ }
198
+ case "checklist": {
199
+ const title = section.title ? `*${section.title}*` : "*Checklist*";
200
+ const items = section.items
201
+ .slice(0, SLACK_SECTION_LINE_MAX)
202
+ .map((item) => `${item.done ? "✅" : "⬜"} ${item.label}`);
203
+ if (items.length > 0) {
204
+ lines.push([title, ...items].join("\n"));
205
+ }
206
+ break;
207
+ }
208
+ case "activity": {
209
+ const title = section.title ? `*${section.title}*` : "*Activity*";
210
+ const items = section.lines.slice(0, SLACK_SECTION_LINE_MAX).map((line) => `• ${line}`);
211
+ if (items.length > 0) {
212
+ lines.push([title, ...items].join("\n"));
213
+ }
214
+ break;
215
+ }
216
+ case "text": {
217
+ const title = section.title ? `*${section.title}*\n` : "";
218
+ lines.push(`${title}${section.text}`);
219
+ break;
220
+ }
221
+ }
222
+ }
223
+ if (lines.length === 0) {
224
+ lines.push(`*Snapshot*\n${doc.snapshot_compact}`);
225
+ }
226
+ return lines;
227
+ }
228
+ function hudActionButtons(doc) {
229
+ return doc.actions.slice(0, SLACK_ACTIONS_MAX_TOTAL).map((action) => ({
230
+ type: "button",
231
+ text: {
232
+ type: "plain_text",
233
+ text: truncateSlackText(action.label, 75),
234
+ },
235
+ value: truncateSlackText(action.command_text, 2_000),
236
+ action_id: buildSlackHudActionId(action.id),
237
+ }));
238
+ }
144
239
  export function splitSlackMessageText(text, maxLen = SLACK_MESSAGE_MAX_LEN) {
145
240
  if (text.length <= maxLen) {
146
241
  return [text];
@@ -161,8 +256,45 @@ export function splitSlackMessageText(text, maxLen = SLACK_MESSAGE_MAX_LEN) {
161
256
  }
162
257
  return chunks;
163
258
  }
164
- function slackBlocksForOutboxRecord(_record, _body) {
165
- return undefined;
259
+ function slackBlocksForOutboxRecord(record, body) {
260
+ const hudDocs = normalizeHudDocs(record.envelope.metadata?.hud_docs, { maxDocs: SLACK_DOCS_MAX });
261
+ if (hudDocs.length === 0) {
262
+ return undefined;
263
+ }
264
+ const blocks = [];
265
+ blocks.push({
266
+ type: "section",
267
+ text: { type: "mrkdwn", text: truncateSlackText(body) },
268
+ });
269
+ for (const doc of hudDocs) {
270
+ if (blocks.length >= SLACK_BLOCKS_MAX) {
271
+ break;
272
+ }
273
+ blocks.push({
274
+ type: "context",
275
+ elements: [{ type: "mrkdwn", text: truncateSlackText(`*HUD · ${doc.title}*`) }],
276
+ });
277
+ for (const line of hudDocSectionLines(doc)) {
278
+ if (blocks.length >= SLACK_BLOCKS_MAX) {
279
+ break;
280
+ }
281
+ blocks.push({
282
+ type: "section",
283
+ text: { type: "mrkdwn", text: truncateSlackText(line) },
284
+ });
285
+ }
286
+ const buttons = hudActionButtons(doc);
287
+ for (let idx = 0; idx < buttons.length; idx += SLACK_ACTIONS_MAX_PER_BLOCK) {
288
+ if (blocks.length >= SLACK_BLOCKS_MAX) {
289
+ break;
290
+ }
291
+ blocks.push({
292
+ type: "actions",
293
+ elements: buttons.slice(idx, idx + SLACK_ACTIONS_MAX_PER_BLOCK),
294
+ });
295
+ }
296
+ }
297
+ return blocks.slice(0, SLACK_BLOCKS_MAX);
166
298
  }
167
299
  function slackThreadTsFromMetadata(metadata) {
168
300
  const candidates = [metadata?.slack_thread_ts, metadata?.slack_message_ts, metadata?.thread_ts];
@@ -181,8 +313,63 @@ function slackStatusMessageTsFromMetadata(metadata) {
181
313
  const trimmed = value.trim();
182
314
  return trimmed.length > 0 ? trimmed : undefined;
183
315
  }
184
- function telegramReplyMarkupForOutboxRecord(_record) {
185
- return undefined;
316
+ const TELEGRAM_CALLBACK_DATA_MAX_BYTES = 64;
317
+ const TELEGRAM_ACTIONS_MAX_TOTAL = 20;
318
+ const TELEGRAM_ACTIONS_PER_ROW = 3;
319
+ const TELEGRAM_DOCS_MAX = 2;
320
+ function utf8ByteLength(value) {
321
+ return new TextEncoder().encode(value).length;
322
+ }
323
+ function telegramTextForOutboxRecord(record, body) {
324
+ const hudDocs = normalizeHudDocs(record.envelope.metadata?.hud_docs, { maxDocs: TELEGRAM_DOCS_MAX });
325
+ if (hudDocs.length === 0) {
326
+ return body;
327
+ }
328
+ const lines = [body.trim()];
329
+ for (const doc of hudDocs) {
330
+ lines.push("", `HUD · ${doc.title}`);
331
+ for (const sectionLine of hudDocSectionLines(doc)) {
332
+ lines.push(sectionLine.replace(/\*/g, ""));
333
+ }
334
+ }
335
+ return lines.join("\n").trim();
336
+ }
337
+ async function compileTelegramHudActions(opts) {
338
+ const hudDocs = normalizeHudDocs(opts.record.envelope.metadata?.hud_docs, { maxDocs: TELEGRAM_DOCS_MAX });
339
+ if (hudDocs.length === 0) {
340
+ return { overflowText: "" };
341
+ }
342
+ const buttons = [];
343
+ const overflowLines = [];
344
+ const actions = hudDocs.flatMap((doc) => doc.actions).slice(0, TELEGRAM_ACTIONS_MAX_TOTAL);
345
+ for (const action of actions) {
346
+ let callbackData = action.command_text;
347
+ if (opts.encodeCallbackData) {
348
+ try {
349
+ callbackData = await opts.encodeCallbackData(action.command_text);
350
+ }
351
+ catch {
352
+ overflowLines.push(`• ${action.label}: ${action.command_text}`);
353
+ continue;
354
+ }
355
+ }
356
+ if (utf8ByteLength(callbackData) > TELEGRAM_CALLBACK_DATA_MAX_BYTES) {
357
+ overflowLines.push(`• ${action.label}: ${action.command_text}`);
358
+ continue;
359
+ }
360
+ buttons.push({
361
+ text: action.label.slice(0, 64),
362
+ callback_data: callbackData,
363
+ });
364
+ }
365
+ const inline_keyboard = [];
366
+ for (let idx = 0; idx < buttons.length; idx += TELEGRAM_ACTIONS_PER_ROW) {
367
+ inline_keyboard.push(buttons.slice(idx, idx + TELEGRAM_ACTIONS_PER_ROW));
368
+ }
369
+ return {
370
+ replyMarkup: inline_keyboard.length > 0 ? { inline_keyboard } : undefined,
371
+ overflowText: overflowLines.length > 0 ? `\n\nActions:\n${overflowLines.join("\n")}` : "",
372
+ };
186
373
  }
187
374
  async function postTelegramMessage(botToken, payload) {
188
375
  return await fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, {
@@ -272,12 +459,17 @@ async function sendTelegramTextChunks(opts) {
272
459
  }
273
460
  export async function deliverTelegramOutboxRecord(opts) {
274
461
  const { botToken, record } = opts;
275
- const replyMarkup = telegramReplyMarkupForOutboxRecord(record);
462
+ const hudActions = await compileTelegramHudActions({
463
+ record,
464
+ encodeCallbackData: opts.encodeCallbackData,
465
+ });
466
+ const replyMarkup = hudActions.replyMarkup;
276
467
  const replyToMessageId = maybeParseTelegramMessageId(record.envelope.metadata?.telegram_reply_to_message_id);
468
+ const telegramText = `${telegramTextForOutboxRecord(record, record.envelope.body)}${hudActions.overflowText}`.trim();
277
469
  const fallbackMessagePayload = {
278
470
  ...buildTelegramSendMessagePayload({
279
471
  chatId: record.envelope.channel_conversation_id,
280
- text: record.envelope.body,
472
+ text: telegramText,
281
473
  richFormatting: true,
282
474
  }),
283
475
  ...(replyMarkup ? { reply_markup: replyMarkup } : {}),
@@ -727,6 +919,13 @@ export async function bootstrapControlPlane(opts) {
727
919
  return await deliverTelegramOutboxRecord({
728
920
  botToken: telegramBotToken,
729
921
  record,
922
+ encodeCallbackData: async (commandText) => {
923
+ const active = telegramManager.activeAdapter();
924
+ if (!active) {
925
+ return commandText;
926
+ }
927
+ return await active.issueCallbackToken({ commandText });
928
+ },
730
929
  });
731
930
  },
732
931
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@femtomc/mu-server",
3
- "version": "26.2.99",
3
+ "version": "26.2.101",
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.99",
34
- "@femtomc/mu-control-plane": "26.2.99",
35
- "@femtomc/mu-core": "26.2.99"
33
+ "@femtomc/mu-agent": "26.2.101",
34
+ "@femtomc/mu-control-plane": "26.2.101",
35
+ "@femtomc/mu-core": "26.2.101"
36
36
  }
37
37
  }