@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.
Files changed (52) hide show
  1. package/PRD-API-CHAINING.md +483 -0
  2. package/PRD-HARDEN-SHELL.md +278 -0
  3. package/README.md +72 -0
  4. package/convex/_generated/api.d.ts +4 -0
  5. package/convex/chains.ts +1095 -0
  6. package/convex/crons.ts +11 -0
  7. package/convex/logs.ts +107 -0
  8. package/convex/schema.ts +107 -0
  9. package/convex/spendAlerts.ts +442 -0
  10. package/convex/workspaces.ts +26 -0
  11. package/dist/chain-types.d.ts +187 -0
  12. package/dist/chain-types.d.ts.map +1 -0
  13. package/dist/chain-types.js +33 -0
  14. package/dist/chain-types.js.map +1 -0
  15. package/dist/chainExecutor.d.ts +122 -0
  16. package/dist/chainExecutor.d.ts.map +1 -0
  17. package/dist/chainExecutor.js +454 -0
  18. package/dist/chainExecutor.js.map +1 -0
  19. package/dist/chainResolver.d.ts +100 -0
  20. package/dist/chainResolver.d.ts.map +1 -0
  21. package/dist/chainResolver.js +519 -0
  22. package/dist/chainResolver.js.map +1 -0
  23. package/dist/chainResolver.test.d.ts +5 -0
  24. package/dist/chainResolver.test.d.ts.map +1 -0
  25. package/dist/chainResolver.test.js +201 -0
  26. package/dist/chainResolver.test.js.map +1 -0
  27. package/dist/execute.d.ts +5 -1
  28. package/dist/execute.d.ts.map +1 -1
  29. package/dist/execute.js +207 -118
  30. package/dist/execute.js.map +1 -1
  31. package/dist/index.js +382 -2
  32. package/dist/index.js.map +1 -1
  33. package/landing/package-lock.json +29 -5
  34. package/landing/package.json +2 -1
  35. package/landing/public/logos/chattgpt.svg +1 -0
  36. package/landing/public/logos/claude.svg +1 -0
  37. package/landing/public/logos/gemini.svg +1 -0
  38. package/landing/public/logos/grok.svg +1 -0
  39. package/landing/src/app/page.tsx +11 -0
  40. package/landing/src/app/security/page.tsx +381 -0
  41. package/landing/src/app/workspace/chains/page.tsx +520 -0
  42. package/landing/src/components/AITestimonials.tsx +195 -0
  43. package/landing/src/components/ChainStepDetail.tsx +310 -0
  44. package/landing/src/components/ChainTrace.tsx +261 -0
  45. package/landing/src/lib/stats.json +1 -1
  46. package/package.json +1 -1
  47. package/src/chain-types.ts +270 -0
  48. package/src/chainExecutor.ts +730 -0
  49. package/src/chainResolver.test.ts +246 -0
  50. package/src/chainResolver.ts +658 -0
  51. package/src/execute.ts +273 -114
  52. 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 { success: false, provider: '46elks', action: 'send_sms', error: 'Missing required params: to, message' };
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 fetch('https://api.46elks.com/a1/sms', {
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 { success: false, provider: '46elks', action: 'send_sms', error: (data.message as string) || 'SMS failed' };
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 { success: false, provider: 'twilio', action: 'send_sms', error: 'Missing required params: to, message' };
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 fetch(
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 { success: false, provider: 'twilio', action: 'send_sms', error: (data.message as string) || 'SMS failed' };
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 { success: false, provider: 'brave_search', action: 'search', error: 'Missing required param: query' };
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 fetch(url.toString(), {
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 { success: false, provider: 'brave_search', action: 'search', error: (data.message as string) || 'Search failed' };
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 { success: false, provider: 'resend', action: 'send_email', error: 'Missing required params: to, subject, html or text' };
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 fetch('https://api.resend.com/emails', {
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 { success: false, provider: 'resend', action: 'send_email', error: (data.message as string) || 'Email failed' };
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 { success: false, provider: 'openrouter', action: 'chat', error: 'Missing required param: messages (array)' };
668
+ return createErrorResult('openrouter', 'chat', 'Missing required param: messages (array)', ERROR_CODES.INVALID_PARAMS);
503
669
  }
504
670
 
505
- const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
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 { success: false, provider: 'openrouter', action: 'chat', error: (errorData?.message as string) || 'Chat failed' };
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 { success: false, provider: 'elevenlabs', action: 'text_to_speech', error: 'Missing required param: text' };
711
+ return createErrorResult('elevenlabs', 'text_to_speech', 'Missing required param: text', ERROR_CODES.INVALID_PARAMS);
546
712
  }
547
713
 
548
- const response = await fetch(
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 { success: false, provider: 'elevenlabs', action: 'text_to_speech', error: (error.detail as string) || 'TTS failed' };
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 { success: false, provider: 'replicate', action: 'run', error: 'Missing required param: model (e.g., "stability-ai/sdxl:...")' };
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 { success: false, provider: 'replicate', action: 'run', error: 'Missing required param: input (object with model inputs)' };
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 fetch('https://api.replicate.com/v1/predictions', {
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 { success: false, provider: 'replicate', action: 'run', error: (error.detail as string) || 'Prediction failed' };
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 new Promise(resolve => setTimeout(resolve, 1000));
636
- const pollResponse = await fetch((result.urls as Record<string, string>)?.get || `https://api.replicate.com/v1/predictions/${result.id}`, {
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 { success: false, provider: 'replicate', action: 'run', error: (result.error as string) || 'Prediction failed' };
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 fetch('https://api.replicate.com/v1/models', {
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 { success: false, provider: 'replicate', action: 'list_models', error: 'Failed to list models' };
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 { success: false, provider: 'firecrawl', action: 'scrape', error: 'Missing required param: url' };
855
+ return createErrorResult('firecrawl', 'scrape', 'Missing required param: url', ERROR_CODES.INVALID_PARAMS);
689
856
  }
690
857
 
691
- const response = await fetch('https://api.firecrawl.dev/v1/scrape', {
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 { success: false, provider: 'firecrawl', action: 'scrape', error: (data.error as string) || 'Scrape failed' };
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 { success: false, provider: 'firecrawl', action: 'crawl', error: 'Missing required param: url' };
885
+ return createErrorResult('firecrawl', 'crawl', 'Missing required param: url', ERROR_CODES.INVALID_PARAMS);
719
886
  }
720
887
 
721
- const response = await fetch('https://api.firecrawl.dev/v1/crawl', {
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 { success: false, provider: 'firecrawl', action: 'crawl', error: (data.error as string) || 'Crawl failed' };
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 { success: false, provider: 'firecrawl', action: 'map', error: 'Missing required param: url' };
915
+ return createErrorResult('firecrawl', 'map', 'Missing required param: url', ERROR_CODES.INVALID_PARAMS);
749
916
  }
750
917
 
751
- const response = await fetch('https://api.firecrawl.dev/v1/map', {
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 { success: false, provider: 'firecrawl', action: 'map', error: (data.error as string) || 'Map failed' };
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 { success: false, provider: 'github', action: 'search_repos', error: 'Missing required param: query' };
948
+ return createErrorResult('github', 'search_repos', 'Missing required param: query', ERROR_CODES.INVALID_PARAMS);
782
949
  }
783
950
 
784
- const response = await fetch(`https://api.github.com/search/repositories?q=${encodeURIComponent(query)}&sort=${sort}&per_page=${limit}`, {
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 { success: false, provider: 'github', action: 'search_repos', error: (data.message as string) || 'Search failed' };
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 { success: false, provider: 'github', action: 'get_repo', error: 'Missing required params: owner, repo' };
987
+ return createErrorResult('github', 'get_repo', 'Missing required params: owner, repo', ERROR_CODES.INVALID_PARAMS);
821
988
  }
822
989
 
823
- const response = await fetch(`https://api.github.com/repos/${owner}/${repo}`, {
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 { success: false, provider: 'github', action: 'get_repo', error: (data.message as string) || 'Get repo failed' };
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 { success: false, provider: 'github', action: 'list_issues', error: 'Missing required params: owner, repo' };
1025
+ return createErrorResult('github', 'list_issues', 'Missing required params: owner, repo', ERROR_CODES.INVALID_PARAMS);
859
1026
  }
860
1027
 
861
- const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/issues?state=${state}&per_page=${limit}`, {
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 { success: false, provider: 'github', action: 'list_issues', error: 'List issues failed' };
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 { success: false, provider: 'github', action: 'create_issue', error: 'Missing required params: owner, repo, title' };
1063
+ return createErrorResult('github', 'create_issue', 'Missing required params: owner, repo, title', ERROR_CODES.INVALID_PARAMS);
897
1064
  }
898
1065
 
899
- const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/issues`, {
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 { success: false, provider: 'github', action: 'create_issue', error: (data.message as string) || 'Create issue failed' };
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 { success: false, provider: 'github', action: 'get_file', error: 'Missing required params: owner, repo, path' };
1098
+ return createErrorResult('github', 'get_file', 'Missing required params: owner, repo, path', ERROR_CODES.INVALID_PARAMS);
932
1099
  }
933
1100
 
934
- const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/contents/${path}`, {
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 { success: false, provider: 'github', action: 'get_file', error: (data.message as string) || 'Get file failed' };
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 { success: false, provider: 'e2b', action: 'run_code', error: 'Missing required param: code' };
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 { success: false, provider: 'e2b', action: 'run_shell', error: 'Missing required param: command' };
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
- success: false,
1103
- provider: providerId,
1258
+ return createErrorResult(
1259
+ providerId,
1104
1260
  action,
1105
- error: `Provider '${providerId}' requires userId for dynamic execution. Available actions: ${dynamicActions.join(', ')}`,
1106
- };
1261
+ `Provider '${providerId}' requires userId for dynamic execution. Available actions: ${dynamicActions.join(', ')}`,
1262
+ ERROR_CODES.INVALID_PARAMS
1263
+ );
1107
1264
  }
1108
- return {
1109
- success: false,
1110
- provider: providerId,
1265
+ return createErrorResult(
1266
+ providerId,
1111
1267
  action,
1112
- error: `Provider '${providerId}' not connected. Available: ${Object.keys(handlers).join(', ')}`,
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
- success: false,
1121
- provider: providerId,
1276
+ return createErrorResult(
1277
+ providerId,
1122
1278
  action,
1123
- error: `Action '${action}' not available for ${providerId}. Available: ${Object.keys(providerHandlers).join(', ')}`,
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
- success: false,
1162
- provider: providerId,
1312
+ return createErrorResult(
1313
+ providerId,
1163
1314
  action,
1164
- error: `No credentials configured for ${providerId}. Set up ~/.secrets/${providerId}.env`,
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
- return {
1174
- success: false,
1175
- provider: providerId,
1176
- action,
1177
- error: error.message || 'Unknown error',
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
  }