fix: align the schemas, params, and descriptions (#128)

This commit is contained in:
Charles Packer
2025-11-26 19:12:31 -08:00
committed by GitHub
parent 3543276709
commit 36c571f38f
24 changed files with 1374 additions and 171 deletions

View File

@@ -1,4 +1,5 @@
import { bash } from "./Bash.js";
import { spawn } from "node:child_process";
import * as path from "node:path";
import { validateRequiredParams } from "./validation.js";
interface ShellArgs {
@@ -15,58 +16,92 @@ interface ShellResult {
stderr: string[];
}
const DEFAULT_TIMEOUT = 120000;
/**
* Codex-style shell tool.
* Runs an array of shell arguments, typically ["bash", "-lc", "..."].
* Runs an array of shell arguments using execvp-style semantics.
* Typically called with ["bash", "-lc", "..."] for shell commands.
*/
export async function shell(args: ShellArgs): Promise<ShellResult> {
validateRequiredParams(args, ["command"], "shell");
const { command, workdir, timeout_ms, justification: description } = args;
const { command, workdir, timeout_ms } = args;
if (!Array.isArray(command) || command.length === 0) {
throw new Error("command must be a non-empty array of strings");
}
const commandString = command.join(" ");
const previousUserCwd = process.env.USER_CWD;
if (workdir) {
process.env.USER_CWD = workdir;
const [executable, ...execArgs] = command;
if (!executable) {
throw new Error("command must be a non-empty array of strings");
}
const timeout = timeout_ms ?? DEFAULT_TIMEOUT;
try {
const result = await bash({
command: commandString,
timeout: timeout_ms ?? 120000,
description,
run_in_background: false,
// Determine working directory
const cwd = workdir
? path.isAbsolute(workdir)
? workdir
: path.resolve(process.env.USER_CWD || process.cwd(), workdir)
: process.env.USER_CWD || process.cwd();
return new Promise((resolve, reject) => {
const stdoutChunks: Buffer[] = [];
const stderrChunks: Buffer[] = [];
const child = spawn(executable, execArgs, {
cwd,
env: process.env,
stdio: ["ignore", "pipe", "pipe"],
});
const text = (result.content ?? [])
.map((item) =>
"text" in item && typeof item.text === "string" ? item.text : "",
)
.filter(Boolean)
.join("\n");
const timeoutId = setTimeout(() => {
child.kill("SIGKILL");
reject(new Error(`Command timed out after ${timeout}ms`));
}, timeout);
const stdout = text ? text.split("\n") : [];
const stderr =
result.status === "error"
? ["Command reported an error. See output for details."]
: [];
child.stdout.on("data", (chunk: Buffer) => {
stdoutChunks.push(chunk);
});
return {
output: text,
stdout,
stderr,
};
} finally {
if (workdir) {
if (previousUserCwd === undefined) {
delete process.env.USER_CWD;
child.stderr.on("data", (chunk: Buffer) => {
stderrChunks.push(chunk);
});
child.on("error", (err: Error) => {
clearTimeout(timeoutId);
reject(new Error(`Failed to execute command: ${err.message}`));
});
child.on("close", (code: number | null) => {
clearTimeout(timeoutId);
const stdoutText = Buffer.concat(stdoutChunks).toString("utf8");
const stderrText = Buffer.concat(stderrChunks).toString("utf8");
const stdoutLines = stdoutText
.split("\n")
.filter((line) => line.length > 0);
const stderrLines = stderrText
.split("\n")
.filter((line) => line.length > 0);
// Combine stdout and stderr for output
const output = [stdoutText, stderrText].filter(Boolean).join("\n").trim();
if (code !== 0 && code !== null) {
// Command failed but we still return the output
resolve({
output: output || `Command exited with code ${code}`,
stdout: stdoutLines,
stderr: stderrLines,
});
} else {
process.env.USER_CWD = previousUserCwd;
resolve({
output,
stdout: stdoutLines,
stderr: stderrLines,
});
}
}
}
});
});
}