@zeroxyz/cli 0.0.25 → 0.0.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import { Command as Command11 } from "commander";
6
6
  // package.json
7
7
  var package_default = {
8
8
  name: "@zeroxyz/cli",
9
- version: "0.0.25",
9
+ version: "0.0.26",
10
10
  type: "module",
11
11
  bin: {
12
12
  zero: "dist/index.js",
@@ -24,8 +24,8 @@ var package_default = {
24
24
  build: "tsup src/index.ts --format esm --out-dir dist --clean",
25
25
  "build:binary": "tsup --config tsup.binary.ts && cp -r skills hooks dist/pkg/ && pnpm exec pkg dist/pkg/index.cjs --config pkg.json --targets node24-macos-arm64,node24-macos-x64,node24-linux-x64 --output dist/bin/zero",
26
26
  prepublishOnly: "pnpm run build",
27
- dev: "tsx src/index.ts",
28
- cli: "ZERO_API_URL=http://localhost:1111 tsx src/index.ts",
27
+ dev: "ZERO_ENV=development tsx src/index.ts",
28
+ cli: "ZERO_ENV=development ZERO_API_URL=http://localhost:1111 tsx src/index.ts",
29
29
  "test:integration": "vitest run --project integration",
30
30
  "test:online": "vitest run --project online",
31
31
  "test:unit": "vitest run --project unit",
@@ -380,7 +380,10 @@ Categories the classifier picks from:
380
380
  ).option("--idempotency-key <key>", "Override the auto-generated dedup key").option(
381
381
  "--from-file <path>",
382
382
  "Submit bug reports in bulk from a JSONL file (one report per line)"
383
- ).option("--json", "Emit the API result as JSON on stdout (for batch use)").action(
383
+ ).option("--json", "Emit the API result as JSON on stdout (for batch use)").option(
384
+ "--agent <name>",
385
+ "Identify your agent host for this invocation. Overrides auto-detect for this call only."
386
+ ).action(
384
387
  async (description, options) => {
385
388
  try {
386
389
  const { analyticsService, apiService, stateService } = appContext.services;
@@ -421,7 +424,10 @@ Categories the classifier picks from:
421
424
  category: result2.category,
422
425
  severity: parsed.severity ?? 2,
423
426
  bulk: true,
424
- deduped: result2.deduped
427
+ deduped: result2.deduped,
428
+ hasCapability: !!result2.attached.capabilityId,
429
+ hasRun: !!result2.attached.runId,
430
+ hasSearch: !!result2.attached.searchId
425
431
  });
426
432
  } catch (err) {
427
433
  failed += 1;
@@ -506,9 +512,15 @@ Bulk bug-report complete: ${ok} ok, ${failed} failed`
506
512
  }
507
513
  analyticsService.capture("bug_report_submitted", {
508
514
  category: result.category,
515
+ categoryOverridden: !!options.category,
509
516
  severity: options.severity ?? 2,
510
517
  deduped: result.deduped,
511
- autoContext: useAutoContext
518
+ autoContext: useAutoContext,
519
+ hasCapability: !!result.attached.capabilityId,
520
+ hasRun: !!result.attached.runId,
521
+ hasSearch: !!result.attached.searchId,
522
+ hasTitle: !!options.title,
523
+ hasReproduction: !!options.reproduction
512
524
  });
513
525
  } catch (err) {
514
526
  console.error(
@@ -1078,6 +1090,18 @@ var PaymentService = class {
1078
1090
  const raw = await this.getBalanceRaw(chain);
1079
1091
  return { amount: formatUnits(raw, 6), asset: "USDC" };
1080
1092
  };
1093
+ /**
1094
+ * Total spendable USDC across Base and Tempo. Users (and their agents)
1095
+ * shouldn't need to know which chain holds funds — the CLI bridges to
1096
+ * Tempo on demand, so the reported balance sums both sides.
1097
+ */
1098
+ getTotalBalance = async () => {
1099
+ const [baseRaw, tempoRaw] = await Promise.all([
1100
+ this.getBalanceRaw("base"),
1101
+ this.getBalanceRaw("tempo")
1102
+ ]);
1103
+ return { amount: formatUnits(baseRaw + tempoRaw, 6), asset: "USDC" };
1104
+ };
1081
1105
  };
1082
1106
 
1083
1107
  // src/util/infer-schema.ts
@@ -1116,7 +1140,33 @@ var tryParseJson = (text) => {
1116
1140
  }
1117
1141
  };
1118
1142
 
1143
+ // src/util/redact.ts
1144
+ var ERROR_MAX = 500;
1145
+ var QUERY_MAX = 200;
1146
+ var redactUrl = (raw) => {
1147
+ try {
1148
+ const parsed = new URL(raw);
1149
+ return `${parsed.origin}${parsed.pathname}`;
1150
+ } catch {
1151
+ return raw;
1152
+ }
1153
+ };
1154
+ var truncateQuery = (raw) => raw.length > QUERY_MAX ? `${raw.slice(0, QUERY_MAX)}\u2026` : raw;
1155
+ var truncateError = (raw) => raw.length > ERROR_MAX ? `${raw.slice(0, ERROR_MAX)}\u2026` : raw;
1156
+
1119
1157
  // src/commands/fetch-command.ts
1158
+ var isTextContentType = (contentType) => {
1159
+ if (!contentType) return true;
1160
+ const ct = contentType.toLowerCase().split(";")[0]?.trim() ?? "";
1161
+ if (ct.startsWith("text/")) return true;
1162
+ if (ct === "application/json" || ct.endsWith("+json")) return true;
1163
+ if (ct === "application/xml" || ct.endsWith("+xml")) return true;
1164
+ if (ct === "application/javascript" || ct === "application/ecmascript") {
1165
+ return true;
1166
+ }
1167
+ if (ct === "application/x-www-form-urlencoded") return true;
1168
+ return false;
1169
+ };
1120
1170
  var detectPaymentRequirement = (headers, status) => {
1121
1171
  if (status !== 402) return null;
1122
1172
  const x402Header = headers.get("payment-required") ?? headers.get("x-payment-required");
@@ -1145,6 +1195,9 @@ var fetchCommand = (appContext) => new Command3("fetch").description("Fetch a ca
1145
1195
  ).option(
1146
1196
  "--json",
1147
1197
  "Emit {runId, status, latencyMs, payment, body} as JSON on stdout (for batch/non-TTY use)"
1198
+ ).option(
1199
+ "--agent <name>",
1200
+ "Identify your agent host for this invocation (e.g. claude-web, codex). Overrides auto-detect for this call only."
1148
1201
  ).action(
1149
1202
  async (url, options) => {
1150
1203
  try {
@@ -1196,7 +1249,9 @@ var fetchCommand = (appContext) => new Command3("fetch").description("Fetch a ca
1196
1249
  );
1197
1250
  }
1198
1251
  let finalResponse;
1252
+ let bodyBytes;
1199
1253
  let body = "";
1254
+ let bodyIsBinary = false;
1200
1255
  let paymentMeta;
1201
1256
  let sessionMeta;
1202
1257
  let fetchError;
@@ -1239,11 +1294,25 @@ var fetchCommand = (appContext) => new Command3("fetch").description("Fetch a ca
1239
1294
  } else {
1240
1295
  finalResponse = response;
1241
1296
  }
1242
- body = await finalResponse.text();
1297
+ const buf = Buffer.from(await finalResponse.arrayBuffer());
1298
+ bodyBytes = buf;
1299
+ bodyIsBinary = !isTextContentType(
1300
+ finalResponse.headers.get("content-type")
1301
+ );
1302
+ body = bodyIsBinary ? "" : buf.toString("utf8");
1243
1303
  } catch (err) {
1244
1304
  if (err instanceof SessionCloseFailedError) {
1245
1305
  finalResponse = err.response;
1246
- body = await err.response.text().catch(() => "");
1306
+ try {
1307
+ const buf = Buffer.from(await err.response.arrayBuffer());
1308
+ bodyBytes = buf;
1309
+ bodyIsBinary = !isTextContentType(
1310
+ err.response.headers.get("content-type")
1311
+ );
1312
+ body = bodyIsBinary ? "" : buf.toString("utf8");
1313
+ } catch {
1314
+ body = "";
1315
+ }
1247
1316
  paymentMeta = {
1248
1317
  protocol: "mpp",
1249
1318
  chain: "tempo",
@@ -1271,16 +1340,12 @@ var fetchCommand = (appContext) => new Command3("fetch").description("Fetch a ca
1271
1340
  }
1272
1341
  const latencyMs = Date.now() - startTime;
1273
1342
  if (finalResponse && !options.json) {
1274
- console.log(body);
1343
+ if (bodyIsBinary && bodyBytes) {
1344
+ process.stdout.write(bodyBytes);
1345
+ } else {
1346
+ console.log(body);
1347
+ }
1275
1348
  }
1276
- analyticsService.capture("fetch_executed", {
1277
- url,
1278
- status: finalResponse?.status,
1279
- hasPayment: !!paymentMeta,
1280
- paymentProtocol: paymentMeta?.protocol,
1281
- paymentAmount: paymentMeta?.amount,
1282
- ...fetchError && { error: fetchError.message }
1283
- });
1284
1349
  if (paymentMeta) {
1285
1350
  try {
1286
1351
  const balance = await walletService.getBalance();
@@ -1293,6 +1358,11 @@ var fetchCommand = (appContext) => new Command3("fetch").description("Fetch a ca
1293
1358
  Warning: Balance is $${balance.amount} \u2014 run \`zero wallet fund\` soon.
1294
1359
  `
1295
1360
  );
1361
+ analyticsService.capture("low_balance_warning_shown", {
1362
+ balance: balance.amount,
1363
+ threshold,
1364
+ paymentProtocol: paymentMeta.protocol
1365
+ });
1296
1366
  }
1297
1367
  }
1298
1368
  } catch {
@@ -1338,17 +1408,35 @@ var fetchCommand = (appContext) => new Command3("fetch").description("Fetch a ca
1338
1408
  );
1339
1409
  }
1340
1410
  }
1411
+ const status = finalResponse?.status;
1412
+ const outcome = !finalResponse ? "network_error" : status === 402 && !paymentMeta ? "payment_failed" : status !== void 0 && status >= 400 && status !== 402 ? "server_error" : "success";
1413
+ analyticsService.capture("fetch_executed", {
1414
+ url: redactUrl(url),
1415
+ status,
1416
+ outcome,
1417
+ latencyMs,
1418
+ hasPayment: !!paymentMeta,
1419
+ paymentProtocol: paymentMeta?.protocol,
1420
+ paymentAmount: paymentMeta?.amount,
1421
+ capabilityId: capabilityId ?? void 0,
1422
+ searchId: searchId ?? void 0,
1423
+ runId: runId ?? void 0,
1424
+ runTracked: !!runId,
1425
+ ...fetchError && { error: truncateError(fetchError.message) }
1426
+ });
1341
1427
  if (fetchError && !options.json) {
1342
1428
  console.error(` Fetch failed: ${fetchError.message}`);
1343
1429
  }
1344
1430
  if (options.json) {
1431
+ const jsonBody = !finalResponse ? null : bodyIsBinary ? (bodyBytes ?? Buffer.alloc(0)).toString("base64") : body;
1345
1432
  console.log(
1346
1433
  JSON.stringify({
1347
1434
  runId,
1348
1435
  status: finalResponse?.status ?? null,
1349
1436
  latencyMs,
1350
1437
  payment: paymentMeta ?? null,
1351
- body: finalResponse ? body : null,
1438
+ body: jsonBody,
1439
+ ...bodyIsBinary && { bodyEncoding: "base64" },
1352
1440
  ...fetchError && { error: fetchError.message },
1353
1441
  ...skipReasons.length > 0 && {
1354
1442
  runTrackingSkipped: skipReasons
@@ -1453,7 +1541,10 @@ var getCommand = (appContext) => new Command4("get").description(
1453
1541
  ).argument(
1454
1542
  "<identifier>",
1455
1543
  "Position number from search results, or a capability slug"
1456
- ).option("--formatted", "Output formatted trust breakdown").action(async (identifier, options) => {
1544
+ ).option("--formatted", "Output formatted trust breakdown").option(
1545
+ "--agent <name>",
1546
+ "Identify your agent host for this invocation. Overrides auto-detect for this call only."
1547
+ ).action(async (identifier, options) => {
1457
1548
  try {
1458
1549
  const { analyticsService, apiService, stateService } = appContext.services;
1459
1550
  const position = Number.parseInt(identifier, 10);
@@ -1493,7 +1584,9 @@ var getCommand = (appContext) => new Command4("get").description(
1493
1584
  }
1494
1585
  analyticsService.capture("capability_viewed", {
1495
1586
  capabilityId,
1496
- ...isPosition ? { position } : {}
1587
+ fromLastSearch: isPosition,
1588
+ ...isPosition ? { position } : {},
1589
+ ...searchId ? { searchId } : {}
1497
1590
  });
1498
1591
  } catch (err) {
1499
1592
  console.error(err instanceof Error ? err.message : "Get failed");
@@ -1717,107 +1810,126 @@ var installSkills = (home) => {
1717
1810
  return installed;
1718
1811
  };
1719
1812
  var initCommand = (appContext) => new Command5("init").description("Initialize Zero CLI for usage").option("--force", "Overwrite existing configuration").action(async (options) => {
1720
- const home = homedir2();
1721
- const zeroDir = join2(home, ".zero");
1722
- const configPath = join2(zeroDir, "config.json");
1723
- let walletCreated = false;
1724
- let walletAddress = null;
1725
- const walletExists = (() => {
1726
- if (!existsSync2(configPath)) return false;
1727
- try {
1728
- const existing = JSON.parse(readFileSync3(configPath, "utf8"));
1729
- return !!existing.privateKey;
1730
- } catch {
1731
- return false;
1732
- }
1733
- })();
1734
- if (!walletExists || options.force) {
1735
- const privateKey = generatePrivateKey();
1736
- const account = privateKeyToAccount(privateKey);
1737
- mkdirSync2(zeroDir, { recursive: true });
1738
- const existing = existsSync2(configPath) ? JSON.parse(readFileSync3(configPath, "utf8")) : {};
1739
- writeFileSync2(
1740
- configPath,
1741
- JSON.stringify(
1742
- { ...existing, privateKey, lowBalanceWarning: 1 },
1743
- null,
1744
- 2
1745
- )
1746
- );
1747
- walletCreated = true;
1748
- walletAddress = account.address;
1749
- console.log(`Wallet address: ${account.address}`);
1750
- } else {
1751
- try {
1752
- const existing = JSON.parse(readFileSync3(configPath, "utf8"));
1753
- const account = privateKeyToAccount(existing.privateKey);
1813
+ appContext.services.analyticsService.capture("init_started", {
1814
+ force: options.force ?? false
1815
+ });
1816
+ let currentStep = "wallet";
1817
+ try {
1818
+ const home = homedir2();
1819
+ const zeroDir = join2(home, ".zero");
1820
+ const configPath = join2(zeroDir, "config.json");
1821
+ let walletCreated = false;
1822
+ let walletAddress = null;
1823
+ const walletExists = (() => {
1824
+ if (!existsSync2(configPath)) return false;
1825
+ try {
1826
+ const existing = JSON.parse(readFileSync3(configPath, "utf8"));
1827
+ return !!existing.privateKey;
1828
+ } catch {
1829
+ return false;
1830
+ }
1831
+ })();
1832
+ if (!walletExists || options.force) {
1833
+ const privateKey = generatePrivateKey();
1834
+ const account = privateKeyToAccount(privateKey);
1835
+ mkdirSync2(zeroDir, { recursive: true });
1836
+ const existing = existsSync2(configPath) ? JSON.parse(readFileSync3(configPath, "utf8")) : {};
1837
+ writeFileSync2(
1838
+ configPath,
1839
+ JSON.stringify(
1840
+ { ...existing, privateKey, lowBalanceWarning: 1 },
1841
+ null,
1842
+ 2
1843
+ )
1844
+ );
1845
+ walletCreated = true;
1754
1846
  walletAddress = account.address;
1755
- } catch {
1847
+ console.log(`Wallet address: ${account.address}`);
1848
+ } else {
1849
+ try {
1850
+ const existing = JSON.parse(readFileSync3(configPath, "utf8"));
1851
+ const account = privateKeyToAccount(existing.privateKey);
1852
+ walletAddress = account.address;
1853
+ } catch {
1854
+ }
1756
1855
  }
1757
- }
1758
- const agentsDetected = [];
1759
- const agentsWithSkills = [];
1760
- let skillsError = null;
1761
- let hookInstalled = false;
1762
- let hookError = null;
1763
- for (const tool of AGENT_TOOLS) {
1764
- if (existsSync2(join2(home, tool.configDir))) {
1765
- agentsDetected.push(tool.name);
1856
+ const agentsDetected = [];
1857
+ const agentsWithSkills = [];
1858
+ let skillsError = null;
1859
+ let hookInstalled = false;
1860
+ let hookError = null;
1861
+ for (const tool of AGENT_TOOLS) {
1862
+ if (existsSync2(join2(home, tool.configDir))) {
1863
+ agentsDetected.push(tool.name);
1864
+ }
1766
1865
  }
1767
- }
1768
- try {
1769
- const installed = installSkills(home);
1770
- for (const entry of installed) {
1771
- const toolName = entry.split(":")[0];
1772
- if (toolName && !agentsWithSkills.includes(toolName)) {
1773
- agentsWithSkills.push(toolName);
1866
+ currentStep = "skills";
1867
+ try {
1868
+ const installed = installSkills(home);
1869
+ for (const entry of installed) {
1870
+ const toolName = entry.split(":")[0];
1871
+ if (toolName && !agentsWithSkills.includes(toolName)) {
1872
+ agentsWithSkills.push(toolName);
1873
+ }
1774
1874
  }
1875
+ } catch (err) {
1876
+ skillsError = err instanceof Error ? err.message : "unknown skills error";
1775
1877
  }
1776
- } catch (err) {
1777
- skillsError = err instanceof Error ? err.message : "unknown skills error";
1778
- }
1779
- try {
1780
- hookInstalled = installHook(home);
1781
- } catch (err) {
1782
- hookError = err instanceof Error ? err.message : "unknown hook error";
1783
- }
1784
- const conflictingSkills = findConflictingSkills(home);
1785
- if (conflictingSkills.length > 0) {
1786
- const skillList = conflictingSkills.map((s) => ` - ${s.tool}: ${s.skillName}`).join("\n");
1787
- console.error(
1788
- `
1878
+ currentStep = "hook";
1879
+ try {
1880
+ hookInstalled = installHook(home);
1881
+ } catch (err) {
1882
+ hookError = err instanceof Error ? err.message : "unknown hook error";
1883
+ }
1884
+ currentStep = "cleanup_scan";
1885
+ const conflictingSkills = findConflictingSkills(home);
1886
+ if (conflictingSkills.length > 0) {
1887
+ const skillList = conflictingSkills.map((s) => ` - ${s.tool}: ${s.skillName}`).join("\n");
1888
+ console.error(
1889
+ `
1789
1890
  Found deprecated skills that may conflict with Zero:
1790
1891
  ${skillList}
1791
1892
 
1792
1893
  To remove them, run: zero init cleanup`
1894
+ );
1895
+ }
1896
+ console.error(
1897
+ 'Zero is ready! Run `zero search` to find capabilities.\n\nBy using Zero, you agree to our Terms of Service:\n https://zero.xyz/terms-of-service\n\nRun `zero terms` to view the full terms.\n\nTry:\n zero search "translate text to Spanish"\n zero search "generate an image"\n zero search "weather forecast"'
1793
1898
  );
1899
+ currentStep = "complete";
1900
+ appContext.services.analyticsService.capture("wallet_initialized", {
1901
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
1902
+ wallet_created: walletCreated,
1903
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
1904
+ wallet_address: walletAddress,
1905
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
1906
+ agents_detected: agentsDetected,
1907
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
1908
+ agents_detected_count: agentsDetected.length,
1909
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
1910
+ skills_installed: agentsWithSkills.length > 0,
1911
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
1912
+ skills_installed_for: agentsWithSkills,
1913
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
1914
+ skills_error: skillsError,
1915
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
1916
+ hook_installed: hookInstalled,
1917
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
1918
+ hook_error: hookError,
1919
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
1920
+ conflicting_skills_found: conflictingSkills.length,
1921
+ force: options.force ?? false
1922
+ });
1923
+ } catch (err) {
1924
+ appContext.services.analyticsService.capture("init_failed", {
1925
+ step: currentStep,
1926
+ error: truncateError(
1927
+ err instanceof Error ? err.message : String(err)
1928
+ ),
1929
+ force: options.force ?? false
1930
+ });
1931
+ throw err;
1794
1932
  }
1795
- console.error(
1796
- 'Zero is ready! Run `zero search` to find capabilities.\n\nBy using Zero, you agree to our Terms of Service:\n https://zero.xyz/terms-of-service\n\nRun `zero terms` to view the full terms.\n\nTry:\n zero search "translate text to Spanish"\n zero search "generate an image"\n zero search "weather forecast"'
1797
- );
1798
- appContext.services.analyticsService.capture("wallet_initialized", {
1799
- // biome-ignore lint/style/useNamingConvention: snake_case for analytics
1800
- wallet_created: walletCreated,
1801
- // biome-ignore lint/style/useNamingConvention: snake_case for analytics
1802
- wallet_address: walletAddress,
1803
- // biome-ignore lint/style/useNamingConvention: snake_case for analytics
1804
- agents_detected: agentsDetected,
1805
- // biome-ignore lint/style/useNamingConvention: snake_case for analytics
1806
- agents_detected_count: agentsDetected.length,
1807
- // biome-ignore lint/style/useNamingConvention: snake_case for analytics
1808
- skills_installed: agentsWithSkills.length > 0,
1809
- // biome-ignore lint/style/useNamingConvention: snake_case for analytics
1810
- skills_installed_for: agentsWithSkills,
1811
- // biome-ignore lint/style/useNamingConvention: snake_case for analytics
1812
- skills_error: skillsError,
1813
- // biome-ignore lint/style/useNamingConvention: snake_case for analytics
1814
- hook_installed: hookInstalled,
1815
- // biome-ignore lint/style/useNamingConvention: snake_case for analytics
1816
- hook_error: hookError,
1817
- // biome-ignore lint/style/useNamingConvention: snake_case for analytics
1818
- conflicting_skills_found: conflictingSkills.length,
1819
- force: options.force ?? false
1820
- });
1821
1933
  }).addCommand(
1822
1934
  new Command5("cleanup").description(
1823
1935
  "Remove deprecated skills (zam, tempo) that conflict with Zero"
@@ -1831,7 +1943,7 @@ To remove them, run: zero init cleanup`
1831
1943
  const removedList = removed.map((s) => ` - ${s}`).join("\n");
1832
1944
  console.error(`Removed deprecated skills:
1833
1945
  ${removedList}`);
1834
- appContext.services.analyticsService.capture("skills_cleanup", {
1946
+ appContext.services.analyticsService.capture("skills_cleaned_up", {
1835
1947
  // biome-ignore lint/style/useNamingConvention: snake_case for analytics
1836
1948
  skills_removed: removed,
1837
1949
  // biome-ignore lint/style/useNamingConvention: snake_case for analytics
@@ -1875,6 +1987,9 @@ Examples:
1875
1987
  ).option("--success", "The capability succeeded").option("--no-success", "The capability failed").option("--accuracy <n>", "Accuracy rating (1-5)", Number.parseInt).option("--value <n>", "Value rating (1-5)", Number.parseInt).option("--reliability <n>", "Reliability rating (1-5)", Number.parseInt).option("--content <text>", "Optional review text").option(
1876
1988
  "--from-file <path>",
1877
1989
  "Submit reviews in bulk from a JSONL file (one review object per line: {runId, success, accuracy, value, reliability, content?})"
1990
+ ).option(
1991
+ "--agent <name>",
1992
+ "Identify your agent host for this invocation. Overrides auto-detect for this call only."
1878
1993
  ).action(
1879
1994
  async (runId, options) => {
1880
1995
  try {
@@ -1896,6 +2011,10 @@ Examples:
1896
2011
  analyticsService.capture("review_submitted", {
1897
2012
  runId: parsed.runId,
1898
2013
  success: parsed.success,
2014
+ accuracy: parsed.accuracy,
2015
+ value: parsed.value,
2016
+ reliability: parsed.reliability,
2017
+ hasContent: !!parsed.content,
1899
2018
  bulk: true
1900
2019
  });
1901
2020
  } catch (err) {
@@ -1978,7 +2097,12 @@ Bulk review complete: ${ok} ok, ${failed} failed`);
1978
2097
  console.log(`Review submitted: ${result.reviewId}`);
1979
2098
  analyticsService.capture("review_submitted", {
1980
2099
  runId,
1981
- success: options.success
2100
+ success: options.success,
2101
+ accuracy,
2102
+ value,
2103
+ reliability,
2104
+ hasContent: !!options.content,
2105
+ resolvedByCapability: !!options.capability
1982
2106
  });
1983
2107
  } catch (err) {
1984
2108
  console.error(err instanceof Error ? err.message : "Review failed");
@@ -2003,6 +2127,9 @@ Examples:
2003
2127
  "--limit <n>",
2004
2128
  "Max rows (1-100, default 25)",
2005
2129
  (v) => Number.parseInt(v, 10)
2130
+ ).option(
2131
+ "--agent <name>",
2132
+ "Identify your agent host for this invocation. Overrides auto-detect for this call only."
2006
2133
  ).action(
2007
2134
  async (options) => {
2008
2135
  try {
@@ -2084,6 +2211,9 @@ var searchCommand = (appContext) => new Command8("search").description("Search f
2084
2211
  ).option(
2085
2212
  "--exclude-source <source>",
2086
2213
  "Exclude results from this crawl source"
2214
+ ).option(
2215
+ "--agent <name>",
2216
+ "Identify your agent host for this invocation (e.g. claude-web, codex). Overrides auto-detect for this call only."
2087
2217
  ).action(
2088
2218
  async (query, options) => {
2089
2219
  try {
@@ -2118,8 +2248,24 @@ var searchCommand = (appContext) => new Command8("search").description("Search f
2118
2248
  excludeSource: options.excludeSource
2119
2249
  });
2120
2250
  analyticsService.capture("search_executed", {
2121
- query,
2122
- resultCount: result.capabilities.length
2251
+ query: truncateQuery(query),
2252
+ queryLength: query.length,
2253
+ resultCount: result.capabilities.length,
2254
+ searchId: result.searchId,
2255
+ total: result.total,
2256
+ hasMore: result.hasMore,
2257
+ offset: options.offset,
2258
+ limit: options.limit,
2259
+ freeOnly: options.free ?? false,
2260
+ maxCost: options.maxCost,
2261
+ minRating: options.minRating,
2262
+ protocol: options.protocol,
2263
+ minTrust: options.minTrust,
2264
+ availabilityStatus: options.status,
2265
+ includeAll: options.all ?? false,
2266
+ source: options.source,
2267
+ excludeSource: options.excludeSource,
2268
+ json: options.json ?? false
2123
2269
  });
2124
2270
  if (options.json) {
2125
2271
  console.log(JSON.stringify(result, null, 2));
@@ -2313,6 +2459,14 @@ var walletCommand = (appContext) => {
2313
2459
  var createApp = (appContext) => {
2314
2460
  const { analyticsService } = appContext.services;
2315
2461
  const program = new Command11().name("zero").description("Zero CLI \u2014 Search engine and payment platform for AI agents").version(package_default.version, "-v, --version").exitOverride().hook("preAction", async (_thisCommand, actionCommand) => {
2462
+ const agentFlag = actionCommand.opts().agent;
2463
+ if (typeof agentFlag === "string" && agentFlag.trim().length > 0) {
2464
+ analyticsService.setAgentHost(agentFlag.trim());
2465
+ }
2466
+ appContext.invocation.current = {
2467
+ command: actionCommand.name(),
2468
+ startMs: Date.now()
2469
+ };
2316
2470
  analyticsService.capture("command_executed", {
2317
2471
  command: actionCommand.name()
2318
2472
  });
@@ -2334,7 +2488,8 @@ var createApp = (appContext) => {
2334
2488
  import z4 from "zod";
2335
2489
  var envSchema = z4.object({
2336
2490
  ZERO_API_URL: z4.string().default("https://api.zero.xyz"),
2337
- ZERO_PRIVATE_KEY: z4.string().optional()
2491
+ ZERO_PRIVATE_KEY: z4.string().optional(),
2492
+ ZERO_ENV: z4.enum(["development", "production"]).default("production")
2338
2493
  });
2339
2494
  var getEnv = () => {
2340
2495
  try {
@@ -2347,6 +2502,7 @@ var getEnv = () => {
2347
2502
  };
2348
2503
 
2349
2504
  // src/app/app-services.ts
2505
+ import { randomUUID as randomUUID2 } from "crypto";
2350
2506
  import { existsSync as existsSync6, readFileSync as readFileSync8 } from "fs";
2351
2507
  import { homedir as homedir4 } from "os";
2352
2508
  import { join as join5 } from "path";
@@ -2362,9 +2518,17 @@ var POSTHOG_HOST = "https://us.i.posthog.com";
2362
2518
  var AnalyticsService = class {
2363
2519
  posthog;
2364
2520
  distinctId;
2521
+ walletAddress;
2365
2522
  cliVersion;
2523
+ environment;
2524
+ requestId;
2525
+ agentHost;
2366
2526
  constructor(opts) {
2367
2527
  this.cliVersion = opts.cliVersion;
2528
+ this.environment = opts.environment;
2529
+ this.walletAddress = opts.walletAddress;
2530
+ this.requestId = opts.requestId;
2531
+ this.agentHost = opts.agentHost;
2368
2532
  let telemetryEnabled = true;
2369
2533
  let persistedAnonId;
2370
2534
  try {
@@ -2417,6 +2581,40 @@ var AnalyticsService = class {
2417
2581
  });
2418
2582
  this.posthog.on("error", () => {
2419
2583
  });
2584
+ if (opts.walletAddress && persistedAnonId) {
2585
+ this.maybeAliasAnonToWallet(
2586
+ opts.configPath,
2587
+ persistedAnonId,
2588
+ opts.walletAddress
2589
+ );
2590
+ }
2591
+ }
2592
+ maybeAliasAnonToWallet(configPath, anonId, walletAddress) {
2593
+ if (!this.posthog) return;
2594
+ if (anonId === walletAddress) return;
2595
+ let aliasedTo;
2596
+ try {
2597
+ const config = JSON.parse(readFileSync6(configPath, "utf8"));
2598
+ if (typeof config.aliasedTo === "string") {
2599
+ aliasedTo = config.aliasedTo;
2600
+ }
2601
+ } catch {
2602
+ }
2603
+ if (aliasedTo === walletAddress) return;
2604
+ this.posthog.alias({ distinctId: walletAddress, alias: anonId });
2605
+ try {
2606
+ const config = existsSync4(configPath) ? JSON.parse(readFileSync6(configPath, "utf8")) : {};
2607
+ writeFileSync4(
2608
+ configPath,
2609
+ JSON.stringify({ ...config, aliasedTo: walletAddress }, null, 2)
2610
+ );
2611
+ } catch {
2612
+ }
2613
+ }
2614
+ // Per-invocation override applied by the preAction hook when `--agent`
2615
+ // is passed. Stateless — affects only this process.
2616
+ setAgentHost(next) {
2617
+ this.agentHost = next;
2420
2618
  }
2421
2619
  capture(event, properties) {
2422
2620
  if (!this.posthog) return;
@@ -2427,6 +2625,13 @@ var AnalyticsService = class {
2427
2625
  source: "cli",
2428
2626
  // biome-ignore lint/style/useNamingConvention: snake_case is standard for analytics event properties
2429
2627
  cli_version: this.cliVersion,
2628
+ environment: this.environment,
2629
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
2630
+ wallet_address: this.walletAddress,
2631
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
2632
+ request_id: this.requestId,
2633
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
2634
+ agent_host: this.agentHost,
2430
2635
  ...properties
2431
2636
  }
2432
2637
  });
@@ -2476,7 +2681,7 @@ var WalletService = class {
2476
2681
  if (!this.address) return null;
2477
2682
  if (this.balanceGetter) {
2478
2683
  try {
2479
- const result = await this.balanceGetter("base");
2684
+ const result = await this.balanceGetter();
2480
2685
  return { amount: result.amount, asset: result.asset };
2481
2686
  } catch {
2482
2687
  return {
@@ -2491,6 +2696,15 @@ var WalletService = class {
2491
2696
  getLowBalanceWarning = () => this.config.lowBalanceWarning;
2492
2697
  };
2493
2698
 
2699
+ // src/util/agent-host.ts
2700
+ var detectAgentHost = (env = process.env) => {
2701
+ if (env.ZERO_AGENT) return env.ZERO_AGENT;
2702
+ if (env.CLAUDECODE === "1") return "claude-code";
2703
+ if (env.CURSOR_TRACE_ID) return "cursor";
2704
+ if (env.TERM_PROGRAM === "vscode") return "vscode";
2705
+ return "unknown";
2706
+ };
2707
+
2494
2708
  // src/app/app-services.ts
2495
2709
  var CLI_VERSION = package_default.version;
2496
2710
  var getServices = (env) => {
@@ -2527,12 +2741,15 @@ var getServices = (env) => {
2527
2741
  {
2528
2742
  lowBalanceWarning
2529
2743
  },
2530
- paymentService.getBalance
2744
+ paymentService.getTotalBalance
2531
2745
  );
2532
2746
  const analyticsService = new AnalyticsService({
2533
2747
  walletAddress: account?.address ?? null,
2534
2748
  configPath,
2535
- cliVersion: CLI_VERSION
2749
+ cliVersion: CLI_VERSION,
2750
+ environment: env.ZERO_ENV,
2751
+ requestId: randomUUID2(),
2752
+ agentHost: detectAgentHost()
2536
2753
  });
2537
2754
  return {
2538
2755
  analyticsService,
@@ -2551,7 +2768,8 @@ var createAppContext = () => {
2551
2768
  }
2552
2769
  return {
2553
2770
  env,
2554
- services: getServices(env)
2771
+ services: getServices(env),
2772
+ invocation: { current: null }
2555
2773
  };
2556
2774
  };
2557
2775
 
@@ -2563,15 +2781,27 @@ var main = async () => {
2563
2781
  process.exit(1);
2564
2782
  }
2565
2783
  const app = createApp(appContext);
2784
+ let caughtError = null;
2566
2785
  try {
2567
2786
  await app.parseAsync(process.argv);
2568
2787
  } catch (err) {
2569
2788
  const isCommanderExit = err instanceof Error && "exitCode" in err && err.exitCode === 0;
2570
2789
  if (!isCommanderExit) {
2571
- console.error(err instanceof Error ? err.message : "Unexpected error");
2790
+ caughtError = err instanceof Error ? err : new Error(String(err));
2791
+ console.error(caughtError.message);
2572
2792
  process.exitCode = 1;
2573
2793
  }
2574
2794
  } finally {
2795
+ const invocation = appContext.invocation.current;
2796
+ if (invocation) {
2797
+ appContext.services.analyticsService.capture("command_completed", {
2798
+ command: invocation.command,
2799
+ success: !caughtError && process.exitCode !== 1,
2800
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
2801
+ duration_ms: Date.now() - invocation.startMs,
2802
+ ...caughtError && { error: truncateError(caughtError.message) }
2803
+ });
2804
+ }
2575
2805
  await appContext.services.analyticsService.shutdown();
2576
2806
  }
2577
2807
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeroxyz/cli",
3
- "version": "0.0.25",
3
+ "version": "0.0.26",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "zero": "dist/index.js",
@@ -18,8 +18,8 @@
18
18
  "build": "tsup src/index.ts --format esm --out-dir dist --clean",
19
19
  "build:binary": "tsup --config tsup.binary.ts && cp -r skills hooks dist/pkg/ && pnpm exec pkg dist/pkg/index.cjs --config pkg.json --targets node24-macos-arm64,node24-macos-x64,node24-linux-x64 --output dist/bin/zero",
20
20
  "prepublishOnly": "pnpm run build",
21
- "dev": "tsx src/index.ts",
22
- "cli": "ZERO_API_URL=http://localhost:1111 tsx src/index.ts",
21
+ "dev": "ZERO_ENV=development tsx src/index.ts",
22
+ "cli": "ZERO_ENV=development ZERO_API_URL=http://localhost:1111 tsx src/index.ts",
23
23
  "test:integration": "vitest run --project integration",
24
24
  "test:online": "vitest run --project online",
25
25
  "test:unit": "vitest run --project unit",
@@ -43,6 +43,16 @@ Run these commands in order. Do not skip steps.
43
43
 
44
44
  **Prefer `zero init` or `zero wallet set` for local/interactive use** so the wallet persists across shells. Use `ZERO_PRIVATE_KEY` only when persisting to disk is undesirable — CI jobs, containers, one-off scripts, or secrets-manager-injected environments.
45
45
 
46
+ **Identify yourself (if your platform isn't auto-detected).** Zero auto-detects Claude Code, Cursor, and VSCode via their session env vars. For platforms without a signature (Claude Web, Managed Agents, Codex, OpenCode, or anything else), pass your identity on every call — either flag or env var:
47
+
48
+ ```
49
+ zero search --agent claude-web "translate hello world"
50
+ # or
51
+ ZERO_AGENT=claude-web zero search "translate hello world"
52
+ ```
53
+
54
+ Both are per-invocation and stateless — there's no persistent config to go stale if you move between sandboxes. Resolution order: `--agent` flag > `ZERO_AGENT` env var > host-specific env signals (`CLAUDECODE`, `CURSOR_TRACE_ID`, `TERM_PROGRAM=vscode`) > `unknown`. Canonical names: `claude-code`, `cursor`, `vscode`, `claude-web`, `codex`, `opencode`. Any string is accepted.
55
+
46
56
  **Step 3 — Fund wallet:** `zero wallet fund` — opens browser to add USDC (Base). For manual transfer: `zero wallet fund --manual`.
47
57
 
48
58
  **Step 4 — Confirm readiness:** `zero wallet balance`