@snowyroad/braid 0.83.0 → 0.84.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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-4PYTQSSA.js");
392
+ const { boundedJsonFetch: boundedJsonFetch2 } = await import("./http-SXNTO4ON.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-4PYTQSSA.js");
481
+ const { boundedJsonFetch: boundedJsonFetch2 } = await import("./http-SXNTO4ON.js");
482
482
  const r = await boundedJsonFetch2(`${relayHttpUrl.replace(/\/$/, "")}/agents/token`, {
483
483
  method: "POST",
484
484
  headers: {
@@ -1361,16 +1361,26 @@ function pickAllowOption(req) {
1361
1361
  "ACP request_permission had no allow option (allow_once/allow_always); refusing to auto-select a non-allow option"
1362
1362
  );
1363
1363
  }
1364
+ function safeAcpToolName(tc) {
1365
+ const title = tc?.title;
1366
+ if (typeof title === "string") {
1367
+ const m = /^mcp__[A-Za-z0-9_.-]+__([A-Za-z0-9_.-]+)$/.exec(title);
1368
+ if (m) return m[1];
1369
+ }
1370
+ return tc?.kind ?? "unknown";
1371
+ }
1364
1372
  var AcpClient = class {
1365
- constructor(launch, scopeWrap, afterChildExitHook) {
1373
+ constructor(launch, scopeWrap, afterChildExitHook, telemetry) {
1366
1374
  this.launch = launch;
1367
1375
  this.scopeWrap = scopeWrap;
1368
1376
  this.afterChildExitHook = afterChildExitHook;
1377
+ this.telemetry = telemetry;
1369
1378
  this.policy = launch.policy ?? { mode: "readonly", configDirAbs: configDir(process.env) };
1370
1379
  }
1371
1380
  launch;
1372
1381
  scopeWrap;
1373
1382
  afterChildExitHook;
1383
+ telemetry;
1374
1384
  /** The tool permission policy; defaults FAIL-SAFE to readonly (never to approval). */
1375
1385
  policy;
1376
1386
  child = null;
@@ -1544,6 +1554,12 @@ var AcpClient = class {
1544
1554
  };
1545
1555
  }
1546
1556
  console.warn(`[braid] denied agent tool permission: ${sanitizeForTty(verdict.reason)}`);
1557
+ this.telemetry?.captureBridgeEvent("tool_call_blocked", {
1558
+ tool: safeAcpToolName(req.toolCall),
1559
+ mode,
1560
+ path: "acp",
1561
+ reason: verdict.deniedByMode ? "mode" : "tripwire"
1562
+ });
1547
1563
  if (verdict.deniedByMode) {
1548
1564
  const cause = this.currentTurnOverrideMode === "readonly" && this.policy.mode !== "readonly" ? "channel" : "operator";
1549
1565
  const hint = takeDenialHint(this.policy, cause);
@@ -3894,8 +3910,26 @@ var RelayClient = class {
3894
3910
  this.bumpCursor(m.channelId, m.seq);
3895
3911
  this.inboundCbs.forEach((cb) => cb(m));
3896
3912
  }
3913
+ /** Returns whether the frame was actually written (WS open) — callers that
3914
+ * don't care (the vast majority) simply ignore the return value. The
3915
+ * telemetry seam's pre-connect latch (BL-177 whole-branch review) relies on
3916
+ * this signal via sendTelemetry below. */
3897
3917
  send(obj) {
3898
- if (this.ws && this.ws.readyState === 1) this.ws.send(JSON.stringify(obj));
3918
+ if (this.ws && this.ws.readyState === 1) {
3919
+ this.ws.send(JSON.stringify(obj));
3920
+ return true;
3921
+ }
3922
+ return false;
3923
+ }
3924
+ /** Public wrapper for the telemetry seam (BL-177 Task 4): forwards an already-
3925
+ * built, already-allowlisted frame (src/telemetry.ts owns consent/allowlist/
3926
+ * debounce) to the guarded protected send() — so a telemetry frame goes out
3927
+ * under the SAME WS-open discipline as everything else and is silently
3928
+ * dropped when the socket isn't open (no queue, per design). Deliberately
3929
+ * dumb: no validation here. Returns delivered=true/false so the seam can
3930
+ * latch an undelivered pre-connect frame (single-slot, D5). */
3931
+ sendTelemetry(frame) {
3932
+ return this.send(frame);
3899
3933
  }
3900
3934
  /** Emit a typing/thinking activity for a channel. `activityOverride` (start only)
3901
3935
  * forces the broadcast label — used by the warm-up path to show "coming_online".
@@ -5783,10 +5817,12 @@ function makeInputQueue() {
5783
5817
  };
5784
5818
  }
5785
5819
  var ClaudeAdapter = class {
5786
- constructor(policy = defaultToolPolicy()) {
5820
+ constructor(policy = defaultToolPolicy(), telemetry) {
5787
5821
  this.policy = policy;
5822
+ this.telemetry = telemetry;
5788
5823
  }
5789
5824
  policy;
5825
+ telemetry;
5790
5826
  // mcpServers/env from AgentStartOptions are ignored here: the generic (bundled Claude
5791
5827
  // SDK) path does not advertise stdio MCP servers to a subprocess. Default no-MCP path.
5792
5828
  async start(opts) {
@@ -5811,6 +5847,12 @@ var ClaudeAdapter = class {
5811
5847
  const verdict = evaluateSdkTool(mode, policy.configDirAbs, toolName, toolInput);
5812
5848
  if (verdict.allow) return { behavior: "allow", updatedInput: toolInput };
5813
5849
  console.warn(`[braid] denied agent tool use: ${sanitizeForTty(verdict.reason)}`);
5850
+ this.telemetry?.captureBridgeEvent("tool_call_blocked", {
5851
+ tool: toolName,
5852
+ mode,
5853
+ path: "sdk",
5854
+ reason: verdict.deniedByMode ? "mode" : "tripwire"
5855
+ });
5814
5856
  if (verdict.deniedByMode) {
5815
5857
  const hint = takeDenialHint(policy);
5816
5858
  if (hint) console.warn(sanitizeForTty(hint));
@@ -5859,14 +5901,14 @@ var ClaudeAdapter = class {
5859
5901
  };
5860
5902
  }
5861
5903
  };
5862
- function createAdapter(cfg, channelId) {
5904
+ function createAdapter(cfg, channelId, telemetry) {
5863
5905
  const policy = {
5864
5906
  mode: cfg.toolMode,
5865
5907
  configDirAbs: resolve2(configDir(process.env)),
5866
5908
  agentName: cfg.agentName
5867
5909
  // for the once-per-process "braid tools full <name>" denial hint
5868
5910
  };
5869
- if (cfg.agentMode !== "acp") return new ClaudeAdapter(policy);
5911
+ if (cfg.agentMode !== "acp") return new ClaudeAdapter(policy, telemetry);
5870
5912
  if (channelId) {
5871
5913
  const prev = detectProviderSwitch(configDir(process.env), cfg.relayWsUrl, cfg.agentName, channelId, cfg.agent);
5872
5914
  if (prev) {
@@ -5888,7 +5930,8 @@ function createAdapter(cfg, channelId) {
5888
5930
  const makeClient = (launch) => new AcpClient(
5889
5931
  launch,
5890
5932
  (l) => scopedLaunch(l, cfg, cfg.agent),
5891
- afterChildExit
5933
+ afterChildExit,
5934
+ telemetry
5892
5935
  );
5893
5936
  return new AcpAdapter(cfg.agent, makeClient, void 0, policy, session, usageSource, cfg.agentName, cfg.stripEnvKeys, cfg.fallbackModel);
5894
5937
  }
@@ -6163,7 +6206,7 @@ function loadConfig(env) {
6163
6206
  function wsToHttp(relayWsUrl) {
6164
6207
  return relayWsUrl.replace(/^wss:\/\//i, "https://").replace(/^ws:\/\//i, "http://");
6165
6208
  }
6166
- async function buildFromStoredAgent(dir, stored, env, flags) {
6209
+ async function buildFromStoredAgent(dir, stored, env, flags, telemetry) {
6167
6210
  if (stored.provider !== void 0 && !ACP_AGENT_IDS.includes(stored.provider)) {
6168
6211
  throw new Error(
6169
6212
  `Invalid provider in saved agent config: ${stored.provider}. Expected one of: ${ACP_AGENT_IDS.join(", ")}`
@@ -6187,8 +6230,10 @@ async function buildFromStoredAgent(dir, stored, env, flags) {
6187
6230
  let toolMode;
6188
6231
  if (flags?.tools) {
6189
6232
  toolMode = flags.tools;
6233
+ telemetry?.captureBridgeEvent("tool_mode_selected", { mode: toolMode, surface: "flag", persisted: false });
6190
6234
  } else if (envToolMode) {
6191
6235
  toolMode = parseToolMode(env);
6236
+ telemetry?.captureBridgeEvent("tool_mode_selected", { mode: toolMode, surface: "env", persisted: false });
6192
6237
  } else if (current.toolMode) {
6193
6238
  toolMode = current.toolMode;
6194
6239
  } else {
@@ -6198,6 +6243,7 @@ async function buildFromStoredAgent(dir, stored, env, flags) {
6198
6243
  current = { ...current, toolMode };
6199
6244
  saveAgent(dir, current);
6200
6245
  }
6246
+ telemetry?.captureBridgeEvent("tool_mode_selected", { mode: toolMode, surface: "prompt", persisted: choice.persist });
6201
6247
  }
6202
6248
  let signingPrivateJwk = current.signingPrivateJwk;
6203
6249
  if (!signingPrivateJwk) {
@@ -6323,7 +6369,7 @@ function parseToolModeForJoin(env) {
6323
6369
  );
6324
6370
  return void 0;
6325
6371
  }
6326
- async function loadConfigFromInvite(code, env, flags) {
6372
+ async function loadConfigFromInvite(code, env, flags, telemetry) {
6327
6373
  resolveAgentSelection(env, flags?.provider);
6328
6374
  const joinToolMode = flags?.tools ?? parseToolModeForJoin(env);
6329
6375
  const envForBuild = env.BRAID_TOOL_MODE && joinToolMode === void 0 ? { ...env, BRAID_TOOL_MODE: void 0 } : env;
@@ -6365,7 +6411,7 @@ async function loadConfigFromInvite(code, env, flags) {
6365
6411
  const file = saveAgent(dir, stored);
6366
6412
  const cmdName = isShellSafeName(bundle.agentName) ? bundle.agentName : "<agent-name>";
6367
6413
  console.log(`[braid] credential saved to ${file} (restart later with: npx @snowyroad/braid start ${cmdName})`);
6368
- return buildFromStoredAgent(dir, stored, envForBuild, flags);
6414
+ return buildFromStoredAgent(dir, stored, envForBuild, flags, telemetry);
6369
6415
  }
6370
6416
  function buildJoinScopeBlock(flags, env) {
6371
6417
  const split = (v) => v ? v.split(/[,:]/).map((s) => s.trim()).filter((s) => s.length > 0) : [];
@@ -6384,10 +6430,10 @@ function buildJoinScopeBlock(flags, env) {
6384
6430
  ...disabled ? { disabled: true } : {}
6385
6431
  };
6386
6432
  }
6387
- async function loadConfigFromStore(agentName, env, flags, io) {
6433
+ async function loadConfigFromStore(agentName, env, flags, io, telemetry) {
6388
6434
  const dir = configDir(env);
6389
6435
  const entry = await resolveStoredAgent(dir, agentName, io);
6390
- return buildFromStoredAgent(dir, entry.agent, env, flags);
6436
+ return buildFromStoredAgent(dir, entry.agent, env, flags, telemetry);
6391
6437
  }
6392
6438
  async function resolveStoredAgent(dir, agentName, io) {
6393
6439
  const name = agentName?.trim();
@@ -6408,7 +6454,7 @@ function getFlag(argv, name) {
6408
6454
  }
6409
6455
  return void 0;
6410
6456
  }
6411
- async function resolveConfig(argv, env) {
6457
+ async function resolveConfig(argv, env, telemetry) {
6412
6458
  const parsed = parseRunFlags(argv);
6413
6459
  if (!parsed.ok) throw new Error(parsed.error);
6414
6460
  const { flags, rest } = parsed;
@@ -6416,12 +6462,12 @@ async function resolveConfig(argv, env) {
6416
6462
  if (positional[0] === "join") {
6417
6463
  const code = positional[1];
6418
6464
  if (!code || code.trim() === "") throw new Error("Missing value for join");
6419
- return loadConfigFromInvite(code.trim(), env, flags);
6465
+ return loadConfigFromInvite(code.trim(), env, flags, telemetry);
6420
6466
  }
6421
6467
  if (positional[0] === "start") {
6422
6468
  const restAfter = positional.slice(1);
6423
6469
  const name = restAfter[0] && !restAfter[0].startsWith("-") ? restAfter[0].trim() : void 0;
6424
- if (name) return loadConfigFromStore(name, env, flags);
6470
+ if (name) return loadConfigFromStore(name, env, flags, void 0, telemetry);
6425
6471
  positional = restAfter;
6426
6472
  }
6427
6473
  const argInvite = getFlag(positional, "--invite");
@@ -6429,9 +6475,9 @@ async function resolveConfig(argv, env) {
6429
6475
  throw new Error("Missing value for --invite");
6430
6476
  }
6431
6477
  const invite = (argInvite ?? env.BRAID_INVITE)?.trim();
6432
- if (invite) return loadConfigFromInvite(invite, env, flags);
6478
+ if (invite) return loadConfigFromInvite(invite, env, flags, telemetry);
6433
6479
  if (env.BRAID_TOKEN && env.BRAID_TOKEN.trim() !== "") return loadConfig(env);
6434
- return loadConfigFromStore(void 0, env, flags);
6480
+ return loadConfigFromStore(void 0, env, flags, void 0, telemetry);
6435
6481
  }
6436
6482
  function parseClaudeConfigModel(jsonText) {
6437
6483
  try {
package/dist/cli.js CHANGED
@@ -70,7 +70,7 @@ import {
70
70
  toolStatusLine,
71
71
  validateRelayWsUrl,
72
72
  wsToHttp
73
- } from "./chunk-QDDQF6RJ.js";
73
+ } from "./chunk-PQGSJM2V.js";
74
74
  import {
75
75
  externalSourceLabel,
76
76
  externalUntrustedPreamble,
@@ -2758,6 +2758,7 @@ async function createAndStartBridge(cfg, deps = {}) {
2758
2758
  wsFactory,
2759
2759
  fetchFn: deps.fetchFn ?? fetch
2760
2760
  });
2761
+ deps.onRelayCreated?.(relay);
2761
2762
  let handle = null;
2762
2763
  relay.onFatal(
2763
2764
  deps.onFatal ?? ((codeOrError, reason) => {
@@ -2777,7 +2778,8 @@ async function createAndStartBridge(cfg, deps = {}) {
2777
2778
  void drainAndExit(handle ? handle.sessions.values() : [], exitCode, void 0, handle?.brokers.values());
2778
2779
  })
2779
2780
  );
2780
- const makeAdapter = deps.makeAdapter ?? createAdapter;
2781
+ const rawMakeAdapter = deps.makeAdapter ?? createAdapter;
2782
+ const makeAdapter = (c, channelId) => rawMakeAdapter(c, channelId, deps.telemetry);
2781
2783
  handle = await startBridge(cfg, relay, { makeAdapter, onReady: deps.onReady, cardCache: deps.cardCache, controlSocket: deps.controlSocket });
2782
2784
  return { relay, sessions: handle.sessions, channelNames: handle.channelNames, brokers: handle.brokers, hub: handle.hub, ensureSession: handle.ensureSession, controlSocketClose: handle.controlSocketClose };
2783
2785
  }
@@ -2829,7 +2831,7 @@ function startUpdateNotifier(opts) {
2829
2831
  }
2830
2832
 
2831
2833
  // src/cli.ts
2832
- import { readFileSync as readFileSync8 } from "fs";
2834
+ import { readFileSync as readFileSync9 } from "fs";
2833
2835
  import { fileURLToPath as fileURLToPath3 } from "url";
2834
2836
 
2835
2837
  // src/cliArgs.ts
@@ -2859,6 +2861,10 @@ Commands:
2859
2861
  (no network; local inspection only)
2860
2862
  scope allow <agent> <host>
2861
2863
  Approve a custom model endpoint host for a saved agent (BL-148)
2864
+ telemetry [status|enable|disable]
2865
+ Show or change anonymous usage telemetry (tool-mode +
2866
+ blocked-tool-call signals only; never content, paths, or
2867
+ names). Machine-level, no saved agent required. Default: status
2862
2868
  service <install|uninstall|start|stop|restart|status|logs> [name]
2863
2869
  Run a saved agent as a background service that starts at login
2864
2870
  and restarts on crashes (launchd on macOS, systemd user unit on
@@ -2911,6 +2917,7 @@ var SERVICE_VERBS = /* @__PURE__ */ new Set([
2911
2917
  "status",
2912
2918
  "logs"
2913
2919
  ]);
2920
+ var TELEMETRY_VERBS = /* @__PURE__ */ new Set(["status", "enable", "disable"]);
2914
2921
  function parseCliArgs(argv) {
2915
2922
  const first = argv[0];
2916
2923
  if (first === "list") return { kind: "list" };
@@ -2987,6 +2994,20 @@ function parseCliArgs(argv) {
2987
2994
  if (first === "update") {
2988
2995
  return { kind: "update" };
2989
2996
  }
2997
+ if (first === "telemetry") {
2998
+ const verbArg = argv[1];
2999
+ const verb = verbArg ?? "status";
3000
+ if (!TELEMETRY_VERBS.has(verb)) {
3001
+ return {
3002
+ kind: "usage",
3003
+ error: `Unknown telemetry subcommand: ${verbArg}. Expected status|enable|disable`
3004
+ };
3005
+ }
3006
+ if (argv.length > 2) {
3007
+ return { kind: "usage", error: `Unexpected argument for telemetry: ${argv[2]}` };
3008
+ }
3009
+ return { kind: "telemetry", verb };
3010
+ }
2990
3011
  if (first === "attach") {
2991
3012
  const agent = argv[1] && !argv[1].startsWith("-") ? argv[1].trim() : void 0;
2992
3013
  return { kind: "attach", agent };
@@ -3322,17 +3343,25 @@ async function runGuidedStart(flags, deps) {
3322
3343
  if (chosen.persist) patch.toolMode = chosen.mode;
3323
3344
  }
3324
3345
  let ephemeralToolMode = toolsPending && patch.toolMode === void 0;
3346
+ let toolModePersistedOnFile = false;
3325
3347
  if (patch.provider !== void 0 || patch.toolMode !== void 0) {
3326
3348
  const persisted = deps.persist(patch);
3327
3349
  if (persisted === false) {
3328
3350
  deps.err(
3329
3351
  `[braid] ${displayName}'s setup answers could not be saved \u2014 it may already be running on this machine. Stop it first, then run: braid start ${cmdName}`
3330
3352
  );
3353
+ if (toolsPending) {
3354
+ deps.telemetry.captureBridgeEvent("tool_mode_selected", { mode: toolMode, surface: "guided", persisted: false });
3355
+ }
3331
3356
  return { action: "continue" };
3332
3357
  }
3333
3358
  provider = persisted.provider ?? provider;
3334
3359
  toolMode = persisted.toolMode ?? toolMode;
3335
3360
  ephemeralToolMode = toolsPending && patch.toolMode === void 0 && persisted.toolMode === void 0;
3361
+ toolModePersistedOnFile = persisted.toolMode !== void 0;
3362
+ }
3363
+ if (toolsPending) {
3364
+ deps.telemetry.captureBridgeEvent("tool_mode_selected", { mode: toolMode, surface: "guided", persisted: toolModePersistedOnFile });
3336
3365
  }
3337
3366
  const cont = () => ephemeralToolMode ? { action: "continue", runToolMode: toolMode } : { action: "continue" };
3338
3367
  const wantBackground = await deps.askBackground(buildBackgroundQuestion(displayName));
@@ -4528,13 +4557,125 @@ function renderStatusJson(input) {
4528
4557
  return JSON.stringify({ agents }, null, 2);
4529
4558
  }
4530
4559
 
4560
+ // src/telemetry.ts
4561
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, mkdirSync as mkdirSync5, renameSync as renameSync3, chmodSync as chmodSync4 } from "fs";
4562
+ import { dirname as dirname5, join as join7 } from "path";
4563
+ var EVENT_PROP_ALLOWLIST = {
4564
+ tool_mode_selected: ["mode", "surface", "persisted"],
4565
+ tool_call_blocked: ["tool", "mode", "path", "reason"]
4566
+ };
4567
+ var DEBOUNCE_KEY_PROPS = ["tool", "mode", "path", "reason"];
4568
+ var DEBOUNCE_MS = 6e4;
4569
+ var DISCLOSURE_LINE = "[braid] anonymous usage telemetry is on (tool-mode + blocked-tool-call signals; never content, paths, or names). Disable: braid telemetry disable";
4570
+ function isDoNotTrackSet(env) {
4571
+ const v = env.DO_NOT_TRACK;
4572
+ if (v === void 0) return false;
4573
+ return /^(1|true|yes)$/i.test(v.trim());
4574
+ }
4575
+ function telemetryConfigPath(dir) {
4576
+ return join7(dir, "telemetry.json");
4577
+ }
4578
+ function readTelemetryConfig(dir) {
4579
+ try {
4580
+ const raw = readFileSync6(telemetryConfigPath(dir), "utf8");
4581
+ const parsed = JSON.parse(raw);
4582
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
4583
+ return parsed;
4584
+ }
4585
+ return {};
4586
+ } catch {
4587
+ return {};
4588
+ }
4589
+ }
4590
+ function writeTelemetryConfig(dir, cfg) {
4591
+ const file = telemetryConfigPath(dir);
4592
+ mkdirSync5(dirname5(file), { recursive: true, mode: 448 });
4593
+ const tmp = `${file}.tmp-${process.pid}`;
4594
+ writeFileSync5(tmp, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
4595
+ renameSync3(tmp, file);
4596
+ try {
4597
+ chmodSync4(file, 384);
4598
+ } catch {
4599
+ }
4600
+ }
4601
+ function createBridgeTelemetry(deps) {
4602
+ let cachedConfig;
4603
+ const debounceLast = /* @__PURE__ */ new Map();
4604
+ let pendingFrame = null;
4605
+ function loadConfigCached() {
4606
+ if (cachedConfig === void 0) cachedConfig = readTelemetryConfig(deps.configDir);
4607
+ return cachedConfig;
4608
+ }
4609
+ function captureBridgeEvent(name, props) {
4610
+ try {
4611
+ if (isDoNotTrackSet(deps.env)) return;
4612
+ const cfg = loadConfigCached();
4613
+ if (cfg.disabled === true) return;
4614
+ const allowedKeys = EVENT_PROP_ALLOWLIST[name];
4615
+ if (!allowedKeys) return;
4616
+ const filteredProps = {};
4617
+ for (const key of allowedKeys) {
4618
+ if (Object.prototype.hasOwnProperty.call(props, key)) filteredProps[key] = props[key];
4619
+ }
4620
+ if (name === "tool_call_blocked") {
4621
+ const debounceKey = DEBOUNCE_KEY_PROPS.map((k) => String(filteredProps[k])).join("|");
4622
+ const nowMs = deps.now();
4623
+ const last = debounceLast.get(debounceKey);
4624
+ if (last !== void 0 && nowMs - last < DEBOUNCE_MS) return;
4625
+ debounceLast.set(debounceKey, nowMs);
4626
+ }
4627
+ if (!cfg.noticeShown) {
4628
+ deps.out(DISCLOSURE_LINE);
4629
+ cachedConfig = { ...cfg, noticeShown: true };
4630
+ try {
4631
+ writeTelemetryConfig(deps.configDir, cachedConfig);
4632
+ } catch {
4633
+ }
4634
+ }
4635
+ const frame = { type: "telemetry", event: name, props: filteredProps };
4636
+ const delivered = deps.send(frame);
4637
+ if (delivered !== true) pendingFrame = frame;
4638
+ } catch {
4639
+ }
4640
+ }
4641
+ function flushPending() {
4642
+ try {
4643
+ if (pendingFrame === null) return;
4644
+ const frame = pendingFrame;
4645
+ pendingFrame = null;
4646
+ deps.send(frame);
4647
+ } catch {
4648
+ }
4649
+ }
4650
+ return { captureBridgeEvent, flushPending };
4651
+ }
4652
+ function telemetryStatus(dir, env) {
4653
+ const cfg = readTelemetryConfig(dir);
4654
+ const forcedOffByDoNotTrack = isDoNotTrackSet(env);
4655
+ return {
4656
+ enabled: !forcedOffByDoNotTrack && cfg.disabled !== true,
4657
+ forcedOffByDoNotTrack,
4658
+ configPath: telemetryConfigPath(dir)
4659
+ };
4660
+ }
4661
+ function renderTelemetryStatusLines(view) {
4662
+ const lines = [];
4663
+ const state = view.enabled ? "ON" : "OFF";
4664
+ lines.push(`[braid] telemetry: ${state}`);
4665
+ if (view.forcedOffByDoNotTrack) {
4666
+ lines.push(" forced off by DO_NOT_TRACK (unset it to allow telemetry)");
4667
+ }
4668
+ lines.push(` config: ${view.configPath}`);
4669
+ return lines;
4670
+ }
4671
+
4531
4672
  // src/service/cli.ts
4532
4673
  import { execFile } from "child_process";
4533
4674
  import { homedir as homedir5 } from "os";
4534
4675
 
4535
4676
  // src/service/manager.ts
4536
- import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync6, rmSync as rmSync2, existsSync as existsSync5 } from "fs";
4537
- import { join as join8 } from "path";
4677
+ import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync7, rmSync as rmSync2, existsSync as existsSync5 } from "fs";
4678
+ import { join as join9 } from "path";
4538
4679
 
4539
4680
  // src/service/templates.ts
4540
4681
  function escapeXml(s) {
@@ -4705,18 +4846,18 @@ function schtasksXml(p) {
4705
4846
  }
4706
4847
 
4707
4848
  // src/service/win32.ts
4708
- import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5, readdirSync as readdirSync4, readFileSync as readFileSync6, unlinkSync as unlinkSync3 } from "fs";
4709
- import { join as join7 } from "path";
4849
+ import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync6, readdirSync as readdirSync4, readFileSync as readFileSync7, unlinkSync as unlinkSync3 } from "fs";
4850
+ import { join as join8 } from "path";
4710
4851
  function win32LogDir(homeDir) {
4711
- return join7(homeDir, "AppData", "Local", "braid", "logs");
4852
+ return join8(homeDir, "AppData", "Local", "braid", "logs");
4712
4853
  }
4713
4854
  function win32Manager(ctx) {
4714
- const serviceDir = join7(ctx.braidDir, "service");
4855
+ const serviceDir = join8(ctx.braidDir, "service");
4715
4856
  const task = (name) => schtasksLabel(name);
4716
4857
  const legacyTask = (name) => legacySchtasksLabel(name);
4717
- const xmlPath = (name) => join7(serviceDir, `${task(name)}.xml`);
4718
- const wrapperPath = (name) => join7(serviceDir, `${task(name)}.cmd`);
4719
- const launcherPath = (name) => join7(serviceDir, `${task(name)}.vbs`);
4858
+ const xmlPath = (name) => join8(serviceDir, `${task(name)}.xml`);
4859
+ const wrapperPath = (name) => join8(serviceDir, `${task(name)}.cmd`);
4860
+ const launcherPath = (name) => join8(serviceDir, `${task(name)}.vbs`);
4720
4861
  const st = (...args) => ctx.exec("schtasks", args);
4721
4862
  const stx = async (...args) => {
4722
4863
  const res = await st(...args);
@@ -4727,18 +4868,18 @@ function win32Manager(ctx) {
4727
4868
  };
4728
4869
  const killBridgeProcess = async (name) => {
4729
4870
  const legacyAgents = legacyTwinPath(ctx.braidDir, ["agents"]);
4730
- const agentDirs = [join7(ctx.braidDir, "agents"), ...legacyAgents ? [legacyAgents] : []];
4871
+ const agentDirs = [join8(ctx.braidDir, "agents"), ...legacyAgents ? [legacyAgents] : []];
4731
4872
  const hostDirs = [];
4732
4873
  for (const agentsDir of agentDirs) {
4733
4874
  try {
4734
- for (const host of readdirSync4(agentsDir)) hostDirs.push(join7(agentsDir, host));
4875
+ for (const host of readdirSync4(agentsDir)) hostDirs.push(join8(agentsDir, host));
4735
4876
  } catch {
4736
4877
  }
4737
4878
  }
4738
4879
  for (const hostDir of hostDirs) {
4739
4880
  let pid = NaN;
4740
4881
  try {
4741
- pid = Number(readFileSync6(join7(hostDir, `${name}.json.lock`), "utf8").trim());
4882
+ pid = Number(readFileSync7(join8(hostDir, `${name}.json.lock`), "utf8").trim());
4742
4883
  } catch {
4743
4884
  continue;
4744
4885
  }
@@ -4754,15 +4895,15 @@ function win32Manager(ctx) {
4754
4895
  }
4755
4896
  };
4756
4897
  const write = (name) => {
4757
- mkdirSync5(serviceDir, { recursive: true });
4898
+ mkdirSync6(serviceDir, { recursive: true });
4758
4899
  const entryPath = pinnedEntryPath({ braidDir: ctx.braidDir, platform: "win32" });
4759
4900
  const cmd = schtasksWrapperCmd({ name, nodePath: ctx.nodePath, entryPath });
4760
- writeFileSync5(wrapperPath(name), cmd);
4901
+ writeFileSync6(wrapperPath(name), cmd);
4761
4902
  const vbs = schtasksLauncherVbs({ wrapperPath: wrapperPath(name) });
4762
- writeFileSync5(launcherPath(name), vbs);
4903
+ writeFileSync6(launcherPath(name), vbs);
4763
4904
  const xml = schtasksXml({ name, launcherPath: launcherPath(name) });
4764
4905
  const path2 = xmlPath(name);
4765
- writeFileSync5(path2, Buffer.concat([Buffer.from([255, 254]), Buffer.from(xml, "utf16le")]));
4906
+ writeFileSync6(path2, Buffer.concat([Buffer.from([255, 254]), Buffer.from(xml, "utf16le")]));
4766
4907
  return path2;
4767
4908
  };
4768
4909
  return {
@@ -4782,7 +4923,7 @@ function win32Manager(ctx) {
4782
4923
  await st("/delete", "/tn", task(name), "/f");
4783
4924
  await st("/delete", "/tn", legacyTask(name), "/f");
4784
4925
  try {
4785
- unlinkSync3(join7(runDir(ctx.braidDir), `${name}.token`));
4926
+ unlinkSync3(join8(runDir(ctx.braidDir), `${name}.token`));
4786
4927
  } catch {
4787
4928
  }
4788
4929
  const legacyToken = legacyTwinPath(ctx.braidDir, ["run", `${name}.token`]);
@@ -4820,7 +4961,7 @@ function win32Manager(ctx) {
4820
4961
  return { installed, loaded, running };
4821
4962
  },
4822
4963
  logsPath(name) {
4823
- return join7(win32LogDir(ctx.homeDir), `${name}.err.log`);
4964
+ return join8(win32LogDir(ctx.homeDir), `${name}.err.log`);
4824
4965
  }
4825
4966
  };
4826
4967
  }
@@ -4834,7 +4975,7 @@ var NotSupportedError = class extends Error {
4834
4975
  };
4835
4976
  function makeServiceManager(platform, exec, deps = {}) {
4836
4977
  const homeDir = deps.homeDir ?? process.env.HOME ?? "";
4837
- const braidDir = deps.braidDir ?? join8(homeDir, ".braid");
4978
+ const braidDir = deps.braidDir ?? join9(homeDir, ".braid");
4838
4979
  const uid = deps.uid ?? (typeof process.getuid === "function" ? process.getuid() : 0);
4839
4980
  const nodePath = deps.nodePath ?? process.execPath;
4840
4981
  const entryPath = pinnedEntryPath({ braidDir });
@@ -4859,18 +5000,18 @@ function notSupportedManager(platform) {
4859
5000
  };
4860
5001
  }
4861
5002
  function darwinManager(ctx) {
4862
- const launchAgentsDir = join8(ctx.homeDir, "Library", "LaunchAgents");
4863
- const logDir = join8(ctx.homeDir, "Library", "Logs", "braid");
4864
- const legacyLogDir = join8(ctx.homeDir, "Library", "Logs", "arp-bridge");
4865
- const plistPath = (name) => join8(launchAgentsDir, `${launchdLabel(name)}.plist`);
4866
- const legacyPlistPath = (name) => join8(launchAgentsDir, `${legacyLaunchdLabel(name)}.plist`);
5003
+ const launchAgentsDir = join9(ctx.homeDir, "Library", "LaunchAgents");
5004
+ const logDir = join9(ctx.homeDir, "Library", "Logs", "braid");
5005
+ const legacyLogDir = join9(ctx.homeDir, "Library", "Logs", "arp-bridge");
5006
+ const plistPath = (name) => join9(launchAgentsDir, `${launchdLabel(name)}.plist`);
5007
+ const legacyPlistPath = (name) => join9(launchAgentsDir, `${legacyLaunchdLabel(name)}.plist`);
4867
5008
  const legacyInstalled = (name) => !existsSync5(plistPath(name)) && existsSync5(legacyPlistPath(name));
4868
5009
  const domainTarget = `gui/${ctx.uid}`;
4869
5010
  const serviceTarget = (name) => `${domainTarget}/${legacyInstalled(name) ? legacyLaunchdLabel(name) : launchdLabel(name)}`;
4870
5011
  const write = (name) => {
4871
- mkdirSync6(launchAgentsDir, { recursive: true });
4872
- mkdirSync6(logDir, { recursive: true });
4873
- writeFileSync6(
5012
+ mkdirSync7(launchAgentsDir, { recursive: true });
5013
+ mkdirSync7(logDir, { recursive: true });
5014
+ writeFileSync7(
4874
5015
  plistPath(name),
4875
5016
  launchdPlist({ name, nodePath: ctx.nodePath, entryPath: ctx.entryPath, logDir }),
4876
5017
  { mode: 420 }
@@ -4928,23 +5069,23 @@ function darwinManager(ctx) {
4928
5069
  };
4929
5070
  },
4930
5071
  logsPath(name) {
4931
- return join8(legacyInstalled(name) ? legacyLogDir : logDir, `${name}.err.log`);
5072
+ return join9(legacyInstalled(name) ? legacyLogDir : logDir, `${name}.err.log`);
4932
5073
  }
4933
5074
  };
4934
5075
  }
4935
5076
  function linuxManager(ctx) {
4936
- const unitDir = join8(ctx.homeDir, ".config", "systemd", "user");
5077
+ const unitDir = join9(ctx.homeDir, ".config", "systemd", "user");
4937
5078
  const newUnitName = (name) => `braid-${name}.service`;
4938
5079
  const legacyUnitName = (name) => `arp-bridge-${name}.service`;
4939
- const newUnitPath = (name) => join8(unitDir, newUnitName(name));
4940
- const legacyUnitPath = (name) => join8(unitDir, legacyUnitName(name));
5080
+ const newUnitPath = (name) => join9(unitDir, newUnitName(name));
5081
+ const legacyUnitPath = (name) => join9(unitDir, legacyUnitName(name));
4941
5082
  const legacyInstalled = (name) => !existsSync5(newUnitPath(name)) && existsSync5(legacyUnitPath(name));
4942
5083
  const unitName = (name) => legacyInstalled(name) ? legacyUnitName(name) : newUnitName(name);
4943
5084
  const unitPath = (name) => legacyInstalled(name) ? legacyUnitPath(name) : newUnitPath(name);
4944
5085
  const sc = (...args) => ctx.exec("systemctl", ["--user", ...args]);
4945
5086
  const write = (name) => {
4946
- mkdirSync6(unitDir, { recursive: true });
4947
- writeFileSync6(
5087
+ mkdirSync7(unitDir, { recursive: true });
5088
+ writeFileSync7(
4948
5089
  newUnitPath(name),
4949
5090
  systemdUnit({ name, nodePath: ctx.nodePath, entryPath: ctx.entryPath }),
4950
5091
  { mode: 420 }
@@ -5218,17 +5359,17 @@ function printStatusHuman(name, st, logsPath) {
5218
5359
  }
5219
5360
 
5220
5361
  // src/service/serviceLog.ts
5221
- import { createWriteStream, mkdirSync as mkdirSync7 } from "fs";
5222
- import { dirname as dirname5, join as join9 } from "path";
5362
+ import { createWriteStream, mkdirSync as mkdirSync8 } from "fs";
5363
+ import { dirname as dirname6, join as join10 } from "path";
5223
5364
 
5224
5365
  // src/service/logRotation.ts
5225
5366
  import {
5226
5367
  existsSync as existsSync6,
5227
5368
  statSync as statSync2,
5228
- renameSync as renameSync3,
5369
+ renameSync as renameSync4,
5229
5370
  rmSync as rmSync3,
5230
- readFileSync as readFileSync7,
5231
- writeFileSync as writeFileSync7
5371
+ readFileSync as readFileSync8,
5372
+ writeFileSync as writeFileSync8
5232
5373
  } from "fs";
5233
5374
  import { gzipSync } from "zlib";
5234
5375
  var DEFAULT_MAX_BYTES = 10 * 1024 * 1024;
@@ -5252,18 +5393,18 @@ function rotateLogFileIfNeeded(logPath, opts = {}) {
5252
5393
  }
5253
5394
  for (let n = keepCount - 1; n >= 1; n--) {
5254
5395
  try {
5255
- if (existsSync6(gz(n))) renameSync3(gz(n), gz(n + 1));
5396
+ if (existsSync6(gz(n))) renameSync4(gz(n), gz(n + 1));
5256
5397
  } catch {
5257
5398
  }
5258
5399
  }
5259
5400
  try {
5260
- const raw = readFileSync7(logPath);
5261
- writeFileSync7(gz(1), gzipSync(raw));
5401
+ const raw = readFileSync8(logPath);
5402
+ writeFileSync8(gz(1), gzipSync(raw));
5262
5403
  } catch {
5263
5404
  return;
5264
5405
  }
5265
5406
  try {
5266
- writeFileSync7(logPath, "");
5407
+ writeFileSync8(logPath, "");
5267
5408
  } catch {
5268
5409
  }
5269
5410
  } catch {
@@ -5272,16 +5413,16 @@ function rotateLogFileIfNeeded(logPath, opts = {}) {
5272
5413
 
5273
5414
  // src/service/serviceLog.ts
5274
5415
  function darwinLogDir(homeDir) {
5275
- return join9(homeDir, "Library", "Logs", "braid");
5416
+ return join10(homeDir, "Library", "Logs", "braid");
5276
5417
  }
5277
5418
  function serviceLogTargets(name, platform, homeDir) {
5278
5419
  if (platform === "darwin") {
5279
5420
  const d = darwinLogDir(homeDir);
5280
- return [join9(d, `${name}.out.log`), join9(d, `${name}.err.log`)];
5421
+ return [join10(d, `${name}.out.log`), join10(d, `${name}.err.log`)];
5281
5422
  }
5282
5423
  if (platform === "win32") {
5283
5424
  const d = win32LogDir(homeDir);
5284
- return [join9(d, `${name}.out.log`), join9(d, `${name}.err.log`)];
5425
+ return [join10(d, `${name}.out.log`), join10(d, `${name}.err.log`)];
5285
5426
  }
5286
5427
  return [];
5287
5428
  }
@@ -5331,7 +5472,7 @@ function initServiceLogging(opts) {
5331
5472
  };
5332
5473
  }
5333
5474
  function defaultTeeStream(path2) {
5334
- mkdirSync7(dirname5(path2), { recursive: true });
5475
+ mkdirSync8(dirname6(path2), { recursive: true });
5335
5476
  const stream = createWriteStream(path2, { flags: "a" });
5336
5477
  stream.on("error", () => {
5337
5478
  });
@@ -5373,7 +5514,7 @@ function printStatus(agentName, json) {
5373
5514
  }
5374
5515
  const readTextFile = (p) => {
5375
5516
  try {
5376
- return readFileSync8(p, "utf8");
5517
+ return readFileSync9(p, "utf8");
5377
5518
  } catch {
5378
5519
  return null;
5379
5520
  }
@@ -5464,7 +5605,7 @@ function printScope(agentName) {
5464
5605
  }
5465
5606
  const readTextFile = (p) => {
5466
5607
  try {
5467
- return readFileSync8(p, "utf8");
5608
+ return readFileSync9(p, "utf8");
5468
5609
  } catch {
5469
5610
  return null;
5470
5611
  }
@@ -5504,7 +5645,7 @@ function scopeAllow(agentName, host) {
5504
5645
  const { agent: provider } = resolveAgentSelection(process.env);
5505
5646
  const readTextFile = (p) => {
5506
5647
  try {
5507
- return readFileSync8(p, "utf8");
5648
+ return readFileSync9(p, "utf8");
5508
5649
  } catch {
5509
5650
  return null;
5510
5651
  }
@@ -5538,6 +5679,19 @@ Set the provider's config to use ${normalizedHost} and try again, or check BRAID
5538
5679
  console.log(`[braid] pinned: ${normalizedHost} (source: ${match.source})`);
5539
5680
  console.log(`[braid] restart the bridge for the change to take effect.`);
5540
5681
  }
5682
+ function runTelemetryVerb(verb) {
5683
+ const dir = configDir(process.env);
5684
+ if (verb === "disable") {
5685
+ const cfg = readTelemetryConfig(dir);
5686
+ writeTelemetryConfig(dir, { ...cfg, disabled: true });
5687
+ } else if (verb === "enable") {
5688
+ const cfg = readTelemetryConfig(dir);
5689
+ const { disabled, ...rest } = cfg;
5690
+ writeTelemetryConfig(dir, rest);
5691
+ }
5692
+ const view = telemetryStatus(dir, process.env);
5693
+ for (const line of renderTelemetryStatusLines(view)) console.log(line);
5694
+ }
5541
5695
  var resolvedAgentName = "";
5542
5696
  async function main() {
5543
5697
  applyEnvAliases();
@@ -5580,6 +5734,10 @@ async function main() {
5580
5734
  scopeAllow(invocation.agent, invocation.host);
5581
5735
  return;
5582
5736
  }
5737
+ if (invocation.kind === "telemetry") {
5738
+ runTelemetryVerb(invocation.verb);
5739
+ return;
5740
+ }
5583
5741
  if (invocation.kind === "status") {
5584
5742
  printStatus(invocation.agent, invocation.json);
5585
5743
  return;
@@ -5609,6 +5767,14 @@ async function main() {
5609
5767
  recover: invocation.recover
5610
5768
  }));
5611
5769
  }
5770
+ const relayRef = { current: null };
5771
+ const telemetry = createBridgeTelemetry({
5772
+ env: process.env,
5773
+ configDir: configDir(process.env),
5774
+ now: Date.now,
5775
+ out: (line) => console.log(line),
5776
+ send: (frame) => relayRef.current ? relayRef.current.sendTelemetry(frame) : false
5777
+ });
5612
5778
  {
5613
5779
  const parsed = parseRunFlags(invocation.argv);
5614
5780
  const gate = parsed.ok ? shouldOfferGuidedStart(parsed.rest, process.env) : { offer: false };
@@ -5680,7 +5846,8 @@ async function main() {
5680
5846
  printStatus: (name) => printStatus(name, false),
5681
5847
  verbose: invocation.argv.includes("--verbose"),
5682
5848
  out: (line) => console.log(line),
5683
- err: (line) => console.error(line)
5849
+ err: (line) => console.error(line),
5850
+ telemetry
5684
5851
  });
5685
5852
  if (outcome.action === "exit") process.exit(outcome.code);
5686
5853
  if (outcome.runToolMode !== void 0) {
@@ -5688,7 +5855,7 @@ async function main() {
5688
5855
  }
5689
5856
  }
5690
5857
  }
5691
- const cfg = await resolveConfig(invocation.argv, process.env);
5858
+ const cfg = await resolveConfig(invocation.argv, process.env, telemetry);
5692
5859
  resolvedAgentName = cfg.agentName ?? "";
5693
5860
  {
5694
5861
  const decision = shouldSuppressStart(
@@ -5740,7 +5907,7 @@ async function main() {
5740
5907
  const epScopeOff = process.env.BRAID_SCOPE === "off" || cfg.scope?.disabled === true;
5741
5908
  const readTextFile = (p) => {
5742
5909
  try {
5743
- return readFileSync8(p, "utf8");
5910
+ return readFileSync9(p, "utf8");
5744
5911
  } catch {
5745
5912
  return null;
5746
5913
  }
@@ -5900,11 +6067,14 @@ async function main() {
5900
6067
  // Honest status: declare "connected" only after the relay confirms the agent
5901
6068
  // (auth succeeded), never optimistically on factory return. Rendered as the
5902
6069
  // operator-guidance block: mode + keys, distinct from stream noise.
5903
- onReady: () => console.log(banner("braid", "CONNECTED", connectedBannerLines({
5904
- keysLive,
5905
- serviceEligible,
5906
- agentName: cfg.agentName ? sanitizeForTty(cfg.agentName) : void 0
5907
- }))),
6070
+ onReady: () => {
6071
+ telemetry.flushPending();
6072
+ console.log(banner("braid", "CONNECTED", connectedBannerLines({
6073
+ keysLive,
6074
+ serviceEligible,
6075
+ agentName: cfg.agentName ? sanitizeForTty(cfg.agentName) : void 0
6076
+ })));
6077
+ },
5908
6078
  // Disk-backed card cache: skips the expensive elicit subprocess on reconnects
5909
6079
  // when the agent's skill inventory hasn't changed (BL-003).
5910
6080
  cardCache: {
@@ -5913,6 +6083,13 @@ async function main() {
5913
6083
  return r && r.fingerprint === fp ? r : null;
5914
6084
  },
5915
6085
  save: (c, fp, card) => saveCachedCard(c, fp, card)
6086
+ },
6087
+ // BL-177 Task 4: the SAME telemetry instance constructed above, now wired to
6088
+ // the live RelayClient the instant it exists (onRelayCreated) so the two
6089
+ // denial-path capture sites (ACP/SDK, wired via makeAdapter) reach the wire.
6090
+ telemetry,
6091
+ onRelayCreated: (relay) => {
6092
+ relayRef.current = relay;
5916
6093
  }
5917
6094
  });
5918
6095
  if (cfg.runtimeVersion) {
@@ -4,7 +4,7 @@ import {
4
4
  boundedJsonFetch,
5
5
  resetHttpMetricsForTest,
6
6
  snapshotHttpMetrics
7
- } from "./chunk-QDDQF6RJ.js";
7
+ } from "./chunk-PQGSJM2V.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.83.0",
3
+ "version": "0.84.1",
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.21",
54
+ "@snowyroad/braid-tui": "0.0.22",
55
55
  "canonicalize": "^3.0.0",
56
56
  "jose": "^6.2.3",
57
57
  "ws": "8.21.0",
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@snowyroad/braid",
3
- "version": "0.83.0",
3
+ "version": "0.84.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@snowyroad/braid",
9
- "version": "0.83.0",
9
+ "version": "0.84.1",
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.21",
17
+ "@snowyroad/braid-tui": "0.0.22",
18
18
  "canonicalize": "^3.0.0",
19
19
  "jose": "^6.2.3",
20
20
  "ws": "8.21.0",
@@ -1183,9 +1183,9 @@
1183
1183
  ]
1184
1184
  },
1185
1185
  "node_modules/@snowyroad/braid-tui": {
1186
- "version": "0.0.21",
1187
- "resolved": "https://registry.npmjs.org/@snowyroad/braid-tui/-/braid-tui-0.0.21.tgz",
1188
- "integrity": "sha512-D4f2feEDibI4kldlkCGHGea5VCTuptmu/065nmiKa3f5aT20uQ/HhMqhTb+/xShVo3slB4L5ns8ukN0NLVk6KQ==",
1186
+ "version": "0.0.22",
1187
+ "resolved": "https://registry.npmjs.org/@snowyroad/braid-tui/-/braid-tui-0.0.22.tgz",
1188
+ "integrity": "sha512-Sgsn8Dl8v6qLwZjRnlPUKJEXM6nOLFu0B/d2bOAJycSu1AI35LyzWjrOoui6TIwDB7KKBYVkbdOKqfwUtHFbyA==",
1189
1189
  "license": "SEE LICENSE IN LICENSE.md",
1190
1190
  "dependencies": {
1191
1191
  "ink": "^5.2.0",