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