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/dist/App.csbvbyak.js +227 -0
- package/dist/index.html +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/db.ts +98 -19
- package/src/integrations/agentdojo.ts +350 -0
- package/src/openapi.ts +195 -0
- package/src/providers.ts +78 -7
- package/src/routes/api/agent-utils.ts +638 -0
- package/src/routes/api/agents.ts +743 -0
- package/src/routes/api/helpers.ts +12 -0
- package/src/routes/api/integrations.ts +608 -0
- package/src/routes/api/mcp.ts +377 -0
- package/src/routes/api/meta-agent.ts +145 -0
- package/src/routes/api/projects.ts +95 -0
- package/src/routes/api/providers.ts +269 -0
- package/src/routes/api/skills.ts +538 -0
- package/src/routes/api/system.ts +215 -0
- package/src/routes/api/telemetry.ts +143 -0
- package/src/routes/api/users.ts +148 -0
- package/src/routes/api.ts +32 -3477
- package/src/server.ts +1 -1
- package/src/web/components/api/ApiDocsPage.tsx +259 -0
- package/src/web/components/dashboard/Dashboard.tsx +92 -3
- package/src/web/components/mcp/IntegrationsPanel.tsx +15 -8
- package/src/web/components/mcp/McpPage.tsx +458 -174
- package/src/web/components/settings/SettingsPage.tsx +275 -36
- package/src/web/components/skills/SkillsPage.tsx +330 -1
- package/src/web/components/tasks/TasksPage.tsx +187 -58
- package/src/web/context/TelemetryContext.tsx +14 -1
- package/src/web/hooks/useAgents.ts +9 -0
- package/src/web/types.ts +22 -4
- package/dist/App.mbp9atpm.js +0 -227
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
} from "
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
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
|
}
|