@zeroxyz/cli 0.0.36 → 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.
package/README.md CHANGED
@@ -46,7 +46,7 @@ Search for capabilities by free-text query.
46
46
  zero search "image classification"
47
47
  ```
48
48
 
49
- Results are numbered. Use `zero get <number>` to view details.
49
+ Results are numbered and include public rating/review context. Use `zero get <number>` to view details.
50
50
 
51
51
  **Cost filtering.** By default, results are filtered to capabilities priced **≤ $30/call** as a wallet-safety cap. Override per-call:
52
52
 
@@ -56,7 +56,7 @@ zero search "expensive deep research" --max-cost 100 # raise the cap for hard ta
56
56
  zero search "image classification" --free # only free capabilities
57
57
  ```
58
58
 
59
- Other useful filters: `--min-rating <1-5>`, `--min-trust <0-100>`, `--protocol x402|mpp`, `--status healthy|degraded|down`, `--source <name>`, `--all` (no trust/health filtering).
59
+ Other useful filters: `--protocol x402|mpp`, `--status healthy|degraded|down`, `--source <name>`, `--all` (disables default quality filtering).
60
60
 
61
61
  ### `zero get <position>`
62
62
 
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.36",
10
+ version: "0.0.38",
11
11
  type: "module",
12
12
  bin: {
13
13
  zero: "dist/index.js",
@@ -23,7 +23,7 @@ var package_default = {
23
23
  },
24
24
  scripts: {
25
25
  build: "tsup src/index.ts --format esm --out-dir dist --clean",
26
- "build:binary": "tsup --config tsup.binary.ts && cp -r skills hooks dist/pkg/ && pnpm exec pkg dist/pkg/index.cjs --config pkg.json --targets node24-macos-arm64,node24-macos-x64,node24-linux-x64 --output dist/bin/zero",
26
+ "build:binary": "tsup --config tsup.binary.ts && cp -r skills hooks dist/pkg/ && pnpm exec pkg dist/pkg/index.cjs --config pkg.json --targets node24-macos-arm64,node24-macos-x64,node24-linux-x64,node24-linux-arm64 --output dist/bin/zero",
27
27
  prepublishOnly: "pnpm run build",
28
28
  dev: "ZERO_ENV=development tsx src/index.ts",
29
29
  cli: "ZERO_ENV=development ZERO_API_URL=http://localhost:1111 tsx src/index.ts",
@@ -41,7 +41,7 @@ var package_default = {
41
41
  "@x402/extensions": "^2.9.0",
42
42
  "@x402/fetch": "^2.9.0",
43
43
  commander: "^13.0.0",
44
- mppx: "^0.5.9",
44
+ mppx: "^0.6.9",
45
45
  open: "^11.0.0",
46
46
  "posthog-node": "^5.29.2",
47
47
  viem: "^2.47.10",
@@ -72,6 +72,13 @@ import { z as z2 } from "zod";
72
72
  // src/services/api-service.ts
73
73
  import { createHash } from "crypto";
74
74
  import z from "zod";
75
+ var ratingSchema = z.object({
76
+ score: z.string(),
77
+ successRate: z.string(),
78
+ reviews: z.number(),
79
+ stars: z.string().nullable().optional(),
80
+ state: z.enum(["unrated", "rated"]).optional()
81
+ });
75
82
  var searchResultSchema = z.object({
76
83
  id: z.string(),
77
84
  position: z.number(),
@@ -83,17 +90,10 @@ var searchResultSchema = z.object({
83
90
  url: z.string(),
84
91
  urlTemplate: z.string().nullable().optional(),
85
92
  cost: z.object({ amount: z.string(), asset: z.string() }),
86
- rating: z.object({
87
- score: z.string(),
88
- successRate: z.string(),
89
- reviews: z.number(),
90
- stars: z.string().nullable().optional(),
91
- state: z.enum(["unrated", "rated"]).optional()
92
- }),
93
- trustScore: z.number().nullable().optional(),
94
- trustSignalCount: z.number().optional(),
93
+ reviewCount: z.number().optional(),
94
+ rating: ratingSchema,
95
95
  availabilityStatus: z.enum(["healthy", "degraded", "down", "unknown"]).nullable().optional(),
96
- relevanceScore: z.number().optional()
96
+ displayStatus: z.enum(["healthy", "stable", "degraded", "unhealthy", "unknown"]).optional()
97
97
  });
98
98
  var searchResponseSchema = z.object({
99
99
  searchId: z.string(),
@@ -115,15 +115,11 @@ 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
- rating: z.object({
121
- score: z.string(),
122
- successRate: z.string(),
123
- reviews: z.number(),
124
- stars: z.string().nullable().optional(),
125
- state: z.enum(["unrated", "rated"]).optional()
126
- }),
121
+ reviewCount: z.number(),
122
+ rating: ratingSchema,
127
123
  priceObserved: z.object({
128
124
  minCents: z.string().nullable(),
129
125
  medianCents: z.string().nullable(),
@@ -146,13 +142,8 @@ var capabilityResponseSchema = z.object({
146
142
  priority: z.number()
147
143
  })
148
144
  ).nullable(),
149
- trustScore: z.number().nullable().optional(),
150
- trustComponents: z.object({
151
- apiQuality: z.number().nullable(),
152
- blockchainActivity: z.number().nullable(),
153
- performance: z.number().nullable()
154
- }).nullable().optional(),
155
145
  availabilityStatus: z.enum(["healthy", "degraded", "down", "unknown"]).nullable().optional(),
146
+ displayStatus: z.enum(["healthy", "stable", "degraded", "unhealthy", "unknown"]).optional(),
156
147
  activationCount: z.number().optional(),
157
148
  lastUsedAt: z.string().nullable().optional(),
158
149
  lastSuccessfullyRanAt: z.string().nullable().optional()
@@ -238,7 +229,7 @@ var buildCanonicalMessage = (method, path, body, timestamp, nonce) => {
238
229
  const bodyHash = createHash("sha256").update(body ?? "").digest("hex");
239
230
  return `${method}:${path}:${bodyHash}:${timestamp}:${nonce}`;
240
231
  };
241
- var ApiService = class {
232
+ var ApiService = class _ApiService {
242
233
  constructor(baseUrl, account) {
243
234
  this.baseUrl = baseUrl;
244
235
  this.account = account;
@@ -246,6 +237,7 @@ var ApiService = class {
246
237
  }
247
238
  walletAddress;
248
239
  account;
240
+ withAccount = (account) => new _ApiService(this.baseUrl, account);
249
241
  signRequest = async (method, path, body) => {
250
242
  if (!this.account) throw new Error("No private key configured");
251
243
  const timestamp = Math.floor(Date.now() / 1e3).toString();
@@ -1130,8 +1122,9 @@ var PaymentService = class {
1130
1122
  return { viemChain: tempoTestnetChain, token: PATHUSD_TEMPO };
1131
1123
  }
1132
1124
  };
1133
- getBalanceRaw = async (chain) => {
1134
- if (!this.account) return 0n;
1125
+ getBalanceRaw = async (chain, address) => {
1126
+ const target = address ?? this.account?.address;
1127
+ if (!target) return 0n;
1135
1128
  const { viemChain, token } = this.resolveChainConfig(chain);
1136
1129
  const client = createPublicClient({
1137
1130
  chain: viemChain,
@@ -1141,7 +1134,7 @@ var PaymentService = class {
1141
1134
  address: token,
1142
1135
  abi: ERC20_BALANCE_ABI,
1143
1136
  functionName: "balanceOf",
1144
- args: [this.account.address]
1137
+ args: [target]
1145
1138
  });
1146
1139
  return balance;
1147
1140
  };
@@ -1161,6 +1154,13 @@ var PaymentService = class {
1161
1154
  ]);
1162
1155
  return { amount: formatUnits(baseRaw + tempoRaw, 6), asset: "USDC" };
1163
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
+ };
1164
1164
  };
1165
1165
 
1166
1166
  // src/util/infer-schema.ts
@@ -1247,30 +1247,69 @@ var isJsonContentType = (contentType) => {
1247
1247
  const ct = contentType.toLowerCase().split(";")[0]?.trim() ?? "";
1248
1248
  return ct === "application/json" || ct.endsWith("+json");
1249
1249
  };
1250
- var MAX_REQUEST_BODY_BYTES = 10 * 1024 * 1024;
1251
- 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) => {
1252
1287
  const fromFile = (spec) => {
1253
1288
  if (spec === "@-") return readFileSync3(0);
1254
1289
  return readFileSync3(resolvePath(spec.slice(1)));
1255
1290
  };
1256
- if (readStdin && rawData !== void 0) {
1291
+ if (readStdin2 && rawData !== void 0) {
1257
1292
  throw new Error(
1258
1293
  "Conflicting body sources: use either --data-stdin or -d, not both."
1259
1294
  );
1260
1295
  }
1261
1296
  let body;
1262
- if (readStdin) {
1297
+ let cap;
1298
+ if (readStdin2) {
1263
1299
  body = readFileSync3(0);
1300
+ cap = MAX_FILE_REQUEST_BODY_BYTES;
1264
1301
  } else if (rawData?.startsWith("@")) {
1265
1302
  body = fromFile(rawData);
1303
+ cap = MAX_FILE_REQUEST_BODY_BYTES;
1266
1304
  } else {
1267
1305
  body = rawData;
1306
+ cap = MAX_INLINE_REQUEST_BODY_BYTES;
1268
1307
  }
1269
1308
  if (body !== void 0) {
1270
1309
  const bytes = Buffer.isBuffer(body) ? body.length : Buffer.byteLength(body, "utf8");
1271
- if (bytes > MAX_REQUEST_BODY_BYTES) {
1310
+ if (bytes > cap) {
1272
1311
  throw new Error(
1273
- `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.`
1274
1313
  );
1275
1314
  }
1276
1315
  }
@@ -1381,14 +1420,16 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1381
1420
  capName = cap.name;
1382
1421
  }
1383
1422
  const searchResult = await apiService.search({ query: capName });
1384
- const slugFoundInResults = searchResult.capabilities.some(
1423
+ const matchedEntry = searchResult.capabilities.find(
1385
1424
  (c) => c.slug === options.capability || c.id === options.capability
1386
1425
  );
1426
+ const slugFoundInResults = Boolean(matchedEntry);
1387
1427
  stateService.saveLastSearch({
1388
1428
  searchId: searchResult.searchId,
1389
1429
  capabilities: searchResult.capabilities.map((c) => ({
1390
1430
  position: c.position,
1391
1431
  id: c.id,
1432
+ slug: c.slug,
1392
1433
  url: c.url,
1393
1434
  urlTemplate: c.urlTemplate ?? null,
1394
1435
  displayCostAmount: c.cost.amount
@@ -1406,7 +1447,13 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1406
1447
  slugFoundInResults
1407
1448
  });
1408
1449
  analyticsService.capture("capability_viewed", {
1450
+ // Existing field preserved as the raw --capability
1451
+ // input for back-compat.
1409
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,
1410
1457
  fromLastSearch: false,
1411
1458
  searchId: searchResult.searchId,
1412
1459
  triggeredBy: "slug_handoff"
@@ -1423,7 +1470,7 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1423
1470
  } catch (err) {
1424
1471
  const message = err instanceof Error ? err.message : "Failed to read request body";
1425
1472
  analyticsService.capture("fetch_error", {
1426
- 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",
1427
1474
  url: redactUrl(resolvedUrl),
1428
1475
  error: truncateError(message)
1429
1476
  });
@@ -1465,6 +1512,8 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1465
1512
  });
1466
1513
  const matchCtx = explicitCapabilityCtx ?? urlCtx;
1467
1514
  const capabilityId = options.capability ?? matchCtx?.capabilityId ?? null;
1515
+ const capabilityUid = matchCtx?.capabilityUid ?? null;
1516
+ const capabilitySlug = matchCtx?.capabilitySlug ?? null;
1468
1517
  const searchId = matchCtx?.searchId;
1469
1518
  const resultRank = matchCtx?.resultRank;
1470
1519
  const matchedDisplayCostAmount = matchCtx?.displayCostAmount;
@@ -1614,10 +1663,12 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1614
1663
  const isFailure = !finalResponse || typeof status === "number" && (status < 200 || status >= 300);
1615
1664
  let errorSnippetHash;
1616
1665
  let errorSnippetLength;
1666
+ let upstreamErrorMessage;
1617
1667
  if (isFailure && body && !bodyIsBinary) {
1618
1668
  const snippet = body.slice(0, 500);
1619
1669
  errorSnippetHash = createHash3("sha256").update(snippet).digest("hex");
1620
1670
  errorSnippetLength = snippet.length;
1671
+ upstreamErrorMessage = extractUpstreamErrorMessage(body);
1621
1672
  }
1622
1673
  let runId = null;
1623
1674
  if (capabilityId && apiService.walletAddress) {
@@ -1661,7 +1712,7 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1661
1712
  );
1662
1713
  }
1663
1714
  }
1664
- 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";
1665
1716
  analyticsService.capture("fetch_executed", {
1666
1717
  url: redactUrl(resolvedUrl),
1667
1718
  status,
@@ -1670,6 +1721,13 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1670
1721
  hasPayment: !!paymentMeta,
1671
1722
  paymentProtocol: paymentMeta?.protocol,
1672
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,
1673
1731
  capabilityId: capabilityId ?? void 0,
1674
1732
  searchId: searchId ?? void 0,
1675
1733
  resultRank: resultRank ?? void 0,
@@ -1679,15 +1737,21 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1679
1737
  });
1680
1738
  const isFetchFailure = Boolean(fetchError) || !finalResponse || typeof status === "number" && (status < 200 || status >= 300);
1681
1739
  if (isFetchFailure) {
1682
- 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";
1683
1741
  analyticsService.capture("fetch_error", {
1684
1742
  cliErrorClass,
1743
+ capabilityUid: capabilityUid ?? void 0,
1744
+ capabilitySlug: capabilitySlug ?? void 0,
1685
1745
  capabilityId: capabilityId ?? void 0,
1686
1746
  searchId: searchId ?? void 0,
1687
1747
  resultRank: resultRank ?? void 0,
1688
1748
  url: redactUrl(resolvedUrl),
1749
+ status: typeof status === "number" ? status : void 0,
1750
+ upstreamErrorMessage,
1751
+ errorSnippetHash,
1752
+ errorSnippetLength,
1689
1753
  error: truncateError(
1690
- fetchError?.message ?? skipReasons.join("; ")
1754
+ fetchError?.message ?? upstreamErrorMessage ?? skipReasons.join("; ")
1691
1755
  ),
1692
1756
  skippedRun: !runId && skipReasons.length > 0
1693
1757
  });
@@ -1695,6 +1759,16 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1695
1759
  if (fetchError && !options.json) {
1696
1760
  console.error(` Fetch failed: ${fetchError.message}`);
1697
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
+ }
1698
1772
  if (options.json) {
1699
1773
  const responseStatus = finalResponse?.status ?? null;
1700
1774
  const ok = responseStatus !== null && responseStatus >= 200 && responseStatus < 300;
@@ -1775,10 +1849,19 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1775
1849
 
1776
1850
  // src/commands/get-command.ts
1777
1851
  import { Command as Command4 } from "commander";
1778
- var formatReviewCount = (count) => {
1779
- if (count >= 1e3) return `${(count / 1e3).toFixed(1)}k`;
1780
- return count.toString();
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);
1781
1862
  };
1863
+
1864
+ // src/commands/get-command.ts
1782
1865
  var formatRelativeTimestamp = (iso) => {
1783
1866
  if (!iso) return "never";
1784
1867
  const then = new Date(iso).getTime();
@@ -1795,22 +1878,6 @@ var formatRelativeTimestamp = (iso) => {
1795
1878
  if (diffMo < 12) return `${diffMo}mo ago`;
1796
1879
  return `${Math.round(diffMo / 12)}y ago`;
1797
1880
  };
1798
- var formatTrustScore = (capability) => {
1799
- if (capability.trustScore != null) {
1800
- return `Trust Score: ${capability.trustScore}/100`;
1801
- }
1802
- return "Trust Score: --";
1803
- };
1804
- var formatTrustComponent = (label, value) => {
1805
- const display = value != null ? `${value}/100` : "--";
1806
- return ` ${label.padEnd(22)}${display}`;
1807
- };
1808
- var centsToDollars = (cents) => {
1809
- const value = Number.parseFloat(cents) / 100;
1810
- if (value < 0.01) return value.toFixed(4);
1811
- if (value < 1) return value.toFixed(3);
1812
- return value.toFixed(2);
1813
- };
1814
1881
  var formatCost = (capability) => {
1815
1882
  const lines = [];
1816
1883
  const observed = capability.priceObserved;
@@ -1820,6 +1887,10 @@ var formatCost = (capability) => {
1820
1887
  const median = observed.medianCents ? centsToDollars(observed.medianCents) : null;
1821
1888
  const detail = median ? `median $${median}, n=${observed.sampleCount}` : `n=${observed.sampleCount}`;
1822
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`);
1823
1894
  } else {
1824
1895
  lines.push(` Cost: $${capability.displayCostAmount}/call`);
1825
1896
  }
@@ -1829,6 +1900,10 @@ var formatCost = (capability) => {
1829
1900
  }
1830
1901
  return lines;
1831
1902
  };
1903
+ var formatReviewCount = (count) => {
1904
+ if (count >= 1e3) return `${(count / 1e3).toFixed(1)}k`;
1905
+ return count.toString();
1906
+ };
1832
1907
  var formatRating = (rating) => {
1833
1908
  if (rating.state === "unrated") return "unrated";
1834
1909
  const successPct = `${Math.round(Number.parseFloat(rating.successRate) * 100)}%`;
@@ -1915,35 +1990,20 @@ var buildTryItExample = (capability) => {
1915
1990
  var formatCapability = (capability) => {
1916
1991
  const lines = [];
1917
1992
  lines.push(capability.name);
1918
- lines.push(` ${formatTrustScore(capability)}`);
1919
- if (capability.trustComponents) {
1920
- lines.push(
1921
- formatTrustComponent(
1922
- "API Quality:",
1923
- capability.trustComponents.apiQuality
1924
- )
1925
- );
1926
- lines.push(
1927
- formatTrustComponent(
1928
- "Blockchain Activity:",
1929
- capability.trustComponents.blockchainActivity
1930
- )
1931
- );
1932
- lines.push(
1933
- formatTrustComponent(
1934
- "Performance:",
1935
- capability.trustComponents.performance
1936
- )
1937
- );
1938
- }
1939
1993
  lines.push(` Rating: ${formatRating(capability.rating)}`);
1940
- lines.push(` Status: ${capability.availabilityStatus ?? "unknown"}`);
1994
+ lines.push(` Status: ${capability.displayStatus ?? "unknown"}`);
1941
1995
  lines.push(...formatCost(capability));
1942
1996
  lines.push(` URL: ${capability.url}`);
1943
1997
  lines.push(` Method: ${capability.method}`);
1944
1998
  lines.push(
1945
1999
  ` Last successful run: ${formatRelativeTimestamp(capability.lastSuccessfullyRanAt)}`
1946
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
+ }
1947
2007
  lines.push(...buildTryItExample(capability));
1948
2008
  return lines.join("\n");
1949
2009
  };
@@ -1952,7 +2012,7 @@ var getCommand = (appContext) => new Command4("get").description(
1952
2012
  ).argument(
1953
2013
  "<identifier>",
1954
2014
  "Position number from search results, or a capability slug"
1955
- ).option("--formatted", "Output formatted trust breakdown").option(
2015
+ ).option("--formatted", "Output formatted capability details").option(
1956
2016
  "--agent <name>",
1957
2017
  "Identify your agent host for this invocation. Overrides auto-detect for this call only."
1958
2018
  ).action(async (identifier, options) => {
@@ -1996,7 +2056,15 @@ var getCommand = (appContext) => new Command4("get").description(
1996
2056
  console.log(JSON.stringify(capability, null, 2));
1997
2057
  }
1998
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.
1999
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,
2000
2068
  fromLastSearch: isPosition,
2001
2069
  ...isPosition ? { position } : {},
2002
2070
  ...searchId ? { searchId } : {}
@@ -2130,7 +2198,12 @@ var printReadyFooter = () => {
2130
2198
  const lines = [
2131
2199
  "",
2132
2200
  ` ${color.boldGreen("Zero is ready!")} Zero works best with an AI agent.`,
2133
- ` 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`,
2134
2207
  " and try this prompt to get started:",
2135
2208
  "",
2136
2209
  ` ${color.cyan("What is zero and how do I use it?")}`,
@@ -2593,12 +2666,13 @@ To remove them, run: ${color.cyan("zero init cleanup")}`
2593
2666
  sectionDivider();
2594
2667
  console.error(printReadyFooter());
2595
2668
  currentStep = "complete";
2669
+ if (walletAddress) {
2670
+ appContext.services.analyticsService.setWalletAddress(walletAddress);
2671
+ }
2596
2672
  appContext.services.analyticsService.capture("wallet_initialized", {
2597
2673
  // biome-ignore lint/style/useNamingConvention: snake_case for analytics
2598
2674
  wallet_created: walletCreated,
2599
2675
  // biome-ignore lint/style/useNamingConvention: snake_case for analytics
2600
- wallet_address: walletAddress,
2601
- // biome-ignore lint/style/useNamingConvention: snake_case for analytics
2602
2676
  agents_detected: agentsDetected,
2603
2677
  // biome-ignore lint/style/useNamingConvention: snake_case for analytics
2604
2678
  agents_detected_count: agentsDetected.length,
@@ -2922,7 +2996,7 @@ var formatReviewCount2 = (count) => {
2922
2996
  return count.toString();
2923
2997
  };
2924
2998
  var formatRatingBadge = (rating) => {
2925
- if (rating.state === "unrated") return "\u2014 unrated";
2999
+ if (rating.state === "unrated") return "unrated";
2926
3000
  const successPct = `${Math.round(Number.parseFloat(rating.successRate) * 100)}%`;
2927
3001
  const reviews = formatReviewCount2(rating.reviews);
2928
3002
  if (rating.stars) {
@@ -2930,13 +3004,7 @@ var formatRatingBadge = (rating) => {
2930
3004
  }
2931
3005
  return `${successPct} success \xB7 ${reviews} reviews`;
2932
3006
  };
2933
- var formatTrustBadge = (item) => {
2934
- if (item.trustScore != null) {
2935
- return `Trust: ${item.trustScore}`;
2936
- }
2937
- return "Trust: --";
2938
- };
2939
- var formatHealthBadge = (status) => {
3007
+ var formatStatusBadge = (status) => {
2940
3008
  if (!status || status === "unknown") return "";
2941
3009
  return ` \u2014 ${status}`;
2942
3010
  };
@@ -2946,21 +3014,20 @@ var formatSearchResults = (results) => {
2946
3014
  const baseName = r.canonicalName ?? r.name;
2947
3015
  const displayName = r.brandName ? `${r.brandName} ${baseName}` : baseName;
2948
3016
  const displayDescription = r.whatItDoes ?? r.description;
2949
- const trustBadge = formatTrustBadge(r);
2950
3017
  const ratingBadge = formatRatingBadge(r.rating);
2951
- const healthBadge = formatHealthBadge(r.availabilityStatus);
2952
- const relevance = r.relevanceScore != null ? ` [${r.relevanceScore.toFixed(3)}]` : "";
2953
- return ` ${r.position}. ${displayName} \u2014 $${r.cost.amount}/call \u2014 ${trustBadge} \u2014 ${ratingBadge}${healthBadge}${relevance}
3018
+ const statusBadge = formatStatusBadge(r.displayStatus);
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}
2954
3021
  "${displayDescription}"`;
2955
3022
  }).join("\n");
2956
3023
  };
2957
3024
  var searchCommand = (appContext) => new Command8("search").description("Search for capabilities").argument("<query>", "Search query").option("--json", "Output raw JSON to stdout").option("--offset <n>", "Pagination offset", Number).option("--limit <n>", "Results per page", Number).option("--free", "Only show free capabilities").option(
2958
3025
  "--max-cost <amount>",
2959
3026
  `Maximum cost per call in USD (default: ${DEFAULT_MAX_COST_USD})`
2960
- ).option("--min-rating <stars>", "Minimum star rating (1-5)", Number).option("--protocol <protocol>", "Payment protocol (x402 or mpp)").option("--min-trust <n>", "Minimum trust score (0-100)", Number).option(
3027
+ ).option("--protocol <protocol>", "Payment protocol (x402 or mpp)").option(
2961
3028
  "--status <status>",
2962
3029
  "Filter by availability (healthy, degraded, down)"
2963
- ).option("--all", "Show all results (no trust or health filtering)").option(
3030
+ ).option("--all", "Disable default quality filtering").option(
2964
3031
  "--source <source>",
2965
3032
  "Only show results from this crawl source (e.g. mpp, bazaar)"
2966
3033
  ).option(
@@ -3000,9 +3067,7 @@ var searchCommand = (appContext) => new Command8("search").description("Search f
3000
3067
  limit: options.limit,
3001
3068
  freeOnly: options.free,
3002
3069
  maxCost: effectiveMaxCost,
3003
- minRating: options.minRating,
3004
3070
  protocol: options.protocol,
3005
- minTrust: options.minTrust,
3006
3071
  availabilityStatus: options.status,
3007
3072
  includeAll: options.all,
3008
3073
  source: options.source,
@@ -3020,13 +3085,11 @@ var searchCommand = (appContext) => new Command8("search").description("Search f
3020
3085
  freeOnly: options.free ?? false,
3021
3086
  maxCost: effectiveMaxCost,
3022
3087
  maxCostDefaulted: appliedDefaultMaxCost,
3023
- minRating: options.minRating,
3024
3088
  protocol: options.protocol,
3025
- minTrust: options.minTrust,
3026
3089
  availabilityStatus: options.status,
3027
3090
  includeAll: options.all ?? false,
3028
- source: options.source,
3029
- excludeSource: options.excludeSource,
3091
+ listingSource: options.source,
3092
+ excludeListingSource: options.excludeSource,
3030
3093
  json: options.json ?? false
3031
3094
  });
3032
3095
  if (options.json) {
@@ -3048,6 +3111,7 @@ var searchCommand = (appContext) => new Command8("search").description("Search f
3048
3111
  capabilities: result.capabilities.map((c) => ({
3049
3112
  position: c.position,
3050
3113
  id: c.id,
3114
+ slug: c.slug,
3051
3115
  url: c.url,
3052
3116
  urlTemplate: c.urlTemplate ?? null,
3053
3117
  displayCostAmount: c.cost.amount
@@ -3098,9 +3162,43 @@ import { homedir as homedir3 } from "os";
3098
3162
  import { join as join3 } from "path";
3099
3163
  import { Command as Command10 } from "commander";
3100
3164
  import open from "open";
3101
- import { privateKeyToAccount as privateKeyToAccount2 } from "viem/accounts";
3102
- var walletBalanceCommand = (appContext) => new Command10("balance").description("Show wallet balance").action(async () => {
3103
- 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
+ }
3104
3202
  const balance = await walletService.getBalance();
3105
3203
  if (balance === null) {
3106
3204
  console.error("No wallet configured. Run `zero init` first.");
@@ -3114,6 +3212,20 @@ var walletBalanceCommand = (appContext) => new Command10("balance").description(
3114
3212
  }
3115
3213
  console.log(`${balance.amount} ${balance.asset}`);
3116
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
+ };
3117
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(
3118
3230
  "--no-open",
3119
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)"
@@ -3121,9 +3233,55 @@ var walletFundCommand = (appContext) => new Command10("fund").description("Fund
3121
3233
  "--use <provider>",
3122
3234
  "Onramp provider: coinbase or stripe",
3123
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."
3124
3239
  ).action(
3125
3240
  async (amount, options) => {
3126
- 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
+ }
3127
3285
  const address = walletService.getAddress();
3128
3286
  if (!address) {
3129
3287
  console.error("No wallet configured. Run `zero init` first.");
@@ -3139,11 +3297,7 @@ ${address}`);
3139
3297
  });
3140
3298
  return;
3141
3299
  }
3142
- const provider = options.use === "stripe" ? "stripe" : "coinbase";
3143
- const url = await appContext.services.apiService.getFundingUrl(
3144
- amount,
3145
- provider
3146
- );
3300
+ const url = await apiService.getFundingUrl(amount, provider);
3147
3301
  if (url) {
3148
3302
  if (options.open) {
3149
3303
  await open(url);
@@ -3224,18 +3378,95 @@ var walletSetCommand = (appContext) => new Command10("set").description("Set wal
3224
3378
  )
3225
3379
  );
3226
3380
  console.log(`Wallet set: ${account.address}`);
3381
+ analyticsService.setWalletAddress(account.address);
3227
3382
  analyticsService.capture("wallet_set", {
3228
- // biome-ignore lint/style/useNamingConvention: snake_case for analytics
3229
- wallet_address: account.address,
3230
3383
  force: options.force ?? false
3231
3384
  });
3232
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
+ );
3233
3463
  var walletCommand = (appContext) => {
3234
3464
  const cmd = new Command10("wallet").description("Manage your wallet");
3235
3465
  cmd.addCommand(walletBalanceCommand(appContext));
3236
3466
  cmd.addCommand(walletFundCommand(appContext));
3237
3467
  cmd.addCommand(walletAddressCommand(appContext));
3238
3468
  cmd.addCommand(walletSetCommand(appContext));
3469
+ cmd.addCommand(walletGenerateCommand(appContext));
3239
3470
  return cmd;
3240
3471
  };
3241
3472
 
@@ -3260,9 +3491,37 @@ var readPrivateKey = () => {
3260
3491
  }
3261
3492
  return null;
3262
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
+ };
3263
3520
  var welcomeCommand = (appContext) => new Command11("welcome").description("Claim your $5 welcome bonus.").action(async () => {
3264
3521
  const { analyticsService } = appContext.services;
3265
3522
  analyticsService.capture("welcome_started", {});
3523
+ let walletAddress;
3524
+ let url;
3266
3525
  try {
3267
3526
  let privateKey = readPrivateKey();
3268
3527
  if (!privateKey) {
@@ -3273,30 +3532,45 @@ var welcomeCommand = (appContext) => new Command11("welcome").description("Claim
3273
3532
  }
3274
3533
  }
3275
3534
  const account = privateKeyToAccount3(privateKey);
3276
- const walletAddress = getAddress(account.address);
3535
+ walletAddress = getAddress(account.address);
3277
3536
  const walletSignature = await account.signMessage({
3278
3537
  message: walletAddress
3279
3538
  });
3280
- const url = new URL("/welcome", appContext.env.ZERO_WEB_URL);
3539
+ url = new URL("/welcome", appContext.env.ZERO_WEB_URL);
3281
3540
  url.searchParams.set("wallet", walletAddress);
3282
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);
3283
3554
  console.log(
3284
- `Opening ${url.toString()}
3555
+ `Opening ${urlString}
3285
3556
 
3286
3557
  If your browser didn't open, paste the URL above.`
3287
3558
  );
3288
- await open2(url.toString());
3289
3559
  analyticsService.capture("welcome_link_opened", {
3290
3560
  // biome-ignore lint/style/useNamingConvention: snake_case for analytics
3291
- wallet_address: walletAddress
3561
+ open_method: "auto"
3292
3562
  });
3293
3563
  } catch (err) {
3294
- analyticsService.capture("welcome_failed", {
3295
- 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(
3296
3570
  err instanceof Error ? err.message : String(err)
3297
- )
3571
+ ),
3572
+ platform: process.platform
3298
3573
  });
3299
- throw err;
3300
3574
  }
3301
3575
  });
3302
3576
 
@@ -3467,12 +3741,30 @@ var AnalyticsService = class {
3467
3741
  setAgentHost(next) {
3468
3742
  this.agentHost = next;
3469
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
+ }
3470
3755
  capture(event, properties) {
3471
3756
  if (!this.posthog) return;
3472
3757
  this.posthog.capture({
3473
3758
  distinctId: this.distinctId,
3474
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.
3475
3766
  properties: {
3767
+ ...properties,
3476
3768
  source: "cli",
3477
3769
  // biome-ignore lint/style/useNamingConvention: snake_case is standard for analytics event properties
3478
3770
  cli_version: this.cliVersion,
@@ -3482,14 +3774,14 @@ var AnalyticsService = class {
3482
3774
  // biome-ignore lint/style/useNamingConvention: snake_case for analytics
3483
3775
  request_id: this.requestId,
3484
3776
  // biome-ignore lint/style/useNamingConvention: snake_case for analytics
3485
- agent_host: this.agentHost,
3486
- ...properties
3777
+ agent_host: this.agentHost
3487
3778
  }
3488
3779
  });
3489
3780
  }
3490
3781
  captureException(error, properties) {
3491
3782
  if (!this.posthog) return;
3492
3783
  this.posthog.captureException(error, this.distinctId, {
3784
+ ...properties,
3493
3785
  source: "cli",
3494
3786
  // biome-ignore lint/style/useNamingConvention: snake_case for analytics
3495
3787
  cli_version: this.cliVersion,
@@ -3499,8 +3791,7 @@ var AnalyticsService = class {
3499
3791
  // biome-ignore lint/style/useNamingConvention: snake_case for analytics
3500
3792
  request_id: this.requestId,
3501
3793
  // biome-ignore lint/style/useNamingConvention: snake_case for analytics
3502
- agent_host: this.agentHost,
3503
- ...properties
3794
+ agent_host: this.agentHost
3504
3795
  });
3505
3796
  }
3506
3797
  async shutdown() {
@@ -3560,18 +3851,23 @@ var StateService = class {
3560
3851
  }
3561
3852
  };
3562
3853
  // Walk recent searches newest-first, returning the first one whose
3563
- // results contain `capabilityId` (uid or slug match). Preferred over
3564
- // "loadLastSearch" for attributing rank — handles the parallel-search
3565
- // case where the most recent search isn't the one being fetched.
3566
- 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) => {
3567
3859
  const recent = this.loadRecentSearches();
3568
3860
  for (const search of recent.searches) {
3569
- const entry = search.capabilities.find((c) => c.id === capabilityId);
3861
+ const entry = search.capabilities.find(
3862
+ (c) => c.id === capabilityRef || c.slug === capabilityRef
3863
+ );
3570
3864
  if (entry) {
3571
3865
  return {
3572
3866
  searchId: search.searchId,
3573
3867
  resultRank: entry.position,
3574
3868
  capabilityId: entry.id,
3869
+ capabilityUid: entry.id,
3870
+ capabilitySlug: entry.slug ?? null,
3575
3871
  url: entry.url,
3576
3872
  displayCostAmount: entry.displayCostAmount
3577
3873
  };
@@ -3596,6 +3892,8 @@ var StateService = class {
3596
3892
  searchId: search.searchId,
3597
3893
  resultRank: entry.position,
3598
3894
  capabilityId: entry.id,
3895
+ capabilityUid: entry.id,
3896
+ capabilitySlug: entry.slug ?? null,
3599
3897
  url: entry.url,
3600
3898
  displayCostAmount: entry.displayCostAmount
3601
3899
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeroxyz/cli",
3
- "version": "0.0.36",
3
+ "version": "0.0.38",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "zero": "dist/index.js",
@@ -16,7 +16,7 @@
16
16
  },
17
17
  "scripts": {
18
18
  "build": "tsup src/index.ts --format esm --out-dir dist --clean",
19
- "build:binary": "tsup --config tsup.binary.ts && cp -r skills hooks dist/pkg/ && pnpm exec pkg dist/pkg/index.cjs --config pkg.json --targets node24-macos-arm64,node24-macos-x64,node24-linux-x64 --output dist/bin/zero",
19
+ "build:binary": "tsup --config tsup.binary.ts && cp -r skills hooks dist/pkg/ && pnpm exec pkg dist/pkg/index.cjs --config pkg.json --targets node24-macos-arm64,node24-macos-x64,node24-linux-x64,node24-linux-arm64 --output dist/bin/zero",
20
20
  "prepublishOnly": "pnpm run build",
21
21
  "dev": "ZERO_ENV=development tsx src/index.ts",
22
22
  "cli": "ZERO_ENV=development ZERO_API_URL=http://localhost:1111 tsx src/index.ts",
@@ -34,7 +34,7 @@
34
34
  "@x402/extensions": "^2.9.0",
35
35
  "@x402/fetch": "^2.9.0",
36
36
  "commander": "^13.0.0",
37
- "mppx": "^0.5.9",
37
+ "mppx": "^0.6.9",
38
38
  "open": "^11.0.0",
39
39
  "posthog-node": "^5.29.2",
40
40
  "viem": "^2.47.10",
@@ -98,13 +98,16 @@ zero review --capability <slug> --success --accuracy <1-5> --value <1-5> --relia
98
98
 
99
99
  ### Workflow
100
100
 
101
- 1. **Search** — `zero search "weather forecast"` finds matching capabilities. Results show name, cost, rating, and success rate.
101
+ 1. **Search** — `zero search "weather forecast"` finds matching capabilities. Results show ranked capabilities with name, cost, availability, and a short description.
102
102
  > **Detail-page → CLI bridge.** When you only have a capability slug (e.g. copied from a zero.xyz capability page), `zero get <slug>` and `zero fetch --capability <slug>` both work as drop-in replacements for the position-based forms. You don't need to run `zero search` first.
103
103
 
104
104
  2. **Inspect** — `zero get 1 --formatted` prints a human summary **and a copy-pasteable `Try it:` command** wired to the capability's schema. Plain `zero get 1` returns full JSON (URL, method, `bodySchema`, examples, pricing) for `jq` pipelines. **If `bodySchema` is `null`**, the capability hasn't been schema-indexed yet — skip it and `zero get 2`, don't invent field names.
105
105
  3. **Call** — `zero fetch <url>` makes the request. If the server returns 402, payment is handled automatically (x402 and MPP, including cross-chain bridging from Base to Tempo).
106
106
  4. **Review** — `zero review <runId>` submits a quality review. Run IDs are printed to **stderr** after a successful fetch (or returned on stdout in `--json` mode). Always review after a paid call, and **pass `--content "<notes>"` whenever you have something specific to say** — the content line lands on the capability's public detail page on zero.xyz, so it's what the next human buyer (and the next agent) reads when deciding whether to call this capability. See "Writing review content" below.
107
107
  5. **Retroactive review** — if you lost a runId, run `zero runs --unreviewed` (or `zero runs --capability <slug> --unreviewed`). `zero review --capability <slug> ...` auto-resolves to your most recent un-reviewed run for that capability.
108
+ 6. **Revise a review** — re-running `zero review <runId> ...` for a run you've already reviewed **overwrites** your prior ratings and content (same wallet only). Use this when a retry succeeded, a result that looked good failed downstream, or you want to expand the notes after more usage. There's no separate edit command — same call, same runId.
109
+
110
+ > **Only your latest review per capability is shown publicly.** When you've reviewed a capability across multiple runs, both the public detail page and the search-ranking math dedupe to your most recent review per capability — older reviews stay in the DB as a version trail but don't display and don't count toward the rating. So if your judgment has changed, submit a fresh review on a recent run (or edit the existing one via the same-`runId` upsert above) — don't assume the old review still represents you.
108
111
 
109
112
  ### Writing review content
110
113
 
@@ -118,6 +121,10 @@ zero review --capability <slug> --success --accuracy <1-5> --value <1-5> --relia
118
121
 
119
122
  Each names the task attempted, what the output actually was, and a specific observation (latency, a gotcha, a fit/misfit note). That's the kind of line a human buyer trusts and another agent can learn from.
120
123
 
124
+ **Name the use case.** State the general type of work you were using the capability for (no private/proprietary detail) so other readers can tell whether their task fits. Examples: *"Used this to generate stock-style hero photography for a vibe-coded landing page"*, *"Called from a daily ETL to enrich new signups with company metadata"*, *"Translating short product blurbs (~80 chars) for a Spanish-language store"*. This is what makes a review useful for someone deciding whether to call the capability for *their* task — not just whether it worked for yours.
125
+
126
+ **Two-part structure works well** when you have enough to say: lead with the human-facing half (use case, what you got, fit/misfit), then a second half with agent-facing technical notes (exact field names, gotchas, retry behavior, schema quirks). Each half is independently useful — humans skim the top, agents grep the bottom.
127
+
121
128
  **Review failures with content too.** Failure notes are arguably more valuable — they warn the next caller. Example: *"FLUX Schnell returned HTTP 500 Internal Server Error — paid 0.003 USDC via MPP but got no image."* Pair with `--no-success`.
122
129
 
123
130
  **Skip `--content` rather than write filler.** "Worked great", "Fast response", or test strings like "trial 1" add noise, pollute the capability's public page, and dilute the signal agents rely on. If you don't have a specific observation, just submit the numeric ratings.
@@ -231,7 +238,7 @@ cat payload.json | zero fetch https://api.example.com --data-stdin
231
238
 
232
239
  ### Rules
233
240
 
234
- - **Always `zero search` fresh, every time.** Never reuse a capability URL, slug, schema, or price from an earlier turn, prior conversation, training data, or memory. Capabilities churn constantly — endpoints go offline, prices change, schemas evolve, and rankings shift as reviews accumulate. A capability that worked yesterday may be dead, repriced, or outranked today. Searching again costs nothing and is the only way to get current trust scores and availability.
241
+ - **Always `zero search` fresh, every time.** Never reuse a capability URL, slug, schema, or price from an earlier turn, prior conversation, training data, or memory. Capabilities churn constantly — endpoints go offline, prices change, schemas evolve, and rankings shift as reviews accumulate. A capability that worked yesterday may be dead, repriced, or outranked today. Searching again costs nothing and is the only way to get current ranking and availability.
235
242
  - **Always `zero get` before `zero fetch`.** Even if you "know" the URL, re-fetch the full details to confirm the URL, method, required headers, body schema, and current price. Do not reconstruct a fetch call from memory.
236
243
  - Never guess endpoint URLs or schemas.
237
244
  - Use `--max-pay` before potentially expensive requests.