apteva 0.4.56 → 0.7.0
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/README.md +216 -54
- package/cli.js +35 -0
- package/install.js +92 -0
- package/package.json +12 -79
- package/LICENSE +0 -63
- package/bin/apteva.js +0 -196
- package/dist/ActivityPage.kxzzb4yc.js +0 -3
- package/dist/ApiDocsPage.zq998hbm.js +0 -4
- package/dist/App.55rea8mn.js +0 -61
- package/dist/App.5ywb23z4.js +0 -53
- package/dist/App.6thds120.js +0 -4
- package/dist/App.9tctxzqm.js +0 -8
- package/dist/App.a8r8ttaz.js +0 -4
- package/dist/App.agsv5bje.js +0 -4
- package/dist/App.cepapqmx.js +0 -4
- package/dist/App.dp041gb3.js +0 -221
- package/dist/App.fds72zb5.js +0 -4
- package/dist/App.fg9qj2dq.js +0 -4
- package/dist/App.ndfejbm9.js +0 -4
- package/dist/App.nxmfmq1h.js +0 -13
- package/dist/App.qdfyt8ba.js +0 -4
- package/dist/App.x2d0ygt6.js +0 -4
- package/dist/App.yt9p4nr3.js +0 -20
- package/dist/App.zn4mw16t.js +0 -1
- package/dist/ConnectionsPage.8r96ryw7.js +0 -3
- package/dist/McpPage.3cwh0gnd.js +0 -3
- package/dist/SettingsPage.ykgdh5ev.js +0 -3
- package/dist/SkillsPage.4np1s65b.js +0 -3
- package/dist/TasksPage.4g08t7p6.js +0 -3
- package/dist/TelemetryPage.72w9pwcp.js +0 -3
- package/dist/TestsPage.z4fk3r7r.js +0 -3
- package/dist/ThreadsPage.63tcajeh.js +0 -3
- package/dist/apteva-kit.css +0 -1
- package/dist/icon.png +0 -0
- package/dist/index.html +0 -16
- package/dist/styles.css +0 -1
- package/scripts/postinstall.mjs +0 -102
- package/src/auth/index.ts +0 -394
- package/src/auth/middleware.ts +0 -213
- package/src/binary.ts +0 -536
- package/src/channels/index.ts +0 -40
- package/src/channels/telegram.ts +0 -311
- package/src/crypto.ts +0 -301
- package/src/db-tests.ts +0 -174
- package/src/db.ts +0 -3133
- package/src/integrations/agentdojo.ts +0 -559
- package/src/integrations/composio.ts +0 -437
- package/src/integrations/index.ts +0 -87
- package/src/integrations/skillsmp.ts +0 -318
- package/src/mcp-client.ts +0 -605
- package/src/mcp-handler.ts +0 -394
- package/src/mcp-platform.ts +0 -2370
- package/src/openapi.ts +0 -2410
- package/src/providers.ts +0 -597
- package/src/routes/api/agent-utils.ts +0 -890
- package/src/routes/api/agents.ts +0 -916
- package/src/routes/api/api-keys.ts +0 -95
- package/src/routes/api/channels.ts +0 -182
- package/src/routes/api/helpers.ts +0 -12
- package/src/routes/api/integrations.ts +0 -639
- package/src/routes/api/mcp.ts +0 -574
- package/src/routes/api/meta-agent.ts +0 -195
- package/src/routes/api/projects.ts +0 -112
- package/src/routes/api/providers.ts +0 -424
- package/src/routes/api/skills.ts +0 -537
- package/src/routes/api/system.ts +0 -333
- package/src/routes/api/telemetry.ts +0 -203
- package/src/routes/api/tests.ts +0 -148
- package/src/routes/api/triggers.ts +0 -518
- package/src/routes/api/users.ts +0 -148
- package/src/routes/api/webhooks.ts +0 -171
- package/src/routes/api.ts +0 -53
- package/src/routes/auth.ts +0 -251
- package/src/routes/share.ts +0 -86
- package/src/routes/static.ts +0 -131
- package/src/server.ts +0 -642
- package/src/test-runner.ts +0 -598
- package/src/triggers/agentdojo.ts +0 -253
- package/src/triggers/composio.ts +0 -264
- package/src/triggers/index.ts +0 -71
- package/src/tui/AgentList.tsx +0 -145
- package/src/tui/App.tsx +0 -102
- package/src/tui/Login.tsx +0 -104
- package/src/tui/api.ts +0 -72
- package/src/tui/index.tsx +0 -7
- package/src/web/App.tsx +0 -455
- package/src/web/components/activity/ActivityPage.tsx +0 -314
- package/src/web/components/activity/index.ts +0 -1
- package/src/web/components/agents/AgentCard.tsx +0 -189
- package/src/web/components/agents/AgentPanel.tsx +0 -2244
- package/src/web/components/agents/AgentsView.tsx +0 -180
- package/src/web/components/agents/CreateAgentModal.tsx +0 -475
- package/src/web/components/agents/index.ts +0 -4
- package/src/web/components/api/ApiDocsPage.tsx +0 -842
- package/src/web/components/auth/CreateAccountStep.tsx +0 -176
- package/src/web/components/auth/LoginPage.tsx +0 -91
- package/src/web/components/auth/index.ts +0 -2
- package/src/web/components/common/Icons.tsx +0 -250
- package/src/web/components/common/LoadingSpinner.tsx +0 -44
- package/src/web/components/common/Modal.tsx +0 -199
- package/src/web/components/common/Select.tsx +0 -97
- package/src/web/components/common/index.ts +0 -20
- package/src/web/components/connections/ConnectionsPage.tsx +0 -54
- package/src/web/components/connections/IntegrationsTab.tsx +0 -170
- package/src/web/components/connections/OverviewTab.tsx +0 -137
- package/src/web/components/connections/TriggersTab.tsx +0 -1346
- package/src/web/components/dashboard/Dashboard.tsx +0 -572
- package/src/web/components/dashboard/index.ts +0 -1
- package/src/web/components/index.ts +0 -21
- package/src/web/components/layout/ErrorBanner.tsx +0 -18
- package/src/web/components/layout/Header.tsx +0 -332
- package/src/web/components/layout/Sidebar.tsx +0 -231
- package/src/web/components/layout/index.ts +0 -3
- package/src/web/components/mcp/IntegrationsPanel.tsx +0 -857
- package/src/web/components/mcp/McpPage.tsx +0 -2515
- package/src/web/components/mcp/index.ts +0 -1
- package/src/web/components/meta-agent/MetaAgent.tsx +0 -245
- package/src/web/components/onboarding/OnboardingWizard.tsx +0 -404
- package/src/web/components/onboarding/index.ts +0 -1
- package/src/web/components/settings/SettingsPage.tsx +0 -2776
- package/src/web/components/settings/index.ts +0 -1
- package/src/web/components/skills/SkillsPage.tsx +0 -1200
- package/src/web/components/tasks/TasksPage.tsx +0 -1116
- package/src/web/components/tasks/index.ts +0 -1
- package/src/web/components/telemetry/TelemetryPage.tsx +0 -1129
- package/src/web/components/tests/TestsPage.tsx +0 -594
- package/src/web/components/threads/ThreadsPage.tsx +0 -315
- package/src/web/context/AuthContext.tsx +0 -242
- package/src/web/context/ProjectContext.tsx +0 -214
- package/src/web/context/TelemetryContext.tsx +0 -299
- package/src/web/context/ThemeContext.tsx +0 -90
- package/src/web/context/UIModeContext.tsx +0 -49
- package/src/web/context/index.ts +0 -12
- package/src/web/hooks/index.ts +0 -3
- package/src/web/hooks/useAgents.ts +0 -115
- package/src/web/hooks/useOnboarding.ts +0 -20
- package/src/web/hooks/useProviders.ts +0 -75
- package/src/web/icon.png +0 -0
- package/src/web/index.html +0 -16
- package/src/web/styles.css +0 -118
- package/src/web/themes.ts +0 -162
- package/src/web/types.ts +0 -298
|
@@ -1,890 +0,0 @@
|
|
|
1
|
-
import { spawn } from "bun";
|
|
2
|
-
import { join } from "path";
|
|
3
|
-
import { homedir } from "os";
|
|
4
|
-
import { mkdirSync, existsSync, rmSync, writeFileSync, readFileSync } from "fs";
|
|
5
|
-
import { agentProcesses, agentsStarting, getBinaryPathForAgent, getBinaryStatus, BIN_DIR, telemetryBroadcaster, isShuttingDown, type TelemetryEvent } from "../../server";
|
|
6
|
-
import { AgentDB, McpServerDB, SkillDB, SubscriptionDB, TelemetryDB, generateId, getMultiAgentConfig, getOperatorConfig, type Agent, type Project } from "../../db";
|
|
7
|
-
import { ProviderKeys, PROVIDERS, type ProviderId } from "../../providers";
|
|
8
|
-
import { binaryExists } from "../../binary";
|
|
9
|
-
|
|
10
|
-
// Data directory for agent instances (in ~/.apteva/agents/)
|
|
11
|
-
export const AGENTS_DATA_DIR = process.env.DATA_DIR
|
|
12
|
-
? join(process.env.DATA_DIR, "agents")
|
|
13
|
-
: join(homedir(), ".apteva", "agents");
|
|
14
|
-
|
|
15
|
-
// Meta Agent configuration
|
|
16
|
-
export const META_AGENT_ENABLED = process.env.META_AGENT_ENABLED === "true";
|
|
17
|
-
export const META_AGENT_ID = "apteva-assistant";
|
|
18
|
-
|
|
19
|
-
// Update agent status + emit telemetry event + broadcast to SSE
|
|
20
|
-
export function setAgentStatus(agentId: string, status: "running" | "stopped", reason?: string): Agent | null {
|
|
21
|
-
const agent = AgentDB.setStatus(agentId, status);
|
|
22
|
-
const event: TelemetryEvent = {
|
|
23
|
-
id: generateId(),
|
|
24
|
-
agent_id: agentId,
|
|
25
|
-
timestamp: new Date().toISOString(),
|
|
26
|
-
category: "system",
|
|
27
|
-
type: status === "running" ? "agent_started" : "agent_stopped",
|
|
28
|
-
level: "info",
|
|
29
|
-
data: { reason: reason || status },
|
|
30
|
-
};
|
|
31
|
-
TelemetryDB.insertBatch(agentId, [event]);
|
|
32
|
-
telemetryBroadcaster.broadcast([event]);
|
|
33
|
-
return agent;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Wait for agent to be healthy (with timeout)
|
|
37
|
-
// Note: /health endpoint is whitelisted in agent, no auth needed
|
|
38
|
-
export async function waitForAgentHealth(port: number, maxAttempts = 30, delayMs = 200): Promise<boolean> {
|
|
39
|
-
for (let i = 0; i < maxAttempts; i++) {
|
|
40
|
-
try {
|
|
41
|
-
const res = await fetch(`http://localhost:${port}/health`, {
|
|
42
|
-
signal: AbortSignal.timeout(1000),
|
|
43
|
-
});
|
|
44
|
-
if (res.ok) return true;
|
|
45
|
-
} catch {
|
|
46
|
-
// Not ready yet
|
|
47
|
-
}
|
|
48
|
-
await new Promise(r => setTimeout(r, delayMs));
|
|
49
|
-
}
|
|
50
|
-
return false;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Check if a port is free by trying to connect
|
|
54
|
-
export async function checkPortFree(port: number): Promise<boolean> {
|
|
55
|
-
return new Promise((resolve) => {
|
|
56
|
-
const net = require("net");
|
|
57
|
-
const server = net.createServer();
|
|
58
|
-
server.once("error", () => {
|
|
59
|
-
resolve(false); // Port in use
|
|
60
|
-
});
|
|
61
|
-
server.once("listening", () => {
|
|
62
|
-
server.close();
|
|
63
|
-
resolve(true); // Port is free
|
|
64
|
-
});
|
|
65
|
-
server.listen(port, "127.0.0.1");
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Make authenticated request to agent
|
|
70
|
-
export async function agentFetch(
|
|
71
|
-
agentId: string,
|
|
72
|
-
port: number,
|
|
73
|
-
endpoint: string,
|
|
74
|
-
options: RequestInit = {}
|
|
75
|
-
): Promise<Response> {
|
|
76
|
-
const apiKey = AgentDB.getApiKey(agentId);
|
|
77
|
-
const headers: Record<string, string> = {
|
|
78
|
-
...(options.headers as Record<string, string> || {}),
|
|
79
|
-
};
|
|
80
|
-
if (apiKey) {
|
|
81
|
-
headers["X-API-Key"] = apiKey;
|
|
82
|
-
}
|
|
83
|
-
return fetch(`http://localhost:${port}${endpoint}`, {
|
|
84
|
-
...options,
|
|
85
|
-
headers,
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Build operator config from features, resolving browser provider credentials
|
|
90
|
-
function buildOperatorConfig(features: Agent["features"], projectId: string | null) {
|
|
91
|
-
const opConfig = getOperatorConfig(features);
|
|
92
|
-
|
|
93
|
-
if (!opConfig.enabled) {
|
|
94
|
-
return {
|
|
95
|
-
enabled: false,
|
|
96
|
-
display_width: 1024,
|
|
97
|
-
display_height: 768,
|
|
98
|
-
max_actions_per_turn: 5,
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const browserProvider = opConfig.browser_provider || "browserengine";
|
|
103
|
-
const displayWidth = opConfig.display_width || 1024;
|
|
104
|
-
const displayHeight = opConfig.display_height || 768;
|
|
105
|
-
const maxActions = opConfig.max_actions_per_turn || 5;
|
|
106
|
-
|
|
107
|
-
const operatorResult: Record<string, unknown> = {
|
|
108
|
-
enabled: true,
|
|
109
|
-
browser_provider: browserProvider,
|
|
110
|
-
display_width: displayWidth,
|
|
111
|
-
display_height: displayHeight,
|
|
112
|
-
max_actions_per_turn: maxActions,
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
// Only include the active provider's config
|
|
116
|
-
if (browserProvider === "browserbase") {
|
|
117
|
-
const raw = ProviderKeys.getDecryptedForProject("browserbase", projectId);
|
|
118
|
-
if (raw) {
|
|
119
|
-
try {
|
|
120
|
-
const parsed = JSON.parse(raw);
|
|
121
|
-
operatorResult.browserbase = { api_key: parsed.api_key || raw, project_id: parsed.project_id || "" };
|
|
122
|
-
} catch {
|
|
123
|
-
operatorResult.browserbase = { api_key: raw, project_id: "" };
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
} else if (browserProvider === "steel") {
|
|
127
|
-
const apiKey = ProviderKeys.getDecryptedForProject("steel", projectId);
|
|
128
|
-
if (apiKey) {
|
|
129
|
-
operatorResult.steel = { api_key: apiKey, base_url: "https://api.steel.dev" };
|
|
130
|
-
}
|
|
131
|
-
} else if (browserProvider === "cdp") {
|
|
132
|
-
// CDP uses a URL, not an API key — stored as the provider key value
|
|
133
|
-
const cdpUrl = ProviderKeys.getDecryptedForProject("cdp", projectId);
|
|
134
|
-
if (cdpUrl) {
|
|
135
|
-
operatorResult.cdp = { url: cdpUrl };
|
|
136
|
-
}
|
|
137
|
-
} else {
|
|
138
|
-
// Default: browserengine
|
|
139
|
-
const apiKey = ProviderKeys.getDecryptedForProject("browserengine", projectId);
|
|
140
|
-
if (apiKey) {
|
|
141
|
-
operatorResult.browserengine = { api_key: apiKey, base_url: "https://api.browserengine.co" };
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return operatorResult;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function buildRealtimeConfig(features: Agent["features"], projectId: string | null) {
|
|
149
|
-
// Backwards compatible: handle both boolean and RealtimeConfig
|
|
150
|
-
const realtimeVal = features.realtime;
|
|
151
|
-
const isEnabled = typeof realtimeVal === "boolean" ? realtimeVal : (realtimeVal as any)?.enabled ?? false;
|
|
152
|
-
|
|
153
|
-
if (!isEnabled) {
|
|
154
|
-
return { enabled: false, provider: "standard", stt: {}, tts: {} };
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const rtConfig = typeof realtimeVal === "object" ? realtimeVal as Record<string, unknown> : {};
|
|
158
|
-
const rtProvider = (rtConfig.provider as string) || "standard";
|
|
159
|
-
|
|
160
|
-
// Native OpenAI Realtime mode
|
|
161
|
-
if (rtProvider === "openai") {
|
|
162
|
-
return {
|
|
163
|
-
enabled: true,
|
|
164
|
-
provider: "openai",
|
|
165
|
-
model: (rtConfig.model as string) || "gpt-realtime",
|
|
166
|
-
voice: (rtConfig.voice as string) || "alloy",
|
|
167
|
-
vad_type: (rtConfig.vadType as string) || "semantic_vad",
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Native Gemini Live mode
|
|
172
|
-
if (rtProvider === "gemini") {
|
|
173
|
-
return {
|
|
174
|
-
enabled: true,
|
|
175
|
-
provider: "gemini",
|
|
176
|
-
gemini_model: (rtConfig.geminiModel as string) || "",
|
|
177
|
-
gemini_voice: (rtConfig.geminiVoice as string) || "Kore",
|
|
178
|
-
google_search: (rtConfig.googleSearch as boolean) || false,
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// Standard mode: STT → Core LLM → TTS pipeline
|
|
183
|
-
const sttProvider = (rtConfig.sttProvider as string) || "elevenlabs";
|
|
184
|
-
const sttModel = (rtConfig.sttModel as string) || (sttProvider === "elevenlabs" ? "scribe_v2_realtime" : undefined);
|
|
185
|
-
const ttsProvider = (rtConfig.ttsProvider as string) || "elevenlabs";
|
|
186
|
-
const ttsModel = (rtConfig.ttsModel as string) || undefined;
|
|
187
|
-
|
|
188
|
-
const sttProviderDef = PROVIDERS[sttProvider as ProviderId];
|
|
189
|
-
const ttsProviderDef = PROVIDERS[ttsProvider as ProviderId];
|
|
190
|
-
|
|
191
|
-
const sttConfig: Record<string, unknown> = { provider: sttProvider };
|
|
192
|
-
if (sttModel) sttConfig.model = sttModel;
|
|
193
|
-
// For local providers, include the base URL in the config
|
|
194
|
-
if (sttProviderDef && "isLocal" in sttProviderDef && sttProviderDef.isLocal) {
|
|
195
|
-
sttConfig.base_url = ProviderKeys.getDecryptedForProject(sttProvider, projectId) ||
|
|
196
|
-
("defaultBaseUrl" in sttProviderDef ? (sttProviderDef as any).defaultBaseUrl : "");
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const ttsConfig: Record<string, unknown> = { provider: ttsProvider };
|
|
200
|
-
if (ttsModel) ttsConfig.model = ttsModel;
|
|
201
|
-
if (ttsProviderDef && "isLocal" in ttsProviderDef && ttsProviderDef.isLocal) {
|
|
202
|
-
ttsConfig.base_url = ProviderKeys.getDecryptedForProject(ttsProvider, projectId) ||
|
|
203
|
-
("defaultBaseUrl" in ttsProviderDef ? (ttsProviderDef as any).defaultBaseUrl : "");
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
return {
|
|
207
|
-
enabled: true,
|
|
208
|
-
provider: "standard",
|
|
209
|
-
stt: sttConfig,
|
|
210
|
-
tts: ttsConfig,
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Build agent config from apteva agent data
|
|
215
|
-
// Note: POST /config expects flat structure WITHOUT "agent" wrapper
|
|
216
|
-
export function buildAgentConfig(agent: Agent, providerKey: string) {
|
|
217
|
-
const features = agent.features;
|
|
218
|
-
|
|
219
|
-
// Get MCP server details for the agent's selected servers
|
|
220
|
-
const mcpServers: Array<{ name: string; type: "http"; url: string; headers: Record<string, string>; enabled: boolean }> = [];
|
|
221
|
-
|
|
222
|
-
// Get skill definitions for the agent's selected skills
|
|
223
|
-
const skillDefinitions: Array<{
|
|
224
|
-
name: string;
|
|
225
|
-
description: string;
|
|
226
|
-
instructions: string;
|
|
227
|
-
icon: string;
|
|
228
|
-
category: string;
|
|
229
|
-
tags: string[];
|
|
230
|
-
tools: string[];
|
|
231
|
-
enabled: boolean;
|
|
232
|
-
}> = [];
|
|
233
|
-
|
|
234
|
-
// Batch load skills and MCP servers (2 queries instead of N+M)
|
|
235
|
-
const skillMap = SkillDB.findByIds(agent.skills || []);
|
|
236
|
-
const mcpMap = McpServerDB.findByIds(agent.mcp_servers || []);
|
|
237
|
-
|
|
238
|
-
for (const skillId of agent.skills || []) {
|
|
239
|
-
const skill = skillMap.get(skillId);
|
|
240
|
-
if (!skill || !skill.enabled) continue;
|
|
241
|
-
|
|
242
|
-
skillDefinitions.push({
|
|
243
|
-
name: skill.name,
|
|
244
|
-
description: skill.description,
|
|
245
|
-
instructions: skill.content,
|
|
246
|
-
icon: "",
|
|
247
|
-
category: "",
|
|
248
|
-
tags: [],
|
|
249
|
-
tools: skill.allowed_tools || [],
|
|
250
|
-
enabled: true,
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
for (const id of agent.mcp_servers || []) {
|
|
255
|
-
const server = mcpMap.get(id);
|
|
256
|
-
if (!server) continue;
|
|
257
|
-
|
|
258
|
-
if (server.type === "local" && server.status === "running") {
|
|
259
|
-
// Local MCP server (in-process, no subprocess)
|
|
260
|
-
const baseUrl = `http://localhost:${process.env.PORT || 4280}`;
|
|
261
|
-
mcpServers.push({
|
|
262
|
-
name: server.name,
|
|
263
|
-
type: "http",
|
|
264
|
-
url: `${baseUrl}/api/mcp/servers/${server.id}/mcp`,
|
|
265
|
-
headers: {},
|
|
266
|
-
enabled: true,
|
|
267
|
-
});
|
|
268
|
-
} else if (server.type === "http" && server.url) {
|
|
269
|
-
// Remote HTTP server (Composio, Smithery, or custom)
|
|
270
|
-
mcpServers.push({
|
|
271
|
-
name: server.name,
|
|
272
|
-
type: "http",
|
|
273
|
-
url: server.url,
|
|
274
|
-
headers: server.headers || {},
|
|
275
|
-
enabled: true,
|
|
276
|
-
});
|
|
277
|
-
} else if (server.status === "running" && server.port) {
|
|
278
|
-
// Subprocess MCP server (npm, github, custom)
|
|
279
|
-
mcpServers.push({
|
|
280
|
-
name: server.name,
|
|
281
|
-
type: "http",
|
|
282
|
-
url: `http://localhost:${server.port}/mcp`,
|
|
283
|
-
headers: {},
|
|
284
|
-
enabled: true,
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// Auto-inject built-in platform MCP server for meta agent
|
|
290
|
-
if (agent.id === META_AGENT_ID) {
|
|
291
|
-
const baseUrl = `http://localhost:${process.env.PORT || 4280}`;
|
|
292
|
-
mcpServers.push({
|
|
293
|
-
name: "Apteva Platform",
|
|
294
|
-
type: "http",
|
|
295
|
-
url: `${baseUrl}/api/mcp/platform`,
|
|
296
|
-
headers: {},
|
|
297
|
-
enabled: true,
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
return {
|
|
302
|
-
id: agent.id,
|
|
303
|
-
name: agent.name,
|
|
304
|
-
description: agent.system_prompt,
|
|
305
|
-
public_url: `http://localhost:${agent.port}`,
|
|
306
|
-
llm: {
|
|
307
|
-
provider: agent.provider,
|
|
308
|
-
model: agent.model,
|
|
309
|
-
max_tokens: 4000,
|
|
310
|
-
max_turns: features.maxTurns || 50,
|
|
311
|
-
temperature: 0.7,
|
|
312
|
-
system_prompt: agent.system_prompt,
|
|
313
|
-
vision: {
|
|
314
|
-
enabled: features.vision,
|
|
315
|
-
max_images: 20,
|
|
316
|
-
max_image_size: 5242880,
|
|
317
|
-
allowed_types: ["jpeg", "png", "gif", "webp"],
|
|
318
|
-
resize_images: true,
|
|
319
|
-
max_dimension: 1568,
|
|
320
|
-
pdf: {
|
|
321
|
-
enabled: features.vision,
|
|
322
|
-
max_file_size: 33554432,
|
|
323
|
-
max_pages: 100,
|
|
324
|
-
allow_urls: true,
|
|
325
|
-
},
|
|
326
|
-
},
|
|
327
|
-
parallel_tools: {
|
|
328
|
-
enabled: true,
|
|
329
|
-
max_concurrent: 10,
|
|
330
|
-
},
|
|
331
|
-
tools: [], // Clear any old tool whitelist - agent uses all registered tools
|
|
332
|
-
builtin_tools: [
|
|
333
|
-
...(features.builtinTools?.webSearch ? [{ type: "web_search_20250305", name: "web_search" }] : []),
|
|
334
|
-
...(features.builtinTools?.webFetch ? [{ type: "web_fetch_20250910", name: "web_fetch" }] : []),
|
|
335
|
-
],
|
|
336
|
-
},
|
|
337
|
-
tasks: {
|
|
338
|
-
enabled: features.tasks,
|
|
339
|
-
allow_scheduling: true,
|
|
340
|
-
allow_recurring: true,
|
|
341
|
-
max_tasks: 100,
|
|
342
|
-
auto_execute: false,
|
|
343
|
-
},
|
|
344
|
-
scheduler: {
|
|
345
|
-
enabled: features.tasks,
|
|
346
|
-
interval: "1m",
|
|
347
|
-
max_tasks: 100,
|
|
348
|
-
},
|
|
349
|
-
memory: {
|
|
350
|
-
enabled: features.memory,
|
|
351
|
-
embedding_model: "text-embedding-3-small",
|
|
352
|
-
decision_model: "gpt-4o-mini",
|
|
353
|
-
max_memories_per_query: 20,
|
|
354
|
-
min_importance: 0.3,
|
|
355
|
-
min_similarity: 0.3,
|
|
356
|
-
auto_prune: true,
|
|
357
|
-
max_memories: 10000,
|
|
358
|
-
embedding_provider: "openai",
|
|
359
|
-
auto_extract_memories: features.memory ? true : null,
|
|
360
|
-
auto_ingest_files: true,
|
|
361
|
-
},
|
|
362
|
-
operator: buildOperatorConfig(features, agent.project_id),
|
|
363
|
-
mcp: {
|
|
364
|
-
enabled: features.mcp || agent.id === META_AGENT_ID,
|
|
365
|
-
base_url: "http://localhost:3000/mcp",
|
|
366
|
-
timeout: "30s",
|
|
367
|
-
retry_count: 3,
|
|
368
|
-
cache_ttl: "15m",
|
|
369
|
-
servers: mcpServers,
|
|
370
|
-
},
|
|
371
|
-
realtime: buildRealtimeConfig(features, agent.project_id),
|
|
372
|
-
context: {
|
|
373
|
-
max_messages: 30,
|
|
374
|
-
max_tokens: 0,
|
|
375
|
-
keep_images: 5,
|
|
376
|
-
},
|
|
377
|
-
filesystem: {
|
|
378
|
-
enabled: features.files,
|
|
379
|
-
max_file_size: 10485760,
|
|
380
|
-
max_total_size: 104857600,
|
|
381
|
-
auto_extract: true,
|
|
382
|
-
auto_cleanup: true,
|
|
383
|
-
retention_days: 7,
|
|
384
|
-
},
|
|
385
|
-
telemetry: {
|
|
386
|
-
enabled: true,
|
|
387
|
-
endpoint: `http://localhost:${process.env.PORT || 4280}/api/telemetry`,
|
|
388
|
-
batch_size: 1,
|
|
389
|
-
flush_interval: 1, // Every 1 second
|
|
390
|
-
categories: [], // Empty = all categories
|
|
391
|
-
},
|
|
392
|
-
skills: {
|
|
393
|
-
enabled: skillDefinitions.length > 0,
|
|
394
|
-
definitions: skillDefinitions,
|
|
395
|
-
},
|
|
396
|
-
agents: (() => {
|
|
397
|
-
const multiAgentConfig = getMultiAgentConfig(features, agent.project_id);
|
|
398
|
-
const baseUrl = process.env.PUBLIC_URL || `http://localhost:${process.env.PORT || 4280}`;
|
|
399
|
-
return {
|
|
400
|
-
enabled: multiAgentConfig.enabled,
|
|
401
|
-
group: multiAgentConfig.group || agent.project_id || undefined,
|
|
402
|
-
// This agent's reachable URL for peer communication
|
|
403
|
-
url: `http://localhost:${agent.port}`,
|
|
404
|
-
// Discovery endpoint to find peer agents in the same group
|
|
405
|
-
discovery_url: `${baseUrl}/api/discovery/agents`,
|
|
406
|
-
};
|
|
407
|
-
})(),
|
|
408
|
-
};
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
// Push config to running agent (with authentication)
|
|
412
|
-
export async function pushConfigToAgent(agentId: string, port: number, config: any): Promise<{ success: boolean; error?: string }> {
|
|
413
|
-
try {
|
|
414
|
-
const res = await agentFetch(agentId, port, "/config", {
|
|
415
|
-
method: "POST",
|
|
416
|
-
headers: { "Content-Type": "application/json" },
|
|
417
|
-
body: JSON.stringify(config),
|
|
418
|
-
signal: AbortSignal.timeout(5000),
|
|
419
|
-
});
|
|
420
|
-
if (res.ok) {
|
|
421
|
-
return { success: true };
|
|
422
|
-
}
|
|
423
|
-
const data = await res.json().catch(() => ({}));
|
|
424
|
-
return { success: false, error: data.error || `HTTP ${res.status}` };
|
|
425
|
-
} catch (err) {
|
|
426
|
-
return { success: false, error: String(err) };
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
// Push skills to running agent via /skills endpoint (not config)
|
|
431
|
-
export async function pushSkillsToAgent(agentId: string, port: number, skills: Array<{
|
|
432
|
-
name: string;
|
|
433
|
-
description: string;
|
|
434
|
-
instructions: string;
|
|
435
|
-
icon?: string;
|
|
436
|
-
category?: string;
|
|
437
|
-
tags?: string[];
|
|
438
|
-
tools?: string[];
|
|
439
|
-
enabled: boolean;
|
|
440
|
-
}>): Promise<{ success: boolean; error?: string }> {
|
|
441
|
-
if (skills.length === 0) {
|
|
442
|
-
return { success: true };
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
try {
|
|
446
|
-
// Push all skills in parallel - try PUT first (update), then POST (create) if not found
|
|
447
|
-
await Promise.allSettled(skills.map(async (skill) => {
|
|
448
|
-
let res = await agentFetch(agentId, port, "/skills", {
|
|
449
|
-
method: "PUT",
|
|
450
|
-
headers: { "Content-Type": "application/json" },
|
|
451
|
-
body: JSON.stringify(skill),
|
|
452
|
-
signal: AbortSignal.timeout(5000),
|
|
453
|
-
});
|
|
454
|
-
|
|
455
|
-
if (res.status === 404) {
|
|
456
|
-
res = await agentFetch(agentId, port, "/skills", {
|
|
457
|
-
method: "POST",
|
|
458
|
-
headers: { "Content-Type": "application/json" },
|
|
459
|
-
body: JSON.stringify(skill),
|
|
460
|
-
signal: AbortSignal.timeout(5000),
|
|
461
|
-
});
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
if (!res.ok) {
|
|
465
|
-
const data = await res.json().catch(() => ({}));
|
|
466
|
-
console.error(`[pushSkillsToAgent] Failed to push skill ${skill.name}:`, data.error || res.status);
|
|
467
|
-
}
|
|
468
|
-
}));
|
|
469
|
-
|
|
470
|
-
// Enable skills globally via POST /skills/status
|
|
471
|
-
const statusRes = await agentFetch(agentId, port, "/skills/status", {
|
|
472
|
-
method: "POST",
|
|
473
|
-
headers: { "Content-Type": "application/json" },
|
|
474
|
-
body: JSON.stringify({ enabled: true }),
|
|
475
|
-
signal: AbortSignal.timeout(5000),
|
|
476
|
-
});
|
|
477
|
-
|
|
478
|
-
if (!statusRes.ok) {
|
|
479
|
-
const data = await statusRes.json().catch(() => ({}));
|
|
480
|
-
return { success: false, error: data.error || `HTTP ${statusRes.status}` };
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
console.log(`[pushSkillsToAgent] Pushed ${skills.length} skill(s) to agent`);
|
|
484
|
-
return { success: true };
|
|
485
|
-
} catch (err) {
|
|
486
|
-
return { success: false, error: String(err) };
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
// Exported helper to start an agent process (used by API route and auto-restart)
|
|
491
|
-
export async function startAgentProcess(
|
|
492
|
-
agent: Agent,
|
|
493
|
-
options: { silent?: boolean; cleanData?: boolean } = {}
|
|
494
|
-
): Promise<{ success: boolean; port?: number; error?: string }> {
|
|
495
|
-
const { silent = false, cleanData = false } = options;
|
|
496
|
-
|
|
497
|
-
// Check if binary exists
|
|
498
|
-
if (!binaryExists(BIN_DIR)) {
|
|
499
|
-
return { success: false, error: "Agent binary not available" };
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
// Check if already running (process map)
|
|
503
|
-
if (agentProcesses.has(agent.id)) {
|
|
504
|
-
return { success: false, error: "Agent already running" };
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
// Check if already being started (race condition prevention)
|
|
508
|
-
if (agentsStarting.has(agent.id)) {
|
|
509
|
-
return { success: false, error: "Agent is already starting" };
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
// Mark as starting
|
|
513
|
-
agentsStarting.add(agent.id);
|
|
514
|
-
|
|
515
|
-
// Get provider config for env var name
|
|
516
|
-
const providerConfig = PROVIDERS[agent.provider as ProviderId];
|
|
517
|
-
if (!providerConfig) {
|
|
518
|
-
agentsStarting.delete(agent.id);
|
|
519
|
-
return { success: false, error: `Unknown provider: ${agent.provider}` };
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
// Get the API key for the agent's provider (local providers like Ollama use URL instead)
|
|
523
|
-
const isLocalProvider = "isLocal" in providerConfig && providerConfig.isLocal;
|
|
524
|
-
const providerKey = ProviderKeys.getDecrypted(agent.provider);
|
|
525
|
-
if (!providerKey && !isLocalProvider) {
|
|
526
|
-
agentsStarting.delete(agent.id);
|
|
527
|
-
return { success: false, error: `No API key for provider: ${agent.provider}` };
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
// Use agent's permanently assigned port
|
|
531
|
-
const port = agent.port;
|
|
532
|
-
if (!port) {
|
|
533
|
-
agentsStarting.delete(agent.id);
|
|
534
|
-
return { success: false, error: "Agent has no assigned port" };
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
// Get or create API key for the agent
|
|
538
|
-
const agentApiKey = AgentDB.ensureApiKey(agent.id);
|
|
539
|
-
if (!agentApiKey) {
|
|
540
|
-
agentsStarting.delete(agent.id);
|
|
541
|
-
return { success: false, error: "Failed to get/create agent API key" };
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
try {
|
|
545
|
-
// Check if something is already running on this port (orphaned process)
|
|
546
|
-
try {
|
|
547
|
-
const res = await fetch(`http://localhost:${port}/health`, { signal: AbortSignal.timeout(500) });
|
|
548
|
-
if (res.ok) {
|
|
549
|
-
// Something is running - try to shut it down
|
|
550
|
-
if (!silent) {
|
|
551
|
-
console.log(` Port ${port} in use, stopping orphaned process...`);
|
|
552
|
-
}
|
|
553
|
-
try {
|
|
554
|
-
await fetch(`http://localhost:${port}/shutdown`, { method: "POST", signal: AbortSignal.timeout(1000) });
|
|
555
|
-
} catch {
|
|
556
|
-
// Shutdown failed - process might not support it
|
|
557
|
-
}
|
|
558
|
-
// Wait longer for port to be released
|
|
559
|
-
await new Promise(r => setTimeout(r, 1500));
|
|
560
|
-
}
|
|
561
|
-
} catch {
|
|
562
|
-
// No HTTP response - but port might still be bound by zombie process
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
// Double-check port is actually free by trying to connect
|
|
566
|
-
const isPortFree = await checkPortFree(port);
|
|
567
|
-
if (!isPortFree) {
|
|
568
|
-
if (!silent) {
|
|
569
|
-
console.log(` Port ${port} still in use, trying to kill process...`);
|
|
570
|
-
}
|
|
571
|
-
// Try to kill process using the port (Linux/Mac)
|
|
572
|
-
try {
|
|
573
|
-
const { execSync } = await import("child_process");
|
|
574
|
-
execSync(`lsof -ti:${port} | xargs kill -9 2>/dev/null || true`, { stdio: "ignore" });
|
|
575
|
-
await new Promise(r => setTimeout(r, 1000));
|
|
576
|
-
} catch {
|
|
577
|
-
// Ignore errors
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
// Final check
|
|
581
|
-
const stillInUse = !(await checkPortFree(port));
|
|
582
|
-
if (stillInUse) {
|
|
583
|
-
agentsStarting.delete(agent.id);
|
|
584
|
-
return { success: false, error: `Port ${port} is still in use` };
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
// Handle data directory
|
|
589
|
-
const agentDataDir = join(AGENTS_DATA_DIR, agent.id);
|
|
590
|
-
if (cleanData && existsSync(agentDataDir)) {
|
|
591
|
-
// Clean old data if requested
|
|
592
|
-
rmSync(agentDataDir, { recursive: true, force: true });
|
|
593
|
-
if (!silent) {
|
|
594
|
-
console.log(` Cleaned old data directory`);
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
if (!existsSync(agentDataDir)) {
|
|
598
|
-
mkdirSync(agentDataDir, { recursive: true });
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
if (!silent) {
|
|
602
|
-
console.log(`Starting agent ${agent.name} on port ${port}...`);
|
|
603
|
-
console.log(` Provider: ${agent.provider}`);
|
|
604
|
-
console.log(` Data dir: ${agentDataDir}`);
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
// Build environment with provider key and agent API key
|
|
608
|
-
// CONFIG_PATH ensures each agent has its own config file (prevents sharing)
|
|
609
|
-
// Remove stale persisted config — platform is source of truth and pushes fresh config after boot
|
|
610
|
-
const agentConfigPath = join(agentDataDir, "agent-config.json");
|
|
611
|
-
rmSync(agentConfigPath, { force: true });
|
|
612
|
-
const env: Record<string, string> = {
|
|
613
|
-
...process.env as Record<string, string>,
|
|
614
|
-
PORT: String(port),
|
|
615
|
-
DATA_DIR: agentDataDir,
|
|
616
|
-
CONFIG_PATH: agentConfigPath,
|
|
617
|
-
AGENT_API_KEY: agentApiKey,
|
|
618
|
-
[providerConfig.envVar]: providerKey || ("defaultBaseUrl" in providerConfig ? (providerConfig as any).defaultBaseUrl : ""),
|
|
619
|
-
};
|
|
620
|
-
|
|
621
|
-
// If memory is enabled and agent doesn't use OpenAI, also pass OpenAI key for embeddings
|
|
622
|
-
if (agent.features.memory && agent.provider !== "openai") {
|
|
623
|
-
const openaiKey = ProviderKeys.getDecrypted("openai");
|
|
624
|
-
if (openaiKey) {
|
|
625
|
-
env.OPENAI_API_KEY = openaiKey;
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
// If realtime voice is enabled, pass voice provider keys/URLs for STT/TTS
|
|
630
|
-
const rtEnabled = typeof agent.features.realtime === "boolean"
|
|
631
|
-
? agent.features.realtime
|
|
632
|
-
: (agent.features.realtime as any)?.enabled ?? false;
|
|
633
|
-
if (rtEnabled) {
|
|
634
|
-
// Cloud voice provider keys (backwards compat — always pass if available)
|
|
635
|
-
const elevenlabsKey = ProviderKeys.getDecryptedForProject("elevenlabs", agent.project_id);
|
|
636
|
-
if (elevenlabsKey) env.ELEVENLABS_API_KEY = elevenlabsKey;
|
|
637
|
-
const deepgramKey = ProviderKeys.getDecryptedForProject("deepgram", agent.project_id);
|
|
638
|
-
if (deepgramKey) env.DEEPGRAM_API_KEY = deepgramKey;
|
|
639
|
-
|
|
640
|
-
// Local voice provider URLs
|
|
641
|
-
const localVoiceProviders = ["speaches", "whisper_cpp", "kokoro", "piper", "fish_speech"] as const;
|
|
642
|
-
for (const pvId of localVoiceProviders) {
|
|
643
|
-
const pv = PROVIDERS[pvId as ProviderId];
|
|
644
|
-
if (pv) {
|
|
645
|
-
const url = ProviderKeys.getDecryptedForProject(pvId, agent.project_id);
|
|
646
|
-
if (url) env[pv.envVar] = url;
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
// Pass API keys for native realtime providers (OpenAI/Gemini)
|
|
651
|
-
// Agent may use a different main LLM provider (e.g., Claude) but needs these for voice
|
|
652
|
-
const rtConfig = typeof agent.features.realtime === "object" ? agent.features.realtime as Record<string, unknown> : {};
|
|
653
|
-
const rtProvider = rtConfig.provider as string;
|
|
654
|
-
if (rtProvider === "openai" && agent.provider !== "openai") {
|
|
655
|
-
const openaiKey = ProviderKeys.getDecryptedForProject("openai", agent.project_id);
|
|
656
|
-
if (openaiKey) env.OPENAI_API_KEY = openaiKey;
|
|
657
|
-
}
|
|
658
|
-
if (rtProvider === "gemini" && agent.provider !== "gemini") {
|
|
659
|
-
const geminiKey = ProviderKeys.getDecryptedForProject("gemini", agent.project_id);
|
|
660
|
-
if (geminiKey) env.GEMINI_API_KEY = geminiKey;
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
// Get binary path dynamically (allows hot-reload of new binary versions)
|
|
665
|
-
const binaryPath = getBinaryPathForAgent();
|
|
666
|
-
|
|
667
|
-
// Write VERSION file so the binary knows its version (ldflags not set in npm builds)
|
|
668
|
-
try {
|
|
669
|
-
const { dirname } = await import("path");
|
|
670
|
-
const pkgJsonPath = require.resolve("@apteva/agent-linux-x64/package.json");
|
|
671
|
-
const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
|
|
672
|
-
if (pkg.version) {
|
|
673
|
-
writeFileSync(join(agentDataDir, "VERSION"), pkg.version);
|
|
674
|
-
}
|
|
675
|
-
} catch {}
|
|
676
|
-
|
|
677
|
-
const proc = spawn({
|
|
678
|
-
cmd: [binaryPath],
|
|
679
|
-
cwd: agentDataDir,
|
|
680
|
-
env,
|
|
681
|
-
stdout: "ignore",
|
|
682
|
-
stderr: "ignore",
|
|
683
|
-
});
|
|
684
|
-
|
|
685
|
-
// Store process with port for tracking
|
|
686
|
-
agentProcesses.set(agent.id, { proc, port });
|
|
687
|
-
|
|
688
|
-
// Detect unexpected process exits (crashes) — but not during server shutdown
|
|
689
|
-
proc.exited.then((code) => {
|
|
690
|
-
if (isShuttingDown()) return; // Don't update DB during shutdown — keeps status "running" for auto-restart
|
|
691
|
-
if (agentProcesses.has(agent.id)) {
|
|
692
|
-
agentProcesses.delete(agent.id);
|
|
693
|
-
setAgentStatus(agent.id, "stopped", code === 0 ? "exited" : "crashed");
|
|
694
|
-
}
|
|
695
|
-
});
|
|
696
|
-
|
|
697
|
-
// Wait for agent to be healthy
|
|
698
|
-
if (!silent) {
|
|
699
|
-
console.log(` Waiting for agent to be ready...`);
|
|
700
|
-
}
|
|
701
|
-
const isHealthy = await waitForAgentHealth(port);
|
|
702
|
-
if (!isHealthy) {
|
|
703
|
-
if (!silent) {
|
|
704
|
-
console.error(` Agent failed to start (health check timeout)`);
|
|
705
|
-
}
|
|
706
|
-
proc.kill();
|
|
707
|
-
agentProcesses.delete(agent.id);
|
|
708
|
-
agentsStarting.delete(agent.id);
|
|
709
|
-
return { success: false, error: "Health check timeout" };
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
// Push configuration to the agent
|
|
713
|
-
if (!silent) {
|
|
714
|
-
console.log(` Pushing configuration...`);
|
|
715
|
-
}
|
|
716
|
-
const config = buildAgentConfig(agent, providerKey);
|
|
717
|
-
if (agent.features.realtime) {
|
|
718
|
-
console.log(`[DEBUG] realtime config being pushed:`, JSON.stringify(config.realtime, null, 2));
|
|
719
|
-
}
|
|
720
|
-
const configResult = await pushConfigToAgent(agent.id, port, config);
|
|
721
|
-
if (!configResult.success) {
|
|
722
|
-
if (!silent) {
|
|
723
|
-
console.error(` Failed to configure agent: ${configResult.error}`);
|
|
724
|
-
}
|
|
725
|
-
// Agent is running but not configured - still usable but log warning
|
|
726
|
-
} else if (!silent) {
|
|
727
|
-
console.log(` Configuration applied successfully`);
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
// Push skills via /skills endpoint (separate from config)
|
|
731
|
-
if (config.skills?.definitions?.length > 0) {
|
|
732
|
-
const skillsResult = await pushSkillsToAgent(agent.id, port, config.skills.definitions);
|
|
733
|
-
if (!skillsResult.success && !silent) {
|
|
734
|
-
console.error(` Failed to push skills: ${skillsResult.error}`);
|
|
735
|
-
} else if (!silent) {
|
|
736
|
-
console.log(` Skills pushed successfully (${config.skills.definitions.length} skills)`);
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
// Update status in database + emit telemetry event
|
|
741
|
-
setAgentStatus(agent.id, "running");
|
|
742
|
-
|
|
743
|
-
if (!silent) {
|
|
744
|
-
console.log(`Agent ${agent.name} started on port ${port} (pid: ${proc.pid})`);
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
agentsStarting.delete(agent.id);
|
|
748
|
-
return { success: true, port };
|
|
749
|
-
} catch (err) {
|
|
750
|
-
agentsStarting.delete(agent.id);
|
|
751
|
-
if (!silent) {
|
|
752
|
-
console.error(`Failed to start agent: ${err}`);
|
|
753
|
-
}
|
|
754
|
-
return { success: false, error: String(err) };
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
// Strip legacy "mode" field from multi-agent config
|
|
759
|
-
function cleanFeatures(features: Agent["features"]): Agent["features"] {
|
|
760
|
-
if (features.agents && typeof features.agents === "object") {
|
|
761
|
-
const { mode, ...rest } = features.agents as any;
|
|
762
|
-
return { ...features, agents: rest };
|
|
763
|
-
}
|
|
764
|
-
return features;
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
// Transform DB agent to API response format (camelCase for frontend compatibility)
|
|
768
|
-
// Uses batch queries + light MCP loading (no decryption) for performance
|
|
769
|
-
export function toApiAgent(agent: Agent) {
|
|
770
|
-
// Batch load MCP servers (light = no decryption) and skills in 2 queries
|
|
771
|
-
const mcpMap = McpServerDB.findByIdsLight(agent.mcp_servers || []);
|
|
772
|
-
const skillMap = SkillDB.findByIds(agent.skills || []);
|
|
773
|
-
|
|
774
|
-
const mcpServerDetails = (agent.mcp_servers || [])
|
|
775
|
-
.map(id => mcpMap.get(id))
|
|
776
|
-
.filter((s): s is NonNullable<typeof s> => !!s)
|
|
777
|
-
.map(s => ({
|
|
778
|
-
id: s.id,
|
|
779
|
-
name: s.name,
|
|
780
|
-
type: s.type,
|
|
781
|
-
status: s.status,
|
|
782
|
-
port: s.port,
|
|
783
|
-
url: s.url,
|
|
784
|
-
}));
|
|
785
|
-
|
|
786
|
-
const skillDetails = (agent.skills || [])
|
|
787
|
-
.map(id => skillMap.get(id))
|
|
788
|
-
.filter((s): s is NonNullable<typeof s> => !!s)
|
|
789
|
-
.map(s => ({
|
|
790
|
-
id: s.id,
|
|
791
|
-
name: s.name,
|
|
792
|
-
description: s.description,
|
|
793
|
-
version: s.version,
|
|
794
|
-
enabled: s.enabled,
|
|
795
|
-
}));
|
|
796
|
-
|
|
797
|
-
// Look up subscriptions
|
|
798
|
-
const subscriptions = SubscriptionDB.findByAgentId(agent.id).map(s => ({
|
|
799
|
-
id: s.id, trigger_slug: s.trigger_slug, enabled: s.enabled,
|
|
800
|
-
}));
|
|
801
|
-
|
|
802
|
-
return {
|
|
803
|
-
id: agent.id,
|
|
804
|
-
name: agent.name,
|
|
805
|
-
model: agent.model,
|
|
806
|
-
provider: agent.provider,
|
|
807
|
-
systemPrompt: agent.system_prompt,
|
|
808
|
-
status: agent.status,
|
|
809
|
-
port: agent.port,
|
|
810
|
-
features: cleanFeatures(agent.features),
|
|
811
|
-
mcpServers: agent.mcp_servers,
|
|
812
|
-
mcpServerDetails,
|
|
813
|
-
skills: agent.skills,
|
|
814
|
-
skillDetails,
|
|
815
|
-
subscriptions,
|
|
816
|
-
projectId: agent.project_id,
|
|
817
|
-
createdAt: agent.created_at,
|
|
818
|
-
updatedAt: agent.updated_at,
|
|
819
|
-
};
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
// Batch transform: fetch all MCP servers + skills + subscriptions in 3 queries instead of N per agent
|
|
823
|
-
export function toApiAgentsBatch(agents: Agent[]) {
|
|
824
|
-
// Collect all unique IDs
|
|
825
|
-
const allMcpIds = new Set<string>();
|
|
826
|
-
const allSkillIds = new Set<string>();
|
|
827
|
-
const allAgentIds: string[] = [];
|
|
828
|
-
for (const agent of agents) {
|
|
829
|
-
allAgentIds.push(agent.id);
|
|
830
|
-
for (const id of agent.mcp_servers || []) allMcpIds.add(id);
|
|
831
|
-
for (const id of agent.skills || []) allSkillIds.add(id);
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
// Batch load in 3 queries (Light = no decryption for MCP servers)
|
|
835
|
-
const mcpMap = McpServerDB.findByIdsLight([...allMcpIds]);
|
|
836
|
-
const skillMap = SkillDB.findByIds([...allSkillIds]);
|
|
837
|
-
const subsMap = SubscriptionDB.findByAgentIds(allAgentIds);
|
|
838
|
-
|
|
839
|
-
return agents.map(agent => {
|
|
840
|
-
const mcpServerDetails = (agent.mcp_servers || [])
|
|
841
|
-
.map(id => mcpMap.get(id))
|
|
842
|
-
.filter((s): s is NonNullable<typeof s> => !!s)
|
|
843
|
-
.map(s => ({ id: s.id, name: s.name, type: s.type, status: s.status, port: s.port, url: s.url }));
|
|
844
|
-
|
|
845
|
-
const skillDetails = (agent.skills || [])
|
|
846
|
-
.map(id => skillMap.get(id))
|
|
847
|
-
.filter((s): s is NonNullable<typeof s> => !!s)
|
|
848
|
-
.map(s => ({ id: s.id, name: s.name, description: s.description, version: s.version, enabled: s.enabled }));
|
|
849
|
-
|
|
850
|
-
const subscriptions = (subsMap.get(agent.id) || []).map(s => ({
|
|
851
|
-
id: s.id, trigger_slug: s.trigger_slug, enabled: s.enabled,
|
|
852
|
-
}));
|
|
853
|
-
|
|
854
|
-
return {
|
|
855
|
-
id: agent.id, name: agent.name, model: agent.model, provider: agent.provider,
|
|
856
|
-
systemPrompt: agent.system_prompt, status: agent.status, port: agent.port,
|
|
857
|
-
features: cleanFeatures(agent.features), mcpServers: agent.mcp_servers, mcpServerDetails,
|
|
858
|
-
skills: agent.skills, skillDetails, subscriptions, projectId: agent.project_id,
|
|
859
|
-
createdAt: agent.created_at, updatedAt: agent.updated_at,
|
|
860
|
-
};
|
|
861
|
-
});
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
// Transform DB project to API response format
|
|
865
|
-
export function toApiProject(project: Project) {
|
|
866
|
-
return {
|
|
867
|
-
id: project.id,
|
|
868
|
-
name: project.name,
|
|
869
|
-
description: project.description,
|
|
870
|
-
color: project.color,
|
|
871
|
-
createdAt: project.created_at,
|
|
872
|
-
updatedAt: project.updated_at,
|
|
873
|
-
};
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
// Helper to fetch from a running agent (with authentication + timeout)
|
|
877
|
-
export async function fetchFromAgent(agentId: string, port: number, endpoint: string, timeoutMs = 3000): Promise<any> {
|
|
878
|
-
try {
|
|
879
|
-
const response = await agentFetch(agentId, port, endpoint, {
|
|
880
|
-
headers: { "Accept": "application/json" },
|
|
881
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
882
|
-
});
|
|
883
|
-
if (response.ok) {
|
|
884
|
-
return await response.json();
|
|
885
|
-
}
|
|
886
|
-
return null;
|
|
887
|
-
} catch {
|
|
888
|
-
return null;
|
|
889
|
-
}
|
|
890
|
-
}
|