apteva 0.4.4 → 0.4.6

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/src/routes/api.ts CHANGED
@@ -1,3482 +1,37 @@
1
- import { spawn } from "bun";
2
- import { join } from "path";
3
- import { homedir } from "os";
4
- import { mkdirSync, existsSync, rmSync } from "fs";
5
- import { agentProcesses, agentsStarting, getBinaryPathForAgent, getNextPort, getBinaryStatus, BIN_DIR, telemetryBroadcaster, type TelemetryEvent } from "../server";
6
- import { AgentDB, McpServerDB, TelemetryDB, UserDB, ProjectDB, SkillDB, generateId, getMultiAgentConfig, type Agent, type AgentFeatures, type McpServer, type Project, type Skill } from "../db";
7
- import { ProviderKeys, Onboarding, getProvidersWithStatus, PROVIDERS, type ProviderId } from "../providers";
8
- import { createUser, hashPassword, validatePassword } from "../auth";
9
1
  import type { AuthContext } from "../auth/middleware";
10
- import {
11
- binaryExists,
12
- checkForUpdates,
13
- getInstalledVersion,
14
- getAptevaVersion,
15
- downloadLatestBinary,
16
- installViaNpm,
17
- } from "../binary";
18
- import {
19
- startMcpProcess,
20
- stopMcpProcess,
21
- initializeMcpServer,
22
- listMcpTools,
23
- callMcpTool,
24
- getMcpProcess,
25
- getMcpProxyUrl,
26
- getHttpMcpClient,
27
- } from "../mcp-client";
28
- import { openApiSpec } from "../openapi";
29
- import { getProvider, getProviderIds, registerProvider } from "../integrations";
30
- import { ComposioProvider } from "../integrations/composio";
31
- import { SkillsmpProvider, parseSkillMd, type SkillsmpSkill } from "../integrations/skillsmp";
32
-
33
- // Register integration providers
34
- registerProvider(ComposioProvider);
35
-
36
- // Data directory for agent instances (in ~/.apteva/agents/)
37
- const AGENTS_DATA_DIR = process.env.DATA_DIR
38
- ? join(process.env.DATA_DIR, "agents")
39
- : join(homedir(), ".apteva", "agents");
40
-
41
- // Meta Agent configuration
42
- const META_AGENT_ENABLED = process.env.META_AGENT_ENABLED === "true";
43
- const META_AGENT_ID = "apteva-assistant";
44
-
45
- function json(data: unknown, status = 200): Response {
46
- return new Response(JSON.stringify(data), {
47
- status,
48
- headers: { "Content-Type": "application/json" },
49
- });
50
- }
51
-
52
- const isDev = process.env.NODE_ENV !== "production";
53
- function debug(...args: unknown[]) {
54
- if (isDev) console.log("[api]", ...args);
55
- }
56
-
57
- // Wait for agent to be healthy (with timeout)
58
- // Note: /health endpoint is whitelisted in agent, no auth needed
59
- async function waitForAgentHealth(port: number, maxAttempts = 30, delayMs = 200): Promise<boolean> {
60
- for (let i = 0; i < maxAttempts; i++) {
61
- try {
62
- const res = await fetch(`http://localhost:${port}/health`, {
63
- signal: AbortSignal.timeout(1000),
64
- });
65
- if (res.ok) return true;
66
- } catch {
67
- // Not ready yet
68
- }
69
- await new Promise(r => setTimeout(r, delayMs));
70
- }
71
- return false;
72
- }
73
-
74
- // Check if a port is free by trying to connect
75
- async function checkPortFree(port: number): Promise<boolean> {
76
- return new Promise((resolve) => {
77
- const net = require("net");
78
- const server = net.createServer();
79
- server.once("error", () => {
80
- resolve(false); // Port in use
81
- });
82
- server.once("listening", () => {
83
- server.close();
84
- resolve(true); // Port is free
85
- });
86
- server.listen(port, "127.0.0.1");
87
- });
88
- }
89
-
90
- // Make authenticated request to agent
91
- async function agentFetch(
92
- agentId: string,
93
- port: number,
94
- endpoint: string,
95
- options: RequestInit = {}
2
+ import { json } from "./api/helpers";
3
+ import { handleSystemRoutes } from "./api/system";
4
+ import { handleProviderRoutes } from "./api/providers";
5
+ import { handleUserRoutes } from "./api/users";
6
+ import { handleProjectRoutes } from "./api/projects";
7
+ import { handleAgentRoutes } from "./api/agents";
8
+ import { handleMcpRoutes } from "./api/mcp";
9
+ import { handleSkillRoutes } from "./api/skills";
10
+ import { handleIntegrationRoutes } from "./api/integrations";
11
+ import { handleMetaAgentRoutes } from "./api/meta-agent";
12
+ import { handleTelemetryRoutes } from "./api/telemetry";
13
+
14
+ // Re-export for backward compatibility (server.ts dynamic import)
15
+ export { startAgentProcess } from "./api/agent-utils";
16
+
17
+ export async function handleApiRequest(
18
+ req: Request,
19
+ path: string,
20
+ authContext?: AuthContext,
96
21
  ): Promise<Response> {
97
- const apiKey = AgentDB.getApiKey(agentId);
98
- const headers: Record<string, string> = {
99
- ...(options.headers as Record<string, string> || {}),
100
- };
101
- if (apiKey) {
102
- headers["X-API-Key"] = apiKey;
103
- }
104
- return fetch(`http://localhost:${port}${endpoint}`, {
105
- ...options,
106
- headers,
107
- });
108
- }
109
-
110
- // Build agent config from apteva agent data
111
- // Note: POST /config expects flat structure WITHOUT "agent" wrapper
112
- function buildAgentConfig(agent: Agent, providerKey: string) {
113
- const features = agent.features;
114
-
115
- // Get MCP server details for the agent's selected servers
116
- const mcpServers: Array<{ name: string; type: "http"; url: string; headers: Record<string, string>; enabled: boolean }> = [];
117
-
118
- // Get skill definitions for the agent's selected skills
119
- const skillDefinitions: Array<{
120
- name: string;
121
- description: string;
122
- instructions: string;
123
- icon: string;
124
- category: string;
125
- tags: string[];
126
- tools: string[];
127
- enabled: boolean;
128
- }> = [];
129
-
130
- for (const skillId of agent.skills || []) {
131
- const skill = SkillDB.findById(skillId);
132
- if (!skill || !skill.enabled) continue;
133
-
134
- skillDefinitions.push({
135
- name: skill.name,
136
- description: skill.description,
137
- instructions: skill.content,
138
- icon: "",
139
- category: "",
140
- tags: [],
141
- tools: skill.allowed_tools || [],
142
- enabled: true,
143
- });
144
- }
145
-
146
- for (const id of agent.mcp_servers || []) {
147
- const server = McpServerDB.findById(id);
148
- if (!server) continue;
149
-
150
- if (server.type === "http" && server.url) {
151
- // Remote HTTP server (Composio, Smithery, or custom)
152
- mcpServers.push({
153
- name: server.name,
154
- type: "http",
155
- url: server.url,
156
- headers: server.headers || {},
157
- enabled: true,
158
- });
159
- } else if (server.status === "running" && server.port) {
160
- // Local MCP server (npm, github, custom)
161
- mcpServers.push({
162
- name: server.name,
163
- type: "http",
164
- url: `http://localhost:${server.port}/mcp`,
165
- headers: {},
166
- enabled: true,
167
- });
168
- }
169
- }
170
-
171
- return {
172
- id: agent.id,
173
- name: agent.name,
174
- description: agent.system_prompt,
175
- public_url: `http://localhost:${agent.port}`,
176
- llm: {
177
- provider: agent.provider,
178
- model: agent.model,
179
- max_tokens: 4000,
180
- temperature: 0.7,
181
- system_prompt: agent.system_prompt,
182
- vision: {
183
- enabled: features.vision,
184
- max_images: 20,
185
- max_image_size: 5242880,
186
- allowed_types: ["jpeg", "png", "gif", "webp"],
187
- resize_images: true,
188
- max_dimension: 1568,
189
- pdf: {
190
- enabled: features.vision,
191
- max_file_size: 33554432,
192
- max_pages: 100,
193
- allow_urls: true,
194
- },
195
- },
196
- parallel_tools: {
197
- enabled: true,
198
- max_concurrent: 10,
199
- },
200
- tools: [], // Clear any old tool whitelist - agent uses all registered tools
201
- builtin_tools: [
202
- ...(features.builtinTools?.webSearch ? [{ type: "web_search_20250305", name: "web_search" }] : []),
203
- ...(features.builtinTools?.webFetch ? [{ type: "web_fetch_20250910", name: "web_fetch" }] : []),
204
- ],
205
- },
206
- tasks: {
207
- enabled: features.tasks,
208
- allow_scheduling: true,
209
- allow_recurring: true,
210
- max_tasks: 100,
211
- auto_execute: false,
212
- },
213
- scheduler: {
214
- enabled: features.tasks,
215
- interval: "1m",
216
- max_tasks: 100,
217
- },
218
- memory: {
219
- enabled: features.memory,
220
- embedding_model: "text-embedding-3-small",
221
- decision_model: "gpt-4o-mini",
222
- max_memories_per_query: 20,
223
- min_importance: 0.3,
224
- min_similarity: 0.3,
225
- auto_prune: true,
226
- max_memories: 10000,
227
- embedding_provider: "openai",
228
- auto_extract_memories: features.memory ? true : null,
229
- auto_ingest_files: true,
230
- },
231
- operator: {
232
- enabled: features.operator,
233
- virtual_browser: "http://localhost:8098",
234
- display_width: 1024,
235
- display_height: 768,
236
- max_actions_per_turn: 5,
237
- },
238
- mcp: {
239
- enabled: features.mcp,
240
- base_url: "http://localhost:3000/mcp",
241
- timeout: "30s",
242
- retry_count: 3,
243
- cache_ttl: "15m",
244
- servers: mcpServers,
245
- },
246
- realtime: {
247
- enabled: features.realtime,
248
- provider: "openai",
249
- model: "gpt-4o-realtime-preview",
250
- voice: "alloy",
251
- },
252
- context: {
253
- max_messages: 30,
254
- max_tokens: 0,
255
- keep_images: 5,
256
- },
257
- filesystem: {
258
- enabled: true,
259
- max_file_size: 10485760,
260
- max_total_size: 104857600,
261
- auto_extract: true,
262
- auto_cleanup: true,
263
- retention_days: 7,
264
- },
265
- telemetry: {
266
- enabled: true,
267
- endpoint: `http://localhost:${process.env.PORT || 4280}/api/telemetry`,
268
- batch_size: 1,
269
- flush_interval: 1, // Every 1 second
270
- categories: [], // Empty = all categories
271
- },
272
- skills: {
273
- enabled: skillDefinitions.length > 0,
274
- definitions: skillDefinitions,
275
- },
276
- agents: (() => {
277
- const multiAgentConfig = getMultiAgentConfig(features, agent.project_id);
278
- const baseUrl = process.env.PUBLIC_URL || `http://localhost:${process.env.PORT || 4280}`;
279
- return {
280
- enabled: multiAgentConfig.enabled,
281
- mode: multiAgentConfig.mode || "worker",
282
- group: multiAgentConfig.group || agent.project_id || undefined,
283
- // This agent's reachable URL for peer communication
284
- url: `http://localhost:${agent.port}`,
285
- // Discovery endpoint to find peer agents in the same group
286
- discovery_url: `${baseUrl}/api/discovery/agents`,
287
- };
288
- })(),
289
- };
290
- }
291
-
292
- // Push config to running agent
293
- // Push config to running agent (with authentication)
294
- async function pushConfigToAgent(agentId: string, port: number, config: any): Promise<{ success: boolean; error?: string }> {
295
- try {
296
- const res = await agentFetch(agentId, port, "/config", {
297
- method: "POST",
298
- headers: { "Content-Type": "application/json" },
299
- body: JSON.stringify(config),
300
- signal: AbortSignal.timeout(5000),
301
- });
302
- if (res.ok) {
303
- return { success: true };
304
- }
305
- const data = await res.json().catch(() => ({}));
306
- return { success: false, error: data.error || `HTTP ${res.status}` };
307
- } catch (err) {
308
- return { success: false, error: String(err) };
309
- }
310
- }
311
-
312
- // Push skills to running agent via /skills endpoint (not config)
313
- async function pushSkillsToAgent(agentId: string, port: number, skills: Array<{
314
- name: string;
315
- description: string;
316
- instructions: string;
317
- icon?: string;
318
- category?: string;
319
- tags?: string[];
320
- tools?: string[];
321
- enabled: boolean;
322
- }>): Promise<{ success: boolean; error?: string }> {
323
- if (skills.length === 0) {
324
- return { success: true };
325
- }
326
-
327
- try {
328
- // Push each skill - try PUT first (update), then POST (create) if not found
329
- for (const skill of skills) {
330
- // First try PUT to update existing skill
331
- let res = await agentFetch(agentId, port, "/skills", {
332
- method: "PUT",
333
- headers: { "Content-Type": "application/json" },
334
- body: JSON.stringify(skill),
335
- signal: AbortSignal.timeout(5000),
336
- });
337
-
338
- // If skill doesn't exist (404), create it with POST
339
- if (res.status === 404) {
340
- res = await agentFetch(agentId, port, "/skills", {
341
- method: "POST",
342
- headers: { "Content-Type": "application/json" },
343
- body: JSON.stringify(skill),
344
- signal: AbortSignal.timeout(5000),
345
- });
346
- }
347
-
348
- if (!res.ok) {
349
- const data = await res.json().catch(() => ({}));
350
- console.error(`[pushSkillsToAgent] Failed to push skill ${skill.name}:`, data.error || res.status);
351
- }
352
- }
353
-
354
- // Enable skills globally via POST /skills/status
355
- const statusRes = await agentFetch(agentId, port, "/skills/status", {
356
- method: "POST",
357
- headers: { "Content-Type": "application/json" },
358
- body: JSON.stringify({ enabled: true }),
359
- signal: AbortSignal.timeout(5000),
360
- });
361
-
362
- if (!statusRes.ok) {
363
- const data = await statusRes.json().catch(() => ({}));
364
- return { success: false, error: data.error || `HTTP ${statusRes.status}` };
365
- }
366
-
367
- console.log(`[pushSkillsToAgent] Pushed ${skills.length} skill(s) to agent`);
368
- return { success: true };
369
- } catch (err) {
370
- return { success: false, error: String(err) };
371
- }
372
- }
373
-
374
- // Exported helper to start an agent process (used by API route and auto-restart)
375
- export async function startAgentProcess(
376
- agent: Agent,
377
- options: { silent?: boolean; cleanData?: boolean } = {}
378
- ): Promise<{ success: boolean; port?: number; error?: string }> {
379
- const { silent = false, cleanData = false } = options;
380
-
381
- // Check if binary exists
382
- if (!binaryExists(BIN_DIR)) {
383
- return { success: false, error: "Agent binary not available" };
384
- }
385
-
386
- // Check if already running (process map)
387
- if (agentProcesses.has(agent.id)) {
388
- return { success: false, error: "Agent already running" };
389
- }
390
-
391
- // Check if already being started (race condition prevention)
392
- if (agentsStarting.has(agent.id)) {
393
- return { success: false, error: "Agent is already starting" };
394
- }
395
-
396
- // Mark as starting
397
- agentsStarting.add(agent.id);
398
-
399
- // Get the API key for the agent's provider
400
- const providerKey = ProviderKeys.getDecrypted(agent.provider);
401
- if (!providerKey) {
402
- agentsStarting.delete(agent.id);
403
- return { success: false, error: `No API key for provider: ${agent.provider}` };
404
- }
405
-
406
- // Get provider config for env var name
407
- const providerConfig = PROVIDERS[agent.provider as ProviderId];
408
- if (!providerConfig) {
409
- agentsStarting.delete(agent.id);
410
- return { success: false, error: `Unknown provider: ${agent.provider}` };
411
- }
412
-
413
- // Use agent's permanently assigned port
414
- const port = agent.port;
415
- if (!port) {
416
- agentsStarting.delete(agent.id);
417
- return { success: false, error: "Agent has no assigned port" };
418
- }
419
-
420
- // Get or create API key for the agent
421
- const agentApiKey = AgentDB.ensureApiKey(agent.id);
422
- if (!agentApiKey) {
423
- agentsStarting.delete(agent.id);
424
- return { success: false, error: "Failed to get/create agent API key" };
425
- }
426
-
427
- try {
428
- // Check if something is already running on this port (orphaned process)
429
- try {
430
- const res = await fetch(`http://localhost:${port}/health`, { signal: AbortSignal.timeout(500) });
431
- if (res.ok) {
432
- // Something is running - try to shut it down
433
- if (!silent) {
434
- console.log(` Port ${port} in use, stopping orphaned process...`);
435
- }
436
- try {
437
- await fetch(`http://localhost:${port}/shutdown`, { method: "POST", signal: AbortSignal.timeout(1000) });
438
- } catch {
439
- // Shutdown failed - process might not support it
440
- }
441
- // Wait longer for port to be released
442
- await new Promise(r => setTimeout(r, 1500));
443
- }
444
- } catch {
445
- // No HTTP response - but port might still be bound by zombie process
446
- }
447
-
448
- // Double-check port is actually free by trying to connect
449
- const isPortFree = await checkPortFree(port);
450
- if (!isPortFree) {
451
- if (!silent) {
452
- console.log(` Port ${port} still in use, trying to kill process...`);
453
- }
454
- // Try to kill process using the port (Linux/Mac)
455
- try {
456
- const { execSync } = await import("child_process");
457
- execSync(`lsof -ti:${port} | xargs kill -9 2>/dev/null || true`, { stdio: "ignore" });
458
- await new Promise(r => setTimeout(r, 1000));
459
- } catch {
460
- // Ignore errors
461
- }
462
-
463
- // Final check
464
- const stillInUse = !(await checkPortFree(port));
465
- if (stillInUse) {
466
- agentsStarting.delete(agent.id);
467
- return { success: false, error: `Port ${port} is still in use` };
468
- }
469
- }
470
-
471
- // Handle data directory
472
- const agentDataDir = join(AGENTS_DATA_DIR, agent.id);
473
- if (cleanData && existsSync(agentDataDir)) {
474
- // Clean old data if requested
475
- rmSync(agentDataDir, { recursive: true, force: true });
476
- if (!silent) {
477
- console.log(` Cleaned old data directory`);
478
- }
479
- }
480
- if (!existsSync(agentDataDir)) {
481
- mkdirSync(agentDataDir, { recursive: true });
482
- }
483
-
484
- if (!silent) {
485
- console.log(`Starting agent ${agent.name} on port ${port}...`);
486
- console.log(` Provider: ${agent.provider}`);
487
- console.log(` Data dir: ${agentDataDir}`);
488
- }
489
-
490
- // Build environment with provider key and agent API key
491
- // CONFIG_PATH ensures each agent has its own config file (prevents sharing)
492
- const agentConfigPath = join(agentDataDir, "agent-config.json");
493
- const env: Record<string, string> = {
494
- ...process.env as Record<string, string>,
495
- PORT: String(port),
496
- DATA_DIR: agentDataDir,
497
- CONFIG_PATH: agentConfigPath,
498
- AGENT_API_KEY: agentApiKey,
499
- [providerConfig.envVar]: providerKey,
500
- };
501
-
502
- // If memory is enabled and agent doesn't use OpenAI, also pass OpenAI key for embeddings
503
- if (agent.features.memory && agent.provider !== "openai") {
504
- const openaiKey = ProviderKeys.getDecrypted("openai");
505
- if (openaiKey) {
506
- env.OPENAI_API_KEY = openaiKey;
507
- }
508
- }
509
-
510
- // Get binary path dynamically (allows hot-reload of new binary versions)
511
- const binaryPath = getBinaryPathForAgent();
512
-
513
- const proc = spawn({
514
- cmd: [binaryPath],
515
- env,
516
- stdout: "inherit",
517
- stderr: "inherit",
518
- });
519
-
520
- // Store process with port for tracking
521
- agentProcesses.set(agent.id, { proc, port });
522
-
523
- // Wait for agent to be healthy
524
- if (!silent) {
525
- console.log(` Waiting for agent to be ready...`);
526
- }
527
- const isHealthy = await waitForAgentHealth(port);
528
- if (!isHealthy) {
529
- if (!silent) {
530
- console.error(` Agent failed to start (health check timeout)`);
531
- }
532
- proc.kill();
533
- agentProcesses.delete(agent.id);
534
- agentsStarting.delete(agent.id);
535
- return { success: false, error: "Health check timeout" };
536
- }
537
-
538
- // Push configuration to the agent
539
- if (!silent) {
540
- console.log(` Pushing configuration...`);
541
- }
542
- const config = buildAgentConfig(agent, providerKey);
543
- const configResult = await pushConfigToAgent(agent.id, port, config);
544
- if (!configResult.success) {
545
- if (!silent) {
546
- console.error(` Failed to configure agent: ${configResult.error}`);
547
- }
548
- // Agent is running but not configured - still usable but log warning
549
- } else if (!silent) {
550
- console.log(` Configuration applied successfully`);
551
- }
552
-
553
- // Push skills via /skills endpoint (separate from config)
554
- if (config.skills?.definitions?.length > 0) {
555
- const skillsResult = await pushSkillsToAgent(agent.id, port, config.skills.definitions);
556
- if (!skillsResult.success && !silent) {
557
- console.error(` Failed to push skills: ${skillsResult.error}`);
558
- } else if (!silent) {
559
- console.log(` Skills pushed successfully (${config.skills.definitions.length} skills)`);
560
- }
561
- }
562
-
563
- // Update status in database (port is already set, just update status)
564
- AgentDB.setStatus(agent.id, "running");
565
-
566
- if (!silent) {
567
- console.log(`Agent ${agent.name} started on port ${port} (pid: ${proc.pid})`);
568
- }
569
-
570
- agentsStarting.delete(agent.id);
571
- return { success: true, port };
572
- } catch (err) {
573
- agentsStarting.delete(agent.id);
574
- if (!silent) {
575
- console.error(`Failed to start agent: ${err}`);
576
- }
577
- return { success: false, error: String(err) };
578
- }
579
- }
580
-
581
- // Transform DB agent to API response format (camelCase for frontend compatibility)
582
- function toApiAgent(agent: Agent) {
583
- // Look up MCP server details
584
- const mcpServerDetails = (agent.mcp_servers || [])
585
- .map(id => McpServerDB.findById(id))
586
- .filter((s): s is NonNullable<typeof s> => s !== null)
587
- .map(s => ({
588
- id: s.id,
589
- name: s.name,
590
- type: s.type,
591
- status: s.status,
592
- port: s.port,
593
- url: s.url, // Include URL for HTTP servers
594
- }));
595
-
596
- // Look up skill details
597
- const skillDetails = (agent.skills || [])
598
- .map(id => SkillDB.findById(id))
599
- .filter((s): s is NonNullable<typeof s> => s !== null)
600
- .map(s => ({
601
- id: s.id,
602
- name: s.name,
603
- description: s.description,
604
- version: s.version,
605
- enabled: s.enabled,
606
- }));
607
-
608
- return {
609
- id: agent.id,
610
- name: agent.name,
611
- model: agent.model,
612
- provider: agent.provider,
613
- systemPrompt: agent.system_prompt,
614
- status: agent.status,
615
- port: agent.port,
616
- features: agent.features,
617
- mcpServers: agent.mcp_servers, // Keep IDs for backwards compatibility
618
- mcpServerDetails, // Include full details
619
- skills: agent.skills, // Skill IDs
620
- skillDetails, // Include full details
621
- projectId: agent.project_id,
622
- createdAt: agent.created_at,
623
- updatedAt: agent.updated_at,
624
- };
625
- }
626
-
627
- // Transform DB project to API response format
628
- function toApiProject(project: Project) {
629
- return {
630
- id: project.id,
631
- name: project.name,
632
- description: project.description,
633
- color: project.color,
634
- createdAt: project.created_at,
635
- updatedAt: project.updated_at,
636
- };
637
- }
638
-
639
- export async function handleApiRequest(req: Request, path: string, authContext?: AuthContext): Promise<Response> {
640
22
  const method = req.method;
641
- const user = authContext?.user;
642
-
643
- // GET /api/health - Health check endpoint (no auth required, handled before middleware in server.ts)
644
- if (path === "/api/health" && method === "GET") {
645
- const agentCount = AgentDB.count();
646
- const runningAgents = AgentDB.findRunning().length;
647
- return json({
648
- status: "ok",
649
- version: getAptevaVersion(),
650
- agents: { total: agentCount, running: runningAgents },
651
- });
652
- }
653
-
654
- // GET /api/features - Feature flags (no auth required)
655
- if (path === "/api/features" && method === "GET") {
656
- return json({
657
- projects: process.env.PROJECTS_ENABLED === "true",
658
- metaAgent: process.env.META_AGENT_ENABLED === "true",
659
- });
660
- }
661
-
662
- // GET /api/openapi - OpenAPI spec (no auth required)
663
- if (path === "/api/openapi" && method === "GET") {
664
- return json(openApiSpec);
665
- }
666
-
667
- // GET /api/agents - List all agents (excludes meta agent)
668
- if (path === "/api/agents" && method === "GET") {
669
- const agents = AgentDB.findAll().filter(a => a.id !== META_AGENT_ID);
670
- return json({ agents: agents.map(toApiAgent) });
671
- }
672
-
673
- // POST /api/agents - Create a new agent
674
- if (path === "/api/agents" && method === "POST") {
675
- try {
676
- const body = await req.json();
677
- const { name, model, provider, systemPrompt, features, projectId } = body;
678
-
679
- if (!name) {
680
- return json({ error: "Name is required" }, 400);
681
- }
682
-
683
- // Import DEFAULT_FEATURES from db.ts
684
- const { DEFAULT_FEATURES } = await import("../db");
685
-
686
- const agent = AgentDB.create({
687
- id: generateId(),
688
- name,
689
- model: model || "claude-sonnet-4-5",
690
- provider: provider || "anthropic",
691
- system_prompt: systemPrompt || "You are a helpful assistant.",
692
- features: features || DEFAULT_FEATURES,
693
- mcp_servers: body.mcpServers || [],
694
- skills: body.skills || [],
695
- project_id: projectId || null,
696
- });
697
-
698
- return json({ agent: toApiAgent(agent) }, 201);
699
- } catch (e) {
700
- console.error("Create agent error:", e);
701
- return json({ error: "Invalid request body" }, 400);
702
- }
703
- }
704
-
705
- // GET /api/agents/:id - Get a specific agent
706
- const agentMatch = path.match(/^\/api\/agents\/([^/]+)$/);
707
- if (agentMatch && method === "GET") {
708
- const agent = AgentDB.findById(agentMatch[1]);
709
- if (!agent) {
710
- return json({ error: "Agent not found" }, 404);
711
- }
712
- return json({ agent: toApiAgent(agent) });
713
- }
714
-
715
- // GET /api/agents/:id/api-key - Get agent API key (dev mode only)
716
- const agentApiKeyMatch = path.match(/^\/api\/agents\/([^/]+)\/api-key$/);
717
- if (agentApiKeyMatch && method === "GET") {
718
- if (!isDev) {
719
- return json({ error: "Only available in development mode" }, 403);
720
- }
721
- const agent = AgentDB.findById(agentApiKeyMatch[1]);
722
- if (!agent) {
723
- return json({ error: "Agent not found" }, 404);
724
- }
725
- const apiKey = AgentDB.getApiKey(agent.id);
726
- return json({ apiKey });
727
- }
728
-
729
- // PUT /api/agents/:id - Update an agent
730
- if (agentMatch && method === "PUT") {
731
- const agent = AgentDB.findById(agentMatch[1]);
732
- if (!agent) {
733
- return json({ error: "Agent not found" }, 404);
734
- }
735
-
736
- try {
737
- const body = await req.json();
738
- const updates: Partial<Agent> = {};
739
-
740
- if (body.name !== undefined) updates.name = body.name;
741
- if (body.model !== undefined) updates.model = body.model;
742
- if (body.provider !== undefined) updates.provider = body.provider;
743
- if (body.systemPrompt !== undefined) updates.system_prompt = body.systemPrompt;
744
- if (body.features !== undefined) updates.features = body.features;
745
- if (body.mcpServers !== undefined) updates.mcp_servers = body.mcpServers;
746
- if (body.skills !== undefined) updates.skills = body.skills;
747
- if (body.projectId !== undefined) updates.project_id = body.projectId;
748
-
749
- const updated = AgentDB.update(agentMatch[1], updates);
750
-
751
- // If agent is running, push the new config and skills
752
- if (updated && updated.status === "running" && updated.port) {
753
- const providerKey = ProviderKeys.getDecrypted(updated.provider);
754
- if (providerKey) {
755
- const config = buildAgentConfig(updated, providerKey);
756
- const configResult = await pushConfigToAgent(updated.id, updated.port, config);
757
- if (!configResult.success) {
758
- console.error(`Failed to push config to running agent: ${configResult.error}`);
759
- }
760
- // Push skills via /skills endpoint
761
- if (config.skills?.definitions?.length > 0) {
762
- const skillsResult = await pushSkillsToAgent(updated.id, updated.port, config.skills.definitions);
763
- if (!skillsResult.success) {
764
- console.error(`Failed to push skills to running agent: ${skillsResult.error}`);
765
- }
766
- }
767
- }
768
- }
769
-
770
- return json({ agent: updated ? toApiAgent(updated) : null });
771
- } catch (e) {
772
- return json({ error: "Invalid request body" }, 400);
773
- }
774
- }
775
-
776
- // DELETE /api/agents/:id - Delete an agent
777
- if (agentMatch && method === "DELETE") {
778
- const agentId = agentMatch[1];
779
- const agent = AgentDB.findById(agentId);
780
- if (!agent) {
781
- return json({ error: "Agent not found" }, 404);
782
- }
783
-
784
- // Stop the agent if running
785
- const agentProc = agentProcesses.get(agentId);
786
- const port = agent.port;
787
-
788
- if (agentProc) {
789
- // Try graceful shutdown first
790
- if (port) {
791
- try {
792
- await fetch(`http://localhost:${port}/shutdown`, {
793
- method: "POST",
794
- signal: AbortSignal.timeout(2000),
795
- });
796
- await new Promise(r => setTimeout(r, 500));
797
- } catch {
798
- // Graceful shutdown failed
799
- }
800
- }
801
-
802
- try {
803
- agentProc.proc.kill();
804
- } catch {
805
- // Already dead
806
- }
807
- agentProcesses.delete(agentId);
808
-
809
- // Ensure port is freed
810
- if (port) {
811
- const isFree = await checkPortFree(port);
812
- if (!isFree) {
813
- try {
814
- const { execSync } = await import("child_process");
815
- execSync(`lsof -ti :${port} | xargs -r kill -9 2>/dev/null || true`, { stdio: "ignore" });
816
- } catch {
817
- // Ignore
818
- }
819
- }
820
- }
821
- }
822
-
823
- // Delete agent's telemetry data
824
- TelemetryDB.deleteByAgent(agentId);
825
-
826
- // Delete agent's data directory (contains threads, messages, etc.)
827
- const agentDataDir = join(AGENTS_DATA_DIR, agentId);
828
- if (existsSync(agentDataDir)) {
829
- try {
830
- rmSync(agentDataDir, { recursive: true, force: true });
831
- console.log(`Deleted agent data directory: ${agentDataDir}`);
832
- } catch (err) {
833
- console.error(`Failed to delete agent data directory: ${err}`);
834
- }
835
- }
836
-
837
- AgentDB.delete(agentId);
838
- return json({ success: true });
839
- }
840
-
841
- // GET /api/agents/:id/api-key - Get the agent's API key (masked)
842
- const apiKeyGetMatch = path.match(/^\/api\/agents\/([^/]+)\/api-key$/);
843
- if (apiKeyGetMatch && method === "GET") {
844
- const agent = AgentDB.findById(apiKeyGetMatch[1]);
845
- if (!agent) {
846
- return json({ error: "Agent not found" }, 404);
847
- }
848
-
849
- const apiKey = AgentDB.getApiKey(agent.id);
850
- if (!apiKey) {
851
- return json({ error: "No API key found for this agent" }, 404);
852
- }
853
-
854
- // Return masked key (show only first 8 chars)
855
- const masked = apiKey.substring(0, 8) + "..." + apiKey.substring(apiKey.length - 4);
856
- return json({
857
- apiKey: masked,
858
- hasKey: true,
859
- });
860
- }
861
-
862
- // POST /api/agents/:id/api-key - Regenerate the agent's API key
863
- if (apiKeyGetMatch && method === "POST") {
864
- const agent = AgentDB.findById(apiKeyGetMatch[1]);
865
- if (!agent) {
866
- return json({ error: "Agent not found" }, 404);
867
- }
868
-
869
- const newKey = AgentDB.regenerateApiKey(agent.id);
870
- if (!newKey) {
871
- return json({ error: "Failed to regenerate API key" }, 500);
872
- }
873
-
874
- // Return the full new key (only time it's fully visible)
875
- return json({
876
- apiKey: newKey,
877
- message: "API key regenerated. This is the only time the full key will be shown.",
878
- });
879
- }
880
-
881
- // POST /api/agents/:id/start - Start an agent
882
- const startMatch = path.match(/^\/api\/agents\/([^/]+)\/start$/);
883
- if (startMatch && method === "POST") {
884
- const agent = AgentDB.findById(startMatch[1]);
885
- if (!agent) {
886
- return json({ error: "Agent not found" }, 404);
887
- }
888
-
889
- const result = await startAgentProcess(agent);
890
- if (!result.success) {
891
- return json({ error: result.error }, 400);
892
- }
893
-
894
- const updated = AgentDB.findById(agent.id);
895
- return json({ agent: updated ? toApiAgent(updated) : null, message: `Agent started on port ${result.port}` });
896
- }
897
-
898
- // POST /api/agents/:id/stop - Stop an agent
899
- const stopMatch = path.match(/^\/api\/agents\/([^/]+)\/stop$/);
900
- if (stopMatch && method === "POST") {
901
- const agent = AgentDB.findById(stopMatch[1]);
902
- if (!agent) {
903
- return json({ error: "Agent not found" }, 404);
904
- }
905
-
906
- const agentProc = agentProcesses.get(agent.id);
907
- const port = agent.port;
908
-
909
- if (agentProc) {
910
- console.log(`Stopping agent ${agent.name} (pid: ${agentProc.proc.pid})...`);
911
-
912
- // Try graceful shutdown first
913
- if (port) {
914
- try {
915
- await fetch(`http://localhost:${port}/shutdown`, {
916
- method: "POST",
917
- signal: AbortSignal.timeout(2000),
918
- });
919
- await new Promise(r => setTimeout(r, 500)); // Wait for graceful shutdown
920
- } catch {
921
- // Graceful shutdown failed or timed out
922
- }
923
- }
924
-
925
- // Force kill if still running
926
- try {
927
- agentProc.proc.kill();
928
- } catch {
929
- // Already dead
930
- }
931
- agentProcesses.delete(agent.id);
932
-
933
- // Ensure port is freed
934
- if (port) {
935
- const isFree = await checkPortFree(port);
936
- if (!isFree) {
937
- // Force kill by port
938
- try {
939
- const { execSync } = await import("child_process");
940
- execSync(`lsof -ti :${port} | xargs -r kill -9 2>/dev/null || true`, { stdio: "ignore" });
941
- } catch {
942
- // Ignore
943
- }
944
- }
945
- }
946
- }
947
-
948
- const updated = AgentDB.setStatus(agent.id, "stopped");
949
- return json({ agent: updated ? toApiAgent(updated) : null, message: "Agent stopped" });
950
- }
951
-
952
- // POST /api/agents/:id/chat - Proxy chat to agent binary with streaming
953
- const chatMatch = path.match(/^\/api\/agents\/([^/]+)\/chat$/);
954
- if (chatMatch && method === "POST") {
955
- const agent = AgentDB.findById(chatMatch[1]);
956
- if (!agent) {
957
- return json({ error: "Agent not found" }, 404);
958
- }
959
-
960
- if (agent.status !== "running" || !agent.port) {
961
- return json({ error: "Agent is not running" }, 400);
962
- }
963
-
964
- try {
965
- const body = await req.json();
966
-
967
- // Proxy to the agent's /chat endpoint with authentication
968
- const response = await agentFetch(agent.id, agent.port, "/chat", {
969
- method: "POST",
970
- headers: { "Content-Type": "application/json" },
971
- body: JSON.stringify(body),
972
- });
973
-
974
- // Stream the response back
975
- if (!response.ok) {
976
- const errorText = await response.text();
977
- return json({ error: `Agent error: ${errorText}` }, response.status);
978
- }
979
-
980
- // Return streaming response with proper headers
981
- return new Response(response.body, {
982
- status: 200,
983
- headers: {
984
- "Content-Type": response.headers.get("Content-Type") || "text/event-stream",
985
- "Cache-Control": "no-cache",
986
- "Connection": "keep-alive",
987
- },
988
- });
989
- } catch (err) {
990
- console.error(`Chat proxy error: ${err}`);
991
- return json({ error: `Failed to proxy chat: ${err}` }, 500);
992
- }
993
- }
994
-
995
- // ==================== THREAD & MESSAGE PROXY ====================
996
-
997
- // GET /api/agents/:id/threads - List threads for an agent
998
- const threadsListMatch = path.match(/^\/api\/agents\/([^/]+)\/threads$/);
999
- if (threadsListMatch && method === "GET") {
1000
- const agent = AgentDB.findById(threadsListMatch[1]);
1001
- if (!agent) {
1002
- return json({ error: "Agent not found" }, 404);
1003
- }
1004
-
1005
- if (agent.status !== "running" || !agent.port) {
1006
- return json({ error: "Agent is not running" }, 400);
1007
- }
1008
-
1009
- try {
1010
- const response = await agentFetch(agent.id, agent.port, "/threads", {
1011
- method: "GET",
1012
- headers: { "Accept": "application/json" },
1013
- });
1014
-
1015
- if (!response.ok) {
1016
- const errorText = await response.text();
1017
- return json({ error: `Agent error: ${errorText}` }, response.status);
1018
- }
1019
-
1020
- const data = await response.json();
1021
- return json(data);
1022
- } catch (err) {
1023
- console.error(`Threads list proxy error: ${err}`);
1024
- return json({ error: `Failed to fetch threads: ${err}` }, 500);
1025
- }
1026
- }
1027
-
1028
- // POST /api/agents/:id/threads - Create a new thread
1029
- if (threadsListMatch && method === "POST") {
1030
- const agent = AgentDB.findById(threadsListMatch[1]);
1031
- if (!agent) {
1032
- return json({ error: "Agent not found" }, 404);
1033
- }
1034
-
1035
- if (agent.status !== "running" || !agent.port) {
1036
- return json({ error: "Agent is not running" }, 400);
1037
- }
1038
-
1039
- try {
1040
- const body = await req.json().catch(() => ({}));
1041
- const response = await agentFetch(agent.id, agent.port, "/threads", {
1042
- method: "POST",
1043
- headers: { "Content-Type": "application/json" },
1044
- body: JSON.stringify(body),
1045
- });
1046
-
1047
- if (!response.ok) {
1048
- const errorText = await response.text();
1049
- return json({ error: `Agent error: ${errorText}` }, response.status);
1050
- }
1051
-
1052
- const data = await response.json();
1053
- return json(data, 201);
1054
- } catch (err) {
1055
- console.error(`Thread create proxy error: ${err}`);
1056
- return json({ error: `Failed to create thread: ${err}` }, 500);
1057
- }
1058
- }
1059
-
1060
- // GET /api/agents/:id/threads/:threadId - Get a specific thread
1061
- const threadDetailMatch = path.match(/^\/api\/agents\/([^/]+)\/threads\/([^/]+)$/);
1062
- if (threadDetailMatch && method === "GET") {
1063
- const agent = AgentDB.findById(threadDetailMatch[1]);
1064
- if (!agent) {
1065
- return json({ error: "Agent not found" }, 404);
1066
- }
1067
-
1068
- if (agent.status !== "running" || !agent.port) {
1069
- return json({ error: "Agent is not running" }, 400);
1070
- }
1071
-
1072
- try {
1073
- const threadId = threadDetailMatch[2];
1074
- const response = await agentFetch(agent.id, agent.port, `/threads/${threadId}`, {
1075
- method: "GET",
1076
- headers: { "Accept": "application/json" },
1077
- });
1078
-
1079
- if (!response.ok) {
1080
- const errorText = await response.text();
1081
- return json({ error: `Agent error: ${errorText}` }, response.status);
1082
- }
1083
-
1084
- const data = await response.json();
1085
- return json(data);
1086
- } catch (err) {
1087
- console.error(`Thread detail proxy error: ${err}`);
1088
- return json({ error: `Failed to fetch thread: ${err}` }, 500);
1089
- }
1090
- }
1091
-
1092
- // DELETE /api/agents/:id/threads/:threadId - Delete a thread
1093
- if (threadDetailMatch && method === "DELETE") {
1094
- const agent = AgentDB.findById(threadDetailMatch[1]);
1095
- if (!agent) {
1096
- return json({ error: "Agent not found" }, 404);
1097
- }
1098
-
1099
- if (agent.status !== "running" || !agent.port) {
1100
- return json({ error: "Agent is not running" }, 400);
1101
- }
1102
-
1103
- try {
1104
- const threadId = threadDetailMatch[2];
1105
- const response = await agentFetch(agent.id, agent.port, `/threads/${threadId}`, {
1106
- method: "DELETE",
1107
- });
1108
-
1109
- if (!response.ok) {
1110
- const errorText = await response.text();
1111
- return json({ error: `Agent error: ${errorText}` }, response.status);
1112
- }
1113
-
1114
- return json({ success: true });
1115
- } catch (err) {
1116
- console.error(`Thread delete proxy error: ${err}`);
1117
- return json({ error: `Failed to delete thread: ${err}` }, 500);
1118
- }
1119
- }
1120
-
1121
- // GET /api/agents/:id/threads/:threadId/messages - Get messages in a thread
1122
- const threadMessagesMatch = path.match(/^\/api\/agents\/([^/]+)\/threads\/([^/]+)\/messages$/);
1123
- if (threadMessagesMatch && method === "GET") {
1124
- const agent = AgentDB.findById(threadMessagesMatch[1]);
1125
- if (!agent) {
1126
- return json({ error: "Agent not found" }, 404);
1127
- }
1128
-
1129
- if (agent.status !== "running" || !agent.port) {
1130
- return json({ error: "Agent is not running" }, 400);
1131
- }
1132
-
1133
- try {
1134
- const threadId = threadMessagesMatch[2];
1135
- const response = await agentFetch(agent.id, agent.port, `/threads/${threadId}/messages`, {
1136
- method: "GET",
1137
- headers: { "Accept": "application/json" },
1138
- });
1139
-
1140
- if (!response.ok) {
1141
- const errorText = await response.text();
1142
- return json({ error: `Agent error: ${errorText}` }, response.status);
1143
- }
1144
-
1145
- const data = await response.json();
1146
- return json(data);
1147
- } catch (err) {
1148
- console.error(`Thread messages proxy error: ${err}`);
1149
- return json({ error: `Failed to fetch messages: ${err}` }, 500);
1150
- }
1151
- }
1152
-
1153
- // ==================== MEMORY PROXY ====================
1154
-
1155
- // GET /api/agents/:id/memories - List memories for an agent
1156
- const memoriesMatch = path.match(/^\/api\/agents\/([^/]+)\/memories$/);
1157
- if (memoriesMatch && method === "GET") {
1158
- const agent = AgentDB.findById(memoriesMatch[1]);
1159
- if (!agent) {
1160
- return json({ error: "Agent not found" }, 404);
1161
- }
1162
-
1163
- if (agent.status !== "running" || !agent.port) {
1164
- return json({ error: "Agent is not running" }, 400);
1165
- }
1166
-
1167
- try {
1168
- const url = new URL(req.url);
1169
- const threadId = url.searchParams.get("thread_id") || "";
1170
- const endpoint = `/memories${threadId ? `?thread_id=${threadId}` : ""}`;
1171
- const response = await agentFetch(agent.id, agent.port, endpoint, {
1172
- method: "GET",
1173
- headers: { "Accept": "application/json" },
1174
- });
1175
-
1176
- if (!response.ok) {
1177
- const errorText = await response.text();
1178
- return json({ error: `Agent error: ${errorText}` }, response.status);
1179
- }
1180
-
1181
- const data = await response.json();
1182
- return json(data);
1183
- } catch (err) {
1184
- console.error(`Memories list proxy error: ${err}`);
1185
- return json({ error: `Failed to fetch memories: ${err}` }, 500);
1186
- }
1187
- }
1188
-
1189
- // DELETE /api/agents/:id/memories - Clear all memories for an agent
1190
- if (memoriesMatch && method === "DELETE") {
1191
- const agent = AgentDB.findById(memoriesMatch[1]);
1192
- if (!agent) {
1193
- return json({ error: "Agent not found" }, 404);
1194
- }
1195
-
1196
- if (agent.status !== "running" || !agent.port) {
1197
- return json({ error: "Agent is not running" }, 400);
1198
- }
1199
-
1200
- try {
1201
- const response = await agentFetch(agent.id, agent.port, "/memories", { method: "DELETE" });
1202
-
1203
- if (!response.ok) {
1204
- const errorText = await response.text();
1205
- return json({ error: `Agent error: ${errorText}` }, response.status);
1206
- }
1207
-
1208
- return json({ success: true });
1209
- } catch (err) {
1210
- console.error(`Memories clear proxy error: ${err}`);
1211
- return json({ error: `Failed to clear memories: ${err}` }, 500);
1212
- }
1213
- }
1214
-
1215
- // DELETE /api/agents/:id/memories/:memoryId - Delete a specific memory
1216
- const memoryDeleteMatch = path.match(/^\/api\/agents\/([^/]+)\/memories\/([^/]+)$/);
1217
- if (memoryDeleteMatch && method === "DELETE") {
1218
- const agent = AgentDB.findById(memoryDeleteMatch[1]);
1219
- if (!agent) {
1220
- return json({ error: "Agent not found" }, 404);
1221
- }
1222
-
1223
- if (agent.status !== "running" || !agent.port) {
1224
- return json({ error: "Agent is not running" }, 400);
1225
- }
1226
-
1227
- try {
1228
- const memoryId = memoryDeleteMatch[2];
1229
- const response = await agentFetch(agent.id, agent.port, `/memories/${memoryId}`, { method: "DELETE" });
1230
-
1231
- if (!response.ok) {
1232
- const errorText = await response.text();
1233
- return json({ error: `Agent error: ${errorText}` }, response.status);
1234
- }
1235
-
1236
- return json({ success: true });
1237
- } catch (err) {
1238
- console.error(`Memory delete proxy error: ${err}`);
1239
- return json({ error: `Failed to delete memory: ${err}` }, 500);
1240
- }
1241
- }
1242
-
1243
- // ==================== FILES PROXY ====================
1244
-
1245
- // POST /api/agents/:id/files - Upload a file
1246
- const filesMatch = path.match(/^\/api\/agents\/([^/]+)\/files$/);
1247
- if (filesMatch && method === "POST") {
1248
- const agent = AgentDB.findById(filesMatch[1]);
1249
- if (!agent) {
1250
- return json({ error: "Agent not found" }, 404);
1251
- }
1252
-
1253
- if (agent.status !== "running" || !agent.port) {
1254
- return json({ error: "Agent is not running" }, 400);
1255
- }
1256
-
1257
- try {
1258
- // Get the raw body and content-type to proxy the multipart upload
1259
- const contentType = req.headers.get("content-type") || "";
1260
- const body = await req.arrayBuffer();
1261
-
1262
- const response = await agentFetch(agent.id, agent.port, "/files", {
1263
- method: "POST",
1264
- headers: {
1265
- "Content-Type": contentType,
1266
- },
1267
- body: body,
1268
- });
1269
-
1270
- if (!response.ok) {
1271
- const errorText = await response.text();
1272
- return json({ error: `Agent error: ${errorText}` }, response.status);
1273
- }
1274
-
1275
- const data = await response.json();
1276
- return json(data);
1277
- } catch (err) {
1278
- console.error(`File upload proxy error: ${err}`);
1279
- return json({ error: `Failed to upload file: ${err}` }, 500);
1280
- }
1281
- }
1282
-
1283
- // GET /api/agents/:id/files - List files for an agent
1284
- if (filesMatch && method === "GET") {
1285
- const agent = AgentDB.findById(filesMatch[1]);
1286
- if (!agent) {
1287
- return json({ error: "Agent not found" }, 404);
1288
- }
1289
-
1290
- if (agent.status !== "running" || !agent.port) {
1291
- return json({ error: "Agent is not running" }, 400);
1292
- }
1293
-
1294
- try {
1295
- const url = new URL(req.url);
1296
- const params = new URLSearchParams();
1297
- if (url.searchParams.get("thread_id")) params.set("thread_id", url.searchParams.get("thread_id")!);
1298
- if (url.searchParams.get("limit")) params.set("limit", url.searchParams.get("limit")!);
1299
-
1300
- const endpoint = `/files${params.toString() ? `?${params}` : ""}`;
1301
- const response = await agentFetch(agent.id, agent.port, endpoint, {
1302
- method: "GET",
1303
- headers: { "Accept": "application/json" },
1304
- });
1305
-
1306
- if (!response.ok) {
1307
- const errorText = await response.text();
1308
- return json({ error: `Agent error: ${errorText}` }, response.status);
1309
- }
1310
-
1311
- const data = await response.json();
1312
- return json(data);
1313
- } catch (err) {
1314
- console.error(`Files list proxy error: ${err}`);
1315
- return json({ error: `Failed to fetch files: ${err}` }, 500);
1316
- }
1317
- }
1318
-
1319
- // GET /api/agents/:id/files/:fileId - Get a specific file
1320
- const fileGetMatch = path.match(/^\/api\/agents\/([^/]+)\/files\/([^/]+)$/);
1321
- if (fileGetMatch && method === "GET") {
1322
- const agent = AgentDB.findById(fileGetMatch[1]);
1323
- if (!agent) {
1324
- return json({ error: "Agent not found" }, 404);
1325
- }
1326
-
1327
- if (agent.status !== "running" || !agent.port) {
1328
- return json({ error: "Agent is not running" }, 400);
1329
- }
1330
-
1331
- try {
1332
- const fileId = fileGetMatch[2];
1333
- const response = await agentFetch(agent.id, agent.port, `/files/${fileId}`, {
1334
- method: "GET",
1335
- headers: { "Accept": "application/json" },
1336
- });
1337
-
1338
- if (!response.ok) {
1339
- const errorText = await response.text();
1340
- return json({ error: `Agent error: ${errorText}` }, response.status);
1341
- }
1342
-
1343
- const data = await response.json();
1344
- return json(data);
1345
- } catch (err) {
1346
- console.error(`File get proxy error: ${err}`);
1347
- return json({ error: `Failed to fetch file: ${err}` }, 500);
1348
- }
1349
- }
1350
-
1351
- // DELETE /api/agents/:id/files/:fileId - Delete a specific file
1352
- if (fileGetMatch && method === "DELETE") {
1353
- const agent = AgentDB.findById(fileGetMatch[1]);
1354
- if (!agent) {
1355
- return json({ error: "Agent not found" }, 404);
1356
- }
1357
-
1358
- if (agent.status !== "running" || !agent.port) {
1359
- return json({ error: "Agent is not running" }, 400);
1360
- }
1361
-
1362
- try {
1363
- const fileId = fileGetMatch[2];
1364
- const response = await agentFetch(agent.id, agent.port, `/files/${fileId}`, {
1365
- method: "DELETE",
1366
- });
1367
-
1368
- if (!response.ok) {
1369
- const errorText = await response.text();
1370
- return json({ error: `Agent error: ${errorText}` }, response.status);
1371
- }
1372
-
1373
- return json({ success: true });
1374
- } catch (err) {
1375
- console.error(`File delete proxy error: ${err}`);
1376
- return json({ error: `Failed to delete file: ${err}` }, 500);
1377
- }
1378
- }
1379
-
1380
- // GET /api/agents/:id/files/:fileId/download - Download a file
1381
- const fileDownloadMatch = path.match(/^\/api\/agents\/([^/]+)\/files\/([^/]+)\/download$/);
1382
- if (fileDownloadMatch && method === "GET") {
1383
- const agent = AgentDB.findById(fileDownloadMatch[1]);
1384
- if (!agent) {
1385
- return json({ error: "Agent not found" }, 404);
1386
- }
1387
-
1388
- if (agent.status !== "running" || !agent.port) {
1389
- return json({ error: "Agent is not running" }, 400);
1390
- }
1391
-
1392
- try {
1393
- const fileId = fileDownloadMatch[2];
1394
- const response = await agentFetch(agent.id, agent.port, `/files/${fileId}/download`);
1395
-
1396
- if (!response.ok) {
1397
- const errorText = await response.text();
1398
- return json({ error: `Agent error: ${errorText}` }, response.status);
1399
- }
1400
-
1401
- // Pass through the file response
1402
- return new Response(response.body, {
1403
- status: response.status,
1404
- headers: {
1405
- "Content-Type": response.headers.get("Content-Type") || "application/octet-stream",
1406
- "Content-Disposition": response.headers.get("Content-Disposition") || "attachment",
1407
- "Content-Length": response.headers.get("Content-Length") || "",
1408
- },
1409
- });
1410
- } catch (err) {
1411
- console.error(`File download proxy error: ${err}`);
1412
- return json({ error: `Failed to download file: ${err}` }, 500);
1413
- }
1414
- }
1415
-
1416
- // ==================== DISCOVERY/PEERS PROXY ====================
1417
-
1418
- // GET /api/discovery/agents - Central discovery endpoint for agents to find peers
1419
- // Called by agent binaries to discover other agents in the same group
1420
- if (path === "/api/discovery/agents" && method === "GET") {
1421
- const group = url.searchParams.get("group");
1422
- const excludeId = url.searchParams.get("exclude") || req.headers.get("X-Agent-ID");
1423
-
1424
- // Find all running agents in the same group
1425
- const allAgents = AgentDB.findAll();
1426
- const peers = allAgents
1427
- .filter(a => {
1428
- // Must be running with a port
1429
- if (a.status !== "running" || !a.port) return false;
1430
- // Exclude the requesting agent
1431
- if (excludeId && a.id === excludeId) return false;
1432
- // Must have multi-agent enabled
1433
- const agentConfig = getMultiAgentConfig(a.features, a.project_id);
1434
- if (!agentConfig.enabled) return false;
1435
- // If group specified, must match
1436
- if (group) {
1437
- const peerGroup = agentConfig.group || a.project_id;
1438
- if (peerGroup !== group) return false;
1439
- }
1440
- return true;
1441
- })
1442
- .map(a => {
1443
- const agentConfig = getMultiAgentConfig(a.features, a.project_id);
1444
- return {
1445
- id: a.id,
1446
- name: a.name,
1447
- url: `http://localhost:${a.port}`,
1448
- mode: agentConfig.mode || "worker",
1449
- group: agentConfig.group || a.project_id,
1450
- };
1451
- });
1452
-
1453
- return json({ agents: peers });
1454
- }
1455
-
1456
- // GET /api/agents/:id/peers - Get discovered peer agents
1457
- const peersMatch = path.match(/^\/api\/agents\/([^/]+)\/peers$/);
1458
- if (peersMatch && method === "GET") {
1459
- const agent = AgentDB.findById(peersMatch[1]);
1460
- if (!agent) {
1461
- return json({ error: "Agent not found" }, 404);
1462
- }
1463
-
1464
- if (agent.status !== "running" || !agent.port) {
1465
- return json({ error: "Agent is not running" }, 400);
1466
- }
1467
-
1468
- try {
1469
- const response = await agentFetch(agent.id, agent.port, "/discovery/agents", {
1470
- method: "GET",
1471
- headers: { "Accept": "application/json" },
1472
- });
1473
-
1474
- if (!response.ok) {
1475
- const errorText = await response.text();
1476
- return json({ error: `Agent error: ${errorText}` }, response.status);
1477
- }
1478
-
1479
- const data = await response.json();
1480
- return json(data);
1481
- } catch (err) {
1482
- console.error(`Peers list proxy error: ${err}`);
1483
- return json({ error: `Failed to fetch peers: ${err}` }, 500);
1484
- }
1485
- }
1486
-
1487
- // GET /api/providers - List supported providers and models with key status
1488
- if (path === "/api/providers" && method === "GET") {
1489
- const providers = getProvidersWithStatus();
1490
- return json({ providers });
1491
- }
1492
-
1493
- // GET /api/providers/ollama/models - Fetch available models from Ollama
1494
- if (path === "/api/providers/ollama/models" && method === "GET") {
1495
- // Get configured Ollama base URL or use default
1496
- const ollamaUrl = ProviderKeys.getDecrypted("ollama") || "http://localhost:11434";
1497
-
1498
- try {
1499
- const response = await fetch(`${ollamaUrl}/api/tags`, {
1500
- method: "GET",
1501
- headers: { "Accept": "application/json" },
1502
- });
1503
-
1504
- if (!response.ok) {
1505
- return json({ error: "Failed to connect to Ollama", models: [] }, 200);
1506
- }
1507
-
1508
- const data = await response.json() as { models?: Array<{ name: string; size: number; modified_at: string }> };
1509
- const models = (data.models || []).map((m: { name: string; size: number }) => ({
1510
- value: m.name,
1511
- label: m.name,
1512
- size: m.size,
1513
- }));
1514
-
1515
- return json({ models, connected: true });
1516
- } catch (err) {
1517
- // Ollama not running or not reachable
1518
- return json({
1519
- error: "Ollama not reachable. Make sure Ollama is running.",
1520
- models: [],
1521
- connected: false,
1522
- }, 200);
1523
- }
1524
- }
1525
-
1526
- // GET /api/providers/ollama/status - Check if Ollama is running
1527
- if (path === "/api/providers/ollama/status" && method === "GET") {
1528
- const ollamaUrl = ProviderKeys.getDecrypted("ollama") || "http://localhost:11434";
1529
-
1530
- try {
1531
- const response = await fetch(`${ollamaUrl}/api/tags`, {
1532
- method: "GET",
1533
- signal: AbortSignal.timeout(3000),
1534
- });
1535
-
1536
- if (response.ok) {
1537
- const data = await response.json() as { models?: Array<{ name: string }> };
1538
- return json({
1539
- connected: true,
1540
- url: ollamaUrl,
1541
- modelCount: data.models?.length || 0,
1542
- });
1543
- }
1544
- return json({ connected: false, url: ollamaUrl, error: "Ollama not responding" });
1545
- } catch {
1546
- return json({ connected: false, url: ollamaUrl, error: "Ollama not reachable" });
1547
- }
1548
- }
1549
-
1550
- // ==================== ONBOARDING ====================
1551
-
1552
- // GET /api/onboarding/status - Check onboarding status
1553
- if (path === "/api/onboarding/status" && method === "GET") {
1554
- return json(Onboarding.getStatus());
1555
- }
1556
-
1557
- // POST /api/onboarding/complete - Mark onboarding as complete
1558
- if (path === "/api/onboarding/complete" && method === "POST") {
1559
- Onboarding.complete();
1560
- return json({ success: true });
1561
- }
1562
-
1563
- // POST /api/onboarding/reset - Reset onboarding (for testing)
1564
- if (path === "/api/onboarding/reset" && method === "POST") {
1565
- Onboarding.reset();
1566
- return json({ success: true });
1567
- }
1568
-
1569
- // POST /api/onboarding/user - Create first user during onboarding
1570
- // This endpoint only works when no users exist (enforced by middleware)
1571
- if (path === "/api/onboarding/user" && method === "POST") {
1572
- debug("POST /api/onboarding/user");
1573
- // Double-check no users exist
1574
- if (UserDB.hasUsers()) {
1575
- debug("Users already exist");
1576
- return json({ error: "Users already exist" }, 403);
1577
- }
1578
-
1579
- try {
1580
- const body = await req.json();
1581
- debug("Onboarding body:", JSON.stringify(body));
1582
- const { username, password, email } = body;
1583
-
1584
- if (!username || !password) {
1585
- debug("Missing username or password");
1586
- return json({ error: "Username and password are required" }, 400);
1587
- }
1588
-
1589
- // Create first user as admin
1590
- debug("Creating user:", username);
1591
- const result = await createUser({
1592
- username,
1593
- password,
1594
- email: email || undefined, // Optional, for password recovery
1595
- role: "admin",
1596
- });
1597
- debug("Create user result:", result.success, result.error);
1598
-
1599
- if (!result.success) {
1600
- return json({ error: result.error }, 400);
1601
- }
1602
-
1603
- return json({
1604
- success: true,
1605
- user: {
1606
- id: result.user!.id,
1607
- username: result.user!.username,
1608
- role: result.user!.role,
1609
- },
1610
- }, 201);
1611
- } catch (e) {
1612
- debug("Onboarding error:", e);
1613
- return json({ error: "Invalid request body" }, 400);
1614
- }
1615
- }
1616
-
1617
- // ==================== META AGENT (Apteva Assistant) ====================
1618
-
1619
- // GET /api/meta-agent/status - Get meta agent status and config
1620
- if (path === "/api/meta-agent/status" && method === "GET") {
1621
- if (!META_AGENT_ENABLED) {
1622
- return json({ enabled: false });
1623
- }
1624
-
1625
- // Check if onboarding is complete
1626
- if (!Onboarding.isComplete()) {
1627
- return json({ enabled: true, available: false, reason: "onboarding_incomplete" });
1628
- }
1629
-
1630
- // Get first configured provider
1631
- const configuredProviders = ProviderKeys.getConfiguredProviders();
1632
- if (configuredProviders.length === 0) {
1633
- return json({ enabled: true, available: false, reason: "no_provider" });
1634
- }
1635
-
1636
- const providerId = configuredProviders[0] as keyof typeof PROVIDERS;
1637
- const provider = PROVIDERS[providerId];
1638
- if (!provider) {
1639
- return json({ enabled: true, available: false, reason: "invalid_provider" });
1640
- }
1641
-
1642
- // Check if meta agent exists, create if not
1643
- let metaAgent = AgentDB.findById(META_AGENT_ID);
1644
- if (!metaAgent) {
1645
- // Find a recommended model or use first one
1646
- const defaultModel = provider.models.find(m => m.recommended)?.value || provider.models[0]?.value;
1647
- if (!defaultModel) {
1648
- return json({ enabled: true, available: false, reason: "no_model" });
1649
- }
1650
-
1651
- // Create the meta agent
1652
- metaAgent = AgentDB.create({
1653
- id: META_AGENT_ID,
1654
- name: "Apteva Assistant",
1655
- model: defaultModel,
1656
- provider: providerId,
1657
- system_prompt: `You are the Apteva Assistant, a helpful guide for users of the Apteva agent management platform.
1658
-
1659
- You can help users with:
1660
- - Creating and configuring AI agents
1661
- - Setting up MCP servers for tool integrations
1662
- - Managing projects and organizing agents
1663
- - Explaining features like Memory, Tasks, Vision, Operator, Files, and Multi-Agent
1664
- - Troubleshooting common issues
1665
-
1666
- Be concise, friendly, and helpful. When users ask about creating something, guide them step by step.
1667
- Keep responses short and actionable. Use markdown formatting when helpful.`,
1668
- features: {
1669
- memory: false,
1670
- tasks: false,
1671
- vision: false,
1672
- operator: false,
1673
- mcp: false,
1674
- realtime: false,
1675
- files: false,
1676
- agents: false,
1677
- },
1678
- mcp_servers: [],
1679
- skills: [],
1680
- project_id: null, // Meta agent belongs to no project
1681
- });
1682
- }
1683
-
1684
- // Return status
1685
- return json({
1686
- enabled: true,
1687
- available: true,
1688
- agent: {
1689
- id: metaAgent.id,
1690
- name: metaAgent.name,
1691
- status: metaAgent.status,
1692
- port: metaAgent.port,
1693
- provider: metaAgent.provider,
1694
- model: metaAgent.model,
1695
- },
1696
- });
1697
- }
1698
-
1699
- // POST /api/meta-agent/start - Start the meta agent
1700
- if (path === "/api/meta-agent/start" && method === "POST") {
1701
- if (!META_AGENT_ENABLED) {
1702
- return json({ error: "Meta agent is not enabled" }, 400);
1703
- }
1704
-
1705
- const metaAgent = AgentDB.findById(META_AGENT_ID);
1706
- if (!metaAgent) {
1707
- return json({ error: "Meta agent not found" }, 404);
1708
- }
1709
-
1710
- if (metaAgent.status === "running") {
1711
- return json({ agent: toApiAgent(metaAgent), message: "Already running" });
1712
- }
1713
-
1714
- // Start the agent using existing startAgentProcess function
1715
- const result = await startAgentProcess(metaAgent, { silent: true });
1716
- if (!result.success) {
1717
- return json({ error: result.error || "Failed to start meta agent" }, 500);
1718
- }
1719
-
1720
- const updated = AgentDB.findById(META_AGENT_ID);
1721
- return json({ agent: updated ? toApiAgent(updated) : null });
1722
- }
1723
-
1724
- // POST /api/meta-agent/stop - Stop the meta agent
1725
- if (path === "/api/meta-agent/stop" && method === "POST") {
1726
- if (!META_AGENT_ENABLED) {
1727
- return json({ error: "Meta agent is not enabled" }, 400);
1728
- }
1729
-
1730
- const metaAgent = AgentDB.findById(META_AGENT_ID);
1731
- if (!metaAgent) {
1732
- return json({ error: "Meta agent not found" }, 404);
1733
- }
1734
-
1735
- if (metaAgent.status === "stopped") {
1736
- return json({ agent: toApiAgent(metaAgent), message: "Already stopped" });
1737
- }
1738
-
1739
- // Stop the agent
1740
- const proc = agentProcesses.get(META_AGENT_ID);
1741
- if (proc) {
1742
- proc.kill();
1743
- agentProcesses.delete(META_AGENT_ID);
1744
- }
1745
- AgentDB.setStatus(META_AGENT_ID, "stopped");
1746
-
1747
- const updated = AgentDB.findById(META_AGENT_ID);
1748
- return json({ agent: updated ? toApiAgent(updated) : null });
1749
- }
1750
-
1751
- // ==================== USER MANAGEMENT (Admin only) ====================
1752
-
1753
- // GET /api/users - List all users
1754
- if (path === "/api/users" && method === "GET") {
1755
- const users = UserDB.findAll().map(u => ({
1756
- id: u.id,
1757
- username: u.username,
1758
- email: u.email,
1759
- role: u.role,
1760
- createdAt: u.created_at,
1761
- lastLoginAt: u.last_login_at,
1762
- }));
1763
- return json({ users });
1764
- }
1765
-
1766
- // POST /api/users - Create a new user
1767
- if (path === "/api/users" && method === "POST") {
1768
- try {
1769
- const body = await req.json();
1770
- const { username, password, email, role } = body;
1771
-
1772
- if (!username || !password) {
1773
- return json({ error: "Username and password are required" }, 400);
1774
- }
1775
-
1776
- const result = await createUser({
1777
- username,
1778
- password,
1779
- email: email || undefined,
1780
- role: role || "user",
1781
- });
1782
-
1783
- if (!result.success) {
1784
- return json({ error: result.error }, 400);
1785
- }
1786
-
1787
- return json({
1788
- user: {
1789
- id: result.user!.id,
1790
- username: result.user!.username,
1791
- email: result.user!.email,
1792
- role: result.user!.role,
1793
- createdAt: result.user!.created_at,
1794
- },
1795
- }, 201);
1796
- } catch (e) {
1797
- return json({ error: "Invalid request body" }, 400);
1798
- }
1799
- }
1800
-
1801
- // GET /api/users/:id - Get a specific user
1802
- const userMatch = path.match(/^\/api\/users\/([^/]+)$/);
1803
- if (userMatch && method === "GET") {
1804
- const targetUser = UserDB.findById(userMatch[1]);
1805
- if (!targetUser) {
1806
- return json({ error: "User not found" }, 404);
1807
- }
1808
- return json({
1809
- user: {
1810
- id: targetUser.id,
1811
- username: targetUser.username,
1812
- email: targetUser.email,
1813
- role: targetUser.role,
1814
- createdAt: targetUser.created_at,
1815
- lastLoginAt: targetUser.last_login_at,
1816
- },
1817
- });
1818
- }
1819
-
1820
- // PUT /api/users/:id - Update a user
1821
- if (userMatch && method === "PUT") {
1822
- const targetUser = UserDB.findById(userMatch[1]);
1823
- if (!targetUser) {
1824
- return json({ error: "User not found" }, 404);
1825
- }
1826
-
1827
- try {
1828
- const body = await req.json();
1829
- const updates: Parameters<typeof UserDB.update>[1] = {};
1830
-
1831
- if (body.email !== undefined) updates.email = body.email;
1832
- if (body.role !== undefined) {
1833
- // Prevent removing last admin
1834
- if (targetUser.role === "admin" && body.role !== "admin") {
1835
- if (UserDB.countAdmins() <= 1) {
1836
- return json({ error: "Cannot remove the last admin" }, 400);
1837
- }
1838
- }
1839
- updates.role = body.role;
1840
- }
1841
- if (body.password !== undefined) {
1842
- const validation = validatePassword(body.password);
1843
- if (!validation.valid) {
1844
- return json({ error: validation.errors.join(". ") }, 400);
1845
- }
1846
- updates.password_hash = await hashPassword(body.password);
1847
- }
1848
-
1849
- const updated = UserDB.update(userMatch[1], updates);
1850
- return json({
1851
- user: updated ? {
1852
- id: updated.id,
1853
- username: updated.username,
1854
- email: updated.email,
1855
- role: updated.role,
1856
- createdAt: updated.created_at,
1857
- lastLoginAt: updated.last_login_at,
1858
- } : null,
1859
- });
1860
- } catch (e) {
1861
- return json({ error: "Invalid request body" }, 400);
1862
- }
1863
- }
1864
-
1865
- // DELETE /api/users/:id - Delete a user
1866
- if (userMatch && method === "DELETE") {
1867
- const targetUser = UserDB.findById(userMatch[1]);
1868
- if (!targetUser) {
1869
- return json({ error: "User not found" }, 404);
1870
- }
1871
-
1872
- // Prevent deleting yourself
1873
- if (user && targetUser.id === user.id) {
1874
- return json({ error: "Cannot delete your own account" }, 400);
1875
- }
1876
-
1877
- // Prevent deleting last admin
1878
- if (targetUser.role === "admin" && UserDB.countAdmins() <= 1) {
1879
- return json({ error: "Cannot delete the last admin" }, 400);
1880
- }
1881
-
1882
- UserDB.delete(userMatch[1]);
1883
- return json({ success: true });
1884
- }
1885
-
1886
- // ==================== PROJECTS ====================
1887
-
1888
- // GET /api/projects - List all projects
1889
- if (path === "/api/projects" && method === "GET") {
1890
- const projects = ProjectDB.findAll();
1891
- const agentCounts = ProjectDB.getAgentCounts();
1892
- return json({
1893
- projects: projects.map(p => ({
1894
- ...toApiProject(p),
1895
- agentCount: agentCounts.get(p.id) || 0,
1896
- })),
1897
- unassignedCount: agentCounts.get(null) || 0,
1898
- });
1899
- }
1900
-
1901
- // POST /api/projects - Create a new project
1902
- if (path === "/api/projects" && method === "POST") {
1903
- try {
1904
- const body = await req.json();
1905
- const { name, description, color } = body;
1906
-
1907
- if (!name) {
1908
- return json({ error: "Name is required" }, 400);
1909
- }
1910
-
1911
- const project = ProjectDB.create({
1912
- name,
1913
- description: description || null,
1914
- color: color || "#6366f1",
1915
- });
1916
-
1917
- return json({ project: toApiProject(project) }, 201);
1918
- } catch (e) {
1919
- console.error("Create project error:", e);
1920
- return json({ error: "Invalid request body" }, 400);
1921
- }
1922
- }
1923
-
1924
- // GET /api/projects/:id - Get a specific project
1925
- const projectMatch = path.match(/^\/api\/projects\/([^/]+)$/);
1926
- if (projectMatch && method === "GET") {
1927
- const project = ProjectDB.findById(projectMatch[1]);
1928
- if (!project) {
1929
- return json({ error: "Project not found" }, 404);
1930
- }
1931
- const agents = AgentDB.findByProject(project.id);
1932
- return json({
1933
- project: toApiProject(project),
1934
- agents: agents.map(toApiAgent),
1935
- });
1936
- }
1937
-
1938
- // PUT /api/projects/:id - Update a project
1939
- if (projectMatch && method === "PUT") {
1940
- const project = ProjectDB.findById(projectMatch[1]);
1941
- if (!project) {
1942
- return json({ error: "Project not found" }, 404);
1943
- }
1944
-
1945
- try {
1946
- const body = await req.json();
1947
- const updates: Partial<Project> = {};
1948
-
1949
- if (body.name !== undefined) updates.name = body.name;
1950
- if (body.description !== undefined) updates.description = body.description;
1951
- if (body.color !== undefined) updates.color = body.color;
1952
-
1953
- const updated = ProjectDB.update(projectMatch[1], updates);
1954
- return json({ project: updated ? toApiProject(updated) : null });
1955
- } catch (e) {
1956
- return json({ error: "Invalid request body" }, 400);
1957
- }
1958
- }
1959
-
1960
- // DELETE /api/projects/:id - Delete a project
1961
- if (projectMatch && method === "DELETE") {
1962
- const project = ProjectDB.findById(projectMatch[1]);
1963
- if (!project) {
1964
- return json({ error: "Project not found" }, 404);
1965
- }
1966
-
1967
- ProjectDB.delete(projectMatch[1]);
1968
- return json({ success: true });
1969
- }
1970
-
1971
- // ==================== API KEYS ====================
1972
-
1973
- // GET /api/keys - List all configured provider keys (without actual keys)
1974
- if (path === "/api/keys" && method === "GET") {
1975
- return json({ keys: ProviderKeys.getAll() });
1976
- }
1977
-
1978
- // POST /api/keys/:provider - Save an API key for a provider
1979
- const saveKeyMatch = path.match(/^\/api\/keys\/([^/]+)$/);
1980
- if (saveKeyMatch && method === "POST") {
1981
- const providerId = saveKeyMatch[1];
1982
-
1983
- // Validate provider exists
1984
- if (!PROVIDERS[providerId as ProviderId]) {
1985
- return json({ error: "Unknown provider" }, 400);
1986
- }
1987
-
1988
- try {
1989
- const body = await req.json();
1990
- const { key } = body;
1991
-
1992
- if (!key) {
1993
- return json({ error: "API key is required" }, 400);
1994
- }
1995
-
1996
- const result = await ProviderKeys.save(providerId, key);
1997
- if (!result.success) {
1998
- return json({ error: result.error }, 400);
1999
- }
2000
-
2001
- // Restart any running agents that use this provider (including meta agent)
2002
- const runningAgents = AgentDB.findAll().filter(
2003
- a => a.status === "running" && a.provider === providerId
2004
- );
2005
-
2006
- const restartResults: Array<{ id: string; name: string; success: boolean; error?: string }> = [];
2007
- for (const agent of runningAgents) {
2008
- try {
2009
- // Stop the agent
2010
- const agentProc = agentProcesses.get(agent.id);
2011
- if (agentProc) {
2012
- agentProc.proc.kill();
2013
- agentProcesses.delete(agent.id);
2014
- }
2015
- AgentDB.setStatus(agent.id, "stopped", null);
2016
-
2017
- // Wait a moment for port to be released
2018
- await new Promise(r => setTimeout(r, 500));
2019
-
2020
- // Restart the agent with new key
2021
- const startResult = await startAgentProcess(agent, { silent: true });
2022
- restartResults.push({
2023
- id: agent.id,
2024
- name: agent.name,
2025
- success: startResult.success,
2026
- error: startResult.error,
2027
- });
2028
- } catch (e) {
2029
- restartResults.push({
2030
- id: agent.id,
2031
- name: agent.name,
2032
- success: false,
2033
- error: String(e),
2034
- });
2035
- }
2036
- }
2037
-
2038
- return json({
2039
- success: true,
2040
- message: "API key saved successfully",
2041
- restartedAgents: restartResults.length > 0 ? restartResults : undefined,
2042
- });
2043
- } catch (e) {
2044
- return json({ error: "Invalid request body" }, 400);
2045
- }
2046
- }
2047
-
2048
- // DELETE /api/keys/:provider - Remove an API key
2049
- if (saveKeyMatch && method === "DELETE") {
2050
- const providerId = saveKeyMatch[1];
2051
- const deleted = ProviderKeys.delete(providerId);
2052
- return json({ success: deleted });
2053
- }
2054
-
2055
- // POST /api/keys/:provider/test - Test an API key
2056
- const testKeyMatch = path.match(/^\/api\/keys\/([^/]+)\/test$/);
2057
- if (testKeyMatch && method === "POST") {
2058
- const providerId = testKeyMatch[1];
2059
-
2060
- // Validate provider exists
2061
- if (!PROVIDERS[providerId as ProviderId]) {
2062
- return json({ error: "Unknown provider" }, 400);
2063
- }
2064
-
2065
- try {
2066
- const body = await req.json().catch(() => ({}));
2067
- const { key } = body as { key?: string };
2068
-
2069
- // Test with provided key or stored key
2070
- const result = await ProviderKeys.test(providerId, key);
2071
- return json(result);
2072
- } catch (e) {
2073
- return json({ error: "Test failed" }, 500);
2074
- }
2075
- }
2076
-
2077
- // GET /api/stats - Get statistics
2078
- if (path === "/api/stats" && method === "GET") {
2079
- return json({
2080
- totalAgents: AgentDB.count(),
2081
- runningAgents: AgentDB.countRunning(),
2082
- });
2083
- }
2084
-
2085
- // GET /api/binary - Get binary status
2086
- if (path === "/api/binary" && method === "GET") {
2087
- return json(getBinaryStatus(BIN_DIR));
2088
- }
2089
-
2090
- // GET /api/version - Check agent binary version info
2091
- if (path === "/api/version" && method === "GET") {
2092
- const versionInfo = await checkForUpdates();
2093
- return json(versionInfo);
2094
- }
2095
-
2096
- // POST /api/version/update - Download/install latest agent binary
2097
- if (path === "/api/version/update" && method === "POST") {
2098
- // Get all running agents to restart later
2099
- const runningAgents = AgentDB.findAll().filter(a => a.status === "running");
2100
- const agentsToRestart = runningAgents.map(a => a.id);
2101
-
2102
- // Stop all running agents
2103
- for (const agent of runningAgents) {
2104
- const agentProc = agentProcesses.get(agent.id);
2105
- if (agentProc) {
2106
- console.log(`Stopping agent ${agent.name} for update...`);
2107
- agentProc.proc.kill();
2108
- agentProcesses.delete(agent.id);
2109
- }
2110
- AgentDB.setStatus(agent.id, "stopped");
2111
- }
2112
-
2113
- // Try npm install first, fall back to direct download
2114
- let result = await installViaNpm();
2115
- if (!result.success) {
2116
- // Fall back to direct download
2117
- result = await downloadLatestBinary(BIN_DIR);
2118
- }
2119
-
2120
- if (!result.success) {
2121
- return json({ success: false, error: result.error }, 500);
2122
- }
2123
-
2124
- // Restart agents that were running
2125
- const restartResults: { id: string; name: string; success: boolean; error?: string }[] = [];
2126
- for (const agentId of agentsToRestart) {
2127
- const agent = AgentDB.findById(agentId);
2128
- if (agent) {
2129
- console.log(`Restarting agent ${agent.name} after update...`);
2130
- const startResult = await startAgentProcess(agent);
2131
- restartResults.push({
2132
- id: agent.id,
2133
- name: agent.name,
2134
- success: startResult.success,
2135
- error: startResult.error,
2136
- });
2137
- }
2138
- }
2139
-
2140
- return json({
2141
- success: true,
2142
- version: result.version,
2143
- restarted: restartResults,
2144
- });
2145
- }
2146
-
2147
- // GET /api/health - Health check
2148
- if (path === "/api/health") {
2149
- const binaryStatus = getBinaryStatus(BIN_DIR);
2150
- const installedVersion = getInstalledVersion();
2151
- return json({
2152
- status: "ok",
2153
- timestamp: new Date().toISOString(),
2154
- agents: {
2155
- total: AgentDB.count(),
2156
- running: AgentDB.countRunning(),
2157
- },
2158
- binary: {
2159
- available: binaryStatus.exists,
2160
- platform: binaryStatus.platform,
2161
- arch: binaryStatus.arch,
2162
- version: installedVersion,
2163
- }
2164
- });
2165
- }
2166
-
2167
- // ==================== TASKS ====================
2168
-
2169
- // Helper to fetch from a running agent (with authentication)
2170
- async function fetchFromAgent(agentId: string, port: number, endpoint: string): Promise<any> {
2171
- try {
2172
- const response = await agentFetch(agentId, port, endpoint, {
2173
- headers: { "Accept": "application/json" },
2174
- });
2175
- if (response.ok) {
2176
- return await response.json();
2177
- }
2178
- return null;
2179
- } catch {
2180
- return null;
2181
- }
2182
- }
2183
-
2184
- // GET /api/tasks - Get all tasks from all running agents
2185
- if (path === "/api/tasks" && method === "GET") {
2186
- const url = new URL(req.url);
2187
- const status = url.searchParams.get("status") || "all";
2188
-
2189
- const runningAgents = AgentDB.findAll().filter(a => a.status === "running" && a.port);
2190
- const allTasks: any[] = [];
2191
-
2192
- for (const agent of runningAgents) {
2193
- const data = await fetchFromAgent(agent.id, agent.port!, `/tasks?status=${status}`);
2194
- if (data?.tasks) {
2195
- // Add agent info to each task
2196
- for (const task of data.tasks) {
2197
- allTasks.push({
2198
- ...task,
2199
- agentId: agent.id,
2200
- agentName: agent.name,
2201
- });
2202
- }
2203
- }
2204
- }
2205
-
2206
- // Sort by created_at descending
2207
- allTasks.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
2208
-
2209
- return json({ tasks: allTasks, count: allTasks.length });
2210
- }
2211
-
2212
- // GET /api/agents/:id/tasks - Get tasks from a specific agent
2213
- const agentTasksMatch = path.match(/^\/api\/agents\/([^/]+)\/tasks$/);
2214
- if (agentTasksMatch && method === "GET") {
2215
- const agentId = agentTasksMatch[1];
2216
- const agent = AgentDB.findById(agentId);
2217
-
2218
- if (!agent) {
2219
- return json({ error: "Agent not found" }, 404);
2220
- }
2221
-
2222
- if (agent.status !== "running" || !agent.port) {
2223
- return json({ error: "Agent is not running" }, 400);
2224
- }
2225
-
2226
- const url = new URL(req.url);
2227
- const status = url.searchParams.get("status") || "all";
2228
-
2229
- const data = await fetchFromAgent(agent.id, agent.port, `/tasks?status=${status}`);
2230
- if (!data) {
2231
- return json({ error: "Failed to fetch tasks from agent" }, 500);
2232
- }
2233
-
2234
- return json(data);
2235
- }
2236
-
2237
- // GET /api/dashboard - Get dashboard statistics
2238
- if (path === "/api/dashboard" && method === "GET") {
2239
- const agents = AgentDB.findAll();
2240
- const runningAgents = agents.filter(a => a.status === "running" && a.port);
2241
-
2242
- let totalTasks = 0;
2243
- let pendingTasks = 0;
2244
- let completedTasks = 0;
2245
- let runningTasks = 0;
2246
-
2247
- for (const agent of runningAgents) {
2248
- const data = await fetchFromAgent(agent.id, agent.port!, "/tasks?status=all");
2249
- if (data?.tasks) {
2250
- totalTasks += data.tasks.length;
2251
- for (const task of data.tasks) {
2252
- if (task.status === "pending") pendingTasks++;
2253
- else if (task.status === "completed") completedTasks++;
2254
- else if (task.status === "running") runningTasks++;
2255
- }
2256
- }
2257
- }
2258
-
2259
- return json({
2260
- agents: {
2261
- total: agents.length,
2262
- running: runningAgents.length,
2263
- },
2264
- tasks: {
2265
- total: totalTasks,
2266
- pending: pendingTasks,
2267
- running: runningTasks,
2268
- completed: completedTasks,
2269
- },
2270
- providers: {
2271
- configured: ProviderKeys.getConfiguredProviders().length,
2272
- },
2273
- });
2274
- }
2275
-
2276
- // GET /api/version - Get current and latest version
2277
- if (path === "/api/version" && method === "GET") {
2278
- try {
2279
- // Get current version from package.json
2280
- const pkg = await import("../../package.json");
2281
- const currentVersion = pkg.version;
2282
-
2283
- // Check npm registry for latest version
2284
- let latestVersion = currentVersion;
2285
- let updateAvailable = false;
2286
-
2287
- try {
2288
- const response = await fetch("https://registry.npmjs.org/apteva/latest", {
2289
- headers: { "Accept": "application/json" },
2290
- });
2291
- if (response.ok) {
2292
- const data = await response.json();
2293
- latestVersion = data.version;
2294
- updateAvailable = latestVersion !== currentVersion;
2295
- }
2296
- } catch {
2297
- // Failed to check, assume current is latest
2298
- }
2299
-
2300
- return json({
2301
- current: currentVersion,
2302
- latest: latestVersion,
2303
- updateAvailable,
2304
- updateCommand: "npm update -g apteva",
2305
- });
2306
- } catch {
2307
- return json({ error: "Failed to check version" }, 500);
2308
- }
2309
- }
2310
-
2311
- // ============ MCP Server API ============
2312
-
2313
- // GET /api/mcp/servers - List MCP servers (optionally filtered by project)
2314
- if (path === "/api/mcp/servers" && method === "GET") {
2315
- const url = new URL(req.url);
2316
- const projectFilter = url.searchParams.get("project"); // "all", "global", or project ID
2317
- const forAgent = url.searchParams.get("forAgent"); // agent's project ID (shows global + project)
2318
-
2319
- let servers;
2320
- if (forAgent !== null) {
2321
- // Get servers available for an agent (global + agent's project)
2322
- servers = McpServerDB.findForAgent(forAgent || null);
2323
- } else if (projectFilter === "global") {
2324
- servers = McpServerDB.findGlobal();
2325
- } else if (projectFilter && projectFilter !== "all") {
2326
- servers = McpServerDB.findByProject(projectFilter);
2327
- } else {
2328
- servers = McpServerDB.findAll();
2329
- }
2330
- return json({ servers });
2331
- }
2332
-
2333
- // GET /api/mcp/registry - Search MCP registry for available servers
2334
- if (path === "/api/mcp/registry" && method === "GET") {
2335
- const url = new URL(req.url);
2336
- const search = url.searchParams.get("search") || "";
2337
- const limit = url.searchParams.get("limit") || "20";
2338
-
2339
- try {
2340
- const registryUrl = `https://registry.modelcontextprotocol.io/v0/servers?search=${encodeURIComponent(search)}&limit=${limit}`;
2341
- const res = await fetch(registryUrl);
2342
- if (!res.ok) {
2343
- return json({ error: "Failed to fetch registry" }, 500);
2344
- }
2345
- const data = await res.json();
2346
-
2347
- // Transform to simpler format - dedupe by name
2348
- const seen = new Set<string>();
2349
- const servers = (data.servers || [])
2350
- .map((item: any) => {
2351
- const s = item.server;
2352
- const pkg = s.packages?.find((p: any) => p.registryType === "npm");
2353
- const remote = s.remotes?.[0];
2354
-
2355
- // Extract a short display name from the full name
2356
- // e.g., "ai.smithery/smithery-ai-github" -> "github"
2357
- // e.g., "io.github.user/my-server" -> "my-server"
2358
- const fullName = s.name || "";
2359
- const shortName = fullName.split("/").pop()?.replace(/-mcp$/, "").replace(/^mcp-/, "") || fullName;
2360
-
2361
- return {
2362
- id: fullName, // Use full name as unique ID
2363
- name: shortName,
2364
- fullName: fullName,
2365
- description: s.description,
2366
- version: s.version,
2367
- repository: s.repository?.url,
2368
- npmPackage: pkg?.identifier || null,
2369
- remoteUrl: remote?.url || null,
2370
- transport: pkg?.transport?.type || (remote ? "http" : "stdio"),
2371
- };
2372
- })
2373
- .filter((s: any) => {
2374
- // Dedupe by fullName
2375
- if (seen.has(s.fullName)) return false;
2376
- seen.add(s.fullName);
2377
- // Only show servers with npm package or remote URL
2378
- return s.npmPackage || s.remoteUrl;
2379
- });
2380
-
2381
- return json({ servers });
2382
- } catch (e) {
2383
- return json({ error: "Failed to search registry" }, 500);
2384
- }
2385
- }
2386
-
2387
- // ============ Generic Integration Providers ============
2388
- // These endpoints work with any registered provider (composio, smithery, etc.)
2389
-
2390
- // GET /api/integrations/providers - List available integration providers
2391
- if (path === "/api/integrations/providers" && method === "GET") {
2392
- const providerIds = getProviderIds();
2393
- const providers = providerIds.map(id => {
2394
- const provider = getProvider(id);
2395
- const hasKey = !!ProviderKeys.getDecrypted(id);
2396
- return {
2397
- id,
2398
- name: provider?.name || id,
2399
- connected: hasKey,
2400
- };
2401
- });
2402
- return json({ providers });
2403
- }
2404
-
2405
- // GET /api/integrations/:provider/apps - List available apps from a provider
2406
- const appsMatch = path.match(/^\/api\/integrations\/([^/]+)\/apps$/);
2407
- if (appsMatch && method === "GET") {
2408
- const providerId = appsMatch[1];
2409
- const provider = getProvider(providerId);
2410
- if (!provider) {
2411
- return json({ error: `Unknown provider: ${providerId}` }, 404);
2412
- }
2413
-
2414
- const apiKey = ProviderKeys.getDecrypted(providerId);
2415
- if (!apiKey) {
2416
- return json({ error: `${provider.name} API key not configured`, apps: [] }, 200);
2417
- }
2418
-
2419
- try {
2420
- const apps = await provider.listApps(apiKey);
2421
- return json({ apps });
2422
- } catch (e) {
2423
- console.error(`Failed to list apps from ${providerId}:`, e);
2424
- return json({ error: "Failed to fetch apps" }, 500);
2425
- }
2426
- }
2427
-
2428
- // GET /api/integrations/:provider/connected - List user's connected accounts
2429
- const connectedMatch = path.match(/^\/api\/integrations\/([^/]+)\/connected$/);
2430
- if (connectedMatch && method === "GET") {
2431
- const providerId = connectedMatch[1];
2432
- const provider = getProvider(providerId);
2433
- if (!provider) {
2434
- return json({ error: `Unknown provider: ${providerId}` }, 404);
2435
- }
2436
-
2437
- const apiKey = ProviderKeys.getDecrypted(providerId);
2438
- if (!apiKey) {
2439
- return json({ error: `${provider.name} API key not configured`, accounts: [] }, 200);
2440
- }
2441
-
2442
- // Use Apteva user ID as the entity ID for the provider
2443
- const userId = user?.id || "default";
2444
-
2445
- try {
2446
- const accounts = await provider.listConnectedAccounts(apiKey, userId);
2447
- return json({ accounts });
2448
- } catch (e) {
2449
- console.error(`Failed to list connected accounts from ${providerId}:`, e);
2450
- return json({ error: "Failed to fetch connected accounts" }, 500);
2451
- }
2452
- }
2453
-
2454
- // POST /api/integrations/:provider/connect - Initiate connection (OAuth or API Key)
2455
- const connectMatch = path.match(/^\/api\/integrations\/([^/]+)\/connect$/);
2456
- if (connectMatch && method === "POST") {
2457
- const providerId = connectMatch[1];
2458
- const provider = getProvider(providerId);
2459
- if (!provider) {
2460
- return json({ error: `Unknown provider: ${providerId}` }, 404);
2461
- }
2462
-
2463
- const apiKey = ProviderKeys.getDecrypted(providerId);
2464
- if (!apiKey) {
2465
- return json({ error: `${provider.name} API key not configured` }, 401);
2466
- }
2467
-
2468
- try {
2469
- const body = await req.json();
2470
- const { appSlug, redirectUrl, credentials } = body;
2471
-
2472
- if (!appSlug) {
2473
- return json({ error: "appSlug is required" }, 400);
2474
- }
2475
-
2476
- // Use Apteva user ID as the entity ID
2477
- const userId = user?.id || "default";
2478
-
2479
- // Default redirect URL back to our integrations page
2480
- const callbackUrl = redirectUrl || `http://localhost:${process.env.PORT || 4280}/mcp?tab=hosted&connected=${appSlug}`;
2481
-
2482
- const result = await provider.initiateConnection(apiKey, userId, appSlug, callbackUrl, credentials);
2483
- return json(result);
2484
- } catch (e) {
2485
- console.error(`Failed to initiate connection for ${providerId}:`, e);
2486
- return json({ error: `Failed to initiate connection: ${e}` }, 500);
2487
- }
2488
- }
2489
-
2490
- // GET /api/integrations/:provider/connection/:id - Check connection status
2491
- const connectionStatusMatch = path.match(/^\/api\/integrations\/([^/]+)\/connection\/([^/]+)$/);
2492
- if (connectionStatusMatch && method === "GET") {
2493
- const providerId = connectionStatusMatch[1];
2494
- const connectionId = connectionStatusMatch[2];
2495
- const provider = getProvider(providerId);
2496
- if (!provider) {
2497
- return json({ error: `Unknown provider: ${providerId}` }, 404);
2498
- }
2499
-
2500
- const apiKey = ProviderKeys.getDecrypted(providerId);
2501
- if (!apiKey) {
2502
- return json({ error: `${provider.name} API key not configured` }, 401);
2503
- }
2504
-
2505
- try {
2506
- const connection = await provider.getConnectionStatus(apiKey, connectionId);
2507
- if (!connection) {
2508
- return json({ error: "Connection not found" }, 404);
2509
- }
2510
- return json({ connection });
2511
- } catch (e) {
2512
- console.error(`Failed to get connection status:`, e);
2513
- return json({ error: "Failed to get connection status" }, 500);
2514
- }
2515
- }
2516
-
2517
- // DELETE /api/integrations/:provider/connection/:id - Disconnect/revoke
2518
- const disconnectMatch = path.match(/^\/api\/integrations\/([^/]+)\/connection\/([^/]+)$/);
2519
- if (disconnectMatch && method === "DELETE") {
2520
- const providerId = disconnectMatch[1];
2521
- const connectionId = disconnectMatch[2];
2522
- const provider = getProvider(providerId);
2523
- if (!provider) {
2524
- return json({ error: `Unknown provider: ${providerId}` }, 404);
2525
- }
2526
-
2527
- const apiKey = ProviderKeys.getDecrypted(providerId);
2528
- if (!apiKey) {
2529
- return json({ error: `${provider.name} API key not configured` }, 401);
2530
- }
2531
-
2532
- try {
2533
- const success = await provider.disconnect(apiKey, connectionId);
2534
- return json({ success });
2535
- } catch (e) {
2536
- console.error(`Failed to disconnect:`, e);
2537
- return json({ error: "Failed to disconnect" }, 500);
2538
- }
2539
- }
2540
-
2541
- // ============ Composio-Specific Routes (MCP Configs) ============
2542
-
2543
- // GET /api/integrations/composio/configs - List Composio MCP configs
2544
- if (path === "/api/integrations/composio/configs" && method === "GET") {
2545
- const apiKey = ProviderKeys.getDecrypted("composio");
2546
- if (!apiKey) {
2547
- return json({ error: "Composio API key not configured", configs: [] }, 200);
2548
- }
2549
-
2550
- try {
2551
- const res = await fetch("https://backend.composio.dev/api/v3/mcp/servers?limit=50", {
2552
- headers: {
2553
- "x-api-key": apiKey,
2554
- "Content-Type": "application/json",
2555
- },
2556
- });
2557
-
2558
- if (!res.ok) {
2559
- const text = await res.text();
2560
- console.error("Composio API error:", res.status, text);
2561
- return json({ error: "Failed to fetch Composio configs" }, 500);
2562
- }
2563
-
2564
- const data = await res.json();
2565
-
2566
- // Transform to our format (no user_id in URLs - that's provided when adding)
2567
- const configs = (data.items || data.servers || []).map((item: any) => ({
2568
- id: item.id,
2569
- name: item.name || item.id,
2570
- toolkits: item.toolkits || item.apps || [],
2571
- toolsCount: item.toolsCount || item.tools?.length || 0,
2572
- createdAt: item.createdAt || item.created_at,
2573
- }));
2574
-
2575
- return json({ configs });
2576
- } catch (e) {
2577
- console.error("Composio fetch error:", e);
2578
- return json({ error: "Failed to connect to Composio" }, 500);
2579
- }
2580
- }
2581
-
2582
- // GET /api/integrations/composio/configs/:id - Get single Composio config details
2583
- const composioConfigMatch = path.match(/^\/api\/integrations\/composio\/configs\/([^/]+)$/);
2584
- if (composioConfigMatch && method === "GET") {
2585
- const configId = composioConfigMatch[1];
2586
- const apiKey = ProviderKeys.getDecrypted("composio");
2587
- if (!apiKey) {
2588
- return json({ error: "Composio API key not configured" }, 401);
2589
- }
2590
-
2591
- try {
2592
- const res = await fetch(`https://backend.composio.dev/api/v3/mcp/${configId}`, {
2593
- headers: {
2594
- "x-api-key": apiKey,
2595
- "Content-Type": "application/json",
2596
- },
2597
- });
2598
-
2599
- if (!res.ok) {
2600
- return json({ error: "Config not found" }, 404);
2601
- }
2602
-
2603
- const data = await res.json();
2604
- return json({
2605
- config: {
2606
- id: data.id,
2607
- name: data.name || data.id,
2608
- toolkits: data.toolkits || data.apps || [],
2609
- tools: data.tools || [],
2610
- },
2611
- });
2612
- } catch (e) {
2613
- return json({ error: "Failed to fetch config" }, 500);
2614
- }
2615
- }
2616
-
2617
- // POST /api/integrations/composio/configs/:id/add - Add a Composio config as an MCP server
2618
- // Fetches the mcp_url directly from Composio API
2619
- const composioAddMatch = path.match(/^\/api\/integrations\/composio\/configs\/([^/]+)\/add$/);
2620
- if (composioAddMatch && method === "POST") {
2621
- const configId = composioAddMatch[1];
2622
- const apiKey = ProviderKeys.getDecrypted("composio");
2623
- if (!apiKey) {
2624
- return json({ error: "Composio API key not configured" }, 401);
2625
- }
2626
-
2627
- try {
2628
- // Fetch config details from Composio to get the name and mcp_url
2629
- const res = await fetch(`https://backend.composio.dev/api/v3/mcp/${configId}`, {
2630
- headers: {
2631
- "x-api-key": apiKey,
2632
- "Content-Type": "application/json",
2633
- },
2634
- });
2635
-
2636
- if (!res.ok) {
2637
- const errText = await res.text();
2638
- console.error("Failed to fetch Composio MCP config:", errText);
2639
- return json({ error: "Failed to fetch MCP config from Composio" }, 400);
2640
- }
2641
-
2642
- const data = await res.json();
2643
- const configName = data.name || `composio-${configId.slice(0, 8)}`;
2644
- const mcpUrl = data.mcp_url;
2645
- const authConfigIds = data.auth_config_ids || [];
2646
- const serverInstanceCount = data.server_instance_count || 0;
2647
-
2648
- if (!mcpUrl) {
2649
- return json({ error: "MCP config does not have a URL" }, 400);
2650
- }
2651
-
2652
- // Get user_id from connected accounts for this auth config
2653
- const { createMcpServerInstance, getUserIdForAuthConfig } = await import("../integrations/composio");
2654
- let userId: string | null = null;
2655
-
2656
- if (authConfigIds.length > 0) {
2657
- userId = await getUserIdForAuthConfig(apiKey, authConfigIds[0]);
2658
-
2659
- // Create server instance if none exists
2660
- if (serverInstanceCount === 0 && userId) {
2661
- const instance = await createMcpServerInstance(apiKey, configId, userId);
2662
- if (instance) {
2663
- console.log(`Created server instance for user ${userId} on server ${configId}`);
2664
- }
2665
- }
2666
- }
2667
-
2668
- // Append user_id to mcp_url for authentication
2669
- const mcpUrlWithUser = userId
2670
- ? `${mcpUrl}?user_id=${encodeURIComponent(userId)}`
2671
- : mcpUrl;
2672
-
2673
- // Check if already exists (match by config ID in URL)
2674
- const existing = McpServerDB.findAll().find(
2675
- s => s.source === "composio" && s.url?.includes(configId)
2676
- );
2677
- if (existing) {
2678
- return json({ server: existing, message: "Server already exists" });
2679
- }
2680
-
2681
- // Create the MCP server entry with user_id in URL
2682
- const server = McpServerDB.create({
2683
- id: generateId(),
2684
- name: configName,
2685
- type: "http",
2686
- package: null,
2687
- command: null,
2688
- args: null,
2689
- env: {},
2690
- url: mcpUrlWithUser,
2691
- headers: { "x-api-key": apiKey },
2692
- source: "composio",
2693
- });
2694
-
2695
- return json({ server, message: "Server added successfully" });
2696
- } catch (e) {
2697
- console.error("Failed to add Composio config:", e);
2698
- return json({ error: "Failed to add Composio config" }, 500);
2699
- }
2700
- }
2701
-
2702
- // POST /api/integrations/composio/configs - Create a new MCP config from connected app
2703
- if (path === "/api/integrations/composio/configs" && method === "POST") {
2704
- const apiKey = ProviderKeys.getDecrypted("composio");
2705
- if (!apiKey) {
2706
- return json({ error: "Composio API key not configured" }, 401);
2707
- }
2708
-
2709
- try {
2710
- const body = await req.json();
2711
- const { name, toolkitSlug, authConfigId } = body;
2712
-
2713
- if (!name || !toolkitSlug) {
2714
- return json({ error: "name and toolkitSlug are required" }, 400);
2715
- }
2716
-
2717
- // If authConfigId not provided, find it from the toolkit
2718
- let configId = authConfigId;
2719
- if (!configId) {
2720
- const { getAuthConfigForToolkit } = await import("../integrations/composio");
2721
- configId = await getAuthConfigForToolkit(apiKey, toolkitSlug);
2722
- if (!configId) {
2723
- return json({ error: `No auth config found for ${toolkitSlug}. Make sure you have connected this app first.` }, 400);
2724
- }
2725
- }
2726
-
2727
- // Create MCP server in Composio
2728
- const { createMcpServer, createMcpServerInstance, getUserIdForAuthConfig } = await import("../integrations/composio");
2729
- const mcpServer = await createMcpServer(apiKey, name, [configId]);
2730
-
2731
- if (!mcpServer) {
2732
- return json({ error: "Failed to create MCP config" }, 500);
2733
- }
2734
-
2735
- // Create server instance for the user who has the connected account
2736
- const userId = await getUserIdForAuthConfig(apiKey, configId);
2737
- if (userId) {
2738
- const instance = await createMcpServerInstance(apiKey, mcpServer.id, userId);
2739
- if (!instance) {
2740
- console.warn(`Created MCP server but failed to create instance for user ${userId}`);
2741
- }
2742
- }
2743
-
2744
- // Append user_id to mcp_url for authentication
2745
- const mcpUrlWithUser = userId
2746
- ? `${mcpServer.mcpUrl}?user_id=${encodeURIComponent(userId)}`
2747
- : mcpServer.mcpUrl;
2748
-
2749
- return json({
2750
- config: {
2751
- id: mcpServer.id,
2752
- name: mcpServer.name,
2753
- toolkits: mcpServer.toolkits,
2754
- mcpUrl: mcpUrlWithUser,
2755
- allowedTools: mcpServer.allowedTools,
2756
- userId,
2757
- },
2758
- }, 201);
2759
- } catch (e: any) {
2760
- console.error("Failed to create Composio MCP config:", e);
2761
- return json({ error: e.message || "Failed to create MCP config" }, 500);
2762
- }
2763
- }
2764
-
2765
- // DELETE /api/integrations/composio/configs/:id - Delete a Composio MCP config
2766
- if (composioConfigMatch && method === "DELETE") {
2767
- const configId = composioConfigMatch[1];
2768
- const apiKey = ProviderKeys.getDecrypted("composio");
2769
- if (!apiKey) {
2770
- return json({ error: "Composio API key not configured" }, 401);
2771
- }
2772
-
2773
- try {
2774
- const { deleteMcpServer } = await import("../integrations/composio");
2775
- const success = await deleteMcpServer(apiKey, configId);
2776
- if (!success) {
2777
- return json({ error: "Failed to delete MCP config" }, 500);
2778
- }
2779
- return json({ success: true });
2780
- } catch (e) {
2781
- console.error("Failed to delete Composio config:", e);
2782
- return json({ error: "Failed to delete MCP config" }, 500);
2783
- }
2784
- }
2785
-
2786
- // POST /api/mcp/servers - Create/install a new MCP server
2787
- if (path === "/api/mcp/servers" && method === "POST") {
2788
- try {
2789
- const body = await req.json();
2790
- const { name, type, package: pkg, pip_module, command, args, env, url, headers, source, project_id } = body;
2791
-
2792
- if (!name) {
2793
- return json({ error: "Name is required" }, 400);
2794
- }
2795
-
2796
- const server = McpServerDB.create({
2797
- id: generateId(),
2798
- name,
2799
- type: type || "npm",
2800
- package: pkg || null,
2801
- pip_module: pip_module || null,
2802
- command: command || null,
2803
- args: args || null,
2804
- env: env || {},
2805
- url: url || null,
2806
- headers: headers || {},
2807
- source: source || null,
2808
- project_id: project_id || null,
2809
- });
2810
-
2811
- return json({ server }, 201);
2812
- } catch (e) {
2813
- console.error("Create MCP server error:", e);
2814
- return json({ error: "Invalid request body" }, 400);
2815
- }
2816
- }
2817
-
2818
- // GET /api/mcp/servers/:id - Get a specific MCP server
2819
- const mcpServerMatch = path.match(/^\/api\/mcp\/servers\/([^/]+)$/);
2820
- if (mcpServerMatch && method === "GET") {
2821
- const server = McpServerDB.findById(mcpServerMatch[1]);
2822
- if (!server) {
2823
- return json({ error: "MCP server not found" }, 404);
2824
- }
2825
- return json({ server });
2826
- }
2827
-
2828
- // PUT /api/mcp/servers/:id - Update an MCP server
2829
- if (mcpServerMatch && method === "PUT") {
2830
- const server = McpServerDB.findById(mcpServerMatch[1]);
2831
- if (!server) {
2832
- return json({ error: "MCP server not found" }, 404);
2833
- }
2834
-
2835
- try {
2836
- const body = await req.json();
2837
- const updates: Partial<McpServer> = {};
2838
-
2839
- if (body.name !== undefined) updates.name = body.name;
2840
- if (body.type !== undefined) updates.type = body.type;
2841
- if (body.package !== undefined) updates.package = body.package;
2842
- if (body.pip_module !== undefined) updates.pip_module = body.pip_module;
2843
- if (body.command !== undefined) updates.command = body.command;
2844
- if (body.args !== undefined) updates.args = body.args;
2845
- if (body.env !== undefined) updates.env = body.env;
2846
- if (body.url !== undefined) updates.url = body.url;
2847
- if (body.headers !== undefined) updates.headers = body.headers;
2848
- if (body.project_id !== undefined) updates.project_id = body.project_id;
2849
-
2850
- const updated = McpServerDB.update(mcpServerMatch[1], updates);
2851
- return json({ server: updated });
2852
- } catch (e) {
2853
- return json({ error: "Invalid request body" }, 400);
2854
- }
2855
- }
2856
-
2857
- // DELETE /api/mcp/servers/:id - Delete an MCP server
2858
- if (mcpServerMatch && method === "DELETE") {
2859
- const server = McpServerDB.findById(mcpServerMatch[1]);
2860
- if (!server) {
2861
- return json({ error: "MCP server not found" }, 404);
2862
- }
2863
-
2864
- // Stop if running
2865
- if (server.status === "running") {
2866
- // TODO: Stop the server process
2867
- }
2868
-
2869
- McpServerDB.delete(mcpServerMatch[1]);
2870
- return json({ success: true });
2871
- }
2872
-
2873
- // POST /api/mcp/servers/:id/start - Start an MCP server
2874
- const mcpStartMatch = path.match(/^\/api\/mcp\/servers\/([^/]+)\/start$/);
2875
- if (mcpStartMatch && method === "POST") {
2876
- const server = McpServerDB.findById(mcpStartMatch[1]);
2877
- if (!server) {
2878
- return json({ error: "MCP server not found" }, 404);
2879
- }
2880
-
2881
- if (server.status === "running") {
2882
- return json({ error: "MCP server already running" }, 400);
2883
- }
2884
-
2885
- // Determine command to run
2886
- // Helper to substitute $ENV_VAR references with actual values
2887
- const substituteEnvVars = (str: string, env: Record<string, string>): string => {
2888
- return str.replace(/\$([A-Z_][A-Z0-9_]*)/g, (_, varName) => {
2889
- return env[varName] || '';
2890
- });
2891
- };
2892
-
2893
- let cmd: string[];
2894
- const serverEnv = server.env || {};
2895
-
2896
- if (server.command) {
2897
- // Custom command - substitute env vars in args
2898
- cmd = server.command.split(" ");
2899
- if (server.args) {
2900
- const substitutedArgs = substituteEnvVars(server.args, serverEnv);
2901
- cmd.push(...substitutedArgs.split(" "));
2902
- }
2903
- } else if (server.type === "pip" && server.package) {
2904
- // Python pip package - install first, then run module
2905
- const pipPackage = server.package;
2906
- const pipModule = server.pip_module || server.package.split("[")[0]; // Default: package name without extras
2907
-
2908
- console.log(`Installing pip package: ${pipPackage}...`);
2909
- const installResult = spawn({
2910
- cmd: ["pip", "install", "--quiet", "--break-system-packages", pipPackage],
2911
- env: { ...process.env as Record<string, string>, ...serverEnv },
2912
- stdout: "pipe",
2913
- stderr: "pipe",
2914
- });
2915
-
2916
- // Wait for installation to complete
2917
- const exitCode = await installResult.exited;
2918
- if (exitCode !== 0) {
2919
- const stderr = await new Response(installResult.stderr).text();
2920
- return json({ error: `Failed to install pip package: ${stderr || "unknown error"}` }, 500);
2921
- }
2922
-
2923
- // Now run the module
2924
- cmd = ["python", "-m", pipModule];
2925
- if (server.args) {
2926
- const substitutedArgs = substituteEnvVars(server.args, serverEnv);
2927
- cmd.push(...substitutedArgs.split(" "));
2928
- }
2929
- } else if (server.package) {
2930
- // npm package - use npx
2931
- cmd = ["npx", "-y", server.package];
2932
- if (server.args) {
2933
- const substitutedArgs = substituteEnvVars(server.args, serverEnv);
2934
- cmd.push(...substitutedArgs.split(" "));
2935
- }
2936
- } else {
2937
- return json({ error: "No command or package specified" }, 400);
2938
- }
2939
-
2940
- // Get a port for the HTTP proxy
2941
- const port = await getNextPort();
2942
-
2943
- console.log(`Starting MCP server ${server.name}...`);
2944
- console.log(` Command: ${cmd.join(" ")}`);
2945
- console.log(` HTTP proxy: http://localhost:${port}/mcp`);
2946
-
2947
- // Start the MCP process with stdio pipes + HTTP proxy
2948
- const result = await startMcpProcess(server.id, cmd, server.env || {}, port);
2949
-
2950
- if (!result.success) {
2951
- console.error(`Failed to start MCP server: ${result.error}`);
2952
- return json({ error: `Failed to start: ${result.error}` }, 500);
2953
- }
2954
-
2955
- // Update status with the HTTP proxy port
2956
- const updated = McpServerDB.setStatus(server.id, "running", port);
2957
-
2958
- return json({
2959
- server: updated,
2960
- message: "MCP server started",
2961
- proxyUrl: `http://localhost:${port}/mcp`,
2962
- });
2963
- }
2964
-
2965
- // POST /api/mcp/servers/:id/stop - Stop an MCP server
2966
- const mcpStopMatch = path.match(/^\/api\/mcp\/servers\/([^/]+)\/stop$/);
2967
- if (mcpStopMatch && method === "POST") {
2968
- const server = McpServerDB.findById(mcpStopMatch[1]);
2969
- if (!server) {
2970
- return json({ error: "MCP server not found" }, 404);
2971
- }
2972
-
2973
- // Stop the MCP process
2974
- stopMcpProcess(server.id);
2975
-
2976
- const updated = McpServerDB.setStatus(server.id, "stopped");
2977
- return json({ server: updated, message: "MCP server stopped" });
2978
- }
2979
-
2980
- // GET /api/mcp/servers/:id/tools - List tools from an MCP server
2981
- const mcpToolsMatch = path.match(/^\/api\/mcp\/servers\/([^/]+)\/tools$/);
2982
- if (mcpToolsMatch && method === "GET") {
2983
- const server = McpServerDB.findById(mcpToolsMatch[1]);
2984
- if (!server) {
2985
- return json({ error: "MCP server not found" }, 404);
2986
- }
2987
-
2988
- // HTTP servers use remote HTTP transport
2989
- if (server.type === "http" && server.url) {
2990
- try {
2991
- const httpClient = getHttpMcpClient(server.url, server.headers || {});
2992
- const serverInfo = await httpClient.initialize();
2993
- const tools = await httpClient.listTools();
2994
-
2995
- return json({
2996
- serverInfo,
2997
- tools,
2998
- });
2999
- } catch (err) {
3000
- console.error(`Failed to list HTTP MCP tools: ${err}`);
3001
- return json({ error: `Failed to communicate with MCP server: ${err}` }, 500);
3002
- }
3003
- }
3004
-
3005
- // Stdio servers require a running process
3006
- const mcpProcess = getMcpProcess(server.id);
3007
- if (!mcpProcess) {
3008
- return json({ error: "MCP server is not running" }, 400);
3009
- }
3010
-
3011
- try {
3012
- const serverInfo = await initializeMcpServer(server.id);
3013
- const tools = await listMcpTools(server.id);
3014
-
3015
- return json({
3016
- serverInfo,
3017
- tools,
3018
- });
3019
- } catch (err) {
3020
- console.error(`Failed to list MCP tools: ${err}`);
3021
- return json({ error: `Failed to communicate with MCP server: ${err}` }, 500);
3022
- }
3023
- }
3024
-
3025
- // POST /api/mcp/servers/:id/tools/:toolName/call - Call a tool on an MCP server
3026
- const mcpToolCallMatch = path.match(/^\/api\/mcp\/servers\/([^/]+)\/tools\/([^/]+)\/call$/);
3027
- if (mcpToolCallMatch && method === "POST") {
3028
- const server = McpServerDB.findById(mcpToolCallMatch[1]);
3029
- if (!server) {
3030
- return json({ error: "MCP server not found" }, 404);
3031
- }
3032
-
3033
- const toolName = decodeURIComponent(mcpToolCallMatch[2]);
3034
-
3035
- // HTTP servers use remote HTTP transport
3036
- if (server.type === "http" && server.url) {
3037
- try {
3038
- const body = await req.json();
3039
- const args = body.arguments || {};
3040
-
3041
- const httpClient = getHttpMcpClient(server.url, server.headers || {});
3042
- const result = await httpClient.callTool(toolName, args);
3043
-
3044
- return json({ result });
3045
- } catch (err) {
3046
- console.error(`Failed to call HTTP MCP tool: ${err}`);
3047
- return json({ error: `Failed to call tool: ${err}` }, 500);
3048
- }
3049
- }
3050
-
3051
- // Stdio servers require a running process
3052
- const mcpProcess = getMcpProcess(server.id);
3053
- if (!mcpProcess) {
3054
- return json({ error: "MCP server is not running" }, 400);
3055
- }
3056
-
3057
- try {
3058
- const body = await req.json();
3059
- const args = body.arguments || {};
3060
-
3061
- const result = await callMcpTool(server.id, toolName, args);
3062
-
3063
- return json({ result });
3064
- } catch (err) {
3065
- console.error(`Failed to call MCP tool: ${err}`);
3066
- return json({ error: `Failed to call tool: ${err}` }, 500);
3067
- }
3068
- }
3069
-
3070
- // ============ Skills Endpoints ============
3071
-
3072
- // GET /api/skills - List skills (optionally filtered by project)
3073
- if (path === "/api/skills" && method === "GET") {
3074
- const url = new URL(req.url);
3075
- const projectFilter = url.searchParams.get("project"); // "all", "global", or project ID
3076
- const forAgent = url.searchParams.get("forAgent"); // agent's project ID (shows global + project)
3077
-
3078
- let skills;
3079
- if (forAgent !== null) {
3080
- // Get skills available for an agent (global + agent's project)
3081
- skills = SkillDB.findForAgent(forAgent || null);
3082
- } else if (projectFilter === "global") {
3083
- skills = SkillDB.findGlobal();
3084
- } else if (projectFilter && projectFilter !== "all") {
3085
- skills = SkillDB.findByProject(projectFilter);
3086
- } else {
3087
- skills = SkillDB.findAll();
3088
- }
3089
- return json({ skills });
3090
- }
3091
-
3092
- // POST /api/skills - Create a new skill
3093
- if (path === "/api/skills" && method === "POST") {
3094
- try {
3095
- const body = await req.json();
3096
- const { name, description, content, version, license, compatibility, metadata, allowed_tools, source, source_url, enabled, project_id } = body;
3097
-
3098
- if (!name || !description || !content) {
3099
- return json({ error: "name, description, and content are required" }, 400);
3100
- }
3101
-
3102
- // Validate name format (lowercase, hyphens only)
3103
- if (!/^[a-z][a-z0-9-]*[a-z0-9]$|^[a-z]$/.test(name)) {
3104
- return json({ error: "name must be lowercase letters, numbers, and hyphens only" }, 400);
3105
- }
3106
-
3107
- if (SkillDB.exists(name)) {
3108
- return json({ error: "A skill with this name already exists" }, 400);
3109
- }
3110
-
3111
- const skill = SkillDB.create({
3112
- name,
3113
- description,
3114
- content,
3115
- version: version || "1.0.0",
3116
- license: license || null,
3117
- compatibility: compatibility || null,
3118
- metadata: metadata || {},
3119
- allowed_tools: allowed_tools || [],
3120
- source: source || "local",
3121
- source_url: source_url || null,
3122
- enabled: enabled !== false,
3123
- project_id: project_id || null,
3124
- });
3125
-
3126
- return json({ skill }, 201);
3127
- } catch (err) {
3128
- console.error("Failed to create skill:", err);
3129
- return json({ error: `Failed to create skill: ${err}` }, 500);
3130
- }
3131
- }
3132
-
3133
- // GET /api/skills/:id - Get a skill
3134
- const skillMatch = path.match(/^\/api\/skills\/([^/]+)$/);
3135
- if (skillMatch && method === "GET") {
3136
- const skill = SkillDB.findById(skillMatch[1]);
3137
- if (!skill) {
3138
- return json({ error: "Skill not found" }, 404);
3139
- }
3140
- return json({ skill });
3141
- }
3142
-
3143
- // PUT /api/skills/:id - Update a skill
3144
- if (skillMatch && method === "PUT") {
3145
- const skill = SkillDB.findById(skillMatch[1]);
3146
- if (!skill) {
3147
- return json({ error: "Skill not found" }, 404);
3148
- }
3149
-
3150
- try {
3151
- const body = await req.json();
3152
- const updates: Partial<Skill> = {};
3153
-
3154
- if (body.name !== undefined) updates.name = body.name;
3155
- if (body.description !== undefined) updates.description = body.description;
3156
- if (body.content !== undefined) updates.content = body.content;
3157
- if (body.license !== undefined) updates.license = body.license;
3158
- if (body.compatibility !== undefined) updates.compatibility = body.compatibility;
3159
- if (body.metadata !== undefined) updates.metadata = body.metadata;
3160
- if (body.allowed_tools !== undefined) updates.allowed_tools = body.allowed_tools;
3161
- if (body.enabled !== undefined) updates.enabled = body.enabled;
3162
- if (body.project_id !== undefined) updates.project_id = body.project_id;
3163
-
3164
- // Auto-increment version if content changed
3165
- if (body.content !== undefined && body.content !== skill.content) {
3166
- const [major, minor, patch] = (skill.version || "1.0.0").split(".").map(Number);
3167
- updates.version = `${major}.${minor}.${patch + 1}`;
3168
- } else if (body.version !== undefined) {
3169
- updates.version = body.version;
3170
- }
3171
-
3172
- const updated = SkillDB.update(skillMatch[1], updates);
3173
-
3174
- // Push updated skill to all running agents that have it
3175
- const agentsWithSkill = AgentDB.findBySkill(skillMatch[1]);
3176
- const runningAgents = agentsWithSkill.filter(a => a.status === "running" && a.port);
3177
-
3178
- for (const agent of runningAgents) {
3179
- try {
3180
- const providerKey = ProviderKeys.getDecrypted(agent.provider);
3181
- if (providerKey) {
3182
- const config = buildAgentConfig(agent, providerKey);
3183
- await pushConfigToAgent(agent.id, agent.port!, config);
3184
- // Push skills via /skills endpoint
3185
- if (config.skills?.definitions?.length > 0) {
3186
- await pushSkillsToAgent(agent.id, agent.port!, config.skills.definitions);
3187
- }
3188
- console.log(`Pushed skill update to agent ${agent.name}`);
3189
- }
3190
- } catch (err) {
3191
- console.error(`Failed to push skill update to agent ${agent.name}:`, err);
3192
- }
3193
- }
3194
-
3195
- return json({ skill: updated, agents_updated: runningAgents.length });
3196
- } catch (err) {
3197
- console.error("Failed to update skill:", err);
3198
- return json({ error: `Failed to update skill: ${err}` }, 500);
3199
- }
3200
- }
3201
-
3202
- // DELETE /api/skills/:id - Delete a skill
3203
- if (skillMatch && method === "DELETE") {
3204
- const skill = SkillDB.findById(skillMatch[1]);
3205
- if (!skill) {
3206
- return json({ error: "Skill not found" }, 404);
3207
- }
3208
-
3209
- SkillDB.delete(skillMatch[1]);
3210
- return json({ success: true });
3211
- }
3212
-
3213
- // POST /api/skills/:id/toggle - Toggle skill enabled/disabled
3214
- const skillToggleMatch = path.match(/^\/api\/skills\/([^/]+)\/toggle$/);
3215
- if (skillToggleMatch && method === "POST") {
3216
- const skill = SkillDB.findById(skillToggleMatch[1]);
3217
- if (!skill) {
3218
- return json({ error: "Skill not found" }, 404);
3219
- }
3220
-
3221
- const updated = SkillDB.setEnabled(skillToggleMatch[1], !skill.enabled);
3222
- return json({ skill: updated });
3223
- }
3224
-
3225
- // POST /api/skills/import - Import a skill from SKILL.md content
3226
- if (path === "/api/skills/import" && method === "POST") {
3227
- try {
3228
- const body = await req.json();
3229
- const { content, source, source_url } = body;
3230
-
3231
- if (!content) {
3232
- return json({ error: "content is required" }, 400);
3233
- }
3234
-
3235
- const parsed = parseSkillMd(content);
3236
- if (!parsed) {
3237
- return json({ error: "Invalid SKILL.md format. Must have YAML frontmatter with name and description." }, 400);
3238
- }
3239
-
3240
- if (SkillDB.exists(parsed.name)) {
3241
- return json({ error: `A skill named "${parsed.name}" already exists` }, 400);
3242
- }
3243
-
3244
- const skill = SkillDB.create({
3245
- name: parsed.name,
3246
- description: parsed.description,
3247
- content: content, // Store full content including frontmatter
3248
- license: parsed.license || null,
3249
- compatibility: parsed.compatibility || null,
3250
- metadata: parsed.metadata || {},
3251
- allowed_tools: parsed.allowedTools || [],
3252
- source: source || "import",
3253
- source_url: source_url || null,
3254
- enabled: true,
3255
- });
3256
-
3257
- return json({ skill }, 201);
3258
- } catch (err) {
3259
- console.error("Failed to import skill:", err);
3260
- return json({ error: `Failed to import skill: ${err}` }, 500);
3261
- }
3262
- }
3263
-
3264
- // GET /api/skills/:id/export - Export a skill as SKILL.md
3265
- const skillExportMatch = path.match(/^\/api\/skills\/([^/]+)\/export$/);
3266
- if (skillExportMatch && method === "GET") {
3267
- const skill = SkillDB.findById(skillExportMatch[1]);
3268
- if (!skill) {
3269
- return json({ error: "Skill not found" }, 404);
3270
- }
3271
-
3272
- // Return the raw content
3273
- return new Response(skill.content, {
3274
- headers: {
3275
- "Content-Type": "text/markdown",
3276
- "Content-Disposition": `attachment; filename="${skill.name}-SKILL.md"`,
3277
- },
3278
- });
3279
- }
3280
-
3281
- // ============ SkillsMP Marketplace Endpoints ============
3282
-
3283
- // GET /api/skills/marketplace/search - Search skills marketplace
3284
- if (path === "/api/skills/marketplace/search" && method === "GET") {
3285
- const url = new URL(req.url);
3286
- const query = url.searchParams.get("q") || "";
3287
- const page = parseInt(url.searchParams.get("page") || "1", 10);
3288
-
3289
- // Get SkillsMP API key if configured
3290
- const skillsmpKey = ProviderKeys.getDecrypted("skillsmp");
3291
-
3292
- const result = await SkillsmpProvider.search(skillsmpKey || "", query, page);
3293
- return json(result);
3294
- }
3295
-
3296
- // GET /api/skills/marketplace/featured - Get featured skills
3297
- if (path === "/api/skills/marketplace/featured" && method === "GET") {
3298
- const skillsmpKey = ProviderKeys.getDecrypted("skillsmp");
3299
- const skills = await SkillsmpProvider.getFeatured(skillsmpKey || "");
3300
- return json({ skills });
3301
- }
3302
-
3303
- // GET /api/skills/marketplace/:id - Get skill details from marketplace
3304
- const marketplaceSkillMatch = path.match(/^\/api\/skills\/marketplace\/([^/]+)$/);
3305
- if (marketplaceSkillMatch && method === "GET") {
3306
- const skillsmpKey = ProviderKeys.getDecrypted("skillsmp");
3307
- const skill = await SkillsmpProvider.getSkill(skillsmpKey || "", marketplaceSkillMatch[1]);
3308
- if (!skill) {
3309
- return json({ error: "Skill not found in marketplace" }, 404);
3310
- }
3311
- return json({ skill });
3312
- }
3313
-
3314
- // POST /api/skills/marketplace/:id/install - Install a skill from marketplace
3315
- const marketplaceInstallMatch = path.match(/^\/api\/skills\/marketplace\/([^/]+)\/install$/);
3316
- if (marketplaceInstallMatch && method === "POST") {
3317
- const skillsmpKey = ProviderKeys.getDecrypted("skillsmp");
3318
- const marketplaceSkill = await SkillsmpProvider.getSkill(skillsmpKey || "", marketplaceInstallMatch[1]);
3319
-
3320
- if (!marketplaceSkill) {
3321
- return json({ error: "Skill not found in marketplace" }, 404);
3322
- }
3323
-
3324
- if (SkillDB.exists(marketplaceSkill.name)) {
3325
- return json({ error: `A skill named "${marketplaceSkill.name}" already exists` }, 400);
3326
- }
3327
-
3328
- const skill = SkillDB.create({
3329
- name: marketplaceSkill.name,
3330
- description: marketplaceSkill.description,
3331
- content: marketplaceSkill.content,
3332
- license: marketplaceSkill.license,
3333
- compatibility: marketplaceSkill.compatibility,
3334
- metadata: {
3335
- author: marketplaceSkill.author,
3336
- version: marketplaceSkill.version,
3337
- ...(marketplaceSkill.repository ? { repository: marketplaceSkill.repository } : {}),
3338
- },
3339
- allowed_tools: [],
3340
- source: "skillsmp",
3341
- source_url: marketplaceSkill.repository || `https://skillsmp.com/skills/${marketplaceSkill.id}`,
3342
- enabled: true,
3343
- });
3344
-
3345
- return json({ skill }, 201);
3346
- }
3347
-
3348
- // ============ Telemetry Endpoints ============
3349
-
3350
- // POST /api/telemetry - Receive telemetry events from agents
3351
- if (path === "/api/telemetry" && method === "POST") {
3352
- try {
3353
- const body = await req.json() as {
3354
- agent_id: string;
3355
- sent_at: string;
3356
- events: Array<{
3357
- id: string;
3358
- timestamp: string;
3359
- category: string;
3360
- type: string;
3361
- level: string;
3362
- trace_id?: string;
3363
- span_id?: string;
3364
- thread_id?: string;
3365
- data?: Record<string, unknown>;
3366
- metadata?: Record<string, unknown>;
3367
- duration_ms?: number;
3368
- error?: string;
3369
- }>;
3370
- };
3371
-
3372
- if (!body.agent_id || !body.events) {
3373
- return json({ error: "agent_id and events are required" }, 400);
3374
- }
3375
-
3376
- // Filter out debug events - too noisy
3377
- const filteredEvents = body.events.filter(e => e.level !== "debug");
3378
- const inserted = TelemetryDB.insertBatch(body.agent_id, filteredEvents);
3379
-
3380
- // Broadcast to SSE clients
3381
- if (filteredEvents.length > 0) {
3382
- const broadcastEvents: TelemetryEvent[] = filteredEvents.map(e => ({
3383
- id: e.id,
3384
- agent_id: body.agent_id,
3385
- timestamp: e.timestamp,
3386
- category: e.category,
3387
- type: e.type,
3388
- level: e.level,
3389
- trace_id: e.trace_id,
3390
- thread_id: e.thread_id,
3391
- data: e.data,
3392
- duration_ms: e.duration_ms,
3393
- error: e.error,
3394
- }));
3395
- telemetryBroadcaster.broadcast(broadcastEvents);
3396
- }
3397
-
3398
- return json({ received: body.events.length, inserted });
3399
- } catch (e) {
3400
- console.error("Telemetry error:", e);
3401
- return json({ error: "Invalid telemetry payload" }, 400);
3402
- }
3403
- }
3404
-
3405
- // GET /api/telemetry/stream - SSE stream for real-time telemetry
3406
- if (path === "/api/telemetry/stream" && method === "GET") {
3407
- let controller: ReadableStreamDefaultController<string>;
3408
-
3409
- const stream = new ReadableStream<string>({
3410
- start(c) {
3411
- controller = c;
3412
- telemetryBroadcaster.addClient(controller);
3413
- // Send initial connection message
3414
- controller.enqueue("data: {\"connected\":true}\n\n");
3415
- },
3416
- cancel() {
3417
- telemetryBroadcaster.removeClient(controller);
3418
- },
3419
- });
3420
-
3421
- return new Response(stream, {
3422
- headers: {
3423
- "Content-Type": "text/event-stream",
3424
- "Cache-Control": "no-cache, no-transform",
3425
- "Connection": "keep-alive",
3426
- "X-Accel-Buffering": "no",
3427
- },
3428
- });
3429
- }
3430
-
3431
- // GET /api/telemetry/events - Query telemetry events
3432
- if (path === "/api/telemetry/events" && method === "GET") {
3433
- const url = new URL(req.url);
3434
- const projectIdParam = url.searchParams.get("project_id");
3435
- const events = TelemetryDB.query({
3436
- agent_id: url.searchParams.get("agent_id") || undefined,
3437
- project_id: projectIdParam === "null" ? null : projectIdParam || undefined,
3438
- category: url.searchParams.get("category") || undefined,
3439
- level: url.searchParams.get("level") || undefined,
3440
- trace_id: url.searchParams.get("trace_id") || undefined,
3441
- since: url.searchParams.get("since") || undefined,
3442
- until: url.searchParams.get("until") || undefined,
3443
- limit: parseInt(url.searchParams.get("limit") || "100"),
3444
- offset: parseInt(url.searchParams.get("offset") || "0"),
3445
- });
3446
- return json({ events });
3447
- }
3448
-
3449
- // GET /api/telemetry/usage - Get usage statistics
3450
- if (path === "/api/telemetry/usage" && method === "GET") {
3451
- const url = new URL(req.url);
3452
- const projectIdParam = url.searchParams.get("project_id");
3453
- const usage = TelemetryDB.getUsage({
3454
- agent_id: url.searchParams.get("agent_id") || undefined,
3455
- project_id: projectIdParam === "null" ? null : projectIdParam || undefined,
3456
- since: url.searchParams.get("since") || undefined,
3457
- until: url.searchParams.get("until") || undefined,
3458
- group_by: (url.searchParams.get("group_by") as "agent" | "day") || undefined,
3459
- });
3460
- return json({ usage });
3461
- }
3462
-
3463
- // GET /api/telemetry/stats - Get summary statistics
3464
- if (path === "/api/telemetry/stats" && method === "GET") {
3465
- const url = new URL(req.url);
3466
- const agentId = url.searchParams.get("agent_id") || undefined;
3467
- const projectIdParam = url.searchParams.get("project_id");
3468
- const stats = TelemetryDB.getStats({
3469
- agentId,
3470
- projectId: projectIdParam === "null" ? null : projectIdParam || undefined,
3471
- });
3472
- return json({ stats });
3473
- }
3474
-
3475
- // POST /api/telemetry/clear - Clear all telemetry data
3476
- if (path === "/api/telemetry/clear" && method === "POST") {
3477
- const deleted = TelemetryDB.deleteOlderThan(0); // Delete all
3478
- return json({ deleted });
3479
- }
3480
23
 
3481
- return json({ error: "Not found" }, 404);
24
+ return (
25
+ (await handleSystemRoutes(req, path, method, authContext)) ??
26
+ (await handleProviderRoutes(req, path, method, authContext)) ??
27
+ (await handleUserRoutes(req, path, method, authContext)) ??
28
+ (await handleProjectRoutes(req, path, method, authContext)) ??
29
+ (await handleAgentRoutes(req, path, method, authContext)) ??
30
+ (await handleMcpRoutes(req, path, method)) ??
31
+ (await handleSkillRoutes(req, path, method)) ??
32
+ (await handleIntegrationRoutes(req, path, method)) ??
33
+ (await handleMetaAgentRoutes(req, path, method)) ??
34
+ (await handleTelemetryRoutes(req, path, method)) ??
35
+ json({ error: "Not found" }, 404)
36
+ );
3482
37
  }