import type { CreateRoomInput, CreatedRoom, RoomView } from '@ai-rendezvous/core'; export class ApiError extends Error { constructor(readonly status: number, readonly code: string, message: string) { super(message); } } /** * Thin client for the AI Rendezvous HTTP API. Uses only fetch — no harness * assumptions, suitable for integrations in any Node-based environment. */ export class RendezvousClient { constructor( readonly baseUrl: string, readonly token?: string, ) {} private async request(method: string, path: string, body?: unknown): Promise { const res = await fetch(`${this.baseUrl}${path}`, { method, headers: { 'content-type': 'application/json', ...(this.token ? { authorization: `Bearer ${this.token}` } : {}), }, body: body === undefined ? undefined : JSON.stringify(body), }); const text = await res.text(); const data = text ? JSON.parse(text) : {}; if (!res.ok) { const err = (data as { error?: { code?: string; message?: string } }).error ?? {}; throw new ApiError(res.status, err.code ?? 'unknown', err.message ?? res.statusText); } return data as T; } createRoom(input: CreateRoomInput): Promise { return this.request('POST', '/api/rooms', input); } getRoom(roomId: string): Promise { return this.request('GET', `/api/rooms/${roomId}`); } join(roomId: string): Promise<{ joined: boolean; participants: { role: string; joined_at: string | null }[] }> { return this.request('POST', `/api/rooms/${roomId}/join`, {}); } postMessage(roomId: string, content: string): Promise { return this.request('POST', `/api/rooms/${roomId}/messages`, { content }); } ask( roomId: string, question: string, opts: { blocking?: boolean; addressed_to_participant_id?: string } = {}, ): Promise { return this.request('POST', `/api/rooms/${roomId}/questions`, { question, blocking: opts.blocking ?? true, ...(opts.addressed_to_participant_id ? { addressed_to_participant_id: opts.addressed_to_participant_id } : {}), }); } resolve(roomId: string, questionId: string, resolution: string): Promise { return this.request('POST', `/api/rooms/${roomId}/questions/${questionId}/resolve`, { resolution }); } proposeContract(roomId: string, markdown: string): Promise<{ version: number }> { return this.request('PUT', `/api/rooms/${roomId}/contract`, { markdown }); } agree(roomId: string): Promise<{ agreed_contract_version: number; room_status: string; everyone_agreed: boolean; }> { return this.request('POST', `/api/rooms/${roomId}/agree`, {}); } async finalMarkdown(roomId: string): Promise { const res = await fetch(`${this.baseUrl}/api/rooms/${roomId}/final.md`, { headers: this.token ? { authorization: `Bearer ${this.token}` } : {}, }); if (!res.ok) throw new ApiError(res.status, 'unknown', await res.text()); return res.text(); } async inviteMarkdown(inviteUrl: string): Promise { const res = await fetch(`${inviteUrl}.md`); if (!res.ok) throw new ApiError(res.status, 'unknown', await res.text()); return res.text(); } /** Client for a specific participant. */ as(token: string): RendezvousClient { return new RendezvousClient(this.baseUrl, token); } }