@zeroxyz/cli 0.0.37 → 0.0.39

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 +807 -221
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { homedir as homedir7 } from "os";
5
- import { join as join8 } from "path";
4
+ import { homedir as homedir8 } from "os";
5
+ import { join as join9 } from "path";
6
6
 
7
7
  // package.json
8
8
  var package_default = {
9
9
  name: "@zeroxyz/cli",
10
- version: "0.0.37",
10
+ version: "0.0.39",
11
11
  type: "module",
12
12
  bin: {
13
13
  zero: "dist/index.js",
@@ -61,12 +61,124 @@ var package_default = {
61
61
  };
62
62
 
63
63
  // src/app.ts
64
- import { Command as Command12 } from "commander";
64
+ import { Command as Command13 } from "commander";
65
+
66
+ // src/commands/auth-command.ts
67
+ import { homedir } from "os";
68
+ import { join } from "path";
69
+ import { Command } from "commander";
70
+ import open from "open";
71
+
72
+ // src/util/secure-config.ts
73
+ import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "fs";
74
+ var SECURE_DIR_MODE = 448;
75
+ var SECURE_FILE_MODE = 384;
76
+ var ensureSecureDir = (path) => {
77
+ mkdirSync(path, { recursive: true, mode: SECURE_DIR_MODE });
78
+ chmodSync(path, SECURE_DIR_MODE);
79
+ };
80
+ var writeSecureFile = (path, contents) => {
81
+ writeFileSync(path, contents, { mode: SECURE_FILE_MODE });
82
+ chmodSync(path, SECURE_FILE_MODE);
83
+ };
84
+ var readConfig = (path) => {
85
+ try {
86
+ const raw = readFileSync(path, "utf8");
87
+ return JSON.parse(raw);
88
+ } catch {
89
+ return {};
90
+ }
91
+ };
92
+
93
+ // src/commands/auth-command.ts
94
+ var getConfigPath = () => join(homedir(), ".zero", "config.json");
95
+ var authLoginCommand = (appContext) => new Command("login").description("Sign in to Zero").option("--no-open", "Do not open the browser automatically").action(async (opts) => {
96
+ const { apiService } = appContext.services;
97
+ const start = await apiService.startDeviceLogin();
98
+ const url = `${start.verificationUri}?code=${start.userCode}`;
99
+ process.stdout.write(
100
+ `Open this URL to authorize:
101
+ ${url}
102
+ User code: ${start.userCode}
103
+ `
104
+ );
105
+ if (opts.open) {
106
+ await open(url).catch(() => {
107
+ });
108
+ }
109
+ while (Date.now() < start.expiresAt) {
110
+ await new Promise(
111
+ (r) => setTimeout(r, start.pollInterval * 1e3)
112
+ );
113
+ const result = await apiService.pollDeviceLogin(start.deviceCode);
114
+ if (result.status === "pending") {
115
+ process.stdout.write(".");
116
+ continue;
117
+ }
118
+ if (result.status === "expired") {
119
+ process.stderr.write(
120
+ "\nDevice code expired. Run `zero auth login` again.\n"
121
+ );
122
+ process.exitCode = 1;
123
+ return;
124
+ }
125
+ const path = getConfigPath();
126
+ const current = readConfig(path);
127
+ const next = {
128
+ ...current,
129
+ session: {
130
+ userId: result.user.id,
131
+ authMethod: "workos",
132
+ accessToken: result.accessToken,
133
+ refreshToken: result.refreshToken
134
+ }
135
+ };
136
+ writeSecureFile(path, JSON.stringify(next, null, 2));
137
+ process.stdout.write(
138
+ `
139
+ Signed in as ${result.user.email ?? result.user.id}
140
+ `
141
+ );
142
+ return;
143
+ }
144
+ process.stderr.write(
145
+ "\nDevice code expired. Run `zero auth login` again.\n"
146
+ );
147
+ process.exitCode = 1;
148
+ });
149
+ var authLogoutCommand = (appContext) => new Command("logout").description("Sign out of Zero").action(async () => {
150
+ const { apiService } = appContext.services;
151
+ const path = getConfigPath();
152
+ const config = readConfig(path);
153
+ if (config.session) {
154
+ await apiService.logout(config.session.refreshToken);
155
+ }
156
+ const { session: _drop, ...rest } = config;
157
+ writeSecureFile(path, JSON.stringify(rest, null, 2));
158
+ process.stdout.write("Signed out.\n");
159
+ });
160
+ var authWhoamiCommand = (appContext) => new Command("whoami").description("Print the current Zero identity").action(async () => {
161
+ const { apiService } = appContext.services;
162
+ const me = await apiService.getMe();
163
+ process.stdout.write(
164
+ `${me.email ?? me.id}
165
+ uid: ${me.id}
166
+ authMethod: ${me.authMethod}
167
+ `
168
+ );
169
+ });
170
+ var authCommand = (appContext) => {
171
+ const cmd = new Command("auth").description("Authentication commands");
172
+ cmd.addCommand(authLoginCommand(appContext));
173
+ cmd.addCommand(authLogoutCommand(appContext));
174
+ cmd.addCommand(authWhoamiCommand(appContext), { hidden: true });
175
+ return cmd;
176
+ };
65
177
 
66
178
  // src/commands/bug-report-command.ts
67
179
  import { createHash as createHash2 } from "crypto";
68
- import { readFileSync } from "fs";
69
- import { Command } from "commander";
180
+ import { readFileSync as readFileSync2 } from "fs";
181
+ import { Command as Command2 } from "commander";
70
182
  import { z as z2 } from "zod";
71
183
 
72
184
  // src/services/api-service.ts
@@ -115,6 +227,7 @@ var capabilityResponseSchema = z.object({
115
227
  responseSchema: z.record(z.string(), z.unknown()).nullable(),
116
228
  example: z.object({ request: z.unknown(), response: z.unknown() }).nullable(),
117
229
  tags: z.array(z.string()).nullable(),
230
+ exampleAgentPrompt: z.string().nullable().optional(),
118
231
  displayCostAmount: z.string(),
119
232
  displayCostAsset: z.string(),
120
233
  reviewCount: z.number(),
@@ -224,18 +337,48 @@ var listRunsResponseSchema = z.object({
224
337
  runs: z.array(runListItemSchema),
225
338
  nextCursor: z.string().nullable()
226
339
  });
340
+ var userDtoSchema = z.object({
341
+ id: z.string(),
342
+ email: z.string().nullable(),
343
+ authMethod: z.string(),
344
+ createdAt: z.union([z.string(), z.date()]).optional(),
345
+ lastLoginAt: z.union([z.string(), z.date()]).nullable().optional()
346
+ });
347
+ var deviceStartResultSchema = z.object({
348
+ deviceCode: z.string(),
349
+ userCode: z.string(),
350
+ verificationUri: z.string(),
351
+ pollInterval: z.number(),
352
+ expiresAt: z.number()
353
+ });
354
+ var devicePollResponseSchema = z.union([
355
+ z.object({ error: z.literal("authorization_pending") }),
356
+ z.object({ error: z.literal("expired_token") }),
357
+ z.object({
358
+ accessToken: z.string(),
359
+ refreshToken: z.string(),
360
+ expiresIn: z.number(),
361
+ user: userDtoSchema
362
+ })
363
+ ]);
227
364
  var buildCanonicalMessage = (method, path, body, timestamp, nonce) => {
228
365
  const bodyHash = createHash("sha256").update(body ?? "").digest("hex");
229
366
  return `${method}:${path}:${bodyHash}:${timestamp}:${nonce}`;
230
367
  };
231
- var ApiService = class {
232
- constructor(baseUrl, account) {
368
+ var ApiService = class _ApiService {
369
+ constructor(baseUrl, account, credentials = { kind: "none" }, onSessionRefreshed = async () => {
370
+ }) {
233
371
  this.baseUrl = baseUrl;
234
372
  this.account = account;
235
373
  this.walletAddress = this.account?.address ?? null;
374
+ this.credentials = credentials;
375
+ this.onSessionRefreshed = onSessionRefreshed;
236
376
  }
237
377
  walletAddress;
238
378
  account;
379
+ credentials;
380
+ onSessionRefreshed;
381
+ withAccount = (account) => new _ApiService(this.baseUrl, account);
239
382
  signRequest = async (method, path, body) => {
240
383
  if (!this.account) throw new Error("No private key configured");
241
384
  const timestamp = Math.floor(Date.now() / 1e3).toString();
@@ -249,21 +392,59 @@ var ApiService = class {
249
392
  "x-zero-signature": signature
250
393
  };
251
394
  };
252
- request = async (method, path, body) => {
253
- const url = `${this.baseUrl}${path}`;
254
- const bodyStr = body ? JSON.stringify(body) : void 0;
255
- const headers = {
395
+ // Session credentials take precedence over EIP-191. We never combine them
396
+ // in a single request: if the user has signed in, the JWT is the identity
397
+ // the API trusts, and the wallet signing path stays out of the picture.
398
+ buildHeaders = async (method, path, bodyStr) => {
399
+ const base2 = {
256
400
  "content-type": "application/json"
257
401
  };
402
+ if (this.credentials.kind === "session") {
403
+ base2.authorization = `Bearer ${this.credentials.accessToken}`;
404
+ return base2;
405
+ }
258
406
  if (this.account) {
259
407
  const walletHeaders = await this.signRequest(method, path, bodyStr);
260
- Object.assign(headers, walletHeaders);
408
+ return { ...base2, ...walletHeaders };
261
409
  }
262
- const response = await fetch(url, {
410
+ return base2;
411
+ };
412
+ // Rotates the in-memory tokens and fires onSessionRefreshed so the
413
+ // caller can persist them. Returns false on any refresh failure — the
414
+ // outer request then propagates the original 401 to the caller.
415
+ tryRefreshSession = async () => {
416
+ if (this.credentials.kind !== "session") return false;
417
+ const { refreshToken } = this.credentials;
418
+ const resp = await fetch(`${this.baseUrl}/v1/auth/refresh`, {
419
+ method: "POST",
420
+ headers: { "content-type": "application/json" },
421
+ body: JSON.stringify({ refreshToken })
422
+ });
423
+ if (!resp.ok) return false;
424
+ const body = await resp.json();
425
+ this.credentials = {
426
+ ...this.credentials,
427
+ accessToken: body.accessToken,
428
+ refreshToken: body.refreshToken
429
+ };
430
+ await this.onSessionRefreshed(body);
431
+ return true;
432
+ };
433
+ request = async (method, path, body) => {
434
+ const url = `${this.baseUrl}${path}`;
435
+ const bodyStr = body ? JSON.stringify(body) : void 0;
436
+ const makeInit = async () => ({
263
437
  method,
264
- headers,
438
+ headers: await this.buildHeaders(method, path, bodyStr),
265
439
  body: bodyStr
266
440
  });
441
+ let response = await fetch(url, await makeInit());
442
+ if (response.status === 401 && this.credentials.kind === "session") {
443
+ const refreshed = await this.tryRefreshSession();
444
+ if (refreshed) {
445
+ response = await fetch(url, await makeInit());
446
+ }
447
+ }
267
448
  if (!response.ok) {
268
449
  const errorBody = await response.json().catch(() => ({ error: "Unknown error" }));
269
450
  throw new Error(
@@ -272,6 +453,44 @@ var ApiService = class {
272
453
  }
273
454
  return response.json();
274
455
  };
456
+ startDeviceLogin = async () => {
457
+ const resp = await fetch(`${this.baseUrl}/v1/auth/device/start`, {
458
+ method: "POST"
459
+ });
460
+ if (!resp.ok) throw new Error(`device_start_${resp.status}`);
461
+ return deviceStartResultSchema.parse(await resp.json());
462
+ };
463
+ pollDeviceLogin = async (deviceCode) => {
464
+ const resp = await fetch(`${this.baseUrl}/v1/auth/device/poll`, {
465
+ method: "POST",
466
+ headers: { "content-type": "application/json" },
467
+ body: JSON.stringify({ deviceCode })
468
+ });
469
+ if (!resp.ok) throw new Error(`device_poll_${resp.status}`);
470
+ const parsed = devicePollResponseSchema.parse(await resp.json());
471
+ if ("error" in parsed) {
472
+ return parsed.error === "authorization_pending" ? { status: "pending" } : { status: "expired" };
473
+ }
474
+ return {
475
+ status: "ok",
476
+ accessToken: parsed.accessToken,
477
+ refreshToken: parsed.refreshToken,
478
+ expiresIn: parsed.expiresIn,
479
+ user: parsed.user
480
+ };
481
+ };
482
+ logout = async (refreshToken) => {
483
+ await fetch(`${this.baseUrl}/v1/auth/logout`, {
484
+ method: "POST",
485
+ headers: { "content-type": "application/json" },
486
+ body: JSON.stringify({ refreshToken })
487
+ }).catch(() => {
488
+ });
489
+ };
490
+ getMe = async () => {
491
+ const json = await this.request("GET", "/v1/users/me");
492
+ return userDtoSchema.parse(json);
493
+ };
275
494
  search = async (options) => {
276
495
  const json = await this.request("POST", "/v1/search", options);
277
496
  return searchResponseSchema.parse(json);
@@ -349,7 +568,7 @@ var autoIdempotencyKey = (description, contextSeed) => {
349
568
  const seed = `${description}|${contextSeed ?? ""}|${bucket}`;
350
569
  return `auto-${createHash2("sha256").update(seed).digest("hex").slice(0, 16)}`;
351
570
  };
352
- var bugReportCommand = (appContext) => new Command("bug-report").description(
571
+ var bugReportCommand = (appContext) => new Command2("bug-report").description(
353
572
  "Report a Zero platform bug \u2014 bad search ranking, wrong indexed URL, CLI bugs, billing issues. The CLI auto-attaches your most recent context (last search + last run), auto-derives a title, and lets the server classify the category. For 'this capability returned a bad result', use `zero review` instead."
354
573
  ).addHelpText(
355
574
  "after",
@@ -426,7 +645,7 @@ Categories the classifier picks from:
426
645
  return;
427
646
  }
428
647
  if (options.fromFile) {
429
- const contents = readFileSync(options.fromFile, "utf8");
648
+ const contents = readFileSync2(options.fromFile, "utf8");
430
649
  const lines = contents.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
431
650
  let ok = 0;
432
651
  let failed = 0;
@@ -563,21 +782,21 @@ Bulk bug-report complete: ${ok} ok, ${failed} failed`
563
782
  );
564
783
 
565
784
  // src/commands/config-command.ts
566
- import { existsSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
567
- import { homedir } from "os";
568
- import { join } from "path";
569
- import { Command as Command2 } from "commander";
785
+ import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
786
+ import { homedir as homedir2 } from "os";
787
+ import { join as join2 } from "path";
788
+ import { Command as Command3 } from "commander";
570
789
  var VALID_KEYS = ["lowBalanceWarning", "auth", "telemetry"];
571
790
  var loadConfig = (configPath) => {
572
791
  try {
573
792
  if (!existsSync(configPath)) return {};
574
- return JSON.parse(readFileSync2(configPath, "utf8"));
793
+ return JSON.parse(readFileSync3(configPath, "utf8"));
575
794
  } catch {
576
795
  return {};
577
796
  }
578
797
  };
579
- var configCommand = (_appContext) => new Command2("config").description("View or update CLI configuration").option("--set <keyValue>", "Set a config value (key=value)").action((options) => {
580
- const configPath = join(homedir(), ".zero", "config.json");
798
+ var configCommand = (_appContext) => new Command3("config").description("View or update CLI configuration").option("--set <keyValue>", "Set a config value (key=value)").action((options) => {
799
+ const configPath = join2(homedir2(), ".zero", "config.json");
581
800
  if (!options.set) {
582
801
  const config2 = loadConfig(configPath);
583
802
  console.log(JSON.stringify(config2, null, 2));
@@ -607,16 +826,16 @@ var configCommand = (_appContext) => new Command2("config").description("View or
607
826
  }
608
827
  const config = loadConfig(configPath);
609
828
  config[key] = value;
610
- mkdirSync(join(homedir(), ".zero"), { recursive: true });
611
- writeFileSync(configPath, JSON.stringify(config, null, 2));
829
+ mkdirSync2(join2(homedir2(), ".zero"), { recursive: true });
830
+ writeFileSync2(configPath, JSON.stringify(config, null, 2));
612
831
  console.log(`Set ${key} = ${JSON.stringify(value)}`);
613
832
  });
614
833
 
615
834
  // src/commands/fetch-command.ts
616
835
  import { createHash as createHash3 } from "crypto";
617
- import { readFileSync as readFileSync3 } from "fs";
836
+ import { readFileSync as readFileSync4 } from "fs";
618
837
  import { resolve as resolvePath } from "path";
619
- import { Command as Command3 } from "commander";
838
+ import { Command as Command4 } from "commander";
620
839
  import { formatUnits as formatUnits2 } from "viem";
621
840
 
622
841
  // src/services/payment-service.ts
@@ -1120,8 +1339,9 @@ var PaymentService = class {
1120
1339
  return { viemChain: tempoTestnetChain, token: PATHUSD_TEMPO };
1121
1340
  }
1122
1341
  };
1123
- getBalanceRaw = async (chain) => {
1124
- if (!this.account) return 0n;
1342
+ getBalanceRaw = async (chain, address) => {
1343
+ const target = address ?? this.account?.address;
1344
+ if (!target) return 0n;
1125
1345
  const { viemChain, token } = this.resolveChainConfig(chain);
1126
1346
  const client = createPublicClient({
1127
1347
  chain: viemChain,
@@ -1131,7 +1351,7 @@ var PaymentService = class {
1131
1351
  address: token,
1132
1352
  abi: ERC20_BALANCE_ABI,
1133
1353
  functionName: "balanceOf",
1134
- args: [this.account.address]
1354
+ args: [target]
1135
1355
  });
1136
1356
  return balance;
1137
1357
  };
@@ -1151,6 +1371,13 @@ var PaymentService = class {
1151
1371
  ]);
1152
1372
  return { amount: formatUnits(baseRaw + tempoRaw, 6), asset: "USDC" };
1153
1373
  };
1374
+ getTotalBalanceForAddress = async (address) => {
1375
+ const [baseRaw, tempoRaw] = await Promise.all([
1376
+ this.getBalanceRaw("base", address),
1377
+ this.getBalanceRaw("tempo", address)
1378
+ ]);
1379
+ return { amount: formatUnits(baseRaw + tempoRaw, 6), asset: "USDC" };
1380
+ };
1154
1381
  };
1155
1382
 
1156
1383
  // src/util/infer-schema.ts
@@ -1237,30 +1464,69 @@ var isJsonContentType = (contentType) => {
1237
1464
  const ct = contentType.toLowerCase().split(";")[0]?.trim() ?? "";
1238
1465
  return ct === "application/json" || ct.endsWith("+json");
1239
1466
  };
1240
- var MAX_REQUEST_BODY_BYTES = 10 * 1024 * 1024;
1241
- var resolveRequestBody = (rawData, readStdin) => {
1467
+ var MAX_INLINE_REQUEST_BODY_BYTES = 10 * 1024 * 1024;
1468
+ var MAX_FILE_REQUEST_BODY_BYTES = 500 * 1024 * 1024;
1469
+ var UPSTREAM_ERROR_FIELDS = [
1470
+ "error",
1471
+ "message",
1472
+ "detail",
1473
+ "reason",
1474
+ "error_description"
1475
+ ];
1476
+ var UPSTREAM_ERROR_MAX_LEN = 200;
1477
+ var clampUpstreamMessage = (value) => {
1478
+ const trimmed = value.trim();
1479
+ if (!trimmed) return "";
1480
+ return trimmed.length > UPSTREAM_ERROR_MAX_LEN ? `${trimmed.slice(0, UPSTREAM_ERROR_MAX_LEN)}\u2026` : trimmed;
1481
+ };
1482
+ var extractUpstreamErrorMessage = (body) => {
1483
+ const parsed = tryParseJson(body);
1484
+ if (parsed === null || typeof parsed !== "object") {
1485
+ const clipped = clampUpstreamMessage(body);
1486
+ return clipped || void 0;
1487
+ }
1488
+ const record = parsed;
1489
+ for (const field of UPSTREAM_ERROR_FIELDS) {
1490
+ const value = record[field];
1491
+ if (typeof value === "string" && value.length > 0) {
1492
+ return clampUpstreamMessage(value);
1493
+ }
1494
+ if (value && typeof value === "object") {
1495
+ const nested = value.message;
1496
+ if (typeof nested === "string" && nested.length > 0) {
1497
+ return clampUpstreamMessage(nested);
1498
+ }
1499
+ }
1500
+ }
1501
+ return void 0;
1502
+ };
1503
+ var resolveRequestBody = (rawData, readStdin2) => {
1242
1504
  const fromFile = (spec) => {
1243
- if (spec === "@-") return readFileSync3(0);
1244
- return readFileSync3(resolvePath(spec.slice(1)));
1505
+ if (spec === "@-") return readFileSync4(0);
1506
+ return readFileSync4(resolvePath(spec.slice(1)));
1245
1507
  };
1246
- if (readStdin && rawData !== void 0) {
1508
+ if (readStdin2 && rawData !== void 0) {
1247
1509
  throw new Error(
1248
1510
  "Conflicting body sources: use either --data-stdin or -d, not both."
1249
1511
  );
1250
1512
  }
1251
1513
  let body;
1252
- if (readStdin) {
1253
- body = readFileSync3(0);
1514
+ let cap;
1515
+ if (readStdin2) {
1516
+ body = readFileSync4(0);
1517
+ cap = MAX_FILE_REQUEST_BODY_BYTES;
1254
1518
  } else if (rawData?.startsWith("@")) {
1255
1519
  body = fromFile(rawData);
1520
+ cap = MAX_FILE_REQUEST_BODY_BYTES;
1256
1521
  } else {
1257
1522
  body = rawData;
1523
+ cap = MAX_INLINE_REQUEST_BODY_BYTES;
1258
1524
  }
1259
1525
  if (body !== void 0) {
1260
1526
  const bytes = Buffer.isBuffer(body) ? body.length : Buffer.byteLength(body, "utf8");
1261
- if (bytes > MAX_REQUEST_BODY_BYTES) {
1527
+ if (bytes > cap) {
1262
1528
  throw new Error(
1263
- `Request body is ${bytes} bytes \u2014 exceeds the ${MAX_REQUEST_BODY_BYTES} byte limit. Split the payload, compress it, or contact the capability owner about raising the cap.`
1529
+ `Request body is ${bytes} bytes \u2014 exceeds the ${cap} byte local cap. This is a CLI-side guard, not a protocol limit. Compress the payload or contact us if you need it raised.`
1264
1530
  );
1265
1531
  }
1266
1532
  }
@@ -1300,7 +1566,7 @@ var detectPaymentRequirement = async (response) => {
1300
1566
  }
1301
1567
  return { protocol: "unknown", raw: {} };
1302
1568
  };
1303
- var fetchCommand = (appContext) => new Command3("fetch").description(
1569
+ var fetchCommand = (appContext) => new Command4("fetch").description(
1304
1570
  "Fetch a capability URL, handling 402 challenges automatically"
1305
1571
  ).argument(
1306
1572
  "[url]",
@@ -1371,14 +1637,16 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1371
1637
  capName = cap.name;
1372
1638
  }
1373
1639
  const searchResult = await apiService.search({ query: capName });
1374
- const slugFoundInResults = searchResult.capabilities.some(
1640
+ const matchedEntry = searchResult.capabilities.find(
1375
1641
  (c) => c.slug === options.capability || c.id === options.capability
1376
1642
  );
1643
+ const slugFoundInResults = Boolean(matchedEntry);
1377
1644
  stateService.saveLastSearch({
1378
1645
  searchId: searchResult.searchId,
1379
1646
  capabilities: searchResult.capabilities.map((c) => ({
1380
1647
  position: c.position,
1381
1648
  id: c.id,
1649
+ slug: c.slug,
1382
1650
  url: c.url,
1383
1651
  urlTemplate: c.urlTemplate ?? null,
1384
1652
  displayCostAmount: c.cost.amount
@@ -1396,7 +1664,13 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1396
1664
  slugFoundInResults
1397
1665
  });
1398
1666
  analyticsService.capture("capability_viewed", {
1667
+ // Existing field preserved as the raw --capability
1668
+ // input for back-compat.
1399
1669
  capabilityId: options.capability,
1670
+ // New canonical identifier fields hydrated from the
1671
+ // resolved search result (when the slug was found).
1672
+ capabilityUid: matchedEntry?.id,
1673
+ capabilitySlug: matchedEntry?.slug,
1400
1674
  fromLastSearch: false,
1401
1675
  searchId: searchResult.searchId,
1402
1676
  triggeredBy: "slug_handoff"
@@ -1413,7 +1687,7 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1413
1687
  } catch (err) {
1414
1688
  const message = err instanceof Error ? err.message : "Failed to read request body";
1415
1689
  analyticsService.capture("fetch_error", {
1416
- cliErrorClass: /exceeds the .* byte limit/.test(message) ? "payload_too_large" : "schema_validation_failed",
1690
+ cliErrorClass: /exceeds the .* byte local cap/.test(message) ? "payload_too_large" : "schema_validation_failed",
1417
1691
  url: redactUrl(resolvedUrl),
1418
1692
  error: truncateError(message)
1419
1693
  });
@@ -1455,6 +1729,8 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1455
1729
  });
1456
1730
  const matchCtx = explicitCapabilityCtx ?? urlCtx;
1457
1731
  const capabilityId = options.capability ?? matchCtx?.capabilityId ?? null;
1732
+ const capabilityUid = matchCtx?.capabilityUid ?? null;
1733
+ const capabilitySlug = matchCtx?.capabilitySlug ?? null;
1458
1734
  const searchId = matchCtx?.searchId;
1459
1735
  const resultRank = matchCtx?.resultRank;
1460
1736
  const matchedDisplayCostAmount = matchCtx?.displayCostAmount;
@@ -1604,10 +1880,12 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1604
1880
  const isFailure = !finalResponse || typeof status === "number" && (status < 200 || status >= 300);
1605
1881
  let errorSnippetHash;
1606
1882
  let errorSnippetLength;
1883
+ let upstreamErrorMessage;
1607
1884
  if (isFailure && body && !bodyIsBinary) {
1608
1885
  const snippet = body.slice(0, 500);
1609
1886
  errorSnippetHash = createHash3("sha256").update(snippet).digest("hex");
1610
1887
  errorSnippetLength = snippet.length;
1888
+ upstreamErrorMessage = extractUpstreamErrorMessage(body);
1611
1889
  }
1612
1890
  let runId = null;
1613
1891
  if (capabilityId && apiService.walletAddress) {
@@ -1651,7 +1929,7 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1651
1929
  );
1652
1930
  }
1653
1931
  }
1654
- const outcome = !finalResponse ? "network_error" : status === 402 && !paymentMeta ? "payment_failed" : status !== void 0 && status >= 400 && status !== 402 ? "server_error" : "success";
1932
+ const outcome = !finalResponse ? "network_error" : status === 402 && !paymentMeta ? "payment_failed" : status === 402 && paymentMeta ? "payment_rejected" : status !== void 0 && status >= 400 && status !== 402 ? "server_error" : "success";
1655
1933
  analyticsService.capture("fetch_executed", {
1656
1934
  url: redactUrl(resolvedUrl),
1657
1935
  status,
@@ -1660,6 +1938,13 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1660
1938
  hasPayment: !!paymentMeta,
1661
1939
  paymentProtocol: paymentMeta?.protocol,
1662
1940
  paymentAmount: paymentMeta?.amount,
1941
+ // Canonical join keys — capabilitySlug is the dashboard
1942
+ // breakdown dimension; capabilityUid is the stable join
1943
+ // key with the capabilities table. capabilityId is the
1944
+ // back-compat alias (same value as capabilityUid when
1945
+ // resolved, else the raw --capability input).
1946
+ capabilityUid: capabilityUid ?? void 0,
1947
+ capabilitySlug: capabilitySlug ?? void 0,
1663
1948
  capabilityId: capabilityId ?? void 0,
1664
1949
  searchId: searchId ?? void 0,
1665
1950
  resultRank: resultRank ?? void 0,
@@ -1669,15 +1954,21 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1669
1954
  });
1670
1955
  const isFetchFailure = Boolean(fetchError) || !finalResponse || typeof status === "number" && (status < 200 || status >= 300);
1671
1956
  if (isFetchFailure) {
1672
- const cliErrorClass = fetchError || !finalResponse ? "network" : !apiService.walletAddress ? "auth_missing" : "unknown";
1957
+ const cliErrorClass = fetchError || !finalResponse ? "network" : !apiService.walletAddress ? "auth_missing" : status === 402 && paymentMeta ? "payment_rejected" : typeof status === "number" && status >= 500 ? "upstream_5xx" : typeof status === "number" && status >= 400 ? "upstream_4xx" : "unknown";
1673
1958
  analyticsService.capture("fetch_error", {
1674
1959
  cliErrorClass,
1960
+ capabilityUid: capabilityUid ?? void 0,
1961
+ capabilitySlug: capabilitySlug ?? void 0,
1675
1962
  capabilityId: capabilityId ?? void 0,
1676
1963
  searchId: searchId ?? void 0,
1677
1964
  resultRank: resultRank ?? void 0,
1678
1965
  url: redactUrl(resolvedUrl),
1966
+ status: typeof status === "number" ? status : void 0,
1967
+ upstreamErrorMessage,
1968
+ errorSnippetHash,
1969
+ errorSnippetLength,
1679
1970
  error: truncateError(
1680
- fetchError?.message ?? skipReasons.join("; ")
1971
+ fetchError?.message ?? upstreamErrorMessage ?? skipReasons.join("; ")
1681
1972
  ),
1682
1973
  skippedRun: !runId && skipReasons.length > 0
1683
1974
  });
@@ -1685,6 +1976,16 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1685
1976
  if (fetchError && !options.json) {
1686
1977
  console.error(` Fetch failed: ${fetchError.message}`);
1687
1978
  }
1979
+ if (finalResponse && !fetchError && !options.json && typeof status === "number" && (status < 200 || status >= 300)) {
1980
+ const messageSuffix = upstreamErrorMessage ? ` \u2014 ${upstreamErrorMessage}` : "";
1981
+ if (status === 402 && paymentMeta) {
1982
+ console.error(
1983
+ ` Upstream returned 402 after payment was sent \u2014 the seller's facilitator rejected the credential${messageSuffix}`
1984
+ );
1985
+ } else {
1986
+ console.error(` Upstream returned ${status}${messageSuffix}`);
1987
+ }
1988
+ }
1688
1989
  if (options.json) {
1689
1990
  const responseStatus = finalResponse?.status ?? null;
1690
1991
  const ok = responseStatus !== null && responseStatus >= 200 && responseStatus < 300;
@@ -1764,7 +2065,20 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
1764
2065
  );
1765
2066
 
1766
2067
  // src/commands/get-command.ts
1767
- import { Command as Command4 } from "commander";
2068
+ import { Command as Command5 } from "commander";
2069
+
2070
+ // src/util/format-price.ts
2071
+ var centsToDollars = (cents) => {
2072
+ const value = Number.parseFloat(cents) / 100;
2073
+ if (!Number.isFinite(value) || value === 0) return "0.00";
2074
+ if (value < 1e-4) return value.toFixed(6);
2075
+ if (value < 1e-3) return value.toFixed(5);
2076
+ if (value < 0.01) return value.toFixed(4);
2077
+ if (value < 1) return value.toFixed(3);
2078
+ return value.toFixed(2);
2079
+ };
2080
+
2081
+ // src/commands/get-command.ts
1768
2082
  var formatRelativeTimestamp = (iso) => {
1769
2083
  if (!iso) return "never";
1770
2084
  const then = new Date(iso).getTime();
@@ -1781,12 +2095,6 @@ var formatRelativeTimestamp = (iso) => {
1781
2095
  if (diffMo < 12) return `${diffMo}mo ago`;
1782
2096
  return `${Math.round(diffMo / 12)}y ago`;
1783
2097
  };
1784
- var centsToDollars = (cents) => {
1785
- const value = Number.parseFloat(cents) / 100;
1786
- if (value < 0.01) return value.toFixed(4);
1787
- if (value < 1) return value.toFixed(3);
1788
- return value.toFixed(2);
1789
- };
1790
2098
  var formatCost = (capability) => {
1791
2099
  const lines = [];
1792
2100
  const observed = capability.priceObserved;
@@ -1796,6 +2104,10 @@ var formatCost = (capability) => {
1796
2104
  const median = observed.medianCents ? centsToDollars(observed.medianCents) : null;
1797
2105
  const detail = median ? `median $${median}, n=${observed.sampleCount}` : `n=${observed.sampleCount}`;
1798
2106
  lines.push(` Cost: $${min}\u2013$${max}/call (${detail})`);
2107
+ } else if (capability.displayCostAmount === "0") {
2108
+ lines.push(` Cost: Free`);
2109
+ } else if (capability.displayCostAmount === "unknown") {
2110
+ lines.push(` Cost: variable pricing`);
1799
2111
  } else {
1800
2112
  lines.push(` Cost: $${capability.displayCostAmount}/call`);
1801
2113
  }
@@ -1903,10 +2215,16 @@ var formatCapability = (capability) => {
1903
2215
  lines.push(
1904
2216
  ` Last successful run: ${formatRelativeTimestamp(capability.lastSuccessfullyRanAt)}`
1905
2217
  );
2218
+ if (capability.exampleAgentPrompt?.trim()) {
2219
+ lines.push(" Example prompt:");
2220
+ for (const promptLine of capability.exampleAgentPrompt.split("\n")) {
2221
+ lines.push(` ${promptLine}`);
2222
+ }
2223
+ }
1906
2224
  lines.push(...buildTryItExample(capability));
1907
2225
  return lines.join("\n");
1908
2226
  };
1909
- var getCommand = (appContext) => new Command4("get").description(
2227
+ var getCommand = (appContext) => new Command5("get").description(
1910
2228
  "Get details for a capability by position from last search, or by slug"
1911
2229
  ).argument(
1912
2230
  "<identifier>",
@@ -1955,7 +2273,15 @@ var getCommand = (appContext) => new Command4("get").description(
1955
2273
  console.log(JSON.stringify(capability, null, 2));
1956
2274
  }
1957
2275
  analyticsService.capture("capability_viewed", {
2276
+ // Existing field — raw user input (uid, slug, or position
2277
+ // resolved to uid via the last-search cache). Preserved
2278
+ // for back-compat with existing dashboards.
1958
2279
  capabilityId,
2280
+ // New canonical identifier fields read off the API response,
2281
+ // so they are always populated regardless of how the user
2282
+ // referenced the capability.
2283
+ capabilityUid: capability.uid,
2284
+ capabilitySlug: capability.slug,
1959
2285
  fromLastSearch: isPosition,
1960
2286
  ...isPosition ? { position } : {},
1961
2287
  ...searchId ? { searchId } : {}
@@ -1973,15 +2299,15 @@ import {
1973
2299
  existsSync as existsSync2,
1974
2300
  mkdirSync as mkdirSync3,
1975
2301
  readdirSync,
1976
- readFileSync as readFileSync4,
2302
+ readFileSync as readFileSync5,
1977
2303
  rmSync,
1978
2304
  statSync,
1979
2305
  writeFileSync as writeFileSync3
1980
2306
  } from "fs";
1981
- import { homedir as homedir2 } from "os";
1982
- import { dirname, join as join2, relative } from "path";
2307
+ import { homedir as homedir3 } from "os";
2308
+ import { dirname, join as join3, relative } from "path";
1983
2309
  import { fileURLToPath } from "url";
1984
- import { Command as Command5 } from "commander";
2310
+ import { Command as Command6 } from "commander";
1985
2311
  import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
1986
2312
 
1987
2313
  // src/util/install-banner.ts
@@ -2089,7 +2415,12 @@ var printReadyFooter = () => {
2089
2415
  const lines = [
2090
2416
  "",
2091
2417
  ` ${color.boldGreen("Zero is ready!")} Zero works best with an AI agent.`,
2092
- ` Open ${color.boldRed("Claude Code")}, Codex, Cursor, Blackbox, or your agent of choice`,
2418
+ "",
2419
+ " First, claim your $5 welcome bonus \u2014 run this now, before opening your agent:",
2420
+ "",
2421
+ ` ${color.cyan("zero welcome")}`,
2422
+ "",
2423
+ ` Then open ${color.boldRed("Claude Code")}, Codex, Cursor, Blackbox, or your agent of choice`,
2093
2424
  " and try this prompt to get started:",
2094
2425
  "",
2095
2426
  ` ${color.cyan("What is zero and how do I use it?")}`,
@@ -2108,19 +2439,6 @@ var printReadyFooter = () => {
2108
2439
  return lines.join("\n");
2109
2440
  };
2110
2441
 
2111
- // src/util/secure-config.ts
2112
- import { chmodSync, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
2113
- var SECURE_DIR_MODE = 448;
2114
- var SECURE_FILE_MODE = 384;
2115
- var ensureSecureDir = (path) => {
2116
- mkdirSync2(path, { recursive: true, mode: SECURE_DIR_MODE });
2117
- chmodSync(path, SECURE_DIR_MODE);
2118
- };
2119
- var writeSecureFile = (path, contents) => {
2120
- writeFileSync2(path, contents, { mode: SECURE_FILE_MODE });
2121
- chmodSync(path, SECURE_FILE_MODE);
2122
- };
2123
-
2124
2442
  // src/commands/init-command.ts
2125
2443
  var AGENT_TOOLS = [
2126
2444
  { name: "Claude Code", detectDir: ".claude", skillsDir: ".claude/skills" },
@@ -2135,7 +2453,7 @@ var AGENT_TOOLS = [
2135
2453
  var findResourceDir = (startDir, resourceName) => {
2136
2454
  let current = startDir;
2137
2455
  while (true) {
2138
- const candidate = join2(current, resourceName);
2456
+ const candidate = join3(current, resourceName);
2139
2457
  if (existsSync2(candidate)) {
2140
2458
  return candidate;
2141
2459
  }
@@ -2154,7 +2472,7 @@ var getCliModuleDir = () => {
2154
2472
  }
2155
2473
  return __dirname;
2156
2474
  };
2157
- var sha256File = (filePath) => createHash4("sha256").update(readFileSync4(filePath)).digest("hex");
2475
+ var sha256File = (filePath) => createHash4("sha256").update(readFileSync5(filePath)).digest("hex");
2158
2476
  var verifyFileCopy = (src, dest) => {
2159
2477
  if (!existsSync2(dest)) return false;
2160
2478
  return sha256File(src) === sha256File(dest);
@@ -2162,7 +2480,7 @@ var verifyFileCopy = (src, dest) => {
2162
2480
  var collectAllFiles = (dir) => {
2163
2481
  const files = [];
2164
2482
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
2165
- const fullPath = join2(dir, entry.name);
2483
+ const fullPath = join3(dir, entry.name);
2166
2484
  if (entry.isDirectory()) {
2167
2485
  files.push(...collectAllFiles(fullPath));
2168
2486
  } else {
@@ -2172,7 +2490,7 @@ var collectAllFiles = (dir) => {
2172
2490
  return files;
2173
2491
  };
2174
2492
  var copyFile = (src, dest) => {
2175
- writeFileSync3(dest, readFileSync4(src));
2493
+ writeFileSync3(dest, readFileSync5(src));
2176
2494
  try {
2177
2495
  chmodSync2(dest, statSync(src).mode);
2178
2496
  } catch {
@@ -2181,8 +2499,8 @@ var copyFile = (src, dest) => {
2181
2499
  var copyDirRecursive = (src, dest) => {
2182
2500
  mkdirSync3(dest, { recursive: true });
2183
2501
  for (const entry of readdirSync(src, { withFileTypes: true })) {
2184
- const srcPath = join2(src, entry.name);
2185
- const destPath = join2(dest, entry.name);
2502
+ const srcPath = join3(src, entry.name);
2503
+ const destPath = join3(dest, entry.name);
2186
2504
  if (entry.isDirectory()) {
2187
2505
  copyDirRecursive(srcPath, destPath);
2188
2506
  } else {
@@ -2191,22 +2509,22 @@ var copyDirRecursive = (src, dest) => {
2191
2509
  }
2192
2510
  };
2193
2511
  var installHook = (home, verbose = false) => {
2194
- const claudeDir = join2(home, ".claude");
2512
+ const claudeDir = join3(home, ".claude");
2195
2513
  if (!existsSync2(claudeDir)) {
2196
2514
  if (verbose) {
2197
2515
  stepInfo(`~/.claude not found \u2014 Claude Code not installed, skipping`);
2198
2516
  }
2199
2517
  return false;
2200
2518
  }
2201
- const zeroHooksDir = join2(home, ".zero", "hooks");
2519
+ const zeroHooksDir = join3(home, ".zero", "hooks");
2202
2520
  mkdirSync3(zeroHooksDir, { recursive: true });
2203
2521
  if (verbose) stepInfo(`staged hook dir at ${zeroHooksDir}`);
2204
2522
  const hookFiles = ["auto-approve-zero.sh", "zero-context.sh"];
2205
2523
  const hookDests = {};
2206
2524
  const hooksSourceDir = findResourceDir(getCliModuleDir(), "hooks");
2207
2525
  for (const hookFile of hookFiles) {
2208
- const hookSource = join2(hooksSourceDir, hookFile);
2209
- const hookDest = join2(zeroHooksDir, hookFile);
2526
+ const hookSource = join3(hooksSourceDir, hookFile);
2527
+ const hookDest = join3(zeroHooksDir, hookFile);
2210
2528
  copyFile(hookSource, hookDest);
2211
2529
  chmodSync2(hookDest, 493);
2212
2530
  if (!verifyFileCopy(hookSource, hookDest)) {
@@ -2217,14 +2535,14 @@ var installHook = (home, verbose = false) => {
2217
2535
  hookDests[hookFile] = hookDest;
2218
2536
  if (verbose) stepInfo(`copied ${hookFile} \u2192 ${hookDest} (verified)`);
2219
2537
  }
2220
- const settingsPath = join2(claudeDir, "settings.json");
2538
+ const settingsPath = join3(claudeDir, "settings.json");
2221
2539
  let settings = {};
2222
2540
  let settingsExisted = false;
2223
2541
  let settingsCorrupted = false;
2224
2542
  if (existsSync2(settingsPath)) {
2225
2543
  settingsExisted = true;
2226
2544
  try {
2227
- settings = JSON.parse(readFileSync4(settingsPath, "utf-8"));
2545
+ settings = JSON.parse(readFileSync5(settingsPath, "utf-8"));
2228
2546
  } catch {
2229
2547
  settingsCorrupted = true;
2230
2548
  }
@@ -2322,7 +2640,7 @@ var CONFLICTING_SKILL_PATTERNS = ["zam"];
2322
2640
  var findConflictingSkills = (home) => {
2323
2641
  const found = [];
2324
2642
  for (const tool of AGENT_TOOLS) {
2325
- const toolSkillsPath = join2(home, tool.skillsDir);
2643
+ const toolSkillsPath = join3(home, tool.skillsDir);
2326
2644
  if (!existsSync2(toolSkillsPath)) continue;
2327
2645
  const entries = readdirSync(toolSkillsPath, { withFileTypes: true });
2328
2646
  for (const entry of entries) {
@@ -2331,7 +2649,7 @@ var findConflictingSkills = (home) => {
2331
2649
  if (CONFLICTING_SKILL_PATTERNS.some((p) => lower.includes(p))) {
2332
2650
  found.push({
2333
2651
  tool: tool.name,
2334
- skillPath: join2(toolSkillsPath, entry.name),
2652
+ skillPath: join3(toolSkillsPath, entry.name),
2335
2653
  skillName: entry.name
2336
2654
  });
2337
2655
  }
@@ -2359,7 +2677,7 @@ var installSkills = (home, verbose = false) => {
2359
2677
  const installed = [];
2360
2678
  const errors = [];
2361
2679
  for (const tool of AGENT_TOOLS) {
2362
- const toolDetectPath = join2(home, tool.detectDir);
2680
+ const toolDetectPath = join3(home, tool.detectDir);
2363
2681
  if (!existsSync2(toolDetectPath)) {
2364
2682
  if (verbose) {
2365
2683
  stepInfo(
@@ -2374,16 +2692,16 @@ var installSkills = (home, verbose = false) => {
2374
2692
  );
2375
2693
  }
2376
2694
  try {
2377
- const toolSkillsPath = join2(home, tool.skillsDir);
2695
+ const toolSkillsPath = join3(home, tool.skillsDir);
2378
2696
  mkdirSync3(toolSkillsPath, { recursive: true });
2379
2697
  for (const skillDir of skillDirs) {
2380
- const src = join2(skillsSourceDir, skillDir);
2381
- const dest = join2(toolSkillsPath, skillDir);
2698
+ const src = join3(skillsSourceDir, skillDir);
2699
+ const dest = join3(toolSkillsPath, skillDir);
2382
2700
  const existed = existsSync2(dest);
2383
2701
  copyDirRecursive(src, dest);
2384
2702
  for (const srcFile of collectAllFiles(src)) {
2385
2703
  const relPath = relative(src, srcFile);
2386
- const destFile = join2(dest, relPath);
2704
+ const destFile = join3(dest, relPath);
2387
2705
  if (!verifyFileCopy(srcFile, destFile)) {
2388
2706
  throw new Error(
2389
2707
  `Integrity check failed: ${destFile} does not match source`
@@ -2414,15 +2732,15 @@ var runInit = async (appContext, options = {}) => {
2414
2732
  let currentStep = "wallet";
2415
2733
  try {
2416
2734
  printZeroBanner();
2417
- const home = homedir2();
2418
- const zeroDir = join2(home, ".zero");
2419
- const configPath = join2(zeroDir, "config.json");
2735
+ const home = homedir3();
2736
+ const zeroDir = join3(home, ".zero");
2737
+ const configPath = join3(zeroDir, "config.json");
2420
2738
  let walletCreated = false;
2421
2739
  let walletAddress = null;
2422
2740
  const walletExists = (() => {
2423
2741
  if (!existsSync2(configPath)) return false;
2424
2742
  try {
2425
- const existing = JSON.parse(readFileSync4(configPath, "utf8"));
2743
+ const existing = JSON.parse(readFileSync5(configPath, "utf8"));
2426
2744
  return !!existing.privateKey;
2427
2745
  } catch {
2428
2746
  return false;
@@ -2432,7 +2750,7 @@ var runInit = async (appContext, options = {}) => {
2432
2750
  const privateKey = generatePrivateKey();
2433
2751
  const account = privateKeyToAccount(privateKey);
2434
2752
  ensureSecureDir(zeroDir);
2435
- const existing = existsSync2(configPath) ? JSON.parse(readFileSync4(configPath, "utf8")) : {};
2753
+ const existing = existsSync2(configPath) ? JSON.parse(readFileSync5(configPath, "utf8")) : {};
2436
2754
  writeSecureFile(
2437
2755
  configPath,
2438
2756
  JSON.stringify(
@@ -2455,7 +2773,7 @@ var runInit = async (appContext, options = {}) => {
2455
2773
  }
2456
2774
  } else {
2457
2775
  try {
2458
- const existing = JSON.parse(readFileSync4(configPath, "utf8"));
2776
+ const existing = JSON.parse(readFileSync5(configPath, "utf8"));
2459
2777
  const account = privateKeyToAccount(existing.privateKey);
2460
2778
  walletAddress = account.address;
2461
2779
  } catch {
@@ -2476,7 +2794,7 @@ var runInit = async (appContext, options = {}) => {
2476
2794
  let hookInstalled = false;
2477
2795
  let hookError = null;
2478
2796
  for (const tool of AGENT_TOOLS) {
2479
- if (existsSync2(join2(home, tool.detectDir))) {
2797
+ if (existsSync2(join3(home, tool.detectDir))) {
2480
2798
  agentsDetected.push(tool.name);
2481
2799
  }
2482
2800
  }
@@ -2552,12 +2870,13 @@ To remove them, run: ${color.cyan("zero init cleanup")}`
2552
2870
  sectionDivider();
2553
2871
  console.error(printReadyFooter());
2554
2872
  currentStep = "complete";
2873
+ if (walletAddress) {
2874
+ appContext.services.analyticsService.setWalletAddress(walletAddress);
2875
+ }
2555
2876
  appContext.services.analyticsService.capture("wallet_initialized", {
2556
2877
  // biome-ignore lint/style/useNamingConvention: snake_case for analytics
2557
2878
  wallet_created: walletCreated,
2558
2879
  // biome-ignore lint/style/useNamingConvention: snake_case for analytics
2559
- wallet_address: walletAddress,
2560
- // biome-ignore lint/style/useNamingConvention: snake_case for analytics
2561
2880
  agents_detected: agentsDetected,
2562
2881
  // biome-ignore lint/style/useNamingConvention: snake_case for analytics
2563
2882
  agents_detected_count: agentsDetected.length,
@@ -2585,14 +2904,14 @@ To remove them, run: ${color.cyan("zero init cleanup")}`
2585
2904
  throw err;
2586
2905
  }
2587
2906
  };
2588
- var initCommand = (appContext) => new Command5("init").description("Initialize Zero CLI for usage").option("--force", "Overwrite existing configuration").option(
2907
+ var initCommand = (appContext) => new Command6("init").description("Initialize Zero CLI for usage").option("--force", "Overwrite existing configuration").option(
2589
2908
  "-v, --verbose",
2590
2909
  "Explain why each install step was taken or skipped"
2591
2910
  ).action(async (options) => {
2592
2911
  await runInit(appContext, options);
2593
2912
  }).addCommand(
2594
- new Command5("cleanup").description("Remove deprecated skills (zam) that conflict with Zero").action(() => {
2595
- const home = homedir2();
2913
+ new Command6("cleanup").description("Remove deprecated skills (zam) that conflict with Zero").action(() => {
2914
+ const home = homedir3();
2596
2915
  const removed = removeConflictingSkills(home);
2597
2916
  if (removed.length === 0) {
2598
2917
  console.error("No conflicting skills found. Nothing to remove.");
@@ -2611,8 +2930,8 @@ ${removedList}`);
2611
2930
  );
2612
2931
 
2613
2932
  // src/commands/review-command.ts
2614
- import { readFileSync as readFileSync5 } from "fs";
2615
- import { Command as Command6 } from "commander";
2933
+ import { readFileSync as readFileSync6 } from "fs";
2934
+ import { Command as Command7 } from "commander";
2616
2935
  import { z as z3 } from "zod";
2617
2936
  var bulkEntrySchema2 = z3.object({
2618
2937
  runId: z3.string(),
@@ -2622,7 +2941,7 @@ var bulkEntrySchema2 = z3.object({
2622
2941
  reliability: z3.number().int().min(1).max(5),
2623
2942
  content: z3.string().optional()
2624
2943
  });
2625
- var reviewCommand = (appContext) => new Command6("review").description("Submit a review for a capability run").addHelpText(
2944
+ var reviewCommand = (appContext) => new Command7("review").description("Submit a review for a capability run").addHelpText(
2626
2945
  "after",
2627
2946
  `
2628
2947
  Tips for a great review:
@@ -2653,7 +2972,7 @@ Examples:
2653
2972
  try {
2654
2973
  const { analyticsService, apiService } = appContext.services;
2655
2974
  if (options.fromFile) {
2656
- const contents = readFileSync5(options.fromFile, "utf8");
2975
+ const contents = readFileSync6(options.fromFile, "utf8");
2657
2976
  const lines = contents.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
2658
2977
  const parsed = [];
2659
2978
  let parseFailures = 0;
@@ -2801,7 +3120,7 @@ Bulk review complete: ${ok} ok, ${failed} failed`);
2801
3120
  );
2802
3121
 
2803
3122
  // src/commands/runs-command.ts
2804
- import { Command as Command7 } from "commander";
3123
+ import { Command as Command8 } from "commander";
2805
3124
  var USD_ASSETS = /* @__PURE__ */ new Set(["USD", "USDC"]);
2806
3125
  var formatCost2 = (cost) => {
2807
3126
  if (!cost) return "free";
@@ -2816,7 +3135,7 @@ var formatPayment = (payment) => {
2816
3135
  const mode = payment.mode ? ` (${payment.mode})` : "";
2817
3136
  return `${payment.protocol}${chain}${mode}`;
2818
3137
  };
2819
- var runsCommand = (appContext) => new Command7("runs").description("List your recent capability runs").addHelpText(
3138
+ var runsCommand = (appContext) => new Command8("runs").description("List your recent capability runs").addHelpText(
2820
3139
  "after",
2821
3140
  `
2822
3141
  View your recent capability runs \u2014 status, latency, cost, and payment info.
@@ -2874,7 +3193,7 @@ Examples:
2874
3193
  );
2875
3194
 
2876
3195
  // src/commands/search-command.ts
2877
- import { Command as Command8 } from "commander";
3196
+ import { Command as Command9 } from "commander";
2878
3197
  var DEFAULT_MAX_COST_USD = "30";
2879
3198
  var formatReviewCount2 = (count) => {
2880
3199
  if (count >= 1e3) return `${(count / 1e3).toFixed(1)}k`;
@@ -2901,16 +3220,17 @@ var formatSearchResults = (results) => {
2901
3220
  const displayDescription = r.whatItDoes ?? r.description;
2902
3221
  const ratingBadge = formatRatingBadge(r.rating);
2903
3222
  const statusBadge = formatStatusBadge(r.displayStatus);
2904
- return ` ${r.position}. ${displayName} \u2014 $${r.cost.amount}/call \u2014 ${ratingBadge}${statusBadge}
3223
+ const costLabel = r.cost.amount === "0" ? "Free" : r.cost.amount === "unknown" ? "variable pricing" : `$${r.cost.amount}/call`;
3224
+ return ` ${r.position}. ${displayName} \u2014 ${costLabel} \u2014 ${ratingBadge}${statusBadge}
2905
3225
  "${displayDescription}"`;
2906
3226
  }).join("\n");
2907
3227
  };
2908
- var searchCommand = (appContext) => new Command8("search").description("Search for capabilities").argument("<query>", "Search query").option("--json", "Output raw JSON to stdout").option("--offset <n>", "Pagination offset", Number).option("--limit <n>", "Results per page", Number).option("--free", "Only show free capabilities").option(
3228
+ var searchCommand = (appContext) => new Command9("search").description("Search for capabilities").argument("<query>", "Search query").option("--json", "Output raw JSON to stdout").option("--offset <n>", "Pagination offset", Number).option("--limit <n>", "Results per page", Number).option("--free", "Only show free capabilities").option(
2909
3229
  "--max-cost <amount>",
2910
3230
  `Maximum cost per call in USD (default: ${DEFAULT_MAX_COST_USD})`
2911
3231
  ).option("--protocol <protocol>", "Payment protocol (x402 or mpp)").option(
2912
3232
  "--status <status>",
2913
- "Filter by availability (healthy, degraded, down)"
3233
+ "Filter by availability (healthy, unknown, down). Defaults to healthy when neither --status nor --all is set; pass --all to see everything."
2914
3234
  ).option("--all", "Disable default quality filtering").option(
2915
3235
  "--source <source>",
2916
3236
  "Only show results from this crawl source (e.g. mpp, bazaar)"
@@ -2937,14 +3257,20 @@ var searchCommand = (appContext) => new Command8("search").description("Search f
2937
3257
  effectiveMaxCost = DEFAULT_MAX_COST_USD;
2938
3258
  appliedDefaultMaxCost = true;
2939
3259
  }
2940
- const validStatuses = ["healthy", "degraded", "down"];
3260
+ const validStatuses = ["healthy", "unknown", "degraded", "down"];
2941
3261
  if (options.status && !validStatuses.includes(options.status)) {
2942
3262
  console.error(
2943
- `Invalid status "${options.status}". Must be one of: healthy, degraded, down.`
3263
+ `Invalid status "${options.status}". Must be one of: ${validStatuses.join(", ")}.`
2944
3264
  );
2945
3265
  process.exitCode = 1;
2946
3266
  return;
2947
3267
  }
3268
+ let effectiveStatus = options.status;
3269
+ let appliedDefaultStatus = false;
3270
+ if (effectiveStatus === void 0 && !options.all) {
3271
+ effectiveStatus = "healthy";
3272
+ appliedDefaultStatus = true;
3273
+ }
2948
3274
  const result = await apiService.search({
2949
3275
  query,
2950
3276
  offset: options.offset,
@@ -2952,7 +3278,7 @@ var searchCommand = (appContext) => new Command8("search").description("Search f
2952
3278
  freeOnly: options.free,
2953
3279
  maxCost: effectiveMaxCost,
2954
3280
  protocol: options.protocol,
2955
- availabilityStatus: options.status,
3281
+ availabilityStatus: effectiveStatus,
2956
3282
  includeAll: options.all,
2957
3283
  source: options.source,
2958
3284
  excludeSource: options.excludeSource
@@ -2970,10 +3296,11 @@ var searchCommand = (appContext) => new Command8("search").description("Search f
2970
3296
  maxCost: effectiveMaxCost,
2971
3297
  maxCostDefaulted: appliedDefaultMaxCost,
2972
3298
  protocol: options.protocol,
2973
- availabilityStatus: options.status,
3299
+ availabilityStatus: effectiveStatus,
3300
+ availabilityStatusDefaulted: appliedDefaultStatus,
2974
3301
  includeAll: options.all ?? false,
2975
- source: options.source,
2976
- excludeSource: options.excludeSource,
3302
+ listingSource: options.source,
3303
+ excludeListingSource: options.excludeSource,
2977
3304
  json: options.json ?? false
2978
3305
  });
2979
3306
  if (options.json) {
@@ -2995,6 +3322,7 @@ var searchCommand = (appContext) => new Command8("search").description("Search f
2995
3322
  capabilities: result.capabilities.map((c) => ({
2996
3323
  position: c.position,
2997
3324
  id: c.id,
3325
+ slug: c.slug,
2998
3326
  url: c.url,
2999
3327
  urlTemplate: c.urlTemplate ?? null,
3000
3328
  displayCostAmount: c.cost.amount
@@ -3020,9 +3348,9 @@ var searchCommand = (appContext) => new Command8("search").description("Search f
3020
3348
  );
3021
3349
 
3022
3350
  // src/commands/terms-command.ts
3023
- import { Command as Command9 } from "commander";
3351
+ import { Command as Command10 } from "commander";
3024
3352
  var TERMS_URL = "https://zero.xyz/terms-of-service";
3025
- var termsCommand = (_appContext) => new Command9("terms").description("View the ZeroClick Terms of Service").action(async () => {
3353
+ var termsCommand = (_appContext) => new Command10("terms").description("View the ZeroClick Terms of Service").action(async () => {
3026
3354
  console.log(
3027
3355
  `ZeroClick Agentic Capability Search \u2014 Terms of Service
3028
3356
 
@@ -3040,14 +3368,48 @@ Read the full terms at: ${TERMS_URL}
3040
3368
  });
3041
3369
 
3042
3370
  // src/commands/wallet-command.ts
3043
- import { existsSync as existsSync3, readFileSync as readFileSync6 } from "fs";
3044
- import { homedir as homedir3 } from "os";
3045
- import { join as join3 } from "path";
3046
- import { Command as Command10 } from "commander";
3047
- import open from "open";
3048
- import { privateKeyToAccount as privateKeyToAccount2 } from "viem/accounts";
3049
- var walletBalanceCommand = (appContext) => new Command10("balance").description("Show wallet balance").action(async () => {
3050
- const { walletService } = appContext.services;
3371
+ import { existsSync as existsSync3, readFileSync as readFileSync7 } from "fs";
3372
+ import { homedir as homedir4 } from "os";
3373
+ import { join as join4 } from "path";
3374
+ import { Command as Command11 } from "commander";
3375
+ import open2 from "open";
3376
+ import { isAddress } from "viem";
3377
+ import { generatePrivateKey as generatePrivateKey2, privateKeyToAccount as privateKeyToAccount2 } from "viem/accounts";
3378
+
3379
+ // src/util/stdin.ts
3380
+ var readStdin = async () => {
3381
+ const chunks = [];
3382
+ for await (const chunk of process.stdin) {
3383
+ chunks.push(chunk);
3384
+ }
3385
+ return Buffer.concat(chunks).toString("utf8");
3386
+ };
3387
+
3388
+ // src/commands/wallet-command.ts
3389
+ var PRIVATE_KEY_PATTERN = /^0x[0-9a-fA-F]{64}$/;
3390
+ var parseProvider = (raw) => raw === "stripe" ? "stripe" : "coinbase";
3391
+ var walletBalanceCommand = (appContext) => new Command11("balance").description("Show wallet balance").option(
3392
+ "--address <address>",
3393
+ "Check balance for an arbitrary address (no key needed). Useful with `zero wallet generate` \u2014 verify funds arrived without setting the wallet as your default."
3394
+ ).action(async (options) => {
3395
+ const { walletService, paymentService } = appContext.services;
3396
+ if (options.address) {
3397
+ if (!isAddress(options.address)) {
3398
+ console.error("Invalid address (expected 0x + 40 hex chars).");
3399
+ process.exitCode = 1;
3400
+ return;
3401
+ }
3402
+ try {
3403
+ const balance2 = await paymentService.getTotalBalanceForAddress(
3404
+ options.address
3405
+ );
3406
+ console.log(`${balance2.amount} ${balance2.asset}`);
3407
+ } catch {
3408
+ console.error("Failed to fetch balance");
3409
+ process.exitCode = 1;
3410
+ }
3411
+ return;
3412
+ }
3051
3413
  const balance = await walletService.getBalance();
3052
3414
  if (balance === null) {
3053
3415
  console.error("No wallet configured. Run `zero init` first.");
@@ -3061,16 +3423,76 @@ var walletBalanceCommand = (appContext) => new Command10("balance").description(
3061
3423
  }
3062
3424
  console.log(`${balance.amount} ${balance.asset}`);
3063
3425
  });
3064
- var walletFundCommand = (appContext) => new Command10("fund").description("Fund your wallet").argument("[amount]", "Amount to fund in USDC").option("--manual", "Show wallet address for manual transfer").option(
3426
+ var readPrivateKeyFromStdin = async () => {
3427
+ if (process.stdin.isTTY) {
3428
+ console.error(
3429
+ "Expected a private key on stdin. Example:\n zero wallet generate --json | jq -r .privateKey | zero wallet fund --key-stdin"
3430
+ );
3431
+ return null;
3432
+ }
3433
+ const raw = (await readStdin()).trim();
3434
+ if (!PRIVATE_KEY_PATTERN.test(raw)) {
3435
+ console.error("Invalid private key on stdin (expected 0x + 64 hex chars).");
3436
+ return null;
3437
+ }
3438
+ return raw;
3439
+ };
3440
+ var walletFundCommand = (appContext) => new Command11("fund").description("Fund your wallet").argument("[amount]", "Amount to fund in USDC").option("--manual", "Show wallet address for manual transfer").option(
3065
3441
  "--no-open",
3066
3442
  "Print the funding URL instead of opening a browser (for agents \u2014 funding links are one-time use, hand the URL to the user)"
3067
3443
  ).option(
3068
3444
  "--use <provider>",
3069
3445
  "Onramp provider: coinbase or stripe",
3070
3446
  "coinbase"
3447
+ ).option(
3448
+ "--key-stdin",
3449
+ "Read a private key from stdin and mint a funding URL for that wallet instead of your configured one. Useful with `zero wallet generate`. Does not modify your config."
3071
3450
  ).action(
3072
3451
  async (amount, options) => {
3073
- const { analyticsService, walletService } = appContext.services;
3452
+ const { analyticsService, apiService, walletService } = appContext.services;
3453
+ const provider = parseProvider(options.use);
3454
+ if (options.keyStdin) {
3455
+ const key = await readPrivateKeyFromStdin();
3456
+ if (!key) {
3457
+ process.exitCode = 1;
3458
+ return;
3459
+ }
3460
+ const altAccount = privateKeyToAccount2(key);
3461
+ const altApi = apiService.withAccount(altAccount);
3462
+ if (options.manual) {
3463
+ console.log(`Send USDC (Base) to:
3464
+ ${altAccount.address}`);
3465
+ analyticsService.capture("wallet_funded", {
3466
+ method: "manual",
3467
+ amount,
3468
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
3469
+ alt_wallet: true
3470
+ });
3471
+ return;
3472
+ }
3473
+ const url2 = await altApi.getFundingUrl(amount, provider);
3474
+ if (!url2) {
3475
+ console.log(`Send USDC (Base) to:
3476
+ ${altAccount.address}`);
3477
+ console.error(
3478
+ "Could not get funding URL. Send USDC (Base) manually."
3479
+ );
3480
+ process.exitCode = 1;
3481
+ return;
3482
+ }
3483
+ console.log(
3484
+ "Funding URL (one-time use \u2014 open it in a browser to fund):"
3485
+ );
3486
+ console.log(url2);
3487
+ console.log(`Wallet address: ${altAccount.address}`);
3488
+ analyticsService.capture("wallet_funded", {
3489
+ method: "url",
3490
+ amount,
3491
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
3492
+ alt_wallet: true
3493
+ });
3494
+ return;
3495
+ }
3074
3496
  const address = walletService.getAddress();
3075
3497
  if (!address) {
3076
3498
  console.error("No wallet configured. Run `zero init` first.");
@@ -3086,14 +3508,10 @@ ${address}`);
3086
3508
  });
3087
3509
  return;
3088
3510
  }
3089
- const provider = options.use === "stripe" ? "stripe" : "coinbase";
3090
- const url = await appContext.services.apiService.getFundingUrl(
3091
- amount,
3092
- provider
3093
- );
3511
+ const url = await apiService.getFundingUrl(amount, provider);
3094
3512
  if (url) {
3095
3513
  if (options.open) {
3096
- await open(url);
3514
+ await open2(url);
3097
3515
  console.log("Opened funding page in your browser.");
3098
3516
  console.log(`If it didn't open, visit: ${url}`);
3099
3517
  } else {
@@ -3116,7 +3534,7 @@ ${address}`);
3116
3534
  }
3117
3535
  }
3118
3536
  );
3119
- var walletAddressCommand = (appContext) => new Command10("address").description("Show wallet address").action(() => {
3537
+ var walletAddressCommand = (appContext) => new Command11("address").description("Show wallet address").action(() => {
3120
3538
  const { walletService } = appContext.services;
3121
3539
  const address = walletService.getAddress();
3122
3540
  if (!address) {
@@ -3126,7 +3544,7 @@ var walletAddressCommand = (appContext) => new Command10("address").description(
3126
3544
  }
3127
3545
  console.log(address);
3128
3546
  });
3129
- var walletSetCommand = (appContext) => new Command10("set").description("Set wallet from an existing private key").argument("<privateKey>", "Hex-encoded private key (0x-prefixed)").option("--force", "Overwrite existing wallet without prompting").action(async (privateKey, options) => {
3547
+ var walletSetCommand = (appContext) => new Command11("set").description("Set wallet from an existing private key").argument("<privateKey>", "Hex-encoded private key (0x-prefixed)").option("--force", "Overwrite existing wallet without prompting").action(async (privateKey, options) => {
3130
3548
  const { analyticsService } = appContext.services;
3131
3549
  if (!privateKey.startsWith("0x")) {
3132
3550
  console.error("Private key must be 0x-prefixed hex string.");
@@ -3141,11 +3559,11 @@ var walletSetCommand = (appContext) => new Command10("set").description("Set wal
3141
3559
  process.exitCode = 1;
3142
3560
  return;
3143
3561
  }
3144
- const zeroDir = join3(homedir3(), ".zero");
3145
- const configPath = join3(zeroDir, "config.json");
3562
+ const zeroDir = join4(homedir4(), ".zero");
3563
+ const configPath = join4(zeroDir, "config.json");
3146
3564
  if (!options.force && existsSync3(configPath)) {
3147
3565
  try {
3148
- const existing2 = JSON.parse(readFileSync6(configPath, "utf8"));
3566
+ const existing2 = JSON.parse(readFileSync7(configPath, "utf8"));
3149
3567
  if (existing2.privateKey) {
3150
3568
  console.error(
3151
3569
  "Wallet already configured. Use --force to overwrite."
@@ -3157,7 +3575,7 @@ var walletSetCommand = (appContext) => new Command10("set").description("Set wal
3157
3575
  }
3158
3576
  }
3159
3577
  ensureSecureDir(zeroDir);
3160
- const existing = existsSync3(configPath) ? JSON.parse(readFileSync6(configPath, "utf8")) : {};
3578
+ const existing = existsSync3(configPath) ? JSON.parse(readFileSync7(configPath, "utf8")) : {};
3161
3579
  writeSecureFile(
3162
3580
  configPath,
3163
3581
  JSON.stringify(
@@ -3171,34 +3589,111 @@ var walletSetCommand = (appContext) => new Command10("set").description("Set wal
3171
3589
  )
3172
3590
  );
3173
3591
  console.log(`Wallet set: ${account.address}`);
3592
+ analyticsService.setWalletAddress(account.address);
3174
3593
  analyticsService.capture("wallet_set", {
3175
- // biome-ignore lint/style/useNamingConvention: snake_case for analytics
3176
- wallet_address: account.address,
3177
3594
  force: options.force ?? false
3178
3595
  });
3179
3596
  });
3597
+ var walletGenerateCommand = (appContext) => new Command11("generate").description(
3598
+ "Generate a fresh wallet (address + private key) without touching your configured wallet"
3599
+ ).option("--json", "Emit { address, privateKey } as JSON").option(
3600
+ "--fund",
3601
+ "Also mint a one-time onramp URL for the new wallet (does not auto-open)"
3602
+ ).option("--amount <amount>", "With --fund: USDC amount to pre-fill").option(
3603
+ "--use <provider>",
3604
+ "With --fund: onramp provider (coinbase | stripe)",
3605
+ "coinbase"
3606
+ ).action(
3607
+ async (options) => {
3608
+ const { analyticsService, apiService } = appContext.services;
3609
+ const privateKey = generatePrivateKey2();
3610
+ const account = privateKeyToAccount2(privateKey);
3611
+ let fundingUrl = null;
3612
+ if (options.fund) {
3613
+ const provider = parseProvider(options.use);
3614
+ fundingUrl = await apiService.withAccount(account).getFundingUrl(options.amount, provider);
3615
+ }
3616
+ if (options.json) {
3617
+ const payload = {
3618
+ address: account.address,
3619
+ privateKey
3620
+ };
3621
+ if (options.fund) {
3622
+ payload.fundingUrl = fundingUrl;
3623
+ }
3624
+ console.log(JSON.stringify(payload));
3625
+ } else {
3626
+ console.log("");
3627
+ console.log(
3628
+ "New wallet generated. Save these \u2014 they are NOT stored anywhere."
3629
+ );
3630
+ console.log("");
3631
+ console.log(
3632
+ " WARNING: Anyone with this private key can spend any funds sent"
3633
+ );
3634
+ console.log(
3635
+ " to the address. Don't paste it into chat, commit it to git, or"
3636
+ );
3637
+ console.log(" share your screen with it visible.");
3638
+ console.log("");
3639
+ console.log(` Address: ${account.address}`);
3640
+ console.log(` Private key: ${privateKey}`);
3641
+ console.log("");
3642
+ if (options.fund) {
3643
+ if (fundingUrl) {
3644
+ console.log("Funding URL (one-time use):");
3645
+ console.log(` ${fundingUrl}`);
3646
+ console.log("");
3647
+ } else {
3648
+ console.log(
3649
+ "Could not mint a funding URL \u2014 fund the address manually with USDC (Base)."
3650
+ );
3651
+ console.log("");
3652
+ }
3653
+ }
3654
+ console.log("Next steps:");
3655
+ if (!options.fund) {
3656
+ console.log(" - Fund the address with USDC (Base) before use.");
3657
+ }
3658
+ console.log(
3659
+ ` - Use it for a single command: ZERO_PRIVATE_KEY=${privateKey} zero <command>`
3660
+ );
3661
+ console.log(
3662
+ " - Or make it your default wallet: zero wallet set <privateKey> --force"
3663
+ );
3664
+ console.log("");
3665
+ }
3666
+ analyticsService.capture("wallet_generated", {
3667
+ format: options.json ? "json" : "text",
3668
+ funded: options.fund ?? false,
3669
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
3670
+ funding_url_minted: options.fund ? fundingUrl !== null : false
3671
+ });
3672
+ }
3673
+ );
3180
3674
  var walletCommand = (appContext) => {
3181
- const cmd = new Command10("wallet").description("Manage your wallet");
3675
+ const cmd = new Command11("wallet").description("Manage your wallet");
3182
3676
  cmd.addCommand(walletBalanceCommand(appContext));
3183
3677
  cmd.addCommand(walletFundCommand(appContext));
3184
3678
  cmd.addCommand(walletAddressCommand(appContext));
3185
3679
  cmd.addCommand(walletSetCommand(appContext));
3680
+ cmd.addCommand(walletGenerateCommand(appContext));
3186
3681
  return cmd;
3187
3682
  };
3188
3683
 
3189
3684
  // src/commands/welcome-command.ts
3190
- import { existsSync as existsSync4, readFileSync as readFileSync7 } from "fs";
3191
- import { homedir as homedir4 } from "os";
3192
- import { join as join4 } from "path";
3193
- import { Command as Command11 } from "commander";
3194
- import open2 from "open";
3685
+ import { existsSync as existsSync4, readFileSync as readFileSync8 } from "fs";
3686
+ import { homedir as homedir5 } from "os";
3687
+ import { join as join5 } from "path";
3688
+ import { Command as Command12 } from "commander";
3689
+ import open3 from "open";
3195
3690
  import { getAddress } from "viem";
3196
3691
  import { privateKeyToAccount as privateKeyToAccount3 } from "viem/accounts";
3197
3692
  var readPrivateKey = () => {
3198
- const configPath = join4(homedir4(), ".zero", "config.json");
3693
+ const configPath = join5(homedir5(), ".zero", "config.json");
3199
3694
  if (!existsSync4(configPath)) return null;
3200
3695
  try {
3201
- const config = JSON.parse(readFileSync7(configPath, "utf8"));
3696
+ const config = JSON.parse(readFileSync8(configPath, "utf8"));
3202
3697
  if (typeof config.privateKey === "string") {
3203
3698
  return config.privateKey;
3204
3699
  }
@@ -3207,9 +3702,37 @@ var readPrivateKey = () => {
3207
3702
  }
3208
3703
  return null;
3209
3704
  };
3210
- var welcomeCommand = (appContext) => new Command11("welcome").description("Claim your $5 welcome bonus.").action(async () => {
3705
+ var linuxInstallHint = () => {
3706
+ return [
3707
+ " On Debian/Ubuntu: sudo apt install xdg-utils",
3708
+ " On RHEL/Fedora: sudo dnf install xdg-utils"
3709
+ ].join("\n");
3710
+ };
3711
+ var printManualFallback = (url) => {
3712
+ const lines = [
3713
+ "",
3714
+ "\u26A0\uFE0F Couldn't auto-open your browser (this is common on WSL, SSH, or",
3715
+ " containers \u2014 no xdg-open installed).",
3716
+ "",
3717
+ "Copy and paste this URL into your browser to claim:",
3718
+ "",
3719
+ ` ${url}`,
3720
+ ""
3721
+ ];
3722
+ if (process.platform === "linux") {
3723
+ lines.push(
3724
+ "Tip: install xdg-utils so 'zero welcome' can open the browser for you:",
3725
+ linuxInstallHint(),
3726
+ ""
3727
+ );
3728
+ }
3729
+ console.log(lines.join("\n"));
3730
+ };
3731
+ var welcomeCommand = (appContext) => new Command12("welcome").description("Claim your $5 welcome bonus.").action(async () => {
3211
3732
  const { analyticsService } = appContext.services;
3212
3733
  analyticsService.capture("welcome_started", {});
3734
+ let walletAddress;
3735
+ let url;
3213
3736
  try {
3214
3737
  let privateKey = readPrivateKey();
3215
3738
  if (!privateKey) {
@@ -3220,37 +3743,52 @@ var welcomeCommand = (appContext) => new Command11("welcome").description("Claim
3220
3743
  }
3221
3744
  }
3222
3745
  const account = privateKeyToAccount3(privateKey);
3223
- const walletAddress = getAddress(account.address);
3746
+ walletAddress = getAddress(account.address);
3224
3747
  const walletSignature = await account.signMessage({
3225
3748
  message: walletAddress
3226
3749
  });
3227
- const url = new URL("/welcome", appContext.env.ZERO_WEB_URL);
3750
+ url = new URL("/welcome", appContext.env.ZERO_WEB_URL);
3228
3751
  url.searchParams.set("wallet", walletAddress);
3229
3752
  url.searchParams.set("walletSignature", walletSignature);
3753
+ } catch (err) {
3754
+ analyticsService.capture("welcome_failed", {
3755
+ error: truncateError(
3756
+ err instanceof Error ? err.message : String(err)
3757
+ )
3758
+ });
3759
+ throw err;
3760
+ }
3761
+ const urlString = url.toString();
3762
+ analyticsService.setWalletAddress(walletAddress);
3763
+ try {
3764
+ await open3(urlString);
3230
3765
  console.log(
3231
- `Opening ${url.toString()}
3766
+ `Opening ${urlString}
3232
3767
 
3233
3768
  If your browser didn't open, paste the URL above.`
3234
3769
  );
3235
- await open2(url.toString());
3236
3770
  analyticsService.capture("welcome_link_opened", {
3237
3771
  // biome-ignore lint/style/useNamingConvention: snake_case for analytics
3238
- wallet_address: walletAddress
3772
+ open_method: "auto"
3239
3773
  });
3240
3774
  } catch (err) {
3241
- analyticsService.capture("welcome_failed", {
3242
- error: truncateError(
3775
+ printManualFallback(urlString);
3776
+ analyticsService.capture("welcome_link_opened", {
3777
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
3778
+ open_method: "manual_copy",
3779
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
3780
+ open_error: truncateError(
3243
3781
  err instanceof Error ? err.message : String(err)
3244
- )
3782
+ ),
3783
+ platform: process.platform
3245
3784
  });
3246
- throw err;
3247
3785
  }
3248
3786
  });
3249
3787
 
3250
3788
  // src/app.ts
3251
3789
  var createApp = (appContext) => {
3252
3790
  const { analyticsService } = appContext.services;
3253
- const program = new Command12().name("zero").description("Zero CLI \u2014 Search engine for AI agents").version(package_default.version, "-v, --version").exitOverride().hook("preAction", async (_thisCommand, actionCommand) => {
3791
+ const program = new Command13().name("zero").description("Zero CLI \u2014 Search engine for AI agents").version(package_default.version, "-v, --version").exitOverride().hook("preAction", async (_thisCommand, actionCommand) => {
3254
3792
  const agentFlag = actionCommand.opts().agent;
3255
3793
  if (typeof agentFlag === "string" && agentFlag.trim().length > 0) {
3256
3794
  analyticsService.setAgentHost(agentFlag.trim());
@@ -3274,6 +3812,7 @@ var createApp = (appContext) => {
3274
3812
  program.addCommand(configCommand(appContext));
3275
3813
  program.addCommand(termsCommand(appContext));
3276
3814
  program.addCommand(welcomeCommand(appContext));
3815
+ program.addCommand(authCommand(appContext), { hidden: true });
3277
3816
  return program;
3278
3817
  };
3279
3818
 
@@ -3297,14 +3836,14 @@ var getEnv = () => {
3297
3836
 
3298
3837
  // src/app/app-services.ts
3299
3838
  import { randomUUID as randomUUID2 } from "crypto";
3300
- import { existsSync as existsSync7, readFileSync as readFileSync10 } from "fs";
3301
- import { homedir as homedir5 } from "os";
3302
- import { join as join6 } from "path";
3839
+ import { existsSync as existsSync7 } from "fs";
3840
+ import { homedir as homedir6 } from "os";
3841
+ import { join as join7 } from "path";
3303
3842
  import { privateKeyToAccount as privateKeyToAccount4 } from "viem/accounts";
3304
3843
 
3305
3844
  // src/services/analytics-service.ts
3306
3845
  import { randomUUID } from "crypto";
3307
- import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
3846
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "fs";
3308
3847
  import { dirname as dirname2 } from "path";
3309
3848
  import { PostHog } from "posthog-node";
3310
3849
  var POSTHOG_API_KEY = "phc_B2vLyNxAf2mnqvdPQajf4d4b2iXc35dep2ZrvebMJLuX";
@@ -3327,7 +3866,7 @@ var AnalyticsService = class {
3327
3866
  let persistedAnonId;
3328
3867
  try {
3329
3868
  if (existsSync5(opts.configPath)) {
3330
- const config = JSON.parse(readFileSync8(opts.configPath, "utf8"));
3869
+ const config = JSON.parse(readFileSync9(opts.configPath, "utf8"));
3331
3870
  if (config.telemetry === false) {
3332
3871
  telemetryEnabled = false;
3333
3872
  }
@@ -3352,7 +3891,7 @@ var AnalyticsService = class {
3352
3891
  try {
3353
3892
  const dir = dirname2(opts.configPath);
3354
3893
  mkdirSync4(dir, { recursive: true });
3355
- const existing = existsSync5(opts.configPath) ? JSON.parse(readFileSync8(opts.configPath, "utf8")) : {};
3894
+ const existing = existsSync5(opts.configPath) ? JSON.parse(readFileSync9(opts.configPath, "utf8")) : {};
3356
3895
  writeFileSync4(
3357
3896
  opts.configPath,
3358
3897
  JSON.stringify({ ...existing, anonId: newAnonId }, null, 2)
@@ -3392,7 +3931,7 @@ var AnalyticsService = class {
3392
3931
  if (anonId === walletAddress) return;
3393
3932
  let aliasedTo;
3394
3933
  try {
3395
- const config = JSON.parse(readFileSync8(configPath, "utf8"));
3934
+ const config = JSON.parse(readFileSync9(configPath, "utf8"));
3396
3935
  if (typeof config.aliasedTo === "string") {
3397
3936
  aliasedTo = config.aliasedTo;
3398
3937
  }
@@ -3401,7 +3940,7 @@ var AnalyticsService = class {
3401
3940
  if (aliasedTo === walletAddress) return;
3402
3941
  this.posthog.alias({ distinctId: walletAddress, alias: anonId });
3403
3942
  try {
3404
- const config = existsSync5(configPath) ? JSON.parse(readFileSync8(configPath, "utf8")) : {};
3943
+ const config = existsSync5(configPath) ? JSON.parse(readFileSync9(configPath, "utf8")) : {};
3405
3944
  writeFileSync4(
3406
3945
  configPath,
3407
3946
  JSON.stringify({ ...config, aliasedTo: walletAddress }, null, 2)
@@ -3414,12 +3953,30 @@ var AnalyticsService = class {
3414
3953
  setAgentHost(next) {
3415
3954
  this.agentHost = next;
3416
3955
  }
3956
+ // Per-invocation override for the `wallet_address` super-prop. Used by
3957
+ // commands that mint or load a wallet mid-process (init, wallet set,
3958
+ // welcome) so subsequent captures carry the now-known address without
3959
+ // needing to pass it as a per-event prop (which would collide with the
3960
+ // super-prop — see ZERO-43). distinctId is intentionally NOT changed:
3961
+ // the anon→wallet identity merge happens on the next CLI invocation
3962
+ // via PostHog alias, when AnalyticsService is constructed with both
3963
+ // the persisted anonId and the new walletAddress.
3964
+ setWalletAddress(walletAddress) {
3965
+ this.walletAddress = walletAddress;
3966
+ }
3417
3967
  capture(event, properties) {
3418
3968
  if (!this.posthog) return;
3419
3969
  this.posthog.capture({
3420
3970
  distinctId: this.distinctId,
3421
3971
  event,
3972
+ // Super-props are spread LAST so they always win against a same-
3973
+ // named per-event prop (see ZERO-43: a `source: undefined` on
3974
+ // `search_executed` was silently clobbering the `cli` surface stamp).
3975
+ // The reserved super-prop names are enforced by the events registry
3976
+ // test in `analytics/__tests__/events.test.ts` — any new event prop
3977
+ // that collides will fail CI.
3422
3978
  properties: {
3979
+ ...properties,
3423
3980
  source: "cli",
3424
3981
  // biome-ignore lint/style/useNamingConvention: snake_case is standard for analytics event properties
3425
3982
  cli_version: this.cliVersion,
@@ -3429,14 +3986,14 @@ var AnalyticsService = class {
3429
3986
  // biome-ignore lint/style/useNamingConvention: snake_case for analytics
3430
3987
  request_id: this.requestId,
3431
3988
  // biome-ignore lint/style/useNamingConvention: snake_case for analytics
3432
- agent_host: this.agentHost,
3433
- ...properties
3989
+ agent_host: this.agentHost
3434
3990
  }
3435
3991
  });
3436
3992
  }
3437
3993
  captureException(error, properties) {
3438
3994
  if (!this.posthog) return;
3439
3995
  this.posthog.captureException(error, this.distinctId, {
3996
+ ...properties,
3440
3997
  source: "cli",
3441
3998
  // biome-ignore lint/style/useNamingConvention: snake_case for analytics
3442
3999
  cli_version: this.cliVersion,
@@ -3446,8 +4003,7 @@ var AnalyticsService = class {
3446
4003
  // biome-ignore lint/style/useNamingConvention: snake_case for analytics
3447
4004
  request_id: this.requestId,
3448
4005
  // biome-ignore lint/style/useNamingConvention: snake_case for analytics
3449
- agent_host: this.agentHost,
3450
- ...properties
4006
+ agent_host: this.agentHost
3451
4007
  });
3452
4008
  }
3453
4009
  async shutdown() {
@@ -3460,14 +4016,14 @@ var AnalyticsService = class {
3460
4016
  };
3461
4017
 
3462
4018
  // src/services/state-service.ts
3463
- import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "fs";
3464
- import { join as join5 } from "path";
4019
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
4020
+ import { join as join6 } from "path";
3465
4021
  var RECENT_SEARCH_LIMIT = 10;
3466
4022
  var StateService = class {
3467
4023
  constructor(zeroDir) {
3468
4024
  this.zeroDir = zeroDir;
3469
- this.lastSearchPath = join5(zeroDir, "last_search.json");
3470
- this.recentSearchesPath = join5(zeroDir, "recent_searches.json");
4025
+ this.lastSearchPath = join6(zeroDir, "last_search.json");
4026
+ this.recentSearchesPath = join6(zeroDir, "recent_searches.json");
3471
4027
  }
3472
4028
  lastSearchPath;
3473
4029
  recentSearchesPath;
@@ -3487,7 +4043,7 @@ var StateService = class {
3487
4043
  loadLastSearch = () => {
3488
4044
  try {
3489
4045
  if (!existsSync6(this.lastSearchPath)) return null;
3490
- const raw = readFileSync9(this.lastSearchPath, "utf8");
4046
+ const raw = readFileSync10(this.lastSearchPath, "utf8");
3491
4047
  return JSON.parse(raw);
3492
4048
  } catch {
3493
4049
  return null;
@@ -3499,7 +4055,7 @@ var StateService = class {
3499
4055
  const last = this.loadLastSearch();
3500
4056
  return { searches: last ? [last] : [] };
3501
4057
  }
3502
- const raw = readFileSync9(this.recentSearchesPath, "utf8");
4058
+ const raw = readFileSync10(this.recentSearchesPath, "utf8");
3503
4059
  const parsed = JSON.parse(raw);
3504
4060
  return { searches: parsed.searches ?? [] };
3505
4061
  } catch {
@@ -3507,18 +4063,23 @@ var StateService = class {
3507
4063
  }
3508
4064
  };
3509
4065
  // Walk recent searches newest-first, returning the first one whose
3510
- // results contain `capabilityId` (uid or slug match). Preferred over
3511
- // "loadLastSearch" for attributing rank — handles the parallel-search
3512
- // case where the most recent search isn't the one being fetched.
3513
- findSearchContextByCapability = (capabilityId) => {
4066
+ // results contain `capabilityRef` (matches against uid OR slug).
4067
+ // Preferred over "loadLastSearch" for attributing rank — handles the
4068
+ // parallel-search case where the most recent search isn't the one being
4069
+ // fetched.
4070
+ findSearchContextByCapability = (capabilityRef) => {
3514
4071
  const recent = this.loadRecentSearches();
3515
4072
  for (const search of recent.searches) {
3516
- const entry = search.capabilities.find((c) => c.id === capabilityId);
4073
+ const entry = search.capabilities.find(
4074
+ (c) => c.id === capabilityRef || c.slug === capabilityRef
4075
+ );
3517
4076
  if (entry) {
3518
4077
  return {
3519
4078
  searchId: search.searchId,
3520
4079
  resultRank: entry.position,
3521
4080
  capabilityId: entry.id,
4081
+ capabilityUid: entry.id,
4082
+ capabilitySlug: entry.slug ?? null,
3522
4083
  url: entry.url,
3523
4084
  displayCostAmount: entry.displayCostAmount
3524
4085
  };
@@ -3543,6 +4104,8 @@ var StateService = class {
3543
4104
  searchId: search.searchId,
3544
4105
  resultRank: entry.position,
3545
4106
  capabilityId: entry.id,
4107
+ capabilityUid: entry.id,
4108
+ capabilitySlug: entry.slug ?? null,
3546
4109
  url: entry.url,
3547
4110
  displayCostAmount: entry.displayCostAmount
3548
4111
  };
@@ -3590,40 +4153,63 @@ var detectAgentHost = (env = process.env) => {
3590
4153
 
3591
4154
  // src/app/app-services.ts
3592
4155
  var CLI_VERSION = package_default.version;
3593
- var getServices = (env) => {
3594
- let privateKey = env.ZERO_PRIVATE_KEY ? env.ZERO_PRIVATE_KEY : null;
3595
- const zeroDir = join6(homedir5(), ".zero");
3596
- const configPath = join6(zeroDir, "config.json");
3597
- if (!privateKey) {
3598
- try {
3599
- if (existsSync7(configPath)) {
3600
- const config = JSON.parse(readFileSync10(configPath, "utf8"));
3601
- if (typeof config.privateKey === "string") {
3602
- privateKey = config.privateKey;
3603
- }
3604
- }
3605
- } catch {
3606
- }
4156
+ var resolveCredentials = (env, config) => {
4157
+ if (config.session) {
4158
+ return {
4159
+ credentials: {
4160
+ kind: "session",
4161
+ accessToken: config.session.accessToken,
4162
+ refreshToken: config.session.refreshToken,
4163
+ userId: config.session.userId
4164
+ },
4165
+ privateKey: null
4166
+ };
3607
4167
  }
3608
- const account = privateKey ? privateKeyToAccount4(privateKey) : null;
3609
- let lowBalanceWarning = 1;
3610
- try {
3611
- if (existsSync7(configPath)) {
3612
- const config = JSON.parse(readFileSync10(configPath, "utf8"));
3613
- if (typeof config.lowBalanceWarning === "number") {
3614
- lowBalanceWarning = config.lowBalanceWarning;
3615
- }
3616
- }
3617
- } catch {
4168
+ if (env.ZERO_PRIVATE_KEY) {
4169
+ return {
4170
+ credentials: { kind: "none" },
4171
+ privateKey: env.ZERO_PRIVATE_KEY
4172
+ };
4173
+ }
4174
+ if (config.privateKey) {
4175
+ return {
4176
+ credentials: { kind: "none" },
4177
+ privateKey: config.privateKey
4178
+ };
3618
4179
  }
3619
- const apiService = new ApiService(env.ZERO_API_URL, account);
4180
+ return { credentials: { kind: "none" }, privateKey: null };
4181
+ };
4182
+ var buildOnSessionRefreshed = (configPath) => async (tokens) => {
4183
+ const current = readConfig(configPath);
4184
+ if (!current.session) return;
4185
+ const next = {
4186
+ ...current,
4187
+ session: {
4188
+ ...current.session,
4189
+ accessToken: tokens.accessToken,
4190
+ refreshToken: tokens.refreshToken
4191
+ }
4192
+ };
4193
+ writeSecureFile(configPath, JSON.stringify(next, null, 2));
4194
+ };
4195
+ var getServices = (env) => {
4196
+ const zeroDir = join7(homedir6(), ".zero");
4197
+ const configPath = join7(zeroDir, "config.json");
4198
+ const config = existsSync7(configPath) ? readConfig(configPath) : {};
4199
+ const { credentials, privateKey } = resolveCredentials(env, config);
4200
+ const account = privateKey ? privateKeyToAccount4(privateKey) : null;
4201
+ const lowBalanceWarning = typeof config.lowBalanceWarning === "number" ? config.lowBalanceWarning : 1;
4202
+ const apiService = new ApiService(
4203
+ env.ZERO_API_URL,
4204
+ account,
4205
+ credentials,
4206
+ buildOnSessionRefreshed(configPath)
4207
+ );
3620
4208
  const paymentService = new PaymentService(account, { lowBalanceWarning });
3621
4209
  const stateService = new StateService(zeroDir);
3622
4210
  const walletService = new WalletService(
3623
4211
  apiService.walletAddress,
3624
- {
3625
- lowBalanceWarning
3626
- },
4212
+ { lowBalanceWarning },
3627
4213
  paymentService.getTotalBalance
3628
4214
  );
3629
4215
  const analyticsService = new AnalyticsService({
@@ -3665,8 +4251,8 @@ import {
3665
4251
  readlinkSync,
3666
4252
  writeFileSync as writeFileSync6
3667
4253
  } from "fs";
3668
- import { homedir as homedir6 } from "os";
3669
- import { dirname as dirname3, join as join7, resolve } from "path";
4254
+ import { homedir as homedir7 } from "os";
4255
+ import { dirname as dirname3, join as join8, resolve } from "path";
3670
4256
  var CACHE_FILENAME = "update_check.json";
3671
4257
  var NPM_REGISTRY_URL = "https://registry.npmjs.org/@zeroxyz/cli/latest";
3672
4258
  var CHECK_INTERVAL_MS = 60 * 60 * 1e3;
@@ -3690,10 +4276,10 @@ var resolveExecPath = (execPath) => {
3690
4276
  var detectInstallMethod = (opts = {}) => {
3691
4277
  const execPath = opts.execPath ?? process.execPath;
3692
4278
  const pkg = opts.pkg ?? process.pkg;
3693
- const home = opts.home ?? homedir6();
4279
+ const home = opts.home ?? homedir7();
3694
4280
  if (pkg) return "binary";
3695
4281
  const resolved = resolveExecPath(execPath);
3696
- const zeroBin = join7(home, ".zero", "bin");
4282
+ const zeroBin = join8(home, ".zero", "bin");
3697
4283
  if (resolved.startsWith(zeroBin)) return "binary";
3698
4284
  return "npm";
3699
4285
  };
@@ -3718,7 +4304,7 @@ var compareVersions = (a, b) => {
3718
4304
  if (pb.pre === null) return -1;
3719
4305
  return pa.pre < pb.pre ? -1 : 1;
3720
4306
  };
3721
- var cachePath = (zeroDir) => join7(zeroDir, CACHE_FILENAME);
4307
+ var cachePath = (zeroDir) => join8(zeroDir, CACHE_FILENAME);
3722
4308
  var readCache = (zeroDir) => {
3723
4309
  try {
3724
4310
  const path = cachePath(zeroDir);
@@ -3812,7 +4398,7 @@ var main = async () => {
3812
4398
  console.error("Failed to create app context");
3813
4399
  process.exit(1);
3814
4400
  }
3815
- const zeroDir = join8(homedir7(), ".zero");
4401
+ const zeroDir = join9(homedir8(), ".zero");
3816
4402
  maybePrintUpdateBanner(zeroDir, package_default.version);
3817
4403
  const app = createApp(appContext);
3818
4404
  let caughtError = null;