137 lines
3.3 KiB
TypeScript
137 lines
3.3 KiB
TypeScript
// Copyright (C) 2026 Rob Colbert <rob.colbert@openplatform.us>
|
|
// 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<boolean> {
|
|
try {
|
|
await fs.access(filePath);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|