@birdybeep/cli 0.0.1

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 ADDED
@@ -0,0 +1,941 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // src/commands/agent.ts
5
+ var import_claude_code = require("@birdybeep/claude-code");
6
+ var import_codex = require("@birdybeep/codex");
7
+ var import_opencode = require("@birdybeep/opencode");
8
+
9
+ // src/framework.ts
10
+ var import_node_fs = require("fs");
11
+ var import_agent_core = require("@birdybeep/agent-core");
12
+ var EXIT = { OK: 0, ERROR: 1, USAGE: 2 };
13
+ function createIo(json, stdout, stderr) {
14
+ return {
15
+ json,
16
+ line: (text) => {
17
+ if (!json) stdout.write(`${text}
18
+ `);
19
+ },
20
+ errline: (text) => stderr.write(`${text}
21
+ `),
22
+ result: (value) => {
23
+ if (json) stdout.write(`${JSON.stringify(value)}
24
+ `);
25
+ },
26
+ emit: (human, value) => {
27
+ if (json) stdout.write(`${JSON.stringify(value)}
28
+ `);
29
+ else stdout.write(`${human}
30
+ `);
31
+ }
32
+ };
33
+ }
34
+ var MissingInputError = class extends Error {
35
+ constructor(field) {
36
+ super(`missing required value: ${field}`);
37
+ this.field = field;
38
+ this.name = "MissingInputError";
39
+ }
40
+ field;
41
+ };
42
+ var GLOBAL_FLAG_TOKENS = /* @__PURE__ */ new Set([
43
+ "--json",
44
+ "--non-interactive",
45
+ "--version",
46
+ "-v",
47
+ "--help",
48
+ "-h"
49
+ ]);
50
+ function parseGlobalFlags(argv) {
51
+ const flags = { json: false, nonInteractive: false, help: false, version: false };
52
+ const rest = [];
53
+ for (const token of argv) {
54
+ switch (token) {
55
+ case "--json":
56
+ flags.json = true;
57
+ break;
58
+ case "--non-interactive":
59
+ flags.nonInteractive = true;
60
+ break;
61
+ case "--version":
62
+ case "-v":
63
+ flags.version = true;
64
+ break;
65
+ case "--help":
66
+ case "-h":
67
+ flags.help = true;
68
+ break;
69
+ default:
70
+ rest.push(token);
71
+ }
72
+ }
73
+ return { flags, rest };
74
+ }
75
+ function isUnknownFlag(token) {
76
+ return token.startsWith("-") && !GLOBAL_FLAG_TOKENS.has(token);
77
+ }
78
+ function renderRootHelp(version, commands) {
79
+ const width = Math.max(...commands.map((c) => c.name.length));
80
+ const lines = commands.map((c) => ` ${c.name.padEnd(width)} ${c.summary}`);
81
+ return [
82
+ `birdybeep ${version} \u2014 stream coding-agent lifecycle events to BirdyBeep.`,
83
+ "",
84
+ "Usage:",
85
+ " birdybeep <command> [options]",
86
+ "",
87
+ "Commands:",
88
+ ...lines,
89
+ "",
90
+ "Global options:",
91
+ " --json Machine-readable JSON output",
92
+ " --non-interactive Never prompt; fail fast if input is required",
93
+ " -h, --help Show help (root or per-command)",
94
+ " -v, --version Show the CLI version"
95
+ ].join("\n");
96
+ }
97
+ function renderCommandHelp(path, command) {
98
+ const lines = [
99
+ `birdybeep ${path} \u2014 ${command.summary}`,
100
+ "",
101
+ "Usage:",
102
+ ` ${command.usage ?? `birdybeep ${path} [options]`}`
103
+ ];
104
+ if (command.subcommands && command.subcommands.length > 0) {
105
+ const width = Math.max(...command.subcommands.map((c) => c.name.length));
106
+ lines.push(
107
+ "",
108
+ "Subcommands:",
109
+ ...command.subcommands.map((c) => ` ${c.name.padEnd(width)} ${c.summary}`)
110
+ );
111
+ }
112
+ return lines.join("\n");
113
+ }
114
+ async function dispatch(argv, deps) {
115
+ const { flags, rest } = parseGlobalFlags(argv);
116
+ const io = createIo(flags.json, deps.stdout, deps.stderr);
117
+ if (deps.ensureConfig !== false) {
118
+ try {
119
+ (0, import_node_fs.mkdirSync)((0, import_agent_core.birdyBeepConfigDir)(), { recursive: true, mode: 448 });
120
+ } catch {
121
+ }
122
+ }
123
+ if (flags.version) {
124
+ io.emit(deps.version, { version: deps.version });
125
+ return EXIT.OK;
126
+ }
127
+ let command = deps.commands.find((c) => c.name === rest[0]);
128
+ const pathParts = [];
129
+ let argsStart = 1;
130
+ if (command) {
131
+ pathParts.push(command.name);
132
+ if (command.subcommands && command.subcommands.length > 0) {
133
+ const sub = command.subcommands.find((c) => c.name === rest[1]);
134
+ if (sub) {
135
+ command = sub;
136
+ pathParts.push(sub.name);
137
+ argsStart = 2;
138
+ }
139
+ }
140
+ }
141
+ if (rest.length === 0 || flags.help && command === void 0) {
142
+ io.emit(renderRootHelp(deps.version, deps.commands), {
143
+ version: deps.version,
144
+ commands: deps.commands.map((c) => ({ name: c.name, summary: c.summary }))
145
+ });
146
+ return EXIT.OK;
147
+ }
148
+ if (command === void 0) {
149
+ io.errline(`birdybeep: unknown command "${rest[0]}". Run \`birdybeep --help\`.`);
150
+ return EXIT.USAGE;
151
+ }
152
+ const path = pathParts.join(" ");
153
+ if (flags.help) {
154
+ io.emit(renderCommandHelp(path, command), {
155
+ name: path,
156
+ summary: command.summary,
157
+ usage: command.usage,
158
+ subcommands: command.subcommands?.map((c) => ({ name: c.name, summary: c.summary }))
159
+ });
160
+ return EXIT.OK;
161
+ }
162
+ if (command.run === void 0) {
163
+ io.errline(renderCommandHelp(path, command));
164
+ return EXIT.USAGE;
165
+ }
166
+ const args = rest.slice(argsStart);
167
+ const unknown = args.find(isUnknownFlag);
168
+ if (unknown !== void 0) {
169
+ io.errline(`birdybeep ${path}: unknown option "${unknown}".`);
170
+ return EXIT.USAGE;
171
+ }
172
+ try {
173
+ return await command.run({ args, flags, io });
174
+ } catch (err) {
175
+ if (err instanceof MissingInputError) {
176
+ io.errline(
177
+ `birdybeep ${path}: ${err.message} (re-run without --non-interactive to be prompted).`
178
+ );
179
+ return EXIT.USAGE;
180
+ }
181
+ io.errline(`birdybeep ${path}: ${err instanceof Error ? err.message : String(err)}`);
182
+ return EXIT.ERROR;
183
+ }
184
+ }
185
+
186
+ // src/commands/agent.ts
187
+ var DEFAULT_ADAPTERS = [import_claude_code.claudeCodeAdapter, import_codex.codexAdapter, import_opencode.opencodeAdapter];
188
+ var TARGET_TO_ID = {
189
+ claude: "claude_code",
190
+ codex: "codex",
191
+ opencode: "opencode"
192
+ };
193
+ var AGENT_TARGETS = ["all", "claude", "codex", "opencode"];
194
+ function selectAdapters(target, adapters) {
195
+ if (target === "all") return adapters;
196
+ const id = TARGET_TO_ID[target];
197
+ if (id === void 0) return "unknown";
198
+ return adapters.filter((a) => a.id === id);
199
+ }
200
+ async function installSelected(adapters, ctx) {
201
+ const target = ctx.args[0] ?? "all";
202
+ const selected = selectAdapters(target, adapters);
203
+ if (selected === "unknown") {
204
+ ctx.io.errline(
205
+ `birdybeep agent install: unknown target "${target}" (expected ${AGENT_TARGETS.join("|")}).`
206
+ );
207
+ return EXIT.USAGE;
208
+ }
209
+ const outcomes = [];
210
+ for (const adapter of selected) {
211
+ const detection = await adapter.detect();
212
+ if (!detection.detected) {
213
+ outcomes.push({ harness: adapter.id, displayName: adapter.displayName, detected: false });
214
+ continue;
215
+ }
216
+ const result = await adapter.install();
217
+ outcomes.push({
218
+ harness: adapter.id,
219
+ displayName: adapter.displayName,
220
+ detected: true,
221
+ status: result.status,
222
+ changedFiles: result.changedFiles,
223
+ backupFiles: result.backupFiles,
224
+ requiredActions: result.requiredActions
225
+ });
226
+ }
227
+ if (ctx.flags.json) {
228
+ ctx.io.result({ target, results: outcomes });
229
+ return EXIT.OK;
230
+ }
231
+ if (outcomes.length === 0 || outcomes.every((o) => !o.detected)) {
232
+ ctx.io.line("No supported harnesses detected \u2014 nothing to install.");
233
+ }
234
+ for (const o of outcomes) {
235
+ if (!o.detected) {
236
+ ctx.io.line(`\u2013 ${o.displayName}: not detected (skipped)`);
237
+ continue;
238
+ }
239
+ const changed = (o.changedFiles ?? []).length > 0 ? o.changedFiles.join(", ") : "no changes";
240
+ ctx.io.line(`\u2713 ${o.displayName}: ${o.status} (${changed})`);
241
+ for (const action of o.requiredActions ?? []) ctx.io.line(` \u2192 ${action}`);
242
+ }
243
+ return EXIT.OK;
244
+ }
245
+ async function uninstallSelected(adapters, ctx) {
246
+ const target = ctx.args[0] ?? "all";
247
+ const selected = selectAdapters(target, adapters);
248
+ if (selected === "unknown") {
249
+ ctx.io.errline(
250
+ `birdybeep agent uninstall: unknown target "${target}" (expected ${AGENT_TARGETS.join("|")}).`
251
+ );
252
+ return EXIT.USAGE;
253
+ }
254
+ const outcomes = [];
255
+ for (const adapter of selected) {
256
+ const result = await adapter.uninstall();
257
+ outcomes.push({
258
+ harness: adapter.id,
259
+ displayName: adapter.displayName,
260
+ changed: result.changed,
261
+ removedFiles: result.removedFiles,
262
+ restoredFiles: result.restoredFiles
263
+ });
264
+ }
265
+ if (ctx.flags.json) {
266
+ ctx.io.result({ target, results: outcomes });
267
+ return EXIT.OK;
268
+ }
269
+ for (const o of outcomes) {
270
+ if (!o.changed) {
271
+ ctx.io.line(`\u2013 ${o.displayName}: nothing to remove`);
272
+ continue;
273
+ }
274
+ const touched = [...o.removedFiles, ...o.restoredFiles].join(", ") || "config restored";
275
+ ctx.io.line(`\u2713 ${o.displayName}: removed (${touched})`);
276
+ }
277
+ return EXIT.OK;
278
+ }
279
+ function createAgentCommand(deps = {}) {
280
+ const adapters = deps.adapters ?? DEFAULT_ADAPTERS;
281
+ return {
282
+ name: "agent",
283
+ summary: "Install or uninstall harness adapters",
284
+ usage: "birdybeep agent <install|uninstall> [all|claude|codex|opencode]",
285
+ subcommands: [
286
+ {
287
+ name: "install",
288
+ summary: "Install adapters (all | claude | codex | opencode)",
289
+ usage: "birdybeep agent install [all|claude|codex|opencode]",
290
+ run: (ctx) => installSelected(adapters, ctx)
291
+ },
292
+ {
293
+ name: "uninstall",
294
+ summary: "Restore harness config to its pre-install state",
295
+ usage: "birdybeep agent uninstall [all|claude|codex|opencode]",
296
+ run: (ctx) => uninstallSelected(adapters, ctx)
297
+ }
298
+ ]
299
+ };
300
+ }
301
+
302
+ // src/commands/doctor.ts
303
+ var import_agent_core4 = require("@birdybeep/agent-core");
304
+ var import_claude_code2 = require("@birdybeep/claude-code");
305
+ var import_codex2 = require("@birdybeep/codex");
306
+ var import_opencode2 = require("@birdybeep/opencode");
307
+
308
+ // src/config.ts
309
+ var import_node_fs2 = require("fs");
310
+ var import_node_path = require("path");
311
+ var import_agent_core2 = require("@birdybeep/agent-core");
312
+ var DEFAULT_API_URL = "https://api.birdybeep.com";
313
+ var CONFIG_FILE = "config.json";
314
+ function cliConfigPath() {
315
+ return (0, import_node_path.join)((0, import_agent_core2.birdyBeepConfigDir)(), CONFIG_FILE);
316
+ }
317
+ function readCliConfig() {
318
+ try {
319
+ const parsed = JSON.parse((0, import_node_fs2.readFileSync)(cliConfigPath(), "utf8"));
320
+ return typeof parsed === "object" && parsed !== null ? parsed : {};
321
+ } catch {
322
+ return {};
323
+ }
324
+ }
325
+ function writeCliConfig(patch) {
326
+ const current = readCliConfig();
327
+ const merged = {};
328
+ const apiUrl = patch.apiUrl ?? current.apiUrl;
329
+ if (apiUrl !== void 0) merged.apiUrl = apiUrl;
330
+ (0, import_node_fs2.mkdirSync)((0, import_agent_core2.birdyBeepConfigDir)(), { recursive: true, mode: 448 });
331
+ (0, import_node_fs2.writeFileSync)(cliConfigPath(), `${JSON.stringify(merged, null, 2)}
332
+ `, { mode: 384 });
333
+ }
334
+ function resolveApiUrl() {
335
+ const env = process.env["BIRDYBEEP_API_URL"];
336
+ if (env !== void 0 && env.length > 0) return env;
337
+ return readCliConfig().apiUrl ?? DEFAULT_API_URL;
338
+ }
339
+
340
+ // src/diagnostics.ts
341
+ var import_agent_core3 = require("@birdybeep/agent-core");
342
+ async function gatherIntegrations(adapters) {
343
+ return Promise.all(
344
+ adapters.map(async (a) => ({
345
+ harness: a.id,
346
+ displayName: a.displayName,
347
+ status: await a.status()
348
+ }))
349
+ );
350
+ }
351
+ async function isLoggedIn(tokenOptions = {}) {
352
+ return await (0, import_agent_core3.getToken)(tokenOptions) !== null;
353
+ }
354
+ function localQueueDepth() {
355
+ return new import_agent_core3.LocalEventQueue().size();
356
+ }
357
+ function machineIdentity() {
358
+ return (0, import_agent_core3.getMachineIdentity)();
359
+ }
360
+
361
+ // src/commands/doctor.ts
362
+ var DEFAULT_ADAPTERS2 = [import_claude_code2.claudeCodeAdapter, import_codex2.codexAdapter, import_opencode2.opencodeAdapter];
363
+ async function defaultProbeNetwork(baseUrl) {
364
+ try {
365
+ const controller = new AbortController();
366
+ const timer = setTimeout(() => controller.abort(), 3e3);
367
+ if (typeof timer.unref === "function") timer.unref();
368
+ const res = await fetch(baseUrl, { method: "HEAD", signal: controller.signal });
369
+ clearTimeout(timer);
370
+ return res.status < 500;
371
+ } catch {
372
+ return false;
373
+ }
374
+ }
375
+ function createDoctorCommand(deps = {}) {
376
+ const adapters = deps.adapters ?? DEFAULT_ADAPTERS2;
377
+ const probeNetwork = deps.probeNetwork ?? defaultProbeNetwork;
378
+ const makeSender = deps.createSender ?? ((baseUrl) => (0, import_agent_core4.createSender)(
379
+ deps.tokenOptions ? { baseUrl, tokenOptions: deps.tokenOptions } : { baseUrl }
380
+ ));
381
+ return {
382
+ name: "doctor",
383
+ summary: "Diagnose token, trust, restart, and offline-queue issues",
384
+ usage: "birdybeep doctor [--json]",
385
+ run: async (ctx) => {
386
+ const checks = [];
387
+ const apiUrl = resolveApiUrl();
388
+ const loggedIn = await isLoggedIn(deps.tokenOptions ?? {});
389
+ checks.push(
390
+ loggedIn ? { name: "Machine token", ok: true } : {
391
+ name: "Machine token",
392
+ ok: false,
393
+ detail: "No machine token found.",
394
+ remedy: "Run `birdybeep login` to pair this machine."
395
+ }
396
+ );
397
+ for (const adapter of adapters) {
398
+ const result = await adapter.doctor();
399
+ for (const c of result.checks) {
400
+ checks.push({
401
+ name: `${adapter.displayName}: ${c.name}`,
402
+ ok: c.ok,
403
+ ...c.detail !== void 0 ? { detail: c.detail } : {},
404
+ ...c.remedy !== void 0 ? { remedy: c.remedy } : {}
405
+ });
406
+ }
407
+ }
408
+ const depthBefore = localQueueDepth();
409
+ const drain = await makeSender(apiUrl).drainNow();
410
+ const depthAfter = localQueueDepth();
411
+ checks.push({
412
+ name: "Local queue",
413
+ ok: true,
414
+ detail: `${depthBefore} queued \u2192 ${drain.delivered} delivered, ${depthAfter} remaining`
415
+ });
416
+ const reachable = await probeNetwork(apiUrl);
417
+ checks.push(
418
+ reachable ? { name: "Backend reachable", ok: true } : {
419
+ name: "Backend reachable",
420
+ ok: false,
421
+ detail: `Could not reach ${apiUrl}.`,
422
+ remedy: "Check your network; queued events will retry automatically."
423
+ }
424
+ );
425
+ const ok = checks.every((c) => c.ok);
426
+ if (ctx.flags.json) {
427
+ ctx.io.result({
428
+ ok,
429
+ checks,
430
+ queue: { depthBefore, delivered: drain.delivered, depthAfter }
431
+ });
432
+ } else {
433
+ for (const c of checks) {
434
+ ctx.io.line(`${c.ok ? "\u2713" : "\u2717"} ${c.name}${c.detail ? ` \u2014 ${c.detail}` : ""}`);
435
+ if (!c.ok && c.remedy) ctx.io.line(` \u2192 ${c.remedy}`);
436
+ }
437
+ ctx.io.line(ok ? "\nAll checks passed." : "\nSome checks failed \u2014 see fixes above.");
438
+ }
439
+ return ok ? EXIT.OK : EXIT.ERROR;
440
+ }
441
+ };
442
+ }
443
+
444
+ // src/commands/hook.ts
445
+ var import_agent_core5 = require("@birdybeep/agent-core");
446
+ var import_claude_code3 = require("@birdybeep/claude-code");
447
+ var import_codex3 = require("@birdybeep/codex");
448
+ var import_opencode3 = require("@birdybeep/opencode");
449
+ var RUNNERS = {
450
+ claude: import_claude_code3.runClaudeHook,
451
+ codex: import_codex3.runCodexHook,
452
+ opencode: import_opencode3.runOpenCodeHook
453
+ };
454
+ var HOOK_HARNESSES = ["claude", "codex", "opencode"];
455
+ var STDIN_READ_TIMEOUT_MS = 3e3;
456
+ function withTimeout(promise, ms, fallback) {
457
+ return new Promise((resolve) => {
458
+ let settled = false;
459
+ const finish = (value) => {
460
+ if (settled) return;
461
+ settled = true;
462
+ clearTimeout(timer);
463
+ resolve(value);
464
+ };
465
+ const timer = setTimeout(() => finish(fallback), ms);
466
+ if (typeof timer.unref === "function") timer.unref();
467
+ void promise.then(finish, () => finish(fallback));
468
+ });
469
+ }
470
+ function isHarnessName(value) {
471
+ return value === "claude" || value === "codex" || value === "opencode";
472
+ }
473
+ function runHookCommand(harness, payload, sender) {
474
+ return RUNNERS[harness](payload, { sender });
475
+ }
476
+ function readStdinDefault() {
477
+ return new Promise((resolve) => {
478
+ if (process.stdin.isTTY) {
479
+ resolve("");
480
+ return;
481
+ }
482
+ let data = "";
483
+ process.stdin.setEncoding("utf8");
484
+ process.stdin.on("data", (chunk) => data += chunk);
485
+ process.stdin.on("end", () => resolve(data));
486
+ process.stdin.on("error", () => resolve(""));
487
+ });
488
+ }
489
+ async function readHookPayload(args, readStdin) {
490
+ return args[1] ?? await readStdin();
491
+ }
492
+ function createHookCommand(deps = {}) {
493
+ const makeSender = deps.createSender ?? ((baseUrl) => (0, import_agent_core5.createSender)({ baseUrl }));
494
+ const readStdin = deps.readStdin ?? readStdinDefault;
495
+ const stdinTimeoutMs = deps.stdinTimeoutMs ?? STDIN_READ_TIMEOUT_MS;
496
+ return {
497
+ name: "hook",
498
+ summary: "Internal: normalize + send an event fired by a harness hook",
499
+ usage: "birdybeep hook <claude|codex|opencode>",
500
+ run: async (ctx) => {
501
+ const harness = ctx.args[0];
502
+ if (!isHarnessName(harness)) {
503
+ ctx.io.errline(`birdybeep hook: expected one of ${HOOK_HARNESSES.join("|")}`);
504
+ return EXIT.USAGE;
505
+ }
506
+ const raw = await withTimeout(readHookPayload(ctx.args, readStdin), stdinTimeoutMs, "");
507
+ let payload;
508
+ try {
509
+ payload = JSON.parse(raw);
510
+ } catch {
511
+ ctx.io.result({ harness, outcome: "skipped" });
512
+ return EXIT.OK;
513
+ }
514
+ const sender = makeSender(resolveApiUrl());
515
+ const result = await runHookCommand(harness, payload, sender);
516
+ ctx.io.result({ harness, outcome: result.outcome, eventType: result.eventType });
517
+ return EXIT.OK;
518
+ }
519
+ };
520
+ }
521
+
522
+ // src/commands/login.ts
523
+ var import_agent_core7 = require("@birdybeep/agent-core");
524
+ var import_uqr = require("uqr");
525
+
526
+ // src/pairing.ts
527
+ var import_agent_core6 = require("@birdybeep/agent-core");
528
+ function base(apiUrl) {
529
+ return apiUrl.replace(/\/$/, "");
530
+ }
531
+ async function pairStart(apiUrl, input, fetchImpl) {
532
+ const body = {
533
+ machine_label: input.machineLabel,
534
+ ...input.os !== void 0 ? { os: input.os } : {},
535
+ ...input.cliVersion !== void 0 ? { cli_version: input.cliVersion } : {}
536
+ };
537
+ const res = await fetchImpl(`${base(apiUrl)}/v1/pair/start`, {
538
+ method: "POST",
539
+ headers: { "content-type": "application/json" },
540
+ body: JSON.stringify(body)
541
+ });
542
+ if (!res.ok) throw new Error(`pairing could not be started (HTTP ${res.status})`);
543
+ const parsed = import_agent_core6.pairStartResponseSchema.safeParse(await res.json());
544
+ if (!parsed.success) throw new Error("pairing start returned an unexpected response shape");
545
+ return parsed.data;
546
+ }
547
+ async function pairTokenPoll(apiUrl, deviceCode, fetchImpl, machineFingerprint) {
548
+ const body = {
549
+ device_code: deviceCode,
550
+ ...machineFingerprint !== void 0 ? { machine_fingerprint: machineFingerprint } : {}
551
+ };
552
+ const res = await fetchImpl(`${base(apiUrl)}/v1/pair/token`, {
553
+ method: "POST",
554
+ headers: { "content-type": "application/json" },
555
+ body: JSON.stringify(body)
556
+ });
557
+ if (!res.ok) return { status: "pending" };
558
+ const parsed = import_agent_core6.pairTokenResponseSchema.safeParse(await res.json());
559
+ if (!parsed.success) return { status: "pending" };
560
+ return {
561
+ status: "paired",
562
+ machineToken: parsed.data.machine_token,
563
+ machineId: parsed.data.machine_id
564
+ };
565
+ }
566
+
567
+ // src/version.ts
568
+ var CLI_VERSION = "0.0.1".length > 0 ? "0.0.1" : "0.0.0";
569
+
570
+ // src/commands/login.ts
571
+ var DEFAULT_POLL_INTERVAL_MS = 2e3;
572
+ function renderQrMatrix(qrPayload) {
573
+ return (0, import_uqr.renderUnicodeCompact)(qrPayload, { border: 2 });
574
+ }
575
+ function createLoginCommand(deps = {}) {
576
+ const fetchImpl = deps.fetchImpl ?? fetch;
577
+ const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
578
+ const clock = deps.now ?? (() => Date.now());
579
+ const renderQr = deps.renderQr ?? renderQrMatrix;
580
+ const intervalMs = deps.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
581
+ return {
582
+ name: "login",
583
+ summary: "Pair this machine with your BirdyBeep account (QR or manual)",
584
+ usage: "birdybeep login [--json]",
585
+ run: async (ctx) => {
586
+ const apiUrl = resolveApiUrl();
587
+ const identity = (0, import_agent_core7.getMachineIdentity)();
588
+ const start = await pairStart(
589
+ apiUrl,
590
+ { machineLabel: identity.label, os: identity.os, cliVersion: CLI_VERSION },
591
+ fetchImpl
592
+ );
593
+ if (ctx.flags.json) {
594
+ ctx.io.result({
595
+ status: "pairing_started",
596
+ user_code: start.user_code,
597
+ qr_payload: start.qr_payload,
598
+ expires_at: start.expires_at
599
+ });
600
+ } else {
601
+ ctx.io.line(
602
+ "To pair this machine, scan the code or open the link, then confirm in the app:"
603
+ );
604
+ const isTTY = deps.isTTY ?? process.stdout.isTTY === true;
605
+ if (isTTY) ctx.io.line(renderQr(start.qr_payload));
606
+ ctx.io.line(` Scan or open: ${start.qr_payload}`);
607
+ ctx.io.line(` Code: ${start.user_code}`);
608
+ ctx.io.line("Waiting for confirmation\u2026");
609
+ }
610
+ const deadline = Date.parse(start.expires_at);
611
+ let paired;
612
+ while (clock() < deadline) {
613
+ await sleep(intervalMs);
614
+ const poll = await pairTokenPoll(
615
+ apiUrl,
616
+ start.device_code,
617
+ fetchImpl,
618
+ identity.fingerprintHash
619
+ );
620
+ if (poll.status === "paired") {
621
+ paired = poll;
622
+ break;
623
+ }
624
+ }
625
+ if (paired === void 0 || paired.status !== "paired") {
626
+ ctx.io.result({ paired: false, reason: "timeout" });
627
+ ctx.io.errline(
628
+ "Pairing timed out before it was confirmed. Run `birdybeep login` to retry."
629
+ );
630
+ return EXIT.ERROR;
631
+ }
632
+ await (0, import_agent_core7.setToken)(paired.machineToken, deps.tokenOptions ?? {});
633
+ writeCliConfig({ apiUrl });
634
+ ctx.io.emit(`\u2713 Paired. Run \`birdybeep test\` to send a test Beep.`, {
635
+ paired: true,
636
+ machineId: paired.machineId
637
+ });
638
+ return EXIT.OK;
639
+ }
640
+ };
641
+ }
642
+
643
+ // src/commands/logout.ts
644
+ var import_agent_core8 = require("@birdybeep/agent-core");
645
+ function createLogoutCommand(deps = {}) {
646
+ return {
647
+ name: "logout",
648
+ summary: "Remove the local machine token",
649
+ usage: "birdybeep logout",
650
+ run: async (ctx) => {
651
+ await (0, import_agent_core8.clearToken)(deps.tokenOptions ?? {});
652
+ ctx.io.emit("Logged out \u2014 the machine token was removed.", { loggedOut: true });
653
+ return EXIT.OK;
654
+ }
655
+ };
656
+ }
657
+
658
+ // src/commands/queue.ts
659
+ var import_agent_core9 = require("@birdybeep/agent-core");
660
+ function createQueueCommand() {
661
+ return {
662
+ name: "queue",
663
+ summary: "Local event-queue maintenance",
664
+ usage: "birdybeep queue <clear>",
665
+ subcommands: [
666
+ {
667
+ name: "clear",
668
+ summary: "Clear the local offline event queue (debug)",
669
+ usage: "birdybeep queue clear",
670
+ run: (ctx) => {
671
+ const cleared = new import_agent_core9.LocalEventQueue().clear();
672
+ ctx.io.emit(`Cleared ${cleared} queued event(s).`, { cleared });
673
+ return EXIT.OK;
674
+ }
675
+ }
676
+ ]
677
+ };
678
+ }
679
+
680
+ // src/commands/report-status.ts
681
+ var import_agent_core10 = require("@birdybeep/agent-core");
682
+ var import_claude_code4 = require("@birdybeep/claude-code");
683
+ var import_codex4 = require("@birdybeep/codex");
684
+ var import_opencode4 = require("@birdybeep/opencode");
685
+ var DEFAULT_ADAPTERS3 = [import_claude_code4.claudeCodeAdapter, import_codex4.codexAdapter, import_opencode4.opencodeAdapter];
686
+ var ADAPTER_VERSIONS = {
687
+ claude_code: import_claude_code4.CLAUDE_CODE_ADAPTER_VERSION,
688
+ codex: import_codex4.CODEX_ADAPTER_VERSION,
689
+ opencode: import_opencode4.OPENCODE_ADAPTER_VERSION
690
+ };
691
+ var base2 = (apiUrl) => apiUrl.replace(/\/$/, "");
692
+ async function gatherItems(adapters) {
693
+ return Promise.all(
694
+ adapters.map(async (a) => {
695
+ const [detection, status] = await Promise.all([a.detect(), a.status()]);
696
+ const item = { harness: a.id, status };
697
+ if (detection.version !== void 0) item.harness_version = detection.version;
698
+ const adapterVersion = ADAPTER_VERSIONS[a.id];
699
+ if (adapterVersion !== void 0) item.adapter_version = adapterVersion;
700
+ return item;
701
+ })
702
+ );
703
+ }
704
+ function createReportStatusCommand(deps = {}) {
705
+ const adapters = deps.adapters ?? DEFAULT_ADAPTERS3;
706
+ const fetchImpl = deps.fetchImpl ?? fetch;
707
+ return {
708
+ name: "report-status",
709
+ summary: "Internal: report integration status to the backend",
710
+ usage: "birdybeep report-status [--json]",
711
+ run: async (ctx) => {
712
+ const token = await (0, import_agent_core10.getToken)(deps.tokenOptions ?? {});
713
+ if (token === null) {
714
+ ctx.io.errline("Not logged in \u2014 run `birdybeep login` first.");
715
+ return EXIT.ERROR;
716
+ }
717
+ const items = await gatherItems(adapters);
718
+ if (items.length === 0) {
719
+ ctx.io.emit("No integrations to report.", { outcome: "reported", integrations: [] });
720
+ return EXIT.OK;
721
+ }
722
+ let effective = items.map((i) => ({ harness: i.harness, status: i.status }));
723
+ let outcome = "deferred";
724
+ let errorCode;
725
+ try {
726
+ const res = await fetchImpl(`${base2(resolveApiUrl())}/v1/integrations/status`, {
727
+ method: "POST",
728
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
729
+ body: JSON.stringify({ integrations: items })
730
+ });
731
+ if (res.ok) {
732
+ outcome = "reported";
733
+ const parsed = import_agent_core10.integrationStatusResponseSchema.safeParse(
734
+ await res.json().catch(() => void 0)
735
+ );
736
+ if (parsed.success) {
737
+ effective = parsed.data.integrations.map((i) => ({
738
+ harness: i.harness,
739
+ status: i.status
740
+ }));
741
+ }
742
+ } else {
743
+ const env = import_agent_core10.errorEnvelopeSchema.safeParse(await res.json().catch(() => void 0));
744
+ errorCode = env.success ? env.data.error.code : void 0;
745
+ const terminal = errorCode !== void 0 ? errorCode === "unauthorized" || errorCode === "forbidden" || errorCode === "token_revoked" : res.status === 401 || res.status === 403;
746
+ outcome = terminal ? "terminal" : "deferred";
747
+ }
748
+ } catch {
749
+ outcome = "deferred";
750
+ }
751
+ if (ctx.flags.json) {
752
+ ctx.io.result({
753
+ outcome,
754
+ integrations: effective,
755
+ ...errorCode !== void 0 ? { error: errorCode } : {}
756
+ });
757
+ } else if (outcome === "terminal") {
758
+ ctx.io.errline(
759
+ `Report rejected (${errorCode ?? "auth"}) \u2014 your token may be revoked. Re-run \`birdybeep login\`.`
760
+ );
761
+ } else {
762
+ for (const e of effective) {
763
+ ctx.io.line(
764
+ outcome === "reported" ? `\u2713 ${e.harness}: ${e.status} (reported)` : `\u2022 ${e.harness}: ${e.status} (deferred \u2014 backend unreachable)`
765
+ );
766
+ }
767
+ }
768
+ return outcome === "terminal" ? EXIT.ERROR : EXIT.OK;
769
+ }
770
+ };
771
+ }
772
+
773
+ // src/commands/status.ts
774
+ var import_agent_core11 = require("@birdybeep/agent-core");
775
+ var import_claude_code5 = require("@birdybeep/claude-code");
776
+ var import_codex5 = require("@birdybeep/codex");
777
+ var import_opencode5 = require("@birdybeep/opencode");
778
+ var DEFAULT_ADAPTERS4 = [import_claude_code5.claudeCodeAdapter, import_codex5.codexAdapter, import_opencode5.opencodeAdapter];
779
+ function createStatusCommand(deps = {}) {
780
+ const adapters = deps.adapters ?? DEFAULT_ADAPTERS4;
781
+ const makeSender = deps.createSender ?? ((baseUrl) => (0, import_agent_core11.createSender)(
782
+ deps.tokenOptions ? { baseUrl, tokenOptions: deps.tokenOptions } : { baseUrl }
783
+ ));
784
+ return {
785
+ name: "status",
786
+ summary: "Show pairing + per-harness integration status",
787
+ usage: "birdybeep status [--json]",
788
+ run: async (ctx) => {
789
+ const machine = machineIdentity();
790
+ const loggedIn = await isLoggedIn(deps.tokenOptions ?? {});
791
+ const integrations = await gatherIntegrations(adapters);
792
+ const depthBefore = localQueueDepth();
793
+ const drain = await makeSender(resolveApiUrl()).drainNow();
794
+ const depthAfter = localQueueDepth();
795
+ const report = {
796
+ machine,
797
+ loggedIn,
798
+ integrations,
799
+ queue: { depthBefore, delivered: drain.delivered, depthAfter }
800
+ };
801
+ if (ctx.flags.json) {
802
+ ctx.io.result(report);
803
+ } else {
804
+ ctx.io.line(`Machine: ${machine.label} (${machine.os})`);
805
+ ctx.io.line(
806
+ loggedIn ? "Login: paired" : "Login: NOT logged in \u2014 run `birdybeep login`"
807
+ );
808
+ ctx.io.line("Integrations:");
809
+ for (const i of integrations) ctx.io.line(` ${i.displayName}: ${i.status}`);
810
+ ctx.io.line(
811
+ `Queue: ${depthBefore} queued \u2192 ${drain.delivered} delivered, ${depthAfter} remaining`
812
+ );
813
+ }
814
+ return loggedIn ? EXIT.OK : EXIT.ERROR;
815
+ }
816
+ };
817
+ }
818
+
819
+ // src/commands/test.ts
820
+ var import_node_crypto = require("crypto");
821
+ var import_agent_core12 = require("@birdybeep/agent-core");
822
+ function buildTestEvent(opts = {}) {
823
+ const machine = (0, import_agent_core12.getMachineIdentity)();
824
+ return (0, import_agent_core12.normalizeEvent)(
825
+ {
826
+ event_type: "test",
827
+ status: "running",
828
+ harness: "claude_code",
829
+ // schema requires a harness; the "test" type distinguishes it
830
+ // Unique per run: a repeat `birdybeep test` inside the backend's dedupe window must
831
+ // still beep — a constant id made the second test silently "deduped" (9fh).
832
+ source_session_id: `birdybeep-cli-test-${(0, import_node_crypto.randomUUID)()}`,
833
+ machine: { label: machine.label, os: machine.os },
834
+ workspace: { cwd: process.cwd() },
835
+ title: "BirdyBeep test event",
836
+ body: "If you can see this, your machine is wired up correctly.",
837
+ metadata: { test: true }
838
+ },
839
+ opts
840
+ );
841
+ }
842
+ function createTestCommand(deps = {}) {
843
+ const makeSender = deps.createSender ?? ((baseUrl) => (0, import_agent_core12.createSender)(
844
+ deps.tokenOptions ? { baseUrl, tokenOptions: deps.tokenOptions } : { baseUrl }
845
+ ));
846
+ return {
847
+ name: "test",
848
+ summary: "Send a test event end-to-end",
849
+ usage: "birdybeep test [--json]",
850
+ run: async (ctx) => {
851
+ const event = buildTestEvent();
852
+ const result = await makeSender(resolveApiUrl()).send(event);
853
+ if (ctx.flags.json) {
854
+ ctx.io.result({
855
+ outcome: result.outcome,
856
+ ...result.status ? { status: result.status } : {},
857
+ ...result.decision ? { decision: result.decision } : {}
858
+ });
859
+ } else if (result.outcome === "delivered") {
860
+ if (result.decision === "notified" || result.decision === void 0) {
861
+ ctx.io.line("\u2713 Test event delivered \u2014 check your phone for a test Beep.");
862
+ } else if (result.decision === "suppressed") {
863
+ ctx.io.line(
864
+ "\u26A0 The backend accepted the test event but suppressed the push \u2014 this machine or integration is probably muted. Check mutes in the app, or run `birdybeep doctor`."
865
+ );
866
+ } else if (result.decision === "deduped") {
867
+ ctx.io.line(
868
+ "\u26A0 The backend accepted the test event but folded it into a recent duplicate \u2014 wait ~30s and run `birdybeep test` again."
869
+ );
870
+ } else {
871
+ ctx.io.line(
872
+ `\u26A0 The backend accepted the test event but decided "${result.decision}" \u2014 no push was sent. Run \`birdybeep doctor\`.`
873
+ );
874
+ }
875
+ } else if (result.outcome === "queued") {
876
+ ctx.io.line("\u2022 Offline \u2014 test event queued; it will deliver when you reconnect.");
877
+ } else {
878
+ ctx.io.line("\u2717 Test event was rejected by the backend. Run `birdybeep doctor`.");
879
+ }
880
+ return result.outcome === "dropped" ? EXIT.ERROR : EXIT.OK;
881
+ }
882
+ };
883
+ }
884
+
885
+ // src/commands.ts
886
+ function buildCommands() {
887
+ return [
888
+ createLoginCommand(),
889
+ createLogoutCommand(),
890
+ createStatusCommand(),
891
+ createTestCommand(),
892
+ createDoctorCommand(),
893
+ createAgentCommand(),
894
+ createHookCommand(),
895
+ createQueueCommand(),
896
+ createReportStatusCommand()
897
+ ];
898
+ }
899
+
900
+ // src/cli.ts
901
+ function runCli(argv, deps = {}) {
902
+ return dispatch(argv, {
903
+ version: CLI_VERSION,
904
+ commands: deps.commands ?? buildCommands(),
905
+ stdout: deps.stdout ?? process.stdout,
906
+ stderr: deps.stderr ?? process.stderr,
907
+ ...deps.ensureConfig !== void 0 ? { ensureConfig: deps.ensureConfig } : {}
908
+ });
909
+ }
910
+
911
+ // src/node-check.ts
912
+ var MIN_NODE_MAJOR = 20;
913
+ function parseNodeMajor(version) {
914
+ const match = /^v?(\d+)\./.exec(version);
915
+ return match ? Number(match[1]) : null;
916
+ }
917
+ function nodeVersionError(version) {
918
+ const major = parseNodeMajor(version);
919
+ if (major === null) return null;
920
+ return major < MIN_NODE_MAJOR ? `birdybeep requires Node ${MIN_NODE_MAJOR}+ (you have ${version}). Please upgrade Node and retry.` : null;
921
+ }
922
+
923
+ // src/bin.ts
924
+ var versionError = nodeVersionError(process.versions.node);
925
+ if (versionError !== null) {
926
+ process.stderr.write(`${versionError}
927
+ `);
928
+ process.exitCode = 1;
929
+ } else {
930
+ runCli(process.argv.slice(2)).then(
931
+ (code) => {
932
+ process.exitCode = code;
933
+ },
934
+ (err) => {
935
+ process.stderr.write(`birdybeep: ${err instanceof Error ? err.message : String(err)}
936
+ `);
937
+ process.exitCode = 1;
938
+ }
939
+ );
940
+ }
941
+ //# sourceMappingURL=bin.cjs.map