apteva 0.4.18 → 0.4.20

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 (84) hide show
  1. package/dist/ActivityPage.h769ek3a.js +3 -0
  2. package/dist/ApiDocsPage.kf6bbwkk.js +4 -0
  3. package/dist/{App.nps62kvt.js → App.039re6cf.js} +3 -3
  4. package/dist/App.2jmkqm8c.js +4 -0
  5. package/dist/{App.np463xvy.js → App.2yy66bnp.js} +3 -3
  6. package/dist/App.3515wsb4.js +4 -0
  7. package/dist/App.7v1w3ys9.js +4 -0
  8. package/dist/{App.nft7h9jt.js → App.c90t3dxg.js} +3 -3
  9. package/dist/App.edwahsvz.js +4 -0
  10. package/dist/App.jfx3der4.js +4 -0
  11. package/dist/App.n4jb3c22.js +13 -0
  12. package/dist/{App.mq6jqare.js → App.p02f4ret.js} +1 -1
  13. package/dist/App.q3bpx15d.js +20 -0
  14. package/dist/App.r0a2nmqs.js +267 -0
  15. package/dist/App.s2yrcz15.js +4 -0
  16. package/dist/App.s5j82a5j.js +4 -0
  17. package/dist/App.tg1b94tx.js +4 -0
  18. package/dist/ConnectionsPage.a67fjgbf.js +3 -0
  19. package/dist/McpPage.d4p3xvtk.js +3 -0
  20. package/dist/SettingsPage.46sqpe39.js +3 -0
  21. package/dist/SkillsPage.j9hkqm99.js +3 -0
  22. package/dist/TasksPage.6pvkb7s7.js +3 -0
  23. package/dist/TelemetryPage.5zq9msb5.js +3 -0
  24. package/dist/TestsPage.24432yqt.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/mcp-platform.ts +353 -2
  35. package/src/providers.ts +22 -9
  36. package/src/routes/api/agents.ts +15 -2
  37. package/src/routes/api/channels.ts +182 -0
  38. package/src/routes/api/integrations.ts +13 -5
  39. package/src/routes/api/mcp.ts +27 -9
  40. package/src/routes/api/system.ts +12 -1
  41. package/src/routes/api/telemetry.ts +30 -0
  42. package/src/routes/api/triggers.ts +22 -2
  43. package/src/routes/api.ts +3 -1
  44. package/src/routes/auth.ts +11 -2
  45. package/src/server.ts +39 -4
  46. package/src/triggers/agentdojo.ts +23 -18
  47. package/src/tui/AgentList.tsx +145 -0
  48. package/src/tui/App.tsx +102 -0
  49. package/src/tui/Login.tsx +104 -0
  50. package/src/tui/api.ts +72 -0
  51. package/src/tui/index.tsx +7 -0
  52. package/src/web/App.tsx +2 -2
  53. package/src/web/components/agents/AgentPanel.tsx +4 -37
  54. package/src/web/components/common/Icons.tsx +8 -0
  55. package/src/web/components/connections/OverviewTab.tsx +22 -68
  56. package/src/web/components/connections/TriggersTab.tsx +549 -70
  57. package/src/web/components/dashboard/Dashboard.tsx +5 -4
  58. package/src/web/components/layout/Header.tsx +196 -4
  59. package/src/web/components/settings/SettingsPage.tsx +269 -1
  60. package/src/web/context/AuthContext.tsx +18 -11
  61. package/src/web/context/TelemetryContext.tsx +14 -1
  62. package/src/web/context/index.ts +1 -1
  63. package/src/web/hooks/useAgents.ts +7 -3
  64. package/src/web/hooks/useOnboarding.ts +9 -30
  65. package/dist/ActivityPage.yv28a2vj.js +0 -3
  66. package/dist/ApiDocsPage.4ccwjjbk.js +0 -4
  67. package/dist/App.155wke5v.js +0 -4
  68. package/dist/App.2e19nvn4.js +0 -13
  69. package/dist/App.2ye1b5n0.js +0 -4
  70. package/dist/App.4da4ycbe.js +0 -4
  71. package/dist/App.b6wtzd1j.js +0 -4
  72. package/dist/App.fjrh28tf.js +0 -4
  73. package/dist/App.htc36cy8.js +0 -4
  74. package/dist/App.me6reaa6.js +0 -4
  75. package/dist/App.n5q6p960.js +0 -4
  76. package/dist/App.q8ws33cc.js +0 -181
  77. package/dist/App.tb0y0jmt.js +0 -40
  78. package/dist/ConnectionsPage.52evzrp7.js +0 -3
  79. package/dist/McpPage.bjqrp0n2.js +0 -3
  80. package/dist/SettingsPage.es76hnj2.js +0 -3
  81. package/dist/SkillsPage.06h8yf0h.js +0 -3
  82. package/dist/TasksPage.99df66mk.js +0 -3
  83. package/dist/TelemetryPage.bmdnxhq7.js +0 -3
  84. package/dist/TestsPage.denxrg8c.js +0 -3
@@ -1,12 +1,24 @@
1
1
  // Built-in MCP server that exposes the Apteva platform API as MCP tools
2
2
  // This allows the meta agent (Apteva Assistant) to control the platform
3
3
 
4
- import { AgentDB, ProjectDB, McpServerDB, SkillDB, TelemetryDB, generateId } from "./db";
4
+ import { AgentDB, ProjectDB, McpServerDB, SkillDB, TelemetryDB, SubscriptionDB, SettingsDB, generateId } from "./db";
5
5
  import { TestCaseDB, TestRunDB } from "./db-tests";
6
6
  import { runTest, runAll } from "./test-runner";
7
- import { getProvidersWithStatus, PROVIDERS } from "./providers";
7
+ import { getProvidersWithStatus, PROVIDERS, ProviderKeys } from "./providers";
8
8
  import { startAgentProcess, setAgentStatus, toApiAgent, META_AGENT_ID, agentFetch } from "./routes/api/agent-utils";
9
9
  import { agentProcesses } from "./server";
10
+ import { getTriggerProvider, getTriggerProviderIds, registerTriggerProvider } from "./triggers";
11
+ import { ComposioTriggerProvider } from "./triggers/composio";
12
+ import { AgentDojoTriggerProvider } from "./triggers/agentdojo";
13
+ import { getProvider, registerProvider } from "./integrations";
14
+ import { ComposioProvider } from "./integrations/composio";
15
+ import { AgentDojoProvider } from "./integrations/agentdojo";
16
+
17
+ // Register trigger + integration providers on module load
18
+ registerTriggerProvider(ComposioTriggerProvider);
19
+ registerTriggerProvider(AgentDojoTriggerProvider);
20
+ registerProvider(ComposioProvider);
21
+ registerProvider(AgentDojoProvider);
10
22
 
11
23
  // MCP Protocol version
12
24
  const PROTOCOL_VERSION = "2024-11-05";
@@ -447,6 +459,128 @@ After creating, assign to agents with assign_mcp_server_to_agent. HTTP servers w
447
459
  required: ["test_id"],
448
460
  },
449
461
  },
462
+ // Subscription & Trigger management
463
+ {
464
+ name: "list_trigger_providers",
465
+ description: "List available trigger/webhook providers (e.g. composio, agentdojo) and whether they have API keys configured.",
466
+ inputSchema: {
467
+ type: "object",
468
+ properties: {
469
+ project_id: { type: "string", description: "Project ID for project-scoped API keys (optional)" },
470
+ },
471
+ },
472
+ },
473
+ {
474
+ name: "list_trigger_types",
475
+ description: `Browse available trigger types from a provider. Trigger types are events you can subscribe to (e.g. github:push, stripe:payment_intent, slack:message).
476
+
477
+ Each trigger type has:
478
+ - slug: unique identifier (e.g. "github:push")
479
+ - name: display name
480
+ - description: what the trigger does
481
+ - config_schema: JSON schema of required config fields (e.g. owner, repo for GitHub triggers)
482
+
483
+ Use this to find trigger slugs before creating a subscription.`,
484
+ inputSchema: {
485
+ type: "object",
486
+ properties: {
487
+ provider: { type: "string", description: "Trigger provider ID: composio or agentdojo" },
488
+ project_id: { type: "string", description: "Project ID for project-scoped API keys (optional)" },
489
+ },
490
+ required: ["provider"],
491
+ },
492
+ },
493
+ {
494
+ name: "list_connected_accounts",
495
+ description: "List connected accounts (OAuth connections) for a provider. You need a connected_account_id to create a subscription. Each account represents a user's authenticated connection to a service (e.g. GitHub, Slack, Stripe).",
496
+ inputSchema: {
497
+ type: "object",
498
+ properties: {
499
+ provider: { type: "string", description: "Integration provider ID: composio or agentdojo" },
500
+ project_id: { type: "string", description: "Project ID for project-scoped API keys (optional)" },
501
+ },
502
+ required: ["provider"],
503
+ },
504
+ },
505
+ {
506
+ name: "list_subscriptions",
507
+ description: "List local trigger subscriptions. Subscriptions route incoming webhook events to agents. Each subscription maps a trigger (e.g. github:push) to a specific agent.",
508
+ inputSchema: {
509
+ type: "object",
510
+ properties: {
511
+ agent_id: { type: "string", description: "Filter by agent ID (optional)" },
512
+ project_id: { type: "string", description: "Filter by project ID (optional)" },
513
+ },
514
+ },
515
+ },
516
+ {
517
+ name: "create_subscription",
518
+ description: `Create a trigger subscription: registers a webhook with the external service (via the provider) and creates a local subscription to route events to an agent.
519
+
520
+ WORKFLOW:
521
+ 1. Use list_trigger_providers to check which providers have keys
522
+ 2. Use list_trigger_types to find the trigger slug you want
523
+ 3. Use list_connected_accounts to find the connected_account_id
524
+ 4. Use list_agents to find the agent_id to route events to
525
+ 5. Call create_subscription with all the above
526
+
527
+ IMPORTANT: Some triggers require extra config fields (e.g. GitHub triggers need "owner" and "repo"). Check the trigger type's config_schema and pass required fields in the config object.`,
528
+ inputSchema: {
529
+ type: "object",
530
+ properties: {
531
+ provider: { type: "string", description: "Trigger provider ID: composio or agentdojo" },
532
+ trigger_slug: { type: "string", description: "Trigger type slug (e.g. 'github:push', 'GITHUB_PUSH_EVENT'). Use list_trigger_types to find slugs." },
533
+ connected_account_id: { type: "string", description: "Connected account ID that owns the integration. Use list_connected_accounts to find IDs." },
534
+ agent_id: { type: "string", description: "Agent ID to route trigger events to. Use list_agents to find IDs." },
535
+ project_id: { type: "string", description: "Project ID for scoping (optional)" },
536
+ config: {
537
+ type: "object",
538
+ description: "Extra config fields required by the trigger type (e.g. { owner: 'myorg', repo: 'myrepo' } for GitHub). Check config_schema from list_trigger_types.",
539
+ },
540
+ },
541
+ required: ["provider", "trigger_slug", "connected_account_id", "agent_id"],
542
+ },
543
+ },
544
+ {
545
+ name: "enable_subscription",
546
+ description: "Enable a disabled subscription so it starts routing events to the agent again. Optionally also enables the remote trigger on the provider.",
547
+ inputSchema: {
548
+ type: "object",
549
+ properties: {
550
+ subscription_id: { type: "string", description: "Local subscription ID" },
551
+ provider: { type: "string", description: "Provider ID to also enable the remote trigger (optional)" },
552
+ project_id: { type: "string", description: "Project ID for API key resolution (optional)" },
553
+ },
554
+ required: ["subscription_id"],
555
+ },
556
+ },
557
+ {
558
+ name: "disable_subscription",
559
+ description: "Disable a subscription so it stops routing events to the agent. Optionally also disables the remote trigger on the provider.",
560
+ inputSchema: {
561
+ type: "object",
562
+ properties: {
563
+ subscription_id: { type: "string", description: "Local subscription ID" },
564
+ provider: { type: "string", description: "Provider ID to also disable the remote trigger (optional)" },
565
+ project_id: { type: "string", description: "Project ID for API key resolution (optional)" },
566
+ },
567
+ required: ["subscription_id"],
568
+ },
569
+ },
570
+ {
571
+ name: "delete_subscription",
572
+ description: "Delete a local subscription. Optionally also deletes the remote trigger on the provider.",
573
+ inputSchema: {
574
+ type: "object",
575
+ properties: {
576
+ subscription_id: { type: "string", description: "Local subscription ID" },
577
+ delete_remote: { type: "boolean", description: "Also delete the remote trigger on the provider (default false)" },
578
+ provider: { type: "string", description: "Provider ID (required if delete_remote is true)" },
579
+ project_id: { type: "string", description: "Project ID for API key resolution (optional)" },
580
+ },
581
+ required: ["subscription_id"],
582
+ },
583
+ },
450
584
  ];
451
585
 
452
586
  // Tool execution handlers
@@ -964,6 +1098,221 @@ async function executeTool(name: string, args: Record<string, any>): Promise<{ c
964
1098
  return { content: [{ type: "text", text: `Test "${tc.name}" deleted.` }] };
965
1099
  }
966
1100
 
1101
+ // Subscription & Trigger tools
1102
+ case "list_trigger_providers": {
1103
+ const providerIds = getTriggerProviderIds();
1104
+ const projectId = args.project_id || null;
1105
+ const result = providerIds.map(id => {
1106
+ const provider = getTriggerProvider(id);
1107
+ const hasKey = !!ProviderKeys.getDecryptedForProject(id, projectId);
1108
+ return {
1109
+ id,
1110
+ name: provider?.name || id,
1111
+ hasKey,
1112
+ };
1113
+ });
1114
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
1115
+ }
1116
+
1117
+ case "list_trigger_types": {
1118
+ const providerId = args.provider;
1119
+ const projectId = args.project_id || null;
1120
+ const triggerProvider = getTriggerProvider(providerId);
1121
+ if (!triggerProvider) {
1122
+ return { content: [{ type: "text", text: `Unknown trigger provider: ${providerId}. Available: ${getTriggerProviderIds().join(", ")}` }], isError: true };
1123
+ }
1124
+ const apiKey = ProviderKeys.getDecryptedForProject(providerId, projectId);
1125
+ if (!apiKey) {
1126
+ return { content: [{ type: "text", text: `${triggerProvider.name} API key not configured` }], isError: true };
1127
+ }
1128
+ const types = await triggerProvider.listTriggerTypes(apiKey);
1129
+ const result = types.map(t => ({
1130
+ slug: t.slug,
1131
+ name: t.name,
1132
+ description: t.description,
1133
+ type: t.type,
1134
+ toolkit_slug: t.toolkit_slug,
1135
+ toolkit_name: t.toolkit_name,
1136
+ config_schema: t.config_schema,
1137
+ }));
1138
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
1139
+ }
1140
+
1141
+ case "list_connected_accounts": {
1142
+ const providerId = args.provider;
1143
+ const projectId = args.project_id || null;
1144
+ const integrationProvider = getProvider(providerId);
1145
+ if (!integrationProvider) {
1146
+ return { content: [{ type: "text", text: `Unknown integration provider: ${providerId}` }], isError: true };
1147
+ }
1148
+ const apiKey = ProviderKeys.getDecryptedForProject(providerId, projectId);
1149
+ if (!apiKey) {
1150
+ return { content: [{ type: "text", text: `${integrationProvider.name} API key not configured` }], isError: true };
1151
+ }
1152
+ const accounts = await integrationProvider.listConnectedAccounts(apiKey, "platform-agent");
1153
+ const result = accounts.map(a => ({
1154
+ id: a.id,
1155
+ appName: a.appName,
1156
+ status: a.status,
1157
+ createdAt: a.createdAt,
1158
+ }));
1159
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
1160
+ }
1161
+
1162
+ case "list_subscriptions": {
1163
+ let subscriptions;
1164
+ if (args.agent_id) {
1165
+ subscriptions = SubscriptionDB.findByAgentId(args.agent_id);
1166
+ } else {
1167
+ subscriptions = SubscriptionDB.findAll(args.project_id || null);
1168
+ }
1169
+ const result = subscriptions.map(s => {
1170
+ const agent = AgentDB.findById(s.agent_id);
1171
+ return {
1172
+ id: s.id,
1173
+ trigger_slug: s.trigger_slug,
1174
+ trigger_instance_id: s.trigger_instance_id,
1175
+ agent_id: s.agent_id,
1176
+ agent_name: agent?.name || "Unknown",
1177
+ enabled: s.enabled,
1178
+ project_id: s.project_id,
1179
+ created_at: s.created_at,
1180
+ };
1181
+ });
1182
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
1183
+ }
1184
+
1185
+ case "create_subscription": {
1186
+ const providerId = args.provider;
1187
+ const projectId = args.project_id || null;
1188
+ const triggerProvider = getTriggerProvider(providerId);
1189
+ if (!triggerProvider) {
1190
+ return { content: [{ type: "text", text: `Unknown trigger provider: ${providerId}. Available: ${getTriggerProviderIds().join(", ")}` }], isError: true };
1191
+ }
1192
+ const apiKey = ProviderKeys.getDecryptedForProject(providerId, projectId);
1193
+ if (!apiKey) {
1194
+ return { content: [{ type: "text", text: `${triggerProvider.name} API key not configured` }], isError: true };
1195
+ }
1196
+ // Validate agent exists
1197
+ const agent = AgentDB.findById(args.agent_id);
1198
+ if (!agent) {
1199
+ return { content: [{ type: "text", text: `Agent not found: ${args.agent_id}` }], isError: true };
1200
+ }
1201
+
1202
+ // Auto-setup webhook if not already configured for this provider
1203
+ const existingWebhook = SettingsDB.get(`${providerId}_webhook_url`);
1204
+ if (!existingWebhook) {
1205
+ try {
1206
+ const instanceUrl = SettingsDB.get("instance_url");
1207
+ if (instanceUrl) {
1208
+ const webhookUrl = `${instanceUrl}/api/webhooks/${providerId}`;
1209
+ const webhookResult = await triggerProvider.setupWebhook(apiKey, webhookUrl);
1210
+ if (webhookResult.secret) {
1211
+ SettingsDB.set(`${providerId}_webhook_secret`, webhookResult.secret);
1212
+ }
1213
+ SettingsDB.set(`${providerId}_webhook_url`, webhookUrl);
1214
+ console.log(`[platform-mcp] Auto-configured ${providerId} webhook: ${webhookUrl}`);
1215
+ }
1216
+ } catch (e) {
1217
+ console.warn(`[platform-mcp] Failed to auto-setup ${providerId} webhook:`, e);
1218
+ }
1219
+ }
1220
+
1221
+ // Create remote trigger on the provider
1222
+ const config: Record<string, unknown> = {
1223
+ agent_id: args.agent_id,
1224
+ ...(args.config || {}),
1225
+ };
1226
+ const triggerResult = await triggerProvider.createTrigger(apiKey, args.trigger_slug, args.connected_account_id, config);
1227
+
1228
+ // Create local subscription for webhook routing
1229
+ const subscription = SubscriptionDB.create({
1230
+ trigger_slug: args.trigger_slug,
1231
+ trigger_instance_id: triggerResult.triggerId || null,
1232
+ agent_id: args.agent_id,
1233
+ enabled: true,
1234
+ project_id: projectId,
1235
+ });
1236
+
1237
+ console.log(`[platform-mcp] Created subscription: ${args.trigger_slug} (instance=${triggerResult.triggerId}) → agent ${agent.name} (${agent.id})`);
1238
+ return { content: [{ type: "text", text: `Subscription created:\n${JSON.stringify({
1239
+ subscription_id: subscription.id,
1240
+ trigger_slug: args.trigger_slug,
1241
+ trigger_instance_id: triggerResult.triggerId,
1242
+ agent: agent.name,
1243
+ provider: providerId,
1244
+ enabled: true,
1245
+ }, null, 2)}` }] };
1246
+ }
1247
+
1248
+ case "enable_subscription": {
1249
+ const sub = SubscriptionDB.findById(args.subscription_id);
1250
+ if (!sub) {
1251
+ return { content: [{ type: "text", text: `Subscription not found: ${args.subscription_id}` }], isError: true };
1252
+ }
1253
+ SubscriptionDB.update(args.subscription_id, { enabled: true });
1254
+
1255
+ // Also enable remote trigger if provider specified
1256
+ if (args.provider && sub.trigger_instance_id) {
1257
+ const triggerProvider = getTriggerProvider(args.provider);
1258
+ const apiKey = triggerProvider ? ProviderKeys.getDecryptedForProject(args.provider, args.project_id || null) : null;
1259
+ if (triggerProvider && apiKey) {
1260
+ try {
1261
+ await triggerProvider.enableTrigger(apiKey, sub.trigger_instance_id);
1262
+ } catch (e) {
1263
+ console.warn(`[platform-mcp] Failed to enable remote trigger ${sub.trigger_instance_id}:`, e);
1264
+ }
1265
+ }
1266
+ }
1267
+ return { content: [{ type: "text", text: `Subscription "${sub.trigger_slug}" enabled` }] };
1268
+ }
1269
+
1270
+ case "disable_subscription": {
1271
+ const sub = SubscriptionDB.findById(args.subscription_id);
1272
+ if (!sub) {
1273
+ return { content: [{ type: "text", text: `Subscription not found: ${args.subscription_id}` }], isError: true };
1274
+ }
1275
+ SubscriptionDB.update(args.subscription_id, { enabled: false });
1276
+
1277
+ // Also disable remote trigger if provider specified
1278
+ if (args.provider && sub.trigger_instance_id) {
1279
+ const triggerProvider = getTriggerProvider(args.provider);
1280
+ const apiKey = triggerProvider ? ProviderKeys.getDecryptedForProject(args.provider, args.project_id || null) : null;
1281
+ if (triggerProvider && apiKey) {
1282
+ try {
1283
+ await triggerProvider.disableTrigger(apiKey, sub.trigger_instance_id);
1284
+ } catch (e) {
1285
+ console.warn(`[platform-mcp] Failed to disable remote trigger ${sub.trigger_instance_id}:`, e);
1286
+ }
1287
+ }
1288
+ }
1289
+ return { content: [{ type: "text", text: `Subscription "${sub.trigger_slug}" disabled` }] };
1290
+ }
1291
+
1292
+ case "delete_subscription": {
1293
+ const sub = SubscriptionDB.findById(args.subscription_id);
1294
+ if (!sub) {
1295
+ return { content: [{ type: "text", text: `Subscription not found: ${args.subscription_id}` }], isError: true };
1296
+ }
1297
+
1298
+ // Delete remote trigger if requested
1299
+ if (args.delete_remote && args.provider && sub.trigger_instance_id) {
1300
+ const triggerProvider = getTriggerProvider(args.provider);
1301
+ const apiKey = triggerProvider ? ProviderKeys.getDecryptedForProject(args.provider, args.project_id || null) : null;
1302
+ if (triggerProvider && apiKey) {
1303
+ try {
1304
+ await triggerProvider.deleteTrigger(apiKey, sub.trigger_instance_id);
1305
+ console.log(`[platform-mcp] Deleted remote trigger ${sub.trigger_instance_id} on ${args.provider}`);
1306
+ } catch (e) {
1307
+ console.warn(`[platform-mcp] Failed to delete remote trigger ${sub.trigger_instance_id}:`, e);
1308
+ }
1309
+ }
1310
+ }
1311
+
1312
+ SubscriptionDB.delete(args.subscription_id);
1313
+ return { content: [{ type: "text", text: `Subscription "${sub.trigger_slug}" deleted` }] };
1314
+ }
1315
+
967
1316
  default:
968
1317
  return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
969
1318
  }
@@ -1020,8 +1369,10 @@ You can manage:
1020
1369
  - 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.
1021
1370
  - PROVIDERS: View which LLM providers have API keys configured.
1022
1371
  - 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.
1372
+ - SUBSCRIPTIONS & TRIGGERS: Subscribe agents to external events (webhooks). Supports multiple providers (composio, agentdojo). Use list_trigger_providers → list_trigger_types → list_connected_accounts → create_subscription. Manage with enable_subscription, disable_subscription, delete_subscription, list_subscriptions.
1023
1373
 
1024
1374
  Typical workflow: list_providers → create_agent → assign MCP servers/skills → start_agent.
1375
+ Subscription workflow: list_trigger_providers → list_trigger_types (pick trigger) → list_connected_accounts (pick account) → create_subscription (link trigger to agent).
1025
1376
  Test workflow: create_test (set agent, message, eval criteria) → run_test → check results.
1026
1377
  Always use list_providers first to check which providers have API keys before creating agents.`,
1027
1378
  };
package/src/providers.ts CHANGED
@@ -198,35 +198,48 @@ export const ProviderKeys = {
198
198
  }
199
199
  },
200
200
 
201
- // Get decrypted API key for a provider (global key)
201
+ // Get decrypted API key for a provider (global key, falls back to env var)
202
202
  getDecrypted(providerId: string): string | null {
203
203
  const record = ProviderKeysDB.findByProvider(providerId);
204
- if (!record) return null;
204
+ if (record) {
205
+ try {
206
+ return decrypt(record.encrypted_key);
207
+ } catch (err) {
208
+ console.error(`Failed to decrypt key for ${providerId}:`, err);
209
+ }
210
+ }
205
211
 
206
- try {
207
- return decrypt(record.encrypted_key);
208
- } catch (err) {
209
- console.error(`Failed to decrypt key for ${providerId}:`, err);
210
- return null;
212
+ // Fall back to environment variable
213
+ const provider = PROVIDERS[providerId as ProviderId];
214
+ if (provider?.envVar) {
215
+ const envVal = process.env[provider.envVar];
216
+ if (envVal) return envVal;
211
217
  }
218
+ return null;
212
219
  },
213
220
 
214
221
  // Get decrypted API key for a provider and project
215
222
  // Falls back to global key if no project-specific key exists
216
223
  getDecryptedForProject(providerId: string, projectId: string | null): string | null {
224
+ console.log(`[ProviderKeys.getDecryptedForProject] providerId=${providerId}, projectId=${projectId}`);
217
225
  // Try project-specific key first
218
226
  if (projectId) {
219
227
  const projectRecord = ProviderKeysDB.findByProviderAndProject(providerId, projectId);
228
+ console.log(`[ProviderKeys.getDecryptedForProject] project record found: ${!!projectRecord}`);
220
229
  if (projectRecord) {
221
230
  try {
222
- return decrypt(projectRecord.encrypted_key);
231
+ const key = decrypt(projectRecord.encrypted_key);
232
+ console.log(`[ProviderKeys.getDecryptedForProject] decrypted project key OK, length=${key?.length}`);
233
+ return key;
223
234
  } catch (err) {
224
235
  console.error(`Failed to decrypt project key for ${providerId}/${projectId}:`, err);
225
236
  }
226
237
  }
227
238
  }
228
239
  // Fall back to global key
229
- return this.getDecrypted(providerId);
240
+ const globalKey = this.getDecrypted(providerId);
241
+ console.log(`[ProviderKeys.getDecryptedForProject] global fallback: found=${!!globalKey}, length=${globalKey?.length || 0}`);
242
+ return globalKey;
230
243
  },
231
244
 
232
245
  // Check if a provider has a key configured (global)
@@ -27,9 +27,22 @@ export async function handleAgentRoutes(
27
27
  ): Promise<Response | null> {
28
28
  // ==================== AGENT CRUD ====================
29
29
 
30
- // GET /api/agents - List all agents (excludes meta agent)
30
+ // GET /api/agents - List agents (excludes meta agent), optionally filtered by project
31
31
  if (path === "/api/agents" && method === "GET") {
32
- const agents = AgentDB.findAll().filter(a => a.id !== META_AGENT_ID);
32
+ const url = new URL(req.url);
33
+ const projectId = url.searchParams.get("project_id");
34
+
35
+ let agents;
36
+ if (projectId === "unassigned") {
37
+ // Agents with no project
38
+ agents = AgentDB.findByProject(null);
39
+ } else if (projectId) {
40
+ agents = AgentDB.findByProject(projectId);
41
+ } else {
42
+ agents = AgentDB.findAll();
43
+ }
44
+
45
+ agents = agents.filter(a => a.id !== META_AGENT_ID);
33
46
  return json({ agents: toApiAgentsBatch(agents) });
34
47
  }
35
48
 
@@ -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
+ }