import type { ToolResultMessage } from "@earendil-works/pi-ai"; import type { ExtensionContext } from "@earendil-works/pi-coding-agent"; import type { Executor } from "@open-cursor/client"; import type { ShellArgs, ShellResult, } from "@open-cursor/protocol/__generated__/agent/v1/shell_exec_pb.js"; import { ShellFailure, ShellRejected, ShellResult as ShellResultClass, ShellSuccess, } from "@open-cursor/protocol/__generated__/agent/v1/shell_exec_pb.js"; import { toolResultToText } from "../../shared/tool-result.js"; import { requestToolExecution } from "../relay.js"; import { decodeToolCallId, type PiToolContext } from "../types.js"; export function buildShellResultFromToolResult( args: { command: string; workingDirectory: string }, result: ToolResultMessage, ): ShellResult { const output = toolResultToText(result); if (result.isError) { return new ShellResultClass({ result: { case: "failure", value: new ShellFailure({ command: args.command, workingDirectory: args.workingDirectory, exitCode: 1, signal: "", stdout: "", stderr: output || "Shell failed", executionTime: 0, aborted: false, }), }, }); } return new ShellResultClass({ result: { case: "success", value: new ShellSuccess({ command: args.command, workingDirectory: args.workingDirectory, exitCode: 0, signal: "", stdout: output, stderr: "", executionTime: 0, }), }, }); } export function buildShellRejectedResult( command: string, workingDirectory: string, reason: string, ): ShellResult { return new ShellResultClass({ result: { case: "rejected", value: new ShellRejected({ command, workingDirectory, reason, isReadonly: false, }), }, }); } function isDangerousShellCommand(command: string): boolean { const c = command.toLowerCase(); if (/(^|\s)sudo\b/.test(c)) return true; if (/\brm\b.*\s-rf\b/.test(c)) return true; if (/\bmkfs\b|\bdd\b|\bshutdown\b|\breboot\b/.test(c)) return true; if (/\bcurl\b.*\|\s*(sh|bash)\b/.test(c)) return true; if (/\bwget\b.*\|\s*(sh|bash)\b/.test(c)) return true; return false; } export async function confirmIfDangerous( getCtx: () => ExtensionContext | null, command: string, ): Promise { if (!isDangerousShellCommand(command)) return true; const ctx = getCtx(); if (!ctx?.hasUI) return false; return ctx.ui.confirm("Cursor command approval", command); } export class LocalShellExecutor implements Executor { private readonly ctx: PiToolContext; constructor(ctx: PiToolContext) { this.ctx = ctx; } async execute(_ctx: unknown, args: ShellArgs): Promise { const toolCallId = decodeToolCallId(args.toolCallId); const workingDirectory = args.workingDirectory || this.ctx.cwd; if (!this.ctx.getActiveTools().has("bash")) { return buildShellRejectedResult(args.command, workingDirectory, "Tool not available"); } const approved = await confirmIfDangerous(() => this.ctx.getCtx(), args.command); if (!approved) { return buildShellRejectedResult(args.command, workingDirectory, "Command rejected"); } const timeoutSeconds = args.timeout && args.timeout > 0 ? args.timeout : undefined; const piResult = await requestToolExecution(this.ctx.getChannel?.() ?? null, { toolCallId, cursorExecType: "shell", piToolName: "bash", piToolArgs: { command: args.command, ...(timeoutSeconds !== undefined ? { timeout: timeoutSeconds } : {}), }, }); return buildShellResultFromToolResult({ command: args.command, workingDirectory }, piResult); } }