mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-11 04:52:50 +00:00
* refactor: extract filesystem safety primitives * refactor: use fs-safe for file access helpers * refactor: reuse fs-safe for media reads * refactor: use fs-safe for image reads * refactor: reuse fs-safe in qqbot media opener * refactor: reuse fs-safe for local media checks * refactor: consume cleaner fs-safe api * refactor: align fs-safe json option names * fix: preserve fs-safe migration contracts * refactor: use fs-safe primitive subpaths * refactor: use grouped fs-safe subpaths * refactor: align fs-safe api usage * refactor: adapt private state store api * chore: refresh proof gate * refactor: follow fs-safe json api split * refactor: follow reduced fs-safe surface * build: default fs-safe python helper off * fix: preserve fs-safe plugin sdk aliases * refactor: consolidate fs-safe usage * refactor: unify fs-safe store usage * refactor: trim fs-safe temp workspace usage * refactor: hide low-level fs-safe primitives * build: use published fs-safe package * fix: preserve outbound recovery durability after rebase * chore: refresh pr checks
173 lines
4.3 KiB
TypeScript
173 lines
4.3 KiB
TypeScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { openRootFileSync } from "../../infra/boundary-file-read.js";
|
|
import { parseFrontmatter, resolveSkillInvocationPolicy } from "./frontmatter.js";
|
|
import { createSyntheticSourceInfo, type Skill } from "./skill-contract.js";
|
|
import type { ParsedSkillFrontmatter } from "./types.js";
|
|
|
|
type LoadedLocalSkill = {
|
|
skill: Skill;
|
|
frontmatter: ParsedSkillFrontmatter;
|
|
};
|
|
|
|
function readSkillFileSync(params: {
|
|
rootRealPath: string;
|
|
filePath: string;
|
|
maxBytes?: number;
|
|
}): string | null {
|
|
const opened = openRootFileSync({
|
|
absolutePath: params.filePath,
|
|
rootPath: params.rootRealPath,
|
|
rootRealPath: params.rootRealPath,
|
|
boundaryLabel: "skill root",
|
|
maxBytes: params.maxBytes,
|
|
});
|
|
if (!opened.ok) {
|
|
return null;
|
|
}
|
|
try {
|
|
return fs.readFileSync(opened.fd, "utf8");
|
|
} finally {
|
|
fs.closeSync(opened.fd);
|
|
}
|
|
}
|
|
|
|
function loadSingleSkillDirectory(params: {
|
|
skillDir: string;
|
|
source: string;
|
|
rootRealPath: string;
|
|
maxBytes?: number;
|
|
}): LoadedLocalSkill | null {
|
|
const skillFilePath = path.join(params.skillDir, "SKILL.md");
|
|
const raw = readSkillFileSync({
|
|
rootRealPath: params.rootRealPath,
|
|
filePath: skillFilePath,
|
|
maxBytes: params.maxBytes,
|
|
});
|
|
if (!raw) {
|
|
return null;
|
|
}
|
|
|
|
let frontmatter: Record<string, string>;
|
|
try {
|
|
frontmatter = parseFrontmatter(raw);
|
|
} catch {
|
|
return null;
|
|
}
|
|
|
|
const fallbackName = path.basename(params.skillDir).trim();
|
|
const name = frontmatter.name?.trim() || fallbackName;
|
|
const description = frontmatter.description?.trim();
|
|
if (!name || !description) {
|
|
return null;
|
|
}
|
|
const invocation = resolveSkillInvocationPolicy(frontmatter);
|
|
const filePath = path.resolve(skillFilePath);
|
|
const baseDir = path.resolve(params.skillDir);
|
|
|
|
return {
|
|
skill: {
|
|
name,
|
|
description,
|
|
filePath,
|
|
baseDir,
|
|
source: params.source,
|
|
sourceInfo: createSyntheticSourceInfo(filePath, {
|
|
source: params.source,
|
|
baseDir,
|
|
scope: "project",
|
|
origin: "top-level",
|
|
}),
|
|
disableModelInvocation: invocation.disableModelInvocation,
|
|
},
|
|
frontmatter,
|
|
};
|
|
}
|
|
|
|
function listCandidateSkillDirs(dir: string): string[] {
|
|
try {
|
|
return fs
|
|
.readdirSync(dir, { withFileTypes: true })
|
|
.filter(
|
|
(entry) =>
|
|
entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules",
|
|
)
|
|
.map((entry) => path.join(dir, entry.name))
|
|
.sort((left, right) => left.localeCompare(right));
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export function loadSkillsFromDirSafe(params: { dir: string; source: string; maxBytes?: number }): {
|
|
skills: Skill[];
|
|
frontmatterByFilePath: ReadonlyMap<string, ParsedSkillFrontmatter>;
|
|
} {
|
|
const rootDir = path.resolve(params.dir);
|
|
let rootRealPath: string;
|
|
try {
|
|
rootRealPath = fs.realpathSync(rootDir);
|
|
} catch {
|
|
return { skills: [], frontmatterByFilePath: new Map() };
|
|
}
|
|
|
|
const rootSkill = loadSingleSkillDirectory({
|
|
skillDir: rootDir,
|
|
source: params.source,
|
|
rootRealPath,
|
|
maxBytes: params.maxBytes,
|
|
});
|
|
if (rootSkill) {
|
|
return {
|
|
skills: [rootSkill.skill],
|
|
frontmatterByFilePath: new Map([[rootSkill.skill.filePath, rootSkill.frontmatter]]),
|
|
};
|
|
}
|
|
|
|
const loadedSkills = listCandidateSkillDirs(rootDir)
|
|
.map((skillDir) =>
|
|
loadSingleSkillDirectory({
|
|
skillDir,
|
|
source: params.source,
|
|
rootRealPath,
|
|
maxBytes: params.maxBytes,
|
|
}),
|
|
)
|
|
.filter((skill): skill is LoadedLocalSkill => skill !== null);
|
|
const frontmatterByFilePath = new Map<string, ParsedSkillFrontmatter>();
|
|
for (const loaded of loadedSkills) {
|
|
frontmatterByFilePath.set(loaded.skill.filePath, loaded.frontmatter);
|
|
}
|
|
|
|
return {
|
|
skills: loadedSkills.map((loaded) => loaded.skill),
|
|
frontmatterByFilePath,
|
|
};
|
|
}
|
|
|
|
export function readSkillFrontmatterSafe(params: {
|
|
rootDir: string;
|
|
filePath: string;
|
|
maxBytes?: number;
|
|
}): Record<string, string> | null {
|
|
let rootRealPath: string;
|
|
try {
|
|
rootRealPath = fs.realpathSync(path.resolve(params.rootDir));
|
|
} catch {
|
|
return null;
|
|
}
|
|
const raw = readSkillFileSync({
|
|
rootRealPath,
|
|
filePath: path.resolve(params.filePath),
|
|
maxBytes: params.maxBytes,
|
|
});
|
|
if (!raw) {
|
|
return null;
|
|
}
|
|
try {
|
|
return parseFrontmatter(raw);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|