apteva 0.4.32 → 0.4.44

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 (113) hide show
  1. package/dist/ActivityPage.c48n83h2.js +3 -0
  2. package/dist/ApiDocsPage.yzcxx5ax.js +4 -0
  3. package/dist/App.09yb8t0b.js +1 -0
  4. package/dist/App.152mbs1r.js +4 -0
  5. package/dist/App.3a67nx9w.js +4 -0
  6. package/dist/App.9epx6785.js +4 -0
  7. package/dist/App.d8955awp.js +4 -0
  8. package/dist/App.drwb57jq.js +4 -0
  9. package/dist/App.gssbmajb.js +4 -0
  10. package/dist/App.qw70pc29.js +53 -0
  11. package/dist/App.qzbx5wtj.js +4 -0
  12. package/dist/App.r5serxkt.js +8 -0
  13. package/dist/App.tpmp9020.js +20 -0
  14. package/dist/App.v2wb4d7d.js +61 -0
  15. package/dist/App.vxmaaj0m.js +13 -0
  16. package/dist/App.w4p2tda9.js +4 -0
  17. package/dist/App.wv2ng55q.js +221 -0
  18. package/dist/App.yncnrn0f.js +4 -0
  19. package/dist/ConnectionsPage.k6cspyqq.js +3 -0
  20. package/dist/McpPage.cdxm48xj.js +3 -0
  21. package/dist/SettingsPage.evpv7c2y.js +3 -0
  22. package/dist/SkillsPage.pvzp6c1a.js +3 -0
  23. package/dist/TasksPage.6jnvbpsy.js +3 -0
  24. package/dist/TelemetryPage.t7vk24zc.js +3 -0
  25. package/dist/TestsPage.5x6658aa.js +3 -0
  26. package/dist/ThreadsPage.3fvhtevh.js +3 -0
  27. package/dist/apteva-kit.css +1 -1
  28. package/dist/index.html +1 -1
  29. package/dist/styles.css +1 -1
  30. package/package.json +10 -9
  31. package/src/crypto.ts +4 -3
  32. package/src/db.ts +171 -36
  33. package/src/integrations/agentdojo.ts +95 -12
  34. package/src/integrations/index.ts +7 -0
  35. package/src/mcp-platform.ts +870 -142
  36. package/src/openapi.ts +96 -0
  37. package/src/providers.ts +60 -34
  38. package/src/routes/api/agent-utils.ts +59 -47
  39. package/src/routes/api/agents.ts +71 -2
  40. package/src/routes/api/integrations.ts +11 -5
  41. package/src/routes/api/mcp.ts +5 -4
  42. package/src/routes/api/meta-agent.ts +37 -1
  43. package/src/routes/api/projects.ts +3 -3
  44. package/src/routes/api/providers.ts +121 -30
  45. package/src/routes/api/skills.ts +2 -3
  46. package/src/routes/api/system.ts +98 -14
  47. package/src/routes/api/telemetry.ts +19 -1
  48. package/src/routes/share.ts +85 -0
  49. package/src/server.ts +43 -32
  50. package/src/triggers/agentdojo.ts +2 -2
  51. package/src/web/App.tsx +107 -21
  52. package/src/web/components/activity/ActivityPage.tsx +242 -389
  53. package/src/web/components/agents/AgentCard.tsx +19 -27
  54. package/src/web/components/agents/AgentPanel.tsx +358 -198
  55. package/src/web/components/agents/AgentsView.tsx +4 -4
  56. package/src/web/components/agents/CreateAgentModal.tsx +21 -79
  57. package/src/web/components/api/ApiDocsPage.tsx +66 -66
  58. package/src/web/components/auth/CreateAccountStep.tsx +16 -16
  59. package/src/web/components/auth/LoginPage.tsx +10 -10
  60. package/src/web/components/common/Icons.tsx +8 -0
  61. package/src/web/components/common/LoadingSpinner.tsx +2 -2
  62. package/src/web/components/common/Modal.tsx +8 -8
  63. package/src/web/components/common/Select.tsx +11 -10
  64. package/src/web/components/connections/ConnectionsPage.tsx +4 -4
  65. package/src/web/components/connections/IntegrationsTab.tsx +18 -18
  66. package/src/web/components/connections/OverviewTab.tsx +13 -13
  67. package/src/web/components/connections/TriggersTab.tsx +99 -99
  68. package/src/web/components/dashboard/Dashboard.tsx +177 -52
  69. package/src/web/components/index.ts +1 -1
  70. package/src/web/components/layout/Header.tsx +50 -34
  71. package/src/web/components/layout/Sidebar.tsx +41 -16
  72. package/src/web/components/mcp/IntegrationsPanel.tsx +160 -69
  73. package/src/web/components/mcp/McpPage.tsx +218 -209
  74. package/src/web/components/meta-agent/MetaAgent.tsx +15 -11
  75. package/src/web/components/onboarding/OnboardingWizard.tsx +25 -25
  76. package/src/web/components/settings/SettingsPage.tsx +389 -221
  77. package/src/web/components/skills/SkillsPage.tsx +88 -88
  78. package/src/web/components/tasks/TasksPage.tsx +385 -68
  79. package/src/web/components/telemetry/TelemetryPage.tsx +294 -39
  80. package/src/web/components/tests/TestsPage.tsx +50 -50
  81. package/src/web/components/threads/ThreadsPage.tsx +315 -0
  82. package/src/web/context/AuthContext.tsx +3 -3
  83. package/src/web/context/ProjectContext.tsx +8 -3
  84. package/src/web/context/TelemetryContext.tsx +24 -6
  85. package/src/web/context/ThemeContext.tsx +69 -0
  86. package/src/web/context/index.ts +3 -1
  87. package/src/web/styles.css +25 -7
  88. package/src/web/themes.ts +99 -0
  89. package/src/web/types.ts +4 -7
  90. package/dist/ActivityPage.41nbye4r.js +0 -3
  91. package/dist/ApiDocsPage.4smnt8m3.js +0 -4
  92. package/dist/App.0sbax9et.js +0 -4
  93. package/dist/App.0ws427h8.js +0 -4
  94. package/dist/App.6q6bar8b.js +0 -4
  95. package/dist/App.80301vdb.js +0 -4
  96. package/dist/App.af2wg84v.js +0 -267
  97. package/dist/App.ca1rz1ph.js +0 -4
  98. package/dist/App.ensa6z0r.js +0 -4
  99. package/dist/App.f8g7tych.js +0 -13
  100. package/dist/App.mvtqv6qc.js +0 -20
  101. package/dist/App.ncgc9cxy.js +0 -4
  102. package/dist/App.p02f4ret.js +0 -1
  103. package/dist/App.p0fb1pds.js +0 -4
  104. package/dist/App.pmaq48sj.js +0 -4
  105. package/dist/App.yv87t9m5.js +0 -4
  106. package/dist/App.zjmfm8p6.js +0 -4
  107. package/dist/ConnectionsPage.anb3rv9a.js +0 -3
  108. package/dist/McpPage.y396h6fy.js +0 -3
  109. package/dist/SettingsPage.p1hc60gk.js +0 -3
  110. package/dist/SkillsPage.yj3xdsay.js +0 -3
  111. package/dist/TasksPage.sjv0khtv.js +0 -3
  112. package/dist/TelemetryPage.2qm4w16r.js +0 -3
  113. package/dist/TestsPage.zzs4qfj8.js +0 -3
@@ -111,8 +111,11 @@ export async function handleIntegrationRoutes(
111
111
 
112
112
  try {
113
113
  const body = await req.json();
114
- const { appSlug, redirectUrl, credentials, project_id } = body;
115
- const apiKey = ProviderKeys.getDecryptedForProject(providerId, project_id || null);
114
+ const { appSlug, redirectUrl, credentials } = body;
115
+ // Read project_id from query param (frontend sends it there), fall back to body
116
+ const url = new URL(req.url);
117
+ const projectId = url.searchParams.get("project_id") || body.project_id || null;
118
+ const apiKey = ProviderKeys.getDecryptedForProject(providerId, projectId);
116
119
  if (!apiKey) {
117
120
  return json({ error: `${provider.name} API key not configured` }, 401);
118
121
  }
@@ -127,7 +130,8 @@ export async function handleIntegrationRoutes(
127
130
  }
128
131
 
129
132
  // Default redirect URL back to our integrations page
130
- const callbackUrl = redirectUrl || `http://localhost:${process.env.PORT || 4280}/mcp?tab=hosted&connected=${appSlug}`;
133
+ const origin = req.headers.get("origin") || process.env.INSTANCE_URL || `http://localhost:${process.env.PORT || 4280}`;
134
+ const callbackUrl = redirectUrl || `${origin}/mcp?tab=hosted&connected=${appSlug}`;
131
135
 
132
136
  const result = await provider.initiateConnection(apiKey, user.id, appSlug, callbackUrl, credentials);
133
137
  return json(result);
@@ -329,7 +333,8 @@ export async function handleIntegrationRoutes(
329
333
  : mcpUrl;
330
334
 
331
335
  // Check if already exists (match by config ID in URL)
332
- const existing = McpServerDB.findAll().find(
336
+ // Use Light variant to skip expensive decryption of env/headers
337
+ const existing = McpServerDB.findAllLight().find(
333
338
  s => s.source === "composio" && s.url?.includes(configId)
334
339
  );
335
340
  if (existing) {
@@ -527,8 +532,9 @@ export async function handleIntegrationRoutes(
527
532
  console.log(`[agentdojo:add] fetched server: slug=${server.slug} name=${server.name} url=${server.url?.substring(0, 80)}`);
528
533
 
529
534
  // Check if already exists — match by slug in URL, scoped to same project
535
+ // Use Light variant to skip expensive decryption of env/headers
530
536
  const effectiveProjectId = projectId && projectId !== "unassigned" ? projectId : null;
531
- const allServers = McpServerDB.findAll();
537
+ const allServers = McpServerDB.findAllLight();
532
538
  const agentdojoServers = allServers.filter(s => s.source === "agentdojo");
533
539
  console.log(`[agentdojo:add] total servers=${allServers.length} agentdojo servers=${agentdojoServers.length}`);
534
540
  const existing = agentdojoServers.find(
@@ -28,16 +28,17 @@ export async function handleMcpRoutes(
28
28
  let queryMode: string;
29
29
  if (forAgent !== null) {
30
30
  // Get servers available for an agent (global + agent's project)
31
- servers = McpServerDB.findForAgent(forAgent || null);
31
+ // Use Light variant: skips expensive decryption of env/headers
32
+ servers = McpServerDB.findForAgentLight(forAgent || null);
32
33
  queryMode = `forAgent=${forAgent}`;
33
34
  } else if (projectFilter === "global") {
34
- servers = McpServerDB.findGlobal();
35
+ servers = McpServerDB.findGlobalLight();
35
36
  queryMode = "global";
36
37
  } else if (projectFilter && projectFilter !== "all") {
37
- servers = McpServerDB.findByProject(projectFilter);
38
+ servers = McpServerDB.findByProjectLight(projectFilter);
38
39
  queryMode = `project=${projectFilter}`;
39
40
  } else {
40
- servers = McpServerDB.findAll();
41
+ servers = McpServerDB.findAllLight();
41
42
  queryMode = "all";
42
43
  }
43
44
  const agentdojoCount = servers.filter(s => s.source === "agentdojo").length;
@@ -56,6 +56,9 @@ WHAT YOU CAN DO:
56
56
  - **Skills**: List, enable/disable, and assign skills to agents
57
57
  - **Providers**: Check which LLM providers have API keys configured
58
58
  - **Communication**: Send messages to running agents
59
+ - **Integrations**: Connect third-party apps (GitHub, Slack, etc.) via API key, create MCP servers from them, and add them locally
60
+ - **Tests**: Create and run automated tests for agent workflows
61
+ - **Triggers**: Subscribe agents to external events (webhooks)
59
62
 
60
63
  WORKFLOW FOR CREATING AGENTS:
61
64
  1. Use list_providers to check which providers have API keys
@@ -63,6 +66,14 @@ WORKFLOW FOR CREATING AGENTS:
63
66
  3. Optionally assign MCP servers (for tools) and skills (for behavior)
64
67
  4. Use start_agent to run it
65
68
 
69
+ WORKFLOW FOR ADDING INTEGRATIONS:
70
+ 1. Use list_integration_providers to see available providers (agentdojo, composio)
71
+ 2. Use list_integration_apps to browse available apps/toolkits
72
+ 3. Use connect_integration_app to authenticate with an API key (for OAuth, direct user to Browse Toolkits UI)
73
+ 4. Use create_integration_config to create an MCP server from the connected app
74
+ 5. Use add_integration_config_locally to add it as a local MCP server
75
+ 6. Use assign_mcp_server_to_agent to give an agent access
76
+
66
77
  AGENT FEATURES (enable when creating/updating):
67
78
  - **memory**: Persistent memory across conversations (needs OpenAI key for embeddings)
68
79
  - **tasks**: Scheduling and task tracking
@@ -70,6 +81,9 @@ AGENT FEATURES (enable when creating/updating):
70
81
  - **mcp**: Required if assigning MCP servers — gives the agent tool-use capability
71
82
  - **files**: File read/write in agent workspace
72
83
 
84
+ CRITICAL — PROJECT CONTEXT:
85
+ Your chat context tells you the current project (name and id). **You MUST pass this project_id to EVERY tool call that accepts a project_id parameter.** API keys, integrations, MCP servers, and agents are scoped per project — calls without project_id will fail to find them. Extract the id from your context and always include it.
86
+
73
87
  ALWAYS use your tools proactively. When a user says "create an agent", don't explain how — just do it. Confirm what you did after.
74
88
  Be concise. Use markdown formatting.`,
75
89
  features: {
@@ -81,6 +95,10 @@ Be concise. Use markdown formatting.`,
81
95
  realtime: false,
82
96
  files: false,
83
97
  agents: false,
98
+ builtinTools: {
99
+ webSearch: providerId === "anthropic",
100
+ webFetch: providerId === "anthropic",
101
+ },
84
102
  },
85
103
  mcp_servers: [],
86
104
  skills: [],
@@ -99,6 +117,8 @@ Be concise. Use markdown formatting.`,
99
117
  port: metaAgent.port,
100
118
  provider: metaAgent.provider,
101
119
  model: metaAgent.model,
120
+ features: metaAgent.features,
121
+ systemPrompt: metaAgent.system_prompt,
102
122
  },
103
123
  });
104
124
  }
@@ -118,8 +138,24 @@ Be concise. Use markdown formatting.`,
118
138
  return json({ agent: toApiAgent(metaAgent), message: "Already running" });
119
139
  }
120
140
 
141
+ // Ensure builtinTools are enabled for anthropic provider
142
+ if (metaAgent.provider === "anthropic" && !metaAgent.features?.builtinTools?.webSearch) {
143
+ const updatedFeatures = {
144
+ ...metaAgent.features,
145
+ builtinTools: { webSearch: true, webFetch: true },
146
+ };
147
+ AgentDB.update(META_AGENT_ID, { features: updatedFeatures });
148
+ console.log(`[meta-agent] Enabled builtinTools for anthropic provider`);
149
+ }
150
+ // Always re-read to get latest features
151
+ const freshAgent = AgentDB.findById(META_AGENT_ID);
152
+ if (!freshAgent) {
153
+ return json({ error: "Meta agent not found after update" }, 500);
154
+ }
155
+ console.log(`[meta-agent] Starting with builtinTools:`, JSON.stringify(freshAgent.features?.builtinTools));
156
+
121
157
  // Start the agent using existing startAgentProcess function
122
- const result = await startAgentProcess(metaAgent, { silent: true });
158
+ const result = await startAgentProcess(freshAgent, { silent: true });
123
159
  if (!result.success) {
124
160
  return json({ error: result.error || "Failed to start meta agent" }, 500);
125
161
  }
@@ -88,9 +88,9 @@ export async function handleProjectRoutes(
88
88
  return json({ error: "Project not found" }, 404);
89
89
  }
90
90
 
91
- // Stop any running agents in this project first
91
+ // Stop any running agents in this project first - in parallel
92
92
  const projectAgents = AgentDB.findByProject(projectMatch[1]);
93
- for (const agent of projectAgents) {
93
+ await Promise.allSettled(projectAgents.map(async (agent) => {
94
94
  if (agent.status === "running") {
95
95
  const entry = agentProcesses.get(agent.id);
96
96
  if (entry) {
@@ -102,7 +102,7 @@ export async function handleProjectRoutes(
102
102
  }
103
103
  setAgentStatus(agent.id, "stopped", "project_deleted");
104
104
  }
105
- }
105
+ }));
106
106
 
107
107
  ProjectDB.delete(projectMatch[1]);
108
108
  return json({ success: true });
@@ -1,4 +1,14 @@
1
+ import { existsSync } from "fs";
1
2
  import { json, debug } from "./helpers";
3
+
4
+ // Detect if running inside a Docker container
5
+ async function isRunningInDocker(): Promise<boolean> {
6
+ try {
7
+ return existsSync("/.dockerenv") || existsSync("/run/.containerenv");
8
+ } catch {
9
+ return false;
10
+ }
11
+ }
2
12
  import { startAgentProcess, setAgentStatus } from "./agent-utils";
3
13
  import { AgentDB, UserDB } from "../../db";
4
14
  import { ProviderKeys, Onboarding, getProvidersWithStatus, PROVIDERS, type ProviderId } from "../../providers";
@@ -55,6 +65,7 @@ export async function handleProviderRoutes(
55
65
  // GET /api/providers/ollama/status - Check if Ollama is running
56
66
  if (path === "/api/providers/ollama/status" && method === "GET") {
57
67
  const ollamaUrl = ProviderKeys.getDecrypted("ollama") || "http://localhost:11434";
68
+ const isDocker = await isRunningInDocker();
58
69
 
59
70
  try {
60
71
  const response = await fetch(`${ollamaUrl}/api/tags`, {
@@ -68,11 +79,96 @@ export async function handleProviderRoutes(
68
79
  connected: true,
69
80
  url: ollamaUrl,
70
81
  modelCount: data.models?.length || 0,
82
+ isDocker,
71
83
  });
72
84
  }
73
- return json({ connected: false, url: ollamaUrl, error: "Ollama not responding" });
85
+ return json({ connected: false, url: ollamaUrl, error: "Ollama not responding", isDocker });
74
86
  } catch {
75
- return json({ connected: false, url: ollamaUrl, error: "Ollama not reachable" });
87
+ return json({ connected: false, url: ollamaUrl, error: "Ollama not reachable", isDocker });
88
+ }
89
+ }
90
+
91
+ // POST /api/providers/ollama/install - Install Ollama automatically
92
+ if (path === "/api/providers/ollama/install" && method === "POST") {
93
+ // Don't allow install inside Docker containers
94
+ if (await isRunningInDocker()) {
95
+ return json({ success: false, error: "Cannot install Ollama inside a Docker container. Configure an external Ollama URL instead." }, 400);
96
+ }
97
+
98
+ // Only supported on Linux/macOS
99
+ const platform = process.platform;
100
+ if (platform !== "linux" && platform !== "darwin") {
101
+ return json({ success: false, error: "Auto-install is only supported on Linux and macOS. Please download from https://ollama.com/download" }, 400);
102
+ }
103
+
104
+ // Check if already installed
105
+ try {
106
+ const which = Bun.spawnSync(["which", "ollama"]);
107
+ if (which.exitCode === 0) {
108
+ // Already installed, just make sure it's running
109
+ Bun.spawn(["ollama", "serve"], { stdout: "ignore", stderr: "ignore" });
110
+ // Wait a moment for it to start
111
+ await new Promise(r => setTimeout(r, 2000));
112
+ return json({ success: true, message: "Ollama is already installed", alreadyInstalled: true });
113
+ }
114
+ } catch { /* not installed */ }
115
+
116
+ // Install Ollama using official install script
117
+ try {
118
+ const proc = Bun.spawnSync(["bash", "-c", "curl -fsSL https://ollama.com/install.sh | sh"], {
119
+ timeout: 120_000, // 2 minute timeout
120
+ stderr: "pipe",
121
+ stdout: "pipe",
122
+ });
123
+
124
+ if (proc.exitCode !== 0) {
125
+ const stderr = proc.stderr.toString().trim();
126
+ return json({ success: false, error: `Installation failed: ${stderr || "Unknown error"}` }, 500);
127
+ }
128
+
129
+ // Start Ollama serve in background
130
+ Bun.spawn(["ollama", "serve"], { stdout: "ignore", stderr: "ignore" });
131
+
132
+ // Wait for it to come up
133
+ await new Promise(r => setTimeout(r, 3000));
134
+
135
+ // Verify it's running
136
+ try {
137
+ const check = await fetch("http://localhost:11434/api/tags", { signal: AbortSignal.timeout(3000) });
138
+ if (check.ok) {
139
+ return json({ success: true, message: "Ollama installed and running" });
140
+ }
141
+ } catch { /* may still be starting */ }
142
+
143
+ return json({ success: true, message: "Ollama installed. It may take a moment to start." });
144
+ } catch (err: any) {
145
+ return json({ success: false, error: `Installation failed: ${err.message}` }, 500);
146
+ }
147
+ }
148
+
149
+ // POST /api/providers/ollama/pull - Pull a model
150
+ if (path === "/api/providers/ollama/pull" && method === "POST") {
151
+ const body = await req.json() as { model?: string };
152
+ const model = body.model;
153
+ if (!model) {
154
+ return json({ error: "Model name required" }, 400);
155
+ }
156
+
157
+ try {
158
+ const ollamaUrl = ProviderKeys.getDecrypted("ollama") || "http://localhost:11434";
159
+ const response = await fetch(`${ollamaUrl}/api/pull`, {
160
+ method: "POST",
161
+ headers: { "Content-Type": "application/json" },
162
+ body: JSON.stringify({ name: model }),
163
+ });
164
+
165
+ if (!response.ok) {
166
+ return json({ success: false, error: "Failed to start model pull" }, 500);
167
+ }
168
+
169
+ return json({ success: true, message: `Pulling ${model}...` });
170
+ } catch (err: any) {
171
+ return json({ success: false, error: `Failed to pull model: ${err.message}` }, 500);
76
172
  }
77
173
  }
78
174
 
@@ -216,38 +312,33 @@ export async function handleProviderRoutes(
216
312
  a => a.status === "running" && a.provider === providerId
217
313
  );
218
314
 
219
- const restartResults: Array<{ id: string; name: string; success: boolean; error?: string }> = [];
315
+ // Stop all agents first
220
316
  for (const agent of runningAgents) {
221
- try {
222
- // Stop the agent
223
- const agentProc = agentProcesses.get(agent.id);
224
- if (agentProc) {
225
- agentProc.proc.kill();
226
- agentProcesses.delete(agent.id);
227
- }
228
- setAgentStatus(agent.id, "stopped", "provider_restart");
229
-
230
- // Wait a moment for port to be released
231
- await new Promise(r => setTimeout(r, 500));
232
-
233
- // Restart the agent with new key
234
- const startResult = await startAgentProcess(agent, { silent: true });
235
- restartResults.push({
236
- id: agent.id,
237
- name: agent.name,
238
- success: startResult.success,
239
- error: startResult.error,
240
- });
241
- } catch (e) {
242
- restartResults.push({
243
- id: agent.id,
244
- name: agent.name,
245
- success: false,
246
- error: String(e),
247
- });
317
+ const agentProc = agentProcesses.get(agent.id);
318
+ if (agentProc) {
319
+ agentProc.proc.kill();
320
+ agentProcesses.delete(agent.id);
248
321
  }
322
+ setAgentStatus(agent.id, "stopped", "provider_restart");
249
323
  }
250
324
 
325
+ // Wait once for ports to be released
326
+ if (runningAgents.length > 0) {
327
+ await new Promise(r => setTimeout(r, 500));
328
+ }
329
+
330
+ // Restart all agents in parallel
331
+ const restartResults = await Promise.all(
332
+ runningAgents.map(async (agent) => {
333
+ try {
334
+ const startResult = await startAgentProcess(agent, { silent: true });
335
+ return { id: agent.id, name: agent.name, success: startResult.success, error: startResult.error };
336
+ } catch (e) {
337
+ return { id: agent.id, name: agent.name, success: false, error: String(e) };
338
+ }
339
+ })
340
+ );
341
+
251
342
  return json({
252
343
  success: true,
253
344
  message: "API key saved successfully",
@@ -499,13 +499,12 @@ export async function handleSkillRoutes(
499
499
  const agentsWithSkill = AgentDB.findBySkill(skillMatch[1]);
500
500
  const runningAgents = agentsWithSkill.filter(a => a.status === "running" && a.port);
501
501
 
502
- for (const agent of runningAgents) {
502
+ await Promise.allSettled(runningAgents.map(async (agent) => {
503
503
  try {
504
504
  const providerKey = ProviderKeys.getDecrypted(agent.provider);
505
505
  if (providerKey) {
506
506
  const config = buildAgentConfig(agent, providerKey);
507
507
  await pushConfigToAgent(agent.id, agent.port!, config);
508
- // Push skills via /skills endpoint
509
508
  if (config.skills?.definitions?.length > 0) {
510
509
  await pushSkillsToAgent(agent.id, agent.port!, config.skills.definitions);
511
510
  }
@@ -514,7 +513,7 @@ export async function handleSkillRoutes(
514
513
  } catch (err) {
515
514
  console.error(`Failed to push skill update to agent ${agent.name}:`, err);
516
515
  }
517
- }
516
+ }));
518
517
 
519
518
  return json({ skill: updated, agents_updated: runningAgents.length });
520
519
  } catch (err) {
@@ -1,5 +1,5 @@
1
1
  import { json } from "./helpers";
2
- import { META_AGENT_ENABLED, fetchFromAgent, startAgentProcess, setAgentStatus } from "./agent-utils";
2
+ import { META_AGENT_ENABLED, fetchFromAgent, agentFetch, startAgentProcess, setAgentStatus } from "./agent-utils";
3
3
  import { AgentDB } from "../../db";
4
4
  import { ProviderKeys } from "../../providers";
5
5
  import { agentProcesses, getBinaryStatus, BIN_DIR } from "../../server";
@@ -44,6 +44,7 @@ export async function handleSystemRoutes(
44
44
  return json({
45
45
  projects: process.env.PROJECTS_ENABLED === "true",
46
46
  metaAgent: process.env.META_AGENT_ENABLED === "true",
47
+ costTracking: process.env.COST_TRACKING_ENABLED !== "false",
47
48
  });
48
49
  }
49
50
 
@@ -99,21 +100,16 @@ export async function handleSystemRoutes(
99
100
  return json({ success: false, error: result.error }, 500);
100
101
  }
101
102
 
102
- // Restart agents that were running
103
- const restartResults: { id: string; name: string; success: boolean; error?: string }[] = [];
104
- for (const agentId of agentsToRestart) {
105
- const agent = AgentDB.findById(agentId);
106
- if (agent) {
103
+ // Restart agents that were running - in parallel
104
+ const restartResults = await Promise.all(
105
+ agentsToRestart.map(async (agentId) => {
106
+ const agent = AgentDB.findById(agentId);
107
+ if (!agent) return null;
107
108
  console.log(`Restarting agent ${agent.name} after update...`);
108
109
  const startResult = await startAgentProcess(agent);
109
- restartResults.push({
110
- id: agent.id,
111
- name: agent.name,
112
- success: startResult.success,
113
- error: startResult.error,
114
- });
115
- }
116
- }
110
+ return { id: agent.id, name: agent.name, success: startResult.success, error: startResult.error };
111
+ })
112
+ ).then(r => r.filter(Boolean));
117
113
 
118
114
  return json({
119
115
  success: true,
@@ -185,6 +181,94 @@ export async function handleSystemRoutes(
185
181
  return json({ task: { ...data, agentId: agent.id, agentName: agent.name } });
186
182
  }
187
183
 
184
+ // POST /api/tasks/:agentId/:taskId/execute - Execute a task immediately
185
+ const executeTaskMatch = path.match(/^\/api\/tasks\/([^/]+)\/([^/]+)\/execute$/);
186
+ if (executeTaskMatch && method === "POST") {
187
+ const [, agentId, taskId] = executeTaskMatch;
188
+ const agent = AgentDB.findById(agentId);
189
+ if (!agent) return json({ error: "Agent not found" }, 404);
190
+ if (agent.status !== "running" || !agent.port) return json({ error: "Agent is not running" }, 400);
191
+
192
+ try {
193
+ const res = await agentFetch(agentId, agent.port, `/tasks/${taskId}/execute`, {
194
+ method: "POST",
195
+ signal: AbortSignal.timeout(5000),
196
+ });
197
+ const data = await res.json();
198
+ if (!res.ok) return json({ error: data.error || `HTTP ${res.status}` }, res.status);
199
+ return json(data);
200
+ } catch (err) {
201
+ return json({ error: `Failed to execute task: ${err}` }, 500);
202
+ }
203
+ }
204
+
205
+ // POST /api/tasks/:agentId - Create a task on an agent
206
+ const createTaskMatch = path.match(/^\/api\/tasks\/([^/]+)$/);
207
+ if (createTaskMatch && method === "POST") {
208
+ const agentId = createTaskMatch[1];
209
+ const agent = AgentDB.findById(agentId);
210
+ if (!agent) return json({ error: "Agent not found" }, 404);
211
+ if (agent.status !== "running" || !agent.port) return json({ error: "Agent is not running" }, 400);
212
+
213
+ try {
214
+ const body = await req.json();
215
+ const res = await agentFetch(agentId, agent.port, "/tasks", {
216
+ method: "POST",
217
+ headers: { "Content-Type": "application/json" },
218
+ body: JSON.stringify(body),
219
+ signal: AbortSignal.timeout(5000),
220
+ });
221
+ const data = await res.json();
222
+ if (!res.ok) return json({ error: data.error || `HTTP ${res.status}` }, res.status);
223
+ return json(data, 201);
224
+ } catch (err) {
225
+ return json({ error: `Failed to create task: ${err}` }, 500);
226
+ }
227
+ }
228
+
229
+ // PUT /api/tasks/:agentId/:taskId - Update a task on an agent
230
+ if (singleTaskMatch && method === "PUT") {
231
+ const [, agentId, taskId] = singleTaskMatch;
232
+ const agent = AgentDB.findById(agentId);
233
+ if (!agent) return json({ error: "Agent not found" }, 404);
234
+ if (agent.status !== "running" || !agent.port) return json({ error: "Agent is not running" }, 400);
235
+
236
+ try {
237
+ const body = await req.json();
238
+ const res = await agentFetch(agentId, agent.port, `/tasks/${taskId}`, {
239
+ method: "PUT",
240
+ headers: { "Content-Type": "application/json" },
241
+ body: JSON.stringify(body),
242
+ signal: AbortSignal.timeout(5000),
243
+ });
244
+ const data = await res.json();
245
+ if (!res.ok) return json({ error: data.error || `HTTP ${res.status}` }, res.status);
246
+ return json(data);
247
+ } catch (err) {
248
+ return json({ error: `Failed to update task: ${err}` }, 500);
249
+ }
250
+ }
251
+
252
+ // DELETE /api/tasks/:agentId/:taskId - Delete a task on an agent
253
+ if (singleTaskMatch && method === "DELETE") {
254
+ const [, agentId, taskId] = singleTaskMatch;
255
+ const agent = AgentDB.findById(agentId);
256
+ if (!agent) return json({ error: "Agent not found" }, 404);
257
+ if (agent.status !== "running" || !agent.port) return json({ error: "Agent is not running" }, 400);
258
+
259
+ try {
260
+ const res = await agentFetch(agentId, agent.port, `/tasks/${taskId}`, {
261
+ method: "DELETE",
262
+ signal: AbortSignal.timeout(5000),
263
+ });
264
+ const data = await res.json();
265
+ if (!res.ok) return json({ error: data.error || `HTTP ${res.status}` }, res.status);
266
+ return json(data);
267
+ } catch (err) {
268
+ return json({ error: `Failed to delete task: ${err}` }, 500);
269
+ }
270
+ }
271
+
188
272
  // GET /api/dashboard - Get dashboard statistics
189
273
  if (path === "/api/dashboard" && method === "GET") {
190
274
  const url = new URL(req.url);
@@ -1,5 +1,6 @@
1
1
  import { json } from "./helpers";
2
- import { TelemetryDB } from "../../db";
2
+ import { TelemetryDB, AgentDB } from "../../db";
3
+ import { getModelCost } from "../../providers";
3
4
  import { telemetryBroadcaster, type TelemetryEvent } from "../../server";
4
5
 
5
6
  export async function handleTelemetryRoutes(
@@ -35,6 +36,23 @@ export async function handleTelemetryRoutes(
35
36
 
36
37
  // Filter out debug events - too noisy
37
38
  const filteredEvents = body.events.filter(e => e.level !== "debug");
39
+
40
+ // Compute cost per LLM event if cost tracking is enabled
41
+ const costTrackingEnabled = process.env.COST_TRACKING_ENABLED !== "false";
42
+ if (costTrackingEnabled) {
43
+ const agent = AgentDB.findById(body.agent_id);
44
+ if (agent) {
45
+ const pricing = getModelCost(agent.provider, agent.model);
46
+ for (const event of filteredEvents) {
47
+ if (event.category === "LLM" && event.data) {
48
+ const inputTokens = (event.data.input_tokens as number) || 0;
49
+ const outputTokens = (event.data.output_tokens as number) || 0;
50
+ (event as any).cost = (inputTokens * pricing.input_cost + outputTokens * pricing.output_cost) / 1_000_000;
51
+ }
52
+ }
53
+ }
54
+ }
55
+
38
56
  const inserted = TelemetryDB.insertBatch(body.agent_id, filteredEvents);
39
57
 
40
58
  // Broadcast to SSE clients
@@ -0,0 +1,85 @@
1
+ import { createHash } from "crypto";
2
+ import { AgentDB, type Agent } from "../db";
3
+ import { agentFetch } from "./api/agent-utils";
4
+
5
+ function deriveShareToken(apiKey: string, agentId: string): string {
6
+ return createHash("sha256")
7
+ .update(apiKey + ":" + agentId + ":share")
8
+ .digest("hex")
9
+ .substring(0, 32);
10
+ }
11
+
12
+ function findAgentByShareToken(token: string): Agent | null {
13
+ const agents = AgentDB.findAll();
14
+ for (const agent of agents) {
15
+ const apiKey = AgentDB.getApiKey(agent.id);
16
+ if (!apiKey) continue;
17
+ if (deriveShareToken(apiKey, agent.id) === token) return agent;
18
+ }
19
+ return null;
20
+ }
21
+
22
+ /** Get the share token for an agent (used by API route for the UI) */
23
+ export function getShareToken(agentId: string): string | null {
24
+ const apiKey = AgentDB.getApiKey(agentId);
25
+ if (!apiKey) return null;
26
+ return deriveShareToken(apiKey, agentId);
27
+ }
28
+
29
+ export async function handleShareRequest(req: Request, path: string): Promise<Response | null> {
30
+ // Match /share/<32 hex chars> sub-paths for API calls
31
+ const infoMatch = path.match(/^\/share\/([a-f0-9]{32})\/info$/);
32
+ const chatMatch = path.match(/^\/share\/([a-f0-9]{32})\/chat$/);
33
+
34
+ if (!infoMatch && !chatMatch) return null;
35
+
36
+ const token = (infoMatch || chatMatch)![1];
37
+ const agent = findAgentByShareToken(token);
38
+
39
+ // Intentionally vague 404 — don't reveal whether token exists
40
+ if (!agent) {
41
+ return new Response("Not found", { status: 404 });
42
+ }
43
+
44
+ // GET /share/:token/info — agent info (no secrets)
45
+ if (infoMatch && req.method === "GET") {
46
+ return Response.json({
47
+ name: agent.name,
48
+ status: agent.status,
49
+ });
50
+ }
51
+
52
+ // POST /share/:token/chat — proxy to agent
53
+ if (chatMatch && req.method === "POST") {
54
+ if (agent.status !== "running" || !agent.port) {
55
+ return Response.json({ error: "Agent is currently offline" }, { status: 503 });
56
+ }
57
+
58
+ try {
59
+ const body = await req.json();
60
+ const response = await agentFetch(agent.id, agent.port, "/chat", {
61
+ method: "POST",
62
+ headers: { "Content-Type": "application/json" },
63
+ body: JSON.stringify(body),
64
+ });
65
+
66
+ if (!response.ok) {
67
+ const errorText = await response.text();
68
+ return Response.json({ error: `Agent error: ${errorText}` }, { status: response.status });
69
+ }
70
+
71
+ return new Response(response.body, {
72
+ status: 200,
73
+ headers: {
74
+ "Content-Type": response.headers.get("Content-Type") || "text/event-stream",
75
+ "Cache-Control": "no-cache",
76
+ "Connection": "keep-alive",
77
+ },
78
+ });
79
+ } catch (err) {
80
+ return Response.json({ error: "Failed to connect to agent" }, { status: 500 });
81
+ }
82
+ }
83
+
84
+ return null;
85
+ }