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.
- 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 +570 -32
- package/src/routes/api.ts +913 -38
- package/src/routes/auth.ts +242 -0
- package/src/server.ts +60 -8
- package/src/web/App.tsx +61 -19
- package/src/web/components/agents/AgentCard.tsx +30 -41
- 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 +91 -31
- package/src/web/components/index.ts +3 -0
- package/src/web/components/layout/Header.tsx +145 -15
- 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 +404 -18
- package/src/web/components/tasks/TasksPage.tsx +21 -19
- package/src/web/components/telemetry/TelemetryPage.tsx +271 -81
- package/src/web/context/AuthContext.tsx +230 -0
- package/src/web/context/ProjectContext.tsx +182 -0
- package/src/web/context/TelemetryContext.tsx +98 -76
- 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.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
|
-
//
|
|
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,
|
|
@@ -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:
|
|
165
|
-
flush_interval:
|
|
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
|
-
|
|
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
|
|
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);
|
|
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(
|
|
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
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
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
|
-
"
|
|
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
|
|
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
|
}
|