apteva 0.4.17 → 0.4.19

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 (78) hide show
  1. package/dist/ActivityPage.9a1qg4bp.js +3 -0
  2. package/dist/ApiDocsPage.rfpf7ws1.js +4 -0
  3. package/dist/App.1nmg2h01.js +4 -0
  4. package/dist/App.5qw2dtxs.js +4 -0
  5. package/dist/App.6nc5acvk.js +4 -0
  6. package/dist/App.7vzbaz56.js +4 -0
  7. package/dist/App.8rfz30p1.js +4 -0
  8. package/dist/App.amwp54wf.js +4 -0
  9. package/dist/App.e4202qb4.js +267 -0
  10. package/dist/App.errxz2q4.js +4 -0
  11. package/dist/App.f8qsyhpr.js +4 -0
  12. package/dist/App.g8vq68n0.js +20 -0
  13. package/dist/App.kfyrnznw.js +13 -0
  14. package/dist/{App.mq6jqare.js → App.p02f4ret.js} +1 -1
  15. package/dist/App.p93mmyqw.js +4 -0
  16. package/dist/App.qmg33p02.js +4 -0
  17. package/dist/App.sdsc0258.js +4 -0
  18. package/dist/ConnectionsPage.7zqba1r0.js +3 -0
  19. package/dist/McpPage.kf2g327t.js +3 -0
  20. package/dist/SettingsPage.472c15ep.js +3 -0
  21. package/dist/SkillsPage.xdxnh68a.js +3 -0
  22. package/dist/TasksPage.7g0b8xwc.js +3 -0
  23. package/dist/TelemetryPage.pr7rbz4r.js +3 -0
  24. package/dist/TestsPage.zhc6rqjm.js +3 -0
  25. package/dist/apteva-kit.css +1 -1
  26. package/dist/index.html +1 -1
  27. package/dist/styles.css +1 -1
  28. package/package.json +9 -4
  29. package/src/auth/middleware.ts +2 -0
  30. package/src/channels/index.ts +40 -0
  31. package/src/channels/telegram.ts +306 -0
  32. package/src/db.ts +342 -11
  33. package/src/integrations/agentdojo.ts +1 -1
  34. package/src/mcp-handler.ts +31 -24
  35. package/src/mcp-platform.ts +41 -1
  36. package/src/providers.ts +22 -9
  37. package/src/routes/api/agent-utils.ts +38 -2
  38. package/src/routes/api/agents.ts +65 -2
  39. package/src/routes/api/channels.ts +182 -0
  40. package/src/routes/api/integrations.ts +13 -5
  41. package/src/routes/api/mcp.ts +27 -9
  42. package/src/routes/api/projects.ts +19 -2
  43. package/src/routes/api/system.ts +26 -12
  44. package/src/routes/api/telemetry.ts +30 -0
  45. package/src/routes/api/triggers.ts +478 -0
  46. package/src/routes/api/webhooks.ts +171 -0
  47. package/src/routes/api.ts +7 -1
  48. package/src/routes/static.ts +12 -3
  49. package/src/server.ts +43 -6
  50. package/src/triggers/agentdojo.ts +253 -0
  51. package/src/triggers/composio.ts +264 -0
  52. package/src/triggers/index.ts +71 -0
  53. package/src/tui/AgentList.tsx +145 -0
  54. package/src/tui/App.tsx +102 -0
  55. package/src/tui/Login.tsx +104 -0
  56. package/src/tui/api.ts +72 -0
  57. package/src/tui/index.tsx +7 -0
  58. package/src/web/App.tsx +18 -11
  59. package/src/web/components/agents/AgentCard.tsx +14 -7
  60. package/src/web/components/agents/AgentPanel.tsx +94 -137
  61. package/src/web/components/common/Icons.tsx +16 -0
  62. package/src/web/components/common/index.ts +1 -0
  63. package/src/web/components/connections/ConnectionsPage.tsx +54 -0
  64. package/src/web/components/connections/IntegrationsTab.tsx +144 -0
  65. package/src/web/components/connections/OverviewTab.tsx +137 -0
  66. package/src/web/components/connections/TriggersTab.tsx +1169 -0
  67. package/src/web/components/index.ts +1 -0
  68. package/src/web/components/layout/Header.tsx +196 -4
  69. package/src/web/components/layout/Sidebar.tsx +7 -1
  70. package/src/web/components/mcp/IntegrationsPanel.tsx +19 -3
  71. package/src/web/components/settings/SettingsPage.tsx +364 -2
  72. package/src/web/components/tasks/TasksPage.tsx +2 -2
  73. package/src/web/components/tests/TestsPage.tsx +1 -2
  74. package/src/web/context/TelemetryContext.tsx +14 -1
  75. package/src/web/context/index.ts +1 -1
  76. package/src/web/hooks/useAgents.ts +15 -11
  77. package/src/web/types.ts +1 -1
  78. package/dist/App.fq4xbpcz.js +0 -228
@@ -2,7 +2,7 @@ import { spawn } from "bun";
2
2
  import { join } from "path";
3
3
  import { homedir } from "os";
4
4
  import { mkdirSync, existsSync, rmSync } from "fs";
5
- import { agentProcesses, agentsStarting, getBinaryPathForAgent, getBinaryStatus, BIN_DIR, telemetryBroadcaster, type TelemetryEvent } from "../../server";
5
+ import { agentProcesses, agentsStarting, getBinaryPathForAgent, getBinaryStatus, BIN_DIR, telemetryBroadcaster, isShuttingDown, type TelemetryEvent } from "../../server";
6
6
  import { AgentDB, McpServerDB, SkillDB, TelemetryDB, generateId, getMultiAgentConfig, type Agent, type Project } from "../../db";
7
7
  import { ProviderKeys, PROVIDERS, type ProviderId } from "../../providers";
8
8
  import { binaryExists } from "../../binary";
@@ -520,8 +520,9 @@ export async function startAgentProcess(
520
520
  // Store process with port for tracking
521
521
  agentProcesses.set(agent.id, { proc, port });
522
522
 
523
- // Detect unexpected process exits (crashes)
523
+ // Detect unexpected process exits (crashes) — but not during server shutdown
524
524
  proc.exited.then((code) => {
525
+ if (isShuttingDown()) return; // Don't update DB during shutdown — keeps status "running" for auto-restart
525
526
  if (agentProcesses.has(agent.id)) {
526
527
  agentProcesses.delete(agent.id);
527
528
  setAgentStatus(agent.id, "stopped", code === 0 ? "exited" : "crashed");
@@ -632,6 +633,41 @@ export function toApiAgent(agent: Agent) {
632
633
  };
633
634
  }
634
635
 
636
+ // Batch transform: fetch all MCP servers + skills in 2 queries instead of N per agent
637
+ export function toApiAgentsBatch(agents: Agent[]) {
638
+ // Collect all unique IDs
639
+ const allMcpIds = new Set<string>();
640
+ const allSkillIds = new Set<string>();
641
+ for (const agent of agents) {
642
+ for (const id of agent.mcp_servers || []) allMcpIds.add(id);
643
+ for (const id of agent.skills || []) allSkillIds.add(id);
644
+ }
645
+
646
+ // Batch load in 2 queries
647
+ const mcpMap = McpServerDB.findByIds([...allMcpIds]);
648
+ const skillMap = SkillDB.findByIds([...allSkillIds]);
649
+
650
+ return agents.map(agent => {
651
+ const mcpServerDetails = (agent.mcp_servers || [])
652
+ .map(id => mcpMap.get(id))
653
+ .filter((s): s is NonNullable<typeof s> => !!s)
654
+ .map(s => ({ id: s.id, name: s.name, type: s.type, status: s.status, port: s.port, url: s.url }));
655
+
656
+ const skillDetails = (agent.skills || [])
657
+ .map(id => skillMap.get(id))
658
+ .filter((s): s is NonNullable<typeof s> => !!s)
659
+ .map(s => ({ id: s.id, name: s.name, description: s.description, version: s.version, enabled: s.enabled }));
660
+
661
+ return {
662
+ id: agent.id, name: agent.name, model: agent.model, provider: agent.provider,
663
+ systemPrompt: agent.system_prompt, status: agent.status, port: agent.port,
664
+ features: agent.features, mcpServers: agent.mcp_servers, mcpServerDetails,
665
+ skills: agent.skills, skillDetails, projectId: agent.project_id,
666
+ createdAt: agent.created_at, updatedAt: agent.updated_at,
667
+ };
668
+ });
669
+ }
670
+
635
671
  // Transform DB project to API response format
636
672
  export function toApiProject(project: Project) {
637
673
  return {
@@ -3,7 +3,7 @@ import { join } from "path";
3
3
  import { json, isDev } from "./helpers";
4
4
  import {
5
5
  agentFetch,
6
- toApiAgent,
6
+ toApiAgent, toApiAgentsBatch,
7
7
  checkPortFree,
8
8
  startAgentProcess,
9
9
  buildAgentConfig,
@@ -30,7 +30,7 @@ export async function handleAgentRoutes(
30
30
  // GET /api/agents - List all agents (excludes meta agent)
31
31
  if (path === "/api/agents" && method === "GET") {
32
32
  const agents = AgentDB.findAll().filter(a => a.id !== META_AGENT_ID);
33
- return json({ agents: agents.map(toApiAgent) });
33
+ return json({ agents: toApiAgentsBatch(agents) });
34
34
  }
35
35
 
36
36
  // POST /api/agents - Create a new agent
@@ -345,6 +345,69 @@ export async function handleAgentRoutes(
345
345
  }
346
346
  }
347
347
 
348
+ // ==================== WEBHOOK ENDPOINT ====================
349
+
350
+ // POST /api/agents/:id/webhook - Receive external trigger events and forward to agent chat
351
+ const webhookMatch = path.match(/^\/api\/agents\/([^/]+)\/webhook$/);
352
+ if (webhookMatch && method === "POST") {
353
+ const agent = AgentDB.findById(webhookMatch[1]);
354
+ if (!agent) {
355
+ return json({ error: "Agent not found" }, 404);
356
+ }
357
+
358
+ if (agent.status !== "running" || !agent.port) {
359
+ return json({ error: "Agent is not running" }, 400);
360
+ }
361
+
362
+ try {
363
+ const body = await req.json();
364
+
365
+ // Format the webhook payload as a chat message
366
+ const triggerSlug = body.trigger_name || body.type || "unknown_trigger";
367
+ const eventPayload = body.payload || body.data || body;
368
+
369
+ const triggerName = String(triggerSlug).replace(/_/g, " ");
370
+ const message = [
371
+ `[Trigger: ${triggerName}]`,
372
+ "",
373
+ "```json",
374
+ JSON.stringify(eventPayload, null, 2),
375
+ "```",
376
+ "",
377
+ "Process this event and take appropriate action.",
378
+ ].join("\n");
379
+
380
+ // Forward to agent's /chat endpoint
381
+ const response = await agentFetch(agent.id, agent.port, "/chat", {
382
+ method: "POST",
383
+ headers: { "Content-Type": "application/json" },
384
+ body: JSON.stringify({ message }),
385
+ });
386
+
387
+ // Consume the streaming response (we don't need the agent's reply)
388
+ if (response.body) {
389
+ try {
390
+ const reader = response.body.getReader();
391
+ while (true) {
392
+ const { done } = await reader.read();
393
+ if (done) break;
394
+ }
395
+ } catch {
396
+ // Ignore read errors
397
+ }
398
+ }
399
+
400
+ if (!response.ok) {
401
+ return json({ error: "Agent failed to process webhook" }, 502);
402
+ }
403
+
404
+ return json({ received: true, agent_id: agent.id, trigger: triggerSlug });
405
+ } catch (err) {
406
+ console.error(`Webhook proxy error for agent ${webhookMatch[1]}:`, err);
407
+ return json({ error: `Failed to process webhook: ${err}` }, 500);
408
+ }
409
+ }
410
+
348
411
  // ==================== THREAD & MESSAGE PROXY ====================
349
412
 
350
413
  // GET/POST /api/agents/:id/threads
@@ -0,0 +1,182 @@
1
+ import { json } from "./helpers";
2
+ import { ChannelDB, AgentDB } from "../../db";
3
+ import { encryptObject } from "../../crypto";
4
+ import { startChannel, stopChannel } from "../../channels";
5
+
6
+ export async function handleChannelRoutes(
7
+ req: Request,
8
+ path: string,
9
+ method: string,
10
+ ): Promise<Response | null> {
11
+ // GET /api/channels - List all channels
12
+ if (path === "/api/channels" && method === "GET") {
13
+ const channels = ChannelDB.findAll();
14
+ // Strip encrypted config from list response
15
+ const safe = channels.map(ch => ({
16
+ id: ch.id,
17
+ type: ch.type,
18
+ name: ch.name,
19
+ agent_id: ch.agent_id,
20
+ status: ch.status,
21
+ error: ch.error,
22
+ project_id: ch.project_id,
23
+ created_at: ch.created_at,
24
+ updated_at: ch.updated_at,
25
+ }));
26
+ return json({ channels: safe });
27
+ }
28
+
29
+ // POST /api/channels - Create a new channel
30
+ if (path === "/api/channels" && method === "POST") {
31
+ const body = await req.json();
32
+ const { type, name, agent_id, config, project_id } = body;
33
+
34
+ if (!type || !name || !agent_id || !config) {
35
+ return json({ error: "Missing required fields: type, name, agent_id, config" }, 400);
36
+ }
37
+
38
+ if (type !== "telegram") {
39
+ return json({ error: `Unsupported channel type: ${type}. Supported: telegram` }, 400);
40
+ }
41
+
42
+ // Validate agent exists
43
+ const agent = AgentDB.findById(agent_id);
44
+ if (!agent) {
45
+ return json({ error: "Agent not found" }, 404);
46
+ }
47
+
48
+ // Validate config has required fields
49
+ if (!config.botToken) {
50
+ return json({ error: "Missing botToken in config" }, 400);
51
+ }
52
+
53
+ // Encrypt config before storing
54
+ const encryptedConfig = encryptObject(config);
55
+
56
+ const channel = ChannelDB.create({
57
+ type,
58
+ name,
59
+ agent_id,
60
+ config: encryptedConfig,
61
+ project_id: project_id || null,
62
+ });
63
+
64
+ return json({
65
+ channel: {
66
+ id: channel.id,
67
+ type: channel.type,
68
+ name: channel.name,
69
+ agent_id: channel.agent_id,
70
+ status: channel.status,
71
+ error: channel.error,
72
+ project_id: channel.project_id,
73
+ created_at: channel.created_at,
74
+ },
75
+ }, 201);
76
+ }
77
+
78
+ // Routes with channel ID
79
+ const channelMatch = path.match(/^\/api\/channels\/([^/]+)$/);
80
+ const channelActionMatch = path.match(/^\/api\/channels\/([^/]+)\/(start|stop)$/);
81
+
82
+ // GET /api/channels/:id - Get channel detail
83
+ if (channelMatch && method === "GET") {
84
+ const channel = ChannelDB.findById(channelMatch[1]);
85
+ if (!channel) return json({ error: "Channel not found" }, 404);
86
+
87
+ return json({
88
+ channel: {
89
+ id: channel.id,
90
+ type: channel.type,
91
+ name: channel.name,
92
+ agent_id: channel.agent_id,
93
+ status: channel.status,
94
+ error: channel.error,
95
+ project_id: channel.project_id,
96
+ created_at: channel.created_at,
97
+ updated_at: channel.updated_at,
98
+ },
99
+ });
100
+ }
101
+
102
+ // PUT /api/channels/:id - Update channel
103
+ if (channelMatch && method === "PUT") {
104
+ const channel = ChannelDB.findById(channelMatch[1]);
105
+ if (!channel) return json({ error: "Channel not found" }, 404);
106
+
107
+ if (channel.status === "running") {
108
+ return json({ error: "Stop the channel before updating" }, 400);
109
+ }
110
+
111
+ const body = await req.json();
112
+ const updates: Record<string, any> = {};
113
+
114
+ if (body.name !== undefined) updates.name = body.name;
115
+ if (body.agent_id !== undefined) {
116
+ const agent = AgentDB.findById(body.agent_id);
117
+ if (!agent) return json({ error: "Agent not found" }, 404);
118
+ updates.agent_id = body.agent_id;
119
+ }
120
+ if (body.config !== undefined) {
121
+ if (!body.config.botToken) {
122
+ return json({ error: "Missing botToken in config" }, 400);
123
+ }
124
+ updates.config = encryptObject(body.config);
125
+ }
126
+ if (body.project_id !== undefined) updates.project_id = body.project_id;
127
+
128
+ const updated = ChannelDB.update(channelMatch[1], updates);
129
+ return json({
130
+ channel: updated ? {
131
+ id: updated.id,
132
+ type: updated.type,
133
+ name: updated.name,
134
+ agent_id: updated.agent_id,
135
+ status: updated.status,
136
+ project_id: updated.project_id,
137
+ } : null,
138
+ });
139
+ }
140
+
141
+ // DELETE /api/channels/:id - Delete channel
142
+ if (channelMatch && method === "DELETE") {
143
+ const channel = ChannelDB.findById(channelMatch[1]);
144
+ if (!channel) return json({ error: "Channel not found" }, 404);
145
+
146
+ // Stop if running
147
+ if (channel.status === "running") {
148
+ await stopChannel(channel.id);
149
+ }
150
+
151
+ ChannelDB.delete(channelMatch[1]);
152
+ return json({ deleted: true });
153
+ }
154
+
155
+ // POST /api/channels/:id/start - Start channel
156
+ if (channelActionMatch && channelActionMatch[2] === "start" && method === "POST") {
157
+ const channel = ChannelDB.findById(channelActionMatch[1]);
158
+ if (!channel) return json({ error: "Channel not found" }, 404);
159
+
160
+ if (channel.status === "running") {
161
+ return json({ error: "Channel is already running" }, 400);
162
+ }
163
+
164
+ const result = await startChannel(channel.id);
165
+ if (!result.success) {
166
+ return json({ error: result.error || "Failed to start channel" }, 500);
167
+ }
168
+
169
+ return json({ started: true });
170
+ }
171
+
172
+ // POST /api/channels/:id/stop - Stop channel
173
+ if (channelActionMatch && channelActionMatch[2] === "stop" && method === "POST") {
174
+ const channel = ChannelDB.findById(channelActionMatch[1]);
175
+ if (!channel) return json({ error: "Channel not found" }, 404);
176
+
177
+ await stopChannel(channel.id);
178
+ return json({ stopped: true });
179
+ }
180
+
181
+ return null;
182
+ }
@@ -77,19 +77,25 @@ export async function handleIntegrationRoutes(
77
77
 
78
78
  const url = new URL(req.url);
79
79
  const projectId = url.searchParams.get("project_id") || null;
80
+ console.log(`[integrations/connected] provider=${providerId}, projectId=${projectId}`);
80
81
  const apiKey = ProviderKeys.getDecryptedForProject(providerId, projectId);
82
+ console.log(`[integrations/connected] apiKey found: ${!!apiKey}, length: ${apiKey?.length || 0}`);
81
83
  if (!apiKey) {
84
+ console.log(`[integrations/connected] NO API KEY for ${providerId}`);
82
85
  return json({ error: `${provider.name} API key not configured`, accounts: [] }, 200);
83
86
  }
84
87
 
85
88
  // Use Apteva user ID as the entity ID for the provider
86
- const userId = user?.id || "default";
89
+ if (!user?.id) {
90
+ return json({ error: "Authentication required" }, 401);
91
+ }
87
92
 
88
93
  try {
89
- const accounts = await provider.listConnectedAccounts(apiKey, userId);
94
+ const accounts = await provider.listConnectedAccounts(apiKey, user.id);
95
+ console.log(`[integrations/connected] Got ${accounts.length} accounts from ${providerId}`);
90
96
  return json({ accounts });
91
97
  } catch (e) {
92
- console.error(`Failed to list connected accounts from ${providerId}:`, e);
98
+ console.error(`[integrations/connected] Failed from ${providerId}:`, e);
93
99
  return json({ error: "Failed to fetch connected accounts" }, 500);
94
100
  }
95
101
  }
@@ -116,12 +122,14 @@ export async function handleIntegrationRoutes(
116
122
  }
117
123
 
118
124
  // Use Apteva user ID as the entity ID
119
- const userId = user?.id || "default";
125
+ if (!user?.id) {
126
+ return json({ error: "Authentication required" }, 401);
127
+ }
120
128
 
121
129
  // Default redirect URL back to our integrations page
122
130
  const callbackUrl = redirectUrl || `http://localhost:${process.env.PORT || 4280}/mcp?tab=hosted&connected=${appSlug}`;
123
131
 
124
- const result = await provider.initiateConnection(apiKey, userId, appSlug, callbackUrl, credentials);
132
+ const result = await provider.initiateConnection(apiKey, user.id, appSlug, callbackUrl, credentials);
125
133
  return json(result);
126
134
  } catch (e) {
127
135
  console.error(`Failed to initiate connection for ${providerId}:`, e);
@@ -221,6 +221,21 @@ export async function handleMcpRoutes(
221
221
  });
222
222
  };
223
223
 
224
+ // Validate package/command names to prevent injection
225
+ const SAFE_PACKAGE_RE = /^(@[a-z0-9._-]+\/)?[a-z0-9._-]+(@[a-z0-9^~>=<.*-]+)?(\[[\w,]+\])?$/i;
226
+
227
+ // Parse args safely — split on whitespace but respect quoted strings
228
+ const parseArgs = (raw: string, env: Record<string, string>): string[] => {
229
+ const substituted = substituteEnvVars(raw, env);
230
+ const args: string[] = [];
231
+ const re = /"([^"]*?)"|'([^']*?)'|(\S+)/g;
232
+ let m;
233
+ while ((m = re.exec(substituted)) !== null) {
234
+ args.push(m[1] ?? m[2] ?? m[3]);
235
+ }
236
+ return args;
237
+ };
238
+
224
239
  let cmd: string[];
225
240
  const serverEnv = server.env || {};
226
241
 
@@ -228,17 +243,19 @@ export async function handleMcpRoutes(
228
243
  // Custom command - substitute env vars in args
229
244
  cmd = server.command.split(" ");
230
245
  if (server.args) {
231
- const substitutedArgs = substituteEnvVars(server.args, serverEnv);
232
- cmd.push(...substitutedArgs.split(" "));
246
+ cmd.push(...parseArgs(server.args, serverEnv));
233
247
  }
234
248
  } else if (server.type === "pip" && server.package) {
235
249
  // Python pip package - install first, then run module
236
250
  const pipPackage = server.package;
251
+ if (!SAFE_PACKAGE_RE.test(pipPackage)) {
252
+ return json({ error: "Invalid pip package name" }, 400);
253
+ }
237
254
  const pipModule = server.pip_module || server.package.split("[")[0]; // Default: package name without extras
238
255
 
239
256
  console.log(`Installing pip package: ${pipPackage}...`);
240
257
  const installResult = spawn({
241
- cmd: ["pip", "install", "--quiet", "--break-system-packages", pipPackage],
258
+ cmd: ["pip", "install", "--quiet", "--no-scripts", pipPackage],
242
259
  env: { ...process.env as Record<string, string>, ...serverEnv },
243
260
  stdout: "pipe",
244
261
  stderr: "pipe",
@@ -254,15 +271,16 @@ export async function handleMcpRoutes(
254
271
  // Now run the module
255
272
  cmd = ["python", "-m", pipModule];
256
273
  if (server.args) {
257
- const substitutedArgs = substituteEnvVars(server.args, serverEnv);
258
- cmd.push(...substitutedArgs.split(" "));
274
+ cmd.push(...parseArgs(server.args, serverEnv));
259
275
  }
260
276
  } else if (server.package) {
261
- // npm package - use npx
262
- cmd = ["npx", "-y", server.package];
277
+ // npm package - use npx with --ignore-scripts to prevent supply chain attacks
278
+ if (!SAFE_PACKAGE_RE.test(server.package)) {
279
+ return json({ error: "Invalid npm package name" }, 400);
280
+ }
281
+ cmd = ["npx", "--ignore-scripts", "-y", server.package];
263
282
  if (server.args) {
264
- const substitutedArgs = substituteEnvVars(server.args, serverEnv);
265
- cmd.push(...substitutedArgs.split(" "));
283
+ cmd.push(...parseArgs(server.args, serverEnv));
266
284
  }
267
285
  } else {
268
286
  return json({ error: "No command or package specified" }, 400);
@@ -1,6 +1,7 @@
1
1
  import { json } from "./helpers";
2
2
  import { AgentDB, ProjectDB, type Project } from "../../db";
3
- import { toApiAgent, toApiProject } from "./agent-utils";
3
+ import { toApiAgent, toApiAgentsBatch, toApiProject, setAgentStatus } from "./agent-utils";
4
+ import { agentProcesses } from "../../server";
4
5
 
5
6
  export async function handleProjectRoutes(
6
7
  req: Request,
@@ -54,7 +55,7 @@ export async function handleProjectRoutes(
54
55
  const agents = AgentDB.findByProject(project.id);
55
56
  return json({
56
57
  project: toApiProject(project),
57
- agents: agents.map(toApiAgent),
58
+ agents: toApiAgentsBatch(agents),
58
59
  });
59
60
  }
60
61
 
@@ -87,6 +88,22 @@ export async function handleProjectRoutes(
87
88
  return json({ error: "Project not found" }, 404);
88
89
  }
89
90
 
91
+ // Stop any running agents in this project first
92
+ const projectAgents = AgentDB.findByProject(projectMatch[1]);
93
+ for (const agent of projectAgents) {
94
+ if (agent.status === "running") {
95
+ const entry = agentProcesses.get(agent.id);
96
+ if (entry) {
97
+ try {
98
+ await fetch(`http://localhost:${entry.port}/shutdown`, { method: "POST", signal: AbortSignal.timeout(1000) }).catch(() => {});
99
+ entry.proc.kill();
100
+ } catch {}
101
+ agentProcesses.delete(agent.id);
102
+ }
103
+ setAgentStatus(agent.id, "stopped", "project_deleted");
104
+ }
105
+ }
106
+
90
107
  ProjectDB.delete(projectMatch[1]);
91
108
  return json({ success: true });
92
109
  }
@@ -139,17 +139,21 @@ export async function handleSystemRoutes(
139
139
 
140
140
  const allTasks: any[] = [];
141
141
 
142
- for (const agent of runningAgents) {
143
- const data = await fetchFromAgent(agent.id, agent.port!, `/tasks?status=${status}`);
144
- if (data?.tasks) {
145
- // Add agent info to each task
146
- for (const task of data.tasks) {
147
- allTasks.push({
148
- ...task,
149
- agentId: agent.id,
150
- agentName: agent.name,
151
- });
142
+ // Fetch tasks from all agents in parallel
143
+ const results = await Promise.all(
144
+ runningAgents.map(async (agent) => {
145
+ try {
146
+ const data = await fetchFromAgent(agent.id, agent.port!, `/tasks?status=${status}`);
147
+ return { agent, tasks: data?.tasks || [] };
148
+ } catch {
149
+ return { agent, tasks: [] };
152
150
  }
151
+ })
152
+ );
153
+
154
+ for (const { agent, tasks } of results) {
155
+ for (const task of tasks) {
156
+ allTasks.push({ ...task, agentId: agent.id, agentName: agent.name });
153
157
  }
154
158
  }
155
159
 
@@ -191,8 +195,18 @@ export async function handleSystemRoutes(
191
195
  let completedTasks = 0;
192
196
  let runningTasks = 0;
193
197
 
194
- for (const agent of runningAgents) {
195
- const data = await fetchFromAgent(agent.id, agent.port!, "/tasks?status=all");
198
+ // Fetch task stats from all agents in parallel
199
+ const taskResults = await Promise.all(
200
+ runningAgents.map(async (agent) => {
201
+ try {
202
+ return await fetchFromAgent(agent.id, agent.port!, "/tasks?status=all");
203
+ } catch {
204
+ return null;
205
+ }
206
+ })
207
+ );
208
+
209
+ for (const data of taskResults) {
196
210
  if (data?.tasks) {
197
211
  totalTasks += data.tasks.length;
198
212
  for (const task of data.tasks) {
@@ -139,5 +139,35 @@ export async function handleTelemetryRoutes(
139
139
  return json({ deleted });
140
140
  }
141
141
 
142
+ // --- Notification endpoints (piggyback on telemetry `seen` flag) ---
143
+
144
+ // GET /api/notifications - Get notification-worthy events
145
+ if (path === "/api/notifications" && method === "GET") {
146
+ const url = new URL(req.url);
147
+ const limit = parseInt(url.searchParams.get("limit") || "50");
148
+ const notifications = TelemetryDB.getNotifications(limit);
149
+ return json({ notifications });
150
+ }
151
+
152
+ // GET /api/notifications/count - Get unseen notification count
153
+ if (path === "/api/notifications/count" && method === "GET") {
154
+ const count = TelemetryDB.getUnseenCount();
155
+ return json({ count });
156
+ }
157
+
158
+ // POST /api/notifications/mark-seen - Mark specific notifications as seen
159
+ if (path === "/api/notifications/mark-seen" && method === "POST") {
160
+ const body = await req.json() as { ids?: string[]; all?: boolean };
161
+ if (body.all) {
162
+ const updated = TelemetryDB.markAllSeen();
163
+ return json({ updated });
164
+ }
165
+ if (body.ids && body.ids.length > 0) {
166
+ const updated = TelemetryDB.markSeen(body.ids);
167
+ return json({ updated });
168
+ }
169
+ return json({ error: "Provide ids array or all: true" }, 400);
170
+ }
171
+
142
172
  return null;
143
173
  }