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; type BuiltInBashDefinition = ReturnType; type BuiltInBashExecute = BuiltInBashDefinition["execute"]; async function resolveWorkingDirectory( sessionCwd: string, requestedCwd: string | undefined, ): Promise { 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[0], params: BashCwdInput, signal: Parameters[2], onUpdate: Parameters[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()); }