apteva 0.4.4 → 0.4.5

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.
@@ -0,0 +1,350 @@
1
+ // AgentDojo Integration Provider
2
+ // Connects to our hosted MCP API
3
+
4
+ import type {
5
+ IntegrationProvider,
6
+ IntegrationApp,
7
+ ConnectedAccount,
8
+ ConnectionRequest,
9
+ ConnectionCredentials,
10
+ } from "./index";
11
+
12
+ // AgentDojo MCP API base URL
13
+ const AGENTDOJO_API_BASE = process.env.AGENTDOJO_API_BASE || "https://api.agentdojo.dev";
14
+
15
+ export const AgentDojoProvider: IntegrationProvider = {
16
+ id: "agentdojo",
17
+ name: "AgentDojo",
18
+
19
+ // List available toolkits as "apps"
20
+ async listApps(apiKey: string): Promise<IntegrationApp[]> {
21
+ const res = await fetch(`${AGENTDOJO_API_BASE}/toolkits?include_tools=true`, {
22
+ headers: {
23
+ "X-API-Key": apiKey,
24
+ "Content-Type": "application/json",
25
+ },
26
+ });
27
+
28
+ if (!res.ok) {
29
+ console.error("AgentDojo listApps error:", res.status, await res.text());
30
+ return [];
31
+ }
32
+
33
+ const data = await res.json();
34
+ const toolkits = data.toolkits || [];
35
+
36
+ return toolkits.map((toolkit: any) => ({
37
+ id: toolkit.id,
38
+ name: toolkit.display_name || toolkit.name,
39
+ slug: toolkit.name,
40
+ description: toolkit.description || null,
41
+ logo: toolkit.icon_url || null,
42
+ categories: [],
43
+ // If toolkit requires auth, it needs API_KEY connection
44
+ authSchemes: toolkit.requires_auth ? ["API_KEY"] : ["NONE"],
45
+ toolsCount: toolkit.tools_count || toolkit.tools?.length || 0,
46
+ tools: toolkit.tools || [],
47
+ }));
48
+ },
49
+
50
+ // List user's credentials (stored locally, validated against toolkits)
51
+ async listConnectedAccounts(apiKey: string, userId: string): Promise<ConnectedAccount[]> {
52
+ // Get list of credentials from our credentials API
53
+ const res = await fetch(`${AGENTDOJO_API_BASE}/credentials`, {
54
+ headers: {
55
+ "X-API-Key": apiKey,
56
+ "Content-Type": "application/json",
57
+ },
58
+ });
59
+
60
+ if (!res.ok) {
61
+ console.error("AgentDojo listConnectedAccounts error:", res.status, await res.text());
62
+ return [];
63
+ }
64
+
65
+ const data = await res.json();
66
+ const credentials = data.data || data.credentials || [];
67
+
68
+ return credentials.map((cred: any) => ({
69
+ id: cred.id,
70
+ appId: cred.provider_id || cred.toolkit_id || cred.provider_name,
71
+ appName: cred.provider_name || cred.toolkit_name || cred.provider_id,
72
+ status: cred.is_valid !== false ? "active" : "failed",
73
+ createdAt: cred.created_at || new Date().toISOString(),
74
+ metadata: {
75
+ keyHint: cred.key_hint,
76
+ },
77
+ }));
78
+ },
79
+
80
+ // Store credentials for a toolkit
81
+ async initiateConnection(
82
+ apiKey: string,
83
+ userId: string,
84
+ appSlug: string,
85
+ redirectUrl: string,
86
+ credentials?: ConnectionCredentials
87
+ ): Promise<ConnectionRequest> {
88
+ if (!credentials?.apiKey) {
89
+ throw new Error("API key is required for AgentDojo connections");
90
+ }
91
+
92
+ // Store the credential
93
+ const res = await fetch(`${AGENTDOJO_API_BASE}/credentials`, {
94
+ method: "POST",
95
+ headers: {
96
+ "X-API-Key": apiKey,
97
+ "Content-Type": "application/json",
98
+ },
99
+ body: JSON.stringify({
100
+ provider_id: appSlug,
101
+ provider_name: appSlug,
102
+ api_key: credentials.apiKey,
103
+ }),
104
+ });
105
+
106
+ if (!res.ok) {
107
+ const text = await res.text();
108
+ console.error("AgentDojo initiateConnection error:", res.status, text);
109
+ throw new Error(`Failed to store credentials: ${text}`);
110
+ }
111
+
112
+ const data = await res.json();
113
+
114
+ return {
115
+ redirectUrl: null, // No OAuth redirect
116
+ connectionId: data.data?.id || data.id,
117
+ status: "active", // Credentials are immediately active
118
+ };
119
+ },
120
+
121
+ async getConnectionStatus(apiKey: string, connectionId: string): Promise<ConnectedAccount | null> {
122
+ const res = await fetch(`${AGENTDOJO_API_BASE}/credentials`, {
123
+ headers: {
124
+ "X-API-Key": apiKey,
125
+ "Content-Type": "application/json",
126
+ },
127
+ });
128
+
129
+ if (!res.ok) return null;
130
+
131
+ const data = await res.json();
132
+ const credentials = data.data || data.credentials || [];
133
+ const cred = credentials.find((c: any) => c.id === connectionId);
134
+
135
+ if (!cred) return null;
136
+
137
+ return {
138
+ id: cred.id,
139
+ appId: cred.provider_id || cred.toolkit_id,
140
+ appName: cred.provider_name || cred.toolkit_name,
141
+ status: "active",
142
+ createdAt: cred.created_at || new Date().toISOString(),
143
+ };
144
+ },
145
+
146
+ async disconnect(apiKey: string, connectionId: string): Promise<boolean> {
147
+ const res = await fetch(`${AGENTDOJO_API_BASE}/credentials/${connectionId}`, {
148
+ method: "DELETE",
149
+ headers: {
150
+ "X-API-Key": apiKey,
151
+ "Content-Type": "application/json",
152
+ },
153
+ });
154
+
155
+ return res.ok;
156
+ },
157
+ };
158
+
159
+ // ============ AgentDojo MCP Server Management ============
160
+
161
+ export interface AgentDojoServer {
162
+ id: string;
163
+ slug: string;
164
+ name: string;
165
+ description: string | null;
166
+ url: string;
167
+ tools: Array<{
168
+ id: string;
169
+ name: string;
170
+ description: string;
171
+ }>;
172
+ isActive: boolean;
173
+ createdAt: string;
174
+ }
175
+
176
+ export interface AgentDojoToolkit {
177
+ id: string;
178
+ name: string;
179
+ displayName: string;
180
+ description: string | null;
181
+ iconUrl: string | null;
182
+ toolsCount: number;
183
+ tools?: Array<{
184
+ id: string;
185
+ name: string;
186
+ displayName: string;
187
+ description: string;
188
+ }>;
189
+ status: string;
190
+ requiresAuth: boolean;
191
+ }
192
+
193
+ // List available toolkits
194
+ export async function listToolkits(apiKey: string, includeTools = false): Promise<AgentDojoToolkit[]> {
195
+ const res = await fetch(
196
+ `${AGENTDOJO_API_BASE}/toolkits?include_tools=${includeTools}`,
197
+ {
198
+ headers: {
199
+ "X-API-Key": apiKey,
200
+ "Content-Type": "application/json",
201
+ },
202
+ }
203
+ );
204
+
205
+ if (!res.ok) {
206
+ console.error("Failed to list AgentDojo toolkits:", await res.text());
207
+ return [];
208
+ }
209
+
210
+ const data = await res.json();
211
+ const toolkits = data.toolkits || [];
212
+
213
+ return toolkits.map((t: any) => ({
214
+ id: t.id,
215
+ name: t.name,
216
+ displayName: t.display_name || t.name,
217
+ description: t.description,
218
+ iconUrl: t.icon_url,
219
+ toolsCount: t.tools_count || t.tools?.length || 0,
220
+ tools: t.tools?.map((tool: any) => ({
221
+ id: tool.id,
222
+ name: tool.name,
223
+ displayName: tool.display_name || tool.name,
224
+ description: tool.description,
225
+ })),
226
+ status: t.status || "active",
227
+ requiresAuth: t.requires_auth || false,
228
+ }));
229
+ }
230
+
231
+ // List user's MCP servers (configs)
232
+ export async function listServers(apiKey: string, includeTools = false): Promise<AgentDojoServer[]> {
233
+ const res = await fetch(
234
+ `${AGENTDOJO_API_BASE}/servers?include_tools=${includeTools}`,
235
+ {
236
+ headers: {
237
+ "X-API-Key": apiKey,
238
+ "Content-Type": "application/json",
239
+ },
240
+ }
241
+ );
242
+
243
+ if (!res.ok) {
244
+ console.error("Failed to list AgentDojo servers:", await res.text());
245
+ return [];
246
+ }
247
+
248
+ const data = await res.json();
249
+ const servers = data.data || [];
250
+
251
+ return servers.map((s: any) => ({
252
+ id: s.id,
253
+ slug: s.slug,
254
+ name: s.name,
255
+ description: s.description,
256
+ url: s.url,
257
+ tools: s.tools || [],
258
+ isActive: s.is_active,
259
+ createdAt: s.created_at,
260
+ }));
261
+ }
262
+
263
+ // Create a new MCP server with selected toolkits
264
+ export async function createServer(
265
+ apiKey: string,
266
+ name: string,
267
+ toolkits: string[],
268
+ description?: string
269
+ ): Promise<AgentDojoServer | null> {
270
+ const res = await fetch(`${AGENTDOJO_API_BASE}/servers`, {
271
+ method: "POST",
272
+ headers: {
273
+ "X-API-Key": apiKey,
274
+ "Content-Type": "application/json",
275
+ },
276
+ body: JSON.stringify({
277
+ name,
278
+ toolkits,
279
+ description,
280
+ }),
281
+ });
282
+
283
+ if (!res.ok) {
284
+ const errText = await res.text();
285
+ console.error("Failed to create AgentDojo server:", errText);
286
+ throw new Error(`Failed to create server: ${errText}`);
287
+ }
288
+
289
+ const data = await res.json();
290
+ const server = data.data || data;
291
+
292
+ return {
293
+ id: server.id,
294
+ slug: server.slug,
295
+ name: server.name,
296
+ description: server.description,
297
+ url: server.url,
298
+ tools: server.tools || [],
299
+ isActive: server.is_active ?? true,
300
+ createdAt: server.created_at,
301
+ };
302
+ }
303
+
304
+ // Get a specific server
305
+ export async function getServer(apiKey: string, serverId: string): Promise<AgentDojoServer | null> {
306
+ const res = await fetch(`${AGENTDOJO_API_BASE}/servers/${serverId}`, {
307
+ headers: {
308
+ "X-API-Key": apiKey,
309
+ "Content-Type": "application/json",
310
+ },
311
+ });
312
+
313
+ if (!res.ok) {
314
+ if (res.status === 404) return null;
315
+ console.error("Failed to get AgentDojo server:", await res.text());
316
+ return null;
317
+ }
318
+
319
+ const data = await res.json();
320
+ const server = data.data || data;
321
+
322
+ return {
323
+ id: server.id,
324
+ slug: server.slug,
325
+ name: server.name,
326
+ description: server.description,
327
+ url: server.url,
328
+ tools: server.tools || [],
329
+ isActive: server.is_active ?? true,
330
+ createdAt: server.created_at,
331
+ };
332
+ }
333
+
334
+ // Delete a server
335
+ export async function deleteServer(apiKey: string, serverId: string): Promise<boolean> {
336
+ const res = await fetch(`${AGENTDOJO_API_BASE}/servers/${serverId}`, {
337
+ method: "DELETE",
338
+ headers: {
339
+ "X-API-Key": apiKey,
340
+ "Content-Type": "application/json",
341
+ },
342
+ });
343
+
344
+ return res.ok;
345
+ }
346
+
347
+ // Get the base URL for constructing MCP server URLs
348
+ export function getBaseUrl(): string {
349
+ return AGENTDOJO_API_BASE;
350
+ }
package/src/openapi.ts CHANGED
@@ -69,6 +69,7 @@ The new access token will be returned and a new refresh token cookie will be set
69
69
  { name: "MCP", description: "Model Context Protocol servers" },
70
70
  { name: "Providers", description: "LLM providers and API keys" },
71
71
  { name: "Projects", description: "Agent grouping" },
72
+ { name: "Skills", description: "Agent skills management" },
72
73
  { name: "System", description: "Health and version info" },
73
74
  ],
74
75
  security: [{ BearerAuth: [] }],
@@ -1297,6 +1298,169 @@ eventSource.onmessage = (event) => {
1297
1298
  },
1298
1299
  },
1299
1300
  },
1301
+ // Skills endpoints
1302
+ "/skills": {
1303
+ get: {
1304
+ tags: ["Skills"],
1305
+ summary: "List all skills",
1306
+ description: "Returns all installed skills. Can filter by project.",
1307
+ parameters: [
1308
+ { name: "project", in: "query", schema: { type: "string" }, description: "Filter: 'all', 'global', or project ID" },
1309
+ { name: "forAgent", in: "query", schema: { type: "string" }, description: "Agent's project ID (shows global + project)" },
1310
+ ],
1311
+ responses: {
1312
+ "200": {
1313
+ description: "List of skills",
1314
+ content: {
1315
+ "application/json": {
1316
+ schema: {
1317
+ type: "object",
1318
+ properties: {
1319
+ skills: { type: "array", items: { $ref: "#/components/schemas/Skill" } },
1320
+ },
1321
+ },
1322
+ },
1323
+ },
1324
+ },
1325
+ },
1326
+ },
1327
+ post: {
1328
+ tags: ["Skills"],
1329
+ summary: "Create a new skill",
1330
+ requestBody: {
1331
+ required: true,
1332
+ content: {
1333
+ "application/json": {
1334
+ schema: { $ref: "#/components/schemas/SkillCreate" },
1335
+ },
1336
+ },
1337
+ },
1338
+ responses: {
1339
+ "201": {
1340
+ description: "Skill created",
1341
+ content: {
1342
+ "application/json": {
1343
+ schema: { $ref: "#/components/schemas/Skill" },
1344
+ },
1345
+ },
1346
+ },
1347
+ },
1348
+ },
1349
+ },
1350
+ "/skills/github/{owner}/{repo}": {
1351
+ get: {
1352
+ tags: ["Skills"],
1353
+ summary: "List skills from GitHub repo",
1354
+ description: "Fetches and lists all skills from a GitHub repository. Skills should be in subdirectories with SKILL.md files.",
1355
+ parameters: [
1356
+ { name: "owner", in: "path", required: true, schema: { type: "string" }, description: "GitHub repo owner" },
1357
+ { name: "repo", in: "path", required: true, schema: { type: "string" }, description: "GitHub repo name" },
1358
+ ],
1359
+ responses: {
1360
+ "200": {
1361
+ description: "List of skills from repo",
1362
+ content: {
1363
+ "application/json": {
1364
+ schema: {
1365
+ type: "object",
1366
+ properties: {
1367
+ skills: {
1368
+ type: "array",
1369
+ items: {
1370
+ type: "object",
1371
+ properties: {
1372
+ name: { type: "string" },
1373
+ description: { type: "string" },
1374
+ path: { type: "string" },
1375
+ size: { type: "integer" },
1376
+ downloadUrl: { type: "string" },
1377
+ },
1378
+ },
1379
+ },
1380
+ repo: {
1381
+ type: "object",
1382
+ properties: {
1383
+ owner: { type: "string" },
1384
+ repo: { type: "string" },
1385
+ url: { type: "string" },
1386
+ },
1387
+ },
1388
+ },
1389
+ },
1390
+ },
1391
+ },
1392
+ },
1393
+ "404": { description: "Repository not found" },
1394
+ },
1395
+ },
1396
+ },
1397
+ "/skills/github/install": {
1398
+ post: {
1399
+ tags: ["Skills"],
1400
+ summary: "Install skill from GitHub",
1401
+ description: "Downloads and installs a skill from a GitHub repository.",
1402
+ requestBody: {
1403
+ required: true,
1404
+ content: {
1405
+ "application/json": {
1406
+ schema: {
1407
+ type: "object",
1408
+ required: ["owner", "repo", "skillName", "downloadUrl"],
1409
+ properties: {
1410
+ owner: { type: "string", description: "GitHub repo owner" },
1411
+ repo: { type: "string", description: "GitHub repo name" },
1412
+ skillName: { type: "string", description: "Name of the skill to install" },
1413
+ downloadUrl: { type: "string", description: "Raw URL to the SKILL.md file" },
1414
+ projectId: { type: "string", nullable: true, description: "Project ID (null for global)" },
1415
+ },
1416
+ },
1417
+ },
1418
+ },
1419
+ },
1420
+ responses: {
1421
+ "201": {
1422
+ description: "Skill installed",
1423
+ content: {
1424
+ "application/json": {
1425
+ schema: { $ref: "#/components/schemas/Skill" },
1426
+ },
1427
+ },
1428
+ },
1429
+ "400": { description: "Skill already exists or invalid request" },
1430
+ },
1431
+ },
1432
+ },
1433
+ "/skills/{skillId}": {
1434
+ get: {
1435
+ tags: ["Skills"],
1436
+ summary: "Get skill by ID",
1437
+ parameters: [
1438
+ { name: "skillId", in: "path", required: true, schema: { type: "string" } },
1439
+ ],
1440
+ responses: {
1441
+ "200": {
1442
+ description: "Skill details",
1443
+ content: {
1444
+ "application/json": {
1445
+ schema: { $ref: "#/components/schemas/Skill" },
1446
+ },
1447
+ },
1448
+ },
1449
+ "404": { description: "Skill not found" },
1450
+ },
1451
+ },
1452
+ delete: {
1453
+ tags: ["Skills"],
1454
+ summary: "Delete skill",
1455
+ parameters: [
1456
+ { name: "skillId", in: "path", required: true, schema: { type: "string" } },
1457
+ ],
1458
+ responses: {
1459
+ "200": { description: "Skill deleted" },
1460
+ "404": { description: "Skill not found" },
1461
+ },
1462
+ },
1463
+ },
1300
1464
  },
1301
1465
  components: {
1302
1466
  securitySchemes: {
@@ -1493,6 +1657,37 @@ eventSource.onmessage = (event) => {
1493
1657
  agentName: { type: "string" },
1494
1658
  },
1495
1659
  },
1660
+ Skill: {
1661
+ type: "object",
1662
+ properties: {
1663
+ id: { type: "string" },
1664
+ name: { type: "string" },
1665
+ description: { type: "string" },
1666
+ content: { type: "string" },
1667
+ version: { type: "string" },
1668
+ license: { type: "string", nullable: true },
1669
+ compatibility: { type: "string", nullable: true },
1670
+ metadata: { type: "object" },
1671
+ allowed_tools: { type: "array", items: { type: "string" } },
1672
+ source: { type: "string", enum: ["local", "skillsmp", "github", "import"] },
1673
+ source_url: { type: "string", nullable: true },
1674
+ enabled: { type: "boolean" },
1675
+ project_id: { type: "string", nullable: true },
1676
+ created_at: { type: "string", format: "date-time" },
1677
+ updated_at: { type: "string", format: "date-time" },
1678
+ },
1679
+ },
1680
+ SkillCreate: {
1681
+ type: "object",
1682
+ required: ["name", "description", "content"],
1683
+ properties: {
1684
+ name: { type: "string", description: "Skill name (lowercase, hyphens allowed)" },
1685
+ description: { type: "string", description: "What this skill does" },
1686
+ content: { type: "string", description: "Skill instructions in markdown" },
1687
+ source: { type: "string", enum: ["local", "skillsmp", "github", "import"] },
1688
+ project_id: { type: "string", nullable: true, description: "Project ID (null for global)" },
1689
+ },
1690
+ },
1496
1691
  McpServer: {
1497
1692
  type: "object",
1498
1693
  properties: {
package/src/providers.ts CHANGED
@@ -175,7 +175,13 @@ export type ProviderId = keyof typeof PROVIDERS;
175
175
  // Provider Keys Management
176
176
  export const ProviderKeys = {
177
177
  // Save an API key (encrypts before storing)
178
- async save(providerId: string, apiKey: string): Promise<{ success: boolean; error?: string }> {
178
+ // projectId: null = global key, string = project-scoped key
179
+ async save(
180
+ providerId: string,
181
+ apiKey: string,
182
+ projectId: string | null = null,
183
+ name: string | null = null
184
+ ): Promise<{ success: boolean; error?: string; id?: string }> {
179
185
  // Validate format
180
186
  const validation = validateKeyFormat(providerId, apiKey);
181
187
  if (!validation.valid) {
@@ -185,14 +191,14 @@ export const ProviderKeys = {
185
191
  try {
186
192
  const encryptedKey = encrypt(apiKey.trim());
187
193
  const keyHint = createKeyHint(apiKey.trim());
188
- ProviderKeysDB.save(providerId, encryptedKey, keyHint);
189
- return { success: true };
194
+ const record = ProviderKeysDB.save(providerId, encryptedKey, keyHint, projectId, name);
195
+ return { success: true, id: record.id };
190
196
  } catch (err) {
191
197
  return { success: false, error: `Failed to save key: ${err}` };
192
198
  }
193
199
  },
194
200
 
195
- // Get decrypted API key for a provider
201
+ // Get decrypted API key for a provider (global key)
196
202
  getDecrypted(providerId: string): string | null {
197
203
  const record = ProviderKeysDB.findByProvider(providerId);
198
204
  if (!record) return null;
@@ -205,33 +211,93 @@ export const ProviderKeys = {
205
211
  }
206
212
  },
207
213
 
208
- // Check if a provider has a key configured
214
+ // Get decrypted API key for a provider and project
215
+ // Falls back to global key if no project-specific key exists
216
+ getDecryptedForProject(providerId: string, projectId: string | null): string | null {
217
+ // Try project-specific key first
218
+ if (projectId) {
219
+ const projectRecord = ProviderKeysDB.findByProviderAndProject(providerId, projectId);
220
+ if (projectRecord) {
221
+ try {
222
+ return decrypt(projectRecord.encrypted_key);
223
+ } catch (err) {
224
+ console.error(`Failed to decrypt project key for ${providerId}/${projectId}:`, err);
225
+ }
226
+ }
227
+ }
228
+ // Fall back to global key
229
+ return this.getDecrypted(providerId);
230
+ },
231
+
232
+ // Check if a provider has a key configured (global)
209
233
  hasKey(providerId: string): boolean {
210
234
  return ProviderKeysDB.findByProvider(providerId) !== null;
211
235
  },
212
236
 
237
+ // Check if a provider has a key for a specific project (or global)
238
+ hasKeyForProject(providerId: string, projectId: string | null): boolean {
239
+ if (projectId && ProviderKeysDB.findByProviderAndProject(providerId, projectId)) {
240
+ return true;
241
+ }
242
+ return ProviderKeysDB.findByProvider(providerId) !== null;
243
+ },
244
+
213
245
  // Get all configured providers with their status (without exposing keys)
214
246
  getAll(): Array<{
247
+ id: string;
215
248
  provider_id: string;
216
249
  key_hint: string;
217
250
  is_valid: boolean;
218
251
  last_tested_at: string | null;
219
252
  created_at: string;
253
+ project_id: string | null;
254
+ name: string | null;
220
255
  }> {
221
256
  return ProviderKeysDB.findAll().map(k => ({
257
+ id: k.id,
222
258
  provider_id: k.provider_id,
223
259
  key_hint: k.key_hint,
224
260
  is_valid: k.is_valid,
225
261
  last_tested_at: k.last_tested_at,
226
262
  created_at: k.created_at,
263
+ project_id: k.project_id,
264
+ name: k.name,
227
265
  }));
228
266
  },
229
267
 
230
- // Delete a provider key
268
+ // Get all keys for a specific provider
269
+ getAllByProvider(providerId: string): Array<{
270
+ id: string;
271
+ provider_id: string;
272
+ key_hint: string;
273
+ is_valid: boolean;
274
+ last_tested_at: string | null;
275
+ created_at: string;
276
+ project_id: string | null;
277
+ name: string | null;
278
+ }> {
279
+ return ProviderKeysDB.findAllByProvider(providerId).map(k => ({
280
+ id: k.id,
281
+ provider_id: k.provider_id,
282
+ key_hint: k.key_hint,
283
+ is_valid: k.is_valid,
284
+ last_tested_at: k.last_tested_at,
285
+ created_at: k.created_at,
286
+ project_id: k.project_id,
287
+ name: k.name,
288
+ }));
289
+ },
290
+
291
+ // Delete a provider key (global)
231
292
  delete(providerId: string): boolean {
232
293
  return ProviderKeysDB.delete(providerId);
233
294
  },
234
295
 
296
+ // Delete a provider key by ID
297
+ deleteById(id: string): boolean {
298
+ return ProviderKeysDB.deleteById(id);
299
+ },
300
+
235
301
  // Test if an API key is valid by making a test request
236
302
  // TODO: Implement actual API testing per provider (Anthropic needs POST, others GET)
237
303
  async test(providerId: string, apiKey?: string): Promise<{ valid: boolean; error?: string }> {
@@ -254,10 +320,15 @@ export const ProviderKeys = {
254
320
  return { valid: true };
255
321
  },
256
322
 
257
- // Get list of provider IDs that have valid keys
323
+ // Get list of provider IDs that have valid keys (global only)
258
324
  getConfiguredProviders(): string[] {
259
325
  return ProviderKeysDB.getConfiguredProviders();
260
326
  },
327
+
328
+ // Get list of all provider IDs that have keys (including project-scoped)
329
+ getAllConfiguredProviders(): string[] {
330
+ return ProviderKeysDB.getAllConfiguredProviders();
331
+ },
261
332
  };
262
333
 
263
334
  // Onboarding status management