@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/bin.cjs CHANGED
@@ -24,15 +24,191 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  ));
25
25
 
26
26
  // src/commands/agent.ts
27
- var import_claude_code = require("@birdybeep/claude-code");
27
+ var import_claude_code2 = require("@birdybeep/claude-code");
28
28
  var import_codex = require("@birdybeep/codex");
29
29
  var import_copilot = require("@birdybeep/copilot");
30
- var import_cursor = require("@birdybeep/cursor");
30
+ var import_cursor2 = require("@birdybeep/cursor");
31
31
  var import_opencode = require("@birdybeep/opencode");
32
32
 
33
- // src/framework.ts
33
+ // src/diagnostics.ts
34
34
  var import_node_fs = require("fs");
35
+ var import_node_os = require("os");
35
36
  var import_agent_core = require("@birdybeep/agent-core");
37
+ var import_claude_code = require("@birdybeep/claude-code");
38
+ var import_cursor = require("@birdybeep/cursor");
39
+ async function gatherIntegrations(adapters) {
40
+ return Promise.all(
41
+ adapters.map(async (a) => ({
42
+ harness: a.id,
43
+ displayName: a.displayName,
44
+ status: await a.status()
45
+ }))
46
+ );
47
+ }
48
+ async function isPaired(tokenOptions = {}) {
49
+ return await (0, import_agent_core.getToken)(tokenOptions) !== null;
50
+ }
51
+ function localQueueDepth() {
52
+ return new import_agent_core.LocalEventQueue().size();
53
+ }
54
+ function localQueueOverflowDrops() {
55
+ return new import_agent_core.LocalEventQueue().overflowDropCount();
56
+ }
57
+ function unpairedActivity() {
58
+ return (0, import_agent_core.readUnpairedNotice)();
59
+ }
60
+ function describeUnpairedActivity(notice) {
61
+ const since = new Date(notice.firstAt).toISOString();
62
+ const from = notice.harnesses.length > 0 ? ` from ${notice.harnesses.join(", ")}` : "";
63
+ return `${notice.count} event(s)${from} fired since ${since} and were NOT sent \u2014 this machine is not paired.`;
64
+ }
65
+ function asRecord(value) {
66
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
67
+ }
68
+ function birdyBeepHookCount(path, events, isBirdyBeepEntry) {
69
+ if (!(0, import_node_fs.existsSync)(path)) return 0;
70
+ let parsed;
71
+ try {
72
+ parsed = JSON.parse((0, import_node_fs.readFileSync)(path, "utf8"));
73
+ } catch {
74
+ return null;
75
+ }
76
+ const hooks = asRecord(asRecord(parsed)["hooks"]);
77
+ let present = 0;
78
+ for (const event of events) {
79
+ const entries = hooks[event];
80
+ if (Array.isArray(entries) && entries.some(isBirdyBeepEntry)) present += 1;
81
+ }
82
+ return present;
83
+ }
84
+ async function cursorBridgeOnly(opts = {}) {
85
+ const home = opts.home ?? (0, import_node_os.homedir)();
86
+ const detection = await (opts.detect ?? (() => (0, import_cursor.detectCursor)({ home })))();
87
+ if (!detection.detected) return false;
88
+ const claude = birdyBeepHookCount((0, import_claude_code.claudeSettingsPath)(home), import_claude_code.BIRDYBEEP_HOOK_EVENTS, import_claude_code.isBirdyBeepEntry);
89
+ if (claude === null || claude === 0) return false;
90
+ return birdyBeepHookCount((0, import_cursor.cursorHooksPath)(home), import_cursor.BIRDYBEEP_HOOK_EVENTS, import_cursor.isBirdyBeepEntry) === 0;
91
+ }
92
+ function filteredActivity() {
93
+ return (0, import_agent_core.readFilteredActivity)();
94
+ }
95
+ function describeFilteredActivity(activity) {
96
+ const types = Object.entries(activity.byType).sort(([, a], [, b]) => b - a).map(([type, n]) => `${type} \xD7${n}`).join(", ");
97
+ const since = new Date(activity.firstAt).toISOString();
98
+ return `${activity.count} local-only event(s) since ${since}${types ? ` (${types})` : ""} \u2014 hooks are firing; these types never beep, so they are not sent.`;
99
+ }
100
+ function machineIdentity() {
101
+ return (0, import_agent_core.getMachineIdentity)();
102
+ }
103
+ var CONFIGURED_STATUSES = /* @__PURE__ */ new Set([
104
+ "installed",
105
+ "needs_trust",
106
+ "needs_restart"
107
+ ]);
108
+ function gradeSurfaces(surfaces, status, observation) {
109
+ const builds = Object.values(observation?.builds ?? {});
110
+ const configured = CONFIGURED_STATUSES.has(status);
111
+ const claimedByKind = /* @__PURE__ */ new Map();
112
+ for (const s of surfaces) {
113
+ if (s.version === void 0) continue;
114
+ const versions = claimedByKind.get(s.kind) ?? /* @__PURE__ */ new Set();
115
+ versions.add(s.version);
116
+ claimedByKind.set(s.kind, versions);
117
+ }
118
+ const graded = surfaces.map((surface) => {
119
+ const exact = builds.filter(
120
+ (b) => b.surface === surface.kind && b.version === surface.version && surface.version !== void 0
121
+ );
122
+ let soleOfKind = [];
123
+ if (surface.version === void 0) {
124
+ const sameKindVersionless = surfaces.filter(
125
+ (s) => s.version === void 0 && s.kind === surface.kind
126
+ );
127
+ const unclaimed = builds.filter(
128
+ (b) => b.surface === surface.kind && !(claimedByKind.get(surface.kind)?.has(b.version) ?? false)
129
+ );
130
+ if (unclaimed.length === 1 && sameKindVersionless.length === 1) soleOfKind = unclaimed;
131
+ }
132
+ const unattributed = surface.version === void 0 ? [] : builds.filter((b) => b.surface === "unknown" && b.version === surface.version);
133
+ const sharesVersion = surface.version !== void 0 && surfaces.some((s) => s !== surface && s.version === surface.version);
134
+ const ambiguous = unattributed.length > 0 && sharesVersion;
135
+ const matched = [...exact, ...soleOfKind, ...ambiguous ? [] : unattributed];
136
+ const events = matched.reduce((total, b) => total + b.count, 0);
137
+ const lastAt = matched.reduce(
138
+ (latest, b) => latest === void 0 || b.lastAt > latest ? b.lastAt : latest,
139
+ void 0
140
+ );
141
+ const observedVersion = surface.version === void 0 ? soleOfKind[0]?.version : void 0;
142
+ return {
143
+ surface,
144
+ events,
145
+ ambiguous,
146
+ ...lastAt !== void 0 ? { lastAt } : {},
147
+ ...observedVersion !== void 0 ? { observedVersion } : {}
148
+ };
149
+ });
150
+ const anyActive = graded.some((g) => g.events > 0 && g.surface.shadowed !== true);
151
+ return graded.map(({ ambiguous, ...g }) => ({
152
+ ...g,
153
+ coverage: !configured ? "uncovered" : g.events > 0 ? "active" : anyActive && g.surface.shadowed !== true && !ambiguous ? "uncovered" : "wired"
154
+ }));
155
+ }
156
+ async function gatherSurfaces(adapters, options = {}) {
157
+ const observed = (0, import_agent_core.readObservedBuilds)(options.observedBuilds ?? {});
158
+ return Promise.all(
159
+ adapters.map(async (adapter) => {
160
+ const observation = observed[adapter.id];
161
+ const base4 = {
162
+ harness: adapter.id,
163
+ displayName: adapter.displayName,
164
+ unversionedEvents: observation?.unversioned ?? 0
165
+ };
166
+ try {
167
+ const [detection, status] = await Promise.all([adapter.detect(), adapter.status()]);
168
+ return {
169
+ ...base4,
170
+ status,
171
+ surfaces: detection.detected ? gradeSurfaces(detection.surfaces ?? [], status, observation) : []
172
+ };
173
+ } catch {
174
+ return { ...base4, status: "unknown", surfaces: [] };
175
+ }
176
+ })
177
+ );
178
+ }
179
+ function describeSurface(state) {
180
+ const version = state.surface.version ?? state.observedVersion;
181
+ return version !== void 0 ? `${state.surface.label} ${version}` : state.surface.label;
182
+ }
183
+ function describeSurfaceCoverage(state, group) {
184
+ if (state.coverage === "active") {
185
+ const last = state.lastAt !== void 0 ? `, last ${new Date(state.lastAt).toISOString()}` : "";
186
+ return `covered \u2014 ${state.events} event(s) from this build${last}`;
187
+ }
188
+ if (state.coverage === "wired") {
189
+ 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`;
190
+ }
191
+ if (!CONFIGURED_STATUSES.has(group.status)) {
192
+ return `not covered \u2014 ${group.displayName} carries no BirdyBeep hooks, so this build cannot beep`;
193
+ }
194
+ const active = group.surfaces.filter((s) => s.coverage === "active").map(describeSurface);
195
+ const delivering = active.join(", ");
196
+ const verb = active.length === 1 ? "is" : "are";
197
+ return `not covered \u2014 nothing has ever fired from this build, while ${delivering} ${verb} delivering through the same config`;
198
+ }
199
+ function installTarget(harness) {
200
+ return harness === "claude_code" ? "claude" : harness;
201
+ }
202
+ function surfaceRemedy(state, group) {
203
+ if (state.coverage !== "uncovered") return void 0;
204
+ if (!CONFIGURED_STATUSES.has(group.status)) return void 0;
205
+ const install = `\`birdybeep agent install ${installTarget(group.harness)}\``;
206
+ 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.`;
207
+ }
208
+
209
+ // src/framework.ts
210
+ var import_node_fs2 = require("fs");
211
+ var import_agent_core2 = require("@birdybeep/agent-core");
36
212
  var EXIT = { OK: 0, ERROR: 1, USAGE: 2 };
37
213
  function createIo(json, stdout, stderr) {
38
214
  return {
@@ -105,11 +281,17 @@ function isUnknownFlag(token, allowed) {
105
281
  function renderRootHelp(version, commands) {
106
282
  const width = Math.max(...commands.map((c) => c.name.length));
107
283
  const lines = commands.map((c) => ` ${c.name.padEnd(width)} ${c.summary}`);
284
+ const featured = commands.filter((c) => c.gettingStarted !== void 0);
108
285
  return [
109
286
  `birdybeep ${version} \u2014 stream coding-agent lifecycle events to BirdyBeep.`,
110
287
  "",
111
288
  "Usage:",
112
289
  " birdybeep <command> [options]",
290
+ ...featured.length > 0 ? [
291
+ "",
292
+ "Getting started:",
293
+ ...featured.map((c) => ` birdybeep ${c.name} ${c.gettingStarted ?? ""}`)
294
+ ] : [],
113
295
  "",
114
296
  "Commands:",
115
297
  ...lines,
@@ -164,7 +346,7 @@ async function dispatch(argv, deps) {
164
346
  const io = createIo(flags.json, deps.stdout, deps.stderr);
165
347
  if (deps.ensureConfig !== false) {
166
348
  try {
167
- (0, import_node_fs.mkdirSync)((0, import_agent_core.birdyBeepConfigDir)(), { recursive: true, mode: 448 });
349
+ (0, import_node_fs2.mkdirSync)((0, import_agent_core2.birdyBeepConfigDir)(), { recursive: true, mode: 448 });
168
350
  } catch {
169
351
  }
170
352
  }
@@ -191,7 +373,11 @@ async function dispatch(argv, deps) {
191
373
  if (rest.length === 0 || flags.help && command === void 0) {
192
374
  io.emit(renderRootHelp(deps.version, deps.commands), {
193
375
  version: deps.version,
194
- commands: deps.commands.map((c) => ({ name: c.name, summary: c.summary }))
376
+ commands: deps.commands.map((c) => ({
377
+ name: c.name,
378
+ summary: c.summary,
379
+ ...c.gettingStarted !== void 0 ? { gettingStarted: c.gettingStarted } : {}
380
+ }))
195
381
  });
196
382
  return EXIT.OK;
197
383
  }
@@ -245,10 +431,10 @@ async function dispatch(argv, deps) {
245
431
 
246
432
  // src/commands/agent.ts
247
433
  var DEFAULT_ADAPTERS = [
248
- import_claude_code.claudeCodeAdapter,
434
+ import_claude_code2.claudeCodeAdapter,
249
435
  import_codex.codexAdapter,
250
436
  import_opencode.opencodeAdapter,
251
- import_cursor.cursorAdapter,
437
+ import_cursor2.cursorAdapter,
252
438
  import_copilot.copilotAdapter
253
439
  ];
254
440
  var TARGET_TO_ID = {
@@ -272,7 +458,10 @@ function selectAdapters(target, adapters) {
272
458
  if (id === void 0) return "unknown";
273
459
  return adapters.filter((a) => a.id === id);
274
460
  }
275
- async function installSelected(adapters, ctx) {
461
+ function installTarget2(harness) {
462
+ return harness === "claude_code" ? "claude" : harness;
463
+ }
464
+ async function installSelected(adapters, ctx, tokenOptions) {
276
465
  const target = ctx.args[0] ?? "all";
277
466
  const selected = selectAdapters(target, adapters);
278
467
  if (selected === "unknown") {
@@ -299,8 +488,9 @@ async function installSelected(adapters, ctx) {
299
488
  requiredActions: result.requiredActions
300
489
  });
301
490
  }
491
+ const paired = await isPaired(tokenOptions);
302
492
  if (ctx.flags.json) {
303
- ctx.io.result({ target, results: outcomes });
493
+ ctx.io.result({ target, paired, results: outcomes });
304
494
  return EXIT.OK;
305
495
  }
306
496
  if (outcomes.length === 0 || outcomes.every((o) => !o.detected)) {
@@ -308,13 +498,20 @@ async function installSelected(adapters, ctx) {
308
498
  }
309
499
  for (const o of outcomes) {
310
500
  if (!o.detected) {
311
- ctx.io.line(`\u2013 ${o.displayName}: not detected (skipped)`);
501
+ ctx.io.line(
502
+ `\u2013 ${o.displayName}: not detected (skipped) \u2014 install it, then run \`birdybeep agent install ${installTarget2(o.harness)}\``
503
+ );
312
504
  continue;
313
505
  }
314
506
  const changed = (o.changedFiles ?? []).length > 0 ? o.changedFiles.join(", ") : "no changes";
315
507
  ctx.io.line(`\u2713 ${o.displayName}: ${o.status} (${changed})`);
316
508
  for (const action of o.requiredActions ?? []) ctx.io.line(` \u2192 ${action}`);
317
509
  }
510
+ if (!paired) {
511
+ ctx.io.line(
512
+ "\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."
513
+ );
514
+ }
318
515
  return EXIT.OK;
319
516
  }
320
517
  async function uninstallSelected(adapters, ctx) {
@@ -353,6 +550,7 @@ async function uninstallSelected(adapters, ctx) {
353
550
  }
354
551
  function createAgentCommand(deps = {}) {
355
552
  const adapters = deps.adapters ?? DEFAULT_ADAPTERS;
553
+ const tokenOptions = deps.tokenOptions ?? {};
356
554
  return {
357
555
  name: "agent",
358
556
  summary: "Install or uninstall harness adapters",
@@ -362,7 +560,7 @@ function createAgentCommand(deps = {}) {
362
560
  name: "install",
363
561
  summary: "Install adapters (all | claude | codex | opencode | cursor | copilot)",
364
562
  usage: "birdybeep agent install [all|claude|codex|opencode|cursor|copilot]",
365
- run: (ctx) => installSelected(adapters, ctx)
563
+ run: (ctx) => installSelected(adapters, ctx, tokenOptions)
366
564
  },
367
565
  {
368
566
  name: "uninstall",
@@ -376,24 +574,24 @@ function createAgentCommand(deps = {}) {
376
574
 
377
575
  // src/commands/doctor.ts
378
576
  var import_agent_core4 = require("@birdybeep/agent-core");
379
- var import_claude_code2 = require("@birdybeep/claude-code");
577
+ var import_claude_code3 = require("@birdybeep/claude-code");
380
578
  var import_codex2 = require("@birdybeep/codex");
381
579
  var import_copilot2 = require("@birdybeep/copilot");
382
- var import_cursor2 = require("@birdybeep/cursor");
580
+ var import_cursor3 = require("@birdybeep/cursor");
383
581
  var import_opencode2 = require("@birdybeep/opencode");
384
582
 
385
583
  // src/config.ts
386
- var import_node_fs2 = require("fs");
584
+ var import_node_fs3 = require("fs");
387
585
  var import_node_path = require("path");
388
- var import_agent_core2 = require("@birdybeep/agent-core");
586
+ var import_agent_core3 = require("@birdybeep/agent-core");
389
587
  var DEFAULT_API_URL = "https://api.birdybeep.com";
390
588
  var CONFIG_FILE = "config.json";
391
589
  function cliConfigPath() {
392
- return (0, import_node_path.join)((0, import_agent_core2.birdyBeepConfigDir)(), CONFIG_FILE);
590
+ return (0, import_node_path.join)((0, import_agent_core3.birdyBeepConfigDir)(), CONFIG_FILE);
393
591
  }
394
592
  function readCliConfig() {
395
593
  try {
396
- const parsed = JSON.parse((0, import_node_fs2.readFileSync)(cliConfigPath(), "utf8"));
594
+ const parsed = JSON.parse((0, import_node_fs3.readFileSync)(cliConfigPath(), "utf8"));
397
595
  return typeof parsed === "object" && parsed !== null ? parsed : {};
398
596
  } catch {
399
597
  return {};
@@ -406,8 +604,8 @@ function writeCliConfig(patch) {
406
604
  if (apiUrl !== void 0) merged.apiUrl = apiUrl;
407
605
  const expectEmail = patch.expectEmail ?? current.expectEmail;
408
606
  if (expectEmail !== void 0) merged.expectEmail = expectEmail;
409
- (0, import_node_fs2.mkdirSync)((0, import_agent_core2.birdyBeepConfigDir)(), { recursive: true, mode: 448 });
410
- (0, import_node_fs2.writeFileSync)(cliConfigPath(), `${JSON.stringify(merged, null, 2)}
607
+ (0, import_node_fs3.mkdirSync)((0, import_agent_core3.birdyBeepConfigDir)(), { recursive: true, mode: 448 });
608
+ (0, import_node_fs3.writeFileSync)(cliConfigPath(), `${JSON.stringify(merged, null, 2)}
411
609
  `, { mode: 384 });
412
610
  }
413
611
  function resolveApiUrl() {
@@ -422,33 +620,12 @@ function resolveRegistryUrl() {
422
620
  return DEFAULT_REGISTRY_URL;
423
621
  }
424
622
 
425
- // src/diagnostics.ts
426
- var import_agent_core3 = require("@birdybeep/agent-core");
427
- async function gatherIntegrations(adapters) {
428
- return Promise.all(
429
- adapters.map(async (a) => ({
430
- harness: a.id,
431
- displayName: a.displayName,
432
- status: await a.status()
433
- }))
434
- );
435
- }
436
- async function isPaired(tokenOptions = {}) {
437
- return await (0, import_agent_core3.getToken)(tokenOptions) !== null;
438
- }
439
- function localQueueDepth() {
440
- return new import_agent_core3.LocalEventQueue().size();
441
- }
442
- function machineIdentity() {
443
- return (0, import_agent_core3.getMachineIdentity)();
444
- }
445
-
446
623
  // src/commands/doctor.ts
447
624
  var DEFAULT_ADAPTERS2 = [
448
- import_claude_code2.claudeCodeAdapter,
625
+ import_claude_code3.claudeCodeAdapter,
449
626
  import_codex2.codexAdapter,
450
627
  import_opencode2.opencodeAdapter,
451
- import_cursor2.cursorAdapter,
628
+ import_cursor3.cursorAdapter,
452
629
  import_copilot2.copilotAdapter
453
630
  ];
454
631
  async function defaultProbeNetwork(baseUrl) {
@@ -485,6 +662,32 @@ function createDoctorCommand(deps = {}) {
485
662
  remedy: "Run `birdybeep pair` to pair this machine."
486
663
  }
487
664
  );
665
+ const unpaired = unpairedActivity();
666
+ if (unpaired !== null) {
667
+ checks.push({
668
+ name: "Events lost while unpaired",
669
+ ok: false,
670
+ detail: describeUnpairedActivity(unpaired),
671
+ remedy: "Run `birdybeep pair`. Events that fired before pairing are gone \u2014 a first pairing does not replay them."
672
+ });
673
+ }
674
+ if (await cursorBridgeOnly(deps.detectCursor ? { detect: deps.detectCursor } : {})) {
675
+ checks.push({
676
+ name: "Approval beeps from Cursor",
677
+ ok: false,
678
+ 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.",
679
+ 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."
680
+ });
681
+ }
682
+ const filtered = filteredActivity();
683
+ if (filtered !== null) {
684
+ checks.push({
685
+ name: "Local-only events (never notifiable)",
686
+ ok: true,
687
+ detail: describeFilteredActivity(filtered)
688
+ });
689
+ }
690
+ const surfaceGroups = await gatherSurfaces(adapters, deps.surfaceOptions ?? {});
488
691
  for (const adapter of adapters) {
489
692
  const result = await adapter.doctor();
490
693
  for (const c of result.checks) {
@@ -495,14 +698,26 @@ function createDoctorCommand(deps = {}) {
495
698
  ...c.remedy !== void 0 ? { remedy: c.remedy } : {}
496
699
  });
497
700
  }
701
+ const group = surfaceGroups.find((g) => g.harness === adapter.id);
702
+ if (group === void 0) continue;
703
+ for (const state of group.surfaces) {
704
+ const remedy = surfaceRemedy(state, group);
705
+ checks.push({
706
+ name: `${adapter.displayName}: ${describeSurface(state)}`,
707
+ ok: state.coverage !== "uncovered",
708
+ detail: describeSurfaceCoverage(state, group),
709
+ ...remedy !== void 0 ? { remedy } : {}
710
+ });
711
+ }
498
712
  }
499
713
  const depthBefore = localQueueDepth();
500
714
  const drain = await makeSender(apiUrl).drainNow();
501
715
  const depthAfter = localQueueDepth();
716
+ const overflowDropped = localQueueOverflowDrops();
502
717
  checks.push({
503
718
  name: "Local queue",
504
719
  ok: true,
505
- detail: `${depthBefore} queued \u2192 ${drain.delivered} delivered, ${depthAfter} remaining`
720
+ detail: `${depthBefore} queued \u2192 ${drain.delivered} delivered, ${depthAfter} remaining` + (overflowDropped > 0 ? `; ${overflowDropped} dropped by the ${import_agent_core4.DEFAULT_QUEUE_MAX_ENTRIES} entry cap` : "")
506
721
  });
507
722
  const reachable = await probeNetwork(apiUrl);
508
723
  checks.push(
@@ -518,7 +733,10 @@ function createDoctorCommand(deps = {}) {
518
733
  ctx.io.result({
519
734
  ok,
520
735
  checks,
521
- queue: { depthBefore, delivered: drain.delivered, depthAfter }
736
+ surfaces: surfaceGroups,
737
+ queue: { depthBefore, delivered: drain.delivered, depthAfter, overflowDropped },
738
+ ...unpaired !== null ? { unpairedActivity: unpaired } : {},
739
+ ...filtered !== null ? { filteredActivity: filtered } : {}
522
740
  });
523
741
  } else {
524
742
  for (const c of checks) {
@@ -535,20 +753,20 @@ function createDoctorCommand(deps = {}) {
535
753
  // src/commands/hook.ts
536
754
  var import_node_child_process = require("child_process");
537
755
  var import_node_crypto = require("crypto");
538
- var import_node_fs3 = require("fs");
539
- var import_node_os = require("os");
756
+ var import_node_fs4 = require("fs");
757
+ var import_node_os2 = require("os");
540
758
  var import_node_path2 = require("path");
541
759
  var import_agent_core5 = require("@birdybeep/agent-core");
542
- var import_claude_code3 = require("@birdybeep/claude-code");
760
+ var import_claude_code4 = require("@birdybeep/claude-code");
543
761
  var import_codex3 = require("@birdybeep/codex");
544
762
  var import_copilot3 = require("@birdybeep/copilot");
545
- var import_cursor3 = require("@birdybeep/cursor");
763
+ var import_cursor4 = require("@birdybeep/cursor");
546
764
  var import_opencode3 = require("@birdybeep/opencode");
547
765
  var RUNNERS = {
548
- claude: import_claude_code3.runClaudeHook,
766
+ claude: import_claude_code4.runClaudeHook,
549
767
  codex: import_codex3.runCodexHook,
550
768
  opencode: import_opencode3.runOpenCodeHook,
551
- cursor: import_cursor3.runCursorHook
769
+ cursor: import_cursor4.runCursorHook
552
770
  };
553
771
  var HOOK_HARNESSES = [
554
772
  "claude",
@@ -575,12 +793,43 @@ function withTimeout(promise, ms, fallback) {
575
793
  function isHarnessName(value) {
576
794
  return value === "claude" || value === "codex" || value === "opencode" || value === "cursor" || value === "copilot";
577
795
  }
796
+ function resolveHookHarness(harness, payload) {
797
+ return harness === "claude" && (0, import_cursor4.isCursorHookPayload)(payload) ? "cursor" : harness;
798
+ }
799
+ function recognizesPayload(harness, payload) {
800
+ switch (harness) {
801
+ case "claude":
802
+ return (0, import_claude_code4.isClaudeCodeHookPayload)(payload);
803
+ case "cursor":
804
+ return (0, import_cursor4.isCursorHookEventName)(asRecord2(payload)["hook_event_name"]);
805
+ case "codex":
806
+ return (0, import_codex3.isCodexHookPayload)(payload);
807
+ case "opencode":
808
+ return (0, import_opencode3.isOpenCodeEventPayload)(payload);
809
+ case "copilot":
810
+ return (0, import_copilot3.isCopilotHookPayload)(payload);
811
+ }
812
+ }
813
+ function asRecord2(value) {
814
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
815
+ }
816
+ function describeDiscriminator(payload) {
817
+ const record = asRecord2(payload);
818
+ for (const field of ["hook_event_name", "type"]) {
819
+ const value = record[field];
820
+ if (typeof value !== "string") continue;
821
+ const capped = value.length > 64 ? `${value.slice(0, 63)}\u2026` : value;
822
+ return `${field} ${JSON.stringify(capped)}`;
823
+ }
824
+ return "the payload";
825
+ }
578
826
  function runHookCommand(harness, payload, sender, copilotEventName) {
579
- if (harness === "copilot") {
827
+ const handler = resolveHookHarness(harness, payload);
828
+ if (handler === "copilot") {
580
829
  if (copilotEventName === void 0) return Promise.resolve({ outcome: "skipped" });
581
830
  return (0, import_copilot3.runCopilotHook)(copilotEventName, payload, { sender });
582
831
  }
583
- return RUNNERS[harness](payload, { sender });
832
+ return RUNNERS[handler](payload, { sender });
584
833
  }
585
834
  function readStdinDefault() {
586
835
  return new Promise((resolve) => {
@@ -606,10 +855,10 @@ function detachCodexNotifyWorker(payload) {
606
855
  try {
607
856
  const birdybeep = (0, import_agent_core5.resolveOnPath)("birdybeep");
608
857
  if (birdybeep === null) return false;
609
- const tmpFile = (0, import_node_path2.join)((0, import_node_os.tmpdir)(), `birdybeep-notify-${(0, import_node_crypto.randomBytes)(16).toString("hex")}.json`);
858
+ const tmpFile = (0, import_node_path2.join)((0, import_node_os2.tmpdir)(), `birdybeep-notify-${(0, import_node_crypto.randomBytes)(16).toString("hex")}.json`);
610
859
  file = tmpFile;
611
- (0, import_node_fs3.writeFileSync)(tmpFile, payload, { mode: 384 });
612
- fd = (0, import_node_fs3.openSync)(tmpFile, "r");
860
+ (0, import_node_fs4.writeFileSync)(tmpFile, payload, { mode: 384 });
861
+ fd = (0, import_node_fs4.openSync)(tmpFile, "r");
613
862
  const child = (0, import_node_child_process.spawn)(birdybeep, ["hook", "codex"], {
614
863
  cwd: (0, import_node_path2.dirname)(birdybeep),
615
864
  // trusted dir, never the inherited/attacker cwd
@@ -623,7 +872,7 @@ function detachCodexNotifyWorker(payload) {
623
872
  });
624
873
  child.on("error", () => {
625
874
  try {
626
- (0, import_node_fs3.rmSync)(tmpFile, { force: true });
875
+ (0, import_node_fs4.rmSync)(tmpFile, { force: true });
627
876
  } catch {
628
877
  }
629
878
  });
@@ -632,7 +881,7 @@ function detachCodexNotifyWorker(payload) {
632
881
  } catch {
633
882
  if (file !== void 0) {
634
883
  try {
635
- (0, import_node_fs3.rmSync)(file, { force: true });
884
+ (0, import_node_fs4.rmSync)(file, { force: true });
636
885
  } catch {
637
886
  }
638
887
  }
@@ -640,7 +889,7 @@ function detachCodexNotifyWorker(payload) {
640
889
  } finally {
641
890
  if (fd !== void 0) {
642
891
  try {
643
- (0, import_node_fs3.closeSync)(fd);
892
+ (0, import_node_fs4.closeSync)(fd);
644
893
  } catch {
645
894
  }
646
895
  }
@@ -667,35 +916,76 @@ function createHookCommand(deps = {}) {
667
916
  return EXIT.OK;
668
917
  }
669
918
  const copilotEventName = harness === "copilot" && (0, import_copilot3.isCopilotHookEventName)(ctx.args[1]) ? ctx.args[1] : void 0;
670
- const raw = await withTimeout(
919
+ if (harness === "copilot" && copilotEventName === void 0) {
920
+ ctx.io.errline(
921
+ `birdybeep hook copilot: second argument must be a Copilot hook event name, got ${JSON.stringify(ctx.args[1] ?? "(none)")} \u2014 nothing was sent.`
922
+ );
923
+ return EXIT.USAGE;
924
+ }
925
+ const read = await withTimeout(
671
926
  readHookPayload(ctx.args, readStdin, harness === "copilot"),
672
927
  stdinTimeoutMs,
673
- ""
928
+ null
674
929
  );
675
930
  const notifyStdinFile = process.env[NOTIFY_STDIN_FILE_ENV];
676
- if (notifyStdinFile !== void 0 && (0, import_node_path2.dirname)(notifyStdinFile) === (0, import_node_os.tmpdir)() && (0, import_node_path2.basename)(notifyStdinFile).startsWith("birdybeep-notify-")) {
931
+ if (notifyStdinFile !== void 0 && (0, import_node_path2.dirname)(notifyStdinFile) === (0, import_node_os2.tmpdir)() && (0, import_node_path2.basename)(notifyStdinFile).startsWith("birdybeep-notify-")) {
677
932
  try {
678
- (0, import_node_fs3.rmSync)(notifyStdinFile, { force: true });
933
+ (0, import_node_fs4.rmSync)(notifyStdinFile, { force: true });
679
934
  } catch {
680
935
  }
681
936
  }
937
+ const drop = (reason, detail) => {
938
+ ctx.io.result({ harness, outcome: "skipped", reason });
939
+ ctx.io.errline(`birdybeep hook ${harness}: ${detail} \u2014 nothing was sent.`);
940
+ return EXIT.ERROR;
941
+ };
942
+ if (read === null) {
943
+ return drop(
944
+ "stdin-timeout",
945
+ `timed out after ${stdinTimeoutMs}ms waiting for the payload on stdin`
946
+ );
947
+ }
948
+ const raw = read;
949
+ if (raw.trim().length === 0) {
950
+ return drop("empty-payload", "the payload was empty");
951
+ }
682
952
  let payload;
683
953
  try {
684
954
  payload = JSON.parse(raw);
685
955
  } catch {
686
- ctx.io.result({ harness, outcome: "skipped" });
687
- return EXIT.OK;
956
+ return drop("invalid-json", `the ${raw.length}-byte payload is not valid JSON`);
957
+ }
958
+ const handler = resolveHookHarness(harness, payload);
959
+ const routedFrom = handler !== harness ? { routedFrom: harness } : {};
960
+ if (!recognizesPayload(handler, payload)) {
961
+ ctx.io.result({
962
+ harness: handler,
963
+ ...routedFrom,
964
+ outcome: "skipped",
965
+ reason: "foreign-payload"
966
+ });
967
+ const article = handler === "opencode" ? "an" : "a";
968
+ ctx.io.errline(
969
+ `birdybeep hook ${harness}: ${describeDiscriminator(payload)} is not ${article} ${handler} hook event \u2014 nothing was sent. Check which tool is running this hook.`
970
+ );
971
+ return EXIT.ERROR;
688
972
  }
689
973
  const sender = makeSender(resolveApiUrl());
690
974
  const result = await runHookCommand(harness, payload, sender, copilotEventName);
691
975
  ctx.io.result({
692
- harness,
976
+ harness: handler,
977
+ ...routedFrom,
693
978
  ...copilotEventName !== void 0 ? { event: copilotEventName } : {},
694
979
  outcome: result.outcome,
695
980
  eventType: result.eventType,
696
981
  ...result.send?.decision ? { decision: result.send.decision } : {},
697
982
  ...result.send?.status !== void 0 ? { status: result.send.status } : {}
698
983
  });
984
+ if (result.outcome === "unpaired") {
985
+ ctx.io.errline(
986
+ "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)."
987
+ );
988
+ }
699
989
  return EXIT.OK;
700
990
  }
701
991
  };
@@ -753,8 +1043,8 @@ function createUnpairCommand(deps = {}) {
753
1043
  }
754
1044
 
755
1045
  // src/commands/pair.ts
756
- var import_node_fs4 = require("fs");
757
- var import_agent_core8 = require("@birdybeep/agent-core");
1046
+ var import_node_fs5 = require("fs");
1047
+ var import_agent_core9 = require("@birdybeep/agent-core");
758
1048
  var import_uqr = require("uqr");
759
1049
 
760
1050
  // src/pairing.ts
@@ -828,7 +1118,277 @@ async function pairTokenPoll(apiUrl, deviceCode, fetchImpl, machineFingerprint,
828
1118
  }
829
1119
 
830
1120
  // src/version.ts
831
- var CLI_VERSION = "0.3.0".length > 0 ? "0.3.0" : "0.0.0";
1121
+ var CLI_VERSION = "0.6.0".length > 0 ? "0.6.0" : "0.0.0";
1122
+
1123
+ // src/commands/setup.ts
1124
+ var import_claude_code5 = require("@birdybeep/claude-code");
1125
+ var import_codex4 = require("@birdybeep/codex");
1126
+ var import_copilot4 = require("@birdybeep/copilot");
1127
+ var import_cursor5 = require("@birdybeep/cursor");
1128
+ var import_opencode4 = require("@birdybeep/opencode");
1129
+
1130
+ // src/commands/test.ts
1131
+ var import_node_crypto2 = require("crypto");
1132
+ var import_agent_core8 = require("@birdybeep/agent-core");
1133
+ function buildTestEvent(opts = {}) {
1134
+ const machine = (0, import_agent_core8.getMachineIdentity)();
1135
+ return (0, import_agent_core8.normalizeEvent)(
1136
+ {
1137
+ event_type: "test",
1138
+ status: "running",
1139
+ harness: "claude_code",
1140
+ // schema requires a harness; the "test" type distinguishes it
1141
+ // Unique per run: a repeat `birdybeep test` inside the backend's dedupe window must
1142
+ // still beep — a constant id made the second test silently "deduped" (9fh).
1143
+ source_session_id: `birdybeep-cli-test-${(0, import_node_crypto2.randomUUID)()}`,
1144
+ machine: { label: machine.label, os: machine.os },
1145
+ workspace: { cwd: process.cwd() },
1146
+ title: "BirdyBeep test event",
1147
+ body: "If you can see this, your machine is wired up correctly.",
1148
+ metadata: { test: true }
1149
+ },
1150
+ opts
1151
+ );
1152
+ }
1153
+ function createTestCommand(deps = {}) {
1154
+ const makeSender = deps.createSender ?? ((baseUrl) => (0, import_agent_core8.createSender)(
1155
+ deps.tokenOptions ? { baseUrl, tokenOptions: deps.tokenOptions } : { baseUrl }
1156
+ ));
1157
+ return {
1158
+ name: "test",
1159
+ summary: "Send a test event end-to-end",
1160
+ usage: "birdybeep test [--json]",
1161
+ run: async (ctx) => {
1162
+ const event = buildTestEvent();
1163
+ const result = await makeSender(resolveApiUrl()).send(event);
1164
+ if (ctx.flags.json) {
1165
+ ctx.io.result({
1166
+ outcome: result.outcome,
1167
+ ...result.status ? { status: result.status } : {},
1168
+ ...result.decision ? { decision: result.decision } : {}
1169
+ });
1170
+ } else if (result.outcome === "delivered") {
1171
+ if (result.decision === "notified" || result.decision === void 0) {
1172
+ ctx.io.line("\u2713 Test event delivered \u2014 check your phone for a test Beep.");
1173
+ } else if (result.decision === "suppressed") {
1174
+ ctx.io.line(
1175
+ "\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`."
1176
+ );
1177
+ } else if (result.decision === "deduped") {
1178
+ ctx.io.line(
1179
+ "\u26A0 The backend accepted the test event but folded it into a recent duplicate \u2014 wait ~30s and run `birdybeep test` again."
1180
+ );
1181
+ } else {
1182
+ ctx.io.line(
1183
+ `\u26A0 The backend accepted the test event but decided "${result.decision}" \u2014 no push was sent. Run \`birdybeep doctor\`.`
1184
+ );
1185
+ }
1186
+ } else if (result.outcome === "unpaired") {
1187
+ ctx.io.line(
1188
+ "\u2717 NOT PAIRED \u2014 this machine has no BirdyBeep machine token, so nothing was sent (and nothing was queued). Run `birdybeep pair`."
1189
+ );
1190
+ } else if (result.outcome === "queued") {
1191
+ ctx.io.line("\u2022 Offline \u2014 test event queued; it will deliver when you reconnect.");
1192
+ } else {
1193
+ ctx.io.line("\u2717 Test event was rejected by the backend. Run `birdybeep doctor`.");
1194
+ }
1195
+ return result.outcome === "dropped" || result.outcome === "unpaired" ? EXIT.ERROR : EXIT.OK;
1196
+ }
1197
+ };
1198
+ }
1199
+
1200
+ // src/commands/setup.ts
1201
+ var SETUP_ADAPTERS = [
1202
+ import_claude_code5.claudeCodeAdapter,
1203
+ import_codex4.codexAdapter,
1204
+ import_opencode4.opencodeAdapter,
1205
+ import_cursor5.cursorAdapter,
1206
+ import_copilot4.copilotAdapter
1207
+ ];
1208
+ function failedSetupReport(error) {
1209
+ return {
1210
+ harnesses: [],
1211
+ counts: { installed: 0, needsYou: 0, notInstalled: 0, failed: 0 },
1212
+ error,
1213
+ ok: false
1214
+ };
1215
+ }
1216
+ var PENDING_STATUSES = /* @__PURE__ */ new Set([
1217
+ "needs_trust",
1218
+ "needs_restart"
1219
+ ]);
1220
+ async function installDetected(adapters) {
1221
+ const installs = [];
1222
+ for (const adapter of adapters) {
1223
+ try {
1224
+ const detection = await adapter.detect();
1225
+ if (!detection.detected) {
1226
+ installs.push({ adapter, detected: false });
1227
+ continue;
1228
+ }
1229
+ installs.push({ adapter, detected: true, result: await adapter.install() });
1230
+ } catch (err) {
1231
+ installs.push({
1232
+ adapter,
1233
+ detected: true,
1234
+ error: err instanceof Error ? err.message : String(err)
1235
+ });
1236
+ }
1237
+ }
1238
+ return installs;
1239
+ }
1240
+ function rowState(state, group, status) {
1241
+ if (status === "error" || group.status === "error") return "failed";
1242
+ if (status !== void 0 && PENDING_STATUSES.has(status)) return "needs you";
1243
+ if (state.coverage === "active") return "beeping";
1244
+ if (state.coverage === "wired") return "ready";
1245
+ return "not covered";
1246
+ }
1247
+ function buildHarnessReports(installs, groups) {
1248
+ return installs.map((install) => {
1249
+ const { adapter } = install;
1250
+ const base4 = {
1251
+ harness: adapter.id,
1252
+ displayName: adapter.displayName,
1253
+ detected: install.detected,
1254
+ ...install.result !== void 0 ? {
1255
+ status: install.result.status,
1256
+ changedFiles: install.result.changedFiles,
1257
+ backupFiles: install.result.backupFiles
1258
+ } : {},
1259
+ ...install.error !== void 0 ? { error: install.error } : {}
1260
+ };
1261
+ if (install.error !== void 0) {
1262
+ return {
1263
+ ...base4,
1264
+ actions: [
1265
+ `${adapter.displayName} could not be set up: ${install.error}`,
1266
+ `Run \`birdybeep agent install ${installTarget2(adapter.id)}\` to retry it on its own.`
1267
+ ],
1268
+ rows: [{ harness: adapter.id, displayName: adapter.displayName, state: "failed" }]
1269
+ };
1270
+ }
1271
+ if (!install.detected) {
1272
+ return {
1273
+ ...base4,
1274
+ actions: [],
1275
+ rows: [
1276
+ {
1277
+ harness: adapter.id,
1278
+ displayName: adapter.displayName,
1279
+ state: "not installed"
1280
+ }
1281
+ ]
1282
+ };
1283
+ }
1284
+ const group = groups.find((g) => g.harness === adapter.id);
1285
+ const status = install.result?.status;
1286
+ const surfaces = group?.surfaces ?? [];
1287
+ const rows = group === void 0 || surfaces.length === 0 ? [
1288
+ {
1289
+ harness: adapter.id,
1290
+ displayName: adapter.displayName,
1291
+ state: status !== void 0 && PENDING_STATUSES.has(status) ? "needs you" : "ready"
1292
+ }
1293
+ ] : surfaces.map((state) => {
1294
+ const graded = rowState(state, group, status);
1295
+ 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);
1296
+ return {
1297
+ harness: adapter.id,
1298
+ displayName: adapter.displayName,
1299
+ build: describeSurface(state),
1300
+ kind: state.surface.kind,
1301
+ state: graded,
1302
+ ...remedy !== void 0 ? { remedy } : {}
1303
+ };
1304
+ });
1305
+ return { ...base4, actions: [...install.result?.requiredActions ?? []], rows };
1306
+ });
1307
+ }
1308
+ function pad(text, width) {
1309
+ return text.length >= width ? text : text + " ".repeat(width - text.length);
1310
+ }
1311
+ var MARKS = {
1312
+ beeping: "\u2713",
1313
+ ready: "\u2713",
1314
+ "needs you": "!",
1315
+ "not covered": "\u2717",
1316
+ "not installed": "\u2013",
1317
+ failed: "\u2717"
1318
+ };
1319
+ function renderCoverageTable(reports) {
1320
+ const rows = reports.flatMap((r) => r.rows);
1321
+ const nameWidth = Math.max(7, ...rows.map((r) => r.displayName.length));
1322
+ const buildWidth = Math.max(5, ...rows.map((r) => (r.build ?? "\u2014").length));
1323
+ const lines = ["coverage", ` ${pad("harness", nameWidth)} ${pad("build", buildWidth)} state`];
1324
+ for (const report of reports) {
1325
+ for (const row of report.rows) {
1326
+ lines.push(
1327
+ `${MARKS[row.state]} ${pad(row.displayName, nameWidth)} ${pad(row.build ?? "\u2014", buildWidth)} ${row.state}`
1328
+ );
1329
+ if (row.remedy !== void 0) lines.push(` \u2192 ${row.remedy}`);
1330
+ }
1331
+ for (const action of report.actions) lines.push(` \u2192 ${action}`);
1332
+ }
1333
+ return lines;
1334
+ }
1335
+ function describeMissing(reports) {
1336
+ const missing = reports.filter((r) => !r.detected && r.error === void 0);
1337
+ if (missing.length === 0) return [];
1338
+ const names = missing.map((r) => r.displayName);
1339
+ if (missing.length === reports.length) {
1340
+ return [
1341
+ "No supported coding agent is installed on this machine, so there was nothing to wire up.",
1342
+ `Install one of ${names.join(", ")}, then run \`birdybeep setup\` again \u2014 pairing is already done, so it picks up from here.`
1343
+ ];
1344
+ }
1345
+ return [
1346
+ `Not installed: ${names.join(", ")}. Install any of them, then run \`birdybeep setup\` again to wire it up.`
1347
+ ];
1348
+ }
1349
+ async function runHarnessSetup(ctx, options, deps = {}) {
1350
+ const adapters = deps.adapters ?? [...SETUP_ADAPTERS];
1351
+ const installs = await installDetected(adapters);
1352
+ const groups = await gatherSurfaces(adapters, deps.surfaceOptions ?? {});
1353
+ const reports = buildHarnessReports(installs, groups);
1354
+ ctx.io.line("");
1355
+ for (const line of renderCoverageTable(reports)) ctx.io.line(line);
1356
+ const missing = describeMissing(reports);
1357
+ if (missing.length > 0) {
1358
+ ctx.io.line("");
1359
+ for (const line of missing) ctx.io.line(line);
1360
+ }
1361
+ const counts = {
1362
+ installed: reports.filter((r) => r.detected && r.error === void 0).length,
1363
+ needsYou: reports.filter((r) => r.rows.some((row) => row.state === "needs you")).length,
1364
+ notInstalled: reports.filter((r) => !r.detected && r.error === void 0).length,
1365
+ // A row that graded `failed` counts too: an adapter that returned status "error" never threw,
1366
+ // so counting only thrown errors would report a clean run over a harness that is broken.
1367
+ failed: reports.filter((r) => r.error !== void 0 || r.rows.some((x) => x.state === "failed")).length
1368
+ };
1369
+ let beep;
1370
+ let beepOk = true;
1371
+ if (options.sendTest) {
1372
+ ctx.io.line("");
1373
+ const command = createTestCommand({
1374
+ ...deps.createSender !== void 0 ? { createSender: deps.createSender } : {},
1375
+ ...deps.tokenOptions !== void 0 ? { tokenOptions: deps.tokenOptions } : {}
1376
+ });
1377
+ const beepIo = {
1378
+ ...ctx.io,
1379
+ result: (value) => {
1380
+ beep = value;
1381
+ }
1382
+ };
1383
+ beepOk = await command.run?.({ args: [], flags: ctx.flags, io: beepIo }) === 0;
1384
+ }
1385
+ return {
1386
+ harnesses: reports,
1387
+ counts,
1388
+ ...beep !== void 0 ? { beep } : {},
1389
+ ok: counts.failed === 0 && beepOk
1390
+ };
1391
+ }
832
1392
 
833
1393
  // src/commands/pair.ts
834
1394
  var DEFAULT_POLL_INTERVAL_MS = 2e3;
@@ -837,11 +1397,15 @@ function renderQrMatrix(qrPayload) {
837
1397
  return (0, import_uqr.renderUnicodeCompact)(qrPayload, { border: 2 });
838
1398
  }
839
1399
  function parsePairFlags(args) {
840
- const flags = { yes: false };
1400
+ const flags = { yes: false, noInstall: false, noTest: false };
841
1401
  for (let i = 0; i < args.length; i += 1) {
842
1402
  const token = args[i] ?? "";
843
1403
  if (token === "--yes" || token === "-y") {
844
1404
  flags.yes = true;
1405
+ } else if (token === "--no-install") {
1406
+ flags.noInstall = true;
1407
+ } else if (token === "--no-test") {
1408
+ flags.noTest = true;
845
1409
  } else if (token === "--expect-email" || token.startsWith("--expect-email=")) {
846
1410
  const inline = token.startsWith("--expect-email=") ? token.slice("--expect-email=".length) : void 0;
847
1411
  const value = inline ?? args[++i];
@@ -908,14 +1472,14 @@ function canOpenControllingTerminal(path = controllingTerminalPath(), platform =
908
1472
  if (platform === "win32") return false;
909
1473
  let fd;
910
1474
  try {
911
- fd = (0, import_node_fs4.openSync)(path, "r");
1475
+ fd = (0, import_node_fs5.openSync)(path, "r");
912
1476
  return true;
913
1477
  } catch {
914
1478
  return false;
915
1479
  } finally {
916
1480
  if (fd !== void 0) {
917
1481
  try {
918
- (0, import_node_fs4.closeSync)(fd);
1482
+ (0, import_node_fs5.closeSync)(fd);
919
1483
  } catch {
920
1484
  }
921
1485
  }
@@ -929,7 +1493,7 @@ async function promptForAnswer(question, on) {
929
1493
  input = process.stdin;
930
1494
  } else {
931
1495
  const { ReadStream } = await import("tty");
932
- ttyFd = (0, import_node_fs4.openSync)(controllingTerminalPath(), "r");
1496
+ ttyFd = (0, import_node_fs5.openSync)(controllingTerminalPath(), "r");
933
1497
  input = new ReadStream(ttyFd);
934
1498
  }
935
1499
  return new Promise((resolve) => {
@@ -949,7 +1513,7 @@ async function promptForAnswer(question, on) {
949
1513
  }
950
1514
  if (ttyFd !== void 0) {
951
1515
  try {
952
- (0, import_node_fs4.closeSync)(ttyFd);
1516
+ (0, import_node_fs5.closeSync)(ttyFd);
953
1517
  } catch {
954
1518
  }
955
1519
  }
@@ -961,7 +1525,18 @@ async function promptForAnswer(question, on) {
961
1525
  input.once?.("error", () => done(""));
962
1526
  });
963
1527
  }
964
- function createPairCommand(deps = {}) {
1528
+ async function runSetupChain(ctx, deps, flags) {
1529
+ try {
1530
+ return await runHarnessSetup(ctx, { sendTest: !flags.noTest }, deps);
1531
+ } catch (err) {
1532
+ const message = err instanceof Error ? err.message : String(err);
1533
+ ctx.io.errline(
1534
+ `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\`.`
1535
+ );
1536
+ return failedSetupReport(message);
1537
+ }
1538
+ }
1539
+ function createPairingCommand(verb, deps = {}) {
965
1540
  const fetchImpl = deps.fetchImpl ?? fetch;
966
1541
  const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
967
1542
  const clock = deps.now ?? (() => Date.now());
@@ -974,9 +1549,10 @@ function createPairCommand(deps = {}) {
974
1549
  return typeof pinned === "string" && pinned.trim().length > 0 ? pinned : void 0;
975
1550
  });
976
1551
  return {
977
- name: "pair",
978
- summary: "Pair this machine with your BirdyBeep account (QR or manual)",
979
- usage: "birdybeep pair [--yes] [--expect-email <addr>] [--json]",
1552
+ name: verb.name,
1553
+ summary: verb.summary,
1554
+ usage: verb.usage,
1555
+ ...verb.gettingStarted !== void 0 ? { gettingStarted: verb.gettingStarted } : {},
980
1556
  options: [
981
1557
  {
982
1558
  flag: "--yes",
@@ -987,18 +1563,43 @@ function createPairCommand(deps = {}) {
987
1563
  flag: "--expect-email",
988
1564
  value: "<addr>",
989
1565
  summary: "Only trust the pairing if this account approved it (else fail)"
1566
+ },
1567
+ {
1568
+ flag: "--no-install",
1569
+ summary: "Stop after pairing \u2014 don't detect or wire up any coding agent"
1570
+ },
1571
+ {
1572
+ flag: "--no-test",
1573
+ summary: "Don't send the test Beep at the end"
990
1574
  }
991
1575
  ],
992
1576
  run: async (ctx) => {
993
1577
  const pairFlags = parsePairFlags(ctx.args);
994
1578
  if (pairFlags.error !== void 0) {
995
- ctx.io.errline(`birdybeep pair: ${pairFlags.error}.`);
1579
+ ctx.io.errline(`birdybeep ${verb.name}: ${pairFlags.error}.`);
996
1580
  return EXIT.USAGE;
997
1581
  }
1582
+ const chain = deps.setup === false || pairFlags.noInstall ? void 0 : deps.setup ?? {};
1583
+ const setupDeps = {
1584
+ ...deps.tokenOptions !== void 0 ? { tokenOptions: deps.tokenOptions } : {},
1585
+ ...chain
1586
+ };
1587
+ if (verb.skipWhenPaired && await (0, import_agent_core9.getToken)(deps.tokenOptions ?? {}) !== null) {
1588
+ ctx.io.line(
1589
+ chain !== void 0 ? "\u2713 Already paired \u2014 checking which coding agents are wired up." : "\u2713 Already paired. Nothing else to do with --no-install."
1590
+ );
1591
+ const report2 = chain !== void 0 ? await runSetupChain(ctx, setupDeps, pairFlags) : void 0;
1592
+ ctx.io.result({
1593
+ paired: true,
1594
+ alreadyPaired: true,
1595
+ ...report2 !== void 0 ? { setup: report2 } : {}
1596
+ });
1597
+ return report2 !== void 0 && !report2.ok ? EXIT.ERROR : EXIT.OK;
1598
+ }
998
1599
  const apiUrl = resolveApiUrl();
999
- const identity = (0, import_agent_core8.getMachineIdentity)();
1000
- const codeVerifier = (0, import_agent_core8.generateCodeVerifier)();
1001
- const codeChallenge = (0, import_agent_core8.deriveCodeChallengeS256)(codeVerifier);
1600
+ const identity = (0, import_agent_core9.getMachineIdentity)();
1601
+ const codeVerifier = (0, import_agent_core9.generateCodeVerifier)();
1602
+ const codeChallenge = (0, import_agent_core9.deriveCodeChallengeS256)(codeVerifier);
1002
1603
  const start = await pairStart(
1003
1604
  apiUrl,
1004
1605
  { machineLabel: identity.label, os: identity.os, cliVersion: CLI_VERSION, codeChallenge },
@@ -1094,21 +1695,52 @@ function createPairCommand(deps = {}) {
1094
1695
  );
1095
1696
  return EXIT.ERROR;
1096
1697
  }
1097
- await (0, import_agent_core8.setToken)(paired.machineToken, deps.tokenOptions ?? {});
1698
+ await (0, import_agent_core9.setToken)(paired.machineToken, deps.tokenOptions ?? {});
1098
1699
  writeCliConfig({ apiUrl });
1700
+ const discarded = new import_agent_core9.LocalEventQueue().discardBefore(clock());
1701
+ (0, import_agent_core9.clearUnpairedNotice)();
1099
1702
  const humanSuffix = approvedBy !== void 0 ? ` to ${approvedBy}` : "";
1100
- ctx.io.emit(`\u2713 Paired${humanSuffix}. Run \`birdybeep test\` to send a test Beep.`, {
1703
+ const discardedSuffix = discarded > 0 ? ` Discarded ${discarded} event(s) queued before pairing \u2014 you won't be beeped about them.` : "";
1704
+ const nextStep = chain === void 0 ? " Run `birdybeep setup` to wire up your coding agents." : "";
1705
+ ctx.io.line(`\u2713 Paired${humanSuffix}.${nextStep}${discardedSuffix}`);
1706
+ const report = chain !== void 0 ? await runSetupChain(ctx, setupDeps, pairFlags) : void 0;
1707
+ ctx.io.result({
1101
1708
  paired: true,
1102
1709
  machineId: paired.machineId,
1103
- ...approvedBy !== void 0 ? { approvedByEmail: approvedBy } : {}
1710
+ discardedPrePairingEvents: discarded,
1711
+ ...approvedBy !== void 0 ? { approvedByEmail: approvedBy } : {},
1712
+ ...report !== void 0 ? { setup: report } : {}
1104
1713
  });
1105
- return EXIT.OK;
1714
+ return report !== void 0 && !report.ok ? EXIT.ERROR : EXIT.OK;
1106
1715
  }
1107
1716
  };
1108
1717
  }
1718
+ function createPairCommand(deps = {}) {
1719
+ return createPairingCommand(
1720
+ {
1721
+ name: "pair",
1722
+ summary: "Pair this machine and wire up every coding agent on it",
1723
+ usage: "birdybeep pair [--yes] [--expect-email <addr>] [--no-install] [--no-test] [--json]",
1724
+ skipWhenPaired: false
1725
+ },
1726
+ deps
1727
+ );
1728
+ }
1729
+ function createSetupCommand(deps = {}) {
1730
+ return createPairingCommand(
1731
+ {
1732
+ name: "setup",
1733
+ summary: "Set up BirdyBeep here: pair, wire up every coding agent, test",
1734
+ usage: "birdybeep setup [--yes] [--expect-email <addr>] [--no-install] [--no-test] [--json]",
1735
+ gettingStarted: "Pair this machine, wire up every coding agent it finds, and send a test Beep.",
1736
+ skipWhenPaired: true
1737
+ },
1738
+ deps
1739
+ );
1740
+ }
1109
1741
 
1110
1742
  // src/commands/queue.ts
1111
- var import_agent_core9 = require("@birdybeep/agent-core");
1743
+ var import_agent_core10 = require("@birdybeep/agent-core");
1112
1744
  function createQueueCommand() {
1113
1745
  return {
1114
1746
  name: "queue",
@@ -1120,7 +1752,7 @@ function createQueueCommand() {
1120
1752
  summary: "Clear the local offline event queue (debug)",
1121
1753
  usage: "birdybeep queue clear",
1122
1754
  run: (ctx) => {
1123
- const cleared = new import_agent_core9.LocalEventQueue().clear();
1755
+ const cleared = new import_agent_core10.LocalEventQueue().clear();
1124
1756
  ctx.io.emit(`Cleared ${cleared} queued event(s).`, { cleared });
1125
1757
  return EXIT.OK;
1126
1758
  }
@@ -1130,25 +1762,25 @@ function createQueueCommand() {
1130
1762
  }
1131
1763
 
1132
1764
  // src/commands/report-status.ts
1133
- var import_agent_core10 = require("@birdybeep/agent-core");
1134
- var import_claude_code4 = require("@birdybeep/claude-code");
1135
- var import_codex4 = require("@birdybeep/codex");
1136
- var import_copilot4 = require("@birdybeep/copilot");
1137
- var import_cursor4 = require("@birdybeep/cursor");
1138
- var import_opencode4 = require("@birdybeep/opencode");
1765
+ var import_agent_core11 = require("@birdybeep/agent-core");
1766
+ var import_claude_code6 = require("@birdybeep/claude-code");
1767
+ var import_codex5 = require("@birdybeep/codex");
1768
+ var import_copilot5 = require("@birdybeep/copilot");
1769
+ var import_cursor6 = require("@birdybeep/cursor");
1770
+ var import_opencode5 = require("@birdybeep/opencode");
1139
1771
  var DEFAULT_ADAPTERS3 = [
1140
- import_claude_code4.claudeCodeAdapter,
1141
- import_codex4.codexAdapter,
1142
- import_opencode4.opencodeAdapter,
1143
- import_cursor4.cursorAdapter,
1144
- import_copilot4.copilotAdapter
1772
+ import_claude_code6.claudeCodeAdapter,
1773
+ import_codex5.codexAdapter,
1774
+ import_opencode5.opencodeAdapter,
1775
+ import_cursor6.cursorAdapter,
1776
+ import_copilot5.copilotAdapter
1145
1777
  ];
1146
1778
  var ADAPTER_VERSIONS = {
1147
- claude_code: import_claude_code4.CLAUDE_CODE_ADAPTER_VERSION,
1148
- codex: import_codex4.CODEX_ADAPTER_VERSION,
1149
- opencode: import_opencode4.OPENCODE_ADAPTER_VERSION,
1150
- cursor: import_cursor4.CURSOR_ADAPTER_VERSION,
1151
- copilot: import_copilot4.COPILOT_ADAPTER_VERSION
1779
+ claude_code: import_claude_code6.CLAUDE_CODE_ADAPTER_VERSION,
1780
+ codex: import_codex5.CODEX_ADAPTER_VERSION,
1781
+ opencode: import_opencode5.OPENCODE_ADAPTER_VERSION,
1782
+ cursor: import_cursor6.CURSOR_ADAPTER_VERSION,
1783
+ copilot: import_copilot5.COPILOT_ADAPTER_VERSION
1152
1784
  };
1153
1785
  var base3 = (apiUrl) => apiUrl.replace(/\/$/, "");
1154
1786
  async function gatherItems(adapters) {
@@ -1171,7 +1803,7 @@ function createReportStatusCommand(deps = {}) {
1171
1803
  summary: "Internal: report integration status to the backend",
1172
1804
  usage: "birdybeep report-status [--json]",
1173
1805
  run: async (ctx) => {
1174
- const token = await (0, import_agent_core10.getToken)(deps.tokenOptions ?? {});
1806
+ const token = await (0, import_agent_core11.getToken)(deps.tokenOptions ?? {});
1175
1807
  if (token === null) {
1176
1808
  ctx.io.errline("No machine token \u2014 run `birdybeep pair` first.");
1177
1809
  return EXIT.ERROR;
@@ -1192,7 +1824,7 @@ function createReportStatusCommand(deps = {}) {
1192
1824
  });
1193
1825
  if (res.ok) {
1194
1826
  outcome = "reported";
1195
- const parsed = import_agent_core10.integrationStatusResponseSchema.safeParse(
1827
+ const parsed = import_agent_core11.integrationStatusResponseSchema.safeParse(
1196
1828
  await res.json().catch(() => void 0)
1197
1829
  );
1198
1830
  if (parsed.success) {
@@ -1202,7 +1834,7 @@ function createReportStatusCommand(deps = {}) {
1202
1834
  }));
1203
1835
  }
1204
1836
  } else {
1205
- const env = import_agent_core10.errorEnvelopeSchema.safeParse(await res.json().catch(() => void 0));
1837
+ const env = import_agent_core11.errorEnvelopeSchema.safeParse(await res.json().catch(() => void 0));
1206
1838
  errorCode = env.success ? env.data.error.code : void 0;
1207
1839
  const terminal = errorCode !== void 0 ? errorCode === "unauthorized" || errorCode === "forbidden" || errorCode === "token_revoked" : res.status === 401 || res.status === 403;
1208
1840
  outcome = terminal ? "terminal" : "deferred";
@@ -1233,22 +1865,22 @@ function createReportStatusCommand(deps = {}) {
1233
1865
  }
1234
1866
 
1235
1867
  // src/commands/status.ts
1236
- var import_agent_core11 = require("@birdybeep/agent-core");
1237
- var import_claude_code5 = require("@birdybeep/claude-code");
1238
- var import_codex5 = require("@birdybeep/codex");
1239
- var import_copilot5 = require("@birdybeep/copilot");
1240
- var import_cursor5 = require("@birdybeep/cursor");
1241
- var import_opencode5 = require("@birdybeep/opencode");
1868
+ var import_agent_core12 = require("@birdybeep/agent-core");
1869
+ var import_claude_code7 = require("@birdybeep/claude-code");
1870
+ var import_codex6 = require("@birdybeep/codex");
1871
+ var import_copilot6 = require("@birdybeep/copilot");
1872
+ var import_cursor7 = require("@birdybeep/cursor");
1873
+ var import_opencode6 = require("@birdybeep/opencode");
1242
1874
  var DEFAULT_ADAPTERS4 = [
1243
- import_claude_code5.claudeCodeAdapter,
1244
- import_codex5.codexAdapter,
1245
- import_opencode5.opencodeAdapter,
1246
- import_cursor5.cursorAdapter,
1247
- import_copilot5.copilotAdapter
1875
+ import_claude_code7.claudeCodeAdapter,
1876
+ import_codex6.codexAdapter,
1877
+ import_opencode6.opencodeAdapter,
1878
+ import_cursor7.cursorAdapter,
1879
+ import_copilot6.copilotAdapter
1248
1880
  ];
1249
1881
  function createStatusCommand(deps = {}) {
1250
1882
  const adapters = deps.adapters ?? DEFAULT_ADAPTERS4;
1251
- const makeSender = deps.createSender ?? ((baseUrl) => (0, import_agent_core11.createSender)(
1883
+ const makeSender = deps.createSender ?? ((baseUrl) => (0, import_agent_core12.createSender)(
1252
1884
  deps.tokenOptions ? { baseUrl, tokenOptions: deps.tokenOptions } : { baseUrl }
1253
1885
  ));
1254
1886
  return {
@@ -1259,14 +1891,21 @@ function createStatusCommand(deps = {}) {
1259
1891
  const machine = machineIdentity();
1260
1892
  const paired = await isPaired(deps.tokenOptions ?? {});
1261
1893
  const integrations = await gatherIntegrations(adapters);
1894
+ const surfaces = await gatherSurfaces(adapters, deps.surfaceOptions ?? {});
1262
1895
  const depthBefore = localQueueDepth();
1896
+ const unpaired = unpairedActivity();
1897
+ const filtered = filteredActivity();
1263
1898
  const drain = await makeSender(resolveApiUrl()).drainNow();
1264
1899
  const depthAfter = localQueueDepth();
1900
+ const overflowDropped = localQueueOverflowDrops();
1265
1901
  const report = {
1266
1902
  machine,
1267
1903
  paired,
1268
1904
  integrations,
1269
- queue: { depthBefore, delivered: drain.delivered, depthAfter }
1905
+ surfaces,
1906
+ queue: { depthBefore, delivered: drain.delivered, depthAfter, overflowDropped },
1907
+ ...unpaired !== null ? { unpairedActivity: unpaired } : {},
1908
+ ...filtered !== null ? { filteredActivity: filtered } : {}
1270
1909
  };
1271
1910
  if (ctx.flags.json) {
1272
1911
  ctx.io.result(report);
@@ -1274,85 +1913,29 @@ function createStatusCommand(deps = {}) {
1274
1913
  ctx.io.line(`Machine: ${machine.label} (${machine.os})`);
1275
1914
  ctx.io.line(paired ? "Paired: yes" : "Paired: no \u2014 run `birdybeep pair`");
1276
1915
  ctx.io.line("Integrations:");
1277
- for (const i of integrations) ctx.io.line(` ${i.displayName}: ${i.status}`);
1916
+ for (const i of integrations) {
1917
+ ctx.io.line(` ${i.displayName}: ${i.status}`);
1918
+ const group = surfaces.find((g) => g.harness === i.harness);
1919
+ for (const state of group?.surfaces ?? []) {
1920
+ const mark = state.coverage === "active" ? "\u2713" : state.coverage === "wired" ? "\xB7" : "\u2717";
1921
+ ctx.io.line(` ${mark} ${describeSurface(state)} \u2014 ${state.coverage}`);
1922
+ }
1923
+ }
1278
1924
  ctx.io.line(
1279
- `Queue: ${depthBefore} queued \u2192 ${drain.delivered} delivered, ${depthAfter} remaining`
1925
+ `Queue: ${depthBefore} queued \u2192 ${drain.delivered} delivered, ${depthAfter} remaining` + (overflowDropped > 0 ? `, ${overflowDropped} dropped by the queue cap` : "")
1280
1926
  );
1927
+ if (unpaired !== null) ctx.io.line(`\u26A0 Lost: ${describeUnpairedActivity(unpaired)}`);
1928
+ if (filtered !== null) ctx.io.line(`Local: ${describeFilteredActivity(filtered)}`);
1281
1929
  }
1282
1930
  return paired ? EXIT.OK : EXIT.ERROR;
1283
1931
  }
1284
1932
  };
1285
1933
  }
1286
1934
 
1287
- // src/commands/test.ts
1288
- var import_node_crypto2 = require("crypto");
1289
- var import_agent_core12 = require("@birdybeep/agent-core");
1290
- function buildTestEvent(opts = {}) {
1291
- const machine = (0, import_agent_core12.getMachineIdentity)();
1292
- return (0, import_agent_core12.normalizeEvent)(
1293
- {
1294
- event_type: "test",
1295
- status: "running",
1296
- harness: "claude_code",
1297
- // schema requires a harness; the "test" type distinguishes it
1298
- // Unique per run: a repeat `birdybeep test` inside the backend's dedupe window must
1299
- // still beep — a constant id made the second test silently "deduped" (9fh).
1300
- source_session_id: `birdybeep-cli-test-${(0, import_node_crypto2.randomUUID)()}`,
1301
- machine: { label: machine.label, os: machine.os },
1302
- workspace: { cwd: process.cwd() },
1303
- title: "BirdyBeep test event",
1304
- body: "If you can see this, your machine is wired up correctly.",
1305
- metadata: { test: true }
1306
- },
1307
- opts
1308
- );
1309
- }
1310
- function createTestCommand(deps = {}) {
1311
- const makeSender = deps.createSender ?? ((baseUrl) => (0, import_agent_core12.createSender)(
1312
- deps.tokenOptions ? { baseUrl, tokenOptions: deps.tokenOptions } : { baseUrl }
1313
- ));
1314
- return {
1315
- name: "test",
1316
- summary: "Send a test event end-to-end",
1317
- usage: "birdybeep test [--json]",
1318
- run: async (ctx) => {
1319
- const event = buildTestEvent();
1320
- const result = await makeSender(resolveApiUrl()).send(event);
1321
- if (ctx.flags.json) {
1322
- ctx.io.result({
1323
- outcome: result.outcome,
1324
- ...result.status ? { status: result.status } : {},
1325
- ...result.decision ? { decision: result.decision } : {}
1326
- });
1327
- } else if (result.outcome === "delivered") {
1328
- if (result.decision === "notified" || result.decision === void 0) {
1329
- ctx.io.line("\u2713 Test event delivered \u2014 check your phone for a test Beep.");
1330
- } else if (result.decision === "suppressed") {
1331
- ctx.io.line(
1332
- "\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`."
1333
- );
1334
- } else if (result.decision === "deduped") {
1335
- ctx.io.line(
1336
- "\u26A0 The backend accepted the test event but folded it into a recent duplicate \u2014 wait ~30s and run `birdybeep test` again."
1337
- );
1338
- } else {
1339
- ctx.io.line(
1340
- `\u26A0 The backend accepted the test event but decided "${result.decision}" \u2014 no push was sent. Run \`birdybeep doctor\`.`
1341
- );
1342
- }
1343
- } else if (result.outcome === "queued") {
1344
- ctx.io.line("\u2022 Offline \u2014 test event queued; it will deliver when you reconnect.");
1345
- } else {
1346
- ctx.io.line("\u2717 Test event was rejected by the backend. Run `birdybeep doctor`.");
1347
- }
1348
- return result.outcome === "dropped" ? EXIT.ERROR : EXIT.OK;
1349
- }
1350
- };
1351
- }
1352
-
1353
1935
  // src/commands.ts
1354
1936
  function buildCommands() {
1355
1937
  return [
1938
+ createSetupCommand(),
1356
1939
  createPairCommand(),
1357
1940
  createLogoutCommand(),
1358
1941
  createUnpairCommand(),
@@ -1367,7 +1950,7 @@ function buildCommands() {
1367
1950
  }
1368
1951
 
1369
1952
  // src/update-check.ts
1370
- var import_node_fs5 = require("fs");
1953
+ var import_node_fs6 = require("fs");
1371
1954
  var import_node_path3 = require("path");
1372
1955
  var import_agent_core13 = require("@birdybeep/agent-core");
1373
1956
  var PACKAGE_NAME = "@birdybeep/cli";
@@ -1427,7 +2010,7 @@ function updateCachePath() {
1427
2010
  }
1428
2011
  function readUpdateCache() {
1429
2012
  try {
1430
- const parsed = JSON.parse((0, import_node_fs5.readFileSync)(updateCachePath(), "utf8"));
2013
+ const parsed = JSON.parse((0, import_node_fs6.readFileSync)(updateCachePath(), "utf8"));
1431
2014
  if (typeof parsed !== "object" || parsed === null) return null;
1432
2015
  const { checkedAt, latest } = parsed;
1433
2016
  if (typeof checkedAt !== "number") return null;
@@ -1438,8 +2021,8 @@ function readUpdateCache() {
1438
2021
  }
1439
2022
  }
1440
2023
  function writeUpdateCache(cache) {
1441
- (0, import_node_fs5.mkdirSync)((0, import_agent_core13.birdyBeepConfigDir)(), { recursive: true, mode: 448 });
1442
- (0, import_node_fs5.writeFileSync)(updateCachePath(), `${JSON.stringify(cache)}
2024
+ (0, import_node_fs6.mkdirSync)((0, import_agent_core13.birdyBeepConfigDir)(), { recursive: true, mode: 448 });
2025
+ (0, import_node_fs6.writeFileSync)(updateCachePath(), `${JSON.stringify(cache)}
1443
2026
  `, { mode: 384 });
1444
2027
  }
1445
2028
  async function fetchLatestVersion(registryUrl, fetchImpl, timeoutMs) {