@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.
@@ -69,8 +69,11 @@ function parseGlobalFlags(argv) {
69
69
  }
70
70
  return { flags, rest };
71
71
  }
72
- function isUnknownFlag(token) {
73
- return token.startsWith("-") && !GLOBAL_FLAG_TOKENS.has(token);
72
+ function isUnknownFlag(token, allowed) {
73
+ if (!token.startsWith("-")) return false;
74
+ if (GLOBAL_FLAG_TOKENS.has(token)) return false;
75
+ const eq = token.indexOf("=");
76
+ return !allowed.has(eq >= 0 ? token.slice(0, eq) : token);
74
77
  }
75
78
  function renderRootHelp(version, commands) {
76
79
  const width = Math.max(...commands.map((c) => c.name.length));
@@ -91,6 +94,16 @@ function renderRootHelp(version, commands) {
91
94
  " -v, --version Show the CLI version"
92
95
  ].join("\n");
93
96
  }
97
+ function commandFlagTokens(...commands) {
98
+ const tokens = /* @__PURE__ */ new Set();
99
+ for (const command of commands) {
100
+ for (const option of command?.options ?? []) {
101
+ tokens.add(option.flag);
102
+ for (const alias of option.aliases ?? []) tokens.add(alias);
103
+ }
104
+ }
105
+ return tokens;
106
+ }
94
107
  function renderCommandHelp(path, command) {
95
108
  const lines = [
96
109
  `birdybeep ${path} \u2014 ${command.summary}`,
@@ -98,6 +111,17 @@ function renderCommandHelp(path, command) {
98
111
  "Usage:",
99
112
  ` ${command.usage ?? `birdybeep ${path} [options]`}`
100
113
  ];
114
+ if (command.options && command.options.length > 0) {
115
+ const labels = command.options.map(
116
+ (o) => `${[o.flag, ...o.aliases ?? []].join(", ")}${o.value ? ` ${o.value}` : ""}`
117
+ );
118
+ const width = Math.max(...labels.map((l) => l.length));
119
+ lines.push(
120
+ "",
121
+ "Options:",
122
+ ...command.options.map((o, i) => ` ${labels[i]?.padEnd(width)} ${o.summary}`)
123
+ );
124
+ }
101
125
  if (command.subcommands && command.subcommands.length > 0) {
102
126
  const width = Math.max(...command.subcommands.map((c) => c.name.length));
103
127
  lines.push(
@@ -122,6 +146,7 @@ async function dispatch(argv, deps) {
122
146
  return EXIT.OK;
123
147
  }
124
148
  let command = deps.commands.find((c) => c.name === rest[0]);
149
+ let parent;
125
150
  const pathParts = [];
126
151
  let argsStart = 1;
127
152
  if (command) {
@@ -129,6 +154,7 @@ async function dispatch(argv, deps) {
129
154
  if (command.subcommands && command.subcommands.length > 0) {
130
155
  const sub = command.subcommands.find((c) => c.name === rest[1]);
131
156
  if (sub) {
157
+ parent = command;
132
158
  command = sub;
133
159
  pathParts.push(sub.name);
134
160
  argsStart = 2;
@@ -152,6 +178,7 @@ async function dispatch(argv, deps) {
152
178
  name: path,
153
179
  summary: command.summary,
154
180
  usage: command.usage,
181
+ options: command.options,
155
182
  subcommands: command.subcommands?.map((c) => ({ name: c.name, summary: c.summary }))
156
183
  });
157
184
  return EXIT.OK;
@@ -161,13 +188,15 @@ async function dispatch(argv, deps) {
161
188
  return EXIT.USAGE;
162
189
  }
163
190
  const args = rest.slice(argsStart);
164
- const unknown = args.find(isUnknownFlag);
191
+ const allowed = commandFlagTokens(command, parent);
192
+ const unknown = args.find((token) => isUnknownFlag(token, allowed));
165
193
  if (unknown !== void 0) {
166
194
  io.errline(`birdybeep ${path}: unknown option "${unknown}".`);
167
195
  return EXIT.USAGE;
168
196
  }
197
+ let code;
169
198
  try {
170
- return await command.run({ args, flags, io });
199
+ code = await command.run({ args, flags, io });
171
200
  } catch (err) {
172
201
  if (err instanceof MissingInputError) {
173
202
  io.errline(
@@ -178,22 +207,46 @@ async function dispatch(argv, deps) {
178
207
  io.errline(`birdybeep ${path}: ${err instanceof Error ? err.message : String(err)}`);
179
208
  return EXIT.ERROR;
180
209
  }
210
+ if (deps.notifyUpdate !== void 0) {
211
+ try {
212
+ await deps.notifyUpdate({ command: pathParts[0] ?? "", flags, io });
213
+ } catch {
214
+ }
215
+ }
216
+ return code;
181
217
  }
182
218
 
183
219
  // src/version.ts
184
- var CLI_VERSION = "0.1.0".length > 0 ? "0.1.0" : "0.0.0";
220
+ var CLI_VERSION = "0.3.0".length > 0 ? "0.3.0" : "0.0.0";
185
221
 
186
222
  // src/commands/agent.ts
187
223
  import { claudeCodeAdapter } from "@birdybeep/claude-code";
188
224
  import { codexAdapter } from "@birdybeep/codex";
225
+ import { copilotAdapter } from "@birdybeep/copilot";
226
+ import { cursorAdapter } from "@birdybeep/cursor";
189
227
  import { opencodeAdapter } from "@birdybeep/opencode";
190
- var DEFAULT_ADAPTERS = [claudeCodeAdapter, codexAdapter, opencodeAdapter];
228
+ var DEFAULT_ADAPTERS = [
229
+ claudeCodeAdapter,
230
+ codexAdapter,
231
+ opencodeAdapter,
232
+ cursorAdapter,
233
+ copilotAdapter
234
+ ];
191
235
  var TARGET_TO_ID = {
192
236
  claude: "claude_code",
193
237
  codex: "codex",
194
- opencode: "opencode"
238
+ opencode: "opencode",
239
+ cursor: "cursor",
240
+ copilot: "copilot"
195
241
  };
196
- var AGENT_TARGETS = ["all", "claude", "codex", "opencode"];
242
+ var AGENT_TARGETS = [
243
+ "all",
244
+ "claude",
245
+ "codex",
246
+ "opencode",
247
+ "cursor",
248
+ "copilot"
249
+ ];
197
250
  function selectAdapters(target, adapters) {
198
251
  if (target === "all") return adapters;
199
252
  const id = TARGET_TO_ID[target];
@@ -284,18 +337,18 @@ function createAgentCommand(deps = {}) {
284
337
  return {
285
338
  name: "agent",
286
339
  summary: "Install or uninstall harness adapters",
287
- usage: "birdybeep agent <install|uninstall> [all|claude|codex|opencode]",
340
+ usage: "birdybeep agent <install|uninstall> [all|claude|codex|opencode|cursor|copilot]",
288
341
  subcommands: [
289
342
  {
290
343
  name: "install",
291
- summary: "Install adapters (all | claude | codex | opencode)",
292
- usage: "birdybeep agent install [all|claude|codex|opencode]",
344
+ summary: "Install adapters (all | claude | codex | opencode | cursor | copilot)",
345
+ usage: "birdybeep agent install [all|claude|codex|opencode|cursor|copilot]",
293
346
  run: (ctx) => installSelected(adapters, ctx)
294
347
  },
295
348
  {
296
349
  name: "uninstall",
297
350
  summary: "Restore harness config to its pre-install state",
298
- usage: "birdybeep agent uninstall [all|claude|codex|opencode]",
351
+ usage: "birdybeep agent uninstall [all|claude|codex|opencode|cursor|copilot]",
299
352
  run: (ctx) => uninstallSelected(adapters, ctx)
300
353
  }
301
354
  ]
@@ -308,6 +361,8 @@ import {
308
361
  } from "@birdybeep/agent-core";
309
362
  import { claudeCodeAdapter as claudeCodeAdapter2 } from "@birdybeep/claude-code";
310
363
  import { codexAdapter as codexAdapter2 } from "@birdybeep/codex";
364
+ import { copilotAdapter as copilotAdapter2 } from "@birdybeep/copilot";
365
+ import { cursorAdapter as cursorAdapter2 } from "@birdybeep/cursor";
311
366
  import { opencodeAdapter as opencodeAdapter2 } from "@birdybeep/opencode";
312
367
 
313
368
  // src/config.ts
@@ -332,6 +387,8 @@ function writeCliConfig(patch) {
332
387
  const merged = {};
333
388
  const apiUrl = patch.apiUrl ?? current.apiUrl;
334
389
  if (apiUrl !== void 0) merged.apiUrl = apiUrl;
390
+ const expectEmail = patch.expectEmail ?? current.expectEmail;
391
+ if (expectEmail !== void 0) merged.expectEmail = expectEmail;
335
392
  mkdirSync2(birdyBeepConfigDir2(), { recursive: true, mode: 448 });
336
393
  writeFileSync(cliConfigPath(), `${JSON.stringify(merged, null, 2)}
337
394
  `, { mode: 384 });
@@ -341,6 +398,12 @@ function resolveApiUrl() {
341
398
  if (env !== void 0 && env.length > 0) return env;
342
399
  return readCliConfig().apiUrl ?? DEFAULT_API_URL;
343
400
  }
401
+ var DEFAULT_REGISTRY_URL = "https://registry.npmjs.org";
402
+ function resolveRegistryUrl() {
403
+ const env = process.env["npm_config_registry"];
404
+ if (env !== void 0 && env.length > 0) return env;
405
+ return DEFAULT_REGISTRY_URL;
406
+ }
344
407
 
345
408
  // src/diagnostics.ts
346
409
  import {
@@ -368,7 +431,13 @@ function machineIdentity() {
368
431
  }
369
432
 
370
433
  // src/commands/doctor.ts
371
- var DEFAULT_ADAPTERS2 = [claudeCodeAdapter2, codexAdapter2, opencodeAdapter2];
434
+ var DEFAULT_ADAPTERS2 = [
435
+ claudeCodeAdapter2,
436
+ codexAdapter2,
437
+ opencodeAdapter2,
438
+ cursorAdapter2,
439
+ copilotAdapter2
440
+ ];
372
441
  async function defaultProbeNetwork(baseUrl) {
373
442
  try {
374
443
  const controller = new AbortController();
@@ -451,18 +520,36 @@ function createDoctorCommand(deps = {}) {
451
520
  }
452
521
 
453
522
  // src/commands/hook.ts
523
+ import { spawn } from "child_process";
524
+ import { randomBytes } from "crypto";
525
+ import { closeSync, openSync, rmSync, writeFileSync as writeFileSync2 } from "fs";
526
+ import { tmpdir } from "os";
527
+ import { basename, dirname, join as join2 } from "path";
454
528
  import {
455
- createSender as defaultCreateSender2
529
+ createSender as defaultCreateSender2,
530
+ resolveOnPath
456
531
  } from "@birdybeep/agent-core";
457
532
  import { runClaudeHook } from "@birdybeep/claude-code";
458
533
  import { runCodexHook } from "@birdybeep/codex";
534
+ import {
535
+ isCopilotHookEventName,
536
+ runCopilotHook
537
+ } from "@birdybeep/copilot";
538
+ import { runCursorHook } from "@birdybeep/cursor";
459
539
  import { runOpenCodeHook } from "@birdybeep/opencode";
460
540
  var RUNNERS = {
461
541
  claude: runClaudeHook,
462
542
  codex: runCodexHook,
463
- opencode: runOpenCodeHook
543
+ opencode: runOpenCodeHook,
544
+ cursor: runCursorHook
464
545
  };
465
- var HOOK_HARNESSES = ["claude", "codex", "opencode"];
546
+ var HOOK_HARNESSES = [
547
+ "claude",
548
+ "codex",
549
+ "opencode",
550
+ "cursor",
551
+ "copilot"
552
+ ];
466
553
  var STDIN_READ_TIMEOUT_MS = 3e3;
467
554
  function withTimeout(promise, ms, fallback) {
468
555
  return new Promise((resolve) => {
@@ -479,9 +566,13 @@ function withTimeout(promise, ms, fallback) {
479
566
  });
480
567
  }
481
568
  function isHarnessName(value) {
482
- return value === "claude" || value === "codex" || value === "opencode";
569
+ return value === "claude" || value === "codex" || value === "opencode" || value === "cursor" || value === "copilot";
483
570
  }
484
- function runHookCommand(harness, payload, sender) {
571
+ function runHookCommand(harness, payload, sender, copilotEventName) {
572
+ if (harness === "copilot") {
573
+ if (copilotEventName === void 0) return Promise.resolve({ outcome: "skipped" });
574
+ return runCopilotHook(copilotEventName, payload, { sender });
575
+ }
485
576
  return RUNNERS[harness](payload, { sender });
486
577
  }
487
578
  function readStdinDefault() {
@@ -497,24 +588,90 @@ function readStdinDefault() {
497
588
  process.stdin.on("error", () => resolve(""));
498
589
  });
499
590
  }
500
- async function readHookPayload(args, readStdin) {
501
- return args[1] ?? await readStdin();
591
+ async function readHookPayload(args, readStdin, stdinOnly = false) {
592
+ return stdinOnly ? readStdin() : args[1] ?? await readStdin();
593
+ }
594
+ var NOTIFY_STDIN_FILE_ENV = "BIRDYBEEP_CODEX_NOTIFY_STDIN_FILE";
595
+ function detachCodexNotifyWorker(payload) {
596
+ if (process.platform === "win32") return false;
597
+ let file;
598
+ let fd;
599
+ try {
600
+ const birdybeep = resolveOnPath("birdybeep");
601
+ if (birdybeep === null) return false;
602
+ const tmpFile = join2(tmpdir(), `birdybeep-notify-${randomBytes(16).toString("hex")}.json`);
603
+ file = tmpFile;
604
+ writeFileSync2(tmpFile, payload, { mode: 384 });
605
+ fd = openSync(tmpFile, "r");
606
+ const child = spawn(birdybeep, ["hook", "codex"], {
607
+ cwd: dirname(birdybeep),
608
+ // trusted dir, never the inherited/attacker cwd
609
+ detached: true,
610
+ // new session (setsid) → survives `codex exec` reaping the group
611
+ stdio: [fd, "ignore", "ignore"],
612
+ // stdin = the temp file; this process holds no pipe
613
+ env: { ...process.env, [NOTIFY_STDIN_FILE_ENV]: tmpFile },
614
+ // worker cleans it up post-read
615
+ windowsHide: true
616
+ });
617
+ child.on("error", () => {
618
+ try {
619
+ rmSync(tmpFile, { force: true });
620
+ } catch {
621
+ }
622
+ });
623
+ child.unref();
624
+ return true;
625
+ } catch {
626
+ if (file !== void 0) {
627
+ try {
628
+ rmSync(file, { force: true });
629
+ } catch {
630
+ }
631
+ }
632
+ return false;
633
+ } finally {
634
+ if (fd !== void 0) {
635
+ try {
636
+ closeSync(fd);
637
+ } catch {
638
+ }
639
+ }
640
+ }
502
641
  }
503
642
  function createHookCommand(deps = {}) {
504
643
  const makeSender = deps.createSender ?? ((baseUrl) => defaultCreateSender2({ baseUrl }));
505
644
  const readStdin = deps.readStdin ?? readStdinDefault;
506
645
  const stdinTimeoutMs = deps.stdinTimeoutMs ?? STDIN_READ_TIMEOUT_MS;
646
+ const detachCodexNotify = deps.detachCodexNotify ?? detachCodexNotifyWorker;
507
647
  return {
508
648
  name: "hook",
509
649
  summary: "Internal: normalize + send an event fired by a harness hook",
510
- usage: "birdybeep hook <claude|codex|opencode>",
650
+ usage: "birdybeep hook <claude|codex|opencode|cursor|copilot> [copilot-event]",
511
651
  run: async (ctx) => {
512
652
  const harness = ctx.args[0];
513
653
  if (!isHarnessName(harness)) {
514
654
  ctx.io.errline(`birdybeep hook: expected one of ${HOOK_HARNESSES.join("|")}`);
515
655
  return EXIT.USAGE;
516
656
  }
517
- const raw = await withTimeout(readHookPayload(ctx.args, readStdin), stdinTimeoutMs, "");
657
+ const notifyPayload = ctx.args[1];
658
+ if (harness === "codex" && notifyPayload !== void 0 && notifyPayload.length > 0 && detachCodexNotify(notifyPayload)) {
659
+ ctx.io.result({ harness, outcome: "detached" });
660
+ return EXIT.OK;
661
+ }
662
+ const copilotEventName = harness === "copilot" && isCopilotHookEventName(ctx.args[1]) ? ctx.args[1] : void 0;
663
+ const raw = await withTimeout(
664
+ readHookPayload(ctx.args, readStdin, harness === "copilot"),
665
+ stdinTimeoutMs,
666
+ ""
667
+ );
668
+ const notifyStdinFile = process.env[NOTIFY_STDIN_FILE_ENV];
669
+ if (notifyStdinFile !== void 0 && dirname(notifyStdinFile) === tmpdir() && basename(notifyStdinFile).startsWith("birdybeep-notify-")) {
670
+ try {
671
+ rmSync(notifyStdinFile, { force: true });
672
+ } catch {
673
+ }
674
+ }
518
675
  let payload;
519
676
  try {
520
677
  payload = JSON.parse(raw);
@@ -523,52 +680,79 @@ function createHookCommand(deps = {}) {
523
680
  return EXIT.OK;
524
681
  }
525
682
  const sender = makeSender(resolveApiUrl());
526
- const result = await runHookCommand(harness, payload, sender);
527
- ctx.io.result({ harness, outcome: result.outcome, eventType: result.eventType });
683
+ const result = await runHookCommand(harness, payload, sender, copilotEventName);
684
+ ctx.io.result({
685
+ harness,
686
+ ...copilotEventName !== void 0 ? { event: copilotEventName } : {},
687
+ outcome: result.outcome,
688
+ eventType: result.eventType,
689
+ ...result.send?.decision ? { decision: result.send.decision } : {},
690
+ ...result.send?.status !== void 0 ? { status: result.send.status } : {}
691
+ });
528
692
  return EXIT.OK;
529
693
  }
530
694
  };
531
695
  }
532
696
 
533
697
  // src/commands/logout.ts
534
- import { clearToken } from "@birdybeep/agent-core";
535
- function createClearTokenCommand(spec, deps = {}) {
698
+ import { clearToken, getToken as getToken2 } from "@birdybeep/agent-core";
699
+ var base = (apiUrl) => apiUrl.replace(/\/$/, "");
700
+ function createLogoutCommand(deps = {}) {
536
701
  return {
537
- name: spec.name,
538
- summary: spec.summary,
539
- usage: `birdybeep ${spec.name}`,
702
+ name: "logout",
703
+ summary: "Remove the local machine token (does NOT revoke the machine server-side)",
704
+ usage: "birdybeep logout",
540
705
  run: async (ctx) => {
541
706
  await clearToken(deps.tokenOptions ?? {});
542
- ctx.io.emit(spec.humanMessage, { [spec.jsonKey]: true });
707
+ ctx.io.emit("Logged out \u2014 the machine token was removed.", { loggedOut: true });
543
708
  return EXIT.OK;
544
709
  }
545
710
  };
546
711
  }
547
- function createLogoutCommand(deps = {}) {
548
- return createClearTokenCommand(
549
- {
550
- name: "logout",
551
- summary: "Remove the local machine token (same as `unpair`)",
552
- humanMessage: "Logged out \u2014 the machine token was removed.",
553
- jsonKey: "loggedOut"
554
- },
555
- deps
556
- );
712
+ async function revokeSelf(token, fetchImpl, timeoutMs) {
713
+ const controller = new AbortController();
714
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
715
+ try {
716
+ const res = await fetchImpl(`${base(resolveApiUrl())}/v1/machine/revoke-self`, {
717
+ method: "POST",
718
+ headers: { authorization: `Bearer ${token}` },
719
+ signal: controller.signal
720
+ });
721
+ if (res.ok || res.status === 403) return "revoked";
722
+ return "rejected";
723
+ } catch {
724
+ return "unreachable";
725
+ } finally {
726
+ clearTimeout(timer);
727
+ }
557
728
  }
558
729
  function createUnpairCommand(deps = {}) {
559
- return createClearTokenCommand(
560
- {
561
- name: "unpair",
562
- summary: "Unpair this machine \u2014 remove the local machine token (same as `logout`)",
563
- humanMessage: "Unpaired \u2014 the machine token was removed.",
564
- jsonKey: "unpaired"
565
- },
566
- deps
567
- );
730
+ const fetchImpl = deps.fetchImpl ?? fetch;
731
+ const timeoutMs = deps.timeoutMs ?? 1e4;
732
+ return {
733
+ name: "unpair",
734
+ summary: "Unpair this machine \u2014 revoke it server-side and remove the local token",
735
+ usage: "birdybeep unpair",
736
+ run: async (ctx) => {
737
+ const token = await getToken2(deps.tokenOptions ?? {});
738
+ const outcome = token === null ? "no_token" : await revokeSelf(token, fetchImpl, timeoutMs);
739
+ await clearToken(deps.tokenOptions ?? {});
740
+ const serverRevoked = outcome === "revoked";
741
+ 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.";
742
+ ctx.io.emit(human, { unpaired: true, serverRevoked });
743
+ return EXIT.OK;
744
+ }
745
+ };
568
746
  }
569
747
 
570
748
  // src/commands/pair.ts
571
- import { getMachineIdentity as getMachineIdentity2, setToken } from "@birdybeep/agent-core";
749
+ import { closeSync as closeSync2, openSync as openSync2 } from "fs";
750
+ import {
751
+ deriveCodeChallengeS256,
752
+ generateCodeVerifier,
753
+ getMachineIdentity as getMachineIdentity2,
754
+ setToken
755
+ } from "@birdybeep/agent-core";
572
756
  import { renderUnicodeCompact } from "uqr";
573
757
 
574
758
  // src/pairing.ts
@@ -577,16 +761,17 @@ import {
577
761
  pairStartResponseSchema,
578
762
  pairTokenResponseSchema
579
763
  } from "@birdybeep/agent-core";
580
- function base(apiUrl) {
764
+ function base2(apiUrl) {
581
765
  return apiUrl.replace(/\/$/, "");
582
766
  }
583
767
  async function pairStart(apiUrl, input, fetchImpl) {
584
768
  const body = {
585
769
  machine_label: input.machineLabel,
586
770
  ...input.os !== void 0 ? { os: input.os } : {},
587
- ...input.cliVersion !== void 0 ? { cli_version: input.cliVersion } : {}
771
+ ...input.cliVersion !== void 0 ? { cli_version: input.cliVersion } : {},
772
+ ...input.codeChallenge !== void 0 ? { code_challenge: input.codeChallenge } : {}
588
773
  };
589
- const res = await fetchImpl(`${base(apiUrl)}/v1/pair/start`, {
774
+ const res = await fetchImpl(`${base2(apiUrl)}/v1/pair/start`, {
590
775
  method: "POST",
591
776
  headers: { "content-type": "application/json" },
592
777
  body: JSON.stringify(body)
@@ -604,12 +789,13 @@ var TERMINAL_TOKEN_ERRORS = /* @__PURE__ */ new Set([
604
789
  "not_found",
605
790
  "payload_too_large"
606
791
  ]);
607
- async function pairTokenPoll(apiUrl, deviceCode, fetchImpl, machineFingerprint) {
792
+ async function pairTokenPoll(apiUrl, deviceCode, fetchImpl, machineFingerprint, codeVerifier) {
608
793
  const body = {
609
794
  device_code: deviceCode,
610
- ...machineFingerprint !== void 0 ? { machine_fingerprint: machineFingerprint } : {}
795
+ ...machineFingerprint !== void 0 ? { machine_fingerprint: machineFingerprint } : {},
796
+ ...codeVerifier !== void 0 ? { code_verifier: codeVerifier } : {}
611
797
  };
612
- const res = await fetchImpl(`${base(apiUrl)}/v1/pair/token`, {
798
+ const res = await fetchImpl(`${base2(apiUrl)}/v1/pair/token`, {
613
799
  method: "POST",
614
800
  headers: { "content-type": "application/json" },
615
801
  body: JSON.stringify(body)
@@ -620,7 +806,10 @@ async function pairTokenPoll(apiUrl, deviceCode, fetchImpl, machineFingerprint)
620
806
  return {
621
807
  status: "paired",
622
808
  machineToken: parsed.data.machine_token,
623
- machineId: parsed.data.machine_id
809
+ machineId: parsed.data.machine_id,
810
+ // Only surface the key when the server reported it (exactOptionalPropertyTypes: no explicit
811
+ // undefined). Older servers omit approved_by_email; newer ones (dgxd) include it.
812
+ ...parsed.data.approved_by_email !== void 0 ? { approvedByEmail: parsed.data.approved_by_email } : {}
624
813
  };
625
814
  }
626
815
  let errBody = null;
@@ -646,22 +835,172 @@ var HEARTBEAT_MS = 15e3;
646
835
  function renderQrMatrix(qrPayload) {
647
836
  return renderUnicodeCompact(qrPayload, { border: 2 });
648
837
  }
838
+ function parsePairFlags(args) {
839
+ const flags = { yes: false };
840
+ for (let i = 0; i < args.length; i += 1) {
841
+ const token = args[i] ?? "";
842
+ if (token === "--yes" || token === "-y") {
843
+ flags.yes = true;
844
+ } else if (token === "--expect-email" || token.startsWith("--expect-email=")) {
845
+ const inline = token.startsWith("--expect-email=") ? token.slice("--expect-email=".length) : void 0;
846
+ const value = inline ?? args[++i];
847
+ if (value === void 0 || value.length === 0 || value.startsWith("-")) {
848
+ return { ...flags, error: "--expect-email requires an email address" };
849
+ }
850
+ flags.expectEmail = value;
851
+ } else {
852
+ return { ...flags, error: `unexpected argument "${token}"` };
853
+ }
854
+ }
855
+ return flags;
856
+ }
857
+ function sameEmail(a, b) {
858
+ const fold = (v) => v.trim().toLowerCase();
859
+ const rawEqual = fold(a) === fold(b);
860
+ const nfkcEqual = fold(a.normalize("NFKC")) === fold(b.normalize("NFKC"));
861
+ return rawEqual && nfkcEqual;
862
+ }
863
+ function decidePairConfirmation(input) {
864
+ const { approvedByEmail, expectEmail } = input;
865
+ const platform = input.platform ?? process.platform;
866
+ if (expectEmail !== void 0) {
867
+ if (approvedByEmail === void 0) {
868
+ 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.";
869
+ return {
870
+ action: "reject",
871
+ reason: "expected_email_unverifiable",
872
+ 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}`
873
+ };
874
+ }
875
+ if (sameEmail(approvedByEmail, expectEmail)) {
876
+ return { action: "approve", reason: "expected_email_match" };
877
+ }
878
+ return {
879
+ action: "reject",
880
+ reason: "expected_email_mismatch",
881
+ 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\`.`
882
+ };
883
+ }
884
+ if (input.yes) return { action: "approve", reason: "yes_flag" };
885
+ 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] ";
886
+ if (!input.nonInteractive) {
887
+ if (input.stdinIsTTY) return { action: "prompt", question, on: "stdin" };
888
+ if (input.controllingTerminalAvailable) {
889
+ return { action: "prompt", question, on: "controlling-terminal" };
890
+ }
891
+ }
892
+ const who = approvedByEmail !== void 0 ? ` (approved by ${approvedByEmail})` : "";
893
+ const winptyHint = platform === "win32" && !input.nonInteractive ? " In Git Bash / MSYS, `winpty birdybeep pair` attaches a real console so the prompt can appear." : "";
894
+ return {
895
+ action: "reject",
896
+ reason: "non_interactive",
897
+ 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
898
+ };
899
+ }
900
+ function isAffirmative(answer) {
901
+ return /^(y|yes)$/i.test(answer.trim());
902
+ }
903
+ function controllingTerminalPath() {
904
+ return "/dev/tty";
905
+ }
906
+ function canOpenControllingTerminal(path = controllingTerminalPath(), platform = process.platform) {
907
+ if (platform === "win32") return false;
908
+ let fd;
909
+ try {
910
+ fd = openSync2(path, "r");
911
+ return true;
912
+ } catch {
913
+ return false;
914
+ } finally {
915
+ if (fd !== void 0) {
916
+ try {
917
+ closeSync2(fd);
918
+ } catch {
919
+ }
920
+ }
921
+ }
922
+ }
923
+ async function promptForAnswer(question, on) {
924
+ const { createInterface } = await import("readline/promises");
925
+ let ttyFd;
926
+ let input;
927
+ if (on === "stdin") {
928
+ input = process.stdin;
929
+ } else {
930
+ const { ReadStream } = await import("tty");
931
+ ttyFd = openSync2(controllingTerminalPath(), "r");
932
+ input = new ReadStream(ttyFd);
933
+ }
934
+ return new Promise((resolve) => {
935
+ const rl = createInterface({ input, output: process.stderr });
936
+ let settled = false;
937
+ const done = (value) => {
938
+ if (settled) return;
939
+ settled = true;
940
+ rl.close();
941
+ if (on === "stdin") {
942
+ process.stdin.unref?.();
943
+ } else {
944
+ try {
945
+ input.unref?.();
946
+ input.destroy?.();
947
+ } catch {
948
+ }
949
+ if (ttyFd !== void 0) {
950
+ try {
951
+ closeSync2(ttyFd);
952
+ } catch {
953
+ }
954
+ }
955
+ }
956
+ resolve(value);
957
+ };
958
+ rl.question(question).then(done, () => done(""));
959
+ rl.once("close", () => done(""));
960
+ input.once?.("error", () => done(""));
961
+ });
962
+ }
649
963
  function createPairCommand(deps = {}) {
650
964
  const fetchImpl = deps.fetchImpl ?? fetch;
651
965
  const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
652
966
  const clock = deps.now ?? (() => Date.now());
653
967
  const renderQr = deps.renderQr ?? renderQrMatrix;
654
968
  const intervalMs = deps.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
969
+ const promptLine = deps.promptLine ?? promptForAnswer;
970
+ const hasControllingTerminal = deps.hasControllingTerminal ?? (() => canOpenControllingTerminal());
971
+ const configuredExpectEmail = deps.configuredExpectEmail ?? (() => {
972
+ const pinned = readCliConfig().expectEmail;
973
+ return typeof pinned === "string" && pinned.trim().length > 0 ? pinned : void 0;
974
+ });
655
975
  return {
656
976
  name: "pair",
657
977
  summary: "Pair this machine with your BirdyBeep account (QR or manual)",
658
- usage: "birdybeep pair [--json]",
978
+ usage: "birdybeep pair [--yes] [--expect-email <addr>] [--json]",
979
+ options: [
980
+ {
981
+ flag: "--yes",
982
+ aliases: ["-y"],
983
+ summary: "Skip the approving-account confirmation (headless/CI)"
984
+ },
985
+ {
986
+ flag: "--expect-email",
987
+ value: "<addr>",
988
+ summary: "Only trust the pairing if this account approved it (else fail)"
989
+ }
990
+ ],
659
991
  run: async (ctx) => {
992
+ const pairFlags = parsePairFlags(ctx.args);
993
+ if (pairFlags.error !== void 0) {
994
+ ctx.io.errline(`birdybeep pair: ${pairFlags.error}.`);
995
+ return EXIT.USAGE;
996
+ }
660
997
  const apiUrl = resolveApiUrl();
661
998
  const identity = getMachineIdentity2();
999
+ const codeVerifier = generateCodeVerifier();
1000
+ const codeChallenge = deriveCodeChallengeS256(codeVerifier);
662
1001
  const start = await pairStart(
663
1002
  apiUrl,
664
- { machineLabel: identity.label, os: identity.os, cliVersion: CLI_VERSION },
1003
+ { machineLabel: identity.label, os: identity.os, cliVersion: CLI_VERSION, codeChallenge },
665
1004
  fetchImpl
666
1005
  );
667
1006
  if (ctx.flags.json) {
@@ -673,12 +1012,14 @@ function createPairCommand(deps = {}) {
673
1012
  });
674
1013
  } else {
675
1014
  ctx.io.line(
676
- "To pair this machine, open the BirdyBeep app, tap \u201Cpair a machine\u201D, and scan this QR (or enter the code):"
1015
+ "To pair this machine, open the BirdyBeep app, tap \u201Cpair a machine\u201D, and scan this QR or open the complete link:"
677
1016
  );
678
1017
  const isTTY = deps.isTTY ?? process.stdout.isTTY === true;
679
1018
  if (isTTY) ctx.io.line(renderQr(start.qr_payload));
680
1019
  ctx.io.line(` Scan or open: ${start.qr_payload}`);
681
- ctx.io.line(` Code: ${start.user_code}`);
1020
+ ctx.io.line(
1021
+ ` Session code (display only; cannot approve by itself): ${start.user_code}`
1022
+ );
682
1023
  ctx.io.line("Waiting for you to approve this machine in the app\u2026");
683
1024
  }
684
1025
  const deadline = Date.parse(start.expires_at);
@@ -694,7 +1035,9 @@ function createPairCommand(deps = {}) {
694
1035
  apiUrl,
695
1036
  start.device_code,
696
1037
  fetchImpl,
697
- identity.fingerprintHash
1038
+ identity.fingerprintHash,
1039
+ codeVerifier
1040
+ // PKCE proof-of-possession (dgxd) — sent on every poll
698
1041
  );
699
1042
  if (poll.status === "paired") {
700
1043
  paired = poll;
@@ -719,15 +1062,44 @@ function createPairCommand(deps = {}) {
719
1062
  if (paired === void 0 || paired.status !== "paired") {
720
1063
  ctx.io.result({ paired: false, reason: "timeout" });
721
1064
  ctx.io.errline(
722
- "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."
1065
+ "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."
1066
+ );
1067
+ return EXIT.ERROR;
1068
+ }
1069
+ const approvedBy = paired.approvedByEmail;
1070
+ const expectEmail = pairFlags.expectEmail ?? configuredExpectEmail();
1071
+ const stdinIsTTY = deps.isStdinTTY ?? process.stdin.isTTY === true;
1072
+ const decision = decidePairConfirmation({
1073
+ ...approvedBy !== void 0 ? { approvedByEmail: approvedBy } : {},
1074
+ ...expectEmail !== void 0 ? { expectEmail } : {},
1075
+ ...expectEmail !== void 0 ? { expectEmailSource: pairFlags.expectEmail !== void 0 ? "flag" : "config" } : {},
1076
+ yes: pairFlags.yes,
1077
+ nonInteractive: ctx.flags.nonInteractive,
1078
+ stdinIsTTY,
1079
+ // Probed ONLY when stdin can't answer — opening /dev/tty is a syscall, and when stdin is
1080
+ // already a terminal the answer is irrelevant.
1081
+ controllingTerminalAvailable: stdinIsTTY ? false : hasControllingTerminal(),
1082
+ configPath: cliConfigPath()
1083
+ });
1084
+ if (decision.action === "reject") {
1085
+ ctx.io.result({ paired: false, reason: decision.reason });
1086
+ ctx.io.errline(decision.message);
1087
+ return EXIT.ERROR;
1088
+ }
1089
+ if (decision.action === "prompt" && !isAffirmative(await promptLine(decision.question, decision.on))) {
1090
+ ctx.io.result({ paired: false, reason: "declined" });
1091
+ ctx.io.errline(
1092
+ "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."
723
1093
  );
724
1094
  return EXIT.ERROR;
725
1095
  }
726
1096
  await setToken(paired.machineToken, deps.tokenOptions ?? {});
727
1097
  writeCliConfig({ apiUrl });
728
- ctx.io.emit(`\u2713 Paired. Run \`birdybeep test\` to send a test Beep.`, {
1098
+ const humanSuffix = approvedBy !== void 0 ? ` to ${approvedBy}` : "";
1099
+ ctx.io.emit(`\u2713 Paired${humanSuffix}. Run \`birdybeep test\` to send a test Beep.`, {
729
1100
  paired: true,
730
- machineId: paired.machineId
1101
+ machineId: paired.machineId,
1102
+ ...approvedBy !== void 0 ? { approvedByEmail: approvedBy } : {}
731
1103
  });
732
1104
  return EXIT.OK;
733
1105
  }
@@ -759,19 +1131,29 @@ function createQueueCommand() {
759
1131
  // src/commands/report-status.ts
760
1132
  import {
761
1133
  errorEnvelopeSchema as errorEnvelopeSchema2,
762
- getToken as getToken2,
1134
+ getToken as getToken3,
763
1135
  integrationStatusResponseSchema
764
1136
  } from "@birdybeep/agent-core";
765
1137
  import { CLAUDE_CODE_ADAPTER_VERSION, claudeCodeAdapter as claudeCodeAdapter3 } from "@birdybeep/claude-code";
766
1138
  import { CODEX_ADAPTER_VERSION, codexAdapter as codexAdapter3 } from "@birdybeep/codex";
1139
+ import { COPILOT_ADAPTER_VERSION, copilotAdapter as copilotAdapter3 } from "@birdybeep/copilot";
1140
+ import { CURSOR_ADAPTER_VERSION, cursorAdapter as cursorAdapter3 } from "@birdybeep/cursor";
767
1141
  import { OPENCODE_ADAPTER_VERSION, opencodeAdapter as opencodeAdapter3 } from "@birdybeep/opencode";
768
- var DEFAULT_ADAPTERS3 = [claudeCodeAdapter3, codexAdapter3, opencodeAdapter3];
1142
+ var DEFAULT_ADAPTERS3 = [
1143
+ claudeCodeAdapter3,
1144
+ codexAdapter3,
1145
+ opencodeAdapter3,
1146
+ cursorAdapter3,
1147
+ copilotAdapter3
1148
+ ];
769
1149
  var ADAPTER_VERSIONS = {
770
1150
  claude_code: CLAUDE_CODE_ADAPTER_VERSION,
771
1151
  codex: CODEX_ADAPTER_VERSION,
772
- opencode: OPENCODE_ADAPTER_VERSION
1152
+ opencode: OPENCODE_ADAPTER_VERSION,
1153
+ cursor: CURSOR_ADAPTER_VERSION,
1154
+ copilot: COPILOT_ADAPTER_VERSION
773
1155
  };
774
- var base2 = (apiUrl) => apiUrl.replace(/\/$/, "");
1156
+ var base3 = (apiUrl) => apiUrl.replace(/\/$/, "");
775
1157
  async function gatherItems(adapters) {
776
1158
  return Promise.all(
777
1159
  adapters.map(async (a) => {
@@ -792,7 +1174,7 @@ function createReportStatusCommand(deps = {}) {
792
1174
  summary: "Internal: report integration status to the backend",
793
1175
  usage: "birdybeep report-status [--json]",
794
1176
  run: async (ctx) => {
795
- const token = await getToken2(deps.tokenOptions ?? {});
1177
+ const token = await getToken3(deps.tokenOptions ?? {});
796
1178
  if (token === null) {
797
1179
  ctx.io.errline("No machine token \u2014 run `birdybeep pair` first.");
798
1180
  return EXIT.ERROR;
@@ -806,7 +1188,7 @@ function createReportStatusCommand(deps = {}) {
806
1188
  let outcome = "deferred";
807
1189
  let errorCode;
808
1190
  try {
809
- const res = await fetchImpl(`${base2(resolveApiUrl())}/v1/integrations/status`, {
1191
+ const res = await fetchImpl(`${base3(resolveApiUrl())}/v1/integrations/status`, {
810
1192
  method: "POST",
811
1193
  headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
812
1194
  body: JSON.stringify({ integrations: items })
@@ -859,8 +1241,16 @@ import {
859
1241
  } from "@birdybeep/agent-core";
860
1242
  import { claudeCodeAdapter as claudeCodeAdapter4 } from "@birdybeep/claude-code";
861
1243
  import { codexAdapter as codexAdapter4 } from "@birdybeep/codex";
1244
+ import { copilotAdapter as copilotAdapter4 } from "@birdybeep/copilot";
1245
+ import { cursorAdapter as cursorAdapter4 } from "@birdybeep/cursor";
862
1246
  import { opencodeAdapter as opencodeAdapter4 } from "@birdybeep/opencode";
863
- var DEFAULT_ADAPTERS4 = [claudeCodeAdapter4, codexAdapter4, opencodeAdapter4];
1247
+ var DEFAULT_ADAPTERS4 = [
1248
+ claudeCodeAdapter4,
1249
+ codexAdapter4,
1250
+ opencodeAdapter4,
1251
+ cursorAdapter4,
1252
+ copilotAdapter4
1253
+ ];
864
1254
  function createStatusCommand(deps = {}) {
865
1255
  const adapters = deps.adapters ?? DEFAULT_ADAPTERS4;
866
1256
  const makeSender = deps.createSender ?? ((baseUrl) => defaultCreateSender3(
@@ -985,13 +1375,152 @@ function buildCommands() {
985
1375
  ];
986
1376
  }
987
1377
 
1378
+ // src/update-check.ts
1379
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
1380
+ import { join as join3 } from "path";
1381
+ import { birdyBeepConfigDir as birdyBeepConfigDir3 } from "@birdybeep/agent-core";
1382
+ var PACKAGE_NAME = "@birdybeep/cli";
1383
+ var PACKAGE_PATH = "@birdybeep%2Fcli";
1384
+ var UPDATE_CACHE_FILE = "update-check.json";
1385
+ var DEFAULT_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
1386
+ var DEFAULT_TIMEOUT_MS = 1500;
1387
+ var SKIP_COMMANDS = /* @__PURE__ */ new Set(["hook", "report-status"]);
1388
+ var SEMVER_RE = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
1389
+ function parseSemver(input) {
1390
+ const m = SEMVER_RE.exec(input.trim());
1391
+ if (m === null) return null;
1392
+ return {
1393
+ major: Number(m[1]),
1394
+ minor: Number(m[2]),
1395
+ patch: Number(m[3]),
1396
+ prerelease: m[4] !== void 0 ? m[4].split(".") : []
1397
+ };
1398
+ }
1399
+ function comparePrerelease(a, b) {
1400
+ if (a.length === 0 && b.length === 0) return 0;
1401
+ if (a.length === 0) return 1;
1402
+ if (b.length === 0) return -1;
1403
+ const len = Math.min(a.length, b.length);
1404
+ for (let i = 0; i < len; i++) {
1405
+ const ai = a[i];
1406
+ const bi = b[i];
1407
+ const aNum = /^\d+$/.test(ai);
1408
+ const bNum = /^\d+$/.test(bi);
1409
+ if (aNum && bNum) {
1410
+ const d = Number(ai) - Number(bi);
1411
+ if (d !== 0) return d < 0 ? -1 : 1;
1412
+ } else if (aNum) {
1413
+ return -1;
1414
+ } else if (bNum) {
1415
+ return 1;
1416
+ } else if (ai !== bi) {
1417
+ return ai < bi ? -1 : 1;
1418
+ }
1419
+ }
1420
+ if (a.length === b.length) return 0;
1421
+ return a.length < b.length ? -1 : 1;
1422
+ }
1423
+ function compareSemver(a, b) {
1424
+ if (a.major !== b.major) return a.major < b.major ? -1 : 1;
1425
+ if (a.minor !== b.minor) return a.minor < b.minor ? -1 : 1;
1426
+ if (a.patch !== b.patch) return a.patch < b.patch ? -1 : 1;
1427
+ return comparePrerelease(a.prerelease, b.prerelease);
1428
+ }
1429
+ function isNewer(current, latest) {
1430
+ const cur = parseSemver(current);
1431
+ const lat = parseSemver(latest);
1432
+ return cur !== null && lat !== null && compareSemver(cur, lat) < 0;
1433
+ }
1434
+ function updateCachePath() {
1435
+ return join3(birdyBeepConfigDir3(), UPDATE_CACHE_FILE);
1436
+ }
1437
+ function readUpdateCache() {
1438
+ try {
1439
+ const parsed = JSON.parse(readFileSync2(updateCachePath(), "utf8"));
1440
+ if (typeof parsed !== "object" || parsed === null) return null;
1441
+ const { checkedAt, latest } = parsed;
1442
+ if (typeof checkedAt !== "number") return null;
1443
+ if (latest !== null && typeof latest !== "string") return null;
1444
+ return { checkedAt, latest };
1445
+ } catch {
1446
+ return null;
1447
+ }
1448
+ }
1449
+ function writeUpdateCache(cache) {
1450
+ mkdirSync3(birdyBeepConfigDir3(), { recursive: true, mode: 448 });
1451
+ writeFileSync3(updateCachePath(), `${JSON.stringify(cache)}
1452
+ `, { mode: 384 });
1453
+ }
1454
+ async function fetchLatestVersion(registryUrl, fetchImpl, timeoutMs) {
1455
+ const url = `${registryUrl.replace(/\/+$/, "")}/${PACKAGE_PATH}/latest`;
1456
+ const controller = new AbortController();
1457
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
1458
+ if (typeof timer.unref === "function") timer.unref();
1459
+ try {
1460
+ const res = await fetchImpl(url, {
1461
+ headers: { accept: "application/json" },
1462
+ signal: controller.signal
1463
+ });
1464
+ if (!res.ok) throw new Error(`registry responded ${res.status}`);
1465
+ const body = await res.json();
1466
+ if (typeof body.version !== "string" || body.version.length === 0) {
1467
+ throw new Error("registry response had no version");
1468
+ }
1469
+ return body.version;
1470
+ } finally {
1471
+ clearTimeout(timer);
1472
+ }
1473
+ }
1474
+ function renderNotice(current, latest) {
1475
+ return `a new version of birdybeep is available: ${current} \u2192 ${latest}
1476
+ upgrade with: npm install -g ${PACKAGE_NAME}@latest`;
1477
+ }
1478
+ async function maybeNotifyUpdate(opts) {
1479
+ try {
1480
+ if (opts.command !== void 0 && SKIP_COMMANDS.has(opts.command)) return;
1481
+ if (opts.flags.json || opts.flags.nonInteractive) return;
1482
+ const env = opts.env ?? process.env;
1483
+ if (env["BIRDYBEEP_NO_UPDATE_NOTIFIER"] || env["NO_UPDATE_NOTIFIER"] || env["CI"]) return;
1484
+ const isTTY = opts.isTTY ?? Boolean(process.stderr.isTTY);
1485
+ if (!isTTY) return;
1486
+ const current = opts.currentVersion ?? CLI_VERSION;
1487
+ const now = opts.now ?? Date.now();
1488
+ const intervalMs = opts.intervalMs ?? DEFAULT_CHECK_INTERVAL_MS;
1489
+ const readCache = opts.readCache ?? readUpdateCache;
1490
+ const writeCache = opts.writeCache ?? writeUpdateCache;
1491
+ let cache = readCache();
1492
+ if (cache === null || now - cache.checkedAt >= intervalMs) {
1493
+ let latest = cache?.latest ?? null;
1494
+ try {
1495
+ latest = await fetchLatestVersion(
1496
+ opts.registryUrl ?? resolveRegistryUrl(),
1497
+ opts.fetchImpl ?? fetch,
1498
+ opts.timeoutMs ?? DEFAULT_TIMEOUT_MS
1499
+ );
1500
+ } catch {
1501
+ }
1502
+ cache = { checkedAt: now, latest };
1503
+ try {
1504
+ writeCache(cache);
1505
+ } catch {
1506
+ }
1507
+ }
1508
+ if (cache.latest !== null && isNewer(current, cache.latest)) {
1509
+ opts.io.errline(renderNotice(current, cache.latest));
1510
+ }
1511
+ } catch {
1512
+ }
1513
+ }
1514
+
988
1515
  // src/cli.ts
989
1516
  function runCli(argv, deps = {}) {
1517
+ const notifyUpdate = deps.updateCheck === false ? void 0 : (ctx) => maybeNotifyUpdate({ ...ctx, ...deps.updateCheck ?? {} });
990
1518
  return dispatch(argv, {
991
1519
  version: CLI_VERSION,
992
1520
  commands: deps.commands ?? buildCommands(),
993
1521
  stdout: deps.stdout ?? process.stdout,
994
1522
  stderr: deps.stderr ?? process.stderr,
1523
+ ...notifyUpdate !== void 0 ? { notifyUpdate } : {},
995
1524
  ...deps.ensureConfig !== void 0 ? { ensureConfig: deps.ensureConfig } : {}
996
1525
  });
997
1526
  }
@@ -1007,4 +1536,4 @@ export {
1007
1536
  buildCommands,
1008
1537
  runCli
1009
1538
  };
1010
- //# sourceMappingURL=chunk-HGH5CDKD.js.map
1539
+ //# sourceMappingURL=chunk-U4EIHC5C.js.map