apteva 0.4.18 → 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.nps62kvt.js → App.7vzbaz56.js} +3 -3
- 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.np463xvy.js → App.p93mmyqw.js} +3 -3
- package/dist/App.qmg33p02.js +4 -0
- package/dist/{App.nft7h9jt.js → App.sdsc0258.js} +3 -3
- 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/channels/index.ts +40 -0
- package/src/channels/telegram.ts +306 -0
- package/src/db.ts +180 -0
- package/src/integrations/agentdojo.ts +1 -1
- package/src/mcp-handler.ts +31 -24
- package/src/providers.ts +22 -9
- 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/telemetry.ts +30 -0
- package/src/routes/api/triggers.ts +22 -2
- package/src/routes/api.ts +3 -1
- package/src/server.ts +39 -4
- package/src/triggers/agentdojo.ts +23 -18
- 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 +1 -1
- package/src/web/components/agents/AgentPanel.tsx +4 -37
- package/src/web/components/common/Icons.tsx +8 -0
- package/src/web/components/connections/OverviewTab.tsx +22 -68
- package/src/web/components/connections/TriggersTab.tsx +549 -70
- package/src/web/components/layout/Header.tsx +196 -4
- package/src/web/components/settings/SettingsPage.tsx +269 -1
- package/src/web/context/TelemetryContext.tsx +14 -1
- package/src/web/context/index.ts +1 -1
- package/dist/ActivityPage.yv28a2vj.js +0 -3
- package/dist/ApiDocsPage.4ccwjjbk.js +0 -4
- package/dist/App.155wke5v.js +0 -4
- package/dist/App.2e19nvn4.js +0 -13
- package/dist/App.2ye1b5n0.js +0 -4
- package/dist/App.4da4ycbe.js +0 -4
- package/dist/App.b6wtzd1j.js +0 -4
- package/dist/App.fjrh28tf.js +0 -4
- package/dist/App.htc36cy8.js +0 -4
- package/dist/App.me6reaa6.js +0 -4
- package/dist/App.n5q6p960.js +0 -4
- package/dist/App.q8ws33cc.js +0 -181
- package/dist/App.tb0y0jmt.js +0 -40
- package/dist/ConnectionsPage.52evzrp7.js +0 -3
- package/dist/McpPage.bjqrp0n2.js +0 -3
- package/dist/SettingsPage.es76hnj2.js +0 -3
- package/dist/SkillsPage.06h8yf0h.js +0 -3
- package/dist/TasksPage.99df66mk.js +0 -3
- package/dist/TelemetryPage.bmdnxhq7.js +0 -3
- package/dist/TestsPage.denxrg8c.js +0 -3
|
@@ -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);
|
|
@@ -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
|
}
|
|
@@ -45,10 +45,12 @@ export async function handleTriggerRoutes(
|
|
|
45
45
|
|
|
46
46
|
// GET /api/triggers/providers - List available trigger providers
|
|
47
47
|
if (path === "/api/triggers/providers" && method === "GET") {
|
|
48
|
+
const url = new URL(req.url);
|
|
49
|
+
const projectId = url.searchParams.get("project_id") || null;
|
|
48
50
|
const providerIds = getTriggerProviderIds();
|
|
49
51
|
const providers = providerIds.map(id => {
|
|
50
52
|
const provider = getTriggerProvider(id);
|
|
51
|
-
const hasKey = !!ProviderKeys.
|
|
53
|
+
const hasKey = !!ProviderKeys.getDecryptedForProject(id, projectId);
|
|
52
54
|
return { id, name: provider?.name || id, connected: hasKey };
|
|
53
55
|
});
|
|
54
56
|
return json({ providers });
|
|
@@ -65,20 +67,24 @@ export async function handleTriggerRoutes(
|
|
|
65
67
|
const projectId = url.searchParams.get("project_id") || null;
|
|
66
68
|
|
|
67
69
|
const provider = getTriggerProvider(providerId);
|
|
70
|
+
console.log(`[triggers/types] provider=${providerId}, toolkitSlugs=${toolkitSlugsParam}, projectId=${projectId}, providerFound=${!!provider}`);
|
|
68
71
|
if (!provider) {
|
|
69
72
|
return json({ error: `Unknown trigger provider: ${providerId}` }, 404);
|
|
70
73
|
}
|
|
71
74
|
|
|
72
75
|
const apiKey = ProviderKeys.getDecryptedForProject(providerId, projectId);
|
|
76
|
+
console.log(`[triggers/types] apiKey found: ${!!apiKey}, length: ${apiKey?.length || 0}, first4: ${apiKey?.substring(0, 4) || 'none'}`);
|
|
73
77
|
if (!apiKey) {
|
|
78
|
+
console.log(`[triggers/types] NO API KEY for ${providerId} (projectId=${projectId})`);
|
|
74
79
|
return json({ error: `${provider.name} API key not configured`, types: [] }, 200);
|
|
75
80
|
}
|
|
76
81
|
|
|
77
82
|
try {
|
|
78
83
|
const types = await provider.listTriggerTypes(apiKey, toolkitSlugs);
|
|
84
|
+
console.log(`[triggers/types] Got ${types.length} types from ${providerId}`);
|
|
79
85
|
return json({ types });
|
|
80
86
|
} catch (e) {
|
|
81
|
-
console.error(`Failed to list trigger types from ${providerId}:`, e);
|
|
87
|
+
console.error(`[triggers/types] Failed to list trigger types from ${providerId}:`, e);
|
|
82
88
|
return json({ error: "Failed to fetch trigger types" }, 500);
|
|
83
89
|
}
|
|
84
90
|
}
|
|
@@ -165,6 +171,20 @@ export async function handleTriggerRoutes(
|
|
|
165
171
|
}
|
|
166
172
|
|
|
167
173
|
const result = await provider.createTrigger(apiKey, slug, connectedAccountId, config);
|
|
174
|
+
|
|
175
|
+
// Also create a local subscription for webhook routing
|
|
176
|
+
if (config?.agent_id && result.triggerId) {
|
|
177
|
+
const triggerSlug = config.server ? `${config.server}:${slug.replace(/^.*?-/, '')}` : slug;
|
|
178
|
+
SubscriptionDB.create({
|
|
179
|
+
trigger_slug: slug,
|
|
180
|
+
trigger_instance_id: result.triggerId,
|
|
181
|
+
agent_id: config.agent_id,
|
|
182
|
+
enabled: true,
|
|
183
|
+
project_id: projectId,
|
|
184
|
+
});
|
|
185
|
+
console.log(`[triggers] Created local subscription: ${slug} (instance=${result.triggerId}) → agent ${config.agent_id}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
168
188
|
return json(result, 201);
|
|
169
189
|
} catch (e: any) {
|
|
170
190
|
console.error(`Failed to create trigger:`, e);
|
package/src/routes/api.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { handleMetaAgentRoutes } from "./api/meta-agent";
|
|
|
14
14
|
import { handleTelemetryRoutes } from "./api/telemetry";
|
|
15
15
|
import { handleTestRoutes } from "./api/tests";
|
|
16
16
|
import { handleApiKeyRoutes } from "./api/api-keys";
|
|
17
|
+
import { handleChannelRoutes } from "./api/channels";
|
|
17
18
|
import { handlePlatformMcpRequest } from "../mcp-platform";
|
|
18
19
|
|
|
19
20
|
// Re-export for backward compatibility (server.ts dynamic import)
|
|
@@ -41,8 +42,9 @@ export async function handleApiRequest(
|
|
|
41
42
|
(await handleAgentRoutes(req, path, method, authContext)) ??
|
|
42
43
|
(await handleMcpRoutes(req, path, method)) ??
|
|
43
44
|
(await handleSkillRoutes(req, path, method)) ??
|
|
44
|
-
(await handleIntegrationRoutes(req, path, method)) ??
|
|
45
|
+
(await handleIntegrationRoutes(req, path, method, authContext)) ??
|
|
45
46
|
(await handleTriggerRoutes(req, path, method, authContext)) ??
|
|
47
|
+
(await handleChannelRoutes(req, path, method)) ??
|
|
46
48
|
(await handleMetaAgentRoutes(req, path, method)) ??
|
|
47
49
|
(await handleTelemetryRoutes(req, path, method)) ??
|
|
48
50
|
(await handleTestRoutes(req, path, method)) ??
|
package/src/server.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { serveStatic } from "./routes/static";
|
|
|
5
5
|
import { join } from "path";
|
|
6
6
|
import { homedir } from "os";
|
|
7
7
|
import { mkdirSync, existsSync } from "fs";
|
|
8
|
-
import { initDatabase, AgentDB, ProviderKeysDB, McpServerDB, type McpServer, type Agent } from "./db";
|
|
8
|
+
import { initDatabase, AgentDB, ProviderKeysDB, McpServerDB, ChannelDB, type McpServer, type Agent } from "./db";
|
|
9
9
|
import { authMiddleware, type AuthContext } from "./auth/middleware";
|
|
10
10
|
import { startMcpProcess } from "./mcp-client";
|
|
11
11
|
import {
|
|
@@ -106,13 +106,18 @@ initDatabase(DATA_DIR);
|
|
|
106
106
|
// Initialize version tracking
|
|
107
107
|
initVersionTracking(DATA_DIR);
|
|
108
108
|
|
|
109
|
-
// Get agents
|
|
109
|
+
// Get agents, MCP servers, and channels that were running before restart (for auto-restart)
|
|
110
110
|
const agentsToRestart = AgentDB.findRunning();
|
|
111
111
|
const mcpServersToRestart = McpServerDB.findRunning();
|
|
112
|
+
const channelsToRestart = ChannelDB.findRunning();
|
|
112
113
|
|
|
113
114
|
// Reset all agents and MCP servers to stopped on startup (processes don't survive restart)
|
|
114
115
|
AgentDB.resetAllStatus();
|
|
115
116
|
McpServerDB.resetAllStatus();
|
|
117
|
+
// Reset channels too (bot polling doesn't survive restart)
|
|
118
|
+
for (const ch of channelsToRestart) {
|
|
119
|
+
ChannelDB.setStatus(ch.id, "stopped");
|
|
120
|
+
}
|
|
116
121
|
|
|
117
122
|
// Clean up orphaned processes on agent ports (targeted cleanup based on DB)
|
|
118
123
|
async function cleanupOrphanedProcesses(): Promise<void> {
|
|
@@ -194,15 +199,26 @@ async function shutdownAllAgents() {
|
|
|
194
199
|
// Handle process termination signals
|
|
195
200
|
let shuttingDown = false;
|
|
196
201
|
export function isShuttingDown(): boolean { return shuttingDown; }
|
|
202
|
+
async function shutdownAllChannels() {
|
|
203
|
+
try {
|
|
204
|
+
const { stopAllChannels } = await import("./channels");
|
|
205
|
+
await stopAllChannels();
|
|
206
|
+
} catch {
|
|
207
|
+
// Ignore import/stop errors during shutdown
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
197
211
|
process.on("SIGINT", async () => {
|
|
198
212
|
if (shuttingDown) return;
|
|
199
213
|
shuttingDown = true;
|
|
214
|
+
await shutdownAllChannels();
|
|
200
215
|
await shutdownAllAgents();
|
|
201
216
|
process.exit(0);
|
|
202
217
|
});
|
|
203
218
|
process.on("SIGTERM", async () => {
|
|
204
219
|
if (shuttingDown) return;
|
|
205
220
|
shuttingDown = true;
|
|
221
|
+
await shutdownAllChannels();
|
|
206
222
|
await shutdownAllAgents();
|
|
207
223
|
process.exit(0);
|
|
208
224
|
});
|
|
@@ -416,8 +432,8 @@ console.log(`
|
|
|
416
432
|
${c.darkGray}Click link or Cmd/Ctrl+C to copy${c.reset}
|
|
417
433
|
`);
|
|
418
434
|
|
|
419
|
-
// Auto-restart agents
|
|
420
|
-
const hasRestarts = agentsToRestart.length > 0 || mcpServersToRestart.length > 0;
|
|
435
|
+
// Auto-restart agents, MCP servers, and channels that were running before restart
|
|
436
|
+
const hasRestarts = agentsToRestart.length > 0 || mcpServersToRestart.length > 0 || channelsToRestart.length > 0;
|
|
421
437
|
|
|
422
438
|
if (hasRestarts) {
|
|
423
439
|
// Restart in background to not block startup
|
|
@@ -492,6 +508,25 @@ if (hasRestarts) {
|
|
|
492
508
|
}
|
|
493
509
|
}
|
|
494
510
|
}
|
|
511
|
+
|
|
512
|
+
// Restart channels (after agents, since channels depend on running agents)
|
|
513
|
+
if (channelsToRestart.length > 0) {
|
|
514
|
+
const { startChannel } = await import("./channels");
|
|
515
|
+
console.log(` ${c.darkGray}Channels${c.reset} ${c.gray}Restarting ${channelsToRestart.length} channel(s)...${c.reset}`);
|
|
516
|
+
|
|
517
|
+
for (const channel of channelsToRestart) {
|
|
518
|
+
try {
|
|
519
|
+
const result = await startChannel(channel.id);
|
|
520
|
+
if (result.success) {
|
|
521
|
+
console.log(` ${c.gray} ✓ ${channel.name} (${channel.type})${c.reset}`);
|
|
522
|
+
} else {
|
|
523
|
+
console.log(` ${c.gray} ✗ ${channel.name}: ${result.error}${c.reset}`);
|
|
524
|
+
}
|
|
525
|
+
} catch (err) {
|
|
526
|
+
console.log(` ${c.gray} ✗ ${channel.name}: ${err}${c.reset}`);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
495
530
|
})();
|
|
496
531
|
}
|
|
497
532
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// AgentDojo Trigger Provider
|
|
2
2
|
// Uses our MCP API's subscription and trigger system
|
|
3
|
-
// Docs: POST /
|
|
3
|
+
// Docs: POST /subscribe, GET /subscriptions, GET /triggers
|
|
4
4
|
|
|
5
5
|
import { createHmac, timingSafeEqual } from "crypto";
|
|
6
6
|
import type { TriggerProvider, TriggerType, TriggerInstance } from "./index";
|
|
@@ -22,7 +22,7 @@ export const AgentDojoTriggerProvider: TriggerProvider = {
|
|
|
22
22
|
const allItems: any[] = [];
|
|
23
23
|
for (const slug of toolkitSlugs) {
|
|
24
24
|
const res = await fetch(
|
|
25
|
-
`${AGENTDOJO_API_BASE}/
|
|
25
|
+
`${AGENTDOJO_API_BASE}/triggers?${new URLSearchParams({ toolkit_name: slug, is_active: "true", limit: "200" })}`,
|
|
26
26
|
{ headers: headers(apiKey) },
|
|
27
27
|
);
|
|
28
28
|
if (res.ok) {
|
|
@@ -34,7 +34,7 @@ export const AgentDojoTriggerProvider: TriggerProvider = {
|
|
|
34
34
|
return mapTriggerTypes(allItems);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
const res = await fetch(`${AGENTDOJO_API_BASE}/
|
|
37
|
+
const res = await fetch(`${AGENTDOJO_API_BASE}/triggers?${params}`, {
|
|
38
38
|
headers: headers(apiKey),
|
|
39
39
|
});
|
|
40
40
|
|
|
@@ -50,7 +50,7 @@ export const AgentDojoTriggerProvider: TriggerProvider = {
|
|
|
50
50
|
|
|
51
51
|
async getTriggerType(apiKey: string, slug: string): Promise<TriggerType | null> {
|
|
52
52
|
const res = await fetch(
|
|
53
|
-
`${AGENTDOJO_API_BASE}/
|
|
53
|
+
`${AGENTDOJO_API_BASE}/triggers/${encodeURIComponent(slug)}`,
|
|
54
54
|
{ headers: headers(apiKey) },
|
|
55
55
|
);
|
|
56
56
|
|
|
@@ -88,24 +88,29 @@ export const AgentDojoTriggerProvider: TriggerProvider = {
|
|
|
88
88
|
throw new Error("callback_url is required in config for AgentDojo triggers");
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
// Separate known top-level fields from extra config (e.g. owner, repo for GitHub)
|
|
92
|
+
const { callback_url, title, events, server, prompt, agent_id, ...extraConfig } = config || {} as Record<string, unknown>;
|
|
93
|
+
|
|
91
94
|
const body: Record<string, unknown> = {
|
|
92
95
|
trigger_type_slug: slug,
|
|
93
96
|
credential_id: connectedAccountId,
|
|
94
97
|
callback_url: callbackUrl,
|
|
95
|
-
title: (
|
|
98
|
+
title: (title as string) || `Trigger: ${slug}`,
|
|
96
99
|
};
|
|
97
100
|
|
|
98
|
-
if (
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
101
|
+
if (events) body.events = events;
|
|
102
|
+
if (server) body.server = server;
|
|
103
|
+
if (prompt) body.prompt = prompt;
|
|
104
|
+
if (agent_id) body.agent_id = agent_id;
|
|
105
|
+
|
|
106
|
+
// Pass extra config fields (owner, repo, etc.) as the config object
|
|
107
|
+
// mcp-subscribe spreads this into the webhook register payload
|
|
108
|
+
if (Object.keys(extraConfig).length > 0) {
|
|
109
|
+
body.config = extraConfig;
|
|
110
|
+
console.log("AgentDojo createTrigger: extra config:", JSON.stringify(extraConfig));
|
|
106
111
|
}
|
|
107
112
|
|
|
108
|
-
const res = await fetch(`${AGENTDOJO_API_BASE}/
|
|
113
|
+
const res = await fetch(`${AGENTDOJO_API_BASE}/subscribe`, {
|
|
109
114
|
method: "POST",
|
|
110
115
|
headers: headers(apiKey),
|
|
111
116
|
body: JSON.stringify(body),
|
|
@@ -122,7 +127,7 @@ export const AgentDojoTriggerProvider: TriggerProvider = {
|
|
|
122
127
|
},
|
|
123
128
|
|
|
124
129
|
async listTriggers(apiKey: string): Promise<TriggerInstance[]> {
|
|
125
|
-
const res = await fetch(`${AGENTDOJO_API_BASE}/
|
|
130
|
+
const res = await fetch(`${AGENTDOJO_API_BASE}/subscriptions?status=active`, {
|
|
126
131
|
headers: headers(apiKey),
|
|
127
132
|
});
|
|
128
133
|
|
|
@@ -151,7 +156,7 @@ export const AgentDojoTriggerProvider: TriggerProvider = {
|
|
|
151
156
|
},
|
|
152
157
|
|
|
153
158
|
async enableTrigger(apiKey: string, triggerId: string): Promise<boolean> {
|
|
154
|
-
const res = await fetch(`${AGENTDOJO_API_BASE}/
|
|
159
|
+
const res = await fetch(`${AGENTDOJO_API_BASE}/subscription/update`, {
|
|
155
160
|
method: "POST",
|
|
156
161
|
headers: headers(apiKey),
|
|
157
162
|
body: JSON.stringify({ subscription_id: parseInt(triggerId), status: "active" }),
|
|
@@ -160,7 +165,7 @@ export const AgentDojoTriggerProvider: TriggerProvider = {
|
|
|
160
165
|
},
|
|
161
166
|
|
|
162
167
|
async disableTrigger(apiKey: string, triggerId: string): Promise<boolean> {
|
|
163
|
-
const res = await fetch(`${AGENTDOJO_API_BASE}/
|
|
168
|
+
const res = await fetch(`${AGENTDOJO_API_BASE}/subscription/update`, {
|
|
164
169
|
method: "POST",
|
|
165
170
|
headers: headers(apiKey),
|
|
166
171
|
body: JSON.stringify({ subscription_id: parseInt(triggerId), status: "disabled" }),
|
|
@@ -169,7 +174,7 @@ export const AgentDojoTriggerProvider: TriggerProvider = {
|
|
|
169
174
|
},
|
|
170
175
|
|
|
171
176
|
async deleteTrigger(apiKey: string, triggerId: string): Promise<boolean> {
|
|
172
|
-
const res = await fetch(`${AGENTDOJO_API_BASE}/
|
|
177
|
+
const res = await fetch(`${AGENTDOJO_API_BASE}/unsubscribe`, {
|
|
173
178
|
method: "POST",
|
|
174
179
|
headers: headers(apiKey),
|
|
175
180
|
body: JSON.stringify({ subscription_id: parseInt(triggerId) }),
|