apteva 0.4.57 → 0.7.1

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.
Files changed (142) hide show
  1. package/README.md +216 -54
  2. package/cli.js +35 -0
  3. package/install.js +92 -0
  4. package/package.json +15 -76
  5. package/LICENSE +0 -63
  6. package/bin/apteva.js +0 -196
  7. package/dist/ActivityPage.kxzzb4yc.js +0 -3
  8. package/dist/ApiDocsPage.zq998hbm.js +0 -4
  9. package/dist/App.55rea8mn.js +0 -61
  10. package/dist/App.5ywb23z4.js +0 -53
  11. package/dist/App.6thds120.js +0 -4
  12. package/dist/App.9tctxzqm.js +0 -8
  13. package/dist/App.a8r8ttaz.js +0 -4
  14. package/dist/App.agsv5bje.js +0 -4
  15. package/dist/App.cepapqmx.js +0 -4
  16. package/dist/App.dp041gb3.js +0 -221
  17. package/dist/App.fds72zb5.js +0 -4
  18. package/dist/App.fg9qj2dq.js +0 -4
  19. package/dist/App.ndfejbm9.js +0 -4
  20. package/dist/App.nxmfmq1h.js +0 -13
  21. package/dist/App.qdfyt8ba.js +0 -4
  22. package/dist/App.x2d0ygt6.js +0 -4
  23. package/dist/App.yt9p4nr3.js +0 -20
  24. package/dist/App.zn4mw16t.js +0 -1
  25. package/dist/ConnectionsPage.8r96ryw7.js +0 -3
  26. package/dist/McpPage.3cwh0gnd.js +0 -3
  27. package/dist/SettingsPage.ykgdh5ev.js +0 -3
  28. package/dist/SkillsPage.4np1s65b.js +0 -3
  29. package/dist/TasksPage.4g08t7p6.js +0 -3
  30. package/dist/TelemetryPage.72w9pwcp.js +0 -3
  31. package/dist/TestsPage.z4fk3r7r.js +0 -3
  32. package/dist/ThreadsPage.63tcajeh.js +0 -3
  33. package/dist/apteva-kit.css +0 -1
  34. package/dist/icon.png +0 -0
  35. package/dist/index.html +0 -16
  36. package/dist/styles.css +0 -1
  37. package/scripts/postinstall.mjs +0 -102
  38. package/src/auth/index.ts +0 -394
  39. package/src/auth/middleware.ts +0 -213
  40. package/src/binary.ts +0 -536
  41. package/src/channels/index.ts +0 -40
  42. package/src/channels/telegram.ts +0 -311
  43. package/src/crypto.ts +0 -301
  44. package/src/db-tests.ts +0 -174
  45. package/src/db.ts +0 -3133
  46. package/src/integrations/agentdojo.ts +0 -559
  47. package/src/integrations/composio.ts +0 -437
  48. package/src/integrations/index.ts +0 -87
  49. package/src/integrations/skillsmp.ts +0 -318
  50. package/src/mcp-client.ts +0 -605
  51. package/src/mcp-handler.ts +0 -394
  52. package/src/mcp-platform.ts +0 -2403
  53. package/src/openapi.ts +0 -2410
  54. package/src/providers.ts +0 -597
  55. package/src/routes/api/agent-utils.ts +0 -890
  56. package/src/routes/api/agents.ts +0 -916
  57. package/src/routes/api/api-keys.ts +0 -95
  58. package/src/routes/api/channels.ts +0 -182
  59. package/src/routes/api/helpers.ts +0 -12
  60. package/src/routes/api/integrations.ts +0 -639
  61. package/src/routes/api/mcp.ts +0 -574
  62. package/src/routes/api/meta-agent.ts +0 -195
  63. package/src/routes/api/projects.ts +0 -112
  64. package/src/routes/api/providers.ts +0 -424
  65. package/src/routes/api/skills.ts +0 -537
  66. package/src/routes/api/system.ts +0 -333
  67. package/src/routes/api/telemetry.ts +0 -203
  68. package/src/routes/api/tests.ts +0 -148
  69. package/src/routes/api/triggers.ts +0 -518
  70. package/src/routes/api/users.ts +0 -148
  71. package/src/routes/api/webhooks.ts +0 -171
  72. package/src/routes/api.ts +0 -53
  73. package/src/routes/auth.ts +0 -251
  74. package/src/routes/share.ts +0 -86
  75. package/src/routes/static.ts +0 -131
  76. package/src/server.ts +0 -642
  77. package/src/test-runner.ts +0 -598
  78. package/src/triggers/agentdojo.ts +0 -253
  79. package/src/triggers/composio.ts +0 -264
  80. package/src/triggers/index.ts +0 -71
  81. package/src/tui/AgentList.tsx +0 -145
  82. package/src/tui/App.tsx +0 -102
  83. package/src/tui/Login.tsx +0 -104
  84. package/src/tui/api.ts +0 -72
  85. package/src/tui/index.tsx +0 -7
  86. package/src/web/App.tsx +0 -455
  87. package/src/web/components/activity/ActivityPage.tsx +0 -314
  88. package/src/web/components/activity/index.ts +0 -1
  89. package/src/web/components/agents/AgentCard.tsx +0 -189
  90. package/src/web/components/agents/AgentPanel.tsx +0 -2244
  91. package/src/web/components/agents/AgentsView.tsx +0 -180
  92. package/src/web/components/agents/CreateAgentModal.tsx +0 -475
  93. package/src/web/components/agents/index.ts +0 -4
  94. package/src/web/components/api/ApiDocsPage.tsx +0 -842
  95. package/src/web/components/auth/CreateAccountStep.tsx +0 -176
  96. package/src/web/components/auth/LoginPage.tsx +0 -91
  97. package/src/web/components/auth/index.ts +0 -2
  98. package/src/web/components/common/Icons.tsx +0 -250
  99. package/src/web/components/common/LoadingSpinner.tsx +0 -44
  100. package/src/web/components/common/Modal.tsx +0 -199
  101. package/src/web/components/common/Select.tsx +0 -97
  102. package/src/web/components/common/index.ts +0 -20
  103. package/src/web/components/connections/ConnectionsPage.tsx +0 -54
  104. package/src/web/components/connections/IntegrationsTab.tsx +0 -170
  105. package/src/web/components/connections/OverviewTab.tsx +0 -137
  106. package/src/web/components/connections/TriggersTab.tsx +0 -1346
  107. package/src/web/components/dashboard/Dashboard.tsx +0 -572
  108. package/src/web/components/dashboard/index.ts +0 -1
  109. package/src/web/components/index.ts +0 -21
  110. package/src/web/components/layout/ErrorBanner.tsx +0 -18
  111. package/src/web/components/layout/Header.tsx +0 -332
  112. package/src/web/components/layout/Sidebar.tsx +0 -231
  113. package/src/web/components/layout/index.ts +0 -3
  114. package/src/web/components/mcp/IntegrationsPanel.tsx +0 -857
  115. package/src/web/components/mcp/McpPage.tsx +0 -2515
  116. package/src/web/components/mcp/index.ts +0 -1
  117. package/src/web/components/meta-agent/MetaAgent.tsx +0 -245
  118. package/src/web/components/onboarding/OnboardingWizard.tsx +0 -404
  119. package/src/web/components/onboarding/index.ts +0 -1
  120. package/src/web/components/settings/SettingsPage.tsx +0 -2776
  121. package/src/web/components/settings/index.ts +0 -1
  122. package/src/web/components/skills/SkillsPage.tsx +0 -1200
  123. package/src/web/components/tasks/TasksPage.tsx +0 -1116
  124. package/src/web/components/tasks/index.ts +0 -1
  125. package/src/web/components/telemetry/TelemetryPage.tsx +0 -1129
  126. package/src/web/components/tests/TestsPage.tsx +0 -594
  127. package/src/web/components/threads/ThreadsPage.tsx +0 -315
  128. package/src/web/context/AuthContext.tsx +0 -242
  129. package/src/web/context/ProjectContext.tsx +0 -214
  130. package/src/web/context/TelemetryContext.tsx +0 -299
  131. package/src/web/context/ThemeContext.tsx +0 -90
  132. package/src/web/context/UIModeContext.tsx +0 -49
  133. package/src/web/context/index.ts +0 -12
  134. package/src/web/hooks/index.ts +0 -3
  135. package/src/web/hooks/useAgents.ts +0 -115
  136. package/src/web/hooks/useOnboarding.ts +0 -20
  137. package/src/web/hooks/useProviders.ts +0 -75
  138. package/src/web/icon.png +0 -0
  139. package/src/web/index.html +0 -16
  140. package/src/web/styles.css +0 -118
  141. package/src/web/themes.ts +0 -162
  142. 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
- }