@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/README.md +56 -4
- package/dist/{chunk-RWSCST2I.js → chunk-YI2UL4IC.js} +706 -52
- package/dist/chunk-YI2UL4IC.js.map +1 -0
- package/dist/cli.js +380 -23
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +317 -6
- package/dist/index.js +21 -1
- package/package.json +2 -1
- package/dist/chunk-RWSCST2I.js.map +0 -1
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
var LlmExcerptRefSchema = z.object({
|
|
4
4
|
hunk: z.string().min(1),
|
|
5
|
-
/** Optional [start, end]
|
|
6
|
-
lines: z.
|
|
5
|
+
/** Optional [start, end] new-file line numbers to narrow the hunk; fewer than two numbers means the whole hunk. */
|
|
6
|
+
lines: z.array(z.number().int().nonnegative()).optional(),
|
|
7
7
|
note: z.string().optional()
|
|
8
8
|
});
|
|
9
9
|
var LlmSectionSchema = z.object({
|
|
@@ -136,36 +136,53 @@ 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(),
|
|
145
|
+
kind: "claude",
|
|
142
146
|
description: "Claude Code CLI with its default model"
|
|
143
147
|
},
|
|
144
148
|
"claude-sonnet": {
|
|
145
149
|
command: [...CLAUDE_BASE, "--model", "sonnet", "--effort", "high"],
|
|
150
|
+
chatCommand: claudeChat("--model", "sonnet", "--effort", "high"),
|
|
151
|
+
kind: "claude",
|
|
146
152
|
description: "Claude Code CLI, Sonnet at high effort"
|
|
147
153
|
},
|
|
148
154
|
"claude-opus": {
|
|
149
155
|
command: [...CLAUDE_BASE, "--model", "opus", "--effort", "high"],
|
|
156
|
+
chatCommand: claudeChat("--model", "opus", "--effort", "high"),
|
|
157
|
+
kind: "claude",
|
|
150
158
|
description: "Claude Code CLI, Opus at high effort"
|
|
151
159
|
},
|
|
152
160
|
"claude-fable": {
|
|
153
161
|
command: [...CLAUDE_BASE, "--model", "fable", "--effort", "high"],
|
|
162
|
+
chatCommand: claudeChat("--model", "fable", "--effort", "high"),
|
|
163
|
+
kind: "claude",
|
|
154
164
|
description: "Claude Code CLI, Fable at high effort"
|
|
155
165
|
},
|
|
156
166
|
"claude-haiku": {
|
|
157
167
|
command: [...CLAUDE_BASE, "--model", "haiku"],
|
|
168
|
+
chatCommand: claudeChat("--model", "haiku"),
|
|
169
|
+
kind: "claude",
|
|
158
170
|
description: "Claude Code CLI, Haiku (fast and cheap)"
|
|
159
171
|
},
|
|
160
172
|
llm: {
|
|
161
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}"],
|
|
162
176
|
description: "Simon Willison's llm CLI with its default model"
|
|
163
177
|
}
|
|
164
178
|
});
|
|
165
179
|
var PromptViaSchema = z2.enum(["stdin", "arg"]);
|
|
180
|
+
var LlmKindSchema = z2.enum(["plain", "claude"]);
|
|
166
181
|
var LlmPresetSchema = z2.object({
|
|
167
182
|
command: z2.array(z2.string()).min(1),
|
|
183
|
+
chatCommand: z2.array(z2.string()).min(1).optional(),
|
|
168
184
|
promptVia: PromptViaSchema.optional(),
|
|
185
|
+
kind: LlmKindSchema.optional(),
|
|
169
186
|
description: z2.string().optional()
|
|
170
187
|
});
|
|
171
188
|
var ConfigSchema = z2.object({
|
|
@@ -174,7 +191,12 @@ var ConfigSchema = z2.object({
|
|
|
174
191
|
presets: z2.record(z2.string(), LlmPresetSchema),
|
|
175
192
|
args: z2.array(z2.string()),
|
|
176
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(),
|
|
177
196
|
promptVia: PromptViaSchema.nullable(),
|
|
197
|
+
kind: LlmKindSchema.nullable(),
|
|
198
|
+
/** Claude kind only: pass --json-schema so the CLI validates the answer itself. Off by default (see README). */
|
|
199
|
+
jsonSchema: z2.boolean(),
|
|
178
200
|
timeoutMs: z2.number().int().positive(),
|
|
179
201
|
maxPromptBytes: z2.number().int().positive(),
|
|
180
202
|
env: z2.record(z2.string(), z2.string())
|
|
@@ -190,7 +212,9 @@ var ConfigSchema = z2.object({
|
|
|
190
212
|
color: z2.enum(["auto", "always", "never"]),
|
|
191
213
|
pager: z2.enum(["auto", "always", "never"]),
|
|
192
214
|
maxExcerptLines: z2.number().int().positive(),
|
|
193
|
-
width: z2.number().int().positive().nullable()
|
|
215
|
+
width: z2.number().int().positive().nullable(),
|
|
216
|
+
/** Language-aware token colors in excerpts. */
|
|
217
|
+
highlight: z2.enum(["auto", "always", "never"])
|
|
194
218
|
}),
|
|
195
219
|
cache: z2.object({
|
|
196
220
|
enabled: z2.boolean(),
|
|
@@ -210,7 +234,10 @@ var DEFAULT_CONFIG = {
|
|
|
210
234
|
presets: {},
|
|
211
235
|
args: [],
|
|
212
236
|
command: null,
|
|
237
|
+
chatCommand: null,
|
|
213
238
|
promptVia: null,
|
|
239
|
+
kind: null,
|
|
240
|
+
jsonSchema: false,
|
|
214
241
|
timeoutMs: 18e4,
|
|
215
242
|
maxPromptBytes: 2e5,
|
|
216
243
|
env: {}
|
|
@@ -229,7 +256,7 @@ var DEFAULT_CONFIG = {
|
|
|
229
256
|
"**/*.map"
|
|
230
257
|
]
|
|
231
258
|
},
|
|
232
|
-
render: { color: "auto", pager: "auto", maxExcerptLines: 60, width: null },
|
|
259
|
+
render: { color: "auto", pager: "auto", maxExcerptLines: 60, width: null, highlight: "auto" },
|
|
233
260
|
cache: { enabled: true, dir: null }
|
|
234
261
|
};
|
|
235
262
|
function userConfigPath(env = process.env) {
|
|
@@ -343,9 +370,9 @@ function allPresets(config) {
|
|
|
343
370
|
}
|
|
344
371
|
function resolveLlm(config) {
|
|
345
372
|
const { llm } = config;
|
|
346
|
-
const
|
|
373
|
+
const common2 = { timeoutMs: llm.timeoutMs, maxPromptBytes: llm.maxPromptBytes, env: llm.env, jsonSchema: llm.jsonSchema };
|
|
347
374
|
if (llm.command) {
|
|
348
|
-
return { command: [...llm.command, ...llm.args], promptVia: llm.promptVia ?? "stdin", preset: null, ...
|
|
375
|
+
return { command: [...llm.command, ...llm.args], promptVia: llm.promptVia ?? "stdin", kind: llm.kind ?? "plain", preset: null, ...common2 };
|
|
349
376
|
}
|
|
350
377
|
const presets = allPresets(config);
|
|
351
378
|
const preset = presets[llm.preset];
|
|
@@ -356,30 +383,49 @@ function resolveLlm(config) {
|
|
|
356
383
|
return {
|
|
357
384
|
command: [...preset.command, ...llm.args],
|
|
358
385
|
promptVia: llm.promptVia ?? preset.promptVia ?? "stdin",
|
|
386
|
+
kind: llm.kind ?? preset.kind ?? "plain",
|
|
359
387
|
preset: llm.preset,
|
|
360
|
-
...
|
|
388
|
+
...common2
|
|
361
389
|
};
|
|
362
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
|
+
}
|
|
363
409
|
function shellSplit(input) {
|
|
364
410
|
const out = [];
|
|
365
411
|
let cur = "";
|
|
366
412
|
let inToken = false;
|
|
367
|
-
let
|
|
413
|
+
let quote2 = null;
|
|
368
414
|
for (let i = 0; i < input.length; i++) {
|
|
369
415
|
const ch = input[i];
|
|
370
|
-
if (
|
|
371
|
-
if (ch === "'")
|
|
416
|
+
if (quote2 === "'") {
|
|
417
|
+
if (ch === "'") quote2 = null;
|
|
372
418
|
else cur += ch;
|
|
373
419
|
continue;
|
|
374
420
|
}
|
|
375
|
-
if (
|
|
376
|
-
if (ch === '"')
|
|
421
|
+
if (quote2 === '"') {
|
|
422
|
+
if (ch === '"') quote2 = null;
|
|
377
423
|
else if (ch === "\\" && i + 1 < input.length && '"\\$`'.includes(input[i + 1])) cur += input[++i];
|
|
378
424
|
else cur += ch;
|
|
379
425
|
continue;
|
|
380
426
|
}
|
|
381
427
|
if (ch === "'" || ch === '"') {
|
|
382
|
-
|
|
428
|
+
quote2 = ch;
|
|
383
429
|
inToken = true;
|
|
384
430
|
} else if (ch === "\\" && i + 1 < input.length) {
|
|
385
431
|
cur += input[++i];
|
|
@@ -395,7 +441,7 @@ function shellSplit(input) {
|
|
|
395
441
|
inToken = true;
|
|
396
442
|
}
|
|
397
443
|
}
|
|
398
|
-
if (
|
|
444
|
+
if (quote2) throw new ConfigError(`Unterminated quote in command: ${input}`);
|
|
399
445
|
if (inToken) out.push(cur);
|
|
400
446
|
return out;
|
|
401
447
|
}
|
|
@@ -767,7 +813,8 @@ function materializeTour(input) {
|
|
|
767
813
|
continue;
|
|
768
814
|
}
|
|
769
815
|
files.add(hit.file.path);
|
|
770
|
-
const
|
|
816
|
+
const range = ref.lines && ref.lines.length >= 2 ? [ref.lines[0], ref.lines[1]] : void 0;
|
|
817
|
+
const lines = sliceHunk(hit.hunk, range, input.maxExcerptLines);
|
|
771
818
|
const excerpt = {
|
|
772
819
|
file: hit.file.path,
|
|
773
820
|
hunkId: hit.hunk.id,
|
|
@@ -851,8 +898,8 @@ function sliceHunk(hunk, range, maxLines) {
|
|
|
851
898
|
}
|
|
852
899
|
|
|
853
900
|
// src/core/prompt/template.ts
|
|
854
|
-
var PROMPT_VERSION =
|
|
855
|
-
var
|
|
901
|
+
var PROMPT_VERSION = 3;
|
|
902
|
+
var PROMPT_HEADER_TEXT = `You are writing a guided code tour of a change for a reviewer who has not seen it before.
|
|
856
903
|
|
|
857
904
|
A code tour is an ordered list of sections. Each section explains one coherent part of the change: what it does, why it is there, and how it connects to the rest. Sections are ordered the way a reader should encounter them: start with the change that makes everything else make sense (a new type, an interface, a data model, a configuration knob), then the code that builds on it, then wiring and plumbing, then tests and housekeeping.
|
|
858
905
|
|
|
@@ -862,7 +909,7 @@ A code tour is an ordered list of sections. Each section explains one coherent p
|
|
|
862
909
|
- Group by concept, not by file. A section may span many files, and a file may appear in several sections.
|
|
863
910
|
- Every file in the change must be claimed by at least one section. Put unrelated housekeeping (formatting, generated code, renames, dependency bumps) in one short final section rather than sprinkling it around.
|
|
864
911
|
- A section's description is 2 to 5 sentences of plain prose written for a colleague. Lead with the purpose (why), then what changed, then anything a reviewer should look at carefully: behavior changes, edge cases, risk. Do not narrate line by line and do not restate the diff.
|
|
865
|
-
- Choose 1 to 3 excerpts per section: the hunks that best show the idea. Reference hunks by their id exactly as given (for example "F2.H1").
|
|
912
|
+
- Choose 1 to 3 excerpts per section: the hunks that best show the idea. Reference hunks by their id exactly as given (for example "F2.H1"). Narrow a hunk to the interesting part with "lines": [start, end] using NEW-file line numbers as they appear in the diff; prefer 10 to 30 lines over a whole hunk. Use an empty "lines" array for the whole hunk. Never quote code in the JSON; the real diff is rendered from your references.
|
|
866
913
|
- Give each excerpt a short "note" (under 15 words) saying what to look at.
|
|
867
914
|
- The tour "title" is a short imperative phrase naming the change, like a good commit subject. The "summary" is 2 to 4 sentences describing the whole change and its motivation.
|
|
868
915
|
- Use the pull request description and commit messages as evidence of intent, but trust the diff over them when they disagree.
|
|
@@ -871,7 +918,7 @@ A code tour is an ordered list of sections. Each section explains one coherent p
|
|
|
871
918
|
|
|
872
919
|
## Output
|
|
873
920
|
|
|
874
|
-
|
|
921
|
+
%%OUTPUT_INSTRUCTION%%
|
|
875
922
|
|
|
876
923
|
{
|
|
877
924
|
"title": "Short imperative title",
|
|
@@ -889,6 +936,12 @@ Respond with ONLY a JSON object: no prose before or after it, and no code fences
|
|
|
889
936
|
]
|
|
890
937
|
}
|
|
891
938
|
`;
|
|
939
|
+
var OUTPUT_PLAIN = "Respond with ONLY a JSON object: no prose before or after it, and no code fences.";
|
|
940
|
+
var OUTPUT_STRUCTURED = "Submit the tour through the structured output tool. Its input is the tour object itself, with title, summary and sections as top-level fields exactly as shown below (do not nest them under another key). Do not write prose before it.";
|
|
941
|
+
function promptHeader(structured) {
|
|
942
|
+
return PROMPT_HEADER_TEXT.replace("%%OUTPUT_INSTRUCTION%%", structured ? OUTPUT_STRUCTURED : OUTPUT_PLAIN);
|
|
943
|
+
}
|
|
944
|
+
var PROMPT_HEADER = promptHeader(false);
|
|
892
945
|
var REPAIR_SUFFIX = (reason) => `
|
|
893
946
|
|
|
894
947
|
---
|
|
@@ -901,6 +954,11 @@ Respond again with ONLY the JSON object described above. No prose, no code fence
|
|
|
901
954
|
var KEEP_LINES = 40;
|
|
902
955
|
var MAX_PR_BODY_BYTES = 12e3;
|
|
903
956
|
function buildPrompt(input) {
|
|
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) {
|
|
904
962
|
const context = renderContext(input);
|
|
905
963
|
const truncation = { truncated: [], omitted: [] };
|
|
906
964
|
const blocks = input.diff.files.map((file) => {
|
|
@@ -917,7 +975,7 @@ function buildPrompt(input) {
|
|
|
917
975
|
});
|
|
918
976
|
const text = (b) => b.mode === "full" ? b.full : b.mode === "truncated" ? b.truncated : b.omitted;
|
|
919
977
|
const total = () => bytes(diffPreamble(truncation)) + blocks.reduce((n, b) => n + bytes(text(b)), 0);
|
|
920
|
-
const budget =
|
|
978
|
+
const budget = maxBytes - bytes(context);
|
|
921
979
|
if (total() > budget) {
|
|
922
980
|
const bySize = [...blocks].sort((a, b) => bytes(b.full) - bytes(a.full));
|
|
923
981
|
for (const block of bySize) {
|
|
@@ -934,8 +992,7 @@ function buildPrompt(input) {
|
|
|
934
992
|
truncation.omitted.push(block.file.path);
|
|
935
993
|
}
|
|
936
994
|
}
|
|
937
|
-
|
|
938
|
-
return { prompt, truncation };
|
|
995
|
+
return { text: [context, diffPreamble(truncation), ...blocks.map(text)].join("\n"), truncation };
|
|
939
996
|
}
|
|
940
997
|
function bytes(s) {
|
|
941
998
|
return Buffer.byteLength(s, "utf8");
|
|
@@ -1326,32 +1383,237 @@ async function nearestAncestorBranch(branch, headSha, defaultBranch, cwd, debug)
|
|
|
1326
1383
|
return best?.name ?? null;
|
|
1327
1384
|
}
|
|
1328
1385
|
|
|
1386
|
+
// src/core/prompt/json-schema.ts
|
|
1387
|
+
var TOUR_JSON_SCHEMA = {
|
|
1388
|
+
type: "object",
|
|
1389
|
+
description: "The tour itself. Its fields (title, summary, sections) are the top-level properties of this object; do not wrap them in another key.",
|
|
1390
|
+
additionalProperties: false,
|
|
1391
|
+
required: ["title", "summary", "sections"],
|
|
1392
|
+
properties: {
|
|
1393
|
+
title: { type: "string", description: "Short imperative title naming the change, like a good commit subject." },
|
|
1394
|
+
summary: { type: "string", description: "2 to 4 sentences describing the whole change and its motivation." },
|
|
1395
|
+
sections: {
|
|
1396
|
+
type: "array",
|
|
1397
|
+
description: "Ordered sections of the tour, 2 to 8 of them.",
|
|
1398
|
+
items: {
|
|
1399
|
+
type: "object",
|
|
1400
|
+
additionalProperties: false,
|
|
1401
|
+
required: ["title", "description", "files", "excerpts"],
|
|
1402
|
+
properties: {
|
|
1403
|
+
title: { type: "string" },
|
|
1404
|
+
description: { type: "string", description: "Why, then what, then what to watch. 2 to 5 sentences." },
|
|
1405
|
+
files: { type: "array", items: { type: "string" }, description: "Paths this section is about, exactly as listed." },
|
|
1406
|
+
excerpts: {
|
|
1407
|
+
type: "array",
|
|
1408
|
+
description: "1 to 3 hunks that best show the idea.",
|
|
1409
|
+
items: {
|
|
1410
|
+
type: "object",
|
|
1411
|
+
additionalProperties: false,
|
|
1412
|
+
required: ["hunk", "lines", "note"],
|
|
1413
|
+
properties: {
|
|
1414
|
+
hunk: { type: "string", description: 'A hunk id exactly as given in the diff, e.g. "F2.H1".' },
|
|
1415
|
+
lines: {
|
|
1416
|
+
type: "array",
|
|
1417
|
+
items: { type: "integer" },
|
|
1418
|
+
description: "Empty for the whole hunk, or [start, end] new-file line numbers to narrow it."
|
|
1419
|
+
},
|
|
1420
|
+
note: { type: "string", description: "Under 15 words: what to look at. May be empty." }
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
};
|
|
1429
|
+
|
|
1430
|
+
// src/llm/claude-stream.ts
|
|
1431
|
+
var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1432
|
+
var ClaudeStreamParser = class {
|
|
1433
|
+
constructor(onEvent = () => {
|
|
1434
|
+
}) {
|
|
1435
|
+
this.onEvent = onEvent;
|
|
1436
|
+
}
|
|
1437
|
+
onEvent;
|
|
1438
|
+
buffer = "";
|
|
1439
|
+
text = "";
|
|
1440
|
+
partialJson = "";
|
|
1441
|
+
messageStructured = null;
|
|
1442
|
+
resultStructured = null;
|
|
1443
|
+
resultText = null;
|
|
1444
|
+
isError = false;
|
|
1445
|
+
errorMessage;
|
|
1446
|
+
usage = {};
|
|
1447
|
+
unparsed = 0;
|
|
1448
|
+
push(chunk) {
|
|
1449
|
+
this.buffer += chunk;
|
|
1450
|
+
let idx;
|
|
1451
|
+
while ((idx = this.buffer.indexOf("\n")) !== -1) {
|
|
1452
|
+
const line = this.buffer.slice(0, idx);
|
|
1453
|
+
this.buffer = this.buffer.slice(idx + 1);
|
|
1454
|
+
this.handleLine(line);
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
finish() {
|
|
1458
|
+
if (this.buffer.trim()) this.handleLine(this.buffer);
|
|
1459
|
+
this.buffer = "";
|
|
1460
|
+
const result = this.resultStructured ?? this.messageStructured ?? (this.partialJson.trim() || null) ?? (this.resultText?.trim() || null) ?? (this.text.trim() || null);
|
|
1461
|
+
const outcome = { result, isError: this.isError, usage: this.usage, unparsedLines: this.unparsed };
|
|
1462
|
+
if (this.errorMessage) outcome.errorMessage = this.errorMessage;
|
|
1463
|
+
return outcome;
|
|
1464
|
+
}
|
|
1465
|
+
handleLine(line) {
|
|
1466
|
+
const trimmed = line.trim();
|
|
1467
|
+
if (!trimmed) return;
|
|
1468
|
+
let ev;
|
|
1469
|
+
try {
|
|
1470
|
+
ev = JSON.parse(trimmed);
|
|
1471
|
+
} catch {
|
|
1472
|
+
this.unparsed++;
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1475
|
+
if (!isObject(ev)) return;
|
|
1476
|
+
switch (ev.type) {
|
|
1477
|
+
case "stream_event":
|
|
1478
|
+
if (isObject(ev.event)) this.handleStreamEvent(ev.event);
|
|
1479
|
+
break;
|
|
1480
|
+
case "assistant":
|
|
1481
|
+
if (isObject(ev.message)) this.handleAssistant(ev.message);
|
|
1482
|
+
break;
|
|
1483
|
+
case "user":
|
|
1484
|
+
if (isObject(ev.message)) this.handleUser(ev.message);
|
|
1485
|
+
break;
|
|
1486
|
+
case "result":
|
|
1487
|
+
this.handleResult(ev);
|
|
1488
|
+
break;
|
|
1489
|
+
default:
|
|
1490
|
+
break;
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
handleStreamEvent(event) {
|
|
1494
|
+
switch (event.type) {
|
|
1495
|
+
case "message_start": {
|
|
1496
|
+
const model = isObject(event.message) && typeof event.message.model === "string" ? event.message.model : void 0;
|
|
1497
|
+
if (model) {
|
|
1498
|
+
this.usage.model = model;
|
|
1499
|
+
this.onEvent({ type: "usage", model });
|
|
1500
|
+
}
|
|
1501
|
+
break;
|
|
1502
|
+
}
|
|
1503
|
+
case "content_block_start": {
|
|
1504
|
+
const block = isObject(event.content_block) ? event.content_block : {};
|
|
1505
|
+
if (block.type === "text" || block.type === "tool_use") {
|
|
1506
|
+
this.text = "";
|
|
1507
|
+
this.partialJson = "";
|
|
1508
|
+
this.onEvent({ type: "answer-start" });
|
|
1509
|
+
}
|
|
1510
|
+
break;
|
|
1511
|
+
}
|
|
1512
|
+
case "content_block_delta": {
|
|
1513
|
+
const delta = isObject(event.delta) ? event.delta : {};
|
|
1514
|
+
if (delta.type === "thinking_delta") {
|
|
1515
|
+
const text = typeof delta.thinking === "string" ? delta.thinking : "";
|
|
1516
|
+
this.onEvent(text ? { type: "thinking", text } : { type: "thinking-pulse" });
|
|
1517
|
+
} else if (delta.type === "text_delta" && typeof delta.text === "string" && delta.text) {
|
|
1518
|
+
this.text += delta.text;
|
|
1519
|
+
this.onEvent({ type: "text", text: delta.text });
|
|
1520
|
+
} else if (delta.type === "input_json_delta" && typeof delta.partial_json === "string" && delta.partial_json) {
|
|
1521
|
+
this.partialJson += delta.partial_json;
|
|
1522
|
+
this.onEvent({ type: "text", text: delta.partial_json });
|
|
1523
|
+
}
|
|
1524
|
+
break;
|
|
1525
|
+
}
|
|
1526
|
+
case "message_delta": {
|
|
1527
|
+
if (isObject(event.usage)) this.recordUsage(event.usage);
|
|
1528
|
+
break;
|
|
1529
|
+
}
|
|
1530
|
+
default:
|
|
1531
|
+
break;
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
handleAssistant(message) {
|
|
1535
|
+
if (!Array.isArray(message.content)) return;
|
|
1536
|
+
for (const block of message.content) {
|
|
1537
|
+
if (isObject(block) && block.type === "tool_use" && isObject(block.input)) {
|
|
1538
|
+
this.messageStructured = JSON.stringify(block.input);
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
/** Tool results the CLI feeds back, e.g. a structured-output validation failure. */
|
|
1543
|
+
handleUser(message) {
|
|
1544
|
+
if (!Array.isArray(message.content)) return;
|
|
1545
|
+
for (const block of message.content) {
|
|
1546
|
+
if (!isObject(block) || block.type !== "tool_result" || block.is_error !== true) continue;
|
|
1547
|
+
const content = typeof block.content === "string" ? block.content : JSON.stringify(block.content ?? "");
|
|
1548
|
+
this.onEvent({ type: "notice", text: `Answer rejected (${content.replace(/^Output does not match required schema:\s*/i, "schema: ").slice(0, 300)}); the model is retrying` });
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
handleResult(ev) {
|
|
1552
|
+
if (isObject(ev.structured_output)) this.resultStructured = JSON.stringify(ev.structured_output);
|
|
1553
|
+
if (typeof ev.result === "string") this.resultText = ev.result;
|
|
1554
|
+
const subtype = typeof ev.subtype === "string" ? ev.subtype : "";
|
|
1555
|
+
if (ev.is_error === true || subtype.startsWith("error")) {
|
|
1556
|
+
this.isError = true;
|
|
1557
|
+
const errors = Array.isArray(ev.errors) ? ev.errors.filter((e) => typeof e === "string").join("; ") : "";
|
|
1558
|
+
this.errorMessage = errors || typeof ev.result === "string" && ev.result || subtype || "unknown error";
|
|
1559
|
+
}
|
|
1560
|
+
if (isObject(ev.usage)) this.recordUsage(ev.usage);
|
|
1561
|
+
if (typeof ev.total_cost_usd === "number") this.usage.costUsd = ev.total_cost_usd;
|
|
1562
|
+
this.onEvent({ type: "usage", ...this.usage });
|
|
1563
|
+
}
|
|
1564
|
+
recordUsage(usage) {
|
|
1565
|
+
if (typeof usage.output_tokens === "number") this.usage.outputTokens = usage.output_tokens;
|
|
1566
|
+
const details = usage.output_tokens_details;
|
|
1567
|
+
if (isObject(details) && typeof details.thinking_tokens === "number") this.usage.thinkingTokens = details.thinking_tokens;
|
|
1568
|
+
this.onEvent({ type: "usage", ...this.usage });
|
|
1569
|
+
}
|
|
1570
|
+
};
|
|
1571
|
+
|
|
1329
1572
|
// src/llm/command.ts
|
|
1330
1573
|
import { spawn } from "child_process";
|
|
1574
|
+
import { StringDecoder } from "string_decoder";
|
|
1331
1575
|
var STDERR_TAIL_LINES = 20;
|
|
1576
|
+
var CLAUDE_STREAM_ARGS = ["--output-format", "stream-json", "--verbose", "--include-partial-messages"];
|
|
1332
1577
|
var CommandProvider = class {
|
|
1333
1578
|
constructor(opts) {
|
|
1334
1579
|
this.opts = opts;
|
|
1335
1580
|
if (opts.command.length === 0) throw new LlmFailedError("LLM command is empty.");
|
|
1336
1581
|
}
|
|
1337
1582
|
opts;
|
|
1583
|
+
get kind() {
|
|
1584
|
+
return this.opts.kind ?? "plain";
|
|
1585
|
+
}
|
|
1338
1586
|
describe() {
|
|
1339
1587
|
return this.opts.command.map(shellQuote).join(" ");
|
|
1340
1588
|
}
|
|
1341
|
-
|
|
1589
|
+
/** The full argv that will run, including any flags added for the command kind. */
|
|
1590
|
+
argv(prompt) {
|
|
1591
|
+
let argv = [...this.opts.command];
|
|
1592
|
+
if (this.kind === "claude") {
|
|
1593
|
+
argv.push(...CLAUDE_STREAM_ARGS);
|
|
1594
|
+
if (this.opts.jsonSchema) argv.push("--json-schema", JSON.stringify(this.opts.jsonSchema));
|
|
1595
|
+
}
|
|
1596
|
+
if (prompt !== void 0 && this.opts.promptVia === "arg") argv = argv.map((a) => a.replaceAll("{prompt}", prompt));
|
|
1597
|
+
return argv;
|
|
1598
|
+
}
|
|
1599
|
+
complete(prompt, opts = {}) {
|
|
1342
1600
|
const { promptVia, timeoutMs } = this.opts;
|
|
1343
|
-
const
|
|
1344
|
-
|
|
1601
|
+
const onEvent = opts.onEvent ?? (() => {
|
|
1602
|
+
});
|
|
1603
|
+
const [bin, ...args] = this.argv(prompt);
|
|
1345
1604
|
const debug = this.opts.debug ?? (() => {
|
|
1346
1605
|
});
|
|
1347
1606
|
const started = Date.now();
|
|
1348
|
-
debug(`running ${this.describe()} (prompt ${Buffer.byteLength(prompt)} bytes via ${promptVia})`);
|
|
1607
|
+
debug(`running ${this.describe()}${this.kind === "claude" ? " (+stream-json)" : ""} (prompt ${Buffer.byteLength(prompt)} bytes via ${promptVia})`);
|
|
1349
1608
|
return new Promise((resolve, reject) => {
|
|
1350
1609
|
const env = { ...process.env, ...this.opts.env };
|
|
1351
1610
|
delete env.CLAUDECODE;
|
|
1352
1611
|
const child = spawn(bin, args, { cwd: this.opts.cwd, env, stdio: ["pipe", "pipe", "pipe"] });
|
|
1612
|
+
const parser = this.kind === "claude" ? new ClaudeStreamParser(onEvent) : null;
|
|
1613
|
+
const decoder = new StringDecoder("utf8");
|
|
1353
1614
|
const out = [];
|
|
1354
1615
|
const err = [];
|
|
1616
|
+
let outBytes = 0;
|
|
1355
1617
|
let settled = false;
|
|
1356
1618
|
const finish = (fn) => {
|
|
1357
1619
|
if (settled) return;
|
|
@@ -1365,7 +1627,16 @@ var CommandProvider = class {
|
|
|
1365
1627
|
hint: "Raise llm.timeoutMs in your config, or pick a faster preset."
|
|
1366
1628
|
})));
|
|
1367
1629
|
}, timeoutMs);
|
|
1368
|
-
|
|
1630
|
+
onEvent({ type: "started" });
|
|
1631
|
+
child.stdout.on("data", (b) => {
|
|
1632
|
+
outBytes += b.length;
|
|
1633
|
+
if (parser) {
|
|
1634
|
+
parser.push(decoder.write(b));
|
|
1635
|
+
} else {
|
|
1636
|
+
out.push(b);
|
|
1637
|
+
onEvent({ type: "output", bytes: outBytes });
|
|
1638
|
+
}
|
|
1639
|
+
});
|
|
1369
1640
|
child.stderr.on("data", (b) => err.push(b));
|
|
1370
1641
|
child.on("error", (e) => {
|
|
1371
1642
|
finish(() => {
|
|
@@ -1378,15 +1649,33 @@ var CommandProvider = class {
|
|
|
1378
1649
|
});
|
|
1379
1650
|
child.on("close", (code, signal) => {
|
|
1380
1651
|
finish(() => {
|
|
1381
|
-
const stdout = Buffer.concat(out).toString("utf8");
|
|
1382
1652
|
const stderr = Buffer.concat(err).toString("utf8");
|
|
1383
|
-
|
|
1653
|
+
const tail = stderr.trim().split("\n").slice(-STDERR_TAIL_LINES).join("\n");
|
|
1654
|
+
debug(`command exited with ${signal ? `signal ${signal}` : `code ${code}`} after ${Date.now() - started}ms; ${outBytes} bytes of stdout`);
|
|
1655
|
+
if (parser) {
|
|
1656
|
+
parser.push(decoder.end());
|
|
1657
|
+
const outcome = parser.finish();
|
|
1658
|
+
if (outcome.unparsedLines) debug(`${outcome.unparsedLines} non-JSON line(s) on stdout were ignored`);
|
|
1659
|
+
if (outcome.isError) {
|
|
1660
|
+
reject(new LlmFailedError(`Claude reported an error: ${outcome.errorMessage ?? "unknown"}`, { hint: tail || void 0 }));
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
if (code !== 0) {
|
|
1664
|
+
reject(new LlmFailedError(`LLM command failed (${signal ? `signal ${signal}` : `exit ${code}`}): ${this.describe()}${tail ? "\n" + tail : ""}`));
|
|
1665
|
+
return;
|
|
1666
|
+
}
|
|
1667
|
+
if (!outcome.result) {
|
|
1668
|
+
reject(new LlmFailedError("Claude produced no result.", { hint: "Run with --debug to see what came back." }));
|
|
1669
|
+
return;
|
|
1670
|
+
}
|
|
1671
|
+
resolve(outcome.result);
|
|
1672
|
+
return;
|
|
1673
|
+
}
|
|
1384
1674
|
if (code !== 0) {
|
|
1385
|
-
const tail = stderr.trim().split("\n").slice(-STDERR_TAIL_LINES).join("\n");
|
|
1386
1675
|
reject(new LlmFailedError(`LLM command failed (${signal ? `signal ${signal}` : `exit ${code}`}): ${this.describe()}${tail ? "\n" + tail : ""}`));
|
|
1387
1676
|
return;
|
|
1388
1677
|
}
|
|
1389
|
-
resolve(
|
|
1678
|
+
resolve(Buffer.concat(out).toString("utf8"));
|
|
1390
1679
|
});
|
|
1391
1680
|
});
|
|
1392
1681
|
if (promptVia === "stdin") {
|
|
@@ -1410,21 +1699,30 @@ function createProvider(llm, opts = {}) {
|
|
|
1410
1699
|
command: llm.command,
|
|
1411
1700
|
promptVia: llm.promptVia,
|
|
1412
1701
|
timeoutMs: llm.timeoutMs,
|
|
1702
|
+
kind: llm.kind,
|
|
1703
|
+
jsonSchema: llm.kind === "claude" && llm.jsonSchema ? TOUR_JSON_SCHEMA : void 0,
|
|
1413
1704
|
env: llm.env,
|
|
1414
1705
|
cwd: opts.cwd,
|
|
1415
1706
|
debug: opts.debug
|
|
1416
1707
|
});
|
|
1417
1708
|
}
|
|
1418
1709
|
|
|
1710
|
+
// src/core/progress.ts
|
|
1711
|
+
var noProgress = { phase() {
|
|
1712
|
+
}, llm() {
|
|
1713
|
+
} };
|
|
1714
|
+
|
|
1419
1715
|
// src/core/tour.ts
|
|
1420
1716
|
async function prepareTour(opts) {
|
|
1421
1717
|
const debug = opts.debug ?? (() => {
|
|
1422
1718
|
});
|
|
1423
1719
|
const warn = opts.warn ?? (() => {
|
|
1424
1720
|
});
|
|
1721
|
+
const progress = opts.progress ?? noProgress;
|
|
1425
1722
|
const { config } = opts;
|
|
1426
1723
|
const llm = resolveLlm(config);
|
|
1427
1724
|
const codehost = opts.codehost ?? createCodeHost(config, { debug });
|
|
1725
|
+
progress.phase("Working out what to tour");
|
|
1428
1726
|
const resolved = await resolveRange(opts.range ?? {}, {
|
|
1429
1727
|
cwd: opts.cwd,
|
|
1430
1728
|
exclude: config.git.exclude,
|
|
@@ -1433,6 +1731,7 @@ async function prepareTour(opts) {
|
|
|
1433
1731
|
debug
|
|
1434
1732
|
});
|
|
1435
1733
|
debug(`source: ${JSON.stringify(resolved.source)}`);
|
|
1734
|
+
progress.phase("Collecting the diff");
|
|
1436
1735
|
const diff = await collectDiff(resolved.source, { cwd: opts.cwd, exclude: config.git.exclude, warn });
|
|
1437
1736
|
if (diff.files.length === 0) {
|
|
1438
1737
|
throw new NoChangesError("The selected range has no changes (after excludes).", "Check git.exclude in your config, or pass a different range.");
|
|
@@ -1440,7 +1739,8 @@ async function prepareTour(opts) {
|
|
|
1440
1739
|
const commits = await collectCommits(resolved.source, opts.cwd);
|
|
1441
1740
|
const context = { source: resolved.source, branch: resolved.branch, diff, commits };
|
|
1442
1741
|
if (resolved.pullRequest) context.pullRequest = resolved.pullRequest;
|
|
1443
|
-
|
|
1742
|
+
progress.phase("Building the prompt");
|
|
1743
|
+
const built = buildPrompt({ ...context, maxBytes: llm.maxPromptBytes, structured: llm.kind === "claude" && llm.jsonSchema });
|
|
1444
1744
|
if (built.truncation.truncated.length || built.truncation.omitted.length) {
|
|
1445
1745
|
warn(`Prompt exceeded ${llm.maxPromptBytes} bytes; truncated ${built.truncation.truncated.length} and omitted ${built.truncation.omitted.length} file diff(s).`);
|
|
1446
1746
|
}
|
|
@@ -1454,11 +1754,13 @@ async function generateTour(opts, prepared) {
|
|
|
1454
1754
|
});
|
|
1455
1755
|
const warn = opts.warn ?? (() => {
|
|
1456
1756
|
});
|
|
1757
|
+
const progress = opts.progress ?? noProgress;
|
|
1457
1758
|
const { config } = opts;
|
|
1458
1759
|
const prep = prepared ?? await prepareTour(opts);
|
|
1459
1760
|
const useCache = config.cache.enabled && !opts.noCache;
|
|
1460
1761
|
const cache = useCache ? new TourCache(opts.cacheDir) : null;
|
|
1461
1762
|
if (cache && !opts.refresh) {
|
|
1763
|
+
progress.phase("Checking the cache");
|
|
1462
1764
|
const hit = await cache.get(prep.cacheKey);
|
|
1463
1765
|
if (hit) {
|
|
1464
1766
|
debug(`cache hit: ${cache.pathFor(prep.cacheKey)}`);
|
|
@@ -1468,7 +1770,9 @@ async function generateTour(opts, prepared) {
|
|
|
1468
1770
|
}
|
|
1469
1771
|
const llm = resolveLlm(config);
|
|
1470
1772
|
const provider = opts.provider ?? createProvider(llm, { cwd: opts.cwd, debug });
|
|
1471
|
-
|
|
1773
|
+
progress.phase(`Asking ${llm.preset ?? llm.command[0]}`);
|
|
1774
|
+
const output = await completeWithRepair(provider, prep.built.prompt, debug, progress);
|
|
1775
|
+
progress.phase("Assembling the tour");
|
|
1472
1776
|
const tour = materializeTour({
|
|
1473
1777
|
output,
|
|
1474
1778
|
diff: prep.context.diff,
|
|
@@ -1485,14 +1789,16 @@ async function generateTour(opts, prepared) {
|
|
|
1485
1789
|
}
|
|
1486
1790
|
return { tour, fromCache: false, cacheKey: cache ? prep.cacheKey : null, cachePath, prompt: prep.built.prompt };
|
|
1487
1791
|
}
|
|
1488
|
-
async function completeWithRepair(provider, prompt, debug) {
|
|
1489
|
-
|
|
1792
|
+
async function completeWithRepair(provider, prompt, debug, progress) {
|
|
1793
|
+
const onEvent = (event) => progress.llm(event);
|
|
1794
|
+
let raw = await provider.complete(prompt, { onEvent });
|
|
1490
1795
|
debug(`raw model output (attempt 1):
|
|
1491
1796
|
${raw}`);
|
|
1492
1797
|
const first = parseOutput(raw);
|
|
1493
1798
|
if (first.ok) return first.value;
|
|
1494
1799
|
debug(`attempt 1 unusable: ${first.reason}; retrying with repair prompt`);
|
|
1495
|
-
|
|
1800
|
+
progress.phase("Asking again (the first answer was not valid JSON)");
|
|
1801
|
+
raw = await provider.complete(prompt + REPAIR_SUFFIX(first.reason), { onEvent });
|
|
1496
1802
|
debug(`raw model output (attempt 2):
|
|
1497
1803
|
${raw}`);
|
|
1498
1804
|
const second = parseOutput(raw);
|
|
@@ -1511,14 +1817,338 @@ function parseOutput(raw) {
|
|
|
1511
1817
|
return { ok: true, value: parsed.data };
|
|
1512
1818
|
}
|
|
1513
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
|
+
|
|
1514
1942
|
// src/render/cli.ts
|
|
1515
1943
|
import pc from "picocolors";
|
|
1944
|
+
|
|
1945
|
+
// src/render/highlight.ts
|
|
1946
|
+
import { common, createLowlight } from "lowlight";
|
|
1947
|
+
var lowlight = createLowlight(common);
|
|
1948
|
+
var BY_EXT = {
|
|
1949
|
+
ts: "typescript",
|
|
1950
|
+
mts: "typescript",
|
|
1951
|
+
cts: "typescript",
|
|
1952
|
+
tsx: "typescript",
|
|
1953
|
+
js: "javascript",
|
|
1954
|
+
mjs: "javascript",
|
|
1955
|
+
cjs: "javascript",
|
|
1956
|
+
jsx: "javascript",
|
|
1957
|
+
json: "json",
|
|
1958
|
+
jsonc: "json",
|
|
1959
|
+
json5: "json",
|
|
1960
|
+
py: "python",
|
|
1961
|
+
pyi: "python",
|
|
1962
|
+
rb: "ruby",
|
|
1963
|
+
rake: "ruby",
|
|
1964
|
+
gemspec: "ruby",
|
|
1965
|
+
go: "go",
|
|
1966
|
+
rs: "rust",
|
|
1967
|
+
java: "java",
|
|
1968
|
+
kt: "kotlin",
|
|
1969
|
+
kts: "kotlin",
|
|
1970
|
+
swift: "swift",
|
|
1971
|
+
scala: "scala",
|
|
1972
|
+
c: "c",
|
|
1973
|
+
h: "c",
|
|
1974
|
+
cc: "cpp",
|
|
1975
|
+
cpp: "cpp",
|
|
1976
|
+
cxx: "cpp",
|
|
1977
|
+
hpp: "cpp",
|
|
1978
|
+
hh: "cpp",
|
|
1979
|
+
cs: "csharp",
|
|
1980
|
+
m: "objectivec",
|
|
1981
|
+
mm: "objectivec",
|
|
1982
|
+
php: "php",
|
|
1983
|
+
pl: "perl",
|
|
1984
|
+
pm: "perl",
|
|
1985
|
+
lua: "lua",
|
|
1986
|
+
r: "r",
|
|
1987
|
+
dart: "dart",
|
|
1988
|
+
sh: "bash",
|
|
1989
|
+
bash: "bash",
|
|
1990
|
+
zsh: "bash",
|
|
1991
|
+
fish: "bash",
|
|
1992
|
+
yml: "yaml",
|
|
1993
|
+
yaml: "yaml",
|
|
1994
|
+
toml: "ini",
|
|
1995
|
+
ini: "ini",
|
|
1996
|
+
cfg: "ini",
|
|
1997
|
+
conf: "ini",
|
|
1998
|
+
md: "markdown",
|
|
1999
|
+
markdown: "markdown",
|
|
2000
|
+
html: "xml",
|
|
2001
|
+
htm: "xml",
|
|
2002
|
+
xml: "xml",
|
|
2003
|
+
svg: "xml",
|
|
2004
|
+
vue: "xml",
|
|
2005
|
+
css: "css",
|
|
2006
|
+
scss: "scss",
|
|
2007
|
+
less: "less",
|
|
2008
|
+
sql: "sql",
|
|
2009
|
+
graphql: "graphql",
|
|
2010
|
+
gql: "graphql",
|
|
2011
|
+
diff: "diff",
|
|
2012
|
+
patch: "diff",
|
|
2013
|
+
makefile: "makefile",
|
|
2014
|
+
mk: "makefile",
|
|
2015
|
+
vb: "vbnet",
|
|
2016
|
+
vbnet: "vbnet",
|
|
2017
|
+
wasm: "wasm"
|
|
2018
|
+
};
|
|
2019
|
+
var BY_NAME = {
|
|
2020
|
+
makefile: "makefile",
|
|
2021
|
+
gnumakefile: "makefile",
|
|
2022
|
+
dockerfile: "bash",
|
|
2023
|
+
rakefile: "ruby",
|
|
2024
|
+
gemfile: "ruby",
|
|
2025
|
+
".bashrc": "bash",
|
|
2026
|
+
".zshrc": "bash",
|
|
2027
|
+
".profile": "bash",
|
|
2028
|
+
"cmakelists.txt": "cmake"
|
|
2029
|
+
};
|
|
2030
|
+
function languageForPath(path) {
|
|
2031
|
+
const base = path.slice(path.lastIndexOf("/") + 1).toLowerCase();
|
|
2032
|
+
const byName = BY_NAME[base];
|
|
2033
|
+
if (byName && lowlight.registered(byName)) return byName;
|
|
2034
|
+
const dot = base.lastIndexOf(".");
|
|
2035
|
+
if (dot === -1) return null;
|
|
2036
|
+
const lang = BY_EXT[base.slice(dot + 1)];
|
|
2037
|
+
return lang && lowlight.registered(lang) ? lang : null;
|
|
2038
|
+
}
|
|
2039
|
+
function stylers(c) {
|
|
2040
|
+
return {
|
|
2041
|
+
keyword: c.magenta,
|
|
2042
|
+
"selector-tag": c.magenta,
|
|
2043
|
+
"template-tag": c.magenta,
|
|
2044
|
+
doctag: c.magenta,
|
|
2045
|
+
string: c.yellow,
|
|
2046
|
+
regexp: c.yellow,
|
|
2047
|
+
"template-variable": c.yellow,
|
|
2048
|
+
subst: (s) => s,
|
|
2049
|
+
comment: c.gray,
|
|
2050
|
+
quote: c.gray,
|
|
2051
|
+
number: c.cyan,
|
|
2052
|
+
literal: c.cyan,
|
|
2053
|
+
symbol: c.cyan,
|
|
2054
|
+
bullet: c.cyan,
|
|
2055
|
+
link: c.cyan,
|
|
2056
|
+
title: c.blue,
|
|
2057
|
+
section: c.blue,
|
|
2058
|
+
"selector-id": c.blue,
|
|
2059
|
+
"selector-class": c.blue,
|
|
2060
|
+
type: c.cyan,
|
|
2061
|
+
built_in: c.cyan,
|
|
2062
|
+
class: c.cyan,
|
|
2063
|
+
attr: c.blue,
|
|
2064
|
+
attribute: c.blue,
|
|
2065
|
+
property: c.blue,
|
|
2066
|
+
variable: (s) => s,
|
|
2067
|
+
params: (s) => s,
|
|
2068
|
+
meta: c.gray,
|
|
2069
|
+
tag: c.blue,
|
|
2070
|
+
name: c.blue,
|
|
2071
|
+
"selector-attr": c.blue,
|
|
2072
|
+
"selector-pseudo": c.blue,
|
|
2073
|
+
addition: (s) => s,
|
|
2074
|
+
deletion: (s) => s,
|
|
2075
|
+
emphasis: c.italic,
|
|
2076
|
+
strong: c.bold
|
|
2077
|
+
};
|
|
2078
|
+
}
|
|
2079
|
+
function highlightCode(code, lang, c) {
|
|
2080
|
+
if (!lowlight.registered(lang)) return null;
|
|
2081
|
+
let tree;
|
|
2082
|
+
try {
|
|
2083
|
+
tree = lowlight.highlight(lang, code);
|
|
2084
|
+
} catch {
|
|
2085
|
+
return null;
|
|
2086
|
+
}
|
|
2087
|
+
const map = stylers(c);
|
|
2088
|
+
const lines = [""];
|
|
2089
|
+
const visit = (node, style) => {
|
|
2090
|
+
if (node.type === "text" && typeof node.value === "string") {
|
|
2091
|
+
const parts = node.value.split("\n");
|
|
2092
|
+
parts.forEach((part, i) => {
|
|
2093
|
+
if (i > 0) lines.push("");
|
|
2094
|
+
if (part) lines[lines.length - 1] += style ? style(part) : part;
|
|
2095
|
+
});
|
|
2096
|
+
return;
|
|
2097
|
+
}
|
|
2098
|
+
let own = style;
|
|
2099
|
+
for (const cls of node.properties?.className ?? []) {
|
|
2100
|
+
const scope = cls.startsWith("hljs-") ? cls.slice(5) : cls;
|
|
2101
|
+
const s = map[scope];
|
|
2102
|
+
if (s) {
|
|
2103
|
+
own = s;
|
|
2104
|
+
break;
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
for (const child of node.children ?? []) visit(child, own);
|
|
2108
|
+
};
|
|
2109
|
+
visit(tree, null);
|
|
2110
|
+
const expected = code.split("\n").length;
|
|
2111
|
+
if (lines.length !== expected) return null;
|
|
2112
|
+
return lines;
|
|
2113
|
+
}
|
|
2114
|
+
var ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
2115
|
+
function stripAnsi(s) {
|
|
2116
|
+
return s.replace(ANSI_RE, "");
|
|
2117
|
+
}
|
|
2118
|
+
function visibleLength(s) {
|
|
2119
|
+
return stripAnsi(s).length;
|
|
2120
|
+
}
|
|
2121
|
+
var SOFT_RESET = "\x1B[39m\x1B[22m\x1B[23m\x1B[24m";
|
|
2122
|
+
function truncateAnsi(s, max) {
|
|
2123
|
+
if (visibleLength(s) <= max) return s;
|
|
2124
|
+
let out = "";
|
|
2125
|
+
let seen = 0;
|
|
2126
|
+
const limit = Math.max(0, max - 1);
|
|
2127
|
+
for (let i = 0; i < s.length; ) {
|
|
2128
|
+
if (s[i] === "\x1B") {
|
|
2129
|
+
const m = /^\x1b\[[0-9;]*m/.exec(s.slice(i));
|
|
2130
|
+
if (m) {
|
|
2131
|
+
out += m[0];
|
|
2132
|
+
i += m[0].length;
|
|
2133
|
+
continue;
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
if (seen >= limit) break;
|
|
2137
|
+
out += s[i];
|
|
2138
|
+
seen++;
|
|
2139
|
+
i++;
|
|
2140
|
+
}
|
|
2141
|
+
return out + SOFT_RESET + "\u2026";
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
// src/render/cli.ts
|
|
1516
2145
|
var MIN_WIDTH = 40;
|
|
1517
2146
|
var MAX_WIDTH = 120;
|
|
1518
2147
|
var CliRenderer = class {
|
|
1519
2148
|
render(tour, opts) {
|
|
1520
2149
|
const c = pc.createColors(opts.color);
|
|
1521
2150
|
const width = Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, opts.width));
|
|
2151
|
+
const style = { highlight: Boolean(opts.color && opts.highlight) };
|
|
1522
2152
|
const out = [];
|
|
1523
2153
|
out.push(...renderHeader(tour, c, width, opts.fromCache ?? false));
|
|
1524
2154
|
out.push("");
|
|
@@ -1527,12 +2157,12 @@ var CliRenderer = class {
|
|
|
1527
2157
|
if (!section) {
|
|
1528
2158
|
throw new UsageError(`No section ${opts.section}; this tour has ${tour.sections.length} section${tour.sections.length === 1 ? "" : "s"}.`);
|
|
1529
2159
|
}
|
|
1530
|
-
out.push(...
|
|
2160
|
+
out.push(...renderSection2(section, opts.section, tour.sections.length, c, width, style));
|
|
1531
2161
|
} else {
|
|
1532
2162
|
out.push(...renderToc(tour, c, width));
|
|
1533
2163
|
out.push("");
|
|
1534
2164
|
tour.sections.forEach((s, i) => {
|
|
1535
|
-
out.push(...
|
|
2165
|
+
out.push(...renderSection2(s, i + 1, tour.sections.length, c, width, style));
|
|
1536
2166
|
out.push("");
|
|
1537
2167
|
});
|
|
1538
2168
|
}
|
|
@@ -1566,12 +2196,12 @@ function renderToc(tour, c, width) {
|
|
|
1566
2196
|
const label = ` ${n}. ${s.title}`;
|
|
1567
2197
|
const right = `${s.stats.files} file${s.stats.files === 1 ? "" : "s"} \xB7 ${statsLine(s.stats, c, true)}`;
|
|
1568
2198
|
const rightPlain = `${s.stats.files} file${s.stats.files === 1 ? "" : "s"} \xB7 +${s.stats.additions} -${s.stats.deletions}`;
|
|
1569
|
-
const gap = Math.max(2, width -
|
|
2199
|
+
const gap = Math.max(2, width - visibleLength2(label) - rightPlain.length);
|
|
1570
2200
|
lines.push(label + " ".repeat(gap) + c.dim(right));
|
|
1571
2201
|
}
|
|
1572
2202
|
return lines;
|
|
1573
2203
|
}
|
|
1574
|
-
function
|
|
2204
|
+
function renderSection2(s, index, count, c, width, style) {
|
|
1575
2205
|
const lines = [];
|
|
1576
2206
|
lines.push(c.dim("\u2500".repeat(width)));
|
|
1577
2207
|
lines.push(c.bold(`${index}. ${s.title}`) + c.dim(` (${index}/${count})`));
|
|
@@ -1586,26 +2216,39 @@ function renderSection(s, index, count, c, width) {
|
|
|
1586
2216
|
}
|
|
1587
2217
|
for (const e of s.excerpts) {
|
|
1588
2218
|
lines.push("");
|
|
1589
|
-
lines.push(...
|
|
2219
|
+
lines.push(...renderExcerpt2(e, c, width, style));
|
|
1590
2220
|
}
|
|
1591
2221
|
return lines;
|
|
1592
2222
|
}
|
|
1593
|
-
function
|
|
2223
|
+
function renderExcerpt2(e, c, width, style) {
|
|
1594
2224
|
const lines = [];
|
|
1595
2225
|
const title = ` ${c.cyan(e.file)}${c.dim(":" + e.newStart)}`;
|
|
1596
2226
|
lines.push(e.note ? `${title} ${c.italic(c.dim(e.note))}` : title);
|
|
1597
2227
|
const maxNo = Math.max(...e.lines.map((l) => Math.max(l.oldNo ?? 0, l.newNo ?? 0)), 1);
|
|
1598
2228
|
const w = String(maxNo).length;
|
|
1599
|
-
const budget = Math.max(20, width - (3 + w * 2 +
|
|
1600
|
-
|
|
2229
|
+
const budget = Math.max(20, width - (3 + w * 2 + 3) - 1);
|
|
2230
|
+
const texts = e.lines.map((l) => expandTabs(l.text));
|
|
2231
|
+
const lang = style.highlight ? languageForPath(e.file) : null;
|
|
2232
|
+
const highlighted = lang ? highlightCode(texts.join("\n"), lang, c) : null;
|
|
2233
|
+
e.lines.forEach((l, i) => {
|
|
1601
2234
|
const oldNo = l.oldNo === void 0 ? " ".repeat(w) : String(l.oldNo).padStart(w);
|
|
1602
2235
|
const newNo = l.newNo === void 0 ? " ".repeat(w) : String(l.newNo).padStart(w);
|
|
1603
2236
|
const sign = l.type === "add" ? "+" : l.type === "del" ? "-" : " ";
|
|
1604
|
-
const text = truncate(expandTabs(l.text), budget);
|
|
1605
2237
|
const gutter = c.dim(` ${oldNo} ${newNo} \u2502`);
|
|
1606
|
-
const
|
|
1607
|
-
|
|
1608
|
-
|
|
2238
|
+
const plain = texts[i];
|
|
2239
|
+
if (!highlighted) {
|
|
2240
|
+
const signStyled2 = l.type === "add" ? c.green(sign) : l.type === "del" ? c.red(sign) : sign;
|
|
2241
|
+
lines.push(`${gutter}${signStyled2}${truncate(plain, budget)}`);
|
|
2242
|
+
return;
|
|
2243
|
+
}
|
|
2244
|
+
const text = truncateAnsi(highlighted[i], budget);
|
|
2245
|
+
if (l.type === "ctx") {
|
|
2246
|
+
lines.push(`${gutter} ${text}`);
|
|
2247
|
+
return;
|
|
2248
|
+
}
|
|
2249
|
+
const signStyled = l.type === "add" ? c.green("+") : c.red("-");
|
|
2250
|
+
lines.push(`${gutter}${signStyled}${text}`);
|
|
2251
|
+
});
|
|
1609
2252
|
return lines;
|
|
1610
2253
|
}
|
|
1611
2254
|
function statsLine(stats, c, omitFiles = false) {
|
|
@@ -1645,7 +2288,7 @@ function truncate(s, max) {
|
|
|
1645
2288
|
function expandTabs(s) {
|
|
1646
2289
|
return s.replaceAll(" ", " ");
|
|
1647
2290
|
}
|
|
1648
|
-
function
|
|
2291
|
+
function visibleLength2(s) {
|
|
1649
2292
|
return s.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
1650
2293
|
}
|
|
1651
2294
|
function relativeTime(iso, now = /* @__PURE__ */ new Date()) {
|
|
@@ -1662,7 +2305,7 @@ function relativeTime(iso, now = /* @__PURE__ */ new Date()) {
|
|
|
1662
2305
|
}
|
|
1663
2306
|
|
|
1664
2307
|
// src/render/pager.ts
|
|
1665
|
-
import { spawn as
|
|
2308
|
+
import { spawn as spawn3 } from "child_process";
|
|
1666
2309
|
async function writeMaybePaged(output, opts) {
|
|
1667
2310
|
const env = opts.env ?? process.env;
|
|
1668
2311
|
const lineCount = output.split("\n").length;
|
|
@@ -1674,7 +2317,7 @@ async function writeMaybePaged(output, opts) {
|
|
|
1674
2317
|
const pagerCmd = env.PAGER && env.PAGER.trim() || "less";
|
|
1675
2318
|
const cmd = pagerCmd === "less" && !env.LESS ? "less -RFX" : pagerCmd;
|
|
1676
2319
|
await new Promise((resolve) => {
|
|
1677
|
-
const child =
|
|
2320
|
+
const child = spawn3(cmd, { shell: true, stdio: ["pipe", "inherit", "inherit"], env });
|
|
1678
2321
|
let fellBack = false;
|
|
1679
2322
|
child.on("error", () => {
|
|
1680
2323
|
fellBack = true;
|
|
@@ -1718,6 +2361,7 @@ export {
|
|
|
1718
2361
|
loadConfig,
|
|
1719
2362
|
allPresets,
|
|
1720
2363
|
resolveLlm,
|
|
2364
|
+
resolveChat,
|
|
1721
2365
|
shellSplit,
|
|
1722
2366
|
TourCache,
|
|
1723
2367
|
extractJson,
|
|
@@ -1727,11 +2371,14 @@ export {
|
|
|
1727
2371
|
materializeTour,
|
|
1728
2372
|
sliceHunk,
|
|
1729
2373
|
PROMPT_VERSION,
|
|
2374
|
+
promptHeader,
|
|
1730
2375
|
buildPrompt,
|
|
2376
|
+
renderChange,
|
|
1731
2377
|
describeSource,
|
|
1732
2378
|
GhCodeHost,
|
|
1733
2379
|
NoCodeHost,
|
|
1734
2380
|
createCodeHost,
|
|
2381
|
+
git,
|
|
1735
2382
|
gitRoot,
|
|
1736
2383
|
revParse,
|
|
1737
2384
|
currentBranch,
|
|
@@ -1739,10 +2386,17 @@ export {
|
|
|
1739
2386
|
collectCommits,
|
|
1740
2387
|
resolveRange,
|
|
1741
2388
|
detectDefaultBranch,
|
|
2389
|
+
TOUR_JSON_SCHEMA,
|
|
2390
|
+
ClaudeStreamParser,
|
|
1742
2391
|
CommandProvider,
|
|
1743
2392
|
createProvider,
|
|
2393
|
+
noProgress,
|
|
1744
2394
|
prepareTour,
|
|
1745
2395
|
generateTour,
|
|
2396
|
+
buildChatContext,
|
|
2397
|
+
chatArgv,
|
|
2398
|
+
chatSlots,
|
|
2399
|
+
launchChat,
|
|
1746
2400
|
CliRenderer,
|
|
1747
2401
|
describeSource2,
|
|
1748
2402
|
wrap,
|
|
@@ -1750,4 +2404,4 @@ export {
|
|
|
1750
2404
|
writeMaybePaged,
|
|
1751
2405
|
writeStdout
|
|
1752
2406
|
};
|
|
1753
|
-
//# sourceMappingURL=chunk-
|
|
2407
|
+
//# sourceMappingURL=chunk-YI2UL4IC.js.map
|