apteva 0.4.3 → 0.4.5

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.
@@ -0,0 +1,743 @@
1
+ import { existsSync, rmSync } from "fs";
2
+ import { join } from "path";
3
+ import { json, isDev } from "./helpers";
4
+ import {
5
+ agentFetch,
6
+ toApiAgent,
7
+ checkPortFree,
8
+ startAgentProcess,
9
+ buildAgentConfig,
10
+ pushConfigToAgent,
11
+ pushSkillsToAgent,
12
+ fetchFromAgent,
13
+ AGENTS_DATA_DIR,
14
+ META_AGENT_ID,
15
+ setAgentStatus,
16
+ } from "./agent-utils";
17
+ import { AgentDB, McpServerDB, SkillDB, TelemetryDB, generateId, getMultiAgentConfig, type Agent } from "../../db";
18
+ import { ProviderKeys } from "../../providers";
19
+ import { agentProcesses } from "../../server";
20
+ import type { AuthContext } from "../../auth/middleware";
21
+
22
+ export async function handleAgentRoutes(
23
+ req: Request,
24
+ path: string,
25
+ method: string,
26
+ authContext?: AuthContext,
27
+ ): Promise<Response | null> {
28
+ // ==================== AGENT CRUD ====================
29
+
30
+ // GET /api/agents - List all agents (excludes meta agent)
31
+ if (path === "/api/agents" && method === "GET") {
32
+ const agents = AgentDB.findAll().filter(a => a.id !== META_AGENT_ID);
33
+ return json({ agents: agents.map(toApiAgent) });
34
+ }
35
+
36
+ // POST /api/agents - Create a new agent
37
+ if (path === "/api/agents" && method === "POST") {
38
+ try {
39
+ const body = await req.json();
40
+ const { name, model, provider, systemPrompt, features, projectId } = body;
41
+
42
+ if (!name) {
43
+ return json({ error: "Name is required" }, 400);
44
+ }
45
+
46
+ // Import DEFAULT_FEATURES from db.ts
47
+ const { DEFAULT_FEATURES } = await import("../../db");
48
+
49
+ const agent = AgentDB.create({
50
+ id: generateId(),
51
+ name,
52
+ model: model || "claude-sonnet-4-5",
53
+ provider: provider || "anthropic",
54
+ system_prompt: systemPrompt || "You are a helpful assistant.",
55
+ features: features || DEFAULT_FEATURES,
56
+ mcp_servers: body.mcpServers || [],
57
+ skills: body.skills || [],
58
+ project_id: projectId || null,
59
+ } as any);
60
+
61
+ return json({ agent: toApiAgent(agent) }, 201);
62
+ } catch (e) {
63
+ console.error("Create agent error:", e);
64
+ return json({ error: "Invalid request body" }, 400);
65
+ }
66
+ }
67
+
68
+ // GET /api/agents/:id - Get a specific agent
69
+ const agentMatch = path.match(/^\/api\/agents\/([^/]+)$/);
70
+ if (agentMatch && method === "GET") {
71
+ const agent = AgentDB.findById(agentMatch[1]);
72
+ if (!agent) {
73
+ return json({ error: "Agent not found" }, 404);
74
+ }
75
+ return json({ agent: toApiAgent(agent) });
76
+ }
77
+
78
+ // PUT /api/agents/:id - Update an agent
79
+ if (agentMatch && method === "PUT") {
80
+ const agent = AgentDB.findById(agentMatch[1]);
81
+ if (!agent) {
82
+ return json({ error: "Agent not found" }, 404);
83
+ }
84
+
85
+ try {
86
+ const body = await req.json();
87
+ const updates: Partial<Agent> = {};
88
+
89
+ if (body.name !== undefined) updates.name = body.name;
90
+ if (body.model !== undefined) updates.model = body.model;
91
+ if (body.provider !== undefined) updates.provider = body.provider;
92
+ if (body.systemPrompt !== undefined) updates.system_prompt = body.systemPrompt;
93
+ if (body.features !== undefined) updates.features = body.features;
94
+ if (body.mcpServers !== undefined) updates.mcp_servers = body.mcpServers;
95
+ if (body.skills !== undefined) updates.skills = body.skills;
96
+ if (body.projectId !== undefined) updates.project_id = body.projectId;
97
+
98
+ const updated = AgentDB.update(agentMatch[1], updates);
99
+
100
+ // If agent is running, push the new config and skills
101
+ if (updated && updated.status === "running" && updated.port) {
102
+ const providerKey = ProviderKeys.getDecrypted(updated.provider);
103
+ if (providerKey) {
104
+ const config = buildAgentConfig(updated, providerKey);
105
+ const configResult = await pushConfigToAgent(updated.id, updated.port, config);
106
+ if (!configResult.success) {
107
+ console.error(`Failed to push config to running agent: ${configResult.error}`);
108
+ }
109
+ // Push skills via /skills endpoint
110
+ if (config.skills?.definitions?.length > 0) {
111
+ const skillsResult = await pushSkillsToAgent(updated.id, updated.port, config.skills.definitions);
112
+ if (!skillsResult.success) {
113
+ console.error(`Failed to push skills to running agent: ${skillsResult.error}`);
114
+ }
115
+ }
116
+ }
117
+ }
118
+
119
+ return json({ agent: updated ? toApiAgent(updated) : null });
120
+ } catch (e) {
121
+ return json({ error: "Invalid request body" }, 400);
122
+ }
123
+ }
124
+
125
+ // DELETE /api/agents/:id - Delete an agent
126
+ if (agentMatch && method === "DELETE") {
127
+ const agentId = agentMatch[1];
128
+ const agent = AgentDB.findById(agentId);
129
+ if (!agent) {
130
+ return json({ error: "Agent not found" }, 404);
131
+ }
132
+
133
+ // Stop the agent if running
134
+ const agentProc = agentProcesses.get(agentId);
135
+ const port = agent.port;
136
+
137
+ if (agentProc) {
138
+ // Try graceful shutdown first
139
+ if (port) {
140
+ try {
141
+ await fetch(`http://localhost:${port}/shutdown`, {
142
+ method: "POST",
143
+ signal: AbortSignal.timeout(2000),
144
+ });
145
+ await new Promise(r => setTimeout(r, 500));
146
+ } catch {
147
+ // Graceful shutdown failed
148
+ }
149
+ }
150
+
151
+ try {
152
+ agentProc.proc.kill();
153
+ } catch {
154
+ // Already dead
155
+ }
156
+ agentProcesses.delete(agentId);
157
+
158
+ // Ensure port is freed
159
+ if (port) {
160
+ const isFree = await checkPortFree(port);
161
+ if (!isFree) {
162
+ try {
163
+ const { execSync } = await import("child_process");
164
+ execSync(`lsof -ti :${port} | xargs -r kill -9 2>/dev/null || true`, { stdio: "ignore" });
165
+ } catch {
166
+ // Ignore
167
+ }
168
+ }
169
+ }
170
+ }
171
+
172
+ // Delete agent's telemetry data
173
+ TelemetryDB.deleteByAgent(agentId);
174
+
175
+ // Delete agent's data directory (contains threads, messages, etc.)
176
+ const agentDataDir = join(AGENTS_DATA_DIR, agentId);
177
+ if (existsSync(agentDataDir)) {
178
+ try {
179
+ rmSync(agentDataDir, { recursive: true, force: true });
180
+ console.log(`Deleted agent data directory: ${agentDataDir}`);
181
+ } catch (err) {
182
+ console.error(`Failed to delete agent data directory: ${err}`);
183
+ }
184
+ }
185
+
186
+ AgentDB.delete(agentId);
187
+ return json({ success: true });
188
+ }
189
+
190
+ // ==================== AGENT API KEY ====================
191
+
192
+ // GET /api/agents/:id/api-key - Get the agent's API key (masked)
193
+ const apiKeyMatch = path.match(/^\/api\/agents\/([^/]+)\/api-key$/);
194
+ if (apiKeyMatch && method === "GET") {
195
+ const agent = AgentDB.findById(apiKeyMatch[1]);
196
+ if (!agent) {
197
+ return json({ error: "Agent not found" }, 404);
198
+ }
199
+
200
+ const apiKey = AgentDB.getApiKey(agent.id);
201
+ if (!apiKey) {
202
+ return json({ error: "No API key found for this agent" }, 404);
203
+ }
204
+
205
+ // Return masked key (show only first 8 chars)
206
+ const masked = apiKey.substring(0, 8) + "..." + apiKey.substring(apiKey.length - 4);
207
+ return json({
208
+ apiKey: masked,
209
+ hasKey: true,
210
+ });
211
+ }
212
+
213
+ // POST /api/agents/:id/api-key - Regenerate the agent's API key
214
+ if (apiKeyMatch && method === "POST") {
215
+ const agent = AgentDB.findById(apiKeyMatch[1]);
216
+ if (!agent) {
217
+ return json({ error: "Agent not found" }, 404);
218
+ }
219
+
220
+ const newKey = AgentDB.regenerateApiKey(agent.id);
221
+ if (!newKey) {
222
+ return json({ error: "Failed to regenerate API key" }, 500);
223
+ }
224
+
225
+ // Return the full new key (only time it's fully visible)
226
+ return json({
227
+ apiKey: newKey,
228
+ message: "API key regenerated. This is the only time the full key will be shown.",
229
+ });
230
+ }
231
+
232
+ // ==================== AGENT LIFECYCLE ====================
233
+
234
+ // POST /api/agents/:id/start - Start an agent
235
+ const startMatch = path.match(/^\/api\/agents\/([^/]+)\/start$/);
236
+ if (startMatch && method === "POST") {
237
+ const agent = AgentDB.findById(startMatch[1]);
238
+ if (!agent) {
239
+ return json({ error: "Agent not found" }, 404);
240
+ }
241
+
242
+ const result = await startAgentProcess(agent);
243
+ if (!result.success) {
244
+ return json({ error: result.error }, 400);
245
+ }
246
+
247
+ const updated = AgentDB.findById(agent.id);
248
+ return json({ agent: updated ? toApiAgent(updated) : null, message: `Agent started on port ${result.port}` });
249
+ }
250
+
251
+ // POST /api/agents/:id/stop - Stop an agent
252
+ const stopMatch = path.match(/^\/api\/agents\/([^/]+)\/stop$/);
253
+ if (stopMatch && method === "POST") {
254
+ const agent = AgentDB.findById(stopMatch[1]);
255
+ if (!agent) {
256
+ return json({ error: "Agent not found" }, 404);
257
+ }
258
+
259
+ const agentProc = agentProcesses.get(agent.id);
260
+ const port = agent.port;
261
+
262
+ if (agentProc) {
263
+ console.log(`Stopping agent ${agent.name} (pid: ${agentProc.proc.pid})...`);
264
+
265
+ // Try graceful shutdown first
266
+ if (port) {
267
+ try {
268
+ await fetch(`http://localhost:${port}/shutdown`, {
269
+ method: "POST",
270
+ signal: AbortSignal.timeout(2000),
271
+ });
272
+ await new Promise(r => setTimeout(r, 500)); // Wait for graceful shutdown
273
+ } catch {
274
+ // Graceful shutdown failed or timed out
275
+ }
276
+ }
277
+
278
+ // Force kill if still running
279
+ try {
280
+ agentProc.proc.kill();
281
+ } catch {
282
+ // Already dead
283
+ }
284
+ agentProcesses.delete(agent.id);
285
+
286
+ // Ensure port is freed
287
+ if (port) {
288
+ const isFree = await checkPortFree(port);
289
+ if (!isFree) {
290
+ // Force kill by port
291
+ try {
292
+ const { execSync } = await import("child_process");
293
+ execSync(`lsof -ti :${port} | xargs -r kill -9 2>/dev/null || true`, { stdio: "ignore" });
294
+ } catch {
295
+ // Ignore
296
+ }
297
+ }
298
+ }
299
+ }
300
+
301
+ const updated = setAgentStatus(agent.id, "stopped", "user_stopped");
302
+ return json({ agent: updated ? toApiAgent(updated) : null, message: "Agent stopped" });
303
+ }
304
+
305
+ // POST /api/agents/:id/chat - Proxy chat to agent binary with streaming
306
+ const chatMatch = path.match(/^\/api\/agents\/([^/]+)\/chat$/);
307
+ if (chatMatch && method === "POST") {
308
+ const agent = AgentDB.findById(chatMatch[1]);
309
+ if (!agent) {
310
+ return json({ error: "Agent not found" }, 404);
311
+ }
312
+
313
+ if (agent.status !== "running" || !agent.port) {
314
+ return json({ error: "Agent is not running" }, 400);
315
+ }
316
+
317
+ try {
318
+ const body = await req.json();
319
+
320
+ // Proxy to the agent's /chat endpoint with authentication
321
+ const response = await agentFetch(agent.id, agent.port, "/chat", {
322
+ method: "POST",
323
+ headers: { "Content-Type": "application/json" },
324
+ body: JSON.stringify(body),
325
+ });
326
+
327
+ // Stream the response back
328
+ if (!response.ok) {
329
+ const errorText = await response.text();
330
+ return json({ error: `Agent error: ${errorText}` }, response.status);
331
+ }
332
+
333
+ // Return streaming response with proper headers
334
+ return new Response(response.body, {
335
+ status: 200,
336
+ headers: {
337
+ "Content-Type": response.headers.get("Content-Type") || "text/event-stream",
338
+ "Cache-Control": "no-cache",
339
+ "Connection": "keep-alive",
340
+ },
341
+ });
342
+ } catch (err) {
343
+ console.error(`Chat proxy error: ${err}`);
344
+ return json({ error: `Failed to proxy chat: ${err}` }, 500);
345
+ }
346
+ }
347
+
348
+ // ==================== THREAD & MESSAGE PROXY ====================
349
+
350
+ // GET/POST /api/agents/:id/threads
351
+ const threadsListMatch = path.match(/^\/api\/agents\/([^/]+)\/threads$/);
352
+ if (threadsListMatch && method === "GET") {
353
+ const agent = AgentDB.findById(threadsListMatch[1]);
354
+ if (!agent) return json({ error: "Agent not found" }, 404);
355
+ if (agent.status !== "running" || !agent.port) return json({ error: "Agent is not running" }, 400);
356
+
357
+ try {
358
+ const response = await agentFetch(agent.id, agent.port, "/threads", {
359
+ method: "GET",
360
+ headers: { "Accept": "application/json" },
361
+ });
362
+ if (!response.ok) {
363
+ const errorText = await response.text();
364
+ return json({ error: `Agent error: ${errorText}` }, response.status);
365
+ }
366
+ const data = await response.json();
367
+ return json(data);
368
+ } catch (err) {
369
+ console.error(`Threads list proxy error: ${err}`);
370
+ return json({ error: `Failed to fetch threads: ${err}` }, 500);
371
+ }
372
+ }
373
+
374
+ if (threadsListMatch && method === "POST") {
375
+ const agent = AgentDB.findById(threadsListMatch[1]);
376
+ if (!agent) return json({ error: "Agent not found" }, 404);
377
+ if (agent.status !== "running" || !agent.port) return json({ error: "Agent is not running" }, 400);
378
+
379
+ try {
380
+ const body = await req.json().catch(() => ({}));
381
+ const response = await agentFetch(agent.id, agent.port, "/threads", {
382
+ method: "POST",
383
+ headers: { "Content-Type": "application/json" },
384
+ body: JSON.stringify(body),
385
+ });
386
+ if (!response.ok) {
387
+ const errorText = await response.text();
388
+ return json({ error: `Agent error: ${errorText}` }, response.status);
389
+ }
390
+ const data = await response.json();
391
+ return json(data, 201);
392
+ } catch (err) {
393
+ console.error(`Thread create proxy error: ${err}`);
394
+ return json({ error: `Failed to create thread: ${err}` }, 500);
395
+ }
396
+ }
397
+
398
+ // GET/DELETE /api/agents/:id/threads/:threadId
399
+ const threadDetailMatch = path.match(/^\/api\/agents\/([^/]+)\/threads\/([^/]+)$/);
400
+ if (threadDetailMatch && method === "GET") {
401
+ const agent = AgentDB.findById(threadDetailMatch[1]);
402
+ if (!agent) return json({ error: "Agent not found" }, 404);
403
+ if (agent.status !== "running" || !agent.port) return json({ error: "Agent is not running" }, 400);
404
+
405
+ try {
406
+ const threadId = threadDetailMatch[2];
407
+ const response = await agentFetch(agent.id, agent.port, `/threads/${threadId}`, {
408
+ method: "GET",
409
+ headers: { "Accept": "application/json" },
410
+ });
411
+ if (!response.ok) {
412
+ const errorText = await response.text();
413
+ return json({ error: `Agent error: ${errorText}` }, response.status);
414
+ }
415
+ const data = await response.json();
416
+ return json(data);
417
+ } catch (err) {
418
+ console.error(`Thread detail proxy error: ${err}`);
419
+ return json({ error: `Failed to fetch thread: ${err}` }, 500);
420
+ }
421
+ }
422
+
423
+ if (threadDetailMatch && method === "DELETE") {
424
+ const agent = AgentDB.findById(threadDetailMatch[1]);
425
+ if (!agent) return json({ error: "Agent not found" }, 404);
426
+ if (agent.status !== "running" || !agent.port) return json({ error: "Agent is not running" }, 400);
427
+
428
+ try {
429
+ const threadId = threadDetailMatch[2];
430
+ const response = await agentFetch(agent.id, agent.port, `/threads/${threadId}`, { method: "DELETE" });
431
+ if (!response.ok) {
432
+ const errorText = await response.text();
433
+ return json({ error: `Agent error: ${errorText}` }, response.status);
434
+ }
435
+ return json({ success: true });
436
+ } catch (err) {
437
+ console.error(`Thread delete proxy error: ${err}`);
438
+ return json({ error: `Failed to delete thread: ${err}` }, 500);
439
+ }
440
+ }
441
+
442
+ // GET /api/agents/:id/threads/:threadId/messages
443
+ const threadMessagesMatch = path.match(/^\/api\/agents\/([^/]+)\/threads\/([^/]+)\/messages$/);
444
+ if (threadMessagesMatch && method === "GET") {
445
+ const agent = AgentDB.findById(threadMessagesMatch[1]);
446
+ if (!agent) return json({ error: "Agent not found" }, 404);
447
+ if (agent.status !== "running" || !agent.port) return json({ error: "Agent is not running" }, 400);
448
+
449
+ try {
450
+ const threadId = threadMessagesMatch[2];
451
+ const response = await agentFetch(agent.id, agent.port, `/threads/${threadId}/messages`, {
452
+ method: "GET",
453
+ headers: { "Accept": "application/json" },
454
+ });
455
+ if (!response.ok) {
456
+ const errorText = await response.text();
457
+ return json({ error: `Agent error: ${errorText}` }, response.status);
458
+ }
459
+ const data = await response.json();
460
+ return json(data);
461
+ } catch (err) {
462
+ console.error(`Thread messages proxy error: ${err}`);
463
+ return json({ error: `Failed to fetch messages: ${err}` }, 500);
464
+ }
465
+ }
466
+
467
+ // ==================== MEMORY PROXY ====================
468
+
469
+ const memoriesMatch = path.match(/^\/api\/agents\/([^/]+)\/memories$/);
470
+ if (memoriesMatch && method === "GET") {
471
+ const agent = AgentDB.findById(memoriesMatch[1]);
472
+ if (!agent) return json({ error: "Agent not found" }, 404);
473
+ if (agent.status !== "running" || !agent.port) return json({ error: "Agent is not running" }, 400);
474
+
475
+ try {
476
+ const url = new URL(req.url);
477
+ const threadId = url.searchParams.get("thread_id") || "";
478
+ const endpoint = `/memories${threadId ? `?thread_id=${threadId}` : ""}`;
479
+ const response = await agentFetch(agent.id, agent.port, endpoint, {
480
+ method: "GET",
481
+ headers: { "Accept": "application/json" },
482
+ });
483
+ if (!response.ok) {
484
+ const errorText = await response.text();
485
+ return json({ error: `Agent error: ${errorText}` }, response.status);
486
+ }
487
+ const data = await response.json();
488
+ return json(data);
489
+ } catch (err) {
490
+ console.error(`Memories list proxy error: ${err}`);
491
+ return json({ error: `Failed to fetch memories: ${err}` }, 500);
492
+ }
493
+ }
494
+
495
+ if (memoriesMatch && method === "DELETE") {
496
+ const agent = AgentDB.findById(memoriesMatch[1]);
497
+ if (!agent) return json({ error: "Agent not found" }, 404);
498
+ if (agent.status !== "running" || !agent.port) return json({ error: "Agent is not running" }, 400);
499
+
500
+ try {
501
+ const response = await agentFetch(agent.id, agent.port, "/memories", { method: "DELETE" });
502
+ if (!response.ok) {
503
+ const errorText = await response.text();
504
+ return json({ error: `Agent error: ${errorText}` }, response.status);
505
+ }
506
+ return json({ success: true });
507
+ } catch (err) {
508
+ console.error(`Memories clear proxy error: ${err}`);
509
+ return json({ error: `Failed to clear memories: ${err}` }, 500);
510
+ }
511
+ }
512
+
513
+ const memoryDeleteMatch = path.match(/^\/api\/agents\/([^/]+)\/memories\/([^/]+)$/);
514
+ if (memoryDeleteMatch && method === "DELETE") {
515
+ const agent = AgentDB.findById(memoryDeleteMatch[1]);
516
+ if (!agent) return json({ error: "Agent not found" }, 404);
517
+ if (agent.status !== "running" || !agent.port) return json({ error: "Agent is not running" }, 400);
518
+
519
+ try {
520
+ const memoryId = memoryDeleteMatch[2];
521
+ const response = await agentFetch(agent.id, agent.port, `/memories/${memoryId}`, { method: "DELETE" });
522
+ if (!response.ok) {
523
+ const errorText = await response.text();
524
+ return json({ error: `Agent error: ${errorText}` }, response.status);
525
+ }
526
+ return json({ success: true });
527
+ } catch (err) {
528
+ console.error(`Memory delete proxy error: ${err}`);
529
+ return json({ error: `Failed to delete memory: ${err}` }, 500);
530
+ }
531
+ }
532
+
533
+ // ==================== FILES PROXY ====================
534
+
535
+ const filesMatch = path.match(/^\/api\/agents\/([^/]+)\/files$/);
536
+ if (filesMatch && method === "POST") {
537
+ const agent = AgentDB.findById(filesMatch[1]);
538
+ if (!agent) return json({ error: "Agent not found" }, 404);
539
+ if (agent.status !== "running" || !agent.port) return json({ error: "Agent is not running" }, 400);
540
+
541
+ try {
542
+ const contentType = req.headers.get("content-type") || "";
543
+ const body = await req.arrayBuffer();
544
+ const response = await agentFetch(agent.id, agent.port, "/files", {
545
+ method: "POST",
546
+ headers: { "Content-Type": contentType },
547
+ body: body,
548
+ });
549
+ if (!response.ok) {
550
+ const errorText = await response.text();
551
+ return json({ error: `Agent error: ${errorText}` }, response.status);
552
+ }
553
+ const data = await response.json();
554
+ return json(data);
555
+ } catch (err) {
556
+ console.error(`File upload proxy error: ${err}`);
557
+ return json({ error: `Failed to upload file: ${err}` }, 500);
558
+ }
559
+ }
560
+
561
+ if (filesMatch && method === "GET") {
562
+ const agent = AgentDB.findById(filesMatch[1]);
563
+ if (!agent) return json({ error: "Agent not found" }, 404);
564
+ if (agent.status !== "running" || !agent.port) return json({ error: "Agent is not running" }, 400);
565
+
566
+ try {
567
+ const url = new URL(req.url);
568
+ const params = new URLSearchParams();
569
+ if (url.searchParams.get("thread_id")) params.set("thread_id", url.searchParams.get("thread_id")!);
570
+ if (url.searchParams.get("limit")) params.set("limit", url.searchParams.get("limit")!);
571
+
572
+ const endpoint = `/files${params.toString() ? `?${params}` : ""}`;
573
+ const response = await agentFetch(agent.id, agent.port, endpoint, {
574
+ method: "GET",
575
+ headers: { "Accept": "application/json" },
576
+ });
577
+ if (!response.ok) {
578
+ const errorText = await response.text();
579
+ return json({ error: `Agent error: ${errorText}` }, response.status);
580
+ }
581
+ const data = await response.json();
582
+ return json(data);
583
+ } catch (err) {
584
+ console.error(`Files list proxy error: ${err}`);
585
+ return json({ error: `Failed to fetch files: ${err}` }, 500);
586
+ }
587
+ }
588
+
589
+ // GET/DELETE /api/agents/:id/files/:fileId/download and /api/agents/:id/files/:fileId
590
+ const fileDownloadMatch = path.match(/^\/api\/agents\/([^/]+)\/files\/([^/]+)\/download$/);
591
+ if (fileDownloadMatch && method === "GET") {
592
+ const agent = AgentDB.findById(fileDownloadMatch[1]);
593
+ if (!agent) return json({ error: "Agent not found" }, 404);
594
+ if (agent.status !== "running" || !agent.port) return json({ error: "Agent is not running" }, 400);
595
+
596
+ try {
597
+ const fileId = fileDownloadMatch[2];
598
+ const response = await agentFetch(agent.id, agent.port, `/files/${fileId}/download`);
599
+ if (!response.ok) {
600
+ const errorText = await response.text();
601
+ return json({ error: `Agent error: ${errorText}` }, response.status);
602
+ }
603
+ return new Response(response.body, {
604
+ status: response.status,
605
+ headers: {
606
+ "Content-Type": response.headers.get("Content-Type") || "application/octet-stream",
607
+ "Content-Disposition": response.headers.get("Content-Disposition") || "attachment",
608
+ "Content-Length": response.headers.get("Content-Length") || "",
609
+ },
610
+ });
611
+ } catch (err) {
612
+ console.error(`File download proxy error: ${err}`);
613
+ return json({ error: `Failed to download file: ${err}` }, 500);
614
+ }
615
+ }
616
+
617
+ const fileGetMatch = path.match(/^\/api\/agents\/([^/]+)\/files\/([^/]+)$/);
618
+ if (fileGetMatch && method === "GET") {
619
+ const agent = AgentDB.findById(fileGetMatch[1]);
620
+ if (!agent) return json({ error: "Agent not found" }, 404);
621
+ if (agent.status !== "running" || !agent.port) return json({ error: "Agent is not running" }, 400);
622
+
623
+ try {
624
+ const fileId = fileGetMatch[2];
625
+ const response = await agentFetch(agent.id, agent.port, `/files/${fileId}`, {
626
+ method: "GET",
627
+ headers: { "Accept": "application/json" },
628
+ });
629
+ if (!response.ok) {
630
+ const errorText = await response.text();
631
+ return json({ error: `Agent error: ${errorText}` }, response.status);
632
+ }
633
+ const data = await response.json();
634
+ return json(data);
635
+ } catch (err) {
636
+ console.error(`File get proxy error: ${err}`);
637
+ return json({ error: `Failed to fetch file: ${err}` }, 500);
638
+ }
639
+ }
640
+
641
+ if (fileGetMatch && method === "DELETE") {
642
+ const agent = AgentDB.findById(fileGetMatch[1]);
643
+ if (!agent) return json({ error: "Agent not found" }, 404);
644
+ if (agent.status !== "running" || !agent.port) return json({ error: "Agent is not running" }, 400);
645
+
646
+ try {
647
+ const fileId = fileGetMatch[2];
648
+ const response = await agentFetch(agent.id, agent.port, `/files/${fileId}`, { method: "DELETE" });
649
+ if (!response.ok) {
650
+ const errorText = await response.text();
651
+ return json({ error: `Agent error: ${errorText}` }, response.status);
652
+ }
653
+ return json({ success: true });
654
+ } catch (err) {
655
+ console.error(`File delete proxy error: ${err}`);
656
+ return json({ error: `Failed to delete file: ${err}` }, 500);
657
+ }
658
+ }
659
+
660
+ // ==================== DISCOVERY/PEERS PROXY ====================
661
+
662
+ // GET /api/discovery/agents - Central discovery endpoint for agents to find peers
663
+ if (path === "/api/discovery/agents" && method === "GET") {
664
+ const url = new URL(req.url); // BUG FIX: was missing url declaration
665
+ const group = url.searchParams.get("group");
666
+ const excludeId = url.searchParams.get("exclude") || req.headers.get("X-Agent-ID");
667
+
668
+ // Find all running agents in the same group
669
+ const allAgents = AgentDB.findAll();
670
+ const peers = allAgents
671
+ .filter(a => {
672
+ if (a.status !== "running" || !a.port) return false;
673
+ if (excludeId && a.id === excludeId) return false;
674
+ const agentConfig = getMultiAgentConfig(a.features, a.project_id);
675
+ if (!agentConfig.enabled) return false;
676
+ if (group) {
677
+ const peerGroup = agentConfig.group || a.project_id;
678
+ if (peerGroup !== group) return false;
679
+ }
680
+ return true;
681
+ })
682
+ .map(a => {
683
+ const agentConfig = getMultiAgentConfig(a.features, a.project_id);
684
+ return {
685
+ id: a.id,
686
+ name: a.name,
687
+ url: `http://localhost:${a.port}`,
688
+ mode: agentConfig.mode || "worker",
689
+ group: agentConfig.group || a.project_id,
690
+ };
691
+ });
692
+
693
+ return json({ agents: peers });
694
+ }
695
+
696
+ // GET /api/agents/:id/peers - Get discovered peer agents
697
+ const peersMatch = path.match(/^\/api\/agents\/([^/]+)\/peers$/);
698
+ if (peersMatch && method === "GET") {
699
+ const agent = AgentDB.findById(peersMatch[1]);
700
+ if (!agent) return json({ error: "Agent not found" }, 404);
701
+ if (agent.status !== "running" || !agent.port) return json({ error: "Agent is not running" }, 400);
702
+
703
+ try {
704
+ const response = await agentFetch(agent.id, agent.port, "/discovery/agents", {
705
+ method: "GET",
706
+ headers: { "Accept": "application/json" },
707
+ });
708
+ if (!response.ok) {
709
+ const errorText = await response.text();
710
+ return json({ error: `Agent error: ${errorText}` }, response.status);
711
+ }
712
+ const data = await response.json();
713
+ return json(data);
714
+ } catch (err) {
715
+ console.error(`Peers list proxy error: ${err}`);
716
+ return json({ error: `Failed to fetch peers: ${err}` }, 500);
717
+ }
718
+ }
719
+
720
+ // ==================== AGENT TASKS ====================
721
+
722
+ // GET /api/agents/:id/tasks - Get tasks from a specific agent
723
+ const agentTasksMatch = path.match(/^\/api\/agents\/([^/]+)\/tasks$/);
724
+ if (agentTasksMatch && method === "GET") {
725
+ const agentId = agentTasksMatch[1];
726
+ const agent = AgentDB.findById(agentId);
727
+
728
+ if (!agent) return json({ error: "Agent not found" }, 404);
729
+ if (agent.status !== "running" || !agent.port) return json({ error: "Agent is not running" }, 400);
730
+
731
+ const url = new URL(req.url);
732
+ const status = url.searchParams.get("status") || "all";
733
+
734
+ const data = await fetchFromAgent(agent.id, agent.port, `/tasks?status=${status}`);
735
+ if (!data) {
736
+ return json({ error: "Failed to fetch tasks from agent" }, 500);
737
+ }
738
+
739
+ return json(data);
740
+ }
741
+
742
+ return null;
743
+ }