// Copyright (C) 2026 Rob Colbert // Licensed under the Apache License, Version 2.0 import fs from "node:fs/promises"; import path from "node:path"; import { formatError, type IToolError } from "@gadget/ai"; import type { AiToolbox } from "../toolbox.ts"; export const BINARY_EXTENSIONS = new Set([ ".o", ".obj", ".a", ".lib", ".so", ".dll", ".exe", ".bin", ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".ico", ".webp", ".pdf", ".zip", ".tar", ".gz", ".bz2", ".7z", ".wasm", ".pyc", ".class", ]); export interface ResolvedProjectPath { inputPath: string; absolutePath: string; displayPath: string; } export function toolError(error: IToolError): string { return formatError(error); } export function getProjectRoot(toolbox: AiToolbox): string | undefined { return toolbox.env.workspace?.projectDir; } export function getCacheDir(toolbox: AiToolbox): string | undefined { return toolbox.env.workspace?.cacheDir; } export function resolveProjectPath( toolbox: AiToolbox, inputPath: string, ): ResolvedProjectPath | string { const projectRoot = getProjectRoot(toolbox); if (!projectRoot) { return toolError({ code: "OPERATION_NOT_ALLOWED", message: "No active project workspace is configured for file tools.", recoveryHint: "Run this tool during an active Agent work order.", }); } const trimmedPath = inputPath.trim(); if (!trimmedPath) { return toolError({ code: "MISSING_PARAMETER", message: "File path must not be empty.", parameter: "path", recoveryHint: "Provide a valid file path within the project directory.", }); } const root = path.resolve(projectRoot); const absolutePath = path.isAbsolute(trimmedPath) ? path.resolve(trimmedPath) : path.resolve(root, trimmedPath); const relative = path.relative(root, absolutePath); if (relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative))) { return { inputPath: trimmedPath, absolutePath, displayPath: relative || ".", }; } return toolError({ code: "SECURITY_VIOLATION", message: `Path is outside the active project directory: ${trimmedPath}`, parameter: "path", recoveryHint: "Use a relative path inside the project workspace.", }); } export function isBinaryBuffer(buffer: Buffer): boolean { const sampleSize = Math.min(buffer.length, 8192); for (let i = 0; i < sampleSize; i++) { const byte = buffer[i]; if (byte === undefined) continue; if (byte === 0) return true; if (byte < 32 && byte !== 9 && byte !== 10 && byte !== 13) return true; } return false; } export function isBinaryPath(filePath: string): boolean { return BINARY_EXTENSIONS.has(path.extname(filePath).toLowerCase()); } export function asPositiveInteger(value: unknown): number | undefined { if (typeof value !== "number" || !Number.isFinite(value)) { return undefined; } return Math.floor(value); } export function formatNumberedLines( lines: string[], startIndex: number, ): string { return lines.map((line, index) => `${startIndex + index + 1}: ${line}`).join("\n"); } export async function pathExists(filePath: string): Promise { try { await fs.access(filePath); return true; } catch { return false; } }