import { resolve } from "node:path";
import stripAnsi from "strip-ansi";
import { Type } from "typebox";
import { z } from "zod";
import { formatCwd } from "../utils/format.js";
import { createToolError, createToolResult } from "../utils/messages.js";
import { bytesToTokens, formatTokenEstimate } from "../utils/token.js";
import { buildHeadTailPreviewLines } from "../utils/tool_preview.js";
import { formatBytes, TRUNCATION_MARKER, truncateForTokens, } from "../utils/truncate.js";
import { TOOL_NAME_BASH } from "./tool_names.js";
const BASH_MODEL_DEFAULT_MAX_TOKENS = 8192;
const BASH_MODEL_DEFAULT_PREVIEW_TOKENS = 2048;
const BASH_MODEL_MAX_AUTONOMOUS_TOKENS = 16384;
const BASH_MAX_OUTPUT_TOKENS = 65536;
const BASH_USER_MAX_TOKENS = BASH_MAX_OUTPUT_TOKENS;
const BASH_MODEL_DEFAULT_POLICY = {
    maxTokens: BASH_MODEL_DEFAULT_MAX_TOKENS,
    gateOnExcess: true,
    previewTokens: BASH_MODEL_DEFAULT_PREVIEW_TOKENS,
};
const BASH_USER_POLICY = {
    maxTokens: BASH_USER_MAX_TOKENS,
};
function clampOutputTokens(value) {
    if (value === undefined || !Number.isFinite(value))
        return undefined;
    return Math.min(Math.max(value, BASH_MODEL_DEFAULT_MAX_TOKENS), BASH_MAX_OUTPUT_TOKENS);
}
export function getBashOutputPolicy(args) {
    if (args.mode === "user") {
        return BASH_USER_POLICY;
    }
    if (args.hasMaxOutputTokens && args.maxOutputTokens !== undefined) {
        const maxTokens = clampOutputTokens(args.maxOutputTokens) ?? BASH_MODEL_DEFAULT_MAX_TOKENS;
        return { maxTokens };
    }
    return BASH_MODEL_DEFAULT_POLICY;
}
export const BASH_DEFAULT_TIMEOUT_MS = 60_000;
const BASH_DESCRIPTION = [
    "Execute a shell command in the current working directory and return its output.",
    "Interactive commands are not supported (no TTY/stdin); commands that prompt or open editors will hang or fail.",
    "CRITICAL: Always evaluate and provide an accurate safetyLevel assessment.",
].join(" ");
const BASH_COMMAND_DESCRIPTION = "The shell command to execute.";
const BASH_SAFETY_LEVEL_DESCRIPTION = [
    "Safety classification: 'read' (query-only, no side effects) or 'write' (modifies or has the potential to modify system state).",
    "Use 'read' for: queries (ls, rg, cat, fd, find, ps, df, etc), information gathering (curl for APIs, git log, etc), analysis (wc, sort, sha256sum, etc).",
    "Use 'write' for: filesystem changes (cp, mv, rm, mkdir, touch, echo >, etc), file modifications (sed -i, tee, chmod, chown, etc), process management (kill, pkill, etc), package management (apt, npm, etc), network changes (firewall, DNS, interfaces, etc), or any command that creates/deletes/modifies resources.",
    "When in doubt, default to 'write' to be conservative. The system will enforce appropriate access controls based on your declared safetyLevel.",
    "Always respect and strictly adhere to user-defined risk tolerance levels; never exceed the configured risk level under any circumstances.",
].join(" ");
const BASH_WORKING_DIRECTORY_DESCRIPTION = "Working directory for the command. If omitted, uses the current working directory. Prefer this over `cd` in the command.";
const BASH_TIMEOUT_DESCRIPTION = "Timeout in milliseconds. If omitted, defaults to 60 seconds. Use a longer timeout for known slow operations like builds or large clones.";
const BASH_MAX_OUTPUT_TOKENS_DESCRIPTION = [
    "Optional maximum number of output tokens to return to the model.",
    `Defaults to ${BASH_MODEL_DEFAULT_MAX_TOKENS} tokens if unset. Most commands should leave this unset. Usually it is better to run a more scoped command than to request more output. Do not set it speculatively or just in case. Only set it when you genuinely need more output, such as after a truncated result or when the user explicitly asks for more detail.`,
    `When more output is truly needed, set a value between ${BASH_MODEL_DEFAULT_MAX_TOKENS} and ${BASH_MODEL_MAX_AUTONOMOUS_TOKENS}.`,
    `Only exceed ${BASH_MODEL_MAX_AUTONOMOUS_TOKENS} when the user explicitly requests more output, up to ${BASH_MAX_OUTPUT_TOKENS}.`,
    `User requests are checked by the system, so do not exceed ${BASH_MODEL_MAX_AUTONOMOUS_TOKENS} autonomously.`,
    `If unset and output exceeds the default ${BASH_MODEL_DEFAULT_MAX_TOKENS} tokens, the tool returns a ${BASH_MODEL_DEFAULT_PREVIEW_TOKENS}-token preview with a gating notice.`,
].join(" ");
export const BASH_TOOL = {
    name: TOOL_NAME_BASH,
    description: BASH_DESCRIPTION,
    parameters: Type.Object({
        command: Type.String({
            description: BASH_COMMAND_DESCRIPTION,
        }),
        safetyLevel: Type.String({
            description: BASH_SAFETY_LEVEL_DESCRIPTION,
            enum: ["read", "write"],
        }),
        workingDirectory: Type.Optional(Type.String({
            description: BASH_WORKING_DIRECTORY_DESCRIPTION,
        })),
        timeout: Type.Optional(Type.Number({
            description: BASH_TIMEOUT_DESCRIPTION,
        })),
        maxOutputTokens: Type.Optional(Type.Integer({
            description: BASH_MAX_OUTPUT_TOKENS_DESCRIPTION,
            minimum: 1,
            maximum: BASH_MAX_OUTPUT_TOKENS,
        })),
    }, { additionalProperties: false }),
};
const BASH_TEMP_FILE_TEMPLATE = "/tmp/tau-bash-output.XXXXXX";
const BASH_TEMP_FILE_TIMEOUT_MS = 2_000;
async function createBashTempFilePath(backend) {
    try {
        const result = await backend.runBash(`mktemp ${BASH_TEMP_FILE_TEMPLATE}`, {
            timeoutMs: BASH_TEMP_FILE_TIMEOUT_MS,
        });
        if (result.exitCode !== 0)
            return undefined;
        const path = result.output.trim().split(/\r?\n/)[0]?.trim();
        return path || undefined;
    }
    catch {
        return undefined;
    }
}
async function writeBashTempFile(backend, content) {
    if (!content)
        return undefined;
    const tempPath = await createBashTempFilePath(backend);
    if (!tempPath)
        return undefined;
    try {
        const result = await backend.writeFile(tempPath, content);
        return result.path;
    }
    catch {
        return undefined;
    }
}
function formatBashOutputFileHint(args) {
    if (!args.path)
        return "";
    return ` Full output saved to ${args.path}. To see more output, either read the file or re-run with a higher maxOutputTokens. If reading the file, be mindful of its size.`;
}
export async function prepareBashOutput(output, captureTruncated, policy, backend) {
    const cleanOutput = stripAnsi(output);
    const maxTruncation = truncateForTokens(cleanOutput, {
        maxTokens: policy.maxTokens,
        strategy: "middle",
    });
    const fullOutputPath = maxTruncation.truncated
        ? await writeBashTempFile(backend, cleanOutput)
        : undefined;
    if (policy.gateOnExcess && maxTruncation.truncated) {
        const previewTokens = policy.previewTokens ?? BASH_MODEL_DEFAULT_PREVIEW_TOKENS;
        const previewTruncation = truncateForTokens(cleanOutput, {
            maxTokens: previewTokens,
            strategy: "middle",
        });
        return {
            output: previewTruncation.content,
            model: previewTruncation,
            captureTruncated,
            gated: true,
            fullOutputPath,
        };
    }
    return {
        output: maxTruncation.content,
        model: maxTruncation,
        captureTruncated,
        fullOutputPath,
    };
}
export function formatBashToolResultText(args) {
    const { truncationInfo, exitCode } = args;
    const { model, captureTruncated, gated, fullOutputPath } = truncationInfo;
    const hasNoOutput = model.outputBytes === 0;
    if (gated) {
        const preview = model.content;
        const totalTokenEstimate = bytesToTokens(model.totalBytes);
        const gateNote = `\n\n[Output gated: This command already ran and any side effects have persisted. Full output estimate: ~${totalTokenEstimate} tokens.${formatBashOutputFileHint({ path: fullOutputPath })} If you need more output from this truncated result, either read the saved file or re-run with maxOutputTokens set to ${BASH_MODEL_DEFAULT_MAX_TOKENS}-${BASH_MODEL_MAX_AUTONOMOUS_TOKENS}. Only exceed ${BASH_MODEL_MAX_AUTONOMOUS_TOKENS} when the user explicitly requests more output, up to ${BASH_MAX_OUTPUT_TOKENS}. User requests are checked by the system, so do not exceed ${BASH_MODEL_MAX_AUTONOMOUS_TOKENS} autonomously.]`;
        const exitNote = exitCode !== null && exitCode !== 0 ? `\n(exit ${exitCode})` : "";
        return `${preview}${gateNote}${exitNote}`;
    }
    if (hasNoOutput && exitCode === 0) {
        return "Command produced no output (exit 0)";
    }
    const outputForContext = model.content;
    const truncNote = model.truncated || captureTruncated
        ? `\n\n[Output truncated for context: ${model.outputLines} lines / ${formatBytes(model.outputBytes)} shown of ${model.totalLines} lines / ${formatBytes(model.totalBytes)} (full output estimate: ~${bytesToTokens(model.totalBytes)} tokens).${formatBashOutputFileHint({ path: fullOutputPath })}]`
        : "";
    const exitNote = exitCode !== null && exitCode !== 0 ? `\n(exit ${exitCode})` : "";
    return `${outputForContext}${truncNote}${exitNote}`;
}
export function formatBashUserMessageText(args) {
    const { command, truncationInfo } = args;
    const { model, captureTruncated, fullOutputPath } = truncationInfo;
    const outputForContext = model.content.trimEnd() || "(no output)";
    const truncNote = model.truncated || captureTruncated
        ? `\n\n[Output truncated for context: ${model.outputLines} lines / ${formatBytes(model.outputBytes)} shown of ${model.totalLines} lines / ${formatBytes(model.totalBytes)} (full output estimate: ~${bytesToTokens(model.totalBytes)} tokens).${formatBashOutputFileHint({ path: fullOutputPath })}]`
        : "";
    const bashContextText = `$ ${command}\n${outputForContext}${truncNote}`;
    return `Bash command output:\n${bashContextText}`;
}
const COMPACT_OUTPUT_HEAD_LINES = 3;
const COMPACT_OUTPUT_TAIL_LINES = 3;
function formatDurationMs(durationMs) {
    if (durationMs === null || durationMs === undefined || !Number.isFinite(durationMs)) {
        return "?ms";
    }
    const ms = Math.max(0, Math.round(durationMs));
    if (ms < 1000)
        return `${ms}ms`;
    if (ms < 10_000)
        return `${(ms / 1000).toFixed(1)}s`;
    return `${Math.round(ms / 1000)}s`;
}
export function buildBashUiText(args) {
    const { truncationInfo, exitCode, durationMs } = args;
    const { model, captureTruncated } = truncationInfo;
    const previewSource = truncationInfo.output;
    const outputLinesPreview = buildHeadTailPreviewLines(previewSource, {
        headLines: args.previewLines?.head ?? COMPACT_OUTPUT_HEAD_LINES,
        tailLines: args.previewLines?.tail ?? COMPACT_OUTPUT_TAIL_LINES,
    });
    const previewLines = outputLinesPreview.map((line) => ({
        text: line,
    }));
    const outputLines = model.outputLines;
    const outputBytes = model.outputBytes;
    const hasOutput = outputBytes > 0;
    const exitSummary = exitCode === null ? "exit ?" : `exit ${exitCode}`;
    const workingDirectoryLabel = args.workingDirectory
        ? formatCwd(args.workingDirectory)
        : undefined;
    const durationLabel = formatDurationMs(durationMs);
    const lineLabel = hasOutput ? `${outputLines} line${outputLines === 1 ? "" : "s"}` : "no output";
    const bytesLabel = hasOutput ? formatBytes(outputBytes) : undefined;
    const tokenLabel = hasOutput ? formatTokenEstimate(outputBytes) : undefined;
    const summaryParts = [];
    if (model.truncated || captureTruncated) {
        summaryParts.push(TRUNCATION_MARKER);
    }
    summaryParts.push(exitSummary);
    if (workingDirectoryLabel) {
        summaryParts.push(workingDirectoryLabel);
    }
    summaryParts.push(durationLabel, lineLabel);
    if (tokenLabel && bytesLabel) {
        summaryParts.push(tokenLabel, bytesLabel);
    }
    const summaryLine = summaryParts.join(" · ");
    const fullText = args.fullText?.trimEnd() ?? "";
    const fullLines = fullText ? fullText.split("\n").map((text) => ({ text })) : [];
    return {
        previewLines,
        statusLine: summaryLine,
        fullLines,
    };
}
function resolveBashWorkingDirectory(args) {
    const baseCwd = args.contextCwd ?? process.cwd();
    return args.workingDirectory ? resolve(baseCwd, args.workingDirectory) : baseCwd;
}
const bashArgsSchema = z.object({
    command: z.string().trim().min(1),
    safetyLevel: z.enum(["read", "write"]),
    workingDirectory: z.string().trim().min(1).optional(),
    timeout: z.number().positive().optional(),
    maxOutputTokens: z.number().int().positive().optional(),
});
function parseBashArgs(raw) {
    const rawRecord = typeof raw === "object" && raw !== null ? raw : undefined;
    const hasMaxOutputTokens = rawRecord ? "maxOutputTokens" in rawRecord : false;
    const commandForDisplay = rawRecord && typeof rawRecord.command === "string"
        ? rawRecord.command.trim() || "(invalid command)"
        : "(invalid command)";
    const parsed = bashArgsSchema.safeParse(raw);
    if (!parsed.success) {
        return {
            ok: false,
            error: parsed.error.issues.map((issue) => issue.message).join("; "),
            commandForDisplay,
        };
    }
    return {
        ok: true,
        data: {
            command: parsed.data.command,
            safetyLevel: parsed.data.safetyLevel,
            workingDirectory: parsed.data.workingDirectory,
            timeout: parsed.data.timeout,
            maxOutputTokens: clampOutputTokens(parsed.data.maxOutputTokens),
            hasMaxOutputTokens,
            commandForDisplay: parsed.data.command,
        },
    };
}
export function createBashToolDefinition(backend) {
    return {
        schema: BASH_TOOL,
        async dispatch(toolCall, riskLevel, signal, context) {
            const parsedArgs = parseBashArgs(toolCall.arguments);
            const commandForDisplay = parsedArgs.ok
                ? parsedArgs.data.commandForDisplay
                : parsedArgs.commandForDisplay;
            const headerTarget = commandForDisplay.split(/\r?\n/)[0] ?? commandForDisplay;
            const blocked = (reason) => {
                const toolResult = createToolError(toolCall, reason);
                const uiEvent = {
                    type: "bash_blocked",
                    toolCallId: toolCall.id,
                    command: commandForDisplay,
                    headerTarget,
                    reason,
                };
                return { kind: "single", toolResult, uiEvent };
            };
            if (!parsedArgs.ok) {
                return blocked(`Invalid arguments: ${parsedArgs.error}`);
            }
            const { command, safetyLevel, workingDirectory, timeout, maxOutputTokens, hasMaxOutputTokens, } = parsedArgs.data;
            if (riskLevel === "read-only" && safetyLevel === "write") {
                return blocked("Blocked because the risk level is set to 'read-only'. The declared safetyLevel 'write' exceeds the current risk level. Ask the user to enable it with /risk:read-write or revise to a read-only command.");
            }
            const effectiveWorkingDirectory = resolveBashWorkingDirectory({
                contextCwd: context.cwd,
                workingDirectory,
            });
            // All acceptance checks passed; return two-phase result
            return {
                kind: "phased",
                startedUiEvent: {
                    type: "bash_started",
                    toolCallId: toolCall.id,
                    command,
                    headerTarget,
                },
                run: (async () => {
                    try {
                        const startedAt = Date.now();
                        const { output, exitCode, truncated: captureTruncated, } = await backend.runBash(command, {
                            signal,
                            timeoutMs: timeout ?? BASH_DEFAULT_TIMEOUT_MS,
                            cwd: effectiveWorkingDirectory,
                        });
                        const durationMs = Math.max(0, Date.now() - startedAt);
                        const outputPolicy = getBashOutputPolicy({
                            mode: "model",
                            maxOutputTokens,
                            hasMaxOutputTokens,
                        });
                        const truncationInfo = await prepareBashOutput(output, captureTruncated, outputPolicy, backend);
                        const toolText = formatBashToolResultText({ truncationInfo, exitCode });
                        const isError = exitCode === null || exitCode !== 0;
                        const uiText = buildBashUiText({
                            truncationInfo,
                            exitCode,
                            durationMs,
                            workingDirectory: workingDirectory ? effectiveWorkingDirectory : undefined,
                            fullText: toolText,
                        });
                        const toolResult = createToolResult(toolCall, toolText, isError);
                        const uiEvent = {
                            type: "bash_execution",
                            toolCallId: toolCall.id,
                            command,
                            headerTarget,
                            exitCode,
                            truncationInfo,
                            uiText,
                            durationMs,
                        };
                        return { kind: "single", toolResult, uiEvent };
                    }
                    catch (e) {
                        const msg = `Bash tool execution failed: ${e instanceof Error ? e.message : String(e)}`;
                        const toolResult = createToolError(toolCall, msg);
                        const uiEvent = {
                            type: "bash_blocked",
                            command: commandForDisplay,
                            headerTarget,
                            reason: msg,
                            toolCallId: toolCall.id,
                        };
                        return { kind: "single", toolResult, uiEvent };
                    }
                })(),
            };
        },
    };
}
//# sourceMappingURL=bash.js.map