From 328ea9c3d694c2dc180806b75aaed7350d64f993 Mon Sep 17 00:00:00 2001 From: EM Date: Wed, 23 Sep 2026 19:27:59 +0800 Subject: [PATCH 1/4] feat: publish agent-friendly documentation surfaces --- docs/api/README.mdx | 1 + docusaurus.config.ts | 21 ++++++++++++++ package.json | 5 +++- pnpm-lock.yaml | 31 +++++++++++++++++++++ scripts/check-agent-docs.mjs | 54 ++++++++++++++++++++++++++++++++++++ src/pages/index.tsx | 4 +++ 6 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 scripts/check-agent-docs.mjs diff --git a/docs/api/README.mdx b/docs/api/README.mdx index 66b7c8f..e5eab21 100644 --- a/docs/api/README.mdx +++ b/docs/api/README.mdx @@ -26,6 +26,7 @@ export const SpecLinks = () => { - New to the API? **[Quickstart](./quickstart)** — mint a key, make your first call, round-trip a resource. ~10 minutes. - Already know the shape? **[Interactive reference](/api)** — every endpoint, rendered from the OpenAPI spec that ships with each release. - Generating a client? Point your generator at . The same URLs feed the [API client guide](./postman) (Postman, Insomnia, Bruno, Hoppscotch). +- Static fallback for Markdown consumers: production specs are `https://app.trakrf.id/api/openapi.json` and `https://app.trakrf.id/api/openapi.yaml`; preview deployments use the matching `app.preview.trakrf.id` host. ## Concept guides diff --git a/docusaurus.config.ts b/docusaurus.config.ts index d2ad0b3..8ac6bab 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -62,6 +62,27 @@ const config: Config = { ], }, ], + [ + "docusaurus-plugin-llms", + { + generateLLMsTxt: true, + generateLLMsFullTxt: true, + generateMarkdownFiles: true, + preserveDirectoryStructure: true, + addMdExtension: true, + excludeImports: true, + removeDuplicateHeadings: true, + customLLMFiles: [ + { + filename: "llms-api.txt", + includePatterns: ["api/**/*.md", "api/**/*.mdx"], + fullContent: true, + title: "TrakRF API Documentation", + description: "Integrator-focused API reference and conventions", + }, + ], + }, + ], ], presets: [ diff --git a/package.json b/package.json index 53a343f..08aae1e 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "swizzle": "docusaurus swizzle", "write-translations": "docusaurus write-translations", "write-heading-ids": "docusaurus write-heading-ids", - "check:contributing": "node scripts/check-contributing.mjs" + "check:contributing": "node scripts/check-contributing.mjs", + "check:agent-docs": "node scripts/check-agent-docs.mjs" }, "dependencies": { "@docusaurus/core": "3.9.2", @@ -32,6 +33,8 @@ "@docusaurus/module-type-aliases": "3.9.2", "@docusaurus/tsconfig": "3.9.2", "@docusaurus/types": "3.9.2", + "docusaurus-plugin-llms": "0.6.0", + "docusaurus-plugin-llms": "0.6.0", "prettier": "^3.0.0", "typescript": "~5.6.2" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7600c8f..4f6f2ef 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -48,6 +48,9 @@ importers: '@docusaurus/types': specifier: 3.9.2 version: 3.9.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + docusaurus-plugin-llms: + specifier: 0.6.0 + version: 0.6.0(@docusaurus/core@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.6.3)) prettier: specifier: ^3.0.0 version: 3.8.1 @@ -2522,6 +2525,12 @@ packages: resolution: {integrity: sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==} engines: {node: '>=6'} + docusaurus-plugin-llms@0.6.0: + resolution: {integrity: sha512-v9I6QEJLujkmtKjfWZ/SniWu7oOGZd0JUkyxO5MaFU09+qZHjQuraYuJZ+2AoZdesrrcktgDmZtqPL/sJtHe6A==} + engines: {node: '>=18.0'} + peerDependencies: + '@docusaurus/core': ^3.0.0 + docusaurus-plugin-redoc@2.5.0: resolution: {integrity: sha512-44sDhuXvItHnUuPdKswF3cRhiN5UW3YZxmMBsQLSfCYKcYr9tgWF2qvDfQoZO9i1DwpaYbIZ/RKMrSgny/iWYA==} engines: {node: '>=18'} @@ -3716,6 +3725,10 @@ packages: resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} engines: {node: '>=10'} + minimatch@9.0.3: + resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} + engines: {node: '>=16 || 14 >=14.17'} + minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -5442,6 +5455,11 @@ packages: resolution: {integrity: sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==} engines: {node: '>= 6'} + yaml@2.8.1: + resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} + engines: {node: '>= 14.6'} + hasBin: true + yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} @@ -9012,6 +9030,13 @@ snapshots: dependencies: '@leichtgewicht/ip-codec': 2.0.5 + docusaurus-plugin-llms@0.6.0(@docusaurus/core@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.6.3)): + dependencies: + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.6.3) + gray-matter: 4.0.3 + minimatch: 9.0.3 + yaml: 2.8.1 + docusaurus-plugin-redoc@2.5.0(@docusaurus/utils@3.10.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(core-js@3.48.0)(mobx@6.15.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(styled-components@6.4.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)): dependencies: '@docusaurus/utils': 3.10.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -10581,6 +10606,10 @@ snapshots: dependencies: brace-expansion: 2.1.0 + minimatch@9.0.3: + dependencies: + brace-expansion: 2.1.0 + minimist@1.2.8: {} mobx-react-lite@4.1.1(mobx@6.15.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): @@ -12553,6 +12582,8 @@ snapshots: yaml@1.10.3: {} + yaml@2.8.1: {} + yargs-parser@21.1.1: {} yargs@17.7.2: diff --git a/scripts/check-agent-docs.mjs b/scripts/check-agent-docs.mjs new file mode 100644 index 0000000..75abcae --- /dev/null +++ b/scripts/check-agent-docs.mjs @@ -0,0 +1,54 @@ +import { readFile } from "node:fs/promises"; +import { resolve } from "node:path"; + +const buildDir = resolve("build"); +const requiredFiles = ["llms.txt", "llms-full.txt", "llms-api.txt"]; +const markdownFiles = [ + "docs/api/authentication.md", + "docs/api/errors.md", + "docs/api/design-notes.md", + "docs/api/quickstart.md", +]; + +async function readBuildFile(file) { + try { + return await readFile(resolve(buildDir, file), "utf8"); + } catch (error) { + throw new Error(`Missing build/${file}: ${error.message}`); + } +} + +function assert(condition, message) { + if (!condition) throw new Error(message); +} + +const files = new Map( + await Promise.all( + [...requiredFiles, ...markdownFiles].map(async (file) => [ + file, + await readBuildFile(file), + ]), + ), +); + +const llms = files.get("llms.txt"); +const api = files.get("llms-api.txt"); + +assert( + /\[[^\]]+\]\([^\s)]+\.md\)/.test(llms), + "llms.txt does not link to Markdown mirrors", +); +assert(api.includes("TrakRF API"), "llms-api.txt does not contain API content"); +assert( + !/\/docs\/(?:user-guide|app-tour)\//.test(api), + "llms-api.txt includes user-guide or app-tour content", +); + +for (const file of markdownFiles) { + assert( + !/^\s*

{siteConfig.tagline}

+

+ Coding with an agent? Browse our machine-readable documentation at{" "} + /llms.txt. +

Date: Wed, 23 Sep 2026 19:51:58 +0800 Subject: [PATCH 2/4] fix: remove duplicate llms plugin dependency --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 08aae1e..740b74a 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,6 @@ "@docusaurus/tsconfig": "3.9.2", "@docusaurus/types": "3.9.2", "docusaurus-plugin-llms": "0.6.0", - "docusaurus-plugin-llms": "0.6.0", "prettier": "^3.0.0", "typescript": "~5.6.2" }, From 6f919cc73e7e069bcc38a41d267797016ca16f2b Mon Sep 17 00:00:00 2001 From: Mike Stankavich Date: Wed, 4 Nov 2026 06:48:37 -0600 Subject: [PATCH 3/4] feat: link llms-full.txt and llms-api.txt from llms.txt and the home page An agent only found llms-full.txt if it already knew the convention: llms.txt and the home page linked llms.txt alone. llms.txt now opens with links to both single-file variants and steers agents to per-page .md as the reliable route. Refs: TRA-862 Co-Authored-By: Claude Opus 5.5 --- docusaurus.config.ts | 6 ++++++ src/pages/index.tsx | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 8ac6bab..dab3349 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -72,6 +72,12 @@ const config: Config = { addMdExtension: true, excludeImports: true, removeDuplicateHeadings: true, + rootContent: [ + "This file links every documentation page to its Markdown version. Fetching a single page's `.md` is the most reliable way for an agent to read it.", + "", + `- [Full documentation in one file](${docsHost}/llms-full.txt): every page's content. Large; older changelog entries are linked rather than included.`, + `- [API documentation in one file](${docsHost}/llms-api.txt): the API concept guides only.`, + ].join("\n"), customLLMFiles: [ { filename: "llms-api.txt", diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 428ed95..5e75a34 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -20,7 +20,8 @@ function HomepageHeader() {

{siteConfig.tagline}

Coding with an agent? Browse our machine-readable documentation at{" "} - /llms.txt. + /llms.txt, or load it all at once from{" "} + /llms-full.txt.

Date: Wed, 4 Nov 2026 06:48:37 -0600 Subject: [PATCH 4/4] feat: trim the changelog in llms-full.txt and llms-api.txt to the newest entries The 150 KB changelog is mostly pre-launch history. The single-file agent docs keep its intro and newest 8 entries and link to the full changelog .md, which stays complete. check-agent-docs now runs in the build and asserts the links, the trim, and the untouched mirror. Refs: TRA-862 Co-Authored-By: Claude Opus 5.5 --- package.json | 2 +- scripts/check-agent-docs.mjs | 29 ++++++++++++++++++++++ scripts/trim-llms-changelog.mjs | 43 +++++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 scripts/trim-llms-changelog.mjs diff --git a/package.json b/package.json index 740b74a..47fb5a8 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "scripts": { "dev": "docusaurus start --host 0.0.0.0", "prebuild": "node scripts/write-health-json.mjs", - "build": "docusaurus build", + "build": "docusaurus build && node scripts/trim-llms-changelog.mjs && node scripts/check-agent-docs.mjs", "serve": "docusaurus serve --host 0.0.0.0", "typecheck": "tsc", "lint": "prettier --check . && node scripts/check-contributing.mjs", diff --git a/scripts/check-agent-docs.mjs b/scripts/check-agent-docs.mjs index 75abcae..4c59d95 100644 --- a/scripts/check-agent-docs.mjs +++ b/scripts/check-agent-docs.mjs @@ -51,4 +51,33 @@ for (const file of markdownFiles) { ); } +// llms-full.txt is only found by agents that already know the convention +// unless the index points to it. +for (const name of ["llms-full.txt", "llms-api.txt"]) { + assert(llms.includes(`/${name}`), `llms.txt does not link to ${name}`); +} + +// The changelog is trimmed in the full-content files, never in its own mirror. +const changelog = await readBuildFile("docs/api/changelog.md"); +const sourceChangelog = await readFile( + resolve("docs/api/changelog.md"), + "utf8", +); +const lastSourceEntry = [...sourceChangelog.matchAll(/^### (.+)$/gm)].at(-1)[1]; +assert( + changelog.includes(lastSourceEntry.replace(/\s*\{#[^}]+\}$/, "")), + "build/docs/api/changelog.md is missing older entries; the mirror must stay complete", +); +for (const name of ["llms-full.txt", "llms-api.txt"]) { + const text = files.get(name); + assert( + !text.includes(lastSourceEntry.replace(/\s*\{#[^}]+\}$/, "")), + `${name} still carries the full changelog history`, + ); + assert( + /Older changelog entries:.*\/docs\/api\/changelog\.md/.test(text), + `${name} does not point to the full changelog`, + ); +} + console.log("Agent documentation build output is complete and API-scoped."); diff --git a/scripts/trim-llms-changelog.mjs b/scripts/trim-llms-changelog.mjs new file mode 100644 index 0000000..b63c7f9 --- /dev/null +++ b/scripts/trim-llms-changelog.mjs @@ -0,0 +1,43 @@ +#!/usr/bin/env node +// Trims the API changelog inside the single-file agent docs after build. +// +// The changelog is the largest page on the site, and almost all of it is +// pre-launch history. Agents loading llms-full.txt or llms-api.txt get the +// newest entries and a link to the full changelog; the per-page Markdown +// mirror (build/docs/api/changelog.md) is left complete. The llms plugin has +// no per-page trimming, hence a post-build step. + +import { readFile, writeFile } from "node:fs/promises"; +import { resolve } from "node:path"; + +const KEEP_ENTRIES = 8; +const buildDir = resolve("build"); +const targets = ["llms-full.txt", "llms-api.txt"]; + +const index = await readFile(resolve(buildDir, "llms.txt"), "utf8"); +const changelogUrl = index.match(/\]\((\S+\/docs\/api\/changelog\.md)\)/)?.[1]; +if (!changelogUrl) throw new Error("llms.txt has no link to the changelog"); + +for (const name of targets) { + const path = resolve(buildDir, name); + const text = await readFile(path, "utf8"); + const start = text.search(/^## Changelog$/m); + if (start === -1) throw new Error(`${name} has no "## Changelog" section`); + const sepAt = text.indexOf("\n---\n", start); + const end = sepAt === -1 ? text.length : sepAt; + const section = text.slice(start, end); + + // Entry headings are level 3 and deeper inside the section, newest first. + const headings = [...section.matchAll(/^#{3,} .*$/gm)]; + if (headings.length <= KEEP_ENTRIES) continue; + const cut = headings[KEEP_ENTRIES].index; + const dropped = headings.length - KEEP_ENTRIES; + const trimmed = + section.slice(0, cut).trimEnd() + + `\n\nOlder changelog entries: ${changelogUrl} (${dropped} more entries).\n`; + + await writeFile(path, text.slice(0, start) + trimmed + text.slice(end)); + console.log( + `${name}: changelog trimmed to ${KEEP_ENTRIES} entries (${dropped} linked).`, + ); +}