/** Thin control-plane client used with Keycloak bearer tokens. */ export class ApiError extends Error { constructor(public status: number, message: string) { super(`API ${status}: ${message}`); this.name = "ApiError"; } } export interface AgentRow { name: string; version: string; status: string; url?: string; description?: string; card?: Record; } export class ControlPlaneClient { constructor( private apiUrl: string, private token: string | null, ) { this.apiUrl = apiUrl.replace(/\/+$/, ""); } private headers(): Record { const h: Record = { accept: "application/json", "content-type": "application/json", }; if (this.token) h["authorization"] = `Bearer ${this.token}`; return h; } private async request(method: string, path: string, body?: unknown): Promise { const resp = await fetch(`${this.apiUrl}${path}`, { method, headers: this.headers(), body: body !== undefined ? JSON.stringify(body) : undefined, }); const text = await resp.text(); if (!resp.ok) { throw new ApiError(resp.status, errorDetail(text, resp.statusText)); } if (resp.status === 204) return undefined as T; return (text ? JSON.parse(text) : undefined) as T; } me() { return this.request<{ id: number; email: string }>("GET", "/v1/me"); } listAgents() { return this.request("GET", "/v1/agents"); } getAgent(name: string) { return this.request("GET", `/v1/agents/${encodeURIComponent(name)}`); } } function errorDetail(text: string, fallback: string): string { if (!text.trim()) return fallback; try { const parsed = JSON.parse(text); const detail = parsed?.detail; if (typeof detail === "string") return detail; if (detail !== undefined) return JSON.stringify(detail); return JSON.stringify(parsed); } catch { return text; } }