@zeroxyz/cli 0.0.37 → 0.0.38

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 (2) hide show
  1. package/dist/index.js +408 -57
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import { join as join8 } from "path";
7
7
  // package.json
8
8
  var package_default = {
9
9
  name: "@zeroxyz/cli",
10
- version: "0.0.37",
10
+ version: "0.0.38",
11
11
  type: "module",
12
12
  bin: {
13
13
  zero: "dist/index.js",
@@ -115,6 +115,7 @@ var capabilityResponseSchema = z.object({
115
115
  responseSchema: z.record(z.string(), z.unknown()).nullable(),
116
116
  example: z.object({ request: z.unknown(), response: z.unknown() }).nullable(),
117
117
  tags: z.array(z.string()).nullable(),
118
+ exampleAgentPrompt: z.string().nullable().optional(),
118
119
  displayCostAmount: z.string(),
119
120
  displayCostAsset: z.string(),
120
121
  reviewCount: z.number(),
@@ -228,7 +229,7 @@ var buildCanonicalMessage = (method, path, body, timestamp, nonce) => {
228
229
  const bodyHash = createHash("sha256").update(body ?? "").digest("hex");
229
230
  return `${method}:${path}:${bodyHash}:${timestamp}:${nonce}`;
230
231
  };
231
- var ApiService = class {
232
+ var ApiService = class _ApiService {
232
233
  constructor(baseUrl, account) {
233
234
  this.baseUrl = baseUrl;
234
235
  this.account = account;
@@ -236,6 +237,7 @@ var ApiService = class {
236
237
  }
237
238
  walletAddress;
238
239
  account;
240
+ withAccount = (account) => new _ApiService(this.baseUrl, account);
239
241
  signRequest = async (method, path, body) => {
240
242
  if (!this.account) throw new Error("No private key configured");
241
243
  const timestamp = Math.floor(Date.now() / 1e3).toString();
@@ -1120,8 +1122,9 @@ var PaymentService = class {
1120
1122
  return { viemChain: tempoTestnetChain, token: PATHUSD_TEMPO };
1121
1123
  }
1122
1124
  };
1123
- getBalanceRaw = async (chain) => {
1124
- if (!this.account) return 0n;
1125
+ getBalanceRaw = async (chain, address) => {
1126
+ const target = address ?? this.account?.address;
1127
+ if (!target) return 0n;
1125
1128
  const { viemChain, token } = this.resolveChainConfig(chain);
1126
1129
  const client = createPublicClient({
1127
1130
  chain: viemChain,
@@ -1131,7 +1134,7 @@ var PaymentService = class {
1131
1134
  address: token,
1132
1135
  abi: ERC20_BALANCE_ABI,
1133
1136
  functionName: "balanceOf",
1134
- args: [this.account.address]
1137
+ args: [target]
1135
1138
  });
1136
1139
  return balance;
1137
1140
  };
@@ -1151,6 +1154,13 @@ var PaymentService = class {
1151
1154
  ]);
1152
1155
  return { amount: formatUnits(baseRaw + tempoRaw, 6), asset: "USDC" };
1153
1156
  };
1157
+ getTotalBalanceForAddress = async (address) => {
1158
+ const [baseRaw, tempoRaw] = await Promise.all([
1159
+ this.getBalanceRaw("base", address),
1160
+ this.getBalanceRaw("tempo", address)
1161
+ ]);
1162
+ return { amount: formatUnits(baseRaw + tempoRaw, 6), asset: "USDC" };
1163
+ };
1154
1164
  };
1155
1165
 
1156
1166
  // src/util/infer-schema.ts
@@ -1237,30 +1247,69 @@ var isJsonContentType = (contentType) => {
1237
1247
  const ct = contentType.toLowerCase().split(";")[0]?.trim() ?? "";
1238
1248
  return ct === "application/json" || ct.endsWith("+json");
1239
1249
  };
1240
- var MAX_REQUEST_BODY_BYTES = 10 * 1024 * 1024;
1241
- var resolveRequestBody = (rawData, readStdin) => {
1250
+ var MAX_INLINE_REQUEST_BODY_BYTES = 10 * 1024 * 1024;
1251
+ var MAX_FILE_REQUEST_BODY_BYTES = 500 * 1024 * 1024;
1252
+ var UPSTREAM_ERROR_FIELDS = [
1253
+ "error",
1254
+ "message",
1255
+ "detail",
1256
+ "reason",
1257
+ "error_description"
1258
+ ];
1259
+ var UPSTREAM_ERROR_MAX_LEN = 200;
1260
+ var clampUpstreamMessage = (value) => {
1261
+ const trimmed = value.trim();
1262
+ if (!trimmed) return "";
1263
+ return trimmed.length > UPSTREAM_ERROR_MAX_LEN ? `${trimmed.slice(0, UPSTREAM_ERROR_MAX_LEN)}\u2026` : trimmed;
1264
+ };
1265
+ var extractUpstreamErrorMessage = (body) => {
1266
+ const parsed = tryParseJson(body);
1267
+ if (parsed === null || typeof parsed !== "object") {
1268
+ const clipped = clampUpstreamMessage(body);
1269
+ return clipped || void 0;
1270
+ }
1271
+ const record = parsed;
1272
+ for (const field of UPSTREAM_ERROR_FIELDS) {
1273
+ const value = record[field];
1274
+ if (typeof value === "string" && value.length > 0) {
1275
+ return clampUpstreamMessage(value);
1276
+ }
1277
+ if (value && typeof value === "object") {
1278
+ const nested = value.message;
1279
+ if (typeof nested === "string" && nested.length > 0) {
1280
+ return clampUpstreamMessage(nested);
1281
+ }
1282
+ }
1283
+ }
1284
+ return void 0;
1285
+ };
1286
+ var resolveRequestBody = (rawData, readStdin2) => {
1242
1287
  const fromFile = (spec) => {
1243
1288
  if (spec === "@-") return readFileSync3(0);
1244
1289
  return readFileSync3(resolvePath(spec.slice(1)));
1245
1290
  };
1246
- if (readStdin && rawData !== void 0) {
1291
+ if (readStdin2 && rawData !== void 0) {
1247
1292
  throw new Error(
1248
1293
  "Conflicting body sources: use either --data-stdin or -d, not both."
1249
1294
  );
1250
1295
  }
1251
1296
  let body;
1252
- if (readStdin) {
1297
+ let cap;
1298
+ if (readStdin2) {
1253
1299
  body = readFileSync3(0);
1300
+ cap = MAX_FILE_REQUEST_BODY_BYTES;
1254
1301
  } else if (rawData?.startsWith("@")) {
1255
1302
  body = fromFile(rawData);
1303
+ cap = MAX_FILE_REQUEST_BODY_BYTES;
1256
1304
  } else {
1257
1305
  body = rawData;
1306
+ cap = MAX_INLINE_REQUEST_BODY_BYTES;
1258
1307
  }
1259
1308
  if (body !== void 0) {
1260
1309
  const bytes = Buffer.isBuffer(body) ? body.length : Buffer.byteLength(body, "utf8");
1261
- if (bytes > MAX_REQUEST_BODY_BYTES) {
1310
+ if (bytes > cap) {
1262
1311
  throw new Error(
1263
- `Request body is ${bytes} bytes \u2014 exceeds the ${MAX_REQUEST_BODY_BYTES} byte limit. Split the payload, compress it, or contact the capability owner about raising the cap.`
1312
+ `Request body is ${bytes} bytes \u2014 exceeds the ${cap} byte local cap. This is a CLI-side guard, not a protocol limit. Compress the payload or contact us if you need it raised.`
1264
1313
  );
1265
1314
  }
1266
1315
  }
@@ -1371,14 +1420,16 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1371
1420
  capName = cap.name;
1372
1421
  }
1373
1422
  const searchResult = await apiService.search({ query: capName });
1374
- const slugFoundInResults = searchResult.capabilities.some(
1423
+ const matchedEntry = searchResult.capabilities.find(
1375
1424
  (c) => c.slug === options.capability || c.id === options.capability
1376
1425
  );
1426
+ const slugFoundInResults = Boolean(matchedEntry);
1377
1427
  stateService.saveLastSearch({
1378
1428
  searchId: searchResult.searchId,
1379
1429
  capabilities: searchResult.capabilities.map((c) => ({
1380
1430
  position: c.position,
1381
1431
  id: c.id,
1432
+ slug: c.slug,
1382
1433
  url: c.url,
1383
1434
  urlTemplate: c.urlTemplate ?? null,
1384
1435
  displayCostAmount: c.cost.amount
@@ -1396,7 +1447,13 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1396
1447
  slugFoundInResults
1397
1448
  });
1398
1449
  analyticsService.capture("capability_viewed", {
1450
+ // Existing field preserved as the raw --capability
1451
+ // input for back-compat.
1399
1452
  capabilityId: options.capability,
1453
+ // New canonical identifier fields hydrated from the
1454
+ // resolved search result (when the slug was found).
1455
+ capabilityUid: matchedEntry?.id,
1456
+ capabilitySlug: matchedEntry?.slug,
1400
1457
  fromLastSearch: false,
1401
1458
  searchId: searchResult.searchId,
1402
1459
  triggeredBy: "slug_handoff"
@@ -1413,7 +1470,7 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1413
1470
  } catch (err) {
1414
1471
  const message = err instanceof Error ? err.message : "Failed to read request body";
1415
1472
  analyticsService.capture("fetch_error", {
1416
- cliErrorClass: /exceeds the .* byte limit/.test(message) ? "payload_too_large" : "schema_validation_failed",
1473
+ cliErrorClass: /exceeds the .* byte local cap/.test(message) ? "payload_too_large" : "schema_validation_failed",
1417
1474
  url: redactUrl(resolvedUrl),
1418
1475
  error: truncateError(message)
1419
1476
  });
@@ -1455,6 +1512,8 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1455
1512
  });
1456
1513
  const matchCtx = explicitCapabilityCtx ?? urlCtx;
1457
1514
  const capabilityId = options.capability ?? matchCtx?.capabilityId ?? null;
1515
+ const capabilityUid = matchCtx?.capabilityUid ?? null;
1516
+ const capabilitySlug = matchCtx?.capabilitySlug ?? null;
1458
1517
  const searchId = matchCtx?.searchId;
1459
1518
  const resultRank = matchCtx?.resultRank;
1460
1519
  const matchedDisplayCostAmount = matchCtx?.displayCostAmount;
@@ -1604,10 +1663,12 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1604
1663
  const isFailure = !finalResponse || typeof status === "number" && (status < 200 || status >= 300);
1605
1664
  let errorSnippetHash;
1606
1665
  let errorSnippetLength;
1666
+ let upstreamErrorMessage;
1607
1667
  if (isFailure && body && !bodyIsBinary) {
1608
1668
  const snippet = body.slice(0, 500);
1609
1669
  errorSnippetHash = createHash3("sha256").update(snippet).digest("hex");
1610
1670
  errorSnippetLength = snippet.length;
1671
+ upstreamErrorMessage = extractUpstreamErrorMessage(body);
1611
1672
  }
1612
1673
  let runId = null;
1613
1674
  if (capabilityId && apiService.walletAddress) {
@@ -1651,7 +1712,7 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1651
1712
  );
1652
1713
  }
1653
1714
  }
1654
- const outcome = !finalResponse ? "network_error" : status === 402 && !paymentMeta ? "payment_failed" : status !== void 0 && status >= 400 && status !== 402 ? "server_error" : "success";
1715
+ const outcome = !finalResponse ? "network_error" : status === 402 && !paymentMeta ? "payment_failed" : status === 402 && paymentMeta ? "payment_rejected" : status !== void 0 && status >= 400 && status !== 402 ? "server_error" : "success";
1655
1716
  analyticsService.capture("fetch_executed", {
1656
1717
  url: redactUrl(resolvedUrl),
1657
1718
  status,
@@ -1660,6 +1721,13 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1660
1721
  hasPayment: !!paymentMeta,
1661
1722
  paymentProtocol: paymentMeta?.protocol,
1662
1723
  paymentAmount: paymentMeta?.amount,
1724
+ // Canonical join keys — capabilitySlug is the dashboard
1725
+ // breakdown dimension; capabilityUid is the stable join
1726
+ // key with the capabilities table. capabilityId is the
1727
+ // back-compat alias (same value as capabilityUid when
1728
+ // resolved, else the raw --capability input).
1729
+ capabilityUid: capabilityUid ?? void 0,
1730
+ capabilitySlug: capabilitySlug ?? void 0,
1663
1731
  capabilityId: capabilityId ?? void 0,
1664
1732
  searchId: searchId ?? void 0,
1665
1733
  resultRank: resultRank ?? void 0,
@@ -1669,15 +1737,21 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1669
1737
  });
1670
1738
  const isFetchFailure = Boolean(fetchError) || !finalResponse || typeof status === "number" && (status < 200 || status >= 300);
1671
1739
  if (isFetchFailure) {
1672
- const cliErrorClass = fetchError || !finalResponse ? "network" : !apiService.walletAddress ? "auth_missing" : "unknown";
1740
+ const cliErrorClass = fetchError || !finalResponse ? "network" : !apiService.walletAddress ? "auth_missing" : status === 402 && paymentMeta ? "payment_rejected" : typeof status === "number" && status >= 500 ? "upstream_5xx" : typeof status === "number" && status >= 400 ? "upstream_4xx" : "unknown";
1673
1741
  analyticsService.capture("fetch_error", {
1674
1742
  cliErrorClass,
1743
+ capabilityUid: capabilityUid ?? void 0,
1744
+ capabilitySlug: capabilitySlug ?? void 0,
1675
1745
  capabilityId: capabilityId ?? void 0,
1676
1746
  searchId: searchId ?? void 0,
1677
1747
  resultRank: resultRank ?? void 0,
1678
1748
  url: redactUrl(resolvedUrl),
1749
+ status: typeof status === "number" ? status : void 0,
1750
+ upstreamErrorMessage,
1751
+ errorSnippetHash,
1752
+ errorSnippetLength,
1679
1753
  error: truncateError(
1680
- fetchError?.message ?? skipReasons.join("; ")
1754
+ fetchError?.message ?? upstreamErrorMessage ?? skipReasons.join("; ")
1681
1755
  ),
1682
1756
  skippedRun: !runId && skipReasons.length > 0
1683
1757
  });
@@ -1685,6 +1759,16 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1685
1759
  if (fetchError && !options.json) {
1686
1760
  console.error(` Fetch failed: ${fetchError.message}`);
1687
1761
  }
1762
+ if (finalResponse && !fetchError && !options.json && typeof status === "number" && (status < 200 || status >= 300)) {
1763
+ const messageSuffix = upstreamErrorMessage ? ` \u2014 ${upstreamErrorMessage}` : "";
1764
+ if (status === 402 && paymentMeta) {
1765
+ console.error(
1766
+ ` Upstream returned 402 after payment was sent \u2014 the seller's facilitator rejected the credential${messageSuffix}`
1767
+ );
1768
+ } else {
1769
+ console.error(` Upstream returned ${status}${messageSuffix}`);
1770
+ }
1771
+ }
1688
1772
  if (options.json) {
1689
1773
  const responseStatus = finalResponse?.status ?? null;
1690
1774
  const ok = responseStatus !== null && responseStatus >= 200 && responseStatus < 300;
@@ -1765,6 +1849,19 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1765
1849
 
1766
1850
  // src/commands/get-command.ts
1767
1851
  import { Command as Command4 } from "commander";
1852
+
1853
+ // src/util/format-price.ts
1854
+ var centsToDollars = (cents) => {
1855
+ const value = Number.parseFloat(cents) / 100;
1856
+ if (!Number.isFinite(value) || value === 0) return "0.00";
1857
+ if (value < 1e-4) return value.toFixed(6);
1858
+ if (value < 1e-3) return value.toFixed(5);
1859
+ if (value < 0.01) return value.toFixed(4);
1860
+ if (value < 1) return value.toFixed(3);
1861
+ return value.toFixed(2);
1862
+ };
1863
+
1864
+ // src/commands/get-command.ts
1768
1865
  var formatRelativeTimestamp = (iso) => {
1769
1866
  if (!iso) return "never";
1770
1867
  const then = new Date(iso).getTime();
@@ -1781,12 +1878,6 @@ var formatRelativeTimestamp = (iso) => {
1781
1878
  if (diffMo < 12) return `${diffMo}mo ago`;
1782
1879
  return `${Math.round(diffMo / 12)}y ago`;
1783
1880
  };
1784
- var centsToDollars = (cents) => {
1785
- const value = Number.parseFloat(cents) / 100;
1786
- if (value < 0.01) return value.toFixed(4);
1787
- if (value < 1) return value.toFixed(3);
1788
- return value.toFixed(2);
1789
- };
1790
1881
  var formatCost = (capability) => {
1791
1882
  const lines = [];
1792
1883
  const observed = capability.priceObserved;
@@ -1796,6 +1887,10 @@ var formatCost = (capability) => {
1796
1887
  const median = observed.medianCents ? centsToDollars(observed.medianCents) : null;
1797
1888
  const detail = median ? `median $${median}, n=${observed.sampleCount}` : `n=${observed.sampleCount}`;
1798
1889
  lines.push(` Cost: $${min}\u2013$${max}/call (${detail})`);
1890
+ } else if (capability.displayCostAmount === "0") {
1891
+ lines.push(` Cost: Free`);
1892
+ } else if (capability.displayCostAmount === "unknown") {
1893
+ lines.push(` Cost: variable pricing`);
1799
1894
  } else {
1800
1895
  lines.push(` Cost: $${capability.displayCostAmount}/call`);
1801
1896
  }
@@ -1903,6 +1998,12 @@ var formatCapability = (capability) => {
1903
1998
  lines.push(
1904
1999
  ` Last successful run: ${formatRelativeTimestamp(capability.lastSuccessfullyRanAt)}`
1905
2000
  );
2001
+ if (capability.exampleAgentPrompt?.trim()) {
2002
+ lines.push(" Example prompt:");
2003
+ for (const promptLine of capability.exampleAgentPrompt.split("\n")) {
2004
+ lines.push(` ${promptLine}`);
2005
+ }
2006
+ }
1906
2007
  lines.push(...buildTryItExample(capability));
1907
2008
  return lines.join("\n");
1908
2009
  };
@@ -1955,7 +2056,15 @@ var getCommand = (appContext) => new Command4("get").description(
1955
2056
  console.log(JSON.stringify(capability, null, 2));
1956
2057
  }
1957
2058
  analyticsService.capture("capability_viewed", {
2059
+ // Existing field — raw user input (uid, slug, or position
2060
+ // resolved to uid via the last-search cache). Preserved
2061
+ // for back-compat with existing dashboards.
1958
2062
  capabilityId,
2063
+ // New canonical identifier fields read off the API response,
2064
+ // so they are always populated regardless of how the user
2065
+ // referenced the capability.
2066
+ capabilityUid: capability.uid,
2067
+ capabilitySlug: capability.slug,
1959
2068
  fromLastSearch: isPosition,
1960
2069
  ...isPosition ? { position } : {},
1961
2070
  ...searchId ? { searchId } : {}
@@ -2089,7 +2198,12 @@ var printReadyFooter = () => {
2089
2198
  const lines = [
2090
2199
  "",
2091
2200
  ` ${color.boldGreen("Zero is ready!")} Zero works best with an AI agent.`,
2092
- ` Open ${color.boldRed("Claude Code")}, Codex, Cursor, Blackbox, or your agent of choice`,
2201
+ "",
2202
+ " First, claim your $5 welcome bonus \u2014 run this now, before opening your agent:",
2203
+ "",
2204
+ ` ${color.cyan("zero welcome")}`,
2205
+ "",
2206
+ ` Then open ${color.boldRed("Claude Code")}, Codex, Cursor, Blackbox, or your agent of choice`,
2093
2207
  " and try this prompt to get started:",
2094
2208
  "",
2095
2209
  ` ${color.cyan("What is zero and how do I use it?")}`,
@@ -2552,12 +2666,13 @@ To remove them, run: ${color.cyan("zero init cleanup")}`
2552
2666
  sectionDivider();
2553
2667
  console.error(printReadyFooter());
2554
2668
  currentStep = "complete";
2669
+ if (walletAddress) {
2670
+ appContext.services.analyticsService.setWalletAddress(walletAddress);
2671
+ }
2555
2672
  appContext.services.analyticsService.capture("wallet_initialized", {
2556
2673
  // biome-ignore lint/style/useNamingConvention: snake_case for analytics
2557
2674
  wallet_created: walletCreated,
2558
2675
  // biome-ignore lint/style/useNamingConvention: snake_case for analytics
2559
- wallet_address: walletAddress,
2560
- // biome-ignore lint/style/useNamingConvention: snake_case for analytics
2561
2676
  agents_detected: agentsDetected,
2562
2677
  // biome-ignore lint/style/useNamingConvention: snake_case for analytics
2563
2678
  agents_detected_count: agentsDetected.length,
@@ -2901,7 +3016,8 @@ var formatSearchResults = (results) => {
2901
3016
  const displayDescription = r.whatItDoes ?? r.description;
2902
3017
  const ratingBadge = formatRatingBadge(r.rating);
2903
3018
  const statusBadge = formatStatusBadge(r.displayStatus);
2904
- return ` ${r.position}. ${displayName} \u2014 $${r.cost.amount}/call \u2014 ${ratingBadge}${statusBadge}
3019
+ const costLabel = r.cost.amount === "0" ? "Free" : r.cost.amount === "unknown" ? "variable pricing" : `$${r.cost.amount}/call`;
3020
+ return ` ${r.position}. ${displayName} \u2014 ${costLabel} \u2014 ${ratingBadge}${statusBadge}
2905
3021
  "${displayDescription}"`;
2906
3022
  }).join("\n");
2907
3023
  };
@@ -2972,8 +3088,8 @@ var searchCommand = (appContext) => new Command8("search").description("Search f
2972
3088
  protocol: options.protocol,
2973
3089
  availabilityStatus: options.status,
2974
3090
  includeAll: options.all ?? false,
2975
- source: options.source,
2976
- excludeSource: options.excludeSource,
3091
+ listingSource: options.source,
3092
+ excludeListingSource: options.excludeSource,
2977
3093
  json: options.json ?? false
2978
3094
  });
2979
3095
  if (options.json) {
@@ -2995,6 +3111,7 @@ var searchCommand = (appContext) => new Command8("search").description("Search f
2995
3111
  capabilities: result.capabilities.map((c) => ({
2996
3112
  position: c.position,
2997
3113
  id: c.id,
3114
+ slug: c.slug,
2998
3115
  url: c.url,
2999
3116
  urlTemplate: c.urlTemplate ?? null,
3000
3117
  displayCostAmount: c.cost.amount
@@ -3045,9 +3162,43 @@ import { homedir as homedir3 } from "os";
3045
3162
  import { join as join3 } from "path";
3046
3163
  import { Command as Command10 } from "commander";
3047
3164
  import open from "open";
3048
- import { privateKeyToAccount as privateKeyToAccount2 } from "viem/accounts";
3049
- var walletBalanceCommand = (appContext) => new Command10("balance").description("Show wallet balance").action(async () => {
3050
- const { walletService } = appContext.services;
3165
+ import { isAddress } from "viem";
3166
+ import { generatePrivateKey as generatePrivateKey2, privateKeyToAccount as privateKeyToAccount2 } from "viem/accounts";
3167
+
3168
+ // src/util/stdin.ts
3169
+ var readStdin = async () => {
3170
+ const chunks = [];
3171
+ for await (const chunk of process.stdin) {
3172
+ chunks.push(chunk);
3173
+ }
3174
+ return Buffer.concat(chunks).toString("utf8");
3175
+ };
3176
+
3177
+ // src/commands/wallet-command.ts
3178
+ var PRIVATE_KEY_PATTERN = /^0x[0-9a-fA-F]{64}$/;
3179
+ var parseProvider = (raw) => raw === "stripe" ? "stripe" : "coinbase";
3180
+ var walletBalanceCommand = (appContext) => new Command10("balance").description("Show wallet balance").option(
3181
+ "--address <address>",
3182
+ "Check balance for an arbitrary address (no key needed). Useful with `zero wallet generate` \u2014 verify funds arrived without setting the wallet as your default."
3183
+ ).action(async (options) => {
3184
+ const { walletService, paymentService } = appContext.services;
3185
+ if (options.address) {
3186
+ if (!isAddress(options.address)) {
3187
+ console.error("Invalid address (expected 0x + 40 hex chars).");
3188
+ process.exitCode = 1;
3189
+ return;
3190
+ }
3191
+ try {
3192
+ const balance2 = await paymentService.getTotalBalanceForAddress(
3193
+ options.address
3194
+ );
3195
+ console.log(`${balance2.amount} ${balance2.asset}`);
3196
+ } catch {
3197
+ console.error("Failed to fetch balance");
3198
+ process.exitCode = 1;
3199
+ }
3200
+ return;
3201
+ }
3051
3202
  const balance = await walletService.getBalance();
3052
3203
  if (balance === null) {
3053
3204
  console.error("No wallet configured. Run `zero init` first.");
@@ -3061,6 +3212,20 @@ var walletBalanceCommand = (appContext) => new Command10("balance").description(
3061
3212
  }
3062
3213
  console.log(`${balance.amount} ${balance.asset}`);
3063
3214
  });
3215
+ var readPrivateKeyFromStdin = async () => {
3216
+ if (process.stdin.isTTY) {
3217
+ console.error(
3218
+ "Expected a private key on stdin. Example:\n zero wallet generate --json | jq -r .privateKey | zero wallet fund --key-stdin"
3219
+ );
3220
+ return null;
3221
+ }
3222
+ const raw = (await readStdin()).trim();
3223
+ if (!PRIVATE_KEY_PATTERN.test(raw)) {
3224
+ console.error("Invalid private key on stdin (expected 0x + 64 hex chars).");
3225
+ return null;
3226
+ }
3227
+ return raw;
3228
+ };
3064
3229
  var walletFundCommand = (appContext) => new Command10("fund").description("Fund your wallet").argument("[amount]", "Amount to fund in USDC").option("--manual", "Show wallet address for manual transfer").option(
3065
3230
  "--no-open",
3066
3231
  "Print the funding URL instead of opening a browser (for agents \u2014 funding links are one-time use, hand the URL to the user)"
@@ -3068,9 +3233,55 @@ var walletFundCommand = (appContext) => new Command10("fund").description("Fund
3068
3233
  "--use <provider>",
3069
3234
  "Onramp provider: coinbase or stripe",
3070
3235
  "coinbase"
3236
+ ).option(
3237
+ "--key-stdin",
3238
+ "Read a private key from stdin and mint a funding URL for that wallet instead of your configured one. Useful with `zero wallet generate`. Does not modify your config."
3071
3239
  ).action(
3072
3240
  async (amount, options) => {
3073
- const { analyticsService, walletService } = appContext.services;
3241
+ const { analyticsService, apiService, walletService } = appContext.services;
3242
+ const provider = parseProvider(options.use);
3243
+ if (options.keyStdin) {
3244
+ const key = await readPrivateKeyFromStdin();
3245
+ if (!key) {
3246
+ process.exitCode = 1;
3247
+ return;
3248
+ }
3249
+ const altAccount = privateKeyToAccount2(key);
3250
+ const altApi = apiService.withAccount(altAccount);
3251
+ if (options.manual) {
3252
+ console.log(`Send USDC (Base) to:
3253
+ ${altAccount.address}`);
3254
+ analyticsService.capture("wallet_funded", {
3255
+ method: "manual",
3256
+ amount,
3257
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
3258
+ alt_wallet: true
3259
+ });
3260
+ return;
3261
+ }
3262
+ const url2 = await altApi.getFundingUrl(amount, provider);
3263
+ if (!url2) {
3264
+ console.log(`Send USDC (Base) to:
3265
+ ${altAccount.address}`);
3266
+ console.error(
3267
+ "Could not get funding URL. Send USDC (Base) manually."
3268
+ );
3269
+ process.exitCode = 1;
3270
+ return;
3271
+ }
3272
+ console.log(
3273
+ "Funding URL (one-time use \u2014 open it in a browser to fund):"
3274
+ );
3275
+ console.log(url2);
3276
+ console.log(`Wallet address: ${altAccount.address}`);
3277
+ analyticsService.capture("wallet_funded", {
3278
+ method: "url",
3279
+ amount,
3280
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
3281
+ alt_wallet: true
3282
+ });
3283
+ return;
3284
+ }
3074
3285
  const address = walletService.getAddress();
3075
3286
  if (!address) {
3076
3287
  console.error("No wallet configured. Run `zero init` first.");
@@ -3086,11 +3297,7 @@ ${address}`);
3086
3297
  });
3087
3298
  return;
3088
3299
  }
3089
- const provider = options.use === "stripe" ? "stripe" : "coinbase";
3090
- const url = await appContext.services.apiService.getFundingUrl(
3091
- amount,
3092
- provider
3093
- );
3300
+ const url = await apiService.getFundingUrl(amount, provider);
3094
3301
  if (url) {
3095
3302
  if (options.open) {
3096
3303
  await open(url);
@@ -3171,18 +3378,95 @@ var walletSetCommand = (appContext) => new Command10("set").description("Set wal
3171
3378
  )
3172
3379
  );
3173
3380
  console.log(`Wallet set: ${account.address}`);
3381
+ analyticsService.setWalletAddress(account.address);
3174
3382
  analyticsService.capture("wallet_set", {
3175
- // biome-ignore lint/style/useNamingConvention: snake_case for analytics
3176
- wallet_address: account.address,
3177
3383
  force: options.force ?? false
3178
3384
  });
3179
3385
  });
3386
+ var walletGenerateCommand = (appContext) => new Command10("generate").description(
3387
+ "Generate a fresh wallet (address + private key) without touching your configured wallet"
3388
+ ).option("--json", "Emit { address, privateKey } as JSON").option(
3389
+ "--fund",
3390
+ "Also mint a one-time onramp URL for the new wallet (does not auto-open)"
3391
+ ).option("--amount <amount>", "With --fund: USDC amount to pre-fill").option(
3392
+ "--use <provider>",
3393
+ "With --fund: onramp provider (coinbase | stripe)",
3394
+ "coinbase"
3395
+ ).action(
3396
+ async (options) => {
3397
+ const { analyticsService, apiService } = appContext.services;
3398
+ const privateKey = generatePrivateKey2();
3399
+ const account = privateKeyToAccount2(privateKey);
3400
+ let fundingUrl = null;
3401
+ if (options.fund) {
3402
+ const provider = parseProvider(options.use);
3403
+ fundingUrl = await apiService.withAccount(account).getFundingUrl(options.amount, provider);
3404
+ }
3405
+ if (options.json) {
3406
+ const payload = {
3407
+ address: account.address,
3408
+ privateKey
3409
+ };
3410
+ if (options.fund) {
3411
+ payload.fundingUrl = fundingUrl;
3412
+ }
3413
+ console.log(JSON.stringify(payload));
3414
+ } else {
3415
+ console.log("");
3416
+ console.log(
3417
+ "New wallet generated. Save these \u2014 they are NOT stored anywhere."
3418
+ );
3419
+ console.log("");
3420
+ console.log(
3421
+ " WARNING: Anyone with this private key can spend any funds sent"
3422
+ );
3423
+ console.log(
3424
+ " to the address. Don't paste it into chat, commit it to git, or"
3425
+ );
3426
+ console.log(" share your screen with it visible.");
3427
+ console.log("");
3428
+ console.log(` Address: ${account.address}`);
3429
+ console.log(` Private key: ${privateKey}`);
3430
+ console.log("");
3431
+ if (options.fund) {
3432
+ if (fundingUrl) {
3433
+ console.log("Funding URL (one-time use):");
3434
+ console.log(` ${fundingUrl}`);
3435
+ console.log("");
3436
+ } else {
3437
+ console.log(
3438
+ "Could not mint a funding URL \u2014 fund the address manually with USDC (Base)."
3439
+ );
3440
+ console.log("");
3441
+ }
3442
+ }
3443
+ console.log("Next steps:");
3444
+ if (!options.fund) {
3445
+ console.log(" - Fund the address with USDC (Base) before use.");
3446
+ }
3447
+ console.log(
3448
+ ` - Use it for a single command: ZERO_PRIVATE_KEY=${privateKey} zero <command>`
3449
+ );
3450
+ console.log(
3451
+ " - Or make it your default wallet: zero wallet set <privateKey> --force"
3452
+ );
3453
+ console.log("");
3454
+ }
3455
+ analyticsService.capture("wallet_generated", {
3456
+ format: options.json ? "json" : "text",
3457
+ funded: options.fund ?? false,
3458
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
3459
+ funding_url_minted: options.fund ? fundingUrl !== null : false
3460
+ });
3461
+ }
3462
+ );
3180
3463
  var walletCommand = (appContext) => {
3181
3464
  const cmd = new Command10("wallet").description("Manage your wallet");
3182
3465
  cmd.addCommand(walletBalanceCommand(appContext));
3183
3466
  cmd.addCommand(walletFundCommand(appContext));
3184
3467
  cmd.addCommand(walletAddressCommand(appContext));
3185
3468
  cmd.addCommand(walletSetCommand(appContext));
3469
+ cmd.addCommand(walletGenerateCommand(appContext));
3186
3470
  return cmd;
3187
3471
  };
3188
3472
 
@@ -3207,9 +3491,37 @@ var readPrivateKey = () => {
3207
3491
  }
3208
3492
  return null;
3209
3493
  };
3494
+ var linuxInstallHint = () => {
3495
+ return [
3496
+ " On Debian/Ubuntu: sudo apt install xdg-utils",
3497
+ " On RHEL/Fedora: sudo dnf install xdg-utils"
3498
+ ].join("\n");
3499
+ };
3500
+ var printManualFallback = (url) => {
3501
+ const lines = [
3502
+ "",
3503
+ "\u26A0\uFE0F Couldn't auto-open your browser (this is common on WSL, SSH, or",
3504
+ " containers \u2014 no xdg-open installed).",
3505
+ "",
3506
+ "Copy and paste this URL into your browser to claim:",
3507
+ "",
3508
+ ` ${url}`,
3509
+ ""
3510
+ ];
3511
+ if (process.platform === "linux") {
3512
+ lines.push(
3513
+ "Tip: install xdg-utils so 'zero welcome' can open the browser for you:",
3514
+ linuxInstallHint(),
3515
+ ""
3516
+ );
3517
+ }
3518
+ console.log(lines.join("\n"));
3519
+ };
3210
3520
  var welcomeCommand = (appContext) => new Command11("welcome").description("Claim your $5 welcome bonus.").action(async () => {
3211
3521
  const { analyticsService } = appContext.services;
3212
3522
  analyticsService.capture("welcome_started", {});
3523
+ let walletAddress;
3524
+ let url;
3213
3525
  try {
3214
3526
  let privateKey = readPrivateKey();
3215
3527
  if (!privateKey) {
@@ -3220,30 +3532,45 @@ var welcomeCommand = (appContext) => new Command11("welcome").description("Claim
3220
3532
  }
3221
3533
  }
3222
3534
  const account = privateKeyToAccount3(privateKey);
3223
- const walletAddress = getAddress(account.address);
3535
+ walletAddress = getAddress(account.address);
3224
3536
  const walletSignature = await account.signMessage({
3225
3537
  message: walletAddress
3226
3538
  });
3227
- const url = new URL("/welcome", appContext.env.ZERO_WEB_URL);
3539
+ url = new URL("/welcome", appContext.env.ZERO_WEB_URL);
3228
3540
  url.searchParams.set("wallet", walletAddress);
3229
3541
  url.searchParams.set("walletSignature", walletSignature);
3542
+ } catch (err) {
3543
+ analyticsService.capture("welcome_failed", {
3544
+ error: truncateError(
3545
+ err instanceof Error ? err.message : String(err)
3546
+ )
3547
+ });
3548
+ throw err;
3549
+ }
3550
+ const urlString = url.toString();
3551
+ analyticsService.setWalletAddress(walletAddress);
3552
+ try {
3553
+ await open2(urlString);
3230
3554
  console.log(
3231
- `Opening ${url.toString()}
3555
+ `Opening ${urlString}
3232
3556
 
3233
3557
  If your browser didn't open, paste the URL above.`
3234
3558
  );
3235
- await open2(url.toString());
3236
3559
  analyticsService.capture("welcome_link_opened", {
3237
3560
  // biome-ignore lint/style/useNamingConvention: snake_case for analytics
3238
- wallet_address: walletAddress
3561
+ open_method: "auto"
3239
3562
  });
3240
3563
  } catch (err) {
3241
- analyticsService.capture("welcome_failed", {
3242
- error: truncateError(
3564
+ printManualFallback(urlString);
3565
+ analyticsService.capture("welcome_link_opened", {
3566
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
3567
+ open_method: "manual_copy",
3568
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
3569
+ open_error: truncateError(
3243
3570
  err instanceof Error ? err.message : String(err)
3244
- )
3571
+ ),
3572
+ platform: process.platform
3245
3573
  });
3246
- throw err;
3247
3574
  }
3248
3575
  });
3249
3576
 
@@ -3414,12 +3741,30 @@ var AnalyticsService = class {
3414
3741
  setAgentHost(next) {
3415
3742
  this.agentHost = next;
3416
3743
  }
3744
+ // Per-invocation override for the `wallet_address` super-prop. Used by
3745
+ // commands that mint or load a wallet mid-process (init, wallet set,
3746
+ // welcome) so subsequent captures carry the now-known address without
3747
+ // needing to pass it as a per-event prop (which would collide with the
3748
+ // super-prop — see ZERO-43). distinctId is intentionally NOT changed:
3749
+ // the anon→wallet identity merge happens on the next CLI invocation
3750
+ // via PostHog alias, when AnalyticsService is constructed with both
3751
+ // the persisted anonId and the new walletAddress.
3752
+ setWalletAddress(walletAddress) {
3753
+ this.walletAddress = walletAddress;
3754
+ }
3417
3755
  capture(event, properties) {
3418
3756
  if (!this.posthog) return;
3419
3757
  this.posthog.capture({
3420
3758
  distinctId: this.distinctId,
3421
3759
  event,
3760
+ // Super-props are spread LAST so they always win against a same-
3761
+ // named per-event prop (see ZERO-43: a `source: undefined` on
3762
+ // `search_executed` was silently clobbering the `cli` surface stamp).
3763
+ // The reserved super-prop names are enforced by the events registry
3764
+ // test in `analytics/__tests__/events.test.ts` — any new event prop
3765
+ // that collides will fail CI.
3422
3766
  properties: {
3767
+ ...properties,
3423
3768
  source: "cli",
3424
3769
  // biome-ignore lint/style/useNamingConvention: snake_case is standard for analytics event properties
3425
3770
  cli_version: this.cliVersion,
@@ -3429,14 +3774,14 @@ var AnalyticsService = class {
3429
3774
  // biome-ignore lint/style/useNamingConvention: snake_case for analytics
3430
3775
  request_id: this.requestId,
3431
3776
  // biome-ignore lint/style/useNamingConvention: snake_case for analytics
3432
- agent_host: this.agentHost,
3433
- ...properties
3777
+ agent_host: this.agentHost
3434
3778
  }
3435
3779
  });
3436
3780
  }
3437
3781
  captureException(error, properties) {
3438
3782
  if (!this.posthog) return;
3439
3783
  this.posthog.captureException(error, this.distinctId, {
3784
+ ...properties,
3440
3785
  source: "cli",
3441
3786
  // biome-ignore lint/style/useNamingConvention: snake_case for analytics
3442
3787
  cli_version: this.cliVersion,
@@ -3446,8 +3791,7 @@ var AnalyticsService = class {
3446
3791
  // biome-ignore lint/style/useNamingConvention: snake_case for analytics
3447
3792
  request_id: this.requestId,
3448
3793
  // biome-ignore lint/style/useNamingConvention: snake_case for analytics
3449
- agent_host: this.agentHost,
3450
- ...properties
3794
+ agent_host: this.agentHost
3451
3795
  });
3452
3796
  }
3453
3797
  async shutdown() {
@@ -3507,18 +3851,23 @@ var StateService = class {
3507
3851
  }
3508
3852
  };
3509
3853
  // Walk recent searches newest-first, returning the first one whose
3510
- // results contain `capabilityId` (uid or slug match). Preferred over
3511
- // "loadLastSearch" for attributing rank — handles the parallel-search
3512
- // case where the most recent search isn't the one being fetched.
3513
- findSearchContextByCapability = (capabilityId) => {
3854
+ // results contain `capabilityRef` (matches against uid OR slug).
3855
+ // Preferred over "loadLastSearch" for attributing rank — handles the
3856
+ // parallel-search case where the most recent search isn't the one being
3857
+ // fetched.
3858
+ findSearchContextByCapability = (capabilityRef) => {
3514
3859
  const recent = this.loadRecentSearches();
3515
3860
  for (const search of recent.searches) {
3516
- const entry = search.capabilities.find((c) => c.id === capabilityId);
3861
+ const entry = search.capabilities.find(
3862
+ (c) => c.id === capabilityRef || c.slug === capabilityRef
3863
+ );
3517
3864
  if (entry) {
3518
3865
  return {
3519
3866
  searchId: search.searchId,
3520
3867
  resultRank: entry.position,
3521
3868
  capabilityId: entry.id,
3869
+ capabilityUid: entry.id,
3870
+ capabilitySlug: entry.slug ?? null,
3522
3871
  url: entry.url,
3523
3872
  displayCostAmount: entry.displayCostAmount
3524
3873
  };
@@ -3543,6 +3892,8 @@ var StateService = class {
3543
3892
  searchId: search.searchId,
3544
3893
  resultRank: entry.position,
3545
3894
  capabilityId: entry.id,
3895
+ capabilityUid: entry.id,
3896
+ capabilitySlug: entry.slug ?? null,
3546
3897
  url: entry.url,
3547
3898
  displayCostAmount: entry.displayCostAmount
3548
3899
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeroxyz/cli",
3
- "version": "0.0.37",
3
+ "version": "0.0.38",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "zero": "dist/index.js",