@femtomc/mu-server 26.2.30 → 26.2.32

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.
@@ -1,4 +1,4 @@
1
- import { type Channel } from "@femtomc/mu-control-plane";
1
+ import { type Channel, type MessagingMetaAgentBackend, MessagingMetaAgentRuntime } from "@femtomc/mu-control-plane";
2
2
  export declare const ENV_VARS: {
3
3
  readonly slack: {
4
4
  readonly signingSecret: "MU_SLACK_SIGNING_SECRET";
@@ -8,9 +8,21 @@ export declare const ENV_VARS: {
8
8
  };
9
9
  readonly telegram: {
10
10
  readonly webhookSecret: "MU_TELEGRAM_WEBHOOK_SECRET";
11
+ readonly botToken: "MU_TELEGRAM_BOT_TOKEN";
11
12
  readonly botUsername: "MU_TELEGRAM_BOT_USERNAME";
12
13
  readonly tenantId: "MU_TELEGRAM_TENANT_ID";
13
14
  };
15
+ readonly metaAgent: {
16
+ readonly enabled: "MU_META_AGENT_ENABLED";
17
+ readonly enabledChannels: "MU_META_AGENT_ENABLED_CHANNELS";
18
+ readonly runTriggersEnabled: "MU_META_AGENT_RUN_TRIGGERS_ENABLED";
19
+ readonly provider: "MU_META_AGENT_PROVIDER";
20
+ readonly model: "MU_META_AGENT_MODEL";
21
+ readonly thinking: "MU_META_AGENT_THINKING";
22
+ readonly systemPrompt: "MU_META_AGENT_SYSTEM_PROMPT";
23
+ readonly timeoutMs: "MU_META_AGENT_TIMEOUT_MS";
24
+ readonly piBinary: "MU_META_AGENT_PI_BINARY";
25
+ };
14
26
  };
15
27
  export type ActiveAdapter = {
16
28
  name: Channel;
@@ -30,12 +42,16 @@ type DetectedAdapter = {
30
42
  } | {
31
43
  name: "telegram";
32
44
  webhookSecret: string;
45
+ botToken: string | null;
33
46
  botUsername: string | null;
34
47
  tenantId: string | null;
35
48
  };
36
49
  export declare function detectAdapters(env: Record<string, string | undefined>): DetectedAdapter[];
37
- export declare function bootstrapControlPlane(opts: {
50
+ export type BootstrapControlPlaneOpts = {
38
51
  repoRoot: string;
39
52
  env?: Record<string, string | undefined>;
40
- }): Promise<ControlPlaneHandle | null>;
53
+ metaAgentRuntime?: MessagingMetaAgentRuntime | null;
54
+ metaAgentBackend?: MessagingMetaAgentBackend;
55
+ };
56
+ export declare function bootstrapControlPlane(opts: BootstrapControlPlaneOpts): Promise<ControlPlaneHandle | null>;
41
57
  export {};
@@ -1,12 +1,24 @@
1
- import { ControlPlaneRuntime, ControlPlaneCommandPipeline, ControlPlaneOutbox, SlackControlPlaneAdapter, DiscordControlPlaneAdapter, TelegramControlPlaneAdapter, getControlPlanePaths, } from "@femtomc/mu-control-plane";
1
+ import { ApprovedCommandBroker, CommandContextResolver, ControlPlaneCommandPipeline, ControlPlaneOutbox, ControlPlaneOutboxDispatcher, ControlPlaneRuntime, DiscordControlPlaneAdapter, getControlPlanePaths, MessagingMetaAgentRuntime, PiMessagingMetaAgentBackend, SlackControlPlaneAdapter, TelegramControlPlaneAdapter, } from "@femtomc/mu-control-plane";
2
2
  export const ENV_VARS = {
3
3
  slack: { signingSecret: "MU_SLACK_SIGNING_SECRET" },
4
4
  discord: { signingSecret: "MU_DISCORD_SIGNING_SECRET" },
5
5
  telegram: {
6
6
  webhookSecret: "MU_TELEGRAM_WEBHOOK_SECRET",
7
+ botToken: "MU_TELEGRAM_BOT_TOKEN",
7
8
  botUsername: "MU_TELEGRAM_BOT_USERNAME",
8
9
  tenantId: "MU_TELEGRAM_TENANT_ID",
9
10
  },
11
+ metaAgent: {
12
+ enabled: "MU_META_AGENT_ENABLED",
13
+ enabledChannels: "MU_META_AGENT_ENABLED_CHANNELS",
14
+ runTriggersEnabled: "MU_META_AGENT_RUN_TRIGGERS_ENABLED",
15
+ provider: "MU_META_AGENT_PROVIDER",
16
+ model: "MU_META_AGENT_MODEL",
17
+ thinking: "MU_META_AGENT_THINKING",
18
+ systemPrompt: "MU_META_AGENT_SYSTEM_PROMPT",
19
+ timeoutMs: "MU_META_AGENT_TIMEOUT_MS",
20
+ piBinary: "MU_META_AGENT_PI_BINARY",
21
+ },
10
22
  };
11
23
  const ROUTE_MAP = {
12
24
  slack: "/webhooks/slack",
@@ -28,12 +40,73 @@ export function detectAdapters(env) {
28
40
  adapters.push({
29
41
  name: "telegram",
30
42
  webhookSecret: telegramSecret,
43
+ botToken: env[ENV_VARS.telegram.botToken] ?? null,
31
44
  botUsername: env[ENV_VARS.telegram.botUsername] ?? null,
32
45
  tenantId: env[ENV_VARS.telegram.tenantId] ?? null,
33
46
  });
34
47
  }
35
48
  return adapters;
36
49
  }
50
+ function parseBooleanEnv(value, defaultValue) {
51
+ if (value == null) {
52
+ return defaultValue;
53
+ }
54
+ const normalized = value.trim().toLowerCase();
55
+ if (["1", "true", "yes", "on", "enabled"].includes(normalized)) {
56
+ return true;
57
+ }
58
+ if (["0", "false", "no", "off", "disabled"].includes(normalized)) {
59
+ return false;
60
+ }
61
+ return defaultValue;
62
+ }
63
+ function parsePositiveIntEnv(value) {
64
+ if (!value) {
65
+ return undefined;
66
+ }
67
+ const parsed = Number.parseInt(value, 10);
68
+ if (!Number.isFinite(parsed) || parsed <= 0) {
69
+ return undefined;
70
+ }
71
+ return parsed;
72
+ }
73
+ function parseCsvEnv(value) {
74
+ if (!value) {
75
+ return undefined;
76
+ }
77
+ const tokens = value
78
+ .split(",")
79
+ .map((token) => token.trim().toLowerCase())
80
+ .filter((token) => token.length > 0);
81
+ return tokens.length > 0 ? tokens : undefined;
82
+ }
83
+ function buildMessagingMetaAgentRuntime(opts) {
84
+ const enabled = parseBooleanEnv(opts.env[ENV_VARS.metaAgent.enabled], true);
85
+ if (!enabled) {
86
+ return null;
87
+ }
88
+ const runTriggersEnabled = parseBooleanEnv(opts.env[ENV_VARS.metaAgent.runTriggersEnabled], true);
89
+ const enabledChannels = parseCsvEnv(opts.env[ENV_VARS.metaAgent.enabledChannels]);
90
+ const timeoutMs = parsePositiveIntEnv(opts.env[ENV_VARS.metaAgent.timeoutMs]);
91
+ const backend = opts.backend ??
92
+ new PiMessagingMetaAgentBackend({
93
+ provider: opts.env[ENV_VARS.metaAgent.provider],
94
+ model: opts.env[ENV_VARS.metaAgent.model],
95
+ thinking: opts.env[ENV_VARS.metaAgent.thinking],
96
+ systemPrompt: opts.env[ENV_VARS.metaAgent.systemPrompt],
97
+ timeoutMs,
98
+ piBinary: opts.env[ENV_VARS.metaAgent.piBinary],
99
+ });
100
+ return new MessagingMetaAgentRuntime({
101
+ backend,
102
+ broker: new ApprovedCommandBroker({
103
+ runTriggersEnabled,
104
+ contextResolver: new CommandContextResolver({ allowedRepoRoots: [opts.repoRoot] }),
105
+ }),
106
+ enabled,
107
+ enabledChannels,
108
+ });
109
+ }
37
110
  export async function bootstrapControlPlane(opts) {
38
111
  const env = opts.env ?? process.env;
39
112
  const detected = detectAdapters(env);
@@ -43,10 +116,19 @@ export async function bootstrapControlPlane(opts) {
43
116
  const paths = getControlPlanePaths(opts.repoRoot);
44
117
  const runtime = new ControlPlaneRuntime({ repoRoot: opts.repoRoot });
45
118
  await runtime.start();
46
- const pipeline = new ControlPlaneCommandPipeline({ runtime });
119
+ const metaAgent = opts.metaAgentRuntime !== undefined
120
+ ? opts.metaAgentRuntime
121
+ : buildMessagingMetaAgentRuntime({
122
+ repoRoot: opts.repoRoot,
123
+ env,
124
+ backend: opts.metaAgentBackend,
125
+ });
126
+ const pipeline = new ControlPlaneCommandPipeline({ runtime, metaAgent });
47
127
  await pipeline.start();
48
128
  const outbox = new ControlPlaneOutbox(paths.outboxPath);
49
129
  await outbox.load();
130
+ // Collect bot tokens for delivery.
131
+ const telegramBotTokens = new Map();
50
132
  const adapterMap = new Map();
51
133
  for (const d of detected) {
52
134
  const route = ROUTE_MAP[d.name];
@@ -74,10 +156,61 @@ export async function bootstrapControlPlane(opts) {
74
156
  botUsername: d.botUsername ?? undefined,
75
157
  tenantId: d.tenantId ?? undefined,
76
158
  });
159
+ if (d.botToken) {
160
+ telegramBotTokens.set(d.tenantId ?? "default", d.botToken);
161
+ }
77
162
  break;
78
163
  }
79
164
  adapterMap.set(route, { adapter, info: { name: d.name, route } });
80
165
  }
166
+ // Build delivery handler that routes by channel.
167
+ const deliver = async (record) => {
168
+ const { envelope } = record;
169
+ if (envelope.channel === "telegram") {
170
+ const tenantId = envelope.channel_tenant_id ?? "default";
171
+ const botToken = telegramBotTokens.get(tenantId) ?? telegramBotTokens.values().next().value;
172
+ if (!botToken) {
173
+ return { kind: "retry", error: "MU_TELEGRAM_BOT_TOKEN not configured" };
174
+ }
175
+ const res = await fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, {
176
+ method: "POST",
177
+ headers: { "Content-Type": "application/json" },
178
+ body: JSON.stringify({
179
+ chat_id: envelope.channel_conversation_id,
180
+ text: envelope.body,
181
+ }),
182
+ });
183
+ if (res.ok) {
184
+ return { kind: "delivered" };
185
+ }
186
+ if (res.status === 429 || res.status >= 500) {
187
+ const retryAfter = res.headers.get("retry-after");
188
+ const retryDelayMs = retryAfter ? Number.parseInt(retryAfter, 10) * 1000 : undefined;
189
+ return {
190
+ kind: "retry",
191
+ error: `telegram sendMessage ${res.status}: ${await res.text().catch(() => "")}`,
192
+ retryDelayMs: retryDelayMs && Number.isFinite(retryDelayMs) ? retryDelayMs : undefined,
193
+ };
194
+ }
195
+ // Permanent error — let it dead-letter after max attempts.
196
+ return {
197
+ kind: "retry",
198
+ error: `telegram sendMessage ${res.status}: ${await res.text().catch(() => "")}`,
199
+ };
200
+ }
201
+ // Other channels: treat as delivered (no-op for now).
202
+ return undefined;
203
+ };
204
+ const dispatcher = new ControlPlaneOutboxDispatcher({ outbox, deliver });
205
+ // Drain loop: check for pending outbox records every 2 seconds.
206
+ const drainInterval = setInterval(async () => {
207
+ try {
208
+ await dispatcher.drainDue();
209
+ }
210
+ catch {
211
+ // Swallow errors — the dispatcher already handles retries internally.
212
+ }
213
+ }, 2_000);
81
214
  return {
82
215
  activeAdapters: [...adapterMap.values()].map((v) => v.info),
83
216
  async handleWebhook(path, req) {
@@ -88,6 +221,7 @@ export async function bootstrapControlPlane(opts) {
88
221
  return result.response;
89
222
  },
90
223
  async stop() {
224
+ clearInterval(drainInterval);
91
225
  await pipeline.stop();
92
226
  },
93
227
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@femtomc/mu-server",
3
- "version": "26.2.30",
3
+ "version": "26.2.32",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -23,9 +23,9 @@
23
23
  "start": "bun run dist/cli.js"
24
24
  },
25
25
  "dependencies": {
26
- "@femtomc/mu-core": "26.2.30",
27
- "@femtomc/mu-issue": "26.2.30",
28
- "@femtomc/mu-forum": "26.2.30",
29
- "@femtomc/mu-control-plane": "26.2.30"
26
+ "@femtomc/mu-core": "26.2.32",
27
+ "@femtomc/mu-issue": "26.2.32",
28
+ "@femtomc/mu-forum": "26.2.32",
29
+ "@femtomc/mu-control-plane": "26.2.32"
30
30
  }
31
31
  }