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.
- package/dist/App.hzbfeg94.js +217 -0
- package/dist/index.html +3 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/auth/index.ts +386 -0
- package/src/auth/middleware.ts +183 -0
- package/src/binary.ts +19 -1
- package/src/db.ts +561 -32
- package/src/routes/api.ts +901 -35
- package/src/routes/auth.ts +242 -0
- package/src/server.ts +46 -5
- package/src/web/App.tsx +61 -19
- package/src/web/components/agents/AgentCard.tsx +24 -22
- package/src/web/components/agents/AgentPanel.tsx +751 -11
- package/src/web/components/agents/AgentsView.tsx +81 -9
- package/src/web/components/agents/CreateAgentModal.tsx +28 -1
- package/src/web/components/auth/CreateAccountStep.tsx +176 -0
- package/src/web/components/auth/LoginPage.tsx +91 -0
- package/src/web/components/auth/index.ts +2 -0
- package/src/web/components/common/Icons.tsx +48 -0
- package/src/web/components/common/Modal.tsx +1 -1
- package/src/web/components/dashboard/Dashboard.tsx +70 -22
- package/src/web/components/index.ts +3 -0
- package/src/web/components/layout/Header.tsx +135 -18
- package/src/web/components/layout/Sidebar.tsx +81 -43
- package/src/web/components/mcp/McpPage.tsx +261 -32
- package/src/web/components/onboarding/OnboardingWizard.tsx +64 -8
- package/src/web/components/settings/SettingsPage.tsx +320 -21
- package/src/web/components/tasks/TasksPage.tsx +21 -19
- package/src/web/components/telemetry/TelemetryPage.tsx +163 -61
- package/src/web/context/AuthContext.tsx +230 -0
- package/src/web/context/ProjectContext.tsx +182 -0
- package/src/web/context/index.ts +5 -0
- package/src/web/hooks/useAgents.ts +18 -6
- package/src/web/hooks/useOnboarding.ts +20 -4
- package/src/web/hooks/useProviders.ts +15 -5
- package/src/web/icon.png +0 -0
- package/src/web/styles.css +12 -0
- package/src/web/types.ts +6 -0
- 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
|
-
//
|
|
61
|
-
const mcpServers
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
477
|
+
const proc = agentProcesses.get(agentId);
|
|
423
478
|
if (proc) {
|
|
424
479
|
proc.kill();
|
|
425
|
-
agentProcesses.delete(
|
|
480
|
+
agentProcesses.delete(agentId);
|
|
426
481
|
}
|
|
427
482
|
|
|
428
483
|
// Delete agent's telemetry data
|
|
429
|
-
TelemetryDB.deleteByAgent(
|
|
484
|
+
TelemetryDB.deleteByAgent(agentId);
|
|
430
485
|
|
|
431
|
-
|
|
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
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
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
|
|
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
|
|