@birdybeep/cli 0.1.0 → 0.3.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
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.ts
@@ -35,6 +45,8 @@ module.exports = __toCommonJS(index_exports);
35
45
  // src/commands/agent.ts
36
46
  var import_claude_code = require("@birdybeep/claude-code");
37
47
  var import_codex = require("@birdybeep/codex");
48
+ var import_copilot = require("@birdybeep/copilot");
49
+ var import_cursor = require("@birdybeep/cursor");
38
50
  var import_opencode = require("@birdybeep/opencode");
39
51
 
40
52
  // src/framework.ts
@@ -108,8 +120,11 @@ function parseGlobalFlags(argv) {
108
120
  }
109
121
  return { flags, rest };
110
122
  }
111
- function isUnknownFlag(token) {
112
- return token.startsWith("-") && !GLOBAL_FLAG_TOKENS.has(token);
123
+ function isUnknownFlag(token, allowed) {
124
+ if (!token.startsWith("-")) return false;
125
+ if (GLOBAL_FLAG_TOKENS.has(token)) return false;
126
+ const eq = token.indexOf("=");
127
+ return !allowed.has(eq >= 0 ? token.slice(0, eq) : token);
113
128
  }
114
129
  function renderRootHelp(version, commands) {
115
130
  const width = Math.max(...commands.map((c) => c.name.length));
@@ -130,6 +145,16 @@ function renderRootHelp(version, commands) {
130
145
  " -v, --version Show the CLI version"
131
146
  ].join("\n");
132
147
  }
148
+ function commandFlagTokens(...commands) {
149
+ const tokens = /* @__PURE__ */ new Set();
150
+ for (const command of commands) {
151
+ for (const option of command?.options ?? []) {
152
+ tokens.add(option.flag);
153
+ for (const alias of option.aliases ?? []) tokens.add(alias);
154
+ }
155
+ }
156
+ return tokens;
157
+ }
133
158
  function renderCommandHelp(path, command) {
134
159
  const lines = [
135
160
  `birdybeep ${path} \u2014 ${command.summary}`,
@@ -137,6 +162,17 @@ function renderCommandHelp(path, command) {
137
162
  "Usage:",
138
163
  ` ${command.usage ?? `birdybeep ${path} [options]`}`
139
164
  ];
165
+ if (command.options && command.options.length > 0) {
166
+ const labels = command.options.map(
167
+ (o) => `${[o.flag, ...o.aliases ?? []].join(", ")}${o.value ? ` ${o.value}` : ""}`
168
+ );
169
+ const width = Math.max(...labels.map((l) => l.length));
170
+ lines.push(
171
+ "",
172
+ "Options:",
173
+ ...command.options.map((o, i) => ` ${labels[i]?.padEnd(width)} ${o.summary}`)
174
+ );
175
+ }
140
176
  if (command.subcommands && command.subcommands.length > 0) {
141
177
  const width = Math.max(...command.subcommands.map((c) => c.name.length));
142
178
  lines.push(
@@ -161,6 +197,7 @@ async function dispatch(argv, deps) {
161
197
  return EXIT.OK;
162
198
  }
163
199
  let command = deps.commands.find((c) => c.name === rest[0]);
200
+ let parent;
164
201
  const pathParts = [];
165
202
  let argsStart = 1;
166
203
  if (command) {
@@ -168,6 +205,7 @@ async function dispatch(argv, deps) {
168
205
  if (command.subcommands && command.subcommands.length > 0) {
169
206
  const sub = command.subcommands.find((c) => c.name === rest[1]);
170
207
  if (sub) {
208
+ parent = command;
171
209
  command = sub;
172
210
  pathParts.push(sub.name);
173
211
  argsStart = 2;
@@ -191,6 +229,7 @@ async function dispatch(argv, deps) {
191
229
  name: path,
192
230
  summary: command.summary,
193
231
  usage: command.usage,
232
+ options: command.options,
194
233
  subcommands: command.subcommands?.map((c) => ({ name: c.name, summary: c.summary }))
195
234
  });
196
235
  return EXIT.OK;
@@ -200,13 +239,15 @@ async function dispatch(argv, deps) {
200
239
  return EXIT.USAGE;
201
240
  }
202
241
  const args = rest.slice(argsStart);
203
- const unknown = args.find(isUnknownFlag);
242
+ const allowed = commandFlagTokens(command, parent);
243
+ const unknown = args.find((token) => isUnknownFlag(token, allowed));
204
244
  if (unknown !== void 0) {
205
245
  io.errline(`birdybeep ${path}: unknown option "${unknown}".`);
206
246
  return EXIT.USAGE;
207
247
  }
248
+ let code;
208
249
  try {
209
- return await command.run({ args, flags, io });
250
+ code = await command.run({ args, flags, io });
210
251
  } catch (err) {
211
252
  if (err instanceof MissingInputError) {
212
253
  io.errline(
@@ -217,16 +258,38 @@ async function dispatch(argv, deps) {
217
258
  io.errline(`birdybeep ${path}: ${err instanceof Error ? err.message : String(err)}`);
218
259
  return EXIT.ERROR;
219
260
  }
261
+ if (deps.notifyUpdate !== void 0) {
262
+ try {
263
+ await deps.notifyUpdate({ command: pathParts[0] ?? "", flags, io });
264
+ } catch {
265
+ }
266
+ }
267
+ return code;
220
268
  }
221
269
 
222
270
  // src/commands/agent.ts
223
- var DEFAULT_ADAPTERS = [import_claude_code.claudeCodeAdapter, import_codex.codexAdapter, import_opencode.opencodeAdapter];
271
+ var DEFAULT_ADAPTERS = [
272
+ import_claude_code.claudeCodeAdapter,
273
+ import_codex.codexAdapter,
274
+ import_opencode.opencodeAdapter,
275
+ import_cursor.cursorAdapter,
276
+ import_copilot.copilotAdapter
277
+ ];
224
278
  var TARGET_TO_ID = {
225
279
  claude: "claude_code",
226
280
  codex: "codex",
227
- opencode: "opencode"
281
+ opencode: "opencode",
282
+ cursor: "cursor",
283
+ copilot: "copilot"
228
284
  };
229
- var AGENT_TARGETS = ["all", "claude", "codex", "opencode"];
285
+ var AGENT_TARGETS = [
286
+ "all",
287
+ "claude",
288
+ "codex",
289
+ "opencode",
290
+ "cursor",
291
+ "copilot"
292
+ ];
230
293
  function selectAdapters(target, adapters) {
231
294
  if (target === "all") return adapters;
232
295
  const id = TARGET_TO_ID[target];
@@ -317,18 +380,18 @@ function createAgentCommand(deps = {}) {
317
380
  return {
318
381
  name: "agent",
319
382
  summary: "Install or uninstall harness adapters",
320
- usage: "birdybeep agent <install|uninstall> [all|claude|codex|opencode]",
383
+ usage: "birdybeep agent <install|uninstall> [all|claude|codex|opencode|cursor|copilot]",
321
384
  subcommands: [
322
385
  {
323
386
  name: "install",
324
- summary: "Install adapters (all | claude | codex | opencode)",
325
- usage: "birdybeep agent install [all|claude|codex|opencode]",
387
+ summary: "Install adapters (all | claude | codex | opencode | cursor | copilot)",
388
+ usage: "birdybeep agent install [all|claude|codex|opencode|cursor|copilot]",
326
389
  run: (ctx) => installSelected(adapters, ctx)
327
390
  },
328
391
  {
329
392
  name: "uninstall",
330
393
  summary: "Restore harness config to its pre-install state",
331
- usage: "birdybeep agent uninstall [all|claude|codex|opencode]",
394
+ usage: "birdybeep agent uninstall [all|claude|codex|opencode|cursor|copilot]",
332
395
  run: (ctx) => uninstallSelected(adapters, ctx)
333
396
  }
334
397
  ]
@@ -339,6 +402,8 @@ function createAgentCommand(deps = {}) {
339
402
  var import_agent_core4 = require("@birdybeep/agent-core");
340
403
  var import_claude_code2 = require("@birdybeep/claude-code");
341
404
  var import_codex2 = require("@birdybeep/codex");
405
+ var import_copilot2 = require("@birdybeep/copilot");
406
+ var import_cursor2 = require("@birdybeep/cursor");
342
407
  var import_opencode2 = require("@birdybeep/opencode");
343
408
 
344
409
  // src/config.ts
@@ -363,6 +428,8 @@ function writeCliConfig(patch) {
363
428
  const merged = {};
364
429
  const apiUrl = patch.apiUrl ?? current.apiUrl;
365
430
  if (apiUrl !== void 0) merged.apiUrl = apiUrl;
431
+ const expectEmail = patch.expectEmail ?? current.expectEmail;
432
+ if (expectEmail !== void 0) merged.expectEmail = expectEmail;
366
433
  (0, import_node_fs2.mkdirSync)((0, import_agent_core2.birdyBeepConfigDir)(), { recursive: true, mode: 448 });
367
434
  (0, import_node_fs2.writeFileSync)(cliConfigPath(), `${JSON.stringify(merged, null, 2)}
368
435
  `, { mode: 384 });
@@ -372,6 +439,12 @@ function resolveApiUrl() {
372
439
  if (env !== void 0 && env.length > 0) return env;
373
440
  return readCliConfig().apiUrl ?? DEFAULT_API_URL;
374
441
  }
442
+ var DEFAULT_REGISTRY_URL = "https://registry.npmjs.org";
443
+ function resolveRegistryUrl() {
444
+ const env = process.env["npm_config_registry"];
445
+ if (env !== void 0 && env.length > 0) return env;
446
+ return DEFAULT_REGISTRY_URL;
447
+ }
375
448
 
376
449
  // src/diagnostics.ts
377
450
  var import_agent_core3 = require("@birdybeep/agent-core");
@@ -395,7 +468,13 @@ function machineIdentity() {
395
468
  }
396
469
 
397
470
  // src/commands/doctor.ts
398
- var DEFAULT_ADAPTERS2 = [import_claude_code2.claudeCodeAdapter, import_codex2.codexAdapter, import_opencode2.opencodeAdapter];
471
+ var DEFAULT_ADAPTERS2 = [
472
+ import_claude_code2.claudeCodeAdapter,
473
+ import_codex2.codexAdapter,
474
+ import_opencode2.opencodeAdapter,
475
+ import_cursor2.cursorAdapter,
476
+ import_copilot2.copilotAdapter
477
+ ];
399
478
  async function defaultProbeNetwork(baseUrl) {
400
479
  try {
401
480
  const controller = new AbortController();
@@ -478,16 +557,30 @@ function createDoctorCommand(deps = {}) {
478
557
  }
479
558
 
480
559
  // src/commands/hook.ts
560
+ var import_node_child_process = require("child_process");
561
+ var import_node_crypto = require("crypto");
562
+ var import_node_fs3 = require("fs");
563
+ var import_node_os = require("os");
564
+ var import_node_path2 = require("path");
481
565
  var import_agent_core5 = require("@birdybeep/agent-core");
482
566
  var import_claude_code3 = require("@birdybeep/claude-code");
483
567
  var import_codex3 = require("@birdybeep/codex");
568
+ var import_copilot3 = require("@birdybeep/copilot");
569
+ var import_cursor3 = require("@birdybeep/cursor");
484
570
  var import_opencode3 = require("@birdybeep/opencode");
485
571
  var RUNNERS = {
486
572
  claude: import_claude_code3.runClaudeHook,
487
573
  codex: import_codex3.runCodexHook,
488
- opencode: import_opencode3.runOpenCodeHook
574
+ opencode: import_opencode3.runOpenCodeHook,
575
+ cursor: import_cursor3.runCursorHook
489
576
  };
490
- var HOOK_HARNESSES = ["claude", "codex", "opencode"];
577
+ var HOOK_HARNESSES = [
578
+ "claude",
579
+ "codex",
580
+ "opencode",
581
+ "cursor",
582
+ "copilot"
583
+ ];
491
584
  var STDIN_READ_TIMEOUT_MS = 3e3;
492
585
  function withTimeout(promise, ms, fallback) {
493
586
  return new Promise((resolve) => {
@@ -504,9 +597,13 @@ function withTimeout(promise, ms, fallback) {
504
597
  });
505
598
  }
506
599
  function isHarnessName(value) {
507
- return value === "claude" || value === "codex" || value === "opencode";
600
+ return value === "claude" || value === "codex" || value === "opencode" || value === "cursor" || value === "copilot";
508
601
  }
509
- function runHookCommand(harness, payload, sender) {
602
+ function runHookCommand(harness, payload, sender, copilotEventName) {
603
+ if (harness === "copilot") {
604
+ if (copilotEventName === void 0) return Promise.resolve({ outcome: "skipped" });
605
+ return (0, import_copilot3.runCopilotHook)(copilotEventName, payload, { sender });
606
+ }
510
607
  return RUNNERS[harness](payload, { sender });
511
608
  }
512
609
  function readStdinDefault() {
@@ -522,24 +619,90 @@ function readStdinDefault() {
522
619
  process.stdin.on("error", () => resolve(""));
523
620
  });
524
621
  }
525
- async function readHookPayload(args, readStdin) {
526
- return args[1] ?? await readStdin();
622
+ async function readHookPayload(args, readStdin, stdinOnly = false) {
623
+ return stdinOnly ? readStdin() : args[1] ?? await readStdin();
624
+ }
625
+ var NOTIFY_STDIN_FILE_ENV = "BIRDYBEEP_CODEX_NOTIFY_STDIN_FILE";
626
+ function detachCodexNotifyWorker(payload) {
627
+ if (process.platform === "win32") return false;
628
+ let file;
629
+ let fd;
630
+ try {
631
+ const birdybeep = (0, import_agent_core5.resolveOnPath)("birdybeep");
632
+ if (birdybeep === null) return false;
633
+ const tmpFile = (0, import_node_path2.join)((0, import_node_os.tmpdir)(), `birdybeep-notify-${(0, import_node_crypto.randomBytes)(16).toString("hex")}.json`);
634
+ file = tmpFile;
635
+ (0, import_node_fs3.writeFileSync)(tmpFile, payload, { mode: 384 });
636
+ fd = (0, import_node_fs3.openSync)(tmpFile, "r");
637
+ const child = (0, import_node_child_process.spawn)(birdybeep, ["hook", "codex"], {
638
+ cwd: (0, import_node_path2.dirname)(birdybeep),
639
+ // trusted dir, never the inherited/attacker cwd
640
+ detached: true,
641
+ // new session (setsid) → survives `codex exec` reaping the group
642
+ stdio: [fd, "ignore", "ignore"],
643
+ // stdin = the temp file; this process holds no pipe
644
+ env: { ...process.env, [NOTIFY_STDIN_FILE_ENV]: tmpFile },
645
+ // worker cleans it up post-read
646
+ windowsHide: true
647
+ });
648
+ child.on("error", () => {
649
+ try {
650
+ (0, import_node_fs3.rmSync)(tmpFile, { force: true });
651
+ } catch {
652
+ }
653
+ });
654
+ child.unref();
655
+ return true;
656
+ } catch {
657
+ if (file !== void 0) {
658
+ try {
659
+ (0, import_node_fs3.rmSync)(file, { force: true });
660
+ } catch {
661
+ }
662
+ }
663
+ return false;
664
+ } finally {
665
+ if (fd !== void 0) {
666
+ try {
667
+ (0, import_node_fs3.closeSync)(fd);
668
+ } catch {
669
+ }
670
+ }
671
+ }
527
672
  }
528
673
  function createHookCommand(deps = {}) {
529
674
  const makeSender = deps.createSender ?? ((baseUrl) => (0, import_agent_core5.createSender)({ baseUrl }));
530
675
  const readStdin = deps.readStdin ?? readStdinDefault;
531
676
  const stdinTimeoutMs = deps.stdinTimeoutMs ?? STDIN_READ_TIMEOUT_MS;
677
+ const detachCodexNotify = deps.detachCodexNotify ?? detachCodexNotifyWorker;
532
678
  return {
533
679
  name: "hook",
534
680
  summary: "Internal: normalize + send an event fired by a harness hook",
535
- usage: "birdybeep hook <claude|codex|opencode>",
681
+ usage: "birdybeep hook <claude|codex|opencode|cursor|copilot> [copilot-event]",
536
682
  run: async (ctx) => {
537
683
  const harness = ctx.args[0];
538
684
  if (!isHarnessName(harness)) {
539
685
  ctx.io.errline(`birdybeep hook: expected one of ${HOOK_HARNESSES.join("|")}`);
540
686
  return EXIT.USAGE;
541
687
  }
542
- const raw = await withTimeout(readHookPayload(ctx.args, readStdin), stdinTimeoutMs, "");
688
+ const notifyPayload = ctx.args[1];
689
+ if (harness === "codex" && notifyPayload !== void 0 && notifyPayload.length > 0 && detachCodexNotify(notifyPayload)) {
690
+ ctx.io.result({ harness, outcome: "detached" });
691
+ return EXIT.OK;
692
+ }
693
+ const copilotEventName = harness === "copilot" && (0, import_copilot3.isCopilotHookEventName)(ctx.args[1]) ? ctx.args[1] : void 0;
694
+ const raw = await withTimeout(
695
+ readHookPayload(ctx.args, readStdin, harness === "copilot"),
696
+ stdinTimeoutMs,
697
+ ""
698
+ );
699
+ const notifyStdinFile = process.env[NOTIFY_STDIN_FILE_ENV];
700
+ if (notifyStdinFile !== void 0 && (0, import_node_path2.dirname)(notifyStdinFile) === (0, import_node_os.tmpdir)() && (0, import_node_path2.basename)(notifyStdinFile).startsWith("birdybeep-notify-")) {
701
+ try {
702
+ (0, import_node_fs3.rmSync)(notifyStdinFile, { force: true });
703
+ } catch {
704
+ }
705
+ }
543
706
  let payload;
544
707
  try {
545
708
  payload = JSON.parse(raw);
@@ -548,8 +711,15 @@ function createHookCommand(deps = {}) {
548
711
  return EXIT.OK;
549
712
  }
550
713
  const sender = makeSender(resolveApiUrl());
551
- const result = await runHookCommand(harness, payload, sender);
552
- ctx.io.result({ harness, outcome: result.outcome, eventType: result.eventType });
714
+ const result = await runHookCommand(harness, payload, sender, copilotEventName);
715
+ ctx.io.result({
716
+ harness,
717
+ ...copilotEventName !== void 0 ? { event: copilotEventName } : {},
718
+ outcome: result.outcome,
719
+ eventType: result.eventType,
720
+ ...result.send?.decision ? { decision: result.send.decision } : {},
721
+ ...result.send?.status !== void 0 ? { status: result.send.status } : {}
722
+ });
553
723
  return EXIT.OK;
554
724
  }
555
725
  };
@@ -557,57 +727,73 @@ function createHookCommand(deps = {}) {
557
727
 
558
728
  // src/commands/logout.ts
559
729
  var import_agent_core6 = require("@birdybeep/agent-core");
560
- function createClearTokenCommand(spec, deps = {}) {
730
+ var base = (apiUrl) => apiUrl.replace(/\/$/, "");
731
+ function createLogoutCommand(deps = {}) {
561
732
  return {
562
- name: spec.name,
563
- summary: spec.summary,
564
- usage: `birdybeep ${spec.name}`,
733
+ name: "logout",
734
+ summary: "Remove the local machine token (does NOT revoke the machine server-side)",
735
+ usage: "birdybeep logout",
565
736
  run: async (ctx) => {
566
737
  await (0, import_agent_core6.clearToken)(deps.tokenOptions ?? {});
567
- ctx.io.emit(spec.humanMessage, { [spec.jsonKey]: true });
738
+ ctx.io.emit("Logged out \u2014 the machine token was removed.", { loggedOut: true });
568
739
  return EXIT.OK;
569
740
  }
570
741
  };
571
742
  }
572
- function createLogoutCommand(deps = {}) {
573
- return createClearTokenCommand(
574
- {
575
- name: "logout",
576
- summary: "Remove the local machine token (same as `unpair`)",
577
- humanMessage: "Logged out \u2014 the machine token was removed.",
578
- jsonKey: "loggedOut"
579
- },
580
- deps
581
- );
743
+ async function revokeSelf(token, fetchImpl, timeoutMs) {
744
+ const controller = new AbortController();
745
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
746
+ try {
747
+ const res = await fetchImpl(`${base(resolveApiUrl())}/v1/machine/revoke-self`, {
748
+ method: "POST",
749
+ headers: { authorization: `Bearer ${token}` },
750
+ signal: controller.signal
751
+ });
752
+ if (res.ok || res.status === 403) return "revoked";
753
+ return "rejected";
754
+ } catch {
755
+ return "unreachable";
756
+ } finally {
757
+ clearTimeout(timer);
758
+ }
582
759
  }
583
760
  function createUnpairCommand(deps = {}) {
584
- return createClearTokenCommand(
585
- {
586
- name: "unpair",
587
- summary: "Unpair this machine \u2014 remove the local machine token (same as `logout`)",
588
- humanMessage: "Unpaired \u2014 the machine token was removed.",
589
- jsonKey: "unpaired"
590
- },
591
- deps
592
- );
761
+ const fetchImpl = deps.fetchImpl ?? fetch;
762
+ const timeoutMs = deps.timeoutMs ?? 1e4;
763
+ return {
764
+ name: "unpair",
765
+ summary: "Unpair this machine \u2014 revoke it server-side and remove the local token",
766
+ usage: "birdybeep unpair",
767
+ run: async (ctx) => {
768
+ const token = await (0, import_agent_core6.getToken)(deps.tokenOptions ?? {});
769
+ const outcome = token === null ? "no_token" : await revokeSelf(token, fetchImpl, timeoutMs);
770
+ await (0, import_agent_core6.clearToken)(deps.tokenOptions ?? {});
771
+ const serverRevoked = outcome === "revoked";
772
+ const human = outcome === "revoked" ? "Unpaired \u2014 the machine was revoked and removed from your account." : outcome === "no_token" ? "Already unpaired \u2014 there was no local token to remove." : outcome === "unreachable" ? "Unpaired locally, but the server was unreachable \u2014 the machine may still show in the app. Open BirdyBeep and revoke it there to fully remove it." : "Unpaired locally, but the server didn't confirm removal \u2014 if the machine still shows in the app, revoke it there.";
773
+ ctx.io.emit(human, { unpaired: true, serverRevoked });
774
+ return EXIT.OK;
775
+ }
776
+ };
593
777
  }
594
778
 
595
779
  // src/commands/pair.ts
780
+ var import_node_fs4 = require("fs");
596
781
  var import_agent_core8 = require("@birdybeep/agent-core");
597
782
  var import_uqr = require("uqr");
598
783
 
599
784
  // src/pairing.ts
600
785
  var import_agent_core7 = require("@birdybeep/agent-core");
601
- function base(apiUrl) {
786
+ function base2(apiUrl) {
602
787
  return apiUrl.replace(/\/$/, "");
603
788
  }
604
789
  async function pairStart(apiUrl, input, fetchImpl) {
605
790
  const body = {
606
791
  machine_label: input.machineLabel,
607
792
  ...input.os !== void 0 ? { os: input.os } : {},
608
- ...input.cliVersion !== void 0 ? { cli_version: input.cliVersion } : {}
793
+ ...input.cliVersion !== void 0 ? { cli_version: input.cliVersion } : {},
794
+ ...input.codeChallenge !== void 0 ? { code_challenge: input.codeChallenge } : {}
609
795
  };
610
- const res = await fetchImpl(`${base(apiUrl)}/v1/pair/start`, {
796
+ const res = await fetchImpl(`${base2(apiUrl)}/v1/pair/start`, {
611
797
  method: "POST",
612
798
  headers: { "content-type": "application/json" },
613
799
  body: JSON.stringify(body)
@@ -625,12 +811,13 @@ var TERMINAL_TOKEN_ERRORS = /* @__PURE__ */ new Set([
625
811
  "not_found",
626
812
  "payload_too_large"
627
813
  ]);
628
- async function pairTokenPoll(apiUrl, deviceCode, fetchImpl, machineFingerprint) {
814
+ async function pairTokenPoll(apiUrl, deviceCode, fetchImpl, machineFingerprint, codeVerifier) {
629
815
  const body = {
630
816
  device_code: deviceCode,
631
- ...machineFingerprint !== void 0 ? { machine_fingerprint: machineFingerprint } : {}
817
+ ...machineFingerprint !== void 0 ? { machine_fingerprint: machineFingerprint } : {},
818
+ ...codeVerifier !== void 0 ? { code_verifier: codeVerifier } : {}
632
819
  };
633
- const res = await fetchImpl(`${base(apiUrl)}/v1/pair/token`, {
820
+ const res = await fetchImpl(`${base2(apiUrl)}/v1/pair/token`, {
634
821
  method: "POST",
635
822
  headers: { "content-type": "application/json" },
636
823
  body: JSON.stringify(body)
@@ -641,7 +828,10 @@ async function pairTokenPoll(apiUrl, deviceCode, fetchImpl, machineFingerprint)
641
828
  return {
642
829
  status: "paired",
643
830
  machineToken: parsed.data.machine_token,
644
- machineId: parsed.data.machine_id
831
+ machineId: parsed.data.machine_id,
832
+ // Only surface the key when the server reported it (exactOptionalPropertyTypes: no explicit
833
+ // undefined). Older servers omit approved_by_email; newer ones (dgxd) include it.
834
+ ...parsed.data.approved_by_email !== void 0 ? { approvedByEmail: parsed.data.approved_by_email } : {}
645
835
  };
646
836
  }
647
837
  let errBody = null;
@@ -662,7 +852,7 @@ async function pairTokenPoll(apiUrl, deviceCode, fetchImpl, machineFingerprint)
662
852
  }
663
853
 
664
854
  // src/version.ts
665
- var CLI_VERSION = "0.1.0".length > 0 ? "0.1.0" : "0.0.0";
855
+ var CLI_VERSION = "0.3.0".length > 0 ? "0.3.0" : "0.0.0";
666
856
 
667
857
  // src/commands/pair.ts
668
858
  var DEFAULT_POLL_INTERVAL_MS = 2e3;
@@ -670,22 +860,172 @@ var HEARTBEAT_MS = 15e3;
670
860
  function renderQrMatrix(qrPayload) {
671
861
  return (0, import_uqr.renderUnicodeCompact)(qrPayload, { border: 2 });
672
862
  }
863
+ function parsePairFlags(args) {
864
+ const flags = { yes: false };
865
+ for (let i = 0; i < args.length; i += 1) {
866
+ const token = args[i] ?? "";
867
+ if (token === "--yes" || token === "-y") {
868
+ flags.yes = true;
869
+ } else if (token === "--expect-email" || token.startsWith("--expect-email=")) {
870
+ const inline = token.startsWith("--expect-email=") ? token.slice("--expect-email=".length) : void 0;
871
+ const value = inline ?? args[++i];
872
+ if (value === void 0 || value.length === 0 || value.startsWith("-")) {
873
+ return { ...flags, error: "--expect-email requires an email address" };
874
+ }
875
+ flags.expectEmail = value;
876
+ } else {
877
+ return { ...flags, error: `unexpected argument "${token}"` };
878
+ }
879
+ }
880
+ return flags;
881
+ }
882
+ function sameEmail(a, b) {
883
+ const fold = (v) => v.trim().toLowerCase();
884
+ const rawEqual = fold(a) === fold(b);
885
+ const nfkcEqual = fold(a.normalize("NFKC")) === fold(b.normalize("NFKC"));
886
+ return rawEqual && nfkcEqual;
887
+ }
888
+ function decidePairConfirmation(input) {
889
+ const { approvedByEmail, expectEmail } = input;
890
+ const platform = input.platform ?? process.platform;
891
+ if (expectEmail !== void 0) {
892
+ if (approvedByEmail === void 0) {
893
+ const remedy = input.expectEmailSource === "config" ? `Remove or correct the "expectEmail" key in ${input.configPath ?? "the BirdyBeep CLI config"} (or upgrade the backend to one that reports the approving account) and re-run.` : "Re-run without --expect-email (and confirm interactively) if that is expected.";
894
+ return {
895
+ action: "reject",
896
+ reason: "expected_email_unverifiable",
897
+ message: `Pairing refused: ${expectEmail} was pinned as the expected approving account, but the server did not report which account approved this machine, so the pin could not be verified. The machine token was NOT stored. ${remedy}`
898
+ };
899
+ }
900
+ if (sameEmail(approvedByEmail, expectEmail)) {
901
+ return { action: "approve", reason: "expected_email_match" };
902
+ }
903
+ return {
904
+ action: "reject",
905
+ reason: "expected_email_mismatch",
906
+ message: `Pairing refused: this machine was approved by ${approvedByEmail}, but ${expectEmail} was expected. The machine token was NOT stored. If you did not expect that account to approve it, open BirdyBeep and revoke the machine, then re-run \`birdybeep pair\`.`
907
+ };
908
+ }
909
+ if (input.yes) return { action: "approve", reason: "yes_flag" };
910
+ const question = approvedByEmail !== void 0 ? `Pair this machine to ${approvedByEmail}? [y/N] ` : "The server did not report which account approved this machine. Pair anyway? [y/N] ";
911
+ if (!input.nonInteractive) {
912
+ if (input.stdinIsTTY) return { action: "prompt", question, on: "stdin" };
913
+ if (input.controllingTerminalAvailable) {
914
+ return { action: "prompt", question, on: "controlling-terminal" };
915
+ }
916
+ }
917
+ const who = approvedByEmail !== void 0 ? ` (approved by ${approvedByEmail})` : "";
918
+ const winptyHint = platform === "win32" && !input.nonInteractive ? " In Git Bash / MSYS, `winpty birdybeep pair` attaches a real console so the prompt can appear." : "";
919
+ return {
920
+ action: "reject",
921
+ reason: "non_interactive",
922
+ message: `Pairing needs confirmation${who}, but there is no terminal to ask on, so the machine token was NOT stored. Re-run with \`--expect-email <addr>\` to pin the approving account (recommended for CI), or \`--yes\` to accept whichever account approved it.` + winptyHint
923
+ };
924
+ }
925
+ function isAffirmative(answer) {
926
+ return /^(y|yes)$/i.test(answer.trim());
927
+ }
928
+ function controllingTerminalPath() {
929
+ return "/dev/tty";
930
+ }
931
+ function canOpenControllingTerminal(path = controllingTerminalPath(), platform = process.platform) {
932
+ if (platform === "win32") return false;
933
+ let fd;
934
+ try {
935
+ fd = (0, import_node_fs4.openSync)(path, "r");
936
+ return true;
937
+ } catch {
938
+ return false;
939
+ } finally {
940
+ if (fd !== void 0) {
941
+ try {
942
+ (0, import_node_fs4.closeSync)(fd);
943
+ } catch {
944
+ }
945
+ }
946
+ }
947
+ }
948
+ async function promptForAnswer(question, on) {
949
+ const { createInterface } = await import("readline/promises");
950
+ let ttyFd;
951
+ let input;
952
+ if (on === "stdin") {
953
+ input = process.stdin;
954
+ } else {
955
+ const { ReadStream } = await import("tty");
956
+ ttyFd = (0, import_node_fs4.openSync)(controllingTerminalPath(), "r");
957
+ input = new ReadStream(ttyFd);
958
+ }
959
+ return new Promise((resolve) => {
960
+ const rl = createInterface({ input, output: process.stderr });
961
+ let settled = false;
962
+ const done = (value) => {
963
+ if (settled) return;
964
+ settled = true;
965
+ rl.close();
966
+ if (on === "stdin") {
967
+ process.stdin.unref?.();
968
+ } else {
969
+ try {
970
+ input.unref?.();
971
+ input.destroy?.();
972
+ } catch {
973
+ }
974
+ if (ttyFd !== void 0) {
975
+ try {
976
+ (0, import_node_fs4.closeSync)(ttyFd);
977
+ } catch {
978
+ }
979
+ }
980
+ }
981
+ resolve(value);
982
+ };
983
+ rl.question(question).then(done, () => done(""));
984
+ rl.once("close", () => done(""));
985
+ input.once?.("error", () => done(""));
986
+ });
987
+ }
673
988
  function createPairCommand(deps = {}) {
674
989
  const fetchImpl = deps.fetchImpl ?? fetch;
675
990
  const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
676
991
  const clock = deps.now ?? (() => Date.now());
677
992
  const renderQr = deps.renderQr ?? renderQrMatrix;
678
993
  const intervalMs = deps.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
994
+ const promptLine = deps.promptLine ?? promptForAnswer;
995
+ const hasControllingTerminal = deps.hasControllingTerminal ?? (() => canOpenControllingTerminal());
996
+ const configuredExpectEmail = deps.configuredExpectEmail ?? (() => {
997
+ const pinned = readCliConfig().expectEmail;
998
+ return typeof pinned === "string" && pinned.trim().length > 0 ? pinned : void 0;
999
+ });
679
1000
  return {
680
1001
  name: "pair",
681
1002
  summary: "Pair this machine with your BirdyBeep account (QR or manual)",
682
- usage: "birdybeep pair [--json]",
1003
+ usage: "birdybeep pair [--yes] [--expect-email <addr>] [--json]",
1004
+ options: [
1005
+ {
1006
+ flag: "--yes",
1007
+ aliases: ["-y"],
1008
+ summary: "Skip the approving-account confirmation (headless/CI)"
1009
+ },
1010
+ {
1011
+ flag: "--expect-email",
1012
+ value: "<addr>",
1013
+ summary: "Only trust the pairing if this account approved it (else fail)"
1014
+ }
1015
+ ],
683
1016
  run: async (ctx) => {
1017
+ const pairFlags = parsePairFlags(ctx.args);
1018
+ if (pairFlags.error !== void 0) {
1019
+ ctx.io.errline(`birdybeep pair: ${pairFlags.error}.`);
1020
+ return EXIT.USAGE;
1021
+ }
684
1022
  const apiUrl = resolveApiUrl();
685
1023
  const identity = (0, import_agent_core8.getMachineIdentity)();
1024
+ const codeVerifier = (0, import_agent_core8.generateCodeVerifier)();
1025
+ const codeChallenge = (0, import_agent_core8.deriveCodeChallengeS256)(codeVerifier);
686
1026
  const start = await pairStart(
687
1027
  apiUrl,
688
- { machineLabel: identity.label, os: identity.os, cliVersion: CLI_VERSION },
1028
+ { machineLabel: identity.label, os: identity.os, cliVersion: CLI_VERSION, codeChallenge },
689
1029
  fetchImpl
690
1030
  );
691
1031
  if (ctx.flags.json) {
@@ -697,12 +1037,14 @@ function createPairCommand(deps = {}) {
697
1037
  });
698
1038
  } else {
699
1039
  ctx.io.line(
700
- "To pair this machine, open the BirdyBeep app, tap \u201Cpair a machine\u201D, and scan this QR (or enter the code):"
1040
+ "To pair this machine, open the BirdyBeep app, tap \u201Cpair a machine\u201D, and scan this QR or open the complete link:"
701
1041
  );
702
1042
  const isTTY = deps.isTTY ?? process.stdout.isTTY === true;
703
1043
  if (isTTY) ctx.io.line(renderQr(start.qr_payload));
704
1044
  ctx.io.line(` Scan or open: ${start.qr_payload}`);
705
- ctx.io.line(` Code: ${start.user_code}`);
1045
+ ctx.io.line(
1046
+ ` Session code (display only; cannot approve by itself): ${start.user_code}`
1047
+ );
706
1048
  ctx.io.line("Waiting for you to approve this machine in the app\u2026");
707
1049
  }
708
1050
  const deadline = Date.parse(start.expires_at);
@@ -718,7 +1060,9 @@ function createPairCommand(deps = {}) {
718
1060
  apiUrl,
719
1061
  start.device_code,
720
1062
  fetchImpl,
721
- identity.fingerprintHash
1063
+ identity.fingerprintHash,
1064
+ codeVerifier
1065
+ // PKCE proof-of-possession (dgxd) — sent on every poll
722
1066
  );
723
1067
  if (poll.status === "paired") {
724
1068
  paired = poll;
@@ -743,15 +1087,44 @@ function createPairCommand(deps = {}) {
743
1087
  if (paired === void 0 || paired.status !== "paired") {
744
1088
  ctx.io.result({ paired: false, reason: "timeout" });
745
1089
  ctx.io.errline(
746
- "Pairing timed out before you approved it. In the BirdyBeep app, tap \u201Cpair a machine\u201D, scan the QR (or enter the code), then run `birdybeep pair` again."
1090
+ "Pairing timed out before you approved it. In the BirdyBeep app, tap \u201Cpair a machine\u201D, scan a fresh QR or open its complete link, then run `birdybeep pair` again."
1091
+ );
1092
+ return EXIT.ERROR;
1093
+ }
1094
+ const approvedBy = paired.approvedByEmail;
1095
+ const expectEmail = pairFlags.expectEmail ?? configuredExpectEmail();
1096
+ const stdinIsTTY = deps.isStdinTTY ?? process.stdin.isTTY === true;
1097
+ const decision = decidePairConfirmation({
1098
+ ...approvedBy !== void 0 ? { approvedByEmail: approvedBy } : {},
1099
+ ...expectEmail !== void 0 ? { expectEmail } : {},
1100
+ ...expectEmail !== void 0 ? { expectEmailSource: pairFlags.expectEmail !== void 0 ? "flag" : "config" } : {},
1101
+ yes: pairFlags.yes,
1102
+ nonInteractive: ctx.flags.nonInteractive,
1103
+ stdinIsTTY,
1104
+ // Probed ONLY when stdin can't answer — opening /dev/tty is a syscall, and when stdin is
1105
+ // already a terminal the answer is irrelevant.
1106
+ controllingTerminalAvailable: stdinIsTTY ? false : hasControllingTerminal(),
1107
+ configPath: cliConfigPath()
1108
+ });
1109
+ if (decision.action === "reject") {
1110
+ ctx.io.result({ paired: false, reason: decision.reason });
1111
+ ctx.io.errline(decision.message);
1112
+ return EXIT.ERROR;
1113
+ }
1114
+ if (decision.action === "prompt" && !isAffirmative(await promptLine(decision.question, decision.on))) {
1115
+ ctx.io.result({ paired: false, reason: "declined" });
1116
+ ctx.io.errline(
1117
+ "Pairing declined \u2014 the machine token was NOT stored, and this machine will send no events. The machine may still appear in the BirdyBeep app; revoke it there if you did not intend to pair it."
747
1118
  );
748
1119
  return EXIT.ERROR;
749
1120
  }
750
1121
  await (0, import_agent_core8.setToken)(paired.machineToken, deps.tokenOptions ?? {});
751
1122
  writeCliConfig({ apiUrl });
752
- ctx.io.emit(`\u2713 Paired. Run \`birdybeep test\` to send a test Beep.`, {
1123
+ const humanSuffix = approvedBy !== void 0 ? ` to ${approvedBy}` : "";
1124
+ ctx.io.emit(`\u2713 Paired${humanSuffix}. Run \`birdybeep test\` to send a test Beep.`, {
753
1125
  paired: true,
754
- machineId: paired.machineId
1126
+ machineId: paired.machineId,
1127
+ ...approvedBy !== void 0 ? { approvedByEmail: approvedBy } : {}
755
1128
  });
756
1129
  return EXIT.OK;
757
1130
  }
@@ -784,14 +1157,24 @@ function createQueueCommand() {
784
1157
  var import_agent_core10 = require("@birdybeep/agent-core");
785
1158
  var import_claude_code4 = require("@birdybeep/claude-code");
786
1159
  var import_codex4 = require("@birdybeep/codex");
1160
+ var import_copilot4 = require("@birdybeep/copilot");
1161
+ var import_cursor4 = require("@birdybeep/cursor");
787
1162
  var import_opencode4 = require("@birdybeep/opencode");
788
- var DEFAULT_ADAPTERS3 = [import_claude_code4.claudeCodeAdapter, import_codex4.codexAdapter, import_opencode4.opencodeAdapter];
1163
+ var DEFAULT_ADAPTERS3 = [
1164
+ import_claude_code4.claudeCodeAdapter,
1165
+ import_codex4.codexAdapter,
1166
+ import_opencode4.opencodeAdapter,
1167
+ import_cursor4.cursorAdapter,
1168
+ import_copilot4.copilotAdapter
1169
+ ];
789
1170
  var ADAPTER_VERSIONS = {
790
1171
  claude_code: import_claude_code4.CLAUDE_CODE_ADAPTER_VERSION,
791
1172
  codex: import_codex4.CODEX_ADAPTER_VERSION,
792
- opencode: import_opencode4.OPENCODE_ADAPTER_VERSION
1173
+ opencode: import_opencode4.OPENCODE_ADAPTER_VERSION,
1174
+ cursor: import_cursor4.CURSOR_ADAPTER_VERSION,
1175
+ copilot: import_copilot4.COPILOT_ADAPTER_VERSION
793
1176
  };
794
- var base2 = (apiUrl) => apiUrl.replace(/\/$/, "");
1177
+ var base3 = (apiUrl) => apiUrl.replace(/\/$/, "");
795
1178
  async function gatherItems(adapters) {
796
1179
  return Promise.all(
797
1180
  adapters.map(async (a) => {
@@ -826,7 +1209,7 @@ function createReportStatusCommand(deps = {}) {
826
1209
  let outcome = "deferred";
827
1210
  let errorCode;
828
1211
  try {
829
- const res = await fetchImpl(`${base2(resolveApiUrl())}/v1/integrations/status`, {
1212
+ const res = await fetchImpl(`${base3(resolveApiUrl())}/v1/integrations/status`, {
830
1213
  method: "POST",
831
1214
  headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
832
1215
  body: JSON.stringify({ integrations: items })
@@ -877,8 +1260,16 @@ function createReportStatusCommand(deps = {}) {
877
1260
  var import_agent_core11 = require("@birdybeep/agent-core");
878
1261
  var import_claude_code5 = require("@birdybeep/claude-code");
879
1262
  var import_codex5 = require("@birdybeep/codex");
1263
+ var import_copilot5 = require("@birdybeep/copilot");
1264
+ var import_cursor5 = require("@birdybeep/cursor");
880
1265
  var import_opencode5 = require("@birdybeep/opencode");
881
- var DEFAULT_ADAPTERS4 = [import_claude_code5.claudeCodeAdapter, import_codex5.codexAdapter, import_opencode5.opencodeAdapter];
1266
+ var DEFAULT_ADAPTERS4 = [
1267
+ import_claude_code5.claudeCodeAdapter,
1268
+ import_codex5.codexAdapter,
1269
+ import_opencode5.opencodeAdapter,
1270
+ import_cursor5.cursorAdapter,
1271
+ import_copilot5.copilotAdapter
1272
+ ];
882
1273
  function createStatusCommand(deps = {}) {
883
1274
  const adapters = deps.adapters ?? DEFAULT_ADAPTERS4;
884
1275
  const makeSender = deps.createSender ?? ((baseUrl) => (0, import_agent_core11.createSender)(
@@ -918,7 +1309,7 @@ function createStatusCommand(deps = {}) {
918
1309
  }
919
1310
 
920
1311
  // src/commands/test.ts
921
- var import_node_crypto = require("crypto");
1312
+ var import_node_crypto2 = require("crypto");
922
1313
  var import_agent_core12 = require("@birdybeep/agent-core");
923
1314
  function buildTestEvent(opts = {}) {
924
1315
  const machine = (0, import_agent_core12.getMachineIdentity)();
@@ -930,7 +1321,7 @@ function buildTestEvent(opts = {}) {
930
1321
  // schema requires a harness; the "test" type distinguishes it
931
1322
  // Unique per run: a repeat `birdybeep test` inside the backend's dedupe window must
932
1323
  // still beep — a constant id made the second test silently "deduped" (9fh).
933
- source_session_id: `birdybeep-cli-test-${(0, import_node_crypto.randomUUID)()}`,
1324
+ source_session_id: `birdybeep-cli-test-${(0, import_node_crypto2.randomUUID)()}`,
934
1325
  machine: { label: machine.label, os: machine.os },
935
1326
  workspace: { cwd: process.cwd() },
936
1327
  title: "BirdyBeep test event",
@@ -999,13 +1390,152 @@ function buildCommands() {
999
1390
  ];
1000
1391
  }
1001
1392
 
1393
+ // src/update-check.ts
1394
+ var import_node_fs5 = require("fs");
1395
+ var import_node_path3 = require("path");
1396
+ var import_agent_core13 = require("@birdybeep/agent-core");
1397
+ var PACKAGE_NAME = "@birdybeep/cli";
1398
+ var PACKAGE_PATH = "@birdybeep%2Fcli";
1399
+ var UPDATE_CACHE_FILE = "update-check.json";
1400
+ var DEFAULT_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
1401
+ var DEFAULT_TIMEOUT_MS = 1500;
1402
+ var SKIP_COMMANDS = /* @__PURE__ */ new Set(["hook", "report-status"]);
1403
+ var SEMVER_RE = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
1404
+ function parseSemver(input) {
1405
+ const m = SEMVER_RE.exec(input.trim());
1406
+ if (m === null) return null;
1407
+ return {
1408
+ major: Number(m[1]),
1409
+ minor: Number(m[2]),
1410
+ patch: Number(m[3]),
1411
+ prerelease: m[4] !== void 0 ? m[4].split(".") : []
1412
+ };
1413
+ }
1414
+ function comparePrerelease(a, b) {
1415
+ if (a.length === 0 && b.length === 0) return 0;
1416
+ if (a.length === 0) return 1;
1417
+ if (b.length === 0) return -1;
1418
+ const len = Math.min(a.length, b.length);
1419
+ for (let i = 0; i < len; i++) {
1420
+ const ai = a[i];
1421
+ const bi = b[i];
1422
+ const aNum = /^\d+$/.test(ai);
1423
+ const bNum = /^\d+$/.test(bi);
1424
+ if (aNum && bNum) {
1425
+ const d = Number(ai) - Number(bi);
1426
+ if (d !== 0) return d < 0 ? -1 : 1;
1427
+ } else if (aNum) {
1428
+ return -1;
1429
+ } else if (bNum) {
1430
+ return 1;
1431
+ } else if (ai !== bi) {
1432
+ return ai < bi ? -1 : 1;
1433
+ }
1434
+ }
1435
+ if (a.length === b.length) return 0;
1436
+ return a.length < b.length ? -1 : 1;
1437
+ }
1438
+ function compareSemver(a, b) {
1439
+ if (a.major !== b.major) return a.major < b.major ? -1 : 1;
1440
+ if (a.minor !== b.minor) return a.minor < b.minor ? -1 : 1;
1441
+ if (a.patch !== b.patch) return a.patch < b.patch ? -1 : 1;
1442
+ return comparePrerelease(a.prerelease, b.prerelease);
1443
+ }
1444
+ function isNewer(current, latest) {
1445
+ const cur = parseSemver(current);
1446
+ const lat = parseSemver(latest);
1447
+ return cur !== null && lat !== null && compareSemver(cur, lat) < 0;
1448
+ }
1449
+ function updateCachePath() {
1450
+ return (0, import_node_path3.join)((0, import_agent_core13.birdyBeepConfigDir)(), UPDATE_CACHE_FILE);
1451
+ }
1452
+ function readUpdateCache() {
1453
+ try {
1454
+ const parsed = JSON.parse((0, import_node_fs5.readFileSync)(updateCachePath(), "utf8"));
1455
+ if (typeof parsed !== "object" || parsed === null) return null;
1456
+ const { checkedAt, latest } = parsed;
1457
+ if (typeof checkedAt !== "number") return null;
1458
+ if (latest !== null && typeof latest !== "string") return null;
1459
+ return { checkedAt, latest };
1460
+ } catch {
1461
+ return null;
1462
+ }
1463
+ }
1464
+ function writeUpdateCache(cache) {
1465
+ (0, import_node_fs5.mkdirSync)((0, import_agent_core13.birdyBeepConfigDir)(), { recursive: true, mode: 448 });
1466
+ (0, import_node_fs5.writeFileSync)(updateCachePath(), `${JSON.stringify(cache)}
1467
+ `, { mode: 384 });
1468
+ }
1469
+ async function fetchLatestVersion(registryUrl, fetchImpl, timeoutMs) {
1470
+ const url = `${registryUrl.replace(/\/+$/, "")}/${PACKAGE_PATH}/latest`;
1471
+ const controller = new AbortController();
1472
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
1473
+ if (typeof timer.unref === "function") timer.unref();
1474
+ try {
1475
+ const res = await fetchImpl(url, {
1476
+ headers: { accept: "application/json" },
1477
+ signal: controller.signal
1478
+ });
1479
+ if (!res.ok) throw new Error(`registry responded ${res.status}`);
1480
+ const body = await res.json();
1481
+ if (typeof body.version !== "string" || body.version.length === 0) {
1482
+ throw new Error("registry response had no version");
1483
+ }
1484
+ return body.version;
1485
+ } finally {
1486
+ clearTimeout(timer);
1487
+ }
1488
+ }
1489
+ function renderNotice(current, latest) {
1490
+ return `a new version of birdybeep is available: ${current} \u2192 ${latest}
1491
+ upgrade with: npm install -g ${PACKAGE_NAME}@latest`;
1492
+ }
1493
+ async function maybeNotifyUpdate(opts) {
1494
+ try {
1495
+ if (opts.command !== void 0 && SKIP_COMMANDS.has(opts.command)) return;
1496
+ if (opts.flags.json || opts.flags.nonInteractive) return;
1497
+ const env = opts.env ?? process.env;
1498
+ if (env["BIRDYBEEP_NO_UPDATE_NOTIFIER"] || env["NO_UPDATE_NOTIFIER"] || env["CI"]) return;
1499
+ const isTTY = opts.isTTY ?? Boolean(process.stderr.isTTY);
1500
+ if (!isTTY) return;
1501
+ const current = opts.currentVersion ?? CLI_VERSION;
1502
+ const now = opts.now ?? Date.now();
1503
+ const intervalMs = opts.intervalMs ?? DEFAULT_CHECK_INTERVAL_MS;
1504
+ const readCache = opts.readCache ?? readUpdateCache;
1505
+ const writeCache = opts.writeCache ?? writeUpdateCache;
1506
+ let cache = readCache();
1507
+ if (cache === null || now - cache.checkedAt >= intervalMs) {
1508
+ let latest = cache?.latest ?? null;
1509
+ try {
1510
+ latest = await fetchLatestVersion(
1511
+ opts.registryUrl ?? resolveRegistryUrl(),
1512
+ opts.fetchImpl ?? fetch,
1513
+ opts.timeoutMs ?? DEFAULT_TIMEOUT_MS
1514
+ );
1515
+ } catch {
1516
+ }
1517
+ cache = { checkedAt: now, latest };
1518
+ try {
1519
+ writeCache(cache);
1520
+ } catch {
1521
+ }
1522
+ }
1523
+ if (cache.latest !== null && isNewer(current, cache.latest)) {
1524
+ opts.io.errline(renderNotice(current, cache.latest));
1525
+ }
1526
+ } catch {
1527
+ }
1528
+ }
1529
+
1002
1530
  // src/cli.ts
1003
1531
  function runCli(argv, deps = {}) {
1532
+ const notifyUpdate = deps.updateCheck === false ? void 0 : (ctx) => maybeNotifyUpdate({ ...ctx, ...deps.updateCheck ?? {} });
1004
1533
  return dispatch(argv, {
1005
1534
  version: CLI_VERSION,
1006
1535
  commands: deps.commands ?? buildCommands(),
1007
1536
  stdout: deps.stdout ?? process.stdout,
1008
1537
  stderr: deps.stderr ?? process.stderr,
1538
+ ...notifyUpdate !== void 0 ? { notifyUpdate } : {},
1009
1539
  ...deps.ensureConfig !== void 0 ? { ensureConfig: deps.ensureConfig } : {}
1010
1540
  });
1011
1541
  }