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