/// Gateway API client — used when running in gateway mode. /// /// The gateway mode is detected by checking `GET /gateway/mode`. If it returns /// `{ "mode": "gateway" }` the frontend switches to the gateway UI. export interface JoinedAgent { id: string; label: string; address: string; registered_at: number; /// Unix timestamp of the last heartbeat from this agent. last_seen: number; /// Project this agent is assigned to, if any. assigned_project?: string; } export interface GatewayProject { name: string; url: string; } export interface GatewayInfo { active: string; projects: GatewayProject[]; } export interface PipelineItem { story_id: string; name: string; stage: string; agent?: { agent_name: string; model: string; status: string } | null; blocked?: boolean; retry_count?: number; merge_failure?: string; } export interface ProjectPipelineStatus { active: PipelineItem[]; backlog: { story_id: string; name: string }[]; backlog_count: number; error?: string; } export interface AllProjectsPipeline { active: string; projects: Record; } export interface GenerateTokenResponse { token: string; } export interface ServerMode { mode: "gateway" | "standard"; } async function gatewayRequest( path: string, options: RequestInit = {}, ): Promise { const res = await fetch(path, { headers: { "Content-Type": "application/json", ...(options.headers ?? {}) }, ...options, }); if (!res.ok) { const text = await res.text(); throw new Error(text || `Request failed (${res.status})`); } // DELETE /gateway/agents/:id returns 204 No Content. if (res.status === 204) { return undefined as unknown as T; } return res.json() as Promise; } let _mcpRequestId = 1; /// Call a gateway MCP tool via JSON-RPC and return the result. async function gatewayMcpCall( toolName: string, args: Record = {}, ): Promise { const id = _mcpRequestId++; const body = JSON.stringify({ jsonrpc: "2.0", id, method: "tools/call", params: { name: toolName, arguments: args }, }); const res = await fetch("/mcp", { method: "POST", headers: { "Content-Type": "application/json" }, body, }); if (!res.ok) { const text = await res.text(); throw new Error(text || `MCP request failed (${res.status})`); } const json = (await res.json()) as { result?: Record; error?: { message: string }; }; if (json.error) { throw new Error(json.error.message); } return json.result as T; } export const gatewayApi = { /// Returns `{ mode: "gateway" }` if this server is a gateway, otherwise rejects. getServerMode(): Promise { return gatewayRequest("/gateway/mode"); }, /// Generate a one-time join token for a new build agent. generateToken(): Promise { return gatewayRequest("/gateway/tokens", { method: "POST", }); }, /// List all build agents that have registered with this gateway. listAgents(): Promise { return gatewayMcpCall<{ agents: JoinedAgent[] }>("agents.list").then( (result) => result.agents ?? [], ); }, /// Remove a registered build agent by its ID. removeAgent(id: string): Promise { return gatewayRequest(`/gateway/agents/${id}`, { method: "DELETE", }); }, /// Assign an agent to a project, or unassign it by passing null. assignAgent(id: string, project: string | null): Promise { return gatewayRequest(`/gateway/agents/${id}/assign`, { method: "POST", body: JSON.stringify({ project }), }); }, /// Get the list of registered projects from the gateway. getGatewayInfo(): Promise { return gatewayRequest("/api/gateway"); }, /// Send a heartbeat for an agent to update its last-seen timestamp. heartbeat(id: string): Promise { return gatewayRequest(`/gateway/agents/${id}/heartbeat`, { method: "POST", }); }, /// Fetch pipeline status from all registered projects via the pipeline.get read-RPC. async getAllProjectsPipeline(): Promise { const res = await fetch("/mcp", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "pipeline.get", params: {} }), }); if (!res.ok) { const text = await res.text(); throw new Error(text || `Request failed (${res.status})`); } const rpc = await res.json() as { result?: AllProjectsPipeline; error?: { message: string } }; if (rpc.error) { throw new Error(rpc.error.message); } return rpc.result!; }, /// Switch the active project via the MCP switch_project tool. async switchProject(project: string): Promise<{ ok: boolean; error?: string }> { const res = await fetch("/mcp", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "switch_project", arguments: { project } }, }), }); const data = await res.json(); if (data.error) { return { ok: false, error: data.error.message ?? String(data.error) }; } return { ok: true }; }, };