@zeroxyz/cli 0.0.29 → 0.0.32

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/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.32",
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
 
@@ -595,7 +629,7 @@ import {
595
629
  } from "@relayprotocol/relay-sdk";
596
630
  import { x402Client as X402Client } from "@x402/core/client";
597
631
  import { decodePaymentResponseHeader, x402HTTPClient } from "@x402/core/http";
598
- import { ExactEvmScheme } from "@x402/evm/exact/client";
632
+ import { registerExactEvmScheme } from "@x402/evm/exact/client";
599
633
  import { createSIWxClientHook } from "@x402/extensions/sign-in-with-x";
600
634
  import { wrapFetchWithPayment } from "@x402/fetch";
601
635
  import { Challenge, Receipt } from "mppx";
@@ -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");
@@ -784,10 +831,9 @@ var PaymentService = class {
784
831
  payX402 = async (url, request, _raw, maxPay) => {
785
832
  if (!this.account) throw new Error("No wallet configured");
786
833
  let capturedAmount = "0";
787
- const client = new X402Client().register(
788
- "eip155:*",
789
- new ExactEvmScheme(this.account)
790
- );
834
+ const client = registerExactEvmScheme(new X402Client(), {
835
+ signer: this.account
836
+ });
791
837
  client.onBeforePaymentCreation(async (context) => {
792
838
  const selected = context.selectedRequirements;
793
839
  if (selected && (!selected.extra?.name || !selected.extra?.version)) {
@@ -798,7 +844,9 @@ var PaymentService = class {
798
844
  }
799
845
  const requirement = context.paymentRequired.accepts[0];
800
846
  if (!requirement) return;
801
- capturedAmount = formatUnits(BigInt(requirement.amount), 6);
847
+ const rawAmount = requirement.amount ?? requirement.maxAmountRequired;
848
+ if (!rawAmount) return;
849
+ capturedAmount = formatUnits(BigInt(rawAmount), 6);
802
850
  if (maxPay && Number.parseFloat(capturedAmount) > Number.parseFloat(maxPay)) {
803
851
  return {
804
852
  abort: true,
@@ -840,15 +888,14 @@ var PaymentService = class {
840
888
  * BEFORE mppx signs a credential — so balance/bridge logic runs in-band
841
889
  * without adding a pre-probe round-trip.
842
890
  */
843
- prepareTempoFunds = async (challenge, maxPay, onProgress) => {
891
+ prepareTempoFunds = async (challenge, maxPay, onProgress, displayCostAmount) => {
844
892
  const challengeRequest = challenge.request;
845
893
  let requiredRaw;
846
894
  if (challenge.intent === "session") {
847
895
  const suggestedDeposit = challengeRequest.suggestedDeposit;
896
+ const defaultPrefund = resolveDefaultPrefund(displayCostAmount);
848
897
  requiredRaw = suggestedDeposit ? BigInt(suggestedDeposit) : BigInt(
849
- Math.floor(
850
- Number.parseFloat(maxPay ?? DEFAULT_MAX_DEPOSIT) * 1e6
851
- )
898
+ Math.floor(Number.parseFloat(maxPay ?? defaultPrefund) * 1e6)
852
899
  );
853
900
  } else {
854
901
  requiredRaw = BigInt(challengeRequest.amount);
@@ -889,7 +936,7 @@ var PaymentService = class {
889
936
  * the extra state and return `410 "channel not found"` on the close.
890
937
  * This mirrors the working production v0.0.21 single-fetch flow.
891
938
  */
892
- payMpp = async (url, request, _raw, maxPay, onProgress) => {
939
+ payMpp = async (url, request, _raw, maxPay, onProgress, displayCostAmount) => {
893
940
  const account = this.account;
894
941
  if (!account) throw new Error("No wallet configured");
895
942
  let capturedAmount = "0";
@@ -901,7 +948,7 @@ var PaymentService = class {
901
948
  methods: [
902
949
  tempo({
903
950
  account,
904
- maxDeposit: maxPay ?? DEFAULT_MAX_DEPOSIT,
951
+ maxDeposit: maxPay ?? resolveDefaultPrefund(displayCostAmount),
905
952
  onChannelUpdate: (entry) => {
906
953
  channelEntry = {
907
954
  channelId: entry.channelId,
@@ -917,7 +964,8 @@ var PaymentService = class {
917
964
  capturedAmount = await this.prepareTempoFunds(
918
965
  challenge,
919
966
  maxPay,
920
- onProgress
967
+ onProgress,
968
+ displayCostAmount
921
969
  );
922
970
  return void 0;
923
971
  }
@@ -1167,6 +1215,40 @@ var isTextContentType = (contentType) => {
1167
1215
  if (ct === "application/x-www-form-urlencoded") return true;
1168
1216
  return false;
1169
1217
  };
1218
+ var isJsonContentType = (contentType) => {
1219
+ if (!contentType) return false;
1220
+ const ct = contentType.toLowerCase().split(";")[0]?.trim() ?? "";
1221
+ return ct === "application/json" || ct.endsWith("+json");
1222
+ };
1223
+ var MAX_REQUEST_BODY_BYTES = 10 * 1024 * 1024;
1224
+ var resolveRequestBody = (rawData, readStdin) => {
1225
+ const fromFile = (spec) => {
1226
+ if (spec === "@-") return readFileSync3(0);
1227
+ return readFileSync3(resolvePath(spec.slice(1)));
1228
+ };
1229
+ if (readStdin && rawData !== void 0) {
1230
+ throw new Error(
1231
+ "Conflicting body sources: use either --data-stdin or -d, not both."
1232
+ );
1233
+ }
1234
+ let body;
1235
+ if (readStdin) {
1236
+ body = readFileSync3(0);
1237
+ } else if (rawData?.startsWith("@")) {
1238
+ body = fromFile(rawData);
1239
+ } else {
1240
+ body = rawData;
1241
+ }
1242
+ if (body !== void 0) {
1243
+ const bytes = Buffer.isBuffer(body) ? body.length : Buffer.byteLength(body, "utf8");
1244
+ if (bytes > MAX_REQUEST_BODY_BYTES) {
1245
+ throw new Error(
1246
+ `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.`
1247
+ );
1248
+ }
1249
+ }
1250
+ return body;
1251
+ };
1170
1252
  var looksLikeX402V1Body = (body) => {
1171
1253
  if (!body || typeof body !== "object") return false;
1172
1254
  const b = body;
@@ -1201,15 +1283,26 @@ var detectPaymentRequirement = async (response) => {
1201
1283
  }
1202
1284
  return { protocol: "unknown", raw: {} };
1203
1285
  };
1204
- var fetchCommand = (appContext) => new Command3("fetch").description("Fetch a capability URL with automatic payment handling").argument("<url>", "URL to fetch").option(
1286
+ var fetchCommand = (appContext) => new Command3("fetch").description(
1287
+ "Fetch a capability URL, handling 402 challenges automatically"
1288
+ ).argument("<url>", "URL to fetch").option(
1205
1289
  "-X, --method <method>",
1206
1290
  "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(
1291
+ ).option(
1292
+ "-d, --data <body>",
1293
+ "Request body. Pass a literal JSON string, or `@path/to/file` to read from a file, or `@-` to read from stdin."
1294
+ ).option(
1295
+ "--data-stdin",
1296
+ "Read the request body from stdin (equivalent to `-d @-`)."
1297
+ ).option("-H, --header <header...>", "Headers in Key:Value format").option("--max-pay <amount>", "Maximum per-call spend limit (USDC)").option(
1208
1298
  "--capability <id>",
1209
1299
  "Bind this fetch to a capability (uid or slug) so a reviewable run is recorded even without a prior `zero search`"
1210
1300
  ).option(
1211
1301
  "--json",
1212
- "Emit {runId, status, latencyMs, payment, body} as JSON on stdout (for batch/non-TTY use)"
1302
+ "Emit {runId, ok, status, latencyMs, payment, body, bodyRaw} as JSON on stdout (for batch/non-TTY use)"
1303
+ ).option(
1304
+ "--raw-body",
1305
+ "With --json: keep `body` as the raw response string instead of parsing JSON. `bodyRaw` still reflects the raw text."
1213
1306
  ).option(
1214
1307
  "--agent <name>",
1215
1308
  "Identify your agent host for this invocation (e.g. claude-web, codex). Overrides auto-detect for this call only."
@@ -1224,6 +1317,19 @@ var fetchCommand = (appContext) => new Command3("fetch").description("Fetch a ca
1224
1317
  walletService
1225
1318
  } = appContext.services;
1226
1319
  const startTime = Date.now();
1320
+ let resolvedBody;
1321
+ try {
1322
+ resolvedBody = resolveRequestBody(
1323
+ options.data,
1324
+ options.dataStdin ?? false
1325
+ );
1326
+ } catch (err) {
1327
+ console.error(
1328
+ err instanceof Error ? err.message : "Failed to read request body"
1329
+ );
1330
+ process.exitCode = 1;
1331
+ return;
1332
+ }
1227
1333
  const headers = {};
1228
1334
  if (options.header) {
1229
1335
  for (const h of options.header) {
@@ -1236,15 +1342,15 @@ var fetchCommand = (appContext) => new Command3("fetch").description("Fetch a ca
1236
1342
  const hasContentType = Object.keys(headers).some(
1237
1343
  (k) => k.toLowerCase() === "content-type"
1238
1344
  );
1239
- if (options.data && !hasContentType) {
1240
- headers["content-type"] = "application/json";
1345
+ if (resolvedBody && !hasContentType) {
1346
+ headers["content-type"] = Buffer.isBuffer(resolvedBody) ? "application/octet-stream" : "application/json";
1241
1347
  }
1242
1348
  const log = (msg) => console.error(` ${msg}`);
1243
- const method = options.method ? options.method.toUpperCase() : options.data ? "POST" : "GET";
1349
+ const method = options.method ? options.method.toUpperCase() : resolvedBody ? "POST" : "GET";
1244
1350
  const requestInit = {
1245
1351
  method,
1246
1352
  headers,
1247
- body: options.data
1353
+ body: resolvedBody
1248
1354
  };
1249
1355
  const lastSearch = stateService.loadLastSearch();
1250
1356
  const matchedCapability = lastSearch?.capabilities.find(
@@ -1283,7 +1389,8 @@ var fetchCommand = (appContext) => new Command3("fetch").description("Fetch a ca
1283
1389
  requestInit,
1284
1390
  paymentReq,
1285
1391
  options.maxPay,
1286
- log
1392
+ log,
1393
+ matchedCapability?.displayCostAmount
1287
1394
  );
1288
1395
  finalResponse = result.response;
1289
1396
  paymentMeta = {
@@ -1384,8 +1491,8 @@ var fetchCommand = (appContext) => new Command3("fetch").description("Fetch a ca
1384
1491
  if (capabilityId && apiService.walletAddress) {
1385
1492
  let requestSchema;
1386
1493
  let responseSchema;
1387
- if (options.data) {
1388
- const parsedReq = tryParseJson(options.data);
1494
+ if (typeof resolvedBody === "string") {
1495
+ const parsedReq = tryParseJson(resolvedBody);
1389
1496
  if (parsedReq !== null && typeof parsedReq === "object") {
1390
1497
  requestSchema = inferSchema(parsedReq);
1391
1498
  }
@@ -1440,14 +1547,30 @@ var fetchCommand = (appContext) => new Command3("fetch").description("Fetch a ca
1440
1547
  console.error(` Fetch failed: ${fetchError.message}`);
1441
1548
  }
1442
1549
  if (options.json) {
1443
- const jsonBody = !finalResponse ? null : bodyIsBinary ? (bodyBytes ?? Buffer.alloc(0)).toString("base64") : body;
1550
+ const responseStatus = finalResponse?.status ?? null;
1551
+ const ok = responseStatus !== null && responseStatus >= 200 && responseStatus < 300;
1552
+ const responseCt = finalResponse?.headers.get("content-type") ?? null;
1553
+ const bodyString = bodyIsBinary ? (bodyBytes ?? Buffer.alloc(0)).toString("base64") : body;
1554
+ let parsedBody = null;
1555
+ if (finalResponse && !bodyIsBinary && !options.rawBody) {
1556
+ if (isJsonContentType(responseCt)) {
1557
+ const parsed = tryParseJson(body);
1558
+ parsedBody = parsed === null ? body : parsed;
1559
+ } else {
1560
+ parsedBody = body;
1561
+ }
1562
+ } else if (finalResponse) {
1563
+ parsedBody = bodyString;
1564
+ }
1444
1565
  console.log(
1445
1566
  JSON.stringify({
1446
1567
  runId,
1447
- status: finalResponse?.status ?? null,
1568
+ ok,
1569
+ status: responseStatus,
1448
1570
  latencyMs,
1449
1571
  payment: paymentMeta ?? null,
1450
- body: jsonBody,
1572
+ body: parsedBody,
1573
+ bodyRaw: finalResponse ? bodyString : null,
1451
1574
  ...bodyIsBinary && { bodyEncoding: "base64" },
1452
1575
  ...fetchError && { error: fetchError.message },
1453
1576
  ...skipReasons.length > 0 && {
@@ -1508,6 +1631,30 @@ var formatTrustComponent = (label, value) => {
1508
1631
  const display = value != null ? `${value}/100` : "--";
1509
1632
  return ` ${label.padEnd(22)}${display}`;
1510
1633
  };
1634
+ var centsToDollars = (cents) => {
1635
+ const value = Number.parseFloat(cents) / 100;
1636
+ if (value < 0.01) return value.toFixed(4);
1637
+ if (value < 1) return value.toFixed(3);
1638
+ return value.toFixed(2);
1639
+ };
1640
+ var formatCost = (capability) => {
1641
+ const lines = [];
1642
+ const observed = capability.priceObserved;
1643
+ if (observed && observed.sampleCount > 0 && observed.varies && observed.minCents && observed.maxCents) {
1644
+ const min = centsToDollars(observed.minCents);
1645
+ const max = centsToDollars(observed.maxCents);
1646
+ const median = observed.medianCents ? centsToDollars(observed.medianCents) : null;
1647
+ const detail = median ? `median $${median}, n=${observed.sampleCount}` : `n=${observed.sampleCount}`;
1648
+ lines.push(` Cost: $${min}\u2013$${max}/call (${detail})`);
1649
+ } else {
1650
+ lines.push(` Cost: $${capability.displayCostAmount}/call`);
1651
+ }
1652
+ if (observed?.failureChargeRate != null && observed.failureChargeRate > 0) {
1653
+ const pct = Math.round(observed.failureChargeRate * 100);
1654
+ lines.push(` \u26A0 ~${pct}% of failed runs still charged the wallet`);
1655
+ }
1656
+ return lines;
1657
+ };
1511
1658
  var formatRating = (rating) => {
1512
1659
  if (rating.state === "unrated") return "unrated";
1513
1660
  const successPct = `${Math.round(Number.parseFloat(rating.successRate) * 100)}%`;
@@ -1517,6 +1664,81 @@ var formatRating = (rating) => {
1517
1664
  }
1518
1665
  return `${successPct} success, ${reviews} reviews`;
1519
1666
  };
1667
+ var asNode = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : null;
1668
+ var placeholderFor = (fieldName, propSchema) => {
1669
+ const node = asNode(propSchema);
1670
+ const example = node?.example ?? node?.default;
1671
+ if (typeof example === "string") return example;
1672
+ if (typeof example === "number" || typeof example === "boolean") {
1673
+ return String(example);
1674
+ }
1675
+ return `<${fieldName.toUpperCase()}>`;
1676
+ };
1677
+ var extractInputEnvelope = (bodySchema, method) => {
1678
+ if (!bodySchema) return {};
1679
+ const props = asNode(bodySchema.properties);
1680
+ if (!props) return {};
1681
+ const inputProps = asNode(asNode(props.input)?.properties);
1682
+ if (inputProps) {
1683
+ return {
1684
+ queryParams: asNode(asNode(inputProps.queryParams)?.properties) ?? void 0,
1685
+ body: asNode(asNode(inputProps.body)?.properties) ?? void 0
1686
+ };
1687
+ }
1688
+ const upperMethod = method.toUpperCase();
1689
+ const isGetLike = upperMethod === "GET" || upperMethod === "DELETE";
1690
+ return isGetLike ? { queryParams: props } : { body: props };
1691
+ };
1692
+ var buildTryItExample = (capability) => {
1693
+ const method = capability.method.toUpperCase();
1694
+ const lines = ["", "Try it:"];
1695
+ const { queryParams, body } = extractInputEnvelope(
1696
+ capability.bodySchema,
1697
+ capability.method
1698
+ );
1699
+ const callerHeaders = Object.entries(capability.headers ?? {});
1700
+ const headerFlags = callerHeaders.map(
1701
+ ([k, v]) => `-H "${k}: ${v}" # [caller-provided]`
1702
+ );
1703
+ if (method === "GET" || queryParams && !body) {
1704
+ const qs = queryParams ? Object.entries(queryParams).map(
1705
+ ([k, schema]) => `${encodeURIComponent(k)}=${encodeURIComponent(placeholderFor(k, schema))}`
1706
+ ).join("&") : "";
1707
+ const url = qs ? `${capability.url}?${qs}` : capability.url;
1708
+ const urlLine = ` zero fetch "${url}"`;
1709
+ if (headerFlags.length === 0) {
1710
+ lines.push(urlLine);
1711
+ } else {
1712
+ lines.push(` zero fetch \\`);
1713
+ for (const h of headerFlags) lines.push(` ${h} \\`);
1714
+ lines.push(` "${url}"`);
1715
+ }
1716
+ if (!queryParams && method === "GET") {
1717
+ lines.push(
1718
+ " # bodySchema did not expose queryParams \u2014 call the URL as-is or inspect the raw schema above."
1719
+ );
1720
+ }
1721
+ return lines;
1722
+ }
1723
+ const samplePayload = body ? Object.fromEntries(
1724
+ Object.entries(body).map(([k, schema]) => [
1725
+ k,
1726
+ placeholderFor(k, schema)
1727
+ ])
1728
+ ) : null;
1729
+ const bodyJson = samplePayload ? JSON.stringify(samplePayload) : "<BODY_JSON>";
1730
+ lines.push(` zero fetch \\`);
1731
+ if (method !== "POST") lines.push(` -X ${method} \\`);
1732
+ for (const h of headerFlags) lines.push(` ${h} \\`);
1733
+ lines.push(` -d '${bodyJson}' \\`);
1734
+ lines.push(` ${capability.url}`);
1735
+ if (!body) {
1736
+ lines.push(
1737
+ " # bodySchema did not expose input.body \u2014 replace <BODY_JSON> with the exact shape shown above."
1738
+ );
1739
+ }
1740
+ return lines;
1741
+ };
1520
1742
  var formatCapability = (capability) => {
1521
1743
  const lines = [];
1522
1744
  lines.push(capability.name);
@@ -1543,9 +1765,10 @@ var formatCapability = (capability) => {
1543
1765
  }
1544
1766
  lines.push(` Rating: ${formatRating(capability.rating)}`);
1545
1767
  lines.push(` Status: ${capability.availabilityStatus ?? "unknown"}`);
1546
- lines.push(` Cost: $${capability.displayCostAmount}/call`);
1768
+ lines.push(...formatCost(capability));
1547
1769
  lines.push(` URL: ${capability.url}`);
1548
1770
  lines.push(` Method: ${capability.method}`);
1771
+ lines.push(...buildTryItExample(capability));
1549
1772
  return lines.join("\n");
1550
1773
  };
1551
1774
  var getCommand = (appContext) => new Command4("get").description(
@@ -1609,21 +1832,153 @@ var getCommand = (appContext) => new Command4("get").description(
1609
1832
  // src/commands/init-command.ts
1610
1833
  import { createHash as createHash3 } from "crypto";
1611
1834
  import {
1612
- chmodSync,
1835
+ chmodSync as chmodSync2,
1613
1836
  cpSync,
1614
1837
  existsSync as existsSync2,
1615
- mkdirSync as mkdirSync2,
1838
+ mkdirSync as mkdirSync3,
1616
1839
  readdirSync,
1617
- readFileSync as readFileSync3,
1840
+ readFileSync as readFileSync4,
1618
1841
  rmSync,
1619
1842
  statSync,
1620
- writeFileSync as writeFileSync2
1843
+ writeFileSync as writeFileSync3
1621
1844
  } from "fs";
1622
1845
  import { homedir as homedir2 } from "os";
1623
1846
  import { dirname, join as join2, relative } from "path";
1624
1847
  import { fileURLToPath } from "url";
1625
1848
  import { Command as Command5 } from "commander";
1626
1849
  import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
1850
+
1851
+ // src/util/install-banner.ts
1852
+ var ZEROMAN_ART = `
1853
+ :++-.
1854
+ .:--==+===--:. +@%@#+-
1855
+ .-+*%@@@@%%%%%%@@@@@%#*=.-# #%-=*=
1856
+ :+#@@@#+-:. .-+#@@@@@%#. %# =#:
1857
+ :+%@@#=: :+@%%@@%=+@: .#=
1858
+ -*@@#=. :%%%%@@%%+ #=
1859
+ :#@@#- .:-====--. :%%%%%@@+ %:
1860
+ +@@#- .-+#%@@@@@@@@@@%*- *@%%%%%+ =#
1861
+ .#@@= .. .-*%@@@@%%@%*+#%%%@@@#:=@%%%%@- %.
1862
+ .%@%: :#=:+%@@@%@%%@@+:-+*%%%%%%@%#%%%%%% %-
1863
+ #@%: .%%@@%%%%#%%*--+=%@@%%%%%%@@%%%%@- %-
1864
+ +@%: :#* :%%%%@==+=*= .%%%%%%%%%%%%%@* %:
1865
+ .%%+ :%@- %#.%%@==@# #@%%%%%%%%%%@# -#
1866
+ .::::: -@%: .%%%* .-%%%+ .. =%%%%%%%%%%%@# #+.
1867
+ .+=-::..-* -@% +@%%@*=-+%%%@@#-..-*@%%%%%%%%%@@+ -@%%%#+:
1868
+ %- .:::*=*%% #%%%%@@@@@@@%%%@@@@@%%%%%%%%%@%- :%%%%@@@@#-
1869
+ ++ ..*- %@@- *@%%%%%%#-. .-#@%%%%%%%@%+. :@@@@@@%%%@@-
1870
+ :# .:--*:-%%%%. .%@%%%%%=:----: :#%%%%@@%+. -#-:.:-#@@@%@%
1871
+ .# *#+=@%@%- .#@@@@@%@@@@@@%*-.=%@@@%+. +* +%%%%%%=
1872
+ =* .====-*:#%%@@#=. .=*#%@%%%%%%%@@@@@%*- -#= -*:.. .:*:
1873
+ -*-. :%*##:-#@@@%#*+==+*#%%%@@@@@@%#+- :*+. .-*+:. .:#-
1874
+ .*+=--+#@@@@@%: .-+#%@@@@@@@@@@%%#+=:. -*+. =+: -*
1875
+ ..:+*###*+=**. .:::::::. .-+=. =# - #:
1876
+ :*+. .=*%%: .+==*- #.
1877
+ -#@@#=:. .:=*#%@@%%%= %- .- .+-*=
1878
+ *@@%%@@@#+==-:::...:::--==+=-=#@@%%%%%@* .+=-=++-==-:.
1879
+ =@%%%%%%- .::---====---:. :#@%%%%%@*
1880
+ =@%%%%%%. *@%%%%%@+
1881
+ #%%%%%@%. #@%%%%%@:
1882
+ .-=++*%%%%%%@* :%%%%%%%#---:.
1883
+ .*%@@@@@%%%%%%%@: %%%%%%%@@@@@%*:
1884
+ %@@@@@@@@@@@@@%%. .%@@@@@@@@@@@@@@:
1885
+ -==++++====--:. -==+++++++++++=.
1886
+ `;
1887
+ var ZERO_BANNER = [
1888
+ "\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 ",
1889
+ "\u255A\u2550\u2550\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557 \u2588\u2588\u2554\u2550\u2588\u2588\u2588\u2588\u2557",
1890
+ " \u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2551",
1891
+ " \u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551",
1892
+ "\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D",
1893
+ "\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D "
1894
+ ].join("\n");
1895
+ var colorsEnabled = () => {
1896
+ if (process.env.NO_COLOR) return false;
1897
+ if (process.env.FORCE_COLOR) return true;
1898
+ return Boolean(process.stdout.isTTY);
1899
+ };
1900
+ var wrap = (code, text) => colorsEnabled() ? `\x1B[${code}m${text}\x1B[0m` : text;
1901
+ var color = {
1902
+ bold: (s) => wrap("1", s),
1903
+ dim: (s) => wrap("2", s),
1904
+ cyan: (s) => wrap("36", s),
1905
+ magenta: (s) => wrap("35", s),
1906
+ green: (s) => wrap("32", s),
1907
+ yellow: (s) => wrap("33", s),
1908
+ gray: (s) => wrap("90", s),
1909
+ boldCyan: (s) => wrap("1;36", s),
1910
+ boldMagenta: (s) => wrap("1;35", s),
1911
+ boldGreen: (s) => wrap("1;32", s)
1912
+ };
1913
+ var printZeroBanner = () => {
1914
+ console.log("");
1915
+ console.log(ZEROMAN_ART);
1916
+ console.log(color.boldCyan(ZERO_BANNER));
1917
+ console.log("");
1918
+ console.log(` ${color.dim("The search engine for AI agents.")}`);
1919
+ console.log("");
1920
+ console.log(color.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1921
+ console.log("");
1922
+ };
1923
+ var supportsUnicode = () => {
1924
+ if (process.platform !== "win32") return true;
1925
+ return Boolean(
1926
+ process.env.WT_SESSION || process.env.TERM_PROGRAM === "vscode"
1927
+ );
1928
+ };
1929
+ var SYMBOLS = supportsUnicode() ? { check: "\u2713", arrow: "\u203A", warn: "!" } : { check: "OK", arrow: ">", warn: "!" };
1930
+ var stepSuccess = (label, detail) => {
1931
+ const line = detail ? ` ${color.boldGreen(SYMBOLS.check)} ${label} ${color.dim(detail)}` : ` ${color.boldGreen(SYMBOLS.check)} ${label}`;
1932
+ console.log(line);
1933
+ };
1934
+ var stepWarn = (label, detail) => {
1935
+ const line = detail ? ` ${color.yellow(SYMBOLS.warn)} ${label} ${color.dim(detail)}` : ` ${color.yellow(SYMBOLS.warn)} ${label}`;
1936
+ console.log(line);
1937
+ };
1938
+ var stepSkip = (label, detail) => {
1939
+ const line = detail ? ` ${color.gray(SYMBOLS.arrow)} ${color.dim(`${label} ${detail}`)}` : ` ${color.gray(SYMBOLS.arrow)} ${color.dim(label)}`;
1940
+ console.log(line);
1941
+ };
1942
+ var stepInfo = (message) => {
1943
+ console.log(` ${color.gray("\xB7")} ${color.dim(message)}`);
1944
+ };
1945
+ var sectionDivider = () => {
1946
+ console.log("");
1947
+ console.log(color.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1948
+ console.log("");
1949
+ };
1950
+ var printReadyFooter = () => {
1951
+ const lines = [
1952
+ "",
1953
+ ` ${color.boldGreen("Zero is ready!")} Run ${color.cyan("`zero search`")} to find capabilities.`,
1954
+ "",
1955
+ ` ${color.dim("Try:")}`,
1956
+ ` ${color.cyan('zero search "translate text to Spanish"')}`,
1957
+ ` ${color.cyan('zero search "generate an image"')}`,
1958
+ ` ${color.cyan('zero search "weather forecast"')}`,
1959
+ "",
1960
+ ` ${color.dim("By using Zero, you agree to our Terms of Service:")}`,
1961
+ ` ${color.dim("https://zero.xyz/terms-of-service")}`,
1962
+ ` ${color.dim("Run `zero terms` to view the full terms.")}`,
1963
+ ""
1964
+ ];
1965
+ return lines.join("\n");
1966
+ };
1967
+
1968
+ // src/util/secure-config.ts
1969
+ import { chmodSync, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
1970
+ var SECURE_DIR_MODE = 448;
1971
+ var SECURE_FILE_MODE = 384;
1972
+ var ensureSecureDir = (path) => {
1973
+ mkdirSync2(path, { recursive: true, mode: SECURE_DIR_MODE });
1974
+ chmodSync(path, SECURE_DIR_MODE);
1975
+ };
1976
+ var writeSecureFile = (path, contents) => {
1977
+ writeFileSync2(path, contents, { mode: SECURE_FILE_MODE });
1978
+ chmodSync(path, SECURE_FILE_MODE);
1979
+ };
1980
+
1981
+ // src/commands/init-command.ts
1627
1982
  var AGENT_TOOLS = [
1628
1983
  { name: "Claude Code", detectDir: ".claude", skillsDir: ".claude/skills" },
1629
1984
  { name: "Codex", detectDir: ".codex", skillsDir: ".agents/skills" },
@@ -1634,21 +1989,29 @@ var AGENT_TOOLS = [
1634
1989
  },
1635
1990
  { name: "Cursor", detectDir: ".cursor", skillsDir: ".cursor/skills" }
1636
1991
  ];
1637
- var getPackageRoot = () => {
1638
- let dir;
1639
- if (import.meta.url) {
1640
- dir = dirname(fileURLToPath(import.meta.url));
1641
- } else {
1642
- dir = __dirname;
1992
+ var findResourceDir = (startDir, resourceName) => {
1993
+ let current = startDir;
1994
+ while (true) {
1995
+ const candidate = join2(current, resourceName);
1996
+ if (existsSync2(candidate)) {
1997
+ return candidate;
1998
+ }
1999
+ const parent = dirname(current);
2000
+ if (parent === current) {
2001
+ throw new Error(
2002
+ `Could not locate bundled '${resourceName}' directory from ${startDir}`
2003
+ );
2004
+ }
2005
+ current = parent;
1643
2006
  }
1644
- while (!existsSync2(join2(dir, "package.json"))) {
1645
- const parent = dirname(dir);
1646
- if (parent === dir) break;
1647
- dir = parent;
2007
+ };
2008
+ var getCliModuleDir = () => {
2009
+ if (import.meta.url) {
2010
+ return dirname(fileURLToPath(import.meta.url));
1648
2011
  }
1649
- return dir;
2012
+ return __dirname;
1650
2013
  };
1651
- var sha256File = (filePath) => createHash3("sha256").update(readFileSync3(filePath)).digest("hex");
2014
+ var sha256File = (filePath) => createHash3("sha256").update(readFileSync4(filePath)).digest("hex");
1652
2015
  var verifyFileCopy = (src, dest) => {
1653
2016
  if (!existsSync2(dest)) return false;
1654
2017
  return sha256File(src) === sha256File(dest);
@@ -1666,50 +2029,69 @@ var collectAllFiles = (dir) => {
1666
2029
  return files;
1667
2030
  };
1668
2031
  var copyDirRecursive = (src, dest) => {
1669
- mkdirSync2(dest, { recursive: true });
2032
+ mkdirSync3(dest, { recursive: true });
1670
2033
  for (const entry of readdirSync(src, { withFileTypes: true })) {
1671
2034
  const srcPath = join2(src, entry.name);
1672
2035
  const destPath = join2(dest, entry.name);
1673
2036
  if (entry.isDirectory()) {
1674
2037
  copyDirRecursive(srcPath, destPath);
1675
2038
  } else {
1676
- const data = readFileSync3(srcPath);
1677
- writeFileSync2(destPath, data);
2039
+ const data = readFileSync4(srcPath);
2040
+ writeFileSync3(destPath, data);
1678
2041
  try {
1679
2042
  const mode = statSync(srcPath).mode;
1680
- chmodSync(destPath, mode);
2043
+ chmodSync2(destPath, mode);
1681
2044
  } catch {
1682
2045
  }
1683
2046
  }
1684
2047
  }
1685
2048
  };
1686
- var installHook = (home) => {
2049
+ var installHook = (home, verbose = false) => {
1687
2050
  const claudeDir = join2(home, ".claude");
1688
2051
  if (!existsSync2(claudeDir)) {
2052
+ if (verbose) {
2053
+ stepInfo(`~/.claude not found \u2014 Claude Code not installed, skipping`);
2054
+ }
1689
2055
  return false;
1690
2056
  }
1691
2057
  const zeroHooksDir = join2(home, ".zero", "hooks");
1692
- mkdirSync2(zeroHooksDir, { recursive: true });
2058
+ mkdirSync3(zeroHooksDir, { recursive: true });
2059
+ if (verbose) stepInfo(`staged hook dir at ${zeroHooksDir}`);
1693
2060
  const hookFiles = ["auto-approve-zero.sh", "zero-context.sh"];
1694
2061
  const hookDests = {};
2062
+ const hooksSourceDir = findResourceDir(getCliModuleDir(), "hooks");
1695
2063
  for (const hookFile of hookFiles) {
1696
- const hookSource = join2(getPackageRoot(), "hooks", hookFile);
2064
+ const hookSource = join2(hooksSourceDir, hookFile);
1697
2065
  const hookDest = join2(zeroHooksDir, hookFile);
1698
2066
  cpSync(hookSource, hookDest);
1699
- chmodSync(hookDest, 493);
2067
+ chmodSync2(hookDest, 493);
1700
2068
  if (!verifyFileCopy(hookSource, hookDest)) {
1701
2069
  throw new Error(
1702
2070
  `Integrity check failed: ${hookDest} does not match source`
1703
2071
  );
1704
2072
  }
1705
2073
  hookDests[hookFile] = hookDest;
2074
+ if (verbose) stepInfo(`copied ${hookFile} \u2192 ${hookDest} (verified)`);
1706
2075
  }
1707
2076
  const settingsPath = join2(claudeDir, "settings.json");
1708
2077
  let settings = {};
2078
+ let settingsExisted = false;
2079
+ let settingsCorrupted = false;
1709
2080
  if (existsSync2(settingsPath)) {
2081
+ settingsExisted = true;
1710
2082
  try {
1711
- settings = JSON.parse(readFileSync3(settingsPath, "utf-8"));
2083
+ settings = JSON.parse(readFileSync4(settingsPath, "utf-8"));
1712
2084
  } catch {
2085
+ settingsCorrupted = true;
2086
+ }
2087
+ }
2088
+ if (verbose) {
2089
+ if (!settingsExisted) {
2090
+ stepInfo(`${settingsPath} did not exist \u2014 creating`);
2091
+ } else if (settingsCorrupted) {
2092
+ stepInfo(`${settingsPath} was unparseable JSON \u2014 rewriting from scratch`);
2093
+ } else {
2094
+ stepInfo(`merging into existing ${settingsPath}`);
1713
2095
  }
1714
2096
  }
1715
2097
  if (!settings.hooks || typeof settings.hooks !== "object") {
@@ -1738,8 +2120,10 @@ var installHook = (home) => {
1738
2120
  });
1739
2121
  if (existingIdx >= 0) {
1740
2122
  preToolUse[existingIdx] = zeroHookEntry;
2123
+ if (verbose) stepInfo(`PreToolUse auto-approve-zero entry replaced`);
1741
2124
  } else {
1742
2125
  preToolUse.push(zeroHookEntry);
2126
+ if (verbose) stepInfo(`PreToolUse auto-approve-zero entry appended`);
1743
2127
  }
1744
2128
  if (!Array.isArray(hooks.UserPromptSubmit)) {
1745
2129
  hooks.UserPromptSubmit = [];
@@ -1762,8 +2146,10 @@ var installHook = (home) => {
1762
2146
  });
1763
2147
  if (existingContextIdx >= 0) {
1764
2148
  userPromptSubmit[existingContextIdx] = contextHookEntry;
2149
+ if (verbose) stepInfo(`UserPromptSubmit zero-context entry replaced`);
1765
2150
  } else {
1766
2151
  userPromptSubmit.push(contextHookEntry);
2152
+ if (verbose) stepInfo(`UserPromptSubmit zero-context entry appended`);
1767
2153
  }
1768
2154
  if (!settings.sandbox || typeof settings.sandbox !== "object") {
1769
2155
  settings.sandbox = {};
@@ -1780,12 +2166,15 @@ var installHook = (home) => {
1780
2166
  const zeroDomain = "*.zero.xyz";
1781
2167
  if (!allowedDomains.includes(zeroDomain)) {
1782
2168
  allowedDomains.push(zeroDomain);
2169
+ if (verbose) stepInfo(`sandbox.network.allowedDomains += ${zeroDomain}`);
2170
+ } else if (verbose) {
2171
+ stepInfo(`${zeroDomain} already in sandbox allowlist \u2014 not modified`);
1783
2172
  }
1784
- writeFileSync2(settingsPath, `${JSON.stringify(settings, null, 2)}
2173
+ writeFileSync3(settingsPath, `${JSON.stringify(settings, null, 2)}
1785
2174
  `);
1786
2175
  return true;
1787
2176
  };
1788
- var CONFLICTING_SKILL_PATTERNS = ["zam", "tempo"];
2177
+ var CONFLICTING_SKILL_PATTERNS = ["zam"];
1789
2178
  var findConflictingSkills = (home) => {
1790
2179
  const found = [];
1791
2180
  for (const tool of AGENT_TOOLS) {
@@ -1815,22 +2204,38 @@ var removeConflictingSkills = (home) => {
1815
2204
  }
1816
2205
  return removed;
1817
2206
  };
1818
- var installSkills = (home) => {
1819
- const skillsSourceDir = join2(getPackageRoot(), "skills");
2207
+ var installSkills = (home, verbose = false) => {
2208
+ const skillsSourceDir = findResourceDir(getCliModuleDir(), "skills");
1820
2209
  const skillDirs = readdirSync(skillsSourceDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
2210
+ if (verbose) {
2211
+ stepInfo(
2212
+ `found ${skillDirs.length} bundled skill(s): ${skillDirs.join(", ")}`
2213
+ );
2214
+ }
1821
2215
  const installed = [];
1822
2216
  const errors = [];
1823
2217
  for (const tool of AGENT_TOOLS) {
1824
2218
  const toolDetectPath = join2(home, tool.detectDir);
1825
2219
  if (!existsSync2(toolDetectPath)) {
2220
+ if (verbose) {
2221
+ stepInfo(
2222
+ `${tool.name}: ~/${tool.detectDir} not found \u2014 skipping install`
2223
+ );
2224
+ }
1826
2225
  continue;
1827
2226
  }
2227
+ if (verbose) {
2228
+ stepInfo(
2229
+ `${tool.name}: detected at ~/${tool.detectDir} \u2014 installing to ~/${tool.skillsDir}`
2230
+ );
2231
+ }
1828
2232
  try {
1829
2233
  const toolSkillsPath = join2(home, tool.skillsDir);
1830
- mkdirSync2(toolSkillsPath, { recursive: true });
2234
+ mkdirSync3(toolSkillsPath, { recursive: true });
1831
2235
  for (const skillDir of skillDirs) {
1832
2236
  const src = join2(skillsSourceDir, skillDir);
1833
2237
  const dest = join2(toolSkillsPath, skillDir);
2238
+ const existed = existsSync2(dest);
1834
2239
  copyDirRecursive(src, dest);
1835
2240
  for (const srcFile of collectAllFiles(src)) {
1836
2241
  const relPath = relative(src, srcFile);
@@ -1842,6 +2247,11 @@ var installSkills = (home) => {
1842
2247
  }
1843
2248
  }
1844
2249
  installed.push(`${tool.name}: ${dest}`);
2250
+ if (verbose) {
2251
+ stepInfo(
2252
+ `${tool.name}: ${existed ? "updated" : "installed"} skill '${skillDir}'`
2253
+ );
2254
+ }
1845
2255
  }
1846
2256
  } catch (err) {
1847
2257
  errors.push({
@@ -1853,11 +2263,13 @@ var installSkills = (home) => {
1853
2263
  return { installed, errors };
1854
2264
  };
1855
2265
  var runInit = async (appContext, options = {}) => {
2266
+ const verbose = options.verbose ?? false;
1856
2267
  appContext.services.analyticsService.capture("init_started", {
1857
2268
  force: options.force ?? false
1858
2269
  });
1859
2270
  let currentStep = "wallet";
1860
2271
  try {
2272
+ printZeroBanner();
1861
2273
  const home = homedir2();
1862
2274
  const zeroDir = join2(home, ".zero");
1863
2275
  const configPath = join2(zeroDir, "config.json");
@@ -1866,7 +2278,7 @@ var runInit = async (appContext, options = {}) => {
1866
2278
  const walletExists = (() => {
1867
2279
  if (!existsSync2(configPath)) return false;
1868
2280
  try {
1869
- const existing = JSON.parse(readFileSync3(configPath, "utf8"));
2281
+ const existing = JSON.parse(readFileSync4(configPath, "utf8"));
1870
2282
  return !!existing.privateKey;
1871
2283
  } catch {
1872
2284
  return false;
@@ -1875,9 +2287,9 @@ var runInit = async (appContext, options = {}) => {
1875
2287
  if (!walletExists || options.force) {
1876
2288
  const privateKey = generatePrivateKey();
1877
2289
  const account = privateKeyToAccount(privateKey);
1878
- mkdirSync2(zeroDir, { recursive: true });
1879
- const existing = existsSync2(configPath) ? JSON.parse(readFileSync3(configPath, "utf8")) : {};
1880
- writeFileSync2(
2290
+ ensureSecureDir(zeroDir);
2291
+ const existing = existsSync2(configPath) ? JSON.parse(readFileSync4(configPath, "utf8")) : {};
2292
+ writeSecureFile(
1881
2293
  configPath,
1882
2294
  JSON.stringify(
1883
2295
  { ...existing, privateKey, lowBalanceWarning: 1 },
@@ -1887,14 +2299,32 @@ var runInit = async (appContext, options = {}) => {
1887
2299
  );
1888
2300
  walletCreated = true;
1889
2301
  walletAddress = account.address;
1890
- console.log(`Wallet address: ${account.address}`);
2302
+ stepSuccess("Wallet created", account.address);
2303
+ if (verbose) {
2304
+ if (options.force) {
2305
+ stepInfo(
2306
+ `--force passed \u2014 generated a new key and overwrote ${configPath}`
2307
+ );
2308
+ } else {
2309
+ stepInfo(`no wallet found at ${configPath} \u2014 generated a new key`);
2310
+ }
2311
+ }
1891
2312
  } else {
1892
2313
  try {
1893
- const existing = JSON.parse(readFileSync3(configPath, "utf8"));
2314
+ const existing = JSON.parse(readFileSync4(configPath, "utf8"));
1894
2315
  const account = privateKeyToAccount(existing.privateKey);
1895
2316
  walletAddress = account.address;
1896
2317
  } catch {
1897
2318
  }
2319
+ stepSkip(
2320
+ "Wallet already configured",
2321
+ walletAddress ?? "run `zero init --force` to reset"
2322
+ );
2323
+ if (verbose) {
2324
+ stepInfo(
2325
+ `existing wallet at ${configPath} was preserved \u2014 pass --force to regenerate`
2326
+ );
2327
+ }
1898
2328
  }
1899
2329
  const agentsDetected = [];
1900
2330
  const agentsWithSkills = [];
@@ -1906,9 +2336,20 @@ var runInit = async (appContext, options = {}) => {
1906
2336
  agentsDetected.push(tool.name);
1907
2337
  }
1908
2338
  }
2339
+ if (verbose) {
2340
+ const missing = AGENT_TOOLS.filter(
2341
+ (t) => !agentsDetected.includes(t.name)
2342
+ ).map((t) => t.name);
2343
+ stepInfo(
2344
+ `agents detected: ${agentsDetected.length > 0 ? agentsDetected.join(", ") : "none"}`
2345
+ );
2346
+ if (missing.length > 0) {
2347
+ stepInfo(`agents not detected: ${missing.join(", ")}`);
2348
+ }
2349
+ }
1909
2350
  currentStep = "skills";
1910
2351
  try {
1911
- const { installed, errors } = installSkills(home);
2352
+ const { installed, errors } = installSkills(home, verbose);
1912
2353
  for (const entry of installed) {
1913
2354
  const toolName = entry.split(":")[0];
1914
2355
  if (toolName && !agentsWithSkills.includes(toolName)) {
@@ -1927,27 +2368,45 @@ var runInit = async (appContext, options = {}) => {
1927
2368
  skillsError = err instanceof Error ? err.message : "unknown skills error";
1928
2369
  console.error(`Warning: skills install failed: ${skillsError}`);
1929
2370
  }
2371
+ if (agentsWithSkills.length > 0) {
2372
+ stepSuccess("Skills installed");
2373
+ } else if (agentsDetected.length === 0) {
2374
+ stepSkip("No agent tools detected");
2375
+ } else if (skillsError) {
2376
+ stepWarn("Skills install had errors", skillsError);
2377
+ }
1930
2378
  currentStep = "hook";
1931
2379
  try {
1932
- hookInstalled = installHook(home);
2380
+ hookInstalled = installHook(home, verbose);
1933
2381
  } catch (err) {
1934
2382
  hookError = err instanceof Error ? err.message : "unknown hook error";
1935
2383
  }
2384
+ if (hookInstalled) {
2385
+ stepSuccess("Agents configured");
2386
+ } else if (hookError) {
2387
+ stepWarn("Agent config failed", hookError);
2388
+ }
1936
2389
  currentStep = "cleanup_scan";
1937
2390
  const conflictingSkills = findConflictingSkills(home);
2391
+ if (verbose) {
2392
+ stepInfo(
2393
+ `scanning agent skill dirs for deprecated skills matching [${CONFLICTING_SKILL_PATTERNS.join(", ")}]`
2394
+ );
2395
+ }
1938
2396
  if (conflictingSkills.length > 0) {
1939
2397
  const skillList = conflictingSkills.map((s) => ` - ${s.tool}: ${s.skillName}`).join("\n");
1940
2398
  console.error(
1941
2399
  `
1942
- Found deprecated skills that may conflict with Zero:
2400
+ ${color.yellow("Found deprecated skills that may conflict with Zero:")}
1943
2401
  ${skillList}
1944
2402
 
1945
- To remove them, run: zero init cleanup`
2403
+ To remove them, run: ${color.cyan("zero init cleanup")}`
1946
2404
  );
2405
+ } else if (verbose) {
2406
+ stepInfo(`no deprecated skills found`);
1947
2407
  }
1948
- console.error(
1949
- 'Zero is ready! Run `zero search` to find capabilities.\n\nBy using Zero, you agree to our Terms of Service:\n https://zero.xyz/terms-of-service\n\nRun `zero terms` to view the full terms.\n\nTry:\n zero search "translate text to Spanish"\n zero search "generate an image"\n zero search "weather forecast"'
1950
- );
2408
+ sectionDivider();
2409
+ console.error(printReadyFooter());
1951
2410
  currentStep = "complete";
1952
2411
  appContext.services.analyticsService.capture("wallet_initialized", {
1953
2412
  // biome-ignore lint/style/useNamingConvention: snake_case for analytics
@@ -1982,12 +2441,13 @@ To remove them, run: zero init cleanup`
1982
2441
  throw err;
1983
2442
  }
1984
2443
  };
1985
- var initCommand = (appContext) => new Command5("init").description("Initialize Zero CLI for usage").option("--force", "Overwrite existing configuration").action(async (options) => {
2444
+ var initCommand = (appContext) => new Command5("init").description("Initialize Zero CLI for usage").option("--force", "Overwrite existing configuration").option(
2445
+ "-v, --verbose",
2446
+ "Explain why each install step was taken or skipped"
2447
+ ).action(async (options) => {
1986
2448
  await runInit(appContext, options);
1987
2449
  }).addCommand(
1988
- new Command5("cleanup").description(
1989
- "Remove deprecated skills (zam, tempo) that conflict with Zero"
1990
- ).action(() => {
2450
+ new Command5("cleanup").description("Remove deprecated skills (zam) that conflict with Zero").action(() => {
1991
2451
  const home = homedir2();
1992
2452
  const removed = removeConflictingSkills(home);
1993
2453
  if (removed.length === 0) {
@@ -2007,7 +2467,7 @@ ${removedList}`);
2007
2467
  );
2008
2468
 
2009
2469
  // src/commands/review-command.ts
2010
- import { readFileSync as readFileSync4 } from "fs";
2470
+ import { readFileSync as readFileSync5 } from "fs";
2011
2471
  import { Command as Command6 } from "commander";
2012
2472
  import { z as z3 } from "zod";
2013
2473
  var bulkEntrySchema2 = z3.object({
@@ -2049,35 +2509,66 @@ Examples:
2049
2509
  try {
2050
2510
  const { analyticsService, apiService } = appContext.services;
2051
2511
  if (options.fromFile) {
2052
- const contents = readFileSync4(options.fromFile, "utf8");
2512
+ const contents = readFileSync5(options.fromFile, "utf8");
2053
2513
  const lines = contents.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
2054
- let ok = 0;
2055
- let failed = 0;
2514
+ const parsed = [];
2515
+ let parseFailures = 0;
2056
2516
  for (const [idx, line] of lines.entries()) {
2057
2517
  const lineNum = idx + 1;
2058
2518
  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
2519
+ parsed.push({
2520
+ lineNum,
2521
+ entry: bulkEntrySchema2.parse(JSON.parse(line))
2073
2522
  });
2074
2523
  } catch (err) {
2075
- failed += 1;
2524
+ parseFailures += 1;
2076
2525
  console.error(
2077
- `[${lineNum}] FAILED: ${err instanceof Error ? err.message : String(err)}`
2526
+ `[${lineNum}] PARSE FAILED: ${err instanceof Error ? err.message : String(err)}`
2078
2527
  );
2079
2528
  }
2080
2529
  }
2530
+ if (parsed.length === 0) {
2531
+ console.error("No valid review lines found in file.");
2532
+ process.exitCode = 1;
2533
+ return;
2534
+ }
2535
+ const Chunk = 100;
2536
+ let ok = 0;
2537
+ let failed = parseFailures;
2538
+ for (let i = 0; i < parsed.length; i += Chunk) {
2539
+ const chunk = parsed.slice(i, i + Chunk);
2540
+ const response = await apiService.createReviewsBatch(
2541
+ chunk.map((p) => p.entry)
2542
+ );
2543
+ for (const [chunkIdx, item] of response.results.entries()) {
2544
+ const lineNum = chunk[chunkIdx]?.lineNum ?? i + chunkIdx + 1;
2545
+ if (item.ok) {
2546
+ ok += 1;
2547
+ const suffix = item.updated ? " (updated)" : "";
2548
+ console.log(
2549
+ `[${lineNum}] ${item.runId} -> ${item.reviewId}${suffix}`
2550
+ );
2551
+ const entry = chunk[chunkIdx]?.entry;
2552
+ if (entry) {
2553
+ analyticsService.capture("review_submitted", {
2554
+ runId: entry.runId,
2555
+ success: entry.success,
2556
+ accuracy: entry.accuracy,
2557
+ value: entry.value,
2558
+ reliability: entry.reliability,
2559
+ hasContent: !!entry.content,
2560
+ bulk: true,
2561
+ updated: item.updated
2562
+ });
2563
+ }
2564
+ } else {
2565
+ failed += 1;
2566
+ console.error(
2567
+ `[${lineNum}] FAILED (${item.status}): ${item.error}`
2568
+ );
2569
+ }
2570
+ }
2571
+ }
2081
2572
  console.log(`
2082
2573
  Bulk review complete: ${ok} ok, ${failed} failed`);
2083
2574
  if (failed > 0) process.exitCode = 1;
@@ -2116,7 +2607,7 @@ Bulk review complete: ${ok} ok, ${failed} failed`);
2116
2607
  }
2117
2608
  if (list.runs.length > 1) {
2118
2609
  console.error(
2119
- `Multiple un-reviewed runs for "${options.capability}". Run "zero runs --capability ${options.capability} --unreviewed" and pass the runId explicitly.`
2610
+ `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
2611
  );
2121
2612
  process.exitCode = 1;
2122
2613
  return;
@@ -2330,7 +2821,8 @@ var searchCommand = (appContext) => new Command8("search").description("Search f
2330
2821
  capabilities: result.capabilities.map((c) => ({
2331
2822
  position: c.position,
2332
2823
  id: c.id,
2333
- url: c.url
2824
+ url: c.url,
2825
+ displayCostAmount: c.cost.amount
2334
2826
  }))
2335
2827
  });
2336
2828
  console.log(formatSearchResults(result.capabilities));
@@ -2373,7 +2865,7 @@ Read the full terms at: ${TERMS_URL}
2373
2865
  });
2374
2866
 
2375
2867
  // src/commands/wallet-command.ts
2376
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
2868
+ import { existsSync as existsSync3, readFileSync as readFileSync6 } from "fs";
2377
2869
  import { homedir as homedir3 } from "os";
2378
2870
  import { join as join3 } from "path";
2379
2871
  import { Command as Command10 } from "commander";
@@ -2395,6 +2887,9 @@ var walletBalanceCommand = (appContext) => new Command10("balance").description(
2395
2887
  console.log(`${balance.amount} ${balance.asset}`);
2396
2888
  });
2397
2889
  var walletFundCommand = (appContext) => new Command10("fund").description("Fund your wallet").argument("[amount]", "Amount to fund in USDC").option("--manual", "Show wallet address for manual transfer").option(
2890
+ "--no-open",
2891
+ "Print the funding URL instead of opening a browser (for agents \u2014 funding links are one-time use, hand the URL to the user)"
2892
+ ).option(
2398
2893
  "--use <provider>",
2399
2894
  "Onramp provider: coinbase or stripe",
2400
2895
  "coinbase"
@@ -2422,12 +2917,19 @@ ${address}`);
2422
2917
  provider
2423
2918
  );
2424
2919
  if (url) {
2425
- await open(url);
2426
- console.log("Opened funding page in your browser.");
2427
- console.log(`If it didn't open, visit: ${url}`);
2920
+ if (options.open) {
2921
+ await open(url);
2922
+ console.log("Opened funding page in your browser.");
2923
+ console.log(`If it didn't open, visit: ${url}`);
2924
+ } else {
2925
+ console.log(
2926
+ "Funding URL (one-time use \u2014 open it in a browser to fund):"
2927
+ );
2928
+ console.log(url);
2929
+ }
2428
2930
  console.log(`Your wallet address: ${address}`);
2429
2931
  analyticsService.capture("wallet_funded", {
2430
- method: "browser",
2932
+ method: options.open ? "browser" : "url",
2431
2933
  amount
2432
2934
  });
2433
2935
  } else {
@@ -2468,7 +2970,7 @@ var walletSetCommand = (appContext) => new Command10("set").description("Set wal
2468
2970
  const configPath = join3(zeroDir, "config.json");
2469
2971
  if (!options.force && existsSync3(configPath)) {
2470
2972
  try {
2471
- const existing2 = JSON.parse(readFileSync5(configPath, "utf8"));
2973
+ const existing2 = JSON.parse(readFileSync6(configPath, "utf8"));
2472
2974
  if (existing2.privateKey) {
2473
2975
  console.error(
2474
2976
  "Wallet already configured. Use --force to overwrite."
@@ -2479,9 +2981,9 @@ var walletSetCommand = (appContext) => new Command10("set").description("Set wal
2479
2981
  } catch {
2480
2982
  }
2481
2983
  }
2482
- mkdirSync3(zeroDir, { recursive: true });
2483
- const existing = existsSync3(configPath) ? JSON.parse(readFileSync5(configPath, "utf8")) : {};
2484
- writeFileSync3(
2984
+ ensureSecureDir(zeroDir);
2985
+ const existing = existsSync3(configPath) ? JSON.parse(readFileSync6(configPath, "utf8")) : {};
2986
+ writeSecureFile(
2485
2987
  configPath,
2486
2988
  JSON.stringify(
2487
2989
  {
@@ -2510,7 +3012,7 @@ var walletCommand = (appContext) => {
2510
3012
  };
2511
3013
 
2512
3014
  // src/commands/welcome-command.ts
2513
- import { existsSync as existsSync4, readFileSync as readFileSync6 } from "fs";
3015
+ import { existsSync as existsSync4, readFileSync as readFileSync7 } from "fs";
2514
3016
  import { homedir as homedir4 } from "os";
2515
3017
  import { join as join4 } from "path";
2516
3018
  import { Command as Command11 } from "commander";
@@ -2521,7 +3023,7 @@ var readPrivateKey = () => {
2521
3023
  const configPath = join4(homedir4(), ".zero", "config.json");
2522
3024
  if (!existsSync4(configPath)) return null;
2523
3025
  try {
2524
- const config = JSON.parse(readFileSync6(configPath, "utf8"));
3026
+ const config = JSON.parse(readFileSync7(configPath, "utf8"));
2525
3027
  if (typeof config.privateKey === "string") {
2526
3028
  return config.privateKey;
2527
3029
  }
@@ -2573,7 +3075,7 @@ If your browser didn't open, paste the URL above.`
2573
3075
  // src/app.ts
2574
3076
  var createApp = (appContext) => {
2575
3077
  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) => {
3078
+ 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
3079
  const agentFlag = actionCommand.opts().agent;
2578
3080
  if (typeof agentFlag === "string" && agentFlag.trim().length > 0) {
2579
3081
  analyticsService.setAgentHost(agentFlag.trim());
@@ -2620,14 +3122,14 @@ var getEnv = () => {
2620
3122
 
2621
3123
  // src/app/app-services.ts
2622
3124
  import { randomUUID as randomUUID2 } from "crypto";
2623
- import { existsSync as existsSync7, readFileSync as readFileSync9 } from "fs";
3125
+ import { existsSync as existsSync7, readFileSync as readFileSync10 } from "fs";
2624
3126
  import { homedir as homedir5 } from "os";
2625
3127
  import { join as join6 } from "path";
2626
3128
  import { privateKeyToAccount as privateKeyToAccount4 } from "viem/accounts";
2627
3129
 
2628
3130
  // src/services/analytics-service.ts
2629
3131
  import { randomUUID } from "crypto";
2630
- import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
3132
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
2631
3133
  import { dirname as dirname2 } from "path";
2632
3134
  import { PostHog } from "posthog-node";
2633
3135
  var POSTHOG_API_KEY = "phc_B2vLyNxAf2mnqvdPQajf4d4b2iXc35dep2ZrvebMJLuX";
@@ -2650,7 +3152,7 @@ var AnalyticsService = class {
2650
3152
  let persistedAnonId;
2651
3153
  try {
2652
3154
  if (existsSync5(opts.configPath)) {
2653
- const config = JSON.parse(readFileSync7(opts.configPath, "utf8"));
3155
+ const config = JSON.parse(readFileSync8(opts.configPath, "utf8"));
2654
3156
  if (config.telemetry === false) {
2655
3157
  telemetryEnabled = false;
2656
3158
  }
@@ -2675,7 +3177,7 @@ var AnalyticsService = class {
2675
3177
  try {
2676
3178
  const dir = dirname2(opts.configPath);
2677
3179
  mkdirSync4(dir, { recursive: true });
2678
- const existing = existsSync5(opts.configPath) ? JSON.parse(readFileSync7(opts.configPath, "utf8")) : {};
3180
+ const existing = existsSync5(opts.configPath) ? JSON.parse(readFileSync8(opts.configPath, "utf8")) : {};
2679
3181
  writeFileSync4(
2680
3182
  opts.configPath,
2681
3183
  JSON.stringify({ ...existing, anonId: newAnonId }, null, 2)
@@ -2711,7 +3213,7 @@ var AnalyticsService = class {
2711
3213
  if (anonId === walletAddress) return;
2712
3214
  let aliasedTo;
2713
3215
  try {
2714
- const config = JSON.parse(readFileSync7(configPath, "utf8"));
3216
+ const config = JSON.parse(readFileSync8(configPath, "utf8"));
2715
3217
  if (typeof config.aliasedTo === "string") {
2716
3218
  aliasedTo = config.aliasedTo;
2717
3219
  }
@@ -2720,7 +3222,7 @@ var AnalyticsService = class {
2720
3222
  if (aliasedTo === walletAddress) return;
2721
3223
  this.posthog.alias({ distinctId: walletAddress, alias: anonId });
2722
3224
  try {
2723
- const config = existsSync5(configPath) ? JSON.parse(readFileSync7(configPath, "utf8")) : {};
3225
+ const config = existsSync5(configPath) ? JSON.parse(readFileSync8(configPath, "utf8")) : {};
2724
3226
  writeFileSync4(
2725
3227
  configPath,
2726
3228
  JSON.stringify({ ...config, aliasedTo: walletAddress }, null, 2)
@@ -2763,7 +3265,7 @@ var AnalyticsService = class {
2763
3265
  };
2764
3266
 
2765
3267
  // src/services/state-service.ts
2766
- import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
3268
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "fs";
2767
3269
  import { join as join5 } from "path";
2768
3270
  var StateService = class {
2769
3271
  constructor(zeroDir) {
@@ -2778,7 +3280,7 @@ var StateService = class {
2778
3280
  loadLastSearch = () => {
2779
3281
  try {
2780
3282
  if (!existsSync6(this.lastSearchPath)) return null;
2781
- const raw = readFileSync8(this.lastSearchPath, "utf8");
3283
+ const raw = readFileSync9(this.lastSearchPath, "utf8");
2782
3284
  return JSON.parse(raw);
2783
3285
  } catch {
2784
3286
  return null;
@@ -2831,7 +3333,7 @@ var getServices = (env) => {
2831
3333
  if (!privateKey) {
2832
3334
  try {
2833
3335
  if (existsSync7(configPath)) {
2834
- const config = JSON.parse(readFileSync9(configPath, "utf8"));
3336
+ const config = JSON.parse(readFileSync10(configPath, "utf8"));
2835
3337
  if (typeof config.privateKey === "string") {
2836
3338
  privateKey = config.privateKey;
2837
3339
  }
@@ -2843,7 +3345,7 @@ var getServices = (env) => {
2843
3345
  let lowBalanceWarning = 1;
2844
3346
  try {
2845
3347
  if (existsSync7(configPath)) {
2846
- const config = JSON.parse(readFileSync9(configPath, "utf8"));
3348
+ const config = JSON.parse(readFileSync10(configPath, "utf8"));
2847
3349
  if (typeof config.lowBalanceWarning === "number") {
2848
3350
  lowBalanceWarning = config.lowBalanceWarning;
2849
3351
  }