apteva 0.4.19 → 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 (43) hide show
  1. package/dist/ActivityPage.h769ek3a.js +3 -0
  2. package/dist/{ApiDocsPage.rfpf7ws1.js → ApiDocsPage.kf6bbwkk.js} +1 -1
  3. package/dist/{App.7vzbaz56.js → App.039re6cf.js} +1 -1
  4. package/dist/{App.amwp54wf.js → App.2jmkqm8c.js} +1 -1
  5. package/dist/{App.p93mmyqw.js → App.2yy66bnp.js} +1 -1
  6. package/dist/{App.f8qsyhpr.js → App.3515wsb4.js} +1 -1
  7. package/dist/{App.6nc5acvk.js → App.7v1w3ys9.js} +1 -1
  8. package/dist/{App.sdsc0258.js → App.c90t3dxg.js} +1 -1
  9. package/dist/{App.5qw2dtxs.js → App.edwahsvz.js} +1 -1
  10. package/dist/{App.qmg33p02.js → App.jfx3der4.js} +1 -1
  11. package/dist/{App.kfyrnznw.js → App.n4jb3c22.js} +1 -1
  12. package/dist/App.q3bpx15d.js +20 -0
  13. package/dist/{App.e4202qb4.js → App.r0a2nmqs.js} +84 -84
  14. package/dist/{App.8rfz30p1.js → App.s2yrcz15.js} +1 -1
  15. package/dist/{App.1nmg2h01.js → App.s5j82a5j.js} +1 -1
  16. package/dist/{App.errxz2q4.js → App.tg1b94tx.js} +1 -1
  17. package/dist/ConnectionsPage.a67fjgbf.js +3 -0
  18. package/dist/McpPage.d4p3xvtk.js +3 -0
  19. package/dist/SettingsPage.46sqpe39.js +3 -0
  20. package/dist/SkillsPage.j9hkqm99.js +3 -0
  21. package/dist/TasksPage.6pvkb7s7.js +3 -0
  22. package/dist/TelemetryPage.5zq9msb5.js +3 -0
  23. package/dist/TestsPage.24432yqt.js +3 -0
  24. package/dist/index.html +1 -1
  25. package/package.json +1 -1
  26. package/src/mcp-platform.ts +353 -2
  27. package/src/routes/api/agents.ts +15 -2
  28. package/src/routes/api/system.ts +12 -1
  29. package/src/routes/auth.ts +11 -2
  30. package/src/web/App.tsx +1 -1
  31. package/src/web/components/dashboard/Dashboard.tsx +5 -4
  32. package/src/web/context/AuthContext.tsx +18 -11
  33. package/src/web/hooks/useAgents.ts +7 -3
  34. package/src/web/hooks/useOnboarding.ts +9 -30
  35. package/dist/ActivityPage.9a1qg4bp.js +0 -3
  36. package/dist/App.g8vq68n0.js +0 -20
  37. package/dist/ConnectionsPage.7zqba1r0.js +0 -3
  38. package/dist/McpPage.kf2g327t.js +0 -3
  39. package/dist/SettingsPage.472c15ep.js +0 -3
  40. package/dist/SkillsPage.xdxnh68a.js +0 -3
  41. package/dist/TasksPage.7g0b8xwc.js +0 -3
  42. package/dist/TelemetryPage.pr7rbz4r.js +0 -3
  43. package/dist/TestsPage.zhc6rqjm.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
  };
@@ -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
 
@@ -187,7 +187,18 @@ export async function handleSystemRoutes(
187
187
 
188
188
  // GET /api/dashboard - Get dashboard statistics
189
189
  if (path === "/api/dashboard" && method === "GET") {
190
- const agents = AgentDB.findAll();
190
+ const url = new URL(req.url);
191
+ const projectId = url.searchParams.get("project_id");
192
+
193
+ let agents = AgentDB.findAll();
194
+
195
+ // Filter agents by project if specified
196
+ if (projectId === "unassigned") {
197
+ agents = agents.filter(a => !a.project_id);
198
+ } else if (projectId) {
199
+ agents = agents.filter(a => a.project_id === projectId);
200
+ }
201
+
191
202
  const runningAgents = agents.filter(a => a.status === "running" && a.port);
192
203
 
193
204
  let totalTasks = 0;
@@ -9,6 +9,7 @@ import {
9
9
  validatePassword,
10
10
  REFRESH_TOKEN_EXPIRY,
11
11
  } from "../auth";
12
+ import { Onboarding } from "../providers";
12
13
  import {
13
14
  getTokenFromRequest,
14
15
  getRefreshTokenFromCookie,
@@ -26,11 +27,12 @@ function json(data: unknown, status = 200, headers: Record<string, string> = {})
26
27
  export async function handleAuthRequest(req: Request, path: string): Promise<Response> {
27
28
  const method = req.method;
28
29
 
29
- // GET /api/auth/check - Check authentication status
30
+ // GET /api/auth/check - Check authentication status (includes onboarding to avoid extra round trip)
30
31
  if (path === "/api/auth/check" && method === "GET") {
31
32
  const token = getTokenFromRequest(req);
32
33
  const status = getAuthStatus(token || undefined);
33
- return json(status);
34
+ const onboarding = Onboarding.getStatus();
35
+ return json({ ...status, onboarding });
34
36
  }
35
37
 
36
38
  // POST /api/auth/login - Login with username and password
@@ -108,10 +110,17 @@ export async function handleAuthRequest(req: Request, path: string): Promise<Res
108
110
  // Set new refresh token cookie
109
111
  const cookieHeader = createRefreshTokenCookie(result.refreshToken, REFRESH_TOKEN_EXPIRY);
110
112
 
113
+ // Include user info + onboarding to avoid extra /api/auth/me round trip
114
+ const payload = verifyAccessToken(result.accessToken);
115
+ const user = payload ? UserDB.findById(payload.userId) : null;
116
+ const onboarding = Onboarding.getStatus();
117
+
111
118
  return json(
112
119
  {
113
120
  accessToken: result.accessToken,
114
121
  expiresIn: result.expiresIn,
122
+ user: user ? { id: user.id, username: user.username, role: user.role } : undefined,
123
+ onboarding,
115
124
  },
116
125
  200,
117
126
  { "Set-Cookie": cookieHeader }
package/src/web/App.tsx CHANGED
@@ -64,7 +64,7 @@ function AppContent() {
64
64
  updateAgent,
65
65
  deleteAgent,
66
66
  toggleAgent,
67
- } = useAgents(shouldFetchData);
67
+ } = useAgents(shouldFetchData, currentProjectId);
68
68
 
69
69
  const {
70
70
  providers,
@@ -45,10 +45,11 @@ export function Dashboard({
45
45
 
46
46
  const fetchDashboardData = useCallback(async () => {
47
47
  try {
48
+ const projectParam = currentProjectId ? `project_id=${encodeURIComponent(currentProjectId)}` : "";
48
49
  const [dashRes, tasksRes, activityRes] = await Promise.all([
49
- authFetch("/api/dashboard"),
50
- authFetch("/api/tasks?status=all"),
51
- authFetch("/api/telemetry/events?type=thread_activity&limit=20"),
50
+ authFetch(`/api/dashboard${projectParam ? `?${projectParam}` : ""}`),
51
+ authFetch(`/api/tasks?status=all${projectParam ? `&${projectParam}` : ""}`),
52
+ authFetch(`/api/telemetry/events?type=thread_activity&limit=20${projectParam ? `&${projectParam}` : ""}`),
52
53
  ]);
53
54
 
54
55
  if (dashRes.ok) {
@@ -68,7 +69,7 @@ export function Dashboard({
68
69
  } catch (e) {
69
70
  console.error("Failed to fetch dashboard data:", e);
70
71
  }
71
- }, [authFetch]);
72
+ }, [authFetch, currentProjectId]);
72
73
 
73
74
  useEffect(() => {
74
75
  fetchDashboardData();
@@ -20,6 +20,8 @@ interface AuthContextValue {
20
20
  hasUsers: boolean | null;
21
21
  isDev: boolean;
22
22
  accessToken: string | null;
23
+ onboardingComplete: boolean | null;
24
+ setOnboardingComplete: (v: boolean) => void;
23
25
  login: (username: string, password: string) => Promise<{ success: boolean; error?: string }>;
24
26
  logout: () => Promise<void>;
25
27
  refreshToken: () => Promise<boolean>;
@@ -47,6 +49,7 @@ export function AuthProvider({ children }: AuthProviderProps) {
47
49
  const [isLoading, setIsLoading] = useState(true);
48
50
  const [hasUsers, setHasUsers] = useState<boolean | null>(null);
49
51
  const [isDev, setIsDev] = useState(false);
52
+ const [onboardingComplete, setOnboardingComplete] = useState<boolean | null>(null);
50
53
 
51
54
  // Refs to track state without causing re-renders
52
55
  const tokenRef = useRef<string | null>(null);
@@ -80,18 +83,15 @@ export function AuthProvider({ children }: AuthProviderProps) {
80
83
  const data = await res.json();
81
84
  updateToken(data.accessToken);
82
85
 
83
- // Get user info with new token
84
- const meRes = await fetch("/api/auth/me", {
85
- headers: { Authorization: `Bearer ${data.accessToken}` },
86
- });
87
-
88
- if (meRes.ok) {
89
- const meData = await meRes.json();
90
- setUser(meData.user);
91
- return true;
86
+ // User info + onboarding included in refresh response to avoid extra round trip
87
+ if (data.user) {
88
+ setUser(data.user);
89
+ }
90
+ if (data.onboarding) {
91
+ setOnboardingComplete(data.onboarding.completed || data.onboarding.has_any_keys);
92
92
  }
93
93
 
94
- return false;
94
+ return !!data.user;
95
95
  } catch (e) {
96
96
  console.error("Token refresh failed:", e);
97
97
  return false;
@@ -107,11 +107,16 @@ export function AuthProvider({ children }: AuthProviderProps) {
107
107
  const res = await fetch("/api/auth/check", {
108
108
  headers: token ? { Authorization: `Bearer ${token}` } : {},
109
109
  });
110
- const data: AuthStatus = await res.json();
110
+ const data: AuthStatus & { onboarding?: { completed: boolean; has_any_keys: boolean } } = await res.json();
111
111
 
112
112
  setHasUsers(data.hasUsers);
113
113
  setIsDev(data.isDev ?? false);
114
114
 
115
+ // Extract onboarding status (piggybacks on auth check to avoid extra round trip)
116
+ if (data.onboarding) {
117
+ setOnboardingComplete(data.onboarding.completed || data.onboarding.has_any_keys);
118
+ }
119
+
115
120
  if (data.authenticated && data.user) {
116
121
  setUser(data.user as User);
117
122
  } else {
@@ -218,6 +223,8 @@ export function AuthProvider({ children }: AuthProviderProps) {
218
223
  hasUsers,
219
224
  isDev,
220
225
  accessToken,
226
+ onboardingComplete,
227
+ setOnboardingComplete,
221
228
  login,
222
229
  logout,
223
230
  refreshToken,
@@ -3,7 +3,7 @@ import type { Agent, AgentFeatures } from "../types";
3
3
  import { useAuth } from "../context";
4
4
  import { useAgentStatusChange } from "../context/TelemetryContext";
5
5
 
6
- export function useAgents(enabled: boolean) {
6
+ export function useAgents(enabled: boolean, projectId?: string | null) {
7
7
  const { accessToken } = useAuth();
8
8
  const [agents, setAgents] = useState<Agent[]>([]);
9
9
  const [loading, setLoading] = useState(true);
@@ -17,11 +17,15 @@ export function useAgents(enabled: boolean) {
17
17
  }, [accessToken]);
18
18
 
19
19
  const fetchAgents = useCallback(async () => {
20
- const res = await fetch("/api/agents", { headers: getHeaders() });
20
+ let url = "/api/agents";
21
+ if (projectId !== undefined && projectId !== null) {
22
+ url += `?project_id=${encodeURIComponent(projectId)}`;
23
+ }
24
+ const res = await fetch(url, { headers: getHeaders() });
21
25
  const data = await res.json();
22
26
  setAgents(data.agents || []);
23
27
  setLoading(false);
24
- }, [getHeaders]);
28
+ }, [getHeaders, projectId]);
25
29
 
26
30
  // Fetch on mount + auto-refetch when agents start/stop/crash (via SSE telemetry)
27
31
  const statusChangeCounter = useAgentStatusChange();
@@ -1,41 +1,20 @@
1
- import { useState, useEffect, useCallback } from "react";
1
+ import { useCallback } from "react";
2
2
  import { useAuth } from "../context";
3
3
 
4
4
  export function useOnboarding() {
5
- const { accessToken, isAuthenticated } = useAuth();
6
- const [isComplete, setIsComplete] = useState<boolean | null>(null);
5
+ const { authFetch, onboardingComplete, setOnboardingComplete } = useAuth();
7
6
 
8
- const getHeaders = useCallback((): Record<string, string> => {
9
- const headers: Record<string, string> = {};
10
- if (accessToken) {
11
- headers.Authorization = `Bearer ${accessToken}`;
12
- }
13
- return headers;
14
- }, [accessToken]);
15
-
16
- useEffect(() => {
17
- // Only check onboarding status when authenticated
18
- if (!isAuthenticated) {
19
- setIsComplete(null);
20
- return;
21
- }
22
-
23
- fetch("/api/onboarding/status", { headers: getHeaders() })
24
- .then(res => res.json())
25
- .then(data => {
26
- setIsComplete(data.completed || data.has_any_keys);
27
- })
28
- .catch(() => setIsComplete(true)); // Fallback to showing app
29
- }, [isAuthenticated, getHeaders]);
7
+ // Onboarding status is now included in the /api/auth/check response,
8
+ // so no separate fetch is needed. This eliminates one round trip on load.
30
9
 
31
10
  const complete = useCallback(async () => {
32
- await fetch("/api/onboarding/complete", { method: "POST", headers: getHeaders() });
33
- setIsComplete(true);
34
- }, [getHeaders]);
11
+ await authFetch("/api/onboarding/complete", { method: "POST" });
12
+ setOnboardingComplete(true);
13
+ }, [authFetch, setOnboardingComplete]);
35
14
 
36
15
  return {
37
- isComplete,
38
- setIsComplete,
16
+ isComplete: onboardingComplete,
17
+ setIsComplete: setOnboardingComplete,
39
18
  complete,
40
19
  };
41
20
  }
@@ -1,3 +0,0 @@
1
- import{d as a}from"./App.1nmg2h01.js";import"./App.sdsc0258.js";import"./App.g8vq68n0.js";export{a as ActivityPage};
2
-
3
- //# debugId=A836402CCF3683F564756E2164756E21