zas-agent 0.5.1 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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",
@@ -822,89 +830,6 @@ async function runPair(opts) {
822
830
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
823
831
  import { z } from "zod";
824
832
 
825
- // src/direct.ts
826
- import { randomBytes } from "node:crypto";
827
- import { openAsBlob } from "node:fs";
828
- import { basename as basename2 } from "node:path";
829
-
830
- // src/shared/manifest.ts
831
- import { xchacha20poly1305 } from "@noble/ciphers/chacha";
832
-
833
- // src/shared/constants.ts
834
- var CHUNKER_VERSION = "chunker_v1";
835
- var CHUNK_MIN = 1 * 1024 * 1024;
836
- var CHUNK_AVG_BITS = 22;
837
- var CHUNK_MAX = 8 * 1024 * 1024;
838
- var STORAGE_V2_PART_BYTES = 16 * 1024 * 1024;
839
- var GEAR_SEED = "zas-gear-v1:";
840
- var HKDF_INFO_CHUNK_KEY = "zas-chunk-key-v1";
841
- var HKDF_INFO_CHUNK_NONCE = "zas-chunk-nonce-v1";
842
- var OPRF_CONTEXT = "OPRFV1-\0-ristretto255-SHA512";
843
- var MANIFEST_VERSION = 1;
844
- var ANON_EMPTY_TTL_MS = 30 * 60 * 1e3;
845
- var FREE_RELAY_MAX_BYTES = 50 * 1024 * 1024;
846
- var SAFE_STORED_FILE_LIMITS = {
847
- anon: 30 * 1024 * 1024,
848
- free: FREE_RELAY_MAX_BYTES,
849
- pro: 200 * 1024 * 1024,
850
- max: 200 * 1024 * 1024
851
- };
852
- var STORED_FILE_LIMITS_V2 = {
853
- anon: 30 * 1024 * 1024,
854
- free: FREE_RELAY_MAX_BYTES,
855
- pro: 5 * 1024 * 1024 * 1024,
856
- max: 20 * 1024 * 1024 * 1024
857
- };
858
- var STORAGE_LIMITS = {
859
- anon: 30 * 1024 * 1024,
860
- free: 100 * 1024 * 1024,
861
- pro: null,
862
- max: null
863
- };
864
- var THUMBNAIL_MAX_EDGE = 640;
865
- var THUMBNAIL_MAX_BYTES = 14e4;
866
-
867
- // src/shared/manifest.ts
868
- function newManifest(partial) {
869
- return { v: MANIFEST_VERSION, chunker: CHUNKER_VERSION, ...partial };
870
- }
871
- var ENVELOPE_MAGIC = 90;
872
- var KEY_VERSION_LEGACY = 1;
873
- function envelopeVersion(sealed) {
874
- if (sealed.length > 26 && sealed[0] === ENVELOPE_MAGIC && sealed[1] >= 1) return sealed[1];
875
- return KEY_VERSION_LEGACY;
876
- }
877
- function sealBytes(key, version, plain) {
878
- if (version < 1 || version > 255) throw new Error("key version out of range");
879
- const nonce = new Uint8Array(24);
880
- crypto.getRandomValues(nonce);
881
- const ct = xchacha20poly1305(key, nonce).encrypt(plain);
882
- return concatBytes(new Uint8Array([ENVELOPE_MAGIC, version]), nonce, ct);
883
- }
884
- function openSealed(key, sealed) {
885
- const versioned = envelopeVersion(sealed) !== KEY_VERSION_LEGACY || sealed[0] === ENVELOPE_MAGIC;
886
- if (versioned && sealed.length > 26) {
887
- try {
888
- return xchacha20poly1305(key, sealed.slice(2, 26)).decrypt(sealed.slice(26));
889
- } catch {
890
- }
891
- }
892
- return xchacha20poly1305(key, sealed.slice(0, 24)).decrypt(sealed.slice(24));
893
- }
894
- var sealRaw = sealBytes;
895
- var openRaw = openSealed;
896
- function sealManifest(channelKey, manifest, version = KEY_VERSION_LEGACY) {
897
- return sealBytes(channelKey, version, new TextEncoder().encode(JSON.stringify(manifest)));
898
- }
899
- function openManifest(channelKey, sealed) {
900
- const m = JSON.parse(new TextDecoder().decode(openSealed(channelKey, sealed)));
901
- if (!Array.isArray(m.chunks)) m.chunks = [];
902
- return m;
903
- }
904
- function decryptChannelName(channelKey, sealed) {
905
- return new TextDecoder().decode(openSealed(channelKey, sealed));
906
- }
907
-
908
833
  // src/shared/direct.ts
909
834
  var DIRECT_OFFER_TTL_MS = 10 * 60 * 1e3;
910
835
  var DIRECT_CLAIMED_TTL_MS = 6 * 60 * 60 * 1e3;
@@ -940,6 +865,13 @@ var FRAME_CHUNK = 2;
940
865
  var FRAME_DONE = 3;
941
866
  var FRAME_ABORT = 4;
942
867
  var FRAME_CREDIT = 5;
868
+ function directMetaOf(file, fallbackName) {
869
+ return {
870
+ name: file.name || fallbackName || "zas",
871
+ size: file.size,
872
+ mime: file.type || "application/octet-stream"
873
+ };
874
+ }
943
875
  function encodeMeta(meta) {
944
876
  const body = new TextEncoder().encode(JSON.stringify(meta));
945
877
  const frame = new Uint8Array(1 + body.length);
@@ -1012,9 +944,63 @@ function candType(candidate) {
1012
944
  const m = /\styp\s+(\S+)/.exec(candidate?.candidate ?? "");
1013
945
  return m ? m[1] : "other";
1014
946
  }
947
+ function candProto(candidate) {
948
+ const line = candidate?.candidate ?? "";
949
+ const m = /^(?:a=)?(?:candidate:)?\S+\s+\d+\s+(\S+)/.exec(line);
950
+ const proto = m ? m[1].toLowerCase() : "?";
951
+ const tcp = /\stcptype\s+(\S+)/.exec(line);
952
+ return tcp ? `${proto}/${tcp[1]}` : proto;
953
+ }
954
+ var TRACE_MAX = 200;
955
+ var DETAIL_MAX = 200;
956
+ function errText(e) {
957
+ const raw = e instanceof Error ? e.message || e.name : String(e);
958
+ const line = raw.replace(/\s+/g, " ").trim();
959
+ return line.length > DETAIL_MAX ? `${line.slice(0, DETAIL_MAX)}\u2026` : line;
960
+ }
961
+ var traceStart = /* @__PURE__ */ new WeakMap();
962
+ function traceAdd(diag, line) {
963
+ if (diag.trace.length >= TRACE_MAX) return;
964
+ if (diag.trace.length === TRACE_MAX - 1) {
965
+ diag.trace.push("trace full");
966
+ return;
967
+ }
968
+ const t0 = traceStart.get(diag);
969
+ const at = t0 === void 0 ? 0 : (Date.now() - t0) / 1e3;
970
+ diag.trace.push(`${at.toFixed(2)} ${line}`);
971
+ }
972
+ function iceShapes(servers) {
973
+ const seen = /* @__PURE__ */ new Map();
974
+ for (const server of servers ?? []) {
975
+ const urls = typeof server.urls === "string" ? [server.urls] : server.urls;
976
+ for (const url of urls ?? []) {
977
+ const scheme = /^(stuns?|turns?):/i.exec(url)?.[1]?.toLowerCase() ?? "other";
978
+ const transport = /[?&]transport=(\w+)/i.exec(url)?.[1]?.toLowerCase();
979
+ const shape = transport ? `${scheme}/${transport}` : scheme;
980
+ seen.set(shape, (seen.get(shape) ?? 0) + 1);
981
+ }
982
+ }
983
+ const parts = [...seen].map(([shape, n]) => `${shape}x${n}`);
984
+ return parts.length > 0 ? parts.join(" ") : "none";
985
+ }
986
+ function formatDirectDiagLines(d) {
987
+ const connect = d.msConnect === void 0 ? "-" : `${d.msConnect}ms`;
988
+ const pair = d.pairLocal || d.pairRemote ? `${d.pairLocal || "?"}/${d.pairRemote || "?"}` : "none";
989
+ return [
990
+ `ms=${d.ms} connect=${connect} bytes=${d.bytes} restarts=${d.restarts} pair=${pair}`,
991
+ ...d.detail === void 0 ? [] : [`detail ${d.detail}`],
992
+ `local host=${d.localHost} srflx=${d.localSrflx} relay=${d.localRelay} remote host=${d.remoteHost} srflx=${d.remoteSrflx} relay=${d.remoteRelay}`,
993
+ `turn supplied=${d.turnUrlsSupplied} configured=${d.turnUrlsConfigured} ice=${d.iceState} gather=${d.gatherState} remote_desc=${d.hadRemoteDesc}`,
994
+ ...d.trace ?? []
995
+ ];
996
+ }
1015
997
  function countCand(diag, side, c) {
1016
- if (!c) return;
998
+ if (!c) {
999
+ traceAdd(diag, `${side} end-of-candidates`);
1000
+ return;
1001
+ }
1017
1002
  const t = candType(c);
1003
+ traceAdd(diag, `${side} ${t} ${candProto(c)}`);
1018
1004
  if (t === "host") diag[side === "local" ? "localHost" : "remoteHost"]++;
1019
1005
  else if (t === "srflx" || t === "prflx") diag[side === "local" ? "localSrflx" : "remoteSrflx"]++;
1020
1006
  else if (t === "relay") diag[side === "local" ? "localRelay" : "remoteRelay"]++;
@@ -1055,7 +1041,17 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
1055
1041
  const initialIce = ice ?? DIRECT_ICE;
1056
1042
  const pc = new RTCPeerConnection({
1057
1043
  iceServers: initialIce,
1058
- iceTransportPolicy
1044
+ iceTransportPolicy,
1045
+ // Gather before there is anything to gather for. A receiver whose peer
1046
+ // is an agent gets the whole remote candidate set at once — the agent
1047
+ // writes it and leaves — so checking starts in the same tick as the local
1048
+ // description, a pair comes up on the first candidate found, and
1049
+ // gathering is reported complete before any relay allocation returns.
1050
+ // Measured 2026-09-04: one host candidate, no relay, gathering done at
1051
+ // 240 ms, and when that pair died five seconds later there was nothing
1052
+ // to fall back to. The pool does the allocations up front, at construction,
1053
+ // so the relay candidates exist before the first check is ever sent.
1054
+ iceCandidatePoolSize: 1
1059
1055
  });
1060
1056
  let phase = "connecting";
1061
1057
  let disconnectTimer;
@@ -1077,7 +1073,13 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
1077
1073
  turnUrlsSupplied: countTurnUrls(initialIce),
1078
1074
  turnUrlsConfigured: 0,
1079
1075
  bytes: 0,
1080
- restarts: 0
1076
+ restarts: 0,
1077
+ trace: []
1078
+ };
1079
+ traceStart.set(diag, startedAt);
1080
+ traceAdd(diag, `ice servers ${iceShapes(initialIce)} policy=${iceTransportPolicy}`);
1081
+ pc.onicegatheringstatechange = () => {
1082
+ traceAdd(diag, `gather ${String(pc.iceGatheringState ?? "")}`);
1081
1083
  };
1082
1084
  const cleanup = () => {
1083
1085
  clearTimeout(waitTimer);
@@ -1086,6 +1088,7 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
1086
1088
  clearTimeout(restartTimer);
1087
1089
  pc.onicecandidate = null;
1088
1090
  pc.oniceconnectionstatechange = null;
1091
+ pc.onicegatheringstatechange = null;
1089
1092
  pc.close();
1090
1093
  };
1091
1094
  const setPhase = (p) => {
@@ -1097,6 +1100,7 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
1097
1100
  diag.gatherState = String(pc.iceGatheringState ?? "");
1098
1101
  diag.hadRemoteDesc = hasRemoteDescription(pc);
1099
1102
  diag.turnUrlsConfigured = countTurnUrls(pc.getConfiguration().iceServers);
1103
+ traceAdd(diag, `${p}${diag.reason ? ` ${diag.reason}` : ""} ice=${diag.iceState} gather=${diag.gatherState}`);
1100
1104
  onDiag?.(diag);
1101
1105
  onPhase(p);
1102
1106
  cleanup();
@@ -1104,9 +1108,13 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
1104
1108
  }
1105
1109
  onPhase(p);
1106
1110
  };
1107
- const fail = (reason) => {
1111
+ const fail = (reason, cause) => {
1108
1112
  if (phase === "done" || phase === "failed") return;
1109
1113
  if (!diag.reason) diag.reason = reason;
1114
+ if (cause !== void 0 && diag.detail === void 0) {
1115
+ diag.detail = errText(cause);
1116
+ traceAdd(diag, `detail ${diag.detail}`);
1117
+ }
1110
1118
  setPhase("failed");
1111
1119
  };
1112
1120
  const waitTimer = setTimeout(() => fail("peer_silent"), DIRECT_SIGNAL_WAIT_MS);
@@ -1130,12 +1138,14 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
1130
1138
  }
1131
1139
  stallState = "restarting";
1132
1140
  diag.restarts++;
1141
+ traceAdd(diag, "ice restart");
1133
1142
  clearTimeout(restartTimer);
1134
1143
  restartTimer = setTimeout(() => fail("disconnected"), stallWindowMs);
1135
1144
  onStall();
1136
1145
  };
1137
1146
  pc.oniceconnectionstatechange = () => {
1138
1147
  const s = pc.iceConnectionState;
1148
+ traceAdd(diag, `ice ${String(s ?? "")}`);
1139
1149
  if (s === "failed") {
1140
1150
  if (diag.msConnect !== void 0 && onStall) stalled();
1141
1151
  else fail("ice_failed");
@@ -1160,7 +1170,10 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
1160
1170
  connected: () => {
1161
1171
  clearTimeout(waitTimer);
1162
1172
  clearTimeout(connectTimer);
1163
- if (diag.msConnect === void 0) diag.msConnect = Date.now() - startedAt;
1173
+ if (diag.msConnect === void 0) {
1174
+ diag.msConnect = Date.now() - startedAt;
1175
+ traceAdd(diag, "channel open");
1176
+ }
1164
1177
  },
1165
1178
  isTerminal: () => phase === "done" || phase === "failed",
1166
1179
  setStall: (fn, windowMs) => {
@@ -1169,6 +1182,7 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
1169
1182
  },
1170
1183
  setIceServers: (iceServers) => {
1171
1184
  diag.turnUrlsSupplied = countTurnUrls(iceServers);
1185
+ traceAdd(diag, `ice servers replaced ${iceShapes(iceServers)}`);
1172
1186
  pc.setConfiguration({ iceServers });
1173
1187
  }
1174
1188
  };
@@ -1250,7 +1264,7 @@ function startSender(opts) {
1250
1264
  const offer = await pc.createOffer({ iceRestart: true });
1251
1265
  await pc.setLocalDescription(offer);
1252
1266
  await opts.send({ kind: "offer", sdp: offer.sdp, protocol: 2, generation });
1253
- })().catch(() => s.fail("signaling"));
1267
+ })().catch((e) => s.fail("signaling", e));
1254
1268
  }, DIRECT_CONNECT_TIMEOUT_MS);
1255
1269
  const dc = pc.createDataChannel("zas-direct", { ordered: true });
1256
1270
  dc.binaryType = "arraybuffer";
@@ -1277,9 +1291,7 @@ function startSender(opts) {
1277
1291
  };
1278
1292
  const pump = async () => {
1279
1293
  const meta = {
1280
- name: opts.file.name ?? opts.name ?? "zas",
1281
- size: opts.file.size,
1282
- mime: opts.file.type || "application/octet-stream",
1294
+ ...directMetaOf(opts.file, opts.name),
1283
1295
  ...opts.label !== void 0 ? { label: opts.label } : {}
1284
1296
  };
1285
1297
  dc.send(encodeMeta(meta));
@@ -1318,7 +1330,7 @@ function startSender(opts) {
1318
1330
  s.diag.pairRemote = pair.remote;
1319
1331
  opts.onPath?.(pathOf(pair));
1320
1332
  }).catch(() => void 0);
1321
- void pump().catch(() => s.fail("transfer"));
1333
+ void pump().catch((e) => s.fail("transfer", e));
1322
1334
  };
1323
1335
  dc.onmessage = (e) => {
1324
1336
  try {
@@ -1344,7 +1356,7 @@ function startSender(opts) {
1344
1356
  plumbing.setLocalGeneration(generation);
1345
1357
  plumbing.expectRemoteGeneration(generation);
1346
1358
  await opts.send({ kind: "offer", sdp: offer.sdp, protocol: 2, generation });
1347
- })().catch(() => s.fail("signaling"));
1359
+ })().catch((e) => s.fail("signaling", e));
1348
1360
  return {
1349
1361
  accept: (msg) => {
1350
1362
  if (s.isTerminal()) return;
@@ -1354,7 +1366,7 @@ function startSender(opts) {
1354
1366
  if (msg.generation !== void 0 && answerGeneration !== generation) return;
1355
1367
  peerProtocol = msg.protocol === 2 ? 2 : 1;
1356
1368
  plumbing.expectRemoteGeneration(answerGeneration);
1357
- void pc.setRemoteDescription({ type: "answer", sdp: msg.sdp }).then(() => plumbing.remoteDescriptionSet(answerGeneration)).catch(() => s.fail("signaling"));
1369
+ void pc.setRemoteDescription({ type: "answer", sdp: msg.sdp }).then(() => plumbing.remoteDescriptionSet(answerGeneration)).catch((e) => s.fail("signaling", e));
1358
1370
  }
1359
1371
  if (msg.kind === "ice") plumbing.applyIce(msg.candidate, msg.generation);
1360
1372
  },
@@ -1427,7 +1439,7 @@ function startReceiver(opts) {
1427
1439
  s.fail(frame.meta.size > DIRECT_FILE_MAX_BYTES ? "too_big" : "protocol");
1428
1440
  return;
1429
1441
  }
1430
- if (opts.expectedMeta && (frame.meta.name !== opts.expectedMeta.name || frame.meta.size !== opts.expectedMeta.size || frame.meta.mime !== opts.expectedMeta.mime)) {
1442
+ if (opts.expectedMeta && (frame.meta.name !== opts.expectedMeta.name || frame.meta.size !== opts.expectedMeta.size)) {
1431
1443
  dc?.send(ABORT_FRAME);
1432
1444
  s.fail("protocol");
1433
1445
  return;
@@ -1439,7 +1451,7 @@ function startReceiver(opts) {
1439
1451
  sinkRef = sink;
1440
1452
  return sink;
1441
1453
  });
1442
- pipeline.catch(() => s.fail("sink"));
1454
+ pipeline.catch((e3) => s.fail("sink", e3));
1443
1455
  } else if (frame.type === "chunk") {
1444
1456
  if (!pipeline || finished) throw new Error("chunk_before_meta");
1445
1457
  queued += frame.payload.length;
@@ -1457,7 +1469,7 @@ function startReceiver(opts) {
1457
1469
  }
1458
1470
  return sink;
1459
1471
  });
1460
- pipeline.catch(() => s.fail("sink"));
1472
+ pipeline.catch((e3) => s.fail("sink", e3));
1461
1473
  } else if (frame.type === "done") {
1462
1474
  if (!pipeline || finished) throw new Error("done_before_meta");
1463
1475
  finished = true;
@@ -1468,7 +1480,7 @@ function startReceiver(opts) {
1468
1480
  await sink.close();
1469
1481
  dc?.send(DONE_FRAME);
1470
1482
  s.setPhase("done");
1471
- }).catch(() => s.fail("integrity"));
1483
+ }).catch((e3) => s.fail("integrity", e3));
1472
1484
  } else if (frame.type === "abort") {
1473
1485
  s.fail("peer_abort");
1474
1486
  }
@@ -1502,7 +1514,7 @@ function startReceiver(opts) {
1502
1514
  const answer = await pc.createAnswer();
1503
1515
  await pc.setLocalDescription(answer);
1504
1516
  await opts.send({ kind: "answer", sdp: answer.sdp, protocol: 2, generation });
1505
- })().catch(() => s.fail("signaling"));
1517
+ })().catch((e) => s.fail("signaling", e));
1506
1518
  }
1507
1519
  if (msg.kind === "ice") plumbing.applyIce(msg.candidate, msg.generation);
1508
1520
  },
@@ -1519,6 +1531,89 @@ function startReceiver(opts) {
1519
1531
  };
1520
1532
  }
1521
1533
 
1534
+ // src/direct.ts
1535
+ import { randomBytes } from "node:crypto";
1536
+ import { openAsBlob } from "node:fs";
1537
+ import { basename as basename2 } from "node:path";
1538
+
1539
+ // src/shared/manifest.ts
1540
+ import { xchacha20poly1305 } from "@noble/ciphers/chacha";
1541
+
1542
+ // src/shared/constants.ts
1543
+ var CHUNKER_VERSION = "chunker_v1";
1544
+ var CHUNK_MIN = 1 * 1024 * 1024;
1545
+ var CHUNK_AVG_BITS = 22;
1546
+ var CHUNK_MAX = 8 * 1024 * 1024;
1547
+ var STORAGE_V2_PART_BYTES = 16 * 1024 * 1024;
1548
+ var GEAR_SEED = "zas-gear-v1:";
1549
+ var HKDF_INFO_CHUNK_KEY = "zas-chunk-key-v1";
1550
+ var HKDF_INFO_CHUNK_NONCE = "zas-chunk-nonce-v1";
1551
+ var OPRF_CONTEXT = "OPRFV1-\0-ristretto255-SHA512";
1552
+ var MANIFEST_VERSION = 1;
1553
+ var ANON_EMPTY_TTL_MS = 30 * 60 * 1e3;
1554
+ var FREE_RELAY_MAX_BYTES = 50 * 1024 * 1024;
1555
+ var SAFE_STORED_FILE_LIMITS = {
1556
+ anon: 30 * 1024 * 1024,
1557
+ free: FREE_RELAY_MAX_BYTES,
1558
+ pro: 200 * 1024 * 1024,
1559
+ max: 200 * 1024 * 1024
1560
+ };
1561
+ var STORED_FILE_LIMITS_V2 = {
1562
+ anon: 30 * 1024 * 1024,
1563
+ free: FREE_RELAY_MAX_BYTES,
1564
+ pro: 5 * 1024 * 1024 * 1024,
1565
+ max: 20 * 1024 * 1024 * 1024
1566
+ };
1567
+ var STORAGE_LIMITS = {
1568
+ anon: 30 * 1024 * 1024,
1569
+ free: 100 * 1024 * 1024,
1570
+ pro: null,
1571
+ max: null
1572
+ };
1573
+ var THUMBNAIL_MAX_EDGE = 640;
1574
+ var THUMBNAIL_MAX_BYTES = 14e4;
1575
+
1576
+ // src/shared/manifest.ts
1577
+ function newManifest(partial) {
1578
+ return { v: MANIFEST_VERSION, chunker: CHUNKER_VERSION, ...partial };
1579
+ }
1580
+ var ENVELOPE_MAGIC = 90;
1581
+ var KEY_VERSION_LEGACY = 1;
1582
+ function envelopeVersion(sealed) {
1583
+ if (sealed.length > 26 && sealed[0] === ENVELOPE_MAGIC && sealed[1] >= 1) return sealed[1];
1584
+ return KEY_VERSION_LEGACY;
1585
+ }
1586
+ function sealBytes(key, version, plain) {
1587
+ if (version < 1 || version > 255) throw new Error("key version out of range");
1588
+ const nonce = new Uint8Array(24);
1589
+ crypto.getRandomValues(nonce);
1590
+ const ct = xchacha20poly1305(key, nonce).encrypt(plain);
1591
+ return concatBytes(new Uint8Array([ENVELOPE_MAGIC, version]), nonce, ct);
1592
+ }
1593
+ function openSealed(key, sealed) {
1594
+ const versioned = envelopeVersion(sealed) !== KEY_VERSION_LEGACY || sealed[0] === ENVELOPE_MAGIC;
1595
+ if (versioned && sealed.length > 26) {
1596
+ try {
1597
+ return xchacha20poly1305(key, sealed.slice(2, 26)).decrypt(sealed.slice(26));
1598
+ } catch {
1599
+ }
1600
+ }
1601
+ return xchacha20poly1305(key, sealed.slice(0, 24)).decrypt(sealed.slice(24));
1602
+ }
1603
+ var sealRaw = sealBytes;
1604
+ var openRaw = openSealed;
1605
+ function sealManifest(channelKey, manifest, version = KEY_VERSION_LEGACY) {
1606
+ return sealBytes(channelKey, version, new TextEncoder().encode(JSON.stringify(manifest)));
1607
+ }
1608
+ function openManifest(channelKey, sealed) {
1609
+ const m = JSON.parse(new TextDecoder().decode(openSealed(channelKey, sealed)));
1610
+ if (!Array.isArray(m.chunks)) m.chunks = [];
1611
+ return m;
1612
+ }
1613
+ function decryptChannelName(channelKey, sealed) {
1614
+ return new TextDecoder().decode(openSealed(channelKey, sealed));
1615
+ }
1616
+
1522
1617
  // src/shared/direct-fallback.ts
1523
1618
  function createFallbackMeta(file) {
1524
1619
  const key = crypto.getRandomValues(new Uint8Array(32));
@@ -1636,15 +1731,15 @@ async function uploadFallback(options) {
1636
1731
  const key = await aesKey(meta);
1637
1732
  const completed = [];
1638
1733
  const partProgress = /* @__PURE__ */ new Map();
1639
- const report = () => options.onProgress?.(
1734
+ const report2 = () => options.onProgress?.(
1640
1735
  Math.min(meta.cipher_size, [...partProgress.values()].reduce((sum, value) => sum + value, 0)),
1641
1736
  meta.cipher_size
1642
1737
  );
1643
1738
  for (let first = 1; first <= meta.part_count; first += URL_BATCH) {
1644
1739
  aborted(signal);
1645
- const count = Math.min(URL_BATCH, meta.part_count - first + 1);
1646
- const signed = await options.getUrls(first, count);
1647
- if (signed.length !== count || signed.some((entry, i) => entry.part_number !== first + i || typeof entry.url !== "string")) throw new Error("bad_fallback_urls");
1740
+ const count2 = Math.min(URL_BATCH, meta.part_count - first + 1);
1741
+ const signed = await options.getUrls(first, count2);
1742
+ if (signed.length !== count2 || signed.some((entry, i) => entry.part_number !== first + i || typeof entry.url !== "string")) throw new Error("bad_fallback_urls");
1648
1743
  let cursor = 0;
1649
1744
  const workers = Array.from({ length: Math.min(CONCURRENCY, signed.length) }, async () => {
1650
1745
  for (; ; ) {
@@ -1661,11 +1756,11 @@ async function uploadFallback(options) {
1661
1756
  for (let attempt = 0; attempt < RETRIES; attempt++) {
1662
1757
  aborted(signal);
1663
1758
  partProgress.set(partNumber, 0);
1664
- report();
1759
+ report2();
1665
1760
  try {
1666
1761
  etag = await options.put(entry.url, cipher, signal, (loaded) => {
1667
1762
  partProgress.set(partNumber, loaded);
1668
- report();
1763
+ report2();
1669
1764
  });
1670
1765
  break;
1671
1766
  } catch (error) {
@@ -2472,13 +2567,13 @@ function sealer(key, keyVersion) {
2472
2567
  open: (enc) => JSON.parse(new TextDecoder().decode(openRaw(key, b64ToBytes(enc))))
2473
2568
  };
2474
2569
  }
2475
- async function sendDirect(ctx, input, report, deps = {}) {
2570
+ async function sendDirect(ctx, input, report2, deps = {}) {
2476
2571
  const now = deps.now ?? (() => Date.now());
2477
2572
  const sleep2 = deps.sleep ?? defaultSleep2;
2478
2573
  const grant = await directGrantFor(ctx, input.channel);
2479
2574
  const key = channelKeyOf(ctx.identity, grant);
2480
2575
  const channelName = channelLabel(ctx, grant);
2481
- const file = await (deps.openFile ?? openAsBlob)(input.path).catch(() => {
2576
+ const file = await (deps.openFile ?? openAsBlob)(input.path, { type: mimeFor(input.path) }).catch(() => {
2482
2577
  throw new ZasError("upload_failed", 400);
2483
2578
  });
2484
2579
  if (file.size > DIRECT_FILE_MAX_BYTES) throw new ZasError("file_too_big", 413);
@@ -2489,8 +2584,8 @@ async function sendDirect(ctx, input, report, deps = {}) {
2489
2584
  const { seal, open } = sealer(key, grant.key_version);
2490
2585
  const stamp = { device, owner_uid: owner };
2491
2586
  const startedAt = now();
2492
- report("offer");
2493
- const meta = { name, size: file.size, mime: mimeFor(input.path) };
2587
+ report2("offer");
2588
+ const meta = directMetaOf(file, name);
2494
2589
  const { id } = await ctx.client.api("POST", `/direct/${cid}`, {
2495
2590
  meta_enc: seal(meta),
2496
2591
  key_version: grant.key_version,
@@ -2523,7 +2618,7 @@ async function sendDirect(ctx, input, report, deps = {}) {
2523
2618
  }
2524
2619
  };
2525
2620
  const turn = await fetchIce();
2526
- report("connecting");
2621
+ report2("connecting");
2527
2622
  let resolveOutcome;
2528
2623
  const outcome = new Promise((resolve2) => {
2529
2624
  resolveOutcome = resolve2;
@@ -2540,7 +2635,7 @@ async function sendDirect(ctx, input, report, deps = {}) {
2540
2635
  send: (msg) => ctx.client.api("POST", `${route}/signal`, { payload_enc: seal(msg), for: "receiver", ...stamp }).then(() => void 0),
2541
2636
  onPhase: (phase2) => {
2542
2637
  if (phase2 === "flight") {
2543
- report("flight");
2638
+ report2("flight");
2544
2639
  heartbeat ??= setInterval(() => {
2545
2640
  void ctx.client.api("POST", `${route}/heartbeat`, stamp).catch(() => void 0);
2546
2641
  }, deps.heartbeatMs ?? HEARTBEAT_MS);
@@ -2552,6 +2647,7 @@ async function sendDirect(ctx, input, report, deps = {}) {
2552
2647
  },
2553
2648
  onDiag: (d) => {
2554
2649
  diag = d;
2650
+ deps.onDiag?.(d);
2555
2651
  }
2556
2652
  });
2557
2653
  let over = false;
@@ -2580,7 +2676,7 @@ async function sendDirect(ctx, input, report, deps = {}) {
2580
2676
  over = true;
2581
2677
  clearInterval(heartbeat);
2582
2678
  await pump;
2583
- report("finishing");
2679
+ report2("finishing");
2584
2680
  if (phase === "done") {
2585
2681
  await setState("done");
2586
2682
  return {
@@ -2619,7 +2715,7 @@ var fetchPut = async (url, bytes, signal, onLoaded) => {
2619
2715
  onLoaded(bytes.length);
2620
2716
  return etag;
2621
2717
  };
2622
- async function sendDirectFallback(ctx, record, report, deps = {}) {
2718
+ async function sendDirectFallback(ctx, record, report2, deps = {}) {
2623
2719
  const now = deps.now ?? (() => Date.now());
2624
2720
  const startedAt = now();
2625
2721
  const opened = await (deps.openFile ?? openAsBlob)(record.path).catch(() => {
@@ -2630,7 +2726,7 @@ async function sendDirectFallback(ctx, record, report, deps = {}) {
2630
2726
  const { seal } = sealer(record.key, record.key_version);
2631
2727
  const stamp = { device: record.device, owner_uid: record.owner_uid };
2632
2728
  const base = `/direct/${record.channel_id}/${record.offer_id}/fallback`;
2633
- report("encrypting");
2729
+ report2("encrypting");
2634
2730
  const meta = createFallbackMeta(file);
2635
2731
  await ctx.client.api("POST", `${base}/start`, {
2636
2732
  meta_enc: seal(meta),
@@ -2639,7 +2735,7 @@ async function sendDirectFallback(ctx, record, report, deps = {}) {
2639
2735
  part_count: meta.part_count,
2640
2736
  ...stamp
2641
2737
  });
2642
- report("uploading");
2738
+ report2("uploading");
2643
2739
  let parts;
2644
2740
  try {
2645
2741
  parts = await uploadFallback({
@@ -2647,11 +2743,11 @@ async function sendDirectFallback(ctx, record, report, deps = {}) {
2647
2743
  offerId: record.offer_id,
2648
2744
  meta,
2649
2745
  put: deps.put ?? fetchPut,
2650
- getUrls: async (from, count) => {
2746
+ getUrls: async (from, count2) => {
2651
2747
  const r = await ctx.client.api(
2652
2748
  "POST",
2653
2749
  `${base}/parts`,
2654
- { from, count, ...stamp }
2750
+ { from, count: count2, ...stamp }
2655
2751
  );
2656
2752
  return r.urls;
2657
2753
  }
@@ -2661,7 +2757,7 @@ async function sendDirectFallback(ctx, record, report, deps = {}) {
2661
2757
  if (error instanceof ZasError) throw error;
2662
2758
  throw new ZasError("upload_failed", 0, error instanceof Error ? error.message : String(error));
2663
2759
  }
2664
- report("finishing");
2760
+ report2("finishing");
2665
2761
  await ctx.client.api("POST", `${base}/complete`, {
2666
2762
  parts: parts.map((part) => ({ part_number: part.partNumber, etag: part.etag })),
2667
2763
  ...stamp
@@ -3047,7 +3143,7 @@ var DiskSink = class {
3047
3143
  return error;
3048
3144
  }
3049
3145
  };
3050
- async function receiveDirect(ctx, input, report, deps = {}) {
3146
+ async function receiveDirect(ctx, input, report2, deps = {}) {
3051
3147
  const now = deps.now ?? (() => Date.now());
3052
3148
  const sleep2 = deps.sleep ?? defaultSleep2;
3053
3149
  const grant = await receiveGrantFor(ctx, input.channel);
@@ -3060,7 +3156,7 @@ async function receiveDirect(ctx, input, report, deps = {}) {
3060
3156
  const stamp = { device, owner_uid: owner };
3061
3157
  const startedAt = now();
3062
3158
  const channelPath = `accounts/${owner}/channels/${cid}`;
3063
- report("waiting");
3159
+ report2("waiting");
3064
3160
  const waitUntil = startedAt + (deps.offerWaitMs ?? OFFER_WAIT_MS2);
3065
3161
  let id;
3066
3162
  let meta;
@@ -3112,7 +3208,7 @@ async function receiveDirect(ctx, input, report, deps = {}) {
3112
3208
  }
3113
3209
  };
3114
3210
  const turn = await fetchIce();
3115
- report("connecting");
3211
+ report2("connecting");
3116
3212
  const sink = new DiskSink(input.dest, offerId);
3117
3213
  let resolveOutcome;
3118
3214
  const outcome = new Promise((resolve2) => {
@@ -3128,7 +3224,7 @@ async function receiveDirect(ctx, input, report, deps = {}) {
3128
3224
  expectedMeta: offered,
3129
3225
  send: (msg) => ctx.client.api("POST", `${route}/signal`, { payload_enc: seal(msg), for: "sender", ...stamp }).then(() => void 0),
3130
3226
  onPhase: (phase2) => {
3131
- if (phase2 === "flight") report("flight");
3227
+ if (phase2 === "flight") report2("flight");
3132
3228
  if (phase2 === "done" || phase2 === "failed") resolveOutcome(phase2);
3133
3229
  },
3134
3230
  onPath: (p) => {
@@ -3136,6 +3232,7 @@ async function receiveDirect(ctx, input, report, deps = {}) {
3136
3232
  },
3137
3233
  onDiag: (d) => {
3138
3234
  diag = d;
3235
+ deps.onDiag?.(d);
3139
3236
  },
3140
3237
  sink: async (m) => {
3141
3238
  sink.open(m);
@@ -3167,7 +3264,7 @@ async function receiveDirect(ctx, input, report, deps = {}) {
3167
3264
  const phase = await outcome;
3168
3265
  over = true;
3169
3266
  await pump;
3170
- report("finishing");
3267
+ report2("finishing");
3171
3268
  if (phase === "done") {
3172
3269
  await setState("done");
3173
3270
  return {
@@ -3202,7 +3299,7 @@ async function receiveDirect(ctx, input, report, deps = {}) {
3202
3299
  });
3203
3300
  throw new ZasError("direct_failed", 0, reason);
3204
3301
  }
3205
- async function receiveDirectFallback(ctx, record, report, deps = {}) {
3302
+ async function receiveDirectFallback(ctx, record, report2, deps = {}) {
3206
3303
  const now = deps.now ?? (() => Date.now());
3207
3304
  const startedAt = now();
3208
3305
  const { open } = sealer(record.key, record.key_version);
@@ -3224,7 +3321,7 @@ async function receiveDirectFallback(ctx, record, report, deps = {}) {
3224
3321
  throw new ZasError("file_changed", 0);
3225
3322
  }
3226
3323
  const sink = new DiskSink(record.dest, record.offer_id);
3227
- report("downloading");
3324
+ report2("downloading");
3228
3325
  try {
3229
3326
  sink.open(meta);
3230
3327
  await downloadFallback({
@@ -3240,7 +3337,7 @@ async function receiveDirectFallback(ctx, record, report, deps = {}) {
3240
3337
  if (error instanceof ZasError) throw error;
3241
3338
  throw new ZasError("network", 0, error instanceof Error ? error.message : String(error));
3242
3339
  }
3243
- report("finishing");
3340
+ report2("finishing");
3244
3341
  await ctx.client.api("POST", `${base}/received`, stamp);
3245
3342
  return {
3246
3343
  offer_id: record.offer_id,
@@ -3281,10 +3378,13 @@ var JobRunner = class {
3281
3378
  };
3282
3379
  this.jobs.unshift(job);
3283
3380
  this.jobs.length = Math.min(this.jobs.length, HISTORY);
3284
- const report = (phase) => {
3381
+ const report2 = (phase) => {
3285
3382
  if (job.status === "running") job.phase = phase;
3286
3383
  };
3287
- this.settled.set(job, work(report).then(
3384
+ const note = (diag) => {
3385
+ job.diag = diag;
3386
+ };
3387
+ this.settled.set(job, work(report2, note).then(
3288
3388
  (result) => {
3289
3389
  job.status = "done";
3290
3390
  job.result = result;
@@ -3328,9 +3428,477 @@ var JobRunner = class {
3328
3428
  }
3329
3429
  };
3330
3430
 
3431
+ // src/shared/product-analytics.ts
3432
+ var PRODUCT_EVENTS = [
3433
+ "account_created",
3434
+ "item.created",
3435
+ "item.downloaded",
3436
+ "item.copied",
3437
+ "item.shared",
3438
+ "item.pinned",
3439
+ "item.unpinned",
3440
+ "item.deleted",
3441
+ "item.departed",
3442
+ "channel_shared",
3443
+ "channel_mode_set",
3444
+ "invite_created",
3445
+ "invite_link_copied",
3446
+ "invite_qr_shown",
3447
+ "invite_system_sheet_opened",
3448
+ "invite_joined",
3449
+ "invite_converted",
3450
+ "channel.invite_preview",
3451
+ "channel.invite_accept",
3452
+ "channel.invite_signin",
3453
+ "enterprise.invite_preview",
3454
+ "enterprise.invite_identity",
3455
+ "enterprise.invite_signin",
3456
+ "enterprise.invite_account_switch",
3457
+ "enterprise.invite_attach",
3458
+ "enterprise.invite_accept",
3459
+ "enterprise.invite_admin",
3460
+ "enterprise.workspace_opened",
3461
+ "enterprise.workspace_switched",
3462
+ "enterprise.channel_created",
3463
+ "enterprise.channel_members_added",
3464
+ "share_link_copied",
3465
+ "share_qr_shown",
3466
+ "share_system_sheet_opened",
3467
+ "share_opened",
3468
+ "share_content_copied",
3469
+ "share_downloaded",
3470
+ "share_signin_click",
3471
+ "share.saved",
3472
+ "share_bar_on",
3473
+ "share_bar_off",
3474
+ "upload.phase_timing",
3475
+ "auth.signin_started",
3476
+ "auth.signin_result",
3477
+ "auth.session_started",
3478
+ "auth.signed_out",
3479
+ "auth.session_lost",
3480
+ "auth.token_refresh_failed",
3481
+ "account_switch.started",
3482
+ "account_switch.completed",
3483
+ "account_switch.failed",
3484
+ "bootstrap.result",
3485
+ "direct_mode_set",
3486
+ "direct.offered",
3487
+ "direct.claimed",
3488
+ "direct.connected",
3489
+ "direct.completed",
3490
+ "direct.received",
3491
+ "direct.failed",
3492
+ "direct.fallback_started",
3493
+ "direct.fallback_ready",
3494
+ "direct.fallback_received",
3495
+ "direct.fallback_failed",
3496
+ "direct.fallback_aborted",
3497
+ "direct.too_big",
3498
+ "agent.pair_opened",
3499
+ "agent.pair_approved",
3500
+ "agent.pair_refused",
3501
+ "agent.paired",
3502
+ "agent.revoked",
3503
+ "agent.send",
3504
+ "agent.tool_call"
3505
+ ];
3506
+ var SERVER_OWNED_PRODUCT_EVENTS = [
3507
+ "account_created",
3508
+ "item.departed",
3509
+ "invite_created",
3510
+ "invite_joined",
3511
+ "invite_converted",
3512
+ "share_opened",
3513
+ "share_content_copied",
3514
+ "share_downloaded",
3515
+ "share_signin_click",
3516
+ "agent.send",
3517
+ "agent.tool_call"
3518
+ ];
3519
+ var SERVER_OWNED_PRODUCT_EVENT_SET = new Set(SERVER_OWNED_PRODUCT_EVENTS);
3520
+ var CLIENT_PRODUCT_EVENTS = PRODUCT_EVENTS.filter(
3521
+ (event) => !SERVER_OWNED_PRODUCT_EVENT_SET.has(event)
3522
+ );
3523
+ var bool = { type: "boolean" };
3524
+ var count = { type: "number", min: 0 };
3525
+ var oneOf = (...values) => ({ type: "string", values: new Set(values) });
3526
+ var sizeBucket = oneOf("lt_1mb", "1_10mb", "10_100mb", "100_512mb", "512mb_5gb", "gt_5gb");
3527
+ var itemKind = oneOf("file", "text");
3528
+ var itemKindOrUnknown = oneOf("file", "text", "unknown");
3529
+ var role = oneOf("sender", "receiver");
3530
+ var candidateType = oneOf("", "host", "srflx", "relay", "prflx", "other");
3531
+ var enterpriseRole = oneOf("owner", "admin", "billing_admin", "member", "guest");
3532
+ var agentKind = oneOf("claude_code", "codex", "other");
3533
+ var pairHandoff = oneOf("loopback", "manual", "unknown");
3534
+ var AGENT_TOOL_NAMES = [
3535
+ "status",
3536
+ "pair",
3537
+ "send_file",
3538
+ "send_note",
3539
+ "send_direct",
3540
+ "send_direct_fallback",
3541
+ "receive_direct",
3542
+ "receive_direct_fallback",
3543
+ "list_items",
3544
+ "get_item",
3545
+ "jobs"
3546
+ ];
3547
+ var AGENT_ERROR_CODES = [
3548
+ "none",
3549
+ "other",
3550
+ "identity_corrupt",
3551
+ "not_paired",
3552
+ "internal",
3553
+ "agent_forbidden",
3554
+ "agent_revoked",
3555
+ "grant_missing",
3556
+ "not_found",
3557
+ "invalid_cap",
3558
+ "write_failed",
3559
+ "send_forbidden",
3560
+ "read_forbidden",
3561
+ "direct_mode",
3562
+ "not_direct_mode",
3563
+ "not_claimed",
3564
+ "no_offer",
3565
+ "offer_taken",
3566
+ "direct_cancelled",
3567
+ "direct_failed",
3568
+ "direct_not_failed",
3569
+ "file_changed",
3570
+ "webrtc_unavailable",
3571
+ "fallback_unavailable",
3572
+ "key_stale",
3573
+ "quota_exceeded",
3574
+ "rate_limited",
3575
+ "file_too_big",
3576
+ "duplicate",
3577
+ "pairing_expired",
3578
+ "pairing_cancelled",
3579
+ "claim_mismatch",
3580
+ "pairing_not_approved",
3581
+ "pairing_claimed",
3582
+ "agent_limit",
3583
+ "grant_limit",
3584
+ "feature_disabled",
3585
+ "upload_failed",
3586
+ "oprf_failed",
3587
+ "network",
3588
+ "sign_in_failed",
3589
+ "bad_signature",
3590
+ "missing_token"
3591
+ ];
3592
+ function agentDurationBucket(ms) {
3593
+ if (ms < 1e3) return "lt_1s";
3594
+ if (ms < 1e4) return "1_10s";
3595
+ if (ms < 6e4) return "10_60s";
3596
+ return "gt_60s";
3597
+ }
3598
+ var EVENT_PROPERTY_RULES = {
3599
+ account_created: {},
3600
+ "item.created": { kind: itemKind, size_bytes: count, size_bucket: sizeBucket },
3601
+ "item.downloaded": { size_bytes: count, size_bucket: sizeBucket },
3602
+ "item.copied": {},
3603
+ "item.shared": { kind: itemKind },
3604
+ "item.pinned": { age_ms: count },
3605
+ "item.unpinned": {},
3606
+ "item.deleted": { age_ms: count, was_pinned: bool, kind: itemKindOrUnknown },
3607
+ "item.departed": {},
3608
+ channel_shared: {},
3609
+ channel_mode_set: { mode: oneOf("edit", "view") },
3610
+ invite_created: {},
3611
+ invite_link_copied: {},
3612
+ invite_qr_shown: {},
3613
+ invite_system_sheet_opened: {},
3614
+ invite_joined: {},
3615
+ invite_converted: {},
3616
+ "channel.invite_preview": {
3617
+ outcome: oneOf("ready", "unavailable", "missing_key"),
3618
+ signed_in: bool
3619
+ },
3620
+ "channel.invite_accept": {
3621
+ outcome: oneOf("started", "completed", "already_member", "unavailable", "failed")
3622
+ },
3623
+ "channel.invite_signin": {
3624
+ stage: oneOf("link_requested", "link_sent", "link_completed", "failed")
3625
+ },
3626
+ "enterprise.invite_preview": {
3627
+ outcome: oneOf("pending", "resumable", "unavailable"),
3628
+ role: enterpriseRole
3629
+ },
3630
+ "enterprise.invite_identity": {
3631
+ outcome: oneOf("matched", "mismatched", "unavailable", "failed")
3632
+ },
3633
+ "enterprise.invite_signin": {
3634
+ stage: oneOf("link_requested", "link_sent", "link_completed", "failed")
3635
+ },
3636
+ "enterprise.invite_account_switch": { stage: oneOf("started", "completed", "failed") },
3637
+ "enterprise.invite_attach": {
3638
+ stage: oneOf("link_requested", "link_sent", "verify_started", "completed", "address_taken", "link_invalid", "failed")
3639
+ },
3640
+ "enterprise.invite_accept": {
3641
+ outcome: oneOf("started", "completed", "recovered", "timeout", "unavailable", "email_mismatch", "seat_limit", "failed"),
3642
+ role: enterpriseRole,
3643
+ destination: oneOf("workspace", "admin_console"),
3644
+ duration_ms: count
3645
+ },
3646
+ "enterprise.invite_admin": {
3647
+ outcome: oneOf("created", "email_sent", "email_failed", "reissued", "revoked"),
3648
+ role: enterpriseRole,
3649
+ external: bool
3650
+ },
3651
+ "enterprise.workspace_opened": { role: enterpriseRole, first_visit: bool },
3652
+ "enterprise.workspace_switched": { destination: oneOf("personal", "organization"), role: enterpriseRole },
3653
+ "enterprise.channel_created": { member_count: count },
3654
+ "enterprise.channel_members_added": { member_count: count },
3655
+ share_link_copied: {},
3656
+ share_qr_shown: {},
3657
+ share_system_sheet_opened: {},
3658
+ share_opened: {},
3659
+ share_content_copied: {},
3660
+ share_downloaded: {},
3661
+ share_signin_click: {},
3662
+ "share.saved": {},
3663
+ share_bar_on: {},
3664
+ share_bar_off: {},
3665
+ "upload.phase_timing": {
3666
+ stage: oneOf("staging", "bootstrap", "thumbnail", "encrypting", "uploading", "finishing"),
3667
+ duration_ms: count,
3668
+ size_bucket: sizeBucket,
3669
+ outcome: oneOf("success", "failure", "cancelled"),
3670
+ attempt: { type: "number", min: 1 }
3671
+ },
3672
+ "auth.signin_started": { provider: oneOf("google.com", "apple.com") },
3673
+ "auth.signin_result": {
3674
+ provider: oneOf("google.com", "apple.com"),
3675
+ outcome: oneOf("signed_in", "cancelled", "taken", "failed", "needs_link"),
3676
+ flow: oneOf("signin", "link", "connect")
3677
+ },
3678
+ "account_switch.started": { operation: oneOf("remember", "switch"), provider: oneOf("google.com", "apple.com", "email_link", "qr", "code") },
3679
+ "account_switch.completed": { operation: oneOf("remember", "switch"), provider: oneOf("google.com", "apple.com", "email_link", "qr", "code") },
3680
+ "account_switch.failed": { operation: oneOf("remember", "switch"), provider: oneOf("google.com", "apple.com", "email_link", "qr", "code") },
3681
+ "auth.session_started": { cold: bool },
3682
+ "auth.signed_out": { reason: oneOf("user", "account_switch", "abandoned_new_account", "account_deleted") },
3683
+ "auth.session_lost": { session_age_ms: count, foreground: bool },
3684
+ "auth.token_refresh_failed": {
3685
+ code: oneOf("network", "token_invalid", "user_disabled", "user_missing", "rate_limited", "internal", "unknown")
3686
+ },
3687
+ "bootstrap.result": {
3688
+ outcome: oneOf("offline", "ok", "timeout_warm", "timeout", "failed_warm", "failed"),
3689
+ stage: oneOf("cached", "done", "unknown", "keyring", "account_init", "listeners"),
3690
+ duration_ms: count,
3691
+ fault: oneOf("intercepted", "offline", "unknown")
3692
+ },
3693
+ direct_mode_set: { on: bool },
3694
+ "direct.offered": { size_bucket: sizeBucket },
3695
+ // `via_agent`: the offer came from a paired agent's process, not from one
3696
+ // of the person's devices. The claim and the two receiver outcomes carry it.
3697
+ "direct.claimed": { size_bucket: sizeBucket, via_agent: bool },
3698
+ "direct.connected": { role, ms: count },
3699
+ "direct.completed": {
3700
+ via: oneOf("lan", "wan", "relay", "unknown"),
3701
+ pair_local: candidateType,
3702
+ pair_remote: candidateType,
3703
+ connect_ms: count,
3704
+ duration_ms: count,
3705
+ size_bucket: sizeBucket,
3706
+ turn_urls_supplied: count,
3707
+ turn_urls_configured: count,
3708
+ restarts: count
3709
+ },
3710
+ "direct.received": {
3711
+ via_agent: bool,
3712
+ via: oneOf("lan", "wan", "relay", "unknown"),
3713
+ pair_local: candidateType,
3714
+ pair_remote: candidateType,
3715
+ connect_ms: count,
3716
+ duration_ms: count,
3717
+ size_bucket: sizeBucket,
3718
+ turn_urls_supplied: count,
3719
+ turn_urls_configured: count,
3720
+ restarts: count
3721
+ },
3722
+ "direct.failed": {
3723
+ role,
3724
+ via_agent: bool,
3725
+ reason: oneOf(
3726
+ "peer_silent",
3727
+ "connect_timeout",
3728
+ "ice_failed",
3729
+ "disconnected",
3730
+ "peer_closed",
3731
+ "peer_abort",
3732
+ "signaling",
3733
+ "signal_send",
3734
+ "protocol",
3735
+ "sink",
3736
+ "transfer",
3737
+ "integrity",
3738
+ "too_big",
3739
+ "unknown"
3740
+ ),
3741
+ ms: count,
3742
+ ice_state: oneOf("", "new", "checking", "connected", "completed", "failed", "disconnected", "closed"),
3743
+ gather_state: oneOf("", "new", "gathering", "complete"),
3744
+ had_remote: bool,
3745
+ local_host: count,
3746
+ local_srflx: count,
3747
+ local_relay: count,
3748
+ remote_host: count,
3749
+ remote_srflx: count,
3750
+ remote_relay: count,
3751
+ turn_urls_supplied: count,
3752
+ turn_urls_configured: count,
3753
+ moved_bucket: sizeBucket,
3754
+ size_bucket: sizeBucket,
3755
+ restarts: count,
3756
+ // Whether the reporting tab was on screen when the run died — the lead a
3757
+ // slept machine or a frozen background tab leaves behind.
3758
+ visibility: oneOf("visible", "hidden", "prerender")
3759
+ },
3760
+ "direct.fallback_started": { size_bucket: sizeBucket },
3761
+ "direct.fallback_ready": { size_bucket: sizeBucket, duration_ms: count, retries: count },
3762
+ "direct.fallback_received": { size_bucket: sizeBucket, duration_ms: count, retries: count },
3763
+ "direct.fallback_failed": {
3764
+ stage: oneOf("upload", "download"),
3765
+ size_bucket: sizeBucket,
3766
+ retries: count
3767
+ },
3768
+ "direct.fallback_aborted": {},
3769
+ "direct.too_big": { size_bucket: sizeBucket },
3770
+ "agent.pair_opened": { agent_kind: agentKind, protocol: count, replaces: bool },
3771
+ "agent.pair_approved": {
3772
+ agent_kind: agentKind,
3773
+ destination: oneOf("new_channel", "existing"),
3774
+ channels: count,
3775
+ handoff: pairHandoff
3776
+ },
3777
+ "agent.pair_refused": {
3778
+ agent_kind: agentKind,
3779
+ step: oneOf("open", "approve", "claim"),
3780
+ reason: oneOf(
3781
+ "agent_limit",
3782
+ "grant_limit",
3783
+ "channel_limit",
3784
+ "code_mismatch",
3785
+ "pairing_expired",
3786
+ "pairing_cancelled",
3787
+ "pairing_claimed",
3788
+ "feature_disabled",
3789
+ "rate_limited",
3790
+ "network",
3791
+ "other"
3792
+ )
3793
+ },
3794
+ "agent.paired": {
3795
+ agent_kind: agentKind,
3796
+ destination: oneOf("new_channel", "existing"),
3797
+ channels: count,
3798
+ handoff: pairHandoff,
3799
+ protocol: count
3800
+ },
3801
+ "agent.revoked": { agent_kind: agentKind },
3802
+ "agent.send": { size_bucket: sizeBucket, agent_kind: agentKind },
3803
+ // What the CLI reports after every tool call: the tool, whether it worked,
3804
+ // the code when it did not, and the version that answered. No path, no file
3805
+ // name, no channel, no sentence.
3806
+ "agent.tool_call": {
3807
+ tool: oneOf(...AGENT_TOOL_NAMES),
3808
+ result: oneOf("ok", "error"),
3809
+ code: oneOf(...AGENT_ERROR_CODES),
3810
+ agent_kind: agentKind,
3811
+ duration_bucket: oneOf("lt_1s", "1_10s", "10_60s", "gt_60s"),
3812
+ version_major: count,
3813
+ version_minor: count,
3814
+ version_patch: count
3815
+ }
3816
+ };
3817
+ var platformRule = oneOf("web", "android", "ios", "agent", "unknown");
3818
+ var PERSON_PROPERTY_RULES = {
3819
+ plan: oneOf("anon", "free", "pro", "max"),
3820
+ locale: oneOf("es-AR", "pt-BR", "en"),
3821
+ storage_bytes: count
3822
+ };
3823
+
3824
+ // src/telemetry.ts
3825
+ var OFF_WORDS = /* @__PURE__ */ new Set(["0", "off", "false", "no"]);
3826
+ var ON_WORDS = /* @__PURE__ */ new Set(["1", "on", "true", "yes"]);
3827
+ function telemetryState() {
3828
+ const env = (process.env.ZAS_AGENT_TELEMETRY ?? "").toLowerCase();
3829
+ if (OFF_WORDS.has(env)) return { on: false, source: "env" };
3830
+ if (ON_WORDS.has(env)) return { on: true, source: "env" };
3831
+ if (process.env.DO_NOT_TRACK === "1") return { on: false, source: "do_not_track" };
3832
+ const stored = loadSettings();
3833
+ if (stored && typeof stored.telemetry === "boolean") return { on: stored.telemetry, source: "file" };
3834
+ return { on: true, source: "default" };
3835
+ }
3836
+ function setTelemetry(on) {
3837
+ const stored = loadSettings();
3838
+ saveSettings({ ...stored ?? {}, version: 1, telemetry: on, decided_at: Date.now() });
3839
+ }
3840
+ function noticeShown() {
3841
+ return typeof loadSettings()?.notified_at === "number";
3842
+ }
3843
+ function markNoticeShown() {
3844
+ const stored = loadSettings();
3845
+ saveSettings({ ...stored ?? {}, version: 1, notified_at: Date.now() });
3846
+ }
3847
+ var TELEMETRY_NOTICE = [
3848
+ "Zas collects usage data from this agent to improve the product: which tool",
3849
+ "ran, whether it worked, the error code when it did not, how long it took,",
3850
+ "and the version. Never file names, file contents, paths or channel names.",
3851
+ "The data is linked to your Zas account.",
3852
+ "Turn it off with: npx -y zas-agent telemetry off"
3853
+ ].join("\n");
3854
+ var SOURCE_WORDS = {
3855
+ env: "ZAS_AGENT_TELEMETRY",
3856
+ do_not_track: "DO_NOT_TRACK",
3857
+ file: "zas-agent telemetry",
3858
+ default: "default"
3859
+ };
3860
+ function telemetryLine() {
3861
+ const state = telemetryState();
3862
+ return `telemetry: ${state.on ? "on" : "off"} (${SOURCE_WORDS[state.source]})`;
3863
+ }
3864
+ var TOOL_NAMES = new Set(AGENT_TOOL_NAMES);
3865
+ function versionParts(version) {
3866
+ const [major = 0, minor = 0, patch = 0] = version.split("-")[0].split(".").map((part) => {
3867
+ const value = Number.parseInt(part, 10);
3868
+ return Number.isFinite(value) && value >= 0 ? value : 0;
3869
+ });
3870
+ return { major, minor, patch };
3871
+ }
3872
+ function toolCallProperties(input) {
3873
+ const tool = input.tool.startsWith("zas_") ? input.tool.slice(4) : input.tool;
3874
+ if (!TOOL_NAMES.has(tool)) return null;
3875
+ const version = versionParts(input.version);
3876
+ return {
3877
+ tool,
3878
+ result: input.code === null ? "ok" : "error",
3879
+ // `other` is the server's word for a code outside the vocabulary. The
3880
+ // shared list is the one that decides, and it drops anything else.
3881
+ code: input.code ?? "none",
3882
+ duration_bucket: agentDurationBucket(input.ms),
3883
+ version_major: version.major,
3884
+ version_minor: version.minor,
3885
+ version_patch: version.patch
3886
+ };
3887
+ }
3888
+ var REPORT_TIMEOUT_MS = 5e3;
3889
+ async function report(client, properties) {
3890
+ if (properties === null || !telemetryState().on) return;
3891
+ const timeout = new Promise((resolve2) => {
3892
+ const timer = setTimeout(resolve2, REPORT_TIMEOUT_MS);
3893
+ timer.unref?.();
3894
+ });
3895
+ const sent = client.api("POST", "/agents/telemetry", { event: "agent.tool_call", properties }).then(() => void 0).catch(() => void 0);
3896
+ await Promise.race([sent, timeout]);
3897
+ }
3898
+
3331
3899
  // src/server.ts
3332
3900
  function agentVersion() {
3333
- return true ? "0.5.1" : "0.0.0-dev";
3901
+ return true ? "0.6.1" : "0.0.0-dev";
3334
3902
  }
3335
3903
  var AGENT_CUE = " The owner sees every item this agent sends with the >_ agent mark and this agent's name, on every device.";
3336
3904
  var PAIR_ANNOUNCE_MS = 15e3;
@@ -3344,9 +3912,33 @@ function rightsOf(grant) {
3344
3912
  if (grant.read) rights.push(grant.direct_mode ? "receive (Directo)" : "read");
3345
3913
  return rights.length > 0 ? rights.join(" \xB7 ") : "no access";
3346
3914
  }
3915
+ function answeredCode(answer) {
3916
+ if (!answer?.isError) return null;
3917
+ const match = /^([a-z_]{2,40}):/.exec(answer.content?.[0]?.text ?? "");
3918
+ return match ? match[1] : "other";
3919
+ }
3347
3920
  function buildServer(profile, deps = {}) {
3348
3921
  const server = new McpServer({ name: "zas", version: agentVersion() });
3349
3922
  const runner = deps.runner ?? new JobRunner();
3923
+ const reportCall = async (tool, ms, code) => {
3924
+ try {
3925
+ if (!(deps.identity ?? loadIdentity(profile))) return;
3926
+ await report(ctx().client, toolCallProperties({ tool, ms, code, version: agentVersion() }));
3927
+ } catch {
3928
+ }
3929
+ };
3930
+ const registerRaw = server.registerTool.bind(server);
3931
+ server.registerTool = (name, meta, handler) => registerRaw(name, meta, async (...args) => {
3932
+ const started = Date.now();
3933
+ try {
3934
+ const answer = await handler(...args);
3935
+ void reportCall(name, Date.now() - started, answeredCode(answer));
3936
+ return answer;
3937
+ } catch (e) {
3938
+ void reportCall(name, Date.now() - started, e instanceof ZasError ? e.code : "internal");
3939
+ throw e;
3940
+ }
3941
+ });
3350
3942
  let client = null;
3351
3943
  const ctx = () => {
3352
3944
  const identity = deps.identity ?? loadIdentity(profile);
@@ -3366,10 +3958,17 @@ function buildServer(profile, deps = {}) {
3366
3958
  return { isError: true, ...text2(`${err.code}: ${humanSentence(err)}`) };
3367
3959
  };
3368
3960
  const settled = (job) => {
3369
- if (job.status === "done") return text2(job.result);
3961
+ if (job.status === "done") {
3962
+ return job.diag ? text2({ ...job.result, diag: formatDirectDiagLines(job.diag) }) : text2(job.result);
3963
+ }
3370
3964
  if (job.status === "failed") {
3371
3965
  const e = job.error;
3372
- return failed(e ? new ZasError(e.code, e.status, e.message, e.retryAfterMs, e.serverCode) : new ZasError("internal", 0));
3966
+ const answer = failed(e ? new ZasError(e.code, e.status, e.message, e.retryAfterMs, e.serverCode) : new ZasError("internal", 0));
3967
+ if (!job.diag) return answer;
3968
+ const said = answer.content[0].text;
3969
+ return { isError: true, ...text2(`${said}
3970
+
3971
+ ${formatDirectDiagLines(job.diag).join("\n")}`) };
3373
3972
  }
3374
3973
  return text2({ job_id: job.id, status: "running", phase: job.phase });
3375
3974
  };
@@ -3394,7 +3993,8 @@ function buildServer(profile, deps = {}) {
3394
3993
  `Paired as \u201C${c.identity.name}\u201D (${c.identity.kind}) with account ${c.identity.owner_uid}.`,
3395
3994
  grants.length > 0 ? "Channels:" : "No channels: the owner has not granted access to any yet.",
3396
3995
  ...lines,
3397
- `${packageName()} ${agentVersion()} \xB7 profile ${profile}`
3996
+ `${packageName()} ${agentVersion()} \xB7 profile ${profile}`,
3997
+ `${telemetryLine()} \xB7 change it with \u201Cnpx -y ${packageName()} telemetry off\u201D`
3398
3998
  ].join("\n"));
3399
3999
  } catch (e) {
3400
4000
  return failed(e);
@@ -3488,7 +4088,7 @@ function buildServer(profile, deps = {}) {
3488
4088
  "file",
3489
4089
  input.title ?? input.path,
3490
4090
  input.channel ?? "",
3491
- (report) => sendFile(c, input, report)
4091
+ (report2) => sendFile(c, input, report2)
3492
4092
  );
3493
4093
  return settled(await runner.wait(job));
3494
4094
  } catch (e) {
@@ -3514,9 +4114,10 @@ function buildServer(profile, deps = {}) {
3514
4114
  "direct",
3515
4115
  input.path,
3516
4116
  input.channel ?? "",
3517
- (report) => sendDirect(c, input, report, {
4117
+ (report2, note) => sendDirect(c, input, report2, {
3518
4118
  ...deps.direct ?? {},
3519
- onFailed: (record) => rememberFailed(job.id, record)
4119
+ onFailed: (record) => rememberFailed(job.id, record),
4120
+ onDiag: note
3520
4121
  })
3521
4122
  );
3522
4123
  return settled(await runner.wait(job));
@@ -3538,7 +4139,7 @@ function buildServer(profile, deps = {}) {
3538
4139
  "fallback",
3539
4140
  record.name,
3540
4141
  record.channel_name,
3541
- (report) => sendDirectFallback(c, record, report, deps.direct)
4142
+ (report2) => sendDirectFallback(c, record, report2, deps.direct)
3542
4143
  );
3543
4144
  const outcome = await runner.wait(job);
3544
4145
  if (outcome.status === "done") failedDirects.delete(input.job);
@@ -3566,9 +4167,10 @@ function buildServer(profile, deps = {}) {
3566
4167
  "receive",
3567
4168
  input.dest ?? "Directo",
3568
4169
  input.channel ?? "",
3569
- (report) => receiveDirect(c, input, report, {
4170
+ (report2, note) => receiveDirect(c, input, report2, {
3570
4171
  ...deps.receive ?? {},
3571
- onFailed: (record) => rememberFailedReceive(job.id, record)
4172
+ onFailed: (record) => rememberFailedReceive(job.id, record),
4173
+ onDiag: note
3572
4174
  })
3573
4175
  );
3574
4176
  return settled(await runner.wait(job));
@@ -3590,7 +4192,7 @@ function buildServer(profile, deps = {}) {
3590
4192
  "fallback",
3591
4193
  record.meta.name,
3592
4194
  record.channel_name,
3593
- (report) => receiveDirectFallback(c, record, report, deps.receive)
4195
+ (report2) => receiveDirectFallback(c, record, report2, deps.receive)
3594
4196
  );
3595
4197
  const outcome = await runner.wait(job);
3596
4198
  if (outcome.status === "done") failedReceives.delete(input.job);
@@ -3667,7 +4269,16 @@ function parseArgs(argv) {
3667
4269
  const inline = arg.startsWith("--") && eq > 0 ? arg.slice(eq + 1) : void 0;
3668
4270
  const take = () => inline !== void 0 ? inline : argv[++i] ?? "";
3669
4271
  if (name === "pair") parsed.command = "pair";
3670
- else if (name === "--version" || name === "-v") parsed.command = "version";
4272
+ else if (name === "telemetry") {
4273
+ parsed.command = "telemetry";
4274
+ const next = argv[i + 1];
4275
+ if (next === "on" || next === "off") {
4276
+ parsed.telemetry = next;
4277
+ i++;
4278
+ } else if (next !== void 0 && !next.startsWith("--")) {
4279
+ return { ...parsed, command: "invalid", message: `Unknown telemetry setting \u201C${next}\u201D. Use \u201Con\u201D or \u201Coff\u201D.` };
4280
+ }
4281
+ } else if (name === "--version" || name === "-v") parsed.command = "version";
3671
4282
  else if (name === "--help" || name === "-h") parsed.command = "help";
3672
4283
  else if (name === "--profile") {
3673
4284
  parsed.profile = take() || DEFAULT_PROFILE;
@@ -3690,6 +4301,7 @@ var USAGE = [
3690
4301
  " zas-agent [--profile <name>] serve the MCP tools over stdio",
3691
4302
  " zas-agent pair [--profile <name>] pair this agent with a Zas account",
3692
4303
  " [--kind claude_code|codex|other] [--host <name>] [--no-open]",
4304
+ " zas-agent telemetry [on|off] say what this machine reports, or change it",
3693
4305
  " zas-agent --version"
3694
4306
  ].join("\n");
3695
4307
  async function main(argv, log = (l) => console.error(l)) {
@@ -3707,7 +4319,17 @@ async function main(argv, log = (l) => console.error(l)) {
3707
4319
  log(USAGE);
3708
4320
  return args.unknown ? 2 : 0;
3709
4321
  }
4322
+ if (args.command === "telemetry") {
4323
+ if (args.telemetry) setTelemetry(args.telemetry === "on");
4324
+ markNoticeShown();
4325
+ log(telemetryLine());
4326
+ log(args.telemetry === "off" ? `Nothing is sent. Turn it on again with: npx -y ${packageName()} telemetry on` : TELEMETRY_NOTICE);
4327
+ return 0;
4328
+ }
3710
4329
  if (args.command === "pair") {
4330
+ log(TELEMETRY_NOTICE);
4331
+ log("");
4332
+ markNoticeShown();
3711
4333
  let rl = null;
3712
4334
  const askCode = process.stdin.isTTY ? () => {
3713
4335
  rl ??= createInterface({ input: process.stdin, output: process.stderr });
@@ -3736,6 +4358,10 @@ async function main(argv, log = (l) => console.error(l)) {
3736
4358
  }
3737
4359
  return 0;
3738
4360
  }
4361
+ if (!noticeShown()) {
4362
+ log(TELEMETRY_NOTICE);
4363
+ markNoticeShown();
4364
+ }
3739
4365
  await buildServer(args.profile).connect(new StdioServerTransport());
3740
4366
  return 0;
3741
4367
  }