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 +27 -0
- package/README.md +42 -3
- package/dist/cli.js +566 -38
- package/package.json +1 -1
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
|
|
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
|
|
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 =
|
|
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
|
|
1485
|
-
const isRestart =
|
|
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
|
|
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
|
|
1643
|
-
const signed = await options.getUrls(first,
|
|
1644
|
-
if (signed.length !==
|
|
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
|
-
|
|
1672
|
+
report2();
|
|
1662
1673
|
try {
|
|
1663
1674
|
etag = await options.put(entry.url, cipher, signal, (loaded) => {
|
|
1664
1675
|
partProgress.set(partNumber, loaded);
|
|
1665
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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")
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3292
|
+
const report2 = (phase) => {
|
|
3282
3293
|
if (job.status === "running") job.phase = phase;
|
|
3283
3294
|
};
|
|
3284
|
-
this.settled.set(job, work(
|
|
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.
|
|
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
|
-
(
|
|
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
|
-
(
|
|
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
|
-
(
|
|
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
|
-
(
|
|
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
|
-
(
|
|
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 === "
|
|
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