import type { DocBridgeConfigV1 } from '../config/schema.js' import { renderNamedTemplate } from '../render/template-source.js' import type { KnowledgeEntry } from '../schemas/doc-bridge-index.js' import { isProjectedEntry } from './project-corpus.js' const routePath = (path: string, pathPrefix: string | undefined): string => { const normalizedPath = path.replaceAll('\\', '/').replace(/\.(?:md|mdx)$/, '') const normalizedPrefix = pathPrefix?.replaceAll('\\', '/').replace(/\/$/, '') const relativePath = normalizedPrefix && normalizedPath.startsWith(`${normalizedPrefix}/`) ? normalizedPath.slice(normalizedPrefix.length + 1) : normalizedPath return relativePath.replace(/^\/+/, '') } export const knowledgeUrl = ( path: string, options: { readonly urlPrefix?: string | undefined; readonly pathPrefix?: string | undefined } = {}, ): string => { const relativePath = routePath(path, options.pathPrefix) if (!options.urlPrefix) return path const base = options.urlPrefix.endsWith('/') ? options.urlPrefix : `${options.urlPrefix}/` return new URL(relativePath, base).toString() } export type LlmsTxtEntry = { readonly title: string readonly url: string readonly description?: string } export type LlmsTxtVariables = { readonly project: string readonly preamble: string readonly entries: readonly LlmsTxtEntry[] } /** What the `llms.txt` template sees. Exported so a project overriding the template knows the variables. */ export const llmsTxtVariables = ( config: DocBridgeConfigV1, knowledge: readonly KnowledgeEntry[], projectName: string, ): LlmsTxtVariables => { const preamble = config.index?.llmsTxt?.preamble ?? `# ${projectName}\n\n> Agent-readable documentation index generated by ak-docs (@agentskit/doc-bridge).\n` /* * llms.txt is a reading order, not an inventory. The index also carries every document and * module the repository projects into it for retrieval; listing those here would bury the * curated entry points an agent is meant to start from. The filter lives in the renderer so * every caller — the builder and the conformance profile that re-renders to check freshness — * agrees on what the file contains. */ const entries = knowledge .filter((entry) => !isProjectedEntry(entry)) .slice(0, 500) .map((entry) => ({ title: entry.title, url: knowledgeUrl(entry.path, config.index?.llmsTxt), ...(entry.description ? { description: entry.description } : {}), })) return { project: projectName, preamble: preamble.trim(), entries } } /** * Render `llms.txt` from the bundled template, or the project's under `render.templates`. * * `root` resolves a project template path; the builder and the conformance profile both pass * theirs, so the file the builder writes is the file the profile expects. The bundled template * reproduces the previous concatenation byte for byte, which the llms and federation tests hold * it to. */ export const renderLlmsTxt = ( config: DocBridgeConfigV1, knowledge: readonly KnowledgeEntry[], projectName: string, options: { readonly root?: string } = {}, ): string => renderNamedTemplate('llms.txt', llmsTxtVariables(config, knowledge, projectName), config, options.root)