@birdybeep/cli 0.8.1 → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -243,10 +243,164 @@ function surfaceRemedy(state, group) {
243
243
  // src/framework.ts
244
244
  var import_node_fs2 = require("fs");
245
245
  var import_agent_core2 = require("@birdybeep/agent-core");
246
+
247
+ // src/presentation.ts
248
+ var BIRD = [
249
+ " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588",
250
+ " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588",
251
+ " \u2588\u2588\u2588\u2588\u2594\u2594\u2594\u2594\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588",
252
+ " \u2588\u2588\u2588\u2588\u2588\u2588 \u25AA \u2588\u2588\u2588\u2588\u2588\u2588",
253
+ "\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588",
254
+ "\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588",
255
+ " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588",
256
+ " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588",
257
+ " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588",
258
+ " \u2588\u2588 \u2588\u2588",
259
+ " \u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588"
260
+ ].join("\n");
261
+ function presentation(output, env = process.env) {
262
+ const terminal = output.isTTY === true && env.TERM !== "dumb";
263
+ const color = terminal && !env.NO_COLOR;
264
+ const lime = (text) => {
265
+ if (!color) return text;
266
+ const code = /^(truecolor|24bit)$/.test(env.COLORTERM ?? "") ? "38;2;198;242;78" : (env.TERM ?? "").includes("256color") ? "38;5;191" : "92";
267
+ return `\x1B[${code}m${text}\x1B[0m`;
268
+ };
269
+ return {
270
+ lime,
271
+ banner: () => terminal ? `
272
+ ${lime(BIRD)}
273
+
274
+ ` : "",
275
+ help: (text) => text.split("\n").map((line, i) => i === 0 || line.endsWith(":") ? lime(line) : line).join("\n")
276
+ };
277
+ }
278
+
279
+ // src/bird-animation.ts
280
+ function birdFrame(blink = false, lift = 0, pose = "rest") {
281
+ const rows = BIRD.split("\n");
282
+ if (pose === "inhale" || pose === "breathe") {
283
+ rows[8] = pose === "inhale" ? " \u2580\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2580" : " \u2580\u2580\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2580\u2580";
284
+ } else if (pose === "crouch" || pose === "land") {
285
+ rows.splice(7, 1);
286
+ rows.unshift("");
287
+ rows[7] = " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588";
288
+ rows[8] = " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588";
289
+ rows[9] = " \u2588\u2588 \u2588\u2588";
290
+ } else if (pose === "stretch") {
291
+ rows[4] = " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588";
292
+ rows[5] = " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588";
293
+ rows[6] = " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588";
294
+ rows[7] = " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588";
295
+ rows[9] = " \u2588\u2588 \u2588\u2588";
296
+ rows[10] = " \u2580\u2580 \u2580\u2580";
297
+ } else if (pose === "flight") {
298
+ rows[4] = "\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588";
299
+ rows[5] = "\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2572\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588";
300
+ rows[6] = " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2572\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588";
301
+ rows[7] = " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588";
302
+ rows[9] = " \u2580\u2580 \u2580\u2580";
303
+ rows[10] = "";
304
+ }
305
+ if (blink)
306
+ rows[pose === "crouch" || pose === "land" ? 4 : 3] = rows[pose === "crouch" || pose === "land" ? 4 : 3].replace(" \u25AA ", " \u2501 ");
307
+ return [...Array(2 - lift).fill(""), ...rows, ...Array(lift).fill("")].join("\n");
308
+ }
309
+ var HOP = [
310
+ { pose: "crouch", lift: 0, ms: 180 },
311
+ { pose: "stretch", lift: 1, ms: 80 },
312
+ { pose: "flight", lift: 2, ms: 130 },
313
+ { pose: "flight", lift: 2, ms: 110 },
314
+ { pose: "stretch", lift: 1, ms: 90 },
315
+ { pose: "land", lift: 0, ms: 140 },
316
+ { pose: "inhale", lift: 0, ms: 120 },
317
+ { pose: "rest", lift: 0, ms: 180 }
318
+ ];
319
+ function createBirdAnimation(output, env = process.env) {
320
+ const allowed = () => output.isTTY === true && env.TERM !== "dumb" && !env.CI && env.BIRDYBEEP_ANIMATION !== "0" && (output.columns ?? 80) >= 26 && (output.rows ?? 40) >= 16;
321
+ const style = presentation(output, env);
322
+ let activeStop;
323
+ const begin = () => {
324
+ activeStop?.();
325
+ if (!allowed()) return void 0;
326
+ let stopped = false;
327
+ let timer;
328
+ const paint = (frame, rewind = false) => {
329
+ output.write(
330
+ (rewind ? "\x1B[13A" : "") + frame.split("\n").map((line) => `\r\x1B[2K${style.lime(line)}
331
+ `).join("")
332
+ );
333
+ };
334
+ const finish = (clear) => {
335
+ if (stopped) return;
336
+ stopped = true;
337
+ if (timer) clearInterval(timer);
338
+ process.off("exit", stop);
339
+ process.stdout.off("resize", resize);
340
+ if (clear) output.write("\x1B[13A" + "\r\x1B[2K\n".repeat(13) + "\x1B[13A\r");
341
+ activeStop = void 0;
342
+ };
343
+ const stop = () => finish(true);
344
+ const resize = () => {
345
+ stopped = true;
346
+ if (timer) clearInterval(timer);
347
+ process.off("exit", stop);
348
+ process.stdout.off("resize", resize);
349
+ activeStop = void 0;
350
+ };
351
+ process.once("exit", stop);
352
+ process.stdout.once("resize", resize);
353
+ activeStop = stop;
354
+ paint(birdFrame());
355
+ return {
356
+ stop,
357
+ settle: () => finish(false),
358
+ draw: (frame) => {
359
+ if (!stopped) {
360
+ paint(frame, true);
361
+ }
362
+ },
363
+ repeat: (callback) => {
364
+ timer = setInterval(callback, 140);
365
+ timer.unref();
366
+ }
367
+ };
368
+ };
369
+ return {
370
+ wait: () => {
371
+ const canvas = begin();
372
+ if (!canvas) return () => {
373
+ };
374
+ let tick = 0;
375
+ canvas.repeat(() => {
376
+ tick += 1;
377
+ const phase = tick % 22;
378
+ const pose = phase >= 8 && phase <= 10 ? "breathe" : phase >= 6 && phase <= 12 ? "inhale" : "rest";
379
+ canvas.draw(birdFrame(phase === 18, 0, pose));
380
+ });
381
+ return canvas.stop;
382
+ },
383
+ celebrate: async () => {
384
+ const canvas = begin();
385
+ if (!canvas) return;
386
+ try {
387
+ for (const { pose, lift, ms } of HOP) {
388
+ canvas.draw(birdFrame(pose === "land", lift, pose));
389
+ await new Promise((resolve) => setTimeout(resolve, ms));
390
+ }
391
+ } finally {
392
+ canvas.settle();
393
+ }
394
+ }
395
+ };
396
+ }
397
+
398
+ // src/framework.ts
246
399
  var EXIT = { OK: 0, ERROR: 1, USAGE: 2 };
247
- function createIo(json, stdout, stderr) {
400
+ function createIo(json, stdout, stderr, animate = false) {
248
401
  return {
249
402
  json,
403
+ ...!json && animate ? { bird: createBirdAnimation(stdout) } : {},
250
404
  line: (text) => {
251
405
  if (!json) stdout.write(`${text}
252
406
  `);
@@ -382,7 +536,8 @@ function renderCommandHelp(path, command) {
382
536
  }
383
537
  async function dispatch(argv, deps) {
384
538
  const { flags, rest } = parseGlobalFlags(argv);
385
- const io = createIo(flags.json, deps.stdout, deps.stderr);
539
+ const io = createIo(flags.json, deps.stdout, deps.stderr, !flags.nonInteractive);
540
+ const style = presentation(deps.stdout);
386
541
  if (deps.ensureConfig !== false) {
387
542
  try {
388
543
  (0, import_node_fs2.mkdirSync)((0, import_agent_core2.birdyBeepConfigDir)(), { recursive: true, mode: 448 });
@@ -410,7 +565,7 @@ async function dispatch(argv, deps) {
410
565
  }
411
566
  }
412
567
  if (rest.length === 0 || flags.help && command === void 0) {
413
- io.emit(renderRootHelp(deps.version, deps.commands), {
568
+ io.emit(style.banner() + style.help(renderRootHelp(deps.version, deps.commands)), {
414
569
  version: deps.version,
415
570
  commands: deps.commands.map((c) => ({
416
571
  name: c.name,
@@ -426,7 +581,7 @@ async function dispatch(argv, deps) {
426
581
  }
427
582
  const path = pathParts.join(" ");
428
583
  if (flags.help) {
429
- io.emit(renderCommandHelp(path, command), {
584
+ io.emit(style.help(renderCommandHelp(path, command)), {
430
585
  name: path,
431
586
  summary: command.summary,
432
587
  usage: command.usage,
@@ -448,6 +603,10 @@ async function dispatch(argv, deps) {
448
603
  }
449
604
  let code;
450
605
  try {
606
+ if (!flags.nonInteractive && (path === "setup" || path === "pair")) {
607
+ const banner = style.banner();
608
+ if (banner) io.line(banner.trimEnd());
609
+ }
451
610
  code = await command.run({ args, flags, io });
452
611
  } catch (err) {
453
612
  if (err instanceof MissingInputError) {
@@ -837,6 +996,7 @@ var import_node_crypto = require("crypto");
837
996
  var import_node_fs4 = require("fs");
838
997
  var import_node_os2 = require("os");
839
998
  var import_node_path2 = require("path");
999
+ var import_node_perf_hooks = require("perf_hooks");
840
1000
  var import_agent_core5 = require("@birdybeep/agent-core");
841
1001
  var import_claude_code4 = require("@birdybeep/claude-code");
842
1002
  var import_codex3 = require("@birdybeep/codex");
@@ -857,6 +1017,21 @@ var HOOK_HARNESSES = [
857
1017
  "copilot"
858
1018
  ];
859
1019
  var STDIN_READ_TIMEOUT_MS = 3e3;
1020
+ var LEGACY_HOOK_RUNTIME_BUDGET_MS = 9e3;
1021
+ function hookRuntimeBudgetMs(configuredTimeoutSeconds) {
1022
+ if (configuredTimeoutSeconds === void 0 || !Number.isFinite(configuredTimeoutSeconds) || configuredTimeoutSeconds <= 0) {
1023
+ return LEGACY_HOOK_RUNTIME_BUDGET_MS;
1024
+ }
1025
+ const configuredBudgetMs = Math.max(1, Math.floor(configuredTimeoutSeconds * 1e3) - 1e3);
1026
+ return Math.min(LEGACY_HOOK_RUNTIME_BUDGET_MS, configuredBudgetMs);
1027
+ }
1028
+ function configuredHookTimeoutSeconds(harness) {
1029
+ if (harness === "claude") return (0, import_claude_code4.configuredClaudeHookTimeoutSeconds)();
1030
+ if (harness === "codex") return (0, import_codex3.configuredCodexHookTimeoutSeconds)();
1031
+ if (harness === "cursor") return (0, import_cursor4.configuredCursorHookTimeoutSeconds)();
1032
+ if (harness === "copilot") return (0, import_copilot3.configuredCopilotHookTimeoutSeconds)();
1033
+ return void 0;
1034
+ }
860
1035
  function withTimeout(promise, ms, fallback) {
861
1036
  return new Promise((resolve) => {
862
1037
  let settled = false;
@@ -977,15 +1152,22 @@ function detachCodexNotifyWorker(payload) {
977
1152
  }
978
1153
  }
979
1154
  function createHookCommand(deps = {}) {
980
- const makeSender = deps.createSender ?? ((baseUrl) => (0, import_agent_core5.createSender)({ baseUrl }));
1155
+ const makeSender = deps.createSender ?? ((baseUrl, budgetMs) => (0, import_agent_core5.createSender)({
1156
+ baseUrl,
1157
+ timeoutMs: Math.min(import_agent_core5.DEFAULT_SEND_TIMEOUT_MS, budgetMs),
1158
+ totalBudgetMs: budgetMs
1159
+ }));
981
1160
  const readStdin = deps.readStdin ?? readStdinDefault;
982
1161
  const stdinTimeoutMs = deps.stdinTimeoutMs ?? STDIN_READ_TIMEOUT_MS;
1162
+ const now = deps.now ?? (() => import_node_perf_hooks.performance.now());
1163
+ const readConfiguredHookTimeoutSeconds = deps.configuredHookTimeoutSeconds ?? configuredHookTimeoutSeconds;
983
1164
  const detachCodexNotify = deps.detachCodexNotify ?? detachCodexNotifyWorker;
984
1165
  return {
985
1166
  name: "hook",
986
1167
  summary: "Internal: normalize + send an event fired by a harness hook",
987
1168
  usage: "birdybeep hook <claude|codex|opencode|cursor|copilot> [copilot-event]",
988
1169
  run: async (ctx) => {
1170
+ const hookStartedAt = now();
989
1171
  const harness = ctx.args[0];
990
1172
  if (!isHarnessName(harness)) {
991
1173
  ctx.io.errline(`birdybeep hook: expected one of ${HOOK_HARNESSES.join("|")}`);
@@ -1051,7 +1233,10 @@ function createHookCommand(deps = {}) {
1051
1233
  );
1052
1234
  return EXIT.ERROR;
1053
1235
  }
1054
- const sender = makeSender(resolveApiUrl());
1236
+ const runtimeBudgetMs = hookRuntimeBudgetMs(readConfiguredHookTimeoutSeconds(harness));
1237
+ const elapsedMs = Math.max(0, now() - hookStartedAt);
1238
+ const budgetMs = Math.max(1, Math.min(import_agent_core5.DEFAULT_TOTAL_BUDGET_MS, runtimeBudgetMs - elapsedMs));
1239
+ const sender = makeSender(resolveApiUrl(), budgetMs);
1055
1240
  const result = await runHookCommand(harness, payload, sender, copilotEventName);
1056
1241
  ctx.io.result({
1057
1242
  harness: handler,
@@ -1212,7 +1397,7 @@ async function pairTokenPoll(apiUrl, deviceCode, fetchImpl, machineFingerprint,
1212
1397
  }
1213
1398
 
1214
1399
  // src/version.ts
1215
- var CLI_VERSION = "0.8.1".length > 0 ? "0.8.1" : "0.0.0";
1400
+ var CLI_VERSION = "0.8.3".length > 0 ? "0.8.3" : "0.0.0";
1216
1401
 
1217
1402
  // src/commands/setup.ts
1218
1403
  var import_claude_code5 = require("@birdybeep/claude-code");
@@ -1765,32 +1950,39 @@ function createPairingCommand(verb, deps = {}) {
1765
1950
  let lastBeat = startedAt;
1766
1951
  let paired;
1767
1952
  let terminal;
1768
- for (; ; ) {
1769
- const nowMs = clock();
1770
- if (nowMs >= deadline) break;
1771
- await sleep(intervalMs);
1772
- const poll = await pairTokenPoll(
1773
- apiUrl,
1774
- start.device_code,
1775
- fetchImpl,
1776
- identity.fingerprintHash,
1777
- codeVerifier
1778
- // PKCE proof-of-possession (dgxd) — sent on every poll
1779
- );
1780
- if (poll.status === "paired") {
1781
- paired = poll;
1782
- break;
1783
- }
1784
- if (poll.status === "error" && !poll.retryable) {
1785
- terminal = poll;
1786
- break;
1787
- }
1788
- if (!ctx.flags.json && nowMs - lastBeat >= HEARTBEAT_MS) {
1789
- ctx.io.line(
1790
- poll.status === "error" ? ` Backend unavailable (${poll.message}); retrying.` : " Waiting for approval in the BirdyBeep app."
1953
+ let stopBird = ctx.io.bird?.wait();
1954
+ try {
1955
+ for (; ; ) {
1956
+ const nowMs = clock();
1957
+ if (nowMs >= deadline) break;
1958
+ await sleep(intervalMs);
1959
+ const poll = await pairTokenPoll(
1960
+ apiUrl,
1961
+ start.device_code,
1962
+ fetchImpl,
1963
+ identity.fingerprintHash,
1964
+ codeVerifier
1965
+ // PKCE proof-of-possession (dgxd) — sent on every poll
1791
1966
  );
1792
- lastBeat = nowMs;
1967
+ if (poll.status === "paired") {
1968
+ paired = poll;
1969
+ break;
1970
+ }
1971
+ if (poll.status === "error" && !poll.retryable) {
1972
+ terminal = poll;
1973
+ break;
1974
+ }
1975
+ if (!ctx.flags.json && nowMs - lastBeat >= HEARTBEAT_MS) {
1976
+ stopBird?.();
1977
+ ctx.io.line(
1978
+ poll.status === "error" ? ` Backend unavailable (${poll.message}); retrying.` : " Waiting for approval in the BirdyBeep app."
1979
+ );
1980
+ lastBeat = nowMs;
1981
+ stopBird = ctx.io.bird?.wait();
1982
+ }
1793
1983
  }
1984
+ } finally {
1985
+ stopBird?.();
1794
1986
  }
1795
1987
  if (terminal !== void 0) {
1796
1988
  ctx.io.result({ paired: false, reason: terminal.code });
@@ -1838,6 +2030,7 @@ function createPairingCommand(verb, deps = {}) {
1838
2030
  const humanSuffix = approvedBy !== void 0 ? ` to ${approvedBy}` : "";
1839
2031
  const discardedSuffix = discarded > 0 ? ` Discarded ${discarded} event(s) queued before pairing. They will not produce notifications.` : "";
1840
2032
  const nextStep = chain === void 0 ? " Run `birdybeep setup` to install coding-agent hooks." : "";
2033
+ await ctx.io.bird?.celebrate();
1841
2034
  ctx.io.line(`\u2713 Paired${humanSuffix}.${nextStep}${discardedSuffix}`);
1842
2035
  const report = chain !== void 0 ? await runSetupChain(ctx, setupDeps, pairFlags) : void 0;
1843
2036
  ctx.io.result({