apteva 0.4.16 → 0.4.18

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 (62) hide show
  1. package/dist/ActivityPage.yv28a2vj.js +3 -0
  2. package/dist/ApiDocsPage.4ccwjjbk.js +4 -0
  3. package/dist/App.155wke5v.js +4 -0
  4. package/dist/App.2e19nvn4.js +13 -0
  5. package/dist/App.2ye1b5n0.js +4 -0
  6. package/dist/App.4da4ycbe.js +4 -0
  7. package/dist/App.b6wtzd1j.js +4 -0
  8. package/dist/App.fjrh28tf.js +4 -0
  9. package/dist/App.htc36cy8.js +4 -0
  10. package/dist/App.me6reaa6.js +4 -0
  11. package/dist/App.n5q6p960.js +4 -0
  12. package/dist/App.nft7h9jt.js +4 -0
  13. package/dist/App.np463xvy.js +4 -0
  14. package/dist/App.nps62kvt.js +4 -0
  15. package/dist/App.q8ws33cc.js +181 -0
  16. package/dist/App.tb0y0jmt.js +40 -0
  17. package/dist/ConnectionsPage.52evzrp7.js +3 -0
  18. package/dist/McpPage.bjqrp0n2.js +3 -0
  19. package/dist/SettingsPage.es76hnj2.js +3 -0
  20. package/dist/SkillsPage.06h8yf0h.js +3 -0
  21. package/dist/TasksPage.99df66mk.js +3 -0
  22. package/dist/TelemetryPage.bmdnxhq7.js +3 -0
  23. package/dist/TestsPage.denxrg8c.js +3 -0
  24. package/dist/index.html +1 -1
  25. package/dist/styles.css +1 -1
  26. package/package.json +1 -1
  27. package/src/auth/middleware.ts +2 -0
  28. package/src/db.ts +162 -11
  29. package/src/mcp-platform.ts +41 -1
  30. package/src/routes/api/agent-utils.ts +38 -2
  31. package/src/routes/api/agents.ts +65 -2
  32. package/src/routes/api/projects.ts +19 -2
  33. package/src/routes/api/system.ts +26 -12
  34. package/src/routes/api/triggers.ts +458 -0
  35. package/src/routes/api/webhooks.ts +171 -0
  36. package/src/routes/api.ts +4 -0
  37. package/src/routes/static.ts +12 -3
  38. package/src/server.ts +6 -4
  39. package/src/triggers/agentdojo.ts +248 -0
  40. package/src/triggers/composio.ts +264 -0
  41. package/src/triggers/index.ts +71 -0
  42. package/src/web/App.tsx +20 -12
  43. package/src/web/components/agents/AgentCard.tsx +14 -7
  44. package/src/web/components/agents/AgentPanel.tsx +105 -115
  45. package/src/web/components/common/Icons.tsx +8 -0
  46. package/src/web/components/common/index.ts +1 -0
  47. package/src/web/components/connections/ConnectionsPage.tsx +54 -0
  48. package/src/web/components/connections/IntegrationsTab.tsx +144 -0
  49. package/src/web/components/connections/OverviewTab.tsx +183 -0
  50. package/src/web/components/connections/TriggersTab.tsx +690 -0
  51. package/src/web/components/index.ts +1 -0
  52. package/src/web/components/layout/Sidebar.tsx +7 -1
  53. package/src/web/components/mcp/IntegrationsPanel.tsx +19 -3
  54. package/src/web/components/mcp/McpPage.tsx +9 -3
  55. package/src/web/components/settings/SettingsPage.tsx +96 -2
  56. package/src/web/components/tasks/TasksPage.tsx +2 -2
  57. package/src/web/components/tests/TestsPage.tsx +1 -2
  58. package/src/web/context/TelemetryContext.tsx +14 -1
  59. package/src/web/context/index.ts +1 -1
  60. package/src/web/hooks/useAgents.ts +15 -11
  61. package/src/web/types.ts +1 -1
  62. package/dist/App.2194efgj.js +0 -228
package/src/db.ts CHANGED
@@ -194,6 +194,29 @@ export interface SkillRow {
194
194
  updated_at: string;
195
195
  }
196
196
 
197
+ // Subscription: maps trigger events to agents for routing
198
+ export interface Subscription {
199
+ id: string;
200
+ trigger_slug: string;
201
+ trigger_instance_id: string | null;
202
+ agent_id: string;
203
+ enabled: boolean;
204
+ project_id: string | null;
205
+ created_at: string;
206
+ updated_at: string;
207
+ }
208
+
209
+ export interface SubscriptionRow {
210
+ id: string;
211
+ trigger_slug: string;
212
+ trigger_instance_id: string | null;
213
+ agent_id: string;
214
+ enabled: number;
215
+ project_id: string | null;
216
+ created_at: string;
217
+ updated_at: string;
218
+ }
219
+
197
220
  export interface McpServerRow {
198
221
  id: string;
199
222
  name: string;
@@ -256,11 +279,21 @@ export function initDatabase(dataDir: string): Database {
256
279
 
257
280
  // Enable WAL mode for better concurrent access
258
281
  db.run("PRAGMA journal_mode = WAL");
282
+ db.run("PRAGMA busy_timeout = 5000");
259
283
  db.run("PRAGMA foreign_keys = ON");
260
284
 
261
285
  // Run migrations
262
286
  runMigrations();
263
287
 
288
+ // Auto-set instance_url from env if not already configured
289
+ const envInstanceUrl = process.env.INSTANCE_URL || process.env.PUBLIC_URL;
290
+ if (envInstanceUrl) {
291
+ const current = SettingsDB.get("instance_url");
292
+ if (!current) {
293
+ SettingsDB.set("instance_url", envInstanceUrl.replace(/\/+$/, ""));
294
+ }
295
+ }
296
+
264
297
  // Database initialized silently
265
298
  return db;
266
299
  }
@@ -685,6 +718,25 @@ function runMigrations() {
685
718
  CREATE INDEX IF NOT EXISTS idx_mcp_server_tools_server ON mcp_server_tools(server_id);
686
719
  `,
687
720
  },
721
+ {
722
+ name: "031_create_subscriptions",
723
+ sql: `
724
+ CREATE TABLE IF NOT EXISTS subscriptions (
725
+ id TEXT PRIMARY KEY,
726
+ trigger_slug TEXT NOT NULL,
727
+ trigger_instance_id TEXT,
728
+ agent_id TEXT NOT NULL,
729
+ enabled INTEGER NOT NULL DEFAULT 1,
730
+ project_id TEXT,
731
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
732
+ updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
733
+ FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE CASCADE
734
+ );
735
+ CREATE INDEX IF NOT EXISTS idx_subscriptions_agent ON subscriptions(agent_id);
736
+ CREATE INDEX IF NOT EXISTS idx_subscriptions_trigger_slug ON subscriptions(trigger_slug);
737
+ CREATE INDEX IF NOT EXISTS idx_subscriptions_trigger_instance ON subscriptions(trigger_instance_id);
738
+ `,
739
+ },
688
740
  {
689
741
  name: "029_fix_provider_keys_unique_constraint",
690
742
  sql: `
@@ -723,8 +775,8 @@ function runMigrations() {
723
775
  for (const migration of migrations) {
724
776
  if (!applied.has(migration.name)) {
725
777
  try {
726
- // Migration runs silently
727
- db.run(migration.sql);
778
+ // Migration runs silently (exec supports multi-statement SQL)
779
+ db.exec(migration.sql);
728
780
  db.run("INSERT INTO migrations (name) VALUES (?)", [migration.name]);
729
781
  } catch (err) {
730
782
  // Log error but continue - some migrations may fail if partially applied
@@ -853,12 +905,13 @@ export const AgentDB = {
853
905
  return rows.map(rowToAgent);
854
906
  },
855
907
 
856
- // Get running agents (for auto-restart)
908
+ // Get running agents
857
909
  findRunning(): Agent[] {
858
910
  const rows = db.query("SELECT * FROM agents WHERE status = 'running'").all() as AgentRow[];
859
911
  return rows.map(rowToAgent);
860
912
  },
861
913
 
914
+
862
915
  // Update agent
863
916
  update(id: string, updates: Partial<Omit<Agent, "id" | "created_at">>): Agent | null {
864
917
  const agent = this.findById(id);
@@ -907,7 +960,6 @@ export const AgentDB = {
907
960
  fields.push("project_id = ?");
908
961
  values.push(updates.project_id);
909
962
  }
910
-
911
963
  if (fields.length > 0) {
912
964
  fields.push("updated_at = ?");
913
965
  values.push(new Date().toISOString());
@@ -1076,8 +1128,12 @@ export const ProjectDB = {
1076
1128
  return this.findById(id);
1077
1129
  },
1078
1130
 
1079
- // Delete project (agents will have project_id set to NULL)
1131
+ // Delete project with full cleanup
1132
+ // FK constraints handle: agents, mcp_servers, skills (SET NULL), provider_keys (CASCADE)
1133
+ // Manual cleanup: subscriptions, test_cases (no FK on project_id)
1080
1134
  delete(id: string): boolean {
1135
+ db.run("UPDATE subscriptions SET project_id = NULL WHERE project_id = ?", [id]);
1136
+ db.run("UPDATE test_cases SET project_id = NULL WHERE project_id = ?", [id]);
1081
1137
  const result = db.run("DELETE FROM projects WHERE id = ?", [id]);
1082
1138
  return result.changes > 0;
1083
1139
  },
@@ -1386,6 +1442,15 @@ export const McpServerDB = {
1386
1442
  return row ? rowToMcpServer(row) : null;
1387
1443
  },
1388
1444
 
1445
+ findByIds(ids: string[]): Map<string, McpServer> {
1446
+ if (ids.length === 0) return new Map();
1447
+ const placeholders = ids.map(() => "?").join(",");
1448
+ const rows = db.query(`SELECT * FROM mcp_servers WHERE id IN (${placeholders})`).all(...ids) as McpServerRow[];
1449
+ const map = new Map<string, McpServer>();
1450
+ for (const row of rows) map.set(row.id, rowToMcpServer(row));
1451
+ return map;
1452
+ },
1453
+
1389
1454
  findAll(): McpServer[] {
1390
1455
  const rows = db.query("SELECT * FROM mcp_servers ORDER BY created_at DESC").all() as McpServerRow[];
1391
1456
  return rows.map(rowToMcpServer);
@@ -2388,6 +2453,15 @@ export const SkillDB = {
2388
2453
  return row ? rowToSkill(row) : null;
2389
2454
  },
2390
2455
 
2456
+ findByIds(ids: string[]): Map<string, Skill> {
2457
+ if (ids.length === 0) return new Map();
2458
+ const placeholders = ids.map(() => "?").join(",");
2459
+ const rows = db.query(`SELECT * FROM skills WHERE id IN (${placeholders})`).all(...ids) as SkillRow[];
2460
+ const map = new Map<string, Skill>();
2461
+ for (const row of rows) map.set(row.id, rowToSkill(row));
2462
+ return map;
2463
+ },
2464
+
2391
2465
  // Find skill by name
2392
2466
  findByName(name: string): Skill | null {
2393
2467
  const row = db.query("SELECT * FROM skills WHERE name = ?").get(name) as SkillRow | null;
@@ -2503,12 +2577,6 @@ export const SkillDB = {
2503
2577
  return row.count;
2504
2578
  },
2505
2579
 
2506
- // Check if skill with name exists
2507
- exists(name: string): boolean {
2508
- const row = db.query("SELECT COUNT(*) as count FROM skills WHERE name = ?").get(name) as { count: number };
2509
- return row.count > 0;
2510
- },
2511
-
2512
2580
  // Find skills by project (null = global only)
2513
2581
  findByProject(projectId: string | null): Skill[] {
2514
2582
  if (projectId === null) {
@@ -2575,6 +2643,89 @@ function rowToSkill(row: SkillRow): Skill {
2575
2643
  };
2576
2644
  }
2577
2645
 
2646
+ // Subscription row → Subscription
2647
+ function rowToSubscription(row: SubscriptionRow): Subscription {
2648
+ return {
2649
+ id: row.id,
2650
+ trigger_slug: row.trigger_slug,
2651
+ trigger_instance_id: row.trigger_instance_id,
2652
+ agent_id: row.agent_id,
2653
+ enabled: row.enabled === 1,
2654
+ project_id: row.project_id,
2655
+ created_at: row.created_at,
2656
+ updated_at: row.updated_at,
2657
+ };
2658
+ }
2659
+
2660
+ export const SubscriptionDB = {
2661
+ create(sub: Omit<Subscription, "id" | "created_at" | "updated_at">): Subscription {
2662
+ const id = generateId();
2663
+ const now = new Date().toISOString();
2664
+ db.run(
2665
+ `INSERT INTO subscriptions (id, trigger_slug, trigger_instance_id, agent_id, enabled, project_id, created_at, updated_at)
2666
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
2667
+ [id, sub.trigger_slug, sub.trigger_instance_id || null, sub.agent_id, sub.enabled ? 1 : 0, sub.project_id || null, now, now]
2668
+ );
2669
+ return this.findById(id)!;
2670
+ },
2671
+
2672
+ findById(id: string): Subscription | null {
2673
+ const row = db.query("SELECT * FROM subscriptions WHERE id = ?").get(id) as SubscriptionRow | null;
2674
+ return row ? rowToSubscription(row) : null;
2675
+ },
2676
+
2677
+ findByTriggerInstanceId(instanceId: string): Subscription[] {
2678
+ const rows = db.query("SELECT * FROM subscriptions WHERE trigger_instance_id = ?").all(instanceId) as SubscriptionRow[];
2679
+ return rows.map(rowToSubscription);
2680
+ },
2681
+
2682
+ findByTriggerSlug(slug: string): Subscription[] {
2683
+ const rows = db.query("SELECT * FROM subscriptions WHERE trigger_slug = ?").all(slug) as SubscriptionRow[];
2684
+ return rows.map(rowToSubscription);
2685
+ },
2686
+
2687
+ findByAgentId(agentId: string): Subscription[] {
2688
+ const rows = db.query("SELECT * FROM subscriptions WHERE agent_id = ?").all(agentId) as SubscriptionRow[];
2689
+ return rows.map(rowToSubscription);
2690
+ },
2691
+
2692
+ findAll(projectId?: string | null): Subscription[] {
2693
+ if (projectId) {
2694
+ const rows = db.query("SELECT * FROM subscriptions WHERE project_id = ? ORDER BY created_at DESC").all(projectId) as SubscriptionRow[];
2695
+ return rows.map(rowToSubscription);
2696
+ }
2697
+ const rows = db.query("SELECT * FROM subscriptions ORDER BY created_at DESC").all() as SubscriptionRow[];
2698
+ return rows.map(rowToSubscription);
2699
+ },
2700
+
2701
+ update(id: string, updates: Partial<Pick<Subscription, "trigger_slug" | "trigger_instance_id" | "agent_id" | "enabled">>): Subscription | null {
2702
+ const sub = this.findById(id);
2703
+ if (!sub) return null;
2704
+
2705
+ const fields: string[] = [];
2706
+ const values: (string | number | null)[] = [];
2707
+
2708
+ if (updates.trigger_slug !== undefined) { fields.push("trigger_slug = ?"); values.push(updates.trigger_slug); }
2709
+ if (updates.trigger_instance_id !== undefined) { fields.push("trigger_instance_id = ?"); values.push(updates.trigger_instance_id || null); }
2710
+ if (updates.agent_id !== undefined) { fields.push("agent_id = ?"); values.push(updates.agent_id); }
2711
+ if (updates.enabled !== undefined) { fields.push("enabled = ?"); values.push(updates.enabled ? 1 : 0); }
2712
+
2713
+ if (fields.length === 0) return sub;
2714
+
2715
+ fields.push("updated_at = ?");
2716
+ values.push(new Date().toISOString());
2717
+ values.push(id);
2718
+
2719
+ db.run(`UPDATE subscriptions SET ${fields.join(", ")} WHERE id = ?`, values);
2720
+ return this.findById(id);
2721
+ },
2722
+
2723
+ delete(id: string): boolean {
2724
+ const result = db.run("DELETE FROM subscriptions WHERE id = ?", [id]);
2725
+ return result.changes > 0;
2726
+ },
2727
+ };
2728
+
2578
2729
  // Generate unique ID
2579
2730
  export function generateId(): string {
2580
2731
  return Math.random().toString(36).substring(2, 15);
@@ -346,6 +346,25 @@ After creating, assign to agents with assign_mcp_server_to_agent. HTTP servers w
346
346
  required: ["agent_id", "skill_id"],
347
347
  },
348
348
  },
349
+ {
350
+ name: "create_skill",
351
+ description: "Create a new skill. Skills are reusable instruction sets (markdown content) that give agents specialized capabilities. Provide a name, description, and the full instructions content (markdown). Optionally specify allowed MCP tools. If the user is working within a project, set project_id to scope the skill to that project.",
352
+ inputSchema: {
353
+ type: "object",
354
+ properties: {
355
+ name: { type: "string", description: "The skill name" },
356
+ description: { type: "string", description: "Short description of what the skill does" },
357
+ content: { type: "string", description: "Full skill instructions in markdown format" },
358
+ allowed_tools: {
359
+ type: "array",
360
+ items: { type: "string" },
361
+ description: "Optional list of MCP tool names this skill is allowed to use",
362
+ },
363
+ project_id: { type: "string", description: "Project ID to scope the skill to. Use the current project ID from context when the user is working within a project. Omit for a global skill." },
364
+ },
365
+ required: ["name", "description", "content"],
366
+ },
367
+ },
349
368
  {
350
369
  name: "delete_skill",
351
370
  description: "Delete a skill. It will be unassigned from all agents.",
@@ -824,6 +843,27 @@ async function executeTool(name: string, args: Record<string, any>): Promise<{ c
824
843
  return { content: [{ type: "text", text: `Removed skill from agent "${agent.name}". Restart the agent for changes to take effect.` }] };
825
844
  }
826
845
 
846
+ case "create_skill": {
847
+ if (!args.name || !args.description || !args.content) {
848
+ return { content: [{ type: "text", text: "name, description, and content are required" }], isError: true };
849
+ }
850
+ const newSkill = SkillDB.create({
851
+ name: args.name,
852
+ description: args.description,
853
+ content: args.content,
854
+ version: "1.0.0",
855
+ license: null,
856
+ compatibility: null,
857
+ metadata: {},
858
+ allowed_tools: args.allowed_tools || [],
859
+ source: "local",
860
+ source_url: null,
861
+ enabled: true,
862
+ project_id: args.project_id || null,
863
+ });
864
+ return { content: [{ type: "text", text: `Skill "${newSkill.name}" created (ID: ${newSkill.id}). You can now assign it to an agent with assign_skill_to_agent.` }] };
865
+ }
866
+
827
867
  case "delete_skill": {
828
868
  const skill = SkillDB.findById(args.skill_id);
829
869
  if (!skill) {
@@ -977,7 +1017,7 @@ You can manage:
977
1017
  - AGENTS: Create, configure, start, stop, and delete AI agents. Each agent has a provider (LLM), model, system prompt, and optional features (memory, tasks, vision, MCP tools, files).
978
1018
  - PROJECTS: Organize agents into projects for grouping.
979
1019
  - MCP SERVERS: Tool integrations that give agents capabilities (web search, file access, APIs). Assign servers to agents.
980
- - SKILLS: Reusable instruction sets that specialize agent behavior. Assign skills to agents.
1020
+ - SKILLS: Reusable instruction sets that specialize agent behavior. Use create_skill to create new skills (pass project_id from context to scope to the current project), then assign them to agents. Use list_skills, get_skill, create_skill, toggle_skill, assign_skill_to_agent, unassign_skill_from_agent, delete_skill.
981
1021
  - PROVIDERS: View which LLM providers have API keys configured.
982
1022
  - TESTS: Create and run automated tests for agent workflows. Tests send a message to an agent, then an LLM judge evaluates the response against success criteria. Use list_tests, create_test, run_test, run_all_tests, get_test_results, delete_test.
983
1023
 
@@ -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
@@ -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) {