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.
- package/dist/ActivityPage.yv28a2vj.js +3 -0
- package/dist/ApiDocsPage.4ccwjjbk.js +4 -0
- package/dist/App.155wke5v.js +4 -0
- package/dist/App.2e19nvn4.js +13 -0
- package/dist/App.2ye1b5n0.js +4 -0
- package/dist/App.4da4ycbe.js +4 -0
- package/dist/App.b6wtzd1j.js +4 -0
- package/dist/App.fjrh28tf.js +4 -0
- package/dist/App.htc36cy8.js +4 -0
- package/dist/App.me6reaa6.js +4 -0
- package/dist/App.n5q6p960.js +4 -0
- package/dist/App.nft7h9jt.js +4 -0
- package/dist/App.np463xvy.js +4 -0
- package/dist/App.nps62kvt.js +4 -0
- package/dist/App.q8ws33cc.js +181 -0
- package/dist/App.tb0y0jmt.js +40 -0
- package/dist/ConnectionsPage.52evzrp7.js +3 -0
- package/dist/McpPage.bjqrp0n2.js +3 -0
- package/dist/SettingsPage.es76hnj2.js +3 -0
- package/dist/SkillsPage.06h8yf0h.js +3 -0
- package/dist/TasksPage.99df66mk.js +3 -0
- package/dist/TelemetryPage.bmdnxhq7.js +3 -0
- package/dist/TestsPage.denxrg8c.js +3 -0
- package/dist/index.html +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/auth/middleware.ts +2 -0
- package/src/db.ts +162 -11
- package/src/mcp-platform.ts +41 -1
- package/src/routes/api/agent-utils.ts +38 -2
- package/src/routes/api/agents.ts +65 -2
- package/src/routes/api/projects.ts +19 -2
- package/src/routes/api/system.ts +26 -12
- package/src/routes/api/triggers.ts +458 -0
- package/src/routes/api/webhooks.ts +171 -0
- package/src/routes/api.ts +4 -0
- package/src/routes/static.ts +12 -3
- package/src/server.ts +6 -4
- package/src/triggers/agentdojo.ts +248 -0
- package/src/triggers/composio.ts +264 -0
- package/src/triggers/index.ts +71 -0
- package/src/web/App.tsx +20 -12
- package/src/web/components/agents/AgentCard.tsx +14 -7
- package/src/web/components/agents/AgentPanel.tsx +105 -115
- package/src/web/components/common/Icons.tsx +8 -0
- package/src/web/components/common/index.ts +1 -0
- package/src/web/components/connections/ConnectionsPage.tsx +54 -0
- package/src/web/components/connections/IntegrationsTab.tsx +144 -0
- package/src/web/components/connections/OverviewTab.tsx +183 -0
- package/src/web/components/connections/TriggersTab.tsx +690 -0
- package/src/web/components/index.ts +1 -0
- package/src/web/components/layout/Sidebar.tsx +7 -1
- package/src/web/components/mcp/IntegrationsPanel.tsx +19 -3
- package/src/web/components/mcp/McpPage.tsx +9 -3
- package/src/web/components/settings/SettingsPage.tsx +96 -2
- package/src/web/components/tasks/TasksPage.tsx +2 -2
- package/src/web/components/tests/TestsPage.tsx +1 -2
- package/src/web/context/TelemetryContext.tsx +14 -1
- package/src/web/context/index.ts +1 -1
- package/src/web/hooks/useAgents.ts +15 -11
- package/src/web/types.ts +1 -1
- 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.
|
|
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
|
|
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
|
|
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);
|
package/src/mcp-platform.ts
CHANGED
|
@@ -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.
|
|
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 {
|
package/src/routes/api/agents.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
}
|
package/src/routes/api/system.ts
CHANGED
|
@@ -139,17 +139,21 @@ export async function handleSystemRoutes(
|
|
|
139
139
|
|
|
140
140
|
const allTasks: any[] = [];
|
|
141
141
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
195
|
-
|
|
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) {
|