@zeroxyz/cli 0.0.25 → 0.0.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/app.ts
4
- import { Command as Command11 } from "commander";
4
+ import { Command as Command12 } from "commander";
5
5
 
6
6
  // package.json
7
7
  var package_default = {
8
8
  name: "@zeroxyz/cli",
9
- version: "0.0.25",
9
+ version: "0.0.27",
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,10 +1140,41 @@ 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
1120
- var detectPaymentRequirement = (headers, status) => {
1121
- if (status !== 402) return null;
1122
- const x402Header = headers.get("payment-required") ?? headers.get("x-payment-required");
1158
+ var isTextContentType = (contentType) => {
1159
+ if (!contentType) return true;
1160
+ const ct = contentType.toLowerCase().split(";")[0]?.trim() ?? "";
1161
+ if (ct.startsWith("text/")) return true;
1162
+ if (ct === "application/json" || ct.endsWith("+json")) return true;
1163
+ if (ct === "application/xml" || ct.endsWith("+xml")) return true;
1164
+ if (ct === "application/javascript" || ct === "application/ecmascript") {
1165
+ return true;
1166
+ }
1167
+ if (ct === "application/x-www-form-urlencoded") return true;
1168
+ return false;
1169
+ };
1170
+ var looksLikeX402V1Body = (body) => {
1171
+ if (!body || typeof body !== "object") return false;
1172
+ const b = body;
1173
+ return b.x402Version === 1 && Array.isArray(b.accepts) && b.accepts.length > 0;
1174
+ };
1175
+ var detectPaymentRequirement = async (response) => {
1176
+ if (response.status !== 402) return null;
1177
+ const x402Header = response.headers.get("payment-required") ?? response.headers.get("x-payment-required");
1123
1178
  if (x402Header) {
1124
1179
  try {
1125
1180
  const decoded = JSON.parse(
@@ -1130,10 +1185,20 @@ var detectPaymentRequirement = (headers, status) => {
1130
1185
  return { protocol: "x402", raw: { encoded: x402Header } };
1131
1186
  }
1132
1187
  }
1133
- const wwwAuth = headers.get("www-authenticate");
1188
+ const wwwAuth = response.headers.get("www-authenticate");
1134
1189
  if (wwwAuth?.toLowerCase().includes("payment")) {
1135
1190
  return { protocol: "mpp", raw: { "www-authenticate": wwwAuth } };
1136
1191
  }
1192
+ try {
1193
+ const text = await response.clone().text();
1194
+ if (text) {
1195
+ const parsed = JSON.parse(text);
1196
+ if (looksLikeX402V1Body(parsed)) {
1197
+ return { protocol: "x402", raw: parsed };
1198
+ }
1199
+ }
1200
+ } catch {
1201
+ }
1137
1202
  return { protocol: "unknown", raw: {} };
1138
1203
  };
1139
1204
  var fetchCommand = (appContext) => new Command3("fetch").description("Fetch a capability URL with automatic payment handling").argument("<url>", "URL to fetch").option(
@@ -1145,6 +1210,9 @@ var fetchCommand = (appContext) => new Command3("fetch").description("Fetch a ca
1145
1210
  ).option(
1146
1211
  "--json",
1147
1212
  "Emit {runId, status, latencyMs, payment, body} as JSON on stdout (for batch/non-TTY use)"
1213
+ ).option(
1214
+ "--agent <name>",
1215
+ "Identify your agent host for this invocation (e.g. claude-web, codex). Overrides auto-detect for this call only."
1148
1216
  ).action(
1149
1217
  async (url, options) => {
1150
1218
  try {
@@ -1196,17 +1264,16 @@ var fetchCommand = (appContext) => new Command3("fetch").description("Fetch a ca
1196
1264
  );
1197
1265
  }
1198
1266
  let finalResponse;
1267
+ let bodyBytes;
1199
1268
  let body = "";
1269
+ let bodyIsBinary = false;
1200
1270
  let paymentMeta;
1201
1271
  let sessionMeta;
1202
1272
  let fetchError;
1203
1273
  try {
1204
1274
  log(`Calling ${url}...`);
1205
1275
  const response = await fetch(url, requestInit);
1206
- const paymentReq = detectPaymentRequirement(
1207
- response.headers,
1208
- response.status
1209
- );
1276
+ const paymentReq = await detectPaymentRequirement(response);
1210
1277
  if (paymentReq) {
1211
1278
  log(
1212
1279
  `Payment required (${paymentReq.protocol}) \u2014 preparing payment...`
@@ -1239,11 +1306,25 @@ var fetchCommand = (appContext) => new Command3("fetch").description("Fetch a ca
1239
1306
  } else {
1240
1307
  finalResponse = response;
1241
1308
  }
1242
- body = await finalResponse.text();
1309
+ const buf = Buffer.from(await finalResponse.arrayBuffer());
1310
+ bodyBytes = buf;
1311
+ bodyIsBinary = !isTextContentType(
1312
+ finalResponse.headers.get("content-type")
1313
+ );
1314
+ body = bodyIsBinary ? "" : buf.toString("utf8");
1243
1315
  } catch (err) {
1244
1316
  if (err instanceof SessionCloseFailedError) {
1245
1317
  finalResponse = err.response;
1246
- body = await err.response.text().catch(() => "");
1318
+ try {
1319
+ const buf = Buffer.from(await err.response.arrayBuffer());
1320
+ bodyBytes = buf;
1321
+ bodyIsBinary = !isTextContentType(
1322
+ err.response.headers.get("content-type")
1323
+ );
1324
+ body = bodyIsBinary ? "" : buf.toString("utf8");
1325
+ } catch {
1326
+ body = "";
1327
+ }
1247
1328
  paymentMeta = {
1248
1329
  protocol: "mpp",
1249
1330
  chain: "tempo",
@@ -1271,16 +1352,12 @@ var fetchCommand = (appContext) => new Command3("fetch").description("Fetch a ca
1271
1352
  }
1272
1353
  const latencyMs = Date.now() - startTime;
1273
1354
  if (finalResponse && !options.json) {
1274
- console.log(body);
1355
+ if (bodyIsBinary && bodyBytes) {
1356
+ process.stdout.write(bodyBytes);
1357
+ } else {
1358
+ console.log(body);
1359
+ }
1275
1360
  }
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
1361
  if (paymentMeta) {
1285
1362
  try {
1286
1363
  const balance = await walletService.getBalance();
@@ -1293,6 +1370,11 @@ var fetchCommand = (appContext) => new Command3("fetch").description("Fetch a ca
1293
1370
  Warning: Balance is $${balance.amount} \u2014 run \`zero wallet fund\` soon.
1294
1371
  `
1295
1372
  );
1373
+ analyticsService.capture("low_balance_warning_shown", {
1374
+ balance: balance.amount,
1375
+ threshold,
1376
+ paymentProtocol: paymentMeta.protocol
1377
+ });
1296
1378
  }
1297
1379
  }
1298
1380
  } catch {
@@ -1338,17 +1420,35 @@ var fetchCommand = (appContext) => new Command3("fetch").description("Fetch a ca
1338
1420
  );
1339
1421
  }
1340
1422
  }
1423
+ const status = finalResponse?.status;
1424
+ const outcome = !finalResponse ? "network_error" : status === 402 && !paymentMeta ? "payment_failed" : status !== void 0 && status >= 400 && status !== 402 ? "server_error" : "success";
1425
+ analyticsService.capture("fetch_executed", {
1426
+ url: redactUrl(url),
1427
+ status,
1428
+ outcome,
1429
+ latencyMs,
1430
+ hasPayment: !!paymentMeta,
1431
+ paymentProtocol: paymentMeta?.protocol,
1432
+ paymentAmount: paymentMeta?.amount,
1433
+ capabilityId: capabilityId ?? void 0,
1434
+ searchId: searchId ?? void 0,
1435
+ runId: runId ?? void 0,
1436
+ runTracked: !!runId,
1437
+ ...fetchError && { error: truncateError(fetchError.message) }
1438
+ });
1341
1439
  if (fetchError && !options.json) {
1342
1440
  console.error(` Fetch failed: ${fetchError.message}`);
1343
1441
  }
1344
1442
  if (options.json) {
1443
+ const jsonBody = !finalResponse ? null : bodyIsBinary ? (bodyBytes ?? Buffer.alloc(0)).toString("base64") : body;
1345
1444
  console.log(
1346
1445
  JSON.stringify({
1347
1446
  runId,
1348
1447
  status: finalResponse?.status ?? null,
1349
1448
  latencyMs,
1350
1449
  payment: paymentMeta ?? null,
1351
- body: finalResponse ? body : null,
1450
+ body: jsonBody,
1451
+ ...bodyIsBinary && { bodyEncoding: "base64" },
1352
1452
  ...fetchError && { error: fetchError.message },
1353
1453
  ...skipReasons.length > 0 && {
1354
1454
  runTrackingSkipped: skipReasons
@@ -1453,7 +1553,10 @@ var getCommand = (appContext) => new Command4("get").description(
1453
1553
  ).argument(
1454
1554
  "<identifier>",
1455
1555
  "Position number from search results, or a capability slug"
1456
- ).option("--formatted", "Output formatted trust breakdown").action(async (identifier, options) => {
1556
+ ).option("--formatted", "Output formatted trust breakdown").option(
1557
+ "--agent <name>",
1558
+ "Identify your agent host for this invocation. Overrides auto-detect for this call only."
1559
+ ).action(async (identifier, options) => {
1457
1560
  try {
1458
1561
  const { analyticsService, apiService, stateService } = appContext.services;
1459
1562
  const position = Number.parseInt(identifier, 10);
@@ -1493,7 +1596,9 @@ var getCommand = (appContext) => new Command4("get").description(
1493
1596
  }
1494
1597
  analyticsService.capture("capability_viewed", {
1495
1598
  capabilityId,
1496
- ...isPosition ? { position } : {}
1599
+ fromLastSearch: isPosition,
1600
+ ...isPosition ? { position } : {},
1601
+ ...searchId ? { searchId } : {}
1497
1602
  });
1498
1603
  } catch (err) {
1499
1604
  console.error(err instanceof Error ? err.message : "Get failed");
@@ -1716,108 +1821,129 @@ var installSkills = (home) => {
1716
1821
  }
1717
1822
  return installed;
1718
1823
  };
1719
- 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);
1824
+ var runInit = async (appContext, options = {}) => {
1825
+ appContext.services.analyticsService.capture("init_started", {
1826
+ force: options.force ?? false
1827
+ });
1828
+ let currentStep = "wallet";
1829
+ try {
1830
+ const home = homedir2();
1831
+ const zeroDir = join2(home, ".zero");
1832
+ const configPath = join2(zeroDir, "config.json");
1833
+ let walletCreated = false;
1834
+ let walletAddress = null;
1835
+ const walletExists = (() => {
1836
+ if (!existsSync2(configPath)) return false;
1837
+ try {
1838
+ const existing = JSON.parse(readFileSync3(configPath, "utf8"));
1839
+ return !!existing.privateKey;
1840
+ } catch {
1841
+ return false;
1842
+ }
1843
+ })();
1844
+ if (!walletExists || options.force) {
1845
+ const privateKey = generatePrivateKey();
1846
+ const account = privateKeyToAccount(privateKey);
1847
+ mkdirSync2(zeroDir, { recursive: true });
1848
+ const existing = existsSync2(configPath) ? JSON.parse(readFileSync3(configPath, "utf8")) : {};
1849
+ writeFileSync2(
1850
+ configPath,
1851
+ JSON.stringify(
1852
+ { ...existing, privateKey, lowBalanceWarning: 1 },
1853
+ null,
1854
+ 2
1855
+ )
1856
+ );
1857
+ walletCreated = true;
1754
1858
  walletAddress = account.address;
1755
- } catch {
1859
+ console.log(`Wallet address: ${account.address}`);
1860
+ } else {
1861
+ try {
1862
+ const existing = JSON.parse(readFileSync3(configPath, "utf8"));
1863
+ const account = privateKeyToAccount(existing.privateKey);
1864
+ walletAddress = account.address;
1865
+ } catch {
1866
+ }
1756
1867
  }
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);
1868
+ const agentsDetected = [];
1869
+ const agentsWithSkills = [];
1870
+ let skillsError = null;
1871
+ let hookInstalled = false;
1872
+ let hookError = null;
1873
+ for (const tool of AGENT_TOOLS) {
1874
+ if (existsSync2(join2(home, tool.configDir))) {
1875
+ agentsDetected.push(tool.name);
1876
+ }
1766
1877
  }
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);
1878
+ currentStep = "skills";
1879
+ try {
1880
+ const installed = installSkills(home);
1881
+ for (const entry of installed) {
1882
+ const toolName = entry.split(":")[0];
1883
+ if (toolName && !agentsWithSkills.includes(toolName)) {
1884
+ agentsWithSkills.push(toolName);
1885
+ }
1774
1886
  }
1887
+ } catch (err) {
1888
+ skillsError = err instanceof Error ? err.message : "unknown skills error";
1775
1889
  }
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
- `
1890
+ currentStep = "hook";
1891
+ try {
1892
+ hookInstalled = installHook(home);
1893
+ } catch (err) {
1894
+ hookError = err instanceof Error ? err.message : "unknown hook error";
1895
+ }
1896
+ currentStep = "cleanup_scan";
1897
+ const conflictingSkills = findConflictingSkills(home);
1898
+ if (conflictingSkills.length > 0) {
1899
+ const skillList = conflictingSkills.map((s) => ` - ${s.tool}: ${s.skillName}`).join("\n");
1900
+ console.error(
1901
+ `
1789
1902
  Found deprecated skills that may conflict with Zero:
1790
1903
  ${skillList}
1791
1904
 
1792
1905
  To remove them, run: zero init cleanup`
1906
+ );
1907
+ }
1908
+ console.error(
1909
+ '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
1910
  );
1911
+ currentStep = "complete";
1912
+ appContext.services.analyticsService.capture("wallet_initialized", {
1913
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
1914
+ wallet_created: walletCreated,
1915
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
1916
+ wallet_address: walletAddress,
1917
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
1918
+ agents_detected: agentsDetected,
1919
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
1920
+ agents_detected_count: agentsDetected.length,
1921
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
1922
+ skills_installed: agentsWithSkills.length > 0,
1923
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
1924
+ skills_installed_for: agentsWithSkills,
1925
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
1926
+ skills_error: skillsError,
1927
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
1928
+ hook_installed: hookInstalled,
1929
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
1930
+ hook_error: hookError,
1931
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
1932
+ conflicting_skills_found: conflictingSkills.length,
1933
+ force: options.force ?? false
1934
+ });
1935
+ return { walletAddress, walletCreated };
1936
+ } catch (err) {
1937
+ appContext.services.analyticsService.capture("init_failed", {
1938
+ step: currentStep,
1939
+ error: truncateError(err instanceof Error ? err.message : String(err)),
1940
+ force: options.force ?? false
1941
+ });
1942
+ throw err;
1794
1943
  }
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
- });
1944
+ };
1945
+ var initCommand = (appContext) => new Command5("init").description("Initialize Zero CLI for usage").option("--force", "Overwrite existing configuration").action(async (options) => {
1946
+ await runInit(appContext, options);
1821
1947
  }).addCommand(
1822
1948
  new Command5("cleanup").description(
1823
1949
  "Remove deprecated skills (zam, tempo) that conflict with Zero"
@@ -1831,7 +1957,7 @@ To remove them, run: zero init cleanup`
1831
1957
  const removedList = removed.map((s) => ` - ${s}`).join("\n");
1832
1958
  console.error(`Removed deprecated skills:
1833
1959
  ${removedList}`);
1834
- appContext.services.analyticsService.capture("skills_cleanup", {
1960
+ appContext.services.analyticsService.capture("skills_cleaned_up", {
1835
1961
  // biome-ignore lint/style/useNamingConvention: snake_case for analytics
1836
1962
  skills_removed: removed,
1837
1963
  // biome-ignore lint/style/useNamingConvention: snake_case for analytics
@@ -1875,6 +2001,9 @@ Examples:
1875
2001
  ).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
2002
  "--from-file <path>",
1877
2003
  "Submit reviews in bulk from a JSONL file (one review object per line: {runId, success, accuracy, value, reliability, content?})"
2004
+ ).option(
2005
+ "--agent <name>",
2006
+ "Identify your agent host for this invocation. Overrides auto-detect for this call only."
1878
2007
  ).action(
1879
2008
  async (runId, options) => {
1880
2009
  try {
@@ -1896,6 +2025,10 @@ Examples:
1896
2025
  analyticsService.capture("review_submitted", {
1897
2026
  runId: parsed.runId,
1898
2027
  success: parsed.success,
2028
+ accuracy: parsed.accuracy,
2029
+ value: parsed.value,
2030
+ reliability: parsed.reliability,
2031
+ hasContent: !!parsed.content,
1899
2032
  bulk: true
1900
2033
  });
1901
2034
  } catch (err) {
@@ -1978,7 +2111,12 @@ Bulk review complete: ${ok} ok, ${failed} failed`);
1978
2111
  console.log(`Review submitted: ${result.reviewId}`);
1979
2112
  analyticsService.capture("review_submitted", {
1980
2113
  runId,
1981
- success: options.success
2114
+ success: options.success,
2115
+ accuracy,
2116
+ value,
2117
+ reliability,
2118
+ hasContent: !!options.content,
2119
+ resolvedByCapability: !!options.capability
1982
2120
  });
1983
2121
  } catch (err) {
1984
2122
  console.error(err instanceof Error ? err.message : "Review failed");
@@ -2003,6 +2141,9 @@ Examples:
2003
2141
  "--limit <n>",
2004
2142
  "Max rows (1-100, default 25)",
2005
2143
  (v) => Number.parseInt(v, 10)
2144
+ ).option(
2145
+ "--agent <name>",
2146
+ "Identify your agent host for this invocation. Overrides auto-detect for this call only."
2006
2147
  ).action(
2007
2148
  async (options) => {
2008
2149
  try {
@@ -2084,6 +2225,9 @@ var searchCommand = (appContext) => new Command8("search").description("Search f
2084
2225
  ).option(
2085
2226
  "--exclude-source <source>",
2086
2227
  "Exclude results from this crawl source"
2228
+ ).option(
2229
+ "--agent <name>",
2230
+ "Identify your agent host for this invocation (e.g. claude-web, codex). Overrides auto-detect for this call only."
2087
2231
  ).action(
2088
2232
  async (query, options) => {
2089
2233
  try {
@@ -2118,8 +2262,24 @@ var searchCommand = (appContext) => new Command8("search").description("Search f
2118
2262
  excludeSource: options.excludeSource
2119
2263
  });
2120
2264
  analyticsService.capture("search_executed", {
2121
- query,
2122
- resultCount: result.capabilities.length
2265
+ query: truncateQuery(query),
2266
+ queryLength: query.length,
2267
+ resultCount: result.capabilities.length,
2268
+ searchId: result.searchId,
2269
+ total: result.total,
2270
+ hasMore: result.hasMore,
2271
+ offset: options.offset,
2272
+ limit: options.limit,
2273
+ freeOnly: options.free ?? false,
2274
+ maxCost: options.maxCost,
2275
+ minRating: options.minRating,
2276
+ protocol: options.protocol,
2277
+ minTrust: options.minTrust,
2278
+ availabilityStatus: options.status,
2279
+ includeAll: options.all ?? false,
2280
+ source: options.source,
2281
+ excludeSource: options.excludeSource,
2282
+ json: options.json ?? false
2123
2283
  });
2124
2284
  if (options.json) {
2125
2285
  console.log(JSON.stringify(result, null, 2));
@@ -2309,10 +2469,79 @@ var walletCommand = (appContext) => {
2309
2469
  return cmd;
2310
2470
  };
2311
2471
 
2472
+ // src/commands/welcome-command.ts
2473
+ import { existsSync as existsSync4, readFileSync as readFileSync6 } from "fs";
2474
+ import { homedir as homedir4 } from "os";
2475
+ import { join as join4 } from "path";
2476
+ import { Command as Command11 } from "commander";
2477
+ import open2 from "open";
2478
+ import { getAddress } from "viem";
2479
+ import { privateKeyToAccount as privateKeyToAccount3 } from "viem/accounts";
2480
+ var readPrivateKey = () => {
2481
+ const configPath = join4(homedir4(), ".zero", "config.json");
2482
+ if (!existsSync4(configPath)) return null;
2483
+ try {
2484
+ const config = JSON.parse(readFileSync6(configPath, "utf8"));
2485
+ if (typeof config.privateKey === "string") {
2486
+ return config.privateKey;
2487
+ }
2488
+ } catch {
2489
+ return null;
2490
+ }
2491
+ return null;
2492
+ };
2493
+ var welcomeCommand = (appContext) => new Command11("welcome").description("Claim your $5 welcome bonus.").action(async () => {
2494
+ const { analyticsService } = appContext.services;
2495
+ analyticsService.capture("welcome_started", {});
2496
+ try {
2497
+ let privateKey = readPrivateKey();
2498
+ if (!privateKey) {
2499
+ await runInit(appContext);
2500
+ privateKey = readPrivateKey();
2501
+ if (!privateKey) {
2502
+ throw new Error("Wallet initialization failed");
2503
+ }
2504
+ }
2505
+ const account = privateKeyToAccount3(privateKey);
2506
+ const walletAddress = getAddress(account.address);
2507
+ const walletSignature = await account.signMessage({
2508
+ message: walletAddress
2509
+ });
2510
+ const url = new URL("/welcome", appContext.env.ZERO_WEB_URL);
2511
+ url.searchParams.set("wallet", walletAddress);
2512
+ url.searchParams.set("walletSignature", walletSignature);
2513
+ console.log(
2514
+ `Opening ${url.toString()}
2515
+
2516
+ If your browser didn't open, paste the URL above.`
2517
+ );
2518
+ await open2(url.toString());
2519
+ analyticsService.capture("welcome_link_opened", {
2520
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
2521
+ wallet_address: walletAddress
2522
+ });
2523
+ } catch (err) {
2524
+ analyticsService.capture("welcome_failed", {
2525
+ error: truncateError(
2526
+ err instanceof Error ? err.message : String(err)
2527
+ )
2528
+ });
2529
+ throw err;
2530
+ }
2531
+ });
2532
+
2312
2533
  // src/app.ts
2313
2534
  var createApp = (appContext) => {
2314
2535
  const { analyticsService } = appContext.services;
2315
- 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) => {
2536
+ const program = new Command12().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) => {
2537
+ const agentFlag = actionCommand.opts().agent;
2538
+ if (typeof agentFlag === "string" && agentFlag.trim().length > 0) {
2539
+ analyticsService.setAgentHost(agentFlag.trim());
2540
+ }
2541
+ appContext.invocation.current = {
2542
+ command: actionCommand.name(),
2543
+ startMs: Date.now()
2544
+ };
2316
2545
  analyticsService.capture("command_executed", {
2317
2546
  command: actionCommand.name()
2318
2547
  });
@@ -2327,6 +2556,7 @@ var createApp = (appContext) => {
2327
2556
  program.addCommand(walletCommand(appContext));
2328
2557
  program.addCommand(configCommand(appContext));
2329
2558
  program.addCommand(termsCommand(appContext));
2559
+ program.addCommand(welcomeCommand(appContext));
2330
2560
  return program;
2331
2561
  };
2332
2562
 
@@ -2334,7 +2564,9 @@ var createApp = (appContext) => {
2334
2564
  import z4 from "zod";
2335
2565
  var envSchema = z4.object({
2336
2566
  ZERO_API_URL: z4.string().default("https://api.zero.xyz"),
2337
- ZERO_PRIVATE_KEY: z4.string().optional()
2567
+ ZERO_WEB_URL: z4.string().default("https://zero.xyz"),
2568
+ ZERO_PRIVATE_KEY: z4.string().optional(),
2569
+ ZERO_ENV: z4.enum(["development", "production"]).default("production")
2338
2570
  });
2339
2571
  var getEnv = () => {
2340
2572
  try {
@@ -2347,14 +2579,15 @@ var getEnv = () => {
2347
2579
  };
2348
2580
 
2349
2581
  // src/app/app-services.ts
2350
- import { existsSync as existsSync6, readFileSync as readFileSync8 } from "fs";
2351
- import { homedir as homedir4 } from "os";
2352
- import { join as join5 } from "path";
2353
- import { privateKeyToAccount as privateKeyToAccount3 } from "viem/accounts";
2582
+ import { randomUUID as randomUUID2 } from "crypto";
2583
+ import { existsSync as existsSync7, readFileSync as readFileSync9 } from "fs";
2584
+ import { homedir as homedir5 } from "os";
2585
+ import { join as join6 } from "path";
2586
+ import { privateKeyToAccount as privateKeyToAccount4 } from "viem/accounts";
2354
2587
 
2355
2588
  // src/services/analytics-service.ts
2356
2589
  import { randomUUID } from "crypto";
2357
- import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
2590
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
2358
2591
  import { dirname as dirname2 } from "path";
2359
2592
  import { PostHog } from "posthog-node";
2360
2593
  var POSTHOG_API_KEY = "phc_B2vLyNxAf2mnqvdPQajf4d4b2iXc35dep2ZrvebMJLuX";
@@ -2362,14 +2595,22 @@ var POSTHOG_HOST = "https://us.i.posthog.com";
2362
2595
  var AnalyticsService = class {
2363
2596
  posthog;
2364
2597
  distinctId;
2598
+ walletAddress;
2365
2599
  cliVersion;
2600
+ environment;
2601
+ requestId;
2602
+ agentHost;
2366
2603
  constructor(opts) {
2367
2604
  this.cliVersion = opts.cliVersion;
2605
+ this.environment = opts.environment;
2606
+ this.walletAddress = opts.walletAddress;
2607
+ this.requestId = opts.requestId;
2608
+ this.agentHost = opts.agentHost;
2368
2609
  let telemetryEnabled = true;
2369
2610
  let persistedAnonId;
2370
2611
  try {
2371
- if (existsSync4(opts.configPath)) {
2372
- const config = JSON.parse(readFileSync6(opts.configPath, "utf8"));
2612
+ if (existsSync5(opts.configPath)) {
2613
+ const config = JSON.parse(readFileSync7(opts.configPath, "utf8"));
2373
2614
  if (config.telemetry === false) {
2374
2615
  telemetryEnabled = false;
2375
2616
  }
@@ -2394,7 +2635,7 @@ var AnalyticsService = class {
2394
2635
  try {
2395
2636
  const dir = dirname2(opts.configPath);
2396
2637
  mkdirSync4(dir, { recursive: true });
2397
- const existing = existsSync4(opts.configPath) ? JSON.parse(readFileSync6(opts.configPath, "utf8")) : {};
2638
+ const existing = existsSync5(opts.configPath) ? JSON.parse(readFileSync7(opts.configPath, "utf8")) : {};
2398
2639
  writeFileSync4(
2399
2640
  opts.configPath,
2400
2641
  JSON.stringify({ ...existing, anonId: newAnonId }, null, 2)
@@ -2417,6 +2658,40 @@ var AnalyticsService = class {
2417
2658
  });
2418
2659
  this.posthog.on("error", () => {
2419
2660
  });
2661
+ if (opts.walletAddress && persistedAnonId) {
2662
+ this.maybeAliasAnonToWallet(
2663
+ opts.configPath,
2664
+ persistedAnonId,
2665
+ opts.walletAddress
2666
+ );
2667
+ }
2668
+ }
2669
+ maybeAliasAnonToWallet(configPath, anonId, walletAddress) {
2670
+ if (!this.posthog) return;
2671
+ if (anonId === walletAddress) return;
2672
+ let aliasedTo;
2673
+ try {
2674
+ const config = JSON.parse(readFileSync7(configPath, "utf8"));
2675
+ if (typeof config.aliasedTo === "string") {
2676
+ aliasedTo = config.aliasedTo;
2677
+ }
2678
+ } catch {
2679
+ }
2680
+ if (aliasedTo === walletAddress) return;
2681
+ this.posthog.alias({ distinctId: walletAddress, alias: anonId });
2682
+ try {
2683
+ const config = existsSync5(configPath) ? JSON.parse(readFileSync7(configPath, "utf8")) : {};
2684
+ writeFileSync4(
2685
+ configPath,
2686
+ JSON.stringify({ ...config, aliasedTo: walletAddress }, null, 2)
2687
+ );
2688
+ } catch {
2689
+ }
2690
+ }
2691
+ // Per-invocation override applied by the preAction hook when `--agent`
2692
+ // is passed. Stateless — affects only this process.
2693
+ setAgentHost(next) {
2694
+ this.agentHost = next;
2420
2695
  }
2421
2696
  capture(event, properties) {
2422
2697
  if (!this.posthog) return;
@@ -2427,6 +2702,13 @@ var AnalyticsService = class {
2427
2702
  source: "cli",
2428
2703
  // biome-ignore lint/style/useNamingConvention: snake_case is standard for analytics event properties
2429
2704
  cli_version: this.cliVersion,
2705
+ environment: this.environment,
2706
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
2707
+ wallet_address: this.walletAddress,
2708
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
2709
+ request_id: this.requestId,
2710
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
2711
+ agent_host: this.agentHost,
2430
2712
  ...properties
2431
2713
  }
2432
2714
  });
@@ -2441,12 +2723,12 @@ var AnalyticsService = class {
2441
2723
  };
2442
2724
 
2443
2725
  // src/services/state-service.ts
2444
- import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
2445
- import { join as join4 } from "path";
2726
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
2727
+ import { join as join5 } from "path";
2446
2728
  var StateService = class {
2447
2729
  constructor(zeroDir) {
2448
2730
  this.zeroDir = zeroDir;
2449
- this.lastSearchPath = join4(zeroDir, "last_search.json");
2731
+ this.lastSearchPath = join5(zeroDir, "last_search.json");
2450
2732
  }
2451
2733
  lastSearchPath;
2452
2734
  saveLastSearch = (data) => {
@@ -2455,8 +2737,8 @@ var StateService = class {
2455
2737
  };
2456
2738
  loadLastSearch = () => {
2457
2739
  try {
2458
- if (!existsSync5(this.lastSearchPath)) return null;
2459
- const raw = readFileSync7(this.lastSearchPath, "utf8");
2740
+ if (!existsSync6(this.lastSearchPath)) return null;
2741
+ const raw = readFileSync8(this.lastSearchPath, "utf8");
2460
2742
  return JSON.parse(raw);
2461
2743
  } catch {
2462
2744
  return null;
@@ -2476,7 +2758,7 @@ var WalletService = class {
2476
2758
  if (!this.address) return null;
2477
2759
  if (this.balanceGetter) {
2478
2760
  try {
2479
- const result = await this.balanceGetter("base");
2761
+ const result = await this.balanceGetter();
2480
2762
  return { amount: result.amount, asset: result.asset };
2481
2763
  } catch {
2482
2764
  return {
@@ -2491,16 +2773,25 @@ var WalletService = class {
2491
2773
  getLowBalanceWarning = () => this.config.lowBalanceWarning;
2492
2774
  };
2493
2775
 
2776
+ // src/util/agent-host.ts
2777
+ var detectAgentHost = (env = process.env) => {
2778
+ if (env.ZERO_AGENT) return env.ZERO_AGENT;
2779
+ if (env.CLAUDECODE === "1") return "claude-code";
2780
+ if (env.CURSOR_TRACE_ID) return "cursor";
2781
+ if (env.TERM_PROGRAM === "vscode") return "vscode";
2782
+ return "unknown";
2783
+ };
2784
+
2494
2785
  // src/app/app-services.ts
2495
2786
  var CLI_VERSION = package_default.version;
2496
2787
  var getServices = (env) => {
2497
2788
  let privateKey = env.ZERO_PRIVATE_KEY ? env.ZERO_PRIVATE_KEY : null;
2498
- const zeroDir = join5(homedir4(), ".zero");
2499
- const configPath = join5(zeroDir, "config.json");
2789
+ const zeroDir = join6(homedir5(), ".zero");
2790
+ const configPath = join6(zeroDir, "config.json");
2500
2791
  if (!privateKey) {
2501
2792
  try {
2502
- if (existsSync6(configPath)) {
2503
- const config = JSON.parse(readFileSync8(configPath, "utf8"));
2793
+ if (existsSync7(configPath)) {
2794
+ const config = JSON.parse(readFileSync9(configPath, "utf8"));
2504
2795
  if (typeof config.privateKey === "string") {
2505
2796
  privateKey = config.privateKey;
2506
2797
  }
@@ -2508,11 +2799,11 @@ var getServices = (env) => {
2508
2799
  } catch {
2509
2800
  }
2510
2801
  }
2511
- const account = privateKey ? privateKeyToAccount3(privateKey) : null;
2802
+ const account = privateKey ? privateKeyToAccount4(privateKey) : null;
2512
2803
  let lowBalanceWarning = 1;
2513
2804
  try {
2514
- if (existsSync6(configPath)) {
2515
- const config = JSON.parse(readFileSync8(configPath, "utf8"));
2805
+ if (existsSync7(configPath)) {
2806
+ const config = JSON.parse(readFileSync9(configPath, "utf8"));
2516
2807
  if (typeof config.lowBalanceWarning === "number") {
2517
2808
  lowBalanceWarning = config.lowBalanceWarning;
2518
2809
  }
@@ -2527,12 +2818,15 @@ var getServices = (env) => {
2527
2818
  {
2528
2819
  lowBalanceWarning
2529
2820
  },
2530
- paymentService.getBalance
2821
+ paymentService.getTotalBalance
2531
2822
  );
2532
2823
  const analyticsService = new AnalyticsService({
2533
2824
  walletAddress: account?.address ?? null,
2534
2825
  configPath,
2535
- cliVersion: CLI_VERSION
2826
+ cliVersion: CLI_VERSION,
2827
+ environment: env.ZERO_ENV,
2828
+ requestId: randomUUID2(),
2829
+ agentHost: detectAgentHost()
2536
2830
  });
2537
2831
  return {
2538
2832
  analyticsService,
@@ -2551,7 +2845,8 @@ var createAppContext = () => {
2551
2845
  }
2552
2846
  return {
2553
2847
  env,
2554
- services: getServices(env)
2848
+ services: getServices(env),
2849
+ invocation: { current: null }
2555
2850
  };
2556
2851
  };
2557
2852
 
@@ -2563,15 +2858,27 @@ var main = async () => {
2563
2858
  process.exit(1);
2564
2859
  }
2565
2860
  const app = createApp(appContext);
2861
+ let caughtError = null;
2566
2862
  try {
2567
2863
  await app.parseAsync(process.argv);
2568
2864
  } catch (err) {
2569
2865
  const isCommanderExit = err instanceof Error && "exitCode" in err && err.exitCode === 0;
2570
2866
  if (!isCommanderExit) {
2571
- console.error(err instanceof Error ? err.message : "Unexpected error");
2867
+ caughtError = err instanceof Error ? err : new Error(String(err));
2868
+ console.error(caughtError.message);
2572
2869
  process.exitCode = 1;
2573
2870
  }
2574
2871
  } finally {
2872
+ const invocation = appContext.invocation.current;
2873
+ if (invocation) {
2874
+ appContext.services.analyticsService.capture("command_completed", {
2875
+ command: invocation.command,
2876
+ success: !caughtError && process.exitCode !== 1,
2877
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
2878
+ duration_ms: Date.now() - invocation.startMs,
2879
+ ...caughtError && { error: truncateError(caughtError.message) }
2880
+ });
2881
+ }
2575
2882
  await appContext.services.analyticsService.shutdown();
2576
2883
  }
2577
2884
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeroxyz/cli",
3
- "version": "0.0.25",
3
+ "version": "0.0.27",
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`