@femtomc/mu-server 26.2.36 → 26.2.38

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,114 +1,49 @@
1
+ import { ApprovedCommandBroker, CommandContextResolver, MessagingMetaAgentRuntime, PiMessagingMetaAgentBackend, serveExtensionPaths, } from "@femtomc/mu-agent";
1
2
  import { ControlPlaneCommandPipeline, ControlPlaneOutbox, ControlPlaneOutboxDispatcher, ControlPlaneRuntime, DiscordControlPlaneAdapter, getControlPlanePaths, SlackControlPlaneAdapter, TelegramControlPlaneAdapter, } from "@femtomc/mu-control-plane";
2
- import { ApprovedCommandBroker, CommandContextResolver, MessagingMetaAgentRuntime, PiMessagingMetaAgentBackend, } from "@femtomc/mu-agent";
3
- export const ENV_VARS = {
4
- slack: { signingSecret: "MU_SLACK_SIGNING_SECRET" },
5
- discord: { signingSecret: "MU_DISCORD_SIGNING_SECRET" },
6
- telegram: {
7
- webhookSecret: "MU_TELEGRAM_WEBHOOK_SECRET",
8
- botToken: "MU_TELEGRAM_BOT_TOKEN",
9
- botUsername: "MU_TELEGRAM_BOT_USERNAME",
10
- tenantId: "MU_TELEGRAM_TENANT_ID",
11
- },
12
- metaAgent: {
13
- enabled: "MU_META_AGENT_ENABLED",
14
- enabledChannels: "MU_META_AGENT_ENABLED_CHANNELS",
15
- runTriggersEnabled: "MU_META_AGENT_RUN_TRIGGERS_ENABLED",
16
- provider: "MU_META_AGENT_PROVIDER",
17
- model: "MU_META_AGENT_MODEL",
18
- thinking: "MU_META_AGENT_THINKING",
19
- systemPrompt: "MU_META_AGENT_SYSTEM_PROMPT",
20
- timeoutMs: "MU_META_AGENT_TIMEOUT_MS",
21
- },
22
- };
23
- const ROUTE_MAP = {
24
- slack: "/webhooks/slack",
25
- discord: "/webhooks/discord",
26
- telegram: "/webhooks/telegram",
27
- };
28
- export function detectAdapters(env) {
3
+ import { DEFAULT_MU_CONFIG } from "./config.js";
4
+ export function detectAdapters(config) {
29
5
  const adapters = [];
30
- const slackSecret = env[ENV_VARS.slack.signingSecret];
6
+ const slackSecret = config.adapters.slack.signing_secret;
31
7
  if (slackSecret) {
32
8
  adapters.push({ name: "slack", signingSecret: slackSecret });
33
9
  }
34
- const discordSecret = env[ENV_VARS.discord.signingSecret];
10
+ const discordSecret = config.adapters.discord.signing_secret;
35
11
  if (discordSecret) {
36
12
  adapters.push({ name: "discord", signingSecret: discordSecret });
37
13
  }
38
- const telegramSecret = env[ENV_VARS.telegram.webhookSecret];
14
+ const telegramSecret = config.adapters.telegram.webhook_secret;
39
15
  if (telegramSecret) {
40
16
  adapters.push({
41
17
  name: "telegram",
42
18
  webhookSecret: telegramSecret,
43
- botToken: env[ENV_VARS.telegram.botToken] ?? null,
44
- botUsername: env[ENV_VARS.telegram.botUsername] ?? null,
45
- tenantId: env[ENV_VARS.telegram.tenantId] ?? null,
19
+ botToken: config.adapters.telegram.bot_token,
20
+ botUsername: config.adapters.telegram.bot_username,
46
21
  });
47
22
  }
48
23
  return adapters;
49
24
  }
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
25
  function buildMessagingMetaAgentRuntime(opts) {
84
- const enabled = parseBooleanEnv(opts.env[ENV_VARS.metaAgent.enabled], true);
85
- if (!enabled) {
26
+ if (!opts.config.meta_agent.enabled) {
86
27
  return null;
87
28
  }
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
29
  const backend = opts.backend ??
92
30
  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,
31
+ provider: opts.config.meta_agent.provider ?? undefined,
32
+ model: opts.config.meta_agent.model ?? undefined,
33
+ extensionPaths: serveExtensionPaths,
98
34
  });
99
35
  return new MessagingMetaAgentRuntime({
100
36
  backend,
101
37
  broker: new ApprovedCommandBroker({
102
- runTriggersEnabled,
38
+ runTriggersEnabled: opts.config.meta_agent.run_triggers_enabled,
103
39
  contextResolver: new CommandContextResolver({ allowedRepoRoots: [opts.repoRoot] }),
104
40
  }),
105
- enabled,
106
- enabledChannels,
41
+ enabled: true,
107
42
  });
108
43
  }
109
44
  export async function bootstrapControlPlane(opts) {
110
- const env = opts.env ?? process.env;
111
- const detected = detectAdapters(env);
45
+ const controlPlaneConfig = opts.config ?? DEFAULT_MU_CONFIG.control_plane;
46
+ const detected = detectAdapters(controlPlaneConfig);
112
47
  if (detected.length === 0) {
113
48
  return null;
114
49
  }
@@ -119,18 +54,16 @@ export async function bootstrapControlPlane(opts) {
119
54
  ? opts.metaAgentRuntime
120
55
  : buildMessagingMetaAgentRuntime({
121
56
  repoRoot: opts.repoRoot,
122
- env,
57
+ config: controlPlaneConfig,
123
58
  backend: opts.metaAgentBackend,
124
59
  });
125
60
  const pipeline = new ControlPlaneCommandPipeline({ runtime, metaAgent });
126
61
  await pipeline.start();
127
62
  const outbox = new ControlPlaneOutbox(paths.outboxPath);
128
63
  await outbox.load();
129
- // Collect bot tokens for delivery.
130
- const telegramBotTokens = new Map();
64
+ let telegramBotToken = null;
131
65
  const adapterMap = new Map();
132
66
  for (const d of detected) {
133
- const route = ROUTE_MAP[d.name];
134
67
  let adapter;
135
68
  switch (d.name) {
136
69
  case "slack":
@@ -153,25 +86,31 @@ export async function bootstrapControlPlane(opts) {
153
86
  outbox,
154
87
  webhookSecret: d.webhookSecret,
155
88
  botUsername: d.botUsername ?? undefined,
156
- tenantId: d.tenantId ?? undefined,
157
89
  });
158
90
  if (d.botToken) {
159
- telegramBotTokens.set(d.tenantId ?? "default", d.botToken);
91
+ telegramBotToken = d.botToken;
160
92
  }
161
93
  break;
162
94
  }
163
- adapterMap.set(route, { adapter, info: { name: d.name, route } });
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
+ });
164
106
  }
165
- // Build delivery handler that routes by channel.
166
107
  const deliver = async (record) => {
167
108
  const { envelope } = record;
168
109
  if (envelope.channel === "telegram") {
169
- const tenantId = envelope.channel_tenant_id ?? "default";
170
- const botToken = telegramBotTokens.get(tenantId) ?? telegramBotTokens.values().next().value;
171
- if (!botToken) {
172
- return { kind: "retry", error: "MU_TELEGRAM_BOT_TOKEN not configured" };
110
+ if (!telegramBotToken) {
111
+ return { kind: "retry", error: "telegram bot token not configured in .mu/config.json" };
173
112
  }
174
- const res = await fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, {
113
+ const res = await fetch(`https://api.telegram.org/bot${telegramBotToken}/sendMessage`, {
175
114
  method: "POST",
176
115
  headers: { "Content-Type": "application/json" },
177
116
  body: JSON.stringify({
@@ -191,17 +130,14 @@ export async function bootstrapControlPlane(opts) {
191
130
  retryDelayMs: retryDelayMs && Number.isFinite(retryDelayMs) ? retryDelayMs : undefined,
192
131
  };
193
132
  }
194
- // Permanent error — let it dead-letter after max attempts.
195
133
  return {
196
134
  kind: "retry",
197
135
  error: `telegram sendMessage ${res.status}: ${await res.text().catch(() => "")}`,
198
136
  };
199
137
  }
200
- // Other channels: treat as delivered (no-op for now).
201
138
  return undefined;
202
139
  };
203
140
  const dispatcher = new ControlPlaneOutboxDispatcher({ outbox, deliver });
204
- // Drain loop: check for pending outbox records every 2 seconds.
205
141
  const drainInterval = setInterval(async () => {
206
142
  try {
207
143
  await dispatcher.drainDue();
package/dist/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
- export type { ServerOptions, ServerContext, ServerWithControlPlane } from "./server.js";
2
- export { createServer, createServerAsync, createContext } from "./server.js";
3
- export type { ControlPlaneHandle, ActiveAdapter } from "./control_plane.js";
4
- export { bootstrapControlPlane, detectAdapters, ENV_VARS } from "./control_plane.js";
1
+ export type { MuConfig, MuConfigPatch, MuConfigPresence } from "./config.js";
2
+ export { applyMuConfigPatch, DEFAULT_MU_CONFIG, getMuConfigPath, muConfigPresence, normalizeMuConfig, readMuConfigFile, redactMuConfigSecrets, writeMuConfigFile, } from "./config.js";
3
+ export type { ActiveAdapter, ControlPlaneConfig, ControlPlaneHandle } from "./control_plane.js";
4
+ export { bootstrapControlPlane, detectAdapters } from "./control_plane.js";
5
+ export type { ServerContext, ServerOptions, ServerWithControlPlane } from "./server.js";
6
+ export { createContext, createServer, createServerAsync } from "./server.js";
package/dist/index.js CHANGED
@@ -1,2 +1,3 @@
1
- export { createServer, createServerAsync, createContext } from "./server.js";
2
- export { bootstrapControlPlane, detectAdapters, ENV_VARS } from "./control_plane.js";
1
+ export { applyMuConfigPatch, DEFAULT_MU_CONFIG, getMuConfigPath, muConfigPresence, normalizeMuConfig, readMuConfigFile, redactMuConfigSecrets, writeMuConfigFile, } from "./config.js";
2
+ export { bootstrapControlPlane, detectAdapters } from "./control_plane.js";
3
+ export { createContext, createServer, createServerAsync } from "./server.js";
package/dist/server.d.ts CHANGED
@@ -1,28 +1,42 @@
1
+ import type { EventEnvelope, JsonlStore } from "@femtomc/mu-core";
1
2
  import { EventLog } from "@femtomc/mu-core/node";
2
- import { IssueStore } from "@femtomc/mu-issue";
3
3
  import { ForumStore } from "@femtomc/mu-forum";
4
- import { type ControlPlaneHandle } from "./control_plane.js";
4
+ import { IssueStore } from "@femtomc/mu-issue";
5
+ import { type MuConfig } from "./config.js";
6
+ import { type ControlPlaneConfig, type ControlPlaneHandle } from "./control_plane.js";
7
+ type ControlPlaneReloader = (opts: {
8
+ repoRoot: string;
9
+ previous: ControlPlaneHandle | null;
10
+ config: ControlPlaneConfig;
11
+ }) => Promise<ControlPlaneHandle | null>;
12
+ type ConfigReader = (repoRoot: string) => Promise<MuConfig>;
13
+ type ConfigWriter = (repoRoot: string, config: MuConfig) => Promise<string>;
5
14
  export type ServerOptions = {
6
15
  repoRoot?: string;
7
16
  port?: number;
8
17
  controlPlane?: ControlPlaneHandle | null;
18
+ controlPlaneReloader?: ControlPlaneReloader;
19
+ config?: MuConfig;
20
+ configReader?: ConfigReader;
21
+ configWriter?: ConfigWriter;
9
22
  };
10
23
  export type ServerContext = {
11
24
  repoRoot: string;
12
25
  issueStore: IssueStore;
13
26
  forumStore: ForumStore;
14
27
  eventLog: EventLog;
28
+ eventsStore: JsonlStore<EventEnvelope>;
15
29
  };
16
30
  export declare function createContext(repoRoot: string): ServerContext;
17
31
  export declare function createServer(options?: ServerOptions): {
18
32
  port: number;
19
33
  fetch: (request: Request) => Promise<Response>;
20
34
  hostname: string;
35
+ controlPlane: ControlPlaneHandle;
21
36
  };
22
37
  export type ServerWithControlPlane = {
23
38
  serverConfig: ReturnType<typeof createServer>;
24
39
  controlPlane: ControlPlaneHandle | null;
25
40
  };
26
- export declare function createServerAsync(options?: Omit<ServerOptions, "controlPlane"> & {
27
- env?: Record<string, string | undefined>;
28
- }): Promise<ServerWithControlPlane>;
41
+ export declare function createServerAsync(options?: Omit<ServerOptions, "controlPlane">): Promise<ServerWithControlPlane>;
42
+ export {};
package/dist/server.js CHANGED
@@ -1,11 +1,11 @@
1
- import { existsSync } from "node:fs";
2
- import { readFile } from "node:fs/promises";
3
1
  import { extname, join, resolve } from "node:path";
4
- import { fsEventLogFromRepoRoot, FsJsonlStore, getStorePaths } from "@femtomc/mu-core/node";
5
- import { IssueStore } from "@femtomc/mu-issue";
2
+ import { currentRunId, EventLog, FsJsonlStore, getStorePaths, JsonlEventSink } from "@femtomc/mu-core/node";
6
3
  import { ForumStore } from "@femtomc/mu-forum";
7
- import { issueRoutes } from "./api/issues.js";
4
+ import { IssueStore } from "@femtomc/mu-issue";
5
+ import { eventRoutes } from "./api/events.js";
8
6
  import { forumRoutes } from "./api/forum.js";
7
+ import { issueRoutes } from "./api/issues.js";
8
+ import { applyMuConfigPatch, DEFAULT_MU_CONFIG, getMuConfigPath, muConfigPresence, readMuConfigFile, redactMuConfigSecrets, writeMuConfigFile, } from "./config.js";
9
9
  import { bootstrapControlPlane } from "./control_plane.js";
10
10
  const MIME_TYPES = {
11
11
  ".html": "text/html; charset=utf-8",
@@ -21,91 +21,243 @@ const MIME_TYPES = {
21
21
  };
22
22
  // Resolve public/ dir relative to this file (works in npm global installs)
23
23
  const PUBLIC_DIR = join(new URL(".", import.meta.url).pathname, "..", "public");
24
+ function describeError(err) {
25
+ if (err instanceof Error)
26
+ return err.message;
27
+ return String(err);
28
+ }
29
+ function summarizeControlPlane(handle) {
30
+ if (!handle) {
31
+ return { active: false, adapters: [], routes: [] };
32
+ }
33
+ return {
34
+ active: handle.activeAdapters.length > 0,
35
+ adapters: handle.activeAdapters.map((adapter) => adapter.name),
36
+ routes: handle.activeAdapters.map((adapter) => ({ name: adapter.name, route: adapter.route })),
37
+ };
38
+ }
24
39
  export function createContext(repoRoot) {
25
40
  const paths = getStorePaths(repoRoot);
26
- const eventLog = fsEventLogFromRepoRoot(repoRoot);
41
+ const eventsStore = new FsJsonlStore(paths.eventsPath);
42
+ const eventLog = new EventLog(new JsonlEventSink(eventsStore), {
43
+ runIdProvider: currentRunId,
44
+ });
27
45
  const issueStore = new IssueStore(new FsJsonlStore(paths.issuesPath), { events: eventLog });
28
46
  const forumStore = new ForumStore(new FsJsonlStore(paths.forumPath), { events: eventLog });
29
- return { repoRoot, issueStore, forumStore, eventLog };
47
+ return { repoRoot, issueStore, forumStore, eventLog, eventsStore };
30
48
  }
31
49
  export function createServer(options = {}) {
32
50
  const repoRoot = options.repoRoot || process.cwd();
33
51
  const context = createContext(repoRoot);
34
- const controlPlane = options.controlPlane ?? null;
52
+ const readConfig = options.configReader ?? readMuConfigFile;
53
+ const writeConfig = options.configWriter ?? writeMuConfigFile;
54
+ const fallbackConfig = options.config ?? DEFAULT_MU_CONFIG;
55
+ let controlPlaneCurrent = options.controlPlane ?? null;
56
+ let reloadInFlight = null;
57
+ const controlPlaneReloader = options.controlPlaneReloader ??
58
+ (async ({ repoRoot, config }) => {
59
+ return await bootstrapControlPlane({ repoRoot, config });
60
+ });
61
+ const controlPlaneProxy = {
62
+ get activeAdapters() {
63
+ return controlPlaneCurrent?.activeAdapters ?? [];
64
+ },
65
+ async handleWebhook(path, req) {
66
+ const handle = controlPlaneCurrent;
67
+ if (!handle)
68
+ return null;
69
+ return await handle.handleWebhook(path, req);
70
+ },
71
+ async stop() {
72
+ const handle = controlPlaneCurrent;
73
+ controlPlaneCurrent = null;
74
+ await handle?.stop();
75
+ },
76
+ };
77
+ const loadConfigFromDisk = async () => {
78
+ try {
79
+ return await readConfig(context.repoRoot);
80
+ }
81
+ catch (err) {
82
+ if (err?.code === "ENOENT") {
83
+ return fallbackConfig;
84
+ }
85
+ throw err;
86
+ }
87
+ };
88
+ const performControlPlaneReload = async (reason) => {
89
+ const previous = controlPlaneCurrent;
90
+ const previousSummary = summarizeControlPlane(previous);
91
+ try {
92
+ const latestConfig = await loadConfigFromDisk();
93
+ const next = await controlPlaneReloader({
94
+ repoRoot: context.repoRoot,
95
+ previous,
96
+ config: latestConfig.control_plane,
97
+ });
98
+ controlPlaneCurrent = next;
99
+ if (previous && previous !== next) {
100
+ await previous.stop();
101
+ }
102
+ return {
103
+ ok: true,
104
+ reason,
105
+ previous_control_plane: previousSummary,
106
+ control_plane: summarizeControlPlane(next),
107
+ };
108
+ }
109
+ catch (err) {
110
+ return {
111
+ ok: false,
112
+ reason,
113
+ previous_control_plane: previousSummary,
114
+ control_plane: summarizeControlPlane(previous),
115
+ error: describeError(err),
116
+ };
117
+ }
118
+ };
119
+ const reloadControlPlane = async (reason) => {
120
+ if (reloadInFlight) {
121
+ return await reloadInFlight;
122
+ }
123
+ reloadInFlight = performControlPlaneReload(reason).finally(() => {
124
+ reloadInFlight = null;
125
+ });
126
+ return await reloadInFlight;
127
+ };
35
128
  const handleRequest = async (request) => {
36
129
  const url = new URL(request.url);
37
130
  const path = url.pathname;
38
- // CORS headers for development
39
131
  const headers = new Headers({
40
132
  "Access-Control-Allow-Origin": "*",
41
133
  "Access-Control-Allow-Methods": "GET, POST, PATCH, DELETE, OPTIONS",
42
134
  "Access-Control-Allow-Headers": "Content-Type",
43
135
  });
44
- // Handle preflight requests
45
136
  if (request.method === "OPTIONS") {
46
137
  return new Response(null, { status: 204, headers });
47
138
  }
48
- // Health check
49
139
  if (path === "/healthz" || path === "/health") {
50
140
  return new Response("ok", { status: 200, headers });
51
141
  }
52
- // Status endpoint
142
+ if (path === "/api/config") {
143
+ if (request.method === "GET") {
144
+ try {
145
+ const config = await loadConfigFromDisk();
146
+ return Response.json({
147
+ repo_root: context.repoRoot,
148
+ config_path: getMuConfigPath(context.repoRoot),
149
+ config: redactMuConfigSecrets(config),
150
+ presence: muConfigPresence(config),
151
+ }, { headers });
152
+ }
153
+ catch (err) {
154
+ return Response.json({ error: `failed to read config: ${describeError(err)}` }, { status: 500, headers });
155
+ }
156
+ }
157
+ if (request.method === "POST") {
158
+ let body;
159
+ try {
160
+ body = (await request.json());
161
+ }
162
+ catch {
163
+ return Response.json({ error: "invalid json body" }, { status: 400, headers });
164
+ }
165
+ if (!body || !("patch" in body)) {
166
+ return Response.json({ error: "missing patch payload" }, { status: 400, headers });
167
+ }
168
+ try {
169
+ const base = await loadConfigFromDisk();
170
+ const next = applyMuConfigPatch(base, body.patch);
171
+ const configPath = await writeConfig(context.repoRoot, next);
172
+ return Response.json({
173
+ ok: true,
174
+ repo_root: context.repoRoot,
175
+ config_path: configPath,
176
+ config: redactMuConfigSecrets(next),
177
+ presence: muConfigPresence(next),
178
+ }, { headers });
179
+ }
180
+ catch (err) {
181
+ return Response.json({ error: `failed to write config: ${describeError(err)}` }, { status: 500, headers });
182
+ }
183
+ }
184
+ return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
185
+ }
186
+ if (path === "/api/control-plane/reload") {
187
+ if (request.method !== "POST") {
188
+ return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
189
+ }
190
+ let reason = "api_control_plane_reload";
191
+ try {
192
+ const body = (await request.json());
193
+ if (typeof body.reason === "string" && body.reason.trim().length > 0) {
194
+ reason = body.reason.trim();
195
+ }
196
+ }
197
+ catch {
198
+ // ignore invalid body for reason
199
+ }
200
+ const result = await reloadControlPlane(reason);
201
+ return Response.json(result, { status: result.ok ? 200 : 500, headers });
202
+ }
53
203
  if (path === "/api/status") {
54
204
  const issues = await context.issueStore.list();
55
- const openIssues = issues.filter(i => i.status === "open");
205
+ const openIssues = issues.filter((i) => i.status === "open");
56
206
  const readyIssues = await context.issueStore.ready();
207
+ const controlPlane = summarizeControlPlane(controlPlaneCurrent);
57
208
  return Response.json({
58
209
  repo_root: context.repoRoot,
59
210
  open_count: openIssues.length,
60
211
  ready_count: readyIssues.length,
61
- control_plane: controlPlane
62
- ? { active: true, adapters: controlPlane.activeAdapters.map(a => a.name) }
63
- : { active: false, adapters: [] },
212
+ control_plane: controlPlane,
64
213
  }, { headers });
65
214
  }
66
- // Issue routes
67
215
  if (path.startsWith("/api/issues")) {
68
216
  const response = await issueRoutes(request, context);
69
- // Add CORS headers to the response
70
- headers.forEach((value, key) => response.headers.set(key, value));
217
+ headers.forEach((value, key) => {
218
+ response.headers.set(key, value);
219
+ });
71
220
  return response;
72
221
  }
73
- // Forum routes
74
222
  if (path.startsWith("/api/forum")) {
75
223
  const response = await forumRoutes(request, context);
76
- // Add CORS headers to the response
77
- headers.forEach((value, key) => response.headers.set(key, value));
224
+ headers.forEach((value, key) => {
225
+ response.headers.set(key, value);
226
+ });
78
227
  return response;
79
228
  }
80
- // Webhook routes (control plane)
81
- if (path.startsWith("/webhooks/") && controlPlane) {
82
- const response = await controlPlane.handleWebhook(path, request);
229
+ if (path.startsWith("/api/events")) {
230
+ const response = await eventRoutes(request, context);
231
+ headers.forEach((value, key) => {
232
+ response.headers.set(key, value);
233
+ });
234
+ return response;
235
+ }
236
+ if (path.startsWith("/webhooks/")) {
237
+ const response = await controlPlaneProxy.handleWebhook(path, request);
83
238
  if (response) {
84
- headers.forEach((value, key) => response.headers.set(key, value));
239
+ headers.forEach((value, key) => {
240
+ response.headers.set(key, value);
241
+ });
85
242
  return response;
86
243
  }
87
244
  }
88
- // Static file serving (bundled web UI)
89
- if (existsSync(PUBLIC_DIR)) {
90
- // Try to serve the exact file (with path traversal protection)
91
- const filePath = resolve(PUBLIC_DIR, `.${path === "/" ? "/index.html" : path}`);
92
- if (!filePath.startsWith(PUBLIC_DIR)) {
93
- return new Response("Forbidden", { status: 403, headers });
94
- }
95
- if (existsSync(filePath)) {
96
- const ext = extname(filePath);
97
- const mime = MIME_TYPES[ext] ?? "application/octet-stream";
98
- const body = await readFile(filePath);
99
- headers.set("Content-Type", mime);
100
- return new Response(body, { status: 200, headers });
101
- }
102
- // SPA fallback: serve index.html for non-API, non-file paths
103
- const indexPath = join(PUBLIC_DIR, "index.html");
104
- if (existsSync(indexPath)) {
105
- const body = await readFile(indexPath);
106
- headers.set("Content-Type", "text/html; charset=utf-8");
107
- return new Response(body, { status: 200, headers });
108
- }
245
+ const filePath = resolve(PUBLIC_DIR, `.${path === "/" ? "/index.html" : path}`);
246
+ if (!filePath.startsWith(PUBLIC_DIR)) {
247
+ return new Response("Forbidden", { status: 403, headers });
248
+ }
249
+ const file = Bun.file(filePath);
250
+ if (await file.exists()) {
251
+ const ext = extname(filePath);
252
+ const mime = MIME_TYPES[ext] ?? "application/octet-stream";
253
+ headers.set("Content-Type", mime);
254
+ return new Response(await file.arrayBuffer(), { status: 200, headers });
255
+ }
256
+ const indexPath = join(PUBLIC_DIR, "index.html");
257
+ const indexFile = Bun.file(indexPath);
258
+ if (await indexFile.exists()) {
259
+ headers.set("Content-Type", "text/html; charset=utf-8");
260
+ return new Response(await indexFile.arrayBuffer(), { status: 200, headers });
109
261
  }
110
262
  return new Response("Not Found", { status: 404, headers });
111
263
  };
@@ -113,12 +265,17 @@ export function createServer(options = {}) {
113
265
  port: options.port || 3000,
114
266
  fetch: handleRequest,
115
267
  hostname: "0.0.0.0",
268
+ controlPlane: controlPlaneProxy,
116
269
  };
117
270
  return server;
118
271
  }
119
272
  export async function createServerAsync(options = {}) {
120
273
  const repoRoot = options.repoRoot || process.cwd();
121
- const controlPlane = await bootstrapControlPlane({ repoRoot, env: options.env });
122
- const serverConfig = createServer({ ...options, controlPlane });
123
- return { serverConfig, controlPlane };
274
+ const config = options.config ?? (await readMuConfigFile(repoRoot));
275
+ const controlPlane = await bootstrapControlPlane({ repoRoot, config: config.control_plane });
276
+ const serverConfig = createServer({ ...options, controlPlane, config });
277
+ return {
278
+ serverConfig,
279
+ controlPlane: serverConfig.controlPlane,
280
+ };
124
281
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@femtomc/mu-server",
3
- "version": "26.2.36",
3
+ "version": "26.2.38",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -23,10 +23,10 @@
23
23
  "start": "bun run dist/cli.js"
24
24
  },
25
25
  "dependencies": {
26
- "@femtomc/mu-agent": "26.2.36",
27
- "@femtomc/mu-control-plane": "26.2.36",
28
- "@femtomc/mu-core": "26.2.36",
29
- "@femtomc/mu-forum": "26.2.36",
30
- "@femtomc/mu-issue": "26.2.36"
26
+ "@femtomc/mu-agent": "26.2.38",
27
+ "@femtomc/mu-control-plane": "26.2.38",
28
+ "@femtomc/mu-core": "26.2.38",
29
+ "@femtomc/mu-forum": "26.2.38",
30
+ "@femtomc/mu-issue": "26.2.38"
31
31
  }
32
32
  }