Skip to content

Commit c1997f8

Browse files
committed
Add docs/llms-full.txt so agents can read the guides in one file
1 parent 55ae8dd commit c1997f8

9 files changed

Lines changed: 17970 additions & 27 deletions

File tree

‎.github/workflows/qa.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ jobs:
4343
- uses: actions/setup-node@v7
4444
with:
4545
node-version: 22.x
46-
- name: Check llms.txt
46+
- name: Check llms.txt and llms-full.txt
4747
run: node scripts/generate_llms_txt.js --check
4848
generated-app:
4949
runs-on: ubuntu-latest

‎AGENTS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ When adding or changing user-visible behavior:
126126
The only docs tree to edit by hand is `docs/docs/`.
127127

128128
- **User guides**: `docs/docs/` (Docusaurus, live preview via `cd docs && npm run start`)
129-
- **Agent index**: `docs/static/llms.txt` is generated from `docs/sidebars.js` and each page's front matter (`node scripts/generate_llms_txt.js`). It is published at <https://martenframework.com/docs/llms.txt> and lists the guides for the version under development (`/docs/next/`). Regenerate it when a guide is added, renamed, removed, or its description changes. `make qa` checks that the committed file matches.
129+
- **Agent guides**: `docs/static/llms.txt` and `docs/static/llms-full.txt` are generated from `docs/sidebars.js` and the guide pages (`node scripts/generate_llms_txt.js`). `llms.txt` is the index. `llms-full.txt` is the guide text. They are published at <https://martenframework.com/docs/llms.txt> and <https://martenframework.com/docs/llms-full.txt> for the version under development (`/docs/next/`). Regenerate them when a guide is added, renamed, removed, or its content changes. `make qa` checks that the committed files match.
130130
- **API reference**: generated with `make docs_api` → `docs/static/api/dev/`
131131
- **Snapshots**: `docs/versioned_docs/` and `docs/static/api/0.5`, `docs/static/api/0.6`, and `docs/static/api/0.7` are published copies. Leave them unchanged.
132132

‎Makefile‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,12 @@ typos:
7070
typos
7171

7272
.PHONY: llms_txt
73-
## Regenerate docs/static/llms.txt from the docs sidebar.
73+
## Regenerate docs/static/llms.txt and docs/static/llms-full.txt from the docs sidebar.
7474
llms_txt:
7575
node scripts/generate_llms_txt.js
7676

7777
.PHONY: llms_txt_check
78-
## Fail when docs/static/llms.txt does not match the docs sidebar.
78+
## Fail when docs/static/llms.txt or docs/static/llms-full.txt does not match the docs.
7979
llms_txt_check:
8080
node scripts/generate_llms_txt.js --check
8181

‎docs/docs/the-marten-project/release-notes/1.0.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ _Under development._
2828

2929
* The [`serve`](../../development/reference/management-commands.md#serve) management command now displays how long project compilation took after each successful build.
3030
* The [`new`](../../development/reference/management-commands.md#new) command now writes an `AGENTS.md` file when creating a project. The file records the Marten version, links to the matching guides and API, and describes the project layout, generators, requires, migrations, CSRF, and specs, so coding agents can work in the generated project. Projects created with `--edge` link to the in-development guides.
31+
* The in-development guides are published as a single file at [`/docs/llms-full.txt`](https://martenframework.com/docs/llms-full.txt), next to the guide index at [`/docs/llms.txt`](https://martenframework.com/docs/llms.txt).
3132
* The [`new`](../../development/reference/management-commands.md#new) command prints its usage and exits with status 1 when `type` or `name` is omitted and stdin is not a terminal, instead of waiting for prompts. Passing both arguments still runs without prompts.
3233

3334
## Backward incompatible changes

‎docs/static/llms-full.txt‎

Lines changed: 17780 additions & 0 deletions
Large diffs are not rendered by default.

‎docs/static/llms.txt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
> Marten is a batteries-included web framework for Crystal. This file indexes the guides for the version under development.
44

5-
These URLs are the in-development guides at https://martenframework.com/docs/next/. The latest stable guides are at https://martenframework.com/docs/. The generated API reference for this version is at https://martenframework.com/docs/api/dev/.
5+
The full text of these guides is at https://martenframework.com/docs/llms-full.txt. These URLs are the in-development guides at https://martenframework.com/docs/next/. The latest stable guides are at https://martenframework.com/docs/. The generated API reference for this version is at https://martenframework.com/docs/api/dev/.
66

77
## Prologue
88

‎scripts/generate_llms_txt.js‎

Lines changed: 182 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
#!/usr/bin/env node
22

3-
// Generates docs/static/llms.txt from docs/sidebars.js and each page's front matter.
3+
// Generates docs/static/llms.txt and docs/static/llms-full.txt from docs/sidebars.js
4+
// and the guide pages.
45
//
5-
// node scripts/generate_llms_txt.js # write the file
6-
// node scripts/generate_llms_txt.js --check # exit 1 when the file is stale
6+
// node scripts/generate_llms_txt.js # write the files
7+
// node scripts/generate_llms_txt.js --check # exit 1 when either file is stale
78

89
const {execFileSync} = require("child_process");
910
const fs = require("fs");
@@ -13,7 +14,9 @@ const path = require("path");
1314
const ROOT = path.join(__dirname, "..");
1415
const DOCS = path.join(ROOT, "docs", "docs");
1516
const OUTPUT = path.join(ROOT, "docs", "static", "llms.txt");
17+
const FULL_OUTPUT = path.join(ROOT, "docs", "static", "llms-full.txt");
1618
const NEXT_DOCS = "https://martenframework.com/docs/next";
19+
const DOCS_ORIGIN = "https://martenframework.com/docs";
1720
const RELEASE_NOTES_ID = "the-marten-project/release-notes";
1821

1922
const sidebar = require(path.join(ROOT, "docs", "sidebars.js")).sidebar;
@@ -51,9 +54,19 @@ function isUnderDevelopment(id) {
5154
return readPage(id).body.startsWith("_Under development._");
5255
}
5356

57+
function pageUrl(id) {
58+
let slug;
59+
try {
60+
slug = readPage(id).frontMatter.slug;
61+
} catch (_error) {
62+
return `${NEXT_DOCS}/${id}`;
63+
}
64+
return slug === "/" ? `${NEXT_DOCS}/` : `${NEXT_DOCS}/${id}`;
65+
}
66+
5467
function pageLink(id) {
5568
const page = readPage(id);
56-
const {title, description, slug} = page.frontMatter;
69+
const {title, description} = page.frontMatter;
5770

5871
if (!title) {
5972
throw new Error(`${path.relative(ROOT, page.file)} is missing a title`);
@@ -65,8 +78,123 @@ function pageLink(id) {
6578
);
6679
}
6780

68-
const url = slug === "/" ? `${NEXT_DOCS}/` : `${NEXT_DOCS}/${id}`;
69-
return `- [${title}](${url}): ${description}`;
81+
return `- [${title}](${pageUrl(id)}): ${description}`;
82+
}
83+
84+
function docCardsToMarkdown(body) {
85+
return body.replace(/<DocCard\s+item=\{\{([\s\S]*?)\}\}\s*\/>/g, (_match, attrs) => {
86+
const label = attrs.match(/label:\s*"([^"]+)"/);
87+
const docId = attrs.match(/docId:\s*"([^"]+)"/);
88+
if (!label || !docId) return "";
89+
return `- [${label[1]}](${pageUrl(docId[1])})`;
90+
});
91+
}
92+
93+
function stripJsxTags(text) {
94+
let result = "";
95+
let index = 0;
96+
97+
while (index < text.length) {
98+
const next = text[index + 1];
99+
if (text[index] === "<" && next && /[A-Za-z/!]/.test(next)) {
100+
let cursor = index + 1;
101+
let braces = 0;
102+
while (cursor < text.length) {
103+
const char = text[cursor];
104+
if (char === "{") braces += 1;
105+
else if (char === "}") braces = Math.max(0, braces - 1);
106+
else if (char === ">" && braces === 0) {
107+
cursor += 1;
108+
break;
109+
}
110+
cursor += 1;
111+
}
112+
index = cursor;
113+
continue;
114+
}
115+
116+
result += text[index];
117+
index += 1;
118+
}
119+
120+
return result;
121+
}
122+
123+
function rewriteTarget(target, id) {
124+
if (target.startsWith("pathname:///")) {
125+
return `${DOCS_ORIGIN}${target.slice("pathname://".length)}`;
126+
}
127+
if (/^[a-z][a-z0-9+.-]*:/i.test(target) || target.startsWith("#") || target.startsWith("//")) {
128+
return target;
129+
}
130+
if (/\s/.test(target)) return target;
131+
132+
const hashAt = target.indexOf("#");
133+
const hash = hashAt === -1 ? "" : target.slice(hashAt);
134+
const pathPart = hashAt === -1 ? target : target.slice(0, hashAt);
135+
if (!pathPart) return target;
136+
137+
const base = path.posix.dirname(id);
138+
const resolved = path.posix.normalize(path.posix.join(base === "." ? "" : base, pathPart));
139+
if (resolved.startsWith("..")) return target;
140+
141+
if (/\.mdx?$/.test(pathPart)) {
142+
return `${pageUrl(resolved.replace(/\.mdx?$/, ""))}${hash}`;
143+
}
144+
145+
if (!path.posix.extname(pathPart)) {
146+
try {
147+
readPage(resolved);
148+
} catch (_error) {
149+
return target;
150+
}
151+
return `${pageUrl(resolved)}${hash}`;
152+
}
153+
154+
const assetAt = resolved.indexOf("static/");
155+
if (assetAt !== -1 && /\.(png|jpe?g|gif|svg|webp)$/i.test(pathPart)) {
156+
return `${DOCS_ORIGIN}/${resolved.slice(assetAt + "static/".length)}${hash}`;
157+
}
158+
159+
return target;
160+
}
161+
162+
function rewriteLinks(segment, id) {
163+
const linkStart = /(!?)\[((?:[^\[\]]|\[[^\]]*\])*)\]\(/g;
164+
let result = "";
165+
let last = 0;
166+
167+
for (const match of segment.matchAll(linkStart)) {
168+
const open = match.index + match[0].length;
169+
let depth = 1;
170+
let cursor = open;
171+
while (cursor < segment.length && depth > 0) {
172+
if (segment[cursor] === "(") depth += 1;
173+
else if (segment[cursor] === ")") depth -= 1;
174+
if (depth > 0) cursor += 1;
175+
}
176+
if (depth !== 0) continue;
177+
178+
result += segment.slice(last, match.index);
179+
result += `${match[1]}[${match[2]}](${rewriteTarget(segment.slice(open, cursor).trim(), id)})`;
180+
last = cursor + 1;
181+
}
182+
183+
return result + segment.slice(last);
184+
}
185+
186+
function prepareBody(id) {
187+
const page = readPage(id);
188+
let body = page.body;
189+
190+
if (page.file.endsWith(".mdx")) {
191+
body = docCardsToMarkdown(body);
192+
body = body.replace(/^import .*$/gm, "");
193+
body = stripJsxTags(body);
194+
body = body.replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n");
195+
}
196+
197+
return rewriteLinks(body, id).trim();
70198
}
71199

72200
function addPage(sections, sectionName, id) {
@@ -111,7 +239,7 @@ function generate() {
111239
"",
112240
"> Marten is a batteries-included web framework for Crystal. This file indexes the guides for the version under development.",
113241
"",
114-
"These URLs are the in-development guides at https://martenframework.com/docs/next/. The latest stable guides are at https://martenframework.com/docs/. The generated API reference for this version is at https://martenframework.com/docs/api/dev/.",
242+
"The full text of these guides is at https://martenframework.com/docs/llms-full.txt. These URLs are the in-development guides at https://martenframework.com/docs/next/. The latest stable guides are at https://martenframework.com/docs/. The generated API reference for this version is at https://martenframework.com/docs/api/dev/.",
115243
"",
116244
];
117245

@@ -131,32 +259,65 @@ function generate() {
131259
return lines.join("\n");
132260
}
133261

262+
function generateFull() {
263+
const sections = [];
264+
walk(sidebar, null, sections);
265+
266+
const lines = [
267+
"# Marten",
268+
"",
269+
"> Marten is a batteries-included web framework for Crystal. This file contains the guides for the version under development.",
270+
"",
271+
"The guide index is at https://martenframework.com/docs/llms.txt. These guides are the in-development guides at https://martenframework.com/docs/next/. The latest stable guides are at https://martenframework.com/docs/. The generated API reference for this version is at https://martenframework.com/docs/api/dev/.",
272+
"",
273+
];
274+
275+
for (const section of sections) {
276+
for (const id of section.items) {
277+
const page = readPage(id);
278+
lines.push(`# ${page.frontMatter.title}`, "");
279+
lines.push(`Source: ${pageUrl(id)}`, "");
280+
lines.push(prepareBody(id), "");
281+
}
282+
}
283+
284+
return lines.join("\n");
285+
}
286+
287+
function checkFile(output, generated) {
288+
const current = fs.existsSync(output) ? fs.readFileSync(output, "utf8") : "";
289+
if (current === generated) return false;
290+
291+
const tmp = path.join(os.tmpdir(), `marten-${path.basename(output)}`);
292+
fs.writeFileSync(tmp, generated);
293+
try {
294+
execFileSync("diff", ["-u", output, tmp], {stdio: "inherit"});
295+
} catch (_error) {
296+
// diff exits with status 1 when the files differ.
297+
}
298+
console.error(`\n${path.relative(ROOT, output)} is out of date. Run: node scripts/generate_llms_txt.js`);
299+
return true;
300+
}
301+
134302
function main() {
135303
const check = process.argv.includes("--check");
136304
if (process.argv.slice(2).some(arg => arg !== "--check")) {
137305
console.error("Usage: node scripts/generate_llms_txt.js [--check]");
138306
process.exit(2);
139307
}
140308

141-
const generated = generate();
309+
const outputs = [
310+
{file: OUTPUT, content: generate()},
311+
{file: FULL_OUTPUT, content: generateFull()},
312+
];
142313

143314
if (!check) {
144-
fs.writeFileSync(OUTPUT, generated);
315+
for (const output of outputs) fs.writeFileSync(output.file, output.content);
145316
return;
146317
}
147318

148-
const current = fs.existsSync(OUTPUT) ? fs.readFileSync(OUTPUT, "utf8") : "";
149-
if (current === generated) return;
150-
151-
const tmp = path.join(os.tmpdir(), "marten-llms.txt");
152-
fs.writeFileSync(tmp, generated);
153-
try {
154-
execFileSync("diff", ["-u", OUTPUT, tmp], {stdio: "inherit"});
155-
} catch (_error) {
156-
// diff exits with status 1 when the files differ.
157-
}
158-
console.error("\ndocs/static/llms.txt is out of date. Run: node scripts/generate_llms_txt.js");
159-
process.exit(1);
319+
const stale = outputs.some(output => checkFile(output.file, output.content));
320+
if (stale) process.exit(1);
160321
}
161322

162323
main();

‎spec/marten/cli/manage/command/new_spec.cr‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,7 @@ describe Marten::CLI::Manage::Command::New do
294294
agents.should contain "https://martenframework.com/docs/next/"
295295
agents.should contain "https://martenframework.com/docs/api/dev/index.html"
296296
agents.should contain "https://martenframework.com/docs/llms.txt"
297+
agents.should contain "https://martenframework.com/docs/llms-full.txt"
297298
agents.should contain "https://martenframework.com/docs/next/getting-started/from-django"
298299
end
299300

‎src/marten/cli/manage/command/new/templates/project/AGENTS.md.ecr‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# Marten project guide for coding agents
33

44
<%- if context.edge -%>
5-
This project uses Marten <%= Marten::VERSION %> from the development branch. Follow the guides at <https://martenframework.com/docs/next/> and the API reference at <https://martenframework.com/docs/api/dev/index.html>. The guide index is at <https://martenframework.com/docs/llms.txt>.
5+
This project uses Marten <%= Marten::VERSION %> from the development branch. Follow the guides at <https://martenframework.com/docs/next/> and the API reference at <https://martenframework.com/docs/api/dev/index.html>. The guide index is at <https://martenframework.com/docs/llms.txt>. The full text of those guides is at <https://martenframework.com/docs/llms-full.txt>.
66
<%- else -%>
77
This project uses Marten <%= Marten::VERSION %>. Follow the guides at <https://martenframework.com/docs/> and the API reference at <https://martenframework.com/docs/api/<%= api_series %>/index.html>. These guide URLs track the latest stable release. The API URL is pinned to <%= api_series %>. When a guide and that API disagree, follow the API.
88
<%- end -%>

0 commit comments

Comments
 (0)