@nordsym/apiclaw 1.3.13 → 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 (44) hide show
  1. package/PRD-API-CHAINING.md +483 -0
  2. package/PRD-HARDEN-SHELL.md +18 -12
  3. package/convex/_generated/api.d.ts +2 -0
  4. package/convex/chains.ts +1095 -0
  5. package/convex/schema.ts +101 -0
  6. package/dist/chain-types.d.ts +187 -0
  7. package/dist/chain-types.d.ts.map +1 -0
  8. package/dist/chain-types.js +33 -0
  9. package/dist/chain-types.js.map +1 -0
  10. package/dist/chainExecutor.d.ts +122 -0
  11. package/dist/chainExecutor.d.ts.map +1 -0
  12. package/dist/chainExecutor.js +454 -0
  13. package/dist/chainExecutor.js.map +1 -0
  14. package/dist/chainResolver.d.ts +100 -0
  15. package/dist/chainResolver.d.ts.map +1 -0
  16. package/dist/chainResolver.js +519 -0
  17. package/dist/chainResolver.js.map +1 -0
  18. package/dist/chainResolver.test.d.ts +5 -0
  19. package/dist/chainResolver.test.d.ts.map +1 -0
  20. package/dist/chainResolver.test.js +201 -0
  21. package/dist/chainResolver.test.js.map +1 -0
  22. package/dist/execute.d.ts +4 -1
  23. package/dist/execute.d.ts.map +1 -1
  24. package/dist/execute.js +3 -0
  25. package/dist/execute.js.map +1 -1
  26. package/dist/index.js +382 -2
  27. package/dist/index.js.map +1 -1
  28. package/landing/public/logos/chattgpt.svg +1 -0
  29. package/landing/public/logos/claude.svg +1 -0
  30. package/landing/public/logos/gemini.svg +1 -0
  31. package/landing/public/logos/grok.svg +1 -0
  32. package/landing/src/app/page.tsx +12 -21
  33. package/landing/src/app/workspace/chains/page.tsx +520 -0
  34. package/landing/src/components/AITestimonials.tsx +15 -9
  35. package/landing/src/components/ChainStepDetail.tsx +310 -0
  36. package/landing/src/components/ChainTrace.tsx +261 -0
  37. package/landing/src/lib/stats.json +1 -1
  38. package/package.json +1 -1
  39. package/src/chain-types.ts +270 -0
  40. package/src/chainExecutor.ts +730 -0
  41. package/src/chainResolver.test.ts +246 -0
  42. package/src/chainResolver.ts +658 -0
  43. package/src/execute.ts +23 -0
  44. package/src/index.ts +423 -2
package/src/index.ts CHANGED
@@ -51,6 +51,16 @@ import {
51
51
  METERED_BILLING
52
52
  } from './stripe.js';
53
53
  import { estimateCost } from './metered.js';
54
+ import {
55
+ executeChain,
56
+ getChainStatus,
57
+ resumeChain,
58
+ type ChainDefinition,
59
+ type ChainResult,
60
+ type Credentials as ChainCredentials,
61
+ type ChainOptions,
62
+ type ChainStepUnion
63
+ } from './chainExecutor.js';
54
64
 
55
65
  // Default agent ID for MVP (in production, this would come from auth)
56
66
  const DEFAULT_AGENT_ID = 'agent_default';
@@ -320,10 +330,28 @@ const tools: Tool[] = [
320
330
  },
321
331
  {
322
332
  name: 'call_api',
323
- description: 'Execute an API call through APIClaw. For actions that cost money (invoices, SMS), you will get a preview first and must confirm with the returned token. For free actions, executes immediately.',
333
+ description: `Execute an API call through APIClaw. Supports single calls AND multi-step chains.
334
+
335
+ SINGLE CALL: Provide provider + action + params
336
+ CHAIN: Provide chain array to execute multiple APIs in sequence/parallel with cross-step references.
337
+
338
+ Chain features:
339
+ - Sequential: Steps execute in order, each can reference previous results via $stepId.property
340
+ - Parallel: Use { parallel: [...steps] } to run concurrently
341
+ - Conditional: Use { if: "$step.success", then: {...}, else: {...} }
342
+ - Loops: Use { forEach: "$step.results", as: "item", do: {...} }
343
+ - Error handling: Per-step retry/fallback via onError
344
+ - Async: Set async: true to get chainId immediately, poll or use webhook
345
+
346
+ Example chain:
347
+ chain: [
348
+ { id: "search", provider: "brave_search", action: "search", params: { query: "AI agents" } },
349
+ { id: "summarize", provider: "openrouter", action: "chat", params: { message: "Summarize: $search.results" } }
350
+ ]`,
324
351
  inputSchema: {
325
352
  type: 'object',
326
353
  properties: {
354
+ // Single call params
327
355
  provider: {
328
356
  type: 'string',
329
357
  description: 'Provider ID (e.g., "46elks", "brave_search", "resend", "openrouter", "elevenlabs", "twilio", "coaccept", "frankfurter")'
@@ -347,9 +375,62 @@ const tools: Tool[] = [
347
375
  dry_run: {
348
376
  type: 'boolean',
349
377
  description: 'If true, shows what WOULD be sent without making actual API calls. Returns mock response and request details. Great for testing and debugging.'
378
+ },
379
+ // Chain execution params
380
+ chain: {
381
+ type: 'array',
382
+ description: 'Execute multiple API calls as a single chain. Each step can reference previous results via $stepId.property',
383
+ items: {
384
+ type: 'object',
385
+ properties: {
386
+ id: { type: 'string', description: 'Step identifier for cross-step references' },
387
+ provider: { type: 'string', description: 'API provider' },
388
+ action: { type: 'string', description: 'Action to execute' },
389
+ params: { type: 'object', description: 'Action parameters. Use $stepId.path for references.' },
390
+ parallel: { type: 'array', description: 'Steps to run in parallel' },
391
+ if: { type: 'string', description: 'Condition for conditional execution (e.g., "$step1.success")' },
392
+ then: { type: 'object', description: 'Step to execute if condition is true' },
393
+ else: { type: 'object', description: 'Step to execute if condition is false' },
394
+ forEach: { type: 'string', description: 'Array reference to iterate (e.g., "$search.results")' },
395
+ as: { type: 'string', description: 'Variable name for current item in loop' },
396
+ do: { type: 'object', description: 'Step to execute for each item' },
397
+ onError: {
398
+ type: 'object',
399
+ description: 'Error handling configuration',
400
+ properties: {
401
+ retry: {
402
+ type: 'object',
403
+ properties: {
404
+ attempts: { type: 'number', description: 'Max retry attempts' },
405
+ backoff: { type: 'string', description: '"exponential" or "linear" or array of ms delays' }
406
+ }
407
+ },
408
+ fallback: { type: 'object', description: 'Fallback step if this fails' },
409
+ abort: { type: 'boolean', description: 'Abort entire chain on failure' }
410
+ }
411
+ }
412
+ }
413
+ }
414
+ },
415
+ // Chain options
416
+ continueOnError: {
417
+ type: 'boolean',
418
+ description: 'Continue chain execution even if a step fails (default: false)'
419
+ },
420
+ timeout: {
421
+ type: 'number',
422
+ description: 'Maximum execution time for the entire chain in milliseconds'
423
+ },
424
+ async: {
425
+ type: 'boolean',
426
+ description: 'Return immediately with chainId. Use get_chain_status to poll or provide webhook.'
427
+ },
428
+ webhook: {
429
+ type: 'string',
430
+ description: 'URL to POST results when async chain completes'
350
431
  }
351
432
  },
352
- required: ['provider', 'action']
433
+ required: []
353
434
  }
354
435
  },
355
436
  {
@@ -485,6 +566,46 @@ const tools: Tool[] = [
485
566
  },
486
567
  required: ['call_count']
487
568
  }
569
+ },
570
+ // ============================================
571
+ // CHAIN MANAGEMENT TOOLS
572
+ // ============================================
573
+ {
574
+ name: 'get_chain_status',
575
+ description: 'Check the status of an async chain execution. Use the chainId returned from call_api with async: true.',
576
+ inputSchema: {
577
+ type: 'object',
578
+ properties: {
579
+ chain_id: {
580
+ type: 'string',
581
+ description: 'Chain ID returned from async chain execution'
582
+ }
583
+ },
584
+ required: ['chain_id']
585
+ }
586
+ },
587
+ {
588
+ name: 'resume_chain',
589
+ description: 'Resume a failed chain from the point of failure. Use the resumeToken from the error response. Requires the original chain definition.',
590
+ inputSchema: {
591
+ type: 'object',
592
+ properties: {
593
+ resume_token: {
594
+ type: 'string',
595
+ description: 'Resume token from a failed chain (e.g., "chain_xyz_step_2")'
596
+ },
597
+ original_chain: {
598
+ type: 'array',
599
+ description: 'The original chain definition that failed. Required to resume execution.',
600
+ items: { type: 'object' }
601
+ },
602
+ overrides: {
603
+ type: 'object',
604
+ description: 'Optional parameter overrides for specific steps. Format: { "stepId": { ...newParams } }'
605
+ }
606
+ },
607
+ required: ['resume_token', 'original_chain']
608
+ }
488
609
  }
489
610
  ];
490
611
 
@@ -775,6 +896,130 @@ Docs: https://apiclaw.nordsym.com
775
896
  const params = (args?.params as Record<string, any>) || {};
776
897
  const confirmToken = args?.confirm_token as string | undefined;
777
898
  const dryRun = args?.dry_run as boolean | undefined;
899
+ const chain = args?.chain as ChainStepUnion[] | undefined;
900
+
901
+ // ============================================
902
+ // CHAIN EXECUTION MODE
903
+ // ============================================
904
+ if (chain && Array.isArray(chain) && chain.length > 0) {
905
+ // Check workspace access for chains
906
+ const access = checkWorkspaceAccess();
907
+ if (!access.allowed) {
908
+ return {
909
+ content: [{
910
+ type: 'text',
911
+ text: JSON.stringify({
912
+ status: 'error',
913
+ error: access.error,
914
+ hint: 'Use register_owner to authenticate your workspace.',
915
+ }, null, 2)
916
+ }],
917
+ isError: true
918
+ };
919
+ }
920
+
921
+ try {
922
+ // Construct ChainDefinition from the input
923
+ const chainDefinition: ChainDefinition = {
924
+ steps: chain as ChainStepUnion[],
925
+ timeout: args?.timeout as number | undefined,
926
+ errorPolicy: args?.continueOnError
927
+ ? { mode: 'best-effort' as const }
928
+ : { mode: 'fail-fast' as const },
929
+ };
930
+
931
+ const chainCredentials: ChainCredentials = {
932
+ userId: DEFAULT_AGENT_ID,
933
+ customerKeys: {},
934
+ };
935
+
936
+ // Add customer key if provided
937
+ const customerKey = args?.customer_key as string | undefined;
938
+ if (customerKey) {
939
+ // Apply to all providers (or could be provider-specific)
940
+ chainCredentials.customerKeys = { default: customerKey };
941
+ }
942
+
943
+ const chainOptions: ChainOptions = {
944
+ verbose: false,
945
+ };
946
+
947
+ // Execute the chain
948
+ const chainResult = await executeChain(
949
+ chainDefinition,
950
+ chainCredentials,
951
+ {}, // inputs
952
+ chainOptions
953
+ );
954
+
955
+ // Track usage for chain (count completed steps)
956
+ if (chainResult.success && workspaceContext) {
957
+ const completedCount = chainResult.completedSteps.length;
958
+
959
+ for (let i = 0; i < completedCount; i++) {
960
+ try {
961
+ await convex.mutation("workspaces:incrementUsage" as any, {
962
+ workspaceId: workspaceContext.workspaceId as any,
963
+ });
964
+ } catch (e) {
965
+ console.error('[APIClaw] Failed to track chain usage:', e);
966
+ }
967
+ }
968
+ }
969
+
970
+ // Format response to match expected chain response format
971
+ return {
972
+ content: [{
973
+ type: 'text',
974
+ text: JSON.stringify({
975
+ status: chainResult.success ? 'success' : 'error',
976
+ mode: 'chain',
977
+ chainId: chainResult.chainId,
978
+ steps: chainResult.trace.map(t => ({
979
+ id: t.stepId,
980
+ status: t.success ? 'completed' : 'failed',
981
+ result: t.output,
982
+ error: t.error,
983
+ latencyMs: t.latencyMs,
984
+ cost: t.cost,
985
+ })),
986
+ finalResult: chainResult.finalResult,
987
+ totalLatencyMs: chainResult.totalLatencyMs,
988
+ totalCost: chainResult.totalCost,
989
+ tokensSaved: (chain.length - 1) * 500, // Estimate tokens saved
990
+ ...(chainResult.error ? {
991
+ completedSteps: chainResult.completedSteps,
992
+ failedStep: chainResult.failedStep ? {
993
+ id: chainResult.failedStep.stepId,
994
+ error: chainResult.failedStep.error,
995
+ code: chainResult.failedStep.errorCode,
996
+ } : undefined,
997
+ partialResults: chainResult.results,
998
+ canResume: chainResult.canResume,
999
+ resumeToken: chainResult.resumeToken,
1000
+ } : {}),
1001
+ }, null, 2)
1002
+ }],
1003
+ isError: !chainResult.success
1004
+ };
1005
+ } catch (error) {
1006
+ return {
1007
+ content: [{
1008
+ type: 'text',
1009
+ text: JSON.stringify({
1010
+ status: 'error',
1011
+ mode: 'chain',
1012
+ error: error instanceof Error ? error.message : String(error),
1013
+ }, null, 2)
1014
+ }],
1015
+ isError: true
1016
+ };
1017
+ }
1018
+ }
1019
+
1020
+ // ============================================
1021
+ // SINGLE CALL MODE (existing logic)
1022
+ // ============================================
778
1023
 
779
1024
  // Handle dry-run mode - no actual API calls, just show what would happen
780
1025
  if (dryRun) {
@@ -1484,6 +1729,182 @@ Docs: https://apiclaw.nordsym.com
1484
1729
  };
1485
1730
  }
1486
1731
 
1732
+ // ============================================
1733
+ // CHAIN MANAGEMENT TOOLS
1734
+ // ============================================
1735
+
1736
+ case 'get_chain_status': {
1737
+ const chainId = args?.chain_id as string;
1738
+
1739
+ if (!chainId) {
1740
+ return {
1741
+ content: [{
1742
+ type: 'text',
1743
+ text: JSON.stringify({
1744
+ status: 'error',
1745
+ error: 'chain_id is required'
1746
+ }, null, 2)
1747
+ }],
1748
+ isError: true
1749
+ };
1750
+ }
1751
+
1752
+ const chainStatus = await getChainStatus(chainId);
1753
+
1754
+ if (chainStatus.status === 'not_found') {
1755
+ return {
1756
+ content: [{
1757
+ type: 'text',
1758
+ text: JSON.stringify({
1759
+ status: 'error',
1760
+ error: `Chain not found: ${chainId}`,
1761
+ hint: 'Chain states expire after 1 hour. The chain may have completed or expired.'
1762
+ }, null, 2)
1763
+ }],
1764
+ isError: true
1765
+ };
1766
+ }
1767
+
1768
+ return {
1769
+ content: [{
1770
+ type: 'text',
1771
+ text: JSON.stringify({
1772
+ status: 'success',
1773
+ chain: {
1774
+ chainId: chainStatus.chainId,
1775
+ executionStatus: chainStatus.status,
1776
+ ...(chainStatus.result ? {
1777
+ result: {
1778
+ success: chainStatus.result.success,
1779
+ completedSteps: chainStatus.result.completedSteps,
1780
+ totalLatencyMs: chainStatus.result.totalLatencyMs,
1781
+ totalCost: chainStatus.result.totalCost,
1782
+ finalResult: chainStatus.result.finalResult,
1783
+ error: chainStatus.result.error,
1784
+ canResume: chainStatus.result.canResume,
1785
+ resumeToken: chainStatus.result.resumeToken,
1786
+ }
1787
+ } : {})
1788
+ }
1789
+ }, null, 2)
1790
+ }]
1791
+ };
1792
+ }
1793
+
1794
+ case 'resume_chain': {
1795
+ const resumeToken = args?.resume_token as string;
1796
+ const overrides = args?.overrides as Record<string, Record<string, any>> | undefined;
1797
+ const originalChain = args?.original_chain as ChainStepUnion[] | undefined;
1798
+
1799
+ if (!resumeToken) {
1800
+ return {
1801
+ content: [{
1802
+ type: 'text',
1803
+ text: JSON.stringify({
1804
+ status: 'error',
1805
+ error: 'resume_token is required'
1806
+ }, null, 2)
1807
+ }],
1808
+ isError: true
1809
+ };
1810
+ }
1811
+
1812
+ // Check workspace access
1813
+ const access = checkWorkspaceAccess();
1814
+ if (!access.allowed) {
1815
+ return {
1816
+ content: [{
1817
+ type: 'text',
1818
+ text: JSON.stringify({
1819
+ status: 'error',
1820
+ error: access.error,
1821
+ hint: 'Use register_owner to authenticate your workspace.',
1822
+ }, null, 2)
1823
+ }],
1824
+ isError: true
1825
+ };
1826
+ }
1827
+
1828
+ try {
1829
+ // Note: The resume_chain function requires the original chain definition
1830
+ // In practice, you'd store this or require the caller to provide it
1831
+ if (!originalChain) {
1832
+ return {
1833
+ content: [{
1834
+ type: 'text',
1835
+ text: JSON.stringify({
1836
+ status: 'error',
1837
+ error: 'original_chain is required to resume. Please provide the original chain definition.',
1838
+ hint: 'Pass original_chain: [...] with the same chain array used in the failed execution.'
1839
+ }, null, 2)
1840
+ }],
1841
+ isError: true
1842
+ };
1843
+ }
1844
+
1845
+ const chainDefinition: ChainDefinition = {
1846
+ steps: originalChain,
1847
+ };
1848
+
1849
+ const chainCredentials: ChainCredentials = {
1850
+ userId: DEFAULT_AGENT_ID,
1851
+ customerKeys: {},
1852
+ };
1853
+
1854
+ const customerKey = args?.customer_key as string | undefined;
1855
+ if (customerKey) {
1856
+ chainCredentials.customerKeys = { default: customerKey };
1857
+ }
1858
+
1859
+ const result = await resumeChain(
1860
+ resumeToken,
1861
+ chainDefinition,
1862
+ chainCredentials,
1863
+ {}, // inputs
1864
+ overrides,
1865
+ { verbose: false }
1866
+ );
1867
+
1868
+ return {
1869
+ content: [{
1870
+ type: 'text',
1871
+ text: JSON.stringify({
1872
+ status: result.success ? 'success' : 'error',
1873
+ mode: 'chain_resumed',
1874
+ chainId: result.chainId,
1875
+ steps: result.trace.map(t => ({
1876
+ id: t.stepId,
1877
+ status: t.success ? 'completed' : 'failed',
1878
+ result: t.output,
1879
+ error: t.error,
1880
+ latencyMs: t.latencyMs,
1881
+ })),
1882
+ finalResult: result.finalResult,
1883
+ totalLatencyMs: result.totalLatencyMs,
1884
+ totalCost: result.totalCost,
1885
+ ...(result.error ? {
1886
+ error: result.error,
1887
+ canResume: result.canResume,
1888
+ resumeToken: result.resumeToken,
1889
+ } : {}),
1890
+ }, null, 2)
1891
+ }],
1892
+ isError: !result.success
1893
+ };
1894
+ } catch (error) {
1895
+ return {
1896
+ content: [{
1897
+ type: 'text',
1898
+ text: JSON.stringify({
1899
+ status: 'error',
1900
+ error: error instanceof Error ? error.message : String(error),
1901
+ }, null, 2)
1902
+ }],
1903
+ isError: true
1904
+ };
1905
+ }
1906
+ }
1907
+
1487
1908
  default:
1488
1909
  return {
1489
1910
  content: [