apteva 0.2.11 → 0.3.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.mvbdnw89.js +227 -0
- package/dist/index.html +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/auth/index.ts +11 -3
- package/src/auth/middleware.ts +1 -0
- package/src/crypto.ts +4 -0
- package/src/db.ts +437 -14
- package/src/integrations/skillsmp.ts +318 -0
- package/src/providers.ts +21 -0
- package/src/routes/api.ts +836 -16
- package/src/server.ts +58 -7
- package/src/web/App.tsx +24 -8
- package/src/web/components/agents/AgentCard.tsx +36 -11
- package/src/web/components/agents/AgentPanel.tsx +333 -24
- package/src/web/components/agents/AgentsView.tsx +1 -1
- package/src/web/components/agents/CreateAgentModal.tsx +169 -23
- package/src/web/components/common/Icons.tsx +8 -0
- package/src/web/components/common/index.ts +1 -0
- package/src/web/components/index.ts +1 -0
- package/src/web/components/layout/Header.tsx +4 -2
- package/src/web/components/layout/Sidebar.tsx +7 -1
- package/src/web/components/mcp/McpPage.tsx +602 -19
- package/src/web/components/meta-agent/MetaAgent.tsx +222 -0
- package/src/web/components/settings/SettingsPage.tsx +212 -150
- package/src/web/components/skills/SkillsPage.tsx +871 -0
- package/src/web/context/AuthContext.tsx +5 -0
- package/src/web/context/ProjectContext.tsx +26 -4
- package/src/web/types.ts +48 -3
- package/dist/App.44ge5b89.js +0 -218
package/src/routes/api.ts
CHANGED
|
@@ -2,8 +2,8 @@ import { spawn } from "bun";
|
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
import { homedir } from "os";
|
|
4
4
|
import { mkdirSync, existsSync, rmSync } from "fs";
|
|
5
|
-
import { agentProcesses, agentsStarting,
|
|
6
|
-
import { AgentDB, McpServerDB, TelemetryDB, UserDB, ProjectDB, generateId, type Agent, type AgentFeatures, type McpServer, type Project } from "../db";
|
|
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
7
|
import { ProviderKeys, Onboarding, getProvidersWithStatus, PROVIDERS, type ProviderId } from "../providers";
|
|
8
8
|
import { createUser, hashPassword, validatePassword } from "../auth";
|
|
9
9
|
import type { AuthContext } from "../auth/middleware";
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
import { openApiSpec } from "../openapi";
|
|
29
29
|
import { getProvider, getProviderIds, registerProvider } from "../integrations";
|
|
30
30
|
import { ComposioProvider } from "../integrations/composio";
|
|
31
|
+
import { SkillsmpProvider, parseSkillMd, type SkillsmpSkill } from "../integrations/skillsmp";
|
|
31
32
|
|
|
32
33
|
// Register integration providers
|
|
33
34
|
registerProvider(ComposioProvider);
|
|
@@ -37,6 +38,10 @@ const AGENTS_DATA_DIR = process.env.DATA_DIR
|
|
|
37
38
|
? join(process.env.DATA_DIR, "agents")
|
|
38
39
|
: join(homedir(), ".apteva", "agents");
|
|
39
40
|
|
|
41
|
+
// Meta Agent configuration
|
|
42
|
+
const META_AGENT_ENABLED = process.env.META_AGENT_ENABLED === "true";
|
|
43
|
+
const META_AGENT_ID = "apteva-assistant";
|
|
44
|
+
|
|
40
45
|
function json(data: unknown, status = 200): Response {
|
|
41
46
|
return new Response(JSON.stringify(data), {
|
|
42
47
|
status,
|
|
@@ -66,6 +71,22 @@ async function waitForAgentHealth(port: number, maxAttempts = 30, delayMs = 200)
|
|
|
66
71
|
return false;
|
|
67
72
|
}
|
|
68
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
|
+
|
|
69
90
|
// Make authenticated request to agent
|
|
70
91
|
async function agentFetch(
|
|
71
92
|
agentId: string,
|
|
@@ -94,6 +115,34 @@ function buildAgentConfig(agent: Agent, providerKey: string) {
|
|
|
94
115
|
// Get MCP server details for the agent's selected servers
|
|
95
116
|
const mcpServers: Array<{ name: string; type: "http"; url: string; headers: Record<string, string>; enabled: boolean }> = [];
|
|
96
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
|
+
|
|
97
146
|
for (const id of agent.mcp_servers || []) {
|
|
98
147
|
const server = McpServerDB.findById(id);
|
|
99
148
|
if (!server) continue;
|
|
@@ -123,6 +172,7 @@ function buildAgentConfig(agent: Agent, providerKey: string) {
|
|
|
123
172
|
id: agent.id,
|
|
124
173
|
name: agent.name,
|
|
125
174
|
description: agent.system_prompt,
|
|
175
|
+
public_url: `http://localhost:${agent.port}`,
|
|
126
176
|
llm: {
|
|
127
177
|
provider: agent.provider,
|
|
128
178
|
model: agent.model,
|
|
@@ -148,6 +198,10 @@ function buildAgentConfig(agent: Agent, providerKey: string) {
|
|
|
148
198
|
max_concurrent: 10,
|
|
149
199
|
},
|
|
150
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
|
+
],
|
|
151
205
|
},
|
|
152
206
|
tasks: {
|
|
153
207
|
enabled: features.tasks,
|
|
@@ -215,6 +269,23 @@ function buildAgentConfig(agent: Agent, providerKey: string) {
|
|
|
215
269
|
flush_interval: 1, // Every 1 second
|
|
216
270
|
categories: [], // Empty = all categories
|
|
217
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
|
+
})(),
|
|
218
289
|
};
|
|
219
290
|
}
|
|
220
291
|
|
|
@@ -238,6 +309,68 @@ async function pushConfigToAgent(agentId: string, port: number, config: any): Pr
|
|
|
238
309
|
}
|
|
239
310
|
}
|
|
240
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
|
+
|
|
241
374
|
// Exported helper to start an agent process (used by API route and auto-restart)
|
|
242
375
|
export async function startAgentProcess(
|
|
243
376
|
agent: Agent,
|
|
@@ -302,13 +435,37 @@ export async function startAgentProcess(
|
|
|
302
435
|
}
|
|
303
436
|
try {
|
|
304
437
|
await fetch(`http://localhost:${port}/shutdown`, { method: "POST", signal: AbortSignal.timeout(1000) });
|
|
305
|
-
await new Promise(r => setTimeout(r, 500)); // Wait for shutdown
|
|
306
438
|
} catch {
|
|
307
439
|
// Shutdown failed - process might not support it
|
|
308
440
|
}
|
|
441
|
+
// Wait longer for port to be released
|
|
442
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
309
443
|
}
|
|
310
444
|
} catch {
|
|
311
|
-
//
|
|
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
|
+
}
|
|
312
469
|
}
|
|
313
470
|
|
|
314
471
|
// Handle data directory
|
|
@@ -331,19 +488,25 @@ export async function startAgentProcess(
|
|
|
331
488
|
}
|
|
332
489
|
|
|
333
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");
|
|
334
493
|
const env: Record<string, string> = {
|
|
335
494
|
...process.env as Record<string, string>,
|
|
336
495
|
PORT: String(port),
|
|
337
496
|
DATA_DIR: agentDataDir,
|
|
497
|
+
CONFIG_PATH: agentConfigPath,
|
|
338
498
|
AGENT_API_KEY: agentApiKey,
|
|
339
499
|
[providerConfig.envVar]: providerKey,
|
|
340
500
|
};
|
|
341
501
|
|
|
502
|
+
// Get binary path dynamically (allows hot-reload of new binary versions)
|
|
503
|
+
const binaryPath = getBinaryPathForAgent();
|
|
504
|
+
|
|
342
505
|
const proc = spawn({
|
|
343
|
-
cmd: [
|
|
506
|
+
cmd: [binaryPath],
|
|
344
507
|
env,
|
|
345
|
-
stdout: "
|
|
346
|
-
stderr: "
|
|
508
|
+
stdout: "inherit",
|
|
509
|
+
stderr: "inherit",
|
|
347
510
|
});
|
|
348
511
|
|
|
349
512
|
// Store process with port for tracking
|
|
@@ -379,6 +542,16 @@ export async function startAgentProcess(
|
|
|
379
542
|
console.log(` Configuration applied successfully`);
|
|
380
543
|
}
|
|
381
544
|
|
|
545
|
+
// Push skills via /skills endpoint (separate from config)
|
|
546
|
+
if (config.skills?.definitions?.length > 0) {
|
|
547
|
+
const skillsResult = await pushSkillsToAgent(agent.id, port, config.skills.definitions);
|
|
548
|
+
if (!skillsResult.success && !silent) {
|
|
549
|
+
console.error(` Failed to push skills: ${skillsResult.error}`);
|
|
550
|
+
} else if (!silent) {
|
|
551
|
+
console.log(` Skills pushed successfully (${config.skills.definitions.length} skills)`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
382
555
|
// Update status in database (port is already set, just update status)
|
|
383
556
|
AgentDB.setStatus(agent.id, "running");
|
|
384
557
|
|
|
@@ -409,6 +582,19 @@ function toApiAgent(agent: Agent) {
|
|
|
409
582
|
type: s.type,
|
|
410
583
|
status: s.status,
|
|
411
584
|
port: s.port,
|
|
585
|
+
url: s.url, // Include URL for HTTP servers
|
|
586
|
+
}));
|
|
587
|
+
|
|
588
|
+
// Look up skill details
|
|
589
|
+
const skillDetails = (agent.skills || [])
|
|
590
|
+
.map(id => SkillDB.findById(id))
|
|
591
|
+
.filter((s): s is NonNullable<typeof s> => s !== null)
|
|
592
|
+
.map(s => ({
|
|
593
|
+
id: s.id,
|
|
594
|
+
name: s.name,
|
|
595
|
+
description: s.description,
|
|
596
|
+
version: s.version,
|
|
597
|
+
enabled: s.enabled,
|
|
412
598
|
}));
|
|
413
599
|
|
|
414
600
|
return {
|
|
@@ -422,6 +608,8 @@ function toApiAgent(agent: Agent) {
|
|
|
422
608
|
features: agent.features,
|
|
423
609
|
mcpServers: agent.mcp_servers, // Keep IDs for backwards compatibility
|
|
424
610
|
mcpServerDetails, // Include full details
|
|
611
|
+
skills: agent.skills, // Skill IDs
|
|
612
|
+
skillDetails, // Include full details
|
|
425
613
|
projectId: agent.project_id,
|
|
426
614
|
createdAt: agent.created_at,
|
|
427
615
|
updatedAt: agent.updated_at,
|
|
@@ -455,14 +643,22 @@ export async function handleApiRequest(req: Request, path: string, authContext?:
|
|
|
455
643
|
});
|
|
456
644
|
}
|
|
457
645
|
|
|
646
|
+
// GET /api/features - Feature flags (no auth required)
|
|
647
|
+
if (path === "/api/features" && method === "GET") {
|
|
648
|
+
return json({
|
|
649
|
+
projects: process.env.PROJECTS_ENABLED === "true",
|
|
650
|
+
metaAgent: process.env.META_AGENT_ENABLED === "true",
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
|
|
458
654
|
// GET /api/openapi - OpenAPI spec (no auth required)
|
|
459
655
|
if (path === "/api/openapi" && method === "GET") {
|
|
460
656
|
return json(openApiSpec);
|
|
461
657
|
}
|
|
462
658
|
|
|
463
|
-
// GET /api/agents - List all agents
|
|
659
|
+
// GET /api/agents - List all agents (excludes meta agent)
|
|
464
660
|
if (path === "/api/agents" && method === "GET") {
|
|
465
|
-
const agents = AgentDB.findAll();
|
|
661
|
+
const agents = AgentDB.findAll().filter(a => a.id !== META_AGENT_ID);
|
|
466
662
|
return json({ agents: agents.map(toApiAgent) });
|
|
467
663
|
}
|
|
468
664
|
|
|
@@ -487,6 +683,7 @@ export async function handleApiRequest(req: Request, path: string, authContext?:
|
|
|
487
683
|
system_prompt: systemPrompt || "You are a helpful assistant.",
|
|
488
684
|
features: features || DEFAULT_FEATURES,
|
|
489
685
|
mcp_servers: body.mcpServers || [],
|
|
686
|
+
skills: body.skills || [],
|
|
490
687
|
project_id: projectId || null,
|
|
491
688
|
});
|
|
492
689
|
|
|
@@ -507,6 +704,20 @@ export async function handleApiRequest(req: Request, path: string, authContext?:
|
|
|
507
704
|
return json({ agent: toApiAgent(agent) });
|
|
508
705
|
}
|
|
509
706
|
|
|
707
|
+
// GET /api/agents/:id/api-key - Get agent API key (dev mode only)
|
|
708
|
+
const agentApiKeyMatch = path.match(/^\/api\/agents\/([^/]+)\/api-key$/);
|
|
709
|
+
if (agentApiKeyMatch && method === "GET") {
|
|
710
|
+
if (!isDev) {
|
|
711
|
+
return json({ error: "Only available in development mode" }, 403);
|
|
712
|
+
}
|
|
713
|
+
const agent = AgentDB.findById(agentApiKeyMatch[1]);
|
|
714
|
+
if (!agent) {
|
|
715
|
+
return json({ error: "Agent not found" }, 404);
|
|
716
|
+
}
|
|
717
|
+
const apiKey = AgentDB.getApiKey(agent.id);
|
|
718
|
+
return json({ apiKey });
|
|
719
|
+
}
|
|
720
|
+
|
|
510
721
|
// PUT /api/agents/:id - Update an agent
|
|
511
722
|
if (agentMatch && method === "PUT") {
|
|
512
723
|
const agent = AgentDB.findById(agentMatch[1]);
|
|
@@ -524,11 +735,12 @@ export async function handleApiRequest(req: Request, path: string, authContext?:
|
|
|
524
735
|
if (body.systemPrompt !== undefined) updates.system_prompt = body.systemPrompt;
|
|
525
736
|
if (body.features !== undefined) updates.features = body.features;
|
|
526
737
|
if (body.mcpServers !== undefined) updates.mcp_servers = body.mcpServers;
|
|
738
|
+
if (body.skills !== undefined) updates.skills = body.skills;
|
|
527
739
|
if (body.projectId !== undefined) updates.project_id = body.projectId;
|
|
528
740
|
|
|
529
741
|
const updated = AgentDB.update(agentMatch[1], updates);
|
|
530
742
|
|
|
531
|
-
// If agent is running, push the new config
|
|
743
|
+
// If agent is running, push the new config and skills
|
|
532
744
|
if (updated && updated.status === "running" && updated.port) {
|
|
533
745
|
const providerKey = ProviderKeys.getDecrypted(updated.provider);
|
|
534
746
|
if (providerKey) {
|
|
@@ -537,6 +749,13 @@ export async function handleApiRequest(req: Request, path: string, authContext?:
|
|
|
537
749
|
if (!configResult.success) {
|
|
538
750
|
console.error(`Failed to push config to running agent: ${configResult.error}`);
|
|
539
751
|
}
|
|
752
|
+
// Push skills via /skills endpoint
|
|
753
|
+
if (config.skills?.definitions?.length > 0) {
|
|
754
|
+
const skillsResult = await pushSkillsToAgent(updated.id, updated.port, config.skills.definitions);
|
|
755
|
+
if (!skillsResult.success) {
|
|
756
|
+
console.error(`Failed to push skills to running agent: ${skillsResult.error}`);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
540
759
|
}
|
|
541
760
|
}
|
|
542
761
|
|
|
@@ -556,9 +775,41 @@ export async function handleApiRequest(req: Request, path: string, authContext?:
|
|
|
556
775
|
|
|
557
776
|
// Stop the agent if running
|
|
558
777
|
const agentProc = agentProcesses.get(agentId);
|
|
778
|
+
const port = agent.port;
|
|
779
|
+
|
|
559
780
|
if (agentProc) {
|
|
560
|
-
|
|
781
|
+
// Try graceful shutdown first
|
|
782
|
+
if (port) {
|
|
783
|
+
try {
|
|
784
|
+
await fetch(`http://localhost:${port}/shutdown`, {
|
|
785
|
+
method: "POST",
|
|
786
|
+
signal: AbortSignal.timeout(2000),
|
|
787
|
+
});
|
|
788
|
+
await new Promise(r => setTimeout(r, 500));
|
|
789
|
+
} catch {
|
|
790
|
+
// Graceful shutdown failed
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
try {
|
|
795
|
+
agentProc.proc.kill();
|
|
796
|
+
} catch {
|
|
797
|
+
// Already dead
|
|
798
|
+
}
|
|
561
799
|
agentProcesses.delete(agentId);
|
|
800
|
+
|
|
801
|
+
// Ensure port is freed
|
|
802
|
+
if (port) {
|
|
803
|
+
const isFree = await checkPortFree(port);
|
|
804
|
+
if (!isFree) {
|
|
805
|
+
try {
|
|
806
|
+
const { execSync } = await import("child_process");
|
|
807
|
+
execSync(`lsof -ti :${port} | xargs -r kill -9 2>/dev/null || true`, { stdio: "ignore" });
|
|
808
|
+
} catch {
|
|
809
|
+
// Ignore
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
562
813
|
}
|
|
563
814
|
|
|
564
815
|
// Delete agent's telemetry data
|
|
@@ -645,10 +896,45 @@ export async function handleApiRequest(req: Request, path: string, authContext?:
|
|
|
645
896
|
}
|
|
646
897
|
|
|
647
898
|
const agentProc = agentProcesses.get(agent.id);
|
|
899
|
+
const port = agent.port;
|
|
900
|
+
|
|
648
901
|
if (agentProc) {
|
|
649
902
|
console.log(`Stopping agent ${agent.name} (pid: ${agentProc.proc.pid})...`);
|
|
650
|
-
|
|
903
|
+
|
|
904
|
+
// Try graceful shutdown first
|
|
905
|
+
if (port) {
|
|
906
|
+
try {
|
|
907
|
+
await fetch(`http://localhost:${port}/shutdown`, {
|
|
908
|
+
method: "POST",
|
|
909
|
+
signal: AbortSignal.timeout(2000),
|
|
910
|
+
});
|
|
911
|
+
await new Promise(r => setTimeout(r, 500)); // Wait for graceful shutdown
|
|
912
|
+
} catch {
|
|
913
|
+
// Graceful shutdown failed or timed out
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// Force kill if still running
|
|
918
|
+
try {
|
|
919
|
+
agentProc.proc.kill();
|
|
920
|
+
} catch {
|
|
921
|
+
// Already dead
|
|
922
|
+
}
|
|
651
923
|
agentProcesses.delete(agent.id);
|
|
924
|
+
|
|
925
|
+
// Ensure port is freed
|
|
926
|
+
if (port) {
|
|
927
|
+
const isFree = await checkPortFree(port);
|
|
928
|
+
if (!isFree) {
|
|
929
|
+
// Force kill by port
|
|
930
|
+
try {
|
|
931
|
+
const { execSync } = await import("child_process");
|
|
932
|
+
execSync(`lsof -ti :${port} | xargs -r kill -9 2>/dev/null || true`, { stdio: "ignore" });
|
|
933
|
+
} catch {
|
|
934
|
+
// Ignore
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
}
|
|
652
938
|
}
|
|
653
939
|
|
|
654
940
|
const updated = AgentDB.setStatus(agent.id, "stopped");
|
|
@@ -1084,6 +1370,44 @@ export async function handleApiRequest(req: Request, path: string, authContext?:
|
|
|
1084
1370
|
|
|
1085
1371
|
// ==================== DISCOVERY/PEERS PROXY ====================
|
|
1086
1372
|
|
|
1373
|
+
// GET /api/discovery/agents - Central discovery endpoint for agents to find peers
|
|
1374
|
+
// Called by agent binaries to discover other agents in the same group
|
|
1375
|
+
if (path === "/api/discovery/agents" && method === "GET") {
|
|
1376
|
+
const group = url.searchParams.get("group");
|
|
1377
|
+
const excludeId = url.searchParams.get("exclude") || req.headers.get("X-Agent-ID");
|
|
1378
|
+
|
|
1379
|
+
// Find all running agents in the same group
|
|
1380
|
+
const allAgents = AgentDB.findAll();
|
|
1381
|
+
const peers = allAgents
|
|
1382
|
+
.filter(a => {
|
|
1383
|
+
// Must be running with a port
|
|
1384
|
+
if (a.status !== "running" || !a.port) return false;
|
|
1385
|
+
// Exclude the requesting agent
|
|
1386
|
+
if (excludeId && a.id === excludeId) return false;
|
|
1387
|
+
// Must have multi-agent enabled
|
|
1388
|
+
const agentConfig = getMultiAgentConfig(a.features, a.project_id);
|
|
1389
|
+
if (!agentConfig.enabled) return false;
|
|
1390
|
+
// If group specified, must match
|
|
1391
|
+
if (group) {
|
|
1392
|
+
const peerGroup = agentConfig.group || a.project_id;
|
|
1393
|
+
if (peerGroup !== group) return false;
|
|
1394
|
+
}
|
|
1395
|
+
return true;
|
|
1396
|
+
})
|
|
1397
|
+
.map(a => {
|
|
1398
|
+
const agentConfig = getMultiAgentConfig(a.features, a.project_id);
|
|
1399
|
+
return {
|
|
1400
|
+
id: a.id,
|
|
1401
|
+
name: a.name,
|
|
1402
|
+
url: `http://localhost:${a.port}`,
|
|
1403
|
+
mode: agentConfig.mode || "worker",
|
|
1404
|
+
group: agentConfig.group || a.project_id,
|
|
1405
|
+
};
|
|
1406
|
+
});
|
|
1407
|
+
|
|
1408
|
+
return json({ agents: peers });
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1087
1411
|
// GET /api/agents/:id/peers - Get discovered peer agents
|
|
1088
1412
|
const peersMatch = path.match(/^\/api\/agents\/([^/]+)\/peers$/);
|
|
1089
1413
|
if (peersMatch && method === "GET") {
|
|
@@ -1188,6 +1512,140 @@ export async function handleApiRequest(req: Request, path: string, authContext?:
|
|
|
1188
1512
|
}
|
|
1189
1513
|
}
|
|
1190
1514
|
|
|
1515
|
+
// ==================== META AGENT (Apteva Assistant) ====================
|
|
1516
|
+
|
|
1517
|
+
// GET /api/meta-agent/status - Get meta agent status and config
|
|
1518
|
+
if (path === "/api/meta-agent/status" && method === "GET") {
|
|
1519
|
+
if (!META_AGENT_ENABLED) {
|
|
1520
|
+
return json({ enabled: false });
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
// Check if onboarding is complete
|
|
1524
|
+
if (!Onboarding.isComplete()) {
|
|
1525
|
+
return json({ enabled: true, available: false, reason: "onboarding_incomplete" });
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
// Get first configured provider
|
|
1529
|
+
const configuredProviders = ProviderKeys.getConfiguredProviders();
|
|
1530
|
+
if (configuredProviders.length === 0) {
|
|
1531
|
+
return json({ enabled: true, available: false, reason: "no_provider" });
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
const providerId = configuredProviders[0] as keyof typeof PROVIDERS;
|
|
1535
|
+
const provider = PROVIDERS[providerId];
|
|
1536
|
+
if (!provider) {
|
|
1537
|
+
return json({ enabled: true, available: false, reason: "invalid_provider" });
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
// Check if meta agent exists, create if not
|
|
1541
|
+
let metaAgent = AgentDB.findById(META_AGENT_ID);
|
|
1542
|
+
if (!metaAgent) {
|
|
1543
|
+
// Find a recommended model or use first one
|
|
1544
|
+
const defaultModel = provider.models.find(m => m.recommended)?.value || provider.models[0]?.value;
|
|
1545
|
+
if (!defaultModel) {
|
|
1546
|
+
return json({ enabled: true, available: false, reason: "no_model" });
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
// Create the meta agent
|
|
1550
|
+
metaAgent = AgentDB.create({
|
|
1551
|
+
id: META_AGENT_ID,
|
|
1552
|
+
name: "Apteva Assistant",
|
|
1553
|
+
model: defaultModel,
|
|
1554
|
+
provider: providerId,
|
|
1555
|
+
system_prompt: `You are the Apteva Assistant, a helpful guide for users of the Apteva agent management platform.
|
|
1556
|
+
|
|
1557
|
+
You can help users with:
|
|
1558
|
+
- Creating and configuring AI agents
|
|
1559
|
+
- Setting up MCP servers for tool integrations
|
|
1560
|
+
- Managing projects and organizing agents
|
|
1561
|
+
- Explaining features like Memory, Tasks, Vision, Operator, Files, and Multi-Agent
|
|
1562
|
+
- Troubleshooting common issues
|
|
1563
|
+
|
|
1564
|
+
Be concise, friendly, and helpful. When users ask about creating something, guide them step by step.
|
|
1565
|
+
Keep responses short and actionable. Use markdown formatting when helpful.`,
|
|
1566
|
+
features: {
|
|
1567
|
+
memory: false,
|
|
1568
|
+
tasks: false,
|
|
1569
|
+
vision: false,
|
|
1570
|
+
operator: false,
|
|
1571
|
+
mcp: false,
|
|
1572
|
+
realtime: false,
|
|
1573
|
+
files: false,
|
|
1574
|
+
agents: false,
|
|
1575
|
+
},
|
|
1576
|
+
mcp_servers: [],
|
|
1577
|
+
skills: [],
|
|
1578
|
+
project_id: null, // Meta agent belongs to no project
|
|
1579
|
+
});
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
// Return status
|
|
1583
|
+
return json({
|
|
1584
|
+
enabled: true,
|
|
1585
|
+
available: true,
|
|
1586
|
+
agent: {
|
|
1587
|
+
id: metaAgent.id,
|
|
1588
|
+
name: metaAgent.name,
|
|
1589
|
+
status: metaAgent.status,
|
|
1590
|
+
port: metaAgent.port,
|
|
1591
|
+
provider: metaAgent.provider,
|
|
1592
|
+
model: metaAgent.model,
|
|
1593
|
+
},
|
|
1594
|
+
});
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
// POST /api/meta-agent/start - Start the meta agent
|
|
1598
|
+
if (path === "/api/meta-agent/start" && method === "POST") {
|
|
1599
|
+
if (!META_AGENT_ENABLED) {
|
|
1600
|
+
return json({ error: "Meta agent is not enabled" }, 400);
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
const metaAgent = AgentDB.findById(META_AGENT_ID);
|
|
1604
|
+
if (!metaAgent) {
|
|
1605
|
+
return json({ error: "Meta agent not found" }, 404);
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
if (metaAgent.status === "running") {
|
|
1609
|
+
return json({ agent: toApiAgent(metaAgent), message: "Already running" });
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
// Start the agent using existing startAgentProcess function
|
|
1613
|
+
const result = await startAgentProcess(metaAgent, { silent: true });
|
|
1614
|
+
if (!result.success) {
|
|
1615
|
+
return json({ error: result.error || "Failed to start meta agent" }, 500);
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
const updated = AgentDB.findById(META_AGENT_ID);
|
|
1619
|
+
return json({ agent: updated ? toApiAgent(updated) : null });
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
// POST /api/meta-agent/stop - Stop the meta agent
|
|
1623
|
+
if (path === "/api/meta-agent/stop" && method === "POST") {
|
|
1624
|
+
if (!META_AGENT_ENABLED) {
|
|
1625
|
+
return json({ error: "Meta agent is not enabled" }, 400);
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
const metaAgent = AgentDB.findById(META_AGENT_ID);
|
|
1629
|
+
if (!metaAgent) {
|
|
1630
|
+
return json({ error: "Meta agent not found" }, 404);
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
if (metaAgent.status === "stopped") {
|
|
1634
|
+
return json({ agent: toApiAgent(metaAgent), message: "Already stopped" });
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
// Stop the agent
|
|
1638
|
+
const proc = agentProcesses.get(META_AGENT_ID);
|
|
1639
|
+
if (proc) {
|
|
1640
|
+
proc.kill();
|
|
1641
|
+
agentProcesses.delete(META_AGENT_ID);
|
|
1642
|
+
}
|
|
1643
|
+
AgentDB.setStatus(META_AGENT_ID, "stopped");
|
|
1644
|
+
|
|
1645
|
+
const updated = AgentDB.findById(META_AGENT_ID);
|
|
1646
|
+
return json({ agent: updated ? toApiAgent(updated) : null });
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1191
1649
|
// ==================== USER MANAGEMENT (Admin only) ====================
|
|
1192
1650
|
|
|
1193
1651
|
// GET /api/users - List all users
|
|
@@ -1438,7 +1896,48 @@ export async function handleApiRequest(req: Request, path: string, authContext?:
|
|
|
1438
1896
|
return json({ error: result.error }, 400);
|
|
1439
1897
|
}
|
|
1440
1898
|
|
|
1441
|
-
|
|
1899
|
+
// Restart any running agents that use this provider (including meta agent)
|
|
1900
|
+
const runningAgents = AgentDB.findAll().filter(
|
|
1901
|
+
a => a.status === "running" && a.provider === providerId
|
|
1902
|
+
);
|
|
1903
|
+
|
|
1904
|
+
const restartResults: Array<{ id: string; name: string; success: boolean; error?: string }> = [];
|
|
1905
|
+
for (const agent of runningAgents) {
|
|
1906
|
+
try {
|
|
1907
|
+
// Stop the agent
|
|
1908
|
+
const agentProc = agentProcesses.get(agent.id);
|
|
1909
|
+
if (agentProc) {
|
|
1910
|
+
agentProc.proc.kill();
|
|
1911
|
+
agentProcesses.delete(agent.id);
|
|
1912
|
+
}
|
|
1913
|
+
AgentDB.setStatus(agent.id, "stopped", null);
|
|
1914
|
+
|
|
1915
|
+
// Wait a moment for port to be released
|
|
1916
|
+
await new Promise(r => setTimeout(r, 500));
|
|
1917
|
+
|
|
1918
|
+
// Restart the agent with new key
|
|
1919
|
+
const startResult = await startAgentProcess(agent, { silent: true });
|
|
1920
|
+
restartResults.push({
|
|
1921
|
+
id: agent.id,
|
|
1922
|
+
name: agent.name,
|
|
1923
|
+
success: startResult.success,
|
|
1924
|
+
error: startResult.error,
|
|
1925
|
+
});
|
|
1926
|
+
} catch (e) {
|
|
1927
|
+
restartResults.push({
|
|
1928
|
+
id: agent.id,
|
|
1929
|
+
name: agent.name,
|
|
1930
|
+
success: false,
|
|
1931
|
+
error: String(e),
|
|
1932
|
+
});
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
return json({
|
|
1937
|
+
success: true,
|
|
1938
|
+
message: "API key saved successfully",
|
|
1939
|
+
restartedAgents: restartResults.length > 0 ? restartResults : undefined,
|
|
1940
|
+
});
|
|
1442
1941
|
} catch (e) {
|
|
1443
1942
|
return json({ error: "Invalid request body" }, 400);
|
|
1444
1943
|
}
|
|
@@ -1709,9 +2208,23 @@ export async function handleApiRequest(req: Request, path: string, authContext?:
|
|
|
1709
2208
|
|
|
1710
2209
|
// ============ MCP Server API ============
|
|
1711
2210
|
|
|
1712
|
-
// GET /api/mcp/servers - List
|
|
2211
|
+
// GET /api/mcp/servers - List MCP servers (optionally filtered by project)
|
|
1713
2212
|
if (path === "/api/mcp/servers" && method === "GET") {
|
|
1714
|
-
const
|
|
2213
|
+
const url = new URL(req.url);
|
|
2214
|
+
const projectFilter = url.searchParams.get("project"); // "all", "global", or project ID
|
|
2215
|
+
const forAgent = url.searchParams.get("forAgent"); // agent's project ID (shows global + project)
|
|
2216
|
+
|
|
2217
|
+
let servers;
|
|
2218
|
+
if (forAgent !== null) {
|
|
2219
|
+
// Get servers available for an agent (global + agent's project)
|
|
2220
|
+
servers = McpServerDB.findForAgent(forAgent || null);
|
|
2221
|
+
} else if (projectFilter === "global") {
|
|
2222
|
+
servers = McpServerDB.findGlobal();
|
|
2223
|
+
} else if (projectFilter && projectFilter !== "all") {
|
|
2224
|
+
servers = McpServerDB.findByProject(projectFilter);
|
|
2225
|
+
} else {
|
|
2226
|
+
servers = McpServerDB.findAll();
|
|
2227
|
+
}
|
|
1715
2228
|
return json({ servers });
|
|
1716
2229
|
}
|
|
1717
2230
|
|
|
@@ -2172,7 +2685,7 @@ export async function handleApiRequest(req: Request, path: string, authContext?:
|
|
|
2172
2685
|
if (path === "/api/mcp/servers" && method === "POST") {
|
|
2173
2686
|
try {
|
|
2174
2687
|
const body = await req.json();
|
|
2175
|
-
const { name, type, package: pkg, command, args, env, url, headers, source } = body;
|
|
2688
|
+
const { name, type, package: pkg, pip_module, command, args, env, url, headers, source, project_id } = body;
|
|
2176
2689
|
|
|
2177
2690
|
if (!name) {
|
|
2178
2691
|
return json({ error: "Name is required" }, 400);
|
|
@@ -2183,12 +2696,14 @@ export async function handleApiRequest(req: Request, path: string, authContext?:
|
|
|
2183
2696
|
name,
|
|
2184
2697
|
type: type || "npm",
|
|
2185
2698
|
package: pkg || null,
|
|
2699
|
+
pip_module: pip_module || null,
|
|
2186
2700
|
command: command || null,
|
|
2187
2701
|
args: args || null,
|
|
2188
2702
|
env: env || {},
|
|
2189
2703
|
url: url || null,
|
|
2190
2704
|
headers: headers || {},
|
|
2191
2705
|
source: source || null,
|
|
2706
|
+
project_id: project_id || null,
|
|
2192
2707
|
});
|
|
2193
2708
|
|
|
2194
2709
|
return json({ server }, 201);
|
|
@@ -2225,6 +2740,7 @@ export async function handleApiRequest(req: Request, path: string, authContext?:
|
|
|
2225
2740
|
if (body.command !== undefined) updates.command = body.command;
|
|
2226
2741
|
if (body.args !== undefined) updates.args = body.args;
|
|
2227
2742
|
if (body.env !== undefined) updates.env = body.env;
|
|
2743
|
+
if (body.project_id !== undefined) updates.project_id = body.project_id;
|
|
2228
2744
|
|
|
2229
2745
|
const updated = McpServerDB.update(mcpServerMatch[1], updates);
|
|
2230
2746
|
return json({ server: updated });
|
|
@@ -2279,6 +2795,32 @@ export async function handleApiRequest(req: Request, path: string, authContext?:
|
|
|
2279
2795
|
const substitutedArgs = substituteEnvVars(server.args, serverEnv);
|
|
2280
2796
|
cmd.push(...substitutedArgs.split(" "));
|
|
2281
2797
|
}
|
|
2798
|
+
} else if (server.type === "pip" && server.package) {
|
|
2799
|
+
// Python pip package - install first, then run module
|
|
2800
|
+
const pipPackage = server.package;
|
|
2801
|
+
const pipModule = server.pip_module || server.package.split("[")[0]; // Default: package name without extras
|
|
2802
|
+
|
|
2803
|
+
console.log(`Installing pip package: ${pipPackage}...`);
|
|
2804
|
+
const installResult = spawn({
|
|
2805
|
+
cmd: ["pip", "install", "--quiet", "--break-system-packages", pipPackage],
|
|
2806
|
+
env: { ...process.env as Record<string, string>, ...serverEnv },
|
|
2807
|
+
stdout: "pipe",
|
|
2808
|
+
stderr: "pipe",
|
|
2809
|
+
});
|
|
2810
|
+
|
|
2811
|
+
// Wait for installation to complete
|
|
2812
|
+
const exitCode = await installResult.exited;
|
|
2813
|
+
if (exitCode !== 0) {
|
|
2814
|
+
const stderr = await new Response(installResult.stderr).text();
|
|
2815
|
+
return json({ error: `Failed to install pip package: ${stderr || "unknown error"}` }, 500);
|
|
2816
|
+
}
|
|
2817
|
+
|
|
2818
|
+
// Now run the module
|
|
2819
|
+
cmd = ["python", "-m", pipModule];
|
|
2820
|
+
if (server.args) {
|
|
2821
|
+
const substitutedArgs = substituteEnvVars(server.args, serverEnv);
|
|
2822
|
+
cmd.push(...substitutedArgs.split(" "));
|
|
2823
|
+
}
|
|
2282
2824
|
} else if (server.package) {
|
|
2283
2825
|
// npm package - use npx
|
|
2284
2826
|
cmd = ["npx", "-y", server.package];
|
|
@@ -2420,6 +2962,284 @@ export async function handleApiRequest(req: Request, path: string, authContext?:
|
|
|
2420
2962
|
}
|
|
2421
2963
|
}
|
|
2422
2964
|
|
|
2965
|
+
// ============ Skills Endpoints ============
|
|
2966
|
+
|
|
2967
|
+
// GET /api/skills - List skills (optionally filtered by project)
|
|
2968
|
+
if (path === "/api/skills" && method === "GET") {
|
|
2969
|
+
const url = new URL(req.url);
|
|
2970
|
+
const projectFilter = url.searchParams.get("project"); // "all", "global", or project ID
|
|
2971
|
+
const forAgent = url.searchParams.get("forAgent"); // agent's project ID (shows global + project)
|
|
2972
|
+
|
|
2973
|
+
let skills;
|
|
2974
|
+
if (forAgent !== null) {
|
|
2975
|
+
// Get skills available for an agent (global + agent's project)
|
|
2976
|
+
skills = SkillDB.findForAgent(forAgent || null);
|
|
2977
|
+
} else if (projectFilter === "global") {
|
|
2978
|
+
skills = SkillDB.findGlobal();
|
|
2979
|
+
} else if (projectFilter && projectFilter !== "all") {
|
|
2980
|
+
skills = SkillDB.findByProject(projectFilter);
|
|
2981
|
+
} else {
|
|
2982
|
+
skills = SkillDB.findAll();
|
|
2983
|
+
}
|
|
2984
|
+
return json({ skills });
|
|
2985
|
+
}
|
|
2986
|
+
|
|
2987
|
+
// POST /api/skills - Create a new skill
|
|
2988
|
+
if (path === "/api/skills" && method === "POST") {
|
|
2989
|
+
try {
|
|
2990
|
+
const body = await req.json();
|
|
2991
|
+
const { name, description, content, version, license, compatibility, metadata, allowed_tools, source, source_url, enabled, project_id } = body;
|
|
2992
|
+
|
|
2993
|
+
if (!name || !description || !content) {
|
|
2994
|
+
return json({ error: "name, description, and content are required" }, 400);
|
|
2995
|
+
}
|
|
2996
|
+
|
|
2997
|
+
// Validate name format (lowercase, hyphens only)
|
|
2998
|
+
if (!/^[a-z][a-z0-9-]*[a-z0-9]$|^[a-z]$/.test(name)) {
|
|
2999
|
+
return json({ error: "name must be lowercase letters, numbers, and hyphens only" }, 400);
|
|
3000
|
+
}
|
|
3001
|
+
|
|
3002
|
+
if (SkillDB.exists(name)) {
|
|
3003
|
+
return json({ error: "A skill with this name already exists" }, 400);
|
|
3004
|
+
}
|
|
3005
|
+
|
|
3006
|
+
const skill = SkillDB.create({
|
|
3007
|
+
name,
|
|
3008
|
+
description,
|
|
3009
|
+
content,
|
|
3010
|
+
version: version || "1.0.0",
|
|
3011
|
+
license: license || null,
|
|
3012
|
+
compatibility: compatibility || null,
|
|
3013
|
+
metadata: metadata || {},
|
|
3014
|
+
allowed_tools: allowed_tools || [],
|
|
3015
|
+
source: source || "local",
|
|
3016
|
+
source_url: source_url || null,
|
|
3017
|
+
enabled: enabled !== false,
|
|
3018
|
+
project_id: project_id || null,
|
|
3019
|
+
});
|
|
3020
|
+
|
|
3021
|
+
return json({ skill }, 201);
|
|
3022
|
+
} catch (err) {
|
|
3023
|
+
console.error("Failed to create skill:", err);
|
|
3024
|
+
return json({ error: `Failed to create skill: ${err}` }, 500);
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3028
|
+
// GET /api/skills/:id - Get a skill
|
|
3029
|
+
const skillMatch = path.match(/^\/api\/skills\/([^/]+)$/);
|
|
3030
|
+
if (skillMatch && method === "GET") {
|
|
3031
|
+
const skill = SkillDB.findById(skillMatch[1]);
|
|
3032
|
+
if (!skill) {
|
|
3033
|
+
return json({ error: "Skill not found" }, 404);
|
|
3034
|
+
}
|
|
3035
|
+
return json({ skill });
|
|
3036
|
+
}
|
|
3037
|
+
|
|
3038
|
+
// PUT /api/skills/:id - Update a skill
|
|
3039
|
+
if (skillMatch && method === "PUT") {
|
|
3040
|
+
const skill = SkillDB.findById(skillMatch[1]);
|
|
3041
|
+
if (!skill) {
|
|
3042
|
+
return json({ error: "Skill not found" }, 404);
|
|
3043
|
+
}
|
|
3044
|
+
|
|
3045
|
+
try {
|
|
3046
|
+
const body = await req.json();
|
|
3047
|
+
const updates: Partial<Skill> = {};
|
|
3048
|
+
|
|
3049
|
+
if (body.name !== undefined) updates.name = body.name;
|
|
3050
|
+
if (body.description !== undefined) updates.description = body.description;
|
|
3051
|
+
if (body.content !== undefined) updates.content = body.content;
|
|
3052
|
+
if (body.license !== undefined) updates.license = body.license;
|
|
3053
|
+
if (body.compatibility !== undefined) updates.compatibility = body.compatibility;
|
|
3054
|
+
if (body.metadata !== undefined) updates.metadata = body.metadata;
|
|
3055
|
+
if (body.allowed_tools !== undefined) updates.allowed_tools = body.allowed_tools;
|
|
3056
|
+
if (body.enabled !== undefined) updates.enabled = body.enabled;
|
|
3057
|
+
if (body.project_id !== undefined) updates.project_id = body.project_id;
|
|
3058
|
+
|
|
3059
|
+
// Auto-increment version if content changed
|
|
3060
|
+
if (body.content !== undefined && body.content !== skill.content) {
|
|
3061
|
+
const [major, minor, patch] = (skill.version || "1.0.0").split(".").map(Number);
|
|
3062
|
+
updates.version = `${major}.${minor}.${patch + 1}`;
|
|
3063
|
+
} else if (body.version !== undefined) {
|
|
3064
|
+
updates.version = body.version;
|
|
3065
|
+
}
|
|
3066
|
+
|
|
3067
|
+
const updated = SkillDB.update(skillMatch[1], updates);
|
|
3068
|
+
|
|
3069
|
+
// Push updated skill to all running agents that have it
|
|
3070
|
+
const agentsWithSkill = AgentDB.findBySkill(skillMatch[1]);
|
|
3071
|
+
const runningAgents = agentsWithSkill.filter(a => a.status === "running" && a.port);
|
|
3072
|
+
|
|
3073
|
+
for (const agent of runningAgents) {
|
|
3074
|
+
try {
|
|
3075
|
+
const providerKey = ProviderKeys.getDecrypted(agent.provider);
|
|
3076
|
+
if (providerKey) {
|
|
3077
|
+
const config = buildAgentConfig(agent, providerKey);
|
|
3078
|
+
await pushConfigToAgent(agent.id, agent.port!, config);
|
|
3079
|
+
// Push skills via /skills endpoint
|
|
3080
|
+
if (config.skills?.definitions?.length > 0) {
|
|
3081
|
+
await pushSkillsToAgent(agent.id, agent.port!, config.skills.definitions);
|
|
3082
|
+
}
|
|
3083
|
+
console.log(`Pushed skill update to agent ${agent.name}`);
|
|
3084
|
+
}
|
|
3085
|
+
} catch (err) {
|
|
3086
|
+
console.error(`Failed to push skill update to agent ${agent.name}:`, err);
|
|
3087
|
+
}
|
|
3088
|
+
}
|
|
3089
|
+
|
|
3090
|
+
return json({ skill: updated, agents_updated: runningAgents.length });
|
|
3091
|
+
} catch (err) {
|
|
3092
|
+
console.error("Failed to update skill:", err);
|
|
3093
|
+
return json({ error: `Failed to update skill: ${err}` }, 500);
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
|
|
3097
|
+
// DELETE /api/skills/:id - Delete a skill
|
|
3098
|
+
if (skillMatch && method === "DELETE") {
|
|
3099
|
+
const skill = SkillDB.findById(skillMatch[1]);
|
|
3100
|
+
if (!skill) {
|
|
3101
|
+
return json({ error: "Skill not found" }, 404);
|
|
3102
|
+
}
|
|
3103
|
+
|
|
3104
|
+
SkillDB.delete(skillMatch[1]);
|
|
3105
|
+
return json({ success: true });
|
|
3106
|
+
}
|
|
3107
|
+
|
|
3108
|
+
// POST /api/skills/:id/toggle - Toggle skill enabled/disabled
|
|
3109
|
+
const skillToggleMatch = path.match(/^\/api\/skills\/([^/]+)\/toggle$/);
|
|
3110
|
+
if (skillToggleMatch && method === "POST") {
|
|
3111
|
+
const skill = SkillDB.findById(skillToggleMatch[1]);
|
|
3112
|
+
if (!skill) {
|
|
3113
|
+
return json({ error: "Skill not found" }, 404);
|
|
3114
|
+
}
|
|
3115
|
+
|
|
3116
|
+
const updated = SkillDB.setEnabled(skillToggleMatch[1], !skill.enabled);
|
|
3117
|
+
return json({ skill: updated });
|
|
3118
|
+
}
|
|
3119
|
+
|
|
3120
|
+
// POST /api/skills/import - Import a skill from SKILL.md content
|
|
3121
|
+
if (path === "/api/skills/import" && method === "POST") {
|
|
3122
|
+
try {
|
|
3123
|
+
const body = await req.json();
|
|
3124
|
+
const { content, source, source_url } = body;
|
|
3125
|
+
|
|
3126
|
+
if (!content) {
|
|
3127
|
+
return json({ error: "content is required" }, 400);
|
|
3128
|
+
}
|
|
3129
|
+
|
|
3130
|
+
const parsed = parseSkillMd(content);
|
|
3131
|
+
if (!parsed) {
|
|
3132
|
+
return json({ error: "Invalid SKILL.md format. Must have YAML frontmatter with name and description." }, 400);
|
|
3133
|
+
}
|
|
3134
|
+
|
|
3135
|
+
if (SkillDB.exists(parsed.name)) {
|
|
3136
|
+
return json({ error: `A skill named "${parsed.name}" already exists` }, 400);
|
|
3137
|
+
}
|
|
3138
|
+
|
|
3139
|
+
const skill = SkillDB.create({
|
|
3140
|
+
name: parsed.name,
|
|
3141
|
+
description: parsed.description,
|
|
3142
|
+
content: content, // Store full content including frontmatter
|
|
3143
|
+
license: parsed.license || null,
|
|
3144
|
+
compatibility: parsed.compatibility || null,
|
|
3145
|
+
metadata: parsed.metadata || {},
|
|
3146
|
+
allowed_tools: parsed.allowedTools || [],
|
|
3147
|
+
source: source || "import",
|
|
3148
|
+
source_url: source_url || null,
|
|
3149
|
+
enabled: true,
|
|
3150
|
+
});
|
|
3151
|
+
|
|
3152
|
+
return json({ skill }, 201);
|
|
3153
|
+
} catch (err) {
|
|
3154
|
+
console.error("Failed to import skill:", err);
|
|
3155
|
+
return json({ error: `Failed to import skill: ${err}` }, 500);
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
|
|
3159
|
+
// GET /api/skills/:id/export - Export a skill as SKILL.md
|
|
3160
|
+
const skillExportMatch = path.match(/^\/api\/skills\/([^/]+)\/export$/);
|
|
3161
|
+
if (skillExportMatch && method === "GET") {
|
|
3162
|
+
const skill = SkillDB.findById(skillExportMatch[1]);
|
|
3163
|
+
if (!skill) {
|
|
3164
|
+
return json({ error: "Skill not found" }, 404);
|
|
3165
|
+
}
|
|
3166
|
+
|
|
3167
|
+
// Return the raw content
|
|
3168
|
+
return new Response(skill.content, {
|
|
3169
|
+
headers: {
|
|
3170
|
+
"Content-Type": "text/markdown",
|
|
3171
|
+
"Content-Disposition": `attachment; filename="${skill.name}-SKILL.md"`,
|
|
3172
|
+
},
|
|
3173
|
+
});
|
|
3174
|
+
}
|
|
3175
|
+
|
|
3176
|
+
// ============ SkillsMP Marketplace Endpoints ============
|
|
3177
|
+
|
|
3178
|
+
// GET /api/skills/marketplace/search - Search skills marketplace
|
|
3179
|
+
if (path === "/api/skills/marketplace/search" && method === "GET") {
|
|
3180
|
+
const url = new URL(req.url);
|
|
3181
|
+
const query = url.searchParams.get("q") || "";
|
|
3182
|
+
const page = parseInt(url.searchParams.get("page") || "1", 10);
|
|
3183
|
+
|
|
3184
|
+
// Get SkillsMP API key if configured
|
|
3185
|
+
const skillsmpKey = ProviderKeys.getDecrypted("skillsmp");
|
|
3186
|
+
|
|
3187
|
+
const result = await SkillsmpProvider.search(skillsmpKey || "", query, page);
|
|
3188
|
+
return json(result);
|
|
3189
|
+
}
|
|
3190
|
+
|
|
3191
|
+
// GET /api/skills/marketplace/featured - Get featured skills
|
|
3192
|
+
if (path === "/api/skills/marketplace/featured" && method === "GET") {
|
|
3193
|
+
const skillsmpKey = ProviderKeys.getDecrypted("skillsmp");
|
|
3194
|
+
const skills = await SkillsmpProvider.getFeatured(skillsmpKey || "");
|
|
3195
|
+
return json({ skills });
|
|
3196
|
+
}
|
|
3197
|
+
|
|
3198
|
+
// GET /api/skills/marketplace/:id - Get skill details from marketplace
|
|
3199
|
+
const marketplaceSkillMatch = path.match(/^\/api\/skills\/marketplace\/([^/]+)$/);
|
|
3200
|
+
if (marketplaceSkillMatch && method === "GET") {
|
|
3201
|
+
const skillsmpKey = ProviderKeys.getDecrypted("skillsmp");
|
|
3202
|
+
const skill = await SkillsmpProvider.getSkill(skillsmpKey || "", marketplaceSkillMatch[1]);
|
|
3203
|
+
if (!skill) {
|
|
3204
|
+
return json({ error: "Skill not found in marketplace" }, 404);
|
|
3205
|
+
}
|
|
3206
|
+
return json({ skill });
|
|
3207
|
+
}
|
|
3208
|
+
|
|
3209
|
+
// POST /api/skills/marketplace/:id/install - Install a skill from marketplace
|
|
3210
|
+
const marketplaceInstallMatch = path.match(/^\/api\/skills\/marketplace\/([^/]+)\/install$/);
|
|
3211
|
+
if (marketplaceInstallMatch && method === "POST") {
|
|
3212
|
+
const skillsmpKey = ProviderKeys.getDecrypted("skillsmp");
|
|
3213
|
+
const marketplaceSkill = await SkillsmpProvider.getSkill(skillsmpKey || "", marketplaceInstallMatch[1]);
|
|
3214
|
+
|
|
3215
|
+
if (!marketplaceSkill) {
|
|
3216
|
+
return json({ error: "Skill not found in marketplace" }, 404);
|
|
3217
|
+
}
|
|
3218
|
+
|
|
3219
|
+
if (SkillDB.exists(marketplaceSkill.name)) {
|
|
3220
|
+
return json({ error: `A skill named "${marketplaceSkill.name}" already exists` }, 400);
|
|
3221
|
+
}
|
|
3222
|
+
|
|
3223
|
+
const skill = SkillDB.create({
|
|
3224
|
+
name: marketplaceSkill.name,
|
|
3225
|
+
description: marketplaceSkill.description,
|
|
3226
|
+
content: marketplaceSkill.content,
|
|
3227
|
+
license: marketplaceSkill.license,
|
|
3228
|
+
compatibility: marketplaceSkill.compatibility,
|
|
3229
|
+
metadata: {
|
|
3230
|
+
author: marketplaceSkill.author,
|
|
3231
|
+
version: marketplaceSkill.version,
|
|
3232
|
+
...(marketplaceSkill.repository ? { repository: marketplaceSkill.repository } : {}),
|
|
3233
|
+
},
|
|
3234
|
+
allowed_tools: [],
|
|
3235
|
+
source: "skillsmp",
|
|
3236
|
+
source_url: marketplaceSkill.repository || `https://skillsmp.com/skills/${marketplaceSkill.id}`,
|
|
3237
|
+
enabled: true,
|
|
3238
|
+
});
|
|
3239
|
+
|
|
3240
|
+
return json({ skill }, 201);
|
|
3241
|
+
}
|
|
3242
|
+
|
|
2423
3243
|
// ============ Telemetry Endpoints ============
|
|
2424
3244
|
|
|
2425
3245
|
// POST /api/telemetry - Receive telemetry events from agents
|