An AI agent built to do Ralph loops - plan mode for planning and ralph mode for implementing.
1/**
2 * Typed HTTP API client for Rustagent V2 daemon.
3 * Wraps fetch() calls with proper error handling and type safety.
4 */
5
6import type {
7 GraphNode,
8 GraphEdge,
9 ProjectResponse,
10 Session,
11 ActiveAgent,
12 NodeWithEdges,
13 GoalTree,
14 DecisionHistory,
15 ExportResult,
16 ImportResult,
17 DiffResult,
18 CreateProjectRequest,
19 CreateGoalRequest,
20 UpdateNodeRequest,
21 CreateChildRequest,
22 CreateEdgeRequest,
23 SearchRequest,
24 ImportRequest,
25} from '../types';
26
27/**
28 * API error thrown when a request fails.
29 */
30export class ApiError extends Error {
31 constructor(
32 public readonly status: number,
33 message: string
34 ) {
35 super(message);
36 this.name = 'ApiError';
37 }
38}
39
40/**
41 * Configurable HTTP API client.
42 * Base URL defaults to empty string for same-origin requests (via Vite proxy in dev).
43 */
44export class ApiClient {
45 constructor(private readonly baseUrl: string = '') {}
46
47 /**
48 * Generic request method with error handling.
49 */
50 private async request<T>(
51 method: string,
52 path: string,
53 body?: unknown
54 ): Promise<T> {
55 const opts: RequestInit = {
56 method,
57 headers: body ? { 'Content-Type': 'application/json' } : {},
58 body: body ? JSON.stringify(body) : undefined,
59 };
60
61 const res = await fetch(`${this.baseUrl}${path}`, opts);
62
63 if (!res.ok) {
64 const text = await res.text();
65 throw new ApiError(res.status, text);
66 }
67
68 if (res.status === 204) {
69 return undefined as T;
70 }
71
72 return res.json();
73 }
74
75 // ============ Projects ============
76
77 async listProjects(): Promise<Array<ProjectResponse>> {
78 return this.request('GET', '/api/projects');
79 }
80
81 async createProject(req: CreateProjectRequest): Promise<ProjectResponse> {
82 return this.request('POST', '/api/projects', req);
83 }
84
85 async getProject(id: string): Promise<ProjectResponse> {
86 return this.request('GET', `/api/projects/${id}`);
87 }
88
89 async deleteProject(id: string): Promise<void> {
90 return this.request('DELETE', `/api/projects/${id}`);
91 }
92
93 // ============ Goals ============
94
95 async listGoals(projectId: string): Promise<Array<GraphNode>> {
96 return this.request('GET', `/api/projects/${projectId}/goals`);
97 }
98
99 async createGoal(projectId: string, req: CreateGoalRequest): Promise<GraphNode> {
100 return this.request('POST', `/api/projects/${projectId}/goals`, req);
101 }
102
103 // ============ Nodes ============
104
105 async getNode(id: string): Promise<NodeWithEdges> {
106 return this.request('GET', `/api/nodes/${id}`);
107 }
108
109 async updateNode(id: string, req: UpdateNodeRequest): Promise<GraphNode> {
110 return this.request('PATCH', `/api/nodes/${id}`, req);
111 }
112
113 async createChild(parentId: string, req: CreateChildRequest): Promise<GraphNode> {
114 return this.request('POST', `/api/nodes/${parentId}/children`, req);
115 }
116
117 // ============ Edges ============
118
119 async createEdge(req: CreateEdgeRequest): Promise<GraphEdge> {
120 return this.request('POST', '/api/edges', req);
121 }
122
123 async deleteEdge(id: string): Promise<void> {
124 return this.request('DELETE', `/api/edges/${id}`);
125 }
126
127 // ============ Goal Tree and Task Views ============
128
129 async getGoalTree(goalId: string): Promise<GoalTree> {
130 return this.request('GET', `/api/goals/${goalId}/tree`);
131 }
132
133 async listTasks(goalId: string): Promise<Array<GraphNode>> {
134 return this.request('GET', `/api/goals/${goalId}/tasks`);
135 }
136
137 async listReadyTasks(goalId: string): Promise<Array<GraphNode>> {
138 return this.request('GET', `/api/goals/${goalId}/tasks/ready`);
139 }
140
141 async getNextTask(goalId: string): Promise<GraphNode | null> {
142 return this.request('GET', `/api/goals/${goalId}/tasks/next`);
143 }
144
145 // ============ Decisions ============
146
147 async listDecisions(projectId: string): Promise<Array<GraphNode>> {
148 return this.request('GET', `/api/projects/${projectId}/decisions`);
149 }
150
151 async getDecisionHistory(projectId: string): Promise<DecisionHistory> {
152 return this.request('GET', `/api/projects/${projectId}/decisions/history`);
153 }
154
155 async exportDecisions(projectId: string): Promise<Array<string>> {
156 return this.request('POST', `/api/projects/${projectId}/decisions/export`);
157 }
158
159 // ============ Graph Import/Export ============
160
161 async exportAllGoals(projectId: string): Promise<Array<ExportResult>> {
162 return this.request('GET', `/api/projects/${projectId}/graph/export`);
163 }
164
165 async exportGoal(goalId: string): Promise<ExportResult> {
166 return this.request('GET', `/api/goals/${goalId}/export`);
167 }
168
169 async importGraph(projectId: string, req: ImportRequest): Promise<ImportResult> {
170 return this.request('POST', `/api/projects/${projectId}/graph/import`, req);
171 }
172
173 async diffGraph(projectId: string, req: ImportRequest): Promise<DiffResult> {
174 return this.request('POST', `/api/projects/${projectId}/graph/diff`, req);
175 }
176
177 // ============ Sessions ============
178
179 async listSessions(goalId: string): Promise<Array<Session>> {
180 return this.request('GET', `/api/goals/${goalId}/sessions`);
181 }
182
183 async getSession(id: string): Promise<Session> {
184 return this.request('GET', `/api/sessions/${id}`);
185 }
186
187 // ============ Search ============
188
189 async searchNodes(projectId: string, req: SearchRequest): Promise<Array<GraphNode>> {
190 return this.request('POST', `/api/projects/${projectId}/search`, req);
191 }
192
193 // ============ Agents ============
194
195 async listAgents(goalId: string): Promise<Array<ActiveAgent>> {
196 return this.request('GET', `/api/goals/${goalId}/agents`);
197 }
198
199 // ============ Health ============
200
201 async healthCheck(): Promise<{ status: string }> {
202 return this.request('GET', '/api/health');
203 }
204}
205
206/**
207 * Singleton-style factory for creating an API client instance.
208 */
209export function createApiClient(baseUrl?: string): ApiClient {
210 return new ApiClient(baseUrl);
211}