@birdybeep/cli 0.4.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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",
@@ -600,20 +818,34 @@ function isHarnessName(value) {
600
818
  return value === "claude" || value === "codex" || value === "opencode" || value === "cursor" || value === "copilot";
601
819
  }
602
820
  function resolveHookHarness(harness, payload) {
603
- return harness === "claude" && (0, import_cursor3.isCursorHookPayload)(payload) ? "cursor" : harness;
821
+ return harness === "claude" && (0, import_cursor4.isCursorHookPayload)(payload) ? "cursor" : harness;
604
822
  }
605
823
  function recognizesPayload(harness, payload) {
606
- if (harness === "claude") return (0, import_claude_code3.isClaudeCodeHookPayload)(payload);
607
- if (harness === "cursor") return (0, import_cursor3.isCursorHookEventName)(asRecord(payload)["hook_event_name"]);
608
- return true;
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
+ }
609
836
  }
610
- function asRecord(value) {
837
+ function asRecord2(value) {
611
838
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
612
839
  }
613
- function describeEventName(payload) {
614
- const name = asRecord(payload)["hook_event_name"];
615
- if (typeof name !== "string") return "(absent)";
616
- return JSON.stringify(name.length > 64 ? `${name.slice(0, 63)}\u2026` : name);
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";
617
849
  }
618
850
  function runHookCommand(harness, payload, sender, copilotEventName) {
619
851
  const handler = resolveHookHarness(harness, payload);
@@ -647,10 +879,10 @@ function detachCodexNotifyWorker(payload) {
647
879
  try {
648
880
  const birdybeep = (0, import_agent_core5.resolveOnPath)("birdybeep");
649
881
  if (birdybeep === null) return false;
650
- 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`);
651
883
  file = tmpFile;
652
- (0, import_node_fs3.writeFileSync)(tmpFile, payload, { mode: 384 });
653
- 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");
654
886
  const child = (0, import_node_child_process.spawn)(birdybeep, ["hook", "codex"], {
655
887
  cwd: (0, import_node_path2.dirname)(birdybeep),
656
888
  // trusted dir, never the inherited/attacker cwd
@@ -664,7 +896,7 @@ function detachCodexNotifyWorker(payload) {
664
896
  });
665
897
  child.on("error", () => {
666
898
  try {
667
- (0, import_node_fs3.rmSync)(tmpFile, { force: true });
899
+ (0, import_node_fs4.rmSync)(tmpFile, { force: true });
668
900
  } catch {
669
901
  }
670
902
  });
@@ -673,7 +905,7 @@ function detachCodexNotifyWorker(payload) {
673
905
  } catch {
674
906
  if (file !== void 0) {
675
907
  try {
676
- (0, import_node_fs3.rmSync)(file, { force: true });
908
+ (0, import_node_fs4.rmSync)(file, { force: true });
677
909
  } catch {
678
910
  }
679
911
  }
@@ -681,7 +913,7 @@ function detachCodexNotifyWorker(payload) {
681
913
  } finally {
682
914
  if (fd !== void 0) {
683
915
  try {
684
- (0, import_node_fs3.closeSync)(fd);
916
+ (0, import_node_fs4.closeSync)(fd);
685
917
  } catch {
686
918
  }
687
919
  }
@@ -708,42 +940,75 @@ function createHookCommand(deps = {}) {
708
940
  return EXIT.OK;
709
941
  }
710
942
  const copilotEventName = harness === "copilot" && (0, import_copilot3.isCopilotHookEventName)(ctx.args[1]) ? ctx.args[1] : void 0;
711
- 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(
712
950
  readHookPayload(ctx.args, readStdin, harness === "copilot"),
713
951
  stdinTimeoutMs,
714
- ""
952
+ null
715
953
  );
716
954
  const notifyStdinFile = process.env[NOTIFY_STDIN_FILE_ENV];
717
- 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-")) {
718
956
  try {
719
- (0, import_node_fs3.rmSync)(notifyStdinFile, { force: true });
957
+ (0, import_node_fs4.rmSync)(notifyStdinFile, { force: true });
720
958
  } catch {
721
959
  }
722
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
+ }
723
976
  let payload;
724
977
  try {
725
978
  payload = JSON.parse(raw);
726
979
  } catch {
727
- ctx.io.result({ harness, outcome: "skipped" });
728
- return EXIT.OK;
980
+ return drop("invalid-json", `the ${raw.length}-byte payload is not valid JSON`);
729
981
  }
730
- const sender = makeSender(resolveApiUrl());
731
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;
996
+ }
997
+ const sender = makeSender(resolveApiUrl());
732
998
  const result = await runHookCommand(harness, payload, sender, copilotEventName);
733
999
  ctx.io.result({
734
1000
  harness: handler,
735
- ...handler !== harness ? { routedFrom: harness } : {},
1001
+ ...routedFrom,
736
1002
  ...copilotEventName !== void 0 ? { event: copilotEventName } : {},
737
1003
  outcome: result.outcome,
738
1004
  eventType: result.eventType,
739
1005
  ...result.send?.decision ? { decision: result.send.decision } : {},
740
1006
  ...result.send?.status !== void 0 ? { status: result.send.status } : {}
741
1007
  });
742
- if (result.outcome === "skipped" && !recognizesPayload(handler, payload)) {
1008
+ if (result.outcome === "unpaired") {
743
1009
  ctx.io.errline(
744
- `birdybeep hook ${harness}: hook_event_name ${describeEventName(payload)} is not a ${handler} hook event \u2014 nothing was sent. Check which tool is running this hook.`
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)."
745
1011
  );
746
- return EXIT.ERROR;
747
1012
  }
748
1013
  return EXIT.OK;
749
1014
  }
@@ -802,8 +1067,8 @@ function createUnpairCommand(deps = {}) {
802
1067
  }
803
1068
 
804
1069
  // src/commands/pair.ts
805
- var import_node_fs4 = require("fs");
806
- var import_agent_core8 = require("@birdybeep/agent-core");
1070
+ var import_node_fs5 = require("fs");
1071
+ var import_agent_core9 = require("@birdybeep/agent-core");
807
1072
  var import_uqr = require("uqr");
808
1073
 
809
1074
  // src/pairing.ts
@@ -877,7 +1142,277 @@ async function pairTokenPoll(apiUrl, deviceCode, fetchImpl, machineFingerprint,
877
1142
  }
878
1143
 
879
1144
  // src/version.ts
880
- var CLI_VERSION = "0.4.0".length > 0 ? "0.4.0" : "0.0.0";
1145
+ var CLI_VERSION = "0.6.1".length > 0 ? "0.6.1" : "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
+ }
881
1416
 
882
1417
  // src/commands/pair.ts
883
1418
  var DEFAULT_POLL_INTERVAL_MS = 2e3;
@@ -886,11 +1421,15 @@ function renderQrMatrix(qrPayload) {
886
1421
  return (0, import_uqr.renderUnicodeCompact)(qrPayload, { border: 2 });
887
1422
  }
888
1423
  function parsePairFlags(args) {
889
- const flags = { yes: false };
1424
+ const flags = { yes: false, noInstall: false, noTest: false };
890
1425
  for (let i = 0; i < args.length; i += 1) {
891
1426
  const token = args[i] ?? "";
892
1427
  if (token === "--yes" || token === "-y") {
893
1428
  flags.yes = true;
1429
+ } else if (token === "--no-install") {
1430
+ flags.noInstall = true;
1431
+ } else if (token === "--no-test") {
1432
+ flags.noTest = true;
894
1433
  } else if (token === "--expect-email" || token.startsWith("--expect-email=")) {
895
1434
  const inline = token.startsWith("--expect-email=") ? token.slice("--expect-email=".length) : void 0;
896
1435
  const value = inline ?? args[++i];
@@ -957,14 +1496,14 @@ function canOpenControllingTerminal(path = controllingTerminalPath(), platform =
957
1496
  if (platform === "win32") return false;
958
1497
  let fd;
959
1498
  try {
960
- fd = (0, import_node_fs4.openSync)(path, "r");
1499
+ fd = (0, import_node_fs5.openSync)(path, "r");
961
1500
  return true;
962
1501
  } catch {
963
1502
  return false;
964
1503
  } finally {
965
1504
  if (fd !== void 0) {
966
1505
  try {
967
- (0, import_node_fs4.closeSync)(fd);
1506
+ (0, import_node_fs5.closeSync)(fd);
968
1507
  } catch {
969
1508
  }
970
1509
  }
@@ -978,7 +1517,7 @@ async function promptForAnswer(question, on) {
978
1517
  input = process.stdin;
979
1518
  } else {
980
1519
  const { ReadStream } = await import("tty");
981
- ttyFd = (0, import_node_fs4.openSync)(controllingTerminalPath(), "r");
1520
+ ttyFd = (0, import_node_fs5.openSync)(controllingTerminalPath(), "r");
982
1521
  input = new ReadStream(ttyFd);
983
1522
  }
984
1523
  return new Promise((resolve) => {
@@ -998,7 +1537,7 @@ async function promptForAnswer(question, on) {
998
1537
  }
999
1538
  if (ttyFd !== void 0) {
1000
1539
  try {
1001
- (0, import_node_fs4.closeSync)(ttyFd);
1540
+ (0, import_node_fs5.closeSync)(ttyFd);
1002
1541
  } catch {
1003
1542
  }
1004
1543
  }
@@ -1010,7 +1549,18 @@ async function promptForAnswer(question, on) {
1010
1549
  input.once?.("error", () => done(""));
1011
1550
  });
1012
1551
  }
1013
- 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 = {}) {
1014
1564
  const fetchImpl = deps.fetchImpl ?? fetch;
1015
1565
  const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
1016
1566
  const clock = deps.now ?? (() => Date.now());
@@ -1023,9 +1573,10 @@ function createPairCommand(deps = {}) {
1023
1573
  return typeof pinned === "string" && pinned.trim().length > 0 ? pinned : void 0;
1024
1574
  });
1025
1575
  return {
1026
- name: "pair",
1027
- summary: "Pair this machine with your BirdyBeep account (QR or manual)",
1028
- 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 } : {},
1029
1580
  options: [
1030
1581
  {
1031
1582
  flag: "--yes",
@@ -1036,18 +1587,43 @@ function createPairCommand(deps = {}) {
1036
1587
  flag: "--expect-email",
1037
1588
  value: "<addr>",
1038
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"
1039
1598
  }
1040
1599
  ],
1041
1600
  run: async (ctx) => {
1042
1601
  const pairFlags = parsePairFlags(ctx.args);
1043
1602
  if (pairFlags.error !== void 0) {
1044
- ctx.io.errline(`birdybeep pair: ${pairFlags.error}.`);
1603
+ ctx.io.errline(`birdybeep ${verb.name}: ${pairFlags.error}.`);
1045
1604
  return EXIT.USAGE;
1046
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
+ }
1047
1623
  const apiUrl = resolveApiUrl();
1048
- const identity = (0, import_agent_core8.getMachineIdentity)();
1049
- const codeVerifier = (0, import_agent_core8.generateCodeVerifier)();
1050
- 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);
1051
1627
  const start = await pairStart(
1052
1628
  apiUrl,
1053
1629
  { machineLabel: identity.label, os: identity.os, cliVersion: CLI_VERSION, codeChallenge },
@@ -1143,21 +1719,52 @@ function createPairCommand(deps = {}) {
1143
1719
  );
1144
1720
  return EXIT.ERROR;
1145
1721
  }
1146
- await (0, import_agent_core8.setToken)(paired.machineToken, deps.tokenOptions ?? {});
1722
+ await (0, import_agent_core9.setToken)(paired.machineToken, deps.tokenOptions ?? {});
1147
1723
  writeCliConfig({ apiUrl });
1724
+ const discarded = new import_agent_core9.LocalEventQueue().discardBefore(clock());
1725
+ (0, import_agent_core9.clearUnpairedNotice)();
1148
1726
  const humanSuffix = approvedBy !== void 0 ? ` to ${approvedBy}` : "";
1149
- 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({
1150
1732
  paired: true,
1151
1733
  machineId: paired.machineId,
1152
- ...approvedBy !== void 0 ? { approvedByEmail: approvedBy } : {}
1734
+ discardedPrePairingEvents: discarded,
1735
+ ...approvedBy !== void 0 ? { approvedByEmail: approvedBy } : {},
1736
+ ...report !== void 0 ? { setup: report } : {}
1153
1737
  });
1154
- return EXIT.OK;
1738
+ return report !== void 0 && !report.ok ? EXIT.ERROR : EXIT.OK;
1155
1739
  }
1156
1740
  };
1157
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
+ }
1158
1765
 
1159
1766
  // src/commands/queue.ts
1160
- var import_agent_core9 = require("@birdybeep/agent-core");
1767
+ var import_agent_core10 = require("@birdybeep/agent-core");
1161
1768
  function createQueueCommand() {
1162
1769
  return {
1163
1770
  name: "queue",
@@ -1169,7 +1776,7 @@ function createQueueCommand() {
1169
1776
  summary: "Clear the local offline event queue (debug)",
1170
1777
  usage: "birdybeep queue clear",
1171
1778
  run: (ctx) => {
1172
- const cleared = new import_agent_core9.LocalEventQueue().clear();
1779
+ const cleared = new import_agent_core10.LocalEventQueue().clear();
1173
1780
  ctx.io.emit(`Cleared ${cleared} queued event(s).`, { cleared });
1174
1781
  return EXIT.OK;
1175
1782
  }
@@ -1179,25 +1786,25 @@ function createQueueCommand() {
1179
1786
  }
1180
1787
 
1181
1788
  // src/commands/report-status.ts
1182
- var import_agent_core10 = require("@birdybeep/agent-core");
1183
- var import_claude_code4 = require("@birdybeep/claude-code");
1184
- var import_codex4 = require("@birdybeep/codex");
1185
- var import_copilot4 = require("@birdybeep/copilot");
1186
- var import_cursor4 = require("@birdybeep/cursor");
1187
- 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");
1188
1795
  var DEFAULT_ADAPTERS3 = [
1189
- import_claude_code4.claudeCodeAdapter,
1190
- import_codex4.codexAdapter,
1191
- import_opencode4.opencodeAdapter,
1192
- import_cursor4.cursorAdapter,
1193
- import_copilot4.copilotAdapter
1796
+ import_claude_code6.claudeCodeAdapter,
1797
+ import_codex5.codexAdapter,
1798
+ import_opencode5.opencodeAdapter,
1799
+ import_cursor6.cursorAdapter,
1800
+ import_copilot5.copilotAdapter
1194
1801
  ];
1195
1802
  var ADAPTER_VERSIONS = {
1196
- claude_code: import_claude_code4.CLAUDE_CODE_ADAPTER_VERSION,
1197
- codex: import_codex4.CODEX_ADAPTER_VERSION,
1198
- opencode: import_opencode4.OPENCODE_ADAPTER_VERSION,
1199
- cursor: import_cursor4.CURSOR_ADAPTER_VERSION,
1200
- 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
1201
1808
  };
1202
1809
  var base3 = (apiUrl) => apiUrl.replace(/\/$/, "");
1203
1810
  async function gatherItems(adapters) {
@@ -1220,7 +1827,7 @@ function createReportStatusCommand(deps = {}) {
1220
1827
  summary: "Internal: report integration status to the backend",
1221
1828
  usage: "birdybeep report-status [--json]",
1222
1829
  run: async (ctx) => {
1223
- const token = await (0, import_agent_core10.getToken)(deps.tokenOptions ?? {});
1830
+ const token = await (0, import_agent_core11.getToken)(deps.tokenOptions ?? {});
1224
1831
  if (token === null) {
1225
1832
  ctx.io.errline("No machine token \u2014 run `birdybeep pair` first.");
1226
1833
  return EXIT.ERROR;
@@ -1241,7 +1848,7 @@ function createReportStatusCommand(deps = {}) {
1241
1848
  });
1242
1849
  if (res.ok) {
1243
1850
  outcome = "reported";
1244
- const parsed = import_agent_core10.integrationStatusResponseSchema.safeParse(
1851
+ const parsed = import_agent_core11.integrationStatusResponseSchema.safeParse(
1245
1852
  await res.json().catch(() => void 0)
1246
1853
  );
1247
1854
  if (parsed.success) {
@@ -1251,7 +1858,7 @@ function createReportStatusCommand(deps = {}) {
1251
1858
  }));
1252
1859
  }
1253
1860
  } else {
1254
- 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));
1255
1862
  errorCode = env.success ? env.data.error.code : void 0;
1256
1863
  const terminal = errorCode !== void 0 ? errorCode === "unauthorized" || errorCode === "forbidden" || errorCode === "token_revoked" : res.status === 401 || res.status === 403;
1257
1864
  outcome = terminal ? "terminal" : "deferred";
@@ -1282,22 +1889,22 @@ function createReportStatusCommand(deps = {}) {
1282
1889
  }
1283
1890
 
1284
1891
  // src/commands/status.ts
1285
- var import_agent_core11 = require("@birdybeep/agent-core");
1286
- var import_claude_code5 = require("@birdybeep/claude-code");
1287
- var import_codex5 = require("@birdybeep/codex");
1288
- var import_copilot5 = require("@birdybeep/copilot");
1289
- var import_cursor5 = require("@birdybeep/cursor");
1290
- 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");
1291
1898
  var DEFAULT_ADAPTERS4 = [
1292
- import_claude_code5.claudeCodeAdapter,
1293
- import_codex5.codexAdapter,
1294
- import_opencode5.opencodeAdapter,
1295
- import_cursor5.cursorAdapter,
1296
- import_copilot5.copilotAdapter
1899
+ import_claude_code7.claudeCodeAdapter,
1900
+ import_codex6.codexAdapter,
1901
+ import_opencode6.opencodeAdapter,
1902
+ import_cursor7.cursorAdapter,
1903
+ import_copilot6.copilotAdapter
1297
1904
  ];
1298
1905
  function createStatusCommand(deps = {}) {
1299
1906
  const adapters = deps.adapters ?? DEFAULT_ADAPTERS4;
1300
- const makeSender = deps.createSender ?? ((baseUrl) => (0, import_agent_core11.createSender)(
1907
+ const makeSender = deps.createSender ?? ((baseUrl) => (0, import_agent_core12.createSender)(
1301
1908
  deps.tokenOptions ? { baseUrl, tokenOptions: deps.tokenOptions } : { baseUrl }
1302
1909
  ));
1303
1910
  return {
@@ -1308,14 +1915,21 @@ function createStatusCommand(deps = {}) {
1308
1915
  const machine = machineIdentity();
1309
1916
  const paired = await isPaired(deps.tokenOptions ?? {});
1310
1917
  const integrations = await gatherIntegrations(adapters);
1918
+ const surfaces = await gatherSurfaces(adapters, deps.surfaceOptions ?? {});
1311
1919
  const depthBefore = localQueueDepth();
1920
+ const unpaired = unpairedActivity();
1921
+ const filtered = filteredActivity();
1312
1922
  const drain = await makeSender(resolveApiUrl()).drainNow();
1313
1923
  const depthAfter = localQueueDepth();
1924
+ const overflowDropped = localQueueOverflowDrops();
1314
1925
  const report = {
1315
1926
  machine,
1316
1927
  paired,
1317
1928
  integrations,
1318
- 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 } : {}
1319
1933
  };
1320
1934
  if (ctx.flags.json) {
1321
1935
  ctx.io.result(report);
@@ -1323,85 +1937,29 @@ function createStatusCommand(deps = {}) {
1323
1937
  ctx.io.line(`Machine: ${machine.label} (${machine.os})`);
1324
1938
  ctx.io.line(paired ? "Paired: yes" : "Paired: no \u2014 run `birdybeep pair`");
1325
1939
  ctx.io.line("Integrations:");
1326
- 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
+ }
1327
1948
  ctx.io.line(
1328
- `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` : "")
1329
1950
  );
1951
+ if (unpaired !== null) ctx.io.line(`\u26A0 Lost: ${describeUnpairedActivity(unpaired)}`);
1952
+ if (filtered !== null) ctx.io.line(`Local: ${describeFilteredActivity(filtered)}`);
1330
1953
  }
1331
1954
  return paired ? EXIT.OK : EXIT.ERROR;
1332
1955
  }
1333
1956
  };
1334
1957
  }
1335
1958
 
1336
- // src/commands/test.ts
1337
- var import_node_crypto2 = require("crypto");
1338
- var import_agent_core12 = require("@birdybeep/agent-core");
1339
- function buildTestEvent(opts = {}) {
1340
- const machine = (0, import_agent_core12.getMachineIdentity)();
1341
- return (0, import_agent_core12.normalizeEvent)(
1342
- {
1343
- event_type: "test",
1344
- status: "running",
1345
- harness: "claude_code",
1346
- // schema requires a harness; the "test" type distinguishes it
1347
- // Unique per run: a repeat `birdybeep test` inside the backend's dedupe window must
1348
- // still beep — a constant id made the second test silently "deduped" (9fh).
1349
- source_session_id: `birdybeep-cli-test-${(0, import_node_crypto2.randomUUID)()}`,
1350
- machine: { label: machine.label, os: machine.os },
1351
- workspace: { cwd: process.cwd() },
1352
- title: "BirdyBeep test event",
1353
- body: "If you can see this, your machine is wired up correctly.",
1354
- metadata: { test: true }
1355
- },
1356
- opts
1357
- );
1358
- }
1359
- function createTestCommand(deps = {}) {
1360
- const makeSender = deps.createSender ?? ((baseUrl) => (0, import_agent_core12.createSender)(
1361
- deps.tokenOptions ? { baseUrl, tokenOptions: deps.tokenOptions } : { baseUrl }
1362
- ));
1363
- return {
1364
- name: "test",
1365
- summary: "Send a test event end-to-end",
1366
- usage: "birdybeep test [--json]",
1367
- run: async (ctx) => {
1368
- const event = buildTestEvent();
1369
- const result = await makeSender(resolveApiUrl()).send(event);
1370
- if (ctx.flags.json) {
1371
- ctx.io.result({
1372
- outcome: result.outcome,
1373
- ...result.status ? { status: result.status } : {},
1374
- ...result.decision ? { decision: result.decision } : {}
1375
- });
1376
- } else if (result.outcome === "delivered") {
1377
- if (result.decision === "notified" || result.decision === void 0) {
1378
- ctx.io.line("\u2713 Test event delivered \u2014 check your phone for a test Beep.");
1379
- } else if (result.decision === "suppressed") {
1380
- ctx.io.line(
1381
- "\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`."
1382
- );
1383
- } else if (result.decision === "deduped") {
1384
- ctx.io.line(
1385
- "\u26A0 The backend accepted the test event but folded it into a recent duplicate \u2014 wait ~30s and run `birdybeep test` again."
1386
- );
1387
- } else {
1388
- ctx.io.line(
1389
- `\u26A0 The backend accepted the test event but decided "${result.decision}" \u2014 no push was sent. Run \`birdybeep doctor\`.`
1390
- );
1391
- }
1392
- } else if (result.outcome === "queued") {
1393
- ctx.io.line("\u2022 Offline \u2014 test event queued; it will deliver when you reconnect.");
1394
- } else {
1395
- ctx.io.line("\u2717 Test event was rejected by the backend. Run `birdybeep doctor`.");
1396
- }
1397
- return result.outcome === "dropped" ? EXIT.ERROR : EXIT.OK;
1398
- }
1399
- };
1400
- }
1401
-
1402
1959
  // src/commands.ts
1403
1960
  function buildCommands() {
1404
1961
  return [
1962
+ createSetupCommand(),
1405
1963
  createPairCommand(),
1406
1964
  createLogoutCommand(),
1407
1965
  createUnpairCommand(),
@@ -1416,7 +1974,7 @@ function buildCommands() {
1416
1974
  }
1417
1975
 
1418
1976
  // src/update-check.ts
1419
- var import_node_fs5 = require("fs");
1977
+ var import_node_fs6 = require("fs");
1420
1978
  var import_node_path3 = require("path");
1421
1979
  var import_agent_core13 = require("@birdybeep/agent-core");
1422
1980
  var PACKAGE_NAME = "@birdybeep/cli";
@@ -1476,7 +2034,7 @@ function updateCachePath() {
1476
2034
  }
1477
2035
  function readUpdateCache() {
1478
2036
  try {
1479
- const parsed = JSON.parse((0, import_node_fs5.readFileSync)(updateCachePath(), "utf8"));
2037
+ const parsed = JSON.parse((0, import_node_fs6.readFileSync)(updateCachePath(), "utf8"));
1480
2038
  if (typeof parsed !== "object" || parsed === null) return null;
1481
2039
  const { checkedAt, latest } = parsed;
1482
2040
  if (typeof checkedAt !== "number") return null;
@@ -1487,8 +2045,8 @@ function readUpdateCache() {
1487
2045
  }
1488
2046
  }
1489
2047
  function writeUpdateCache(cache) {
1490
- (0, import_node_fs5.mkdirSync)((0, import_agent_core13.birdyBeepConfigDir)(), { recursive: true, mode: 448 });
1491
- (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)}
1492
2050
  `, { mode: 384 });
1493
2051
  }
1494
2052
  async function fetchLatestVersion(registryUrl, fetchImpl, timeoutMs) {