@zeroxyz/cli 0.0.28 → 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.28",
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,8 +1839,9 @@ 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,
1844
+ statSync,
1619
1845
  writeFileSync as writeFileSync2
1620
1846
  } from "fs";
1621
1847
  import { homedir as homedir2 } from "os";
@@ -1647,7 +1873,7 @@ var getPackageRoot = () => {
1647
1873
  }
1648
1874
  return dir;
1649
1875
  };
1650
- var sha256File = (filePath) => createHash3("sha256").update(readFileSync3(filePath)).digest("hex");
1876
+ var sha256File = (filePath) => createHash3("sha256").update(readFileSync4(filePath)).digest("hex");
1651
1877
  var verifyFileCopy = (src, dest) => {
1652
1878
  if (!existsSync2(dest)) return false;
1653
1879
  return sha256File(src) === sha256File(dest);
@@ -1664,6 +1890,24 @@ var collectAllFiles = (dir) => {
1664
1890
  }
1665
1891
  return files;
1666
1892
  };
1893
+ var copyDirRecursive = (src, dest) => {
1894
+ mkdirSync2(dest, { recursive: true });
1895
+ for (const entry of readdirSync(src, { withFileTypes: true })) {
1896
+ const srcPath = join2(src, entry.name);
1897
+ const destPath = join2(dest, entry.name);
1898
+ if (entry.isDirectory()) {
1899
+ copyDirRecursive(srcPath, destPath);
1900
+ } else {
1901
+ const data = readFileSync4(srcPath);
1902
+ writeFileSync2(destPath, data);
1903
+ try {
1904
+ const mode = statSync(srcPath).mode;
1905
+ chmodSync(destPath, mode);
1906
+ } catch {
1907
+ }
1908
+ }
1909
+ }
1910
+ };
1667
1911
  var installHook = (home) => {
1668
1912
  const claudeDir = join2(home, ".claude");
1669
1913
  if (!existsSync2(claudeDir)) {
@@ -1689,7 +1933,7 @@ var installHook = (home) => {
1689
1933
  let settings = {};
1690
1934
  if (existsSync2(settingsPath)) {
1691
1935
  try {
1692
- settings = JSON.parse(readFileSync3(settingsPath, "utf-8"));
1936
+ settings = JSON.parse(readFileSync4(settingsPath, "utf-8"));
1693
1937
  } catch {
1694
1938
  }
1695
1939
  }
@@ -1800,30 +2044,38 @@ var installSkills = (home) => {
1800
2044
  const skillsSourceDir = join2(getPackageRoot(), "skills");
1801
2045
  const skillDirs = readdirSync(skillsSourceDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
1802
2046
  const installed = [];
2047
+ const errors = [];
1803
2048
  for (const tool of AGENT_TOOLS) {
1804
2049
  const toolDetectPath = join2(home, tool.detectDir);
1805
2050
  if (!existsSync2(toolDetectPath)) {
1806
2051
  continue;
1807
2052
  }
1808
- const toolSkillsPath = join2(home, tool.skillsDir);
1809
- mkdirSync2(toolSkillsPath, { recursive: true });
1810
- for (const skillDir of skillDirs) {
1811
- const src = join2(skillsSourceDir, skillDir);
1812
- const dest = join2(toolSkillsPath, skillDir);
1813
- cpSync(src, dest, { recursive: true });
1814
- for (const srcFile of collectAllFiles(src)) {
1815
- const relPath = relative(src, srcFile);
1816
- const destFile = join2(dest, relPath);
1817
- if (!verifyFileCopy(srcFile, destFile)) {
1818
- throw new Error(
1819
- `Integrity check failed: ${destFile} does not match source`
1820
- );
2053
+ try {
2054
+ const toolSkillsPath = join2(home, tool.skillsDir);
2055
+ mkdirSync2(toolSkillsPath, { recursive: true });
2056
+ for (const skillDir of skillDirs) {
2057
+ const src = join2(skillsSourceDir, skillDir);
2058
+ const dest = join2(toolSkillsPath, skillDir);
2059
+ copyDirRecursive(src, dest);
2060
+ for (const srcFile of collectAllFiles(src)) {
2061
+ const relPath = relative(src, srcFile);
2062
+ const destFile = join2(dest, relPath);
2063
+ if (!verifyFileCopy(srcFile, destFile)) {
2064
+ throw new Error(
2065
+ `Integrity check failed: ${destFile} does not match source`
2066
+ );
2067
+ }
1821
2068
  }
2069
+ installed.push(`${tool.name}: ${dest}`);
1822
2070
  }
1823
- installed.push(`${tool.name}: ${dest}`);
2071
+ } catch (err) {
2072
+ errors.push({
2073
+ tool: tool.name,
2074
+ message: err instanceof Error ? err.message : String(err)
2075
+ });
1824
2076
  }
1825
2077
  }
1826
- return installed;
2078
+ return { installed, errors };
1827
2079
  };
1828
2080
  var runInit = async (appContext, options = {}) => {
1829
2081
  appContext.services.analyticsService.capture("init_started", {
@@ -1839,7 +2091,7 @@ var runInit = async (appContext, options = {}) => {
1839
2091
  const walletExists = (() => {
1840
2092
  if (!existsSync2(configPath)) return false;
1841
2093
  try {
1842
- const existing = JSON.parse(readFileSync3(configPath, "utf8"));
2094
+ const existing = JSON.parse(readFileSync4(configPath, "utf8"));
1843
2095
  return !!existing.privateKey;
1844
2096
  } catch {
1845
2097
  return false;
@@ -1849,7 +2101,7 @@ var runInit = async (appContext, options = {}) => {
1849
2101
  const privateKey = generatePrivateKey();
1850
2102
  const account = privateKeyToAccount(privateKey);
1851
2103
  mkdirSync2(zeroDir, { recursive: true });
1852
- const existing = existsSync2(configPath) ? JSON.parse(readFileSync3(configPath, "utf8")) : {};
2104
+ const existing = existsSync2(configPath) ? JSON.parse(readFileSync4(configPath, "utf8")) : {};
1853
2105
  writeFileSync2(
1854
2106
  configPath,
1855
2107
  JSON.stringify(
@@ -1863,7 +2115,7 @@ var runInit = async (appContext, options = {}) => {
1863
2115
  console.log(`Wallet address: ${account.address}`);
1864
2116
  } else {
1865
2117
  try {
1866
- const existing = JSON.parse(readFileSync3(configPath, "utf8"));
2118
+ const existing = JSON.parse(readFileSync4(configPath, "utf8"));
1867
2119
  const account = privateKeyToAccount(existing.privateKey);
1868
2120
  walletAddress = account.address;
1869
2121
  } catch {
@@ -1881,15 +2133,24 @@ var runInit = async (appContext, options = {}) => {
1881
2133
  }
1882
2134
  currentStep = "skills";
1883
2135
  try {
1884
- const installed = installSkills(home);
2136
+ const { installed, errors } = installSkills(home);
1885
2137
  for (const entry of installed) {
1886
2138
  const toolName = entry.split(":")[0];
1887
2139
  if (toolName && !agentsWithSkills.includes(toolName)) {
1888
2140
  agentsWithSkills.push(toolName);
1889
2141
  }
1890
2142
  }
2143
+ if (errors.length > 0) {
2144
+ skillsError = errors.map((e) => `${e.tool}: ${e.message}`).join("; ");
2145
+ for (const e of errors) {
2146
+ console.error(
2147
+ `Warning: failed to install skills for ${e.tool}: ${e.message}`
2148
+ );
2149
+ }
2150
+ }
1891
2151
  } catch (err) {
1892
2152
  skillsError = err instanceof Error ? err.message : "unknown skills error";
2153
+ console.error(`Warning: skills install failed: ${skillsError}`);
1893
2154
  }
1894
2155
  currentStep = "hook";
1895
2156
  try {
@@ -1971,7 +2232,7 @@ ${removedList}`);
1971
2232
  );
1972
2233
 
1973
2234
  // src/commands/review-command.ts
1974
- import { readFileSync as readFileSync4 } from "fs";
2235
+ import { readFileSync as readFileSync5 } from "fs";
1975
2236
  import { Command as Command6 } from "commander";
1976
2237
  import { z as z3 } from "zod";
1977
2238
  var bulkEntrySchema2 = z3.object({
@@ -2013,35 +2274,66 @@ Examples:
2013
2274
  try {
2014
2275
  const { analyticsService, apiService } = appContext.services;
2015
2276
  if (options.fromFile) {
2016
- const contents = readFileSync4(options.fromFile, "utf8");
2277
+ const contents = readFileSync5(options.fromFile, "utf8");
2017
2278
  const lines = contents.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
2018
- let ok = 0;
2019
- let failed = 0;
2279
+ const parsed = [];
2280
+ let parseFailures = 0;
2020
2281
  for (const [idx, line] of lines.entries()) {
2021
2282
  const lineNum = idx + 1;
2022
2283
  try {
2023
- const parsed = bulkEntrySchema2.parse(JSON.parse(line));
2024
- const result2 = await apiService.createReview(parsed);
2025
- ok += 1;
2026
- console.log(
2027
- `[${lineNum}] ${parsed.runId} -> ${result2.reviewId}`
2028
- );
2029
- analyticsService.capture("review_submitted", {
2030
- runId: parsed.runId,
2031
- success: parsed.success,
2032
- accuracy: parsed.accuracy,
2033
- value: parsed.value,
2034
- reliability: parsed.reliability,
2035
- hasContent: !!parsed.content,
2036
- bulk: true
2284
+ parsed.push({
2285
+ lineNum,
2286
+ entry: bulkEntrySchema2.parse(JSON.parse(line))
2037
2287
  });
2038
2288
  } catch (err) {
2039
- failed += 1;
2289
+ parseFailures += 1;
2040
2290
  console.error(
2041
- `[${lineNum}] FAILED: ${err instanceof Error ? err.message : String(err)}`
2291
+ `[${lineNum}] PARSE FAILED: ${err instanceof Error ? err.message : String(err)}`
2042
2292
  );
2043
2293
  }
2044
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
+ }
2045
2337
  console.log(`
2046
2338
  Bulk review complete: ${ok} ok, ${failed} failed`);
2047
2339
  if (failed > 0) process.exitCode = 1;
@@ -2080,7 +2372,7 @@ Bulk review complete: ${ok} ok, ${failed} failed`);
2080
2372
  }
2081
2373
  if (list.runs.length > 1) {
2082
2374
  console.error(
2083
- `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>".`
2084
2376
  );
2085
2377
  process.exitCode = 1;
2086
2378
  return;
@@ -2294,7 +2586,8 @@ var searchCommand = (appContext) => new Command8("search").description("Search f
2294
2586
  capabilities: result.capabilities.map((c) => ({
2295
2587
  position: c.position,
2296
2588
  id: c.id,
2297
- url: c.url
2589
+ url: c.url,
2590
+ displayCostAmount: c.cost.amount
2298
2591
  }))
2299
2592
  });
2300
2593
  console.log(formatSearchResults(result.capabilities));
@@ -2337,7 +2630,7 @@ Read the full terms at: ${TERMS_URL}
2337
2630
  });
2338
2631
 
2339
2632
  // src/commands/wallet-command.ts
2340
- 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";
2341
2634
  import { homedir as homedir3 } from "os";
2342
2635
  import { join as join3 } from "path";
2343
2636
  import { Command as Command10 } from "commander";
@@ -2432,7 +2725,7 @@ var walletSetCommand = (appContext) => new Command10("set").description("Set wal
2432
2725
  const configPath = join3(zeroDir, "config.json");
2433
2726
  if (!options.force && existsSync3(configPath)) {
2434
2727
  try {
2435
- const existing2 = JSON.parse(readFileSync5(configPath, "utf8"));
2728
+ const existing2 = JSON.parse(readFileSync6(configPath, "utf8"));
2436
2729
  if (existing2.privateKey) {
2437
2730
  console.error(
2438
2731
  "Wallet already configured. Use --force to overwrite."
@@ -2444,7 +2737,7 @@ var walletSetCommand = (appContext) => new Command10("set").description("Set wal
2444
2737
  }
2445
2738
  }
2446
2739
  mkdirSync3(zeroDir, { recursive: true });
2447
- const existing = existsSync3(configPath) ? JSON.parse(readFileSync5(configPath, "utf8")) : {};
2740
+ const existing = existsSync3(configPath) ? JSON.parse(readFileSync6(configPath, "utf8")) : {};
2448
2741
  writeFileSync3(
2449
2742
  configPath,
2450
2743
  JSON.stringify(
@@ -2474,7 +2767,7 @@ var walletCommand = (appContext) => {
2474
2767
  };
2475
2768
 
2476
2769
  // src/commands/welcome-command.ts
2477
- import { existsSync as existsSync4, readFileSync as readFileSync6 } from "fs";
2770
+ import { existsSync as existsSync4, readFileSync as readFileSync7 } from "fs";
2478
2771
  import { homedir as homedir4 } from "os";
2479
2772
  import { join as join4 } from "path";
2480
2773
  import { Command as Command11 } from "commander";
@@ -2485,7 +2778,7 @@ var readPrivateKey = () => {
2485
2778
  const configPath = join4(homedir4(), ".zero", "config.json");
2486
2779
  if (!existsSync4(configPath)) return null;
2487
2780
  try {
2488
- const config = JSON.parse(readFileSync6(configPath, "utf8"));
2781
+ const config = JSON.parse(readFileSync7(configPath, "utf8"));
2489
2782
  if (typeof config.privateKey === "string") {
2490
2783
  return config.privateKey;
2491
2784
  }
@@ -2537,7 +2830,7 @@ If your browser didn't open, paste the URL above.`
2537
2830
  // src/app.ts
2538
2831
  var createApp = (appContext) => {
2539
2832
  const { analyticsService } = appContext.services;
2540
- 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) => {
2541
2834
  const agentFlag = actionCommand.opts().agent;
2542
2835
  if (typeof agentFlag === "string" && agentFlag.trim().length > 0) {
2543
2836
  analyticsService.setAgentHost(agentFlag.trim());
@@ -2584,14 +2877,14 @@ var getEnv = () => {
2584
2877
 
2585
2878
  // src/app/app-services.ts
2586
2879
  import { randomUUID as randomUUID2 } from "crypto";
2587
- import { existsSync as existsSync7, readFileSync as readFileSync9 } from "fs";
2880
+ import { existsSync as existsSync7, readFileSync as readFileSync10 } from "fs";
2588
2881
  import { homedir as homedir5 } from "os";
2589
2882
  import { join as join6 } from "path";
2590
2883
  import { privateKeyToAccount as privateKeyToAccount4 } from "viem/accounts";
2591
2884
 
2592
2885
  // src/services/analytics-service.ts
2593
2886
  import { randomUUID } from "crypto";
2594
- 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";
2595
2888
  import { dirname as dirname2 } from "path";
2596
2889
  import { PostHog } from "posthog-node";
2597
2890
  var POSTHOG_API_KEY = "phc_B2vLyNxAf2mnqvdPQajf4d4b2iXc35dep2ZrvebMJLuX";
@@ -2614,7 +2907,7 @@ var AnalyticsService = class {
2614
2907
  let persistedAnonId;
2615
2908
  try {
2616
2909
  if (existsSync5(opts.configPath)) {
2617
- const config = JSON.parse(readFileSync7(opts.configPath, "utf8"));
2910
+ const config = JSON.parse(readFileSync8(opts.configPath, "utf8"));
2618
2911
  if (config.telemetry === false) {
2619
2912
  telemetryEnabled = false;
2620
2913
  }
@@ -2639,7 +2932,7 @@ var AnalyticsService = class {
2639
2932
  try {
2640
2933
  const dir = dirname2(opts.configPath);
2641
2934
  mkdirSync4(dir, { recursive: true });
2642
- const existing = existsSync5(opts.configPath) ? JSON.parse(readFileSync7(opts.configPath, "utf8")) : {};
2935
+ const existing = existsSync5(opts.configPath) ? JSON.parse(readFileSync8(opts.configPath, "utf8")) : {};
2643
2936
  writeFileSync4(
2644
2937
  opts.configPath,
2645
2938
  JSON.stringify({ ...existing, anonId: newAnonId }, null, 2)
@@ -2675,7 +2968,7 @@ var AnalyticsService = class {
2675
2968
  if (anonId === walletAddress) return;
2676
2969
  let aliasedTo;
2677
2970
  try {
2678
- const config = JSON.parse(readFileSync7(configPath, "utf8"));
2971
+ const config = JSON.parse(readFileSync8(configPath, "utf8"));
2679
2972
  if (typeof config.aliasedTo === "string") {
2680
2973
  aliasedTo = config.aliasedTo;
2681
2974
  }
@@ -2684,7 +2977,7 @@ var AnalyticsService = class {
2684
2977
  if (aliasedTo === walletAddress) return;
2685
2978
  this.posthog.alias({ distinctId: walletAddress, alias: anonId });
2686
2979
  try {
2687
- const config = existsSync5(configPath) ? JSON.parse(readFileSync7(configPath, "utf8")) : {};
2980
+ const config = existsSync5(configPath) ? JSON.parse(readFileSync8(configPath, "utf8")) : {};
2688
2981
  writeFileSync4(
2689
2982
  configPath,
2690
2983
  JSON.stringify({ ...config, aliasedTo: walletAddress }, null, 2)
@@ -2727,7 +3020,7 @@ var AnalyticsService = class {
2727
3020
  };
2728
3021
 
2729
3022
  // src/services/state-service.ts
2730
- 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";
2731
3024
  import { join as join5 } from "path";
2732
3025
  var StateService = class {
2733
3026
  constructor(zeroDir) {
@@ -2742,7 +3035,7 @@ var StateService = class {
2742
3035
  loadLastSearch = () => {
2743
3036
  try {
2744
3037
  if (!existsSync6(this.lastSearchPath)) return null;
2745
- const raw = readFileSync8(this.lastSearchPath, "utf8");
3038
+ const raw = readFileSync9(this.lastSearchPath, "utf8");
2746
3039
  return JSON.parse(raw);
2747
3040
  } catch {
2748
3041
  return null;
@@ -2795,7 +3088,7 @@ var getServices = (env) => {
2795
3088
  if (!privateKey) {
2796
3089
  try {
2797
3090
  if (existsSync7(configPath)) {
2798
- const config = JSON.parse(readFileSync9(configPath, "utf8"));
3091
+ const config = JSON.parse(readFileSync10(configPath, "utf8"));
2799
3092
  if (typeof config.privateKey === "string") {
2800
3093
  privateKey = config.privateKey;
2801
3094
  }
@@ -2807,7 +3100,7 @@ var getServices = (env) => {
2807
3100
  let lowBalanceWarning = 1;
2808
3101
  try {
2809
3102
  if (existsSync7(configPath)) {
2810
- const config = JSON.parse(readFileSync9(configPath, "utf8"));
3103
+ const config = JSON.parse(readFileSync10(configPath, "utf8"));
2811
3104
  if (typeof config.lowBalanceWarning === "number") {
2812
3105
  lowBalanceWarning = config.lowBalanceWarning;
2813
3106
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeroxyz/cli",
3
- "version": "0.0.28",
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