@nordsym/apiclaw 1.3.12 → 1.4.0
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-API-CHAINING.md +483 -0
- package/PRD-HARDEN-SHELL.md +278 -0
- package/README.md +72 -0
- package/convex/_generated/api.d.ts +4 -0
- package/convex/chains.ts +1095 -0
- package/convex/crons.ts +11 -0
- package/convex/logs.ts +107 -0
- package/convex/schema.ts +107 -0
- package/convex/spendAlerts.ts +442 -0
- package/convex/workspaces.ts +26 -0
- package/dist/chain-types.d.ts +187 -0
- package/dist/chain-types.d.ts.map +1 -0
- package/dist/chain-types.js +33 -0
- package/dist/chain-types.js.map +1 -0
- package/dist/chainExecutor.d.ts +122 -0
- package/dist/chainExecutor.d.ts.map +1 -0
- package/dist/chainExecutor.js +454 -0
- package/dist/chainExecutor.js.map +1 -0
- package/dist/chainResolver.d.ts +100 -0
- package/dist/chainResolver.d.ts.map +1 -0
- package/dist/chainResolver.js +519 -0
- package/dist/chainResolver.js.map +1 -0
- package/dist/chainResolver.test.d.ts +5 -0
- package/dist/chainResolver.test.d.ts.map +1 -0
- package/dist/chainResolver.test.js +201 -0
- package/dist/chainResolver.test.js.map +1 -0
- package/dist/execute.d.ts +5 -1
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +207 -118
- package/dist/execute.js.map +1 -1
- package/dist/index.js +382 -2
- package/dist/index.js.map +1 -1
- package/landing/package-lock.json +29 -5
- package/landing/package.json +2 -1
- package/landing/public/logos/chattgpt.svg +1 -0
- package/landing/public/logos/claude.svg +1 -0
- package/landing/public/logos/gemini.svg +1 -0
- package/landing/public/logos/grok.svg +1 -0
- package/landing/src/app/page.tsx +11 -0
- package/landing/src/app/security/page.tsx +381 -0
- package/landing/src/app/workspace/chains/page.tsx +520 -0
- package/landing/src/components/AITestimonials.tsx +195 -0
- package/landing/src/components/ChainStepDetail.tsx +310 -0
- package/landing/src/components/ChainTrace.tsx +261 -0
- package/landing/src/lib/stats.json +1 -1
- package/package.json +1 -1
- package/src/chain-types.ts +270 -0
- package/src/chainExecutor.ts +730 -0
- package/src/chainResolver.test.ts +246 -0
- package/src/chainResolver.ts +658 -0
- package/src/execute.ts +273 -114
- package/src/index.ts +423 -2
package/src/execute.ts
CHANGED
|
@@ -6,12 +6,36 @@ import { getCredentials } from './credentials.js';
|
|
|
6
6
|
import { callProxy, PROXY_PROVIDERS } from './proxy.js';
|
|
7
7
|
import { executeDynamicAction, hasDynamicConfig, listDynamicActions } from './execute-dynamic.js';
|
|
8
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
|
+
|
|
9
32
|
interface ExecuteResult {
|
|
10
33
|
success: boolean;
|
|
11
34
|
provider: string;
|
|
12
35
|
action: string;
|
|
13
36
|
data?: unknown;
|
|
14
37
|
error?: string;
|
|
38
|
+
code?: string; // Structured error code (e.g., RATE_LIMITED, SERVICE_UNAVAILABLE)
|
|
15
39
|
cost?: number;
|
|
16
40
|
// Normalized top-level fields (extracted from data for convenience)
|
|
17
41
|
url?: string;
|
|
@@ -20,6 +44,128 @@ interface ExecuteResult {
|
|
|
20
44
|
status?: string;
|
|
21
45
|
}
|
|
22
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
|
+
|
|
23
169
|
/**
|
|
24
170
|
* Normalize response by extracting common fields to top-level
|
|
25
171
|
* Makes it easier for agents to access key data without digging into provider-specific structures
|
|
@@ -329,6 +475,25 @@ export function generateDryRun(
|
|
|
329
475
|
};
|
|
330
476
|
}
|
|
331
477
|
|
|
478
|
+
/**
|
|
479
|
+
* Create a structured error result with error code
|
|
480
|
+
*/
|
|
481
|
+
function createErrorResult(
|
|
482
|
+
provider: string,
|
|
483
|
+
action: string,
|
|
484
|
+
error: string,
|
|
485
|
+
code: ErrorCode,
|
|
486
|
+
status?: number
|
|
487
|
+
): ExecuteResult {
|
|
488
|
+
return {
|
|
489
|
+
success: false,
|
|
490
|
+
provider,
|
|
491
|
+
action,
|
|
492
|
+
error,
|
|
493
|
+
code,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
332
497
|
// Helper to safely access properties
|
|
333
498
|
function safeGet(obj: unknown, ...keys: string[]): unknown {
|
|
334
499
|
let current: unknown = obj;
|
|
@@ -351,24 +516,24 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
351
516
|
const { to, message, from = 'APIClaw' } = params;
|
|
352
517
|
|
|
353
518
|
if (!to || !message) {
|
|
354
|
-
return
|
|
519
|
+
return createErrorResult('46elks', 'send_sms', 'Missing required params: to, message', ERROR_CODES.INVALID_PARAMS);
|
|
355
520
|
}
|
|
356
521
|
|
|
357
522
|
const auth = Buffer.from(`${creds.username}:${creds.password}`).toString('base64');
|
|
358
523
|
|
|
359
|
-
const response = await
|
|
524
|
+
const response = await fetchWithRetry('https://api.46elks.com/a1/sms', {
|
|
360
525
|
method: 'POST',
|
|
361
526
|
headers: {
|
|
362
527
|
'Authorization': `Basic ${auth}`,
|
|
363
528
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
364
529
|
},
|
|
365
530
|
body: new URLSearchParams({ from, to, message }),
|
|
366
|
-
});
|
|
531
|
+
}, { provider: '46elks', action: 'send_sms' });
|
|
367
532
|
|
|
368
533
|
const data = await response.json() as Record<string, unknown>;
|
|
369
534
|
|
|
370
535
|
if (!response.ok) {
|
|
371
|
-
return
|
|
536
|
+
return createErrorResult('46elks', 'send_sms', (data.message as string) || 'SMS failed', statusToErrorCode(response.status));
|
|
372
537
|
}
|
|
373
538
|
|
|
374
539
|
return {
|
|
@@ -387,13 +552,13 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
387
552
|
const { to, message, from } = params;
|
|
388
553
|
|
|
389
554
|
if (!to || !message) {
|
|
390
|
-
return
|
|
555
|
+
return createErrorResult('twilio', 'send_sms', 'Missing required params: to, message', ERROR_CODES.INVALID_PARAMS);
|
|
391
556
|
}
|
|
392
557
|
|
|
393
558
|
const auth = Buffer.from(`${creds.username}:${creds.password}`).toString('base64');
|
|
394
559
|
const fromNumber = from || creds.from_number || '+15017122661';
|
|
395
560
|
|
|
396
|
-
const response = await
|
|
561
|
+
const response = await fetchWithRetry(
|
|
397
562
|
`https://api.twilio.com/2010-04-01/Accounts/${creds.username}/Messages.json`,
|
|
398
563
|
{
|
|
399
564
|
method: 'POST',
|
|
@@ -402,13 +567,14 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
402
567
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
403
568
|
},
|
|
404
569
|
body: new URLSearchParams({ From: fromNumber, To: to, Body: message }),
|
|
405
|
-
}
|
|
570
|
+
},
|
|
571
|
+
{ provider: 'twilio', action: 'send_sms' }
|
|
406
572
|
);
|
|
407
573
|
|
|
408
574
|
const data = await response.json() as Record<string, unknown>;
|
|
409
575
|
|
|
410
576
|
if (!response.ok) {
|
|
411
|
-
return
|
|
577
|
+
return createErrorResult('twilio', 'send_sms', (data.message as string) || 'SMS failed', statusToErrorCode(response.status));
|
|
412
578
|
}
|
|
413
579
|
|
|
414
580
|
return {
|
|
@@ -426,21 +592,21 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
426
592
|
const { query, count = 5 } = params;
|
|
427
593
|
|
|
428
594
|
if (!query) {
|
|
429
|
-
return
|
|
595
|
+
return createErrorResult('brave_search', 'search', 'Missing required param: query', ERROR_CODES.INVALID_PARAMS);
|
|
430
596
|
}
|
|
431
597
|
|
|
432
598
|
const url = new URL('https://api.search.brave.com/res/v1/web/search');
|
|
433
599
|
url.searchParams.set('q', query);
|
|
434
600
|
url.searchParams.set('count', count.toString());
|
|
435
601
|
|
|
436
|
-
const response = await
|
|
602
|
+
const response = await fetchWithRetry(url.toString(), {
|
|
437
603
|
headers: { 'X-Subscription-Token': creds.api_key },
|
|
438
|
-
});
|
|
604
|
+
}, { provider: 'brave_search', action: 'search' });
|
|
439
605
|
|
|
440
606
|
const data = await response.json() as Record<string, unknown>;
|
|
441
607
|
|
|
442
608
|
if (!response.ok) {
|
|
443
|
-
return
|
|
609
|
+
return createErrorResult('brave_search', 'search', (data.message as string) || 'Search failed', statusToErrorCode(response.status));
|
|
444
610
|
}
|
|
445
611
|
|
|
446
612
|
const webData = data.web as Record<string, unknown> | undefined;
|
|
@@ -466,22 +632,22 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
466
632
|
const { to, subject, html, text, from = 'APIClaw <noreply@apiclaw.nordsym.com>' } = params;
|
|
467
633
|
|
|
468
634
|
if (!to || !subject || (!html && !text)) {
|
|
469
|
-
return
|
|
635
|
+
return createErrorResult('resend', 'send_email', 'Missing required params: to, subject, html or text', ERROR_CODES.INVALID_PARAMS);
|
|
470
636
|
}
|
|
471
637
|
|
|
472
|
-
const response = await
|
|
638
|
+
const response = await fetchWithRetry('https://api.resend.com/emails', {
|
|
473
639
|
method: 'POST',
|
|
474
640
|
headers: {
|
|
475
641
|
'Authorization': `Bearer ${creds.api_key}`,
|
|
476
642
|
'Content-Type': 'application/json',
|
|
477
643
|
},
|
|
478
644
|
body: JSON.stringify({ from, to, subject, html, text }),
|
|
479
|
-
});
|
|
645
|
+
}, { provider: 'resend', action: 'send_email' });
|
|
480
646
|
|
|
481
647
|
const data = await response.json() as Record<string, unknown>;
|
|
482
648
|
|
|
483
649
|
if (!response.ok) {
|
|
484
|
-
return
|
|
650
|
+
return createErrorResult('resend', 'send_email', (data.message as string) || 'Email failed', statusToErrorCode(response.status));
|
|
485
651
|
}
|
|
486
652
|
|
|
487
653
|
return {
|
|
@@ -499,10 +665,10 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
499
665
|
const { messages, model = 'anthropic/claude-3-haiku', max_tokens = 1000 } = params;
|
|
500
666
|
|
|
501
667
|
if (!messages || !Array.isArray(messages)) {
|
|
502
|
-
return
|
|
668
|
+
return createErrorResult('openrouter', 'chat', 'Missing required param: messages (array)', ERROR_CODES.INVALID_PARAMS);
|
|
503
669
|
}
|
|
504
670
|
|
|
505
|
-
const response = await
|
|
671
|
+
const response = await fetchWithRetry('https://openrouter.ai/api/v1/chat/completions', {
|
|
506
672
|
method: 'POST',
|
|
507
673
|
headers: {
|
|
508
674
|
'Authorization': `Bearer ${creds.api_key}`,
|
|
@@ -510,13 +676,13 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
510
676
|
'HTTP-Referer': 'https://apiclaw.nordsym.com',
|
|
511
677
|
},
|
|
512
678
|
body: JSON.stringify({ model, messages, max_tokens }),
|
|
513
|
-
});
|
|
679
|
+
}, { provider: 'openrouter', action: 'chat' });
|
|
514
680
|
|
|
515
681
|
const data = await response.json() as Record<string, unknown>;
|
|
516
682
|
|
|
517
683
|
if (!response.ok) {
|
|
518
684
|
const errorData = data.error as Record<string, unknown> | undefined;
|
|
519
|
-
return
|
|
685
|
+
return createErrorResult('openrouter', 'chat', (errorData?.message as string) || 'Chat failed', statusToErrorCode(response.status));
|
|
520
686
|
}
|
|
521
687
|
|
|
522
688
|
const choices = data.choices as Array<Record<string, unknown>> | undefined;
|
|
@@ -542,10 +708,10 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
542
708
|
const { text, voice_id = '21m00Tcm4TlvDq8ikWAM', model_id = 'eleven_monolingual_v1' } = params;
|
|
543
709
|
|
|
544
710
|
if (!text) {
|
|
545
|
-
return
|
|
711
|
+
return createErrorResult('elevenlabs', 'text_to_speech', 'Missing required param: text', ERROR_CODES.INVALID_PARAMS);
|
|
546
712
|
}
|
|
547
713
|
|
|
548
|
-
const response = await
|
|
714
|
+
const response = await fetchWithRetry(
|
|
549
715
|
`https://api.elevenlabs.io/v1/text-to-speech/${voice_id}`,
|
|
550
716
|
{
|
|
551
717
|
method: 'POST',
|
|
@@ -554,12 +720,13 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
554
720
|
'Content-Type': 'application/json',
|
|
555
721
|
},
|
|
556
722
|
body: JSON.stringify({ text, model_id }),
|
|
557
|
-
}
|
|
723
|
+
},
|
|
724
|
+
{ provider: 'elevenlabs', action: 'text_to_speech' }
|
|
558
725
|
);
|
|
559
726
|
|
|
560
727
|
if (!response.ok) {
|
|
561
728
|
const error = await response.json().catch(() => ({})) as Record<string, unknown>;
|
|
562
|
-
return
|
|
729
|
+
return createErrorResult('elevenlabs', 'text_to_speech', (error.detail as string) || 'TTS failed', statusToErrorCode(response.status));
|
|
563
730
|
}
|
|
564
731
|
|
|
565
732
|
// Return audio as base64
|
|
@@ -585,17 +752,17 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
585
752
|
const { model, input } = params;
|
|
586
753
|
|
|
587
754
|
if (!model) {
|
|
588
|
-
return
|
|
755
|
+
return createErrorResult('replicate', 'run', 'Missing required param: model (e.g., "stability-ai/sdxl:...")', ERROR_CODES.INVALID_PARAMS);
|
|
589
756
|
}
|
|
590
757
|
if (!input) {
|
|
591
|
-
return
|
|
758
|
+
return createErrorResult('replicate', 'run', 'Missing required param: input (object with model inputs)', ERROR_CODES.INVALID_PARAMS);
|
|
592
759
|
}
|
|
593
760
|
|
|
594
761
|
// Parse model into owner/name and version
|
|
595
762
|
const [modelPath, version] = model.split(':');
|
|
596
763
|
|
|
597
764
|
// Create prediction
|
|
598
|
-
const response = await
|
|
765
|
+
const response = await fetchWithRetry('https://api.replicate.com/v1/predictions', {
|
|
599
766
|
method: 'POST',
|
|
600
767
|
headers: {
|
|
601
768
|
'Authorization': `Bearer ${creds.api_key}`,
|
|
@@ -606,11 +773,11 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
606
773
|
model: version ? undefined : modelPath,
|
|
607
774
|
input,
|
|
608
775
|
}),
|
|
609
|
-
});
|
|
776
|
+
}, { provider: 'replicate', action: 'run' });
|
|
610
777
|
|
|
611
778
|
if (!response.ok) {
|
|
612
779
|
const error = await response.json().catch(() => ({})) as Record<string, unknown>;
|
|
613
|
-
return
|
|
780
|
+
return createErrorResult('replicate', 'run', (error.detail as string) || 'Prediction failed', statusToErrorCode(response.status));
|
|
614
781
|
}
|
|
615
782
|
|
|
616
783
|
const prediction = await response.json() as Record<string, unknown>;
|
|
@@ -632,15 +799,15 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
632
799
|
}
|
|
633
800
|
};
|
|
634
801
|
}
|
|
635
|
-
await
|
|
636
|
-
const pollResponse = await
|
|
802
|
+
await sleep(1000);
|
|
803
|
+
const pollResponse = await fetchWithRetry((result.urls as Record<string, string>)?.get || `https://api.replicate.com/v1/predictions/${result.id}`, {
|
|
637
804
|
headers: { 'Authorization': `Bearer ${creds.api_key}` },
|
|
638
|
-
});
|
|
805
|
+
}, { provider: 'replicate', action: 'run_poll' });
|
|
639
806
|
result = await pollResponse.json() as Record<string, unknown>;
|
|
640
807
|
}
|
|
641
808
|
|
|
642
809
|
if (result.status === 'failed') {
|
|
643
|
-
return
|
|
810
|
+
return createErrorResult('replicate', 'run', (result.error as string) || 'Prediction failed', ERROR_CODES.PROVIDER_ERROR);
|
|
644
811
|
}
|
|
645
812
|
|
|
646
813
|
return {
|
|
@@ -657,12 +824,12 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
657
824
|
},
|
|
658
825
|
|
|
659
826
|
list_models: async (_params, creds) => {
|
|
660
|
-
const response = await
|
|
827
|
+
const response = await fetchWithRetry('https://api.replicate.com/v1/models', {
|
|
661
828
|
headers: { 'Authorization': `Bearer ${creds.api_key}` },
|
|
662
|
-
});
|
|
829
|
+
}, { provider: 'replicate', action: 'list_models' });
|
|
663
830
|
|
|
664
831
|
if (!response.ok) {
|
|
665
|
-
return
|
|
832
|
+
return createErrorResult('replicate', 'list_models', 'Failed to list models', statusToErrorCode(response.status));
|
|
666
833
|
}
|
|
667
834
|
|
|
668
835
|
const data = await response.json() as Record<string, unknown>;
|
|
@@ -685,22 +852,22 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
685
852
|
const { url, formats = ['markdown'] } = params;
|
|
686
853
|
|
|
687
854
|
if (!url) {
|
|
688
|
-
return
|
|
855
|
+
return createErrorResult('firecrawl', 'scrape', 'Missing required param: url', ERROR_CODES.INVALID_PARAMS);
|
|
689
856
|
}
|
|
690
857
|
|
|
691
|
-
const response = await
|
|
858
|
+
const response = await fetchWithRetry('https://api.firecrawl.dev/v1/scrape', {
|
|
692
859
|
method: 'POST',
|
|
693
860
|
headers: {
|
|
694
861
|
'Authorization': `Bearer ${creds.api_key}`,
|
|
695
862
|
'Content-Type': 'application/json',
|
|
696
863
|
},
|
|
697
864
|
body: JSON.stringify({ url, formats }),
|
|
698
|
-
});
|
|
865
|
+
}, { provider: 'firecrawl', action: 'scrape' });
|
|
699
866
|
|
|
700
867
|
const data = await response.json() as Record<string, unknown>;
|
|
701
868
|
|
|
702
869
|
if (!response.ok || !data.success) {
|
|
703
|
-
return
|
|
870
|
+
return createErrorResult('firecrawl', 'scrape', (data.error as string) || 'Scrape failed', statusToErrorCode(response.status));
|
|
704
871
|
}
|
|
705
872
|
|
|
706
873
|
return {
|
|
@@ -715,22 +882,22 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
715
882
|
const { url, limit = 10 } = params;
|
|
716
883
|
|
|
717
884
|
if (!url) {
|
|
718
|
-
return
|
|
885
|
+
return createErrorResult('firecrawl', 'crawl', 'Missing required param: url', ERROR_CODES.INVALID_PARAMS);
|
|
719
886
|
}
|
|
720
887
|
|
|
721
|
-
const response = await
|
|
888
|
+
const response = await fetchWithRetry('https://api.firecrawl.dev/v1/crawl', {
|
|
722
889
|
method: 'POST',
|
|
723
890
|
headers: {
|
|
724
891
|
'Authorization': `Bearer ${creds.api_key}`,
|
|
725
892
|
'Content-Type': 'application/json',
|
|
726
893
|
},
|
|
727
894
|
body: JSON.stringify({ url, limit }),
|
|
728
|
-
});
|
|
895
|
+
}, { provider: 'firecrawl', action: 'crawl' });
|
|
729
896
|
|
|
730
897
|
const data = await response.json() as Record<string, unknown>;
|
|
731
898
|
|
|
732
899
|
if (!response.ok || !data.success) {
|
|
733
|
-
return
|
|
900
|
+
return createErrorResult('firecrawl', 'crawl', (data.error as string) || 'Crawl failed', statusToErrorCode(response.status));
|
|
734
901
|
}
|
|
735
902
|
|
|
736
903
|
return {
|
|
@@ -745,22 +912,22 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
745
912
|
const { url } = params;
|
|
746
913
|
|
|
747
914
|
if (!url) {
|
|
748
|
-
return
|
|
915
|
+
return createErrorResult('firecrawl', 'map', 'Missing required param: url', ERROR_CODES.INVALID_PARAMS);
|
|
749
916
|
}
|
|
750
917
|
|
|
751
|
-
const response = await
|
|
918
|
+
const response = await fetchWithRetry('https://api.firecrawl.dev/v1/map', {
|
|
752
919
|
method: 'POST',
|
|
753
920
|
headers: {
|
|
754
921
|
'Authorization': `Bearer ${creds.api_key}`,
|
|
755
922
|
'Content-Type': 'application/json',
|
|
756
923
|
},
|
|
757
924
|
body: JSON.stringify({ url }),
|
|
758
|
-
});
|
|
925
|
+
}, { provider: 'firecrawl', action: 'map' });
|
|
759
926
|
|
|
760
927
|
const data = await response.json() as Record<string, unknown>;
|
|
761
928
|
|
|
762
929
|
if (!response.ok || !data.success) {
|
|
763
|
-
return
|
|
930
|
+
return createErrorResult('firecrawl', 'map', (data.error as string) || 'Map failed', statusToErrorCode(response.status));
|
|
764
931
|
}
|
|
765
932
|
|
|
766
933
|
return {
|
|
@@ -778,21 +945,21 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
778
945
|
const { query, sort = 'stars', limit = 10 } = params;
|
|
779
946
|
|
|
780
947
|
if (!query) {
|
|
781
|
-
return
|
|
948
|
+
return createErrorResult('github', 'search_repos', 'Missing required param: query', ERROR_CODES.INVALID_PARAMS);
|
|
782
949
|
}
|
|
783
950
|
|
|
784
|
-
const response = await
|
|
951
|
+
const response = await fetchWithRetry(`https://api.github.com/search/repositories?q=${encodeURIComponent(query)}&sort=${sort}&per_page=${limit}`, {
|
|
785
952
|
headers: {
|
|
786
953
|
'Authorization': `Bearer ${creds.token}`,
|
|
787
954
|
'Accept': 'application/vnd.github+json',
|
|
788
955
|
'User-Agent': 'APIClaw',
|
|
789
956
|
},
|
|
790
|
-
});
|
|
957
|
+
}, { provider: 'github', action: 'search_repos' });
|
|
791
958
|
|
|
792
959
|
const data = await response.json() as Record<string, unknown>;
|
|
793
960
|
|
|
794
961
|
if (!response.ok) {
|
|
795
|
-
return
|
|
962
|
+
return createErrorResult('github', 'search_repos', (data.message as string) || 'Search failed', statusToErrorCode(response.status));
|
|
796
963
|
}
|
|
797
964
|
|
|
798
965
|
const items = (data.items as any[]) || [];
|
|
@@ -817,21 +984,21 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
817
984
|
const { owner, repo } = params;
|
|
818
985
|
|
|
819
986
|
if (!owner || !repo) {
|
|
820
|
-
return
|
|
987
|
+
return createErrorResult('github', 'get_repo', 'Missing required params: owner, repo', ERROR_CODES.INVALID_PARAMS);
|
|
821
988
|
}
|
|
822
989
|
|
|
823
|
-
const response = await
|
|
990
|
+
const response = await fetchWithRetry(`https://api.github.com/repos/${owner}/${repo}`, {
|
|
824
991
|
headers: {
|
|
825
992
|
'Authorization': `Bearer ${creds.token}`,
|
|
826
993
|
'Accept': 'application/vnd.github+json',
|
|
827
994
|
'User-Agent': 'APIClaw',
|
|
828
995
|
},
|
|
829
|
-
});
|
|
996
|
+
}, { provider: 'github', action: 'get_repo' });
|
|
830
997
|
|
|
831
998
|
const data = await response.json() as Record<string, unknown>;
|
|
832
999
|
|
|
833
1000
|
if (!response.ok) {
|
|
834
|
-
return
|
|
1001
|
+
return createErrorResult('github', 'get_repo', (data.message as string) || 'Get repo failed', statusToErrorCode(response.status));
|
|
835
1002
|
}
|
|
836
1003
|
|
|
837
1004
|
return {
|
|
@@ -855,21 +1022,21 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
855
1022
|
const { owner, repo, state = 'open', limit = 10 } = params;
|
|
856
1023
|
|
|
857
1024
|
if (!owner || !repo) {
|
|
858
|
-
return
|
|
1025
|
+
return createErrorResult('github', 'list_issues', 'Missing required params: owner, repo', ERROR_CODES.INVALID_PARAMS);
|
|
859
1026
|
}
|
|
860
1027
|
|
|
861
|
-
const response = await
|
|
1028
|
+
const response = await fetchWithRetry(`https://api.github.com/repos/${owner}/${repo}/issues?state=${state}&per_page=${limit}`, {
|
|
862
1029
|
headers: {
|
|
863
1030
|
'Authorization': `Bearer ${creds.token}`,
|
|
864
1031
|
'Accept': 'application/vnd.github+json',
|
|
865
1032
|
'User-Agent': 'APIClaw',
|
|
866
1033
|
},
|
|
867
|
-
});
|
|
1034
|
+
}, { provider: 'github', action: 'list_issues' });
|
|
868
1035
|
|
|
869
1036
|
const data = await response.json() as unknown[];
|
|
870
1037
|
|
|
871
1038
|
if (!response.ok) {
|
|
872
|
-
return
|
|
1039
|
+
return createErrorResult('github', 'list_issues', 'List issues failed', statusToErrorCode(response.status));
|
|
873
1040
|
}
|
|
874
1041
|
|
|
875
1042
|
return {
|
|
@@ -893,10 +1060,10 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
893
1060
|
const { owner, repo, title, body = '' } = params;
|
|
894
1061
|
|
|
895
1062
|
if (!owner || !repo || !title) {
|
|
896
|
-
return
|
|
1063
|
+
return createErrorResult('github', 'create_issue', 'Missing required params: owner, repo, title', ERROR_CODES.INVALID_PARAMS);
|
|
897
1064
|
}
|
|
898
1065
|
|
|
899
|
-
const response = await
|
|
1066
|
+
const response = await fetchWithRetry(`https://api.github.com/repos/${owner}/${repo}/issues`, {
|
|
900
1067
|
method: 'POST',
|
|
901
1068
|
headers: {
|
|
902
1069
|
'Authorization': `Bearer ${creds.token}`,
|
|
@@ -905,12 +1072,12 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
905
1072
|
'Content-Type': 'application/json',
|
|
906
1073
|
},
|
|
907
1074
|
body: JSON.stringify({ title, body }),
|
|
908
|
-
});
|
|
1075
|
+
}, { provider: 'github', action: 'create_issue' });
|
|
909
1076
|
|
|
910
1077
|
const data = await response.json() as Record<string, unknown>;
|
|
911
1078
|
|
|
912
1079
|
if (!response.ok) {
|
|
913
|
-
return
|
|
1080
|
+
return createErrorResult('github', 'create_issue', (data.message as string) || 'Create issue failed', statusToErrorCode(response.status));
|
|
914
1081
|
}
|
|
915
1082
|
|
|
916
1083
|
return {
|
|
@@ -928,21 +1095,21 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
928
1095
|
const { owner, repo, path } = params;
|
|
929
1096
|
|
|
930
1097
|
if (!owner || !repo || !path) {
|
|
931
|
-
return
|
|
1098
|
+
return createErrorResult('github', 'get_file', 'Missing required params: owner, repo, path', ERROR_CODES.INVALID_PARAMS);
|
|
932
1099
|
}
|
|
933
1100
|
|
|
934
|
-
const response = await
|
|
1101
|
+
const response = await fetchWithRetry(`https://api.github.com/repos/${owner}/${repo}/contents/${path}`, {
|
|
935
1102
|
headers: {
|
|
936
1103
|
'Authorization': `Bearer ${creds.token}`,
|
|
937
1104
|
'Accept': 'application/vnd.github+json',
|
|
938
1105
|
'User-Agent': 'APIClaw',
|
|
939
1106
|
},
|
|
940
|
-
});
|
|
1107
|
+
}, { provider: 'github', action: 'get_file' });
|
|
941
1108
|
|
|
942
1109
|
const data = await response.json() as Record<string, unknown>;
|
|
943
1110
|
|
|
944
1111
|
if (!response.ok) {
|
|
945
|
-
return
|
|
1112
|
+
return createErrorResult('github', 'get_file', (data.message as string) || 'Get file failed', statusToErrorCode(response.status));
|
|
946
1113
|
}
|
|
947
1114
|
|
|
948
1115
|
// Decode base64 content
|
|
@@ -969,7 +1136,7 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
969
1136
|
const { code, language = 'python' } = params;
|
|
970
1137
|
|
|
971
1138
|
if (!code) {
|
|
972
|
-
return
|
|
1139
|
+
return createErrorResult('e2b', 'run_code', 'Missing required param: code', ERROR_CODES.INVALID_PARAMS);
|
|
973
1140
|
}
|
|
974
1141
|
|
|
975
1142
|
try {
|
|
@@ -998,12 +1165,7 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
998
1165
|
await sandbox.kill().catch(() => {});
|
|
999
1166
|
}
|
|
1000
1167
|
} catch (error: any) {
|
|
1001
|
-
return
|
|
1002
|
-
success: false,
|
|
1003
|
-
provider: 'e2b',
|
|
1004
|
-
action: 'run_code',
|
|
1005
|
-
error: error.message || 'Code execution failed'
|
|
1006
|
-
};
|
|
1168
|
+
return createErrorResult('e2b', 'run_code', error.message || 'Code execution failed', ERROR_CODES.PROVIDER_ERROR);
|
|
1007
1169
|
}
|
|
1008
1170
|
},
|
|
1009
1171
|
|
|
@@ -1011,7 +1173,7 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
1011
1173
|
const { command } = params;
|
|
1012
1174
|
|
|
1013
1175
|
if (!command) {
|
|
1014
|
-
return
|
|
1176
|
+
return createErrorResult('e2b', 'run_shell', 'Missing required param: command', ERROR_CODES.INVALID_PARAMS);
|
|
1015
1177
|
}
|
|
1016
1178
|
|
|
1017
1179
|
try {
|
|
@@ -1038,12 +1200,7 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
1038
1200
|
await sandbox.kill().catch(() => {});
|
|
1039
1201
|
}
|
|
1040
1202
|
} catch (error: any) {
|
|
1041
|
-
return
|
|
1042
|
-
success: false,
|
|
1043
|
-
provider: 'e2b',
|
|
1044
|
-
action: 'run_shell',
|
|
1045
|
-
error: error.message || 'Shell execution failed'
|
|
1046
|
-
};
|
|
1203
|
+
return createErrorResult('e2b', 'run_shell', error.message || 'Shell execution failed', ERROR_CODES.PROVIDER_ERROR);
|
|
1047
1204
|
}
|
|
1048
1205
|
},
|
|
1049
1206
|
},
|
|
@@ -1098,30 +1255,30 @@ export async function executeAPICall(
|
|
|
1098
1255
|
// Check if it might be a dynamic provider without userId
|
|
1099
1256
|
const dynamicActions = await listDynamicActions(providerId);
|
|
1100
1257
|
if (dynamicActions.length > 0) {
|
|
1101
|
-
return
|
|
1102
|
-
|
|
1103
|
-
provider: providerId,
|
|
1258
|
+
return createErrorResult(
|
|
1259
|
+
providerId,
|
|
1104
1260
|
action,
|
|
1105
|
-
|
|
1106
|
-
|
|
1261
|
+
`Provider '${providerId}' requires userId for dynamic execution. Available actions: ${dynamicActions.join(', ')}`,
|
|
1262
|
+
ERROR_CODES.INVALID_PARAMS
|
|
1263
|
+
);
|
|
1107
1264
|
}
|
|
1108
|
-
return
|
|
1109
|
-
|
|
1110
|
-
provider: providerId,
|
|
1265
|
+
return createErrorResult(
|
|
1266
|
+
providerId,
|
|
1111
1267
|
action,
|
|
1112
|
-
|
|
1113
|
-
|
|
1268
|
+
`Provider '${providerId}' not connected. Available: ${Object.keys(handlers).join(', ')}`,
|
|
1269
|
+
ERROR_CODES.UNKNOWN_PROVIDER
|
|
1270
|
+
);
|
|
1114
1271
|
}
|
|
1115
1272
|
|
|
1116
1273
|
// Check if action exists
|
|
1117
1274
|
const handler = providerHandlers[action];
|
|
1118
1275
|
if (!handler) {
|
|
1119
|
-
return
|
|
1120
|
-
|
|
1121
|
-
provider: providerId,
|
|
1276
|
+
return createErrorResult(
|
|
1277
|
+
providerId,
|
|
1122
1278
|
action,
|
|
1123
|
-
|
|
1124
|
-
|
|
1279
|
+
`Action '${action}' not available for ${providerId}. Available: ${Object.keys(providerHandlers).join(', ')}`,
|
|
1280
|
+
ERROR_CODES.UNKNOWN_ACTION
|
|
1281
|
+
);
|
|
1125
1282
|
}
|
|
1126
1283
|
|
|
1127
1284
|
// Providers that don't require credentials (free/open APIs)
|
|
@@ -1149,20 +1306,15 @@ export async function executeAPICall(
|
|
|
1149
1306
|
data: proxyResult,
|
|
1150
1307
|
});
|
|
1151
1308
|
} catch (e: any) {
|
|
1152
|
-
return
|
|
1153
|
-
success: false,
|
|
1154
|
-
provider: providerId,
|
|
1155
|
-
action,
|
|
1156
|
-
error: e.message || 'Proxy call failed',
|
|
1157
|
-
};
|
|
1309
|
+
return createErrorResult(providerId, action, e.message || 'Proxy call failed', ERROR_CODES.PROVIDER_ERROR);
|
|
1158
1310
|
}
|
|
1159
1311
|
}
|
|
1160
|
-
return
|
|
1161
|
-
|
|
1162
|
-
provider: providerId,
|
|
1312
|
+
return createErrorResult(
|
|
1313
|
+
providerId,
|
|
1163
1314
|
action,
|
|
1164
|
-
|
|
1165
|
-
|
|
1315
|
+
`No credentials configured for ${providerId}. Set up ~/.secrets/${providerId}.env`,
|
|
1316
|
+
ERROR_CODES.NO_CREDENTIALS
|
|
1317
|
+
);
|
|
1166
1318
|
}
|
|
1167
1319
|
|
|
1168
1320
|
// Execute and normalize response
|
|
@@ -1170,11 +1322,18 @@ export async function executeAPICall(
|
|
|
1170
1322
|
const result = await handler(params, creds);
|
|
1171
1323
|
return normalizeResponse(result);
|
|
1172
1324
|
} catch (error: any) {
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1325
|
+
// Check if it's a network/timeout error
|
|
1326
|
+
const errorMessage = error.message || 'Unknown error';
|
|
1327
|
+
let errorCode: ErrorCode = ERROR_CODES.PROVIDER_ERROR;
|
|
1328
|
+
|
|
1329
|
+
if (errorMessage.includes('Max retries exceeded')) {
|
|
1330
|
+
errorCode = ERROR_CODES.MAX_RETRIES_EXCEEDED;
|
|
1331
|
+
} else if (errorMessage.includes('timeout') || errorMessage.includes('ETIMEDOUT')) {
|
|
1332
|
+
errorCode = ERROR_CODES.TIMEOUT;
|
|
1333
|
+
} else if (errorMessage.includes('ECONNREFUSED') || errorMessage.includes('ENOTFOUND') || errorMessage.includes('fetch')) {
|
|
1334
|
+
errorCode = ERROR_CODES.NETWORK_ERROR;
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
return createErrorResult(providerId, action, errorMessage, errorCode);
|
|
1179
1338
|
}
|
|
1180
1339
|
}
|