loader.ts
2104 bytes
1import { dirname } from "node:path";
2import type { FileSystem } from "../ports/fs.ts";
3import type { FrontmatterCodec } from "../frontmatter/parser.ts";
4import type { SkillLocator } from "../discovery/skill-locator.ts";
5import type { SkillRecord } from "../types.ts";
6import { deriveSkillMetadata } from "../frontmatter/validation.ts";
7import { classifyInvocationMode } from "./classifier.ts";
8
9export interface SkillInventory {
10 load(cwd: string): Promise<SkillRecord[]>;
11}
12
13export class DefaultSkillInventory implements SkillInventory {
14 constructor(
15 private readonly locator: SkillLocator,
16 private readonly fs: FileSystem,
17 private readonly codec: FrontmatterCodec,
18 ) {}
19
20 async load(cwd: string): Promise<SkillRecord[]> {
21 const located = await this.locator.findSkillFiles(cwd);
22 const records: SkillRecord[] = [];
23
24 for (const file of located) {
25 try {
26 const raw = await this.fs.readFile(file.filePath);
27 const doc = this.codec.parse(raw);
28 const metadata = deriveSkillMetadata(file.filePath, doc);
29 records.push({
30 id: file.filePath,
31 name: metadata.name,
32 description: metadata.description,
33 filePath: file.filePath,
34 baseDir: dirname(file.filePath),
35 source: file.source,
36 editable: file.editable && doc.hasFrontmatter && !metadata.diagnostics.some((d) => d.severity === "error"),
37 mode: classifyInvocationMode(doc),
38 diagnostics: metadata.diagnostics,
39 });
40 } catch (error) {
41 records.push({
42 id: file.filePath,
43 name: file.filePath.split("/").at(-2) ?? file.filePath,
44 description: "",
45 filePath: file.filePath,
46 baseDir: dirname(file.filePath),
47 source: file.source,
48 editable: false,
49 mode: "agent-invocable",
50 diagnostics: [{ severity: "error", message: error instanceof Error ? error.message : String(error) }],
51 });
52 }
53 }
54
55 return records.sort((a, b) => a.name.localeCompare(b.name) || a.filePath.localeCompare(b.filePath));
56 }
57}