@zeroxyz/cli 1.26.0 → 1.27.0

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 +474 -134
  2. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -78668,7 +78668,7 @@ init_esm_shims();
78668
78668
  // ../sdk/dist/index.js
78669
78669
  init_esm_shims();
78670
78670
 
78671
- // ../sdk/dist/chunk-YRHWENJ7.js
78671
+ // ../sdk/dist/chunk-MQZT5KEN.js
78672
78672
  init_esm_shims();
78673
78673
 
78674
78674
  // ../../node_modules/.pnpm/viem@2.52.2_bufferutil@4.1.0_typescript@5.9.3_utf-8-validate@6.0.6_zod@4.3.6/node_modules/viem/_esm/index.js
@@ -164225,6 +164225,20 @@ var ERC20_BALANCE_ABI = [
164225
164225
  }
164226
164226
  ];
164227
164227
  var USDC_DECIMALS = 6;
164228
+ var ERROR_MAX = 500;
164229
+ var redactUrl = (raw) => {
164230
+ try {
164231
+ const parsed = new URL(raw);
164232
+ return `${parsed.origin}${parsed.pathname}`;
164233
+ } catch {
164234
+ return "<invalid-url>";
164235
+ }
164236
+ };
164237
+ var truncateError = (raw) => raw.length > ERROR_MAX ? `${raw.slice(0, ERROR_MAX)}\u2026` : raw;
164238
+ var package_default = {
164239
+ version: "0.20.0"
164240
+ };
164241
+ var SDK_VERSION2 = package_default.version;
164228
164242
  var ZeroError = class extends Error {
164229
164243
  code;
164230
164244
  constructor(code, message2, options) {
@@ -164384,10 +164398,6 @@ var normalizeTransportEnvelopeRequest = (url4, opts) => {
164384
164398
  body: hasInnerBody ? JSON.stringify(innerBody) : void 0
164385
164399
  };
164386
164400
  };
164387
- var package_default = {
164388
- version: "0.19.0"
164389
- };
164390
- var SDK_VERSION2 = package_default.version;
164391
164401
  var refreshResponseSchema = external_exports3.object({
164392
164402
  accessToken: external_exports3.string(),
164393
164403
  refreshToken: external_exports3.string()
@@ -165044,6 +165054,10 @@ var resolveDefaultPrefund = (displayCostAmount) => {
165044
165054
  return bounded.toFixed(6).replace(/\.?0+$/, "");
165045
165055
  };
165046
165056
  var isUintString = (v) => typeof v === "string" && v.length > 0 && v.length <= MAX_AMOUNT_DIGITS && UINT_RE.test(v);
165057
+ var buildTempoPaymentMethods = (parameters) => [
165058
+ tempo2(parameters),
165059
+ tempo2.sessionLegacy.method(parameters)
165060
+ ];
165047
165061
  var coerceTempoChainId = (raw) => {
165048
165062
  const id = typeof raw === "number" && Number.isFinite(raw) ? raw : typeof raw === "string" && /^-?[0-9]+$/.test(raw) ? Number(raw) : null;
165049
165063
  if (id === TEMPO_CHAIN_ID2) return { id, chain: "tempo" };
@@ -165433,43 +165447,44 @@ var Payments = class {
165433
165447
  let capturedChallenge;
165434
165448
  let channelEntry;
165435
165449
  let openTimeCumulative;
165450
+ const handleChannelUpdate = (entry) => {
165451
+ let cumulative;
165452
+ if (typeof entry.cumulativeAmount === "bigint") {
165453
+ cumulative = entry.cumulativeAmount;
165454
+ } else {
165455
+ cumulative = 0n;
165456
+ this.client.logger?.({
165457
+ level: "warn",
165458
+ message: "mppx emitted non-bigint cumulativeAmount; coerced to 0n",
165459
+ meta: { actualType: typeof entry.cumulativeAmount }
165460
+ });
165461
+ }
165462
+ if (openTimeCumulative === void 0 && entry.opened) {
165463
+ openTimeCumulative = cumulative;
165464
+ }
165465
+ const wasOpened = channelEntry?.opened ?? false;
165466
+ channelEntry = {
165467
+ channelId: entry.channelId,
165468
+ // mppx >=0.8 renamed the channel-entry escrow field from
165469
+ // `escrowContract` to `escrow` (see SessionManager
165470
+ // entryFromSnapshot); the legacy client still emits
165471
+ // `escrowContract`. Read `escrow` first, fall back to the old
165472
+ // name, then to any value we already latched so a later
165473
+ // escrow-less emit can't blank an open channel's escrow.
165474
+ escrowContract: entry.escrow ?? entry.escrowContract ?? channelEntry?.escrowContract ?? "0x0000000000000000000000000000000000000000",
165475
+ cumulativeAmount: cumulative,
165476
+ opened: wasOpened || entry.opened
165477
+ };
165478
+ };
165479
+ const tempoMethodParams = {
165480
+ account,
165481
+ maxDeposit: input.maxPay ?? resolveDefaultPrefund(input.displayCostAmount),
165482
+ onChannelUpdate: handleChannelUpdate
165483
+ };
165436
165484
  const mppx = Mppx_exports.create({
165437
165485
  polyfill: false,
165438
165486
  fetch: configuredFetch,
165439
- methods: [
165440
- tempo2({
165441
- account,
165442
- maxDeposit: input.maxPay ?? resolveDefaultPrefund(input.displayCostAmount),
165443
- onChannelUpdate: (entry) => {
165444
- let cumulative;
165445
- if (typeof entry.cumulativeAmount === "bigint") {
165446
- cumulative = entry.cumulativeAmount;
165447
- } else {
165448
- cumulative = 0n;
165449
- this.client.logger?.({
165450
- level: "warn",
165451
- message: "mppx emitted non-bigint cumulativeAmount; coerced to 0n",
165452
- meta: { actualType: typeof entry.cumulativeAmount }
165453
- });
165454
- }
165455
- if (openTimeCumulative === void 0 && entry.opened) {
165456
- openTimeCumulative = cumulative;
165457
- }
165458
- const wasOpened = channelEntry?.opened ?? false;
165459
- channelEntry = {
165460
- channelId: entry.channelId,
165461
- // mppx >=0.8 renamed the channel-entry escrow field from
165462
- // `escrowContract` to `escrow` (see SessionManager
165463
- // entryFromSnapshot). Read `escrow` first, fall back to the
165464
- // old name, then to any value we already latched so a later
165465
- // escrow-less emit can't blank an open channel's escrow.
165466
- escrowContract: entry.escrow ?? entry.escrowContract ?? channelEntry?.escrowContract ?? "0x0000000000000000000000000000000000000000",
165467
- cumulativeAmount: cumulative,
165468
- opened: wasOpened || entry.opened
165469
- };
165470
- }
165471
- })
165472
- ],
165487
+ methods: buildTempoPaymentMethods(tempoMethodParams),
165473
165488
  onChallenge: async (challenge2) => {
165474
165489
  capturedChallenge = challenge2;
165475
165490
  capturedAmount = await this.prepareTempoFunds(
@@ -165989,7 +166004,7 @@ var isTextContentType = (contentType) => {
165989
166004
  const lower = contentType.toLowerCase();
165990
166005
  return TEXT_CONTENT_TYPES.some((marker) => lower.includes(marker));
165991
166006
  };
165992
- var redactUrl = (url4) => {
166007
+ var redactUrl2 = (url4) => {
165993
166008
  try {
165994
166009
  const u = new URL(url4);
165995
166010
  return `${u.origin}${u.pathname}`;
@@ -166301,7 +166316,7 @@ var resolveRecordingClient = (client, opts) => {
166301
166316
  var recordErrorFetchRun = async (client, opts, result, capabilityIdOverride, requestUrl, requestMethod) => {
166302
166317
  const effectiveCapId = capabilityIdOverride ?? opts.capabilityId;
166303
166318
  const urlTarget = requestUrl ? {
166304
- requestUrl: redactUrl(requestUrl),
166319
+ requestUrl: redactUrl2(requestUrl),
166305
166320
  ...requestMethod && { requestMethod }
166306
166321
  } : void 0;
166307
166322
  const runTarget = effectiveCapId !== void 0 ? { capabilityId: effectiveCapId, ...urlTarget } : urlTarget ?? null;
@@ -166386,7 +166401,7 @@ var clientFetch = async (client, url4, opts = {}) => {
166386
166401
  client,
166387
166402
  opts
166388
166403
  );
166389
- const resolvePromise = !capabilityIdRequested && canRecordForResolve ? client.capabilities.resolveByUrl(redactUrl(requestUrl), method).catch(() => null) : Promise.resolve(null);
166404
+ const resolvePromise = !capabilityIdRequested && canRecordForResolve ? client.capabilities.resolveByUrl(redactUrl2(requestUrl), method).catch(() => null) : Promise.resolve(null);
166390
166405
  const probeHeaders = withDefaultContentType(opts.headers, requestBody);
166391
166406
  const requestInit = {
166392
166407
  method,
@@ -166671,9 +166686,9 @@ var clientFetch = async (client, url4, opts = {}) => {
166671
166686
  } : { resolved: false } : void 0;
166672
166687
  const runTarget = effectiveCapabilityId !== void 0 ? {
166673
166688
  capabilityId: effectiveCapabilityId,
166674
- requestUrl: redactUrl(requestUrl),
166689
+ requestUrl: redactUrl2(requestUrl),
166675
166690
  requestMethod: method
166676
- } : { requestUrl: redactUrl(requestUrl), requestMethod: method };
166691
+ } : { requestUrl: redactUrl2(requestUrl), requestMethod: method };
166677
166692
  const fetchOutcome = computeOutcome(response.status, payment, warnings);
166678
166693
  {
166679
166694
  const { recordingClient, canRecord } = resolveRecordingClient(client, opts);
@@ -166740,7 +166755,7 @@ var clientFetch = async (client, url4, opts = {}) => {
166740
166755
  level: "warn",
166741
166756
  message: "Failed to record run for fetch",
166742
166757
  meta: {
166743
- url: redactUrl(url4),
166758
+ url: redactUrl2(url4),
166744
166759
  capabilityId: effectiveCapabilityId,
166745
166760
  error: errorMessage2,
166746
166761
  errorCode: code
@@ -168069,6 +168084,219 @@ var Wallet = class {
168069
168084
  var DEFAULT_BASE_URL = "https://api.zero.xyz";
168070
168085
  var DEFAULT_TIMEOUT_MS = 18e4;
168071
168086
  var DEFAULT_MAX_RETRIES = 2;
168087
+ var QUERY_MAX = 200;
168088
+ var toAnalyticsWallet = (address5) => address5.toLowerCase();
168089
+ var SAFE_ERROR_NAMES = /* @__PURE__ */ new Set([
168090
+ "DOMException",
168091
+ "Error",
168092
+ "TypeError",
168093
+ "ZeroAgentAuthError",
168094
+ "ZeroApiError",
168095
+ "ZeroAuthError",
168096
+ "ZeroConfigurationError",
168097
+ "ZeroError",
168098
+ "ZeroInsufficientFundsError",
168099
+ "ZeroPaymentError",
168100
+ "ZeroSessionCloseFailedError",
168101
+ "ZeroTimeoutError",
168102
+ "ZeroValidationError",
168103
+ "ZeroWalletError"
168104
+ ]);
168105
+ var SAFE_ERROR_CODES = /* @__PURE__ */ new Set([
168106
+ "api_error",
168107
+ "auth_error",
168108
+ "auth_no_credentials",
168109
+ "auth_session_expired",
168110
+ "configuration_error",
168111
+ "insufficient_funds",
168112
+ "payment_error",
168113
+ "payment_session_close_failed",
168114
+ "timeout_error",
168115
+ "validation_error",
168116
+ "wallet_error"
168117
+ ]);
168118
+ var safeErrorLabel = (error98, fallback2) => {
168119
+ const record3 = typeof error98 === "object" && error98 !== null ? error98 : null;
168120
+ const name = typeof record3?.name === "string" && SAFE_ERROR_NAMES.has(record3.name) ? record3.name : "Error";
168121
+ const code = typeof record3?.code === "string" && SAFE_ERROR_CODES.has(record3.code) ? record3.code : fallback2;
168122
+ return `${name}:${code}`;
168123
+ };
168124
+ var sanitizeRunTrackingSkipReason = (reason) => reason.split(":", 1)[0] ?? "run_tracking_failed";
168125
+ var outcomeToErrorClass = (outcome, status) => {
168126
+ switch (outcome) {
168127
+ case "insufficient_funds":
168128
+ return "insufficient_funds";
168129
+ case "payment_failed":
168130
+ return "payment_failed";
168131
+ case "payment_rejected":
168132
+ case "payment_close_failed":
168133
+ return "payment_rejected";
168134
+ case "network_error":
168135
+ return "network";
168136
+ case "server_error":
168137
+ if (status !== null && status >= 400 && status < 500)
168138
+ return "upstream_4xx";
168139
+ if (status !== null && status >= 500) return "upstream_5xx";
168140
+ return "unknown";
168141
+ default:
168142
+ return "unknown";
168143
+ }
168144
+ };
168145
+ var classifyFetchError = (error98) => {
168146
+ const record3 = typeof error98 === "object" && error98 !== null ? error98 : null;
168147
+ const status = typeof record3?.status === "number" ? record3.status : null;
168148
+ const code = typeof record3?.code === "string" ? record3.code : null;
168149
+ switch (code) {
168150
+ case "validation_error":
168151
+ return { errorClass: "schema_validation_failed", status };
168152
+ case "timeout_error":
168153
+ return { errorClass: "network", status };
168154
+ case "auth_error":
168155
+ case "auth_session_expired":
168156
+ case "auth_no_credentials":
168157
+ return { errorClass: "auth_missing", status };
168158
+ case "insufficient_funds":
168159
+ return { errorClass: "insufficient_funds", status };
168160
+ case "payment_error":
168161
+ return { errorClass: "payment_failed", status };
168162
+ case "payment_session_close_failed":
168163
+ return { errorClass: "payment_rejected", status };
168164
+ case "api_error":
168165
+ if (status !== null && status >= 400 && status < 500)
168166
+ return { errorClass: "upstream_4xx", status };
168167
+ if (status !== null && status >= 500)
168168
+ return { errorClass: "upstream_5xx", status };
168169
+ return { errorClass: "unknown", status };
168170
+ default:
168171
+ return { errorClass: "unknown", status };
168172
+ }
168173
+ };
168174
+ var isCallerAbort = (error98) => {
168175
+ const record3 = typeof error98 === "object" && error98 !== null ? error98 : null;
168176
+ if (record3?.name === "AbortError") return true;
168177
+ if (record3?.code !== "api_error" || record3.status !== 0) return false;
168178
+ const cause = typeof record3.cause === "object" && record3.cause !== null ? record3.cause : null;
168179
+ return cause?.name === "AbortError";
168180
+ };
168181
+ var AnalyticsEmitter = class {
168182
+ isEnabled;
168183
+ sink;
168184
+ distinctIdOverride;
168185
+ extraProps;
168186
+ // Mutable: session-mode clients don't know their wallet until the managed
168187
+ // account resolves. setWalletAddress stamps it so later events attribute
168188
+ // correctly and the wallet becomes the distinctId fallback.
168189
+ walletAddress;
168190
+ constructor(opts, initialWallet) {
168191
+ this.isEnabled = opts?.enabled === true && typeof opts.capture === "function";
168192
+ this.sink = this.isEnabled ? opts?.capture : void 0;
168193
+ this.distinctIdOverride = opts?.distinctId;
168194
+ this.extraProps = opts?.properties ?? {};
168195
+ this.walletAddress = this.isEnabled && initialWallet ? toAnalyticsWallet(initialWallet) : null;
168196
+ }
168197
+ setWalletAddress(address5) {
168198
+ this.walletAddress = toAnalyticsWallet(address5);
168199
+ }
168200
+ emit(event, properties) {
168201
+ if (!this.sink) return;
168202
+ const distinctId = this.distinctIdOverride ?? this.walletAddress ?? null;
168203
+ try {
168204
+ this.sink({
168205
+ event,
168206
+ distinctId,
168207
+ properties: {
168208
+ ...this.extraProps,
168209
+ // SDK-native truth wins over caller enrichment. Per-call/base props
168210
+ // may attach context but cannot rewrite outcomes, IDs, or filters.
168211
+ ...properties,
168212
+ // Reserved stamps always win (ZERO-43: a stray same-named prop must
168213
+ // not clobber the cross-surface dimensions).
168214
+ source: "sdk",
168215
+ sdk_version: SDK_VERSION2,
168216
+ wallet_address: this.walletAddress
168217
+ }
168218
+ });
168219
+ } catch {
168220
+ }
168221
+ }
168222
+ search(query, opts, res) {
168223
+ if (!this.sink) return;
168224
+ this.emit("sdk_search", {
168225
+ // Caller-side context is enrichment only: native fields below win if
168226
+ // an adopter accidentally reuses one of their names.
168227
+ ...opts.analytics?.props ?? {},
168228
+ // Truncate with a trailing ellipsis — same shape as truncateError and
168229
+ // the CLI's truncateQuery, so a consumer mapping this back to its own
168230
+ // event (e.g. CLI search_executed) is byte-for-byte identical.
168231
+ query: query.length > QUERY_MAX ? `${query.slice(0, QUERY_MAX)}\u2026` : query,
168232
+ query_length: query.length,
168233
+ result_count: res.capabilities.length,
168234
+ search_id: res.searchId,
168235
+ total: res.total,
168236
+ has_more: res.hasMore,
168237
+ offset: opts.offset ?? null,
168238
+ limit: opts.limit ?? null,
168239
+ free_only: opts.freeOnly ?? null,
168240
+ max_cost: opts.maxCost ?? null,
168241
+ protocol: opts.protocol ?? null,
168242
+ availability_status: opts.availabilityStatus ?? null,
168243
+ include_all: opts.includeAll ?? null,
168244
+ listing_source: opts.source ?? null,
168245
+ exclude_listing_source: opts.excludeSource ?? null
168246
+ });
168247
+ }
168248
+ fetchExecuted(url4, opts, res) {
168249
+ if (!this.sink) return;
168250
+ const resolution = res.capabilityResolution;
168251
+ this.emit("fetch_executed", {
168252
+ // Caller-side context is enrichment only; execution truth below wins.
168253
+ ...opts.analytics?.props ?? {},
168254
+ url: redactUrl(url4),
168255
+ status: res.status,
168256
+ outcome: res.outcome,
168257
+ error_class: res.outcome === "success" ? null : outcomeToErrorClass(res.outcome, res.status),
168258
+ latency_ms: res.latencyMs,
168259
+ has_payment: res.payment !== null,
168260
+ payment_protocol: res.payment?.protocol ?? null,
168261
+ payment_amount: res.payment?.amount ?? null,
168262
+ // Opaque cap uid / slug / attribution token — not PII, useful for
168263
+ // joining back to the capabilities table in BI.
168264
+ capability_id: opts.capabilityId ?? null,
168265
+ capability_uid: resolution?.resolved ? resolution.capabilityId ?? null : null,
168266
+ capability_slug: resolution?.resolved ? resolution.capabilitySlug ?? null : null,
168267
+ search_id: opts.searchId ?? null,
168268
+ // Always-present correlation key (runId is null exactly when recording
168269
+ // failed), so dashboards can join the event to the execution even then.
168270
+ fetch_id: res.fetchId,
168271
+ run_id: res.runId,
168272
+ run_tracked: res.runId !== null,
168273
+ run_tracking_skipped: res.runTrackingSkipped?.map(sanitizeRunTrackingSkipReason) ?? null,
168274
+ // Upstream response text can contain credentials or user content. Keep
168275
+ // only the already-derived client-side hash/length for clustering.
168276
+ error_snippet_hash: res.upstreamError?.snippetHash ?? null,
168277
+ error_snippet_length: res.upstreamError?.snippetLength ?? null
168278
+ });
168279
+ }
168280
+ fetchError(url4, opts, error98, errorClass, status) {
168281
+ if (!this.sink) return;
168282
+ this.emit("fetch_error", {
168283
+ url: redactUrl(url4),
168284
+ error_class: errorClass,
168285
+ status,
168286
+ capability_id: opts.capabilityId ?? null,
168287
+ search_id: opts.searchId ?? null,
168288
+ error: safeErrorLabel(error98, errorClass)
168289
+ });
168290
+ }
168291
+ walletResolved(address5, walletSource) {
168292
+ if (!this.sink) return;
168293
+ this.setWalletAddress(address5);
168294
+ this.emit("sdk_wallet_resolved", {
168295
+ resolved_wallet_address: toAnalyticsWallet(address5),
168296
+ wallet_source: walletSource
168297
+ });
168298
+ }
168299
+ };
168072
168300
  var noopRefresh = async () => {
168073
168301
  };
168074
168302
  var searchResultSchema = external_exports3.object({
@@ -168151,6 +168379,13 @@ var ZeroClient = class _ZeroClient {
168151
168379
  fetchImpl;
168152
168380
  fetchOptions;
168153
168381
  logger;
168382
+ analyticsOptions;
168383
+ // Opt-in telemetry (ZERO-434). Always constructed; a no-op when analytics
168384
+ // was omitted/disabled (its methods short-circuit before the sink), so the
168385
+ // hot paths carry no branch of their own. Kept private so the supported
168386
+ // public surface is the constructor option + emitted event types, not the
168387
+ // mutable emitter implementation.
168388
+ analytics;
168154
168389
  // Private — read via getCredentials(), mutate only via applyRefreshedTokens().
168155
168390
  // Keeping a single mutator funnel is what lets the transport replay
168156
168391
  // requests with a fresh access token after a 401 → refresh.
@@ -168209,6 +168444,7 @@ var ZeroClient = class _ZeroClient {
168209
168444
  this.fetchImpl = options.fetch ?? ((...args) => globalThis.fetch(...args));
168210
168445
  this.fetchOptions = options.fetchOptions;
168211
168446
  this.logger = options.logger;
168447
+ this.analyticsOptions = options.analytics;
168212
168448
  this.refreshOverride = options.refresh;
168213
168449
  this.withAccountCache = options._sharedAccountCache ?? /* @__PURE__ */ new Map();
168214
168450
  this.auth = new Auth(this);
@@ -168217,6 +168453,11 @@ var ZeroClient = class _ZeroClient {
168217
168453
  this.payments = new Payments(this);
168218
168454
  this.runs = new Runs(this);
168219
168455
  this.wallet = new Wallet(this);
168456
+ const initialWallet = this.credentials.kind === "account" ? this.credentials.account.address : null;
168457
+ this.analytics = new AnalyticsEmitter(options.analytics, initialWallet);
168458
+ if (initialWallet && this.analytics.isEnabled) {
168459
+ this.analytics.walletResolved(initialWallet, "self_custody");
168460
+ }
168220
168461
  }
168221
168462
  // Convenience factory: derives a viem account from a raw 0x-prefixed
168222
168463
  // private key and constructs a client in account mode. Equivalent to
@@ -168324,7 +168565,8 @@ var ZeroClient = class _ZeroClient {
168324
168565
  defaultHeaders: { ...this.defaultHeaders },
168325
168566
  fetch: this.fetchImpl,
168326
168567
  fetchOptions: this.fetchOptions ? { ...this.fetchOptions } : void 0,
168327
- logger: this.logger
168568
+ logger: this.logger,
168569
+ analytics: this.analyticsOptions
168328
168570
  });
168329
168571
  // Returns a new client bound to a different viem account. Mirrors
168330
168572
  // withSession for the self-custody auth path — multi-tenant servers
@@ -168376,6 +168618,7 @@ var ZeroClient = class _ZeroClient {
168376
168618
  fetch: this.fetchImpl,
168377
168619
  fetchOptions: this.fetchOptions ? { ...this.fetchOptions } : void 0,
168378
168620
  logger: this.logger,
168621
+ analytics: this.analyticsOptions,
168379
168622
  _sharedAccountCache: this.withAccountCache
168380
168623
  });
168381
168624
  this.withAccountCache.set(key, sub);
@@ -168436,6 +168679,7 @@ var ZeroClient = class _ZeroClient {
168436
168679
  }
168437
168680
  const acc = createManagedAccount(this, getAddress(primary.walletAddress));
168438
168681
  this.managedAccount = acc;
168682
+ this.analytics.walletResolved(primary.walletAddress, "privy_embedded");
168439
168683
  return acc;
168440
168684
  })().finally(() => {
168441
168685
  if (this.managedAccountGen === startedAtGen) {
@@ -168479,6 +168723,7 @@ var ZeroClient = class _ZeroClient {
168479
168723
  explicitWallet: true
168480
168724
  });
168481
168725
  this.managedAccountByAddress.set(canonical.toLowerCase(), acc);
168726
+ this.analytics.walletResolved(match.walletAddress, "privy_embedded");
168482
168727
  return acc;
168483
168728
  };
168484
168729
  // Per-caller signal gate: each caller can reject on their own abort
@@ -168519,9 +168764,9 @@ var ZeroClient = class _ZeroClient {
168519
168764
  // `search` is a flat method, not a namespace — it's the hot path most
168520
168765
  // consumers hit first, and Stripe/Anthropic/OpenAI all keep their primary
168521
168766
  // entry point flat for the same reason.
168522
- search = (query, opts = {}) => {
168523
- const { signal, ...rest } = opts;
168524
- return request2(
168767
+ search = async (query, opts = {}) => {
168768
+ const { signal, analytics: _analytics, ...rest } = opts;
168769
+ const response = await request2(
168525
168770
  this,
168526
168771
  {
168527
168772
  method: "POST",
@@ -168531,11 +168776,25 @@ var ZeroClient = class _ZeroClient {
168531
168776
  },
168532
168777
  searchResponseSchema
168533
168778
  );
168779
+ this.analytics.search(query, opts, response);
168780
+ return response;
168534
168781
  };
168535
168782
  // Paid fetch — search → get → fetch is Zero's canonical loop. The hot
168536
168783
  // path is flat on the client, same rationale as `search` above.
168537
168784
  // Orchestration lives in fetch.ts so this class stays readable.
168538
- fetch = (url4, opts = {}) => clientFetch(this, url4, opts);
168785
+ fetch = async (url4, opts = {}) => {
168786
+ try {
168787
+ const result = await clientFetch(this, url4, opts);
168788
+ this.analytics.fetchExecuted(url4, opts, result);
168789
+ return result;
168790
+ } catch (error98) {
168791
+ if (this.analytics.isEnabled && !isCallerAbort(error98)) {
168792
+ const { errorClass, status } = classifyFetchError(error98);
168793
+ this.analytics.fetchError(url4, opts, error98, errorClass, status);
168794
+ }
168795
+ throw error98;
168796
+ }
168797
+ };
168539
168798
  // Poll the API server's /health endpoint until it responds OK. Useful for
168540
168799
  // warming up a cold container before spawning a subprocess that needs the
168541
168800
  // API ready. Resolves when healthy, resolves (silently) when maxAttempts
@@ -168654,16 +168913,6 @@ var tryParseJson2 = (text) => {
168654
168913
  return null;
168655
168914
  }
168656
168915
  };
168657
- var ERROR_MAX = 500;
168658
- var redactUrl2 = (raw) => {
168659
- try {
168660
- const parsed = new URL(raw);
168661
- return `${parsed.origin}${parsed.pathname}`;
168662
- } catch {
168663
- return raw;
168664
- }
168665
- };
168666
- var truncateError = (raw) => raw.length > ERROR_MAX ? `${raw.slice(0, ERROR_MAX)}\u2026` : raw;
168667
168916
  var schemaTypeLabel = (node) => {
168668
168917
  const t = node.type;
168669
168918
  if (typeof t === "string") return t;
@@ -168777,7 +169026,7 @@ var placeholderFor = (fieldName, propSchema) => String(sampleValueFor(fieldName,
168777
169026
 
168778
169027
  // package.json
168779
169028
  var package_default2 = {
168780
- version: "1.26.0"};
169029
+ version: "1.27.0"};
168781
169030
 
168782
169031
  // src/app.ts
168783
169032
  init_esm_shims();
@@ -170489,7 +170738,7 @@ var fetchCommand = (appContext) => new Command("fetch").description(
170489
170738
  const message2 = err instanceof Error ? err.message : "Failed to read request body";
170490
170739
  analyticsService.capture("fetch_error", {
170491
170740
  cliErrorClass: /exceeds the .* byte local cap/.test(message2) ? "payload_too_large" : "schema_validation_failed",
170492
- url: redactUrl2(resolvedUrl),
170741
+ url: redactUrl(resolvedUrl),
170493
170742
  error: truncateError(message2)
170494
170743
  });
170495
170744
  console.error(message2);
@@ -170596,13 +170845,28 @@ var fetchCommand = (appContext) => new Command("fetch").description(
170596
170845
  }
170597
170846
  }
170598
170847
  return {};
170848
+ },
170849
+ // fetch_executed is emitted by the SDK (ZERO-434) and mapped
170850
+ // back to the CLI taxonomy in captureSdk. These are the
170851
+ // CLI-only props the SDK can't compute; they ride onto the
170852
+ // emitted event verbatim. The recovered capabilityUid/Slug on
170853
+ // the bare-URL path come from the SDK's own capabilityResolution
170854
+ // (surfaced on the SDK-origin fetch_executed), NOT from here.
170855
+ analytics: {
170856
+ props: {
170857
+ capabilityId: capabilityId ?? void 0,
170858
+ capabilityUid: capabilityUid ?? void 0,
170859
+ capabilitySlug: capabilitySlug ?? void 0,
170860
+ signingSource: appContext.services.signing?.source ?? "none",
170861
+ fetchOrigin
170862
+ }
170599
170863
  }
170600
170864
  });
170601
170865
  } catch (err) {
170602
170866
  const message2 = err instanceof Error ? err.message : "Fetch failed";
170603
170867
  analyticsService.capture("fetch_error", {
170604
170868
  cliErrorClass: "network",
170605
- url: redactUrl2(resolvedUrl),
170869
+ url: redactUrl(resolvedUrl),
170606
170870
  error: truncateError(message2)
170607
170871
  });
170608
170872
  console.error(` Fetch failed: ${message2}`);
@@ -170610,7 +170874,6 @@ var fetchCommand = (appContext) => new Command("fetch").description(
170610
170874
  return;
170611
170875
  }
170612
170876
  const {
170613
- fetchId,
170614
170877
  runId,
170615
170878
  status,
170616
170879
  latencyMs,
@@ -170701,7 +170964,7 @@ var fetchCommand = (appContext) => new Command("fetch").description(
170701
170964
  capabilityUid = capabilityResolution.capabilityId ?? capabilityUid;
170702
170965
  capabilitySlug = capabilityResolution.capabilitySlug ?? capabilitySlug;
170703
170966
  analyticsService.capture("capability_resolution_recovered", {
170704
- url: redactUrl2(resolvedUrl),
170967
+ url: redactUrl(resolvedUrl),
170705
170968
  method,
170706
170969
  capabilityUid: capabilityResolution.capabilityId,
170707
170970
  capabilitySlug: capabilityResolution.capabilitySlug,
@@ -170715,42 +170978,11 @@ var fetchCommand = (appContext) => new Command("fetch").description(
170715
170978
  );
170716
170979
  }
170717
170980
  analyticsService.capture("capability_resolution_missed", {
170718
- url: redactUrl2(resolvedUrl),
170981
+ url: redactUrl(resolvedUrl),
170719
170982
  method
170720
170983
  });
170721
170984
  }
170722
170985
  }
170723
- analyticsService.capture("fetch_executed", {
170724
- // The correlation key: joins this client event to the server's
170725
- // run_started / run_completed / payment_processed, and to the run
170726
- // row itself. Works even when runId is null — i.e. exactly when
170727
- // the run failed to record, which is what we most need to trace.
170728
- fetchId,
170729
- url: redactUrl2(resolvedUrl),
170730
- status: status ?? void 0,
170731
- outcome,
170732
- latencyMs,
170733
- hasPayment: !!payment,
170734
- paymentProtocol: payment?.protocol,
170735
- paymentAmount: payment && payment.amount !== "unknown" ? payment.amount : void 0,
170736
- // Canonical join keys — capabilitySlug is the dashboard
170737
- // breakdown dimension; capabilityUid is the stable join
170738
- // key with the capabilities table. capabilityId is the
170739
- // back-compat alias (same value as capabilityUid when
170740
- // resolved, else the raw --capability input).
170741
- capabilityUid: capabilityUid ?? void 0,
170742
- capabilitySlug: capabilitySlug ?? void 0,
170743
- capabilityId: capabilityId ?? void 0,
170744
- searchId: searchId ?? void 0,
170745
- resultRank: resultRank ?? void 0,
170746
- runId: runId ?? void 0,
170747
- runTracked: !!runId,
170748
- signingSource: signing?.source ?? "none",
170749
- // A NULL searchId on a direct_slug fetch is correct here,
170750
- // not a funnel gap to paper over.
170751
- fetchOrigin,
170752
- ...resultError && { error: truncateError(resultError) }
170753
- });
170754
170986
  const outcomeInfo = OUTCOMES[outcome];
170755
170987
  if (outcomeInfo.errorClass !== null) {
170756
170988
  const cliErrorClass = cliErrorClassFor(
@@ -170765,7 +170997,7 @@ var fetchCommand = (appContext) => new Command("fetch").description(
170765
170997
  capabilityId: capabilityId ?? void 0,
170766
170998
  searchId: searchId ?? void 0,
170767
170999
  resultRank: resultRank ?? void 0,
170768
- url: redactUrl2(resolvedUrl),
171000
+ url: redactUrl(resolvedUrl),
170769
171001
  status: typeof status === "number" ? status : void 0,
170770
171002
  upstreamErrorMessage: upstreamError?.message,
170771
171003
  errorSnippetHash: upstreamError?.snippetHash,
@@ -170910,7 +171142,7 @@ var fetchCommand = (appContext) => new Command("fetch").description(
170910
171142
  try {
170911
171143
  appContext.services.analyticsService.capture("fetch_error", {
170912
171144
  cliErrorClass: "unknown",
170913
- url: url4 ? redactUrl2(url4) : "unknown",
171145
+ url: url4 ? redactUrl(url4) : "unknown",
170914
171146
  error: truncateError(message2)
170915
171147
  });
170916
171148
  } catch {
@@ -172514,13 +172746,6 @@ Examples:
172514
172746
 
172515
172747
  // src/commands/search-command.ts
172516
172748
  init_esm_shims();
172517
-
172518
- // src/util/redact.ts
172519
- init_esm_shims();
172520
- var QUERY_MAX = 200;
172521
- var truncateQuery = (raw) => raw.length > QUERY_MAX ? `${raw.slice(0, QUERY_MAX)}\u2026` : raw;
172522
-
172523
- // src/commands/search-command.ts
172524
172749
  var DEFAULT_MAX_COST_USD = "30";
172525
172750
  var formatStatusBadge = (status) => {
172526
172751
  if (!status || !(status in AVAILABILITY_BADGES)) return "";
@@ -172557,7 +172782,7 @@ var searchCommand = (appContext) => new Command("search").description("Search fo
172557
172782
  ).action(
172558
172783
  async (query, options) => {
172559
172784
  try {
172560
- const { analyticsService, client } = appContext.services;
172785
+ const { client } = appContext.services;
172561
172786
  if (options.protocol && !["x402", "mpp"].includes(options.protocol)) {
172562
172787
  console.error(
172563
172788
  `Invalid protocol "${options.protocol}". Must be x402 or mpp.`
@@ -172586,27 +172811,14 @@ var searchCommand = (appContext) => new Command("search").description("Search fo
172586
172811
  availabilityStatus: effectiveStatus,
172587
172812
  includeAll: options.all,
172588
172813
  source: options.source,
172589
- excludeSource: options.excludeSource
172590
- });
172591
- analyticsService.capture("search_executed", {
172592
- query: truncateQuery(query),
172593
- queryLength: query.length,
172594
- resultCount: result.capabilities.length,
172595
- searchId: result.searchId,
172596
- total: result.total,
172597
- hasMore: result.hasMore,
172598
- offset: options.offset,
172599
- limit: options.limit,
172600
- freeOnly: options.free ?? false,
172601
- maxCost: effectiveMaxCost,
172602
- maxCostDefaulted: appliedDefaultMaxCost,
172603
- protocol: options.protocol,
172604
- availabilityStatus: effectiveStatus,
172605
- availabilityStatusDefaulted: appliedDefaultStatus,
172606
- includeAll: options.all ?? false,
172607
- listingSource: options.source,
172608
- excludeListingSource: options.excludeSource,
172609
- json: options.json ?? false
172814
+ excludeSource: options.excludeSource,
172815
+ analytics: {
172816
+ props: {
172817
+ maxCostDefaulted: appliedDefaultMaxCost,
172818
+ availabilityStatusDefaulted: appliedDefaultStatus,
172819
+ json: options.json ?? false
172820
+ }
172821
+ }
172610
172822
  });
172611
172823
  if (options.json) {
172612
172824
  console.log(
@@ -177481,10 +177693,82 @@ var PostHog = class extends PostHogBackendClient {
177481
177693
  }
177482
177694
  };
177483
177695
 
177696
+ // src/analytics/sdk-event-map.ts
177697
+ init_esm_shims();
177698
+ var orUndef = (value) => value === null ? void 0 : value;
177699
+ var dropUnknownAmount = (value) => value === null || value === "unknown" ? void 0 : value;
177700
+ var mapSdkEventToCliEvent = (event) => {
177701
+ const p = event.properties;
177702
+ switch (event.event) {
177703
+ case "sdk_search":
177704
+ return {
177705
+ event: "search_executed",
177706
+ props: {
177707
+ query: p.query,
177708
+ queryLength: p.query_length,
177709
+ resultCount: p.result_count,
177710
+ searchId: p.search_id,
177711
+ total: p.total,
177712
+ hasMore: p.has_more,
177713
+ offset: orUndef(p.offset),
177714
+ limit: orUndef(p.limit),
177715
+ // The CLI historically defaulted these two booleans (never null).
177716
+ freeOnly: p.free_only ?? false,
177717
+ maxCost: orUndef(p.max_cost),
177718
+ // CLI-only, carried in via the SDK's per-call analytics.props.
177719
+ maxCostDefaulted: p.maxCostDefaulted,
177720
+ protocol: orUndef(p.protocol),
177721
+ availabilityStatus: orUndef(p.availability_status),
177722
+ availabilityStatusDefaulted: p.availabilityStatusDefaulted,
177723
+ includeAll: p.include_all ?? false,
177724
+ listingSource: orUndef(p.listing_source),
177725
+ excludeListingSource: orUndef(p.exclude_listing_source),
177726
+ json: p.json
177727
+ }
177728
+ };
177729
+ case "fetch_executed":
177730
+ return {
177731
+ event: "fetch_executed",
177732
+ props: {
177733
+ // SDK-native execution truth (snake→camel).
177734
+ fetchId: p.fetch_id,
177735
+ url: p.url,
177736
+ status: orUndef(p.status),
177737
+ outcome: p.outcome,
177738
+ latencyMs: p.latency_ms,
177739
+ hasPayment: p.has_payment,
177740
+ paymentProtocol: orUndef(p.payment_protocol),
177741
+ paymentAmount: dropUnknownAmount(p.payment_amount),
177742
+ runId: orUndef(p.run_id),
177743
+ runTracked: p.run_tracked,
177744
+ error: orUndef(p.error),
177745
+ // capabilityUid/Slug: prefer the adopter-supplied value (token /
177746
+ // slug paths, from analytics.props), else the SDK's bare-URL
177747
+ // resolution — mirroring how the CLI recovered them post-fetch.
177748
+ capabilityUid: p.capabilityUid ?? orUndef(p.capability_uid),
177749
+ capabilitySlug: p.capabilitySlug ?? orUndef(p.capability_slug),
177750
+ // CLI-only context carried in via analytics.props.
177751
+ capabilityId: p.capabilityId,
177752
+ signingSource: p.signingSource,
177753
+ fetchOrigin: p.fetchOrigin,
177754
+ // searchId is server-derived (always omitted client-side);
177755
+ // resultRank is likewise dead on the CLI event.
177756
+ searchId: orUndef(p.search_id)
177757
+ }
177758
+ };
177759
+ default:
177760
+ return null;
177761
+ }
177762
+ };
177763
+
177484
177764
  // src/services/analytics-service.ts
177765
+ var FORWARDED_SDK_EVENTS = /* @__PURE__ */ new Set([
177766
+ "sdk_wallet_resolved"
177767
+ ]);
177485
177768
  var POSTHOG_API_KEY = "phc_B2vLyNxAf2mnqvdPQajf4d4b2iXc35dep2ZrvebMJLuX";
177486
177769
  var POSTHOG_HOST = "https://us.i.posthog.com";
177487
- var toAnalyticsWallet = (walletAddress) => walletAddress.toLowerCase();
177770
+ var resolvePostHogHost = () => process.env.NODE_ENV === "test" && process.env.ZERO_ANALYTICS_TEST_HOST ? process.env.ZERO_ANALYTICS_TEST_HOST : POSTHOG_HOST;
177771
+ var toAnalyticsWallet2 = (walletAddress) => walletAddress.toLowerCase();
177488
177772
  var AnalyticsService = class {
177489
177773
  posthog;
177490
177774
  distinctId;
@@ -177496,7 +177780,7 @@ var AnalyticsService = class {
177496
177780
  constructor(opts) {
177497
177781
  this.cliVersion = opts.cliVersion;
177498
177782
  this.environment = opts.environment;
177499
- this.walletAddress = opts.walletAddress ? toAnalyticsWallet(opts.walletAddress) : null;
177783
+ this.walletAddress = opts.walletAddress ? toAnalyticsWallet2(opts.walletAddress) : null;
177500
177784
  this.requestId = opts.requestId;
177501
177785
  this.agentHost = opts.agentHost;
177502
177786
  let telemetryEnabled = true;
@@ -177519,7 +177803,7 @@ var AnalyticsService = class {
177519
177803
  return;
177520
177804
  }
177521
177805
  if (opts.walletAddress) {
177522
- this.distinctId = toAnalyticsWallet(opts.walletAddress);
177806
+ this.distinctId = toAnalyticsWallet2(opts.walletAddress);
177523
177807
  } else if (persistedAnonId) {
177524
177808
  this.distinctId = persistedAnonId;
177525
177809
  } else {
@@ -177547,7 +177831,7 @@ var AnalyticsService = class {
177547
177831
  originalConsoleError.apply(console, args);
177548
177832
  };
177549
177833
  this.posthog = new PostHog(POSTHOG_API_KEY, {
177550
- host: POSTHOG_HOST,
177834
+ host: resolvePostHogHost(),
177551
177835
  flushAt: 1,
177552
177836
  flushInterval: 0,
177553
177837
  // Vitest spins up many AnalyticsService instances per process; each
@@ -177561,7 +177845,7 @@ var AnalyticsService = class {
177561
177845
  this.maybeAliasAnonToWallet(
177562
177846
  opts.configPath,
177563
177847
  persistedAnonId,
177564
- toAnalyticsWallet(opts.walletAddress)
177848
+ toAnalyticsWallet2(opts.walletAddress)
177565
177849
  );
177566
177850
  }
177567
177851
  }
@@ -177602,7 +177886,7 @@ var AnalyticsService = class {
177602
177886
  // via PostHog alias, when AnalyticsService is constructed with both
177603
177887
  // the persisted anonId and the new walletAddress.
177604
177888
  setWalletAddress(walletAddress) {
177605
- this.walletAddress = toAnalyticsWallet(walletAddress);
177889
+ this.walletAddress = toAnalyticsWallet2(walletAddress);
177606
177890
  }
177607
177891
  capture(event, properties) {
177608
177892
  if (!this.posthog) return;
@@ -177630,6 +177914,42 @@ var AnalyticsService = class {
177630
177914
  }
177631
177915
  });
177632
177916
  }
177917
+ // Sink for the SDK's opt-in analytics (ZERO-434), wired into the ZeroClient
177918
+ // as its `capture` callback in app-services. This is the CLI end of the
177919
+ // injected-sink pattern — the same shape a partner would write. It keeps the
177920
+ // SDK's own `source: "sdk"` stamp (the event genuinely came from the SDK
177921
+ // layer, not a hand-authored CLI event) but adds the CLI's stitching
177922
+ // super-props so it correlates to the rest of the invocation. Overlapping
177923
+ // events are filtered out via FORWARDED_SDK_EVENTS above — the CLI's own
177924
+ // emissions remain the source of truth for search/fetch.
177925
+ captureSdk(event) {
177926
+ if (!this.posthog) return;
177927
+ const mapped = mapSdkEventToCliEvent(event);
177928
+ if (mapped) {
177929
+ this.capture(mapped.event, mapped.props);
177930
+ return;
177931
+ }
177932
+ if (!FORWARDED_SDK_EVENTS.has(event.event)) return;
177933
+ this.posthog.capture({
177934
+ // The SDK resolves the wallet as distinctId; fall back to the CLI's
177935
+ // distinctId (anon UUID before any wallet) so the event still stitches.
177936
+ distinctId: event.distinctId ?? this.distinctId,
177937
+ event: event.event,
177938
+ properties: {
177939
+ // event.properties already carries source:"sdk", sdk_version, and
177940
+ // wallet_address. Add the CLI's correlation super-props (but NOT
177941
+ // source:"cli" — this is an SDK-layer event, honestly stamped).
177942
+ ...event.properties,
177943
+ environment: this.environment,
177944
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
177945
+ cli_version: this.cliVersion,
177946
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
177947
+ request_id: this.requestId,
177948
+ // biome-ignore lint/style/useNamingConvention: snake_case for analytics
177949
+ agent_host: this.agentHost
177950
+ }
177951
+ });
177952
+ }
177633
177953
  captureException(error98, properties) {
177634
177954
  if (!this.posthog) return;
177635
177955
  this.posthog.captureException(error98, this.distinctId, {
@@ -177715,7 +178035,19 @@ var getServices = async (env) => {
177715
178035
  const byoAccount = byoPrivateKey ? privateKeyToAccount(byoPrivateKey) : null;
177716
178036
  const lowBalanceWarning = typeof config3.lowBalanceWarning === "number" ? config3.lowBalanceWarning : 1;
177717
178037
  const onSessionRefreshed = buildOnSessionRefreshed(configPath);
178038
+ let analyticsSink = null;
178039
+ const pendingSdkEvents = [];
177718
178040
  const client = new ZeroClient({
178041
+ analytics: config3.telemetry === false ? { enabled: false } : {
178042
+ enabled: true,
178043
+ capture: (event) => {
178044
+ if (analyticsSink) {
178045
+ analyticsSink.captureSdk(event);
178046
+ return;
178047
+ }
178048
+ pendingSdkEvents.push(event);
178049
+ }
178050
+ },
177719
178051
  baseUrl: env.ZERO_API_URL,
177720
178052
  // The SDK scopes defaultHeaders to same-origin API calls, so this
177721
178053
  // never reaches capability URLs.
@@ -177756,6 +178088,14 @@ var getServices = async (env) => {
177756
178088
  requestId: randomUUID(),
177757
178089
  agentHost: detectAgentHost()
177758
178090
  });
178091
+ analyticsSink = analyticsService;
178092
+ for (const event of pendingSdkEvents) {
178093
+ try {
178094
+ analyticsService.captureSdk(event);
178095
+ } catch {
178096
+ }
178097
+ }
178098
+ pendingSdkEvents.length = 0;
177759
178099
  return {
177760
178100
  analyticsService,
177761
178101
  client,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeroxyz/cli",
3
- "version": "1.26.0",
3
+ "version": "1.27.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "zero": "dist/index.js",
@@ -19,6 +19,7 @@
19
19
  "dev": "ZERO_ENV=development tsx src/index.ts",
20
20
  "cli": "ZERO_ENV=development ZERO_API_URL=http://localhost:1111 tsx src/index.ts",
21
21
  "test:integration": "vitest run --project integration",
22
+ "test:e2e": "tsup && vitest run --project e2e",
22
23
  "test:online": "vitest run --project online",
23
24
  "test:unit": "vitest run --project unit",
24
25
  "test": "pnpm run test:unit && pnpm run test:integration",