@birdybeep/cli 0.3.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.3.0".length > 0 ? "0.3.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) {
@@ -529,14 +762,15 @@ import {
529
762
  createSender as defaultCreateSender2,
530
763
  resolveOnPath
531
764
  } from "@birdybeep/agent-core";
532
- import { runClaudeHook } from "@birdybeep/claude-code";
533
- import { runCodexHook } from "@birdybeep/codex";
765
+ import { isClaudeCodeHookPayload, runClaudeHook } from "@birdybeep/claude-code";
766
+ import { isCodexHookPayload, runCodexHook } from "@birdybeep/codex";
534
767
  import {
535
768
  isCopilotHookEventName,
769
+ isCopilotHookPayload,
536
770
  runCopilotHook
537
771
  } from "@birdybeep/copilot";
538
- import { runCursorHook } from "@birdybeep/cursor";
539
- import { runOpenCodeHook } from "@birdybeep/opencode";
772
+ import { isCursorHookEventName, isCursorHookPayload, runCursorHook } from "@birdybeep/cursor";
773
+ import { isOpenCodeEventPayload, runOpenCodeHook } from "@birdybeep/opencode";
540
774
  var RUNNERS = {
541
775
  claude: runClaudeHook,
542
776
  codex: runCodexHook,
@@ -568,12 +802,43 @@ function withTimeout(promise, ms, fallback) {
568
802
  function isHarnessName(value) {
569
803
  return value === "claude" || value === "codex" || value === "opencode" || value === "cursor" || value === "copilot";
570
804
  }
805
+ function resolveHookHarness(harness, payload) {
806
+ return harness === "claude" && isCursorHookPayload(payload) ? "cursor" : harness;
807
+ }
808
+ function recognizesPayload(harness, payload) {
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
+ }
821
+ }
822
+ function asRecord2(value) {
823
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
824
+ }
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";
834
+ }
571
835
  function runHookCommand(harness, payload, sender, copilotEventName) {
572
- if (harness === "copilot") {
836
+ const handler = resolveHookHarness(harness, payload);
837
+ if (handler === "copilot") {
573
838
  if (copilotEventName === void 0) return Promise.resolve({ outcome: "skipped" });
574
839
  return runCopilotHook(copilotEventName, payload, { sender });
575
840
  }
576
- return RUNNERS[harness](payload, { sender });
841
+ return RUNNERS[handler](payload, { sender });
577
842
  }
578
843
  function readStdinDefault() {
579
844
  return new Promise((resolve) => {
@@ -660,10 +925,16 @@ function createHookCommand(deps = {}) {
660
925
  return EXIT.OK;
661
926
  }
662
927
  const copilotEventName = harness === "copilot" && isCopilotHookEventName(ctx.args[1]) ? ctx.args[1] : void 0;
663
- 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(
664
935
  readHookPayload(ctx.args, readStdin, harness === "copilot"),
665
936
  stdinTimeoutMs,
666
- ""
937
+ null
667
938
  );
668
939
  const notifyStdinFile = process.env[NOTIFY_STDIN_FILE_ENV];
669
940
  if (notifyStdinFile !== void 0 && dirname(notifyStdinFile) === tmpdir() && basename(notifyStdinFile).startsWith("birdybeep-notify-")) {
@@ -672,23 +943,58 @@ function createHookCommand(deps = {}) {
672
943
  } catch {
673
944
  }
674
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
+ }
675
961
  let payload;
676
962
  try {
677
963
  payload = JSON.parse(raw);
678
964
  } catch {
679
- ctx.io.result({ harness, outcome: "skipped" });
680
- return EXIT.OK;
965
+ return drop("invalid-json", `the ${raw.length}-byte payload is not valid JSON`);
966
+ }
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;
681
981
  }
682
982
  const sender = makeSender(resolveApiUrl());
683
983
  const result = await runHookCommand(harness, payload, sender, copilotEventName);
684
984
  ctx.io.result({
685
- harness,
985
+ harness: handler,
986
+ ...routedFrom,
686
987
  ...copilotEventName !== void 0 ? { event: copilotEventName } : {},
687
988
  outcome: result.outcome,
688
989
  eventType: result.eventType,
689
990
  ...result.send?.decision ? { decision: result.send.decision } : {},
690
991
  ...result.send?.status !== void 0 ? { status: result.send.status } : {}
691
992
  });
993
+ if (result.outcome === "unpaired") {
994
+ ctx.io.errline(
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)."
996
+ );
997
+ }
692
998
  return EXIT.OK;
693
999
  }
694
1000
  };
@@ -748,9 +1054,12 @@ function createUnpairCommand(deps = {}) {
748
1054
  // src/commands/pair.ts
749
1055
  import { closeSync as closeSync2, openSync as openSync2 } from "fs";
750
1056
  import {
1057
+ clearUnpairedNotice,
751
1058
  deriveCodeChallengeS256,
752
1059
  generateCodeVerifier,
753
- getMachineIdentity as getMachineIdentity2,
1060
+ getMachineIdentity as getMachineIdentity3,
1061
+ getToken as getToken3,
1062
+ LocalEventQueue as LocalEventQueue2,
754
1063
  setToken
755
1064
  } from "@birdybeep/agent-core";
756
1065
  import { renderUnicodeCompact } from "uqr";
@@ -829,6 +1138,280 @@ async function pairTokenPoll(apiUrl, deviceCode, fetchImpl, machineFingerprint,
829
1138
  return { status: "error", code: code ?? "unknown", message, retryable: true };
830
1139
  }
831
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
+
832
1415
  // src/commands/pair.ts
833
1416
  var DEFAULT_POLL_INTERVAL_MS = 2e3;
834
1417
  var HEARTBEAT_MS = 15e3;
@@ -836,11 +1419,15 @@ function renderQrMatrix(qrPayload) {
836
1419
  return renderUnicodeCompact(qrPayload, { border: 2 });
837
1420
  }
838
1421
  function parsePairFlags(args) {
839
- const flags = { yes: false };
1422
+ const flags = { yes: false, noInstall: false, noTest: false };
840
1423
  for (let i = 0; i < args.length; i += 1) {
841
1424
  const token = args[i] ?? "";
842
1425
  if (token === "--yes" || token === "-y") {
843
1426
  flags.yes = true;
1427
+ } else if (token === "--no-install") {
1428
+ flags.noInstall = true;
1429
+ } else if (token === "--no-test") {
1430
+ flags.noTest = true;
844
1431
  } else if (token === "--expect-email" || token.startsWith("--expect-email=")) {
845
1432
  const inline = token.startsWith("--expect-email=") ? token.slice("--expect-email=".length) : void 0;
846
1433
  const value = inline ?? args[++i];
@@ -960,7 +1547,18 @@ async function promptForAnswer(question, on) {
960
1547
  input.once?.("error", () => done(""));
961
1548
  });
962
1549
  }
963
- 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 = {}) {
964
1562
  const fetchImpl = deps.fetchImpl ?? fetch;
965
1563
  const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
966
1564
  const clock = deps.now ?? (() => Date.now());
@@ -973,9 +1571,10 @@ function createPairCommand(deps = {}) {
973
1571
  return typeof pinned === "string" && pinned.trim().length > 0 ? pinned : void 0;
974
1572
  });
975
1573
  return {
976
- name: "pair",
977
- summary: "Pair this machine with your BirdyBeep account (QR or manual)",
978
- 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 } : {},
979
1578
  options: [
980
1579
  {
981
1580
  flag: "--yes",
@@ -986,16 +1585,41 @@ function createPairCommand(deps = {}) {
986
1585
  flag: "--expect-email",
987
1586
  value: "<addr>",
988
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"
989
1596
  }
990
1597
  ],
991
1598
  run: async (ctx) => {
992
1599
  const pairFlags = parsePairFlags(ctx.args);
993
1600
  if (pairFlags.error !== void 0) {
994
- ctx.io.errline(`birdybeep pair: ${pairFlags.error}.`);
1601
+ ctx.io.errline(`birdybeep ${verb.name}: ${pairFlags.error}.`);
995
1602
  return EXIT.USAGE;
996
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
+ }
997
1621
  const apiUrl = resolveApiUrl();
998
- const identity = getMachineIdentity2();
1622
+ const identity = getMachineIdentity3();
999
1623
  const codeVerifier = generateCodeVerifier();
1000
1624
  const codeChallenge = deriveCodeChallengeS256(codeVerifier);
1001
1625
  const start = await pairStart(
@@ -1095,19 +1719,50 @@ function createPairCommand(deps = {}) {
1095
1719
  }
1096
1720
  await setToken(paired.machineToken, deps.tokenOptions ?? {});
1097
1721
  writeCliConfig({ apiUrl });
1722
+ const discarded = new LocalEventQueue2().discardBefore(clock());
1723
+ clearUnpairedNotice();
1098
1724
  const humanSuffix = approvedBy !== void 0 ? ` to ${approvedBy}` : "";
1099
- 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({
1100
1730
  paired: true,
1101
1731
  machineId: paired.machineId,
1102
- ...approvedBy !== void 0 ? { approvedByEmail: approvedBy } : {}
1732
+ discardedPrePairingEvents: discarded,
1733
+ ...approvedBy !== void 0 ? { approvedByEmail: approvedBy } : {},
1734
+ ...report !== void 0 ? { setup: report } : {}
1103
1735
  });
1104
- return EXIT.OK;
1736
+ return report !== void 0 && !report.ok ? EXIT.ERROR : EXIT.OK;
1105
1737
  }
1106
1738
  };
1107
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
+ }
1108
1763
 
1109
1764
  // src/commands/queue.ts
1110
- import { LocalEventQueue as LocalEventQueue2 } from "@birdybeep/agent-core";
1765
+ import { LocalEventQueue as LocalEventQueue3 } from "@birdybeep/agent-core";
1111
1766
  function createQueueCommand() {
1112
1767
  return {
1113
1768
  name: "queue",
@@ -1119,7 +1774,7 @@ function createQueueCommand() {
1119
1774
  summary: "Clear the local offline event queue (debug)",
1120
1775
  usage: "birdybeep queue clear",
1121
1776
  run: (ctx) => {
1122
- const cleared = new LocalEventQueue2().clear();
1777
+ const cleared = new LocalEventQueue3().clear();
1123
1778
  ctx.io.emit(`Cleared ${cleared} queued event(s).`, { cleared });
1124
1779
  return EXIT.OK;
1125
1780
  }
@@ -1131,20 +1786,20 @@ function createQueueCommand() {
1131
1786
  // src/commands/report-status.ts
1132
1787
  import {
1133
1788
  errorEnvelopeSchema as errorEnvelopeSchema2,
1134
- getToken as getToken3,
1789
+ getToken as getToken4,
1135
1790
  integrationStatusResponseSchema
1136
1791
  } from "@birdybeep/agent-core";
1137
- import { CLAUDE_CODE_ADAPTER_VERSION, claudeCodeAdapter as claudeCodeAdapter3 } from "@birdybeep/claude-code";
1138
- import { CODEX_ADAPTER_VERSION, codexAdapter as codexAdapter3 } from "@birdybeep/codex";
1139
- import { COPILOT_ADAPTER_VERSION, copilotAdapter as copilotAdapter3 } from "@birdybeep/copilot";
1140
- import { CURSOR_ADAPTER_VERSION, cursorAdapter as cursorAdapter3 } from "@birdybeep/cursor";
1141
- 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";
1142
1797
  var DEFAULT_ADAPTERS3 = [
1143
- claudeCodeAdapter3,
1144
- codexAdapter3,
1145
- opencodeAdapter3,
1146
- cursorAdapter3,
1147
- copilotAdapter3
1798
+ claudeCodeAdapter4,
1799
+ codexAdapter4,
1800
+ opencodeAdapter4,
1801
+ cursorAdapter4,
1802
+ copilotAdapter4
1148
1803
  ];
1149
1804
  var ADAPTER_VERSIONS = {
1150
1805
  claude_code: CLAUDE_CODE_ADAPTER_VERSION,
@@ -1174,7 +1829,7 @@ function createReportStatusCommand(deps = {}) {
1174
1829
  summary: "Internal: report integration status to the backend",
1175
1830
  usage: "birdybeep report-status [--json]",
1176
1831
  run: async (ctx) => {
1177
- const token = await getToken3(deps.tokenOptions ?? {});
1832
+ const token = await getToken4(deps.tokenOptions ?? {});
1178
1833
  if (token === null) {
1179
1834
  ctx.io.errline("No machine token \u2014 run `birdybeep pair` first.");
1180
1835
  return EXIT.ERROR;
@@ -1237,23 +1892,23 @@ function createReportStatusCommand(deps = {}) {
1237
1892
 
1238
1893
  // src/commands/status.ts
1239
1894
  import {
1240
- createSender as defaultCreateSender3
1895
+ createSender as defaultCreateSender4
1241
1896
  } from "@birdybeep/agent-core";
1242
- import { claudeCodeAdapter as claudeCodeAdapter4 } from "@birdybeep/claude-code";
1243
- import { codexAdapter as codexAdapter4 } from "@birdybeep/codex";
1244
- import { copilotAdapter as copilotAdapter4 } from "@birdybeep/copilot";
1245
- import { cursorAdapter as cursorAdapter4 } from "@birdybeep/cursor";
1246
- 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";
1247
1902
  var DEFAULT_ADAPTERS4 = [
1248
- claudeCodeAdapter4,
1249
- codexAdapter4,
1250
- opencodeAdapter4,
1251
- cursorAdapter4,
1252
- copilotAdapter4
1903
+ claudeCodeAdapter5,
1904
+ codexAdapter5,
1905
+ opencodeAdapter5,
1906
+ cursorAdapter5,
1907
+ copilotAdapter5
1253
1908
  ];
1254
1909
  function createStatusCommand(deps = {}) {
1255
1910
  const adapters = deps.adapters ?? DEFAULT_ADAPTERS4;
1256
- const makeSender = deps.createSender ?? ((baseUrl) => defaultCreateSender3(
1911
+ const makeSender = deps.createSender ?? ((baseUrl) => defaultCreateSender4(
1257
1912
  deps.tokenOptions ? { baseUrl, tokenOptions: deps.tokenOptions } : { baseUrl }
1258
1913
  ));
1259
1914
  return {
@@ -1264,14 +1919,21 @@ function createStatusCommand(deps = {}) {
1264
1919
  const machine = machineIdentity();
1265
1920
  const paired = await isPaired(deps.tokenOptions ?? {});
1266
1921
  const integrations = await gatherIntegrations(adapters);
1922
+ const surfaces = await gatherSurfaces(adapters, deps.surfaceOptions ?? {});
1267
1923
  const depthBefore = localQueueDepth();
1924
+ const unpaired = unpairedActivity();
1925
+ const filtered = filteredActivity();
1268
1926
  const drain = await makeSender(resolveApiUrl()).drainNow();
1269
1927
  const depthAfter = localQueueDepth();
1928
+ const overflowDropped = localQueueOverflowDrops();
1270
1929
  const report = {
1271
1930
  machine,
1272
1931
  paired,
1273
1932
  integrations,
1274
- 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 } : {}
1275
1937
  };
1276
1938
  if (ctx.flags.json) {
1277
1939
  ctx.io.result(report);
@@ -1279,89 +1941,29 @@ function createStatusCommand(deps = {}) {
1279
1941
  ctx.io.line(`Machine: ${machine.label} (${machine.os})`);
1280
1942
  ctx.io.line(paired ? "Paired: yes" : "Paired: no \u2014 run `birdybeep pair`");
1281
1943
  ctx.io.line("Integrations:");
1282
- 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
+ }
1283
1952
  ctx.io.line(
1284
- `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` : "")
1285
1954
  );
1955
+ if (unpaired !== null) ctx.io.line(`\u26A0 Lost: ${describeUnpairedActivity(unpaired)}`);
1956
+ if (filtered !== null) ctx.io.line(`Local: ${describeFilteredActivity(filtered)}`);
1286
1957
  }
1287
1958
  return paired ? EXIT.OK : EXIT.ERROR;
1288
1959
  }
1289
1960
  };
1290
1961
  }
1291
1962
 
1292
- // src/commands/test.ts
1293
- import { randomUUID } from "crypto";
1294
- import {
1295
- createSender as defaultCreateSender4,
1296
- getMachineIdentity as getMachineIdentity3,
1297
- normalizeEvent
1298
- } from "@birdybeep/agent-core";
1299
- function buildTestEvent(opts = {}) {
1300
- const machine = getMachineIdentity3();
1301
- return normalizeEvent(
1302
- {
1303
- event_type: "test",
1304
- status: "running",
1305
- harness: "claude_code",
1306
- // schema requires a harness; the "test" type distinguishes it
1307
- // Unique per run: a repeat `birdybeep test` inside the backend's dedupe window must
1308
- // still beep — a constant id made the second test silently "deduped" (9fh).
1309
- source_session_id: `birdybeep-cli-test-${randomUUID()}`,
1310
- machine: { label: machine.label, os: machine.os },
1311
- workspace: { cwd: process.cwd() },
1312
- title: "BirdyBeep test event",
1313
- body: "If you can see this, your machine is wired up correctly.",
1314
- metadata: { test: true }
1315
- },
1316
- opts
1317
- );
1318
- }
1319
- function createTestCommand(deps = {}) {
1320
- const makeSender = deps.createSender ?? ((baseUrl) => defaultCreateSender4(
1321
- deps.tokenOptions ? { baseUrl, tokenOptions: deps.tokenOptions } : { baseUrl }
1322
- ));
1323
- return {
1324
- name: "test",
1325
- summary: "Send a test event end-to-end",
1326
- usage: "birdybeep test [--json]",
1327
- run: async (ctx) => {
1328
- const event = buildTestEvent();
1329
- const result = await makeSender(resolveApiUrl()).send(event);
1330
- if (ctx.flags.json) {
1331
- ctx.io.result({
1332
- outcome: result.outcome,
1333
- ...result.status ? { status: result.status } : {},
1334
- ...result.decision ? { decision: result.decision } : {}
1335
- });
1336
- } else if (result.outcome === "delivered") {
1337
- if (result.decision === "notified" || result.decision === void 0) {
1338
- ctx.io.line("\u2713 Test event delivered \u2014 check your phone for a test Beep.");
1339
- } else if (result.decision === "suppressed") {
1340
- ctx.io.line(
1341
- "\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`."
1342
- );
1343
- } else if (result.decision === "deduped") {
1344
- ctx.io.line(
1345
- "\u26A0 The backend accepted the test event but folded it into a recent duplicate \u2014 wait ~30s and run `birdybeep test` again."
1346
- );
1347
- } else {
1348
- ctx.io.line(
1349
- `\u26A0 The backend accepted the test event but decided "${result.decision}" \u2014 no push was sent. Run \`birdybeep doctor\`.`
1350
- );
1351
- }
1352
- } else if (result.outcome === "queued") {
1353
- ctx.io.line("\u2022 Offline \u2014 test event queued; it will deliver when you reconnect.");
1354
- } else {
1355
- ctx.io.line("\u2717 Test event was rejected by the backend. Run `birdybeep doctor`.");
1356
- }
1357
- return result.outcome === "dropped" ? EXIT.ERROR : EXIT.OK;
1358
- }
1359
- };
1360
- }
1361
-
1362
1963
  // src/commands.ts
1363
1964
  function buildCommands() {
1364
1965
  return [
1966
+ createSetupCommand(),
1365
1967
  createPairCommand(),
1366
1968
  createLogoutCommand(),
1367
1969
  createUnpairCommand(),
@@ -1376,7 +1978,7 @@ function buildCommands() {
1376
1978
  }
1377
1979
 
1378
1980
  // src/update-check.ts
1379
- 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";
1380
1982
  import { join as join3 } from "path";
1381
1983
  import { birdyBeepConfigDir as birdyBeepConfigDir3 } from "@birdybeep/agent-core";
1382
1984
  var PACKAGE_NAME = "@birdybeep/cli";
@@ -1436,7 +2038,7 @@ function updateCachePath() {
1436
2038
  }
1437
2039
  function readUpdateCache() {
1438
2040
  try {
1439
- const parsed = JSON.parse(readFileSync2(updateCachePath(), "utf8"));
2041
+ const parsed = JSON.parse(readFileSync3(updateCachePath(), "utf8"));
1440
2042
  if (typeof parsed !== "object" || parsed === null) return null;
1441
2043
  const { checkedAt, latest } = parsed;
1442
2044
  if (typeof checkedAt !== "number") return null;
@@ -1536,4 +2138,4 @@ export {
1536
2138
  buildCommands,
1537
2139
  runCli
1538
2140
  };
1539
- //# sourceMappingURL=chunk-U4EIHC5C.js.map
2141
+ //# sourceMappingURL=chunk-BY5MNQE3.js.map