zas-agent 0.5.0 → 0.6.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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.6.0] - 2026-09-03
11
+
12
+ ### Added
13
+
14
+ - Usage reporting, so a defect like 0.5.0's — a release that could not
15
+ receive a file at all — is visible without somebody writing in. One report
16
+ after each tool call: the tool, whether it worked, the closed error code
17
+ when it did not, a duration bucket and the version. Never a file name, a
18
+ path, a channel name, a message or a stack. It goes to Zas, not to an
19
+ analytics service: this package holds no analytics token and opens no
20
+ connection to a third party.
21
+ - `zas-agent telemetry [on|off]`, `ZAS_AGENT_TELEMETRY` and `DO_NOT_TRACK`.
22
+ Reporting is on by default; `pair` prints what it collects before it opens
23
+ a browser, the first `serve` on a machine prints it once, and `zas_status`
24
+ always says which way it is set. The choice lives in
25
+ `~/.zas/agent/settings.json` and survives pairing again.
26
+
27
+ ## [0.5.1] - 2026-09-03
28
+
29
+ ### Fixed
30
+
31
+ - `zas_receive_direct` could not connect. The WebRTC engine in Node reports
32
+ an empty description object where a browser reports none, and the
33
+ receiver read that as a stale connection restart, so it dropped the very
34
+ first offer: it never answered, and both ends waited out the thirty
35
+ second clock. 0.5.0 could not receive a file at all. It can now.
36
+
10
37
  ## [0.5.0] - 2026-09-03
11
38
 
12
39
  ### Added
package/README.md CHANGED
@@ -252,6 +252,36 @@ Two things worth knowing before you point a model at your account:
252
252
  Everything those tools touch lands inside your own account and your own
253
253
  machine. Revoking the agent stops all of them.
254
254
 
255
+ ## What it sends back
256
+
257
+ This package reports how it is used, so defects like a release that could not
258
+ receive a file at all stop being invisible. It is on by default and it is
259
+ yours to turn off:
260
+
261
+ ```bash
262
+ npx -y zas-agent telemetry off # and `on` again, and `telemetry` to see
263
+ ```
264
+
265
+ `ZAS_AGENT_TELEMETRY=0` and the cross-vendor `DO_NOT_TRACK=1` do the same
266
+ without writing anything. The choice lives in `~/.zas/agent/settings.json` and
267
+ covers every profile on the machine; pairing again does not reset it.
268
+ `zas_status` always says which way it is set.
269
+
270
+ What one report carries, and nothing else: the tool that ran, whether it
271
+ worked, the closed error code when it did not, which of four duration buckets
272
+ it fell in, and the package version. No file name, no file contents, no path,
273
+ no channel name, no error message, no stack.
274
+
275
+ Where it goes: to Zas, never to an analytics service directly. This package
276
+ holds no analytics token and opens no connection to a third party, so no
277
+ address of yours reaches one. The report is attributed to the Zas account the
278
+ agent is paired with, which the server reads from the session — the request
279
+ carries no identifier of its own. Nothing is reported before pairing, because
280
+ until then there is no account it could belong to.
281
+
282
+ A report is never allowed to matter: it is sent after the answer, waited on
283
+ for at most five seconds, tried once, and dropped in silence if it fails.
284
+
255
285
  ## Data on disk
256
286
 
257
287
  One directory per profile, so one machine can hold a Claude Code agent and a
@@ -262,8 +292,8 @@ Codex agent side by side without either reading the other's keys:
262
292
  | macOS, Linux | `~/.zas/agent/<profile>/` |
263
293
  | Windows | `%USERPROFILE%\.zas\agent\<profile>\` |
264
294
 
265
- Four files, all written through a temporary file and renamed into place, so a
266
- crash mid-write cannot leave half a file behind:
295
+ Four files in the profile directory, all written through a temporary file and
296
+ renamed into place, so a crash mid-write cannot leave half a file behind:
267
297
 
268
298
  - `identity.json` — the agent uid, the owner uid, the name, and the two key
269
299
  pairs. Back it up like a private key, or delete it and pair again.
@@ -276,6 +306,12 @@ crash mid-write cannot leave half a file behind:
276
306
  ten minutes, so a retried tool call answers without touching the network. It
277
307
  stores hashes, never a title or a note's first line. Disposable.
278
308
 
309
+ One more file sits next to the profiles, in `~/.zas/agent/`:
310
+
311
+ - `settings.json` — whether this machine reports usage, and when the notice
312
+ above was last printed. It belongs to the machine, not to a profile, so one
313
+ decision covers every agent on it.
314
+
279
315
  On macOS and Linux the directory is created `0700` and every file `0600`. On
280
316
  Windows those bits have no effect: the files carry the permissions of the user
281
317
  profile they live in, and the package does not try to set any others.
@@ -306,6 +342,8 @@ agent: same keys, same channels, one row.
306
342
  | --- | --- | --- |
307
343
  | `--profile <name>` | `claude-code` | Which identity directory this process uses. Letters, digits, `.`, `_` and `-`, up to 64, and it may not start with a dot. |
308
344
  | `ZAS_AGENT_HOME` | `~/.zas/agent` | Where the profile directories live. |
345
+ | `ZAS_AGENT_TELEMETRY` | unset | `0` turns usage reporting off for this process, `1` on. It outranks the stored choice. |
346
+ | `DO_NOT_TRACK` | unset | `1` turns usage reporting off. Read only to turn it off. |
309
347
  | `ZAS_WEB_BASE` | `https://zas.red` | The web app the pairing URL points at. |
310
348
  | `ZAS_API_BASE` | `https://zas.red/api` | The API. |
311
349
  | `ZAS_TOKEN_BASE` | `https://zas.red/anon-token` | The challenge and token routes. |
@@ -313,7 +351,8 @@ agent: same keys, same channels, one row.
313
351
  | `ZAS_FIREBASE_PROJECT` | `zas-me` | The project whose Firestore the read path queries. |
314
352
  | `ZAS_FIREBASE_API_KEY` | the public web key | The key used to exchange a custom token for a session. |
315
353
 
316
- Only `--profile` and `ZAS_AGENT_HOME` are worth setting by hand. The rest exist
354
+ Only `--profile`, `ZAS_AGENT_HOME` and the two telemetry switches are worth
355
+ setting by hand. The rest exist
317
356
  so the package can be pointed at a test deployment.
318
357
 
319
358
  ## Development
package/dist/cli.js CHANGED
@@ -211,6 +211,7 @@ var IDENTITY_FILE = "identity.json";
211
211
  var PENDING_FILE = "pending.json";
212
212
  var GRANTS_FILE = "grants.json";
213
213
  var FINGERPRINTS_FILE = "fingerprints.json";
214
+ var SETTINGS_FILE = "settings.json";
214
215
  function agentHome() {
215
216
  return process.env.ZAS_AGENT_HOME || join(homedir(), ".zas", "agent");
216
217
  }
@@ -300,6 +301,13 @@ function loadFingerprints(profile) {
300
301
  function saveFingerprints(profile, cache) {
301
302
  writePrivate(profileDir(profile), FINGERPRINTS_FILE, cache);
302
303
  }
304
+ function loadSettings() {
305
+ const stored = readCache(agentHome(), SETTINGS_FILE);
306
+ return isPlainObject(stored) ? stored : null;
307
+ }
308
+ function saveSettings(settings) {
309
+ writePrivate(agentHome(), SETTINGS_FILE, settings);
310
+ }
303
311
  function defaultEndpoints() {
304
312
  return {
305
313
  api_base: process.env.ZAS_API_BASE || "https://zas.red/api",
@@ -1048,6 +1056,9 @@ function pathOf(pair) {
1048
1056
  if (pair.local === "relay" || pair.remote === "relay") return "relay";
1049
1057
  return pair.local === "host" && pair.remote === "host" ? "lan" : "wan";
1050
1058
  }
1059
+ function hasRemoteDescription(pc) {
1060
+ return !!pc.remoteDescription?.sdp;
1061
+ }
1051
1062
  function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
1052
1063
  const initialIce = ice ?? DIRECT_ICE;
1053
1064
  const pc = new RTCPeerConnection({
@@ -1092,7 +1103,7 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
1092
1103
  diag.ms = Date.now() - startedAt;
1093
1104
  diag.iceState = String(pc.iceConnectionState ?? "");
1094
1105
  diag.gatherState = String(pc.iceGatheringState ?? "");
1095
- diag.hadRemoteDesc = !!pc.remoteDescription;
1106
+ diag.hadRemoteDesc = hasRemoteDescription(pc);
1096
1107
  diag.turnUrlsConfigured = countTurnUrls(pc.getConfiguration().iceServers);
1097
1108
  onDiag?.(diag);
1098
1109
  onPhase(p);
@@ -1481,8 +1492,8 @@ function startReceiver(opts) {
1481
1492
  accept: (msg) => {
1482
1493
  if (s.isTerminal()) return;
1483
1494
  s.signaled();
1484
- if (msg.kind === "offer" && (!pc.remoteDescription || pc.signalingState === "stable")) {
1485
- const isRestart = !!pc.remoteDescription;
1495
+ if (msg.kind === "offer" && (!hasRemoteDescription(pc) || pc.signalingState === "stable")) {
1496
+ const isRestart = hasRemoteDescription(pc);
1486
1497
  const offerGeneration = msg.generation ?? (isRestart ? generation + 1 : 1);
1487
1498
  if (isRestart && msg.generation !== void 0 && offerGeneration <= generation) return;
1488
1499
  generation = offerGeneration;
@@ -1633,15 +1644,15 @@ async function uploadFallback(options) {
1633
1644
  const key = await aesKey(meta);
1634
1645
  const completed = [];
1635
1646
  const partProgress = /* @__PURE__ */ new Map();
1636
- const report = () => options.onProgress?.(
1647
+ const report2 = () => options.onProgress?.(
1637
1648
  Math.min(meta.cipher_size, [...partProgress.values()].reduce((sum, value) => sum + value, 0)),
1638
1649
  meta.cipher_size
1639
1650
  );
1640
1651
  for (let first = 1; first <= meta.part_count; first += URL_BATCH) {
1641
1652
  aborted(signal);
1642
- const count = Math.min(URL_BATCH, meta.part_count - first + 1);
1643
- const signed = await options.getUrls(first, count);
1644
- if (signed.length !== count || signed.some((entry, i) => entry.part_number !== first + i || typeof entry.url !== "string")) throw new Error("bad_fallback_urls");
1653
+ const count2 = Math.min(URL_BATCH, meta.part_count - first + 1);
1654
+ const signed = await options.getUrls(first, count2);
1655
+ if (signed.length !== count2 || signed.some((entry, i) => entry.part_number !== first + i || typeof entry.url !== "string")) throw new Error("bad_fallback_urls");
1645
1656
  let cursor = 0;
1646
1657
  const workers = Array.from({ length: Math.min(CONCURRENCY, signed.length) }, async () => {
1647
1658
  for (; ; ) {
@@ -1658,11 +1669,11 @@ async function uploadFallback(options) {
1658
1669
  for (let attempt = 0; attempt < RETRIES; attempt++) {
1659
1670
  aborted(signal);
1660
1671
  partProgress.set(partNumber, 0);
1661
- report();
1672
+ report2();
1662
1673
  try {
1663
1674
  etag = await options.put(entry.url, cipher, signal, (loaded) => {
1664
1675
  partProgress.set(partNumber, loaded);
1665
- report();
1676
+ report2();
1666
1677
  });
1667
1678
  break;
1668
1679
  } catch (error) {
@@ -2469,7 +2480,7 @@ function sealer(key, keyVersion) {
2469
2480
  open: (enc) => JSON.parse(new TextDecoder().decode(openRaw(key, b64ToBytes(enc))))
2470
2481
  };
2471
2482
  }
2472
- async function sendDirect(ctx, input, report, deps = {}) {
2483
+ async function sendDirect(ctx, input, report2, deps = {}) {
2473
2484
  const now = deps.now ?? (() => Date.now());
2474
2485
  const sleep2 = deps.sleep ?? defaultSleep2;
2475
2486
  const grant = await directGrantFor(ctx, input.channel);
@@ -2486,7 +2497,7 @@ async function sendDirect(ctx, input, report, deps = {}) {
2486
2497
  const { seal, open } = sealer(key, grant.key_version);
2487
2498
  const stamp = { device, owner_uid: owner };
2488
2499
  const startedAt = now();
2489
- report("offer");
2500
+ report2("offer");
2490
2501
  const meta = { name, size: file.size, mime: mimeFor(input.path) };
2491
2502
  const { id } = await ctx.client.api("POST", `/direct/${cid}`, {
2492
2503
  meta_enc: seal(meta),
@@ -2520,7 +2531,7 @@ async function sendDirect(ctx, input, report, deps = {}) {
2520
2531
  }
2521
2532
  };
2522
2533
  const turn = await fetchIce();
2523
- report("connecting");
2534
+ report2("connecting");
2524
2535
  let resolveOutcome;
2525
2536
  const outcome = new Promise((resolve2) => {
2526
2537
  resolveOutcome = resolve2;
@@ -2537,7 +2548,7 @@ async function sendDirect(ctx, input, report, deps = {}) {
2537
2548
  send: (msg) => ctx.client.api("POST", `${route}/signal`, { payload_enc: seal(msg), for: "receiver", ...stamp }).then(() => void 0),
2538
2549
  onPhase: (phase2) => {
2539
2550
  if (phase2 === "flight") {
2540
- report("flight");
2551
+ report2("flight");
2541
2552
  heartbeat ??= setInterval(() => {
2542
2553
  void ctx.client.api("POST", `${route}/heartbeat`, stamp).catch(() => void 0);
2543
2554
  }, deps.heartbeatMs ?? HEARTBEAT_MS);
@@ -2577,7 +2588,7 @@ async function sendDirect(ctx, input, report, deps = {}) {
2577
2588
  over = true;
2578
2589
  clearInterval(heartbeat);
2579
2590
  await pump;
2580
- report("finishing");
2591
+ report2("finishing");
2581
2592
  if (phase === "done") {
2582
2593
  await setState("done");
2583
2594
  return {
@@ -2616,7 +2627,7 @@ var fetchPut = async (url, bytes, signal, onLoaded) => {
2616
2627
  onLoaded(bytes.length);
2617
2628
  return etag;
2618
2629
  };
2619
- async function sendDirectFallback(ctx, record, report, deps = {}) {
2630
+ async function sendDirectFallback(ctx, record, report2, deps = {}) {
2620
2631
  const now = deps.now ?? (() => Date.now());
2621
2632
  const startedAt = now();
2622
2633
  const opened = await (deps.openFile ?? openAsBlob)(record.path).catch(() => {
@@ -2627,7 +2638,7 @@ async function sendDirectFallback(ctx, record, report, deps = {}) {
2627
2638
  const { seal } = sealer(record.key, record.key_version);
2628
2639
  const stamp = { device: record.device, owner_uid: record.owner_uid };
2629
2640
  const base = `/direct/${record.channel_id}/${record.offer_id}/fallback`;
2630
- report("encrypting");
2641
+ report2("encrypting");
2631
2642
  const meta = createFallbackMeta(file);
2632
2643
  await ctx.client.api("POST", `${base}/start`, {
2633
2644
  meta_enc: seal(meta),
@@ -2636,7 +2647,7 @@ async function sendDirectFallback(ctx, record, report, deps = {}) {
2636
2647
  part_count: meta.part_count,
2637
2648
  ...stamp
2638
2649
  });
2639
- report("uploading");
2650
+ report2("uploading");
2640
2651
  let parts;
2641
2652
  try {
2642
2653
  parts = await uploadFallback({
@@ -2644,11 +2655,11 @@ async function sendDirectFallback(ctx, record, report, deps = {}) {
2644
2655
  offerId: record.offer_id,
2645
2656
  meta,
2646
2657
  put: deps.put ?? fetchPut,
2647
- getUrls: async (from, count) => {
2658
+ getUrls: async (from, count2) => {
2648
2659
  const r = await ctx.client.api(
2649
2660
  "POST",
2650
2661
  `${base}/parts`,
2651
- { from, count, ...stamp }
2662
+ { from, count: count2, ...stamp }
2652
2663
  );
2653
2664
  return r.urls;
2654
2665
  }
@@ -2658,7 +2669,7 @@ async function sendDirectFallback(ctx, record, report, deps = {}) {
2658
2669
  if (error instanceof ZasError) throw error;
2659
2670
  throw new ZasError("upload_failed", 0, error instanceof Error ? error.message : String(error));
2660
2671
  }
2661
- report("finishing");
2672
+ report2("finishing");
2662
2673
  await ctx.client.api("POST", `${base}/complete`, {
2663
2674
  parts: parts.map((part) => ({ part_number: part.partNumber, etag: part.etag })),
2664
2675
  ...stamp
@@ -3044,7 +3055,7 @@ var DiskSink = class {
3044
3055
  return error;
3045
3056
  }
3046
3057
  };
3047
- async function receiveDirect(ctx, input, report, deps = {}) {
3058
+ async function receiveDirect(ctx, input, report2, deps = {}) {
3048
3059
  const now = deps.now ?? (() => Date.now());
3049
3060
  const sleep2 = deps.sleep ?? defaultSleep2;
3050
3061
  const grant = await receiveGrantFor(ctx, input.channel);
@@ -3057,7 +3068,7 @@ async function receiveDirect(ctx, input, report, deps = {}) {
3057
3068
  const stamp = { device, owner_uid: owner };
3058
3069
  const startedAt = now();
3059
3070
  const channelPath = `accounts/${owner}/channels/${cid}`;
3060
- report("waiting");
3071
+ report2("waiting");
3061
3072
  const waitUntil = startedAt + (deps.offerWaitMs ?? OFFER_WAIT_MS2);
3062
3073
  let id;
3063
3074
  let meta;
@@ -3109,7 +3120,7 @@ async function receiveDirect(ctx, input, report, deps = {}) {
3109
3120
  }
3110
3121
  };
3111
3122
  const turn = await fetchIce();
3112
- report("connecting");
3123
+ report2("connecting");
3113
3124
  const sink = new DiskSink(input.dest, offerId);
3114
3125
  let resolveOutcome;
3115
3126
  const outcome = new Promise((resolve2) => {
@@ -3125,7 +3136,7 @@ async function receiveDirect(ctx, input, report, deps = {}) {
3125
3136
  expectedMeta: offered,
3126
3137
  send: (msg) => ctx.client.api("POST", `${route}/signal`, { payload_enc: seal(msg), for: "sender", ...stamp }).then(() => void 0),
3127
3138
  onPhase: (phase2) => {
3128
- if (phase2 === "flight") report("flight");
3139
+ if (phase2 === "flight") report2("flight");
3129
3140
  if (phase2 === "done" || phase2 === "failed") resolveOutcome(phase2);
3130
3141
  },
3131
3142
  onPath: (p) => {
@@ -3164,7 +3175,7 @@ async function receiveDirect(ctx, input, report, deps = {}) {
3164
3175
  const phase = await outcome;
3165
3176
  over = true;
3166
3177
  await pump;
3167
- report("finishing");
3178
+ report2("finishing");
3168
3179
  if (phase === "done") {
3169
3180
  await setState("done");
3170
3181
  return {
@@ -3199,7 +3210,7 @@ async function receiveDirect(ctx, input, report, deps = {}) {
3199
3210
  });
3200
3211
  throw new ZasError("direct_failed", 0, reason);
3201
3212
  }
3202
- async function receiveDirectFallback(ctx, record, report, deps = {}) {
3213
+ async function receiveDirectFallback(ctx, record, report2, deps = {}) {
3203
3214
  const now = deps.now ?? (() => Date.now());
3204
3215
  const startedAt = now();
3205
3216
  const { open } = sealer(record.key, record.key_version);
@@ -3221,7 +3232,7 @@ async function receiveDirectFallback(ctx, record, report, deps = {}) {
3221
3232
  throw new ZasError("file_changed", 0);
3222
3233
  }
3223
3234
  const sink = new DiskSink(record.dest, record.offer_id);
3224
- report("downloading");
3235
+ report2("downloading");
3225
3236
  try {
3226
3237
  sink.open(meta);
3227
3238
  await downloadFallback({
@@ -3237,7 +3248,7 @@ async function receiveDirectFallback(ctx, record, report, deps = {}) {
3237
3248
  if (error instanceof ZasError) throw error;
3238
3249
  throw new ZasError("network", 0, error instanceof Error ? error.message : String(error));
3239
3250
  }
3240
- report("finishing");
3251
+ report2("finishing");
3241
3252
  await ctx.client.api("POST", `${base}/received`, stamp);
3242
3253
  return {
3243
3254
  offer_id: record.offer_id,
@@ -3278,10 +3289,10 @@ var JobRunner = class {
3278
3289
  };
3279
3290
  this.jobs.unshift(job);
3280
3291
  this.jobs.length = Math.min(this.jobs.length, HISTORY);
3281
- const report = (phase) => {
3292
+ const report2 = (phase) => {
3282
3293
  if (job.status === "running") job.phase = phase;
3283
3294
  };
3284
- this.settled.set(job, work(report).then(
3295
+ this.settled.set(job, work(report2).then(
3285
3296
  (result) => {
3286
3297
  job.status = "done";
3287
3298
  job.result = result;
@@ -3325,9 +3336,477 @@ var JobRunner = class {
3325
3336
  }
3326
3337
  };
3327
3338
 
3339
+ // src/shared/product-analytics.ts
3340
+ var PRODUCT_EVENTS = [
3341
+ "account_created",
3342
+ "item.created",
3343
+ "item.downloaded",
3344
+ "item.copied",
3345
+ "item.shared",
3346
+ "item.pinned",
3347
+ "item.unpinned",
3348
+ "item.deleted",
3349
+ "item.departed",
3350
+ "channel_shared",
3351
+ "channel_mode_set",
3352
+ "invite_created",
3353
+ "invite_link_copied",
3354
+ "invite_qr_shown",
3355
+ "invite_system_sheet_opened",
3356
+ "invite_joined",
3357
+ "invite_converted",
3358
+ "channel.invite_preview",
3359
+ "channel.invite_accept",
3360
+ "channel.invite_signin",
3361
+ "enterprise.invite_preview",
3362
+ "enterprise.invite_identity",
3363
+ "enterprise.invite_signin",
3364
+ "enterprise.invite_account_switch",
3365
+ "enterprise.invite_attach",
3366
+ "enterprise.invite_accept",
3367
+ "enterprise.invite_admin",
3368
+ "enterprise.workspace_opened",
3369
+ "enterprise.workspace_switched",
3370
+ "enterprise.channel_created",
3371
+ "enterprise.channel_members_added",
3372
+ "share_link_copied",
3373
+ "share_qr_shown",
3374
+ "share_system_sheet_opened",
3375
+ "share_opened",
3376
+ "share_content_copied",
3377
+ "share_downloaded",
3378
+ "share_signin_click",
3379
+ "share.saved",
3380
+ "share_bar_on",
3381
+ "share_bar_off",
3382
+ "upload.phase_timing",
3383
+ "auth.signin_started",
3384
+ "auth.signin_result",
3385
+ "auth.session_started",
3386
+ "auth.signed_out",
3387
+ "auth.session_lost",
3388
+ "auth.token_refresh_failed",
3389
+ "account_switch.started",
3390
+ "account_switch.completed",
3391
+ "account_switch.failed",
3392
+ "bootstrap.result",
3393
+ "direct_mode_set",
3394
+ "direct.offered",
3395
+ "direct.claimed",
3396
+ "direct.connected",
3397
+ "direct.completed",
3398
+ "direct.received",
3399
+ "direct.failed",
3400
+ "direct.fallback_started",
3401
+ "direct.fallback_ready",
3402
+ "direct.fallback_received",
3403
+ "direct.fallback_failed",
3404
+ "direct.fallback_aborted",
3405
+ "direct.too_big",
3406
+ "agent.pair_opened",
3407
+ "agent.pair_approved",
3408
+ "agent.pair_refused",
3409
+ "agent.paired",
3410
+ "agent.revoked",
3411
+ "agent.send",
3412
+ "agent.tool_call"
3413
+ ];
3414
+ var SERVER_OWNED_PRODUCT_EVENTS = [
3415
+ "account_created",
3416
+ "item.departed",
3417
+ "invite_created",
3418
+ "invite_joined",
3419
+ "invite_converted",
3420
+ "share_opened",
3421
+ "share_content_copied",
3422
+ "share_downloaded",
3423
+ "share_signin_click",
3424
+ "agent.send",
3425
+ "agent.tool_call"
3426
+ ];
3427
+ var SERVER_OWNED_PRODUCT_EVENT_SET = new Set(SERVER_OWNED_PRODUCT_EVENTS);
3428
+ var CLIENT_PRODUCT_EVENTS = PRODUCT_EVENTS.filter(
3429
+ (event) => !SERVER_OWNED_PRODUCT_EVENT_SET.has(event)
3430
+ );
3431
+ var bool = { type: "boolean" };
3432
+ var count = { type: "number", min: 0 };
3433
+ var oneOf = (...values) => ({ type: "string", values: new Set(values) });
3434
+ var sizeBucket = oneOf("lt_1mb", "1_10mb", "10_100mb", "100_512mb", "512mb_5gb", "gt_5gb");
3435
+ var itemKind = oneOf("file", "text");
3436
+ var itemKindOrUnknown = oneOf("file", "text", "unknown");
3437
+ var role = oneOf("sender", "receiver");
3438
+ var candidateType = oneOf("", "host", "srflx", "relay", "prflx", "other");
3439
+ var enterpriseRole = oneOf("owner", "admin", "billing_admin", "member", "guest");
3440
+ var agentKind = oneOf("claude_code", "codex", "other");
3441
+ var pairHandoff = oneOf("loopback", "manual", "unknown");
3442
+ var AGENT_TOOL_NAMES = [
3443
+ "status",
3444
+ "pair",
3445
+ "send_file",
3446
+ "send_note",
3447
+ "send_direct",
3448
+ "send_direct_fallback",
3449
+ "receive_direct",
3450
+ "receive_direct_fallback",
3451
+ "list_items",
3452
+ "get_item",
3453
+ "jobs"
3454
+ ];
3455
+ var AGENT_ERROR_CODES = [
3456
+ "none",
3457
+ "other",
3458
+ "identity_corrupt",
3459
+ "not_paired",
3460
+ "internal",
3461
+ "agent_forbidden",
3462
+ "agent_revoked",
3463
+ "grant_missing",
3464
+ "not_found",
3465
+ "invalid_cap",
3466
+ "write_failed",
3467
+ "send_forbidden",
3468
+ "read_forbidden",
3469
+ "direct_mode",
3470
+ "not_direct_mode",
3471
+ "not_claimed",
3472
+ "no_offer",
3473
+ "offer_taken",
3474
+ "direct_cancelled",
3475
+ "direct_failed",
3476
+ "direct_not_failed",
3477
+ "file_changed",
3478
+ "webrtc_unavailable",
3479
+ "fallback_unavailable",
3480
+ "key_stale",
3481
+ "quota_exceeded",
3482
+ "rate_limited",
3483
+ "file_too_big",
3484
+ "duplicate",
3485
+ "pairing_expired",
3486
+ "pairing_cancelled",
3487
+ "claim_mismatch",
3488
+ "pairing_not_approved",
3489
+ "pairing_claimed",
3490
+ "agent_limit",
3491
+ "grant_limit",
3492
+ "feature_disabled",
3493
+ "upload_failed",
3494
+ "oprf_failed",
3495
+ "network",
3496
+ "sign_in_failed",
3497
+ "bad_signature",
3498
+ "missing_token"
3499
+ ];
3500
+ function agentDurationBucket(ms) {
3501
+ if (ms < 1e3) return "lt_1s";
3502
+ if (ms < 1e4) return "1_10s";
3503
+ if (ms < 6e4) return "10_60s";
3504
+ return "gt_60s";
3505
+ }
3506
+ var EVENT_PROPERTY_RULES = {
3507
+ account_created: {},
3508
+ "item.created": { kind: itemKind, size_bytes: count, size_bucket: sizeBucket },
3509
+ "item.downloaded": { size_bytes: count, size_bucket: sizeBucket },
3510
+ "item.copied": {},
3511
+ "item.shared": { kind: itemKind },
3512
+ "item.pinned": { age_ms: count },
3513
+ "item.unpinned": {},
3514
+ "item.deleted": { age_ms: count, was_pinned: bool, kind: itemKindOrUnknown },
3515
+ "item.departed": {},
3516
+ channel_shared: {},
3517
+ channel_mode_set: { mode: oneOf("edit", "view") },
3518
+ invite_created: {},
3519
+ invite_link_copied: {},
3520
+ invite_qr_shown: {},
3521
+ invite_system_sheet_opened: {},
3522
+ invite_joined: {},
3523
+ invite_converted: {},
3524
+ "channel.invite_preview": {
3525
+ outcome: oneOf("ready", "unavailable", "missing_key"),
3526
+ signed_in: bool
3527
+ },
3528
+ "channel.invite_accept": {
3529
+ outcome: oneOf("started", "completed", "already_member", "unavailable", "failed")
3530
+ },
3531
+ "channel.invite_signin": {
3532
+ stage: oneOf("link_requested", "link_sent", "link_completed", "failed")
3533
+ },
3534
+ "enterprise.invite_preview": {
3535
+ outcome: oneOf("pending", "resumable", "unavailable"),
3536
+ role: enterpriseRole
3537
+ },
3538
+ "enterprise.invite_identity": {
3539
+ outcome: oneOf("matched", "mismatched", "unavailable", "failed")
3540
+ },
3541
+ "enterprise.invite_signin": {
3542
+ stage: oneOf("link_requested", "link_sent", "link_completed", "failed")
3543
+ },
3544
+ "enterprise.invite_account_switch": { stage: oneOf("started", "completed", "failed") },
3545
+ "enterprise.invite_attach": {
3546
+ stage: oneOf("link_requested", "link_sent", "verify_started", "completed", "address_taken", "link_invalid", "failed")
3547
+ },
3548
+ "enterprise.invite_accept": {
3549
+ outcome: oneOf("started", "completed", "recovered", "timeout", "unavailable", "email_mismatch", "seat_limit", "failed"),
3550
+ role: enterpriseRole,
3551
+ destination: oneOf("workspace", "admin_console"),
3552
+ duration_ms: count
3553
+ },
3554
+ "enterprise.invite_admin": {
3555
+ outcome: oneOf("created", "email_sent", "email_failed", "reissued", "revoked"),
3556
+ role: enterpriseRole,
3557
+ external: bool
3558
+ },
3559
+ "enterprise.workspace_opened": { role: enterpriseRole, first_visit: bool },
3560
+ "enterprise.workspace_switched": { destination: oneOf("personal", "organization"), role: enterpriseRole },
3561
+ "enterprise.channel_created": { member_count: count },
3562
+ "enterprise.channel_members_added": { member_count: count },
3563
+ share_link_copied: {},
3564
+ share_qr_shown: {},
3565
+ share_system_sheet_opened: {},
3566
+ share_opened: {},
3567
+ share_content_copied: {},
3568
+ share_downloaded: {},
3569
+ share_signin_click: {},
3570
+ "share.saved": {},
3571
+ share_bar_on: {},
3572
+ share_bar_off: {},
3573
+ "upload.phase_timing": {
3574
+ stage: oneOf("staging", "bootstrap", "thumbnail", "encrypting", "uploading", "finishing"),
3575
+ duration_ms: count,
3576
+ size_bucket: sizeBucket,
3577
+ outcome: oneOf("success", "failure", "cancelled"),
3578
+ attempt: { type: "number", min: 1 }
3579
+ },
3580
+ "auth.signin_started": { provider: oneOf("google.com", "apple.com") },
3581
+ "auth.signin_result": {
3582
+ provider: oneOf("google.com", "apple.com"),
3583
+ outcome: oneOf("signed_in", "cancelled", "taken", "failed", "needs_link"),
3584
+ flow: oneOf("signin", "link", "connect")
3585
+ },
3586
+ "account_switch.started": { operation: oneOf("remember", "switch"), provider: oneOf("google.com", "apple.com", "email_link", "qr", "code") },
3587
+ "account_switch.completed": { operation: oneOf("remember", "switch"), provider: oneOf("google.com", "apple.com", "email_link", "qr", "code") },
3588
+ "account_switch.failed": { operation: oneOf("remember", "switch"), provider: oneOf("google.com", "apple.com", "email_link", "qr", "code") },
3589
+ "auth.session_started": { cold: bool },
3590
+ "auth.signed_out": { reason: oneOf("user", "account_switch", "abandoned_new_account", "account_deleted") },
3591
+ "auth.session_lost": { session_age_ms: count, foreground: bool },
3592
+ "auth.token_refresh_failed": {
3593
+ code: oneOf("network", "token_invalid", "user_disabled", "user_missing", "rate_limited", "internal", "unknown")
3594
+ },
3595
+ "bootstrap.result": {
3596
+ outcome: oneOf("offline", "ok", "timeout_warm", "timeout", "failed_warm", "failed"),
3597
+ stage: oneOf("cached", "done", "unknown", "keyring", "account_init", "listeners"),
3598
+ duration_ms: count,
3599
+ fault: oneOf("intercepted", "offline", "unknown")
3600
+ },
3601
+ direct_mode_set: { on: bool },
3602
+ "direct.offered": { size_bucket: sizeBucket },
3603
+ // `via_agent`: the offer came from a paired agent's process, not from one
3604
+ // of the person's devices. The claim and the two receiver outcomes carry it.
3605
+ "direct.claimed": { size_bucket: sizeBucket, via_agent: bool },
3606
+ "direct.connected": { role, ms: count },
3607
+ "direct.completed": {
3608
+ via: oneOf("lan", "wan", "relay", "unknown"),
3609
+ pair_local: candidateType,
3610
+ pair_remote: candidateType,
3611
+ connect_ms: count,
3612
+ duration_ms: count,
3613
+ size_bucket: sizeBucket,
3614
+ turn_urls_supplied: count,
3615
+ turn_urls_configured: count,
3616
+ restarts: count
3617
+ },
3618
+ "direct.received": {
3619
+ via_agent: bool,
3620
+ via: oneOf("lan", "wan", "relay", "unknown"),
3621
+ pair_local: candidateType,
3622
+ pair_remote: candidateType,
3623
+ connect_ms: count,
3624
+ duration_ms: count,
3625
+ size_bucket: sizeBucket,
3626
+ turn_urls_supplied: count,
3627
+ turn_urls_configured: count,
3628
+ restarts: count
3629
+ },
3630
+ "direct.failed": {
3631
+ role,
3632
+ via_agent: bool,
3633
+ reason: oneOf(
3634
+ "peer_silent",
3635
+ "connect_timeout",
3636
+ "ice_failed",
3637
+ "disconnected",
3638
+ "peer_closed",
3639
+ "peer_abort",
3640
+ "signaling",
3641
+ "signal_send",
3642
+ "protocol",
3643
+ "sink",
3644
+ "transfer",
3645
+ "integrity",
3646
+ "too_big",
3647
+ "unknown"
3648
+ ),
3649
+ ms: count,
3650
+ ice_state: oneOf("", "new", "checking", "connected", "completed", "failed", "disconnected", "closed"),
3651
+ gather_state: oneOf("", "new", "gathering", "complete"),
3652
+ had_remote: bool,
3653
+ local_host: count,
3654
+ local_srflx: count,
3655
+ local_relay: count,
3656
+ remote_host: count,
3657
+ remote_srflx: count,
3658
+ remote_relay: count,
3659
+ turn_urls_supplied: count,
3660
+ turn_urls_configured: count,
3661
+ moved_bucket: sizeBucket,
3662
+ size_bucket: sizeBucket,
3663
+ restarts: count,
3664
+ // Whether the reporting tab was on screen when the run died — the lead a
3665
+ // slept machine or a frozen background tab leaves behind.
3666
+ visibility: oneOf("visible", "hidden", "prerender")
3667
+ },
3668
+ "direct.fallback_started": { size_bucket: sizeBucket },
3669
+ "direct.fallback_ready": { size_bucket: sizeBucket, duration_ms: count, retries: count },
3670
+ "direct.fallback_received": { size_bucket: sizeBucket, duration_ms: count, retries: count },
3671
+ "direct.fallback_failed": {
3672
+ stage: oneOf("upload", "download"),
3673
+ size_bucket: sizeBucket,
3674
+ retries: count
3675
+ },
3676
+ "direct.fallback_aborted": {},
3677
+ "direct.too_big": { size_bucket: sizeBucket },
3678
+ "agent.pair_opened": { agent_kind: agentKind, protocol: count, replaces: bool },
3679
+ "agent.pair_approved": {
3680
+ agent_kind: agentKind,
3681
+ destination: oneOf("new_channel", "existing"),
3682
+ channels: count,
3683
+ handoff: pairHandoff
3684
+ },
3685
+ "agent.pair_refused": {
3686
+ agent_kind: agentKind,
3687
+ step: oneOf("open", "approve", "claim"),
3688
+ reason: oneOf(
3689
+ "agent_limit",
3690
+ "grant_limit",
3691
+ "channel_limit",
3692
+ "code_mismatch",
3693
+ "pairing_expired",
3694
+ "pairing_cancelled",
3695
+ "pairing_claimed",
3696
+ "feature_disabled",
3697
+ "rate_limited",
3698
+ "network",
3699
+ "other"
3700
+ )
3701
+ },
3702
+ "agent.paired": {
3703
+ agent_kind: agentKind,
3704
+ destination: oneOf("new_channel", "existing"),
3705
+ channels: count,
3706
+ handoff: pairHandoff,
3707
+ protocol: count
3708
+ },
3709
+ "agent.revoked": { agent_kind: agentKind },
3710
+ "agent.send": { size_bucket: sizeBucket, agent_kind: agentKind },
3711
+ // What the CLI reports after every tool call: the tool, whether it worked,
3712
+ // the code when it did not, and the version that answered. No path, no file
3713
+ // name, no channel, no sentence.
3714
+ "agent.tool_call": {
3715
+ tool: oneOf(...AGENT_TOOL_NAMES),
3716
+ result: oneOf("ok", "error"),
3717
+ code: oneOf(...AGENT_ERROR_CODES),
3718
+ agent_kind: agentKind,
3719
+ duration_bucket: oneOf("lt_1s", "1_10s", "10_60s", "gt_60s"),
3720
+ version_major: count,
3721
+ version_minor: count,
3722
+ version_patch: count
3723
+ }
3724
+ };
3725
+ var platformRule = oneOf("web", "android", "ios", "agent", "unknown");
3726
+ var PERSON_PROPERTY_RULES = {
3727
+ plan: oneOf("anon", "free", "pro", "max"),
3728
+ locale: oneOf("es-AR", "pt-BR", "en"),
3729
+ storage_bytes: count
3730
+ };
3731
+
3732
+ // src/telemetry.ts
3733
+ var OFF_WORDS = /* @__PURE__ */ new Set(["0", "off", "false", "no"]);
3734
+ var ON_WORDS = /* @__PURE__ */ new Set(["1", "on", "true", "yes"]);
3735
+ function telemetryState() {
3736
+ const env = (process.env.ZAS_AGENT_TELEMETRY ?? "").toLowerCase();
3737
+ if (OFF_WORDS.has(env)) return { on: false, source: "env" };
3738
+ if (ON_WORDS.has(env)) return { on: true, source: "env" };
3739
+ if (process.env.DO_NOT_TRACK === "1") return { on: false, source: "do_not_track" };
3740
+ const stored = loadSettings();
3741
+ if (stored && typeof stored.telemetry === "boolean") return { on: stored.telemetry, source: "file" };
3742
+ return { on: true, source: "default" };
3743
+ }
3744
+ function setTelemetry(on) {
3745
+ const stored = loadSettings();
3746
+ saveSettings({ ...stored ?? {}, version: 1, telemetry: on, decided_at: Date.now() });
3747
+ }
3748
+ function noticeShown() {
3749
+ return typeof loadSettings()?.notified_at === "number";
3750
+ }
3751
+ function markNoticeShown() {
3752
+ const stored = loadSettings();
3753
+ saveSettings({ ...stored ?? {}, version: 1, notified_at: Date.now() });
3754
+ }
3755
+ var TELEMETRY_NOTICE = [
3756
+ "Zas collects usage data from this agent to improve the product: which tool",
3757
+ "ran, whether it worked, the error code when it did not, how long it took,",
3758
+ "and the version. Never file names, file contents, paths or channel names.",
3759
+ "The data is linked to your Zas account.",
3760
+ "Turn it off with: npx -y zas-agent telemetry off"
3761
+ ].join("\n");
3762
+ var SOURCE_WORDS = {
3763
+ env: "ZAS_AGENT_TELEMETRY",
3764
+ do_not_track: "DO_NOT_TRACK",
3765
+ file: "zas-agent telemetry",
3766
+ default: "default"
3767
+ };
3768
+ function telemetryLine() {
3769
+ const state = telemetryState();
3770
+ return `telemetry: ${state.on ? "on" : "off"} (${SOURCE_WORDS[state.source]})`;
3771
+ }
3772
+ var TOOL_NAMES = new Set(AGENT_TOOL_NAMES);
3773
+ function versionParts(version) {
3774
+ const [major = 0, minor = 0, patch = 0] = version.split("-")[0].split(".").map((part) => {
3775
+ const value = Number.parseInt(part, 10);
3776
+ return Number.isFinite(value) && value >= 0 ? value : 0;
3777
+ });
3778
+ return { major, minor, patch };
3779
+ }
3780
+ function toolCallProperties(input) {
3781
+ const tool = input.tool.startsWith("zas_") ? input.tool.slice(4) : input.tool;
3782
+ if (!TOOL_NAMES.has(tool)) return null;
3783
+ const version = versionParts(input.version);
3784
+ return {
3785
+ tool,
3786
+ result: input.code === null ? "ok" : "error",
3787
+ // `other` is the server's word for a code outside the vocabulary. The
3788
+ // shared list is the one that decides, and it drops anything else.
3789
+ code: input.code ?? "none",
3790
+ duration_bucket: agentDurationBucket(input.ms),
3791
+ version_major: version.major,
3792
+ version_minor: version.minor,
3793
+ version_patch: version.patch
3794
+ };
3795
+ }
3796
+ var REPORT_TIMEOUT_MS = 5e3;
3797
+ async function report(client, properties) {
3798
+ if (properties === null || !telemetryState().on) return;
3799
+ const timeout = new Promise((resolve2) => {
3800
+ const timer = setTimeout(resolve2, REPORT_TIMEOUT_MS);
3801
+ timer.unref?.();
3802
+ });
3803
+ const sent = client.api("POST", "/agents/telemetry", { event: "agent.tool_call", properties }).then(() => void 0).catch(() => void 0);
3804
+ await Promise.race([sent, timeout]);
3805
+ }
3806
+
3328
3807
  // src/server.ts
3329
3808
  function agentVersion() {
3330
- return true ? "0.5.0" : "0.0.0-dev";
3809
+ return true ? "0.6.0" : "0.0.0-dev";
3331
3810
  }
3332
3811
  var AGENT_CUE = " The owner sees every item this agent sends with the >_ agent mark and this agent's name, on every device.";
3333
3812
  var PAIR_ANNOUNCE_MS = 15e3;
@@ -3341,9 +3820,33 @@ function rightsOf(grant) {
3341
3820
  if (grant.read) rights.push(grant.direct_mode ? "receive (Directo)" : "read");
3342
3821
  return rights.length > 0 ? rights.join(" \xB7 ") : "no access";
3343
3822
  }
3823
+ function answeredCode(answer) {
3824
+ if (!answer?.isError) return null;
3825
+ const match = /^([a-z_]{2,40}):/.exec(answer.content?.[0]?.text ?? "");
3826
+ return match ? match[1] : "other";
3827
+ }
3344
3828
  function buildServer(profile, deps = {}) {
3345
3829
  const server = new McpServer({ name: "zas", version: agentVersion() });
3346
3830
  const runner = deps.runner ?? new JobRunner();
3831
+ const reportCall = async (tool, ms, code) => {
3832
+ try {
3833
+ if (!(deps.identity ?? loadIdentity(profile))) return;
3834
+ await report(ctx().client, toolCallProperties({ tool, ms, code, version: agentVersion() }));
3835
+ } catch {
3836
+ }
3837
+ };
3838
+ const registerRaw = server.registerTool.bind(server);
3839
+ server.registerTool = (name, meta, handler) => registerRaw(name, meta, async (...args) => {
3840
+ const started = Date.now();
3841
+ try {
3842
+ const answer = await handler(...args);
3843
+ void reportCall(name, Date.now() - started, answeredCode(answer));
3844
+ return answer;
3845
+ } catch (e) {
3846
+ void reportCall(name, Date.now() - started, e instanceof ZasError ? e.code : "internal");
3847
+ throw e;
3848
+ }
3849
+ });
3347
3850
  let client = null;
3348
3851
  const ctx = () => {
3349
3852
  const identity = deps.identity ?? loadIdentity(profile);
@@ -3391,7 +3894,8 @@ function buildServer(profile, deps = {}) {
3391
3894
  `Paired as \u201C${c.identity.name}\u201D (${c.identity.kind}) with account ${c.identity.owner_uid}.`,
3392
3895
  grants.length > 0 ? "Channels:" : "No channels: the owner has not granted access to any yet.",
3393
3896
  ...lines,
3394
- `${packageName()} ${agentVersion()} \xB7 profile ${profile}`
3897
+ `${packageName()} ${agentVersion()} \xB7 profile ${profile}`,
3898
+ `${telemetryLine()} \xB7 change it with \u201Cnpx -y ${packageName()} telemetry off\u201D`
3395
3899
  ].join("\n"));
3396
3900
  } catch (e) {
3397
3901
  return failed(e);
@@ -3485,7 +3989,7 @@ function buildServer(profile, deps = {}) {
3485
3989
  "file",
3486
3990
  input.title ?? input.path,
3487
3991
  input.channel ?? "",
3488
- (report) => sendFile(c, input, report)
3992
+ (report2) => sendFile(c, input, report2)
3489
3993
  );
3490
3994
  return settled(await runner.wait(job));
3491
3995
  } catch (e) {
@@ -3511,7 +4015,7 @@ function buildServer(profile, deps = {}) {
3511
4015
  "direct",
3512
4016
  input.path,
3513
4017
  input.channel ?? "",
3514
- (report) => sendDirect(c, input, report, {
4018
+ (report2) => sendDirect(c, input, report2, {
3515
4019
  ...deps.direct ?? {},
3516
4020
  onFailed: (record) => rememberFailed(job.id, record)
3517
4021
  })
@@ -3535,7 +4039,7 @@ function buildServer(profile, deps = {}) {
3535
4039
  "fallback",
3536
4040
  record.name,
3537
4041
  record.channel_name,
3538
- (report) => sendDirectFallback(c, record, report, deps.direct)
4042
+ (report2) => sendDirectFallback(c, record, report2, deps.direct)
3539
4043
  );
3540
4044
  const outcome = await runner.wait(job);
3541
4045
  if (outcome.status === "done") failedDirects.delete(input.job);
@@ -3563,7 +4067,7 @@ function buildServer(profile, deps = {}) {
3563
4067
  "receive",
3564
4068
  input.dest ?? "Directo",
3565
4069
  input.channel ?? "",
3566
- (report) => receiveDirect(c, input, report, {
4070
+ (report2) => receiveDirect(c, input, report2, {
3567
4071
  ...deps.receive ?? {},
3568
4072
  onFailed: (record) => rememberFailedReceive(job.id, record)
3569
4073
  })
@@ -3587,7 +4091,7 @@ function buildServer(profile, deps = {}) {
3587
4091
  "fallback",
3588
4092
  record.meta.name,
3589
4093
  record.channel_name,
3590
- (report) => receiveDirectFallback(c, record, report, deps.receive)
4094
+ (report2) => receiveDirectFallback(c, record, report2, deps.receive)
3591
4095
  );
3592
4096
  const outcome = await runner.wait(job);
3593
4097
  if (outcome.status === "done") failedReceives.delete(input.job);
@@ -3664,7 +4168,16 @@ function parseArgs(argv) {
3664
4168
  const inline = arg.startsWith("--") && eq > 0 ? arg.slice(eq + 1) : void 0;
3665
4169
  const take = () => inline !== void 0 ? inline : argv[++i] ?? "";
3666
4170
  if (name === "pair") parsed.command = "pair";
3667
- else if (name === "--version" || name === "-v") parsed.command = "version";
4171
+ else if (name === "telemetry") {
4172
+ parsed.command = "telemetry";
4173
+ const next = argv[i + 1];
4174
+ if (next === "on" || next === "off") {
4175
+ parsed.telemetry = next;
4176
+ i++;
4177
+ } else if (next !== void 0 && !next.startsWith("--")) {
4178
+ return { ...parsed, command: "invalid", message: `Unknown telemetry setting \u201C${next}\u201D. Use \u201Con\u201D or \u201Coff\u201D.` };
4179
+ }
4180
+ } else if (name === "--version" || name === "-v") parsed.command = "version";
3668
4181
  else if (name === "--help" || name === "-h") parsed.command = "help";
3669
4182
  else if (name === "--profile") {
3670
4183
  parsed.profile = take() || DEFAULT_PROFILE;
@@ -3687,6 +4200,7 @@ var USAGE = [
3687
4200
  " zas-agent [--profile <name>] serve the MCP tools over stdio",
3688
4201
  " zas-agent pair [--profile <name>] pair this agent with a Zas account",
3689
4202
  " [--kind claude_code|codex|other] [--host <name>] [--no-open]",
4203
+ " zas-agent telemetry [on|off] say what this machine reports, or change it",
3690
4204
  " zas-agent --version"
3691
4205
  ].join("\n");
3692
4206
  async function main(argv, log = (l) => console.error(l)) {
@@ -3704,7 +4218,17 @@ async function main(argv, log = (l) => console.error(l)) {
3704
4218
  log(USAGE);
3705
4219
  return args.unknown ? 2 : 0;
3706
4220
  }
4221
+ if (args.command === "telemetry") {
4222
+ if (args.telemetry) setTelemetry(args.telemetry === "on");
4223
+ markNoticeShown();
4224
+ log(telemetryLine());
4225
+ log(args.telemetry === "off" ? `Nothing is sent. Turn it on again with: npx -y ${packageName()} telemetry on` : TELEMETRY_NOTICE);
4226
+ return 0;
4227
+ }
3707
4228
  if (args.command === "pair") {
4229
+ log(TELEMETRY_NOTICE);
4230
+ log("");
4231
+ markNoticeShown();
3708
4232
  let rl = null;
3709
4233
  const askCode = process.stdin.isTTY ? () => {
3710
4234
  rl ??= createInterface({ input: process.stdin, output: process.stderr });
@@ -3733,6 +4257,10 @@ async function main(argv, log = (l) => console.error(l)) {
3733
4257
  }
3734
4258
  return 0;
3735
4259
  }
4260
+ if (!noticeShown()) {
4261
+ log(TELEMETRY_NOTICE);
4262
+ markNoticeShown();
4263
+ }
3736
4264
  await buildServer(args.profile).connect(new StdioServerTransport());
3737
4265
  return 0;
3738
4266
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zas-agent",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "MCP server that lets a coding agent send files and notes into your Zas channels, and send or receive files live through Directo.",
5
5
  "type": "module",
6
6
  "license": "MIT",