@snowyroad/braid 0.91.0 → 0.92.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/README.md CHANGED
@@ -284,6 +284,13 @@ relay to reject unsigned machine-agent posts (HTTP 403 `signing_required`).
284
284
  Human messages are never gated. The flag ships off by default; enable it only
285
285
  once all agents in the workspace are on a signing-capable bridge version.
286
286
 
287
+ ## Telemetry
288
+
289
+ The bridge sends a small set of usage signals (which CLI verbs and providers you use, guided-setup
290
+ outcomes, tool-permission denials) — never message content, file paths, or command arguments — tied
291
+ to your account, not anonymous. Disable with `DO_NOT_TRACK=1` (any surface) or `braid telemetry
292
+ disable`. Full collection list: [`telemetry-collection.md`](https://github.com/Snowy-Road/braid-platform/blob/main/docs/telemetry-collection.md).
293
+
287
294
  ## Supply chain
288
295
 
289
296
  Provider ACP adapters (Claude Code, Codex, Gemini) are exact version-pinned and
@@ -389,7 +389,7 @@ var InviteExpiredError = class extends Error {
389
389
  }
390
390
  };
391
391
  async function redeemInvite(relayHttpUrl, code) {
392
- const { boundedJsonFetch: boundedJsonFetch2 } = await import("./http-YQL65H2M.js");
392
+ const { boundedJsonFetch: boundedJsonFetch2 } = await import("./http-HGIWSVON.js");
393
393
  const r = await boundedJsonFetch2(`${relayHttpUrl.replace(/\/$/, "")}/invites/redeem`, {
394
394
  method: "POST",
395
395
  headers: { "content-type": "application/json" },
@@ -478,7 +478,7 @@ function validateReplacementToken(newToken, currentToken, nowMs = Date.now()) {
478
478
  return { ok: true };
479
479
  }
480
480
  async function mintAccessToken(relayHttpUrl, agentKey, fetchFn = fetch, dpopProof) {
481
- const { boundedJsonFetch: boundedJsonFetch2 } = await import("./http-YQL65H2M.js");
481
+ const { boundedJsonFetch: boundedJsonFetch2 } = await import("./http-HGIWSVON.js");
482
482
  const r = await boundedJsonFetch2(`${relayHttpUrl.replace(/\/$/, "")}/agents/token`, {
483
483
  method: "POST",
484
484
  headers: {
@@ -6682,6 +6682,10 @@ async function loadConfigFromInvite(code, env, flags, telemetry) {
6682
6682
  provider = choice.provider;
6683
6683
  providerExplicit = choice.persist;
6684
6684
  }
6685
+ telemetry?.captureBridgeEvent("bridge_provider_selected", {
6686
+ provider,
6687
+ surface: flags?.provider !== void 0 ? "flag" : env.BRAID_AGENT?.trim() ? "env" : "prompt"
6688
+ });
6685
6689
  const joinModel = flags?.model ?? (env.BRAID_MODEL?.trim() || void 0);
6686
6690
  const scopePersist = buildJoinScopeBlock(flags, env);
6687
6691
  const dir = configDir(env);
package/dist/cli.js CHANGED
@@ -71,7 +71,7 @@ import {
71
71
  toolStatusLine,
72
72
  validateRelayWsUrl,
73
73
  wsToHttp
74
- } from "./chunk-KHC4MLRB.js";
74
+ } from "./chunk-2B5FDTWM.js";
75
75
  import {
76
76
  externalSourceLabel,
77
77
  externalUntrustedPreamble,
@@ -2873,7 +2873,7 @@ Commands:
2873
2873
  scope allow <agent> <host>
2874
2874
  Approve a custom model endpoint host for a saved agent (BL-148)
2875
2875
  telemetry [status|enable|disable]
2876
- Show or change anonymous usage telemetry (tool-mode +
2876
+ Show or change usage telemetry (tool-mode +
2877
2877
  blocked-tool-call signals only; never content, paths, or
2878
2878
  names). Machine-level, no saved agent required. Default: status
2879
2879
  service <install|uninstall|start|stop|restart|status|logs> [name]
@@ -2921,6 +2921,14 @@ Options for join/start (each also has an env twin; flag > env > file > default):
2921
2921
  background; details any time via: braid status <name>)
2922
2922
  At join, explicit choices persist to the saved agent; at start they apply to
2923
2923
  that run only.`;
2924
+ function verbForInvocation(invocation) {
2925
+ if (invocation.kind === "usage") return null;
2926
+ if (invocation.kind === "run") {
2927
+ const first = invocation.argv.find((a) => !a.startsWith("-"));
2928
+ return first === "join" || first === "start" ? first : "run";
2929
+ }
2930
+ return invocation.kind;
2931
+ }
2924
2932
  var SERVICE_VERBS = /* @__PURE__ */ new Set([
2925
2933
  "install",
2926
2934
  "uninstall",
@@ -3348,6 +3356,8 @@ async function runGuidedStart(flags, deps) {
3348
3356
  const chosen = await deps.chooseProvider(name);
3349
3357
  provider = chosen.provider;
3350
3358
  if (chosen.persist) patch.provider = chosen.provider;
3359
+ deps.telemetry.captureBridgeEvent("bridge_provider_selected", { provider: chosen.provider, surface: "guided" });
3360
+ deps.telemetry.captureBridgeEvent("bridge_guided_step", { step: "provider", outcome: "completed" });
3351
3361
  }
3352
3362
  let toolMode = flags?.tools ?? envTool ?? stored.toolMode ?? "readonly";
3353
3363
  if (toolsPending) {
@@ -3365,6 +3375,7 @@ async function runGuidedStart(flags, deps) {
3365
3375
  );
3366
3376
  if (toolsPending) {
3367
3377
  deps.telemetry.captureBridgeEvent("tool_mode_selected", { mode: toolMode, surface: "guided", persisted: false });
3378
+ deps.telemetry.captureBridgeEvent("bridge_guided_step", { step: "tool_mode", outcome: "completed" });
3368
3379
  }
3369
3380
  return { action: "continue" };
3370
3381
  }
@@ -3375,6 +3386,7 @@ async function runGuidedStart(flags, deps) {
3375
3386
  }
3376
3387
  if (toolsPending) {
3377
3388
  deps.telemetry.captureBridgeEvent("tool_mode_selected", { mode: toolMode, surface: "guided", persisted: toolModePersistedOnFile });
3389
+ deps.telemetry.captureBridgeEvent("bridge_guided_step", { step: "tool_mode", outcome: "completed" });
3378
3390
  }
3379
3391
  const cont = () => ephemeralToolMode ? { action: "continue", runToolMode: toolMode } : { action: "continue" };
3380
3392
  const wantBackground = await deps.askBackground(buildBackgroundQuestion(displayName));
@@ -3382,6 +3394,7 @@ async function runGuidedStart(flags, deps) {
3382
3394
  deps.out(
3383
3395
  `[braid] starting ${displayName} in the foreground (Ctrl-C stops it). For a background service later: braid service install ${cmdName}`
3384
3396
  );
3397
+ deps.telemetry.captureBridgeEvent("bridge_guided_step", { step: "background", outcome: "aborted" });
3385
3398
  return cont();
3386
3399
  }
3387
3400
  deps.out(`[braid] provider: ${PROVIDER_LABELS[provider] ?? provider} (${provider})`);
@@ -3397,6 +3410,7 @@ async function runGuidedStart(flags, deps) {
3397
3410
  deps.err(
3398
3411
  `[braid] couldn't set up the background service \u2014 starting ${displayName} in the foreground instead (Ctrl-C stops it; restart later with: braid start ${cmdName})`
3399
3412
  );
3413
+ deps.telemetry.captureBridgeEvent("bridge_guided_step", { step: "background", outcome: "aborted" });
3400
3414
  return cont();
3401
3415
  }
3402
3416
  if (deps.verbose) {
@@ -3408,6 +3422,7 @@ async function runGuidedStart(flags, deps) {
3408
3422
  deps.out(`[braid] ${displayName} is running in the background \u2014 it stays online after you close this terminal`);
3409
3423
  deps.out(`[braid] manage it: braid service <stop|restart|logs> ${cmdName} \xB7 details: braid status ${cmdName}`);
3410
3424
  deps.out(`[braid] back in Braid \u2014 talk to ${displayName} in a channel`);
3425
+ deps.telemetry.captureBridgeEvent("bridge_guided_step", { step: "background", outcome: "completed" });
3411
3426
  return { action: "exit", code: 0 };
3412
3427
  }
3413
3428
 
@@ -4575,11 +4590,17 @@ import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, mkdirSy
4575
4590
  import { dirname as dirname5, join as join7 } from "path";
4576
4591
  var EVENT_PROP_ALLOWLIST = {
4577
4592
  tool_mode_selected: ["mode", "surface", "persisted"],
4578
- tool_call_blocked: ["tool", "mode", "path", "reason"]
4593
+ tool_call_blocked: ["tool", "mode", "path", "reason"],
4594
+ bridge_verb_invoked: ["verb"],
4595
+ bridge_provider_selected: ["provider", "surface"],
4596
+ bridge_guided_step: ["step", "outcome"],
4597
+ bridge_handoff_invoked: [],
4598
+ bridge_service_lifecycle: ["action"]
4579
4599
  };
4580
4600
  var DEBOUNCE_KEY_PROPS = ["tool", "mode", "path", "reason"];
4581
4601
  var DEBOUNCE_MS = 6e4;
4582
- var DISCLOSURE_LINE = "[braid] anonymous usage telemetry is on (tool-mode + blocked-tool-call signals; never content, paths, or names). Disable: braid telemetry disable";
4602
+ var PENDING_CAP = 8;
4603
+ var DISCLOSURE_LINE = "[braid] usage telemetry is on (tool + CLI usage signals, tied to your account; never content, paths, or names). Disable: braid telemetry disable";
4583
4604
  function isDoNotTrackSet(env) {
4584
4605
  const v = env.DO_NOT_TRACK;
4585
4606
  if (v === void 0) return false;
@@ -4614,7 +4635,7 @@ function writeTelemetryConfig(dir, cfg) {
4614
4635
  function createBridgeTelemetry(deps) {
4615
4636
  let cachedConfig;
4616
4637
  const debounceLast = /* @__PURE__ */ new Map();
4617
- let pendingFrame = null;
4638
+ const pendingFrames = [];
4618
4639
  function loadConfigCached() {
4619
4640
  if (cachedConfig === void 0) cachedConfig = readTelemetryConfig(deps.configDir);
4620
4641
  return cachedConfig;
@@ -4647,16 +4668,19 @@ function createBridgeTelemetry(deps) {
4647
4668
  }
4648
4669
  const frame = { type: "telemetry", event: name, props: filteredProps };
4649
4670
  const delivered = deps.send(frame);
4650
- if (delivered !== true) pendingFrame = frame;
4671
+ if (delivered !== true) {
4672
+ pendingFrames.push(frame);
4673
+ if (pendingFrames.length > PENDING_CAP) pendingFrames.shift();
4674
+ }
4651
4675
  } catch {
4652
4676
  }
4653
4677
  }
4654
4678
  function flushPending() {
4655
4679
  try {
4656
- if (pendingFrame === null) return;
4657
- const frame = pendingFrame;
4658
- pendingFrame = null;
4659
- deps.send(frame);
4680
+ while (pendingFrames.length > 0) {
4681
+ const frame = pendingFrames.shift();
4682
+ deps.send(frame);
4683
+ }
4660
4684
  } catch {
4661
4685
  }
4662
4686
  }
@@ -5929,6 +5953,21 @@ async function main() {
5929
5953
  applyEnvAliases();
5930
5954
  installCrashGuard();
5931
5955
  const invocation = parseCliArgs(process.argv.slice(2));
5956
+ const relayRef = { current: null };
5957
+ const telemetry = createBridgeTelemetry({
5958
+ env: process.env,
5959
+ configDir: configDir(process.env),
5960
+ now: Date.now,
5961
+ // BL-265 review: stderr, not stdout — the verb census can fire this
5962
+ // disclosure ahead of ANY dispatch, including machine-read verbs
5963
+ // (`status --json`, `service status --json`, `version`) whose stdout a
5964
+ // parser consumes verbatim. Mirrors FULL_ACCESS_WARNING's stderr channel.
5965
+ // D6 (disclosure precedes any send) is unaffected — only the channel moved.
5966
+ out: (line) => console.error(line),
5967
+ send: (frame) => relayRef.current ? relayRef.current.sendTelemetry(frame) : false
5968
+ });
5969
+ const invokedVerb = verbForInvocation(invocation);
5970
+ if (invokedVerb !== null) telemetry.captureBridgeEvent("bridge_verb_invoked", { verb: invokedVerb });
5932
5971
  if (invocation.kind === "tui") {
5933
5972
  if (invocation.bare && !process.stdout.isTTY) {
5934
5973
  console.error(USAGE);
@@ -5984,6 +6023,9 @@ async function main() {
5984
6023
  );
5985
6024
  }
5986
6025
  if (invocation.kind === "service") {
6026
+ if (invocation.verb === "install" || invocation.verb === "uninstall" || invocation.verb === "start" || invocation.verb === "stop") {
6027
+ telemetry.captureBridgeEvent("bridge_service_lifecycle", { action: invocation.verb });
6028
+ }
5987
6029
  process.exit(await runServiceVerb(invocation.verb, invocation.agent, invocation.json));
5988
6030
  }
5989
6031
  if (invocation.kind === "update") {
@@ -5993,20 +6035,13 @@ async function main() {
5993
6035
  process.exit(await runAttach(invocation.agent));
5994
6036
  }
5995
6037
  if (invocation.kind === "handoff") {
6038
+ telemetry.captureBridgeEvent("bridge_handoff_invoked", {});
5996
6039
  process.exit(await runHandoffCommand({
5997
6040
  agent: invocation.agent,
5998
6041
  channel: invocation.channel,
5999
6042
  recover: invocation.recover
6000
6043
  }));
6001
6044
  }
6002
- const relayRef = { current: null };
6003
- const telemetry = createBridgeTelemetry({
6004
- env: process.env,
6005
- configDir: configDir(process.env),
6006
- now: Date.now,
6007
- out: (line) => console.log(line),
6008
- send: (frame) => relayRef.current ? relayRef.current.sendTelemetry(frame) : false
6009
- });
6010
6045
  {
6011
6046
  const parsed = parseRunFlags(invocation.argv);
6012
6047
  const gate = parsed.ok ? shouldOfferGuidedStart(parsed.rest, process.env) : { offer: false };
@@ -4,7 +4,7 @@ import {
4
4
  boundedJsonFetch,
5
5
  resetHttpMetricsForTest,
6
6
  snapshotHttpMetrics
7
- } from "./chunk-KHC4MLRB.js";
7
+ } from "./chunk-2B5FDTWM.js";
8
8
  import "./chunk-BDOF72CL.js";
9
9
  export {
10
10
  assertHttpEndpoint,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@snowyroad/braid",
3
- "version": "0.91.0",
3
+ "version": "0.92.0",
4
4
  "description": "Braid: connect your own coding agent (Claude Code, Codex, Gemini, Grok) to a Braid channel and collaborate with other agents and humans.",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "author": "SnowyRoad",
@@ -51,7 +51,7 @@
51
51
  "@anthropic-ai/sandbox-runtime": "0.0.63",
52
52
  "@anthropic-ai/sdk": "0.104.1",
53
53
  "@modelcontextprotocol/sdk": "1.29.0",
54
- "@snowyroad/braid-tui": "0.0.26",
54
+ "@snowyroad/braid-tui": "0.0.27",
55
55
  "canonicalize": "^3.0.0",
56
56
  "diff": "^9.0.0",
57
57
  "jose": "^6.2.3",
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@snowyroad/braid",
3
- "version": "0.91.0",
3
+ "version": "0.92.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@snowyroad/braid",
9
- "version": "0.91.0",
9
+ "version": "0.92.0",
10
10
  "license": "SEE LICENSE IN LICENSE.md",
11
11
  "dependencies": {
12
12
  "@agentclientprotocol/sdk": "0.25.1",
@@ -14,7 +14,7 @@
14
14
  "@anthropic-ai/sandbox-runtime": "0.0.63",
15
15
  "@anthropic-ai/sdk": "0.104.1",
16
16
  "@modelcontextprotocol/sdk": "1.29.0",
17
- "@snowyroad/braid-tui": "0.0.26",
17
+ "@snowyroad/braid-tui": "0.0.27",
18
18
  "canonicalize": "^3.0.0",
19
19
  "diff": "^9.0.0",
20
20
  "jose": "^6.2.3",
@@ -1204,9 +1204,9 @@
1204
1204
  ]
1205
1205
  },
1206
1206
  "node_modules/@snowyroad/braid-tui": {
1207
- "version": "0.0.26",
1208
- "resolved": "https://registry.npmjs.org/@snowyroad/braid-tui/-/braid-tui-0.0.26.tgz",
1209
- "integrity": "sha512-FTXVV0BB5Ko/gRfbzMjLoh1wG+j7aiBWjEhXp+k0cI67gUuQgxMaykV5gELWepVofUh8t3ZAsRgthZoWoqFVPw==",
1207
+ "version": "0.0.27",
1208
+ "resolved": "https://registry.npmjs.org/@snowyroad/braid-tui/-/braid-tui-0.0.27.tgz",
1209
+ "integrity": "sha512-CZ9ZjImbe7sYHc8vFMOF0KlEYbN/1MyP1HeX8H4cfYQRwi4Q/IzAbWpfOGZo4iWZuwm/P7LbE9quBX4pfL8nww==",
1210
1210
  "license": "SEE LICENSE IN LICENSE.md",
1211
1211
  "dependencies": {
1212
1212
  "ink": "^5.2.0",
@@ -2143,9 +2143,9 @@
2143
2143
  }
2144
2144
  },
2145
2145
  "node_modules/express-rate-limit": {
2146
- "version": "8.6.1",
2147
- "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.1.tgz",
2148
- "integrity": "sha512-0D493aP61w0TJ2A0wy27riRsO7FMQ7FK+KUHOKCSfPvYo0R55aiC6emCVgFUeShH0fq0ICPVzNcgoS+BsbXQCA==",
2146
+ "version": "8.6.2",
2147
+ "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.2.tgz",
2148
+ "integrity": "sha512-YH4ru+eOJxQABscKFfRCy9R7x9QFGdezclVMwwgFFndzS2Xnm0uo6B0ABZsLhcpeptGv2qvuJVWlQr9gQZoC3A==",
2149
2149
  "license": "MIT",
2150
2150
  "dependencies": {
2151
2151
  "debug": "^4.4.3",
@@ -2943,9 +2943,9 @@
2943
2943
  }
2944
2944
  },
2945
2945
  "node_modules/postcss": {
2946
- "version": "8.5.25",
2947
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz",
2948
- "integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==",
2946
+ "version": "8.5.26",
2947
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz",
2948
+ "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==",
2949
2949
  "dev": true,
2950
2950
  "funding": [
2951
2951
  {
@@ -2963,7 +2963,7 @@
2963
2963
  ],
2964
2964
  "license": "MIT",
2965
2965
  "dependencies": {
2966
- "nanoid": "^3.3.16",
2966
+ "nanoid": "^3.3.17",
2967
2967
  "picocolors": "^1.1.1",
2968
2968
  "source-map-js": "^1.2.1"
2969
2969
  },
@@ -3713,9 +3713,9 @@
3713
3713
  }
3714
3714
  },
3715
3715
  "node_modules/tsx": {
3716
- "version": "4.23.5",
3717
- "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.5.tgz",
3718
- "integrity": "sha512-rw55FUaqOoI7RvlQwLbhO4nSDApnQ4/CykPuiQ/EPvtrX3WA9Ig55jIt9VvbBJbzJuj12ueRu4PMZ2SxPVbihg==",
3716
+ "version": "4.23.9",
3717
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.9.tgz",
3718
+ "integrity": "sha512-6q8uTORRGauQVjqMQnKUucLFoeXZAfw6zKvG35GLbdKWbLdeOtZ3H4mhyA5mxuUd2o2cRTskhj59nLLQseUvUw==",
3719
3719
  "dev": true,
3720
3720
  "license": "MIT",
3721
3721
  "dependencies": {