apteva 0.2.6 → 0.2.8

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 (41) hide show
  1. package/dist/App.hzbfeg94.js +217 -0
  2. package/dist/index.html +3 -1
  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 +570 -32
  9. package/src/routes/api.ts +913 -38
  10. package/src/routes/auth.ts +242 -0
  11. package/src/server.ts +60 -8
  12. package/src/web/App.tsx +61 -19
  13. package/src/web/components/agents/AgentCard.tsx +30 -41
  14. package/src/web/components/agents/AgentPanel.tsx +751 -11
  15. package/src/web/components/agents/AgentsView.tsx +81 -9
  16. package/src/web/components/agents/CreateAgentModal.tsx +28 -1
  17. package/src/web/components/auth/CreateAccountStep.tsx +176 -0
  18. package/src/web/components/auth/LoginPage.tsx +91 -0
  19. package/src/web/components/auth/index.ts +2 -0
  20. package/src/web/components/common/Icons.tsx +48 -0
  21. package/src/web/components/common/Modal.tsx +1 -1
  22. package/src/web/components/dashboard/Dashboard.tsx +91 -31
  23. package/src/web/components/index.ts +3 -0
  24. package/src/web/components/layout/Header.tsx +145 -15
  25. package/src/web/components/layout/Sidebar.tsx +81 -43
  26. package/src/web/components/mcp/McpPage.tsx +261 -32
  27. package/src/web/components/onboarding/OnboardingWizard.tsx +64 -8
  28. package/src/web/components/settings/SettingsPage.tsx +404 -18
  29. package/src/web/components/tasks/TasksPage.tsx +21 -19
  30. package/src/web/components/telemetry/TelemetryPage.tsx +271 -81
  31. package/src/web/context/AuthContext.tsx +230 -0
  32. package/src/web/context/ProjectContext.tsx +182 -0
  33. package/src/web/context/TelemetryContext.tsx +98 -76
  34. package/src/web/context/index.ts +5 -0
  35. package/src/web/hooks/useAgents.ts +18 -6
  36. package/src/web/hooks/useOnboarding.ts +20 -4
  37. package/src/web/hooks/useProviders.ts +15 -5
  38. package/src/web/icon.png +0 -0
  39. package/src/web/styles.css +12 -0
  40. package/src/web/types.ts +6 -0
  41. package/dist/App.0mzj9cz9.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";
4
+ import { mkdirSync, existsSync, rmSync } from "fs";
5
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";
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,
@@ -35,6 +37,11 @@ function json(data: unknown, status = 200): Response {
35
37
  });
36
38
  }
37
39
 
40
+ const isDev = process.env.NODE_ENV !== "production";
41
+ function debug(...args: unknown[]) {
42
+ if (isDev) console.log("[api]", ...args);
43
+ }
44
+
38
45
  // Wait for agent to be healthy (with timeout)
39
46
  async function waitForAgentHealth(port: number, maxAttempts = 30, delayMs = 200): Promise<boolean> {
40
47
  for (let i = 0; i < maxAttempts; i++) {
@@ -57,17 +64,37 @@ function buildAgentConfig(agent: Agent, providerKey: string) {
57
64
  const features = agent.features;
58
65
 
59
66
  // 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
- }));
67
+ // Supports both local servers and Composio configs (prefixed with "composio:")
68
+ const mcpServers: Array<{ name: string; type: "http"; url: string; headers: Record<string, string>; enabled: boolean }> = [];
69
+
70
+ for (const id of agent.mcp_servers || []) {
71
+ // Check if this is a Composio config
72
+ if (id.startsWith("composio:")) {
73
+ const configId = id.slice(9); // Remove "composio:" prefix
74
+ const composioKey = ProviderKeys.getDecrypted("composio");
75
+ if (composioKey) {
76
+ mcpServers.push({
77
+ name: `composio-${configId.slice(0, 8)}`,
78
+ type: "http",
79
+ url: `https://backend.composio.dev/v3/mcp/${configId}`,
80
+ headers: { "x-api-key": composioKey },
81
+ enabled: true,
82
+ });
83
+ }
84
+ } else {
85
+ // Local MCP server
86
+ const server = McpServerDB.findById(id);
87
+ if (server && server.status === "running" && server.port) {
88
+ mcpServers.push({
89
+ name: server.name,
90
+ type: "http",
91
+ url: `http://localhost:${server.port}/mcp`,
92
+ headers: {},
93
+ enabled: true,
94
+ });
95
+ }
96
+ }
97
+ }
71
98
 
72
99
  return {
73
100
  id: agent.id,
@@ -161,8 +188,8 @@ function buildAgentConfig(agent: Agent, providerKey: string) {
161
188
  telemetry: {
162
189
  enabled: true,
163
190
  endpoint: `http://localhost:${process.env.PORT || 4280}/api/telemetry`,
164
- batch_size: 10,
165
- flush_interval: 30,
191
+ batch_size: 1,
192
+ flush_interval: 1, // Every 1 second
166
193
  categories: [], // Empty = all categories
167
194
  },
168
195
  };
@@ -319,13 +346,38 @@ function toApiAgent(agent: Agent) {
319
346
  features: agent.features,
320
347
  mcpServers: agent.mcp_servers, // Keep IDs for backwards compatibility
321
348
  mcpServerDetails, // Include full details
349
+ projectId: agent.project_id,
322
350
  createdAt: agent.created_at,
323
351
  updatedAt: agent.updated_at,
324
352
  };
325
353
  }
326
354
 
327
- export async function handleApiRequest(req: Request, path: string): Promise<Response> {
355
+ // Transform DB project to API response format
356
+ function toApiProject(project: Project) {
357
+ return {
358
+ id: project.id,
359
+ name: project.name,
360
+ description: project.description,
361
+ color: project.color,
362
+ createdAt: project.created_at,
363
+ updatedAt: project.updated_at,
364
+ };
365
+ }
366
+
367
+ export async function handleApiRequest(req: Request, path: string, authContext?: AuthContext): Promise<Response> {
328
368
  const method = req.method;
369
+ const user = authContext?.user;
370
+
371
+ // GET /api/health - Health check endpoint (no auth required, handled before middleware in server.ts)
372
+ if (path === "/api/health" && method === "GET") {
373
+ const agentCount = AgentDB.count();
374
+ const runningAgents = AgentDB.findRunning().length;
375
+ return json({
376
+ status: "ok",
377
+ version: getAptevaVersion(),
378
+ agents: { total: agentCount, running: runningAgents },
379
+ });
380
+ }
329
381
 
330
382
  // GET /api/agents - List all agents
331
383
  if (path === "/api/agents" && method === "GET") {
@@ -337,7 +389,7 @@ export async function handleApiRequest(req: Request, path: string): Promise<Resp
337
389
  if (path === "/api/agents" && method === "POST") {
338
390
  try {
339
391
  const body = await req.json();
340
- const { name, model, provider, systemPrompt, features } = body;
392
+ const { name, model, provider, systemPrompt, features, projectId } = body;
341
393
 
342
394
  if (!name) {
343
395
  return json({ error: "Name is required" }, 400);
@@ -354,6 +406,7 @@ export async function handleApiRequest(req: Request, path: string): Promise<Resp
354
406
  system_prompt: systemPrompt || "You are a helpful assistant.",
355
407
  features: features || DEFAULT_FEATURES,
356
408
  mcp_servers: body.mcpServers || [],
409
+ project_id: projectId || null,
357
410
  });
358
411
 
359
412
  return json({ agent: toApiAgent(agent) }, 201);
@@ -390,6 +443,7 @@ export async function handleApiRequest(req: Request, path: string): Promise<Resp
390
443
  if (body.systemPrompt !== undefined) updates.system_prompt = body.systemPrompt;
391
444
  if (body.features !== undefined) updates.features = body.features;
392
445
  if (body.mcpServers !== undefined) updates.mcp_servers = body.mcpServers;
446
+ if (body.projectId !== undefined) updates.project_id = body.projectId;
393
447
 
394
448
  const updated = AgentDB.update(agentMatch[1], updates);
395
449
 
@@ -413,19 +467,34 @@ export async function handleApiRequest(req: Request, path: string): Promise<Resp
413
467
 
414
468
  // DELETE /api/agents/:id - Delete an agent
415
469
  if (agentMatch && method === "DELETE") {
416
- const agent = AgentDB.findById(agentMatch[1]);
470
+ const agentId = agentMatch[1];
471
+ const agent = AgentDB.findById(agentId);
417
472
  if (!agent) {
418
473
  return json({ error: "Agent not found" }, 404);
419
474
  }
420
475
 
421
476
  // Stop the agent if running
422
- const proc = agentProcesses.get(agentMatch[1]);
477
+ const proc = agentProcesses.get(agentId);
423
478
  if (proc) {
424
479
  proc.kill();
425
- agentProcesses.delete(agentMatch[1]);
480
+ agentProcesses.delete(agentId);
481
+ }
482
+
483
+ // Delete agent's telemetry data
484
+ TelemetryDB.deleteByAgent(agentId);
485
+
486
+ // Delete agent's data directory (contains threads, messages, etc.)
487
+ const agentDataDir = join(AGENTS_DATA_DIR, agentId);
488
+ if (existsSync(agentDataDir)) {
489
+ try {
490
+ rmSync(agentDataDir, { recursive: true, force: true });
491
+ console.log(`Deleted agent data directory: ${agentDataDir}`);
492
+ } catch (err) {
493
+ console.error(`Failed to delete agent data directory: ${err}`);
494
+ }
426
495
  }
427
496
 
428
- AgentDB.delete(agentMatch[1]);
497
+ AgentDB.delete(agentId);
429
498
  return json({ success: true });
430
499
  }
431
500
 
@@ -511,6 +580,432 @@ export async function handleApiRequest(req: Request, path: string): Promise<Resp
511
580
  }
512
581
  }
513
582
 
583
+ // ==================== THREAD & MESSAGE PROXY ====================
584
+
585
+ // GET /api/agents/:id/threads - List threads for an agent
586
+ const threadsListMatch = path.match(/^\/api\/agents\/([^/]+)\/threads$/);
587
+ if (threadsListMatch && method === "GET") {
588
+ const agent = AgentDB.findById(threadsListMatch[1]);
589
+ if (!agent) {
590
+ return json({ error: "Agent not found" }, 404);
591
+ }
592
+
593
+ if (agent.status !== "running" || !agent.port) {
594
+ return json({ error: "Agent is not running" }, 400);
595
+ }
596
+
597
+ try {
598
+ const agentUrl = `http://localhost:${agent.port}/threads`;
599
+ const response = await fetch(agentUrl, {
600
+ method: "GET",
601
+ headers: { "Accept": "application/json" },
602
+ });
603
+
604
+ if (!response.ok) {
605
+ const errorText = await response.text();
606
+ return json({ error: `Agent error: ${errorText}` }, response.status);
607
+ }
608
+
609
+ const data = await response.json();
610
+ return json(data);
611
+ } catch (err) {
612
+ console.error(`Threads list proxy error: ${err}`);
613
+ return json({ error: `Failed to fetch threads: ${err}` }, 500);
614
+ }
615
+ }
616
+
617
+ // POST /api/agents/:id/threads - Create a new thread
618
+ if (threadsListMatch && method === "POST") {
619
+ const agent = AgentDB.findById(threadsListMatch[1]);
620
+ if (!agent) {
621
+ return json({ error: "Agent not found" }, 404);
622
+ }
623
+
624
+ if (agent.status !== "running" || !agent.port) {
625
+ return json({ error: "Agent is not running" }, 400);
626
+ }
627
+
628
+ try {
629
+ const body = await req.json().catch(() => ({}));
630
+ const agentUrl = `http://localhost:${agent.port}/threads`;
631
+ const response = await fetch(agentUrl, {
632
+ method: "POST",
633
+ headers: { "Content-Type": "application/json" },
634
+ body: JSON.stringify(body),
635
+ });
636
+
637
+ if (!response.ok) {
638
+ const errorText = await response.text();
639
+ return json({ error: `Agent error: ${errorText}` }, response.status);
640
+ }
641
+
642
+ const data = await response.json();
643
+ return json(data, 201);
644
+ } catch (err) {
645
+ console.error(`Thread create proxy error: ${err}`);
646
+ return json({ error: `Failed to create thread: ${err}` }, 500);
647
+ }
648
+ }
649
+
650
+ // GET /api/agents/:id/threads/:threadId - Get a specific thread
651
+ const threadDetailMatch = path.match(/^\/api\/agents\/([^/]+)\/threads\/([^/]+)$/);
652
+ if (threadDetailMatch && method === "GET") {
653
+ const agent = AgentDB.findById(threadDetailMatch[1]);
654
+ if (!agent) {
655
+ return json({ error: "Agent not found" }, 404);
656
+ }
657
+
658
+ if (agent.status !== "running" || !agent.port) {
659
+ return json({ error: "Agent is not running" }, 400);
660
+ }
661
+
662
+ try {
663
+ const threadId = threadDetailMatch[2];
664
+ const agentUrl = `http://localhost:${agent.port}/threads/${threadId}`;
665
+ const response = await fetch(agentUrl, {
666
+ method: "GET",
667
+ headers: { "Accept": "application/json" },
668
+ });
669
+
670
+ if (!response.ok) {
671
+ const errorText = await response.text();
672
+ return json({ error: `Agent error: ${errorText}` }, response.status);
673
+ }
674
+
675
+ const data = await response.json();
676
+ return json(data);
677
+ } catch (err) {
678
+ console.error(`Thread detail proxy error: ${err}`);
679
+ return json({ error: `Failed to fetch thread: ${err}` }, 500);
680
+ }
681
+ }
682
+
683
+ // DELETE /api/agents/:id/threads/:threadId - Delete a thread
684
+ if (threadDetailMatch && method === "DELETE") {
685
+ const agent = AgentDB.findById(threadDetailMatch[1]);
686
+ if (!agent) {
687
+ return json({ error: "Agent not found" }, 404);
688
+ }
689
+
690
+ if (agent.status !== "running" || !agent.port) {
691
+ return json({ error: "Agent is not running" }, 400);
692
+ }
693
+
694
+ try {
695
+ const threadId = threadDetailMatch[2];
696
+ const agentUrl = `http://localhost:${agent.port}/threads/${threadId}`;
697
+ const response = await fetch(agentUrl, {
698
+ method: "DELETE",
699
+ });
700
+
701
+ if (!response.ok) {
702
+ const errorText = await response.text();
703
+ return json({ error: `Agent error: ${errorText}` }, response.status);
704
+ }
705
+
706
+ return json({ success: true });
707
+ } catch (err) {
708
+ console.error(`Thread delete proxy error: ${err}`);
709
+ return json({ error: `Failed to delete thread: ${err}` }, 500);
710
+ }
711
+ }
712
+
713
+ // GET /api/agents/:id/threads/:threadId/messages - Get messages in a thread
714
+ const threadMessagesMatch = path.match(/^\/api\/agents\/([^/]+)\/threads\/([^/]+)\/messages$/);
715
+ if (threadMessagesMatch && method === "GET") {
716
+ const agent = AgentDB.findById(threadMessagesMatch[1]);
717
+ if (!agent) {
718
+ return json({ error: "Agent not found" }, 404);
719
+ }
720
+
721
+ if (agent.status !== "running" || !agent.port) {
722
+ return json({ error: "Agent is not running" }, 400);
723
+ }
724
+
725
+ try {
726
+ const threadId = threadMessagesMatch[2];
727
+ const agentUrl = `http://localhost:${agent.port}/threads/${threadId}/messages`;
728
+ const response = await fetch(agentUrl, {
729
+ method: "GET",
730
+ headers: { "Accept": "application/json" },
731
+ });
732
+
733
+ if (!response.ok) {
734
+ const errorText = await response.text();
735
+ return json({ error: `Agent error: ${errorText}` }, response.status);
736
+ }
737
+
738
+ const data = await response.json();
739
+ return json(data);
740
+ } catch (err) {
741
+ console.error(`Thread messages proxy error: ${err}`);
742
+ return json({ error: `Failed to fetch messages: ${err}` }, 500);
743
+ }
744
+ }
745
+
746
+ // ==================== MEMORY PROXY ====================
747
+
748
+ // GET /api/agents/:id/memories - List memories for an agent
749
+ const memoriesMatch = path.match(/^\/api\/agents\/([^/]+)\/memories$/);
750
+ if (memoriesMatch && method === "GET") {
751
+ const agent = AgentDB.findById(memoriesMatch[1]);
752
+ if (!agent) {
753
+ return json({ error: "Agent not found" }, 404);
754
+ }
755
+
756
+ if (agent.status !== "running" || !agent.port) {
757
+ return json({ error: "Agent is not running" }, 400);
758
+ }
759
+
760
+ try {
761
+ const url = new URL(req.url);
762
+ const threadId = url.searchParams.get("thread_id") || "";
763
+ const agentUrl = `http://localhost:${agent.port}/memories${threadId ? `?thread_id=${threadId}` : ""}`;
764
+ const response = await fetch(agentUrl, {
765
+ method: "GET",
766
+ headers: { "Accept": "application/json" },
767
+ });
768
+
769
+ if (!response.ok) {
770
+ const errorText = await response.text();
771
+ return json({ error: `Agent error: ${errorText}` }, response.status);
772
+ }
773
+
774
+ const data = await response.json();
775
+ return json(data);
776
+ } catch (err) {
777
+ console.error(`Memories list proxy error: ${err}`);
778
+ return json({ error: `Failed to fetch memories: ${err}` }, 500);
779
+ }
780
+ }
781
+
782
+ // DELETE /api/agents/:id/memories - Clear all memories for an agent
783
+ if (memoriesMatch && method === "DELETE") {
784
+ const agent = AgentDB.findById(memoriesMatch[1]);
785
+ if (!agent) {
786
+ return json({ error: "Agent not found" }, 404);
787
+ }
788
+
789
+ if (agent.status !== "running" || !agent.port) {
790
+ return json({ error: "Agent is not running" }, 400);
791
+ }
792
+
793
+ try {
794
+ const agentUrl = `http://localhost:${agent.port}/memories`;
795
+ const response = await fetch(agentUrl, { method: "DELETE" });
796
+
797
+ if (!response.ok) {
798
+ const errorText = await response.text();
799
+ return json({ error: `Agent error: ${errorText}` }, response.status);
800
+ }
801
+
802
+ return json({ success: true });
803
+ } catch (err) {
804
+ console.error(`Memories clear proxy error: ${err}`);
805
+ return json({ error: `Failed to clear memories: ${err}` }, 500);
806
+ }
807
+ }
808
+
809
+ // DELETE /api/agents/:id/memories/:memoryId - Delete a specific memory
810
+ const memoryDeleteMatch = path.match(/^\/api\/agents\/([^/]+)\/memories\/([^/]+)$/);
811
+ if (memoryDeleteMatch && method === "DELETE") {
812
+ const agent = AgentDB.findById(memoryDeleteMatch[1]);
813
+ if (!agent) {
814
+ return json({ error: "Agent not found" }, 404);
815
+ }
816
+
817
+ if (agent.status !== "running" || !agent.port) {
818
+ return json({ error: "Agent is not running" }, 400);
819
+ }
820
+
821
+ try {
822
+ const memoryId = memoryDeleteMatch[2];
823
+ const agentUrl = `http://localhost:${agent.port}/memories/${memoryId}`;
824
+ const response = await fetch(agentUrl, { method: "DELETE" });
825
+
826
+ if (!response.ok) {
827
+ const errorText = await response.text();
828
+ return json({ error: `Agent error: ${errorText}` }, response.status);
829
+ }
830
+
831
+ return json({ success: true });
832
+ } catch (err) {
833
+ console.error(`Memory delete proxy error: ${err}`);
834
+ return json({ error: `Failed to delete memory: ${err}` }, 500);
835
+ }
836
+ }
837
+
838
+ // ==================== FILES PROXY ====================
839
+
840
+ // GET /api/agents/:id/files - List files for an agent
841
+ const filesMatch = path.match(/^\/api\/agents\/([^/]+)\/files$/);
842
+ if (filesMatch && method === "GET") {
843
+ const agent = AgentDB.findById(filesMatch[1]);
844
+ if (!agent) {
845
+ return json({ error: "Agent not found" }, 404);
846
+ }
847
+
848
+ if (agent.status !== "running" || !agent.port) {
849
+ return json({ error: "Agent is not running" }, 400);
850
+ }
851
+
852
+ try {
853
+ const url = new URL(req.url);
854
+ const params = new URLSearchParams();
855
+ if (url.searchParams.get("thread_id")) params.set("thread_id", url.searchParams.get("thread_id")!);
856
+ if (url.searchParams.get("limit")) params.set("limit", url.searchParams.get("limit")!);
857
+
858
+ const agentUrl = `http://localhost:${agent.port}/files${params.toString() ? `?${params}` : ""}`;
859
+ const response = await fetch(agentUrl, {
860
+ method: "GET",
861
+ headers: { "Accept": "application/json" },
862
+ });
863
+
864
+ if (!response.ok) {
865
+ const errorText = await response.text();
866
+ return json({ error: `Agent error: ${errorText}` }, response.status);
867
+ }
868
+
869
+ const data = await response.json();
870
+ return json(data);
871
+ } catch (err) {
872
+ console.error(`Files list proxy error: ${err}`);
873
+ return json({ error: `Failed to fetch files: ${err}` }, 500);
874
+ }
875
+ }
876
+
877
+ // GET /api/agents/:id/files/:fileId - Get a specific file
878
+ const fileGetMatch = path.match(/^\/api\/agents\/([^/]+)\/files\/([^/]+)$/);
879
+ if (fileGetMatch && method === "GET") {
880
+ const agent = AgentDB.findById(fileGetMatch[1]);
881
+ if (!agent) {
882
+ return json({ error: "Agent not found" }, 404);
883
+ }
884
+
885
+ if (agent.status !== "running" || !agent.port) {
886
+ return json({ error: "Agent is not running" }, 400);
887
+ }
888
+
889
+ try {
890
+ const fileId = fileGetMatch[2];
891
+ const agentUrl = `http://localhost:${agent.port}/files/${fileId}`;
892
+ const response = await fetch(agentUrl, {
893
+ method: "GET",
894
+ headers: { "Accept": "application/json" },
895
+ });
896
+
897
+ if (!response.ok) {
898
+ const errorText = await response.text();
899
+ return json({ error: `Agent error: ${errorText}` }, response.status);
900
+ }
901
+
902
+ const data = await response.json();
903
+ return json(data);
904
+ } catch (err) {
905
+ console.error(`File get proxy error: ${err}`);
906
+ return json({ error: `Failed to fetch file: ${err}` }, 500);
907
+ }
908
+ }
909
+
910
+ // DELETE /api/agents/:id/files/:fileId - Delete a specific file
911
+ if (fileGetMatch && method === "DELETE") {
912
+ const agent = AgentDB.findById(fileGetMatch[1]);
913
+ if (!agent) {
914
+ return json({ error: "Agent not found" }, 404);
915
+ }
916
+
917
+ if (agent.status !== "running" || !agent.port) {
918
+ return json({ error: "Agent is not running" }, 400);
919
+ }
920
+
921
+ try {
922
+ const fileId = fileGetMatch[2];
923
+ const agentUrl = `http://localhost:${agent.port}/files/${fileId}`;
924
+ const response = await fetch(agentUrl, { method: "DELETE" });
925
+
926
+ if (!response.ok) {
927
+ const errorText = await response.text();
928
+ return json({ error: `Agent error: ${errorText}` }, response.status);
929
+ }
930
+
931
+ return json({ success: true });
932
+ } catch (err) {
933
+ console.error(`File delete proxy error: ${err}`);
934
+ return json({ error: `Failed to delete file: ${err}` }, 500);
935
+ }
936
+ }
937
+
938
+ // GET /api/agents/:id/files/:fileId/download - Download a file
939
+ const fileDownloadMatch = path.match(/^\/api\/agents\/([^/]+)\/files\/([^/]+)\/download$/);
940
+ if (fileDownloadMatch && method === "GET") {
941
+ const agent = AgentDB.findById(fileDownloadMatch[1]);
942
+ if (!agent) {
943
+ return json({ error: "Agent not found" }, 404);
944
+ }
945
+
946
+ if (agent.status !== "running" || !agent.port) {
947
+ return json({ error: "Agent is not running" }, 400);
948
+ }
949
+
950
+ try {
951
+ const fileId = fileDownloadMatch[2];
952
+ const agentUrl = `http://localhost:${agent.port}/files/${fileId}/download`;
953
+ const response = await fetch(agentUrl);
954
+
955
+ if (!response.ok) {
956
+ const errorText = await response.text();
957
+ return json({ error: `Agent error: ${errorText}` }, response.status);
958
+ }
959
+
960
+ // Pass through the file response
961
+ return new Response(response.body, {
962
+ status: response.status,
963
+ headers: {
964
+ "Content-Type": response.headers.get("Content-Type") || "application/octet-stream",
965
+ "Content-Disposition": response.headers.get("Content-Disposition") || "attachment",
966
+ "Content-Length": response.headers.get("Content-Length") || "",
967
+ },
968
+ });
969
+ } catch (err) {
970
+ console.error(`File download proxy error: ${err}`);
971
+ return json({ error: `Failed to download file: ${err}` }, 500);
972
+ }
973
+ }
974
+
975
+ // ==================== DISCOVERY/PEERS PROXY ====================
976
+
977
+ // GET /api/agents/:id/peers - Get discovered peer agents
978
+ const peersMatch = path.match(/^\/api\/agents\/([^/]+)\/peers$/);
979
+ if (peersMatch && method === "GET") {
980
+ const agent = AgentDB.findById(peersMatch[1]);
981
+ if (!agent) {
982
+ return json({ error: "Agent not found" }, 404);
983
+ }
984
+
985
+ if (agent.status !== "running" || !agent.port) {
986
+ return json({ error: "Agent is not running" }, 400);
987
+ }
988
+
989
+ try {
990
+ const agentUrl = `http://localhost:${agent.port}/discovery/agents`;
991
+ const response = await fetch(agentUrl, {
992
+ method: "GET",
993
+ headers: { "Accept": "application/json" },
994
+ });
995
+
996
+ if (!response.ok) {
997
+ const errorText = await response.text();
998
+ return json({ error: `Agent error: ${errorText}` }, response.status);
999
+ }
1000
+
1001
+ const data = await response.json();
1002
+ return json(data);
1003
+ } catch (err) {
1004
+ console.error(`Peers list proxy error: ${err}`);
1005
+ return json({ error: `Failed to fetch peers: ${err}` }, 500);
1006
+ }
1007
+ }
1008
+
514
1009
  // GET /api/providers - List supported providers and models with key status
515
1010
  if (path === "/api/providers" && method === "GET") {
516
1011
  const providers = getProvidersWithStatus();
@@ -536,6 +1031,274 @@ export async function handleApiRequest(req: Request, path: string): Promise<Resp
536
1031
  return json({ success: true });
537
1032
  }
538
1033
 
1034
+ // POST /api/onboarding/user - Create first user during onboarding
1035
+ // This endpoint only works when no users exist (enforced by middleware)
1036
+ if (path === "/api/onboarding/user" && method === "POST") {
1037
+ debug("POST /api/onboarding/user");
1038
+ // Double-check no users exist
1039
+ if (UserDB.hasUsers()) {
1040
+ debug("Users already exist");
1041
+ return json({ error: "Users already exist" }, 403);
1042
+ }
1043
+
1044
+ try {
1045
+ const body = await req.json();
1046
+ debug("Onboarding body:", JSON.stringify(body));
1047
+ const { username, password, email } = body;
1048
+
1049
+ if (!username || !password) {
1050
+ debug("Missing username or password");
1051
+ return json({ error: "Username and password are required" }, 400);
1052
+ }
1053
+
1054
+ // Create first user as admin
1055
+ debug("Creating user:", username);
1056
+ const result = await createUser({
1057
+ username,
1058
+ password,
1059
+ email: email || undefined, // Optional, for password recovery
1060
+ role: "admin",
1061
+ });
1062
+ debug("Create user result:", result.success, result.error);
1063
+
1064
+ if (!result.success) {
1065
+ return json({ error: result.error }, 400);
1066
+ }
1067
+
1068
+ return json({
1069
+ success: true,
1070
+ user: {
1071
+ id: result.user!.id,
1072
+ username: result.user!.username,
1073
+ role: result.user!.role,
1074
+ },
1075
+ }, 201);
1076
+ } catch (e) {
1077
+ debug("Onboarding error:", e);
1078
+ return json({ error: "Invalid request body" }, 400);
1079
+ }
1080
+ }
1081
+
1082
+ // ==================== USER MANAGEMENT (Admin only) ====================
1083
+
1084
+ // GET /api/users - List all users
1085
+ if (path === "/api/users" && method === "GET") {
1086
+ const users = UserDB.findAll().map(u => ({
1087
+ id: u.id,
1088
+ username: u.username,
1089
+ email: u.email,
1090
+ role: u.role,
1091
+ createdAt: u.created_at,
1092
+ lastLoginAt: u.last_login_at,
1093
+ }));
1094
+ return json({ users });
1095
+ }
1096
+
1097
+ // POST /api/users - Create a new user
1098
+ if (path === "/api/users" && method === "POST") {
1099
+ try {
1100
+ const body = await req.json();
1101
+ const { username, password, email, role } = body;
1102
+
1103
+ if (!username || !password) {
1104
+ return json({ error: "Username and password are required" }, 400);
1105
+ }
1106
+
1107
+ const result = await createUser({
1108
+ username,
1109
+ password,
1110
+ email: email || undefined,
1111
+ role: role || "user",
1112
+ });
1113
+
1114
+ if (!result.success) {
1115
+ return json({ error: result.error }, 400);
1116
+ }
1117
+
1118
+ return json({
1119
+ user: {
1120
+ id: result.user!.id,
1121
+ username: result.user!.username,
1122
+ email: result.user!.email,
1123
+ role: result.user!.role,
1124
+ createdAt: result.user!.created_at,
1125
+ },
1126
+ }, 201);
1127
+ } catch (e) {
1128
+ return json({ error: "Invalid request body" }, 400);
1129
+ }
1130
+ }
1131
+
1132
+ // GET /api/users/:id - Get a specific user
1133
+ const userMatch = path.match(/^\/api\/users\/([^/]+)$/);
1134
+ if (userMatch && method === "GET") {
1135
+ const targetUser = UserDB.findById(userMatch[1]);
1136
+ if (!targetUser) {
1137
+ return json({ error: "User not found" }, 404);
1138
+ }
1139
+ return json({
1140
+ user: {
1141
+ id: targetUser.id,
1142
+ username: targetUser.username,
1143
+ email: targetUser.email,
1144
+ role: targetUser.role,
1145
+ createdAt: targetUser.created_at,
1146
+ lastLoginAt: targetUser.last_login_at,
1147
+ },
1148
+ });
1149
+ }
1150
+
1151
+ // PUT /api/users/:id - Update a user
1152
+ if (userMatch && method === "PUT") {
1153
+ const targetUser = UserDB.findById(userMatch[1]);
1154
+ if (!targetUser) {
1155
+ return json({ error: "User not found" }, 404);
1156
+ }
1157
+
1158
+ try {
1159
+ const body = await req.json();
1160
+ const updates: Parameters<typeof UserDB.update>[1] = {};
1161
+
1162
+ if (body.email !== undefined) updates.email = body.email;
1163
+ if (body.role !== undefined) {
1164
+ // Prevent removing last admin
1165
+ if (targetUser.role === "admin" && body.role !== "admin") {
1166
+ if (UserDB.countAdmins() <= 1) {
1167
+ return json({ error: "Cannot remove the last admin" }, 400);
1168
+ }
1169
+ }
1170
+ updates.role = body.role;
1171
+ }
1172
+ if (body.password !== undefined) {
1173
+ const validation = validatePassword(body.password);
1174
+ if (!validation.valid) {
1175
+ return json({ error: validation.errors.join(". ") }, 400);
1176
+ }
1177
+ updates.password_hash = await hashPassword(body.password);
1178
+ }
1179
+
1180
+ const updated = UserDB.update(userMatch[1], updates);
1181
+ return json({
1182
+ user: updated ? {
1183
+ id: updated.id,
1184
+ username: updated.username,
1185
+ email: updated.email,
1186
+ role: updated.role,
1187
+ createdAt: updated.created_at,
1188
+ lastLoginAt: updated.last_login_at,
1189
+ } : null,
1190
+ });
1191
+ } catch (e) {
1192
+ return json({ error: "Invalid request body" }, 400);
1193
+ }
1194
+ }
1195
+
1196
+ // DELETE /api/users/:id - Delete a user
1197
+ if (userMatch && method === "DELETE") {
1198
+ const targetUser = UserDB.findById(userMatch[1]);
1199
+ if (!targetUser) {
1200
+ return json({ error: "User not found" }, 404);
1201
+ }
1202
+
1203
+ // Prevent deleting yourself
1204
+ if (user && targetUser.id === user.id) {
1205
+ return json({ error: "Cannot delete your own account" }, 400);
1206
+ }
1207
+
1208
+ // Prevent deleting last admin
1209
+ if (targetUser.role === "admin" && UserDB.countAdmins() <= 1) {
1210
+ return json({ error: "Cannot delete the last admin" }, 400);
1211
+ }
1212
+
1213
+ UserDB.delete(userMatch[1]);
1214
+ return json({ success: true });
1215
+ }
1216
+
1217
+ // ==================== PROJECTS ====================
1218
+
1219
+ // GET /api/projects - List all projects
1220
+ if (path === "/api/projects" && method === "GET") {
1221
+ const projects = ProjectDB.findAll();
1222
+ const agentCounts = ProjectDB.getAgentCounts();
1223
+ return json({
1224
+ projects: projects.map(p => ({
1225
+ ...toApiProject(p),
1226
+ agentCount: agentCounts.get(p.id) || 0,
1227
+ })),
1228
+ unassignedCount: agentCounts.get(null) || 0,
1229
+ });
1230
+ }
1231
+
1232
+ // POST /api/projects - Create a new project
1233
+ if (path === "/api/projects" && method === "POST") {
1234
+ try {
1235
+ const body = await req.json();
1236
+ const { name, description, color } = body;
1237
+
1238
+ if (!name) {
1239
+ return json({ error: "Name is required" }, 400);
1240
+ }
1241
+
1242
+ const project = ProjectDB.create({
1243
+ name,
1244
+ description: description || null,
1245
+ color: color || "#6366f1",
1246
+ });
1247
+
1248
+ return json({ project: toApiProject(project) }, 201);
1249
+ } catch (e) {
1250
+ console.error("Create project error:", e);
1251
+ return json({ error: "Invalid request body" }, 400);
1252
+ }
1253
+ }
1254
+
1255
+ // GET /api/projects/:id - Get a specific project
1256
+ const projectMatch = path.match(/^\/api\/projects\/([^/]+)$/);
1257
+ if (projectMatch && method === "GET") {
1258
+ const project = ProjectDB.findById(projectMatch[1]);
1259
+ if (!project) {
1260
+ return json({ error: "Project not found" }, 404);
1261
+ }
1262
+ const agents = AgentDB.findByProject(project.id);
1263
+ return json({
1264
+ project: toApiProject(project),
1265
+ agents: agents.map(toApiAgent),
1266
+ });
1267
+ }
1268
+
1269
+ // PUT /api/projects/:id - Update a project
1270
+ if (projectMatch && method === "PUT") {
1271
+ const project = ProjectDB.findById(projectMatch[1]);
1272
+ if (!project) {
1273
+ return json({ error: "Project not found" }, 404);
1274
+ }
1275
+
1276
+ try {
1277
+ const body = await req.json();
1278
+ const updates: Partial<Project> = {};
1279
+
1280
+ if (body.name !== undefined) updates.name = body.name;
1281
+ if (body.description !== undefined) updates.description = body.description;
1282
+ if (body.color !== undefined) updates.color = body.color;
1283
+
1284
+ const updated = ProjectDB.update(projectMatch[1], updates);
1285
+ return json({ project: updated ? toApiProject(updated) : null });
1286
+ } catch (e) {
1287
+ return json({ error: "Invalid request body" }, 400);
1288
+ }
1289
+ }
1290
+
1291
+ // DELETE /api/projects/:id - Delete a project
1292
+ if (projectMatch && method === "DELETE") {
1293
+ const project = ProjectDB.findById(projectMatch[1]);
1294
+ if (!project) {
1295
+ return json({ error: "Project not found" }, 404);
1296
+ }
1297
+
1298
+ ProjectDB.delete(projectMatch[1]);
1299
+ return json({ success: true });
1300
+ }
1301
+
539
1302
  // ==================== API KEYS ====================
540
1303
 
541
1304
  // GET /api/keys - List all configured provider keys (without actual keys)
@@ -830,20 +1593,39 @@ export async function handleApiRequest(req: Request, path: string): Promise<Resp
830
1593
  }
831
1594
  const data = await res.json();
832
1595
 
833
- // Transform to simpler format
834
- const servers = (data.servers || []).map((item: any) => {
835
- const s = item.server;
836
- const pkg = s.packages?.find((p: any) => p.registryType === "npm");
837
- return {
838
- name: s.name,
839
- description: s.description,
840
- version: s.version,
841
- repository: s.repository?.url,
842
- npmPackage: pkg?.identifier,
843
- transport: pkg?.transport?.type || "stdio",
844
- envVars: pkg?.environmentVariables || [],
845
- };
846
- }).filter((s: any) => s.npmPackage); // Only show npm packages for now
1596
+ // Transform to simpler format - dedupe by name
1597
+ const seen = new Set<string>();
1598
+ const servers = (data.servers || [])
1599
+ .map((item: any) => {
1600
+ const s = item.server;
1601
+ const pkg = s.packages?.find((p: any) => p.registryType === "npm");
1602
+ const remote = s.remotes?.[0];
1603
+
1604
+ // Extract a short display name from the full name
1605
+ // e.g., "ai.smithery/smithery-ai-github" -> "github"
1606
+ // e.g., "io.github.user/my-server" -> "my-server"
1607
+ const fullName = s.name || "";
1608
+ const shortName = fullName.split("/").pop()?.replace(/-mcp$/, "").replace(/^mcp-/, "") || fullName;
1609
+
1610
+ return {
1611
+ id: fullName, // Use full name as unique ID
1612
+ name: shortName,
1613
+ fullName: fullName,
1614
+ description: s.description,
1615
+ version: s.version,
1616
+ repository: s.repository?.url,
1617
+ npmPackage: pkg?.identifier || null,
1618
+ remoteUrl: remote?.url || null,
1619
+ transport: pkg?.transport?.type || (remote ? "http" : "stdio"),
1620
+ };
1621
+ })
1622
+ .filter((s: any) => {
1623
+ // Dedupe by fullName
1624
+ if (seen.has(s.fullName)) return false;
1625
+ seen.add(s.fullName);
1626
+ // Only show servers with npm package or remote URL
1627
+ return s.npmPackage || s.remoteUrl;
1628
+ });
847
1629
 
848
1630
  return json({ servers });
849
1631
  } catch (e) {
@@ -851,6 +1633,85 @@ export async function handleApiRequest(req: Request, path: string): Promise<Resp
851
1633
  }
852
1634
  }
853
1635
 
1636
+ // ============ Composio Integration ============
1637
+
1638
+ // GET /api/integrations/composio/configs - List Composio MCP configs
1639
+ if (path === "/api/integrations/composio/configs" && method === "GET") {
1640
+ const apiKey = ProviderKeys.getDecrypted("composio");
1641
+ if (!apiKey) {
1642
+ return json({ error: "Composio API key not configured", configs: [] }, 200);
1643
+ }
1644
+
1645
+ try {
1646
+ const res = await fetch("https://backend.composio.dev/api/v3/mcp/servers?limit=50", {
1647
+ headers: {
1648
+ "x-api-key": apiKey,
1649
+ "Content-Type": "application/json",
1650
+ },
1651
+ });
1652
+
1653
+ if (!res.ok) {
1654
+ const text = await res.text();
1655
+ console.error("Composio API error:", res.status, text);
1656
+ return json({ error: "Failed to fetch Composio configs" }, 500);
1657
+ }
1658
+
1659
+ const data = await res.json();
1660
+
1661
+ // Transform to our format
1662
+ const configs = (data.items || data.servers || []).map((item: any) => ({
1663
+ id: item.id,
1664
+ name: item.name || item.id,
1665
+ toolkits: item.toolkits || item.apps || [],
1666
+ toolsCount: item.toolsCount || item.tools?.length || 0,
1667
+ createdAt: item.createdAt || item.created_at,
1668
+ // Build the MCP URL for this config
1669
+ mcpUrl: `https://backend.composio.dev/v3/mcp/${item.id}`,
1670
+ }));
1671
+
1672
+ return json({ configs });
1673
+ } catch (e) {
1674
+ console.error("Composio fetch error:", e);
1675
+ return json({ error: "Failed to connect to Composio" }, 500);
1676
+ }
1677
+ }
1678
+
1679
+ // GET /api/integrations/composio/configs/:id - Get single Composio config details
1680
+ const composioConfigMatch = path.match(/^\/api\/integrations\/composio\/configs\/([^/]+)$/);
1681
+ if (composioConfigMatch && method === "GET") {
1682
+ const configId = composioConfigMatch[1];
1683
+ const apiKey = ProviderKeys.getDecrypted("composio");
1684
+ if (!apiKey) {
1685
+ return json({ error: "Composio API key not configured" }, 401);
1686
+ }
1687
+
1688
+ try {
1689
+ const res = await fetch(`https://backend.composio.dev/api/v3/mcp/${configId}`, {
1690
+ headers: {
1691
+ "x-api-key": apiKey,
1692
+ "Content-Type": "application/json",
1693
+ },
1694
+ });
1695
+
1696
+ if (!res.ok) {
1697
+ return json({ error: "Config not found" }, 404);
1698
+ }
1699
+
1700
+ const data = await res.json();
1701
+ return json({
1702
+ config: {
1703
+ id: data.id,
1704
+ name: data.name || data.id,
1705
+ toolkits: data.toolkits || data.apps || [],
1706
+ tools: data.tools || [],
1707
+ mcpUrl: `https://backend.composio.dev/v3/mcp/${data.id}`,
1708
+ }
1709
+ });
1710
+ } catch (e) {
1711
+ return json({ error: "Failed to fetch config" }, 500);
1712
+ }
1713
+ }
1714
+
854
1715
  // POST /api/mcp/servers - Create/install a new MCP server
855
1716
  if (path === "/api/mcp/servers" && method === "POST") {
856
1717
  try {
@@ -1143,9 +2004,9 @@ export async function handleApiRequest(req: Request, path: string): Promise<Resp
1143
2004
  return new Response(stream, {
1144
2005
  headers: {
1145
2006
  "Content-Type": "text/event-stream",
1146
- "Cache-Control": "no-cache",
2007
+ "Cache-Control": "no-cache, no-transform",
1147
2008
  "Connection": "keep-alive",
1148
- "Access-Control-Allow-Origin": "*",
2009
+ "X-Accel-Buffering": "no",
1149
2010
  },
1150
2011
  });
1151
2012
  }
@@ -1153,8 +2014,10 @@ export async function handleApiRequest(req: Request, path: string): Promise<Resp
1153
2014
  // GET /api/telemetry/events - Query telemetry events
1154
2015
  if (path === "/api/telemetry/events" && method === "GET") {
1155
2016
  const url = new URL(req.url);
2017
+ const projectIdParam = url.searchParams.get("project_id");
1156
2018
  const events = TelemetryDB.query({
1157
2019
  agent_id: url.searchParams.get("agent_id") || undefined,
2020
+ project_id: projectIdParam === "null" ? null : projectIdParam || undefined,
1158
2021
  category: url.searchParams.get("category") || undefined,
1159
2022
  level: url.searchParams.get("level") || undefined,
1160
2023
  trace_id: url.searchParams.get("trace_id") || undefined,
@@ -1169,8 +2032,10 @@ export async function handleApiRequest(req: Request, path: string): Promise<Resp
1169
2032
  // GET /api/telemetry/usage - Get usage statistics
1170
2033
  if (path === "/api/telemetry/usage" && method === "GET") {
1171
2034
  const url = new URL(req.url);
2035
+ const projectIdParam = url.searchParams.get("project_id");
1172
2036
  const usage = TelemetryDB.getUsage({
1173
2037
  agent_id: url.searchParams.get("agent_id") || undefined,
2038
+ project_id: projectIdParam === "null" ? null : projectIdParam || undefined,
1174
2039
  since: url.searchParams.get("since") || undefined,
1175
2040
  until: url.searchParams.get("until") || undefined,
1176
2041
  group_by: (url.searchParams.get("group_by") as "agent" | "day") || undefined,
@@ -1182,9 +2047,19 @@ export async function handleApiRequest(req: Request, path: string): Promise<Resp
1182
2047
  if (path === "/api/telemetry/stats" && method === "GET") {
1183
2048
  const url = new URL(req.url);
1184
2049
  const agentId = url.searchParams.get("agent_id") || undefined;
1185
- const stats = TelemetryDB.getStats(agentId);
2050
+ const projectIdParam = url.searchParams.get("project_id");
2051
+ const stats = TelemetryDB.getStats({
2052
+ agentId,
2053
+ projectId: projectIdParam === "null" ? null : projectIdParam || undefined,
2054
+ });
1186
2055
  return json({ stats });
1187
2056
  }
1188
2057
 
2058
+ // POST /api/telemetry/clear - Clear all telemetry data
2059
+ if (path === "/api/telemetry/clear" && method === "POST") {
2060
+ const deleted = TelemetryDB.deleteOlderThan(0); // Delete all
2061
+ return json({ deleted });
2062
+ }
2063
+
1189
2064
  return json({ error: "Not found" }, 404);
1190
2065
  }