@zeroxyz/cli 0.0.34 → 0.0.37

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
@@ -1,12 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // src/app.ts
4
- import { Command as Command12 } from "commander";
3
+ // src/index.ts
4
+ import { homedir as homedir7 } from "os";
5
+ import { join as join8 } from "path";
5
6
 
6
7
  // package.json
7
8
  var package_default = {
8
9
  name: "@zeroxyz/cli",
9
- version: "0.0.34",
10
+ version: "0.0.37",
10
11
  type: "module",
11
12
  bin: {
12
13
  zero: "dist/index.js",
@@ -22,7 +23,7 @@ var package_default = {
22
23
  },
23
24
  scripts: {
24
25
  build: "tsup src/index.ts --format esm --out-dir dist --clean",
25
- "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",
26
27
  prepublishOnly: "pnpm run build",
27
28
  dev: "ZERO_ENV=development tsx src/index.ts",
28
29
  cli: "ZERO_ENV=development ZERO_API_URL=http://localhost:1111 tsx src/index.ts",
@@ -40,7 +41,7 @@ var package_default = {
40
41
  "@x402/extensions": "^2.9.0",
41
42
  "@x402/fetch": "^2.9.0",
42
43
  commander: "^13.0.0",
43
- mppx: "^0.5.9",
44
+ mppx: "^0.6.9",
44
45
  open: "^11.0.0",
45
46
  "posthog-node": "^5.29.2",
46
47
  viem: "^2.47.10",
@@ -59,6 +60,9 @@ var package_default = {
59
60
  }
60
61
  };
61
62
 
63
+ // src/app.ts
64
+ import { Command as Command12 } from "commander";
65
+
62
66
  // src/commands/bug-report-command.ts
63
67
  import { createHash as createHash2 } from "crypto";
64
68
  import { readFileSync } from "fs";
@@ -68,6 +72,13 @@ import { z as z2 } from "zod";
68
72
  // src/services/api-service.ts
69
73
  import { createHash } from "crypto";
70
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
+ });
71
82
  var searchResultSchema = z.object({
72
83
  id: z.string(),
73
84
  position: z.number(),
@@ -77,18 +88,12 @@ var searchResultSchema = z.object({
77
88
  description: z.string(),
78
89
  whatItDoes: z.string().nullable().optional(),
79
90
  url: z.string(),
91
+ urlTemplate: z.string().nullable().optional(),
80
92
  cost: z.object({ amount: z.string(), asset: z.string() }),
81
- rating: z.object({
82
- score: z.string(),
83
- successRate: z.string(),
84
- reviews: z.number(),
85
- stars: z.string().nullable().optional(),
86
- state: z.enum(["unrated", "rated"]).optional()
87
- }),
88
- trustScore: z.number().nullable().optional(),
89
- trustSignalCount: z.number().optional(),
93
+ reviewCount: z.number().optional(),
94
+ rating: ratingSchema,
90
95
  availabilityStatus: z.enum(["healthy", "degraded", "down", "unknown"]).nullable().optional(),
91
- relevanceScore: z.number().optional()
96
+ displayStatus: z.enum(["healthy", "stable", "degraded", "unhealthy", "unknown"]).optional()
92
97
  });
93
98
  var searchResponseSchema = z.object({
94
99
  searchId: z.string(),
@@ -103,6 +108,7 @@ var capabilityResponseSchema = z.object({
103
108
  name: z.string(),
104
109
  description: z.string(),
105
110
  url: z.string(),
111
+ urlTemplate: z.string().nullable().optional(),
106
112
  method: z.string(),
107
113
  headers: z.record(z.string(), z.string()).nullable(),
108
114
  bodySchema: z.record(z.string(), z.unknown()).nullable(),
@@ -111,13 +117,8 @@ var capabilityResponseSchema = z.object({
111
117
  tags: z.array(z.string()).nullable(),
112
118
  displayCostAmount: z.string(),
113
119
  displayCostAsset: z.string(),
114
- rating: z.object({
115
- score: z.string(),
116
- successRate: z.string(),
117
- reviews: z.number(),
118
- stars: z.string().nullable().optional(),
119
- state: z.enum(["unrated", "rated"]).optional()
120
- }),
120
+ reviewCount: z.number(),
121
+ rating: ratingSchema,
121
122
  priceObserved: z.object({
122
123
  minCents: z.string().nullable(),
123
124
  medianCents: z.string().nullable(),
@@ -140,13 +141,8 @@ var capabilityResponseSchema = z.object({
140
141
  priority: z.number()
141
142
  })
142
143
  ).nullable(),
143
- trustScore: z.number().nullable().optional(),
144
- trustComponents: z.object({
145
- apiQuality: z.number().nullable(),
146
- blockchainActivity: z.number().nullable(),
147
- performance: z.number().nullable()
148
- }).nullable().optional(),
149
144
  availabilityStatus: z.enum(["healthy", "degraded", "down", "unknown"]).nullable().optional(),
145
+ displayStatus: z.enum(["healthy", "stable", "degraded", "unhealthy", "unknown"]).optional(),
150
146
  activationCount: z.number().optional(),
151
147
  lastUsedAt: z.string().nullable().optional(),
152
148
  lastSuccessfullyRanAt: z.string().nullable().optional()
@@ -617,6 +613,7 @@ var configCommand = (_appContext) => new Command2("config").description("View or
617
613
  });
618
614
 
619
615
  // src/commands/fetch-command.ts
616
+ import { createHash as createHash3 } from "crypto";
620
617
  import { readFileSync as readFileSync3 } from "fs";
621
618
  import { resolve as resolvePath } from "path";
622
619
  import { Command as Command3 } from "commander";
@@ -740,6 +737,7 @@ var pickSessionCloseAmount = (receipt, openTimeCumulative) => {
740
737
  const accepted = BigInt(receipt.acceptedCumulative);
741
738
  const spent = BigInt(receipt.spent);
742
739
  const fromReceipt = accepted > spent ? accepted : spent;
740
+ if (receipt.metered === true) return fromReceipt;
743
741
  return fromReceipt > openTimeCumulative ? fromReceipt : openTimeCumulative;
744
742
  };
745
743
  var PaymentService = class {
@@ -1206,6 +1204,22 @@ var truncateQuery = (raw) => raw.length > QUERY_MAX ? `${raw.slice(0, QUERY_MAX)
1206
1204
  var truncateError = (raw) => raw.length > ERROR_MAX ? `${raw.slice(0, ERROR_MAX)}\u2026` : raw;
1207
1205
 
1208
1206
  // src/commands/fetch-command.ts
1207
+ var sniffJsonShape = (buf) => {
1208
+ let i = 0;
1209
+ if (buf.length >= 3 && buf[0] === 239 && buf[1] === 187 && buf[2] === 191) {
1210
+ i = 3;
1211
+ }
1212
+ while (i < buf.length && (buf[i] === 32 || buf[i] === 9 || buf[i] === 10 || buf[i] === 13)) {
1213
+ i++;
1214
+ }
1215
+ if (i >= buf.length) return false;
1216
+ return buf[i] === 123 || buf[i] === 91;
1217
+ };
1218
+ var urlMatchesTemplate = (url, template) => {
1219
+ const escaped = template.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1220
+ const withCaptures = escaped.replace(/\\\{[^/\\}]+\\\}/g, "[^/]+");
1221
+ return new RegExp(`^${withCaptures}$`).test(url);
1222
+ };
1209
1223
  var isTextContentType = (contentType) => {
1210
1224
  if (!contentType) return true;
1211
1225
  const ct = contentType.toLowerCase().split(";")[0]?.trim() ?? "";
@@ -1325,6 +1339,7 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1325
1339
  const startTime = Date.now();
1326
1340
  let resolvedUrl;
1327
1341
  let resolvedMethodFromCapability;
1342
+ let resolvedCapabilityName;
1328
1343
  if (!url) {
1329
1344
  if (!options.capability) {
1330
1345
  console.error(
@@ -1337,6 +1352,7 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1337
1352
  const cap = await apiService.getCapability(options.capability);
1338
1353
  resolvedUrl = cap.url;
1339
1354
  resolvedMethodFromCapability = cap.method;
1355
+ resolvedCapabilityName = cap.name;
1340
1356
  } catch (err) {
1341
1357
  console.error(
1342
1358
  `Failed to resolve --capability ${options.capability}: ${err instanceof Error ? err.message : String(err)}`
@@ -1347,6 +1363,47 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1347
1363
  } else {
1348
1364
  resolvedUrl = url;
1349
1365
  }
1366
+ if (!url && options.capability && !stateService.findSearchContextByCapability(options.capability)) {
1367
+ try {
1368
+ let capName = resolvedCapabilityName;
1369
+ if (capName === void 0) {
1370
+ const cap = await apiService.getCapability(options.capability);
1371
+ capName = cap.name;
1372
+ }
1373
+ const searchResult = await apiService.search({ query: capName });
1374
+ const slugFoundInResults = searchResult.capabilities.some(
1375
+ (c) => c.slug === options.capability || c.id === options.capability
1376
+ );
1377
+ stateService.saveLastSearch({
1378
+ searchId: searchResult.searchId,
1379
+ capabilities: searchResult.capabilities.map((c) => ({
1380
+ position: c.position,
1381
+ id: c.id,
1382
+ url: c.url,
1383
+ urlTemplate: c.urlTemplate ?? null,
1384
+ displayCostAmount: c.cost.amount
1385
+ }))
1386
+ });
1387
+ analyticsService.capture("search_executed", {
1388
+ query: truncateQuery(capName),
1389
+ queryLength: capName.length,
1390
+ resultCount: searchResult.capabilities.length,
1391
+ searchId: searchResult.searchId,
1392
+ total: searchResult.total,
1393
+ hasMore: searchResult.hasMore,
1394
+ json: false,
1395
+ triggeredBy: "slug_handoff",
1396
+ slugFoundInResults
1397
+ });
1398
+ analyticsService.capture("capability_viewed", {
1399
+ capabilityId: options.capability,
1400
+ fromLastSearch: false,
1401
+ searchId: searchResult.searchId,
1402
+ triggeredBy: "slug_handoff"
1403
+ });
1404
+ } catch {
1405
+ }
1406
+ }
1350
1407
  let resolvedBody;
1351
1408
  try {
1352
1409
  resolvedBody = resolveRequestBody(
@@ -1354,9 +1411,13 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1354
1411
  options.dataStdin ?? false
1355
1412
  );
1356
1413
  } catch (err) {
1357
- console.error(
1358
- err instanceof Error ? err.message : "Failed to read request body"
1359
- );
1414
+ const message = err instanceof Error ? err.message : "Failed to read request body";
1415
+ analyticsService.capture("fetch_error", {
1416
+ cliErrorClass: /exceeds the .* byte limit/.test(message) ? "payload_too_large" : "schema_validation_failed",
1417
+ url: redactUrl(resolvedUrl),
1418
+ error: truncateError(message)
1419
+ });
1420
+ console.error(message);
1360
1421
  process.exitCode = 1;
1361
1422
  return;
1362
1423
  }
@@ -1373,7 +1434,7 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1373
1434
  (k) => k.toLowerCase() === "content-type"
1374
1435
  );
1375
1436
  if (resolvedBody && !hasContentType) {
1376
- headers["content-type"] = Buffer.isBuffer(resolvedBody) ? "application/octet-stream" : "application/json";
1437
+ headers["content-type"] = Buffer.isBuffer(resolvedBody) ? sniffJsonShape(resolvedBody) ? "application/json" : "application/octet-stream" : "application/json";
1377
1438
  }
1378
1439
  const log = (msg) => console.error(` ${msg}`);
1379
1440
  const method = options.method ? options.method.toUpperCase() : resolvedMethodFromCapability && !resolvedBody ? resolvedMethodFromCapability.toUpperCase() : resolvedBody ? "POST" : "GET";
@@ -1382,12 +1443,21 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1382
1443
  headers,
1383
1444
  body: resolvedBody
1384
1445
  };
1385
- const lastSearch = stateService.loadLastSearch();
1386
- const matchedCapability = lastSearch?.capabilities.find(
1387
- (c) => resolvedUrl.startsWith(c.url)
1388
- );
1389
- const capabilityId = options.capability ?? matchedCapability?.id ?? null;
1390
- const searchId = matchedCapability ? lastSearch?.searchId : void 0;
1446
+ const explicitCapabilityCtx = options.capability ? stateService.findSearchContextByCapability(options.capability) : null;
1447
+ const urlCtx = stateService.findSearchContextByUrl(resolvedUrl, {
1448
+ matchTemplate: (template) => {
1449
+ try {
1450
+ return urlMatchesTemplate(resolvedUrl, template);
1451
+ } catch {
1452
+ return false;
1453
+ }
1454
+ }
1455
+ });
1456
+ const matchCtx = explicitCapabilityCtx ?? urlCtx;
1457
+ const capabilityId = options.capability ?? matchCtx?.capabilityId ?? null;
1458
+ const searchId = matchCtx?.searchId;
1459
+ const resultRank = matchCtx?.resultRank;
1460
+ const matchedDisplayCostAmount = matchCtx?.displayCostAmount;
1391
1461
  const skipReasons = [];
1392
1462
  if (!apiService.walletAddress) {
1393
1463
  skipReasons.push(
@@ -1398,6 +1468,19 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1398
1468
  skipReasons.push(
1399
1469
  "no capability resolved \u2014 pass --capability <uid|slug> or run `zero search` first so the URL can be matched"
1400
1470
  );
1471
+ if (!options.capability) {
1472
+ const lastSearch = stateService.loadLastSearch();
1473
+ const cached = lastSearch?.capabilities ?? [];
1474
+ analyticsService.capture("capability_resolution_missed", {
1475
+ url: redactUrl(resolvedUrl),
1476
+ method,
1477
+ hasLastSearch: lastSearch !== null,
1478
+ lastSearchSize: cached.length,
1479
+ lastSearchHasTemplates: cached.some(
1480
+ (c) => Boolean(c.urlTemplate)
1481
+ )
1482
+ });
1483
+ }
1401
1484
  }
1402
1485
  let finalResponse;
1403
1486
  let bodyBytes;
@@ -1420,7 +1503,7 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1420
1503
  paymentReq,
1421
1504
  options.maxPay,
1422
1505
  log,
1423
- matchedCapability?.displayCostAmount
1506
+ matchedDisplayCostAmount
1424
1507
  );
1425
1508
  finalResponse = result.response;
1426
1509
  paymentMeta = {
@@ -1517,6 +1600,15 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1517
1600
  } catch {
1518
1601
  }
1519
1602
  }
1603
+ const status = finalResponse?.status;
1604
+ const isFailure = !finalResponse || typeof status === "number" && (status < 200 || status >= 300);
1605
+ let errorSnippetHash;
1606
+ let errorSnippetLength;
1607
+ if (isFailure && body && !bodyIsBinary) {
1608
+ const snippet = body.slice(0, 500);
1609
+ errorSnippetHash = createHash3("sha256").update(snippet).digest("hex");
1610
+ errorSnippetLength = snippet.length;
1611
+ }
1520
1612
  let runId = null;
1521
1613
  if (capabilityId && apiService.walletAddress) {
1522
1614
  let requestSchema;
@@ -1541,6 +1633,8 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1541
1633
  latencyMs,
1542
1634
  requestSchema,
1543
1635
  responseSchema,
1636
+ errorSnippetHash,
1637
+ errorSnippetLength,
1544
1638
  ...paymentMeta && {
1545
1639
  costAmount: paymentMeta.amount,
1546
1640
  costAsset: paymentMeta.asset,
@@ -1557,7 +1651,6 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1557
1651
  );
1558
1652
  }
1559
1653
  }
1560
- const status = finalResponse?.status;
1561
1654
  const outcome = !finalResponse ? "network_error" : status === 402 && !paymentMeta ? "payment_failed" : status !== void 0 && status >= 400 && status !== 402 ? "server_error" : "success";
1562
1655
  analyticsService.capture("fetch_executed", {
1563
1656
  url: redactUrl(resolvedUrl),
@@ -1569,10 +1662,26 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1569
1662
  paymentAmount: paymentMeta?.amount,
1570
1663
  capabilityId: capabilityId ?? void 0,
1571
1664
  searchId: searchId ?? void 0,
1665
+ resultRank: resultRank ?? void 0,
1572
1666
  runId: runId ?? void 0,
1573
1667
  runTracked: !!runId,
1574
1668
  ...fetchError && { error: truncateError(fetchError.message) }
1575
1669
  });
1670
+ const isFetchFailure = Boolean(fetchError) || !finalResponse || typeof status === "number" && (status < 200 || status >= 300);
1671
+ if (isFetchFailure) {
1672
+ const cliErrorClass = fetchError || !finalResponse ? "network" : !apiService.walletAddress ? "auth_missing" : "unknown";
1673
+ analyticsService.capture("fetch_error", {
1674
+ cliErrorClass,
1675
+ capabilityId: capabilityId ?? void 0,
1676
+ searchId: searchId ?? void 0,
1677
+ resultRank: resultRank ?? void 0,
1678
+ url: redactUrl(resolvedUrl),
1679
+ error: truncateError(
1680
+ fetchError?.message ?? skipReasons.join("; ")
1681
+ ),
1682
+ skippedRun: !runId && skipReasons.length > 0
1683
+ });
1684
+ }
1576
1685
  if (fetchError && !options.json) {
1577
1686
  console.error(` Fetch failed: ${fetchError.message}`);
1578
1687
  }
@@ -1639,7 +1748,16 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1639
1748
  process.exitCode = 1;
1640
1749
  }
1641
1750
  } catch (err) {
1642
- console.error(err instanceof Error ? err.message : "Fetch failed");
1751
+ const message = err instanceof Error ? err.message : "Fetch failed";
1752
+ try {
1753
+ appContext.services.analyticsService.capture("fetch_error", {
1754
+ cliErrorClass: "unknown",
1755
+ url: url ? redactUrl(url) : "unknown",
1756
+ error: truncateError(message)
1757
+ });
1758
+ } catch {
1759
+ }
1760
+ console.error(message);
1643
1761
  process.exitCode = 1;
1644
1762
  }
1645
1763
  }
@@ -1647,10 +1765,6 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1647
1765
 
1648
1766
  // src/commands/get-command.ts
1649
1767
  import { Command as Command4 } from "commander";
1650
- var formatReviewCount = (count) => {
1651
- if (count >= 1e3) return `${(count / 1e3).toFixed(1)}k`;
1652
- return count.toString();
1653
- };
1654
1768
  var formatRelativeTimestamp = (iso) => {
1655
1769
  if (!iso) return "never";
1656
1770
  const then = new Date(iso).getTime();
@@ -1667,16 +1781,6 @@ var formatRelativeTimestamp = (iso) => {
1667
1781
  if (diffMo < 12) return `${diffMo}mo ago`;
1668
1782
  return `${Math.round(diffMo / 12)}y ago`;
1669
1783
  };
1670
- var formatTrustScore = (capability) => {
1671
- if (capability.trustScore != null) {
1672
- return `Trust Score: ${capability.trustScore}/100`;
1673
- }
1674
- return "Trust Score: --";
1675
- };
1676
- var formatTrustComponent = (label, value) => {
1677
- const display = value != null ? `${value}/100` : "--";
1678
- return ` ${label.padEnd(22)}${display}`;
1679
- };
1680
1784
  var centsToDollars = (cents) => {
1681
1785
  const value = Number.parseFloat(cents) / 100;
1682
1786
  if (value < 0.01) return value.toFixed(4);
@@ -1701,6 +1805,10 @@ var formatCost = (capability) => {
1701
1805
  }
1702
1806
  return lines;
1703
1807
  };
1808
+ var formatReviewCount = (count) => {
1809
+ if (count >= 1e3) return `${(count / 1e3).toFixed(1)}k`;
1810
+ return count.toString();
1811
+ };
1704
1812
  var formatRating = (rating) => {
1705
1813
  if (rating.state === "unrated") return "unrated";
1706
1814
  const successPct = `${Math.round(Number.parseFloat(rating.successRate) * 100)}%`;
@@ -1787,29 +1895,8 @@ var buildTryItExample = (capability) => {
1787
1895
  var formatCapability = (capability) => {
1788
1896
  const lines = [];
1789
1897
  lines.push(capability.name);
1790
- lines.push(` ${formatTrustScore(capability)}`);
1791
- if (capability.trustComponents) {
1792
- lines.push(
1793
- formatTrustComponent(
1794
- "API Quality:",
1795
- capability.trustComponents.apiQuality
1796
- )
1797
- );
1798
- lines.push(
1799
- formatTrustComponent(
1800
- "Blockchain Activity:",
1801
- capability.trustComponents.blockchainActivity
1802
- )
1803
- );
1804
- lines.push(
1805
- formatTrustComponent(
1806
- "Performance:",
1807
- capability.trustComponents.performance
1808
- )
1809
- );
1810
- }
1811
1898
  lines.push(` Rating: ${formatRating(capability.rating)}`);
1812
- lines.push(` Status: ${capability.availabilityStatus ?? "unknown"}`);
1899
+ lines.push(` Status: ${capability.displayStatus ?? "unknown"}`);
1813
1900
  lines.push(...formatCost(capability));
1814
1901
  lines.push(` URL: ${capability.url}`);
1815
1902
  lines.push(` Method: ${capability.method}`);
@@ -1824,7 +1911,7 @@ var getCommand = (appContext) => new Command4("get").description(
1824
1911
  ).argument(
1825
1912
  "<identifier>",
1826
1913
  "Position number from search results, or a capability slug"
1827
- ).option("--formatted", "Output formatted trust breakdown").option(
1914
+ ).option("--formatted", "Output formatted capability details").option(
1828
1915
  "--agent <name>",
1829
1916
  "Identify your agent host for this invocation. Overrides auto-detect for this call only."
1830
1917
  ).action(async (identifier, options) => {
@@ -1855,6 +1942,8 @@ var getCommand = (appContext) => new Command4("get").description(
1855
1942
  searchId = lastSearch.searchId;
1856
1943
  } else {
1857
1944
  capabilityId = identifier;
1945
+ const ctx = stateService.findSearchContextByCapability(identifier);
1946
+ if (ctx) searchId = ctx.searchId;
1858
1947
  }
1859
1948
  const capability = await apiService.getCapability(
1860
1949
  capabilityId,
@@ -1878,7 +1967,7 @@ var getCommand = (appContext) => new Command4("get").description(
1878
1967
  });
1879
1968
 
1880
1969
  // src/commands/init-command.ts
1881
- import { createHash as createHash3 } from "crypto";
1970
+ import { createHash as createHash4 } from "crypto";
1882
1971
  import {
1883
1972
  chmodSync as chmodSync2,
1884
1973
  existsSync as existsSync2,
@@ -1952,10 +2041,12 @@ var color = {
1952
2041
  magenta: (s) => wrap("35", s),
1953
2042
  green: (s) => wrap("32", s),
1954
2043
  yellow: (s) => wrap("33", s),
2044
+ red: (s) => wrap("31", s),
1955
2045
  gray: (s) => wrap("90", s),
1956
2046
  boldCyan: (s) => wrap("1;36", s),
1957
2047
  boldMagenta: (s) => wrap("1;35", s),
1958
- boldGreen: (s) => wrap("1;32", s)
2048
+ boldGreen: (s) => wrap("1;32", s),
2049
+ boldRed: (s) => wrap("1;31", s)
1959
2050
  };
1960
2051
  var printZeroBanner = () => {
1961
2052
  console.log("");
@@ -1998,12 +2089,16 @@ var printReadyFooter = () => {
1998
2089
  const lines = [
1999
2090
  "",
2000
2091
  ` ${color.boldGreen("Zero is ready!")} Zero works best with an AI agent.`,
2001
- " Open Claude Code, Codex, Cursor, Blackbox, or your agent of choice",
2002
- " and try one of these commands to get started:",
2092
+ ` Open ${color.boldRed("Claude Code")}, Codex, Cursor, Blackbox, or your agent of choice`,
2093
+ " and try this prompt to get started:",
2003
2094
  "",
2004
- ` ${color.cyan('zero search "translate text to Spanish"')}`,
2005
- ` ${color.cyan('zero search "generate an image"')}`,
2006
- ` ${color.cyan('zero search "weather forecast"')}`,
2095
+ ` ${color.cyan("What is zero and how do I use it?")}`,
2096
+ "",
2097
+ " You can also just tell your agent examples of what you are trying to do:",
2098
+ "",
2099
+ ` ${color.cyan("Can you use zero to deploy an NYC weather website")}`,
2100
+ ` ${color.cyan("Email me an image of a crystalline rocket ship with zero")}`,
2101
+ ` ${color.cyan("Create a demo video with real voiceover for my project using zero")}`,
2007
2102
  "",
2008
2103
  ` ${color.dim("By using Zero, you agree to our Terms of Service:")}`,
2009
2104
  ` ${color.dim("https://zero.xyz/terms-of-service")}`,
@@ -2059,7 +2154,7 @@ var getCliModuleDir = () => {
2059
2154
  }
2060
2155
  return __dirname;
2061
2156
  };
2062
- var sha256File = (filePath) => createHash3("sha256").update(readFileSync4(filePath)).digest("hex");
2157
+ var sha256File = (filePath) => createHash4("sha256").update(readFileSync4(filePath)).digest("hex");
2063
2158
  var verifyFileCopy = (src, dest) => {
2064
2159
  if (!existsSync2(dest)) return false;
2065
2160
  return sha256File(src) === sha256File(dest);
@@ -2786,7 +2881,7 @@ var formatReviewCount2 = (count) => {
2786
2881
  return count.toString();
2787
2882
  };
2788
2883
  var formatRatingBadge = (rating) => {
2789
- if (rating.state === "unrated") return "\u2014 unrated";
2884
+ if (rating.state === "unrated") return "unrated";
2790
2885
  const successPct = `${Math.round(Number.parseFloat(rating.successRate) * 100)}%`;
2791
2886
  const reviews = formatReviewCount2(rating.reviews);
2792
2887
  if (rating.stars) {
@@ -2794,13 +2889,7 @@ var formatRatingBadge = (rating) => {
2794
2889
  }
2795
2890
  return `${successPct} success \xB7 ${reviews} reviews`;
2796
2891
  };
2797
- var formatTrustBadge = (item) => {
2798
- if (item.trustScore != null) {
2799
- return `Trust: ${item.trustScore}`;
2800
- }
2801
- return "Trust: --";
2802
- };
2803
- var formatHealthBadge = (status) => {
2892
+ var formatStatusBadge = (status) => {
2804
2893
  if (!status || status === "unknown") return "";
2805
2894
  return ` \u2014 ${status}`;
2806
2895
  };
@@ -2810,21 +2899,19 @@ var formatSearchResults = (results) => {
2810
2899
  const baseName = r.canonicalName ?? r.name;
2811
2900
  const displayName = r.brandName ? `${r.brandName} ${baseName}` : baseName;
2812
2901
  const displayDescription = r.whatItDoes ?? r.description;
2813
- const trustBadge = formatTrustBadge(r);
2814
2902
  const ratingBadge = formatRatingBadge(r.rating);
2815
- const healthBadge = formatHealthBadge(r.availabilityStatus);
2816
- const relevance = r.relevanceScore != null ? ` [${r.relevanceScore.toFixed(3)}]` : "";
2817
- return ` ${r.position}. ${displayName} \u2014 $${r.cost.amount}/call \u2014 ${trustBadge} \u2014 ${ratingBadge}${healthBadge}${relevance}
2903
+ const statusBadge = formatStatusBadge(r.displayStatus);
2904
+ return ` ${r.position}. ${displayName} \u2014 $${r.cost.amount}/call \u2014 ${ratingBadge}${statusBadge}
2818
2905
  "${displayDescription}"`;
2819
2906
  }).join("\n");
2820
2907
  };
2821
2908
  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(
2822
2909
  "--max-cost <amount>",
2823
2910
  `Maximum cost per call in USD (default: ${DEFAULT_MAX_COST_USD})`
2824
- ).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(
2911
+ ).option("--protocol <protocol>", "Payment protocol (x402 or mpp)").option(
2825
2912
  "--status <status>",
2826
2913
  "Filter by availability (healthy, degraded, down)"
2827
- ).option("--all", "Show all results (no trust or health filtering)").option(
2914
+ ).option("--all", "Disable default quality filtering").option(
2828
2915
  "--source <source>",
2829
2916
  "Only show results from this crawl source (e.g. mpp, bazaar)"
2830
2917
  ).option(
@@ -2864,9 +2951,7 @@ var searchCommand = (appContext) => new Command8("search").description("Search f
2864
2951
  limit: options.limit,
2865
2952
  freeOnly: options.free,
2866
2953
  maxCost: effectiveMaxCost,
2867
- minRating: options.minRating,
2868
2954
  protocol: options.protocol,
2869
- minTrust: options.minTrust,
2870
2955
  availabilityStatus: options.status,
2871
2956
  includeAll: options.all,
2872
2957
  source: options.source,
@@ -2884,9 +2969,7 @@ var searchCommand = (appContext) => new Command8("search").description("Search f
2884
2969
  freeOnly: options.free ?? false,
2885
2970
  maxCost: effectiveMaxCost,
2886
2971
  maxCostDefaulted: appliedDefaultMaxCost,
2887
- minRating: options.minRating,
2888
2972
  protocol: options.protocol,
2889
- minTrust: options.minTrust,
2890
2973
  availabilityStatus: options.status,
2891
2974
  includeAll: options.all ?? false,
2892
2975
  source: options.source,
@@ -2913,6 +2996,7 @@ var searchCommand = (appContext) => new Command8("search").description("Search f
2913
2996
  position: c.position,
2914
2997
  id: c.id,
2915
2998
  url: c.url,
2999
+ urlTemplate: c.urlTemplate ?? null,
2916
3000
  displayCostAmount: c.cost.amount
2917
3001
  }))
2918
3002
  });
@@ -3287,7 +3371,11 @@ var AnalyticsService = class {
3287
3371
  this.posthog = new PostHog(POSTHOG_API_KEY, {
3288
3372
  host: POSTHOG_HOST,
3289
3373
  flushAt: 1,
3290
- flushInterval: 0
3374
+ flushInterval: 0,
3375
+ // Vitest spins up many AnalyticsService instances per process; each
3376
+ // autocapture listener adds to process.{uncaughtException,unhandledRejection}
3377
+ // and trips Node's MaxListeners warning. Real CLI is one instance per process.
3378
+ enableExceptionAutocapture: !process.env.VITEST
3291
3379
  });
3292
3380
  this.posthog.on("error", () => {
3293
3381
  });
@@ -3346,6 +3434,22 @@ var AnalyticsService = class {
3346
3434
  }
3347
3435
  });
3348
3436
  }
3437
+ captureException(error, properties) {
3438
+ if (!this.posthog) return;
3439
+ this.posthog.captureException(error, this.distinctId, {
3440
+ source: "cli",
3441
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
3442
+ cli_version: this.cliVersion,
3443
+ environment: this.environment,
3444
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
3445
+ wallet_address: this.walletAddress,
3446
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
3447
+ request_id: this.requestId,
3448
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
3449
+ agent_host: this.agentHost,
3450
+ ...properties
3451
+ });
3452
+ }
3349
3453
  async shutdown() {
3350
3454
  if (!this.posthog) return;
3351
3455
  try {
@@ -3358,15 +3462,27 @@ var AnalyticsService = class {
3358
3462
  // src/services/state-service.ts
3359
3463
  import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "fs";
3360
3464
  import { join as join5 } from "path";
3465
+ var RECENT_SEARCH_LIMIT = 10;
3361
3466
  var StateService = class {
3362
3467
  constructor(zeroDir) {
3363
3468
  this.zeroDir = zeroDir;
3364
3469
  this.lastSearchPath = join5(zeroDir, "last_search.json");
3470
+ this.recentSearchesPath = join5(zeroDir, "recent_searches.json");
3365
3471
  }
3366
3472
  lastSearchPath;
3473
+ recentSearchesPath;
3367
3474
  saveLastSearch = (data) => {
3368
3475
  mkdirSync5(this.zeroDir, { recursive: true });
3369
3476
  writeFileSync5(this.lastSearchPath, JSON.stringify(data, null, 2));
3477
+ const recent = this.loadRecentSearches();
3478
+ const filtered = recent.searches.filter(
3479
+ (s) => s.searchId !== data.searchId
3480
+ );
3481
+ const next = [data, ...filtered].slice(0, RECENT_SEARCH_LIMIT);
3482
+ writeFileSync5(
3483
+ this.recentSearchesPath,
3484
+ JSON.stringify({ searches: next }, null, 2)
3485
+ );
3370
3486
  };
3371
3487
  loadLastSearch = () => {
3372
3488
  try {
@@ -3377,6 +3493,63 @@ var StateService = class {
3377
3493
  return null;
3378
3494
  }
3379
3495
  };
3496
+ loadRecentSearches = () => {
3497
+ try {
3498
+ if (!existsSync6(this.recentSearchesPath)) {
3499
+ const last = this.loadLastSearch();
3500
+ return { searches: last ? [last] : [] };
3501
+ }
3502
+ const raw = readFileSync9(this.recentSearchesPath, "utf8");
3503
+ const parsed = JSON.parse(raw);
3504
+ return { searches: parsed.searches ?? [] };
3505
+ } catch {
3506
+ return { searches: [] };
3507
+ }
3508
+ };
3509
+ // 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) => {
3514
+ const recent = this.loadRecentSearches();
3515
+ for (const search of recent.searches) {
3516
+ const entry = search.capabilities.find((c) => c.id === capabilityId);
3517
+ if (entry) {
3518
+ return {
3519
+ searchId: search.searchId,
3520
+ resultRank: entry.position,
3521
+ capabilityId: entry.id,
3522
+ url: entry.url,
3523
+ displayCostAmount: entry.displayCostAmount
3524
+ };
3525
+ }
3526
+ }
3527
+ return null;
3528
+ };
3529
+ // URL-based attribution for `zero fetch <url>`. Matches by URL prefix
3530
+ // (some capabilities accept query params or path tails the agent appends)
3531
+ // across the recent ring, newest match wins. Falls back to a caller-
3532
+ // supplied urlTemplate matcher (ADS-509 path-parameterized URLs).
3533
+ findSearchContextByUrl = (url, options) => {
3534
+ const recent = this.loadRecentSearches();
3535
+ for (const search of recent.searches) {
3536
+ const prefixHit = search.capabilities.find((c) => url.startsWith(c.url));
3537
+ const templateHit = prefixHit ?? search.capabilities.find(
3538
+ (c) => c.urlTemplate && options?.matchTemplate ? options.matchTemplate(c.urlTemplate) : false
3539
+ );
3540
+ const entry = templateHit;
3541
+ if (entry) {
3542
+ return {
3543
+ searchId: search.searchId,
3544
+ resultRank: entry.position,
3545
+ capabilityId: entry.id,
3546
+ url: entry.url,
3547
+ displayCostAmount: entry.displayCostAmount
3548
+ };
3549
+ }
3550
+ }
3551
+ return null;
3552
+ };
3380
3553
  };
3381
3554
 
3382
3555
  // src/services/wallet-service.ts
@@ -3483,6 +3656,155 @@ var createAppContext = () => {
3483
3656
  };
3484
3657
  };
3485
3658
 
3659
+ // src/util/update-check.ts
3660
+ import {
3661
+ existsSync as existsSync8,
3662
+ lstatSync,
3663
+ mkdirSync as mkdirSync6,
3664
+ readFileSync as readFileSync11,
3665
+ readlinkSync,
3666
+ writeFileSync as writeFileSync6
3667
+ } from "fs";
3668
+ import { homedir as homedir6 } from "os";
3669
+ import { dirname as dirname3, join as join7, resolve } from "path";
3670
+ var CACHE_FILENAME = "update_check.json";
3671
+ var NPM_REGISTRY_URL = "https://registry.npmjs.org/@zeroxyz/cli/latest";
3672
+ var CHECK_INTERVAL_MS = 60 * 60 * 1e3;
3673
+ var FETCH_TIMEOUT_MS = 3e3;
3674
+ var emptyCache = {
3675
+ lastCheckedMs: 0,
3676
+ latestVersion: null,
3677
+ lastShownMs: 0
3678
+ };
3679
+ var resolveExecPath = (execPath) => {
3680
+ try {
3681
+ const stat = lstatSync(execPath);
3682
+ if (stat.isSymbolicLink()) {
3683
+ const target = readlinkSync(execPath);
3684
+ return resolve(dirname3(execPath), target);
3685
+ }
3686
+ } catch {
3687
+ }
3688
+ return execPath;
3689
+ };
3690
+ var detectInstallMethod = (opts = {}) => {
3691
+ const execPath = opts.execPath ?? process.execPath;
3692
+ const pkg = opts.pkg ?? process.pkg;
3693
+ const home = opts.home ?? homedir6();
3694
+ if (pkg) return "binary";
3695
+ const resolved = resolveExecPath(execPath);
3696
+ const zeroBin = join7(home, ".zero", "bin");
3697
+ if (resolved.startsWith(zeroBin)) return "binary";
3698
+ return "npm";
3699
+ };
3700
+ var compareVersions = (a, b) => {
3701
+ const parse = (v) => {
3702
+ const dashIdx = v.indexOf("-");
3703
+ const base2 = dashIdx === -1 ? v : v.slice(0, dashIdx);
3704
+ const pre = dashIdx === -1 ? null : v.slice(dashIdx + 1);
3705
+ const nums = base2.split(".").map((n) => Number.parseInt(n, 10) || 0);
3706
+ while (nums.length < 3) nums.push(0);
3707
+ return { nums, pre };
3708
+ };
3709
+ const pa = parse(a);
3710
+ const pb = parse(b);
3711
+ for (let i = 0; i < 3; i++) {
3712
+ const na = pa.nums[i] ?? 0;
3713
+ const nb = pb.nums[i] ?? 0;
3714
+ if (na !== nb) return na - nb;
3715
+ }
3716
+ if (pa.pre === pb.pre) return 0;
3717
+ if (pa.pre === null) return 1;
3718
+ if (pb.pre === null) return -1;
3719
+ return pa.pre < pb.pre ? -1 : 1;
3720
+ };
3721
+ var cachePath = (zeroDir) => join7(zeroDir, CACHE_FILENAME);
3722
+ var readCache = (zeroDir) => {
3723
+ try {
3724
+ const path = cachePath(zeroDir);
3725
+ if (!existsSync8(path)) return emptyCache;
3726
+ const raw = readFileSync11(path, "utf8");
3727
+ const parsed = JSON.parse(raw);
3728
+ return {
3729
+ lastCheckedMs: typeof parsed.lastCheckedMs === "number" ? parsed.lastCheckedMs : 0,
3730
+ latestVersion: typeof parsed.latestVersion === "string" ? parsed.latestVersion : null,
3731
+ lastShownMs: typeof parsed.lastShownMs === "number" ? parsed.lastShownMs : 0
3732
+ };
3733
+ } catch {
3734
+ return emptyCache;
3735
+ }
3736
+ };
3737
+ var writeCache = (zeroDir, cache) => {
3738
+ try {
3739
+ mkdirSync6(zeroDir, { recursive: true });
3740
+ writeFileSync6(cachePath(zeroDir), JSON.stringify(cache, null, 2));
3741
+ } catch {
3742
+ }
3743
+ };
3744
+ var fetchLatestVersion = async (url = NPM_REGISTRY_URL) => {
3745
+ try {
3746
+ const controller = new AbortController();
3747
+ const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
3748
+ const response = await fetch(url, {
3749
+ signal: controller.signal,
3750
+ headers: { accept: "application/json" }
3751
+ });
3752
+ clearTimeout(timeout);
3753
+ if (!response.ok) return null;
3754
+ const json = await response.json();
3755
+ return typeof json.version === "string" ? json.version : null;
3756
+ } catch {
3757
+ return null;
3758
+ }
3759
+ };
3760
+ var refreshUpdateCache = async (zeroDir, opts = {}) => {
3761
+ const now = opts.now ?? Date.now();
3762
+ const fetcher = opts.fetchLatest ?? fetchLatestVersion;
3763
+ const cache = readCache(zeroDir);
3764
+ if (cache.lastCheckedMs > 0 && now - cache.lastCheckedMs < CHECK_INTERVAL_MS) {
3765
+ return;
3766
+ }
3767
+ const latest = await fetcher();
3768
+ if (!latest) {
3769
+ writeCache(zeroDir, { ...cache, lastCheckedMs: now });
3770
+ return;
3771
+ }
3772
+ writeCache(zeroDir, {
3773
+ ...cache,
3774
+ lastCheckedMs: now,
3775
+ latestVersion: latest
3776
+ });
3777
+ };
3778
+ var updateCommandFor = (method) => method === "binary" ? "curl -fsSL https://zero.xyz/install.sh | bash" : "npm install -g @zeroxyz/cli";
3779
+ var formatBanner = (currentVersion, latestVersion, method) => {
3780
+ const arrow = `${currentVersion} \u2192 ${latestVersion}`;
3781
+ const cmd = updateCommandFor(method);
3782
+ const lines = [
3783
+ "",
3784
+ ` ${color.yellow("\u26A1 Update available")} ${color.dim(arrow)}`,
3785
+ ` ${color.dim("Run:")} ${color.cyan(cmd)}`,
3786
+ ""
3787
+ ];
3788
+ return lines.join("\n");
3789
+ };
3790
+ var consumeBannerIfDue = (zeroDir, currentVersion, opts = {}) => {
3791
+ const now = opts.now ?? Date.now();
3792
+ const method = opts.method ?? detectInstallMethod();
3793
+ const cache = readCache(zeroDir);
3794
+ if (!cache.latestVersion) return null;
3795
+ if (compareVersions(currentVersion, cache.latestVersion) >= 0) return null;
3796
+ if (cache.lastShownMs > 0 && now - cache.lastShownMs < CHECK_INTERVAL_MS) {
3797
+ return null;
3798
+ }
3799
+ writeCache(zeroDir, { ...cache, lastShownMs: now });
3800
+ return formatBanner(currentVersion, cache.latestVersion, method);
3801
+ };
3802
+ var maybePrintUpdateBanner = (zeroDir, currentVersion) => {
3803
+ if (!process.stderr.isTTY) return;
3804
+ const banner = consumeBannerIfDue(zeroDir, currentVersion);
3805
+ if (banner) process.stderr.write(banner);
3806
+ };
3807
+
3486
3808
  // src/index.ts
3487
3809
  var main = async () => {
3488
3810
  const appContext = createAppContext();
@@ -3490,6 +3812,8 @@ var main = async () => {
3490
3812
  console.error("Failed to create app context");
3491
3813
  process.exit(1);
3492
3814
  }
3815
+ const zeroDir = join8(homedir7(), ".zero");
3816
+ maybePrintUpdateBanner(zeroDir, package_default.version);
3493
3817
  const app = createApp(appContext);
3494
3818
  let caughtError = null;
3495
3819
  try {
@@ -3500,6 +3824,9 @@ var main = async () => {
3500
3824
  caughtError = err instanceof Error ? err : new Error(String(err));
3501
3825
  console.error(caughtError.message);
3502
3826
  process.exitCode = 1;
3827
+ appContext.services.analyticsService.captureException(caughtError, {
3828
+ command: appContext.invocation.current?.command
3829
+ });
3503
3830
  }
3504
3831
  } finally {
3505
3832
  const invocation = appContext.invocation.current;
@@ -3513,6 +3840,7 @@ var main = async () => {
3513
3840
  });
3514
3841
  }
3515
3842
  await appContext.services.analyticsService.shutdown();
3843
+ await refreshUpdateCache(zeroDir);
3516
3844
  }
3517
3845
  };
3518
3846
  main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeroxyz/cli",
3
- "version": "0.0.34",
3
+ "version": "0.0.37",
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.