apteva 0.2.7 → 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 (40) 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 +561 -32
  9. package/src/routes/api.ts +901 -35
  10. package/src/routes/auth.ts +242 -0
  11. package/src/server.ts +46 -5
  12. package/src/web/App.tsx +61 -19
  13. package/src/web/components/agents/AgentCard.tsx +24 -22
  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 +70 -22
  23. package/src/web/components/index.ts +3 -0
  24. package/src/web/components/layout/Header.tsx +135 -18
  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 +320 -21
  29. package/src/web/components/tasks/TasksPage.tsx +21 -19
  30. package/src/web/components/telemetry/TelemetryPage.tsx +163 -61
  31. package/src/web/context/AuthContext.tsx +230 -0
  32. package/src/web/context/ProjectContext.tsx +182 -0
  33. package/src/web/context/index.ts +5 -0
  34. package/src/web/hooks/useAgents.ts +18 -6
  35. package/src/web/hooks/useOnboarding.ts +20 -4
  36. package/src/web/hooks/useProviders.ts +15 -5
  37. package/src/web/icon.png +0 -0
  38. package/src/web/styles.css +12 -0
  39. package/src/web/types.ts +6 -0
  40. 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";
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,
@@ -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,22 +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);
426
481
  }
427
482
 
428
483
  // Delete agent's telemetry data
429
- TelemetryDB.deleteByAgent(agentMatch[1]);
484
+ TelemetryDB.deleteByAgent(agentId);
430
485
 
431
- AgentDB.delete(agentMatch[1]);
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
+ }
495
+ }
496
+
497
+ AgentDB.delete(agentId);
432
498
  return json({ success: true });
433
499
  }
434
500
 
@@ -514,6 +580,432 @@ export async function handleApiRequest(req: Request, path: string): Promise<Resp
514
580
  }
515
581
  }
516
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
+
517
1009
  // GET /api/providers - List supported providers and models with key status
518
1010
  if (path === "/api/providers" && method === "GET") {
519
1011
  const providers = getProvidersWithStatus();
@@ -539,6 +1031,274 @@ export async function handleApiRequest(req: Request, path: string): Promise<Resp
539
1031
  return json({ success: true });
540
1032
  }
541
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
+
542
1302
  // ==================== API KEYS ====================
543
1303
 
544
1304
  // GET /api/keys - List all configured provider keys (without actual keys)
@@ -833,20 +1593,39 @@ export async function handleApiRequest(req: Request, path: string): Promise<Resp
833
1593
  }
834
1594
  const data = await res.json();
835
1595
 
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
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
+ });
850
1629
 
851
1630
  return json({ servers });
852
1631
  } catch (e) {
@@ -854,6 +1633,85 @@ export async function handleApiRequest(req: Request, path: string): Promise<Resp
854
1633
  }
855
1634
  }
856
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
+
857
1715
  // POST /api/mcp/servers - Create/install a new MCP server
858
1716
  if (path === "/api/mcp/servers" && method === "POST") {
859
1717
  try {
@@ -1156,8 +2014,10 @@ export async function handleApiRequest(req: Request, path: string): Promise<Resp
1156
2014
  // GET /api/telemetry/events - Query telemetry events
1157
2015
  if (path === "/api/telemetry/events" && method === "GET") {
1158
2016
  const url = new URL(req.url);
2017
+ const projectIdParam = url.searchParams.get("project_id");
1159
2018
  const events = TelemetryDB.query({
1160
2019
  agent_id: url.searchParams.get("agent_id") || undefined,
2020
+ project_id: projectIdParam === "null" ? null : projectIdParam || undefined,
1161
2021
  category: url.searchParams.get("category") || undefined,
1162
2022
  level: url.searchParams.get("level") || undefined,
1163
2023
  trace_id: url.searchParams.get("trace_id") || undefined,
@@ -1172,8 +2032,10 @@ export async function handleApiRequest(req: Request, path: string): Promise<Resp
1172
2032
  // GET /api/telemetry/usage - Get usage statistics
1173
2033
  if (path === "/api/telemetry/usage" && method === "GET") {
1174
2034
  const url = new URL(req.url);
2035
+ const projectIdParam = url.searchParams.get("project_id");
1175
2036
  const usage = TelemetryDB.getUsage({
1176
2037
  agent_id: url.searchParams.get("agent_id") || undefined,
2038
+ project_id: projectIdParam === "null" ? null : projectIdParam || undefined,
1177
2039
  since: url.searchParams.get("since") || undefined,
1178
2040
  until: url.searchParams.get("until") || undefined,
1179
2041
  group_by: (url.searchParams.get("group_by") as "agent" | "day") || undefined,
@@ -1185,7 +2047,11 @@ export async function handleApiRequest(req: Request, path: string): Promise<Resp
1185
2047
  if (path === "/api/telemetry/stats" && method === "GET") {
1186
2048
  const url = new URL(req.url);
1187
2049
  const agentId = url.searchParams.get("agent_id") || undefined;
1188
- 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
+ });
1189
2055
  return json({ stats });
1190
2056
  }
1191
2057