@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.
- package/README.md +29 -1
- package/dist/api/events.d.ts +2 -0
- package/dist/api/events.js +45 -0
- package/dist/api/forum.js +2 -2
- package/dist/api/issues.js +5 -5
- package/dist/cli.js +3 -3
- package/dist/config.d.ts +99 -0
- package/dist/config.js +360 -0
- package/dist/control_plane.d.ts +5 -28
- package/dist/control_plane.js +33 -97
- package/dist/index.d.ts +6 -4
- package/dist/index.js +3 -2
- package/dist/server.d.ts +19 -5
- package/dist/server.js +207 -50
- package/package.json +6 -6
- package/public/assets/index-CxkevQNh.js +100 -0
- package/public/index.html +1 -1
- package/public/assets/index-0FGFtKeu.js +0 -85
package/dist/control_plane.js
CHANGED
|
@@ -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 {
|
|
3
|
-
export
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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:
|
|
44
|
-
botUsername:
|
|
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
|
-
|
|
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.
|
|
94
|
-
model: opts.
|
|
95
|
-
|
|
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
|
|
111
|
-
const detected = detectAdapters(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
91
|
+
telegramBotToken = d.botToken;
|
|
160
92
|
}
|
|
161
93
|
break;
|
|
162
94
|
}
|
|
163
|
-
|
|
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
|
-
|
|
170
|
-
|
|
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${
|
|
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 {
|
|
2
|
-
export {
|
|
3
|
-
export type {
|
|
4
|
-
export { bootstrapControlPlane, detectAdapters
|
|
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 {
|
|
2
|
-
export { bootstrapControlPlane, detectAdapters
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
70
|
-
|
|
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
|
-
|
|
77
|
-
|
|
224
|
+
headers.forEach((value, key) => {
|
|
225
|
+
response.headers.set(key, value);
|
|
226
|
+
});
|
|
78
227
|
return response;
|
|
79
228
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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) =>
|
|
239
|
+
headers.forEach((value, key) => {
|
|
240
|
+
response.headers.set(key, value);
|
|
241
|
+
});
|
|
85
242
|
return response;
|
|
86
243
|
}
|
|
87
244
|
}
|
|
88
|
-
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
122
|
-
const
|
|
123
|
-
|
|
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.
|
|
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.
|
|
27
|
-
"@femtomc/mu-control-plane": "26.2.
|
|
28
|
-
"@femtomc/mu-core": "26.2.
|
|
29
|
-
"@femtomc/mu-forum": "26.2.
|
|
30
|
-
"@femtomc/mu-issue": "26.2.
|
|
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
|
}
|