@femtomc/mu-server 26.2.106 → 26.2.108

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
@@ -4,9 +4,9 @@ HTTP API server for mu control-plane infrastructure.
4
4
  Powers `mu serve`, messaging frontend transport routes, and
5
5
  control-plane/session coordination endpoints.
6
6
 
7
- > Scope note: server-routed business query/mutation gateway endpoints have
8
- > been removed. Business reads/writes are CLI-first, while long-lived runtime
9
- > coordination (runs/heartbeats/cron) stays server-owned.
7
+ > Scope note: server-routed endpoints focus on runtime coordination only.
8
+ > Business reads/writes are CLI-first, while long-lived runtime coordination
9
+ > (runs/heartbeats/cron) stays server-owned.
10
10
 
11
11
  ## Installation
12
12
 
@@ -44,7 +44,7 @@ Use `mu store paths --pretty` to resolve `<store>` for the active repo/workspace
44
44
 
45
45
  ### Health Check
46
46
 
47
- - `GET /healthz` or `GET /health` - Returns 200 OK
47
+ - `GET /healthz` - Returns 200 OK
48
48
 
49
49
  ### Status
50
50
 
@@ -194,11 +194,7 @@ Operational fallbacks:
194
194
  - `telegram_reply_to_message_id` metadata anchors replies when parseable.
195
195
  - Missing Slack/Telegram bot tokens surface capability reason codes (`*_bot_token_missing`) and retry behavior.
196
196
 
197
- Server channel renderers consume canonical `hud_docs` metadata (`HudDoc`) for Slack/Telegram HUD
198
- rendering + actions. Optional HUD presentation hints (`title_style`, `snapshot_style`, chip/item styles)
199
- and metadata presets (`metadata.style_preset`) may be used by richer renderers and safely ignored by
200
- plain-text channels. New features should extend the shared HUD contract path instead of bespoke
201
- channel-specific HUD payload formats.
197
+ Server channel renderers consume canonical `ui_docs` metadata (`UiDoc`) for Slack/Telegram/Discord/Neovim delivery. Interactive actions use callback tokens (`mu-ui:*`) with deterministic text fallbacks when tokens cannot be issued or rendered.
202
198
 
203
199
  ## Running the Server
204
200
 
@@ -256,6 +252,6 @@ The server uses:
256
252
  - Filesystem-backed JSONL event storage (FsJsonlStore)
257
253
  - Bun's built-in HTTP server
258
254
  - Control-plane adapter/webhook transport + session coordination routes
259
- - Generation-supervised control-plane hot reload lifecycle (see `docs/adr-0001-control-plane-hot-reload.md`)
255
+ - Generation-supervised control-plane hot reload lifecycle
260
256
 
261
257
  Control-plane/coordination data is persisted under `<store>/` (for example `<store>/events.jsonl` and `<store>/control-plane/*`). Use `mu store paths` to resolve `<store>` for your repo.
@@ -1,4 +1,5 @@
1
1
  import { CONTROL_PLANE_CHANNEL_ADAPTER_SPECS, ingressModeForValue, summarizeInboundAttachmentPolicy, } from "@femtomc/mu-control-plane";
2
+ import { UI_ACTIONS_UNSUPPORTED_REASON, UI_COMPONENT_SUPPORT } from "../control_plane.js";
2
3
  import { configRoutes } from "./config.js";
3
4
  import { eventRoutes } from "./events.js";
4
5
  import { identityRoutes } from "./identities.js";
@@ -92,9 +93,71 @@ function mediaInboundAttachmentCapability(config, channel) {
92
93
  reason: hasToken ? null : "telegram_bot_token_missing",
93
94
  };
94
95
  }
96
+ const TEXT_ONLY_UI_COMPONENT_SUPPORT = {
97
+ text: true,
98
+ list: false,
99
+ key_value: false,
100
+ divider: false,
101
+ };
102
+ const STATUS_PROFILE_ACTIONS_FALLBACK_REASON = "status_profile_actions_degrade_to_text";
103
+ const CHANNEL_UI_CAPABILITIES = {
104
+ slack: {
105
+ supported: true,
106
+ reason: null,
107
+ components: UI_COMPONENT_SUPPORT,
108
+ actions: {
109
+ supported: true,
110
+ reason: STATUS_PROFILE_ACTIONS_FALLBACK_REASON,
111
+ },
112
+ },
113
+ discord: {
114
+ supported: true,
115
+ reason: null,
116
+ components: TEXT_ONLY_UI_COMPONENT_SUPPORT,
117
+ actions: {
118
+ supported: true,
119
+ reason: STATUS_PROFILE_ACTIONS_FALLBACK_REASON,
120
+ },
121
+ },
122
+ telegram: {
123
+ supported: true,
124
+ reason: null,
125
+ components: TEXT_ONLY_UI_COMPONENT_SUPPORT,
126
+ actions: {
127
+ supported: true,
128
+ reason: STATUS_PROFILE_ACTIONS_FALLBACK_REASON,
129
+ },
130
+ },
131
+ neovim: {
132
+ supported: true,
133
+ reason: null,
134
+ components: TEXT_ONLY_UI_COMPONENT_SUPPORT,
135
+ actions: {
136
+ supported: true,
137
+ reason: STATUS_PROFILE_ACTIONS_FALLBACK_REASON,
138
+ },
139
+ },
140
+ terminal: {
141
+ supported: true,
142
+ reason: null,
143
+ components: TEXT_ONLY_UI_COMPONENT_SUPPORT,
144
+ actions: {
145
+ supported: false,
146
+ reason: UI_ACTIONS_UNSUPPORTED_REASON,
147
+ },
148
+ },
149
+ };
150
+ function uiCapabilityForChannel(channel) {
151
+ return CHANNEL_UI_CAPABILITIES[channel] ?? {
152
+ supported: true,
153
+ reason: null,
154
+ components: TEXT_ONLY_UI_COMPONENT_SUPPORT,
155
+ actions: { supported: false, reason: UI_ACTIONS_UNSUPPORTED_REASON },
156
+ };
157
+ }
95
158
  export async function controlPlaneRoutes(request, url, deps, headers) {
96
159
  const path = url.pathname;
97
- if (path === "/api/control-plane" || path === "/api/control-plane/status") {
160
+ if (path === "/api/control-plane/status") {
98
161
  if (request.method !== "GET") {
99
162
  return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
100
163
  }
@@ -164,6 +227,7 @@ export async function controlPlaneRoutes(request, url, deps, headers) {
164
227
  outbound_delivery: mediaOutboundCapability(config, spec.channel),
165
228
  inbound_attachment_download: mediaInboundAttachmentCapability(config, spec.channel),
166
229
  },
230
+ ui: uiCapabilityForChannel(spec.channel),
167
231
  }));
168
232
  return Response.json({
169
233
  ok: true,
@@ -1,5 +1,6 @@
1
1
  import type { MessagingOperatorBackend, MessagingOperatorRuntime } from "@femtomc/mu-agent";
2
- import { type GenerationTelemetryRecorder, type OutboxDeliveryHandlerResult, type OutboxRecord } from "@femtomc/mu-control-plane";
2
+ import { type UiDoc, type UiEvent } from "@femtomc/mu-core";
3
+ import { type GenerationTelemetryRecorder, type OutboxDeliveryHandlerResult, type OutboxRecord, UiCallbackTokenStore } from "@femtomc/mu-control-plane";
3
4
  import { type ControlPlaneConfig, type ControlPlaneGenerationContext, type ControlPlaneHandle, type ControlPlaneSessionLifecycle, type TelegramGenerationSwapHooks, type WakeDeliveryObserver } from "./control_plane_contract.js";
4
5
  import { detectAdapters } from "./control_plane_adapter_registry.js";
5
6
  export type { ActiveAdapter, ControlPlaneConfig, ControlPlaneGenerationContext, ControlPlaneHandle, ControlPlaneSessionLifecycle, ControlPlaneSessionMutationAction, ControlPlaneSessionMutationResult, NotifyOperatorsOpts, NotifyOperatorsResult, TelegramGenerationReloadResult, TelegramGenerationRollbackTrigger, TelegramGenerationSwapHooks, WakeDeliveryEvent, WakeDeliveryObserver, WakeNotifyContext, WakeNotifyDecision, } from "./control_plane_contract.js";
@@ -52,8 +53,22 @@ export declare function buildTelegramSendMessagePayload(opts: {
52
53
  richFormatting: boolean;
53
54
  }): TelegramSendMessagePayload;
54
55
  export declare function splitTelegramMessageText(text: string, maxLen?: number): string[];
56
+ export declare const UI_COMPONENT_SUPPORT: {
57
+ readonly text: true;
58
+ readonly list: true;
59
+ readonly key_value: true;
60
+ readonly divider: true;
61
+ };
62
+ export declare const UI_ACTIONS_UNSUPPORTED_REASON = "ui_actions_not_implemented";
63
+ export declare function uiDocTextLines(doc: UiDoc, opts?: {
64
+ includeActions?: boolean;
65
+ }): string[];
66
+ export declare function uiDocsTextFallback(uiDocs: readonly UiDoc[]): string;
55
67
  export declare function splitSlackMessageText(text: string, maxLen?: number): string[];
56
- type TelegramCallbackDataEncoder = (commandText: string) => Promise<string>;
68
+ type TelegramCallbackDataEncoder = (commandText: string, opts: {
69
+ record: OutboxRecord;
70
+ uiEvent?: UiEvent;
71
+ }) => Promise<string>;
57
72
  export declare function deliverTelegramOutboxRecord(opts: {
58
73
  botToken: string;
59
74
  record: OutboxRecord;
@@ -62,6 +77,9 @@ export declare function deliverTelegramOutboxRecord(opts: {
62
77
  export declare function deliverSlackOutboxRecord(opts: {
63
78
  botToken: string;
64
79
  record: OutboxRecord;
80
+ uiCallbackTokenStore?: UiCallbackTokenStore;
81
+ nowMs?: () => number;
82
+ fetchImpl?: typeof fetch;
65
83
  }): Promise<OutboxDeliveryHandlerResult>;
66
84
  export type BootstrapControlPlaneOpts = {
67
85
  repoRoot: string;
@@ -1,5 +1,5 @@
1
- import { applyHudStylePreset, normalizeHudDocs } from "@femtomc/mu-core";
2
- import { ControlPlaneCommandPipeline, ControlPlaneOutbox, ControlPlaneRuntime, buildSlackHudActionId, getControlPlanePaths, TelegramControlPlaneAdapterSpec, } from "@femtomc/mu-control-plane";
1
+ import { normalizeUiDocs, resolveUiStatusProfileName, } from "@femtomc/mu-core";
2
+ import { ControlPlaneCommandPipeline, ControlPlaneOutbox, ControlPlaneRuntime, buildSanitizedUiEventForAction, buildSlackUiActionId, getControlPlanePaths, issueUiDocActionPayloads, TelegramControlPlaneAdapterSpec, uiActionPayloadContextFromOutboxRecord, uiDocActionPayloadKey, UiCallbackTokenStore, } from "@femtomc/mu-control-plane";
3
3
  import { DEFAULT_MU_CONFIG } from "./config.js";
4
4
  import { buildMessagingOperatorRuntime, createOutboxDrainLoop } from "./control_plane_bootstrap_helpers.js";
5
5
  import { buildWakeOutboundEnvelope, resolveWakeFanoutCapability, wakeDeliveryMetadataFromOutboxRecord, wakeDispatchReasonCode, wakeFanoutDedupeKey, } from "./control_plane_wake_delivery.js";
@@ -146,11 +146,17 @@ const SLACK_BLOCK_TEXT_MAX_LEN = 3_000;
146
146
  const SLACK_BLOCKS_MAX = 50;
147
147
  const SLACK_ACTIONS_MAX_PER_BLOCK = 5;
148
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
- }
149
+ const SLACK_ACTION_VALUE_MAX_CHARS = 2_000;
150
+ const SLACK_UI_EVENT_ACTION_ID = buildSlackUiActionId("ui_event");
151
+ const UI_DOCS_MAX = 3;
152
+ const UI_CALLBACK_TOKEN_TTL_MS = 15 * 60_000;
153
+ export const UI_COMPONENT_SUPPORT = {
154
+ text: true,
155
+ list: true,
156
+ key_value: true,
157
+ divider: true,
158
+ };
159
+ export const UI_ACTIONS_UNSUPPORTED_REASON = "ui_actions_not_implemented";
154
160
  function truncateSlackText(text, maxLen = SLACK_BLOCK_TEXT_MAX_LEN) {
155
161
  if (text.length <= maxLen) {
156
162
  return text;
@@ -160,113 +166,146 @@ function truncateSlackText(text, maxLen = SLACK_BLOCK_TEXT_MAX_LEN) {
160
166
  }
161
167
  return `${text.slice(0, maxLen - 1)}…`;
162
168
  }
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) {
169
+ function uiDocComponentLines(doc) {
202
170
  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
- }
171
+ const components = [...doc.components].sort((a, b) => a.id.localeCompare(b.id));
172
+ for (const component of components) {
173
+ switch (component.kind) {
174
+ case "text": {
175
+ lines.push(component.text);
224
176
  break;
225
177
  }
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"));
178
+ case "list": {
179
+ if (component.title) {
180
+ lines.push(component.title);
181
+ }
182
+ for (const item of component.items) {
183
+ lines.push(`• ${item.label}${item.detail ? ` · ${item.detail}` : ""}`);
233
184
  }
234
185
  break;
235
186
  }
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"));
187
+ case "key_value": {
188
+ if (component.title) {
189
+ lines.push(component.title);
190
+ }
191
+ for (const row of component.rows) {
192
+ lines.push(`• ${row.key}: ${row.value}`);
241
193
  }
242
194
  break;
243
195
  }
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}`);
196
+ case "divider": {
197
+ lines.push("────────");
250
198
  break;
251
199
  }
252
200
  }
253
201
  }
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}`);
202
+ return lines;
203
+ }
204
+ function uiDocActionLines(doc) {
205
+ const actions = [...doc.actions].sort((a, b) => a.id.localeCompare(b.id));
206
+ return actions.map((action) => {
207
+ const parts = [`• ${action.label}`];
208
+ if (action.description) {
209
+ parts.push(action.description);
210
+ }
211
+ parts.push(`(id=${action.id})`);
212
+ return parts.join(" ");
213
+ });
214
+ }
215
+ export function uiDocTextLines(doc, opts = {}) {
216
+ const lines = [`UI · ${doc.title}`];
217
+ if (doc.summary) {
218
+ lines.push(doc.summary);
219
+ }
220
+ const componentLines = uiDocComponentLines(doc);
221
+ if (componentLines.length > 0) {
222
+ lines.push(...componentLines);
223
+ }
224
+ if (opts.includeActions !== false) {
225
+ const actionLines = uiDocActionLines(doc);
226
+ if (actionLines.length > 0) {
227
+ lines.push("Actions:");
228
+ lines.push(...actionLines);
229
+ }
257
230
  }
258
231
  return lines;
259
232
  }
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
- }));
233
+ export function uiDocsTextFallback(uiDocs) {
234
+ if (uiDocs.length === 0) {
235
+ return "";
236
+ }
237
+ const sections = uiDocs.map((doc) => uiDocTextLines(doc).join("\n"));
238
+ return sections.join("\n\n");
239
+ }
240
+ function appendUiDocText(body, uiDocs) {
241
+ const fallback = uiDocsTextFallback(uiDocs);
242
+ if (!fallback) {
243
+ return body;
244
+ }
245
+ const trimmed = body.trim();
246
+ if (trimmed.length === 0) {
247
+ return fallback;
248
+ }
249
+ return `${trimmed}\n\n${fallback}`;
250
+ }
251
+ function uiDocActionTextLine(action, opts = {}) {
252
+ const parts = [`• ${action.label}`];
253
+ if (action.description) {
254
+ parts.push(action.description);
255
+ }
256
+ parts.push(`(id=${action.id}${opts.suffix ? `; ${opts.suffix}` : ""})`);
257
+ return parts.join(" ");
258
+ }
259
+ function statusProfileVariant(doc) {
260
+ const profile = doc.metadata.profile;
261
+ if (!profile || typeof profile !== "object" || Array.isArray(profile)) {
262
+ return "status";
263
+ }
264
+ const rawVariant = typeof profile.variant === "string"
265
+ ? profile.variant.trim().toLowerCase()
266
+ : "";
267
+ return rawVariant.length > 0 ? rawVariant : "status";
268
+ }
269
+ function isStatusProfileStatusVariant(doc) {
270
+ return resolveUiStatusProfileName(doc) !== null && statusProfileVariant(doc) === "status";
271
+ }
272
+ function uiDocDeterministicActionFallbackLine(action) {
273
+ const commandText = commandTextForUiDocAction(action);
274
+ if (commandText) {
275
+ return `• ${action.label}: ${commandText}`;
276
+ }
277
+ return `• ${action.label}: interactive unavailable (missing command_text)`;
278
+ }
279
+ function uiDocActionButtons(doc, actionPayloadsByKey) {
280
+ const buttons = [];
281
+ const fallbackLines = [];
282
+ const statusProfile = isStatusProfileStatusVariant(doc);
283
+ const actions = [...doc.actions].sort((a, b) => a.id.localeCompare(b.id)).slice(0, SLACK_ACTIONS_MAX_TOTAL);
284
+ for (const action of actions) {
285
+ if (statusProfile) {
286
+ fallbackLines.push(uiDocDeterministicActionFallbackLine(action));
287
+ continue;
288
+ }
289
+ const payload = actionPayloadsByKey.get(uiDocActionPayloadKey(doc.ui_id, action.id));
290
+ if (!payload) {
291
+ fallbackLines.push(uiDocActionTextLine(action, { suffix: "interactive unavailable" }));
292
+ continue;
293
+ }
294
+ if (payload.length > SLACK_ACTION_VALUE_MAX_CHARS) {
295
+ fallbackLines.push(uiDocActionTextLine(action, { suffix: "interactive payload too large" }));
296
+ continue;
297
+ }
298
+ buttons.push({
299
+ type: "button",
300
+ text: {
301
+ type: "plain_text",
302
+ text: truncateSlackText(action.label, 75),
303
+ },
304
+ value: payload,
305
+ action_id: SLACK_UI_EVENT_ACTION_ID,
306
+ });
307
+ }
308
+ return { buttons, fallbackLines };
270
309
  }
271
310
  export function splitSlackMessageText(text, maxLen = SLACK_MESSAGE_MAX_LEN) {
272
311
  if (text.length <= maxLen) {
@@ -288,26 +327,27 @@ export function splitSlackMessageText(text, maxLen = SLACK_MESSAGE_MAX_LEN) {
288
327
  }
289
328
  return chunks;
290
329
  }
291
- function slackBlocksForOutboxRecord(record, body) {
292
- const hudDocs = hudDocsForPresentation(record.envelope.metadata?.hud_docs, SLACK_DOCS_MAX);
293
- if (hudDocs.length === 0) {
330
+ function slackBlocksForOutboxRecord(body, uiDocs, opts = {}) {
331
+ if (uiDocs.length === 0) {
294
332
  return undefined;
295
333
  }
334
+ const uiDocActionPayloadsByKey = opts.uiDocActionPayloadsByKey ?? new Map();
296
335
  const blocks = [];
336
+ const headerText = body.trim().length > 0 ? body : "Update";
297
337
  blocks.push({
298
338
  type: "section",
299
- text: { type: "mrkdwn", text: truncateSlackText(body) },
339
+ text: { type: "mrkdwn", text: truncateSlackText(headerText) },
300
340
  });
301
- for (const doc of hudDocs) {
341
+ for (const doc of uiDocs) {
302
342
  if (blocks.length >= SLACK_BLOCKS_MAX) {
303
343
  break;
304
344
  }
305
- const styledTitle = applySlackHudTextStyle(doc.title, doc.title_style, { defaultWeight: "strong" });
345
+ const lines = uiDocTextLines(doc, { includeActions: false });
306
346
  blocks.push({
307
347
  type: "context",
308
- elements: [{ type: "mrkdwn", text: truncateSlackText(`*HUD* · ${styledTitle}`) }],
348
+ elements: [{ type: "mrkdwn", text: truncateSlackText(lines[0]) }],
309
349
  });
310
- for (const line of hudDocSectionLines(doc)) {
350
+ for (const line of lines.slice(1)) {
311
351
  if (blocks.length >= SLACK_BLOCKS_MAX) {
312
352
  break;
313
353
  }
@@ -316,27 +356,35 @@ function slackBlocksForOutboxRecord(record, body) {
316
356
  text: { type: "mrkdwn", text: truncateSlackText(line) },
317
357
  });
318
358
  }
319
- const buttons = hudActionButtons(doc);
320
- for (let idx = 0; idx < buttons.length; idx += SLACK_ACTIONS_MAX_PER_BLOCK) {
359
+ const actionRender = uiDocActionButtons(doc, uiDocActionPayloadsByKey);
360
+ for (let idx = 0; idx < actionRender.buttons.length; idx += SLACK_ACTIONS_MAX_PER_BLOCK) {
321
361
  if (blocks.length >= SLACK_BLOCKS_MAX) {
322
362
  break;
323
363
  }
324
364
  blocks.push({
325
365
  type: "actions",
326
- elements: buttons.slice(idx, idx + SLACK_ACTIONS_MAX_PER_BLOCK),
366
+ elements: actionRender.buttons.slice(idx, idx + SLACK_ACTIONS_MAX_PER_BLOCK),
367
+ });
368
+ }
369
+ if (actionRender.fallbackLines.length > 0 && blocks.length < SLACK_BLOCKS_MAX) {
370
+ blocks.push({
371
+ type: "section",
372
+ text: {
373
+ type: "mrkdwn",
374
+ text: truncateSlackText(`Actions:\n${actionRender.fallbackLines.join("\n")}`),
375
+ },
327
376
  });
328
377
  }
329
378
  }
330
379
  return blocks.slice(0, SLACK_BLOCKS_MAX);
331
380
  }
332
381
  function slackThreadTsFromMetadata(metadata) {
333
- const candidates = [metadata?.slack_thread_ts, metadata?.slack_message_ts, metadata?.thread_ts];
334
- for (const value of candidates) {
335
- if (typeof value === "string" && value.trim().length > 0) {
336
- return value.trim();
337
- }
382
+ const value = metadata?.slack_thread_ts;
383
+ if (typeof value !== "string") {
384
+ return undefined;
338
385
  }
339
- return undefined;
386
+ const trimmed = value.trim();
387
+ return trimmed.length > 0 ? trimmed : undefined;
340
388
  }
341
389
  function slackStatusMessageTsFromMetadata(metadata) {
342
390
  const value = metadata?.slack_status_message_ts;
@@ -349,60 +397,96 @@ function slackStatusMessageTsFromMetadata(metadata) {
349
397
  const TELEGRAM_CALLBACK_DATA_MAX_BYTES = 64;
350
398
  const TELEGRAM_ACTIONS_MAX_TOTAL = 20;
351
399
  const TELEGRAM_ACTIONS_PER_ROW = 3;
352
- const TELEGRAM_DOCS_MAX = 2;
353
400
  function utf8ByteLength(value) {
354
401
  return new TextEncoder().encode(value).length;
355
402
  }
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
- }
403
+ function telegramTextForOutboxRecord(body, uiDocs) {
361
404
  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
- }
405
+ const uiFallback = uiDocsTextFallback(uiDocs);
406
+ if (uiFallback) {
407
+ lines.push("", uiFallback);
367
408
  }
368
409
  return lines.join("\n").trim();
369
410
  }
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: "" };
411
+ function telegramReplyMarkupFromButtons(buttons) {
412
+ if (buttons.length === 0) {
413
+ return undefined;
374
414
  }
375
- const buttons = [];
415
+ const inline_keyboard = [];
416
+ for (let idx = 0; idx < buttons.length; idx += TELEGRAM_ACTIONS_PER_ROW) {
417
+ inline_keyboard.push(buttons.slice(idx, idx + TELEGRAM_ACTIONS_PER_ROW));
418
+ }
419
+ return { inline_keyboard };
420
+ }
421
+ function telegramOverflowText(lines) {
422
+ if (lines.length === 0) {
423
+ return "";
424
+ }
425
+ return `\n\nActions:\n${lines.join("\n")}`;
426
+ }
427
+ function commandTextForUiDocAction(action) {
428
+ const fromMetadata = typeof action.metadata.command_text === "string" ? action.metadata.command_text.trim() : "";
429
+ if (fromMetadata.length === 0) {
430
+ return null;
431
+ }
432
+ return fromMetadata;
433
+ }
434
+ async function compileTelegramUiDocActions(opts) {
435
+ if (opts.uiDocs.length === 0) {
436
+ return { buttonEntries: [], overflowLines: [] };
437
+ }
438
+ const buttonEntries = [];
376
439
  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) {
440
+ for (const doc of opts.uiDocs) {
441
+ const statusProfile = isStatusProfileStatusVariant(doc);
442
+ const actions = [...doc.actions].sort((a, b) => a.id.localeCompare(b.id));
443
+ for (const action of actions) {
444
+ const commandText = commandTextForUiDocAction(action);
445
+ const fallbackLine = commandText
446
+ ? `• ${action.label}: ${commandText}`
447
+ : `• ${action.label}: interactive unavailable (missing command_text)`;
448
+ if (statusProfile) {
449
+ overflowLines.push(fallbackLine);
450
+ continue;
451
+ }
452
+ const uiEvent = buildSanitizedUiEventForAction({
453
+ doc,
454
+ action,
455
+ createdAtMs: opts.nowMs,
456
+ });
457
+ if (!commandText || !uiEvent) {
458
+ overflowLines.push(fallbackLine);
459
+ continue;
460
+ }
461
+ if (!opts.encodeCallbackData) {
462
+ overflowLines.push(fallbackLine);
463
+ continue;
464
+ }
465
+ let callbackData;
381
466
  try {
382
- callbackData = await opts.encodeCallbackData(action.command_text);
467
+ callbackData = await opts.encodeCallbackData(commandText, {
468
+ record: opts.record,
469
+ uiEvent,
470
+ });
383
471
  }
384
472
  catch {
385
- overflowLines.push(`• ${action.label}: ${action.command_text}`);
473
+ overflowLines.push(fallbackLine);
386
474
  continue;
387
475
  }
476
+ if (utf8ByteLength(callbackData) > TELEGRAM_CALLBACK_DATA_MAX_BYTES) {
477
+ overflowLines.push(fallbackLine);
478
+ continue;
479
+ }
480
+ buttonEntries.push({
481
+ button: {
482
+ text: action.label.slice(0, 64),
483
+ callback_data: callbackData,
484
+ },
485
+ fallbackLine,
486
+ });
388
487
  }
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
488
  }
402
- return {
403
- replyMarkup: inline_keyboard.length > 0 ? { inline_keyboard } : undefined,
404
- overflowText: overflowLines.length > 0 ? `\n\nActions:\n${overflowLines.join("\n")}` : "",
405
- };
489
+ return { buttonEntries, overflowLines };
406
490
  }
407
491
  async function postTelegramMessage(botToken, payload) {
408
492
  return await fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, {
@@ -492,13 +576,22 @@ async function sendTelegramTextChunks(opts) {
492
576
  }
493
577
  export async function deliverTelegramOutboxRecord(opts) {
494
578
  const { botToken, record } = opts;
495
- const hudActions = await compileTelegramHudActions({
579
+ const uiDocs = normalizeUiDocs(record.envelope.metadata?.ui_docs, { maxDocs: UI_DOCS_MAX });
580
+ const nowMs = Math.trunc(Date.now());
581
+ const uiActions = await compileTelegramUiDocActions({
496
582
  record,
583
+ uiDocs,
584
+ nowMs,
497
585
  encodeCallbackData: opts.encodeCallbackData,
498
586
  });
499
- const replyMarkup = hudActions.replyMarkup;
587
+ const visibleEntries = uiActions.buttonEntries.slice(0, TELEGRAM_ACTIONS_MAX_TOTAL);
588
+ const overflowLines = [
589
+ ...uiActions.overflowLines,
590
+ ...uiActions.buttonEntries.slice(TELEGRAM_ACTIONS_MAX_TOTAL).map((entry) => entry.fallbackLine),
591
+ ];
592
+ const replyMarkup = telegramReplyMarkupFromButtons(visibleEntries.map((entry) => entry.button));
500
593
  const replyToMessageId = maybeParseTelegramMessageId(record.envelope.metadata?.telegram_reply_to_message_id);
501
- const telegramText = `${telegramTextForOutboxRecord(record, record.envelope.body)}${hudActions.overflowText}`.trim();
594
+ const telegramText = `${telegramTextForOutboxRecord(record.envelope.body, uiDocs)}${telegramOverflowText(overflowLines)}`.trim();
502
595
  const fallbackMessagePayload = {
503
596
  ...buildTelegramSendMessagePayload({
504
597
  chatId: record.envelope.channel_conversation_id,
@@ -640,7 +733,8 @@ export async function deliverTelegramOutboxRecord(opts) {
640
733
  };
641
734
  }
642
735
  async function postSlackJson(opts) {
643
- const response = await fetch(`https://slack.com/api/${opts.method}`, {
736
+ const fetchImpl = opts.fetchImpl ?? fetch;
737
+ const response = await fetchImpl(`https://slack.com/api/${opts.method}`, {
644
738
  method: "POST",
645
739
  headers: {
646
740
  Authorization: `Bearer ${opts.botToken}`,
@@ -653,10 +747,32 @@ async function postSlackJson(opts) {
653
747
  }
654
748
  export async function deliverSlackOutboxRecord(opts) {
655
749
  const { botToken, record } = opts;
750
+ const fetchImpl = opts.fetchImpl ?? fetch;
656
751
  const attachments = record.envelope.attachments ?? [];
657
- const renderedBody = renderSlackMarkdown(record.envelope.body);
752
+ const uiDocs = normalizeUiDocs(record.envelope.metadata?.ui_docs, { maxDocs: UI_DOCS_MAX });
753
+ const interactiveUiDocs = uiDocs.filter((doc) => !isStatusProfileStatusVariant(doc));
754
+ let uiDocActionPayloadsByKey = new Map();
755
+ if (opts.uiCallbackTokenStore && interactiveUiDocs.some((doc) => doc.actions.length > 0)) {
756
+ const issued = await issueUiDocActionPayloads({
757
+ uiDocs: interactiveUiDocs,
758
+ tokenStore: opts.uiCallbackTokenStore,
759
+ context: uiActionPayloadContextFromOutboxRecord(record),
760
+ ttlMs: UI_CALLBACK_TOKEN_TTL_MS,
761
+ nowMs: Math.trunc((opts.nowMs ?? Date.now)()),
762
+ });
763
+ uiDocActionPayloadsByKey = new Map(issued.map((entry) => [entry.key, entry.payload_json]));
764
+ }
765
+ const renderedBodyForBlocks = renderSlackMarkdown(record.envelope.body);
766
+ const blocks = slackBlocksForOutboxRecord(renderedBodyForBlocks, uiDocs, {
767
+ uiDocActionPayloadsByKey,
768
+ });
769
+ const bodyForText = blocks
770
+ ? record.envelope.body.trim().length > 0
771
+ ? record.envelope.body
772
+ : "Update"
773
+ : appendUiDocText(record.envelope.body, uiDocs);
774
+ const renderedBody = renderSlackMarkdown(bodyForText);
658
775
  const textChunks = splitSlackMessageText(renderedBody);
659
- const blocks = slackBlocksForOutboxRecord(record, renderedBody);
660
776
  const threadTs = slackThreadTsFromMetadata(record.envelope.metadata);
661
777
  const statusMessageTs = slackStatusMessageTsFromMetadata(record.envelope.metadata);
662
778
  if (attachments.length === 0) {
@@ -673,6 +789,7 @@ export async function deliverSlackOutboxRecord(opts) {
673
789
  unfurl_media: false,
674
790
  ...(blocks ? { blocks } : {}),
675
791
  },
792
+ fetchImpl,
676
793
  });
677
794
  if (updated.response.ok && updated.payload?.ok) {
678
795
  chunkStartIndex = 1;
@@ -707,6 +824,7 @@ export async function deliverSlackOutboxRecord(opts) {
707
824
  ...(index === 0 && blocks ? { blocks } : {}),
708
825
  ...(threadTs ? { thread_ts: threadTs } : {}),
709
826
  },
827
+ fetchImpl,
710
828
  });
711
829
  if (delivered.response.ok && delivered.payload?.ok) {
712
830
  continue;
@@ -733,7 +851,7 @@ export async function deliverSlackOutboxRecord(opts) {
733
851
  error: `slack attachment ${index + 1} missing reference.url`,
734
852
  };
735
853
  }
736
- const source = await fetch(referenceUrl);
854
+ const source = await fetchImpl(referenceUrl);
737
855
  if (!source.ok) {
738
856
  const sourceErr = await source.text().catch(() => "");
739
857
  if (source.status === 429 || source.status >= 500) {
@@ -759,7 +877,7 @@ export async function deliverSlackOutboxRecord(opts) {
759
877
  form.set("thread_ts", threadTs);
760
878
  }
761
879
  form.set("file", new Blob([bytes], { type: contentType }), filename);
762
- const uploaded = await fetch("https://slack.com/api/files.upload", {
880
+ const uploaded = await fetchImpl("https://slack.com/api/files.upload", {
763
881
  method: "POST",
764
882
  headers: {
765
883
  Authorization: `Bearer ${botToken}`,
@@ -797,6 +915,7 @@ export async function deliverSlackOutboxRecord(opts) {
797
915
  unfurl_media: false,
798
916
  ...(threadTs ? { thread_ts: threadTs } : {}),
799
917
  },
918
+ fetchImpl,
800
919
  });
801
920
  if (fallback.response.ok && fallback.payload?.ok) {
802
921
  continue;
@@ -840,6 +959,7 @@ export async function bootstrapControlPlane(opts) {
840
959
  return null;
841
960
  }
842
961
  const paths = getControlPlanePaths(opts.repoRoot);
962
+ const uiCallbackTokenStore = new UiCallbackTokenStore(paths.uiCallbackTokenPath);
843
963
  const runtime = new ControlPlaneRuntime({ repoRoot: opts.repoRoot });
844
964
  let pipeline = null;
845
965
  let outboxDrainLoop = null;
@@ -847,6 +967,7 @@ export async function bootstrapControlPlane(opts) {
847
967
  const outboundDeliveryChannels = new Set();
848
968
  const adapterMap = new Map();
849
969
  try {
970
+ await uiCallbackTokenStore.load();
850
971
  await runtime.start();
851
972
  const operator = opts.operatorRuntime !== undefined
852
973
  ? opts.operatorRuntime
@@ -868,6 +989,7 @@ export async function bootstrapControlPlane(opts) {
868
989
  const telegramManager = new TelegramAdapterGenerationManager({
869
990
  pipeline,
870
991
  outbox,
992
+ uiCallbackTokenStore,
871
993
  initialConfig: controlPlaneConfig,
872
994
  onOutboxEnqueued: () => {
873
995
  scheduleOutboxDrainRef?.();
@@ -881,6 +1003,7 @@ export async function bootstrapControlPlane(opts) {
881
1003
  config: controlPlaneConfig,
882
1004
  pipeline,
883
1005
  outbox,
1006
+ uiCallbackTokenStore,
884
1007
  })) {
885
1008
  const route = adapter.spec.route;
886
1009
  if (adapterMap.has(route)) {
@@ -939,6 +1062,7 @@ export async function bootstrapControlPlane(opts) {
939
1062
  return await deliverSlackOutboxRecord({
940
1063
  botToken: slackBotToken,
941
1064
  record,
1065
+ uiCallbackTokenStore,
942
1066
  });
943
1067
  },
944
1068
  },
@@ -952,12 +1076,18 @@ export async function bootstrapControlPlane(opts) {
952
1076
  return await deliverTelegramOutboxRecord({
953
1077
  botToken: telegramBotToken,
954
1078
  record,
955
- encodeCallbackData: async (commandText) => {
1079
+ encodeCallbackData: async (commandText, encodeOpts) => {
956
1080
  const active = telegramManager.activeAdapter();
957
1081
  if (!active) {
958
1082
  return commandText;
959
1083
  }
960
- return await active.issueCallbackToken({ commandText });
1084
+ return await active.issueCallbackToken({
1085
+ commandText,
1086
+ actorId: encodeOpts.record.envelope.correlation.actor_id,
1087
+ actorBindingId: encodeOpts.record.envelope.correlation.actor_binding_id,
1088
+ conversationId: encodeOpts.record.envelope.channel_conversation_id,
1089
+ uiEvent: encodeOpts.uiEvent,
1090
+ });
961
1091
  },
962
1092
  });
963
1093
  },
@@ -1,4 +1,4 @@
1
- import { type ControlPlaneAdapter, type ControlPlaneCommandPipeline, type ControlPlaneOutbox } from "@femtomc/mu-control-plane";
1
+ import { type ControlPlaneAdapter, type ControlPlaneCommandPipeline, type ControlPlaneOutbox, UiCallbackTokenStore } from "@femtomc/mu-control-plane";
2
2
  import type { ControlPlaneConfig } from "./control_plane_contract.js";
3
3
  export type DetectedStaticAdapter = {
4
4
  name: "slack" | "discord" | "neovim";
@@ -16,4 +16,5 @@ export declare function createStaticAdaptersFromDetected(opts: {
16
16
  config: ControlPlaneConfig;
17
17
  pipeline: ControlPlaneCommandPipeline;
18
18
  outbox: ControlPlaneOutbox;
19
+ uiCallbackTokenStore: UiCallbackTokenStore;
19
20
  }): ControlPlaneAdapter[];
@@ -8,6 +8,7 @@ const STATIC_ADAPTER_MODULES = [
8
8
  outbox: opts.outbox,
9
9
  signingSecret: opts.secret,
10
10
  botToken: opts.config.adapters.slack.bot_token,
11
+ uiCallbackTokenStore: opts.uiCallbackTokenStore,
11
12
  }),
12
13
  },
13
14
  {
@@ -17,6 +18,7 @@ const STATIC_ADAPTER_MODULES = [
17
18
  pipeline: opts.pipeline,
18
19
  outbox: opts.outbox,
19
20
  signingSecret: opts.secret,
21
+ uiCallbackTokenStore: opts.uiCallbackTokenStore,
20
22
  }),
21
23
  },
22
24
  {
@@ -25,6 +27,7 @@ const STATIC_ADAPTER_MODULES = [
25
27
  create: (opts) => new NeovimControlPlaneAdapter({
26
28
  pipeline: opts.pipeline,
27
29
  sharedSecret: opts.secret,
30
+ uiCallbackTokenStore: opts.uiCallbackTokenStore,
28
31
  }),
29
32
  },
30
33
  ];
@@ -69,6 +72,7 @@ export function createStaticAdaptersFromDetected(opts) {
69
72
  outbox: opts.outbox,
70
73
  secret: detected.secret,
71
74
  config: opts.config,
75
+ uiCallbackTokenStore: opts.uiCallbackTokenStore,
72
76
  }));
73
77
  }
74
78
  return adapters;
@@ -1,10 +1,11 @@
1
- import { ControlPlaneCommandPipeline, ControlPlaneOutbox, type ControlPlaneSignalObserver, type ReloadableGenerationIdentity, TelegramControlPlaneAdapter } from "@femtomc/mu-control-plane";
1
+ import { ControlPlaneCommandPipeline, ControlPlaneOutbox, type ControlPlaneSignalObserver, type ReloadableGenerationIdentity, TelegramControlPlaneAdapter, UiCallbackTokenStore } from "@femtomc/mu-control-plane";
2
2
  import type { ControlPlaneConfig, TelegramGenerationReloadResult, TelegramGenerationSwapHooks } from "./control_plane_contract.js";
3
3
  export declare class TelegramAdapterGenerationManager {
4
4
  #private;
5
5
  constructor(opts: {
6
6
  pipeline: ControlPlaneCommandPipeline;
7
7
  outbox: ControlPlaneOutbox;
8
+ uiCallbackTokenStore: UiCallbackTokenStore;
8
9
  initialConfig: ControlPlaneConfig;
9
10
  nowMs?: () => number;
10
11
  onOutboxEnqueued?: () => void;
@@ -79,6 +79,7 @@ async function runWithTimeout(opts) {
79
79
  export class TelegramAdapterGenerationManager {
80
80
  #pipeline;
81
81
  #outbox;
82
+ #uiCallbackTokenStore;
82
83
  #nowMs;
83
84
  #onOutboxEnqueued;
84
85
  #signalObserver;
@@ -90,6 +91,7 @@ export class TelegramAdapterGenerationManager {
90
91
  constructor(opts) {
91
92
  this.#pipeline = opts.pipeline;
92
93
  this.#outbox = opts.outbox;
94
+ this.#uiCallbackTokenStore = opts.uiCallbackTokenStore;
93
95
  this.#nowMs = opts.nowMs ?? Date.now;
94
96
  this.#onOutboxEnqueued = opts.onOutboxEnqueued ?? null;
95
97
  this.#signalObserver = opts.signalObserver ?? null;
@@ -115,6 +117,7 @@ export class TelegramAdapterGenerationManager {
115
117
  acceptIngress: opts.acceptIngress,
116
118
  ingressDrainEnabled: opts.ingressDrainEnabled,
117
119
  nowMs: this.#nowMs,
120
+ uiCallbackTokenStore: this.#uiCallbackTokenStore,
118
121
  });
119
122
  }
120
123
  async initialize() {
@@ -13,7 +13,7 @@ export function createServerRequestHandler(deps) {
13
13
  if (request.method === "OPTIONS") {
14
14
  return new Response(null, { status: 204, headers });
15
15
  }
16
- if (path === "/healthz" || path === "/health") {
16
+ if (path === "/healthz") {
17
17
  return new Response("ok", { status: 200, headers });
18
18
  }
19
19
  if (path === "/api/server/shutdown") {
package/package.json CHANGED
@@ -1,37 +1,37 @@
1
1
  {
2
- "name": "@femtomc/mu-server",
3
- "version": "26.2.106",
4
- "description": "HTTP API server for mu control-plane transport/session plus run/activity scheduling coordination.",
5
- "keywords": [
6
- "mu",
7
- "server",
8
- "api",
9
- "web",
10
- "automation"
11
- ],
12
- "type": "module",
13
- "main": "./dist/index.js",
14
- "types": "./dist/index.d.ts",
15
- "bin": {
16
- "mu-server": "./dist/cli.js"
17
- },
18
- "exports": {
19
- ".": {
20
- "types": "./dist/index.d.ts",
21
- "default": "./dist/index.js"
22
- }
23
- },
24
- "files": [
25
- "dist/**"
26
- ],
27
- "scripts": {
28
- "build": "tsc -p tsconfig.build.json",
29
- "test": "bun test",
30
- "start": "bun run dist/cli.js"
31
- },
32
- "dependencies": {
33
- "@femtomc/mu-agent": "26.2.106",
34
- "@femtomc/mu-control-plane": "26.2.106",
35
- "@femtomc/mu-core": "26.2.106"
36
- }
2
+ "name": "@femtomc/mu-server",
3
+ "version": "26.2.108",
4
+ "description": "HTTP API server for mu control-plane transport/session plus run/activity scheduling coordination.",
5
+ "keywords": [
6
+ "mu",
7
+ "server",
8
+ "api",
9
+ "web",
10
+ "automation"
11
+ ],
12
+ "type": "module",
13
+ "main": "./dist/index.js",
14
+ "types": "./dist/index.d.ts",
15
+ "bin": {
16
+ "mu-server": "./dist/cli.js"
17
+ },
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "default": "./dist/index.js"
22
+ }
23
+ },
24
+ "files": [
25
+ "dist/**"
26
+ ],
27
+ "scripts": {
28
+ "build": "tsc -p tsconfig.build.json",
29
+ "test": "bun test",
30
+ "start": "bun run dist/cli.js"
31
+ },
32
+ "dependencies": {
33
+ "@femtomc/mu-agent": "26.2.108",
34
+ "@femtomc/mu-control-plane": "26.2.108",
35
+ "@femtomc/mu-core": "26.2.108"
36
+ }
37
37
  }