topchester-ai 0.10.0 → 0.11.0
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.mjs +1875 -286
- package/dist/cli.mjs.map +1 -1
- package/package.json +5 -2
package/dist/cli.mjs
CHANGED
|
@@ -15,7 +15,7 @@ import { parse } from "yaml";
|
|
|
15
15
|
import pino from "pino";
|
|
16
16
|
import picomatch from "picomatch";
|
|
17
17
|
import { uuidv7 } from "uuidv7";
|
|
18
|
-
import {
|
|
18
|
+
import { CURSOR_MARKER, Markdown, ProcessTerminal, TUI, decodeKittyPrintable, isKeyRelease, isKeyRepeat, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
19
19
|
import { highlight, supportsLanguage } from "cli-highlight";
|
|
20
20
|
//#region src/agent/tools/ai-sdk-tools.ts
|
|
21
21
|
function toAiSdkToolSet(definitions) {
|
|
@@ -97,23 +97,26 @@ async function enqueueFileMutation(path, mutate) {
|
|
|
97
97
|
}
|
|
98
98
|
//#endregion
|
|
99
99
|
//#region src/agent/tools/types.ts
|
|
100
|
+
function isToolErrorResult(result) {
|
|
101
|
+
return "error" in result && typeof result.error === "string";
|
|
102
|
+
}
|
|
100
103
|
function defineTool(definition) {
|
|
101
104
|
return definition;
|
|
102
105
|
}
|
|
103
106
|
//#endregion
|
|
104
107
|
//#region src/agent/tools/edit-file.ts
|
|
105
108
|
const editFileEditSchema = z.object({
|
|
106
|
-
old_text: z.string(),
|
|
107
|
-
new_text: z.string()
|
|
109
|
+
old_text: z.string().describe("Exact current file text to replace; include whitespace exactly."),
|
|
110
|
+
new_text: z.string().describe("Replacement text for old_text.")
|
|
108
111
|
});
|
|
109
112
|
const editFileTool = defineTool({
|
|
110
113
|
name: "edit_file",
|
|
111
|
-
description: "Edit an existing UTF-8 file inside the workspace with exact text replacements.",
|
|
112
|
-
prompt: "edit_file: edit an existing UTF-8 file inside the workspace with exact old_text/new_text replacements; read the file first, keep old_text small but unique, and make multiple disjoint edits for one file in one call. To use it, reply with only JSON: {\"tool\":\"edit_file\",\"args\":{\"path\":\"src/example.ts\",\"
|
|
114
|
+
description: "Edit an existing UTF-8 file inside the workspace with exact text replacements. Use expected_current_hash only as the current/pre-edit hash from read_file, never as a predicted post-edit hash.",
|
|
115
|
+
prompt: "edit_file: edit an existing UTF-8 file inside the workspace with exact old_text/new_text replacements; read the file first, keep old_text small but unique, and make multiple disjoint edits for one file in one call. expected_current_hash is optional and must be the current/pre-edit hash returned by the latest read_file for that file; never invent it or use a predicted after-edit hash. To use it, reply with only JSON: {\"tool\":\"edit_file\",\"args\":{\"path\":\"src/example.ts\",\"expected_current_hash\":\"sha256:current-file-hash-from-read_file\",\"edits\":[{\"old_text\":\"const enabled = false;\\n\",\"new_text\":\"const enabled = true;\\n\"}]}}",
|
|
113
116
|
argsSchema: z.object({
|
|
114
|
-
path: z.string(),
|
|
115
|
-
|
|
116
|
-
edits: z.array(editFileEditSchema).min(1)
|
|
117
|
+
path: z.string().describe("Workspace-relative path to the existing UTF-8 file to edit."),
|
|
118
|
+
expected_current_hash: z.string().optional().describe("Optional current file hash returned by the latest read_file result for this file. This is checked before editing to catch stale reads; it is not the hash after the edit."),
|
|
119
|
+
edits: z.array(editFileEditSchema).min(1).describe("Exact text replacements to apply to the original file.")
|
|
117
120
|
}),
|
|
118
121
|
execute: (context, args) => editWorkspaceFile(context.workspaceRoot, args, { logger: context.logger })
|
|
119
122
|
});
|
|
@@ -123,7 +126,7 @@ async function editWorkspaceFile(workspaceRoot, args, options = {}) {
|
|
|
123
126
|
const fileStat = await statExistingFile(scopedPath.path, args.path);
|
|
124
127
|
const beforeBytes = await readFile(scopedPath.path);
|
|
125
128
|
const beforeHash = hashBytes$1(beforeBytes);
|
|
126
|
-
if (args.
|
|
129
|
+
if (args.expected_current_hash && args.expected_current_hash !== beforeHash) throw new Error(`edit_file expected_current_hash did not match ${scopedPath.relativePath}.`);
|
|
127
130
|
const result = applyExactEdits(decodeUtf8$1(scopedPath.relativePath, beforeBytes), args.edits, scopedPath.relativePath);
|
|
128
131
|
const afterBytes = Buffer.from(result.newContent, "utf8");
|
|
129
132
|
const afterHash = hashBytes$1(afterBytes);
|
|
@@ -376,7 +379,7 @@ const ignoredDirectories = new Set([
|
|
|
376
379
|
]);
|
|
377
380
|
const findFileTool = defineTool({
|
|
378
381
|
name: "find_file",
|
|
379
|
-
description: "Find files by fuzzy name inside the workspace.",
|
|
382
|
+
description: "Find files by fuzzy name inside the workspace. Results are file paths, not file contents; use read_file next when the user needs contents.",
|
|
380
383
|
prompt: "find_file: find existing files by fuzzy path or filename inside the workspace; matches may appear in the middle of a filename, and results are file paths, not file contents. To use it, reply with only JSON: {\"tool\":\"find_file\",\"args\":{\"query\":\"runtime\"}}",
|
|
381
384
|
argsSchema: findFileArgsSchema,
|
|
382
385
|
execute: (context, args) => findWorkspaceFilesByName(context.workspaceRoot, args, {
|
|
@@ -829,7 +832,7 @@ function parseGitLog(output) {
|
|
|
829
832
|
};
|
|
830
833
|
});
|
|
831
834
|
}
|
|
832
|
-
function truncateText(content, maxBytes) {
|
|
835
|
+
function truncateText$1(content, maxBytes) {
|
|
833
836
|
if (Buffer.byteLength(content) <= maxBytes) return {
|
|
834
837
|
content,
|
|
835
838
|
truncated: false
|
|
@@ -1056,7 +1059,7 @@ async function inspectGitDiff(context, args) {
|
|
|
1056
1059
|
changedFiles.add(file);
|
|
1057
1060
|
}
|
|
1058
1061
|
}
|
|
1059
|
-
const bounded = truncateText(sections.join("\n").trimEnd() || "No diff.", args.max_bytes);
|
|
1062
|
+
const bounded = truncateText$1(sections.join("\n").trimEnd() || "No diff.", args.max_bytes);
|
|
1060
1063
|
return {
|
|
1061
1064
|
tool: "git_diff",
|
|
1062
1065
|
path: path ?? void 0,
|
|
@@ -1644,6 +1647,8 @@ const READ_ONLY_COMMANDS = new Set([
|
|
|
1644
1647
|
"find",
|
|
1645
1648
|
"fd",
|
|
1646
1649
|
"cat",
|
|
1650
|
+
"sed",
|
|
1651
|
+
"sort",
|
|
1647
1652
|
"head",
|
|
1648
1653
|
"tail",
|
|
1649
1654
|
"wc",
|
|
@@ -1851,6 +1856,7 @@ function validateSimpleCommand(command, context) {
|
|
|
1851
1856
|
case "git": return validateGitCommand(command, context);
|
|
1852
1857
|
case "find": return validateFindCommand(command, context);
|
|
1853
1858
|
case "fd": return validateGenericCommandArgs(command, context, FD_OPTIONS_WITH_VALUES);
|
|
1859
|
+
case "sed": return validateSedCommand(command, context);
|
|
1854
1860
|
default: return validateGenericCommandArgs(command, context, COMMON_OPTIONS_WITH_VALUES);
|
|
1855
1861
|
}
|
|
1856
1862
|
}
|
|
@@ -1873,6 +1879,88 @@ function validateGitCommand(command, context) {
|
|
|
1873
1879
|
function validateFindCommand(command, context) {
|
|
1874
1880
|
return validateGenericCommandArgs(command, context, FIND_OPTIONS_WITH_VALUES);
|
|
1875
1881
|
}
|
|
1882
|
+
function validateSedCommand(command, context) {
|
|
1883
|
+
let sawScript = false;
|
|
1884
|
+
for (let index = 0; index < command.args.length; index += 1) {
|
|
1885
|
+
const arg = command.args[index] ?? "";
|
|
1886
|
+
if (arg === "-i" || arg.startsWith("-i") || arg === "--in-place" || arg.startsWith("--in-place=")) return {
|
|
1887
|
+
allowed: false,
|
|
1888
|
+
reason: "inspect_command rejected 'sed' because in-place edits are unsafe."
|
|
1889
|
+
};
|
|
1890
|
+
if (arg === "-e" || arg === "--expression") {
|
|
1891
|
+
const script = command.args[index + 1];
|
|
1892
|
+
if (!script) return {
|
|
1893
|
+
allowed: false,
|
|
1894
|
+
reason: "inspect_command rejected 'sed' because -e requires a script."
|
|
1895
|
+
};
|
|
1896
|
+
const scriptResult = validateSedScript(script);
|
|
1897
|
+
if (!scriptResult.allowed) return scriptResult;
|
|
1898
|
+
sawScript = true;
|
|
1899
|
+
index += 1;
|
|
1900
|
+
continue;
|
|
1901
|
+
}
|
|
1902
|
+
if (arg === "-n" || arg === "--quiet" || arg === "--silent" || /^-[Erun]+$/.test(arg)) continue;
|
|
1903
|
+
if (arg.startsWith("-")) return {
|
|
1904
|
+
allowed: false,
|
|
1905
|
+
reason: `inspect_command rejected 'sed' because '${arg}' is not allowed.`
|
|
1906
|
+
};
|
|
1907
|
+
if (!sawScript && looksLikeSedScript(arg)) {
|
|
1908
|
+
const scriptResult = validateSedScript(arg);
|
|
1909
|
+
if (!scriptResult.allowed) return scriptResult;
|
|
1910
|
+
sawScript = true;
|
|
1911
|
+
continue;
|
|
1912
|
+
}
|
|
1913
|
+
const scoped = resolveWorkspacePath$1(context.workspaceRoot, arg, context.cwd);
|
|
1914
|
+
if (!scoped.allowed) return {
|
|
1915
|
+
allowed: false,
|
|
1916
|
+
reason: scoped.reason
|
|
1917
|
+
};
|
|
1918
|
+
}
|
|
1919
|
+
return sawScript ? { allowed: true } : {
|
|
1920
|
+
allowed: false,
|
|
1921
|
+
reason: "inspect_command rejected 'sed' because it requires an inline script."
|
|
1922
|
+
};
|
|
1923
|
+
}
|
|
1924
|
+
function looksLikeSedScript(arg) {
|
|
1925
|
+
return arg.startsWith("s") || /^(\d+|\$)?(,(\d+|\$))?[pdq]$/.test(arg);
|
|
1926
|
+
}
|
|
1927
|
+
function validateSedScript(script) {
|
|
1928
|
+
if (script.includes("\n")) return {
|
|
1929
|
+
allowed: false,
|
|
1930
|
+
reason: "inspect_command rejected 'sed' because multiline scripts are unsafe."
|
|
1931
|
+
};
|
|
1932
|
+
if (/^(\d+|\$)?(,(\d+|\$))?[pdq]$/.test(script)) return { allowed: true };
|
|
1933
|
+
if (!script.startsWith("s") || script.length < 4) return {
|
|
1934
|
+
allowed: false,
|
|
1935
|
+
reason: "inspect_command rejected 'sed' because only simple read-only scripts are allowed."
|
|
1936
|
+
};
|
|
1937
|
+
const delimiter = script[1] ?? "";
|
|
1938
|
+
if (!delimiter || /[A-Za-z0-9\\\n]/.test(delimiter)) return {
|
|
1939
|
+
allowed: false,
|
|
1940
|
+
reason: "inspect_command rejected 'sed' because the substitution delimiter is invalid."
|
|
1941
|
+
};
|
|
1942
|
+
const delimiters = findUnescapedDelimiterIndexes(script, delimiter);
|
|
1943
|
+
if (delimiters.length < 3) return {
|
|
1944
|
+
allowed: false,
|
|
1945
|
+
reason: "inspect_command rejected 'sed' because the substitution script is incomplete."
|
|
1946
|
+
};
|
|
1947
|
+
const flags = script.slice(delimiters[2] + 1);
|
|
1948
|
+
if (/[^gIpM0-9]/.test(flags) || /[ew]/.test(flags)) return {
|
|
1949
|
+
allowed: false,
|
|
1950
|
+
reason: "inspect_command rejected 'sed' because substitution flags are unsafe."
|
|
1951
|
+
};
|
|
1952
|
+
return { allowed: true };
|
|
1953
|
+
}
|
|
1954
|
+
function findUnescapedDelimiterIndexes(script, delimiter) {
|
|
1955
|
+
const indexes = [];
|
|
1956
|
+
for (let index = 0; index < script.length; index += 1) {
|
|
1957
|
+
if (script[index] !== delimiter) continue;
|
|
1958
|
+
let slashCount = 0;
|
|
1959
|
+
for (let back = index - 1; back >= 0 && script[back] === "\\"; back -= 1) slashCount += 1;
|
|
1960
|
+
if (slashCount % 2 === 0) indexes.push(index);
|
|
1961
|
+
}
|
|
1962
|
+
return indexes;
|
|
1963
|
+
}
|
|
1876
1964
|
function validateGenericCommandArgs(command, context, optionsWithValues, knownPathlessWords = /* @__PURE__ */ new Set()) {
|
|
1877
1965
|
for (let index = 0; index < command.args.length; index += 1) {
|
|
1878
1966
|
const arg = command.args[index] ?? "";
|
|
@@ -2310,6 +2398,256 @@ function resolveWorkspaceScopedPath$1(workspaceRoot, path) {
|
|
|
2310
2398
|
relativePath: relativePath || "."
|
|
2311
2399
|
};
|
|
2312
2400
|
}
|
|
2401
|
+
//#endregion
|
|
2402
|
+
//#region src/cli/ui.ts
|
|
2403
|
+
const colors = {
|
|
2404
|
+
bgSoftGray: "\x1B[48;5;236m",
|
|
2405
|
+
blue: "\x1B[34m",
|
|
2406
|
+
cyan: "\x1B[36m",
|
|
2407
|
+
darkGray: "\x1B[90m",
|
|
2408
|
+
dim: "\x1B[2m",
|
|
2409
|
+
green: "\x1B[32m",
|
|
2410
|
+
orange: "\x1B[38;5;208m",
|
|
2411
|
+
purple: "\x1B[35m",
|
|
2412
|
+
red: "\x1B[31m",
|
|
2413
|
+
reset: "\x1B[0m",
|
|
2414
|
+
resetForeground: "\x1B[39m",
|
|
2415
|
+
yellow: "\x1B[33m"
|
|
2416
|
+
};
|
|
2417
|
+
const ui = {
|
|
2418
|
+
heading(text) {
|
|
2419
|
+
return color(`Topchester ${text}`, "cyan");
|
|
2420
|
+
},
|
|
2421
|
+
label(text) {
|
|
2422
|
+
return color(text, "dim");
|
|
2423
|
+
},
|
|
2424
|
+
muted(text) {
|
|
2425
|
+
return color(text, "darkGray");
|
|
2426
|
+
},
|
|
2427
|
+
model(text) {
|
|
2428
|
+
return color(text, "blue");
|
|
2429
|
+
},
|
|
2430
|
+
modelInline(text) {
|
|
2431
|
+
if (!shouldUseColor()) return text;
|
|
2432
|
+
return `${colors.blue}${text}${colors.resetForeground}`;
|
|
2433
|
+
},
|
|
2434
|
+
ok(text) {
|
|
2435
|
+
return color(text, "green");
|
|
2436
|
+
},
|
|
2437
|
+
warn(text) {
|
|
2438
|
+
return color(text, "yellow");
|
|
2439
|
+
},
|
|
2440
|
+
error(text) {
|
|
2441
|
+
return color(text, "red");
|
|
2442
|
+
},
|
|
2443
|
+
softBackground(text) {
|
|
2444
|
+
return color(text, "bgSoftGray");
|
|
2445
|
+
},
|
|
2446
|
+
async spinner(text, action) {
|
|
2447
|
+
return withStatusLine(text, action, void 0, 80, false);
|
|
2448
|
+
},
|
|
2449
|
+
async progress(text, action) {
|
|
2450
|
+
let latest = text;
|
|
2451
|
+
return withStatusLine(text, () => action((message) => {
|
|
2452
|
+
latest = message;
|
|
2453
|
+
}), () => latest, 80, true);
|
|
2454
|
+
}
|
|
2455
|
+
};
|
|
2456
|
+
async function withStatusLine(text, action, getText = () => text, progressEveryMs = 80, emitPlainProgress = false) {
|
|
2457
|
+
if (!shouldUseColor()) {
|
|
2458
|
+
if (!emitPlainProgress) return action();
|
|
2459
|
+
const timer = setInterval(() => {
|
|
2460
|
+
stderr.write(`${getText()}\n`);
|
|
2461
|
+
}, Math.max(progressEveryMs, 5e3));
|
|
2462
|
+
try {
|
|
2463
|
+
return await action();
|
|
2464
|
+
} finally {
|
|
2465
|
+
clearInterval(timer);
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
const frames = [
|
|
2469
|
+
"⠋",
|
|
2470
|
+
"⠙",
|
|
2471
|
+
"⠹",
|
|
2472
|
+
"⠸",
|
|
2473
|
+
"⠼",
|
|
2474
|
+
"⠴",
|
|
2475
|
+
"⠦",
|
|
2476
|
+
"⠧",
|
|
2477
|
+
"⠇",
|
|
2478
|
+
"⠏"
|
|
2479
|
+
];
|
|
2480
|
+
let index = 0;
|
|
2481
|
+
stderr.write(`${color(frames[index], "cyan")} ${getText()}`);
|
|
2482
|
+
const timer = setInterval(() => {
|
|
2483
|
+
index = (index + 1) % frames.length;
|
|
2484
|
+
stderr.write(`\r\u001b[2K${color(frames[index], "cyan")} ${getText()}`);
|
|
2485
|
+
}, progressEveryMs);
|
|
2486
|
+
try {
|
|
2487
|
+
return await action();
|
|
2488
|
+
} finally {
|
|
2489
|
+
clearInterval(timer);
|
|
2490
|
+
stderr.write(`\r\u001b[2K`);
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
function color(text, colorName) {
|
|
2494
|
+
if (!shouldUseColor()) return text;
|
|
2495
|
+
return `${colors[colorName]}${text}${colors.reset}`;
|
|
2496
|
+
}
|
|
2497
|
+
function shouldUseColor() {
|
|
2498
|
+
if (process.env.NO_COLOR) return false;
|
|
2499
|
+
if (process.env.FORCE_COLOR && process.env.FORCE_COLOR !== "0") return true;
|
|
2500
|
+
return stdout.isTTY === true;
|
|
2501
|
+
}
|
|
2502
|
+
//#endregion
|
|
2503
|
+
//#region src/agent/task-plan.ts
|
|
2504
|
+
const planTodoStatusSchema = z.enum([
|
|
2505
|
+
"pending",
|
|
2506
|
+
"in_progress",
|
|
2507
|
+
"completed"
|
|
2508
|
+
]);
|
|
2509
|
+
const taskPlanItemSchema = z.object({
|
|
2510
|
+
text: z.string().trim().min(1, "Plan item text cannot be empty."),
|
|
2511
|
+
status: planTodoStatusSchema
|
|
2512
|
+
});
|
|
2513
|
+
const planTodoArgsSchema = z.object({ items: z.array(taskPlanItemSchema).max(20, "Plan updates are limited to 20 items.") }).superRefine((args, context) => {
|
|
2514
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2515
|
+
let inProgressCount = 0;
|
|
2516
|
+
let incompleteCount = 0;
|
|
2517
|
+
args.items.forEach((item, index) => {
|
|
2518
|
+
const key = item.text.toLocaleLowerCase("en");
|
|
2519
|
+
if (seen.has(key)) context.addIssue({
|
|
2520
|
+
code: "custom",
|
|
2521
|
+
message: "Plan item text must be unique.",
|
|
2522
|
+
path: [
|
|
2523
|
+
"items",
|
|
2524
|
+
index,
|
|
2525
|
+
"text"
|
|
2526
|
+
]
|
|
2527
|
+
});
|
|
2528
|
+
seen.add(key);
|
|
2529
|
+
if (item.status === "in_progress") inProgressCount += 1;
|
|
2530
|
+
if (item.status !== "completed") incompleteCount += 1;
|
|
2531
|
+
});
|
|
2532
|
+
if (inProgressCount > 1) context.addIssue({
|
|
2533
|
+
code: "custom",
|
|
2534
|
+
message: "At most one plan item can be in_progress.",
|
|
2535
|
+
path: ["items"]
|
|
2536
|
+
});
|
|
2537
|
+
if (args.items.length > 0 && incompleteCount > 0 && inProgressCount === 0) context.addIssue({
|
|
2538
|
+
code: "custom",
|
|
2539
|
+
message: "A non-completed plan must have exactly one in_progress item.",
|
|
2540
|
+
path: ["items"]
|
|
2541
|
+
});
|
|
2542
|
+
});
|
|
2543
|
+
function createEmptyTaskPlanState(now = /* @__PURE__ */ new Date()) {
|
|
2544
|
+
return {
|
|
2545
|
+
items: [],
|
|
2546
|
+
updatedAt: now.toISOString()
|
|
2547
|
+
};
|
|
2548
|
+
}
|
|
2549
|
+
function applyTaskPlanUpdate(_previous, args, now = /* @__PURE__ */ new Date()) {
|
|
2550
|
+
return {
|
|
2551
|
+
items: planTodoArgsSchema.parse(args).items.map((item) => ({
|
|
2552
|
+
text: item.text,
|
|
2553
|
+
status: item.status
|
|
2554
|
+
})),
|
|
2555
|
+
updatedAt: now.toISOString()
|
|
2556
|
+
};
|
|
2557
|
+
}
|
|
2558
|
+
function createTaskPlanController(initialState = createEmptyTaskPlanState(), now = () => /* @__PURE__ */ new Date()) {
|
|
2559
|
+
let state = initialState;
|
|
2560
|
+
return {
|
|
2561
|
+
update(args) {
|
|
2562
|
+
state = applyTaskPlanUpdate(state, args, now());
|
|
2563
|
+
return state;
|
|
2564
|
+
},
|
|
2565
|
+
get() {
|
|
2566
|
+
return state;
|
|
2567
|
+
}
|
|
2568
|
+
};
|
|
2569
|
+
}
|
|
2570
|
+
function summarizeTaskPlan(state) {
|
|
2571
|
+
const pendingCount = state.items.filter((item) => item.status === "pending").length;
|
|
2572
|
+
const inProgressCount = state.items.filter((item) => item.status === "in_progress").length;
|
|
2573
|
+
const completedCount = state.items.filter((item) => item.status === "completed").length;
|
|
2574
|
+
const currentItem = state.items.find((item) => item.status === "in_progress")?.text;
|
|
2575
|
+
return {
|
|
2576
|
+
pendingCount,
|
|
2577
|
+
inProgressCount,
|
|
2578
|
+
completedCount,
|
|
2579
|
+
...currentItem === void 0 ? {} : { currentItem }
|
|
2580
|
+
};
|
|
2581
|
+
}
|
|
2582
|
+
function hasOpenTaskPlan(state) {
|
|
2583
|
+
return Boolean(state && state.items.some((item) => item.status !== "completed"));
|
|
2584
|
+
}
|
|
2585
|
+
function formatTaskPlanForPrompt(state) {
|
|
2586
|
+
const summary = summarizeTaskPlan(state);
|
|
2587
|
+
return [
|
|
2588
|
+
"Plan updated",
|
|
2589
|
+
`pending: ${summary.pendingCount}`,
|
|
2590
|
+
`in_progress: ${summary.inProgressCount}`,
|
|
2591
|
+
`completed: ${summary.completedCount}`,
|
|
2592
|
+
summary.currentItem ? `current: ${summary.currentItem}` : ""
|
|
2593
|
+
].filter(Boolean).join("\n");
|
|
2594
|
+
}
|
|
2595
|
+
function detectTaskPlanChange(previous, next) {
|
|
2596
|
+
const hadPlan = Boolean(previous && previous.items.length > 0);
|
|
2597
|
+
const hasPlan = Boolean(next && next.items.length > 0);
|
|
2598
|
+
if (!hadPlan && hasPlan) return "created";
|
|
2599
|
+
if (hadPlan && !hasPlan) return "cleared";
|
|
2600
|
+
if (hadPlan && hasPlan) return "updated";
|
|
2601
|
+
return "unchanged";
|
|
2602
|
+
}
|
|
2603
|
+
function formatTaskPlanNotice(change, state) {
|
|
2604
|
+
if (change === "unchanged") return;
|
|
2605
|
+
if (change === "cleared" || state.items.length === 0) return "todo plan cleared";
|
|
2606
|
+
const summary = summarizeTaskPlan(state);
|
|
2607
|
+
if (summary.inProgressCount === 0 && summary.pendingCount === 0) return "todo plan completed";
|
|
2608
|
+
const prefix = change === "created" ? "todo plan created" : "todo plan updated";
|
|
2609
|
+
return summary.currentItem ? `${prefix}: ${summary.currentItem}` : prefix;
|
|
2610
|
+
}
|
|
2611
|
+
function formatTaskPlanForTui(state, width, visibleLimit = 6) {
|
|
2612
|
+
if (state.items.length === 0) return [];
|
|
2613
|
+
const itemWidth = Math.max(1, Math.max(12, width) - 6);
|
|
2614
|
+
const visibleItems = state.items.slice(0, visibleLimit);
|
|
2615
|
+
const lines = visibleItems.map((item) => formatTaskPlanTuiLine(item, truncateText(item.text, itemWidth)));
|
|
2616
|
+
const remaining = state.items.length - visibleItems.length;
|
|
2617
|
+
if (remaining > 0) lines.push(ui.muted(` +${remaining} more`));
|
|
2618
|
+
return lines;
|
|
2619
|
+
}
|
|
2620
|
+
function formatTaskPlanTuiLine(item, text) {
|
|
2621
|
+
switch (item.status) {
|
|
2622
|
+
case "completed": return ` ${ui.ok("[x]")} ${ui.muted(text)}`;
|
|
2623
|
+
case "in_progress": return ` ${ui.ok("[>]")} ${ui.ok(text)}`;
|
|
2624
|
+
case "pending": return ` ${ui.muted("[ ]")} ${text}`;
|
|
2625
|
+
}
|
|
2626
|
+
}
|
|
2627
|
+
function truncateText(text, width) {
|
|
2628
|
+
if (text.length <= width) return text;
|
|
2629
|
+
if (width <= 3) return ".".repeat(Math.max(0, width));
|
|
2630
|
+
return `${text.slice(0, width - 3)}...`;
|
|
2631
|
+
}
|
|
2632
|
+
//#endregion
|
|
2633
|
+
//#region src/agent/tools/plan-todo.ts
|
|
2634
|
+
const planTodoTool = defineTool({
|
|
2635
|
+
name: "plan_todo",
|
|
2636
|
+
description: "Replace the visible session task plan for multi-step work.",
|
|
2637
|
+
prompt: "plan_todo: replace the visible session task plan for non-trivial multi-step work; keep 2-6 short items, exactly one in_progress item while work remains, and use [] only to clear. To use it, reply with only JSON: {\"tool\":\"plan_todo\",\"args\":{\"items\":[{\"text\":\"Inspect relevant files\",\"status\":\"in_progress\"},{\"text\":\"Implement focused change\",\"status\":\"pending\"}]}}",
|
|
2638
|
+
argsSchema: planTodoArgsSchema,
|
|
2639
|
+
async execute(context, args) {
|
|
2640
|
+
if (!context.taskPlan) throw new Error("plan_todo requires runtime task-plan state.");
|
|
2641
|
+
const plan = context.taskPlan.update(args);
|
|
2642
|
+
const summary = summarizeTaskPlan(plan);
|
|
2643
|
+
return {
|
|
2644
|
+
tool: "plan_todo",
|
|
2645
|
+
content: formatTaskPlanForPrompt(plan),
|
|
2646
|
+
plan,
|
|
2647
|
+
...summary
|
|
2648
|
+
};
|
|
2649
|
+
}
|
|
2650
|
+
});
|
|
2313
2651
|
const readFileTool = defineTool({
|
|
2314
2652
|
name: "read_file",
|
|
2315
2653
|
description: "Read a UTF-8 file inside the workspace.",
|
|
@@ -2333,14 +2671,14 @@ async function readWorkspaceFile(workspaceRoot, path) {
|
|
|
2333
2671
|
}
|
|
2334
2672
|
const writeFileTool = defineTool({
|
|
2335
2673
|
name: "write_file",
|
|
2336
|
-
description: "Create a new UTF-8 file inside the workspace, or explicitly replace one
|
|
2337
|
-
prompt: "write_file: create a new UTF-8 file inside the workspace by default; use edit_file for targeted changes to existing files
|
|
2674
|
+
description: "Create a new UTF-8 file inside the workspace, or explicitly replace one. For overwrite:true, expected_current_hash must be the current/pre-write hash from read_file, never a predicted post-write hash.",
|
|
2675
|
+
prompt: "write_file: create a new UTF-8 file inside the workspace by default; use edit_file for targeted changes to existing files; pass create_parent_dirs:true only when creating the folder path is intended. Replace an existing whole file only with overwrite:true and expected_current_hash set to the current/pre-write hash returned by the latest read_file for that file; never invent it or use a predicted after-write hash. To create a file, reply with only JSON: {\"tool\":\"write_file\",\"args\":{\"path\":\"test/example.test.ts\",\"content\":\"import { it, expect } from \\\"vitest\\\";\\n\\nit(\\\"works\\\", () => {\\n expect(true).toBe(true);\\n});\\n\",\"create_parent_dirs\":true}}",
|
|
2338
2676
|
argsSchema: z.object({
|
|
2339
|
-
path: z.string(),
|
|
2340
|
-
content: z.string(),
|
|
2341
|
-
create_parent_dirs: z.boolean().optional(),
|
|
2342
|
-
overwrite: z.boolean().optional(),
|
|
2343
|
-
|
|
2677
|
+
path: z.string().describe("Workspace-relative path to write."),
|
|
2678
|
+
content: z.string().describe("Complete UTF-8 file content to write."),
|
|
2679
|
+
create_parent_dirs: z.boolean().optional().describe("Create missing parent directories only when that is explicitly intended."),
|
|
2680
|
+
overwrite: z.boolean().optional().describe("Set true only for intentional whole-file replacement."),
|
|
2681
|
+
expected_current_hash: z.string().optional().describe("Required with overwrite:true. Use the current file hash returned by the latest read_file result for this file. This is checked before writing and is not the hash after the write.")
|
|
2344
2682
|
}),
|
|
2345
2683
|
execute: (context, args) => writeWorkspaceFile(context.workspaceRoot, args, { logger: context.logger })
|
|
2346
2684
|
});
|
|
@@ -2396,12 +2734,12 @@ async function writeWorkspaceFile(workspaceRoot, args, options = {}) {
|
|
|
2396
2734
|
});
|
|
2397
2735
|
}
|
|
2398
2736
|
async function overwriteExistingFile(workspaceRoot, scopedPath, args, existingTarget, options) {
|
|
2399
|
-
if (!args.
|
|
2737
|
+
if (!args.expected_current_hash) throw new Error(`write_file overwrite requires expected_current_hash for ${scopedPath.relativePath}.`);
|
|
2400
2738
|
if (!existingTarget) throw new Error(`write_file overwrite requires an existing file: ${scopedPath.relativePath}`);
|
|
2401
2739
|
if (!existingTarget.isFile()) throw new Error(`write_file overwrite requires a regular file: ${scopedPath.relativePath}`);
|
|
2402
2740
|
const beforeBytes = await readFile(scopedPath.path);
|
|
2403
2741
|
const beforeHash = hashBytes(beforeBytes);
|
|
2404
|
-
if (args.
|
|
2742
|
+
if (args.expected_current_hash !== beforeHash) throw new Error(`write_file expected_current_hash did not match ${scopedPath.relativePath}.`);
|
|
2405
2743
|
const beforeLineCount = countLogicalLines$1(decodeUtf8(scopedPath.relativePath, beforeBytes));
|
|
2406
2744
|
const afterBytes = encodeUtf8Text(args.content);
|
|
2407
2745
|
const afterHash = hashBytes(afterBytes);
|
|
@@ -2543,6 +2881,7 @@ function isNodeError$2(error) {
|
|
|
2543
2881
|
//#endregion
|
|
2544
2882
|
//#region src/agent/tools/registry.ts
|
|
2545
2883
|
const toolRegistry = {
|
|
2884
|
+
[planTodoTool.name]: planTodoTool,
|
|
2546
2885
|
[readFileTool.name]: readFileTool,
|
|
2547
2886
|
[listFilesTool.name]: listFilesTool,
|
|
2548
2887
|
[grepTool.name]: grepTool,
|
|
@@ -2769,16 +3108,23 @@ var ModelGateway = class ModelGateway {
|
|
|
2769
3108
|
}
|
|
2770
3109
|
async generateText(request) {
|
|
2771
3110
|
const resolved = this.resolveModel(request.purpose);
|
|
3111
|
+
const result = await generateText({
|
|
3112
|
+
model: resolved.model,
|
|
3113
|
+
system: request.system,
|
|
3114
|
+
prompt: request.prompt,
|
|
3115
|
+
abortSignal: request.abortSignal
|
|
3116
|
+
});
|
|
3117
|
+
const usage = normalizeUsage(result.usage, {
|
|
3118
|
+
providerId: resolved.providerId,
|
|
3119
|
+
providerConfig: resolved.providerConfig,
|
|
3120
|
+
responseBody: result.response.body
|
|
3121
|
+
});
|
|
2772
3122
|
return {
|
|
2773
|
-
text:
|
|
2774
|
-
model: resolved.model,
|
|
2775
|
-
system: request.system,
|
|
2776
|
-
prompt: request.prompt,
|
|
2777
|
-
abortSignal: request.abortSignal
|
|
2778
|
-
})).text,
|
|
3123
|
+
text: result.text,
|
|
2779
3124
|
providerId: resolved.providerId,
|
|
2780
3125
|
modelId: resolved.modelId,
|
|
2781
|
-
purpose: resolved.purpose
|
|
3126
|
+
purpose: resolved.purpose,
|
|
3127
|
+
...usage ? { usage } : {}
|
|
2782
3128
|
};
|
|
2783
3129
|
}
|
|
2784
3130
|
async generateAgentStep(request) {
|
|
@@ -2811,7 +3157,7 @@ var ModelGateway = class ModelGateway {
|
|
|
2811
3157
|
}
|
|
2812
3158
|
return result;
|
|
2813
3159
|
} catch (error) {
|
|
2814
|
-
const reason = formatErrorMessage$
|
|
3160
|
+
const reason = formatErrorMessage$2(error);
|
|
2815
3161
|
attempts.push({
|
|
2816
3162
|
protocol: "native-openai-compatible",
|
|
2817
3163
|
status: "failed",
|
|
@@ -2847,6 +3193,11 @@ var ModelGateway = class ModelGateway {
|
|
|
2847
3193
|
providerOptions,
|
|
2848
3194
|
abortSignal: request.abortSignal
|
|
2849
3195
|
});
|
|
3196
|
+
const usage = normalizeUsage(result.usage, {
|
|
3197
|
+
providerId: resolved.providerId,
|
|
3198
|
+
providerConfig: resolved.providerConfig,
|
|
3199
|
+
responseBody: result.response.body
|
|
3200
|
+
});
|
|
2850
3201
|
const toolCalls = result.toolCalls.map((call, index) => {
|
|
2851
3202
|
const parsed = parseNativeToolCall(call.toolName, call.input);
|
|
2852
3203
|
if (!parsed) throw new Error(`Native tool call for ${call.toolName} did not match the registered schema.`);
|
|
@@ -2866,6 +3217,7 @@ var ModelGateway = class ModelGateway {
|
|
|
2866
3217
|
providerId: resolved.providerId,
|
|
2867
3218
|
modelId: resolved.modelId,
|
|
2868
3219
|
purpose: resolved.purpose,
|
|
3220
|
+
...usage ? { usage } : {},
|
|
2869
3221
|
toolCalls,
|
|
2870
3222
|
toolProtocol: "native-openai-compatible",
|
|
2871
3223
|
protocolAttempts: attempts,
|
|
@@ -2881,6 +3233,11 @@ var ModelGateway = class ModelGateway {
|
|
|
2881
3233
|
prompt: request.prompt,
|
|
2882
3234
|
abortSignal: request.abortSignal
|
|
2883
3235
|
});
|
|
3236
|
+
const usage = normalizeUsage(result.usage, {
|
|
3237
|
+
providerId: resolved.providerId,
|
|
3238
|
+
providerConfig: resolved.providerConfig,
|
|
3239
|
+
responseBody: result.response.body
|
|
3240
|
+
});
|
|
2884
3241
|
const parsed = parseToolCallWithSource(result.text, allowedSources);
|
|
2885
3242
|
const defaultProtocol = allowedSources.length === 1 && allowedSources[0] === "text-xml" ? "text-xml" : "text-json";
|
|
2886
3243
|
const toolProtocol = parsed?.source === "text-xml" ? "text-xml" : parsed ? "text-json" : defaultProtocol;
|
|
@@ -2894,6 +3251,7 @@ var ModelGateway = class ModelGateway {
|
|
|
2894
3251
|
providerId: resolved.providerId,
|
|
2895
3252
|
modelId: resolved.modelId,
|
|
2896
3253
|
purpose: resolved.purpose,
|
|
3254
|
+
...usage ? { usage } : {},
|
|
2897
3255
|
toolCalls: parsed ? [{
|
|
2898
3256
|
id: `${parsed.source}-0`,
|
|
2899
3257
|
tool: parsed.call.tool,
|
|
@@ -2922,6 +3280,9 @@ function buildNativeProviderOptions(providerId, config) {
|
|
|
2922
3280
|
function shouldApplyOpenRouterRoutingOptions(providerId, config) {
|
|
2923
3281
|
if (config.openRouterToolRouting === "force") return true;
|
|
2924
3282
|
if (config.openRouterToolRouting === "off") return false;
|
|
3283
|
+
return isOpenRouterProvider(providerId, config);
|
|
3284
|
+
}
|
|
3285
|
+
function isOpenRouterProvider(providerId, config) {
|
|
2925
3286
|
return providerId.toLowerCase().includes("openrouter") || config.baseURL.toLowerCase().includes("openrouter.ai");
|
|
2926
3287
|
}
|
|
2927
3288
|
function hasOpenRouterRoutingOptions(providerOptions, providerId) {
|
|
@@ -2929,14 +3290,32 @@ function hasOpenRouterRoutingOptions(providerOptions, providerId) {
|
|
|
2929
3290
|
return Boolean(options && typeof options === "object" && "provider" in options);
|
|
2930
3291
|
}
|
|
2931
3292
|
function isNativeToolFallbackError(error) {
|
|
2932
|
-
const message = formatErrorMessage$
|
|
3293
|
+
const message = formatErrorMessage$2(error).toLowerCase();
|
|
2933
3294
|
return message.includes("tool") || message.includes("function") || message.includes("parallel_tool_calls") || message.includes("tool_choice") || message.includes("requested parameters") || message.includes("provider routing") || message.includes("provider-selection") || message.includes("invalid request");
|
|
2934
3295
|
}
|
|
2935
3296
|
function extractWarningMessages(warnings) {
|
|
2936
3297
|
if (!Array.isArray(warnings)) return [];
|
|
2937
|
-
return warnings.map((warning) => formatErrorMessage$
|
|
3298
|
+
return warnings.map((warning) => formatErrorMessage$2(warning));
|
|
3299
|
+
}
|
|
3300
|
+
function normalizeUsage(usage, context) {
|
|
3301
|
+
const costUsd = context && isOpenRouterProvider(context.providerId, context.providerConfig) ? extractOpenRouterCost(context.responseBody) : void 0;
|
|
3302
|
+
if (!usage) return costUsd === void 0 ? void 0 : { costUsd };
|
|
3303
|
+
const normalized = {
|
|
3304
|
+
...typeof usage.inputTokens === "number" ? { inputTokens: usage.inputTokens } : {},
|
|
3305
|
+
...typeof usage.outputTokens === "number" ? { outputTokens: usage.outputTokens } : {},
|
|
3306
|
+
...typeof usage.totalTokens === "number" ? { totalTokens: usage.totalTokens } : {},
|
|
3307
|
+
...costUsd === void 0 ? {} : { costUsd }
|
|
3308
|
+
};
|
|
3309
|
+
return Object.keys(normalized).length > 0 ? normalized : void 0;
|
|
2938
3310
|
}
|
|
2939
|
-
function
|
|
3311
|
+
function extractOpenRouterCost(responseBody) {
|
|
3312
|
+
if (!responseBody || typeof responseBody !== "object") return;
|
|
3313
|
+
const usage = responseBody.usage;
|
|
3314
|
+
if (!usage || typeof usage !== "object") return;
|
|
3315
|
+
const cost = usage.cost;
|
|
3316
|
+
return typeof cost === "number" && Number.isFinite(cost) ? cost : void 0;
|
|
3317
|
+
}
|
|
3318
|
+
function formatErrorMessage$2(error) {
|
|
2940
3319
|
return error instanceof Error ? error.message : String(error);
|
|
2941
3320
|
}
|
|
2942
3321
|
//#endregion
|
|
@@ -2946,8 +3325,6 @@ const modelPurposeSchema = z.enum([
|
|
|
2946
3325
|
"agent.fast",
|
|
2947
3326
|
"kb.scan",
|
|
2948
3327
|
"kb.summarize",
|
|
2949
|
-
"kb.extract",
|
|
2950
|
-
"kb.embed",
|
|
2951
3328
|
"fallback"
|
|
2952
3329
|
]);
|
|
2953
3330
|
const modelPurposes = modelPurposeSchema.options;
|
|
@@ -3042,7 +3419,7 @@ function readConfigFile(path) {
|
|
|
3042
3419
|
try {
|
|
3043
3420
|
return parse(readFileSync(path, "utf8"));
|
|
3044
3421
|
} catch (error) {
|
|
3045
|
-
throw new Error(`Invalid Topchester config at ${path}: ${formatErrorMessage(error)}`);
|
|
3422
|
+
throw new Error(`Invalid Topchester config at ${path}: ${formatErrorMessage$1(error)}`);
|
|
3046
3423
|
}
|
|
3047
3424
|
}
|
|
3048
3425
|
function parseConfigFile(path, value) {
|
|
@@ -3078,6 +3455,7 @@ function normalizeConfigInput(value) {
|
|
|
3078
3455
|
ensureKnownProvider(providers, kbSummarizeModelRef.provider);
|
|
3079
3456
|
delete models["kb.summarize"];
|
|
3080
3457
|
}
|
|
3458
|
+
applyKnownProviderDefaults(providers);
|
|
3081
3459
|
return {
|
|
3082
3460
|
...value,
|
|
3083
3461
|
models: {
|
|
@@ -3134,6 +3512,20 @@ function ensureKnownProvider(providers, provider) {
|
|
|
3134
3512
|
}
|
|
3135
3513
|
};
|
|
3136
3514
|
}
|
|
3515
|
+
function applyKnownProviderDefaults(providers) {
|
|
3516
|
+
for (const [providerId, provider] of Object.entries(providers)) {
|
|
3517
|
+
if (!isPlainObject(provider) || provider.type !== "openai-compatible" || typeof provider.baseURL !== "string") continue;
|
|
3518
|
+
if (isOpenAIProvider(providerId, provider.baseURL)) {
|
|
3519
|
+
provider.supportsStructuredOutputs ??= true;
|
|
3520
|
+
provider.toolProtocol ??= "native";
|
|
3521
|
+
}
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
3524
|
+
function isOpenAIProvider(providerId, baseURL) {
|
|
3525
|
+
const normalizedProvider = providerId.toLowerCase();
|
|
3526
|
+
const normalizedBaseURL = baseURL.toLowerCase();
|
|
3527
|
+
return normalizedProvider === "openai" || normalizedProvider === "gpt" || normalizedProvider.includes("openai") || normalizedBaseURL.includes("api.openai.com");
|
|
3528
|
+
}
|
|
3137
3529
|
function deepMerge(base, override, path = []) {
|
|
3138
3530
|
if (Array.isArray(base) && Array.isArray(override)) return path.join(".") === "ignore.paths" ? [...base, ...override] : override;
|
|
3139
3531
|
if (!isPlainObject(base) || !isPlainObject(override)) return override;
|
|
@@ -3147,7 +3539,7 @@ function isPlainObject(value) {
|
|
|
3147
3539
|
function formatZodIssue(issue) {
|
|
3148
3540
|
return `${issue.path.length > 0 ? issue.path.join(".") : "<root>"}: ${issue.message}`;
|
|
3149
3541
|
}
|
|
3150
|
-
function formatErrorMessage(error) {
|
|
3542
|
+
function formatErrorMessage$1(error) {
|
|
3151
3543
|
return error instanceof Error ? error.message : String(error);
|
|
3152
3544
|
}
|
|
3153
3545
|
//#endregion
|
|
@@ -3241,107 +3633,6 @@ function normalizeModelGatewayConfig(config) {
|
|
|
3241
3633
|
};
|
|
3242
3634
|
}
|
|
3243
3635
|
//#endregion
|
|
3244
|
-
//#region src/cli/ui.ts
|
|
3245
|
-
const colors = {
|
|
3246
|
-
bgSoftGray: "\x1B[48;5;236m",
|
|
3247
|
-
blue: "\x1B[34m",
|
|
3248
|
-
cyan: "\x1B[36m",
|
|
3249
|
-
darkGray: "\x1B[90m",
|
|
3250
|
-
dim: "\x1B[2m",
|
|
3251
|
-
green: "\x1B[32m",
|
|
3252
|
-
orange: "\x1B[38;5;208m",
|
|
3253
|
-
purple: "\x1B[35m",
|
|
3254
|
-
red: "\x1B[31m",
|
|
3255
|
-
reset: "\x1B[0m",
|
|
3256
|
-
resetForeground: "\x1B[39m",
|
|
3257
|
-
yellow: "\x1B[33m"
|
|
3258
|
-
};
|
|
3259
|
-
const ui = {
|
|
3260
|
-
heading(text) {
|
|
3261
|
-
return color(`Topchester ${text}`, "cyan");
|
|
3262
|
-
},
|
|
3263
|
-
label(text) {
|
|
3264
|
-
return color(text, "dim");
|
|
3265
|
-
},
|
|
3266
|
-
muted(text) {
|
|
3267
|
-
return color(text, "darkGray");
|
|
3268
|
-
},
|
|
3269
|
-
model(text) {
|
|
3270
|
-
return color(text, "blue");
|
|
3271
|
-
},
|
|
3272
|
-
modelInline(text) {
|
|
3273
|
-
if (!shouldUseColor()) return text;
|
|
3274
|
-
return `${colors.blue}${text}${colors.resetForeground}`;
|
|
3275
|
-
},
|
|
3276
|
-
ok(text) {
|
|
3277
|
-
return color(text, "green");
|
|
3278
|
-
},
|
|
3279
|
-
warn(text) {
|
|
3280
|
-
return color(text, "yellow");
|
|
3281
|
-
},
|
|
3282
|
-
error(text) {
|
|
3283
|
-
return color(text, "red");
|
|
3284
|
-
},
|
|
3285
|
-
softBackground(text) {
|
|
3286
|
-
return color(text, "bgSoftGray");
|
|
3287
|
-
},
|
|
3288
|
-
async spinner(text, action) {
|
|
3289
|
-
return withStatusLine(text, action, void 0, 80, false);
|
|
3290
|
-
},
|
|
3291
|
-
async progress(text, action) {
|
|
3292
|
-
let latest = text;
|
|
3293
|
-
return withStatusLine(text, () => action((message) => {
|
|
3294
|
-
latest = message;
|
|
3295
|
-
}), () => latest, 80, true);
|
|
3296
|
-
}
|
|
3297
|
-
};
|
|
3298
|
-
async function withStatusLine(text, action, getText = () => text, progressEveryMs = 80, emitPlainProgress = false) {
|
|
3299
|
-
if (!shouldUseColor()) {
|
|
3300
|
-
if (!emitPlainProgress) return action();
|
|
3301
|
-
const timer = setInterval(() => {
|
|
3302
|
-
stderr.write(`${getText()}\n`);
|
|
3303
|
-
}, Math.max(progressEveryMs, 5e3));
|
|
3304
|
-
try {
|
|
3305
|
-
return await action();
|
|
3306
|
-
} finally {
|
|
3307
|
-
clearInterval(timer);
|
|
3308
|
-
}
|
|
3309
|
-
}
|
|
3310
|
-
const frames = [
|
|
3311
|
-
"⠋",
|
|
3312
|
-
"⠙",
|
|
3313
|
-
"⠹",
|
|
3314
|
-
"⠸",
|
|
3315
|
-
"⠼",
|
|
3316
|
-
"⠴",
|
|
3317
|
-
"⠦",
|
|
3318
|
-
"⠧",
|
|
3319
|
-
"⠇",
|
|
3320
|
-
"⠏"
|
|
3321
|
-
];
|
|
3322
|
-
let index = 0;
|
|
3323
|
-
stderr.write(`${color(frames[index], "cyan")} ${getText()}`);
|
|
3324
|
-
const timer = setInterval(() => {
|
|
3325
|
-
index = (index + 1) % frames.length;
|
|
3326
|
-
stderr.write(`\r\u001b[2K${color(frames[index], "cyan")} ${getText()}`);
|
|
3327
|
-
}, progressEveryMs);
|
|
3328
|
-
try {
|
|
3329
|
-
return await action();
|
|
3330
|
-
} finally {
|
|
3331
|
-
clearInterval(timer);
|
|
3332
|
-
stderr.write(`\r\u001b[2K`);
|
|
3333
|
-
}
|
|
3334
|
-
}
|
|
3335
|
-
function color(text, colorName) {
|
|
3336
|
-
if (!shouldUseColor()) return text;
|
|
3337
|
-
return `${colors[colorName]}${text}${colors.reset}`;
|
|
3338
|
-
}
|
|
3339
|
-
function shouldUseColor() {
|
|
3340
|
-
if (process.env.NO_COLOR) return false;
|
|
3341
|
-
if (process.env.FORCE_COLOR && process.env.FORCE_COLOR !== "0") return true;
|
|
3342
|
-
return stdout.isTTY === true;
|
|
3343
|
-
}
|
|
3344
|
-
//#endregion
|
|
3345
3636
|
//#region src/knowledge/status.ts
|
|
3346
3637
|
function getKnowledgeStatus(workspaceRoot) {
|
|
3347
3638
|
const kbPathSource = process.env.TOPCHESTER_KB_DIR ? "env" : "default";
|
|
@@ -3674,6 +3965,14 @@ const l1ConfidenceLevels = [
|
|
|
3674
3965
|
"medium",
|
|
3675
3966
|
"high"
|
|
3676
3967
|
];
|
|
3968
|
+
const l1FileRoles = [
|
|
3969
|
+
"source",
|
|
3970
|
+
"test",
|
|
3971
|
+
"config",
|
|
3972
|
+
"doc",
|
|
3973
|
+
"script",
|
|
3974
|
+
"unknown"
|
|
3975
|
+
];
|
|
3677
3976
|
const nonEmptyStringSchema = z.string().min(1);
|
|
3678
3977
|
const sha256HashSchema = z.string().regex(/^sha256:[a-f0-9]{64}$/);
|
|
3679
3978
|
const isoUtcTimestampSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/).refine((value) => !Number.isNaN(Date.parse(value)), { message: "Expected a valid UTC ISO timestamp" });
|
|
@@ -3685,7 +3984,7 @@ const l1FileSymbolSchema = z.object({
|
|
|
3685
3984
|
kind: nonEmptyStringSchema,
|
|
3686
3985
|
name: nonEmptyStringSchema,
|
|
3687
3986
|
exported: z.boolean(),
|
|
3688
|
-
summary: nonEmptyStringSchema
|
|
3987
|
+
summary: nonEmptyStringSchema.optional()
|
|
3689
3988
|
}).strict();
|
|
3690
3989
|
const l1FileEvidenceSchema = z.object({
|
|
3691
3990
|
kind: nonEmptyStringSchema,
|
|
@@ -3702,6 +4001,7 @@ const l1FileEntrySchema = z.object({
|
|
|
3702
4001
|
size_bytes: z.number().int().nonnegative(),
|
|
3703
4002
|
last_scanned_at: isoUtcTimestampSchema,
|
|
3704
4003
|
scan_status: z.enum(l1FileScanStatuses),
|
|
4004
|
+
file_role: z.enum(l1FileRoles).default("unknown"),
|
|
3705
4005
|
summary: nonEmptyStringSchema,
|
|
3706
4006
|
responsibilities: z.array(nonEmptyStringSchema),
|
|
3707
4007
|
symbols: z.array(l1FileSymbolSchema),
|
|
@@ -3710,6 +4010,9 @@ const l1FileEntrySchema = z.object({
|
|
|
3710
4010
|
module_ids: z.array(l1ModuleIdSchema),
|
|
3711
4011
|
feature_ids: z.array(l1FeatureIdSchema),
|
|
3712
4012
|
test_ids: z.array(l1FileIdSchema),
|
|
4013
|
+
declared_test_targets: z.array(l1FileIdSchema).default([]),
|
|
4014
|
+
likely_test_targets: z.array(l1FileIdSchema).default([]),
|
|
4015
|
+
tested_by: z.array(l1FileIdSchema).default([]),
|
|
3713
4016
|
evidence: z.array(l1FileEvidenceSchema),
|
|
3714
4017
|
confidence: z.enum(l1ConfidenceLevels)
|
|
3715
4018
|
}).strict().refine((entry) => entry.id === `file:${entry.path}`, {
|
|
@@ -3777,6 +4080,123 @@ function formatCountProgress(label, completed, total, detail) {
|
|
|
3777
4080
|
return `${label} [${formatProgressBar(safeCompleted, safeTotal)}] ${safeCompleted}/${safeTotal} (${percent}%)${suffix}`;
|
|
3778
4081
|
}
|
|
3779
4082
|
//#endregion
|
|
4083
|
+
//#region src/knowledge/compiler/l1-postprocess.ts
|
|
4084
|
+
async function postProcessL1Entries(kbPath) {
|
|
4085
|
+
const entries = await loadL1Entries(kbPath);
|
|
4086
|
+
const entriesById = new Map(entries.map(({ entry }) => [entry.id, entry]));
|
|
4087
|
+
const entriesByPath = new Map(entries.map(({ entry }) => [entry.path, entry]));
|
|
4088
|
+
const testTargetsById = /* @__PURE__ */ new Map();
|
|
4089
|
+
for (const { entry } of entries) {
|
|
4090
|
+
if (inferL1FileRole(entry.path) !== "test") {
|
|
4091
|
+
testTargetsById.set(entry.id, {
|
|
4092
|
+
declared: [],
|
|
4093
|
+
likely: []
|
|
4094
|
+
});
|
|
4095
|
+
continue;
|
|
4096
|
+
}
|
|
4097
|
+
const declared = dedupeStrings$1([...entry.declared_test_targets.filter((id) => isExistingNonSelfFileId(id, entry.id, entriesById)), ...entry.imports.filter((id) => isExistingNonSelfFileId(id, entry.id, entriesById))]);
|
|
4098
|
+
const likely = dedupeStrings$1([...entry.likely_test_targets.filter((id) => isExistingNonSelfFileId(id, entry.id, entriesById)), ...inferLikelyTestTargets(entry.path, entriesByPath)]);
|
|
4099
|
+
testTargetsById.set(entry.id, {
|
|
4100
|
+
declared,
|
|
4101
|
+
likely
|
|
4102
|
+
});
|
|
4103
|
+
}
|
|
4104
|
+
const testedBy = /* @__PURE__ */ new Map();
|
|
4105
|
+
for (const [testId, links] of testTargetsById) for (const targetId of dedupeStrings$1([...links.declared, ...links.likely])) {
|
|
4106
|
+
const list = testedBy.get(targetId) ?? [];
|
|
4107
|
+
list.push(testId);
|
|
4108
|
+
testedBy.set(targetId, list);
|
|
4109
|
+
}
|
|
4110
|
+
let entriesUpdated = 0;
|
|
4111
|
+
let testLinksAdded = 0;
|
|
4112
|
+
for (const { entry, entryPath } of entries) {
|
|
4113
|
+
const fileRole = inferL1FileRole(entry.path);
|
|
4114
|
+
const links = testTargetsById.get(entry.id) ?? {
|
|
4115
|
+
declared: [],
|
|
4116
|
+
likely: []
|
|
4117
|
+
};
|
|
4118
|
+
const nextEntry = parseL1FileEntry({
|
|
4119
|
+
...entry,
|
|
4120
|
+
file_role: fileRole,
|
|
4121
|
+
declared_test_targets: links.declared,
|
|
4122
|
+
likely_test_targets: links.likely,
|
|
4123
|
+
tested_by: dedupeStrings$1(testedBy.get(entry.id) ?? []).sort()
|
|
4124
|
+
});
|
|
4125
|
+
testLinksAdded += nextEntry.declared_test_targets.length + nextEntry.likely_test_targets.length + nextEntry.tested_by.length;
|
|
4126
|
+
if (JSON.stringify(nextEntry) !== JSON.stringify(entry)) {
|
|
4127
|
+
await writeFile(entryPath, `${JSON.stringify(nextEntry, null, 2)}\n`);
|
|
4128
|
+
entriesUpdated += 1;
|
|
4129
|
+
}
|
|
4130
|
+
}
|
|
4131
|
+
return {
|
|
4132
|
+
entriesRead: entries.length,
|
|
4133
|
+
entriesUpdated,
|
|
4134
|
+
testLinksAdded
|
|
4135
|
+
};
|
|
4136
|
+
}
|
|
4137
|
+
function inferL1FileRole(path) {
|
|
4138
|
+
const lowerPath = path.toLowerCase();
|
|
4139
|
+
const name = basename(lowerPath);
|
|
4140
|
+
if (isTestPath(lowerPath)) return "test";
|
|
4141
|
+
if (lowerPath.startsWith("scripts/") || lowerPath.startsWith("script/") || lowerPath.endsWith(".sh")) return "script";
|
|
4142
|
+
if (lowerPath.endsWith(".md") || lowerPath.endsWith(".mdx") || lowerPath.startsWith("docs/")) return "doc";
|
|
4143
|
+
if (name === "package.json" || name.endsWith("lock.json") || name.endsWith("-lock.yaml") || name.endsWith(".config.ts") || name.endsWith(".config.js") || name.endsWith(".config.mjs") || name.endsWith(".config.cjs") || name === "tsconfig.json" || name.startsWith(".")) return "config";
|
|
4144
|
+
if (/\.(ts|tsx|js|jsx|mts|cts)$/.test(lowerPath)) return "source";
|
|
4145
|
+
return "unknown";
|
|
4146
|
+
}
|
|
4147
|
+
async function loadL1Entries(kbPath) {
|
|
4148
|
+
const entryPaths = await listJsonFiles$1(join(kbPath, "l1-files")).catch((error) => {
|
|
4149
|
+
if (isFileNotFoundError$3(error)) return [];
|
|
4150
|
+
throw error;
|
|
4151
|
+
});
|
|
4152
|
+
const entries = [];
|
|
4153
|
+
for (const entryPath of entryPaths) entries.push({
|
|
4154
|
+
entryPath,
|
|
4155
|
+
entry: parseL1FileEntry(JSON.parse(await readFile(entryPath, "utf8")))
|
|
4156
|
+
});
|
|
4157
|
+
return entries.sort((a, b) => a.entry.path.localeCompare(b.entry.path));
|
|
4158
|
+
}
|
|
4159
|
+
async function listJsonFiles$1(directory) {
|
|
4160
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
4161
|
+
const files = [];
|
|
4162
|
+
for (const entry of entries) {
|
|
4163
|
+
const entryPath = join(directory, entry.name);
|
|
4164
|
+
if (entry.isDirectory()) files.push(...await listJsonFiles$1(entryPath));
|
|
4165
|
+
else if (entry.isFile() && entry.name.endsWith(".json")) files.push(entryPath);
|
|
4166
|
+
}
|
|
4167
|
+
return files.sort();
|
|
4168
|
+
}
|
|
4169
|
+
function inferLikelyTestTargets(testPath, entriesByPath) {
|
|
4170
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
4171
|
+
const sourceLikePath = removeTestSuffix(testPath);
|
|
4172
|
+
candidates.add(sourceLikePath);
|
|
4173
|
+
for (const prefix of [
|
|
4174
|
+
"test/",
|
|
4175
|
+
"tests/",
|
|
4176
|
+
"__tests__/"
|
|
4177
|
+
]) if (testPath.startsWith(prefix)) candidates.add(`src/${removeTestSuffix(testPath.slice(prefix.length))}`);
|
|
4178
|
+
if (testPath.includes("/__tests__/")) candidates.add(removeTestSuffix(testPath.replace("/__tests__/", "/")));
|
|
4179
|
+
return [...candidates].filter((candidate) => candidate !== testPath).flatMap((candidate) => {
|
|
4180
|
+
const entry = entriesByPath.get(candidate);
|
|
4181
|
+
return entry ? [entry.id] : [];
|
|
4182
|
+
});
|
|
4183
|
+
}
|
|
4184
|
+
function removeTestSuffix(path) {
|
|
4185
|
+
return path.replace(/\.(test|spec)(\.[^./]+)$/i, "$2");
|
|
4186
|
+
}
|
|
4187
|
+
function isTestPath(path) {
|
|
4188
|
+
return /\.(test|spec)\.(ts|tsx|js|jsx|mts|cts)$/.test(path) || path.startsWith("test/") || path.startsWith("tests/") || path.startsWith("__tests__/") || path.includes("/__tests__/");
|
|
4189
|
+
}
|
|
4190
|
+
function isExistingNonSelfFileId(id, selfId, entriesById) {
|
|
4191
|
+
return id !== selfId && entriesById.has(id);
|
|
4192
|
+
}
|
|
4193
|
+
function dedupeStrings$1(values) {
|
|
4194
|
+
return [...new Set(values)];
|
|
4195
|
+
}
|
|
4196
|
+
function isFileNotFoundError$3(error) {
|
|
4197
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
4198
|
+
}
|
|
4199
|
+
//#endregion
|
|
3780
4200
|
//#region src/knowledge/compiler/manifest.ts
|
|
3781
4201
|
const knowledgeCompilerIdentity = {
|
|
3782
4202
|
name: "topchester-knowledge-compiler",
|
|
@@ -3819,6 +4239,8 @@ async function processL1Queue(options) {
|
|
|
3819
4239
|
await persistQueue(options.queuePath, queuedFiles, now().toISOString());
|
|
3820
4240
|
options.onProgress?.({ message: formatL1ProgressMessage("Processing L1 files", index + 1, queuedFiles.length, item.path) });
|
|
3821
4241
|
}
|
|
4242
|
+
options.onProgress?.({ message: "Linking L1 file relationships..." });
|
|
4243
|
+
await postProcessL1Entries(options.kbPath);
|
|
3822
4244
|
const summary = await summarizeL1Queue(options.kbPath, queuedFiles);
|
|
3823
4245
|
await writeManifest(options, summary, now().toISOString());
|
|
3824
4246
|
return {
|
|
@@ -3878,7 +4300,11 @@ async function processL1QueueItem(options) {
|
|
|
3878
4300
|
}
|
|
3879
4301
|
function buildL1FileEntrySystemPrompt() {
|
|
3880
4302
|
return [
|
|
3881
|
-
"You
|
|
4303
|
+
"You create concise, structured repository knowledge for one file.",
|
|
4304
|
+
"Prefer concrete facts visible in the file over generic descriptions.",
|
|
4305
|
+
"Do not invent modules, features, tests, routes, or dependencies.",
|
|
4306
|
+
"If uncertain, leave arrays empty and use lower confidence.",
|
|
4307
|
+
"Avoid filler such as \"This file contains code\" or \"Symbol named X\".",
|
|
3882
4308
|
"Return exactly one JSON object and no markdown.",
|
|
3883
4309
|
"Do not include secrets, credentials, or raw provider payloads."
|
|
3884
4310
|
].join("\n");
|
|
@@ -3887,6 +4313,35 @@ function buildL1FileEntryPrompt(input) {
|
|
|
3887
4313
|
return [
|
|
3888
4314
|
"Create an L1 file entry for this workspace-relative path.",
|
|
3889
4315
|
"The compiler will overwrite id, path, content_hash, size_bytes, last_scanned_at, and scan_status.",
|
|
4316
|
+
"",
|
|
4317
|
+
"Extraction rules:",
|
|
4318
|
+
"- summary: one specific sentence about the file's role in this project.",
|
|
4319
|
+
"- responsibilities: 2-6 concrete responsibilities, no duplicates, no generic boilerplate.",
|
|
4320
|
+
"- symbols: important declared or exported interfaces, types, classes, functions, constants, schemas, commands, routes, React components, tests, or config objects.",
|
|
4321
|
+
" For each symbol, set:",
|
|
4322
|
+
" - kind: interface | type | class | function | const | component | schema | command | route | test | config | symbol",
|
|
4323
|
+
" - name: exact identifier or stable label",
|
|
4324
|
+
" - exported: true only when exported from this file",
|
|
4325
|
+
" - summary: include only if it adds useful meaning beyond the name",
|
|
4326
|
+
"- imports: only workspace-local file dependencies as file:<path>; omit packages and built-ins.",
|
|
4327
|
+
"- exports: exact exported names from this file as strings.",
|
|
4328
|
+
"- test_ids: only file:<path> when this file is clearly a test or clearly references a test target.",
|
|
4329
|
+
"- file_role: source | test | config | doc | script | unknown.",
|
|
4330
|
+
"- declared_test_targets: for test files, file:<path> entries that this test directly imports or names.",
|
|
4331
|
+
"- likely_test_targets: for test files, file:<path> entries likely covered by path/name convention.",
|
|
4332
|
+
"- tested_by: leave empty; the compiler fills reverse test links after all files are processed.",
|
|
4333
|
+
"- module_ids and feature_ids: leave empty unless there is strong evidence.",
|
|
4334
|
+
"- evidence: include at least { \"kind\": \"path\", \"value\": \"<path>\" } and any high-signal local evidence.",
|
|
4335
|
+
"- confidence: high for simple files with clear structure, medium for normal files, low for vague/generated/config-heavy files.",
|
|
4336
|
+
"",
|
|
4337
|
+
"Quality rules:",
|
|
4338
|
+
"- Return valid JSON only.",
|
|
4339
|
+
"- Keep arrays concise.",
|
|
4340
|
+
"- Deduplicate all arrays.",
|
|
4341
|
+
"- Prefer exact names from source.",
|
|
4342
|
+
"- Do not copy large code snippets.",
|
|
4343
|
+
"- Do not include secrets or raw credentials.",
|
|
4344
|
+
"",
|
|
3890
4345
|
"Use this JSON shape:",
|
|
3891
4346
|
JSON.stringify({
|
|
3892
4347
|
$schema: l1FileEntrySchemaPath,
|
|
@@ -3899,6 +4354,7 @@ function buildL1FileEntryPrompt(input) {
|
|
|
3899
4354
|
size_bytes: 0,
|
|
3900
4355
|
last_scanned_at: "2026-05-11T00:00:00Z",
|
|
3901
4356
|
scan_status: "current",
|
|
4357
|
+
file_role: "source",
|
|
3902
4358
|
summary: "One clear sentence.",
|
|
3903
4359
|
responsibilities: ["What this file owns or does."],
|
|
3904
4360
|
symbols: [],
|
|
@@ -3907,6 +4363,9 @@ function buildL1FileEntryPrompt(input) {
|
|
|
3907
4363
|
module_ids: [],
|
|
3908
4364
|
feature_ids: [],
|
|
3909
4365
|
test_ids: [],
|
|
4366
|
+
declared_test_targets: [],
|
|
4367
|
+
likely_test_targets: [],
|
|
4368
|
+
tested_by: [],
|
|
3910
4369
|
evidence: [{
|
|
3911
4370
|
kind: "path",
|
|
3912
4371
|
value: "<path>"
|
|
@@ -3944,41 +4403,49 @@ function normalizeL1FileEntry(value, deterministic) {
|
|
|
3944
4403
|
}
|
|
3945
4404
|
function normalizeModelOwnedL1Fields(value, path) {
|
|
3946
4405
|
const record = value;
|
|
4406
|
+
const exports = normalizeStringArray(record.exports);
|
|
3947
4407
|
return {
|
|
3948
4408
|
...record,
|
|
4409
|
+
file_role: inferL1FileRole(path),
|
|
3949
4410
|
responsibilities: normalizeStringArray(record.responsibilities),
|
|
3950
|
-
symbols: normalizeSymbols(record.symbols, path),
|
|
4411
|
+
symbols: normalizeSymbols(record.symbols, path, exports),
|
|
3951
4412
|
imports: normalizePrefixedIds(record.imports, "file:"),
|
|
3952
|
-
exports
|
|
4413
|
+
exports,
|
|
3953
4414
|
module_ids: normalizePrefixedIds(record.module_ids, "module:"),
|
|
3954
4415
|
feature_ids: normalizePrefixedIds(record.feature_ids, "feature:"),
|
|
3955
4416
|
test_ids: normalizePrefixedIds(record.test_ids, "file:"),
|
|
4417
|
+
declared_test_targets: normalizePrefixedIds(record.declared_test_targets, "file:"),
|
|
4418
|
+
likely_test_targets: normalizePrefixedIds(record.likely_test_targets, "file:"),
|
|
4419
|
+
tested_by: normalizePrefixedIds(record.tested_by, "file:"),
|
|
3956
4420
|
evidence: normalizeEvidence(record.evidence)
|
|
3957
4421
|
};
|
|
3958
4422
|
}
|
|
3959
4423
|
function normalizeStringArray(value) {
|
|
3960
4424
|
if (!Array.isArray(value)) return [];
|
|
3961
|
-
return value.filter((item) => typeof item === "string"
|
|
4425
|
+
return dedupeStrings(value.filter((item) => typeof item === "string").map((item) => item.trim()));
|
|
3962
4426
|
}
|
|
3963
4427
|
function normalizePrefixedIds(value, prefix) {
|
|
3964
4428
|
return normalizeStringArray(value).filter((item) => item.startsWith(prefix));
|
|
3965
4429
|
}
|
|
3966
4430
|
function normalizeEvidence(value) {
|
|
3967
4431
|
if (!Array.isArray(value)) return [];
|
|
3968
|
-
return value.flatMap((item) => {
|
|
4432
|
+
return dedupeRecords(value.flatMap((item) => {
|
|
3969
4433
|
if (!item || typeof item !== "object" || Array.isArray(item)) return [];
|
|
3970
4434
|
const record = item;
|
|
3971
|
-
|
|
3972
|
-
|
|
4435
|
+
const kind = typeof record.kind === "string" ? record.kind.trim() : "";
|
|
4436
|
+
const recordValue = typeof record.value === "string" ? record.value.trim() : "";
|
|
4437
|
+
if (kind.length === 0) return [];
|
|
4438
|
+
if (recordValue.length === 0) return [];
|
|
3973
4439
|
return [{
|
|
3974
|
-
kind
|
|
3975
|
-
value:
|
|
4440
|
+
kind,
|
|
4441
|
+
value: recordValue
|
|
3976
4442
|
}];
|
|
3977
|
-
});
|
|
4443
|
+
}), (item) => `${item.kind}\0${item.value}`);
|
|
3978
4444
|
}
|
|
3979
|
-
function normalizeSymbols(value, path) {
|
|
4445
|
+
function normalizeSymbols(value, path, exports) {
|
|
3980
4446
|
if (!Array.isArray(value)) return [];
|
|
3981
|
-
|
|
4447
|
+
const exportedNames = new Set(exports);
|
|
4448
|
+
return dedupeRecords(value.flatMap((item) => {
|
|
3982
4449
|
if (typeof item === "string") {
|
|
3983
4450
|
const name = item.trim();
|
|
3984
4451
|
if (!name || !path) return [];
|
|
@@ -3986,8 +4453,7 @@ function normalizeSymbols(value, path) {
|
|
|
3986
4453
|
id: `symbol:${path}#${name}`,
|
|
3987
4454
|
kind: "symbol",
|
|
3988
4455
|
name,
|
|
3989
|
-
exported:
|
|
3990
|
-
summary: `Symbol named ${name}.`
|
|
4456
|
+
exported: exportedNames.has(name)
|
|
3991
4457
|
}];
|
|
3992
4458
|
}
|
|
3993
4459
|
if (!item || typeof item !== "object" || Array.isArray(item)) return [];
|
|
@@ -3995,14 +4461,36 @@ function normalizeSymbols(value, path) {
|
|
|
3995
4461
|
const rawId = typeof record.id === "string" && record.id.startsWith("symbol:") ? record.id : void 0;
|
|
3996
4462
|
const name = typeof record.name === "string" && record.name.trim().length > 0 ? record.name : rawId?.slice(rawId.lastIndexOf("#") + 1);
|
|
3997
4463
|
if (!name || !path) return [];
|
|
3998
|
-
|
|
4464
|
+
const summary = typeof record.summary === "string" && record.summary.trim().length > 0 ? record.summary.trim() : "";
|
|
4465
|
+
return [removeUndefinedValues({
|
|
3999
4466
|
id: rawId ?? `symbol:${path}#${name}`,
|
|
4000
|
-
kind: typeof record.kind === "string" && record.kind.trim().length > 0 ? record.kind : "symbol",
|
|
4467
|
+
kind: typeof record.kind === "string" && record.kind.trim().length > 0 ? record.kind.trim() : "symbol",
|
|
4001
4468
|
name,
|
|
4002
|
-
exported: typeof record.exported === "boolean" ? record.exported : false,
|
|
4003
|
-
summary:
|
|
4004
|
-
}];
|
|
4005
|
-
});
|
|
4469
|
+
exported: exportedNames.has(name) || (typeof record.exported === "boolean" ? record.exported : false),
|
|
4470
|
+
summary: summary && !isGenericSymbolSummary$1(summary, name) ? summary : void 0
|
|
4471
|
+
})];
|
|
4472
|
+
}), (item) => String(item.id));
|
|
4473
|
+
}
|
|
4474
|
+
function dedupeStrings(values) {
|
|
4475
|
+
return [...new Set(values.filter((value) => value.length > 0))];
|
|
4476
|
+
}
|
|
4477
|
+
function dedupeRecords(values, keyFor) {
|
|
4478
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4479
|
+
const deduped = [];
|
|
4480
|
+
for (const value of values) {
|
|
4481
|
+
const key = keyFor(value);
|
|
4482
|
+
if (seen.has(key)) continue;
|
|
4483
|
+
seen.add(key);
|
|
4484
|
+
deduped.push(value);
|
|
4485
|
+
}
|
|
4486
|
+
return deduped;
|
|
4487
|
+
}
|
|
4488
|
+
function removeUndefinedValues(record) {
|
|
4489
|
+
return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== void 0));
|
|
4490
|
+
}
|
|
4491
|
+
function isGenericSymbolSummary$1(summary, name) {
|
|
4492
|
+
const normalizedSummary = summary.trim().replace(/\s+/g, " ");
|
|
4493
|
+
return normalizedSummary === `Symbol named ${name}.` || normalizedSummary === `Symbol named ${name}` || normalizedSummary === name;
|
|
4006
4494
|
}
|
|
4007
4495
|
function extractTopLevelJsonObjects(text) {
|
|
4008
4496
|
const objects = [];
|
|
@@ -4404,11 +4892,11 @@ async function getL1SyncStatus(kbPath, kbReady, file) {
|
|
|
4404
4892
|
if (entry.path !== file.path || entry.size_bytes !== file.sizeBytes || entry.content_hash !== file.hash) return "changed";
|
|
4405
4893
|
return entry.scan_status;
|
|
4406
4894
|
} catch (error) {
|
|
4407
|
-
if (isFileNotFoundError$
|
|
4895
|
+
if (isFileNotFoundError$2(error)) return "missing_entry";
|
|
4408
4896
|
return "invalid";
|
|
4409
4897
|
}
|
|
4410
4898
|
}
|
|
4411
|
-
function isFileNotFoundError$
|
|
4899
|
+
function isFileNotFoundError$2(error) {
|
|
4412
4900
|
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
4413
4901
|
}
|
|
4414
4902
|
function assertKbSummarizeModelConfigured(model) {
|
|
@@ -4522,6 +5010,430 @@ function isNodeError(error) {
|
|
|
4522
5010
|
return error instanceof Error && "code" in error;
|
|
4523
5011
|
}
|
|
4524
5012
|
//#endregion
|
|
5013
|
+
//#region src/knowledge/search.ts
|
|
5014
|
+
var L1InMemoryIndex = class {
|
|
5015
|
+
entriesById = /* @__PURE__ */ new Map();
|
|
5016
|
+
postingsByToken = /* @__PURE__ */ new Map();
|
|
5017
|
+
prefixTokensByPrefix = /* @__PURE__ */ new Map();
|
|
5018
|
+
constructor(entries) {
|
|
5019
|
+
for (const entry of entries) {
|
|
5020
|
+
this.entriesById.set(entry.id, entry);
|
|
5021
|
+
this.indexEntry(entry);
|
|
5022
|
+
}
|
|
5023
|
+
this.indexPrefixTokens();
|
|
5024
|
+
}
|
|
5025
|
+
get size() {
|
|
5026
|
+
return this.entriesById.size;
|
|
5027
|
+
}
|
|
5028
|
+
search(query, options = {}) {
|
|
5029
|
+
const tokens = tokenizeQuery(query);
|
|
5030
|
+
const scoresByEntryId = /* @__PURE__ */ new Map();
|
|
5031
|
+
const reasonsByEntryId = /* @__PURE__ */ new Map();
|
|
5032
|
+
for (const token of tokens) {
|
|
5033
|
+
this.addMatches(token, 1, scoresByEntryId, reasonsByEntryId);
|
|
5034
|
+
if (token.length >= 4) this.addPrefixMatches(token, scoresByEntryId, reasonsByEntryId);
|
|
5035
|
+
}
|
|
5036
|
+
const limit = options.limit ?? 10;
|
|
5037
|
+
return [...scoresByEntryId.entries()].map(([entryId, score]) => {
|
|
5038
|
+
const entry = this.entriesById.get(entryId);
|
|
5039
|
+
if (!entry) return;
|
|
5040
|
+
return {
|
|
5041
|
+
id: entry.id,
|
|
5042
|
+
path: entry.path,
|
|
5043
|
+
score: Math.round(score * 100) / 100,
|
|
5044
|
+
summary: entry.summary,
|
|
5045
|
+
contentHash: entry.content_hash,
|
|
5046
|
+
scanStatus: entry.scan_status,
|
|
5047
|
+
reasons: [...(reasonsByEntryId.get(entryId) ?? /* @__PURE__ */ new Map()).entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([reason]) => reason).slice(0, 6)
|
|
5048
|
+
};
|
|
5049
|
+
}).filter((match) => Boolean(match)).sort((a, b) => b.score - a.score || a.path.localeCompare(b.path)).slice(0, limit);
|
|
5050
|
+
}
|
|
5051
|
+
getEntry(id) {
|
|
5052
|
+
return this.entriesById.get(id);
|
|
5053
|
+
}
|
|
5054
|
+
indexEntry(entry) {
|
|
5055
|
+
this.addField(entry, "path", [entry.path, basename(entry.path)], 6);
|
|
5056
|
+
this.addField(entry, "symbol", entry.symbols.flatMap((symbol) => [
|
|
5057
|
+
symbol.name,
|
|
5058
|
+
symbol.kind,
|
|
5059
|
+
symbol.summary
|
|
5060
|
+
].filter(isString)), 10);
|
|
5061
|
+
this.addField(entry, "export", entry.exports, 9);
|
|
5062
|
+
this.addField(entry, "responsibility", entry.responsibilities, 6);
|
|
5063
|
+
this.addField(entry, "summary", [entry.summary], 5);
|
|
5064
|
+
this.addField(entry, "import", entry.imports, 4);
|
|
5065
|
+
this.addField(entry, "test", entry.test_ids, 4);
|
|
5066
|
+
this.addField(entry, "relationship", [...entry.module_ids, ...entry.feature_ids], 3);
|
|
5067
|
+
this.addField(entry, "evidence", entry.evidence.map((evidence) => evidence.value), 3);
|
|
5068
|
+
}
|
|
5069
|
+
addField(entry, field, values, weight) {
|
|
5070
|
+
const tokens = new Set(values.flatMap(tokenizeText));
|
|
5071
|
+
for (const token of tokens) {
|
|
5072
|
+
const postings = this.postingsByToken.get(token) ?? [];
|
|
5073
|
+
postings.push({
|
|
5074
|
+
entryId: entry.id,
|
|
5075
|
+
weight,
|
|
5076
|
+
reason: `${formatField(field)} matched ${token}`
|
|
5077
|
+
});
|
|
5078
|
+
this.postingsByToken.set(token, postings);
|
|
5079
|
+
}
|
|
5080
|
+
}
|
|
5081
|
+
addMatches(token, multiplier, scoresByEntryId, reasonsByEntryId) {
|
|
5082
|
+
for (const posting of this.postingsByToken.get(token) ?? []) {
|
|
5083
|
+
scoresByEntryId.set(posting.entryId, (scoresByEntryId.get(posting.entryId) ?? 0) + posting.weight * multiplier);
|
|
5084
|
+
const reasons = reasonsByEntryId.get(posting.entryId) ?? /* @__PURE__ */ new Map();
|
|
5085
|
+
reasons.set(posting.reason, Math.max(reasons.get(posting.reason) ?? 0, posting.weight * multiplier));
|
|
5086
|
+
reasonsByEntryId.set(posting.entryId, reasons);
|
|
5087
|
+
}
|
|
5088
|
+
}
|
|
5089
|
+
addPrefixMatches(token, scoresByEntryId, reasonsByEntryId) {
|
|
5090
|
+
const matchedTokens = /* @__PURE__ */ new Set();
|
|
5091
|
+
for (const indexedToken of this.prefixTokensByPrefix.get(token) ?? []) matchedTokens.add(indexedToken);
|
|
5092
|
+
for (let prefixLength = 1; prefixLength < token.length; prefixLength += 1) {
|
|
5093
|
+
const prefix = token.slice(0, prefixLength);
|
|
5094
|
+
if (this.postingsByToken.has(prefix)) matchedTokens.add(prefix);
|
|
5095
|
+
}
|
|
5096
|
+
for (const indexedToken of matchedTokens) {
|
|
5097
|
+
if (indexedToken === token) continue;
|
|
5098
|
+
this.addMatches(indexedToken, .6, scoresByEntryId, reasonsByEntryId);
|
|
5099
|
+
}
|
|
5100
|
+
}
|
|
5101
|
+
indexPrefixTokens() {
|
|
5102
|
+
for (const indexedToken of this.postingsByToken.keys()) for (let prefixLength = 4; prefixLength < indexedToken.length; prefixLength += 1) {
|
|
5103
|
+
const prefix = indexedToken.slice(0, prefixLength);
|
|
5104
|
+
const tokens = this.prefixTokensByPrefix.get(prefix) ?? [];
|
|
5105
|
+
tokens.push(indexedToken);
|
|
5106
|
+
this.prefixTokensByPrefix.set(prefix, tokens);
|
|
5107
|
+
}
|
|
5108
|
+
}
|
|
5109
|
+
};
|
|
5110
|
+
function buildL1InMemoryIndex(entries) {
|
|
5111
|
+
return new L1InMemoryIndex(entries);
|
|
5112
|
+
}
|
|
5113
|
+
async function searchL1Knowledge(workspaceRoot, query, options = {}) {
|
|
5114
|
+
const status = getKnowledgeStatus(workspaceRoot);
|
|
5115
|
+
if (!status.kbExists || !status.kbIsDirectory) throw new Error("Run `topchester kb init` and `topchester kb compile` before searching the knowledge base.");
|
|
5116
|
+
const loadResult = await loadL1FileEntries(status.kbPath);
|
|
5117
|
+
const index = buildL1InMemoryIndex(loadResult.entries);
|
|
5118
|
+
return {
|
|
5119
|
+
workspaceRoot,
|
|
5120
|
+
kbPath: status.kbPath,
|
|
5121
|
+
query,
|
|
5122
|
+
entryCount: index.size,
|
|
5123
|
+
invalidEntryCount: loadResult.invalidEntryCount,
|
|
5124
|
+
matches: index.search(query, options)
|
|
5125
|
+
};
|
|
5126
|
+
}
|
|
5127
|
+
function createL1ContextPackFromIndex(source, query, options = {}) {
|
|
5128
|
+
const limit = options.limit ?? 8;
|
|
5129
|
+
const minScore = options.minScore ?? 12;
|
|
5130
|
+
const relevantFiles = source.index.search(query, { limit: Math.max(limit * 3, limit) }).filter((match) => match.score >= minScore).slice(0, limit).map((match) => {
|
|
5131
|
+
const entry = source.index.getEntry(match.id);
|
|
5132
|
+
if (!entry) return;
|
|
5133
|
+
return {
|
|
5134
|
+
id: match.id,
|
|
5135
|
+
path: match.path,
|
|
5136
|
+
score: match.score,
|
|
5137
|
+
reasons: match.reasons,
|
|
5138
|
+
contentHash: match.contentHash,
|
|
5139
|
+
scanStatus: match.scanStatus,
|
|
5140
|
+
l1: compactL1Entry(entry),
|
|
5141
|
+
fullL1: options.includeFullL1 ? entry : void 0
|
|
5142
|
+
};
|
|
5143
|
+
}).filter((file) => Boolean(file));
|
|
5144
|
+
const warnings = relevantFiles.length === 0 ? ["No L1 entries met the context pack score threshold."] : [];
|
|
5145
|
+
return {
|
|
5146
|
+
workspaceRoot: source.workspaceRoot,
|
|
5147
|
+
kbPath: source.kbPath,
|
|
5148
|
+
query,
|
|
5149
|
+
entryCount: source.index.size,
|
|
5150
|
+
invalidEntryCount: source.invalidEntryCount,
|
|
5151
|
+
selection: {
|
|
5152
|
+
limit,
|
|
5153
|
+
minScore
|
|
5154
|
+
},
|
|
5155
|
+
drift: {
|
|
5156
|
+
status: "unchecked",
|
|
5157
|
+
warnings: ["L1 context pack includes stored scan statuses; exact file-hash drift check has not run yet."]
|
|
5158
|
+
},
|
|
5159
|
+
summary: summarizeContextPack(query, relevantFiles),
|
|
5160
|
+
warnings,
|
|
5161
|
+
relevantFiles
|
|
5162
|
+
};
|
|
5163
|
+
}
|
|
5164
|
+
async function createL1ContextPack(workspaceRoot, query, options = {}) {
|
|
5165
|
+
const limit = options.limit ?? 8;
|
|
5166
|
+
const minScore = options.minScore ?? 12;
|
|
5167
|
+
const status = getKnowledgeStatus(workspaceRoot);
|
|
5168
|
+
if (!status.kbExists || !status.kbIsDirectory) throw new Error("Run `topchester kb init` and `topchester kb compile` before creating a context pack.");
|
|
5169
|
+
const loadResult = await loadL1FileEntries(status.kbPath);
|
|
5170
|
+
const index = buildL1InMemoryIndex(loadResult.entries);
|
|
5171
|
+
return createL1ContextPackFromIndex({
|
|
5172
|
+
workspaceRoot,
|
|
5173
|
+
kbPath: status.kbPath,
|
|
5174
|
+
index,
|
|
5175
|
+
invalidEntryCount: loadResult.invalidEntryCount
|
|
5176
|
+
}, query, {
|
|
5177
|
+
limit,
|
|
5178
|
+
minScore,
|
|
5179
|
+
includeFullL1: options.includeFullL1
|
|
5180
|
+
});
|
|
5181
|
+
}
|
|
5182
|
+
function formatL1KnowledgeSearchResult(result) {
|
|
5183
|
+
return [
|
|
5184
|
+
"KB search",
|
|
5185
|
+
`workspace: ${result.workspaceRoot}`,
|
|
5186
|
+
`knowledge folder: ${result.kbPath} [ok]`,
|
|
5187
|
+
`query: ${result.query}`,
|
|
5188
|
+
`entries indexed: ${result.entryCount}`,
|
|
5189
|
+
`invalid L1 entries skipped: ${result.invalidEntryCount}`,
|
|
5190
|
+
`matches: ${result.matches.length}`,
|
|
5191
|
+
...result.matches.length === 0 ? ["state: no L1 matches found"] : [""],
|
|
5192
|
+
...result.matches.flatMap((match) => [
|
|
5193
|
+
`${match.score}\t${match.path}\t${match.scanStatus}\t${match.contentHash}`,
|
|
5194
|
+
` reasons: ${match.reasons.join("; ") || "score match"}`,
|
|
5195
|
+
` summary: ${match.summary}`
|
|
5196
|
+
]),
|
|
5197
|
+
"----",
|
|
5198
|
+
`total matches: ${result.matches.length}`
|
|
5199
|
+
];
|
|
5200
|
+
}
|
|
5201
|
+
function formatL1ContextPackResult(result) {
|
|
5202
|
+
return [
|
|
5203
|
+
"KB context",
|
|
5204
|
+
`workspace: ${result.workspaceRoot}`,
|
|
5205
|
+
`knowledge folder: ${result.kbPath} [ok]`,
|
|
5206
|
+
`query: ${result.query}`,
|
|
5207
|
+
`entries indexed: ${result.entryCount}`,
|
|
5208
|
+
`invalid L1 entries skipped: ${result.invalidEntryCount}`,
|
|
5209
|
+
`selection: top ${result.selection.limit}, min score ${result.selection.minScore}`,
|
|
5210
|
+
`drift: ${result.drift.status}`,
|
|
5211
|
+
`relevant files: ${result.relevantFiles.length}`,
|
|
5212
|
+
`summary: ${result.summary}`,
|
|
5213
|
+
...result.warnings.map((warning) => `warning: ${warning}`),
|
|
5214
|
+
"",
|
|
5215
|
+
...result.relevantFiles.flatMap((file) => [
|
|
5216
|
+
`${file.score}\t${file.path}\t${file.scanStatus}\t${file.contentHash}`,
|
|
5217
|
+
` reasons: ${file.reasons.join("; ") || "score match"}`,
|
|
5218
|
+
` responsibilities: ${(file.l1.responsibilities ?? []).join("; ") || "(none)"}`,
|
|
5219
|
+
` symbols: ${(file.l1.symbols ?? []).map((symbol) => symbol.name).join(", ") || "(none)"}`,
|
|
5220
|
+
` imports: ${(file.l1.imports ?? []).join(", ") || "(none)"}`,
|
|
5221
|
+
` exports: ${(file.l1.exports ?? []).join(", ") || "(none)"}`,
|
|
5222
|
+
` tests: ${(file.l1.test_ids ?? []).join(", ") || "(none)"}`
|
|
5223
|
+
]),
|
|
5224
|
+
"----",
|
|
5225
|
+
`total relevant files: ${result.relevantFiles.length}`
|
|
5226
|
+
];
|
|
5227
|
+
}
|
|
5228
|
+
function formatL1ContextPackForPrompt(result) {
|
|
5229
|
+
return [
|
|
5230
|
+
"Topchester KB context pack:",
|
|
5231
|
+
"Use this as orientation only. For task-critical facts, read current source files before editing or making exact claims.",
|
|
5232
|
+
"## -- kb summary start",
|
|
5233
|
+
"```json",
|
|
5234
|
+
JSON.stringify(stripEmptyContainers({
|
|
5235
|
+
query: result.query,
|
|
5236
|
+
summary: result.summary,
|
|
5237
|
+
drift: result.drift,
|
|
5238
|
+
warnings: result.warnings,
|
|
5239
|
+
relevantFiles: result.relevantFiles.map((file) => ({
|
|
5240
|
+
id: file.id,
|
|
5241
|
+
path: file.path,
|
|
5242
|
+
score: file.score,
|
|
5243
|
+
reasons: file.reasons,
|
|
5244
|
+
contentHash: file.contentHash,
|
|
5245
|
+
scanStatus: file.scanStatus,
|
|
5246
|
+
l1: {
|
|
5247
|
+
summary: file.l1.summary,
|
|
5248
|
+
file_role: file.l1.file_role,
|
|
5249
|
+
responsibilities: file.l1.responsibilities,
|
|
5250
|
+
symbols: file.l1.symbols,
|
|
5251
|
+
imports: file.l1.imports,
|
|
5252
|
+
exports: file.l1.exports,
|
|
5253
|
+
module_ids: file.l1.module_ids,
|
|
5254
|
+
feature_ids: file.l1.feature_ids,
|
|
5255
|
+
test_ids: file.l1.test_ids,
|
|
5256
|
+
declared_test_targets: file.l1.declared_test_targets,
|
|
5257
|
+
likely_test_targets: file.l1.likely_test_targets,
|
|
5258
|
+
tested_by: file.l1.tested_by,
|
|
5259
|
+
confidence: file.l1.confidence
|
|
5260
|
+
}
|
|
5261
|
+
}))
|
|
5262
|
+
})),
|
|
5263
|
+
"```",
|
|
5264
|
+
"## -- kb summary end"
|
|
5265
|
+
].join("\n");
|
|
5266
|
+
}
|
|
5267
|
+
async function loadL1FileEntries(kbPath) {
|
|
5268
|
+
const entryPaths = await listJsonFiles(join(kbPath, "l1-files")).catch((error) => {
|
|
5269
|
+
if (isFileNotFoundError$1(error)) return [];
|
|
5270
|
+
throw error;
|
|
5271
|
+
});
|
|
5272
|
+
const parsedEntries = await Promise.all(entryPaths.map(loadL1FileEntry));
|
|
5273
|
+
const entries = parsedEntries.filter((entry) => Boolean(entry));
|
|
5274
|
+
return {
|
|
5275
|
+
entries,
|
|
5276
|
+
invalidEntryCount: parsedEntries.length - entries.length
|
|
5277
|
+
};
|
|
5278
|
+
}
|
|
5279
|
+
async function loadL1FileEntry(entryPath) {
|
|
5280
|
+
try {
|
|
5281
|
+
return parseL1FileEntry(JSON.parse(await readFile(entryPath, "utf8")));
|
|
5282
|
+
} catch {
|
|
5283
|
+
return;
|
|
5284
|
+
}
|
|
5285
|
+
}
|
|
5286
|
+
function summarizeContextPack(query, files) {
|
|
5287
|
+
if (files.length === 0) return `No strong L1 matches were found for "${query}".`;
|
|
5288
|
+
const paths = files.slice(0, 5).map((file) => file.path);
|
|
5289
|
+
return `Likely relevant L1 files for "${query}": ${paths.join(", ")}${files.length > paths.length ? ", ..." : ""}.`;
|
|
5290
|
+
}
|
|
5291
|
+
function compactL1Entry(entry) {
|
|
5292
|
+
const responsibilities = take(entry.responsibilities, 5);
|
|
5293
|
+
const symbols = take(entry.symbols, 12).map(compactSymbol);
|
|
5294
|
+
const imports = take(entry.imports, 20);
|
|
5295
|
+
const exports = take(entry.exports, 20);
|
|
5296
|
+
const moduleIds = take(entry.module_ids, 10);
|
|
5297
|
+
const featureIds = take(entry.feature_ids, 10);
|
|
5298
|
+
const testIds = take(entry.test_ids, 10);
|
|
5299
|
+
const declaredTestTargets = take(entry.declared_test_targets, 10);
|
|
5300
|
+
const likelyTestTargets = take(entry.likely_test_targets, 10);
|
|
5301
|
+
const testedBy = take(entry.tested_by, 10);
|
|
5302
|
+
return stripUndefinedProperties({
|
|
5303
|
+
file_role: entry.file_role,
|
|
5304
|
+
summary: entry.summary,
|
|
5305
|
+
responsibilities: nonEmptyArray(responsibilities),
|
|
5306
|
+
symbols: nonEmptyArray(symbols),
|
|
5307
|
+
imports: nonEmptyArray(imports),
|
|
5308
|
+
exports: nonEmptyArray(exports),
|
|
5309
|
+
module_ids: nonEmptyArray(moduleIds),
|
|
5310
|
+
feature_ids: nonEmptyArray(featureIds),
|
|
5311
|
+
test_ids: nonEmptyArray(testIds),
|
|
5312
|
+
declared_test_targets: nonEmptyArray(declaredTestTargets),
|
|
5313
|
+
likely_test_targets: nonEmptyArray(likelyTestTargets),
|
|
5314
|
+
tested_by: nonEmptyArray(testedBy),
|
|
5315
|
+
confidence: entry.confidence
|
|
5316
|
+
});
|
|
5317
|
+
}
|
|
5318
|
+
function take(items, count) {
|
|
5319
|
+
return items.slice(0, count);
|
|
5320
|
+
}
|
|
5321
|
+
function compactSymbol(symbol) {
|
|
5322
|
+
const compacted = {
|
|
5323
|
+
name: symbol.name,
|
|
5324
|
+
exported: symbol.exported,
|
|
5325
|
+
kind: symbol.kind === "symbol" ? void 0 : symbol.kind,
|
|
5326
|
+
summary: symbol.summary && !isGenericSymbolSummary(symbol.summary, symbol.name) ? symbol.summary : void 0
|
|
5327
|
+
};
|
|
5328
|
+
return compacted.kind || compacted.summary ? compacted : {
|
|
5329
|
+
name: compacted.name,
|
|
5330
|
+
exported: compacted.exported
|
|
5331
|
+
};
|
|
5332
|
+
}
|
|
5333
|
+
function stripEmptyContainers(value) {
|
|
5334
|
+
if (Array.isArray(value)) {
|
|
5335
|
+
const stripped = value.map(stripEmptyContainers).filter((item) => item !== void 0);
|
|
5336
|
+
return stripped.length > 0 ? stripped : void 0;
|
|
5337
|
+
}
|
|
5338
|
+
if (value && typeof value === "object") {
|
|
5339
|
+
const entries = Object.entries(value).map(([key, item]) => [key, stripEmptyContainers(item)]).filter(([, item]) => item !== void 0);
|
|
5340
|
+
return entries.length > 0 ? Object.fromEntries(entries) : void 0;
|
|
5341
|
+
}
|
|
5342
|
+
return value;
|
|
5343
|
+
}
|
|
5344
|
+
function stripUndefinedProperties(value) {
|
|
5345
|
+
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== void 0));
|
|
5346
|
+
}
|
|
5347
|
+
function nonEmptyArray(items) {
|
|
5348
|
+
return items.length > 0 ? items : void 0;
|
|
5349
|
+
}
|
|
5350
|
+
function isGenericSymbolSummary(summary, name) {
|
|
5351
|
+
const normalizedSummary = summary.trim().replace(/\s+/g, " ");
|
|
5352
|
+
return normalizedSummary === `Symbol named ${name}.` || normalizedSummary === `Symbol named ${name}` || normalizedSummary === name;
|
|
5353
|
+
}
|
|
5354
|
+
async function listJsonFiles(directory) {
|
|
5355
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
5356
|
+
const files = [];
|
|
5357
|
+
for (const entry of entries) {
|
|
5358
|
+
const entryPath = join(directory, entry.name);
|
|
5359
|
+
if (entry.isDirectory()) files.push(...await listJsonFiles(entryPath));
|
|
5360
|
+
else if (entry.isFile() && entry.name.endsWith(".json")) files.push(entryPath);
|
|
5361
|
+
}
|
|
5362
|
+
return files.sort();
|
|
5363
|
+
}
|
|
5364
|
+
function tokenizeQuery(text) {
|
|
5365
|
+
return [...new Set(tokenizeText(text).filter((token) => !queryStopWords.has(token)))];
|
|
5366
|
+
}
|
|
5367
|
+
function tokenizeText(text) {
|
|
5368
|
+
const rawTokens = text.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").toLowerCase().match(/[a-z0-9_]+/g) ?? [];
|
|
5369
|
+
const tokens = [];
|
|
5370
|
+
for (const rawToken of rawTokens) {
|
|
5371
|
+
const token = rawToken.replace(/^_+|_+$/g, "");
|
|
5372
|
+
if (!token || indexStopWords.has(token)) continue;
|
|
5373
|
+
tokens.push(token);
|
|
5374
|
+
const singular = singularizeToken(token);
|
|
5375
|
+
if (singular !== token) tokens.push(singular);
|
|
5376
|
+
}
|
|
5377
|
+
return tokens;
|
|
5378
|
+
}
|
|
5379
|
+
function singularizeToken(token) {
|
|
5380
|
+
if (token.length > 3 && token.endsWith("ies")) return `${token.slice(0, -3)}y`;
|
|
5381
|
+
if (token.length > 3 && token.endsWith("s") && !token.endsWith("ss") && !token.endsWith("us")) return token.slice(0, -1);
|
|
5382
|
+
return token;
|
|
5383
|
+
}
|
|
5384
|
+
function formatField(field) {
|
|
5385
|
+
switch (field) {
|
|
5386
|
+
case "path": return "path";
|
|
5387
|
+
case "symbol": return "symbol";
|
|
5388
|
+
case "export": return "export";
|
|
5389
|
+
case "responsibility": return "responsibility";
|
|
5390
|
+
case "summary": return "summary";
|
|
5391
|
+
case "import": return "import";
|
|
5392
|
+
case "test": return "test";
|
|
5393
|
+
case "relationship": return "relationship";
|
|
5394
|
+
case "evidence": return "evidence";
|
|
5395
|
+
}
|
|
5396
|
+
}
|
|
5397
|
+
function isFileNotFoundError$1(error) {
|
|
5398
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
5399
|
+
}
|
|
5400
|
+
function isString(value) {
|
|
5401
|
+
return typeof value === "string";
|
|
5402
|
+
}
|
|
5403
|
+
const indexStopWords = new Set([
|
|
5404
|
+
"a",
|
|
5405
|
+
"an",
|
|
5406
|
+
"and",
|
|
5407
|
+
"are",
|
|
5408
|
+
"as",
|
|
5409
|
+
"at",
|
|
5410
|
+
"be",
|
|
5411
|
+
"by",
|
|
5412
|
+
"for",
|
|
5413
|
+
"from",
|
|
5414
|
+
"in",
|
|
5415
|
+
"is",
|
|
5416
|
+
"it",
|
|
5417
|
+
"of",
|
|
5418
|
+
"on",
|
|
5419
|
+
"or",
|
|
5420
|
+
"the",
|
|
5421
|
+
"to",
|
|
5422
|
+
"with"
|
|
5423
|
+
]);
|
|
5424
|
+
const queryStopWords = new Set([
|
|
5425
|
+
...indexStopWords,
|
|
5426
|
+
"error",
|
|
5427
|
+
"here",
|
|
5428
|
+
"log",
|
|
5429
|
+
"see",
|
|
5430
|
+
"se",
|
|
5431
|
+
"tries",
|
|
5432
|
+
"trying",
|
|
5433
|
+
"user",
|
|
5434
|
+
"when"
|
|
5435
|
+
]);
|
|
5436
|
+
//#endregion
|
|
4525
5437
|
//#region src/tui/markdown.ts
|
|
4526
5438
|
const codeFenceSentinel = "topchester-code-fence";
|
|
4527
5439
|
function renderMarkdown(text, width) {
|
|
@@ -4753,6 +5665,19 @@ const toolCallPayloadSchema = z.object({
|
|
|
4753
5665
|
label: z.string(),
|
|
4754
5666
|
call: z.record(z.string(), jsonValueSchema)
|
|
4755
5667
|
});
|
|
5668
|
+
const taskPlanItemPayloadSchema = z.object({
|
|
5669
|
+
text: z.string(),
|
|
5670
|
+
status: z.enum([
|
|
5671
|
+
"pending",
|
|
5672
|
+
"in_progress",
|
|
5673
|
+
"completed"
|
|
5674
|
+
])
|
|
5675
|
+
});
|
|
5676
|
+
const taskPlanPayloadSchema = z.object({
|
|
5677
|
+
kind: z.literal("task_plan"),
|
|
5678
|
+
items: z.array(taskPlanItemPayloadSchema),
|
|
5679
|
+
updatedAt: isoTimestampSchema
|
|
5680
|
+
});
|
|
4756
5681
|
const statusPayloadSchema = z.object({
|
|
4757
5682
|
kind: z.literal("status"),
|
|
4758
5683
|
status: z.string()
|
|
@@ -4774,6 +5699,7 @@ const choicePayloadSchema = z.object({
|
|
|
4774
5699
|
const sessionEventPayloadSchema = z.discriminatedUnion("kind", [
|
|
4775
5700
|
messagePayloadSchema,
|
|
4776
5701
|
toolCallPayloadSchema,
|
|
5702
|
+
taskPlanPayloadSchema,
|
|
4777
5703
|
statusPayloadSchema,
|
|
4778
5704
|
knowledgeStatusPayloadSchema,
|
|
4779
5705
|
choicePayloadSchema
|
|
@@ -4854,6 +5780,7 @@ async function resolveLatestSessionId(workspaceRoot) {
|
|
|
4854
5780
|
function rehydrateSession(events) {
|
|
4855
5781
|
const messages = [];
|
|
4856
5782
|
let status;
|
|
5783
|
+
let taskPlan;
|
|
4857
5784
|
let visibleOnlyActionValues = /* @__PURE__ */ new Set();
|
|
4858
5785
|
for (const event of events) switch (event.kind) {
|
|
4859
5786
|
case "message":
|
|
@@ -4869,6 +5796,12 @@ function rehydrateSession(events) {
|
|
|
4869
5796
|
case "tool_call":
|
|
4870
5797
|
messages.push(toolCallMessage(event.call, event.label));
|
|
4871
5798
|
break;
|
|
5799
|
+
case "task_plan":
|
|
5800
|
+
taskPlan = {
|
|
5801
|
+
items: event.items,
|
|
5802
|
+
updatedAt: event.updatedAt
|
|
5803
|
+
};
|
|
5804
|
+
break;
|
|
4872
5805
|
case "knowledge_status": break;
|
|
4873
5806
|
case "choice":
|
|
4874
5807
|
messages.push({
|
|
@@ -4886,7 +5819,8 @@ function rehydrateSession(events) {
|
|
|
4886
5819
|
}
|
|
4887
5820
|
return {
|
|
4888
5821
|
messages,
|
|
4889
|
-
status
|
|
5822
|
+
status,
|
|
5823
|
+
...taskPlan === void 0 ? {} : { taskPlan }
|
|
4890
5824
|
};
|
|
4891
5825
|
}
|
|
4892
5826
|
function buildHandle(sessionDir, metadata) {
|
|
@@ -5264,6 +6198,71 @@ async function executeKbCommand(args, context) {
|
|
|
5264
6198
|
return { messages: ["Usage: /kb init, /kb compile, /kb sync, /kb reset, or /kb status"] };
|
|
5265
6199
|
}
|
|
5266
6200
|
//#endregion
|
|
6201
|
+
//#region src/agent/events.ts
|
|
6202
|
+
const ABORT_CHOICE_VALUE = "__topchester_abort__";
|
|
6203
|
+
const agentEvent = {
|
|
6204
|
+
status(status) {
|
|
6205
|
+
return {
|
|
6206
|
+
type: "status",
|
|
6207
|
+
status
|
|
6208
|
+
};
|
|
6209
|
+
},
|
|
6210
|
+
systemMessage(text) {
|
|
6211
|
+
return {
|
|
6212
|
+
type: "message",
|
|
6213
|
+
role: "system",
|
|
6214
|
+
text
|
|
6215
|
+
};
|
|
6216
|
+
},
|
|
6217
|
+
assistantMessage(text, meta) {
|
|
6218
|
+
return meta === void 0 ? {
|
|
6219
|
+
type: "message",
|
|
6220
|
+
role: "assistant",
|
|
6221
|
+
text
|
|
6222
|
+
} : {
|
|
6223
|
+
type: "message",
|
|
6224
|
+
role: "assistant",
|
|
6225
|
+
text,
|
|
6226
|
+
meta
|
|
6227
|
+
};
|
|
6228
|
+
},
|
|
6229
|
+
toolCall(call, label) {
|
|
6230
|
+
return {
|
|
6231
|
+
type: "tool_call",
|
|
6232
|
+
call,
|
|
6233
|
+
label
|
|
6234
|
+
};
|
|
6235
|
+
},
|
|
6236
|
+
taskPlan(plan) {
|
|
6237
|
+
return {
|
|
6238
|
+
type: "task_plan",
|
|
6239
|
+
plan
|
|
6240
|
+
};
|
|
6241
|
+
},
|
|
6242
|
+
knowledgeStatus(status, guidance) {
|
|
6243
|
+
return guidance === void 0 ? {
|
|
6244
|
+
type: "knowledge_status",
|
|
6245
|
+
status
|
|
6246
|
+
} : {
|
|
6247
|
+
type: "knowledge_status",
|
|
6248
|
+
status,
|
|
6249
|
+
guidance
|
|
6250
|
+
};
|
|
6251
|
+
},
|
|
6252
|
+
choice(options) {
|
|
6253
|
+
return {
|
|
6254
|
+
type: "choice",
|
|
6255
|
+
...options
|
|
6256
|
+
};
|
|
6257
|
+
}
|
|
6258
|
+
};
|
|
6259
|
+
function choiceAction(label, value) {
|
|
6260
|
+
return value === void 0 ? { label } : {
|
|
6261
|
+
label,
|
|
6262
|
+
value
|
|
6263
|
+
};
|
|
6264
|
+
}
|
|
6265
|
+
//#endregion
|
|
5267
6266
|
//#region src/tui/keys.ts
|
|
5268
6267
|
function isUpKey(data) {
|
|
5269
6268
|
return matchesKey(data, "up") || data === "\x1B[A";
|
|
@@ -5274,6 +6273,9 @@ function isDownKey(data) {
|
|
|
5274
6273
|
function isEnterKey(data) {
|
|
5275
6274
|
return matchesKey(data, "enter") || data === "\n" || data === "\r";
|
|
5276
6275
|
}
|
|
6276
|
+
function isNewLineKey(data) {
|
|
6277
|
+
return matchesKey(data, "shift+enter") || matchesKey(data, "alt+enter") || matchesKey(data, "ctrl+enter") || data === "\x1B\r" || data === "\x1B[13;2~";
|
|
6278
|
+
}
|
|
5277
6279
|
function isTabKey(data) {
|
|
5278
6280
|
return matchesKey(data, "tab") || data === " ";
|
|
5279
6281
|
}
|
|
@@ -5380,12 +6382,14 @@ function getStartupThreadMessages(context) {
|
|
|
5380
6382
|
lines.push("Ask Topchester what you want to change.");
|
|
5381
6383
|
return [systemMessage(lines.join("\n"))];
|
|
5382
6384
|
}
|
|
5383
|
-
function renderStaticLayout(messages, folderName = "", modelLabel = "") {
|
|
6385
|
+
function renderStaticLayout(messages, folderName = "", modelLabel = "", taskPlan) {
|
|
5384
6386
|
const threadLines = messages.flatMap((message) => renderChatMessage(message));
|
|
5385
6387
|
const status = formatStatusLine(folderName, modelLabel);
|
|
6388
|
+
const planLines = taskPlan && taskPlan.items.length > 0 ? [...formatTaskPlanForTui(taskPlan, 72), ""] : [];
|
|
5386
6389
|
return [
|
|
5387
6390
|
...threadLines,
|
|
5388
6391
|
"",
|
|
6392
|
+
...planLines,
|
|
5389
6393
|
"┌──────────────────────────────────────────────────────────────────────┐",
|
|
5390
6394
|
"│ > │",
|
|
5391
6395
|
"└──────────────────────────────────────────────────────────────────────┘",
|
|
@@ -5467,23 +6471,35 @@ function stripAnsi(text) {
|
|
|
5467
6471
|
}
|
|
5468
6472
|
//#endregion
|
|
5469
6473
|
//#region src/tui/layout.ts
|
|
6474
|
+
const PROMPT_VISIBLE_CONTENT_LINES = 5;
|
|
6475
|
+
const PASTE_PREVIEW_MIN_LINES = 6;
|
|
6476
|
+
const PASTE_PREVIEW_MIN_CHARS = 500;
|
|
6477
|
+
const BRACKETED_PASTE_START = "\x1B[200~";
|
|
6478
|
+
const BRACKETED_PASTE_END = "\x1B[201~";
|
|
5470
6479
|
var ChatLayout = class {
|
|
5471
6480
|
terminal;
|
|
5472
6481
|
messages;
|
|
5473
6482
|
folderName;
|
|
5474
6483
|
modelLabel;
|
|
5475
|
-
|
|
6484
|
+
inputFocused = false;
|
|
6485
|
+
promptValue = "";
|
|
6486
|
+
promptCursor = 0;
|
|
5476
6487
|
status = "ready";
|
|
5477
6488
|
knowledgeStatus;
|
|
5478
6489
|
ephemeralLine;
|
|
6490
|
+
taskPlanNoticeLine;
|
|
5479
6491
|
noticeLine;
|
|
5480
6492
|
promptHint;
|
|
6493
|
+
taskPlan;
|
|
5481
6494
|
cancelPending;
|
|
5482
6495
|
submitMessage;
|
|
5483
6496
|
submitCommand;
|
|
5484
6497
|
activeModalActionIndex = 0;
|
|
5485
6498
|
activeSlashSuggestionIndex = 0;
|
|
5486
6499
|
threadScrollOffset = 0;
|
|
6500
|
+
pasteBuffer;
|
|
6501
|
+
pasteCounter = 0;
|
|
6502
|
+
pastedContent = /* @__PURE__ */ new Map();
|
|
5487
6503
|
promptHistory = new PromptHistory();
|
|
5488
6504
|
exitAgent;
|
|
5489
6505
|
transcriptMode;
|
|
@@ -5494,14 +6510,6 @@ var ChatLayout = class {
|
|
|
5494
6510
|
this.modelLabel = modelLabel;
|
|
5495
6511
|
this.exitAgent = typeof options === "function" ? options : options.exitAgent ?? (() => {});
|
|
5496
6512
|
this.transcriptMode = typeof options === "function" ? "viewport" : options.transcriptMode ?? "viewport";
|
|
5497
|
-
this.input.onSubmit = (value) => {
|
|
5498
|
-
if (value.trim().length > 0) {
|
|
5499
|
-
const message = value.trim();
|
|
5500
|
-
this.addMessage(userMessage(message));
|
|
5501
|
-
this.input.setValue("");
|
|
5502
|
-
this.submitUserInput(message);
|
|
5503
|
-
}
|
|
5504
|
-
};
|
|
5505
6513
|
}
|
|
5506
6514
|
addMessage(message) {
|
|
5507
6515
|
this.messages.push(message);
|
|
@@ -5514,6 +6522,24 @@ var ChatLayout = class {
|
|
|
5514
6522
|
setKnowledgeStatus(status) {
|
|
5515
6523
|
this.knowledgeStatus = formatKnowledgeFooterStatus(status);
|
|
5516
6524
|
}
|
|
6525
|
+
setTaskPlan(plan) {
|
|
6526
|
+
const change = detectTaskPlanChange(this.taskPlan, plan);
|
|
6527
|
+
this.taskPlan = plan && plan.items.length > 0 ? plan : void 0;
|
|
6528
|
+
return change;
|
|
6529
|
+
}
|
|
6530
|
+
setTaskPlanNotice(line) {
|
|
6531
|
+
this.taskPlanNoticeLine = line;
|
|
6532
|
+
}
|
|
6533
|
+
clearTaskPlan(now = /* @__PURE__ */ new Date()) {
|
|
6534
|
+
if (!this.taskPlan) return;
|
|
6535
|
+
const cleared = {
|
|
6536
|
+
items: [],
|
|
6537
|
+
updatedAt: now.toISOString()
|
|
6538
|
+
};
|
|
6539
|
+
this.taskPlan = void 0;
|
|
6540
|
+
this.taskPlanNoticeLine = void 0;
|
|
6541
|
+
return cleared;
|
|
6542
|
+
}
|
|
5517
6543
|
isReady() {
|
|
5518
6544
|
return this.status === "ready";
|
|
5519
6545
|
}
|
|
@@ -5536,7 +6562,10 @@ var ChatLayout = class {
|
|
|
5536
6562
|
this.submitCommand = submit;
|
|
5537
6563
|
}
|
|
5538
6564
|
setInputValue(value) {
|
|
5539
|
-
this.
|
|
6565
|
+
this.promptValue = value;
|
|
6566
|
+
this.promptCursor = value.length;
|
|
6567
|
+
this.pastedContent.clear();
|
|
6568
|
+
this.pasteCounter = 0;
|
|
5540
6569
|
}
|
|
5541
6570
|
getConversationTurns() {
|
|
5542
6571
|
return this.messages.flatMap((message) => {
|
|
@@ -5556,10 +6585,10 @@ var ChatLayout = class {
|
|
|
5556
6585
|
});
|
|
5557
6586
|
}
|
|
5558
6587
|
get focused() {
|
|
5559
|
-
return this.
|
|
6588
|
+
return this.inputFocused;
|
|
5560
6589
|
}
|
|
5561
6590
|
set focused(value) {
|
|
5562
|
-
this.
|
|
6591
|
+
this.inputFocused = value;
|
|
5563
6592
|
}
|
|
5564
6593
|
handleInput(data) {
|
|
5565
6594
|
if (this.cancelPending && matchesKey(data, "escape")) {
|
|
@@ -5568,15 +6597,15 @@ var ChatLayout = class {
|
|
|
5568
6597
|
}
|
|
5569
6598
|
if (this.handleModalInput(data)) return;
|
|
5570
6599
|
if (this.handleSlashSuggestionInput(data)) return;
|
|
6600
|
+
if (this.handlePromptPasteInput(data)) return;
|
|
6601
|
+
if (this.handlePromptNewLineInput(data)) return;
|
|
6602
|
+
if (this.handlePromptSubmitInput(data)) return;
|
|
5571
6603
|
if (this.handleThreadScrollInput(data)) return;
|
|
6604
|
+
if (this.handlePromptVerticalCursorInput(data)) return;
|
|
5572
6605
|
if (this.handlePromptHistoryInput(data)) return;
|
|
5573
|
-
|
|
5574
|
-
this.input.handleInput(data);
|
|
5575
|
-
if (this.input.getValue() !== previousInput) this.promptHistory.resetBrowsing();
|
|
5576
|
-
}
|
|
5577
|
-
invalidate() {
|
|
5578
|
-
this.input.invalidate();
|
|
6606
|
+
this.handlePromptEditInput(data);
|
|
5579
6607
|
}
|
|
6608
|
+
invalidate() {}
|
|
5580
6609
|
render(width) {
|
|
5581
6610
|
const safeWidth = Math.max(20, width);
|
|
5582
6611
|
const footerLines = this.getActiveModal() ? this.renderModalHelp(safeWidth) : this.renderPrompt(safeWidth);
|
|
@@ -5604,6 +6633,7 @@ var ChatLayout = class {
|
|
|
5604
6633
|
return [...this.renderThreadMessageLines(messageLines, innerWidth, width, message.kind === "user"), ...spacer];
|
|
5605
6634
|
});
|
|
5606
6635
|
if (this.ephemeralLine) lines.push(...this.renderThreadMessageLines([` ${this.ephemeralLine}`], innerWidth, width, false));
|
|
6636
|
+
if (this.taskPlanNoticeLine) lines.push(...this.renderThreadMessageLines([` ${this.taskPlanNoticeLine}`], innerWidth, width, false));
|
|
5607
6637
|
if (this.noticeLine) lines.push(...this.renderThreadMessageLines([` ${this.noticeLine}`], innerWidth, width, false));
|
|
5608
6638
|
return lines;
|
|
5609
6639
|
}
|
|
@@ -5619,17 +6649,69 @@ var ChatLayout = class {
|
|
|
5619
6649
|
const bottom = `└${"─".repeat(Math.max(0, width - 2))}┘`;
|
|
5620
6650
|
const prefix = "> ";
|
|
5621
6651
|
const innerWidth = Math.max(1, width - 4 - 2);
|
|
5622
|
-
const
|
|
6652
|
+
const inputLines = this.promptHint ? [truncateToWidth(ui.label(this.promptHint), innerWidth, "…", true)] : this.renderPromptInputLines(innerWidth);
|
|
5623
6653
|
const statusInnerWidth = Math.max(1, width - 2);
|
|
5624
6654
|
const status = truncateToWidth(` ${formatStatusLine(this.folderName, this.modelLabel, this.status, this.knowledgeStatus, statusInnerWidth)} `, width, "…", true);
|
|
5625
6655
|
return [
|
|
5626
6656
|
...this.renderSlashSuggestions(width),
|
|
6657
|
+
...this.renderTaskPlan(width),
|
|
5627
6658
|
top,
|
|
5628
|
-
`│ ${prefix}${
|
|
6659
|
+
...inputLines.map((line, index) => `│ ${index === 0 ? prefix : " "}${padPromptInputLine(line, innerWidth)} │`),
|
|
5629
6660
|
bottom,
|
|
5630
6661
|
status
|
|
5631
6662
|
];
|
|
5632
6663
|
}
|
|
6664
|
+
renderPromptInputLines(innerWidth) {
|
|
6665
|
+
const value = this.promptValue;
|
|
6666
|
+
if (!value.includes("\n")) return [this.renderPromptLineWithCursor(value, this.promptCursor, innerWidth)];
|
|
6667
|
+
const rows = this.getPromptRows(innerWidth);
|
|
6668
|
+
const cursorRowIndex = rows.findIndex((row) => this.promptCursor >= row.start && this.promptCursor <= row.end);
|
|
6669
|
+
const latestStart = Math.max(0, rows.length - PROMPT_VISIBLE_CONTENT_LINES);
|
|
6670
|
+
const visibleStart = cursorRowIndex === -1 ? latestStart : Math.min(Math.max(0, cursorRowIndex - 2), latestStart);
|
|
6671
|
+
return rows.slice(visibleStart, visibleStart + PROMPT_VISIBLE_CONTENT_LINES).map((row) => {
|
|
6672
|
+
if (this.promptCursor >= row.start && this.promptCursor <= row.end) return this.renderPromptLineWithCursor(row.text, this.promptCursor - row.start, innerWidth);
|
|
6673
|
+
return truncateToWidth(row.text.length === 0 ? " " : row.text, innerWidth, "…", true);
|
|
6674
|
+
});
|
|
6675
|
+
}
|
|
6676
|
+
getPromptRows(width) {
|
|
6677
|
+
const rows = [];
|
|
6678
|
+
let offset = 0;
|
|
6679
|
+
for (const line of this.promptValue.split("\n")) {
|
|
6680
|
+
if (line.length === 0) rows.push({
|
|
6681
|
+
text: "",
|
|
6682
|
+
start: offset,
|
|
6683
|
+
end: offset
|
|
6684
|
+
});
|
|
6685
|
+
else for (let index = 0; index < line.length; index += width) {
|
|
6686
|
+
const text = line.slice(index, index + width);
|
|
6687
|
+
rows.push({
|
|
6688
|
+
text,
|
|
6689
|
+
start: offset + index,
|
|
6690
|
+
end: offset + index + text.length
|
|
6691
|
+
});
|
|
6692
|
+
}
|
|
6693
|
+
offset += line.length + 1;
|
|
6694
|
+
}
|
|
6695
|
+
return rows.length > 0 ? rows : [{
|
|
6696
|
+
text: "",
|
|
6697
|
+
start: 0,
|
|
6698
|
+
end: 0
|
|
6699
|
+
}];
|
|
6700
|
+
}
|
|
6701
|
+
renderPromptLineWithCursor(text, cursor, width) {
|
|
6702
|
+
const safeCursor = Math.max(0, Math.min(cursor, text.length));
|
|
6703
|
+
const windowStart = safeCursor >= width ? safeCursor - width + 1 : 0;
|
|
6704
|
+
const visibleText = text.slice(windowStart, windowStart + width);
|
|
6705
|
+
const visibleCursor = safeCursor - windowStart;
|
|
6706
|
+
const beforeCursor = visibleText.slice(0, visibleCursor);
|
|
6707
|
+
const cursorChar = visibleText[visibleCursor] ?? " ";
|
|
6708
|
+
const afterCursor = visibleText.slice(visibleCursor + cursorChar.length);
|
|
6709
|
+
return truncateToWidth(`${beforeCursor}${this.inputFocused ? CURSOR_MARKER : ""}\u001b[7m${cursorChar}\u001b[27m${afterCursor}`, width, "…", true);
|
|
6710
|
+
}
|
|
6711
|
+
renderTaskPlan(width) {
|
|
6712
|
+
if (!this.taskPlan) return [];
|
|
6713
|
+
return formatTaskPlanForTui(this.taskPlan, Math.max(1, width));
|
|
6714
|
+
}
|
|
5633
6715
|
renderSlashSuggestions(width) {
|
|
5634
6716
|
const suggestions = this.getSlashSuggestions();
|
|
5635
6717
|
if (suggestions.length === 0 || this.promptHint) return [];
|
|
@@ -5676,6 +6758,14 @@ var ChatLayout = class {
|
|
|
5676
6758
|
this.exitAgent();
|
|
5677
6759
|
return true;
|
|
5678
6760
|
}
|
|
6761
|
+
if (action.value === "__topchester_abort__") {
|
|
6762
|
+
this.addMessage({
|
|
6763
|
+
kind: "user",
|
|
6764
|
+
text: action.label,
|
|
6765
|
+
modelContext: false
|
|
6766
|
+
});
|
|
6767
|
+
return true;
|
|
6768
|
+
}
|
|
5679
6769
|
this.submitModalAction(action.value ?? action.label);
|
|
5680
6770
|
return true;
|
|
5681
6771
|
}
|
|
@@ -5718,17 +6808,196 @@ var ChatLayout = class {
|
|
|
5718
6808
|
handlePromptHistoryInput(data) {
|
|
5719
6809
|
if (this.promptHint) return false;
|
|
5720
6810
|
if (isUpKey(data)) {
|
|
5721
|
-
const prompt = this.promptHistory.previous(this.
|
|
5722
|
-
if (prompt !== void 0)
|
|
6811
|
+
const prompt = this.promptHistory.previous(this.promptValue);
|
|
6812
|
+
if (prompt !== void 0) {
|
|
6813
|
+
this.promptValue = prompt;
|
|
6814
|
+
this.promptCursor = prompt.length;
|
|
6815
|
+
}
|
|
5723
6816
|
return true;
|
|
5724
6817
|
}
|
|
5725
6818
|
if (isDownKey(data)) {
|
|
5726
6819
|
const prompt = this.promptHistory.next();
|
|
5727
|
-
if (prompt !== void 0)
|
|
6820
|
+
if (prompt !== void 0) {
|
|
6821
|
+
this.promptValue = prompt;
|
|
6822
|
+
this.promptCursor = prompt.length;
|
|
6823
|
+
}
|
|
5728
6824
|
return true;
|
|
5729
6825
|
}
|
|
5730
6826
|
return false;
|
|
5731
6827
|
}
|
|
6828
|
+
handlePromptVerticalCursorInput(data) {
|
|
6829
|
+
if (this.promptHint || !this.promptValue.includes("\n")) return false;
|
|
6830
|
+
if (isUpKey(data)) {
|
|
6831
|
+
if (this.canMovePromptCursorVertically(-1)) {
|
|
6832
|
+
this.movePromptCursorVertically(-1);
|
|
6833
|
+
return true;
|
|
6834
|
+
}
|
|
6835
|
+
if (this.promptCursor > 0) {
|
|
6836
|
+
this.promptCursor = this.getCurrentPromptLineStart();
|
|
6837
|
+
return true;
|
|
6838
|
+
}
|
|
6839
|
+
return false;
|
|
6840
|
+
}
|
|
6841
|
+
if (isDownKey(data)) {
|
|
6842
|
+
if (this.canMovePromptCursorVertically(1)) {
|
|
6843
|
+
this.movePromptCursorVertically(1);
|
|
6844
|
+
return true;
|
|
6845
|
+
}
|
|
6846
|
+
if (this.promptCursor < this.promptValue.length) {
|
|
6847
|
+
this.promptCursor = this.getCurrentPromptLineEnd();
|
|
6848
|
+
return true;
|
|
6849
|
+
}
|
|
6850
|
+
return false;
|
|
6851
|
+
}
|
|
6852
|
+
return false;
|
|
6853
|
+
}
|
|
6854
|
+
canMovePromptCursorVertically(delta) {
|
|
6855
|
+
const lines = this.promptValue.split("\n");
|
|
6856
|
+
const current = this.getPromptLineCursor(lines);
|
|
6857
|
+
if (delta === -1) return current.line > 0;
|
|
6858
|
+
return current.line < lines.length - 1;
|
|
6859
|
+
}
|
|
6860
|
+
handlePromptNewLineInput(data) {
|
|
6861
|
+
if (this.promptHint || !isNewLineKey(data)) return false;
|
|
6862
|
+
this.insertPromptText("\n");
|
|
6863
|
+
this.promptHistory.resetBrowsing();
|
|
6864
|
+
return true;
|
|
6865
|
+
}
|
|
6866
|
+
handlePromptSubmitInput(data) {
|
|
6867
|
+
if (this.promptHint || !isEnterKey(data)) return false;
|
|
6868
|
+
this.submitPromptValue();
|
|
6869
|
+
return true;
|
|
6870
|
+
}
|
|
6871
|
+
handlePromptPasteInput(data) {
|
|
6872
|
+
if (this.promptHint) return false;
|
|
6873
|
+
if (this.pasteBuffer !== void 0) {
|
|
6874
|
+
this.pasteBuffer += data;
|
|
6875
|
+
this.flushPromptPasteBuffer();
|
|
6876
|
+
return true;
|
|
6877
|
+
}
|
|
6878
|
+
const startIndex = data.indexOf(BRACKETED_PASTE_START);
|
|
6879
|
+
if (startIndex === -1) return false;
|
|
6880
|
+
const beforePaste = data.slice(0, startIndex);
|
|
6881
|
+
if (beforePaste.length > 0) this.insertPromptText(beforePaste);
|
|
6882
|
+
this.pasteBuffer = data.slice(startIndex + 6);
|
|
6883
|
+
this.flushPromptPasteBuffer();
|
|
6884
|
+
this.promptHistory.resetBrowsing();
|
|
6885
|
+
return true;
|
|
6886
|
+
}
|
|
6887
|
+
flushPromptPasteBuffer() {
|
|
6888
|
+
if (this.pasteBuffer === void 0) return;
|
|
6889
|
+
const endIndex = this.pasteBuffer.indexOf(BRACKETED_PASTE_END);
|
|
6890
|
+
if (endIndex === -1) return;
|
|
6891
|
+
const pasted = this.pasteBuffer.slice(0, endIndex);
|
|
6892
|
+
const remaining = this.pasteBuffer.slice(endIndex + 6);
|
|
6893
|
+
this.pasteBuffer = void 0;
|
|
6894
|
+
this.insertPastedText(pasted);
|
|
6895
|
+
if (remaining.length > 0) this.handleInput(remaining);
|
|
6896
|
+
}
|
|
6897
|
+
insertPastedText(text) {
|
|
6898
|
+
const normalizedText = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\t/g, " ");
|
|
6899
|
+
const trimmedText = normalizedText.trim();
|
|
6900
|
+
if (trimmedText.length === 0) return;
|
|
6901
|
+
const lineCount = trimmedText.split("\n").length;
|
|
6902
|
+
if (lineCount >= PASTE_PREVIEW_MIN_LINES || trimmedText.length >= PASTE_PREVIEW_MIN_CHARS) {
|
|
6903
|
+
this.pasteCounter += 1;
|
|
6904
|
+
const marker = `[Pasted #${this.pasteCounter} ${lineCount} lines ${trimmedText.length} chars]`;
|
|
6905
|
+
this.pastedContent.set(marker, trimmedText);
|
|
6906
|
+
this.insertPromptText(marker);
|
|
6907
|
+
return;
|
|
6908
|
+
}
|
|
6909
|
+
this.insertPromptText(normalizedText);
|
|
6910
|
+
}
|
|
6911
|
+
insertPromptText(text) {
|
|
6912
|
+
this.promptValue = `${this.promptValue.slice(0, this.promptCursor)}${text}${this.promptValue.slice(this.promptCursor)}`;
|
|
6913
|
+
this.promptCursor += text.length;
|
|
6914
|
+
}
|
|
6915
|
+
expandPastedContent(value) {
|
|
6916
|
+
let expanded = value;
|
|
6917
|
+
for (const [marker, content] of this.pastedContent) expanded = expanded.split(marker).join(content);
|
|
6918
|
+
return expanded;
|
|
6919
|
+
}
|
|
6920
|
+
submitPromptValue() {
|
|
6921
|
+
if (this.promptValue.trim().length === 0) return;
|
|
6922
|
+
const message = this.expandPastedContent(this.promptValue).trim();
|
|
6923
|
+
this.addMessage(userMessage(message));
|
|
6924
|
+
this.promptValue = "";
|
|
6925
|
+
this.promptCursor = 0;
|
|
6926
|
+
this.pastedContent.clear();
|
|
6927
|
+
this.pasteCounter = 0;
|
|
6928
|
+
this.submitUserInput(message);
|
|
6929
|
+
}
|
|
6930
|
+
handlePromptEditInput(data) {
|
|
6931
|
+
if (this.promptHint) return false;
|
|
6932
|
+
if (matchesKey(data, "left") || data === "\x1B[D") {
|
|
6933
|
+
this.promptCursor = Math.max(0, this.promptCursor - 1);
|
|
6934
|
+
return true;
|
|
6935
|
+
}
|
|
6936
|
+
if (matchesKey(data, "right") || data === "\x1B[C") {
|
|
6937
|
+
this.promptCursor = Math.min(this.promptValue.length, this.promptCursor + 1);
|
|
6938
|
+
return true;
|
|
6939
|
+
}
|
|
6940
|
+
if (isHomeKey(data)) {
|
|
6941
|
+
this.promptCursor = this.getCurrentPromptLineStart();
|
|
6942
|
+
return true;
|
|
6943
|
+
}
|
|
6944
|
+
if (isEndKey(data)) {
|
|
6945
|
+
this.promptCursor = this.getCurrentPromptLineEnd();
|
|
6946
|
+
return true;
|
|
6947
|
+
}
|
|
6948
|
+
if (matchesKey(data, "backspace") || data === "" || data === "\b") {
|
|
6949
|
+
if (this.promptCursor > 0) {
|
|
6950
|
+
this.promptValue = `${this.promptValue.slice(0, this.promptCursor - 1)}${this.promptValue.slice(this.promptCursor)}`;
|
|
6951
|
+
this.promptCursor -= 1;
|
|
6952
|
+
this.promptHistory.resetBrowsing();
|
|
6953
|
+
}
|
|
6954
|
+
return true;
|
|
6955
|
+
}
|
|
6956
|
+
if (matchesKey(data, "delete") || data === "\x1B[3~") {
|
|
6957
|
+
if (this.promptCursor < this.promptValue.length) {
|
|
6958
|
+
this.promptValue = `${this.promptValue.slice(0, this.promptCursor)}${this.promptValue.slice(this.promptCursor + 1)}`;
|
|
6959
|
+
this.promptHistory.resetBrowsing();
|
|
6960
|
+
}
|
|
6961
|
+
return true;
|
|
6962
|
+
}
|
|
6963
|
+
const printable = decodeKittyPrintable(data) ?? (isPrintableInput(data) ? data : void 0);
|
|
6964
|
+
if (printable !== void 0) {
|
|
6965
|
+
this.insertPromptText(printable);
|
|
6966
|
+
this.promptHistory.resetBrowsing();
|
|
6967
|
+
return true;
|
|
6968
|
+
}
|
|
6969
|
+
return false;
|
|
6970
|
+
}
|
|
6971
|
+
movePromptCursorVertically(delta) {
|
|
6972
|
+
const lines = this.promptValue.split("\n");
|
|
6973
|
+
const current = this.getPromptLineCursor(lines);
|
|
6974
|
+
const targetLine = Math.max(0, Math.min(lines.length - 1, current.line + delta));
|
|
6975
|
+
const targetColumn = Math.min(current.column, lines[targetLine]?.length ?? 0);
|
|
6976
|
+
this.promptCursor = lines.slice(0, targetLine).reduce((total, line) => total + line.length + 1, 0) + targetColumn;
|
|
6977
|
+
}
|
|
6978
|
+
getPromptLineCursor(lines = this.promptValue.split("\n")) {
|
|
6979
|
+
let offset = 0;
|
|
6980
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
6981
|
+
const line = lines[lineIndex] ?? "";
|
|
6982
|
+
const end = offset + line.length;
|
|
6983
|
+
if (this.promptCursor <= end || lineIndex === lines.length - 1) return {
|
|
6984
|
+
line: lineIndex,
|
|
6985
|
+
column: Math.max(0, this.promptCursor - offset)
|
|
6986
|
+
};
|
|
6987
|
+
offset = end + 1;
|
|
6988
|
+
}
|
|
6989
|
+
return {
|
|
6990
|
+
line: 0,
|
|
6991
|
+
column: 0
|
|
6992
|
+
};
|
|
6993
|
+
}
|
|
6994
|
+
getCurrentPromptLineStart() {
|
|
6995
|
+
return this.promptValue.lastIndexOf("\n", Math.max(0, this.promptCursor - 1)) + 1;
|
|
6996
|
+
}
|
|
6997
|
+
getCurrentPromptLineEnd() {
|
|
6998
|
+
const end = this.promptValue.indexOf("\n", this.promptCursor);
|
|
6999
|
+
return end === -1 ? this.promptValue.length : end;
|
|
7000
|
+
}
|
|
5732
7001
|
handleSlashSuggestionInput(data) {
|
|
5733
7002
|
const suggestions = this.getSlashSuggestions();
|
|
5734
7003
|
if (suggestions.length === 0) {
|
|
@@ -5747,19 +7016,19 @@ var ChatLayout = class {
|
|
|
5747
7016
|
this.completeSlashSuggestion(suggestions);
|
|
5748
7017
|
return true;
|
|
5749
7018
|
}
|
|
5750
|
-
if (isEnterKey(data) && this.
|
|
7019
|
+
if (isEnterKey(data) && this.promptValue.trim() !== suggestions[this.activeSlashSuggestionIndex]?.value) {
|
|
5751
7020
|
this.completeSlashSuggestion(suggestions);
|
|
5752
7021
|
return true;
|
|
5753
7022
|
}
|
|
5754
7023
|
return false;
|
|
5755
7024
|
}
|
|
5756
7025
|
completeSlashSuggestion(suggestions) {
|
|
5757
|
-
this.
|
|
5758
|
-
this.
|
|
7026
|
+
this.promptValue = suggestions[this.activeSlashSuggestionIndex]?.value ?? this.promptValue;
|
|
7027
|
+
this.promptCursor = this.promptValue.length;
|
|
5759
7028
|
this.promptHistory.resetBrowsing();
|
|
5760
7029
|
}
|
|
5761
7030
|
getSlashSuggestions() {
|
|
5762
|
-
return getSlashCommandSuggestions(this.
|
|
7031
|
+
return getSlashCommandSuggestions(this.promptValue);
|
|
5763
7032
|
}
|
|
5764
7033
|
getActiveModal() {
|
|
5765
7034
|
return this.messages[this.getActiveModalIndex()];
|
|
@@ -5772,6 +7041,7 @@ var ChatLayout = class {
|
|
|
5772
7041
|
this.submitUserInput(message);
|
|
5773
7042
|
}
|
|
5774
7043
|
submitUserInput(message) {
|
|
7044
|
+
this.setTaskPlanNotice(void 0);
|
|
5775
7045
|
this.promptHistory.add(message);
|
|
5776
7046
|
if (message.startsWith("/")) this.submitCommand?.(message);
|
|
5777
7047
|
else this.submitMessage?.(message);
|
|
@@ -5780,8 +7050,15 @@ var ChatLayout = class {
|
|
|
5780
7050
|
function colorUserMessageBorder(line) {
|
|
5781
7051
|
return line.replace("▌", ui.modelInline("▌"));
|
|
5782
7052
|
}
|
|
5783
|
-
function
|
|
5784
|
-
return (
|
|
7053
|
+
function padPromptInputLine(line, width) {
|
|
7054
|
+
return `${line}${" ".repeat(Math.max(0, width - stripAnsi(line).length))}`;
|
|
7055
|
+
}
|
|
7056
|
+
function isPrintableInput(data) {
|
|
7057
|
+
if (data.length === 0) return false;
|
|
7058
|
+
return [...data].every((char) => {
|
|
7059
|
+
const code = char.charCodeAt(0);
|
|
7060
|
+
return code >= 32 && code !== 127 && (code < 128 || code > 159);
|
|
7061
|
+
});
|
|
5785
7062
|
}
|
|
5786
7063
|
//#endregion
|
|
5787
7064
|
//#region src/agent/conversation.ts
|
|
@@ -5793,58 +7070,6 @@ function buildConversationPrompt(turns, latestMessage) {
|
|
|
5793
7070
|
return lines.join("\n\n");
|
|
5794
7071
|
}
|
|
5795
7072
|
//#endregion
|
|
5796
|
-
//#region src/agent/events.ts
|
|
5797
|
-
const agentEvent = {
|
|
5798
|
-
status(status) {
|
|
5799
|
-
return {
|
|
5800
|
-
type: "status",
|
|
5801
|
-
status
|
|
5802
|
-
};
|
|
5803
|
-
},
|
|
5804
|
-
systemMessage(text) {
|
|
5805
|
-
return {
|
|
5806
|
-
type: "message",
|
|
5807
|
-
role: "system",
|
|
5808
|
-
text
|
|
5809
|
-
};
|
|
5810
|
-
},
|
|
5811
|
-
assistantMessage(text, meta) {
|
|
5812
|
-
return meta === void 0 ? {
|
|
5813
|
-
type: "message",
|
|
5814
|
-
role: "assistant",
|
|
5815
|
-
text
|
|
5816
|
-
} : {
|
|
5817
|
-
type: "message",
|
|
5818
|
-
role: "assistant",
|
|
5819
|
-
text,
|
|
5820
|
-
meta
|
|
5821
|
-
};
|
|
5822
|
-
},
|
|
5823
|
-
toolCall(call, label) {
|
|
5824
|
-
return {
|
|
5825
|
-
type: "tool_call",
|
|
5826
|
-
call,
|
|
5827
|
-
label
|
|
5828
|
-
};
|
|
5829
|
-
},
|
|
5830
|
-
knowledgeStatus(status, guidance) {
|
|
5831
|
-
return guidance === void 0 ? {
|
|
5832
|
-
type: "knowledge_status",
|
|
5833
|
-
status
|
|
5834
|
-
} : {
|
|
5835
|
-
type: "knowledge_status",
|
|
5836
|
-
status,
|
|
5837
|
-
guidance
|
|
5838
|
-
};
|
|
5839
|
-
},
|
|
5840
|
-
choice(options) {
|
|
5841
|
-
return {
|
|
5842
|
-
type: "choice",
|
|
5843
|
-
...options
|
|
5844
|
-
};
|
|
5845
|
-
}
|
|
5846
|
-
};
|
|
5847
|
-
//#endregion
|
|
5848
7073
|
//#region src/agent/health.ts
|
|
5849
7074
|
async function checkAgentReady(modelGateway, abortSignal) {
|
|
5850
7075
|
const abortController = new AbortController();
|
|
@@ -5877,20 +7102,25 @@ function isAbortError(error) {
|
|
|
5877
7102
|
//#endregion
|
|
5878
7103
|
//#region src/agent/tools/executor.ts
|
|
5879
7104
|
async function executeToolCall(workspaceRoot, call, options = {}) {
|
|
5880
|
-
const definition = getToolDefinition(call.tool);
|
|
5881
7105
|
const startedAt = Date.now();
|
|
5882
7106
|
const context = {
|
|
5883
7107
|
workspaceRoot,
|
|
5884
7108
|
pathEnv: options.pathEnv,
|
|
5885
|
-
logger: options.logger
|
|
7109
|
+
logger: options.logger,
|
|
7110
|
+
taskPlan: options.taskPlan
|
|
5886
7111
|
};
|
|
5887
|
-
options.logger?.debug({
|
|
5888
|
-
event: "tool_call",
|
|
5889
|
-
tool: call.tool,
|
|
5890
|
-
args: summarizeToolArgs(call)
|
|
5891
|
-
}, "tool call");
|
|
5892
7112
|
try {
|
|
5893
|
-
const
|
|
7113
|
+
const definition = getToolDefinition(call.tool);
|
|
7114
|
+
const parsedCall = {
|
|
7115
|
+
...call,
|
|
7116
|
+
args: definition.argsSchema.parse(call.args)
|
|
7117
|
+
};
|
|
7118
|
+
options.logger?.debug({
|
|
7119
|
+
event: "tool_call",
|
|
7120
|
+
tool: parsedCall.tool,
|
|
7121
|
+
args: summarizeToolArgs(parsedCall)
|
|
7122
|
+
}, "tool call");
|
|
7123
|
+
const result = await definition.execute(context, parsedCall.args);
|
|
5894
7124
|
const durationMs = Date.now() - startedAt;
|
|
5895
7125
|
options.logger?.debug({
|
|
5896
7126
|
event: "tool_result",
|
|
@@ -5910,23 +7140,40 @@ async function executeToolCall(workspaceRoot, call, options = {}) {
|
|
|
5910
7140
|
}, "tool result content");
|
|
5911
7141
|
return result;
|
|
5912
7142
|
} catch (error) {
|
|
5913
|
-
|
|
5914
|
-
|
|
7143
|
+
const message = formatErrorMessage(error);
|
|
7144
|
+
const logPayload = {
|
|
7145
|
+
event: "tool_result",
|
|
5915
7146
|
tool: call.tool,
|
|
5916
7147
|
durationMs: Date.now() - startedAt,
|
|
7148
|
+
error: message,
|
|
5917
7149
|
err: error
|
|
5918
|
-
}
|
|
5919
|
-
|
|
7150
|
+
};
|
|
7151
|
+
if (typeof options.logger?.warn === "function") options.logger.warn(logPayload, "tool returned error");
|
|
7152
|
+
else options.logger?.debug(logPayload, "tool returned error");
|
|
7153
|
+
return {
|
|
7154
|
+
tool: call.tool,
|
|
7155
|
+
content: `Tool ${call.tool} failed: ${message}`,
|
|
7156
|
+
error: message,
|
|
7157
|
+
warning: message
|
|
7158
|
+
};
|
|
5920
7159
|
}
|
|
5921
7160
|
}
|
|
5922
7161
|
function summarizeToolArgs(call) {
|
|
7162
|
+
if (call.tool === "plan_todo") {
|
|
7163
|
+
const activeItem = call.args.items.find((item) => item.status === "in_progress")?.text;
|
|
7164
|
+
return {
|
|
7165
|
+
itemCount: call.args.items.length,
|
|
7166
|
+
activeItem,
|
|
7167
|
+
completedCount: call.args.items.filter((item) => item.status === "completed").length
|
|
7168
|
+
};
|
|
7169
|
+
}
|
|
5923
7170
|
if (call.tool === "write_file") return {
|
|
5924
7171
|
path: call.args.path,
|
|
5925
7172
|
contentLength: call.args.content.length,
|
|
5926
7173
|
lineCount: countLogicalLines(call.args.content),
|
|
5927
7174
|
createParentDirs: Boolean(call.args.create_parent_dirs),
|
|
5928
7175
|
overwrite: Boolean(call.args.overwrite),
|
|
5929
|
-
|
|
7176
|
+
expectedCurrentHashProvided: Boolean(call.args.expected_current_hash)
|
|
5930
7177
|
};
|
|
5931
7178
|
if (call.tool !== "edit_file") return call.args;
|
|
5932
7179
|
return {
|
|
@@ -5934,10 +7181,15 @@ function summarizeToolArgs(call) {
|
|
|
5934
7181
|
editCount: call.args.edits.length,
|
|
5935
7182
|
oldTextLengths: call.args.edits.map((edit) => edit.old_text.length),
|
|
5936
7183
|
newTextLengths: call.args.edits.map((edit) => edit.new_text.length),
|
|
5937
|
-
|
|
7184
|
+
expectedCurrentHashProvided: Boolean(call.args.expected_current_hash)
|
|
5938
7185
|
};
|
|
5939
7186
|
}
|
|
5940
7187
|
function summarizeToolResult(result) {
|
|
7188
|
+
if (result.tool === "plan_todo") return {
|
|
7189
|
+
itemCount: result.plan.items.length,
|
|
7190
|
+
activeItem: result.currentItem,
|
|
7191
|
+
completedCount: result.completedCount
|
|
7192
|
+
};
|
|
5941
7193
|
if (result.tool === "inspect_command") return {
|
|
5942
7194
|
cwd: result.cwd,
|
|
5943
7195
|
exitCode: result.exitCode,
|
|
@@ -6006,6 +7258,9 @@ function countLogicalLines(content) {
|
|
|
6006
7258
|
const withoutTrailingLineEnding = content.replace(/\r?\n$/u, "");
|
|
6007
7259
|
return withoutTrailingLineEnding.length === 0 ? 1 : withoutTrailingLineEnding.split(/\r?\n/u).length;
|
|
6008
7260
|
}
|
|
7261
|
+
function formatErrorMessage(error) {
|
|
7262
|
+
return error instanceof Error ? error.message : String(error);
|
|
7263
|
+
}
|
|
6009
7264
|
//#endregion
|
|
6010
7265
|
//#region src/agent/prompts.ts
|
|
6011
7266
|
function getChatSystemPrompt() {
|
|
@@ -6029,6 +7284,11 @@ function getChatSystemPrompt() {
|
|
|
6029
7284
|
"",
|
|
6030
7285
|
"Tool use:",
|
|
6031
7286
|
"- When using a tool, output exactly one tool JSON object and no prose, markdown, or additional JSON. After the tool result, either output the next single tool JSON object or a final plain-text answer.",
|
|
7287
|
+
"- You already have permission to use the available tools to handle the user's request. Do not ask the user to provide tool results or permission to use an available tool.",
|
|
7288
|
+
"- Do not claim to have read, created, edited, staged, committed, or run anything unless a tool result in this turn confirms it.",
|
|
7289
|
+
"- Use plan_todo for non-trivial multi-step work before the first substantive repository tool call.",
|
|
7290
|
+
"- Keep plan_todo items short, user-safe, and usually 2 to 6 items. Maintain exactly one in_progress item while work remains, update it after major progress changes, and clear it only when abandoning the plan or when no visible plan is useful.",
|
|
7291
|
+
"- Do not use plan_todo for simple one-step answers, tiny reads, or trivial edits.",
|
|
6032
7292
|
"- Use read/search tools when the user asks about files, code, symbols, usages, tests, or project behavior.",
|
|
6033
7293
|
"- Use find_file for path or filename lookup. Use grep for text inside files. If grep output mentions another path, treat that mentioned path as content until find_file or read_file confirms it exists.",
|
|
6034
7294
|
"- Use list_files, grep, find_file, and read_file for exact file listing, search, lookup, and reading tasks.",
|
|
@@ -6037,8 +7297,10 @@ function getChatSystemPrompt() {
|
|
|
6037
7297
|
"- Use inspect_command only for quick read-only repo orientation when a short familiar command chain is clearer than several dedicated tool calls.",
|
|
6038
7298
|
"- inspect_command is not a shell. Unsafe commands, shell expansion, scripts, installs, builds, tests, network access, and file mutation are not available through it.",
|
|
6039
7299
|
"- Use read_file before editing a file so your edit is based on current file content and hash metadata.",
|
|
7300
|
+
"- When passing expected_current_hash to edit_file or write_file, use the current pre-edit/pre-write hash from the latest read_file result for that exact file. Never invent it and never use a predicted after-edit or after-write hash.",
|
|
6040
7301
|
"- Use edit_file for targeted edits to existing files. Make multiple disjoint edits for the same file in one call when possible.",
|
|
6041
|
-
"- Use write_file to create new files by default. It fails when the file already exists unless you are replacing the whole file with overwrite:true and
|
|
7302
|
+
"- Use write_file to create new files by default. It fails when the file already exists unless you are replacing the whole file with overwrite:true and expected_current_hash from read_file.",
|
|
7303
|
+
"- When the user asks you to create a new file, call write_file. Do not answer that the file was created until write_file succeeds.",
|
|
6042
7304
|
"- Pass write_file create_parent_dirs:true only when the user intent clearly includes creating that folder path.",
|
|
6043
7305
|
"- Do not use inspect_command for file creation or file mutation.",
|
|
6044
7306
|
"- Keep edit_file old_text small but unique. Do not include line labels or grep prefixes in old_text; use exact file text only.",
|
|
@@ -6050,34 +7312,83 @@ function getChatSystemPrompt() {
|
|
|
6050
7312
|
}
|
|
6051
7313
|
//#endregion
|
|
6052
7314
|
//#region src/agent/runtime.ts
|
|
6053
|
-
const MAX_TOOL_CALLS_PER_TURN =
|
|
7315
|
+
const MAX_TOOL_CALLS_PER_TURN = 75;
|
|
6054
7316
|
var TopchesterAgentRuntime = class {
|
|
6055
7317
|
context;
|
|
7318
|
+
taskPlan = createTaskPlanController();
|
|
7319
|
+
/**
|
|
7320
|
+
* Holds the shared application context for one runtime instance.
|
|
7321
|
+
* The runtime does not own those dependencies; it coordinates the
|
|
7322
|
+
* workspace, model gateway, logger, config, and task-plan state that
|
|
7323
|
+
* are passed in by the CLI or TUI layer.
|
|
7324
|
+
*/
|
|
6056
7325
|
constructor(context) {
|
|
6057
7326
|
this.context = context;
|
|
6058
7327
|
}
|
|
7328
|
+
/**
|
|
7329
|
+
* Performs the lightweight startup model check used by the interactive
|
|
7330
|
+
* agent before accepting work. The check is intentionally non-blocking
|
|
7331
|
+
* from the user's point of view: timeout and failure both produce a
|
|
7332
|
+
* visible status message, but the runtime still moves to ready so the
|
|
7333
|
+
* user can continue.
|
|
7334
|
+
*/
|
|
6059
7335
|
async checkAgent(abortSignal) {
|
|
6060
7336
|
const result = await checkAgentReady(this.context.modelGateway, abortSignal);
|
|
6061
7337
|
if (result === "ready") return [agentEvent.status("ready")];
|
|
6062
7338
|
if (result === "timed-out") return [agentEvent.systemMessage("Agent is taking a while, so I skipped the startup check."), agentEvent.status("ready")];
|
|
6063
7339
|
return [agentEvent.systemMessage("Agent did not say it was ready."), agentEvent.status("ready")];
|
|
6064
7340
|
}
|
|
7341
|
+
/**
|
|
7342
|
+
* Builds the initial knowledge-base status events shown by the TUI.
|
|
7343
|
+
* This wraps the raw filesystem status with the same non-clean file count
|
|
7344
|
+
* used by `/kb status`, so startup messaging reflects whether project
|
|
7345
|
+
* knowledge is ready, missing, stale, or waiting for a sync.
|
|
7346
|
+
*/
|
|
6065
7347
|
async checkKnowledgeBase() {
|
|
6066
7348
|
return getKnowledgeStatusEvents(await this.getKnowledgeStatusWithNonCleanFileCount());
|
|
6067
7349
|
}
|
|
6068
|
-
|
|
6069
|
-
|
|
7350
|
+
/**
|
|
7351
|
+
* Runs one user chat turn through the agent loop. It builds the model
|
|
7352
|
+
* prompt with relevant KB context, calls the model, executes any requested
|
|
7353
|
+
* tools, feeds tool results back into the next prompt, and repeats until
|
|
7354
|
+
* the model returns a normal assistant message or the loop hits its safety
|
|
7355
|
+
* limit.
|
|
7356
|
+
*
|
|
7357
|
+
* Events are accumulated for the caller and optionally streamed through
|
|
7358
|
+
* `onEvent` as soon as tool calls, task-plan updates, choices, or final
|
|
7359
|
+
* messages are available. The method also enforces visible task-plan
|
|
7360
|
+
* closure before a final answer when the model leaves an open plan.
|
|
7361
|
+
*/
|
|
7362
|
+
async submitMessage(conversation, message, abortSignal, onEvent) {
|
|
7363
|
+
const prompt = await this.buildPromptWithKnowledgeContext(buildConversationPrompt(conversation, message), message);
|
|
6070
7364
|
const events = [];
|
|
7365
|
+
const emit = async (...nextEvents) => {
|
|
7366
|
+
events.push(...nextEvents);
|
|
7367
|
+
if (!onEvent) return;
|
|
7368
|
+
for (const event of nextEvents) await onEvent(event);
|
|
7369
|
+
};
|
|
6071
7370
|
let nextPrompt = prompt;
|
|
6072
7371
|
let totalDurationMs = 0;
|
|
6073
7372
|
let lastModelId = "model";
|
|
6074
7373
|
let afterTool;
|
|
6075
7374
|
let toolProtocolOverride = readToolProtocolEnvOverride();
|
|
7375
|
+
let requestedPlanClosure = false;
|
|
6076
7376
|
for (let toolCalls = 0; toolCalls <= MAX_TOOL_CALLS_PER_TURN; toolCalls += 1) {
|
|
6077
7377
|
const startedAt = Date.now();
|
|
7378
|
+
const system = getChatSystemPrompt();
|
|
7379
|
+
this.context.logger.debug({
|
|
7380
|
+
event: "model_prompt",
|
|
7381
|
+
purpose: "agent.primary",
|
|
7382
|
+
afterTool,
|
|
7383
|
+
toolProtocol: toolProtocolOverride,
|
|
7384
|
+
promptLength: nextPrompt.length,
|
|
7385
|
+
systemLength: system.length,
|
|
7386
|
+
prompt: nextPrompt,
|
|
7387
|
+
system
|
|
7388
|
+
}, afterTool ? "model prompt after tool" : "model prompt");
|
|
6078
7389
|
const result = await generateAgentStep(this.context, {
|
|
6079
7390
|
purpose: "agent.primary",
|
|
6080
|
-
system
|
|
7391
|
+
system,
|
|
6081
7392
|
prompt: nextPrompt,
|
|
6082
7393
|
abortSignal,
|
|
6083
7394
|
toolProtocol: toolProtocolOverride
|
|
@@ -6093,6 +7404,11 @@ var TopchesterAgentRuntime = class {
|
|
|
6093
7404
|
durationMs,
|
|
6094
7405
|
totalDurationMs,
|
|
6095
7406
|
textLength: result.text.length,
|
|
7407
|
+
usage: result.usage,
|
|
7408
|
+
inputTokens: result.usage?.inputTokens,
|
|
7409
|
+
outputTokens: result.usage?.outputTokens,
|
|
7410
|
+
totalTokens: result.usage?.totalTokens,
|
|
7411
|
+
costUsd: result.usage?.costUsd,
|
|
6096
7412
|
hasToolCall: Boolean(toolCall),
|
|
6097
7413
|
toolProtocol: result.toolProtocol,
|
|
6098
7414
|
protocolAttempts: result.protocolAttempts,
|
|
@@ -6113,25 +7429,45 @@ var TopchesterAgentRuntime = class {
|
|
|
6113
7429
|
if (result.providerRejectedTools && result.toolProtocol === "text-json") toolProtocolOverride = "text-json";
|
|
6114
7430
|
else if (result.providerRejectedTools && result.toolProtocol === "text-xml") toolProtocolOverride = "text-xml";
|
|
6115
7431
|
if (!toolCall) {
|
|
6116
|
-
|
|
7432
|
+
if (hasOpenTaskPlan(this.taskPlan.get())) {
|
|
7433
|
+
if (!requestedPlanClosure) {
|
|
7434
|
+
requestedPlanClosure = true;
|
|
7435
|
+
nextPrompt = `${nextPrompt}\n\n${formatOpenPlanClosureInstruction(result.text, result.toolProtocol)}`;
|
|
7436
|
+
continue;
|
|
7437
|
+
}
|
|
7438
|
+
await emit(agentEvent.taskPlan(this.taskPlan.update({ items: [] })));
|
|
7439
|
+
}
|
|
7440
|
+
await emit(agentEvent.assistantMessage(result.text.trim() || "I got an empty response from the model.", formatAgentMessageMeta(result.modelId, totalDurationMs)), agentEvent.status("ready"));
|
|
6117
7441
|
return events;
|
|
6118
7442
|
}
|
|
6119
7443
|
if (toolCalls === MAX_TOOL_CALLS_PER_TURN) {
|
|
6120
|
-
|
|
7444
|
+
await emit(agentEvent.choice({
|
|
7445
|
+
tone: "warning",
|
|
7446
|
+
title: "Tool call limit reached",
|
|
7447
|
+
body: `Stopped after ${MAX_TOOL_CALLS_PER_TURN} tool calls in one turn. Continue starts another turn; abort leaves the call stopped.`,
|
|
7448
|
+
actions: [choiceAction("Continue", "Continue the previous task from where you stopped."), choiceAction("Abort", ABORT_CHOICE_VALUE)]
|
|
7449
|
+
}), agentEvent.status("ready"));
|
|
6121
7450
|
return events;
|
|
6122
7451
|
}
|
|
6123
7452
|
const executableToolCall = toolCall;
|
|
6124
|
-
const toolResult = await executeToolCall(this.context.workspaceRoot, executableToolCall, {
|
|
6125
|
-
|
|
7453
|
+
const toolResult = await executeToolCall(this.context.workspaceRoot, executableToolCall, {
|
|
7454
|
+
logger: this.context.logger,
|
|
7455
|
+
taskPlan: this.taskPlan
|
|
7456
|
+
});
|
|
7457
|
+
await emit(agentEvent.toolCall(executableToolCall, formatToolCallMessage(executableToolCall, toolResult)));
|
|
7458
|
+
if (!isToolErrorResult(toolResult) && toolResult.tool === "plan_todo") await emit(agentEvent.taskPlan(toolResult.plan));
|
|
6126
7459
|
afterTool = executableToolCall.tool;
|
|
6127
|
-
nextPrompt = `${nextPrompt}\n\n${formatToolResultForPrompt(toolResult)}\n\n${formatContinuationInstruction(result.toolProtocol)}`;
|
|
7460
|
+
nextPrompt = `${nextPrompt}\n\n${formatToolResultForPrompt(toolResult)}\n\n${formatContinuationInstruction(result.toolProtocol, toolResult)}`;
|
|
6128
7461
|
}
|
|
6129
|
-
|
|
6130
|
-
|
|
6131
|
-
agentEvent.assistantMessage("I stopped because the tool loop ended unexpectedly.", formatAgentMessageMeta(lastModelId, totalDurationMs)),
|
|
6132
|
-
agentEvent.status("ready")
|
|
6133
|
-
];
|
|
7462
|
+
await emit(agentEvent.assistantMessage("I stopped because the tool loop ended unexpectedly.", formatAgentMessageMeta(lastModelId, totalDurationMs)), agentEvent.status("ready"));
|
|
7463
|
+
return events;
|
|
6134
7464
|
}
|
|
7465
|
+
/**
|
|
7466
|
+
* Executes a slash command through the shared command dispatcher and maps
|
|
7467
|
+
* the command output into runtime events. Commands that can change KB
|
|
7468
|
+
* readiness also refresh the displayed knowledge status so the TUI footer
|
|
7469
|
+
* and chat status stay aligned with the command result.
|
|
7470
|
+
*/
|
|
6135
7471
|
async submitSlashCommand(command, onProgress) {
|
|
6136
7472
|
const result = await executeSlashCommand(command, {
|
|
6137
7473
|
workspaceRoot: this.context.workspaceRoot,
|
|
@@ -6145,6 +7481,12 @@ var TopchesterAgentRuntime = class {
|
|
|
6145
7481
|
events.push(agentEvent.status("ready"));
|
|
6146
7482
|
return events;
|
|
6147
7483
|
}
|
|
7484
|
+
/**
|
|
7485
|
+
* Reads the project KB status and augments it with a count of files that
|
|
7486
|
+
* would be touched by a dry-run compile. The dry run is only performed for
|
|
7487
|
+
* a ready KB directory, because missing or incomplete KB states already
|
|
7488
|
+
* have enough information for the startup and status messages.
|
|
7489
|
+
*/
|
|
6148
7490
|
async getKnowledgeStatusWithNonCleanFileCount() {
|
|
6149
7491
|
const status = getKnowledgeStatus(this.context.workspaceRoot);
|
|
6150
7492
|
if (!status.kbExists || !status.kbIsDirectory || status.kbContentState !== "ready") return status;
|
|
@@ -6154,7 +7496,50 @@ var TopchesterAgentRuntime = class {
|
|
|
6154
7496
|
nonCleanFileCount: result.files.length
|
|
6155
7497
|
};
|
|
6156
7498
|
}
|
|
7499
|
+
/**
|
|
7500
|
+
* Adds relevant L1 knowledge context to the conversation prompt when the
|
|
7501
|
+
* compiled KB is present and ready. Search failures are logged and then
|
|
7502
|
+
* ignored on purpose: stale or broken KB search should not prevent the
|
|
7503
|
+
* user's chat turn from reaching the model.
|
|
7504
|
+
*/
|
|
7505
|
+
async buildPromptWithKnowledgeContext(prompt, message) {
|
|
7506
|
+
const status = getKnowledgeStatus(this.context.workspaceRoot);
|
|
7507
|
+
if (!status.kbExists || !status.kbIsDirectory || status.kbContentState !== "ready") return prompt;
|
|
7508
|
+
try {
|
|
7509
|
+
const contextPack = await createL1ContextPack(this.context.workspaceRoot, message, {
|
|
7510
|
+
limit: 8,
|
|
7511
|
+
minScore: 12
|
|
7512
|
+
});
|
|
7513
|
+
this.context.logger.debug({
|
|
7514
|
+
event: "kb_context_pack",
|
|
7515
|
+
query: message,
|
|
7516
|
+
entryCount: contextPack.entryCount,
|
|
7517
|
+
relevantFileCount: contextPack.relevantFiles.length,
|
|
7518
|
+
paths: contextPack.relevantFiles.map((file) => file.path),
|
|
7519
|
+
warnings: contextPack.warnings
|
|
7520
|
+
}, "kb context pack");
|
|
7521
|
+
this.context.logger.trace({
|
|
7522
|
+
event: "kb_context_pack_payload",
|
|
7523
|
+
contextPack
|
|
7524
|
+
}, "kb context pack payload");
|
|
7525
|
+
if (contextPack.relevantFiles.length === 0) return prompt;
|
|
7526
|
+
return `${formatL1ContextPackForPrompt(contextPack)}\n\nConversation:\n${prompt}`;
|
|
7527
|
+
} catch (error) {
|
|
7528
|
+
this.context.logger.debug({
|
|
7529
|
+
event: "kb_context_pack_failed",
|
|
7530
|
+
error: error instanceof Error ? error.message : String(error)
|
|
7531
|
+
}, "kb context pack failed");
|
|
7532
|
+
return prompt;
|
|
7533
|
+
}
|
|
7534
|
+
}
|
|
6157
7535
|
};
|
|
7536
|
+
/**
|
|
7537
|
+
* Calls the configured model gateway for a single agent step and normalizes
|
|
7538
|
+
* the result into the newer `ModelAgentResult` shape. Gateways that implement
|
|
7539
|
+
* native agent stepping receive the tool registry directly; older text-only
|
|
7540
|
+
* gateways fall back to parsing a JSON or XML tool call out of the model text
|
|
7541
|
+
* so the rest of the runtime can use the same tool loop.
|
|
7542
|
+
*/
|
|
6158
7543
|
async function generateAgentStep(context, request) {
|
|
6159
7544
|
if ("generateAgentStep" in context.modelGateway && typeof context.modelGateway.generateAgentStep === "function") return context.modelGateway.generateAgentStep({
|
|
6160
7545
|
...request,
|
|
@@ -6183,15 +7568,33 @@ async function generateAgentStep(context, request) {
|
|
|
6183
7568
|
openRouterRoutingApplied: false
|
|
6184
7569
|
};
|
|
6185
7570
|
}
|
|
7571
|
+
/**
|
|
7572
|
+
* Reads the optional environment override for the tool-calling protocol.
|
|
7573
|
+
* Invalid values are ignored instead of failing startup, which keeps local
|
|
7574
|
+
* experimentation contained to supported protocol names while preserving the
|
|
7575
|
+
* normal automatic negotiation path by default.
|
|
7576
|
+
*/
|
|
6186
7577
|
function readToolProtocolEnvOverride() {
|
|
6187
7578
|
const value = process.env.TOPCHESTER_TOOL_PROTOCOL;
|
|
6188
7579
|
if (value === "auto" || value === "native" || value === "text-json" || value === "text-xml") return value;
|
|
6189
7580
|
}
|
|
7581
|
+
/**
|
|
7582
|
+
* Applies TUI styling to per-file KB sync states. The raw scanner statuses
|
|
7583
|
+
* are preserved as text, but success, warning, and error categories get
|
|
7584
|
+
* different colors so slash-command output is readable without changing the
|
|
7585
|
+
* underlying command semantics.
|
|
7586
|
+
*/
|
|
6190
7587
|
function formatTuiSyncStatus(status) {
|
|
6191
7588
|
if (status === "current") return ui.ok(status);
|
|
6192
7589
|
if (status === "invalid" || status === "missing_file") return ui.error(status);
|
|
6193
7590
|
return ui.warn(status);
|
|
6194
7591
|
}
|
|
7592
|
+
/**
|
|
7593
|
+
* Decides whether a slash command should trigger a fresh KB status event.
|
|
7594
|
+
* Only KB subcommands that can initialize, rebuild, sync, reset, or inspect
|
|
7595
|
+
* the compiled knowledge state need the refresh; other commands can return
|
|
7596
|
+
* their output without doing extra filesystem work.
|
|
7597
|
+
*/
|
|
6195
7598
|
function shouldRefreshKnowledgeStatus(command) {
|
|
6196
7599
|
const parsed = parseSlashCommand(command);
|
|
6197
7600
|
return parsed?.name === "kb" && [
|
|
@@ -6202,19 +7605,44 @@ function shouldRefreshKnowledgeStatus(command) {
|
|
|
6202
7605
|
"status"
|
|
6203
7606
|
].includes(parsed.args[0] ?? "");
|
|
6204
7607
|
}
|
|
7608
|
+
/**
|
|
7609
|
+
* Converts a computed KB status into the startup event shape consumed by the
|
|
7610
|
+
* TUI. The event carries both the structured status and a short next-step
|
|
7611
|
+
* message, letting renderers show precise state while keeping user-facing
|
|
7612
|
+
* guidance in one place.
|
|
7613
|
+
*/
|
|
6205
7614
|
function getKnowledgeStatusEvents(status) {
|
|
6206
7615
|
return [agentEvent.knowledgeStatus(status, formatStartupKnowledgeGuidance(status))];
|
|
6207
7616
|
}
|
|
7617
|
+
/**
|
|
7618
|
+
* Produces the short guidance line shown with startup KB status. The message
|
|
7619
|
+
* is deliberately action-oriented: it points to the next command that would
|
|
7620
|
+
* fix the current state and returns nothing when the KB is ready and clean.
|
|
7621
|
+
*/
|
|
6208
7622
|
function formatStartupKnowledgeGuidance(status) {
|
|
6209
7623
|
if (!status.kbExists) return "Next: run /kb init, then /kb compile to create project knowledge.";
|
|
6210
7624
|
if (!status.kbIsDirectory) return "Fix the KB path or config, then run /kb status.";
|
|
6211
7625
|
if (status.kbContentState !== "ready") return "Next: run /kb compile to build project knowledge.";
|
|
6212
7626
|
if ((status.nonCleanFileCount ?? 0) > 0) return "Next: run /kb sync to update project knowledge, or /kb status to inspect the files.";
|
|
6213
7627
|
}
|
|
7628
|
+
/**
|
|
7629
|
+
* Serializes a tool execution result into the text that is fed back to the
|
|
7630
|
+
* model after a tool call. Each tool gets the metadata the model needs for
|
|
7631
|
+
* the next step, such as file hashes, diffs, command exit status, truncation
|
|
7632
|
+
* state, or KB dirty-state signals, while errors are presented in a uniform
|
|
7633
|
+
* error block.
|
|
7634
|
+
*/
|
|
6214
7635
|
function formatToolResultForPrompt(result) {
|
|
6215
7636
|
const path = result.path ? ` ${JSON.stringify(result.path)}` : "";
|
|
6216
7637
|
const command = result.command ? ` via ${result.command}` : "";
|
|
6217
7638
|
const warning = result.warning ? `\nWarning: ${result.warning}` : "";
|
|
7639
|
+
if (isToolErrorResult(result)) return [
|
|
7640
|
+
`Tool result from ${result.tool}${path}${command}:`,
|
|
7641
|
+
`Error: ${result.error}`,
|
|
7642
|
+
"```",
|
|
7643
|
+
result.content,
|
|
7644
|
+
"```"
|
|
7645
|
+
].join("\n");
|
|
6218
7646
|
if (result.tool === "read_file") return [
|
|
6219
7647
|
`Tool result from ${result.tool}${path}${command}:${warning}`,
|
|
6220
7648
|
`hash: ${result.hash}`,
|
|
@@ -6222,6 +7650,7 @@ function formatToolResultForPrompt(result) {
|
|
|
6222
7650
|
result.content,
|
|
6223
7651
|
"```"
|
|
6224
7652
|
].join("\n");
|
|
7653
|
+
if (result.tool === "plan_todo") return [`Tool result from ${result.tool}:`, result.content].join("\n");
|
|
6225
7654
|
if (result.tool === "edit_file") return [
|
|
6226
7655
|
`Tool result from ${result.tool}${path}:`,
|
|
6227
7656
|
`before_hash: ${result.beforeHash}`,
|
|
@@ -6319,11 +7748,49 @@ function formatToolResultForPrompt(result) {
|
|
|
6319
7748
|
"```"
|
|
6320
7749
|
].join("\n");
|
|
6321
7750
|
}
|
|
6322
|
-
|
|
6323
|
-
|
|
7751
|
+
/**
|
|
7752
|
+
* Builds the follow-up instruction appended after each tool result. It keeps
|
|
7753
|
+
* the model on the active task, reminds it to maintain the visible plan, and
|
|
7754
|
+
* restates the current tool-call protocol so the next model step remains
|
|
7755
|
+
* parseable by the runtime.
|
|
7756
|
+
*/
|
|
7757
|
+
function formatContinuationInstruction(protocol, result) {
|
|
7758
|
+
const toolInstruction = protocol === "text-xml" ? "If another tool is needed, reply with only one XML tool call." : protocol === "text-json" ? "If another tool is needed, reply with only that tool JSON." : "If another tool is needed, use the available tool calling path.";
|
|
7759
|
+
return [
|
|
7760
|
+
"Continue the user's request using the tool result above and the visible plan when one is active.",
|
|
7761
|
+
result.tool === "find_file" ? "find_file results are paths only; if the user asked to read or answer from file contents, call read_file on the relevant path before answering. Do not ask the user to provide the read_file result or permission." : "",
|
|
7762
|
+
"Update plan_todo after major progress changes.",
|
|
7763
|
+
"Before a final answer, close the visible plan by calling plan_todo with all finished items marked completed, or with [] if abandoning the plan.",
|
|
7764
|
+
toolInstruction,
|
|
7765
|
+
"Otherwise answer the user. Do not guess."
|
|
7766
|
+
].filter(Boolean).join(" ");
|
|
7767
|
+
}
|
|
7768
|
+
/**
|
|
7769
|
+
* Creates the corrective prompt used when the model tries to answer while a
|
|
7770
|
+
* visible task plan is still open. The draft final answer is preserved so the
|
|
7771
|
+
* model can reuse it after closing the plan, but the immediate instruction is
|
|
7772
|
+
* to call `plan_todo` first.
|
|
7773
|
+
*/
|
|
7774
|
+
function formatOpenPlanClosureInstruction(draftAnswer, protocol) {
|
|
7775
|
+
const toolInstruction = protocol === "text-xml" ? "Reply now with only one XML plan_todo tool call." : protocol === "text-json" ? "Reply now with only the plan_todo JSON object." : "Use the available tool calling path now to call plan_todo.";
|
|
7776
|
+
const trimmedDraft = draftAnswer.trim();
|
|
7777
|
+
return [
|
|
7778
|
+
"The visible plan still has unfinished items, so do not provide the final answer yet.",
|
|
7779
|
+
"First close the plan with plan_todo: mark completed work as completed, keep one item in_progress only if work truly remains, or use [] if abandoning the plan.",
|
|
7780
|
+
toolInstruction,
|
|
7781
|
+
trimmedDraft ? `After the plan_todo result, use this draft final answer if it is still accurate:\n${trimmedDraft}` : ""
|
|
7782
|
+
].filter(Boolean).join("\n");
|
|
6324
7783
|
}
|
|
7784
|
+
/**
|
|
7785
|
+
* Formats a compact, user-visible summary for a tool call event. When a
|
|
7786
|
+
* result is available the summary includes useful completion details, such as
|
|
7787
|
+
* changed-line counts, staged paths, commit subjects, or command failures,
|
|
7788
|
+
* instead of echoing the full tool payload.
|
|
7789
|
+
*/
|
|
6325
7790
|
function formatToolCallMessage(call, result) {
|
|
7791
|
+
if (result && isToolErrorResult(result)) return `${call.tool} failed: ${result.error}`;
|
|
6326
7792
|
switch (call.tool) {
|
|
7793
|
+
case "plan_todo": return result?.tool === "plan_todo" ? `plan_todo: ${result.plan.items.length} items, ${result.inProgressCount} active` : `plan_todo: ${call.args.items.length} items`;
|
|
6327
7794
|
case "read_file": return `read_file: ${call.args.path}`;
|
|
6328
7795
|
case "list_files": return `list_files: ${call.args.path}${call.args.recursive ? " (recursive)" : ""}`;
|
|
6329
7796
|
case "grep": return `grep: ${call.args.pattern} in ${call.args.path ?? "."}`;
|
|
@@ -6338,21 +7805,48 @@ function formatToolCallMessage(call, result) {
|
|
|
6338
7805
|
case "inspect_command": return `inspect_command: ${call.args.command}`;
|
|
6339
7806
|
}
|
|
6340
7807
|
}
|
|
7808
|
+
/**
|
|
7809
|
+
* Summarizes a `git_diff` call for the TUI event list. Successful results
|
|
7810
|
+
* report the resolved scope, file count, and truncation marker; pending or
|
|
7811
|
+
* failed calls fall back to the requested scope from the tool arguments.
|
|
7812
|
+
*/
|
|
6341
7813
|
function formatGitDiffCallSummary(call, result) {
|
|
6342
|
-
if (result?.tool === "git_diff") return `${result.scope} (${result.fileCount} files${result.truncated ? ", truncated" : ""})`;
|
|
7814
|
+
if (result?.tool === "git_diff" && !isToolErrorResult(result)) return `${result.scope} (${result.fileCount} files${result.truncated ? ", truncated" : ""})`;
|
|
6343
7815
|
return call.args.scope;
|
|
6344
7816
|
}
|
|
7817
|
+
/**
|
|
7818
|
+
* Returns the parenthesized change summary for a successful `edit_file`
|
|
7819
|
+
* result. Non-edit results and failed edits intentionally return an empty
|
|
7820
|
+
* suffix so the main tool-call formatter can keep one path for success,
|
|
7821
|
+
* failure, and pre-result display.
|
|
7822
|
+
*/
|
|
6345
7823
|
function formatEditFileChangeSummary(result) {
|
|
6346
|
-
if (result?.tool !== "edit_file") return "";
|
|
7824
|
+
if (result?.tool !== "edit_file" || isToolErrorResult(result)) return "";
|
|
6347
7825
|
return ` (changed ${result.editEvent.diffSummary})`;
|
|
6348
7826
|
}
|
|
7827
|
+
/**
|
|
7828
|
+
* Returns the parenthesized write summary for a successful `write_file`
|
|
7829
|
+
* result. The helper mirrors the edit summary helper, keeping write-specific
|
|
7830
|
+
* result details out of the larger switch that formats all tool-call messages.
|
|
7831
|
+
*/
|
|
6349
7832
|
function formatWriteFileChangeSummary(result) {
|
|
6350
|
-
if (result?.tool !== "write_file") return "";
|
|
7833
|
+
if (result?.tool !== "write_file" || isToolErrorResult(result)) return "";
|
|
6351
7834
|
return ` (${result.writeEvent.writeSummary})`;
|
|
6352
7835
|
}
|
|
7836
|
+
/**
|
|
7837
|
+
* Formats the assistant-message metadata shown next to the final response.
|
|
7838
|
+
* The model identifier and cumulative turn duration are kept together here
|
|
7839
|
+
* so callers do not need to know how agent-loop timing should be presented.
|
|
7840
|
+
*/
|
|
6353
7841
|
function formatAgentMessageMeta(model, durationMs) {
|
|
6354
7842
|
return `${model} · ${formatDuration$1(durationMs)}`;
|
|
6355
7843
|
}
|
|
7844
|
+
/**
|
|
7845
|
+
* Converts elapsed milliseconds into the short human-readable duration used
|
|
7846
|
+
* in assistant metadata. Very short turns keep one decimal place, normal
|
|
7847
|
+
* sub-minute turns round to seconds, and longer turns switch to minutes plus
|
|
7848
|
+
* remaining seconds.
|
|
7849
|
+
*/
|
|
6356
7850
|
function formatDuration$1(durationMs) {
|
|
6357
7851
|
const totalSeconds = Math.max(0, durationMs / 1e3);
|
|
6358
7852
|
if (totalSeconds < 10) return `${formatNumber(totalSeconds, 1)} sec`;
|
|
@@ -6362,6 +7856,12 @@ function formatDuration$1(durationMs) {
|
|
|
6362
7856
|
if (seconds === 0) return `${minutes} min`;
|
|
6363
7857
|
return `${minutes} min ${seconds} sec`;
|
|
6364
7858
|
}
|
|
7859
|
+
/**
|
|
7860
|
+
* Formats a number with a fixed number of fraction digits using the English
|
|
7861
|
+
* locale expected by the TUI metadata strings. Keeping this tiny wrapper
|
|
7862
|
+
* avoids repeating the minimum and maximum fraction-digit options at every
|
|
7863
|
+
* call site.
|
|
7864
|
+
*/
|
|
6365
7865
|
function formatNumber(value, fractionDigits) {
|
|
6366
7866
|
return value.toLocaleString("en", {
|
|
6367
7867
|
minimumFractionDigits: fractionDigits,
|
|
@@ -6381,6 +7881,7 @@ function renderRuntimeEvent(event) {
|
|
|
6381
7881
|
body: event.body,
|
|
6382
7882
|
actions: event.actions
|
|
6383
7883
|
})];
|
|
7884
|
+
case "task_plan": return [];
|
|
6384
7885
|
case "status": return [];
|
|
6385
7886
|
}
|
|
6386
7887
|
}
|
|
@@ -6394,6 +7895,7 @@ var TopchesterTuiShell = class {
|
|
|
6394
7895
|
options;
|
|
6395
7896
|
runtime;
|
|
6396
7897
|
session;
|
|
7898
|
+
taskPlanNoticeTimer;
|
|
6397
7899
|
constructor(context, runtime, options = {}) {
|
|
6398
7900
|
this.context = context;
|
|
6399
7901
|
this.options = options;
|
|
@@ -6410,7 +7912,7 @@ var TopchesterTuiShell = class {
|
|
|
6410
7912
|
const folderName = getFolderName(this.context.workspaceRoot);
|
|
6411
7913
|
const modelLabel = getModelLabel(this.context);
|
|
6412
7914
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
6413
|
-
console.log(renderStaticLayout(messages, folderName, modelLabel));
|
|
7915
|
+
console.log(renderStaticLayout(messages, folderName, modelLabel, this.options.initialTaskPlan));
|
|
6414
7916
|
return;
|
|
6415
7917
|
}
|
|
6416
7918
|
const terminal = new ProcessTerminal();
|
|
@@ -6429,6 +7931,7 @@ var TopchesterTuiShell = class {
|
|
|
6429
7931
|
process.exit(0);
|
|
6430
7932
|
}
|
|
6431
7933
|
});
|
|
7934
|
+
app.setTaskPlan(this.options.initialTaskPlan);
|
|
6432
7935
|
app.setSubmitMessage((message) => {
|
|
6433
7936
|
this.submitChatMessage(app, tui, message);
|
|
6434
7937
|
});
|
|
@@ -6471,7 +7974,7 @@ var TopchesterTuiShell = class {
|
|
|
6471
7974
|
busy.start();
|
|
6472
7975
|
tui.requestRender();
|
|
6473
7976
|
try {
|
|
6474
|
-
await this.applyRuntimeEvents(app, await this.runtime.checkAgent(abortController.signal));
|
|
7977
|
+
await this.applyRuntimeEvents(app, await this.runtime.checkAgent(abortController.signal), tui);
|
|
6475
7978
|
} catch (error) {
|
|
6476
7979
|
if (cancelled) {
|
|
6477
7980
|
app.addMessage(systemMessage("Agent check stopped."));
|
|
@@ -6485,7 +7988,7 @@ var TopchesterTuiShell = class {
|
|
|
6485
7988
|
app.setCancelPending(void 0);
|
|
6486
7989
|
busy.stop();
|
|
6487
7990
|
}
|
|
6488
|
-
if (app.isReady()) await this.applyRuntimeEvents(app, await this.runtime.checkKnowledgeBase());
|
|
7991
|
+
if (app.isReady()) await this.applyRuntimeEvents(app, await this.runtime.checkKnowledgeBase(), tui);
|
|
6489
7992
|
tui.requestRender();
|
|
6490
7993
|
}
|
|
6491
7994
|
async submitChatMessage(app, tui, message) {
|
|
@@ -6507,12 +8010,16 @@ var TopchesterTuiShell = class {
|
|
|
6507
8010
|
busy.start();
|
|
6508
8011
|
tui.requestRender();
|
|
6509
8012
|
try {
|
|
8013
|
+
await this.clearTaskPlanForNewTurn(app);
|
|
6510
8014
|
await this.persistPayloadWithWarning(app, {
|
|
6511
8015
|
kind: "message",
|
|
6512
8016
|
role: "user",
|
|
6513
8017
|
text: message
|
|
6514
8018
|
});
|
|
6515
|
-
await this.
|
|
8019
|
+
await this.runtime.submitMessage(app.getConversationTurns(), message, abortController.signal, async (event) => {
|
|
8020
|
+
await this.applyRuntimeEvents(app, [event], tui);
|
|
8021
|
+
tui.requestRender();
|
|
8022
|
+
});
|
|
6516
8023
|
} catch (error) {
|
|
6517
8024
|
if (cancelled) {
|
|
6518
8025
|
app.addMessage(systemMessage("Response stopped."));
|
|
@@ -6542,10 +8049,11 @@ var TopchesterTuiShell = class {
|
|
|
6542
8049
|
busy.start();
|
|
6543
8050
|
tui.requestRender();
|
|
6544
8051
|
try {
|
|
8052
|
+
await this.clearTaskPlanForNewTurn(app);
|
|
6545
8053
|
await this.persistPayloadWithWarning(app, slashCommandToSessionPayload(command));
|
|
6546
8054
|
await this.applyRuntimeEvents(app, await this.runtime.submitSlashCommand(command, (event) => {
|
|
6547
8055
|
busy.setActivity(event.message);
|
|
6548
|
-
}));
|
|
8056
|
+
}), tui);
|
|
6549
8057
|
} catch (error) {
|
|
6550
8058
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
6551
8059
|
app.addMessage(systemMessage(`Command failed: ${errorMessage}`));
|
|
@@ -6559,14 +8067,41 @@ var TopchesterTuiShell = class {
|
|
|
6559
8067
|
tui.requestRender();
|
|
6560
8068
|
}
|
|
6561
8069
|
}
|
|
6562
|
-
async applyRuntimeEvents(app, events) {
|
|
8070
|
+
async applyRuntimeEvents(app, events, renderRequester) {
|
|
6563
8071
|
for (const event of events) {
|
|
6564
8072
|
if (event.type === "status") app.setStatus(event.status);
|
|
6565
8073
|
if (event.type === "knowledge_status") app.setKnowledgeStatus(event.status);
|
|
8074
|
+
if (event.type === "task_plan") {
|
|
8075
|
+
const change = app.setTaskPlan(event.plan);
|
|
8076
|
+
app.setTaskPlanNotice(formatTaskPlanNotice(change, event.plan));
|
|
8077
|
+
this.scheduleTaskPlanNoticeClear(app, renderRequester);
|
|
8078
|
+
}
|
|
6566
8079
|
for (const message of renderRuntimeEvent(event)) app.addMessage(message);
|
|
6567
8080
|
await this.persistPayloadWithWarning(app, runtimeEventToSessionPayload(event));
|
|
6568
8081
|
}
|
|
6569
8082
|
}
|
|
8083
|
+
scheduleTaskPlanNoticeClear(app, renderRequester) {
|
|
8084
|
+
if (this.taskPlanNoticeTimer) {
|
|
8085
|
+
clearTimeout(this.taskPlanNoticeTimer);
|
|
8086
|
+
this.taskPlanNoticeTimer = void 0;
|
|
8087
|
+
}
|
|
8088
|
+
if (!renderRequester) return;
|
|
8089
|
+
this.taskPlanNoticeTimer = setTimeout(() => {
|
|
8090
|
+
this.taskPlanNoticeTimer = void 0;
|
|
8091
|
+
app.setTaskPlanNotice(void 0);
|
|
8092
|
+
renderRequester.requestRender();
|
|
8093
|
+
}, 2500);
|
|
8094
|
+
this.taskPlanNoticeTimer.unref?.();
|
|
8095
|
+
}
|
|
8096
|
+
async clearTaskPlanForNewTurn(app) {
|
|
8097
|
+
const clearedPlan = app.clearTaskPlan();
|
|
8098
|
+
if (!clearedPlan) return;
|
|
8099
|
+
await this.persistPayloadWithWarning(app, {
|
|
8100
|
+
kind: "task_plan",
|
|
8101
|
+
items: clearedPlan.items,
|
|
8102
|
+
updatedAt: clearedPlan.updatedAt
|
|
8103
|
+
});
|
|
8104
|
+
}
|
|
6570
8105
|
async persistPayloadWithWarning(app, payload) {
|
|
6571
8106
|
if (!this.session || !payload) return;
|
|
6572
8107
|
try {
|
|
@@ -6626,6 +8161,11 @@ function runtimeEventToSessionPayload(event) {
|
|
|
6626
8161
|
label: event.label,
|
|
6627
8162
|
call: event.call
|
|
6628
8163
|
};
|
|
8164
|
+
case "task_plan": return {
|
|
8165
|
+
kind: "task_plan",
|
|
8166
|
+
items: event.plan.items,
|
|
8167
|
+
updatedAt: event.plan.updatedAt
|
|
8168
|
+
};
|
|
6629
8169
|
case "knowledge_status": return;
|
|
6630
8170
|
case "choice": return {
|
|
6631
8171
|
kind: "choice",
|
|
@@ -6874,7 +8414,14 @@ function printPlainEvent(event) {
|
|
|
6874
8414
|
console.log(event.label);
|
|
6875
8415
|
return;
|
|
6876
8416
|
}
|
|
6877
|
-
if (event.type === "knowledge_status" && event.guidance)
|
|
8417
|
+
if (event.type === "knowledge_status" && event.guidance) {
|
|
8418
|
+
console.log(event.guidance);
|
|
8419
|
+
return;
|
|
8420
|
+
}
|
|
8421
|
+
if (event.type === "task_plan") {
|
|
8422
|
+
const notice = formatTaskPlanNotice("updated", event.plan);
|
|
8423
|
+
if (notice) console.log(notice);
|
|
8424
|
+
}
|
|
6878
8425
|
}
|
|
6879
8426
|
function pushJson(events, runId, sessionId, type, fields) {
|
|
6880
8427
|
events.push({
|
|
@@ -6906,9 +8453,12 @@ program.action(async () => {
|
|
|
6906
8453
|
try {
|
|
6907
8454
|
if (options.resume) {
|
|
6908
8455
|
const loaded = await loadSession(context.workspaceRoot, options.resume);
|
|
8456
|
+
const session = await loadSessionForAppend(context.workspaceRoot, loaded.sessionId);
|
|
8457
|
+
const rehydrated = rehydrateSession(loaded.events);
|
|
6909
8458
|
await new TopchesterTuiShell(context, void 0, {
|
|
6910
|
-
session
|
|
6911
|
-
initialMessages:
|
|
8459
|
+
session,
|
|
8460
|
+
initialMessages: rehydrated.messages,
|
|
8461
|
+
initialTaskPlan: rehydrated.taskPlan
|
|
6912
8462
|
}).render();
|
|
6913
8463
|
return;
|
|
6914
8464
|
}
|
|
@@ -6940,6 +8490,9 @@ program.command("run").description("run one prompt or slash command without open
|
|
|
6940
8490
|
process.exitCode = 1;
|
|
6941
8491
|
}
|
|
6942
8492
|
});
|
|
8493
|
+
program.command("search").description("search compiled L1 knowledge entries").argument("<query...>", "search query").option("--limit <count>", "maximum number of matches", parsePositiveInteger).option("--json", "write full JSON search result to stdout").action(async (queryParts, options) => {
|
|
8494
|
+
await executeKbSearchCommand(queryParts, options);
|
|
8495
|
+
});
|
|
6943
8496
|
const kbCommand = program.command("kb").description("knowledge base commands");
|
|
6944
8497
|
kbCommand.command("init").description("initialize a project knowledge base").action(async () => {
|
|
6945
8498
|
const context = createContextFromOptions();
|
|
@@ -6973,6 +8526,12 @@ kbCommand.command("sync").description("sync non-clean project files into the kno
|
|
|
6973
8526
|
console.log(formatKnowledgeSyncResult(result).join("\n"));
|
|
6974
8527
|
if (isPartialKnowledgeCompileResult(result)) process.exitCode = 2;
|
|
6975
8528
|
});
|
|
8529
|
+
kbCommand.command("search").alias("query").description("search compiled L1 knowledge entries").argument("<query...>", "search query").option("--limit <count>", "maximum number of matches", parsePositiveInteger).option("--json", "write full JSON search result to stdout").action(async (queryParts, options) => {
|
|
8530
|
+
await executeKbSearchCommand(queryParts, options);
|
|
8531
|
+
});
|
|
8532
|
+
kbCommand.command("context").description("create an L1 context pack for a query").argument("<query...>", "context query").option("--limit <count>", "maximum number of relevant files", parsePositiveInteger).option("--min-score <score>", "minimum match score", parseNonNegativeNumber).option("--json", "write JSON context pack to stdout").option("--full-l1", "include full raw L1 entries in JSON output").action(async (queryParts, options) => {
|
|
8533
|
+
await executeKbContextCommand(queryParts, options);
|
|
8534
|
+
});
|
|
6976
8535
|
kbCommand.command("reset").description("delete the local project knowledge base and cache").action(async () => {
|
|
6977
8536
|
const context = createContextFromOptions();
|
|
6978
8537
|
const result = await ui.progress("Resetting project knowledge base...", (report) => resetKnowledgeBase(context.workspaceRoot, { onProgress: (event) => report(event.message) }));
|
|
@@ -7019,6 +8578,31 @@ function createContextFromOptions() {
|
|
|
7019
8578
|
devFlags: options.dev
|
|
7020
8579
|
});
|
|
7021
8580
|
}
|
|
8581
|
+
async function executeKbSearchCommand(queryParts, options) {
|
|
8582
|
+
const context = createContextFromOptions();
|
|
8583
|
+
const query = queryParts.join(" ");
|
|
8584
|
+
const result = options.json ? await searchL1Knowledge(context.workspaceRoot, query, { limit: options.limit }) : await ui.spinner("Searching L1 knowledge entries...", () => searchL1Knowledge(context.workspaceRoot, query, { limit: options.limit }));
|
|
8585
|
+
if (options.json) {
|
|
8586
|
+
console.log(JSON.stringify(stripEmptyContainers(result), null, 2));
|
|
8587
|
+
return;
|
|
8588
|
+
}
|
|
8589
|
+
console.log(formatL1KnowledgeSearchResult(result).join("\n"));
|
|
8590
|
+
}
|
|
8591
|
+
async function executeKbContextCommand(queryParts, options) {
|
|
8592
|
+
const context = createContextFromOptions();
|
|
8593
|
+
const query = queryParts.join(" ");
|
|
8594
|
+
const contextPackOptions = {
|
|
8595
|
+
limit: options.limit,
|
|
8596
|
+
minScore: options.minScore,
|
|
8597
|
+
includeFullL1: options.fullL1
|
|
8598
|
+
};
|
|
8599
|
+
const result = options.json ? await createL1ContextPack(context.workspaceRoot, query, contextPackOptions) : await ui.spinner("Creating L1 context pack...", () => createL1ContextPack(context.workspaceRoot, query, contextPackOptions));
|
|
8600
|
+
if (options.json) {
|
|
8601
|
+
console.log(JSON.stringify(stripEmptyContainers(result), null, 2));
|
|
8602
|
+
return;
|
|
8603
|
+
}
|
|
8604
|
+
console.log(formatL1ContextPackResult(result).join("\n"));
|
|
8605
|
+
}
|
|
7022
8606
|
function formatStartupError(error) {
|
|
7023
8607
|
const message = error instanceof Error ? error.message : String(error);
|
|
7024
8608
|
if (message.includes("Could not read session metadata") || message.includes("Could not read session event") || message.includes("ENOENT")) return message.includes("metadata.json") || message.includes("events.jsonl") ? message : "Session not found";
|
|
@@ -7032,6 +8616,11 @@ function parsePositiveInteger(value) {
|
|
|
7032
8616
|
if (!Number.isInteger(parsed) || parsed <= 0) throw new Error("Expected a positive integer.");
|
|
7033
8617
|
return parsed;
|
|
7034
8618
|
}
|
|
8619
|
+
function parseNonNegativeNumber(value) {
|
|
8620
|
+
const parsed = Number(value);
|
|
8621
|
+
if (!Number.isFinite(parsed) || parsed < 0) throw new Error("Expected a non-negative number.");
|
|
8622
|
+
return parsed;
|
|
8623
|
+
}
|
|
7035
8624
|
function formatDryRunSyncStatus(status) {
|
|
7036
8625
|
if (status === "current") return ui.ok(status);
|
|
7037
8626
|
if (status === "invalid" || status === "missing_file") return ui.error(status);
|