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.
Files changed (76) hide show
  1. package/dist/ActivityPage.9a1qg4bp.js +3 -0
  2. package/dist/ApiDocsPage.rfpf7ws1.js +4 -0
  3. package/dist/App.1nmg2h01.js +4 -0
  4. package/dist/App.5qw2dtxs.js +4 -0
  5. package/dist/App.6nc5acvk.js +4 -0
  6. package/dist/{App.nps62kvt.js → App.7vzbaz56.js} +3 -3
  7. package/dist/App.8rfz30p1.js +4 -0
  8. package/dist/App.amwp54wf.js +4 -0
  9. package/dist/App.e4202qb4.js +267 -0
  10. package/dist/App.errxz2q4.js +4 -0
  11. package/dist/App.f8qsyhpr.js +4 -0
  12. package/dist/App.g8vq68n0.js +20 -0
  13. package/dist/App.kfyrnznw.js +13 -0
  14. package/dist/{App.mq6jqare.js → App.p02f4ret.js} +1 -1
  15. package/dist/{App.np463xvy.js → App.p93mmyqw.js} +3 -3
  16. package/dist/App.qmg33p02.js +4 -0
  17. package/dist/{App.nft7h9jt.js → App.sdsc0258.js} +3 -3
  18. package/dist/ConnectionsPage.7zqba1r0.js +3 -0
  19. package/dist/McpPage.kf2g327t.js +3 -0
  20. package/dist/SettingsPage.472c15ep.js +3 -0
  21. package/dist/SkillsPage.xdxnh68a.js +3 -0
  22. package/dist/TasksPage.7g0b8xwc.js +3 -0
  23. package/dist/TelemetryPage.pr7rbz4r.js +3 -0
  24. package/dist/TestsPage.zhc6rqjm.js +3 -0
  25. package/dist/apteva-kit.css +1 -1
  26. package/dist/index.html +1 -1
  27. package/dist/styles.css +1 -1
  28. package/package.json +9 -4
  29. package/src/channels/index.ts +40 -0
  30. package/src/channels/telegram.ts +306 -0
  31. package/src/db.ts +180 -0
  32. package/src/integrations/agentdojo.ts +1 -1
  33. package/src/mcp-handler.ts +31 -24
  34. package/src/providers.ts +22 -9
  35. package/src/routes/api/channels.ts +182 -0
  36. package/src/routes/api/integrations.ts +13 -5
  37. package/src/routes/api/mcp.ts +27 -9
  38. package/src/routes/api/telemetry.ts +30 -0
  39. package/src/routes/api/triggers.ts +22 -2
  40. package/src/routes/api.ts +3 -1
  41. package/src/server.ts +39 -4
  42. package/src/triggers/agentdojo.ts +23 -18
  43. package/src/tui/AgentList.tsx +145 -0
  44. package/src/tui/App.tsx +102 -0
  45. package/src/tui/Login.tsx +104 -0
  46. package/src/tui/api.ts +72 -0
  47. package/src/tui/index.tsx +7 -0
  48. package/src/web/App.tsx +1 -1
  49. package/src/web/components/agents/AgentPanel.tsx +4 -37
  50. package/src/web/components/common/Icons.tsx +8 -0
  51. package/src/web/components/connections/OverviewTab.tsx +22 -68
  52. package/src/web/components/connections/TriggersTab.tsx +549 -70
  53. package/src/web/components/layout/Header.tsx +196 -4
  54. package/src/web/components/settings/SettingsPage.tsx +269 -1
  55. package/src/web/context/TelemetryContext.tsx +14 -1
  56. package/src/web/context/index.ts +1 -1
  57. package/dist/ActivityPage.yv28a2vj.js +0 -3
  58. package/dist/ApiDocsPage.4ccwjjbk.js +0 -4
  59. package/dist/App.155wke5v.js +0 -4
  60. package/dist/App.2e19nvn4.js +0 -13
  61. package/dist/App.2ye1b5n0.js +0 -4
  62. package/dist/App.4da4ycbe.js +0 -4
  63. package/dist/App.b6wtzd1j.js +0 -4
  64. package/dist/App.fjrh28tf.js +0 -4
  65. package/dist/App.htc36cy8.js +0 -4
  66. package/dist/App.me6reaa6.js +0 -4
  67. package/dist/App.n5q6p960.js +0 -4
  68. package/dist/App.q8ws33cc.js +0 -181
  69. package/dist/App.tb0y0jmt.js +0 -40
  70. package/dist/ConnectionsPage.52evzrp7.js +0 -3
  71. package/dist/McpPage.bjqrp0n2.js +0 -3
  72. package/dist/SettingsPage.es76hnj2.js +0 -3
  73. package/dist/SkillsPage.06h8yf0h.js +0 -3
  74. package/dist/TasksPage.99df66mk.js +0 -3
  75. package/dist/TelemetryPage.bmdnxhq7.js +0 -3
  76. 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
- const userId = user?.id || "default";
89
+ if (!user?.id) {
90
+ return json({ error: "Authentication required" }, 401);
91
+ }
87
92
 
88
93
  try {
89
- const accounts = await provider.listConnectedAccounts(apiKey, userId);
94
+ const accounts = await provider.listConnectedAccounts(apiKey, user.id);
95
+ console.log(`[integrations/connected] Got ${accounts.length} accounts from ${providerId}`);
90
96
  return json({ accounts });
91
97
  } catch (e) {
92
- console.error(`Failed to list connected accounts from ${providerId}:`, e);
98
+ console.error(`[integrations/connected] Failed from ${providerId}:`, e);
93
99
  return json({ error: "Failed to fetch connected accounts" }, 500);
94
100
  }
95
101
  }
@@ -116,12 +122,14 @@ export async function handleIntegrationRoutes(
116
122
  }
117
123
 
118
124
  // Use Apteva user ID as the entity ID
119
- const userId = user?.id || "default";
125
+ if (!user?.id) {
126
+ return json({ error: "Authentication required" }, 401);
127
+ }
120
128
 
121
129
  // Default redirect URL back to our integrations page
122
130
  const callbackUrl = redirectUrl || `http://localhost:${process.env.PORT || 4280}/mcp?tab=hosted&connected=${appSlug}`;
123
131
 
124
- const result = await provider.initiateConnection(apiKey, userId, appSlug, callbackUrl, credentials);
132
+ const result = await provider.initiateConnection(apiKey, user.id, appSlug, callbackUrl, credentials);
125
133
  return json(result);
126
134
  } catch (e) {
127
135
  console.error(`Failed to initiate connection for ${providerId}:`, e);
@@ -221,6 +221,21 @@ export async function handleMcpRoutes(
221
221
  });
222
222
  };
223
223
 
224
+ // Validate package/command names to prevent injection
225
+ const SAFE_PACKAGE_RE = /^(@[a-z0-9._-]+\/)?[a-z0-9._-]+(@[a-z0-9^~>=<.*-]+)?(\[[\w,]+\])?$/i;
226
+
227
+ // Parse args safely — split on whitespace but respect quoted strings
228
+ const parseArgs = (raw: string, env: Record<string, string>): string[] => {
229
+ const substituted = substituteEnvVars(raw, env);
230
+ const args: string[] = [];
231
+ const re = /"([^"]*?)"|'([^']*?)'|(\S+)/g;
232
+ let m;
233
+ while ((m = re.exec(substituted)) !== null) {
234
+ args.push(m[1] ?? m[2] ?? m[3]);
235
+ }
236
+ return args;
237
+ };
238
+
224
239
  let cmd: string[];
225
240
  const serverEnv = server.env || {};
226
241
 
@@ -228,17 +243,19 @@ export async function handleMcpRoutes(
228
243
  // Custom command - substitute env vars in args
229
244
  cmd = server.command.split(" ");
230
245
  if (server.args) {
231
- const substitutedArgs = substituteEnvVars(server.args, serverEnv);
232
- cmd.push(...substitutedArgs.split(" "));
246
+ cmd.push(...parseArgs(server.args, serverEnv));
233
247
  }
234
248
  } else if (server.type === "pip" && server.package) {
235
249
  // Python pip package - install first, then run module
236
250
  const pipPackage = server.package;
251
+ if (!SAFE_PACKAGE_RE.test(pipPackage)) {
252
+ return json({ error: "Invalid pip package name" }, 400);
253
+ }
237
254
  const pipModule = server.pip_module || server.package.split("[")[0]; // Default: package name without extras
238
255
 
239
256
  console.log(`Installing pip package: ${pipPackage}...`);
240
257
  const installResult = spawn({
241
- cmd: ["pip", "install", "--quiet", "--break-system-packages", pipPackage],
258
+ cmd: ["pip", "install", "--quiet", "--no-scripts", pipPackage],
242
259
  env: { ...process.env as Record<string, string>, ...serverEnv },
243
260
  stdout: "pipe",
244
261
  stderr: "pipe",
@@ -254,15 +271,16 @@ export async function handleMcpRoutes(
254
271
  // Now run the module
255
272
  cmd = ["python", "-m", pipModule];
256
273
  if (server.args) {
257
- const substitutedArgs = substituteEnvVars(server.args, serverEnv);
258
- cmd.push(...substitutedArgs.split(" "));
274
+ cmd.push(...parseArgs(server.args, serverEnv));
259
275
  }
260
276
  } else if (server.package) {
261
- // npm package - use npx
262
- cmd = ["npx", "-y", server.package];
277
+ // npm package - use npx with --ignore-scripts to prevent supply chain attacks
278
+ if (!SAFE_PACKAGE_RE.test(server.package)) {
279
+ return json({ error: "Invalid npm package name" }, 400);
280
+ }
281
+ cmd = ["npx", "--ignore-scripts", "-y", server.package];
263
282
  if (server.args) {
264
- const substitutedArgs = substituteEnvVars(server.args, serverEnv);
265
- cmd.push(...substitutedArgs.split(" "));
283
+ cmd.push(...parseArgs(server.args, serverEnv));
266
284
  }
267
285
  } else {
268
286
  return json({ error: "No command or package specified" }, 400);
@@ -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.getDecrypted(id);
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 and MCP servers that were running before restart (for auto-restart)
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 and MCP servers that were running before restart
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 /mcp/subscribe, GET /mcp/subscriptions, GET /mcp/triggers
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}/mcp/triggers?${new URLSearchParams({ toolkit_name: slug, is_active: "true", limit: "200" })}`,
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}/mcp/triggers?${params}`, {
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}/mcp/triggers/${encodeURIComponent(slug)}`,
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: (config?.title as string) || `Trigger: ${slug}`,
98
+ title: (title as string) || `Trigger: ${slug}`,
96
99
  };
97
100
 
98
- if (config?.events) {
99
- body.events = config.events;
100
- }
101
- if (config?.server) {
102
- body.server = config.server;
103
- }
104
- if (config?.prompt) {
105
- body.prompt = config.prompt;
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}/mcp/subscribe`, {
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}/mcp/subscriptions?status=active`, {
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}/mcp/subscription/update`, {
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}/mcp/subscription/update`, {
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}/mcp/unsubscribe`, {
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) }),