@zeroxyz/cli 0.0.39 → 0.0.41

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.
Files changed (2) hide show
  1. package/dist/index.js +529 -135
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2,12 +2,12 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { homedir as homedir8 } from "os";
5
- import { join as join9 } from "path";
5
+ import { join as join10 } from "path";
6
6
 
7
7
  // package.json
8
8
  var package_default = {
9
9
  name: "@zeroxyz/cli",
10
- version: "0.0.39",
10
+ version: "0.0.41",
11
11
  type: "module",
12
12
  bin: {
13
13
  zero: "dist/index.js",
@@ -205,7 +205,11 @@ var searchResultSchema = z.object({
205
205
  reviewCount: z.number().optional(),
206
206
  rating: ratingSchema,
207
207
  availabilityStatus: z.enum(["healthy", "degraded", "down", "unknown"]).nullable().optional(),
208
- displayStatus: z.enum(["healthy", "stable", "degraded", "unhealthy", "unknown"]).optional()
208
+ displayStatus: z.enum(["healthy", "stable", "degraded", "unhealthy", "unknown"]).optional(),
209
+ // Pass-through: API stamps this on Zero-published / withzero.{ai,xyz}
210
+ // services so `zero search --json` consumers can render their own
211
+ // provenance UI. CLI doesn't render a badge today.
212
+ isFirstParty: z.boolean().optional().default(false)
209
213
  });
210
214
  var searchResponseSchema = z.object({
211
215
  searchId: z.string(),
@@ -258,7 +262,9 @@ var capabilityResponseSchema = z.object({
258
262
  displayStatus: z.enum(["healthy", "stable", "degraded", "unhealthy", "unknown"]).optional(),
259
263
  activationCount: z.number().optional(),
260
264
  lastUsedAt: z.string().nullable().optional(),
261
- lastSuccessfullyRanAt: z.string().nullable().optional()
265
+ lastSuccessfullyRanAt: z.string().nullable().optional(),
266
+ // Pass-through (see searchResultSchema). Available on `zero get --json`.
267
+ isFirstParty: z.boolean().optional().default(false)
262
268
  });
263
269
  var createRunResponseSchema = z.object({
264
270
  runId: z.string()
@@ -344,6 +350,19 @@ var userDtoSchema = z.object({
344
350
  createdAt: z.union([z.string(), z.date()]).optional(),
345
351
  lastLoginAt: z.union([z.string(), z.date()]).nullable().optional()
346
352
  });
353
+ var userWalletDtoSchema = z.object({
354
+ walletAddress: z.string(),
355
+ source: z.enum(["self_custody", "privy_embedded"]),
356
+ isPrimary: z.boolean(),
357
+ linkedAt: z.union([z.string(), z.date()])
358
+ });
359
+ var signResultSchema = z.object({
360
+ signature: z.string(),
361
+ walletAddress: z.string()
362
+ });
363
+ var migrateResultSchema = z.object({
364
+ transactionHash: z.string()
365
+ });
347
366
  var deviceStartResultSchema = z.object({
348
367
  deviceCode: z.string(),
349
368
  userCode: z.string(),
@@ -361,6 +380,10 @@ var devicePollResponseSchema = z.union([
361
380
  user: userDtoSchema
362
381
  })
363
382
  ]);
383
+ var jsonStringifyBigintSafe = (value) => JSON.stringify(
384
+ value,
385
+ (_key, v) => typeof v === "bigint" ? v.toString() : v
386
+ );
364
387
  var buildCanonicalMessage = (method, path, body, timestamp, nonce) => {
365
388
  const bodyHash = createHash("sha256").update(body ?? "").digest("hex");
366
389
  return `${method}:${path}:${bodyHash}:${timestamp}:${nonce}`;
@@ -378,6 +401,9 @@ var ApiService = class _ApiService {
378
401
  account;
379
402
  credentials;
380
403
  onSessionRefreshed;
404
+ setWalletAddress = (address) => {
405
+ this.walletAddress = address;
406
+ };
381
407
  withAccount = (account) => new _ApiService(this.baseUrl, account);
382
408
  signRequest = async (method, path, body) => {
383
409
  if (!this.account) throw new Error("No private key configured");
@@ -392,13 +418,28 @@ var ApiService = class _ApiService {
392
418
  "x-zero-signature": signature
393
419
  };
394
420
  };
395
- // Session credentials take precedence over EIP-191. We never combine them
396
- // in a single request: if the user has signed in, the JWT is the identity
397
- // the API trusts, and the wallet signing path stays out of the picture.
398
- buildHeaders = async (method, path, bodyStr) => {
421
+ // Auth modes per request. `this.account` is only ever the local private key
422
+ // (the managed Privy proxy lives on PaymentService, never here), so an
423
+ // account here means a BYO key is present.
424
+ // - "default": session JWT takes precedence over EIP-191. If signed in, the
425
+ // JWT is the identity the API trusts; otherwise fall back to a wallet
426
+ // signature, else anonymous. Used by session-scoped endpoints
427
+ // (/users/me, sign-*, wallets) and read endpoints.
428
+ // - "wallet-attributed": the action is attributed to a wallet (runs,
429
+ // reviews, bug reports). Prefer the local private key (EIP-191) so the run
430
+ // is attributed to the wallet that actually paid; when there's no local
431
+ // key (managed user) fall back to the session Bearer and let the server
432
+ // resolve the wallet from the authenticated user. We never send both — a
433
+ // present session would make the server skip EIP-191 verification, so a
434
+ // Bearer + wallet headers combo must never happen.
435
+ buildHeaders = async (method, path, bodyStr, auth) => {
399
436
  const base2 = {
400
437
  "content-type": "application/json"
401
438
  };
439
+ if (auth === "wallet-attributed" && this.account) {
440
+ const walletHeaders = await this.signRequest(method, path, bodyStr);
441
+ return { ...base2, ...walletHeaders };
442
+ }
402
443
  if (this.credentials.kind === "session") {
403
444
  base2.authorization = `Bearer ${this.credentials.accessToken}`;
404
445
  return base2;
@@ -430,12 +471,13 @@ var ApiService = class _ApiService {
430
471
  await this.onSessionRefreshed(body);
431
472
  return true;
432
473
  };
433
- request = async (method, path, body) => {
474
+ request = async (method, path, body, opts = {}) => {
434
475
  const url = `${this.baseUrl}${path}`;
435
- const bodyStr = body ? JSON.stringify(body) : void 0;
476
+ const bodyStr = body ? jsonStringifyBigintSafe(body) : void 0;
477
+ const auth = opts.auth ?? "default";
436
478
  const makeInit = async () => ({
437
479
  method,
438
- headers: await this.buildHeaders(method, path, bodyStr),
480
+ headers: await this.buildHeaders(method, path, bodyStr, auth),
439
481
  body: bodyStr
440
482
  });
441
483
  let response = await fetch(url, await makeInit());
@@ -504,7 +546,9 @@ var ApiService = class _ApiService {
504
546
  return capabilityResponseSchema.parse(json);
505
547
  };
506
548
  createRun = async (data) => {
507
- const json = await this.request("POST", "/v1/runs", data);
549
+ const json = await this.request("POST", "/v1/runs", data, {
550
+ auth: "wallet-attributed"
551
+ });
508
552
  return createRunResponseSchema.parse(json);
509
553
  };
510
554
  listRuns = async (params = {}) => {
@@ -514,19 +558,32 @@ var ApiService = class _ApiService {
514
558
  if (params.limit) qs.set("limit", String(params.limit));
515
559
  if (params.cursor) qs.set("cursor", params.cursor);
516
560
  const suffix = qs.toString() ? `?${qs.toString()}` : "";
517
- const json = await this.request("GET", `/v1/runs${suffix}`);
561
+ const json = await this.request("GET", `/v1/runs${suffix}`, void 0, {
562
+ auth: "wallet-attributed"
563
+ });
518
564
  return listRunsResponseSchema.parse(json);
519
565
  };
520
566
  createReview = async (data) => {
521
- const json = await this.request("POST", "/v1/reviews", data);
567
+ const json = await this.request("POST", "/v1/reviews", data, {
568
+ auth: "wallet-attributed"
569
+ });
522
570
  return createReviewResponseSchema.parse(json);
523
571
  };
524
572
  createReviewsBatch = async (reviews) => {
525
- const json = await this.request("POST", "/v1/reviews/batch", { reviews });
573
+ const json = await this.request(
574
+ "POST",
575
+ "/v1/reviews/batch",
576
+ { reviews },
577
+ {
578
+ auth: "wallet-attributed"
579
+ }
580
+ );
526
581
  return batchReviewResponseSchema.parse(json);
527
582
  };
528
583
  createBugReport = async (data) => {
529
- const json = await this.request("POST", "/v1/bug-reports", data);
584
+ const json = await this.request("POST", "/v1/bug-reports", data, {
585
+ auth: "wallet-attributed"
586
+ });
530
587
  return createBugReportResponseSchema.parse(json);
531
588
  };
532
589
  getFundingUrl = async (amount, provider = "coinbase") => {
@@ -543,6 +600,53 @@ var ApiService = class _ApiService {
543
600
  return null;
544
601
  }
545
602
  };
603
+ getWallets = async () => {
604
+ const json = await this.request("GET", "/v1/users/me/wallets");
605
+ return z.array(userWalletDtoSchema).parse(json);
606
+ };
607
+ provisionWallet = async () => {
608
+ const json = await this.request("POST", "/v1/users/me/wallets/provision");
609
+ return userWalletDtoSchema.parse(json);
610
+ };
611
+ // Hands a BYO-wallet-signed EIP-3009 authorization to the server, which
612
+ // broadcasts it on Base via the gas-paying relayer (sweeping USDC into the
613
+ // caller's provisioned wallet). Session-authed.
614
+ migrateWallet = async (authorization) => {
615
+ const json = await this.request("POST", "/v1/users/me/wallets/migrate", {
616
+ authorization
617
+ });
618
+ return migrateResultSchema.parse(json);
619
+ };
620
+ signTypedDataRemote = async (typedData) => {
621
+ const json = await this.request("POST", "/v1/users/me/sign-typed-data", {
622
+ typedData
623
+ });
624
+ const parsed = signResultSchema.parse(json);
625
+ return {
626
+ signature: parsed.signature,
627
+ walletAddress: parsed.walletAddress
628
+ };
629
+ };
630
+ signMessageRemote = async (message) => {
631
+ const json = await this.request("POST", "/v1/users/me/sign-message", {
632
+ message
633
+ });
634
+ const parsed = signResultSchema.parse(json);
635
+ return {
636
+ signature: parsed.signature,
637
+ walletAddress: parsed.walletAddress
638
+ };
639
+ };
640
+ signTransactionRemote = async (input) => {
641
+ const json = await this.request("POST", "/v1/users/me/sign-transaction", {
642
+ unsignedTransaction: input.unsignedTransaction
643
+ });
644
+ const parsed = signResultSchema.parse(json);
645
+ return {
646
+ signature: parsed.signature,
647
+ walletAddress: parsed.walletAddress
648
+ };
649
+ };
546
650
  };
547
651
 
548
652
  // src/commands/bug-report-command.ts
@@ -839,6 +943,7 @@ import { Command as Command4 } from "commander";
839
943
  import { formatUnits as formatUnits2 } from "viem";
840
944
 
841
945
  // src/services/payment-service.ts
946
+ import { randomBytes } from "crypto";
842
947
  import {
843
948
  adaptViemWallet,
844
949
  convertViemChainToRelayChain,
@@ -859,7 +964,7 @@ import {
859
964
  formatUnits,
860
965
  http
861
966
  } from "viem";
862
- import { base, baseSepolia } from "viem/chains";
967
+ import { base, baseSepolia, tempo as viemTempoChain } from "viem/chains";
863
968
  var SessionCloseFailedError = class extends Error {
864
969
  session;
865
970
  response;
@@ -933,6 +1038,32 @@ var ERC20_BALANCE_ABI = [
933
1038
  type: "function"
934
1039
  }
935
1040
  ];
1041
+ var ERC20_TRANSFER_ABI = [
1042
+ {
1043
+ inputs: [
1044
+ { name: "to", type: "address" },
1045
+ { name: "amount", type: "uint256" }
1046
+ ],
1047
+ name: "transfer",
1048
+ outputs: [{ name: "", type: "bool" }],
1049
+ stateMutability: "nonpayable",
1050
+ type: "function"
1051
+ }
1052
+ ];
1053
+ var USDC_BASE_SWEEP_DOMAIN = { name: "USD Coin", version: "2" };
1054
+ var SWEEP_AUTHORIZATION_TTL_S = 900;
1055
+ var TEMPO_FEE_RESERVE_RAW = 10000n;
1056
+ var EIP3009_TRANSFER_TYPES = {
1057
+ // biome-ignore lint/style/useNamingConvention: EIP-712 primaryType must match the on-chain type name
1058
+ TransferWithAuthorization: [
1059
+ { name: "from", type: "address" },
1060
+ { name: "to", type: "address" },
1061
+ { name: "value", type: "uint256" },
1062
+ { name: "validAfter", type: "uint256" },
1063
+ { name: "validBefore", type: "uint256" },
1064
+ { name: "nonce", type: "bytes32" }
1065
+ ]
1066
+ };
936
1067
  var decodeSessionReceiptHeader = (header) => {
937
1068
  if (!header) return null;
938
1069
  try {
@@ -1378,6 +1509,115 @@ var PaymentService = class {
1378
1509
  ]);
1379
1510
  return { amount: formatUnits(baseRaw + tempoRaw, 6), asset: "USDC" };
1380
1511
  };
1512
+ // Total USDC across both legs (Base + Tempo) for the configured wallet — what
1513
+ // the migrate confirmation prompt quotes. Best-effort: a chain whose RPC read
1514
+ // fails counts as 0 rather than blocking the other leg.
1515
+ getSweepableBalance = async () => {
1516
+ const [baseRaw, tempoRaw] = await Promise.all([
1517
+ this.getBalanceRaw("base").catch(() => 0n),
1518
+ this.getBalanceRaw("tempo").catch(() => 0n)
1519
+ ]);
1520
+ const raw = baseRaw + tempoRaw;
1521
+ return { raw, usdc: formatUnits(raw, 6) };
1522
+ };
1523
+ // Sweeps all USDC from the configured wallet into `to` across Base and Tempo.
1524
+ // Each leg is independent and never throws — failures are captured in the
1525
+ // returned per-chain result so the caller can decide whether to retire the
1526
+ // source key.
1527
+ migrateAllUsdc = async (to, relayer) => {
1528
+ const baseResult = await this.sweepBaseUsdc(to, relayer);
1529
+ const tempoResult = await this.sweepTempoUsdc(to);
1530
+ return [baseResult, tempoResult];
1531
+ };
1532
+ // Base leg: sign an EIP-3009 transferWithAuthorization for the full balance
1533
+ // and hand it to the relayer, which broadcasts it gaslessly.
1534
+ sweepBaseUsdc = async (to, relayer) => {
1535
+ try {
1536
+ if (!this.account) throw new Error("No wallet configured");
1537
+ const balance = await this.getBalanceRaw("base");
1538
+ if (balance <= 0n) return { chain: "base", status: "skipped" };
1539
+ const nowS = Math.floor(Date.now() / 1e3);
1540
+ const message = {
1541
+ from: this.account.address,
1542
+ to,
1543
+ value: balance,
1544
+ validAfter: 0n,
1545
+ validBefore: BigInt(nowS + SWEEP_AUTHORIZATION_TTL_S),
1546
+ nonce: `0x${randomBytes(32).toString("hex")}`
1547
+ };
1548
+ const signature = await this.account.signTypedData({
1549
+ domain: {
1550
+ name: USDC_BASE_SWEEP_DOMAIN.name,
1551
+ version: USDC_BASE_SWEEP_DOMAIN.version,
1552
+ chainId: BASE_CHAIN_ID,
1553
+ verifyingContract: USDC_BASE
1554
+ },
1555
+ types: EIP3009_TRANSFER_TYPES,
1556
+ primaryType: "TransferWithAuthorization",
1557
+ message
1558
+ });
1559
+ const { transactionHash } = await relayer.migrateWallet({
1560
+ from: message.from,
1561
+ to: message.to,
1562
+ value: message.value.toString(),
1563
+ validAfter: message.validAfter.toString(),
1564
+ validBefore: message.validBefore.toString(),
1565
+ nonce: message.nonce,
1566
+ signature
1567
+ });
1568
+ return {
1569
+ chain: "base",
1570
+ status: "swept",
1571
+ amount: formatUnits(balance, 6),
1572
+ txHash: transactionHash
1573
+ };
1574
+ } catch (err) {
1575
+ return { chain: "base", status: "failed", error: err.message };
1576
+ }
1577
+ };
1578
+ // Tempo leg: self-broadcast an ERC-20 transfer paying the fee in USDC via
1579
+ // `feeToken` (no native gas token needed). Uses viem's tempo chain for its
1580
+ // type-0x76 serializer; reserves a tiny amount to cover the fee.
1581
+ sweepTempoUsdc = async (to) => {
1582
+ try {
1583
+ if (!this.account) throw new Error("No wallet configured");
1584
+ const balance = await this.getBalanceRaw("tempo");
1585
+ if (balance <= TEMPO_FEE_RESERVE_RAW) {
1586
+ return { chain: "tempo", status: "skipped" };
1587
+ }
1588
+ const amount = balance - TEMPO_FEE_RESERVE_RAW;
1589
+ const wallet = createWalletClient({
1590
+ account: this.account,
1591
+ chain: viemTempoChain,
1592
+ transport: http()
1593
+ });
1594
+ const txHash = await wallet.writeContract({
1595
+ address: USDC_TEMPO,
1596
+ abi: ERC20_TRANSFER_ABI,
1597
+ functionName: "transfer",
1598
+ args: [to, amount],
1599
+ feeToken: USDC_TEMPO
1600
+ // biome-ignore lint/suspicious/noExplicitAny: tempo feeToken param
1601
+ });
1602
+ const publicClient = createPublicClient({
1603
+ chain: viemTempoChain,
1604
+ transport: http()
1605
+ });
1606
+ await publicClient.waitForTransactionReceipt({ hash: txHash });
1607
+ return {
1608
+ chain: "tempo",
1609
+ status: "swept",
1610
+ amount: formatUnits(amount, 6),
1611
+ txHash
1612
+ };
1613
+ } catch (err) {
1614
+ return {
1615
+ chain: "tempo",
1616
+ status: "failed",
1617
+ error: err.message
1618
+ };
1619
+ }
1620
+ };
1381
1621
  };
1382
1622
 
1383
1623
  // src/util/infer-schema.ts
@@ -1629,55 +1869,6 @@ var fetchCommand = (appContext) => new Command4("fetch").description(
1629
1869
  } else {
1630
1870
  resolvedUrl = url;
1631
1871
  }
1632
- if (!url && options.capability && !stateService.findSearchContextByCapability(options.capability)) {
1633
- try {
1634
- let capName = resolvedCapabilityName;
1635
- if (capName === void 0) {
1636
- const cap = await apiService.getCapability(options.capability);
1637
- capName = cap.name;
1638
- }
1639
- const searchResult = await apiService.search({ query: capName });
1640
- const matchedEntry = searchResult.capabilities.find(
1641
- (c) => c.slug === options.capability || c.id === options.capability
1642
- );
1643
- const slugFoundInResults = Boolean(matchedEntry);
1644
- stateService.saveLastSearch({
1645
- searchId: searchResult.searchId,
1646
- capabilities: searchResult.capabilities.map((c) => ({
1647
- position: c.position,
1648
- id: c.id,
1649
- slug: c.slug,
1650
- url: c.url,
1651
- urlTemplate: c.urlTemplate ?? null,
1652
- displayCostAmount: c.cost.amount
1653
- }))
1654
- });
1655
- analyticsService.capture("search_executed", {
1656
- query: truncateQuery(capName),
1657
- queryLength: capName.length,
1658
- resultCount: searchResult.capabilities.length,
1659
- searchId: searchResult.searchId,
1660
- total: searchResult.total,
1661
- hasMore: searchResult.hasMore,
1662
- json: false,
1663
- triggeredBy: "slug_handoff",
1664
- slugFoundInResults
1665
- });
1666
- analyticsService.capture("capability_viewed", {
1667
- // Existing field preserved as the raw --capability
1668
- // input for back-compat.
1669
- capabilityId: options.capability,
1670
- // New canonical identifier fields hydrated from the
1671
- // resolved search result (when the slug was found).
1672
- capabilityUid: matchedEntry?.id,
1673
- capabilitySlug: matchedEntry?.slug,
1674
- fromLastSearch: false,
1675
- searchId: searchResult.searchId,
1676
- triggeredBy: "slug_handoff"
1677
- });
1678
- } catch {
1679
- }
1680
- }
1681
1872
  let resolvedBody;
1682
1873
  try {
1683
1874
  resolvedBody = resolveRequestBody(
@@ -1733,6 +1924,7 @@ var fetchCommand = (appContext) => new Command4("fetch").description(
1733
1924
  const capabilitySlug = matchCtx?.capabilitySlug ?? null;
1734
1925
  const searchId = matchCtx?.searchId;
1735
1926
  const resultRank = matchCtx?.resultRank;
1927
+ const fetchOrigin = matchCtx ? "from_search" : options.capability ? "direct_slug" : "direct_url";
1736
1928
  const matchedDisplayCostAmount = matchCtx?.displayCostAmount;
1737
1929
  const skipReasons = [];
1738
1930
  if (!apiService.walletAddress) {
@@ -1950,6 +2142,9 @@ var fetchCommand = (appContext) => new Command4("fetch").description(
1950
2142
  resultRank: resultRank ?? void 0,
1951
2143
  runId: runId ?? void 0,
1952
2144
  runTracked: !!runId,
2145
+ // A NULL searchId on a direct_slug fetch is correct here,
2146
+ // not a funnel gap to paper over.
2147
+ fetchOrigin,
1953
2148
  ...fetchError && { error: truncateError(fetchError.message) }
1954
2149
  });
1955
2150
  const isFetchFailure = Boolean(fetchError) || !finalResponse || typeof status === "number" && (status < 200 || status >= 300);
@@ -3368,14 +3563,30 @@ Read the full terms at: ${TERMS_URL}
3368
3563
  });
3369
3564
 
3370
3565
  // src/commands/wallet-command.ts
3371
- import { existsSync as existsSync3, readFileSync as readFileSync7 } from "fs";
3566
+ import { existsSync as existsSync3, readFileSync as readFileSync8 } from "fs";
3372
3567
  import { homedir as homedir4 } from "os";
3373
- import { join as join4 } from "path";
3568
+ import { join as join5 } from "path";
3569
+ import { confirm, isCancel } from "@clack/prompts";
3374
3570
  import { Command as Command11 } from "commander";
3375
3571
  import open2 from "open";
3376
3572
  import { isAddress } from "viem";
3377
3573
  import { generatePrivateKey as generatePrivateKey2, privateKeyToAccount as privateKeyToAccount2 } from "viem/accounts";
3378
3574
 
3575
+ // src/util/migrate-config.ts
3576
+ import { readFileSync as readFileSync7 } from "fs";
3577
+ import { join as join4 } from "path";
3578
+ var backupAndStripPrivateKey = (zeroDir) => {
3579
+ const configPath = join4(zeroDir, "config.json");
3580
+ const backupPath = join4(zeroDir, "config.backup.json");
3581
+ const raw = readFileSync7(configPath, "utf8");
3582
+ ensureSecureDir(zeroDir);
3583
+ writeSecureFile(backupPath, raw);
3584
+ const parsed = JSON.parse(raw);
3585
+ delete parsed.privateKey;
3586
+ writeSecureFile(configPath, JSON.stringify(parsed, null, 2));
3587
+ return { backupPath, configPath };
3588
+ };
3589
+
3379
3590
  // src/util/stdin.ts
3380
3591
  var readStdin = async () => {
3381
3592
  const chunks = [];
@@ -3559,11 +3770,11 @@ var walletSetCommand = (appContext) => new Command11("set").description("Set wal
3559
3770
  process.exitCode = 1;
3560
3771
  return;
3561
3772
  }
3562
- const zeroDir = join4(homedir4(), ".zero");
3563
- const configPath = join4(zeroDir, "config.json");
3773
+ const zeroDir = join5(homedir4(), ".zero");
3774
+ const configPath = join5(zeroDir, "config.json");
3564
3775
  if (!options.force && existsSync3(configPath)) {
3565
3776
  try {
3566
- const existing2 = JSON.parse(readFileSync7(configPath, "utf8"));
3777
+ const existing2 = JSON.parse(readFileSync8(configPath, "utf8"));
3567
3778
  if (existing2.privateKey) {
3568
3779
  console.error(
3569
3780
  "Wallet already configured. Use --force to overwrite."
@@ -3575,7 +3786,7 @@ var walletSetCommand = (appContext) => new Command11("set").description("Set wal
3575
3786
  }
3576
3787
  }
3577
3788
  ensureSecureDir(zeroDir);
3578
- const existing = existsSync3(configPath) ? JSON.parse(readFileSync7(configPath, "utf8")) : {};
3789
+ const existing = existsSync3(configPath) ? JSON.parse(readFileSync8(configPath, "utf8")) : {};
3579
3790
  writeSecureFile(
3580
3791
  configPath,
3581
3792
  JSON.stringify(
@@ -3671,6 +3882,142 @@ var walletGenerateCommand = (appContext) => new Command11("generate").descriptio
3671
3882
  });
3672
3883
  }
3673
3884
  );
3885
+ var resolveZeroWalletAddress = async (apiService) => {
3886
+ try {
3887
+ const wallets = await apiService.getWallets();
3888
+ const primary = wallets.find(
3889
+ (w) => w.source === "privy_embedded" && w.isPrimary
3890
+ );
3891
+ if (primary) return primary.walletAddress;
3892
+ const provisioned = await apiService.provisionWallet();
3893
+ return provisioned.walletAddress ?? null;
3894
+ } catch {
3895
+ return null;
3896
+ }
3897
+ };
3898
+ var walletMigrateCommand = (appContext) => new Command11("migrate").description(
3899
+ "Sweep all USDC from your private-key wallet into your Zero wallet"
3900
+ ).option("--json", "Emit the migration result as JSON").option("-y, --yes", "Skip the confirmation prompt").action(async (options) => {
3901
+ const { apiService, paymentService } = appContext.services;
3902
+ const zeroDir = join5(homedir4(), ".zero");
3903
+ const configPath = join5(zeroDir, "config.json");
3904
+ const config = existsSync3(configPath) ? readConfig(configPath) : {};
3905
+ const privateKey = appContext.env.ZERO_PRIVATE_KEY ?? config.privateKey;
3906
+ if (!privateKey) {
3907
+ console.error(
3908
+ "No private-key wallet found. Run `zero wallet set <key>` first."
3909
+ );
3910
+ process.exitCode = 1;
3911
+ return;
3912
+ }
3913
+ const hasSession = Boolean(
3914
+ appContext.env.ZERO_SESSION_TOKEN || config.session
3915
+ );
3916
+ if (!hasSession) {
3917
+ console.error("Not logged in. Run `zero auth login` first.");
3918
+ process.exitCode = 1;
3919
+ return;
3920
+ }
3921
+ let source;
3922
+ try {
3923
+ source = privateKeyToAccount2(privateKey);
3924
+ } catch {
3925
+ console.error("Invalid private key in config.");
3926
+ process.exitCode = 1;
3927
+ return;
3928
+ }
3929
+ const to = await resolveZeroWalletAddress(apiService);
3930
+ if (!to) {
3931
+ console.error(
3932
+ "Could not resolve your Zero wallet. Run `zero auth login` and try again."
3933
+ );
3934
+ process.exitCode = 1;
3935
+ return;
3936
+ }
3937
+ if (to.toLowerCase() === source.address.toLowerCase()) {
3938
+ console.error(
3939
+ "Source and destination are the same wallet \u2014 nothing to migrate."
3940
+ );
3941
+ process.exitCode = 1;
3942
+ return;
3943
+ }
3944
+ const balance = await paymentService.getSweepableBalance();
3945
+ if (balance.raw <= 0n) {
3946
+ if (options.json) {
3947
+ console.log(
3948
+ JSON.stringify({ destination: to, legs: [], keyRemoved: false })
3949
+ );
3950
+ } else {
3951
+ console.log("Nothing to migrate (no USDC found).");
3952
+ }
3953
+ return;
3954
+ }
3955
+ if (!options.yes) {
3956
+ if (options.json || !process.stdin.isTTY) {
3957
+ console.error(
3958
+ "Refusing to migrate without confirmation. Re-run with --yes to proceed."
3959
+ );
3960
+ process.exitCode = 1;
3961
+ return;
3962
+ }
3963
+ const proceed = await confirm({
3964
+ message: `Please confirm you would like to transfer $${balance.usdc} from your private wallet to your Zero managed wallet`
3965
+ });
3966
+ if (isCancel(proceed) || !proceed) {
3967
+ console.log("Migration cancelled.");
3968
+ return;
3969
+ }
3970
+ }
3971
+ const results = await paymentService.migrateAllUsdc(to, apiService);
3972
+ const anyFailed = results.some((r) => r.status === "failed");
3973
+ const anySwept = results.some((r) => r.status === "swept");
3974
+ let backupPath = null;
3975
+ if (anySwept && !anyFailed && config.privateKey) {
3976
+ try {
3977
+ backupPath = backupAndStripPrivateKey(zeroDir).backupPath;
3978
+ } catch {
3979
+ backupPath = null;
3980
+ }
3981
+ }
3982
+ if (options.json) {
3983
+ console.log(
3984
+ JSON.stringify({
3985
+ destination: to,
3986
+ legs: results,
3987
+ keyRemoved: backupPath !== null,
3988
+ backupPath
3989
+ })
3990
+ );
3991
+ } else {
3992
+ console.log(`Migrating USDC \u2192 ${to}`);
3993
+ for (const r of results) {
3994
+ if (r.status === "swept") {
3995
+ console.log(
3996
+ ` ${r.chain}: swept ${r.amount} USDC (tx ${r.txHash})`
3997
+ );
3998
+ } else if (r.status === "skipped") {
3999
+ console.log(` ${r.chain}: skipped (no balance)`);
4000
+ } else {
4001
+ console.log(` ${r.chain}: FAILED \u2014 ${r.error}`);
4002
+ }
4003
+ }
4004
+ if (backupPath) {
4005
+ console.log("");
4006
+ console.log(
4007
+ `Private key removed from config (backed up to ${backupPath}). Your Zero wallet is now primary.`
4008
+ );
4009
+ } else if (anyFailed) {
4010
+ console.log("");
4011
+ console.log(
4012
+ "A transfer failed \u2014 your private key was kept. Re-run `zero wallet migrate` to retry."
4013
+ );
4014
+ } else if (!anySwept) {
4015
+ console.log("");
4016
+ console.log("Nothing to migrate (no USDC found).");
4017
+ }
4018
+ }
4019
+ if (anyFailed) process.exitCode = 1;
4020
+ });
3674
4021
  var walletCommand = (appContext) => {
3675
4022
  const cmd = new Command11("wallet").description("Manage your wallet");
3676
4023
  cmd.addCommand(walletBalanceCommand(appContext));
@@ -3678,22 +4025,23 @@ var walletCommand = (appContext) => {
3678
4025
  cmd.addCommand(walletAddressCommand(appContext));
3679
4026
  cmd.addCommand(walletSetCommand(appContext));
3680
4027
  cmd.addCommand(walletGenerateCommand(appContext));
4028
+ cmd.addCommand(walletMigrateCommand(appContext), { hidden: true });
3681
4029
  return cmd;
3682
4030
  };
3683
4031
 
3684
4032
  // src/commands/welcome-command.ts
3685
- import { existsSync as existsSync4, readFileSync as readFileSync8 } from "fs";
4033
+ import { existsSync as existsSync4, readFileSync as readFileSync9 } from "fs";
3686
4034
  import { homedir as homedir5 } from "os";
3687
- import { join as join5 } from "path";
4035
+ import { join as join6 } from "path";
3688
4036
  import { Command as Command12 } from "commander";
3689
4037
  import open3 from "open";
3690
4038
  import { getAddress } from "viem";
3691
4039
  import { privateKeyToAccount as privateKeyToAccount3 } from "viem/accounts";
3692
4040
  var readPrivateKey = () => {
3693
- const configPath = join5(homedir5(), ".zero", "config.json");
4041
+ const configPath = join6(homedir5(), ".zero", "config.json");
3694
4042
  if (!existsSync4(configPath)) return null;
3695
4043
  try {
3696
- const config = JSON.parse(readFileSync8(configPath, "utf8"));
4044
+ const config = JSON.parse(readFileSync9(configPath, "utf8"));
3697
4045
  if (typeof config.privateKey === "string") {
3698
4046
  return config.privateKey;
3699
4047
  }
@@ -3822,6 +4170,7 @@ var envSchema = z4.object({
3822
4170
  ZERO_API_URL: z4.string().default("https://api.zero.xyz"),
3823
4171
  ZERO_WEB_URL: z4.string().default("https://zero.xyz"),
3824
4172
  ZERO_PRIVATE_KEY: z4.string().optional(),
4173
+ ZERO_SESSION_TOKEN: z4.string().optional(),
3825
4174
  ZERO_ENV: z4.enum(["development", "production"]).default("production")
3826
4175
  });
3827
4176
  var getEnv = () => {
@@ -3838,12 +4187,12 @@ var getEnv = () => {
3838
4187
  import { randomUUID as randomUUID2 } from "crypto";
3839
4188
  import { existsSync as existsSync7 } from "fs";
3840
4189
  import { homedir as homedir6 } from "os";
3841
- import { join as join7 } from "path";
4190
+ import { join as join8 } from "path";
3842
4191
  import { privateKeyToAccount as privateKeyToAccount4 } from "viem/accounts";
3843
4192
 
3844
4193
  // src/services/analytics-service.ts
3845
4194
  import { randomUUID } from "crypto";
3846
- import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "fs";
4195
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync10, writeFileSync as writeFileSync4 } from "fs";
3847
4196
  import { dirname as dirname2 } from "path";
3848
4197
  import { PostHog } from "posthog-node";
3849
4198
  var POSTHOG_API_KEY = "phc_B2vLyNxAf2mnqvdPQajf4d4b2iXc35dep2ZrvebMJLuX";
@@ -3866,7 +4215,7 @@ var AnalyticsService = class {
3866
4215
  let persistedAnonId;
3867
4216
  try {
3868
4217
  if (existsSync5(opts.configPath)) {
3869
- const config = JSON.parse(readFileSync9(opts.configPath, "utf8"));
4218
+ const config = JSON.parse(readFileSync10(opts.configPath, "utf8"));
3870
4219
  if (config.telemetry === false) {
3871
4220
  telemetryEnabled = false;
3872
4221
  }
@@ -3888,15 +4237,17 @@ var AnalyticsService = class {
3888
4237
  } else {
3889
4238
  const newAnonId = randomUUID();
3890
4239
  this.distinctId = newAnonId;
3891
- try {
3892
- const dir = dirname2(opts.configPath);
3893
- mkdirSync4(dir, { recursive: true });
3894
- const existing = existsSync5(opts.configPath) ? JSON.parse(readFileSync9(opts.configPath, "utf8")) : {};
3895
- writeFileSync4(
3896
- opts.configPath,
3897
- JSON.stringify({ ...existing, anonId: newAnonId }, null, 2)
3898
- );
3899
- } catch {
4240
+ if (!process.env.VITEST) {
4241
+ try {
4242
+ const dir = dirname2(opts.configPath);
4243
+ mkdirSync4(dir, { recursive: true });
4244
+ const existing = existsSync5(opts.configPath) ? JSON.parse(readFileSync10(opts.configPath, "utf8")) : {};
4245
+ writeFileSync4(
4246
+ opts.configPath,
4247
+ JSON.stringify({ ...existing, anonId: newAnonId }, null, 2)
4248
+ );
4249
+ } catch {
4250
+ }
3900
4251
  }
3901
4252
  }
3902
4253
  const originalConsoleError = console.error;
@@ -3931,7 +4282,7 @@ var AnalyticsService = class {
3931
4282
  if (anonId === walletAddress) return;
3932
4283
  let aliasedTo;
3933
4284
  try {
3934
- const config = JSON.parse(readFileSync9(configPath, "utf8"));
4285
+ const config = JSON.parse(readFileSync10(configPath, "utf8"));
3935
4286
  if (typeof config.aliasedTo === "string") {
3936
4287
  aliasedTo = config.aliasedTo;
3937
4288
  }
@@ -3939,8 +4290,9 @@ var AnalyticsService = class {
3939
4290
  }
3940
4291
  if (aliasedTo === walletAddress) return;
3941
4292
  this.posthog.alias({ distinctId: walletAddress, alias: anonId });
4293
+ if (process.env.VITEST) return;
3942
4294
  try {
3943
- const config = existsSync5(configPath) ? JSON.parse(readFileSync9(configPath, "utf8")) : {};
4295
+ const config = existsSync5(configPath) ? JSON.parse(readFileSync10(configPath, "utf8")) : {};
3944
4296
  writeFileSync4(
3945
4297
  configPath,
3946
4298
  JSON.stringify({ ...config, aliasedTo: walletAddress }, null, 2)
@@ -4015,15 +4367,43 @@ var AnalyticsService = class {
4015
4367
  }
4016
4368
  };
4017
4369
 
4370
+ // src/services/api-account.ts
4371
+ import {
4372
+ bytesToHex,
4373
+ parseSignature,
4374
+ serializeTransaction
4375
+ } from "viem";
4376
+ import { toAccount } from "viem/accounts";
4377
+ var createApiAccount = (walletAddress, api) => toAccount({
4378
+ address: walletAddress,
4379
+ async signMessage({ message }) {
4380
+ const asMessage = typeof message === "string" ? message : typeof message.raw === "string" ? message.raw : bytesToHex(message.raw);
4381
+ const { signature } = await api.signMessageRemote(asMessage);
4382
+ return signature;
4383
+ },
4384
+ async signTypedData(typedData) {
4385
+ const { signature } = await api.signTypedDataRemote(typedData);
4386
+ return signature;
4387
+ },
4388
+ async signTransaction(transaction, options) {
4389
+ const serialize = options?.serializer ?? serializeTransaction;
4390
+ const unsigned = await serialize(transaction);
4391
+ const { signature } = await api.signTransactionRemote({
4392
+ unsignedTransaction: unsigned
4393
+ });
4394
+ return await serialize(transaction, parseSignature(signature));
4395
+ }
4396
+ });
4397
+
4018
4398
  // src/services/state-service.ts
4019
- import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
4020
- import { join as join6 } from "path";
4399
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync11, writeFileSync as writeFileSync5 } from "fs";
4400
+ import { join as join7 } from "path";
4021
4401
  var RECENT_SEARCH_LIMIT = 10;
4022
4402
  var StateService = class {
4023
4403
  constructor(zeroDir) {
4024
4404
  this.zeroDir = zeroDir;
4025
- this.lastSearchPath = join6(zeroDir, "last_search.json");
4026
- this.recentSearchesPath = join6(zeroDir, "recent_searches.json");
4405
+ this.lastSearchPath = join7(zeroDir, "last_search.json");
4406
+ this.recentSearchesPath = join7(zeroDir, "recent_searches.json");
4027
4407
  }
4028
4408
  lastSearchPath;
4029
4409
  recentSearchesPath;
@@ -4043,7 +4423,7 @@ var StateService = class {
4043
4423
  loadLastSearch = () => {
4044
4424
  try {
4045
4425
  if (!existsSync6(this.lastSearchPath)) return null;
4046
- const raw = readFileSync10(this.lastSearchPath, "utf8");
4426
+ const raw = readFileSync11(this.lastSearchPath, "utf8");
4047
4427
  return JSON.parse(raw);
4048
4428
  } catch {
4049
4429
  return null;
@@ -4055,7 +4435,7 @@ var StateService = class {
4055
4435
  const last = this.loadLastSearch();
4056
4436
  return { searches: last ? [last] : [] };
4057
4437
  }
4058
- const raw = readFileSync10(this.recentSearchesPath, "utf8");
4438
+ const raw = readFileSync11(this.recentSearchesPath, "utf8");
4059
4439
  const parsed = JSON.parse(raw);
4060
4440
  return { searches: parsed.searches ?? [] };
4061
4441
  } catch {
@@ -4154,30 +4534,20 @@ var detectAgentHost = (env = process.env) => {
4154
4534
  // src/app/app-services.ts
4155
4535
  var CLI_VERSION = package_default.version;
4156
4536
  var resolveCredentials = (env, config) => {
4157
- if (config.session) {
4158
- return {
4159
- credentials: {
4160
- kind: "session",
4161
- accessToken: config.session.accessToken,
4162
- refreshToken: config.session.refreshToken,
4163
- userId: config.session.userId
4164
- },
4165
- privateKey: null
4166
- };
4167
- }
4168
- if (env.ZERO_PRIVATE_KEY) {
4169
- return {
4170
- credentials: { kind: "none" },
4171
- privateKey: env.ZERO_PRIVATE_KEY
4172
- };
4173
- }
4174
- if (config.privateKey) {
4175
- return {
4176
- credentials: { kind: "none" },
4177
- privateKey: config.privateKey
4178
- };
4179
- }
4180
- return { credentials: { kind: "none" }, privateKey: null };
4537
+ const injectedToken = env.ZERO_SESSION_TOKEN;
4538
+ const credentials = injectedToken ? {
4539
+ kind: "session",
4540
+ accessToken: injectedToken,
4541
+ refreshToken: "",
4542
+ userId: ""
4543
+ } : config.session ? {
4544
+ kind: "session",
4545
+ accessToken: config.session.accessToken,
4546
+ refreshToken: config.session.refreshToken,
4547
+ userId: config.session.userId
4548
+ } : { kind: "none" };
4549
+ const privateKey = env.ZERO_PRIVATE_KEY ?? config.privateKey ?? null;
4550
+ return { credentials, privateKey };
4181
4551
  };
4182
4552
  var buildOnSessionRefreshed = (configPath) => async (tokens) => {
4183
4553
  const current = readConfig(configPath);
@@ -4192,20 +4562,31 @@ var buildOnSessionRefreshed = (configPath) => async (tokens) => {
4192
4562
  };
4193
4563
  writeSecureFile(configPath, JSON.stringify(next, null, 2));
4194
4564
  };
4195
- var getServices = (env) => {
4196
- const zeroDir = join7(homedir6(), ".zero");
4197
- const configPath = join7(zeroDir, "config.json");
4565
+ var getServices = async (env) => {
4566
+ const zeroDir = join8(homedir6(), ".zero");
4567
+ const configPath = join8(zeroDir, "config.json");
4198
4568
  const config = existsSync7(configPath) ? readConfig(configPath) : {};
4199
4569
  const { credentials, privateKey } = resolveCredentials(env, config);
4200
- const account = privateKey ? privateKeyToAccount4(privateKey) : null;
4201
4570
  const lowBalanceWarning = typeof config.lowBalanceWarning === "number" ? config.lowBalanceWarning : 1;
4202
4571
  const apiService = new ApiService(
4203
4572
  env.ZERO_API_URL,
4204
- account,
4573
+ privateKey ? privateKeyToAccount4(privateKey) : null,
4205
4574
  credentials,
4206
4575
  buildOnSessionRefreshed(configPath)
4207
4576
  );
4208
- const paymentService = new PaymentService(account, { lowBalanceWarning });
4577
+ let account = privateKey ? privateKeyToAccount4(privateKey) : null;
4578
+ if (!account && credentials.kind === "session") {
4579
+ const address = await resolveManagedWalletAddress(apiService);
4580
+ if (address) {
4581
+ account = createApiAccount(address, apiService);
4582
+ }
4583
+ }
4584
+ if (account && !apiService.walletAddress) {
4585
+ apiService.setWalletAddress(account.address);
4586
+ }
4587
+ const paymentService = new PaymentService(account, {
4588
+ lowBalanceWarning
4589
+ });
4209
4590
  const stateService = new StateService(zeroDir);
4210
4591
  const walletService = new WalletService(
4211
4592
  apiService.walletAddress,
@@ -4228,16 +4609,29 @@ var getServices = (env) => {
4228
4609
  walletService
4229
4610
  };
4230
4611
  };
4612
+ var resolveManagedWalletAddress = async (api) => {
4613
+ try {
4614
+ const wallets = await api.getWallets();
4615
+ const primary = wallets.find(
4616
+ (w) => w.source === "privy_embedded" && w.isPrimary
4617
+ );
4618
+ if (primary) return primary.walletAddress;
4619
+ const provisioned = await api.provisionWallet();
4620
+ return provisioned.walletAddress ?? null;
4621
+ } catch {
4622
+ return null;
4623
+ }
4624
+ };
4231
4625
 
4232
4626
  // src/app/app-context.ts
4233
- var createAppContext = () => {
4627
+ var createAppContext = async () => {
4234
4628
  const env = getEnv();
4235
4629
  if (!env) {
4236
4630
  return null;
4237
4631
  }
4238
4632
  return {
4239
4633
  env,
4240
- services: getServices(env),
4634
+ services: await getServices(env),
4241
4635
  invocation: { current: null }
4242
4636
  };
4243
4637
  };
@@ -4247,12 +4641,12 @@ import {
4247
4641
  existsSync as existsSync8,
4248
4642
  lstatSync,
4249
4643
  mkdirSync as mkdirSync6,
4250
- readFileSync as readFileSync11,
4644
+ readFileSync as readFileSync12,
4251
4645
  readlinkSync,
4252
4646
  writeFileSync as writeFileSync6
4253
4647
  } from "fs";
4254
4648
  import { homedir as homedir7 } from "os";
4255
- import { dirname as dirname3, join as join8, resolve } from "path";
4649
+ import { dirname as dirname3, join as join9, resolve } from "path";
4256
4650
  var CACHE_FILENAME = "update_check.json";
4257
4651
  var NPM_REGISTRY_URL = "https://registry.npmjs.org/@zeroxyz/cli/latest";
4258
4652
  var CHECK_INTERVAL_MS = 60 * 60 * 1e3;
@@ -4279,7 +4673,7 @@ var detectInstallMethod = (opts = {}) => {
4279
4673
  const home = opts.home ?? homedir7();
4280
4674
  if (pkg) return "binary";
4281
4675
  const resolved = resolveExecPath(execPath);
4282
- const zeroBin = join8(home, ".zero", "bin");
4676
+ const zeroBin = join9(home, ".zero", "bin");
4283
4677
  if (resolved.startsWith(zeroBin)) return "binary";
4284
4678
  return "npm";
4285
4679
  };
@@ -4304,12 +4698,12 @@ var compareVersions = (a, b) => {
4304
4698
  if (pb.pre === null) return -1;
4305
4699
  return pa.pre < pb.pre ? -1 : 1;
4306
4700
  };
4307
- var cachePath = (zeroDir) => join8(zeroDir, CACHE_FILENAME);
4701
+ var cachePath = (zeroDir) => join9(zeroDir, CACHE_FILENAME);
4308
4702
  var readCache = (zeroDir) => {
4309
4703
  try {
4310
4704
  const path = cachePath(zeroDir);
4311
4705
  if (!existsSync8(path)) return emptyCache;
4312
- const raw = readFileSync11(path, "utf8");
4706
+ const raw = readFileSync12(path, "utf8");
4313
4707
  const parsed = JSON.parse(raw);
4314
4708
  return {
4315
4709
  lastCheckedMs: typeof parsed.lastCheckedMs === "number" ? parsed.lastCheckedMs : 0,
@@ -4393,12 +4787,12 @@ var maybePrintUpdateBanner = (zeroDir, currentVersion) => {
4393
4787
 
4394
4788
  // src/index.ts
4395
4789
  var main = async () => {
4396
- const appContext = createAppContext();
4790
+ const appContext = await createAppContext();
4397
4791
  if (!appContext) {
4398
4792
  console.error("Failed to create app context");
4399
4793
  process.exit(1);
4400
4794
  }
4401
- const zeroDir = join9(homedir8(), ".zero");
4795
+ const zeroDir = join10(homedir8(), ".zero");
4402
4796
  maybePrintUpdateBanner(zeroDir, package_default.version);
4403
4797
  const app = createApp(appContext);
4404
4798
  let caughtError = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeroxyz/cli",
3
- "version": "0.0.39",
3
+ "version": "0.0.41",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "zero": "dist/index.js",