Files
AgentCompany/src/main/skills/discovery.ts
T

162 lines
5.2 KiB
TypeScript

import { readdirSync, realpathSync, statSync, type Dirent } from "node:fs";
import { join, resolve, relative } from "node:path";
import type { FileInventoryEntry } from "../skill-types.js";
import { classifyInventoryKind } from "../skill-format.js";
export const DEFAULT_SKILL_WALK_MAX_DEPTH = 8;
export const DEFAULT_SKILL_WALK_MAX_FILES = 256;
const BLOCKED_PATH_PREFIXES_UNIX = ["/etc", "/var", "/proc", "/sys", "/dev", "/boot", "/sbin", "/usr/sbin"];
const BLOCKED_PATH_PREFIXES_WIN = ["C:\\Windows", "C:\\Program Files", "C:\\ProgramData"];
export interface DiscoveryLogger {
warn: (...args: unknown[]) => void;
}
export function assertNotSystemPath(realPath: string): void {
const normalized = realPath.replace(/\\/g, "/");
const blocked = process.platform === "win32" ? BLOCKED_PATH_PREFIXES_WIN : BLOCKED_PATH_PREFIXES_UNIX;
for (const prefix of blocked) {
const normalizedPrefix = prefix.replace(/\\/g, "/");
if (normalized === normalizedPrefix || normalized.startsWith(normalizedPrefix + "/")) {
throw new Error(`Importing skills from system directory is not allowed: ${realPath}`);
}
}
}
export function findSkillMdFiles(
rootDir: string,
logger: DiscoveryLogger,
opts?: { maxDepth?: number; maxFiles?: number },
): string[] {
const maxDepth = opts?.maxDepth ?? DEFAULT_SKILL_WALK_MAX_DEPTH;
const maxFiles = opts?.maxFiles ?? DEFAULT_SKILL_WALK_MAX_FILES;
const found: string[] = [];
const visitedRealPaths = new Set<string>();
let filesSeen = 0;
function walk(dir: string, depth: number) {
if (depth > maxDepth) {
throw new Error(
`Skill directory too deep, refusing to walk beyond ${maxDepth} levels`,
);
}
let realDir: string;
try {
realDir = realpathSync(dir);
} catch (err) {
logger.warn(`[skill-import] realpathSync failed for ${dir}:`, err instanceof Error ? err.message : String(err));
return;
}
if (visitedRealPaths.has(realDir)) {
return;
}
visitedRealPaths.add(realDir);
let entries: Dirent[];
try {
entries = readdirSync(dir, { withFileTypes: true });
} catch (err) {
logger.warn(`[skill-import] readdirSync failed for ${dir}:`, err instanceof Error ? err.message : String(err));
return;
}
for (const entry of entries) {
filesSeen++;
if (filesSeen > maxFiles) {
throw new Error(
`Skill directory contains too many files, refusing after ${maxFiles} entries`,
);
}
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) {
walk(fullPath, depth + 1);
} else if (entry.name === "SKILL.md") {
found.push(fullPath);
}
}
}
walk(rootDir, 0);
return found;
}
export function buildLocalInventory(
skillDir: string,
skillMdPath: string,
logger: DiscoveryLogger,
opts?: { maxDepth?: number; maxFiles?: number },
): FileInventoryEntry[] {
const maxDepth = opts?.maxDepth ?? DEFAULT_SKILL_WALK_MAX_DEPTH;
const maxFiles = opts?.maxFiles ?? DEFAULT_SKILL_WALK_MAX_FILES;
const inventory: FileInventoryEntry[] = [
{ path: "SKILL.md", kind: "skill" },
];
const visitedRealPaths = new Set<string>();
let filesSeen = 0;
function walk(dir: string, depth: number) {
if (depth > maxDepth) {
throw new Error(
`Skill directory too deep, refusing to walk beyond ${maxDepth} levels`,
);
}
let realDir: string;
try {
realDir = realpathSync(dir);
} catch (err) {
logger.warn(`[skill-import] realpathSync failed for ${dir}:`, err instanceof Error ? err.message : String(err));
return;
}
if (visitedRealPaths.has(realDir)) {
return;
}
visitedRealPaths.add(realDir);
let entries: Dirent[];
try {
entries = readdirSync(dir, { withFileTypes: true });
} catch (err) {
logger.warn(`[skill-import] readdirSync failed for ${dir}:`, err instanceof Error ? err.message : String(err));
return;
}
for (const entry of entries) {
filesSeen++;
if (filesSeen > maxFiles) {
throw new Error(
`Skill directory contains too many files, refusing after ${maxFiles} entries`,
);
}
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) {
walk(fullPath, depth + 1);
continue;
}
if (!entry.isFile()) continue;
const resolvedPath = resolve(fullPath);
if (resolvedPath === skillMdPath) continue;
let fileStat;
try {
fileStat = statSync(resolvedPath);
} catch (err) {
logger.warn(`[skill-import] statSync failed for ${resolvedPath}:`, err instanceof Error ? err.message : String(err));
continue;
}
if (!fileStat.isFile()) continue;
const relativePath = relative(skillDir, resolvedPath).replace(/\\/g, "/");
inventory.push({
path: relativePath,
kind: (fileStat.mode & 0o111) !== 0 ? "script" : classifyInventoryKind(relativePath),
});
}
}
try {
walk(skillDir, 0);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
if (msg.startsWith("Skill directory too deep") || msg.startsWith("Skill directory contains too many files")) {
throw err;
}
}
return inventory;
}