topchester-ai 0.5.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 +803 -21
- package/dist/cli.mjs.map +1 -1
- package/package.json +1 -1
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 {
|
|
@@ -1983,8 +1983,12 @@ function renderSystemMessage(lines) {
|
|
|
1983
1983
|
return [` ${ui.ok("✦")} ${ui.label("System")}:`, ...lines.map((line) => `${bodyPrefix}${formatSystemBodyLine(line)}`)];
|
|
1984
1984
|
}
|
|
1985
1985
|
function formatSystemBodyLine(line) {
|
|
1986
|
+
if (isToolCallLine(line)) return ui.muted(line);
|
|
1986
1987
|
return line.replace(/\(changed \+\d+\/-\d+\)$/u, (summary) => ui.muted(summary));
|
|
1987
1988
|
}
|
|
1989
|
+
function isToolCallLine(line) {
|
|
1990
|
+
return /^(read_file|list_files|grep|find_file|edit_file|inspect_command): /u.test(line);
|
|
1991
|
+
}
|
|
1988
1992
|
function getPrefix(kind) {
|
|
1989
1993
|
switch (kind) {
|
|
1990
1994
|
case "agent": return " ";
|
|
@@ -3053,7 +3057,7 @@ async function collectWorkspaceFilesWithNode(workspaceRoot, startPath) {
|
|
|
3053
3057
|
return files;
|
|
3054
3058
|
}
|
|
3055
3059
|
async function createRipgrepCollector(pathEnv, relativeStartPath) {
|
|
3056
|
-
const command = await findExecutable$
|
|
3060
|
+
const command = await findExecutable$2("rg", pathEnv);
|
|
3057
3061
|
if (!command) return;
|
|
3058
3062
|
return {
|
|
3059
3063
|
name: "rg",
|
|
@@ -3069,8 +3073,8 @@ async function createRipgrepCollector(pathEnv, relativeStartPath) {
|
|
|
3069
3073
|
};
|
|
3070
3074
|
}
|
|
3071
3075
|
async function createFdCollector(pathEnv, relativeStartPath) {
|
|
3072
|
-
const fdCommand = await findExecutable$
|
|
3073
|
-
const fdfindCommand = fdCommand ? void 0 : await findExecutable$
|
|
3076
|
+
const fdCommand = await findExecutable$2("fd", pathEnv);
|
|
3077
|
+
const fdfindCommand = fdCommand ? void 0 : await findExecutable$2("fdfind", pathEnv);
|
|
3074
3078
|
const command = fdCommand ?? fdfindCommand;
|
|
3075
3079
|
if (!command) return;
|
|
3076
3080
|
return {
|
|
@@ -3090,7 +3094,7 @@ async function createFdCollector(pathEnv, relativeStartPath) {
|
|
|
3090
3094
|
};
|
|
3091
3095
|
}
|
|
3092
3096
|
async function createFindCollector(pathEnv, relativeStartPath) {
|
|
3093
|
-
const command = await findExecutable$
|
|
3097
|
+
const command = await findExecutable$2("find", pathEnv);
|
|
3094
3098
|
if (!command) return;
|
|
3095
3099
|
return {
|
|
3096
3100
|
name: "find",
|
|
@@ -3185,7 +3189,7 @@ function resolveWorkspaceScopedPath$2(workspaceRoot, path) {
|
|
|
3185
3189
|
relativePath: relativePath || "."
|
|
3186
3190
|
};
|
|
3187
3191
|
}
|
|
3188
|
-
async function findExecutable$
|
|
3192
|
+
async function findExecutable$2(name, pathEnv) {
|
|
3189
3193
|
for (const pathEntry of pathEnv.split(delimiter).filter(Boolean)) {
|
|
3190
3194
|
const executablePath = join(pathEntry, name);
|
|
3191
3195
|
try {
|
|
@@ -3309,14 +3313,14 @@ function resolveWorkspaceScopedPath$1(workspaceRoot, path) {
|
|
|
3309
3313
|
}
|
|
3310
3314
|
async function findSearchExecutable(pathEnv = process.env.PATH ?? "") {
|
|
3311
3315
|
for (const name of ["rg", "grep"]) {
|
|
3312
|
-
const executablePath = await findExecutable(name, pathEnv);
|
|
3316
|
+
const executablePath = await findExecutable$1(name, pathEnv);
|
|
3313
3317
|
if (executablePath) return {
|
|
3314
3318
|
name,
|
|
3315
3319
|
path: executablePath
|
|
3316
3320
|
};
|
|
3317
3321
|
}
|
|
3318
3322
|
}
|
|
3319
|
-
async function findExecutable(name, pathEnv) {
|
|
3323
|
+
async function findExecutable$1(name, pathEnv) {
|
|
3320
3324
|
for (const pathEntry of pathEnv.split(delimiter).filter(Boolean)) {
|
|
3321
3325
|
const executablePath = join(pathEntry, name);
|
|
3322
3326
|
try {
|
|
@@ -3354,6 +3358,758 @@ function truncateToolOutput(output) {
|
|
|
3354
3358
|
function isRecord$1(value) {
|
|
3355
3359
|
return typeof value === "object" && value !== null;
|
|
3356
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
|
+
}
|
|
3357
4113
|
const listFilesTool = defineTool({
|
|
3358
4114
|
name: "list_files",
|
|
3359
4115
|
description: "List files and directories inside a workspace folder.",
|
|
@@ -3469,7 +4225,8 @@ const toolRegistry = {
|
|
|
3469
4225
|
[listFilesTool.name]: listFilesTool,
|
|
3470
4226
|
[grepTool.name]: grepTool,
|
|
3471
4227
|
[findFileTool.name]: findFileTool,
|
|
3472
|
-
[editFileTool.name]: editFileTool
|
|
4228
|
+
[editFileTool.name]: editFileTool,
|
|
4229
|
+
[inspectCommandTool.name]: inspectCommandTool
|
|
3473
4230
|
};
|
|
3474
4231
|
function isToolName(name) {
|
|
3475
4232
|
return name in toolRegistry;
|
|
@@ -3536,6 +4293,15 @@ function summarizeToolArgs(call) {
|
|
|
3536
4293
|
};
|
|
3537
4294
|
}
|
|
3538
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
|
+
};
|
|
3539
4305
|
if (result.tool !== "edit_file") return {};
|
|
3540
4306
|
return {
|
|
3541
4307
|
beforeHash: result.beforeHash,
|
|
@@ -3622,6 +4388,9 @@ function getChatSystemPrompt() {
|
|
|
3622
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.",
|
|
3623
4389
|
"- Use read/search tools when the user asks about files, code, symbols, usages, tests, or project behavior.",
|
|
3624
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.",
|
|
3625
4394
|
"- Use read_file before editing a file so your edit is based on current file content and hash metadata.",
|
|
3626
4395
|
"- Use edit_file for targeted edits to existing files. Make multiple disjoint edits for the same file in one call when possible.",
|
|
3627
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.",
|
|
@@ -3763,6 +4532,18 @@ function formatToolResultForPrompt(result) {
|
|
|
3763
4532
|
result.diff,
|
|
3764
4533
|
"```"
|
|
3765
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");
|
|
3766
4547
|
return [
|
|
3767
4548
|
`Tool result from ${result.tool}${path}${command}:${warning}`,
|
|
3768
4549
|
"```",
|
|
@@ -3772,11 +4553,12 @@ function formatToolResultForPrompt(result) {
|
|
|
3772
4553
|
}
|
|
3773
4554
|
function formatToolCallMessage(call, result) {
|
|
3774
4555
|
switch (call.tool) {
|
|
3775
|
-
case "read_file": return `
|
|
3776
|
-
case "list_files": return `
|
|
3777
|
-
case "grep": return `
|
|
3778
|
-
case "find_file": return `
|
|
3779
|
-
case "edit_file": return `
|
|
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}`;
|
|
3780
4562
|
}
|
|
3781
4563
|
}
|
|
3782
4564
|
function formatEditFileChangeSummary(result) {
|