{"version":3,"file":"index.cjs","names":["path","loadSkillsFromDir","materializePluginEntries","resolvePluginEntries","listDomainNames","fs","formatSkillsForPrompt"],"sources":["../../../../src/services/skillCatalog/index.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\n\nimport { listDomainNames, type PluginSkillDiscovery, resolvePluginEntries } from '@agimon-ai/doompi-config/domains';\nimport type { SkillSourceContribution } from '@agimon-ai/doompi-core/skills';\nimport { materializePluginEntries } from '@agimon-ai/doompi-domain/plugins';\nimport { formatSkillsForPrompt, loadSkillsFromDir, type Skill } from '@earendil-works/pi-coding-agent';\n\nconst SKILLS_DIRECTORY = 'skills';\nconst SHARED_SKILLS_OWNER = '.claude/skills';\n\nexport type SkillGroupKey = 'extensions' | 'help' | 'plugins' | 'default';\n\nexport interface SkillEntry {\n  name: string;\n  description: string;\n  filePath: string;\n  baseDir: string;\n  group: SkillGroupKey;\n  /** Package name for an extension, plugin directory name, or `.claude/skills`. */\n  owner: string;\n  modelInvocable: boolean;\n  /**\n   * What removing this skill would save from the `<available_skills>` block.\n   *\n   * Marginal rather than proportional: the block has fixed framing that no\n   * single skill pays for, so the useful figure is what actually goes away.\n   * Bodies are excluded because they load on demand and are not a resting cost.\n   */\n  promptTokens?: number;\n}\n\nexport interface SkillOwnerNode {\n  owner: string;\n  skills: SkillEntry[];\n}\n\nexport interface SkillGroup {\n  key: SkillGroupKey;\n  label: string;\n  owners: SkillOwnerNode[];\n}\n\nexport interface SkillCatalog {\n  groups: SkillGroup[];\n  skillCount: number;\n  /** Tokens the `<available_skills>` prompt block costs on every request. */\n  promptTokens: number;\n  /** Tokens every loaded SKILL.md costs in full, if all of them were read. */\n  bodyTokens: number;\n  /** Frontmatter and read failures, surfaced instead of thrown. */\n  diagnostics: string[];\n}\n\nexport interface SkillCatalogOptions {\n  repoRoot: string;\n  /** Exact files the current selection resolved to, from `HarnessState.skillDirectories`. */\n  activeSkillDirectories: readonly string[];\n  extensionSources: readonly SkillSourceContribution[];\n  /** Activation-gated wrappers already accepted by the merged active-skill view. */\n  helpSkills?: readonly Skill[];\n  /** Bounded Help activation and collision diagnostics. */\n  helpDiagnostics?: readonly string[];\n}\n\n/**\n * The tokenizer, loaded on first use rather than at import.\n *\n * `gpt-tokenizer` carries its whole vocabulary and costs about a tenth of a\n * second to load. This module is reachable from the skills extension, which Pi\n * loads at session start, so a top-level import would charge every session for\n * a surface most of them never open.\n */\nlet tokenizer: Promise<typeof import('gpt-tokenizer')> | undefined;\n\nexport async function counter(): Promise<(text: string) => number> {\n  tokenizer ??= import('gpt-tokenizer');\n  return (await tokenizer).countTokens;\n}\n\n/**\n * Matches a directory against the active set.\n *\n * A domain that lists a plugin subset puts individual skill directories in the\n * set rather than the plugin's `skills` root, so containment has to be checked\n * both ways: the set may hold an ancestor of the directory, or a descendant of\n * it.\n */\nfunction createActiveMatcher(directories: readonly string[]): (directory: string) => boolean {\n  const active = directories.map((directory) => path.resolve(directory));\n  return (directory) => {\n    const resolved = path.resolve(directory);\n    return active.some(\n      (entry) =>\n        entry === resolved || entry.startsWith(`${resolved}${path.sep}`) || resolved.startsWith(`${entry}${path.sep}`),\n    );\n  };\n}\n\nfunction collect(\n  directory: string,\n  group: SkillGroupKey,\n  owner: string,\n  isActive: (directory: string) => boolean,\n  loaded: Skill[],\n  diagnostics: string[],\n  discovery: PluginSkillDiscovery = 'recursive',\n): SkillEntry[] {\n  const result = loadSkillsFromDir({ dir: directory, source: owner });\n  for (const diagnostic of result.diagnostics) {\n    diagnostics.push(`${diagnostic.path ?? directory}: ${diagnostic.message}`);\n  }\n  // Checked per skill, not per root: a subset domain selects some of a plugin's\n  // skills and leaves the rest on disk, unloaded and therefore not listed.\n  const root = path.resolve(directory);\n  const kept = result.skills.filter((skill) => {\n    const skillDirectory = path.dirname(skill.filePath);\n    if (discovery === 'direct-children' && path.dirname(skillDirectory) !== root) return false;\n    return isActive(skillDirectory);\n  });\n  // Kept in their loaded form as well, because the prompt-block cost is measured\n  // by handing them back to Pi's own formatter.\n  loaded.push(...kept);\n  return kept\n    .map((skill) => ({\n      name: skill.name,\n      description: skill.description,\n      filePath: skill.filePath,\n      baseDir: skill.baseDir,\n      group,\n      owner,\n      modelInvocable: !skill.disableModelInvocation,\n    }))\n    .sort((left, right) => left.name.localeCompare(right.name));\n}\n\nfunction sortOwners(owners: SkillOwnerNode[]): SkillOwnerNode[] {\n  return owners.sort((left, right) => left.owner.localeCompare(right.owner));\n}\n\nfunction collectHelpSkills(skills: readonly Skill[], loaded: Skill[], diagnostics: string[]): SkillOwnerNode[] {\n  const occupiedNames = new Set(loaded.map((skill) => skill.name));\n  const owners = new Map<string, SkillEntry[]>();\n  const candidates = [...skills].sort(\n    (left, right) =>\n      left.sourceInfo.source.localeCompare(right.sourceInfo.source) ||\n      left.name.localeCompare(right.name) ||\n      left.filePath.localeCompare(right.filePath),\n  );\n  for (const skill of candidates) {\n    if (occupiedNames.has(skill.name)) {\n      diagnostics.push(\n        `Help ${skill.sourceInfo.source} [HELP_SKILL_COLLISION]: '${skill.name}' was ignored because a normal skill wins.`,\n      );\n      continue;\n    }\n    occupiedNames.add(skill.name);\n    loaded.push(skill);\n    const owner = skill.sourceInfo.source;\n    const entries = owners.get(owner) ?? [];\n    entries.push({\n      name: skill.name,\n      description: skill.description,\n      filePath: skill.filePath,\n      baseDir: skill.baseDir,\n      group: 'help',\n      owner,\n      modelInvocable: !skill.disableModelInvocation,\n    });\n    owners.set(owner, entries);\n  }\n  return sortOwners(\n    [...owners.entries()].map(([owner, entries]) => ({\n      owner,\n      skills: entries.sort((left, right) => left.name.localeCompare(right.name)),\n    })),\n  );\n}\n\n/**\n * Every skill this session actually loaded, grouped by where it comes from,\n * priced in tokens.\n *\n * Discovery runs over `loadSkillsFromDir` from Pi rather than the harness's own\n * walker so the listing matches what Pi would load, including its ignore-file\n * handling; the same skills are then handed to Pi's `formatSkillsForPrompt` so\n * the always-on figure is the real prompt block rather than a reconstruction of\n * it.\n */\nexport async function buildSkillCatalog(options: SkillCatalogOptions): Promise<SkillCatalog> {\n  const { repoRoot } = options;\n  const isActive = createActiveMatcher(options.activeSkillDirectories);\n  const diagnostics: string[] = [...(options.helpDiagnostics ?? [])];\n  const loaded: Skill[] = [];\n\n  const extensionOwners: SkillOwnerNode[] = [];\n  for (const entry of options.extensionSources) {\n    const skills = entry.directories.flatMap((directory) =>\n      collect(directory, 'extensions', entry.source, () => true, loaded, diagnostics),\n    );\n    if (skills.length > 0) extensionOwners.push({ owner: entry.source, skills });\n  }\n\n  const pluginOwners: SkillOwnerNode[] = [];\n  const domainPlugins = await materializePluginEntries(resolvePluginEntries(repoRoot, listDomainNames(repoRoot), []));\n  for (const entry of domainPlugins) {\n    const directory = path.join(entry.directory, SKILLS_DIRECTORY);\n    if (!fs.existsSync(directory)) continue;\n    const owner = path.basename(entry.directory);\n    const skills = collect(directory, 'plugins', owner, isActive, loaded, diagnostics, entry.skillDiscovery);\n    if (skills.length > 0) pluginOwners.push({ owner, skills });\n  }\n\n  const sharedDirectory = path.join(repoRoot, '.claude', SKILLS_DIRECTORY);\n  const sharedSkills = collect(sharedDirectory, 'default', SHARED_SKILLS_OWNER, isActive, loaded, diagnostics);\n  const defaultOwners = sharedSkills.length > 0 ? [{ owner: SHARED_SKILLS_OWNER, skills: sharedSkills }] : [];\n  const helpOwners = collectHelpSkills(options.helpSkills ?? [], loaded, diagnostics);\n\n  const groups: SkillGroup[] = [\n    { key: 'extensions', label: 'extensions', owners: sortOwners(extensionOwners) },\n    { key: 'help', label: 'Help', owners: helpOwners },\n    { key: 'plugins', label: 'plugins', owners: sortOwners(pluginOwners) },\n    { key: 'default', label: 'default', owners: defaultOwners },\n  ];\n\n  const countTokens = await counter();\n  const prompt = formatSkillsForPrompt(loaded);\n  const framingTokens = countTokens(formatSkillsForPrompt([]));\n  const marginal = new Map<string, number>();\n  for (const skill of loaded) {\n    marginal.set(skill.name, Math.max(0, countTokens(formatSkillsForPrompt([skill])) - framingTokens));\n  }\n  for (const group of groups) {\n    for (const owner of group.owners) {\n      for (const skill of owner.skills) {\n        const tokens = marginal.get(skill.name);\n        if (tokens !== undefined) skill.promptTokens = tokens;\n      }\n    }\n  }\n  let bodyTokens = 0;\n  for (const skill of loaded) {\n    try {\n      bodyTokens += countTokens(fs.readFileSync(skill.filePath, 'utf8'));\n    } catch (error) {\n      diagnostics.push(`${skill.filePath}: ${error instanceof Error ? error.message : String(error)}`);\n    }\n  }\n\n  return {\n    groups,\n    skillCount: loaded.length,\n    promptTokens: prompt ? countTokens(prompt) : 0,\n    bodyTokens,\n    diagnostics,\n  };\n}\n\n/** The skills a name resolves to, used to decide what `enter` can invoke. */\nexport function findSkill(catalog: SkillCatalog, name: string): SkillEntry | undefined {\n  for (const group of catalog.groups) {\n    for (const owner of group.owners) {\n      const match = owner.skills.find((skill) => skill.name === name);\n      if (match) return match;\n    }\n  }\n  return undefined;\n}\n"],"mappings":"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"}