@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.
@@ -78,11 +78,17 @@ function isUnknownFlag(token, allowed) {
78
78
  function renderRootHelp(version, commands) {
79
79
  const width = Math.max(...commands.map((c) => c.name.length));
80
80
  const lines = commands.map((c) => ` ${c.name.padEnd(width)} ${c.summary}`);
81
+ const featured = commands.filter((c) => c.gettingStarted !== void 0);
81
82
  return [
82
83
  `birdybeep ${version} \u2014 stream coding-agent lifecycle events to BirdyBeep.`,
83
84
  "",
84
85
  "Usage:",
85
86
  " birdybeep <command> [options]",
87
+ ...featured.length > 0 ? [
88
+ "",
89
+ "Getting started:",
90
+ ...featured.map((c) => ` birdybeep ${c.name} ${c.gettingStarted ?? ""}`)
91
+ ] : [],
86
92
  "",
87
93
  "Commands:",
88
94
  ...lines,
@@ -164,7 +170,11 @@ async function dispatch(argv, deps) {
164
170
  if (rest.length === 0 || flags.help && command === void 0) {
165
171
  io.emit(renderRootHelp(deps.version, deps.commands), {
166
172
  version: deps.version,
167
- commands: deps.commands.map((c) => ({ name: c.name, summary: c.summary }))
173
+ commands: deps.commands.map((c) => ({
174
+ name: c.name,
175
+ summary: c.summary,
176
+ ...c.gettingStarted !== void 0 ? { gettingStarted: c.gettingStarted } : {}
177
+ }))
168
178
  });
169
179
  return EXIT.OK;
170
180
  }
@@ -217,7 +227,7 @@ async function dispatch(argv, deps) {
217
227
  }
218
228
 
219
229
  // src/version.ts
220
- var CLI_VERSION = "0.4.0".length > 0 ? "0.4.0" : "0.0.0";
230
+ var CLI_VERSION = "0.6.0".length > 0 ? "0.6.0" : "0.0.0";
221
231
 
222
232
  // src/commands/agent.ts
223
233
  import { claudeCodeAdapter } from "@birdybeep/claude-code";
@@ -225,6 +235,200 @@ import { codexAdapter } from "@birdybeep/codex";
225
235
  import { copilotAdapter } from "@birdybeep/copilot";
226
236
  import { cursorAdapter } from "@birdybeep/cursor";
227
237
  import { opencodeAdapter } from "@birdybeep/opencode";
238
+
239
+ // src/diagnostics.ts
240
+ import { existsSync, readFileSync } from "fs";
241
+ import { homedir } from "os";
242
+ import {
243
+ getMachineIdentity,
244
+ getToken,
245
+ LocalEventQueue,
246
+ readFilteredActivity,
247
+ readObservedBuilds,
248
+ readUnpairedNotice
249
+ } from "@birdybeep/agent-core";
250
+ import {
251
+ BIRDYBEEP_HOOK_EVENTS as CLAUDE_HOOK_EVENTS,
252
+ claudeSettingsPath,
253
+ isBirdyBeepEntry as isClaudeEntry
254
+ } from "@birdybeep/claude-code";
255
+ import {
256
+ BIRDYBEEP_HOOK_EVENTS as CURSOR_HOOK_EVENTS,
257
+ cursorHooksPath,
258
+ detectCursor,
259
+ isBirdyBeepEntry as isCursorEntry
260
+ } from "@birdybeep/cursor";
261
+ async function gatherIntegrations(adapters) {
262
+ return Promise.all(
263
+ adapters.map(async (a) => ({
264
+ harness: a.id,
265
+ displayName: a.displayName,
266
+ status: await a.status()
267
+ }))
268
+ );
269
+ }
270
+ async function isPaired(tokenOptions = {}) {
271
+ return await getToken(tokenOptions) !== null;
272
+ }
273
+ function localQueueDepth() {
274
+ return new LocalEventQueue().size();
275
+ }
276
+ function localQueueOverflowDrops() {
277
+ return new LocalEventQueue().overflowDropCount();
278
+ }
279
+ function unpairedActivity() {
280
+ return readUnpairedNotice();
281
+ }
282
+ function describeUnpairedActivity(notice) {
283
+ const since = new Date(notice.firstAt).toISOString();
284
+ const from = notice.harnesses.length > 0 ? ` from ${notice.harnesses.join(", ")}` : "";
285
+ return `${notice.count} event(s)${from} fired since ${since} and were NOT sent \u2014 this machine is not paired.`;
286
+ }
287
+ function asRecord(value) {
288
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
289
+ }
290
+ function birdyBeepHookCount(path, events, isBirdyBeepEntry) {
291
+ if (!existsSync(path)) return 0;
292
+ let parsed;
293
+ try {
294
+ parsed = JSON.parse(readFileSync(path, "utf8"));
295
+ } catch {
296
+ return null;
297
+ }
298
+ const hooks = asRecord(asRecord(parsed)["hooks"]);
299
+ let present = 0;
300
+ for (const event of events) {
301
+ const entries = hooks[event];
302
+ if (Array.isArray(entries) && entries.some(isBirdyBeepEntry)) present += 1;
303
+ }
304
+ return present;
305
+ }
306
+ async function cursorBridgeOnly(opts = {}) {
307
+ const home = opts.home ?? homedir();
308
+ const detection = await (opts.detect ?? (() => detectCursor({ home })))();
309
+ if (!detection.detected) return false;
310
+ const claude = birdyBeepHookCount(claudeSettingsPath(home), CLAUDE_HOOK_EVENTS, isClaudeEntry);
311
+ if (claude === null || claude === 0) return false;
312
+ return birdyBeepHookCount(cursorHooksPath(home), CURSOR_HOOK_EVENTS, isCursorEntry) === 0;
313
+ }
314
+ function filteredActivity() {
315
+ return readFilteredActivity();
316
+ }
317
+ function describeFilteredActivity(activity) {
318
+ const types = Object.entries(activity.byType).sort(([, a], [, b]) => b - a).map(([type, n]) => `${type} \xD7${n}`).join(", ");
319
+ const since = new Date(activity.firstAt).toISOString();
320
+ return `${activity.count} local-only event(s) since ${since}${types ? ` (${types})` : ""} \u2014 hooks are firing; these types never beep, so they are not sent.`;
321
+ }
322
+ function machineIdentity() {
323
+ return getMachineIdentity();
324
+ }
325
+ var CONFIGURED_STATUSES = /* @__PURE__ */ new Set([
326
+ "installed",
327
+ "needs_trust",
328
+ "needs_restart"
329
+ ]);
330
+ function gradeSurfaces(surfaces, status, observation) {
331
+ const builds = Object.values(observation?.builds ?? {});
332
+ const configured = CONFIGURED_STATUSES.has(status);
333
+ const claimedByKind = /* @__PURE__ */ new Map();
334
+ for (const s of surfaces) {
335
+ if (s.version === void 0) continue;
336
+ const versions = claimedByKind.get(s.kind) ?? /* @__PURE__ */ new Set();
337
+ versions.add(s.version);
338
+ claimedByKind.set(s.kind, versions);
339
+ }
340
+ const graded = surfaces.map((surface) => {
341
+ const exact = builds.filter(
342
+ (b) => b.surface === surface.kind && b.version === surface.version && surface.version !== void 0
343
+ );
344
+ let soleOfKind = [];
345
+ if (surface.version === void 0) {
346
+ const sameKindVersionless = surfaces.filter(
347
+ (s) => s.version === void 0 && s.kind === surface.kind
348
+ );
349
+ const unclaimed = builds.filter(
350
+ (b) => b.surface === surface.kind && !(claimedByKind.get(surface.kind)?.has(b.version) ?? false)
351
+ );
352
+ if (unclaimed.length === 1 && sameKindVersionless.length === 1) soleOfKind = unclaimed;
353
+ }
354
+ const unattributed = surface.version === void 0 ? [] : builds.filter((b) => b.surface === "unknown" && b.version === surface.version);
355
+ const sharesVersion = surface.version !== void 0 && surfaces.some((s) => s !== surface && s.version === surface.version);
356
+ const ambiguous = unattributed.length > 0 && sharesVersion;
357
+ const matched = [...exact, ...soleOfKind, ...ambiguous ? [] : unattributed];
358
+ const events = matched.reduce((total, b) => total + b.count, 0);
359
+ const lastAt = matched.reduce(
360
+ (latest, b) => latest === void 0 || b.lastAt > latest ? b.lastAt : latest,
361
+ void 0
362
+ );
363
+ const observedVersion = surface.version === void 0 ? soleOfKind[0]?.version : void 0;
364
+ return {
365
+ surface,
366
+ events,
367
+ ambiguous,
368
+ ...lastAt !== void 0 ? { lastAt } : {},
369
+ ...observedVersion !== void 0 ? { observedVersion } : {}
370
+ };
371
+ });
372
+ const anyActive = graded.some((g) => g.events > 0 && g.surface.shadowed !== true);
373
+ return graded.map(({ ambiguous, ...g }) => ({
374
+ ...g,
375
+ coverage: !configured ? "uncovered" : g.events > 0 ? "active" : anyActive && g.surface.shadowed !== true && !ambiguous ? "uncovered" : "wired"
376
+ }));
377
+ }
378
+ async function gatherSurfaces(adapters, options = {}) {
379
+ const observed = readObservedBuilds(options.observedBuilds ?? {});
380
+ return Promise.all(
381
+ adapters.map(async (adapter) => {
382
+ const observation = observed[adapter.id];
383
+ const base4 = {
384
+ harness: adapter.id,
385
+ displayName: adapter.displayName,
386
+ unversionedEvents: observation?.unversioned ?? 0
387
+ };
388
+ try {
389
+ const [detection, status] = await Promise.all([adapter.detect(), adapter.status()]);
390
+ return {
391
+ ...base4,
392
+ status,
393
+ surfaces: detection.detected ? gradeSurfaces(detection.surfaces ?? [], status, observation) : []
394
+ };
395
+ } catch {
396
+ return { ...base4, status: "unknown", surfaces: [] };
397
+ }
398
+ })
399
+ );
400
+ }
401
+ function describeSurface(state) {
402
+ const version = state.surface.version ?? state.observedVersion;
403
+ return version !== void 0 ? `${state.surface.label} ${version}` : state.surface.label;
404
+ }
405
+ function describeSurfaceCoverage(state, group) {
406
+ if (state.coverage === "active") {
407
+ const last = state.lastAt !== void 0 ? `, last ${new Date(state.lastAt).toISOString()}` : "";
408
+ return `covered \u2014 ${state.events} event(s) from this build${last}`;
409
+ }
410
+ if (state.coverage === "wired") {
411
+ 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`;
412
+ }
413
+ if (!CONFIGURED_STATUSES.has(group.status)) {
414
+ return `not covered \u2014 ${group.displayName} carries no BirdyBeep hooks, so this build cannot beep`;
415
+ }
416
+ const active = group.surfaces.filter((s) => s.coverage === "active").map(describeSurface);
417
+ const delivering = active.join(", ");
418
+ const verb = active.length === 1 ? "is" : "are";
419
+ return `not covered \u2014 nothing has ever fired from this build, while ${delivering} ${verb} delivering through the same config`;
420
+ }
421
+ function installTarget(harness) {
422
+ return harness === "claude_code" ? "claude" : harness;
423
+ }
424
+ function surfaceRemedy(state, group) {
425
+ if (state.coverage !== "uncovered") return void 0;
426
+ if (!CONFIGURED_STATUSES.has(group.status)) return void 0;
427
+ const install = `\`birdybeep agent install ${installTarget(group.harness)}\``;
428
+ 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.`;
429
+ }
430
+
431
+ // src/commands/agent.ts
228
432
  var DEFAULT_ADAPTERS = [
229
433
  claudeCodeAdapter,
230
434
  codexAdapter,
@@ -253,7 +457,10 @@ function selectAdapters(target, adapters) {
253
457
  if (id === void 0) return "unknown";
254
458
  return adapters.filter((a) => a.id === id);
255
459
  }
256
- async function installSelected(adapters, ctx) {
460
+ function installTarget2(harness) {
461
+ return harness === "claude_code" ? "claude" : harness;
462
+ }
463
+ async function installSelected(adapters, ctx, tokenOptions) {
257
464
  const target = ctx.args[0] ?? "all";
258
465
  const selected = selectAdapters(target, adapters);
259
466
  if (selected === "unknown") {
@@ -280,8 +487,9 @@ async function installSelected(adapters, ctx) {
280
487
  requiredActions: result.requiredActions
281
488
  });
282
489
  }
490
+ const paired = await isPaired(tokenOptions);
283
491
  if (ctx.flags.json) {
284
- ctx.io.result({ target, results: outcomes });
492
+ ctx.io.result({ target, paired, results: outcomes });
285
493
  return EXIT.OK;
286
494
  }
287
495
  if (outcomes.length === 0 || outcomes.every((o) => !o.detected)) {
@@ -289,13 +497,20 @@ async function installSelected(adapters, ctx) {
289
497
  }
290
498
  for (const o of outcomes) {
291
499
  if (!o.detected) {
292
- ctx.io.line(`\u2013 ${o.displayName}: not detected (skipped)`);
500
+ ctx.io.line(
501
+ `\u2013 ${o.displayName}: not detected (skipped) \u2014 install it, then run \`birdybeep agent install ${installTarget2(o.harness)}\``
502
+ );
293
503
  continue;
294
504
  }
295
505
  const changed = (o.changedFiles ?? []).length > 0 ? o.changedFiles.join(", ") : "no changes";
296
506
  ctx.io.line(`\u2713 ${o.displayName}: ${o.status} (${changed})`);
297
507
  for (const action of o.requiredActions ?? []) ctx.io.line(` \u2192 ${action}`);
298
508
  }
509
+ if (!paired) {
510
+ ctx.io.line(
511
+ "\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."
512
+ );
513
+ }
299
514
  return EXIT.OK;
300
515
  }
301
516
  async function uninstallSelected(adapters, ctx) {
@@ -334,6 +549,7 @@ async function uninstallSelected(adapters, ctx) {
334
549
  }
335
550
  function createAgentCommand(deps = {}) {
336
551
  const adapters = deps.adapters ?? DEFAULT_ADAPTERS;
552
+ const tokenOptions = deps.tokenOptions ?? {};
337
553
  return {
338
554
  name: "agent",
339
555
  summary: "Install or uninstall harness adapters",
@@ -343,7 +559,7 @@ function createAgentCommand(deps = {}) {
343
559
  name: "install",
344
560
  summary: "Install adapters (all | claude | codex | opencode | cursor | copilot)",
345
561
  usage: "birdybeep agent install [all|claude|codex|opencode|cursor|copilot]",
346
- run: (ctx) => installSelected(adapters, ctx)
562
+ run: (ctx) => installSelected(adapters, ctx, tokenOptions)
347
563
  },
348
564
  {
349
565
  name: "uninstall",
@@ -357,7 +573,8 @@ function createAgentCommand(deps = {}) {
357
573
 
358
574
  // src/commands/doctor.ts
359
575
  import {
360
- createSender as defaultCreateSender
576
+ createSender as defaultCreateSender,
577
+ DEFAULT_QUEUE_MAX_ENTRIES as QUEUE_CAP
361
578
  } from "@birdybeep/agent-core";
362
579
  import { claudeCodeAdapter as claudeCodeAdapter2 } from "@birdybeep/claude-code";
363
580
  import { codexAdapter as codexAdapter2 } from "@birdybeep/codex";
@@ -366,7 +583,7 @@ import { cursorAdapter as cursorAdapter2 } from "@birdybeep/cursor";
366
583
  import { opencodeAdapter as opencodeAdapter2 } from "@birdybeep/opencode";
367
584
 
368
585
  // src/config.ts
369
- import { mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "fs";
586
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
370
587
  import { join } from "path";
371
588
  import { birdyBeepConfigDir as birdyBeepConfigDir2 } from "@birdybeep/agent-core";
372
589
  var DEFAULT_API_URL = "https://api.birdybeep.com";
@@ -376,7 +593,7 @@ function cliConfigPath() {
376
593
  }
377
594
  function readCliConfig() {
378
595
  try {
379
- const parsed = JSON.parse(readFileSync(cliConfigPath(), "utf8"));
596
+ const parsed = JSON.parse(readFileSync2(cliConfigPath(), "utf8"));
380
597
  return typeof parsed === "object" && parsed !== null ? parsed : {};
381
598
  } catch {
382
599
  return {};
@@ -405,31 +622,6 @@ function resolveRegistryUrl() {
405
622
  return DEFAULT_REGISTRY_URL;
406
623
  }
407
624
 
408
- // src/diagnostics.ts
409
- import {
410
- getMachineIdentity,
411
- getToken,
412
- LocalEventQueue
413
- } from "@birdybeep/agent-core";
414
- async function gatherIntegrations(adapters) {
415
- return Promise.all(
416
- adapters.map(async (a) => ({
417
- harness: a.id,
418
- displayName: a.displayName,
419
- status: await a.status()
420
- }))
421
- );
422
- }
423
- async function isPaired(tokenOptions = {}) {
424
- return await getToken(tokenOptions) !== null;
425
- }
426
- function localQueueDepth() {
427
- return new LocalEventQueue().size();
428
- }
429
- function machineIdentity() {
430
- return getMachineIdentity();
431
- }
432
-
433
625
  // src/commands/doctor.ts
434
626
  var DEFAULT_ADAPTERS2 = [
435
627
  claudeCodeAdapter2,
@@ -472,6 +664,32 @@ function createDoctorCommand(deps = {}) {
472
664
  remedy: "Run `birdybeep pair` to pair this machine."
473
665
  }
474
666
  );
667
+ const unpaired = unpairedActivity();
668
+ if (unpaired !== null) {
669
+ checks.push({
670
+ name: "Events lost while unpaired",
671
+ ok: false,
672
+ detail: describeUnpairedActivity(unpaired),
673
+ remedy: "Run `birdybeep pair`. Events that fired before pairing are gone \u2014 a first pairing does not replay them."
674
+ });
675
+ }
676
+ if (await cursorBridgeOnly(deps.detectCursor ? { detect: deps.detectCursor } : {})) {
677
+ checks.push({
678
+ name: "Approval beeps from Cursor",
679
+ ok: false,
680
+ 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.",
681
+ 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."
682
+ });
683
+ }
684
+ const filtered = filteredActivity();
685
+ if (filtered !== null) {
686
+ checks.push({
687
+ name: "Local-only events (never notifiable)",
688
+ ok: true,
689
+ detail: describeFilteredActivity(filtered)
690
+ });
691
+ }
692
+ const surfaceGroups = await gatherSurfaces(adapters, deps.surfaceOptions ?? {});
475
693
  for (const adapter of adapters) {
476
694
  const result = await adapter.doctor();
477
695
  for (const c of result.checks) {
@@ -482,14 +700,26 @@ function createDoctorCommand(deps = {}) {
482
700
  ...c.remedy !== void 0 ? { remedy: c.remedy } : {}
483
701
  });
484
702
  }
703
+ const group = surfaceGroups.find((g) => g.harness === adapter.id);
704
+ if (group === void 0) continue;
705
+ for (const state of group.surfaces) {
706
+ const remedy = surfaceRemedy(state, group);
707
+ checks.push({
708
+ name: `${adapter.displayName}: ${describeSurface(state)}`,
709
+ ok: state.coverage !== "uncovered",
710
+ detail: describeSurfaceCoverage(state, group),
711
+ ...remedy !== void 0 ? { remedy } : {}
712
+ });
713
+ }
485
714
  }
486
715
  const depthBefore = localQueueDepth();
487
716
  const drain = await makeSender(apiUrl).drainNow();
488
717
  const depthAfter = localQueueDepth();
718
+ const overflowDropped = localQueueOverflowDrops();
489
719
  checks.push({
490
720
  name: "Local queue",
491
721
  ok: true,
492
- detail: `${depthBefore} queued \u2192 ${drain.delivered} delivered, ${depthAfter} remaining`
722
+ detail: `${depthBefore} queued \u2192 ${drain.delivered} delivered, ${depthAfter} remaining` + (overflowDropped > 0 ? `; ${overflowDropped} dropped by the ${QUEUE_CAP} entry cap` : "")
493
723
  });
494
724
  const reachable = await probeNetwork(apiUrl);
495
725
  checks.push(
@@ -505,7 +735,10 @@ function createDoctorCommand(deps = {}) {
505
735
  ctx.io.result({
506
736
  ok,
507
737
  checks,
508
- queue: { depthBefore, delivered: drain.delivered, depthAfter }
738
+ surfaces: surfaceGroups,
739
+ queue: { depthBefore, delivered: drain.delivered, depthAfter, overflowDropped },
740
+ ...unpaired !== null ? { unpairedActivity: unpaired } : {},
741
+ ...filtered !== null ? { filteredActivity: filtered } : {}
509
742
  });
510
743
  } else {
511
744
  for (const c of checks) {
@@ -530,13 +763,14 @@ import {
530
763
  resolveOnPath
531
764
  } from "@birdybeep/agent-core";
532
765
  import { isClaudeCodeHookPayload, runClaudeHook } from "@birdybeep/claude-code";
533
- import { runCodexHook } from "@birdybeep/codex";
766
+ import { isCodexHookPayload, runCodexHook } from "@birdybeep/codex";
534
767
  import {
535
768
  isCopilotHookEventName,
769
+ isCopilotHookPayload,
536
770
  runCopilotHook
537
771
  } from "@birdybeep/copilot";
538
772
  import { isCursorHookEventName, isCursorHookPayload, runCursorHook } from "@birdybeep/cursor";
539
- import { runOpenCodeHook } from "@birdybeep/opencode";
773
+ import { isOpenCodeEventPayload, runOpenCodeHook } from "@birdybeep/opencode";
540
774
  var RUNNERS = {
541
775
  claude: runClaudeHook,
542
776
  codex: runCodexHook,
@@ -572,17 +806,31 @@ function resolveHookHarness(harness, payload) {
572
806
  return harness === "claude" && isCursorHookPayload(payload) ? "cursor" : harness;
573
807
  }
574
808
  function recognizesPayload(harness, payload) {
575
- if (harness === "claude") return isClaudeCodeHookPayload(payload);
576
- if (harness === "cursor") return isCursorHookEventName(asRecord(payload)["hook_event_name"]);
577
- return true;
809
+ switch (harness) {
810
+ case "claude":
811
+ return isClaudeCodeHookPayload(payload);
812
+ case "cursor":
813
+ return isCursorHookEventName(asRecord2(payload)["hook_event_name"]);
814
+ case "codex":
815
+ return isCodexHookPayload(payload);
816
+ case "opencode":
817
+ return isOpenCodeEventPayload(payload);
818
+ case "copilot":
819
+ return isCopilotHookPayload(payload);
820
+ }
578
821
  }
579
- function asRecord(value) {
822
+ function asRecord2(value) {
580
823
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
581
824
  }
582
- function describeEventName(payload) {
583
- const name = asRecord(payload)["hook_event_name"];
584
- if (typeof name !== "string") return "(absent)";
585
- return JSON.stringify(name.length > 64 ? `${name.slice(0, 63)}\u2026` : name);
825
+ function describeDiscriminator(payload) {
826
+ const record = asRecord2(payload);
827
+ for (const field of ["hook_event_name", "type"]) {
828
+ const value = record[field];
829
+ if (typeof value !== "string") continue;
830
+ const capped = value.length > 64 ? `${value.slice(0, 63)}\u2026` : value;
831
+ return `${field} ${JSON.stringify(capped)}`;
832
+ }
833
+ return "the payload";
586
834
  }
587
835
  function runHookCommand(harness, payload, sender, copilotEventName) {
588
836
  const handler = resolveHookHarness(harness, payload);
@@ -677,10 +925,16 @@ function createHookCommand(deps = {}) {
677
925
  return EXIT.OK;
678
926
  }
679
927
  const copilotEventName = harness === "copilot" && isCopilotHookEventName(ctx.args[1]) ? ctx.args[1] : void 0;
680
- const raw = await withTimeout(
928
+ if (harness === "copilot" && copilotEventName === void 0) {
929
+ ctx.io.errline(
930
+ `birdybeep hook copilot: second argument must be a Copilot hook event name, got ${JSON.stringify(ctx.args[1] ?? "(none)")} \u2014 nothing was sent.`
931
+ );
932
+ return EXIT.USAGE;
933
+ }
934
+ const read = await withTimeout(
681
935
  readHookPayload(ctx.args, readStdin, harness === "copilot"),
682
936
  stdinTimeoutMs,
683
- ""
937
+ null
684
938
  );
685
939
  const notifyStdinFile = process.env[NOTIFY_STDIN_FILE_ENV];
686
940
  if (notifyStdinFile !== void 0 && dirname(notifyStdinFile) === tmpdir() && basename(notifyStdinFile).startsWith("birdybeep-notify-")) {
@@ -689,30 +943,57 @@ function createHookCommand(deps = {}) {
689
943
  } catch {
690
944
  }
691
945
  }
946
+ const drop = (reason, detail) => {
947
+ ctx.io.result({ harness, outcome: "skipped", reason });
948
+ ctx.io.errline(`birdybeep hook ${harness}: ${detail} \u2014 nothing was sent.`);
949
+ return EXIT.ERROR;
950
+ };
951
+ if (read === null) {
952
+ return drop(
953
+ "stdin-timeout",
954
+ `timed out after ${stdinTimeoutMs}ms waiting for the payload on stdin`
955
+ );
956
+ }
957
+ const raw = read;
958
+ if (raw.trim().length === 0) {
959
+ return drop("empty-payload", "the payload was empty");
960
+ }
692
961
  let payload;
693
962
  try {
694
963
  payload = JSON.parse(raw);
695
964
  } catch {
696
- ctx.io.result({ harness, outcome: "skipped" });
697
- return EXIT.OK;
965
+ return drop("invalid-json", `the ${raw.length}-byte payload is not valid JSON`);
698
966
  }
699
- const sender = makeSender(resolveApiUrl());
700
967
  const handler = resolveHookHarness(harness, payload);
968
+ const routedFrom = handler !== harness ? { routedFrom: harness } : {};
969
+ if (!recognizesPayload(handler, payload)) {
970
+ ctx.io.result({
971
+ harness: handler,
972
+ ...routedFrom,
973
+ outcome: "skipped",
974
+ reason: "foreign-payload"
975
+ });
976
+ const article = handler === "opencode" ? "an" : "a";
977
+ ctx.io.errline(
978
+ `birdybeep hook ${harness}: ${describeDiscriminator(payload)} is not ${article} ${handler} hook event \u2014 nothing was sent. Check which tool is running this hook.`
979
+ );
980
+ return EXIT.ERROR;
981
+ }
982
+ const sender = makeSender(resolveApiUrl());
701
983
  const result = await runHookCommand(harness, payload, sender, copilotEventName);
702
984
  ctx.io.result({
703
985
  harness: handler,
704
- ...handler !== harness ? { routedFrom: harness } : {},
986
+ ...routedFrom,
705
987
  ...copilotEventName !== void 0 ? { event: copilotEventName } : {},
706
988
  outcome: result.outcome,
707
989
  eventType: result.eventType,
708
990
  ...result.send?.decision ? { decision: result.send.decision } : {},
709
991
  ...result.send?.status !== void 0 ? { status: result.send.status } : {}
710
992
  });
711
- if (result.outcome === "skipped" && !recognizesPayload(handler, payload)) {
993
+ if (result.outcome === "unpaired") {
712
994
  ctx.io.errline(
713
- `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.`
995
+ "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)."
714
996
  );
715
- return EXIT.ERROR;
716
997
  }
717
998
  return EXIT.OK;
718
999
  }
@@ -773,9 +1054,12 @@ function createUnpairCommand(deps = {}) {
773
1054
  // src/commands/pair.ts
774
1055
  import { closeSync as closeSync2, openSync as openSync2 } from "fs";
775
1056
  import {
1057
+ clearUnpairedNotice,
776
1058
  deriveCodeChallengeS256,
777
1059
  generateCodeVerifier,
778
- getMachineIdentity as getMachineIdentity2,
1060
+ getMachineIdentity as getMachineIdentity3,
1061
+ getToken as getToken3,
1062
+ LocalEventQueue as LocalEventQueue2,
779
1063
  setToken
780
1064
  } from "@birdybeep/agent-core";
781
1065
  import { renderUnicodeCompact } from "uqr";
@@ -854,6 +1138,280 @@ async function pairTokenPoll(apiUrl, deviceCode, fetchImpl, machineFingerprint,
854
1138
  return { status: "error", code: code ?? "unknown", message, retryable: true };
855
1139
  }
856
1140
 
1141
+ // src/commands/setup.ts
1142
+ import { claudeCodeAdapter as claudeCodeAdapter3 } from "@birdybeep/claude-code";
1143
+ import { codexAdapter as codexAdapter3 } from "@birdybeep/codex";
1144
+ import { copilotAdapter as copilotAdapter3 } from "@birdybeep/copilot";
1145
+ import { cursorAdapter as cursorAdapter3 } from "@birdybeep/cursor";
1146
+ import { opencodeAdapter as opencodeAdapter3 } from "@birdybeep/opencode";
1147
+
1148
+ // src/commands/test.ts
1149
+ import { randomUUID } from "crypto";
1150
+ import {
1151
+ createSender as defaultCreateSender3,
1152
+ getMachineIdentity as getMachineIdentity2,
1153
+ normalizeEvent
1154
+ } from "@birdybeep/agent-core";
1155
+ function buildTestEvent(opts = {}) {
1156
+ const machine = getMachineIdentity2();
1157
+ return normalizeEvent(
1158
+ {
1159
+ event_type: "test",
1160
+ status: "running",
1161
+ harness: "claude_code",
1162
+ // schema requires a harness; the "test" type distinguishes it
1163
+ // Unique per run: a repeat `birdybeep test` inside the backend's dedupe window must
1164
+ // still beep — a constant id made the second test silently "deduped" (9fh).
1165
+ source_session_id: `birdybeep-cli-test-${randomUUID()}`,
1166
+ machine: { label: machine.label, os: machine.os },
1167
+ workspace: { cwd: process.cwd() },
1168
+ title: "BirdyBeep test event",
1169
+ body: "If you can see this, your machine is wired up correctly.",
1170
+ metadata: { test: true }
1171
+ },
1172
+ opts
1173
+ );
1174
+ }
1175
+ function createTestCommand(deps = {}) {
1176
+ const makeSender = deps.createSender ?? ((baseUrl) => defaultCreateSender3(
1177
+ deps.tokenOptions ? { baseUrl, tokenOptions: deps.tokenOptions } : { baseUrl }
1178
+ ));
1179
+ return {
1180
+ name: "test",
1181
+ summary: "Send a test event end-to-end",
1182
+ usage: "birdybeep test [--json]",
1183
+ run: async (ctx) => {
1184
+ const event = buildTestEvent();
1185
+ const result = await makeSender(resolveApiUrl()).send(event);
1186
+ if (ctx.flags.json) {
1187
+ ctx.io.result({
1188
+ outcome: result.outcome,
1189
+ ...result.status ? { status: result.status } : {},
1190
+ ...result.decision ? { decision: result.decision } : {}
1191
+ });
1192
+ } else if (result.outcome === "delivered") {
1193
+ if (result.decision === "notified" || result.decision === void 0) {
1194
+ ctx.io.line("\u2713 Test event delivered \u2014 check your phone for a test Beep.");
1195
+ } else if (result.decision === "suppressed") {
1196
+ ctx.io.line(
1197
+ "\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`."
1198
+ );
1199
+ } else if (result.decision === "deduped") {
1200
+ ctx.io.line(
1201
+ "\u26A0 The backend accepted the test event but folded it into a recent duplicate \u2014 wait ~30s and run `birdybeep test` again."
1202
+ );
1203
+ } else {
1204
+ ctx.io.line(
1205
+ `\u26A0 The backend accepted the test event but decided "${result.decision}" \u2014 no push was sent. Run \`birdybeep doctor\`.`
1206
+ );
1207
+ }
1208
+ } else if (result.outcome === "unpaired") {
1209
+ ctx.io.line(
1210
+ "\u2717 NOT PAIRED \u2014 this machine has no BirdyBeep machine token, so nothing was sent (and nothing was queued). Run `birdybeep pair`."
1211
+ );
1212
+ } else if (result.outcome === "queued") {
1213
+ ctx.io.line("\u2022 Offline \u2014 test event queued; it will deliver when you reconnect.");
1214
+ } else {
1215
+ ctx.io.line("\u2717 Test event was rejected by the backend. Run `birdybeep doctor`.");
1216
+ }
1217
+ return result.outcome === "dropped" || result.outcome === "unpaired" ? EXIT.ERROR : EXIT.OK;
1218
+ }
1219
+ };
1220
+ }
1221
+
1222
+ // src/commands/setup.ts
1223
+ var SETUP_ADAPTERS = [
1224
+ claudeCodeAdapter3,
1225
+ codexAdapter3,
1226
+ opencodeAdapter3,
1227
+ cursorAdapter3,
1228
+ copilotAdapter3
1229
+ ];
1230
+ function failedSetupReport(error) {
1231
+ return {
1232
+ harnesses: [],
1233
+ counts: { installed: 0, needsYou: 0, notInstalled: 0, failed: 0 },
1234
+ error,
1235
+ ok: false
1236
+ };
1237
+ }
1238
+ var PENDING_STATUSES = /* @__PURE__ */ new Set([
1239
+ "needs_trust",
1240
+ "needs_restart"
1241
+ ]);
1242
+ async function installDetected(adapters) {
1243
+ const installs = [];
1244
+ for (const adapter of adapters) {
1245
+ try {
1246
+ const detection = await adapter.detect();
1247
+ if (!detection.detected) {
1248
+ installs.push({ adapter, detected: false });
1249
+ continue;
1250
+ }
1251
+ installs.push({ adapter, detected: true, result: await adapter.install() });
1252
+ } catch (err) {
1253
+ installs.push({
1254
+ adapter,
1255
+ detected: true,
1256
+ error: err instanceof Error ? err.message : String(err)
1257
+ });
1258
+ }
1259
+ }
1260
+ return installs;
1261
+ }
1262
+ function rowState(state, group, status) {
1263
+ if (status === "error" || group.status === "error") return "failed";
1264
+ if (status !== void 0 && PENDING_STATUSES.has(status)) return "needs you";
1265
+ if (state.coverage === "active") return "beeping";
1266
+ if (state.coverage === "wired") return "ready";
1267
+ return "not covered";
1268
+ }
1269
+ function buildHarnessReports(installs, groups) {
1270
+ return installs.map((install) => {
1271
+ const { adapter } = install;
1272
+ const base4 = {
1273
+ harness: adapter.id,
1274
+ displayName: adapter.displayName,
1275
+ detected: install.detected,
1276
+ ...install.result !== void 0 ? {
1277
+ status: install.result.status,
1278
+ changedFiles: install.result.changedFiles,
1279
+ backupFiles: install.result.backupFiles
1280
+ } : {},
1281
+ ...install.error !== void 0 ? { error: install.error } : {}
1282
+ };
1283
+ if (install.error !== void 0) {
1284
+ return {
1285
+ ...base4,
1286
+ actions: [
1287
+ `${adapter.displayName} could not be set up: ${install.error}`,
1288
+ `Run \`birdybeep agent install ${installTarget2(adapter.id)}\` to retry it on its own.`
1289
+ ],
1290
+ rows: [{ harness: adapter.id, displayName: adapter.displayName, state: "failed" }]
1291
+ };
1292
+ }
1293
+ if (!install.detected) {
1294
+ return {
1295
+ ...base4,
1296
+ actions: [],
1297
+ rows: [
1298
+ {
1299
+ harness: adapter.id,
1300
+ displayName: adapter.displayName,
1301
+ state: "not installed"
1302
+ }
1303
+ ]
1304
+ };
1305
+ }
1306
+ const group = groups.find((g) => g.harness === adapter.id);
1307
+ const status = install.result?.status;
1308
+ const surfaces = group?.surfaces ?? [];
1309
+ const rows = group === void 0 || surfaces.length === 0 ? [
1310
+ {
1311
+ harness: adapter.id,
1312
+ displayName: adapter.displayName,
1313
+ state: status !== void 0 && PENDING_STATUSES.has(status) ? "needs you" : "ready"
1314
+ }
1315
+ ] : surfaces.map((state) => {
1316
+ const graded = rowState(state, group, status);
1317
+ 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);
1318
+ return {
1319
+ harness: adapter.id,
1320
+ displayName: adapter.displayName,
1321
+ build: describeSurface(state),
1322
+ kind: state.surface.kind,
1323
+ state: graded,
1324
+ ...remedy !== void 0 ? { remedy } : {}
1325
+ };
1326
+ });
1327
+ return { ...base4, actions: [...install.result?.requiredActions ?? []], rows };
1328
+ });
1329
+ }
1330
+ function pad(text, width) {
1331
+ return text.length >= width ? text : text + " ".repeat(width - text.length);
1332
+ }
1333
+ var MARKS = {
1334
+ beeping: "\u2713",
1335
+ ready: "\u2713",
1336
+ "needs you": "!",
1337
+ "not covered": "\u2717",
1338
+ "not installed": "\u2013",
1339
+ failed: "\u2717"
1340
+ };
1341
+ function renderCoverageTable(reports) {
1342
+ const rows = reports.flatMap((r) => r.rows);
1343
+ const nameWidth = Math.max(7, ...rows.map((r) => r.displayName.length));
1344
+ const buildWidth = Math.max(5, ...rows.map((r) => (r.build ?? "\u2014").length));
1345
+ const lines = ["coverage", ` ${pad("harness", nameWidth)} ${pad("build", buildWidth)} state`];
1346
+ for (const report of reports) {
1347
+ for (const row of report.rows) {
1348
+ lines.push(
1349
+ `${MARKS[row.state]} ${pad(row.displayName, nameWidth)} ${pad(row.build ?? "\u2014", buildWidth)} ${row.state}`
1350
+ );
1351
+ if (row.remedy !== void 0) lines.push(` \u2192 ${row.remedy}`);
1352
+ }
1353
+ for (const action of report.actions) lines.push(` \u2192 ${action}`);
1354
+ }
1355
+ return lines;
1356
+ }
1357
+ function describeMissing(reports) {
1358
+ const missing = reports.filter((r) => !r.detected && r.error === void 0);
1359
+ if (missing.length === 0) return [];
1360
+ const names = missing.map((r) => r.displayName);
1361
+ if (missing.length === reports.length) {
1362
+ return [
1363
+ "No supported coding agent is installed on this machine, so there was nothing to wire up.",
1364
+ `Install one of ${names.join(", ")}, then run \`birdybeep setup\` again \u2014 pairing is already done, so it picks up from here.`
1365
+ ];
1366
+ }
1367
+ return [
1368
+ `Not installed: ${names.join(", ")}. Install any of them, then run \`birdybeep setup\` again to wire it up.`
1369
+ ];
1370
+ }
1371
+ async function runHarnessSetup(ctx, options, deps = {}) {
1372
+ const adapters = deps.adapters ?? [...SETUP_ADAPTERS];
1373
+ const installs = await installDetected(adapters);
1374
+ const groups = await gatherSurfaces(adapters, deps.surfaceOptions ?? {});
1375
+ const reports = buildHarnessReports(installs, groups);
1376
+ ctx.io.line("");
1377
+ for (const line of renderCoverageTable(reports)) ctx.io.line(line);
1378
+ const missing = describeMissing(reports);
1379
+ if (missing.length > 0) {
1380
+ ctx.io.line("");
1381
+ for (const line of missing) ctx.io.line(line);
1382
+ }
1383
+ const counts = {
1384
+ installed: reports.filter((r) => r.detected && r.error === void 0).length,
1385
+ needsYou: reports.filter((r) => r.rows.some((row) => row.state === "needs you")).length,
1386
+ notInstalled: reports.filter((r) => !r.detected && r.error === void 0).length,
1387
+ // A row that graded `failed` counts too: an adapter that returned status "error" never threw,
1388
+ // so counting only thrown errors would report a clean run over a harness that is broken.
1389
+ failed: reports.filter((r) => r.error !== void 0 || r.rows.some((x) => x.state === "failed")).length
1390
+ };
1391
+ let beep;
1392
+ let beepOk = true;
1393
+ if (options.sendTest) {
1394
+ ctx.io.line("");
1395
+ const command = createTestCommand({
1396
+ ...deps.createSender !== void 0 ? { createSender: deps.createSender } : {},
1397
+ ...deps.tokenOptions !== void 0 ? { tokenOptions: deps.tokenOptions } : {}
1398
+ });
1399
+ const beepIo = {
1400
+ ...ctx.io,
1401
+ result: (value) => {
1402
+ beep = value;
1403
+ }
1404
+ };
1405
+ beepOk = await command.run?.({ args: [], flags: ctx.flags, io: beepIo }) === 0;
1406
+ }
1407
+ return {
1408
+ harnesses: reports,
1409
+ counts,
1410
+ ...beep !== void 0 ? { beep } : {},
1411
+ ok: counts.failed === 0 && beepOk
1412
+ };
1413
+ }
1414
+
857
1415
  // src/commands/pair.ts
858
1416
  var DEFAULT_POLL_INTERVAL_MS = 2e3;
859
1417
  var HEARTBEAT_MS = 15e3;
@@ -861,11 +1419,15 @@ function renderQrMatrix(qrPayload) {
861
1419
  return renderUnicodeCompact(qrPayload, { border: 2 });
862
1420
  }
863
1421
  function parsePairFlags(args) {
864
- const flags = { yes: false };
1422
+ const flags = { yes: false, noInstall: false, noTest: false };
865
1423
  for (let i = 0; i < args.length; i += 1) {
866
1424
  const token = args[i] ?? "";
867
1425
  if (token === "--yes" || token === "-y") {
868
1426
  flags.yes = true;
1427
+ } else if (token === "--no-install") {
1428
+ flags.noInstall = true;
1429
+ } else if (token === "--no-test") {
1430
+ flags.noTest = true;
869
1431
  } else if (token === "--expect-email" || token.startsWith("--expect-email=")) {
870
1432
  const inline = token.startsWith("--expect-email=") ? token.slice("--expect-email=".length) : void 0;
871
1433
  const value = inline ?? args[++i];
@@ -985,7 +1547,18 @@ async function promptForAnswer(question, on) {
985
1547
  input.once?.("error", () => done(""));
986
1548
  });
987
1549
  }
988
- function createPairCommand(deps = {}) {
1550
+ async function runSetupChain(ctx, deps, flags) {
1551
+ try {
1552
+ return await runHarnessSetup(ctx, { sendTest: !flags.noTest }, deps);
1553
+ } catch (err) {
1554
+ const message = err instanceof Error ? err.message : String(err);
1555
+ ctx.io.errline(
1556
+ `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\`.`
1557
+ );
1558
+ return failedSetupReport(message);
1559
+ }
1560
+ }
1561
+ function createPairingCommand(verb, deps = {}) {
989
1562
  const fetchImpl = deps.fetchImpl ?? fetch;
990
1563
  const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
991
1564
  const clock = deps.now ?? (() => Date.now());
@@ -998,9 +1571,10 @@ function createPairCommand(deps = {}) {
998
1571
  return typeof pinned === "string" && pinned.trim().length > 0 ? pinned : void 0;
999
1572
  });
1000
1573
  return {
1001
- name: "pair",
1002
- summary: "Pair this machine with your BirdyBeep account (QR or manual)",
1003
- usage: "birdybeep pair [--yes] [--expect-email <addr>] [--json]",
1574
+ name: verb.name,
1575
+ summary: verb.summary,
1576
+ usage: verb.usage,
1577
+ ...verb.gettingStarted !== void 0 ? { gettingStarted: verb.gettingStarted } : {},
1004
1578
  options: [
1005
1579
  {
1006
1580
  flag: "--yes",
@@ -1011,16 +1585,41 @@ function createPairCommand(deps = {}) {
1011
1585
  flag: "--expect-email",
1012
1586
  value: "<addr>",
1013
1587
  summary: "Only trust the pairing if this account approved it (else fail)"
1588
+ },
1589
+ {
1590
+ flag: "--no-install",
1591
+ summary: "Stop after pairing \u2014 don't detect or wire up any coding agent"
1592
+ },
1593
+ {
1594
+ flag: "--no-test",
1595
+ summary: "Don't send the test Beep at the end"
1014
1596
  }
1015
1597
  ],
1016
1598
  run: async (ctx) => {
1017
1599
  const pairFlags = parsePairFlags(ctx.args);
1018
1600
  if (pairFlags.error !== void 0) {
1019
- ctx.io.errline(`birdybeep pair: ${pairFlags.error}.`);
1601
+ ctx.io.errline(`birdybeep ${verb.name}: ${pairFlags.error}.`);
1020
1602
  return EXIT.USAGE;
1021
1603
  }
1604
+ const chain = deps.setup === false || pairFlags.noInstall ? void 0 : deps.setup ?? {};
1605
+ const setupDeps = {
1606
+ ...deps.tokenOptions !== void 0 ? { tokenOptions: deps.tokenOptions } : {},
1607
+ ...chain
1608
+ };
1609
+ if (verb.skipWhenPaired && await getToken3(deps.tokenOptions ?? {}) !== null) {
1610
+ ctx.io.line(
1611
+ chain !== void 0 ? "\u2713 Already paired \u2014 checking which coding agents are wired up." : "\u2713 Already paired. Nothing else to do with --no-install."
1612
+ );
1613
+ const report2 = chain !== void 0 ? await runSetupChain(ctx, setupDeps, pairFlags) : void 0;
1614
+ ctx.io.result({
1615
+ paired: true,
1616
+ alreadyPaired: true,
1617
+ ...report2 !== void 0 ? { setup: report2 } : {}
1618
+ });
1619
+ return report2 !== void 0 && !report2.ok ? EXIT.ERROR : EXIT.OK;
1620
+ }
1022
1621
  const apiUrl = resolveApiUrl();
1023
- const identity = getMachineIdentity2();
1622
+ const identity = getMachineIdentity3();
1024
1623
  const codeVerifier = generateCodeVerifier();
1025
1624
  const codeChallenge = deriveCodeChallengeS256(codeVerifier);
1026
1625
  const start = await pairStart(
@@ -1120,19 +1719,50 @@ function createPairCommand(deps = {}) {
1120
1719
  }
1121
1720
  await setToken(paired.machineToken, deps.tokenOptions ?? {});
1122
1721
  writeCliConfig({ apiUrl });
1722
+ const discarded = new LocalEventQueue2().discardBefore(clock());
1723
+ clearUnpairedNotice();
1123
1724
  const humanSuffix = approvedBy !== void 0 ? ` to ${approvedBy}` : "";
1124
- ctx.io.emit(`\u2713 Paired${humanSuffix}. Run \`birdybeep test\` to send a test Beep.`, {
1725
+ const discardedSuffix = discarded > 0 ? ` Discarded ${discarded} event(s) queued before pairing \u2014 you won't be beeped about them.` : "";
1726
+ const nextStep = chain === void 0 ? " Run `birdybeep setup` to wire up your coding agents." : "";
1727
+ ctx.io.line(`\u2713 Paired${humanSuffix}.${nextStep}${discardedSuffix}`);
1728
+ const report = chain !== void 0 ? await runSetupChain(ctx, setupDeps, pairFlags) : void 0;
1729
+ ctx.io.result({
1125
1730
  paired: true,
1126
1731
  machineId: paired.machineId,
1127
- ...approvedBy !== void 0 ? { approvedByEmail: approvedBy } : {}
1732
+ discardedPrePairingEvents: discarded,
1733
+ ...approvedBy !== void 0 ? { approvedByEmail: approvedBy } : {},
1734
+ ...report !== void 0 ? { setup: report } : {}
1128
1735
  });
1129
- return EXIT.OK;
1736
+ return report !== void 0 && !report.ok ? EXIT.ERROR : EXIT.OK;
1130
1737
  }
1131
1738
  };
1132
1739
  }
1740
+ function createPairCommand(deps = {}) {
1741
+ return createPairingCommand(
1742
+ {
1743
+ name: "pair",
1744
+ summary: "Pair this machine and wire up every coding agent on it",
1745
+ usage: "birdybeep pair [--yes] [--expect-email <addr>] [--no-install] [--no-test] [--json]",
1746
+ skipWhenPaired: false
1747
+ },
1748
+ deps
1749
+ );
1750
+ }
1751
+ function createSetupCommand(deps = {}) {
1752
+ return createPairingCommand(
1753
+ {
1754
+ name: "setup",
1755
+ summary: "Set up BirdyBeep here: pair, wire up every coding agent, test",
1756
+ usage: "birdybeep setup [--yes] [--expect-email <addr>] [--no-install] [--no-test] [--json]",
1757
+ gettingStarted: "Pair this machine, wire up every coding agent it finds, and send a test Beep.",
1758
+ skipWhenPaired: true
1759
+ },
1760
+ deps
1761
+ );
1762
+ }
1133
1763
 
1134
1764
  // src/commands/queue.ts
1135
- import { LocalEventQueue as LocalEventQueue2 } from "@birdybeep/agent-core";
1765
+ import { LocalEventQueue as LocalEventQueue3 } from "@birdybeep/agent-core";
1136
1766
  function createQueueCommand() {
1137
1767
  return {
1138
1768
  name: "queue",
@@ -1144,7 +1774,7 @@ function createQueueCommand() {
1144
1774
  summary: "Clear the local offline event queue (debug)",
1145
1775
  usage: "birdybeep queue clear",
1146
1776
  run: (ctx) => {
1147
- const cleared = new LocalEventQueue2().clear();
1777
+ const cleared = new LocalEventQueue3().clear();
1148
1778
  ctx.io.emit(`Cleared ${cleared} queued event(s).`, { cleared });
1149
1779
  return EXIT.OK;
1150
1780
  }
@@ -1156,20 +1786,20 @@ function createQueueCommand() {
1156
1786
  // src/commands/report-status.ts
1157
1787
  import {
1158
1788
  errorEnvelopeSchema as errorEnvelopeSchema2,
1159
- getToken as getToken3,
1789
+ getToken as getToken4,
1160
1790
  integrationStatusResponseSchema
1161
1791
  } from "@birdybeep/agent-core";
1162
- import { CLAUDE_CODE_ADAPTER_VERSION, claudeCodeAdapter as claudeCodeAdapter3 } from "@birdybeep/claude-code";
1163
- import { CODEX_ADAPTER_VERSION, codexAdapter as codexAdapter3 } from "@birdybeep/codex";
1164
- import { COPILOT_ADAPTER_VERSION, copilotAdapter as copilotAdapter3 } from "@birdybeep/copilot";
1165
- import { CURSOR_ADAPTER_VERSION, cursorAdapter as cursorAdapter3 } from "@birdybeep/cursor";
1166
- import { OPENCODE_ADAPTER_VERSION, opencodeAdapter as opencodeAdapter3 } from "@birdybeep/opencode";
1792
+ import { CLAUDE_CODE_ADAPTER_VERSION, claudeCodeAdapter as claudeCodeAdapter4 } from "@birdybeep/claude-code";
1793
+ import { CODEX_ADAPTER_VERSION, codexAdapter as codexAdapter4 } from "@birdybeep/codex";
1794
+ import { COPILOT_ADAPTER_VERSION, copilotAdapter as copilotAdapter4 } from "@birdybeep/copilot";
1795
+ import { CURSOR_ADAPTER_VERSION, cursorAdapter as cursorAdapter4 } from "@birdybeep/cursor";
1796
+ import { OPENCODE_ADAPTER_VERSION, opencodeAdapter as opencodeAdapter4 } from "@birdybeep/opencode";
1167
1797
  var DEFAULT_ADAPTERS3 = [
1168
- claudeCodeAdapter3,
1169
- codexAdapter3,
1170
- opencodeAdapter3,
1171
- cursorAdapter3,
1172
- copilotAdapter3
1798
+ claudeCodeAdapter4,
1799
+ codexAdapter4,
1800
+ opencodeAdapter4,
1801
+ cursorAdapter4,
1802
+ copilotAdapter4
1173
1803
  ];
1174
1804
  var ADAPTER_VERSIONS = {
1175
1805
  claude_code: CLAUDE_CODE_ADAPTER_VERSION,
@@ -1199,7 +1829,7 @@ function createReportStatusCommand(deps = {}) {
1199
1829
  summary: "Internal: report integration status to the backend",
1200
1830
  usage: "birdybeep report-status [--json]",
1201
1831
  run: async (ctx) => {
1202
- const token = await getToken3(deps.tokenOptions ?? {});
1832
+ const token = await getToken4(deps.tokenOptions ?? {});
1203
1833
  if (token === null) {
1204
1834
  ctx.io.errline("No machine token \u2014 run `birdybeep pair` first.");
1205
1835
  return EXIT.ERROR;
@@ -1262,23 +1892,23 @@ function createReportStatusCommand(deps = {}) {
1262
1892
 
1263
1893
  // src/commands/status.ts
1264
1894
  import {
1265
- createSender as defaultCreateSender3
1895
+ createSender as defaultCreateSender4
1266
1896
  } from "@birdybeep/agent-core";
1267
- import { claudeCodeAdapter as claudeCodeAdapter4 } from "@birdybeep/claude-code";
1268
- import { codexAdapter as codexAdapter4 } from "@birdybeep/codex";
1269
- import { copilotAdapter as copilotAdapter4 } from "@birdybeep/copilot";
1270
- import { cursorAdapter as cursorAdapter4 } from "@birdybeep/cursor";
1271
- import { opencodeAdapter as opencodeAdapter4 } from "@birdybeep/opencode";
1897
+ import { claudeCodeAdapter as claudeCodeAdapter5 } from "@birdybeep/claude-code";
1898
+ import { codexAdapter as codexAdapter5 } from "@birdybeep/codex";
1899
+ import { copilotAdapter as copilotAdapter5 } from "@birdybeep/copilot";
1900
+ import { cursorAdapter as cursorAdapter5 } from "@birdybeep/cursor";
1901
+ import { opencodeAdapter as opencodeAdapter5 } from "@birdybeep/opencode";
1272
1902
  var DEFAULT_ADAPTERS4 = [
1273
- claudeCodeAdapter4,
1274
- codexAdapter4,
1275
- opencodeAdapter4,
1276
- cursorAdapter4,
1277
- copilotAdapter4
1903
+ claudeCodeAdapter5,
1904
+ codexAdapter5,
1905
+ opencodeAdapter5,
1906
+ cursorAdapter5,
1907
+ copilotAdapter5
1278
1908
  ];
1279
1909
  function createStatusCommand(deps = {}) {
1280
1910
  const adapters = deps.adapters ?? DEFAULT_ADAPTERS4;
1281
- const makeSender = deps.createSender ?? ((baseUrl) => defaultCreateSender3(
1911
+ const makeSender = deps.createSender ?? ((baseUrl) => defaultCreateSender4(
1282
1912
  deps.tokenOptions ? { baseUrl, tokenOptions: deps.tokenOptions } : { baseUrl }
1283
1913
  ));
1284
1914
  return {
@@ -1289,14 +1919,21 @@ function createStatusCommand(deps = {}) {
1289
1919
  const machine = machineIdentity();
1290
1920
  const paired = await isPaired(deps.tokenOptions ?? {});
1291
1921
  const integrations = await gatherIntegrations(adapters);
1922
+ const surfaces = await gatherSurfaces(adapters, deps.surfaceOptions ?? {});
1292
1923
  const depthBefore = localQueueDepth();
1924
+ const unpaired = unpairedActivity();
1925
+ const filtered = filteredActivity();
1293
1926
  const drain = await makeSender(resolveApiUrl()).drainNow();
1294
1927
  const depthAfter = localQueueDepth();
1928
+ const overflowDropped = localQueueOverflowDrops();
1295
1929
  const report = {
1296
1930
  machine,
1297
1931
  paired,
1298
1932
  integrations,
1299
- queue: { depthBefore, delivered: drain.delivered, depthAfter }
1933
+ surfaces,
1934
+ queue: { depthBefore, delivered: drain.delivered, depthAfter, overflowDropped },
1935
+ ...unpaired !== null ? { unpairedActivity: unpaired } : {},
1936
+ ...filtered !== null ? { filteredActivity: filtered } : {}
1300
1937
  };
1301
1938
  if (ctx.flags.json) {
1302
1939
  ctx.io.result(report);
@@ -1304,89 +1941,29 @@ function createStatusCommand(deps = {}) {
1304
1941
  ctx.io.line(`Machine: ${machine.label} (${machine.os})`);
1305
1942
  ctx.io.line(paired ? "Paired: yes" : "Paired: no \u2014 run `birdybeep pair`");
1306
1943
  ctx.io.line("Integrations:");
1307
- for (const i of integrations) ctx.io.line(` ${i.displayName}: ${i.status}`);
1944
+ for (const i of integrations) {
1945
+ ctx.io.line(` ${i.displayName}: ${i.status}`);
1946
+ const group = surfaces.find((g) => g.harness === i.harness);
1947
+ for (const state of group?.surfaces ?? []) {
1948
+ const mark = state.coverage === "active" ? "\u2713" : state.coverage === "wired" ? "\xB7" : "\u2717";
1949
+ ctx.io.line(` ${mark} ${describeSurface(state)} \u2014 ${state.coverage}`);
1950
+ }
1951
+ }
1308
1952
  ctx.io.line(
1309
- `Queue: ${depthBefore} queued \u2192 ${drain.delivered} delivered, ${depthAfter} remaining`
1953
+ `Queue: ${depthBefore} queued \u2192 ${drain.delivered} delivered, ${depthAfter} remaining` + (overflowDropped > 0 ? `, ${overflowDropped} dropped by the queue cap` : "")
1310
1954
  );
1955
+ if (unpaired !== null) ctx.io.line(`\u26A0 Lost: ${describeUnpairedActivity(unpaired)}`);
1956
+ if (filtered !== null) ctx.io.line(`Local: ${describeFilteredActivity(filtered)}`);
1311
1957
  }
1312
1958
  return paired ? EXIT.OK : EXIT.ERROR;
1313
1959
  }
1314
1960
  };
1315
1961
  }
1316
1962
 
1317
- // src/commands/test.ts
1318
- import { randomUUID } from "crypto";
1319
- import {
1320
- createSender as defaultCreateSender4,
1321
- getMachineIdentity as getMachineIdentity3,
1322
- normalizeEvent
1323
- } from "@birdybeep/agent-core";
1324
- function buildTestEvent(opts = {}) {
1325
- const machine = getMachineIdentity3();
1326
- return normalizeEvent(
1327
- {
1328
- event_type: "test",
1329
- status: "running",
1330
- harness: "claude_code",
1331
- // schema requires a harness; the "test" type distinguishes it
1332
- // Unique per run: a repeat `birdybeep test` inside the backend's dedupe window must
1333
- // still beep — a constant id made the second test silently "deduped" (9fh).
1334
- source_session_id: `birdybeep-cli-test-${randomUUID()}`,
1335
- machine: { label: machine.label, os: machine.os },
1336
- workspace: { cwd: process.cwd() },
1337
- title: "BirdyBeep test event",
1338
- body: "If you can see this, your machine is wired up correctly.",
1339
- metadata: { test: true }
1340
- },
1341
- opts
1342
- );
1343
- }
1344
- function createTestCommand(deps = {}) {
1345
- const makeSender = deps.createSender ?? ((baseUrl) => defaultCreateSender4(
1346
- deps.tokenOptions ? { baseUrl, tokenOptions: deps.tokenOptions } : { baseUrl }
1347
- ));
1348
- return {
1349
- name: "test",
1350
- summary: "Send a test event end-to-end",
1351
- usage: "birdybeep test [--json]",
1352
- run: async (ctx) => {
1353
- const event = buildTestEvent();
1354
- const result = await makeSender(resolveApiUrl()).send(event);
1355
- if (ctx.flags.json) {
1356
- ctx.io.result({
1357
- outcome: result.outcome,
1358
- ...result.status ? { status: result.status } : {},
1359
- ...result.decision ? { decision: result.decision } : {}
1360
- });
1361
- } else if (result.outcome === "delivered") {
1362
- if (result.decision === "notified" || result.decision === void 0) {
1363
- ctx.io.line("\u2713 Test event delivered \u2014 check your phone for a test Beep.");
1364
- } else if (result.decision === "suppressed") {
1365
- ctx.io.line(
1366
- "\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`."
1367
- );
1368
- } else if (result.decision === "deduped") {
1369
- ctx.io.line(
1370
- "\u26A0 The backend accepted the test event but folded it into a recent duplicate \u2014 wait ~30s and run `birdybeep test` again."
1371
- );
1372
- } else {
1373
- ctx.io.line(
1374
- `\u26A0 The backend accepted the test event but decided "${result.decision}" \u2014 no push was sent. Run \`birdybeep doctor\`.`
1375
- );
1376
- }
1377
- } else if (result.outcome === "queued") {
1378
- ctx.io.line("\u2022 Offline \u2014 test event queued; it will deliver when you reconnect.");
1379
- } else {
1380
- ctx.io.line("\u2717 Test event was rejected by the backend. Run `birdybeep doctor`.");
1381
- }
1382
- return result.outcome === "dropped" ? EXIT.ERROR : EXIT.OK;
1383
- }
1384
- };
1385
- }
1386
-
1387
1963
  // src/commands.ts
1388
1964
  function buildCommands() {
1389
1965
  return [
1966
+ createSetupCommand(),
1390
1967
  createPairCommand(),
1391
1968
  createLogoutCommand(),
1392
1969
  createUnpairCommand(),
@@ -1401,7 +1978,7 @@ function buildCommands() {
1401
1978
  }
1402
1979
 
1403
1980
  // src/update-check.ts
1404
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
1981
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
1405
1982
  import { join as join3 } from "path";
1406
1983
  import { birdyBeepConfigDir as birdyBeepConfigDir3 } from "@birdybeep/agent-core";
1407
1984
  var PACKAGE_NAME = "@birdybeep/cli";
@@ -1461,7 +2038,7 @@ function updateCachePath() {
1461
2038
  }
1462
2039
  function readUpdateCache() {
1463
2040
  try {
1464
- const parsed = JSON.parse(readFileSync2(updateCachePath(), "utf8"));
2041
+ const parsed = JSON.parse(readFileSync3(updateCachePath(), "utf8"));
1465
2042
  if (typeof parsed !== "object" || parsed === null) return null;
1466
2043
  const { checkedAt, latest } = parsed;
1467
2044
  if (typeof checkedAt !== "number") return null;
@@ -1561,4 +2138,4 @@ export {
1561
2138
  buildCommands,
1562
2139
  runCli
1563
2140
  };
1564
- //# sourceMappingURL=chunk-ZYFMLHY4.js.map
2141
+ //# sourceMappingURL=chunk-BY5MNQE3.js.map