@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 +611 -69
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-HGH5CDKD.js → chunk-U4EIHC5C.js} +602 -73
- package/dist/chunk-U4EIHC5C.js.map +1 -0
- package/dist/index.cjs +599 -69
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +58 -1
- package/dist/index.d.ts +58 -1
- package/dist/index.js +1 -1
- package/package.json +7 -5
- package/dist/chunk-HGH5CDKD.js.map +0 -1
|
@@ -69,8 +69,11 @@ function parseGlobalFlags(argv) {
|
|
|
69
69
|
}
|
|
70
70
|
return { flags, rest };
|
|
71
71
|
}
|
|
72
|
-
function isUnknownFlag(token) {
|
|
73
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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 = [
|
|
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 = [
|
|
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 = [
|
|
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 = [
|
|
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
|
|
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({
|
|
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
|
-
|
|
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:
|
|
538
|
-
summary:
|
|
539
|
-
usage:
|
|
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(
|
|
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
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
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
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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 {
|
|
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
|
|
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(`${
|
|
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(`${
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
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 = [
|
|
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
|
|
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
|
|
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(`${
|
|
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 = [
|
|
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-
|
|
1539
|
+
//# sourceMappingURL=chunk-U4EIHC5C.js.map
|