apteva 0.2.7 → 0.2.9

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.
Files changed (46) hide show
  1. package/dist/App.m4hg4bxq.js +218 -0
  2. package/dist/index.html +4 -2
  3. package/dist/styles.css +1 -1
  4. package/package.json +1 -1
  5. package/src/auth/index.ts +386 -0
  6. package/src/auth/middleware.ts +183 -0
  7. package/src/binary.ts +19 -1
  8. package/src/db.ts +688 -45
  9. package/src/integrations/composio.ts +437 -0
  10. package/src/integrations/index.ts +80 -0
  11. package/src/openapi.ts +1724 -0
  12. package/src/routes/api.ts +1476 -118
  13. package/src/routes/auth.ts +242 -0
  14. package/src/server.ts +121 -11
  15. package/src/web/App.tsx +64 -19
  16. package/src/web/components/agents/AgentCard.tsx +24 -22
  17. package/src/web/components/agents/AgentPanel.tsx +810 -45
  18. package/src/web/components/agents/AgentsView.tsx +81 -9
  19. package/src/web/components/agents/CreateAgentModal.tsx +28 -1
  20. package/src/web/components/api/ApiDocsPage.tsx +583 -0
  21. package/src/web/components/auth/CreateAccountStep.tsx +176 -0
  22. package/src/web/components/auth/LoginPage.tsx +91 -0
  23. package/src/web/components/auth/index.ts +2 -0
  24. package/src/web/components/common/Icons.tsx +56 -0
  25. package/src/web/components/common/Modal.tsx +184 -1
  26. package/src/web/components/dashboard/Dashboard.tsx +70 -22
  27. package/src/web/components/index.ts +3 -0
  28. package/src/web/components/layout/Header.tsx +135 -18
  29. package/src/web/components/layout/Sidebar.tsx +87 -43
  30. package/src/web/components/mcp/IntegrationsPanel.tsx +743 -0
  31. package/src/web/components/mcp/McpPage.tsx +451 -63
  32. package/src/web/components/onboarding/OnboardingWizard.tsx +64 -8
  33. package/src/web/components/settings/SettingsPage.tsx +340 -26
  34. package/src/web/components/tasks/TasksPage.tsx +22 -20
  35. package/src/web/components/telemetry/TelemetryPage.tsx +163 -61
  36. package/src/web/context/AuthContext.tsx +230 -0
  37. package/src/web/context/ProjectContext.tsx +182 -0
  38. package/src/web/context/index.ts +5 -0
  39. package/src/web/hooks/useAgents.ts +18 -6
  40. package/src/web/hooks/useOnboarding.ts +20 -4
  41. package/src/web/hooks/useProviders.ts +15 -5
  42. package/src/web/icon.png +0 -0
  43. package/src/web/index.html +1 -1
  44. package/src/web/styles.css +12 -0
  45. package/src/web/types.ts +10 -1
  46. package/dist/App.3kb50qa3.js +0 -213
package/src/routes/api.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  import { spawn } from "bun";
2
2
  import { join } from "path";
3
3
  import { homedir } from "os";
4
- import { mkdirSync, existsSync } from "fs";
5
- import { agentProcesses, BINARY_PATH, getNextPort, getBinaryStatus, BIN_DIR, telemetryBroadcaster, type TelemetryEvent } from "../server";
6
- import { AgentDB, McpServerDB, TelemetryDB, generateId, type Agent, type AgentFeatures, type McpServer } from "../db";
4
+ import { mkdirSync, existsSync, rmSync } from "fs";
5
+ import { agentProcesses, agentsStarting, BINARY_PATH, getNextPort, getBinaryStatus, BIN_DIR, telemetryBroadcaster, type TelemetryEvent } from "../server";
6
+ import { AgentDB, McpServerDB, TelemetryDB, UserDB, ProjectDB, generateId, type Agent, type AgentFeatures, type McpServer, type Project } from "../db";
7
7
  import { ProviderKeys, Onboarding, getProvidersWithStatus, PROVIDERS, type ProviderId } from "../providers";
8
+ import { createUser, hashPassword, validatePassword } from "../auth";
9
+ import type { AuthContext } from "../auth/middleware";
8
10
  import {
9
11
  binaryExists,
10
12
  checkForUpdates,
@@ -21,7 +23,14 @@ import {
21
23
  callMcpTool,
22
24
  getMcpProcess,
23
25
  getMcpProxyUrl,
26
+ getHttpMcpClient,
24
27
  } from "../mcp-client";
28
+ import { openApiSpec } from "../openapi";
29
+ import { getProvider, getProviderIds, registerProvider } from "../integrations";
30
+ import { ComposioProvider } from "../integrations/composio";
31
+
32
+ // Register integration providers
33
+ registerProvider(ComposioProvider);
25
34
 
26
35
  // Data directory for agent instances (in ~/.apteva/agents/)
27
36
  const AGENTS_DATA_DIR = process.env.DATA_DIR
@@ -35,7 +44,13 @@ function json(data: unknown, status = 200): Response {
35
44
  });
36
45
  }
37
46
 
47
+ const isDev = process.env.NODE_ENV !== "production";
48
+ function debug(...args: unknown[]) {
49
+ if (isDev) console.log("[api]", ...args);
50
+ }
51
+
38
52
  // Wait for agent to be healthy (with timeout)
53
+ // Note: /health endpoint is whitelisted in agent, no auth needed
39
54
  async function waitForAgentHealth(port: number, maxAttempts = 30, delayMs = 200): Promise<boolean> {
40
55
  for (let i = 0; i < maxAttempts; i++) {
41
56
  try {
@@ -51,23 +66,58 @@ async function waitForAgentHealth(port: number, maxAttempts = 30, delayMs = 200)
51
66
  return false;
52
67
  }
53
68
 
69
+ // Make authenticated request to agent
70
+ async function agentFetch(
71
+ agentId: string,
72
+ port: number,
73
+ endpoint: string,
74
+ options: RequestInit = {}
75
+ ): Promise<Response> {
76
+ const apiKey = AgentDB.getApiKey(agentId);
77
+ const headers: Record<string, string> = {
78
+ ...(options.headers as Record<string, string> || {}),
79
+ };
80
+ if (apiKey) {
81
+ headers["X-API-Key"] = apiKey;
82
+ }
83
+ return fetch(`http://localhost:${port}${endpoint}`, {
84
+ ...options,
85
+ headers,
86
+ });
87
+ }
88
+
54
89
  // Build agent config from apteva agent data
55
90
  // Note: POST /config expects flat structure WITHOUT "agent" wrapper
56
91
  function buildAgentConfig(agent: Agent, providerKey: string) {
57
92
  const features = agent.features;
58
93
 
59
94
  // Get MCP server details for the agent's selected servers
60
- // All MCP servers are accessed via HTTP proxy (apteva manages the stdio processes)
61
- const mcpServers = (agent.mcp_servers || [])
62
- .map(id => McpServerDB.findById(id))
63
- .filter((s): s is NonNullable<typeof s> => s !== null && s.status === "running" && s.port)
64
- .map(s => ({
65
- name: s.name,
66
- type: "http" as const,
67
- url: `http://localhost:${s.port}/mcp`,
68
- headers: {},
69
- enabled: true,
70
- }));
95
+ const mcpServers: Array<{ name: string; type: "http"; url: string; headers: Record<string, string>; enabled: boolean }> = [];
96
+
97
+ for (const id of agent.mcp_servers || []) {
98
+ const server = McpServerDB.findById(id);
99
+ if (!server) continue;
100
+
101
+ if (server.type === "http" && server.url) {
102
+ // Remote HTTP server (Composio, Smithery, or custom)
103
+ mcpServers.push({
104
+ name: server.name,
105
+ type: "http",
106
+ url: server.url,
107
+ headers: server.headers || {},
108
+ enabled: true,
109
+ });
110
+ } else if (server.status === "running" && server.port) {
111
+ // Local MCP server (npm, github, custom)
112
+ mcpServers.push({
113
+ name: server.name,
114
+ type: "http",
115
+ url: `http://localhost:${server.port}/mcp`,
116
+ headers: {},
117
+ enabled: true,
118
+ });
119
+ }
120
+ }
71
121
 
72
122
  return {
73
123
  id: agent.id,
@@ -169,9 +219,10 @@ function buildAgentConfig(agent: Agent, providerKey: string) {
169
219
  }
170
220
 
171
221
  // Push config to running agent
172
- async function pushConfigToAgent(port: number, config: any): Promise<{ success: boolean; error?: string }> {
222
+ // Push config to running agent (with authentication)
223
+ async function pushConfigToAgent(agentId: string, port: number, config: any): Promise<{ success: boolean; error?: string }> {
173
224
  try {
174
- const res = await fetch(`http://localhost:${port}/config`, {
225
+ const res = await agentFetch(agentId, port, "/config", {
175
226
  method: "POST",
176
227
  headers: { "Content-Type": "application/json" },
177
228
  body: JSON.stringify(config),
@@ -190,38 +241,85 @@ async function pushConfigToAgent(port: number, config: any): Promise<{ success:
190
241
  // Exported helper to start an agent process (used by API route and auto-restart)
191
242
  export async function startAgentProcess(
192
243
  agent: Agent,
193
- options: { silent?: boolean } = {}
244
+ options: { silent?: boolean; cleanData?: boolean } = {}
194
245
  ): Promise<{ success: boolean; port?: number; error?: string }> {
195
- const { silent = false } = options;
246
+ const { silent = false, cleanData = false } = options;
196
247
 
197
248
  // Check if binary exists
198
249
  if (!binaryExists(BIN_DIR)) {
199
250
  return { success: false, error: "Agent binary not available" };
200
251
  }
201
252
 
202
- // Check if already running
253
+ // Check if already running (process map)
203
254
  if (agentProcesses.has(agent.id)) {
204
255
  return { success: false, error: "Agent already running" };
205
256
  }
206
257
 
258
+ // Check if already being started (race condition prevention)
259
+ if (agentsStarting.has(agent.id)) {
260
+ return { success: false, error: "Agent is already starting" };
261
+ }
262
+
263
+ // Mark as starting
264
+ agentsStarting.add(agent.id);
265
+
207
266
  // Get the API key for the agent's provider
208
267
  const providerKey = ProviderKeys.getDecrypted(agent.provider);
209
268
  if (!providerKey) {
269
+ agentsStarting.delete(agent.id);
210
270
  return { success: false, error: `No API key for provider: ${agent.provider}` };
211
271
  }
212
272
 
213
273
  // Get provider config for env var name
214
274
  const providerConfig = PROVIDERS[agent.provider as ProviderId];
215
275
  if (!providerConfig) {
276
+ agentsStarting.delete(agent.id);
216
277
  return { success: false, error: `Unknown provider: ${agent.provider}` };
217
278
  }
218
279
 
219
- // Assign port
220
- const port = getNextPort();
280
+ // Use agent's permanently assigned port
281
+ const port = agent.port;
282
+ if (!port) {
283
+ agentsStarting.delete(agent.id);
284
+ return { success: false, error: "Agent has no assigned port" };
285
+ }
286
+
287
+ // Get or create API key for the agent
288
+ const agentApiKey = AgentDB.ensureApiKey(agent.id);
289
+ if (!agentApiKey) {
290
+ agentsStarting.delete(agent.id);
291
+ return { success: false, error: "Failed to get/create agent API key" };
292
+ }
221
293
 
222
294
  try {
223
- // Create data directory for this agent
295
+ // Check if something is already running on this port (orphaned process)
296
+ try {
297
+ const res = await fetch(`http://localhost:${port}/health`, { signal: AbortSignal.timeout(500) });
298
+ if (res.ok) {
299
+ // Something is running - try to shut it down
300
+ if (!silent) {
301
+ console.log(` Port ${port} in use, stopping orphaned process...`);
302
+ }
303
+ try {
304
+ await fetch(`http://localhost:${port}/shutdown`, { method: "POST", signal: AbortSignal.timeout(1000) });
305
+ await new Promise(r => setTimeout(r, 500)); // Wait for shutdown
306
+ } catch {
307
+ // Shutdown failed - process might not support it
308
+ }
309
+ }
310
+ } catch {
311
+ // Port is free - good
312
+ }
313
+
314
+ // Handle data directory
224
315
  const agentDataDir = join(AGENTS_DATA_DIR, agent.id);
316
+ if (cleanData && existsSync(agentDataDir)) {
317
+ // Clean old data if requested
318
+ rmSync(agentDataDir, { recursive: true, force: true });
319
+ if (!silent) {
320
+ console.log(` Cleaned old data directory`);
321
+ }
322
+ }
225
323
  if (!existsSync(agentDataDir)) {
226
324
  mkdirSync(agentDataDir, { recursive: true });
227
325
  }
@@ -232,11 +330,12 @@ export async function startAgentProcess(
232
330
  console.log(` Data dir: ${agentDataDir}`);
233
331
  }
234
332
 
235
- // Build environment with provider key
333
+ // Build environment with provider key and agent API key
236
334
  const env: Record<string, string> = {
237
335
  ...process.env as Record<string, string>,
238
336
  PORT: String(port),
239
337
  DATA_DIR: agentDataDir,
338
+ AGENT_API_KEY: agentApiKey,
240
339
  [providerConfig.envVar]: providerKey,
241
340
  };
242
341
 
@@ -247,7 +346,8 @@ export async function startAgentProcess(
247
346
  stderr: "ignore",
248
347
  });
249
348
 
250
- agentProcesses.set(agent.id, proc);
349
+ // Store process with port for tracking
350
+ agentProcesses.set(agent.id, { proc, port });
251
351
 
252
352
  // Wait for agent to be healthy
253
353
  if (!silent) {
@@ -260,6 +360,7 @@ export async function startAgentProcess(
260
360
  }
261
361
  proc.kill();
262
362
  agentProcesses.delete(agent.id);
363
+ agentsStarting.delete(agent.id);
263
364
  return { success: false, error: "Health check timeout" };
264
365
  }
265
366
 
@@ -268,7 +369,7 @@ export async function startAgentProcess(
268
369
  console.log(` Pushing configuration...`);
269
370
  }
270
371
  const config = buildAgentConfig(agent, providerKey);
271
- const configResult = await pushConfigToAgent(port, config);
372
+ const configResult = await pushConfigToAgent(agent.id, port, config);
272
373
  if (!configResult.success) {
273
374
  if (!silent) {
274
375
  console.error(` Failed to configure agent: ${configResult.error}`);
@@ -278,15 +379,17 @@ export async function startAgentProcess(
278
379
  console.log(` Configuration applied successfully`);
279
380
  }
280
381
 
281
- // Update status in database
282
- AgentDB.setStatus(agent.id, "running", port);
382
+ // Update status in database (port is already set, just update status)
383
+ AgentDB.setStatus(agent.id, "running");
283
384
 
284
385
  if (!silent) {
285
386
  console.log(`Agent ${agent.name} started on port ${port} (pid: ${proc.pid})`);
286
387
  }
287
388
 
389
+ agentsStarting.delete(agent.id);
288
390
  return { success: true, port };
289
391
  } catch (err) {
392
+ agentsStarting.delete(agent.id);
290
393
  if (!silent) {
291
394
  console.error(`Failed to start agent: ${err}`);
292
395
  }
@@ -319,13 +422,43 @@ function toApiAgent(agent: Agent) {
319
422
  features: agent.features,
320
423
  mcpServers: agent.mcp_servers, // Keep IDs for backwards compatibility
321
424
  mcpServerDetails, // Include full details
425
+ projectId: agent.project_id,
322
426
  createdAt: agent.created_at,
323
427
  updatedAt: agent.updated_at,
324
428
  };
325
429
  }
326
430
 
327
- export async function handleApiRequest(req: Request, path: string): Promise<Response> {
431
+ // Transform DB project to API response format
432
+ function toApiProject(project: Project) {
433
+ return {
434
+ id: project.id,
435
+ name: project.name,
436
+ description: project.description,
437
+ color: project.color,
438
+ createdAt: project.created_at,
439
+ updatedAt: project.updated_at,
440
+ };
441
+ }
442
+
443
+ export async function handleApiRequest(req: Request, path: string, authContext?: AuthContext): Promise<Response> {
328
444
  const method = req.method;
445
+ const user = authContext?.user;
446
+
447
+ // GET /api/health - Health check endpoint (no auth required, handled before middleware in server.ts)
448
+ if (path === "/api/health" && method === "GET") {
449
+ const agentCount = AgentDB.count();
450
+ const runningAgents = AgentDB.findRunning().length;
451
+ return json({
452
+ status: "ok",
453
+ version: getAptevaVersion(),
454
+ agents: { total: agentCount, running: runningAgents },
455
+ });
456
+ }
457
+
458
+ // GET /api/openapi - OpenAPI spec (no auth required)
459
+ if (path === "/api/openapi" && method === "GET") {
460
+ return json(openApiSpec);
461
+ }
329
462
 
330
463
  // GET /api/agents - List all agents
331
464
  if (path === "/api/agents" && method === "GET") {
@@ -337,7 +470,7 @@ export async function handleApiRequest(req: Request, path: string): Promise<Resp
337
470
  if (path === "/api/agents" && method === "POST") {
338
471
  try {
339
472
  const body = await req.json();
340
- const { name, model, provider, systemPrompt, features } = body;
473
+ const { name, model, provider, systemPrompt, features, projectId } = body;
341
474
 
342
475
  if (!name) {
343
476
  return json({ error: "Name is required" }, 400);
@@ -354,6 +487,7 @@ export async function handleApiRequest(req: Request, path: string): Promise<Resp
354
487
  system_prompt: systemPrompt || "You are a helpful assistant.",
355
488
  features: features || DEFAULT_FEATURES,
356
489
  mcp_servers: body.mcpServers || [],
490
+ project_id: projectId || null,
357
491
  });
358
492
 
359
493
  return json({ agent: toApiAgent(agent) }, 201);
@@ -390,6 +524,7 @@ export async function handleApiRequest(req: Request, path: string): Promise<Resp
390
524
  if (body.systemPrompt !== undefined) updates.system_prompt = body.systemPrompt;
391
525
  if (body.features !== undefined) updates.features = body.features;
392
526
  if (body.mcpServers !== undefined) updates.mcp_servers = body.mcpServers;
527
+ if (body.projectId !== undefined) updates.project_id = body.projectId;
393
528
 
394
529
  const updated = AgentDB.update(agentMatch[1], updates);
395
530
 
@@ -398,7 +533,7 @@ export async function handleApiRequest(req: Request, path: string): Promise<Resp
398
533
  const providerKey = ProviderKeys.getDecrypted(updated.provider);
399
534
  if (providerKey) {
400
535
  const config = buildAgentConfig(updated, providerKey);
401
- const configResult = await pushConfigToAgent(updated.port, config);
536
+ const configResult = await pushConfigToAgent(updated.id, updated.port, config);
402
537
  if (!configResult.success) {
403
538
  console.error(`Failed to push config to running agent: ${configResult.error}`);
404
539
  }
@@ -413,25 +548,77 @@ export async function handleApiRequest(req: Request, path: string): Promise<Resp
413
548
 
414
549
  // DELETE /api/agents/:id - Delete an agent
415
550
  if (agentMatch && method === "DELETE") {
416
- const agent = AgentDB.findById(agentMatch[1]);
551
+ const agentId = agentMatch[1];
552
+ const agent = AgentDB.findById(agentId);
417
553
  if (!agent) {
418
554
  return json({ error: "Agent not found" }, 404);
419
555
  }
420
556
 
421
557
  // Stop the agent if running
422
- const proc = agentProcesses.get(agentMatch[1]);
423
- if (proc) {
424
- proc.kill();
425
- agentProcesses.delete(agentMatch[1]);
558
+ const agentProc = agentProcesses.get(agentId);
559
+ if (agentProc) {
560
+ agentProc.proc.kill();
561
+ agentProcesses.delete(agentId);
426
562
  }
427
563
 
428
564
  // Delete agent's telemetry data
429
- TelemetryDB.deleteByAgent(agentMatch[1]);
565
+ TelemetryDB.deleteByAgent(agentId);
566
+
567
+ // Delete agent's data directory (contains threads, messages, etc.)
568
+ const agentDataDir = join(AGENTS_DATA_DIR, agentId);
569
+ if (existsSync(agentDataDir)) {
570
+ try {
571
+ rmSync(agentDataDir, { recursive: true, force: true });
572
+ console.log(`Deleted agent data directory: ${agentDataDir}`);
573
+ } catch (err) {
574
+ console.error(`Failed to delete agent data directory: ${err}`);
575
+ }
576
+ }
430
577
 
431
- AgentDB.delete(agentMatch[1]);
578
+ AgentDB.delete(agentId);
432
579
  return json({ success: true });
433
580
  }
434
581
 
582
+ // GET /api/agents/:id/api-key - Get the agent's API key (masked)
583
+ const apiKeyGetMatch = path.match(/^\/api\/agents\/([^/]+)\/api-key$/);
584
+ if (apiKeyGetMatch && method === "GET") {
585
+ const agent = AgentDB.findById(apiKeyGetMatch[1]);
586
+ if (!agent) {
587
+ return json({ error: "Agent not found" }, 404);
588
+ }
589
+
590
+ const apiKey = AgentDB.getApiKey(agent.id);
591
+ if (!apiKey) {
592
+ return json({ error: "No API key found for this agent" }, 404);
593
+ }
594
+
595
+ // Return masked key (show only first 8 chars)
596
+ const masked = apiKey.substring(0, 8) + "..." + apiKey.substring(apiKey.length - 4);
597
+ return json({
598
+ apiKey: masked,
599
+ hasKey: true,
600
+ });
601
+ }
602
+
603
+ // POST /api/agents/:id/api-key - Regenerate the agent's API key
604
+ if (apiKeyGetMatch && method === "POST") {
605
+ const agent = AgentDB.findById(apiKeyGetMatch[1]);
606
+ if (!agent) {
607
+ return json({ error: "Agent not found" }, 404);
608
+ }
609
+
610
+ const newKey = AgentDB.regenerateApiKey(agent.id);
611
+ if (!newKey) {
612
+ return json({ error: "Failed to regenerate API key" }, 500);
613
+ }
614
+
615
+ // Return the full new key (only time it's fully visible)
616
+ return json({
617
+ apiKey: newKey,
618
+ message: "API key regenerated. This is the only time the full key will be shown.",
619
+ });
620
+ }
621
+
435
622
  // POST /api/agents/:id/start - Start an agent
436
623
  const startMatch = path.match(/^\/api\/agents\/([^/]+)\/start$/);
437
624
  if (startMatch && method === "POST") {
@@ -457,10 +644,10 @@ export async function handleApiRequest(req: Request, path: string): Promise<Resp
457
644
  return json({ error: "Agent not found" }, 404);
458
645
  }
459
646
 
460
- const proc = agentProcesses.get(agent.id);
461
- if (proc) {
462
- console.log(`Stopping agent ${agent.name} (pid: ${proc.pid})...`);
463
- proc.kill();
647
+ const agentProc = agentProcesses.get(agent.id);
648
+ if (agentProc) {
649
+ console.log(`Stopping agent ${agent.name} (pid: ${agentProc.proc.pid})...`);
650
+ agentProc.proc.kill();
464
651
  agentProcesses.delete(agent.id);
465
652
  }
466
653
 
@@ -483,59 +670,741 @@ export async function handleApiRequest(req: Request, path: string): Promise<Resp
483
670
  try {
484
671
  const body = await req.json();
485
672
 
486
- // Proxy to the agent's /chat endpoint
487
- const agentUrl = `http://localhost:${agent.port}/chat`;
488
- const response = await fetch(agentUrl, {
489
- method: "POST",
490
- headers: {
491
- "Content-Type": "application/json",
492
- },
493
- body: JSON.stringify(body),
494
- });
495
-
496
- // Stream the response back
497
- if (!response.ok) {
498
- const errorText = await response.text();
499
- return json({ error: `Agent error: ${errorText}` }, response.status);
673
+ // Proxy to the agent's /chat endpoint with authentication
674
+ const response = await agentFetch(agent.id, agent.port, "/chat", {
675
+ method: "POST",
676
+ headers: { "Content-Type": "application/json" },
677
+ body: JSON.stringify(body),
678
+ });
679
+
680
+ // Stream the response back
681
+ if (!response.ok) {
682
+ const errorText = await response.text();
683
+ return json({ error: `Agent error: ${errorText}` }, response.status);
684
+ }
685
+
686
+ // Return streaming response with proper headers
687
+ return new Response(response.body, {
688
+ status: 200,
689
+ headers: {
690
+ "Content-Type": response.headers.get("Content-Type") || "text/event-stream",
691
+ "Cache-Control": "no-cache",
692
+ "Connection": "keep-alive",
693
+ },
694
+ });
695
+ } catch (err) {
696
+ console.error(`Chat proxy error: ${err}`);
697
+ return json({ error: `Failed to proxy chat: ${err}` }, 500);
698
+ }
699
+ }
700
+
701
+ // ==================== THREAD & MESSAGE PROXY ====================
702
+
703
+ // GET /api/agents/:id/threads - List threads for an agent
704
+ const threadsListMatch = path.match(/^\/api\/agents\/([^/]+)\/threads$/);
705
+ if (threadsListMatch && method === "GET") {
706
+ const agent = AgentDB.findById(threadsListMatch[1]);
707
+ if (!agent) {
708
+ return json({ error: "Agent not found" }, 404);
709
+ }
710
+
711
+ if (agent.status !== "running" || !agent.port) {
712
+ return json({ error: "Agent is not running" }, 400);
713
+ }
714
+
715
+ try {
716
+ const response = await agentFetch(agent.id, agent.port, "/threads", {
717
+ method: "GET",
718
+ headers: { "Accept": "application/json" },
719
+ });
720
+
721
+ if (!response.ok) {
722
+ const errorText = await response.text();
723
+ return json({ error: `Agent error: ${errorText}` }, response.status);
724
+ }
725
+
726
+ const data = await response.json();
727
+ return json(data);
728
+ } catch (err) {
729
+ console.error(`Threads list proxy error: ${err}`);
730
+ return json({ error: `Failed to fetch threads: ${err}` }, 500);
731
+ }
732
+ }
733
+
734
+ // POST /api/agents/:id/threads - Create a new thread
735
+ if (threadsListMatch && method === "POST") {
736
+ const agent = AgentDB.findById(threadsListMatch[1]);
737
+ if (!agent) {
738
+ return json({ error: "Agent not found" }, 404);
739
+ }
740
+
741
+ if (agent.status !== "running" || !agent.port) {
742
+ return json({ error: "Agent is not running" }, 400);
743
+ }
744
+
745
+ try {
746
+ const body = await req.json().catch(() => ({}));
747
+ const response = await agentFetch(agent.id, agent.port, "/threads", {
748
+ method: "POST",
749
+ headers: { "Content-Type": "application/json" },
750
+ body: JSON.stringify(body),
751
+ });
752
+
753
+ if (!response.ok) {
754
+ const errorText = await response.text();
755
+ return json({ error: `Agent error: ${errorText}` }, response.status);
756
+ }
757
+
758
+ const data = await response.json();
759
+ return json(data, 201);
760
+ } catch (err) {
761
+ console.error(`Thread create proxy error: ${err}`);
762
+ return json({ error: `Failed to create thread: ${err}` }, 500);
763
+ }
764
+ }
765
+
766
+ // GET /api/agents/:id/threads/:threadId - Get a specific thread
767
+ const threadDetailMatch = path.match(/^\/api\/agents\/([^/]+)\/threads\/([^/]+)$/);
768
+ if (threadDetailMatch && method === "GET") {
769
+ const agent = AgentDB.findById(threadDetailMatch[1]);
770
+ if (!agent) {
771
+ return json({ error: "Agent not found" }, 404);
772
+ }
773
+
774
+ if (agent.status !== "running" || !agent.port) {
775
+ return json({ error: "Agent is not running" }, 400);
776
+ }
777
+
778
+ try {
779
+ const threadId = threadDetailMatch[2];
780
+ const response = await agentFetch(agent.id, agent.port, `/threads/${threadId}`, {
781
+ method: "GET",
782
+ headers: { "Accept": "application/json" },
783
+ });
784
+
785
+ if (!response.ok) {
786
+ const errorText = await response.text();
787
+ return json({ error: `Agent error: ${errorText}` }, response.status);
788
+ }
789
+
790
+ const data = await response.json();
791
+ return json(data);
792
+ } catch (err) {
793
+ console.error(`Thread detail proxy error: ${err}`);
794
+ return json({ error: `Failed to fetch thread: ${err}` }, 500);
795
+ }
796
+ }
797
+
798
+ // DELETE /api/agents/:id/threads/:threadId - Delete a thread
799
+ if (threadDetailMatch && method === "DELETE") {
800
+ const agent = AgentDB.findById(threadDetailMatch[1]);
801
+ if (!agent) {
802
+ return json({ error: "Agent not found" }, 404);
803
+ }
804
+
805
+ if (agent.status !== "running" || !agent.port) {
806
+ return json({ error: "Agent is not running" }, 400);
807
+ }
808
+
809
+ try {
810
+ const threadId = threadDetailMatch[2];
811
+ const response = await agentFetch(agent.id, agent.port, `/threads/${threadId}`, {
812
+ method: "DELETE",
813
+ });
814
+
815
+ if (!response.ok) {
816
+ const errorText = await response.text();
817
+ return json({ error: `Agent error: ${errorText}` }, response.status);
818
+ }
819
+
820
+ return json({ success: true });
821
+ } catch (err) {
822
+ console.error(`Thread delete proxy error: ${err}`);
823
+ return json({ error: `Failed to delete thread: ${err}` }, 500);
824
+ }
825
+ }
826
+
827
+ // GET /api/agents/:id/threads/:threadId/messages - Get messages in a thread
828
+ const threadMessagesMatch = path.match(/^\/api\/agents\/([^/]+)\/threads\/([^/]+)\/messages$/);
829
+ if (threadMessagesMatch && method === "GET") {
830
+ const agent = AgentDB.findById(threadMessagesMatch[1]);
831
+ if (!agent) {
832
+ return json({ error: "Agent not found" }, 404);
833
+ }
834
+
835
+ if (agent.status !== "running" || !agent.port) {
836
+ return json({ error: "Agent is not running" }, 400);
837
+ }
838
+
839
+ try {
840
+ const threadId = threadMessagesMatch[2];
841
+ const response = await agentFetch(agent.id, agent.port, `/threads/${threadId}/messages`, {
842
+ method: "GET",
843
+ headers: { "Accept": "application/json" },
844
+ });
845
+
846
+ if (!response.ok) {
847
+ const errorText = await response.text();
848
+ return json({ error: `Agent error: ${errorText}` }, response.status);
849
+ }
850
+
851
+ const data = await response.json();
852
+ return json(data);
853
+ } catch (err) {
854
+ console.error(`Thread messages proxy error: ${err}`);
855
+ return json({ error: `Failed to fetch messages: ${err}` }, 500);
856
+ }
857
+ }
858
+
859
+ // ==================== MEMORY PROXY ====================
860
+
861
+ // GET /api/agents/:id/memories - List memories for an agent
862
+ const memoriesMatch = path.match(/^\/api\/agents\/([^/]+)\/memories$/);
863
+ if (memoriesMatch && method === "GET") {
864
+ const agent = AgentDB.findById(memoriesMatch[1]);
865
+ if (!agent) {
866
+ return json({ error: "Agent not found" }, 404);
867
+ }
868
+
869
+ if (agent.status !== "running" || !agent.port) {
870
+ return json({ error: "Agent is not running" }, 400);
871
+ }
872
+
873
+ try {
874
+ const url = new URL(req.url);
875
+ const threadId = url.searchParams.get("thread_id") || "";
876
+ const endpoint = `/memories${threadId ? `?thread_id=${threadId}` : ""}`;
877
+ const response = await agentFetch(agent.id, agent.port, endpoint, {
878
+ method: "GET",
879
+ headers: { "Accept": "application/json" },
880
+ });
881
+
882
+ if (!response.ok) {
883
+ const errorText = await response.text();
884
+ return json({ error: `Agent error: ${errorText}` }, response.status);
885
+ }
886
+
887
+ const data = await response.json();
888
+ return json(data);
889
+ } catch (err) {
890
+ console.error(`Memories list proxy error: ${err}`);
891
+ return json({ error: `Failed to fetch memories: ${err}` }, 500);
892
+ }
893
+ }
894
+
895
+ // DELETE /api/agents/:id/memories - Clear all memories for an agent
896
+ if (memoriesMatch && method === "DELETE") {
897
+ const agent = AgentDB.findById(memoriesMatch[1]);
898
+ if (!agent) {
899
+ return json({ error: "Agent not found" }, 404);
900
+ }
901
+
902
+ if (agent.status !== "running" || !agent.port) {
903
+ return json({ error: "Agent is not running" }, 400);
904
+ }
905
+
906
+ try {
907
+ const response = await agentFetch(agent.id, agent.port, "/memories", { method: "DELETE" });
908
+
909
+ if (!response.ok) {
910
+ const errorText = await response.text();
911
+ return json({ error: `Agent error: ${errorText}` }, response.status);
912
+ }
913
+
914
+ return json({ success: true });
915
+ } catch (err) {
916
+ console.error(`Memories clear proxy error: ${err}`);
917
+ return json({ error: `Failed to clear memories: ${err}` }, 500);
918
+ }
919
+ }
920
+
921
+ // DELETE /api/agents/:id/memories/:memoryId - Delete a specific memory
922
+ const memoryDeleteMatch = path.match(/^\/api\/agents\/([^/]+)\/memories\/([^/]+)$/);
923
+ if (memoryDeleteMatch && method === "DELETE") {
924
+ const agent = AgentDB.findById(memoryDeleteMatch[1]);
925
+ if (!agent) {
926
+ return json({ error: "Agent not found" }, 404);
927
+ }
928
+
929
+ if (agent.status !== "running" || !agent.port) {
930
+ return json({ error: "Agent is not running" }, 400);
931
+ }
932
+
933
+ try {
934
+ const memoryId = memoryDeleteMatch[2];
935
+ const response = await agentFetch(agent.id, agent.port, `/memories/${memoryId}`, { method: "DELETE" });
936
+
937
+ if (!response.ok) {
938
+ const errorText = await response.text();
939
+ return json({ error: `Agent error: ${errorText}` }, response.status);
940
+ }
941
+
942
+ return json({ success: true });
943
+ } catch (err) {
944
+ console.error(`Memory delete proxy error: ${err}`);
945
+ return json({ error: `Failed to delete memory: ${err}` }, 500);
946
+ }
947
+ }
948
+
949
+ // ==================== FILES PROXY ====================
950
+
951
+ // GET /api/agents/:id/files - List files for an agent
952
+ const filesMatch = path.match(/^\/api\/agents\/([^/]+)\/files$/);
953
+ if (filesMatch && method === "GET") {
954
+ const agent = AgentDB.findById(filesMatch[1]);
955
+ if (!agent) {
956
+ return json({ error: "Agent not found" }, 404);
957
+ }
958
+
959
+ if (agent.status !== "running" || !agent.port) {
960
+ return json({ error: "Agent is not running" }, 400);
961
+ }
962
+
963
+ try {
964
+ const url = new URL(req.url);
965
+ const params = new URLSearchParams();
966
+ if (url.searchParams.get("thread_id")) params.set("thread_id", url.searchParams.get("thread_id")!);
967
+ if (url.searchParams.get("limit")) params.set("limit", url.searchParams.get("limit")!);
968
+
969
+ const endpoint = `/files${params.toString() ? `?${params}` : ""}`;
970
+ const response = await agentFetch(agent.id, agent.port, endpoint, {
971
+ method: "GET",
972
+ headers: { "Accept": "application/json" },
973
+ });
974
+
975
+ if (!response.ok) {
976
+ const errorText = await response.text();
977
+ return json({ error: `Agent error: ${errorText}` }, response.status);
978
+ }
979
+
980
+ const data = await response.json();
981
+ return json(data);
982
+ } catch (err) {
983
+ console.error(`Files list proxy error: ${err}`);
984
+ return json({ error: `Failed to fetch files: ${err}` }, 500);
985
+ }
986
+ }
987
+
988
+ // GET /api/agents/:id/files/:fileId - Get a specific file
989
+ const fileGetMatch = path.match(/^\/api\/agents\/([^/]+)\/files\/([^/]+)$/);
990
+ if (fileGetMatch && method === "GET") {
991
+ const agent = AgentDB.findById(fileGetMatch[1]);
992
+ if (!agent) {
993
+ return json({ error: "Agent not found" }, 404);
994
+ }
995
+
996
+ if (agent.status !== "running" || !agent.port) {
997
+ return json({ error: "Agent is not running" }, 400);
998
+ }
999
+
1000
+ try {
1001
+ const fileId = fileGetMatch[2];
1002
+ const response = await agentFetch(agent.id, agent.port, `/files/${fileId}`, {
1003
+ method: "GET",
1004
+ headers: { "Accept": "application/json" },
1005
+ });
1006
+
1007
+ if (!response.ok) {
1008
+ const errorText = await response.text();
1009
+ return json({ error: `Agent error: ${errorText}` }, response.status);
1010
+ }
1011
+
1012
+ const data = await response.json();
1013
+ return json(data);
1014
+ } catch (err) {
1015
+ console.error(`File get proxy error: ${err}`);
1016
+ return json({ error: `Failed to fetch file: ${err}` }, 500);
1017
+ }
1018
+ }
1019
+
1020
+ // DELETE /api/agents/:id/files/:fileId - Delete a specific file
1021
+ if (fileGetMatch && method === "DELETE") {
1022
+ const agent = AgentDB.findById(fileGetMatch[1]);
1023
+ if (!agent) {
1024
+ return json({ error: "Agent not found" }, 404);
1025
+ }
1026
+
1027
+ if (agent.status !== "running" || !agent.port) {
1028
+ return json({ error: "Agent is not running" }, 400);
1029
+ }
1030
+
1031
+ try {
1032
+ const fileId = fileGetMatch[2];
1033
+ const response = await agentFetch(agent.id, agent.port, `/files/${fileId}`, {
1034
+ method: "DELETE",
1035
+ });
1036
+
1037
+ if (!response.ok) {
1038
+ const errorText = await response.text();
1039
+ return json({ error: `Agent error: ${errorText}` }, response.status);
1040
+ }
1041
+
1042
+ return json({ success: true });
1043
+ } catch (err) {
1044
+ console.error(`File delete proxy error: ${err}`);
1045
+ return json({ error: `Failed to delete file: ${err}` }, 500);
1046
+ }
1047
+ }
1048
+
1049
+ // GET /api/agents/:id/files/:fileId/download - Download a file
1050
+ const fileDownloadMatch = path.match(/^\/api\/agents\/([^/]+)\/files\/([^/]+)\/download$/);
1051
+ if (fileDownloadMatch && method === "GET") {
1052
+ const agent = AgentDB.findById(fileDownloadMatch[1]);
1053
+ if (!agent) {
1054
+ return json({ error: "Agent not found" }, 404);
1055
+ }
1056
+
1057
+ if (agent.status !== "running" || !agent.port) {
1058
+ return json({ error: "Agent is not running" }, 400);
1059
+ }
1060
+
1061
+ try {
1062
+ const fileId = fileDownloadMatch[2];
1063
+ const response = await agentFetch(agent.id, agent.port, `/files/${fileId}/download`);
1064
+
1065
+ if (!response.ok) {
1066
+ const errorText = await response.text();
1067
+ return json({ error: `Agent error: ${errorText}` }, response.status);
1068
+ }
1069
+
1070
+ // Pass through the file response
1071
+ return new Response(response.body, {
1072
+ status: response.status,
1073
+ headers: {
1074
+ "Content-Type": response.headers.get("Content-Type") || "application/octet-stream",
1075
+ "Content-Disposition": response.headers.get("Content-Disposition") || "attachment",
1076
+ "Content-Length": response.headers.get("Content-Length") || "",
1077
+ },
1078
+ });
1079
+ } catch (err) {
1080
+ console.error(`File download proxy error: ${err}`);
1081
+ return json({ error: `Failed to download file: ${err}` }, 500);
1082
+ }
1083
+ }
1084
+
1085
+ // ==================== DISCOVERY/PEERS PROXY ====================
1086
+
1087
+ // GET /api/agents/:id/peers - Get discovered peer agents
1088
+ const peersMatch = path.match(/^\/api\/agents\/([^/]+)\/peers$/);
1089
+ if (peersMatch && method === "GET") {
1090
+ const agent = AgentDB.findById(peersMatch[1]);
1091
+ if (!agent) {
1092
+ return json({ error: "Agent not found" }, 404);
1093
+ }
1094
+
1095
+ if (agent.status !== "running" || !agent.port) {
1096
+ return json({ error: "Agent is not running" }, 400);
1097
+ }
1098
+
1099
+ try {
1100
+ const response = await agentFetch(agent.id, agent.port, "/discovery/agents", {
1101
+ method: "GET",
1102
+ headers: { "Accept": "application/json" },
1103
+ });
1104
+
1105
+ if (!response.ok) {
1106
+ const errorText = await response.text();
1107
+ return json({ error: `Agent error: ${errorText}` }, response.status);
1108
+ }
1109
+
1110
+ const data = await response.json();
1111
+ return json(data);
1112
+ } catch (err) {
1113
+ console.error(`Peers list proxy error: ${err}`);
1114
+ return json({ error: `Failed to fetch peers: ${err}` }, 500);
1115
+ }
1116
+ }
1117
+
1118
+ // GET /api/providers - List supported providers and models with key status
1119
+ if (path === "/api/providers" && method === "GET") {
1120
+ const providers = getProvidersWithStatus();
1121
+ return json({ providers });
1122
+ }
1123
+
1124
+ // ==================== ONBOARDING ====================
1125
+
1126
+ // GET /api/onboarding/status - Check onboarding status
1127
+ if (path === "/api/onboarding/status" && method === "GET") {
1128
+ return json(Onboarding.getStatus());
1129
+ }
1130
+
1131
+ // POST /api/onboarding/complete - Mark onboarding as complete
1132
+ if (path === "/api/onboarding/complete" && method === "POST") {
1133
+ Onboarding.complete();
1134
+ return json({ success: true });
1135
+ }
1136
+
1137
+ // POST /api/onboarding/reset - Reset onboarding (for testing)
1138
+ if (path === "/api/onboarding/reset" && method === "POST") {
1139
+ Onboarding.reset();
1140
+ return json({ success: true });
1141
+ }
1142
+
1143
+ // POST /api/onboarding/user - Create first user during onboarding
1144
+ // This endpoint only works when no users exist (enforced by middleware)
1145
+ if (path === "/api/onboarding/user" && method === "POST") {
1146
+ debug("POST /api/onboarding/user");
1147
+ // Double-check no users exist
1148
+ if (UserDB.hasUsers()) {
1149
+ debug("Users already exist");
1150
+ return json({ error: "Users already exist" }, 403);
1151
+ }
1152
+
1153
+ try {
1154
+ const body = await req.json();
1155
+ debug("Onboarding body:", JSON.stringify(body));
1156
+ const { username, password, email } = body;
1157
+
1158
+ if (!username || !password) {
1159
+ debug("Missing username or password");
1160
+ return json({ error: "Username and password are required" }, 400);
1161
+ }
1162
+
1163
+ // Create first user as admin
1164
+ debug("Creating user:", username);
1165
+ const result = await createUser({
1166
+ username,
1167
+ password,
1168
+ email: email || undefined, // Optional, for password recovery
1169
+ role: "admin",
1170
+ });
1171
+ debug("Create user result:", result.success, result.error);
1172
+
1173
+ if (!result.success) {
1174
+ return json({ error: result.error }, 400);
1175
+ }
1176
+
1177
+ return json({
1178
+ success: true,
1179
+ user: {
1180
+ id: result.user!.id,
1181
+ username: result.user!.username,
1182
+ role: result.user!.role,
1183
+ },
1184
+ }, 201);
1185
+ } catch (e) {
1186
+ debug("Onboarding error:", e);
1187
+ return json({ error: "Invalid request body" }, 400);
1188
+ }
1189
+ }
1190
+
1191
+ // ==================== USER MANAGEMENT (Admin only) ====================
1192
+
1193
+ // GET /api/users - List all users
1194
+ if (path === "/api/users" && method === "GET") {
1195
+ const users = UserDB.findAll().map(u => ({
1196
+ id: u.id,
1197
+ username: u.username,
1198
+ email: u.email,
1199
+ role: u.role,
1200
+ createdAt: u.created_at,
1201
+ lastLoginAt: u.last_login_at,
1202
+ }));
1203
+ return json({ users });
1204
+ }
1205
+
1206
+ // POST /api/users - Create a new user
1207
+ if (path === "/api/users" && method === "POST") {
1208
+ try {
1209
+ const body = await req.json();
1210
+ const { username, password, email, role } = body;
1211
+
1212
+ if (!username || !password) {
1213
+ return json({ error: "Username and password are required" }, 400);
1214
+ }
1215
+
1216
+ const result = await createUser({
1217
+ username,
1218
+ password,
1219
+ email: email || undefined,
1220
+ role: role || "user",
1221
+ });
1222
+
1223
+ if (!result.success) {
1224
+ return json({ error: result.error }, 400);
1225
+ }
1226
+
1227
+ return json({
1228
+ user: {
1229
+ id: result.user!.id,
1230
+ username: result.user!.username,
1231
+ email: result.user!.email,
1232
+ role: result.user!.role,
1233
+ createdAt: result.user!.created_at,
1234
+ },
1235
+ }, 201);
1236
+ } catch (e) {
1237
+ return json({ error: "Invalid request body" }, 400);
1238
+ }
1239
+ }
1240
+
1241
+ // GET /api/users/:id - Get a specific user
1242
+ const userMatch = path.match(/^\/api\/users\/([^/]+)$/);
1243
+ if (userMatch && method === "GET") {
1244
+ const targetUser = UserDB.findById(userMatch[1]);
1245
+ if (!targetUser) {
1246
+ return json({ error: "User not found" }, 404);
1247
+ }
1248
+ return json({
1249
+ user: {
1250
+ id: targetUser.id,
1251
+ username: targetUser.username,
1252
+ email: targetUser.email,
1253
+ role: targetUser.role,
1254
+ createdAt: targetUser.created_at,
1255
+ lastLoginAt: targetUser.last_login_at,
1256
+ },
1257
+ });
1258
+ }
1259
+
1260
+ // PUT /api/users/:id - Update a user
1261
+ if (userMatch && method === "PUT") {
1262
+ const targetUser = UserDB.findById(userMatch[1]);
1263
+ if (!targetUser) {
1264
+ return json({ error: "User not found" }, 404);
1265
+ }
1266
+
1267
+ try {
1268
+ const body = await req.json();
1269
+ const updates: Parameters<typeof UserDB.update>[1] = {};
1270
+
1271
+ if (body.email !== undefined) updates.email = body.email;
1272
+ if (body.role !== undefined) {
1273
+ // Prevent removing last admin
1274
+ if (targetUser.role === "admin" && body.role !== "admin") {
1275
+ if (UserDB.countAdmins() <= 1) {
1276
+ return json({ error: "Cannot remove the last admin" }, 400);
1277
+ }
1278
+ }
1279
+ updates.role = body.role;
1280
+ }
1281
+ if (body.password !== undefined) {
1282
+ const validation = validatePassword(body.password);
1283
+ if (!validation.valid) {
1284
+ return json({ error: validation.errors.join(". ") }, 400);
1285
+ }
1286
+ updates.password_hash = await hashPassword(body.password);
1287
+ }
1288
+
1289
+ const updated = UserDB.update(userMatch[1], updates);
1290
+ return json({
1291
+ user: updated ? {
1292
+ id: updated.id,
1293
+ username: updated.username,
1294
+ email: updated.email,
1295
+ role: updated.role,
1296
+ createdAt: updated.created_at,
1297
+ lastLoginAt: updated.last_login_at,
1298
+ } : null,
1299
+ });
1300
+ } catch (e) {
1301
+ return json({ error: "Invalid request body" }, 400);
1302
+ }
1303
+ }
1304
+
1305
+ // DELETE /api/users/:id - Delete a user
1306
+ if (userMatch && method === "DELETE") {
1307
+ const targetUser = UserDB.findById(userMatch[1]);
1308
+ if (!targetUser) {
1309
+ return json({ error: "User not found" }, 404);
1310
+ }
1311
+
1312
+ // Prevent deleting yourself
1313
+ if (user && targetUser.id === user.id) {
1314
+ return json({ error: "Cannot delete your own account" }, 400);
1315
+ }
1316
+
1317
+ // Prevent deleting last admin
1318
+ if (targetUser.role === "admin" && UserDB.countAdmins() <= 1) {
1319
+ return json({ error: "Cannot delete the last admin" }, 400);
1320
+ }
1321
+
1322
+ UserDB.delete(userMatch[1]);
1323
+ return json({ success: true });
1324
+ }
1325
+
1326
+ // ==================== PROJECTS ====================
1327
+
1328
+ // GET /api/projects - List all projects
1329
+ if (path === "/api/projects" && method === "GET") {
1330
+ const projects = ProjectDB.findAll();
1331
+ const agentCounts = ProjectDB.getAgentCounts();
1332
+ return json({
1333
+ projects: projects.map(p => ({
1334
+ ...toApiProject(p),
1335
+ agentCount: agentCounts.get(p.id) || 0,
1336
+ })),
1337
+ unassignedCount: agentCounts.get(null) || 0,
1338
+ });
1339
+ }
1340
+
1341
+ // POST /api/projects - Create a new project
1342
+ if (path === "/api/projects" && method === "POST") {
1343
+ try {
1344
+ const body = await req.json();
1345
+ const { name, description, color } = body;
1346
+
1347
+ if (!name) {
1348
+ return json({ error: "Name is required" }, 400);
500
1349
  }
501
1350
 
502
- // Return streaming response with proper headers
503
- return new Response(response.body, {
504
- status: 200,
505
- headers: {
506
- "Content-Type": response.headers.get("Content-Type") || "text/event-stream",
507
- "Cache-Control": "no-cache",
508
- "Connection": "keep-alive",
509
- },
1351
+ const project = ProjectDB.create({
1352
+ name,
1353
+ description: description || null,
1354
+ color: color || "#6366f1",
510
1355
  });
511
- } catch (err) {
512
- console.error(`Chat proxy error: ${err}`);
513
- return json({ error: `Failed to proxy chat: ${err}` }, 500);
1356
+
1357
+ return json({ project: toApiProject(project) }, 201);
1358
+ } catch (e) {
1359
+ console.error("Create project error:", e);
1360
+ return json({ error: "Invalid request body" }, 400);
514
1361
  }
515
1362
  }
516
1363
 
517
- // GET /api/providers - List supported providers and models with key status
518
- if (path === "/api/providers" && method === "GET") {
519
- const providers = getProvidersWithStatus();
520
- return json({ providers });
1364
+ // GET /api/projects/:id - Get a specific project
1365
+ const projectMatch = path.match(/^\/api\/projects\/([^/]+)$/);
1366
+ if (projectMatch && method === "GET") {
1367
+ const project = ProjectDB.findById(projectMatch[1]);
1368
+ if (!project) {
1369
+ return json({ error: "Project not found" }, 404);
1370
+ }
1371
+ const agents = AgentDB.findByProject(project.id);
1372
+ return json({
1373
+ project: toApiProject(project),
1374
+ agents: agents.map(toApiAgent),
1375
+ });
521
1376
  }
522
1377
 
523
- // ==================== ONBOARDING ====================
1378
+ // PUT /api/projects/:id - Update a project
1379
+ if (projectMatch && method === "PUT") {
1380
+ const project = ProjectDB.findById(projectMatch[1]);
1381
+ if (!project) {
1382
+ return json({ error: "Project not found" }, 404);
1383
+ }
524
1384
 
525
- // GET /api/onboarding/status - Check onboarding status
526
- if (path === "/api/onboarding/status" && method === "GET") {
527
- return json(Onboarding.getStatus());
528
- }
1385
+ try {
1386
+ const body = await req.json();
1387
+ const updates: Partial<Project> = {};
529
1388
 
530
- // POST /api/onboarding/complete - Mark onboarding as complete
531
- if (path === "/api/onboarding/complete" && method === "POST") {
532
- Onboarding.complete();
533
- return json({ success: true });
1389
+ if (body.name !== undefined) updates.name = body.name;
1390
+ if (body.description !== undefined) updates.description = body.description;
1391
+ if (body.color !== undefined) updates.color = body.color;
1392
+
1393
+ const updated = ProjectDB.update(projectMatch[1], updates);
1394
+ return json({ project: updated ? toApiProject(updated) : null });
1395
+ } catch (e) {
1396
+ return json({ error: "Invalid request body" }, 400);
1397
+ }
534
1398
  }
535
1399
 
536
- // POST /api/onboarding/reset - Reset onboarding (for testing)
537
- if (path === "/api/onboarding/reset" && method === "POST") {
538
- Onboarding.reset();
1400
+ // DELETE /api/projects/:id - Delete a project
1401
+ if (projectMatch && method === "DELETE") {
1402
+ const project = ProjectDB.findById(projectMatch[1]);
1403
+ if (!project) {
1404
+ return json({ error: "Project not found" }, 404);
1405
+ }
1406
+
1407
+ ProjectDB.delete(projectMatch[1]);
539
1408
  return json({ success: true });
540
1409
  }
541
1410
 
@@ -625,13 +1494,19 @@ export async function handleApiRequest(req: Request, path: string): Promise<Resp
625
1494
 
626
1495
  // POST /api/version/update - Download/install latest agent binary
627
1496
  if (path === "/api/version/update" && method === "POST") {
628
- // Check if any agents are running
1497
+ // Get all running agents to restart later
629
1498
  const runningAgents = AgentDB.findAll().filter(a => a.status === "running");
630
- if (runningAgents.length > 0) {
631
- return json(
632
- { success: false, error: "Cannot update while agents are running. Stop all agents first." },
633
- { status: 400 }
634
- );
1499
+ const agentsToRestart = runningAgents.map(a => a.id);
1500
+
1501
+ // Stop all running agents
1502
+ for (const agent of runningAgents) {
1503
+ const agentProc = agentProcesses.get(agent.id);
1504
+ if (agentProc) {
1505
+ console.log(`Stopping agent ${agent.name} for update...`);
1506
+ agentProc.proc.kill();
1507
+ agentProcesses.delete(agent.id);
1508
+ }
1509
+ AgentDB.setStatus(agent.id, "stopped");
635
1510
  }
636
1511
 
637
1512
  // Try npm install first, fall back to direct download
@@ -641,10 +1516,31 @@ export async function handleApiRequest(req: Request, path: string): Promise<Resp
641
1516
  result = await downloadLatestBinary(BIN_DIR);
642
1517
  }
643
1518
 
644
- if (result.success) {
645
- return json({ success: true, version: result.version });
1519
+ if (!result.success) {
1520
+ return json({ success: false, error: result.error }, 500);
1521
+ }
1522
+
1523
+ // Restart agents that were running
1524
+ const restartResults: { id: string; name: string; success: boolean; error?: string }[] = [];
1525
+ for (const agentId of agentsToRestart) {
1526
+ const agent = AgentDB.findById(agentId);
1527
+ if (agent) {
1528
+ console.log(`Restarting agent ${agent.name} after update...`);
1529
+ const startResult = await startAgentProcess(agent);
1530
+ restartResults.push({
1531
+ id: agent.id,
1532
+ name: agent.name,
1533
+ success: startResult.success,
1534
+ error: startResult.error,
1535
+ });
1536
+ }
646
1537
  }
647
- return json({ success: false, error: result.error }, { status: 500 });
1538
+
1539
+ return json({
1540
+ success: true,
1541
+ version: result.version,
1542
+ restarted: restartResults,
1543
+ });
648
1544
  }
649
1545
 
650
1546
  // GET /api/health - Health check
@@ -669,10 +1565,10 @@ export async function handleApiRequest(req: Request, path: string): Promise<Resp
669
1565
 
670
1566
  // ==================== TASKS ====================
671
1567
 
672
- // Helper to fetch from a running agent
673
- async function fetchFromAgent(port: number, endpoint: string): Promise<any> {
1568
+ // Helper to fetch from a running agent (with authentication)
1569
+ async function fetchFromAgent(agentId: string, port: number, endpoint: string): Promise<any> {
674
1570
  try {
675
- const response = await fetch(`http://localhost:${port}${endpoint}`, {
1571
+ const response = await agentFetch(agentId, port, endpoint, {
676
1572
  headers: { "Accept": "application/json" },
677
1573
  });
678
1574
  if (response.ok) {
@@ -693,7 +1589,7 @@ export async function handleApiRequest(req: Request, path: string): Promise<Resp
693
1589
  const allTasks: any[] = [];
694
1590
 
695
1591
  for (const agent of runningAgents) {
696
- const data = await fetchFromAgent(agent.port!, `/tasks?status=${status}`);
1592
+ const data = await fetchFromAgent(agent.id, agent.port!, `/tasks?status=${status}`);
697
1593
  if (data?.tasks) {
698
1594
  // Add agent info to each task
699
1595
  for (const task of data.tasks) {
@@ -729,7 +1625,7 @@ export async function handleApiRequest(req: Request, path: string): Promise<Resp
729
1625
  const url = new URL(req.url);
730
1626
  const status = url.searchParams.get("status") || "all";
731
1627
 
732
- const data = await fetchFromAgent(agent.port, `/tasks?status=${status}`);
1628
+ const data = await fetchFromAgent(agent.id, agent.port, `/tasks?status=${status}`);
733
1629
  if (!data) {
734
1630
  return json({ error: "Failed to fetch tasks from agent" }, 500);
735
1631
  }
@@ -748,7 +1644,7 @@ export async function handleApiRequest(req: Request, path: string): Promise<Resp
748
1644
  let runningTasks = 0;
749
1645
 
750
1646
  for (const agent of runningAgents) {
751
- const data = await fetchFromAgent(agent.port!, "/tasks?status=all");
1647
+ const data = await fetchFromAgent(agent.id, agent.port!, "/tasks?status=all");
752
1648
  if (data?.tasks) {
753
1649
  totalTasks += data.tasks.length;
754
1650
  for (const task of data.tasks) {
@@ -833,20 +1729,39 @@ export async function handleApiRequest(req: Request, path: string): Promise<Resp
833
1729
  }
834
1730
  const data = await res.json();
835
1731
 
836
- // Transform to simpler format
837
- const servers = (data.servers || []).map((item: any) => {
838
- const s = item.server;
839
- const pkg = s.packages?.find((p: any) => p.registryType === "npm");
840
- return {
841
- name: s.name,
842
- description: s.description,
843
- version: s.version,
844
- repository: s.repository?.url,
845
- npmPackage: pkg?.identifier,
846
- transport: pkg?.transport?.type || "stdio",
847
- envVars: pkg?.environmentVariables || [],
848
- };
849
- }).filter((s: any) => s.npmPackage); // Only show npm packages for now
1732
+ // Transform to simpler format - dedupe by name
1733
+ const seen = new Set<string>();
1734
+ const servers = (data.servers || [])
1735
+ .map((item: any) => {
1736
+ const s = item.server;
1737
+ const pkg = s.packages?.find((p: any) => p.registryType === "npm");
1738
+ const remote = s.remotes?.[0];
1739
+
1740
+ // Extract a short display name from the full name
1741
+ // e.g., "ai.smithery/smithery-ai-github" -> "github"
1742
+ // e.g., "io.github.user/my-server" -> "my-server"
1743
+ const fullName = s.name || "";
1744
+ const shortName = fullName.split("/").pop()?.replace(/-mcp$/, "").replace(/^mcp-/, "") || fullName;
1745
+
1746
+ return {
1747
+ id: fullName, // Use full name as unique ID
1748
+ name: shortName,
1749
+ fullName: fullName,
1750
+ description: s.description,
1751
+ version: s.version,
1752
+ repository: s.repository?.url,
1753
+ npmPackage: pkg?.identifier || null,
1754
+ remoteUrl: remote?.url || null,
1755
+ transport: pkg?.transport?.type || (remote ? "http" : "stdio"),
1756
+ };
1757
+ })
1758
+ .filter((s: any) => {
1759
+ // Dedupe by fullName
1760
+ if (seen.has(s.fullName)) return false;
1761
+ seen.add(s.fullName);
1762
+ // Only show servers with npm package or remote URL
1763
+ return s.npmPackage || s.remoteUrl;
1764
+ });
850
1765
 
851
1766
  return json({ servers });
852
1767
  } catch (e) {
@@ -854,11 +1769,410 @@ export async function handleApiRequest(req: Request, path: string): Promise<Resp
854
1769
  }
855
1770
  }
856
1771
 
1772
+ // ============ Generic Integration Providers ============
1773
+ // These endpoints work with any registered provider (composio, smithery, etc.)
1774
+
1775
+ // GET /api/integrations/providers - List available integration providers
1776
+ if (path === "/api/integrations/providers" && method === "GET") {
1777
+ const providerIds = getProviderIds();
1778
+ const providers = providerIds.map(id => {
1779
+ const provider = getProvider(id);
1780
+ const hasKey = !!ProviderKeys.getDecrypted(id);
1781
+ return {
1782
+ id,
1783
+ name: provider?.name || id,
1784
+ connected: hasKey,
1785
+ };
1786
+ });
1787
+ return json({ providers });
1788
+ }
1789
+
1790
+ // GET /api/integrations/:provider/apps - List available apps from a provider
1791
+ const appsMatch = path.match(/^\/api\/integrations\/([^/]+)\/apps$/);
1792
+ if (appsMatch && method === "GET") {
1793
+ const providerId = appsMatch[1];
1794
+ const provider = getProvider(providerId);
1795
+ if (!provider) {
1796
+ return json({ error: `Unknown provider: ${providerId}` }, 404);
1797
+ }
1798
+
1799
+ const apiKey = ProviderKeys.getDecrypted(providerId);
1800
+ if (!apiKey) {
1801
+ return json({ error: `${provider.name} API key not configured`, apps: [] }, 200);
1802
+ }
1803
+
1804
+ try {
1805
+ const apps = await provider.listApps(apiKey);
1806
+ return json({ apps });
1807
+ } catch (e) {
1808
+ console.error(`Failed to list apps from ${providerId}:`, e);
1809
+ return json({ error: "Failed to fetch apps" }, 500);
1810
+ }
1811
+ }
1812
+
1813
+ // GET /api/integrations/:provider/connected - List user's connected accounts
1814
+ const connectedMatch = path.match(/^\/api\/integrations\/([^/]+)\/connected$/);
1815
+ if (connectedMatch && method === "GET") {
1816
+ const providerId = connectedMatch[1];
1817
+ const provider = getProvider(providerId);
1818
+ if (!provider) {
1819
+ return json({ error: `Unknown provider: ${providerId}` }, 404);
1820
+ }
1821
+
1822
+ const apiKey = ProviderKeys.getDecrypted(providerId);
1823
+ if (!apiKey) {
1824
+ return json({ error: `${provider.name} API key not configured`, accounts: [] }, 200);
1825
+ }
1826
+
1827
+ // Use Apteva user ID as the entity ID for the provider
1828
+ const userId = user?.id || "default";
1829
+
1830
+ try {
1831
+ const accounts = await provider.listConnectedAccounts(apiKey, userId);
1832
+ return json({ accounts });
1833
+ } catch (e) {
1834
+ console.error(`Failed to list connected accounts from ${providerId}:`, e);
1835
+ return json({ error: "Failed to fetch connected accounts" }, 500);
1836
+ }
1837
+ }
1838
+
1839
+ // POST /api/integrations/:provider/connect - Initiate connection (OAuth or API Key)
1840
+ const connectMatch = path.match(/^\/api\/integrations\/([^/]+)\/connect$/);
1841
+ if (connectMatch && method === "POST") {
1842
+ const providerId = connectMatch[1];
1843
+ const provider = getProvider(providerId);
1844
+ if (!provider) {
1845
+ return json({ error: `Unknown provider: ${providerId}` }, 404);
1846
+ }
1847
+
1848
+ const apiKey = ProviderKeys.getDecrypted(providerId);
1849
+ if (!apiKey) {
1850
+ return json({ error: `${provider.name} API key not configured` }, 401);
1851
+ }
1852
+
1853
+ try {
1854
+ const body = await req.json();
1855
+ const { appSlug, redirectUrl, credentials } = body;
1856
+
1857
+ if (!appSlug) {
1858
+ return json({ error: "appSlug is required" }, 400);
1859
+ }
1860
+
1861
+ // Use Apteva user ID as the entity ID
1862
+ const userId = user?.id || "default";
1863
+
1864
+ // Default redirect URL back to our integrations page
1865
+ const callbackUrl = redirectUrl || `http://localhost:${process.env.PORT || 4280}/mcp?tab=hosted&connected=${appSlug}`;
1866
+
1867
+ const result = await provider.initiateConnection(apiKey, userId, appSlug, callbackUrl, credentials);
1868
+ return json(result);
1869
+ } catch (e) {
1870
+ console.error(`Failed to initiate connection for ${providerId}:`, e);
1871
+ return json({ error: `Failed to initiate connection: ${e}` }, 500);
1872
+ }
1873
+ }
1874
+
1875
+ // GET /api/integrations/:provider/connection/:id - Check connection status
1876
+ const connectionStatusMatch = path.match(/^\/api\/integrations\/([^/]+)\/connection\/([^/]+)$/);
1877
+ if (connectionStatusMatch && method === "GET") {
1878
+ const providerId = connectionStatusMatch[1];
1879
+ const connectionId = connectionStatusMatch[2];
1880
+ const provider = getProvider(providerId);
1881
+ if (!provider) {
1882
+ return json({ error: `Unknown provider: ${providerId}` }, 404);
1883
+ }
1884
+
1885
+ const apiKey = ProviderKeys.getDecrypted(providerId);
1886
+ if (!apiKey) {
1887
+ return json({ error: `${provider.name} API key not configured` }, 401);
1888
+ }
1889
+
1890
+ try {
1891
+ const connection = await provider.getConnectionStatus(apiKey, connectionId);
1892
+ if (!connection) {
1893
+ return json({ error: "Connection not found" }, 404);
1894
+ }
1895
+ return json({ connection });
1896
+ } catch (e) {
1897
+ console.error(`Failed to get connection status:`, e);
1898
+ return json({ error: "Failed to get connection status" }, 500);
1899
+ }
1900
+ }
1901
+
1902
+ // DELETE /api/integrations/:provider/connection/:id - Disconnect/revoke
1903
+ const disconnectMatch = path.match(/^\/api\/integrations\/([^/]+)\/connection\/([^/]+)$/);
1904
+ if (disconnectMatch && method === "DELETE") {
1905
+ const providerId = disconnectMatch[1];
1906
+ const connectionId = disconnectMatch[2];
1907
+ const provider = getProvider(providerId);
1908
+ if (!provider) {
1909
+ return json({ error: `Unknown provider: ${providerId}` }, 404);
1910
+ }
1911
+
1912
+ const apiKey = ProviderKeys.getDecrypted(providerId);
1913
+ if (!apiKey) {
1914
+ return json({ error: `${provider.name} API key not configured` }, 401);
1915
+ }
1916
+
1917
+ try {
1918
+ const success = await provider.disconnect(apiKey, connectionId);
1919
+ return json({ success });
1920
+ } catch (e) {
1921
+ console.error(`Failed to disconnect:`, e);
1922
+ return json({ error: "Failed to disconnect" }, 500);
1923
+ }
1924
+ }
1925
+
1926
+ // ============ Composio-Specific Routes (MCP Configs) ============
1927
+
1928
+ // GET /api/integrations/composio/configs - List Composio MCP configs
1929
+ if (path === "/api/integrations/composio/configs" && method === "GET") {
1930
+ const apiKey = ProviderKeys.getDecrypted("composio");
1931
+ if (!apiKey) {
1932
+ return json({ error: "Composio API key not configured", configs: [] }, 200);
1933
+ }
1934
+
1935
+ try {
1936
+ const res = await fetch("https://backend.composio.dev/api/v3/mcp/servers?limit=50", {
1937
+ headers: {
1938
+ "x-api-key": apiKey,
1939
+ "Content-Type": "application/json",
1940
+ },
1941
+ });
1942
+
1943
+ if (!res.ok) {
1944
+ const text = await res.text();
1945
+ console.error("Composio API error:", res.status, text);
1946
+ return json({ error: "Failed to fetch Composio configs" }, 500);
1947
+ }
1948
+
1949
+ const data = await res.json();
1950
+
1951
+ // Transform to our format (no user_id in URLs - that's provided when adding)
1952
+ const configs = (data.items || data.servers || []).map((item: any) => ({
1953
+ id: item.id,
1954
+ name: item.name || item.id,
1955
+ toolkits: item.toolkits || item.apps || [],
1956
+ toolsCount: item.toolsCount || item.tools?.length || 0,
1957
+ createdAt: item.createdAt || item.created_at,
1958
+ }));
1959
+
1960
+ return json({ configs });
1961
+ } catch (e) {
1962
+ console.error("Composio fetch error:", e);
1963
+ return json({ error: "Failed to connect to Composio" }, 500);
1964
+ }
1965
+ }
1966
+
1967
+ // GET /api/integrations/composio/configs/:id - Get single Composio config details
1968
+ const composioConfigMatch = path.match(/^\/api\/integrations\/composio\/configs\/([^/]+)$/);
1969
+ if (composioConfigMatch && method === "GET") {
1970
+ const configId = composioConfigMatch[1];
1971
+ const apiKey = ProviderKeys.getDecrypted("composio");
1972
+ if (!apiKey) {
1973
+ return json({ error: "Composio API key not configured" }, 401);
1974
+ }
1975
+
1976
+ try {
1977
+ const res = await fetch(`https://backend.composio.dev/api/v3/mcp/${configId}`, {
1978
+ headers: {
1979
+ "x-api-key": apiKey,
1980
+ "Content-Type": "application/json",
1981
+ },
1982
+ });
1983
+
1984
+ if (!res.ok) {
1985
+ return json({ error: "Config not found" }, 404);
1986
+ }
1987
+
1988
+ const data = await res.json();
1989
+ return json({
1990
+ config: {
1991
+ id: data.id,
1992
+ name: data.name || data.id,
1993
+ toolkits: data.toolkits || data.apps || [],
1994
+ tools: data.tools || [],
1995
+ },
1996
+ });
1997
+ } catch (e) {
1998
+ return json({ error: "Failed to fetch config" }, 500);
1999
+ }
2000
+ }
2001
+
2002
+ // POST /api/integrations/composio/configs/:id/add - Add a Composio config as an MCP server
2003
+ // Fetches the mcp_url directly from Composio API
2004
+ const composioAddMatch = path.match(/^\/api\/integrations\/composio\/configs\/([^/]+)\/add$/);
2005
+ if (composioAddMatch && method === "POST") {
2006
+ const configId = composioAddMatch[1];
2007
+ const apiKey = ProviderKeys.getDecrypted("composio");
2008
+ if (!apiKey) {
2009
+ return json({ error: "Composio API key not configured" }, 401);
2010
+ }
2011
+
2012
+ try {
2013
+ // Fetch config details from Composio to get the name and mcp_url
2014
+ const res = await fetch(`https://backend.composio.dev/api/v3/mcp/${configId}`, {
2015
+ headers: {
2016
+ "x-api-key": apiKey,
2017
+ "Content-Type": "application/json",
2018
+ },
2019
+ });
2020
+
2021
+ if (!res.ok) {
2022
+ const errText = await res.text();
2023
+ console.error("Failed to fetch Composio MCP config:", errText);
2024
+ return json({ error: "Failed to fetch MCP config from Composio" }, 400);
2025
+ }
2026
+
2027
+ const data = await res.json();
2028
+ const configName = data.name || `composio-${configId.slice(0, 8)}`;
2029
+ const mcpUrl = data.mcp_url;
2030
+ const authConfigIds = data.auth_config_ids || [];
2031
+ const serverInstanceCount = data.server_instance_count || 0;
2032
+
2033
+ if (!mcpUrl) {
2034
+ return json({ error: "MCP config does not have a URL" }, 400);
2035
+ }
2036
+
2037
+ // Get user_id from connected accounts for this auth config
2038
+ const { createMcpServerInstance, getUserIdForAuthConfig } = await import("../integrations/composio");
2039
+ let userId: string | null = null;
2040
+
2041
+ if (authConfigIds.length > 0) {
2042
+ userId = await getUserIdForAuthConfig(apiKey, authConfigIds[0]);
2043
+
2044
+ // Create server instance if none exists
2045
+ if (serverInstanceCount === 0 && userId) {
2046
+ const instance = await createMcpServerInstance(apiKey, configId, userId);
2047
+ if (instance) {
2048
+ console.log(`Created server instance for user ${userId} on server ${configId}`);
2049
+ }
2050
+ }
2051
+ }
2052
+
2053
+ // Append user_id to mcp_url for authentication
2054
+ const mcpUrlWithUser = userId
2055
+ ? `${mcpUrl}?user_id=${encodeURIComponent(userId)}`
2056
+ : mcpUrl;
2057
+
2058
+ // Check if already exists (match by config ID in URL)
2059
+ const existing = McpServerDB.findAll().find(
2060
+ s => s.source === "composio" && s.url?.includes(configId)
2061
+ );
2062
+ if (existing) {
2063
+ return json({ server: existing, message: "Server already exists" });
2064
+ }
2065
+
2066
+ // Create the MCP server entry with user_id in URL
2067
+ const server = McpServerDB.create({
2068
+ id: generateId(),
2069
+ name: configName,
2070
+ type: "http",
2071
+ package: null,
2072
+ command: null,
2073
+ args: null,
2074
+ env: {},
2075
+ url: mcpUrlWithUser,
2076
+ headers: { "x-api-key": apiKey },
2077
+ source: "composio",
2078
+ });
2079
+
2080
+ return json({ server, message: "Server added successfully" });
2081
+ } catch (e) {
2082
+ console.error("Failed to add Composio config:", e);
2083
+ return json({ error: "Failed to add Composio config" }, 500);
2084
+ }
2085
+ }
2086
+
2087
+ // POST /api/integrations/composio/configs - Create a new MCP config from connected app
2088
+ if (path === "/api/integrations/composio/configs" && method === "POST") {
2089
+ const apiKey = ProviderKeys.getDecrypted("composio");
2090
+ if (!apiKey) {
2091
+ return json({ error: "Composio API key not configured" }, 401);
2092
+ }
2093
+
2094
+ try {
2095
+ const body = await req.json();
2096
+ const { name, toolkitSlug, authConfigId } = body;
2097
+
2098
+ if (!name || !toolkitSlug) {
2099
+ return json({ error: "name and toolkitSlug are required" }, 400);
2100
+ }
2101
+
2102
+ // If authConfigId not provided, find it from the toolkit
2103
+ let configId = authConfigId;
2104
+ if (!configId) {
2105
+ const { getAuthConfigForToolkit } = await import("../integrations/composio");
2106
+ configId = await getAuthConfigForToolkit(apiKey, toolkitSlug);
2107
+ if (!configId) {
2108
+ return json({ error: `No auth config found for ${toolkitSlug}. Make sure you have connected this app first.` }, 400);
2109
+ }
2110
+ }
2111
+
2112
+ // Create MCP server in Composio
2113
+ const { createMcpServer, createMcpServerInstance, getUserIdForAuthConfig } = await import("../integrations/composio");
2114
+ const mcpServer = await createMcpServer(apiKey, name, [configId]);
2115
+
2116
+ if (!mcpServer) {
2117
+ return json({ error: "Failed to create MCP config" }, 500);
2118
+ }
2119
+
2120
+ // Create server instance for the user who has the connected account
2121
+ const userId = await getUserIdForAuthConfig(apiKey, configId);
2122
+ if (userId) {
2123
+ const instance = await createMcpServerInstance(apiKey, mcpServer.id, userId);
2124
+ if (!instance) {
2125
+ console.warn(`Created MCP server but failed to create instance for user ${userId}`);
2126
+ }
2127
+ }
2128
+
2129
+ // Append user_id to mcp_url for authentication
2130
+ const mcpUrlWithUser = userId
2131
+ ? `${mcpServer.mcpUrl}?user_id=${encodeURIComponent(userId)}`
2132
+ : mcpServer.mcpUrl;
2133
+
2134
+ return json({
2135
+ config: {
2136
+ id: mcpServer.id,
2137
+ name: mcpServer.name,
2138
+ toolkits: mcpServer.toolkits,
2139
+ mcpUrl: mcpUrlWithUser,
2140
+ allowedTools: mcpServer.allowedTools,
2141
+ userId,
2142
+ },
2143
+ }, 201);
2144
+ } catch (e: any) {
2145
+ console.error("Failed to create Composio MCP config:", e);
2146
+ return json({ error: e.message || "Failed to create MCP config" }, 500);
2147
+ }
2148
+ }
2149
+
2150
+ // DELETE /api/integrations/composio/configs/:id - Delete a Composio MCP config
2151
+ if (composioConfigMatch && method === "DELETE") {
2152
+ const configId = composioConfigMatch[1];
2153
+ const apiKey = ProviderKeys.getDecrypted("composio");
2154
+ if (!apiKey) {
2155
+ return json({ error: "Composio API key not configured" }, 401);
2156
+ }
2157
+
2158
+ try {
2159
+ const { deleteMcpServer } = await import("../integrations/composio");
2160
+ const success = await deleteMcpServer(apiKey, configId);
2161
+ if (!success) {
2162
+ return json({ error: "Failed to delete MCP config" }, 500);
2163
+ }
2164
+ return json({ success: true });
2165
+ } catch (e) {
2166
+ console.error("Failed to delete Composio config:", e);
2167
+ return json({ error: "Failed to delete MCP config" }, 500);
2168
+ }
2169
+ }
2170
+
857
2171
  // POST /api/mcp/servers - Create/install a new MCP server
858
2172
  if (path === "/api/mcp/servers" && method === "POST") {
859
2173
  try {
860
2174
  const body = await req.json();
861
- const { name, type, package: pkg, command, args, env } = body;
2175
+ const { name, type, package: pkg, command, args, env, url, headers, source } = body;
862
2176
 
863
2177
  if (!name) {
864
2178
  return json({ error: "Name is required" }, 400);
@@ -872,6 +2186,9 @@ export async function handleApiRequest(req: Request, path: string): Promise<Resp
872
2186
  command: command || null,
873
2187
  args: args || null,
874
2188
  env: env || {},
2189
+ url: url || null,
2190
+ headers: headers || {},
2191
+ source: source || null,
875
2192
  });
876
2193
 
877
2194
  return json({ server }, 201);
@@ -974,7 +2291,7 @@ export async function handleApiRequest(req: Request, path: string): Promise<Resp
974
2291
  }
975
2292
 
976
2293
  // Get a port for the HTTP proxy
977
- const port = getNextPort();
2294
+ const port = await getNextPort();
978
2295
 
979
2296
  console.log(`Starting MCP server ${server.name}...`);
980
2297
  console.log(` Command: ${cmd.join(" ")}`);
@@ -1021,7 +2338,24 @@ export async function handleApiRequest(req: Request, path: string): Promise<Resp
1021
2338
  return json({ error: "MCP server not found" }, 404);
1022
2339
  }
1023
2340
 
1024
- // Check if process is running
2341
+ // HTTP servers use remote HTTP transport
2342
+ if (server.type === "http" && server.url) {
2343
+ try {
2344
+ const httpClient = getHttpMcpClient(server.url, server.headers || {});
2345
+ const serverInfo = await httpClient.initialize();
2346
+ const tools = await httpClient.listTools();
2347
+
2348
+ return json({
2349
+ serverInfo,
2350
+ tools,
2351
+ });
2352
+ } catch (err) {
2353
+ console.error(`Failed to list HTTP MCP tools: ${err}`);
2354
+ return json({ error: `Failed to communicate with MCP server: ${err}` }, 500);
2355
+ }
2356
+ }
2357
+
2358
+ // Stdio servers require a running process
1025
2359
  const mcpProcess = getMcpProcess(server.id);
1026
2360
  if (!mcpProcess) {
1027
2361
  return json({ error: "MCP server is not running" }, 400);
@@ -1049,14 +2383,30 @@ export async function handleApiRequest(req: Request, path: string): Promise<Resp
1049
2383
  return json({ error: "MCP server not found" }, 404);
1050
2384
  }
1051
2385
 
1052
- // Check if process is running
2386
+ const toolName = decodeURIComponent(mcpToolCallMatch[2]);
2387
+
2388
+ // HTTP servers use remote HTTP transport
2389
+ if (server.type === "http" && server.url) {
2390
+ try {
2391
+ const body = await req.json();
2392
+ const args = body.arguments || {};
2393
+
2394
+ const httpClient = getHttpMcpClient(server.url, server.headers || {});
2395
+ const result = await httpClient.callTool(toolName, args);
2396
+
2397
+ return json({ result });
2398
+ } catch (err) {
2399
+ console.error(`Failed to call HTTP MCP tool: ${err}`);
2400
+ return json({ error: `Failed to call tool: ${err}` }, 500);
2401
+ }
2402
+ }
2403
+
2404
+ // Stdio servers require a running process
1053
2405
  const mcpProcess = getMcpProcess(server.id);
1054
2406
  if (!mcpProcess) {
1055
2407
  return json({ error: "MCP server is not running" }, 400);
1056
2408
  }
1057
2409
 
1058
- const toolName = decodeURIComponent(mcpToolCallMatch[2]);
1059
-
1060
2410
  try {
1061
2411
  const body = await req.json();
1062
2412
  const args = body.arguments || {};
@@ -1156,8 +2506,10 @@ export async function handleApiRequest(req: Request, path: string): Promise<Resp
1156
2506
  // GET /api/telemetry/events - Query telemetry events
1157
2507
  if (path === "/api/telemetry/events" && method === "GET") {
1158
2508
  const url = new URL(req.url);
2509
+ const projectIdParam = url.searchParams.get("project_id");
1159
2510
  const events = TelemetryDB.query({
1160
2511
  agent_id: url.searchParams.get("agent_id") || undefined,
2512
+ project_id: projectIdParam === "null" ? null : projectIdParam || undefined,
1161
2513
  category: url.searchParams.get("category") || undefined,
1162
2514
  level: url.searchParams.get("level") || undefined,
1163
2515
  trace_id: url.searchParams.get("trace_id") || undefined,
@@ -1172,8 +2524,10 @@ export async function handleApiRequest(req: Request, path: string): Promise<Resp
1172
2524
  // GET /api/telemetry/usage - Get usage statistics
1173
2525
  if (path === "/api/telemetry/usage" && method === "GET") {
1174
2526
  const url = new URL(req.url);
2527
+ const projectIdParam = url.searchParams.get("project_id");
1175
2528
  const usage = TelemetryDB.getUsage({
1176
2529
  agent_id: url.searchParams.get("agent_id") || undefined,
2530
+ project_id: projectIdParam === "null" ? null : projectIdParam || undefined,
1177
2531
  since: url.searchParams.get("since") || undefined,
1178
2532
  until: url.searchParams.get("until") || undefined,
1179
2533
  group_by: (url.searchParams.get("group_by") as "agent" | "day") || undefined,
@@ -1185,7 +2539,11 @@ export async function handleApiRequest(req: Request, path: string): Promise<Resp
1185
2539
  if (path === "/api/telemetry/stats" && method === "GET") {
1186
2540
  const url = new URL(req.url);
1187
2541
  const agentId = url.searchParams.get("agent_id") || undefined;
1188
- const stats = TelemetryDB.getStats(agentId);
2542
+ const projectIdParam = url.searchParams.get("project_id");
2543
+ const stats = TelemetryDB.getStats({
2544
+ agentId,
2545
+ projectId: projectIdParam === "null" ? null : projectIdParam || undefined,
2546
+ });
1189
2547
  return json({ stats });
1190
2548
  }
1191
2549