@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/bin.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runCli
4
- } from "./chunk-J45QGUDO.js";
4
+ } from "./chunk-NSHPZ3RX.js";
5
5
 
6
6
  // src/node-check.ts
7
7
  var MIN_NODE_MAJOR = 20;
@@ -1,10 +1,164 @@
1
1
  // src/framework.ts
2
2
  import { mkdirSync } from "fs";
3
3
  import { birdyBeepConfigDir } from "@birdybeep/agent-core";
4
+
5
+ // src/presentation.ts
6
+ var BIRD = [
7
+ " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588",
8
+ " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588",
9
+ " \u2588\u2588\u2588\u2588\u2594\u2594\u2594\u2594\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588",
10
+ " \u2588\u2588\u2588\u2588\u2588\u2588 \u25AA \u2588\u2588\u2588\u2588\u2588\u2588",
11
+ "\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588",
12
+ "\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588",
13
+ " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588",
14
+ " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588",
15
+ " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588",
16
+ " \u2588\u2588 \u2588\u2588",
17
+ " \u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588"
18
+ ].join("\n");
19
+ function presentation(output, env = process.env) {
20
+ const terminal = output.isTTY === true && env.TERM !== "dumb";
21
+ const color = terminal && !env.NO_COLOR;
22
+ const lime = (text) => {
23
+ if (!color) return text;
24
+ const code = /^(truecolor|24bit)$/.test(env.COLORTERM ?? "") ? "38;2;198;242;78" : (env.TERM ?? "").includes("256color") ? "38;5;191" : "92";
25
+ return `\x1B[${code}m${text}\x1B[0m`;
26
+ };
27
+ return {
28
+ lime,
29
+ banner: () => terminal ? `
30
+ ${lime(BIRD)}
31
+
32
+ ` : "",
33
+ help: (text) => text.split("\n").map((line, i) => i === 0 || line.endsWith(":") ? lime(line) : line).join("\n")
34
+ };
35
+ }
36
+
37
+ // src/bird-animation.ts
38
+ function birdFrame(blink = false, lift = 0, pose = "rest") {
39
+ const rows = BIRD.split("\n");
40
+ if (pose === "inhale" || pose === "breathe") {
41
+ 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";
42
+ } else if (pose === "crouch" || pose === "land") {
43
+ rows.splice(7, 1);
44
+ rows.unshift("");
45
+ rows[7] = " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588";
46
+ rows[8] = " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588";
47
+ rows[9] = " \u2588\u2588 \u2588\u2588";
48
+ } else if (pose === "stretch") {
49
+ rows[4] = " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588";
50
+ rows[5] = " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588";
51
+ rows[6] = " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588";
52
+ rows[7] = " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588";
53
+ rows[9] = " \u2588\u2588 \u2588\u2588";
54
+ rows[10] = " \u2580\u2580 \u2580\u2580";
55
+ } else if (pose === "flight") {
56
+ rows[4] = "\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588";
57
+ rows[5] = "\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2572\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588";
58
+ rows[6] = " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2572\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588";
59
+ rows[7] = " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588";
60
+ rows[9] = " \u2580\u2580 \u2580\u2580";
61
+ rows[10] = "";
62
+ }
63
+ if (blink)
64
+ rows[pose === "crouch" || pose === "land" ? 4 : 3] = rows[pose === "crouch" || pose === "land" ? 4 : 3].replace(" \u25AA ", " \u2501 ");
65
+ return [...Array(2 - lift).fill(""), ...rows, ...Array(lift).fill("")].join("\n");
66
+ }
67
+ var HOP = [
68
+ { pose: "crouch", lift: 0, ms: 180 },
69
+ { pose: "stretch", lift: 1, ms: 80 },
70
+ { pose: "flight", lift: 2, ms: 130 },
71
+ { pose: "flight", lift: 2, ms: 110 },
72
+ { pose: "stretch", lift: 1, ms: 90 },
73
+ { pose: "land", lift: 0, ms: 140 },
74
+ { pose: "inhale", lift: 0, ms: 120 },
75
+ { pose: "rest", lift: 0, ms: 180 }
76
+ ];
77
+ function createBirdAnimation(output, env = process.env) {
78
+ const allowed = () => output.isTTY === true && env.TERM !== "dumb" && !env.CI && env.BIRDYBEEP_ANIMATION !== "0" && (output.columns ?? 80) >= 26 && (output.rows ?? 40) >= 16;
79
+ const style = presentation(output, env);
80
+ let activeStop;
81
+ const begin = () => {
82
+ activeStop?.();
83
+ if (!allowed()) return void 0;
84
+ let stopped = false;
85
+ let timer;
86
+ const paint = (frame, rewind = false) => {
87
+ output.write(
88
+ (rewind ? "\x1B[13A" : "") + frame.split("\n").map((line) => `\r\x1B[2K${style.lime(line)}
89
+ `).join("")
90
+ );
91
+ };
92
+ const finish = (clear) => {
93
+ if (stopped) return;
94
+ stopped = true;
95
+ if (timer) clearInterval(timer);
96
+ process.off("exit", stop);
97
+ process.stdout.off("resize", resize);
98
+ if (clear) output.write("\x1B[13A" + "\r\x1B[2K\n".repeat(13) + "\x1B[13A\r");
99
+ activeStop = void 0;
100
+ };
101
+ const stop = () => finish(true);
102
+ const resize = () => {
103
+ stopped = true;
104
+ if (timer) clearInterval(timer);
105
+ process.off("exit", stop);
106
+ process.stdout.off("resize", resize);
107
+ activeStop = void 0;
108
+ };
109
+ process.once("exit", stop);
110
+ process.stdout.once("resize", resize);
111
+ activeStop = stop;
112
+ paint(birdFrame());
113
+ return {
114
+ stop,
115
+ settle: () => finish(false),
116
+ draw: (frame) => {
117
+ if (!stopped) {
118
+ paint(frame, true);
119
+ }
120
+ },
121
+ repeat: (callback) => {
122
+ timer = setInterval(callback, 140);
123
+ timer.unref();
124
+ }
125
+ };
126
+ };
127
+ return {
128
+ wait: () => {
129
+ const canvas = begin();
130
+ if (!canvas) return () => {
131
+ };
132
+ let tick = 0;
133
+ canvas.repeat(() => {
134
+ tick += 1;
135
+ const phase = tick % 22;
136
+ const pose = phase >= 8 && phase <= 10 ? "breathe" : phase >= 6 && phase <= 12 ? "inhale" : "rest";
137
+ canvas.draw(birdFrame(phase === 18, 0, pose));
138
+ });
139
+ return canvas.stop;
140
+ },
141
+ celebrate: async () => {
142
+ const canvas = begin();
143
+ if (!canvas) return;
144
+ try {
145
+ for (const { pose, lift, ms } of HOP) {
146
+ canvas.draw(birdFrame(pose === "land", lift, pose));
147
+ await new Promise((resolve) => setTimeout(resolve, ms));
148
+ }
149
+ } finally {
150
+ canvas.settle();
151
+ }
152
+ }
153
+ };
154
+ }
155
+
156
+ // src/framework.ts
4
157
  var EXIT = { OK: 0, ERROR: 1, USAGE: 2 };
5
- function createIo(json, stdout, stderr) {
158
+ function createIo(json, stdout, stderr, animate = false) {
6
159
  return {
7
160
  json,
161
+ ...!json && animate ? { bird: createBirdAnimation(stdout) } : {},
8
162
  line: (text) => {
9
163
  if (!json) stdout.write(`${text}
10
164
  `);
@@ -140,7 +294,8 @@ function renderCommandHelp(path, command) {
140
294
  }
141
295
  async function dispatch(argv, deps) {
142
296
  const { flags, rest } = parseGlobalFlags(argv);
143
- const io = createIo(flags.json, deps.stdout, deps.stderr);
297
+ const io = createIo(flags.json, deps.stdout, deps.stderr, !flags.nonInteractive);
298
+ const style = presentation(deps.stdout);
144
299
  if (deps.ensureConfig !== false) {
145
300
  try {
146
301
  mkdirSync(birdyBeepConfigDir(), { recursive: true, mode: 448 });
@@ -168,7 +323,7 @@ async function dispatch(argv, deps) {
168
323
  }
169
324
  }
170
325
  if (rest.length === 0 || flags.help && command === void 0) {
171
- io.emit(renderRootHelp(deps.version, deps.commands), {
326
+ io.emit(style.banner() + style.help(renderRootHelp(deps.version, deps.commands)), {
172
327
  version: deps.version,
173
328
  commands: deps.commands.map((c) => ({
174
329
  name: c.name,
@@ -184,7 +339,7 @@ async function dispatch(argv, deps) {
184
339
  }
185
340
  const path = pathParts.join(" ");
186
341
  if (flags.help) {
187
- io.emit(renderCommandHelp(path, command), {
342
+ io.emit(style.help(renderCommandHelp(path, command)), {
188
343
  name: path,
189
344
  summary: command.summary,
190
345
  usage: command.usage,
@@ -206,6 +361,10 @@ async function dispatch(argv, deps) {
206
361
  }
207
362
  let code;
208
363
  try {
364
+ if (!flags.nonInteractive && (path === "setup" || path === "pair")) {
365
+ const banner = style.banner();
366
+ if (banner) io.line(banner.trimEnd());
367
+ }
209
368
  code = await command.run({ args, flags, io });
210
369
  } catch (err) {
211
370
  if (err instanceof MissingInputError) {
@@ -227,7 +386,7 @@ async function dispatch(argv, deps) {
227
386
  }
228
387
 
229
388
  // src/version.ts
230
- var CLI_VERSION = "0.8.1".length > 0 ? "0.8.1" : "0.0.0";
389
+ var CLI_VERSION = "0.8.3".length > 0 ? "0.8.3" : "0.0.0";
231
390
 
232
391
  // src/commands/agent.ts
233
392
  import { claudeCodeAdapter } from "@birdybeep/claude-code";
@@ -820,18 +979,35 @@ import { randomBytes } from "crypto";
820
979
  import { closeSync, openSync, rmSync, writeFileSync as writeFileSync2 } from "fs";
821
980
  import { tmpdir } from "os";
822
981
  import { basename, dirname, join as join2 } from "path";
982
+ import { performance } from "perf_hooks";
823
983
  import {
824
984
  createSender as defaultCreateSender2,
985
+ DEFAULT_SEND_TIMEOUT_MS,
986
+ DEFAULT_TOTAL_BUDGET_MS,
825
987
  resolveOnPath
826
988
  } from "@birdybeep/agent-core";
827
- import { isClaudeCodeHookPayload, runClaudeHook } from "@birdybeep/claude-code";
828
- import { isCodexHookPayload, runCodexHook } from "@birdybeep/codex";
829
989
  import {
990
+ configuredClaudeHookTimeoutSeconds,
991
+ isClaudeCodeHookPayload,
992
+ runClaudeHook
993
+ } from "@birdybeep/claude-code";
994
+ import {
995
+ configuredCodexHookTimeoutSeconds,
996
+ isCodexHookPayload,
997
+ runCodexHook
998
+ } from "@birdybeep/codex";
999
+ import {
1000
+ configuredCopilotHookTimeoutSeconds,
830
1001
  isCopilotHookEventName,
831
1002
  isCopilotHookPayload,
832
1003
  runCopilotHook
833
1004
  } from "@birdybeep/copilot";
834
- import { isCursorHookEventName, isCursorHookPayload, runCursorHook } from "@birdybeep/cursor";
1005
+ import {
1006
+ configuredCursorHookTimeoutSeconds,
1007
+ isCursorHookEventName,
1008
+ isCursorHookPayload,
1009
+ runCursorHook
1010
+ } from "@birdybeep/cursor";
835
1011
  import { isOpenCodeEventPayload, runOpenCodeHook } from "@birdybeep/opencode";
836
1012
  var RUNNERS = {
837
1013
  claude: runClaudeHook,
@@ -847,6 +1023,21 @@ var HOOK_HARNESSES = [
847
1023
  "copilot"
848
1024
  ];
849
1025
  var STDIN_READ_TIMEOUT_MS = 3e3;
1026
+ var LEGACY_HOOK_RUNTIME_BUDGET_MS = 9e3;
1027
+ function hookRuntimeBudgetMs(configuredTimeoutSeconds) {
1028
+ if (configuredTimeoutSeconds === void 0 || !Number.isFinite(configuredTimeoutSeconds) || configuredTimeoutSeconds <= 0) {
1029
+ return LEGACY_HOOK_RUNTIME_BUDGET_MS;
1030
+ }
1031
+ const configuredBudgetMs = Math.max(1, Math.floor(configuredTimeoutSeconds * 1e3) - 1e3);
1032
+ return Math.min(LEGACY_HOOK_RUNTIME_BUDGET_MS, configuredBudgetMs);
1033
+ }
1034
+ function configuredHookTimeoutSeconds(harness) {
1035
+ if (harness === "claude") return configuredClaudeHookTimeoutSeconds();
1036
+ if (harness === "codex") return configuredCodexHookTimeoutSeconds();
1037
+ if (harness === "cursor") return configuredCursorHookTimeoutSeconds();
1038
+ if (harness === "copilot") return configuredCopilotHookTimeoutSeconds();
1039
+ return void 0;
1040
+ }
850
1041
  function withTimeout(promise, ms, fallback) {
851
1042
  return new Promise((resolve) => {
852
1043
  let settled = false;
@@ -967,15 +1158,22 @@ function detachCodexNotifyWorker(payload) {
967
1158
  }
968
1159
  }
969
1160
  function createHookCommand(deps = {}) {
970
- const makeSender = deps.createSender ?? ((baseUrl) => defaultCreateSender2({ baseUrl }));
1161
+ const makeSender = deps.createSender ?? ((baseUrl, budgetMs) => defaultCreateSender2({
1162
+ baseUrl,
1163
+ timeoutMs: Math.min(DEFAULT_SEND_TIMEOUT_MS, budgetMs),
1164
+ totalBudgetMs: budgetMs
1165
+ }));
971
1166
  const readStdin = deps.readStdin ?? readStdinDefault;
972
1167
  const stdinTimeoutMs = deps.stdinTimeoutMs ?? STDIN_READ_TIMEOUT_MS;
1168
+ const now = deps.now ?? (() => performance.now());
1169
+ const readConfiguredHookTimeoutSeconds = deps.configuredHookTimeoutSeconds ?? configuredHookTimeoutSeconds;
973
1170
  const detachCodexNotify = deps.detachCodexNotify ?? detachCodexNotifyWorker;
974
1171
  return {
975
1172
  name: "hook",
976
1173
  summary: "Internal: normalize + send an event fired by a harness hook",
977
1174
  usage: "birdybeep hook <claude|codex|opencode|cursor|copilot> [copilot-event]",
978
1175
  run: async (ctx) => {
1176
+ const hookStartedAt = now();
979
1177
  const harness = ctx.args[0];
980
1178
  if (!isHarnessName(harness)) {
981
1179
  ctx.io.errline(`birdybeep hook: expected one of ${HOOK_HARNESSES.join("|")}`);
@@ -1041,7 +1239,10 @@ function createHookCommand(deps = {}) {
1041
1239
  );
1042
1240
  return EXIT.ERROR;
1043
1241
  }
1044
- const sender = makeSender(resolveApiUrl());
1242
+ const runtimeBudgetMs = hookRuntimeBudgetMs(readConfiguredHookTimeoutSeconds(harness));
1243
+ const elapsedMs = Math.max(0, now() - hookStartedAt);
1244
+ const budgetMs = Math.max(1, Math.min(DEFAULT_TOTAL_BUDGET_MS, runtimeBudgetMs - elapsedMs));
1245
+ const sender = makeSender(resolveApiUrl(), budgetMs);
1045
1246
  const result = await runHookCommand(harness, payload, sender, copilotEventName);
1046
1247
  ctx.io.result({
1047
1248
  harness: handler,
@@ -1770,32 +1971,39 @@ function createPairingCommand(verb, deps = {}) {
1770
1971
  let lastBeat = startedAt;
1771
1972
  let paired;
1772
1973
  let terminal;
1773
- for (; ; ) {
1774
- const nowMs = clock();
1775
- if (nowMs >= deadline) break;
1776
- await sleep(intervalMs);
1777
- const poll = await pairTokenPoll(
1778
- apiUrl,
1779
- start.device_code,
1780
- fetchImpl,
1781
- identity.fingerprintHash,
1782
- codeVerifier
1783
- // PKCE proof-of-possession (dgxd) — sent on every poll
1784
- );
1785
- if (poll.status === "paired") {
1786
- paired = poll;
1787
- break;
1788
- }
1789
- if (poll.status === "error" && !poll.retryable) {
1790
- terminal = poll;
1791
- break;
1792
- }
1793
- if (!ctx.flags.json && nowMs - lastBeat >= HEARTBEAT_MS) {
1794
- ctx.io.line(
1795
- poll.status === "error" ? ` Backend unavailable (${poll.message}); retrying.` : " Waiting for approval in the BirdyBeep app."
1974
+ let stopBird = ctx.io.bird?.wait();
1975
+ try {
1976
+ for (; ; ) {
1977
+ const nowMs = clock();
1978
+ if (nowMs >= deadline) break;
1979
+ await sleep(intervalMs);
1980
+ const poll = await pairTokenPoll(
1981
+ apiUrl,
1982
+ start.device_code,
1983
+ fetchImpl,
1984
+ identity.fingerprintHash,
1985
+ codeVerifier
1986
+ // PKCE proof-of-possession (dgxd) — sent on every poll
1796
1987
  );
1797
- lastBeat = nowMs;
1988
+ if (poll.status === "paired") {
1989
+ paired = poll;
1990
+ break;
1991
+ }
1992
+ if (poll.status === "error" && !poll.retryable) {
1993
+ terminal = poll;
1994
+ break;
1995
+ }
1996
+ if (!ctx.flags.json && nowMs - lastBeat >= HEARTBEAT_MS) {
1997
+ stopBird?.();
1998
+ ctx.io.line(
1999
+ poll.status === "error" ? ` Backend unavailable (${poll.message}); retrying.` : " Waiting for approval in the BirdyBeep app."
2000
+ );
2001
+ lastBeat = nowMs;
2002
+ stopBird = ctx.io.bird?.wait();
2003
+ }
1798
2004
  }
2005
+ } finally {
2006
+ stopBird?.();
1799
2007
  }
1800
2008
  if (terminal !== void 0) {
1801
2009
  ctx.io.result({ paired: false, reason: terminal.code });
@@ -1843,6 +2051,7 @@ function createPairingCommand(verb, deps = {}) {
1843
2051
  const humanSuffix = approvedBy !== void 0 ? ` to ${approvedBy}` : "";
1844
2052
  const discardedSuffix = discarded > 0 ? ` Discarded ${discarded} event(s) queued before pairing. They will not produce notifications.` : "";
1845
2053
  const nextStep = chain === void 0 ? " Run `birdybeep setup` to install coding-agent hooks." : "";
2054
+ await ctx.io.bird?.celebrate();
1846
2055
  ctx.io.line(`\u2713 Paired${humanSuffix}.${nextStep}${discardedSuffix}`);
1847
2056
  const report = chain !== void 0 ? await runSetupChain(ctx, setupDeps, pairFlags) : void 0;
1848
2057
  ctx.io.result({
@@ -2262,4 +2471,4 @@ export {
2262
2471
  buildCommands,
2263
2472
  runCli
2264
2473
  };
2265
- //# sourceMappingURL=chunk-J45QGUDO.js.map
2474
+ //# sourceMappingURL=chunk-NSHPZ3RX.js.map