@nordsym/apiclaw 2.2.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 (176) 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/gateway-client.d.ts.map +1 -1
  5. package/dist/gateway-client.js +24 -2
  6. package/dist/gateway-client.js.map +1 -1
  7. package/dist/index.bundled.js +61263 -0
  8. package/dist/index.js +2 -2
  9. package/dist/index.js.map +1 -1
  10. package/package.json +7 -2
  11. package/.claude/settings.local.json +0 -13
  12. package/.env.prod +0 -1
  13. package/apiclaw-README.md +0 -494
  14. package/convex/_generated/api.d.ts +0 -145
  15. package/convex/_generated/api.js +0 -23
  16. package/convex/_generated/dataModel.d.ts +0 -60
  17. package/convex/_generated/server.d.ts +0 -143
  18. package/convex/_generated/server.js +0 -93
  19. package/convex/_listWorkspaces.ts +0 -13
  20. package/convex/adminActivate.ts +0 -53
  21. package/convex/adminStats.ts +0 -306
  22. package/convex/agents.ts +0 -939
  23. package/convex/analytics.ts +0 -187
  24. package/convex/apiKeys.ts +0 -220
  25. package/convex/backfillAnalytics.ts +0 -272
  26. package/convex/backfillSearchLogs.ts +0 -35
  27. package/convex/billing.ts +0 -834
  28. package/convex/capabilities.ts +0 -157
  29. package/convex/chains.ts +0 -1318
  30. package/convex/credits.ts +0 -211
  31. package/convex/crons.ts +0 -65
  32. package/convex/debugFilestackLogs.ts +0 -16
  33. package/convex/debugGetToken.ts +0 -18
  34. package/convex/directCall.ts +0 -713
  35. package/convex/earnProgress.ts +0 -753
  36. package/convex/email.ts +0 -329
  37. package/convex/feedback.ts +0 -265
  38. package/convex/funnel.ts +0 -431
  39. package/convex/guards.ts +0 -174
  40. package/convex/http.ts +0 -3756
  41. package/convex/inbound.ts +0 -32
  42. package/convex/logs.ts +0 -701
  43. package/convex/migrateFilestack.ts +0 -81
  44. package/convex/migratePartnersProd.ts +0 -174
  45. package/convex/migratePratham.ts +0 -126
  46. package/convex/migrateProviderWorkspaces.ts +0 -175
  47. package/convex/mou.ts +0 -91
  48. package/convex/nurture.ts +0 -355
  49. package/convex/providerKeys.ts +0 -289
  50. package/convex/providers.ts +0 -1135
  51. package/convex/purchases.ts +0 -183
  52. package/convex/ratelimit.ts +0 -104
  53. package/convex/schema.ts +0 -926
  54. package/convex/searchLogs.ts +0 -265
  55. package/convex/seedAPILayerAPIs.ts +0 -191
  56. package/convex/seedDirectCallConfigs.ts +0 -336
  57. package/convex/seedPratham.ts +0 -149
  58. package/convex/spendAlerts.ts +0 -442
  59. package/convex/stripeActions.ts +0 -607
  60. package/convex/teams.ts +0 -243
  61. package/convex/telemetry.ts +0 -81
  62. package/convex/tsconfig.json +0 -25
  63. package/convex/updateAPIStatus.ts +0 -44
  64. package/convex/usage.ts +0 -260
  65. package/convex/usageReports.ts +0 -357
  66. package/convex/waitlist.ts +0 -55
  67. package/convex/webhooks.ts +0 -494
  68. package/convex/workspaceSettings.ts +0 -143
  69. package/convex/workspaces.ts +0 -1331
  70. package/convex.json +0 -3
  71. package/direct-test.mjs +0 -51
  72. package/email-templates/filestack-provider-outreach.html +0 -162
  73. package/email-templates/partnership-template.html +0 -116
  74. package/email-templates/pratham-draft-preview.txt +0 -57
  75. package/email-templates/pratham-partnership-draft.html +0 -141
  76. package/reports/APIClaw-Session-Report-2026-04-05.pdf +0 -0
  77. package/reports/pipeline/PIPELINE-REPORT.json +0 -153
  78. package/reports/pipeline/acquire_apisguru.json +0 -17
  79. package/reports/pipeline/capabilities.json +0 -38
  80. package/reports/pipeline/discover_azure_recursive.json +0 -1551
  81. package/reports/pipeline/discover_github.json +0 -25
  82. package/reports/pipeline/discover_github_repos.json +0 -49
  83. package/reports/pipeline/discover_swaggerhub.json +0 -24
  84. package/reports/pipeline/discover_well_known.json +0 -23
  85. package/reports/pipeline/fetch_specs.json +0 -19
  86. package/reports/pipeline/generate_providers.json +0 -14
  87. package/reports/pipeline/match_registry.json +0 -11
  88. package/reports/pipeline/parse_specs.json +0 -17
  89. package/reports/pipeline/promote_candidates.json +0 -34
  90. package/reports/pipeline/validate.json +0 -30
  91. package/reports/pipeline/validate_smoke_details.json +0 -3835
  92. package/reports/session-report-2026-04-05.html +0 -433
  93. package/seed-apis-direct.mjs +0 -106
  94. package/src/access-control.ts +0 -174
  95. package/src/adapters/base.ts +0 -364
  96. package/src/adapters/claude-desktop.ts +0 -41
  97. package/src/adapters/cline.ts +0 -88
  98. package/src/adapters/continue.ts +0 -91
  99. package/src/adapters/cursor.ts +0 -43
  100. package/src/adapters/custom.ts +0 -188
  101. package/src/adapters/detect.ts +0 -202
  102. package/src/adapters/index.ts +0 -47
  103. package/src/adapters/windsurf.ts +0 -44
  104. package/src/bin-http.ts +0 -45
  105. package/src/bin.ts +0 -34
  106. package/src/capability-router.ts +0 -331
  107. package/src/chainExecutor.ts +0 -730
  108. package/src/chainResolver.test.ts +0 -246
  109. package/src/chainResolver.ts +0 -658
  110. package/src/cli/commands/demo.ts +0 -109
  111. package/src/cli/commands/doctor.ts +0 -435
  112. package/src/cli/commands/index.ts +0 -9
  113. package/src/cli/commands/login.ts +0 -203
  114. package/src/cli/commands/mcp-install.ts +0 -373
  115. package/src/cli/commands/restore.ts +0 -333
  116. package/src/cli/commands/setup.ts +0 -297
  117. package/src/cli/commands/uninstall.ts +0 -240
  118. package/src/cli/index.ts +0 -148
  119. package/src/cli.ts +0 -370
  120. package/src/confirmation.ts +0 -296
  121. package/src/credentials.ts +0 -455
  122. package/src/credits.ts +0 -329
  123. package/src/crypto.ts +0 -75
  124. package/src/discovery.ts +0 -568
  125. package/src/enterprise/env.ts +0 -156
  126. package/src/enterprise/index.ts +0 -7
  127. package/src/enterprise/script-generator.ts +0 -481
  128. package/src/execute-dynamic.ts +0 -617
  129. package/src/execute.ts +0 -2386
  130. package/src/funnel-client.ts +0 -168
  131. package/src/funnel.test.ts +0 -187
  132. package/src/gateway-client.ts +0 -192
  133. package/src/hivr-whitelist.ts +0 -110
  134. package/src/http-api.ts +0 -286
  135. package/src/http-server-minimal.ts +0 -154
  136. package/src/index.ts +0 -2702
  137. package/src/intelligent-gateway.ts +0 -339
  138. package/src/mcp-analytics.ts +0 -156
  139. package/src/metered.ts +0 -149
  140. package/src/open-apis-generated.ts +0 -157
  141. package/src/open-apis.ts +0 -558
  142. package/src/postinstall.ts +0 -40
  143. package/src/product-whitelist.ts +0 -246
  144. package/src/proxy.ts +0 -36
  145. package/src/registration-guard.ts +0 -117
  146. package/src/session.ts +0 -129
  147. package/src/stripe.ts +0 -497
  148. package/src/telemetry.ts +0 -71
  149. package/src/test.ts +0 -135
  150. package/src/types/convex-api.d.ts +0 -20
  151. package/src/types/convex-api.ts +0 -21
  152. package/src/types.ts +0 -109
  153. package/src/ui/colors.ts +0 -219
  154. package/src/ui/errors.ts +0 -394
  155. package/src/ui/index.ts +0 -17
  156. package/src/ui/prompts.ts +0 -390
  157. package/src/ui/spinner.ts +0 -325
  158. package/src/utils/backup.ts +0 -224
  159. package/src/utils/config.ts +0 -318
  160. package/src/utils/os.ts +0 -124
  161. package/src/utils/paths.ts +0 -203
  162. package/src/webhook.ts +0 -107
  163. package/test-10-working.cjs +0 -97
  164. package/test-14-final.cjs +0 -96
  165. package/test-actual-handlers.ts +0 -92
  166. package/test-apilayer-all-14.ts +0 -249
  167. package/test-apilayer-fixed.ts +0 -248
  168. package/test-direct-endpoints.ts +0 -174
  169. package/test-exact-endpoints.ts +0 -144
  170. package/test-final.ts +0 -83
  171. package/test-full-routing.ts +0 -100
  172. package/test-handlers-correct.ts +0 -217
  173. package/test-numverify-key.ts +0 -41
  174. package/test-via-handlers.ts +0 -92
  175. package/test-worldnews.mjs +0 -26
  176. 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
- }