mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 15:03:31 +00:00
- List the specific paths that are checked - Make it clear that default installation path should be used
211 lines
5.4 KiB
TypeScript
211 lines
5.4 KiB
TypeScript
import type { AgentTool } from "@mariozechner/pi-ai";
|
|
import { Type } from "@sinclair/typebox";
|
|
import { spawn } from "child_process";
|
|
import { existsSync } from "fs";
|
|
|
|
/**
|
|
* Find Git Bash installation on Windows
|
|
* Searches common installation paths
|
|
*/
|
|
function findGitBash(): string | null {
|
|
if (process.platform !== "win32") {
|
|
return null;
|
|
}
|
|
|
|
const paths = ["C:\\Program Files\\Git\\bin\\bash.exe", "C:\\Program Files (x86)\\Git\\bin\\bash.exe"];
|
|
|
|
// Also check ProgramFiles environment variables
|
|
const programFiles = process.env.ProgramFiles;
|
|
if (programFiles) {
|
|
paths.push(`${programFiles}\\Git\\bin\\bash.exe`);
|
|
}
|
|
|
|
const programFilesX86 = process.env["ProgramFiles(x86)"];
|
|
if (programFilesX86) {
|
|
paths.push(`${programFilesX86}\\Git\\bin\\bash.exe`);
|
|
}
|
|
|
|
for (const path of paths) {
|
|
if (existsSync(path)) {
|
|
return path;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get shell configuration based on platform
|
|
*/
|
|
function getShellConfig(): { shell: string; args: string[] } {
|
|
if (process.platform === "win32") {
|
|
const gitBash = findGitBash();
|
|
if (!gitBash) {
|
|
const paths = ["C:\\Program Files\\Git\\bin\\bash.exe", "C:\\Program Files (x86)\\Git\\bin\\bash.exe"];
|
|
const pathList = paths.map((p) => ` - ${p}`).join("\n");
|
|
throw new Error(
|
|
`Git Bash not found in standard installation locations:\n${pathList}\n\nPlease install Git for Windows from https://git-scm.com/download/win using the default installation path.`,
|
|
);
|
|
}
|
|
return { shell: gitBash, args: ["-c"] };
|
|
}
|
|
return { shell: "sh", args: ["-c"] };
|
|
}
|
|
|
|
/**
|
|
* Kill a process and all its children
|
|
*/
|
|
function killProcessTree(pid: number): void {
|
|
if (process.platform === "win32") {
|
|
// Use taskkill on Windows to kill process tree
|
|
try {
|
|
spawn("taskkill", ["/F", "/T", "/PID", String(pid)], {
|
|
stdio: "ignore",
|
|
detached: true,
|
|
});
|
|
} catch (e) {
|
|
// Ignore errors if taskkill fails
|
|
}
|
|
} else {
|
|
// Use SIGKILL on Unix/Linux/Mac
|
|
try {
|
|
process.kill(-pid, "SIGKILL");
|
|
} catch (e) {
|
|
// Fallback to killing just the child if process group kill fails
|
|
try {
|
|
process.kill(pid, "SIGKILL");
|
|
} catch (e2) {
|
|
// Process already dead
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const bashSchema = Type.Object({
|
|
command: Type.String({ description: "Bash command to execute" }),
|
|
timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (optional, no default timeout)" })),
|
|
});
|
|
|
|
export const bashTool: AgentTool<typeof bashSchema> = {
|
|
name: "bash",
|
|
label: "bash",
|
|
description:
|
|
"Execute a bash command in the current working directory. Returns stdout and stderr. Optionally provide a timeout in seconds.",
|
|
parameters: bashSchema,
|
|
execute: async (
|
|
_toolCallId: string,
|
|
{ command, timeout }: { command: string; timeout?: number },
|
|
signal?: AbortSignal,
|
|
) => {
|
|
return new Promise((resolve, _reject) => {
|
|
const { shell, args } = getShellConfig();
|
|
const child = spawn(shell, [...args, command], {
|
|
detached: true,
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
});
|
|
|
|
let stdout = "";
|
|
let stderr = "";
|
|
let timedOut = false;
|
|
|
|
// Set timeout if provided
|
|
let timeoutHandle: NodeJS.Timeout | undefined;
|
|
if (timeout !== undefined && timeout > 0) {
|
|
timeoutHandle = setTimeout(() => {
|
|
timedOut = true;
|
|
onAbort();
|
|
}, timeout * 1000);
|
|
}
|
|
|
|
// Collect stdout
|
|
if (child.stdout) {
|
|
child.stdout.on("data", (data) => {
|
|
stdout += data.toString();
|
|
// Limit buffer size
|
|
if (stdout.length > 10 * 1024 * 1024) {
|
|
stdout = stdout.slice(0, 10 * 1024 * 1024);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Collect stderr
|
|
if (child.stderr) {
|
|
child.stderr.on("data", (data) => {
|
|
stderr += data.toString();
|
|
// Limit buffer size
|
|
if (stderr.length > 10 * 1024 * 1024) {
|
|
stderr = stderr.slice(0, 10 * 1024 * 1024);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Handle process exit
|
|
child.on("close", (code) => {
|
|
if (timeoutHandle) {
|
|
clearTimeout(timeoutHandle);
|
|
}
|
|
if (signal) {
|
|
signal.removeEventListener("abort", onAbort);
|
|
}
|
|
|
|
if (signal?.aborted) {
|
|
let output = "";
|
|
if (stdout) output += stdout;
|
|
if (stderr) {
|
|
if (output) output += "\n";
|
|
output += stderr;
|
|
}
|
|
if (output) output += "\n\n";
|
|
output += "Command aborted";
|
|
resolve({ content: [{ type: "text", text: `Command failed\n\n${output}` }], details: undefined });
|
|
return;
|
|
}
|
|
|
|
if (timedOut) {
|
|
let output = "";
|
|
if (stdout) output += stdout;
|
|
if (stderr) {
|
|
if (output) output += "\n";
|
|
output += stderr;
|
|
}
|
|
if (output) output += "\n\n";
|
|
output += `Command timed out after ${timeout} seconds`;
|
|
resolve({ content: [{ type: "text", text: `Command failed\n\n${output}` }], details: undefined });
|
|
return;
|
|
}
|
|
|
|
let output = "";
|
|
if (stdout) output += stdout;
|
|
if (stderr) {
|
|
if (output) output += "\n";
|
|
output += stderr;
|
|
}
|
|
|
|
if (code !== 0 && code !== null) {
|
|
if (output) output += "\n\n";
|
|
resolve({
|
|
content: [{ type: "text", text: `Command failed\n\n${output}Command exited with code ${code}` }],
|
|
details: undefined,
|
|
});
|
|
} else {
|
|
resolve({ content: [{ type: "text", text: output || "(no output)" }], details: undefined });
|
|
}
|
|
});
|
|
|
|
// Handle abort signal - kill entire process tree
|
|
const onAbort = () => {
|
|
if (child.pid) {
|
|
killProcessTree(child.pid);
|
|
}
|
|
};
|
|
|
|
if (signal) {
|
|
if (signal.aborted) {
|
|
onAbort();
|
|
} else {
|
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
}
|
|
}
|
|
});
|
|
},
|
|
};
|