@birdybeep/cli 0.3.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -43,15 +43,191 @@ __export(index_exports, {
43
43
  module.exports = __toCommonJS(index_exports);
44
44
 
45
45
  // src/commands/agent.ts
46
- var import_claude_code = require("@birdybeep/claude-code");
46
+ var import_claude_code2 = require("@birdybeep/claude-code");
47
47
  var import_codex = require("@birdybeep/codex");
48
48
  var import_copilot = require("@birdybeep/copilot");
49
- var import_cursor = require("@birdybeep/cursor");
49
+ var import_cursor2 = require("@birdybeep/cursor");
50
50
  var import_opencode = require("@birdybeep/opencode");
51
51
 
52
- // src/framework.ts
52
+ // src/diagnostics.ts
53
53
  var import_node_fs = require("fs");
54
+ var import_node_os = require("os");
54
55
  var import_agent_core = require("@birdybeep/agent-core");
56
+ var import_claude_code = require("@birdybeep/claude-code");
57
+ var import_cursor = require("@birdybeep/cursor");
58
+ async function gatherIntegrations(adapters) {
59
+ return Promise.all(
60
+ adapters.map(async (a) => ({
61
+ harness: a.id,
62
+ displayName: a.displayName,
63
+ status: await a.status()
64
+ }))
65
+ );
66
+ }
67
+ async function isPaired(tokenOptions = {}) {
68
+ return await (0, import_agent_core.getToken)(tokenOptions) !== null;
69
+ }
70
+ function localQueueDepth() {
71
+ return new import_agent_core.LocalEventQueue().size();
72
+ }
73
+ function localQueueOverflowDrops() {
74
+ return new import_agent_core.LocalEventQueue().overflowDropCount();
75
+ }
76
+ function unpairedActivity() {
77
+ return (0, import_agent_core.readUnpairedNotice)();
78
+ }
79
+ function describeUnpairedActivity(notice) {
80
+ const since = new Date(notice.firstAt).toISOString();
81
+ const from = notice.harnesses.length > 0 ? ` from ${notice.harnesses.join(", ")}` : "";
82
+ return `${notice.count} event(s)${from} fired since ${since} and were NOT sent \u2014 this machine is not paired.`;
83
+ }
84
+ function asRecord(value) {
85
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
86
+ }
87
+ function birdyBeepHookCount(path, events, isBirdyBeepEntry) {
88
+ if (!(0, import_node_fs.existsSync)(path)) return 0;
89
+ let parsed;
90
+ try {
91
+ parsed = JSON.parse((0, import_node_fs.readFileSync)(path, "utf8"));
92
+ } catch {
93
+ return null;
94
+ }
95
+ const hooks = asRecord(asRecord(parsed)["hooks"]);
96
+ let present = 0;
97
+ for (const event of events) {
98
+ const entries = hooks[event];
99
+ if (Array.isArray(entries) && entries.some(isBirdyBeepEntry)) present += 1;
100
+ }
101
+ return present;
102
+ }
103
+ async function cursorBridgeOnly(opts = {}) {
104
+ const home = opts.home ?? (0, import_node_os.homedir)();
105
+ const detection = await (opts.detect ?? (() => (0, import_cursor.detectCursor)({ home })))();
106
+ if (!detection.detected) return false;
107
+ const claude = birdyBeepHookCount((0, import_claude_code.claudeSettingsPath)(home), import_claude_code.BIRDYBEEP_HOOK_EVENTS, import_claude_code.isBirdyBeepEntry);
108
+ if (claude === null || claude === 0) return false;
109
+ return birdyBeepHookCount((0, import_cursor.cursorHooksPath)(home), import_cursor.BIRDYBEEP_HOOK_EVENTS, import_cursor.isBirdyBeepEntry) === 0;
110
+ }
111
+ function filteredActivity() {
112
+ return (0, import_agent_core.readFilteredActivity)();
113
+ }
114
+ function describeFilteredActivity(activity) {
115
+ const types = Object.entries(activity.byType).sort(([, a], [, b]) => b - a).map(([type, n]) => `${type} \xD7${n}`).join(", ");
116
+ const since = new Date(activity.firstAt).toISOString();
117
+ return `${activity.count} local-only event(s) since ${since}${types ? ` (${types})` : ""} \u2014 hooks are firing; these types never beep, so they are not sent.`;
118
+ }
119
+ function machineIdentity() {
120
+ return (0, import_agent_core.getMachineIdentity)();
121
+ }
122
+ var CONFIGURED_STATUSES = /* @__PURE__ */ new Set([
123
+ "installed",
124
+ "needs_trust",
125
+ "needs_restart"
126
+ ]);
127
+ function gradeSurfaces(surfaces, status, observation) {
128
+ const builds = Object.values(observation?.builds ?? {});
129
+ const configured = CONFIGURED_STATUSES.has(status);
130
+ const claimedByKind = /* @__PURE__ */ new Map();
131
+ for (const s of surfaces) {
132
+ if (s.version === void 0) continue;
133
+ const versions = claimedByKind.get(s.kind) ?? /* @__PURE__ */ new Set();
134
+ versions.add(s.version);
135
+ claimedByKind.set(s.kind, versions);
136
+ }
137
+ const graded = surfaces.map((surface) => {
138
+ const exact = builds.filter(
139
+ (b) => b.surface === surface.kind && b.version === surface.version && surface.version !== void 0
140
+ );
141
+ let soleOfKind = [];
142
+ if (surface.version === void 0) {
143
+ const sameKindVersionless = surfaces.filter(
144
+ (s) => s.version === void 0 && s.kind === surface.kind
145
+ );
146
+ const unclaimed = builds.filter(
147
+ (b) => b.surface === surface.kind && !(claimedByKind.get(surface.kind)?.has(b.version) ?? false)
148
+ );
149
+ if (unclaimed.length === 1 && sameKindVersionless.length === 1) soleOfKind = unclaimed;
150
+ }
151
+ const unattributed = surface.version === void 0 ? [] : builds.filter((b) => b.surface === "unknown" && b.version === surface.version);
152
+ const sharesVersion = surface.version !== void 0 && surfaces.some((s) => s !== surface && s.version === surface.version);
153
+ const ambiguous = unattributed.length > 0 && sharesVersion;
154
+ const matched = [...exact, ...soleOfKind, ...ambiguous ? [] : unattributed];
155
+ const events = matched.reduce((total, b) => total + b.count, 0);
156
+ const lastAt = matched.reduce(
157
+ (latest, b) => latest === void 0 || b.lastAt > latest ? b.lastAt : latest,
158
+ void 0
159
+ );
160
+ const observedVersion = surface.version === void 0 ? soleOfKind[0]?.version : void 0;
161
+ return {
162
+ surface,
163
+ events,
164
+ ambiguous,
165
+ ...lastAt !== void 0 ? { lastAt } : {},
166
+ ...observedVersion !== void 0 ? { observedVersion } : {}
167
+ };
168
+ });
169
+ const anyActive = graded.some((g) => g.events > 0 && g.surface.shadowed !== true);
170
+ return graded.map(({ ambiguous, ...g }) => ({
171
+ ...g,
172
+ coverage: !configured ? "uncovered" : g.events > 0 ? "active" : anyActive && g.surface.shadowed !== true && !ambiguous ? "uncovered" : "wired"
173
+ }));
174
+ }
175
+ async function gatherSurfaces(adapters, options = {}) {
176
+ const observed = (0, import_agent_core.readObservedBuilds)(options.observedBuilds ?? {});
177
+ return Promise.all(
178
+ adapters.map(async (adapter) => {
179
+ const observation = observed[adapter.id];
180
+ const base4 = {
181
+ harness: adapter.id,
182
+ displayName: adapter.displayName,
183
+ unversionedEvents: observation?.unversioned ?? 0
184
+ };
185
+ try {
186
+ const [detection, status] = await Promise.all([adapter.detect(), adapter.status()]);
187
+ return {
188
+ ...base4,
189
+ status,
190
+ surfaces: detection.detected ? gradeSurfaces(detection.surfaces ?? [], status, observation) : []
191
+ };
192
+ } catch {
193
+ return { ...base4, status: "unknown", surfaces: [] };
194
+ }
195
+ })
196
+ );
197
+ }
198
+ function describeSurface(state) {
199
+ const version = state.surface.version ?? state.observedVersion;
200
+ return version !== void 0 ? `${state.surface.label} ${version}` : state.surface.label;
201
+ }
202
+ function describeSurfaceCoverage(state, group) {
203
+ if (state.coverage === "active") {
204
+ const last = state.lastAt !== void 0 ? `, last ${new Date(state.lastAt).toISOString()}` : "";
205
+ return `covered \u2014 ${state.events} event(s) from this build${last}`;
206
+ }
207
+ if (state.coverage === "wired") {
208
+ return state.surface.shadowed === true ? `${group.displayName}'s hooks are installed and this build shares them, but another install comes first on PATH \u2014 it only runs if that order changes` : `${group.displayName}'s hooks are installed and this build shares them; nothing has fired from it yet`;
209
+ }
210
+ if (!CONFIGURED_STATUSES.has(group.status)) {
211
+ return `not covered \u2014 ${group.displayName} carries no BirdyBeep hooks, so this build cannot beep`;
212
+ }
213
+ const active = group.surfaces.filter((s) => s.coverage === "active").map(describeSurface);
214
+ const delivering = active.join(", ");
215
+ const verb = active.length === 1 ? "is" : "are";
216
+ return `not covered \u2014 nothing has ever fired from this build, while ${delivering} ${verb} delivering through the same config`;
217
+ }
218
+ function installTarget(harness) {
219
+ return harness === "claude_code" ? "claude" : harness;
220
+ }
221
+ function surfaceRemedy(state, group) {
222
+ if (state.coverage !== "uncovered") return void 0;
223
+ if (!CONFIGURED_STATUSES.has(group.status)) return void 0;
224
+ const install = `\`birdybeep agent install ${installTarget(group.harness)}\``;
225
+ return state.surface.kind === "desktop" ? `Run a turn in ${state.surface.label}. If it stays uncovered, that build cannot run the hook command: a desktop app spawns its engine with your LOGIN shell's PATH, not an interactive shell's, so a bare command is invisible to it. Re-run ${install} from a shell where \`birdybeep\` resolves \u2014 it rewrites the entry with absolute paths that need no PATH at all.` : `Run a turn in ${state.surface.label}. If it stays uncovered, re-run ${install} from a shell where \`birdybeep\` resolves, then check that ${state.surface.enginePath} is the build you are actually running.`;
226
+ }
227
+
228
+ // src/framework.ts
229
+ var import_node_fs2 = require("fs");
230
+ var import_agent_core2 = require("@birdybeep/agent-core");
55
231
  var EXIT = { OK: 0, ERROR: 1, USAGE: 2 };
56
232
  function createIo(json, stdout, stderr) {
57
233
  return {
@@ -129,11 +305,17 @@ function isUnknownFlag(token, allowed) {
129
305
  function renderRootHelp(version, commands) {
130
306
  const width = Math.max(...commands.map((c) => c.name.length));
131
307
  const lines = commands.map((c) => ` ${c.name.padEnd(width)} ${c.summary}`);
308
+ const featured = commands.filter((c) => c.gettingStarted !== void 0);
132
309
  return [
133
310
  `birdybeep ${version} \u2014 stream coding-agent lifecycle events to BirdyBeep.`,
134
311
  "",
135
312
  "Usage:",
136
313
  " birdybeep <command> [options]",
314
+ ...featured.length > 0 ? [
315
+ "",
316
+ "Getting started:",
317
+ ...featured.map((c) => ` birdybeep ${c.name} ${c.gettingStarted ?? ""}`)
318
+ ] : [],
137
319
  "",
138
320
  "Commands:",
139
321
  ...lines,
@@ -188,7 +370,7 @@ async function dispatch(argv, deps) {
188
370
  const io = createIo(flags.json, deps.stdout, deps.stderr);
189
371
  if (deps.ensureConfig !== false) {
190
372
  try {
191
- (0, import_node_fs.mkdirSync)((0, import_agent_core.birdyBeepConfigDir)(), { recursive: true, mode: 448 });
373
+ (0, import_node_fs2.mkdirSync)((0, import_agent_core2.birdyBeepConfigDir)(), { recursive: true, mode: 448 });
192
374
  } catch {
193
375
  }
194
376
  }
@@ -215,7 +397,11 @@ async function dispatch(argv, deps) {
215
397
  if (rest.length === 0 || flags.help && command === void 0) {
216
398
  io.emit(renderRootHelp(deps.version, deps.commands), {
217
399
  version: deps.version,
218
- commands: deps.commands.map((c) => ({ name: c.name, summary: c.summary }))
400
+ commands: deps.commands.map((c) => ({
401
+ name: c.name,
402
+ summary: c.summary,
403
+ ...c.gettingStarted !== void 0 ? { gettingStarted: c.gettingStarted } : {}
404
+ }))
219
405
  });
220
406
  return EXIT.OK;
221
407
  }
@@ -269,10 +455,10 @@ async function dispatch(argv, deps) {
269
455
 
270
456
  // src/commands/agent.ts
271
457
  var DEFAULT_ADAPTERS = [
272
- import_claude_code.claudeCodeAdapter,
458
+ import_claude_code2.claudeCodeAdapter,
273
459
  import_codex.codexAdapter,
274
460
  import_opencode.opencodeAdapter,
275
- import_cursor.cursorAdapter,
461
+ import_cursor2.cursorAdapter,
276
462
  import_copilot.copilotAdapter
277
463
  ];
278
464
  var TARGET_TO_ID = {
@@ -296,7 +482,10 @@ function selectAdapters(target, adapters) {
296
482
  if (id === void 0) return "unknown";
297
483
  return adapters.filter((a) => a.id === id);
298
484
  }
299
- async function installSelected(adapters, ctx) {
485
+ function installTarget2(harness) {
486
+ return harness === "claude_code" ? "claude" : harness;
487
+ }
488
+ async function installSelected(adapters, ctx, tokenOptions) {
300
489
  const target = ctx.args[0] ?? "all";
301
490
  const selected = selectAdapters(target, adapters);
302
491
  if (selected === "unknown") {
@@ -323,8 +512,9 @@ async function installSelected(adapters, ctx) {
323
512
  requiredActions: result.requiredActions
324
513
  });
325
514
  }
515
+ const paired = await isPaired(tokenOptions);
326
516
  if (ctx.flags.json) {
327
- ctx.io.result({ target, results: outcomes });
517
+ ctx.io.result({ target, paired, results: outcomes });
328
518
  return EXIT.OK;
329
519
  }
330
520
  if (outcomes.length === 0 || outcomes.every((o) => !o.detected)) {
@@ -332,13 +522,20 @@ async function installSelected(adapters, ctx) {
332
522
  }
333
523
  for (const o of outcomes) {
334
524
  if (!o.detected) {
335
- ctx.io.line(`\u2013 ${o.displayName}: not detected (skipped)`);
525
+ ctx.io.line(
526
+ `\u2013 ${o.displayName}: not detected (skipped) \u2014 install it, then run \`birdybeep agent install ${installTarget2(o.harness)}\``
527
+ );
336
528
  continue;
337
529
  }
338
530
  const changed = (o.changedFiles ?? []).length > 0 ? o.changedFiles.join(", ") : "no changes";
339
531
  ctx.io.line(`\u2713 ${o.displayName}: ${o.status} (${changed})`);
340
532
  for (const action of o.requiredActions ?? []) ctx.io.line(` \u2192 ${action}`);
341
533
  }
534
+ if (!paired) {
535
+ ctx.io.line(
536
+ "\u26A0 This machine is not paired, so nothing these hooks produce can reach you. Run `birdybeep setup` \u2014 it pairs, wires up every agent, and sends a test Beep."
537
+ );
538
+ }
342
539
  return EXIT.OK;
343
540
  }
344
541
  async function uninstallSelected(adapters, ctx) {
@@ -377,6 +574,7 @@ async function uninstallSelected(adapters, ctx) {
377
574
  }
378
575
  function createAgentCommand(deps = {}) {
379
576
  const adapters = deps.adapters ?? DEFAULT_ADAPTERS;
577
+ const tokenOptions = deps.tokenOptions ?? {};
380
578
  return {
381
579
  name: "agent",
382
580
  summary: "Install or uninstall harness adapters",
@@ -386,7 +584,7 @@ function createAgentCommand(deps = {}) {
386
584
  name: "install",
387
585
  summary: "Install adapters (all | claude | codex | opencode | cursor | copilot)",
388
586
  usage: "birdybeep agent install [all|claude|codex|opencode|cursor|copilot]",
389
- run: (ctx) => installSelected(adapters, ctx)
587
+ run: (ctx) => installSelected(adapters, ctx, tokenOptions)
390
588
  },
391
589
  {
392
590
  name: "uninstall",
@@ -400,24 +598,24 @@ function createAgentCommand(deps = {}) {
400
598
 
401
599
  // src/commands/doctor.ts
402
600
  var import_agent_core4 = require("@birdybeep/agent-core");
403
- var import_claude_code2 = require("@birdybeep/claude-code");
601
+ var import_claude_code3 = require("@birdybeep/claude-code");
404
602
  var import_codex2 = require("@birdybeep/codex");
405
603
  var import_copilot2 = require("@birdybeep/copilot");
406
- var import_cursor2 = require("@birdybeep/cursor");
604
+ var import_cursor3 = require("@birdybeep/cursor");
407
605
  var import_opencode2 = require("@birdybeep/opencode");
408
606
 
409
607
  // src/config.ts
410
- var import_node_fs2 = require("fs");
608
+ var import_node_fs3 = require("fs");
411
609
  var import_node_path = require("path");
412
- var import_agent_core2 = require("@birdybeep/agent-core");
610
+ var import_agent_core3 = require("@birdybeep/agent-core");
413
611
  var DEFAULT_API_URL = "https://api.birdybeep.com";
414
612
  var CONFIG_FILE = "config.json";
415
613
  function cliConfigPath() {
416
- return (0, import_node_path.join)((0, import_agent_core2.birdyBeepConfigDir)(), CONFIG_FILE);
614
+ return (0, import_node_path.join)((0, import_agent_core3.birdyBeepConfigDir)(), CONFIG_FILE);
417
615
  }
418
616
  function readCliConfig() {
419
617
  try {
420
- const parsed = JSON.parse((0, import_node_fs2.readFileSync)(cliConfigPath(), "utf8"));
618
+ const parsed = JSON.parse((0, import_node_fs3.readFileSync)(cliConfigPath(), "utf8"));
421
619
  return typeof parsed === "object" && parsed !== null ? parsed : {};
422
620
  } catch {
423
621
  return {};
@@ -430,8 +628,8 @@ function writeCliConfig(patch) {
430
628
  if (apiUrl !== void 0) merged.apiUrl = apiUrl;
431
629
  const expectEmail = patch.expectEmail ?? current.expectEmail;
432
630
  if (expectEmail !== void 0) merged.expectEmail = expectEmail;
433
- (0, import_node_fs2.mkdirSync)((0, import_agent_core2.birdyBeepConfigDir)(), { recursive: true, mode: 448 });
434
- (0, import_node_fs2.writeFileSync)(cliConfigPath(), `${JSON.stringify(merged, null, 2)}
631
+ (0, import_node_fs3.mkdirSync)((0, import_agent_core3.birdyBeepConfigDir)(), { recursive: true, mode: 448 });
632
+ (0, import_node_fs3.writeFileSync)(cliConfigPath(), `${JSON.stringify(merged, null, 2)}
435
633
  `, { mode: 384 });
436
634
  }
437
635
  function resolveApiUrl() {
@@ -446,33 +644,12 @@ function resolveRegistryUrl() {
446
644
  return DEFAULT_REGISTRY_URL;
447
645
  }
448
646
 
449
- // src/diagnostics.ts
450
- var import_agent_core3 = require("@birdybeep/agent-core");
451
- async function gatherIntegrations(adapters) {
452
- return Promise.all(
453
- adapters.map(async (a) => ({
454
- harness: a.id,
455
- displayName: a.displayName,
456
- status: await a.status()
457
- }))
458
- );
459
- }
460
- async function isPaired(tokenOptions = {}) {
461
- return await (0, import_agent_core3.getToken)(tokenOptions) !== null;
462
- }
463
- function localQueueDepth() {
464
- return new import_agent_core3.LocalEventQueue().size();
465
- }
466
- function machineIdentity() {
467
- return (0, import_agent_core3.getMachineIdentity)();
468
- }
469
-
470
647
  // src/commands/doctor.ts
471
648
  var DEFAULT_ADAPTERS2 = [
472
- import_claude_code2.claudeCodeAdapter,
649
+ import_claude_code3.claudeCodeAdapter,
473
650
  import_codex2.codexAdapter,
474
651
  import_opencode2.opencodeAdapter,
475
- import_cursor2.cursorAdapter,
652
+ import_cursor3.cursorAdapter,
476
653
  import_copilot2.copilotAdapter
477
654
  ];
478
655
  async function defaultProbeNetwork(baseUrl) {
@@ -509,6 +686,32 @@ function createDoctorCommand(deps = {}) {
509
686
  remedy: "Run `birdybeep pair` to pair this machine."
510
687
  }
511
688
  );
689
+ const unpaired = unpairedActivity();
690
+ if (unpaired !== null) {
691
+ checks.push({
692
+ name: "Events lost while unpaired",
693
+ ok: false,
694
+ detail: describeUnpairedActivity(unpaired),
695
+ remedy: "Run `birdybeep pair`. Events that fired before pairing are gone \u2014 a first pairing does not replay them."
696
+ });
697
+ }
698
+ if (await cursorBridgeOnly(deps.detectCursor ? { detect: deps.detectCursor } : {})) {
699
+ checks.push({
700
+ name: "Approval beeps from Cursor",
701
+ ok: false,
702
+ detail: "Cursor is running your agent through the Claude Code hooks \u2014 that is why Cursor events arrive without a Cursor install. Its bridge drops Notification and PermissionRequest, so approvals never reach you.",
703
+ remedy: "Run `birdybeep agent install cursor` to get approval beeps from Cursor's own shell and MCP permission prompts. Keeping both installed is safe \u2014 duplicate events are collapsed."
704
+ });
705
+ }
706
+ const filtered = filteredActivity();
707
+ if (filtered !== null) {
708
+ checks.push({
709
+ name: "Local-only events (never notifiable)",
710
+ ok: true,
711
+ detail: describeFilteredActivity(filtered)
712
+ });
713
+ }
714
+ const surfaceGroups = await gatherSurfaces(adapters, deps.surfaceOptions ?? {});
512
715
  for (const adapter of adapters) {
513
716
  const result = await adapter.doctor();
514
717
  for (const c of result.checks) {
@@ -519,14 +722,26 @@ function createDoctorCommand(deps = {}) {
519
722
  ...c.remedy !== void 0 ? { remedy: c.remedy } : {}
520
723
  });
521
724
  }
725
+ const group = surfaceGroups.find((g) => g.harness === adapter.id);
726
+ if (group === void 0) continue;
727
+ for (const state of group.surfaces) {
728
+ const remedy = surfaceRemedy(state, group);
729
+ checks.push({
730
+ name: `${adapter.displayName}: ${describeSurface(state)}`,
731
+ ok: state.coverage !== "uncovered",
732
+ detail: describeSurfaceCoverage(state, group),
733
+ ...remedy !== void 0 ? { remedy } : {}
734
+ });
735
+ }
522
736
  }
523
737
  const depthBefore = localQueueDepth();
524
738
  const drain = await makeSender(apiUrl).drainNow();
525
739
  const depthAfter = localQueueDepth();
740
+ const overflowDropped = localQueueOverflowDrops();
526
741
  checks.push({
527
742
  name: "Local queue",
528
743
  ok: true,
529
- detail: `${depthBefore} queued \u2192 ${drain.delivered} delivered, ${depthAfter} remaining`
744
+ detail: `${depthBefore} queued \u2192 ${drain.delivered} delivered, ${depthAfter} remaining` + (overflowDropped > 0 ? `; ${overflowDropped} dropped by the ${import_agent_core4.DEFAULT_QUEUE_MAX_ENTRIES} entry cap` : "")
530
745
  });
531
746
  const reachable = await probeNetwork(apiUrl);
532
747
  checks.push(
@@ -542,7 +757,10 @@ function createDoctorCommand(deps = {}) {
542
757
  ctx.io.result({
543
758
  ok,
544
759
  checks,
545
- queue: { depthBefore, delivered: drain.delivered, depthAfter }
760
+ surfaces: surfaceGroups,
761
+ queue: { depthBefore, delivered: drain.delivered, depthAfter, overflowDropped },
762
+ ...unpaired !== null ? { unpairedActivity: unpaired } : {},
763
+ ...filtered !== null ? { filteredActivity: filtered } : {}
546
764
  });
547
765
  } else {
548
766
  for (const c of checks) {
@@ -559,20 +777,20 @@ function createDoctorCommand(deps = {}) {
559
777
  // src/commands/hook.ts
560
778
  var import_node_child_process = require("child_process");
561
779
  var import_node_crypto = require("crypto");
562
- var import_node_fs3 = require("fs");
563
- var import_node_os = require("os");
780
+ var import_node_fs4 = require("fs");
781
+ var import_node_os2 = require("os");
564
782
  var import_node_path2 = require("path");
565
783
  var import_agent_core5 = require("@birdybeep/agent-core");
566
- var import_claude_code3 = require("@birdybeep/claude-code");
784
+ var import_claude_code4 = require("@birdybeep/claude-code");
567
785
  var import_codex3 = require("@birdybeep/codex");
568
786
  var import_copilot3 = require("@birdybeep/copilot");
569
- var import_cursor3 = require("@birdybeep/cursor");
787
+ var import_cursor4 = require("@birdybeep/cursor");
570
788
  var import_opencode3 = require("@birdybeep/opencode");
571
789
  var RUNNERS = {
572
- claude: import_claude_code3.runClaudeHook,
790
+ claude: import_claude_code4.runClaudeHook,
573
791
  codex: import_codex3.runCodexHook,
574
792
  opencode: import_opencode3.runOpenCodeHook,
575
- cursor: import_cursor3.runCursorHook
793
+ cursor: import_cursor4.runCursorHook
576
794
  };
577
795
  var HOOK_HARNESSES = [
578
796
  "claude",
@@ -599,12 +817,43 @@ function withTimeout(promise, ms, fallback) {
599
817
  function isHarnessName(value) {
600
818
  return value === "claude" || value === "codex" || value === "opencode" || value === "cursor" || value === "copilot";
601
819
  }
820
+ function resolveHookHarness(harness, payload) {
821
+ return harness === "claude" && (0, import_cursor4.isCursorHookPayload)(payload) ? "cursor" : harness;
822
+ }
823
+ function recognizesPayload(harness, payload) {
824
+ switch (harness) {
825
+ case "claude":
826
+ return (0, import_claude_code4.isClaudeCodeHookPayload)(payload);
827
+ case "cursor":
828
+ return (0, import_cursor4.isCursorHookEventName)(asRecord2(payload)["hook_event_name"]);
829
+ case "codex":
830
+ return (0, import_codex3.isCodexHookPayload)(payload);
831
+ case "opencode":
832
+ return (0, import_opencode3.isOpenCodeEventPayload)(payload);
833
+ case "copilot":
834
+ return (0, import_copilot3.isCopilotHookPayload)(payload);
835
+ }
836
+ }
837
+ function asRecord2(value) {
838
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
839
+ }
840
+ function describeDiscriminator(payload) {
841
+ const record = asRecord2(payload);
842
+ for (const field of ["hook_event_name", "type"]) {
843
+ const value = record[field];
844
+ if (typeof value !== "string") continue;
845
+ const capped = value.length > 64 ? `${value.slice(0, 63)}\u2026` : value;
846
+ return `${field} ${JSON.stringify(capped)}`;
847
+ }
848
+ return "the payload";
849
+ }
602
850
  function runHookCommand(harness, payload, sender, copilotEventName) {
603
- if (harness === "copilot") {
851
+ const handler = resolveHookHarness(harness, payload);
852
+ if (handler === "copilot") {
604
853
  if (copilotEventName === void 0) return Promise.resolve({ outcome: "skipped" });
605
854
  return (0, import_copilot3.runCopilotHook)(copilotEventName, payload, { sender });
606
855
  }
607
- return RUNNERS[harness](payload, { sender });
856
+ return RUNNERS[handler](payload, { sender });
608
857
  }
609
858
  function readStdinDefault() {
610
859
  return new Promise((resolve) => {
@@ -630,10 +879,10 @@ function detachCodexNotifyWorker(payload) {
630
879
  try {
631
880
  const birdybeep = (0, import_agent_core5.resolveOnPath)("birdybeep");
632
881
  if (birdybeep === null) return false;
633
- const tmpFile = (0, import_node_path2.join)((0, import_node_os.tmpdir)(), `birdybeep-notify-${(0, import_node_crypto.randomBytes)(16).toString("hex")}.json`);
882
+ const tmpFile = (0, import_node_path2.join)((0, import_node_os2.tmpdir)(), `birdybeep-notify-${(0, import_node_crypto.randomBytes)(16).toString("hex")}.json`);
634
883
  file = tmpFile;
635
- (0, import_node_fs3.writeFileSync)(tmpFile, payload, { mode: 384 });
636
- fd = (0, import_node_fs3.openSync)(tmpFile, "r");
884
+ (0, import_node_fs4.writeFileSync)(tmpFile, payload, { mode: 384 });
885
+ fd = (0, import_node_fs4.openSync)(tmpFile, "r");
637
886
  const child = (0, import_node_child_process.spawn)(birdybeep, ["hook", "codex"], {
638
887
  cwd: (0, import_node_path2.dirname)(birdybeep),
639
888
  // trusted dir, never the inherited/attacker cwd
@@ -647,7 +896,7 @@ function detachCodexNotifyWorker(payload) {
647
896
  });
648
897
  child.on("error", () => {
649
898
  try {
650
- (0, import_node_fs3.rmSync)(tmpFile, { force: true });
899
+ (0, import_node_fs4.rmSync)(tmpFile, { force: true });
651
900
  } catch {
652
901
  }
653
902
  });
@@ -656,7 +905,7 @@ function detachCodexNotifyWorker(payload) {
656
905
  } catch {
657
906
  if (file !== void 0) {
658
907
  try {
659
- (0, import_node_fs3.rmSync)(file, { force: true });
908
+ (0, import_node_fs4.rmSync)(file, { force: true });
660
909
  } catch {
661
910
  }
662
911
  }
@@ -664,7 +913,7 @@ function detachCodexNotifyWorker(payload) {
664
913
  } finally {
665
914
  if (fd !== void 0) {
666
915
  try {
667
- (0, import_node_fs3.closeSync)(fd);
916
+ (0, import_node_fs4.closeSync)(fd);
668
917
  } catch {
669
918
  }
670
919
  }
@@ -691,35 +940,76 @@ function createHookCommand(deps = {}) {
691
940
  return EXIT.OK;
692
941
  }
693
942
  const copilotEventName = harness === "copilot" && (0, import_copilot3.isCopilotHookEventName)(ctx.args[1]) ? ctx.args[1] : void 0;
694
- const raw = await withTimeout(
943
+ if (harness === "copilot" && copilotEventName === void 0) {
944
+ ctx.io.errline(
945
+ `birdybeep hook copilot: second argument must be a Copilot hook event name, got ${JSON.stringify(ctx.args[1] ?? "(none)")} \u2014 nothing was sent.`
946
+ );
947
+ return EXIT.USAGE;
948
+ }
949
+ const read = await withTimeout(
695
950
  readHookPayload(ctx.args, readStdin, harness === "copilot"),
696
951
  stdinTimeoutMs,
697
- ""
952
+ null
698
953
  );
699
954
  const notifyStdinFile = process.env[NOTIFY_STDIN_FILE_ENV];
700
- if (notifyStdinFile !== void 0 && (0, import_node_path2.dirname)(notifyStdinFile) === (0, import_node_os.tmpdir)() && (0, import_node_path2.basename)(notifyStdinFile).startsWith("birdybeep-notify-")) {
955
+ if (notifyStdinFile !== void 0 && (0, import_node_path2.dirname)(notifyStdinFile) === (0, import_node_os2.tmpdir)() && (0, import_node_path2.basename)(notifyStdinFile).startsWith("birdybeep-notify-")) {
701
956
  try {
702
- (0, import_node_fs3.rmSync)(notifyStdinFile, { force: true });
957
+ (0, import_node_fs4.rmSync)(notifyStdinFile, { force: true });
703
958
  } catch {
704
959
  }
705
960
  }
961
+ const drop = (reason, detail) => {
962
+ ctx.io.result({ harness, outcome: "skipped", reason });
963
+ ctx.io.errline(`birdybeep hook ${harness}: ${detail} \u2014 nothing was sent.`);
964
+ return EXIT.ERROR;
965
+ };
966
+ if (read === null) {
967
+ return drop(
968
+ "stdin-timeout",
969
+ `timed out after ${stdinTimeoutMs}ms waiting for the payload on stdin`
970
+ );
971
+ }
972
+ const raw = read;
973
+ if (raw.trim().length === 0) {
974
+ return drop("empty-payload", "the payload was empty");
975
+ }
706
976
  let payload;
707
977
  try {
708
978
  payload = JSON.parse(raw);
709
979
  } catch {
710
- ctx.io.result({ harness, outcome: "skipped" });
711
- return EXIT.OK;
980
+ return drop("invalid-json", `the ${raw.length}-byte payload is not valid JSON`);
981
+ }
982
+ const handler = resolveHookHarness(harness, payload);
983
+ const routedFrom = handler !== harness ? { routedFrom: harness } : {};
984
+ if (!recognizesPayload(handler, payload)) {
985
+ ctx.io.result({
986
+ harness: handler,
987
+ ...routedFrom,
988
+ outcome: "skipped",
989
+ reason: "foreign-payload"
990
+ });
991
+ const article = handler === "opencode" ? "an" : "a";
992
+ ctx.io.errline(
993
+ `birdybeep hook ${harness}: ${describeDiscriminator(payload)} is not ${article} ${handler} hook event \u2014 nothing was sent. Check which tool is running this hook.`
994
+ );
995
+ return EXIT.ERROR;
712
996
  }
713
997
  const sender = makeSender(resolveApiUrl());
714
998
  const result = await runHookCommand(harness, payload, sender, copilotEventName);
715
999
  ctx.io.result({
716
- harness,
1000
+ harness: handler,
1001
+ ...routedFrom,
717
1002
  ...copilotEventName !== void 0 ? { event: copilotEventName } : {},
718
1003
  outcome: result.outcome,
719
1004
  eventType: result.eventType,
720
1005
  ...result.send?.decision ? { decision: result.send.decision } : {},
721
1006
  ...result.send?.status !== void 0 ? { status: result.send.status } : {}
722
1007
  });
1008
+ if (result.outcome === "unpaired") {
1009
+ ctx.io.errline(
1010
+ "birdybeep: this machine is not paired \u2014 the event was not sent and was not queued. Run `birdybeep pair` (or `birdybeep doctor` to see how many events this has cost)."
1011
+ );
1012
+ }
723
1013
  return EXIT.OK;
724
1014
  }
725
1015
  };
@@ -777,8 +1067,8 @@ function createUnpairCommand(deps = {}) {
777
1067
  }
778
1068
 
779
1069
  // src/commands/pair.ts
780
- var import_node_fs4 = require("fs");
781
- var import_agent_core8 = require("@birdybeep/agent-core");
1070
+ var import_node_fs5 = require("fs");
1071
+ var import_agent_core9 = require("@birdybeep/agent-core");
782
1072
  var import_uqr = require("uqr");
783
1073
 
784
1074
  // src/pairing.ts
@@ -852,7 +1142,277 @@ async function pairTokenPoll(apiUrl, deviceCode, fetchImpl, machineFingerprint,
852
1142
  }
853
1143
 
854
1144
  // src/version.ts
855
- var CLI_VERSION = "0.3.0".length > 0 ? "0.3.0" : "0.0.0";
1145
+ var CLI_VERSION = "0.6.0".length > 0 ? "0.6.0" : "0.0.0";
1146
+
1147
+ // src/commands/setup.ts
1148
+ var import_claude_code5 = require("@birdybeep/claude-code");
1149
+ var import_codex4 = require("@birdybeep/codex");
1150
+ var import_copilot4 = require("@birdybeep/copilot");
1151
+ var import_cursor5 = require("@birdybeep/cursor");
1152
+ var import_opencode4 = require("@birdybeep/opencode");
1153
+
1154
+ // src/commands/test.ts
1155
+ var import_node_crypto2 = require("crypto");
1156
+ var import_agent_core8 = require("@birdybeep/agent-core");
1157
+ function buildTestEvent(opts = {}) {
1158
+ const machine = (0, import_agent_core8.getMachineIdentity)();
1159
+ return (0, import_agent_core8.normalizeEvent)(
1160
+ {
1161
+ event_type: "test",
1162
+ status: "running",
1163
+ harness: "claude_code",
1164
+ // schema requires a harness; the "test" type distinguishes it
1165
+ // Unique per run: a repeat `birdybeep test` inside the backend's dedupe window must
1166
+ // still beep — a constant id made the second test silently "deduped" (9fh).
1167
+ source_session_id: `birdybeep-cli-test-${(0, import_node_crypto2.randomUUID)()}`,
1168
+ machine: { label: machine.label, os: machine.os },
1169
+ workspace: { cwd: process.cwd() },
1170
+ title: "BirdyBeep test event",
1171
+ body: "If you can see this, your machine is wired up correctly.",
1172
+ metadata: { test: true }
1173
+ },
1174
+ opts
1175
+ );
1176
+ }
1177
+ function createTestCommand(deps = {}) {
1178
+ const makeSender = deps.createSender ?? ((baseUrl) => (0, import_agent_core8.createSender)(
1179
+ deps.tokenOptions ? { baseUrl, tokenOptions: deps.tokenOptions } : { baseUrl }
1180
+ ));
1181
+ return {
1182
+ name: "test",
1183
+ summary: "Send a test event end-to-end",
1184
+ usage: "birdybeep test [--json]",
1185
+ run: async (ctx) => {
1186
+ const event = buildTestEvent();
1187
+ const result = await makeSender(resolveApiUrl()).send(event);
1188
+ if (ctx.flags.json) {
1189
+ ctx.io.result({
1190
+ outcome: result.outcome,
1191
+ ...result.status ? { status: result.status } : {},
1192
+ ...result.decision ? { decision: result.decision } : {}
1193
+ });
1194
+ } else if (result.outcome === "delivered") {
1195
+ if (result.decision === "notified" || result.decision === void 0) {
1196
+ ctx.io.line("\u2713 Test event delivered \u2014 check your phone for a test Beep.");
1197
+ } else if (result.decision === "suppressed") {
1198
+ ctx.io.line(
1199
+ "\u26A0 The backend accepted the test event but suppressed the push \u2014 this machine or integration is probably muted. Check mutes in the app, or run `birdybeep doctor`."
1200
+ );
1201
+ } else if (result.decision === "deduped") {
1202
+ ctx.io.line(
1203
+ "\u26A0 The backend accepted the test event but folded it into a recent duplicate \u2014 wait ~30s and run `birdybeep test` again."
1204
+ );
1205
+ } else {
1206
+ ctx.io.line(
1207
+ `\u26A0 The backend accepted the test event but decided "${result.decision}" \u2014 no push was sent. Run \`birdybeep doctor\`.`
1208
+ );
1209
+ }
1210
+ } else if (result.outcome === "unpaired") {
1211
+ ctx.io.line(
1212
+ "\u2717 NOT PAIRED \u2014 this machine has no BirdyBeep machine token, so nothing was sent (and nothing was queued). Run `birdybeep pair`."
1213
+ );
1214
+ } else if (result.outcome === "queued") {
1215
+ ctx.io.line("\u2022 Offline \u2014 test event queued; it will deliver when you reconnect.");
1216
+ } else {
1217
+ ctx.io.line("\u2717 Test event was rejected by the backend. Run `birdybeep doctor`.");
1218
+ }
1219
+ return result.outcome === "dropped" || result.outcome === "unpaired" ? EXIT.ERROR : EXIT.OK;
1220
+ }
1221
+ };
1222
+ }
1223
+
1224
+ // src/commands/setup.ts
1225
+ var SETUP_ADAPTERS = [
1226
+ import_claude_code5.claudeCodeAdapter,
1227
+ import_codex4.codexAdapter,
1228
+ import_opencode4.opencodeAdapter,
1229
+ import_cursor5.cursorAdapter,
1230
+ import_copilot4.copilotAdapter
1231
+ ];
1232
+ function failedSetupReport(error) {
1233
+ return {
1234
+ harnesses: [],
1235
+ counts: { installed: 0, needsYou: 0, notInstalled: 0, failed: 0 },
1236
+ error,
1237
+ ok: false
1238
+ };
1239
+ }
1240
+ var PENDING_STATUSES = /* @__PURE__ */ new Set([
1241
+ "needs_trust",
1242
+ "needs_restart"
1243
+ ]);
1244
+ async function installDetected(adapters) {
1245
+ const installs = [];
1246
+ for (const adapter of adapters) {
1247
+ try {
1248
+ const detection = await adapter.detect();
1249
+ if (!detection.detected) {
1250
+ installs.push({ adapter, detected: false });
1251
+ continue;
1252
+ }
1253
+ installs.push({ adapter, detected: true, result: await adapter.install() });
1254
+ } catch (err) {
1255
+ installs.push({
1256
+ adapter,
1257
+ detected: true,
1258
+ error: err instanceof Error ? err.message : String(err)
1259
+ });
1260
+ }
1261
+ }
1262
+ return installs;
1263
+ }
1264
+ function rowState(state, group, status) {
1265
+ if (status === "error" || group.status === "error") return "failed";
1266
+ if (status !== void 0 && PENDING_STATUSES.has(status)) return "needs you";
1267
+ if (state.coverage === "active") return "beeping";
1268
+ if (state.coverage === "wired") return "ready";
1269
+ return "not covered";
1270
+ }
1271
+ function buildHarnessReports(installs, groups) {
1272
+ return installs.map((install) => {
1273
+ const { adapter } = install;
1274
+ const base4 = {
1275
+ harness: adapter.id,
1276
+ displayName: adapter.displayName,
1277
+ detected: install.detected,
1278
+ ...install.result !== void 0 ? {
1279
+ status: install.result.status,
1280
+ changedFiles: install.result.changedFiles,
1281
+ backupFiles: install.result.backupFiles
1282
+ } : {},
1283
+ ...install.error !== void 0 ? { error: install.error } : {}
1284
+ };
1285
+ if (install.error !== void 0) {
1286
+ return {
1287
+ ...base4,
1288
+ actions: [
1289
+ `${adapter.displayName} could not be set up: ${install.error}`,
1290
+ `Run \`birdybeep agent install ${installTarget2(adapter.id)}\` to retry it on its own.`
1291
+ ],
1292
+ rows: [{ harness: adapter.id, displayName: adapter.displayName, state: "failed" }]
1293
+ };
1294
+ }
1295
+ if (!install.detected) {
1296
+ return {
1297
+ ...base4,
1298
+ actions: [],
1299
+ rows: [
1300
+ {
1301
+ harness: adapter.id,
1302
+ displayName: adapter.displayName,
1303
+ state: "not installed"
1304
+ }
1305
+ ]
1306
+ };
1307
+ }
1308
+ const group = groups.find((g) => g.harness === adapter.id);
1309
+ const status = install.result?.status;
1310
+ const surfaces = group?.surfaces ?? [];
1311
+ const rows = group === void 0 || surfaces.length === 0 ? [
1312
+ {
1313
+ harness: adapter.id,
1314
+ displayName: adapter.displayName,
1315
+ state: status !== void 0 && PENDING_STATUSES.has(status) ? "needs you" : "ready"
1316
+ }
1317
+ ] : surfaces.map((state) => {
1318
+ const graded = rowState(state, group, status);
1319
+ const remedy = surfaceRemedy(state, group) ?? (graded === "not covered" ? `${adapter.displayName} carries no BirdyBeep hooks \u2014 re-run \`birdybeep agent install ${installTarget2(adapter.id)}\` from a shell where \`birdybeep\` resolves.` : void 0);
1320
+ return {
1321
+ harness: adapter.id,
1322
+ displayName: adapter.displayName,
1323
+ build: describeSurface(state),
1324
+ kind: state.surface.kind,
1325
+ state: graded,
1326
+ ...remedy !== void 0 ? { remedy } : {}
1327
+ };
1328
+ });
1329
+ return { ...base4, actions: [...install.result?.requiredActions ?? []], rows };
1330
+ });
1331
+ }
1332
+ function pad(text, width) {
1333
+ return text.length >= width ? text : text + " ".repeat(width - text.length);
1334
+ }
1335
+ var MARKS = {
1336
+ beeping: "\u2713",
1337
+ ready: "\u2713",
1338
+ "needs you": "!",
1339
+ "not covered": "\u2717",
1340
+ "not installed": "\u2013",
1341
+ failed: "\u2717"
1342
+ };
1343
+ function renderCoverageTable(reports) {
1344
+ const rows = reports.flatMap((r) => r.rows);
1345
+ const nameWidth = Math.max(7, ...rows.map((r) => r.displayName.length));
1346
+ const buildWidth = Math.max(5, ...rows.map((r) => (r.build ?? "\u2014").length));
1347
+ const lines = ["coverage", ` ${pad("harness", nameWidth)} ${pad("build", buildWidth)} state`];
1348
+ for (const report of reports) {
1349
+ for (const row of report.rows) {
1350
+ lines.push(
1351
+ `${MARKS[row.state]} ${pad(row.displayName, nameWidth)} ${pad(row.build ?? "\u2014", buildWidth)} ${row.state}`
1352
+ );
1353
+ if (row.remedy !== void 0) lines.push(` \u2192 ${row.remedy}`);
1354
+ }
1355
+ for (const action of report.actions) lines.push(` \u2192 ${action}`);
1356
+ }
1357
+ return lines;
1358
+ }
1359
+ function describeMissing(reports) {
1360
+ const missing = reports.filter((r) => !r.detected && r.error === void 0);
1361
+ if (missing.length === 0) return [];
1362
+ const names = missing.map((r) => r.displayName);
1363
+ if (missing.length === reports.length) {
1364
+ return [
1365
+ "No supported coding agent is installed on this machine, so there was nothing to wire up.",
1366
+ `Install one of ${names.join(", ")}, then run \`birdybeep setup\` again \u2014 pairing is already done, so it picks up from here.`
1367
+ ];
1368
+ }
1369
+ return [
1370
+ `Not installed: ${names.join(", ")}. Install any of them, then run \`birdybeep setup\` again to wire it up.`
1371
+ ];
1372
+ }
1373
+ async function runHarnessSetup(ctx, options, deps = {}) {
1374
+ const adapters = deps.adapters ?? [...SETUP_ADAPTERS];
1375
+ const installs = await installDetected(adapters);
1376
+ const groups = await gatherSurfaces(adapters, deps.surfaceOptions ?? {});
1377
+ const reports = buildHarnessReports(installs, groups);
1378
+ ctx.io.line("");
1379
+ for (const line of renderCoverageTable(reports)) ctx.io.line(line);
1380
+ const missing = describeMissing(reports);
1381
+ if (missing.length > 0) {
1382
+ ctx.io.line("");
1383
+ for (const line of missing) ctx.io.line(line);
1384
+ }
1385
+ const counts = {
1386
+ installed: reports.filter((r) => r.detected && r.error === void 0).length,
1387
+ needsYou: reports.filter((r) => r.rows.some((row) => row.state === "needs you")).length,
1388
+ notInstalled: reports.filter((r) => !r.detected && r.error === void 0).length,
1389
+ // A row that graded `failed` counts too: an adapter that returned status "error" never threw,
1390
+ // so counting only thrown errors would report a clean run over a harness that is broken.
1391
+ failed: reports.filter((r) => r.error !== void 0 || r.rows.some((x) => x.state === "failed")).length
1392
+ };
1393
+ let beep;
1394
+ let beepOk = true;
1395
+ if (options.sendTest) {
1396
+ ctx.io.line("");
1397
+ const command = createTestCommand({
1398
+ ...deps.createSender !== void 0 ? { createSender: deps.createSender } : {},
1399
+ ...deps.tokenOptions !== void 0 ? { tokenOptions: deps.tokenOptions } : {}
1400
+ });
1401
+ const beepIo = {
1402
+ ...ctx.io,
1403
+ result: (value) => {
1404
+ beep = value;
1405
+ }
1406
+ };
1407
+ beepOk = await command.run?.({ args: [], flags: ctx.flags, io: beepIo }) === 0;
1408
+ }
1409
+ return {
1410
+ harnesses: reports,
1411
+ counts,
1412
+ ...beep !== void 0 ? { beep } : {},
1413
+ ok: counts.failed === 0 && beepOk
1414
+ };
1415
+ }
856
1416
 
857
1417
  // src/commands/pair.ts
858
1418
  var DEFAULT_POLL_INTERVAL_MS = 2e3;
@@ -861,11 +1421,15 @@ function renderQrMatrix(qrPayload) {
861
1421
  return (0, import_uqr.renderUnicodeCompact)(qrPayload, { border: 2 });
862
1422
  }
863
1423
  function parsePairFlags(args) {
864
- const flags = { yes: false };
1424
+ const flags = { yes: false, noInstall: false, noTest: false };
865
1425
  for (let i = 0; i < args.length; i += 1) {
866
1426
  const token = args[i] ?? "";
867
1427
  if (token === "--yes" || token === "-y") {
868
1428
  flags.yes = true;
1429
+ } else if (token === "--no-install") {
1430
+ flags.noInstall = true;
1431
+ } else if (token === "--no-test") {
1432
+ flags.noTest = true;
869
1433
  } else if (token === "--expect-email" || token.startsWith("--expect-email=")) {
870
1434
  const inline = token.startsWith("--expect-email=") ? token.slice("--expect-email=".length) : void 0;
871
1435
  const value = inline ?? args[++i];
@@ -932,14 +1496,14 @@ function canOpenControllingTerminal(path = controllingTerminalPath(), platform =
932
1496
  if (platform === "win32") return false;
933
1497
  let fd;
934
1498
  try {
935
- fd = (0, import_node_fs4.openSync)(path, "r");
1499
+ fd = (0, import_node_fs5.openSync)(path, "r");
936
1500
  return true;
937
1501
  } catch {
938
1502
  return false;
939
1503
  } finally {
940
1504
  if (fd !== void 0) {
941
1505
  try {
942
- (0, import_node_fs4.closeSync)(fd);
1506
+ (0, import_node_fs5.closeSync)(fd);
943
1507
  } catch {
944
1508
  }
945
1509
  }
@@ -953,7 +1517,7 @@ async function promptForAnswer(question, on) {
953
1517
  input = process.stdin;
954
1518
  } else {
955
1519
  const { ReadStream } = await import("tty");
956
- ttyFd = (0, import_node_fs4.openSync)(controllingTerminalPath(), "r");
1520
+ ttyFd = (0, import_node_fs5.openSync)(controllingTerminalPath(), "r");
957
1521
  input = new ReadStream(ttyFd);
958
1522
  }
959
1523
  return new Promise((resolve) => {
@@ -973,7 +1537,7 @@ async function promptForAnswer(question, on) {
973
1537
  }
974
1538
  if (ttyFd !== void 0) {
975
1539
  try {
976
- (0, import_node_fs4.closeSync)(ttyFd);
1540
+ (0, import_node_fs5.closeSync)(ttyFd);
977
1541
  } catch {
978
1542
  }
979
1543
  }
@@ -985,7 +1549,18 @@ async function promptForAnswer(question, on) {
985
1549
  input.once?.("error", () => done(""));
986
1550
  });
987
1551
  }
988
- function createPairCommand(deps = {}) {
1552
+ async function runSetupChain(ctx, deps, flags) {
1553
+ try {
1554
+ return await runHarnessSetup(ctx, { sendTest: !flags.noTest }, deps);
1555
+ } catch (err) {
1556
+ const message = err instanceof Error ? err.message : String(err);
1557
+ ctx.io.errline(
1558
+ `This machine is paired, but wiring up your coding agents failed: ${message}. Run \`birdybeep agent install all\` to do it on its own, then \`birdybeep doctor\`.`
1559
+ );
1560
+ return failedSetupReport(message);
1561
+ }
1562
+ }
1563
+ function createPairingCommand(verb, deps = {}) {
989
1564
  const fetchImpl = deps.fetchImpl ?? fetch;
990
1565
  const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
991
1566
  const clock = deps.now ?? (() => Date.now());
@@ -998,9 +1573,10 @@ function createPairCommand(deps = {}) {
998
1573
  return typeof pinned === "string" && pinned.trim().length > 0 ? pinned : void 0;
999
1574
  });
1000
1575
  return {
1001
- name: "pair",
1002
- summary: "Pair this machine with your BirdyBeep account (QR or manual)",
1003
- usage: "birdybeep pair [--yes] [--expect-email <addr>] [--json]",
1576
+ name: verb.name,
1577
+ summary: verb.summary,
1578
+ usage: verb.usage,
1579
+ ...verb.gettingStarted !== void 0 ? { gettingStarted: verb.gettingStarted } : {},
1004
1580
  options: [
1005
1581
  {
1006
1582
  flag: "--yes",
@@ -1011,18 +1587,43 @@ function createPairCommand(deps = {}) {
1011
1587
  flag: "--expect-email",
1012
1588
  value: "<addr>",
1013
1589
  summary: "Only trust the pairing if this account approved it (else fail)"
1590
+ },
1591
+ {
1592
+ flag: "--no-install",
1593
+ summary: "Stop after pairing \u2014 don't detect or wire up any coding agent"
1594
+ },
1595
+ {
1596
+ flag: "--no-test",
1597
+ summary: "Don't send the test Beep at the end"
1014
1598
  }
1015
1599
  ],
1016
1600
  run: async (ctx) => {
1017
1601
  const pairFlags = parsePairFlags(ctx.args);
1018
1602
  if (pairFlags.error !== void 0) {
1019
- ctx.io.errline(`birdybeep pair: ${pairFlags.error}.`);
1603
+ ctx.io.errline(`birdybeep ${verb.name}: ${pairFlags.error}.`);
1020
1604
  return EXIT.USAGE;
1021
1605
  }
1606
+ const chain = deps.setup === false || pairFlags.noInstall ? void 0 : deps.setup ?? {};
1607
+ const setupDeps = {
1608
+ ...deps.tokenOptions !== void 0 ? { tokenOptions: deps.tokenOptions } : {},
1609
+ ...chain
1610
+ };
1611
+ if (verb.skipWhenPaired && await (0, import_agent_core9.getToken)(deps.tokenOptions ?? {}) !== null) {
1612
+ ctx.io.line(
1613
+ chain !== void 0 ? "\u2713 Already paired \u2014 checking which coding agents are wired up." : "\u2713 Already paired. Nothing else to do with --no-install."
1614
+ );
1615
+ const report2 = chain !== void 0 ? await runSetupChain(ctx, setupDeps, pairFlags) : void 0;
1616
+ ctx.io.result({
1617
+ paired: true,
1618
+ alreadyPaired: true,
1619
+ ...report2 !== void 0 ? { setup: report2 } : {}
1620
+ });
1621
+ return report2 !== void 0 && !report2.ok ? EXIT.ERROR : EXIT.OK;
1622
+ }
1022
1623
  const apiUrl = resolveApiUrl();
1023
- const identity = (0, import_agent_core8.getMachineIdentity)();
1024
- const codeVerifier = (0, import_agent_core8.generateCodeVerifier)();
1025
- const codeChallenge = (0, import_agent_core8.deriveCodeChallengeS256)(codeVerifier);
1624
+ const identity = (0, import_agent_core9.getMachineIdentity)();
1625
+ const codeVerifier = (0, import_agent_core9.generateCodeVerifier)();
1626
+ const codeChallenge = (0, import_agent_core9.deriveCodeChallengeS256)(codeVerifier);
1026
1627
  const start = await pairStart(
1027
1628
  apiUrl,
1028
1629
  { machineLabel: identity.label, os: identity.os, cliVersion: CLI_VERSION, codeChallenge },
@@ -1118,21 +1719,52 @@ function createPairCommand(deps = {}) {
1118
1719
  );
1119
1720
  return EXIT.ERROR;
1120
1721
  }
1121
- await (0, import_agent_core8.setToken)(paired.machineToken, deps.tokenOptions ?? {});
1722
+ await (0, import_agent_core9.setToken)(paired.machineToken, deps.tokenOptions ?? {});
1122
1723
  writeCliConfig({ apiUrl });
1724
+ const discarded = new import_agent_core9.LocalEventQueue().discardBefore(clock());
1725
+ (0, import_agent_core9.clearUnpairedNotice)();
1123
1726
  const humanSuffix = approvedBy !== void 0 ? ` to ${approvedBy}` : "";
1124
- ctx.io.emit(`\u2713 Paired${humanSuffix}. Run \`birdybeep test\` to send a test Beep.`, {
1727
+ const discardedSuffix = discarded > 0 ? ` Discarded ${discarded} event(s) queued before pairing \u2014 you won't be beeped about them.` : "";
1728
+ const nextStep = chain === void 0 ? " Run `birdybeep setup` to wire up your coding agents." : "";
1729
+ ctx.io.line(`\u2713 Paired${humanSuffix}.${nextStep}${discardedSuffix}`);
1730
+ const report = chain !== void 0 ? await runSetupChain(ctx, setupDeps, pairFlags) : void 0;
1731
+ ctx.io.result({
1125
1732
  paired: true,
1126
1733
  machineId: paired.machineId,
1127
- ...approvedBy !== void 0 ? { approvedByEmail: approvedBy } : {}
1734
+ discardedPrePairingEvents: discarded,
1735
+ ...approvedBy !== void 0 ? { approvedByEmail: approvedBy } : {},
1736
+ ...report !== void 0 ? { setup: report } : {}
1128
1737
  });
1129
- return EXIT.OK;
1738
+ return report !== void 0 && !report.ok ? EXIT.ERROR : EXIT.OK;
1130
1739
  }
1131
1740
  };
1132
1741
  }
1742
+ function createPairCommand(deps = {}) {
1743
+ return createPairingCommand(
1744
+ {
1745
+ name: "pair",
1746
+ summary: "Pair this machine and wire up every coding agent on it",
1747
+ usage: "birdybeep pair [--yes] [--expect-email <addr>] [--no-install] [--no-test] [--json]",
1748
+ skipWhenPaired: false
1749
+ },
1750
+ deps
1751
+ );
1752
+ }
1753
+ function createSetupCommand(deps = {}) {
1754
+ return createPairingCommand(
1755
+ {
1756
+ name: "setup",
1757
+ summary: "Set up BirdyBeep here: pair, wire up every coding agent, test",
1758
+ usage: "birdybeep setup [--yes] [--expect-email <addr>] [--no-install] [--no-test] [--json]",
1759
+ gettingStarted: "Pair this machine, wire up every coding agent it finds, and send a test Beep.",
1760
+ skipWhenPaired: true
1761
+ },
1762
+ deps
1763
+ );
1764
+ }
1133
1765
 
1134
1766
  // src/commands/queue.ts
1135
- var import_agent_core9 = require("@birdybeep/agent-core");
1767
+ var import_agent_core10 = require("@birdybeep/agent-core");
1136
1768
  function createQueueCommand() {
1137
1769
  return {
1138
1770
  name: "queue",
@@ -1144,7 +1776,7 @@ function createQueueCommand() {
1144
1776
  summary: "Clear the local offline event queue (debug)",
1145
1777
  usage: "birdybeep queue clear",
1146
1778
  run: (ctx) => {
1147
- const cleared = new import_agent_core9.LocalEventQueue().clear();
1779
+ const cleared = new import_agent_core10.LocalEventQueue().clear();
1148
1780
  ctx.io.emit(`Cleared ${cleared} queued event(s).`, { cleared });
1149
1781
  return EXIT.OK;
1150
1782
  }
@@ -1154,25 +1786,25 @@ function createQueueCommand() {
1154
1786
  }
1155
1787
 
1156
1788
  // src/commands/report-status.ts
1157
- var import_agent_core10 = require("@birdybeep/agent-core");
1158
- var import_claude_code4 = require("@birdybeep/claude-code");
1159
- var import_codex4 = require("@birdybeep/codex");
1160
- var import_copilot4 = require("@birdybeep/copilot");
1161
- var import_cursor4 = require("@birdybeep/cursor");
1162
- var import_opencode4 = require("@birdybeep/opencode");
1789
+ var import_agent_core11 = require("@birdybeep/agent-core");
1790
+ var import_claude_code6 = require("@birdybeep/claude-code");
1791
+ var import_codex5 = require("@birdybeep/codex");
1792
+ var import_copilot5 = require("@birdybeep/copilot");
1793
+ var import_cursor6 = require("@birdybeep/cursor");
1794
+ var import_opencode5 = require("@birdybeep/opencode");
1163
1795
  var DEFAULT_ADAPTERS3 = [
1164
- import_claude_code4.claudeCodeAdapter,
1165
- import_codex4.codexAdapter,
1166
- import_opencode4.opencodeAdapter,
1167
- import_cursor4.cursorAdapter,
1168
- import_copilot4.copilotAdapter
1796
+ import_claude_code6.claudeCodeAdapter,
1797
+ import_codex5.codexAdapter,
1798
+ import_opencode5.opencodeAdapter,
1799
+ import_cursor6.cursorAdapter,
1800
+ import_copilot5.copilotAdapter
1169
1801
  ];
1170
1802
  var ADAPTER_VERSIONS = {
1171
- claude_code: import_claude_code4.CLAUDE_CODE_ADAPTER_VERSION,
1172
- codex: import_codex4.CODEX_ADAPTER_VERSION,
1173
- opencode: import_opencode4.OPENCODE_ADAPTER_VERSION,
1174
- cursor: import_cursor4.CURSOR_ADAPTER_VERSION,
1175
- copilot: import_copilot4.COPILOT_ADAPTER_VERSION
1803
+ claude_code: import_claude_code6.CLAUDE_CODE_ADAPTER_VERSION,
1804
+ codex: import_codex5.CODEX_ADAPTER_VERSION,
1805
+ opencode: import_opencode5.OPENCODE_ADAPTER_VERSION,
1806
+ cursor: import_cursor6.CURSOR_ADAPTER_VERSION,
1807
+ copilot: import_copilot5.COPILOT_ADAPTER_VERSION
1176
1808
  };
1177
1809
  var base3 = (apiUrl) => apiUrl.replace(/\/$/, "");
1178
1810
  async function gatherItems(adapters) {
@@ -1195,7 +1827,7 @@ function createReportStatusCommand(deps = {}) {
1195
1827
  summary: "Internal: report integration status to the backend",
1196
1828
  usage: "birdybeep report-status [--json]",
1197
1829
  run: async (ctx) => {
1198
- const token = await (0, import_agent_core10.getToken)(deps.tokenOptions ?? {});
1830
+ const token = await (0, import_agent_core11.getToken)(deps.tokenOptions ?? {});
1199
1831
  if (token === null) {
1200
1832
  ctx.io.errline("No machine token \u2014 run `birdybeep pair` first.");
1201
1833
  return EXIT.ERROR;
@@ -1216,7 +1848,7 @@ function createReportStatusCommand(deps = {}) {
1216
1848
  });
1217
1849
  if (res.ok) {
1218
1850
  outcome = "reported";
1219
- const parsed = import_agent_core10.integrationStatusResponseSchema.safeParse(
1851
+ const parsed = import_agent_core11.integrationStatusResponseSchema.safeParse(
1220
1852
  await res.json().catch(() => void 0)
1221
1853
  );
1222
1854
  if (parsed.success) {
@@ -1226,7 +1858,7 @@ function createReportStatusCommand(deps = {}) {
1226
1858
  }));
1227
1859
  }
1228
1860
  } else {
1229
- const env = import_agent_core10.errorEnvelopeSchema.safeParse(await res.json().catch(() => void 0));
1861
+ const env = import_agent_core11.errorEnvelopeSchema.safeParse(await res.json().catch(() => void 0));
1230
1862
  errorCode = env.success ? env.data.error.code : void 0;
1231
1863
  const terminal = errorCode !== void 0 ? errorCode === "unauthorized" || errorCode === "forbidden" || errorCode === "token_revoked" : res.status === 401 || res.status === 403;
1232
1864
  outcome = terminal ? "terminal" : "deferred";
@@ -1257,22 +1889,22 @@ function createReportStatusCommand(deps = {}) {
1257
1889
  }
1258
1890
 
1259
1891
  // src/commands/status.ts
1260
- var import_agent_core11 = require("@birdybeep/agent-core");
1261
- var import_claude_code5 = require("@birdybeep/claude-code");
1262
- var import_codex5 = require("@birdybeep/codex");
1263
- var import_copilot5 = require("@birdybeep/copilot");
1264
- var import_cursor5 = require("@birdybeep/cursor");
1265
- var import_opencode5 = require("@birdybeep/opencode");
1892
+ var import_agent_core12 = require("@birdybeep/agent-core");
1893
+ var import_claude_code7 = require("@birdybeep/claude-code");
1894
+ var import_codex6 = require("@birdybeep/codex");
1895
+ var import_copilot6 = require("@birdybeep/copilot");
1896
+ var import_cursor7 = require("@birdybeep/cursor");
1897
+ var import_opencode6 = require("@birdybeep/opencode");
1266
1898
  var DEFAULT_ADAPTERS4 = [
1267
- import_claude_code5.claudeCodeAdapter,
1268
- import_codex5.codexAdapter,
1269
- import_opencode5.opencodeAdapter,
1270
- import_cursor5.cursorAdapter,
1271
- import_copilot5.copilotAdapter
1899
+ import_claude_code7.claudeCodeAdapter,
1900
+ import_codex6.codexAdapter,
1901
+ import_opencode6.opencodeAdapter,
1902
+ import_cursor7.cursorAdapter,
1903
+ import_copilot6.copilotAdapter
1272
1904
  ];
1273
1905
  function createStatusCommand(deps = {}) {
1274
1906
  const adapters = deps.adapters ?? DEFAULT_ADAPTERS4;
1275
- const makeSender = deps.createSender ?? ((baseUrl) => (0, import_agent_core11.createSender)(
1907
+ const makeSender = deps.createSender ?? ((baseUrl) => (0, import_agent_core12.createSender)(
1276
1908
  deps.tokenOptions ? { baseUrl, tokenOptions: deps.tokenOptions } : { baseUrl }
1277
1909
  ));
1278
1910
  return {
@@ -1283,14 +1915,21 @@ function createStatusCommand(deps = {}) {
1283
1915
  const machine = machineIdentity();
1284
1916
  const paired = await isPaired(deps.tokenOptions ?? {});
1285
1917
  const integrations = await gatherIntegrations(adapters);
1918
+ const surfaces = await gatherSurfaces(adapters, deps.surfaceOptions ?? {});
1286
1919
  const depthBefore = localQueueDepth();
1920
+ const unpaired = unpairedActivity();
1921
+ const filtered = filteredActivity();
1287
1922
  const drain = await makeSender(resolveApiUrl()).drainNow();
1288
1923
  const depthAfter = localQueueDepth();
1924
+ const overflowDropped = localQueueOverflowDrops();
1289
1925
  const report = {
1290
1926
  machine,
1291
1927
  paired,
1292
1928
  integrations,
1293
- queue: { depthBefore, delivered: drain.delivered, depthAfter }
1929
+ surfaces,
1930
+ queue: { depthBefore, delivered: drain.delivered, depthAfter, overflowDropped },
1931
+ ...unpaired !== null ? { unpairedActivity: unpaired } : {},
1932
+ ...filtered !== null ? { filteredActivity: filtered } : {}
1294
1933
  };
1295
1934
  if (ctx.flags.json) {
1296
1935
  ctx.io.result(report);
@@ -1298,85 +1937,29 @@ function createStatusCommand(deps = {}) {
1298
1937
  ctx.io.line(`Machine: ${machine.label} (${machine.os})`);
1299
1938
  ctx.io.line(paired ? "Paired: yes" : "Paired: no \u2014 run `birdybeep pair`");
1300
1939
  ctx.io.line("Integrations:");
1301
- for (const i of integrations) ctx.io.line(` ${i.displayName}: ${i.status}`);
1940
+ for (const i of integrations) {
1941
+ ctx.io.line(` ${i.displayName}: ${i.status}`);
1942
+ const group = surfaces.find((g) => g.harness === i.harness);
1943
+ for (const state of group?.surfaces ?? []) {
1944
+ const mark = state.coverage === "active" ? "\u2713" : state.coverage === "wired" ? "\xB7" : "\u2717";
1945
+ ctx.io.line(` ${mark} ${describeSurface(state)} \u2014 ${state.coverage}`);
1946
+ }
1947
+ }
1302
1948
  ctx.io.line(
1303
- `Queue: ${depthBefore} queued \u2192 ${drain.delivered} delivered, ${depthAfter} remaining`
1949
+ `Queue: ${depthBefore} queued \u2192 ${drain.delivered} delivered, ${depthAfter} remaining` + (overflowDropped > 0 ? `, ${overflowDropped} dropped by the queue cap` : "")
1304
1950
  );
1951
+ if (unpaired !== null) ctx.io.line(`\u26A0 Lost: ${describeUnpairedActivity(unpaired)}`);
1952
+ if (filtered !== null) ctx.io.line(`Local: ${describeFilteredActivity(filtered)}`);
1305
1953
  }
1306
1954
  return paired ? EXIT.OK : EXIT.ERROR;
1307
1955
  }
1308
1956
  };
1309
1957
  }
1310
1958
 
1311
- // src/commands/test.ts
1312
- var import_node_crypto2 = require("crypto");
1313
- var import_agent_core12 = require("@birdybeep/agent-core");
1314
- function buildTestEvent(opts = {}) {
1315
- const machine = (0, import_agent_core12.getMachineIdentity)();
1316
- return (0, import_agent_core12.normalizeEvent)(
1317
- {
1318
- event_type: "test",
1319
- status: "running",
1320
- harness: "claude_code",
1321
- // schema requires a harness; the "test" type distinguishes it
1322
- // Unique per run: a repeat `birdybeep test` inside the backend's dedupe window must
1323
- // still beep — a constant id made the second test silently "deduped" (9fh).
1324
- source_session_id: `birdybeep-cli-test-${(0, import_node_crypto2.randomUUID)()}`,
1325
- machine: { label: machine.label, os: machine.os },
1326
- workspace: { cwd: process.cwd() },
1327
- title: "BirdyBeep test event",
1328
- body: "If you can see this, your machine is wired up correctly.",
1329
- metadata: { test: true }
1330
- },
1331
- opts
1332
- );
1333
- }
1334
- function createTestCommand(deps = {}) {
1335
- const makeSender = deps.createSender ?? ((baseUrl) => (0, import_agent_core12.createSender)(
1336
- deps.tokenOptions ? { baseUrl, tokenOptions: deps.tokenOptions } : { baseUrl }
1337
- ));
1338
- return {
1339
- name: "test",
1340
- summary: "Send a test event end-to-end",
1341
- usage: "birdybeep test [--json]",
1342
- run: async (ctx) => {
1343
- const event = buildTestEvent();
1344
- const result = await makeSender(resolveApiUrl()).send(event);
1345
- if (ctx.flags.json) {
1346
- ctx.io.result({
1347
- outcome: result.outcome,
1348
- ...result.status ? { status: result.status } : {},
1349
- ...result.decision ? { decision: result.decision } : {}
1350
- });
1351
- } else if (result.outcome === "delivered") {
1352
- if (result.decision === "notified" || result.decision === void 0) {
1353
- ctx.io.line("\u2713 Test event delivered \u2014 check your phone for a test Beep.");
1354
- } else if (result.decision === "suppressed") {
1355
- ctx.io.line(
1356
- "\u26A0 The backend accepted the test event but suppressed the push \u2014 this machine or integration is probably muted. Check mutes in the app, or run `birdybeep doctor`."
1357
- );
1358
- } else if (result.decision === "deduped") {
1359
- ctx.io.line(
1360
- "\u26A0 The backend accepted the test event but folded it into a recent duplicate \u2014 wait ~30s and run `birdybeep test` again."
1361
- );
1362
- } else {
1363
- ctx.io.line(
1364
- `\u26A0 The backend accepted the test event but decided "${result.decision}" \u2014 no push was sent. Run \`birdybeep doctor\`.`
1365
- );
1366
- }
1367
- } else if (result.outcome === "queued") {
1368
- ctx.io.line("\u2022 Offline \u2014 test event queued; it will deliver when you reconnect.");
1369
- } else {
1370
- ctx.io.line("\u2717 Test event was rejected by the backend. Run `birdybeep doctor`.");
1371
- }
1372
- return result.outcome === "dropped" ? EXIT.ERROR : EXIT.OK;
1373
- }
1374
- };
1375
- }
1376
-
1377
1959
  // src/commands.ts
1378
1960
  function buildCommands() {
1379
1961
  return [
1962
+ createSetupCommand(),
1380
1963
  createPairCommand(),
1381
1964
  createLogoutCommand(),
1382
1965
  createUnpairCommand(),
@@ -1391,7 +1974,7 @@ function buildCommands() {
1391
1974
  }
1392
1975
 
1393
1976
  // src/update-check.ts
1394
- var import_node_fs5 = require("fs");
1977
+ var import_node_fs6 = require("fs");
1395
1978
  var import_node_path3 = require("path");
1396
1979
  var import_agent_core13 = require("@birdybeep/agent-core");
1397
1980
  var PACKAGE_NAME = "@birdybeep/cli";
@@ -1451,7 +2034,7 @@ function updateCachePath() {
1451
2034
  }
1452
2035
  function readUpdateCache() {
1453
2036
  try {
1454
- const parsed = JSON.parse((0, import_node_fs5.readFileSync)(updateCachePath(), "utf8"));
2037
+ const parsed = JSON.parse((0, import_node_fs6.readFileSync)(updateCachePath(), "utf8"));
1455
2038
  if (typeof parsed !== "object" || parsed === null) return null;
1456
2039
  const { checkedAt, latest } = parsed;
1457
2040
  if (typeof checkedAt !== "number") return null;
@@ -1462,8 +2045,8 @@ function readUpdateCache() {
1462
2045
  }
1463
2046
  }
1464
2047
  function writeUpdateCache(cache) {
1465
- (0, import_node_fs5.mkdirSync)((0, import_agent_core13.birdyBeepConfigDir)(), { recursive: true, mode: 448 });
1466
- (0, import_node_fs5.writeFileSync)(updateCachePath(), `${JSON.stringify(cache)}
2048
+ (0, import_node_fs6.mkdirSync)((0, import_agent_core13.birdyBeepConfigDir)(), { recursive: true, mode: 448 });
2049
+ (0, import_node_fs6.writeFileSync)(updateCachePath(), `${JSON.stringify(cache)}
1467
2050
  `, { mode: 384 });
1468
2051
  }
1469
2052
  async function fetchLatestVersion(registryUrl, fetchImpl, timeoutMs) {