@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/bin.cjs CHANGED
@@ -1,9 +1,33 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
3
25
 
4
26
  // src/commands/agent.ts
5
27
  var import_claude_code = require("@birdybeep/claude-code");
6
28
  var import_codex = require("@birdybeep/codex");
29
+ var import_copilot = require("@birdybeep/copilot");
30
+ var import_cursor = require("@birdybeep/cursor");
7
31
  var import_opencode = require("@birdybeep/opencode");
8
32
 
9
33
  // src/framework.ts
@@ -72,8 +96,11 @@ function parseGlobalFlags(argv) {
72
96
  }
73
97
  return { flags, rest };
74
98
  }
75
- function isUnknownFlag(token) {
76
- return token.startsWith("-") && !GLOBAL_FLAG_TOKENS.has(token);
99
+ function isUnknownFlag(token, allowed) {
100
+ if (!token.startsWith("-")) return false;
101
+ if (GLOBAL_FLAG_TOKENS.has(token)) return false;
102
+ const eq = token.indexOf("=");
103
+ return !allowed.has(eq >= 0 ? token.slice(0, eq) : token);
77
104
  }
78
105
  function renderRootHelp(version, commands) {
79
106
  const width = Math.max(...commands.map((c) => c.name.length));
@@ -94,6 +121,16 @@ function renderRootHelp(version, commands) {
94
121
  " -v, --version Show the CLI version"
95
122
  ].join("\n");
96
123
  }
124
+ function commandFlagTokens(...commands) {
125
+ const tokens = /* @__PURE__ */ new Set();
126
+ for (const command of commands) {
127
+ for (const option of command?.options ?? []) {
128
+ tokens.add(option.flag);
129
+ for (const alias of option.aliases ?? []) tokens.add(alias);
130
+ }
131
+ }
132
+ return tokens;
133
+ }
97
134
  function renderCommandHelp(path, command) {
98
135
  const lines = [
99
136
  `birdybeep ${path} \u2014 ${command.summary}`,
@@ -101,6 +138,17 @@ function renderCommandHelp(path, command) {
101
138
  "Usage:",
102
139
  ` ${command.usage ?? `birdybeep ${path} [options]`}`
103
140
  ];
141
+ if (command.options && command.options.length > 0) {
142
+ const labels = command.options.map(
143
+ (o) => `${[o.flag, ...o.aliases ?? []].join(", ")}${o.value ? ` ${o.value}` : ""}`
144
+ );
145
+ const width = Math.max(...labels.map((l) => l.length));
146
+ lines.push(
147
+ "",
148
+ "Options:",
149
+ ...command.options.map((o, i) => ` ${labels[i]?.padEnd(width)} ${o.summary}`)
150
+ );
151
+ }
104
152
  if (command.subcommands && command.subcommands.length > 0) {
105
153
  const width = Math.max(...command.subcommands.map((c) => c.name.length));
106
154
  lines.push(
@@ -125,6 +173,7 @@ async function dispatch(argv, deps) {
125
173
  return EXIT.OK;
126
174
  }
127
175
  let command = deps.commands.find((c) => c.name === rest[0]);
176
+ let parent;
128
177
  const pathParts = [];
129
178
  let argsStart = 1;
130
179
  if (command) {
@@ -132,6 +181,7 @@ async function dispatch(argv, deps) {
132
181
  if (command.subcommands && command.subcommands.length > 0) {
133
182
  const sub = command.subcommands.find((c) => c.name === rest[1]);
134
183
  if (sub) {
184
+ parent = command;
135
185
  command = sub;
136
186
  pathParts.push(sub.name);
137
187
  argsStart = 2;
@@ -155,6 +205,7 @@ async function dispatch(argv, deps) {
155
205
  name: path,
156
206
  summary: command.summary,
157
207
  usage: command.usage,
208
+ options: command.options,
158
209
  subcommands: command.subcommands?.map((c) => ({ name: c.name, summary: c.summary }))
159
210
  });
160
211
  return EXIT.OK;
@@ -164,13 +215,15 @@ async function dispatch(argv, deps) {
164
215
  return EXIT.USAGE;
165
216
  }
166
217
  const args = rest.slice(argsStart);
167
- const unknown = args.find(isUnknownFlag);
218
+ const allowed = commandFlagTokens(command, parent);
219
+ const unknown = args.find((token) => isUnknownFlag(token, allowed));
168
220
  if (unknown !== void 0) {
169
221
  io.errline(`birdybeep ${path}: unknown option "${unknown}".`);
170
222
  return EXIT.USAGE;
171
223
  }
224
+ let code;
172
225
  try {
173
- return await command.run({ args, flags, io });
226
+ code = await command.run({ args, flags, io });
174
227
  } catch (err) {
175
228
  if (err instanceof MissingInputError) {
176
229
  io.errline(
@@ -181,16 +234,38 @@ async function dispatch(argv, deps) {
181
234
  io.errline(`birdybeep ${path}: ${err instanceof Error ? err.message : String(err)}`);
182
235
  return EXIT.ERROR;
183
236
  }
237
+ if (deps.notifyUpdate !== void 0) {
238
+ try {
239
+ await deps.notifyUpdate({ command: pathParts[0] ?? "", flags, io });
240
+ } catch {
241
+ }
242
+ }
243
+ return code;
184
244
  }
185
245
 
186
246
  // src/commands/agent.ts
187
- var DEFAULT_ADAPTERS = [import_claude_code.claudeCodeAdapter, import_codex.codexAdapter, import_opencode.opencodeAdapter];
247
+ var DEFAULT_ADAPTERS = [
248
+ import_claude_code.claudeCodeAdapter,
249
+ import_codex.codexAdapter,
250
+ import_opencode.opencodeAdapter,
251
+ import_cursor.cursorAdapter,
252
+ import_copilot.copilotAdapter
253
+ ];
188
254
  var TARGET_TO_ID = {
189
255
  claude: "claude_code",
190
256
  codex: "codex",
191
- opencode: "opencode"
257
+ opencode: "opencode",
258
+ cursor: "cursor",
259
+ copilot: "copilot"
192
260
  };
193
- var AGENT_TARGETS = ["all", "claude", "codex", "opencode"];
261
+ var AGENT_TARGETS = [
262
+ "all",
263
+ "claude",
264
+ "codex",
265
+ "opencode",
266
+ "cursor",
267
+ "copilot"
268
+ ];
194
269
  function selectAdapters(target, adapters) {
195
270
  if (target === "all") return adapters;
196
271
  const id = TARGET_TO_ID[target];
@@ -281,18 +356,18 @@ function createAgentCommand(deps = {}) {
281
356
  return {
282
357
  name: "agent",
283
358
  summary: "Install or uninstall harness adapters",
284
- usage: "birdybeep agent <install|uninstall> [all|claude|codex|opencode]",
359
+ usage: "birdybeep agent <install|uninstall> [all|claude|codex|opencode|cursor|copilot]",
285
360
  subcommands: [
286
361
  {
287
362
  name: "install",
288
- summary: "Install adapters (all | claude | codex | opencode)",
289
- usage: "birdybeep agent install [all|claude|codex|opencode]",
363
+ summary: "Install adapters (all | claude | codex | opencode | cursor | copilot)",
364
+ usage: "birdybeep agent install [all|claude|codex|opencode|cursor|copilot]",
290
365
  run: (ctx) => installSelected(adapters, ctx)
291
366
  },
292
367
  {
293
368
  name: "uninstall",
294
369
  summary: "Restore harness config to its pre-install state",
295
- usage: "birdybeep agent uninstall [all|claude|codex|opencode]",
370
+ usage: "birdybeep agent uninstall [all|claude|codex|opencode|cursor|copilot]",
296
371
  run: (ctx) => uninstallSelected(adapters, ctx)
297
372
  }
298
373
  ]
@@ -303,6 +378,8 @@ function createAgentCommand(deps = {}) {
303
378
  var import_agent_core4 = require("@birdybeep/agent-core");
304
379
  var import_claude_code2 = require("@birdybeep/claude-code");
305
380
  var import_codex2 = require("@birdybeep/codex");
381
+ var import_copilot2 = require("@birdybeep/copilot");
382
+ var import_cursor2 = require("@birdybeep/cursor");
306
383
  var import_opencode2 = require("@birdybeep/opencode");
307
384
 
308
385
  // src/config.ts
@@ -327,6 +404,8 @@ function writeCliConfig(patch) {
327
404
  const merged = {};
328
405
  const apiUrl = patch.apiUrl ?? current.apiUrl;
329
406
  if (apiUrl !== void 0) merged.apiUrl = apiUrl;
407
+ const expectEmail = patch.expectEmail ?? current.expectEmail;
408
+ if (expectEmail !== void 0) merged.expectEmail = expectEmail;
330
409
  (0, import_node_fs2.mkdirSync)((0, import_agent_core2.birdyBeepConfigDir)(), { recursive: true, mode: 448 });
331
410
  (0, import_node_fs2.writeFileSync)(cliConfigPath(), `${JSON.stringify(merged, null, 2)}
332
411
  `, { mode: 384 });
@@ -336,6 +415,12 @@ function resolveApiUrl() {
336
415
  if (env !== void 0 && env.length > 0) return env;
337
416
  return readCliConfig().apiUrl ?? DEFAULT_API_URL;
338
417
  }
418
+ var DEFAULT_REGISTRY_URL = "https://registry.npmjs.org";
419
+ function resolveRegistryUrl() {
420
+ const env = process.env["npm_config_registry"];
421
+ if (env !== void 0 && env.length > 0) return env;
422
+ return DEFAULT_REGISTRY_URL;
423
+ }
339
424
 
340
425
  // src/diagnostics.ts
341
426
  var import_agent_core3 = require("@birdybeep/agent-core");
@@ -359,7 +444,13 @@ function machineIdentity() {
359
444
  }
360
445
 
361
446
  // src/commands/doctor.ts
362
- var DEFAULT_ADAPTERS2 = [import_claude_code2.claudeCodeAdapter, import_codex2.codexAdapter, import_opencode2.opencodeAdapter];
447
+ var DEFAULT_ADAPTERS2 = [
448
+ import_claude_code2.claudeCodeAdapter,
449
+ import_codex2.codexAdapter,
450
+ import_opencode2.opencodeAdapter,
451
+ import_cursor2.cursorAdapter,
452
+ import_copilot2.copilotAdapter
453
+ ];
363
454
  async function defaultProbeNetwork(baseUrl) {
364
455
  try {
365
456
  const controller = new AbortController();
@@ -442,16 +533,30 @@ function createDoctorCommand(deps = {}) {
442
533
  }
443
534
 
444
535
  // src/commands/hook.ts
536
+ var import_node_child_process = require("child_process");
537
+ var import_node_crypto = require("crypto");
538
+ var import_node_fs3 = require("fs");
539
+ var import_node_os = require("os");
540
+ var import_node_path2 = require("path");
445
541
  var import_agent_core5 = require("@birdybeep/agent-core");
446
542
  var import_claude_code3 = require("@birdybeep/claude-code");
447
543
  var import_codex3 = require("@birdybeep/codex");
544
+ var import_copilot3 = require("@birdybeep/copilot");
545
+ var import_cursor3 = require("@birdybeep/cursor");
448
546
  var import_opencode3 = require("@birdybeep/opencode");
449
547
  var RUNNERS = {
450
548
  claude: import_claude_code3.runClaudeHook,
451
549
  codex: import_codex3.runCodexHook,
452
- opencode: import_opencode3.runOpenCodeHook
550
+ opencode: import_opencode3.runOpenCodeHook,
551
+ cursor: import_cursor3.runCursorHook
453
552
  };
454
- var HOOK_HARNESSES = ["claude", "codex", "opencode"];
553
+ var HOOK_HARNESSES = [
554
+ "claude",
555
+ "codex",
556
+ "opencode",
557
+ "cursor",
558
+ "copilot"
559
+ ];
455
560
  var STDIN_READ_TIMEOUT_MS = 3e3;
456
561
  function withTimeout(promise, ms, fallback) {
457
562
  return new Promise((resolve) => {
@@ -468,9 +573,13 @@ function withTimeout(promise, ms, fallback) {
468
573
  });
469
574
  }
470
575
  function isHarnessName(value) {
471
- return value === "claude" || value === "codex" || value === "opencode";
576
+ return value === "claude" || value === "codex" || value === "opencode" || value === "cursor" || value === "copilot";
472
577
  }
473
- function runHookCommand(harness, payload, sender) {
578
+ function runHookCommand(harness, payload, sender, copilotEventName) {
579
+ if (harness === "copilot") {
580
+ if (copilotEventName === void 0) return Promise.resolve({ outcome: "skipped" });
581
+ return (0, import_copilot3.runCopilotHook)(copilotEventName, payload, { sender });
582
+ }
474
583
  return RUNNERS[harness](payload, { sender });
475
584
  }
476
585
  function readStdinDefault() {
@@ -486,24 +595,90 @@ function readStdinDefault() {
486
595
  process.stdin.on("error", () => resolve(""));
487
596
  });
488
597
  }
489
- async function readHookPayload(args, readStdin) {
490
- return args[1] ?? await readStdin();
598
+ async function readHookPayload(args, readStdin, stdinOnly = false) {
599
+ return stdinOnly ? readStdin() : args[1] ?? await readStdin();
600
+ }
601
+ var NOTIFY_STDIN_FILE_ENV = "BIRDYBEEP_CODEX_NOTIFY_STDIN_FILE";
602
+ function detachCodexNotifyWorker(payload) {
603
+ if (process.platform === "win32") return false;
604
+ let file;
605
+ let fd;
606
+ try {
607
+ const birdybeep = (0, import_agent_core5.resolveOnPath)("birdybeep");
608
+ if (birdybeep === null) return false;
609
+ const tmpFile = (0, import_node_path2.join)((0, import_node_os.tmpdir)(), `birdybeep-notify-${(0, import_node_crypto.randomBytes)(16).toString("hex")}.json`);
610
+ file = tmpFile;
611
+ (0, import_node_fs3.writeFileSync)(tmpFile, payload, { mode: 384 });
612
+ fd = (0, import_node_fs3.openSync)(tmpFile, "r");
613
+ const child = (0, import_node_child_process.spawn)(birdybeep, ["hook", "codex"], {
614
+ cwd: (0, import_node_path2.dirname)(birdybeep),
615
+ // trusted dir, never the inherited/attacker cwd
616
+ detached: true,
617
+ // new session (setsid) → survives `codex exec` reaping the group
618
+ stdio: [fd, "ignore", "ignore"],
619
+ // stdin = the temp file; this process holds no pipe
620
+ env: { ...process.env, [NOTIFY_STDIN_FILE_ENV]: tmpFile },
621
+ // worker cleans it up post-read
622
+ windowsHide: true
623
+ });
624
+ child.on("error", () => {
625
+ try {
626
+ (0, import_node_fs3.rmSync)(tmpFile, { force: true });
627
+ } catch {
628
+ }
629
+ });
630
+ child.unref();
631
+ return true;
632
+ } catch {
633
+ if (file !== void 0) {
634
+ try {
635
+ (0, import_node_fs3.rmSync)(file, { force: true });
636
+ } catch {
637
+ }
638
+ }
639
+ return false;
640
+ } finally {
641
+ if (fd !== void 0) {
642
+ try {
643
+ (0, import_node_fs3.closeSync)(fd);
644
+ } catch {
645
+ }
646
+ }
647
+ }
491
648
  }
492
649
  function createHookCommand(deps = {}) {
493
650
  const makeSender = deps.createSender ?? ((baseUrl) => (0, import_agent_core5.createSender)({ baseUrl }));
494
651
  const readStdin = deps.readStdin ?? readStdinDefault;
495
652
  const stdinTimeoutMs = deps.stdinTimeoutMs ?? STDIN_READ_TIMEOUT_MS;
653
+ const detachCodexNotify = deps.detachCodexNotify ?? detachCodexNotifyWorker;
496
654
  return {
497
655
  name: "hook",
498
656
  summary: "Internal: normalize + send an event fired by a harness hook",
499
- usage: "birdybeep hook <claude|codex|opencode>",
657
+ usage: "birdybeep hook <claude|codex|opencode|cursor|copilot> [copilot-event]",
500
658
  run: async (ctx) => {
501
659
  const harness = ctx.args[0];
502
660
  if (!isHarnessName(harness)) {
503
661
  ctx.io.errline(`birdybeep hook: expected one of ${HOOK_HARNESSES.join("|")}`);
504
662
  return EXIT.USAGE;
505
663
  }
506
- const raw = await withTimeout(readHookPayload(ctx.args, readStdin), stdinTimeoutMs, "");
664
+ const notifyPayload = ctx.args[1];
665
+ if (harness === "codex" && notifyPayload !== void 0 && notifyPayload.length > 0 && detachCodexNotify(notifyPayload)) {
666
+ ctx.io.result({ harness, outcome: "detached" });
667
+ return EXIT.OK;
668
+ }
669
+ const copilotEventName = harness === "copilot" && (0, import_copilot3.isCopilotHookEventName)(ctx.args[1]) ? ctx.args[1] : void 0;
670
+ const raw = await withTimeout(
671
+ readHookPayload(ctx.args, readStdin, harness === "copilot"),
672
+ stdinTimeoutMs,
673
+ ""
674
+ );
675
+ const notifyStdinFile = process.env[NOTIFY_STDIN_FILE_ENV];
676
+ if (notifyStdinFile !== void 0 && (0, import_node_path2.dirname)(notifyStdinFile) === (0, import_node_os.tmpdir)() && (0, import_node_path2.basename)(notifyStdinFile).startsWith("birdybeep-notify-")) {
677
+ try {
678
+ (0, import_node_fs3.rmSync)(notifyStdinFile, { force: true });
679
+ } catch {
680
+ }
681
+ }
507
682
  let payload;
508
683
  try {
509
684
  payload = JSON.parse(raw);
@@ -512,8 +687,15 @@ function createHookCommand(deps = {}) {
512
687
  return EXIT.OK;
513
688
  }
514
689
  const sender = makeSender(resolveApiUrl());
515
- const result = await runHookCommand(harness, payload, sender);
516
- ctx.io.result({ harness, outcome: result.outcome, eventType: result.eventType });
690
+ const result = await runHookCommand(harness, payload, sender, copilotEventName);
691
+ ctx.io.result({
692
+ harness,
693
+ ...copilotEventName !== void 0 ? { event: copilotEventName } : {},
694
+ outcome: result.outcome,
695
+ eventType: result.eventType,
696
+ ...result.send?.decision ? { decision: result.send.decision } : {},
697
+ ...result.send?.status !== void 0 ? { status: result.send.status } : {}
698
+ });
517
699
  return EXIT.OK;
518
700
  }
519
701
  };
@@ -521,57 +703,73 @@ function createHookCommand(deps = {}) {
521
703
 
522
704
  // src/commands/logout.ts
523
705
  var import_agent_core6 = require("@birdybeep/agent-core");
524
- function createClearTokenCommand(spec, deps = {}) {
706
+ var base = (apiUrl) => apiUrl.replace(/\/$/, "");
707
+ function createLogoutCommand(deps = {}) {
525
708
  return {
526
- name: spec.name,
527
- summary: spec.summary,
528
- usage: `birdybeep ${spec.name}`,
709
+ name: "logout",
710
+ summary: "Remove the local machine token (does NOT revoke the machine server-side)",
711
+ usage: "birdybeep logout",
529
712
  run: async (ctx) => {
530
713
  await (0, import_agent_core6.clearToken)(deps.tokenOptions ?? {});
531
- ctx.io.emit(spec.humanMessage, { [spec.jsonKey]: true });
714
+ ctx.io.emit("Logged out \u2014 the machine token was removed.", { loggedOut: true });
532
715
  return EXIT.OK;
533
716
  }
534
717
  };
535
718
  }
536
- function createLogoutCommand(deps = {}) {
537
- return createClearTokenCommand(
538
- {
539
- name: "logout",
540
- summary: "Remove the local machine token (same as `unpair`)",
541
- humanMessage: "Logged out \u2014 the machine token was removed.",
542
- jsonKey: "loggedOut"
543
- },
544
- deps
545
- );
719
+ async function revokeSelf(token, fetchImpl, timeoutMs) {
720
+ const controller = new AbortController();
721
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
722
+ try {
723
+ const res = await fetchImpl(`${base(resolveApiUrl())}/v1/machine/revoke-self`, {
724
+ method: "POST",
725
+ headers: { authorization: `Bearer ${token}` },
726
+ signal: controller.signal
727
+ });
728
+ if (res.ok || res.status === 403) return "revoked";
729
+ return "rejected";
730
+ } catch {
731
+ return "unreachable";
732
+ } finally {
733
+ clearTimeout(timer);
734
+ }
546
735
  }
547
736
  function createUnpairCommand(deps = {}) {
548
- return createClearTokenCommand(
549
- {
550
- name: "unpair",
551
- summary: "Unpair this machine \u2014 remove the local machine token (same as `logout`)",
552
- humanMessage: "Unpaired \u2014 the machine token was removed.",
553
- jsonKey: "unpaired"
554
- },
555
- deps
556
- );
737
+ const fetchImpl = deps.fetchImpl ?? fetch;
738
+ const timeoutMs = deps.timeoutMs ?? 1e4;
739
+ return {
740
+ name: "unpair",
741
+ summary: "Unpair this machine \u2014 revoke it server-side and remove the local token",
742
+ usage: "birdybeep unpair",
743
+ run: async (ctx) => {
744
+ const token = await (0, import_agent_core6.getToken)(deps.tokenOptions ?? {});
745
+ const outcome = token === null ? "no_token" : await revokeSelf(token, fetchImpl, timeoutMs);
746
+ await (0, import_agent_core6.clearToken)(deps.tokenOptions ?? {});
747
+ const serverRevoked = outcome === "revoked";
748
+ 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.";
749
+ ctx.io.emit(human, { unpaired: true, serverRevoked });
750
+ return EXIT.OK;
751
+ }
752
+ };
557
753
  }
558
754
 
559
755
  // src/commands/pair.ts
756
+ var import_node_fs4 = require("fs");
560
757
  var import_agent_core8 = require("@birdybeep/agent-core");
561
758
  var import_uqr = require("uqr");
562
759
 
563
760
  // src/pairing.ts
564
761
  var import_agent_core7 = require("@birdybeep/agent-core");
565
- function base(apiUrl) {
762
+ function base2(apiUrl) {
566
763
  return apiUrl.replace(/\/$/, "");
567
764
  }
568
765
  async function pairStart(apiUrl, input, fetchImpl) {
569
766
  const body = {
570
767
  machine_label: input.machineLabel,
571
768
  ...input.os !== void 0 ? { os: input.os } : {},
572
- ...input.cliVersion !== void 0 ? { cli_version: input.cliVersion } : {}
769
+ ...input.cliVersion !== void 0 ? { cli_version: input.cliVersion } : {},
770
+ ...input.codeChallenge !== void 0 ? { code_challenge: input.codeChallenge } : {}
573
771
  };
574
- const res = await fetchImpl(`${base(apiUrl)}/v1/pair/start`, {
772
+ const res = await fetchImpl(`${base2(apiUrl)}/v1/pair/start`, {
575
773
  method: "POST",
576
774
  headers: { "content-type": "application/json" },
577
775
  body: JSON.stringify(body)
@@ -589,12 +787,13 @@ var TERMINAL_TOKEN_ERRORS = /* @__PURE__ */ new Set([
589
787
  "not_found",
590
788
  "payload_too_large"
591
789
  ]);
592
- async function pairTokenPoll(apiUrl, deviceCode, fetchImpl, machineFingerprint) {
790
+ async function pairTokenPoll(apiUrl, deviceCode, fetchImpl, machineFingerprint, codeVerifier) {
593
791
  const body = {
594
792
  device_code: deviceCode,
595
- ...machineFingerprint !== void 0 ? { machine_fingerprint: machineFingerprint } : {}
793
+ ...machineFingerprint !== void 0 ? { machine_fingerprint: machineFingerprint } : {},
794
+ ...codeVerifier !== void 0 ? { code_verifier: codeVerifier } : {}
596
795
  };
597
- const res = await fetchImpl(`${base(apiUrl)}/v1/pair/token`, {
796
+ const res = await fetchImpl(`${base2(apiUrl)}/v1/pair/token`, {
598
797
  method: "POST",
599
798
  headers: { "content-type": "application/json" },
600
799
  body: JSON.stringify(body)
@@ -605,7 +804,10 @@ async function pairTokenPoll(apiUrl, deviceCode, fetchImpl, machineFingerprint)
605
804
  return {
606
805
  status: "paired",
607
806
  machineToken: parsed.data.machine_token,
608
- machineId: parsed.data.machine_id
807
+ machineId: parsed.data.machine_id,
808
+ // Only surface the key when the server reported it (exactOptionalPropertyTypes: no explicit
809
+ // undefined). Older servers omit approved_by_email; newer ones (dgxd) include it.
810
+ ...parsed.data.approved_by_email !== void 0 ? { approvedByEmail: parsed.data.approved_by_email } : {}
609
811
  };
610
812
  }
611
813
  let errBody = null;
@@ -626,7 +828,7 @@ async function pairTokenPoll(apiUrl, deviceCode, fetchImpl, machineFingerprint)
626
828
  }
627
829
 
628
830
  // src/version.ts
629
- var CLI_VERSION = "0.1.0".length > 0 ? "0.1.0" : "0.0.0";
831
+ var CLI_VERSION = "0.3.0".length > 0 ? "0.3.0" : "0.0.0";
630
832
 
631
833
  // src/commands/pair.ts
632
834
  var DEFAULT_POLL_INTERVAL_MS = 2e3;
@@ -634,22 +836,172 @@ var HEARTBEAT_MS = 15e3;
634
836
  function renderQrMatrix(qrPayload) {
635
837
  return (0, import_uqr.renderUnicodeCompact)(qrPayload, { border: 2 });
636
838
  }
839
+ function parsePairFlags(args) {
840
+ const flags = { yes: false };
841
+ for (let i = 0; i < args.length; i += 1) {
842
+ const token = args[i] ?? "";
843
+ if (token === "--yes" || token === "-y") {
844
+ flags.yes = true;
845
+ } else if (token === "--expect-email" || token.startsWith("--expect-email=")) {
846
+ const inline = token.startsWith("--expect-email=") ? token.slice("--expect-email=".length) : void 0;
847
+ const value = inline ?? args[++i];
848
+ if (value === void 0 || value.length === 0 || value.startsWith("-")) {
849
+ return { ...flags, error: "--expect-email requires an email address" };
850
+ }
851
+ flags.expectEmail = value;
852
+ } else {
853
+ return { ...flags, error: `unexpected argument "${token}"` };
854
+ }
855
+ }
856
+ return flags;
857
+ }
858
+ function sameEmail(a, b) {
859
+ const fold = (v) => v.trim().toLowerCase();
860
+ const rawEqual = fold(a) === fold(b);
861
+ const nfkcEqual = fold(a.normalize("NFKC")) === fold(b.normalize("NFKC"));
862
+ return rawEqual && nfkcEqual;
863
+ }
864
+ function decidePairConfirmation(input) {
865
+ const { approvedByEmail, expectEmail } = input;
866
+ const platform = input.platform ?? process.platform;
867
+ if (expectEmail !== void 0) {
868
+ if (approvedByEmail === void 0) {
869
+ 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.";
870
+ return {
871
+ action: "reject",
872
+ reason: "expected_email_unverifiable",
873
+ 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}`
874
+ };
875
+ }
876
+ if (sameEmail(approvedByEmail, expectEmail)) {
877
+ return { action: "approve", reason: "expected_email_match" };
878
+ }
879
+ return {
880
+ action: "reject",
881
+ reason: "expected_email_mismatch",
882
+ 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\`.`
883
+ };
884
+ }
885
+ if (input.yes) return { action: "approve", reason: "yes_flag" };
886
+ 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] ";
887
+ if (!input.nonInteractive) {
888
+ if (input.stdinIsTTY) return { action: "prompt", question, on: "stdin" };
889
+ if (input.controllingTerminalAvailable) {
890
+ return { action: "prompt", question, on: "controlling-terminal" };
891
+ }
892
+ }
893
+ const who = approvedByEmail !== void 0 ? ` (approved by ${approvedByEmail})` : "";
894
+ const winptyHint = platform === "win32" && !input.nonInteractive ? " In Git Bash / MSYS, `winpty birdybeep pair` attaches a real console so the prompt can appear." : "";
895
+ return {
896
+ action: "reject",
897
+ reason: "non_interactive",
898
+ 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
899
+ };
900
+ }
901
+ function isAffirmative(answer) {
902
+ return /^(y|yes)$/i.test(answer.trim());
903
+ }
904
+ function controllingTerminalPath() {
905
+ return "/dev/tty";
906
+ }
907
+ function canOpenControllingTerminal(path = controllingTerminalPath(), platform = process.platform) {
908
+ if (platform === "win32") return false;
909
+ let fd;
910
+ try {
911
+ fd = (0, import_node_fs4.openSync)(path, "r");
912
+ return true;
913
+ } catch {
914
+ return false;
915
+ } finally {
916
+ if (fd !== void 0) {
917
+ try {
918
+ (0, import_node_fs4.closeSync)(fd);
919
+ } catch {
920
+ }
921
+ }
922
+ }
923
+ }
924
+ async function promptForAnswer(question, on) {
925
+ const { createInterface } = await import("readline/promises");
926
+ let ttyFd;
927
+ let input;
928
+ if (on === "stdin") {
929
+ input = process.stdin;
930
+ } else {
931
+ const { ReadStream } = await import("tty");
932
+ ttyFd = (0, import_node_fs4.openSync)(controllingTerminalPath(), "r");
933
+ input = new ReadStream(ttyFd);
934
+ }
935
+ return new Promise((resolve) => {
936
+ const rl = createInterface({ input, output: process.stderr });
937
+ let settled = false;
938
+ const done = (value) => {
939
+ if (settled) return;
940
+ settled = true;
941
+ rl.close();
942
+ if (on === "stdin") {
943
+ process.stdin.unref?.();
944
+ } else {
945
+ try {
946
+ input.unref?.();
947
+ input.destroy?.();
948
+ } catch {
949
+ }
950
+ if (ttyFd !== void 0) {
951
+ try {
952
+ (0, import_node_fs4.closeSync)(ttyFd);
953
+ } catch {
954
+ }
955
+ }
956
+ }
957
+ resolve(value);
958
+ };
959
+ rl.question(question).then(done, () => done(""));
960
+ rl.once("close", () => done(""));
961
+ input.once?.("error", () => done(""));
962
+ });
963
+ }
637
964
  function createPairCommand(deps = {}) {
638
965
  const fetchImpl = deps.fetchImpl ?? fetch;
639
966
  const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
640
967
  const clock = deps.now ?? (() => Date.now());
641
968
  const renderQr = deps.renderQr ?? renderQrMatrix;
642
969
  const intervalMs = deps.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
970
+ const promptLine = deps.promptLine ?? promptForAnswer;
971
+ const hasControllingTerminal = deps.hasControllingTerminal ?? (() => canOpenControllingTerminal());
972
+ const configuredExpectEmail = deps.configuredExpectEmail ?? (() => {
973
+ const pinned = readCliConfig().expectEmail;
974
+ return typeof pinned === "string" && pinned.trim().length > 0 ? pinned : void 0;
975
+ });
643
976
  return {
644
977
  name: "pair",
645
978
  summary: "Pair this machine with your BirdyBeep account (QR or manual)",
646
- usage: "birdybeep pair [--json]",
979
+ usage: "birdybeep pair [--yes] [--expect-email <addr>] [--json]",
980
+ options: [
981
+ {
982
+ flag: "--yes",
983
+ aliases: ["-y"],
984
+ summary: "Skip the approving-account confirmation (headless/CI)"
985
+ },
986
+ {
987
+ flag: "--expect-email",
988
+ value: "<addr>",
989
+ summary: "Only trust the pairing if this account approved it (else fail)"
990
+ }
991
+ ],
647
992
  run: async (ctx) => {
993
+ const pairFlags = parsePairFlags(ctx.args);
994
+ if (pairFlags.error !== void 0) {
995
+ ctx.io.errline(`birdybeep pair: ${pairFlags.error}.`);
996
+ return EXIT.USAGE;
997
+ }
648
998
  const apiUrl = resolveApiUrl();
649
999
  const identity = (0, import_agent_core8.getMachineIdentity)();
1000
+ const codeVerifier = (0, import_agent_core8.generateCodeVerifier)();
1001
+ const codeChallenge = (0, import_agent_core8.deriveCodeChallengeS256)(codeVerifier);
650
1002
  const start = await pairStart(
651
1003
  apiUrl,
652
- { machineLabel: identity.label, os: identity.os, cliVersion: CLI_VERSION },
1004
+ { machineLabel: identity.label, os: identity.os, cliVersion: CLI_VERSION, codeChallenge },
653
1005
  fetchImpl
654
1006
  );
655
1007
  if (ctx.flags.json) {
@@ -661,12 +1013,14 @@ function createPairCommand(deps = {}) {
661
1013
  });
662
1014
  } else {
663
1015
  ctx.io.line(
664
- "To pair this machine, open the BirdyBeep app, tap \u201Cpair a machine\u201D, and scan this QR (or enter the code):"
1016
+ "To pair this machine, open the BirdyBeep app, tap \u201Cpair a machine\u201D, and scan this QR or open the complete link:"
665
1017
  );
666
1018
  const isTTY = deps.isTTY ?? process.stdout.isTTY === true;
667
1019
  if (isTTY) ctx.io.line(renderQr(start.qr_payload));
668
1020
  ctx.io.line(` Scan or open: ${start.qr_payload}`);
669
- ctx.io.line(` Code: ${start.user_code}`);
1021
+ ctx.io.line(
1022
+ ` Session code (display only; cannot approve by itself): ${start.user_code}`
1023
+ );
670
1024
  ctx.io.line("Waiting for you to approve this machine in the app\u2026");
671
1025
  }
672
1026
  const deadline = Date.parse(start.expires_at);
@@ -682,7 +1036,9 @@ function createPairCommand(deps = {}) {
682
1036
  apiUrl,
683
1037
  start.device_code,
684
1038
  fetchImpl,
685
- identity.fingerprintHash
1039
+ identity.fingerprintHash,
1040
+ codeVerifier
1041
+ // PKCE proof-of-possession (dgxd) — sent on every poll
686
1042
  );
687
1043
  if (poll.status === "paired") {
688
1044
  paired = poll;
@@ -707,15 +1063,44 @@ function createPairCommand(deps = {}) {
707
1063
  if (paired === void 0 || paired.status !== "paired") {
708
1064
  ctx.io.result({ paired: false, reason: "timeout" });
709
1065
  ctx.io.errline(
710
- "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."
1066
+ "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."
1067
+ );
1068
+ return EXIT.ERROR;
1069
+ }
1070
+ const approvedBy = paired.approvedByEmail;
1071
+ const expectEmail = pairFlags.expectEmail ?? configuredExpectEmail();
1072
+ const stdinIsTTY = deps.isStdinTTY ?? process.stdin.isTTY === true;
1073
+ const decision = decidePairConfirmation({
1074
+ ...approvedBy !== void 0 ? { approvedByEmail: approvedBy } : {},
1075
+ ...expectEmail !== void 0 ? { expectEmail } : {},
1076
+ ...expectEmail !== void 0 ? { expectEmailSource: pairFlags.expectEmail !== void 0 ? "flag" : "config" } : {},
1077
+ yes: pairFlags.yes,
1078
+ nonInteractive: ctx.flags.nonInteractive,
1079
+ stdinIsTTY,
1080
+ // Probed ONLY when stdin can't answer — opening /dev/tty is a syscall, and when stdin is
1081
+ // already a terminal the answer is irrelevant.
1082
+ controllingTerminalAvailable: stdinIsTTY ? false : hasControllingTerminal(),
1083
+ configPath: cliConfigPath()
1084
+ });
1085
+ if (decision.action === "reject") {
1086
+ ctx.io.result({ paired: false, reason: decision.reason });
1087
+ ctx.io.errline(decision.message);
1088
+ return EXIT.ERROR;
1089
+ }
1090
+ if (decision.action === "prompt" && !isAffirmative(await promptLine(decision.question, decision.on))) {
1091
+ ctx.io.result({ paired: false, reason: "declined" });
1092
+ ctx.io.errline(
1093
+ "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."
711
1094
  );
712
1095
  return EXIT.ERROR;
713
1096
  }
714
1097
  await (0, import_agent_core8.setToken)(paired.machineToken, deps.tokenOptions ?? {});
715
1098
  writeCliConfig({ apiUrl });
716
- ctx.io.emit(`\u2713 Paired. Run \`birdybeep test\` to send a test Beep.`, {
1099
+ const humanSuffix = approvedBy !== void 0 ? ` to ${approvedBy}` : "";
1100
+ ctx.io.emit(`\u2713 Paired${humanSuffix}. Run \`birdybeep test\` to send a test Beep.`, {
717
1101
  paired: true,
718
- machineId: paired.machineId
1102
+ machineId: paired.machineId,
1103
+ ...approvedBy !== void 0 ? { approvedByEmail: approvedBy } : {}
719
1104
  });
720
1105
  return EXIT.OK;
721
1106
  }
@@ -748,14 +1133,24 @@ function createQueueCommand() {
748
1133
  var import_agent_core10 = require("@birdybeep/agent-core");
749
1134
  var import_claude_code4 = require("@birdybeep/claude-code");
750
1135
  var import_codex4 = require("@birdybeep/codex");
1136
+ var import_copilot4 = require("@birdybeep/copilot");
1137
+ var import_cursor4 = require("@birdybeep/cursor");
751
1138
  var import_opencode4 = require("@birdybeep/opencode");
752
- var DEFAULT_ADAPTERS3 = [import_claude_code4.claudeCodeAdapter, import_codex4.codexAdapter, import_opencode4.opencodeAdapter];
1139
+ var DEFAULT_ADAPTERS3 = [
1140
+ import_claude_code4.claudeCodeAdapter,
1141
+ import_codex4.codexAdapter,
1142
+ import_opencode4.opencodeAdapter,
1143
+ import_cursor4.cursorAdapter,
1144
+ import_copilot4.copilotAdapter
1145
+ ];
753
1146
  var ADAPTER_VERSIONS = {
754
1147
  claude_code: import_claude_code4.CLAUDE_CODE_ADAPTER_VERSION,
755
1148
  codex: import_codex4.CODEX_ADAPTER_VERSION,
756
- opencode: import_opencode4.OPENCODE_ADAPTER_VERSION
1149
+ opencode: import_opencode4.OPENCODE_ADAPTER_VERSION,
1150
+ cursor: import_cursor4.CURSOR_ADAPTER_VERSION,
1151
+ copilot: import_copilot4.COPILOT_ADAPTER_VERSION
757
1152
  };
758
- var base2 = (apiUrl) => apiUrl.replace(/\/$/, "");
1153
+ var base3 = (apiUrl) => apiUrl.replace(/\/$/, "");
759
1154
  async function gatherItems(adapters) {
760
1155
  return Promise.all(
761
1156
  adapters.map(async (a) => {
@@ -790,7 +1185,7 @@ function createReportStatusCommand(deps = {}) {
790
1185
  let outcome = "deferred";
791
1186
  let errorCode;
792
1187
  try {
793
- const res = await fetchImpl(`${base2(resolveApiUrl())}/v1/integrations/status`, {
1188
+ const res = await fetchImpl(`${base3(resolveApiUrl())}/v1/integrations/status`, {
794
1189
  method: "POST",
795
1190
  headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
796
1191
  body: JSON.stringify({ integrations: items })
@@ -841,8 +1236,16 @@ function createReportStatusCommand(deps = {}) {
841
1236
  var import_agent_core11 = require("@birdybeep/agent-core");
842
1237
  var import_claude_code5 = require("@birdybeep/claude-code");
843
1238
  var import_codex5 = require("@birdybeep/codex");
1239
+ var import_copilot5 = require("@birdybeep/copilot");
1240
+ var import_cursor5 = require("@birdybeep/cursor");
844
1241
  var import_opencode5 = require("@birdybeep/opencode");
845
- var DEFAULT_ADAPTERS4 = [import_claude_code5.claudeCodeAdapter, import_codex5.codexAdapter, import_opencode5.opencodeAdapter];
1242
+ var DEFAULT_ADAPTERS4 = [
1243
+ import_claude_code5.claudeCodeAdapter,
1244
+ import_codex5.codexAdapter,
1245
+ import_opencode5.opencodeAdapter,
1246
+ import_cursor5.cursorAdapter,
1247
+ import_copilot5.copilotAdapter
1248
+ ];
846
1249
  function createStatusCommand(deps = {}) {
847
1250
  const adapters = deps.adapters ?? DEFAULT_ADAPTERS4;
848
1251
  const makeSender = deps.createSender ?? ((baseUrl) => (0, import_agent_core11.createSender)(
@@ -882,7 +1285,7 @@ function createStatusCommand(deps = {}) {
882
1285
  }
883
1286
 
884
1287
  // src/commands/test.ts
885
- var import_node_crypto = require("crypto");
1288
+ var import_node_crypto2 = require("crypto");
886
1289
  var import_agent_core12 = require("@birdybeep/agent-core");
887
1290
  function buildTestEvent(opts = {}) {
888
1291
  const machine = (0, import_agent_core12.getMachineIdentity)();
@@ -894,7 +1297,7 @@ function buildTestEvent(opts = {}) {
894
1297
  // schema requires a harness; the "test" type distinguishes it
895
1298
  // Unique per run: a repeat `birdybeep test` inside the backend's dedupe window must
896
1299
  // still beep — a constant id made the second test silently "deduped" (9fh).
897
- source_session_id: `birdybeep-cli-test-${(0, import_node_crypto.randomUUID)()}`,
1300
+ source_session_id: `birdybeep-cli-test-${(0, import_node_crypto2.randomUUID)()}`,
898
1301
  machine: { label: machine.label, os: machine.os },
899
1302
  workspace: { cwd: process.cwd() },
900
1303
  title: "BirdyBeep test event",
@@ -963,13 +1366,152 @@ function buildCommands() {
963
1366
  ];
964
1367
  }
965
1368
 
1369
+ // src/update-check.ts
1370
+ var import_node_fs5 = require("fs");
1371
+ var import_node_path3 = require("path");
1372
+ var import_agent_core13 = require("@birdybeep/agent-core");
1373
+ var PACKAGE_NAME = "@birdybeep/cli";
1374
+ var PACKAGE_PATH = "@birdybeep%2Fcli";
1375
+ var UPDATE_CACHE_FILE = "update-check.json";
1376
+ var DEFAULT_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
1377
+ var DEFAULT_TIMEOUT_MS = 1500;
1378
+ var SKIP_COMMANDS = /* @__PURE__ */ new Set(["hook", "report-status"]);
1379
+ var SEMVER_RE = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
1380
+ function parseSemver(input) {
1381
+ const m = SEMVER_RE.exec(input.trim());
1382
+ if (m === null) return null;
1383
+ return {
1384
+ major: Number(m[1]),
1385
+ minor: Number(m[2]),
1386
+ patch: Number(m[3]),
1387
+ prerelease: m[4] !== void 0 ? m[4].split(".") : []
1388
+ };
1389
+ }
1390
+ function comparePrerelease(a, b) {
1391
+ if (a.length === 0 && b.length === 0) return 0;
1392
+ if (a.length === 0) return 1;
1393
+ if (b.length === 0) return -1;
1394
+ const len = Math.min(a.length, b.length);
1395
+ for (let i = 0; i < len; i++) {
1396
+ const ai = a[i];
1397
+ const bi = b[i];
1398
+ const aNum = /^\d+$/.test(ai);
1399
+ const bNum = /^\d+$/.test(bi);
1400
+ if (aNum && bNum) {
1401
+ const d = Number(ai) - Number(bi);
1402
+ if (d !== 0) return d < 0 ? -1 : 1;
1403
+ } else if (aNum) {
1404
+ return -1;
1405
+ } else if (bNum) {
1406
+ return 1;
1407
+ } else if (ai !== bi) {
1408
+ return ai < bi ? -1 : 1;
1409
+ }
1410
+ }
1411
+ if (a.length === b.length) return 0;
1412
+ return a.length < b.length ? -1 : 1;
1413
+ }
1414
+ function compareSemver(a, b) {
1415
+ if (a.major !== b.major) return a.major < b.major ? -1 : 1;
1416
+ if (a.minor !== b.minor) return a.minor < b.minor ? -1 : 1;
1417
+ if (a.patch !== b.patch) return a.patch < b.patch ? -1 : 1;
1418
+ return comparePrerelease(a.prerelease, b.prerelease);
1419
+ }
1420
+ function isNewer(current, latest) {
1421
+ const cur = parseSemver(current);
1422
+ const lat = parseSemver(latest);
1423
+ return cur !== null && lat !== null && compareSemver(cur, lat) < 0;
1424
+ }
1425
+ function updateCachePath() {
1426
+ return (0, import_node_path3.join)((0, import_agent_core13.birdyBeepConfigDir)(), UPDATE_CACHE_FILE);
1427
+ }
1428
+ function readUpdateCache() {
1429
+ try {
1430
+ const parsed = JSON.parse((0, import_node_fs5.readFileSync)(updateCachePath(), "utf8"));
1431
+ if (typeof parsed !== "object" || parsed === null) return null;
1432
+ const { checkedAt, latest } = parsed;
1433
+ if (typeof checkedAt !== "number") return null;
1434
+ if (latest !== null && typeof latest !== "string") return null;
1435
+ return { checkedAt, latest };
1436
+ } catch {
1437
+ return null;
1438
+ }
1439
+ }
1440
+ function writeUpdateCache(cache) {
1441
+ (0, import_node_fs5.mkdirSync)((0, import_agent_core13.birdyBeepConfigDir)(), { recursive: true, mode: 448 });
1442
+ (0, import_node_fs5.writeFileSync)(updateCachePath(), `${JSON.stringify(cache)}
1443
+ `, { mode: 384 });
1444
+ }
1445
+ async function fetchLatestVersion(registryUrl, fetchImpl, timeoutMs) {
1446
+ const url = `${registryUrl.replace(/\/+$/, "")}/${PACKAGE_PATH}/latest`;
1447
+ const controller = new AbortController();
1448
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
1449
+ if (typeof timer.unref === "function") timer.unref();
1450
+ try {
1451
+ const res = await fetchImpl(url, {
1452
+ headers: { accept: "application/json" },
1453
+ signal: controller.signal
1454
+ });
1455
+ if (!res.ok) throw new Error(`registry responded ${res.status}`);
1456
+ const body = await res.json();
1457
+ if (typeof body.version !== "string" || body.version.length === 0) {
1458
+ throw new Error("registry response had no version");
1459
+ }
1460
+ return body.version;
1461
+ } finally {
1462
+ clearTimeout(timer);
1463
+ }
1464
+ }
1465
+ function renderNotice(current, latest) {
1466
+ return `a new version of birdybeep is available: ${current} \u2192 ${latest}
1467
+ upgrade with: npm install -g ${PACKAGE_NAME}@latest`;
1468
+ }
1469
+ async function maybeNotifyUpdate(opts) {
1470
+ try {
1471
+ if (opts.command !== void 0 && SKIP_COMMANDS.has(opts.command)) return;
1472
+ if (opts.flags.json || opts.flags.nonInteractive) return;
1473
+ const env = opts.env ?? process.env;
1474
+ if (env["BIRDYBEEP_NO_UPDATE_NOTIFIER"] || env["NO_UPDATE_NOTIFIER"] || env["CI"]) return;
1475
+ const isTTY = opts.isTTY ?? Boolean(process.stderr.isTTY);
1476
+ if (!isTTY) return;
1477
+ const current = opts.currentVersion ?? CLI_VERSION;
1478
+ const now = opts.now ?? Date.now();
1479
+ const intervalMs = opts.intervalMs ?? DEFAULT_CHECK_INTERVAL_MS;
1480
+ const readCache = opts.readCache ?? readUpdateCache;
1481
+ const writeCache = opts.writeCache ?? writeUpdateCache;
1482
+ let cache = readCache();
1483
+ if (cache === null || now - cache.checkedAt >= intervalMs) {
1484
+ let latest = cache?.latest ?? null;
1485
+ try {
1486
+ latest = await fetchLatestVersion(
1487
+ opts.registryUrl ?? resolveRegistryUrl(),
1488
+ opts.fetchImpl ?? fetch,
1489
+ opts.timeoutMs ?? DEFAULT_TIMEOUT_MS
1490
+ );
1491
+ } catch {
1492
+ }
1493
+ cache = { checkedAt: now, latest };
1494
+ try {
1495
+ writeCache(cache);
1496
+ } catch {
1497
+ }
1498
+ }
1499
+ if (cache.latest !== null && isNewer(current, cache.latest)) {
1500
+ opts.io.errline(renderNotice(current, cache.latest));
1501
+ }
1502
+ } catch {
1503
+ }
1504
+ }
1505
+
966
1506
  // src/cli.ts
967
1507
  function runCli(argv, deps = {}) {
1508
+ const notifyUpdate = deps.updateCheck === false ? void 0 : (ctx) => maybeNotifyUpdate({ ...ctx, ...deps.updateCheck ?? {} });
968
1509
  return dispatch(argv, {
969
1510
  version: CLI_VERSION,
970
1511
  commands: deps.commands ?? buildCommands(),
971
1512
  stdout: deps.stdout ?? process.stdout,
972
1513
  stderr: deps.stderr ?? process.stderr,
1514
+ ...notifyUpdate !== void 0 ? { notifyUpdate } : {},
973
1515
  ...deps.ensureConfig !== void 0 ? { ensureConfig: deps.ensureConfig } : {}
974
1516
  });
975
1517
  }