@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/index.cjs CHANGED
@@ -67,6 +67,21 @@ async function gatherIntegrations(adapters) {
67
67
  async function isPaired(tokenOptions = {}) {
68
68
  return await (0, import_agent_core.getToken)(tokenOptions) !== null;
69
69
  }
70
+ async function pairingReport(tokenOptions = {}) {
71
+ const lookup = await (0, import_agent_core.readToken)(tokenOptions);
72
+ if (lookup.state === "present") return { state: "paired" };
73
+ if (lookup.state === "absent") return { state: "unpaired" };
74
+ return { state: "unknown", reason: lookup.reason, store: lookup.store };
75
+ }
76
+ function describeTokenStoreUnavailable(report) {
77
+ 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.`;
78
+ }
79
+ function tokenStoreRemedy(report) {
80
+ if (report.store === "file") {
81
+ 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.";
82
+ }
83
+ 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`.";
84
+ }
70
85
  function localQueueDepth() {
71
86
  return new import_agent_core.LocalEventQueue().size();
72
87
  }
@@ -676,16 +691,48 @@ function createDoctorCommand(deps = {}) {
676
691
  usage: "birdybeep doctor [--json]",
677
692
  run: async (ctx) => {
678
693
  const checks = [];
679
- const apiUrl = resolveApiUrl();
680
- const paired = await isPaired(deps.tokenOptions ?? {});
694
+ const apiUrl = deps.baseUrl ?? resolveApiUrl();
695
+ const pairing = await pairingReport(deps.tokenOptions ?? {});
681
696
  checks.push(
682
- paired ? { name: "Machine token", ok: true } : {
697
+ pairing.state === "paired" ? { name: "Machine token", ok: true } : pairing.state === "unpaired" ? {
683
698
  name: "Machine token",
684
699
  ok: false,
685
700
  detail: "No machine token found.",
686
701
  remedy: "Run `birdybeep pair` to pair this machine."
702
+ } : {
703
+ name: "Machine token",
704
+ ok: false,
705
+ detail: describeTokenStoreUnavailable(pairing),
706
+ remedy: tokenStoreRemedy(pairing)
687
707
  }
688
708
  );
709
+ const reachability = await (0, import_agent_core4.fetchPushReachability)({
710
+ baseUrl: apiUrl,
711
+ ...deps.tokenOptions ? { tokenOptions: deps.tokenOptions } : {},
712
+ ...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}
713
+ });
714
+ const reach = (0, import_agent_core4.describeReachability)(reachability);
715
+ if (reach !== null) {
716
+ checks.push({
717
+ name: "Push reachability",
718
+ ok: reach.ok,
719
+ detail: reach.detail,
720
+ ...reach.remedy !== void 0 ? { remedy: reach.remedy } : {}
721
+ });
722
+ }
723
+ const checkIn = (0, import_agent_core4.describeCheckIn)(reachability);
724
+ if (checkIn !== null) {
725
+ checks.push({ name: "Device check-in", ok: checkIn.ok, detail: checkIn.detail });
726
+ }
727
+ const quota = (0, import_agent_core4.describeQuota)(reachability);
728
+ if (quota !== null) {
729
+ checks.push({
730
+ name: "Beep quota",
731
+ ok: quota.ok,
732
+ detail: quota.detail,
733
+ ...quota.remedy !== void 0 ? { remedy: quota.remedy } : {}
734
+ });
735
+ }
689
736
  const unpaired = unpairedActivity();
690
737
  if (unpaired !== null) {
691
738
  checks.push({
@@ -757,6 +804,16 @@ function createDoctorCommand(deps = {}) {
757
804
  ctx.io.result({
758
805
  ok,
759
806
  checks,
807
+ pairing,
808
+ // gcgp.23: paired | unpaired | unknown — never a bare boolean
809
+ // The backend's own quota numbers, unrendered (58l) — a script (or a bug report) gets
810
+ // the window and the counts, not just the sentence built from them.
811
+ ...reachability.state === "ok" && reachability.data.quota !== void 0 ? { quota: reachability.data.quota } : {},
812
+ // The raw check-in (2x9s), unrendered. Present ONLY when the server reported the field
813
+ // at all, so a script can still tell "this backend predates check-ins" (key absent)
814
+ // from "no device has ever checked in" (key present, null) — the same distinction the
815
+ // rendered row is careful about.
816
+ ...reachability.state === "ok" && reachability.data.most_recent_check_in_at !== void 0 ? { mostRecentCheckInAt: reachability.data.most_recent_check_in_at } : {},
760
817
  surfaces: surfaceGroups,
761
818
  queue: { depthBefore, delivered: drain.delivered, depthAfter, overflowDropped },
762
819
  ...unpaired !== null ? { unpairedActivity: unpaired } : {},
@@ -1003,13 +1060,20 @@ function createHookCommand(deps = {}) {
1003
1060
  outcome: result.outcome,
1004
1061
  eventType: result.eventType,
1005
1062
  ...result.send?.decision ? { decision: result.send.decision } : {},
1006
- ...result.send?.status !== void 0 ? { status: result.send.status } : {}
1063
+ ...result.send?.status !== void 0 ? { status: result.send.status } : {},
1064
+ ...result.send?.tokenStoreUnavailable !== void 0 ? { tokenStore: "unavailable" } : {}
1007
1065
  });
1008
1066
  if (result.outcome === "unpaired") {
1009
1067
  ctx.io.errline(
1010
1068
  "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)."
1011
1069
  );
1012
1070
  }
1071
+ const unavailable = result.send?.tokenStoreUnavailable;
1072
+ if (unavailable !== void 0) {
1073
+ ctx.io.errline(
1074
+ `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.`
1075
+ );
1076
+ }
1013
1077
  return EXIT.OK;
1014
1078
  }
1015
1079
  };
@@ -1142,7 +1206,7 @@ async function pairTokenPoll(apiUrl, deviceCode, fetchImpl, machineFingerprint,
1142
1206
  }
1143
1207
 
1144
1208
  // src/version.ts
1145
- var CLI_VERSION = "0.6.1".length > 0 ? "0.6.1" : "0.0.0";
1209
+ var CLI_VERSION = "0.8.0".length > 0 ? "0.8.0" : "0.0.0";
1146
1210
 
1147
1211
  // src/commands/setup.ts
1148
1212
  var import_claude_code5 = require("@birdybeep/claude-code");
@@ -1184,16 +1248,34 @@ function createTestCommand(deps = {}) {
1184
1248
  usage: "birdybeep test [--json]",
1185
1249
  run: async (ctx) => {
1186
1250
  const event = buildTestEvent();
1187
- const result = await makeSender(resolveApiUrl()).send(event);
1251
+ const baseUrl = deps.baseUrl ?? resolveApiUrl();
1252
+ const result = await makeSender(baseUrl).send(event);
1188
1253
  if (ctx.flags.json) {
1189
1254
  ctx.io.result({
1190
1255
  outcome: result.outcome,
1191
1256
  ...result.status ? { status: result.status } : {},
1192
- ...result.decision ? { decision: result.decision } : {}
1257
+ ...result.decision ? { decision: result.decision } : {},
1258
+ ...result.queueCause ? { queueCause: result.queueCause } : {},
1259
+ ...result.tokenStoreUnavailable !== void 0 ? { tokenStore: "unavailable", tokenStoreReason: result.tokenStoreUnavailable.reason } : {}
1193
1260
  });
1194
1261
  } else if (result.outcome === "delivered") {
1195
1262
  if (result.decision === "notified" || result.decision === void 0) {
1196
- ctx.io.line("\u2713 Test event delivered \u2014 check your phone for a test Beep.");
1263
+ const reach = await (0, import_agent_core8.fetchPushReachability)({
1264
+ baseUrl,
1265
+ ...deps.tokenOptions ? { tokenOptions: deps.tokenOptions } : {},
1266
+ ...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}
1267
+ });
1268
+ if (reach.state === "ok" && reach.data.active_device_count === 0) {
1269
+ ctx.io.line(
1270
+ "\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)."
1271
+ );
1272
+ } else if (reach.state === "ok") {
1273
+ ctx.io.line(
1274
+ `\u2713 Test event accepted and queued for ${String(reach.data.active_device_count)} registered device(s) \u2014 check your phone for a test Beep.`
1275
+ );
1276
+ } else {
1277
+ ctx.io.line("\u2713 Test event accepted by the backend \u2014 check your phone for a test Beep.");
1278
+ }
1197
1279
  } else if (result.decision === "suppressed") {
1198
1280
  ctx.io.line(
1199
1281
  "\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`."
@@ -1211,8 +1293,32 @@ function createTestCommand(deps = {}) {
1211
1293
  ctx.io.line(
1212
1294
  "\u2717 NOT PAIRED \u2014 this machine has no BirdyBeep machine token, so nothing was sent (and nothing was queued). Run `birdybeep pair`."
1213
1295
  );
1296
+ } else if (result.tokenStoreUnavailable !== void 0) {
1297
+ ctx.io.line(
1298
+ `\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.`
1299
+ );
1300
+ } else if (result.outcome === "queued" && result.queueCause === "backend") {
1301
+ const status = result.status !== void 0 ? ` (HTTP ${String(result.status)})` : "";
1302
+ ctx.io.line(
1303
+ 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.`
1304
+ );
1214
1305
  } else if (result.outcome === "queued") {
1215
1306
  ctx.io.line("\u2022 Offline \u2014 test event queued; it will deliver when you reconnect.");
1307
+ } else if (result.code === "quota_exceeded") {
1308
+ const reach = await (0, import_agent_core8.fetchPushReachability)({
1309
+ baseUrl,
1310
+ ...deps.tokenOptions ? { tokenOptions: deps.tokenOptions } : {},
1311
+ ...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}
1312
+ });
1313
+ const quota = reach.state === "ok" ? reach.data.quota : void 0;
1314
+ const exhausted = quota ? (0, import_agent_core8.describeExhaustedQuota)(quota) : void 0;
1315
+ ctx.io.line(
1316
+ 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}` : (
1317
+ // An older backend reports no quota, so there is no period and no reset date to
1318
+ // give — and `doctor` cannot supply them either, since it reads this same response.
1319
+ "\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."
1320
+ )
1321
+ );
1216
1322
  } else {
1217
1323
  ctx.io.line("\u2717 Test event was rejected by the backend. Run `birdybeep doctor`.");
1218
1324
  }
@@ -1913,7 +2019,8 @@ function createStatusCommand(deps = {}) {
1913
2019
  usage: "birdybeep status [--json]",
1914
2020
  run: async (ctx) => {
1915
2021
  const machine = machineIdentity();
1916
- const paired = await isPaired(deps.tokenOptions ?? {});
2022
+ const pairing = await pairingReport(deps.tokenOptions ?? {});
2023
+ const paired = pairing.state === "paired";
1917
2024
  const integrations = await gatherIntegrations(adapters);
1918
2025
  const surfaces = await gatherSurfaces(adapters, deps.surfaceOptions ?? {});
1919
2026
  const depthBefore = localQueueDepth();
@@ -1925,6 +2032,8 @@ function createStatusCommand(deps = {}) {
1925
2032
  const report = {
1926
2033
  machine,
1927
2034
  paired,
2035
+ pairing,
2036
+ // gcgp.23: paired | unpaired | unknown — `paired: false` cannot say which
1928
2037
  integrations,
1929
2038
  surfaces,
1930
2039
  queue: { depthBefore, delivered: drain.delivered, depthAfter, overflowDropped },
@@ -1935,7 +2044,9 @@ function createStatusCommand(deps = {}) {
1935
2044
  ctx.io.result(report);
1936
2045
  } else {
1937
2046
  ctx.io.line(`Machine: ${machine.label} (${machine.os})`);
1938
- ctx.io.line(paired ? "Paired: yes" : "Paired: no \u2014 run `birdybeep pair`");
2047
+ ctx.io.line(
2048
+ pairing.state === "paired" ? "Paired: yes" : pairing.state === "unpaired" ? "Paired: no \u2014 run `birdybeep pair`" : `Paired: unknown \u2014 ${describeTokenStoreUnavailable(pairing)}`
2049
+ );
1939
2050
  ctx.io.line("Integrations:");
1940
2051
  for (const i of integrations) {
1941
2052
  ctx.io.line(` ${i.displayName}: ${i.status}`);