apteva 0.2.10 → 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/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, BINARY_PATH, getNextPort, getBinaryStatus, BIN_DIR, telemetryBroadcaster, type TelemetryEvent } from "../server";
6
- import { AgentDB, McpServerDB, TelemetryDB, UserDB, ProjectDB, generateId, type Agent, type AgentFeatures, type McpServer, type Project } from "../db";
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
- // Port is free - good
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: [BINARY_PATH],
506
+ cmd: [binaryPath],
344
507
  env,
345
- stdout: "ignore",
346
- stderr: "ignore",
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
- agentProc.proc.kill();
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
- agentProc.proc.kill();
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
- return json({ success: true, message: "API key saved successfully" });
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 all MCP servers
2211
+ // GET /api/mcp/servers - List MCP servers (optionally filtered by project)
1713
2212
  if (path === "/api/mcp/servers" && method === "GET") {
1714
- const servers = McpServerDB.findAll();
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