@zeroxyz/cli 0.0.40 → 0.0.42

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 +667 -151
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { homedir as homedir8 } from "os";
5
- import { join as join9 } from "path";
4
+ import { homedir as homedir9 } from "os";
5
+ import { join as join11 } from "path";
6
6
 
7
7
  // package.json
8
8
  var package_default = {
9
9
  name: "@zeroxyz/cli",
10
- version: "0.0.40",
10
+ version: "0.0.42",
11
11
  type: "module",
12
12
  bin: {
13
13
  zero: "dist/index.js",
@@ -61,7 +61,7 @@ var package_default = {
61
61
  };
62
62
 
63
63
  // src/app.ts
64
- import { Command as Command13 } from "commander";
64
+ import { Command as Command14 } from "commander";
65
65
 
66
66
  // src/commands/auth-command.ts
67
67
  import { homedir } from "os";
@@ -204,8 +204,7 @@ var searchResultSchema = z.object({
204
204
  cost: z.object({ amount: z.string(), asset: z.string() }),
205
205
  reviewCount: z.number().optional(),
206
206
  rating: ratingSchema,
207
- availabilityStatus: z.enum(["healthy", "degraded", "down", "unknown"]).nullable().optional(),
208
- displayStatus: z.enum(["healthy", "stable", "degraded", "unhealthy", "unknown"]).optional(),
207
+ availabilityStatus: z.enum(["healthy", "unknown", "down"]).nullable().optional(),
209
208
  // Pass-through: API stamps this on Zero-published / withzero.{ai,xyz}
210
209
  // services so `zero search --json` consumers can render their own
211
210
  // provenance UI. CLI doesn't render a badge today.
@@ -258,8 +257,7 @@ var capabilityResponseSchema = z.object({
258
257
  priority: z.number()
259
258
  })
260
259
  ).nullable(),
261
- availabilityStatus: z.enum(["healthy", "degraded", "down", "unknown"]).nullable().optional(),
262
- displayStatus: z.enum(["healthy", "stable", "degraded", "unhealthy", "unknown"]).optional(),
260
+ availabilityStatus: z.enum(["healthy", "unknown", "down"]).nullable().optional(),
263
261
  activationCount: z.number().optional(),
264
262
  lastUsedAt: z.string().nullable().optional(),
265
263
  lastSuccessfullyRanAt: z.string().nullable().optional(),
@@ -360,6 +358,9 @@ var signResultSchema = z.object({
360
358
  signature: z.string(),
361
359
  walletAddress: z.string()
362
360
  });
361
+ var migrateResultSchema = z.object({
362
+ transactionHash: z.string()
363
+ });
363
364
  var deviceStartResultSchema = z.object({
364
365
  deviceCode: z.string(),
365
366
  userCode: z.string(),
@@ -583,16 +584,36 @@ var ApiService = class _ApiService {
583
584
  });
584
585
  return createBugReportResponseSchema.parse(json);
585
586
  };
587
+ // Mints a one-time onramp URL for the caller's wallet. Two paths, picked by
588
+ // which identity is available:
589
+ // • BYO key present (this.account) → /v1/wallet/fund-url, proving ownership
590
+ // with an EIP-191 signature. Honors the `provider` choice.
591
+ // • No key but a session (managed/runner) → /v1/users/me/fund-url, which
592
+ // resolves the user's managed wallet server-side from the Bearer. Coinbase
593
+ // only (the managed onramp doesn't take a provider), so `provider` is
594
+ // ignored on this path.
595
+ // Returns null on any failure so callers can fall back to manual transfer.
586
596
  getFundingUrl = async (amount, provider = "coinbase") => {
587
597
  try {
588
- const params = new URLSearchParams({ provider });
589
- if (amount) params.set("amount", amount);
590
- const json = await this.request(
591
- "GET",
592
- `/v1/wallet/fund-url?${params.toString()}`
593
- );
594
- const parsed = z.object({ url: z.string() }).parse(json);
595
- return parsed.url;
598
+ if (this.account) {
599
+ const params = new URLSearchParams({ provider });
600
+ if (amount) params.set("amount", amount);
601
+ const json = await this.request(
602
+ "GET",
603
+ `/v1/wallet/fund-url?${params.toString()}`,
604
+ void 0,
605
+ { auth: "wallet-attributed" }
606
+ );
607
+ return z.object({ url: z.string() }).parse(json).url;
608
+ }
609
+ if (this.credentials.kind === "session") {
610
+ const params = new URLSearchParams();
611
+ if (amount) params.set("amount", amount);
612
+ const qs = params.toString() ? `?${params.toString()}` : "";
613
+ const json = await this.request("GET", `/v1/users/me/fund-url${qs}`);
614
+ return z.object({ url: z.string(), walletAddress: z.string() }).parse(json).url;
615
+ }
616
+ return null;
596
617
  } catch {
597
618
  return null;
598
619
  }
@@ -605,6 +626,15 @@ var ApiService = class _ApiService {
605
626
  const json = await this.request("POST", "/v1/users/me/wallets/provision");
606
627
  return userWalletDtoSchema.parse(json);
607
628
  };
629
+ // Hands a BYO-wallet-signed EIP-3009 authorization to the server, which
630
+ // broadcasts it on Base via the gas-paying relayer (sweeping USDC into the
631
+ // caller's provisioned wallet). Session-authed.
632
+ migrateWallet = async (authorization) => {
633
+ const json = await this.request("POST", "/v1/users/me/wallets/migrate", {
634
+ authorization
635
+ });
636
+ return migrateResultSchema.parse(json);
637
+ };
608
638
  signTypedDataRemote = async (typedData) => {
609
639
  const json = await this.request("POST", "/v1/users/me/sign-typed-data", {
610
640
  typedData
@@ -931,6 +961,7 @@ import { Command as Command4 } from "commander";
931
961
  import { formatUnits as formatUnits2 } from "viem";
932
962
 
933
963
  // src/services/payment-service.ts
964
+ import { randomBytes } from "crypto";
934
965
  import {
935
966
  adaptViemWallet,
936
967
  convertViemChainToRelayChain,
@@ -951,7 +982,7 @@ import {
951
982
  formatUnits,
952
983
  http
953
984
  } from "viem";
954
- import { base, baseSepolia } from "viem/chains";
985
+ import { base, baseSepolia, tempo as viemTempoChain } from "viem/chains";
955
986
  var SessionCloseFailedError = class extends Error {
956
987
  session;
957
988
  response;
@@ -1025,6 +1056,32 @@ var ERC20_BALANCE_ABI = [
1025
1056
  type: "function"
1026
1057
  }
1027
1058
  ];
1059
+ var ERC20_TRANSFER_ABI = [
1060
+ {
1061
+ inputs: [
1062
+ { name: "to", type: "address" },
1063
+ { name: "amount", type: "uint256" }
1064
+ ],
1065
+ name: "transfer",
1066
+ outputs: [{ name: "", type: "bool" }],
1067
+ stateMutability: "nonpayable",
1068
+ type: "function"
1069
+ }
1070
+ ];
1071
+ var USDC_BASE_SWEEP_DOMAIN = { name: "USD Coin", version: "2" };
1072
+ var SWEEP_AUTHORIZATION_TTL_S = 900;
1073
+ var TEMPO_FEE_RESERVE_RAW = 10000n;
1074
+ var EIP3009_TRANSFER_TYPES = {
1075
+ // biome-ignore lint/style/useNamingConvention: EIP-712 primaryType must match the on-chain type name
1076
+ TransferWithAuthorization: [
1077
+ { name: "from", type: "address" },
1078
+ { name: "to", type: "address" },
1079
+ { name: "value", type: "uint256" },
1080
+ { name: "validAfter", type: "uint256" },
1081
+ { name: "validBefore", type: "uint256" },
1082
+ { name: "nonce", type: "bytes32" }
1083
+ ]
1084
+ };
1028
1085
  var decodeSessionReceiptHeader = (header) => {
1029
1086
  if (!header) return null;
1030
1087
  try {
@@ -1231,11 +1288,6 @@ var PaymentService = class {
1231
1288
  `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`
1232
1289
  );
1233
1290
  }
1234
- if (this.config.managed) {
1235
- throw new Error(
1236
- `Insufficient USDC on Tempo: have ${formatUnits(tempoBalance, 6)}, need ${capturedAmount}. Managed wallets can't auto-bridge from Base yet \u2014 fund your Tempo wallet${this.account ? ` (${this.account.address})` : ""} directly, or set a local ZERO_PRIVATE_KEY to bridge.`
1237
- );
1238
- }
1239
1291
  await this.bridgeToTempo(requiredRaw, onProgress);
1240
1292
  }
1241
1293
  return capturedAmount;
@@ -1475,6 +1527,115 @@ var PaymentService = class {
1475
1527
  ]);
1476
1528
  return { amount: formatUnits(baseRaw + tempoRaw, 6), asset: "USDC" };
1477
1529
  };
1530
+ // Total USDC across both legs (Base + Tempo) for the configured wallet — what
1531
+ // the migrate confirmation prompt quotes. Best-effort: a chain whose RPC read
1532
+ // fails counts as 0 rather than blocking the other leg.
1533
+ getSweepableBalance = async () => {
1534
+ const [baseRaw, tempoRaw] = await Promise.all([
1535
+ this.getBalanceRaw("base").catch(() => 0n),
1536
+ this.getBalanceRaw("tempo").catch(() => 0n)
1537
+ ]);
1538
+ const raw = baseRaw + tempoRaw;
1539
+ return { raw, usdc: formatUnits(raw, 6) };
1540
+ };
1541
+ // Sweeps all USDC from the configured wallet into `to` across Base and Tempo.
1542
+ // Each leg is independent and never throws — failures are captured in the
1543
+ // returned per-chain result so the caller can decide whether to retire the
1544
+ // source key.
1545
+ migrateAllUsdc = async (to, relayer) => {
1546
+ const baseResult = await this.sweepBaseUsdc(to, relayer);
1547
+ const tempoResult = await this.sweepTempoUsdc(to);
1548
+ return [baseResult, tempoResult];
1549
+ };
1550
+ // Base leg: sign an EIP-3009 transferWithAuthorization for the full balance
1551
+ // and hand it to the relayer, which broadcasts it gaslessly.
1552
+ sweepBaseUsdc = async (to, relayer) => {
1553
+ try {
1554
+ if (!this.account) throw new Error("No wallet configured");
1555
+ const balance = await this.getBalanceRaw("base");
1556
+ if (balance <= 0n) return { chain: "base", status: "skipped" };
1557
+ const nowS = Math.floor(Date.now() / 1e3);
1558
+ const message = {
1559
+ from: this.account.address,
1560
+ to,
1561
+ value: balance,
1562
+ validAfter: 0n,
1563
+ validBefore: BigInt(nowS + SWEEP_AUTHORIZATION_TTL_S),
1564
+ nonce: `0x${randomBytes(32).toString("hex")}`
1565
+ };
1566
+ const signature = await this.account.signTypedData({
1567
+ domain: {
1568
+ name: USDC_BASE_SWEEP_DOMAIN.name,
1569
+ version: USDC_BASE_SWEEP_DOMAIN.version,
1570
+ chainId: BASE_CHAIN_ID,
1571
+ verifyingContract: USDC_BASE
1572
+ },
1573
+ types: EIP3009_TRANSFER_TYPES,
1574
+ primaryType: "TransferWithAuthorization",
1575
+ message
1576
+ });
1577
+ const { transactionHash } = await relayer.migrateWallet({
1578
+ from: message.from,
1579
+ to: message.to,
1580
+ value: message.value.toString(),
1581
+ validAfter: message.validAfter.toString(),
1582
+ validBefore: message.validBefore.toString(),
1583
+ nonce: message.nonce,
1584
+ signature
1585
+ });
1586
+ return {
1587
+ chain: "base",
1588
+ status: "swept",
1589
+ amount: formatUnits(balance, 6),
1590
+ txHash: transactionHash
1591
+ };
1592
+ } catch (err) {
1593
+ return { chain: "base", status: "failed", error: err.message };
1594
+ }
1595
+ };
1596
+ // Tempo leg: self-broadcast an ERC-20 transfer paying the fee in USDC via
1597
+ // `feeToken` (no native gas token needed). Uses viem's tempo chain for its
1598
+ // type-0x76 serializer; reserves a tiny amount to cover the fee.
1599
+ sweepTempoUsdc = async (to) => {
1600
+ try {
1601
+ if (!this.account) throw new Error("No wallet configured");
1602
+ const balance = await this.getBalanceRaw("tempo");
1603
+ if (balance <= TEMPO_FEE_RESERVE_RAW) {
1604
+ return { chain: "tempo", status: "skipped" };
1605
+ }
1606
+ const amount = balance - TEMPO_FEE_RESERVE_RAW;
1607
+ const wallet = createWalletClient({
1608
+ account: this.account,
1609
+ chain: viemTempoChain,
1610
+ transport: http()
1611
+ });
1612
+ const txHash = await wallet.writeContract({
1613
+ address: USDC_TEMPO,
1614
+ abi: ERC20_TRANSFER_ABI,
1615
+ functionName: "transfer",
1616
+ args: [to, amount],
1617
+ feeToken: USDC_TEMPO
1618
+ // biome-ignore lint/suspicious/noExplicitAny: tempo feeToken param
1619
+ });
1620
+ const publicClient = createPublicClient({
1621
+ chain: viemTempoChain,
1622
+ transport: http()
1623
+ });
1624
+ await publicClient.waitForTransactionReceipt({ hash: txHash });
1625
+ return {
1626
+ chain: "tempo",
1627
+ status: "swept",
1628
+ amount: formatUnits(amount, 6),
1629
+ txHash
1630
+ };
1631
+ } catch (err) {
1632
+ return {
1633
+ chain: "tempo",
1634
+ status: "failed",
1635
+ error: err.message
1636
+ };
1637
+ }
1638
+ };
1478
1639
  };
1479
1640
 
1480
1641
  // src/util/infer-schema.ts
@@ -1726,55 +1887,6 @@ var fetchCommand = (appContext) => new Command4("fetch").description(
1726
1887
  } else {
1727
1888
  resolvedUrl = url;
1728
1889
  }
1729
- if (!url && options.capability && !stateService.findSearchContextByCapability(options.capability)) {
1730
- try {
1731
- let capName = resolvedCapabilityName;
1732
- if (capName === void 0) {
1733
- const cap = await apiService.getCapability(options.capability);
1734
- capName = cap.name;
1735
- }
1736
- const searchResult = await apiService.search({ query: capName });
1737
- const matchedEntry = searchResult.capabilities.find(
1738
- (c) => c.slug === options.capability || c.id === options.capability
1739
- );
1740
- const slugFoundInResults = Boolean(matchedEntry);
1741
- stateService.saveLastSearch({
1742
- searchId: searchResult.searchId,
1743
- capabilities: searchResult.capabilities.map((c) => ({
1744
- position: c.position,
1745
- id: c.id,
1746
- slug: c.slug,
1747
- url: c.url,
1748
- urlTemplate: c.urlTemplate ?? null,
1749
- displayCostAmount: c.cost.amount
1750
- }))
1751
- });
1752
- analyticsService.capture("search_executed", {
1753
- query: truncateQuery(capName),
1754
- queryLength: capName.length,
1755
- resultCount: searchResult.capabilities.length,
1756
- searchId: searchResult.searchId,
1757
- total: searchResult.total,
1758
- hasMore: searchResult.hasMore,
1759
- json: false,
1760
- triggeredBy: "slug_handoff",
1761
- slugFoundInResults
1762
- });
1763
- analyticsService.capture("capability_viewed", {
1764
- // Existing field preserved as the raw --capability
1765
- // input for back-compat.
1766
- capabilityId: options.capability,
1767
- // New canonical identifier fields hydrated from the
1768
- // resolved search result (when the slug was found).
1769
- capabilityUid: matchedEntry?.id,
1770
- capabilitySlug: matchedEntry?.slug,
1771
- fromLastSearch: false,
1772
- searchId: searchResult.searchId,
1773
- triggeredBy: "slug_handoff"
1774
- });
1775
- } catch {
1776
- }
1777
- }
1778
1890
  let resolvedBody;
1779
1891
  try {
1780
1892
  resolvedBody = resolveRequestBody(
@@ -1830,6 +1942,7 @@ var fetchCommand = (appContext) => new Command4("fetch").description(
1830
1942
  const capabilitySlug = matchCtx?.capabilitySlug ?? null;
1831
1943
  const searchId = matchCtx?.searchId;
1832
1944
  const resultRank = matchCtx?.resultRank;
1945
+ const fetchOrigin = matchCtx ? "from_search" : options.capability ? "direct_slug" : "direct_url";
1833
1946
  const matchedDisplayCostAmount = matchCtx?.displayCostAmount;
1834
1947
  const skipReasons = [];
1835
1948
  if (!apiService.walletAddress) {
@@ -2047,6 +2160,9 @@ var fetchCommand = (appContext) => new Command4("fetch").description(
2047
2160
  resultRank: resultRank ?? void 0,
2048
2161
  runId: runId ?? void 0,
2049
2162
  runTracked: !!runId,
2163
+ // A NULL searchId on a direct_slug fetch is correct here,
2164
+ // not a funnel gap to paper over.
2165
+ fetchOrigin,
2050
2166
  ...fetchError && { error: truncateError(fetchError.message) }
2051
2167
  });
2052
2168
  const isFetchFailure = Boolean(fetchError) || !finalResponse || typeof status === "number" && (status < 200 || status >= 300);
@@ -2305,7 +2421,7 @@ var formatCapability = (capability) => {
2305
2421
  const lines = [];
2306
2422
  lines.push(capability.name);
2307
2423
  lines.push(` Rating: ${formatRating(capability.rating)}`);
2308
- lines.push(` Status: ${capability.displayStatus ?? "unknown"}`);
2424
+ lines.push(` Status: ${capability.availabilityStatus ?? "unknown"}`);
2309
2425
  lines.push(...formatCost(capability));
2310
2426
  lines.push(` URL: ${capability.url}`);
2311
2427
  lines.push(` Method: ${capability.method}`);
@@ -2547,6 +2663,8 @@ var AGENT_TOOLS = [
2547
2663
  },
2548
2664
  { name: "Cursor", detectDir: ".cursor", skillsDir: ".cursor/skills" }
2549
2665
  ];
2666
+ var HOOK_FILES = ["auto-approve-zero.sh", "zero-context.sh"];
2667
+ var ZERO_SANDBOX_DOMAIN = "*.zero.xyz";
2550
2668
  var findResourceDir = (startDir, resourceName) => {
2551
2669
  let current = startDir;
2552
2670
  while (true) {
@@ -2616,10 +2734,9 @@ var installHook = (home, verbose = false) => {
2616
2734
  const zeroHooksDir = join3(home, ".zero", "hooks");
2617
2735
  mkdirSync3(zeroHooksDir, { recursive: true });
2618
2736
  if (verbose) stepInfo(`staged hook dir at ${zeroHooksDir}`);
2619
- const hookFiles = ["auto-approve-zero.sh", "zero-context.sh"];
2620
2737
  const hookDests = {};
2621
2738
  const hooksSourceDir = findResourceDir(getCliModuleDir(), "hooks");
2622
- for (const hookFile of hookFiles) {
2739
+ for (const hookFile of HOOK_FILES) {
2623
2740
  const hookSource = join3(hooksSourceDir, hookFile);
2624
2741
  const hookDest = join3(zeroHooksDir, hookFile);
2625
2742
  copyFile(hookSource, hookDest);
@@ -2722,12 +2839,15 @@ var installHook = (home, verbose = false) => {
2722
2839
  network.allowedDomains = [];
2723
2840
  }
2724
2841
  const allowedDomains = network.allowedDomains;
2725
- const zeroDomain = "*.zero.xyz";
2726
- if (!allowedDomains.includes(zeroDomain)) {
2727
- allowedDomains.push(zeroDomain);
2728
- if (verbose) stepInfo(`sandbox.network.allowedDomains += ${zeroDomain}`);
2842
+ if (!allowedDomains.includes(ZERO_SANDBOX_DOMAIN)) {
2843
+ allowedDomains.push(ZERO_SANDBOX_DOMAIN);
2844
+ if (verbose) {
2845
+ stepInfo(`sandbox.network.allowedDomains += ${ZERO_SANDBOX_DOMAIN}`);
2846
+ }
2729
2847
  } else if (verbose) {
2730
- stepInfo(`${zeroDomain} already in sandbox allowlist \u2014 not modified`);
2848
+ stepInfo(
2849
+ `${ZERO_SANDBOX_DOMAIN} already in sandbox allowlist \u2014 not modified`
2850
+ );
2731
2851
  }
2732
2852
  writeFileSync3(settingsPath, `${JSON.stringify(settings, null, 2)}
2733
2853
  `);
@@ -2763,6 +2883,14 @@ var removeConflictingSkills = (home) => {
2763
2883
  }
2764
2884
  return removed;
2765
2885
  };
2886
+ var getBundledSkillNames = () => {
2887
+ try {
2888
+ const skillsSourceDir = findResourceDir(getCliModuleDir(), "skills");
2889
+ return readdirSync(skillsSourceDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
2890
+ } catch {
2891
+ return ["zero"];
2892
+ }
2893
+ };
2766
2894
  var installSkills = (home, verbose = false) => {
2767
2895
  const skillsSourceDir = findResourceDir(getCliModuleDir(), "skills");
2768
2896
  const skillDirs = readdirSync(skillsSourceDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
@@ -3306,8 +3434,16 @@ var formatRatingBadge = (rating) => {
3306
3434
  return `${successPct} success \xB7 ${reviews} reviews`;
3307
3435
  };
3308
3436
  var formatStatusBadge = (status) => {
3309
- if (!status || status === "unknown") return "";
3310
- return ` \u2014 ${status}`;
3437
+ switch (status) {
3438
+ case "healthy":
3439
+ return ` \u2014 ${color.green("\u2713 healthy")}`;
3440
+ case "down":
3441
+ return ` \u2014 ${color.red("\u2717 down")}`;
3442
+ case "unknown":
3443
+ return ` \u2014 ${color.yellow("? unknown")}`;
3444
+ default:
3445
+ return "";
3446
+ }
3311
3447
  };
3312
3448
  var formatSearchResults = (results) => {
3313
3449
  if (results.length === 0) return "No capabilities found.";
@@ -3316,7 +3452,7 @@ var formatSearchResults = (results) => {
3316
3452
  const displayName = r.brandName ? `${r.brandName} ${baseName}` : baseName;
3317
3453
  const displayDescription = r.whatItDoes ?? r.description;
3318
3454
  const ratingBadge = formatRatingBadge(r.rating);
3319
- const statusBadge = formatStatusBadge(r.displayStatus);
3455
+ const statusBadge = formatStatusBadge(r.availabilityStatus);
3320
3456
  const costLabel = r.cost.amount === "0" ? "Free" : r.cost.amount === "unknown" ? "variable pricing" : `$${r.cost.amount}/call`;
3321
3457
  return ` ${r.position}. ${displayName} \u2014 ${costLabel} \u2014 ${ratingBadge}${statusBadge}
3322
3458
  "${displayDescription}"`;
@@ -3327,7 +3463,7 @@ var searchCommand = (appContext) => new Command9("search").description("Search f
3327
3463
  `Maximum cost per call in USD (default: ${DEFAULT_MAX_COST_USD})`
3328
3464
  ).option("--protocol <protocol>", "Payment protocol (x402 or mpp)").option(
3329
3465
  "--status <status>",
3330
- "Filter by availability (healthy, unknown, down). Defaults to healthy when neither --status nor --all is set; pass --all to see everything."
3466
+ "Filter by availability (healthy, unknown, down). Default (unset) returns healthy + unknown with healthy first; pass 'healthy' for only-healthy, or --all to include everything."
3331
3467
  ).option("--all", "Disable default quality filtering").option(
3332
3468
  "--source <source>",
3333
3469
  "Only show results from this crawl source (e.g. mpp, bazaar)"
@@ -3362,12 +3498,8 @@ var searchCommand = (appContext) => new Command9("search").description("Search f
3362
3498
  process.exitCode = 1;
3363
3499
  return;
3364
3500
  }
3365
- let effectiveStatus = options.status;
3366
- let appliedDefaultStatus = false;
3367
- if (effectiveStatus === void 0 && !options.all) {
3368
- effectiveStatus = "healthy";
3369
- appliedDefaultStatus = true;
3370
- }
3501
+ const effectiveStatus = options.status;
3502
+ const appliedDefaultStatus = effectiveStatus === void 0 && !options.all;
3371
3503
  const result = await apiService.search({
3372
3504
  query,
3373
3505
  offset: options.offset,
@@ -3464,15 +3596,264 @@ Read the full terms at: ${TERMS_URL}
3464
3596
  }
3465
3597
  });
3466
3598
 
3467
- // src/commands/wallet-command.ts
3468
- import { existsSync as existsSync3, readFileSync as readFileSync7 } from "fs";
3599
+ // src/commands/uninstall-command.ts
3600
+ import {
3601
+ existsSync as existsSync3,
3602
+ readdirSync as readdirSync2,
3603
+ readFileSync as readFileSync7,
3604
+ rmSync as rmSync2,
3605
+ writeFileSync as writeFileSync4
3606
+ } from "fs";
3469
3607
  import { homedir as homedir4 } from "os";
3470
3608
  import { join as join4 } from "path";
3471
3609
  import { Command as Command11 } from "commander";
3610
+ var removeSkills = (home, verbose = false) => {
3611
+ const skillNames = getBundledSkillNames();
3612
+ const removed = [];
3613
+ for (const tool of AGENT_TOOLS) {
3614
+ const toolSkillsPath = join4(home, tool.skillsDir);
3615
+ if (!existsSync3(toolSkillsPath)) {
3616
+ if (verbose) {
3617
+ stepInfo(
3618
+ `${tool.name}: ~/${tool.skillsDir} not found \u2014 nothing to remove`
3619
+ );
3620
+ }
3621
+ continue;
3622
+ }
3623
+ for (const skillName of skillNames) {
3624
+ const skillPath = join4(toolSkillsPath, skillName);
3625
+ if (!existsSync3(skillPath)) continue;
3626
+ rmSync2(skillPath, { recursive: true, force: true });
3627
+ removed.push(`${tool.name}: ${skillPath}`);
3628
+ if (verbose) stepInfo(`${tool.name}: removed skill '${skillName}'`);
3629
+ }
3630
+ }
3631
+ return removed;
3632
+ };
3633
+ var removeHooks = (home, verbose = false) => {
3634
+ let settingsChanged = false;
3635
+ const settingsPath = join4(home, ".claude", "settings.json");
3636
+ if (!existsSync3(settingsPath)) {
3637
+ if (verbose) stepInfo(`${settingsPath} not found \u2014 no settings to clean`);
3638
+ } else {
3639
+ let settings = null;
3640
+ try {
3641
+ settings = JSON.parse(readFileSync7(settingsPath, "utf-8"));
3642
+ } catch {
3643
+ if (verbose) {
3644
+ stepInfo(`${settingsPath} was unparseable JSON \u2014 leaving it untouched`);
3645
+ }
3646
+ }
3647
+ if (settings) {
3648
+ settingsChanged = stripZeroFromSettings(settings, verbose);
3649
+ if (settingsChanged) {
3650
+ writeFileSync4(settingsPath, `${JSON.stringify(settings, null, 2)}
3651
+ `);
3652
+ if (verbose) stepInfo(`rewrote ${settingsPath} without Zero entries`);
3653
+ } else if (verbose) {
3654
+ stepInfo(`no Zero entries found in ${settingsPath}`);
3655
+ }
3656
+ }
3657
+ }
3658
+ let scriptsRemoved = 0;
3659
+ const zeroHooksDir = join4(home, ".zero", "hooks");
3660
+ if (existsSync3(zeroHooksDir)) {
3661
+ for (const hookFile of HOOK_FILES) {
3662
+ const hookPath = join4(zeroHooksDir, hookFile);
3663
+ if (!existsSync3(hookPath)) continue;
3664
+ rmSync2(hookPath, { force: true });
3665
+ scriptsRemoved++;
3666
+ if (verbose) stepInfo(`removed ${hookPath}`);
3667
+ }
3668
+ if (readdirSync2(zeroHooksDir).length === 0) {
3669
+ rmSync2(zeroHooksDir, { recursive: true, force: true });
3670
+ if (verbose) stepInfo(`removed empty ${zeroHooksDir}`);
3671
+ }
3672
+ } else if (verbose) {
3673
+ stepInfo(`${zeroHooksDir} not found \u2014 no hook scripts to remove`);
3674
+ }
3675
+ return { settingsChanged, scriptsRemoved };
3676
+ };
3677
+ var stripZeroFromSettings = (settings, verbose) => {
3678
+ let changed = false;
3679
+ const hooks = settings.hooks && typeof settings.hooks === "object" ? settings.hooks : null;
3680
+ if (hooks) {
3681
+ changed = removeHookEntries(hooks, "PreToolUse", "auto-approve-zero", verbose) || changed;
3682
+ changed = removeHookEntries(hooks, "UserPromptSubmit", "zero-context", verbose) || changed;
3683
+ for (const key of ["PreToolUse", "UserPromptSubmit"]) {
3684
+ const arr = hooks[key];
3685
+ if (Array.isArray(arr) && arr.length === 0) delete hooks[key];
3686
+ }
3687
+ if (Object.keys(hooks).length === 0) delete settings.hooks;
3688
+ }
3689
+ const sandbox = settings.sandbox && typeof settings.sandbox === "object" ? settings.sandbox : null;
3690
+ const network = sandbox?.network && typeof sandbox.network === "object" ? sandbox.network : null;
3691
+ if (network && Array.isArray(network.allowedDomains)) {
3692
+ const domains = network.allowedDomains;
3693
+ const next = domains.filter((d) => d !== ZERO_SANDBOX_DOMAIN);
3694
+ if (next.length !== domains.length) {
3695
+ changed = true;
3696
+ if (verbose) {
3697
+ stepInfo(`sandbox.network.allowedDomains -= ${ZERO_SANDBOX_DOMAIN}`);
3698
+ }
3699
+ if (next.length === 0) {
3700
+ delete network.allowedDomains;
3701
+ if (sandbox && Object.keys(network).length === 0)
3702
+ delete sandbox.network;
3703
+ if (sandbox && Object.keys(sandbox).length === 0)
3704
+ delete settings.sandbox;
3705
+ } else {
3706
+ network.allowedDomains = next;
3707
+ }
3708
+ }
3709
+ }
3710
+ return changed;
3711
+ };
3712
+ var removeHookEntries = (hooks, hookType, commandSubstring, verbose) => {
3713
+ const entries = hooks[hookType];
3714
+ if (!Array.isArray(entries)) return false;
3715
+ const next = entries.filter((entry) => {
3716
+ const entryHooks = entry?.hooks;
3717
+ if (!Array.isArray(entryHooks)) return true;
3718
+ return !entryHooks.some(
3719
+ (h) => typeof h.command === "string" && h.command.includes(commandSubstring)
3720
+ );
3721
+ });
3722
+ if (next.length === entries.length) return false;
3723
+ hooks[hookType] = next;
3724
+ if (verbose) stepInfo(`removed ${hookType} entry (${commandSubstring})`);
3725
+ return true;
3726
+ };
3727
+ var removeWallet = (home, verbose = false) => {
3728
+ const zeroDir = join4(home, ".zero");
3729
+ const configPath = join4(zeroDir, "config.json");
3730
+ let removed = false;
3731
+ if (existsSync3(configPath)) {
3732
+ rmSync2(configPath, { force: true });
3733
+ removed = true;
3734
+ if (verbose) stepInfo(`removed ${configPath}`);
3735
+ }
3736
+ if (existsSync3(zeroDir) && readdirSync2(zeroDir).length === 0) {
3737
+ rmSync2(zeroDir, { recursive: true, force: true });
3738
+ if (verbose) stepInfo(`removed empty ${zeroDir}`);
3739
+ }
3740
+ return removed;
3741
+ };
3742
+ var runUninstall = async (appContext, options = {}) => {
3743
+ const verbose = options.verbose ?? false;
3744
+ const purge = options.purge ?? false;
3745
+ appContext.services.analyticsService.capture("uninstall_started", { purge });
3746
+ let currentStep = "skills";
3747
+ try {
3748
+ console.log("");
3749
+ console.log(` ${color.boldCyan("Uninstalling Zero")}`);
3750
+ console.log("");
3751
+ console.log(color.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
3752
+ console.log("");
3753
+ const home = homedir4();
3754
+ currentStep = "skills";
3755
+ const skillsRemoved = removeSkills(home, verbose);
3756
+ if (skillsRemoved.length > 0) {
3757
+ stepSuccess("Skills removed", `${skillsRemoved.length} removed`);
3758
+ } else {
3759
+ stepSkip("No Zero skills found");
3760
+ }
3761
+ currentStep = "hooks";
3762
+ const { settingsChanged, scriptsRemoved } = removeHooks(home, verbose);
3763
+ if (settingsChanged || scriptsRemoved > 0) {
3764
+ stepSuccess("Hooks removed");
3765
+ } else {
3766
+ stepSkip("No Zero hooks found");
3767
+ }
3768
+ currentStep = "wallet";
3769
+ let walletRemoved = false;
3770
+ if (purge) {
3771
+ walletRemoved = removeWallet(home, verbose);
3772
+ if (walletRemoved) {
3773
+ stepWarn("Wallet config deleted", "~/.zero/config.json");
3774
+ } else {
3775
+ stepSkip("No wallet config to delete");
3776
+ }
3777
+ } else {
3778
+ stepSkip(
3779
+ "Wallet preserved",
3780
+ "run `zero uninstall --purge` to also delete ~/.zero"
3781
+ );
3782
+ }
3783
+ currentStep = "complete";
3784
+ appContext.services.analyticsService.capture("uninstall_completed", {
3785
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
3786
+ skills_removed: skillsRemoved,
3787
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
3788
+ skills_removed_count: skillsRemoved.length,
3789
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
3790
+ hooks_removed: settingsChanged,
3791
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
3792
+ hook_scripts_removed: scriptsRemoved,
3793
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
3794
+ wallet_removed: walletRemoved,
3795
+ purge
3796
+ });
3797
+ sectionDivider();
3798
+ console.log(` ${color.boldGreen("Zero uninstalled.")}`);
3799
+ if (!purge) {
3800
+ console.log(
3801
+ ` ${color.dim("Your wallet is kept at ~/.zero \u2014 reinstall anytime with `zero init`.")}`
3802
+ );
3803
+ }
3804
+ console.log("");
3805
+ return {
3806
+ skillsRemoved,
3807
+ hooksRemoved: settingsChanged,
3808
+ hookScriptsRemoved: scriptsRemoved,
3809
+ walletRemoved
3810
+ };
3811
+ } catch (err) {
3812
+ appContext.services.analyticsService.capture("uninstall_failed", {
3813
+ step: currentStep,
3814
+ error: truncateError(err instanceof Error ? err.message : String(err)),
3815
+ purge
3816
+ });
3817
+ throw err;
3818
+ }
3819
+ };
3820
+ var uninstallCommand = (appContext) => new Command11("uninstall").description(
3821
+ "Remove Zero skills and hooks installed by `zero init` (keeps your wallet)"
3822
+ ).option(
3823
+ "--purge",
3824
+ "Also delete the wallet config at ~/.zero (irreversible \u2014 destroys the private key)"
3825
+ ).option(
3826
+ "-v, --verbose",
3827
+ "Explain why each removal step was taken or skipped"
3828
+ ).action(async (options) => {
3829
+ await runUninstall(appContext, options);
3830
+ });
3831
+
3832
+ // src/commands/wallet-command.ts
3833
+ import { existsSync as existsSync4, readFileSync as readFileSync9 } from "fs";
3834
+ import { homedir as homedir5 } from "os";
3835
+ import { join as join6 } from "path";
3836
+ import { confirm, isCancel } from "@clack/prompts";
3837
+ import { Command as Command12 } from "commander";
3472
3838
  import open2 from "open";
3473
3839
  import { isAddress } from "viem";
3474
3840
  import { generatePrivateKey as generatePrivateKey2, privateKeyToAccount as privateKeyToAccount2 } from "viem/accounts";
3475
3841
 
3842
+ // src/util/migrate-config.ts
3843
+ import { readFileSync as readFileSync8 } from "fs";
3844
+ import { join as join5 } from "path";
3845
+ var backupAndStripPrivateKey = (zeroDir) => {
3846
+ const configPath = join5(zeroDir, "config.json");
3847
+ const backupPath = join5(zeroDir, "config.backup.json");
3848
+ const raw = readFileSync8(configPath, "utf8");
3849
+ ensureSecureDir(zeroDir);
3850
+ writeSecureFile(backupPath, raw);
3851
+ const parsed = JSON.parse(raw);
3852
+ delete parsed.privateKey;
3853
+ writeSecureFile(configPath, JSON.stringify(parsed, null, 2));
3854
+ return { backupPath, configPath };
3855
+ };
3856
+
3476
3857
  // src/util/stdin.ts
3477
3858
  var readStdin = async () => {
3478
3859
  const chunks = [];
@@ -3485,7 +3866,7 @@ var readStdin = async () => {
3485
3866
  // src/commands/wallet-command.ts
3486
3867
  var PRIVATE_KEY_PATTERN = /^0x[0-9a-fA-F]{64}$/;
3487
3868
  var parseProvider = (raw) => raw === "stripe" ? "stripe" : "coinbase";
3488
- var walletBalanceCommand = (appContext) => new Command11("balance").description("Show wallet balance").option(
3869
+ var walletBalanceCommand = (appContext) => new Command12("balance").description("Show wallet balance").option(
3489
3870
  "--address <address>",
3490
3871
  "Check balance for an arbitrary address (no key needed). Useful with `zero wallet generate` \u2014 verify funds arrived without setting the wallet as your default."
3491
3872
  ).action(async (options) => {
@@ -3534,7 +3915,7 @@ var readPrivateKeyFromStdin = async () => {
3534
3915
  }
3535
3916
  return raw;
3536
3917
  };
3537
- var walletFundCommand = (appContext) => new Command11("fund").description("Fund your wallet").argument("[amount]", "Amount to fund in USDC").option("--manual", "Show wallet address for manual transfer").option(
3918
+ var walletFundCommand = (appContext) => new Command12("fund").description("Fund your wallet").argument("[amount]", "Amount to fund in USDC").option("--manual", "Show wallet address for manual transfer").option(
3538
3919
  "--no-open",
3539
3920
  "Print the funding URL instead of opening a browser (for agents \u2014 funding links are one-time use, hand the URL to the user)"
3540
3921
  ).option(
@@ -3631,7 +4012,7 @@ ${address}`);
3631
4012
  }
3632
4013
  }
3633
4014
  );
3634
- var walletAddressCommand = (appContext) => new Command11("address").description("Show wallet address").action(() => {
4015
+ var walletAddressCommand = (appContext) => new Command12("address").description("Show wallet address").action(() => {
3635
4016
  const { walletService } = appContext.services;
3636
4017
  const address = walletService.getAddress();
3637
4018
  if (!address) {
@@ -3641,7 +4022,7 @@ var walletAddressCommand = (appContext) => new Command11("address").description(
3641
4022
  }
3642
4023
  console.log(address);
3643
4024
  });
3644
- var walletSetCommand = (appContext) => new Command11("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) => {
4025
+ var walletSetCommand = (appContext) => new Command12("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) => {
3645
4026
  const { analyticsService } = appContext.services;
3646
4027
  if (!privateKey.startsWith("0x")) {
3647
4028
  console.error("Private key must be 0x-prefixed hex string.");
@@ -3656,11 +4037,11 @@ var walletSetCommand = (appContext) => new Command11("set").description("Set wal
3656
4037
  process.exitCode = 1;
3657
4038
  return;
3658
4039
  }
3659
- const zeroDir = join4(homedir4(), ".zero");
3660
- const configPath = join4(zeroDir, "config.json");
3661
- if (!options.force && existsSync3(configPath)) {
4040
+ const zeroDir = join6(homedir5(), ".zero");
4041
+ const configPath = join6(zeroDir, "config.json");
4042
+ if (!options.force && existsSync4(configPath)) {
3662
4043
  try {
3663
- const existing2 = JSON.parse(readFileSync7(configPath, "utf8"));
4044
+ const existing2 = JSON.parse(readFileSync9(configPath, "utf8"));
3664
4045
  if (existing2.privateKey) {
3665
4046
  console.error(
3666
4047
  "Wallet already configured. Use --force to overwrite."
@@ -3672,7 +4053,7 @@ var walletSetCommand = (appContext) => new Command11("set").description("Set wal
3672
4053
  }
3673
4054
  }
3674
4055
  ensureSecureDir(zeroDir);
3675
- const existing = existsSync3(configPath) ? JSON.parse(readFileSync7(configPath, "utf8")) : {};
4056
+ const existing = existsSync4(configPath) ? JSON.parse(readFileSync9(configPath, "utf8")) : {};
3676
4057
  writeSecureFile(
3677
4058
  configPath,
3678
4059
  JSON.stringify(
@@ -3691,7 +4072,7 @@ var walletSetCommand = (appContext) => new Command11("set").description("Set wal
3691
4072
  force: options.force ?? false
3692
4073
  });
3693
4074
  });
3694
- var walletGenerateCommand = (appContext) => new Command11("generate").description(
4075
+ var walletGenerateCommand = (appContext) => new Command12("generate").description(
3695
4076
  "Generate a fresh wallet (address + private key) without touching your configured wallet"
3696
4077
  ).option("--json", "Emit { address, privateKey } as JSON").option(
3697
4078
  "--fund",
@@ -3768,29 +4149,166 @@ var walletGenerateCommand = (appContext) => new Command11("generate").descriptio
3768
4149
  });
3769
4150
  }
3770
4151
  );
4152
+ var resolveZeroWalletAddress = async (apiService) => {
4153
+ try {
4154
+ const wallets = await apiService.getWallets();
4155
+ const primary = wallets.find(
4156
+ (w) => w.source === "privy_embedded" && w.isPrimary
4157
+ );
4158
+ if (primary) return primary.walletAddress;
4159
+ const provisioned = await apiService.provisionWallet();
4160
+ return provisioned.walletAddress ?? null;
4161
+ } catch {
4162
+ return null;
4163
+ }
4164
+ };
4165
+ var walletMigrateCommand = (appContext) => new Command12("migrate").description(
4166
+ "Sweep all USDC from your private-key wallet into your Zero wallet"
4167
+ ).option("--json", "Emit the migration result as JSON").option("-y, --yes", "Skip the confirmation prompt").action(async (options) => {
4168
+ const { apiService, paymentService } = appContext.services;
4169
+ const zeroDir = join6(homedir5(), ".zero");
4170
+ const configPath = join6(zeroDir, "config.json");
4171
+ const config = existsSync4(configPath) ? readConfig(configPath) : {};
4172
+ const privateKey = appContext.env.ZERO_PRIVATE_KEY ?? config.privateKey;
4173
+ if (!privateKey) {
4174
+ console.error(
4175
+ "No private-key wallet found. Run `zero wallet set <key>` first."
4176
+ );
4177
+ process.exitCode = 1;
4178
+ return;
4179
+ }
4180
+ const hasSession = Boolean(
4181
+ appContext.env.ZERO_SESSION_TOKEN || config.session
4182
+ );
4183
+ if (!hasSession) {
4184
+ console.error("Not logged in. Run `zero auth login` first.");
4185
+ process.exitCode = 1;
4186
+ return;
4187
+ }
4188
+ let source;
4189
+ try {
4190
+ source = privateKeyToAccount2(privateKey);
4191
+ } catch {
4192
+ console.error("Invalid private key in config.");
4193
+ process.exitCode = 1;
4194
+ return;
4195
+ }
4196
+ const to = await resolveZeroWalletAddress(apiService);
4197
+ if (!to) {
4198
+ console.error(
4199
+ "Could not resolve your Zero wallet. Run `zero auth login` and try again."
4200
+ );
4201
+ process.exitCode = 1;
4202
+ return;
4203
+ }
4204
+ if (to.toLowerCase() === source.address.toLowerCase()) {
4205
+ console.error(
4206
+ "Source and destination are the same wallet \u2014 nothing to migrate."
4207
+ );
4208
+ process.exitCode = 1;
4209
+ return;
4210
+ }
4211
+ const balance = await paymentService.getSweepableBalance();
4212
+ if (balance.raw <= 0n) {
4213
+ if (options.json) {
4214
+ console.log(
4215
+ JSON.stringify({ destination: to, legs: [], keyRemoved: false })
4216
+ );
4217
+ } else {
4218
+ console.log("Nothing to migrate (no USDC found).");
4219
+ }
4220
+ return;
4221
+ }
4222
+ if (!options.yes) {
4223
+ if (options.json || !process.stdin.isTTY) {
4224
+ console.error(
4225
+ "Refusing to migrate without confirmation. Re-run with --yes to proceed."
4226
+ );
4227
+ process.exitCode = 1;
4228
+ return;
4229
+ }
4230
+ const proceed = await confirm({
4231
+ message: `Please confirm you would like to transfer $${balance.usdc} from your private wallet to your Zero managed wallet`
4232
+ });
4233
+ if (isCancel(proceed) || !proceed) {
4234
+ console.log("Migration cancelled.");
4235
+ return;
4236
+ }
4237
+ }
4238
+ const results = await paymentService.migrateAllUsdc(to, apiService);
4239
+ const anyFailed = results.some((r) => r.status === "failed");
4240
+ const anySwept = results.some((r) => r.status === "swept");
4241
+ let backupPath = null;
4242
+ if (anySwept && !anyFailed && config.privateKey) {
4243
+ try {
4244
+ backupPath = backupAndStripPrivateKey(zeroDir).backupPath;
4245
+ } catch {
4246
+ backupPath = null;
4247
+ }
4248
+ }
4249
+ if (options.json) {
4250
+ console.log(
4251
+ JSON.stringify({
4252
+ destination: to,
4253
+ legs: results,
4254
+ keyRemoved: backupPath !== null,
4255
+ backupPath
4256
+ })
4257
+ );
4258
+ } else {
4259
+ console.log(`Migrating USDC \u2192 ${to}`);
4260
+ for (const r of results) {
4261
+ if (r.status === "swept") {
4262
+ console.log(
4263
+ ` ${r.chain}: swept ${r.amount} USDC (tx ${r.txHash})`
4264
+ );
4265
+ } else if (r.status === "skipped") {
4266
+ console.log(` ${r.chain}: skipped (no balance)`);
4267
+ } else {
4268
+ console.log(` ${r.chain}: FAILED \u2014 ${r.error}`);
4269
+ }
4270
+ }
4271
+ if (backupPath) {
4272
+ console.log("");
4273
+ console.log(
4274
+ `Private key removed from config (backed up to ${backupPath}). Your Zero wallet is now primary.`
4275
+ );
4276
+ } else if (anyFailed) {
4277
+ console.log("");
4278
+ console.log(
4279
+ "A transfer failed \u2014 your private key was kept. Re-run `zero wallet migrate` to retry."
4280
+ );
4281
+ } else if (!anySwept) {
4282
+ console.log("");
4283
+ console.log("Nothing to migrate (no USDC found).");
4284
+ }
4285
+ }
4286
+ if (anyFailed) process.exitCode = 1;
4287
+ });
3771
4288
  var walletCommand = (appContext) => {
3772
- const cmd = new Command11("wallet").description("Manage your wallet");
4289
+ const cmd = new Command12("wallet").description("Manage your wallet");
3773
4290
  cmd.addCommand(walletBalanceCommand(appContext));
3774
4291
  cmd.addCommand(walletFundCommand(appContext));
3775
4292
  cmd.addCommand(walletAddressCommand(appContext));
3776
4293
  cmd.addCommand(walletSetCommand(appContext));
3777
4294
  cmd.addCommand(walletGenerateCommand(appContext));
4295
+ cmd.addCommand(walletMigrateCommand(appContext), { hidden: true });
3778
4296
  return cmd;
3779
4297
  };
3780
4298
 
3781
4299
  // src/commands/welcome-command.ts
3782
- import { existsSync as existsSync4, readFileSync as readFileSync8 } from "fs";
3783
- import { homedir as homedir5 } from "os";
3784
- import { join as join5 } from "path";
3785
- import { Command as Command12 } from "commander";
4300
+ import { existsSync as existsSync5, readFileSync as readFileSync10 } from "fs";
4301
+ import { homedir as homedir6 } from "os";
4302
+ import { join as join7 } from "path";
4303
+ import { Command as Command13 } from "commander";
3786
4304
  import open3 from "open";
3787
4305
  import { getAddress } from "viem";
3788
4306
  import { privateKeyToAccount as privateKeyToAccount3 } from "viem/accounts";
3789
4307
  var readPrivateKey = () => {
3790
- const configPath = join5(homedir5(), ".zero", "config.json");
3791
- if (!existsSync4(configPath)) return null;
4308
+ const configPath = join7(homedir6(), ".zero", "config.json");
4309
+ if (!existsSync5(configPath)) return null;
3792
4310
  try {
3793
- const config = JSON.parse(readFileSync8(configPath, "utf8"));
4311
+ const config = JSON.parse(readFileSync10(configPath, "utf8"));
3794
4312
  if (typeof config.privateKey === "string") {
3795
4313
  return config.privateKey;
3796
4314
  }
@@ -3825,7 +4343,7 @@ var printManualFallback = (url) => {
3825
4343
  }
3826
4344
  console.log(lines.join("\n"));
3827
4345
  };
3828
- var welcomeCommand = (appContext) => new Command12("welcome").description("Claim your $5 welcome bonus.").action(async () => {
4346
+ var welcomeCommand = (appContext) => new Command13("welcome").description("Claim your $5 welcome bonus.").action(async () => {
3829
4347
  const { analyticsService } = appContext.services;
3830
4348
  analyticsService.capture("welcome_started", {});
3831
4349
  let walletAddress;
@@ -3885,7 +4403,7 @@ If your browser didn't open, paste the URL above.`
3885
4403
  // src/app.ts
3886
4404
  var createApp = (appContext) => {
3887
4405
  const { analyticsService } = appContext.services;
3888
- const program = new Command13().name("zero").description("Zero CLI \u2014 Search engine for AI agents").version(package_default.version, "-v, --version").exitOverride().hook("preAction", async (_thisCommand, actionCommand) => {
4406
+ const program = new Command14().name("zero").description("Zero CLI \u2014 Search engine for AI agents").version(package_default.version, "-v, --version").exitOverride().hook("preAction", async (_thisCommand, actionCommand) => {
3889
4407
  const agentFlag = actionCommand.opts().agent;
3890
4408
  if (typeof agentFlag === "string" && agentFlag.trim().length > 0) {
3891
4409
  analyticsService.setAgentHost(agentFlag.trim());
@@ -3899,6 +4417,7 @@ var createApp = (appContext) => {
3899
4417
  });
3900
4418
  });
3901
4419
  program.addCommand(initCommand(appContext));
4420
+ program.addCommand(uninstallCommand(appContext));
3902
4421
  program.addCommand(searchCommand(appContext));
3903
4422
  program.addCommand(getCommand(appContext));
3904
4423
  program.addCommand(fetchCommand(appContext));
@@ -3934,14 +4453,14 @@ var getEnv = () => {
3934
4453
 
3935
4454
  // src/app/app-services.ts
3936
4455
  import { randomUUID as randomUUID2 } from "crypto";
3937
- import { existsSync as existsSync7 } from "fs";
3938
- import { homedir as homedir6 } from "os";
3939
- import { join as join7 } from "path";
4456
+ import { existsSync as existsSync8 } from "fs";
4457
+ import { homedir as homedir7 } from "os";
4458
+ import { join as join9 } from "path";
3940
4459
  import { privateKeyToAccount as privateKeyToAccount4 } from "viem/accounts";
3941
4460
 
3942
4461
  // src/services/analytics-service.ts
3943
4462
  import { randomUUID } from "crypto";
3944
- import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "fs";
4463
+ import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync11, writeFileSync as writeFileSync5 } from "fs";
3945
4464
  import { dirname as dirname2 } from "path";
3946
4465
  import { PostHog } from "posthog-node";
3947
4466
  var POSTHOG_API_KEY = "phc_B2vLyNxAf2mnqvdPQajf4d4b2iXc35dep2ZrvebMJLuX";
@@ -3963,8 +4482,8 @@ var AnalyticsService = class {
3963
4482
  let telemetryEnabled = true;
3964
4483
  let persistedAnonId;
3965
4484
  try {
3966
- if (existsSync5(opts.configPath)) {
3967
- const config = JSON.parse(readFileSync9(opts.configPath, "utf8"));
4485
+ if (existsSync6(opts.configPath)) {
4486
+ const config = JSON.parse(readFileSync11(opts.configPath, "utf8"));
3968
4487
  if (config.telemetry === false) {
3969
4488
  telemetryEnabled = false;
3970
4489
  }
@@ -3990,8 +4509,8 @@ var AnalyticsService = class {
3990
4509
  try {
3991
4510
  const dir = dirname2(opts.configPath);
3992
4511
  mkdirSync4(dir, { recursive: true });
3993
- const existing = existsSync5(opts.configPath) ? JSON.parse(readFileSync9(opts.configPath, "utf8")) : {};
3994
- writeFileSync4(
4512
+ const existing = existsSync6(opts.configPath) ? JSON.parse(readFileSync11(opts.configPath, "utf8")) : {};
4513
+ writeFileSync5(
3995
4514
  opts.configPath,
3996
4515
  JSON.stringify({ ...existing, anonId: newAnonId }, null, 2)
3997
4516
  );
@@ -4031,7 +4550,7 @@ var AnalyticsService = class {
4031
4550
  if (anonId === walletAddress) return;
4032
4551
  let aliasedTo;
4033
4552
  try {
4034
- const config = JSON.parse(readFileSync9(configPath, "utf8"));
4553
+ const config = JSON.parse(readFileSync11(configPath, "utf8"));
4035
4554
  if (typeof config.aliasedTo === "string") {
4036
4555
  aliasedTo = config.aliasedTo;
4037
4556
  }
@@ -4041,8 +4560,8 @@ var AnalyticsService = class {
4041
4560
  this.posthog.alias({ distinctId: walletAddress, alias: anonId });
4042
4561
  if (process.env.VITEST) return;
4043
4562
  try {
4044
- const config = existsSync5(configPath) ? JSON.parse(readFileSync9(configPath, "utf8")) : {};
4045
- writeFileSync4(
4563
+ const config = existsSync6(configPath) ? JSON.parse(readFileSync11(configPath, "utf8")) : {};
4564
+ writeFileSync5(
4046
4565
  configPath,
4047
4566
  JSON.stringify({ ...config, aliasedTo: walletAddress }, null, 2)
4048
4567
  );
@@ -4145,34 +4664,34 @@ var createApiAccount = (walletAddress, api) => toAccount({
4145
4664
  });
4146
4665
 
4147
4666
  // src/services/state-service.ts
4148
- import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
4149
- import { join as join6 } from "path";
4667
+ import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync12, writeFileSync as writeFileSync6 } from "fs";
4668
+ import { join as join8 } from "path";
4150
4669
  var RECENT_SEARCH_LIMIT = 10;
4151
4670
  var StateService = class {
4152
4671
  constructor(zeroDir) {
4153
4672
  this.zeroDir = zeroDir;
4154
- this.lastSearchPath = join6(zeroDir, "last_search.json");
4155
- this.recentSearchesPath = join6(zeroDir, "recent_searches.json");
4673
+ this.lastSearchPath = join8(zeroDir, "last_search.json");
4674
+ this.recentSearchesPath = join8(zeroDir, "recent_searches.json");
4156
4675
  }
4157
4676
  lastSearchPath;
4158
4677
  recentSearchesPath;
4159
4678
  saveLastSearch = (data) => {
4160
4679
  mkdirSync5(this.zeroDir, { recursive: true });
4161
- writeFileSync5(this.lastSearchPath, JSON.stringify(data, null, 2));
4680
+ writeFileSync6(this.lastSearchPath, JSON.stringify(data, null, 2));
4162
4681
  const recent = this.loadRecentSearches();
4163
4682
  const filtered = recent.searches.filter(
4164
4683
  (s) => s.searchId !== data.searchId
4165
4684
  );
4166
4685
  const next = [data, ...filtered].slice(0, RECENT_SEARCH_LIMIT);
4167
- writeFileSync5(
4686
+ writeFileSync6(
4168
4687
  this.recentSearchesPath,
4169
4688
  JSON.stringify({ searches: next }, null, 2)
4170
4689
  );
4171
4690
  };
4172
4691
  loadLastSearch = () => {
4173
4692
  try {
4174
- if (!existsSync6(this.lastSearchPath)) return null;
4175
- const raw = readFileSync10(this.lastSearchPath, "utf8");
4693
+ if (!existsSync7(this.lastSearchPath)) return null;
4694
+ const raw = readFileSync12(this.lastSearchPath, "utf8");
4176
4695
  return JSON.parse(raw);
4177
4696
  } catch {
4178
4697
  return null;
@@ -4180,11 +4699,11 @@ var StateService = class {
4180
4699
  };
4181
4700
  loadRecentSearches = () => {
4182
4701
  try {
4183
- if (!existsSync6(this.recentSearchesPath)) {
4702
+ if (!existsSync7(this.recentSearchesPath)) {
4184
4703
  const last = this.loadLastSearch();
4185
4704
  return { searches: last ? [last] : [] };
4186
4705
  }
4187
- const raw = readFileSync10(this.recentSearchesPath, "utf8");
4706
+ const raw = readFileSync12(this.recentSearchesPath, "utf8");
4188
4707
  const parsed = JSON.parse(raw);
4189
4708
  return { searches: parsed.searches ?? [] };
4190
4709
  } catch {
@@ -4312,9 +4831,9 @@ var buildOnSessionRefreshed = (configPath) => async (tokens) => {
4312
4831
  writeSecureFile(configPath, JSON.stringify(next, null, 2));
4313
4832
  };
4314
4833
  var getServices = async (env) => {
4315
- const zeroDir = join7(homedir6(), ".zero");
4316
- const configPath = join7(zeroDir, "config.json");
4317
- const config = existsSync7(configPath) ? readConfig(configPath) : {};
4834
+ const zeroDir = join9(homedir7(), ".zero");
4835
+ const configPath = join9(zeroDir, "config.json");
4836
+ const config = existsSync8(configPath) ? readConfig(configPath) : {};
4318
4837
  const { credentials, privateKey } = resolveCredentials(env, config);
4319
4838
  const lowBalanceWarning = typeof config.lowBalanceWarning === "number" ? config.lowBalanceWarning : 1;
4320
4839
  const apiService = new ApiService(
@@ -4324,20 +4843,17 @@ var getServices = async (env) => {
4324
4843
  buildOnSessionRefreshed(configPath)
4325
4844
  );
4326
4845
  let account = privateKey ? privateKeyToAccount4(privateKey) : null;
4327
- let managed = false;
4328
4846
  if (!account && credentials.kind === "session") {
4329
4847
  const address = await resolveManagedWalletAddress(apiService);
4330
4848
  if (address) {
4331
4849
  account = createApiAccount(address, apiService);
4332
- managed = true;
4333
4850
  }
4334
4851
  }
4335
4852
  if (account && !apiService.walletAddress) {
4336
4853
  apiService.setWalletAddress(account.address);
4337
4854
  }
4338
4855
  const paymentService = new PaymentService(account, {
4339
- lowBalanceWarning,
4340
- managed
4856
+ lowBalanceWarning
4341
4857
  });
4342
4858
  const stateService = new StateService(zeroDir);
4343
4859
  const walletService = new WalletService(
@@ -4390,15 +4906,15 @@ var createAppContext = async () => {
4390
4906
 
4391
4907
  // src/util/update-check.ts
4392
4908
  import {
4393
- existsSync as existsSync8,
4909
+ existsSync as existsSync9,
4394
4910
  lstatSync,
4395
4911
  mkdirSync as mkdirSync6,
4396
- readFileSync as readFileSync11,
4912
+ readFileSync as readFileSync13,
4397
4913
  readlinkSync,
4398
- writeFileSync as writeFileSync6
4914
+ writeFileSync as writeFileSync7
4399
4915
  } from "fs";
4400
- import { homedir as homedir7 } from "os";
4401
- import { dirname as dirname3, join as join8, resolve } from "path";
4916
+ import { homedir as homedir8 } from "os";
4917
+ import { dirname as dirname3, join as join10, resolve } from "path";
4402
4918
  var CACHE_FILENAME = "update_check.json";
4403
4919
  var NPM_REGISTRY_URL = "https://registry.npmjs.org/@zeroxyz/cli/latest";
4404
4920
  var CHECK_INTERVAL_MS = 60 * 60 * 1e3;
@@ -4422,10 +4938,10 @@ var resolveExecPath = (execPath) => {
4422
4938
  var detectInstallMethod = (opts = {}) => {
4423
4939
  const execPath = opts.execPath ?? process.execPath;
4424
4940
  const pkg = opts.pkg ?? process.pkg;
4425
- const home = opts.home ?? homedir7();
4941
+ const home = opts.home ?? homedir8();
4426
4942
  if (pkg) return "binary";
4427
4943
  const resolved = resolveExecPath(execPath);
4428
- const zeroBin = join8(home, ".zero", "bin");
4944
+ const zeroBin = join10(home, ".zero", "bin");
4429
4945
  if (resolved.startsWith(zeroBin)) return "binary";
4430
4946
  return "npm";
4431
4947
  };
@@ -4450,12 +4966,12 @@ var compareVersions = (a, b) => {
4450
4966
  if (pb.pre === null) return -1;
4451
4967
  return pa.pre < pb.pre ? -1 : 1;
4452
4968
  };
4453
- var cachePath = (zeroDir) => join8(zeroDir, CACHE_FILENAME);
4969
+ var cachePath = (zeroDir) => join10(zeroDir, CACHE_FILENAME);
4454
4970
  var readCache = (zeroDir) => {
4455
4971
  try {
4456
4972
  const path = cachePath(zeroDir);
4457
- if (!existsSync8(path)) return emptyCache;
4458
- const raw = readFileSync11(path, "utf8");
4973
+ if (!existsSync9(path)) return emptyCache;
4974
+ const raw = readFileSync13(path, "utf8");
4459
4975
  const parsed = JSON.parse(raw);
4460
4976
  return {
4461
4977
  lastCheckedMs: typeof parsed.lastCheckedMs === "number" ? parsed.lastCheckedMs : 0,
@@ -4469,7 +4985,7 @@ var readCache = (zeroDir) => {
4469
4985
  var writeCache = (zeroDir, cache) => {
4470
4986
  try {
4471
4987
  mkdirSync6(zeroDir, { recursive: true });
4472
- writeFileSync6(cachePath(zeroDir), JSON.stringify(cache, null, 2));
4988
+ writeFileSync7(cachePath(zeroDir), JSON.stringify(cache, null, 2));
4473
4989
  } catch {
4474
4990
  }
4475
4991
  };
@@ -4544,7 +5060,7 @@ var main = async () => {
4544
5060
  console.error("Failed to create app context");
4545
5061
  process.exit(1);
4546
5062
  }
4547
- const zeroDir = join9(homedir8(), ".zero");
5063
+ const zeroDir = join11(homedir9(), ".zero");
4548
5064
  maybePrintUpdateBanner(zeroDir, package_default.version);
4549
5065
  const app = createApp(appContext);
4550
5066
  let caughtError = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeroxyz/cli",
3
- "version": "0.0.40",
3
+ "version": "0.0.42",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "zero": "dist/index.js",