diff --git a/znai-cli/src/main/java/org/testingisdocumenting/znai/cli/ZnaiCliApp.java b/znai-cli/src/main/java/org/testingisdocumenting/znai/cli/ZnaiCliApp.java index fe4529b14..267b3fe8f 100644 --- a/znai-cli/src/main/java/org/testingisdocumenting/znai/cli/ZnaiCliApp.java +++ b/znai-cli/src/main/java/org/testingisdocumenting/znai/cli/ZnaiCliApp.java @@ -92,6 +92,7 @@ private void start() { if (needsDocGeneration()) { webSite = generateDocs(config.getSourceRoot()); + exportLlmTxtIfNeeded(); } if (config.isPreviewMode()) { @@ -150,6 +151,21 @@ public void export() { FileUtils.writeTextContent(lookupPath, artifactsDirName); } + private void exportLlmTxtIfNeeded() { + Path llmTxtOutputPath = config.getLlmTxtOutputPath(); + if (llmTxtOutputPath == null) { + return; + } + + Path fullOutputPath = llmTxtOutputPath.isAbsolute() ? + llmTxtOutputPath : + config.getSourceRoot().resolve(llmTxtOutputPath); + + ProgressReporter.reportPhase("exporting llm.txt"); + Path llmTxtSource = deployPath.resolve("llm.txt"); + copyFile(llmTxtSource, fullOutputPath); + } + private static void copyFile(Path source, Path target) { ConsoleOutputs.out("copy ", Color.PURPLE, source, Color.YELLOW, " to ", Color.PURPLE, target); FileUtils.copyFile(source, target); diff --git a/znai-cli/src/main/java/org/testingisdocumenting/znai/cli/ZnaiCliConfig.java b/znai-cli/src/main/java/org/testingisdocumenting/znai/cli/ZnaiCliConfig.java index c566c234b..0fe89cf0b 100644 --- a/znai-cli/src/main/java/org/testingisdocumenting/znai/cli/ZnaiCliConfig.java +++ b/znai-cli/src/main/java/org/testingisdocumenting/znai/cli/ZnaiCliConfig.java @@ -91,6 +91,7 @@ public enum OptionKey { DEPLOY("deploy", "documentation deploy root dir", true, false), DOC_ID("doc-id", "documentation id", true, false), + LLM_TXT_OUTPUT_PATH("llm-txt-output-path", "save llm.txt to specified path during build", true, false), EXPORT("export", "export destination directory", true, false), @@ -153,6 +154,7 @@ public boolean hasMultipleArgs() { Set buildOptions = EnumSet.copyOf(commonOptions); buildOptions.add(OptionKey.DOC_ID); buildOptions.add(OptionKey.DEPLOY); + buildOptions.add(OptionKey.LLM_TXT_OUTPUT_PATH); COMMAND_OPTIONS.put(Command.BUILD, buildOptions); Set exportOptions = EnumSet.copyOf(commonOptions); @@ -227,6 +229,7 @@ public enum ModifiedTimeStrategy { private List specifiedCustomCommands; private String actor; private List lookupPaths; + private Path llmTxtOutputPath; private boolean isValidateExternalLinks; @@ -335,6 +338,10 @@ public ModifiedTimeStrategy getModifiedTimeStrategy() { return modifiedTimeStrategy; } + public Path getLlmTxtOutputPath() { + return llmTxtOutputPath; + } + public Path getDeployRoot() { return validateIsSet("deployRoot", deployRoot); } @@ -342,21 +349,19 @@ public Path getDeployRoot() { public void print() { printVersion(); - if (!isScaffoldMode() && !isServeMode()) { - print("source root", sourceRoot); - } - if (isPreviewMode() || isServeMode()) { + print("source root", sourceRoot); print("deploy root", deployRoot); print(" host", host); print(" port", port); - } - - if (isGenerateOnlyMode()) { - print(" doc id", docId); - } - - if (isExportMode()) { + } else if (isGenerateOnlyMode()) { + print(" source root", sourceRoot); + print(" doc id", docId); + if (getLlmTxtOutputPath() != null) { + print("llm txt output path", llmTxtOutputPath); + } + } else if (isExportMode()) { + print("source root", sourceRoot); print("export root", exportRoot); } } @@ -435,6 +440,10 @@ private void parseArgs(String[] args) { Arrays.asList(commandLine.getOptionValues(OptionKey.LOOKUP_PATHS.getKey())): Collections.emptyList(); + llmTxtOutputPath = commandLine.hasOption(OptionKey.LLM_TXT_OUTPUT_PATH.getKey()) ? + Paths.get(commandLine.getOptionValue(OptionKey.LLM_TXT_OUTPUT_PATH.getKey())).toAbsolutePath() : + null; + modifiedTimeStrategy = determineModifiedTimeStrategy(commandLine); validateMode(commandLine); diff --git a/znai-cli/src/test/groovy/org/testingisdocumenting/znai/cli/ZnaiCliConfigTest.groovy b/znai-cli/src/test/groovy/org/testingisdocumenting/znai/cli/ZnaiCliConfigTest.groovy index be0742758..f9065d26e 100644 --- a/znai-cli/src/test/groovy/org/testingisdocumenting/znai/cli/ZnaiCliConfigTest.groovy +++ b/znai-cli/src/test/groovy/org/testingisdocumenting/znai/cli/ZnaiCliConfigTest.groovy @@ -97,7 +97,8 @@ class ZnaiCliConfigTest { OptionKey.VALIDATE_EXTERNAL_LINKS, OptionKey.ACTOR, OptionKey.MODIFIED_TIME, - OptionKey.LOOKUP_PATHS + OptionKey.LOOKUP_PATHS, + OptionKey.LLM_TXT_OUTPUT_PATH ) } diff --git a/znai-docs/pre-filter/maven-plugin-llm.xml b/znai-docs/pre-filter/maven-plugin-llm.xml new file mode 100644 index 000000000..f75d8323b --- /dev/null +++ b/znai-docs/pre-filter/maven-plugin-llm.xml @@ -0,0 +1,8 @@ + + org.testingisdocumenting.znai + znai-maven-plugin + ${project.version} + + \${project.basedir}/full-guide.md + + \ No newline at end of file diff --git a/znai-docs/znai/flow/llm.md b/znai-docs/znai/flow/llm.md index 21b7db854..3028550e2 100644 --- a/znai-docs/znai/flow/llm.md +++ b/znai-docs/znai/flow/llm.md @@ -4,4 +4,16 @@ Note: Initial experimental support Znai generates a single markdown file called `llm.txt` to be used by LLMs. It is deployed at the root of the documentation and can be used in multiple scenarios. -The primary use case is to provide guide context to systems like Claude Code. \ No newline at end of file +The primary use case is to provide guide context to systems like Claude Code. + +# Generate During Build To Keep In Repository + +`````tabs +CLI: +```cli {highlight: "llm-txt"} +znai build --llm-txt-output-path full-guide.md +``` + +Maven: :include-file: maven-plugin-llm.xml {title: "expose llm.txt to source tree", highlight: "llmTxt"} +````` + diff --git a/znai-docs/znai/release-notes/1.83/add-2025-11-08-expose-llm-txt.md b/znai-docs/znai/release-notes/1.83/add-2025-11-08-expose-llm-txt.md new file mode 100644 index 000000000..1f37690e9 --- /dev/null +++ b/znai-docs/znai/release-notes/1.83/add-2025-11-08-expose-llm-txt.md @@ -0,0 +1 @@ +* Add: Option to expose llm.txt during build to be pushed to repo \ No newline at end of file diff --git a/znai-maven-plugin/src/main/groovy/org/testingisdocumenting/znai/maven/ZnaiMavenBuildRunner.groovy b/znai-maven-plugin/src/main/groovy/org/testingisdocumenting/znai/maven/ZnaiMavenBuildRunner.groovy index 526b7f728..6714e95ef 100644 --- a/znai-maven-plugin/src/main/groovy/org/testingisdocumenting/znai/maven/ZnaiMavenBuildRunner.groovy +++ b/znai-maven-plugin/src/main/groovy/org/testingisdocumenting/znai/maven/ZnaiMavenBuildRunner.groovy @@ -38,12 +38,18 @@ class ZnaiMavenBuildRunner extends AbstractMojo { @Parameter(defaultValue = '${project.build.directory}') private String deployRoot + @Parameter + private String llmTxtOutputPath + @Override void execute() throws MojoExecutionException, MojoFailureException { + def extraArgs = llmTxtOutputPath ? + ['llm-txt-output-path': llmTxtOutputPath] : [:] ZnaiMavenRunner.run(new MavenPluginConsoleOuput(getLog()), ["build"], [ 'doc-id': docId, source : sourceRoot, deploy : deployRoot, + *: extraArgs ]) } } diff --git a/znai-website-gen/src/main/java/org/testingisdocumenting/znai/website/LlmContentGenerator.java b/znai-website-gen/src/main/java/org/testingisdocumenting/znai/website/LlmContentGenerator.java index 05c334d36..4395561a6 100644 --- a/znai-website-gen/src/main/java/org/testingisdocumenting/znai/website/LlmContentGenerator.java +++ b/znai-website-gen/src/main/java/org/testingisdocumenting/znai/website/LlmContentGenerator.java @@ -43,6 +43,7 @@ public LlmContentGenerator(DocMeta docMeta, public String generateContent() { StringBuilder llmContent = new StringBuilder(); + llmContent.append("[//]: # (this is an auto generated file)\n"); llmContent.append("\"").append(docMeta.getTitle()).append("\" full guide:\n\n"); pageByTocItem.forEach((tocItem, page) -> { @@ -62,11 +63,11 @@ public String generateContent() { return; } - llmContent.append("# ").append(tocItem.getChapterTitle()).append(" :: ").append(tocItem.getPageTitle()); + var headerParts = Stream.of(tocItem.isIndex() ? "Index": "", tocItem.getChapterTitle(), tocItem.getPageTitle(), section.title()) + .filter(s -> !s.isEmpty()) + .collect(Collectors.joining(" :: ")); - if (!section.title().isEmpty()) { - llmContent.append(" :: ").append(section.title()); - } + llmContent.append("# ").append(headerParts); llmContent.append("\n"); String sectionUrl = section.title().isEmpty()