@zeroxyz/cli 0.0.34 → 0.0.36

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 +406 -25
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,12 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // src/app.ts
4
- import { Command as Command12 } from "commander";
3
+ // src/index.ts
4
+ import { homedir as homedir7 } from "os";
5
+ import { join as join8 } from "path";
5
6
 
6
7
  // package.json
7
8
  var package_default = {
8
9
  name: "@zeroxyz/cli",
9
- version: "0.0.34",
10
+ version: "0.0.36",
10
11
  type: "module",
11
12
  bin: {
12
13
  zero: "dist/index.js",
@@ -59,6 +60,9 @@ var package_default = {
59
60
  }
60
61
  };
61
62
 
63
+ // src/app.ts
64
+ import { Command as Command12 } from "commander";
65
+
62
66
  // src/commands/bug-report-command.ts
63
67
  import { createHash as createHash2 } from "crypto";
64
68
  import { readFileSync } from "fs";
@@ -77,6 +81,7 @@ var searchResultSchema = z.object({
77
81
  description: z.string(),
78
82
  whatItDoes: z.string().nullable().optional(),
79
83
  url: z.string(),
84
+ urlTemplate: z.string().nullable().optional(),
80
85
  cost: z.object({ amount: z.string(), asset: z.string() }),
81
86
  rating: z.object({
82
87
  score: z.string(),
@@ -103,6 +108,7 @@ var capabilityResponseSchema = z.object({
103
108
  name: z.string(),
104
109
  description: z.string(),
105
110
  url: z.string(),
111
+ urlTemplate: z.string().nullable().optional(),
106
112
  method: z.string(),
107
113
  headers: z.record(z.string(), z.string()).nullable(),
108
114
  bodySchema: z.record(z.string(), z.unknown()).nullable(),
@@ -617,6 +623,7 @@ var configCommand = (_appContext) => new Command2("config").description("View or
617
623
  });
618
624
 
619
625
  // src/commands/fetch-command.ts
626
+ import { createHash as createHash3 } from "crypto";
620
627
  import { readFileSync as readFileSync3 } from "fs";
621
628
  import { resolve as resolvePath } from "path";
622
629
  import { Command as Command3 } from "commander";
@@ -740,6 +747,7 @@ var pickSessionCloseAmount = (receipt, openTimeCumulative) => {
740
747
  const accepted = BigInt(receipt.acceptedCumulative);
741
748
  const spent = BigInt(receipt.spent);
742
749
  const fromReceipt = accepted > spent ? accepted : spent;
750
+ if (receipt.metered === true) return fromReceipt;
743
751
  return fromReceipt > openTimeCumulative ? fromReceipt : openTimeCumulative;
744
752
  };
745
753
  var PaymentService = class {
@@ -1206,6 +1214,22 @@ var truncateQuery = (raw) => raw.length > QUERY_MAX ? `${raw.slice(0, QUERY_MAX)
1206
1214
  var truncateError = (raw) => raw.length > ERROR_MAX ? `${raw.slice(0, ERROR_MAX)}\u2026` : raw;
1207
1215
 
1208
1216
  // src/commands/fetch-command.ts
1217
+ var sniffJsonShape = (buf) => {
1218
+ let i = 0;
1219
+ if (buf.length >= 3 && buf[0] === 239 && buf[1] === 187 && buf[2] === 191) {
1220
+ i = 3;
1221
+ }
1222
+ while (i < buf.length && (buf[i] === 32 || buf[i] === 9 || buf[i] === 10 || buf[i] === 13)) {
1223
+ i++;
1224
+ }
1225
+ if (i >= buf.length) return false;
1226
+ return buf[i] === 123 || buf[i] === 91;
1227
+ };
1228
+ var urlMatchesTemplate = (url, template) => {
1229
+ const escaped = template.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1230
+ const withCaptures = escaped.replace(/\\\{[^/\\}]+\\\}/g, "[^/]+");
1231
+ return new RegExp(`^${withCaptures}$`).test(url);
1232
+ };
1209
1233
  var isTextContentType = (contentType) => {
1210
1234
  if (!contentType) return true;
1211
1235
  const ct = contentType.toLowerCase().split(";")[0]?.trim() ?? "";
@@ -1325,6 +1349,7 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1325
1349
  const startTime = Date.now();
1326
1350
  let resolvedUrl;
1327
1351
  let resolvedMethodFromCapability;
1352
+ let resolvedCapabilityName;
1328
1353
  if (!url) {
1329
1354
  if (!options.capability) {
1330
1355
  console.error(
@@ -1337,6 +1362,7 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1337
1362
  const cap = await apiService.getCapability(options.capability);
1338
1363
  resolvedUrl = cap.url;
1339
1364
  resolvedMethodFromCapability = cap.method;
1365
+ resolvedCapabilityName = cap.name;
1340
1366
  } catch (err) {
1341
1367
  console.error(
1342
1368
  `Failed to resolve --capability ${options.capability}: ${err instanceof Error ? err.message : String(err)}`
@@ -1347,6 +1373,47 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1347
1373
  } else {
1348
1374
  resolvedUrl = url;
1349
1375
  }
1376
+ if (!url && options.capability && !stateService.findSearchContextByCapability(options.capability)) {
1377
+ try {
1378
+ let capName = resolvedCapabilityName;
1379
+ if (capName === void 0) {
1380
+ const cap = await apiService.getCapability(options.capability);
1381
+ capName = cap.name;
1382
+ }
1383
+ const searchResult = await apiService.search({ query: capName });
1384
+ const slugFoundInResults = searchResult.capabilities.some(
1385
+ (c) => c.slug === options.capability || c.id === options.capability
1386
+ );
1387
+ stateService.saveLastSearch({
1388
+ searchId: searchResult.searchId,
1389
+ capabilities: searchResult.capabilities.map((c) => ({
1390
+ position: c.position,
1391
+ id: c.id,
1392
+ url: c.url,
1393
+ urlTemplate: c.urlTemplate ?? null,
1394
+ displayCostAmount: c.cost.amount
1395
+ }))
1396
+ });
1397
+ analyticsService.capture("search_executed", {
1398
+ query: truncateQuery(capName),
1399
+ queryLength: capName.length,
1400
+ resultCount: searchResult.capabilities.length,
1401
+ searchId: searchResult.searchId,
1402
+ total: searchResult.total,
1403
+ hasMore: searchResult.hasMore,
1404
+ json: false,
1405
+ triggeredBy: "slug_handoff",
1406
+ slugFoundInResults
1407
+ });
1408
+ analyticsService.capture("capability_viewed", {
1409
+ capabilityId: options.capability,
1410
+ fromLastSearch: false,
1411
+ searchId: searchResult.searchId,
1412
+ triggeredBy: "slug_handoff"
1413
+ });
1414
+ } catch {
1415
+ }
1416
+ }
1350
1417
  let resolvedBody;
1351
1418
  try {
1352
1419
  resolvedBody = resolveRequestBody(
@@ -1354,9 +1421,13 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1354
1421
  options.dataStdin ?? false
1355
1422
  );
1356
1423
  } catch (err) {
1357
- console.error(
1358
- err instanceof Error ? err.message : "Failed to read request body"
1359
- );
1424
+ const message = err instanceof Error ? err.message : "Failed to read request body";
1425
+ analyticsService.capture("fetch_error", {
1426
+ cliErrorClass: /exceeds the .* byte limit/.test(message) ? "payload_too_large" : "schema_validation_failed",
1427
+ url: redactUrl(resolvedUrl),
1428
+ error: truncateError(message)
1429
+ });
1430
+ console.error(message);
1360
1431
  process.exitCode = 1;
1361
1432
  return;
1362
1433
  }
@@ -1373,7 +1444,7 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1373
1444
  (k) => k.toLowerCase() === "content-type"
1374
1445
  );
1375
1446
  if (resolvedBody && !hasContentType) {
1376
- headers["content-type"] = Buffer.isBuffer(resolvedBody) ? "application/octet-stream" : "application/json";
1447
+ headers["content-type"] = Buffer.isBuffer(resolvedBody) ? sniffJsonShape(resolvedBody) ? "application/json" : "application/octet-stream" : "application/json";
1377
1448
  }
1378
1449
  const log = (msg) => console.error(` ${msg}`);
1379
1450
  const method = options.method ? options.method.toUpperCase() : resolvedMethodFromCapability && !resolvedBody ? resolvedMethodFromCapability.toUpperCase() : resolvedBody ? "POST" : "GET";
@@ -1382,12 +1453,21 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1382
1453
  headers,
1383
1454
  body: resolvedBody
1384
1455
  };
1385
- const lastSearch = stateService.loadLastSearch();
1386
- const matchedCapability = lastSearch?.capabilities.find(
1387
- (c) => resolvedUrl.startsWith(c.url)
1388
- );
1389
- const capabilityId = options.capability ?? matchedCapability?.id ?? null;
1390
- const searchId = matchedCapability ? lastSearch?.searchId : void 0;
1456
+ const explicitCapabilityCtx = options.capability ? stateService.findSearchContextByCapability(options.capability) : null;
1457
+ const urlCtx = stateService.findSearchContextByUrl(resolvedUrl, {
1458
+ matchTemplate: (template) => {
1459
+ try {
1460
+ return urlMatchesTemplate(resolvedUrl, template);
1461
+ } catch {
1462
+ return false;
1463
+ }
1464
+ }
1465
+ });
1466
+ const matchCtx = explicitCapabilityCtx ?? urlCtx;
1467
+ const capabilityId = options.capability ?? matchCtx?.capabilityId ?? null;
1468
+ const searchId = matchCtx?.searchId;
1469
+ const resultRank = matchCtx?.resultRank;
1470
+ const matchedDisplayCostAmount = matchCtx?.displayCostAmount;
1391
1471
  const skipReasons = [];
1392
1472
  if (!apiService.walletAddress) {
1393
1473
  skipReasons.push(
@@ -1398,6 +1478,19 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1398
1478
  skipReasons.push(
1399
1479
  "no capability resolved \u2014 pass --capability <uid|slug> or run `zero search` first so the URL can be matched"
1400
1480
  );
1481
+ if (!options.capability) {
1482
+ const lastSearch = stateService.loadLastSearch();
1483
+ const cached = lastSearch?.capabilities ?? [];
1484
+ analyticsService.capture("capability_resolution_missed", {
1485
+ url: redactUrl(resolvedUrl),
1486
+ method,
1487
+ hasLastSearch: lastSearch !== null,
1488
+ lastSearchSize: cached.length,
1489
+ lastSearchHasTemplates: cached.some(
1490
+ (c) => Boolean(c.urlTemplate)
1491
+ )
1492
+ });
1493
+ }
1401
1494
  }
1402
1495
  let finalResponse;
1403
1496
  let bodyBytes;
@@ -1420,7 +1513,7 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1420
1513
  paymentReq,
1421
1514
  options.maxPay,
1422
1515
  log,
1423
- matchedCapability?.displayCostAmount
1516
+ matchedDisplayCostAmount
1424
1517
  );
1425
1518
  finalResponse = result.response;
1426
1519
  paymentMeta = {
@@ -1517,6 +1610,15 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1517
1610
  } catch {
1518
1611
  }
1519
1612
  }
1613
+ const status = finalResponse?.status;
1614
+ const isFailure = !finalResponse || typeof status === "number" && (status < 200 || status >= 300);
1615
+ let errorSnippetHash;
1616
+ let errorSnippetLength;
1617
+ if (isFailure && body && !bodyIsBinary) {
1618
+ const snippet = body.slice(0, 500);
1619
+ errorSnippetHash = createHash3("sha256").update(snippet).digest("hex");
1620
+ errorSnippetLength = snippet.length;
1621
+ }
1520
1622
  let runId = null;
1521
1623
  if (capabilityId && apiService.walletAddress) {
1522
1624
  let requestSchema;
@@ -1541,6 +1643,8 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1541
1643
  latencyMs,
1542
1644
  requestSchema,
1543
1645
  responseSchema,
1646
+ errorSnippetHash,
1647
+ errorSnippetLength,
1544
1648
  ...paymentMeta && {
1545
1649
  costAmount: paymentMeta.amount,
1546
1650
  costAsset: paymentMeta.asset,
@@ -1557,7 +1661,6 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1557
1661
  );
1558
1662
  }
1559
1663
  }
1560
- const status = finalResponse?.status;
1561
1664
  const outcome = !finalResponse ? "network_error" : status === 402 && !paymentMeta ? "payment_failed" : status !== void 0 && status >= 400 && status !== 402 ? "server_error" : "success";
1562
1665
  analyticsService.capture("fetch_executed", {
1563
1666
  url: redactUrl(resolvedUrl),
@@ -1569,10 +1672,26 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1569
1672
  paymentAmount: paymentMeta?.amount,
1570
1673
  capabilityId: capabilityId ?? void 0,
1571
1674
  searchId: searchId ?? void 0,
1675
+ resultRank: resultRank ?? void 0,
1572
1676
  runId: runId ?? void 0,
1573
1677
  runTracked: !!runId,
1574
1678
  ...fetchError && { error: truncateError(fetchError.message) }
1575
1679
  });
1680
+ const isFetchFailure = Boolean(fetchError) || !finalResponse || typeof status === "number" && (status < 200 || status >= 300);
1681
+ if (isFetchFailure) {
1682
+ const cliErrorClass = fetchError || !finalResponse ? "network" : !apiService.walletAddress ? "auth_missing" : "unknown";
1683
+ analyticsService.capture("fetch_error", {
1684
+ cliErrorClass,
1685
+ capabilityId: capabilityId ?? void 0,
1686
+ searchId: searchId ?? void 0,
1687
+ resultRank: resultRank ?? void 0,
1688
+ url: redactUrl(resolvedUrl),
1689
+ error: truncateError(
1690
+ fetchError?.message ?? skipReasons.join("; ")
1691
+ ),
1692
+ skippedRun: !runId && skipReasons.length > 0
1693
+ });
1694
+ }
1576
1695
  if (fetchError && !options.json) {
1577
1696
  console.error(` Fetch failed: ${fetchError.message}`);
1578
1697
  }
@@ -1639,7 +1758,16 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1639
1758
  process.exitCode = 1;
1640
1759
  }
1641
1760
  } catch (err) {
1642
- console.error(err instanceof Error ? err.message : "Fetch failed");
1761
+ const message = err instanceof Error ? err.message : "Fetch failed";
1762
+ try {
1763
+ appContext.services.analyticsService.capture("fetch_error", {
1764
+ cliErrorClass: "unknown",
1765
+ url: url ? redactUrl(url) : "unknown",
1766
+ error: truncateError(message)
1767
+ });
1768
+ } catch {
1769
+ }
1770
+ console.error(message);
1643
1771
  process.exitCode = 1;
1644
1772
  }
1645
1773
  }
@@ -1855,6 +1983,8 @@ var getCommand = (appContext) => new Command4("get").description(
1855
1983
  searchId = lastSearch.searchId;
1856
1984
  } else {
1857
1985
  capabilityId = identifier;
1986
+ const ctx = stateService.findSearchContextByCapability(identifier);
1987
+ if (ctx) searchId = ctx.searchId;
1858
1988
  }
1859
1989
  const capability = await apiService.getCapability(
1860
1990
  capabilityId,
@@ -1878,7 +2008,7 @@ var getCommand = (appContext) => new Command4("get").description(
1878
2008
  });
1879
2009
 
1880
2010
  // src/commands/init-command.ts
1881
- import { createHash as createHash3 } from "crypto";
2011
+ import { createHash as createHash4 } from "crypto";
1882
2012
  import {
1883
2013
  chmodSync as chmodSync2,
1884
2014
  existsSync as existsSync2,
@@ -1952,10 +2082,12 @@ var color = {
1952
2082
  magenta: (s) => wrap("35", s),
1953
2083
  green: (s) => wrap("32", s),
1954
2084
  yellow: (s) => wrap("33", s),
2085
+ red: (s) => wrap("31", s),
1955
2086
  gray: (s) => wrap("90", s),
1956
2087
  boldCyan: (s) => wrap("1;36", s),
1957
2088
  boldMagenta: (s) => wrap("1;35", s),
1958
- boldGreen: (s) => wrap("1;32", s)
2089
+ boldGreen: (s) => wrap("1;32", s),
2090
+ boldRed: (s) => wrap("1;31", s)
1959
2091
  };
1960
2092
  var printZeroBanner = () => {
1961
2093
  console.log("");
@@ -1998,12 +2130,16 @@ var printReadyFooter = () => {
1998
2130
  const lines = [
1999
2131
  "",
2000
2132
  ` ${color.boldGreen("Zero is ready!")} Zero works best with an AI agent.`,
2001
- " Open Claude Code, Codex, Cursor, Blackbox, or your agent of choice",
2002
- " and try one of these commands to get started:",
2133
+ ` Open ${color.boldRed("Claude Code")}, Codex, Cursor, Blackbox, or your agent of choice`,
2134
+ " and try this prompt to get started:",
2003
2135
  "",
2004
- ` ${color.cyan('zero search "translate text to Spanish"')}`,
2005
- ` ${color.cyan('zero search "generate an image"')}`,
2006
- ` ${color.cyan('zero search "weather forecast"')}`,
2136
+ ` ${color.cyan("What is zero and how do I use it?")}`,
2137
+ "",
2138
+ " You can also just tell your agent examples of what you are trying to do:",
2139
+ "",
2140
+ ` ${color.cyan("Can you use zero to deploy an NYC weather website")}`,
2141
+ ` ${color.cyan("Email me an image of a crystalline rocket ship with zero")}`,
2142
+ ` ${color.cyan("Create a demo video with real voiceover for my project using zero")}`,
2007
2143
  "",
2008
2144
  ` ${color.dim("By using Zero, you agree to our Terms of Service:")}`,
2009
2145
  ` ${color.dim("https://zero.xyz/terms-of-service")}`,
@@ -2059,7 +2195,7 @@ var getCliModuleDir = () => {
2059
2195
  }
2060
2196
  return __dirname;
2061
2197
  };
2062
- var sha256File = (filePath) => createHash3("sha256").update(readFileSync4(filePath)).digest("hex");
2198
+ var sha256File = (filePath) => createHash4("sha256").update(readFileSync4(filePath)).digest("hex");
2063
2199
  var verifyFileCopy = (src, dest) => {
2064
2200
  if (!existsSync2(dest)) return false;
2065
2201
  return sha256File(src) === sha256File(dest);
@@ -2913,6 +3049,7 @@ var searchCommand = (appContext) => new Command8("search").description("Search f
2913
3049
  position: c.position,
2914
3050
  id: c.id,
2915
3051
  url: c.url,
3052
+ urlTemplate: c.urlTemplate ?? null,
2916
3053
  displayCostAmount: c.cost.amount
2917
3054
  }))
2918
3055
  });
@@ -3287,7 +3424,11 @@ var AnalyticsService = class {
3287
3424
  this.posthog = new PostHog(POSTHOG_API_KEY, {
3288
3425
  host: POSTHOG_HOST,
3289
3426
  flushAt: 1,
3290
- flushInterval: 0
3427
+ flushInterval: 0,
3428
+ // Vitest spins up many AnalyticsService instances per process; each
3429
+ // autocapture listener adds to process.{uncaughtException,unhandledRejection}
3430
+ // and trips Node's MaxListeners warning. Real CLI is one instance per process.
3431
+ enableExceptionAutocapture: !process.env.VITEST
3291
3432
  });
3292
3433
  this.posthog.on("error", () => {
3293
3434
  });
@@ -3346,6 +3487,22 @@ var AnalyticsService = class {
3346
3487
  }
3347
3488
  });
3348
3489
  }
3490
+ captureException(error, properties) {
3491
+ if (!this.posthog) return;
3492
+ this.posthog.captureException(error, this.distinctId, {
3493
+ source: "cli",
3494
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
3495
+ cli_version: this.cliVersion,
3496
+ environment: this.environment,
3497
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
3498
+ wallet_address: this.walletAddress,
3499
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
3500
+ request_id: this.requestId,
3501
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
3502
+ agent_host: this.agentHost,
3503
+ ...properties
3504
+ });
3505
+ }
3349
3506
  async shutdown() {
3350
3507
  if (!this.posthog) return;
3351
3508
  try {
@@ -3358,15 +3515,27 @@ var AnalyticsService = class {
3358
3515
  // src/services/state-service.ts
3359
3516
  import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "fs";
3360
3517
  import { join as join5 } from "path";
3518
+ var RECENT_SEARCH_LIMIT = 10;
3361
3519
  var StateService = class {
3362
3520
  constructor(zeroDir) {
3363
3521
  this.zeroDir = zeroDir;
3364
3522
  this.lastSearchPath = join5(zeroDir, "last_search.json");
3523
+ this.recentSearchesPath = join5(zeroDir, "recent_searches.json");
3365
3524
  }
3366
3525
  lastSearchPath;
3526
+ recentSearchesPath;
3367
3527
  saveLastSearch = (data) => {
3368
3528
  mkdirSync5(this.zeroDir, { recursive: true });
3369
3529
  writeFileSync5(this.lastSearchPath, JSON.stringify(data, null, 2));
3530
+ const recent = this.loadRecentSearches();
3531
+ const filtered = recent.searches.filter(
3532
+ (s) => s.searchId !== data.searchId
3533
+ );
3534
+ const next = [data, ...filtered].slice(0, RECENT_SEARCH_LIMIT);
3535
+ writeFileSync5(
3536
+ this.recentSearchesPath,
3537
+ JSON.stringify({ searches: next }, null, 2)
3538
+ );
3370
3539
  };
3371
3540
  loadLastSearch = () => {
3372
3541
  try {
@@ -3377,6 +3546,63 @@ var StateService = class {
3377
3546
  return null;
3378
3547
  }
3379
3548
  };
3549
+ loadRecentSearches = () => {
3550
+ try {
3551
+ if (!existsSync6(this.recentSearchesPath)) {
3552
+ const last = this.loadLastSearch();
3553
+ return { searches: last ? [last] : [] };
3554
+ }
3555
+ const raw = readFileSync9(this.recentSearchesPath, "utf8");
3556
+ const parsed = JSON.parse(raw);
3557
+ return { searches: parsed.searches ?? [] };
3558
+ } catch {
3559
+ return { searches: [] };
3560
+ }
3561
+ };
3562
+ // Walk recent searches newest-first, returning the first one whose
3563
+ // results contain `capabilityId` (uid or slug match). Preferred over
3564
+ // "loadLastSearch" for attributing rank — handles the parallel-search
3565
+ // case where the most recent search isn't the one being fetched.
3566
+ findSearchContextByCapability = (capabilityId) => {
3567
+ const recent = this.loadRecentSearches();
3568
+ for (const search of recent.searches) {
3569
+ const entry = search.capabilities.find((c) => c.id === capabilityId);
3570
+ if (entry) {
3571
+ return {
3572
+ searchId: search.searchId,
3573
+ resultRank: entry.position,
3574
+ capabilityId: entry.id,
3575
+ url: entry.url,
3576
+ displayCostAmount: entry.displayCostAmount
3577
+ };
3578
+ }
3579
+ }
3580
+ return null;
3581
+ };
3582
+ // URL-based attribution for `zero fetch <url>`. Matches by URL prefix
3583
+ // (some capabilities accept query params or path tails the agent appends)
3584
+ // across the recent ring, newest match wins. Falls back to a caller-
3585
+ // supplied urlTemplate matcher (ADS-509 path-parameterized URLs).
3586
+ findSearchContextByUrl = (url, options) => {
3587
+ const recent = this.loadRecentSearches();
3588
+ for (const search of recent.searches) {
3589
+ const prefixHit = search.capabilities.find((c) => url.startsWith(c.url));
3590
+ const templateHit = prefixHit ?? search.capabilities.find(
3591
+ (c) => c.urlTemplate && options?.matchTemplate ? options.matchTemplate(c.urlTemplate) : false
3592
+ );
3593
+ const entry = templateHit;
3594
+ if (entry) {
3595
+ return {
3596
+ searchId: search.searchId,
3597
+ resultRank: entry.position,
3598
+ capabilityId: entry.id,
3599
+ url: entry.url,
3600
+ displayCostAmount: entry.displayCostAmount
3601
+ };
3602
+ }
3603
+ }
3604
+ return null;
3605
+ };
3380
3606
  };
3381
3607
 
3382
3608
  // src/services/wallet-service.ts
@@ -3483,6 +3709,155 @@ var createAppContext = () => {
3483
3709
  };
3484
3710
  };
3485
3711
 
3712
+ // src/util/update-check.ts
3713
+ import {
3714
+ existsSync as existsSync8,
3715
+ lstatSync,
3716
+ mkdirSync as mkdirSync6,
3717
+ readFileSync as readFileSync11,
3718
+ readlinkSync,
3719
+ writeFileSync as writeFileSync6
3720
+ } from "fs";
3721
+ import { homedir as homedir6 } from "os";
3722
+ import { dirname as dirname3, join as join7, resolve } from "path";
3723
+ var CACHE_FILENAME = "update_check.json";
3724
+ var NPM_REGISTRY_URL = "https://registry.npmjs.org/@zeroxyz/cli/latest";
3725
+ var CHECK_INTERVAL_MS = 60 * 60 * 1e3;
3726
+ var FETCH_TIMEOUT_MS = 3e3;
3727
+ var emptyCache = {
3728
+ lastCheckedMs: 0,
3729
+ latestVersion: null,
3730
+ lastShownMs: 0
3731
+ };
3732
+ var resolveExecPath = (execPath) => {
3733
+ try {
3734
+ const stat = lstatSync(execPath);
3735
+ if (stat.isSymbolicLink()) {
3736
+ const target = readlinkSync(execPath);
3737
+ return resolve(dirname3(execPath), target);
3738
+ }
3739
+ } catch {
3740
+ }
3741
+ return execPath;
3742
+ };
3743
+ var detectInstallMethod = (opts = {}) => {
3744
+ const execPath = opts.execPath ?? process.execPath;
3745
+ const pkg = opts.pkg ?? process.pkg;
3746
+ const home = opts.home ?? homedir6();
3747
+ if (pkg) return "binary";
3748
+ const resolved = resolveExecPath(execPath);
3749
+ const zeroBin = join7(home, ".zero", "bin");
3750
+ if (resolved.startsWith(zeroBin)) return "binary";
3751
+ return "npm";
3752
+ };
3753
+ var compareVersions = (a, b) => {
3754
+ const parse = (v) => {
3755
+ const dashIdx = v.indexOf("-");
3756
+ const base2 = dashIdx === -1 ? v : v.slice(0, dashIdx);
3757
+ const pre = dashIdx === -1 ? null : v.slice(dashIdx + 1);
3758
+ const nums = base2.split(".").map((n) => Number.parseInt(n, 10) || 0);
3759
+ while (nums.length < 3) nums.push(0);
3760
+ return { nums, pre };
3761
+ };
3762
+ const pa = parse(a);
3763
+ const pb = parse(b);
3764
+ for (let i = 0; i < 3; i++) {
3765
+ const na = pa.nums[i] ?? 0;
3766
+ const nb = pb.nums[i] ?? 0;
3767
+ if (na !== nb) return na - nb;
3768
+ }
3769
+ if (pa.pre === pb.pre) return 0;
3770
+ if (pa.pre === null) return 1;
3771
+ if (pb.pre === null) return -1;
3772
+ return pa.pre < pb.pre ? -1 : 1;
3773
+ };
3774
+ var cachePath = (zeroDir) => join7(zeroDir, CACHE_FILENAME);
3775
+ var readCache = (zeroDir) => {
3776
+ try {
3777
+ const path = cachePath(zeroDir);
3778
+ if (!existsSync8(path)) return emptyCache;
3779
+ const raw = readFileSync11(path, "utf8");
3780
+ const parsed = JSON.parse(raw);
3781
+ return {
3782
+ lastCheckedMs: typeof parsed.lastCheckedMs === "number" ? parsed.lastCheckedMs : 0,
3783
+ latestVersion: typeof parsed.latestVersion === "string" ? parsed.latestVersion : null,
3784
+ lastShownMs: typeof parsed.lastShownMs === "number" ? parsed.lastShownMs : 0
3785
+ };
3786
+ } catch {
3787
+ return emptyCache;
3788
+ }
3789
+ };
3790
+ var writeCache = (zeroDir, cache) => {
3791
+ try {
3792
+ mkdirSync6(zeroDir, { recursive: true });
3793
+ writeFileSync6(cachePath(zeroDir), JSON.stringify(cache, null, 2));
3794
+ } catch {
3795
+ }
3796
+ };
3797
+ var fetchLatestVersion = async (url = NPM_REGISTRY_URL) => {
3798
+ try {
3799
+ const controller = new AbortController();
3800
+ const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
3801
+ const response = await fetch(url, {
3802
+ signal: controller.signal,
3803
+ headers: { accept: "application/json" }
3804
+ });
3805
+ clearTimeout(timeout);
3806
+ if (!response.ok) return null;
3807
+ const json = await response.json();
3808
+ return typeof json.version === "string" ? json.version : null;
3809
+ } catch {
3810
+ return null;
3811
+ }
3812
+ };
3813
+ var refreshUpdateCache = async (zeroDir, opts = {}) => {
3814
+ const now = opts.now ?? Date.now();
3815
+ const fetcher = opts.fetchLatest ?? fetchLatestVersion;
3816
+ const cache = readCache(zeroDir);
3817
+ if (cache.lastCheckedMs > 0 && now - cache.lastCheckedMs < CHECK_INTERVAL_MS) {
3818
+ return;
3819
+ }
3820
+ const latest = await fetcher();
3821
+ if (!latest) {
3822
+ writeCache(zeroDir, { ...cache, lastCheckedMs: now });
3823
+ return;
3824
+ }
3825
+ writeCache(zeroDir, {
3826
+ ...cache,
3827
+ lastCheckedMs: now,
3828
+ latestVersion: latest
3829
+ });
3830
+ };
3831
+ var updateCommandFor = (method) => method === "binary" ? "curl -fsSL https://zero.xyz/install.sh | bash" : "npm install -g @zeroxyz/cli";
3832
+ var formatBanner = (currentVersion, latestVersion, method) => {
3833
+ const arrow = `${currentVersion} \u2192 ${latestVersion}`;
3834
+ const cmd = updateCommandFor(method);
3835
+ const lines = [
3836
+ "",
3837
+ ` ${color.yellow("\u26A1 Update available")} ${color.dim(arrow)}`,
3838
+ ` ${color.dim("Run:")} ${color.cyan(cmd)}`,
3839
+ ""
3840
+ ];
3841
+ return lines.join("\n");
3842
+ };
3843
+ var consumeBannerIfDue = (zeroDir, currentVersion, opts = {}) => {
3844
+ const now = opts.now ?? Date.now();
3845
+ const method = opts.method ?? detectInstallMethod();
3846
+ const cache = readCache(zeroDir);
3847
+ if (!cache.latestVersion) return null;
3848
+ if (compareVersions(currentVersion, cache.latestVersion) >= 0) return null;
3849
+ if (cache.lastShownMs > 0 && now - cache.lastShownMs < CHECK_INTERVAL_MS) {
3850
+ return null;
3851
+ }
3852
+ writeCache(zeroDir, { ...cache, lastShownMs: now });
3853
+ return formatBanner(currentVersion, cache.latestVersion, method);
3854
+ };
3855
+ var maybePrintUpdateBanner = (zeroDir, currentVersion) => {
3856
+ if (!process.stderr.isTTY) return;
3857
+ const banner = consumeBannerIfDue(zeroDir, currentVersion);
3858
+ if (banner) process.stderr.write(banner);
3859
+ };
3860
+
3486
3861
  // src/index.ts
3487
3862
  var main = async () => {
3488
3863
  const appContext = createAppContext();
@@ -3490,6 +3865,8 @@ var main = async () => {
3490
3865
  console.error("Failed to create app context");
3491
3866
  process.exit(1);
3492
3867
  }
3868
+ const zeroDir = join8(homedir7(), ".zero");
3869
+ maybePrintUpdateBanner(zeroDir, package_default.version);
3493
3870
  const app = createApp(appContext);
3494
3871
  let caughtError = null;
3495
3872
  try {
@@ -3500,6 +3877,9 @@ var main = async () => {
3500
3877
  caughtError = err instanceof Error ? err : new Error(String(err));
3501
3878
  console.error(caughtError.message);
3502
3879
  process.exitCode = 1;
3880
+ appContext.services.analyticsService.captureException(caughtError, {
3881
+ command: appContext.invocation.current?.command
3882
+ });
3503
3883
  }
3504
3884
  } finally {
3505
3885
  const invocation = appContext.invocation.current;
@@ -3513,6 +3893,7 @@ var main = async () => {
3513
3893
  });
3514
3894
  }
3515
3895
  await appContext.services.analyticsService.shutdown();
3896
+ await refreshUpdateCache(zeroDir);
3516
3897
  }
3517
3898
  };
3518
3899
  main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeroxyz/cli",
3
- "version": "0.0.34",
3
+ "version": "0.0.36",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "zero": "dist/index.js",