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.
- package/dist/ActivityPage.9a1qg4bp.js +3 -0
- package/dist/ApiDocsPage.rfpf7ws1.js +4 -0
- package/dist/App.1nmg2h01.js +4 -0
- package/dist/App.5qw2dtxs.js +4 -0
- package/dist/App.6nc5acvk.js +4 -0
- package/dist/App.7vzbaz56.js +4 -0
- package/dist/App.8rfz30p1.js +4 -0
- package/dist/App.amwp54wf.js +4 -0
- package/dist/App.e4202qb4.js +267 -0
- package/dist/App.errxz2q4.js +4 -0
- package/dist/App.f8qsyhpr.js +4 -0
- package/dist/App.g8vq68n0.js +20 -0
- package/dist/App.kfyrnznw.js +13 -0
- package/dist/{App.mq6jqare.js → App.p02f4ret.js} +1 -1
- package/dist/App.p93mmyqw.js +4 -0
- package/dist/App.qmg33p02.js +4 -0
- package/dist/App.sdsc0258.js +4 -0
- package/dist/ConnectionsPage.7zqba1r0.js +3 -0
- package/dist/McpPage.kf2g327t.js +3 -0
- package/dist/SettingsPage.472c15ep.js +3 -0
- package/dist/SkillsPage.xdxnh68a.js +3 -0
- package/dist/TasksPage.7g0b8xwc.js +3 -0
- package/dist/TelemetryPage.pr7rbz4r.js +3 -0
- package/dist/TestsPage.zhc6rqjm.js +3 -0
- package/dist/apteva-kit.css +1 -1
- package/dist/index.html +1 -1
- package/dist/styles.css +1 -1
- package/package.json +9 -4
- package/src/auth/middleware.ts +2 -0
- package/src/channels/index.ts +40 -0
- package/src/channels/telegram.ts +306 -0
- package/src/db.ts +342 -11
- package/src/integrations/agentdojo.ts +1 -1
- package/src/mcp-handler.ts +31 -24
- package/src/mcp-platform.ts +41 -1
- package/src/providers.ts +22 -9
- package/src/routes/api/agent-utils.ts +38 -2
- package/src/routes/api/agents.ts +65 -2
- package/src/routes/api/channels.ts +182 -0
- package/src/routes/api/integrations.ts +13 -5
- package/src/routes/api/mcp.ts +27 -9
- package/src/routes/api/projects.ts +19 -2
- package/src/routes/api/system.ts +26 -12
- package/src/routes/api/telemetry.ts +30 -0
- package/src/routes/api/triggers.ts +478 -0
- package/src/routes/api/webhooks.ts +171 -0
- package/src/routes/api.ts +7 -1
- package/src/routes/static.ts +12 -3
- package/src/server.ts +43 -6
- package/src/triggers/agentdojo.ts +253 -0
- package/src/triggers/composio.ts +264 -0
- package/src/triggers/index.ts +71 -0
- package/src/tui/AgentList.tsx +145 -0
- package/src/tui/App.tsx +102 -0
- package/src/tui/Login.tsx +104 -0
- package/src/tui/api.ts +72 -0
- package/src/tui/index.tsx +7 -0
- package/src/web/App.tsx +18 -11
- package/src/web/components/agents/AgentCard.tsx +14 -7
- package/src/web/components/agents/AgentPanel.tsx +94 -137
- package/src/web/components/common/Icons.tsx +16 -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 +137 -0
- package/src/web/components/connections/TriggersTab.tsx +1169 -0
- package/src/web/components/index.ts +1 -0
- package/src/web/components/layout/Header.tsx +196 -4
- package/src/web/components/layout/Sidebar.tsx +7 -1
- package/src/web/components/mcp/IntegrationsPanel.tsx +19 -3
- package/src/web/components/settings/SettingsPage.tsx +364 -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.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 {
|
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
|
|
@@ -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
|
-
|
|
89
|
+
if (!user?.id) {
|
|
90
|
+
return json({ error: "Authentication required" }, 401);
|
|
91
|
+
}
|
|
87
92
|
|
|
88
93
|
try {
|
|
89
|
-
const accounts = await provider.listConnectedAccounts(apiKey,
|
|
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(`
|
|
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
|
-
|
|
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,
|
|
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);
|
package/src/routes/api/mcp.ts
CHANGED
|
@@ -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
|
-
|
|
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", "--
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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) {
|
|
@@ -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
|
}
|