apteva 0.4.31 → 0.4.41

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 (89) hide show
  1. package/dist/ActivityPage.7907h64p.js +3 -0
  2. package/dist/ApiDocsPage.k3jjenpq.js +4 -0
  3. package/dist/App.01nq20st.js +4 -0
  4. package/dist/App.1maqvamf.js +4 -0
  5. package/dist/App.2yjrh32f.js +4 -0
  6. package/dist/App.3qw8nben.js +20 -0
  7. package/dist/App.7fb3e7mp.js +4 -0
  8. package/dist/App.7sy3wq8c.js +4 -0
  9. package/dist/App.apjrmctz.js +57 -0
  10. package/dist/App.av6t2yhe.js +4 -0
  11. package/dist/App.jqj5a094.js +46 -0
  12. package/dist/App.mc7xf85h.js +4 -0
  13. package/dist/App.myxqcj9x.js +4 -0
  14. package/dist/App.nm91r1mp.js +13 -0
  15. package/dist/App.qcknavjz.js +221 -0
  16. package/dist/App.vc7vfhg4.js +4 -0
  17. package/dist/App.z4s9zkw5.js +4 -0
  18. package/dist/ConnectionsPage.z1pw5xe2.js +3 -0
  19. package/dist/McpPage.8vc97z0b.js +3 -0
  20. package/dist/SettingsPage.p61bz8kd.js +3 -0
  21. package/dist/SkillsPage.r9x43g3g.js +3 -0
  22. package/dist/TasksPage.1e0zkye4.js +3 -0
  23. package/dist/TelemetryPage.p9vbe4gf.js +3 -0
  24. package/dist/TestsPage.d4xy504e.js +3 -0
  25. package/dist/ThreadsPage.m016am3x.js +3 -0
  26. package/dist/index.html +1 -1
  27. package/dist/styles.css +1 -1
  28. package/package.json +8 -7
  29. package/src/crypto.ts +4 -3
  30. package/src/db.ts +153 -28
  31. package/src/integrations/agentdojo.ts +94 -12
  32. package/src/integrations/index.ts +7 -0
  33. package/src/mcp-platform.ts +494 -121
  34. package/src/providers.ts +12 -12
  35. package/src/routes/api/agent-utils.ts +59 -46
  36. package/src/routes/api/agents.ts +52 -1
  37. package/src/routes/api/integrations.ts +11 -5
  38. package/src/routes/api/mcp.ts +5 -4
  39. package/src/routes/api/meta-agent.ts +35 -1
  40. package/src/routes/api/projects.ts +3 -3
  41. package/src/routes/api/providers.ts +121 -30
  42. package/src/routes/api/skills.ts +2 -3
  43. package/src/routes/api/system.ts +8 -13
  44. package/src/server.ts +31 -32
  45. package/src/triggers/agentdojo.ts +2 -2
  46. package/src/web/App.tsx +18 -10
  47. package/src/web/components/activity/ActivityPage.tsx +241 -388
  48. package/src/web/components/agents/AgentCard.tsx +5 -13
  49. package/src/web/components/common/Icons.tsx +8 -0
  50. package/src/web/components/common/Select.tsx +4 -3
  51. package/src/web/components/dashboard/Dashboard.tsx +155 -30
  52. package/src/web/components/index.ts +1 -1
  53. package/src/web/components/layout/Sidebar.tsx +7 -1
  54. package/src/web/components/mcp/IntegrationsPanel.tsx +126 -35
  55. package/src/web/components/mcp/McpPage.tsx +10 -1
  56. package/src/web/components/meta-agent/MetaAgent.tsx +4 -2
  57. package/src/web/components/settings/SettingsPage.tsx +133 -48
  58. package/src/web/components/tasks/TasksPage.tsx +48 -16
  59. package/src/web/components/telemetry/TelemetryPage.tsx +184 -0
  60. package/src/web/components/threads/ThreadsPage.tsx +313 -0
  61. package/src/web/context/AuthContext.tsx +3 -3
  62. package/src/web/context/ProjectContext.tsx +3 -3
  63. package/src/web/context/TelemetryContext.tsx +24 -6
  64. package/src/web/context/index.ts +1 -1
  65. package/src/web/styles.css +20 -4
  66. package/src/web/types.ts +4 -3
  67. package/dist/ActivityPage.41nbye4r.js +0 -3
  68. package/dist/ApiDocsPage.4smnt8m3.js +0 -4
  69. package/dist/App.0sbax9et.js +0 -4
  70. package/dist/App.0ws427h8.js +0 -4
  71. package/dist/App.6q6bar8b.js +0 -4
  72. package/dist/App.80301vdb.js +0 -4
  73. package/dist/App.af2wg84v.js +0 -267
  74. package/dist/App.ca1rz1ph.js +0 -4
  75. package/dist/App.ensa6z0r.js +0 -4
  76. package/dist/App.f8g7tych.js +0 -13
  77. package/dist/App.mvtqv6qc.js +0 -20
  78. package/dist/App.ncgc9cxy.js +0 -4
  79. package/dist/App.p0fb1pds.js +0 -4
  80. package/dist/App.pmaq48sj.js +0 -4
  81. package/dist/App.yv87t9m5.js +0 -4
  82. package/dist/App.zjmfm8p6.js +0 -4
  83. package/dist/ConnectionsPage.anb3rv9a.js +0 -3
  84. package/dist/McpPage.y396h6fy.js +0 -3
  85. package/dist/SettingsPage.p1hc60gk.js +0 -3
  86. package/dist/SkillsPage.yj3xdsay.js +0 -3
  87. package/dist/TasksPage.sjv0khtv.js +0 -3
  88. package/dist/TelemetryPage.2qm4w16r.js +0 -3
  89. package/dist/TestsPage.zzs4qfj8.js +0 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apteva",
3
- "version": "0.4.31",
3
+ "version": "0.4.41",
4
4
  "description": "Run AI agents locally. Multi-provider support for Claude, GPT, Gemini, Llama, and more.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -72,14 +72,15 @@
72
72
  "ink-spinner": "^5.0.0",
73
73
  "ink-text-input": "^6.0.0",
74
74
  "react": "19",
75
- "react-dom": "19"
75
+ "react-dom": "19",
76
+ "recharts": "^3.7.0"
76
77
  },
77
78
  "optionalDependencies": {
78
- "@apteva/apteva-darwin-arm64": "0.4.31",
79
- "@apteva/apteva-darwin-x64": "0.4.31",
80
- "@apteva/apteva-linux-arm64": "0.4.31",
81
- "@apteva/apteva-linux-x64": "0.4.31",
82
- "@apteva/apteva-win32-x64": "0.4.31",
79
+ "@apteva/apteva-darwin-arm64": "0.4.33",
80
+ "@apteva/apteva-darwin-x64": "0.4.33",
81
+ "@apteva/apteva-linux-arm64": "0.4.33",
82
+ "@apteva/apteva-linux-x64": "0.4.33",
83
+ "@apteva/apteva-win32-x64": "0.4.33",
83
84
  "@apteva/agent-darwin-arm64": "^1.33.73",
84
85
  "@apteva/agent-darwin-x64": "^1.33.73",
85
86
  "@apteva/agent-linux-arm64": "^1.33.73",
package/src/crypto.ts CHANGED
@@ -136,12 +136,13 @@ export function validateKeyFormat(provider: string, key: string): { valid: boole
136
136
  return { valid: false, error: "API key cannot be empty" };
137
137
  }
138
138
 
139
- // Ollama and local browser providers use base URL instead of API key
140
- if (provider === "ollama" || provider === "browserengine" || provider === "chrome") {
139
+ // Ollama and CDP use URLs instead of API keys
140
+ if (provider === "ollama" || provider === "cdp") {
141
141
  if (trimmed.startsWith("http://") || trimmed.startsWith("https://") || trimmed.startsWith("ws://") || trimmed.startsWith("wss://")) {
142
142
  return { valid: true };
143
143
  }
144
- return { valid: false, error: `${provider} requires a valid URL (e.g., http://localhost:8098)` };
144
+ const example = provider === "cdp" ? "ws://localhost:9222" : "http://localhost:11434";
145
+ return { valid: false, error: `${provider} requires a valid URL (e.g., ${example})` };
145
146
  }
146
147
 
147
148
  // Multi-field providers store JSON objects — validate the inner api_key
package/src/db.ts CHANGED
@@ -20,7 +20,7 @@ export interface AgentBuiltinTools {
20
20
 
21
21
  export interface OperatorConfig {
22
22
  enabled: boolean;
23
- browser_provider?: string; // "browserbase" | "steel" | "browserengine" | "chrome"
23
+ browser_provider?: string; // "browserengine" | "browserbase" | "steel" | "cdp"
24
24
  display_width?: number;
25
25
  display_height?: number;
26
26
  max_actions_per_turn?: number;
@@ -328,6 +328,12 @@ export function initDatabase(dataDir: string): Database {
328
328
  db.run("PRAGMA busy_timeout = 5000");
329
329
  db.run("PRAGMA foreign_keys = ON");
330
330
 
331
+ // Performance PRAGMAs
332
+ db.run("PRAGMA synchronous = NORMAL"); // Safe with WAL, much faster than FULL
333
+ db.run("PRAGMA cache_size = -20000"); // 20MB page cache (negative = KB)
334
+ db.run("PRAGMA mmap_size = 30000000"); // 30MB memory-mapped I/O
335
+ db.run("PRAGMA temp_store = MEMORY"); // Keep temp tables in memory
336
+
331
337
  // Run migrations
332
338
  runMigrations();
333
339
 
@@ -486,6 +492,7 @@ function runMigrations() {
486
492
  );
487
493
  CREATE INDEX IF NOT EXISTS idx_telemetry_agent ON telemetry_events(agent_id);
488
494
  CREATE INDEX IF NOT EXISTS idx_telemetry_time ON telemetry_events(timestamp);
495
+ CREATE INDEX IF NOT EXISTS idx_telemetry_agent_time ON telemetry_events(agent_id, timestamp DESC);
489
496
  CREATE INDEX IF NOT EXISTS idx_telemetry_category ON telemetry_events(category);
490
497
  CREATE INDEX IF NOT EXISTS idx_telemetry_level ON telemetry_events(level);
491
498
  CREATE INDEX IF NOT EXISTS idx_telemetry_trace ON telemetry_events(trace_id);
@@ -835,6 +842,31 @@ function runMigrations() {
835
842
  CREATE UNIQUE INDEX IF NOT EXISTS idx_provider_keys_unique ON provider_keys(provider_id, COALESCE(project_id, ''));
836
843
  `,
837
844
  },
845
+ {
846
+ name: "034_repair_provider_keys_project_support",
847
+ sql: `
848
+ -- Repair: migrations 022 and 029 may have failed but were marked as applied.
849
+ -- Recreate provider_keys with project_id support from whatever current state.
850
+ CREATE TABLE IF NOT EXISTS provider_keys_repair (
851
+ id TEXT PRIMARY KEY,
852
+ provider_id TEXT NOT NULL,
853
+ encrypted_key TEXT NOT NULL,
854
+ key_hint TEXT,
855
+ is_valid INTEGER DEFAULT 1,
856
+ last_tested_at TEXT,
857
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
858
+ project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
859
+ name TEXT
860
+ );
861
+ INSERT OR IGNORE INTO provider_keys_repair (id, provider_id, encrypted_key, key_hint, is_valid, last_tested_at, created_at)
862
+ SELECT id, provider_id, encrypted_key, key_hint, is_valid, last_tested_at, created_at FROM provider_keys;
863
+ DROP TABLE IF EXISTS provider_keys;
864
+ ALTER TABLE provider_keys_repair RENAME TO provider_keys;
865
+ CREATE INDEX IF NOT EXISTS idx_provider_keys_provider ON provider_keys(provider_id);
866
+ CREATE INDEX IF NOT EXISTS idx_provider_keys_project ON provider_keys(project_id);
867
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_provider_keys_unique ON provider_keys(provider_id, COALESCE(project_id, ''));
868
+ `,
869
+ },
838
870
  ];
839
871
 
840
872
  // Check which migrations have been applied
@@ -1056,10 +1088,10 @@ export const AgentDB = {
1056
1088
 
1057
1089
  // Find agents that have a specific skill
1058
1090
  findBySkill(skillId: string): Agent[] {
1059
- // SQLite JSON query: check if skills array contains the skillId
1091
+ // Use json_each to properly search the JSON array (avoids full table scan with LIKE)
1060
1092
  const rows = db.query(
1061
- `SELECT * FROM agents WHERE skills LIKE ? ORDER BY created_at DESC`
1062
- ).all(`%"${skillId}"%`) as AgentRow[];
1093
+ `SELECT DISTINCT a.* FROM agents a, json_each(a.skills) AS s WHERE s.value = ? ORDER BY a.created_at DESC`
1094
+ ).all(skillId) as AgentRow[];
1063
1095
  return rows.map(rowToAgent);
1064
1096
  },
1065
1097
 
@@ -1091,14 +1123,23 @@ export const AgentDB = {
1091
1123
  return row.count;
1092
1124
  },
1093
1125
 
1094
- // Get decrypted API key for an agent
1126
+ // In-memory cache for decrypted API keys (avoids expensive scryptSync on every request)
1127
+ _apiKeyCache: new Map<string, string>(),
1128
+
1129
+ // Get decrypted API key for an agent (cached)
1095
1130
  getApiKey(id: string): string | null {
1131
+ // Check cache first
1132
+ const cached = this._apiKeyCache.get(id);
1133
+ if (cached) return cached;
1134
+
1096
1135
  const agent = this.findById(id);
1097
1136
  if (!agent || !agent.api_key_encrypted) {
1098
1137
  return null;
1099
1138
  }
1100
1139
  try {
1101
- return decrypt(agent.api_key_encrypted);
1140
+ const key = decrypt(agent.api_key_encrypted);
1141
+ if (key) this._apiKeyCache.set(id, key);
1142
+ return key;
1102
1143
  } catch {
1103
1144
  return null;
1104
1145
  }
@@ -1118,6 +1159,8 @@ export const AgentDB = {
1118
1159
  [encrypted, now, id]
1119
1160
  );
1120
1161
 
1162
+ // Update cache
1163
+ this._apiKeyCache.set(id, newApiKey);
1121
1164
  return newApiKey;
1122
1165
  },
1123
1166
 
@@ -1129,7 +1172,9 @@ export const AgentDB = {
1129
1172
  // If agent already has a key, return it
1130
1173
  if (agent.api_key_encrypted) {
1131
1174
  try {
1132
- return decrypt(agent.api_key_encrypted);
1175
+ const key = decrypt(agent.api_key_encrypted);
1176
+ if (key) this._apiKeyCache.set(id, key);
1177
+ return key;
1133
1178
  } catch {
1134
1179
  // Key is corrupted, regenerate
1135
1180
  }
@@ -1395,7 +1440,7 @@ export const ProviderKeysDB = {
1395
1440
 
1396
1441
  // Find all keys for a provider (global + all projects)
1397
1442
  findAllByProvider(providerId: string): ProviderKey[] {
1398
- const rows = db.query("SELECT * FROM provider_keys WHERE provider_id = ? ORDER BY project_id NULLS FIRST, created_at DESC").all(providerId) as ProviderKeyRow[];
1443
+ const rows = db.query("SELECT * FROM provider_keys WHERE provider_id = ? ORDER BY (project_id IS NOT NULL), created_at DESC").all(providerId) as ProviderKeyRow[];
1399
1444
  return rows.map(rowToProviderKey);
1400
1445
  },
1401
1446
 
@@ -1407,7 +1452,7 @@ export const ProviderKeysDB = {
1407
1452
 
1408
1453
  // Get all provider keys
1409
1454
  findAll(): ProviderKey[] {
1410
- const rows = db.query("SELECT * FROM provider_keys ORDER BY provider_id, project_id NULLS FIRST, created_at DESC").all() as ProviderKeyRow[];
1455
+ const rows = db.query("SELECT * FROM provider_keys ORDER BY provider_id, (project_id IS NOT NULL), created_at DESC").all() as ProviderKeyRow[];
1411
1456
  return rows.map(rowToProviderKey);
1412
1457
  },
1413
1458
 
@@ -1535,6 +1580,22 @@ export const McpServerDB = {
1535
1580
  return rows.map(rowToMcpServer);
1536
1581
  },
1537
1582
 
1583
+ // Light version: skips expensive decryption for listing endpoints
1584
+ findAllLight(): McpServer[] {
1585
+ const rows = db.query("SELECT * FROM mcp_servers ORDER BY created_at DESC").all() as McpServerRow[];
1586
+ return rows.map(rowToMcpServerLight);
1587
+ },
1588
+
1589
+ // Light batch load by IDs: skips decryption (used by toApiAgentsBatch)
1590
+ findByIdsLight(ids: string[]): Map<string, McpServer> {
1591
+ if (ids.length === 0) return new Map();
1592
+ const placeholders = ids.map(() => "?").join(",");
1593
+ const rows = db.query(`SELECT * FROM mcp_servers WHERE id IN (${placeholders})`).all(...ids) as McpServerRow[];
1594
+ const map = new Map<string, McpServer>();
1595
+ for (const row of rows) map.set(row.id, rowToMcpServerLight(row));
1596
+ return map;
1597
+ },
1598
+
1538
1599
  findRunning(): McpServer[] {
1539
1600
  const rows = db.query("SELECT * FROM mcp_servers WHERE status = 'running'").all() as McpServerRow[];
1540
1601
  return rows.map(rowToMcpServer);
@@ -1661,6 +1722,30 @@ export const McpServerDB = {
1661
1722
  const rows = db.query("SELECT * FROM mcp_servers WHERE project_id IS NULL ORDER BY created_at DESC").all() as McpServerRow[];
1662
1723
  return rows.map(rowToMcpServer);
1663
1724
  },
1725
+
1726
+ // Light versions (skip decryption) for listing endpoints
1727
+ findByProjectLight(projectId: string | null): McpServer[] {
1728
+ if (projectId === null) {
1729
+ const rows = db.query("SELECT * FROM mcp_servers WHERE project_id IS NULL ORDER BY created_at DESC").all() as McpServerRow[];
1730
+ return rows.map(rowToMcpServerLight);
1731
+ }
1732
+ const rows = db.query("SELECT * FROM mcp_servers WHERE project_id = ? ORDER BY created_at DESC").all(projectId) as McpServerRow[];
1733
+ return rows.map(rowToMcpServerLight);
1734
+ },
1735
+
1736
+ findForAgentLight(agentProjectId: string | null): McpServer[] {
1737
+ if (agentProjectId === null) {
1738
+ const rows = db.query("SELECT * FROM mcp_servers WHERE project_id IS NULL ORDER BY created_at DESC").all() as McpServerRow[];
1739
+ return rows.map(rowToMcpServerLight);
1740
+ }
1741
+ const rows = db.query("SELECT * FROM mcp_servers WHERE project_id IS NULL OR project_id = ? ORDER BY created_at DESC").all(agentProjectId) as McpServerRow[];
1742
+ return rows.map(rowToMcpServerLight);
1743
+ },
1744
+
1745
+ findGlobalLight(): McpServer[] {
1746
+ const rows = db.query("SELECT * FROM mcp_servers WHERE project_id IS NULL ORDER BY created_at DESC").all() as McpServerRow[];
1747
+ return rows.map(rowToMcpServerLight);
1748
+ },
1664
1749
  };
1665
1750
 
1666
1751
  // MCP Server Tool CRUD operations (for local servers)
@@ -1792,6 +1877,27 @@ function rowToMcpServer(row: McpServerRow): McpServer {
1792
1877
  };
1793
1878
  }
1794
1879
 
1880
+ // Light version: skips expensive decryption of env/headers for listing endpoints
1881
+ function rowToMcpServerLight(row: McpServerRow): McpServer {
1882
+ return {
1883
+ id: row.id,
1884
+ name: row.name,
1885
+ type: row.type as McpServer["type"],
1886
+ package: row.package,
1887
+ pip_module: row.pip_module,
1888
+ command: row.command,
1889
+ args: row.args,
1890
+ env: {},
1891
+ url: row.url,
1892
+ headers: {},
1893
+ port: row.port,
1894
+ status: row.status as "stopped" | "running",
1895
+ source: row.source,
1896
+ project_id: row.project_id,
1897
+ created_at: row.created_at,
1898
+ };
1899
+ }
1900
+
1795
1901
  // Telemetry Event types
1796
1902
  // User types
1797
1903
  export interface User {
@@ -1892,26 +1998,30 @@ export const TelemetryDB = {
1892
1998
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1893
1999
  `);
1894
2000
 
2001
+ // Wrap in transaction for massive speedup (single fsync instead of one per row)
1895
2002
  let inserted = 0;
1896
- for (const event of events) {
1897
- const result = stmt.run(
1898
- event.id,
1899
- agentId,
1900
- event.timestamp,
1901
- event.category,
1902
- event.type,
1903
- event.level,
1904
- event.trace_id || null,
1905
- event.span_id || null,
1906
- event.thread_id || null,
1907
- event.data ? JSON.stringify(event.data) : null,
1908
- event.metadata ? JSON.stringify(event.metadata) : null,
1909
- event.duration_ms || null,
1910
- event.error || null,
1911
- now
1912
- );
1913
- if (result.changes > 0) inserted++;
1914
- }
2003
+ const insertAll = db.transaction(() => {
2004
+ for (const event of events) {
2005
+ const result = stmt.run(
2006
+ event.id,
2007
+ agentId,
2008
+ event.timestamp,
2009
+ event.category,
2010
+ event.type,
2011
+ event.level,
2012
+ event.trace_id || null,
2013
+ event.span_id || null,
2014
+ event.thread_id || null,
2015
+ event.data ? JSON.stringify(event.data) : null,
2016
+ event.metadata ? JSON.stringify(event.metadata) : null,
2017
+ event.duration_ms || null,
2018
+ event.error || null,
2019
+ now
2020
+ );
2021
+ if (result.changes > 0) inserted++;
2022
+ }
2023
+ });
2024
+ insertAll();
1915
2025
  return inserted;
1916
2026
  },
1917
2027
 
@@ -2810,6 +2920,21 @@ export const SubscriptionDB = {
2810
2920
  return rows.map(rowToSubscription);
2811
2921
  },
2812
2922
 
2923
+ // Batch load subscriptions for multiple agents (1 query instead of N)
2924
+ findByAgentIds(agentIds: string[]): Map<string, Subscription[]> {
2925
+ const result = new Map<string, Subscription[]>();
2926
+ if (agentIds.length === 0) return result;
2927
+ const placeholders = agentIds.map(() => "?").join(",");
2928
+ const rows = db.query(`SELECT * FROM subscriptions WHERE agent_id IN (${placeholders})`).all(...agentIds) as SubscriptionRow[];
2929
+ for (const row of rows) {
2930
+ const sub = rowToSubscription(row);
2931
+ const list = result.get(sub.agent_id) || [];
2932
+ list.push(sub);
2933
+ result.set(sub.agent_id, list);
2934
+ }
2935
+ return result;
2936
+ },
2937
+
2813
2938
  findAll(projectId?: string | null): Subscription[] {
2814
2939
  if (projectId) {
2815
2940
  const rows = db.query("SELECT * FROM subscriptions WHERE project_id = ? ORDER BY created_at DESC").all(projectId) as SubscriptionRow[];
@@ -56,18 +56,30 @@ export const AgentDojoProvider: IntegrationProvider = {
56
56
  console.error("AgentDojo listApps providers error:", providersRes.status);
57
57
  }
58
58
 
59
- // Index providers by name for quick lookup
59
+ // Index providers by id and name for quick lookup
60
+ const providerById = new Map<number, any>();
60
61
  const providerByName = new Map<string, any>();
61
62
  for (const p of providers) {
63
+ if (p.id) providerById.set(p.id, p);
62
64
  providerByName.set(p.name, p);
63
65
  if (p.display_name) providerByName.set(p.display_name.toLowerCase(), p);
64
66
  }
65
67
 
66
- // Map toolkits to apps, enriching auth info from providers
68
+ // Map toolkits to apps, enriching auth info from providers.
69
+ // Use auth_provider_id from the toolkit to find the parent provider —
70
+ // one provider can serve many toolkits (e.g. provider "omnikit" id=49
71
+ // serves "omnikit-messaging", "omnikit-cms", "omnikit-billing", etc.)
67
72
  const apps: IntegrationApp[] = toolkits.map((toolkit: any) => {
68
73
  const name = toolkit.name || toolkit.slug;
69
- // Try to find matching provider for this toolkit
70
- const provider = providerByName.get(name) || providerByName.get(name?.toLowerCase());
74
+
75
+ // Primary: match via auth_provider_id (reliable link from API)
76
+ let provider = toolkit.auth_provider_id
77
+ ? providerById.get(toolkit.auth_provider_id)
78
+ : null;
79
+ // Fallback: match by name
80
+ if (!provider) {
81
+ provider = providerByName.get(name) || providerByName.get(name?.toLowerCase());
82
+ }
71
83
 
72
84
  let authSchemes: string[];
73
85
  if (provider) {
@@ -78,6 +90,19 @@ export const AgentDojoProvider: IntegrationProvider = {
78
90
  authSchemes = ["NONE"];
79
91
  }
80
92
 
93
+ // Build credential fields from provider auth_config
94
+ let credentialFields: IntegrationApp["credentialFields"] | undefined;
95
+ if (provider?.auth_config?.required_fields) {
96
+ const descriptions = provider.auth_config.field_descriptions || {};
97
+ const required = new Set(provider.auth_config.required_fields || []);
98
+ const allFields = [...(provider.auth_config.required_fields || []), ...(provider.auth_config.optional_fields || [])];
99
+ credentialFields = allFields.map((f: string) => ({
100
+ name: f,
101
+ description: descriptions[f] || undefined,
102
+ required: required.has(f),
103
+ }));
104
+ }
105
+
81
106
  return {
82
107
  id: String(toolkit.id),
83
108
  name: toolkit.display_name || toolkit.name,
@@ -86,13 +111,27 @@ export const AgentDojoProvider: IntegrationProvider = {
86
111
  logo: provider?.favicon || toolkit.icon_url || null,
87
112
  categories: [],
88
113
  authSchemes,
114
+ providerSlug: provider?.name || undefined,
115
+ credentialFields,
89
116
  };
90
117
  });
91
118
 
92
119
  // Also add any providers that don't match a toolkit (standalone OAuth providers)
93
120
  const toolkitNames = new Set(toolkits.map((t: any) => t.name));
121
+ const toolkitProviderIds = new Set(toolkits.map((t: any) => t.auth_provider_id).filter(Boolean));
94
122
  for (const p of providers) {
95
- if (!toolkitNames.has(p.name)) {
123
+ if (!toolkitNames.has(p.name) && !toolkitProviderIds.has(p.id)) {
124
+ let credentialFields: IntegrationApp["credentialFields"] | undefined;
125
+ if (p.auth_config?.required_fields) {
126
+ const descriptions = p.auth_config.field_descriptions || {};
127
+ const required = new Set(p.auth_config.required_fields || []);
128
+ const allFields = [...(p.auth_config.required_fields || []), ...(p.auth_config.optional_fields || [])];
129
+ credentialFields = allFields.map((f: string) => ({
130
+ name: f,
131
+ description: descriptions[f] || undefined,
132
+ required: required.has(f),
133
+ }));
134
+ }
96
135
  apps.push({
97
136
  id: String(p.id),
98
137
  name: p.display_name || p.name,
@@ -101,6 +140,7 @@ export const AgentDojoProvider: IntegrationProvider = {
101
140
  logo: p.favicon || p.icon_url || null,
102
141
  categories: [],
103
142
  authSchemes: mapAuthSchemes(p.provider_type),
143
+ credentialFields,
104
144
  });
105
145
  }
106
146
  }
@@ -146,8 +186,9 @@ export const AgentDojoProvider: IntegrationProvider = {
146
186
  redirectUrl: string,
147
187
  credentials?: ConnectionCredentials
148
188
  ): Promise<ConnectionRequest> {
149
- // OAuth flow: no credentials provided, or explicit OAUTH2 scheme
150
- if (!credentials?.apiKey) {
189
+ // OAuth flow: no credentials provided (neither apiKey nor multi-field)
190
+ const hasCredentials = credentials?.apiKey || (credentials?.fields && Object.keys(credentials.fields).length > 0);
191
+ if (!hasCredentials) {
151
192
  // Init OAuth via MCP API
152
193
  const res = await fetch(`${AGENTDOJO_API_BASE}/oauth/init`, {
153
194
  method: "POST",
@@ -181,17 +222,58 @@ export const AgentDojoProvider: IntegrationProvider = {
181
222
  }
182
223
 
183
224
  // API key flow: store credential directly
225
+ // Support arbitrary credential fields via credentials.fields, fall back to single api_key
226
+ const credentialData = credentials.fields && Object.keys(credentials.fields).length > 0
227
+ ? credentials.fields
228
+ : { api_key: credentials.apiKey };
229
+
230
+ // Resolve toolkit slug to actual provider name/id — the credential API
231
+ // needs the provider (e.g. "pushover" id=26), not the toolkit slug
232
+ // (e.g. "pushover-notifications"). Fetch the toolkit to get auth_provider_id.
233
+ let providerName = appSlug;
234
+ let providerId: string | number = appSlug;
235
+ try {
236
+ const tkRes = await fetch(`${AGENTDOJO_API_BASE}/toolkits?include_tools=false`, {
237
+ headers: { "X-API-Key": apiKey, "Content-Type": "application/json" },
238
+ });
239
+ if (tkRes.ok) {
240
+ const tkData = await tkRes.json();
241
+ const toolkits = tkData.toolkits || tkData.data || [];
242
+ const toolkit = toolkits.find((t: any) => t.name === appSlug || t.slug === appSlug);
243
+ if (toolkit?.auth_provider_id) {
244
+ // Fetch provider details
245
+ const provRes = await fetch(`${AGENTDOJO_API_BASE}/providers?is_active=true`, {
246
+ headers: { "X-API-Key": apiKey, "Content-Type": "application/json" },
247
+ });
248
+ if (provRes.ok) {
249
+ const provData = await provRes.json();
250
+ const providers = provData.providers || provData.data || [];
251
+ const prov = providers.find((p: any) => p.id === toolkit.auth_provider_id);
252
+ if (prov) {
253
+ providerName = prov.name;
254
+ providerId = prov.id;
255
+ }
256
+ }
257
+ }
258
+ }
259
+ } catch (e) {
260
+ console.warn("[AgentDojo] Failed to resolve provider for toolkit:", appSlug, e);
261
+ }
262
+
263
+ const credBody: Record<string, unknown> = {
264
+ name: providerName,
265
+ provider_id: providerId,
266
+ provider_name: providerName,
267
+ credential_data: credentialData,
268
+ };
269
+ console.log("[AgentDojo] Creating credential:", JSON.stringify(credBody));
184
270
  const res = await fetch(`${AGENTDOJO_API_BASE}/credentials`, {
185
271
  method: "POST",
186
272
  headers: {
187
273
  "X-API-Key": apiKey,
188
274
  "Content-Type": "application/json",
189
275
  },
190
- body: JSON.stringify({
191
- provider_id: appSlug,
192
- provider_name: appSlug,
193
- credential_data: { api_key: credentials.apiKey },
194
- }),
276
+ body: JSON.stringify(credBody),
195
277
  });
196
278
 
197
279
  if (!res.ok) {
@@ -9,6 +9,12 @@ export interface IntegrationApp {
9
9
  logo: string | null;
10
10
  categories: string[];
11
11
  authSchemes: string[]; // e.g., ["OAUTH2", "API_KEY"]
12
+ providerSlug?: string; // The underlying provider name (one provider can serve multiple toolkits)
13
+ credentialFields?: { // Fields required for API_KEY auth (from provider auth_config)
14
+ name: string;
15
+ description?: string;
16
+ required?: boolean;
17
+ }[];
12
18
  }
13
19
 
14
20
  export interface ConnectedAccount {
@@ -32,6 +38,7 @@ export interface ConnectionCredentials {
32
38
  bearerToken?: string;
33
39
  username?: string;
34
40
  password?: string;
41
+ fields?: Record<string, string>; // Arbitrary credential fields (e.g. { appToken: "...", userKey: "..." })
35
42
  }
36
43
 
37
44
  export interface IntegrationProvider {