@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/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 { success: false, provider: '46elks', action: 'send_sms', error: 'Missing required params: to, message' };
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 fetch('https://api.46elks.com/a1/sms', {
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 { success: false, provider: '46elks', action: 'send_sms', error: (data.message as string) || 'SMS failed' };
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 { success: false, provider: 'twilio', action: 'send_sms', error: 'Missing required params: to, message' };
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 fetch(
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 { success: false, provider: 'twilio', action: 'send_sms', error: (data.message as string) || 'SMS failed' };
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 { success: false, provider: 'brave_search', action: 'search', error: 'Missing required param: query' };
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 fetch(url.toString(), {
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 { success: false, provider: 'brave_search', action: 'search', error: (data.message as string) || 'Search failed' };
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 { success: false, provider: 'resend', action: 'send_email', error: 'Missing required params: to, subject, html or text' };
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 fetch('https://api.resend.com/emails', {
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 { success: false, provider: 'resend', action: 'send_email', error: (data.message as string) || 'Email failed' };
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 { success: false, provider: 'openrouter', action: 'chat', error: 'Missing required param: messages (array)' };
645
+ return createErrorResult('openrouter', 'chat', 'Missing required param: messages (array)', ERROR_CODES.INVALID_PARAMS);
503
646
  }
504
647
 
505
- const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
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 { success: false, provider: 'openrouter', action: 'chat', error: (errorData?.message as string) || 'Chat failed' };
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 { success: false, provider: 'elevenlabs', action: 'text_to_speech', error: 'Missing required param: text' };
688
+ return createErrorResult('elevenlabs', 'text_to_speech', 'Missing required param: text', ERROR_CODES.INVALID_PARAMS);
546
689
  }
547
690
 
548
- const response = await fetch(
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 { success: false, provider: 'elevenlabs', action: 'text_to_speech', error: (error.detail as string) || 'TTS failed' };
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 { success: false, provider: 'replicate', action: 'run', error: 'Missing required param: model (e.g., "stability-ai/sdxl:...")' };
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 { success: false, provider: 'replicate', action: 'run', error: 'Missing required param: input (object with model inputs)' };
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 fetch('https://api.replicate.com/v1/predictions', {
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 { success: false, provider: 'replicate', action: 'run', error: (error.detail as string) || 'Prediction failed' };
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 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}`, {
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 { success: false, provider: 'replicate', action: 'run', error: (result.error as string) || 'Prediction failed' };
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 fetch('https://api.replicate.com/v1/models', {
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 { success: false, provider: 'replicate', action: 'list_models', error: 'Failed to list models' };
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 { success: false, provider: 'firecrawl', action: 'scrape', error: 'Missing required param: url' };
832
+ return createErrorResult('firecrawl', 'scrape', 'Missing required param: url', ERROR_CODES.INVALID_PARAMS);
689
833
  }
690
834
 
691
- const response = await fetch('https://api.firecrawl.dev/v1/scrape', {
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 { success: false, provider: 'firecrawl', action: 'scrape', error: (data.error as string) || 'Scrape failed' };
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 { success: false, provider: 'firecrawl', action: 'crawl', error: 'Missing required param: url' };
862
+ return createErrorResult('firecrawl', 'crawl', 'Missing required param: url', ERROR_CODES.INVALID_PARAMS);
719
863
  }
720
864
 
721
- const response = await fetch('https://api.firecrawl.dev/v1/crawl', {
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 { success: false, provider: 'firecrawl', action: 'crawl', error: (data.error as string) || 'Crawl failed' };
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 { success: false, provider: 'firecrawl', action: 'map', error: 'Missing required param: url' };
892
+ return createErrorResult('firecrawl', 'map', 'Missing required param: url', ERROR_CODES.INVALID_PARAMS);
749
893
  }
750
894
 
751
- const response = await fetch('https://api.firecrawl.dev/v1/map', {
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 { success: false, provider: 'firecrawl', action: 'map', error: (data.error as string) || 'Map failed' };
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 { success: false, provider: 'github', action: 'search_repos', error: 'Missing required param: query' };
925
+ return createErrorResult('github', 'search_repos', 'Missing required param: query', ERROR_CODES.INVALID_PARAMS);
782
926
  }
783
927
 
784
- const response = await fetch(`https://api.github.com/search/repositories?q=${encodeURIComponent(query)}&sort=${sort}&per_page=${limit}`, {
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 { success: false, provider: 'github', action: 'search_repos', error: (data.message as string) || 'Search failed' };
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 { success: false, provider: 'github', action: 'get_repo', error: 'Missing required params: owner, repo' };
964
+ return createErrorResult('github', 'get_repo', 'Missing required params: owner, repo', ERROR_CODES.INVALID_PARAMS);
821
965
  }
822
966
 
823
- const response = await fetch(`https://api.github.com/repos/${owner}/${repo}`, {
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 { success: false, provider: 'github', action: 'get_repo', error: (data.message as string) || 'Get repo failed' };
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 { success: false, provider: 'github', action: 'list_issues', error: 'Missing required params: owner, repo' };
1002
+ return createErrorResult('github', 'list_issues', 'Missing required params: owner, repo', ERROR_CODES.INVALID_PARAMS);
859
1003
  }
860
1004
 
861
- const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/issues?state=${state}&per_page=${limit}`, {
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 { success: false, provider: 'github', action: 'list_issues', error: 'List issues failed' };
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 { success: false, provider: 'github', action: 'create_issue', error: 'Missing required params: owner, repo, title' };
1040
+ return createErrorResult('github', 'create_issue', 'Missing required params: owner, repo, title', ERROR_CODES.INVALID_PARAMS);
897
1041
  }
898
1042
 
899
- const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/issues`, {
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 { success: false, provider: 'github', action: 'create_issue', error: (data.message as string) || 'Create issue failed' };
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 { success: false, provider: 'github', action: 'get_file', error: 'Missing required params: owner, repo, path' };
1075
+ return createErrorResult('github', 'get_file', 'Missing required params: owner, repo, path', ERROR_CODES.INVALID_PARAMS);
932
1076
  }
933
1077
 
934
- const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/contents/${path}`, {
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 { success: false, provider: 'github', action: 'get_file', error: (data.message as string) || 'Get file failed' };
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 { success: false, provider: 'e2b', action: 'run_code', error: 'Missing required param: code' };
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 { success: false, provider: 'e2b', action: 'run_shell', error: 'Missing required param: command' };
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
- success: false,
1103
- provider: providerId,
1235
+ return createErrorResult(
1236
+ providerId,
1104
1237
  action,
1105
- error: `Provider '${providerId}' requires userId for dynamic execution. Available actions: ${dynamicActions.join(', ')}`,
1106
- };
1238
+ `Provider '${providerId}' requires userId for dynamic execution. Available actions: ${dynamicActions.join(', ')}`,
1239
+ ERROR_CODES.INVALID_PARAMS
1240
+ );
1107
1241
  }
1108
- return {
1109
- success: false,
1110
- provider: providerId,
1242
+ return createErrorResult(
1243
+ providerId,
1111
1244
  action,
1112
- error: `Provider '${providerId}' not connected. Available: ${Object.keys(handlers).join(', ')}`,
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
- success: false,
1121
- provider: providerId,
1253
+ return createErrorResult(
1254
+ providerId,
1122
1255
  action,
1123
- error: `Action '${action}' not available for ${providerId}. Available: ${Object.keys(providerHandlers).join(', ')}`,
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
- success: false,
1162
- provider: providerId,
1289
+ return createErrorResult(
1290
+ providerId,
1163
1291
  action,
1164
- error: `No credentials configured for ${providerId}. Set up ~/.secrets/${providerId}.env`,
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
- return {
1174
- success: false,
1175
- provider: providerId,
1176
- action,
1177
- error: error.message || 'Unknown error',
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
  }