@cam5/baby-bird 0.1.1 → 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/README.md +37 -6
- package/dist/{chunk-7ALXKEST.js → chunk-YI2UL4IC.js} +189 -39
- package/dist/chunk-YI2UL4IC.js.map +1 -0
- package/dist/cli.js +153 -48
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +89 -13
- package/dist/index.js +13 -1
- package/package.json +1 -1
- package/dist/chunk-7ALXKEST.js.map +0 -1
package/README.md
CHANGED
|
@@ -61,6 +61,8 @@ bb --dump-prompt print the prompt instead of calling the model
|
|
|
61
61
|
bb --refresh regenerate even if a cached tour exists
|
|
62
62
|
bb --preset claude-sonnet
|
|
63
63
|
bb --no-progress no live status block
|
|
64
|
+
bb ask "why is the cache keyed by command?" chat about the current branch's tour
|
|
65
|
+
bb ask HEAD~1 "what is going on in progress.ts?" chat about a specific range
|
|
64
66
|
```
|
|
65
67
|
|
|
66
68
|
### What does bare `bb` tour?
|
|
@@ -72,6 +74,25 @@ bb --no-progress no live status block
|
|
|
72
74
|
|
|
73
75
|
The header of every tour says which one was used.
|
|
74
76
|
|
|
77
|
+
### Asking questions about a tour
|
|
78
|
+
|
|
79
|
+
`bb ask` opens an interactive chat with the same LLM CLI, primed with the tour:
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
bb ask the tour bare `bb` would show
|
|
83
|
+
bb ask "where is the retry handled?" with an opening question
|
|
84
|
+
bb ask HEAD~1 "what is going on in progress.ts?"
|
|
85
|
+
bb ask main..feature --preset claude-opus
|
|
86
|
+
bb ask --staged "is this safe to commit?"
|
|
87
|
+
bb ask --dump-context print what the chat would be given, and exit
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
It takes the same range arguments and flags as `bb`, then an opening message. The first word is treated as a range only when git can resolve it (`HEAD~3`, `main..feature`); anything else is part of the message, so quoting is optional for plain questions.
|
|
91
|
+
|
|
92
|
+
The tour is generated first, or taken from the cache: run `bb HEAD~1` in one window, and `bb ask HEAD~1 ...` in another reuses that tour and opens the chat straight away. The chat gets, as a system prompt: a short framing, the whole tour (sections, descriptions, excerpts with line numbers, the base and head SHAs), and then the same labeled diff the tour was built from, shortened to `llm.maxPromptBytes` like the prompt. With the Claude presets the chat is a normal interactive Claude Code session in the repository, so it can also open files and run git. `bb ask` exits with the chat's exit code.
|
|
93
|
+
|
|
94
|
+
The command that opens the chat is the preset's `chatCommand` (see [LLM presets](#llm-presets)); presets without one cannot be asked.
|
|
95
|
+
|
|
75
96
|
## Configuration
|
|
76
97
|
|
|
77
98
|
Config is JSON, merged in this order (later wins; objects merge, arrays replace):
|
|
@@ -80,7 +101,7 @@ Config is JSON, merged in this order (later wins; objects merge, arrays replace)
|
|
|
80
101
|
2. `$XDG_CONFIG_HOME/baby-bird/config.json` (default `~/.config/baby-bird/config.json`)
|
|
81
102
|
3. `<repo>/.baby-bird/config.json` (create one with `bb init`)
|
|
82
103
|
4. environment: `BB_PRESET`, `BB_LLM_COMMAND`, `BB_CODEHOST`, `BB_CACHE_DIR`, `BB_NO_CACHE`, `NO_COLOR`
|
|
83
|
-
5. flags: `--preset`, `--color/--no-color`, `--no-cache`, `--no-pager`
|
|
104
|
+
5. flags: `--preset`, `--color/--no-color`, `--no-cache`, `--no-pager` (before or after a subcommand name)
|
|
84
105
|
|
|
85
106
|
`bb config` shows every layer, what it contributed, the resolved LLM command, and all presets.
|
|
86
107
|
|
|
@@ -93,6 +114,7 @@ All keys, with defaults:
|
|
|
93
114
|
"presets": {}, // your own presets; same names shadow built-ins
|
|
94
115
|
"args": [], // extra argv appended to the preset's command
|
|
95
116
|
"command": null, // a whole custom invocation; when set, "preset" is ignored
|
|
117
|
+
"chatCommand": null, // what `bb ask` runs; replaces the preset's (see below)
|
|
96
118
|
"promptVia": null, // "stdin" (default) or "arg" (replace {prompt} in argv)
|
|
97
119
|
"kind": null, // "claude" (streams progress from the Claude Code CLI) or "plain"; default from the preset
|
|
98
120
|
"jsonSchema": false, // Claude kind: also pass --json-schema so the CLI validates the answer (see below)
|
|
@@ -108,8 +130,7 @@ All keys, with defaults:
|
|
|
108
130
|
},
|
|
109
131
|
"render": {
|
|
110
132
|
"color": "auto", "pager": "auto", "maxExcerptLines": 60, "width": null,
|
|
111
|
-
"highlight": "auto"
|
|
112
|
-
"theme": "dark" // background tint palette for added/removed lines; "light" for light terminals
|
|
133
|
+
"highlight": "auto" // syntax colors in excerpts; "always" | "never"
|
|
113
134
|
},
|
|
114
135
|
"cache": { "enabled": true, "dir": null } // default $XDG_CACHE_HOME/baby-bird
|
|
115
136
|
}
|
|
@@ -134,6 +155,8 @@ The model is just a command: the prompt goes in on stdin, the answer comes out o
|
|
|
134
155
|
| `claude-haiku` | the above plus `--model haiku` |
|
|
135
156
|
| `llm` | `llm` ([Simon Willison's CLI](https://llm.datasette.io/)) |
|
|
136
157
|
|
|
158
|
+
Each preset also has a `chatCommand`, which is what `bb ask` runs. For the Claude presets it is `claude [model and effort flags] --append-system-prompt-file {context-file} -- {message}`: a regular interactive session (your settings and CLAUDE.md apply) with the tour appended to the system prompt and the question as the first message. The `llm` preset uses `llm chat -s {context}`; `llm chat` cannot take an opening message, so `bb` says so and you type the question into the chat. In a chat command, `{context-file}` is replaced by the path of a temporary file holding the context, `{context}` by the text itself (mind your OS's argument size limits with big diffs), and `{message}` by the opening message; an argv entry that is only `{message}` is dropped when there is none.
|
|
159
|
+
|
|
137
160
|
Add flags to a preset, define your own, or replace the command entirely:
|
|
138
161
|
|
|
139
162
|
```jsonc
|
|
@@ -143,12 +166,18 @@ Add flags to a preset, define your own, or replace the command entirely:
|
|
|
143
166
|
"args": ["--fallback-model", "haiku"],
|
|
144
167
|
"presets": {
|
|
145
168
|
"local": { "command": ["ollama", "run", "qwen2.5-coder:14b"] },
|
|
146
|
-
"gemini": {
|
|
169
|
+
"gemini": {
|
|
170
|
+
"command": ["gemini", "-p", "{prompt}"],
|
|
171
|
+
"promptVia": "arg",
|
|
172
|
+
"chatCommand": ["gemini", "-i", "{message}"]
|
|
173
|
+
}
|
|
147
174
|
}
|
|
148
175
|
}
|
|
149
176
|
}
|
|
150
177
|
```
|
|
151
178
|
|
|
179
|
+
`llm.args` only applies to the tour command. A custom `llm.command` has no chat command of its own; set `llm.chatCommand` next to it if you want `bb ask` to work.
|
|
180
|
+
|
|
152
181
|
```sh
|
|
153
182
|
bb --preset local
|
|
154
183
|
BB_LLM_COMMAND='llm -m gpt-4.1' bb
|
|
@@ -162,7 +191,7 @@ The effective command is part of the cache key, so switching presets regenerates
|
|
|
162
191
|
|
|
163
192
|
### Excerpt highlighting
|
|
164
193
|
|
|
165
|
-
|
|
194
|
+
Excerpts get language-aware token colors (keywords, strings, comments) chosen by file extension, and added/removed lines are always marked by a green/red `+`/`-` sign — no row background and no bold (some terminal themes render bold color via a washed-out "bright" palette slot), so it reads well on any terminal theme. The token palette never uses green or red, so the sign stays unambiguous. Turn syntax coloring off with `--no-highlight` or `render.highlight: "never"`; files with no known language keep the same green/red sign with plain text.
|
|
166
195
|
|
|
167
196
|
### Code host
|
|
168
197
|
|
|
@@ -190,7 +219,7 @@ bb cache path
|
|
|
190
219
|
|
|
191
220
|
## Using it as a library
|
|
192
221
|
|
|
193
|
-
The CLI is one consumer of a small core. `import { generateTour, CliRenderer, type Tour } from '@cam5/baby-bird'` gives you the same pipeline for a TUI, a web view, or a bot; a `Tour` is plain JSON (sections, stats, excerpts) with no git or LLM dependencies.
|
|
222
|
+
The CLI is one consumer of a small core. `import { generateTour, CliRenderer, type Tour } from '@cam5/baby-bird'` gives you the same pipeline for a TUI, a web view, or a bot; a `Tour` is plain JSON (sections, stats, excerpts) with no git or LLM dependencies. `buildChatContext` produces the text `bb ask` hands to a chat, and `launchChat` runs a chat command with it.
|
|
194
223
|
|
|
195
224
|
## Development
|
|
196
225
|
|
|
@@ -221,6 +250,8 @@ git push --follow-tags
|
|
|
221
250
|
| 4 | the LLM command failed to run |
|
|
222
251
|
| 5 | the model's output was unusable even after a repair attempt |
|
|
223
252
|
|
|
253
|
+
`bb ask` exits with the chat command's own exit code once the chat ends.
|
|
254
|
+
|
|
224
255
|
## License
|
|
225
256
|
|
|
226
257
|
MIT
|
|
@@ -136,34 +136,43 @@ import { homedir } from "os";
|
|
|
136
136
|
import { join } from "path";
|
|
137
137
|
import { z as z2 } from "zod";
|
|
138
138
|
var CLAUDE_BASE = ["claude", "-p", "--no-session-persistence", "--setting-sources", "", "--tools", ""];
|
|
139
|
+
var CLAUDE_CHAT = ["claude", "--append-system-prompt-file", "{context-file}", "--", "{message}"];
|
|
140
|
+
var claudeChat = (...flags) => [CLAUDE_CHAT[0], ...flags, ...CLAUDE_CHAT.slice(1)];
|
|
139
141
|
var BUILTIN_PRESETS = Object.freeze({
|
|
140
142
|
claude: {
|
|
141
143
|
command: [...CLAUDE_BASE],
|
|
144
|
+
chatCommand: claudeChat(),
|
|
142
145
|
kind: "claude",
|
|
143
146
|
description: "Claude Code CLI with its default model"
|
|
144
147
|
},
|
|
145
148
|
"claude-sonnet": {
|
|
146
149
|
command: [...CLAUDE_BASE, "--model", "sonnet", "--effort", "high"],
|
|
150
|
+
chatCommand: claudeChat("--model", "sonnet", "--effort", "high"),
|
|
147
151
|
kind: "claude",
|
|
148
152
|
description: "Claude Code CLI, Sonnet at high effort"
|
|
149
153
|
},
|
|
150
154
|
"claude-opus": {
|
|
151
155
|
command: [...CLAUDE_BASE, "--model", "opus", "--effort", "high"],
|
|
156
|
+
chatCommand: claudeChat("--model", "opus", "--effort", "high"),
|
|
152
157
|
kind: "claude",
|
|
153
158
|
description: "Claude Code CLI, Opus at high effort"
|
|
154
159
|
},
|
|
155
160
|
"claude-fable": {
|
|
156
161
|
command: [...CLAUDE_BASE, "--model", "fable", "--effort", "high"],
|
|
162
|
+
chatCommand: claudeChat("--model", "fable", "--effort", "high"),
|
|
157
163
|
kind: "claude",
|
|
158
164
|
description: "Claude Code CLI, Fable at high effort"
|
|
159
165
|
},
|
|
160
166
|
"claude-haiku": {
|
|
161
167
|
command: [...CLAUDE_BASE, "--model", "haiku"],
|
|
168
|
+
chatCommand: claudeChat("--model", "haiku"),
|
|
162
169
|
kind: "claude",
|
|
163
170
|
description: "Claude Code CLI, Haiku (fast and cheap)"
|
|
164
171
|
},
|
|
165
172
|
llm: {
|
|
166
173
|
command: ["llm"],
|
|
174
|
+
// `llm chat` has no opening-message argument; bb warns and the question is typed into the chat.
|
|
175
|
+
chatCommand: ["llm", "chat", "-s", "{context}"],
|
|
167
176
|
description: "Simon Willison's llm CLI with its default model"
|
|
168
177
|
}
|
|
169
178
|
});
|
|
@@ -171,6 +180,7 @@ var PromptViaSchema = z2.enum(["stdin", "arg"]);
|
|
|
171
180
|
var LlmKindSchema = z2.enum(["plain", "claude"]);
|
|
172
181
|
var LlmPresetSchema = z2.object({
|
|
173
182
|
command: z2.array(z2.string()).min(1),
|
|
183
|
+
chatCommand: z2.array(z2.string()).min(1).optional(),
|
|
174
184
|
promptVia: PromptViaSchema.optional(),
|
|
175
185
|
kind: LlmKindSchema.optional(),
|
|
176
186
|
description: z2.string().optional()
|
|
@@ -181,6 +191,8 @@ var ConfigSchema = z2.object({
|
|
|
181
191
|
presets: z2.record(z2.string(), LlmPresetSchema),
|
|
182
192
|
args: z2.array(z2.string()),
|
|
183
193
|
command: z2.array(z2.string()).min(1).nullable(),
|
|
194
|
+
/** Interactive command for `bb ask`; when set it replaces the preset's chatCommand. Same tokens as a preset's. */
|
|
195
|
+
chatCommand: z2.array(z2.string()).min(1).nullable(),
|
|
184
196
|
promptVia: PromptViaSchema.nullable(),
|
|
185
197
|
kind: LlmKindSchema.nullable(),
|
|
186
198
|
/** Claude kind only: pass --json-schema so the CLI validates the answer itself. Off by default (see README). */
|
|
@@ -201,10 +213,8 @@ var ConfigSchema = z2.object({
|
|
|
201
213
|
pager: z2.enum(["auto", "always", "never"]),
|
|
202
214
|
maxExcerptLines: z2.number().int().positive(),
|
|
203
215
|
width: z2.number().int().positive().nullable(),
|
|
204
|
-
/** Language-aware token colors in excerpts
|
|
205
|
-
highlight: z2.enum(["auto", "always", "never"])
|
|
206
|
-
/** Background tint palette for added/removed lines. */
|
|
207
|
-
theme: z2.enum(["dark", "light"])
|
|
216
|
+
/** Language-aware token colors in excerpts. */
|
|
217
|
+
highlight: z2.enum(["auto", "always", "never"])
|
|
208
218
|
}),
|
|
209
219
|
cache: z2.object({
|
|
210
220
|
enabled: z2.boolean(),
|
|
@@ -224,6 +234,7 @@ var DEFAULT_CONFIG = {
|
|
|
224
234
|
presets: {},
|
|
225
235
|
args: [],
|
|
226
236
|
command: null,
|
|
237
|
+
chatCommand: null,
|
|
227
238
|
promptVia: null,
|
|
228
239
|
kind: null,
|
|
229
240
|
jsonSchema: false,
|
|
@@ -245,7 +256,7 @@ var DEFAULT_CONFIG = {
|
|
|
245
256
|
"**/*.map"
|
|
246
257
|
]
|
|
247
258
|
},
|
|
248
|
-
render: { color: "auto", pager: "auto", maxExcerptLines: 60, width: null, highlight: "auto"
|
|
259
|
+
render: { color: "auto", pager: "auto", maxExcerptLines: 60, width: null, highlight: "auto" },
|
|
249
260
|
cache: { enabled: true, dir: null }
|
|
250
261
|
};
|
|
251
262
|
function userConfigPath(env = process.env) {
|
|
@@ -377,26 +388,44 @@ function resolveLlm(config) {
|
|
|
377
388
|
...common2
|
|
378
389
|
};
|
|
379
390
|
}
|
|
391
|
+
function resolveChat(config) {
|
|
392
|
+
const { llm } = config;
|
|
393
|
+
if (llm.chatCommand) return { command: [...llm.chatCommand], preset: null, env: llm.env };
|
|
394
|
+
if (llm.command) {
|
|
395
|
+
throw new ConfigError("llm.command is set but llm.chatCommand is not, so there is nothing to chat with.", "Set llm.chatCommand (see README), or pick a preset with --preset.");
|
|
396
|
+
}
|
|
397
|
+
const presets = allPresets(config);
|
|
398
|
+
const preset = presets[llm.preset];
|
|
399
|
+
if (!preset) {
|
|
400
|
+
const names = Object.keys(presets).sort().join(", ");
|
|
401
|
+
throw new ConfigError(`Unknown LLM preset "${llm.preset}"`, `Available presets: ${names}. Define your own under llm.presets, or set llm.command.`);
|
|
402
|
+
}
|
|
403
|
+
if (!preset.chatCommand) {
|
|
404
|
+
const withChat = Object.entries(presets).filter(([, p]) => p.chatCommand).map(([n]) => n).sort().join(", ");
|
|
405
|
+
throw new ConfigError(`Preset "${llm.preset}" has no chatCommand.`, `Add one under llm.presets.${llm.preset}.chatCommand, set llm.chatCommand, or use a preset that has one: ${withChat}.`);
|
|
406
|
+
}
|
|
407
|
+
return { command: [...preset.chatCommand], preset: llm.preset, env: llm.env };
|
|
408
|
+
}
|
|
380
409
|
function shellSplit(input) {
|
|
381
410
|
const out = [];
|
|
382
411
|
let cur = "";
|
|
383
412
|
let inToken = false;
|
|
384
|
-
let
|
|
413
|
+
let quote2 = null;
|
|
385
414
|
for (let i = 0; i < input.length; i++) {
|
|
386
415
|
const ch = input[i];
|
|
387
|
-
if (
|
|
388
|
-
if (ch === "'")
|
|
416
|
+
if (quote2 === "'") {
|
|
417
|
+
if (ch === "'") quote2 = null;
|
|
389
418
|
else cur += ch;
|
|
390
419
|
continue;
|
|
391
420
|
}
|
|
392
|
-
if (
|
|
393
|
-
if (ch === '"')
|
|
421
|
+
if (quote2 === '"') {
|
|
422
|
+
if (ch === '"') quote2 = null;
|
|
394
423
|
else if (ch === "\\" && i + 1 < input.length && '"\\$`'.includes(input[i + 1])) cur += input[++i];
|
|
395
424
|
else cur += ch;
|
|
396
425
|
continue;
|
|
397
426
|
}
|
|
398
427
|
if (ch === "'" || ch === '"') {
|
|
399
|
-
|
|
428
|
+
quote2 = ch;
|
|
400
429
|
inToken = true;
|
|
401
430
|
} else if (ch === "\\" && i + 1 < input.length) {
|
|
402
431
|
cur += input[++i];
|
|
@@ -412,7 +441,7 @@ function shellSplit(input) {
|
|
|
412
441
|
inToken = true;
|
|
413
442
|
}
|
|
414
443
|
}
|
|
415
|
-
if (
|
|
444
|
+
if (quote2) throw new ConfigError(`Unterminated quote in command: ${input}`);
|
|
416
445
|
if (inToken) out.push(cur);
|
|
417
446
|
return out;
|
|
418
447
|
}
|
|
@@ -926,6 +955,10 @@ var KEEP_LINES = 40;
|
|
|
926
955
|
var MAX_PR_BODY_BYTES = 12e3;
|
|
927
956
|
function buildPrompt(input) {
|
|
928
957
|
const header = promptHeader(Boolean(input.structured));
|
|
958
|
+
const change = renderChange(input, input.maxBytes - bytes(header) - 8);
|
|
959
|
+
return { prompt: [header, change.text].join("\n"), truncation: change.truncation };
|
|
960
|
+
}
|
|
961
|
+
function renderChange(input, maxBytes) {
|
|
929
962
|
const context = renderContext(input);
|
|
930
963
|
const truncation = { truncated: [], omitted: [] };
|
|
931
964
|
const blocks = input.diff.files.map((file) => {
|
|
@@ -942,7 +975,7 @@ function buildPrompt(input) {
|
|
|
942
975
|
});
|
|
943
976
|
const text = (b) => b.mode === "full" ? b.full : b.mode === "truncated" ? b.truncated : b.omitted;
|
|
944
977
|
const total = () => bytes(diffPreamble(truncation)) + blocks.reduce((n, b) => n + bytes(text(b)), 0);
|
|
945
|
-
const budget =
|
|
978
|
+
const budget = maxBytes - bytes(context);
|
|
946
979
|
if (total() > budget) {
|
|
947
980
|
const bySize = [...blocks].sort((a, b) => bytes(b.full) - bytes(a.full));
|
|
948
981
|
for (const block of bySize) {
|
|
@@ -959,8 +992,7 @@ function buildPrompt(input) {
|
|
|
959
992
|
truncation.omitted.push(block.file.path);
|
|
960
993
|
}
|
|
961
994
|
}
|
|
962
|
-
|
|
963
|
-
return { prompt, truncation };
|
|
995
|
+
return { text: [context, diffPreamble(truncation), ...blocks.map(text)].join("\n"), truncation };
|
|
964
996
|
}
|
|
965
997
|
function bytes(s) {
|
|
966
998
|
return Buffer.byteLength(s, "utf8");
|
|
@@ -1785,6 +1817,128 @@ function parseOutput(raw) {
|
|
|
1785
1817
|
return { ok: true, value: parsed.data };
|
|
1786
1818
|
}
|
|
1787
1819
|
|
|
1820
|
+
// src/core/chat.ts
|
|
1821
|
+
var FRAMING = `# About this conversation
|
|
1822
|
+
|
|
1823
|
+
You are chatting with a developer about one specific change in the git repository you are running in. \`bb\` (the baby-bird CLI) generated the guided code tour below for that change; the developer has it open next to this chat, so treat it as shared context. Answer questions about the change using the tour, the diff after it, and the repository's files whenever you can read them. Refer to tour sections by number and title, and to code by file path and line number. If the diff below was shortened to fit, say so instead of guessing at what was left out.`;
|
|
1824
|
+
function buildChatContext(input) {
|
|
1825
|
+
const head = [FRAMING, "", ...renderTour(input.tour, input.change, input.tourPath ?? null), ""].join("\n");
|
|
1826
|
+
const change = renderChange(input.change, Math.max(0, input.maxBytes - Buffer.byteLength(head, "utf8") - 1));
|
|
1827
|
+
return { text: head + change.text, truncation: change.truncation };
|
|
1828
|
+
}
|
|
1829
|
+
function renderTour(tour, change, tourPath) {
|
|
1830
|
+
const lines = [`# The code tour: ${tour.title}`, ""];
|
|
1831
|
+
lines.push(`- Change: ${describeSource(change.source, change.branch)}`);
|
|
1832
|
+
lines.push(`- Revisions: ${describeRevisions(change.source)}`);
|
|
1833
|
+
if (tour.pullRequest) lines.push(`- Pull request #${tour.pullRequest.number}: ${tour.pullRequest.title}${tour.pullRequest.url ? ` (${tour.pullRequest.url})` : ""}`);
|
|
1834
|
+
lines.push(`- Size: ${tour.stats.files} file${tour.stats.files === 1 ? "" : "s"}, +${tour.stats.additions} -${tour.stats.deletions}, ${tour.sections.length} section${tour.sections.length === 1 ? "" : "s"}`);
|
|
1835
|
+
lines.push(`- Generated: ${tour.generatedAt} by ${tour.generator.preset ?? tour.generator.command[0] ?? "unknown"}`);
|
|
1836
|
+
if (tourPath) lines.push(`- Tour JSON: ${tourPath}`);
|
|
1837
|
+
if (tour.summary.trim()) lines.push("", tour.summary.trim());
|
|
1838
|
+
tour.sections.forEach((s, i) => lines.push("", ...renderSection(s, i + 1)));
|
|
1839
|
+
return lines;
|
|
1840
|
+
}
|
|
1841
|
+
function describeRevisions(source) {
|
|
1842
|
+
const head = source.headSha.slice(0, 12);
|
|
1843
|
+
if (source.kind === "working") return `HEAD is ${head}`;
|
|
1844
|
+
const base = (source.mergeBase ?? source.baseSha).slice(0, 12);
|
|
1845
|
+
return `base ${base}, head ${head}; \`git diff ${base} ${head}\` reproduces the diff`;
|
|
1846
|
+
}
|
|
1847
|
+
function renderSection(s, n) {
|
|
1848
|
+
const lines = [`## ${n}. ${s.title}`, "", `${s.stats.files} file${s.stats.files === 1 ? "" : "s"}, +${s.stats.additions} -${s.stats.deletions}`];
|
|
1849
|
+
if (s.description.trim()) lines.push("", s.description.trim());
|
|
1850
|
+
if (s.files.length) lines.push("", "Files:", ...s.files.map((f) => `- ${f}`));
|
|
1851
|
+
for (const e of s.excerpts) lines.push("", ...renderExcerpt(e));
|
|
1852
|
+
return lines;
|
|
1853
|
+
}
|
|
1854
|
+
function renderExcerpt(e) {
|
|
1855
|
+
const note = e.note ? ` \u2014 ${e.note}` : "";
|
|
1856
|
+
const lines = [`Excerpt \`${e.file}:${e.newStart}\` (hunk ${e.hunkId})${note}`, "", "```"];
|
|
1857
|
+
const maxNo = Math.max(...e.lines.map((l) => Math.max(l.oldNo ?? 0, l.newNo ?? 0)), 1);
|
|
1858
|
+
const w = String(maxNo).length;
|
|
1859
|
+
for (const l of e.lines) {
|
|
1860
|
+
const oldNo = l.oldNo === void 0 ? " ".repeat(w) : String(l.oldNo).padStart(w);
|
|
1861
|
+
const newNo = l.newNo === void 0 ? " ".repeat(w) : String(l.newNo).padStart(w);
|
|
1862
|
+
const sign = l.type === "add" ? "+" : l.type === "del" ? "-" : " ";
|
|
1863
|
+
lines.push(`${oldNo} ${newNo} \u2502${sign}${l.text}`);
|
|
1864
|
+
}
|
|
1865
|
+
lines.push("```");
|
|
1866
|
+
return lines;
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
// src/llm/chat.ts
|
|
1870
|
+
import { spawn as spawn2 } from "child_process";
|
|
1871
|
+
import { mkdtemp, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
1872
|
+
import { constants as osConstants, tmpdir } from "os";
|
|
1873
|
+
import { join as join4 } from "path";
|
|
1874
|
+
var CONTEXT_FILE_TOKEN = "{context-file}";
|
|
1875
|
+
var CONTEXT_TOKEN = "{context}";
|
|
1876
|
+
var MESSAGE_TOKEN = "{message}";
|
|
1877
|
+
function chatArgv(input) {
|
|
1878
|
+
const message = input.message?.trim() ?? "";
|
|
1879
|
+
const out = [];
|
|
1880
|
+
for (const arg of input.command) {
|
|
1881
|
+
if (arg === MESSAGE_TOKEN && !message) continue;
|
|
1882
|
+
out.push(arg.replaceAll(CONTEXT_FILE_TOKEN, input.contextFile).replaceAll(CONTEXT_TOKEN, input.context).replaceAll(MESSAGE_TOKEN, message));
|
|
1883
|
+
}
|
|
1884
|
+
return out;
|
|
1885
|
+
}
|
|
1886
|
+
function chatSlots(command) {
|
|
1887
|
+
const joined = command.join("\0");
|
|
1888
|
+
return { context: joined.includes(CONTEXT_FILE_TOKEN) || joined.includes(CONTEXT_TOKEN), message: joined.includes(MESSAGE_TOKEN) };
|
|
1889
|
+
}
|
|
1890
|
+
async function launchChat(opts) {
|
|
1891
|
+
const debug = opts.debug ?? (() => {
|
|
1892
|
+
});
|
|
1893
|
+
const dir = await mkdtemp(join4(tmpdir(), "bb-chat-"));
|
|
1894
|
+
const contextFile = join4(dir, "context.md");
|
|
1895
|
+
await writeFile2(contextFile, opts.context, { encoding: "utf8", mode: 384 });
|
|
1896
|
+
const [bin, ...args] = chatArgv({ command: opts.command, contextFile, context: opts.context, message: opts.message });
|
|
1897
|
+
debug(`launching ${describeArgv([bin, ...args], opts.context)} (context ${Buffer.byteLength(opts.context)} bytes in ${contextFile})`);
|
|
1898
|
+
try {
|
|
1899
|
+
return await new Promise((resolve, reject) => {
|
|
1900
|
+
const env = { ...process.env, ...opts.env };
|
|
1901
|
+
delete env.CLAUDECODE;
|
|
1902
|
+
const child = spawn2(bin, args, { cwd: opts.cwd, env, stdio: opts.stdio ?? "inherit" });
|
|
1903
|
+
const onInt = () => {
|
|
1904
|
+
};
|
|
1905
|
+
const onTerm = () => child.kill("SIGTERM");
|
|
1906
|
+
const onHup = () => child.kill("SIGHUP");
|
|
1907
|
+
process.on("SIGINT", onInt);
|
|
1908
|
+
process.on("SIGTERM", onTerm);
|
|
1909
|
+
process.on("SIGHUP", onHup);
|
|
1910
|
+
const cleanup = () => {
|
|
1911
|
+
process.off("SIGINT", onInt);
|
|
1912
|
+
process.off("SIGTERM", onTerm);
|
|
1913
|
+
process.off("SIGHUP", onHup);
|
|
1914
|
+
};
|
|
1915
|
+
child.on("error", (e) => {
|
|
1916
|
+
cleanup();
|
|
1917
|
+
if (e.code === "ENOENT") {
|
|
1918
|
+
reject(new LlmFailedError(`Chat command not found: ${bin}`, { hint: "Install it, or choose another preset with --preset / llm.preset.", cause: e }));
|
|
1919
|
+
} else {
|
|
1920
|
+
reject(new LlmFailedError(`Could not run ${bin}: ${e.message}`, { cause: e }));
|
|
1921
|
+
}
|
|
1922
|
+
});
|
|
1923
|
+
child.on("close", (code, signal) => {
|
|
1924
|
+
cleanup();
|
|
1925
|
+
debug(`chat exited with ${signal ? `signal ${signal}` : `code ${code}`}`);
|
|
1926
|
+
const exitCode = code ?? (signal ? 128 + (osConstants.signals[signal] ?? 0) : 1);
|
|
1927
|
+
resolve({ exitCode, signal });
|
|
1928
|
+
});
|
|
1929
|
+
});
|
|
1930
|
+
} finally {
|
|
1931
|
+
await rm2(dir, { recursive: true, force: true });
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
function describeArgv(argv, context) {
|
|
1935
|
+
return argv.map((a) => context && a.includes(context) ? a.replace(context, "<context>") : a).map(quote).join(" ");
|
|
1936
|
+
}
|
|
1937
|
+
function quote(arg) {
|
|
1938
|
+
if (arg === "") return '""';
|
|
1939
|
+
return /^[\w@%+=:,./{}-]+$/.test(arg) ? arg : `'${arg.replaceAll("'", `'\\''`)}'`;
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1788
1942
|
// src/render/cli.ts
|
|
1789
1943
|
import pc from "picocolors";
|
|
1790
1944
|
|
|
@@ -1986,14 +2140,6 @@ function truncateAnsi(s, max) {
|
|
|
1986
2140
|
}
|
|
1987
2141
|
return out + SOFT_RESET + "\u2026";
|
|
1988
2142
|
}
|
|
1989
|
-
function lineTint(type, theme) {
|
|
1990
|
-
const code = theme === "dark" ? type === "add" ? 22 : 52 : type === "add" ? 194 : 224;
|
|
1991
|
-
return { open: `\x1B[48;5;${code}m`, close: "\x1B[49m" };
|
|
1992
|
-
}
|
|
1993
|
-
function supports256Colors(env = process.env) {
|
|
1994
|
-
if (env.TERM === "dumb") return false;
|
|
1995
|
-
return Boolean(env.COLORTERM) || /256color|truecolor|direct/i.test(env.TERM ?? "") || Boolean(env.TERM_PROGRAM) || Boolean(env.WT_SESSION);
|
|
1996
|
-
}
|
|
1997
2143
|
|
|
1998
2144
|
// src/render/cli.ts
|
|
1999
2145
|
var MIN_WIDTH = 40;
|
|
@@ -2002,7 +2148,7 @@ var CliRenderer = class {
|
|
|
2002
2148
|
render(tour, opts) {
|
|
2003
2149
|
const c = pc.createColors(opts.color);
|
|
2004
2150
|
const width = Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, opts.width));
|
|
2005
|
-
const style = { highlight: Boolean(opts.color && opts.highlight)
|
|
2151
|
+
const style = { highlight: Boolean(opts.color && opts.highlight) };
|
|
2006
2152
|
const out = [];
|
|
2007
2153
|
out.push(...renderHeader(tour, c, width, opts.fromCache ?? false));
|
|
2008
2154
|
out.push("");
|
|
@@ -2011,12 +2157,12 @@ var CliRenderer = class {
|
|
|
2011
2157
|
if (!section) {
|
|
2012
2158
|
throw new UsageError(`No section ${opts.section}; this tour has ${tour.sections.length} section${tour.sections.length === 1 ? "" : "s"}.`);
|
|
2013
2159
|
}
|
|
2014
|
-
out.push(...
|
|
2160
|
+
out.push(...renderSection2(section, opts.section, tour.sections.length, c, width, style));
|
|
2015
2161
|
} else {
|
|
2016
2162
|
out.push(...renderToc(tour, c, width));
|
|
2017
2163
|
out.push("");
|
|
2018
2164
|
tour.sections.forEach((s, i) => {
|
|
2019
|
-
out.push(...
|
|
2165
|
+
out.push(...renderSection2(s, i + 1, tour.sections.length, c, width, style));
|
|
2020
2166
|
out.push("");
|
|
2021
2167
|
});
|
|
2022
2168
|
}
|
|
@@ -2055,7 +2201,7 @@ function renderToc(tour, c, width) {
|
|
|
2055
2201
|
}
|
|
2056
2202
|
return lines;
|
|
2057
2203
|
}
|
|
2058
|
-
function
|
|
2204
|
+
function renderSection2(s, index, count, c, width, style) {
|
|
2059
2205
|
const lines = [];
|
|
2060
2206
|
lines.push(c.dim("\u2500".repeat(width)));
|
|
2061
2207
|
lines.push(c.bold(`${index}. ${s.title}`) + c.dim(` (${index}/${count})`));
|
|
@@ -2070,11 +2216,11 @@ function renderSection(s, index, count, c, width, style) {
|
|
|
2070
2216
|
}
|
|
2071
2217
|
for (const e of s.excerpts) {
|
|
2072
2218
|
lines.push("");
|
|
2073
|
-
lines.push(...
|
|
2219
|
+
lines.push(...renderExcerpt2(e, c, width, style));
|
|
2074
2220
|
}
|
|
2075
2221
|
return lines;
|
|
2076
2222
|
}
|
|
2077
|
-
function
|
|
2223
|
+
function renderExcerpt2(e, c, width, style) {
|
|
2078
2224
|
const lines = [];
|
|
2079
2225
|
const title = ` ${c.cyan(e.file)}${c.dim(":" + e.newStart)}`;
|
|
2080
2226
|
lines.push(e.note ? `${title} ${c.italic(c.dim(e.note))}` : title);
|
|
@@ -2091,19 +2237,17 @@ function renderExcerpt(e, c, width, style) {
|
|
|
2091
2237
|
const gutter = c.dim(` ${oldNo} ${newNo} \u2502`);
|
|
2092
2238
|
const plain = texts[i];
|
|
2093
2239
|
if (!highlighted) {
|
|
2094
|
-
const
|
|
2095
|
-
lines.push(`${gutter}${
|
|
2240
|
+
const signStyled2 = l.type === "add" ? c.green(sign) : l.type === "del" ? c.red(sign) : sign;
|
|
2241
|
+
lines.push(`${gutter}${signStyled2}${truncate(plain, budget)}`);
|
|
2096
2242
|
return;
|
|
2097
2243
|
}
|
|
2098
2244
|
const text = truncateAnsi(highlighted[i], budget);
|
|
2099
|
-
const pad = " ".repeat(Math.max(0, budget - visibleLength(text)));
|
|
2100
2245
|
if (l.type === "ctx") {
|
|
2101
2246
|
lines.push(`${gutter} ${text}`);
|
|
2102
2247
|
return;
|
|
2103
2248
|
}
|
|
2104
|
-
const
|
|
2105
|
-
|
|
2106
|
-
lines.push(`${gutter}${tint.open}${signStyled}${text}${pad}${tint.close}`);
|
|
2249
|
+
const signStyled = l.type === "add" ? c.green("+") : c.red("-");
|
|
2250
|
+
lines.push(`${gutter}${signStyled}${text}`);
|
|
2107
2251
|
});
|
|
2108
2252
|
return lines;
|
|
2109
2253
|
}
|
|
@@ -2161,7 +2305,7 @@ function relativeTime(iso, now = /* @__PURE__ */ new Date()) {
|
|
|
2161
2305
|
}
|
|
2162
2306
|
|
|
2163
2307
|
// src/render/pager.ts
|
|
2164
|
-
import { spawn as
|
|
2308
|
+
import { spawn as spawn3 } from "child_process";
|
|
2165
2309
|
async function writeMaybePaged(output, opts) {
|
|
2166
2310
|
const env = opts.env ?? process.env;
|
|
2167
2311
|
const lineCount = output.split("\n").length;
|
|
@@ -2173,7 +2317,7 @@ async function writeMaybePaged(output, opts) {
|
|
|
2173
2317
|
const pagerCmd = env.PAGER && env.PAGER.trim() || "less";
|
|
2174
2318
|
const cmd = pagerCmd === "less" && !env.LESS ? "less -RFX" : pagerCmd;
|
|
2175
2319
|
await new Promise((resolve) => {
|
|
2176
|
-
const child =
|
|
2320
|
+
const child = spawn3(cmd, { shell: true, stdio: ["pipe", "inherit", "inherit"], env });
|
|
2177
2321
|
let fellBack = false;
|
|
2178
2322
|
child.on("error", () => {
|
|
2179
2323
|
fellBack = true;
|
|
@@ -2217,6 +2361,7 @@ export {
|
|
|
2217
2361
|
loadConfig,
|
|
2218
2362
|
allPresets,
|
|
2219
2363
|
resolveLlm,
|
|
2364
|
+
resolveChat,
|
|
2220
2365
|
shellSplit,
|
|
2221
2366
|
TourCache,
|
|
2222
2367
|
extractJson,
|
|
@@ -2228,10 +2373,12 @@ export {
|
|
|
2228
2373
|
PROMPT_VERSION,
|
|
2229
2374
|
promptHeader,
|
|
2230
2375
|
buildPrompt,
|
|
2376
|
+
renderChange,
|
|
2231
2377
|
describeSource,
|
|
2232
2378
|
GhCodeHost,
|
|
2233
2379
|
NoCodeHost,
|
|
2234
2380
|
createCodeHost,
|
|
2381
|
+
git,
|
|
2235
2382
|
gitRoot,
|
|
2236
2383
|
revParse,
|
|
2237
2384
|
currentBranch,
|
|
@@ -2246,7 +2393,10 @@ export {
|
|
|
2246
2393
|
noProgress,
|
|
2247
2394
|
prepareTour,
|
|
2248
2395
|
generateTour,
|
|
2249
|
-
|
|
2396
|
+
buildChatContext,
|
|
2397
|
+
chatArgv,
|
|
2398
|
+
chatSlots,
|
|
2399
|
+
launchChat,
|
|
2250
2400
|
CliRenderer,
|
|
2251
2401
|
describeSource2,
|
|
2252
2402
|
wrap,
|
|
@@ -2254,4 +2404,4 @@ export {
|
|
|
2254
2404
|
writeMaybePaged,
|
|
2255
2405
|
writeStdout
|
|
2256
2406
|
};
|
|
2257
|
-
//# sourceMappingURL=chunk-
|
|
2407
|
+
//# sourceMappingURL=chunk-YI2UL4IC.js.map
|