@birdybeep/cli 0.2.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,7 +239,8 @@ 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;
@@ -228,13 +268,28 @@ async function dispatch(argv, deps) {
228
268
  }
229
269
 
230
270
  // src/commands/agent.ts
231
- 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
+ ];
232
278
  var TARGET_TO_ID = {
233
279
  claude: "claude_code",
234
280
  codex: "codex",
235
- opencode: "opencode"
281
+ opencode: "opencode",
282
+ cursor: "cursor",
283
+ copilot: "copilot"
236
284
  };
237
- var AGENT_TARGETS = ["all", "claude", "codex", "opencode"];
285
+ var AGENT_TARGETS = [
286
+ "all",
287
+ "claude",
288
+ "codex",
289
+ "opencode",
290
+ "cursor",
291
+ "copilot"
292
+ ];
238
293
  function selectAdapters(target, adapters) {
239
294
  if (target === "all") return adapters;
240
295
  const id = TARGET_TO_ID[target];
@@ -325,18 +380,18 @@ function createAgentCommand(deps = {}) {
325
380
  return {
326
381
  name: "agent",
327
382
  summary: "Install or uninstall harness adapters",
328
- usage: "birdybeep agent <install|uninstall> [all|claude|codex|opencode]",
383
+ usage: "birdybeep agent <install|uninstall> [all|claude|codex|opencode|cursor|copilot]",
329
384
  subcommands: [
330
385
  {
331
386
  name: "install",
332
- summary: "Install adapters (all | claude | codex | opencode)",
333
- 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]",
334
389
  run: (ctx) => installSelected(adapters, ctx)
335
390
  },
336
391
  {
337
392
  name: "uninstall",
338
393
  summary: "Restore harness config to its pre-install state",
339
- usage: "birdybeep agent uninstall [all|claude|codex|opencode]",
394
+ usage: "birdybeep agent uninstall [all|claude|codex|opencode|cursor|copilot]",
340
395
  run: (ctx) => uninstallSelected(adapters, ctx)
341
396
  }
342
397
  ]
@@ -347,6 +402,8 @@ function createAgentCommand(deps = {}) {
347
402
  var import_agent_core4 = require("@birdybeep/agent-core");
348
403
  var import_claude_code2 = require("@birdybeep/claude-code");
349
404
  var import_codex2 = require("@birdybeep/codex");
405
+ var import_copilot2 = require("@birdybeep/copilot");
406
+ var import_cursor2 = require("@birdybeep/cursor");
350
407
  var import_opencode2 = require("@birdybeep/opencode");
351
408
 
352
409
  // src/config.ts
@@ -371,6 +428,8 @@ function writeCliConfig(patch) {
371
428
  const merged = {};
372
429
  const apiUrl = patch.apiUrl ?? current.apiUrl;
373
430
  if (apiUrl !== void 0) merged.apiUrl = apiUrl;
431
+ const expectEmail = patch.expectEmail ?? current.expectEmail;
432
+ if (expectEmail !== void 0) merged.expectEmail = expectEmail;
374
433
  (0, import_node_fs2.mkdirSync)((0, import_agent_core2.birdyBeepConfigDir)(), { recursive: true, mode: 448 });
375
434
  (0, import_node_fs2.writeFileSync)(cliConfigPath(), `${JSON.stringify(merged, null, 2)}
376
435
  `, { mode: 384 });
@@ -409,7 +468,13 @@ function machineIdentity() {
409
468
  }
410
469
 
411
470
  // src/commands/doctor.ts
412
- 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
+ ];
413
478
  async function defaultProbeNetwork(baseUrl) {
414
479
  try {
415
480
  const controller = new AbortController();
@@ -492,16 +557,30 @@ function createDoctorCommand(deps = {}) {
492
557
  }
493
558
 
494
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");
495
565
  var import_agent_core5 = require("@birdybeep/agent-core");
496
566
  var import_claude_code3 = require("@birdybeep/claude-code");
497
567
  var import_codex3 = require("@birdybeep/codex");
568
+ var import_copilot3 = require("@birdybeep/copilot");
569
+ var import_cursor3 = require("@birdybeep/cursor");
498
570
  var import_opencode3 = require("@birdybeep/opencode");
499
571
  var RUNNERS = {
500
572
  claude: import_claude_code3.runClaudeHook,
501
573
  codex: import_codex3.runCodexHook,
502
- opencode: import_opencode3.runOpenCodeHook
574
+ opencode: import_opencode3.runOpenCodeHook,
575
+ cursor: import_cursor3.runCursorHook
503
576
  };
504
- var HOOK_HARNESSES = ["claude", "codex", "opencode"];
577
+ var HOOK_HARNESSES = [
578
+ "claude",
579
+ "codex",
580
+ "opencode",
581
+ "cursor",
582
+ "copilot"
583
+ ];
505
584
  var STDIN_READ_TIMEOUT_MS = 3e3;
506
585
  function withTimeout(promise, ms, fallback) {
507
586
  return new Promise((resolve) => {
@@ -518,9 +597,13 @@ function withTimeout(promise, ms, fallback) {
518
597
  });
519
598
  }
520
599
  function isHarnessName(value) {
521
- return value === "claude" || value === "codex" || value === "opencode";
600
+ return value === "claude" || value === "codex" || value === "opencode" || value === "cursor" || value === "copilot";
522
601
  }
523
- 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
+ }
524
607
  return RUNNERS[harness](payload, { sender });
525
608
  }
526
609
  function readStdinDefault() {
@@ -536,24 +619,90 @@ function readStdinDefault() {
536
619
  process.stdin.on("error", () => resolve(""));
537
620
  });
538
621
  }
539
- async function readHookPayload(args, readStdin) {
540
- 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
+ }
541
672
  }
542
673
  function createHookCommand(deps = {}) {
543
674
  const makeSender = deps.createSender ?? ((baseUrl) => (0, import_agent_core5.createSender)({ baseUrl }));
544
675
  const readStdin = deps.readStdin ?? readStdinDefault;
545
676
  const stdinTimeoutMs = deps.stdinTimeoutMs ?? STDIN_READ_TIMEOUT_MS;
677
+ const detachCodexNotify = deps.detachCodexNotify ?? detachCodexNotifyWorker;
546
678
  return {
547
679
  name: "hook",
548
680
  summary: "Internal: normalize + send an event fired by a harness hook",
549
- usage: "birdybeep hook <claude|codex|opencode>",
681
+ usage: "birdybeep hook <claude|codex|opencode|cursor|copilot> [copilot-event]",
550
682
  run: async (ctx) => {
551
683
  const harness = ctx.args[0];
552
684
  if (!isHarnessName(harness)) {
553
685
  ctx.io.errline(`birdybeep hook: expected one of ${HOOK_HARNESSES.join("|")}`);
554
686
  return EXIT.USAGE;
555
687
  }
556
- 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
+ }
557
706
  let payload;
558
707
  try {
559
708
  payload = JSON.parse(raw);
@@ -562,8 +711,15 @@ function createHookCommand(deps = {}) {
562
711
  return EXIT.OK;
563
712
  }
564
713
  const sender = makeSender(resolveApiUrl());
565
- const result = await runHookCommand(harness, payload, sender);
566
- 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
+ });
567
723
  return EXIT.OK;
568
724
  }
569
725
  };
@@ -571,57 +727,73 @@ function createHookCommand(deps = {}) {
571
727
 
572
728
  // src/commands/logout.ts
573
729
  var import_agent_core6 = require("@birdybeep/agent-core");
574
- function createClearTokenCommand(spec, deps = {}) {
730
+ var base = (apiUrl) => apiUrl.replace(/\/$/, "");
731
+ function createLogoutCommand(deps = {}) {
575
732
  return {
576
- name: spec.name,
577
- summary: spec.summary,
578
- 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",
579
736
  run: async (ctx) => {
580
737
  await (0, import_agent_core6.clearToken)(deps.tokenOptions ?? {});
581
- ctx.io.emit(spec.humanMessage, { [spec.jsonKey]: true });
738
+ ctx.io.emit("Logged out \u2014 the machine token was removed.", { loggedOut: true });
582
739
  return EXIT.OK;
583
740
  }
584
741
  };
585
742
  }
586
- function createLogoutCommand(deps = {}) {
587
- return createClearTokenCommand(
588
- {
589
- name: "logout",
590
- summary: "Remove the local machine token (same as `unpair`)",
591
- humanMessage: "Logged out \u2014 the machine token was removed.",
592
- jsonKey: "loggedOut"
593
- },
594
- deps
595
- );
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
+ }
596
759
  }
597
760
  function createUnpairCommand(deps = {}) {
598
- return createClearTokenCommand(
599
- {
600
- name: "unpair",
601
- summary: "Unpair this machine \u2014 remove the local machine token (same as `logout`)",
602
- humanMessage: "Unpaired \u2014 the machine token was removed.",
603
- jsonKey: "unpaired"
604
- },
605
- deps
606
- );
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
+ };
607
777
  }
608
778
 
609
779
  // src/commands/pair.ts
780
+ var import_node_fs4 = require("fs");
610
781
  var import_agent_core8 = require("@birdybeep/agent-core");
611
782
  var import_uqr = require("uqr");
612
783
 
613
784
  // src/pairing.ts
614
785
  var import_agent_core7 = require("@birdybeep/agent-core");
615
- function base(apiUrl) {
786
+ function base2(apiUrl) {
616
787
  return apiUrl.replace(/\/$/, "");
617
788
  }
618
789
  async function pairStart(apiUrl, input, fetchImpl) {
619
790
  const body = {
620
791
  machine_label: input.machineLabel,
621
792
  ...input.os !== void 0 ? { os: input.os } : {},
622
- ...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 } : {}
623
795
  };
624
- const res = await fetchImpl(`${base(apiUrl)}/v1/pair/start`, {
796
+ const res = await fetchImpl(`${base2(apiUrl)}/v1/pair/start`, {
625
797
  method: "POST",
626
798
  headers: { "content-type": "application/json" },
627
799
  body: JSON.stringify(body)
@@ -639,12 +811,13 @@ var TERMINAL_TOKEN_ERRORS = /* @__PURE__ */ new Set([
639
811
  "not_found",
640
812
  "payload_too_large"
641
813
  ]);
642
- async function pairTokenPoll(apiUrl, deviceCode, fetchImpl, machineFingerprint) {
814
+ async function pairTokenPoll(apiUrl, deviceCode, fetchImpl, machineFingerprint, codeVerifier) {
643
815
  const body = {
644
816
  device_code: deviceCode,
645
- ...machineFingerprint !== void 0 ? { machine_fingerprint: machineFingerprint } : {}
817
+ ...machineFingerprint !== void 0 ? { machine_fingerprint: machineFingerprint } : {},
818
+ ...codeVerifier !== void 0 ? { code_verifier: codeVerifier } : {}
646
819
  };
647
- const res = await fetchImpl(`${base(apiUrl)}/v1/pair/token`, {
820
+ const res = await fetchImpl(`${base2(apiUrl)}/v1/pair/token`, {
648
821
  method: "POST",
649
822
  headers: { "content-type": "application/json" },
650
823
  body: JSON.stringify(body)
@@ -655,7 +828,10 @@ async function pairTokenPoll(apiUrl, deviceCode, fetchImpl, machineFingerprint)
655
828
  return {
656
829
  status: "paired",
657
830
  machineToken: parsed.data.machine_token,
658
- 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 } : {}
659
835
  };
660
836
  }
661
837
  let errBody = null;
@@ -676,7 +852,7 @@ async function pairTokenPoll(apiUrl, deviceCode, fetchImpl, machineFingerprint)
676
852
  }
677
853
 
678
854
  // src/version.ts
679
- var CLI_VERSION = "0.2.0".length > 0 ? "0.2.0" : "0.0.0";
855
+ var CLI_VERSION = "0.3.0".length > 0 ? "0.3.0" : "0.0.0";
680
856
 
681
857
  // src/commands/pair.ts
682
858
  var DEFAULT_POLL_INTERVAL_MS = 2e3;
@@ -684,22 +860,172 @@ var HEARTBEAT_MS = 15e3;
684
860
  function renderQrMatrix(qrPayload) {
685
861
  return (0, import_uqr.renderUnicodeCompact)(qrPayload, { border: 2 });
686
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
+ }
687
988
  function createPairCommand(deps = {}) {
688
989
  const fetchImpl = deps.fetchImpl ?? fetch;
689
990
  const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
690
991
  const clock = deps.now ?? (() => Date.now());
691
992
  const renderQr = deps.renderQr ?? renderQrMatrix;
692
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
+ });
693
1000
  return {
694
1001
  name: "pair",
695
1002
  summary: "Pair this machine with your BirdyBeep account (QR or manual)",
696
- 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
+ ],
697
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
+ }
698
1022
  const apiUrl = resolveApiUrl();
699
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);
700
1026
  const start = await pairStart(
701
1027
  apiUrl,
702
- { machineLabel: identity.label, os: identity.os, cliVersion: CLI_VERSION },
1028
+ { machineLabel: identity.label, os: identity.os, cliVersion: CLI_VERSION, codeChallenge },
703
1029
  fetchImpl
704
1030
  );
705
1031
  if (ctx.flags.json) {
@@ -711,12 +1037,14 @@ function createPairCommand(deps = {}) {
711
1037
  });
712
1038
  } else {
713
1039
  ctx.io.line(
714
- "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:"
715
1041
  );
716
1042
  const isTTY = deps.isTTY ?? process.stdout.isTTY === true;
717
1043
  if (isTTY) ctx.io.line(renderQr(start.qr_payload));
718
1044
  ctx.io.line(` Scan or open: ${start.qr_payload}`);
719
- ctx.io.line(` Code: ${start.user_code}`);
1045
+ ctx.io.line(
1046
+ ` Session code (display only; cannot approve by itself): ${start.user_code}`
1047
+ );
720
1048
  ctx.io.line("Waiting for you to approve this machine in the app\u2026");
721
1049
  }
722
1050
  const deadline = Date.parse(start.expires_at);
@@ -732,7 +1060,9 @@ function createPairCommand(deps = {}) {
732
1060
  apiUrl,
733
1061
  start.device_code,
734
1062
  fetchImpl,
735
- identity.fingerprintHash
1063
+ identity.fingerprintHash,
1064
+ codeVerifier
1065
+ // PKCE proof-of-possession (dgxd) — sent on every poll
736
1066
  );
737
1067
  if (poll.status === "paired") {
738
1068
  paired = poll;
@@ -757,15 +1087,44 @@ function createPairCommand(deps = {}) {
757
1087
  if (paired === void 0 || paired.status !== "paired") {
758
1088
  ctx.io.result({ paired: false, reason: "timeout" });
759
1089
  ctx.io.errline(
760
- "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."
761
1118
  );
762
1119
  return EXIT.ERROR;
763
1120
  }
764
1121
  await (0, import_agent_core8.setToken)(paired.machineToken, deps.tokenOptions ?? {});
765
1122
  writeCliConfig({ apiUrl });
766
- 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.`, {
767
1125
  paired: true,
768
- machineId: paired.machineId
1126
+ machineId: paired.machineId,
1127
+ ...approvedBy !== void 0 ? { approvedByEmail: approvedBy } : {}
769
1128
  });
770
1129
  return EXIT.OK;
771
1130
  }
@@ -798,14 +1157,24 @@ function createQueueCommand() {
798
1157
  var import_agent_core10 = require("@birdybeep/agent-core");
799
1158
  var import_claude_code4 = require("@birdybeep/claude-code");
800
1159
  var import_codex4 = require("@birdybeep/codex");
1160
+ var import_copilot4 = require("@birdybeep/copilot");
1161
+ var import_cursor4 = require("@birdybeep/cursor");
801
1162
  var import_opencode4 = require("@birdybeep/opencode");
802
- 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
+ ];
803
1170
  var ADAPTER_VERSIONS = {
804
1171
  claude_code: import_claude_code4.CLAUDE_CODE_ADAPTER_VERSION,
805
1172
  codex: import_codex4.CODEX_ADAPTER_VERSION,
806
- 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
807
1176
  };
808
- var base2 = (apiUrl) => apiUrl.replace(/\/$/, "");
1177
+ var base3 = (apiUrl) => apiUrl.replace(/\/$/, "");
809
1178
  async function gatherItems(adapters) {
810
1179
  return Promise.all(
811
1180
  adapters.map(async (a) => {
@@ -840,7 +1209,7 @@ function createReportStatusCommand(deps = {}) {
840
1209
  let outcome = "deferred";
841
1210
  let errorCode;
842
1211
  try {
843
- const res = await fetchImpl(`${base2(resolveApiUrl())}/v1/integrations/status`, {
1212
+ const res = await fetchImpl(`${base3(resolveApiUrl())}/v1/integrations/status`, {
844
1213
  method: "POST",
845
1214
  headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
846
1215
  body: JSON.stringify({ integrations: items })
@@ -891,8 +1260,16 @@ function createReportStatusCommand(deps = {}) {
891
1260
  var import_agent_core11 = require("@birdybeep/agent-core");
892
1261
  var import_claude_code5 = require("@birdybeep/claude-code");
893
1262
  var import_codex5 = require("@birdybeep/codex");
1263
+ var import_copilot5 = require("@birdybeep/copilot");
1264
+ var import_cursor5 = require("@birdybeep/cursor");
894
1265
  var import_opencode5 = require("@birdybeep/opencode");
895
- 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
+ ];
896
1273
  function createStatusCommand(deps = {}) {
897
1274
  const adapters = deps.adapters ?? DEFAULT_ADAPTERS4;
898
1275
  const makeSender = deps.createSender ?? ((baseUrl) => (0, import_agent_core11.createSender)(
@@ -932,7 +1309,7 @@ function createStatusCommand(deps = {}) {
932
1309
  }
933
1310
 
934
1311
  // src/commands/test.ts
935
- var import_node_crypto = require("crypto");
1312
+ var import_node_crypto2 = require("crypto");
936
1313
  var import_agent_core12 = require("@birdybeep/agent-core");
937
1314
  function buildTestEvent(opts = {}) {
938
1315
  const machine = (0, import_agent_core12.getMachineIdentity)();
@@ -944,7 +1321,7 @@ function buildTestEvent(opts = {}) {
944
1321
  // schema requires a harness; the "test" type distinguishes it
945
1322
  // Unique per run: a repeat `birdybeep test` inside the backend's dedupe window must
946
1323
  // still beep — a constant id made the second test silently "deduped" (9fh).
947
- source_session_id: `birdybeep-cli-test-${(0, import_node_crypto.randomUUID)()}`,
1324
+ source_session_id: `birdybeep-cli-test-${(0, import_node_crypto2.randomUUID)()}`,
948
1325
  machine: { label: machine.label, os: machine.os },
949
1326
  workspace: { cwd: process.cwd() },
950
1327
  title: "BirdyBeep test event",
@@ -1014,8 +1391,8 @@ function buildCommands() {
1014
1391
  }
1015
1392
 
1016
1393
  // src/update-check.ts
1017
- var import_node_fs3 = require("fs");
1018
- var import_node_path2 = require("path");
1394
+ var import_node_fs5 = require("fs");
1395
+ var import_node_path3 = require("path");
1019
1396
  var import_agent_core13 = require("@birdybeep/agent-core");
1020
1397
  var PACKAGE_NAME = "@birdybeep/cli";
1021
1398
  var PACKAGE_PATH = "@birdybeep%2Fcli";
@@ -1070,11 +1447,11 @@ function isNewer(current, latest) {
1070
1447
  return cur !== null && lat !== null && compareSemver(cur, lat) < 0;
1071
1448
  }
1072
1449
  function updateCachePath() {
1073
- return (0, import_node_path2.join)((0, import_agent_core13.birdyBeepConfigDir)(), UPDATE_CACHE_FILE);
1450
+ return (0, import_node_path3.join)((0, import_agent_core13.birdyBeepConfigDir)(), UPDATE_CACHE_FILE);
1074
1451
  }
1075
1452
  function readUpdateCache() {
1076
1453
  try {
1077
- const parsed = JSON.parse((0, import_node_fs3.readFileSync)(updateCachePath(), "utf8"));
1454
+ const parsed = JSON.parse((0, import_node_fs5.readFileSync)(updateCachePath(), "utf8"));
1078
1455
  if (typeof parsed !== "object" || parsed === null) return null;
1079
1456
  const { checkedAt, latest } = parsed;
1080
1457
  if (typeof checkedAt !== "number") return null;
@@ -1085,8 +1462,8 @@ function readUpdateCache() {
1085
1462
  }
1086
1463
  }
1087
1464
  function writeUpdateCache(cache) {
1088
- (0, import_node_fs3.mkdirSync)((0, import_agent_core13.birdyBeepConfigDir)(), { recursive: true, mode: 448 });
1089
- (0, import_node_fs3.writeFileSync)(updateCachePath(), `${JSON.stringify(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)}
1090
1467
  `, { mode: 384 });
1091
1468
  }
1092
1469
  async function fetchLatestVersion(registryUrl, fetchImpl, timeoutMs) {