@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/src/execute.ts
CHANGED
|
@@ -12,6 +12,7 @@ interface ExecuteResult {
|
|
|
12
12
|
action: string;
|
|
13
13
|
data?: unknown;
|
|
14
14
|
error?: string;
|
|
15
|
+
code?: string; // Structured error code (e.g., RATE_LIMITED, SERVICE_UNAVAILABLE)
|
|
15
16
|
cost?: number;
|
|
16
17
|
// Normalized top-level fields (extracted from data for convenience)
|
|
17
18
|
url?: string;
|
|
@@ -20,6 +21,128 @@ interface ExecuteResult {
|
|
|
20
21
|
status?: string;
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
// Error codes for structured error responses
|
|
25
|
+
const ERROR_CODES = {
|
|
26
|
+
RATE_LIMITED: 'RATE_LIMITED',
|
|
27
|
+
SERVICE_UNAVAILABLE: 'SERVICE_UNAVAILABLE',
|
|
28
|
+
UNAUTHORIZED: 'UNAUTHORIZED',
|
|
29
|
+
FORBIDDEN: 'FORBIDDEN',
|
|
30
|
+
NOT_FOUND: 'NOT_FOUND',
|
|
31
|
+
BAD_REQUEST: 'BAD_REQUEST',
|
|
32
|
+
TIMEOUT: 'TIMEOUT',
|
|
33
|
+
NETWORK_ERROR: 'NETWORK_ERROR',
|
|
34
|
+
PROVIDER_ERROR: 'PROVIDER_ERROR',
|
|
35
|
+
INVALID_PARAMS: 'INVALID_PARAMS',
|
|
36
|
+
NO_CREDENTIALS: 'NO_CREDENTIALS',
|
|
37
|
+
UNKNOWN_PROVIDER: 'UNKNOWN_PROVIDER',
|
|
38
|
+
UNKNOWN_ACTION: 'UNKNOWN_ACTION',
|
|
39
|
+
MAX_RETRIES_EXCEEDED: 'MAX_RETRIES_EXCEEDED',
|
|
40
|
+
} as const;
|
|
41
|
+
|
|
42
|
+
type ErrorCode = typeof ERROR_CODES[keyof typeof ERROR_CODES];
|
|
43
|
+
|
|
44
|
+
// Retry configuration
|
|
45
|
+
const RETRY_CONFIG = {
|
|
46
|
+
maxRetries: 3,
|
|
47
|
+
baseDelayMs: 1000, // Start with 1 second
|
|
48
|
+
maxDelayMs: 30000, // Cap at 30 seconds
|
|
49
|
+
retryableStatusCodes: [429, 503, 502, 504], // Rate limit + service unavailable variants
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Calculate exponential backoff delay with jitter
|
|
54
|
+
*/
|
|
55
|
+
function calculateBackoff(attempt: number): number {
|
|
56
|
+
const exponentialDelay = RETRY_CONFIG.baseDelayMs * Math.pow(2, attempt);
|
|
57
|
+
const jitter = Math.random() * 0.3 * exponentialDelay; // 0-30% jitter
|
|
58
|
+
return Math.min(exponentialDelay + jitter, RETRY_CONFIG.maxDelayMs);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Sleep for a given number of milliseconds
|
|
63
|
+
*/
|
|
64
|
+
function sleep(ms: number): Promise<void> {
|
|
65
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Map HTTP status code to error code
|
|
70
|
+
*/
|
|
71
|
+
function statusToErrorCode(status: number): ErrorCode {
|
|
72
|
+
switch (status) {
|
|
73
|
+
case 400: return ERROR_CODES.BAD_REQUEST;
|
|
74
|
+
case 401: return ERROR_CODES.UNAUTHORIZED;
|
|
75
|
+
case 403: return ERROR_CODES.FORBIDDEN;
|
|
76
|
+
case 404: return ERROR_CODES.NOT_FOUND;
|
|
77
|
+
case 429: return ERROR_CODES.RATE_LIMITED;
|
|
78
|
+
case 502:
|
|
79
|
+
case 503:
|
|
80
|
+
case 504: return ERROR_CODES.SERVICE_UNAVAILABLE;
|
|
81
|
+
default: return ERROR_CODES.PROVIDER_ERROR;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Check if a response status code is retryable
|
|
87
|
+
*/
|
|
88
|
+
function isRetryableStatus(status: number): boolean {
|
|
89
|
+
return RETRY_CONFIG.retryableStatusCodes.includes(status);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Fetch with automatic retry for transient failures (429, 503)
|
|
94
|
+
*/
|
|
95
|
+
async function fetchWithRetry(
|
|
96
|
+
url: string,
|
|
97
|
+
options: RequestInit,
|
|
98
|
+
context: { provider: string; action: string }
|
|
99
|
+
): Promise<Response> {
|
|
100
|
+
let lastError: Error | null = null;
|
|
101
|
+
let lastResponse: Response | null = null;
|
|
102
|
+
|
|
103
|
+
for (let attempt = 0; attempt <= RETRY_CONFIG.maxRetries; attempt++) {
|
|
104
|
+
try {
|
|
105
|
+
const response = await fetch(url, options);
|
|
106
|
+
|
|
107
|
+
// Check if we should retry
|
|
108
|
+
if (isRetryableStatus(response.status) && attempt < RETRY_CONFIG.maxRetries) {
|
|
109
|
+
lastResponse = response;
|
|
110
|
+
const delay = calculateBackoff(attempt);
|
|
111
|
+
|
|
112
|
+
// Check for Retry-After header
|
|
113
|
+
const retryAfter = response.headers.get('Retry-After');
|
|
114
|
+
const retryDelay = retryAfter
|
|
115
|
+
? (parseInt(retryAfter) * 1000 || delay)
|
|
116
|
+
: delay;
|
|
117
|
+
|
|
118
|
+
console.log(`[APIClaw] ${context.provider}/${context.action}: Got ${response.status}, retrying in ${Math.round(retryDelay)}ms (attempt ${attempt + 1}/${RETRY_CONFIG.maxRetries})`);
|
|
119
|
+
await sleep(Math.min(retryDelay, RETRY_CONFIG.maxDelayMs));
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return response;
|
|
124
|
+
} catch (error) {
|
|
125
|
+
lastError = error as Error;
|
|
126
|
+
|
|
127
|
+
// Retry on network errors
|
|
128
|
+
if (attempt < RETRY_CONFIG.maxRetries) {
|
|
129
|
+
const delay = calculateBackoff(attempt);
|
|
130
|
+
console.log(`[APIClaw] ${context.provider}/${context.action}: Network error, retrying in ${Math.round(delay)}ms (attempt ${attempt + 1}/${RETRY_CONFIG.maxRetries})`);
|
|
131
|
+
await sleep(delay);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// If we have a response (even if error status), return it for proper error handling
|
|
138
|
+
if (lastResponse) {
|
|
139
|
+
return lastResponse;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// All retries exhausted with network errors
|
|
143
|
+
throw lastError || new Error('Max retries exceeded');
|
|
144
|
+
}
|
|
145
|
+
|
|
23
146
|
/**
|
|
24
147
|
* Normalize response by extracting common fields to top-level
|
|
25
148
|
* Makes it easier for agents to access key data without digging into provider-specific structures
|
|
@@ -329,6 +452,25 @@ export function generateDryRun(
|
|
|
329
452
|
};
|
|
330
453
|
}
|
|
331
454
|
|
|
455
|
+
/**
|
|
456
|
+
* Create a structured error result with error code
|
|
457
|
+
*/
|
|
458
|
+
function createErrorResult(
|
|
459
|
+
provider: string,
|
|
460
|
+
action: string,
|
|
461
|
+
error: string,
|
|
462
|
+
code: ErrorCode,
|
|
463
|
+
status?: number
|
|
464
|
+
): ExecuteResult {
|
|
465
|
+
return {
|
|
466
|
+
success: false,
|
|
467
|
+
provider,
|
|
468
|
+
action,
|
|
469
|
+
error,
|
|
470
|
+
code,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
332
474
|
// Helper to safely access properties
|
|
333
475
|
function safeGet(obj: unknown, ...keys: string[]): unknown {
|
|
334
476
|
let current: unknown = obj;
|
|
@@ -351,24 +493,24 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
351
493
|
const { to, message, from = 'APIClaw' } = params;
|
|
352
494
|
|
|
353
495
|
if (!to || !message) {
|
|
354
|
-
return
|
|
496
|
+
return createErrorResult('46elks', 'send_sms', 'Missing required params: to, message', ERROR_CODES.INVALID_PARAMS);
|
|
355
497
|
}
|
|
356
498
|
|
|
357
499
|
const auth = Buffer.from(`${creds.username}:${creds.password}`).toString('base64');
|
|
358
500
|
|
|
359
|
-
const response = await
|
|
501
|
+
const response = await fetchWithRetry('https://api.46elks.com/a1/sms', {
|
|
360
502
|
method: 'POST',
|
|
361
503
|
headers: {
|
|
362
504
|
'Authorization': `Basic ${auth}`,
|
|
363
505
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
364
506
|
},
|
|
365
507
|
body: new URLSearchParams({ from, to, message }),
|
|
366
|
-
});
|
|
508
|
+
}, { provider: '46elks', action: 'send_sms' });
|
|
367
509
|
|
|
368
510
|
const data = await response.json() as Record<string, unknown>;
|
|
369
511
|
|
|
370
512
|
if (!response.ok) {
|
|
371
|
-
return
|
|
513
|
+
return createErrorResult('46elks', 'send_sms', (data.message as string) || 'SMS failed', statusToErrorCode(response.status));
|
|
372
514
|
}
|
|
373
515
|
|
|
374
516
|
return {
|
|
@@ -387,13 +529,13 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
387
529
|
const { to, message, from } = params;
|
|
388
530
|
|
|
389
531
|
if (!to || !message) {
|
|
390
|
-
return
|
|
532
|
+
return createErrorResult('twilio', 'send_sms', 'Missing required params: to, message', ERROR_CODES.INVALID_PARAMS);
|
|
391
533
|
}
|
|
392
534
|
|
|
393
535
|
const auth = Buffer.from(`${creds.username}:${creds.password}`).toString('base64');
|
|
394
536
|
const fromNumber = from || creds.from_number || '+15017122661';
|
|
395
537
|
|
|
396
|
-
const response = await
|
|
538
|
+
const response = await fetchWithRetry(
|
|
397
539
|
`https://api.twilio.com/2010-04-01/Accounts/${creds.username}/Messages.json`,
|
|
398
540
|
{
|
|
399
541
|
method: 'POST',
|
|
@@ -402,13 +544,14 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
402
544
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
403
545
|
},
|
|
404
546
|
body: new URLSearchParams({ From: fromNumber, To: to, Body: message }),
|
|
405
|
-
}
|
|
547
|
+
},
|
|
548
|
+
{ provider: 'twilio', action: 'send_sms' }
|
|
406
549
|
);
|
|
407
550
|
|
|
408
551
|
const data = await response.json() as Record<string, unknown>;
|
|
409
552
|
|
|
410
553
|
if (!response.ok) {
|
|
411
|
-
return
|
|
554
|
+
return createErrorResult('twilio', 'send_sms', (data.message as string) || 'SMS failed', statusToErrorCode(response.status));
|
|
412
555
|
}
|
|
413
556
|
|
|
414
557
|
return {
|
|
@@ -426,21 +569,21 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
426
569
|
const { query, count = 5 } = params;
|
|
427
570
|
|
|
428
571
|
if (!query) {
|
|
429
|
-
return
|
|
572
|
+
return createErrorResult('brave_search', 'search', 'Missing required param: query', ERROR_CODES.INVALID_PARAMS);
|
|
430
573
|
}
|
|
431
574
|
|
|
432
575
|
const url = new URL('https://api.search.brave.com/res/v1/web/search');
|
|
433
576
|
url.searchParams.set('q', query);
|
|
434
577
|
url.searchParams.set('count', count.toString());
|
|
435
578
|
|
|
436
|
-
const response = await
|
|
579
|
+
const response = await fetchWithRetry(url.toString(), {
|
|
437
580
|
headers: { 'X-Subscription-Token': creds.api_key },
|
|
438
|
-
});
|
|
581
|
+
}, { provider: 'brave_search', action: 'search' });
|
|
439
582
|
|
|
440
583
|
const data = await response.json() as Record<string, unknown>;
|
|
441
584
|
|
|
442
585
|
if (!response.ok) {
|
|
443
|
-
return
|
|
586
|
+
return createErrorResult('brave_search', 'search', (data.message as string) || 'Search failed', statusToErrorCode(response.status));
|
|
444
587
|
}
|
|
445
588
|
|
|
446
589
|
const webData = data.web as Record<string, unknown> | undefined;
|
|
@@ -466,22 +609,22 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
466
609
|
const { to, subject, html, text, from = 'APIClaw <noreply@apiclaw.nordsym.com>' } = params;
|
|
467
610
|
|
|
468
611
|
if (!to || !subject || (!html && !text)) {
|
|
469
|
-
return
|
|
612
|
+
return createErrorResult('resend', 'send_email', 'Missing required params: to, subject, html or text', ERROR_CODES.INVALID_PARAMS);
|
|
470
613
|
}
|
|
471
614
|
|
|
472
|
-
const response = await
|
|
615
|
+
const response = await fetchWithRetry('https://api.resend.com/emails', {
|
|
473
616
|
method: 'POST',
|
|
474
617
|
headers: {
|
|
475
618
|
'Authorization': `Bearer ${creds.api_key}`,
|
|
476
619
|
'Content-Type': 'application/json',
|
|
477
620
|
},
|
|
478
621
|
body: JSON.stringify({ from, to, subject, html, text }),
|
|
479
|
-
});
|
|
622
|
+
}, { provider: 'resend', action: 'send_email' });
|
|
480
623
|
|
|
481
624
|
const data = await response.json() as Record<string, unknown>;
|
|
482
625
|
|
|
483
626
|
if (!response.ok) {
|
|
484
|
-
return
|
|
627
|
+
return createErrorResult('resend', 'send_email', (data.message as string) || 'Email failed', statusToErrorCode(response.status));
|
|
485
628
|
}
|
|
486
629
|
|
|
487
630
|
return {
|
|
@@ -499,10 +642,10 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
499
642
|
const { messages, model = 'anthropic/claude-3-haiku', max_tokens = 1000 } = params;
|
|
500
643
|
|
|
501
644
|
if (!messages || !Array.isArray(messages)) {
|
|
502
|
-
return
|
|
645
|
+
return createErrorResult('openrouter', 'chat', 'Missing required param: messages (array)', ERROR_CODES.INVALID_PARAMS);
|
|
503
646
|
}
|
|
504
647
|
|
|
505
|
-
const response = await
|
|
648
|
+
const response = await fetchWithRetry('https://openrouter.ai/api/v1/chat/completions', {
|
|
506
649
|
method: 'POST',
|
|
507
650
|
headers: {
|
|
508
651
|
'Authorization': `Bearer ${creds.api_key}`,
|
|
@@ -510,13 +653,13 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
510
653
|
'HTTP-Referer': 'https://apiclaw.nordsym.com',
|
|
511
654
|
},
|
|
512
655
|
body: JSON.stringify({ model, messages, max_tokens }),
|
|
513
|
-
});
|
|
656
|
+
}, { provider: 'openrouter', action: 'chat' });
|
|
514
657
|
|
|
515
658
|
const data = await response.json() as Record<string, unknown>;
|
|
516
659
|
|
|
517
660
|
if (!response.ok) {
|
|
518
661
|
const errorData = data.error as Record<string, unknown> | undefined;
|
|
519
|
-
return
|
|
662
|
+
return createErrorResult('openrouter', 'chat', (errorData?.message as string) || 'Chat failed', statusToErrorCode(response.status));
|
|
520
663
|
}
|
|
521
664
|
|
|
522
665
|
const choices = data.choices as Array<Record<string, unknown>> | undefined;
|
|
@@ -542,10 +685,10 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
542
685
|
const { text, voice_id = '21m00Tcm4TlvDq8ikWAM', model_id = 'eleven_monolingual_v1' } = params;
|
|
543
686
|
|
|
544
687
|
if (!text) {
|
|
545
|
-
return
|
|
688
|
+
return createErrorResult('elevenlabs', 'text_to_speech', 'Missing required param: text', ERROR_CODES.INVALID_PARAMS);
|
|
546
689
|
}
|
|
547
690
|
|
|
548
|
-
const response = await
|
|
691
|
+
const response = await fetchWithRetry(
|
|
549
692
|
`https://api.elevenlabs.io/v1/text-to-speech/${voice_id}`,
|
|
550
693
|
{
|
|
551
694
|
method: 'POST',
|
|
@@ -554,12 +697,13 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
554
697
|
'Content-Type': 'application/json',
|
|
555
698
|
},
|
|
556
699
|
body: JSON.stringify({ text, model_id }),
|
|
557
|
-
}
|
|
700
|
+
},
|
|
701
|
+
{ provider: 'elevenlabs', action: 'text_to_speech' }
|
|
558
702
|
);
|
|
559
703
|
|
|
560
704
|
if (!response.ok) {
|
|
561
705
|
const error = await response.json().catch(() => ({})) as Record<string, unknown>;
|
|
562
|
-
return
|
|
706
|
+
return createErrorResult('elevenlabs', 'text_to_speech', (error.detail as string) || 'TTS failed', statusToErrorCode(response.status));
|
|
563
707
|
}
|
|
564
708
|
|
|
565
709
|
// Return audio as base64
|
|
@@ -585,17 +729,17 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
585
729
|
const { model, input } = params;
|
|
586
730
|
|
|
587
731
|
if (!model) {
|
|
588
|
-
return
|
|
732
|
+
return createErrorResult('replicate', 'run', 'Missing required param: model (e.g., "stability-ai/sdxl:...")', ERROR_CODES.INVALID_PARAMS);
|
|
589
733
|
}
|
|
590
734
|
if (!input) {
|
|
591
|
-
return
|
|
735
|
+
return createErrorResult('replicate', 'run', 'Missing required param: input (object with model inputs)', ERROR_CODES.INVALID_PARAMS);
|
|
592
736
|
}
|
|
593
737
|
|
|
594
738
|
// Parse model into owner/name and version
|
|
595
739
|
const [modelPath, version] = model.split(':');
|
|
596
740
|
|
|
597
741
|
// Create prediction
|
|
598
|
-
const response = await
|
|
742
|
+
const response = await fetchWithRetry('https://api.replicate.com/v1/predictions', {
|
|
599
743
|
method: 'POST',
|
|
600
744
|
headers: {
|
|
601
745
|
'Authorization': `Bearer ${creds.api_key}`,
|
|
@@ -606,11 +750,11 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
606
750
|
model: version ? undefined : modelPath,
|
|
607
751
|
input,
|
|
608
752
|
}),
|
|
609
|
-
});
|
|
753
|
+
}, { provider: 'replicate', action: 'run' });
|
|
610
754
|
|
|
611
755
|
if (!response.ok) {
|
|
612
756
|
const error = await response.json().catch(() => ({})) as Record<string, unknown>;
|
|
613
|
-
return
|
|
757
|
+
return createErrorResult('replicate', 'run', (error.detail as string) || 'Prediction failed', statusToErrorCode(response.status));
|
|
614
758
|
}
|
|
615
759
|
|
|
616
760
|
const prediction = await response.json() as Record<string, unknown>;
|
|
@@ -632,15 +776,15 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
632
776
|
}
|
|
633
777
|
};
|
|
634
778
|
}
|
|
635
|
-
await
|
|
636
|
-
const pollResponse = await
|
|
779
|
+
await sleep(1000);
|
|
780
|
+
const pollResponse = await fetchWithRetry((result.urls as Record<string, string>)?.get || `https://api.replicate.com/v1/predictions/${result.id}`, {
|
|
637
781
|
headers: { 'Authorization': `Bearer ${creds.api_key}` },
|
|
638
|
-
});
|
|
782
|
+
}, { provider: 'replicate', action: 'run_poll' });
|
|
639
783
|
result = await pollResponse.json() as Record<string, unknown>;
|
|
640
784
|
}
|
|
641
785
|
|
|
642
786
|
if (result.status === 'failed') {
|
|
643
|
-
return
|
|
787
|
+
return createErrorResult('replicate', 'run', (result.error as string) || 'Prediction failed', ERROR_CODES.PROVIDER_ERROR);
|
|
644
788
|
}
|
|
645
789
|
|
|
646
790
|
return {
|
|
@@ -657,12 +801,12 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
657
801
|
},
|
|
658
802
|
|
|
659
803
|
list_models: async (_params, creds) => {
|
|
660
|
-
const response = await
|
|
804
|
+
const response = await fetchWithRetry('https://api.replicate.com/v1/models', {
|
|
661
805
|
headers: { 'Authorization': `Bearer ${creds.api_key}` },
|
|
662
|
-
});
|
|
806
|
+
}, { provider: 'replicate', action: 'list_models' });
|
|
663
807
|
|
|
664
808
|
if (!response.ok) {
|
|
665
|
-
return
|
|
809
|
+
return createErrorResult('replicate', 'list_models', 'Failed to list models', statusToErrorCode(response.status));
|
|
666
810
|
}
|
|
667
811
|
|
|
668
812
|
const data = await response.json() as Record<string, unknown>;
|
|
@@ -685,22 +829,22 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
685
829
|
const { url, formats = ['markdown'] } = params;
|
|
686
830
|
|
|
687
831
|
if (!url) {
|
|
688
|
-
return
|
|
832
|
+
return createErrorResult('firecrawl', 'scrape', 'Missing required param: url', ERROR_CODES.INVALID_PARAMS);
|
|
689
833
|
}
|
|
690
834
|
|
|
691
|
-
const response = await
|
|
835
|
+
const response = await fetchWithRetry('https://api.firecrawl.dev/v1/scrape', {
|
|
692
836
|
method: 'POST',
|
|
693
837
|
headers: {
|
|
694
838
|
'Authorization': `Bearer ${creds.api_key}`,
|
|
695
839
|
'Content-Type': 'application/json',
|
|
696
840
|
},
|
|
697
841
|
body: JSON.stringify({ url, formats }),
|
|
698
|
-
});
|
|
842
|
+
}, { provider: 'firecrawl', action: 'scrape' });
|
|
699
843
|
|
|
700
844
|
const data = await response.json() as Record<string, unknown>;
|
|
701
845
|
|
|
702
846
|
if (!response.ok || !data.success) {
|
|
703
|
-
return
|
|
847
|
+
return createErrorResult('firecrawl', 'scrape', (data.error as string) || 'Scrape failed', statusToErrorCode(response.status));
|
|
704
848
|
}
|
|
705
849
|
|
|
706
850
|
return {
|
|
@@ -715,22 +859,22 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
715
859
|
const { url, limit = 10 } = params;
|
|
716
860
|
|
|
717
861
|
if (!url) {
|
|
718
|
-
return
|
|
862
|
+
return createErrorResult('firecrawl', 'crawl', 'Missing required param: url', ERROR_CODES.INVALID_PARAMS);
|
|
719
863
|
}
|
|
720
864
|
|
|
721
|
-
const response = await
|
|
865
|
+
const response = await fetchWithRetry('https://api.firecrawl.dev/v1/crawl', {
|
|
722
866
|
method: 'POST',
|
|
723
867
|
headers: {
|
|
724
868
|
'Authorization': `Bearer ${creds.api_key}`,
|
|
725
869
|
'Content-Type': 'application/json',
|
|
726
870
|
},
|
|
727
871
|
body: JSON.stringify({ url, limit }),
|
|
728
|
-
});
|
|
872
|
+
}, { provider: 'firecrawl', action: 'crawl' });
|
|
729
873
|
|
|
730
874
|
const data = await response.json() as Record<string, unknown>;
|
|
731
875
|
|
|
732
876
|
if (!response.ok || !data.success) {
|
|
733
|
-
return
|
|
877
|
+
return createErrorResult('firecrawl', 'crawl', (data.error as string) || 'Crawl failed', statusToErrorCode(response.status));
|
|
734
878
|
}
|
|
735
879
|
|
|
736
880
|
return {
|
|
@@ -745,22 +889,22 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
745
889
|
const { url } = params;
|
|
746
890
|
|
|
747
891
|
if (!url) {
|
|
748
|
-
return
|
|
892
|
+
return createErrorResult('firecrawl', 'map', 'Missing required param: url', ERROR_CODES.INVALID_PARAMS);
|
|
749
893
|
}
|
|
750
894
|
|
|
751
|
-
const response = await
|
|
895
|
+
const response = await fetchWithRetry('https://api.firecrawl.dev/v1/map', {
|
|
752
896
|
method: 'POST',
|
|
753
897
|
headers: {
|
|
754
898
|
'Authorization': `Bearer ${creds.api_key}`,
|
|
755
899
|
'Content-Type': 'application/json',
|
|
756
900
|
},
|
|
757
901
|
body: JSON.stringify({ url }),
|
|
758
|
-
});
|
|
902
|
+
}, { provider: 'firecrawl', action: 'map' });
|
|
759
903
|
|
|
760
904
|
const data = await response.json() as Record<string, unknown>;
|
|
761
905
|
|
|
762
906
|
if (!response.ok || !data.success) {
|
|
763
|
-
return
|
|
907
|
+
return createErrorResult('firecrawl', 'map', (data.error as string) || 'Map failed', statusToErrorCode(response.status));
|
|
764
908
|
}
|
|
765
909
|
|
|
766
910
|
return {
|
|
@@ -778,21 +922,21 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
778
922
|
const { query, sort = 'stars', limit = 10 } = params;
|
|
779
923
|
|
|
780
924
|
if (!query) {
|
|
781
|
-
return
|
|
925
|
+
return createErrorResult('github', 'search_repos', 'Missing required param: query', ERROR_CODES.INVALID_PARAMS);
|
|
782
926
|
}
|
|
783
927
|
|
|
784
|
-
const response = await
|
|
928
|
+
const response = await fetchWithRetry(`https://api.github.com/search/repositories?q=${encodeURIComponent(query)}&sort=${sort}&per_page=${limit}`, {
|
|
785
929
|
headers: {
|
|
786
930
|
'Authorization': `Bearer ${creds.token}`,
|
|
787
931
|
'Accept': 'application/vnd.github+json',
|
|
788
932
|
'User-Agent': 'APIClaw',
|
|
789
933
|
},
|
|
790
|
-
});
|
|
934
|
+
}, { provider: 'github', action: 'search_repos' });
|
|
791
935
|
|
|
792
936
|
const data = await response.json() as Record<string, unknown>;
|
|
793
937
|
|
|
794
938
|
if (!response.ok) {
|
|
795
|
-
return
|
|
939
|
+
return createErrorResult('github', 'search_repos', (data.message as string) || 'Search failed', statusToErrorCode(response.status));
|
|
796
940
|
}
|
|
797
941
|
|
|
798
942
|
const items = (data.items as any[]) || [];
|
|
@@ -817,21 +961,21 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
817
961
|
const { owner, repo } = params;
|
|
818
962
|
|
|
819
963
|
if (!owner || !repo) {
|
|
820
|
-
return
|
|
964
|
+
return createErrorResult('github', 'get_repo', 'Missing required params: owner, repo', ERROR_CODES.INVALID_PARAMS);
|
|
821
965
|
}
|
|
822
966
|
|
|
823
|
-
const response = await
|
|
967
|
+
const response = await fetchWithRetry(`https://api.github.com/repos/${owner}/${repo}`, {
|
|
824
968
|
headers: {
|
|
825
969
|
'Authorization': `Bearer ${creds.token}`,
|
|
826
970
|
'Accept': 'application/vnd.github+json',
|
|
827
971
|
'User-Agent': 'APIClaw',
|
|
828
972
|
},
|
|
829
|
-
});
|
|
973
|
+
}, { provider: 'github', action: 'get_repo' });
|
|
830
974
|
|
|
831
975
|
const data = await response.json() as Record<string, unknown>;
|
|
832
976
|
|
|
833
977
|
if (!response.ok) {
|
|
834
|
-
return
|
|
978
|
+
return createErrorResult('github', 'get_repo', (data.message as string) || 'Get repo failed', statusToErrorCode(response.status));
|
|
835
979
|
}
|
|
836
980
|
|
|
837
981
|
return {
|
|
@@ -855,21 +999,21 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
855
999
|
const { owner, repo, state = 'open', limit = 10 } = params;
|
|
856
1000
|
|
|
857
1001
|
if (!owner || !repo) {
|
|
858
|
-
return
|
|
1002
|
+
return createErrorResult('github', 'list_issues', 'Missing required params: owner, repo', ERROR_CODES.INVALID_PARAMS);
|
|
859
1003
|
}
|
|
860
1004
|
|
|
861
|
-
const response = await
|
|
1005
|
+
const response = await fetchWithRetry(`https://api.github.com/repos/${owner}/${repo}/issues?state=${state}&per_page=${limit}`, {
|
|
862
1006
|
headers: {
|
|
863
1007
|
'Authorization': `Bearer ${creds.token}`,
|
|
864
1008
|
'Accept': 'application/vnd.github+json',
|
|
865
1009
|
'User-Agent': 'APIClaw',
|
|
866
1010
|
},
|
|
867
|
-
});
|
|
1011
|
+
}, { provider: 'github', action: 'list_issues' });
|
|
868
1012
|
|
|
869
1013
|
const data = await response.json() as unknown[];
|
|
870
1014
|
|
|
871
1015
|
if (!response.ok) {
|
|
872
|
-
return
|
|
1016
|
+
return createErrorResult('github', 'list_issues', 'List issues failed', statusToErrorCode(response.status));
|
|
873
1017
|
}
|
|
874
1018
|
|
|
875
1019
|
return {
|
|
@@ -893,10 +1037,10 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
893
1037
|
const { owner, repo, title, body = '' } = params;
|
|
894
1038
|
|
|
895
1039
|
if (!owner || !repo || !title) {
|
|
896
|
-
return
|
|
1040
|
+
return createErrorResult('github', 'create_issue', 'Missing required params: owner, repo, title', ERROR_CODES.INVALID_PARAMS);
|
|
897
1041
|
}
|
|
898
1042
|
|
|
899
|
-
const response = await
|
|
1043
|
+
const response = await fetchWithRetry(`https://api.github.com/repos/${owner}/${repo}/issues`, {
|
|
900
1044
|
method: 'POST',
|
|
901
1045
|
headers: {
|
|
902
1046
|
'Authorization': `Bearer ${creds.token}`,
|
|
@@ -905,12 +1049,12 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
905
1049
|
'Content-Type': 'application/json',
|
|
906
1050
|
},
|
|
907
1051
|
body: JSON.stringify({ title, body }),
|
|
908
|
-
});
|
|
1052
|
+
}, { provider: 'github', action: 'create_issue' });
|
|
909
1053
|
|
|
910
1054
|
const data = await response.json() as Record<string, unknown>;
|
|
911
1055
|
|
|
912
1056
|
if (!response.ok) {
|
|
913
|
-
return
|
|
1057
|
+
return createErrorResult('github', 'create_issue', (data.message as string) || 'Create issue failed', statusToErrorCode(response.status));
|
|
914
1058
|
}
|
|
915
1059
|
|
|
916
1060
|
return {
|
|
@@ -928,21 +1072,21 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
928
1072
|
const { owner, repo, path } = params;
|
|
929
1073
|
|
|
930
1074
|
if (!owner || !repo || !path) {
|
|
931
|
-
return
|
|
1075
|
+
return createErrorResult('github', 'get_file', 'Missing required params: owner, repo, path', ERROR_CODES.INVALID_PARAMS);
|
|
932
1076
|
}
|
|
933
1077
|
|
|
934
|
-
const response = await
|
|
1078
|
+
const response = await fetchWithRetry(`https://api.github.com/repos/${owner}/${repo}/contents/${path}`, {
|
|
935
1079
|
headers: {
|
|
936
1080
|
'Authorization': `Bearer ${creds.token}`,
|
|
937
1081
|
'Accept': 'application/vnd.github+json',
|
|
938
1082
|
'User-Agent': 'APIClaw',
|
|
939
1083
|
},
|
|
940
|
-
});
|
|
1084
|
+
}, { provider: 'github', action: 'get_file' });
|
|
941
1085
|
|
|
942
1086
|
const data = await response.json() as Record<string, unknown>;
|
|
943
1087
|
|
|
944
1088
|
if (!response.ok) {
|
|
945
|
-
return
|
|
1089
|
+
return createErrorResult('github', 'get_file', (data.message as string) || 'Get file failed', statusToErrorCode(response.status));
|
|
946
1090
|
}
|
|
947
1091
|
|
|
948
1092
|
// Decode base64 content
|
|
@@ -969,7 +1113,7 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
969
1113
|
const { code, language = 'python' } = params;
|
|
970
1114
|
|
|
971
1115
|
if (!code) {
|
|
972
|
-
return
|
|
1116
|
+
return createErrorResult('e2b', 'run_code', 'Missing required param: code', ERROR_CODES.INVALID_PARAMS);
|
|
973
1117
|
}
|
|
974
1118
|
|
|
975
1119
|
try {
|
|
@@ -998,12 +1142,7 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
998
1142
|
await sandbox.kill().catch(() => {});
|
|
999
1143
|
}
|
|
1000
1144
|
} catch (error: any) {
|
|
1001
|
-
return
|
|
1002
|
-
success: false,
|
|
1003
|
-
provider: 'e2b',
|
|
1004
|
-
action: 'run_code',
|
|
1005
|
-
error: error.message || 'Code execution failed'
|
|
1006
|
-
};
|
|
1145
|
+
return createErrorResult('e2b', 'run_code', error.message || 'Code execution failed', ERROR_CODES.PROVIDER_ERROR);
|
|
1007
1146
|
}
|
|
1008
1147
|
},
|
|
1009
1148
|
|
|
@@ -1011,7 +1150,7 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
1011
1150
|
const { command } = params;
|
|
1012
1151
|
|
|
1013
1152
|
if (!command) {
|
|
1014
|
-
return
|
|
1153
|
+
return createErrorResult('e2b', 'run_shell', 'Missing required param: command', ERROR_CODES.INVALID_PARAMS);
|
|
1015
1154
|
}
|
|
1016
1155
|
|
|
1017
1156
|
try {
|
|
@@ -1038,12 +1177,7 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
1038
1177
|
await sandbox.kill().catch(() => {});
|
|
1039
1178
|
}
|
|
1040
1179
|
} catch (error: any) {
|
|
1041
|
-
return
|
|
1042
|
-
success: false,
|
|
1043
|
-
provider: 'e2b',
|
|
1044
|
-
action: 'run_shell',
|
|
1045
|
-
error: error.message || 'Shell execution failed'
|
|
1046
|
-
};
|
|
1180
|
+
return createErrorResult('e2b', 'run_shell', error.message || 'Shell execution failed', ERROR_CODES.PROVIDER_ERROR);
|
|
1047
1181
|
}
|
|
1048
1182
|
},
|
|
1049
1183
|
},
|
|
@@ -1098,30 +1232,30 @@ export async function executeAPICall(
|
|
|
1098
1232
|
// Check if it might be a dynamic provider without userId
|
|
1099
1233
|
const dynamicActions = await listDynamicActions(providerId);
|
|
1100
1234
|
if (dynamicActions.length > 0) {
|
|
1101
|
-
return
|
|
1102
|
-
|
|
1103
|
-
provider: providerId,
|
|
1235
|
+
return createErrorResult(
|
|
1236
|
+
providerId,
|
|
1104
1237
|
action,
|
|
1105
|
-
|
|
1106
|
-
|
|
1238
|
+
`Provider '${providerId}' requires userId for dynamic execution. Available actions: ${dynamicActions.join(', ')}`,
|
|
1239
|
+
ERROR_CODES.INVALID_PARAMS
|
|
1240
|
+
);
|
|
1107
1241
|
}
|
|
1108
|
-
return
|
|
1109
|
-
|
|
1110
|
-
provider: providerId,
|
|
1242
|
+
return createErrorResult(
|
|
1243
|
+
providerId,
|
|
1111
1244
|
action,
|
|
1112
|
-
|
|
1113
|
-
|
|
1245
|
+
`Provider '${providerId}' not connected. Available: ${Object.keys(handlers).join(', ')}`,
|
|
1246
|
+
ERROR_CODES.UNKNOWN_PROVIDER
|
|
1247
|
+
);
|
|
1114
1248
|
}
|
|
1115
1249
|
|
|
1116
1250
|
// Check if action exists
|
|
1117
1251
|
const handler = providerHandlers[action];
|
|
1118
1252
|
if (!handler) {
|
|
1119
|
-
return
|
|
1120
|
-
|
|
1121
|
-
provider: providerId,
|
|
1253
|
+
return createErrorResult(
|
|
1254
|
+
providerId,
|
|
1122
1255
|
action,
|
|
1123
|
-
|
|
1124
|
-
|
|
1256
|
+
`Action '${action}' not available for ${providerId}. Available: ${Object.keys(providerHandlers).join(', ')}`,
|
|
1257
|
+
ERROR_CODES.UNKNOWN_ACTION
|
|
1258
|
+
);
|
|
1125
1259
|
}
|
|
1126
1260
|
|
|
1127
1261
|
// Providers that don't require credentials (free/open APIs)
|
|
@@ -1149,20 +1283,15 @@ export async function executeAPICall(
|
|
|
1149
1283
|
data: proxyResult,
|
|
1150
1284
|
});
|
|
1151
1285
|
} catch (e: any) {
|
|
1152
|
-
return
|
|
1153
|
-
success: false,
|
|
1154
|
-
provider: providerId,
|
|
1155
|
-
action,
|
|
1156
|
-
error: e.message || 'Proxy call failed',
|
|
1157
|
-
};
|
|
1286
|
+
return createErrorResult(providerId, action, e.message || 'Proxy call failed', ERROR_CODES.PROVIDER_ERROR);
|
|
1158
1287
|
}
|
|
1159
1288
|
}
|
|
1160
|
-
return
|
|
1161
|
-
|
|
1162
|
-
provider: providerId,
|
|
1289
|
+
return createErrorResult(
|
|
1290
|
+
providerId,
|
|
1163
1291
|
action,
|
|
1164
|
-
|
|
1165
|
-
|
|
1292
|
+
`No credentials configured for ${providerId}. Set up ~/.secrets/${providerId}.env`,
|
|
1293
|
+
ERROR_CODES.NO_CREDENTIALS
|
|
1294
|
+
);
|
|
1166
1295
|
}
|
|
1167
1296
|
|
|
1168
1297
|
// Execute and normalize response
|
|
@@ -1170,11 +1299,18 @@ export async function executeAPICall(
|
|
|
1170
1299
|
const result = await handler(params, creds);
|
|
1171
1300
|
return normalizeResponse(result);
|
|
1172
1301
|
} catch (error: any) {
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1302
|
+
// Check if it's a network/timeout error
|
|
1303
|
+
const errorMessage = error.message || 'Unknown error';
|
|
1304
|
+
let errorCode: ErrorCode = ERROR_CODES.PROVIDER_ERROR;
|
|
1305
|
+
|
|
1306
|
+
if (errorMessage.includes('Max retries exceeded')) {
|
|
1307
|
+
errorCode = ERROR_CODES.MAX_RETRIES_EXCEEDED;
|
|
1308
|
+
} else if (errorMessage.includes('timeout') || errorMessage.includes('ETIMEDOUT')) {
|
|
1309
|
+
errorCode = ERROR_CODES.TIMEOUT;
|
|
1310
|
+
} else if (errorMessage.includes('ECONNREFUSED') || errorMessage.includes('ENOTFOUND') || errorMessage.includes('fetch')) {
|
|
1311
|
+
errorCode = ERROR_CODES.NETWORK_ERROR;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
return createErrorResult(providerId, action, errorMessage, errorCode);
|
|
1179
1315
|
}
|
|
1180
1316
|
}
|