{"version":3,"file":"contextCatalog.mjs","names":[],"sources":["../../../src/builders/cli/contextCatalog.ts"],"sourcesContent":["import { resolvePluginEntries } from '@agimon-ai/doompi-config/domains';\nimport { getHarnessState, harnessRoot } from '@agimon-ai/doompi-config/harnessStore';\nimport type { PackageAttribution } from '@agimon-ai/doompi-config/types';\nimport { buildContextDetail } from '@agimon-ai/doompi-core/context-detail';\nimport { DOOM_CONTEXT_ENTRY_TYPE, projectContext } from '@agimon-ai/doompi-core/context-projection';\nimport { readDoomMcpStatus } from '@agimon-ai/doompi-core/mcp-status';\nimport { readDoomSkillSourcesService } from '@agimon-ai/doompi-core/skills';\nimport type { ContextItemDetail, ContextPromptStage } from '@agimon-ai/doompi-core/types-context-api';\nimport { buildSkillCatalog, counter, type SkillEntry } from '@agimon-ai/doompi-skill/catalog';\nimport { extensionName, extensionPackageName, extensionToolSource } from '@agimon-ai/doompi-ui/extensionName';\nimport { buildToolSources } from '@agimon-ai/doompi-ui/toolInventory';\nimport type { Context } from '@deepseek-ai/cordis';\nimport type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\n\n/**\n * Publishes what the session is composed of, and what it costs.\n *\n * Read at publish time rather than accumulated, because the answer changes for\n * reasons this module does not see: a reconnected MCP server, a domain switch,\n * a plan-mode tool swap. Reading the live inventory is cheap next to being\n * wrong about it.\n */\nexport interface ContextPublisher {\n  /** Never rejects: callers fire this and forget it. */\n  publish: () => Promise<void>;\n  dispose: () => void;\n}\n\n/** The active minor modes at publish time, read rather than remembered. */\nexport type ReadMinorModes = () => readonly { readonly id: string; readonly label: string }[];\n\nexport interface ContextPublisherOptions {\n  readMinorModes?: ReadMinorModes;\n  /**\n   * The session the detail file is keyed by, read at publish time.\n   *\n   * Undefined until a session is bound, and undefined in a host that has no\n   * session at all; the projection is still journaled either way, and only the\n   * click-through detail goes unwritten.\n   */\n  readSessionId?: () => string | undefined;\n  /**\n   * Where the click-through detail is left for the session API to find.\n   *\n   * Injected because writing it is filesystem work and this is not the layer\n   * that does filesystem work. A host that supplies neither still gets the\n   * projection; only the detail behind a row goes unpublished.\n   */\n  writeDetail?: (sessionId: string, revision: number, items: readonly ContextItemDetail[]) => void;\n  removeDetail?: (sessionId: string) => void;\n  /**\n   * The system prompt as the session currently stands.\n   *\n   * Read rather than pushed, for the same reason the tool inventory is: the\n   * prompt changes for reasons this module does not see. A host with no way to\n   * answer returns undefined and the panel reports no prompt rather than a\n   * stale one.\n   */\n  readSystemPrompt?: () => { readonly text: string; readonly stage: ContextPromptStage } | undefined;\n}\n\n/** Skills sit under owners, which is one level deeper than a flat list. */\nfunction flattenSkills(groups: Awaited<ReturnType<typeof buildSkillCatalog>>['groups']): SkillEntry[] {\n  return groups.flatMap((group) => group.owners.flatMap((owner) => owner.skills));\n}\n\n/**\n * Package and plugin names to the mode that admitted them.\n *\n * Two halves meet here. Layer packages come from the harness, recorded when the\n * composition resolved. Domain plugins are re-resolved, because only the plugin\n * entry knows the domain that carried it.\n */\nfunction attributionFor(repoRoot: string, domains: readonly string[], pluginDirectories: readonly string[]) {\n  const harness = getHarnessState();\n  const attribution: Record<string, PackageAttribution> = { ...harness.packageAttribution };\n  try {\n    for (const entry of resolvePluginEntries(repoRoot, [...domains], [...pluginDirectories])) {\n      if (entry.name && entry.domain) attribution[entry.name] = { kind: 'domain', mode: entry.domain };\n    }\n  } catch {\n    // A domain that no longer resolves must not cost the reader the tool list.\n    // Anything unmatched simply lands under core.\n  }\n  return attribution;\n}\n\nexport function createContextPublisher(\n  pi: ExtensionAPI,\n  cordis: Context,\n  options: ContextPublisherOptions = {},\n): ContextPublisher {\n  let published: string | undefined;\n  let revision = 0;\n  let disposed = false;\n  /** Removed on disposal, so a session leaves nothing behind in the store. */\n  let detailSessionId: string | undefined;\n\n  const build = async (): Promise<void> => {\n    if (disposed) return;\n    const harness = getHarnessState();\n    const mcpServers = readDoomMcpStatus(cordis)?.getSnapshot().servers;\n    const sources = buildToolSources({\n      tools: pi.getAllTools(),\n      activeTools: pi.getActiveTools(),\n      ...(mcpServers ? { mcpServers } : {}),\n      resolveExtensionName: extensionName,\n      resolveExtensionPackageName: extensionPackageName,\n      resolveExtensionToolSource: (toolName) => extensionToolSource(pi, toolName),\n    });\n\n    const repoRoot = harnessRoot(harness);\n    let skills: SkillEntry[] = [];\n    try {\n      const catalog = await buildSkillCatalog({\n        repoRoot,\n        activeSkillDirectories: harness.skillDirectories,\n        extensionSources: readDoomSkillSourcesService(cordis)?.list() ?? [],\n      });\n      skills = flattenSkills(catalog.groups);\n    } catch {\n      // Tools are the larger cost and are already in hand; a skill walk that\n      // fails should not take the whole figure down with it.\n    }\n\n    const countTokens = await counter();\n    const prompt = options.readSystemPrompt?.();\n    const promptCost = prompt === undefined ? undefined : { tokens: countTokens(prompt.text), stage: prompt.stage };\n    const projection = projectContext({\n      revision: revision + 1,\n      majorMode: harness.majorMode,\n      groups: (options.readMinorModes?.() ?? []).map((mode) => ({ ...mode, kind: 'minor' as const })),\n      domains: harness.domains,\n      sources,\n      skills,\n      attribution: attributionFor(repoRoot, harness.domains, harness.pluginDirectories),\n      countTokens,\n      ...(promptCost === undefined ? {} : { systemPrompt: promptCost }),\n    });\n\n    // Revision is compared out, so a republish that changed nothing is silent\n    // rather than a new journal entry saying the same thing. The prompt joins by\n    // its text: a reworded prompt of the same length is still a different answer.\n    const serialized = JSON.stringify({ ...projection, revision: 0, promptText: prompt?.text });\n    if (serialized === published || disposed) return;\n    published = serialized;\n    revision += 1;\n    // The detail lands before the entry that invites a reader to ask for it, so\n    // a click that follows the panel's update cannot outrun the file behind it.\n    const sessionId = options.readSessionId?.();\n    if (sessionId !== undefined && sessionId !== '' && options.writeDetail !== undefined) {\n      detailSessionId = sessionId;\n      options.writeDetail(\n        sessionId,\n        revision,\n        buildContextDetail({\n          sources,\n          skills,\n          countTokens,\n          ...(prompt === undefined || promptCost === undefined\n            ? {}\n            : { systemPrompt: { text: prompt.text, tokens: promptCost.tokens, stage: prompt.stage } }),\n        }),\n      );\n    }\n    pi.appendEntry(DOOM_CONTEXT_ENTRY_TYPE, { ...projection, revision });\n  };\n\n  /**\n   * Reporting the composition is a convenience, so it fails quietly.\n   *\n   * Callers publish without awaiting, and an escaping rejection would surface\n   * as an unhandled error in a live session. A panel that cannot say what the\n   * toolbox costs is a far smaller problem than that.\n   */\n  const publish = async (): Promise<void> => {\n    try {\n      await build();\n    } catch {\n      // Left unreported on purpose: the next composition change tries again.\n    }\n  };\n\n  return {\n    publish,\n    dispose: () => {\n      disposed = true;\n      if (detailSessionId !== undefined) options.removeDetail?.(detailSessionId);\n      detailSessionId = undefined;\n    },\n  };\n}\n"],"mappings":"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"}