@zeroxyz/cli 0.0.24 → 0.0.26

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
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/app.ts
4
- import { Command as Command10 } from "commander";
4
+ import { Command as Command11 } from "commander";
5
5
 
6
6
  // package.json
7
7
  var package_default = {
8
8
  name: "@zeroxyz/cli",
9
- version: "0.0.24",
9
+ version: "0.0.26",
10
10
  type: "module",
11
11
  bin: {
12
12
  zero: "dist/index.js",
@@ -24,8 +24,8 @@ var package_default = {
24
24
  build: "tsup src/index.ts --format esm --out-dir dist --clean",
25
25
  "build:binary": "tsup --config tsup.binary.ts && cp -r skills hooks dist/pkg/ && pnpm exec pkg dist/pkg/index.cjs --config pkg.json --targets node24-macos-arm64,node24-macos-x64,node24-linux-x64 --output dist/bin/zero",
26
26
  prepublishOnly: "pnpm run build",
27
- dev: "tsx src/index.ts",
28
- cli: "ZERO_API_URL=http://localhost:1111 tsx src/index.ts",
27
+ dev: "ZERO_ENV=development tsx src/index.ts",
28
+ cli: "ZERO_ENV=development ZERO_API_URL=http://localhost:1111 tsx src/index.ts",
29
29
  "test:integration": "vitest run --project integration",
30
30
  "test:online": "vitest run --project online",
31
31
  "test:unit": "vitest run --project unit",
@@ -279,10 +279,14 @@ var ApiService = class {
279
279
  const json = await this.request("POST", "/v1/bug-reports", data);
280
280
  return createBugReportResponseSchema.parse(json);
281
281
  };
282
- getFundingUrl = async (amount) => {
282
+ getFundingUrl = async (amount, provider = "coinbase") => {
283
283
  try {
284
- const qs = amount ? `?amount=${encodeURIComponent(amount)}` : "";
285
- const json = await this.request("GET", `/v1/wallet/fund-url${qs}`);
284
+ const params = new URLSearchParams({ provider });
285
+ if (amount) params.set("amount", amount);
286
+ const json = await this.request(
287
+ "GET",
288
+ `/v1/wallet/fund-url?${params.toString()}`
289
+ );
286
290
  const parsed = z.object({ url: z.string() }).parse(json);
287
291
  return parsed.url;
288
292
  } catch {
@@ -376,7 +380,10 @@ Categories the classifier picks from:
376
380
  ).option("--idempotency-key <key>", "Override the auto-generated dedup key").option(
377
381
  "--from-file <path>",
378
382
  "Submit bug reports in bulk from a JSONL file (one report per line)"
379
- ).option("--json", "Emit the API result as JSON on stdout (for batch use)").action(
383
+ ).option("--json", "Emit the API result as JSON on stdout (for batch use)").option(
384
+ "--agent <name>",
385
+ "Identify your agent host for this invocation. Overrides auto-detect for this call only."
386
+ ).action(
380
387
  async (description, options) => {
381
388
  try {
382
389
  const { analyticsService, apiService, stateService } = appContext.services;
@@ -417,7 +424,10 @@ Categories the classifier picks from:
417
424
  category: result2.category,
418
425
  severity: parsed.severity ?? 2,
419
426
  bulk: true,
420
- deduped: result2.deduped
427
+ deduped: result2.deduped,
428
+ hasCapability: !!result2.attached.capabilityId,
429
+ hasRun: !!result2.attached.runId,
430
+ hasSearch: !!result2.attached.searchId
421
431
  });
422
432
  } catch (err) {
423
433
  failed += 1;
@@ -502,9 +512,15 @@ Bulk bug-report complete: ${ok} ok, ${failed} failed`
502
512
  }
503
513
  analyticsService.capture("bug_report_submitted", {
504
514
  category: result.category,
515
+ categoryOverridden: !!options.category,
505
516
  severity: options.severity ?? 2,
506
517
  deduped: result.deduped,
507
- autoContext: useAutoContext
518
+ autoContext: useAutoContext,
519
+ hasCapability: !!result.attached.capabilityId,
520
+ hasRun: !!result.attached.runId,
521
+ hasSearch: !!result.attached.searchId,
522
+ hasTitle: !!options.title,
523
+ hasReproduction: !!options.reproduction
508
524
  });
509
525
  } catch (err) {
510
526
  console.error(
@@ -567,125 +583,677 @@ var configCommand = (_appContext) => new Command2("config").description("View or
567
583
 
568
584
  // src/commands/fetch-command.ts
569
585
  import { Command as Command3 } from "commander";
586
+ import { formatUnits as formatUnits2 } from "viem";
570
587
 
571
- // src/util/infer-schema.ts
572
- var inferSchema = (value, depth = 0) => {
573
- if (depth > 6) return { type: typeOf(value) };
574
- if (value === null) return { type: "null" };
575
- if (Array.isArray(value)) {
576
- const itemSchemas = value.slice(0, 3).map((v) => inferSchema(v, depth + 1));
577
- return {
578
- type: "array",
579
- items: itemSchemas[0] ?? { type: "unknown" }
580
- };
588
+ // src/services/payment-service.ts
589
+ import {
590
+ adaptViemWallet,
591
+ convertViemChainToRelayChain,
592
+ createClient as createRelayClient,
593
+ getClient as getRelayClient,
594
+ MAINNET_RELAY_API
595
+ } from "@relayprotocol/relay-sdk";
596
+ import { x402Client as X402Client } from "@x402/core/client";
597
+ import { decodePaymentResponseHeader, x402HTTPClient } from "@x402/core/http";
598
+ import { ExactEvmScheme } from "@x402/evm/exact/client";
599
+ import { createSIWxClientHook } from "@x402/extensions/sign-in-with-x";
600
+ import { wrapFetchWithPayment } from "@x402/fetch";
601
+ import { Challenge, Receipt } from "mppx";
602
+ import { Mppx, tempo } from "mppx/client";
603
+ import {
604
+ createPublicClient,
605
+ createWalletClient,
606
+ formatUnits,
607
+ http
608
+ } from "viem";
609
+ import { base, baseSepolia } from "viem/chains";
610
+ var SessionCloseFailedError = class extends Error {
611
+ session;
612
+ response;
613
+ capturedAmount;
614
+ constructor(params) {
615
+ super(params.message);
616
+ this.name = "SessionCloseFailedError";
617
+ this.session = params.session;
618
+ this.response = params.response;
619
+ this.capturedAmount = params.capturedAmount;
581
620
  }
582
- if (typeof value === "object") {
583
- const obj = value;
584
- const properties = {};
585
- const required = [];
586
- for (const [k, v] of Object.entries(obj)) {
587
- properties[k] = inferSchema(v, depth + 1);
588
- required.push(k);
589
- }
590
- return { type: "object", properties, required };
621
+ };
622
+ var USDC_BASE = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913";
623
+ var USDC_BASE_SEPOLIA = "0x036CbD53842c5426634e7929541eC2318f3dCF7e";
624
+ var USDC_TEMPO = "0x20c000000000000000000000b9537d11c60e8b50";
625
+ var PATHUSD_TEMPO = "0x20c0000000000000000000000000000000000000";
626
+ var BASE_CHAIN_ID = 8453;
627
+ var TEMPO_CHAIN_ID = 4217;
628
+ var TEMPO_TESTNET_CHAIN_ID = 42431;
629
+ var DEFAULT_MAX_DEPOSIT = "100";
630
+ var KNOWN_EIP712_DOMAINS = {
631
+ // USDC on Base
632
+ [USDC_BASE.toLowerCase()]: { name: "USDC", version: "2" },
633
+ // USDC on Base Sepolia
634
+ [USDC_BASE_SEPOLIA.toLowerCase()]: { name: "USDC", version: "2" }
635
+ };
636
+ var buildRelayClientOptions = () => ({
637
+ baseApiUrl: MAINNET_RELAY_API,
638
+ source: "zero-cli",
639
+ chains: [convertViemChainToRelayChain(base)]
640
+ });
641
+ var calculateBuffer = (baseBalance) => {
642
+ const twentyFivePercent = baseBalance / 4n;
643
+ const twoDollars = 2000000n;
644
+ return twentyFivePercent < twoDollars ? twentyFivePercent : twoDollars;
645
+ };
646
+ var tempoChain = {
647
+ id: TEMPO_CHAIN_ID,
648
+ name: "Tempo",
649
+ nativeCurrency: { name: "USD", symbol: "USD", decimals: 18 },
650
+ rpcUrls: {
651
+ default: { http: ["https://rpc.tempo.xyz"] }
591
652
  }
592
- return { type: typeOf(value) };
593
653
  };
594
- var typeOf = (v) => {
595
- if (v === null) return "null";
596
- if (Array.isArray(v)) return "array";
597
- return typeof v;
654
+ var tempoTestnetChain = {
655
+ id: TEMPO_TESTNET_CHAIN_ID,
656
+ name: "Tempo Testnet",
657
+ nativeCurrency: { name: "USD", symbol: "USD", decimals: 18 },
658
+ rpcUrls: {
659
+ default: { http: ["https://rpc.moderato.tempo.xyz"] }
660
+ }
598
661
  };
599
- var tryParseJson = (text) => {
662
+ var ERC20_BALANCE_ABI = [
663
+ {
664
+ inputs: [{ name: "account", type: "address" }],
665
+ name: "balanceOf",
666
+ outputs: [{ name: "", type: "uint256" }],
667
+ stateMutability: "view",
668
+ type: "function"
669
+ }
670
+ ];
671
+ var decodeSessionReceiptHeader = (header) => {
672
+ if (!header) return null;
600
673
  try {
601
- return JSON.parse(text);
674
+ const padLen = (4 - header.length % 4) % 4;
675
+ const padded = header.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat(padLen);
676
+ const json = Buffer.from(padded, "base64").toString("utf8");
677
+ const parsed = JSON.parse(json);
678
+ if (typeof parsed.channelId !== "string" || typeof parsed.acceptedCumulative !== "string" || typeof parsed.spent !== "string" || typeof parsed.challengeId !== "string") {
679
+ return null;
680
+ }
681
+ return parsed;
602
682
  } catch {
603
683
  return null;
604
684
  }
605
685
  };
606
-
607
- // src/commands/fetch-command.ts
608
- var detectPaymentRequirement = (headers, status) => {
609
- if (status !== 402) return null;
610
- const x402Header = headers.get("payment-required") ?? headers.get("x-payment-required");
611
- if (x402Header) {
612
- try {
613
- const decoded = JSON.parse(
614
- Buffer.from(x402Header, "base64").toString("utf8")
686
+ var readSessionReceipt = (response) => decodeSessionReceiptHeader(
687
+ response.headers.get("Payment-Receipt") ?? response.headers.get("payment-receipt")
688
+ );
689
+ var pickSessionCloseAmount = (receipt, openTimeCumulative) => {
690
+ if (!receipt) return openTimeCumulative;
691
+ const accepted = BigInt(receipt.acceptedCumulative);
692
+ const spent = BigInt(receipt.spent);
693
+ const fromReceipt = accepted > spent ? accepted : spent;
694
+ return fromReceipt > openTimeCumulative ? fromReceipt : openTimeCumulative;
695
+ };
696
+ var PaymentService = class {
697
+ constructor(account, config, deps = {}) {
698
+ this.account = account;
699
+ this.config = config;
700
+ this.fetchOverride = deps.fetchImpl;
701
+ }
702
+ relayInitialized = false;
703
+ fetchOverride;
704
+ /**
705
+ * Resolve the fetch implementation lazily so tests can `vi.stubGlobal`
706
+ * the global `fetch` after the service is constructed.
707
+ */
708
+ get fetchImpl() {
709
+ return this.fetchOverride ?? globalThis.fetch;
710
+ }
711
+ getAccount = () => this.account;
712
+ ensureRelayClient = () => {
713
+ if (!this.relayInitialized) {
714
+ createRelayClient(buildRelayClientOptions());
715
+ this.relayInitialized = true;
716
+ }
717
+ };
718
+ bridgeToTempo = async (requiredAmount, onProgress) => {
719
+ if (!this.account) throw new Error("No wallet configured");
720
+ this.ensureRelayClient();
721
+ const baseBalance = await this.getBalanceRaw("base");
722
+ const buffer = calculateBuffer(baseBalance);
723
+ const bridgeAmount = requiredAmount + buffer;
724
+ if (baseBalance < bridgeAmount) {
725
+ throw new Error(
726
+ `Insufficient Base USDC to bridge: have ${formatUnits(baseBalance, 6)}, need ${formatUnits(bridgeAmount, 6)} (${formatUnits(requiredAmount, 6)} + ${formatUnits(buffer, 6)} buffer)`
615
727
  );
616
- return { protocol: "x402", raw: decoded };
617
- } catch {
618
- return { protocol: "x402", raw: { encoded: x402Header } };
619
728
  }
620
- }
621
- const wwwAuth = headers.get("www-authenticate");
622
- if (wwwAuth?.toLowerCase().includes("payment")) {
623
- return { protocol: "mpp", raw: { "www-authenticate": wwwAuth } };
624
- }
625
- return { protocol: "unknown", raw: {} };
626
- };
627
- var fetchCommand = (appContext) => new Command3("fetch").description("Fetch a capability URL with automatic payment handling").argument("<url>", "URL to fetch").option(
628
- "-X, --method <method>",
629
- "HTTP method (GET, POST, PUT, PATCH, DELETE). Defaults to POST when -d is set, otherwise GET"
630
- ).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(
631
- "--capability <id>",
632
- "Bind this fetch to a capability (uid or slug) so a reviewable run is recorded even without a prior `zero search`"
633
- ).option(
634
- "--json",
635
- "Emit {runId, status, latencyMs, payment, body} as JSON on stdout (for batch/non-TTY use)"
636
- ).action(
637
- async (url, options) => {
638
- try {
639
- const {
640
- analyticsService,
641
- apiService,
642
- paymentService,
643
- stateService,
644
- walletService
645
- } = appContext.services;
646
- const startTime = Date.now();
647
- const headers = {};
648
- if (options.header) {
649
- for (const h of options.header) {
650
- const colonIdx = h.indexOf(":");
651
- if (colonIdx > 0) {
652
- headers[h.slice(0, colonIdx).trim()] = h.slice(colonIdx + 1).trim();
653
- }
729
+ onProgress?.(
730
+ `Bridging ${formatUnits(bridgeAmount, 6)} USDC from Base to Tempo...`
731
+ );
732
+ const walletClient = createWalletClient({
733
+ account: this.account,
734
+ chain: base,
735
+ transport: http()
736
+ });
737
+ const quote = await getRelayClient().actions.getQuote({
738
+ chainId: BASE_CHAIN_ID,
739
+ toChainId: TEMPO_CHAIN_ID,
740
+ currency: USDC_BASE,
741
+ toCurrency: USDC_TEMPO,
742
+ amount: bridgeAmount.toString(),
743
+ tradeType: "EXACT_INPUT",
744
+ user: this.account.address,
745
+ recipient: this.account.address,
746
+ options: {
747
+ usePermit: true
748
+ }
749
+ });
750
+ let bridgeTxHash = null;
751
+ await getRelayClient().actions.execute({
752
+ quote,
753
+ wallet: adaptViemWallet(walletClient),
754
+ onProgress: ({ txHashes }) => {
755
+ if (txHashes?.length && !bridgeTxHash) {
756
+ bridgeTxHash = txHashes[0]?.txHash ?? null;
654
757
  }
655
758
  }
656
- const hasContentType = Object.keys(headers).some(
657
- (k) => k.toLowerCase() === "content-type"
759
+ });
760
+ return bridgeTxHash;
761
+ };
762
+ handlePayment = async (url, request, paymentRequirement, maxPay, onProgress) => {
763
+ if (!this.account) {
764
+ throw new Error(
765
+ "No wallet configured \u2014 run `zero init` or set ZERO_PRIVATE_KEY"
658
766
  );
659
- if (options.data && !hasContentType) {
660
- headers["content-type"] = "application/json";
661
- }
662
- const log = (msg) => console.error(` ${msg}`);
663
- const method = options.method ? options.method.toUpperCase() : options.data ? "POST" : "GET";
664
- const requestInit = {
665
- method,
666
- headers,
667
- body: options.data
668
- };
669
- const lastSearch = stateService.loadLastSearch();
670
- const matchedCapability = lastSearch?.capabilities.find(
671
- (c) => url.startsWith(c.url)
767
+ }
768
+ if (paymentRequirement.protocol === "x402") {
769
+ onProgress?.("Paying via x402 on Base...");
770
+ return this.payX402(url, request, paymentRequirement.raw, maxPay);
771
+ }
772
+ if (paymentRequirement.protocol === "mpp") {
773
+ onProgress?.("Paying via MPP on Tempo...");
774
+ return this.payMpp(
775
+ url,
776
+ request,
777
+ paymentRequirement.raw,
778
+ maxPay,
779
+ onProgress
672
780
  );
673
- const capabilityId = options.capability ?? matchedCapability?.id ?? null;
674
- const searchId = matchedCapability ? lastSearch?.searchId : void 0;
675
- const skipReasons = [];
676
- if (!apiService.walletAddress) {
677
- skipReasons.push(
678
- "no wallet configured (run `zero wallet import` / set ZERO_PRIVATE_KEY)"
679
- );
781
+ }
782
+ throw new Error("Unrecognized 402 payment protocol");
783
+ };
784
+ payX402 = async (url, request, _raw, maxPay) => {
785
+ if (!this.account) throw new Error("No wallet configured");
786
+ let capturedAmount = "0";
787
+ const client = new X402Client().register(
788
+ "eip155:*",
789
+ new ExactEvmScheme(this.account)
790
+ );
791
+ client.onBeforePaymentCreation(async (context) => {
792
+ const selected = context.selectedRequirements;
793
+ if (selected && (!selected.extra?.name || !selected.extra?.version)) {
794
+ const known = KNOWN_EIP712_DOMAINS[selected.asset?.toLowerCase() ?? ""];
795
+ if (known) {
796
+ selected.extra = { ...selected.extra, ...known };
797
+ }
680
798
  }
681
- if (!capabilityId) {
682
- skipReasons.push(
799
+ const requirement = context.paymentRequired.accepts[0];
800
+ if (!requirement) return;
801
+ capturedAmount = formatUnits(BigInt(requirement.amount), 6);
802
+ if (maxPay && Number.parseFloat(capturedAmount) > Number.parseFloat(maxPay)) {
803
+ return {
804
+ abort: true,
805
+ reason: `Payment of ${capturedAmount} USDC exceeds --max-pay ${maxPay}`
806
+ };
807
+ }
808
+ });
809
+ const httpClient = new x402HTTPClient(client).onPaymentRequired(
810
+ createSIWxClientHook(this.account)
811
+ );
812
+ const wrappedFetch = wrapFetchWithPayment(fetch, httpClient);
813
+ const response = await wrappedFetch(url, {
814
+ method: request.method,
815
+ headers: request.headers,
816
+ body: request.body
817
+ });
818
+ let txHash = null;
819
+ const paymentResponseHeader = response.headers.get("payment-response") ?? response.headers.get("x-payment-response");
820
+ if (paymentResponseHeader) {
821
+ try {
822
+ const settlement = decodePaymentResponseHeader(paymentResponseHeader);
823
+ txHash = settlement.transaction ?? null;
824
+ } catch {
825
+ }
826
+ }
827
+ return {
828
+ response,
829
+ protocol: "x402",
830
+ chain: "base",
831
+ txHash,
832
+ amount: capturedAmount,
833
+ asset: "USDC"
834
+ };
835
+ };
836
+ /**
837
+ * Shared pre-payment work: compute the Tempo amount we need, enforce
838
+ * --max-pay, and bridge from Base USDC if the Tempo balance is short.
839
+ * Invoked from mppx's `onChallenge` — i.e. AFTER the server's 402 and
840
+ * BEFORE mppx signs a credential — so balance/bridge logic runs in-band
841
+ * without adding a pre-probe round-trip.
842
+ */
843
+ prepareTempoFunds = async (challenge, maxPay, onProgress) => {
844
+ const challengeRequest = challenge.request;
845
+ let requiredRaw;
846
+ if (challenge.intent === "session") {
847
+ const suggestedDeposit = challengeRequest.suggestedDeposit;
848
+ requiredRaw = suggestedDeposit ? BigInt(suggestedDeposit) : BigInt(
849
+ Math.floor(
850
+ Number.parseFloat(maxPay ?? DEFAULT_MAX_DEPOSIT) * 1e6
851
+ )
852
+ );
853
+ } else {
854
+ requiredRaw = BigInt(challengeRequest.amount);
855
+ }
856
+ const capturedAmount = formatUnits(requiredRaw, 6);
857
+ if (maxPay && Number.parseFloat(capturedAmount) > Number.parseFloat(maxPay)) {
858
+ throw new Error(
859
+ `Payment of ${capturedAmount} USDC exceeds --max-pay ${maxPay}`
860
+ );
861
+ }
862
+ const methodDetails = challengeRequest.methodDetails;
863
+ const challengeChainId = challengeRequest.chainId ?? methodDetails?.chainId;
864
+ const isTestnet = challengeChainId === TEMPO_TESTNET_CHAIN_ID;
865
+ onProgress?.(`Checking Tempo balance...`);
866
+ const tempoBalance = await this.getBalanceRaw(
867
+ isTestnet ? "tempo-testnet" : "tempo"
868
+ );
869
+ if (tempoBalance < requiredRaw) {
870
+ if (isTestnet) {
871
+ throw new Error(
872
+ `Insufficient pathUSD on Tempo testnet: have ${formatUnits(tempoBalance, 6)}, need ${capturedAmount}. Fund your wallet with the Tempo testnet faucet: https://docs.tempo.xyz/quickstart/faucet`
873
+ );
874
+ }
875
+ await this.bridgeToTempo(requiredRaw, onProgress);
876
+ }
877
+ return capturedAmount;
878
+ };
879
+ /**
880
+ * MPP payment entrypoint. Runs a SINGLE `mppx.fetch` and uses mppx's
881
+ * `onChallenge` callback to run balance/bridge/max-pay logic in-band
882
+ * with the server's 402. After the 200 comes back, we branch on the
883
+ * captured challenge intent + whether the tempo method reported the
884
+ * channel as opened (only session lifecycle fires `onChannelUpdate`).
885
+ *
886
+ * Why no pre-probe: a pre-probe adds an extra unauthenticated POST
887
+ * before mppx's own 402 dance (2 → 3 server-side requests). Session-
888
+ * intent facilitators that re-run the LLM per request can't reconcile
889
+ * the extra state and return `410 "channel not found"` on the close.
890
+ * This mirrors the working production v0.0.21 single-fetch flow.
891
+ */
892
+ payMpp = async (url, request, _raw, maxPay, onProgress) => {
893
+ const account = this.account;
894
+ if (!account) throw new Error("No wallet configured");
895
+ let capturedAmount = "0";
896
+ let capturedChallenge;
897
+ let channelEntry;
898
+ const mppx = Mppx.create({
899
+ polyfill: false,
900
+ fetch: this.fetchImpl,
901
+ methods: [
902
+ tempo({
903
+ account,
904
+ maxDeposit: maxPay ?? DEFAULT_MAX_DEPOSIT,
905
+ onChannelUpdate: (entry) => {
906
+ channelEntry = {
907
+ channelId: entry.channelId,
908
+ escrowContract: entry.escrowContract,
909
+ cumulativeAmount: entry.cumulativeAmount,
910
+ opened: entry.opened
911
+ };
912
+ }
913
+ })
914
+ ],
915
+ onChallenge: async (challenge) => {
916
+ capturedChallenge = challenge;
917
+ capturedAmount = await this.prepareTempoFunds(
918
+ challenge,
919
+ maxPay,
920
+ onProgress
921
+ );
922
+ return void 0;
923
+ }
924
+ });
925
+ const response = await mppx.fetch(url, {
926
+ method: request.method,
927
+ headers: request.headers,
928
+ body: request.body
929
+ });
930
+ if (capturedChallenge?.intent === "session" && channelEntry?.opened) {
931
+ return this.completeMppSession({
932
+ url,
933
+ request,
934
+ challenge: capturedChallenge,
935
+ channelEntry,
936
+ response,
937
+ capturedAmount,
938
+ mppx
939
+ });
940
+ }
941
+ let txHash = null;
942
+ try {
943
+ const receipt = Receipt.fromResponse(response);
944
+ txHash = receipt.reference ?? null;
945
+ } catch {
946
+ }
947
+ return {
948
+ response,
949
+ protocol: "mpp",
950
+ chain: "tempo",
951
+ txHash,
952
+ amount: capturedAmount,
953
+ asset: "USDC"
954
+ };
955
+ };
956
+ /**
957
+ * Close an opened MPP session channel after the seller returns a 200.
958
+ *
959
+ * Decodes the server's `Payment-Receipt` to get the settled
960
+ * `acceptedCumulative`/`spent`, signs a close voucher at that amount
961
+ * (never below on-chain settled), and POSTs it back to the seller.
962
+ * Needed for dynamic-pricing sellers whose open-time `amount` is `0` —
963
+ * signing close with `cumulativeAmount=0` would be rejected with
964
+ * `voucher cumulativeAmount is below on-chain settled amount`.
965
+ *
966
+ * When the seller's facilitator rejects the close (e.g. strict matching
967
+ * of fresh challenge IDs per request), `SessionCloseFailedError` is
968
+ * thrown carrying the session metadata so the caller can record the run
969
+ * against the orphaned channel.
970
+ */
971
+ completeMppSession = async (params) => {
972
+ const {
973
+ url,
974
+ request,
975
+ challenge,
976
+ channelEntry,
977
+ response,
978
+ capturedAmount,
979
+ mppx
980
+ } = params;
981
+ const challengeRequest = challenge.request;
982
+ const recipient = challengeRequest.recipient;
983
+ const methodDetails = challengeRequest.methodDetails;
984
+ const challengeEscrow = methodDetails?.escrowContract;
985
+ const challengeChainId = challengeRequest.chainId ?? methodDetails?.chainId;
986
+ if (!recipient || !challengeEscrow) {
987
+ throw new Error("session challenge missing recipient/escrowContract");
988
+ }
989
+ if (!challengeChainId) {
990
+ throw new Error("session challenge missing chainId");
991
+ }
992
+ const {
993
+ channelId,
994
+ escrowContract,
995
+ cumulativeAmount: openTimeCumulative
996
+ } = channelEntry;
997
+ const receipt = readSessionReceipt(response);
998
+ const closeAmount = pickSessionCloseAmount(receipt, openTimeCumulative);
999
+ const orphanedSession = {
1000
+ channelId,
1001
+ escrowContract,
1002
+ chainId: challengeChainId,
1003
+ recipient,
1004
+ cumulativeAmount: closeAmount.toString()
1005
+ };
1006
+ const closeFailed = (message) => new SessionCloseFailedError({
1007
+ message,
1008
+ session: orphanedSession,
1009
+ response,
1010
+ capturedAmount
1011
+ });
1012
+ const closeChallengeResponse = new Response("", {
1013
+ status: 402,
1014
+ headers: { "WWW-Authenticate": Challenge.serialize(challenge) }
1015
+ });
1016
+ let closeCredential;
1017
+ try {
1018
+ closeCredential = await mppx.createCredential(closeChallengeResponse, {
1019
+ action: "close",
1020
+ channelId,
1021
+ cumulativeAmountRaw: closeAmount.toString()
1022
+ });
1023
+ } catch (err) {
1024
+ throw closeFailed(
1025
+ `Session close credential build failed for channel ${channelId}: ${err instanceof Error ? err.message : String(err)}`
1026
+ );
1027
+ }
1028
+ const closeMethod = request.method.toUpperCase();
1029
+ const methodCarriesBody = closeMethod === "POST" || closeMethod === "PUT" || closeMethod === "PATCH";
1030
+ const closeResponse = await this.fetchImpl(url, {
1031
+ method: closeMethod,
1032
+ headers: {
1033
+ ...request.headers,
1034
+ // biome-ignore lint/style/useNamingConvention: HTTP header name
1035
+ Authorization: closeCredential
1036
+ },
1037
+ ...methodCarriesBody && request.body ? { body: request.body } : {}
1038
+ });
1039
+ if (!closeResponse.ok) {
1040
+ const body = await closeResponse.text().catch(() => "");
1041
+ throw closeFailed(
1042
+ `Seller rejected close credential (status ${closeResponse.status}${body ? `: ${body.slice(0, 200)}` : ""}) for channel ${channelId}`
1043
+ );
1044
+ }
1045
+ const closeReceipt = readSessionReceipt(closeResponse);
1046
+ return {
1047
+ response,
1048
+ protocol: "mpp",
1049
+ chain: "tempo",
1050
+ txHash: null,
1051
+ amount: capturedAmount,
1052
+ asset: "USDC",
1053
+ session: {
1054
+ channelId,
1055
+ escrowContract,
1056
+ chainId: challengeChainId,
1057
+ recipient,
1058
+ cumulativeAmount: closeReceipt?.acceptedCumulative ?? closeAmount.toString()
1059
+ }
1060
+ };
1061
+ };
1062
+ resolveChainConfig = (chain) => {
1063
+ switch (chain) {
1064
+ case "base":
1065
+ return { viemChain: base, token: USDC_BASE };
1066
+ case "base-sepolia":
1067
+ return { viemChain: baseSepolia, token: USDC_BASE_SEPOLIA };
1068
+ case "tempo":
1069
+ return { viemChain: tempoChain, token: USDC_TEMPO };
1070
+ case "tempo-testnet":
1071
+ return { viemChain: tempoTestnetChain, token: PATHUSD_TEMPO };
1072
+ }
1073
+ };
1074
+ getBalanceRaw = async (chain) => {
1075
+ if (!this.account) return 0n;
1076
+ const { viemChain, token } = this.resolveChainConfig(chain);
1077
+ const client = createPublicClient({
1078
+ chain: viemChain,
1079
+ transport: http()
1080
+ });
1081
+ const balance = await client.readContract({
1082
+ address: token,
1083
+ abi: ERC20_BALANCE_ABI,
1084
+ functionName: "balanceOf",
1085
+ args: [this.account.address]
1086
+ });
1087
+ return balance;
1088
+ };
1089
+ getBalance = async (chain) => {
1090
+ const raw = await this.getBalanceRaw(chain);
1091
+ return { amount: formatUnits(raw, 6), asset: "USDC" };
1092
+ };
1093
+ /**
1094
+ * Total spendable USDC across Base and Tempo. Users (and their agents)
1095
+ * shouldn't need to know which chain holds funds — the CLI bridges to
1096
+ * Tempo on demand, so the reported balance sums both sides.
1097
+ */
1098
+ getTotalBalance = async () => {
1099
+ const [baseRaw, tempoRaw] = await Promise.all([
1100
+ this.getBalanceRaw("base"),
1101
+ this.getBalanceRaw("tempo")
1102
+ ]);
1103
+ return { amount: formatUnits(baseRaw + tempoRaw, 6), asset: "USDC" };
1104
+ };
1105
+ };
1106
+
1107
+ // src/util/infer-schema.ts
1108
+ var inferSchema = (value, depth = 0) => {
1109
+ if (depth > 6) return { type: typeOf(value) };
1110
+ if (value === null) return { type: "null" };
1111
+ if (Array.isArray(value)) {
1112
+ const itemSchemas = value.slice(0, 3).map((v) => inferSchema(v, depth + 1));
1113
+ return {
1114
+ type: "array",
1115
+ items: itemSchemas[0] ?? { type: "unknown" }
1116
+ };
1117
+ }
1118
+ if (typeof value === "object") {
1119
+ const obj = value;
1120
+ const properties = {};
1121
+ const required = [];
1122
+ for (const [k, v] of Object.entries(obj)) {
1123
+ properties[k] = inferSchema(v, depth + 1);
1124
+ required.push(k);
1125
+ }
1126
+ return { type: "object", properties, required };
1127
+ }
1128
+ return { type: typeOf(value) };
1129
+ };
1130
+ var typeOf = (v) => {
1131
+ if (v === null) return "null";
1132
+ if (Array.isArray(v)) return "array";
1133
+ return typeof v;
1134
+ };
1135
+ var tryParseJson = (text) => {
1136
+ try {
1137
+ return JSON.parse(text);
1138
+ } catch {
1139
+ return null;
1140
+ }
1141
+ };
1142
+
1143
+ // src/util/redact.ts
1144
+ var ERROR_MAX = 500;
1145
+ var QUERY_MAX = 200;
1146
+ var redactUrl = (raw) => {
1147
+ try {
1148
+ const parsed = new URL(raw);
1149
+ return `${parsed.origin}${parsed.pathname}`;
1150
+ } catch {
1151
+ return raw;
1152
+ }
1153
+ };
1154
+ var truncateQuery = (raw) => raw.length > QUERY_MAX ? `${raw.slice(0, QUERY_MAX)}\u2026` : raw;
1155
+ var truncateError = (raw) => raw.length > ERROR_MAX ? `${raw.slice(0, ERROR_MAX)}\u2026` : raw;
1156
+
1157
+ // src/commands/fetch-command.ts
1158
+ var isTextContentType = (contentType) => {
1159
+ if (!contentType) return true;
1160
+ const ct = contentType.toLowerCase().split(";")[0]?.trim() ?? "";
1161
+ if (ct.startsWith("text/")) return true;
1162
+ if (ct === "application/json" || ct.endsWith("+json")) return true;
1163
+ if (ct === "application/xml" || ct.endsWith("+xml")) return true;
1164
+ if (ct === "application/javascript" || ct === "application/ecmascript") {
1165
+ return true;
1166
+ }
1167
+ if (ct === "application/x-www-form-urlencoded") return true;
1168
+ return false;
1169
+ };
1170
+ var detectPaymentRequirement = (headers, status) => {
1171
+ if (status !== 402) return null;
1172
+ const x402Header = headers.get("payment-required") ?? headers.get("x-payment-required");
1173
+ if (x402Header) {
1174
+ try {
1175
+ const decoded = JSON.parse(
1176
+ Buffer.from(x402Header, "base64").toString("utf8")
1177
+ );
1178
+ return { protocol: "x402", raw: decoded };
1179
+ } catch {
1180
+ return { protocol: "x402", raw: { encoded: x402Header } };
1181
+ }
1182
+ }
1183
+ const wwwAuth = headers.get("www-authenticate");
1184
+ if (wwwAuth?.toLowerCase().includes("payment")) {
1185
+ return { protocol: "mpp", raw: { "www-authenticate": wwwAuth } };
1186
+ }
1187
+ return { protocol: "unknown", raw: {} };
1188
+ };
1189
+ var fetchCommand = (appContext) => new Command3("fetch").description("Fetch a capability URL with automatic payment handling").argument("<url>", "URL to fetch").option(
1190
+ "-X, --method <method>",
1191
+ "HTTP method (GET, POST, PUT, PATCH, DELETE). Defaults to POST when -d is set, otherwise GET"
1192
+ ).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(
1193
+ "--capability <id>",
1194
+ "Bind this fetch to a capability (uid or slug) so a reviewable run is recorded even without a prior `zero search`"
1195
+ ).option(
1196
+ "--json",
1197
+ "Emit {runId, status, latencyMs, payment, body} as JSON on stdout (for batch/non-TTY use)"
1198
+ ).option(
1199
+ "--agent <name>",
1200
+ "Identify your agent host for this invocation (e.g. claude-web, codex). Overrides auto-detect for this call only."
1201
+ ).action(
1202
+ async (url, options) => {
1203
+ try {
1204
+ const {
1205
+ analyticsService,
1206
+ apiService,
1207
+ paymentService,
1208
+ stateService,
1209
+ walletService
1210
+ } = appContext.services;
1211
+ const startTime = Date.now();
1212
+ const headers = {};
1213
+ if (options.header) {
1214
+ for (const h of options.header) {
1215
+ const colonIdx = h.indexOf(":");
1216
+ if (colonIdx > 0) {
1217
+ headers[h.slice(0, colonIdx).trim()] = h.slice(colonIdx + 1).trim();
1218
+ }
1219
+ }
1220
+ }
1221
+ const hasContentType = Object.keys(headers).some(
1222
+ (k) => k.toLowerCase() === "content-type"
1223
+ );
1224
+ if (options.data && !hasContentType) {
1225
+ headers["content-type"] = "application/json";
1226
+ }
1227
+ const log = (msg) => console.error(` ${msg}`);
1228
+ const method = options.method ? options.method.toUpperCase() : options.data ? "POST" : "GET";
1229
+ const requestInit = {
1230
+ method,
1231
+ headers,
1232
+ body: options.data
1233
+ };
1234
+ const lastSearch = stateService.loadLastSearch();
1235
+ const matchedCapability = lastSearch?.capabilities.find(
1236
+ (c) => url.startsWith(c.url)
1237
+ );
1238
+ const capabilityId = options.capability ?? matchedCapability?.id ?? null;
1239
+ const searchId = matchedCapability ? lastSearch?.searchId : void 0;
1240
+ const skipReasons = [];
1241
+ if (!apiService.walletAddress) {
1242
+ skipReasons.push(
1243
+ "no wallet configured (run `zero wallet import` / set ZERO_PRIVATE_KEY)"
1244
+ );
1245
+ }
1246
+ if (!capabilityId) {
1247
+ skipReasons.push(
683
1248
  "no capability resolved \u2014 pass --capability <uid|slug> or run `zero search` first so the URL can be matched"
684
1249
  );
685
1250
  }
686
1251
  let finalResponse;
1252
+ let bodyBytes;
687
1253
  let body = "";
1254
+ let bodyIsBinary = false;
688
1255
  let paymentMeta;
1256
+ let sessionMeta;
689
1257
  let fetchError;
690
1258
  try {
691
1259
  log(`Calling ${url}...`);
@@ -711,30 +1279,73 @@ var fetchCommand = (appContext) => new Command3("fetch").description("Fetch a ca
711
1279
  chain: result.chain,
712
1280
  txHash: result.txHash,
713
1281
  amount: result.amount,
714
- asset: result.asset
1282
+ asset: result.asset,
1283
+ ...result.session && { session: result.session }
715
1284
  };
716
1285
  log(
717
1286
  `Paid ${result.amount} ${result.asset} via ${result.protocol} on ${result.chain}`
718
1287
  );
1288
+ if (result.session) {
1289
+ sessionMeta = result.session;
1290
+ log(
1291
+ `MPP session closed \u2014 channel ${result.session.channelId.slice(0, 10)}... \u2192 ${result.session.recipient.slice(0, 8)}... (${formatUnits2(BigInt(result.session.cumulativeAmount), 6)} USDC settled)`
1292
+ );
1293
+ }
719
1294
  } else {
720
1295
  finalResponse = response;
721
1296
  }
722
- body = await finalResponse.text();
1297
+ const buf = Buffer.from(await finalResponse.arrayBuffer());
1298
+ bodyBytes = buf;
1299
+ bodyIsBinary = !isTextContentType(
1300
+ finalResponse.headers.get("content-type")
1301
+ );
1302
+ body = bodyIsBinary ? "" : buf.toString("utf8");
723
1303
  } catch (err) {
724
- fetchError = err instanceof Error ? err : new Error(String(err));
1304
+ if (err instanceof SessionCloseFailedError) {
1305
+ finalResponse = err.response;
1306
+ try {
1307
+ const buf = Buffer.from(await err.response.arrayBuffer());
1308
+ bodyBytes = buf;
1309
+ bodyIsBinary = !isTextContentType(
1310
+ err.response.headers.get("content-type")
1311
+ );
1312
+ body = bodyIsBinary ? "" : buf.toString("utf8");
1313
+ } catch {
1314
+ body = "";
1315
+ }
1316
+ paymentMeta = {
1317
+ protocol: "mpp",
1318
+ chain: "tempo",
1319
+ txHash: null,
1320
+ amount: err.capturedAmount,
1321
+ asset: "USDC",
1322
+ session: err.session
1323
+ };
1324
+ sessionMeta = err.session;
1325
+ fetchError = err;
1326
+ console.error(
1327
+ [
1328
+ "",
1329
+ " WARNING: failed to auto-close MPP session.",
1330
+ ` ${err.message}`,
1331
+ ` Channel ${err.session.channelId} is still open on chain \u2014 see payment.session in --json output.`,
1332
+ " Contact the seller to settle the channel from their facilitator, or use",
1333
+ " your wallet to call escrow.close/requestClose directly.",
1334
+ ""
1335
+ ].join("\n")
1336
+ );
1337
+ } else {
1338
+ fetchError = err instanceof Error ? err : new Error(String(err));
1339
+ }
725
1340
  }
726
1341
  const latencyMs = Date.now() - startTime;
727
1342
  if (finalResponse && !options.json) {
728
- console.log(body);
1343
+ if (bodyIsBinary && bodyBytes) {
1344
+ process.stdout.write(bodyBytes);
1345
+ } else {
1346
+ console.log(body);
1347
+ }
729
1348
  }
730
- analyticsService.capture("fetch_executed", {
731
- url,
732
- status: finalResponse?.status,
733
- hasPayment: !!paymentMeta,
734
- paymentProtocol: paymentMeta?.protocol,
735
- paymentAmount: paymentMeta?.amount,
736
- ...fetchError && { error: fetchError.message }
737
- });
738
1349
  if (paymentMeta) {
739
1350
  try {
740
1351
  const balance = await walletService.getBalance();
@@ -747,6 +1358,11 @@ var fetchCommand = (appContext) => new Command3("fetch").description("Fetch a ca
747
1358
  Warning: Balance is $${balance.amount} \u2014 run \`zero wallet fund\` soon.
748
1359
  `
749
1360
  );
1361
+ analyticsService.capture("low_balance_warning_shown", {
1362
+ balance: balance.amount,
1363
+ threshold,
1364
+ paymentProtocol: paymentMeta.protocol
1365
+ });
750
1366
  }
751
1367
  }
752
1368
  } catch {
@@ -782,7 +1398,7 @@ var fetchCommand = (appContext) => new Command3("fetch").description("Fetch a ca
782
1398
  paymentProtocol: paymentMeta.protocol,
783
1399
  paymentChain: paymentMeta.chain,
784
1400
  paymentTxHash: paymentMeta.txHash ?? void 0,
785
- paymentMode: "charge"
1401
+ paymentMode: sessionMeta ? "session" : "charge"
786
1402
  }
787
1403
  });
788
1404
  runId = runResult.runId;
@@ -792,17 +1408,35 @@ var fetchCommand = (appContext) => new Command3("fetch").description("Fetch a ca
792
1408
  );
793
1409
  }
794
1410
  }
1411
+ const status = finalResponse?.status;
1412
+ const outcome = !finalResponse ? "network_error" : status === 402 && !paymentMeta ? "payment_failed" : status !== void 0 && status >= 400 && status !== 402 ? "server_error" : "success";
1413
+ analyticsService.capture("fetch_executed", {
1414
+ url: redactUrl(url),
1415
+ status,
1416
+ outcome,
1417
+ latencyMs,
1418
+ hasPayment: !!paymentMeta,
1419
+ paymentProtocol: paymentMeta?.protocol,
1420
+ paymentAmount: paymentMeta?.amount,
1421
+ capabilityId: capabilityId ?? void 0,
1422
+ searchId: searchId ?? void 0,
1423
+ runId: runId ?? void 0,
1424
+ runTracked: !!runId,
1425
+ ...fetchError && { error: truncateError(fetchError.message) }
1426
+ });
795
1427
  if (fetchError && !options.json) {
796
1428
  console.error(` Fetch failed: ${fetchError.message}`);
797
1429
  }
798
1430
  if (options.json) {
1431
+ const jsonBody = !finalResponse ? null : bodyIsBinary ? (bodyBytes ?? Buffer.alloc(0)).toString("base64") : body;
799
1432
  console.log(
800
1433
  JSON.stringify({
801
1434
  runId,
802
1435
  status: finalResponse?.status ?? null,
803
1436
  latencyMs,
804
1437
  payment: paymentMeta ?? null,
805
- body: finalResponse ? body : null,
1438
+ body: jsonBody,
1439
+ ...bodyIsBinary && { bodyEncoding: "base64" },
806
1440
  ...fetchError && { error: fetchError.message },
807
1441
  ...skipReasons.length > 0 && {
808
1442
  runTrackingSkipped: skipReasons
@@ -907,7 +1541,10 @@ var getCommand = (appContext) => new Command4("get").description(
907
1541
  ).argument(
908
1542
  "<identifier>",
909
1543
  "Position number from search results, or a capability slug"
910
- ).option("--formatted", "Output formatted trust breakdown").action(async (identifier, options) => {
1544
+ ).option("--formatted", "Output formatted trust breakdown").option(
1545
+ "--agent <name>",
1546
+ "Identify your agent host for this invocation. Overrides auto-detect for this call only."
1547
+ ).action(async (identifier, options) => {
911
1548
  try {
912
1549
  const { analyticsService, apiService, stateService } = appContext.services;
913
1550
  const position = Number.parseInt(identifier, 10);
@@ -947,7 +1584,9 @@ var getCommand = (appContext) => new Command4("get").description(
947
1584
  }
948
1585
  analyticsService.capture("capability_viewed", {
949
1586
  capabilityId,
950
- ...isPosition ? { position } : {}
1587
+ fromLastSearch: isPosition,
1588
+ ...isPosition ? { position } : {},
1589
+ ...searchId ? { searchId } : {}
951
1590
  });
952
1591
  } catch (err) {
953
1592
  console.error(err instanceof Error ? err.message : "Get failed");
@@ -1171,107 +1810,126 @@ var installSkills = (home) => {
1171
1810
  return installed;
1172
1811
  };
1173
1812
  var initCommand = (appContext) => new Command5("init").description("Initialize Zero CLI for usage").option("--force", "Overwrite existing configuration").action(async (options) => {
1174
- const home = homedir2();
1175
- const zeroDir = join2(home, ".zero");
1176
- const configPath = join2(zeroDir, "config.json");
1177
- let walletCreated = false;
1178
- let walletAddress = null;
1179
- const walletExists = (() => {
1180
- if (!existsSync2(configPath)) return false;
1181
- try {
1182
- const existing = JSON.parse(readFileSync3(configPath, "utf8"));
1183
- return !!existing.privateKey;
1184
- } catch {
1185
- return false;
1186
- }
1187
- })();
1188
- if (!walletExists || options.force) {
1189
- const privateKey = generatePrivateKey();
1190
- const account = privateKeyToAccount(privateKey);
1191
- mkdirSync2(zeroDir, { recursive: true });
1192
- const existing = existsSync2(configPath) ? JSON.parse(readFileSync3(configPath, "utf8")) : {};
1193
- writeFileSync2(
1194
- configPath,
1195
- JSON.stringify(
1196
- { ...existing, privateKey, lowBalanceWarning: 1 },
1197
- null,
1198
- 2
1199
- )
1200
- );
1201
- walletCreated = true;
1202
- walletAddress = account.address;
1203
- console.log(`Wallet address: ${account.address}`);
1204
- } else {
1205
- try {
1206
- const existing = JSON.parse(readFileSync3(configPath, "utf8"));
1207
- const account = privateKeyToAccount(existing.privateKey);
1813
+ appContext.services.analyticsService.capture("init_started", {
1814
+ force: options.force ?? false
1815
+ });
1816
+ let currentStep = "wallet";
1817
+ try {
1818
+ const home = homedir2();
1819
+ const zeroDir = join2(home, ".zero");
1820
+ const configPath = join2(zeroDir, "config.json");
1821
+ let walletCreated = false;
1822
+ let walletAddress = null;
1823
+ const walletExists = (() => {
1824
+ if (!existsSync2(configPath)) return false;
1825
+ try {
1826
+ const existing = JSON.parse(readFileSync3(configPath, "utf8"));
1827
+ return !!existing.privateKey;
1828
+ } catch {
1829
+ return false;
1830
+ }
1831
+ })();
1832
+ if (!walletExists || options.force) {
1833
+ const privateKey = generatePrivateKey();
1834
+ const account = privateKeyToAccount(privateKey);
1835
+ mkdirSync2(zeroDir, { recursive: true });
1836
+ const existing = existsSync2(configPath) ? JSON.parse(readFileSync3(configPath, "utf8")) : {};
1837
+ writeFileSync2(
1838
+ configPath,
1839
+ JSON.stringify(
1840
+ { ...existing, privateKey, lowBalanceWarning: 1 },
1841
+ null,
1842
+ 2
1843
+ )
1844
+ );
1845
+ walletCreated = true;
1208
1846
  walletAddress = account.address;
1209
- } catch {
1847
+ console.log(`Wallet address: ${account.address}`);
1848
+ } else {
1849
+ try {
1850
+ const existing = JSON.parse(readFileSync3(configPath, "utf8"));
1851
+ const account = privateKeyToAccount(existing.privateKey);
1852
+ walletAddress = account.address;
1853
+ } catch {
1854
+ }
1210
1855
  }
1211
- }
1212
- const agentsDetected = [];
1213
- const agentsWithSkills = [];
1214
- let skillsError = null;
1215
- let hookInstalled = false;
1216
- let hookError = null;
1217
- for (const tool of AGENT_TOOLS) {
1218
- if (existsSync2(join2(home, tool.configDir))) {
1219
- agentsDetected.push(tool.name);
1856
+ const agentsDetected = [];
1857
+ const agentsWithSkills = [];
1858
+ let skillsError = null;
1859
+ let hookInstalled = false;
1860
+ let hookError = null;
1861
+ for (const tool of AGENT_TOOLS) {
1862
+ if (existsSync2(join2(home, tool.configDir))) {
1863
+ agentsDetected.push(tool.name);
1864
+ }
1220
1865
  }
1221
- }
1222
- try {
1223
- const installed = installSkills(home);
1224
- for (const entry of installed) {
1225
- const toolName = entry.split(":")[0];
1226
- if (toolName && !agentsWithSkills.includes(toolName)) {
1227
- agentsWithSkills.push(toolName);
1866
+ currentStep = "skills";
1867
+ try {
1868
+ const installed = installSkills(home);
1869
+ for (const entry of installed) {
1870
+ const toolName = entry.split(":")[0];
1871
+ if (toolName && !agentsWithSkills.includes(toolName)) {
1872
+ agentsWithSkills.push(toolName);
1873
+ }
1228
1874
  }
1875
+ } catch (err) {
1876
+ skillsError = err instanceof Error ? err.message : "unknown skills error";
1229
1877
  }
1230
- } catch (err) {
1231
- skillsError = err instanceof Error ? err.message : "unknown skills error";
1232
- }
1233
- try {
1234
- hookInstalled = installHook(home);
1235
- } catch (err) {
1236
- hookError = err instanceof Error ? err.message : "unknown hook error";
1237
- }
1238
- const conflictingSkills = findConflictingSkills(home);
1239
- if (conflictingSkills.length > 0) {
1240
- const skillList = conflictingSkills.map((s) => ` - ${s.tool}: ${s.skillName}`).join("\n");
1241
- console.error(
1242
- `
1878
+ currentStep = "hook";
1879
+ try {
1880
+ hookInstalled = installHook(home);
1881
+ } catch (err) {
1882
+ hookError = err instanceof Error ? err.message : "unknown hook error";
1883
+ }
1884
+ currentStep = "cleanup_scan";
1885
+ const conflictingSkills = findConflictingSkills(home);
1886
+ if (conflictingSkills.length > 0) {
1887
+ const skillList = conflictingSkills.map((s) => ` - ${s.tool}: ${s.skillName}`).join("\n");
1888
+ console.error(
1889
+ `
1243
1890
  Found deprecated skills that may conflict with Zero:
1244
1891
  ${skillList}
1245
1892
 
1246
1893
  To remove them, run: zero init cleanup`
1894
+ );
1895
+ }
1896
+ console.error(
1897
+ '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"'
1247
1898
  );
1899
+ currentStep = "complete";
1900
+ appContext.services.analyticsService.capture("wallet_initialized", {
1901
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
1902
+ wallet_created: walletCreated,
1903
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
1904
+ wallet_address: walletAddress,
1905
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
1906
+ agents_detected: agentsDetected,
1907
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
1908
+ agents_detected_count: agentsDetected.length,
1909
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
1910
+ skills_installed: agentsWithSkills.length > 0,
1911
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
1912
+ skills_installed_for: agentsWithSkills,
1913
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
1914
+ skills_error: skillsError,
1915
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
1916
+ hook_installed: hookInstalled,
1917
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
1918
+ hook_error: hookError,
1919
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
1920
+ conflicting_skills_found: conflictingSkills.length,
1921
+ force: options.force ?? false
1922
+ });
1923
+ } catch (err) {
1924
+ appContext.services.analyticsService.capture("init_failed", {
1925
+ step: currentStep,
1926
+ error: truncateError(
1927
+ err instanceof Error ? err.message : String(err)
1928
+ ),
1929
+ force: options.force ?? false
1930
+ });
1931
+ throw err;
1248
1932
  }
1249
- console.error(
1250
- 'Zero is ready! Run `zero search` to find capabilities.\n\nTry:\n zero search "translate text to Spanish"\n zero search "generate an image"\n zero search "weather forecast"'
1251
- );
1252
- appContext.services.analyticsService.capture("wallet_initialized", {
1253
- // biome-ignore lint/style/useNamingConvention: snake_case for analytics
1254
- wallet_created: walletCreated,
1255
- // biome-ignore lint/style/useNamingConvention: snake_case for analytics
1256
- wallet_address: walletAddress,
1257
- // biome-ignore lint/style/useNamingConvention: snake_case for analytics
1258
- agents_detected: agentsDetected,
1259
- // biome-ignore lint/style/useNamingConvention: snake_case for analytics
1260
- agents_detected_count: agentsDetected.length,
1261
- // biome-ignore lint/style/useNamingConvention: snake_case for analytics
1262
- skills_installed: agentsWithSkills.length > 0,
1263
- // biome-ignore lint/style/useNamingConvention: snake_case for analytics
1264
- skills_installed_for: agentsWithSkills,
1265
- // biome-ignore lint/style/useNamingConvention: snake_case for analytics
1266
- skills_error: skillsError,
1267
- // biome-ignore lint/style/useNamingConvention: snake_case for analytics
1268
- hook_installed: hookInstalled,
1269
- // biome-ignore lint/style/useNamingConvention: snake_case for analytics
1270
- hook_error: hookError,
1271
- // biome-ignore lint/style/useNamingConvention: snake_case for analytics
1272
- conflicting_skills_found: conflictingSkills.length,
1273
- force: options.force ?? false
1274
- });
1275
1933
  }).addCommand(
1276
1934
  new Command5("cleanup").description(
1277
1935
  "Remove deprecated skills (zam, tempo) that conflict with Zero"
@@ -1285,7 +1943,7 @@ To remove them, run: zero init cleanup`
1285
1943
  const removedList = removed.map((s) => ` - ${s}`).join("\n");
1286
1944
  console.error(`Removed deprecated skills:
1287
1945
  ${removedList}`);
1288
- appContext.services.analyticsService.capture("skills_cleanup", {
1946
+ appContext.services.analyticsService.capture("skills_cleaned_up", {
1289
1947
  // biome-ignore lint/style/useNamingConvention: snake_case for analytics
1290
1948
  skills_removed: removed,
1291
1949
  // biome-ignore lint/style/useNamingConvention: snake_case for analytics
@@ -1329,6 +1987,9 @@ Examples:
1329
1987
  ).option("--success", "The capability succeeded").option("--no-success", "The capability failed").option("--accuracy <n>", "Accuracy rating (1-5)", Number.parseInt).option("--value <n>", "Value rating (1-5)", Number.parseInt).option("--reliability <n>", "Reliability rating (1-5)", Number.parseInt).option("--content <text>", "Optional review text").option(
1330
1988
  "--from-file <path>",
1331
1989
  "Submit reviews in bulk from a JSONL file (one review object per line: {runId, success, accuracy, value, reliability, content?})"
1990
+ ).option(
1991
+ "--agent <name>",
1992
+ "Identify your agent host for this invocation. Overrides auto-detect for this call only."
1332
1993
  ).action(
1333
1994
  async (runId, options) => {
1334
1995
  try {
@@ -1350,6 +2011,10 @@ Examples:
1350
2011
  analyticsService.capture("review_submitted", {
1351
2012
  runId: parsed.runId,
1352
2013
  success: parsed.success,
2014
+ accuracy: parsed.accuracy,
2015
+ value: parsed.value,
2016
+ reliability: parsed.reliability,
2017
+ hasContent: !!parsed.content,
1353
2018
  bulk: true
1354
2019
  });
1355
2020
  } catch (err) {
@@ -1432,7 +2097,12 @@ Bulk review complete: ${ok} ok, ${failed} failed`);
1432
2097
  console.log(`Review submitted: ${result.reviewId}`);
1433
2098
  analyticsService.capture("review_submitted", {
1434
2099
  runId,
1435
- success: options.success
2100
+ success: options.success,
2101
+ accuracy,
2102
+ value,
2103
+ reliability,
2104
+ hasContent: !!options.content,
2105
+ resolvedByCapability: !!options.capability
1436
2106
  });
1437
2107
  } catch (err) {
1438
2108
  console.error(err instanceof Error ? err.message : "Review failed");
@@ -1457,6 +2127,9 @@ Examples:
1457
2127
  "--limit <n>",
1458
2128
  "Max rows (1-100, default 25)",
1459
2129
  (v) => Number.parseInt(v, 10)
2130
+ ).option(
2131
+ "--agent <name>",
2132
+ "Identify your agent host for this invocation. Overrides auto-detect for this call only."
1460
2133
  ).action(
1461
2134
  async (options) => {
1462
2135
  try {
@@ -1538,6 +2211,9 @@ var searchCommand = (appContext) => new Command8("search").description("Search f
1538
2211
  ).option(
1539
2212
  "--exclude-source <source>",
1540
2213
  "Exclude results from this crawl source"
2214
+ ).option(
2215
+ "--agent <name>",
2216
+ "Identify your agent host for this invocation (e.g. claude-web, codex). Overrides auto-detect for this call only."
1541
2217
  ).action(
1542
2218
  async (query, options) => {
1543
2219
  try {
@@ -1572,8 +2248,24 @@ var searchCommand = (appContext) => new Command8("search").description("Search f
1572
2248
  excludeSource: options.excludeSource
1573
2249
  });
1574
2250
  analyticsService.capture("search_executed", {
1575
- query,
1576
- resultCount: result.capabilities.length
2251
+ query: truncateQuery(query),
2252
+ queryLength: query.length,
2253
+ resultCount: result.capabilities.length,
2254
+ searchId: result.searchId,
2255
+ total: result.total,
2256
+ hasMore: result.hasMore,
2257
+ offset: options.offset,
2258
+ limit: options.limit,
2259
+ freeOnly: options.free ?? false,
2260
+ maxCost: options.maxCost,
2261
+ minRating: options.minRating,
2262
+ protocol: options.protocol,
2263
+ minTrust: options.minTrust,
2264
+ availabilityStatus: options.status,
2265
+ includeAll: options.all ?? false,
2266
+ source: options.source,
2267
+ excludeSource: options.excludeSource,
2268
+ json: options.json ?? false
1577
2269
  });
1578
2270
  if (options.json) {
1579
2271
  console.log(JSON.stringify(result, null, 2));
@@ -1606,14 +2298,34 @@ var searchCommand = (appContext) => new Command8("search").description("Search f
1606
2298
  }
1607
2299
  );
1608
2300
 
2301
+ // src/commands/terms-command.ts
2302
+ import { Command as Command9 } from "commander";
2303
+ var TERMS_URL = "https://zero.xyz/terms-of-service";
2304
+ var termsCommand = (_appContext) => new Command9("terms").description("View the ZeroClick Terms of Service").action(async () => {
2305
+ console.log(
2306
+ `ZeroClick Agentic Capability Search \u2014 Terms of Service
2307
+
2308
+ By using Zero, you agree to our Terms of Service.
2309
+
2310
+ Read the full terms at: ${TERMS_URL}
2311
+ `
2312
+ );
2313
+ try {
2314
+ const { exec } = await import("child_process");
2315
+ const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
2316
+ exec(`${openCmd} ${TERMS_URL}`);
2317
+ } catch {
2318
+ }
2319
+ });
2320
+
1609
2321
  // src/commands/wallet-command.ts
1610
2322
  import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
1611
2323
  import { homedir as homedir3 } from "os";
1612
2324
  import { join as join3 } from "path";
1613
- import { Command as Command9 } from "commander";
2325
+ import { Command as Command10 } from "commander";
1614
2326
  import open from "open";
1615
2327
  import { privateKeyToAccount as privateKeyToAccount2 } from "viem/accounts";
1616
- var walletBalanceCommand = (appContext) => new Command9("balance").description("Show wallet balance").action(async () => {
2328
+ var walletBalanceCommand = (appContext) => new Command10("balance").description("Show wallet balance").action(async () => {
1617
2329
  const { walletService } = appContext.services;
1618
2330
  const balance = await walletService.getBalance();
1619
2331
  if (balance === null) {
@@ -1628,7 +2340,11 @@ var walletBalanceCommand = (appContext) => new Command9("balance").description("
1628
2340
  }
1629
2341
  console.log(`${balance.amount} ${balance.asset}`);
1630
2342
  });
1631
- var walletFundCommand = (appContext) => new Command9("fund").description("Fund your wallet").argument("[amount]", "Amount to fund in USDC").option("--manual", "Show wallet address for manual transfer").action(
2343
+ 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(
2344
+ "--use <provider>",
2345
+ "Onramp provider: coinbase or stripe",
2346
+ "coinbase"
2347
+ ).action(
1632
2348
  async (amount, options) => {
1633
2349
  const { analyticsService, walletService } = appContext.services;
1634
2350
  const address = walletService.getAddress();
@@ -1646,7 +2362,11 @@ ${address}`);
1646
2362
  });
1647
2363
  return;
1648
2364
  }
1649
- const url = await appContext.services.apiService.getFundingUrl(amount);
2365
+ const provider = options.use === "stripe" ? "stripe" : "coinbase";
2366
+ const url = await appContext.services.apiService.getFundingUrl(
2367
+ amount,
2368
+ provider
2369
+ );
1650
2370
  if (url) {
1651
2371
  await open(url);
1652
2372
  console.log("Opened funding page in your browser.");
@@ -1665,7 +2385,7 @@ ${address}`);
1665
2385
  }
1666
2386
  }
1667
2387
  );
1668
- var walletAddressCommand = (appContext) => new Command9("address").description("Show wallet address").action(() => {
2388
+ var walletAddressCommand = (appContext) => new Command10("address").description("Show wallet address").action(() => {
1669
2389
  const { walletService } = appContext.services;
1670
2390
  const address = walletService.getAddress();
1671
2391
  if (!address) {
@@ -1675,7 +2395,7 @@ var walletAddressCommand = (appContext) => new Command9("address").description("
1675
2395
  }
1676
2396
  console.log(address);
1677
2397
  });
1678
- var walletSetCommand = (appContext) => new Command9("set").description("Set wallet from an existing private key").argument("<privateKey>", "Hex-encoded private key (0x-prefixed)").option("--force", "Overwrite existing wallet without prompting").action(async (privateKey, options) => {
2398
+ var walletSetCommand = (appContext) => new Command10("set").description("Set wallet from an existing private key").argument("<privateKey>", "Hex-encoded private key (0x-prefixed)").option("--force", "Overwrite existing wallet without prompting").action(async (privateKey, options) => {
1679
2399
  const { analyticsService } = appContext.services;
1680
2400
  if (!privateKey.startsWith("0x")) {
1681
2401
  console.error("Private key must be 0x-prefixed hex string.");
@@ -1727,7 +2447,7 @@ var walletSetCommand = (appContext) => new Command9("set").description("Set wall
1727
2447
  });
1728
2448
  });
1729
2449
  var walletCommand = (appContext) => {
1730
- const cmd = new Command9("wallet").description("Manage your wallet");
2450
+ const cmd = new Command10("wallet").description("Manage your wallet");
1731
2451
  cmd.addCommand(walletBalanceCommand(appContext));
1732
2452
  cmd.addCommand(walletFundCommand(appContext));
1733
2453
  cmd.addCommand(walletAddressCommand(appContext));
@@ -1738,436 +2458,191 @@ var walletCommand = (appContext) => {
1738
2458
  // src/app.ts
1739
2459
  var createApp = (appContext) => {
1740
2460
  const { analyticsService } = appContext.services;
1741
- const program = new Command10().name("zero").description("Zero CLI \u2014 Search engine and payment platform for AI agents").version(package_default.version, "-v, --version").exitOverride().hook("preAction", (_thisCommand, actionCommand) => {
2461
+ const program = new Command11().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) => {
2462
+ const agentFlag = actionCommand.opts().agent;
2463
+ if (typeof agentFlag === "string" && agentFlag.trim().length > 0) {
2464
+ analyticsService.setAgentHost(agentFlag.trim());
2465
+ }
2466
+ appContext.invocation.current = {
2467
+ command: actionCommand.name(),
2468
+ startMs: Date.now()
2469
+ };
1742
2470
  analyticsService.capture("command_executed", {
1743
2471
  command: actionCommand.name()
1744
2472
  });
1745
2473
  });
1746
2474
  program.addCommand(initCommand(appContext));
1747
2475
  program.addCommand(searchCommand(appContext));
1748
- program.addCommand(getCommand(appContext));
1749
- program.addCommand(fetchCommand(appContext));
1750
- program.addCommand(reviewCommand(appContext));
1751
- program.addCommand(runsCommand(appContext));
1752
- program.addCommand(bugReportCommand(appContext));
1753
- program.addCommand(walletCommand(appContext));
1754
- program.addCommand(configCommand(appContext));
1755
- return program;
1756
- };
1757
-
1758
- // src/app/app-env.ts
1759
- import z4 from "zod";
1760
- var envSchema = z4.object({
1761
- ZERO_API_URL: z4.string().default("https://api.zero.xyz"),
1762
- ZERO_PRIVATE_KEY: z4.string().optional()
1763
- });
1764
- var getEnv = () => {
1765
- try {
1766
- return envSchema.parse(process.env);
1767
- } catch (error) {
1768
- console.error("Error parsing environment variables");
1769
- console.error(error);
1770
- return null;
1771
- }
1772
- };
1773
-
1774
- // src/app/app-services.ts
1775
- import { existsSync as existsSync6, readFileSync as readFileSync8 } from "fs";
1776
- import { homedir as homedir4 } from "os";
1777
- import { join as join5 } from "path";
1778
- import { privateKeyToAccount as privateKeyToAccount3 } from "viem/accounts";
1779
-
1780
- // src/services/analytics-service.ts
1781
- import { randomUUID } from "crypto";
1782
- import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
1783
- import { dirname as dirname2 } from "path";
1784
- import { PostHog } from "posthog-node";
1785
- var POSTHOG_API_KEY = "phc_B2vLyNxAf2mnqvdPQajf4d4b2iXc35dep2ZrvebMJLuX";
1786
- var POSTHOG_HOST = "https://us.i.posthog.com";
1787
- var AnalyticsService = class {
1788
- posthog;
1789
- distinctId;
1790
- cliVersion;
1791
- constructor(opts) {
1792
- this.cliVersion = opts.cliVersion;
1793
- let telemetryEnabled = true;
1794
- let persistedAnonId;
1795
- try {
1796
- if (existsSync4(opts.configPath)) {
1797
- const config = JSON.parse(readFileSync6(opts.configPath, "utf8"));
1798
- if (config.telemetry === false) {
1799
- telemetryEnabled = false;
1800
- }
1801
- if (typeof config.anonId === "string") {
1802
- persistedAnonId = config.anonId;
1803
- }
1804
- }
1805
- } catch {
1806
- }
1807
- if (!telemetryEnabled) {
1808
- this.posthog = null;
1809
- this.distinctId = "";
1810
- return;
1811
- }
1812
- if (opts.walletAddress) {
1813
- this.distinctId = opts.walletAddress;
1814
- } else if (persistedAnonId) {
1815
- this.distinctId = persistedAnonId;
1816
- } else {
1817
- const newAnonId = randomUUID();
1818
- this.distinctId = newAnonId;
1819
- try {
1820
- const dir = dirname2(opts.configPath);
1821
- mkdirSync4(dir, { recursive: true });
1822
- const existing = existsSync4(opts.configPath) ? JSON.parse(readFileSync6(opts.configPath, "utf8")) : {};
1823
- writeFileSync4(
1824
- opts.configPath,
1825
- JSON.stringify({ ...existing, anonId: newAnonId }, null, 2)
1826
- );
1827
- } catch {
1828
- }
1829
- }
1830
- const originalConsoleError = console.error;
1831
- console.error = (...args) => {
1832
- const first = args[0];
1833
- if (typeof first === "string" && first.includes("Error while flushing PostHog")) {
1834
- return;
1835
- }
1836
- originalConsoleError.apply(console, args);
1837
- };
1838
- this.posthog = new PostHog(POSTHOG_API_KEY, {
1839
- host: POSTHOG_HOST,
1840
- flushAt: 1,
1841
- flushInterval: 0
1842
- });
1843
- this.posthog.on("error", () => {
1844
- });
1845
- }
1846
- capture(event, properties) {
1847
- if (!this.posthog) return;
1848
- this.posthog.capture({
1849
- distinctId: this.distinctId,
1850
- event,
1851
- properties: {
1852
- source: "cli",
1853
- // biome-ignore lint/style/useNamingConvention: snake_case is standard for analytics event properties
1854
- cli_version: this.cliVersion,
1855
- ...properties
1856
- }
1857
- });
1858
- }
1859
- async shutdown() {
1860
- if (!this.posthog) return;
1861
- try {
1862
- await this.posthog.shutdown();
1863
- } catch {
1864
- }
1865
- }
1866
- };
1867
-
1868
- // src/services/payment-service.ts
1869
- import {
1870
- adaptViemWallet,
1871
- convertViemChainToRelayChain,
1872
- createClient as createRelayClient,
1873
- getClient as getRelayClient,
1874
- MAINNET_RELAY_API
1875
- } from "@relayprotocol/relay-sdk";
1876
- import { x402Client as X402Client } from "@x402/core/client";
1877
- import { decodePaymentResponseHeader, x402HTTPClient } from "@x402/core/http";
1878
- import { ExactEvmScheme } from "@x402/evm/exact/client";
1879
- import { createSIWxClientHook } from "@x402/extensions/sign-in-with-x";
1880
- import { wrapFetchWithPayment } from "@x402/fetch";
1881
- import { Receipt } from "mppx";
1882
- import { Mppx, tempo } from "mppx/client";
1883
- import {
1884
- createPublicClient,
1885
- createWalletClient,
1886
- formatUnits,
1887
- http
1888
- } from "viem";
1889
- import { base, baseSepolia } from "viem/chains";
1890
- var USDC_BASE = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913";
1891
- var USDC_BASE_SEPOLIA = "0x036CbD53842c5426634e7929541eC2318f3dCF7e";
1892
- var USDC_TEMPO = "0x20c000000000000000000000b9537d11c60e8b50";
1893
- var PATHUSD_TEMPO = "0x20c0000000000000000000000000000000000000";
1894
- var BASE_CHAIN_ID = 8453;
1895
- var TEMPO_CHAIN_ID = 4217;
1896
- var TEMPO_TESTNET_CHAIN_ID = 42431;
1897
- var DEFAULT_MAX_DEPOSIT = "100";
1898
- var KNOWN_EIP712_DOMAINS = {
1899
- // USDC on Base
1900
- [USDC_BASE.toLowerCase()]: { name: "USDC", version: "2" },
1901
- // USDC on Base Sepolia
1902
- [USDC_BASE_SEPOLIA.toLowerCase()]: { name: "USDC", version: "2" }
1903
- };
1904
- var buildRelayClientOptions = () => ({
1905
- baseApiUrl: MAINNET_RELAY_API,
1906
- source: "zero-cli",
1907
- chains: [convertViemChainToRelayChain(base)]
1908
- });
1909
- var calculateBuffer = (baseBalance) => {
1910
- const twentyFivePercent = baseBalance / 4n;
1911
- const twoDollars = 2000000n;
1912
- return twentyFivePercent < twoDollars ? twentyFivePercent : twoDollars;
1913
- };
1914
- var tempoChain = {
1915
- id: TEMPO_CHAIN_ID,
1916
- name: "Tempo",
1917
- nativeCurrency: { name: "USD", symbol: "USD", decimals: 18 },
1918
- rpcUrls: {
1919
- default: { http: ["https://rpc.tempo.xyz"] }
1920
- }
1921
- };
1922
- var tempoTestnetChain = {
1923
- id: TEMPO_TESTNET_CHAIN_ID,
1924
- name: "Tempo Testnet",
1925
- nativeCurrency: { name: "USD", symbol: "USD", decimals: 18 },
1926
- rpcUrls: {
1927
- default: { http: ["https://rpc.moderato.tempo.xyz"] }
1928
- }
1929
- };
1930
- var ERC20_BALANCE_ABI = [
1931
- {
1932
- inputs: [{ name: "account", type: "address" }],
1933
- name: "balanceOf",
1934
- outputs: [{ name: "", type: "uint256" }],
1935
- stateMutability: "view",
1936
- type: "function"
1937
- }
1938
- ];
1939
- var PaymentService = class {
1940
- constructor(account, config) {
1941
- this.account = account;
1942
- this.config = config;
1943
- }
1944
- relayInitialized = false;
1945
- ensureRelayClient = () => {
1946
- if (!this.relayInitialized) {
1947
- createRelayClient(buildRelayClientOptions());
1948
- this.relayInitialized = true;
1949
- }
1950
- };
1951
- bridgeToTempo = async (requiredAmount, onProgress) => {
1952
- if (!this.account) throw new Error("No wallet configured");
1953
- this.ensureRelayClient();
1954
- const baseBalance = await this.getBalanceRaw("base");
1955
- const buffer = calculateBuffer(baseBalance);
1956
- const bridgeAmount = requiredAmount + buffer;
1957
- if (baseBalance < bridgeAmount) {
1958
- throw new Error(
1959
- `Insufficient Base USDC to bridge: have ${formatUnits(baseBalance, 6)}, need ${formatUnits(bridgeAmount, 6)} (${formatUnits(requiredAmount, 6)} + ${formatUnits(buffer, 6)} buffer)`
1960
- );
1961
- }
1962
- onProgress?.(
1963
- `Bridging ${formatUnits(bridgeAmount, 6)} USDC from Base to Tempo...`
1964
- );
1965
- const walletClient = createWalletClient({
1966
- account: this.account,
1967
- chain: base,
1968
- transport: http()
1969
- });
1970
- const quote = await getRelayClient().actions.getQuote({
1971
- chainId: BASE_CHAIN_ID,
1972
- toChainId: TEMPO_CHAIN_ID,
1973
- currency: USDC_BASE,
1974
- toCurrency: USDC_TEMPO,
1975
- amount: bridgeAmount.toString(),
1976
- tradeType: "EXACT_INPUT",
1977
- user: this.account.address,
1978
- recipient: this.account.address,
1979
- options: {
1980
- usePermit: true
1981
- }
1982
- });
1983
- let bridgeTxHash = null;
1984
- await getRelayClient().actions.execute({
1985
- quote,
1986
- wallet: adaptViemWallet(walletClient),
1987
- onProgress: ({ txHashes }) => {
1988
- if (txHashes?.length && !bridgeTxHash) {
1989
- bridgeTxHash = txHashes[0]?.txHash ?? null;
2476
+ program.addCommand(getCommand(appContext));
2477
+ program.addCommand(fetchCommand(appContext));
2478
+ program.addCommand(reviewCommand(appContext));
2479
+ program.addCommand(runsCommand(appContext));
2480
+ program.addCommand(bugReportCommand(appContext));
2481
+ program.addCommand(walletCommand(appContext));
2482
+ program.addCommand(configCommand(appContext));
2483
+ program.addCommand(termsCommand(appContext));
2484
+ return program;
2485
+ };
2486
+
2487
+ // src/app/app-env.ts
2488
+ import z4 from "zod";
2489
+ var envSchema = z4.object({
2490
+ ZERO_API_URL: z4.string().default("https://api.zero.xyz"),
2491
+ ZERO_PRIVATE_KEY: z4.string().optional(),
2492
+ ZERO_ENV: z4.enum(["development", "production"]).default("production")
2493
+ });
2494
+ var getEnv = () => {
2495
+ try {
2496
+ return envSchema.parse(process.env);
2497
+ } catch (error) {
2498
+ console.error("Error parsing environment variables");
2499
+ console.error(error);
2500
+ return null;
2501
+ }
2502
+ };
2503
+
2504
+ // src/app/app-services.ts
2505
+ import { randomUUID as randomUUID2 } from "crypto";
2506
+ import { existsSync as existsSync6, readFileSync as readFileSync8 } from "fs";
2507
+ import { homedir as homedir4 } from "os";
2508
+ import { join as join5 } from "path";
2509
+ import { privateKeyToAccount as privateKeyToAccount3 } from "viem/accounts";
2510
+
2511
+ // src/services/analytics-service.ts
2512
+ import { randomUUID } from "crypto";
2513
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
2514
+ import { dirname as dirname2 } from "path";
2515
+ import { PostHog } from "posthog-node";
2516
+ var POSTHOG_API_KEY = "phc_B2vLyNxAf2mnqvdPQajf4d4b2iXc35dep2ZrvebMJLuX";
2517
+ var POSTHOG_HOST = "https://us.i.posthog.com";
2518
+ var AnalyticsService = class {
2519
+ posthog;
2520
+ distinctId;
2521
+ walletAddress;
2522
+ cliVersion;
2523
+ environment;
2524
+ requestId;
2525
+ agentHost;
2526
+ constructor(opts) {
2527
+ this.cliVersion = opts.cliVersion;
2528
+ this.environment = opts.environment;
2529
+ this.walletAddress = opts.walletAddress;
2530
+ this.requestId = opts.requestId;
2531
+ this.agentHost = opts.agentHost;
2532
+ let telemetryEnabled = true;
2533
+ let persistedAnonId;
2534
+ try {
2535
+ if (existsSync4(opts.configPath)) {
2536
+ const config = JSON.parse(readFileSync6(opts.configPath, "utf8"));
2537
+ if (config.telemetry === false) {
2538
+ telemetryEnabled = false;
2539
+ }
2540
+ if (typeof config.anonId === "string") {
2541
+ persistedAnonId = config.anonId;
1990
2542
  }
1991
2543
  }
1992
- });
1993
- return bridgeTxHash;
1994
- };
1995
- handlePayment = async (url, request, paymentRequirement, maxPay, onProgress) => {
1996
- if (!this.account) {
1997
- throw new Error(
1998
- "No wallet configured \u2014 run `zero init` or set ZERO_PRIVATE_KEY"
1999
- );
2000
- }
2001
- if (paymentRequirement.protocol === "x402") {
2002
- onProgress?.("Paying via x402 on Base...");
2003
- return this.payX402(url, request, paymentRequirement.raw, maxPay);
2544
+ } catch {
2004
2545
  }
2005
- if (paymentRequirement.protocol === "mpp") {
2006
- onProgress?.("Paying via MPP on Tempo...");
2007
- return this.payMpp(
2008
- url,
2009
- request,
2010
- paymentRequirement.raw,
2011
- maxPay,
2012
- onProgress
2013
- );
2546
+ if (!telemetryEnabled) {
2547
+ this.posthog = null;
2548
+ this.distinctId = "";
2549
+ return;
2014
2550
  }
2015
- throw new Error("Unrecognized 402 payment protocol");
2016
- };
2017
- payX402 = async (url, request, _raw, maxPay) => {
2018
- if (!this.account) throw new Error("No wallet configured");
2019
- let capturedAmount = "0";
2020
- const client = new X402Client().register(
2021
- "eip155:*",
2022
- new ExactEvmScheme(this.account)
2023
- );
2024
- client.onBeforePaymentCreation(async (context) => {
2025
- const selected = context.selectedRequirements;
2026
- if (selected && (!selected.extra?.name || !selected.extra?.version)) {
2027
- const known = KNOWN_EIP712_DOMAINS[selected.asset?.toLowerCase() ?? ""];
2028
- if (known) {
2029
- selected.extra = { ...selected.extra, ...known };
2030
- }
2031
- }
2032
- const requirement = context.paymentRequired.accepts[0];
2033
- if (!requirement) return;
2034
- capturedAmount = formatUnits(BigInt(requirement.amount), 6);
2035
- if (maxPay && Number.parseFloat(capturedAmount) > Number.parseFloat(maxPay)) {
2036
- return {
2037
- abort: true,
2038
- reason: `Payment of ${capturedAmount} USDC exceeds --max-pay ${maxPay}`
2039
- };
2040
- }
2041
- });
2042
- const httpClient = new x402HTTPClient(client).onPaymentRequired(
2043
- createSIWxClientHook(this.account)
2044
- );
2045
- const wrappedFetch = wrapFetchWithPayment(fetch, httpClient);
2046
- const response = await wrappedFetch(url, {
2047
- method: request.method,
2048
- headers: request.headers,
2049
- body: request.body
2050
- });
2051
- let txHash = null;
2052
- const paymentResponseHeader = response.headers.get("payment-response") ?? response.headers.get("x-payment-response");
2053
- if (paymentResponseHeader) {
2551
+ if (opts.walletAddress) {
2552
+ this.distinctId = opts.walletAddress;
2553
+ } else if (persistedAnonId) {
2554
+ this.distinctId = persistedAnonId;
2555
+ } else {
2556
+ const newAnonId = randomUUID();
2557
+ this.distinctId = newAnonId;
2054
2558
  try {
2055
- const settlement = decodePaymentResponseHeader(paymentResponseHeader);
2056
- txHash = settlement.transaction ?? null;
2559
+ const dir = dirname2(opts.configPath);
2560
+ mkdirSync4(dir, { recursive: true });
2561
+ const existing = existsSync4(opts.configPath) ? JSON.parse(readFileSync6(opts.configPath, "utf8")) : {};
2562
+ writeFileSync4(
2563
+ opts.configPath,
2564
+ JSON.stringify({ ...existing, anonId: newAnonId }, null, 2)
2565
+ );
2057
2566
  } catch {
2058
2567
  }
2059
2568
  }
2060
- return {
2061
- response,
2062
- protocol: "x402",
2063
- chain: "base",
2064
- txHash,
2065
- amount: capturedAmount,
2066
- asset: "USDC"
2067
- };
2068
- };
2069
- payMpp = async (url, request, _raw, maxPay, onProgress) => {
2070
- if (!this.account) throw new Error("No wallet configured");
2071
- let capturedAmount = "0";
2072
- const mppx = Mppx.create({
2073
- polyfill: false,
2074
- methods: [
2075
- tempo({
2076
- account: this.account,
2077
- maxDeposit: maxPay ?? DEFAULT_MAX_DEPOSIT
2078
- })
2079
- ],
2080
- onChallenge: async (challenge) => {
2081
- const challengeRequest = challenge.request;
2082
- const intent = challenge.intent;
2083
- let requiredRaw;
2084
- if (intent === "session") {
2085
- const suggestedDeposit = challengeRequest.suggestedDeposit;
2086
- if (suggestedDeposit) {
2087
- requiredRaw = BigInt(suggestedDeposit);
2088
- } else {
2089
- const depositStr = maxPay ?? DEFAULT_MAX_DEPOSIT;
2090
- requiredRaw = BigInt(
2091
- Math.floor(Number.parseFloat(depositStr) * 1e6)
2092
- );
2093
- }
2094
- } else {
2095
- requiredRaw = BigInt(challengeRequest.amount);
2096
- }
2097
- capturedAmount = formatUnits(requiredRaw, 6);
2098
- if (maxPay && Number.parseFloat(capturedAmount) > Number.parseFloat(maxPay)) {
2099
- throw new Error(
2100
- `Payment of ${capturedAmount} USDC exceeds --max-pay ${maxPay}`
2101
- );
2102
- }
2103
- const methodDetails = challengeRequest.methodDetails;
2104
- const challengeChainId = challengeRequest.chainId ?? methodDetails?.chainId;
2105
- const isTestnet = challengeChainId === TEMPO_TESTNET_CHAIN_ID;
2106
- const balanceChain = isTestnet ? "tempo-testnet" : "tempo";
2107
- onProgress?.(`Checking Tempo balance...`);
2108
- const tempoBalance = await this.getBalanceRaw(balanceChain);
2109
- if (tempoBalance < requiredRaw) {
2110
- if (isTestnet) {
2111
- throw new Error(
2112
- `Insufficient pathUSD on Tempo testnet: have ${formatUnits(tempoBalance, 6)}, need ${capturedAmount}. Fund your wallet with the Tempo testnet faucet: https://docs.tempo.xyz/quickstart/faucet`
2113
- );
2114
- }
2115
- await this.bridgeToTempo(requiredRaw, onProgress);
2116
- }
2117
- return void 0;
2569
+ const originalConsoleError = console.error;
2570
+ console.error = (...args) => {
2571
+ const first = args[0];
2572
+ if (typeof first === "string" && first.includes("Error while flushing PostHog")) {
2573
+ return;
2118
2574
  }
2575
+ originalConsoleError.apply(console, args);
2576
+ };
2577
+ this.posthog = new PostHog(POSTHOG_API_KEY, {
2578
+ host: POSTHOG_HOST,
2579
+ flushAt: 1,
2580
+ flushInterval: 0
2119
2581
  });
2120
- const response = await mppx.fetch(url, {
2121
- method: request.method,
2122
- headers: request.headers,
2123
- body: request.body
2582
+ this.posthog.on("error", () => {
2124
2583
  });
2125
- let txHash = null;
2584
+ if (opts.walletAddress && persistedAnonId) {
2585
+ this.maybeAliasAnonToWallet(
2586
+ opts.configPath,
2587
+ persistedAnonId,
2588
+ opts.walletAddress
2589
+ );
2590
+ }
2591
+ }
2592
+ maybeAliasAnonToWallet(configPath, anonId, walletAddress) {
2593
+ if (!this.posthog) return;
2594
+ if (anonId === walletAddress) return;
2595
+ let aliasedTo;
2126
2596
  try {
2127
- const receipt = Receipt.fromResponse(response);
2128
- txHash = receipt.reference ?? null;
2597
+ const config = JSON.parse(readFileSync6(configPath, "utf8"));
2598
+ if (typeof config.aliasedTo === "string") {
2599
+ aliasedTo = config.aliasedTo;
2600
+ }
2129
2601
  } catch {
2130
2602
  }
2131
- return {
2132
- response,
2133
- protocol: "mpp",
2134
- chain: "tempo",
2135
- txHash,
2136
- amount: capturedAmount,
2137
- asset: "USDC"
2138
- };
2139
- };
2140
- resolveChainConfig = (chain) => {
2141
- switch (chain) {
2142
- case "base":
2143
- return { viemChain: base, token: USDC_BASE };
2144
- case "base-sepolia":
2145
- return { viemChain: baseSepolia, token: USDC_BASE_SEPOLIA };
2146
- case "tempo":
2147
- return { viemChain: tempoChain, token: USDC_TEMPO };
2148
- case "tempo-testnet":
2149
- return { viemChain: tempoTestnetChain, token: PATHUSD_TEMPO };
2603
+ if (aliasedTo === walletAddress) return;
2604
+ this.posthog.alias({ distinctId: walletAddress, alias: anonId });
2605
+ try {
2606
+ const config = existsSync4(configPath) ? JSON.parse(readFileSync6(configPath, "utf8")) : {};
2607
+ writeFileSync4(
2608
+ configPath,
2609
+ JSON.stringify({ ...config, aliasedTo: walletAddress }, null, 2)
2610
+ );
2611
+ } catch {
2150
2612
  }
2151
- };
2152
- getBalanceRaw = async (chain) => {
2153
- if (!this.account) return 0n;
2154
- const { viemChain, token } = this.resolveChainConfig(chain);
2155
- const client = createPublicClient({
2156
- chain: viemChain,
2157
- transport: http()
2158
- });
2159
- const balance = await client.readContract({
2160
- address: token,
2161
- abi: ERC20_BALANCE_ABI,
2162
- functionName: "balanceOf",
2163
- args: [this.account.address]
2613
+ }
2614
+ // Per-invocation override applied by the preAction hook when `--agent`
2615
+ // is passed. Stateless — affects only this process.
2616
+ setAgentHost(next) {
2617
+ this.agentHost = next;
2618
+ }
2619
+ capture(event, properties) {
2620
+ if (!this.posthog) return;
2621
+ this.posthog.capture({
2622
+ distinctId: this.distinctId,
2623
+ event,
2624
+ properties: {
2625
+ source: "cli",
2626
+ // biome-ignore lint/style/useNamingConvention: snake_case is standard for analytics event properties
2627
+ cli_version: this.cliVersion,
2628
+ environment: this.environment,
2629
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
2630
+ wallet_address: this.walletAddress,
2631
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
2632
+ request_id: this.requestId,
2633
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
2634
+ agent_host: this.agentHost,
2635
+ ...properties
2636
+ }
2164
2637
  });
2165
- return balance;
2166
- };
2167
- getBalance = async (chain) => {
2168
- const raw = await this.getBalanceRaw(chain);
2169
- return { amount: formatUnits(raw, 6), asset: "USDC" };
2170
- };
2638
+ }
2639
+ async shutdown() {
2640
+ if (!this.posthog) return;
2641
+ try {
2642
+ await this.posthog.shutdown();
2643
+ } catch {
2644
+ }
2645
+ }
2171
2646
  };
2172
2647
 
2173
2648
  // src/services/state-service.ts
@@ -2206,7 +2681,7 @@ var WalletService = class {
2206
2681
  if (!this.address) return null;
2207
2682
  if (this.balanceGetter) {
2208
2683
  try {
2209
- const result = await this.balanceGetter("base");
2684
+ const result = await this.balanceGetter();
2210
2685
  return { amount: result.amount, asset: result.asset };
2211
2686
  } catch {
2212
2687
  return {
@@ -2221,6 +2696,15 @@ var WalletService = class {
2221
2696
  getLowBalanceWarning = () => this.config.lowBalanceWarning;
2222
2697
  };
2223
2698
 
2699
+ // src/util/agent-host.ts
2700
+ var detectAgentHost = (env = process.env) => {
2701
+ if (env.ZERO_AGENT) return env.ZERO_AGENT;
2702
+ if (env.CLAUDECODE === "1") return "claude-code";
2703
+ if (env.CURSOR_TRACE_ID) return "cursor";
2704
+ if (env.TERM_PROGRAM === "vscode") return "vscode";
2705
+ return "unknown";
2706
+ };
2707
+
2224
2708
  // src/app/app-services.ts
2225
2709
  var CLI_VERSION = package_default.version;
2226
2710
  var getServices = (env) => {
@@ -2257,12 +2741,15 @@ var getServices = (env) => {
2257
2741
  {
2258
2742
  lowBalanceWarning
2259
2743
  },
2260
- paymentService.getBalance
2744
+ paymentService.getTotalBalance
2261
2745
  );
2262
2746
  const analyticsService = new AnalyticsService({
2263
2747
  walletAddress: account?.address ?? null,
2264
2748
  configPath,
2265
- cliVersion: CLI_VERSION
2749
+ cliVersion: CLI_VERSION,
2750
+ environment: env.ZERO_ENV,
2751
+ requestId: randomUUID2(),
2752
+ agentHost: detectAgentHost()
2266
2753
  });
2267
2754
  return {
2268
2755
  analyticsService,
@@ -2281,7 +2768,8 @@ var createAppContext = () => {
2281
2768
  }
2282
2769
  return {
2283
2770
  env,
2284
- services: getServices(env)
2771
+ services: getServices(env),
2772
+ invocation: { current: null }
2285
2773
  };
2286
2774
  };
2287
2775
 
@@ -2293,15 +2781,27 @@ var main = async () => {
2293
2781
  process.exit(1);
2294
2782
  }
2295
2783
  const app = createApp(appContext);
2784
+ let caughtError = null;
2296
2785
  try {
2297
2786
  await app.parseAsync(process.argv);
2298
2787
  } catch (err) {
2299
2788
  const isCommanderExit = err instanceof Error && "exitCode" in err && err.exitCode === 0;
2300
2789
  if (!isCommanderExit) {
2301
- console.error(err instanceof Error ? err.message : "Unexpected error");
2790
+ caughtError = err instanceof Error ? err : new Error(String(err));
2791
+ console.error(caughtError.message);
2302
2792
  process.exitCode = 1;
2303
2793
  }
2304
2794
  } finally {
2795
+ const invocation = appContext.invocation.current;
2796
+ if (invocation) {
2797
+ appContext.services.analyticsService.capture("command_completed", {
2798
+ command: invocation.command,
2799
+ success: !caughtError && process.exitCode !== 1,
2800
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
2801
+ duration_ms: Date.now() - invocation.startMs,
2802
+ ...caughtError && { error: truncateError(caughtError.message) }
2803
+ });
2804
+ }
2305
2805
  await appContext.services.analyticsService.shutdown();
2306
2806
  }
2307
2807
  };