@birdybeep/cli 0.4.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",
@@ -576,20 +794,34 @@ function isHarnessName(value) {
576
794
  return value === "claude" || value === "codex" || value === "opencode" || value === "cursor" || value === "copilot";
577
795
  }
578
796
  function resolveHookHarness(harness, payload) {
579
- return harness === "claude" && (0, import_cursor3.isCursorHookPayload)(payload) ? "cursor" : harness;
797
+ return harness === "claude" && (0, import_cursor4.isCursorHookPayload)(payload) ? "cursor" : harness;
580
798
  }
581
799
  function recognizesPayload(harness, payload) {
582
- if (harness === "claude") return (0, import_claude_code3.isClaudeCodeHookPayload)(payload);
583
- if (harness === "cursor") return (0, import_cursor3.isCursorHookEventName)(asRecord(payload)["hook_event_name"]);
584
- return true;
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
+ }
585
812
  }
586
- function asRecord(value) {
813
+ function asRecord2(value) {
587
814
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
588
815
  }
589
- function describeEventName(payload) {
590
- const name = asRecord(payload)["hook_event_name"];
591
- if (typeof name !== "string") return "(absent)";
592
- return JSON.stringify(name.length > 64 ? `${name.slice(0, 63)}\u2026` : name);
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";
593
825
  }
594
826
  function runHookCommand(harness, payload, sender, copilotEventName) {
595
827
  const handler = resolveHookHarness(harness, payload);
@@ -623,10 +855,10 @@ function detachCodexNotifyWorker(payload) {
623
855
  try {
624
856
  const birdybeep = (0, import_agent_core5.resolveOnPath)("birdybeep");
625
857
  if (birdybeep === null) return false;
626
- 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`);
627
859
  file = tmpFile;
628
- (0, import_node_fs3.writeFileSync)(tmpFile, payload, { mode: 384 });
629
- 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");
630
862
  const child = (0, import_node_child_process.spawn)(birdybeep, ["hook", "codex"], {
631
863
  cwd: (0, import_node_path2.dirname)(birdybeep),
632
864
  // trusted dir, never the inherited/attacker cwd
@@ -640,7 +872,7 @@ function detachCodexNotifyWorker(payload) {
640
872
  });
641
873
  child.on("error", () => {
642
874
  try {
643
- (0, import_node_fs3.rmSync)(tmpFile, { force: true });
875
+ (0, import_node_fs4.rmSync)(tmpFile, { force: true });
644
876
  } catch {
645
877
  }
646
878
  });
@@ -649,7 +881,7 @@ function detachCodexNotifyWorker(payload) {
649
881
  } catch {
650
882
  if (file !== void 0) {
651
883
  try {
652
- (0, import_node_fs3.rmSync)(file, { force: true });
884
+ (0, import_node_fs4.rmSync)(file, { force: true });
653
885
  } catch {
654
886
  }
655
887
  }
@@ -657,7 +889,7 @@ function detachCodexNotifyWorker(payload) {
657
889
  } finally {
658
890
  if (fd !== void 0) {
659
891
  try {
660
- (0, import_node_fs3.closeSync)(fd);
892
+ (0, import_node_fs4.closeSync)(fd);
661
893
  } catch {
662
894
  }
663
895
  }
@@ -684,42 +916,75 @@ function createHookCommand(deps = {}) {
684
916
  return EXIT.OK;
685
917
  }
686
918
  const copilotEventName = harness === "copilot" && (0, import_copilot3.isCopilotHookEventName)(ctx.args[1]) ? ctx.args[1] : void 0;
687
- 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(
688
926
  readHookPayload(ctx.args, readStdin, harness === "copilot"),
689
927
  stdinTimeoutMs,
690
- ""
928
+ null
691
929
  );
692
930
  const notifyStdinFile = process.env[NOTIFY_STDIN_FILE_ENV];
693
- 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-")) {
694
932
  try {
695
- (0, import_node_fs3.rmSync)(notifyStdinFile, { force: true });
933
+ (0, import_node_fs4.rmSync)(notifyStdinFile, { force: true });
696
934
  } catch {
697
935
  }
698
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
+ }
699
952
  let payload;
700
953
  try {
701
954
  payload = JSON.parse(raw);
702
955
  } catch {
703
- ctx.io.result({ harness, outcome: "skipped" });
704
- return EXIT.OK;
956
+ return drop("invalid-json", `the ${raw.length}-byte payload is not valid JSON`);
705
957
  }
706
- const sender = makeSender(resolveApiUrl());
707
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;
972
+ }
973
+ const sender = makeSender(resolveApiUrl());
708
974
  const result = await runHookCommand(harness, payload, sender, copilotEventName);
709
975
  ctx.io.result({
710
976
  harness: handler,
711
- ...handler !== harness ? { routedFrom: harness } : {},
977
+ ...routedFrom,
712
978
  ...copilotEventName !== void 0 ? { event: copilotEventName } : {},
713
979
  outcome: result.outcome,
714
980
  eventType: result.eventType,
715
981
  ...result.send?.decision ? { decision: result.send.decision } : {},
716
982
  ...result.send?.status !== void 0 ? { status: result.send.status } : {}
717
983
  });
718
- if (result.outcome === "skipped" && !recognizesPayload(handler, payload)) {
984
+ if (result.outcome === "unpaired") {
719
985
  ctx.io.errline(
720
- `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.`
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)."
721
987
  );
722
- return EXIT.ERROR;
723
988
  }
724
989
  return EXIT.OK;
725
990
  }
@@ -778,8 +1043,8 @@ function createUnpairCommand(deps = {}) {
778
1043
  }
779
1044
 
780
1045
  // src/commands/pair.ts
781
- var import_node_fs4 = require("fs");
782
- var import_agent_core8 = require("@birdybeep/agent-core");
1046
+ var import_node_fs5 = require("fs");
1047
+ var import_agent_core9 = require("@birdybeep/agent-core");
783
1048
  var import_uqr = require("uqr");
784
1049
 
785
1050
  // src/pairing.ts
@@ -853,7 +1118,277 @@ async function pairTokenPoll(apiUrl, deviceCode, fetchImpl, machineFingerprint,
853
1118
  }
854
1119
 
855
1120
  // src/version.ts
856
- var CLI_VERSION = "0.4.0".length > 0 ? "0.4.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
+ }
857
1392
 
858
1393
  // src/commands/pair.ts
859
1394
  var DEFAULT_POLL_INTERVAL_MS = 2e3;
@@ -862,11 +1397,15 @@ function renderQrMatrix(qrPayload) {
862
1397
  return (0, import_uqr.renderUnicodeCompact)(qrPayload, { border: 2 });
863
1398
  }
864
1399
  function parsePairFlags(args) {
865
- const flags = { yes: false };
1400
+ const flags = { yes: false, noInstall: false, noTest: false };
866
1401
  for (let i = 0; i < args.length; i += 1) {
867
1402
  const token = args[i] ?? "";
868
1403
  if (token === "--yes" || token === "-y") {
869
1404
  flags.yes = true;
1405
+ } else if (token === "--no-install") {
1406
+ flags.noInstall = true;
1407
+ } else if (token === "--no-test") {
1408
+ flags.noTest = true;
870
1409
  } else if (token === "--expect-email" || token.startsWith("--expect-email=")) {
871
1410
  const inline = token.startsWith("--expect-email=") ? token.slice("--expect-email=".length) : void 0;
872
1411
  const value = inline ?? args[++i];
@@ -933,14 +1472,14 @@ function canOpenControllingTerminal(path = controllingTerminalPath(), platform =
933
1472
  if (platform === "win32") return false;
934
1473
  let fd;
935
1474
  try {
936
- fd = (0, import_node_fs4.openSync)(path, "r");
1475
+ fd = (0, import_node_fs5.openSync)(path, "r");
937
1476
  return true;
938
1477
  } catch {
939
1478
  return false;
940
1479
  } finally {
941
1480
  if (fd !== void 0) {
942
1481
  try {
943
- (0, import_node_fs4.closeSync)(fd);
1482
+ (0, import_node_fs5.closeSync)(fd);
944
1483
  } catch {
945
1484
  }
946
1485
  }
@@ -954,7 +1493,7 @@ async function promptForAnswer(question, on) {
954
1493
  input = process.stdin;
955
1494
  } else {
956
1495
  const { ReadStream } = await import("tty");
957
- ttyFd = (0, import_node_fs4.openSync)(controllingTerminalPath(), "r");
1496
+ ttyFd = (0, import_node_fs5.openSync)(controllingTerminalPath(), "r");
958
1497
  input = new ReadStream(ttyFd);
959
1498
  }
960
1499
  return new Promise((resolve) => {
@@ -974,7 +1513,7 @@ async function promptForAnswer(question, on) {
974
1513
  }
975
1514
  if (ttyFd !== void 0) {
976
1515
  try {
977
- (0, import_node_fs4.closeSync)(ttyFd);
1516
+ (0, import_node_fs5.closeSync)(ttyFd);
978
1517
  } catch {
979
1518
  }
980
1519
  }
@@ -986,7 +1525,18 @@ async function promptForAnswer(question, on) {
986
1525
  input.once?.("error", () => done(""));
987
1526
  });
988
1527
  }
989
- 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 = {}) {
990
1540
  const fetchImpl = deps.fetchImpl ?? fetch;
991
1541
  const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
992
1542
  const clock = deps.now ?? (() => Date.now());
@@ -999,9 +1549,10 @@ function createPairCommand(deps = {}) {
999
1549
  return typeof pinned === "string" && pinned.trim().length > 0 ? pinned : void 0;
1000
1550
  });
1001
1551
  return {
1002
- name: "pair",
1003
- summary: "Pair this machine with your BirdyBeep account (QR or manual)",
1004
- 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 } : {},
1005
1556
  options: [
1006
1557
  {
1007
1558
  flag: "--yes",
@@ -1012,18 +1563,43 @@ function createPairCommand(deps = {}) {
1012
1563
  flag: "--expect-email",
1013
1564
  value: "<addr>",
1014
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"
1015
1574
  }
1016
1575
  ],
1017
1576
  run: async (ctx) => {
1018
1577
  const pairFlags = parsePairFlags(ctx.args);
1019
1578
  if (pairFlags.error !== void 0) {
1020
- ctx.io.errline(`birdybeep pair: ${pairFlags.error}.`);
1579
+ ctx.io.errline(`birdybeep ${verb.name}: ${pairFlags.error}.`);
1021
1580
  return EXIT.USAGE;
1022
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
+ }
1023
1599
  const apiUrl = resolveApiUrl();
1024
- const identity = (0, import_agent_core8.getMachineIdentity)();
1025
- const codeVerifier = (0, import_agent_core8.generateCodeVerifier)();
1026
- 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);
1027
1603
  const start = await pairStart(
1028
1604
  apiUrl,
1029
1605
  { machineLabel: identity.label, os: identity.os, cliVersion: CLI_VERSION, codeChallenge },
@@ -1119,21 +1695,52 @@ function createPairCommand(deps = {}) {
1119
1695
  );
1120
1696
  return EXIT.ERROR;
1121
1697
  }
1122
- await (0, import_agent_core8.setToken)(paired.machineToken, deps.tokenOptions ?? {});
1698
+ await (0, import_agent_core9.setToken)(paired.machineToken, deps.tokenOptions ?? {});
1123
1699
  writeCliConfig({ apiUrl });
1700
+ const discarded = new import_agent_core9.LocalEventQueue().discardBefore(clock());
1701
+ (0, import_agent_core9.clearUnpairedNotice)();
1124
1702
  const humanSuffix = approvedBy !== void 0 ? ` to ${approvedBy}` : "";
1125
- 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({
1126
1708
  paired: true,
1127
1709
  machineId: paired.machineId,
1128
- ...approvedBy !== void 0 ? { approvedByEmail: approvedBy } : {}
1710
+ discardedPrePairingEvents: discarded,
1711
+ ...approvedBy !== void 0 ? { approvedByEmail: approvedBy } : {},
1712
+ ...report !== void 0 ? { setup: report } : {}
1129
1713
  });
1130
- return EXIT.OK;
1714
+ return report !== void 0 && !report.ok ? EXIT.ERROR : EXIT.OK;
1131
1715
  }
1132
1716
  };
1133
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
+ }
1134
1741
 
1135
1742
  // src/commands/queue.ts
1136
- var import_agent_core9 = require("@birdybeep/agent-core");
1743
+ var import_agent_core10 = require("@birdybeep/agent-core");
1137
1744
  function createQueueCommand() {
1138
1745
  return {
1139
1746
  name: "queue",
@@ -1145,7 +1752,7 @@ function createQueueCommand() {
1145
1752
  summary: "Clear the local offline event queue (debug)",
1146
1753
  usage: "birdybeep queue clear",
1147
1754
  run: (ctx) => {
1148
- const cleared = new import_agent_core9.LocalEventQueue().clear();
1755
+ const cleared = new import_agent_core10.LocalEventQueue().clear();
1149
1756
  ctx.io.emit(`Cleared ${cleared} queued event(s).`, { cleared });
1150
1757
  return EXIT.OK;
1151
1758
  }
@@ -1155,25 +1762,25 @@ function createQueueCommand() {
1155
1762
  }
1156
1763
 
1157
1764
  // src/commands/report-status.ts
1158
- var import_agent_core10 = require("@birdybeep/agent-core");
1159
- var import_claude_code4 = require("@birdybeep/claude-code");
1160
- var import_codex4 = require("@birdybeep/codex");
1161
- var import_copilot4 = require("@birdybeep/copilot");
1162
- var import_cursor4 = require("@birdybeep/cursor");
1163
- 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");
1164
1771
  var DEFAULT_ADAPTERS3 = [
1165
- import_claude_code4.claudeCodeAdapter,
1166
- import_codex4.codexAdapter,
1167
- import_opencode4.opencodeAdapter,
1168
- import_cursor4.cursorAdapter,
1169
- import_copilot4.copilotAdapter
1772
+ import_claude_code6.claudeCodeAdapter,
1773
+ import_codex5.codexAdapter,
1774
+ import_opencode5.opencodeAdapter,
1775
+ import_cursor6.cursorAdapter,
1776
+ import_copilot5.copilotAdapter
1170
1777
  ];
1171
1778
  var ADAPTER_VERSIONS = {
1172
- claude_code: import_claude_code4.CLAUDE_CODE_ADAPTER_VERSION,
1173
- codex: import_codex4.CODEX_ADAPTER_VERSION,
1174
- opencode: import_opencode4.OPENCODE_ADAPTER_VERSION,
1175
- cursor: import_cursor4.CURSOR_ADAPTER_VERSION,
1176
- 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
1177
1784
  };
1178
1785
  var base3 = (apiUrl) => apiUrl.replace(/\/$/, "");
1179
1786
  async function gatherItems(adapters) {
@@ -1196,7 +1803,7 @@ function createReportStatusCommand(deps = {}) {
1196
1803
  summary: "Internal: report integration status to the backend",
1197
1804
  usage: "birdybeep report-status [--json]",
1198
1805
  run: async (ctx) => {
1199
- const token = await (0, import_agent_core10.getToken)(deps.tokenOptions ?? {});
1806
+ const token = await (0, import_agent_core11.getToken)(deps.tokenOptions ?? {});
1200
1807
  if (token === null) {
1201
1808
  ctx.io.errline("No machine token \u2014 run `birdybeep pair` first.");
1202
1809
  return EXIT.ERROR;
@@ -1217,7 +1824,7 @@ function createReportStatusCommand(deps = {}) {
1217
1824
  });
1218
1825
  if (res.ok) {
1219
1826
  outcome = "reported";
1220
- const parsed = import_agent_core10.integrationStatusResponseSchema.safeParse(
1827
+ const parsed = import_agent_core11.integrationStatusResponseSchema.safeParse(
1221
1828
  await res.json().catch(() => void 0)
1222
1829
  );
1223
1830
  if (parsed.success) {
@@ -1227,7 +1834,7 @@ function createReportStatusCommand(deps = {}) {
1227
1834
  }));
1228
1835
  }
1229
1836
  } else {
1230
- 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));
1231
1838
  errorCode = env.success ? env.data.error.code : void 0;
1232
1839
  const terminal = errorCode !== void 0 ? errorCode === "unauthorized" || errorCode === "forbidden" || errorCode === "token_revoked" : res.status === 401 || res.status === 403;
1233
1840
  outcome = terminal ? "terminal" : "deferred";
@@ -1258,22 +1865,22 @@ function createReportStatusCommand(deps = {}) {
1258
1865
  }
1259
1866
 
1260
1867
  // src/commands/status.ts
1261
- var import_agent_core11 = require("@birdybeep/agent-core");
1262
- var import_claude_code5 = require("@birdybeep/claude-code");
1263
- var import_codex5 = require("@birdybeep/codex");
1264
- var import_copilot5 = require("@birdybeep/copilot");
1265
- var import_cursor5 = require("@birdybeep/cursor");
1266
- 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");
1267
1874
  var DEFAULT_ADAPTERS4 = [
1268
- import_claude_code5.claudeCodeAdapter,
1269
- import_codex5.codexAdapter,
1270
- import_opencode5.opencodeAdapter,
1271
- import_cursor5.cursorAdapter,
1272
- import_copilot5.copilotAdapter
1875
+ import_claude_code7.claudeCodeAdapter,
1876
+ import_codex6.codexAdapter,
1877
+ import_opencode6.opencodeAdapter,
1878
+ import_cursor7.cursorAdapter,
1879
+ import_copilot6.copilotAdapter
1273
1880
  ];
1274
1881
  function createStatusCommand(deps = {}) {
1275
1882
  const adapters = deps.adapters ?? DEFAULT_ADAPTERS4;
1276
- const makeSender = deps.createSender ?? ((baseUrl) => (0, import_agent_core11.createSender)(
1883
+ const makeSender = deps.createSender ?? ((baseUrl) => (0, import_agent_core12.createSender)(
1277
1884
  deps.tokenOptions ? { baseUrl, tokenOptions: deps.tokenOptions } : { baseUrl }
1278
1885
  ));
1279
1886
  return {
@@ -1284,14 +1891,21 @@ function createStatusCommand(deps = {}) {
1284
1891
  const machine = machineIdentity();
1285
1892
  const paired = await isPaired(deps.tokenOptions ?? {});
1286
1893
  const integrations = await gatherIntegrations(adapters);
1894
+ const surfaces = await gatherSurfaces(adapters, deps.surfaceOptions ?? {});
1287
1895
  const depthBefore = localQueueDepth();
1896
+ const unpaired = unpairedActivity();
1897
+ const filtered = filteredActivity();
1288
1898
  const drain = await makeSender(resolveApiUrl()).drainNow();
1289
1899
  const depthAfter = localQueueDepth();
1900
+ const overflowDropped = localQueueOverflowDrops();
1290
1901
  const report = {
1291
1902
  machine,
1292
1903
  paired,
1293
1904
  integrations,
1294
- 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 } : {}
1295
1909
  };
1296
1910
  if (ctx.flags.json) {
1297
1911
  ctx.io.result(report);
@@ -1299,85 +1913,29 @@ function createStatusCommand(deps = {}) {
1299
1913
  ctx.io.line(`Machine: ${machine.label} (${machine.os})`);
1300
1914
  ctx.io.line(paired ? "Paired: yes" : "Paired: no \u2014 run `birdybeep pair`");
1301
1915
  ctx.io.line("Integrations:");
1302
- 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
+ }
1303
1924
  ctx.io.line(
1304
- `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` : "")
1305
1926
  );
1927
+ if (unpaired !== null) ctx.io.line(`\u26A0 Lost: ${describeUnpairedActivity(unpaired)}`);
1928
+ if (filtered !== null) ctx.io.line(`Local: ${describeFilteredActivity(filtered)}`);
1306
1929
  }
1307
1930
  return paired ? EXIT.OK : EXIT.ERROR;
1308
1931
  }
1309
1932
  };
1310
1933
  }
1311
1934
 
1312
- // src/commands/test.ts
1313
- var import_node_crypto2 = require("crypto");
1314
- var import_agent_core12 = require("@birdybeep/agent-core");
1315
- function buildTestEvent(opts = {}) {
1316
- const machine = (0, import_agent_core12.getMachineIdentity)();
1317
- return (0, import_agent_core12.normalizeEvent)(
1318
- {
1319
- event_type: "test",
1320
- status: "running",
1321
- harness: "claude_code",
1322
- // schema requires a harness; the "test" type distinguishes it
1323
- // Unique per run: a repeat `birdybeep test` inside the backend's dedupe window must
1324
- // still beep — a constant id made the second test silently "deduped" (9fh).
1325
- source_session_id: `birdybeep-cli-test-${(0, import_node_crypto2.randomUUID)()}`,
1326
- machine: { label: machine.label, os: machine.os },
1327
- workspace: { cwd: process.cwd() },
1328
- title: "BirdyBeep test event",
1329
- body: "If you can see this, your machine is wired up correctly.",
1330
- metadata: { test: true }
1331
- },
1332
- opts
1333
- );
1334
- }
1335
- function createTestCommand(deps = {}) {
1336
- const makeSender = deps.createSender ?? ((baseUrl) => (0, import_agent_core12.createSender)(
1337
- deps.tokenOptions ? { baseUrl, tokenOptions: deps.tokenOptions } : { baseUrl }
1338
- ));
1339
- return {
1340
- name: "test",
1341
- summary: "Send a test event end-to-end",
1342
- usage: "birdybeep test [--json]",
1343
- run: async (ctx) => {
1344
- const event = buildTestEvent();
1345
- const result = await makeSender(resolveApiUrl()).send(event);
1346
- if (ctx.flags.json) {
1347
- ctx.io.result({
1348
- outcome: result.outcome,
1349
- ...result.status ? { status: result.status } : {},
1350
- ...result.decision ? { decision: result.decision } : {}
1351
- });
1352
- } else if (result.outcome === "delivered") {
1353
- if (result.decision === "notified" || result.decision === void 0) {
1354
- ctx.io.line("\u2713 Test event delivered \u2014 check your phone for a test Beep.");
1355
- } else if (result.decision === "suppressed") {
1356
- ctx.io.line(
1357
- "\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`."
1358
- );
1359
- } else if (result.decision === "deduped") {
1360
- ctx.io.line(
1361
- "\u26A0 The backend accepted the test event but folded it into a recent duplicate \u2014 wait ~30s and run `birdybeep test` again."
1362
- );
1363
- } else {
1364
- ctx.io.line(
1365
- `\u26A0 The backend accepted the test event but decided "${result.decision}" \u2014 no push was sent. Run \`birdybeep doctor\`.`
1366
- );
1367
- }
1368
- } else if (result.outcome === "queued") {
1369
- ctx.io.line("\u2022 Offline \u2014 test event queued; it will deliver when you reconnect.");
1370
- } else {
1371
- ctx.io.line("\u2717 Test event was rejected by the backend. Run `birdybeep doctor`.");
1372
- }
1373
- return result.outcome === "dropped" ? EXIT.ERROR : EXIT.OK;
1374
- }
1375
- };
1376
- }
1377
-
1378
1935
  // src/commands.ts
1379
1936
  function buildCommands() {
1380
1937
  return [
1938
+ createSetupCommand(),
1381
1939
  createPairCommand(),
1382
1940
  createLogoutCommand(),
1383
1941
  createUnpairCommand(),
@@ -1392,7 +1950,7 @@ function buildCommands() {
1392
1950
  }
1393
1951
 
1394
1952
  // src/update-check.ts
1395
- var import_node_fs5 = require("fs");
1953
+ var import_node_fs6 = require("fs");
1396
1954
  var import_node_path3 = require("path");
1397
1955
  var import_agent_core13 = require("@birdybeep/agent-core");
1398
1956
  var PACKAGE_NAME = "@birdybeep/cli";
@@ -1452,7 +2010,7 @@ function updateCachePath() {
1452
2010
  }
1453
2011
  function readUpdateCache() {
1454
2012
  try {
1455
- const parsed = JSON.parse((0, import_node_fs5.readFileSync)(updateCachePath(), "utf8"));
2013
+ const parsed = JSON.parse((0, import_node_fs6.readFileSync)(updateCachePath(), "utf8"));
1456
2014
  if (typeof parsed !== "object" || parsed === null) return null;
1457
2015
  const { checkedAt, latest } = parsed;
1458
2016
  if (typeof checkedAt !== "number") return null;
@@ -1463,8 +2021,8 @@ function readUpdateCache() {
1463
2021
  }
1464
2022
  }
1465
2023
  function writeUpdateCache(cache) {
1466
- (0, import_node_fs5.mkdirSync)((0, import_agent_core13.birdyBeepConfigDir)(), { recursive: true, mode: 448 });
1467
- (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)}
1468
2026
  `, { mode: 384 });
1469
2027
  }
1470
2028
  async function fetchLatestVersion(registryUrl, fetchImpl, timeoutMs) {