{"version":3,"file":"index.cjs","names":["readFile","path","readHarnessState","os","listDomainNames","materializePluginEntries","resolvePluginEntries","collectResources","discoverSkillsAsync","resolveSharedSkills","DeferredSkillLoader","formatSkillsForPrompt"],"sources":["../../../../src/services/serverInventory/index.ts"],"sourcesContent":["import { readFile } from 'node:fs/promises';\nimport os from 'node:os';\nimport path from 'node:path';\n\nimport {\n  listDomainNames,\n  resolvePluginEntries,\n  resolveSharedSkills,\n  type PluginEntry,\n} from '@agimon-ai/doompi-config/domains';\nimport { readHarnessState } from '@agimon-ai/doompi-config/harnessState';\nimport type { DoomHeadlessExecutionContext } from '@agimon-ai/doompi-core/headless';\nimport { materializePluginEntries } from '@agimon-ai/doompi-domain/plugins';\nimport { collectResources, discoverSkillsAsync } from '@agimon-ai/doompi-domain/resources';\nimport { formatSkillsForPrompt, type Skill } from '@earendil-works/pi-coding-agent';\n\nimport { DeferredSkillLoader, type DeferredSkillSnapshot } from '../deferredSkills';\n\n/** Skills and the domain whose selection activates them, or always-on when absent. */\nexport interface ServerSkillGroup {\n  readonly domain?: string;\n  readonly skills: readonly Skill[];\n}\n\nexport interface ServerSkillInventory {\n  /** Every skill any selection can reach, so an activated command can still expand. */\n  readonly inventory: DeferredSkillSnapshot;\n  readonly groups: readonly ServerSkillGroup[];\n  readonly mcpGroups: readonly ServerSkillGroup[];\n  readonly catalog: string;\n}\n\ninterface SkillSource {\n  readonly domain?: string;\n  readonly paths: readonly string[];\n  readonly mcpPaths: readonly string[];\n}\n\nconst LOCAL_ONLY_MCP_PACKAGES = new Set([\n  '@agimon-ai/doompi-author',\n  '@agimon-ai/doompi-git',\n  '@agimon-ai/doompi-goal',\n  '@agimon-ai/doompi-help',\n  '@agimon-ai/doompi-loop',\n  '@agimon-ai/doompi-user-feedback',\n  '@agimon-ai/doompi-voice',\n  '@agimon-ai/doompi-workflow',\n]);\n\nasync function isMcpSkillPlugin(entry: PluginEntry): Promise<boolean> {\n  if (entry.source?.type === 'npm') return !LOCAL_ONLY_MCP_PACKAGES.has(entry.source.package);\n  try {\n    const manifest = JSON.parse(await readFile(path.join(entry.directory, 'package.json'), 'utf8')) as unknown;\n    if (typeof manifest !== 'object' || manifest === null || Array.isArray(manifest)) return false;\n    const name = (manifest as { name?: unknown }).name;\n    return typeof name === 'string' && name.length > 0 && name.trim() === name && !LOCAL_ONLY_MCP_PACKAGES.has(name);\n  } catch (error) {\n    // A package manifest is optional for catalog plugins, but a present identity\n    // that cannot be read or parsed must never make the plugin remotely eligible.\n    return (error as NodeJS.ErrnoException).code === 'ENOENT';\n  }\n}\n\n/**\n * Whether a discovered skill came from one of these paths.\n *\n * A source holds exact SKILL.md files for plugin and shared skills, but a bare\n * directory for `.doom/skills`, so membership is containment rather than\n * equality.\n */\nfunction within(filePath: string, roots: readonly string[]): boolean {\n  const resolved = path.resolve(filePath);\n  return roots.some((root) => {\n    const base = path.resolve(root);\n    return resolved === base || resolved.startsWith(`${base}${path.sep}`);\n  });\n}\n/**\n * Every skill the session could reach, grouped by what activates it.\n *\n * A cockpit `/domains` switch reconciles contributions rather than rebuilding\n * the facet: the session declaration is mounted once. So enumerating only the\n * launch selection would freeze the command list at whatever was selected when\n * the session host was built. Enumerating every domain up front and tagging\n * each group with its `when` lets the existing kernel refresh do the work, the\n * same way domainServerResources registers per-domain skill resources.\n */\nexport async function discoverServerSkills(\n  execution: DoomHeadlessExecutionContext,\n  signal: AbortSignal,\n): Promise<ServerSkillInventory> {\n  signal.throwIfAborted();\n  const state = readHarnessState(execution.environment);\n  const home = execution.environment.HOME ?? os.homedir();\n  const repoSkills = path.join(execution.repoRoot, '.doom', 'skills');\n  const sources: SkillSource[] = [];\n  const diagnostics: string[] = [];\n  // A malformed domain is a configuration error in one domain, so it must not\n  // remove every other domain's skills. Enumerating the whole manifest is new\n  // exposure here: reading the launch selection could not fail this way.\n  const domains: string[] = [];\n  try {\n    domains.push(...listDomainNames(execution.repoRoot, home));\n  } catch (error) {\n    diagnostics.push(`Domain skills are unavailable: ${error instanceof Error ? error.message : String(error)}`);\n  }\n\n  // Domain-scoped plugin skills come first. The host keeps the first command\n  // registered for a name, so this ordering reproduces the collector's rule\n  // that a selected plugin outranks a shared fallback of the same name.\n  for (const domain of domains) {\n    signal.throwIfAborted();\n    try {\n      const plugins = await materializePluginEntries(resolvePluginEntries(execution.repoRoot, [domain], [], home));\n      const mcpPluginRoots = (\n        await Promise.all(\n          plugins.map(async (plugin) =>\n            (await isMcpSkillPlugin(plugin)) ? path.join(plugin.directory, 'skills') : undefined,\n          ),\n        )\n      ).filter((root): root is string => root !== undefined);\n      const collected = await collectResources(execution.repoRoot, plugins, {\n        agents: false,\n        mcp: false,\n        sharedSkills: false,\n      });\n      try {\n        if (collected.skillDirectories.length > 0) {\n          const paths = [...collected.skillDirectories];\n          sources.push({\n            domain,\n            paths,\n            // Exact collected paths retain this domain's per-plugin skill subset.\n            // A plugin root would also admit skills loaded for another domain.\n            mcpPaths: paths.filter((skillPath) => within(skillPath, mcpPluginRoots)),\n          });\n        }\n      } finally {\n        await collected.cleanup();\n      }\n    } catch (error) {\n      diagnostics.push(`Domain '${domain}': ${error instanceof Error ? error.message : String(error)}`);\n    }\n  }\n\n  // Shared .claude/skills are not owned by any one domain, and a domain can opt\n  // out of them. `when` cannot express \"unless every selected domain opts out\",\n  // but registering a copy per domain that keeps them says the same thing: the\n  // group is active if any such domain is selected, which is exactly what\n  // resolveSharedSkills computes.\n  signal.throwIfAborted();\n  const shared = await discoverSkillsAsync(execution.repoRoot, path.join(execution.repoRoot, '.claude', 'skills'));\n  if (shared.length > 0) {\n    const sharedPaths = shared.map((skill) => skill.path);\n    for (const domain of domains) {\n      if (resolveSharedSkills(execution.repoRoot, [domain], home)) {\n        sources.push({ domain, paths: sharedPaths, mcpPaths: sharedPaths });\n      }\n    }\n  }\n\n  // Repository-local skills belong to the repository, not to a selection.\n  sources.push({ paths: [repoSkills], mcpPaths: [repoSkills] });\n\n  signal.throwIfAborted();\n  // One walk for every group. Pi drops a path it has already loaded, so the\n  // loader gets the unique set and each group selects its own skills back out.\n  const unique = [...new Set([...sources.flatMap((source) => source.paths), ...(state.skillDirectories ?? [])])];\n  const inventory = await new DeferredSkillLoader({ cwd: execution.cwd, skillPaths: unique }).ready();\n  signal.throwIfAborted();\n\n  const groups = sources\n    .map((source) => ({\n      ...(source.domain === undefined ? {} : { domain: source.domain }),\n      skills: inventory.skills.filter((skill) => within(skill.filePath, source.paths)),\n    }))\n    .filter((group) => group.skills.length > 0);\n  const mcpGroups = sources\n    .map((source) => ({\n      ...(source.domain === undefined ? {} : { domain: source.domain }),\n      skills: inventory.skills.filter((skill) => within(skill.filePath, source.mcpPaths)),\n    }))\n    .filter((group) => group.skills.length > 0);\n\n  // The catalog still describes the active selection rather than everything on\n  // disk, because it is prompt text rather than a menu.\n  const activeSkills = inventory.skills.filter((skill) =>\n    within(skill.filePath, [...(state.skillDirectories ?? []), repoSkills]),\n  );\n  return {\n    inventory: { skills: inventory.skills, diagnostics: [...inventory.diagnostics, ...diagnostics] },\n    groups,\n    mcpGroups,\n    catalog: formatSkillsForPrompt(activeSkills) || '(no discovered skills)',\n  };\n}\n"],"mappings":"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"}