@zeroxyz/cli 1.26.1 → 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 +435 -100
  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-YB3TVCUC.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.1"
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()
@@ -165994,7 +166004,7 @@ var isTextContentType = (contentType) => {
165994
166004
  const lower = contentType.toLowerCase();
165995
166005
  return TEXT_CONTENT_TYPES.some((marker) => lower.includes(marker));
165996
166006
  };
165997
- var redactUrl = (url4) => {
166007
+ var redactUrl2 = (url4) => {
165998
166008
  try {
165999
166009
  const u = new URL(url4);
166000
166010
  return `${u.origin}${u.pathname}`;
@@ -166306,7 +166316,7 @@ var resolveRecordingClient = (client, opts) => {
166306
166316
  var recordErrorFetchRun = async (client, opts, result, capabilityIdOverride, requestUrl, requestMethod) => {
166307
166317
  const effectiveCapId = capabilityIdOverride ?? opts.capabilityId;
166308
166318
  const urlTarget = requestUrl ? {
166309
- requestUrl: redactUrl(requestUrl),
166319
+ requestUrl: redactUrl2(requestUrl),
166310
166320
  ...requestMethod && { requestMethod }
166311
166321
  } : void 0;
166312
166322
  const runTarget = effectiveCapId !== void 0 ? { capabilityId: effectiveCapId, ...urlTarget } : urlTarget ?? null;
@@ -166391,7 +166401,7 @@ var clientFetch = async (client, url4, opts = {}) => {
166391
166401
  client,
166392
166402
  opts
166393
166403
  );
166394
- 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);
166395
166405
  const probeHeaders = withDefaultContentType(opts.headers, requestBody);
166396
166406
  const requestInit = {
166397
166407
  method,
@@ -166676,9 +166686,9 @@ var clientFetch = async (client, url4, opts = {}) => {
166676
166686
  } : { resolved: false } : void 0;
166677
166687
  const runTarget = effectiveCapabilityId !== void 0 ? {
166678
166688
  capabilityId: effectiveCapabilityId,
166679
- requestUrl: redactUrl(requestUrl),
166689
+ requestUrl: redactUrl2(requestUrl),
166680
166690
  requestMethod: method
166681
- } : { requestUrl: redactUrl(requestUrl), requestMethod: method };
166691
+ } : { requestUrl: redactUrl2(requestUrl), requestMethod: method };
166682
166692
  const fetchOutcome = computeOutcome(response.status, payment, warnings);
166683
166693
  {
166684
166694
  const { recordingClient, canRecord } = resolveRecordingClient(client, opts);
@@ -166745,7 +166755,7 @@ var clientFetch = async (client, url4, opts = {}) => {
166745
166755
  level: "warn",
166746
166756
  message: "Failed to record run for fetch",
166747
166757
  meta: {
166748
- url: redactUrl(url4),
166758
+ url: redactUrl2(url4),
166749
166759
  capabilityId: effectiveCapabilityId,
166750
166760
  error: errorMessage2,
166751
166761
  errorCode: code
@@ -168074,6 +168084,219 @@ var Wallet = class {
168074
168084
  var DEFAULT_BASE_URL = "https://api.zero.xyz";
168075
168085
  var DEFAULT_TIMEOUT_MS = 18e4;
168076
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
+ };
168077
168300
  var noopRefresh = async () => {
168078
168301
  };
168079
168302
  var searchResultSchema = external_exports3.object({
@@ -168156,6 +168379,13 @@ var ZeroClient = class _ZeroClient {
168156
168379
  fetchImpl;
168157
168380
  fetchOptions;
168158
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;
168159
168389
  // Private — read via getCredentials(), mutate only via applyRefreshedTokens().
168160
168390
  // Keeping a single mutator funnel is what lets the transport replay
168161
168391
  // requests with a fresh access token after a 401 → refresh.
@@ -168214,6 +168444,7 @@ var ZeroClient = class _ZeroClient {
168214
168444
  this.fetchImpl = options.fetch ?? ((...args) => globalThis.fetch(...args));
168215
168445
  this.fetchOptions = options.fetchOptions;
168216
168446
  this.logger = options.logger;
168447
+ this.analyticsOptions = options.analytics;
168217
168448
  this.refreshOverride = options.refresh;
168218
168449
  this.withAccountCache = options._sharedAccountCache ?? /* @__PURE__ */ new Map();
168219
168450
  this.auth = new Auth(this);
@@ -168222,6 +168453,11 @@ var ZeroClient = class _ZeroClient {
168222
168453
  this.payments = new Payments(this);
168223
168454
  this.runs = new Runs(this);
168224
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
+ }
168225
168461
  }
168226
168462
  // Convenience factory: derives a viem account from a raw 0x-prefixed
168227
168463
  // private key and constructs a client in account mode. Equivalent to
@@ -168329,7 +168565,8 @@ var ZeroClient = class _ZeroClient {
168329
168565
  defaultHeaders: { ...this.defaultHeaders },
168330
168566
  fetch: this.fetchImpl,
168331
168567
  fetchOptions: this.fetchOptions ? { ...this.fetchOptions } : void 0,
168332
- logger: this.logger
168568
+ logger: this.logger,
168569
+ analytics: this.analyticsOptions
168333
168570
  });
168334
168571
  // Returns a new client bound to a different viem account. Mirrors
168335
168572
  // withSession for the self-custody auth path — multi-tenant servers
@@ -168381,6 +168618,7 @@ var ZeroClient = class _ZeroClient {
168381
168618
  fetch: this.fetchImpl,
168382
168619
  fetchOptions: this.fetchOptions ? { ...this.fetchOptions } : void 0,
168383
168620
  logger: this.logger,
168621
+ analytics: this.analyticsOptions,
168384
168622
  _sharedAccountCache: this.withAccountCache
168385
168623
  });
168386
168624
  this.withAccountCache.set(key, sub);
@@ -168441,6 +168679,7 @@ var ZeroClient = class _ZeroClient {
168441
168679
  }
168442
168680
  const acc = createManagedAccount(this, getAddress(primary.walletAddress));
168443
168681
  this.managedAccount = acc;
168682
+ this.analytics.walletResolved(primary.walletAddress, "privy_embedded");
168444
168683
  return acc;
168445
168684
  })().finally(() => {
168446
168685
  if (this.managedAccountGen === startedAtGen) {
@@ -168484,6 +168723,7 @@ var ZeroClient = class _ZeroClient {
168484
168723
  explicitWallet: true
168485
168724
  });
168486
168725
  this.managedAccountByAddress.set(canonical.toLowerCase(), acc);
168726
+ this.analytics.walletResolved(match.walletAddress, "privy_embedded");
168487
168727
  return acc;
168488
168728
  };
168489
168729
  // Per-caller signal gate: each caller can reject on their own abort
@@ -168524,9 +168764,9 @@ var ZeroClient = class _ZeroClient {
168524
168764
  // `search` is a flat method, not a namespace — it's the hot path most
168525
168765
  // consumers hit first, and Stripe/Anthropic/OpenAI all keep their primary
168526
168766
  // entry point flat for the same reason.
168527
- search = (query, opts = {}) => {
168528
- const { signal, ...rest } = opts;
168529
- return request2(
168767
+ search = async (query, opts = {}) => {
168768
+ const { signal, analytics: _analytics, ...rest } = opts;
168769
+ const response = await request2(
168530
168770
  this,
168531
168771
  {
168532
168772
  method: "POST",
@@ -168536,11 +168776,25 @@ var ZeroClient = class _ZeroClient {
168536
168776
  },
168537
168777
  searchResponseSchema
168538
168778
  );
168779
+ this.analytics.search(query, opts, response);
168780
+ return response;
168539
168781
  };
168540
168782
  // Paid fetch — search → get → fetch is Zero's canonical loop. The hot
168541
168783
  // path is flat on the client, same rationale as `search` above.
168542
168784
  // Orchestration lives in fetch.ts so this class stays readable.
168543
- 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
+ };
168544
168798
  // Poll the API server's /health endpoint until it responds OK. Useful for
168545
168799
  // warming up a cold container before spawning a subprocess that needs the
168546
168800
  // API ready. Resolves when healthy, resolves (silently) when maxAttempts
@@ -168659,16 +168913,6 @@ var tryParseJson2 = (text) => {
168659
168913
  return null;
168660
168914
  }
168661
168915
  };
168662
- var ERROR_MAX = 500;
168663
- var redactUrl2 = (raw) => {
168664
- try {
168665
- const parsed = new URL(raw);
168666
- return `${parsed.origin}${parsed.pathname}`;
168667
- } catch {
168668
- return raw;
168669
- }
168670
- };
168671
- var truncateError = (raw) => raw.length > ERROR_MAX ? `${raw.slice(0, ERROR_MAX)}\u2026` : raw;
168672
168916
  var schemaTypeLabel = (node) => {
168673
168917
  const t = node.type;
168674
168918
  if (typeof t === "string") return t;
@@ -168782,7 +169026,7 @@ var placeholderFor = (fieldName, propSchema) => String(sampleValueFor(fieldName,
168782
169026
 
168783
169027
  // package.json
168784
169028
  var package_default2 = {
168785
- version: "1.26.1"};
169029
+ version: "1.27.0"};
168786
169030
 
168787
169031
  // src/app.ts
168788
169032
  init_esm_shims();
@@ -170494,7 +170738,7 @@ var fetchCommand = (appContext) => new Command("fetch").description(
170494
170738
  const message2 = err instanceof Error ? err.message : "Failed to read request body";
170495
170739
  analyticsService.capture("fetch_error", {
170496
170740
  cliErrorClass: /exceeds the .* byte local cap/.test(message2) ? "payload_too_large" : "schema_validation_failed",
170497
- url: redactUrl2(resolvedUrl),
170741
+ url: redactUrl(resolvedUrl),
170498
170742
  error: truncateError(message2)
170499
170743
  });
170500
170744
  console.error(message2);
@@ -170601,13 +170845,28 @@ var fetchCommand = (appContext) => new Command("fetch").description(
170601
170845
  }
170602
170846
  }
170603
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
+ }
170604
170863
  }
170605
170864
  });
170606
170865
  } catch (err) {
170607
170866
  const message2 = err instanceof Error ? err.message : "Fetch failed";
170608
170867
  analyticsService.capture("fetch_error", {
170609
170868
  cliErrorClass: "network",
170610
- url: redactUrl2(resolvedUrl),
170869
+ url: redactUrl(resolvedUrl),
170611
170870
  error: truncateError(message2)
170612
170871
  });
170613
170872
  console.error(` Fetch failed: ${message2}`);
@@ -170615,7 +170874,6 @@ var fetchCommand = (appContext) => new Command("fetch").description(
170615
170874
  return;
170616
170875
  }
170617
170876
  const {
170618
- fetchId,
170619
170877
  runId,
170620
170878
  status,
170621
170879
  latencyMs,
@@ -170706,7 +170964,7 @@ var fetchCommand = (appContext) => new Command("fetch").description(
170706
170964
  capabilityUid = capabilityResolution.capabilityId ?? capabilityUid;
170707
170965
  capabilitySlug = capabilityResolution.capabilitySlug ?? capabilitySlug;
170708
170966
  analyticsService.capture("capability_resolution_recovered", {
170709
- url: redactUrl2(resolvedUrl),
170967
+ url: redactUrl(resolvedUrl),
170710
170968
  method,
170711
170969
  capabilityUid: capabilityResolution.capabilityId,
170712
170970
  capabilitySlug: capabilityResolution.capabilitySlug,
@@ -170720,42 +170978,11 @@ var fetchCommand = (appContext) => new Command("fetch").description(
170720
170978
  );
170721
170979
  }
170722
170980
  analyticsService.capture("capability_resolution_missed", {
170723
- url: redactUrl2(resolvedUrl),
170981
+ url: redactUrl(resolvedUrl),
170724
170982
  method
170725
170983
  });
170726
170984
  }
170727
170985
  }
170728
- analyticsService.capture("fetch_executed", {
170729
- // The correlation key: joins this client event to the server's
170730
- // run_started / run_completed / payment_processed, and to the run
170731
- // row itself. Works even when runId is null — i.e. exactly when
170732
- // the run failed to record, which is what we most need to trace.
170733
- fetchId,
170734
- url: redactUrl2(resolvedUrl),
170735
- status: status ?? void 0,
170736
- outcome,
170737
- latencyMs,
170738
- hasPayment: !!payment,
170739
- paymentProtocol: payment?.protocol,
170740
- paymentAmount: payment && payment.amount !== "unknown" ? payment.amount : void 0,
170741
- // Canonical join keys — capabilitySlug is the dashboard
170742
- // breakdown dimension; capabilityUid is the stable join
170743
- // key with the capabilities table. capabilityId is the
170744
- // back-compat alias (same value as capabilityUid when
170745
- // resolved, else the raw --capability input).
170746
- capabilityUid: capabilityUid ?? void 0,
170747
- capabilitySlug: capabilitySlug ?? void 0,
170748
- capabilityId: capabilityId ?? void 0,
170749
- searchId: searchId ?? void 0,
170750
- resultRank: resultRank ?? void 0,
170751
- runId: runId ?? void 0,
170752
- runTracked: !!runId,
170753
- signingSource: signing?.source ?? "none",
170754
- // A NULL searchId on a direct_slug fetch is correct here,
170755
- // not a funnel gap to paper over.
170756
- fetchOrigin,
170757
- ...resultError && { error: truncateError(resultError) }
170758
- });
170759
170986
  const outcomeInfo = OUTCOMES[outcome];
170760
170987
  if (outcomeInfo.errorClass !== null) {
170761
170988
  const cliErrorClass = cliErrorClassFor(
@@ -170770,7 +170997,7 @@ var fetchCommand = (appContext) => new Command("fetch").description(
170770
170997
  capabilityId: capabilityId ?? void 0,
170771
170998
  searchId: searchId ?? void 0,
170772
170999
  resultRank: resultRank ?? void 0,
170773
- url: redactUrl2(resolvedUrl),
171000
+ url: redactUrl(resolvedUrl),
170774
171001
  status: typeof status === "number" ? status : void 0,
170775
171002
  upstreamErrorMessage: upstreamError?.message,
170776
171003
  errorSnippetHash: upstreamError?.snippetHash,
@@ -170915,7 +171142,7 @@ var fetchCommand = (appContext) => new Command("fetch").description(
170915
171142
  try {
170916
171143
  appContext.services.analyticsService.capture("fetch_error", {
170917
171144
  cliErrorClass: "unknown",
170918
- url: url4 ? redactUrl2(url4) : "unknown",
171145
+ url: url4 ? redactUrl(url4) : "unknown",
170919
171146
  error: truncateError(message2)
170920
171147
  });
170921
171148
  } catch {
@@ -172519,13 +172746,6 @@ Examples:
172519
172746
 
172520
172747
  // src/commands/search-command.ts
172521
172748
  init_esm_shims();
172522
-
172523
- // src/util/redact.ts
172524
- init_esm_shims();
172525
- var QUERY_MAX = 200;
172526
- var truncateQuery = (raw) => raw.length > QUERY_MAX ? `${raw.slice(0, QUERY_MAX)}\u2026` : raw;
172527
-
172528
- // src/commands/search-command.ts
172529
172749
  var DEFAULT_MAX_COST_USD = "30";
172530
172750
  var formatStatusBadge = (status) => {
172531
172751
  if (!status || !(status in AVAILABILITY_BADGES)) return "";
@@ -172562,7 +172782,7 @@ var searchCommand = (appContext) => new Command("search").description("Search fo
172562
172782
  ).action(
172563
172783
  async (query, options) => {
172564
172784
  try {
172565
- const { analyticsService, client } = appContext.services;
172785
+ const { client } = appContext.services;
172566
172786
  if (options.protocol && !["x402", "mpp"].includes(options.protocol)) {
172567
172787
  console.error(
172568
172788
  `Invalid protocol "${options.protocol}". Must be x402 or mpp.`
@@ -172591,27 +172811,14 @@ var searchCommand = (appContext) => new Command("search").description("Search fo
172591
172811
  availabilityStatus: effectiveStatus,
172592
172812
  includeAll: options.all,
172593
172813
  source: options.source,
172594
- excludeSource: options.excludeSource
172595
- });
172596
- analyticsService.capture("search_executed", {
172597
- query: truncateQuery(query),
172598
- queryLength: query.length,
172599
- resultCount: result.capabilities.length,
172600
- searchId: result.searchId,
172601
- total: result.total,
172602
- hasMore: result.hasMore,
172603
- offset: options.offset,
172604
- limit: options.limit,
172605
- freeOnly: options.free ?? false,
172606
- maxCost: effectiveMaxCost,
172607
- maxCostDefaulted: appliedDefaultMaxCost,
172608
- protocol: options.protocol,
172609
- availabilityStatus: effectiveStatus,
172610
- availabilityStatusDefaulted: appliedDefaultStatus,
172611
- includeAll: options.all ?? false,
172612
- listingSource: options.source,
172613
- excludeListingSource: options.excludeSource,
172614
- 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
+ }
172615
172822
  });
172616
172823
  if (options.json) {
172617
172824
  console.log(
@@ -177486,10 +177693,82 @@ var PostHog = class extends PostHogBackendClient {
177486
177693
  }
177487
177694
  };
177488
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
+
177489
177764
  // src/services/analytics-service.ts
177765
+ var FORWARDED_SDK_EVENTS = /* @__PURE__ */ new Set([
177766
+ "sdk_wallet_resolved"
177767
+ ]);
177490
177768
  var POSTHOG_API_KEY = "phc_B2vLyNxAf2mnqvdPQajf4d4b2iXc35dep2ZrvebMJLuX";
177491
177769
  var POSTHOG_HOST = "https://us.i.posthog.com";
177492
- 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();
177493
177772
  var AnalyticsService = class {
177494
177773
  posthog;
177495
177774
  distinctId;
@@ -177501,7 +177780,7 @@ var AnalyticsService = class {
177501
177780
  constructor(opts) {
177502
177781
  this.cliVersion = opts.cliVersion;
177503
177782
  this.environment = opts.environment;
177504
- this.walletAddress = opts.walletAddress ? toAnalyticsWallet(opts.walletAddress) : null;
177783
+ this.walletAddress = opts.walletAddress ? toAnalyticsWallet2(opts.walletAddress) : null;
177505
177784
  this.requestId = opts.requestId;
177506
177785
  this.agentHost = opts.agentHost;
177507
177786
  let telemetryEnabled = true;
@@ -177524,7 +177803,7 @@ var AnalyticsService = class {
177524
177803
  return;
177525
177804
  }
177526
177805
  if (opts.walletAddress) {
177527
- this.distinctId = toAnalyticsWallet(opts.walletAddress);
177806
+ this.distinctId = toAnalyticsWallet2(opts.walletAddress);
177528
177807
  } else if (persistedAnonId) {
177529
177808
  this.distinctId = persistedAnonId;
177530
177809
  } else {
@@ -177552,7 +177831,7 @@ var AnalyticsService = class {
177552
177831
  originalConsoleError.apply(console, args);
177553
177832
  };
177554
177833
  this.posthog = new PostHog(POSTHOG_API_KEY, {
177555
- host: POSTHOG_HOST,
177834
+ host: resolvePostHogHost(),
177556
177835
  flushAt: 1,
177557
177836
  flushInterval: 0,
177558
177837
  // Vitest spins up many AnalyticsService instances per process; each
@@ -177566,7 +177845,7 @@ var AnalyticsService = class {
177566
177845
  this.maybeAliasAnonToWallet(
177567
177846
  opts.configPath,
177568
177847
  persistedAnonId,
177569
- toAnalyticsWallet(opts.walletAddress)
177848
+ toAnalyticsWallet2(opts.walletAddress)
177570
177849
  );
177571
177850
  }
177572
177851
  }
@@ -177607,7 +177886,7 @@ var AnalyticsService = class {
177607
177886
  // via PostHog alias, when AnalyticsService is constructed with both
177608
177887
  // the persisted anonId and the new walletAddress.
177609
177888
  setWalletAddress(walletAddress) {
177610
- this.walletAddress = toAnalyticsWallet(walletAddress);
177889
+ this.walletAddress = toAnalyticsWallet2(walletAddress);
177611
177890
  }
177612
177891
  capture(event, properties) {
177613
177892
  if (!this.posthog) return;
@@ -177635,6 +177914,42 @@ var AnalyticsService = class {
177635
177914
  }
177636
177915
  });
177637
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
+ }
177638
177953
  captureException(error98, properties) {
177639
177954
  if (!this.posthog) return;
177640
177955
  this.posthog.captureException(error98, this.distinctId, {
@@ -177720,7 +178035,19 @@ var getServices = async (env) => {
177720
178035
  const byoAccount = byoPrivateKey ? privateKeyToAccount(byoPrivateKey) : null;
177721
178036
  const lowBalanceWarning = typeof config3.lowBalanceWarning === "number" ? config3.lowBalanceWarning : 1;
177722
178037
  const onSessionRefreshed = buildOnSessionRefreshed(configPath);
178038
+ let analyticsSink = null;
178039
+ const pendingSdkEvents = [];
177723
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
+ },
177724
178051
  baseUrl: env.ZERO_API_URL,
177725
178052
  // The SDK scopes defaultHeaders to same-origin API calls, so this
177726
178053
  // never reaches capability URLs.
@@ -177761,6 +178088,14 @@ var getServices = async (env) => {
177761
178088
  requestId: randomUUID(),
177762
178089
  agentHost: detectAgentHost()
177763
178090
  });
178091
+ analyticsSink = analyticsService;
178092
+ for (const event of pendingSdkEvents) {
178093
+ try {
178094
+ analyticsService.captureSdk(event);
178095
+ } catch {
178096
+ }
178097
+ }
178098
+ pendingSdkEvents.length = 0;
177764
178099
  return {
177765
178100
  analyticsService,
177766
178101
  client,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeroxyz/cli",
3
- "version": "1.26.1",
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",