@nordsym/apiclaw 1.3.12 → 1.3.13
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.
- package/PRD-HARDEN-SHELL.md +272 -0
- package/README.md +72 -0
- package/convex/_generated/api.d.ts +2 -0
- package/convex/crons.ts +11 -0
- package/convex/logs.ts +107 -0
- package/convex/schema.ts +6 -0
- package/convex/spendAlerts.ts +442 -0
- package/convex/workspaces.ts +26 -0
- package/dist/execute.d.ts +1 -0
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +204 -118
- package/dist/execute.js.map +1 -1
- package/landing/package-lock.json +29 -5
- package/landing/package.json +2 -1
- package/landing/src/app/page.tsx +32 -12
- package/landing/src/app/security/page.tsx +381 -0
- package/landing/src/components/AITestimonials.tsx +189 -0
- package/landing/src/lib/stats.json +1 -1
- package/package.json +1 -1
- package/src/execute.ts +250 -114
package/dist/execute.js
CHANGED
|
@@ -4,6 +4,108 @@
|
|
|
4
4
|
import { getCredentials } from './credentials.js';
|
|
5
5
|
import { callProxy, PROXY_PROVIDERS } from './proxy.js';
|
|
6
6
|
import { executeDynamicAction, hasDynamicConfig, listDynamicActions } from './execute-dynamic.js';
|
|
7
|
+
// Error codes for structured error responses
|
|
8
|
+
const ERROR_CODES = {
|
|
9
|
+
RATE_LIMITED: 'RATE_LIMITED',
|
|
10
|
+
SERVICE_UNAVAILABLE: 'SERVICE_UNAVAILABLE',
|
|
11
|
+
UNAUTHORIZED: 'UNAUTHORIZED',
|
|
12
|
+
FORBIDDEN: 'FORBIDDEN',
|
|
13
|
+
NOT_FOUND: 'NOT_FOUND',
|
|
14
|
+
BAD_REQUEST: 'BAD_REQUEST',
|
|
15
|
+
TIMEOUT: 'TIMEOUT',
|
|
16
|
+
NETWORK_ERROR: 'NETWORK_ERROR',
|
|
17
|
+
PROVIDER_ERROR: 'PROVIDER_ERROR',
|
|
18
|
+
INVALID_PARAMS: 'INVALID_PARAMS',
|
|
19
|
+
NO_CREDENTIALS: 'NO_CREDENTIALS',
|
|
20
|
+
UNKNOWN_PROVIDER: 'UNKNOWN_PROVIDER',
|
|
21
|
+
UNKNOWN_ACTION: 'UNKNOWN_ACTION',
|
|
22
|
+
MAX_RETRIES_EXCEEDED: 'MAX_RETRIES_EXCEEDED',
|
|
23
|
+
};
|
|
24
|
+
// Retry configuration
|
|
25
|
+
const RETRY_CONFIG = {
|
|
26
|
+
maxRetries: 3,
|
|
27
|
+
baseDelayMs: 1000, // Start with 1 second
|
|
28
|
+
maxDelayMs: 30000, // Cap at 30 seconds
|
|
29
|
+
retryableStatusCodes: [429, 503, 502, 504], // Rate limit + service unavailable variants
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Calculate exponential backoff delay with jitter
|
|
33
|
+
*/
|
|
34
|
+
function calculateBackoff(attempt) {
|
|
35
|
+
const exponentialDelay = RETRY_CONFIG.baseDelayMs * Math.pow(2, attempt);
|
|
36
|
+
const jitter = Math.random() * 0.3 * exponentialDelay; // 0-30% jitter
|
|
37
|
+
return Math.min(exponentialDelay + jitter, RETRY_CONFIG.maxDelayMs);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Sleep for a given number of milliseconds
|
|
41
|
+
*/
|
|
42
|
+
function sleep(ms) {
|
|
43
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Map HTTP status code to error code
|
|
47
|
+
*/
|
|
48
|
+
function statusToErrorCode(status) {
|
|
49
|
+
switch (status) {
|
|
50
|
+
case 400: return ERROR_CODES.BAD_REQUEST;
|
|
51
|
+
case 401: return ERROR_CODES.UNAUTHORIZED;
|
|
52
|
+
case 403: return ERROR_CODES.FORBIDDEN;
|
|
53
|
+
case 404: return ERROR_CODES.NOT_FOUND;
|
|
54
|
+
case 429: return ERROR_CODES.RATE_LIMITED;
|
|
55
|
+
case 502:
|
|
56
|
+
case 503:
|
|
57
|
+
case 504: return ERROR_CODES.SERVICE_UNAVAILABLE;
|
|
58
|
+
default: return ERROR_CODES.PROVIDER_ERROR;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Check if a response status code is retryable
|
|
63
|
+
*/
|
|
64
|
+
function isRetryableStatus(status) {
|
|
65
|
+
return RETRY_CONFIG.retryableStatusCodes.includes(status);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Fetch with automatic retry for transient failures (429, 503)
|
|
69
|
+
*/
|
|
70
|
+
async function fetchWithRetry(url, options, context) {
|
|
71
|
+
let lastError = null;
|
|
72
|
+
let lastResponse = null;
|
|
73
|
+
for (let attempt = 0; attempt <= RETRY_CONFIG.maxRetries; attempt++) {
|
|
74
|
+
try {
|
|
75
|
+
const response = await fetch(url, options);
|
|
76
|
+
// Check if we should retry
|
|
77
|
+
if (isRetryableStatus(response.status) && attempt < RETRY_CONFIG.maxRetries) {
|
|
78
|
+
lastResponse = response;
|
|
79
|
+
const delay = calculateBackoff(attempt);
|
|
80
|
+
// Check for Retry-After header
|
|
81
|
+
const retryAfter = response.headers.get('Retry-After');
|
|
82
|
+
const retryDelay = retryAfter
|
|
83
|
+
? (parseInt(retryAfter) * 1000 || delay)
|
|
84
|
+
: delay;
|
|
85
|
+
console.log(`[APIClaw] ${context.provider}/${context.action}: Got ${response.status}, retrying in ${Math.round(retryDelay)}ms (attempt ${attempt + 1}/${RETRY_CONFIG.maxRetries})`);
|
|
86
|
+
await sleep(Math.min(retryDelay, RETRY_CONFIG.maxDelayMs));
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
return response;
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
lastError = error;
|
|
93
|
+
// Retry on network errors
|
|
94
|
+
if (attempt < RETRY_CONFIG.maxRetries) {
|
|
95
|
+
const delay = calculateBackoff(attempt);
|
|
96
|
+
console.log(`[APIClaw] ${context.provider}/${context.action}: Network error, retrying in ${Math.round(delay)}ms (attempt ${attempt + 1}/${RETRY_CONFIG.maxRetries})`);
|
|
97
|
+
await sleep(delay);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// If we have a response (even if error status), return it for proper error handling
|
|
103
|
+
if (lastResponse) {
|
|
104
|
+
return lastResponse;
|
|
105
|
+
}
|
|
106
|
+
// All retries exhausted with network errors
|
|
107
|
+
throw lastError || new Error('Max retries exceeded');
|
|
108
|
+
}
|
|
7
109
|
/**
|
|
8
110
|
* Normalize response by extracting common fields to top-level
|
|
9
111
|
* Makes it easier for agents to access key data without digging into provider-specific structures
|
|
@@ -282,6 +384,18 @@ export function generateDryRun(providerId, action, params) {
|
|
|
282
384
|
notes,
|
|
283
385
|
};
|
|
284
386
|
}
|
|
387
|
+
/**
|
|
388
|
+
* Create a structured error result with error code
|
|
389
|
+
*/
|
|
390
|
+
function createErrorResult(provider, action, error, code, status) {
|
|
391
|
+
return {
|
|
392
|
+
success: false,
|
|
393
|
+
provider,
|
|
394
|
+
action,
|
|
395
|
+
error,
|
|
396
|
+
code,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
285
399
|
// Helper to safely access properties
|
|
286
400
|
function safeGet(obj, ...keys) {
|
|
287
401
|
let current = obj;
|
|
@@ -302,20 +416,20 @@ const handlers = {
|
|
|
302
416
|
send_sms: async (params, creds) => {
|
|
303
417
|
const { to, message, from = 'APIClaw' } = params;
|
|
304
418
|
if (!to || !message) {
|
|
305
|
-
return
|
|
419
|
+
return createErrorResult('46elks', 'send_sms', 'Missing required params: to, message', ERROR_CODES.INVALID_PARAMS);
|
|
306
420
|
}
|
|
307
421
|
const auth = Buffer.from(`${creds.username}:${creds.password}`).toString('base64');
|
|
308
|
-
const response = await
|
|
422
|
+
const response = await fetchWithRetry('https://api.46elks.com/a1/sms', {
|
|
309
423
|
method: 'POST',
|
|
310
424
|
headers: {
|
|
311
425
|
'Authorization': `Basic ${auth}`,
|
|
312
426
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
313
427
|
},
|
|
314
428
|
body: new URLSearchParams({ from, to, message }),
|
|
315
|
-
});
|
|
429
|
+
}, { provider: '46elks', action: 'send_sms' });
|
|
316
430
|
const data = await response.json();
|
|
317
431
|
if (!response.ok) {
|
|
318
|
-
return
|
|
432
|
+
return createErrorResult('46elks', 'send_sms', data.message || 'SMS failed', statusToErrorCode(response.status));
|
|
319
433
|
}
|
|
320
434
|
return {
|
|
321
435
|
success: true,
|
|
@@ -331,21 +445,21 @@ const handlers = {
|
|
|
331
445
|
send_sms: async (params, creds) => {
|
|
332
446
|
const { to, message, from } = params;
|
|
333
447
|
if (!to || !message) {
|
|
334
|
-
return
|
|
448
|
+
return createErrorResult('twilio', 'send_sms', 'Missing required params: to, message', ERROR_CODES.INVALID_PARAMS);
|
|
335
449
|
}
|
|
336
450
|
const auth = Buffer.from(`${creds.username}:${creds.password}`).toString('base64');
|
|
337
451
|
const fromNumber = from || creds.from_number || '+15017122661';
|
|
338
|
-
const response = await
|
|
452
|
+
const response = await fetchWithRetry(`https://api.twilio.com/2010-04-01/Accounts/${creds.username}/Messages.json`, {
|
|
339
453
|
method: 'POST',
|
|
340
454
|
headers: {
|
|
341
455
|
'Authorization': `Basic ${auth}`,
|
|
342
456
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
343
457
|
},
|
|
344
458
|
body: new URLSearchParams({ From: fromNumber, To: to, Body: message }),
|
|
345
|
-
});
|
|
459
|
+
}, { provider: 'twilio', action: 'send_sms' });
|
|
346
460
|
const data = await response.json();
|
|
347
461
|
if (!response.ok) {
|
|
348
|
-
return
|
|
462
|
+
return createErrorResult('twilio', 'send_sms', data.message || 'SMS failed', statusToErrorCode(response.status));
|
|
349
463
|
}
|
|
350
464
|
return {
|
|
351
465
|
success: true,
|
|
@@ -360,17 +474,17 @@ const handlers = {
|
|
|
360
474
|
search: async (params, creds) => {
|
|
361
475
|
const { query, count = 5 } = params;
|
|
362
476
|
if (!query) {
|
|
363
|
-
return
|
|
477
|
+
return createErrorResult('brave_search', 'search', 'Missing required param: query', ERROR_CODES.INVALID_PARAMS);
|
|
364
478
|
}
|
|
365
479
|
const url = new URL('https://api.search.brave.com/res/v1/web/search');
|
|
366
480
|
url.searchParams.set('q', query);
|
|
367
481
|
url.searchParams.set('count', count.toString());
|
|
368
|
-
const response = await
|
|
482
|
+
const response = await fetchWithRetry(url.toString(), {
|
|
369
483
|
headers: { 'X-Subscription-Token': creds.api_key },
|
|
370
|
-
});
|
|
484
|
+
}, { provider: 'brave_search', action: 'search' });
|
|
371
485
|
const data = await response.json();
|
|
372
486
|
if (!response.ok) {
|
|
373
|
-
return
|
|
487
|
+
return createErrorResult('brave_search', 'search', data.message || 'Search failed', statusToErrorCode(response.status));
|
|
374
488
|
}
|
|
375
489
|
const webData = data.web;
|
|
376
490
|
const rawResults = webData?.results || [];
|
|
@@ -392,19 +506,19 @@ const handlers = {
|
|
|
392
506
|
send_email: async (params, creds) => {
|
|
393
507
|
const { to, subject, html, text, from = 'APIClaw <noreply@apiclaw.nordsym.com>' } = params;
|
|
394
508
|
if (!to || !subject || (!html && !text)) {
|
|
395
|
-
return
|
|
509
|
+
return createErrorResult('resend', 'send_email', 'Missing required params: to, subject, html or text', ERROR_CODES.INVALID_PARAMS);
|
|
396
510
|
}
|
|
397
|
-
const response = await
|
|
511
|
+
const response = await fetchWithRetry('https://api.resend.com/emails', {
|
|
398
512
|
method: 'POST',
|
|
399
513
|
headers: {
|
|
400
514
|
'Authorization': `Bearer ${creds.api_key}`,
|
|
401
515
|
'Content-Type': 'application/json',
|
|
402
516
|
},
|
|
403
517
|
body: JSON.stringify({ from, to, subject, html, text }),
|
|
404
|
-
});
|
|
518
|
+
}, { provider: 'resend', action: 'send_email' });
|
|
405
519
|
const data = await response.json();
|
|
406
520
|
if (!response.ok) {
|
|
407
|
-
return
|
|
521
|
+
return createErrorResult('resend', 'send_email', data.message || 'Email failed', statusToErrorCode(response.status));
|
|
408
522
|
}
|
|
409
523
|
return {
|
|
410
524
|
success: true,
|
|
@@ -419,9 +533,9 @@ const handlers = {
|
|
|
419
533
|
chat: async (params, creds) => {
|
|
420
534
|
const { messages, model = 'anthropic/claude-3-haiku', max_tokens = 1000 } = params;
|
|
421
535
|
if (!messages || !Array.isArray(messages)) {
|
|
422
|
-
return
|
|
536
|
+
return createErrorResult('openrouter', 'chat', 'Missing required param: messages (array)', ERROR_CODES.INVALID_PARAMS);
|
|
423
537
|
}
|
|
424
|
-
const response = await
|
|
538
|
+
const response = await fetchWithRetry('https://openrouter.ai/api/v1/chat/completions', {
|
|
425
539
|
method: 'POST',
|
|
426
540
|
headers: {
|
|
427
541
|
'Authorization': `Bearer ${creds.api_key}`,
|
|
@@ -429,11 +543,11 @@ const handlers = {
|
|
|
429
543
|
'HTTP-Referer': 'https://apiclaw.nordsym.com',
|
|
430
544
|
},
|
|
431
545
|
body: JSON.stringify({ model, messages, max_tokens }),
|
|
432
|
-
});
|
|
546
|
+
}, { provider: 'openrouter', action: 'chat' });
|
|
433
547
|
const data = await response.json();
|
|
434
548
|
if (!response.ok) {
|
|
435
549
|
const errorData = data.error;
|
|
436
|
-
return
|
|
550
|
+
return createErrorResult('openrouter', 'chat', errorData?.message || 'Chat failed', statusToErrorCode(response.status));
|
|
437
551
|
}
|
|
438
552
|
const choices = data.choices;
|
|
439
553
|
const firstChoice = choices?.[0];
|
|
@@ -455,19 +569,19 @@ const handlers = {
|
|
|
455
569
|
text_to_speech: async (params, creds) => {
|
|
456
570
|
const { text, voice_id = '21m00Tcm4TlvDq8ikWAM', model_id = 'eleven_monolingual_v1' } = params;
|
|
457
571
|
if (!text) {
|
|
458
|
-
return
|
|
572
|
+
return createErrorResult('elevenlabs', 'text_to_speech', 'Missing required param: text', ERROR_CODES.INVALID_PARAMS);
|
|
459
573
|
}
|
|
460
|
-
const response = await
|
|
574
|
+
const response = await fetchWithRetry(`https://api.elevenlabs.io/v1/text-to-speech/${voice_id}`, {
|
|
461
575
|
method: 'POST',
|
|
462
576
|
headers: {
|
|
463
577
|
'xi-api-key': creds.api_key,
|
|
464
578
|
'Content-Type': 'application/json',
|
|
465
579
|
},
|
|
466
580
|
body: JSON.stringify({ text, model_id }),
|
|
467
|
-
});
|
|
581
|
+
}, { provider: 'elevenlabs', action: 'text_to_speech' });
|
|
468
582
|
if (!response.ok) {
|
|
469
583
|
const error = await response.json().catch(() => ({}));
|
|
470
|
-
return
|
|
584
|
+
return createErrorResult('elevenlabs', 'text_to_speech', error.detail || 'TTS failed', statusToErrorCode(response.status));
|
|
471
585
|
}
|
|
472
586
|
// Return audio as base64
|
|
473
587
|
const buffer = await response.arrayBuffer();
|
|
@@ -489,15 +603,15 @@ const handlers = {
|
|
|
489
603
|
run: async (params, creds) => {
|
|
490
604
|
const { model, input } = params;
|
|
491
605
|
if (!model) {
|
|
492
|
-
return
|
|
606
|
+
return createErrorResult('replicate', 'run', 'Missing required param: model (e.g., "stability-ai/sdxl:...")', ERROR_CODES.INVALID_PARAMS);
|
|
493
607
|
}
|
|
494
608
|
if (!input) {
|
|
495
|
-
return
|
|
609
|
+
return createErrorResult('replicate', 'run', 'Missing required param: input (object with model inputs)', ERROR_CODES.INVALID_PARAMS);
|
|
496
610
|
}
|
|
497
611
|
// Parse model into owner/name and version
|
|
498
612
|
const [modelPath, version] = model.split(':');
|
|
499
613
|
// Create prediction
|
|
500
|
-
const response = await
|
|
614
|
+
const response = await fetchWithRetry('https://api.replicate.com/v1/predictions', {
|
|
501
615
|
method: 'POST',
|
|
502
616
|
headers: {
|
|
503
617
|
'Authorization': `Bearer ${creds.api_key}`,
|
|
@@ -508,10 +622,10 @@ const handlers = {
|
|
|
508
622
|
model: version ? undefined : modelPath,
|
|
509
623
|
input,
|
|
510
624
|
}),
|
|
511
|
-
});
|
|
625
|
+
}, { provider: 'replicate', action: 'run' });
|
|
512
626
|
if (!response.ok) {
|
|
513
627
|
const error = await response.json().catch(() => ({}));
|
|
514
|
-
return
|
|
628
|
+
return createErrorResult('replicate', 'run', error.detail || 'Prediction failed', statusToErrorCode(response.status));
|
|
515
629
|
}
|
|
516
630
|
const prediction = await response.json();
|
|
517
631
|
// Poll for completion (max 60 seconds)
|
|
@@ -531,14 +645,14 @@ const handlers = {
|
|
|
531
645
|
}
|
|
532
646
|
};
|
|
533
647
|
}
|
|
534
|
-
await
|
|
535
|
-
const pollResponse = await
|
|
648
|
+
await sleep(1000);
|
|
649
|
+
const pollResponse = await fetchWithRetry(result.urls?.get || `https://api.replicate.com/v1/predictions/${result.id}`, {
|
|
536
650
|
headers: { 'Authorization': `Bearer ${creds.api_key}` },
|
|
537
|
-
});
|
|
651
|
+
}, { provider: 'replicate', action: 'run_poll' });
|
|
538
652
|
result = await pollResponse.json();
|
|
539
653
|
}
|
|
540
654
|
if (result.status === 'failed') {
|
|
541
|
-
return
|
|
655
|
+
return createErrorResult('replicate', 'run', result.error || 'Prediction failed', ERROR_CODES.PROVIDER_ERROR);
|
|
542
656
|
}
|
|
543
657
|
return {
|
|
544
658
|
success: true,
|
|
@@ -553,11 +667,11 @@ const handlers = {
|
|
|
553
667
|
};
|
|
554
668
|
},
|
|
555
669
|
list_models: async (_params, creds) => {
|
|
556
|
-
const response = await
|
|
670
|
+
const response = await fetchWithRetry('https://api.replicate.com/v1/models', {
|
|
557
671
|
headers: { 'Authorization': `Bearer ${creds.api_key}` },
|
|
558
|
-
});
|
|
672
|
+
}, { provider: 'replicate', action: 'list_models' });
|
|
559
673
|
if (!response.ok) {
|
|
560
|
-
return
|
|
674
|
+
return createErrorResult('replicate', 'list_models', 'Failed to list models', statusToErrorCode(response.status));
|
|
561
675
|
}
|
|
562
676
|
const data = await response.json();
|
|
563
677
|
return {
|
|
@@ -576,19 +690,19 @@ const handlers = {
|
|
|
576
690
|
scrape: async (params, creds) => {
|
|
577
691
|
const { url, formats = ['markdown'] } = params;
|
|
578
692
|
if (!url) {
|
|
579
|
-
return
|
|
693
|
+
return createErrorResult('firecrawl', 'scrape', 'Missing required param: url', ERROR_CODES.INVALID_PARAMS);
|
|
580
694
|
}
|
|
581
|
-
const response = await
|
|
695
|
+
const response = await fetchWithRetry('https://api.firecrawl.dev/v1/scrape', {
|
|
582
696
|
method: 'POST',
|
|
583
697
|
headers: {
|
|
584
698
|
'Authorization': `Bearer ${creds.api_key}`,
|
|
585
699
|
'Content-Type': 'application/json',
|
|
586
700
|
},
|
|
587
701
|
body: JSON.stringify({ url, formats }),
|
|
588
|
-
});
|
|
702
|
+
}, { provider: 'firecrawl', action: 'scrape' });
|
|
589
703
|
const data = await response.json();
|
|
590
704
|
if (!response.ok || !data.success) {
|
|
591
|
-
return
|
|
705
|
+
return createErrorResult('firecrawl', 'scrape', data.error || 'Scrape failed', statusToErrorCode(response.status));
|
|
592
706
|
}
|
|
593
707
|
return {
|
|
594
708
|
success: true,
|
|
@@ -600,19 +714,19 @@ const handlers = {
|
|
|
600
714
|
crawl: async (params, creds) => {
|
|
601
715
|
const { url, limit = 10 } = params;
|
|
602
716
|
if (!url) {
|
|
603
|
-
return
|
|
717
|
+
return createErrorResult('firecrawl', 'crawl', 'Missing required param: url', ERROR_CODES.INVALID_PARAMS);
|
|
604
718
|
}
|
|
605
|
-
const response = await
|
|
719
|
+
const response = await fetchWithRetry('https://api.firecrawl.dev/v1/crawl', {
|
|
606
720
|
method: 'POST',
|
|
607
721
|
headers: {
|
|
608
722
|
'Authorization': `Bearer ${creds.api_key}`,
|
|
609
723
|
'Content-Type': 'application/json',
|
|
610
724
|
},
|
|
611
725
|
body: JSON.stringify({ url, limit }),
|
|
612
|
-
});
|
|
726
|
+
}, { provider: 'firecrawl', action: 'crawl' });
|
|
613
727
|
const data = await response.json();
|
|
614
728
|
if (!response.ok || !data.success) {
|
|
615
|
-
return
|
|
729
|
+
return createErrorResult('firecrawl', 'crawl', data.error || 'Crawl failed', statusToErrorCode(response.status));
|
|
616
730
|
}
|
|
617
731
|
return {
|
|
618
732
|
success: true,
|
|
@@ -624,19 +738,19 @@ const handlers = {
|
|
|
624
738
|
map: async (params, creds) => {
|
|
625
739
|
const { url } = params;
|
|
626
740
|
if (!url) {
|
|
627
|
-
return
|
|
741
|
+
return createErrorResult('firecrawl', 'map', 'Missing required param: url', ERROR_CODES.INVALID_PARAMS);
|
|
628
742
|
}
|
|
629
|
-
const response = await
|
|
743
|
+
const response = await fetchWithRetry('https://api.firecrawl.dev/v1/map', {
|
|
630
744
|
method: 'POST',
|
|
631
745
|
headers: {
|
|
632
746
|
'Authorization': `Bearer ${creds.api_key}`,
|
|
633
747
|
'Content-Type': 'application/json',
|
|
634
748
|
},
|
|
635
749
|
body: JSON.stringify({ url }),
|
|
636
|
-
});
|
|
750
|
+
}, { provider: 'firecrawl', action: 'map' });
|
|
637
751
|
const data = await response.json();
|
|
638
752
|
if (!response.ok || !data.success) {
|
|
639
|
-
return
|
|
753
|
+
return createErrorResult('firecrawl', 'map', data.error || 'Map failed', statusToErrorCode(response.status));
|
|
640
754
|
}
|
|
641
755
|
return {
|
|
642
756
|
success: true,
|
|
@@ -651,18 +765,18 @@ const handlers = {
|
|
|
651
765
|
search_repos: async (params, creds) => {
|
|
652
766
|
const { query, sort = 'stars', limit = 10 } = params;
|
|
653
767
|
if (!query) {
|
|
654
|
-
return
|
|
768
|
+
return createErrorResult('github', 'search_repos', 'Missing required param: query', ERROR_CODES.INVALID_PARAMS);
|
|
655
769
|
}
|
|
656
|
-
const response = await
|
|
770
|
+
const response = await fetchWithRetry(`https://api.github.com/search/repositories?q=${encodeURIComponent(query)}&sort=${sort}&per_page=${limit}`, {
|
|
657
771
|
headers: {
|
|
658
772
|
'Authorization': `Bearer ${creds.token}`,
|
|
659
773
|
'Accept': 'application/vnd.github+json',
|
|
660
774
|
'User-Agent': 'APIClaw',
|
|
661
775
|
},
|
|
662
|
-
});
|
|
776
|
+
}, { provider: 'github', action: 'search_repos' });
|
|
663
777
|
const data = await response.json();
|
|
664
778
|
if (!response.ok) {
|
|
665
|
-
return
|
|
779
|
+
return createErrorResult('github', 'search_repos', data.message || 'Search failed', statusToErrorCode(response.status));
|
|
666
780
|
}
|
|
667
781
|
const items = data.items || [];
|
|
668
782
|
return {
|
|
@@ -684,18 +798,18 @@ const handlers = {
|
|
|
684
798
|
get_repo: async (params, creds) => {
|
|
685
799
|
const { owner, repo } = params;
|
|
686
800
|
if (!owner || !repo) {
|
|
687
|
-
return
|
|
801
|
+
return createErrorResult('github', 'get_repo', 'Missing required params: owner, repo', ERROR_CODES.INVALID_PARAMS);
|
|
688
802
|
}
|
|
689
|
-
const response = await
|
|
803
|
+
const response = await fetchWithRetry(`https://api.github.com/repos/${owner}/${repo}`, {
|
|
690
804
|
headers: {
|
|
691
805
|
'Authorization': `Bearer ${creds.token}`,
|
|
692
806
|
'Accept': 'application/vnd.github+json',
|
|
693
807
|
'User-Agent': 'APIClaw',
|
|
694
808
|
},
|
|
695
|
-
});
|
|
809
|
+
}, { provider: 'github', action: 'get_repo' });
|
|
696
810
|
const data = await response.json();
|
|
697
811
|
if (!response.ok) {
|
|
698
|
-
return
|
|
812
|
+
return createErrorResult('github', 'get_repo', data.message || 'Get repo failed', statusToErrorCode(response.status));
|
|
699
813
|
}
|
|
700
814
|
return {
|
|
701
815
|
success: true,
|
|
@@ -716,18 +830,18 @@ const handlers = {
|
|
|
716
830
|
list_issues: async (params, creds) => {
|
|
717
831
|
const { owner, repo, state = 'open', limit = 10 } = params;
|
|
718
832
|
if (!owner || !repo) {
|
|
719
|
-
return
|
|
833
|
+
return createErrorResult('github', 'list_issues', 'Missing required params: owner, repo', ERROR_CODES.INVALID_PARAMS);
|
|
720
834
|
}
|
|
721
|
-
const response = await
|
|
835
|
+
const response = await fetchWithRetry(`https://api.github.com/repos/${owner}/${repo}/issues?state=${state}&per_page=${limit}`, {
|
|
722
836
|
headers: {
|
|
723
837
|
'Authorization': `Bearer ${creds.token}`,
|
|
724
838
|
'Accept': 'application/vnd.github+json',
|
|
725
839
|
'User-Agent': 'APIClaw',
|
|
726
840
|
},
|
|
727
|
-
});
|
|
841
|
+
}, { provider: 'github', action: 'list_issues' });
|
|
728
842
|
const data = await response.json();
|
|
729
843
|
if (!response.ok) {
|
|
730
|
-
return
|
|
844
|
+
return createErrorResult('github', 'list_issues', 'List issues failed', statusToErrorCode(response.status));
|
|
731
845
|
}
|
|
732
846
|
return {
|
|
733
847
|
success: true,
|
|
@@ -748,9 +862,9 @@ const handlers = {
|
|
|
748
862
|
create_issue: async (params, creds) => {
|
|
749
863
|
const { owner, repo, title, body = '' } = params;
|
|
750
864
|
if (!owner || !repo || !title) {
|
|
751
|
-
return
|
|
865
|
+
return createErrorResult('github', 'create_issue', 'Missing required params: owner, repo, title', ERROR_CODES.INVALID_PARAMS);
|
|
752
866
|
}
|
|
753
|
-
const response = await
|
|
867
|
+
const response = await fetchWithRetry(`https://api.github.com/repos/${owner}/${repo}/issues`, {
|
|
754
868
|
method: 'POST',
|
|
755
869
|
headers: {
|
|
756
870
|
'Authorization': `Bearer ${creds.token}`,
|
|
@@ -759,10 +873,10 @@ const handlers = {
|
|
|
759
873
|
'Content-Type': 'application/json',
|
|
760
874
|
},
|
|
761
875
|
body: JSON.stringify({ title, body }),
|
|
762
|
-
});
|
|
876
|
+
}, { provider: 'github', action: 'create_issue' });
|
|
763
877
|
const data = await response.json();
|
|
764
878
|
if (!response.ok) {
|
|
765
|
-
return
|
|
879
|
+
return createErrorResult('github', 'create_issue', data.message || 'Create issue failed', statusToErrorCode(response.status));
|
|
766
880
|
}
|
|
767
881
|
return {
|
|
768
882
|
success: true,
|
|
@@ -777,18 +891,18 @@ const handlers = {
|
|
|
777
891
|
get_file: async (params, creds) => {
|
|
778
892
|
const { owner, repo, path } = params;
|
|
779
893
|
if (!owner || !repo || !path) {
|
|
780
|
-
return
|
|
894
|
+
return createErrorResult('github', 'get_file', 'Missing required params: owner, repo, path', ERROR_CODES.INVALID_PARAMS);
|
|
781
895
|
}
|
|
782
|
-
const response = await
|
|
896
|
+
const response = await fetchWithRetry(`https://api.github.com/repos/${owner}/${repo}/contents/${path}`, {
|
|
783
897
|
headers: {
|
|
784
898
|
'Authorization': `Bearer ${creds.token}`,
|
|
785
899
|
'Accept': 'application/vnd.github+json',
|
|
786
900
|
'User-Agent': 'APIClaw',
|
|
787
901
|
},
|
|
788
|
-
});
|
|
902
|
+
}, { provider: 'github', action: 'get_file' });
|
|
789
903
|
const data = await response.json();
|
|
790
904
|
if (!response.ok) {
|
|
791
|
-
return
|
|
905
|
+
return createErrorResult('github', 'get_file', data.message || 'Get file failed', statusToErrorCode(response.status));
|
|
792
906
|
}
|
|
793
907
|
// Decode base64 content
|
|
794
908
|
const content = data.content ? Buffer.from(data.content, 'base64').toString('utf-8') : null;
|
|
@@ -811,7 +925,7 @@ const handlers = {
|
|
|
811
925
|
run_code: async (params, creds) => {
|
|
812
926
|
const { code, language = 'python' } = params;
|
|
813
927
|
if (!code) {
|
|
814
|
-
return
|
|
928
|
+
return createErrorResult('e2b', 'run_code', 'Missing required param: code', ERROR_CODES.INVALID_PARAMS);
|
|
815
929
|
}
|
|
816
930
|
try {
|
|
817
931
|
// Dynamic import to avoid issues if SDK not installed
|
|
@@ -837,18 +951,13 @@ const handlers = {
|
|
|
837
951
|
}
|
|
838
952
|
}
|
|
839
953
|
catch (error) {
|
|
840
|
-
return
|
|
841
|
-
success: false,
|
|
842
|
-
provider: 'e2b',
|
|
843
|
-
action: 'run_code',
|
|
844
|
-
error: error.message || 'Code execution failed'
|
|
845
|
-
};
|
|
954
|
+
return createErrorResult('e2b', 'run_code', error.message || 'Code execution failed', ERROR_CODES.PROVIDER_ERROR);
|
|
846
955
|
}
|
|
847
956
|
},
|
|
848
957
|
run_shell: async (params, creds) => {
|
|
849
958
|
const { command } = params;
|
|
850
959
|
if (!command) {
|
|
851
|
-
return
|
|
960
|
+
return createErrorResult('e2b', 'run_shell', 'Missing required param: command', ERROR_CODES.INVALID_PARAMS);
|
|
852
961
|
}
|
|
853
962
|
try {
|
|
854
963
|
const { Sandbox } = await import('@e2b/code-interpreter');
|
|
@@ -872,12 +981,7 @@ const handlers = {
|
|
|
872
981
|
}
|
|
873
982
|
}
|
|
874
983
|
catch (error) {
|
|
875
|
-
return
|
|
876
|
-
success: false,
|
|
877
|
-
provider: 'e2b',
|
|
878
|
-
action: 'run_shell',
|
|
879
|
-
error: error.message || 'Shell execution failed'
|
|
880
|
-
};
|
|
984
|
+
return createErrorResult('e2b', 'run_shell', error.message || 'Shell execution failed', ERROR_CODES.PROVIDER_ERROR);
|
|
881
985
|
}
|
|
882
986
|
},
|
|
883
987
|
},
|
|
@@ -920,29 +1024,14 @@ export async function executeAPICall(providerId, action, params, userId, custome
|
|
|
920
1024
|
// Check if it might be a dynamic provider without userId
|
|
921
1025
|
const dynamicActions = await listDynamicActions(providerId);
|
|
922
1026
|
if (dynamicActions.length > 0) {
|
|
923
|
-
return {
|
|
924
|
-
success: false,
|
|
925
|
-
provider: providerId,
|
|
926
|
-
action,
|
|
927
|
-
error: `Provider '${providerId}' requires userId for dynamic execution. Available actions: ${dynamicActions.join(', ')}`,
|
|
928
|
-
};
|
|
1027
|
+
return createErrorResult(providerId, action, `Provider '${providerId}' requires userId for dynamic execution. Available actions: ${dynamicActions.join(', ')}`, ERROR_CODES.INVALID_PARAMS);
|
|
929
1028
|
}
|
|
930
|
-
return {
|
|
931
|
-
success: false,
|
|
932
|
-
provider: providerId,
|
|
933
|
-
action,
|
|
934
|
-
error: `Provider '${providerId}' not connected. Available: ${Object.keys(handlers).join(', ')}`,
|
|
935
|
-
};
|
|
1029
|
+
return createErrorResult(providerId, action, `Provider '${providerId}' not connected. Available: ${Object.keys(handlers).join(', ')}`, ERROR_CODES.UNKNOWN_PROVIDER);
|
|
936
1030
|
}
|
|
937
1031
|
// Check if action exists
|
|
938
1032
|
const handler = providerHandlers[action];
|
|
939
1033
|
if (!handler) {
|
|
940
|
-
return {
|
|
941
|
-
success: false,
|
|
942
|
-
provider: providerId,
|
|
943
|
-
action,
|
|
944
|
-
error: `Action '${action}' not available for ${providerId}. Available: ${Object.keys(providerHandlers).join(', ')}`,
|
|
945
|
-
};
|
|
1034
|
+
return createErrorResult(providerId, action, `Action '${action}' not available for ${providerId}. Available: ${Object.keys(providerHandlers).join(', ')}`, ERROR_CODES.UNKNOWN_ACTION);
|
|
946
1035
|
}
|
|
947
1036
|
// Providers that don't require credentials (free/open APIs)
|
|
948
1037
|
const NO_CREDS_PROVIDERS = ['coingecko'];
|
|
@@ -967,20 +1056,10 @@ export async function executeAPICall(providerId, action, params, userId, custome
|
|
|
967
1056
|
});
|
|
968
1057
|
}
|
|
969
1058
|
catch (e) {
|
|
970
|
-
return
|
|
971
|
-
success: false,
|
|
972
|
-
provider: providerId,
|
|
973
|
-
action,
|
|
974
|
-
error: e.message || 'Proxy call failed',
|
|
975
|
-
};
|
|
1059
|
+
return createErrorResult(providerId, action, e.message || 'Proxy call failed', ERROR_CODES.PROVIDER_ERROR);
|
|
976
1060
|
}
|
|
977
1061
|
}
|
|
978
|
-
return {
|
|
979
|
-
success: false,
|
|
980
|
-
provider: providerId,
|
|
981
|
-
action,
|
|
982
|
-
error: `No credentials configured for ${providerId}. Set up ~/.secrets/${providerId}.env`,
|
|
983
|
-
};
|
|
1062
|
+
return createErrorResult(providerId, action, `No credentials configured for ${providerId}. Set up ~/.secrets/${providerId}.env`, ERROR_CODES.NO_CREDENTIALS);
|
|
984
1063
|
}
|
|
985
1064
|
// Execute and normalize response
|
|
986
1065
|
try {
|
|
@@ -988,12 +1067,19 @@ export async function executeAPICall(providerId, action, params, userId, custome
|
|
|
988
1067
|
return normalizeResponse(result);
|
|
989
1068
|
}
|
|
990
1069
|
catch (error) {
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
}
|
|
1070
|
+
// Check if it's a network/timeout error
|
|
1071
|
+
const errorMessage = error.message || 'Unknown error';
|
|
1072
|
+
let errorCode = ERROR_CODES.PROVIDER_ERROR;
|
|
1073
|
+
if (errorMessage.includes('Max retries exceeded')) {
|
|
1074
|
+
errorCode = ERROR_CODES.MAX_RETRIES_EXCEEDED;
|
|
1075
|
+
}
|
|
1076
|
+
else if (errorMessage.includes('timeout') || errorMessage.includes('ETIMEDOUT')) {
|
|
1077
|
+
errorCode = ERROR_CODES.TIMEOUT;
|
|
1078
|
+
}
|
|
1079
|
+
else if (errorMessage.includes('ECONNREFUSED') || errorMessage.includes('ENOTFOUND') || errorMessage.includes('fetch')) {
|
|
1080
|
+
errorCode = ERROR_CODES.NETWORK_ERROR;
|
|
1081
|
+
}
|
|
1082
|
+
return createErrorResult(providerId, action, errorMessage, errorCode);
|
|
997
1083
|
}
|
|
998
1084
|
}
|
|
999
1085
|
//# sourceMappingURL=execute.js.map
|