Files
harness-tools/extensions/bash-cwd/index.ts
T

106 lines
2.9 KiB
TypeScript

import { stat } from "node:fs/promises";
import { resolve } from "node:path";
import {
createBashToolDefinition,
getAgentDir,
SettingsManager,
type BashToolOptions,
type ExtensionAPI,
type ExtensionContext,
} from "@earendil-works/pi-coding-agent";
import { type Static, Type } from "typebox";
const bashCwdParameters = Type.Object({
command: Type.String({ description: "Shell command to execute" }),
cwd: Type.Optional(
Type.String({
description:
"Working directory. Relative paths resolve from the session working directory.",
}),
),
timeout: Type.Optional(
Type.Number({ description: "Timeout in seconds (optional, no default timeout)" }),
),
});
type BashCwdInput = Static<typeof bashCwdParameters>;
type BuiltInBashDefinition = ReturnType<typeof createBashToolDefinition>;
type BuiltInBashExecute = BuiltInBashDefinition["execute"];
async function resolveWorkingDirectory(
sessionCwd: string,
requestedCwd: string | undefined,
): Promise<string> {
const cwd = resolve(sessionCwd, requestedCwd ?? ".");
let metadata;
try {
metadata = await stat(cwd);
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
throw new Error(
`Working directory does not exist: ${cwd}\nCannot execute bash commands.`,
);
}
throw error;
}
if (!metadata.isDirectory()) {
throw new Error(
`Working directory is not a directory: ${cwd}\nCannot execute bash commands.`,
);
}
return cwd;
}
function loadBashOptions(ctx: ExtensionContext): BashToolOptions {
const settings = SettingsManager.create(ctx.cwd, getAgentDir(), {
projectTrusted: ctx.isProjectTrusted(),
});
return {
commandPrefix: settings.getShellCommandPrefix(),
shellPath: settings.getShellPath(),
};
}
export function createContextualBashTool(options?: BashToolOptions) {
const baseTool = createBashToolDefinition(process.cwd(), options);
return {
...baseTool,
description:
`${baseTool.description} The optional cwd may be absolute or relative to the session working directory.`,
parameters: bashCwdParameters,
async execute(
toolCallId: Parameters<BuiltInBashExecute>[0],
params: BashCwdInput,
signal: Parameters<BuiltInBashExecute>[2],
onUpdate: Parameters<BuiltInBashExecute>[3],
ctx: ExtensionContext,
) {
if (!ctx) {
throw new Error("Bash execution context is unavailable");
}
const cwd = await resolveWorkingDirectory(ctx.cwd, params.cwd);
const contextualTool = createBashToolDefinition(
cwd,
options ?? loadBashOptions(ctx),
);
return contextualTool.execute(
toolCallId,
{ command: params.command, timeout: params.timeout },
signal,
onUpdate,
ctx,
);
},
};
}
export default function bashCwd(pi: ExtensionAPI): void {
pi.registerTool(createContextualBashTool());
}