{"version":3,"file":"directory-source.mjs","names":[],"sources":["../../../../../../../../ai/src/skills/sources/directory-source.ts"],"sourcesContent":["import type {\n  SkillCatalogEntry,\n  SkillRecord,\n} from \"../contracts/skill-record.type\";\nimport type { SkillsStoreContract } from \"../contracts/skills-store.contract\";\nimport { parseFrontmatter, parseTags } from \"./parse-frontmatter\";\n\n/**\n * Lazily-loaded `node:fs/promises`. Core takes no new filesystem\n * dependency — a `store`-only or `url`-only consumer never touches the\n * filesystem because the module is imported on first read, not at module\n * load. Settled once and cached.\n */\nlet fsMod: typeof import(\"node:fs/promises\") | undefined;\nlet pathMod: typeof import(\"node:path\") | undefined;\n\nasync function loadFs(): Promise<{\n  fs: typeof import(\"node:fs/promises\");\n  path: typeof import(\"node:path\");\n}> {\n  if (!fsMod) {\n    fsMod = await import(\"node:fs/promises\");\n  }\n\n  if (!pathMod) {\n    pathMod = await import(\"node:path\");\n  }\n\n  return { fs: fsMod, path: pathMod };\n}\n\n/**\n * Read `path/<folder>/SKILL.md` into {@link SkillRecord}s, parsing the\n * same `key: value` front-matter as `scripts/generate-llms.mjs`. Each\n * direct sub-directory holding a `SKILL.md` becomes one skill named after\n * the folder; the `description` comes from front-matter, `tags` from a\n * comma-separated `tags:` line, and the body is everything after the\n * closing `---`. Files at the root (e.g. `README.md`) are ignored.\n *\n * Reads are a snapshot at first call and cached for the source's lifetime\n * (a single agent run reads the catalog and bodies from one consistent\n * view). A missing directory yields an empty library, not a throw.\n */\nexport function directorySource(dirPath: string): SkillsStoreContract {\n  let cache: Promise<Map<string, SkillRecord>> | undefined;\n\n  const records = (): Promise<Map<string, SkillRecord>> => {\n    if (!cache) {\n      cache = readDirectory(dirPath);\n    }\n\n    return cache;\n  };\n\n  return {\n    async list(scope?: { tags?: string[] }): Promise<SkillCatalogEntry[]> {\n      const all = await records();\n      const wanted = scope?.tags;\n\n      return [...all.values()]\n        .filter((record) => intersects(record.tags, wanted))\n        .map(toCatalogEntry);\n    },\n    async load(name: string, version?: number): Promise<SkillRecord | undefined> {\n      const all = await records();\n      const record = all.get(name);\n\n      if (!record) {\n        return undefined;\n      }\n\n      if (version !== undefined && record.version !== version) {\n        return undefined;\n      }\n\n      return record;\n    },\n    async saveCandidate(): Promise<SkillRecord> {\n      throw new Error(\n        \"directory source is read-only — saveCandidate requires a writable store (set `review.store`)\",\n      );\n    },\n    async promote(): Promise<SkillRecord> {\n      throw new Error(\n        \"directory source is read-only — promote requires a writable store (set `review.store`)\",\n      );\n    },\n  };\n}\n\n/** Walk the directory once, parsing every `<folder>/SKILL.md` into a record. */\nasync function readDirectory(dirPath: string): Promise<Map<string, SkillRecord>> {\n  const { fs, path } = await loadFs();\n  const records = new Map<string, SkillRecord>();\n\n  let entries: Array<{ name: string; isDirectory(): boolean }>;\n\n  try {\n    entries = await fs.readdir(dirPath, { withFileTypes: true });\n  } catch {\n    // Missing directory ⇒ empty library; the catalog simply omits it.\n    return records;\n  }\n\n  for (const entry of entries) {\n    if (!entry.isDirectory()) {\n      continue;\n    }\n\n    const skillFile = path.join(dirPath, entry.name, \"SKILL.md\");\n\n    let text: string;\n\n    try {\n      text = await fs.readFile(skillFile, \"utf8\");\n    } catch {\n      // A sub-directory without a SKILL.md is not a skill — skip it.\n      continue;\n    }\n\n    const { meta, body } = parseFrontmatter(text);\n\n    records.set(entry.name, {\n      name: entry.name,\n      description: meta.description ?? \"(no description)\",\n      version: 1,\n      body: body.trim(),\n      tags: parseTags(meta.tags),\n      type: \"authored\",\n    });\n  }\n\n  return records;\n}\n\n/** Project a full record down to its catalog entry (body omitted). */\nfunction toCatalogEntry(record: SkillRecord): SkillCatalogEntry {\n  return {\n    name: record.name,\n    description: record.description,\n    version: record.version,\n    tags: record.tags,\n    type: record.type,\n  };\n}\n\n/** True when no filter is requested or the record shares a requested tag. */\nfunction intersects(recordTags: string[] | undefined, wanted: string[] | undefined): boolean {\n  if (!wanted || wanted.length === 0) {\n    return true;\n  }\n\n  if (!recordTags || recordTags.length === 0) {\n    return false;\n  }\n\n  return recordTags.some((tag) => wanted.includes(tag));\n}\n"],"mappings":";;;;;;;;;AAaA,IAAI;AACJ,IAAI;AAEJ,eAAe,SAGZ;CACD,IAAI,CAAC,OACH,QAAQ,MAAM,OAAO;CAGvB,IAAI,CAAC,SACH,UAAU,MAAM,OAAO;CAGzB,OAAO;EAAE,IAAI;EAAO,MAAM;CAAQ;AACpC;;;;;;;;;;;;;AAcA,SAAgB,gBAAgB,SAAsC;CACpE,IAAI;CAEJ,MAAM,gBAAmD;EACvD,IAAI,CAAC,OACH,QAAQ,cAAc,OAAO;EAG/B,OAAO;CACT;CAEA,OAAO;EACL,MAAM,KAAK,OAA2D;GACpE,MAAM,MAAM,MAAM,QAAQ;GAC1B,MAAM,SAAS,OAAO;GAEtB,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,CAAC,CACrB,QAAQ,WAAW,WAAW,OAAO,MAAM,MAAM,CAAC,CAAC,CACnD,IAAI,cAAc;EACvB;EACA,MAAM,KAAK,MAAc,SAAoD;GAE3E,MAAM,UAAS,MADG,QAAQ,EACR,CAAC,IAAI,IAAI;GAE3B,IAAI,CAAC,QACH;GAGF,IAAI,YAAY,UAAa,OAAO,YAAY,SAC9C;GAGF,OAAO;EACT;EACA,MAAM,gBAAsC;GAC1C,MAAM,IAAI,MACR,8FACF;EACF;EACA,MAAM,UAAgC;GACpC,MAAM,IAAI,MACR,wFACF;EACF;CACF;AACF;;AAGA,eAAe,cAAc,SAAoD;CAC/E,MAAM,EAAE,IAAI,SAAS,MAAM,OAAO;CAClC,MAAM,0BAAU,IAAI,IAAyB;CAE7C,IAAI;CAEJ,IAAI;EACF,UAAU,MAAM,GAAG,QAAQ,SAAS,EAAE,eAAe,KAAK,CAAC;CAC7D,QAAQ;EAEN,OAAO;CACT;CAEA,KAAK,MAAM,SAAS,SAAS;EAC3B,IAAI,CAAC,MAAM,YAAY,GACrB;EAGF,MAAM,YAAY,KAAK,KAAK,SAAS,MAAM,MAAM,UAAU;EAE3D,IAAI;EAEJ,IAAI;GACF,OAAO,MAAM,GAAG,SAAS,WAAW,MAAM;EAC5C,QAAQ;GAEN;EACF;EAEA,MAAM,EAAE,MAAM,SAAS,iBAAiB,IAAI;EAE5C,QAAQ,IAAI,MAAM,MAAM;GACtB,MAAM,MAAM;GACZ,aAAa,KAAK,eAAe;GACjC,SAAS;GACT,MAAM,KAAK,KAAK;GAChB,MAAM,UAAU,KAAK,IAAI;GACzB,MAAM;EACR,CAAC;CACH;CAEA,OAAO;AACT;;AAGA,SAAS,eAAe,QAAwC;CAC9D,OAAO;EACL,MAAM,OAAO;EACb,aAAa,OAAO;EACpB,SAAS,OAAO;EAChB,MAAM,OAAO;EACb,MAAM,OAAO;CACf;AACF;;AAGA,SAAS,WAAW,YAAkC,QAAuC;CAC3F,IAAI,CAAC,UAAU,OAAO,WAAW,GAC/B,OAAO;CAGT,IAAI,CAAC,cAAc,WAAW,WAAW,GACvC,OAAO;CAGT,OAAO,WAAW,MAAM,QAAQ,OAAO,SAAS,GAAG,CAAC;AACtD"}