@zeroxyz/cli 0.0.29 → 0.0.30

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
@@ -56,11 +56,11 @@ Get full details for a capability from the last search results.
56
56
  zero get 1
57
57
  ```
58
58
 
59
- Outputs the capability as JSON (URL, method, headers, body schema, cost, payment methods, etc.).
59
+ Outputs the capability as JSON (URL, method, headers, body schema, cost, supported protocols, etc.).
60
60
 
61
61
  ### `zero fetch <url>`
62
62
 
63
- Fetch a capability URL. Automatically detects `402 Payment Required` responses and reports the payment protocol (x402, MPP, etc.).
63
+ Fetch a capability URL. Automatically detects `402 Payment Required` responses and reports the protocol in use (x402, MPP, etc.).
64
64
 
65
65
  ```bash
66
66
  # GET request
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import { Command as Command12 } from "commander";
6
6
  // package.json
7
7
  var package_default = {
8
8
  name: "@zeroxyz/cli",
9
- version: "0.0.29",
9
+ version: "0.0.30",
10
10
  type: "module",
11
11
  bin: {
12
12
  zero: "dist/index.js",
@@ -119,10 +119,14 @@ var capabilityResponseSchema = z.object({
119
119
  state: z.enum(["unrated", "rated"]).optional()
120
120
  }),
121
121
  priceObserved: z.object({
122
- medianCents: z.string(),
123
- p95Cents: z.string(),
122
+ minCents: z.string().nullable(),
123
+ medianCents: z.string().nullable(),
124
+ maxCents: z.string().nullable(),
125
+ // Deprecated alias for maxCents; kept for backward compat.
126
+ p95Cents: z.string().nullable(),
124
127
  sampleCount: z.number(),
125
- varies: z.boolean()
128
+ varies: z.boolean(),
129
+ failureChargeRate: z.number().nullable()
126
130
  }).nullable().optional(),
127
131
  paymentMethods: z.array(
128
132
  z.object({
@@ -149,7 +153,31 @@ var createRunResponseSchema = z.object({
149
153
  });
150
154
  var createReviewResponseSchema = z.object({
151
155
  reviewId: z.string(),
152
- recorded: z.boolean()
156
+ recorded: z.boolean(),
157
+ updated: z.boolean().optional()
158
+ });
159
+ var batchReviewResponseSchema = z.object({
160
+ results: z.array(
161
+ z.union([
162
+ z.object({
163
+ runId: z.string(),
164
+ ok: z.literal(true),
165
+ reviewId: z.string(),
166
+ updated: z.boolean()
167
+ }),
168
+ z.object({
169
+ runId: z.string(),
170
+ ok: z.literal(false),
171
+ status: z.number(),
172
+ error: z.string()
173
+ })
174
+ ])
175
+ ),
176
+ summary: z.object({
177
+ total: z.number(),
178
+ ok: z.number(),
179
+ failed: z.number()
180
+ })
153
181
  });
154
182
  var BUG_REPORT_CATEGORIES = [
155
183
  "search_relevance",
@@ -275,6 +303,10 @@ var ApiService = class {
275
303
  const json = await this.request("POST", "/v1/reviews", data);
276
304
  return createReviewResponseSchema.parse(json);
277
305
  };
306
+ createReviewsBatch = async (reviews) => {
307
+ const json = await this.request("POST", "/v1/reviews/batch", { reviews });
308
+ return batchReviewResponseSchema.parse(json);
309
+ };
278
310
  createBugReport = async (data) => {
279
311
  const json = await this.request("POST", "/v1/bug-reports", data);
280
312
  return createBugReportResponseSchema.parse(json);
@@ -582,6 +614,8 @@ var configCommand = (_appContext) => new Command2("config").description("View or
582
614
  });
583
615
 
584
616
  // src/commands/fetch-command.ts
617
+ import { readFileSync as readFileSync3 } from "fs";
618
+ import { resolve as resolvePath } from "path";
585
619
  import { Command as Command3 } from "commander";
586
620
  import { formatUnits as formatUnits2 } from "viem";
587
621
 
@@ -626,7 +660,9 @@ var PATHUSD_TEMPO = "0x20c0000000000000000000000000000000000000";
626
660
  var BASE_CHAIN_ID = 8453;
627
661
  var TEMPO_CHAIN_ID = 4217;
628
662
  var TEMPO_TESTNET_CHAIN_ID = 42431;
629
- var DEFAULT_MAX_DEPOSIT = "100";
663
+ var DEFAULT_PREFUND_NO_COST_INFO = "1";
664
+ var ADAPTIVE_PREFUND_MULTIPLIER = 10;
665
+ var ADAPTIVE_PREFUND_CEILING = "100";
630
666
  var KNOWN_EIP712_DOMAINS = {
631
667
  // USDC on Base
632
668
  [USDC_BASE.toLowerCase()]: { name: "USDC", version: "2" },
@@ -643,6 +679,16 @@ var calculateBuffer = (baseBalance) => {
643
679
  const twoDollars = 2000000n;
644
680
  return twentyFivePercent < twoDollars ? twentyFivePercent : twoDollars;
645
681
  };
682
+ var resolveDefaultPrefund = (displayCostAmount) => {
683
+ const floor = Number.parseFloat(DEFAULT_PREFUND_NO_COST_INFO);
684
+ const ceiling = Number.parseFloat(ADAPTIVE_PREFUND_CEILING);
685
+ if (!displayCostAmount) return DEFAULT_PREFUND_NO_COST_INFO;
686
+ const cost = Number.parseFloat(displayCostAmount);
687
+ if (!Number.isFinite(cost) || cost <= 0) return DEFAULT_PREFUND_NO_COST_INFO;
688
+ const scaled = cost * ADAPTIVE_PREFUND_MULTIPLIER;
689
+ const bounded = scaled < floor ? floor : scaled > ceiling ? ceiling : scaled;
690
+ return bounded.toFixed(6).replace(/\.?0+$/, "");
691
+ };
646
692
  var tempoChain = {
647
693
  id: TEMPO_CHAIN_ID,
648
694
  name: "Tempo",
@@ -723,7 +769,7 @@ var PaymentService = class {
723
769
  const bridgeAmount = requiredAmount + buffer;
724
770
  if (baseBalance < bridgeAmount) {
725
771
  throw new Error(
726
- `Insufficient Base USDC to bridge: have ${formatUnits(baseBalance, 6)}, need ${formatUnits(bridgeAmount, 6)} (${formatUnits(requiredAmount, 6)} + ${formatUnits(buffer, 6)} buffer)`
772
+ `Insufficient Base USDC to bridge: have ${formatUnits(baseBalance, 6)}, need ${formatUnits(bridgeAmount, 6)} (${formatUnits(requiredAmount, 6)} + ${formatUnits(buffer, 6)} buffer). This gateway uses an MPP payment channel, which is pre-funded up front. Either fund your wallet on Base, or pass \`--max-pay <amount>\` to use a smaller channel (e.g. \`--max-pay 0.05\` for a ~5-cent channel sized for a single cheap call).`
727
773
  );
728
774
  }
729
775
  onProgress?.(
@@ -759,7 +805,7 @@ var PaymentService = class {
759
805
  });
760
806
  return bridgeTxHash;
761
807
  };
762
- handlePayment = async (url, request, paymentRequirement, maxPay, onProgress) => {
808
+ handlePayment = async (url, request, paymentRequirement, maxPay, onProgress, displayCostAmount) => {
763
809
  if (!this.account) {
764
810
  throw new Error(
765
811
  "No wallet configured \u2014 run `zero init` or set ZERO_PRIVATE_KEY"
@@ -776,7 +822,8 @@ var PaymentService = class {
776
822
  request,
777
823
  paymentRequirement.raw,
778
824
  maxPay,
779
- onProgress
825
+ onProgress,
826
+ displayCostAmount
780
827
  );
781
828
  }
782
829
  throw new Error("Unrecognized 402 payment protocol");
@@ -840,15 +887,14 @@ var PaymentService = class {
840
887
  * BEFORE mppx signs a credential — so balance/bridge logic runs in-band
841
888
  * without adding a pre-probe round-trip.
842
889
  */
843
- prepareTempoFunds = async (challenge, maxPay, onProgress) => {
890
+ prepareTempoFunds = async (challenge, maxPay, onProgress, displayCostAmount) => {
844
891
  const challengeRequest = challenge.request;
845
892
  let requiredRaw;
846
893
  if (challenge.intent === "session") {
847
894
  const suggestedDeposit = challengeRequest.suggestedDeposit;
895
+ const defaultPrefund = resolveDefaultPrefund(displayCostAmount);
848
896
  requiredRaw = suggestedDeposit ? BigInt(suggestedDeposit) : BigInt(
849
- Math.floor(
850
- Number.parseFloat(maxPay ?? DEFAULT_MAX_DEPOSIT) * 1e6
851
- )
897
+ Math.floor(Number.parseFloat(maxPay ?? defaultPrefund) * 1e6)
852
898
  );
853
899
  } else {
854
900
  requiredRaw = BigInt(challengeRequest.amount);
@@ -889,7 +935,7 @@ var PaymentService = class {
889
935
  * the extra state and return `410 "channel not found"` on the close.
890
936
  * This mirrors the working production v0.0.21 single-fetch flow.
891
937
  */
892
- payMpp = async (url, request, _raw, maxPay, onProgress) => {
938
+ payMpp = async (url, request, _raw, maxPay, onProgress, displayCostAmount) => {
893
939
  const account = this.account;
894
940
  if (!account) throw new Error("No wallet configured");
895
941
  let capturedAmount = "0";
@@ -901,7 +947,7 @@ var PaymentService = class {
901
947
  methods: [
902
948
  tempo({
903
949
  account,
904
- maxDeposit: maxPay ?? DEFAULT_MAX_DEPOSIT,
950
+ maxDeposit: maxPay ?? resolveDefaultPrefund(displayCostAmount),
905
951
  onChannelUpdate: (entry) => {
906
952
  channelEntry = {
907
953
  channelId: entry.channelId,
@@ -917,7 +963,8 @@ var PaymentService = class {
917
963
  capturedAmount = await this.prepareTempoFunds(
918
964
  challenge,
919
965
  maxPay,
920
- onProgress
966
+ onProgress,
967
+ displayCostAmount
921
968
  );
922
969
  return void 0;
923
970
  }
@@ -1167,6 +1214,43 @@ var isTextContentType = (contentType) => {
1167
1214
  if (ct === "application/x-www-form-urlencoded") return true;
1168
1215
  return false;
1169
1216
  };
1217
+ var isJsonContentType = (contentType) => {
1218
+ if (!contentType) return false;
1219
+ const ct = contentType.toLowerCase().split(";")[0]?.trim() ?? "";
1220
+ return ct === "application/json" || ct.endsWith("+json");
1221
+ };
1222
+ var MAX_REQUEST_BODY_BYTES = 10 * 1024 * 1024;
1223
+ var resolveRequestBody = (rawData, readStdin) => {
1224
+ const fromFile = (spec) => {
1225
+ if (spec === "@-") {
1226
+ return readFileSync3(0, "utf8");
1227
+ }
1228
+ const path = resolvePath(spec.slice(1));
1229
+ return readFileSync3(path, "utf8");
1230
+ };
1231
+ let body;
1232
+ if (readStdin && rawData !== void 0) {
1233
+ throw new Error(
1234
+ "Conflicting body sources: use either --data-stdin or -d, not both."
1235
+ );
1236
+ }
1237
+ if (readStdin) {
1238
+ body = readFileSync3(0, "utf8");
1239
+ } else if (rawData?.startsWith("@")) {
1240
+ body = fromFile(rawData);
1241
+ } else {
1242
+ body = rawData;
1243
+ }
1244
+ if (body !== void 0) {
1245
+ const bytes = Buffer.byteLength(body, "utf8");
1246
+ if (bytes > MAX_REQUEST_BODY_BYTES) {
1247
+ throw new Error(
1248
+ `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.`
1249
+ );
1250
+ }
1251
+ }
1252
+ return body;
1253
+ };
1170
1254
  var looksLikeX402V1Body = (body) => {
1171
1255
  if (!body || typeof body !== "object") return false;
1172
1256
  const b = body;
@@ -1201,15 +1285,26 @@ var detectPaymentRequirement = async (response) => {
1201
1285
  }
1202
1286
  return { protocol: "unknown", raw: {} };
1203
1287
  };
1204
- var fetchCommand = (appContext) => new Command3("fetch").description("Fetch a capability URL with automatic payment handling").argument("<url>", "URL to fetch").option(
1288
+ var fetchCommand = (appContext) => new Command3("fetch").description(
1289
+ "Fetch a capability URL, handling 402 challenges automatically"
1290
+ ).argument("<url>", "URL to fetch").option(
1205
1291
  "-X, --method <method>",
1206
1292
  "HTTP method (GET, POST, PUT, PATCH, DELETE). Defaults to POST when -d is set, otherwise GET"
1207
- ).option("-d, --data <body>", "Request body (JSON string)").option("-H, --header <header...>", "Headers in Key:Value format").option("--max-pay <amount>", "Maximum amount willing to pay (USDC)").option(
1293
+ ).option(
1294
+ "-d, --data <body>",
1295
+ "Request body. Pass a literal JSON string, or `@path/to/file` to read from a file, or `@-` to read from stdin."
1296
+ ).option(
1297
+ "--data-stdin",
1298
+ "Read the request body from stdin (equivalent to `-d @-`)."
1299
+ ).option("-H, --header <header...>", "Headers in Key:Value format").option("--max-pay <amount>", "Maximum per-call spend limit (USDC)").option(
1208
1300
  "--capability <id>",
1209
1301
  "Bind this fetch to a capability (uid or slug) so a reviewable run is recorded even without a prior `zero search`"
1210
1302
  ).option(
1211
1303
  "--json",
1212
- "Emit {runId, status, latencyMs, payment, body} as JSON on stdout (for batch/non-TTY use)"
1304
+ "Emit {runId, ok, status, latencyMs, payment, body, bodyRaw} as JSON on stdout (for batch/non-TTY use)"
1305
+ ).option(
1306
+ "--raw-body",
1307
+ "With --json: keep `body` as the raw response string instead of parsing JSON. `bodyRaw` still reflects the raw text."
1213
1308
  ).option(
1214
1309
  "--agent <name>",
1215
1310
  "Identify your agent host for this invocation (e.g. claude-web, codex). Overrides auto-detect for this call only."
@@ -1224,6 +1319,19 @@ var fetchCommand = (appContext) => new Command3("fetch").description("Fetch a ca
1224
1319
  walletService
1225
1320
  } = appContext.services;
1226
1321
  const startTime = Date.now();
1322
+ let resolvedBody;
1323
+ try {
1324
+ resolvedBody = resolveRequestBody(
1325
+ options.data,
1326
+ options.dataStdin ?? false
1327
+ );
1328
+ } catch (err) {
1329
+ console.error(
1330
+ err instanceof Error ? err.message : "Failed to read request body"
1331
+ );
1332
+ process.exitCode = 1;
1333
+ return;
1334
+ }
1227
1335
  const headers = {};
1228
1336
  if (options.header) {
1229
1337
  for (const h of options.header) {
@@ -1236,15 +1344,15 @@ var fetchCommand = (appContext) => new Command3("fetch").description("Fetch a ca
1236
1344
  const hasContentType = Object.keys(headers).some(
1237
1345
  (k) => k.toLowerCase() === "content-type"
1238
1346
  );
1239
- if (options.data && !hasContentType) {
1347
+ if (resolvedBody && !hasContentType) {
1240
1348
  headers["content-type"] = "application/json";
1241
1349
  }
1242
1350
  const log = (msg) => console.error(` ${msg}`);
1243
- const method = options.method ? options.method.toUpperCase() : options.data ? "POST" : "GET";
1351
+ const method = options.method ? options.method.toUpperCase() : resolvedBody ? "POST" : "GET";
1244
1352
  const requestInit = {
1245
1353
  method,
1246
1354
  headers,
1247
- body: options.data
1355
+ body: resolvedBody
1248
1356
  };
1249
1357
  const lastSearch = stateService.loadLastSearch();
1250
1358
  const matchedCapability = lastSearch?.capabilities.find(
@@ -1283,7 +1391,8 @@ var fetchCommand = (appContext) => new Command3("fetch").description("Fetch a ca
1283
1391
  requestInit,
1284
1392
  paymentReq,
1285
1393
  options.maxPay,
1286
- log
1394
+ log,
1395
+ matchedCapability?.displayCostAmount
1287
1396
  );
1288
1397
  finalResponse = result.response;
1289
1398
  paymentMeta = {
@@ -1384,8 +1493,8 @@ var fetchCommand = (appContext) => new Command3("fetch").description("Fetch a ca
1384
1493
  if (capabilityId && apiService.walletAddress) {
1385
1494
  let requestSchema;
1386
1495
  let responseSchema;
1387
- if (options.data) {
1388
- const parsedReq = tryParseJson(options.data);
1496
+ if (resolvedBody) {
1497
+ const parsedReq = tryParseJson(resolvedBody);
1389
1498
  if (parsedReq !== null && typeof parsedReq === "object") {
1390
1499
  requestSchema = inferSchema(parsedReq);
1391
1500
  }
@@ -1440,14 +1549,30 @@ var fetchCommand = (appContext) => new Command3("fetch").description("Fetch a ca
1440
1549
  console.error(` Fetch failed: ${fetchError.message}`);
1441
1550
  }
1442
1551
  if (options.json) {
1443
- const jsonBody = !finalResponse ? null : bodyIsBinary ? (bodyBytes ?? Buffer.alloc(0)).toString("base64") : body;
1552
+ const responseStatus = finalResponse?.status ?? null;
1553
+ const ok = responseStatus !== null && responseStatus >= 200 && responseStatus < 300;
1554
+ const responseCt = finalResponse?.headers.get("content-type") ?? null;
1555
+ const bodyString = bodyIsBinary ? (bodyBytes ?? Buffer.alloc(0)).toString("base64") : body;
1556
+ let parsedBody = null;
1557
+ if (finalResponse && !bodyIsBinary && !options.rawBody) {
1558
+ if (isJsonContentType(responseCt)) {
1559
+ const parsed = tryParseJson(body);
1560
+ parsedBody = parsed === null ? body : parsed;
1561
+ } else {
1562
+ parsedBody = body;
1563
+ }
1564
+ } else if (finalResponse) {
1565
+ parsedBody = bodyString;
1566
+ }
1444
1567
  console.log(
1445
1568
  JSON.stringify({
1446
1569
  runId,
1447
- status: finalResponse?.status ?? null,
1570
+ ok,
1571
+ status: responseStatus,
1448
1572
  latencyMs,
1449
1573
  payment: paymentMeta ?? null,
1450
- body: jsonBody,
1574
+ body: parsedBody,
1575
+ bodyRaw: finalResponse ? bodyString : null,
1451
1576
  ...bodyIsBinary && { bodyEncoding: "base64" },
1452
1577
  ...fetchError && { error: fetchError.message },
1453
1578
  ...skipReasons.length > 0 && {
@@ -1508,6 +1633,30 @@ var formatTrustComponent = (label, value) => {
1508
1633
  const display = value != null ? `${value}/100` : "--";
1509
1634
  return ` ${label.padEnd(22)}${display}`;
1510
1635
  };
1636
+ var centsToDollars = (cents) => {
1637
+ const value = Number.parseFloat(cents) / 100;
1638
+ if (value < 0.01) return value.toFixed(4);
1639
+ if (value < 1) return value.toFixed(3);
1640
+ return value.toFixed(2);
1641
+ };
1642
+ var formatCost = (capability) => {
1643
+ const lines = [];
1644
+ const observed = capability.priceObserved;
1645
+ if (observed && observed.sampleCount > 0 && observed.varies && observed.minCents && observed.maxCents) {
1646
+ const min = centsToDollars(observed.minCents);
1647
+ const max = centsToDollars(observed.maxCents);
1648
+ const median = observed.medianCents ? centsToDollars(observed.medianCents) : null;
1649
+ const detail = median ? `median $${median}, n=${observed.sampleCount}` : `n=${observed.sampleCount}`;
1650
+ lines.push(` Cost: $${min}\u2013$${max}/call (${detail})`);
1651
+ } else {
1652
+ lines.push(` Cost: $${capability.displayCostAmount}/call`);
1653
+ }
1654
+ if (observed?.failureChargeRate != null && observed.failureChargeRate > 0) {
1655
+ const pct = Math.round(observed.failureChargeRate * 100);
1656
+ lines.push(` \u26A0 ~${pct}% of failed runs still charged the wallet`);
1657
+ }
1658
+ return lines;
1659
+ };
1511
1660
  var formatRating = (rating) => {
1512
1661
  if (rating.state === "unrated") return "unrated";
1513
1662
  const successPct = `${Math.round(Number.parseFloat(rating.successRate) * 100)}%`;
@@ -1517,6 +1666,81 @@ var formatRating = (rating) => {
1517
1666
  }
1518
1667
  return `${successPct} success, ${reviews} reviews`;
1519
1668
  };
1669
+ var asNode = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : null;
1670
+ var placeholderFor = (fieldName, propSchema) => {
1671
+ const node = asNode(propSchema);
1672
+ const example = node?.example ?? node?.default;
1673
+ if (typeof example === "string") return example;
1674
+ if (typeof example === "number" || typeof example === "boolean") {
1675
+ return String(example);
1676
+ }
1677
+ return `<${fieldName.toUpperCase()}>`;
1678
+ };
1679
+ var extractInputEnvelope = (bodySchema, method) => {
1680
+ if (!bodySchema) return {};
1681
+ const props = asNode(bodySchema.properties);
1682
+ if (!props) return {};
1683
+ const inputProps = asNode(asNode(props.input)?.properties);
1684
+ if (inputProps) {
1685
+ return {
1686
+ queryParams: asNode(asNode(inputProps.queryParams)?.properties) ?? void 0,
1687
+ body: asNode(asNode(inputProps.body)?.properties) ?? void 0
1688
+ };
1689
+ }
1690
+ const upperMethod = method.toUpperCase();
1691
+ const isGetLike = upperMethod === "GET" || upperMethod === "DELETE";
1692
+ return isGetLike ? { queryParams: props } : { body: props };
1693
+ };
1694
+ var buildTryItExample = (capability) => {
1695
+ const method = capability.method.toUpperCase();
1696
+ const lines = ["", "Try it:"];
1697
+ const { queryParams, body } = extractInputEnvelope(
1698
+ capability.bodySchema,
1699
+ capability.method
1700
+ );
1701
+ const callerHeaders = Object.entries(capability.headers ?? {});
1702
+ const headerFlags = callerHeaders.map(
1703
+ ([k, v]) => `-H "${k}: ${v}" # [caller-provided]`
1704
+ );
1705
+ if (method === "GET" || queryParams && !body) {
1706
+ const qs = queryParams ? Object.entries(queryParams).map(
1707
+ ([k, schema]) => `${encodeURIComponent(k)}=${encodeURIComponent(placeholderFor(k, schema))}`
1708
+ ).join("&") : "";
1709
+ const url = qs ? `${capability.url}?${qs}` : capability.url;
1710
+ const urlLine = ` zero fetch "${url}"`;
1711
+ if (headerFlags.length === 0) {
1712
+ lines.push(urlLine);
1713
+ } else {
1714
+ lines.push(` zero fetch \\`);
1715
+ for (const h of headerFlags) lines.push(` ${h} \\`);
1716
+ lines.push(` "${url}"`);
1717
+ }
1718
+ if (!queryParams && method === "GET") {
1719
+ lines.push(
1720
+ " # bodySchema did not expose queryParams \u2014 call the URL as-is or inspect the raw schema above."
1721
+ );
1722
+ }
1723
+ return lines;
1724
+ }
1725
+ const samplePayload = body ? Object.fromEntries(
1726
+ Object.entries(body).map(([k, schema]) => [
1727
+ k,
1728
+ placeholderFor(k, schema)
1729
+ ])
1730
+ ) : null;
1731
+ const bodyJson = samplePayload ? JSON.stringify(samplePayload) : "<BODY_JSON>";
1732
+ lines.push(` zero fetch \\`);
1733
+ if (method !== "POST") lines.push(` -X ${method} \\`);
1734
+ for (const h of headerFlags) lines.push(` ${h} \\`);
1735
+ lines.push(` -d '${bodyJson}' \\`);
1736
+ lines.push(` ${capability.url}`);
1737
+ if (!body) {
1738
+ lines.push(
1739
+ " # bodySchema did not expose input.body \u2014 replace <BODY_JSON> with the exact shape shown above."
1740
+ );
1741
+ }
1742
+ return lines;
1743
+ };
1520
1744
  var formatCapability = (capability) => {
1521
1745
  const lines = [];
1522
1746
  lines.push(capability.name);
@@ -1543,9 +1767,10 @@ var formatCapability = (capability) => {
1543
1767
  }
1544
1768
  lines.push(` Rating: ${formatRating(capability.rating)}`);
1545
1769
  lines.push(` Status: ${capability.availabilityStatus ?? "unknown"}`);
1546
- lines.push(` Cost: $${capability.displayCostAmount}/call`);
1770
+ lines.push(...formatCost(capability));
1547
1771
  lines.push(` URL: ${capability.url}`);
1548
1772
  lines.push(` Method: ${capability.method}`);
1773
+ lines.push(...buildTryItExample(capability));
1549
1774
  return lines.join("\n");
1550
1775
  };
1551
1776
  var getCommand = (appContext) => new Command4("get").description(
@@ -1614,7 +1839,7 @@ import {
1614
1839
  existsSync as existsSync2,
1615
1840
  mkdirSync as mkdirSync2,
1616
1841
  readdirSync,
1617
- readFileSync as readFileSync3,
1842
+ readFileSync as readFileSync4,
1618
1843
  rmSync,
1619
1844
  statSync,
1620
1845
  writeFileSync as writeFileSync2
@@ -1648,7 +1873,7 @@ var getPackageRoot = () => {
1648
1873
  }
1649
1874
  return dir;
1650
1875
  };
1651
- var sha256File = (filePath) => createHash3("sha256").update(readFileSync3(filePath)).digest("hex");
1876
+ var sha256File = (filePath) => createHash3("sha256").update(readFileSync4(filePath)).digest("hex");
1652
1877
  var verifyFileCopy = (src, dest) => {
1653
1878
  if (!existsSync2(dest)) return false;
1654
1879
  return sha256File(src) === sha256File(dest);
@@ -1673,7 +1898,7 @@ var copyDirRecursive = (src, dest) => {
1673
1898
  if (entry.isDirectory()) {
1674
1899
  copyDirRecursive(srcPath, destPath);
1675
1900
  } else {
1676
- const data = readFileSync3(srcPath);
1901
+ const data = readFileSync4(srcPath);
1677
1902
  writeFileSync2(destPath, data);
1678
1903
  try {
1679
1904
  const mode = statSync(srcPath).mode;
@@ -1708,7 +1933,7 @@ var installHook = (home) => {
1708
1933
  let settings = {};
1709
1934
  if (existsSync2(settingsPath)) {
1710
1935
  try {
1711
- settings = JSON.parse(readFileSync3(settingsPath, "utf-8"));
1936
+ settings = JSON.parse(readFileSync4(settingsPath, "utf-8"));
1712
1937
  } catch {
1713
1938
  }
1714
1939
  }
@@ -1866,7 +2091,7 @@ var runInit = async (appContext, options = {}) => {
1866
2091
  const walletExists = (() => {
1867
2092
  if (!existsSync2(configPath)) return false;
1868
2093
  try {
1869
- const existing = JSON.parse(readFileSync3(configPath, "utf8"));
2094
+ const existing = JSON.parse(readFileSync4(configPath, "utf8"));
1870
2095
  return !!existing.privateKey;
1871
2096
  } catch {
1872
2097
  return false;
@@ -1876,7 +2101,7 @@ var runInit = async (appContext, options = {}) => {
1876
2101
  const privateKey = generatePrivateKey();
1877
2102
  const account = privateKeyToAccount(privateKey);
1878
2103
  mkdirSync2(zeroDir, { recursive: true });
1879
- const existing = existsSync2(configPath) ? JSON.parse(readFileSync3(configPath, "utf8")) : {};
2104
+ const existing = existsSync2(configPath) ? JSON.parse(readFileSync4(configPath, "utf8")) : {};
1880
2105
  writeFileSync2(
1881
2106
  configPath,
1882
2107
  JSON.stringify(
@@ -1890,7 +2115,7 @@ var runInit = async (appContext, options = {}) => {
1890
2115
  console.log(`Wallet address: ${account.address}`);
1891
2116
  } else {
1892
2117
  try {
1893
- const existing = JSON.parse(readFileSync3(configPath, "utf8"));
2118
+ const existing = JSON.parse(readFileSync4(configPath, "utf8"));
1894
2119
  const account = privateKeyToAccount(existing.privateKey);
1895
2120
  walletAddress = account.address;
1896
2121
  } catch {
@@ -2007,7 +2232,7 @@ ${removedList}`);
2007
2232
  );
2008
2233
 
2009
2234
  // src/commands/review-command.ts
2010
- import { readFileSync as readFileSync4 } from "fs";
2235
+ import { readFileSync as readFileSync5 } from "fs";
2011
2236
  import { Command as Command6 } from "commander";
2012
2237
  import { z as z3 } from "zod";
2013
2238
  var bulkEntrySchema2 = z3.object({
@@ -2049,35 +2274,66 @@ Examples:
2049
2274
  try {
2050
2275
  const { analyticsService, apiService } = appContext.services;
2051
2276
  if (options.fromFile) {
2052
- const contents = readFileSync4(options.fromFile, "utf8");
2277
+ const contents = readFileSync5(options.fromFile, "utf8");
2053
2278
  const lines = contents.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
2054
- let ok = 0;
2055
- let failed = 0;
2279
+ const parsed = [];
2280
+ let parseFailures = 0;
2056
2281
  for (const [idx, line] of lines.entries()) {
2057
2282
  const lineNum = idx + 1;
2058
2283
  try {
2059
- const parsed = bulkEntrySchema2.parse(JSON.parse(line));
2060
- const result2 = await apiService.createReview(parsed);
2061
- ok += 1;
2062
- console.log(
2063
- `[${lineNum}] ${parsed.runId} -> ${result2.reviewId}`
2064
- );
2065
- analyticsService.capture("review_submitted", {
2066
- runId: parsed.runId,
2067
- success: parsed.success,
2068
- accuracy: parsed.accuracy,
2069
- value: parsed.value,
2070
- reliability: parsed.reliability,
2071
- hasContent: !!parsed.content,
2072
- bulk: true
2284
+ parsed.push({
2285
+ lineNum,
2286
+ entry: bulkEntrySchema2.parse(JSON.parse(line))
2073
2287
  });
2074
2288
  } catch (err) {
2075
- failed += 1;
2289
+ parseFailures += 1;
2076
2290
  console.error(
2077
- `[${lineNum}] FAILED: ${err instanceof Error ? err.message : String(err)}`
2291
+ `[${lineNum}] PARSE FAILED: ${err instanceof Error ? err.message : String(err)}`
2078
2292
  );
2079
2293
  }
2080
2294
  }
2295
+ if (parsed.length === 0) {
2296
+ console.error("No valid review lines found in file.");
2297
+ process.exitCode = 1;
2298
+ return;
2299
+ }
2300
+ const Chunk = 100;
2301
+ let ok = 0;
2302
+ let failed = parseFailures;
2303
+ for (let i = 0; i < parsed.length; i += Chunk) {
2304
+ const chunk = parsed.slice(i, i + Chunk);
2305
+ const response = await apiService.createReviewsBatch(
2306
+ chunk.map((p) => p.entry)
2307
+ );
2308
+ for (const [chunkIdx, item] of response.results.entries()) {
2309
+ const lineNum = chunk[chunkIdx]?.lineNum ?? i + chunkIdx + 1;
2310
+ if (item.ok) {
2311
+ ok += 1;
2312
+ const suffix = item.updated ? " (updated)" : "";
2313
+ console.log(
2314
+ `[${lineNum}] ${item.runId} -> ${item.reviewId}${suffix}`
2315
+ );
2316
+ const entry = chunk[chunkIdx]?.entry;
2317
+ if (entry) {
2318
+ analyticsService.capture("review_submitted", {
2319
+ runId: entry.runId,
2320
+ success: entry.success,
2321
+ accuracy: entry.accuracy,
2322
+ value: entry.value,
2323
+ reliability: entry.reliability,
2324
+ hasContent: !!entry.content,
2325
+ bulk: true,
2326
+ updated: item.updated
2327
+ });
2328
+ }
2329
+ } else {
2330
+ failed += 1;
2331
+ console.error(
2332
+ `[${lineNum}] FAILED (${item.status}): ${item.error}`
2333
+ );
2334
+ }
2335
+ }
2336
+ }
2081
2337
  console.log(`
2082
2338
  Bulk review complete: ${ok} ok, ${failed} failed`);
2083
2339
  if (failed > 0) process.exitCode = 1;
@@ -2116,7 +2372,7 @@ Bulk review complete: ${ok} ok, ${failed} failed`);
2116
2372
  }
2117
2373
  if (list.runs.length > 1) {
2118
2374
  console.error(
2119
- `Multiple un-reviewed runs for "${options.capability}". Run "zero runs --capability ${options.capability} --unreviewed" and pass the runId explicitly.`
2375
+ `Multiple un-reviewed runs for "${options.capability}". Either pass a runId explicitly (see "zero runs --capability ${options.capability} --unreviewed"), or review them in one call via "zero review --from-file <reviews.jsonl>".`
2120
2376
  );
2121
2377
  process.exitCode = 1;
2122
2378
  return;
@@ -2330,7 +2586,8 @@ var searchCommand = (appContext) => new Command8("search").description("Search f
2330
2586
  capabilities: result.capabilities.map((c) => ({
2331
2587
  position: c.position,
2332
2588
  id: c.id,
2333
- url: c.url
2589
+ url: c.url,
2590
+ displayCostAmount: c.cost.amount
2334
2591
  }))
2335
2592
  });
2336
2593
  console.log(formatSearchResults(result.capabilities));
@@ -2373,7 +2630,7 @@ Read the full terms at: ${TERMS_URL}
2373
2630
  });
2374
2631
 
2375
2632
  // src/commands/wallet-command.ts
2376
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
2633
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
2377
2634
  import { homedir as homedir3 } from "os";
2378
2635
  import { join as join3 } from "path";
2379
2636
  import { Command as Command10 } from "commander";
@@ -2468,7 +2725,7 @@ var walletSetCommand = (appContext) => new Command10("set").description("Set wal
2468
2725
  const configPath = join3(zeroDir, "config.json");
2469
2726
  if (!options.force && existsSync3(configPath)) {
2470
2727
  try {
2471
- const existing2 = JSON.parse(readFileSync5(configPath, "utf8"));
2728
+ const existing2 = JSON.parse(readFileSync6(configPath, "utf8"));
2472
2729
  if (existing2.privateKey) {
2473
2730
  console.error(
2474
2731
  "Wallet already configured. Use --force to overwrite."
@@ -2480,7 +2737,7 @@ var walletSetCommand = (appContext) => new Command10("set").description("Set wal
2480
2737
  }
2481
2738
  }
2482
2739
  mkdirSync3(zeroDir, { recursive: true });
2483
- const existing = existsSync3(configPath) ? JSON.parse(readFileSync5(configPath, "utf8")) : {};
2740
+ const existing = existsSync3(configPath) ? JSON.parse(readFileSync6(configPath, "utf8")) : {};
2484
2741
  writeFileSync3(
2485
2742
  configPath,
2486
2743
  JSON.stringify(
@@ -2510,7 +2767,7 @@ var walletCommand = (appContext) => {
2510
2767
  };
2511
2768
 
2512
2769
  // src/commands/welcome-command.ts
2513
- import { existsSync as existsSync4, readFileSync as readFileSync6 } from "fs";
2770
+ import { existsSync as existsSync4, readFileSync as readFileSync7 } from "fs";
2514
2771
  import { homedir as homedir4 } from "os";
2515
2772
  import { join as join4 } from "path";
2516
2773
  import { Command as Command11 } from "commander";
@@ -2521,7 +2778,7 @@ var readPrivateKey = () => {
2521
2778
  const configPath = join4(homedir4(), ".zero", "config.json");
2522
2779
  if (!existsSync4(configPath)) return null;
2523
2780
  try {
2524
- const config = JSON.parse(readFileSync6(configPath, "utf8"));
2781
+ const config = JSON.parse(readFileSync7(configPath, "utf8"));
2525
2782
  if (typeof config.privateKey === "string") {
2526
2783
  return config.privateKey;
2527
2784
  }
@@ -2573,7 +2830,7 @@ If your browser didn't open, paste the URL above.`
2573
2830
  // src/app.ts
2574
2831
  var createApp = (appContext) => {
2575
2832
  const { analyticsService } = appContext.services;
2576
- const program = new Command12().name("zero").description("Zero CLI \u2014 Search engine and payment platform for AI agents").version(package_default.version, "-v, --version").exitOverride().hook("preAction", async (_thisCommand, actionCommand) => {
2833
+ const program = new Command12().name("zero").description("Zero CLI \u2014 Search engine for AI agents").version(package_default.version, "-v, --version").exitOverride().hook("preAction", async (_thisCommand, actionCommand) => {
2577
2834
  const agentFlag = actionCommand.opts().agent;
2578
2835
  if (typeof agentFlag === "string" && agentFlag.trim().length > 0) {
2579
2836
  analyticsService.setAgentHost(agentFlag.trim());
@@ -2620,14 +2877,14 @@ var getEnv = () => {
2620
2877
 
2621
2878
  // src/app/app-services.ts
2622
2879
  import { randomUUID as randomUUID2 } from "crypto";
2623
- import { existsSync as existsSync7, readFileSync as readFileSync9 } from "fs";
2880
+ import { existsSync as existsSync7, readFileSync as readFileSync10 } from "fs";
2624
2881
  import { homedir as homedir5 } from "os";
2625
2882
  import { join as join6 } from "path";
2626
2883
  import { privateKeyToAccount as privateKeyToAccount4 } from "viem/accounts";
2627
2884
 
2628
2885
  // src/services/analytics-service.ts
2629
2886
  import { randomUUID } from "crypto";
2630
- import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
2887
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
2631
2888
  import { dirname as dirname2 } from "path";
2632
2889
  import { PostHog } from "posthog-node";
2633
2890
  var POSTHOG_API_KEY = "phc_B2vLyNxAf2mnqvdPQajf4d4b2iXc35dep2ZrvebMJLuX";
@@ -2650,7 +2907,7 @@ var AnalyticsService = class {
2650
2907
  let persistedAnonId;
2651
2908
  try {
2652
2909
  if (existsSync5(opts.configPath)) {
2653
- const config = JSON.parse(readFileSync7(opts.configPath, "utf8"));
2910
+ const config = JSON.parse(readFileSync8(opts.configPath, "utf8"));
2654
2911
  if (config.telemetry === false) {
2655
2912
  telemetryEnabled = false;
2656
2913
  }
@@ -2675,7 +2932,7 @@ var AnalyticsService = class {
2675
2932
  try {
2676
2933
  const dir = dirname2(opts.configPath);
2677
2934
  mkdirSync4(dir, { recursive: true });
2678
- const existing = existsSync5(opts.configPath) ? JSON.parse(readFileSync7(opts.configPath, "utf8")) : {};
2935
+ const existing = existsSync5(opts.configPath) ? JSON.parse(readFileSync8(opts.configPath, "utf8")) : {};
2679
2936
  writeFileSync4(
2680
2937
  opts.configPath,
2681
2938
  JSON.stringify({ ...existing, anonId: newAnonId }, null, 2)
@@ -2711,7 +2968,7 @@ var AnalyticsService = class {
2711
2968
  if (anonId === walletAddress) return;
2712
2969
  let aliasedTo;
2713
2970
  try {
2714
- const config = JSON.parse(readFileSync7(configPath, "utf8"));
2971
+ const config = JSON.parse(readFileSync8(configPath, "utf8"));
2715
2972
  if (typeof config.aliasedTo === "string") {
2716
2973
  aliasedTo = config.aliasedTo;
2717
2974
  }
@@ -2720,7 +2977,7 @@ var AnalyticsService = class {
2720
2977
  if (aliasedTo === walletAddress) return;
2721
2978
  this.posthog.alias({ distinctId: walletAddress, alias: anonId });
2722
2979
  try {
2723
- const config = existsSync5(configPath) ? JSON.parse(readFileSync7(configPath, "utf8")) : {};
2980
+ const config = existsSync5(configPath) ? JSON.parse(readFileSync8(configPath, "utf8")) : {};
2724
2981
  writeFileSync4(
2725
2982
  configPath,
2726
2983
  JSON.stringify({ ...config, aliasedTo: walletAddress }, null, 2)
@@ -2763,7 +3020,7 @@ var AnalyticsService = class {
2763
3020
  };
2764
3021
 
2765
3022
  // src/services/state-service.ts
2766
- import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
3023
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "fs";
2767
3024
  import { join as join5 } from "path";
2768
3025
  var StateService = class {
2769
3026
  constructor(zeroDir) {
@@ -2778,7 +3035,7 @@ var StateService = class {
2778
3035
  loadLastSearch = () => {
2779
3036
  try {
2780
3037
  if (!existsSync6(this.lastSearchPath)) return null;
2781
- const raw = readFileSync8(this.lastSearchPath, "utf8");
3038
+ const raw = readFileSync9(this.lastSearchPath, "utf8");
2782
3039
  return JSON.parse(raw);
2783
3040
  } catch {
2784
3041
  return null;
@@ -2831,7 +3088,7 @@ var getServices = (env) => {
2831
3088
  if (!privateKey) {
2832
3089
  try {
2833
3090
  if (existsSync7(configPath)) {
2834
- const config = JSON.parse(readFileSync9(configPath, "utf8"));
3091
+ const config = JSON.parse(readFileSync10(configPath, "utf8"));
2835
3092
  if (typeof config.privateKey === "string") {
2836
3093
  privateKey = config.privateKey;
2837
3094
  }
@@ -2843,7 +3100,7 @@ var getServices = (env) => {
2843
3100
  let lowBalanceWarning = 1;
2844
3101
  try {
2845
3102
  if (existsSync7(configPath)) {
2846
- const config = JSON.parse(readFileSync9(configPath, "utf8"));
3103
+ const config = JSON.parse(readFileSync10(configPath, "utf8"));
2847
3104
  if (typeof config.lowBalanceWarning === "number") {
2848
3105
  lowBalanceWarning = config.lowBalanceWarning;
2849
3106
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeroxyz/cli",
3
- "version": "0.0.29",
3
+ "version": "0.0.30",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "zero": "dist/index.js",
@@ -85,8 +85,8 @@ Starter prompts should be user-facing tasks, not command templates:
85
85
 
86
86
  ```bash
87
87
  zero search "<query>"
88
- zero get <position>
89
- zero fetch <url> [-d '<json>'] [-H "Key:Value"] [--max-pay <amount>]
88
+ zero get <position> [--formatted]
89
+ zero fetch <url> [-X <method>] [-d '<json>' | -d @file | --data-stdin] [-H "Key:Value"] [--max-pay <amount>] [--json [--raw-body]] [--capability <id>]
90
90
  zero runs [--capability <slug>] [--unreviewed]
91
91
  zero review <runId> --accuracy <1-5> --value <1-5> --reliability <1-5>
92
92
  zero review --capability <slug> --success --accuracy <1-5> --value <1-5> --reliability <1-5>
@@ -95,32 +95,118 @@ zero review --capability <slug> --success --accuracy <1-5> --value <1-5> --relia
95
95
  ### Workflow
96
96
 
97
97
  1. **Search** — `zero search "weather forecast"` finds matching capabilities. Results show name, cost, rating, and success rate.
98
- 2. **Inspect** — `zero get 1` returns full details for result #1: URL, method, headers, body schema, examples, and pricing.
99
- 3. **Call** — `zero fetch <url>` makes the request. If the server returns 402, payment is handled automatically (x402 and MPP protocols, including cross-chain bridging from Base to Tempo).
100
- 4. **Review** — `zero review <runId>` submits a quality review. Run IDs are printed after a successful fetch. **Always review after a paid call** reviews bias future search rankings, so this is how you tune future results to services that actually work for you.
101
- 5. **Retroactive review** — if you lost a runId, run `zero runs --unreviewed` (or `zero runs --capability <slug> --unreviewed`) to find it. `zero review --capability <slug> ...` will auto-resolve to your most recent un-reviewed run for that capability.
98
+ 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.
99
+ 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).
100
+ 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.
101
+ 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.
102
102
 
103
- ### Request Templates
103
+ **Fastest path:** `zero search "..." → zero get <n> --formatted → copy the `Try it:` line → edit placeholders → run it`. The `Try it` block already knows whether to use querystring vs `-d`, and labels every header as `[caller-provided]` so you know which `-H` flags to fill in yourself.
104
+
105
+ ### Request Shape Cheatsheet
106
+
107
+ Read `bodySchema` from `zero get <n>` first. The schema describes an `input` envelope with `type: "http"`, a `method`, and either `queryParams` (GET) or `body` (POST). Translate the envelope into a real HTTP call — do **not** send the envelope as the request body.
108
+
109
+ **GET capabilities — put `queryParams` in the URL, not a body:**
104
110
 
105
111
  ```bash
106
- # GET
107
- zero fetch https://api.example.com/weather
112
+ # bodySchema declares: input.method = "GET", input.queryParams = { ip }
113
+ zero fetch "https://api.example.com/ip-geo/locate?ip=8.8.8.8"
114
+ ```
108
115
 
109
- # POST with JSON body
116
+ **POST capabilities send `input.body` as JSON:**
117
+
118
+ ```bash
119
+ # bodySchema declares: input.method = "POST", input.body = { text, to }
110
120
  zero fetch https://api.example.com/translate \
111
121
  -d '{"text":"hello","to":"es"}' \
112
122
  -H "Content-Type:application/json"
123
+ ```
124
+
125
+ **Cap spend:**
113
126
 
114
- # Cap spend at $0.50
127
+ ```bash
115
128
  zero fetch https://api.example.com/expensive --max-pay 0.50
116
129
  ```
117
130
 
131
+ ### Flag Reference (`zero fetch`)
132
+
133
+ | Flag | When to use |
134
+ |---|---|
135
+ | `-X, --method <verb>` | Force HTTP method. Defaults to `POST` when `-d` is set, otherwise `GET`. |
136
+ | `-d, --data <body>` | Request body. Three shapes: a literal JSON string (`-d '{"k":"v"}'`), a file reference (`-d @./payload.json`), or stdin (`-d @-`). Implies POST and auto-sets `Content-Type: application/json` if you didn't pass `-H`. |
137
+ | `--data-stdin` | Alias for `-d @-`. Read the request body from stdin. Mutually exclusive with `-d`. |
138
+ | `-H, --header <k:v>` | Add a header. Repeatable. Use for caller-provided auth/API keys the capability requires. |
139
+ | `--json` | Emit `{runId, ok, status, latencyMs, payment, body, bodyRaw}` as JSON on stdout. `body` is **parsed** when the response is JSON (opt out with `--raw-body`); `bodyRaw` is always the exact text. `ok` is `true` iff `status` is 2xx. |
140
+ | `--raw-body` | With `--json`: keep `body` as the raw response string instead of parsing JSON. |
141
+ | `--max-pay <usdc>` | Refuse to pay more than this per call. |
142
+ | `--capability <uid\|slug>` | Bind this fetch to a capability when you didn't just `zero search` — required to record a reviewable run in batch contexts. |
143
+
144
+ **Body size cap:** `-d` (inline, file, or stdin) rejects bodies over 10 MB with a clear error. For truly large payloads, split, compress, or contact the capability owner.
145
+
146
+ ### Output Handling
147
+
148
+ `zero fetch` separates streams so piping works:
149
+
150
+ - **stdout** — response body only (text) or raw bytes (binary). In `--json` mode, a single JSON envelope instead.
151
+ - **stderr** — progress logs, payment info, the `Run ID: ...` line, review tips, warnings.
152
+
153
+ ```bash
154
+ # Default mode — body is clean on stdout
155
+ zero fetch "https://api.example.com/ip-geo/locate?ip=8.8.8.8" | jq .country
156
+
157
+ # --json mode — body is already parsed (ok flag + structured body)
158
+ zero fetch --json "https://api.example.com/ip-geo/locate?ip=8.8.8.8" \
159
+ | jq 'select(.ok) | {runId, country: .body.country}'
160
+
161
+ # Opt out of parsing if you need the literal bytes
162
+ zero fetch --json --raw-body "<url>" | jq '.bodyRaw'
163
+
164
+ # Suppress progress entirely
165
+ zero fetch "<url>" 2>/dev/null | jq .
166
+
167
+ # Binary (image/audio/pdf): redirect stdout to a file
168
+ zero fetch "<image-url>" > out.png
169
+
170
+ # Large payloads: use a file or stdin to avoid arg-size limits
171
+ zero fetch https://upload.example.com -d @./big-image.b64
172
+ cat payload.json | zero fetch https://api.example.com --data-stdin
173
+ ```
174
+
175
+ **`--json` envelope fields:**
176
+
177
+ | Field | Type | Notes |
178
+ |---|---|---|
179
+ | `runId` | `string\|null` | Zero-side run ID for `zero review`. `null` when the run wasn't recorded (missing wallet or capability). |
180
+ | `ok` | `boolean` | `true` iff `status` is in the 200–299 range. Use this, not `status`, for success checks. |
181
+ | `status` | `number\|null` | Upstream HTTP status code. |
182
+ | `latencyMs` | `number` | End-to-end call latency. |
183
+ | `payment` | `object\|null` | `{protocol, chain, txHash, amount, asset}` when a payment was made; `null` for free calls. |
184
+ | `body` | `any` | Parsed JSON for `application/json` responses, or the string for other text, or base64 string for binary. Pass `--raw-body` to always keep it as the raw string. |
185
+ | `bodyRaw` | `string\|null` | The response as text (or base64 when binary). Always present for forwarding / hashing. |
186
+ | `bodyEncoding` | `"base64"` | Only set when the response was binary. |
187
+ | `error` | `string` | Only set when the fetch or session-close failed. |
188
+
189
+ **When to reach for `--json`:** batch/agent pipelines where you need `runId`, `ok`, or `payment` programmatically. Default text mode is fine for human-directed one-offs.
190
+
191
+ **Reviewing programmatically:** capture `runId` from the envelope, then call `zero review <runId> --success --accuracy N --value N --reliability N` (use `--no-success` if `ok` was `false`).
192
+
118
193
  ### Response Handling
119
194
 
120
195
  - Return the response payload to the user directly.
121
- - If response contains a file URL, download it locally: `curl -fsSL "<url>" -o <filename>`.
196
+ - If response contains a file URL, download it: `curl -fsSL "<url>" -o <filename>`.
122
197
  - After multi-request workflows, check remaining balance with `zero wallet balance`.
123
198
 
199
+ ### Gotchas
200
+
201
+ - **Don't POST a GET envelope.** If `bodySchema` says `method: "GET"` with `queryParams`, encode those as URL query string. POSTing `{"input":{"queryParams":{...}}}` to a GET endpoint will 4xx.
202
+ - **Don't guess field names when `bodySchema` is `null`.** Skip to the next search result. The POST example above (`{text, to}`) is illustrative — real request bodies must match whatever the capability's own `bodySchema` declares (e.g. `{text, target_language}`), not the example's field names.
203
+ - **Large bodies go through `-d @file` or `--data-stdin`.** Inline `-d '<long-string>'` can run past shell arg limits (~1 MB) and fail silently. Anything bigger than a few KB is safer through a file or stdin.
204
+ - **`--json`'s `body` is already parsed for JSON responses.** No more `fromjson`/`JSON.parse` on the body field. If you want the literal bytes (e.g. to hash or forward), use `bodyRaw` or pass `--raw-body`.
205
+ - **Check `ok`, not `status`, for success.** `ok` is a pre-computed 2xx boolean; `status` is the raw HTTP code (useful for distinguishing 404 from 500 but not a success flag).
206
+ - **`--max-pay` is your cost guard.** Always set it before calling an unfamiliar capability or one with per-call pricing you haven't verified.
207
+ - **Capability must be resolvable.** If you skip `zero search` and call `zero fetch <url>` directly, pass `--capability <uid|slug>` so the run is recorded for review.
208
+ - **Review failures too, when they're the capability's fault.** A 4xx/5xx from the upstream API counts as a real result — submit `zero review <runId> --no-success` so future agents see the failure. Do **not** review failures caused by CLI-internal bugs (see Common Issues).
209
+
124
210
  ### Rules
125
211
 
126
212
  - **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.
@@ -218,6 +304,7 @@ zero wallet balance
218
304
  | No search results | Query too narrow | Broaden search terms: `zero search "<broader query>"`. |
219
305
  | Wrong request schema (4xx error) | Incorrect body or headers | Run `zero get <position>` to check the exact schema, method, and required headers. |
220
306
  | Cross-chain bridge delay | Bridging USDC from Base to Tempo | Automatic — the CLI bridges with a 25% buffer. Wait for confirmation and retry if needed. |
307
+ | `No client registered for x402 version: N` | CLI-internal payment bug — not a capability problem | Skip to the next search result. Optionally `zero bug-report "x402 version N unsupported on <slug>"`. Do not `zero review --no-success` (not the capability's fault). |
221
308
 
222
309
  ## Try These
223
310