@cam5/baby-bird 0.1.0 → 0.1.2

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/cli.js CHANGED
@@ -4,30 +4,37 @@ import {
4
4
  BbError,
5
5
  CliRenderer,
6
6
  CommandProvider,
7
+ ConfigError,
7
8
  NotARepoError,
8
9
  TourCache,
9
10
  UsageError,
10
11
  allPresets,
12
+ buildChatContext,
13
+ chatSlots,
11
14
  describeSource2 as describeSource,
12
15
  generateTour,
16
+ git,
13
17
  gitRoot,
18
+ launchChat,
14
19
  loadConfig,
15
20
  prepareTour,
16
21
  projectConfigPath,
17
22
  relativeTime,
23
+ resolveChat,
18
24
  resolveLlm,
25
+ wrap,
19
26
  writeMaybePaged,
20
27
  writeStdout
21
- } from "./chunk-RWSCST2I.js";
28
+ } from "./chunk-YI2UL4IC.js";
22
29
 
23
30
  // src/cli/main.ts
24
31
  import { Command, InvalidArgumentError } from "commander";
25
- import pc4 from "picocolors";
32
+ import pc6 from "picocolors";
26
33
 
27
34
  // package.json
28
35
  var package_default = {
29
36
  name: "@cam5/baby-bird",
30
- version: "0.1.0",
37
+ version: "0.1.2",
31
38
  description: "Guided code tours for git changes, on the command line.",
32
39
  license: "MIT",
33
40
  type: "module",
@@ -73,6 +80,7 @@ var package_default = {
73
80
  },
74
81
  dependencies: {
75
82
  commander: "^15.0.0",
83
+ lowlight: "^3.3.0",
76
84
  picocolors: "^1.1.1",
77
85
  zod: "^4.6.2"
78
86
  },
@@ -101,6 +109,7 @@ function flagsToOverrides(flags) {
101
109
  else if (flags.color === false) o.render = { color: "never" };
102
110
  if (flags.cache === false) o.cache = { enabled: false };
103
111
  if (flags.pager === false) o.render = { ...o.render ?? {}, pager: "never" };
112
+ if (flags.highlight === false) o.render = { ...o.render ?? {}, highlight: "never" };
104
113
  return o;
105
114
  }
106
115
  async function openSession(flags, opts) {
@@ -126,6 +135,10 @@ function resolveColor(mode) {
126
135
  if (mode === "never") return false;
127
136
  return Boolean(process.stdout.isTTY) && process.env.TERM !== "dumb";
128
137
  }
138
+ function resolveHighlight(mode, colorEnabled) {
139
+ if (mode === "never") return false;
140
+ return colorEnabled;
141
+ }
129
142
  function terminalWidth(configured, stream = process.stdout) {
130
143
  return configured ?? (stream.columns || 80);
131
144
  }
@@ -191,6 +204,13 @@ async function configCommand(flags) {
191
204
  } catch (err) {
192
205
  out.push(` ${c.red(err.message)}`);
193
206
  }
207
+ try {
208
+ const chat = resolveChat(config);
209
+ const provider = new CommandProvider({ command: chat.command, promptVia: "arg", timeoutMs: 1 });
210
+ out.push(` chat: ${provider.describe()}${c.dim(" (bb ask)")}`);
211
+ } catch (err) {
212
+ out.push(` chat: ${c.dim(`none: ${err.message}`)}`);
213
+ }
194
214
  out.push("");
195
215
  out.push(c.bold("Presets"));
196
216
  const presets = allPresets(config);
@@ -205,6 +225,311 @@ async function configCommand(flags) {
205
225
  process.stdout.write(out.join("\n") + "\n");
206
226
  }
207
227
 
228
+ // src/cli/ask-cmd.ts
229
+ import pc5 from "picocolors";
230
+
231
+ // src/cli/progress.ts
232
+ import pc4 from "picocolors";
233
+ var SPINNER = ["\u2722", "\u2733", "\u2736", "\u273B", "\u273D", "\u273B", "\u2736", "\u2733"];
234
+ var VERBS = ["Hatching", "Pecking", "Nesting", "Chirping", "Preening", "Fledging", "Brooding", "Warbling", "Peeping", "Squawking", "Swooping", "Tweeting", "Xeeting", "Cawing", "Twiddling About", "Hopping", "\u{1F425}-ing", "\u{1F423}-ing", "Masticating"];
235
+ var THINKING_TAIL_CHARS = 2e3;
236
+ var VERB_INTERVAL_MS = 7500;
237
+ var LiveProgress = class {
238
+ constructor(stream, opts) {
239
+ this.stream = stream;
240
+ this.opts = opts;
241
+ this.now = opts.now ?? Date.now;
242
+ this.startedAt = this.now();
243
+ this.verbs = opts.verb ? [opts.verb] : opts.verbs && opts.verbs.length ? opts.verbs : shuffle(VERBS);
244
+ this.windowLines = opts.windowLines ?? 5;
245
+ }
246
+ stream;
247
+ opts;
248
+ phaseLabel = "";
249
+ stage = "prep";
250
+ thinking = "";
251
+ pulses = 0;
252
+ answer = "";
253
+ titles = [];
254
+ notices = [];
255
+ outputBytes = 0;
256
+ usage = {};
257
+ startedAt;
258
+ frameIndex = 0;
259
+ lastLines = 0;
260
+ timer = null;
261
+ stopped = false;
262
+ verbs;
263
+ now;
264
+ windowLines;
265
+ onSigint = () => {
266
+ this.stop();
267
+ process.exit(130);
268
+ };
269
+ start() {
270
+ this.stream.write("\x1B[?25l");
271
+ process.once("SIGINT", this.onSigint);
272
+ this.timer = setInterval(() => {
273
+ this.frameIndex++;
274
+ this.render();
275
+ }, this.opts.frameMs ?? 80);
276
+ this.timer.unref?.();
277
+ this.render();
278
+ }
279
+ phase(label) {
280
+ this.phaseLabel = label;
281
+ if (/^asking/i.test(label)) {
282
+ this.stage = "thinking";
283
+ this.thinking = "";
284
+ this.pulses = 0;
285
+ this.answer = "";
286
+ this.titles = [];
287
+ this.notices = [];
288
+ this.outputBytes = 0;
289
+ } else if (this.stage !== "prep") {
290
+ this.stage = "prep";
291
+ }
292
+ this.render();
293
+ }
294
+ llm(event) {
295
+ switch (event.type) {
296
+ case "thinking":
297
+ this.thinking += event.text;
298
+ this.stage = "thinking";
299
+ break;
300
+ case "thinking-pulse":
301
+ this.pulses++;
302
+ this.stage = "thinking";
303
+ break;
304
+ case "answer-start":
305
+ this.answer = "";
306
+ this.titles = [];
307
+ break;
308
+ case "notice":
309
+ this.notices.push(event.text);
310
+ break;
311
+ case "text":
312
+ this.answer += event.text;
313
+ this.titles = extractTitles(this.answer);
314
+ this.stage = "writing";
315
+ break;
316
+ case "output":
317
+ this.outputBytes = event.bytes;
318
+ this.stage = "output";
319
+ break;
320
+ case "usage":
321
+ if (event.outputTokens !== void 0) this.usage.outputTokens = event.outputTokens;
322
+ if (event.thinkingTokens !== void 0) this.usage.thinkingTokens = event.thinkingTokens;
323
+ if (event.model !== void 0) this.usage.model = event.model;
324
+ break;
325
+ default:
326
+ break;
327
+ }
328
+ }
329
+ /** The lines that make up the current status block. */
330
+ frame() {
331
+ const c = pc4.createColors(this.opts.color);
332
+ const width = Math.max(30, this.opts.width);
333
+ const spin = c.magenta(SPINNER[this.frameIndex % SPINNER.length]);
334
+ const elapsed = this.elapsed();
335
+ const fit = (s) => s.length > width - 1 ? s.slice(0, width - 2) + "\u2026" : s;
336
+ const lines = [];
337
+ switch (this.stage) {
338
+ case "prep":
339
+ lines.push(`${spin} ${fit(this.phaseLabel || "Starting")}\u2026 ${c.dim(elapsed)}`);
340
+ break;
341
+ case "thinking": {
342
+ const meta = [elapsed, this.phaseLabel.replace(/^asking\s+/i, "")].filter(Boolean).join(" \xB7 ");
343
+ lines.push(`${spin} ${this.verb()}\u2026 ${c.dim(meta)}`);
344
+ for (const n of this.notices.slice(-1)) lines.push(` ${c.yellow("\u21BB")} ${c.yellow(fit(n).slice(0, width - 4))}`);
345
+ if (this.thinking) {
346
+ const tail = this.thinking.slice(-THINKING_TAIL_CHARS).replace(/\s+/g, " ").trim();
347
+ const wrapped = wrap(tail, width - 4).slice(-this.windowLines);
348
+ for (const l of wrapped) lines.push(` ${c.dim("\u2502")} ${c.dim(l)}`);
349
+ } else if (this.pulses > 0) {
350
+ const room = Math.max(8, width - 16);
351
+ const n = this.pulses % (room + 1);
352
+ lines.push(` ${c.dim("\u2502")} ${c.dim("reasoning")} ${c.magenta("\xB7".repeat(n))}`);
353
+ }
354
+ break;
355
+ }
356
+ case "writing": {
357
+ const n = Math.max(0, this.titles.length - 1);
358
+ lines.push(`${spin} Writing the tour\u2026 ${c.dim(`${elapsed} \xB7 ${n} section${n === 1 ? "" : "s"} so far`)}`);
359
+ for (const note of this.notices.slice(-1)) lines.push(` ${c.yellow("\u21BB")} ${c.yellow(fit(note).slice(0, width - 4))}`);
360
+ const [tourTitle, ...sections] = this.titles;
361
+ const items = [];
362
+ if (tourTitle) items.push(` \u{1F423} ${c.bold(fit(tourTitle).slice(0, width - 6))}`);
363
+ sections.forEach((t) => items.push(` ${c.green("\u2713")} ${fit(t).slice(0, width - 5)}`));
364
+ for (const l of items.slice(-this.windowLines)) lines.push(l);
365
+ break;
366
+ }
367
+ case "output":
368
+ lines.push(`${spin} ${this.verb()}\u2026 ${c.dim(`${elapsed} \xB7 ${formatBytes(this.outputBytes)} received`)}`);
369
+ break;
370
+ }
371
+ return lines;
372
+ }
373
+ render() {
374
+ if (this.stopped) return;
375
+ const lines = this.frame();
376
+ this.stream.write(this.erase() + lines.join("\n") + "\n");
377
+ this.lastLines = lines.length;
378
+ }
379
+ /** Clear the block, restore the cursor, and optionally leave a final line behind. */
380
+ stop(finalLine) {
381
+ if (this.stopped) return;
382
+ this.stopped = true;
383
+ if (this.timer) clearInterval(this.timer);
384
+ process.removeListener("SIGINT", this.onSigint);
385
+ this.stream.write(this.erase() + (finalLine ? finalLine + "\n" : "") + "\x1B[?25h");
386
+ this.lastLines = 0;
387
+ }
388
+ /** A one-line summary of the model call, for the final line. */
389
+ summary() {
390
+ const c = pc4.createColors(this.opts.color);
391
+ const parts = [`Hatched in ${this.elapsed()}`];
392
+ if (this.usage.outputTokens) {
393
+ parts.push(`${formatCount(this.usage.outputTokens)} tokens${this.usage.thinkingTokens ? ` (${formatCount(this.usage.thinkingTokens)} thinking)` : ""}`);
394
+ }
395
+ if (this.usage.model) parts.push(this.usage.model);
396
+ return c.dim(`\u273B ${parts.join(" \xB7 ")}`);
397
+ }
398
+ /** The current verb; rotates every verbIntervalMs so long waits do not stare at one word. */
399
+ verb() {
400
+ const step = Math.floor((this.now() - this.startedAt) / (this.opts.verbIntervalMs ?? VERB_INTERVAL_MS));
401
+ return this.verbs[step % this.verbs.length];
402
+ }
403
+ elapsed() {
404
+ const s = Math.max(0, Math.round((this.now() - this.startedAt) / 1e3));
405
+ return s >= 60 ? `${Math.floor(s / 60)}m ${s % 60}s` : `${s}s`;
406
+ }
407
+ erase() {
408
+ return this.lastLines ? `\x1B[${this.lastLines}A\x1B[0J` : "";
409
+ }
410
+ };
411
+ function extractTitles(text) {
412
+ const out = [];
413
+ for (const m of text.matchAll(/"title"\s*:\s*"((?:[^"\\]|\\.)*)"/g)) {
414
+ out.push(m[1].replace(/\\(["\\/bfnrt]|u[0-9a-fA-F]{4})/g, (_, esc) => {
415
+ switch (esc[0]) {
416
+ case '"':
417
+ return '"';
418
+ case "\\":
419
+ return "\\";
420
+ case "/":
421
+ return "/";
422
+ case "n":
423
+ return "\n";
424
+ case "t":
425
+ return " ";
426
+ case "u":
427
+ return String.fromCharCode(parseInt(esc.slice(1), 16));
428
+ default:
429
+ return "";
430
+ }
431
+ }));
432
+ }
433
+ return out;
434
+ }
435
+ function loggingProgress(log) {
436
+ return {
437
+ phase: (label) => log(label),
438
+ llm: () => {
439
+ }
440
+ };
441
+ }
442
+ function shuffle(items) {
443
+ const out = [...items];
444
+ for (let i = out.length - 1; i > 0; i--) {
445
+ const j = Math.floor(Math.random() * (i + 1));
446
+ [out[i], out[j]] = [out[j], out[i]];
447
+ }
448
+ return out;
449
+ }
450
+ function formatBytes(n) {
451
+ return n < 1024 ? `${n} B` : `${(n / 1024).toFixed(1)} KB`;
452
+ }
453
+ function formatCount(n) {
454
+ return n >= 1e3 ? `${(n / 1e3).toFixed(1)}k` : String(n);
455
+ }
456
+
457
+ // src/cli/ask-cmd.ts
458
+ async function splitAskArgs(words, opts) {
459
+ const [first, ...rest] = words;
460
+ if (first !== void 0 && opts.rangeAllowed && await isRange(first, opts.cwd)) {
461
+ return { range: first, message: joinMessage(rest) };
462
+ }
463
+ return { message: joinMessage(words) };
464
+ }
465
+ async function isRange(word, cwd) {
466
+ if (!word || /\s/.test(word)) return false;
467
+ const res = await git(["rev-parse", "--quiet", "--end-of-options", word], { cwd, allowFailure: true });
468
+ return res.code === 0;
469
+ }
470
+ function joinMessage(words) {
471
+ const text = words.join(" ").trim();
472
+ return text ? text : void 0;
473
+ }
474
+ async function askCommand(words, flags) {
475
+ const session = await openSession(flags, { requireRepo: true });
476
+ if (!session.root) throw new NotARepoError(session.cwd);
477
+ const { config } = session.loaded;
478
+ const c = pc5.createColors(session.colorEnabled);
479
+ const chat = resolveChat(config);
480
+ const slots = chatSlots(chat.command);
481
+ if (!slots.context) {
482
+ throw new ConfigError("The chat command has no {context-file} or {context} token, so the tour cannot be handed to it.", "Add one of them to the chatCommand; see the README.");
483
+ }
484
+ const llm = resolveLlm(config);
485
+ const { range, message } = await splitAskArgs(words, { cwd: session.root, rangeAllowed: !flags.working && !flags.staged });
486
+ session.debug(range ? `range: ${range}` : "no range argument");
487
+ session.debug(message ? `opening message: ${JSON.stringify(message)}` : "no opening message");
488
+ const live = flags.progress !== false && !flags.debug && Boolean(process.stderr.isTTY) && !flags.dumpContext;
489
+ const progress = live ? new LiveProgress(process.stderr, { color: session.colorEnabled, width: terminalWidth(null, process.stderr) }) : loggingProgress(session.debug);
490
+ const options = {
491
+ cwd: session.root,
492
+ config,
493
+ cacheDir: session.loaded.cacheDir,
494
+ range: { arg: range, working: flags.working, staged: flags.staged },
495
+ refresh: flags.refresh,
496
+ noCache: flags.cache === false,
497
+ debug: session.debug,
498
+ warn: session.warn,
499
+ progress
500
+ };
501
+ if (progress instanceof LiveProgress) progress.start();
502
+ let prepared;
503
+ let result;
504
+ try {
505
+ prepared = await prepareTour(options);
506
+ result = await generateTour(options, prepared);
507
+ } catch (err) {
508
+ if (progress instanceof LiveProgress) progress.stop();
509
+ throw err;
510
+ }
511
+ if (progress instanceof LiveProgress) progress.stop(result.fromCache ? void 0 : progress.summary());
512
+ const context = buildChatContext({ tour: result.tour, change: prepared.context, maxBytes: llm.maxPromptBytes, tourPath: result.cachePath });
513
+ if (context.truncation.truncated.length || context.truncation.omitted.length) {
514
+ session.warn(`Chat context exceeded ${llm.maxPromptBytes} bytes; truncated ${context.truncation.truncated.length} and omitted ${context.truncation.omitted.length} file diff(s).`);
515
+ }
516
+ if (flags.dumpContext) {
517
+ await writeStdout(context.text.endsWith("\n") ? context.text : context.text + "\n");
518
+ return;
519
+ }
520
+ const tour = result.tour;
521
+ const when = result.fromCache ? `cached ${relativeTime(tour.generatedAt)}` : "generated just now";
522
+ process.stderr.write(`${c.bold(`\u{1F423} ${tour.title}`)}${c.dim(` \xB7 ${describeSource(tour.source)} \xB7 ${when}`)}
523
+ `);
524
+ if (message && !slots.message) {
525
+ session.warn("This chat command has no {message} slot; type your question into the chat once it opens.");
526
+ }
527
+ process.stderr.write(c.dim(`Opening ${chat.preset ?? chat.command[0]} with the tour as context\u2026
528
+ `));
529
+ const outcome = await launchChat({ command: chat.command, context: context.text, message, cwd: session.root, env: chat.env, debug: session.debug });
530
+ process.exitCode = outcome.exitCode;
531
+ }
532
+
208
533
  // src/cli/init-cmd.ts
209
534
  import { mkdir, writeFile } from "fs/promises";
210
535
  import { dirname } from "path";
@@ -232,6 +557,8 @@ async function tourCommand(rangeArg, flags) {
232
557
  const session = await openSession(flags, { requireRepo: true });
233
558
  if (!session.root) throw new NotARepoError(session.cwd);
234
559
  const { config } = session.loaded;
560
+ const live = flags.progress !== false && !flags.debug && Boolean(process.stderr.isTTY) && !flags.dumpPrompt;
561
+ const progress = live ? new LiveProgress(process.stderr, { color: session.colorEnabled, width: terminalWidth(null, process.stderr) }) : loggingProgress(session.debug);
235
562
  const options = {
236
563
  cwd: session.root,
237
564
  config,
@@ -240,14 +567,23 @@ async function tourCommand(rangeArg, flags) {
240
567
  refresh: flags.refresh,
241
568
  noCache: flags.cache === false,
242
569
  debug: session.debug,
243
- warn: session.warn
570
+ warn: session.warn,
571
+ progress
244
572
  };
245
- const prepared = await prepareTour(options);
246
- if (flags.dumpPrompt) {
247
- await writeStdout(prepared.built.prompt.endsWith("\n") ? prepared.built.prompt : prepared.built.prompt + "\n");
248
- return;
573
+ if (progress instanceof LiveProgress) progress.start();
574
+ let result;
575
+ try {
576
+ const prepared = await prepareTour(options);
577
+ if (flags.dumpPrompt) {
578
+ await writeStdout(prepared.built.prompt.endsWith("\n") ? prepared.built.prompt : prepared.built.prompt + "\n");
579
+ return;
580
+ }
581
+ result = await generateTour(options, prepared);
582
+ } catch (err) {
583
+ if (progress instanceof LiveProgress) progress.stop();
584
+ throw err;
249
585
  }
250
- const result = await generateTour(options, prepared);
586
+ if (progress instanceof LiveProgress) progress.stop(result.fromCache ? void 0 : progress.summary());
251
587
  if (flags.json) {
252
588
  await writeStdout(JSON.stringify(result.tour, null, 2) + "\n");
253
589
  return;
@@ -257,7 +593,8 @@ async function tourCommand(rangeArg, flags) {
257
593
  color: session.colorEnabled,
258
594
  width: terminalWidth(config.render.width),
259
595
  section: flags.section,
260
- fromCache: result.fromCache
596
+ fromCache: result.fromCache,
597
+ highlight: resolveHighlight(config.render.highlight, session.colorEnabled)
261
598
  });
262
599
  await writeMaybePaged(output, {
263
600
  mode: config.render.pager,
@@ -272,6 +609,9 @@ function positiveInt(value) {
272
609
  if (!Number.isInteger(n) || n < 1) throw new InvalidArgumentError("must be a positive integer");
273
610
  return n;
274
611
  }
612
+ function optsWithGlobals(cmd) {
613
+ return cmd.optsWithGlobals();
614
+ }
275
615
  function addGlobalFlags(cmd) {
276
616
  return cmd.option("-C, --cwd <dir>", "run as if started in <dir>").option("--preset <name>", "LLM preset to use (see `bb config`)").option("--color", "force colored output").option("--no-color", "disable colored output").option("--debug", "print diagnostics (resolved range, prompt size, raw model output) to stderr");
277
617
  }
@@ -280,8 +620,8 @@ process.stdout.on("error", (err) => {
280
620
  throw err;
281
621
  });
282
622
  var program = new Command();
283
- program.name("bb").description("Guided code tours for git changes, on the command line.").version(package_default.version, "-V, --version").configureOutput({ outputError: (str, write) => write(pc4.red(str)) }).showHelpAfterError("(run with --help for usage)");
284
- addGlobalFlags(program).argument("[range]", "what to tour: A..B, A...B, or a single ref (compared at its merge-base with HEAD)").option("-w, --working", "tour uncommitted changes in the working tree (tracked and untracked)").option("-s, --staged", "tour staged changes only").option("--section <n>", "render only section <n>", positiveInt).option("--json", "print the tour as JSON instead of rendering it").option("--dump-prompt", "print the prompt that would be sent to the model and exit").option("--refresh", "ignore a cached tour and regenerate it").option("--no-cache", "neither read nor write the tour cache").option("--no-pager", "never pipe output through a pager").addHelpText(
623
+ program.name("bb").description("Guided code tours for git changes, on the command line.").version(package_default.version, "-V, --version").enablePositionalOptions().configureOutput({ outputError: (str, write) => write(pc6.red(str)) }).showHelpAfterError("(run with --help for usage)");
624
+ addGlobalFlags(program).argument("[range]", "what to tour: A..B, A...B, or a single ref (compared at its merge-base with HEAD)").option("-w, --working", "tour uncommitted changes in the working tree (tracked and untracked)").option("-s, --staged", "tour staged changes only").option("--section <n>", "render only section <n>", positiveInt).option("--json", "print the tour as JSON instead of rendering it").option("--dump-prompt", "print the prompt that would be sent to the model and exit").option("--refresh", "ignore a cached tour and regenerate it").option("--no-cache", "neither read nor write the tour cache").option("--no-pager", "never pipe output through a pager").option("--no-highlight", "plain excerpts with green/red +/- signs instead of syntax colors").option("--no-progress", "do not show the live status block while generating").addHelpText(
285
625
  "after",
286
626
  `
287
627
  Examples:
@@ -292,34 +632,51 @@ Examples:
292
632
  bb --section 2 show only the second section
293
633
  bb --json | jq machine-readable output
294
634
  bb --preset claude-sonnet --refresh
635
+ bb ask HEAD~1 "what changed in progress.ts?" chat about a tour (see bb ask --help)
295
636
  `
296
637
  ).action(async (range, flags) => {
297
638
  await tourCommand(range, flags);
298
639
  });
640
+ addGlobalFlags(program.command("ask").description("chat with the LLM about a tour (the tour is generated or taken from the cache first)")).argument("[range]", "what to tour, as for `bb`; omit it to tour the same thing bare `bb` would").argument("[message...]", "opening message for the chat").option("-w, --working", "tour uncommitted changes in the working tree (tracked and untracked)").option("-s, --staged", "tour staged changes only").option("--refresh", "ignore a cached tour and regenerate it").option("--no-cache", "neither read nor write the tour cache").option("--no-progress", "do not show the live status block while generating").option("--dump-context", "print the context that would be handed to the chat and exit").addHelpText(
641
+ "after",
642
+ `
643
+ The first word is taken as a range only when git can resolve it (HEAD~3, main..feature);
644
+ everything else is the opening message. The chat command comes from the preset (see \`bb config\`).
645
+
646
+ Examples:
647
+ bb ask chat about the current branch's tour
648
+ bb ask "why does the cache key include the command?"
649
+ bb ask HEAD~1 "what is going on in progress.ts?"
650
+ bb ask main..feature --preset claude-opus
651
+ bb ask --staged "is this safe to commit?"
652
+ `
653
+ ).action(async (range, message, _flags, cmd) => {
654
+ await askCommand([range, ...message].filter((w) => w !== void 0), optsWithGlobals(cmd));
655
+ });
299
656
  var cache = program.command("cache").description("manage cached tours");
300
- addGlobalFlags(cache.command("ls").description("list cached tours")).action(cacheLsCommand);
301
- addGlobalFlags(cache.command("clear").description("delete all cached tours")).action(cacheClearCommand);
302
- addGlobalFlags(cache.command("path").description("print the cache directory")).action(cachePathCommand);
303
- addGlobalFlags(program.command("config").description("show the effective configuration, its sources, and available presets")).option("--json", "print only the effective config as JSON").action(configCommand);
304
- addGlobalFlags(program.command("init").description("write a starter .baby-bird/config.json in this repository")).option("--force", "overwrite an existing project config").action(initCommand);
657
+ addGlobalFlags(cache.command("ls").description("list cached tours")).action((_flags, cmd) => cacheLsCommand(optsWithGlobals(cmd)));
658
+ addGlobalFlags(cache.command("clear").description("delete all cached tours")).action((_flags, cmd) => cacheClearCommand(optsWithGlobals(cmd)));
659
+ addGlobalFlags(cache.command("path").description("print the cache directory")).action((_flags, cmd) => cachePathCommand(optsWithGlobals(cmd)));
660
+ addGlobalFlags(program.command("config").description("show the effective configuration, its sources, and available presets")).option("--json", "print only the effective config as JSON").action((_flags, cmd) => configCommand(optsWithGlobals(cmd)));
661
+ addGlobalFlags(program.command("init").description("write a starter .baby-bird/config.json in this repository")).option("--force", "overwrite an existing project config").action((_flags, cmd) => initCommand(optsWithGlobals(cmd)));
305
662
  program.parseAsync(process.argv).catch((err) => {
306
663
  const debug = process.argv.includes("--debug");
307
664
  if (err instanceof BbError) {
308
- process.stderr.write(pc4.red(`error: ${err.message}
665
+ process.stderr.write(pc6.red(`error: ${err.message}
309
666
  `));
310
- if (err.hint) process.stderr.write(pc4.dim(`${err.hint}
667
+ if (err.hint) process.stderr.write(pc6.dim(`${err.hint}
311
668
  `));
312
- if (debug && err instanceof BadLlmOutputError) process.stderr.write(pc4.dim(`--- raw model output ---
669
+ if (debug && err instanceof BadLlmOutputError) process.stderr.write(pc6.dim(`--- raw model output ---
313
670
  ${err.raw}
314
671
  `));
315
- if (debug && err.cause) process.stderr.write(pc4.dim(`${String(err.cause.stack ?? err.cause)}
672
+ if (debug && err.cause) process.stderr.write(pc6.dim(`${String(err.cause.stack ?? err.cause)}
316
673
  `));
317
674
  process.exitCode = err.exitCode;
318
675
  return;
319
676
  }
320
- process.stderr.write(pc4.red(`error: ${err.message ?? String(err)}
677
+ process.stderr.write(pc6.red(`error: ${err.message ?? String(err)}
321
678
  `));
322
- if (debug && err.stack) process.stderr.write(pc4.dim(`${err.stack}
679
+ if (debug && err.stack) process.stderr.write(pc6.dim(`${err.stack}
323
680
  `));
324
681
  process.exitCode = 1;
325
682
  });