@zoralabs/cli 0.2.4 → 0.3.1

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 (3) hide show
  1. package/README.md +76 -0
  2. package/dist/index.js +1709 -643
  3. package/package.json +4 -3
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.tsx
4
- import { Command as Command11 } from "commander";
4
+ import { Command as Command12 } from "commander";
5
5
  import { ExitPromptError } from "@inquirer/core";
6
6
  import "fs";
7
7
  import { setApiBaseUrl } from "@zoralabs/coins-sdk";
@@ -18,6 +18,56 @@ import {
18
18
  chmodSync
19
19
  } from "fs";
20
20
  import { join } from "path";
21
+
22
+ // src/lib/errors.ts
23
+ import { BaseError as ViemBaseError, InsufficientFundsError } from "viem";
24
+ function formatError(err) {
25
+ if (!(err instanceof Error)) return String(err);
26
+ const msg = err.message;
27
+ return msg.length > 120 ? msg.slice(0, 120) + "..." : msg;
28
+ }
29
+ function tradeErrorMessage(err) {
30
+ if (!(err instanceof Error)) return String(err);
31
+ if (err instanceof ViemBaseError) {
32
+ const insufficient = err.walk((e) => e instanceof InsufficientFundsError);
33
+ if (insufficient)
34
+ return "Not enough funds. Try a lower amount or run 'zora balance spendable' to check your balance.";
35
+ return err.shortMessage;
36
+ }
37
+ return apiErrorMessage(err);
38
+ }
39
+ function apiErrorMessage(err) {
40
+ if (!(err instanceof Error)) return String(err);
41
+ const code = err.code;
42
+ if (code === "ECONNREFUSED" || code === "ENOTFOUND")
43
+ return "Can't connect. Check your internet connection.";
44
+ if (code === "ETIMEDOUT" || code === "UND_ERR_CONNECT_TIMEOUT")
45
+ return "Request timed out. Try again.";
46
+ const status = err.status;
47
+ if (status === 429)
48
+ return "Rate limited. Wait a moment or run 'zora auth configure' for higher limits.";
49
+ if (status === 401 || status === 403)
50
+ return "Auth failed. Run 'zora auth configure' to update your API key.";
51
+ if (typeof status === "number" && status >= 500)
52
+ return "Zora is temporarily unavailable. Try again later.";
53
+ return formatError(err);
54
+ }
55
+ function bannedCoinMessage(address) {
56
+ return `The coin at ${address} is unavailable because it violates the Zora terms of service.`;
57
+ }
58
+ function bannedCoinBuyMessage(address) {
59
+ return `Unable to buy ${address} because it violates the Zora terms of service. Already own this coin? Run zora sell ${address} --all to exit your position.`;
60
+ }
61
+ function fsErrorMessage(err, path) {
62
+ if (!(err instanceof Error)) return String(err);
63
+ const code = err.code;
64
+ if (code === "EACCES") return `Permission denied accessing ${path}.`;
65
+ if (code === "EISDIR")
66
+ return `Expected a file but found a directory at ${path}.`;
67
+ return formatError(err);
68
+ }
69
+
70
+ // src/lib/config.ts
21
71
  import { homedir, platform } from "os";
22
72
  function getConfigDir() {
23
73
  if (platform() === "win32") {
@@ -55,7 +105,7 @@ function readConfig() {
55
105
  parsed = JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
56
106
  } catch (err) {
57
107
  console.error(
58
- `Warning: could not parse ${CONFIG_FILE}: ${err.message}. Run 'zora auth configure' to fix.`
108
+ `Warning: could not parse ${CONFIG_FILE}: ${formatError(err)}. Run 'zora auth configure' to fix.`
59
109
  );
60
110
  configReadOnly = true;
61
111
  return { version: CONFIG_VERSION };
@@ -64,7 +114,7 @@ function readConfig() {
64
114
  assertVersion(parsed, CONFIG_VERSION, CONFIG_FILE);
65
115
  } catch (err) {
66
116
  console.error(
67
- `Warning: ${err.message}. Delete ${CONFIG_FILE} or run 'zora auth configure' to reset.`
117
+ `Warning: ${formatError(err)}. Delete ${CONFIG_FILE} or run 'zora auth configure' to reset.`
68
118
  );
69
119
  configReadOnly = true;
70
120
  return { version: CONFIG_VERSION };
@@ -155,18 +205,29 @@ function maskKey(key) {
155
205
  }
156
206
 
157
207
  // src/lib/output.ts
158
- var VALID_OUTPUT_MODES = ["table", "json", "live"];
159
208
  var getOutputMode = (cmd, defaultMode) => {
160
- const raw = cmd.optsWithGlobals().output;
161
- if (!raw) return defaultMode;
162
- if (VALID_OUTPUT_MODES.includes(raw)) return raw;
163
- return outputErrorAndExit(
164
- false,
165
- `Invalid --output value: ${raw}.`,
166
- `Supported: ${VALID_OUTPUT_MODES.join(", ")}`
167
- );
209
+ const globals = cmd.optsWithGlobals();
210
+ const json = globals.json ?? false;
211
+ const live = globals.live ?? false;
212
+ const static_ = globals.static ?? false;
213
+ const set = [
214
+ json && "--json",
215
+ live && "--live",
216
+ static_ && "--static"
217
+ ].filter(Boolean);
218
+ if (set.length > 1) {
219
+ return outputErrorAndExit(
220
+ false,
221
+ `${set.join(", ")} cannot be used together.`,
222
+ "Choose one: --json, --live, or --static"
223
+ );
224
+ }
225
+ if (json) return "json";
226
+ if (live) return "live";
227
+ if (static_) return "static";
228
+ return defaultMode;
168
229
  };
169
- var getJson = (cmd) => getOutputMode(cmd, "table") === "json";
230
+ var getJson = (cmd) => cmd.optsWithGlobals().json ?? false;
170
231
  var getYes = (cmd) => cmd.optsWithGlobals().yes ?? false;
171
232
  var outputJson = (data) => {
172
233
  console.log(JSON.stringify(data, null, 2));
@@ -188,14 +249,18 @@ var outputData = (json, opts) => {
188
249
  if (json) {
189
250
  outputJson(opts.json);
190
251
  } else {
191
- opts.table();
252
+ opts.render();
192
253
  }
193
254
  };
194
- var getLiveConfig = (cmd, defaultMode) => {
195
- const mode = getOutputMode(cmd, defaultMode);
255
+ var getLiveConfig = (cmd, mode) => {
196
256
  const live = mode === "live";
197
- const intervalRaw = parseInt(cmd.optsWithGlobals().interval, 10);
257
+ const intervalRaw = parseInt(cmd.opts().refresh, 10);
198
258
  const intervalSeconds = isNaN(intervalRaw) || intervalRaw < 5 ? 30 : intervalRaw;
259
+ if (!live && cmd.getOptionValueSource("refresh") === "cli") {
260
+ console.warn(
261
+ "\x1B[33mWarning:\x1B[0m --refresh has no effect without --live"
262
+ );
263
+ }
199
264
  return { live, intervalSeconds };
200
265
  };
201
266
 
@@ -282,9 +347,7 @@ var resolveAccount = (json = false) => {
282
347
  try {
283
348
  return privateKeyToAccount(normalizeKey(key));
284
349
  } catch (err) {
285
- console.error(
286
- `\u2717 Invalid private key: ${err instanceof Error ? err.message : String(err)}`
287
- );
350
+ console.error(`\u2717 Invalid private key: ${formatError(err)}`);
288
351
  console.error(" Run 'zora setup --force' to replace it.");
289
352
  return process.exit(1);
290
353
  }
@@ -374,7 +437,7 @@ var getClient = () => {
374
437
  return client;
375
438
  };
376
439
  var commonProperties = () => ({
377
- cli_version: true ? "0.2.4" : "development",
440
+ cli_version: true ? "0.3.1" : "development",
378
441
  os: process.platform,
379
442
  arch: process.arch,
380
443
  node_version: process.version
@@ -447,7 +510,7 @@ authCommand.command("configure").description("Set your Zora API key").option("--
447
510
  status: "env_override",
448
511
  message: "API key is set via ZORA_API_KEY environment variable."
449
512
  },
450
- table: () => console.log(
513
+ render: () => console.log(
451
514
  "API key is set via ZORA_API_KEY environment variable. Unset it to configure manually."
452
515
  )
453
516
  });
@@ -475,7 +538,7 @@ authCommand.command("configure").description("Set your Zora API key").option("--
475
538
  saveApiKey(trimmed);
476
539
  outputData(json, {
477
540
  json: { saved: true, path: getConfigPath() },
478
- table: () => console.log(`API key saved to ${getConfigPath()}`)
541
+ render: () => console.log(`API key saved to ${getConfigPath()}`)
479
542
  });
480
543
  track("cli_auth_configure", {
481
544
  output_format: json ? "json" : "text"
@@ -483,7 +546,7 @@ authCommand.command("configure").description("Set your Zora API key").option("--
483
546
  } catch (err) {
484
547
  outputErrorAndExit(
485
548
  json,
486
- `Failed to save API key: ${err.message}`
549
+ `Failed to save API key: ${fsErrorMessage(err, getConfigPath())}`
487
550
  );
488
551
  }
489
552
  });
@@ -493,7 +556,7 @@ authCommand.command("status").description("Check authentication status").action(
493
556
  if (!apiKey) {
494
557
  outputData(json, {
495
558
  json: { authenticated: false },
496
- table: () => {
559
+ render: () => {
497
560
  console.log(
498
561
  "No API key configured. The CLI works without one, but requests are rate-limited."
499
562
  );
@@ -512,7 +575,7 @@ authCommand.command("status").description("Check authentication status").action(
512
575
  const source = getEnvApiKey() ? "env (ZORA_API_KEY)" : getConfigPath();
513
576
  outputData(json, {
514
577
  json: { authenticated: true, key: maskKey(apiKey), source },
515
- table: () => {
578
+ render: () => {
516
579
  console.log(`Authenticated: ${maskKey(apiKey)}`);
517
580
  console.log(`Source: ${source}`);
518
581
  }
@@ -577,7 +640,8 @@ var Table = ({
577
640
  title,
578
641
  subtitle,
579
642
  fullWidth = true,
580
- footer
643
+ footer,
644
+ selectedRow
581
645
  }) => {
582
646
  const widths = computeColumnWidths(columns, fullWidth);
583
647
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingTop: 1, paddingBottom: 1, children: [
@@ -589,26 +653,31 @@ var Table = ({
589
653
  ] })
590
654
  ] }),
591
655
  /* @__PURE__ */ jsx(Box, { paddingLeft: PADDING_LEFT, children: columns.map((col, i) => /* @__PURE__ */ jsx(Box, { width: widths[i], children: /* @__PURE__ */ jsx(Text, { bold: true, dimColor: true, wrap: "truncate", children: col.header }) }, col.header)) }),
592
- data.map((row, i) => /* @__PURE__ */ jsx(Box, { paddingLeft: PADDING_LEFT, children: columns.map((col, colIdx) => {
593
- const colWidth = widths[colIdx];
594
- const value = col.noTruncate ? col.accessor(row) : truncate(col.accessor(row), colWidth - 2);
595
- const colorName = col.color?.(row);
596
- return /* @__PURE__ */ jsx(Box, { width: colWidth, children: /* @__PURE__ */ jsx(
597
- Text,
598
- {
599
- color: colorName,
600
- wrap: col.noTruncate ? "wrap" : "truncate",
601
- children: value
602
- }
603
- ) }, col.header);
604
- }) }, i)),
656
+ data.map((row, i) => {
657
+ const isSelected = selectedRow === i;
658
+ return /* @__PURE__ */ jsx(Box, { paddingLeft: PADDING_LEFT, children: columns.map((col, colIdx) => {
659
+ const colWidth = widths[colIdx];
660
+ const value = col.noTruncate ? col.accessor(row) : truncate(col.accessor(row), colWidth - 2);
661
+ const colorName = col.color?.(row);
662
+ return /* @__PURE__ */ jsx(Box, { width: colWidth, children: /* @__PURE__ */ jsx(
663
+ Text,
664
+ {
665
+ color: colorName,
666
+ bold: isSelected,
667
+ inverse: isSelected,
668
+ wrap: col.noTruncate ? "wrap" : "truncate",
669
+ children: value
670
+ }
671
+ ) }, col.header);
672
+ }) }, i);
673
+ }),
605
674
  footer && /* @__PURE__ */ jsx(Box, { paddingLeft: PADDING_LEFT, marginTop: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, wrap: "wrap", children: footer }) })
606
675
  ] });
607
676
  };
608
677
 
609
678
  // src/lib/format.ts
610
679
  import { format, formatDistanceStrict } from "date-fns";
611
- import { formatEther } from "viem";
680
+ import { formatUnits } from "viem";
612
681
  function formatCompactUsd(value) {
613
682
  if (!value || Number(value) === 0) return "$0";
614
683
  return new Intl.NumberFormat("en-US", {
@@ -657,16 +726,28 @@ function formatCreatedAt(isoDate, now) {
657
726
  if (isNaN(date.getTime())) return "-";
658
727
  return `${formatRelativeTime(date, now)} (${formatAbsoluteTime(date)})`;
659
728
  }
660
- var formatEthDisplay = (wei) => {
661
- const eth = formatEther(wei);
662
- const parts = eth.split(".");
663
- if (!parts[1]) return eth;
664
- const trimmed = parts[1].replace(/0+$/, "") || "0";
665
- return `${parts[0]}.${trimmed}`;
729
+ var formatAmountDisplay = (amount, decimals) => {
730
+ const formatted = formatUnits(amount, decimals);
731
+ const parts = formatted.split(".");
732
+ if (!parts[1]) {
733
+ return new Intl.NumberFormat("en-US", {
734
+ maximumFractionDigits: 2
735
+ }).format(Number(formatted));
736
+ }
737
+ const twoDecimal = `${parts[0]}.${parts[1].slice(0, 2)}`;
738
+ if (Number(twoDecimal) === 0 && amount > 0n) {
739
+ const sigIndex = parts[1].search(/[1-9]/);
740
+ const maxDecimals = sigIndex === -1 ? 6 : Math.min(sigIndex + 4, parts[1].length);
741
+ const truncated = `${parts[0]}.${parts[1].slice(0, maxDecimals)}`;
742
+ return new Intl.NumberFormat("en-US", {
743
+ maximumFractionDigits: maxDecimals
744
+ }).format(Number(truncated));
745
+ }
746
+ return new Intl.NumberFormat("en-US", {
747
+ maximumFractionDigits: 2
748
+ }).format(Number(formatted));
666
749
  };
667
- var formatCoinsDisplay = (coinsOut) => new Intl.NumberFormat("en-US", {
668
- maximumFractionDigits: 2
669
- }).format(Number(coinsOut));
750
+ var truncateAddress = (address) => `${address.slice(0, 6)}\u2026${address.slice(-4)}`;
670
751
 
671
752
  // src/lib/balance-format.ts
672
753
  var COIN_DECIMALS = 18;
@@ -800,6 +881,7 @@ var BalanceView = ({
800
881
  fetchData,
801
882
  sort,
802
883
  mode = "full",
884
+ initialCursor,
803
885
  autoRefresh = false,
804
886
  intervalSeconds = 30
805
887
  }) => {
@@ -808,37 +890,64 @@ var BalanceView = ({
808
890
  const [isRefreshing, setIsRefreshing] = useState2(false);
809
891
  const [error, setError] = useState2(null);
810
892
  const [data, setData] = useState2(null);
893
+ const paginated = mode === "coins";
894
+ const [page, setPage] = useState2(1);
895
+ const [cursorHistory, setCursorHistory] = useState2(
896
+ []
897
+ );
898
+ const [currentCursor, setCurrentCursor] = useState2(
899
+ initialCursor
900
+ );
811
901
  const { refreshCount, secondsUntilRefresh, triggerManualRefresh } = useAutoRefresh(intervalSeconds, autoRefresh);
812
902
  const [manualRefreshCount, setManualRefreshCount] = useState2(0);
813
903
  const hasLoadedOnce = useRef(false);
814
- const load = useCallback2(async () => {
815
- if (hasLoadedOnce.current) {
816
- setIsRefreshing(true);
817
- } else {
818
- setLoading(true);
819
- }
820
- setError(null);
821
- try {
822
- const result = await fetchData();
823
- setData(result);
824
- hasLoadedOnce.current = true;
825
- } catch (err) {
826
- setError(err instanceof Error ? err.message : String(err));
827
- }
828
- setLoading(false);
829
- setIsRefreshing(false);
830
- }, [fetchData]);
904
+ const load = useCallback2(
905
+ async (cursor) => {
906
+ if (hasLoadedOnce.current) {
907
+ setIsRefreshing(true);
908
+ } else {
909
+ setLoading(true);
910
+ }
911
+ setError(null);
912
+ try {
913
+ const result = await fetchData(cursor);
914
+ setData(result);
915
+ hasLoadedOnce.current = true;
916
+ } catch (err) {
917
+ setError(err instanceof Error ? err.message : String(err));
918
+ }
919
+ setLoading(false);
920
+ setIsRefreshing(false);
921
+ },
922
+ [fetchData]
923
+ );
831
924
  useEffect2(() => {
832
- load();
833
- }, [refreshCount, manualRefreshCount]);
925
+ load(currentCursor);
926
+ }, [load, refreshCount, manualRefreshCount, currentCursor]);
834
927
  useInput((input, key) => {
835
928
  if (input === "q" || key.escape) {
836
929
  exit();
837
930
  return;
838
931
  }
839
- if (input === "r" && !loading) {
932
+ if (loading) return;
933
+ if (input === "r") {
840
934
  triggerManualRefresh();
841
935
  setManualRefreshCount((c) => c + 1);
936
+ return;
937
+ }
938
+ if (!paginated) return;
939
+ const canGoNext = data?.pageInfo?.hasNextPage && data.pageInfo.endCursor;
940
+ const canGoPrev = cursorHistory.length > 0;
941
+ if ((input === "n" || key.rightArrow) && canGoNext) {
942
+ setCursorHistory((prev) => [...prev, currentCursor]);
943
+ setCurrentCursor(data.pageInfo.endCursor);
944
+ setPage((p) => p + 1);
945
+ }
946
+ if ((input === "p" || key.leftArrow) && canGoPrev) {
947
+ const prev = cursorHistory[cursorHistory.length - 1];
948
+ setCursorHistory((h) => h.slice(0, -1));
949
+ setCurrentCursor(prev);
950
+ setPage((p) => p - 1);
842
951
  }
843
952
  });
844
953
  if (error && !data) {
@@ -866,8 +975,10 @@ var BalanceView = ({
866
975
  ] }) });
867
976
  }
868
977
  if (!data) return null;
869
- const hints = ["r refresh"];
870
- if (autoRefresh) hints.push(`auto: ${secondsUntilRefresh}s`);
978
+ const hints = [];
979
+ if (paginated && cursorHistory.length > 0) hints.push("\u2190 prev");
980
+ if (paginated && data?.pageInfo?.hasNextPage) hints.push("\u2192 next");
981
+ hints.push(autoRefresh ? `r refresh (${secondsUntilRefresh}s)` : "r refresh");
871
982
  hints.push("q quit");
872
983
  const footer = hints.join(" \xB7 ");
873
984
  const showWallet = mode === "full" || mode === "wallet";
@@ -910,7 +1021,7 @@ var BalanceView = ({
910
1021
  columns: balanceColumns,
911
1022
  data: data.rankedBalances,
912
1023
  title: `Coins \xB7 sorted by ${SORT_LABELS[sort]}`,
913
- subtitle: `${data.rankedBalances.length} of ${data.total}`
1024
+ subtitle: paginated ? `Page ${page} \xB7 ${data.rankedBalances.length} result${data.rankedBalances.length !== 1 ? "s" : ""}` : `${data.rankedBalances.length} of ${data.total}`
914
1025
  }
915
1026
  ) : null,
916
1027
  /* @__PURE__ */ jsx2(Box2, { paddingLeft: 1, paddingBottom: 1, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: footer }) })
@@ -922,7 +1033,7 @@ import { getTokenInfo } from "@zoralabs/coins-sdk";
922
1033
  import {
923
1034
  createPublicClient as createPublicClient2,
924
1035
  erc20Abi,
925
- formatUnits,
1036
+ formatUnits as formatUnits2,
926
1037
  http
927
1038
  } from "viem";
928
1039
  import { base as base2 } from "viem/chains";
@@ -957,7 +1068,7 @@ var fetchTokenPriceUsd = async (address, chainId = BASE_CHAIN_ID) => {
957
1068
  return res.data?.erc20Token?.currency?.priceUsd ? Number(res.data.erc20Token.currency.priceUsd) : null;
958
1069
  } catch (err) {
959
1070
  console.warn(
960
- `Warning: failed to fetch price for ${address}: ${err instanceof Error ? err.message : String(err)}`
1071
+ `Warning: failed to fetch price for ${address}: ${formatError(err)}`
961
1072
  );
962
1073
  return null;
963
1074
  }
@@ -1007,7 +1118,7 @@ var fetchWalletBalances = async (walletAddress) => {
1007
1118
  });
1008
1119
  const visible = resolved.filter((r) => r.balance > 0n || r.token.isNative);
1009
1120
  const intermediate = visible.map(({ token, balance, priceUsd }) => {
1010
- const human = formatUnits(balance, token.decimals);
1121
+ const human = formatUnits2(balance, token.decimals);
1011
1122
  const usdValue = priceUsd !== null ? Number(human) * priceUsd : null;
1012
1123
  return { token, human, priceUsd, usdValue };
1013
1124
  });
@@ -1094,7 +1205,7 @@ function resolveContext(json) {
1094
1205
  function renderWallet(json, walletResult) {
1095
1206
  outputData(json, {
1096
1207
  json: { wallet: walletResult.walletBalancesJson },
1097
- table: () => {
1208
+ render: () => {
1098
1209
  renderOnce(
1099
1210
  /* @__PURE__ */ jsx3(
1100
1211
  Table,
@@ -1108,7 +1219,7 @@ function renderWallet(json, walletResult) {
1108
1219
  }
1109
1220
  });
1110
1221
  }
1111
- function renderCoins(json, balances, total, sort) {
1222
+ function renderCoins(json, balances, total, sort, limit, pageInfo) {
1112
1223
  const rankedBalances = balances.map((balance, index) => ({
1113
1224
  ...balance,
1114
1225
  rank: index + 1
@@ -1117,14 +1228,16 @@ function renderCoins(json, balances, total, sort) {
1117
1228
  json: {
1118
1229
  coins: rankedBalances.map(
1119
1230
  (balance) => formatBalanceJson(balance, balance.rank)
1120
- )
1231
+ ),
1232
+ pageInfo: pageInfo ?? null
1121
1233
  },
1122
- table: () => {
1234
+ render: () => {
1123
1235
  if (balances.length === 0) {
1124
1236
  console.log("\n No coin balances found.\n");
1125
1237
  console.log(" Buy coins to see them here:");
1126
1238
  console.log(" zora buy <address> --eth 0.001\n");
1127
1239
  } else {
1240
+ const footer = pageInfo?.hasNextPage && pageInfo.endCursor ? `Next page: zora balance coins --sort ${sort} --limit ${limit} --after ${pageInfo.endCursor}` : void 0;
1128
1241
  renderOnce(
1129
1242
  /* @__PURE__ */ jsx3(
1130
1243
  Table,
@@ -1132,7 +1245,8 @@ function renderCoins(json, balances, total, sort) {
1132
1245
  columns: balanceColumns,
1133
1246
  data: rankedBalances,
1134
1247
  title: `Coins \xB7 sorted by ${SORT_LABELS[sort]}`,
1135
- subtitle: `${balances.length} of ${total}`
1248
+ subtitle: `${balances.length} of ${total}`,
1249
+ footer
1136
1250
  }
1137
1251
  )
1138
1252
  );
@@ -1140,19 +1254,17 @@ function renderCoins(json, balances, total, sort) {
1140
1254
  }
1141
1255
  });
1142
1256
  }
1143
- async function fetchCoins(json, address, sort, limit) {
1257
+ async function fetchCoins(json, address, sort, limit, after) {
1144
1258
  let response;
1145
1259
  try {
1146
1260
  response = await getProfileBalances({
1147
1261
  identifier: address,
1148
1262
  count: limit,
1149
- sortOption: SORT_MAP[sort]
1263
+ sortOption: SORT_MAP[sort],
1264
+ after
1150
1265
  });
1151
1266
  } catch (err) {
1152
- outputErrorAndExit(
1153
- json,
1154
- `Request failed: ${err instanceof Error ? err.message : String(err)}`
1155
- );
1267
+ outputErrorAndExit(json, `Request failed: ${apiErrorMessage(err)}`);
1156
1268
  }
1157
1269
  if (response.error) {
1158
1270
  outputErrorAndExit(
@@ -1165,7 +1277,8 @@ async function fetchCoins(json, address, sort, limit) {
1165
1277
  (e) => e.node
1166
1278
  );
1167
1279
  const total = response.data?.profile?.coinBalances?.count ?? balances.length;
1168
- return { balances, total };
1280
+ const pageInfo = response.data?.profile?.coinBalances?.pageInfo;
1281
+ return { balances, total, pageInfo };
1169
1282
  }
1170
1283
  function validateCoinOpts(json, sort, limitStr) {
1171
1284
  if (!SORT_MAP[sort]) {
@@ -1184,11 +1297,15 @@ function validateCoinOpts(json, sort, limitStr) {
1184
1297
  }
1185
1298
  return { sort, limit };
1186
1299
  }
1187
- var balanceCommand = new Command2("balance").description("Show balances in your wallet").action(async function() {
1300
+ var balanceCommand = new Command2("balance").description("Show balances in your wallet").option("--live", "Interactive live-updating display (default)").option("--static", "Static snapshot").option(
1301
+ "--refresh <seconds>",
1302
+ "Auto-refresh interval in seconds, requires --live (min 5)",
1303
+ "30"
1304
+ ).action(async function() {
1188
1305
  const output = getOutputMode(this, "live");
1189
1306
  const json = output === "json";
1190
1307
  const account = resolveContext(json);
1191
- const { live, intervalSeconds } = getLiveConfig(this, "live");
1308
+ const { live, intervalSeconds } = getLiveConfig(this, output);
1192
1309
  const sort = "usd-value";
1193
1310
  const limit = 10;
1194
1311
  const fetchBalanceData = async () => {
@@ -1198,7 +1315,7 @@ var balanceCommand = new Command2("balance").description("Show balances in your
1198
1315
  ]);
1199
1316
  if (walletResult.status === "rejected" || coinsResult.status === "rejected") {
1200
1317
  const err = walletResult.status === "rejected" ? walletResult.reason : coinsResult.reason;
1201
- throw new Error(err instanceof Error ? err.message : String(err));
1318
+ throw err instanceof Error ? err : new Error(String(err));
1202
1319
  }
1203
1320
  const rankedBalances = coinsResult.value.balances.map(
1204
1321
  (balance, index) => ({
@@ -1215,10 +1332,7 @@ var balanceCommand = new Command2("balance").description("Show balances in your
1215
1332
  };
1216
1333
  if (json) {
1217
1334
  const data = await fetchBalanceData().catch(
1218
- (err) => outputErrorAndExit(
1219
- json,
1220
- `Request failed: ${err instanceof Error ? err.message : String(err)}`
1221
- )
1335
+ (err) => outputErrorAndExit(json, `Request failed: ${apiErrorMessage(err)}`)
1222
1336
  );
1223
1337
  outputData(json, {
1224
1338
  json: {
@@ -1227,7 +1341,7 @@ var balanceCommand = new Command2("balance").description("Show balances in your
1227
1341
  (balance) => formatBalanceJson(balance, balance.rank)
1228
1342
  )
1229
1343
  },
1230
- table: () => {
1344
+ render: () => {
1231
1345
  }
1232
1346
  });
1233
1347
  track("cli_balances", {
@@ -1260,10 +1374,7 @@ var balanceCommand = new Command2("balance").description("Show balances in your
1260
1374
  });
1261
1375
  } else {
1262
1376
  const data = await fetchBalanceData().catch(
1263
- (err) => outputErrorAndExit(
1264
- json,
1265
- `Request failed: ${err instanceof Error ? err.message : String(err)}`
1266
- )
1377
+ (err) => outputErrorAndExit(json, `Request failed: ${apiErrorMessage(err)}`)
1267
1378
  );
1268
1379
  renderOnce(
1269
1380
  /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
@@ -1311,15 +1422,19 @@ var balanceCommand = new Command2("balance").description("Show balances in your
1311
1422
  live: false,
1312
1423
  result_count: data.rankedBalances.length,
1313
1424
  total_count: data.total,
1314
- output_format: "text"
1425
+ output_format: "static"
1315
1426
  });
1316
1427
  }
1317
1428
  });
1318
- balanceCommand.command("spendable").description("Show wallet token balances (ETH, USDC, ZORA)").action(async function() {
1429
+ balanceCommand.command("spendable").description("Show wallet token balances (ETH, USDC, ZORA)").option("--live", "Interactive live-updating display (default)").option("--static", "Static snapshot").option(
1430
+ "--refresh <seconds>",
1431
+ "Auto-refresh interval in seconds, requires --live (min 5)",
1432
+ "30"
1433
+ ).action(async function() {
1319
1434
  const output = getOutputMode(this, "live");
1320
1435
  const json = output === "json";
1321
1436
  const account = resolveContext(json);
1322
- const { live, intervalSeconds } = getLiveConfig(this, "live");
1437
+ const { live, intervalSeconds } = getLiveConfig(this, output);
1323
1438
  const fetchSpendableData = async () => {
1324
1439
  const walletResult = await fetchWalletBalances(account.address);
1325
1440
  return {
@@ -1331,14 +1446,11 @@ balanceCommand.command("spendable").description("Show wallet token balances (ETH
1331
1446
  };
1332
1447
  if (json) {
1333
1448
  const data = await fetchSpendableData().catch(
1334
- (err) => outputErrorAndExit(
1335
- json,
1336
- `Request failed: ${err instanceof Error ? err.message : String(err)}`
1337
- )
1449
+ (err) => outputErrorAndExit(json, `Request failed: ${apiErrorMessage(err)}`)
1338
1450
  );
1339
1451
  outputData(json, {
1340
1452
  json: { wallet: data.walletBalancesJson },
1341
- table: () => {
1453
+ render: () => {
1342
1454
  }
1343
1455
  });
1344
1456
  } else if (live) {
@@ -1356,26 +1468,29 @@ balanceCommand.command("spendable").description("Show wallet token balances (ETH
1356
1468
  );
1357
1469
  } else {
1358
1470
  const walletResult = await fetchWalletBalances(account.address).catch(
1359
- (err) => outputErrorAndExit(
1360
- json,
1361
- `Request failed: ${err instanceof Error ? err.message : String(err)}`
1362
- )
1471
+ (err) => outputErrorAndExit(json, `Request failed: ${apiErrorMessage(err)}`)
1363
1472
  );
1364
1473
  renderWallet(json, walletResult);
1365
1474
  }
1366
1475
  });
1367
- balanceCommand.command("coins").description("Show coin positions").option("--sort <sort>", `Sort by: ${SORT_OPTIONS}`, "usd-value").option("--limit <n>", "Number of results (max 20)", "10").action(async function(opts) {
1476
+ balanceCommand.command("coins").description("Show coin positions").option("--sort <sort>", `Sort by: ${SORT_OPTIONS}`, "usd-value").option("--limit <n>", "Number of results (max 20)", "10").option("--live", "Interactive live-updating display (default)").option("--static", "Static snapshot").option(
1477
+ "--refresh <seconds>",
1478
+ "Auto-refresh interval in seconds, requires --live (min 5)",
1479
+ "30"
1480
+ ).option("--after <cursor>", "Pagination cursor from a previous result").action(async function(opts) {
1368
1481
  const output = getOutputMode(this, "live");
1369
1482
  const json = output === "json";
1370
1483
  const { sort, limit } = validateCoinOpts(json, opts.sort, opts.limit);
1484
+ const after = opts.after;
1371
1485
  const account = resolveContext(json);
1372
- const { live, intervalSeconds } = getLiveConfig(this, "live");
1373
- const fetchCoinsData = async () => {
1374
- const { balances, total } = await fetchCoins(
1486
+ const { live, intervalSeconds } = getLiveConfig(this, output);
1487
+ const fetchCoinsPage = async (cursor) => {
1488
+ const { balances, total, pageInfo } = await fetchCoins(
1375
1489
  json,
1376
1490
  account.address,
1377
1491
  sort,
1378
- limit
1492
+ limit,
1493
+ cursor
1379
1494
  );
1380
1495
  const rankedBalances = balances.map((balance, index) => ({
1381
1496
  ...balance,
@@ -1385,43 +1500,53 @@ balanceCommand.command("coins").description("Show coin positions").option("--sor
1385
1500
  walletBalances: [],
1386
1501
  walletBalancesJson: [],
1387
1502
  rankedBalances,
1388
- total
1503
+ total,
1504
+ pageInfo
1389
1505
  };
1390
1506
  };
1391
1507
  if (json) {
1392
- const data = await fetchCoinsData();
1393
- renderCoins(json, data.rankedBalances, data.total, sort);
1508
+ const data = await fetchCoinsPage(after);
1509
+ renderCoins(
1510
+ json,
1511
+ data.rankedBalances,
1512
+ data.total,
1513
+ sort,
1514
+ limit,
1515
+ data.pageInfo
1516
+ );
1394
1517
  } else if (live) {
1395
1518
  await renderLive(
1396
1519
  /* @__PURE__ */ jsx3(
1397
1520
  BalanceView,
1398
1521
  {
1399
- fetchData: fetchCoinsData,
1522
+ fetchData: fetchCoinsPage,
1400
1523
  sort,
1401
1524
  mode: "coins",
1525
+ initialCursor: after,
1402
1526
  autoRefresh: live,
1403
1527
  intervalSeconds
1404
1528
  }
1405
1529
  )
1406
1530
  );
1407
1531
  } else {
1408
- const { balances, total } = await fetchCoins(
1532
+ const { balances, total, pageInfo } = await fetchCoins(
1409
1533
  json,
1410
1534
  account.address,
1411
1535
  sort,
1412
- limit
1536
+ limit,
1537
+ after
1413
1538
  );
1414
- renderCoins(json, balances, total, sort);
1539
+ renderCoins(json, balances, total, sort, limit, pageInfo);
1415
1540
  }
1416
1541
  });
1417
1542
 
1418
1543
  // src/commands/buy.ts
1419
1544
  import { Command as Command3 } from "commander";
1420
1545
  import confirm2 from "@inquirer/confirm";
1421
- import { parseUnits, formatUnits as formatUnits3, isAddress } from "viem";
1546
+ import { parseUnits, formatUnits as formatUnits4, isAddress } from "viem";
1422
1547
  import {
1423
1548
  setApiKey as setApiKey2,
1424
- getCoin,
1549
+ getCoin as getCoin2,
1425
1550
  tradeCoin,
1426
1551
  createTradeCall
1427
1552
  } from "@zoralabs/coins-sdk";
@@ -1429,7 +1554,7 @@ import {
1429
1554
  // src/lib/trade-helpers.ts
1430
1555
  import {
1431
1556
  parseEther,
1432
- formatUnits as formatUnits2,
1557
+ formatUnits as formatUnits3,
1433
1558
  isAddressEqual,
1434
1559
  parseEventLogs,
1435
1560
  erc20Abi as erc20Abi2
@@ -1462,25 +1587,6 @@ var parsePercentageLikeValue = (value) => {
1462
1587
  const parsed = Number(value);
1463
1588
  return Number.isFinite(parsed) ? parsed : void 0;
1464
1589
  };
1465
- var formatAmountDisplay = (amount, decimals) => {
1466
- const formatted = formatUnits2(amount, decimals);
1467
- const parts = formatted.split(".");
1468
- if (!parts[1]) {
1469
- return new Intl.NumberFormat("en-US", {
1470
- maximumFractionDigits: 2
1471
- }).format(Number(formatted));
1472
- }
1473
- const twoDecimal = `${parts[0]}.${parts[1].slice(0, 2)}`;
1474
- let maxDecimals = 2;
1475
- if (Number(twoDecimal) === 0 && amount > 0n) {
1476
- const sigIndex = parts[1].search(/[1-9]/);
1477
- maxDecimals = sigIndex === -1 ? 6 : Math.min(sigIndex + 4, parts[1].length);
1478
- }
1479
- const truncated = `${parts[0]}.${parts[1].slice(0, maxDecimals)}`;
1480
- return new Intl.NumberFormat("en-US", {
1481
- maximumFractionDigits: maxDecimals
1482
- }).format(Number(truncated));
1483
- };
1484
1590
  var getReceivedAmountFromReceipt = ({
1485
1591
  receipt,
1486
1592
  tokenAddress,
@@ -1546,12 +1652,12 @@ var printQuote = (json, info) => {
1546
1652
  coin: info.coinSymbol,
1547
1653
  address: info.address,
1548
1654
  spend: {
1549
- amount: formatUnits2(info.amountIn, info.inputTokenDecimals),
1655
+ amount: formatUnits3(info.amountIn, info.inputTokenDecimals),
1550
1656
  raw: info.amountIn.toString(),
1551
1657
  symbol: info.inputTokenSymbol
1552
1658
  },
1553
1659
  estimated: {
1554
- amount: formatUnits2(BigInt(info.amountOut), 18),
1660
+ amount: formatUnits3(BigInt(info.amountOut), 18),
1555
1661
  raw: info.amountOut,
1556
1662
  symbol: info.coinSymbol
1557
1663
  },
@@ -1559,29 +1665,33 @@ var printQuote = (json, info) => {
1559
1665
  });
1560
1666
  return;
1561
1667
  }
1668
+ const spendFormatted = formatAmountDisplay(
1669
+ info.amountIn,
1670
+ info.inputTokenDecimals
1671
+ );
1672
+ const coinsFormatted = formatAmountDisplay(BigInt(info.amountOut), 18);
1562
1673
  console.log(`
1563
- Buy ${info.coinName} (${info.coinSymbol})
1674
+ Buy \x1B[1m${info.coinName}\x1B[0m`);
1675
+ console.log(` ${info.coinType} \xB7 ${info.address}
1564
1676
  `);
1565
- console.log(` Amount ${info.spendAmount}`);
1566
- console.log(` You get ~${info.coinsFormatted} ${info.coinSymbol}`);
1677
+ console.log(` Amount ${spendFormatted} ${info.inputTokenSymbol}`);
1678
+ console.log(` You get ~${coinsFormatted} ${info.coinSymbol}`);
1567
1679
  console.log(` Slippage ${info.slippagePct}%
1568
1680
  `);
1569
1681
  };
1570
1682
  var printTradeResult = (json, info) => {
1571
- const receivedAmount = formatUnits2(info.receivedAmountOut, 18);
1572
- const receivedFormatted = formatCoinsDisplay(receivedAmount);
1573
1683
  if (json) {
1574
1684
  outputJson({
1575
1685
  action: "buy",
1576
1686
  coin: info.coinSymbol,
1577
1687
  address: info.address,
1578
1688
  spent: {
1579
- amount: formatUnits2(info.amountIn, info.inputTokenDecimals),
1689
+ amount: formatUnits3(info.amountIn, info.inputTokenDecimals),
1580
1690
  raw: info.amountIn.toString(),
1581
1691
  symbol: info.inputTokenSymbol
1582
1692
  },
1583
1693
  received: {
1584
- amount: receivedAmount,
1694
+ amount: formatUnits3(info.receivedAmountOut, 18),
1585
1695
  raw: info.receivedAmountOut.toString(),
1586
1696
  symbol: info.coinSymbol
1587
1697
  },
@@ -1589,65 +1699,274 @@ var printTradeResult = (json, info) => {
1589
1699
  });
1590
1700
  return;
1591
1701
  }
1702
+ const spentFormatted = formatAmountDisplay(
1703
+ info.amountIn,
1704
+ info.inputTokenDecimals
1705
+ );
1706
+ const receivedFormatted = formatAmountDisplay(info.receivedAmountOut, 18);
1592
1707
  console.log(`
1593
- Bought ${info.coinName}
1708
+ Bought \x1B[1m${info.coinName}\x1B[0m`);
1709
+ console.log(` ${info.coinType} \xB7 ${info.address}
1594
1710
  `);
1595
- console.log(` Spent ${info.spendAmount} ${info.inputTokenSymbol}`);
1711
+ console.log(` Spent ${spentFormatted} ${info.inputTokenSymbol}`);
1596
1712
  console.log(` Received ${receivedFormatted} ${info.coinSymbol}`);
1597
1713
  console.log(` Tx ${info.txHash}
1598
1714
  `);
1599
1715
  };
1600
1716
 
1717
+ // src/lib/coin-ref.ts
1718
+ import { getCoin, getProfile, getTrend } from "@zoralabs/coins-sdk";
1719
+ var TYPE_KEYWORDS = /* @__PURE__ */ new Set(["creator-coin", "trend"]);
1720
+ var CoinArgError = class extends Error {
1721
+ suggestion;
1722
+ constructor(message, suggestion) {
1723
+ super(message);
1724
+ this.suggestion = suggestion;
1725
+ }
1726
+ };
1727
+ function parsePositionalCoinArgs(firstArg, secondArg) {
1728
+ if (TYPE_KEYWORDS.has(firstArg)) {
1729
+ if (!secondArg) {
1730
+ throw new CoinArgError(
1731
+ `Missing identifier after "${firstArg}".`,
1732
+ `Usage: zora <command> ${firstArg} <name>`
1733
+ );
1734
+ }
1735
+ return { kind: "typed", type: firstArg, identifier: secondArg };
1736
+ }
1737
+ if (firstArg.startsWith("0x")) {
1738
+ return { kind: "address", address: firstArg };
1739
+ }
1740
+ return { kind: "ambiguous-name", name: firstArg };
1741
+ }
1742
+ function coinArgsToRef(parsed) {
1743
+ switch (parsed.kind) {
1744
+ case "typed":
1745
+ return { kind: "prefixed", type: parsed.type, name: parsed.identifier };
1746
+ case "address":
1747
+ return { kind: "address", address: parsed.address };
1748
+ case "ambiguous-name":
1749
+ return { kind: "ambiguous", name: parsed.name };
1750
+ }
1751
+ }
1752
+ async function resolveAmbiguousName(name) {
1753
+ const [creatorResult, trendResult] = await Promise.all([
1754
+ resolveByCreatorName(name),
1755
+ resolveByTrendTicker(name)
1756
+ ]);
1757
+ const creatorFound = creatorResult.kind === "found" ? creatorResult.coin : null;
1758
+ const trendFound = trendResult.kind === "found" ? trendResult.coin : null;
1759
+ if (creatorFound && trendFound) {
1760
+ return { kind: "ambiguous", creator: creatorFound, trend: trendFound };
1761
+ }
1762
+ if (creatorFound) {
1763
+ return { kind: "found", coin: creatorFound };
1764
+ }
1765
+ if (trendFound) {
1766
+ return { kind: "found", coin: trendFound };
1767
+ }
1768
+ return {
1769
+ kind: "not-found",
1770
+ message: `No coin found matching "${name}".`
1771
+ };
1772
+ }
1773
+ function formatAmbiguousError(name, creator, trend, command) {
1774
+ const creatorMcap = formatCompactUsd(creator.marketCap);
1775
+ const trendMcap = formatCompactUsd(trend.marketCap);
1776
+ return {
1777
+ message: [
1778
+ `Multiple coins match "${name}":`,
1779
+ ` creator-coin ${creator.name} ${creatorMcap} mcap`,
1780
+ ` trend ${trend.name} ${trendMcap} mcap`
1781
+ ].join("\n"),
1782
+ suggestion: `Use: zora ${command} creator-coin ${name} or zora ${command} trend ${name}`
1783
+ };
1784
+ }
1785
+ var COIN_TYPE_MAP = {
1786
+ CONTENT: "post",
1787
+ CREATOR: "creator-coin",
1788
+ TREND: "trend"
1789
+ };
1790
+ function mapCoinType(raw) {
1791
+ if (!raw) return "unknown";
1792
+ return COIN_TYPE_MAP[raw] ?? "unknown";
1793
+ }
1794
+ function coinFromToken(token) {
1795
+ return {
1796
+ name: token.name ?? "Unknown",
1797
+ address: token.address ?? "",
1798
+ coinType: mapCoinType(token.coinType),
1799
+ marketCap: token.marketCap ?? "0",
1800
+ marketCapDelta24h: token.marketCapDelta24h ?? "0",
1801
+ volume24h: token.volume24h ?? "0",
1802
+ uniqueHolders: token.uniqueHolders ?? 0,
1803
+ createdAt: token.createdAt,
1804
+ creatorAddress: token.creatorAddress,
1805
+ creatorHandle: token.creatorProfile?.handle,
1806
+ platformBlocked: token.platformBlocked ?? false
1807
+ };
1808
+ }
1809
+ async function resolveByAddress(address) {
1810
+ const response = await getCoin({ address });
1811
+ if (response.error || !response.data?.zora20Token) {
1812
+ return {
1813
+ kind: "not-found",
1814
+ message: `No coin found at address ${address}`
1815
+ };
1816
+ }
1817
+ return { kind: "found", coin: coinFromToken(response.data.zora20Token) };
1818
+ }
1819
+ async function resolveByTrendTicker(ticker) {
1820
+ const response = await getTrend({ ticker });
1821
+ if (response.error || !response.data?.trendCoin) {
1822
+ return {
1823
+ kind: "not-found",
1824
+ message: `No trend coin found with ticker "${ticker}"`
1825
+ };
1826
+ }
1827
+ return { kind: "found", coin: coinFromToken(response.data.trendCoin) };
1828
+ }
1829
+ async function resolveByCreatorName(name) {
1830
+ const response = await getProfile({ identifier: name });
1831
+ if (response.error || !response.data?.profile) {
1832
+ return {
1833
+ kind: "not-found",
1834
+ message: `No creator found with name "${name}"`
1835
+ };
1836
+ }
1837
+ const profile = response.data.profile;
1838
+ if (!profile.creatorCoin) {
1839
+ return {
1840
+ kind: "not-found",
1841
+ message: `"${name}" does not have a creator coin`
1842
+ };
1843
+ }
1844
+ return resolveByAddress(profile.creatorCoin.address);
1845
+ }
1846
+ async function resolveCoin(ref) {
1847
+ switch (ref.kind) {
1848
+ case "address":
1849
+ return resolveByAddress(ref.address);
1850
+ case "prefixed":
1851
+ if (ref.type === "trend") {
1852
+ return resolveByTrendTicker(ref.name);
1853
+ }
1854
+ return resolveByCreatorName(ref.name);
1855
+ case "ambiguous":
1856
+ return resolveByCreatorName(ref.name);
1857
+ }
1858
+ }
1859
+
1601
1860
  // src/commands/buy.ts
1602
- var buyCommand = new Command3("buy").description("Buy a coin").argument("<address>", "Coin contract address (0x\u2026)").option("--eth <value>", "Buy with ETH amount").option("--usd <value>", "Buy with USD equivalent (use with --token)").option("--token <asset>", "Token to spend: eth, usdc, zora", "eth").option("--percent <value>", "Buy with percentage of ETH balance").option("--all", "Swap all ETH for coin").option("--quote", "Print quote and exit without trading").option("--yes", "Skip confirmation and execute directly").option("--slippage <pct>", "Slippage tolerance percent", "1").option("--debug", "Print full quote request/response JSON").action(async function(coinAddress, opts) {
1861
+ var buyCommand = new Command3("buy").description("Buy a coin").argument(
1862
+ "[typeOrId]",
1863
+ "Type prefix (creator-coin, trend) or coin address/name"
1864
+ ).argument("[identifier]", "Coin name (when type prefix is given)").option("--eth <value>", "Buy with ETH amount").option("--usd <value>", "Buy with USD equivalent (use with --token)").option("--token <asset>", "Token to spend: eth, usdc, zora", "eth").option("--percent <value>", "Buy with percentage of ETH balance").option("--all", "Swap all ETH for coin").option("--quote", "Print quote and exit without trading").option("--yes", "Skip confirmation and execute directly").option("--slippage <pct>", "Slippage tolerance percent", "1").option("--debug", "Print full quote request/response JSON").action(async function(typeOrId, identifier, opts) {
1603
1865
  const json = getJson(this);
1604
1866
  const debug = opts.debug === true;
1605
- if (!isAddress(coinAddress)) {
1606
- outputErrorAndExit(json, `Invalid address: ${coinAddress}`);
1607
- }
1608
- const tokenKey = opts.token.toLowerCase();
1609
- if (!(tokenKey in BASE_TRADE_TOKENS)) {
1610
- outputErrorAndExit(
1611
- json,
1612
- `Invalid --token value: ${opts.token}. Use: eth, usdc, zora`
1613
- );
1614
- }
1615
- const inputToken = BASE_TRADE_TOKENS[tokenKey];
1616
- const amountMode = getAmountMode(
1617
- json,
1618
- opts,
1619
- BUY_AMOUNT_CHECKS,
1620
- "--eth, --usd, --percent, or --all"
1621
- );
1622
- const slippagePct = parsePercentageLikeValue(opts.slippage);
1623
- if (slippagePct === void 0 || slippagePct < 0 || slippagePct > 99) {
1624
- outputErrorAndExit(
1625
- json,
1626
- "Invalid --slippage value. Must be between 0 and 99."
1627
- );
1867
+ let parsed;
1868
+ try {
1869
+ parsed = parsePositionalCoinArgs(typeOrId, identifier);
1870
+ } catch (err) {
1871
+ if (err instanceof CoinArgError) {
1872
+ outputErrorAndExit(json, err.message, err.suggestion);
1873
+ }
1874
+ throw err;
1628
1875
  }
1629
- const slippage = slippagePct / 100;
1630
1876
  const apiKey = getApiKey();
1631
1877
  if (apiKey) {
1632
1878
  setApiKey2(apiKey);
1633
1879
  }
1634
- const account = resolveAccount(json);
1635
- const { publicClient, walletClient } = createClients(account);
1636
- let token;
1637
- try {
1638
- const response = await getCoin({ address: coinAddress });
1639
- token = response.data?.zora20Token;
1640
- } catch (err) {
1880
+ let coinAddress;
1881
+ if (parsed.kind === "address") {
1882
+ if (!isAddress(parsed.address)) {
1883
+ outputErrorAndExit(json, `Invalid address: ${parsed.address}`);
1884
+ return;
1885
+ }
1886
+ coinAddress = parsed.address;
1887
+ } else if (parsed.kind === "ambiguous-name") {
1888
+ let ambResult;
1889
+ try {
1890
+ ambResult = await resolveAmbiguousName(parsed.name);
1891
+ } catch (err) {
1892
+ outputErrorAndExit(
1893
+ json,
1894
+ `Request failed: ${err instanceof Error ? err.message : String(err)}`
1895
+ );
1896
+ return;
1897
+ }
1898
+ if (ambResult.kind === "not-found") {
1899
+ outputErrorAndExit(json, ambResult.message);
1900
+ return;
1901
+ }
1902
+ if (ambResult.kind === "ambiguous") {
1903
+ const { message, suggestion } = formatAmbiguousError(
1904
+ parsed.name,
1905
+ ambResult.creator,
1906
+ ambResult.trend,
1907
+ "buy"
1908
+ );
1909
+ outputErrorAndExit(json, message, suggestion);
1910
+ return;
1911
+ }
1912
+ coinAddress = ambResult.coin.address;
1913
+ } else {
1914
+ const ref = coinArgsToRef(parsed);
1915
+ try {
1916
+ const result = await resolveCoin(ref);
1917
+ if (result.kind === "not-found") {
1918
+ outputErrorAndExit(json, result.message, result.suggestion);
1919
+ return;
1920
+ }
1921
+ coinAddress = result.coin.address;
1922
+ } catch (err) {
1923
+ outputErrorAndExit(
1924
+ json,
1925
+ `Request failed: ${err instanceof Error ? err.message : String(err)}`
1926
+ );
1927
+ return;
1928
+ }
1929
+ }
1930
+ const tokenKey = opts.token.toLowerCase();
1931
+ if (!(tokenKey in BASE_TRADE_TOKENS)) {
1932
+ outputErrorAndExit(
1933
+ json,
1934
+ `Invalid --token value: ${opts.token}. Use: eth, usdc, zora`
1935
+ );
1936
+ }
1937
+ const inputToken = BASE_TRADE_TOKENS[tokenKey];
1938
+ const amountMode = getAmountMode(
1939
+ json,
1940
+ opts,
1941
+ BUY_AMOUNT_CHECKS,
1942
+ "--eth, --usd, --percent, or --all"
1943
+ );
1944
+ const slippagePct = parsePercentageLikeValue(opts.slippage);
1945
+ if (slippagePct === void 0 || slippagePct < 0 || slippagePct > 99) {
1641
1946
  outputErrorAndExit(
1642
1947
  json,
1643
- `Failed to fetch coin: ${err instanceof Error ? err.message : String(err)}`
1948
+ "Invalid --slippage value. Must be between 0 and 99."
1644
1949
  );
1645
1950
  }
1951
+ const slippage = slippagePct / 100;
1952
+ const account = resolveAccount(json);
1953
+ const { publicClient, walletClient } = createClients(account);
1954
+ let token;
1955
+ try {
1956
+ const response = await getCoin2({ address: coinAddress });
1957
+ token = response.data?.zora20Token;
1958
+ } catch (err) {
1959
+ outputErrorAndExit(json, `Failed to fetch coin: ${apiErrorMessage(err)}`);
1960
+ }
1646
1961
  if (!token) {
1647
1962
  outputErrorAndExit(json, `Coin not found: ${coinAddress}`);
1648
1963
  }
1964
+ if (token.platformBlocked) {
1965
+ outputErrorAndExit(json, bannedCoinBuyMessage(coinAddress));
1966
+ }
1649
1967
  const coinName = token.name;
1650
1968
  const coinSymbol = token.symbol;
1969
+ const coinType = mapCoinType(token.coinType);
1651
1970
  let amountIn;
1652
1971
  if (amountMode === "usd") {
1653
1972
  const usdVal = parsePercentageLikeValue(opts.usd);
@@ -1682,7 +2001,7 @@ var buyCommand = new Command3("buy").description("Buy a coin").argument("<addres
1682
2001
  }
1683
2002
  if (debug) {
1684
2003
  console.error(
1685
- `[debug] $${usdVal} USD = ${formatUnits3(amountIn, inputToken.decimals)} ${inputToken.symbol} (price: $${priceUsd})`
2004
+ `[debug] $${usdVal} USD = ${formatUnits4(amountIn, inputToken.decimals)} ${inputToken.symbol} (price: $${priceUsd})`
1686
2005
  );
1687
2006
  }
1688
2007
  } else if (amountMode === "eth") {
@@ -1735,7 +2054,7 @@ var buyCommand = new Command3("buy").description("Buy a coin").argument("<addres
1735
2054
  if (isEth && balance <= gasReserve) {
1736
2055
  outputErrorAndExit(
1737
2056
  json,
1738
- `Balance too low (${formatEthDisplay(balance)} ETH). Need >${formatEthDisplay(GAS_RESERVE)} ETH for gas.`
2057
+ `Balance too low (${formatAmountDisplay(balance, 18)} ETH). Need >${formatAmountDisplay(GAS_RESERVE, 18)} ETH for gas.`
1739
2058
  );
1740
2059
  }
1741
2060
  const spendableBalance = balance - gasReserve;
@@ -1765,7 +2084,7 @@ var buyCommand = new Command3("buy").description("Buy a coin").argument("<addres
1765
2084
  const priceUsd = inputToken.fixedPriceUsd ?? await fetchTokenPriceUsd(inputToken.priceAddress);
1766
2085
  if (priceUsd != null) {
1767
2086
  swapAmountUsd = Number(
1768
- (Number(formatUnits3(amountIn, inputToken.decimals)) * priceUsd).toFixed(2)
2087
+ (Number(formatUnits4(amountIn, inputToken.decimals)) * priceUsd).toFixed(2)
1769
2088
  );
1770
2089
  }
1771
2090
  }
@@ -1816,29 +2135,23 @@ ${err instanceof Error ? err.stack || err.message : String(err)}
1816
2135
  }
1817
2136
  outputErrorAndExit(
1818
2137
  json,
1819
- `Quote failed: ${msg}`,
2138
+ `Quote failed: ${apiErrorMessage(err)}`,
1820
2139
  "Check the coin address is valid and try again. Use --debug for full error details."
1821
2140
  );
1822
2141
  }
1823
- const spendAmount = formatUnits3(amountIn, inputToken.decimals);
1824
- const spendFormatted = new Intl.NumberFormat("en-US", {
1825
- maximumFractionDigits: 6
1826
- }).format(Number(spendAmount));
1827
- const coinsOut = formatUnits3(BigInt(amountOut), 18);
1828
- const coinsFormatted = formatCoinsDisplay(coinsOut);
2142
+ const quoteInfo = {
2143
+ coinName,
2144
+ coinSymbol,
2145
+ coinType,
2146
+ address: coinAddress,
2147
+ amountIn,
2148
+ inputTokenSymbol: inputToken.symbol,
2149
+ inputTokenDecimals: inputToken.decimals,
2150
+ amountOut,
2151
+ slippagePct
2152
+ };
1829
2153
  if (opts.quote) {
1830
- printQuote(json, {
1831
- coinName,
1832
- coinSymbol,
1833
- address: coinAddress,
1834
- spendAmount: `${spendFormatted} ${inputToken.symbol}`,
1835
- amountIn,
1836
- inputTokenSymbol: inputToken.symbol,
1837
- inputTokenDecimals: inputToken.decimals,
1838
- coinsFormatted,
1839
- amountOut,
1840
- slippagePct
1841
- });
2154
+ printQuote(json, quoteInfo);
1842
2155
  track("cli_buy", {
1843
2156
  action: "quote",
1844
2157
  coin_address: coinAddress,
@@ -1849,23 +2162,12 @@ ${err instanceof Error ? err.stack || err.message : String(err)}
1849
2162
  valueUsd: swapAmountUsd,
1850
2163
  swapCoinType: token.coinType ?? null,
1851
2164
  slippage: slippagePct,
1852
- output_format: json ? "json" : "table"
2165
+ output_format: json ? "json" : "static"
1853
2166
  });
1854
2167
  return;
1855
2168
  }
1856
2169
  if (!opts.yes) {
1857
- printQuote(false, {
1858
- coinName,
1859
- coinSymbol,
1860
- address: coinAddress,
1861
- spendAmount: `${spendFormatted} ${inputToken.symbol}`,
1862
- amountIn,
1863
- inputTokenSymbol: inputToken.symbol,
1864
- inputTokenDecimals: inputToken.decimals,
1865
- coinsFormatted,
1866
- amountOut,
1867
- slippagePct
1868
- });
2170
+ printQuote(false, quoteInfo);
1869
2171
  const ok = await confirm2({
1870
2172
  message: "Confirm?",
1871
2173
  default: false
@@ -1897,15 +2199,12 @@ ${err instanceof Error ? err.stack || err.message : String(err)}
1897
2199
  valueUsd: swapAmountUsd,
1898
2200
  swapCoinType,
1899
2201
  slippage: slippagePct,
1900
- output_format: json ? "json" : "table",
2202
+ output_format: json ? "json" : "static",
1901
2203
  success: false,
1902
2204
  error_type: err instanceof Error ? err.constructor.name : "unknown"
1903
2205
  });
1904
2206
  await shutdownAnalytics();
1905
- outputErrorAndExit(
1906
- json,
1907
- `Transaction failed: ${err instanceof Error ? err.message : String(err)}`
1908
- );
2207
+ outputErrorAndExit(json, tradeErrorMessage(err));
1909
2208
  }
1910
2209
  txHash = receipt.transactionHash;
1911
2210
  try {
@@ -1925,8 +2224,8 @@ ${err instanceof Error ? err.stack || err.message : String(err)}
1925
2224
  printTradeResult(json, {
1926
2225
  coinName,
1927
2226
  coinSymbol,
2227
+ coinType,
1928
2228
  address: coinAddress,
1929
- spendAmount,
1930
2229
  amountIn,
1931
2230
  inputTokenSymbol: inputToken.symbol,
1932
2231
  inputTokenDecimals: inputToken.decimals,
@@ -1947,7 +2246,7 @@ ${err instanceof Error ? err.stack || err.message : String(err)}
1947
2246
  transactionHash: txHash,
1948
2247
  logIndex: swapLogIndex,
1949
2248
  slippage: slippagePct,
1950
- output_format: json ? "json" : "table",
2249
+ output_format: json ? "json" : "static",
1951
2250
  success: true,
1952
2251
  tx_hash: txHash
1953
2252
  });
@@ -1960,9 +2259,6 @@ import {
1960
2259
  getCoinsTopVolume24h,
1961
2260
  getCoinsMostValuable,
1962
2261
  getCoinsNew,
1963
- getCoinsTopGainers,
1964
- getCoinsLastTraded,
1965
- getCoinsLastTradedUnique,
1966
2262
  getExploreTopVolumeAll24h,
1967
2263
  getExploreTopVolumeCreators24h,
1968
2264
  getExploreNewAll,
@@ -1985,9 +2281,6 @@ var SORT_LABELS2 = {
1985
2281
  mcap: "Top by Market Cap",
1986
2282
  volume: "Top by 24h Volume",
1987
2283
  new: "New",
1988
- gainers: "Top Gainers (24h)",
1989
- "last-traded": "Last Traded",
1990
- "last-traded-unique": "Last Traded (Unique)",
1991
2284
  trending: "Trending",
1992
2285
  featured: "Featured"
1993
2286
  };
@@ -2007,11 +2300,47 @@ var COIN_TYPE_DISPLAY = {
2007
2300
  import { useState as useState3, useEffect as useEffect3, useCallback as useCallback3, useRef as useRef2 } from "react";
2008
2301
  import { Box as Box4, Text as Text4, useInput as useInput2, useApp as useApp2 } from "ink";
2009
2302
  import Spinner2 from "ink-spinner";
2303
+
2304
+ // src/lib/clipboard.ts
2305
+ import { execFileSync } from "child_process";
2306
+ import { platform as platform2 } from "os";
2307
+ var copyToClipboard = (text) => {
2308
+ const os = platform2();
2309
+ try {
2310
+ if (os === "darwin") {
2311
+ execFileSync("pbcopy", {
2312
+ input: text,
2313
+ stdio: ["pipe", "ignore", "ignore"]
2314
+ });
2315
+ } else if (os === "linux") {
2316
+ execFileSync("xclip", ["-selection", "clipboard"], {
2317
+ input: text,
2318
+ stdio: ["pipe", "ignore", "ignore"]
2319
+ });
2320
+ } else if (os === "win32") {
2321
+ execFileSync("clip", {
2322
+ input: text,
2323
+ stdio: ["pipe", "ignore", "ignore"]
2324
+ });
2325
+ } else {
2326
+ return false;
2327
+ }
2328
+ return true;
2329
+ } catch {
2330
+ return false;
2331
+ }
2332
+ };
2333
+
2334
+ // src/components/ExploreView.tsx
2010
2335
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
2011
2336
  var COLUMNS = [
2012
2337
  { header: "#", width: 4, accessor: (c) => String(c.rank) },
2013
2338
  { header: "Name", width: 20, accessor: (c) => c.name ?? "Unknown" },
2014
- { header: "Address", width: 44, accessor: (c) => c.address ?? "" },
2339
+ {
2340
+ header: "Address",
2341
+ width: 14,
2342
+ accessor: (c) => c.address ? truncateAddress(c.address) : ""
2343
+ },
2015
2344
  {
2016
2345
  header: "Type",
2017
2346
  width: 14,
@@ -2061,6 +2390,11 @@ var ExploreView = ({
2061
2390
  const cache = useRef2(/* @__PURE__ */ new Map());
2062
2391
  const { refreshCount, secondsUntilRefresh, triggerManualRefresh } = useAutoRefresh(intervalSeconds, autoRefresh);
2063
2392
  const [manualRefreshCount, setManualRefreshCount] = useState3(0);
2393
+ const [selectedRow, setSelectedRow] = useState3(0);
2394
+ const [copyFeedback, setCopyFeedback] = useState3(null);
2395
+ useEffect3(() => {
2396
+ setSelectedRow((r) => Math.min(r, Math.max(0, coins.length - 1)));
2397
+ }, [coins.length]);
2064
2398
  const loadPage = useCallback3(
2065
2399
  async (cursor) => {
2066
2400
  const cacheKey = cursor ?? CACHE_KEY_FIRST;
@@ -2101,24 +2435,44 @@ var ExploreView = ({
2101
2435
  return;
2102
2436
  }
2103
2437
  if (loading) return;
2438
+ if (key.upArrow || input === "k") {
2439
+ setSelectedRow((r) => Math.max(0, r - 1));
2440
+ return;
2441
+ }
2442
+ if (key.downArrow || input === "j") {
2443
+ setSelectedRow((r) => Math.min(coins.length - 1, r + 1));
2444
+ return;
2445
+ }
2446
+ if (input === "c") {
2447
+ const coin = coins[selectedRow];
2448
+ if (coin?.address) {
2449
+ const ok = copyToClipboard(coin.address);
2450
+ setCopyFeedback(ok ? "Copied!" : "Copy failed");
2451
+ setTimeout(() => setCopyFeedback(null), 1500);
2452
+ }
2453
+ return;
2454
+ }
2104
2455
  const canGoNext = pageInfo?.hasNextPage && pageInfo.endCursor;
2105
2456
  const canGoPrev = cursorHistory.length > 0;
2106
2457
  if ((input === "n" || key.rightArrow) && canGoNext) {
2107
2458
  setCursorHistory((prev) => [...prev, currentCursor]);
2108
2459
  setCurrentCursor(pageInfo.endCursor);
2109
2460
  setPage((p) => p + 1);
2461
+ setSelectedRow(0);
2110
2462
  }
2111
2463
  if ((input === "p" || key.leftArrow) && canGoPrev) {
2112
2464
  const prev = cursorHistory[cursorHistory.length - 1];
2113
2465
  setCursorHistory((h) => h.slice(0, -1));
2114
2466
  setCurrentCursor(prev);
2115
2467
  setPage((p) => p - 1);
2468
+ setSelectedRow(0);
2116
2469
  }
2117
2470
  if (input === "r") {
2118
2471
  const cacheKey = currentCursor ?? CACHE_KEY_FIRST;
2119
2472
  cache.current.delete(cacheKey);
2120
2473
  triggerManualRefresh();
2121
2474
  setManualRefreshCount((c) => c + 1);
2475
+ setSelectedRow(0);
2122
2476
  }
2123
2477
  });
2124
2478
  if (error) {
@@ -2172,12 +2526,13 @@ var ExploreView = ({
2172
2526
  rank: (page - 1) * limit + i + 1
2173
2527
  }));
2174
2528
  const hints = [];
2529
+ hints.push("\u2191\u2193 select");
2530
+ hints.push("c copy address");
2175
2531
  if (cursorHistory.length > 0) hints.push("\u2190 prev");
2176
2532
  if (pageInfo?.hasNextPage) hints.push("\u2192 next");
2177
- hints.push("r refresh");
2178
- if (autoRefresh) hints.push(`auto: ${secondsUntilRefresh}s`);
2533
+ hints.push(autoRefresh ? `r refresh (${secondsUntilRefresh}s)` : "r refresh");
2179
2534
  hints.push("q quit");
2180
- const footer = hints.join(" \xB7 ");
2535
+ const footer = hints.join(" \xB7 ") + (copyFeedback ? ` ${copyFeedback}` : "");
2181
2536
  return /* @__PURE__ */ jsx4(
2182
2537
  Table,
2183
2538
  {
@@ -2185,7 +2540,8 @@ var ExploreView = ({
2185
2540
  columns: COLUMNS,
2186
2541
  title,
2187
2542
  subtitle,
2188
- footer
2543
+ footer,
2544
+ selectedRow
2189
2545
  }
2190
2546
  );
2191
2547
  };
@@ -2211,15 +2567,6 @@ var QUERY_MAP = {
2211
2567
  "creator-coin": getCreatorCoins,
2212
2568
  post: getCoinsNew
2213
2569
  },
2214
- gainers: {
2215
- post: getCoinsTopGainers
2216
- },
2217
- "last-traded": {
2218
- post: getCoinsLastTraded
2219
- },
2220
- "last-traded-unique": {
2221
- post: getCoinsLastTradedUnique
2222
- },
2223
2570
  trending: {
2224
2571
  all: getTrendingAll,
2225
2572
  trend: getTrendingTrends,
@@ -2231,12 +2578,42 @@ var QUERY_MAP = {
2231
2578
  post: getExploreFeaturedVideos
2232
2579
  }
2233
2580
  };
2581
+ var STATIC_COLUMNS = [
2582
+ { header: "#", width: 4, accessor: (c) => String(c.rank) },
2583
+ { header: "Name", width: 20, accessor: (c) => c.name ?? "Unknown" },
2584
+ { header: "Address", width: 48, accessor: (c) => c.address ?? "" },
2585
+ {
2586
+ header: "Type",
2587
+ width: 14,
2588
+ accessor: (c) => COIN_TYPE_DISPLAY[c.coinType ?? ""] ?? c.coinType ?? ""
2589
+ },
2590
+ {
2591
+ header: "Market Cap",
2592
+ width: 12,
2593
+ accessor: (c) => formatCompactUsd(c.marketCap)
2594
+ },
2595
+ {
2596
+ header: "24h Vol",
2597
+ width: 12,
2598
+ accessor: (c) => formatCompactUsd(c.volume24h)
2599
+ },
2600
+ {
2601
+ header: "24h Change",
2602
+ width: 11,
2603
+ accessor: (c) => formatMcapChange(c.marketCap, c.marketCapDelta24h).text,
2604
+ color: (c) => formatMcapChange(c.marketCap, c.marketCapDelta24h).color
2605
+ }
2606
+ ];
2234
2607
  var SORT_OPTIONS2 = Object.keys(SORT_LABELS2).join(", ");
2235
2608
  var exploreCommand = new Command4("explore").description("Browse top, new, and highest volume coins").option("--sort <sort>", `Sort by: ${SORT_OPTIONS2}`, "mcap").option(
2236
2609
  "--type <type>",
2237
2610
  "Filter by type: all, trend, creator-coin, post (availability varies by sort)",
2238
- "post"
2239
- ).option("--limit <n>", "Number of results (max 20)", "10").option("--after <cursor>", "Pagination cursor from a previous result").action(async function(opts) {
2611
+ "creator-coin"
2612
+ ).option("--limit <n>", "Number of results (max 20)", "10").option("--after <cursor>", "Pagination cursor from a previous result").option("--live", "Interactive live-updating display (default)").option("--static", "Static snapshot").option(
2613
+ "--refresh <seconds>",
2614
+ "Auto-refresh interval in seconds, requires --live (min 5)",
2615
+ "30"
2616
+ ).action(async function(opts) {
2240
2617
  const output = getOutputMode(this, "live");
2241
2618
  const json = output === "json";
2242
2619
  const sort = opts.sort;
@@ -2275,10 +2652,7 @@ var exploreCommand = new Command4("explore").description("Browse top, new, and h
2275
2652
  try {
2276
2653
  response = await queryFn({ count: limit, after });
2277
2654
  } catch (err) {
2278
- outputErrorAndExit(
2279
- json,
2280
- `Request failed: ${err instanceof Error ? err.message : String(err)}`
2281
- );
2655
+ outputErrorAndExit(json, `Request failed: ${apiErrorMessage(err)}`);
2282
2656
  }
2283
2657
  if (response.error) {
2284
2658
  const msg = typeof response.error === "object" && response.error.error ? response.error.error : JSON.stringify(response.error);
@@ -2298,7 +2672,7 @@ var exploreCommand = new Command4("explore").description("Browse top, new, and h
2298
2672
  output_format: "json"
2299
2673
  });
2300
2674
  } else {
2301
- const { live, intervalSeconds } = getLiveConfig(this, "live");
2675
+ const { live, intervalSeconds } = getLiveConfig(this, output);
2302
2676
  const fetchPage = async (cursor) => {
2303
2677
  const response = await queryFn({ count: limit, after: cursor });
2304
2678
  if (response.error) {
@@ -2310,29 +2684,55 @@ var exploreCommand = new Command4("explore").description("Browse top, new, and h
2310
2684
  const pageInfo = response.data?.exploreList?.pageInfo;
2311
2685
  return { coins, pageInfo };
2312
2686
  };
2313
- await renderLive(
2314
- /* @__PURE__ */ jsx5(
2315
- ExploreView,
2316
- {
2317
- fetchPage,
2318
- sort,
2319
- type,
2320
- limit,
2321
- initialCursor: after,
2322
- autoRefresh: live,
2323
- intervalSeconds
2324
- }
2325
- )
2326
- );
2327
- track("cli_explore", {
2328
- sort,
2329
- type,
2330
- limit,
2331
- live,
2332
- interval: intervalSeconds,
2333
- paginated: after !== void 0,
2334
- output_format: "text"
2335
- });
2687
+ if (live) {
2688
+ await renderLive(
2689
+ /* @__PURE__ */ jsx5(
2690
+ ExploreView,
2691
+ {
2692
+ fetchPage,
2693
+ sort,
2694
+ type,
2695
+ limit,
2696
+ initialCursor: after,
2697
+ autoRefresh: live,
2698
+ intervalSeconds
2699
+ }
2700
+ )
2701
+ );
2702
+ track("cli_explore", {
2703
+ sort,
2704
+ type,
2705
+ limit,
2706
+ live,
2707
+ interval: intervalSeconds,
2708
+ paginated: after !== void 0,
2709
+ output_format: "live"
2710
+ });
2711
+ } else {
2712
+ const { coins } = await fetchPage(after).catch(
2713
+ (err) => outputErrorAndExit(
2714
+ false,
2715
+ `Request failed: ${err instanceof Error ? err.message : String(err)}`
2716
+ )
2717
+ );
2718
+ const title = type !== "all" ? `${SORT_LABELS2[sort]} \xB7 ${TYPE_LABELS[type]}` : SORT_LABELS2[sort];
2719
+ const rankedCoins = coins.map((c, i) => ({
2720
+ ...c,
2721
+ rank: i + 1
2722
+ }));
2723
+ renderOnce(
2724
+ /* @__PURE__ */ jsx5(Table, { columns: STATIC_COLUMNS, data: rankedCoins, title })
2725
+ );
2726
+ track("cli_explore", {
2727
+ sort,
2728
+ type,
2729
+ limit,
2730
+ live: false,
2731
+ paginated: after !== void 0,
2732
+ result_count: coins.length,
2733
+ output_format: "static"
2734
+ });
2735
+ }
2336
2736
  }
2337
2737
  });
2338
2738
 
@@ -2340,94 +2740,6 @@ var exploreCommand = new Command4("explore").description("Browse top, new, and h
2340
2740
  import { Command as Command5 } from "commander";
2341
2741
  import { setApiKey as setApiKey4 } from "@zoralabs/coins-sdk";
2342
2742
 
2343
- // src/lib/coin-ref.ts
2344
- import { getCoin as getCoin2, getProfile, getTrend } from "@zoralabs/coins-sdk";
2345
- var COIN_TYPE_MAP = {
2346
- CONTENT: "post",
2347
- CREATOR: "creator-coin",
2348
- TREND: "trend"
2349
- };
2350
- function mapCoinType(raw) {
2351
- if (!raw) return "unknown";
2352
- return COIN_TYPE_MAP[raw] ?? "unknown";
2353
- }
2354
- function coinFromToken(token) {
2355
- return {
2356
- name: token.name ?? "Unknown",
2357
- address: token.address ?? "",
2358
- coinType: mapCoinType(token.coinType),
2359
- marketCap: token.marketCap ?? "0",
2360
- marketCapDelta24h: token.marketCapDelta24h ?? "0",
2361
- volume24h: token.volume24h ?? "0",
2362
- uniqueHolders: token.uniqueHolders ?? 0,
2363
- createdAt: token.createdAt,
2364
- creatorAddress: token.creatorAddress,
2365
- creatorHandle: token.creatorProfile?.handle
2366
- };
2367
- }
2368
- function parseCoinRef(identifier, type) {
2369
- if (identifier.startsWith("0x")) {
2370
- return { kind: "address", address: identifier };
2371
- }
2372
- if (type === "creator-coin") {
2373
- return { kind: "prefixed", type: "creator-coin", name: identifier };
2374
- }
2375
- if (type === "trend") {
2376
- return { kind: "prefixed", type: "trend", name: identifier };
2377
- }
2378
- return { kind: "ambiguous", name: identifier };
2379
- }
2380
- async function resolveByAddress(address) {
2381
- const response = await getCoin2({ address });
2382
- if (response.error || !response.data?.zora20Token) {
2383
- return {
2384
- kind: "not-found",
2385
- message: `No coin found at address ${address}`
2386
- };
2387
- }
2388
- return { kind: "found", coin: coinFromToken(response.data.zora20Token) };
2389
- }
2390
- async function resolveByTrendTicker(ticker) {
2391
- const response = await getTrend({ ticker });
2392
- if (response.error || !response.data?.trendCoin) {
2393
- return {
2394
- kind: "not-found",
2395
- message: `No trend coin found with ticker "${ticker}"`
2396
- };
2397
- }
2398
- return { kind: "found", coin: coinFromToken(response.data.trendCoin) };
2399
- }
2400
- async function resolveByCreatorName(name) {
2401
- const response = await getProfile({ identifier: name });
2402
- if (response.error || !response.data?.profile) {
2403
- return {
2404
- kind: "not-found",
2405
- message: `No creator found with name "${name}"`
2406
- };
2407
- }
2408
- const profile = response.data.profile;
2409
- if (!profile.creatorCoin) {
2410
- return {
2411
- kind: "not-found",
2412
- message: `"${name}" does not have a creator coin`
2413
- };
2414
- }
2415
- return resolveByAddress(profile.creatorCoin.address);
2416
- }
2417
- async function resolveCoin(ref) {
2418
- switch (ref.kind) {
2419
- case "address":
2420
- return resolveByAddress(ref.address);
2421
- case "prefixed":
2422
- if (ref.type === "trend") {
2423
- return resolveByTrendTicker(ref.name);
2424
- }
2425
- return resolveByCreatorName(ref.name);
2426
- case "ambiguous":
2427
- return resolveByCreatorName(ref.name);
2428
- }
2429
- }
2430
-
2431
2743
  // src/components/CoinDetail.tsx
2432
2744
  import { Box as Box5, Text as Text5 } from "ink";
2433
2745
  import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
@@ -2482,62 +2794,108 @@ function formatCoinJson(coin) {
2482
2794
  creatorHandle: coin.creatorHandle ?? null
2483
2795
  };
2484
2796
  }
2485
- var VALID_TYPES = ["creator-coin", "post", "trend"];
2486
- var getCommand = new Command5("get").description("Look up a coin by address or name").argument("<identifier>", "Coin address (0x...) or creator name").option("--type <type>", "Coin type: creator-coin, post, trend").action(async function(identifier, opts) {
2797
+ function outputCoin(json, coin) {
2798
+ outputData(json, {
2799
+ json: formatCoinJson(coin),
2800
+ render: () => {
2801
+ renderOnce(/* @__PURE__ */ jsx7(CoinDetail, { coin }));
2802
+ }
2803
+ });
2804
+ }
2805
+ var getCommand = new Command5("get").description("Look up a coin by address or name").argument("[typeOrId]", "Type prefix (creator-coin, trend) or identifier").argument(
2806
+ "[identifier]",
2807
+ "Coin address (0x...) or name (when type prefix is given)"
2808
+ ).action(async function(typeOrId, identifier) {
2487
2809
  const json = getJson(this);
2488
- if (opts.type !== void 0 && !VALID_TYPES.includes(opts.type)) {
2489
- outputErrorAndExit(
2490
- json,
2491
- `Invalid --type value: ${opts.type}.`,
2492
- `Supported: ${VALID_TYPES.join(", ")}`
2493
- );
2810
+ let parsed;
2811
+ try {
2812
+ parsed = parsePositionalCoinArgs(typeOrId, identifier);
2813
+ } catch (err) {
2814
+ if (err instanceof CoinArgError) {
2815
+ outputErrorAndExit(json, err.message, err.suggestion);
2816
+ }
2817
+ throw err;
2494
2818
  }
2495
- const type = opts.type;
2496
- if (type === "post" && !identifier.startsWith("0x")) {
2497
- outputErrorAndExit(
2498
- json,
2499
- "Posts can only be looked up by address.",
2500
- "Use: zora get 0x..."
2501
- );
2502
- }
2503
- const ref = parseCoinRef(identifier, opts.type);
2504
2819
  const apiKey = getApiKey();
2505
2820
  if (apiKey) {
2506
2821
  setApiKey4(apiKey);
2507
2822
  }
2823
+ if (parsed.kind === "ambiguous-name") {
2824
+ let ambResult;
2825
+ try {
2826
+ ambResult = await resolveAmbiguousName(parsed.name);
2827
+ } catch (err) {
2828
+ outputErrorAndExit(
2829
+ json,
2830
+ `Request failed: ${err instanceof Error ? err.message : String(err)}`
2831
+ );
2832
+ return;
2833
+ }
2834
+ if (ambResult.kind === "not-found") {
2835
+ outputErrorAndExit(json, ambResult.message);
2836
+ return;
2837
+ }
2838
+ if (ambResult.kind === "ambiguous") {
2839
+ if (json) {
2840
+ outputData(json, {
2841
+ json: {
2842
+ matches: [
2843
+ { type: "creator-coin", ...formatCoinJson(ambResult.creator) },
2844
+ { type: "trend", ...formatCoinJson(ambResult.trend) }
2845
+ ],
2846
+ hint: `Use: zora get creator-coin ${parsed.name} or zora get trend ${parsed.name}`
2847
+ },
2848
+ render: () => {
2849
+ }
2850
+ });
2851
+ } else {
2852
+ outputCoin(false, ambResult.creator);
2853
+ console.log("");
2854
+ outputCoin(false, ambResult.trend);
2855
+ console.log(
2856
+ `
2857
+ \x1B[2mUse \`zora get creator-coin ${parsed.name}\` or \`zora get trend ${parsed.name}\` for a specific type.\x1B[0m`
2858
+ );
2859
+ }
2860
+ track("cli_get", {
2861
+ lookup_type: "name",
2862
+ found: true,
2863
+ ambiguous: true,
2864
+ output_format: json ? "json" : "text"
2865
+ });
2866
+ return;
2867
+ }
2868
+ outputCoin(json, ambResult.coin);
2869
+ track("cli_get", {
2870
+ lookup_type: "name",
2871
+ found: true,
2872
+ coin_type: ambResult.coin.coinType,
2873
+ output_format: json ? "json" : "text"
2874
+ });
2875
+ return;
2876
+ }
2877
+ const ref = coinArgsToRef(parsed);
2508
2878
  let result;
2509
2879
  try {
2510
2880
  result = await resolveCoin(ref);
2511
2881
  } catch (err) {
2512
- outputErrorAndExit(
2513
- json,
2514
- `Request failed: ${err instanceof Error ? err.message : String(err)}`
2515
- );
2516
- return;
2517
- }
2518
- if (type && result.kind === "found" && result.coin.coinType !== type) {
2519
- outputErrorAndExit(
2520
- json,
2521
- `Coin at ${result.coin.address} is a ${result.coin.coinType}, not a ${type}.`,
2522
- `Use: zora get ${result.coin.address} --type ${result.coin.coinType}`
2523
- );
2882
+ outputErrorAndExit(json, `Request failed: ${apiErrorMessage(err)}`);
2524
2883
  return;
2525
2884
  }
2526
2885
  if (result.kind === "not-found") {
2527
2886
  outputErrorAndExit(json, result.message);
2528
2887
  return;
2529
2888
  }
2530
- outputData(json, {
2531
- json: formatCoinJson(result.coin),
2532
- table: () => {
2533
- renderOnce(/* @__PURE__ */ jsx7(CoinDetail, { coin: result.coin }));
2534
- }
2535
- });
2889
+ if (result.coin.platformBlocked) {
2890
+ outputErrorAndExit(json, bannedCoinMessage(result.coin.address));
2891
+ return;
2892
+ }
2893
+ outputCoin(json, result.coin);
2536
2894
  track("cli_get", {
2537
- lookup_type: identifier.startsWith("0x") ? "address" : "name",
2538
- coin_type_filter: type ?? null,
2539
- found: result.kind === "found",
2540
- coin_type: result.kind === "found" ? result.coin.coinType : null,
2895
+ lookup_type: typeOrId.startsWith("0x") ? "address" : "name",
2896
+ coin_type_filter: parsed.kind === "typed" ? parsed.type : null,
2897
+ found: true,
2898
+ coin_type: result.coin.coinType,
2541
2899
  output_format: json ? "json" : "text"
2542
2900
  });
2543
2901
  });
@@ -2610,7 +2968,6 @@ var downsample = (values, maxWidth) => {
2610
2968
 
2611
2969
  // src/commands/price-history.tsx
2612
2970
  import { jsx as jsx9 } from "react/jsx-runtime";
2613
- var VALID_TYPES2 = ["creator-coin", "post", "trend"];
2614
2971
  var VALID_INTERVALS = ["1h", "24h", "1w", "1m", "ALL"];
2615
2972
  var INTERVAL_TO_API_FIELD = {
2616
2973
  "1h": "oneHour",
@@ -2651,54 +3008,7 @@ var fetchPriceHistory = async (address, interval) => {
2651
3008
  price: Number(p.closePrice)
2652
3009
  }));
2653
3010
  };
2654
- var priceHistoryCommand = new Command6("price-history").description("Display price history for a coin").argument("<identifier>", "Coin address (0x...) or name").option("--type <type>", "Coin type: creator-coin, post, trend").option(
2655
- "--interval <interval>",
2656
- `Time range: ${VALID_INTERVALS.join(", ")}`,
2657
- "1w"
2658
- ).action(async function(identifier, opts) {
2659
- const json = getJson(this);
2660
- const interval = opts.interval ?? "1w";
2661
- if (!VALID_INTERVALS.includes(interval)) {
2662
- outputErrorAndExit(
2663
- json,
2664
- `Invalid --interval value: ${interval}.`,
2665
- `Supported: ${VALID_INTERVALS.join(", ")}`
2666
- );
2667
- }
2668
- if (opts.type !== void 0 && !VALID_TYPES2.includes(opts.type)) {
2669
- outputErrorAndExit(
2670
- json,
2671
- `Invalid --type value: ${opts.type}.`,
2672
- `Supported: ${VALID_TYPES2.join(", ")}`
2673
- );
2674
- }
2675
- if (opts.type === "post" && !identifier.startsWith("0x")) {
2676
- outputErrorAndExit(
2677
- json,
2678
- "Posts can only be looked up by address.",
2679
- "Use: zora price-history 0x..."
2680
- );
2681
- }
2682
- const ref = parseCoinRef(identifier, opts.type);
2683
- const apiKey = getApiKey();
2684
- if (apiKey) {
2685
- setApiKey5(apiKey);
2686
- }
2687
- let result;
2688
- try {
2689
- result = await resolveCoin(ref);
2690
- } catch (err) {
2691
- outputErrorAndExit(
2692
- json,
2693
- `Request failed: ${err instanceof Error ? err.message : String(err)}`
2694
- );
2695
- return;
2696
- }
2697
- if (result.kind === "not-found") {
2698
- outputErrorAndExit(json, result.message, result.suggestion);
2699
- return;
2700
- }
2701
- const { coin } = result;
3011
+ async function showPriceHistory(json, coin, interval) {
2702
3012
  let prices;
2703
3013
  try {
2704
3014
  prices = await fetchPriceHistory(coin.address, interval);
@@ -2724,9 +3034,7 @@ var priceHistoryCommand = new Command6("price-history").description("Display pri
2724
3034
  priceValues[0],
2725
3035
  priceValues[priceValues.length - 1]
2726
3036
  );
2727
- const sparklineText = sparkline(
2728
- downsample(priceValues, MAX_SPARKLINE_WIDTH)
2729
- );
3037
+ const sparklineText = sparkline(downsample(priceValues, MAX_SPARKLINE_WIDTH));
2730
3038
  outputData(json, {
2731
3039
  json: {
2732
3040
  coin: coin.name,
@@ -2740,7 +3048,7 @@ var priceHistoryCommand = new Command6("price-history").description("Display pri
2740
3048
  price: p.price
2741
3049
  }))
2742
3050
  },
2743
- table: () => {
3051
+ render: () => {
2744
3052
  renderOnce(
2745
3053
  /* @__PURE__ */ jsx9(
2746
3054
  PriceHistory,
@@ -2757,11 +3065,139 @@ var priceHistoryCommand = new Command6("price-history").description("Display pri
2757
3065
  );
2758
3066
  }
2759
3067
  });
3068
+ return prices.length;
3069
+ }
3070
+ var priceHistoryCommand = new Command6("price-history").description("Display price history for a coin").argument("[typeOrId]", "Type prefix (creator-coin, trend) or identifier").argument(
3071
+ "[identifier]",
3072
+ "Coin address (0x...) or name (when type prefix is given)"
3073
+ ).option(
3074
+ "--interval <interval>",
3075
+ `Time range: ${VALID_INTERVALS.join(", ")}`,
3076
+ "1w"
3077
+ ).action(async function(typeOrId, identifier, opts) {
3078
+ const json = getJson(this);
3079
+ const interval = opts.interval ?? "1w";
3080
+ if (!VALID_INTERVALS.includes(interval)) {
3081
+ outputErrorAndExit(
3082
+ json,
3083
+ `Invalid --interval value: ${interval}.`,
3084
+ `Supported: ${VALID_INTERVALS.join(", ")}`
3085
+ );
3086
+ }
3087
+ let parsed;
3088
+ try {
3089
+ parsed = parsePositionalCoinArgs(typeOrId, identifier);
3090
+ } catch (err) {
3091
+ if (err instanceof CoinArgError) {
3092
+ outputErrorAndExit(json, err.message, err.suggestion);
3093
+ }
3094
+ throw err;
3095
+ }
3096
+ const apiKey = getApiKey();
3097
+ if (apiKey) {
3098
+ setApiKey5(apiKey);
3099
+ }
3100
+ if (parsed.kind === "ambiguous-name") {
3101
+ let ambResult;
3102
+ try {
3103
+ ambResult = await resolveAmbiguousName(parsed.name);
3104
+ } catch (err) {
3105
+ outputErrorAndExit(
3106
+ json,
3107
+ `Request failed: ${err instanceof Error ? err.message : String(err)}`
3108
+ );
3109
+ return;
3110
+ }
3111
+ if (ambResult.kind === "not-found") {
3112
+ outputErrorAndExit(json, ambResult.message);
3113
+ return;
3114
+ }
3115
+ if (ambResult.kind === "ambiguous") {
3116
+ if (json) {
3117
+ const [creatorPrices, trendPrices] = await Promise.all([
3118
+ fetchPriceHistory(ambResult.creator.address, interval),
3119
+ fetchPriceHistory(ambResult.trend.address, interval)
3120
+ ]);
3121
+ outputData(json, {
3122
+ json: {
3123
+ matches: [
3124
+ {
3125
+ type: "creator-coin",
3126
+ coin: ambResult.creator.name,
3127
+ prices: creatorPrices
3128
+ },
3129
+ {
3130
+ type: "trend",
3131
+ coin: ambResult.trend.name,
3132
+ prices: trendPrices
3133
+ }
3134
+ ],
3135
+ hint: `Use: zora price-history creator-coin ${parsed.name} or zora price-history trend ${parsed.name}`
3136
+ },
3137
+ render: () => {
3138
+ }
3139
+ });
3140
+ } else {
3141
+ await showPriceHistory(
3142
+ false,
3143
+ ambResult.creator,
3144
+ interval
3145
+ );
3146
+ console.log("");
3147
+ await showPriceHistory(false, ambResult.trend, interval);
3148
+ console.log(
3149
+ `
3150
+ \x1B[2mUse \`zora price-history creator-coin ${parsed.name}\` or \`zora price-history trend ${parsed.name}\` for a specific type.\x1B[0m`
3151
+ );
3152
+ }
3153
+ track("cli_price_history", {
3154
+ lookup_type: "name",
3155
+ ambiguous: true,
3156
+ interval,
3157
+ output_format: json ? "json" : "text"
3158
+ });
3159
+ return;
3160
+ }
3161
+ const dataPoints2 = await showPriceHistory(
3162
+ json,
3163
+ ambResult.coin,
3164
+ interval
3165
+ );
3166
+ track("cli_price_history", {
3167
+ lookup_type: "name",
3168
+ coin_type: ambResult.coin.coinType,
3169
+ interval,
3170
+ data_points: dataPoints2,
3171
+ output_format: json ? "json" : "text"
3172
+ });
3173
+ return;
3174
+ }
3175
+ const ref = coinArgsToRef(parsed);
3176
+ let result;
3177
+ try {
3178
+ result = await resolveCoin(ref);
3179
+ } catch (err) {
3180
+ outputErrorAndExit(
3181
+ json,
3182
+ `Request failed: ${err instanceof Error ? err.message : String(err)}`
3183
+ );
3184
+ return;
3185
+ }
3186
+ if (result.kind === "not-found") {
3187
+ outputErrorAndExit(json, result.message, result.suggestion);
3188
+ return;
3189
+ }
3190
+ if (result.coin.platformBlocked) {
3191
+ outputErrorAndExit(json, bannedCoinMessage(result.coin.address));
3192
+ return;
3193
+ }
3194
+ const { coin } = result;
3195
+ const dataPoints = await showPriceHistory(json, coin, interval);
2760
3196
  track("cli_price_history", {
2761
- lookup_type: identifier.startsWith("0x") ? "address" : "name",
3197
+ lookup_type: typeOrId.startsWith("0x") ? "address" : "name",
2762
3198
  coin_type: coin.coinType,
2763
3199
  interval,
2764
- data_points: prices.length,
3200
+ data_points: dataPoints,
2765
3201
  output_format: json ? "json" : "text"
2766
3202
  });
2767
3203
  });
@@ -2771,7 +3207,7 @@ import { Command as Command7 } from "commander";
2771
3207
  import confirm3 from "@inquirer/confirm";
2772
3208
  import {
2773
3209
  erc20Abi as erc20Abi3,
2774
- formatUnits as formatUnits4,
3210
+ formatUnits as formatUnits5,
2775
3211
  isAddress as isAddress2,
2776
3212
  parseUnits as parseUnits2
2777
3213
  } from "viem";
@@ -2788,12 +3224,12 @@ function printSellQuote(output, info) {
2788
3224
  coin: info.coinSymbol,
2789
3225
  address: info.address,
2790
3226
  sell: {
2791
- amount: formatUnits4(info.amountIn, info.coinDecimals),
3227
+ amount: formatUnits5(info.amountIn, info.coinDecimals),
2792
3228
  raw: info.amountIn.toString(),
2793
3229
  symbol: info.coinSymbol
2794
3230
  },
2795
3231
  estimated: {
2796
- amount: formatUnits4(BigInt(info.quoteAmountOut), info.outputDecimals),
3232
+ amount: formatUnits5(BigInt(info.quoteAmountOut), info.outputDecimals),
2797
3233
  raw: info.quoteAmountOut,
2798
3234
  symbol: info.outputSymbol
2799
3235
  },
@@ -2802,7 +3238,8 @@ function printSellQuote(output, info) {
2802
3238
  return;
2803
3239
  }
2804
3240
  console.log(`
2805
- Sell ${info.coinName} (${info.coinSymbol})
3241
+ Sell \x1B[1m${info.coinName}\x1B[0m`);
3242
+ console.log(` ${info.coinType} \xB7 ${info.address}
2806
3243
  `);
2807
3244
  console.log(` Amount ${info.soldFormatted} ${info.coinSymbol}`);
2808
3245
  console.log(
@@ -2812,7 +3249,7 @@ function printSellQuote(output, info) {
2812
3249
  `);
2813
3250
  }
2814
3251
  function printSellResult(output, info) {
2815
- const receivedAmount = formatUnits4(
3252
+ const receivedAmount = formatUnits5(
2816
3253
  info.receivedAmountOut,
2817
3254
  info.outputDecimals
2818
3255
  );
@@ -2826,7 +3263,7 @@ function printSellResult(output, info) {
2826
3263
  coin: info.coinSymbol,
2827
3264
  address: info.address,
2828
3265
  sold: {
2829
- amount: formatUnits4(info.amountIn, info.coinDecimals),
3266
+ amount: formatUnits5(info.amountIn, info.coinDecimals),
2830
3267
  raw: info.amountIn.toString(),
2831
3268
  symbol: info.coinSymbol
2832
3269
  },
@@ -2841,7 +3278,8 @@ function printSellResult(output, info) {
2841
3278
  return;
2842
3279
  }
2843
3280
  console.log(`
2844
- Sold ${info.coinName}
3281
+ Sold \x1B[1m${info.coinName}\x1B[0m`);
3282
+ console.log(` ${info.coinType} \xB7 ${info.address}
2845
3283
  `);
2846
3284
  console.log(` Sold ${info.soldFormatted} ${info.coinSymbol}`);
2847
3285
  console.log(
@@ -2853,13 +3291,76 @@ function printSellResult(output, info) {
2853
3291
  console.log(` Tx ${info.txHash}
2854
3292
  `);
2855
3293
  }
2856
- var sellCommand = new Command7("sell").description("Sell a coin").argument("<address>", "Coin contract address (0x\u2026)").option("--amount <value>", "Sell specific number of coins").option("--usd <value>", "Sell USD equivalent worth of coins").option("--percent <value>", "Sell percentage of coin balance").option("--all", "Sell entire coin balance").option("--to <asset>", "Receive asset: eth, usdc, zora", "eth").option("--token <asset>", "Receive asset: eth, usdc, zora (alias for --to)").option("--quote", "Print quote and exit without trading").option("--yes", "Skip confirmation and execute directly").option("--slippage <pct>", "Slippage tolerance percent", "1").option("--debug", "Print full quote request/response JSON").action(async function(coinAddress, opts) {
3294
+ var sellCommand = new Command7("sell").description("Sell a coin").argument(
3295
+ "[typeOrId]",
3296
+ "Type prefix (creator-coin, trend) or coin address/name"
3297
+ ).argument("[identifier]", "Coin name (when type prefix is given)").option("--amount <value>", "Sell specific number of coins").option("--usd <value>", "Sell USD equivalent worth of coins").option("--percent <value>", "Sell percentage of coin balance").option("--all", "Sell entire coin balance").option("--to <asset>", "Receive asset: eth, usdc, zora", "eth").option("--token <asset>", "Receive asset: eth, usdc, zora (alias for --to)").option("--quote", "Print quote and exit without trading").option("--yes", "Skip confirmation and execute directly").option("--slippage <pct>", "Slippage tolerance percent", "1").option("--debug", "Print full quote request/response JSON").action(async function(typeOrId, identifier, opts) {
2857
3298
  const json = getJson(this);
2858
3299
  const debug = opts.debug === true;
2859
- if (!isAddress2(coinAddress)) {
2860
- outputErrorAndExit(json, `Invalid address: ${coinAddress}`);
3300
+ let parsed;
3301
+ try {
3302
+ parsed = parsePositionalCoinArgs(typeOrId, identifier);
3303
+ } catch (err) {
3304
+ if (err instanceof CoinArgError) {
3305
+ outputErrorAndExit(json, err.message, err.suggestion);
3306
+ }
3307
+ throw err;
3308
+ }
3309
+ const apiKey = getApiKey();
3310
+ if (apiKey) {
3311
+ setApiKey6(apiKey);
2861
3312
  }
2862
- const output = json ? "json" : "table";
3313
+ let coinAddress;
3314
+ if (parsed.kind === "address") {
3315
+ if (!isAddress2(parsed.address)) {
3316
+ outputErrorAndExit(json, `Invalid address: ${parsed.address}`);
3317
+ return;
3318
+ }
3319
+ coinAddress = parsed.address;
3320
+ } else if (parsed.kind === "ambiguous-name") {
3321
+ let ambResult;
3322
+ try {
3323
+ ambResult = await resolveAmbiguousName(parsed.name);
3324
+ } catch (err) {
3325
+ outputErrorAndExit(
3326
+ json,
3327
+ `Request failed: ${err instanceof Error ? err.message : String(err)}`
3328
+ );
3329
+ return;
3330
+ }
3331
+ if (ambResult.kind === "not-found") {
3332
+ outputErrorAndExit(json, ambResult.message);
3333
+ return;
3334
+ }
3335
+ if (ambResult.kind === "ambiguous") {
3336
+ const { message, suggestion } = formatAmbiguousError(
3337
+ parsed.name,
3338
+ ambResult.creator,
3339
+ ambResult.trend,
3340
+ "sell"
3341
+ );
3342
+ outputErrorAndExit(json, message, suggestion);
3343
+ return;
3344
+ }
3345
+ coinAddress = ambResult.coin.address;
3346
+ } else {
3347
+ const ref = coinArgsToRef(parsed);
3348
+ try {
3349
+ const result = await resolveCoin(ref);
3350
+ if (result.kind === "not-found") {
3351
+ outputErrorAndExit(json, result.message, result.suggestion);
3352
+ return;
3353
+ }
3354
+ coinAddress = result.coin.address;
3355
+ } catch (err) {
3356
+ outputErrorAndExit(
3357
+ json,
3358
+ `Request failed: ${err instanceof Error ? err.message : String(err)}`
3359
+ );
3360
+ return;
3361
+ }
3362
+ }
3363
+ const output = json ? "json" : "static";
2863
3364
  const outputAsset = opts.token ? opts.token.toLowerCase() : opts.to;
2864
3365
  if (!(outputAsset in BASE_TRADE_TOKENS)) {
2865
3366
  outputErrorAndExit(
@@ -2882,10 +3383,6 @@ var sellCommand = new Command7("sell").description("Sell a coin").argument("<add
2882
3383
  );
2883
3384
  }
2884
3385
  const slippage = slippagePct / 100;
2885
- const apiKey = getApiKey();
2886
- if (apiKey) {
2887
- setApiKey6(apiKey);
2888
- }
2889
3386
  const account = resolveAccount(json);
2890
3387
  const { publicClient, walletClient } = createClients(account);
2891
3388
  let token;
@@ -2893,16 +3390,14 @@ var sellCommand = new Command7("sell").description("Sell a coin").argument("<add
2893
3390
  const response = await getCoin3({ address: coinAddress });
2894
3391
  token = response.data?.zora20Token;
2895
3392
  } catch (err) {
2896
- outputErrorAndExit(
2897
- json,
2898
- `Failed to fetch coin: ${err instanceof Error ? err.message : String(err)}`
2899
- );
3393
+ outputErrorAndExit(json, `Failed to fetch coin: ${apiErrorMessage(err)}`);
2900
3394
  }
2901
3395
  if (!token) {
2902
3396
  outputErrorAndExit(json, `Coin not found: ${coinAddress}`);
2903
3397
  }
2904
3398
  const coinName = token.name;
2905
3399
  const coinSymbol = token.symbol;
3400
+ const coinType = mapCoinType(token.coinType);
2906
3401
  const coinDecimals = Number(token.decimals ?? 18);
2907
3402
  let amountIn;
2908
3403
  if (amountMode === "usd") {
@@ -2929,7 +3424,7 @@ var sellCommand = new Command7("sell").description("Sell a coin").argument("<add
2929
3424
  }
2930
3425
  if (debug) {
2931
3426
  console.error(
2932
- `[debug] $${usdVal} USD = ${formatUnits4(amountIn, coinDecimals)} ${coinSymbol} (coin price: $${coinPriceUsd})`
3427
+ `[debug] $${usdVal} USD = ${formatUnits5(amountIn, coinDecimals)} ${coinSymbol} (coin price: $${coinPriceUsd})`
2933
3428
  );
2934
3429
  }
2935
3430
  } else if (amountMode === "amount") {
@@ -2984,7 +3479,7 @@ var sellCommand = new Command7("sell").description("Sell a coin").argument("<add
2984
3479
  const coinPriceUsd = await fetchTokenPriceUsd(coinAddress);
2985
3480
  if (coinPriceUsd !== null && coinPriceUsd > 0) {
2986
3481
  swapAmountUsd = Number(
2987
- (Number(formatUnits4(amountIn, coinDecimals)) * coinPriceUsd).toFixed(
3482
+ (Number(formatUnits5(amountIn, coinDecimals)) * coinPriceUsd).toFixed(
2988
3483
  2
2989
3484
  )
2990
3485
  );
@@ -3037,7 +3532,7 @@ ${err instanceof Error ? err.stack || err.message : String(err)}
3037
3532
  }
3038
3533
  outputErrorAndExit(
3039
3534
  json,
3040
- `Quote failed: ${msg}`,
3535
+ `Quote failed: ${apiErrorMessage(err)}`,
3041
3536
  "Check the coin address and amount, then try again. Use --debug for full error details."
3042
3537
  );
3043
3538
  }
@@ -3050,6 +3545,7 @@ ${err instanceof Error ? err.stack || err.message : String(err)}
3050
3545
  printSellQuote(output, {
3051
3546
  coinName,
3052
3547
  coinSymbol,
3548
+ coinType,
3053
3549
  address: coinAddress,
3054
3550
  soldFormatted,
3055
3551
  amountIn,
@@ -3076,9 +3572,10 @@ ${err instanceof Error ? err.stack || err.message : String(err)}
3076
3572
  return;
3077
3573
  }
3078
3574
  if (!opts.yes) {
3079
- printSellQuote("table", {
3575
+ printSellQuote("static", {
3080
3576
  coinName,
3081
3577
  coinSymbol,
3578
+ coinType,
3082
3579
  address: coinAddress,
3083
3580
  soldFormatted,
3084
3581
  amountIn,
@@ -3098,103 +3595,497 @@ ${err instanceof Error ? err.stack || err.message : String(err)}
3098
3595
  process.exit(0);
3099
3596
  }
3100
3597
  }
3101
- let receipt;
3102
- let txHash;
3103
- let receivedAmountOut = BigInt(quoteAmountOut);
3104
- let receivedSource = "quote";
3105
- let swapLogIndex = null;
3106
- const swapCoinType = token.coinType ?? null;
3107
- try {
3108
- receipt = await tradeCoin2({
3109
- tradeParameters,
3110
- walletClient,
3111
- publicClient,
3112
- account
3113
- });
3114
- } catch (err) {
3115
- track("cli_sell", {
3116
- action: "trade",
3117
- coin_address: coinAddress,
3118
- coin_name: coinName,
3119
- coin_symbol: coinSymbol,
3120
- amount_mode: amountMode,
3121
- swap_amount_usd: swapAmountUsd,
3122
- valueUsd: swapAmountUsd,
3123
- swapCoinType,
3124
- output_asset: outputAsset,
3125
- slippage: slippagePct,
3126
- output_format: output,
3127
- success: false,
3128
- error_type: err instanceof Error ? err.constructor.name : "unknown"
3129
- });
3130
- await shutdownAnalytics();
3598
+ let receipt;
3599
+ let txHash;
3600
+ let receivedAmountOut = BigInt(quoteAmountOut);
3601
+ let receivedSource = "quote";
3602
+ let swapLogIndex = null;
3603
+ const swapCoinType = token.coinType ?? null;
3604
+ try {
3605
+ receipt = await tradeCoin2({
3606
+ tradeParameters,
3607
+ walletClient,
3608
+ publicClient,
3609
+ account
3610
+ });
3611
+ } catch (err) {
3612
+ track("cli_sell", {
3613
+ action: "trade",
3614
+ coin_address: coinAddress,
3615
+ coin_name: coinName,
3616
+ coin_symbol: coinSymbol,
3617
+ amount_mode: amountMode,
3618
+ swap_amount_usd: swapAmountUsd,
3619
+ valueUsd: swapAmountUsd,
3620
+ swapCoinType,
3621
+ output_asset: outputAsset,
3622
+ slippage: slippagePct,
3623
+ output_format: output,
3624
+ success: false,
3625
+ error_type: err instanceof Error ? err.constructor.name : "unknown"
3626
+ });
3627
+ await shutdownAnalytics();
3628
+ outputErrorAndExit(json, tradeErrorMessage(err));
3629
+ }
3630
+ txHash = receipt.transactionHash;
3631
+ if (outputToken.trade.type === "erc20") {
3632
+ try {
3633
+ const result = getReceivedAmountFromReceipt({
3634
+ receipt,
3635
+ tokenAddress: outputToken.trade.address,
3636
+ recipient: account.address
3637
+ });
3638
+ receivedAmountOut = result.amount;
3639
+ swapLogIndex = result.logIndex;
3640
+ receivedSource = "receipt";
3641
+ } catch {
3642
+ }
3643
+ }
3644
+ printSellResult(output, {
3645
+ coinName,
3646
+ coinSymbol,
3647
+ coinType,
3648
+ address: coinAddress,
3649
+ amountIn,
3650
+ coinDecimals,
3651
+ soldFormatted,
3652
+ receivedAmountOut,
3653
+ outputSymbol: outputToken.symbol,
3654
+ outputDecimals: outputToken.decimals,
3655
+ receivedSource,
3656
+ txHash
3657
+ });
3658
+ track("cli_sell", {
3659
+ action: "trade",
3660
+ coin_address: coinAddress,
3661
+ coin_name: coinName,
3662
+ coin_symbol: coinSymbol,
3663
+ amount_mode: amountMode,
3664
+ swap_amount_usd: swapAmountUsd,
3665
+ valueUsd: swapAmountUsd,
3666
+ swapCoinType,
3667
+ transactionHash: txHash,
3668
+ logIndex: swapLogIndex,
3669
+ output_asset: outputAsset,
3670
+ slippage: slippagePct,
3671
+ output_format: output,
3672
+ success: true,
3673
+ tx_hash: txHash
3674
+ });
3675
+ });
3676
+
3677
+ // src/commands/profile.tsx
3678
+ import { Command as Command8 } from "commander";
3679
+ import { Box as Box8, Text as Text8 } from "ink";
3680
+ import {
3681
+ getProfileCoins,
3682
+ getProfileBalances as getProfileBalances2,
3683
+ setApiKey as setApiKey7
3684
+ } from "@zoralabs/coins-sdk";
3685
+ import { privateKeyToAccount as privateKeyToAccount3 } from "viem/accounts";
3686
+
3687
+ // src/components/ProfileView.tsx
3688
+ import { useState as useState4, useEffect as useEffect4, useCallback as useCallback4, useRef as useRef3 } from "react";
3689
+ import { Box as Box7, Text as Text7, useInput as useInput3, useApp as useApp3 } from "ink";
3690
+ import Spinner3 from "ink-spinner";
3691
+ import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
3692
+ var TAB_NAMES = ["Posts", "Holdings"];
3693
+ var postColumns = [
3694
+ { header: "#", width: 4, accessor: (c) => String(c.rank) },
3695
+ { header: "Name", width: 20, accessor: (c) => c.name ?? "Unknown" },
3696
+ {
3697
+ header: "Type",
3698
+ width: 14,
3699
+ accessor: (c) => COIN_TYPE_DISPLAY[c.coinType ?? ""] ?? c.coinType ?? ""
3700
+ },
3701
+ {
3702
+ header: "Market Cap",
3703
+ width: 12,
3704
+ accessor: (c) => formatCompactUsd(c.marketCap)
3705
+ },
3706
+ {
3707
+ header: "24h Vol",
3708
+ width: 12,
3709
+ accessor: (c) => formatCompactUsd(c.volume24h)
3710
+ },
3711
+ {
3712
+ header: "24h Change",
3713
+ width: 11,
3714
+ accessor: (c) => formatMcapChange(c.marketCap, c.marketCapDelta24h).text,
3715
+ color: (c) => formatMcapChange(c.marketCap, c.marketCapDelta24h).color
3716
+ },
3717
+ {
3718
+ header: "Created",
3719
+ width: 16,
3720
+ accessor: (c) => {
3721
+ if (!c.createdAt) return "-";
3722
+ const date = new Date(c.createdAt);
3723
+ if (isNaN(date.getTime())) return "-";
3724
+ return formatRelativeTime(date);
3725
+ }
3726
+ }
3727
+ ];
3728
+ var ProfileView = ({
3729
+ fetchData,
3730
+ identifier,
3731
+ autoRefresh = false,
3732
+ intervalSeconds = 30
3733
+ }) => {
3734
+ const { exit } = useApp3();
3735
+ const [activeTab, setActiveTab] = useState4(0);
3736
+ const [loading, setLoading] = useState4(true);
3737
+ const [isRefreshing, setIsRefreshing] = useState4(false);
3738
+ const [error, setError] = useState4(null);
3739
+ const [data, setData] = useState4(null);
3740
+ const { refreshCount, secondsUntilRefresh, triggerManualRefresh } = useAutoRefresh(intervalSeconds, autoRefresh);
3741
+ const [manualRefreshCount, setManualRefreshCount] = useState4(0);
3742
+ const hasLoadedOnce = useRef3(false);
3743
+ const load = useCallback4(async () => {
3744
+ if (hasLoadedOnce.current) {
3745
+ setIsRefreshing(true);
3746
+ } else {
3747
+ setLoading(true);
3748
+ }
3749
+ setError(null);
3750
+ try {
3751
+ const result = await fetchData();
3752
+ setData(result);
3753
+ hasLoadedOnce.current = true;
3754
+ } catch (err) {
3755
+ setError(err instanceof Error ? err.message : String(err));
3756
+ }
3757
+ setLoading(false);
3758
+ setIsRefreshing(false);
3759
+ }, [fetchData]);
3760
+ useEffect4(() => {
3761
+ load();
3762
+ }, [load, refreshCount, manualRefreshCount]);
3763
+ useInput3((input, key) => {
3764
+ if (input === "q" || key.escape) {
3765
+ exit();
3766
+ return;
3767
+ }
3768
+ if (input === "r" && !loading) {
3769
+ triggerManualRefresh();
3770
+ setManualRefreshCount((c) => c + 1);
3771
+ }
3772
+ if (key.leftArrow || input === "1") {
3773
+ setActiveTab(0);
3774
+ }
3775
+ if (key.rightArrow || input === "2") {
3776
+ setActiveTab(1);
3777
+ }
3778
+ });
3779
+ if (error && !data) {
3780
+ return /* @__PURE__ */ jsxs7(
3781
+ Box7,
3782
+ {
3783
+ flexDirection: "column",
3784
+ paddingLeft: 1,
3785
+ paddingTop: 1,
3786
+ paddingBottom: 1,
3787
+ children: [
3788
+ /* @__PURE__ */ jsxs7(Text7, { color: "red", children: [
3789
+ "Error: ",
3790
+ error
3791
+ ] }),
3792
+ /* @__PURE__ */ jsx10(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx10(Text7, { dimColor: true, children: "Press q to exit" }) })
3793
+ ]
3794
+ }
3795
+ );
3796
+ }
3797
+ if (loading && !data) {
3798
+ return /* @__PURE__ */ jsx10(Box7, { paddingLeft: 1, paddingTop: 1, children: /* @__PURE__ */ jsxs7(Text7, { children: [
3799
+ /* @__PURE__ */ jsx10(Spinner3, { type: "dots" }),
3800
+ " Loading profile\u2026"
3801
+ ] }) });
3802
+ }
3803
+ if (!data) return null;
3804
+ const hints = [
3805
+ "\u2190 \u2192 switch tab",
3806
+ autoRefresh ? `r refresh (${secondsUntilRefresh}s)` : "r refresh"
3807
+ ];
3808
+ hints.push("q quit");
3809
+ const footer = hints.join(" \xB7 ");
3810
+ const rankedPosts = data.posts.map((p, i) => ({ ...p, rank: i + 1 }));
3811
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
3812
+ isRefreshing && /* @__PURE__ */ jsx10(Box7, { paddingLeft: 1, children: /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
3813
+ /* @__PURE__ */ jsx10(Spinner3, { type: "dots" }),
3814
+ " Refreshing\u2026"
3815
+ ] }) }),
3816
+ /* @__PURE__ */ jsxs7(Box7, { paddingLeft: 1, paddingTop: 1, gap: 2, children: [
3817
+ TAB_NAMES.map((name, i) => /* @__PURE__ */ jsx10(Text7, { bold: activeTab === i, dimColor: activeTab !== i, children: activeTab === i ? `[${name}]` : name }, name)),
3818
+ /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
3819
+ " ",
3820
+ identifier
3821
+ ] })
3822
+ ] }),
3823
+ activeTab === 0 ? rankedPosts.length === 0 ? /* @__PURE__ */ jsx10(
3824
+ Box7,
3825
+ {
3826
+ flexDirection: "column",
3827
+ paddingLeft: 1,
3828
+ paddingTop: 1,
3829
+ paddingBottom: 1,
3830
+ children: /* @__PURE__ */ jsx10(Text7, { children: "No posts found for this profile." })
3831
+ }
3832
+ ) : /* @__PURE__ */ jsx10(
3833
+ Table,
3834
+ {
3835
+ columns: postColumns,
3836
+ data: rankedPosts,
3837
+ title: "Posts",
3838
+ subtitle: `${rankedPosts.length} of ${data.postsCount}`
3839
+ }
3840
+ ) : data.holdings.length === 0 ? /* @__PURE__ */ jsx10(
3841
+ Box7,
3842
+ {
3843
+ flexDirection: "column",
3844
+ paddingLeft: 1,
3845
+ paddingTop: 1,
3846
+ paddingBottom: 1,
3847
+ children: /* @__PURE__ */ jsx10(Text7, { children: "No holdings found for this profile." })
3848
+ }
3849
+ ) : /* @__PURE__ */ jsx10(
3850
+ Table,
3851
+ {
3852
+ columns: balanceColumns,
3853
+ data: data.holdings,
3854
+ title: "Holdings",
3855
+ subtitle: `${data.holdings.length} of ${data.holdingsCount}`
3856
+ }
3857
+ ),
3858
+ /* @__PURE__ */ jsx10(Box7, { paddingLeft: 1, paddingBottom: 1, children: /* @__PURE__ */ jsx10(Text7, { dimColor: true, children: footer }) })
3859
+ ] });
3860
+ };
3861
+
3862
+ // src/commands/profile.tsx
3863
+ import { jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
3864
+ var extractErrorMessage2 = (error) => {
3865
+ if (typeof error === "object" && error !== null && "error" in error) {
3866
+ return String(error.error);
3867
+ }
3868
+ return JSON.stringify(error);
3869
+ };
3870
+ var resolveApiKey = (json) => {
3871
+ const apiKey = getApiKey();
3872
+ if (!apiKey) {
3131
3873
  outputErrorAndExit(
3132
3874
  json,
3133
- `Transaction failed: ${err instanceof Error ? err.message : String(err)}`
3875
+ "Not authenticated. Run 'zora auth configure' to set your API key."
3134
3876
  );
3135
3877
  }
3136
- txHash = receipt.transactionHash;
3137
- if (outputToken.trade.type === "erc20") {
3878
+ setApiKey7(apiKey);
3879
+ };
3880
+ var formatPostJson = (post, rank) => ({
3881
+ rank,
3882
+ name: post.name,
3883
+ symbol: post.symbol,
3884
+ coinType: COIN_TYPE_DISPLAY[post.coinType] ?? post.coinType,
3885
+ address: post.address,
3886
+ marketCap: post.marketCap ?? null,
3887
+ marketCapDelta24h: post.marketCapDelta24h ?? null,
3888
+ volume24h: post.volume24h ?? null,
3889
+ createdAt: post.createdAt ?? null
3890
+ });
3891
+ var formatHoldingJson = (balance) => {
3892
+ const priceUsd = balance.coin?.tokenPrice?.priceInUsdc ? Number(balance.coin.tokenPrice.priceInUsdc) : null;
3893
+ const usdValue = priceUsd !== null ? Number((parseRawBalance(balance.balance) * priceUsd).toFixed(6)) : null;
3894
+ return {
3895
+ rank: balance.rank,
3896
+ name: balance.coin?.name ?? null,
3897
+ symbol: balance.coin?.symbol ?? null,
3898
+ coinType: balance.coin?.coinType ?? null,
3899
+ address: balance.coin?.address ?? null,
3900
+ balance: normalizeTokenAmount(balance.balance),
3901
+ usdValue,
3902
+ priceUsd,
3903
+ marketCap: balance.coin?.marketCap ? Number(balance.coin.marketCap) : null
3904
+ };
3905
+ };
3906
+ var fetchProfileData = async (identifier) => {
3907
+ const [postsResult, holdingsResult] = await Promise.allSettled([
3908
+ getProfileCoins({ identifier, count: 20 }),
3909
+ getProfileBalances2({ identifier, count: 20, sortOption: "USD_VALUE" })
3910
+ ]);
3911
+ if (postsResult.status === "rejected") {
3912
+ throw new Error(
3913
+ postsResult.reason instanceof Error ? postsResult.reason.message : String(postsResult.reason)
3914
+ );
3915
+ }
3916
+ if (holdingsResult.status === "rejected") {
3917
+ throw new Error(
3918
+ holdingsResult.reason instanceof Error ? holdingsResult.reason.message : String(holdingsResult.reason)
3919
+ );
3920
+ }
3921
+ if (postsResult.value.error) {
3922
+ throw new Error(
3923
+ `API error (posts): ${extractErrorMessage2(postsResult.value.error)}`
3924
+ );
3925
+ }
3926
+ if (holdingsResult.value.error) {
3927
+ throw new Error(
3928
+ `API error (holdings): ${extractErrorMessage2(holdingsResult.value.error)}`
3929
+ );
3930
+ }
3931
+ const postEdges = postsResult.value.data?.profile?.createdCoins?.edges ?? [];
3932
+ const posts = postEdges.map((e) => e.node);
3933
+ const postsCount = postsResult.value.data?.profile?.createdCoins?.count ?? posts.length;
3934
+ const holdingEdges = holdingsResult.value.data?.profile?.coinBalances?.edges ?? [];
3935
+ const holdings = holdingEdges.map(
3936
+ (e, i) => ({
3937
+ ...e.node,
3938
+ rank: i + 1
3939
+ })
3940
+ );
3941
+ const holdingsCount = holdingsResult.value.data?.profile?.coinBalances?.count ?? holdings.length;
3942
+ return { posts, postsCount, holdings, holdingsCount };
3943
+ };
3944
+ var profileCommand = new Command8("profile").description("View profile activity (posts and holdings)").argument(
3945
+ "[identifier]",
3946
+ "Wallet address or profile handle (defaults to your wallet)"
3947
+ ).option("--live", "Interactive live-updating display (default)").option("--static", "Static snapshot").option(
3948
+ "--refresh <seconds>",
3949
+ "Auto-refresh interval in seconds, requires --live (min 5)",
3950
+ "30"
3951
+ ).action(async function(identifierArg) {
3952
+ const output = getOutputMode(this, "live");
3953
+ const json = output === "json";
3954
+ resolveApiKey(json);
3955
+ const { live, intervalSeconds } = getLiveConfig(this, output);
3956
+ let identifier = identifierArg;
3957
+ if (!identifier) {
3958
+ const envKey = process.env.ZORA_PRIVATE_KEY;
3959
+ const key = envKey || getPrivateKey();
3960
+ if (!key) {
3961
+ outputErrorAndExit(
3962
+ json,
3963
+ "No identifier provided and no wallet configured.",
3964
+ "Pass an address or handle, or run 'zora setup' first."
3965
+ );
3966
+ }
3138
3967
  try {
3139
- const result = getReceivedAmountFromReceipt({
3140
- receipt,
3141
- tokenAddress: outputToken.trade.address,
3142
- recipient: account.address
3143
- });
3144
- receivedAmountOut = result.amount;
3145
- swapLogIndex = result.logIndex;
3146
- receivedSource = "receipt";
3968
+ identifier = privateKeyToAccount3(normalizeKey(key)).address;
3147
3969
  } catch {
3970
+ outputErrorAndExit(
3971
+ json,
3972
+ "Invalid wallet key. Run 'zora setup --force' to replace it."
3973
+ );
3148
3974
  }
3149
3975
  }
3150
- printSellResult(output, {
3151
- coinName,
3152
- coinSymbol,
3153
- address: coinAddress,
3154
- amountIn,
3155
- coinDecimals,
3156
- soldFormatted,
3157
- receivedAmountOut,
3158
- outputSymbol: outputToken.symbol,
3159
- outputDecimals: outputToken.decimals,
3160
- receivedSource,
3161
- txHash
3162
- });
3163
- track("cli_sell", {
3164
- action: "trade",
3165
- coin_address: coinAddress,
3166
- coin_name: coinName,
3167
- coin_symbol: coinSymbol,
3168
- amount_mode: amountMode,
3169
- swap_amount_usd: swapAmountUsd,
3170
- valueUsd: swapAmountUsd,
3171
- swapCoinType,
3172
- transactionHash: txHash,
3173
- logIndex: swapLogIndex,
3174
- output_asset: outputAsset,
3175
- slippage: slippagePct,
3176
- output_format: output,
3177
- success: true,
3178
- tx_hash: txHash
3179
- });
3976
+ if (json) {
3977
+ const data = await fetchProfileData(identifier).catch(
3978
+ (err) => outputErrorAndExit(
3979
+ json,
3980
+ `Request failed: ${err instanceof Error ? err.message : String(err)}`
3981
+ )
3982
+ );
3983
+ outputData(json, {
3984
+ json: {
3985
+ posts: data.posts.map((p, i) => formatPostJson(p, i + 1)),
3986
+ holdings: data.holdings.map(formatHoldingJson)
3987
+ },
3988
+ render: () => {
3989
+ }
3990
+ });
3991
+ track("cli_profile", {
3992
+ identifier,
3993
+ output_format: "json",
3994
+ posts_count: data.postsCount,
3995
+ holdings_count: data.holdingsCount
3996
+ });
3997
+ } else if (live) {
3998
+ const fetchData = () => fetchProfileData(identifier);
3999
+ await renderLive(
4000
+ /* @__PURE__ */ jsx11(
4001
+ ProfileView,
4002
+ {
4003
+ fetchData,
4004
+ identifier,
4005
+ autoRefresh: live,
4006
+ intervalSeconds
4007
+ }
4008
+ )
4009
+ );
4010
+ track("cli_profile", {
4011
+ identifier,
4012
+ output_format: "live",
4013
+ live,
4014
+ interval: intervalSeconds
4015
+ });
4016
+ } else {
4017
+ const data = await fetchProfileData(identifier).catch(
4018
+ (err) => outputErrorAndExit(
4019
+ json,
4020
+ `Request failed: ${err instanceof Error ? err.message : String(err)}`
4021
+ )
4022
+ );
4023
+ const rankedPosts = data.posts.map((p, i) => ({ ...p, rank: i + 1 }));
4024
+ renderOnce(
4025
+ /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
4026
+ rankedPosts.length === 0 ? /* @__PURE__ */ jsx11(
4027
+ Box8,
4028
+ {
4029
+ flexDirection: "column",
4030
+ paddingLeft: 1,
4031
+ paddingTop: 1,
4032
+ paddingBottom: 1,
4033
+ children: /* @__PURE__ */ jsx11(Box8, { children: /* @__PURE__ */ jsx11(Text8, { children: "No posts found for this profile." }) })
4034
+ }
4035
+ ) : /* @__PURE__ */ jsx11(
4036
+ Table,
4037
+ {
4038
+ columns: postColumns,
4039
+ data: rankedPosts,
4040
+ title: "Posts",
4041
+ subtitle: `${rankedPosts.length} of ${data.postsCount}`
4042
+ }
4043
+ ),
4044
+ data.holdings.length === 0 ? /* @__PURE__ */ jsx11(
4045
+ Box8,
4046
+ {
4047
+ flexDirection: "column",
4048
+ paddingLeft: 1,
4049
+ paddingTop: 1,
4050
+ paddingBottom: 1,
4051
+ children: /* @__PURE__ */ jsx11(Box8, { children: /* @__PURE__ */ jsx11(Text8, { children: "No holdings found for this profile." }) })
4052
+ }
4053
+ ) : /* @__PURE__ */ jsx11(
4054
+ Table,
4055
+ {
4056
+ columns: balanceColumns,
4057
+ data: data.holdings,
4058
+ title: "Holdings",
4059
+ subtitle: `${data.holdings.length} of ${data.holdingsCount}`
4060
+ }
4061
+ )
4062
+ ] })
4063
+ );
4064
+ track("cli_profile", {
4065
+ identifier,
4066
+ output_format: "static",
4067
+ posts_count: data.postsCount,
4068
+ holdings_count: data.holdingsCount
4069
+ });
4070
+ }
3180
4071
  });
3181
4072
 
3182
4073
  // src/commands/send.ts
3183
- import { Command as Command8 } from "commander";
4074
+ import { Command as Command9 } from "commander";
3184
4075
  import confirm4 from "@inquirer/confirm";
3185
4076
  import {
3186
4077
  erc20Abi as erc20Abi4,
3187
- formatUnits as formatUnits5,
4078
+ formatUnits as formatUnits6,
3188
4079
  isAddress as isAddress3,
3189
4080
  parseUnits as parseUnits3
3190
4081
  } from "viem";
3191
- import { setApiKey as setApiKey7 } from "@zoralabs/coins-sdk";
4082
+ import { setApiKey as setApiKey8 } from "@zoralabs/coins-sdk";
3192
4083
  var SEND_AMOUNT_CHECKS = {
3193
4084
  amount: (opts) => opts.amount !== void 0,
3194
4085
  percent: (opts) => opts.percent !== void 0,
3195
4086
  all: (opts) => opts.all === true
3196
4087
  };
3197
- var VALID_TYPES3 = ["creator-coin", "post", "trend"];
4088
+ var KNOWN_TOKEN_NAMES = /* @__PURE__ */ new Set(["eth", "usdc", "zora"]);
3198
4089
  function printSendPreview(info) {
3199
4090
  const usdStr = info.amountUsd != null ? ` ($${info.amountUsd.toFixed(2)})` : "";
3200
4091
  console.log(`
@@ -3215,7 +4106,7 @@ function printSendResult(json, info) {
3215
4106
  coin: info.symbol,
3216
4107
  address: info.address,
3217
4108
  sent: {
3218
- amount: formatUnits5(info.amount, info.decimals),
4109
+ amount: formatUnits6(info.amount, info.decimals),
3219
4110
  raw: info.amount.toString(),
3220
4111
  symbol: info.symbol,
3221
4112
  amountUsd: info.amountUsd
@@ -3236,34 +4127,35 @@ function printSendResult(json, info) {
3236
4127
  console.log(` Tx ${info.txHash}
3237
4128
  `);
3238
4129
  }
3239
- var sendCommand = new Command8("send").description("Send coins or ETH to an address").argument("<identifier>", "Coin address, name, or token (eth, usdc, zora)").requiredOption("--to <address>", "Recipient address (0x...)").option("--type <type>", "Coin type: creator-coin, post, trend").option("--amount <value>", "Send specific amount").option("--percent <value>", "Send percentage of balance (1-100)").option("--all", "Send entire balance").option("--yes", "Skip confirmation").action(async function(identifier, opts) {
4130
+ var sendCommand = new Command9("send").description("Send coins or ETH to an address").argument(
4131
+ "[typeOrId]",
4132
+ "Token (eth, usdc, zora), type prefix (creator-coin, trend), or coin address/name"
4133
+ ).argument("[identifier]", "Coin name (when type prefix is given)").option("--to <address>", "Recipient address (0x...)").option("--amount <value>", "Send specific amount").option("--percent <value>", "Send percentage of balance (1-100)").option("--all", "Send entire balance").option("--yes", "Skip confirmation").action(async function(firstArg, secondArg, opts) {
3240
4134
  const json = getJson(this);
3241
- if (!isAddress3(opts.to)) {
4135
+ if (!opts.to) {
3242
4136
  outputErrorAndExit(
3243
4137
  json,
3244
- `Invalid recipient address: ${opts.to}`,
3245
- "Must be a valid 0x address."
4138
+ "Missing --to flag.",
4139
+ "Usage: zora send <identifier> --to <address>"
3246
4140
  );
3247
4141
  }
3248
- const recipient = opts.to;
3249
- if (opts.type !== void 0 && !VALID_TYPES3.includes(opts.type)) {
4142
+ if (!isAddress3(opts.to)) {
3250
4143
  outputErrorAndExit(
3251
4144
  json,
3252
- `Invalid --type value: ${opts.type}.`,
3253
- `Supported: ${VALID_TYPES3.join(", ")}`
4145
+ `Invalid recipient address: ${opts.to}`,
4146
+ "Must be a valid 0x address."
3254
4147
  );
3255
4148
  }
4149
+ const recipient = opts.to;
3256
4150
  const amountMode = getAmountMode(
3257
4151
  json,
3258
4152
  opts,
3259
4153
  SEND_AMOUNT_CHECKS,
3260
4154
  "--amount, --percent, or --all"
3261
4155
  );
3262
- const isEth = identifier.toLowerCase() === "eth";
4156
+ const isKnownToken = KNOWN_TOKEN_NAMES.has(firstArg.toLowerCase());
4157
+ const isEth = firstArg.toLowerCase() === "eth";
3263
4158
  if (isEth) {
3264
- if (opts.type) {
3265
- outputErrorAndExit(json, "--type is not valid when sending ETH.");
3266
- }
3267
4159
  const account = resolveAccount(json);
3268
4160
  const { publicClient, walletClient } = createClients(account);
3269
4161
  const balance = await publicClient.getBalance({
@@ -3301,14 +4193,14 @@ var sendCommand = new Command8("send").description("Send coins or ETH to an addr
3301
4193
  if (amount + GAS_RESERVE > balance) {
3302
4194
  outputErrorAndExit(
3303
4195
  json,
3304
- `Insufficient balance. Have ${formatEthDisplay(balance)} ETH (need to reserve ~${formatEthDisplay(GAS_RESERVE)} ETH for gas).`
4196
+ `Insufficient balance. Have ${formatAmountDisplay(balance, 18)} ETH (need to reserve ~${formatAmountDisplay(GAS_RESERVE, 18)} ETH for gas).`
3305
4197
  );
3306
4198
  }
3307
4199
  } else {
3308
4200
  if (balance <= GAS_RESERVE) {
3309
4201
  outputErrorAndExit(
3310
4202
  json,
3311
- `Balance too low (${formatEthDisplay(balance)} ETH). Need >${formatEthDisplay(GAS_RESERVE)} ETH for gas.`
4203
+ `Balance too low (${formatAmountDisplay(balance, 18)} ETH). Need >${formatAmountDisplay(GAS_RESERVE, 18)} ETH for gas.`
3312
4204
  );
3313
4205
  }
3314
4206
  const spendable = balance - GAS_RESERVE;
@@ -3331,12 +4223,12 @@ var sendCommand = new Command8("send").description("Send coins or ETH to an addr
3331
4223
  }
3332
4224
  }
3333
4225
  }
3334
- const amountFormatted = formatEthDisplay(amount);
4226
+ const amountFormatted = formatAmountDisplay(amount, 18);
3335
4227
  let amountUsd = null;
3336
4228
  const ethPriceUsd = await fetchTokenPriceUsd(WETH_ADDRESS);
3337
4229
  if (ethPriceUsd != null) {
3338
4230
  amountUsd = Number(
3339
- (Number(formatUnits5(amount, 18)) * ethPriceUsd).toFixed(2)
4231
+ (Number(formatUnits6(amount, 18)) * ethPriceUsd).toFixed(2)
3340
4232
  );
3341
4233
  }
3342
4234
  if (!opts.yes) {
@@ -3362,7 +4254,7 @@ var sendCommand = new Command8("send").description("Send coins or ETH to an addr
3362
4254
  } catch (err) {
3363
4255
  track("cli_send", {
3364
4256
  asset: "eth",
3365
- output_format: json ? "json" : "table",
4257
+ output_format: json ? "json" : "static",
3366
4258
  success: false,
3367
4259
  error_type: err instanceof Error ? err.constructor.name : "unknown"
3368
4260
  });
@@ -3391,19 +4283,13 @@ var sendCommand = new Command8("send").description("Send coins or ETH to an addr
3391
4283
  amount_mode: amountMode,
3392
4284
  amount_usd: amountUsd,
3393
4285
  transactionHash: txHash,
3394
- output_format: json ? "json" : "table",
4286
+ output_format: json ? "json" : "static",
3395
4287
  success: true,
3396
4288
  tx_hash: txHash
3397
4289
  });
3398
4290
  } else {
3399
- const knownTokenKey = identifier.toLowerCase();
3400
- const knownToken = knownTokenKey !== "eth" && knownTokenKey in BASE_TRADE_TOKENS ? BASE_TRADE_TOKENS[knownTokenKey] : void 0;
3401
- if (knownToken && opts.type) {
3402
- outputErrorAndExit(
3403
- json,
3404
- `--type is not valid when sending ${knownToken.symbol}.`
3405
- );
3406
- }
4291
+ const knownTokenKey = firstArg.toLowerCase();
4292
+ const knownToken = isKnownToken && knownTokenKey !== "eth" && knownTokenKey in BASE_TRADE_TOKENS ? BASE_TRADE_TOKENS[knownTokenKey] : void 0;
3407
4293
  let tokenAddress;
3408
4294
  let tokenName;
3409
4295
  if (knownToken) {
@@ -3413,23 +4299,62 @@ var sendCommand = new Command8("send").description("Send coins or ETH to an addr
3413
4299
  } else {
3414
4300
  const apiKey = getApiKey();
3415
4301
  if (apiKey) {
3416
- setApiKey7(apiKey);
4302
+ setApiKey8(apiKey);
3417
4303
  }
3418
- const ref = parseCoinRef(identifier, opts.type);
3419
- let result;
4304
+ let parsed;
3420
4305
  try {
3421
- result = await resolveCoin(ref);
4306
+ parsed = parsePositionalCoinArgs(firstArg, secondArg);
3422
4307
  } catch (err) {
3423
- outputErrorAndExit(
3424
- json,
3425
- `Request failed: ${err instanceof Error ? err.message : String(err)}`
3426
- );
4308
+ if (err instanceof CoinArgError) {
4309
+ outputErrorAndExit(json, err.message, err.suggestion);
4310
+ }
4311
+ throw err;
3427
4312
  }
3428
- if (result.kind === "not-found") {
3429
- outputErrorAndExit(json, result.message, result.suggestion);
4313
+ if (parsed.kind === "ambiguous-name") {
4314
+ let ambResult;
4315
+ try {
4316
+ ambResult = await resolveAmbiguousName(parsed.name);
4317
+ } catch (err) {
4318
+ outputErrorAndExit(
4319
+ json,
4320
+ `Request failed: ${err instanceof Error ? err.message : String(err)}`
4321
+ );
4322
+ return;
4323
+ }
4324
+ if (ambResult.kind === "not-found") {
4325
+ outputErrorAndExit(json, ambResult.message);
4326
+ return;
4327
+ }
4328
+ if (ambResult.kind === "ambiguous") {
4329
+ const { message, suggestion } = formatAmbiguousError(
4330
+ parsed.name,
4331
+ ambResult.creator,
4332
+ ambResult.trend,
4333
+ "send"
4334
+ );
4335
+ outputErrorAndExit(json, message, suggestion);
4336
+ return;
4337
+ }
4338
+ tokenAddress = ambResult.coin.address;
4339
+ tokenName = ambResult.coin.name;
4340
+ } else {
4341
+ const ref = coinArgsToRef(parsed);
4342
+ try {
4343
+ const result = await resolveCoin(ref);
4344
+ if (result.kind === "not-found") {
4345
+ outputErrorAndExit(json, result.message, result.suggestion);
4346
+ return;
4347
+ }
4348
+ tokenAddress = result.coin.address;
4349
+ tokenName = result.coin.name;
4350
+ } catch (err) {
4351
+ outputErrorAndExit(
4352
+ json,
4353
+ `Request failed: ${err instanceof Error ? err.message : String(err)}`
4354
+ );
4355
+ return;
4356
+ }
3430
4357
  }
3431
- tokenAddress = result.coin.address;
3432
- tokenName = result.coin.name;
3433
4358
  }
3434
4359
  const account = resolveAccount(json);
3435
4360
  const { publicClient, walletClient } = createClients(account);
@@ -3527,7 +4452,7 @@ var sendCommand = new Command8("send").description("Send coins or ETH to an addr
3527
4452
  const priceUsd = knownToken?.fixedPriceUsd ?? await fetchTokenPriceUsd(priceAddress);
3528
4453
  if (priceUsd != null) {
3529
4454
  amountUsd = Number(
3530
- (Number(formatUnits5(amount, decimals)) * priceUsd).toFixed(2)
4455
+ (Number(formatUnits6(amount, decimals)) * priceUsd).toFixed(2)
3531
4456
  );
3532
4457
  }
3533
4458
  if (!opts.yes) {
@@ -3558,7 +4483,7 @@ var sendCommand = new Command8("send").description("Send coins or ETH to an addr
3558
4483
  coin_address: tokenAddress,
3559
4484
  coin_name: tokenName,
3560
4485
  coin_symbol: symbol,
3561
- output_format: json ? "json" : "table",
4486
+ output_format: json ? "json" : "static",
3562
4487
  success: false,
3563
4488
  error_type: err instanceof Error ? err.constructor.name : "unknown"
3564
4489
  });
@@ -3588,7 +4513,7 @@ var sendCommand = new Command8("send").description("Send coins or ETH to an addr
3588
4513
  amount_mode: amountMode,
3589
4514
  amount_usd: amountUsd,
3590
4515
  transactionHash: txHash,
3591
- output_format: json ? "json" : "table",
4516
+ output_format: json ? "json" : "static",
3592
4517
  success: true,
3593
4518
  tx_hash: txHash
3594
4519
  });
@@ -3596,8 +4521,8 @@ var sendCommand = new Command8("send").description("Send coins or ETH to an addr
3596
4521
  });
3597
4522
 
3598
4523
  // src/commands/setup.ts
3599
- import { Command as Command9 } from "commander";
3600
- import { generatePrivateKey, privateKeyToAccount as privateKeyToAccount3 } from "viem/accounts";
4524
+ import { Command as Command10 } from "commander";
4525
+ import { generatePrivateKey, privateKeyToAccount as privateKeyToAccount4 } from "viem/accounts";
3601
4526
 
3602
4527
  // src/lib/strings.ts
3603
4528
  var DEPOSIT_INSTRUCTIONS = "Deposit ETH or USDC to this address on Base to start trading.\n\n You can do this from:\n - Coinbase \u2014 withdraw directly to Base\n - Another wallet (MetaMask, Rainbow, etc.) \u2014 send on Base network\n - Bridge from other chains \u2014 use https://superbridge.app/base";
@@ -3610,7 +4535,7 @@ var BACKUP_WARNING = "Back up this file \u2014 it's the only copy of your key.";
3610
4535
  var isValidPrivateKey = (key) => /^(0x)?[0-9a-fA-F]{64}$/.test(key);
3611
4536
  var toAccount = (json, key, errorPrefix) => {
3612
4537
  try {
3613
- return privateKeyToAccount3(normalizeKey(key));
4538
+ return privateKeyToAccount4(normalizeKey(key));
3614
4539
  } catch {
3615
4540
  outputErrorAndExit(
3616
4541
  json,
@@ -3618,7 +4543,7 @@ var toAccount = (json, key, errorPrefix) => {
3618
4543
  );
3619
4544
  }
3620
4545
  };
3621
- var setupCommand = new Command9("setup").description("Set up your Zora wallet").option("--create", "Create a new wallet without prompting").option("--force", "Overwrite existing wallet without prompting").option("--yes", "Skip interactive prompt and execute directly").action(async function(options) {
4546
+ var setupCommand = new Command10("setup").description("Set up your Zora wallet").option("--create", "Create a new wallet without prompting").option("--force", "Overwrite existing wallet without prompting").option("--yes", "Skip interactive prompt and execute directly").action(async function(options) {
3622
4547
  const json = getJson(this);
3623
4548
  const nonInteractive = getYes(this);
3624
4549
  const envKey = process.env.ZORA_PRIVATE_KEY;
@@ -3633,7 +4558,7 @@ var setupCommand = new Command9("setup").description("Set up your Zora wallet").
3633
4558
  const account = toAccount(json, envKey, "ZORA_PRIVATE_KEY");
3634
4559
  outputData(json, {
3635
4560
  json: { source: "env", address: account.address },
3636
- table: () => {
4561
+ render: () => {
3637
4562
  console.log(" Using wallet from ZORA_PRIVATE_KEY.\n");
3638
4563
  console.log(` Address: ${account.address}
3639
4564
  `);
@@ -3654,14 +4579,14 @@ var setupCommand = new Command9("setup").description("Set up your Zora wallet").
3654
4579
  } catch (err) {
3655
4580
  outputErrorAndExit(
3656
4581
  json,
3657
- `\u2717 Could not read wallet: ${err.message}`,
4582
+ `\u2717 Could not read wallet: ${formatError(err)}`,
3658
4583
  "Run 'zora setup --force' to overwrite it."
3659
4584
  );
3660
4585
  }
3661
4586
  }
3662
4587
  if (existing) {
3663
4588
  const account = toAccount(json, existing, "Stored private key");
3664
- const truncated = `${account.address.slice(0, 6)}\u2026${account.address.slice(-4)}`;
4589
+ const truncated = truncateAddress(account.address);
3665
4590
  console.log(` Wallet already configured: ${truncated}
3666
4591
  `);
3667
4592
  if (!options.force) {
@@ -3723,7 +4648,7 @@ var setupCommand = new Command9("setup").description("Set up your Zora wallet").
3723
4648
  address: account.address,
3724
4649
  path: getWalletPath()
3725
4650
  },
3726
- table: () => {
4651
+ render: () => {
3727
4652
  console.log("\n\u2713 Wallet imported\n");
3728
4653
  console.log(` Address: ${account.address}`);
3729
4654
  console.log(` Private key: saved to ${getWalletPath()}
@@ -3758,7 +4683,7 @@ var setupCommand = new Command9("setup").description("Set up your Zora wallet").
3758
4683
  address: account.address,
3759
4684
  path: getWalletPath()
3760
4685
  },
3761
- table: () => {
4686
+ render: () => {
3762
4687
  console.log("\n\u2713 Wallet created\n");
3763
4688
  console.log(` Address: ${account.address}`);
3764
4689
  console.log(` Private key: saved to ${getWalletPath()}
@@ -3777,8 +4702,8 @@ var setupCommand = new Command9("setup").description("Set up your Zora wallet").
3777
4702
  });
3778
4703
 
3779
4704
  // src/commands/wallet.ts
3780
- import { Command as Command10 } from "commander";
3781
- import { privateKeyToAccount as privateKeyToAccount4 } from "viem/accounts";
4705
+ import { Command as Command11 } from "commander";
4706
+ import { privateKeyToAccount as privateKeyToAccount5 } from "viem/accounts";
3782
4707
  var resolvePrivateKey = () => {
3783
4708
  const envKey = process.env.ZORA_PRIVATE_KEY;
3784
4709
  if (envKey) {
@@ -3790,7 +4715,7 @@ var resolvePrivateKey = () => {
3790
4715
  }
3791
4716
  return void 0;
3792
4717
  };
3793
- var walletCommand = new Command10("wallet").description(
4718
+ var walletCommand = new Command11("wallet").description(
3794
4719
  "Manage your Zora wallet"
3795
4720
  );
3796
4721
  walletCommand.command("info").description("Show wallet address and storage location").action(function() {
@@ -3801,7 +4726,7 @@ walletCommand.command("info").description("Show wallet address and storage locat
3801
4726
  }
3802
4727
  let account;
3803
4728
  try {
3804
- account = privateKeyToAccount4(normalizeKey(resolved.key));
4729
+ account = privateKeyToAccount5(normalizeKey(resolved.key));
3805
4730
  } catch {
3806
4731
  const msg = resolved.source === "env" ? "ZORA_PRIVATE_KEY is not a valid private key." : "Stored private key is invalid.";
3807
4732
  const suggestion = resolved.source === "env" ? void 0 : "Run 'zora setup --force' to replace it.";
@@ -3810,7 +4735,7 @@ walletCommand.command("info").description("Show wallet address and storage locat
3810
4735
  const source = resolved.source === "env" ? "env (ZORA_PRIVATE_KEY)" : getWalletPath();
3811
4736
  outputData(json, {
3812
4737
  json: { address: account.address, source },
3813
- table: () => {
4738
+ render: () => {
3814
4739
  console.log(` Address: ${account.address}`);
3815
4740
  console.log(` Source: ${source}`);
3816
4741
  }
@@ -3849,8 +4774,103 @@ walletCommand.command("export").description("Print the raw private key to stdout
3849
4774
  });
3850
4775
  });
3851
4776
 
4777
+ // src/components/StyledHelp.tsx
4778
+ import { Text as Text10, Box as Box10 } from "ink";
4779
+
4780
+ // src/components/KeyValueTable.tsx
4781
+ import { Text as Text9, Box as Box9 } from "ink";
4782
+ import { jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
4783
+ function KeyValueTable({
4784
+ rows,
4785
+ labelWidth
4786
+ }) {
4787
+ const pad = labelWidth ?? Math.max(0, ...rows.map((r) => r.label.length)) + 2;
4788
+ return /* @__PURE__ */ jsx12(Box9, { flexDirection: "column", children: rows.map((row, i) => /* @__PURE__ */ jsxs9(Text9, { children: [
4789
+ /* @__PURE__ */ jsx12(Text9, { bold: true, children: row.label.padEnd(pad) }),
4790
+ /* @__PURE__ */ jsx12(Text9, { dimColor: true, children: row.value })
4791
+ ] }, i)) });
4792
+ }
4793
+
4794
+ // src/lib/parse-help.ts
4795
+ var DEFAULT_DESC_COLUMN = 38;
4796
+ var TWO_COLUMN_REGEX = /^(.+\S)([ \t]{2,})(\S.*)/m;
4797
+ function getDescriptionColumnOffset(sections) {
4798
+ for (const section of sections) {
4799
+ const match = section.content.match(TWO_COLUMN_REGEX);
4800
+ if (match) {
4801
+ return match[1].length + match[2].length;
4802
+ }
4803
+ }
4804
+ return DEFAULT_DESC_COLUMN;
4805
+ }
4806
+ function parseHelpSections(text) {
4807
+ const sections = [];
4808
+ let currentTitle = "";
4809
+ let currentLines = [];
4810
+ for (const line of text.split("\n")) {
4811
+ const match = line.match(/^([A-Z]\w+):(.*)/);
4812
+ if (match) {
4813
+ if (currentTitle) {
4814
+ const content = currentLines.join("\n").replace(/^\n+|\n+$/g, "");
4815
+ sections.push({ title: currentTitle, content });
4816
+ }
4817
+ currentTitle = match[1];
4818
+ currentLines = match[2].trim() ? [match[2].trim()] : [];
4819
+ } else if (currentTitle) {
4820
+ currentLines.push(line.startsWith(" ") ? line.slice(2) : line);
4821
+ }
4822
+ }
4823
+ if (currentTitle) {
4824
+ const content = currentLines.join("\n").replace(/^\n+|\n+$/g, "");
4825
+ sections.push({ title: currentTitle, content });
4826
+ }
4827
+ return sections.filter((s) => s.content);
4828
+ }
4829
+
4830
+ // src/components/StyledHelp.tsx
4831
+ import { jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
4832
+ function StyledHelp({
4833
+ sections,
4834
+ header
4835
+ }) {
4836
+ const descriptionColumnOffset = getDescriptionColumnOffset(sections);
4837
+ return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", gap: 1, children: [
4838
+ header,
4839
+ sections.map((section, i) => {
4840
+ const hasTwoColumns = TWO_COLUMN_REGEX.test(section.content);
4841
+ const rows = hasTwoColumns ? section.content.split("\n").map((line) => {
4842
+ const m = line.match(TWO_COLUMN_REGEX);
4843
+ if (m)
4844
+ return {
4845
+ label: m[1],
4846
+ value: m[3][0].toUpperCase() + m[3].slice(1)
4847
+ };
4848
+ return { label: "", value: line.trimStart() };
4849
+ }) : null;
4850
+ return /* @__PURE__ */ jsxs10(
4851
+ Box10,
4852
+ {
4853
+ flexDirection: "column",
4854
+ borderStyle: "single",
4855
+ borderDimColor: true,
4856
+ paddingX: 1,
4857
+ paddingY: 1,
4858
+ children: [
4859
+ /* @__PURE__ */ jsx13(Text10, { bold: true, children: section.title }),
4860
+ rows ? /* @__PURE__ */ jsx13(KeyValueTable, { rows, labelWidth: descriptionColumnOffset }) : /* @__PURE__ */ jsx13(Text10, { children: section.content })
4861
+ ]
4862
+ },
4863
+ i
4864
+ );
4865
+ })
4866
+ ] });
4867
+ }
4868
+
4869
+ // src/components/StyledHelpHeader.tsx
4870
+ import { Text as Text12, Box as Box12 } from "ink";
4871
+
3852
4872
  // src/components/Zorb.tsx
3853
- import { Text as Text7, Box as Box7 } from "ink";
4873
+ import { Text as Text11, Box as Box11 } from "ink";
3854
4874
 
3855
4875
  // src/lib/zorb-pixels.ts
3856
4876
  function supportsTruecolor() {
@@ -3995,7 +5015,7 @@ function generateZorbPixels(size) {
3995
5015
  }
3996
5016
 
3997
5017
  // src/components/Zorb.tsx
3998
- import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
5018
+ import { jsx as jsx14, jsxs as jsxs11 } from "react/jsx-runtime";
3999
5019
  var LOWER_HALF_BLOCK = "\u2584";
4000
5020
  var UPPER_HALF_BLOCK = "\u2580";
4001
5021
  function rgbString([r, g, b]) {
@@ -4018,19 +5038,19 @@ function Zorb({ size = 20 }) {
4018
5038
  const topIsBlack = isBlack(top);
4019
5039
  const bottomIsBlack = isBlack(bottom);
4020
5040
  if (topIsBlack && bottomIsBlack) {
4021
- cells.push(/* @__PURE__ */ jsx10(Text7, { children: " " }, x));
5041
+ cells.push(/* @__PURE__ */ jsx14(Text11, { children: " " }, x));
4022
5042
  } else if (topIsBlack) {
4023
5043
  cells.push(
4024
- /* @__PURE__ */ jsx10(Text7, { color: rgbString(bottom), children: LOWER_HALF_BLOCK }, x)
5044
+ /* @__PURE__ */ jsx14(Text11, { color: rgbString(bottom), children: LOWER_HALF_BLOCK }, x)
4025
5045
  );
4026
5046
  } else if (bottomIsBlack) {
4027
5047
  cells.push(
4028
- /* @__PURE__ */ jsx10(Text7, { color: rgbString(top), children: UPPER_HALF_BLOCK }, x)
5048
+ /* @__PURE__ */ jsx14(Text11, { color: rgbString(top), children: UPPER_HALF_BLOCK }, x)
4029
5049
  );
4030
5050
  } else {
4031
5051
  cells.push(
4032
- /* @__PURE__ */ jsx10(
4033
- Text7,
5052
+ /* @__PURE__ */ jsx14(
5053
+ Text11,
4034
5054
  {
4035
5055
  backgroundColor: rgbString(top),
4036
5056
  color: rgbString(bottom),
@@ -4041,50 +5061,96 @@ function Zorb({ size = 20 }) {
4041
5061
  );
4042
5062
  }
4043
5063
  }
4044
- rows.push(/* @__PURE__ */ jsx10(Text7, { children: cells }, y));
5064
+ rows.push(/* @__PURE__ */ jsx14(Text11, { children: cells }, y));
4045
5065
  }
4046
- return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
4047
- /* @__PURE__ */ jsx10(Text7, { children: " " }),
5066
+ return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", children: [
5067
+ /* @__PURE__ */ jsx14(Text11, { children: " " }),
4048
5068
  rows,
4049
- /* @__PURE__ */ jsx10(Text7, { children: " " })
5069
+ /* @__PURE__ */ jsx14(Text11, { children: " " })
4050
5070
  ] });
4051
5071
  }
4052
5072
 
5073
+ // src/components/StyledHelpHeader.tsx
5074
+ import { jsx as jsx15, jsxs as jsxs12 } from "react/jsx-runtime";
5075
+ function StyledHelpHeader({
5076
+ sections
5077
+ }) {
5078
+ const descriptionColumnOffset = getDescriptionColumnOffset(sections);
5079
+ return /* @__PURE__ */ jsxs12(
5080
+ Box12,
5081
+ {
5082
+ flexDirection: "row",
5083
+ borderStyle: "single",
5084
+ borderDimColor: true,
5085
+ paddingX: 1,
5086
+ paddingY: 1,
5087
+ children: [
5088
+ /* @__PURE__ */ jsx15(Box12, { flexShrink: 0, width: descriptionColumnOffset, children: /* @__PURE__ */ jsx15(Zorb, { size: 20 }) }),
5089
+ /* @__PURE__ */ jsx15(Box12, { flexDirection: "column", flexGrow: 1, justifyContent: "center", children: /* @__PURE__ */ jsx15(Text12, { bold: true, children: "Zora CLI" }) })
5090
+ ]
5091
+ }
5092
+ );
5093
+ }
5094
+
4053
5095
  // src/index.tsx
4054
- import { jsx as jsx11 } from "react/jsx-runtime";
5096
+ import { jsx as jsx16 } from "react/jsx-runtime";
4055
5097
  if (process.env.ZORA_API_TARGET) {
4056
5098
  setApiBaseUrl(process.env.ZORA_API_TARGET);
4057
5099
  }
4058
- var version = true ? "0.2.4" : JSON.parse(
5100
+ var version = true ? "0.3.1" : JSON.parse(
4059
5101
  readFileSync2(new URL("../package.json", import.meta.url), "utf-8")
4060
5102
  ).version;
5103
+ function styledHelpWriteOut(showHeader) {
5104
+ return (str) => {
5105
+ if (supportsTruecolor()) {
5106
+ const sections = parseHelpSections(str);
5107
+ if (sections.length > 0) {
5108
+ const header = showHeader ? /* @__PURE__ */ jsx16(StyledHelpHeader, { sections }) : void 0;
5109
+ renderOnce(/* @__PURE__ */ jsx16(StyledHelp, { sections, header }));
5110
+ return;
5111
+ }
5112
+ }
5113
+ process.stdout.write(str);
5114
+ };
5115
+ }
4061
5116
  var buildProgram = () => {
4062
- const program2 = new Command11().name("zora").description("Zora CLI").version(version).option(
4063
- "--output <format>",
4064
- "Output format: table, json, live (default varies by command)"
4065
- ).option(
4066
- "--interval <seconds>",
4067
- "Auto-refresh interval in seconds (min 5)",
4068
- "30"
4069
- );
5117
+ const program2 = new Command12().name("zora").description("A developer CLI for the Zora platform").version(version).option("--json", "Output as JSON (for scripts and automation)", false);
5118
+ const helpWidth = (process.stdout.columns || 80) - 4;
5119
+ program2.configureHelp({ helpWidth });
5120
+ program2.configureOutput({ writeOut: styledHelpWriteOut(true) });
5121
+ program2.action(() => {
5122
+ program2.outputHelp();
5123
+ });
4070
5124
  program2.addCommand(authCommand);
4071
5125
  program2.addCommand(balanceCommand);
4072
5126
  program2.addCommand(buyCommand);
4073
5127
  program2.addCommand(exploreCommand);
4074
5128
  program2.addCommand(getCommand);
4075
5129
  program2.addCommand(priceHistoryCommand);
5130
+ program2.addCommand(profileCommand);
4076
5131
  program2.addCommand(setupCommand);
4077
5132
  program2.addCommand(walletCommand);
4078
5133
  program2.addCommand(sellCommand);
4079
5134
  program2.addCommand(sendCommand);
5135
+ const applyToSubcommands = (parent) => {
5136
+ for (const cmd of parent.commands) {
5137
+ cmd.configureHelp({ helpWidth });
5138
+ cmd.configureOutput({ writeOut: styledHelpWriteOut(false) });
5139
+ applyToSubcommands(cmd);
5140
+ }
5141
+ };
5142
+ applyToSubcommands(program2);
5143
+ program2.hook("preAction", (_thisCommand, actionCommand) => {
5144
+ const expected = actionCommand.registeredArguments.length;
5145
+ if (expected > 0 && actionCommand.args.length === 0) {
5146
+ actionCommand.outputHelp();
5147
+ process.exit(1);
5148
+ }
5149
+ });
4080
5150
  return program2;
4081
5151
  };
4082
5152
  var program = buildProgram();
4083
5153
  if (!process.env.VITEST) {
4084
- const showingHelp = process.argv.length <= 2 || process.argv.includes("--help") || process.argv.includes("-h");
4085
- if (showingHelp && !process.argv.includes("--output") && supportsTruecolor()) {
4086
- renderOnce(/* @__PURE__ */ jsx11(Zorb, { size: 20 }));
4087
- }
4088
5154
  console.warn(
4089
5155
  "\x1B[33m\u26A0 Beta:\x1B[0m This CLI is in beta and should be used with caution."
4090
5156
  );