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