@birdybeep/cli 0.6.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.cjs CHANGED
@@ -48,6 +48,21 @@ async function gatherIntegrations(adapters) {
48
48
  async function isPaired(tokenOptions = {}) {
49
49
  return await (0, import_agent_core.getToken)(tokenOptions) !== null;
50
50
  }
51
+ async function pairingReport(tokenOptions = {}) {
52
+ const lookup = await (0, import_agent_core.readToken)(tokenOptions);
53
+ if (lookup.state === "present") return { state: "paired" };
54
+ if (lookup.state === "absent") return { state: "unpaired" };
55
+ return { state: "unknown", reason: lookup.reason, store: lookup.store };
56
+ }
57
+ function describeTokenStoreUnavailable(report) {
58
+ return `Could not read the token store (${report.reason ?? "unknown error"}), so whether this machine is paired is unknown. Events fired now are QUEUED, not lost, and send once it is readable.`;
59
+ }
60
+ function tokenStoreRemedy(report) {
61
+ if (report.store === "file") {
62
+ return "Repair the token file \u2014 check that its directory and the file itself are readable and writable by you (`chmod 700` the directory, `chmod 600` the file), then run `birdybeep doctor` again to drain the queue.";
63
+ }
64
+ return "Unlock your login keychain (log in to the desktop session, or unlock the screen), then run `birdybeep doctor` again to drain the queue. If it stays unreadable, run `birdybeep pair`.";
65
+ }
51
66
  function localQueueDepth() {
52
67
  return new import_agent_core.LocalEventQueue().size();
53
68
  }
@@ -652,16 +667,48 @@ function createDoctorCommand(deps = {}) {
652
667
  usage: "birdybeep doctor [--json]",
653
668
  run: async (ctx) => {
654
669
  const checks = [];
655
- const apiUrl = resolveApiUrl();
656
- const paired = await isPaired(deps.tokenOptions ?? {});
670
+ const apiUrl = deps.baseUrl ?? resolveApiUrl();
671
+ const pairing = await pairingReport(deps.tokenOptions ?? {});
657
672
  checks.push(
658
- paired ? { name: "Machine token", ok: true } : {
673
+ pairing.state === "paired" ? { name: "Machine token", ok: true } : pairing.state === "unpaired" ? {
659
674
  name: "Machine token",
660
675
  ok: false,
661
676
  detail: "No machine token found.",
662
677
  remedy: "Run `birdybeep pair` to pair this machine."
678
+ } : {
679
+ name: "Machine token",
680
+ ok: false,
681
+ detail: describeTokenStoreUnavailable(pairing),
682
+ remedy: tokenStoreRemedy(pairing)
663
683
  }
664
684
  );
685
+ const reachability = await (0, import_agent_core4.fetchPushReachability)({
686
+ baseUrl: apiUrl,
687
+ ...deps.tokenOptions ? { tokenOptions: deps.tokenOptions } : {},
688
+ ...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}
689
+ });
690
+ const reach = (0, import_agent_core4.describeReachability)(reachability);
691
+ if (reach !== null) {
692
+ checks.push({
693
+ name: "Push reachability",
694
+ ok: reach.ok,
695
+ detail: reach.detail,
696
+ ...reach.remedy !== void 0 ? { remedy: reach.remedy } : {}
697
+ });
698
+ }
699
+ const checkIn = (0, import_agent_core4.describeCheckIn)(reachability);
700
+ if (checkIn !== null) {
701
+ checks.push({ name: "Device check-in", ok: checkIn.ok, detail: checkIn.detail });
702
+ }
703
+ const quota = (0, import_agent_core4.describeQuota)(reachability);
704
+ if (quota !== null) {
705
+ checks.push({
706
+ name: "Beep quota",
707
+ ok: quota.ok,
708
+ detail: quota.detail,
709
+ ...quota.remedy !== void 0 ? { remedy: quota.remedy } : {}
710
+ });
711
+ }
665
712
  const unpaired = unpairedActivity();
666
713
  if (unpaired !== null) {
667
714
  checks.push({
@@ -733,6 +780,16 @@ function createDoctorCommand(deps = {}) {
733
780
  ctx.io.result({
734
781
  ok,
735
782
  checks,
783
+ pairing,
784
+ // gcgp.23: paired | unpaired | unknown — never a bare boolean
785
+ // The backend's own quota numbers, unrendered (58l) — a script (or a bug report) gets
786
+ // the window and the counts, not just the sentence built from them.
787
+ ...reachability.state === "ok" && reachability.data.quota !== void 0 ? { quota: reachability.data.quota } : {},
788
+ // The raw check-in (2x9s), unrendered. Present ONLY when the server reported the field
789
+ // at all, so a script can still tell "this backend predates check-ins" (key absent)
790
+ // from "no device has ever checked in" (key present, null) — the same distinction the
791
+ // rendered row is careful about.
792
+ ...reachability.state === "ok" && reachability.data.most_recent_check_in_at !== void 0 ? { mostRecentCheckInAt: reachability.data.most_recent_check_in_at } : {},
736
793
  surfaces: surfaceGroups,
737
794
  queue: { depthBefore, delivered: drain.delivered, depthAfter, overflowDropped },
738
795
  ...unpaired !== null ? { unpairedActivity: unpaired } : {},
@@ -979,13 +1036,20 @@ function createHookCommand(deps = {}) {
979
1036
  outcome: result.outcome,
980
1037
  eventType: result.eventType,
981
1038
  ...result.send?.decision ? { decision: result.send.decision } : {},
982
- ...result.send?.status !== void 0 ? { status: result.send.status } : {}
1039
+ ...result.send?.status !== void 0 ? { status: result.send.status } : {},
1040
+ ...result.send?.tokenStoreUnavailable !== void 0 ? { tokenStore: "unavailable" } : {}
983
1041
  });
984
1042
  if (result.outcome === "unpaired") {
985
1043
  ctx.io.errline(
986
1044
  "birdybeep: this machine is not paired \u2014 the event was not sent and was not queued. Run `birdybeep pair` (or `birdybeep doctor` to see how many events this has cost)."
987
1045
  );
988
1046
  }
1047
+ const unavailable = result.send?.tokenStoreUnavailable;
1048
+ if (unavailable !== void 0) {
1049
+ ctx.io.errline(
1050
+ `birdybeep: could not read the machine token (${unavailable.reason}) \u2014 the event was QUEUED, not sent. It will deliver once the token store is readable (unlock your keychain); \`birdybeep doctor\` drains the queue.`
1051
+ );
1052
+ }
989
1053
  return EXIT.OK;
990
1054
  }
991
1055
  };
@@ -1118,7 +1182,7 @@ async function pairTokenPoll(apiUrl, deviceCode, fetchImpl, machineFingerprint,
1118
1182
  }
1119
1183
 
1120
1184
  // src/version.ts
1121
- var CLI_VERSION = "0.6.1".length > 0 ? "0.6.1" : "0.0.0";
1185
+ var CLI_VERSION = "0.8.0".length > 0 ? "0.8.0" : "0.0.0";
1122
1186
 
1123
1187
  // src/commands/setup.ts
1124
1188
  var import_claude_code5 = require("@birdybeep/claude-code");
@@ -1160,16 +1224,34 @@ function createTestCommand(deps = {}) {
1160
1224
  usage: "birdybeep test [--json]",
1161
1225
  run: async (ctx) => {
1162
1226
  const event = buildTestEvent();
1163
- const result = await makeSender(resolveApiUrl()).send(event);
1227
+ const baseUrl = deps.baseUrl ?? resolveApiUrl();
1228
+ const result = await makeSender(baseUrl).send(event);
1164
1229
  if (ctx.flags.json) {
1165
1230
  ctx.io.result({
1166
1231
  outcome: result.outcome,
1167
1232
  ...result.status ? { status: result.status } : {},
1168
- ...result.decision ? { decision: result.decision } : {}
1233
+ ...result.decision ? { decision: result.decision } : {},
1234
+ ...result.queueCause ? { queueCause: result.queueCause } : {},
1235
+ ...result.tokenStoreUnavailable !== void 0 ? { tokenStore: "unavailable", tokenStoreReason: result.tokenStoreUnavailable.reason } : {}
1169
1236
  });
1170
1237
  } else if (result.outcome === "delivered") {
1171
1238
  if (result.decision === "notified" || result.decision === void 0) {
1172
- ctx.io.line("\u2713 Test event delivered \u2014 check your phone for a test Beep.");
1239
+ const reach = await (0, import_agent_core8.fetchPushReachability)({
1240
+ baseUrl,
1241
+ ...deps.tokenOptions ? { tokenOptions: deps.tokenOptions } : {},
1242
+ ...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}
1243
+ });
1244
+ if (reach.state === "ok" && reach.data.active_device_count === 0) {
1245
+ ctx.io.line(
1246
+ "\u26A0 The backend accepted the test event, but NO device on this account can receive it \u2014 nothing will arrive. Open the BirdyBeep app on your phone to register it (if it says the device limit is full, free a slot in Settings \u203A devices)."
1247
+ );
1248
+ } else if (reach.state === "ok") {
1249
+ ctx.io.line(
1250
+ `\u2713 Test event accepted and queued for ${String(reach.data.active_device_count)} registered device(s) \u2014 check your phone for a test Beep.`
1251
+ );
1252
+ } else {
1253
+ ctx.io.line("\u2713 Test event accepted by the backend \u2014 check your phone for a test Beep.");
1254
+ }
1173
1255
  } else if (result.decision === "suppressed") {
1174
1256
  ctx.io.line(
1175
1257
  "\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`."
@@ -1187,8 +1269,32 @@ function createTestCommand(deps = {}) {
1187
1269
  ctx.io.line(
1188
1270
  "\u2717 NOT PAIRED \u2014 this machine has no BirdyBeep machine token, so nothing was sent (and nothing was queued). Run `birdybeep pair`."
1189
1271
  );
1272
+ } else if (result.tokenStoreUnavailable !== void 0) {
1273
+ ctx.io.line(
1274
+ `\u2022 Could not read the machine token (${result.tokenStoreUnavailable.reason}) \u2014 the test event was queued and delivers once the token store is readable. If your keychain is locked, unlock it and run \`birdybeep test\` again.`
1275
+ );
1276
+ } else if (result.outcome === "queued" && result.queueCause === "backend") {
1277
+ const status = result.status !== void 0 ? ` (HTTP ${String(result.status)})` : "";
1278
+ ctx.io.line(
1279
+ result.code === "rate_limited" || result.status === 429 ? `\u2022 Throttled by the backend${status} \u2014 test event queued; it retries on its own. Not your network: the request got through.` : `\u2022 The backend is having trouble${status} \u2014 test event queued; it retries on its own. Not your network: the request got through.`
1280
+ );
1190
1281
  } else if (result.outcome === "queued") {
1191
1282
  ctx.io.line("\u2022 Offline \u2014 test event queued; it will deliver when you reconnect.");
1283
+ } else if (result.code === "quota_exceeded") {
1284
+ const reach = await (0, import_agent_core8.fetchPushReachability)({
1285
+ baseUrl,
1286
+ ...deps.tokenOptions ? { tokenOptions: deps.tokenOptions } : {},
1287
+ ...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}
1288
+ });
1289
+ const quota = reach.state === "ok" ? reach.data.quota : void 0;
1290
+ const exhausted = quota ? (0, import_agent_core8.describeExhaustedQuota)(quota) : void 0;
1291
+ ctx.io.line(
1292
+ quota && exhausted ? `\u2717 Test event REJECTED \u2014 this account's monthly beep quota is used up (${String(quota.beeps_accepted)}/${String(quota.beeps_limit)} beeps on the ${quota.plan} plan, period ${exhausted.window}). Every notifiable event is being rejected. ${exhausted.remedy}` : (
1293
+ // An older backend reports no quota, so there is no period and no reset date to
1294
+ // give — and `doctor` cannot supply them either, since it reads this same response.
1295
+ "\u2717 Test event REJECTED \u2014 this account's monthly beep quota is used up, so no beep can be sent. This server does not report the period or the reset date; check your usage in the BirdyBeep app."
1296
+ )
1297
+ );
1192
1298
  } else {
1193
1299
  ctx.io.line("\u2717 Test event was rejected by the backend. Run `birdybeep doctor`.");
1194
1300
  }
@@ -1889,7 +1995,8 @@ function createStatusCommand(deps = {}) {
1889
1995
  usage: "birdybeep status [--json]",
1890
1996
  run: async (ctx) => {
1891
1997
  const machine = machineIdentity();
1892
- const paired = await isPaired(deps.tokenOptions ?? {});
1998
+ const pairing = await pairingReport(deps.tokenOptions ?? {});
1999
+ const paired = pairing.state === "paired";
1893
2000
  const integrations = await gatherIntegrations(adapters);
1894
2001
  const surfaces = await gatherSurfaces(adapters, deps.surfaceOptions ?? {});
1895
2002
  const depthBefore = localQueueDepth();
@@ -1901,6 +2008,8 @@ function createStatusCommand(deps = {}) {
1901
2008
  const report = {
1902
2009
  machine,
1903
2010
  paired,
2011
+ pairing,
2012
+ // gcgp.23: paired | unpaired | unknown — `paired: false` cannot say which
1904
2013
  integrations,
1905
2014
  surfaces,
1906
2015
  queue: { depthBefore, delivered: drain.delivered, depthAfter, overflowDropped },
@@ -1911,7 +2020,9 @@ function createStatusCommand(deps = {}) {
1911
2020
  ctx.io.result(report);
1912
2021
  } else {
1913
2022
  ctx.io.line(`Machine: ${machine.label} (${machine.os})`);
1914
- ctx.io.line(paired ? "Paired: yes" : "Paired: no \u2014 run `birdybeep pair`");
2023
+ ctx.io.line(
2024
+ pairing.state === "paired" ? "Paired: yes" : pairing.state === "unpaired" ? "Paired: no \u2014 run `birdybeep pair`" : `Paired: unknown \u2014 ${describeTokenStoreUnavailable(pairing)}`
2025
+ );
1915
2026
  ctx.io.line("Integrations:");
1916
2027
  for (const i of integrations) {
1917
2028
  ctx.io.line(` ${i.displayName}: ${i.status}`);