topchester-ai 0.4.0 → 0.6.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 CHANGED
@@ -14,7 +14,7 @@ import { createHash, randomUUID } from "node:crypto";
14
14
  import { uuidv7 } from "uuidv7";
15
15
  import { Input, Markdown, ProcessTerminal, TUI, isKeyRelease, isKeyRepeat, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
16
16
  import { highlight, supportsLanguage } from "cli-highlight";
17
- import { execFile } from "node:child_process";
17
+ import { execFile, spawn } from "node:child_process";
18
18
  //#region src/model/index.ts
19
19
  var ModelGateway = class {
20
20
  #config;
@@ -131,20 +131,20 @@ const TOPCHESTER_STATE_DIR = ".agents/topchester";
131
131
  const TOPCHESTER_SESSIONS_DIR = `${TOPCHESTER_STATE_DIR}/sessions`;
132
132
  const TOPCHESTER_LOGS_DIR = `${TOPCHESTER_STATE_DIR}/logs`;
133
133
  `${TOPCHESTER_LOGS_DIR}`;
134
- function resolveWorkspacePath(workspaceRoot, path) {
134
+ function resolveWorkspacePath$1(workspaceRoot, path) {
135
135
  return isAbsolute(path) ? path : resolve(workspaceRoot, path);
136
136
  }
137
137
  function getTopchesterStatePath(workspaceRoot) {
138
- return resolveWorkspacePath(workspaceRoot, TOPCHESTER_STATE_DIR);
138
+ return resolveWorkspacePath$1(workspaceRoot, TOPCHESTER_STATE_DIR);
139
139
  }
140
140
  function getTopchesterSessionsPath(workspaceRoot) {
141
- return resolveWorkspacePath(workspaceRoot, TOPCHESTER_SESSIONS_DIR);
141
+ return resolveWorkspacePath$1(workspaceRoot, TOPCHESTER_SESSIONS_DIR);
142
142
  }
143
143
  function getTopchesterLogsPath(workspaceRoot) {
144
- return resolveWorkspacePath(workspaceRoot, TOPCHESTER_LOGS_DIR);
144
+ return resolveWorkspacePath$1(workspaceRoot, TOPCHESTER_LOGS_DIR);
145
145
  }
146
146
  function getTopchesterLogFilePath(workspaceRoot, logFile = process.env.TOPCHESTER_LOG_FILE) {
147
- return logFile ? resolveWorkspacePath(workspaceRoot, logFile) : join(getTopchesterLogsPath(workspaceRoot), "topchester.log");
147
+ return logFile ? resolveWorkspacePath$1(workspaceRoot, logFile) : join(getTopchesterLogsPath(workspaceRoot), "topchester.log");
148
148
  }
149
149
  //#endregion
150
150
  //#region src/logging/index.ts
@@ -321,8 +321,8 @@ function shouldUseColor() {
321
321
  function getKnowledgeStatus(workspaceRoot) {
322
322
  const kbPathSource = process.env.TOPCHESTER_KB_DIR ? "env" : "default";
323
323
  const cachePathSource = process.env.TOPCHESTER_KB_CACHE_DIR ? "env" : "default";
324
- const kbPath = resolveWorkspacePath(workspaceRoot, process.env.TOPCHESTER_KB_DIR ?? "topchester-kb");
325
- const cachePath = resolveWorkspacePath(workspaceRoot, process.env.TOPCHESTER_KB_CACHE_DIR ?? ".agents/topchester-kb-cache");
324
+ const kbPath = resolveWorkspacePath$1(workspaceRoot, process.env.TOPCHESTER_KB_DIR ?? "topchester-kb");
325
+ const cachePath = resolveWorkspacePath$1(workspaceRoot, process.env.TOPCHESTER_KB_CACHE_DIR ?? ".agents/topchester-kb-cache");
326
326
  const kbStat = safeStat(kbPath);
327
327
  const cacheStat = safeStat(cachePath);
328
328
  return {
@@ -1843,19 +1843,26 @@ function formatKnowledgeStatus(status) {
1843
1843
  const lines = [
1844
1844
  "KB status",
1845
1845
  `workspace: ${status.workspaceRoot}`,
1846
- `knowledge folder: ${formatPathStatus$2(status.kbPath, status.kbExists, status.kbIsDirectory)} (${status.kbPathSource})`,
1847
- `local cache folder: ${formatPathStatus$2(status.cachePath, status.cacheExists, status.cacheIsDirectory)} (${status.cachePathSource})`
1846
+ `knowledge folder: ${formatKnowledgePathStatus$2(status)} (${status.kbPathSource})`,
1847
+ `local cache folder: ${formatPathStatus$1(status.cachePath, status.cacheExists, status.cacheIsDirectory)} (${status.cachePathSource})`
1848
1848
  ];
1849
1849
  if (!status.kbExists) lines.push("state: no knowledge base found yet");
1850
1850
  else if (!status.kbIsDirectory) lines.push("state: knowledge base path is not a folder");
1851
+ else if (status.kbContentState !== "ready") lines.push("state: knowledge base folder is empty");
1851
1852
  else lines.push("state: knowledge base found");
1852
1853
  return lines;
1853
1854
  }
1854
- function formatPathStatus$2(path, exists, isDirectory) {
1855
+ function formatPathStatus$1(path, exists, isDirectory) {
1855
1856
  if (!exists) return `${path} [missing]`;
1856
1857
  if (!isDirectory) return `${path} [not a folder]`;
1857
1858
  return `${path} [ok]`;
1858
1859
  }
1860
+ function formatKnowledgePathStatus$2(status) {
1861
+ if (!status.kbExists) return `${status.kbPath} [missing]`;
1862
+ if (!status.kbIsDirectory) return `${status.kbPath} [not a folder]`;
1863
+ if (status.kbContentState !== "ready") return `${status.kbPath} [empty]`;
1864
+ return `${status.kbPath} [ok]`;
1865
+ }
1859
1866
  //#endregion
1860
1867
  //#region src/tui/markdown.ts
1861
1868
  const codeFenceSentinel = "topchester-code-fence";
@@ -1976,8 +1983,12 @@ function renderSystemMessage(lines) {
1976
1983
  return [` ${ui.ok("✦")} ${ui.label("System")}:`, ...lines.map((line) => `${bodyPrefix}${formatSystemBodyLine(line)}`)];
1977
1984
  }
1978
1985
  function formatSystemBodyLine(line) {
1986
+ if (isToolCallLine(line)) return ui.muted(line);
1979
1987
  return line.replace(/\(changed \+\d+\/-\d+\)$/u, (summary) => ui.muted(summary));
1980
1988
  }
1989
+ function isToolCallLine(line) {
1990
+ return /^(read_file|list_files|grep|find_file|edit_file|inspect_command): /u.test(line);
1991
+ }
1981
1992
  function getPrefix(kind) {
1982
1993
  switch (kind) {
1983
1994
  case "agent": return " ";
@@ -2058,6 +2069,48 @@ function escapeRegex(value) {
2058
2069
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2059
2070
  }
2060
2071
  //#endregion
2072
+ //#region src/tui/prompt-history.ts
2073
+ const DEFAULT_MAX_PROMPTS = 100;
2074
+ var PromptHistory = class {
2075
+ maxPrompts;
2076
+ prompts = [];
2077
+ historyIndex = -1;
2078
+ draft = "";
2079
+ constructor(maxPrompts = DEFAULT_MAX_PROMPTS) {
2080
+ this.maxPrompts = Math.max(1, maxPrompts);
2081
+ }
2082
+ add(value) {
2083
+ const prompt = value.trim();
2084
+ this.resetBrowsing();
2085
+ if (prompt.length === 0 || prompt === this.prompts[0]) return;
2086
+ this.prompts.unshift(prompt);
2087
+ this.prompts = this.prompts.slice(0, this.maxPrompts);
2088
+ }
2089
+ previous(currentDraft) {
2090
+ if (this.prompts.length === 0) return;
2091
+ if (this.historyIndex === -1) {
2092
+ this.draft = currentDraft;
2093
+ this.historyIndex = 0;
2094
+ return this.prompts[this.historyIndex];
2095
+ }
2096
+ this.historyIndex = Math.min(this.historyIndex + 1, this.prompts.length - 1);
2097
+ return this.prompts[this.historyIndex];
2098
+ }
2099
+ next() {
2100
+ if (this.historyIndex === -1) return;
2101
+ if (this.historyIndex === 0) {
2102
+ this.historyIndex = -1;
2103
+ return this.draft;
2104
+ }
2105
+ this.historyIndex -= 1;
2106
+ return this.prompts[this.historyIndex];
2107
+ }
2108
+ resetBrowsing() {
2109
+ this.historyIndex = -1;
2110
+ this.draft = "";
2111
+ }
2112
+ };
2113
+ //#endregion
2061
2114
  //#region src/tui/status.ts
2062
2115
  function getStartupThreadMessages(context) {
2063
2116
  const assignments = context.config.models?.assignments ?? {};
@@ -2136,10 +2189,17 @@ function formatKnowledgeFooterStatus(status) {
2136
2189
  if (status.kbContentState !== "ready") return `${ui.label("○")} kb: ${ui.label("empty")}`;
2137
2190
  return `${ui.ok("✅")} kb: ${ui.ok("ready")}`;
2138
2191
  }
2139
- function formatPathStatus$1(path, exists, isDirectory) {
2140
- if (!exists) return `${path} ${ui.warn("[missing]")}`;
2141
- if (!isDirectory) return `${path} ${ui.error("[not a folder]")}`;
2142
- return `${path} ${ui.ok("[ok]")}`;
2192
+ function formatKnowledgePathStatus$1(status) {
2193
+ const pathLabel = `${ui.label("")} ${formatWorkspaceRelativePath(status.kbPath, status.workspaceRoot)}`;
2194
+ if (!status.kbExists) return `${pathLabel} ${ui.warn("[missing]")}`;
2195
+ if (!status.kbIsDirectory) return `${pathLabel} ${ui.error("[not a folder]")}`;
2196
+ if (status.kbContentState !== "ready") return `${pathLabel} ${ui.label("[empty]")}`;
2197
+ return `${pathLabel} ${ui.ok("[ok]")}`;
2198
+ }
2199
+ function formatWorkspaceRelativePath(path, workspaceRoot) {
2200
+ const relativePath = relative(workspaceRoot, path);
2201
+ if (relativePath && !relativePath.startsWith("..") && !isAbsolute(relativePath)) return relativePath;
2202
+ return path;
2143
2203
  }
2144
2204
  function getModelLabel(context) {
2145
2205
  const purpose = context.config.models?.defaultPurpose ?? "agent.primary";
@@ -2188,6 +2248,7 @@ var ChatLayout = class {
2188
2248
  activeModalActionIndex = 0;
2189
2249
  activeSlashSuggestionIndex = 0;
2190
2250
  threadScrollOffset = 0;
2251
+ promptHistory = new PromptHistory();
2191
2252
  constructor(terminal, messages, folderName, modelLabel, exitAgent = () => {}) {
2192
2253
  this.terminal = terminal;
2193
2254
  this.messages = messages;
@@ -2264,8 +2325,11 @@ var ChatLayout = class {
2264
2325
  }
2265
2326
  if (this.handleModalInput(data)) return;
2266
2327
  if (this.handleSlashSuggestionInput(data)) return;
2328
+ if (this.handlePromptHistoryInput(data)) return;
2267
2329
  if (this.handleThreadScrollInput(data)) return;
2330
+ const previousInput = this.input.getValue();
2268
2331
  this.input.handleInput(data);
2332
+ if (this.input.getValue() !== previousInput) this.promptHistory.resetBrowsing();
2269
2333
  }
2270
2334
  invalidate() {
2271
2335
  this.input.invalidate();
@@ -2377,14 +2441,6 @@ var ChatLayout = class {
2377
2441
  handleThreadScrollInput(data) {
2378
2442
  const pageSize = Math.max(1, Math.floor(this.terminal.rows / 2));
2379
2443
  const wheel = parseMouseWheel(data);
2380
- if (isUpKey(data)) {
2381
- this.threadScrollOffset += 3;
2382
- return true;
2383
- }
2384
- if (isDownKey(data)) {
2385
- this.threadScrollOffset = Math.max(0, this.threadScrollOffset - 3);
2386
- return true;
2387
- }
2388
2444
  if (wheel === "up") {
2389
2445
  this.threadScrollOffset += 3;
2390
2446
  return true;
@@ -2411,6 +2467,20 @@ var ChatLayout = class {
2411
2467
  }
2412
2468
  return false;
2413
2469
  }
2470
+ handlePromptHistoryInput(data) {
2471
+ if (this.promptHint) return false;
2472
+ if (isUpKey(data)) {
2473
+ const prompt = this.promptHistory.previous(this.input.getValue());
2474
+ if (prompt !== void 0) this.input.setValue(prompt);
2475
+ return true;
2476
+ }
2477
+ if (isDownKey(data)) {
2478
+ const prompt = this.promptHistory.next();
2479
+ if (prompt !== void 0) this.input.setValue(prompt);
2480
+ return true;
2481
+ }
2482
+ return false;
2483
+ }
2414
2484
  handleSlashSuggestionInput(data) {
2415
2485
  const suggestions = this.getSlashSuggestions();
2416
2486
  if (suggestions.length === 0) {
@@ -2438,6 +2508,7 @@ var ChatLayout = class {
2438
2508
  completeSlashSuggestion(suggestions) {
2439
2509
  this.input.setValue(suggestions[this.activeSlashSuggestionIndex]?.value ?? this.input.getValue());
2440
2510
  this.input.handleInput("\x1B[F");
2511
+ this.promptHistory.resetBrowsing();
2441
2512
  }
2442
2513
  getSlashSuggestions() {
2443
2514
  return getSlashCommandSuggestions(this.input.getValue());
@@ -2453,6 +2524,7 @@ var ChatLayout = class {
2453
2524
  this.submitUserInput(message);
2454
2525
  }
2455
2526
  submitUserInput(message) {
2527
+ this.promptHistory.add(message);
2456
2528
  if (message.startsWith("/")) this.submitCommand?.(message);
2457
2529
  else this.submitMessage?.(message);
2458
2530
  }
@@ -2985,7 +3057,7 @@ async function collectWorkspaceFilesWithNode(workspaceRoot, startPath) {
2985
3057
  return files;
2986
3058
  }
2987
3059
  async function createRipgrepCollector(pathEnv, relativeStartPath) {
2988
- const command = await findExecutable$1("rg", pathEnv);
3060
+ const command = await findExecutable$2("rg", pathEnv);
2989
3061
  if (!command) return;
2990
3062
  return {
2991
3063
  name: "rg",
@@ -3001,8 +3073,8 @@ async function createRipgrepCollector(pathEnv, relativeStartPath) {
3001
3073
  };
3002
3074
  }
3003
3075
  async function createFdCollector(pathEnv, relativeStartPath) {
3004
- const fdCommand = await findExecutable$1("fd", pathEnv);
3005
- const fdfindCommand = fdCommand ? void 0 : await findExecutable$1("fdfind", pathEnv);
3076
+ const fdCommand = await findExecutable$2("fd", pathEnv);
3077
+ const fdfindCommand = fdCommand ? void 0 : await findExecutable$2("fdfind", pathEnv);
3006
3078
  const command = fdCommand ?? fdfindCommand;
3007
3079
  if (!command) return;
3008
3080
  return {
@@ -3022,7 +3094,7 @@ async function createFdCollector(pathEnv, relativeStartPath) {
3022
3094
  };
3023
3095
  }
3024
3096
  async function createFindCollector(pathEnv, relativeStartPath) {
3025
- const command = await findExecutable$1("find", pathEnv);
3097
+ const command = await findExecutable$2("find", pathEnv);
3026
3098
  if (!command) return;
3027
3099
  return {
3028
3100
  name: "find",
@@ -3117,7 +3189,7 @@ function resolveWorkspaceScopedPath$2(workspaceRoot, path) {
3117
3189
  relativePath: relativePath || "."
3118
3190
  };
3119
3191
  }
3120
- async function findExecutable$1(name, pathEnv) {
3192
+ async function findExecutable$2(name, pathEnv) {
3121
3193
  for (const pathEntry of pathEnv.split(delimiter).filter(Boolean)) {
3122
3194
  const executablePath = join(pathEntry, name);
3123
3195
  try {
@@ -3241,14 +3313,14 @@ function resolveWorkspaceScopedPath$1(workspaceRoot, path) {
3241
3313
  }
3242
3314
  async function findSearchExecutable(pathEnv = process.env.PATH ?? "") {
3243
3315
  for (const name of ["rg", "grep"]) {
3244
- const executablePath = await findExecutable(name, pathEnv);
3316
+ const executablePath = await findExecutable$1(name, pathEnv);
3245
3317
  if (executablePath) return {
3246
3318
  name,
3247
3319
  path: executablePath
3248
3320
  };
3249
3321
  }
3250
3322
  }
3251
- async function findExecutable(name, pathEnv) {
3323
+ async function findExecutable$1(name, pathEnv) {
3252
3324
  for (const pathEntry of pathEnv.split(delimiter).filter(Boolean)) {
3253
3325
  const executablePath = join(pathEntry, name);
3254
3326
  try {
@@ -3286,6 +3358,758 @@ function truncateToolOutput(output) {
3286
3358
  function isRecord$1(value) {
3287
3359
  return typeof value === "object" && value !== null;
3288
3360
  }
3361
+ //#endregion
3362
+ //#region src/agent/tools/inspect-command-parser.ts
3363
+ const REJECTED_SYNTAX = [
3364
+ [/\r|\n/, "multiline commands are not allowed"],
3365
+ [/[<>]/, "redirects are not allowed"],
3366
+ [/\|&/, "stderr pipelines are not allowed"],
3367
+ [/\$\(|\$\{|\$/, "shell expansion is not allowed"],
3368
+ [/`/, "command substitution is not allowed"],
3369
+ [/[()]/, "subshells are not allowed"],
3370
+ [/[{}]/, "command groups are not allowed"],
3371
+ [/\*/, "globs are not allowed"],
3372
+ [/\?/, "globs are not allowed"],
3373
+ [/\[/, "globs are not allowed"],
3374
+ [/\]/, "globs are not allowed"]
3375
+ ];
3376
+ function parseInspectCommand(command) {
3377
+ const trimmed = command.trim();
3378
+ if (!trimmed) throw new Error("inspect_command requires a command.");
3379
+ for (const [pattern, reason] of REJECTED_SYNTAX) if (pattern.test(trimmed)) throw new Error(`inspect_command rejected this command because ${reason}.`);
3380
+ const tokens = tokenize(trimmed);
3381
+ if (tokens.length === 0) throw new Error("inspect_command requires a command.");
3382
+ return {
3383
+ command: trimmed,
3384
+ entries: parseCommandList(tokens)
3385
+ };
3386
+ }
3387
+ function tokenize(command) {
3388
+ const tokens = [];
3389
+ let index = 0;
3390
+ while (index < command.length) {
3391
+ const char = command[index];
3392
+ if (/\s/.test(char)) {
3393
+ index += 1;
3394
+ continue;
3395
+ }
3396
+ if (command.startsWith("&&", index) || command.startsWith("||", index)) {
3397
+ tokens.push({
3398
+ type: "operator",
3399
+ value: command.slice(index, index + 2)
3400
+ });
3401
+ index += 2;
3402
+ continue;
3403
+ }
3404
+ if (char === "|" || char === ";") {
3405
+ tokens.push({
3406
+ type: "operator",
3407
+ value: char
3408
+ });
3409
+ index += 1;
3410
+ continue;
3411
+ }
3412
+ if (char === "&") throw new Error("inspect_command rejected this command because background jobs are not allowed.");
3413
+ const word = readWord(command, index);
3414
+ tokens.push({
3415
+ type: "word",
3416
+ value: word.value
3417
+ });
3418
+ index = word.nextIndex;
3419
+ }
3420
+ return tokens;
3421
+ }
3422
+ function readWord(command, startIndex) {
3423
+ let index = startIndex;
3424
+ let value = "";
3425
+ while (index < command.length) {
3426
+ const char = command[index];
3427
+ if (/\s/.test(char) || char === "|" || char === ";" || char === "&") break;
3428
+ if (char === "'") {
3429
+ const quoted = readQuotedWord(command, index + 1, "'");
3430
+ value += quoted.value;
3431
+ index = quoted.nextIndex;
3432
+ continue;
3433
+ }
3434
+ if (char === "\"") {
3435
+ const quoted = readQuotedWord(command, index + 1, "\"");
3436
+ value += quoted.value;
3437
+ index = quoted.nextIndex;
3438
+ continue;
3439
+ }
3440
+ value += char;
3441
+ index += 1;
3442
+ }
3443
+ if (!value) throw new Error("inspect_command rejected this command because empty words are not allowed.");
3444
+ return {
3445
+ value,
3446
+ nextIndex: index
3447
+ };
3448
+ }
3449
+ function readQuotedWord(command, startIndex, quote) {
3450
+ let index = startIndex;
3451
+ let value = "";
3452
+ while (index < command.length) {
3453
+ const char = command[index];
3454
+ if (char === quote) return {
3455
+ value,
3456
+ nextIndex: index + 1
3457
+ };
3458
+ value += char;
3459
+ index += 1;
3460
+ }
3461
+ throw new Error("inspect_command rejected this command because quoted strings must be closed.");
3462
+ }
3463
+ function parseCommandList(tokens) {
3464
+ const entries = [];
3465
+ let index = 0;
3466
+ let operator = "start";
3467
+ while (index < tokens.length) {
3468
+ if (tokens[index]?.type === "operator") throw new Error("inspect_command rejected this command because operators must appear between commands.");
3469
+ const parsed = parsePipeline(tokens, index);
3470
+ entries.push({
3471
+ operator,
3472
+ pipeline: parsed.pipeline
3473
+ });
3474
+ index = parsed.nextIndex;
3475
+ if (index >= tokens.length) break;
3476
+ const token = tokens[index];
3477
+ if (token?.type !== "operator" || token.value === "|") throw new Error("inspect_command rejected this command because pipelines must contain commands on both sides.");
3478
+ operator = token.value;
3479
+ index += 1;
3480
+ if (index >= tokens.length) throw new Error("inspect_command rejected this command because operators must appear between commands.");
3481
+ }
3482
+ return entries;
3483
+ }
3484
+ function parsePipeline(tokens, startIndex) {
3485
+ const commands = [];
3486
+ let index = startIndex;
3487
+ while (index < tokens.length) {
3488
+ const parsed = parseSimpleCommand(tokens, index);
3489
+ commands.push(parsed.command);
3490
+ index = parsed.nextIndex;
3491
+ const token = tokens[index];
3492
+ if (token?.type !== "operator" || token.value !== "|") break;
3493
+ index += 1;
3494
+ if (index >= tokens.length || tokens[index]?.type === "operator") throw new Error("inspect_command rejected this command because pipelines must contain commands on both sides.");
3495
+ }
3496
+ return {
3497
+ pipeline: { commands },
3498
+ nextIndex: index
3499
+ };
3500
+ }
3501
+ function parseSimpleCommand(tokens, startIndex) {
3502
+ const words = [];
3503
+ let index = startIndex;
3504
+ while (index < tokens.length) {
3505
+ const token = tokens[index];
3506
+ if (token?.type !== "word") break;
3507
+ words.push(token.value);
3508
+ index += 1;
3509
+ }
3510
+ if (words.length === 0) throw new Error("inspect_command rejected this command because empty commands are not allowed.");
3511
+ return {
3512
+ command: {
3513
+ executable: words[0] ?? "",
3514
+ args: words.slice(1)
3515
+ },
3516
+ nextIndex: index
3517
+ };
3518
+ }
3519
+ //#endregion
3520
+ //#region src/agent/tools/inspect-command-policy.ts
3521
+ const inspectCommandArgsSchema = z.object({
3522
+ command: z.string().min(1).max(2e3),
3523
+ workdir: z.string().optional().default("."),
3524
+ timeout_ms: z.number().int().min(100).max(1e4).optional().default(1e4)
3525
+ });
3526
+ const READ_ONLY_COMMANDS = new Set([
3527
+ "pwd",
3528
+ "ls",
3529
+ "rg",
3530
+ "grep",
3531
+ "find",
3532
+ "fd",
3533
+ "cat",
3534
+ "head",
3535
+ "tail",
3536
+ "wc",
3537
+ "stat",
3538
+ "file",
3539
+ "du",
3540
+ "git"
3541
+ ]);
3542
+ const PATHLESS_GIT_SUBCOMMANDS = new Set([
3543
+ "status",
3544
+ "log",
3545
+ "diff",
3546
+ "show",
3547
+ "branch",
3548
+ "rev-parse",
3549
+ "ls-files"
3550
+ ]);
3551
+ const GIT_OPTIONS_WITH_PATH_VALUES = new Set(["--", "--pathspec-from-file"]);
3552
+ const COMMON_OPTIONS_WITH_VALUES = new Set([
3553
+ "-A",
3554
+ "-B",
3555
+ "-C",
3556
+ "-c",
3557
+ "-e",
3558
+ "-m",
3559
+ "-n",
3560
+ "-S",
3561
+ "-s",
3562
+ "--after-context",
3563
+ "--before-context",
3564
+ "--color",
3565
+ "--context",
3566
+ "--encoding",
3567
+ "--glob",
3568
+ "--heading",
3569
+ "--ignore-file",
3570
+ "--max-count",
3571
+ "--max-depth",
3572
+ "--max-filesize",
3573
+ "--sort",
3574
+ "--sort-files",
3575
+ "--type",
3576
+ "--type-add"
3577
+ ]);
3578
+ const FIND_OPTIONS_WITH_VALUES = new Set([
3579
+ "-name",
3580
+ "-iname",
3581
+ "-path",
3582
+ "-ipath",
3583
+ "-type",
3584
+ "-maxdepth",
3585
+ "-mindepth",
3586
+ "-size",
3587
+ "-mtime",
3588
+ "-newer"
3589
+ ]);
3590
+ const FD_OPTIONS_WITH_VALUES = new Set([
3591
+ "-e",
3592
+ "-t",
3593
+ "-d",
3594
+ "--extension",
3595
+ "--type",
3596
+ "--max-depth",
3597
+ "--min-depth"
3598
+ ]);
3599
+ const SAFE_PATHLESS_FLAGS = new Set([
3600
+ "--",
3601
+ "-0",
3602
+ "-1",
3603
+ "-a",
3604
+ "-A",
3605
+ "-B",
3606
+ "-C",
3607
+ "-F",
3608
+ "-G",
3609
+ "-H",
3610
+ "-L",
3611
+ "-R",
3612
+ "-S",
3613
+ "-a",
3614
+ "-b",
3615
+ "-c",
3616
+ "-d",
3617
+ "-f",
3618
+ "-g",
3619
+ "-h",
3620
+ "-i",
3621
+ "-l",
3622
+ "-m",
3623
+ "-n",
3624
+ "-p",
3625
+ "-r",
3626
+ "-s",
3627
+ "-t",
3628
+ "-u",
3629
+ "-v",
3630
+ "-w",
3631
+ "-x",
3632
+ "--all",
3633
+ "--brief",
3634
+ "--bytes",
3635
+ "--color",
3636
+ "--count",
3637
+ "--dereference",
3638
+ "--files",
3639
+ "--follow",
3640
+ "--heading",
3641
+ "--hidden",
3642
+ "--ignore-case",
3643
+ "--line-number",
3644
+ "--long",
3645
+ "--max-depth",
3646
+ "--no-heading",
3647
+ "--no-ignore",
3648
+ "--null",
3649
+ "--oneline",
3650
+ "--print",
3651
+ "--recursive",
3652
+ "--short",
3653
+ "--show-current",
3654
+ "--show-toplevel",
3655
+ "--sort",
3656
+ "--word-regexp"
3657
+ ]);
3658
+ const DENIED_FLAGS = new Set([
3659
+ "--hostname-bin",
3660
+ "--pre",
3661
+ "--pre-glob",
3662
+ "--search-zip",
3663
+ "-z",
3664
+ "-Z",
3665
+ "-delete",
3666
+ "-exec",
3667
+ "-execdir",
3668
+ "-ok",
3669
+ "-okdir",
3670
+ "-fls",
3671
+ "-fprintf",
3672
+ "-fprint",
3673
+ "--exec",
3674
+ "-x",
3675
+ "--exec-batch",
3676
+ "-X"
3677
+ ]);
3678
+ function validateInspectCommand(args, context) {
3679
+ let plan;
3680
+ try {
3681
+ plan = parseInspectCommand(args.command);
3682
+ } catch (error) {
3683
+ return {
3684
+ allowed: false,
3685
+ reason: error instanceof Error ? error.message : "inspect_command rejected this command.",
3686
+ commands: []
3687
+ };
3688
+ }
3689
+ const commands = plan.entries.flatMap((entry) => entry.pipeline.commands.map(formatSimpleCommand));
3690
+ const workspace = resolve(context.workspaceRoot);
3691
+ const workdir = resolveWorkspacePath(workspace, context.workdir ?? args.workdir);
3692
+ if (!workdir.allowed) return {
3693
+ allowed: false,
3694
+ reason: workdir.reason,
3695
+ commands,
3696
+ plan
3697
+ };
3698
+ for (const command of plan.entries.flatMap((entry) => entry.pipeline.commands)) {
3699
+ const result = validateSimpleCommand(command, {
3700
+ workspaceRoot: workspace,
3701
+ cwd: workdir.path
3702
+ });
3703
+ if (!result.allowed) return {
3704
+ allowed: false,
3705
+ reason: result.reason,
3706
+ commands,
3707
+ plan
3708
+ };
3709
+ }
3710
+ return {
3711
+ allowed: true,
3712
+ reason: "command uses the inspect_command read-only allowlist",
3713
+ commands,
3714
+ plan
3715
+ };
3716
+ }
3717
+ function validateSimpleCommand(command, context) {
3718
+ if (command.executable.includes("/") || command.executable === "cd") return {
3719
+ allowed: false,
3720
+ reason: `inspect_command rejected '${command.executable}' because it is not allowed.`
3721
+ };
3722
+ if (!READ_ONLY_COMMANDS.has(command.executable)) return {
3723
+ allowed: false,
3724
+ reason: `inspect_command rejected '${command.executable}' because it is not allowed.`
3725
+ };
3726
+ if (command.args.some((arg) => DENIED_FLAGS.has(arg))) {
3727
+ const flag = command.args.find((arg) => DENIED_FLAGS.has(arg));
3728
+ return {
3729
+ allowed: false,
3730
+ reason: `inspect_command rejected '${command.executable}' because '${flag}' is unsafe.`
3731
+ };
3732
+ }
3733
+ switch (command.executable) {
3734
+ case "pwd": return command.args.length === 0 ? { allowed: true } : {
3735
+ allowed: false,
3736
+ reason: "inspect_command rejected 'pwd' because it does not accept path arguments."
3737
+ };
3738
+ case "git": return validateGitCommand(command, context);
3739
+ case "find": return validateFindCommand(command, context);
3740
+ case "fd": return validateGenericCommandArgs(command, context, FD_OPTIONS_WITH_VALUES);
3741
+ default: return validateGenericCommandArgs(command, context, COMMON_OPTIONS_WITH_VALUES);
3742
+ }
3743
+ }
3744
+ function validateGitCommand(command, context) {
3745
+ const subcommand = command.args.find((arg) => !arg.startsWith("-"));
3746
+ if (!subcommand || !PATHLESS_GIT_SUBCOMMANDS.has(subcommand)) return {
3747
+ allowed: false,
3748
+ reason: "inspect_command rejected 'git' because only read-only git subcommands are allowed."
3749
+ };
3750
+ if (subcommand === "branch" && command.args.some((arg) => arg !== "branch" && arg !== "--show-current")) return {
3751
+ allowed: false,
3752
+ reason: "inspect_command rejected 'git branch' because only --show-current is allowed."
3753
+ };
3754
+ if (subcommand === "rev-parse" && command.args.some((arg) => arg !== "rev-parse" && arg !== "--show-toplevel")) return {
3755
+ allowed: false,
3756
+ reason: "inspect_command rejected 'git rev-parse' because only --show-toplevel is allowed."
3757
+ };
3758
+ return validateGenericCommandArgs(command, context, GIT_OPTIONS_WITH_PATH_VALUES, new Set(["git", subcommand]));
3759
+ }
3760
+ function validateFindCommand(command, context) {
3761
+ return validateGenericCommandArgs(command, context, FIND_OPTIONS_WITH_VALUES);
3762
+ }
3763
+ function validateGenericCommandArgs(command, context, optionsWithValues, knownPathlessWords = /* @__PURE__ */ new Set()) {
3764
+ for (let index = 0; index < command.args.length; index += 1) {
3765
+ const arg = command.args[index] ?? "";
3766
+ if (knownPathlessWords.has(arg)) continue;
3767
+ if (optionsWithValues.has(arg)) {
3768
+ index += 1;
3769
+ continue;
3770
+ }
3771
+ if (arg.startsWith("--") && arg.includes("=")) {
3772
+ const [flag, value] = arg.split("=", 2);
3773
+ if (DENIED_FLAGS.has(flag ?? "")) return {
3774
+ allowed: false,
3775
+ reason: `inspect_command rejected '${command.executable}' because '${flag}' is unsafe.`
3776
+ };
3777
+ if (looksLikePath(value ?? "")) {
3778
+ const scoped = resolveWorkspacePath(context.workspaceRoot, value ?? "", context.cwd);
3779
+ if (!scoped.allowed) return {
3780
+ allowed: false,
3781
+ reason: scoped.reason
3782
+ };
3783
+ }
3784
+ continue;
3785
+ }
3786
+ if (arg.startsWith("-")) {
3787
+ if (SAFE_PATHLESS_FLAGS.has(arg) || /^-[A-Za-z0-9]+$/.test(arg)) continue;
3788
+ return {
3789
+ allowed: false,
3790
+ reason: `inspect_command rejected '${command.executable}' because '${arg}' is not allowed.`
3791
+ };
3792
+ }
3793
+ if (!looksLikePath(arg)) continue;
3794
+ const scoped = resolveWorkspacePath(context.workspaceRoot, arg, context.cwd);
3795
+ if (!scoped.allowed) return {
3796
+ allowed: false,
3797
+ reason: scoped.reason
3798
+ };
3799
+ }
3800
+ return { allowed: true };
3801
+ }
3802
+ function resolveWorkspacePath(workspaceRoot, path, cwd = workspaceRoot) {
3803
+ const resolved = isAbsolute(path) ? resolve(path) : resolve(cwd, path);
3804
+ const relativePath = relative(workspaceRoot, resolved);
3805
+ if (relativePath.startsWith("..") || isAbsolute(relativePath)) return {
3806
+ allowed: false,
3807
+ reason: `inspect_command rejected path outside the workspace: ${path}`
3808
+ };
3809
+ return {
3810
+ allowed: true,
3811
+ path: resolved
3812
+ };
3813
+ }
3814
+ function looksLikePath(arg) {
3815
+ return arg === "." || arg === ".." || arg.startsWith("./") || arg.startsWith("../") || arg.startsWith("/") || arg.includes("/");
3816
+ }
3817
+ function formatSimpleCommand(command) {
3818
+ return [command.executable, ...command.args].join(" ");
3819
+ }
3820
+ //#endregion
3821
+ //#region src/agent/tools/inspect-command.ts
3822
+ const MAX_OUTPUT_BYTES = 4e4;
3823
+ const MAX_OUTPUT_LINES = 1e3;
3824
+ const inspectCommandTool = defineTool({
3825
+ name: "inspect_command",
3826
+ description: "Run a narrowly validated read-only command for repository orientation.",
3827
+ prompt: "inspect_command: run a safe read-only discovery command inside the workspace for quick orientation; prefer read_file, list_files, grep, and find_file for exact file tasks, and do not use it for builds, tests, installs, network, shell scripts, or edits. To use it, reply with only JSON: {\"tool\":\"inspect_command\",\"args\":{\"command\":\"pwd && rg --files docs/plans | head -20\",\"workdir\":\".\",\"timeout_ms\":10000}}",
3828
+ argsSchema: inspectCommandArgsSchema,
3829
+ execute: (context, args) => inspectWorkspaceCommand(context.workspaceRoot, args, { pathEnv: context.pathEnv })
3830
+ });
3831
+ async function inspectWorkspaceCommand(workspaceRoot, args, options = {}) {
3832
+ const startedAt = Date.now();
3833
+ const decision = validateInspectCommand(args, { workspaceRoot });
3834
+ if (!decision.allowed) throw new Error(decision.reason);
3835
+ const resolvedWorkspace = await realpath(resolve(workspaceRoot));
3836
+ const cwd = await resolveWorkspaceCwd(resolvedWorkspace, args.workdir);
3837
+ const deadlineAt = startedAt + args.timeout_ms;
3838
+ const result = await executePlan(decision.plan, {
3839
+ cwd,
3840
+ pathEnv: options.pathEnv ?? process.env.PATH ?? "",
3841
+ deadlineAt
3842
+ });
3843
+ const durationMs = Date.now() - startedAt;
3844
+ return {
3845
+ tool: "inspect_command",
3846
+ command: args.command,
3847
+ cwd: relative(resolvedWorkspace, cwd) || ".",
3848
+ content: formatInspectCommandContent(result),
3849
+ exitCode: result.exitCode,
3850
+ durationMs,
3851
+ timedOut: result.timedOut,
3852
+ truncated: result.truncated,
3853
+ warning: result.missingExecutable ? `inspect_command could not run because '${result.missingExecutable}' is not available on PATH.` : result.timedOut ? "inspect_command timed out." : result.truncated ? "inspect_command output was truncated." : void 0,
3854
+ decision: {
3855
+ allowed: true,
3856
+ reason: decision.reason,
3857
+ commands: decision.commands
3858
+ },
3859
+ stdout: result.stdout,
3860
+ stderr: result.stderr
3861
+ };
3862
+ }
3863
+ async function executePlan(plan, context) {
3864
+ let lastExitCode = 0;
3865
+ let stdout = "";
3866
+ let stderr = "";
3867
+ let timedOut = false;
3868
+ let truncated = false;
3869
+ for (const entry of plan.entries) {
3870
+ if (!shouldExecuteEntry(entry, lastExitCode)) continue;
3871
+ const remainingMs = getRemainingTimeoutMs(context.deadlineAt);
3872
+ if (remainingMs <= 0) return {
3873
+ stdout,
3874
+ stderr,
3875
+ exitCode: lastExitCode,
3876
+ timedOut: true,
3877
+ truncated
3878
+ };
3879
+ const result = await executePipeline(entry.pipeline, {
3880
+ ...context,
3881
+ timeoutMs: remainingMs
3882
+ });
3883
+ stdout = appendBoundedOutput(stdout, result.stdout).output;
3884
+ const nextStderr = appendBoundedOutput(stderr, result.stderr);
3885
+ stderr = nextStderr.output;
3886
+ truncated = truncated || result.truncated || nextStderr.truncated;
3887
+ timedOut = timedOut || result.timedOut;
3888
+ lastExitCode = result.exitCode;
3889
+ if (result.missingExecutable || result.timedOut) return {
3890
+ stdout,
3891
+ stderr,
3892
+ exitCode: result.exitCode,
3893
+ timedOut,
3894
+ truncated,
3895
+ missingExecutable: result.missingExecutable
3896
+ };
3897
+ }
3898
+ return {
3899
+ stdout,
3900
+ stderr,
3901
+ exitCode: lastExitCode,
3902
+ timedOut,
3903
+ truncated
3904
+ };
3905
+ }
3906
+ async function executePipeline(pipeline, context) {
3907
+ let input = "";
3908
+ let stderr = "";
3909
+ let exitCode = 0;
3910
+ let timedOut = false;
3911
+ let truncated = false;
3912
+ for (const command of pipeline.commands) {
3913
+ const remainingMs = getRemainingTimeoutMs(context.deadlineAt);
3914
+ if (remainingMs <= 0) return {
3915
+ stdout: input,
3916
+ stderr,
3917
+ exitCode,
3918
+ timedOut: true,
3919
+ truncated
3920
+ };
3921
+ const result = await executeSimpleCommand(command, input, {
3922
+ ...context,
3923
+ timeoutMs: remainingMs
3924
+ });
3925
+ input = result.stdout;
3926
+ const nextStderr = appendBoundedOutput(stderr, result.stderr);
3927
+ stderr = nextStderr.output;
3928
+ exitCode = result.exitCode;
3929
+ timedOut = timedOut || result.timedOut;
3930
+ truncated = truncated || result.truncated || nextStderr.truncated;
3931
+ if (result.missingExecutable || result.timedOut || result.exitCode !== 0) return {
3932
+ stdout: input,
3933
+ stderr,
3934
+ exitCode,
3935
+ timedOut,
3936
+ truncated,
3937
+ missingExecutable: result.missingExecutable
3938
+ };
3939
+ }
3940
+ return {
3941
+ stdout: input,
3942
+ stderr,
3943
+ exitCode,
3944
+ timedOut,
3945
+ truncated
3946
+ };
3947
+ }
3948
+ async function executeSimpleCommand(command, input, context) {
3949
+ if (command.executable === "pwd") return {
3950
+ stdout: `${context.cwd}\n`,
3951
+ stderr: "",
3952
+ exitCode: 0,
3953
+ timedOut: false,
3954
+ truncated: false
3955
+ };
3956
+ const executablePath = await findExecutable(command.executable, context.pathEnv);
3957
+ if (!executablePath) return {
3958
+ stdout: "",
3959
+ stderr: `inspect_command could not run because '${command.executable}' is not available on PATH.\n`,
3960
+ exitCode: 127,
3961
+ timedOut: false,
3962
+ truncated: false,
3963
+ missingExecutable: command.executable
3964
+ };
3965
+ return runSpawnedCommand(executablePath, command.args, input, context);
3966
+ }
3967
+ function runSpawnedCommand(command, args, input, context) {
3968
+ return new Promise((resolveCommand) => {
3969
+ const detached = process.platform !== "win32";
3970
+ const child = spawn(command, args, {
3971
+ cwd: context.cwd,
3972
+ detached,
3973
+ env: {
3974
+ ...process.env,
3975
+ PATH: context.pathEnv,
3976
+ PAGER: "cat",
3977
+ GIT_PAGER: "cat",
3978
+ LESS: "-F -X"
3979
+ },
3980
+ stdio: [
3981
+ input.length > 0 ? "pipe" : "ignore",
3982
+ "pipe",
3983
+ "pipe"
3984
+ ]
3985
+ });
3986
+ let stdout = "";
3987
+ let stderr = "";
3988
+ let truncated = false;
3989
+ let settled = false;
3990
+ let timedOut = false;
3991
+ const timeout = setTimeout(() => {
3992
+ timedOut = true;
3993
+ killChild(child.pid, detached, "SIGTERM");
3994
+ setTimeout(() => {
3995
+ if (!settled) killChild(child.pid, detached, "SIGKILL");
3996
+ }, 250).unref();
3997
+ }, context.timeoutMs);
3998
+ child.stdout?.on("data", (chunk) => {
3999
+ const next = appendBoundedOutput(stdout, stripUnsafeControlCharacters(chunk.toString("utf8")));
4000
+ stdout = next.output;
4001
+ truncated = truncated || next.truncated;
4002
+ });
4003
+ child.stderr?.on("data", (chunk) => {
4004
+ const next = appendBoundedOutput(stderr, stripUnsafeControlCharacters(chunk.toString("utf8")));
4005
+ stderr = next.output;
4006
+ truncated = truncated || next.truncated;
4007
+ });
4008
+ child.on("error", (error) => {
4009
+ clearTimeout(timeout);
4010
+ settled = true;
4011
+ resolveCommand({
4012
+ stdout,
4013
+ stderr: stderr || `${error.message}\n`,
4014
+ exitCode: 1,
4015
+ timedOut,
4016
+ truncated
4017
+ });
4018
+ });
4019
+ child.on("close", (code, signal) => {
4020
+ clearTimeout(timeout);
4021
+ settled = true;
4022
+ resolveCommand({
4023
+ stdout,
4024
+ stderr,
4025
+ exitCode: timedOut ? 124 : code ?? (signal ? 1 : 0),
4026
+ timedOut,
4027
+ truncated
4028
+ });
4029
+ });
4030
+ if (child.stdin) {
4031
+ child.stdin.on("error", (error) => {
4032
+ if (error.code !== "EPIPE") {
4033
+ const next = appendBoundedOutput(stderr, `${error.message}\n`);
4034
+ stderr = next.output;
4035
+ truncated = truncated || next.truncated;
4036
+ }
4037
+ });
4038
+ child.stdin.end(input);
4039
+ }
4040
+ });
4041
+ }
4042
+ function killChild(pid, detached, signal) {
4043
+ if (pid === void 0) return;
4044
+ try {
4045
+ process.kill(detached ? -pid : pid, signal);
4046
+ } catch {
4047
+ try {
4048
+ process.kill(pid, signal);
4049
+ } catch {}
4050
+ }
4051
+ }
4052
+ async function resolveWorkspaceCwd(workspaceRoot, workdir) {
4053
+ const resolvedCwd = resolve(workspaceRoot, workdir);
4054
+ if (!(await stat(resolvedCwd)).isDirectory()) throw new Error(`inspect_command workdir must be a directory inside the workspace: ${workdir}`);
4055
+ const realCwd = await realpath(resolvedCwd);
4056
+ const relativePath = relative(workspaceRoot, realCwd);
4057
+ if (relativePath.startsWith("..") || isAbsolute(relativePath)) throw new Error(`inspect_command rejected path outside the workspace: ${workdir}`);
4058
+ return realCwd;
4059
+ }
4060
+ async function findExecutable(name, pathEnv) {
4061
+ for (const pathEntry of pathEnv.split(delimiter).filter(Boolean)) {
4062
+ const executablePath = join(pathEntry, name);
4063
+ try {
4064
+ await access(executablePath, constants.X_OK);
4065
+ return executablePath;
4066
+ } catch {
4067
+ continue;
4068
+ }
4069
+ }
4070
+ }
4071
+ function shouldExecuteEntry(entry, previousExitCode) {
4072
+ switch (entry.operator) {
4073
+ case "start":
4074
+ case ";": return true;
4075
+ case "&&": return previousExitCode === 0;
4076
+ case "||": return previousExitCode !== 0;
4077
+ }
4078
+ }
4079
+ function getRemainingTimeoutMs(deadlineAt) {
4080
+ return deadlineAt - Date.now();
4081
+ }
4082
+ function formatInspectCommandContent(result) {
4083
+ const sections = [];
4084
+ sections.push(result.stdout.trimEnd() || "(no output)");
4085
+ if (result.stderr.trim()) sections.push(["stderr:", result.stderr.trimEnd()].join("\n"));
4086
+ sections.push([
4087
+ "metadata:",
4088
+ `exit_code: ${result.exitCode}`,
4089
+ `timed_out: ${result.timedOut}`,
4090
+ `truncated: ${result.truncated}`
4091
+ ].join("\n"));
4092
+ return sections.join("\n\n");
4093
+ }
4094
+ function appendBoundedOutput(current, next) {
4095
+ const combined = current + next;
4096
+ const byteLimited = Buffer.byteLength(combined, "utf8") > MAX_OUTPUT_BYTES;
4097
+ const lineLimited = combined.split("\n").length > MAX_OUTPUT_LINES;
4098
+ if (!byteLimited && !lineLimited) return {
4099
+ output: combined,
4100
+ truncated: false
4101
+ };
4102
+ let output = combined;
4103
+ if (byteLimited) output = output.slice(0, MAX_OUTPUT_BYTES);
4104
+ if (lineLimited) output = output.split("\n").slice(0, MAX_OUTPUT_LINES).join("\n");
4105
+ return {
4106
+ output: `${output.trimEnd()}\n[truncated]\n`,
4107
+ truncated: true
4108
+ };
4109
+ }
4110
+ function stripUnsafeControlCharacters(output) {
4111
+ return output.replace(/[^\t\n\r -~]/g, "");
4112
+ }
3289
4113
  const listFilesTool = defineTool({
3290
4114
  name: "list_files",
3291
4115
  description: "List files and directories inside a workspace folder.",
@@ -3401,7 +4225,8 @@ const toolRegistry = {
3401
4225
  [listFilesTool.name]: listFilesTool,
3402
4226
  [grepTool.name]: grepTool,
3403
4227
  [findFileTool.name]: findFileTool,
3404
- [editFileTool.name]: editFileTool
4228
+ [editFileTool.name]: editFileTool,
4229
+ [inspectCommandTool.name]: inspectCommandTool
3405
4230
  };
3406
4231
  function isToolName(name) {
3407
4232
  return name in toolRegistry;
@@ -3468,6 +4293,15 @@ function summarizeToolArgs(call) {
3468
4293
  };
3469
4294
  }
3470
4295
  function summarizeToolResult(result) {
4296
+ if (result.tool === "inspect_command") return {
4297
+ cwd: result.cwd,
4298
+ exitCode: result.exitCode,
4299
+ timedOut: result.timedOut,
4300
+ truncated: result.truncated,
4301
+ decision: result.decision,
4302
+ stdoutLength: result.stdout.length,
4303
+ stderrLength: result.stderr.length
4304
+ };
3471
4305
  if (result.tool !== "edit_file") return {};
3472
4306
  return {
3473
4307
  beforeHash: result.beforeHash,
@@ -3554,6 +4388,9 @@ function getChatSystemPrompt() {
3554
4388
  "- 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.",
3555
4389
  "- Use read/search tools when the user asks about files, code, symbols, usages, tests, or project behavior.",
3556
4390
  "- 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.",
4391
+ "- Use list_files, grep, find_file, and read_file for exact file listing, search, lookup, and reading tasks.",
4392
+ "- Use inspect_command only for quick read-only repo orientation when a short familiar command chain is clearer than several dedicated tool calls.",
4393
+ "- inspect_command is not a shell. Unsafe commands, shell expansion, scripts, installs, builds, tests, network access, and file mutation are not available through it.",
3557
4394
  "- Use read_file before editing a file so your edit is based on current file content and hash metadata.",
3558
4395
  "- Use edit_file for targeted edits to existing files. Make multiple disjoint edits for the same file in one call when possible.",
3559
4396
  "- Keep edit_file old_text small but unique. Do not include line labels or grep prefixes in old_text; use exact file text only.",
@@ -3695,6 +4532,18 @@ function formatToolResultForPrompt(result) {
3695
4532
  result.diff,
3696
4533
  "```"
3697
4534
  ].join("\n");
4535
+ if (result.tool === "inspect_command") return [
4536
+ `Tool result from ${result.tool} via ${result.command}:`,
4537
+ `cwd: ${result.cwd}`,
4538
+ `exit_code: ${result.exitCode}`,
4539
+ `timed_out: ${result.timedOut}`,
4540
+ `truncated: ${result.truncated}`,
4541
+ `decision: ${result.decision.reason}`,
4542
+ warning ? warning.trimStart() : "",
4543
+ "```",
4544
+ result.content,
4545
+ "```"
4546
+ ].filter(Boolean).join("\n");
3698
4547
  return [
3699
4548
  `Tool result from ${result.tool}${path}${command}:${warning}`,
3700
4549
  "```",
@@ -3704,11 +4553,12 @@ function formatToolResultForPrompt(result) {
3704
4553
  }
3705
4554
  function formatToolCallMessage(call, result) {
3706
4555
  switch (call.tool) {
3707
- case "read_file": return `Tool read_file: ${call.args.path}`;
3708
- case "list_files": return `Tool list_files: ${call.args.path}${call.args.recursive ? " (recursive)" : ""}`;
3709
- case "grep": return `Tool grep: ${call.args.pattern} in ${call.args.path ?? "."}`;
3710
- case "find_file": return `Tool find_file: ${call.args.query} in ${call.args.path}`;
3711
- case "edit_file": return `Tool edit_file: ${call.args.path}${formatEditFileChangeSummary(result)}`;
4556
+ case "read_file": return `read_file: ${call.args.path}`;
4557
+ case "list_files": return `list_files: ${call.args.path}${call.args.recursive ? " (recursive)" : ""}`;
4558
+ case "grep": return `grep: ${call.args.pattern} in ${call.args.path ?? "."}`;
4559
+ case "find_file": return `find_file: ${call.args.query} in ${call.args.path}`;
4560
+ case "edit_file": return `edit_file: ${call.args.path}${formatEditFileChangeSummary(result)}`;
4561
+ case "inspect_command": return `inspect_command: ${call.args.command}`;
3712
4562
  }
3713
4563
  }
3714
4564
  function formatEditFileChangeSummary(result) {
@@ -3739,7 +4589,7 @@ function renderRuntimeEvent(event) {
3739
4589
  switch (event.type) {
3740
4590
  case "message": return [event.role === "assistant" ? agentMessage(event.text, event.meta) : systemMessage(event.text)];
3741
4591
  case "tool_call": return [systemMessage(event.label)];
3742
- case "knowledge_status": return [systemMessage(`KB status: ${formatPathStatus$1(event.status.kbPath, event.status.kbExists, event.status.kbIsDirectory)} (${event.status.kbPathSource})`)];
4592
+ case "knowledge_status": return [systemMessage(`KB status: ${formatKnowledgePathStatus$1(event.status)}${formatKbPathSource(event.status)}`)];
3743
4593
  case "choice": return [modalMessage({
3744
4594
  tone: event.tone,
3745
4595
  title: event.title,
@@ -3749,6 +4599,9 @@ function renderRuntimeEvent(event) {
3749
4599
  case "status": return [];
3750
4600
  }
3751
4601
  }
4602
+ function formatKbPathSource(status) {
4603
+ return status.kbPathSource === "env" ? " (custom)" : "";
4604
+ }
3752
4605
  //#endregion
3753
4606
  //#region src/tui/terminal.ts
3754
4607
  function enterAlternateScreen(terminal) {
@@ -4135,10 +4988,11 @@ kbCommand.command("status").description("show project knowledge base status").ac
4135
4988
  const status = await ui.spinner("Checking knowledge base...", () => getKnowledgeStatus(context.workspaceRoot));
4136
4989
  console.log(ui.heading("KB status"));
4137
4990
  console.log(`${ui.label("workspace")}: ${status.workspaceRoot}`);
4138
- console.log(`${ui.label("knowledge folder")}: ${formatPathStatus(status.kbPath, status.kbExists, status.kbIsDirectory)} ${ui.label(`(${status.kbPathSource})`)}`);
4991
+ console.log(`${ui.label("knowledge folder")}: ${formatKnowledgePathStatus(status)} ${ui.label(`(${status.kbPathSource})`)}`);
4139
4992
  console.log(`${ui.label("local cache folder")}: ${formatPathStatus(status.cachePath, status.cacheExists, status.cacheIsDirectory)} ${ui.label(`(${status.cachePathSource})`)}`);
4140
4993
  if (!status.kbExists) console.log(`${ui.label("state")}: ${ui.warn("no knowledge base found yet")}`);
4141
4994
  else if (!status.kbIsDirectory) console.log(`${ui.label("state")}: ${ui.error("knowledge base path is not a folder")}`);
4995
+ else if (status.kbContentState !== "ready") console.log(`${ui.label("state")}: ${ui.label("knowledge base folder is empty")}`);
4142
4996
  else console.log(`${ui.label("state")}: ${ui.ok("knowledge base found")}`);
4143
4997
  });
4144
4998
  await program.parseAsync();
@@ -4190,6 +5044,12 @@ function formatPathStatus(path, exists, isDirectory) {
4190
5044
  if (!isDirectory) return `${path} ${ui.error("[not a folder]")}`;
4191
5045
  return `${path} ${ui.ok("[ok]")}`;
4192
5046
  }
5047
+ function formatKnowledgePathStatus(status) {
5048
+ if (!status.kbExists) return `${status.kbPath} ${ui.warn("[missing]")}`;
5049
+ if (!status.kbIsDirectory) return `${status.kbPath} ${ui.error("[not a folder]")}`;
5050
+ if (status.kbContentState !== "ready") return `${status.kbPath} ${ui.label("[empty]")}`;
5051
+ return `${status.kbPath} ${ui.ok("[ok]")}`;
5052
+ }
4193
5053
  //#endregion
4194
5054
  export {};
4195
5055