// Copyright (C) 2026 Rob Colbert // Licensed under the Apache License, Version 2.0 import path from "node:path"; import { formatError, type IToolError } from "@gadget/ai"; import type { AiToolbox } from "../toolbox.ts"; import { getProjectRoot, toolError } from "../system/common.ts"; export interface ResolvedPlanPath { inputPath: string; absolutePath: string; displayPath: string; } export function getGadgetDir(toolbox: AiToolbox): string | undefined { const projectRoot = getProjectRoot(toolbox); if (!projectRoot) return undefined; return path.resolve(projectRoot, ".gadget"); } export function resolvePlanPath( toolbox: AiToolbox, inputPath: string, ): ResolvedPlanPath | string { const gadgetDir = getGadgetDir(toolbox); if (!gadgetDir) { return toolError({ code: "OPERATION_NOT_ALLOWED", message: "No active project workspace is configured for plan file tools.", recoveryHint: "Run this tool during an active Agent work order in Plan mode.", }); } 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 .gadget directory.", }); } const absolutePath = path.isAbsolute(trimmedPath) ? path.resolve(trimmedPath) : path.resolve(gadgetDir, trimmedPath); const relative = path.relative(gadgetDir, absolutePath); if (relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative))) { return { inputPath: trimmedPath, absolutePath, displayPath: relative || ".", }; } return toolError({ code: "SECURITY_VIOLATION", message: `Path is outside the .gadget directory: ${trimmedPath}`, parameter: "path", recoveryHint: "Use a relative path inside the project's .gadget directory.", }); }