@nordsym/apiclaw 2.1.0 → 2.2.1

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 (185) hide show
  1. package/README.md +15 -2
  2. package/dist/bin-http.js +0 -0
  3. package/dist/bin.bundled.js +79288 -0
  4. package/dist/funnel-client.d.ts +24 -0
  5. package/dist/funnel-client.d.ts.map +1 -0
  6. package/dist/funnel-client.js +131 -0
  7. package/dist/funnel-client.js.map +1 -0
  8. package/dist/funnel.test.d.ts +2 -0
  9. package/dist/funnel.test.d.ts.map +1 -0
  10. package/dist/funnel.test.js +145 -0
  11. package/dist/funnel.test.js.map +1 -0
  12. package/dist/gateway-client.d.ts.map +1 -1
  13. package/dist/gateway-client.js +24 -2
  14. package/dist/gateway-client.js.map +1 -1
  15. package/dist/index.bundled.js +61263 -0
  16. package/dist/index.js +161 -74
  17. package/dist/index.js.map +1 -1
  18. package/dist/postinstall.d.ts +0 -5
  19. package/dist/postinstall.d.ts.map +1 -1
  20. package/dist/postinstall.js +24 -3
  21. package/dist/postinstall.js.map +1 -1
  22. package/dist/registration-guard.d.ts +29 -0
  23. package/dist/registration-guard.d.ts.map +1 -0
  24. package/dist/registration-guard.js +87 -0
  25. package/dist/registration-guard.js.map +1 -0
  26. package/package.json +7 -2
  27. package/.claude/settings.local.json +0 -9
  28. package/.env.prod +0 -1
  29. package/apiclaw-README.md +0 -494
  30. package/convex/_generated/api.d.ts +0 -137
  31. package/convex/_generated/api.js +0 -23
  32. package/convex/_generated/dataModel.d.ts +0 -60
  33. package/convex/_generated/server.d.ts +0 -143
  34. package/convex/_generated/server.js +0 -93
  35. package/convex/adminActivate.ts +0 -53
  36. package/convex/adminStats.ts +0 -306
  37. package/convex/agents.ts +0 -939
  38. package/convex/analytics.ts +0 -187
  39. package/convex/apiKeys.ts +0 -220
  40. package/convex/backfillAnalytics.ts +0 -272
  41. package/convex/backfillSearchLogs.ts +0 -35
  42. package/convex/billing.ts +0 -834
  43. package/convex/capabilities.ts +0 -157
  44. package/convex/chains.ts +0 -1318
  45. package/convex/credits.ts +0 -211
  46. package/convex/crons.ts +0 -50
  47. package/convex/debugFilestackLogs.ts +0 -16
  48. package/convex/debugGetToken.ts +0 -18
  49. package/convex/directCall.ts +0 -713
  50. package/convex/earnProgress.ts +0 -753
  51. package/convex/email.ts +0 -329
  52. package/convex/feedback.ts +0 -265
  53. package/convex/http.ts +0 -3430
  54. package/convex/inbound.ts +0 -32
  55. package/convex/logs.ts +0 -701
  56. package/convex/migrateFilestack.ts +0 -81
  57. package/convex/migratePartnersProd.ts +0 -174
  58. package/convex/migratePratham.ts +0 -126
  59. package/convex/migrateProviderWorkspaces.ts +0 -175
  60. package/convex/mou.ts +0 -91
  61. package/convex/providerKeys.ts +0 -289
  62. package/convex/providers.ts +0 -1135
  63. package/convex/purchases.ts +0 -183
  64. package/convex/ratelimit.ts +0 -104
  65. package/convex/schema.ts +0 -869
  66. package/convex/searchLogs.ts +0 -265
  67. package/convex/seedAPILayerAPIs.ts +0 -191
  68. package/convex/seedDirectCallConfigs.ts +0 -336
  69. package/convex/seedPratham.ts +0 -149
  70. package/convex/spendAlerts.ts +0 -442
  71. package/convex/stripeActions.ts +0 -607
  72. package/convex/teams.ts +0 -243
  73. package/convex/telemetry.ts +0 -81
  74. package/convex/tsconfig.json +0 -25
  75. package/convex/updateAPIStatus.ts +0 -44
  76. package/convex/usage.ts +0 -260
  77. package/convex/usageReports.ts +0 -357
  78. package/convex/waitlist.ts +0 -55
  79. package/convex/webhooks.ts +0 -494
  80. package/convex/workspaceSettings.ts +0 -143
  81. package/convex/workspaces.ts +0 -1331
  82. package/convex.json +0 -3
  83. package/direct-test.mjs +0 -51
  84. package/email-templates/filestack-provider-outreach.html +0 -162
  85. package/email-templates/partnership-template.html +0 -116
  86. package/email-templates/pratham-draft-preview.txt +0 -57
  87. package/email-templates/pratham-partnership-draft.html +0 -141
  88. package/reports/APIClaw-Session-Report-2026-04-05.pdf +0 -0
  89. package/reports/pipeline/PIPELINE-REPORT.json +0 -153
  90. package/reports/pipeline/acquire_apisguru.json +0 -17
  91. package/reports/pipeline/capabilities.json +0 -38
  92. package/reports/pipeline/discover_azure_recursive.json +0 -1551
  93. package/reports/pipeline/discover_github.json +0 -25
  94. package/reports/pipeline/discover_github_repos.json +0 -49
  95. package/reports/pipeline/discover_swaggerhub.json +0 -24
  96. package/reports/pipeline/discover_well_known.json +0 -23
  97. package/reports/pipeline/fetch_specs.json +0 -19
  98. package/reports/pipeline/generate_providers.json +0 -14
  99. package/reports/pipeline/match_registry.json +0 -11
  100. package/reports/pipeline/parse_specs.json +0 -17
  101. package/reports/pipeline/promote_candidates.json +0 -34
  102. package/reports/pipeline/validate.json +0 -30
  103. package/reports/pipeline/validate_smoke_details.json +0 -3835
  104. package/reports/session-report-2026-04-05.html +0 -433
  105. package/seed-apis-direct.mjs +0 -106
  106. package/src/access-control.ts +0 -174
  107. package/src/adapters/base.ts +0 -364
  108. package/src/adapters/claude-desktop.ts +0 -41
  109. package/src/adapters/cline.ts +0 -88
  110. package/src/adapters/continue.ts +0 -91
  111. package/src/adapters/cursor.ts +0 -43
  112. package/src/adapters/custom.ts +0 -188
  113. package/src/adapters/detect.ts +0 -202
  114. package/src/adapters/index.ts +0 -47
  115. package/src/adapters/windsurf.ts +0 -44
  116. package/src/bin-http.ts +0 -45
  117. package/src/bin.ts +0 -34
  118. package/src/capability-router.ts +0 -331
  119. package/src/chainExecutor.ts +0 -730
  120. package/src/chainResolver.test.ts +0 -246
  121. package/src/chainResolver.ts +0 -658
  122. package/src/cli/commands/demo.ts +0 -109
  123. package/src/cli/commands/doctor.ts +0 -435
  124. package/src/cli/commands/index.ts +0 -9
  125. package/src/cli/commands/login.ts +0 -203
  126. package/src/cli/commands/mcp-install.ts +0 -373
  127. package/src/cli/commands/restore.ts +0 -333
  128. package/src/cli/commands/setup.ts +0 -297
  129. package/src/cli/commands/uninstall.ts +0 -240
  130. package/src/cli/index.ts +0 -148
  131. package/src/cli.ts +0 -370
  132. package/src/confirmation.ts +0 -296
  133. package/src/credentials.ts +0 -455
  134. package/src/credits.ts +0 -329
  135. package/src/crypto.ts +0 -75
  136. package/src/discovery.ts +0 -568
  137. package/src/enterprise/env.ts +0 -156
  138. package/src/enterprise/index.ts +0 -7
  139. package/src/enterprise/script-generator.ts +0 -481
  140. package/src/execute-dynamic.ts +0 -617
  141. package/src/execute.ts +0 -2386
  142. package/src/gateway-client.ts +0 -192
  143. package/src/hivr-whitelist.ts +0 -110
  144. package/src/http-api.ts +0 -286
  145. package/src/http-server-minimal.ts +0 -154
  146. package/src/index.ts +0 -2611
  147. package/src/intelligent-gateway.ts +0 -339
  148. package/src/mcp-analytics.ts +0 -156
  149. package/src/metered.ts +0 -149
  150. package/src/open-apis-generated.ts +0 -157
  151. package/src/open-apis.ts +0 -558
  152. package/src/postinstall.ts +0 -18
  153. package/src/product-whitelist.ts +0 -246
  154. package/src/proxy.ts +0 -36
  155. package/src/session.ts +0 -129
  156. package/src/stripe.ts +0 -497
  157. package/src/telemetry.ts +0 -71
  158. package/src/test.ts +0 -135
  159. package/src/types/convex-api.d.ts +0 -20
  160. package/src/types/convex-api.ts +0 -21
  161. package/src/types.ts +0 -109
  162. package/src/ui/colors.ts +0 -219
  163. package/src/ui/errors.ts +0 -394
  164. package/src/ui/index.ts +0 -17
  165. package/src/ui/prompts.ts +0 -390
  166. package/src/ui/spinner.ts +0 -325
  167. package/src/utils/backup.ts +0 -224
  168. package/src/utils/config.ts +0 -318
  169. package/src/utils/os.ts +0 -124
  170. package/src/utils/paths.ts +0 -203
  171. package/src/webhook.ts +0 -107
  172. package/test-10-working.cjs +0 -97
  173. package/test-14-final.cjs +0 -96
  174. package/test-actual-handlers.ts +0 -92
  175. package/test-apilayer-all-14.ts +0 -249
  176. package/test-apilayer-fixed.ts +0 -248
  177. package/test-direct-endpoints.ts +0 -174
  178. package/test-exact-endpoints.ts +0 -144
  179. package/test-final.ts +0 -83
  180. package/test-full-routing.ts +0 -100
  181. package/test-handlers-correct.ts +0 -217
  182. package/test-numverify-key.ts +0 -41
  183. package/test-via-handlers.ts +0 -92
  184. package/test-worldnews.mjs +0 -26
  185. package/tsconfig.json +0 -20
package/src/execute.ts DELETED
@@ -1,2386 +0,0 @@
1
- /**
2
- * APIClaw Direct Call - Execute API calls through connected providers
3
- */
4
-
5
- import { getCredentials } from './credentials.js';
6
- import { callProxy, PROXY_PROVIDERS } from './proxy.js';
7
- import { executeDynamicAction, hasDynamicConfig, listDynamicActions } from './execute-dynamic.js';
8
-
9
- // Re-export chain execution
10
- export { executeChain } from './chainExecutor.js';
11
- export type {
12
- ChainDefinition,
13
- ChainResult,
14
- ChainOptions,
15
- Credentials,
16
- StepTrace,
17
- ChainError,
18
- } from './chainExecutor.js';
19
- export {
20
- resolveReferences,
21
- validateReferences,
22
- extractReferences,
23
- } from './chainResolver.js';
24
- export type {
25
- ChainContext,
26
- ChainStep,
27
- ChainStepUnion,
28
- Reference,
29
- ValidationResult,
30
- } from './chainResolver.js';
31
-
32
- interface ExecuteResult {
33
- success: boolean;
34
- provider: string;
35
- action: string;
36
- data?: unknown;
37
- error?: string;
38
- code?: string; // Structured error code (e.g., RATE_LIMITED, SERVICE_UNAVAILABLE)
39
- cost?: number;
40
- // Normalized top-level fields (extracted from data for convenience)
41
- url?: string;
42
- id?: string;
43
- content?: string;
44
- status?: string;
45
- }
46
-
47
- // Error codes for structured error responses
48
- const ERROR_CODES = {
49
- RATE_LIMITED: 'RATE_LIMITED',
50
- SERVICE_UNAVAILABLE: 'SERVICE_UNAVAILABLE',
51
- UNAUTHORIZED: 'UNAUTHORIZED',
52
- FORBIDDEN: 'FORBIDDEN',
53
- NOT_FOUND: 'NOT_FOUND',
54
- BAD_REQUEST: 'BAD_REQUEST',
55
- TIMEOUT: 'TIMEOUT',
56
- NETWORK_ERROR: 'NETWORK_ERROR',
57
- PROVIDER_ERROR: 'PROVIDER_ERROR',
58
- INVALID_PARAMS: 'INVALID_PARAMS',
59
- NO_CREDENTIALS: 'NO_CREDENTIALS',
60
- UNKNOWN_PROVIDER: 'UNKNOWN_PROVIDER',
61
- UNKNOWN_ACTION: 'UNKNOWN_ACTION',
62
- MAX_RETRIES_EXCEEDED: 'MAX_RETRIES_EXCEEDED',
63
- } as const;
64
-
65
- type ErrorCode = typeof ERROR_CODES[keyof typeof ERROR_CODES];
66
-
67
- // Retry configuration
68
- const RETRY_CONFIG = {
69
- maxRetries: 3,
70
- baseDelayMs: 1000, // Start with 1 second
71
- maxDelayMs: 30000, // Cap at 30 seconds
72
- retryableStatusCodes: [429, 503, 502, 504], // Rate limit + service unavailable variants
73
- };
74
-
75
- /**
76
- * Calculate exponential backoff delay with jitter
77
- */
78
- function calculateBackoff(attempt: number): number {
79
- const exponentialDelay = RETRY_CONFIG.baseDelayMs * Math.pow(2, attempt);
80
- const jitter = Math.random() * 0.3 * exponentialDelay; // 0-30% jitter
81
- return Math.min(exponentialDelay + jitter, RETRY_CONFIG.maxDelayMs);
82
- }
83
-
84
- /**
85
- * Sleep for a given number of milliseconds
86
- */
87
- function sleep(ms: number): Promise<void> {
88
- return new Promise(resolve => setTimeout(resolve, ms));
89
- }
90
-
91
- /**
92
- * Map HTTP status code to error code
93
- */
94
- function statusToErrorCode(status: number): ErrorCode {
95
- switch (status) {
96
- case 400: return ERROR_CODES.BAD_REQUEST;
97
- case 401: return ERROR_CODES.UNAUTHORIZED;
98
- case 403: return ERROR_CODES.FORBIDDEN;
99
- case 404: return ERROR_CODES.NOT_FOUND;
100
- case 429: return ERROR_CODES.RATE_LIMITED;
101
- case 502:
102
- case 503:
103
- case 504: return ERROR_CODES.SERVICE_UNAVAILABLE;
104
- default: return ERROR_CODES.PROVIDER_ERROR;
105
- }
106
- }
107
-
108
- /**
109
- * Check if a response status code is retryable
110
- */
111
- function isRetryableStatus(status: number): boolean {
112
- return RETRY_CONFIG.retryableStatusCodes.includes(status);
113
- }
114
-
115
- /**
116
- * Fetch with automatic retry for transient failures (429, 503)
117
- */
118
- async function fetchWithRetry(
119
- url: string,
120
- options: RequestInit,
121
- context: { provider: string; action: string }
122
- ): Promise<Response> {
123
- let lastError: Error | null = null;
124
- let lastResponse: Response | null = null;
125
-
126
- for (let attempt = 0; attempt <= RETRY_CONFIG.maxRetries; attempt++) {
127
- try {
128
- const response = await fetch(url, options);
129
-
130
- // Check if we should retry
131
- if (isRetryableStatus(response.status) && attempt < RETRY_CONFIG.maxRetries) {
132
- lastResponse = response;
133
- const delay = calculateBackoff(attempt);
134
-
135
- // Check for Retry-After header
136
- const retryAfter = response.headers.get('Retry-After');
137
- const retryDelay = retryAfter
138
- ? (parseInt(retryAfter) * 1000 || delay)
139
- : delay;
140
-
141
- console.log(`[APIClaw] ${context.provider}/${context.action}: Got ${response.status}, retrying in ${Math.round(retryDelay)}ms (attempt ${attempt + 1}/${RETRY_CONFIG.maxRetries})`);
142
- await sleep(Math.min(retryDelay, RETRY_CONFIG.maxDelayMs));
143
- continue;
144
- }
145
-
146
- return response;
147
- } catch (error) {
148
- lastError = error as Error;
149
-
150
- // Retry on network errors
151
- if (attempt < RETRY_CONFIG.maxRetries) {
152
- const delay = calculateBackoff(attempt);
153
- console.log(`[APIClaw] ${context.provider}/${context.action}: Network error, retrying in ${Math.round(delay)}ms (attempt ${attempt + 1}/${RETRY_CONFIG.maxRetries})`);
154
- await sleep(delay);
155
- continue;
156
- }
157
- }
158
- }
159
-
160
- // If we have a response (even if error status), return it for proper error handling
161
- if (lastResponse) {
162
- return lastResponse;
163
- }
164
-
165
- // All retries exhausted with network errors
166
- throw lastError || new Error('Max retries exceeded');
167
- }
168
-
169
- /**
170
- * Normalize response by extracting common fields to top-level
171
- * Makes it easier for agents to access key data without digging into provider-specific structures
172
- */
173
- function normalizeResponse(result: ExecuteResult): ExecuteResult {
174
- if (!result.success || !result.data) return result;
175
-
176
- const data = result.data as Record<string, unknown>;
177
- const normalized: ExecuteResult = { ...result };
178
-
179
- // Extract URL (various field names across providers)
180
- const urlFields = ['url', 'audioUrl', 'audio_url', 'output_url', 'image_url', 'video_url', 'file_url', 'download_url'];
181
- for (const field of urlFields) {
182
- if (data[field] && typeof data[field] === 'string') {
183
- normalized.url = data[field] as string;
184
- break;
185
- }
186
- }
187
- // Handle array outputs (e.g., Replicate returns output: ["url1", "url2"])
188
- if (!normalized.url && Array.isArray(data.output) && data.output.length > 0 && typeof data.output[0] === 'string') {
189
- normalized.url = data.output[0];
190
- }
191
-
192
- // Extract ID
193
- const idFields = ['id', 'sid', 'message_id', 'prediction_id', 'job_id', 'request_id'];
194
- for (const field of idFields) {
195
- if (data[field] && (typeof data[field] === 'string' || typeof data[field] === 'number')) {
196
- normalized.id = String(data[field]);
197
- break;
198
- }
199
- }
200
-
201
- // Extract content (for LLM/text responses)
202
- const contentFields = ['content', 'text', 'message', 'response', 'output'];
203
- for (const field of contentFields) {
204
- if (data[field] && typeof data[field] === 'string') {
205
- normalized.content = data[field] as string;
206
- break;
207
- }
208
- }
209
-
210
- // Extract status
211
- const statusFields = ['status', 'state'];
212
- for (const field of statusFields) {
213
- if (data[field] && typeof data[field] === 'string') {
214
- normalized.status = data[field] as string;
215
- break;
216
- }
217
- }
218
-
219
- return normalized;
220
- }
221
-
222
- interface DryRunResult {
223
- dry_run: true;
224
- provider: string;
225
- action: string;
226
- would_send: {
227
- url: string;
228
- method: string;
229
- headers: Record<string, string>;
230
- body?: unknown;
231
- };
232
- mock_response: {
233
- success: boolean;
234
- data: unknown;
235
- estimated_cost?: string;
236
- };
237
- notes: string[];
238
- }
239
-
240
- // Mock response generators for dry-run mode
241
- const mockResponses: Record<string, Record<string, (params: any) => unknown>> = {
242
- '46elks': {
243
- send_sms: (params) => ({
244
- id: 'mock_sms_' + Date.now(),
245
- to: params.to,
246
- from: params.from || 'APIClaw',
247
- message: params.message,
248
- status: 'delivered',
249
- cost: 5200, // ~0.52 SEK in microöre
250
- }),
251
- },
252
- twilio: {
253
- send_sms: (params) => ({
254
- sid: 'SMmock' + Date.now(),
255
- to: params.to,
256
- from: params.from || '+15017122661',
257
- body: params.message,
258
- status: 'queued',
259
- }),
260
- },
261
- brave_search: {
262
- search: (params) => ({
263
- query: params.query,
264
- results: [
265
- { title: 'Mock Result 1', url: 'https://example.com/1', description: 'This is a mock search result for dry-run testing.' },
266
- { title: 'Mock Result 2', url: 'https://example.com/2', description: 'Another mock result to simulate search.' },
267
- ],
268
- total: 2,
269
- }),
270
- },
271
- resend: {
272
- send_email: (params) => ({
273
- id: 'mock_email_' + Date.now(),
274
- to: params.to,
275
- from: params.from || 'APIClaw <noreply@apiclaw.cloud>',
276
- subject: params.subject,
277
- status: 'sent',
278
- }),
279
- },
280
- openrouter: {
281
- chat: (params) => ({
282
- content: '[DRY-RUN] This is a mock response. In production, this would be generated by ' + (params.model || 'anthropic/claude-3-haiku'),
283
- model: params.model || 'anthropic/claude-3-haiku',
284
- usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 },
285
- }),
286
- },
287
- elevenlabs: {
288
- text_to_speech: (params) => ({
289
- audio_base64: 'W0RSWV9SVU5fTU9DS19BVURJT10=', // "[DRY_RUN_MOCK_AUDIO]" in base64
290
- format: 'mp3',
291
- text_length: params.text?.length || 0,
292
- }),
293
- },
294
- replicate: {
295
- run: (params) => ({
296
- status: 'succeeded',
297
- output: ['https://example.com/mock-image.png'],
298
- model: params.model,
299
- metrics: { predict_time: 2.5 },
300
- }),
301
- list_models: () => ({
302
- models: [
303
- { name: 'stability-ai/sdxl', description: 'Mock model listing' },
304
- ],
305
- message: 'DRY-RUN: Would list available models',
306
- }),
307
- },
308
- firecrawl: {
309
- scrape: (params) => ({
310
- markdown: `# Mock Scrape of ${params.url}\n\nThis is a dry-run mock of scraped content.`,
311
- metadata: { title: 'Mock Page', url: params.url },
312
- }),
313
- crawl: (params) => ({
314
- id: 'crawl_mock_' + Date.now(),
315
- status: 'started',
316
- message: 'DRY-RUN: Crawl job would be started',
317
- }),
318
- map: (params) => ({
319
- links: ['https://example.com/page1', 'https://example.com/page2'],
320
- }),
321
- },
322
- github: {
323
- search_repos: (params) => ({
324
- total: 2,
325
- repos: [
326
- { name: 'mock/repo', description: 'Mock repository', stars: 100, url: 'https://github.com/mock/repo', language: 'TypeScript' },
327
- ],
328
- }),
329
- get_repo: (params) => ({
330
- name: `${params.owner}/${params.repo}`,
331
- description: 'Mock repository details',
332
- stars: 100,
333
- forks: 10,
334
- language: 'TypeScript',
335
- url: `https://github.com/${params.owner}/${params.repo}`,
336
- }),
337
- list_issues: () => ({
338
- issues: [{ number: 1, title: 'Mock Issue', state: 'open', user: 'mock-user' }],
339
- }),
340
- create_issue: (params) => ({
341
- number: 1,
342
- url: `https://github.com/${params.owner}/${params.repo}/issues/1`,
343
- }),
344
- get_file: (params) => ({
345
- name: params.path.split('/').pop(),
346
- path: params.path,
347
- size: 100,
348
- content: '// Mock file content for dry-run',
349
- }),
350
- },
351
- e2b: {
352
- run_code: (params) => ({
353
- text: 'DRY-RUN: Code would execute: ' + (params.code?.substring(0, 50) || ''),
354
- logs: { stdout: [], stderr: [] },
355
- results: [],
356
- }),
357
- run_shell: (params) => ({
358
- stdout: 'DRY-RUN: Would run: ' + params.command,
359
- stderr: '',
360
- exitCode: 0,
361
- }),
362
- },
363
- };
364
-
365
- // API endpoint info for dry-run
366
- const apiEndpoints: Record<string, Record<string, { url: string; method: string; estimatedCost?: string }>> = {
367
- '46elks': {
368
- send_sms: { url: 'https://api.46elks.com/a1/sms', method: 'POST', estimatedCost: '~0.35-0.52 SEK' },
369
- },
370
- twilio: {
371
- send_sms: { url: 'https://api.twilio.com/2010-04-01/Accounts/{accountSid}/Messages.json', method: 'POST', estimatedCost: '~$0.0079/SMS' },
372
- },
373
- brave_search: {
374
- search: { url: 'https://api.search.brave.com/res/v1/web/search', method: 'GET' },
375
- },
376
- resend: {
377
- send_email: { url: 'https://api.resend.com/emails', method: 'POST' },
378
- },
379
- openrouter: {
380
- chat: { url: 'https://openrouter.ai/api/v1/chat/completions', method: 'POST', estimatedCost: 'varies by model' },
381
- },
382
- elevenlabs: {
383
- text_to_speech: { url: 'https://api.elevenlabs.io/v1/text-to-speech/{voice_id}', method: 'POST', estimatedCost: '~$0.30/1000 chars' },
384
- },
385
- replicate: {
386
- run: { url: 'https://api.replicate.com/v1/predictions', method: 'POST', estimatedCost: 'varies by model' },
387
- list_models: { url: 'https://api.replicate.com/v1/models', method: 'GET' },
388
- },
389
- firecrawl: {
390
- scrape: { url: 'https://api.firecrawl.dev/v1/scrape', method: 'POST' },
391
- crawl: { url: 'https://api.firecrawl.dev/v1/crawl', method: 'POST' },
392
- map: { url: 'https://api.firecrawl.dev/v1/map', method: 'POST' },
393
- },
394
- github: {
395
- search_repos: { url: 'https://api.github.com/search/repositories', method: 'GET' },
396
- get_repo: { url: 'https://api.github.com/repos/{owner}/{repo}', method: 'GET' },
397
- list_issues: { url: 'https://api.github.com/repos/{owner}/{repo}/issues', method: 'GET' },
398
- create_issue: { url: 'https://api.github.com/repos/{owner}/{repo}/issues', method: 'POST' },
399
- get_file: { url: 'https://api.github.com/repos/{owner}/{repo}/contents/{path}', method: 'GET' },
400
- },
401
- e2b: {
402
- run_code: { url: 'https://api.e2b.dev/v1/sandboxes', method: 'POST' },
403
- run_shell: { url: 'https://api.e2b.dev/v1/sandboxes', method: 'POST' },
404
- },
405
- apilayer: {
406
- // Unified APILayer APIs (apilayer.com)
407
- exchange_rates: { url: 'https://api.apilayer.com/exchangerates_data/latest', method: 'GET' },
408
- market_data: { url: 'http://api.marketstack.com/v1/eod', method: 'GET' },
409
- aviation: { url: 'http://api.aviationstack.com/v1/flights', method: 'GET' },
410
- pdf_generate: { url: 'https://api.pdflayer.com/api', method: 'POST' },
411
- screenshot: { url: 'https://api.screenshotlayer.com/api/capture', method: 'GET' },
412
- verify_email: { url: 'https://api.apilayer.com/email_verification/check', method: 'GET' },
413
- verify_number: { url: 'https://api.apilayer.com/number_verification/validate', method: 'GET' },
414
- vat_check: { url: 'https://apilayer.net/api/validate', method: 'GET' },
415
- world_news: { url: 'https://api.apilayer.com/world_news/extract-news', method: 'GET' },
416
- finance_news: { url: 'https://api.apilayer.com/financelayer/news', method: 'GET' },
417
- scrape: { url: 'https://api.apilayer.com/adv_scraper/scraper', method: 'GET' },
418
- image_crop: { url: 'https://api.apilayer.com/smart_crop/url', method: 'POST' },
419
- skills: { url: 'https://api.promptapi.com/skills', method: 'GET' },
420
- form_submit: { url: 'https://api.apilayer.com/form_api/{endpoint}', method: 'POST' },
421
-
422
- // Legacy APIs (separate domains, each with own key)
423
- // FINANCE
424
- fixer_convert: { url: 'http://data.fixer.io/api/convert', method: 'GET' },
425
- fixer_latest: { url: 'http://data.fixer.io/api/latest', method: 'GET' },
426
- currencylayer_live: { url: 'http://api.currencylayer.com/live', method: 'GET' },
427
- currencylayer_convert: { url: 'http://api.currencylayer.com/convert', method: 'GET' },
428
- coinlayer_live: { url: 'http://api.coinlayer.com/live', method: 'GET' },
429
- exchangeratehost_latest: { url: 'https://api.exchangerate.host/live', method: 'GET' },
430
-
431
- // GEOLOCATION
432
- weatherstack_current: { url: 'http://api.weatherstack.com/current', method: 'GET' },
433
- weatherstack_forecast: { url: 'http://api.weatherstack.com/forecast', method: 'GET' },
434
- ipstack_lookup: { url: 'http://api.ipstack.com/{ip}', method: 'GET' },
435
- ipapi_lookup: { url: 'http://api.ipapi.com/{ip}', method: 'GET' },
436
- positionstack_forward: { url: 'http://api.positionstack.com/v1/forward', method: 'GET' },
437
- positionstack_reverse: { url: 'http://api.positionstack.com/v1/reverse', method: 'GET' },
438
- languagelayer_detect: { url: 'http://api.languagelayer.com/detect', method: 'GET' },
439
-
440
- // SCRAPING
441
- scrapestack_scrape: { url: 'http://api.scrapestack.com/scrape', method: 'GET' },
442
- serpstack_search: { url: 'http://api.serpstack.com/search', method: 'GET' },
443
-
444
- // NEWS
445
- mediastack_news: { url: 'http://api.mediastack.com/v1/news', method: 'GET' },
446
-
447
- // DEVTOOLS
448
- userstack_detect: { url: 'http://api.userstack.com/detect', method: 'GET' },
449
- },
450
- };
451
-
452
- /**
453
- * Generate a dry-run result showing what would be sent without making actual API calls
454
- */
455
- export function generateDryRun(
456
- providerId: string,
457
- action: string,
458
- params: Record<string, any>
459
- ): DryRunResult {
460
- const endpoint = apiEndpoints[providerId]?.[action] || { url: 'unknown', method: 'POST' };
461
- const mockGen = mockResponses[providerId]?.[action];
462
- const mockData = mockGen ? mockGen(params) : { message: 'Mock response for ' + action };
463
-
464
- // Build what would be sent
465
- const headers: Record<string, string> = {
466
- 'Content-Type': 'application/json',
467
- };
468
-
469
- // Add auth header hints
470
- if (['46elks', 'twilio'].includes(providerId)) {
471
- headers['Authorization'] = 'Basic [base64(username:password)]';
472
- } else if (['brave_search'].includes(providerId)) {
473
- headers['X-Subscription-Token'] = '[API_KEY]';
474
- } else if (['resend', 'openrouter', 'replicate', 'firecrawl'].includes(providerId)) {
475
- headers['Authorization'] = 'Bearer [API_KEY]';
476
- } else if (providerId === 'elevenlabs') {
477
- headers['xi-api-key'] = '[API_KEY]';
478
- } else if (providerId === 'github') {
479
- headers['Authorization'] = 'Bearer [GITHUB_TOKEN]';
480
- headers['User-Agent'] = 'APIClaw';
481
- }
482
-
483
- // Determine body based on method
484
- let body: unknown = undefined;
485
- if (endpoint.method === 'POST') {
486
- if (providerId === '46elks') {
487
- body = { from: params.from || 'APIClaw', to: params.to, message: params.message };
488
- } else if (providerId === 'twilio') {
489
- body = { From: params.from, To: params.to, Body: params.message };
490
- } else {
491
- body = params;
492
- }
493
- }
494
-
495
- const notes: string[] = [
496
- '⚠️ DRY-RUN MODE: No actual API call was made',
497
- 'This shows what WOULD be sent if you remove dry_run: true',
498
- ];
499
-
500
- if (endpoint.estimatedCost) {
501
- notes.push(`Estimated cost: ${endpoint.estimatedCost}`);
502
- }
503
-
504
- return {
505
- dry_run: true,
506
- provider: providerId,
507
- action,
508
- would_send: {
509
- url: endpoint.url,
510
- method: endpoint.method,
511
- headers,
512
- ...(body ? { body } : {}),
513
- },
514
- mock_response: {
515
- success: true,
516
- data: mockData,
517
- ...(endpoint.estimatedCost ? { estimated_cost: endpoint.estimatedCost } : {}),
518
- },
519
- notes,
520
- };
521
- }
522
-
523
- /**
524
- * Create a structured error result with error code
525
- */
526
- function createErrorResult(
527
- provider: string,
528
- action: string,
529
- error: string,
530
- code: ErrorCode,
531
- status?: number
532
- ): ExecuteResult {
533
- return {
534
- success: false,
535
- provider,
536
- action,
537
- error,
538
- code,
539
- };
540
- }
541
-
542
- // Helper to safely access properties
543
- function safeGet(obj: unknown, ...keys: string[]): unknown {
544
- let current: unknown = obj;
545
- for (const key of keys) {
546
- if (current && typeof current === 'object' && key in current) {
547
- current = (current as Record<string, unknown>)[key];
548
- } else {
549
- return undefined;
550
- }
551
- }
552
- return current;
553
- }
554
-
555
- // Provider action handlers
556
- const handlers: Record<string, Record<string, (params: any, creds: any) => Promise<ExecuteResult>>> = {
557
-
558
- // 46elks - Swedish SMS/Voice
559
- '46elks': {
560
- send_sms: async (params, creds) => {
561
- const { to, message, from = 'APIClaw' } = params;
562
-
563
- if (!to || !message) {
564
- return createErrorResult('46elks', 'send_sms', 'Missing required params: to, message', ERROR_CODES.INVALID_PARAMS);
565
- }
566
-
567
- const auth = Buffer.from(`${creds.username}:${creds.password}`).toString('base64');
568
-
569
- const response = await fetchWithRetry('https://api.46elks.com/a1/sms', {
570
- method: 'POST',
571
- headers: {
572
- 'Authorization': `Basic ${auth}`,
573
- 'Content-Type': 'application/x-www-form-urlencoded',
574
- },
575
- body: new URLSearchParams({ from, to, message }),
576
- }, { provider: '46elks', action: 'send_sms' });
577
-
578
- const data = await response.json() as Record<string, unknown>;
579
-
580
- if (!response.ok) {
581
- return createErrorResult('46elks', 'send_sms', (data.message as string) || 'SMS failed', statusToErrorCode(response.status));
582
- }
583
-
584
- return {
585
- success: true,
586
- provider: '46elks',
587
- action: 'send_sms',
588
- data: { id: data.id, to: data.to, cost: data.cost },
589
- cost: parseInt(String(data.cost)) / 10000000 // Convert microöre to SEK
590
- };
591
- },
592
- },
593
-
594
- // Twilio - Global SMS/Voice
595
- twilio: {
596
- send_sms: async (params, creds) => {
597
- const { to, message, from } = params;
598
-
599
- if (!to || !message) {
600
- return createErrorResult('twilio', 'send_sms', 'Missing required params: to, message', ERROR_CODES.INVALID_PARAMS);
601
- }
602
-
603
- const auth = Buffer.from(`${creds.username}:${creds.password}`).toString('base64');
604
- const fromNumber = from || creds.from_number || '+15017122661';
605
-
606
- const response = await fetchWithRetry(
607
- `https://api.twilio.com/2010-04-01/Accounts/${creds.username}/Messages.json`,
608
- {
609
- method: 'POST',
610
- headers: {
611
- 'Authorization': `Basic ${auth}`,
612
- 'Content-Type': 'application/x-www-form-urlencoded',
613
- },
614
- body: new URLSearchParams({ From: fromNumber, To: to, Body: message }),
615
- },
616
- { provider: 'twilio', action: 'send_sms' }
617
- );
618
-
619
- const data = await response.json() as Record<string, unknown>;
620
-
621
- if (!response.ok) {
622
- return createErrorResult('twilio', 'send_sms', (data.message as string) || 'SMS failed', statusToErrorCode(response.status));
623
- }
624
-
625
- return {
626
- success: true,
627
- provider: 'twilio',
628
- action: 'send_sms',
629
- data: { sid: data.sid, to: data.to, status: data.status }
630
- };
631
- },
632
- },
633
-
634
- // Brave Search
635
- brave_search: {
636
- search: async (params, creds) => {
637
- const { query, count = 5 } = params;
638
-
639
- if (!query) {
640
- return createErrorResult('brave_search', 'search', 'Missing required param: query', ERROR_CODES.INVALID_PARAMS);
641
- }
642
-
643
- const url = new URL('https://api.search.brave.com/res/v1/web/search');
644
- url.searchParams.set('q', query);
645
- url.searchParams.set('count', count.toString());
646
-
647
- const response = await fetchWithRetry(url.toString(), {
648
- headers: { 'X-Subscription-Token': creds.api_key },
649
- }, { provider: 'brave_search', action: 'search' });
650
-
651
- const data = await response.json() as Record<string, unknown>;
652
-
653
- if (!response.ok) {
654
- return createErrorResult('brave_search', 'search', (data.message as string) || 'Search failed', statusToErrorCode(response.status));
655
- }
656
-
657
- const webData = data.web as Record<string, unknown> | undefined;
658
- const rawResults = (webData?.results as Array<Record<string, unknown>>) || [];
659
- const results = rawResults.map((r) => ({
660
- title: r.title,
661
- url: r.url,
662
- description: r.description,
663
- }));
664
-
665
- return {
666
- success: true,
667
- provider: 'brave_search',
668
- action: 'search',
669
- data: { query, results, total: results.length }
670
- };
671
- },
672
- },
673
-
674
- // Resend - Email
675
- resend: {
676
- send_email: async (params, creds) => {
677
- const { to, subject, html, text, from = 'APIClaw <noreply@apiclaw.cloud>' } = params;
678
-
679
- if (!to || !subject || (!html && !text)) {
680
- return createErrorResult('resend', 'send_email', 'Missing required params: to, subject, html or text', ERROR_CODES.INVALID_PARAMS);
681
- }
682
-
683
- const response = await fetchWithRetry('https://api.resend.com/emails', {
684
- method: 'POST',
685
- headers: {
686
- 'Authorization': `Bearer ${creds.api_key}`,
687
- 'Content-Type': 'application/json',
688
- },
689
- body: JSON.stringify({ from, to, subject, html, text }),
690
- }, { provider: 'resend', action: 'send_email' });
691
-
692
- const data = await response.json() as Record<string, unknown>;
693
-
694
- if (!response.ok) {
695
- return createErrorResult('resend', 'send_email', (data.message as string) || 'Email failed', statusToErrorCode(response.status));
696
- }
697
-
698
- return {
699
- success: true,
700
- provider: 'resend',
701
- action: 'send_email',
702
- data: { id: data.id }
703
- };
704
- },
705
- },
706
-
707
- // OpenRouter - AI Models
708
- openrouter: {
709
- chat: async (params, creds) => {
710
- const { messages, model = 'anthropic/claude-3-haiku', max_tokens = 1000 } = params;
711
-
712
- if (!messages || !Array.isArray(messages)) {
713
- return createErrorResult('openrouter', 'chat', 'Missing required param: messages (array)', ERROR_CODES.INVALID_PARAMS);
714
- }
715
-
716
- const response = await fetchWithRetry('https://openrouter.ai/api/v1/chat/completions', {
717
- method: 'POST',
718
- headers: {
719
- 'Authorization': `Bearer ${creds.api_key}`,
720
- 'Content-Type': 'application/json',
721
- 'HTTP-Referer': 'https://apiclaw.cloud',
722
- },
723
- body: JSON.stringify({ model, messages, max_tokens }),
724
- }, { provider: 'openrouter', action: 'chat' });
725
-
726
- const data = await response.json() as Record<string, unknown>;
727
-
728
- if (!response.ok) {
729
- const errorData = data.error as Record<string, unknown> | undefined;
730
- return createErrorResult('openrouter', 'chat', (errorData?.message as string) || 'Chat failed', statusToErrorCode(response.status));
731
- }
732
-
733
- const choices = data.choices as Array<Record<string, unknown>> | undefined;
734
- const firstChoice = choices?.[0];
735
- const message = firstChoice?.message as Record<string, unknown> | undefined;
736
-
737
- return {
738
- success: true,
739
- provider: 'openrouter',
740
- action: 'chat',
741
- data: {
742
- content: message?.content,
743
- model: data.model,
744
- usage: data.usage
745
- }
746
- };
747
- },
748
- },
749
-
750
- // ElevenLabs - Text-to-Speech
751
- elevenlabs: {
752
- text_to_speech: async (params, creds) => {
753
- const { text, voice_id = '21m00Tcm4TlvDq8ikWAM', model_id = 'eleven_monolingual_v1' } = params;
754
-
755
- if (!text) {
756
- return createErrorResult('elevenlabs', 'text_to_speech', 'Missing required param: text', ERROR_CODES.INVALID_PARAMS);
757
- }
758
-
759
- const response = await fetchWithRetry(
760
- `https://api.elevenlabs.io/v1/text-to-speech/${voice_id}`,
761
- {
762
- method: 'POST',
763
- headers: {
764
- 'xi-api-key': creds.api_key,
765
- 'Content-Type': 'application/json',
766
- },
767
- body: JSON.stringify({ text, model_id }),
768
- },
769
- { provider: 'elevenlabs', action: 'text_to_speech' }
770
- );
771
-
772
- if (!response.ok) {
773
- const error = await response.json().catch(() => ({})) as Record<string, unknown>;
774
- return createErrorResult('elevenlabs', 'text_to_speech', (error.detail as string) || 'TTS failed', statusToErrorCode(response.status));
775
- }
776
-
777
- // Return audio as base64
778
- const buffer = await response.arrayBuffer();
779
- const base64 = Buffer.from(buffer).toString('base64');
780
-
781
- return {
782
- success: true,
783
- provider: 'elevenlabs',
784
- action: 'text_to_speech',
785
- data: {
786
- audio_base64: base64,
787
- format: 'mp3',
788
- text_length: text.length
789
- }
790
- };
791
- },
792
- },
793
-
794
- // Replicate - Run any AI model (images, audio, video, text)
795
- replicate: {
796
- run: async (params, creds) => {
797
- const { model, input } = params;
798
-
799
- if (!model) {
800
- return createErrorResult('replicate', 'run', 'Missing required param: model (e.g., "stability-ai/sdxl:...")', ERROR_CODES.INVALID_PARAMS);
801
- }
802
- if (!input) {
803
- return createErrorResult('replicate', 'run', 'Missing required param: input (object with model inputs)', ERROR_CODES.INVALID_PARAMS);
804
- }
805
-
806
- // Parse model into owner/name and version
807
- const [modelPath, version] = model.split(':');
808
-
809
- // Create prediction
810
- const response = await fetchWithRetry('https://api.replicate.com/v1/predictions', {
811
- method: 'POST',
812
- headers: {
813
- 'Authorization': `Bearer ${creds.api_key}`,
814
- 'Content-Type': 'application/json',
815
- },
816
- body: JSON.stringify({
817
- version: version || undefined,
818
- model: version ? undefined : modelPath,
819
- input,
820
- }),
821
- }, { provider: 'replicate', action: 'run' });
822
-
823
- if (!response.ok) {
824
- const error = await response.json().catch(() => ({})) as Record<string, unknown>;
825
- return createErrorResult('replicate', 'run', (error.detail as string) || 'Prediction failed', statusToErrorCode(response.status));
826
- }
827
-
828
- const prediction = await response.json() as Record<string, unknown>;
829
-
830
- // Poll for completion (max 60 seconds)
831
- let result = prediction;
832
- const startTime = Date.now();
833
- while (result.status === 'starting' || result.status === 'processing') {
834
- if (Date.now() - startTime > 60000) {
835
- return {
836
- success: true,
837
- provider: 'replicate',
838
- action: 'run',
839
- data: {
840
- status: 'pending',
841
- prediction_id: result.id,
842
- message: 'Prediction still running. Use prediction_id to check status.',
843
- urls: result.urls
844
- }
845
- };
846
- }
847
- await sleep(1000);
848
- const pollResponse = await fetchWithRetry((result.urls as Record<string, string>)?.get || `https://api.replicate.com/v1/predictions/${result.id}`, {
849
- headers: { 'Authorization': `Bearer ${creds.api_key}` },
850
- }, { provider: 'replicate', action: 'run_poll' });
851
- result = await pollResponse.json() as Record<string, unknown>;
852
- }
853
-
854
- if (result.status === 'failed') {
855
- return createErrorResult('replicate', 'run', (result.error as string) || 'Prediction failed', ERROR_CODES.PROVIDER_ERROR);
856
- }
857
-
858
- return {
859
- success: true,
860
- provider: 'replicate',
861
- action: 'run',
862
- data: {
863
- status: result.status,
864
- output: result.output,
865
- model: modelPath,
866
- metrics: result.metrics
867
- }
868
- };
869
- },
870
-
871
- list_models: async (_params, creds) => {
872
- const response = await fetchWithRetry('https://api.replicate.com/v1/models', {
873
- headers: { 'Authorization': `Bearer ${creds.api_key}` },
874
- }, { provider: 'replicate', action: 'list_models' });
875
-
876
- if (!response.ok) {
877
- return createErrorResult('replicate', 'list_models', 'Failed to list models', statusToErrorCode(response.status));
878
- }
879
-
880
- const data = await response.json() as Record<string, unknown>;
881
-
882
- return {
883
- success: true,
884
- provider: 'replicate',
885
- action: 'list_models',
886
- data: {
887
- models: data.results,
888
- message: 'Use model owner/name with run action. Popular: stability-ai/sdxl, meta/llama-2-70b-chat, openai/whisper'
889
- }
890
- };
891
- },
892
- },
893
-
894
- // Firecrawl - Web scraping and crawling
895
- firecrawl: {
896
- scrape: async (params, creds) => {
897
- const { url, formats = ['markdown'] } = params;
898
-
899
- if (!url) {
900
- return createErrorResult('firecrawl', 'scrape', 'Missing required param: url', ERROR_CODES.INVALID_PARAMS);
901
- }
902
-
903
- const response = await fetchWithRetry('https://api.firecrawl.dev/v1/scrape', {
904
- method: 'POST',
905
- headers: {
906
- 'Authorization': `Bearer ${creds.api_key}`,
907
- 'Content-Type': 'application/json',
908
- },
909
- body: JSON.stringify({ url, formats }),
910
- }, { provider: 'firecrawl', action: 'scrape' });
911
-
912
- const data = await response.json() as Record<string, unknown>;
913
-
914
- if (!response.ok || !data.success) {
915
- return createErrorResult('firecrawl', 'scrape', (data.error as string) || 'Scrape failed', statusToErrorCode(response.status));
916
- }
917
-
918
- return {
919
- success: true,
920
- provider: 'firecrawl',
921
- action: 'scrape',
922
- data: data.data,
923
- };
924
- },
925
-
926
- crawl: async (params, creds) => {
927
- const { url, limit = 10 } = params;
928
-
929
- if (!url) {
930
- return createErrorResult('firecrawl', 'crawl', 'Missing required param: url', ERROR_CODES.INVALID_PARAMS);
931
- }
932
-
933
- const response = await fetchWithRetry('https://api.firecrawl.dev/v1/crawl', {
934
- method: 'POST',
935
- headers: {
936
- 'Authorization': `Bearer ${creds.api_key}`,
937
- 'Content-Type': 'application/json',
938
- },
939
- body: JSON.stringify({ url, limit }),
940
- }, { provider: 'firecrawl', action: 'crawl' });
941
-
942
- const data = await response.json() as Record<string, unknown>;
943
-
944
- if (!response.ok || !data.success) {
945
- return createErrorResult('firecrawl', 'crawl', (data.error as string) || 'Crawl failed', statusToErrorCode(response.status));
946
- }
947
-
948
- return {
949
- success: true,
950
- provider: 'firecrawl',
951
- action: 'crawl',
952
- data: { id: data.id, status: 'started', message: 'Crawl job started. Poll status with crawl_status action.' },
953
- };
954
- },
955
-
956
- map: async (params, creds) => {
957
- const { url } = params;
958
-
959
- if (!url) {
960
- return createErrorResult('firecrawl', 'map', 'Missing required param: url', ERROR_CODES.INVALID_PARAMS);
961
- }
962
-
963
- const response = await fetchWithRetry('https://api.firecrawl.dev/v1/map', {
964
- method: 'POST',
965
- headers: {
966
- 'Authorization': `Bearer ${creds.api_key}`,
967
- 'Content-Type': 'application/json',
968
- },
969
- body: JSON.stringify({ url }),
970
- }, { provider: 'firecrawl', action: 'map' });
971
-
972
- const data = await response.json() as Record<string, unknown>;
973
-
974
- if (!response.ok || !data.success) {
975
- return createErrorResult('firecrawl', 'map', (data.error as string) || 'Map failed', statusToErrorCode(response.status));
976
- }
977
-
978
- return {
979
- success: true,
980
- provider: 'firecrawl',
981
- action: 'map',
982
- data: { links: data.links },
983
- };
984
- },
985
- },
986
-
987
- // GitHub - Code & Repos
988
- github: {
989
- search_repos: async (params, creds) => {
990
- const { query, sort = 'stars', limit = 10 } = params;
991
-
992
- if (!query) {
993
- return createErrorResult('github', 'search_repos', 'Missing required param: query', ERROR_CODES.INVALID_PARAMS);
994
- }
995
-
996
- const response = await fetchWithRetry(`https://api.github.com/search/repositories?q=${encodeURIComponent(query)}&sort=${sort}&per_page=${limit}`, {
997
- headers: {
998
- 'Authorization': `Bearer ${creds.token}`,
999
- 'Accept': 'application/vnd.github+json',
1000
- 'User-Agent': 'APIClaw',
1001
- },
1002
- }, { provider: 'github', action: 'search_repos' });
1003
-
1004
- const data = await response.json() as Record<string, unknown>;
1005
-
1006
- if (!response.ok) {
1007
- return createErrorResult('github', 'search_repos', (data.message as string) || 'Search failed', statusToErrorCode(response.status));
1008
- }
1009
-
1010
- const items = (data.items as any[]) || [];
1011
- return {
1012
- success: true,
1013
- provider: 'github',
1014
- action: 'search_repos',
1015
- data: {
1016
- total: data.total_count,
1017
- repos: items.slice(0, limit).map(r => ({
1018
- name: r.full_name,
1019
- description: r.description,
1020
- stars: r.stargazers_count,
1021
- url: r.html_url,
1022
- language: r.language,
1023
- }))
1024
- },
1025
- };
1026
- },
1027
-
1028
- get_repo: async (params, creds) => {
1029
- const { owner, repo } = params;
1030
-
1031
- if (!owner || !repo) {
1032
- return createErrorResult('github', 'get_repo', 'Missing required params: owner, repo', ERROR_CODES.INVALID_PARAMS);
1033
- }
1034
-
1035
- const response = await fetchWithRetry(`https://api.github.com/repos/${owner}/${repo}`, {
1036
- headers: {
1037
- 'Authorization': `Bearer ${creds.token}`,
1038
- 'Accept': 'application/vnd.github+json',
1039
- 'User-Agent': 'APIClaw',
1040
- },
1041
- }, { provider: 'github', action: 'get_repo' });
1042
-
1043
- const data = await response.json() as Record<string, unknown>;
1044
-
1045
- if (!response.ok) {
1046
- return createErrorResult('github', 'get_repo', (data.message as string) || 'Get repo failed', statusToErrorCode(response.status));
1047
- }
1048
-
1049
- return {
1050
- success: true,
1051
- provider: 'github',
1052
- action: 'get_repo',
1053
- data: {
1054
- name: data.full_name,
1055
- description: data.description,
1056
- stars: data.stargazers_count,
1057
- forks: data.forks_count,
1058
- language: data.language,
1059
- url: data.html_url,
1060
- created: data.created_at,
1061
- updated: data.updated_at,
1062
- },
1063
- };
1064
- },
1065
-
1066
- list_issues: async (params, creds) => {
1067
- const { owner, repo, state = 'open', limit = 10 } = params;
1068
-
1069
- if (!owner || !repo) {
1070
- return createErrorResult('github', 'list_issues', 'Missing required params: owner, repo', ERROR_CODES.INVALID_PARAMS);
1071
- }
1072
-
1073
- const response = await fetchWithRetry(`https://api.github.com/repos/${owner}/${repo}/issues?state=${state}&per_page=${limit}`, {
1074
- headers: {
1075
- 'Authorization': `Bearer ${creds.token}`,
1076
- 'Accept': 'application/vnd.github+json',
1077
- 'User-Agent': 'APIClaw',
1078
- },
1079
- }, { provider: 'github', action: 'list_issues' });
1080
-
1081
- const data = await response.json() as unknown[];
1082
-
1083
- if (!response.ok) {
1084
- return createErrorResult('github', 'list_issues', 'List issues failed', statusToErrorCode(response.status));
1085
- }
1086
-
1087
- return {
1088
- success: true,
1089
- provider: 'github',
1090
- action: 'list_issues',
1091
- data: {
1092
- issues: (data as any[]).map(i => ({
1093
- number: i.number,
1094
- title: i.title,
1095
- state: i.state,
1096
- user: i.user?.login,
1097
- url: i.html_url,
1098
- created: i.created_at,
1099
- }))
1100
- },
1101
- };
1102
- },
1103
-
1104
- create_issue: async (params, creds) => {
1105
- const { owner, repo, title, body = '' } = params;
1106
-
1107
- if (!owner || !repo || !title) {
1108
- return createErrorResult('github', 'create_issue', 'Missing required params: owner, repo, title', ERROR_CODES.INVALID_PARAMS);
1109
- }
1110
-
1111
- const response = await fetchWithRetry(`https://api.github.com/repos/${owner}/${repo}/issues`, {
1112
- method: 'POST',
1113
- headers: {
1114
- 'Authorization': `Bearer ${creds.token}`,
1115
- 'Accept': 'application/vnd.github+json',
1116
- 'User-Agent': 'APIClaw',
1117
- 'Content-Type': 'application/json',
1118
- },
1119
- body: JSON.stringify({ title, body }),
1120
- }, { provider: 'github', action: 'create_issue' });
1121
-
1122
- const data = await response.json() as Record<string, unknown>;
1123
-
1124
- if (!response.ok) {
1125
- return createErrorResult('github', 'create_issue', (data.message as string) || 'Create issue failed', statusToErrorCode(response.status));
1126
- }
1127
-
1128
- return {
1129
- success: true,
1130
- provider: 'github',
1131
- action: 'create_issue',
1132
- data: {
1133
- number: data.number,
1134
- url: data.html_url,
1135
- },
1136
- };
1137
- },
1138
-
1139
- get_file: async (params, creds) => {
1140
- const { owner, repo, path } = params;
1141
-
1142
- if (!owner || !repo || !path) {
1143
- return createErrorResult('github', 'get_file', 'Missing required params: owner, repo, path', ERROR_CODES.INVALID_PARAMS);
1144
- }
1145
-
1146
- const response = await fetchWithRetry(`https://api.github.com/repos/${owner}/${repo}/contents/${path}`, {
1147
- headers: {
1148
- 'Authorization': `Bearer ${creds.token}`,
1149
- 'Accept': 'application/vnd.github+json',
1150
- 'User-Agent': 'APIClaw',
1151
- },
1152
- }, { provider: 'github', action: 'get_file' });
1153
-
1154
- const data = await response.json() as Record<string, unknown>;
1155
-
1156
- if (!response.ok) {
1157
- return createErrorResult('github', 'get_file', (data.message as string) || 'Get file failed', statusToErrorCode(response.status));
1158
- }
1159
-
1160
- // Decode base64 content
1161
- const content = data.content ? Buffer.from(data.content as string, 'base64').toString('utf-8') : null;
1162
-
1163
- return {
1164
- success: true,
1165
- provider: 'github',
1166
- action: 'get_file',
1167
- data: {
1168
- name: data.name,
1169
- path: data.path,
1170
- size: data.size,
1171
- content,
1172
- },
1173
- };
1174
- },
1175
- },
1176
-
1177
- // E2B - Code Sandbox for AI Agents
1178
- // Uses @e2b/code-interpreter SDK
1179
- e2b: {
1180
- run_code: async (params, creds) => {
1181
- const { code, language = 'python' } = params;
1182
-
1183
- if (!code) {
1184
- return createErrorResult('e2b', 'run_code', 'Missing required param: code', ERROR_CODES.INVALID_PARAMS);
1185
- }
1186
-
1187
- try {
1188
- // Dynamic import to avoid issues if SDK not installed
1189
- const { Sandbox } = await import('@e2b/code-interpreter');
1190
-
1191
- // Set API key via env (SDK reads from E2B_API_KEY)
1192
- process.env.E2B_API_KEY = creds.api_key;
1193
-
1194
- const sandbox = await Sandbox.create();
1195
-
1196
- try {
1197
- const execution = await sandbox.runCode(code);
1198
-
1199
- return {
1200
- success: true,
1201
- provider: 'e2b',
1202
- action: 'run_code',
1203
- data: {
1204
- text: execution.text,
1205
- logs: execution.logs,
1206
- results: execution.results,
1207
- },
1208
- };
1209
- } finally {
1210
- await sandbox.kill().catch(() => {});
1211
- }
1212
- } catch (error: any) {
1213
- return createErrorResult('e2b', 'run_code', error.message || 'Code execution failed', ERROR_CODES.PROVIDER_ERROR);
1214
- }
1215
- },
1216
-
1217
- run_shell: async (params, creds) => {
1218
- const { command } = params;
1219
-
1220
- if (!command) {
1221
- return createErrorResult('e2b', 'run_shell', 'Missing required param: command', ERROR_CODES.INVALID_PARAMS);
1222
- }
1223
-
1224
- try {
1225
- const { Sandbox } = await import('@e2b/code-interpreter');
1226
-
1227
- process.env.E2B_API_KEY = creds.api_key;
1228
-
1229
- const sandbox = await Sandbox.create();
1230
-
1231
- try {
1232
- const result = await sandbox.commands.run(command);
1233
-
1234
- return {
1235
- success: true,
1236
- provider: 'e2b',
1237
- action: 'run_shell',
1238
- data: {
1239
- stdout: result.stdout,
1240
- stderr: result.stderr,
1241
- exitCode: result.exitCode,
1242
- },
1243
- };
1244
- } finally {
1245
- await sandbox.kill().catch(() => {});
1246
- }
1247
- } catch (error: any) {
1248
- return createErrorResult('e2b', 'run_shell', error.message || 'Shell execution failed', ERROR_CODES.PROVIDER_ERROR);
1249
- }
1250
- },
1251
- },
1252
-
1253
- // Groq - Ultra-fast LLM inference
1254
- groq: {
1255
- chat: async (params, creds) => {
1256
- const { messages, model = 'llama3-8b-8192', max_tokens = 1024 } = params;
1257
-
1258
- if (!messages || !Array.isArray(messages)) {
1259
- return createErrorResult('groq', 'chat', 'Missing required param: messages (array)', ERROR_CODES.INVALID_PARAMS);
1260
- }
1261
-
1262
- const response = await fetchWithRetry('https://api.groq.com/openai/v1/chat/completions', {
1263
- method: 'POST',
1264
- headers: {
1265
- 'Authorization': `Bearer ${creds.api_key}`,
1266
- 'Content-Type': 'application/json',
1267
- },
1268
- body: JSON.stringify({ model, messages, max_tokens }),
1269
- }, { provider: 'groq', action: 'chat' });
1270
-
1271
- const data = await response.json() as Record<string, unknown>;
1272
-
1273
- if (!response.ok) {
1274
- const err = data.error as Record<string, unknown> | undefined;
1275
- return createErrorResult('groq', 'chat', (err?.message as string) || 'Chat failed', statusToErrorCode(response.status));
1276
- }
1277
-
1278
- const choices = data.choices as Array<Record<string, unknown>> | undefined;
1279
- const message = choices?.[0]?.message as Record<string, unknown> | undefined;
1280
-
1281
- return {
1282
- success: true,
1283
- provider: 'groq',
1284
- action: 'chat',
1285
- data: {
1286
- content: message?.content,
1287
- model: data.model,
1288
- usage: data.usage,
1289
- },
1290
- };
1291
- },
1292
- },
1293
-
1294
- // Deepgram - Speech-to-text transcription
1295
- deepgram: {
1296
- transcribe: async (params, creds) => {
1297
- const { url, model = 'nova-2', language = 'en' } = params;
1298
-
1299
- if (!url) {
1300
- return createErrorResult('deepgram', 'transcribe', 'Missing required param: url (audio file URL)', ERROR_CODES.INVALID_PARAMS);
1301
- }
1302
-
1303
- const response = await fetchWithRetry(`https://api.deepgram.com/v1/listen?model=${model}&language=${language}&smart_format=true`, {
1304
- method: 'POST',
1305
- headers: {
1306
- 'Authorization': `Token ${creds.api_key}`,
1307
- 'Content-Type': 'application/json',
1308
- },
1309
- body: JSON.stringify({ url }),
1310
- }, { provider: 'deepgram', action: 'transcribe' });
1311
-
1312
- const data = await response.json() as Record<string, unknown>;
1313
-
1314
- if (!response.ok) {
1315
- return createErrorResult('deepgram', 'transcribe', (data.err_msg as string) || 'Transcription failed', statusToErrorCode(response.status));
1316
- }
1317
-
1318
- const results = data.results as Record<string, unknown> | undefined;
1319
- const channels = results?.channels as Array<Record<string, unknown>> | undefined;
1320
- const alternatives = channels?.[0]?.alternatives as Array<Record<string, unknown>> | undefined;
1321
- const transcript = alternatives?.[0]?.transcript as string | undefined;
1322
-
1323
- return {
1324
- success: true,
1325
- provider: 'deepgram',
1326
- action: 'transcribe',
1327
- data: {
1328
- transcript,
1329
- confidence: alternatives?.[0]?.confidence,
1330
- duration: (data.metadata as Record<string, unknown> | undefined)?.duration,
1331
- },
1332
- };
1333
- },
1334
- },
1335
-
1336
- // Serper - Google Search API for AI
1337
- serper: {
1338
- search: async (params, creds) => {
1339
- const { query, num = 10, gl = 'us', hl = 'en' } = params;
1340
-
1341
- if (!query) {
1342
- return createErrorResult('serper', 'search', 'Missing required param: query', ERROR_CODES.INVALID_PARAMS);
1343
- }
1344
-
1345
- const response = await fetchWithRetry('https://google.serper.dev/search', {
1346
- method: 'POST',
1347
- headers: {
1348
- 'X-API-KEY': creds.api_key,
1349
- 'Content-Type': 'application/json',
1350
- },
1351
- body: JSON.stringify({ q: query, num, gl, hl }),
1352
- }, { provider: 'serper', action: 'search' });
1353
-
1354
- const data = await response.json() as Record<string, unknown>;
1355
-
1356
- if (!response.ok) {
1357
- return createErrorResult('serper', 'search', (data.message as string) || 'Search failed', statusToErrorCode(response.status));
1358
- }
1359
-
1360
- const organic = (data.organic as Array<Record<string, unknown>>) || [];
1361
-
1362
- return {
1363
- success: true,
1364
- provider: 'serper',
1365
- action: 'search',
1366
- data: {
1367
- query,
1368
- results: organic.map(r => ({
1369
- title: r.title,
1370
- url: r.link,
1371
- snippet: r.snippet,
1372
- position: r.position,
1373
- })),
1374
- total: organic.length,
1375
- answerBox: data.answerBox,
1376
- knowledgeGraph: data.knowledgeGraph,
1377
- },
1378
- };
1379
- },
1380
- },
1381
-
1382
- // Mistral - Open-weight LLMs
1383
- mistral: {
1384
- chat: async (params, creds) => {
1385
- const { messages, model = 'mistral-small-latest', max_tokens = 1024 } = params;
1386
-
1387
- if (!messages || !Array.isArray(messages)) {
1388
- return createErrorResult('mistral', 'chat', 'Missing required param: messages (array)', ERROR_CODES.INVALID_PARAMS);
1389
- }
1390
-
1391
- const response = await fetchWithRetry('https://api.mistral.ai/v1/chat/completions', {
1392
- method: 'POST',
1393
- headers: {
1394
- 'Authorization': `Bearer ${creds.api_key}`,
1395
- 'Content-Type': 'application/json',
1396
- },
1397
- body: JSON.stringify({ model, messages, max_tokens }),
1398
- }, { provider: 'mistral', action: 'chat' });
1399
-
1400
- const data = await response.json() as Record<string, unknown>;
1401
-
1402
- if (!response.ok) {
1403
- const err = data.message as string | undefined;
1404
- return createErrorResult('mistral', 'chat', err || 'Chat failed', statusToErrorCode(response.status));
1405
- }
1406
-
1407
- const choices = data.choices as Array<Record<string, unknown>> | undefined;
1408
- const message = choices?.[0]?.message as Record<string, unknown> | undefined;
1409
-
1410
- return {
1411
- success: true,
1412
- provider: 'mistral',
1413
- action: 'chat',
1414
- data: {
1415
- content: message?.content,
1416
- model: data.model,
1417
- usage: data.usage,
1418
- },
1419
- };
1420
- },
1421
-
1422
- embed: async (params, creds) => {
1423
- const { input, model = 'mistral-embed' } = params;
1424
-
1425
- if (!input) {
1426
- return createErrorResult('mistral', 'embed', 'Missing required param: input (string or array)', ERROR_CODES.INVALID_PARAMS);
1427
- }
1428
-
1429
- const inputs = Array.isArray(input) ? input : [input];
1430
-
1431
- const response = await fetchWithRetry('https://api.mistral.ai/v1/embeddings', {
1432
- method: 'POST',
1433
- headers: {
1434
- 'Authorization': `Bearer ${creds.api_key}`,
1435
- 'Content-Type': 'application/json',
1436
- },
1437
- body: JSON.stringify({ model, input: inputs }),
1438
- }, { provider: 'mistral', action: 'embed' });
1439
-
1440
- const data = await response.json() as Record<string, unknown>;
1441
-
1442
- if (!response.ok) {
1443
- return createErrorResult('mistral', 'embed', (data.message as string) || 'Embedding failed', statusToErrorCode(response.status));
1444
- }
1445
-
1446
- const embedData = data.data as Array<Record<string, unknown>> | undefined;
1447
-
1448
- return {
1449
- success: true,
1450
- provider: 'mistral',
1451
- action: 'embed',
1452
- data: {
1453
- embeddings: embedData?.map(d => d.embedding),
1454
- model: data.model,
1455
- usage: data.usage,
1456
- },
1457
- };
1458
- },
1459
- },
1460
-
1461
- // Cohere - Enterprise NLP and embeddings
1462
- cohere: {
1463
- chat: async (params, creds) => {
1464
- const { message, model = 'command-r', max_tokens = 1024, preamble } = params;
1465
-
1466
- if (!message) {
1467
- return createErrorResult('cohere', 'chat', 'Missing required param: message', ERROR_CODES.INVALID_PARAMS);
1468
- }
1469
-
1470
- const body: Record<string, unknown> = { model, message, max_tokens };
1471
- if (preamble) body.preamble = preamble;
1472
-
1473
- const response = await fetchWithRetry('https://api.cohere.com/v1/chat', {
1474
- method: 'POST',
1475
- headers: {
1476
- 'Authorization': `Bearer ${creds.api_key}`,
1477
- 'Content-Type': 'application/json',
1478
- },
1479
- body: JSON.stringify(body),
1480
- }, { provider: 'cohere', action: 'chat' });
1481
-
1482
- const data = await response.json() as Record<string, unknown>;
1483
-
1484
- if (!response.ok) {
1485
- return createErrorResult('cohere', 'chat', (data.message as string) || 'Chat failed', statusToErrorCode(response.status));
1486
- }
1487
-
1488
- return {
1489
- success: true,
1490
- provider: 'cohere',
1491
- action: 'chat',
1492
- data: {
1493
- content: data.text,
1494
- generation_id: data.generation_id,
1495
- usage: data.meta,
1496
- },
1497
- };
1498
- },
1499
-
1500
- embed: async (params, creds) => {
1501
- const { texts, model = 'embed-english-v3.0', input_type = 'search_document' } = params;
1502
-
1503
- if (!texts || !Array.isArray(texts)) {
1504
- return createErrorResult('cohere', 'embed', 'Missing required param: texts (array of strings)', ERROR_CODES.INVALID_PARAMS);
1505
- }
1506
-
1507
- const response = await fetchWithRetry('https://api.cohere.com/v1/embed', {
1508
- method: 'POST',
1509
- headers: {
1510
- 'Authorization': `Bearer ${creds.api_key}`,
1511
- 'Content-Type': 'application/json',
1512
- },
1513
- body: JSON.stringify({ model, texts, input_type }),
1514
- }, { provider: 'cohere', action: 'embed' });
1515
-
1516
- const data = await response.json() as Record<string, unknown>;
1517
-
1518
- if (!response.ok) {
1519
- return createErrorResult('cohere', 'embed', (data.message as string) || 'Embedding failed', statusToErrorCode(response.status));
1520
- }
1521
-
1522
- return {
1523
- success: true,
1524
- provider: 'cohere',
1525
- action: 'embed',
1526
- data: {
1527
- embeddings: data.embeddings,
1528
- model: data.model,
1529
- },
1530
- };
1531
- },
1532
- },
1533
-
1534
- // Together AI - Open-source model inference
1535
- together: {
1536
- chat: async (params, creds) => {
1537
- const { messages, model = 'meta-llama/Llama-3-8b-chat-hf', max_tokens = 1024 } = params;
1538
-
1539
- if (!messages || !Array.isArray(messages)) {
1540
- return createErrorResult('together', 'chat', 'Missing required param: messages (array)', ERROR_CODES.INVALID_PARAMS);
1541
- }
1542
-
1543
- const response = await fetchWithRetry('https://api.together.xyz/v1/chat/completions', {
1544
- method: 'POST',
1545
- headers: {
1546
- 'Authorization': `Bearer ${creds.api_key}`,
1547
- 'Content-Type': 'application/json',
1548
- },
1549
- body: JSON.stringify({ model, messages, max_tokens }),
1550
- }, { provider: 'together', action: 'chat' });
1551
-
1552
- const data = await response.json() as Record<string, unknown>;
1553
-
1554
- if (!response.ok) {
1555
- const err = data.error as Record<string, unknown> | undefined;
1556
- return createErrorResult('together', 'chat', (err?.message as string) || 'Chat failed', statusToErrorCode(response.status));
1557
- }
1558
-
1559
- const choices = data.choices as Array<Record<string, unknown>> | undefined;
1560
- const message = choices?.[0]?.message as Record<string, unknown> | undefined;
1561
-
1562
- return {
1563
- success: true,
1564
- provider: 'together',
1565
- action: 'chat',
1566
- data: {
1567
- content: message?.content,
1568
- model: data.model,
1569
- usage: data.usage,
1570
- },
1571
- };
1572
- },
1573
- },
1574
-
1575
- // Stability AI - Image generation
1576
- stability: {
1577
- generate_image: async (params, creds) => {
1578
- const { prompt, model = 'stable-diffusion-xl-1024-v1-0', width = 1024, height = 1024, steps = 30 } = params;
1579
-
1580
- if (!prompt) {
1581
- return createErrorResult('stability', 'generate_image', 'Missing required param: prompt', ERROR_CODES.INVALID_PARAMS);
1582
- }
1583
-
1584
- const response = await fetchWithRetry(`https://api.stability.ai/v1/generation/${model}/text-to-image`, {
1585
- method: 'POST',
1586
- headers: {
1587
- 'Authorization': `Bearer ${creds.api_key}`,
1588
- 'Content-Type': 'application/json',
1589
- 'Accept': 'application/json',
1590
- },
1591
- body: JSON.stringify({
1592
- text_prompts: [{ text: prompt, weight: 1 }],
1593
- width,
1594
- height,
1595
- steps,
1596
- samples: 1,
1597
- }),
1598
- }, { provider: 'stability', action: 'generate_image' });
1599
-
1600
- const data = await response.json() as Record<string, unknown>;
1601
-
1602
- if (!response.ok) {
1603
- return createErrorResult('stability', 'generate_image', (data.message as string) || 'Image generation failed', statusToErrorCode(response.status));
1604
- }
1605
-
1606
- const artifacts = data.artifacts as Array<Record<string, unknown>> | undefined;
1607
- const image = artifacts?.[0];
1608
-
1609
- return {
1610
- success: true,
1611
- provider: 'stability',
1612
- action: 'generate_image',
1613
- data: {
1614
- image_base64: image?.base64,
1615
- finish_reason: image?.finishReason,
1616
- seed: image?.seed,
1617
- },
1618
- };
1619
- },
1620
- },
1621
-
1622
- // AssemblyAI - Audio transcription and intelligence
1623
- assemblyai: {
1624
- transcribe: async (params, creds) => {
1625
- const { audio_url, language_code = 'en', speaker_labels = false, sentiment_analysis = false } = params;
1626
-
1627
- if (!audio_url) {
1628
- return createErrorResult('assemblyai', 'transcribe', 'Missing required param: audio_url', ERROR_CODES.INVALID_PARAMS);
1629
- }
1630
-
1631
- // Submit transcription job
1632
- const submitResponse = await fetchWithRetry('https://api.assemblyai.com/v2/transcript', {
1633
- method: 'POST',
1634
- headers: {
1635
- 'Authorization': creds.api_key,
1636
- 'Content-Type': 'application/json',
1637
- },
1638
- body: JSON.stringify({ audio_url, language_code, speaker_labels, sentiment_analysis }),
1639
- }, { provider: 'assemblyai', action: 'transcribe' });
1640
-
1641
- const submitData = await submitResponse.json() as Record<string, unknown>;
1642
-
1643
- if (!submitResponse.ok) {
1644
- return createErrorResult('assemblyai', 'transcribe', (submitData.error as string) || 'Submit failed', statusToErrorCode(submitResponse.status));
1645
- }
1646
-
1647
- const transcriptId = submitData.id as string;
1648
-
1649
- // Poll until complete (max 120 seconds)
1650
- const startTime = Date.now();
1651
- while (Date.now() - startTime < 120000) {
1652
- await sleep(3000);
1653
-
1654
- const pollResponse = await fetchWithRetry(`https://api.assemblyai.com/v2/transcript/${transcriptId}`, {
1655
- headers: { 'Authorization': creds.api_key },
1656
- }, { provider: 'assemblyai', action: 'transcribe_poll' });
1657
-
1658
- const pollData = await pollResponse.json() as Record<string, unknown>;
1659
-
1660
- if (pollData.status === 'completed') {
1661
- return {
1662
- success: true,
1663
- provider: 'assemblyai',
1664
- action: 'transcribe',
1665
- data: {
1666
- transcript: pollData.text,
1667
- words: pollData.words,
1668
- utterances: pollData.utterances,
1669
- sentiment_analysis_results: pollData.sentiment_analysis_results,
1670
- audio_duration: pollData.audio_duration,
1671
- },
1672
- };
1673
- }
1674
-
1675
- if (pollData.status === 'error') {
1676
- return createErrorResult('assemblyai', 'transcribe', (pollData.error as string) || 'Transcription failed', ERROR_CODES.PROVIDER_ERROR);
1677
- }
1678
- }
1679
-
1680
- return {
1681
- success: true,
1682
- provider: 'assemblyai',
1683
- action: 'transcribe',
1684
- data: {
1685
- status: 'processing',
1686
- transcript_id: transcriptId,
1687
- message: 'Transcription still processing. Use transcript_id to poll manually.',
1688
- },
1689
- };
1690
- },
1691
- },
1692
-
1693
- // APILayer - 14 APIs via one provider
1694
- apilayer: {
1695
- // Helper to pick the right key per action
1696
- exchange_rates: async (params, creds) => {
1697
- const key = creds.APILAYER_EXCHANGERATE_KEY || creds.api_key;
1698
- const { base = 'USD', symbols, date } = params;
1699
- const endpoint = date ? 'historical' : 'latest';
1700
- const url = new URL(`https://api.apilayer.com/exchangerates_data/${endpoint}`);
1701
- url.searchParams.set('base', base);
1702
- if (symbols) url.searchParams.set('symbols', symbols);
1703
- if (date) url.searchParams.set('date', date);
1704
-
1705
- const response = await fetchWithRetry(url.toString(), {
1706
- headers: { 'apikey': key },
1707
- }, { provider: 'apilayer', action: 'exchange_rates' });
1708
-
1709
- const data = await response.json() as Record<string, unknown>;
1710
- if (!response.ok) return createErrorResult('apilayer', 'exchange_rates', (data.message as string) || 'Request failed', statusToErrorCode(response.status));
1711
- return { success: true, provider: 'apilayer', action: 'exchange_rates', data };
1712
- },
1713
-
1714
- market_data: async (params, creds) => {
1715
- const key = creds.APILAYER_MARKETSTACK_KEY || creds.api_key;
1716
- const { symbols, date_from, date_to, limit = 10 } = params;
1717
- if (!symbols) return createErrorResult('apilayer', 'market_data', 'Missing required param: symbols', ERROR_CODES.INVALID_PARAMS);
1718
-
1719
- const url = new URL('http://api.marketstack.com/v1/eod');
1720
- url.searchParams.set('access_key', key);
1721
- url.searchParams.set('symbols', symbols);
1722
- url.searchParams.set('limit', limit.toString());
1723
- if (date_from) url.searchParams.set('date_from', date_from);
1724
- if (date_to) url.searchParams.set('date_to', date_to);
1725
-
1726
- const response = await fetchWithRetry(url.toString(), {}, { provider: 'apilayer', action: 'market_data' });
1727
- const data = await response.json() as Record<string, unknown>;
1728
- if (!response.ok) return createErrorResult('apilayer', 'market_data', (data.error as any)?.message || 'Request failed', statusToErrorCode(response.status));
1729
- return { success: true, provider: 'apilayer', action: 'market_data', data };
1730
- },
1731
-
1732
- aviation: async (params, creds) => {
1733
- const key = creds.APILAYER_AVIATIONSTACK_KEY || creds.api_key;
1734
- const { flight_iata, dep_iata, arr_iata, airline_iata } = params;
1735
- const url = new URL('http://api.aviationstack.com/v1/flights');
1736
- url.searchParams.set('access_key', key);
1737
- if (flight_iata) url.searchParams.set('flight_iata', flight_iata);
1738
- if (dep_iata) url.searchParams.set('dep_iata', dep_iata);
1739
- if (arr_iata) url.searchParams.set('arr_iata', arr_iata);
1740
- if (airline_iata) url.searchParams.set('airline_iata', airline_iata);
1741
-
1742
- const response = await fetchWithRetry(url.toString(), {}, { provider: 'apilayer', action: 'aviation' });
1743
- const data = await response.json() as Record<string, unknown>;
1744
- if (!response.ok) return createErrorResult('apilayer', 'aviation', 'Request failed', statusToErrorCode(response.status));
1745
- return { success: true, provider: 'apilayer', action: 'aviation', data };
1746
- },
1747
-
1748
- pdf_generate: async (params, creds) => {
1749
- const key = creds.APILAYER_PDFLAYER_KEY || creds.api_key;
1750
- const { document_url, document_html, page_size = 'A4' } = params;
1751
- if (!document_url && !document_html) return createErrorResult('apilayer', 'pdf_generate', 'Missing: document_url or document_html', ERROR_CODES.INVALID_PARAMS);
1752
-
1753
- // PDFLayer uses access_key as query param (not header)
1754
- const url = new URL('https://api.pdflayer.com/api/convert');
1755
- url.searchParams.set('access_key', key);
1756
- url.searchParams.set('page_size', page_size);
1757
- if (document_url) url.searchParams.set('document_url', document_url);
1758
-
1759
- const fetchOptions: RequestInit = { method: 'GET' };
1760
- // For document_html, use POST with form body
1761
- if (document_html && !document_url) {
1762
- fetchOptions.method = 'POST';
1763
- fetchOptions.headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
1764
- fetchOptions.body = `document_html=${encodeURIComponent(document_html)}`;
1765
- }
1766
-
1767
- const response = await fetchWithRetry(url.toString(), fetchOptions, { provider: 'apilayer', action: 'pdf_generate' });
1768
- const contentType = response.headers.get('content-type') || '';
1769
- if (contentType.includes('application/pdf')) {
1770
- // Return base64-encoded PDF for downstream use
1771
- const buffer = await response.arrayBuffer();
1772
- const base64 = Buffer.from(buffer).toString('base64');
1773
- return { success: true, provider: 'apilayer', action: 'pdf_generate', data: { message: 'PDF generated', content_type: 'application/pdf', size: buffer.byteLength, pdf_base64: base64 } };
1774
- }
1775
- const data = await response.json() as Record<string, unknown>;
1776
- if (!response.ok) return createErrorResult('apilayer', 'pdf_generate', (data.error as any)?.info || 'Request failed', statusToErrorCode(response.status));
1777
- return { success: true, provider: 'apilayer', action: 'pdf_generate', data };
1778
- },
1779
-
1780
- screenshot: async (params, creds) => {
1781
- const key = creds.APILAYER_SCREENSHOTLAYER_KEY || creds.api_key;
1782
- const { url: targetUrl, viewport = '1440x900', fullpage = 0 } = params;
1783
- if (!targetUrl) return createErrorResult('apilayer', 'screenshot', 'Missing required param: url', ERROR_CODES.INVALID_PARAMS);
1784
-
1785
- const url = new URL('https://api.screenshotlayer.com/api/capture');
1786
- url.searchParams.set('access_key', key);
1787
- url.searchParams.set('url', targetUrl);
1788
- url.searchParams.set('viewport', viewport);
1789
- url.searchParams.set('fullpage', fullpage.toString());
1790
-
1791
- const response = await fetchWithRetry(url.toString(), {}, { provider: 'apilayer', action: 'screenshot' });
1792
- const contentType = response.headers.get('content-type') || '';
1793
- if (contentType.includes('image/')) {
1794
- return { success: true, provider: 'apilayer', action: 'screenshot', data: { message: 'Screenshot captured', content_type: contentType, url: url.toString() } };
1795
- }
1796
- const data = await response.json() as Record<string, unknown>;
1797
- if (!response.ok) return createErrorResult('apilayer', 'screenshot', 'Request failed', statusToErrorCode(response.status));
1798
- return { success: true, provider: 'apilayer', action: 'screenshot', data };
1799
- },
1800
-
1801
- verify_email: async (params, creds) => {
1802
- const key = creds.APILAYER_EMAILVERIFY_KEY || creds.api_key;
1803
- const { email } = params;
1804
- if (!email) return createErrorResult('apilayer', 'verify_email', 'Missing required param: email', ERROR_CODES.INVALID_PARAMS);
1805
-
1806
- const url = new URL('https://api.apilayer.com/email_verification/check');
1807
- url.searchParams.set('email', email);
1808
-
1809
- const response = await fetchWithRetry(url.toString(), {
1810
- headers: { 'apikey': key },
1811
- }, { provider: 'apilayer', action: 'verify_email' });
1812
- const data = await response.json() as Record<string, unknown>;
1813
- if (!response.ok) return createErrorResult('apilayer', 'verify_email', 'Request failed', statusToErrorCode(response.status));
1814
- return { success: true, provider: 'apilayer', action: 'verify_email', data };
1815
- },
1816
-
1817
- verify_number: async (params, creds) => {
1818
- const key = creds.APILAYER_NUMVERIFY_KEY || creds.api_key;
1819
- const { number } = params;
1820
- if (!number) return createErrorResult('apilayer', 'verify_number', 'Missing required param: number', ERROR_CODES.INVALID_PARAMS);
1821
-
1822
- const url = new URL('https://api.apilayer.com/number_verification/validate');
1823
- url.searchParams.set('number', number);
1824
-
1825
- const response = await fetchWithRetry(url.toString(), {
1826
- headers: { 'apikey': key },
1827
- }, { provider: 'apilayer', action: 'verify_number' });
1828
- const data = await response.json() as Record<string, unknown>;
1829
- if (!response.ok) return createErrorResult('apilayer', 'verify_number', 'Request failed', statusToErrorCode(response.status));
1830
- return { success: true, provider: 'apilayer', action: 'verify_number', data };
1831
- },
1832
-
1833
- vat_check: async (params, creds) => {
1834
- const key = creds.APILAYER_VATLAYER_KEY || creds.api_key;
1835
- const { vat_number } = params;
1836
- if (!vat_number) return createErrorResult('apilayer', 'vat_check', 'Missing required param: vat_number', ERROR_CODES.INVALID_PARAMS);
1837
-
1838
- const url = new URL('http://apilayer.net/api/validate');
1839
- url.searchParams.set('access_key', key);
1840
- url.searchParams.set('vat_number', vat_number);
1841
-
1842
- const response = await fetchWithRetry(url.toString(), {}, { provider: 'apilayer', action: 'vat_check' });
1843
- const data = await response.json() as Record<string, unknown>;
1844
- if (!response.ok) return createErrorResult('apilayer', 'vat_check', 'Request failed', statusToErrorCode(response.status));
1845
- return { success: true, provider: 'apilayer', action: 'vat_check', data };
1846
- },
1847
-
1848
- world_news: async (params, creds) => {
1849
- const key = creds.APILAYER_WORLDNEWS_KEY || creds.api_key;
1850
- const { url: newsUrl, analyze = true } = params;
1851
- if (!newsUrl) return createErrorResult('apilayer', 'world_news', 'Missing required param: url', ERROR_CODES.INVALID_PARAMS);
1852
-
1853
- const url = new URL('https://api.apilayer.com/world_news/extract-news');
1854
- url.searchParams.set('url', newsUrl);
1855
- url.searchParams.set('analyze', analyze ? 'true' : 'false');
1856
-
1857
- const response = await fetchWithRetry(url.toString(), {
1858
- headers: { 'apikey': key },
1859
- }, { provider: 'apilayer', action: 'world_news' });
1860
- const data = await response.json() as Record<string, unknown>;
1861
- if (!response.ok) return createErrorResult('apilayer', 'world_news', 'Request failed', statusToErrorCode(response.status));
1862
- return { success: true, provider: 'apilayer', action: 'world_news', data };
1863
- },
1864
-
1865
- finance_news: async (params, creds) => {
1866
- const key = creds.APILAYER_FINANCENEWS_KEY || creds.api_key;
1867
- const { tickers, text, number = 5 } = params;
1868
-
1869
- const url = new URL('https://api.apilayer.com/financelayer/news');
1870
- if (tickers) url.searchParams.set('tickers', tickers);
1871
- if (text) url.searchParams.set('keywords', text);
1872
- url.searchParams.set('limit', number.toString());
1873
-
1874
- const response = await fetchWithRetry(url.toString(), {
1875
- headers: { 'apikey': key },
1876
- }, { provider: 'apilayer', action: 'finance_news' });
1877
- const data = await response.json() as Record<string, unknown>;
1878
- if (!response.ok) return createErrorResult('apilayer', 'finance_news', 'Request failed', statusToErrorCode(response.status));
1879
- return { success: true, provider: 'apilayer', action: 'finance_news', data };
1880
- },
1881
-
1882
- scrape: async (params, creds) => {
1883
- const key = creds.APILAYER_SCRAPER_KEY || creds.api_key;
1884
- const { url: targetUrl } = params;
1885
- if (!targetUrl) return createErrorResult('apilayer', 'scrape', 'Missing required param: url', ERROR_CODES.INVALID_PARAMS);
1886
-
1887
- const url = new URL('https://api.apilayer.com/adv_scraper/scraper');
1888
- url.searchParams.set('url', targetUrl);
1889
-
1890
- const response = await fetchWithRetry(url.toString(), {
1891
- headers: { 'apikey': key },
1892
- }, { provider: 'apilayer', action: 'scrape' });
1893
- const data = await response.json() as Record<string, unknown>;
1894
- if (!response.ok) return createErrorResult('apilayer', 'scrape', 'Request failed', statusToErrorCode(response.status));
1895
- return { success: true, provider: 'apilayer', action: 'scrape', data };
1896
- },
1897
-
1898
- image_crop: async (params, creds) => {
1899
- const key = creds.APILAYER_IMAGECROP_KEY || creds.api_key;
1900
- const { url: imageUrl, width, height } = params;
1901
- if (!imageUrl) return createErrorResult('apilayer', 'image_crop', 'Missing required param: url', ERROR_CODES.INVALID_PARAMS);
1902
-
1903
- // APILayer smart_crop expects POST with form data
1904
- const formData = new URLSearchParams();
1905
- formData.set('url', imageUrl);
1906
- if (width) formData.set('width', width.toString());
1907
- if (height) formData.set('height', height.toString());
1908
-
1909
- const response = await fetchWithRetry('https://api.apilayer.com/smart_crop/url', {
1910
- method: 'POST',
1911
- headers: {
1912
- 'apikey': key,
1913
- 'Content-Type': 'application/x-www-form-urlencoded',
1914
- },
1915
- body: formData.toString(),
1916
- }, { provider: 'apilayer', action: 'image_crop' });
1917
- const contentType = response.headers.get('content-type') || '';
1918
- if (contentType.includes('image/')) {
1919
- return { success: true, provider: 'apilayer', action: 'image_crop', data: { message: 'Image cropped', content_type: contentType } };
1920
- }
1921
- const data = await response.json() as Record<string, unknown>;
1922
- if (!response.ok) return createErrorResult('apilayer', 'image_crop', 'Request failed', statusToErrorCode(response.status));
1923
- return { success: true, provider: 'apilayer', action: 'image_crop', data };
1924
- },
1925
-
1926
- skills: async (params, creds) => {
1927
- // Skills API is on PromptAPI domain, uses unified APILayer key
1928
- const key = creds.APILAYER_SKILLAPI_KEY || creds.APILAYER_EXCHANGERATE_KEY || creds.api_key;
1929
- const { q } = params;
1930
- if (!q) return createErrorResult('apilayer', 'skills', 'Missing required param: q', ERROR_CODES.INVALID_PARAMS);
1931
-
1932
- const url = new URL('https://api.promptapi.com/skills');
1933
- url.searchParams.set('q', q);
1934
- if (params.count) url.searchParams.set('count', String(params.count));
1935
-
1936
- const response = await fetchWithRetry(url.toString(), {
1937
- headers: { 'apikey': key },
1938
- }, { provider: 'apilayer', action: 'skills' });
1939
- const data = await response.json() as Record<string, unknown>;
1940
- if (!response.ok) return createErrorResult('apilayer', 'skills', 'Request failed', statusToErrorCode(response.status));
1941
- return { success: true, provider: 'apilayer', action: 'skills', data };
1942
- },
1943
-
1944
- form_submit: async (params, creds) => {
1945
- const key = creds.APILAYER_FORMAPI_KEY || creds.api_key;
1946
- const { endpoint, data: formData } = params;
1947
- if (!endpoint) return createErrorResult('apilayer', 'form_submit', 'Missing required param: endpoint', ERROR_CODES.INVALID_PARAMS);
1948
-
1949
- const response = await fetchWithRetry(`https://api.apilayer.com/form_api/${endpoint}`, {
1950
- method: 'POST',
1951
- headers: {
1952
- 'apikey': key,
1953
- 'Content-Type': 'application/json',
1954
- },
1955
- body: JSON.stringify(formData || {}),
1956
- }, { provider: 'apilayer', 'action': 'form_submit' });
1957
- const data = await response.json() as Record<string, unknown>;
1958
- if (!response.ok) return createErrorResult('apilayer', 'form_submit', 'Request failed', statusToErrorCode(response.status));
1959
- return { success: true, provider: 'apilayer', action: 'form_submit', data };
1960
- },
1961
-
1962
- // ========== LEGACY APIs (separate domains, each with own key) ==========
1963
-
1964
- // FINANCE
1965
- fixer_convert: async (params, creds) => {
1966
- const key = creds.FIXER_API_KEY || creds.api_key;
1967
- const { from, to, amount, date } = params;
1968
- if (!from || !to || !amount) return createErrorResult('apilayer', 'fixer_convert', 'Missing required params: from, to, amount', ERROR_CODES.INVALID_PARAMS);
1969
-
1970
- const url = new URL('http://data.fixer.io/api/convert');
1971
- url.searchParams.set('access_key', key);
1972
- url.searchParams.set('from', from);
1973
- url.searchParams.set('to', to);
1974
- url.searchParams.set('amount', amount.toString());
1975
- if (date) url.searchParams.set('date', date);
1976
-
1977
- const response = await fetchWithRetry(url.toString(), {}, { provider: 'apilayer', action: 'fixer_convert' });
1978
- const data = await response.json() as Record<string, unknown>;
1979
- if (!response.ok) return createErrorResult('apilayer', 'fixer_convert', 'Request failed', statusToErrorCode(response.status));
1980
- return { success: true, provider: 'apilayer', action: 'fixer_convert', data };
1981
- },
1982
-
1983
- fixer_latest: async (params, creds) => {
1984
- const key = creds.FIXER_API_KEY || creds.api_key;
1985
- const { base = 'EUR', symbols } = params;
1986
-
1987
- const url = new URL('http://data.fixer.io/api/latest');
1988
- url.searchParams.set('access_key', key);
1989
- url.searchParams.set('base', base);
1990
- if (symbols) url.searchParams.set('symbols', symbols);
1991
-
1992
- const response = await fetchWithRetry(url.toString(), {}, { provider: 'apilayer', action: 'fixer_latest' });
1993
- const data = await response.json() as Record<string, unknown>;
1994
- if (!response.ok) return createErrorResult('apilayer', 'fixer_latest', 'Request failed', statusToErrorCode(response.status));
1995
- return { success: true, provider: 'apilayer', action: 'fixer_latest', data };
1996
- },
1997
-
1998
- currencylayer_live: async (params, creds) => {
1999
- const key = creds.CURRENCYLAYER_API_KEY || creds.api_key;
2000
- const { currencies, source = 'USD' } = params;
2001
-
2002
- const url = new URL('http://api.currencylayer.com/live');
2003
- url.searchParams.set('access_key', key);
2004
- url.searchParams.set('source', source);
2005
- if (currencies) url.searchParams.set('currencies', currencies);
2006
-
2007
- const response = await fetchWithRetry(url.toString(), {}, { provider: 'apilayer', action: 'currencylayer_live' });
2008
- const data = await response.json() as Record<string, unknown>;
2009
- if (!response.ok) return createErrorResult('apilayer', 'currencylayer_live', 'Request failed', statusToErrorCode(response.status));
2010
- return { success: true, provider: 'apilayer', action: 'currencylayer_live', data };
2011
- },
2012
-
2013
- currencylayer_convert: async (params, creds) => {
2014
- const key = creds.CURRENCYLAYER_API_KEY || creds.api_key;
2015
- const { from, to, amount, date } = params;
2016
- if (!from || !to || !amount) return createErrorResult('apilayer', 'currencylayer_convert', 'Missing required params: from, to, amount', ERROR_CODES.INVALID_PARAMS);
2017
-
2018
- const url = new URL('http://api.currencylayer.com/convert');
2019
- url.searchParams.set('access_key', key);
2020
- url.searchParams.set('from', from);
2021
- url.searchParams.set('to', to);
2022
- url.searchParams.set('amount', amount.toString());
2023
- if (date) url.searchParams.set('date', date);
2024
-
2025
- const response = await fetchWithRetry(url.toString(), {}, { provider: 'apilayer', action: 'currencylayer_convert' });
2026
- const data = await response.json() as Record<string, unknown>;
2027
- if (!response.ok) return createErrorResult('apilayer', 'currencylayer_convert', 'Request failed', statusToErrorCode(response.status));
2028
- return { success: true, provider: 'apilayer', action: 'currencylayer_convert', data };
2029
- },
2030
-
2031
- coinlayer_live: async (params, creds) => {
2032
- const key = creds.COINLAYER_API_KEY || creds.api_key;
2033
- const { symbols, target = 'USD' } = params;
2034
-
2035
- const url = new URL('http://api.coinlayer.com/live');
2036
- url.searchParams.set('access_key', key);
2037
- url.searchParams.set('target', target);
2038
- if (symbols) url.searchParams.set('symbols', symbols);
2039
-
2040
- const response = await fetchWithRetry(url.toString(), {}, { provider: 'apilayer', action: 'coinlayer_live' });
2041
- const data = await response.json() as Record<string, unknown>;
2042
- if (!response.ok) return createErrorResult('apilayer', 'coinlayer_live', 'Request failed', statusToErrorCode(response.status));
2043
- return { success: true, provider: 'apilayer', action: 'coinlayer_live', data };
2044
- },
2045
-
2046
- exchangeratehost_latest: async (params, creds) => {
2047
- const key = creds.EXCHANGERATEHOST_API_KEY || creds.api_key;
2048
- const { base = 'EUR', symbols } = params;
2049
-
2050
- const url = new URL('https://api.exchangerate.host/live');
2051
- url.searchParams.set('access_key', key);
2052
- url.searchParams.set('base', base);
2053
- if (symbols) url.searchParams.set('symbols', symbols);
2054
-
2055
- const response = await fetchWithRetry(url.toString(), {}, { provider: 'apilayer', action: 'exchangeratehost_latest' });
2056
- const data = await response.json() as Record<string, unknown>;
2057
- if (!response.ok) return createErrorResult('apilayer', 'exchangeratehost_latest', 'Request failed', statusToErrorCode(response.status));
2058
- return { success: true, provider: 'apilayer', action: 'exchangeratehost_latest', data };
2059
- },
2060
-
2061
- // GEOLOCATION
2062
- weatherstack_current: async (params, creds) => {
2063
- const key = creds.WEATHERSTACK_API_KEY || creds.api_key;
2064
- const { query, units = 'm' } = params;
2065
- if (!query) return createErrorResult('apilayer', 'weatherstack_current', 'Missing required param: query (city name or coordinates)', ERROR_CODES.INVALID_PARAMS);
2066
-
2067
- const url = new URL('http://api.weatherstack.com/current');
2068
- url.searchParams.set('access_key', key);
2069
- url.searchParams.set('query', query);
2070
- url.searchParams.set('units', units);
2071
-
2072
- const response = await fetchWithRetry(url.toString(), {}, { provider: 'apilayer', action: 'weatherstack_current' });
2073
- const data = await response.json() as Record<string, unknown>;
2074
- if (!response.ok) return createErrorResult('apilayer', 'weatherstack_current', 'Request failed', statusToErrorCode(response.status));
2075
- return { success: true, provider: 'apilayer', action: 'weatherstack_current', data };
2076
- },
2077
-
2078
- weatherstack_forecast: async (params, creds) => {
2079
- const key = creds.WEATHERSTACK_API_KEY || creds.api_key;
2080
- const { query, forecast_days = 1, units = 'm' } = params;
2081
- if (!query) return createErrorResult('apilayer', 'weatherstack_forecast', 'Missing required param: query', ERROR_CODES.INVALID_PARAMS);
2082
-
2083
- const url = new URL('http://api.weatherstack.com/forecast');
2084
- url.searchParams.set('access_key', key);
2085
- url.searchParams.set('query', query);
2086
- url.searchParams.set('forecast_days', forecast_days.toString());
2087
- url.searchParams.set('units', units);
2088
-
2089
- const response = await fetchWithRetry(url.toString(), {}, { provider: 'apilayer', action: 'weatherstack_forecast' });
2090
- const data = await response.json() as Record<string, unknown>;
2091
- if (!response.ok) return createErrorResult('apilayer', 'weatherstack_forecast', 'Request failed', statusToErrorCode(response.status));
2092
- return { success: true, provider: 'apilayer', action: 'weatherstack_forecast', data };
2093
- },
2094
-
2095
- ipstack_lookup: async (params, creds) => {
2096
- const key = creds.IPSTACK_API_KEY || creds.api_key;
2097
- const { ip } = params;
2098
- if (!ip) return createErrorResult('apilayer', 'ipstack_lookup', 'Missing required param: ip', ERROR_CODES.INVALID_PARAMS);
2099
-
2100
- const url = new URL(`http://api.ipstack.com/${ip}`);
2101
- url.searchParams.set('access_key', key);
2102
-
2103
- const response = await fetchWithRetry(url.toString(), {}, { provider: 'apilayer', action: 'ipstack_lookup' });
2104
- const data = await response.json() as Record<string, unknown>;
2105
- if (!response.ok) return createErrorResult('apilayer', 'ipstack_lookup', 'Request failed', statusToErrorCode(response.status));
2106
- return { success: true, provider: 'apilayer', action: 'ipstack_lookup', data };
2107
- },
2108
-
2109
- ipapi_lookup: async (params, creds) => {
2110
- const key = creds.IPAPI_API_KEY || creds.APILAYER_IPSTACK_KEY || creds.api_key;
2111
- const { ip } = params;
2112
- if (!ip) return createErrorResult('apilayer', 'ipapi_lookup', 'Missing required param: ip', ERROR_CODES.INVALID_PARAMS);
2113
-
2114
- // ipapi uses same format as ipstack — access_key query param
2115
- const url = new URL(`http://api.ipapi.com/${ip}`);
2116
- url.searchParams.set('access_key', key);
2117
-
2118
- const response = await fetchWithRetry(url.toString(), {}, { provider: 'apilayer', action: 'ipapi_lookup' });
2119
- const data = await response.json() as Record<string, unknown>;
2120
- if (!response.ok) return createErrorResult('apilayer', 'ipapi_lookup', 'Request failed', statusToErrorCode(response.status));
2121
- return { success: true, provider: 'apilayer', action: 'ipapi_lookup', data };
2122
- },
2123
-
2124
- positionstack_forward: async (params, creds) => {
2125
- const key = creds.POSITIONSTACK_API_KEY || creds.api_key;
2126
- const { query, limit = 1 } = params;
2127
- if (!query) return createErrorResult('apilayer', 'positionstack_forward', 'Missing required param: query (address)', ERROR_CODES.INVALID_PARAMS);
2128
-
2129
- const url = new URL('http://api.positionstack.com/v1/forward');
2130
- url.searchParams.set('access_key', key);
2131
- url.searchParams.set('query', query);
2132
- url.searchParams.set('limit', limit.toString());
2133
-
2134
- const response = await fetchWithRetry(url.toString(), {}, { provider: 'apilayer', action: 'positionstack_forward' });
2135
- const data = await response.json() as Record<string, unknown>;
2136
- if (!response.ok) return createErrorResult('apilayer', 'positionstack_forward', 'Request failed', statusToErrorCode(response.status));
2137
- return { success: true, provider: 'apilayer', action: 'positionstack_forward', data };
2138
- },
2139
-
2140
- positionstack_reverse: async (params, creds) => {
2141
- const key = creds.POSITIONSTACK_API_KEY || creds.api_key;
2142
- const { query, limit = 1 } = params;
2143
- if (!query) return createErrorResult('apilayer', 'positionstack_reverse', 'Missing required param: query (lat,lng)', ERROR_CODES.INVALID_PARAMS);
2144
-
2145
- const url = new URL('http://api.positionstack.com/v1/reverse');
2146
- url.searchParams.set('access_key', key);
2147
- url.searchParams.set('query', query);
2148
- url.searchParams.set('limit', limit.toString());
2149
-
2150
- const response = await fetchWithRetry(url.toString(), {}, { provider: 'apilayer', action: 'positionstack_reverse' });
2151
- const data = await response.json() as Record<string, unknown>;
2152
- if (!response.ok) return createErrorResult('apilayer', 'positionstack_reverse', 'Request failed', statusToErrorCode(response.status));
2153
- return { success: true, provider: 'apilayer', action: 'positionstack_reverse', data };
2154
- },
2155
-
2156
- languagelayer_detect: async (params, creds) => {
2157
- const key = creds.LANGUAGELAYER_API_KEY || creds.api_key;
2158
- const { query } = params;
2159
- if (!query) return createErrorResult('apilayer', 'languagelayer_detect', 'Missing required param: query (text to analyze)', ERROR_CODES.INVALID_PARAMS);
2160
-
2161
- // Languagelayer uses access_key as query param (like other legacy APIs)
2162
- const url = new URL('http://api.languagelayer.com/detect');
2163
- url.searchParams.set('access_key', key);
2164
- url.searchParams.set('query', query);
2165
-
2166
- const response = await fetchWithRetry(url.toString(), {}, { provider: 'apilayer', action: 'languagelayer_detect' });
2167
- const data = await response.json() as Record<string, unknown>;
2168
- if (!response.ok) return createErrorResult('apilayer', 'languagelayer_detect', 'Request failed', statusToErrorCode(response.status));
2169
- return { success: true, provider: 'apilayer', action: 'languagelayer_detect', data };
2170
- },
2171
-
2172
- // SCRAPING
2173
- scrapestack_scrape: async (params, creds) => {
2174
- const key = creds.SCRAPESTACK_API_KEY || creds.api_key;
2175
- const { url: targetUrl } = params;
2176
- if (!targetUrl) return createErrorResult('apilayer', 'scrapestack_scrape', 'Missing required param: url', ERROR_CODES.INVALID_PARAMS);
2177
-
2178
- const url = new URL('http://api.scrapestack.com/scrape');
2179
- url.searchParams.set('access_key', key);
2180
- url.searchParams.set('url', targetUrl);
2181
-
2182
- const response = await fetchWithRetry(url.toString(), {}, { provider: 'apilayer', action: 'scrapestack_scrape' });
2183
- const data = await response.text();
2184
- if (!response.ok) return createErrorResult('apilayer', 'scrapestack_scrape', 'Request failed', statusToErrorCode(response.status));
2185
- return { success: true, provider: 'apilayer', action: 'scrapestack_scrape', data: { html: data } };
2186
- },
2187
-
2188
- serpstack_search: async (params, creds) => {
2189
- const key = creds.SERPSTACK_API_KEY || creds.api_key;
2190
- const { query, num = 10 } = params;
2191
- if (!query) return createErrorResult('apilayer', 'serpstack_search', 'Missing required param: query', ERROR_CODES.INVALID_PARAMS);
2192
-
2193
- const url = new URL('http://api.serpstack.com/search');
2194
- url.searchParams.set('access_key', key);
2195
- url.searchParams.set('query', query);
2196
- url.searchParams.set('num', num.toString());
2197
-
2198
- const response = await fetchWithRetry(url.toString(), {}, { provider: 'apilayer', action: 'serpstack_search' });
2199
- const data = await response.json() as Record<string, unknown>;
2200
- if (!response.ok) return createErrorResult('apilayer', 'serpstack_search', 'Request failed', statusToErrorCode(response.status));
2201
- return { success: true, provider: 'apilayer', action: 'serpstack_search', data };
2202
- },
2203
-
2204
- // NEWS
2205
- mediastack_news: async (params, creds) => {
2206
- const key = creds.MEDIASTACK_API_KEY || creds.api_key;
2207
- const { keywords, categories, countries, languages, limit = 25 } = params;
2208
-
2209
- const url = new URL('http://api.mediastack.com/v1/news');
2210
- url.searchParams.set('access_key', key);
2211
- if (keywords) url.searchParams.set('keywords', keywords);
2212
- if (categories) url.searchParams.set('categories', categories);
2213
- if (countries) url.searchParams.set('countries', countries);
2214
- if (languages) url.searchParams.set('languages', languages);
2215
- url.searchParams.set('limit', limit.toString());
2216
-
2217
- const response = await fetchWithRetry(url.toString(), {}, { provider: 'apilayer', action: 'mediastack_news' });
2218
- const data = await response.json() as Record<string, unknown>;
2219
- if (!response.ok) return createErrorResult('apilayer', 'mediastack_news', 'Request failed', statusToErrorCode(response.status));
2220
- return { success: true, provider: 'apilayer', action: 'mediastack_news', data };
2221
- },
2222
-
2223
- // DEVTOOLS
2224
- userstack_detect: async (params, creds) => {
2225
- const key = creds.USERSTACK_API_KEY || creds.api_key;
2226
- const { ua } = params;
2227
- if (!ua) return createErrorResult('apilayer', 'userstack_detect', 'Missing required param: ua (user agent string)', ERROR_CODES.INVALID_PARAMS);
2228
-
2229
- const url = new URL('http://api.userstack.com/detect');
2230
- url.searchParams.set('access_key', key);
2231
- url.searchParams.set('ua', ua);
2232
-
2233
- const response = await fetchWithRetry(url.toString(), {}, { provider: 'apilayer', action: 'userstack_detect' });
2234
- const data = await response.json() as Record<string, unknown>;
2235
- if (!response.ok) return createErrorResult('apilayer', 'userstack_detect', 'Request failed', statusToErrorCode(response.status));
2236
- return { success: true, provider: 'apilayer', action: 'userstack_detect', data };
2237
- },
2238
- },
2239
- };
2240
-
2241
- // Get available actions for a provider (static handlers only)
2242
- export function getProviderActions(providerId: string): string[] {
2243
- return Object.keys(handlers[providerId] || {});
2244
- }
2245
-
2246
- // Get available actions for a provider (includes dynamic providers)
2247
- export async function getProviderActionsAsync(providerId: string): Promise<string[]> {
2248
- // First check static handlers
2249
- const staticActions = Object.keys(handlers[providerId] || {});
2250
- if (staticActions.length > 0) {
2251
- return staticActions;
2252
- }
2253
-
2254
- // Then check dynamic providers
2255
- return listDynamicActions(providerId);
2256
- }
2257
-
2258
- // Get all connected providers with their actions (static handlers only)
2259
- // APILayer actions blocked by subscription tier
2260
- const BLOCKED_ACTIONS = ['verify_number', 'world_news', 'image_crop', 'form_submit'];
2261
- const RATE_LIMITED_ACTIONS = ['pdf_generate'];
2262
-
2263
- export function getConnectedProviders(): { provider: string; actions: string[]; blocked?: string[]; rate_limited?: string[] }[] {
2264
- return Object.entries(handlers).map(([provider, actions]) => {
2265
- const allActions = Object.keys(actions);
2266
- if (provider === 'apilayer') {
2267
- const live = allActions.filter(a => !BLOCKED_ACTIONS.includes(a) && !RATE_LIMITED_ACTIONS.includes(a));
2268
- const blocked = allActions.filter(a => BLOCKED_ACTIONS.includes(a));
2269
- const rateLimited = allActions.filter(a => RATE_LIMITED_ACTIONS.includes(a));
2270
- return {
2271
- provider,
2272
- actions: live,
2273
- ...(blocked.length > 0 ? { blocked } : {}),
2274
- ...(rateLimited.length > 0 ? { rate_limited: rateLimited } : {}),
2275
- };
2276
- }
2277
- return { provider, actions: allActions };
2278
- });
2279
- }
2280
-
2281
- // Execute an API call
2282
- export async function executeAPICall(
2283
- providerId: string,
2284
- action: string,
2285
- params: Record<string, any>,
2286
- userId?: string,
2287
- customerKey?: string
2288
- ): Promise<ExecuteResult> {
2289
- // Check for dynamic (self-service) provider config first
2290
- if (userId) {
2291
- const isDynamic = await hasDynamicConfig(providerId);
2292
- if (isDynamic) {
2293
- const dynamicResult = await executeDynamicAction(providerId, action, params, userId, customerKey);
2294
- return normalizeResponse(dynamicResult);
2295
- }
2296
- }
2297
-
2298
- // Fall back to hardcoded handlers
2299
- // Check if provider exists
2300
- const providerHandlers = handlers[providerId];
2301
- if (!providerHandlers) {
2302
- // Check if it might be a dynamic provider without userId
2303
- const dynamicActions = await listDynamicActions(providerId);
2304
- if (dynamicActions.length > 0) {
2305
- return createErrorResult(
2306
- providerId,
2307
- action,
2308
- `Provider '${providerId}' requires userId for dynamic execution. Available actions: ${dynamicActions.join(', ')}`,
2309
- ERROR_CODES.INVALID_PARAMS
2310
- );
2311
- }
2312
- return createErrorResult(
2313
- providerId,
2314
- action,
2315
- `Provider '${providerId}' not connected. Available: ${Object.keys(handlers).join(', ')}`,
2316
- ERROR_CODES.UNKNOWN_PROVIDER
2317
- );
2318
- }
2319
-
2320
- // Check if action exists
2321
- const handler = providerHandlers[action];
2322
- if (!handler) {
2323
- return createErrorResult(
2324
- providerId,
2325
- action,
2326
- `Action '${action}' not available for ${providerId}. Available: ${Object.keys(providerHandlers).join(', ')}`,
2327
- ERROR_CODES.UNKNOWN_ACTION
2328
- );
2329
- }
2330
-
2331
- // Providers that don't require credentials (free/open APIs)
2332
- const NO_CREDS_PROVIDERS = ['coingecko'];
2333
-
2334
- // Get credentials - customer key takes priority, then local secrets, then proxy
2335
- // Set both apiKey and token so it works with different handler patterns (most use apiKey, GitHub uses token)
2336
- let creds = customerKey ? { apiKey: customerKey, api_key: customerKey, token: customerKey, apiSecret: '' } : getCredentials(providerId);
2337
- const usingCustomerKey = !!customerKey;
2338
-
2339
- // For providers that don't need credentials, use empty creds
2340
- if (!creds && NO_CREDS_PROVIDERS.includes(providerId)) {
2341
- creds = { apiKey: '', api_key: '', token: '', apiSecret: '' };
2342
- }
2343
-
2344
- if (!creds) {
2345
- // Try proxy for supported providers
2346
- if (PROXY_PROVIDERS.includes(providerId)) {
2347
- try {
2348
- const proxyResult = await callProxy(providerId, { action, ...params });
2349
- return normalizeResponse({
2350
- success: true,
2351
- provider: providerId,
2352
- action,
2353
- data: proxyResult,
2354
- });
2355
- } catch (e: any) {
2356
- return createErrorResult(providerId, action, e.message || 'Proxy call failed', ERROR_CODES.PROVIDER_ERROR);
2357
- }
2358
- }
2359
- return createErrorResult(
2360
- providerId,
2361
- action,
2362
- `No credentials configured for ${providerId}. Set up ~/.secrets/${providerId}.env`,
2363
- ERROR_CODES.NO_CREDENTIALS
2364
- );
2365
- }
2366
-
2367
- // Execute and normalize response
2368
- try {
2369
- const result = await handler(params, creds);
2370
- return normalizeResponse(result);
2371
- } catch (error: any) {
2372
- // Check if it's a network/timeout error
2373
- const errorMessage = error.message || 'Unknown error';
2374
- let errorCode: ErrorCode = ERROR_CODES.PROVIDER_ERROR;
2375
-
2376
- if (errorMessage.includes('Max retries exceeded')) {
2377
- errorCode = ERROR_CODES.MAX_RETRIES_EXCEEDED;
2378
- } else if (errorMessage.includes('timeout') || errorMessage.includes('ETIMEDOUT')) {
2379
- errorCode = ERROR_CODES.TIMEOUT;
2380
- } else if (errorMessage.includes('ECONNREFUSED') || errorMessage.includes('ENOTFOUND') || errorMessage.includes('fetch')) {
2381
- errorCode = ERROR_CODES.NETWORK_ERROR;
2382
- }
2383
-
2384
- return createErrorResult(providerId, action, errorMessage, errorCode);
2385
- }
2386
- }