zas-agent 0.5.1 → 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 +17 -0
- package/README.md +42 -3
- package/dist/cli.js +560 -35
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,23 @@ 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
|
+
|
|
10
27
|
## [0.5.1] - 2026-09-03
|
|
11
28
|
|
|
12
29
|
### Fixed
|
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",
|
|
@@ -1636,15 +1644,15 @@ async function uploadFallback(options) {
|
|
|
1636
1644
|
const key = await aesKey(meta);
|
|
1637
1645
|
const completed = [];
|
|
1638
1646
|
const partProgress = /* @__PURE__ */ new Map();
|
|
1639
|
-
const
|
|
1647
|
+
const report2 = () => options.onProgress?.(
|
|
1640
1648
|
Math.min(meta.cipher_size, [...partProgress.values()].reduce((sum, value) => sum + value, 0)),
|
|
1641
1649
|
meta.cipher_size
|
|
1642
1650
|
);
|
|
1643
1651
|
for (let first = 1; first <= meta.part_count; first += URL_BATCH) {
|
|
1644
1652
|
aborted(signal);
|
|
1645
|
-
const
|
|
1646
|
-
const signed = await options.getUrls(first,
|
|
1647
|
-
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");
|
|
1648
1656
|
let cursor = 0;
|
|
1649
1657
|
const workers = Array.from({ length: Math.min(CONCURRENCY, signed.length) }, async () => {
|
|
1650
1658
|
for (; ; ) {
|
|
@@ -1661,11 +1669,11 @@ async function uploadFallback(options) {
|
|
|
1661
1669
|
for (let attempt = 0; attempt < RETRIES; attempt++) {
|
|
1662
1670
|
aborted(signal);
|
|
1663
1671
|
partProgress.set(partNumber, 0);
|
|
1664
|
-
|
|
1672
|
+
report2();
|
|
1665
1673
|
try {
|
|
1666
1674
|
etag = await options.put(entry.url, cipher, signal, (loaded) => {
|
|
1667
1675
|
partProgress.set(partNumber, loaded);
|
|
1668
|
-
|
|
1676
|
+
report2();
|
|
1669
1677
|
});
|
|
1670
1678
|
break;
|
|
1671
1679
|
} catch (error) {
|
|
@@ -2472,7 +2480,7 @@ function sealer(key, keyVersion) {
|
|
|
2472
2480
|
open: (enc) => JSON.parse(new TextDecoder().decode(openRaw(key, b64ToBytes(enc))))
|
|
2473
2481
|
};
|
|
2474
2482
|
}
|
|
2475
|
-
async function sendDirect(ctx, input,
|
|
2483
|
+
async function sendDirect(ctx, input, report2, deps = {}) {
|
|
2476
2484
|
const now = deps.now ?? (() => Date.now());
|
|
2477
2485
|
const sleep2 = deps.sleep ?? defaultSleep2;
|
|
2478
2486
|
const grant = await directGrantFor(ctx, input.channel);
|
|
@@ -2489,7 +2497,7 @@ async function sendDirect(ctx, input, report, deps = {}) {
|
|
|
2489
2497
|
const { seal, open } = sealer(key, grant.key_version);
|
|
2490
2498
|
const stamp = { device, owner_uid: owner };
|
|
2491
2499
|
const startedAt = now();
|
|
2492
|
-
|
|
2500
|
+
report2("offer");
|
|
2493
2501
|
const meta = { name, size: file.size, mime: mimeFor(input.path) };
|
|
2494
2502
|
const { id } = await ctx.client.api("POST", `/direct/${cid}`, {
|
|
2495
2503
|
meta_enc: seal(meta),
|
|
@@ -2523,7 +2531,7 @@ async function sendDirect(ctx, input, report, deps = {}) {
|
|
|
2523
2531
|
}
|
|
2524
2532
|
};
|
|
2525
2533
|
const turn = await fetchIce();
|
|
2526
|
-
|
|
2534
|
+
report2("connecting");
|
|
2527
2535
|
let resolveOutcome;
|
|
2528
2536
|
const outcome = new Promise((resolve2) => {
|
|
2529
2537
|
resolveOutcome = resolve2;
|
|
@@ -2540,7 +2548,7 @@ async function sendDirect(ctx, input, report, deps = {}) {
|
|
|
2540
2548
|
send: (msg) => ctx.client.api("POST", `${route}/signal`, { payload_enc: seal(msg), for: "receiver", ...stamp }).then(() => void 0),
|
|
2541
2549
|
onPhase: (phase2) => {
|
|
2542
2550
|
if (phase2 === "flight") {
|
|
2543
|
-
|
|
2551
|
+
report2("flight");
|
|
2544
2552
|
heartbeat ??= setInterval(() => {
|
|
2545
2553
|
void ctx.client.api("POST", `${route}/heartbeat`, stamp).catch(() => void 0);
|
|
2546
2554
|
}, deps.heartbeatMs ?? HEARTBEAT_MS);
|
|
@@ -2580,7 +2588,7 @@ async function sendDirect(ctx, input, report, deps = {}) {
|
|
|
2580
2588
|
over = true;
|
|
2581
2589
|
clearInterval(heartbeat);
|
|
2582
2590
|
await pump;
|
|
2583
|
-
|
|
2591
|
+
report2("finishing");
|
|
2584
2592
|
if (phase === "done") {
|
|
2585
2593
|
await setState("done");
|
|
2586
2594
|
return {
|
|
@@ -2619,7 +2627,7 @@ var fetchPut = async (url, bytes, signal, onLoaded) => {
|
|
|
2619
2627
|
onLoaded(bytes.length);
|
|
2620
2628
|
return etag;
|
|
2621
2629
|
};
|
|
2622
|
-
async function sendDirectFallback(ctx, record,
|
|
2630
|
+
async function sendDirectFallback(ctx, record, report2, deps = {}) {
|
|
2623
2631
|
const now = deps.now ?? (() => Date.now());
|
|
2624
2632
|
const startedAt = now();
|
|
2625
2633
|
const opened = await (deps.openFile ?? openAsBlob)(record.path).catch(() => {
|
|
@@ -2630,7 +2638,7 @@ async function sendDirectFallback(ctx, record, report, deps = {}) {
|
|
|
2630
2638
|
const { seal } = sealer(record.key, record.key_version);
|
|
2631
2639
|
const stamp = { device: record.device, owner_uid: record.owner_uid };
|
|
2632
2640
|
const base = `/direct/${record.channel_id}/${record.offer_id}/fallback`;
|
|
2633
|
-
|
|
2641
|
+
report2("encrypting");
|
|
2634
2642
|
const meta = createFallbackMeta(file);
|
|
2635
2643
|
await ctx.client.api("POST", `${base}/start`, {
|
|
2636
2644
|
meta_enc: seal(meta),
|
|
@@ -2639,7 +2647,7 @@ async function sendDirectFallback(ctx, record, report, deps = {}) {
|
|
|
2639
2647
|
part_count: meta.part_count,
|
|
2640
2648
|
...stamp
|
|
2641
2649
|
});
|
|
2642
|
-
|
|
2650
|
+
report2("uploading");
|
|
2643
2651
|
let parts;
|
|
2644
2652
|
try {
|
|
2645
2653
|
parts = await uploadFallback({
|
|
@@ -2647,11 +2655,11 @@ async function sendDirectFallback(ctx, record, report, deps = {}) {
|
|
|
2647
2655
|
offerId: record.offer_id,
|
|
2648
2656
|
meta,
|
|
2649
2657
|
put: deps.put ?? fetchPut,
|
|
2650
|
-
getUrls: async (from,
|
|
2658
|
+
getUrls: async (from, count2) => {
|
|
2651
2659
|
const r = await ctx.client.api(
|
|
2652
2660
|
"POST",
|
|
2653
2661
|
`${base}/parts`,
|
|
2654
|
-
{ from, count, ...stamp }
|
|
2662
|
+
{ from, count: count2, ...stamp }
|
|
2655
2663
|
);
|
|
2656
2664
|
return r.urls;
|
|
2657
2665
|
}
|
|
@@ -2661,7 +2669,7 @@ async function sendDirectFallback(ctx, record, report, deps = {}) {
|
|
|
2661
2669
|
if (error instanceof ZasError) throw error;
|
|
2662
2670
|
throw new ZasError("upload_failed", 0, error instanceof Error ? error.message : String(error));
|
|
2663
2671
|
}
|
|
2664
|
-
|
|
2672
|
+
report2("finishing");
|
|
2665
2673
|
await ctx.client.api("POST", `${base}/complete`, {
|
|
2666
2674
|
parts: parts.map((part) => ({ part_number: part.partNumber, etag: part.etag })),
|
|
2667
2675
|
...stamp
|
|
@@ -3047,7 +3055,7 @@ var DiskSink = class {
|
|
|
3047
3055
|
return error;
|
|
3048
3056
|
}
|
|
3049
3057
|
};
|
|
3050
|
-
async function receiveDirect(ctx, input,
|
|
3058
|
+
async function receiveDirect(ctx, input, report2, deps = {}) {
|
|
3051
3059
|
const now = deps.now ?? (() => Date.now());
|
|
3052
3060
|
const sleep2 = deps.sleep ?? defaultSleep2;
|
|
3053
3061
|
const grant = await receiveGrantFor(ctx, input.channel);
|
|
@@ -3060,7 +3068,7 @@ async function receiveDirect(ctx, input, report, deps = {}) {
|
|
|
3060
3068
|
const stamp = { device, owner_uid: owner };
|
|
3061
3069
|
const startedAt = now();
|
|
3062
3070
|
const channelPath = `accounts/${owner}/channels/${cid}`;
|
|
3063
|
-
|
|
3071
|
+
report2("waiting");
|
|
3064
3072
|
const waitUntil = startedAt + (deps.offerWaitMs ?? OFFER_WAIT_MS2);
|
|
3065
3073
|
let id;
|
|
3066
3074
|
let meta;
|
|
@@ -3112,7 +3120,7 @@ async function receiveDirect(ctx, input, report, deps = {}) {
|
|
|
3112
3120
|
}
|
|
3113
3121
|
};
|
|
3114
3122
|
const turn = await fetchIce();
|
|
3115
|
-
|
|
3123
|
+
report2("connecting");
|
|
3116
3124
|
const sink = new DiskSink(input.dest, offerId);
|
|
3117
3125
|
let resolveOutcome;
|
|
3118
3126
|
const outcome = new Promise((resolve2) => {
|
|
@@ -3128,7 +3136,7 @@ async function receiveDirect(ctx, input, report, deps = {}) {
|
|
|
3128
3136
|
expectedMeta: offered,
|
|
3129
3137
|
send: (msg) => ctx.client.api("POST", `${route}/signal`, { payload_enc: seal(msg), for: "sender", ...stamp }).then(() => void 0),
|
|
3130
3138
|
onPhase: (phase2) => {
|
|
3131
|
-
if (phase2 === "flight")
|
|
3139
|
+
if (phase2 === "flight") report2("flight");
|
|
3132
3140
|
if (phase2 === "done" || phase2 === "failed") resolveOutcome(phase2);
|
|
3133
3141
|
},
|
|
3134
3142
|
onPath: (p) => {
|
|
@@ -3167,7 +3175,7 @@ async function receiveDirect(ctx, input, report, deps = {}) {
|
|
|
3167
3175
|
const phase = await outcome;
|
|
3168
3176
|
over = true;
|
|
3169
3177
|
await pump;
|
|
3170
|
-
|
|
3178
|
+
report2("finishing");
|
|
3171
3179
|
if (phase === "done") {
|
|
3172
3180
|
await setState("done");
|
|
3173
3181
|
return {
|
|
@@ -3202,7 +3210,7 @@ async function receiveDirect(ctx, input, report, deps = {}) {
|
|
|
3202
3210
|
});
|
|
3203
3211
|
throw new ZasError("direct_failed", 0, reason);
|
|
3204
3212
|
}
|
|
3205
|
-
async function receiveDirectFallback(ctx, record,
|
|
3213
|
+
async function receiveDirectFallback(ctx, record, report2, deps = {}) {
|
|
3206
3214
|
const now = deps.now ?? (() => Date.now());
|
|
3207
3215
|
const startedAt = now();
|
|
3208
3216
|
const { open } = sealer(record.key, record.key_version);
|
|
@@ -3224,7 +3232,7 @@ async function receiveDirectFallback(ctx, record, report, deps = {}) {
|
|
|
3224
3232
|
throw new ZasError("file_changed", 0);
|
|
3225
3233
|
}
|
|
3226
3234
|
const sink = new DiskSink(record.dest, record.offer_id);
|
|
3227
|
-
|
|
3235
|
+
report2("downloading");
|
|
3228
3236
|
try {
|
|
3229
3237
|
sink.open(meta);
|
|
3230
3238
|
await downloadFallback({
|
|
@@ -3240,7 +3248,7 @@ async function receiveDirectFallback(ctx, record, report, deps = {}) {
|
|
|
3240
3248
|
if (error instanceof ZasError) throw error;
|
|
3241
3249
|
throw new ZasError("network", 0, error instanceof Error ? error.message : String(error));
|
|
3242
3250
|
}
|
|
3243
|
-
|
|
3251
|
+
report2("finishing");
|
|
3244
3252
|
await ctx.client.api("POST", `${base}/received`, stamp);
|
|
3245
3253
|
return {
|
|
3246
3254
|
offer_id: record.offer_id,
|
|
@@ -3281,10 +3289,10 @@ var JobRunner = class {
|
|
|
3281
3289
|
};
|
|
3282
3290
|
this.jobs.unshift(job);
|
|
3283
3291
|
this.jobs.length = Math.min(this.jobs.length, HISTORY);
|
|
3284
|
-
const
|
|
3292
|
+
const report2 = (phase) => {
|
|
3285
3293
|
if (job.status === "running") job.phase = phase;
|
|
3286
3294
|
};
|
|
3287
|
-
this.settled.set(job, work(
|
|
3295
|
+
this.settled.set(job, work(report2).then(
|
|
3288
3296
|
(result) => {
|
|
3289
3297
|
job.status = "done";
|
|
3290
3298
|
job.result = result;
|
|
@@ -3328,9 +3336,477 @@ var JobRunner = class {
|
|
|
3328
3336
|
}
|
|
3329
3337
|
};
|
|
3330
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
|
+
|
|
3331
3807
|
// src/server.ts
|
|
3332
3808
|
function agentVersion() {
|
|
3333
|
-
return true ? "0.
|
|
3809
|
+
return true ? "0.6.0" : "0.0.0-dev";
|
|
3334
3810
|
}
|
|
3335
3811
|
var AGENT_CUE = " The owner sees every item this agent sends with the >_ agent mark and this agent's name, on every device.";
|
|
3336
3812
|
var PAIR_ANNOUNCE_MS = 15e3;
|
|
@@ -3344,9 +3820,33 @@ function rightsOf(grant) {
|
|
|
3344
3820
|
if (grant.read) rights.push(grant.direct_mode ? "receive (Directo)" : "read");
|
|
3345
3821
|
return rights.length > 0 ? rights.join(" \xB7 ") : "no access";
|
|
3346
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
|
+
}
|
|
3347
3828
|
function buildServer(profile, deps = {}) {
|
|
3348
3829
|
const server = new McpServer({ name: "zas", version: agentVersion() });
|
|
3349
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
|
+
});
|
|
3350
3850
|
let client = null;
|
|
3351
3851
|
const ctx = () => {
|
|
3352
3852
|
const identity = deps.identity ?? loadIdentity(profile);
|
|
@@ -3394,7 +3894,8 @@ function buildServer(profile, deps = {}) {
|
|
|
3394
3894
|
`Paired as \u201C${c.identity.name}\u201D (${c.identity.kind}) with account ${c.identity.owner_uid}.`,
|
|
3395
3895
|
grants.length > 0 ? "Channels:" : "No channels: the owner has not granted access to any yet.",
|
|
3396
3896
|
...lines,
|
|
3397
|
-
`${packageName()} ${agentVersion()} \xB7 profile ${profile}
|
|
3897
|
+
`${packageName()} ${agentVersion()} \xB7 profile ${profile}`,
|
|
3898
|
+
`${telemetryLine()} \xB7 change it with \u201Cnpx -y ${packageName()} telemetry off\u201D`
|
|
3398
3899
|
].join("\n"));
|
|
3399
3900
|
} catch (e) {
|
|
3400
3901
|
return failed(e);
|
|
@@ -3488,7 +3989,7 @@ function buildServer(profile, deps = {}) {
|
|
|
3488
3989
|
"file",
|
|
3489
3990
|
input.title ?? input.path,
|
|
3490
3991
|
input.channel ?? "",
|
|
3491
|
-
(
|
|
3992
|
+
(report2) => sendFile(c, input, report2)
|
|
3492
3993
|
);
|
|
3493
3994
|
return settled(await runner.wait(job));
|
|
3494
3995
|
} catch (e) {
|
|
@@ -3514,7 +4015,7 @@ function buildServer(profile, deps = {}) {
|
|
|
3514
4015
|
"direct",
|
|
3515
4016
|
input.path,
|
|
3516
4017
|
input.channel ?? "",
|
|
3517
|
-
(
|
|
4018
|
+
(report2) => sendDirect(c, input, report2, {
|
|
3518
4019
|
...deps.direct ?? {},
|
|
3519
4020
|
onFailed: (record) => rememberFailed(job.id, record)
|
|
3520
4021
|
})
|
|
@@ -3538,7 +4039,7 @@ function buildServer(profile, deps = {}) {
|
|
|
3538
4039
|
"fallback",
|
|
3539
4040
|
record.name,
|
|
3540
4041
|
record.channel_name,
|
|
3541
|
-
(
|
|
4042
|
+
(report2) => sendDirectFallback(c, record, report2, deps.direct)
|
|
3542
4043
|
);
|
|
3543
4044
|
const outcome = await runner.wait(job);
|
|
3544
4045
|
if (outcome.status === "done") failedDirects.delete(input.job);
|
|
@@ -3566,7 +4067,7 @@ function buildServer(profile, deps = {}) {
|
|
|
3566
4067
|
"receive",
|
|
3567
4068
|
input.dest ?? "Directo",
|
|
3568
4069
|
input.channel ?? "",
|
|
3569
|
-
(
|
|
4070
|
+
(report2) => receiveDirect(c, input, report2, {
|
|
3570
4071
|
...deps.receive ?? {},
|
|
3571
4072
|
onFailed: (record) => rememberFailedReceive(job.id, record)
|
|
3572
4073
|
})
|
|
@@ -3590,7 +4091,7 @@ function buildServer(profile, deps = {}) {
|
|
|
3590
4091
|
"fallback",
|
|
3591
4092
|
record.meta.name,
|
|
3592
4093
|
record.channel_name,
|
|
3593
|
-
(
|
|
4094
|
+
(report2) => receiveDirectFallback(c, record, report2, deps.receive)
|
|
3594
4095
|
);
|
|
3595
4096
|
const outcome = await runner.wait(job);
|
|
3596
4097
|
if (outcome.status === "done") failedReceives.delete(input.job);
|
|
@@ -3667,7 +4168,16 @@ function parseArgs(argv) {
|
|
|
3667
4168
|
const inline = arg.startsWith("--") && eq > 0 ? arg.slice(eq + 1) : void 0;
|
|
3668
4169
|
const take = () => inline !== void 0 ? inline : argv[++i] ?? "";
|
|
3669
4170
|
if (name === "pair") parsed.command = "pair";
|
|
3670
|
-
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";
|
|
3671
4181
|
else if (name === "--help" || name === "-h") parsed.command = "help";
|
|
3672
4182
|
else if (name === "--profile") {
|
|
3673
4183
|
parsed.profile = take() || DEFAULT_PROFILE;
|
|
@@ -3690,6 +4200,7 @@ var USAGE = [
|
|
|
3690
4200
|
" zas-agent [--profile <name>] serve the MCP tools over stdio",
|
|
3691
4201
|
" zas-agent pair [--profile <name>] pair this agent with a Zas account",
|
|
3692
4202
|
" [--kind claude_code|codex|other] [--host <name>] [--no-open]",
|
|
4203
|
+
" zas-agent telemetry [on|off] say what this machine reports, or change it",
|
|
3693
4204
|
" zas-agent --version"
|
|
3694
4205
|
].join("\n");
|
|
3695
4206
|
async function main(argv, log = (l) => console.error(l)) {
|
|
@@ -3707,7 +4218,17 @@ async function main(argv, log = (l) => console.error(l)) {
|
|
|
3707
4218
|
log(USAGE);
|
|
3708
4219
|
return args.unknown ? 2 : 0;
|
|
3709
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
|
+
}
|
|
3710
4228
|
if (args.command === "pair") {
|
|
4229
|
+
log(TELEMETRY_NOTICE);
|
|
4230
|
+
log("");
|
|
4231
|
+
markNoticeShown();
|
|
3711
4232
|
let rl = null;
|
|
3712
4233
|
const askCode = process.stdin.isTTY ? () => {
|
|
3713
4234
|
rl ??= createInterface({ input: process.stdin, output: process.stderr });
|
|
@@ -3736,6 +4257,10 @@ async function main(argv, log = (l) => console.error(l)) {
|
|
|
3736
4257
|
}
|
|
3737
4258
|
return 0;
|
|
3738
4259
|
}
|
|
4260
|
+
if (!noticeShown()) {
|
|
4261
|
+
log(TELEMETRY_NOTICE);
|
|
4262
|
+
markNoticeShown();
|
|
4263
|
+
}
|
|
3739
4264
|
await buildServer(args.profile).connect(new StdioServerTransport());
|
|
3740
4265
|
return 0;
|
|
3741
4266
|
}
|
package/package.json
CHANGED