gadget/gadget-drone/src/tools/system/common.ts
Rob Colbert a4cff5be69 agent toolbox refactor and updates
- reorganized tools into better named directories
- added the file & shell tools
2026-05-10 07:35:09 -04:00

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;
}
}