@opensea/cli 0.4.0 → 0.4.2

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/README.md CHANGED
@@ -167,8 +167,9 @@ console.log(formatToon(data))
167
167
  ## Exit Codes
168
168
 
169
169
  - `0` - Success
170
- - `1` - API error
170
+ - `1` - API error (non-429)
171
171
  - `2` - Authentication error
172
+ - `3` - Rate limited (HTTP 429)
172
173
 
173
174
  ## Requirements
174
175
 
package/dist/cli.js CHANGED
@@ -1,26 +1,52 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { Command as Command10 } from "commander";
4
+ import { Command as Command11 } from "commander";
5
5
 
6
6
  // src/client.ts
7
7
  var DEFAULT_BASE_URL = "https://api.opensea.io";
8
- var DEFAULT_GRAPHQL_URL = "https://gql.opensea.io/graphql";
9
8
  var DEFAULT_TIMEOUT_MS = 3e4;
9
+ var USER_AGENT = `opensea-cli/${"0.4.2"}`;
10
+ var DEFAULT_MAX_RETRIES = 0;
11
+ var DEFAULT_RETRY_BASE_DELAY_MS = 1e3;
12
+ function isRetryableStatus(status, method) {
13
+ if (status === 429) return true;
14
+ return status >= 500 && method === "GET";
15
+ }
16
+ function parseRetryAfter(header) {
17
+ if (!header) return void 0;
18
+ const seconds = Number(header);
19
+ if (!Number.isNaN(seconds)) return seconds * 1e3;
20
+ const date = Date.parse(header);
21
+ if (!Number.isNaN(date)) return Math.max(0, date - Date.now());
22
+ return void 0;
23
+ }
24
+ function sleep(ms) {
25
+ return new Promise((resolve) => setTimeout(resolve, ms));
26
+ }
10
27
  var OpenSeaClient = class {
11
28
  apiKey;
12
29
  baseUrl;
13
- graphqlUrl;
14
30
  defaultChain;
15
31
  timeoutMs;
16
32
  verbose;
33
+ maxRetries;
34
+ retryBaseDelay;
17
35
  constructor(config) {
18
36
  this.apiKey = config.apiKey;
19
37
  this.baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
20
- this.graphqlUrl = config.graphqlUrl ?? DEFAULT_GRAPHQL_URL;
21
38
  this.defaultChain = config.chain ?? "ethereum";
22
39
  this.timeoutMs = config.timeout ?? DEFAULT_TIMEOUT_MS;
23
40
  this.verbose = config.verbose ?? false;
41
+ this.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
42
+ this.retryBaseDelay = config.retryBaseDelay ?? DEFAULT_RETRY_BASE_DELAY_MS;
43
+ }
44
+ get defaultHeaders() {
45
+ return {
46
+ Accept: "application/json",
47
+ "User-Agent": USER_AGENT,
48
+ "x-api-key": this.apiKey
49
+ };
24
50
  }
25
51
  async get(path, params) {
26
52
  const url = new URL(`${this.baseUrl}${path}`);
@@ -34,21 +60,14 @@ var OpenSeaClient = class {
34
60
  if (this.verbose) {
35
61
  console.error(`[verbose] GET ${url.toString()}`);
36
62
  }
37
- const response = await fetch(url.toString(), {
38
- method: "GET",
39
- headers: {
40
- Accept: "application/json",
41
- "x-api-key": this.apiKey
63
+ const response = await this.fetchWithRetry(
64
+ url.toString(),
65
+ {
66
+ method: "GET",
67
+ headers: this.defaultHeaders
42
68
  },
43
- signal: AbortSignal.timeout(this.timeoutMs)
44
- });
45
- if (this.verbose) {
46
- console.error(`[verbose] ${response.status} ${response.statusText}`);
47
- }
48
- if (!response.ok) {
49
- const body = await response.text();
50
- throw new OpenSeaAPIError(response.status, body, path);
51
- }
69
+ path
70
+ );
52
71
  return response.json();
53
72
  }
54
73
  async post(path, body, params) {
@@ -60,68 +79,67 @@ var OpenSeaClient = class {
60
79
  }
61
80
  }
62
81
  }
63
- const headers = {
64
- Accept: "application/json",
65
- "x-api-key": this.apiKey
66
- };
82
+ const headers = { ...this.defaultHeaders };
67
83
  if (body) {
68
84
  headers["Content-Type"] = "application/json";
69
85
  }
70
86
  if (this.verbose) {
71
87
  console.error(`[verbose] POST ${url.toString()}`);
72
88
  }
73
- const response = await fetch(url.toString(), {
74
- method: "POST",
75
- headers,
76
- body: body ? JSON.stringify(body) : void 0,
77
- signal: AbortSignal.timeout(this.timeoutMs)
78
- });
79
- if (this.verbose) {
80
- console.error(`[verbose] ${response.status} ${response.statusText}`);
81
- }
82
- if (!response.ok) {
83
- const text = await response.text();
84
- throw new OpenSeaAPIError(response.status, text, path);
85
- }
86
- return response.json();
87
- }
88
- async graphql(query, variables) {
89
- if (this.verbose) {
90
- console.error(`[verbose] POST ${this.graphqlUrl}`);
91
- }
92
- const response = await fetch(this.graphqlUrl, {
93
- method: "POST",
94
- headers: {
95
- "Content-Type": "application/json",
96
- Accept: "application/json",
97
- "x-api-key": this.apiKey
89
+ const response = await this.fetchWithRetry(
90
+ url.toString(),
91
+ {
92
+ method: "POST",
93
+ headers,
94
+ body: body ? JSON.stringify(body) : void 0
98
95
  },
99
- body: JSON.stringify({ query, variables }),
100
- signal: AbortSignal.timeout(this.timeoutMs)
101
- });
102
- if (this.verbose) {
103
- console.error(`[verbose] ${response.status} ${response.statusText}`);
104
- }
105
- if (!response.ok) {
106
- const body = await response.text();
107
- throw new OpenSeaAPIError(response.status, body, "graphql");
108
- }
109
- const json = await response.json();
110
- if (json.errors?.length) {
111
- throw new OpenSeaAPIError(
112
- 400,
113
- json.errors.map((e) => e.message).join("; "),
114
- "graphql"
115
- );
116
- }
117
- if (!json.data) {
118
- throw new OpenSeaAPIError(500, "GraphQL response missing data", "graphql");
119
- }
120
- return json.data;
96
+ path
97
+ );
98
+ return response.json();
121
99
  }
122
100
  getDefaultChain() {
123
101
  return this.defaultChain;
124
102
  }
103
+ getApiKeyPrefix() {
104
+ if (this.apiKey.length < 8) return "***";
105
+ return `${this.apiKey.slice(0, 4)}...`;
106
+ }
107
+ async fetchWithRetry(url, init, path) {
108
+ for (let attempt = 0; ; attempt++) {
109
+ const response = await fetch(url, {
110
+ ...init,
111
+ signal: AbortSignal.timeout(this.timeoutMs)
112
+ });
113
+ if (this.verbose) {
114
+ console.error(`[verbose] ${response.status} ${response.statusText}`);
115
+ }
116
+ if (response.ok) {
117
+ return response;
118
+ }
119
+ const method = init.method ?? "GET";
120
+ if (attempt < this.maxRetries && isRetryableStatus(response.status, method)) {
121
+ const retryAfterMs = parseRetryAfter(
122
+ response.headers.get("Retry-After")
123
+ );
124
+ const backoffMs = this.retryBaseDelay * 2 ** attempt;
125
+ const jitterMs = Math.random() * this.retryBaseDelay;
126
+ const delayMs = Math.max(retryAfterMs ?? 0, backoffMs) + jitterMs;
127
+ if (this.verbose) {
128
+ console.error(
129
+ `[verbose] Retry ${attempt + 1}/${this.maxRetries} after ${Math.round(delayMs)}ms (status ${response.status})`
130
+ );
131
+ }
132
+ try {
133
+ await response.body?.cancel();
134
+ } catch {
135
+ }
136
+ await sleep(delayMs);
137
+ continue;
138
+ }
139
+ const text = await response.text();
140
+ throw new OpenSeaAPIError(response.status, text, path);
141
+ }
142
+ }
125
143
  };
126
144
  var OpenSeaAPIError = class extends Error {
127
145
  constructor(statusCode, responseBody, path) {
@@ -407,14 +425,24 @@ function formatToon(data) {
407
425
  }
408
426
 
409
427
  // src/output.ts
428
+ var _outputOptions = {};
429
+ function setOutputOptions(options) {
430
+ _outputOptions = options;
431
+ }
410
432
  function formatOutput(data, format) {
433
+ const processed = _outputOptions.fields ? filterFields(data, _outputOptions.fields) : data;
434
+ let result;
411
435
  if (format === "table") {
412
- return formatTable(data);
436
+ result = formatTable(processed);
437
+ } else if (format === "toon") {
438
+ result = formatToon(processed);
439
+ } else {
440
+ result = JSON.stringify(processed, null, 2);
413
441
  }
414
- if (format === "toon") {
415
- return formatToon(data);
442
+ if (_outputOptions.maxLines != null) {
443
+ result = truncateOutput(result, _outputOptions.maxLines);
416
444
  }
417
- return JSON.stringify(data, null, 2);
445
+ return result;
418
446
  }
419
447
  function formatTable(data) {
420
448
  if (Array.isArray(data)) {
@@ -450,6 +478,31 @@ function formatTable(data) {
450
478
  }
451
479
  return String(data);
452
480
  }
481
+ function pickFields(obj, fields) {
482
+ const result = {};
483
+ for (const field of fields) {
484
+ if (field in obj) {
485
+ result[field] = obj[field];
486
+ }
487
+ }
488
+ return result;
489
+ }
490
+ function filterFields(data, fields) {
491
+ if (Array.isArray(data)) {
492
+ return data.map((item) => filterFields(item, fields));
493
+ }
494
+ if (data && typeof data === "object") {
495
+ return pickFields(data, fields);
496
+ }
497
+ return data;
498
+ }
499
+ function truncateOutput(text, maxLines) {
500
+ const lines = text.split("\n");
501
+ if (lines.length <= maxLines) return text;
502
+ const omitted = lines.length - maxLines;
503
+ return lines.slice(0, maxLines).join("\n") + `
504
+ ... (${omitted} more line${omitted === 1 ? "" : "s"})`;
505
+ }
453
506
 
454
507
  // src/commands/accounts.ts
455
508
  function accountsCommand(getClient2, getFormat2) {
@@ -586,10 +639,88 @@ function eventsCommand(getClient2, getFormat2) {
586
639
  return cmd;
587
640
  }
588
641
 
589
- // src/commands/listings.ts
642
+ // src/commands/health.ts
590
643
  import { Command as Command4 } from "commander";
644
+
645
+ // src/health.ts
646
+ async function checkHealth(client) {
647
+ const keyPrefix = client.getApiKeyPrefix();
648
+ try {
649
+ await client.get("/api/v2/collections", { limit: 1 });
650
+ } catch (error) {
651
+ let message;
652
+ if (error instanceof OpenSeaAPIError) {
653
+ message = error.statusCode === 429 ? "Rate limited: too many requests" : `API error (${error.statusCode}): ${error.responseBody}`;
654
+ } else {
655
+ message = `Network error: ${error.message}`;
656
+ }
657
+ return {
658
+ status: "error",
659
+ key_prefix: keyPrefix,
660
+ authenticated: false,
661
+ rate_limited: error instanceof OpenSeaAPIError && error.statusCode === 429,
662
+ message
663
+ };
664
+ }
665
+ try {
666
+ await client.get("/api/v2/listings/collection/boredapeyachtclub/all", {
667
+ limit: 1
668
+ });
669
+ return {
670
+ status: "ok",
671
+ key_prefix: keyPrefix,
672
+ authenticated: true,
673
+ rate_limited: false,
674
+ message: "Connectivity and authentication are working"
675
+ };
676
+ } catch (error) {
677
+ if (error instanceof OpenSeaAPIError) {
678
+ if (error.statusCode === 429) {
679
+ return {
680
+ status: "error",
681
+ key_prefix: keyPrefix,
682
+ authenticated: false,
683
+ rate_limited: true,
684
+ message: "Rate limited: too many requests"
685
+ };
686
+ }
687
+ if (error.statusCode === 401 || error.statusCode === 403) {
688
+ return {
689
+ status: "error",
690
+ key_prefix: keyPrefix,
691
+ authenticated: false,
692
+ rate_limited: false,
693
+ message: `Authentication failed (${error.statusCode}): invalid API key`
694
+ };
695
+ }
696
+ }
697
+ return {
698
+ status: "ok",
699
+ key_prefix: keyPrefix,
700
+ authenticated: false,
701
+ rate_limited: false,
702
+ message: "Connectivity is working but authentication could not be verified"
703
+ };
704
+ }
705
+ }
706
+
707
+ // src/commands/health.ts
708
+ function healthCommand(getClient2, getFormat2) {
709
+ const cmd = new Command4("health").description("Check API connectivity and authentication").action(async () => {
710
+ const client = getClient2();
711
+ const result = await checkHealth(client);
712
+ console.log(formatOutput(result, getFormat2()));
713
+ if (result.status === "error") {
714
+ process.exit(result.rate_limited ? 3 : 1);
715
+ }
716
+ });
717
+ return cmd;
718
+ }
719
+
720
+ // src/commands/listings.ts
721
+ import { Command as Command5 } from "commander";
591
722
  function listingsCommand(getClient2, getFormat2) {
592
- const cmd = new Command4("listings").description("Query NFT listings");
723
+ const cmd = new Command5("listings").description("Query NFT listings");
593
724
  cmd.command("all").description("Get all listings for a collection").argument("<collection>", "Collection slug").option("--limit <limit>", "Number of results", "20").option("--next <cursor>", "Pagination cursor").action(
594
725
  async (collection, options) => {
595
726
  const client = getClient2();
@@ -621,9 +752,9 @@ function listingsCommand(getClient2, getFormat2) {
621
752
  }
622
753
 
623
754
  // src/commands/nfts.ts
624
- import { Command as Command5 } from "commander";
755
+ import { Command as Command6 } from "commander";
625
756
  function nftsCommand(getClient2, getFormat2) {
626
- const cmd = new Command5("nfts").description("Query NFTs");
757
+ const cmd = new Command6("nfts").description("Query NFTs");
627
758
  cmd.command("get").description("Get a single NFT").argument("<chain>", "Chain (e.g. ethereum, base)").argument("<contract>", "Contract address").argument("<token-id>", "Token ID").action(async (chain, contract, tokenId) => {
628
759
  const client = getClient2();
629
760
  const result = await client.get(
@@ -691,9 +822,9 @@ function nftsCommand(getClient2, getFormat2) {
691
822
  }
692
823
 
693
824
  // src/commands/offers.ts
694
- import { Command as Command6 } from "commander";
825
+ import { Command as Command7 } from "commander";
695
826
  function offersCommand(getClient2, getFormat2) {
696
- const cmd = new Command6("offers").description("Query NFT offers");
827
+ const cmd = new Command7("offers").description("Query NFT offers");
697
828
  cmd.command("all").description("Get all offers for a collection").argument("<collection>", "Collection slug").option("--limit <limit>", "Number of results", "20").option("--next <cursor>", "Pagination cursor").action(
698
829
  async (collection, options) => {
699
830
  const client = getClient2();
@@ -737,155 +868,38 @@ function offersCommand(getClient2, getFormat2) {
737
868
  }
738
869
 
739
870
  // src/commands/search.ts
740
- import { Command as Command7 } from "commander";
741
-
742
- // src/queries.ts
743
- var SEARCH_COLLECTIONS_QUERY = `
744
- query SearchCollections($query: String!, $limit: Int, $chains: [ChainIdentifier!]) {
745
- collectionsByQuery(query: $query, limit: $limit, chains: $chains) {
746
- slug
747
- name
748
- description
749
- imageUrl
750
- chain {
751
- identifier
752
- name
753
- }
754
- stats {
755
- totalSupply
756
- ownerCount
757
- volume {
758
- usd
759
- }
760
- sales
761
- }
762
- floorPrice {
763
- pricePerItem {
764
- usd
765
- native {
766
- unit
767
- symbol
768
- }
769
- }
770
- }
771
- }
772
- }`;
773
- var SEARCH_NFTS_QUERY = `
774
- query SearchItems($query: String!, $collectionSlug: String, $limit: Int, $chains: [ChainIdentifier!]) {
775
- itemsByQuery(query: $query, collectionSlug: $collectionSlug, limit: $limit, chains: $chains) {
776
- tokenId
777
- name
778
- description
779
- imageUrl
780
- contractAddress
781
- collection {
782
- slug
783
- name
784
- }
785
- chain {
786
- identifier
787
- name
788
- }
789
- bestListing {
790
- pricePerItem {
791
- usd
792
- native {
793
- unit
794
- symbol
795
- }
796
- }
797
- }
798
- owner {
799
- address
800
- displayName
801
- }
802
- }
803
- }`;
804
- var SEARCH_TOKENS_QUERY = `
805
- query SearchCurrencies($query: String!, $limit: Int, $chain: ChainIdentifier) {
806
- currenciesByQuery(query: $query, limit: $limit, chain: $chain, allowlistOnly: false) {
807
- name
808
- symbol
809
- imageUrl
810
- usdPrice
811
- contractAddress
812
- chain {
813
- identifier
814
- name
815
- }
816
- stats {
817
- marketCapUsd
818
- oneDay {
819
- priceChange
820
- volume
821
- }
822
- }
823
- }
824
- }`;
825
- var SEARCH_ACCOUNTS_QUERY = `
826
- query SearchAccounts($query: String!, $limit: Int) {
827
- accountsByQuery(query: $query, limit: $limit) {
828
- address
829
- username
830
- imageUrl
831
- isVerified
832
- }
833
- }`;
834
-
835
- // src/commands/search.ts
871
+ import { Command as Command8 } from "commander";
836
872
  function searchCommand(getClient2, getFormat2) {
837
- const cmd = new Command7("search").description(
838
- "Search for collections, NFTs, tokens, and accounts"
839
- );
840
- cmd.command("collections").description("Search collections by name or slug").argument("<query>", "Search query").option("--chains <chains>", "Filter by chains (comma-separated)").option("--limit <limit>", "Number of results", "10").action(
841
- async (query, options) => {
842
- const client = getClient2();
843
- const result = await client.graphql(SEARCH_COLLECTIONS_QUERY, {
844
- query,
845
- limit: parseIntOption(options.limit, "--limit"),
846
- chains: options.chains?.split(",")
847
- });
848
- console.log(formatOutput(result.collectionsByQuery, getFormat2()));
849
- }
850
- );
851
- cmd.command("nfts").description("Search NFTs by name").argument("<query>", "Search query").option("--collection <slug>", "Filter by collection slug").option("--chains <chains>", "Filter by chains (comma-separated)").option("--limit <limit>", "Number of results", "10").action(
852
- async (query, options) => {
853
- const client = getClient2();
854
- const result = await client.graphql(SEARCH_NFTS_QUERY, {
855
- query,
856
- collectionSlug: options.collection,
857
- limit: parseIntOption(options.limit, "--limit"),
858
- chains: options.chains?.split(",")
859
- });
860
- console.log(formatOutput(result.itemsByQuery, getFormat2()));
861
- }
862
- );
863
- cmd.command("tokens").description("Search tokens/currencies by name or symbol").argument("<query>", "Search query").option("--chain <chain>", "Filter by chain").option("--limit <limit>", "Number of results", "10").action(
873
+ const cmd = new Command8("search").description("Search across collections, tokens, NFTs, and accounts").argument("<query>", "Search query").option(
874
+ "--types <types>",
875
+ "Filter by type (comma-separated: collection,nft,token,account)"
876
+ ).option("--chains <chains>", "Filter by chains (comma-separated)").option("--limit <limit>", "Number of results", "20").action(
864
877
  async (query, options) => {
865
878
  const client = getClient2();
866
- const result = await client.graphql(SEARCH_TOKENS_QUERY, {
879
+ const params = {
867
880
  query,
868
- limit: parseIntOption(options.limit, "--limit"),
869
- chain: options.chain
870
- });
871
- console.log(formatOutput(result.currenciesByQuery, getFormat2()));
881
+ limit: parseIntOption(options.limit, "--limit")
882
+ };
883
+ if (options.types) {
884
+ params.asset_types = options.types;
885
+ }
886
+ if (options.chains) {
887
+ params.chains = options.chains;
888
+ }
889
+ const result = await client.get(
890
+ "/api/v2/search",
891
+ params
892
+ );
893
+ console.log(formatOutput(result, getFormat2()));
872
894
  }
873
895
  );
874
- cmd.command("accounts").description("Search accounts by username or address").argument("<query>", "Search query").option("--limit <limit>", "Number of results", "10").action(async (query, options) => {
875
- const client = getClient2();
876
- const result = await client.graphql(SEARCH_ACCOUNTS_QUERY, {
877
- query,
878
- limit: parseIntOption(options.limit, "--limit")
879
- });
880
- console.log(formatOutput(result.accountsByQuery, getFormat2()));
881
- });
882
896
  return cmd;
883
897
  }
884
898
 
885
899
  // src/commands/swaps.ts
886
- import { Command as Command8 } from "commander";
900
+ import { Command as Command9 } from "commander";
887
901
  function swapsCommand(getClient2, getFormat2) {
888
- const cmd = new Command8("swaps").description(
902
+ const cmd = new Command9("swaps").description(
889
903
  "Get swap quotes for token trading"
890
904
  );
891
905
  cmd.command("quote").description(
@@ -925,9 +939,9 @@ function swapsCommand(getClient2, getFormat2) {
925
939
  }
926
940
 
927
941
  // src/commands/tokens.ts
928
- import { Command as Command9 } from "commander";
942
+ import { Command as Command10 } from "commander";
929
943
  function tokensCommand(getClient2, getFormat2) {
930
- const cmd = new Command9("tokens").description(
944
+ const cmd = new Command10("tokens").description(
931
945
  "Query trending tokens, top tokens, and token details"
932
946
  );
933
947
  cmd.command("trending").description("Get trending tokens based on OpenSea's trending score").option("--chains <chains>", "Comma-separated list of chains to filter by").option("--limit <limit>", "Number of results (max 100)", "20").option("--next <cursor>", "Pagination cursor").action(
@@ -971,6 +985,9 @@ function tokensCommand(getClient2, getFormat2) {
971
985
  }
972
986
 
973
987
  // src/cli.ts
988
+ var EXIT_API_ERROR = 1;
989
+ var EXIT_AUTH_ERROR = 2;
990
+ var EXIT_RATE_LIMITED = 3;
974
991
  var BANNER = `
975
992
  ____ _____
976
993
  / __ \\ / ____|
@@ -981,8 +998,11 @@ var BANNER = `
981
998
  | |
982
999
  |_|
983
1000
  `;
984
- var program = new Command10();
985
- program.name("opensea").description("OpenSea CLI - Query the OpenSea API from the command line").version(process.env.npm_package_version ?? "0.0.0").addHelpText("before", BANNER).option("--api-key <key>", "OpenSea API key (or set OPENSEA_API_KEY env var)").option("--chain <chain>", "Default chain", "ethereum").option("--format <format>", "Output format (json, table, or toon)", "json").option("--base-url <url>", "API base URL").option("--timeout <ms>", "Request timeout in milliseconds", "30000").option("--verbose", "Log request and response info to stderr");
1001
+ var program = new Command11();
1002
+ program.name("opensea").description("OpenSea CLI - Query the OpenSea API from the command line").version(process.env.npm_package_version ?? "0.0.0").addHelpText("before", BANNER).option("--api-key <key>", "OpenSea API key (or set OPENSEA_API_KEY env var)").option("--chain <chain>", "Default chain", "ethereum").option("--format <format>", "Output format (json, table, or toon)", "json").option("--base-url <url>", "API base URL").option("--timeout <ms>", "Request timeout in milliseconds", "30000").option("--verbose", "Log request and response info to stderr").option(
1003
+ "--fields <fields>",
1004
+ "Comma-separated list of fields to include in output"
1005
+ ).option("--max-lines <lines>", "Truncate output after N lines").option("--max-retries <n>", "Max retries on 429/5xx (0 to disable)", "3").option("--no-retry", "Disable request retries");
986
1006
  function getClient() {
987
1007
  const opts = program.opts();
988
1008
  const apiKey = opts.apiKey ?? process.env.OPENSEA_API_KEY;
@@ -990,14 +1010,16 @@ function getClient() {
990
1010
  console.error(
991
1011
  "Error: API key required. Use --api-key or set OPENSEA_API_KEY environment variable."
992
1012
  );
993
- process.exit(2);
1013
+ process.exit(EXIT_AUTH_ERROR);
994
1014
  }
1015
+ const maxRetries = opts.retry ? parseIntOption(opts.maxRetries, "--max-retries") : 0;
995
1016
  return new OpenSeaClient({
996
1017
  apiKey,
997
1018
  chain: opts.chain,
998
1019
  baseUrl: opts.baseUrl,
999
1020
  timeout: parseIntOption(opts.timeout, "--timeout"),
1000
- verbose: opts.verbose
1021
+ verbose: opts.verbose,
1022
+ maxRetries
1001
1023
  });
1002
1024
  }
1003
1025
  function getFormat() {
@@ -1006,6 +1028,21 @@ function getFormat() {
1006
1028
  if (opts.format === "toon") return "toon";
1007
1029
  return "json";
1008
1030
  }
1031
+ program.hook("preAction", () => {
1032
+ const opts = program.opts();
1033
+ let maxLines;
1034
+ if (opts.maxLines) {
1035
+ maxLines = parseIntOption(opts.maxLines, "--max-lines");
1036
+ if (maxLines < 1) {
1037
+ console.error("Error: --max-lines must be >= 1");
1038
+ process.exit(2);
1039
+ }
1040
+ }
1041
+ setOutputOptions({
1042
+ fields: opts.fields?.split(",").map((f) => f.trim()),
1043
+ maxLines
1044
+ });
1045
+ });
1009
1046
  program.addCommand(collectionsCommand(getClient, getFormat));
1010
1047
  program.addCommand(nftsCommand(getClient, getFormat));
1011
1048
  program.addCommand(listingsCommand(getClient, getFormat));
@@ -1015,15 +1052,17 @@ program.addCommand(accountsCommand(getClient, getFormat));
1015
1052
  program.addCommand(tokensCommand(getClient, getFormat));
1016
1053
  program.addCommand(searchCommand(getClient, getFormat));
1017
1054
  program.addCommand(swapsCommand(getClient, getFormat));
1055
+ program.addCommand(healthCommand(getClient, getFormat));
1018
1056
  async function main() {
1019
1057
  try {
1020
1058
  await program.parseAsync(process.argv);
1021
1059
  } catch (error) {
1022
1060
  if (error instanceof OpenSeaAPIError) {
1061
+ const isRateLimited = error.statusCode === 429;
1023
1062
  console.error(
1024
1063
  JSON.stringify(
1025
1064
  {
1026
- error: "API Error",
1065
+ error: isRateLimited ? "Rate Limited" : "API Error",
1027
1066
  status: error.statusCode,
1028
1067
  path: error.path,
1029
1068
  message: error.responseBody
@@ -1032,7 +1071,7 @@ async function main() {
1032
1071
  2
1033
1072
  )
1034
1073
  );
1035
- process.exit(1);
1074
+ process.exit(isRateLimited ? EXIT_RATE_LIMITED : EXIT_API_ERROR);
1036
1075
  }
1037
1076
  const label = error instanceof TypeError ? "Network Error" : error.name;
1038
1077
  console.error(
@@ -1045,7 +1084,7 @@ async function main() {
1045
1084
  2
1046
1085
  )
1047
1086
  );
1048
- process.exit(1);
1087
+ process.exit(EXIT_API_ERROR);
1049
1088
  }
1050
1089
  }
1051
1090
  main();