@zoralabs/cli 0.2.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +76 -0
  2. package/dist/index.js +834 -221
  3. package/package.json +1 -1
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,50 @@ 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 fsErrorMessage(err, path) {
56
+ if (!(err instanceof Error)) return String(err);
57
+ const code = err.code;
58
+ if (code === "EACCES") return `Permission denied accessing ${path}.`;
59
+ if (code === "EISDIR")
60
+ return `Expected a file but found a directory at ${path}.`;
61
+ return formatError(err);
62
+ }
63
+
64
+ // src/lib/config.ts
21
65
  import { homedir, platform } from "os";
22
66
  function getConfigDir() {
23
67
  if (platform() === "win32") {
@@ -55,7 +99,7 @@ function readConfig() {
55
99
  parsed = JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
56
100
  } catch (err) {
57
101
  console.error(
58
- `Warning: could not parse ${CONFIG_FILE}: ${err.message}. Run 'zora auth configure' to fix.`
102
+ `Warning: could not parse ${CONFIG_FILE}: ${formatError(err)}. Run 'zora auth configure' to fix.`
59
103
  );
60
104
  configReadOnly = true;
61
105
  return { version: CONFIG_VERSION };
@@ -64,7 +108,7 @@ function readConfig() {
64
108
  assertVersion(parsed, CONFIG_VERSION, CONFIG_FILE);
65
109
  } catch (err) {
66
110
  console.error(
67
- `Warning: ${err.message}. Delete ${CONFIG_FILE} or run 'zora auth configure' to reset.`
111
+ `Warning: ${formatError(err)}. Delete ${CONFIG_FILE} or run 'zora auth configure' to reset.`
68
112
  );
69
113
  configReadOnly = true;
70
114
  return { version: CONFIG_VERSION };
@@ -155,18 +199,29 @@ function maskKey(key) {
155
199
  }
156
200
 
157
201
  // src/lib/output.ts
158
- var VALID_OUTPUT_MODES = ["table", "json", "live"];
159
202
  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
- );
203
+ const globals = cmd.optsWithGlobals();
204
+ const json = globals.json ?? false;
205
+ const live = globals.live ?? false;
206
+ const static_ = globals.static ?? false;
207
+ const set = [
208
+ json && "--json",
209
+ live && "--live",
210
+ static_ && "--static"
211
+ ].filter(Boolean);
212
+ if (set.length > 1) {
213
+ return outputErrorAndExit(
214
+ false,
215
+ `${set.join(", ")} cannot be used together.`,
216
+ "Choose one: --json, --live, or --static"
217
+ );
218
+ }
219
+ if (json) return "json";
220
+ if (live) return "live";
221
+ if (static_) return "static";
222
+ return defaultMode;
168
223
  };
169
- var getJson = (cmd) => getOutputMode(cmd, "table") === "json";
224
+ var getJson = (cmd) => cmd.optsWithGlobals().json ?? false;
170
225
  var getYes = (cmd) => cmd.optsWithGlobals().yes ?? false;
171
226
  var outputJson = (data) => {
172
227
  console.log(JSON.stringify(data, null, 2));
@@ -188,14 +243,18 @@ var outputData = (json, opts) => {
188
243
  if (json) {
189
244
  outputJson(opts.json);
190
245
  } else {
191
- opts.table();
246
+ opts.render();
192
247
  }
193
248
  };
194
- var getLiveConfig = (cmd, defaultMode) => {
195
- const mode = getOutputMode(cmd, defaultMode);
249
+ var getLiveConfig = (cmd, mode) => {
196
250
  const live = mode === "live";
197
- const intervalRaw = parseInt(cmd.optsWithGlobals().interval, 10);
251
+ const intervalRaw = parseInt(cmd.opts().refresh, 10);
198
252
  const intervalSeconds = isNaN(intervalRaw) || intervalRaw < 5 ? 30 : intervalRaw;
253
+ if (!live && cmd.getOptionValueSource("refresh") === "cli") {
254
+ console.warn(
255
+ "\x1B[33mWarning:\x1B[0m --refresh has no effect without --live"
256
+ );
257
+ }
199
258
  return { live, intervalSeconds };
200
259
  };
201
260
 
@@ -282,9 +341,7 @@ var resolveAccount = (json = false) => {
282
341
  try {
283
342
  return privateKeyToAccount(normalizeKey(key));
284
343
  } catch (err) {
285
- console.error(
286
- `\u2717 Invalid private key: ${err instanceof Error ? err.message : String(err)}`
287
- );
344
+ console.error(`\u2717 Invalid private key: ${formatError(err)}`);
288
345
  console.error(" Run 'zora setup --force' to replace it.");
289
346
  return process.exit(1);
290
347
  }
@@ -374,7 +431,7 @@ var getClient = () => {
374
431
  return client;
375
432
  };
376
433
  var commonProperties = () => ({
377
- cli_version: true ? "0.2.4" : "development",
434
+ cli_version: true ? "0.3.0" : "development",
378
435
  os: process.platform,
379
436
  arch: process.arch,
380
437
  node_version: process.version
@@ -447,7 +504,7 @@ authCommand.command("configure").description("Set your Zora API key").option("--
447
504
  status: "env_override",
448
505
  message: "API key is set via ZORA_API_KEY environment variable."
449
506
  },
450
- table: () => console.log(
507
+ render: () => console.log(
451
508
  "API key is set via ZORA_API_KEY environment variable. Unset it to configure manually."
452
509
  )
453
510
  });
@@ -475,7 +532,7 @@ authCommand.command("configure").description("Set your Zora API key").option("--
475
532
  saveApiKey(trimmed);
476
533
  outputData(json, {
477
534
  json: { saved: true, path: getConfigPath() },
478
- table: () => console.log(`API key saved to ${getConfigPath()}`)
535
+ render: () => console.log(`API key saved to ${getConfigPath()}`)
479
536
  });
480
537
  track("cli_auth_configure", {
481
538
  output_format: json ? "json" : "text"
@@ -483,7 +540,7 @@ authCommand.command("configure").description("Set your Zora API key").option("--
483
540
  } catch (err) {
484
541
  outputErrorAndExit(
485
542
  json,
486
- `Failed to save API key: ${err.message}`
543
+ `Failed to save API key: ${fsErrorMessage(err, getConfigPath())}`
487
544
  );
488
545
  }
489
546
  });
@@ -493,7 +550,7 @@ authCommand.command("status").description("Check authentication status").action(
493
550
  if (!apiKey) {
494
551
  outputData(json, {
495
552
  json: { authenticated: false },
496
- table: () => {
553
+ render: () => {
497
554
  console.log(
498
555
  "No API key configured. The CLI works without one, but requests are rate-limited."
499
556
  );
@@ -512,7 +569,7 @@ authCommand.command("status").description("Check authentication status").action(
512
569
  const source = getEnvApiKey() ? "env (ZORA_API_KEY)" : getConfigPath();
513
570
  outputData(json, {
514
571
  json: { authenticated: true, key: maskKey(apiKey), source },
515
- table: () => {
572
+ render: () => {
516
573
  console.log(`Authenticated: ${maskKey(apiKey)}`);
517
574
  console.log(`Source: ${source}`);
518
575
  }
@@ -577,7 +634,8 @@ var Table = ({
577
634
  title,
578
635
  subtitle,
579
636
  fullWidth = true,
580
- footer
637
+ footer,
638
+ selectedRow
581
639
  }) => {
582
640
  const widths = computeColumnWidths(columns, fullWidth);
583
641
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingTop: 1, paddingBottom: 1, children: [
@@ -589,19 +647,24 @@ var Table = ({
589
647
  ] })
590
648
  ] }),
591
649
  /* @__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)),
650
+ data.map((row, i) => {
651
+ const isSelected = selectedRow === i;
652
+ return /* @__PURE__ */ jsx(Box, { paddingLeft: PADDING_LEFT, children: columns.map((col, colIdx) => {
653
+ const colWidth = widths[colIdx];
654
+ const value = col.noTruncate ? col.accessor(row) : truncate(col.accessor(row), colWidth - 2);
655
+ const colorName = col.color?.(row);
656
+ return /* @__PURE__ */ jsx(Box, { width: colWidth, children: /* @__PURE__ */ jsx(
657
+ Text,
658
+ {
659
+ color: colorName,
660
+ bold: isSelected,
661
+ inverse: isSelected,
662
+ wrap: col.noTruncate ? "wrap" : "truncate",
663
+ children: value
664
+ }
665
+ ) }, col.header);
666
+ }) }, i);
667
+ }),
605
668
  footer && /* @__PURE__ */ jsx(Box, { paddingLeft: PADDING_LEFT, marginTop: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, wrap: "wrap", children: footer }) })
606
669
  ] });
607
670
  };
@@ -664,6 +727,7 @@ var formatEthDisplay = (wei) => {
664
727
  const trimmed = parts[1].replace(/0+$/, "") || "0";
665
728
  return `${parts[0]}.${trimmed}`;
666
729
  };
730
+ var truncateAddress = (address) => `${address.slice(0, 6)}\u2026${address.slice(-4)}`;
667
731
  var formatCoinsDisplay = (coinsOut) => new Intl.NumberFormat("en-US", {
668
732
  maximumFractionDigits: 2
669
733
  }).format(Number(coinsOut));
@@ -800,6 +864,7 @@ var BalanceView = ({
800
864
  fetchData,
801
865
  sort,
802
866
  mode = "full",
867
+ initialCursor,
803
868
  autoRefresh = false,
804
869
  intervalSeconds = 30
805
870
  }) => {
@@ -808,37 +873,64 @@ var BalanceView = ({
808
873
  const [isRefreshing, setIsRefreshing] = useState2(false);
809
874
  const [error, setError] = useState2(null);
810
875
  const [data, setData] = useState2(null);
876
+ const paginated = mode === "coins";
877
+ const [page, setPage] = useState2(1);
878
+ const [cursorHistory, setCursorHistory] = useState2(
879
+ []
880
+ );
881
+ const [currentCursor, setCurrentCursor] = useState2(
882
+ initialCursor
883
+ );
811
884
  const { refreshCount, secondsUntilRefresh, triggerManualRefresh } = useAutoRefresh(intervalSeconds, autoRefresh);
812
885
  const [manualRefreshCount, setManualRefreshCount] = useState2(0);
813
886
  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]);
887
+ const load = useCallback2(
888
+ async (cursor) => {
889
+ if (hasLoadedOnce.current) {
890
+ setIsRefreshing(true);
891
+ } else {
892
+ setLoading(true);
893
+ }
894
+ setError(null);
895
+ try {
896
+ const result = await fetchData(cursor);
897
+ setData(result);
898
+ hasLoadedOnce.current = true;
899
+ } catch (err) {
900
+ setError(err instanceof Error ? err.message : String(err));
901
+ }
902
+ setLoading(false);
903
+ setIsRefreshing(false);
904
+ },
905
+ [fetchData]
906
+ );
831
907
  useEffect2(() => {
832
- load();
833
- }, [refreshCount, manualRefreshCount]);
908
+ load(currentCursor);
909
+ }, [load, refreshCount, manualRefreshCount, currentCursor]);
834
910
  useInput((input, key) => {
835
911
  if (input === "q" || key.escape) {
836
912
  exit();
837
913
  return;
838
914
  }
839
- if (input === "r" && !loading) {
915
+ if (loading) return;
916
+ if (input === "r") {
840
917
  triggerManualRefresh();
841
918
  setManualRefreshCount((c) => c + 1);
919
+ return;
920
+ }
921
+ if (!paginated) return;
922
+ const canGoNext = data?.pageInfo?.hasNextPage && data.pageInfo.endCursor;
923
+ const canGoPrev = cursorHistory.length > 0;
924
+ if ((input === "n" || key.rightArrow) && canGoNext) {
925
+ setCursorHistory((prev) => [...prev, currentCursor]);
926
+ setCurrentCursor(data.pageInfo.endCursor);
927
+ setPage((p) => p + 1);
928
+ }
929
+ if ((input === "p" || key.leftArrow) && canGoPrev) {
930
+ const prev = cursorHistory[cursorHistory.length - 1];
931
+ setCursorHistory((h) => h.slice(0, -1));
932
+ setCurrentCursor(prev);
933
+ setPage((p) => p - 1);
842
934
  }
843
935
  });
844
936
  if (error && !data) {
@@ -866,7 +958,10 @@ var BalanceView = ({
866
958
  ] }) });
867
959
  }
868
960
  if (!data) return null;
869
- const hints = ["r refresh"];
961
+ const hints = [];
962
+ if (paginated && cursorHistory.length > 0) hints.push("\u2190 prev");
963
+ if (paginated && data?.pageInfo?.hasNextPage) hints.push("\u2192 next");
964
+ hints.push("r refresh");
870
965
  if (autoRefresh) hints.push(`auto: ${secondsUntilRefresh}s`);
871
966
  hints.push("q quit");
872
967
  const footer = hints.join(" \xB7 ");
@@ -910,7 +1005,7 @@ var BalanceView = ({
910
1005
  columns: balanceColumns,
911
1006
  data: data.rankedBalances,
912
1007
  title: `Coins \xB7 sorted by ${SORT_LABELS[sort]}`,
913
- subtitle: `${data.rankedBalances.length} of ${data.total}`
1008
+ subtitle: paginated ? `Page ${page} \xB7 ${data.rankedBalances.length} result${data.rankedBalances.length !== 1 ? "s" : ""}` : `${data.rankedBalances.length} of ${data.total}`
914
1009
  }
915
1010
  ) : null,
916
1011
  /* @__PURE__ */ jsx2(Box2, { paddingLeft: 1, paddingBottom: 1, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: footer }) })
@@ -957,7 +1052,7 @@ var fetchTokenPriceUsd = async (address, chainId = BASE_CHAIN_ID) => {
957
1052
  return res.data?.erc20Token?.currency?.priceUsd ? Number(res.data.erc20Token.currency.priceUsd) : null;
958
1053
  } catch (err) {
959
1054
  console.warn(
960
- `Warning: failed to fetch price for ${address}: ${err instanceof Error ? err.message : String(err)}`
1055
+ `Warning: failed to fetch price for ${address}: ${formatError(err)}`
961
1056
  );
962
1057
  return null;
963
1058
  }
@@ -1094,7 +1189,7 @@ function resolveContext(json) {
1094
1189
  function renderWallet(json, walletResult) {
1095
1190
  outputData(json, {
1096
1191
  json: { wallet: walletResult.walletBalancesJson },
1097
- table: () => {
1192
+ render: () => {
1098
1193
  renderOnce(
1099
1194
  /* @__PURE__ */ jsx3(
1100
1195
  Table,
@@ -1108,7 +1203,7 @@ function renderWallet(json, walletResult) {
1108
1203
  }
1109
1204
  });
1110
1205
  }
1111
- function renderCoins(json, balances, total, sort) {
1206
+ function renderCoins(json, balances, total, sort, limit, pageInfo) {
1112
1207
  const rankedBalances = balances.map((balance, index) => ({
1113
1208
  ...balance,
1114
1209
  rank: index + 1
@@ -1117,14 +1212,16 @@ function renderCoins(json, balances, total, sort) {
1117
1212
  json: {
1118
1213
  coins: rankedBalances.map(
1119
1214
  (balance) => formatBalanceJson(balance, balance.rank)
1120
- )
1215
+ ),
1216
+ pageInfo: pageInfo ?? null
1121
1217
  },
1122
- table: () => {
1218
+ render: () => {
1123
1219
  if (balances.length === 0) {
1124
1220
  console.log("\n No coin balances found.\n");
1125
1221
  console.log(" Buy coins to see them here:");
1126
1222
  console.log(" zora buy <address> --eth 0.001\n");
1127
1223
  } else {
1224
+ const footer = pageInfo?.hasNextPage && pageInfo.endCursor ? `Next page: zora balance coins --sort ${sort} --limit ${limit} --after ${pageInfo.endCursor}` : void 0;
1128
1225
  renderOnce(
1129
1226
  /* @__PURE__ */ jsx3(
1130
1227
  Table,
@@ -1132,7 +1229,8 @@ function renderCoins(json, balances, total, sort) {
1132
1229
  columns: balanceColumns,
1133
1230
  data: rankedBalances,
1134
1231
  title: `Coins \xB7 sorted by ${SORT_LABELS[sort]}`,
1135
- subtitle: `${balances.length} of ${total}`
1232
+ subtitle: `${balances.length} of ${total}`,
1233
+ footer
1136
1234
  }
1137
1235
  )
1138
1236
  );
@@ -1140,19 +1238,17 @@ function renderCoins(json, balances, total, sort) {
1140
1238
  }
1141
1239
  });
1142
1240
  }
1143
- async function fetchCoins(json, address, sort, limit) {
1241
+ async function fetchCoins(json, address, sort, limit, after) {
1144
1242
  let response;
1145
1243
  try {
1146
1244
  response = await getProfileBalances({
1147
1245
  identifier: address,
1148
1246
  count: limit,
1149
- sortOption: SORT_MAP[sort]
1247
+ sortOption: SORT_MAP[sort],
1248
+ after
1150
1249
  });
1151
1250
  } catch (err) {
1152
- outputErrorAndExit(
1153
- json,
1154
- `Request failed: ${err instanceof Error ? err.message : String(err)}`
1155
- );
1251
+ outputErrorAndExit(json, `Request failed: ${apiErrorMessage(err)}`);
1156
1252
  }
1157
1253
  if (response.error) {
1158
1254
  outputErrorAndExit(
@@ -1165,7 +1261,8 @@ async function fetchCoins(json, address, sort, limit) {
1165
1261
  (e) => e.node
1166
1262
  );
1167
1263
  const total = response.data?.profile?.coinBalances?.count ?? balances.length;
1168
- return { balances, total };
1264
+ const pageInfo = response.data?.profile?.coinBalances?.pageInfo;
1265
+ return { balances, total, pageInfo };
1169
1266
  }
1170
1267
  function validateCoinOpts(json, sort, limitStr) {
1171
1268
  if (!SORT_MAP[sort]) {
@@ -1184,11 +1281,15 @@ function validateCoinOpts(json, sort, limitStr) {
1184
1281
  }
1185
1282
  return { sort, limit };
1186
1283
  }
1187
- var balanceCommand = new Command2("balance").description("Show balances in your wallet").action(async function() {
1284
+ var balanceCommand = new Command2("balance").description("Show balances in your wallet").option("--live", "Interactive live-updating display (default)").option("--static", "Static snapshot").option(
1285
+ "--refresh <seconds>",
1286
+ "Auto-refresh interval in seconds, requires --live (min 5)",
1287
+ "30"
1288
+ ).action(async function() {
1188
1289
  const output = getOutputMode(this, "live");
1189
1290
  const json = output === "json";
1190
1291
  const account = resolveContext(json);
1191
- const { live, intervalSeconds } = getLiveConfig(this, "live");
1292
+ const { live, intervalSeconds } = getLiveConfig(this, output);
1192
1293
  const sort = "usd-value";
1193
1294
  const limit = 10;
1194
1295
  const fetchBalanceData = async () => {
@@ -1198,7 +1299,7 @@ var balanceCommand = new Command2("balance").description("Show balances in your
1198
1299
  ]);
1199
1300
  if (walletResult.status === "rejected" || coinsResult.status === "rejected") {
1200
1301
  const err = walletResult.status === "rejected" ? walletResult.reason : coinsResult.reason;
1201
- throw new Error(err instanceof Error ? err.message : String(err));
1302
+ throw err instanceof Error ? err : new Error(String(err));
1202
1303
  }
1203
1304
  const rankedBalances = coinsResult.value.balances.map(
1204
1305
  (balance, index) => ({
@@ -1215,10 +1316,7 @@ var balanceCommand = new Command2("balance").description("Show balances in your
1215
1316
  };
1216
1317
  if (json) {
1217
1318
  const data = await fetchBalanceData().catch(
1218
- (err) => outputErrorAndExit(
1219
- json,
1220
- `Request failed: ${err instanceof Error ? err.message : String(err)}`
1221
- )
1319
+ (err) => outputErrorAndExit(json, `Request failed: ${apiErrorMessage(err)}`)
1222
1320
  );
1223
1321
  outputData(json, {
1224
1322
  json: {
@@ -1227,7 +1325,7 @@ var balanceCommand = new Command2("balance").description("Show balances in your
1227
1325
  (balance) => formatBalanceJson(balance, balance.rank)
1228
1326
  )
1229
1327
  },
1230
- table: () => {
1328
+ render: () => {
1231
1329
  }
1232
1330
  });
1233
1331
  track("cli_balances", {
@@ -1260,10 +1358,7 @@ var balanceCommand = new Command2("balance").description("Show balances in your
1260
1358
  });
1261
1359
  } else {
1262
1360
  const data = await fetchBalanceData().catch(
1263
- (err) => outputErrorAndExit(
1264
- json,
1265
- `Request failed: ${err instanceof Error ? err.message : String(err)}`
1266
- )
1361
+ (err) => outputErrorAndExit(json, `Request failed: ${apiErrorMessage(err)}`)
1267
1362
  );
1268
1363
  renderOnce(
1269
1364
  /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
@@ -1311,15 +1406,19 @@ var balanceCommand = new Command2("balance").description("Show balances in your
1311
1406
  live: false,
1312
1407
  result_count: data.rankedBalances.length,
1313
1408
  total_count: data.total,
1314
- output_format: "text"
1409
+ output_format: "static"
1315
1410
  });
1316
1411
  }
1317
1412
  });
1318
- balanceCommand.command("spendable").description("Show wallet token balances (ETH, USDC, ZORA)").action(async function() {
1413
+ balanceCommand.command("spendable").description("Show wallet token balances (ETH, USDC, ZORA)").option("--live", "Interactive live-updating display (default)").option("--static", "Static snapshot").option(
1414
+ "--refresh <seconds>",
1415
+ "Auto-refresh interval in seconds, requires --live (min 5)",
1416
+ "30"
1417
+ ).action(async function() {
1319
1418
  const output = getOutputMode(this, "live");
1320
1419
  const json = output === "json";
1321
1420
  const account = resolveContext(json);
1322
- const { live, intervalSeconds } = getLiveConfig(this, "live");
1421
+ const { live, intervalSeconds } = getLiveConfig(this, output);
1323
1422
  const fetchSpendableData = async () => {
1324
1423
  const walletResult = await fetchWalletBalances(account.address);
1325
1424
  return {
@@ -1331,14 +1430,11 @@ balanceCommand.command("spendable").description("Show wallet token balances (ETH
1331
1430
  };
1332
1431
  if (json) {
1333
1432
  const data = await fetchSpendableData().catch(
1334
- (err) => outputErrorAndExit(
1335
- json,
1336
- `Request failed: ${err instanceof Error ? err.message : String(err)}`
1337
- )
1433
+ (err) => outputErrorAndExit(json, `Request failed: ${apiErrorMessage(err)}`)
1338
1434
  );
1339
1435
  outputData(json, {
1340
1436
  json: { wallet: data.walletBalancesJson },
1341
- table: () => {
1437
+ render: () => {
1342
1438
  }
1343
1439
  });
1344
1440
  } else if (live) {
@@ -1356,26 +1452,29 @@ balanceCommand.command("spendable").description("Show wallet token balances (ETH
1356
1452
  );
1357
1453
  } else {
1358
1454
  const walletResult = await fetchWalletBalances(account.address).catch(
1359
- (err) => outputErrorAndExit(
1360
- json,
1361
- `Request failed: ${err instanceof Error ? err.message : String(err)}`
1362
- )
1455
+ (err) => outputErrorAndExit(json, `Request failed: ${apiErrorMessage(err)}`)
1363
1456
  );
1364
1457
  renderWallet(json, walletResult);
1365
1458
  }
1366
1459
  });
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) {
1460
+ 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(
1461
+ "--refresh <seconds>",
1462
+ "Auto-refresh interval in seconds, requires --live (min 5)",
1463
+ "30"
1464
+ ).option("--after <cursor>", "Pagination cursor from a previous result").action(async function(opts) {
1368
1465
  const output = getOutputMode(this, "live");
1369
1466
  const json = output === "json";
1370
1467
  const { sort, limit } = validateCoinOpts(json, opts.sort, opts.limit);
1468
+ const after = opts.after;
1371
1469
  const account = resolveContext(json);
1372
- const { live, intervalSeconds } = getLiveConfig(this, "live");
1373
- const fetchCoinsData = async () => {
1374
- const { balances, total } = await fetchCoins(
1470
+ const { live, intervalSeconds } = getLiveConfig(this, output);
1471
+ const fetchCoinsPage = async (cursor) => {
1472
+ const { balances, total, pageInfo } = await fetchCoins(
1375
1473
  json,
1376
1474
  account.address,
1377
1475
  sort,
1378
- limit
1476
+ limit,
1477
+ cursor
1379
1478
  );
1380
1479
  const rankedBalances = balances.map((balance, index) => ({
1381
1480
  ...balance,
@@ -1385,33 +1484,43 @@ balanceCommand.command("coins").description("Show coin positions").option("--sor
1385
1484
  walletBalances: [],
1386
1485
  walletBalancesJson: [],
1387
1486
  rankedBalances,
1388
- total
1487
+ total,
1488
+ pageInfo
1389
1489
  };
1390
1490
  };
1391
1491
  if (json) {
1392
- const data = await fetchCoinsData();
1393
- renderCoins(json, data.rankedBalances, data.total, sort);
1492
+ const data = await fetchCoinsPage(after);
1493
+ renderCoins(
1494
+ json,
1495
+ data.rankedBalances,
1496
+ data.total,
1497
+ sort,
1498
+ limit,
1499
+ data.pageInfo
1500
+ );
1394
1501
  } else if (live) {
1395
1502
  await renderLive(
1396
1503
  /* @__PURE__ */ jsx3(
1397
1504
  BalanceView,
1398
1505
  {
1399
- fetchData: fetchCoinsData,
1506
+ fetchData: fetchCoinsPage,
1400
1507
  sort,
1401
1508
  mode: "coins",
1509
+ initialCursor: after,
1402
1510
  autoRefresh: live,
1403
1511
  intervalSeconds
1404
1512
  }
1405
1513
  )
1406
1514
  );
1407
1515
  } else {
1408
- const { balances, total } = await fetchCoins(
1516
+ const { balances, total, pageInfo } = await fetchCoins(
1409
1517
  json,
1410
1518
  account.address,
1411
1519
  sort,
1412
- limit
1520
+ limit,
1521
+ after
1413
1522
  );
1414
- renderCoins(json, balances, total, sort);
1523
+ renderCoins(json, balances, total, sort, limit, pageInfo);
1415
1524
  }
1416
1525
  });
1417
1526
 
@@ -1599,7 +1708,7 @@ var printTradeResult = (json, info) => {
1599
1708
  };
1600
1709
 
1601
1710
  // 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) {
1711
+ 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) {
1603
1712
  const json = getJson(this);
1604
1713
  const debug = opts.debug === true;
1605
1714
  if (!isAddress(coinAddress)) {
@@ -1638,10 +1747,7 @@ var buyCommand = new Command3("buy").description("Buy a coin").argument("<addres
1638
1747
  const response = await getCoin({ address: coinAddress });
1639
1748
  token = response.data?.zora20Token;
1640
1749
  } catch (err) {
1641
- outputErrorAndExit(
1642
- json,
1643
- `Failed to fetch coin: ${err instanceof Error ? err.message : String(err)}`
1644
- );
1750
+ outputErrorAndExit(json, `Failed to fetch coin: ${apiErrorMessage(err)}`);
1645
1751
  }
1646
1752
  if (!token) {
1647
1753
  outputErrorAndExit(json, `Coin not found: ${coinAddress}`);
@@ -1816,7 +1922,7 @@ ${err instanceof Error ? err.stack || err.message : String(err)}
1816
1922
  }
1817
1923
  outputErrorAndExit(
1818
1924
  json,
1819
- `Quote failed: ${msg}`,
1925
+ `Quote failed: ${apiErrorMessage(err)}`,
1820
1926
  "Check the coin address is valid and try again. Use --debug for full error details."
1821
1927
  );
1822
1928
  }
@@ -1849,7 +1955,7 @@ ${err instanceof Error ? err.stack || err.message : String(err)}
1849
1955
  valueUsd: swapAmountUsd,
1850
1956
  swapCoinType: token.coinType ?? null,
1851
1957
  slippage: slippagePct,
1852
- output_format: json ? "json" : "table"
1958
+ output_format: json ? "json" : "static"
1853
1959
  });
1854
1960
  return;
1855
1961
  }
@@ -1897,15 +2003,12 @@ ${err instanceof Error ? err.stack || err.message : String(err)}
1897
2003
  valueUsd: swapAmountUsd,
1898
2004
  swapCoinType,
1899
2005
  slippage: slippagePct,
1900
- output_format: json ? "json" : "table",
2006
+ output_format: json ? "json" : "static",
1901
2007
  success: false,
1902
2008
  error_type: err instanceof Error ? err.constructor.name : "unknown"
1903
2009
  });
1904
2010
  await shutdownAnalytics();
1905
- outputErrorAndExit(
1906
- json,
1907
- `Transaction failed: ${err instanceof Error ? err.message : String(err)}`
1908
- );
2011
+ outputErrorAndExit(json, tradeErrorMessage(err));
1909
2012
  }
1910
2013
  txHash = receipt.transactionHash;
1911
2014
  try {
@@ -1947,7 +2050,7 @@ ${err instanceof Error ? err.stack || err.message : String(err)}
1947
2050
  transactionHash: txHash,
1948
2051
  logIndex: swapLogIndex,
1949
2052
  slippage: slippagePct,
1950
- output_format: json ? "json" : "table",
2053
+ output_format: json ? "json" : "static",
1951
2054
  success: true,
1952
2055
  tx_hash: txHash
1953
2056
  });
@@ -2007,11 +2110,47 @@ var COIN_TYPE_DISPLAY = {
2007
2110
  import { useState as useState3, useEffect as useEffect3, useCallback as useCallback3, useRef as useRef2 } from "react";
2008
2111
  import { Box as Box4, Text as Text4, useInput as useInput2, useApp as useApp2 } from "ink";
2009
2112
  import Spinner2 from "ink-spinner";
2113
+
2114
+ // src/lib/clipboard.ts
2115
+ import { execFileSync } from "child_process";
2116
+ import { platform as platform2 } from "os";
2117
+ var copyToClipboard = (text) => {
2118
+ const os = platform2();
2119
+ try {
2120
+ if (os === "darwin") {
2121
+ execFileSync("pbcopy", {
2122
+ input: text,
2123
+ stdio: ["pipe", "ignore", "ignore"]
2124
+ });
2125
+ } else if (os === "linux") {
2126
+ execFileSync("xclip", ["-selection", "clipboard"], {
2127
+ input: text,
2128
+ stdio: ["pipe", "ignore", "ignore"]
2129
+ });
2130
+ } else if (os === "win32") {
2131
+ execFileSync("clip", {
2132
+ input: text,
2133
+ stdio: ["pipe", "ignore", "ignore"]
2134
+ });
2135
+ } else {
2136
+ return false;
2137
+ }
2138
+ return true;
2139
+ } catch {
2140
+ return false;
2141
+ }
2142
+ };
2143
+
2144
+ // src/components/ExploreView.tsx
2010
2145
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
2011
2146
  var COLUMNS = [
2012
2147
  { header: "#", width: 4, accessor: (c) => String(c.rank) },
2013
2148
  { header: "Name", width: 20, accessor: (c) => c.name ?? "Unknown" },
2014
- { header: "Address", width: 44, accessor: (c) => c.address ?? "" },
2149
+ {
2150
+ header: "Address",
2151
+ width: 14,
2152
+ accessor: (c) => c.address ? truncateAddress(c.address) : ""
2153
+ },
2015
2154
  {
2016
2155
  header: "Type",
2017
2156
  width: 14,
@@ -2061,6 +2200,11 @@ var ExploreView = ({
2061
2200
  const cache = useRef2(/* @__PURE__ */ new Map());
2062
2201
  const { refreshCount, secondsUntilRefresh, triggerManualRefresh } = useAutoRefresh(intervalSeconds, autoRefresh);
2063
2202
  const [manualRefreshCount, setManualRefreshCount] = useState3(0);
2203
+ const [selectedRow, setSelectedRow] = useState3(0);
2204
+ const [copyFeedback, setCopyFeedback] = useState3(null);
2205
+ useEffect3(() => {
2206
+ setSelectedRow((r) => Math.min(r, Math.max(0, coins.length - 1)));
2207
+ }, [coins.length]);
2064
2208
  const loadPage = useCallback3(
2065
2209
  async (cursor) => {
2066
2210
  const cacheKey = cursor ?? CACHE_KEY_FIRST;
@@ -2101,24 +2245,44 @@ var ExploreView = ({
2101
2245
  return;
2102
2246
  }
2103
2247
  if (loading) return;
2248
+ if (key.upArrow || input === "k") {
2249
+ setSelectedRow((r) => Math.max(0, r - 1));
2250
+ return;
2251
+ }
2252
+ if (key.downArrow || input === "j") {
2253
+ setSelectedRow((r) => Math.min(coins.length - 1, r + 1));
2254
+ return;
2255
+ }
2256
+ if (input === "c") {
2257
+ const coin = coins[selectedRow];
2258
+ if (coin?.address) {
2259
+ const ok = copyToClipboard(coin.address);
2260
+ setCopyFeedback(ok ? "Copied!" : "Copy failed");
2261
+ setTimeout(() => setCopyFeedback(null), 1500);
2262
+ }
2263
+ return;
2264
+ }
2104
2265
  const canGoNext = pageInfo?.hasNextPage && pageInfo.endCursor;
2105
2266
  const canGoPrev = cursorHistory.length > 0;
2106
2267
  if ((input === "n" || key.rightArrow) && canGoNext) {
2107
2268
  setCursorHistory((prev) => [...prev, currentCursor]);
2108
2269
  setCurrentCursor(pageInfo.endCursor);
2109
2270
  setPage((p) => p + 1);
2271
+ setSelectedRow(0);
2110
2272
  }
2111
2273
  if ((input === "p" || key.leftArrow) && canGoPrev) {
2112
2274
  const prev = cursorHistory[cursorHistory.length - 1];
2113
2275
  setCursorHistory((h) => h.slice(0, -1));
2114
2276
  setCurrentCursor(prev);
2115
2277
  setPage((p) => p - 1);
2278
+ setSelectedRow(0);
2116
2279
  }
2117
2280
  if (input === "r") {
2118
2281
  const cacheKey = currentCursor ?? CACHE_KEY_FIRST;
2119
2282
  cache.current.delete(cacheKey);
2120
2283
  triggerManualRefresh();
2121
2284
  setManualRefreshCount((c) => c + 1);
2285
+ setSelectedRow(0);
2122
2286
  }
2123
2287
  });
2124
2288
  if (error) {
@@ -2172,12 +2336,14 @@ var ExploreView = ({
2172
2336
  rank: (page - 1) * limit + i + 1
2173
2337
  }));
2174
2338
  const hints = [];
2339
+ hints.push("\u2191\u2193 select");
2340
+ hints.push("c copy address");
2175
2341
  if (cursorHistory.length > 0) hints.push("\u2190 prev");
2176
2342
  if (pageInfo?.hasNextPage) hints.push("\u2192 next");
2177
2343
  hints.push("r refresh");
2178
2344
  if (autoRefresh) hints.push(`auto: ${secondsUntilRefresh}s`);
2179
2345
  hints.push("q quit");
2180
- const footer = hints.join(" \xB7 ");
2346
+ const footer = hints.join(" \xB7 ") + (copyFeedback ? ` ${copyFeedback}` : "");
2181
2347
  return /* @__PURE__ */ jsx4(
2182
2348
  Table,
2183
2349
  {
@@ -2185,7 +2351,8 @@ var ExploreView = ({
2185
2351
  columns: COLUMNS,
2186
2352
  title,
2187
2353
  subtitle,
2188
- footer
2354
+ footer,
2355
+ selectedRow
2189
2356
  }
2190
2357
  );
2191
2358
  };
@@ -2231,12 +2398,42 @@ var QUERY_MAP = {
2231
2398
  post: getExploreFeaturedVideos
2232
2399
  }
2233
2400
  };
2401
+ var STATIC_COLUMNS = [
2402
+ { header: "#", width: 4, accessor: (c) => String(c.rank) },
2403
+ { header: "Name", width: 20, accessor: (c) => c.name ?? "Unknown" },
2404
+ { header: "Address", width: 48, accessor: (c) => c.address ?? "" },
2405
+ {
2406
+ header: "Type",
2407
+ width: 14,
2408
+ accessor: (c) => COIN_TYPE_DISPLAY[c.coinType ?? ""] ?? c.coinType ?? ""
2409
+ },
2410
+ {
2411
+ header: "Market Cap",
2412
+ width: 12,
2413
+ accessor: (c) => formatCompactUsd(c.marketCap)
2414
+ },
2415
+ {
2416
+ header: "24h Vol",
2417
+ width: 12,
2418
+ accessor: (c) => formatCompactUsd(c.volume24h)
2419
+ },
2420
+ {
2421
+ header: "24h Change",
2422
+ width: 11,
2423
+ accessor: (c) => formatMcapChange(c.marketCap, c.marketCapDelta24h).text,
2424
+ color: (c) => formatMcapChange(c.marketCap, c.marketCapDelta24h).color
2425
+ }
2426
+ ];
2234
2427
  var SORT_OPTIONS2 = Object.keys(SORT_LABELS2).join(", ");
2235
2428
  var exploreCommand = new Command4("explore").description("Browse top, new, and highest volume coins").option("--sort <sort>", `Sort by: ${SORT_OPTIONS2}`, "mcap").option(
2236
2429
  "--type <type>",
2237
2430
  "Filter by type: all, trend, creator-coin, post (availability varies by sort)",
2238
2431
  "post"
2239
- ).option("--limit <n>", "Number of results (max 20)", "10").option("--after <cursor>", "Pagination cursor from a previous result").action(async function(opts) {
2432
+ ).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(
2433
+ "--refresh <seconds>",
2434
+ "Auto-refresh interval in seconds, requires --live (min 5)",
2435
+ "30"
2436
+ ).action(async function(opts) {
2240
2437
  const output = getOutputMode(this, "live");
2241
2438
  const json = output === "json";
2242
2439
  const sort = opts.sort;
@@ -2275,10 +2472,7 @@ var exploreCommand = new Command4("explore").description("Browse top, new, and h
2275
2472
  try {
2276
2473
  response = await queryFn({ count: limit, after });
2277
2474
  } catch (err) {
2278
- outputErrorAndExit(
2279
- json,
2280
- `Request failed: ${err instanceof Error ? err.message : String(err)}`
2281
- );
2475
+ outputErrorAndExit(json, `Request failed: ${apiErrorMessage(err)}`);
2282
2476
  }
2283
2477
  if (response.error) {
2284
2478
  const msg = typeof response.error === "object" && response.error.error ? response.error.error : JSON.stringify(response.error);
@@ -2298,7 +2492,7 @@ var exploreCommand = new Command4("explore").description("Browse top, new, and h
2298
2492
  output_format: "json"
2299
2493
  });
2300
2494
  } else {
2301
- const { live, intervalSeconds } = getLiveConfig(this, "live");
2495
+ const { live, intervalSeconds } = getLiveConfig(this, output);
2302
2496
  const fetchPage = async (cursor) => {
2303
2497
  const response = await queryFn({ count: limit, after: cursor });
2304
2498
  if (response.error) {
@@ -2310,29 +2504,55 @@ var exploreCommand = new Command4("explore").description("Browse top, new, and h
2310
2504
  const pageInfo = response.data?.exploreList?.pageInfo;
2311
2505
  return { coins, pageInfo };
2312
2506
  };
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
- });
2507
+ if (live) {
2508
+ await renderLive(
2509
+ /* @__PURE__ */ jsx5(
2510
+ ExploreView,
2511
+ {
2512
+ fetchPage,
2513
+ sort,
2514
+ type,
2515
+ limit,
2516
+ initialCursor: after,
2517
+ autoRefresh: live,
2518
+ intervalSeconds
2519
+ }
2520
+ )
2521
+ );
2522
+ track("cli_explore", {
2523
+ sort,
2524
+ type,
2525
+ limit,
2526
+ live,
2527
+ interval: intervalSeconds,
2528
+ paginated: after !== void 0,
2529
+ output_format: "live"
2530
+ });
2531
+ } else {
2532
+ const { coins } = await fetchPage(after).catch(
2533
+ (err) => outputErrorAndExit(
2534
+ false,
2535
+ `Request failed: ${err instanceof Error ? err.message : String(err)}`
2536
+ )
2537
+ );
2538
+ const title = type !== "all" ? `${SORT_LABELS2[sort]} \xB7 ${TYPE_LABELS[type]}` : SORT_LABELS2[sort];
2539
+ const rankedCoins = coins.map((c, i) => ({
2540
+ ...c,
2541
+ rank: i + 1
2542
+ }));
2543
+ renderOnce(
2544
+ /* @__PURE__ */ jsx5(Table, { columns: STATIC_COLUMNS, data: rankedCoins, title })
2545
+ );
2546
+ track("cli_explore", {
2547
+ sort,
2548
+ type,
2549
+ limit,
2550
+ live: false,
2551
+ paginated: after !== void 0,
2552
+ result_count: coins.length,
2553
+ output_format: "static"
2554
+ });
2555
+ }
2336
2556
  }
2337
2557
  });
2338
2558
 
@@ -2483,7 +2703,7 @@ function formatCoinJson(coin) {
2483
2703
  };
2484
2704
  }
2485
2705
  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) {
2706
+ 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) {
2487
2707
  const json = getJson(this);
2488
2708
  if (opts.type !== void 0 && !VALID_TYPES.includes(opts.type)) {
2489
2709
  outputErrorAndExit(
@@ -2509,10 +2729,7 @@ var getCommand = new Command5("get").description("Look up a coin by address or n
2509
2729
  try {
2510
2730
  result = await resolveCoin(ref);
2511
2731
  } catch (err) {
2512
- outputErrorAndExit(
2513
- json,
2514
- `Request failed: ${err instanceof Error ? err.message : String(err)}`
2515
- );
2732
+ outputErrorAndExit(json, `Request failed: ${apiErrorMessage(err)}`);
2516
2733
  return;
2517
2734
  }
2518
2735
  if (type && result.kind === "found" && result.coin.coinType !== type) {
@@ -2529,7 +2746,7 @@ var getCommand = new Command5("get").description("Look up a coin by address or n
2529
2746
  }
2530
2747
  outputData(json, {
2531
2748
  json: formatCoinJson(result.coin),
2532
- table: () => {
2749
+ render: () => {
2533
2750
  renderOnce(/* @__PURE__ */ jsx7(CoinDetail, { coin: result.coin }));
2534
2751
  }
2535
2752
  });
@@ -2651,7 +2868,7 @@ var fetchPriceHistory = async (address, interval) => {
2651
2868
  price: Number(p.closePrice)
2652
2869
  }));
2653
2870
  };
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(
2871
+ 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
2872
  "--interval <interval>",
2656
2873
  `Time range: ${VALID_INTERVALS.join(", ")}`,
2657
2874
  "1w"
@@ -2740,7 +2957,7 @@ var priceHistoryCommand = new Command6("price-history").description("Display pri
2740
2957
  price: p.price
2741
2958
  }))
2742
2959
  },
2743
- table: () => {
2960
+ render: () => {
2744
2961
  renderOnce(
2745
2962
  /* @__PURE__ */ jsx9(
2746
2963
  PriceHistory,
@@ -2853,13 +3070,13 @@ function printSellResult(output, info) {
2853
3070
  console.log(` Tx ${info.txHash}
2854
3071
  `);
2855
3072
  }
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) {
3073
+ 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) {
2857
3074
  const json = getJson(this);
2858
3075
  const debug = opts.debug === true;
2859
3076
  if (!isAddress2(coinAddress)) {
2860
3077
  outputErrorAndExit(json, `Invalid address: ${coinAddress}`);
2861
3078
  }
2862
- const output = json ? "json" : "table";
3079
+ const output = json ? "json" : "static";
2863
3080
  const outputAsset = opts.token ? opts.token.toLowerCase() : opts.to;
2864
3081
  if (!(outputAsset in BASE_TRADE_TOKENS)) {
2865
3082
  outputErrorAndExit(
@@ -2893,10 +3110,7 @@ var sellCommand = new Command7("sell").description("Sell a coin").argument("<add
2893
3110
  const response = await getCoin3({ address: coinAddress });
2894
3111
  token = response.data?.zora20Token;
2895
3112
  } catch (err) {
2896
- outputErrorAndExit(
2897
- json,
2898
- `Failed to fetch coin: ${err instanceof Error ? err.message : String(err)}`
2899
- );
3113
+ outputErrorAndExit(json, `Failed to fetch coin: ${apiErrorMessage(err)}`);
2900
3114
  }
2901
3115
  if (!token) {
2902
3116
  outputErrorAndExit(json, `Coin not found: ${coinAddress}`);
@@ -3037,7 +3251,7 @@ ${err instanceof Error ? err.stack || err.message : String(err)}
3037
3251
  }
3038
3252
  outputErrorAndExit(
3039
3253
  json,
3040
- `Quote failed: ${msg}`,
3254
+ `Quote failed: ${apiErrorMessage(err)}`,
3041
3255
  "Check the coin address and amount, then try again. Use --debug for full error details."
3042
3256
  );
3043
3257
  }
@@ -3076,7 +3290,7 @@ ${err instanceof Error ? err.stack || err.message : String(err)}
3076
3290
  return;
3077
3291
  }
3078
3292
  if (!opts.yes) {
3079
- printSellQuote("table", {
3293
+ printSellQuote("static", {
3080
3294
  coinName,
3081
3295
  coinSymbol,
3082
3296
  address: coinAddress,
@@ -3128,10 +3342,7 @@ ${err instanceof Error ? err.stack || err.message : String(err)}
3128
3342
  error_type: err instanceof Error ? err.constructor.name : "unknown"
3129
3343
  });
3130
3344
  await shutdownAnalytics();
3131
- outputErrorAndExit(
3132
- json,
3133
- `Transaction failed: ${err instanceof Error ? err.message : String(err)}`
3134
- );
3345
+ outputErrorAndExit(json, tradeErrorMessage(err));
3135
3346
  }
3136
3347
  txHash = receipt.transactionHash;
3137
3348
  if (outputToken.trade.type === "erc20") {
@@ -3179,8 +3390,402 @@ ${err instanceof Error ? err.stack || err.message : String(err)}
3179
3390
  });
3180
3391
  });
3181
3392
 
3182
- // src/commands/send.ts
3393
+ // src/commands/profile.tsx
3183
3394
  import { Command as Command8 } from "commander";
3395
+ import { Box as Box8, Text as Text8 } from "ink";
3396
+ import {
3397
+ getProfileCoins,
3398
+ getProfileBalances as getProfileBalances2,
3399
+ setApiKey as setApiKey7
3400
+ } from "@zoralabs/coins-sdk";
3401
+ import { privateKeyToAccount as privateKeyToAccount3 } from "viem/accounts";
3402
+
3403
+ // src/components/ProfileView.tsx
3404
+ import { useState as useState4, useEffect as useEffect4, useCallback as useCallback4, useRef as useRef3 } from "react";
3405
+ import { Box as Box7, Text as Text7, useInput as useInput3, useApp as useApp3 } from "ink";
3406
+ import Spinner3 from "ink-spinner";
3407
+ import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
3408
+ var TAB_NAMES = ["Posts", "Holdings"];
3409
+ var postColumns = [
3410
+ { header: "#", width: 4, accessor: (c) => String(c.rank) },
3411
+ { header: "Name", width: 20, accessor: (c) => c.name ?? "Unknown" },
3412
+ {
3413
+ header: "Type",
3414
+ width: 14,
3415
+ accessor: (c) => COIN_TYPE_DISPLAY[c.coinType ?? ""] ?? c.coinType ?? ""
3416
+ },
3417
+ {
3418
+ header: "Market Cap",
3419
+ width: 12,
3420
+ accessor: (c) => formatCompactUsd(c.marketCap)
3421
+ },
3422
+ {
3423
+ header: "24h Vol",
3424
+ width: 12,
3425
+ accessor: (c) => formatCompactUsd(c.volume24h)
3426
+ },
3427
+ {
3428
+ header: "24h Change",
3429
+ width: 11,
3430
+ accessor: (c) => formatMcapChange(c.marketCap, c.marketCapDelta24h).text,
3431
+ color: (c) => formatMcapChange(c.marketCap, c.marketCapDelta24h).color
3432
+ },
3433
+ {
3434
+ header: "Created",
3435
+ width: 16,
3436
+ accessor: (c) => {
3437
+ if (!c.createdAt) return "-";
3438
+ const date = new Date(c.createdAt);
3439
+ if (isNaN(date.getTime())) return "-";
3440
+ return formatRelativeTime(date);
3441
+ }
3442
+ }
3443
+ ];
3444
+ var ProfileView = ({
3445
+ fetchData,
3446
+ identifier,
3447
+ autoRefresh = false,
3448
+ intervalSeconds = 30
3449
+ }) => {
3450
+ const { exit } = useApp3();
3451
+ const [activeTab, setActiveTab] = useState4(0);
3452
+ const [loading, setLoading] = useState4(true);
3453
+ const [isRefreshing, setIsRefreshing] = useState4(false);
3454
+ const [error, setError] = useState4(null);
3455
+ const [data, setData] = useState4(null);
3456
+ const { refreshCount, secondsUntilRefresh, triggerManualRefresh } = useAutoRefresh(intervalSeconds, autoRefresh);
3457
+ const [manualRefreshCount, setManualRefreshCount] = useState4(0);
3458
+ const hasLoadedOnce = useRef3(false);
3459
+ const load = useCallback4(async () => {
3460
+ if (hasLoadedOnce.current) {
3461
+ setIsRefreshing(true);
3462
+ } else {
3463
+ setLoading(true);
3464
+ }
3465
+ setError(null);
3466
+ try {
3467
+ const result = await fetchData();
3468
+ setData(result);
3469
+ hasLoadedOnce.current = true;
3470
+ } catch (err) {
3471
+ setError(err instanceof Error ? err.message : String(err));
3472
+ }
3473
+ setLoading(false);
3474
+ setIsRefreshing(false);
3475
+ }, [fetchData]);
3476
+ useEffect4(() => {
3477
+ load();
3478
+ }, [load, refreshCount, manualRefreshCount]);
3479
+ useInput3((input, key) => {
3480
+ if (input === "q" || key.escape) {
3481
+ exit();
3482
+ return;
3483
+ }
3484
+ if (input === "r" && !loading) {
3485
+ triggerManualRefresh();
3486
+ setManualRefreshCount((c) => c + 1);
3487
+ }
3488
+ if (key.leftArrow || input === "1") {
3489
+ setActiveTab(0);
3490
+ }
3491
+ if (key.rightArrow || input === "2") {
3492
+ setActiveTab(1);
3493
+ }
3494
+ });
3495
+ if (error && !data) {
3496
+ return /* @__PURE__ */ jsxs7(
3497
+ Box7,
3498
+ {
3499
+ flexDirection: "column",
3500
+ paddingLeft: 1,
3501
+ paddingTop: 1,
3502
+ paddingBottom: 1,
3503
+ children: [
3504
+ /* @__PURE__ */ jsxs7(Text7, { color: "red", children: [
3505
+ "Error: ",
3506
+ error
3507
+ ] }),
3508
+ /* @__PURE__ */ jsx10(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx10(Text7, { dimColor: true, children: "Press q to exit" }) })
3509
+ ]
3510
+ }
3511
+ );
3512
+ }
3513
+ if (loading && !data) {
3514
+ return /* @__PURE__ */ jsx10(Box7, { paddingLeft: 1, paddingTop: 1, children: /* @__PURE__ */ jsxs7(Text7, { children: [
3515
+ /* @__PURE__ */ jsx10(Spinner3, { type: "dots" }),
3516
+ " Loading profile\u2026"
3517
+ ] }) });
3518
+ }
3519
+ if (!data) return null;
3520
+ const hints = ["\u2190 \u2192 switch tab", "r refresh"];
3521
+ if (autoRefresh) hints.push(`auto: ${secondsUntilRefresh}s`);
3522
+ hints.push("q quit");
3523
+ const footer = hints.join(" \xB7 ");
3524
+ const rankedPosts = data.posts.map((p, i) => ({ ...p, rank: i + 1 }));
3525
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
3526
+ isRefreshing && /* @__PURE__ */ jsx10(Box7, { paddingLeft: 1, children: /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
3527
+ /* @__PURE__ */ jsx10(Spinner3, { type: "dots" }),
3528
+ " Refreshing\u2026"
3529
+ ] }) }),
3530
+ /* @__PURE__ */ jsxs7(Box7, { paddingLeft: 1, paddingTop: 1, gap: 2, children: [
3531
+ TAB_NAMES.map((name, i) => /* @__PURE__ */ jsx10(Text7, { bold: activeTab === i, dimColor: activeTab !== i, children: activeTab === i ? `[${name}]` : name }, name)),
3532
+ /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
3533
+ " ",
3534
+ identifier
3535
+ ] })
3536
+ ] }),
3537
+ activeTab === 0 ? rankedPosts.length === 0 ? /* @__PURE__ */ jsx10(
3538
+ Box7,
3539
+ {
3540
+ flexDirection: "column",
3541
+ paddingLeft: 1,
3542
+ paddingTop: 1,
3543
+ paddingBottom: 1,
3544
+ children: /* @__PURE__ */ jsx10(Text7, { children: "No posts found for this profile." })
3545
+ }
3546
+ ) : /* @__PURE__ */ jsx10(
3547
+ Table,
3548
+ {
3549
+ columns: postColumns,
3550
+ data: rankedPosts,
3551
+ title: "Posts",
3552
+ subtitle: `${rankedPosts.length} of ${data.postsCount}`
3553
+ }
3554
+ ) : data.holdings.length === 0 ? /* @__PURE__ */ jsx10(
3555
+ Box7,
3556
+ {
3557
+ flexDirection: "column",
3558
+ paddingLeft: 1,
3559
+ paddingTop: 1,
3560
+ paddingBottom: 1,
3561
+ children: /* @__PURE__ */ jsx10(Text7, { children: "No holdings found for this profile." })
3562
+ }
3563
+ ) : /* @__PURE__ */ jsx10(
3564
+ Table,
3565
+ {
3566
+ columns: balanceColumns,
3567
+ data: data.holdings,
3568
+ title: "Holdings",
3569
+ subtitle: `${data.holdings.length} of ${data.holdingsCount}`
3570
+ }
3571
+ ),
3572
+ /* @__PURE__ */ jsx10(Box7, { paddingLeft: 1, paddingBottom: 1, children: /* @__PURE__ */ jsx10(Text7, { dimColor: true, children: footer }) })
3573
+ ] });
3574
+ };
3575
+
3576
+ // src/commands/profile.tsx
3577
+ import { jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
3578
+ var extractErrorMessage2 = (error) => {
3579
+ if (typeof error === "object" && error !== null && "error" in error) {
3580
+ return String(error.error);
3581
+ }
3582
+ return JSON.stringify(error);
3583
+ };
3584
+ var resolveApiKey = (json) => {
3585
+ const apiKey = getApiKey();
3586
+ if (!apiKey) {
3587
+ outputErrorAndExit(
3588
+ json,
3589
+ "Not authenticated. Run 'zora auth configure' to set your API key."
3590
+ );
3591
+ }
3592
+ setApiKey7(apiKey);
3593
+ };
3594
+ var formatPostJson = (post, rank) => ({
3595
+ rank,
3596
+ name: post.name,
3597
+ symbol: post.symbol,
3598
+ coinType: COIN_TYPE_DISPLAY[post.coinType] ?? post.coinType,
3599
+ address: post.address,
3600
+ marketCap: post.marketCap ?? null,
3601
+ marketCapDelta24h: post.marketCapDelta24h ?? null,
3602
+ volume24h: post.volume24h ?? null,
3603
+ createdAt: post.createdAt ?? null
3604
+ });
3605
+ var formatHoldingJson = (balance) => {
3606
+ const priceUsd = balance.coin?.tokenPrice?.priceInUsdc ? Number(balance.coin.tokenPrice.priceInUsdc) : null;
3607
+ const usdValue = priceUsd !== null ? Number((parseRawBalance(balance.balance) * priceUsd).toFixed(6)) : null;
3608
+ return {
3609
+ rank: balance.rank,
3610
+ name: balance.coin?.name ?? null,
3611
+ symbol: balance.coin?.symbol ?? null,
3612
+ coinType: balance.coin?.coinType ?? null,
3613
+ address: balance.coin?.address ?? null,
3614
+ balance: normalizeTokenAmount(balance.balance),
3615
+ usdValue,
3616
+ priceUsd,
3617
+ marketCap: balance.coin?.marketCap ? Number(balance.coin.marketCap) : null
3618
+ };
3619
+ };
3620
+ var fetchProfileData = async (identifier) => {
3621
+ const [postsResult, holdingsResult] = await Promise.allSettled([
3622
+ getProfileCoins({ identifier, count: 20 }),
3623
+ getProfileBalances2({ identifier, count: 20, sortOption: "USD_VALUE" })
3624
+ ]);
3625
+ if (postsResult.status === "rejected") {
3626
+ throw new Error(
3627
+ postsResult.reason instanceof Error ? postsResult.reason.message : String(postsResult.reason)
3628
+ );
3629
+ }
3630
+ if (holdingsResult.status === "rejected") {
3631
+ throw new Error(
3632
+ holdingsResult.reason instanceof Error ? holdingsResult.reason.message : String(holdingsResult.reason)
3633
+ );
3634
+ }
3635
+ if (postsResult.value.error) {
3636
+ throw new Error(
3637
+ `API error (posts): ${extractErrorMessage2(postsResult.value.error)}`
3638
+ );
3639
+ }
3640
+ if (holdingsResult.value.error) {
3641
+ throw new Error(
3642
+ `API error (holdings): ${extractErrorMessage2(holdingsResult.value.error)}`
3643
+ );
3644
+ }
3645
+ const postEdges = postsResult.value.data?.profile?.createdCoins?.edges ?? [];
3646
+ const posts = postEdges.map((e) => e.node);
3647
+ const postsCount = postsResult.value.data?.profile?.createdCoins?.count ?? posts.length;
3648
+ const holdingEdges = holdingsResult.value.data?.profile?.coinBalances?.edges ?? [];
3649
+ const holdings = holdingEdges.map(
3650
+ (e, i) => ({
3651
+ ...e.node,
3652
+ rank: i + 1
3653
+ })
3654
+ );
3655
+ const holdingsCount = holdingsResult.value.data?.profile?.coinBalances?.count ?? holdings.length;
3656
+ return { posts, postsCount, holdings, holdingsCount };
3657
+ };
3658
+ var profileCommand = new Command8("profile").description("View profile activity (posts and holdings)").argument(
3659
+ "[identifier]",
3660
+ "Wallet address or profile handle (defaults to your wallet)"
3661
+ ).option("--live", "Interactive live-updating display (default)").option("--static", "Static snapshot").option(
3662
+ "--refresh <seconds>",
3663
+ "Auto-refresh interval in seconds, requires --live (min 5)",
3664
+ "30"
3665
+ ).action(async function(identifierArg) {
3666
+ const output = getOutputMode(this, "live");
3667
+ const json = output === "json";
3668
+ resolveApiKey(json);
3669
+ const { live, intervalSeconds } = getLiveConfig(this, output);
3670
+ let identifier = identifierArg;
3671
+ if (!identifier) {
3672
+ const envKey = process.env.ZORA_PRIVATE_KEY;
3673
+ const key = envKey || getPrivateKey();
3674
+ if (!key) {
3675
+ outputErrorAndExit(
3676
+ json,
3677
+ "No identifier provided and no wallet configured.",
3678
+ "Pass an address or handle, or run 'zora setup' first."
3679
+ );
3680
+ }
3681
+ try {
3682
+ identifier = privateKeyToAccount3(normalizeKey(key)).address;
3683
+ } catch {
3684
+ outputErrorAndExit(
3685
+ json,
3686
+ "Invalid wallet key. Run 'zora setup --force' to replace it."
3687
+ );
3688
+ }
3689
+ }
3690
+ if (json) {
3691
+ const data = await fetchProfileData(identifier).catch(
3692
+ (err) => outputErrorAndExit(
3693
+ json,
3694
+ `Request failed: ${err instanceof Error ? err.message : String(err)}`
3695
+ )
3696
+ );
3697
+ outputData(json, {
3698
+ json: {
3699
+ posts: data.posts.map((p, i) => formatPostJson(p, i + 1)),
3700
+ holdings: data.holdings.map(formatHoldingJson)
3701
+ },
3702
+ render: () => {
3703
+ }
3704
+ });
3705
+ track("cli_profile", {
3706
+ identifier,
3707
+ output_format: "json",
3708
+ posts_count: data.postsCount,
3709
+ holdings_count: data.holdingsCount
3710
+ });
3711
+ } else if (live) {
3712
+ const fetchData = () => fetchProfileData(identifier);
3713
+ await renderLive(
3714
+ /* @__PURE__ */ jsx11(
3715
+ ProfileView,
3716
+ {
3717
+ fetchData,
3718
+ identifier,
3719
+ autoRefresh: live,
3720
+ intervalSeconds
3721
+ }
3722
+ )
3723
+ );
3724
+ track("cli_profile", {
3725
+ identifier,
3726
+ output_format: "live",
3727
+ live,
3728
+ interval: intervalSeconds
3729
+ });
3730
+ } else {
3731
+ const data = await fetchProfileData(identifier).catch(
3732
+ (err) => outputErrorAndExit(
3733
+ json,
3734
+ `Request failed: ${err instanceof Error ? err.message : String(err)}`
3735
+ )
3736
+ );
3737
+ const rankedPosts = data.posts.map((p, i) => ({ ...p, rank: i + 1 }));
3738
+ renderOnce(
3739
+ /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
3740
+ rankedPosts.length === 0 ? /* @__PURE__ */ jsx11(
3741
+ Box8,
3742
+ {
3743
+ flexDirection: "column",
3744
+ paddingLeft: 1,
3745
+ paddingTop: 1,
3746
+ paddingBottom: 1,
3747
+ children: /* @__PURE__ */ jsx11(Box8, { children: /* @__PURE__ */ jsx11(Text8, { children: "No posts found for this profile." }) })
3748
+ }
3749
+ ) : /* @__PURE__ */ jsx11(
3750
+ Table,
3751
+ {
3752
+ columns: postColumns,
3753
+ data: rankedPosts,
3754
+ title: "Posts",
3755
+ subtitle: `${rankedPosts.length} of ${data.postsCount}`
3756
+ }
3757
+ ),
3758
+ data.holdings.length === 0 ? /* @__PURE__ */ jsx11(
3759
+ Box8,
3760
+ {
3761
+ flexDirection: "column",
3762
+ paddingLeft: 1,
3763
+ paddingTop: 1,
3764
+ paddingBottom: 1,
3765
+ children: /* @__PURE__ */ jsx11(Box8, { children: /* @__PURE__ */ jsx11(Text8, { children: "No holdings found for this profile." }) })
3766
+ }
3767
+ ) : /* @__PURE__ */ jsx11(
3768
+ Table,
3769
+ {
3770
+ columns: balanceColumns,
3771
+ data: data.holdings,
3772
+ title: "Holdings",
3773
+ subtitle: `${data.holdings.length} of ${data.holdingsCount}`
3774
+ }
3775
+ )
3776
+ ] })
3777
+ );
3778
+ track("cli_profile", {
3779
+ identifier,
3780
+ output_format: "static",
3781
+ posts_count: data.postsCount,
3782
+ holdings_count: data.holdingsCount
3783
+ });
3784
+ }
3785
+ });
3786
+
3787
+ // src/commands/send.ts
3788
+ import { Command as Command9 } from "commander";
3184
3789
  import confirm4 from "@inquirer/confirm";
3185
3790
  import {
3186
3791
  erc20Abi as erc20Abi4,
@@ -3188,7 +3793,7 @@ import {
3188
3793
  isAddress as isAddress3,
3189
3794
  parseUnits as parseUnits3
3190
3795
  } from "viem";
3191
- import { setApiKey as setApiKey7 } from "@zoralabs/coins-sdk";
3796
+ import { setApiKey as setApiKey8 } from "@zoralabs/coins-sdk";
3192
3797
  var SEND_AMOUNT_CHECKS = {
3193
3798
  amount: (opts) => opts.amount !== void 0,
3194
3799
  percent: (opts) => opts.percent !== void 0,
@@ -3236,8 +3841,15 @@ function printSendResult(json, info) {
3236
3841
  console.log(` Tx ${info.txHash}
3237
3842
  `);
3238
3843
  }
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) {
3844
+ var sendCommand = new Command9("send").description("Send coins or ETH to an address").argument("[identifier]", "Coin address, name, or token (eth, usdc, zora)").option("--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) {
3240
3845
  const json = getJson(this);
3846
+ if (!opts.to) {
3847
+ outputErrorAndExit(
3848
+ json,
3849
+ "Missing --to flag.",
3850
+ "Usage: zora send <identifier> --to <address>"
3851
+ );
3852
+ }
3241
3853
  if (!isAddress3(opts.to)) {
3242
3854
  outputErrorAndExit(
3243
3855
  json,
@@ -3362,7 +3974,7 @@ var sendCommand = new Command8("send").description("Send coins or ETH to an addr
3362
3974
  } catch (err) {
3363
3975
  track("cli_send", {
3364
3976
  asset: "eth",
3365
- output_format: json ? "json" : "table",
3977
+ output_format: json ? "json" : "static",
3366
3978
  success: false,
3367
3979
  error_type: err instanceof Error ? err.constructor.name : "unknown"
3368
3980
  });
@@ -3391,7 +4003,7 @@ var sendCommand = new Command8("send").description("Send coins or ETH to an addr
3391
4003
  amount_mode: amountMode,
3392
4004
  amount_usd: amountUsd,
3393
4005
  transactionHash: txHash,
3394
- output_format: json ? "json" : "table",
4006
+ output_format: json ? "json" : "static",
3395
4007
  success: true,
3396
4008
  tx_hash: txHash
3397
4009
  });
@@ -3413,7 +4025,7 @@ var sendCommand = new Command8("send").description("Send coins or ETH to an addr
3413
4025
  } else {
3414
4026
  const apiKey = getApiKey();
3415
4027
  if (apiKey) {
3416
- setApiKey7(apiKey);
4028
+ setApiKey8(apiKey);
3417
4029
  }
3418
4030
  const ref = parseCoinRef(identifier, opts.type);
3419
4031
  let result;
@@ -3558,7 +4170,7 @@ var sendCommand = new Command8("send").description("Send coins or ETH to an addr
3558
4170
  coin_address: tokenAddress,
3559
4171
  coin_name: tokenName,
3560
4172
  coin_symbol: symbol,
3561
- output_format: json ? "json" : "table",
4173
+ output_format: json ? "json" : "static",
3562
4174
  success: false,
3563
4175
  error_type: err instanceof Error ? err.constructor.name : "unknown"
3564
4176
  });
@@ -3588,7 +4200,7 @@ var sendCommand = new Command8("send").description("Send coins or ETH to an addr
3588
4200
  amount_mode: amountMode,
3589
4201
  amount_usd: amountUsd,
3590
4202
  transactionHash: txHash,
3591
- output_format: json ? "json" : "table",
4203
+ output_format: json ? "json" : "static",
3592
4204
  success: true,
3593
4205
  tx_hash: txHash
3594
4206
  });
@@ -3596,8 +4208,8 @@ var sendCommand = new Command8("send").description("Send coins or ETH to an addr
3596
4208
  });
3597
4209
 
3598
4210
  // src/commands/setup.ts
3599
- import { Command as Command9 } from "commander";
3600
- import { generatePrivateKey, privateKeyToAccount as privateKeyToAccount3 } from "viem/accounts";
4211
+ import { Command as Command10 } from "commander";
4212
+ import { generatePrivateKey, privateKeyToAccount as privateKeyToAccount4 } from "viem/accounts";
3601
4213
 
3602
4214
  // src/lib/strings.ts
3603
4215
  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 +4222,7 @@ var BACKUP_WARNING = "Back up this file \u2014 it's the only copy of your key.";
3610
4222
  var isValidPrivateKey = (key) => /^(0x)?[0-9a-fA-F]{64}$/.test(key);
3611
4223
  var toAccount = (json, key, errorPrefix) => {
3612
4224
  try {
3613
- return privateKeyToAccount3(normalizeKey(key));
4225
+ return privateKeyToAccount4(normalizeKey(key));
3614
4226
  } catch {
3615
4227
  outputErrorAndExit(
3616
4228
  json,
@@ -3618,7 +4230,7 @@ var toAccount = (json, key, errorPrefix) => {
3618
4230
  );
3619
4231
  }
3620
4232
  };
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) {
4233
+ 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
4234
  const json = getJson(this);
3623
4235
  const nonInteractive = getYes(this);
3624
4236
  const envKey = process.env.ZORA_PRIVATE_KEY;
@@ -3633,7 +4245,7 @@ var setupCommand = new Command9("setup").description("Set up your Zora wallet").
3633
4245
  const account = toAccount(json, envKey, "ZORA_PRIVATE_KEY");
3634
4246
  outputData(json, {
3635
4247
  json: { source: "env", address: account.address },
3636
- table: () => {
4248
+ render: () => {
3637
4249
  console.log(" Using wallet from ZORA_PRIVATE_KEY.\n");
3638
4250
  console.log(` Address: ${account.address}
3639
4251
  `);
@@ -3654,14 +4266,14 @@ var setupCommand = new Command9("setup").description("Set up your Zora wallet").
3654
4266
  } catch (err) {
3655
4267
  outputErrorAndExit(
3656
4268
  json,
3657
- `\u2717 Could not read wallet: ${err.message}`,
4269
+ `\u2717 Could not read wallet: ${formatError(err)}`,
3658
4270
  "Run 'zora setup --force' to overwrite it."
3659
4271
  );
3660
4272
  }
3661
4273
  }
3662
4274
  if (existing) {
3663
4275
  const account = toAccount(json, existing, "Stored private key");
3664
- const truncated = `${account.address.slice(0, 6)}\u2026${account.address.slice(-4)}`;
4276
+ const truncated = truncateAddress(account.address);
3665
4277
  console.log(` Wallet already configured: ${truncated}
3666
4278
  `);
3667
4279
  if (!options.force) {
@@ -3723,7 +4335,7 @@ var setupCommand = new Command9("setup").description("Set up your Zora wallet").
3723
4335
  address: account.address,
3724
4336
  path: getWalletPath()
3725
4337
  },
3726
- table: () => {
4338
+ render: () => {
3727
4339
  console.log("\n\u2713 Wallet imported\n");
3728
4340
  console.log(` Address: ${account.address}`);
3729
4341
  console.log(` Private key: saved to ${getWalletPath()}
@@ -3758,7 +4370,7 @@ var setupCommand = new Command9("setup").description("Set up your Zora wallet").
3758
4370
  address: account.address,
3759
4371
  path: getWalletPath()
3760
4372
  },
3761
- table: () => {
4373
+ render: () => {
3762
4374
  console.log("\n\u2713 Wallet created\n");
3763
4375
  console.log(` Address: ${account.address}`);
3764
4376
  console.log(` Private key: saved to ${getWalletPath()}
@@ -3777,8 +4389,8 @@ var setupCommand = new Command9("setup").description("Set up your Zora wallet").
3777
4389
  });
3778
4390
 
3779
4391
  // src/commands/wallet.ts
3780
- import { Command as Command10 } from "commander";
3781
- import { privateKeyToAccount as privateKeyToAccount4 } from "viem/accounts";
4392
+ import { Command as Command11 } from "commander";
4393
+ import { privateKeyToAccount as privateKeyToAccount5 } from "viem/accounts";
3782
4394
  var resolvePrivateKey = () => {
3783
4395
  const envKey = process.env.ZORA_PRIVATE_KEY;
3784
4396
  if (envKey) {
@@ -3790,7 +4402,7 @@ var resolvePrivateKey = () => {
3790
4402
  }
3791
4403
  return void 0;
3792
4404
  };
3793
- var walletCommand = new Command10("wallet").description(
4405
+ var walletCommand = new Command11("wallet").description(
3794
4406
  "Manage your Zora wallet"
3795
4407
  );
3796
4408
  walletCommand.command("info").description("Show wallet address and storage location").action(function() {
@@ -3801,7 +4413,7 @@ walletCommand.command("info").description("Show wallet address and storage locat
3801
4413
  }
3802
4414
  let account;
3803
4415
  try {
3804
- account = privateKeyToAccount4(normalizeKey(resolved.key));
4416
+ account = privateKeyToAccount5(normalizeKey(resolved.key));
3805
4417
  } catch {
3806
4418
  const msg = resolved.source === "env" ? "ZORA_PRIVATE_KEY is not a valid private key." : "Stored private key is invalid.";
3807
4419
  const suggestion = resolved.source === "env" ? void 0 : "Run 'zora setup --force' to replace it.";
@@ -3810,7 +4422,7 @@ walletCommand.command("info").description("Show wallet address and storage locat
3810
4422
  const source = resolved.source === "env" ? "env (ZORA_PRIVATE_KEY)" : getWalletPath();
3811
4423
  outputData(json, {
3812
4424
  json: { address: account.address, source },
3813
- table: () => {
4425
+ render: () => {
3814
4426
  console.log(` Address: ${account.address}`);
3815
4427
  console.log(` Source: ${source}`);
3816
4428
  }
@@ -3850,7 +4462,7 @@ walletCommand.command("export").description("Print the raw private key to stdout
3850
4462
  });
3851
4463
 
3852
4464
  // src/components/Zorb.tsx
3853
- import { Text as Text7, Box as Box7 } from "ink";
4465
+ import { Text as Text9, Box as Box9 } from "ink";
3854
4466
 
3855
4467
  // src/lib/zorb-pixels.ts
3856
4468
  function supportsTruecolor() {
@@ -3995,7 +4607,7 @@ function generateZorbPixels(size) {
3995
4607
  }
3996
4608
 
3997
4609
  // src/components/Zorb.tsx
3998
- import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
4610
+ import { jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
3999
4611
  var LOWER_HALF_BLOCK = "\u2584";
4000
4612
  var UPPER_HALF_BLOCK = "\u2580";
4001
4613
  function rgbString([r, g, b]) {
@@ -4018,19 +4630,19 @@ function Zorb({ size = 20 }) {
4018
4630
  const topIsBlack = isBlack(top);
4019
4631
  const bottomIsBlack = isBlack(bottom);
4020
4632
  if (topIsBlack && bottomIsBlack) {
4021
- cells.push(/* @__PURE__ */ jsx10(Text7, { children: " " }, x));
4633
+ cells.push(/* @__PURE__ */ jsx12(Text9, { children: " " }, x));
4022
4634
  } else if (topIsBlack) {
4023
4635
  cells.push(
4024
- /* @__PURE__ */ jsx10(Text7, { color: rgbString(bottom), children: LOWER_HALF_BLOCK }, x)
4636
+ /* @__PURE__ */ jsx12(Text9, { color: rgbString(bottom), children: LOWER_HALF_BLOCK }, x)
4025
4637
  );
4026
4638
  } else if (bottomIsBlack) {
4027
4639
  cells.push(
4028
- /* @__PURE__ */ jsx10(Text7, { color: rgbString(top), children: UPPER_HALF_BLOCK }, x)
4640
+ /* @__PURE__ */ jsx12(Text9, { color: rgbString(top), children: UPPER_HALF_BLOCK }, x)
4029
4641
  );
4030
4642
  } else {
4031
4643
  cells.push(
4032
- /* @__PURE__ */ jsx10(
4033
- Text7,
4644
+ /* @__PURE__ */ jsx12(
4645
+ Text9,
4034
4646
  {
4035
4647
  backgroundColor: rgbString(top),
4036
4648
  color: rgbString(bottom),
@@ -4041,49 +4653,50 @@ function Zorb({ size = 20 }) {
4041
4653
  );
4042
4654
  }
4043
4655
  }
4044
- rows.push(/* @__PURE__ */ jsx10(Text7, { children: cells }, y));
4656
+ rows.push(/* @__PURE__ */ jsx12(Text9, { children: cells }, y));
4045
4657
  }
4046
- return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
4047
- /* @__PURE__ */ jsx10(Text7, { children: " " }),
4658
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
4659
+ /* @__PURE__ */ jsx12(Text9, { children: " " }),
4048
4660
  rows,
4049
- /* @__PURE__ */ jsx10(Text7, { children: " " })
4661
+ /* @__PURE__ */ jsx12(Text9, { children: " " })
4050
4662
  ] });
4051
4663
  }
4052
4664
 
4053
4665
  // src/index.tsx
4054
- import { jsx as jsx11 } from "react/jsx-runtime";
4666
+ import { jsx as jsx13 } from "react/jsx-runtime";
4055
4667
  if (process.env.ZORA_API_TARGET) {
4056
4668
  setApiBaseUrl(process.env.ZORA_API_TARGET);
4057
4669
  }
4058
- var version = true ? "0.2.4" : JSON.parse(
4670
+ var version = true ? "0.3.0" : JSON.parse(
4059
4671
  readFileSync2(new URL("../package.json", import.meta.url), "utf-8")
4060
4672
  ).version;
4061
4673
  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
- );
4674
+ const program2 = new Command12().name("zora").description("Zora CLI").version(version).option("--json", "Output as JSON (for scripts and automation)", false);
4070
4675
  program2.addCommand(authCommand);
4071
4676
  program2.addCommand(balanceCommand);
4072
4677
  program2.addCommand(buyCommand);
4073
4678
  program2.addCommand(exploreCommand);
4074
4679
  program2.addCommand(getCommand);
4075
4680
  program2.addCommand(priceHistoryCommand);
4681
+ program2.addCommand(profileCommand);
4076
4682
  program2.addCommand(setupCommand);
4077
4683
  program2.addCommand(walletCommand);
4078
4684
  program2.addCommand(sellCommand);
4079
4685
  program2.addCommand(sendCommand);
4686
+ program2.hook("preAction", (_thisCommand, actionCommand) => {
4687
+ const expected = actionCommand.registeredArguments.length;
4688
+ if (expected > 0 && actionCommand.args.length < expected) {
4689
+ actionCommand.outputHelp();
4690
+ process.exit(1);
4691
+ }
4692
+ });
4080
4693
  return program2;
4081
4694
  };
4082
4695
  var program = buildProgram();
4083
4696
  if (!process.env.VITEST) {
4084
4697
  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 }));
4698
+ if (showingHelp && !process.argv.includes("--json") && supportsTruecolor()) {
4699
+ renderOnce(/* @__PURE__ */ jsx13(Zorb, { size: 20 }));
4087
4700
  }
4088
4701
  console.warn(
4089
4702
  "\x1B[33m\u26A0 Beta:\x1B[0m This CLI is in beta and should be used with caution."