agent-relay-server 0.32.2 → 0.32.4

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.
Files changed (42) hide show
  1. package/package.json +2 -2
  2. package/public/assets/display-JI19Vc7L.js.map +1 -1
  3. package/src/branch-landed.ts +38 -2
  4. package/src/cli.ts +3 -3
  5. package/src/maintenance.ts +21 -21
  6. package/src/mcp.ts +2 -2
  7. package/src/ratchet-files.ts +37 -0
  8. package/src/routes/_shared.ts +376 -0
  9. package/src/routes/activity.ts +61 -0
  10. package/src/routes/agent-profiles.ts +47 -0
  11. package/src/routes/agent-sessions.ts +488 -0
  12. package/src/routes/agents-spawn.ts +274 -0
  13. package/src/routes/agents.ts +251 -0
  14. package/src/routes/artifacts.ts +226 -0
  15. package/src/routes/automations.ts +83 -0
  16. package/src/routes/commands.ts +317 -0
  17. package/src/routes/config.ts +66 -0
  18. package/src/routes/connectors.ts +108 -0
  19. package/src/routes/inbox.ts +142 -0
  20. package/src/routes/index.ts +293 -0
  21. package/src/routes/insights.ts +81 -0
  22. package/src/routes/integrations.ts +592 -0
  23. package/src/routes/memory.ts +337 -0
  24. package/src/routes/messages.ts +529 -0
  25. package/src/routes/orchestrator-bootstrap.ts +100 -0
  26. package/src/routes/orchestrator-proxy.ts +160 -0
  27. package/src/routes/orchestrator.ts +490 -0
  28. package/src/routes/pairs.ts +197 -0
  29. package/src/routes/provider-config.ts +112 -0
  30. package/src/routes/recipes.ts +113 -0
  31. package/src/routes/spawn-policy.ts +231 -0
  32. package/src/routes/spec.ts +54 -0
  33. package/src/routes/sse.ts +9 -0
  34. package/src/routes/stats.ts +32 -0
  35. package/src/routes/steward.ts +45 -0
  36. package/src/routes/tasks.ts +174 -0
  37. package/src/routes/tokens.ts +311 -0
  38. package/src/routes/workspaces.ts +364 -0
  39. package/src/routes.ts +3 -6822
  40. package/src/validation.ts +134 -0
  41. package/src/workspace-actions.ts +7 -1
  42. package/src/workspace-merge.ts +12 -1
@@ -0,0 +1,66 @@
1
+ // Auto-split from routes.ts (#299). Domain: config.
2
+ import { ValidationError } from "../db";
3
+ import { cleanString } from "../validation";
4
+ import { deleteConfig, getConfig, getConfigHistory, listConfig, setConfig } from "../config-store";
5
+ import { emitConfigChanged } from "../sse";
6
+ import { error, json, normalizeConfigPathParam, parseBody, parseQueryInt, type Handler } from "./_shared";
7
+ import { isRecord } from "agent-relay-sdk";
8
+
9
+ export const getConfigNamespace: Handler = (_req, params) => {
10
+ const namespace = normalizeConfigPathParam(params.namespace, "namespace");
11
+ return json(listConfig(namespace));
12
+ };
13
+
14
+ export const getConfigKey: Handler = (_req, params) => {
15
+ const namespace = normalizeConfigPathParam(params.namespace, "namespace");
16
+ const key = normalizeConfigPathParam(params.key, "key");
17
+ const entry = getConfig(namespace, key);
18
+ return entry ? json(entry) : error("config not found", 404);
19
+ };
20
+
21
+ export const putConfigKey: Handler = async (req, params) => {
22
+ const parsed = await parseBody<unknown>(req);
23
+ if (!parsed.ok) return error(parsed.error, parsed.status);
24
+ try {
25
+ if (!isRecord(parsed.body)) throw new ValidationError("JSON object body required");
26
+ if (!Object.prototype.hasOwnProperty.call(parsed.body, "value")) throw new ValidationError("value required");
27
+ const namespace = normalizeConfigPathParam(params.namespace, "namespace");
28
+ const key = normalizeConfigPathParam(params.key, "key");
29
+ const updatedBy = cleanString(parsed.body.updatedBy, "updatedBy", { max: 200 });
30
+ const entry = setConfig(namespace, key, parsed.body.value, updatedBy);
31
+ emitConfigChanged(entry.namespace, entry.key, entry.version);
32
+ return json(entry, entry.version === 1 ? 201 : 200);
33
+ } catch (e) {
34
+ if (e instanceof ValidationError) return error(e.message, 400);
35
+ throw e;
36
+ }
37
+ };
38
+
39
+ export const deleteConfigKey: Handler = (req, params) => {
40
+ try {
41
+ const namespace = normalizeConfigPathParam(params.namespace, "namespace");
42
+ const key = normalizeConfigPathParam(params.key, "key");
43
+ const updatedBy = cleanString(new URL(req.url).searchParams.get("updatedBy") ?? undefined, "updatedBy", { max: 200 });
44
+ const existing = getConfig(namespace, key);
45
+ if (!existing) return error("config not found", 404);
46
+ deleteConfig(namespace, key, updatedBy);
47
+ emitConfigChanged(namespace, key, existing.version + 1);
48
+ return json({ ok: true });
49
+ } catch (e) {
50
+ if (e instanceof ValidationError) return error(e.message, 400);
51
+ throw e;
52
+ }
53
+ };
54
+
55
+ export const getConfigKeyHistory: Handler = (req, params) => {
56
+ try {
57
+ const namespace = normalizeConfigPathParam(params.namespace, "namespace");
58
+ const key = normalizeConfigPathParam(params.key, "key");
59
+ const limitRaw = parseQueryInt(new URL(req.url).searchParams.get("limit"), { min: 1, max: 500 });
60
+ if (Number.isNaN(limitRaw)) return error("limit must be an integer between 1 and 500");
61
+ return json(getConfigHistory(namespace, key, limitRaw ?? undefined));
62
+ } catch (e) {
63
+ if (e instanceof ValidationError) return error(e.message, 400);
64
+ throw e;
65
+ }
66
+ };
@@ -0,0 +1,108 @@
1
+ // Auto-split from routes.ts (#299). Domain: connectors.
2
+ import { ValidationError } from "../db";
3
+ import { error, json, parseBody, type Handler } from "./_shared";
4
+ import { getConnector, listConnectors, markConnectorUnreachable, readConnectorConfig, registerConnectorManifest, runConnectorAction, writeConnectorConfig } from "../connectors";
5
+ import { isRecord } from "agent-relay-sdk";
6
+ import { optionalEnum } from "../validation";
7
+
8
+ const VALID_CONNECTOR_ACTIONS = ["install", "uninstall", "enable", "disable", "start", "stop", "restart", "status", "doctor"] as const;
9
+
10
+ export const getConnectors: Handler = () => json(listConnectors());
11
+
12
+ export const getConnectorById: Handler = (_req, params) => {
13
+ const connector = getConnector(params.id!);
14
+ return connector ? json(connector) : error("connector not found", 404);
15
+ };
16
+
17
+ export const postConnectorRegister: Handler = async (req) => {
18
+ const parsed = await parseBody<unknown>(req);
19
+ if (!parsed.ok) return error(parsed.error, parsed.status);
20
+ try {
21
+ if (!isRecord(parsed.body)) throw new ValidationError("JSON object body required");
22
+ const manifest = isRecord(parsed.body.manifest) ? parsed.body.manifest : parsed.body;
23
+ const config = isRecord(parsed.body.config) ? parsed.body.config : undefined;
24
+ const state = isRecord(parsed.body.state) ? parsed.body.state : undefined;
25
+ return json(registerConnectorManifest(manifest as any, { config, state }), 201);
26
+ } catch (e) {
27
+ if (e instanceof ValidationError) return error(e.message, 400);
28
+ throw e;
29
+ }
30
+ };
31
+
32
+ export const postConnectorAction: Handler = async (req, params) => {
33
+ const parsed = await parseBody<unknown>(req);
34
+ if (!parsed.ok) return error(parsed.error, parsed.status);
35
+ try {
36
+ if (!isRecord(parsed.body)) throw new ValidationError("JSON object body required");
37
+ const action = optionalEnum(parsed.body.action, "action", VALID_CONNECTOR_ACTIONS)!;
38
+ const result = await runConnectorAction(params.id!, action);
39
+ return json(result, result.ok ? 200 : 502);
40
+ } catch (e) {
41
+ if (e instanceof ValidationError) return error(e.message, e.message.includes("not found") ? 404 : 400);
42
+ throw e;
43
+ }
44
+ };
45
+
46
+ export const getConnectorConfig: Handler = (_req, params) => {
47
+ try {
48
+ if (!getConnector(params.id!)) return error("connector not found", 404);
49
+ return json(readConnectorConfig(params.id!));
50
+ } catch (e) {
51
+ if (e instanceof ValidationError) return error(e.message, 400);
52
+ throw e;
53
+ }
54
+ };
55
+
56
+ export const putConnectorConfig: Handler = async (req, params) => {
57
+ const parsed = await parseBody<unknown>(req);
58
+ if (!parsed.ok) return error(parsed.error, parsed.status);
59
+ try {
60
+ if (!getConnector(params.id!)) return error("connector not found", 404);
61
+ if (!isRecord(parsed.body)) throw new ValidationError("connector config must be an object");
62
+ return json(writeConnectorConfig(params.id!, parsed.body));
63
+ } catch (e) {
64
+ if (e instanceof ValidationError) return error(e.message, 400);
65
+ throw e;
66
+ }
67
+ };
68
+
69
+ const PROXYABLE_CONNECTOR_CALLS = new Set(["transcribe", "utterance", "speak"]);
70
+
71
+ function connectorAdvertisedEndpoint(connector: NonNullable<ReturnType<typeof getConnector>>): string | null {
72
+ const raw = connector.state?.raw;
73
+ if (isRecord(raw) && typeof raw.endpoint === "string" && raw.endpoint) return raw.endpoint;
74
+ const cfg = connector.config;
75
+ if (isRecord(cfg) && typeof cfg.endpoint === "string" && cfg.endpoint) return cfg.endpoint;
76
+ return null;
77
+ }
78
+
79
+ export const postConnectorCall: Handler = async (req, params) => {
80
+ const connector = getConnector(params.id!);
81
+ if (!connector) return error("connector not found", 404);
82
+ const name = params.name!;
83
+ if (!PROXYABLE_CONNECTOR_CALLS.has(name)) return error("unsupported connector call", 404);
84
+ const endpoint = connectorAdvertisedEndpoint(connector);
85
+ if (!endpoint) return error("connector is not running (no advertised endpoint)", 422);
86
+ try {
87
+ const body = await req.arrayBuffer();
88
+ const res = await fetch(`${endpoint.replace(/\/$/, "")}/${name}`, {
89
+ method: "POST",
90
+ headers: { "content-type": req.headers.get("content-type") || "application/octet-stream" },
91
+ body,
92
+ signal: AbortSignal.timeout(120_000),
93
+ });
94
+ return new Response(res.body, {
95
+ status: res.status,
96
+ headers: { "content-type": res.headers.get("content-type") || "application/json" },
97
+ });
98
+ } catch (e) {
99
+ // A connection error (refused/reset/DNS) means the advertised endpoint is
100
+ // genuinely down — reconcile the stale running:true so the dashboard stops
101
+ // claiming the connector is healthy. A timeout (AbortError) may just be a
102
+ // slow daemon, so leave its state alone.
103
+ if ((e as Error).name !== "AbortError") {
104
+ markConnectorUnreachable(params.id!, `endpoint unreachable: ${(e as Error).message}`);
105
+ }
106
+ return error(`failed to reach connector: ${(e as Error).message}`, 502);
107
+ }
108
+ };
@@ -0,0 +1,142 @@
1
+ // Auto-split from routes.ts (#299). Domain: inbox.
2
+ import { MAX_BODY_BYTES } from "../config";
3
+ import { ValidationError, createChatHistoryImport, deleteInboxDraft, getInboxState, listChatHistoryImports, setInboxDraft, setInboxThreadState } from "../db";
4
+ import { authorizeRoute, error, json, parseBody, parseQueryInt, type Handler } from "./_shared";
5
+ import { cleanNullablePositiveId, cleanOperatorId, cleanPositiveIdArray, cleanString } from "../validation";
6
+ import { isRecord } from "agent-relay-sdk";
7
+
8
+ function normalizeInboxThreadStateInput(body: unknown): {
9
+ operatorId: string;
10
+ peerId: string;
11
+ readCursorMessageId?: number | null;
12
+ archivedAtMessageId?: number | null;
13
+ } {
14
+ if (!isRecord(body)) throw new ValidationError("JSON object body required");
15
+ const input: {
16
+ operatorId: string;
17
+ peerId: string;
18
+ readCursorMessageId?: number | null;
19
+ archivedAtMessageId?: number | null;
20
+ } = {
21
+ operatorId: cleanOperatorId(body.operatorId),
22
+ peerId: cleanString(body.peerId, "peerId", { required: true, max: 200 })!,
23
+ };
24
+ const readCursorMessageId = cleanNullablePositiveId(body.readCursorMessageId, "readCursorMessageId");
25
+ if (readCursorMessageId !== undefined) input.readCursorMessageId = readCursorMessageId;
26
+ const archivedAtMessageId = cleanNullablePositiveId(body.archivedAtMessageId, "archivedAtMessageId");
27
+ if (archivedAtMessageId !== undefined) input.archivedAtMessageId = archivedAtMessageId;
28
+ return input;
29
+ }
30
+
31
+ function normalizeInboxDraftInput(body: unknown): {
32
+ operatorId: string;
33
+ peerId: string;
34
+ body: string;
35
+ subject?: string;
36
+ channel?: string;
37
+ } {
38
+ if (!isRecord(body)) throw new ValidationError("JSON object body required");
39
+ return {
40
+ operatorId: cleanOperatorId(body.operatorId),
41
+ peerId: cleanString(body.peerId, "peerId", { required: true, max: 200 })!,
42
+ body: cleanString(body.body, "body", { required: true, max: MAX_BODY_BYTES })!,
43
+ subject: cleanString(body.subject, "subject", { max: 200 }),
44
+ channel: cleanString(body.channel, "channel", { max: 120 }),
45
+ };
46
+ }
47
+
48
+ function normalizeChatHistoryImportInput(body: unknown): {
49
+ targetAgentId?: string;
50
+ targetSpawnRequestId?: string;
51
+ sourcePeerId: string;
52
+ sourceAgentId?: string;
53
+ sourceThreadId?: string;
54
+ sourceAgentLabel?: string;
55
+ importedBy?: string;
56
+ messageIds: number[];
57
+ } {
58
+ if (!isRecord(body)) throw new ValidationError("JSON object body required");
59
+ const targetAgentId = cleanString(body.targetAgentId, "targetAgentId", { max: 200 });
60
+ const targetSpawnRequestId = cleanString(body.targetSpawnRequestId, "targetSpawnRequestId", { max: 160 });
61
+ if (!targetAgentId && !targetSpawnRequestId) throw new ValidationError("targetAgentId or targetSpawnRequestId required");
62
+ return {
63
+ targetAgentId,
64
+ targetSpawnRequestId,
65
+ sourcePeerId: cleanString(body.sourcePeerId, "sourcePeerId", { required: true, max: 200 })!,
66
+ sourceAgentId: cleanString(body.sourceAgentId, "sourceAgentId", { max: 200 }),
67
+ sourceThreadId: cleanString(body.sourceThreadId, "sourceThreadId", { max: 240 }),
68
+ sourceAgentLabel: cleanString(body.sourceAgentLabel, "sourceAgentLabel", { max: 240 }),
69
+ importedBy: cleanOperatorId(body.importedBy),
70
+ messageIds: cleanPositiveIdArray(body.messageIds, "messageIds"),
71
+ };
72
+ }
73
+
74
+ export const getInboxStateRoute: Handler = (req) => {
75
+ const url = new URL(req.url);
76
+ const operatorId = cleanOperatorId(url.searchParams.get("operatorId") ?? undefined);
77
+ return json(getInboxState(operatorId));
78
+ };
79
+
80
+ export const patchInboxThreadState: Handler = async (req) => {
81
+ const parsed = await parseBody<unknown>(req);
82
+ if (!parsed.ok) return error(parsed.error, parsed.status);
83
+ try {
84
+ return json(setInboxThreadState(normalizeInboxThreadStateInput(parsed.body)));
85
+ } catch (e) {
86
+ if (e instanceof ValidationError) return error(e.message, 400);
87
+ throw e;
88
+ }
89
+ };
90
+
91
+ export const putInboxDraft: Handler = async (req) => {
92
+ const parsed = await parseBody<unknown>(req);
93
+ if (!parsed.ok) return error(parsed.error, parsed.status);
94
+ try {
95
+ return json(setInboxDraft(normalizeInboxDraftInput(parsed.body)));
96
+ } catch (e) {
97
+ if (e instanceof ValidationError) return error(e.message, 400);
98
+ throw e;
99
+ }
100
+ };
101
+
102
+ export const deleteInboxDraftRoute: Handler = (req) => {
103
+ const url = new URL(req.url);
104
+ try {
105
+ const operatorId = cleanOperatorId(url.searchParams.get("operatorId") ?? undefined);
106
+ const peerId = cleanString(url.searchParams.get("peerId") ?? undefined, "peerId", { required: true, max: 200 })!;
107
+ deleteInboxDraft(operatorId, peerId);
108
+ return json({ ok: true });
109
+ } catch (e) {
110
+ if (e instanceof ValidationError) return error(e.message, 400);
111
+ throw e;
112
+ }
113
+ };
114
+
115
+ export const getChatHistoryImportsRoute: Handler = (req) => {
116
+ const url = new URL(req.url);
117
+ try {
118
+ const targetAgentId = cleanString(url.searchParams.get("targetAgentId") ?? undefined, "targetAgentId", { max: 200 });
119
+ const targetSpawnRequestId = cleanString(url.searchParams.get("targetSpawnRequestId") ?? undefined, "targetSpawnRequestId", { max: 160 });
120
+ const limitRaw = parseQueryInt(url.searchParams.get("limit"), { min: 1, max: 500 });
121
+ if (Number.isNaN(limitRaw)) return error("limit must be an integer between 1 and 500");
122
+ return json(listChatHistoryImports({ targetAgentId, targetSpawnRequestId, limit: limitRaw ?? 100 }));
123
+ } catch (e) {
124
+ if (e instanceof ValidationError) return error(e.message, 400);
125
+ throw e;
126
+ }
127
+ };
128
+
129
+ export const postChatHistoryImportRoute: Handler = async (req) => {
130
+ const parsed = await parseBody<unknown>(req);
131
+ if (!parsed.ok) return error(parsed.error, parsed.status);
132
+ try {
133
+ const input = normalizeChatHistoryImportInput(parsed.body);
134
+ const denied = authorizeRoute(req, { scope: "agent:write", resource: { agentId: input.targetAgentId, spawnRequestId: input.targetSpawnRequestId } });
135
+ if (denied) return denied;
136
+ const imported = createChatHistoryImport(input);
137
+ return json(imported, 201);
138
+ } catch (e) {
139
+ if (e instanceof ValidationError) return error(e.message, 400);
140
+ throw e;
141
+ }
142
+ };
@@ -0,0 +1,293 @@
1
+ // Auto-split from routes.ts (#299). Router: route table + matchRoute dispatch.
2
+ import { deleteAgentActiveMemories, deleteAgentById, findAgents, getAgentActiveMemories, getAgentById, getAgentContextSnapshots, getAgentReplyObligations, getAgentTimelineRoute, getAgents, getMessageArtifactsRoute, getTaskArtifactsRoute, patchAgentLabel, patchAgentReady, patchAgentStatus, patchAgentTags, postHeartbeat } from "./agents";
3
+ import { deleteAgentProfileRoute, getAgentProfileRoute, getAgentProfilesRoute, putAgentProfileRoute } from "./agent-profiles";
4
+ import { deleteAgentTerminalSession, postAgentAction, postAgentPermissionDecision, postAgentPrompt, postAgentTerminalSession } from "./agent-sessions";
5
+ import { deleteArtifactById, getArtifactById, getArtifactContent, getArtifacts, headArtifactContent, postArtifact } from "./artifacts";
6
+ import { deleteAutomationById, getAutomationById, getAutomationRuns, getAutomations, patchAutomation, postAutomation, postAutomationRun } from "./automations";
7
+ import { deleteCommandById, getProviderConfigRoute, getProviderConfigsRoute, getProvidersRoute, postProviderConfigTestRoute, putProviderConfigRoute } from "./provider-config";
8
+ import { deleteConfigKey, getConfigKey, getConfigKeyHistory, getConfigNamespace, putConfigKey } from "./config";
9
+ import { deleteInboxDraftRoute, getChatHistoryImportsRoute, getInboxStateRoute, patchInboxThreadState, postChatHistoryImportRoute, putInboxDraft } from "./inbox";
10
+ import { deleteMemoryRoute, getMemories, getMemoryBrokerInfo, getMemoryById, getMemoryStats, patchMemory, postMemory, postMemoryInject, postMemorySearch } from "./memory";
11
+ import { deleteMessageById, getCursorRoute, getMessageById, getMessageDeliveryRoute, getMessageStatusRoute, getMessageThread, getMessages, getQueuedMessagesRoute, patchMessage, postClaimMessage, postMessage, postMessageDeliveryAction, postMessageDeliveryAttempt, postMessageReaction, postRenewMessageClaim, postSystemBroadcast } from "./messages";
12
+ import { deleteOrchestratorById, getOrchestratorById, getOrchestrators, patchOrchestratorAgents, postOrchestrator, postOrchestratorAction, postOrchestratorHeartbeat } from "./orchestrator";
13
+ import { deleteSpawnPolicyRoute, getSpawnPoliciesHealth, getSpawnPoliciesRoute, getSpawnPolicyHealth, getSpawnPolicyRoute, getSpawnPolicyStatus, postSpawnPolicyRestart, postSpawnPolicyStart, postSpawnPolicyStop, putSpawnPolicyRoute } from "./spawn-policy";
14
+ import { deleteTokenProfileRoute, getTokenById, getTokenProfiles, getTokens, patchTokenProfile, postIntegrationRuntimeToken, postInteractiveRunnerRuntimeToken, postMcpRuntimeToken, postOrchestratorRunnerToken, postRuntimeTokenRenew, postToken, postTokenProfile, postTokenRevoke } from "./tokens";
15
+ import { deleteWorkspaceById, getWorkspaceById, getWorkspaceDiagnostics, getWorkspaceDiff, getWorkspaceGitState, getWorkspaceMergePreview, getWorkspaceOrphans, getWorkspaceStewards, getWorkspaces, postWorkspaceAction, postWorkspaceCleanupStale, postWorkspaceOrphanReclaim } from "./workspaces";
16
+ import { error, type Handler } from "./_shared";
17
+ import { getActivityEvents, postActivityEvent } from "./activity";
18
+ import { getAnalyticsRoute, getHealthRoute, getMaintenanceJobs, getStatsRoute, postMaintenanceJobRun, postSystemReap } from "./stats";
19
+ import { getApiDocs, getApiSpec } from "./spec";
20
+ import { getChannelBindings, getChannels, getIntegrations, postChannelActivity, postChannelBinding, postChannelEvent, postIntegrationEvent, putIntegrationRegistryRoute } from "./integrations";
21
+ import { getCommandById, getCommands, patchCommand, postCommand } from "./commands";
22
+ import { getConnectorById, getConnectorConfig, getConnectors, postConnectorAction, postConnectorCall, postConnectorRegister, putConnectorConfig } from "./connectors";
23
+ import { getEvents } from "./sse";
24
+ import { getHostDirectories, postAgent, postAgentSpawn, postRouteAdvice } from "./agents-spawn";
25
+ import { getInsightsConfigRoute, getInsightsObservationsRoute, postInsightsObservationRoute, putInsightsConfigRoute } from "./insights";
26
+ import { getOrchestratorDirectories, getOrchestratorFileRead, getOrchestratorFileStat, getOrchestratorFilesList, getOrchestratorLogs, getOrchestratorProviders, getOrchestratorSessions, getOrchestratorTerminal, getOrchestratorVersion, getOrchestratorWorkspaceProbe, postOrchestratorCreateDirectory, postOrchestratorTerminalInput, postOrchestratorTerminalResize } from "./orchestrator-proxy";
27
+ import { getPairById, getPairs, postAcceptPair, postHangupPair, postPair, postPairMessage, postRejectPair } from "./pairs";
28
+ import { getRecipeByName, getRecipeInstanceArtifacts, getRecipeInstanceById, getRecipeInstances, getRecipes, postRecipeInstanceArtifacts, postRecipeStart, postRecipeStop } from "./recipes";
29
+ import { getStewardConfigRoute, getWorkspaceConfigRoute, putStewardConfigRoute, putWorkspaceConfigRoute } from "./steward";
30
+ import { getTaskById, getTaskEvents, getTasks, patchTaskStatus, postClaimTask, postRenewTaskClaim } from "./tasks";
31
+ import { postMcp } from "../mcp";
32
+ import { postOrchestratorBootstrap, postOrchestratorBootstrapExchange } from "./orchestrator-bootstrap";
33
+
34
+ interface Route {
35
+ method: string;
36
+ pattern: RegExp;
37
+ paramNames: string[];
38
+ handler: Handler;
39
+ }
40
+
41
+ function route(method: string, path: string, handler: Handler): Route {
42
+ const paramNames: string[] = [];
43
+ const pattern = new RegExp(
44
+ "^" +
45
+ path.replace(/:(\w+)/g, (_, name) => {
46
+ paramNames.push(name);
47
+ return "([^/]+)";
48
+ }) +
49
+ "$"
50
+ );
51
+ return { method, pattern, paramNames, handler };
52
+ }
53
+
54
+ const routes: Route[] = [
55
+ route("POST", "/api/artifacts", postArtifact),
56
+ route("GET", "/api/artifacts", getArtifacts),
57
+ route("GET", "/api/artifacts/:id/content", getArtifactContent),
58
+ route("HEAD", "/api/artifacts/:id/content", headArtifactContent),
59
+ route("GET", "/api/artifacts/:id", getArtifactById),
60
+ route("DELETE", "/api/artifacts/:id", deleteArtifactById),
61
+
62
+ route("POST", "/api/memories", postMemory),
63
+ route("GET", "/api/memories", getMemories),
64
+ route("POST", "/api/memories/inject", postMemoryInject),
65
+ route("POST", "/api/memories/search", postMemorySearch),
66
+ route("GET", "/api/memories/stats", getMemoryStats),
67
+ route("GET", "/api/memories/broker", getMemoryBrokerInfo),
68
+ route("GET", "/api/memories/:id", getMemoryById),
69
+ route("PATCH", "/api/memories/:id", patchMemory),
70
+ route("DELETE", "/api/memories/:id", deleteMemoryRoute),
71
+
72
+ route("POST", "/api/agents", postAgent),
73
+ route("POST", "/api/agents/spawn", postAgentSpawn),
74
+ route("GET", "/api/agents", getAgents),
75
+ route("GET", "/api/agents/find", findAgents),
76
+ route("POST", "/api/route", postRouteAdvice),
77
+ route("GET", "/api/agents/spawn/directories", getHostDirectories),
78
+ route("GET", "/api/agents/:id/context-snapshots", getAgentContextSnapshots),
79
+ route("GET", "/api/agents/:id/timeline", getAgentTimelineRoute),
80
+ route("GET", "/api/agents/:id/active-memories", getAgentActiveMemories),
81
+ route("GET", "/api/agents/:id/reply-obligations", getAgentReplyObligations),
82
+ route("DELETE", "/api/agents/:id/active-memories", deleteAgentActiveMemories),
83
+ route("GET", "/api/agents/:id", getAgentById),
84
+ route("PATCH", "/api/agents/:id/status", patchAgentStatus),
85
+ route("PATCH", "/api/agents/:id/ready", patchAgentReady),
86
+ route("PATCH", "/api/agents/:id/label", patchAgentLabel),
87
+ route("PATCH", "/api/agents/:id/tags", patchAgentTags),
88
+ route("POST", "/api/agents/:id/heartbeat", postHeartbeat),
89
+ route("POST", "/api/agents/:id/actions", postAgentAction),
90
+ route("POST", "/api/agents/:id/prompt", postAgentPrompt),
91
+ route("POST", "/api/agents/:id/permission-decision", postAgentPermissionDecision),
92
+ route("POST", "/api/agents/:id/terminal-session", postAgentTerminalSession),
93
+ route("DELETE", "/api/agents/:id/terminal-session/:session", deleteAgentTerminalSession),
94
+ route("DELETE", "/api/agents/:id", deleteAgentById),
95
+
96
+ route("POST", "/api/orchestrators", postOrchestrator),
97
+ route("POST", "/api/orchestrators/bootstrap", postOrchestratorBootstrap),
98
+ route("POST", "/api/orchestrators/bootstrap/exchange", postOrchestratorBootstrapExchange),
99
+ route("GET", "/api/orchestrators", getOrchestrators),
100
+ route("GET", "/api/orchestrators/:id", getOrchestratorById),
101
+ route("POST", "/api/orchestrators/:id/heartbeat", postOrchestratorHeartbeat),
102
+ route("PATCH", "/api/orchestrators/:id/agents", patchOrchestratorAgents),
103
+ route("POST", "/api/orchestrators/:id/runner-token", postOrchestratorRunnerToken),
104
+ route("POST", "/api/orchestrators/:id/actions", postOrchestratorAction),
105
+ route("GET", "/api/orchestrators/:id/directories", getOrchestratorDirectories),
106
+ route("POST", "/api/orchestrators/:id/directories", postOrchestratorCreateDirectory),
107
+ route("GET", "/api/orchestrators/:id/files/list", getOrchestratorFilesList),
108
+ route("GET", "/api/orchestrators/:id/files/read", getOrchestratorFileRead),
109
+ route("GET", "/api/orchestrators/:id/files/stat", getOrchestratorFileStat),
110
+ route("GET", "/api/orchestrators/:id/workspace/probe", getOrchestratorWorkspaceProbe),
111
+ route("GET", "/api/orchestrators/:id/providers", getOrchestratorProviders),
112
+ route("GET", "/api/orchestrators/:id/sessions", getOrchestratorSessions),
113
+ route("GET", "/api/orchestrators/:id/version", getOrchestratorVersion),
114
+ route("GET", "/api/orchestrators/:id/logs/:session", getOrchestratorLogs),
115
+ route("GET", "/api/orchestrators/:id/terminal/:session", getOrchestratorTerminal),
116
+ route("POST", "/api/orchestrators/:id/terminal/:session/input", postOrchestratorTerminalInput),
117
+ route("POST", "/api/orchestrators/:id/terminal/:session/resize", postOrchestratorTerminalResize),
118
+ route("DELETE", "/api/orchestrators/:id", deleteOrchestratorById),
119
+
120
+ route("GET", "/api/workspaces", getWorkspaces),
121
+ // Static segments before :id so "/workspaces/orphans" isn't captured as an id.
122
+ route("GET", "/api/workspaces/orphans", getWorkspaceOrphans),
123
+ route("POST", "/api/workspaces/orphans/reclaim", postWorkspaceOrphanReclaim),
124
+ route("GET", "/api/workspaces/stewards", getWorkspaceStewards),
125
+ route("POST", "/api/workspaces/actions/cleanup-stale", postWorkspaceCleanupStale),
126
+ route("GET", "/api/workspaces/:id", getWorkspaceById),
127
+ route("GET", "/api/workspaces/:id/git-state", getWorkspaceGitState),
128
+ route("GET", "/api/workspaces/:id/merge-preview", getWorkspaceMergePreview),
129
+ route("GET", "/api/workspaces/:id/diagnostics", getWorkspaceDiagnostics),
130
+ route("GET", "/api/workspaces/:id/diff", getWorkspaceDiff),
131
+ route("POST", "/api/workspaces/:id/actions", postWorkspaceAction),
132
+ route("DELETE", "/api/workspaces/:id", deleteWorkspaceById),
133
+
134
+ route("POST", "/api/system/broadcast", postSystemBroadcast),
135
+ route("GET", "/api/recipes", getRecipes),
136
+ route("POST", "/api/recipes/start", postRecipeStart),
137
+ route("GET", "/api/recipes/instances", getRecipeInstances),
138
+ route("GET", "/api/recipes/instances/:id", getRecipeInstanceById),
139
+ route("GET", "/api/recipes/instances/:id/artifacts", getRecipeInstanceArtifacts),
140
+ route("POST", "/api/recipes/instances/:id/artifacts", postRecipeInstanceArtifacts),
141
+ route("POST", "/api/recipes/instances/:id/stop", postRecipeStop),
142
+ route("GET", "/api/recipes/:name", getRecipeByName),
143
+ route("GET", "/api/tokens", getTokens),
144
+ route("POST", "/api/tokens", postToken),
145
+ route("POST", "/api/runtime-tokens/renew", postRuntimeTokenRenew),
146
+ route("POST", "/api/runtime-tokens/interactive-runner", postInteractiveRunnerRuntimeToken),
147
+ route("POST", "/api/runtime-tokens/mcp-client", postMcpRuntimeToken),
148
+ route("POST", "/api/runtime-tokens/integration", postIntegrationRuntimeToken),
149
+ route("GET", "/api/token-profiles", getTokenProfiles),
150
+ route("POST", "/api/token-profiles", postTokenProfile),
151
+ route("PATCH", "/api/token-profiles/:id", patchTokenProfile),
152
+ route("DELETE", "/api/token-profiles/:id", deleteTokenProfileRoute),
153
+ route("GET", "/api/tokens/:jti", getTokenById),
154
+ route("POST", "/api/tokens/:jti/revoke", postTokenRevoke),
155
+ route("POST", "/api/commands", postCommand),
156
+ route("GET", "/api/commands", getCommands),
157
+ route("GET", "/api/commands/:id", getCommandById),
158
+ route("PATCH", "/api/commands/:id", patchCommand),
159
+ route("DELETE", "/api/commands/:id", deleteCommandById),
160
+ route("GET", "/api/agent-profiles", getAgentProfilesRoute),
161
+ route("GET", "/api/agent-profiles/:name", getAgentProfileRoute),
162
+ route("PUT", "/api/agent-profiles/:name", putAgentProfileRoute),
163
+ route("DELETE", "/api/agent-profiles/:name", deleteAgentProfileRoute),
164
+ route("GET", "/api/steward-config", getStewardConfigRoute),
165
+ route("PUT", "/api/steward-config", putStewardConfigRoute),
166
+ route("GET", "/api/workspace-config", getWorkspaceConfigRoute),
167
+ route("PUT", "/api/workspace-config", putWorkspaceConfigRoute),
168
+ route("GET", "/api/insights/config", getInsightsConfigRoute),
169
+ route("PUT", "/api/insights/config", putInsightsConfigRoute),
170
+ route("GET", "/api/insights/observations", getInsightsObservationsRoute),
171
+ route("POST", "/api/insights/observations", postInsightsObservationRoute),
172
+ route("GET", "/api/config/:namespace", getConfigNamespace),
173
+ route("GET", "/api/config/:namespace/:key/history", getConfigKeyHistory),
174
+ route("GET", "/api/config/:namespace/:key", getConfigKey),
175
+ route("PUT", "/api/config/:namespace/:key", putConfigKey),
176
+ route("DELETE", "/api/config/:namespace/:key", deleteConfigKey),
177
+ route("GET", "/api/spawn-policies", getSpawnPoliciesRoute),
178
+ route("GET", "/api/spawn-policy/:name", getSpawnPolicyRoute),
179
+ route("PUT", "/api/spawn-policy/:name", putSpawnPolicyRoute),
180
+ route("DELETE", "/api/spawn-policy/:name", deleteSpawnPolicyRoute),
181
+ route("POST", "/api/spawn-policy/:name/start", postSpawnPolicyStart),
182
+ route("POST", "/api/spawn-policy/:name/stop", postSpawnPolicyStop),
183
+ route("POST", "/api/spawn-policy/:name/restart", postSpawnPolicyRestart),
184
+ route("GET", "/api/spawn-policies/health", getSpawnPoliciesHealth),
185
+ route("GET", "/api/spawn-policy/:name/status", getSpawnPolicyStatus),
186
+ route("GET", "/api/spawn-policy/:name/health", getSpawnPolicyHealth),
187
+ route("GET", "/api/providers", getProvidersRoute),
188
+ route("GET", "/api/providers/config", getProviderConfigsRoute),
189
+ route("GET", "/api/providers/:provider/config", getProviderConfigRoute),
190
+ route("PUT", "/api/providers/:provider/config", putProviderConfigRoute),
191
+ route("POST", "/api/providers/:provider/config/test", postProviderConfigTestRoute),
192
+
193
+ route("POST", "/api/mcp", postMcp),
194
+
195
+ route("POST", "/api/messages", postMessage),
196
+ route("GET", "/api/messages", getMessages),
197
+ route("GET", "/api/messages/queued", getQueuedMessagesRoute),
198
+ route("GET", "/api/messages/cursor", getCursorRoute),
199
+ route("GET", "/api/messages/:id/status", getMessageStatusRoute),
200
+ route("GET", "/api/messages/:id/delivery", getMessageDeliveryRoute),
201
+ route("GET", "/api/messages/:id/artifacts", getMessageArtifactsRoute),
202
+ route("POST", "/api/messages/:id/delivery/attempts", postMessageDeliveryAttempt),
203
+ route("POST", "/api/messages/:id/delivery/actions", postMessageDeliveryAction),
204
+ route("GET", "/api/messages/:id", getMessageById),
205
+ route("GET", "/api/messages/:id/thread", getMessageThread),
206
+ route("POST", "/api/messages/:id/reactions", postMessageReaction),
207
+ route("POST", "/api/messages/:id/claim", postClaimMessage),
208
+ route("POST", "/api/messages/:id/claim/renew", postRenewMessageClaim),
209
+ route("PATCH", "/api/messages/:id", patchMessage),
210
+ route("DELETE", "/api/messages/:id", deleteMessageById),
211
+
212
+ route("GET", "/api/inbox/state", getInboxStateRoute),
213
+ route("PATCH", "/api/inbox/threads", patchInboxThreadState),
214
+ route("PUT", "/api/inbox/drafts", putInboxDraft),
215
+ route("DELETE", "/api/inbox/drafts", deleteInboxDraftRoute),
216
+ route("GET", "/api/chat/history-imports", getChatHistoryImportsRoute),
217
+ route("POST", "/api/chat/history-imports", postChatHistoryImportRoute),
218
+
219
+ route("GET", "/api/activity", getActivityEvents),
220
+ route("POST", "/api/activity", postActivityEvent),
221
+
222
+ route("GET", "/api/connectors", getConnectors),
223
+ route("POST", "/api/connectors", postConnectorRegister),
224
+ route("GET", "/api/connectors/:id", getConnectorById),
225
+ route("POST", "/api/connectors/:id/actions", postConnectorAction),
226
+ route("GET", "/api/connectors/:id/config", getConnectorConfig),
227
+ route("PUT", "/api/connectors/:id/config", putConnectorConfig),
228
+ route("POST", "/api/connectors/:id/call/:name", postConnectorCall),
229
+
230
+ route("GET", "/api/channels", getChannels),
231
+ route("POST", "/api/channels/:id/events", postChannelEvent),
232
+ route("POST", "/api/channels/:id/activities", postChannelActivity),
233
+ route("GET", "/api/channel-bindings", getChannelBindings),
234
+ route("POST", "/api/channel-bindings", postChannelBinding),
235
+ route("GET", "/api/integrations", getIntegrations),
236
+ route("PUT", "/api/integrations/:name", putIntegrationRegistryRoute),
237
+ route("POST", "/api/integrations/events", postIntegrationEvent),
238
+ route("GET", "/api/automations", getAutomations),
239
+ route("POST", "/api/automations", postAutomation),
240
+ route("GET", "/api/automation-runs", getAutomationRuns),
241
+ route("GET", "/api/automations/:id", getAutomationById),
242
+ route("PATCH", "/api/automations/:id", patchAutomation),
243
+ route("DELETE", "/api/automations/:id", deleteAutomationById),
244
+ route("POST", "/api/automations/:id/run", postAutomationRun),
245
+ route("GET", "/api/tasks", getTasks),
246
+ route("GET", "/api/tasks/:id", getTaskById),
247
+ route("GET", "/api/tasks/:id/artifacts", getTaskArtifactsRoute),
248
+ route("GET", "/api/tasks/:id/events", getTaskEvents),
249
+ route("POST", "/api/tasks/:id/claim", postClaimTask),
250
+ route("POST", "/api/tasks/:id/claim/renew", postRenewTaskClaim),
251
+ route("PATCH", "/api/tasks/:id/status", patchTaskStatus),
252
+
253
+ route("POST", "/api/pairs", postPair),
254
+ route("GET", "/api/pairs", getPairs),
255
+ route("GET", "/api/pairs/:id", getPairById),
256
+ route("POST", "/api/pairs/:id/accept", postAcceptPair),
257
+ route("POST", "/api/pairs/:id/reject", postRejectPair),
258
+ route("POST", "/api/pairs/:id/hangup", postHangupPair),
259
+ route("POST", "/api/pairs/:id/messages", postPairMessage),
260
+
261
+ route("GET", "/api/spec", getApiSpec),
262
+ route("GET", "/api/docs", getApiDocs),
263
+
264
+ route("GET", "/api/events", getEvents),
265
+ route("GET", "/api/stats", getStatsRoute),
266
+ route("GET", "/api/stats/analytics", getAnalyticsRoute),
267
+ route("GET", "/api/health", getHealthRoute),
268
+ route("GET", "/api/maintenance/jobs", getMaintenanceJobs),
269
+ route("POST", "/api/maintenance/jobs/:id/run", postMaintenanceJobRun),
270
+ route("POST", "/api/system/reap", postSystemReap),
271
+ ];
272
+
273
+ export function matchRoute(
274
+ method: string,
275
+ pathname: string
276
+ ): { handler: Handler; params: Record<string, string> } | null {
277
+ for (const r of routes) {
278
+ if (r.method !== method) continue;
279
+ const match = pathname.match(r.pattern);
280
+ if (!match) continue;
281
+
282
+ const params: Record<string, string> = {};
283
+ try {
284
+ r.paramNames.forEach((name, i) => {
285
+ params[name] = decodeURIComponent(match[i + 1]!);
286
+ });
287
+ } catch {
288
+ return { handler: () => error("invalid url encoding"), params: {} };
289
+ }
290
+ return { handler: r.handler, params };
291
+ }
292
+ return null;
293
+ }