apteva 0.4.32 → 0.4.44

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 (113) hide show
  1. package/dist/ActivityPage.c48n83h2.js +3 -0
  2. package/dist/ApiDocsPage.yzcxx5ax.js +4 -0
  3. package/dist/App.09yb8t0b.js +1 -0
  4. package/dist/App.152mbs1r.js +4 -0
  5. package/dist/App.3a67nx9w.js +4 -0
  6. package/dist/App.9epx6785.js +4 -0
  7. package/dist/App.d8955awp.js +4 -0
  8. package/dist/App.drwb57jq.js +4 -0
  9. package/dist/App.gssbmajb.js +4 -0
  10. package/dist/App.qw70pc29.js +53 -0
  11. package/dist/App.qzbx5wtj.js +4 -0
  12. package/dist/App.r5serxkt.js +8 -0
  13. package/dist/App.tpmp9020.js +20 -0
  14. package/dist/App.v2wb4d7d.js +61 -0
  15. package/dist/App.vxmaaj0m.js +13 -0
  16. package/dist/App.w4p2tda9.js +4 -0
  17. package/dist/App.wv2ng55q.js +221 -0
  18. package/dist/App.yncnrn0f.js +4 -0
  19. package/dist/ConnectionsPage.k6cspyqq.js +3 -0
  20. package/dist/McpPage.cdxm48xj.js +3 -0
  21. package/dist/SettingsPage.evpv7c2y.js +3 -0
  22. package/dist/SkillsPage.pvzp6c1a.js +3 -0
  23. package/dist/TasksPage.6jnvbpsy.js +3 -0
  24. package/dist/TelemetryPage.t7vk24zc.js +3 -0
  25. package/dist/TestsPage.5x6658aa.js +3 -0
  26. package/dist/ThreadsPage.3fvhtevh.js +3 -0
  27. package/dist/apteva-kit.css +1 -1
  28. package/dist/index.html +1 -1
  29. package/dist/styles.css +1 -1
  30. package/package.json +10 -9
  31. package/src/crypto.ts +4 -3
  32. package/src/db.ts +171 -36
  33. package/src/integrations/agentdojo.ts +95 -12
  34. package/src/integrations/index.ts +7 -0
  35. package/src/mcp-platform.ts +870 -142
  36. package/src/openapi.ts +96 -0
  37. package/src/providers.ts +60 -34
  38. package/src/routes/api/agent-utils.ts +59 -47
  39. package/src/routes/api/agents.ts +71 -2
  40. package/src/routes/api/integrations.ts +11 -5
  41. package/src/routes/api/mcp.ts +5 -4
  42. package/src/routes/api/meta-agent.ts +37 -1
  43. package/src/routes/api/projects.ts +3 -3
  44. package/src/routes/api/providers.ts +121 -30
  45. package/src/routes/api/skills.ts +2 -3
  46. package/src/routes/api/system.ts +98 -14
  47. package/src/routes/api/telemetry.ts +19 -1
  48. package/src/routes/share.ts +85 -0
  49. package/src/server.ts +43 -32
  50. package/src/triggers/agentdojo.ts +2 -2
  51. package/src/web/App.tsx +107 -21
  52. package/src/web/components/activity/ActivityPage.tsx +242 -389
  53. package/src/web/components/agents/AgentCard.tsx +19 -27
  54. package/src/web/components/agents/AgentPanel.tsx +358 -198
  55. package/src/web/components/agents/AgentsView.tsx +4 -4
  56. package/src/web/components/agents/CreateAgentModal.tsx +21 -79
  57. package/src/web/components/api/ApiDocsPage.tsx +66 -66
  58. package/src/web/components/auth/CreateAccountStep.tsx +16 -16
  59. package/src/web/components/auth/LoginPage.tsx +10 -10
  60. package/src/web/components/common/Icons.tsx +8 -0
  61. package/src/web/components/common/LoadingSpinner.tsx +2 -2
  62. package/src/web/components/common/Modal.tsx +8 -8
  63. package/src/web/components/common/Select.tsx +11 -10
  64. package/src/web/components/connections/ConnectionsPage.tsx +4 -4
  65. package/src/web/components/connections/IntegrationsTab.tsx +18 -18
  66. package/src/web/components/connections/OverviewTab.tsx +13 -13
  67. package/src/web/components/connections/TriggersTab.tsx +99 -99
  68. package/src/web/components/dashboard/Dashboard.tsx +177 -52
  69. package/src/web/components/index.ts +1 -1
  70. package/src/web/components/layout/Header.tsx +50 -34
  71. package/src/web/components/layout/Sidebar.tsx +41 -16
  72. package/src/web/components/mcp/IntegrationsPanel.tsx +160 -69
  73. package/src/web/components/mcp/McpPage.tsx +218 -209
  74. package/src/web/components/meta-agent/MetaAgent.tsx +15 -11
  75. package/src/web/components/onboarding/OnboardingWizard.tsx +25 -25
  76. package/src/web/components/settings/SettingsPage.tsx +389 -221
  77. package/src/web/components/skills/SkillsPage.tsx +88 -88
  78. package/src/web/components/tasks/TasksPage.tsx +385 -68
  79. package/src/web/components/telemetry/TelemetryPage.tsx +294 -39
  80. package/src/web/components/tests/TestsPage.tsx +50 -50
  81. package/src/web/components/threads/ThreadsPage.tsx +315 -0
  82. package/src/web/context/AuthContext.tsx +3 -3
  83. package/src/web/context/ProjectContext.tsx +8 -3
  84. package/src/web/context/TelemetryContext.tsx +24 -6
  85. package/src/web/context/ThemeContext.tsx +69 -0
  86. package/src/web/context/index.ts +3 -1
  87. package/src/web/styles.css +25 -7
  88. package/src/web/themes.ts +99 -0
  89. package/src/web/types.ts +4 -7
  90. package/dist/ActivityPage.41nbye4r.js +0 -3
  91. package/dist/ApiDocsPage.4smnt8m3.js +0 -4
  92. package/dist/App.0sbax9et.js +0 -4
  93. package/dist/App.0ws427h8.js +0 -4
  94. package/dist/App.6q6bar8b.js +0 -4
  95. package/dist/App.80301vdb.js +0 -4
  96. package/dist/App.af2wg84v.js +0 -267
  97. package/dist/App.ca1rz1ph.js +0 -4
  98. package/dist/App.ensa6z0r.js +0 -4
  99. package/dist/App.f8g7tych.js +0 -13
  100. package/dist/App.mvtqv6qc.js +0 -20
  101. package/dist/App.ncgc9cxy.js +0 -4
  102. package/dist/App.p02f4ret.js +0 -1
  103. package/dist/App.p0fb1pds.js +0 -4
  104. package/dist/App.pmaq48sj.js +0 -4
  105. package/dist/App.yv87t9m5.js +0 -4
  106. package/dist/App.zjmfm8p6.js +0 -4
  107. package/dist/ConnectionsPage.anb3rv9a.js +0 -3
  108. package/dist/McpPage.y396h6fy.js +0 -3
  109. package/dist/SettingsPage.p1hc60gk.js +0 -3
  110. package/dist/SkillsPage.yj3xdsay.js +0 -3
  111. package/dist/TasksPage.sjv0khtv.js +0 -3
  112. package/dist/TelemetryPage.2qm4w16r.js +0 -3
  113. package/dist/TestsPage.zzs4qfj8.js +0 -3
package/src/db.ts CHANGED
@@ -5,11 +5,8 @@ import { encrypt, decrypt, encryptObject, decryptObject } from "./crypto";
5
5
  import { randomBytes, createHash } from "crypto";
6
6
 
7
7
  // Types
8
- export type AgentMode = "coordinator" | "worker";
9
-
10
8
  export interface MultiAgentConfig {
11
9
  enabled: boolean;
12
- mode?: AgentMode;
13
10
  group?: string; // Defaults to projectId if not specified
14
11
  }
15
12
 
@@ -20,7 +17,7 @@ export interface AgentBuiltinTools {
20
17
 
21
18
  export interface OperatorConfig {
22
19
  enabled: boolean;
23
- browser_provider?: string; // "browserbase" | "steel" | "browserengine" | "chrome"
20
+ browser_provider?: string; // "browserengine" | "browserbase" | "steel" | "cdp"
24
21
  display_width?: number;
25
22
  display_height?: number;
26
23
  max_actions_per_turn?: number;
@@ -67,7 +64,6 @@ export function getMultiAgentConfig(features: AgentFeatures, projectId?: string
67
64
  if (typeof agents === "boolean") {
68
65
  return {
69
66
  enabled: agents,
70
- mode: "worker",
71
67
  group: projectId || undefined,
72
68
  };
73
69
  }
@@ -328,6 +324,12 @@ export function initDatabase(dataDir: string): Database {
328
324
  db.run("PRAGMA busy_timeout = 5000");
329
325
  db.run("PRAGMA foreign_keys = ON");
330
326
 
327
+ // Performance PRAGMAs
328
+ db.run("PRAGMA synchronous = NORMAL"); // Safe with WAL, much faster than FULL
329
+ db.run("PRAGMA cache_size = -20000"); // 20MB page cache (negative = KB)
330
+ db.run("PRAGMA mmap_size = 30000000"); // 30MB memory-mapped I/O
331
+ db.run("PRAGMA temp_store = MEMORY"); // Keep temp tables in memory
332
+
331
333
  // Run migrations
332
334
  runMigrations();
333
335
 
@@ -486,6 +488,7 @@ function runMigrations() {
486
488
  );
487
489
  CREATE INDEX IF NOT EXISTS idx_telemetry_agent ON telemetry_events(agent_id);
488
490
  CREATE INDEX IF NOT EXISTS idx_telemetry_time ON telemetry_events(timestamp);
491
+ CREATE INDEX IF NOT EXISTS idx_telemetry_agent_time ON telemetry_events(agent_id, timestamp DESC);
489
492
  CREATE INDEX IF NOT EXISTS idx_telemetry_category ON telemetry_events(category);
490
493
  CREATE INDEX IF NOT EXISTS idx_telemetry_level ON telemetry_events(level);
491
494
  CREATE INDEX IF NOT EXISTS idx_telemetry_trace ON telemetry_events(trace_id);
@@ -835,6 +838,37 @@ function runMigrations() {
835
838
  CREATE UNIQUE INDEX IF NOT EXISTS idx_provider_keys_unique ON provider_keys(provider_id, COALESCE(project_id, ''));
836
839
  `,
837
840
  },
841
+ {
842
+ name: "034_repair_provider_keys_project_support",
843
+ sql: `
844
+ -- Repair: migrations 022 and 029 may have failed but were marked as applied.
845
+ -- Recreate provider_keys with project_id support from whatever current state.
846
+ CREATE TABLE IF NOT EXISTS provider_keys_repair (
847
+ id TEXT PRIMARY KEY,
848
+ provider_id TEXT NOT NULL,
849
+ encrypted_key TEXT NOT NULL,
850
+ key_hint TEXT,
851
+ is_valid INTEGER DEFAULT 1,
852
+ last_tested_at TEXT,
853
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
854
+ project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
855
+ name TEXT
856
+ );
857
+ INSERT OR IGNORE INTO provider_keys_repair (id, provider_id, encrypted_key, key_hint, is_valid, last_tested_at, created_at)
858
+ SELECT id, provider_id, encrypted_key, key_hint, is_valid, last_tested_at, created_at FROM provider_keys;
859
+ DROP TABLE IF EXISTS provider_keys;
860
+ ALTER TABLE provider_keys_repair RENAME TO provider_keys;
861
+ CREATE INDEX IF NOT EXISTS idx_provider_keys_provider ON provider_keys(provider_id);
862
+ CREATE INDEX IF NOT EXISTS idx_provider_keys_project ON provider_keys(project_id);
863
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_provider_keys_unique ON provider_keys(provider_id, COALESCE(project_id, ''));
864
+ `,
865
+ },
866
+ {
867
+ name: "035_add_telemetry_cost",
868
+ sql: `
869
+ ALTER TABLE telemetry_events ADD COLUMN cost REAL DEFAULT 0;
870
+ `,
871
+ },
838
872
  ];
839
873
 
840
874
  // Check which migrations have been applied
@@ -1056,10 +1090,10 @@ export const AgentDB = {
1056
1090
 
1057
1091
  // Find agents that have a specific skill
1058
1092
  findBySkill(skillId: string): Agent[] {
1059
- // SQLite JSON query: check if skills array contains the skillId
1093
+ // Use json_each to properly search the JSON array (avoids full table scan with LIKE)
1060
1094
  const rows = db.query(
1061
- `SELECT * FROM agents WHERE skills LIKE ? ORDER BY created_at DESC`
1062
- ).all(`%"${skillId}"%`) as AgentRow[];
1095
+ `SELECT DISTINCT a.* FROM agents a, json_each(a.skills) AS s WHERE s.value = ? ORDER BY a.created_at DESC`
1096
+ ).all(skillId) as AgentRow[];
1063
1097
  return rows.map(rowToAgent);
1064
1098
  },
1065
1099
 
@@ -1091,14 +1125,23 @@ export const AgentDB = {
1091
1125
  return row.count;
1092
1126
  },
1093
1127
 
1094
- // Get decrypted API key for an agent
1128
+ // In-memory cache for decrypted API keys (avoids expensive scryptSync on every request)
1129
+ _apiKeyCache: new Map<string, string>(),
1130
+
1131
+ // Get decrypted API key for an agent (cached)
1095
1132
  getApiKey(id: string): string | null {
1133
+ // Check cache first
1134
+ const cached = this._apiKeyCache.get(id);
1135
+ if (cached) return cached;
1136
+
1096
1137
  const agent = this.findById(id);
1097
1138
  if (!agent || !agent.api_key_encrypted) {
1098
1139
  return null;
1099
1140
  }
1100
1141
  try {
1101
- return decrypt(agent.api_key_encrypted);
1142
+ const key = decrypt(agent.api_key_encrypted);
1143
+ if (key) this._apiKeyCache.set(id, key);
1144
+ return key;
1102
1145
  } catch {
1103
1146
  return null;
1104
1147
  }
@@ -1118,6 +1161,8 @@ export const AgentDB = {
1118
1161
  [encrypted, now, id]
1119
1162
  );
1120
1163
 
1164
+ // Update cache
1165
+ this._apiKeyCache.set(id, newApiKey);
1121
1166
  return newApiKey;
1122
1167
  },
1123
1168
 
@@ -1129,7 +1174,9 @@ export const AgentDB = {
1129
1174
  // If agent already has a key, return it
1130
1175
  if (agent.api_key_encrypted) {
1131
1176
  try {
1132
- return decrypt(agent.api_key_encrypted);
1177
+ const key = decrypt(agent.api_key_encrypted);
1178
+ if (key) this._apiKeyCache.set(id, key);
1179
+ return key;
1133
1180
  } catch {
1134
1181
  // Key is corrupted, regenerate
1135
1182
  }
@@ -1395,7 +1442,7 @@ export const ProviderKeysDB = {
1395
1442
 
1396
1443
  // Find all keys for a provider (global + all projects)
1397
1444
  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[];
1445
+ 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
1446
  return rows.map(rowToProviderKey);
1400
1447
  },
1401
1448
 
@@ -1407,7 +1454,7 @@ export const ProviderKeysDB = {
1407
1454
 
1408
1455
  // Get all provider keys
1409
1456
  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[];
1457
+ const rows = db.query("SELECT * FROM provider_keys ORDER BY provider_id, (project_id IS NOT NULL), created_at DESC").all() as ProviderKeyRow[];
1411
1458
  return rows.map(rowToProviderKey);
1412
1459
  },
1413
1460
 
@@ -1535,6 +1582,22 @@ export const McpServerDB = {
1535
1582
  return rows.map(rowToMcpServer);
1536
1583
  },
1537
1584
 
1585
+ // Light version: skips expensive decryption for listing endpoints
1586
+ findAllLight(): McpServer[] {
1587
+ const rows = db.query("SELECT * FROM mcp_servers ORDER BY created_at DESC").all() as McpServerRow[];
1588
+ return rows.map(rowToMcpServerLight);
1589
+ },
1590
+
1591
+ // Light batch load by IDs: skips decryption (used by toApiAgentsBatch)
1592
+ findByIdsLight(ids: string[]): Map<string, McpServer> {
1593
+ if (ids.length === 0) return new Map();
1594
+ const placeholders = ids.map(() => "?").join(",");
1595
+ const rows = db.query(`SELECT * FROM mcp_servers WHERE id IN (${placeholders})`).all(...ids) as McpServerRow[];
1596
+ const map = new Map<string, McpServer>();
1597
+ for (const row of rows) map.set(row.id, rowToMcpServerLight(row));
1598
+ return map;
1599
+ },
1600
+
1538
1601
  findRunning(): McpServer[] {
1539
1602
  const rows = db.query("SELECT * FROM mcp_servers WHERE status = 'running'").all() as McpServerRow[];
1540
1603
  return rows.map(rowToMcpServer);
@@ -1661,6 +1724,30 @@ export const McpServerDB = {
1661
1724
  const rows = db.query("SELECT * FROM mcp_servers WHERE project_id IS NULL ORDER BY created_at DESC").all() as McpServerRow[];
1662
1725
  return rows.map(rowToMcpServer);
1663
1726
  },
1727
+
1728
+ // Light versions (skip decryption) for listing endpoints
1729
+ findByProjectLight(projectId: string | null): McpServer[] {
1730
+ if (projectId === null) {
1731
+ const rows = db.query("SELECT * FROM mcp_servers WHERE project_id IS NULL ORDER BY created_at DESC").all() as McpServerRow[];
1732
+ return rows.map(rowToMcpServerLight);
1733
+ }
1734
+ const rows = db.query("SELECT * FROM mcp_servers WHERE project_id = ? ORDER BY created_at DESC").all(projectId) as McpServerRow[];
1735
+ return rows.map(rowToMcpServerLight);
1736
+ },
1737
+
1738
+ findForAgentLight(agentProjectId: string | null): McpServer[] {
1739
+ if (agentProjectId === null) {
1740
+ const rows = db.query("SELECT * FROM mcp_servers WHERE project_id IS NULL ORDER BY created_at DESC").all() as McpServerRow[];
1741
+ return rows.map(rowToMcpServerLight);
1742
+ }
1743
+ 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[];
1744
+ return rows.map(rowToMcpServerLight);
1745
+ },
1746
+
1747
+ findGlobalLight(): McpServer[] {
1748
+ const rows = db.query("SELECT * FROM mcp_servers WHERE project_id IS NULL ORDER BY created_at DESC").all() as McpServerRow[];
1749
+ return rows.map(rowToMcpServerLight);
1750
+ },
1664
1751
  };
1665
1752
 
1666
1753
  // MCP Server Tool CRUD operations (for local servers)
@@ -1792,6 +1879,27 @@ function rowToMcpServer(row: McpServerRow): McpServer {
1792
1879
  };
1793
1880
  }
1794
1881
 
1882
+ // Light version: skips expensive decryption of env/headers for listing endpoints
1883
+ function rowToMcpServerLight(row: McpServerRow): McpServer {
1884
+ return {
1885
+ id: row.id,
1886
+ name: row.name,
1887
+ type: row.type as McpServer["type"],
1888
+ package: row.package,
1889
+ pip_module: row.pip_module,
1890
+ command: row.command,
1891
+ args: row.args,
1892
+ env: {},
1893
+ url: row.url,
1894
+ headers: {},
1895
+ port: row.port,
1896
+ status: row.status as "stopped" | "running",
1897
+ source: row.source,
1898
+ project_id: row.project_id,
1899
+ created_at: row.created_at,
1900
+ };
1901
+ }
1902
+
1795
1903
  // Telemetry Event types
1796
1904
  // User types
1797
1905
  export interface User {
@@ -1884,34 +1992,40 @@ export const TelemetryDB = {
1884
1992
  metadata?: Record<string, unknown>;
1885
1993
  duration_ms?: number;
1886
1994
  error?: string;
1995
+ cost?: number;
1887
1996
  }>): number {
1888
1997
  const now = new Date().toISOString();
1889
1998
  const stmt = db.prepare(`
1890
1999
  INSERT OR IGNORE INTO telemetry_events
1891
- (id, agent_id, timestamp, category, type, level, trace_id, span_id, thread_id, data, metadata, duration_ms, error, received_at)
1892
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2000
+ (id, agent_id, timestamp, category, type, level, trace_id, span_id, thread_id, data, metadata, duration_ms, error, received_at, cost)
2001
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1893
2002
  `);
1894
2003
 
2004
+ // Wrap in transaction for massive speedup (single fsync instead of one per row)
1895
2005
  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
- }
2006
+ const insertAll = db.transaction(() => {
2007
+ for (const event of events) {
2008
+ const result = stmt.run(
2009
+ event.id,
2010
+ agentId,
2011
+ event.timestamp,
2012
+ event.category,
2013
+ event.type,
2014
+ event.level,
2015
+ event.trace_id || null,
2016
+ event.span_id || null,
2017
+ event.thread_id || null,
2018
+ event.data ? JSON.stringify(event.data) : null,
2019
+ event.metadata ? JSON.stringify(event.metadata) : null,
2020
+ event.duration_ms || null,
2021
+ event.error || null,
2022
+ now,
2023
+ event.cost || 0
2024
+ );
2025
+ if (result.changes > 0) inserted++;
2026
+ }
2027
+ });
2028
+ insertAll();
1915
2029
  return inserted;
1916
2030
  },
1917
2031
 
@@ -1998,6 +2112,7 @@ export const TelemetryDB = {
1998
2112
  llm_calls: number;
1999
2113
  tool_calls: number;
2000
2114
  errors: number;
2115
+ cost: number;
2001
2116
  }> {
2002
2117
  const conditions: string[] = [];
2003
2118
  const params: unknown[] = [];
@@ -2048,7 +2163,8 @@ export const TelemetryDB = {
2048
2163
  COALESCE(SUM(CASE WHEN t.category = 'LLM' THEN json_extract(t.data, '$.output_tokens') ELSE 0 END), 0) as output_tokens,
2049
2164
  COALESCE(SUM(CASE WHEN t.category = 'LLM' THEN 1 ELSE 0 END), 0) as llm_calls,
2050
2165
  COALESCE(SUM(CASE WHEN t.category = 'TOOL' THEN 1 ELSE 0 END), 0) as tool_calls,
2051
- COALESCE(SUM(CASE WHEN t.level = 'error' THEN 1 ELSE 0 END), 0) as errors
2166
+ COALESCE(SUM(CASE WHEN t.level = 'error' THEN 1 ELSE 0 END), 0) as errors,
2167
+ COALESCE(SUM(t.cost), 0) as cost
2052
2168
  ${fromClause}
2053
2169
  ${where}
2054
2170
  ${groupBy}
@@ -2062,6 +2178,7 @@ export const TelemetryDB = {
2062
2178
  llm_calls: number;
2063
2179
  tool_calls: number;
2064
2180
  errors: number;
2181
+ cost: number;
2065
2182
  }>;
2066
2183
  },
2067
2184
 
@@ -2073,6 +2190,7 @@ export const TelemetryDB = {
2073
2190
  total_errors: number;
2074
2191
  total_input_tokens: number;
2075
2192
  total_output_tokens: number;
2193
+ total_cost: number;
2076
2194
  } {
2077
2195
  const conditions: string[] = [];
2078
2196
  const params: unknown[] = [];
@@ -2103,7 +2221,8 @@ export const TelemetryDB = {
2103
2221
  COALESCE(SUM(CASE WHEN t.category = 'TOOL' THEN 1 ELSE 0 END), 0) as total_tool_calls,
2104
2222
  COALESCE(SUM(CASE WHEN t.level = 'error' THEN 1 ELSE 0 END), 0) as total_errors,
2105
2223
  COALESCE(SUM(CASE WHEN t.category = 'LLM' THEN json_extract(t.data, '$.input_tokens') ELSE 0 END), 0) as total_input_tokens,
2106
- COALESCE(SUM(CASE WHEN t.category = 'LLM' THEN json_extract(t.data, '$.output_tokens') ELSE 0 END), 0) as total_output_tokens
2224
+ COALESCE(SUM(CASE WHEN t.category = 'LLM' THEN json_extract(t.data, '$.output_tokens') ELSE 0 END), 0) as total_output_tokens,
2225
+ COALESCE(SUM(t.cost), 0) as total_cost
2107
2226
  ${fromClause}
2108
2227
  ${where}
2109
2228
  `;
@@ -2115,6 +2234,7 @@ export const TelemetryDB = {
2115
2234
  total_errors: number;
2116
2235
  total_input_tokens: number;
2117
2236
  total_output_tokens: number;
2237
+ total_cost: number;
2118
2238
  };
2119
2239
  },
2120
2240
 
@@ -2810,6 +2930,21 @@ export const SubscriptionDB = {
2810
2930
  return rows.map(rowToSubscription);
2811
2931
  },
2812
2932
 
2933
+ // Batch load subscriptions for multiple agents (1 query instead of N)
2934
+ findByAgentIds(agentIds: string[]): Map<string, Subscription[]> {
2935
+ const result = new Map<string, Subscription[]>();
2936
+ if (agentIds.length === 0) return result;
2937
+ const placeholders = agentIds.map(() => "?").join(",");
2938
+ const rows = db.query(`SELECT * FROM subscriptions WHERE agent_id IN (${placeholders})`).all(...agentIds) as SubscriptionRow[];
2939
+ for (const row of rows) {
2940
+ const sub = rowToSubscription(row);
2941
+ const list = result.get(sub.agent_id) || [];
2942
+ list.push(sub);
2943
+ result.set(sub.agent_id, list);
2944
+ }
2945
+ return result;
2946
+ },
2947
+
2813
2948
  findAll(projectId?: string | null): Subscription[] {
2814
2949
  if (projectId) {
2815
2950
  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
  }
@@ -124,6 +164,7 @@ export const AgentDojoProvider: IntegrationProvider = {
124
164
 
125
165
  const data = await res.json();
126
166
  const credentials = data.data || data.credentials || [];
167
+ console.log(`[AgentDojo] listConnectedAccounts: got ${credentials.length} credentials`, JSON.stringify(credentials.map((c: any) => ({ id: c.id, name: c.name, provider_name: c.provider_name, credential_type: c.credential_type }))));
127
168
 
128
169
  return credentials.map((cred: any) => ({
129
170
  id: String(cred.id),
@@ -146,8 +187,9 @@ export const AgentDojoProvider: IntegrationProvider = {
146
187
  redirectUrl: string,
147
188
  credentials?: ConnectionCredentials
148
189
  ): Promise<ConnectionRequest> {
149
- // OAuth flow: no credentials provided, or explicit OAUTH2 scheme
150
- if (!credentials?.apiKey) {
190
+ // OAuth flow: no credentials provided (neither apiKey nor multi-field)
191
+ const hasCredentials = credentials?.apiKey || (credentials?.fields && Object.keys(credentials.fields).length > 0);
192
+ if (!hasCredentials) {
151
193
  // Init OAuth via MCP API
152
194
  const res = await fetch(`${AGENTDOJO_API_BASE}/oauth/init`, {
153
195
  method: "POST",
@@ -181,17 +223,58 @@ export const AgentDojoProvider: IntegrationProvider = {
181
223
  }
182
224
 
183
225
  // API key flow: store credential directly
226
+ // Support arbitrary credential fields via credentials.fields, fall back to single api_key
227
+ const credentialData = credentials.fields && Object.keys(credentials.fields).length > 0
228
+ ? credentials.fields
229
+ : { api_key: credentials.apiKey };
230
+
231
+ // Resolve toolkit slug to actual provider name/id — the credential API
232
+ // needs the provider (e.g. "pushover" id=26), not the toolkit slug
233
+ // (e.g. "pushover-notifications"). Fetch the toolkit to get auth_provider_id.
234
+ let providerName = appSlug;
235
+ let providerId: string | number = appSlug;
236
+ try {
237
+ const tkRes = await fetch(`${AGENTDOJO_API_BASE}/toolkits?include_tools=false`, {
238
+ headers: { "X-API-Key": apiKey, "Content-Type": "application/json" },
239
+ });
240
+ if (tkRes.ok) {
241
+ const tkData = await tkRes.json();
242
+ const toolkits = tkData.toolkits || tkData.data || [];
243
+ const toolkit = toolkits.find((t: any) => t.name === appSlug || t.slug === appSlug);
244
+ if (toolkit?.auth_provider_id) {
245
+ // Fetch provider details
246
+ const provRes = await fetch(`${AGENTDOJO_API_BASE}/providers?is_active=true`, {
247
+ headers: { "X-API-Key": apiKey, "Content-Type": "application/json" },
248
+ });
249
+ if (provRes.ok) {
250
+ const provData = await provRes.json();
251
+ const providers = provData.providers || provData.data || [];
252
+ const prov = providers.find((p: any) => p.id === toolkit.auth_provider_id);
253
+ if (prov) {
254
+ providerName = prov.name;
255
+ providerId = prov.id;
256
+ }
257
+ }
258
+ }
259
+ }
260
+ } catch (e) {
261
+ console.warn("[AgentDojo] Failed to resolve provider for toolkit:", appSlug, e);
262
+ }
263
+
264
+ const credBody: Record<string, unknown> = {
265
+ name: providerName,
266
+ provider_id: providerId,
267
+ provider_name: providerName,
268
+ credential_data: credentialData,
269
+ };
270
+ console.log("[AgentDojo] Creating credential:", JSON.stringify(credBody));
184
271
  const res = await fetch(`${AGENTDOJO_API_BASE}/credentials`, {
185
272
  method: "POST",
186
273
  headers: {
187
274
  "X-API-Key": apiKey,
188
275
  "Content-Type": "application/json",
189
276
  },
190
- body: JSON.stringify({
191
- provider_id: appSlug,
192
- provider_name: appSlug,
193
- credential_data: { api_key: credentials.apiKey },
194
- }),
277
+ body: JSON.stringify(credBody),
195
278
  });
196
279
 
197
280
  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 {