chore(vendor): import pi 0.85.1
Imported as a linear vendor commit so forge rebases do not replay the upstream root tree. git-subtree-dir: third_party/pi git-subtree-split: d981de1229ef899957bbe968bc8dcda02a21f477
This commit is contained in:
1 parent
5ebc7018c3
commit
4def357d09
1669 files changed
+397627
No files matched your search
+396
@@ -0,0 +1,396 @@
|
||||
import ignore from "ignore";
|
||||
import { parse } from "yaml";
|
||||
import type { Context } from "./context.ts";
|
||||
import { type ExecutionEnv, type FileInfo, type Result, type Skill, toError } from "./types.ts";
|
||||
|
||||
const MAX_NAME_LENGTH = 64;
|
||||
const MAX_DESCRIPTION_LENGTH = 1024;
|
||||
const IGNORE_FILE_NAMES = [".gitignore", ".ignore", ".fdignore"];
|
||||
|
||||
type IgnoreMatcher = ReturnType<typeof ignore>;
|
||||
|
||||
export type SkillDiagnosticCode =
|
||||
| "file_info_failed"
|
||||
| "list_failed"
|
||||
| "read_failed"
|
||||
| "parse_failed"
|
||||
| "invalid_metadata";
|
||||
|
||||
/** Warning produced while loading skills. */
|
||||
export interface SkillDiagnostic {
|
||||
/** Diagnostic severity. Currently only warnings are emitted. */
|
||||
type: "warning";
|
||||
/** Stable diagnostic code. */
|
||||
code: SkillDiagnosticCode;
|
||||
/** Human-readable diagnostic message. */
|
||||
message: string;
|
||||
/** Path associated with the diagnostic. */
|
||||
path: string;
|
||||
}
|
||||
|
||||
interface SkillFrontmatter {
|
||||
name?: string;
|
||||
description?: string;
|
||||
"disable-model-invocation"?: boolean;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/** Format a skill invocation prompt, optionally appending additional user instructions. */
|
||||
export function formatSkillInvocation(skill: Skill, additionalInstructions?: string): string {
|
||||
const skillBlock = `<skill name="${skill.name}" location="${skill.filePath}">\nReferences are relative to ${dirnameEnvPath(skill.filePath)}.\n\n${skill.content}\n</skill>`;
|
||||
return additionalInstructions ? `${skillBlock}\n\n${additionalInstructions}` : skillBlock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load skills from one or more directories.
|
||||
*
|
||||
* Traverses directories recursively, loads `SKILL.md` files, loads direct root `.md` files with skill
|
||||
* frontmatter, honors ignore files, and returns diagnostics for invalid declared skill files. Missing input
|
||||
* directories are skipped.
|
||||
*/
|
||||
export async function loadSkills(
|
||||
env: ExecutionEnv,
|
||||
dirs: string | string[],
|
||||
context: Context,
|
||||
): Promise<{ skills: Skill[]; diagnostics: SkillDiagnostic[] }> {
|
||||
const skills: Skill[] = [];
|
||||
const diagnostics: SkillDiagnostic[] = [];
|
||||
for (const dir of Array.isArray(dirs) ? dirs : [dirs]) {
|
||||
const rootInfoResult = await env.fileInfo(dir, context);
|
||||
if (!rootInfoResult.ok) {
|
||||
if (rootInfoResult.error.code !== "not_found") {
|
||||
diagnostics.push({
|
||||
type: "warning",
|
||||
code: "file_info_failed",
|
||||
message: rootInfoResult.error.message,
|
||||
path: dir,
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const rootInfo = rootInfoResult.value;
|
||||
if ((await resolveKind(env, rootInfo, diagnostics, context)) !== "directory") continue;
|
||||
const result = await loadSkillsFromDirInternal(env, rootInfo.path, true, ignore(), rootInfo.path, context);
|
||||
skills.push(...result.skills);
|
||||
diagnostics.push(...result.diagnostics);
|
||||
}
|
||||
return { skills, diagnostics };
|
||||
}
|
||||
|
||||
/**
|
||||
* Load skills from source-tagged directories.
|
||||
*
|
||||
* Source values are preserved exactly and attached to every loaded skill and diagnostic. The agent package does not
|
||||
* interpret source values; applications define their own provenance shape.
|
||||
*/
|
||||
export async function loadSourcedSkills<TSource, TSkill extends Skill = Skill>(
|
||||
env: ExecutionEnv,
|
||||
inputs: Array<{ path: string; source: TSource }>,
|
||||
mapSkill: ((skill: Skill, source: TSource, context: Context) => TSkill) | undefined,
|
||||
context: Context,
|
||||
): Promise<{
|
||||
skills: Array<{ skill: TSkill; source: TSource }>;
|
||||
diagnostics: Array<SkillDiagnostic & { source: TSource }>;
|
||||
}> {
|
||||
const skills: Array<{ skill: TSkill; source: TSource }> = [];
|
||||
const diagnostics: Array<SkillDiagnostic & { source: TSource }> = [];
|
||||
for (const input of inputs) {
|
||||
const result = await loadSkills(env, input.path, context);
|
||||
for (const skill of result.skills) {
|
||||
skills.push({
|
||||
skill: mapSkill ? mapSkill(skill, input.source, context) : (skill as TSkill),
|
||||
source: input.source,
|
||||
});
|
||||
}
|
||||
for (const diagnostic of result.diagnostics) diagnostics.push({ ...diagnostic, source: input.source });
|
||||
}
|
||||
return { skills, diagnostics };
|
||||
}
|
||||
|
||||
async function loadSkillsFromDirInternal(
|
||||
env: ExecutionEnv,
|
||||
dir: string,
|
||||
includeRootFiles: boolean,
|
||||
ignoreMatcher: IgnoreMatcher,
|
||||
rootDir: string,
|
||||
context: Context,
|
||||
): Promise<{ skills: Skill[]; diagnostics: SkillDiagnostic[] }> {
|
||||
const skills: Skill[] = [];
|
||||
const diagnostics: SkillDiagnostic[] = [];
|
||||
|
||||
const dirInfoResult = await env.fileInfo(dir, context);
|
||||
if (!dirInfoResult.ok) {
|
||||
if (dirInfoResult.error.code !== "not_found") {
|
||||
diagnostics.push({
|
||||
type: "warning",
|
||||
code: "file_info_failed",
|
||||
message: dirInfoResult.error.message,
|
||||
path: dir,
|
||||
});
|
||||
}
|
||||
return { skills, diagnostics };
|
||||
}
|
||||
const dirInfo = dirInfoResult.value;
|
||||
if ((await resolveKind(env, dirInfo, diagnostics, context)) !== "directory") return { skills, diagnostics };
|
||||
|
||||
await addIgnoreRules(env, ignoreMatcher, dir, rootDir, diagnostics, context);
|
||||
|
||||
const entriesResult = await env.listDir(dir, context);
|
||||
if (!entriesResult.ok) {
|
||||
diagnostics.push({ type: "warning", code: "list_failed", message: entriesResult.error.message, path: dir });
|
||||
return { skills, diagnostics };
|
||||
}
|
||||
const entries = entriesResult.value;
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.name !== "SKILL.md") continue;
|
||||
const fullPath = entry.path;
|
||||
const kind = await resolveKind(env, entry, diagnostics, context);
|
||||
if (kind !== "file") continue;
|
||||
const relPath = relativeEnvPath(rootDir, fullPath);
|
||||
if (ignoreMatcher.ignores(relPath)) continue;
|
||||
|
||||
const result = await loadSkillFromFile(env, fullPath, dirInfo.name, context);
|
||||
if (result.skill) skills.push(result.skill);
|
||||
diagnostics.push(...result.diagnostics);
|
||||
return { skills, diagnostics };
|
||||
}
|
||||
|
||||
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
||||
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
||||
const fullPath = entry.path;
|
||||
const kind = await resolveKind(env, entry, diagnostics, context);
|
||||
if (!kind) continue;
|
||||
|
||||
const relPath = relativeEnvPath(rootDir, fullPath);
|
||||
const ignorePath = kind === "directory" ? `${relPath}/` : relPath;
|
||||
if (ignoreMatcher.ignores(ignorePath)) continue;
|
||||
|
||||
if (kind === "directory") {
|
||||
const result = await loadSkillsFromDirInternal(env, fullPath, false, ignoreMatcher, rootDir, context);
|
||||
skills.push(...result.skills);
|
||||
diagnostics.push(...result.diagnostics);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (kind !== "file" || !includeRootFiles || !entry.name.endsWith(".md")) continue;
|
||||
const result = await loadSkillFromFile(env, fullPath, dirInfo.name, context);
|
||||
if (result.skill) skills.push(result.skill);
|
||||
diagnostics.push(...result.diagnostics);
|
||||
}
|
||||
|
||||
return { skills, diagnostics };
|
||||
}
|
||||
|
||||
async function addIgnoreRules(
|
||||
env: ExecutionEnv,
|
||||
ig: IgnoreMatcher,
|
||||
dir: string,
|
||||
rootDir: string,
|
||||
diagnostics: SkillDiagnostic[],
|
||||
context: Context,
|
||||
): Promise<void> {
|
||||
const relativeDir = relativeEnvPath(rootDir, dir);
|
||||
const prefix = relativeDir ? `${relativeDir}/` : "";
|
||||
|
||||
for (const filename of IGNORE_FILE_NAMES) {
|
||||
const ignorePathResult = await env.joinPath([dir, filename], context);
|
||||
if (!ignorePathResult.ok) {
|
||||
diagnostics.push({
|
||||
type: "warning",
|
||||
code: "file_info_failed",
|
||||
message: ignorePathResult.error.message,
|
||||
path: dir,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const ignorePath = ignorePathResult.value;
|
||||
const info = await env.fileInfo(ignorePath, context);
|
||||
if (!info.ok) {
|
||||
if (info.error.code !== "not_found") {
|
||||
diagnostics.push({
|
||||
type: "warning",
|
||||
code: "file_info_failed",
|
||||
message: info.error.message,
|
||||
path: ignorePath,
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (info.value.kind !== "file") continue;
|
||||
const content = await env.readTextFile(ignorePath, context);
|
||||
if (!content.ok) {
|
||||
diagnostics.push({ type: "warning", code: "read_failed", message: content.error.message, path: ignorePath });
|
||||
continue;
|
||||
}
|
||||
const patterns = content.value
|
||||
.split(/\r?\n/)
|
||||
.map((line) => prefixIgnorePattern(line, prefix))
|
||||
.filter((line): line is string => Boolean(line));
|
||||
if (patterns.length > 0) ig.add(patterns);
|
||||
}
|
||||
}
|
||||
|
||||
function prefixIgnorePattern(line: string, prefix: string): string | null {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) return null;
|
||||
if (trimmed.startsWith("#") && !trimmed.startsWith("\\#")) return null;
|
||||
|
||||
let pattern = line;
|
||||
let negated = false;
|
||||
if (pattern.startsWith("!")) {
|
||||
negated = true;
|
||||
pattern = pattern.slice(1);
|
||||
} else if (pattern.startsWith("\\!")) {
|
||||
pattern = pattern.slice(1);
|
||||
}
|
||||
if (pattern.startsWith("/")) pattern = pattern.slice(1);
|
||||
const prefixed = prefix ? `${prefix}${pattern}` : pattern;
|
||||
return negated ? `!${prefixed}` : prefixed;
|
||||
}
|
||||
|
||||
async function loadSkillFromFile(
|
||||
env: ExecutionEnv,
|
||||
filePath: string,
|
||||
parentDirName: string,
|
||||
context: Context,
|
||||
): Promise<{ skill: Skill | null; diagnostics: SkillDiagnostic[] }> {
|
||||
const diagnostics: SkillDiagnostic[] = [];
|
||||
const isDeclaredSkill =
|
||||
filePath
|
||||
.replace(/[\\/]+$/, "")
|
||||
.split(/[\\/]/)
|
||||
.pop() === "SKILL.md";
|
||||
const rawContent = await env.readTextFile(filePath, context);
|
||||
if (!rawContent.ok) {
|
||||
diagnostics.push({ type: "warning", code: "read_failed", message: rawContent.error.message, path: filePath });
|
||||
return { skill: null, diagnostics };
|
||||
}
|
||||
|
||||
const parsed = parseFrontmatter<SkillFrontmatter>(rawContent.value);
|
||||
if (!parsed.ok) {
|
||||
if (isDeclaredSkill) {
|
||||
diagnostics.push({ type: "warning", code: "parse_failed", message: parsed.error.message, path: filePath });
|
||||
}
|
||||
return { skill: null, diagnostics };
|
||||
}
|
||||
|
||||
const { frontmatter, body } = parsed.value;
|
||||
const description = typeof frontmatter.description === "string" ? frontmatter.description : undefined;
|
||||
if (!isDeclaredSkill && (!description || description.trim() === "")) {
|
||||
return { skill: null, diagnostics };
|
||||
}
|
||||
|
||||
for (const error of validateDescription(description)) {
|
||||
diagnostics.push({ type: "warning", code: "invalid_metadata", message: error, path: filePath });
|
||||
}
|
||||
|
||||
const frontmatterName = typeof frontmatter.name === "string" ? frontmatter.name : undefined;
|
||||
const name = frontmatterName || parentDirName;
|
||||
for (const error of validateName(name, parentDirName)) {
|
||||
diagnostics.push({ type: "warning", code: "invalid_metadata", message: error, path: filePath });
|
||||
}
|
||||
|
||||
if (!description || description.trim() === "") {
|
||||
return { skill: null, diagnostics };
|
||||
}
|
||||
|
||||
return {
|
||||
skill: {
|
||||
name,
|
||||
description,
|
||||
content: body,
|
||||
filePath,
|
||||
disableModelInvocation: frontmatter["disable-model-invocation"] === true,
|
||||
},
|
||||
diagnostics,
|
||||
};
|
||||
}
|
||||
|
||||
function validateName(name: string, parentDirName: string): string[] {
|
||||
const errors: string[] = [];
|
||||
if (name !== parentDirName) errors.push(`name "${name}" does not match parent directory "${parentDirName}"`);
|
||||
if (name.length > MAX_NAME_LENGTH) errors.push(`name exceeds ${MAX_NAME_LENGTH} characters (${name.length})`);
|
||||
if (!/^[a-z0-9-]+$/.test(name)) {
|
||||
errors.push("name contains invalid characters (must be lowercase a-z, 0-9, hyphens only)");
|
||||
}
|
||||
if (name.startsWith("-") || name.endsWith("-")) errors.push("name must not start or end with a hyphen");
|
||||
if (name.includes("--")) errors.push("name must not contain consecutive hyphens");
|
||||
return errors;
|
||||
}
|
||||
|
||||
function validateDescription(description: string | undefined): string[] {
|
||||
const errors: string[] = [];
|
||||
if (!description || description.trim() === "") {
|
||||
errors.push("description is required");
|
||||
} else if (description.length > MAX_DESCRIPTION_LENGTH) {
|
||||
errors.push(`description exceeds ${MAX_DESCRIPTION_LENGTH} characters (${description.length})`);
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
function parseFrontmatter<T extends Record<string, unknown>>(
|
||||
content: string,
|
||||
): Result<{ frontmatter: T; body: string }, Error> {
|
||||
try {
|
||||
const normalized = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
||||
if (!normalized.startsWith("---")) return { ok: true, value: { frontmatter: {} as T, body: normalized } };
|
||||
const endIndex = normalized.indexOf("\n---", 3);
|
||||
if (endIndex === -1) return { ok: true, value: { frontmatter: {} as T, body: normalized } };
|
||||
const yamlString = normalized.slice(4, endIndex);
|
||||
const body = normalized.slice(endIndex + 4).trim();
|
||||
return { ok: true, value: { frontmatter: (parse(yamlString) ?? {}) as T, body } };
|
||||
} catch (error) {
|
||||
return { ok: false, error: toError(error) };
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveKind(
|
||||
env: ExecutionEnv,
|
||||
info: FileInfo,
|
||||
diagnostics: SkillDiagnostic[],
|
||||
context: Context,
|
||||
): Promise<"file" | "directory" | undefined> {
|
||||
if (info.kind === "file" || info.kind === "directory") return info.kind;
|
||||
const canonicalPath = await env.canonicalPath(info.path, context);
|
||||
if (!canonicalPath.ok) {
|
||||
if (canonicalPath.error.code !== "not_found") {
|
||||
diagnostics.push({
|
||||
type: "warning",
|
||||
code: "file_info_failed",
|
||||
message: canonicalPath.error.message,
|
||||
path: info.path,
|
||||
});
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
const target = await env.fileInfo(canonicalPath.value, context);
|
||||
if (!target.ok) {
|
||||
if (target.error.code !== "not_found") {
|
||||
diagnostics.push({
|
||||
type: "warning",
|
||||
code: "file_info_failed",
|
||||
message: target.error.message,
|
||||
path: info.path,
|
||||
});
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
return target.value.kind === "file" || target.value.kind === "directory" ? target.value.kind : undefined;
|
||||
}
|
||||
|
||||
function dirnameEnvPath(path: string): string {
|
||||
const normalized = path.replace(/[\\/]+$/, "");
|
||||
const separatorIndex = Math.max(normalized.lastIndexOf("/"), normalized.lastIndexOf("\\"));
|
||||
if (separatorIndex === 2 && normalized[1] === ":") return normalized.slice(0, 3);
|
||||
return separatorIndex <= 0 ? "/" : normalized.slice(0, separatorIndex);
|
||||
}
|
||||
|
||||
function relativeEnvPath(root: string, path: string): string {
|
||||
const normalizedRoot = root.replace(/\\/g, "/").replace(/\/+$/, "");
|
||||
const normalizedPath = path.replace(/\\/g, "/").replace(/\/+$/, "");
|
||||
if (normalizedPath === normalizedRoot) return "";
|
||||
return normalizedPath.startsWith(`${normalizedRoot}/`)
|
||||
? normalizedPath.slice(normalizedRoot.length + 1)
|
||||
: normalizedPath.replace(/^\/+/, "");
|
||||
}
|
||||
Reference in new issue
Block a user