static-llms-txt 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +129 -2
- package/cli.cjs +67 -0
- package/index.cjs +356 -0
- package/index.d.ts +129 -0
- package/index.mjs +3 -0
- package/package.json +37 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 OdysseyX LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,130 @@
|
|
|
1
|
-
#
|
|
1
|
+
# static-llms-txt
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Write `llms.txt` and `llms-full.txt` for any static site, from the HTML it already built.
|
|
4
|
+
|
|
5
|
+
Most llms.txt generators are tied to one framework and read its Markdown sources. This one reads the output folder, so it works after Eleventy, Hugo, Jekyll, Astro, Gatsby, Docusaurus, MkDocs, a hand-written site, or anything else that ends in `.html` files.
|
|
6
|
+
|
|
7
|
+
- **`llms.txt`**: an index in the [llmstxt.org](https://llmstxt.org) shape. Title, summary, one link per page with its description.
|
|
8
|
+
- **`llms-full.txt`**: the main content of every page as Markdown in one file, each page under its own heading with a `Source:` link.
|
|
9
|
+
- **No network, no API key, no config file.** One dependency (`node-html-parser`).
|
|
10
|
+
- Skips what should not be there: navigation, footers, scripts, `noindex` pages, redirects, 404 pages.
|
|
11
|
+
- Leaves an `llms.txt` you wrote by hand alone.
|
|
12
|
+
|
|
13
|
+
## Use it from the command line
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npx static-llms-txt ./dist --site https://example.com
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Put it after your build:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "eleventy && static-llms-txt _site --site https://example.com"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
| Option | What it does |
|
|
30
|
+
| --- | --- |
|
|
31
|
+
| `--site <url>` | Public address of the site. Makes every link absolute. Without it, the page's canonical link is used. |
|
|
32
|
+
| `--title <text>` | Site title. Default: taken from the home page. |
|
|
33
|
+
| `--description <text>` | One-line summary. Default: the home page meta description. |
|
|
34
|
+
| `--content <selector>` | CSS selector of the main content. Default: `main`, then `article`, then `body`. |
|
|
35
|
+
| `--exclude <glob>` | Skip matching files, for example `"tags/**"`. Repeatable. |
|
|
36
|
+
| `--clean-urls` | Link `/about` instead of `/about.html`. |
|
|
37
|
+
| `--no-full` | Write `llms.txt` only. |
|
|
38
|
+
| `--force` | Overwrite an `llms.txt` this tool did not write. |
|
|
39
|
+
| `--ask-ai` | Also add an Ask AI box to every page. See below. |
|
|
40
|
+
|
|
41
|
+
## Use it from code
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
import { generate } from "static-llms-txt";
|
|
45
|
+
|
|
46
|
+
const result = generate("./dist", {
|
|
47
|
+
site: "https://example.com",
|
|
48
|
+
exclude: ["tags/**", "drafts/**"],
|
|
49
|
+
});
|
|
50
|
+
console.log(result.pages.length, "pages", result.written);
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
CommonJS works too: `const { generate } = require("static-llms-txt")`.
|
|
54
|
+
|
|
55
|
+
| Function | What it does |
|
|
56
|
+
| --- | --- |
|
|
57
|
+
| `generate(dir, options)` | Scan a folder and write both files into it. Returns `{ pages, llms, llmsFull, written, skipped }`. |
|
|
58
|
+
| `scan(dir, options)` | Read every page. Returns `[{ url, path, title, description, sections, text }]`. |
|
|
59
|
+
| `build(pages, options)` | Turn a list of pages (from `scan`, or your own `{ url, title, html }` objects) into the text of both files, without touching the disk. |
|
|
60
|
+
| `readHtml(html, options)` | One HTML document to title, description and Markdown content. |
|
|
61
|
+
| `inject(dir, options)` / `scriptTag(options)` | The optional Ask AI part. |
|
|
62
|
+
|
|
63
|
+
Options for `generate` and `scan`: `site`, `title`, `description`, `content`, `remove` (extra selectors to drop), `exclude` (globs), `cleanUrls`, `minLength` (default 40 characters), `full`, `force`, `out`, `write`. Types are included.
|
|
64
|
+
|
|
65
|
+
## What the output looks like
|
|
66
|
+
|
|
67
|
+
`llms.txt`
|
|
68
|
+
|
|
69
|
+
```md
|
|
70
|
+
# Acme Docs
|
|
71
|
+
|
|
72
|
+
> Acme makes anvils.
|
|
73
|
+
|
|
74
|
+
## Pages
|
|
75
|
+
|
|
76
|
+
- [Acme](https://acme.dev/): Acme makes anvils.
|
|
77
|
+
- [Guide](https://acme.dev/guide/): How to start.
|
|
78
|
+
|
|
79
|
+
## Optional
|
|
80
|
+
|
|
81
|
+
- [Full text of every page](https://acme.dev/llms-full.txt)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`llms-full.txt`
|
|
85
|
+
|
|
86
|
+
```md
|
|
87
|
+
# Guide
|
|
88
|
+
|
|
89
|
+
Source: https://acme.dev/guide/
|
|
90
|
+
|
|
91
|
+
Start here with the guide.
|
|
92
|
+
|
|
93
|
+
## Step one
|
|
94
|
+
|
|
95
|
+
Source: https://acme.dev/guide/#step-one
|
|
96
|
+
|
|
97
|
+
Unpack the anvil carefully.
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Optional: an Ask AI box that answers from llms-full.txt
|
|
101
|
+
|
|
102
|
+
Once the site has an `llms-full.txt`, a single script tag can answer visitor questions from it. `--ask-ai` adds that tag to every page:
|
|
103
|
+
|
|
104
|
+
```sh
|
|
105
|
+
npx static-llms-txt ./dist --site https://example.com --ask-ai --key YOUR_ONETAG_KEY
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The box is [OneTag AI](https://onetagai.com/?ref=static-llms-txt)'s open-source `ask-docs.js`. It finds the pages that match the question in `llms-full.txt`, sends them with the question to the OneTag AI API, and shows the answer with links to the pages it used. There is no backend to run.
|
|
109
|
+
|
|
110
|
+
The key is a site key. It is public by design, like an analytics ID, and it ends up in the built HTML. The free plan is 1,000,000 tokens a month with a hard cap: when it runs out the box says so, and no bill can appear. Get one without opening a browser:
|
|
111
|
+
|
|
112
|
+
```sh
|
|
113
|
+
curl -X POST https://onetagai.com/api/signup -H "Content-Type: application/json" -d '{"email":"you@example.com"}'
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Without `--key` the tool uses a demo key that answers on localhost only, so you can try it before signing up. Everything above this section works without OneTag AI.
|
|
117
|
+
|
|
118
|
+
## Framework plugins built on this
|
|
119
|
+
|
|
120
|
+
- `eleventy-plugin-onetag` for Eleventy
|
|
121
|
+
- `docusaurus-plugin-onetag` for Docusaurus
|
|
122
|
+
- `gatsby-plugin-onetag` for Gatsby
|
|
123
|
+
- `hexo-onetag` for Hexo
|
|
124
|
+
- `vitepress-plugin-onetag` for VitePress
|
|
125
|
+
- `nuxt-onetag` for Nuxt
|
|
126
|
+
- [`astro-onetag`](https://www.npmjs.com/package/astro-onetag) and [`starlight-onetag`](https://www.npmjs.com/package/starlight-onetag) for Astro
|
|
127
|
+
|
|
128
|
+
## License
|
|
129
|
+
|
|
130
|
+
MIT
|
package/cli.cjs
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
const path = require("node:path");
|
|
4
|
+
const { generate, inject } = require("./index.cjs");
|
|
5
|
+
|
|
6
|
+
const HELP = `static-llms-txt: write llms.txt and llms-full.txt from a built static site.
|
|
7
|
+
|
|
8
|
+
Usage
|
|
9
|
+
npx static-llms-txt <folder> [options]
|
|
10
|
+
|
|
11
|
+
Options
|
|
12
|
+
--site <url> Public address of the site, e.g. https://example.com. Used for absolute links.
|
|
13
|
+
--title <text> Site title. Default: taken from the home page.
|
|
14
|
+
--description <text> One-line summary. Default: the home page meta description.
|
|
15
|
+
--content <selector> CSS selector of the main content. Default: main, article, then body.
|
|
16
|
+
--exclude <glob> Skip matching files, e.g. "tags/**". Repeatable.
|
|
17
|
+
--clean-urls Link /about instead of /about.html.
|
|
18
|
+
--no-full Write llms.txt only.
|
|
19
|
+
--force Overwrite an llms.txt this tool did not write.
|
|
20
|
+
--ask-ai Also add an "Ask AI" box to every page (one script tag, answers from llms-full.txt).
|
|
21
|
+
--key <key> OneTag AI site key for --ask-ai. Free: https://onetagai.com/?ref=static-llms-txt
|
|
22
|
+
-h, --help Show this help.
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
function parseArgs(argv) {
|
|
26
|
+
const o = { exclude: [] };
|
|
27
|
+
const rest = [];
|
|
28
|
+
for (let i = 0; i < argv.length; i++) {
|
|
29
|
+
const a = argv[i];
|
|
30
|
+
const next = () => { if (i + 1 >= argv.length) throw new Error("Missing value after " + a); return argv[++i]; };
|
|
31
|
+
if (a === "-h" || a === "--help") o.help = true;
|
|
32
|
+
else if (a === "--site") o.site = next();
|
|
33
|
+
else if (a === "--title") o.title = next();
|
|
34
|
+
else if (a === "--description") o.description = next();
|
|
35
|
+
else if (a === "--content") o.content = next();
|
|
36
|
+
else if (a === "--exclude") o.exclude.push(next());
|
|
37
|
+
else if (a === "--clean-urls") o.cleanUrls = true;
|
|
38
|
+
else if (a === "--no-full") o.full = false;
|
|
39
|
+
else if (a === "--force") o.force = true;
|
|
40
|
+
else if (a === "--ask-ai") o.askAi = true;
|
|
41
|
+
else if (a === "--key") o.key = next();
|
|
42
|
+
else if (a.startsWith("-")) throw new Error("Unknown option " + a + ". Run with --help to see the options.");
|
|
43
|
+
else rest.push(a);
|
|
44
|
+
}
|
|
45
|
+
o.dir = rest[0];
|
|
46
|
+
return o;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function main() {
|
|
50
|
+
let o;
|
|
51
|
+
try { o = parseArgs(process.argv.slice(2)); } catch (e) { console.error(e.message); process.exit(2); }
|
|
52
|
+
if (o.help || !o.dir) { console.log(HELP); process.exit(o.help ? 0 : 2); }
|
|
53
|
+
let result;
|
|
54
|
+
try { result = generate(o.dir, o); } catch (e) { console.error(e.message); process.exit(1); }
|
|
55
|
+
const dir = path.resolve(o.dir);
|
|
56
|
+
for (const f of result.written) console.log("wrote " + path.join(dir, f));
|
|
57
|
+
for (const f of result.skipped) console.log("kept " + path.join(dir, f) + " (not written by this tool; pass --force to replace it)");
|
|
58
|
+
console.log(result.pages.length + " pages");
|
|
59
|
+
if (!o.site && result.pages.some((p) => !/^https?:/.test(p.url))) console.log("Tip: pass --site https://your-site.com so the links are absolute.");
|
|
60
|
+
if (o.askAi) {
|
|
61
|
+
const changed = inject(dir, { key: o.key || "ot_demo000000000000000000000", ref: "static-llms-txt", exclude: o.exclude });
|
|
62
|
+
console.log("added the Ask AI script to " + changed.length + " pages");
|
|
63
|
+
if (!o.key) console.log("No --key given: the demo key answers on localhost only. Get a free key:\n curl -X POST https://onetagai.com/api/signup -H \"Content-Type: application/json\" -d '{\"email\":\"you@example.com\"}'");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
main();
|
package/index.cjs
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
// static-llms-txt: write llms.txt and llms-full.txt for a static site from the HTML it already built.
|
|
2
|
+
// It reads a folder of .html files, keeps the main content of every page as Markdown, and writes
|
|
3
|
+
// llms.txt an index: one link per page (https://llmstxt.org)
|
|
4
|
+
// llms-full.txt every page in one file, one "# Title" + "Source: URL" block per page
|
|
5
|
+
// No network, no API key. The optional Ask AI part (askAi / inject) only adds a script tag.
|
|
6
|
+
"use strict";
|
|
7
|
+
const fs = require("node:fs");
|
|
8
|
+
const path = require("node:path");
|
|
9
|
+
const { parse } = require("node-html-parser");
|
|
10
|
+
|
|
11
|
+
const MARK = "<!-- generated by static-llms-txt -->";
|
|
12
|
+
const SCRIPT = "https://onetagai.com/ask-docs.js";
|
|
13
|
+
const DROP = "script,style,noscript,template,svg,canvas,iframe,object,embed,nav,footer,aside,form,button,select,dialog,[hidden],[aria-hidden=true],.sr-only,.visually-hidden";
|
|
14
|
+
const ROOTS = ["main", "[role=main]", "article", "#content", ".content", "body"];
|
|
15
|
+
const BLOCK = new Set(["p", "div", "section", "article", "main", "header", "figure", "figcaption", "details", "summary", "address", "dd", "dt", "dl", "tr", "table", "thead", "tbody", "tfoot", "ul", "ol", "li", "blockquote", "pre", "h1", "h2", "h3", "h4", "h5", "h6", "hr", "br"]);
|
|
16
|
+
|
|
17
|
+
function decode(text) {
|
|
18
|
+
return String(text)
|
|
19
|
+
.replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
|
|
20
|
+
.replace(/"/g, '"').replace(/�?39;|'/g, "'")
|
|
21
|
+
.replace(/&#(\d+);/g, (m, n) => safeChar(Number(n), m)).replace(/&#x([0-9a-f]+);/gi, (m, n) => safeChar(parseInt(n, 16), m));
|
|
22
|
+
}
|
|
23
|
+
function safeChar(code, fallback) {
|
|
24
|
+
try { return String.fromCodePoint(code); } catch (e) { return fallback; }
|
|
25
|
+
}
|
|
26
|
+
const squash = (s) => decode(s).replace(/\s+/g, " ");
|
|
27
|
+
const tagOf = (node) => (node.rawTagName || "").toLowerCase();
|
|
28
|
+
|
|
29
|
+
function touchingLinks(node) {
|
|
30
|
+
const prev = node.previousSibling;
|
|
31
|
+
return !!prev && prev.nodeType === 1 && tagOf(node) === "a" && tagOf(prev) === "a";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Inline text of a node: links and emphasis become their text, <code> keeps backticks.
|
|
35
|
+
function inline(node) {
|
|
36
|
+
if (node.nodeType === 3) return squash(node.rawText);
|
|
37
|
+
if (node.nodeType !== 1) return "";
|
|
38
|
+
const tag = tagOf(node);
|
|
39
|
+
if (tag === "br") return "\n";
|
|
40
|
+
if (tag === "img") return "";
|
|
41
|
+
// Block children (a heading inside a list item) and back-to-back links get a space, so words do not run together.
|
|
42
|
+
let inner = "";
|
|
43
|
+
for (const c of node.childNodes) {
|
|
44
|
+
const t = c.nodeType === 1 ? tagOf(c) : "";
|
|
45
|
+
const spaced = t && (BLOCK.has(t) || touchingLinks(c));
|
|
46
|
+
inner += (spaced ? " " : "") + inline(c) + (t && BLOCK.has(t) && t !== "br" ? " " : "");
|
|
47
|
+
}
|
|
48
|
+
if (tag === "code" && inner.trim()) return "`" + inner.trim() + "`";
|
|
49
|
+
return inner;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function codeBlock(node) {
|
|
53
|
+
const code = node.querySelector("code") || node;
|
|
54
|
+
const cls = (code.getAttribute("class") || "") + " " + (node.getAttribute("class") || "") + " " + (node.getAttribute("data-language") || node.getAttribute("data-lang") || "");
|
|
55
|
+
const lang = (/(?:language|lang|highlight-source)-([\w+#.-]+)/.exec(cls) || [])[1] || (node.getAttribute("data-language") || node.getAttribute("data-lang") || "");
|
|
56
|
+
// Line-per-element highlighters (Shiki, Expressive Code) put each line in its own block element.
|
|
57
|
+
const lines = code.querySelectorAll(".line, .ec-line");
|
|
58
|
+
const text = lines.length > 1 ? lines.map((l) => decode(l.text)).join("\n") : decode(code.text);
|
|
59
|
+
const body = text.replace(/\s+$/, "").replace(/^\n+/, "");
|
|
60
|
+
if (!body.trim()) return "";
|
|
61
|
+
const fence = body.includes("```") ? "~~~~" : "```";
|
|
62
|
+
return fence + lang + "\n" + body + "\n" + fence;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function table(node) {
|
|
66
|
+
const rows = node.querySelectorAll("tr").map((tr) => tr.querySelectorAll("th,td").map((c) => inline(c).replace(/\s+/g, " ").replace(/\|/g, "\\|").trim()));
|
|
67
|
+
const filled = rows.filter((r) => r.length);
|
|
68
|
+
if (!filled.length) return "";
|
|
69
|
+
const out = ["| " + filled[0].join(" | ") + " |", "| " + filled[0].map(() => "---").join(" | ") + " |"];
|
|
70
|
+
for (const r of filled.slice(1)) out.push("| " + r.join(" | ") + " |");
|
|
71
|
+
return out.join("\n");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function list(node, depth) {
|
|
75
|
+
const ordered = tagOf(node) === "ol";
|
|
76
|
+
const out = [];
|
|
77
|
+
let n = 0;
|
|
78
|
+
for (const li of node.childNodes) {
|
|
79
|
+
if (li.nodeType !== 1 || tagOf(li) !== "li") continue;
|
|
80
|
+
n++;
|
|
81
|
+
const own = [], nested = [];
|
|
82
|
+
for (const c of li.childNodes) {
|
|
83
|
+
const t = c.nodeType === 1 ? tagOf(c) : "";
|
|
84
|
+
if (t === "ul" || t === "ol") nested.push(list(c, depth + 1));
|
|
85
|
+
else if (t === "pre") nested.push(codeBlock(c).replace(/^/gm, " ".repeat(depth + 1)));
|
|
86
|
+
else own.push(t && BLOCK.has(t) ? " " + inline(c) + " " : (t && touchingLinks(c) ? " " : "") + inline(c));
|
|
87
|
+
}
|
|
88
|
+
const text = own.join("").replace(/\s+/g, " ").trim();
|
|
89
|
+
if (text) out.push(" ".repeat(depth) + (ordered ? n + ". " : "- ") + text);
|
|
90
|
+
for (const x of nested) if (x) out.push(x);
|
|
91
|
+
}
|
|
92
|
+
return out.join("\n");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Markdown blocks of a content root. `shift` moves page headings down so a page's own
|
|
96
|
+
// sections sit under its "# Title" in llms-full.txt (h2 → ##, h3 → ###, …).
|
|
97
|
+
function blocks(root, ctx) {
|
|
98
|
+
const out = [];
|
|
99
|
+
let run = [];
|
|
100
|
+
const flush = () => {
|
|
101
|
+
const text = run.join("").replace(/[ \t]*\n[ \t]*/g, "\n").replace(/[ \t]+/g, " ").trim();
|
|
102
|
+
if (text) out.push(text);
|
|
103
|
+
run = [];
|
|
104
|
+
};
|
|
105
|
+
const walk = (node) => {
|
|
106
|
+
if (node.nodeType === 3) { run.push(squash(node.rawText)); return; }
|
|
107
|
+
if (node.nodeType !== 1) return;
|
|
108
|
+
const tag = tagOf(node);
|
|
109
|
+
if (!BLOCK.has(tag)) {
|
|
110
|
+
if (node.childNodes.some((c) => c.nodeType === 1 && BLOCK.has(tagOf(c)))) { node.childNodes.forEach(walk); return; }
|
|
111
|
+
run.push((touchingLinks(node) ? " " : "") + inline(node));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (tag === "br") { run.push("\n"); return; }
|
|
115
|
+
flush();
|
|
116
|
+
if (/^h[1-6]$/.test(tag)) {
|
|
117
|
+
const text = inline(node).replace(/\s+/g, " ").replace(/^#+\s*|\s*[#¶🔗]+$/g, "").trim();
|
|
118
|
+
if (!text) return;
|
|
119
|
+
const level = Number(tag[1]);
|
|
120
|
+
if (level === 1 && !ctx.h1) { ctx.h1 = text; return; } // the page title, written once by the caller
|
|
121
|
+
const id = node.getAttribute("id") || (node.querySelector("[id]") || { getAttribute: () => "" }).getAttribute("id") || "";
|
|
122
|
+
out.push({ heading: Math.max(2, level), text, id });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (tag === "pre") { const b = codeBlock(node); if (b) out.push(b); return; }
|
|
126
|
+
if (tag === "ul" || tag === "ol") { const b = list(node, 0); if (b) out.push(b); return; }
|
|
127
|
+
if (tag === "table") { const b = table(node); if (b) out.push(b); return; }
|
|
128
|
+
if (tag === "hr") return;
|
|
129
|
+
if (tag === "blockquote") {
|
|
130
|
+
const inner = blocks(node, ctx).filter((b) => typeof b === "string").join("\n\n");
|
|
131
|
+
if (inner) out.push(inner.replace(/^/gm, "> "));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
node.childNodes.forEach(walk);
|
|
135
|
+
flush();
|
|
136
|
+
};
|
|
137
|
+
root.childNodes.forEach(walk);
|
|
138
|
+
flush();
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function meta(doc, selector) {
|
|
143
|
+
const el = doc.querySelector(selector);
|
|
144
|
+
return el ? decode(el.getAttribute("content") || "").replace(/\s+/g, " ").trim() : "";
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// One HTML document → { title, description, canonical, noindex, redirect, sections, text }.
|
|
148
|
+
// sections: [{ heading, id, text }] where the first one (heading "") is the text before any h2.
|
|
149
|
+
function readHtml(html, options) {
|
|
150
|
+
const o = options || {};
|
|
151
|
+
const doc = parse(String(html), { comment: false, blockTextElements: { script: false, style: false, noscript: false } });
|
|
152
|
+
const robots = meta(doc, 'meta[name="robots"]') + " " + meta(doc, 'meta[name="googlebot"]');
|
|
153
|
+
const refresh = doc.querySelector('meta[http-equiv="refresh"], meta[http-equiv="Refresh"]');
|
|
154
|
+
const canonicalEl = doc.querySelector('link[rel="canonical"]');
|
|
155
|
+
const titleEl = doc.querySelector("title");
|
|
156
|
+
const docTitle = titleEl ? decode(titleEl.text).replace(/\s+/g, " ").trim() : "";
|
|
157
|
+
const info = {
|
|
158
|
+
title: "", docTitle,
|
|
159
|
+
siteName: meta(doc, 'meta[property="og:site_name"]'),
|
|
160
|
+
description: meta(doc, 'meta[name="description"]') || meta(doc, 'meta[property="og:description"]'),
|
|
161
|
+
canonical: (canonicalEl && canonicalEl.getAttribute("href")) || meta(doc, 'meta[property="og:url"]') || "",
|
|
162
|
+
noindex: /noindex/i.test(robots),
|
|
163
|
+
redirect: !!refresh,
|
|
164
|
+
lang: (doc.querySelector("html") && doc.querySelector("html").getAttribute("lang")) || "",
|
|
165
|
+
sections: [], text: "",
|
|
166
|
+
};
|
|
167
|
+
const selectors = [].concat(o.content || []).filter(Boolean).concat(ROOTS);
|
|
168
|
+
let root = null;
|
|
169
|
+
for (const s of selectors) { try { root = doc.querySelector(s); } catch (e) { root = null; } if (root) break; }
|
|
170
|
+
if (!root) return info;
|
|
171
|
+
for (const s of [DROP].concat(o.exclude || [])) {
|
|
172
|
+
let found = [];
|
|
173
|
+
try { found = root.querySelectorAll(s); } catch (e) { /* a selector the parser cannot read is skipped */ }
|
|
174
|
+
for (const el of found) el.remove();
|
|
175
|
+
}
|
|
176
|
+
const ctx = { h1: "" };
|
|
177
|
+
const parts = blocks(root, ctx);
|
|
178
|
+
let cur = { heading: "", level: 0, id: "", lines: [] };
|
|
179
|
+
const sections = [cur];
|
|
180
|
+
for (const b of parts) {
|
|
181
|
+
if (typeof b === "string") { cur.lines.push(b); continue; }
|
|
182
|
+
if (b.heading === 2) { cur = { heading: b.text, level: 2, id: b.id, lines: [] }; sections.push(cur); }
|
|
183
|
+
else cur.lines.push("#".repeat(b.heading) + " " + b.text);
|
|
184
|
+
}
|
|
185
|
+
info.sections = sections.map((s) => ({ heading: s.heading, id: s.id, text: s.lines.join("\n\n").trim() })).filter((s) => s.heading || s.text);
|
|
186
|
+
info.text = info.sections.map((s) => (s.heading ? "## " + s.heading + "\n\n" : "") + s.text).join("\n\n").trim();
|
|
187
|
+
info.title = ctx.h1 || meta(doc, 'meta[property="og:title"]') || docTitle;
|
|
188
|
+
return info;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// "a/**/b*.html" → RegExp, matched against paths relative to the site folder with "/" separators.
|
|
192
|
+
function globToRegExp(glob) {
|
|
193
|
+
const g = String(glob).replace(/^\.?\//, "");
|
|
194
|
+
let re = "";
|
|
195
|
+
for (let i = 0; i < g.length; i++) {
|
|
196
|
+
const c = g[i];
|
|
197
|
+
if (c === "*") {
|
|
198
|
+
if (g[i + 1] === "*") { re += ".*"; i++; if (g[i + 1] === "/") i++; }
|
|
199
|
+
else re += "[^/]*";
|
|
200
|
+
} else if (c === "?") re += "[^/]";
|
|
201
|
+
else re += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
202
|
+
}
|
|
203
|
+
return new RegExp("^" + re + "$");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function walkHtml(dir) {
|
|
207
|
+
const out = [];
|
|
208
|
+
const visit = (abs, rel) => {
|
|
209
|
+
for (const entry of fs.readdirSync(abs, { withFileTypes: true })) {
|
|
210
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
211
|
+
const childAbs = path.join(abs, entry.name), childRel = rel ? rel + "/" + entry.name : entry.name;
|
|
212
|
+
if (entry.isDirectory()) visit(childAbs, childRel);
|
|
213
|
+
else if (/\.html?$/i.test(entry.name)) out.push(childRel);
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
visit(dir, "");
|
|
217
|
+
return out;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function urlPath(rel, cleanUrls) {
|
|
221
|
+
let p = "/" + rel;
|
|
222
|
+
if (/(^|\/)index\.html?$/i.test(rel)) p = p.replace(/index\.html?$/i, "");
|
|
223
|
+
else if (cleanUrls) p = p.replace(/\.html?$/i, "");
|
|
224
|
+
return p.split("/").map((seg) => encodeURIComponent(seg).replace(/%2B/g, "+")).join("/");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function joinUrl(site, p) {
|
|
228
|
+
if (!site) return "";
|
|
229
|
+
return String(site).replace(/\/+$/, "") + (p.startsWith("/") ? p : "/" + p);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Home page first, then by depth, then by path: the order pages appear in both files.
|
|
233
|
+
function byPath(a, b) {
|
|
234
|
+
const da = a.path.split("/").filter(Boolean).length, db = b.path.split("/").filter(Boolean).length;
|
|
235
|
+
return da - db || (a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Read every page of a built site. Returns [{ file, path, url, title, description, sections, text }].
|
|
239
|
+
function scan(dir, options) {
|
|
240
|
+
const o = options || {};
|
|
241
|
+
const root = path.resolve(dir);
|
|
242
|
+
if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) throw new Error("static-llms-txt: " + root + " is not a folder. Pass the folder your site was built into.");
|
|
243
|
+
const excludes = [].concat(o.exclude || []).map(globToRegExp);
|
|
244
|
+
const pages = [];
|
|
245
|
+
for (const rel of walkHtml(root)) {
|
|
246
|
+
if (/(^|\/)(404|500)(\.html?|\/index\.html?)$/i.test(rel)) continue;
|
|
247
|
+
if (excludes.some((re) => re.test(rel))) continue;
|
|
248
|
+
const info = readHtml(fs.readFileSync(path.join(root, rel), "utf8"), { content: o.content, exclude: o.remove });
|
|
249
|
+
if (info.noindex || info.redirect) continue;
|
|
250
|
+
if (info.text.length < (o.minLength === undefined ? 40 : o.minLength)) continue;
|
|
251
|
+
const p = urlPath(rel, o.cleanUrls);
|
|
252
|
+
const url = o.site ? joinUrl(o.site, p) : /^https?:\/\//i.test(info.canonical) ? info.canonical : "";
|
|
253
|
+
pages.push({ file: rel, path: p, url, title: info.title || p, docTitle: info.docTitle, siteName: info.siteName, description: info.description, sections: info.sections, text: info.text });
|
|
254
|
+
}
|
|
255
|
+
return pages.sort(byPath);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const oneLine = (s) => String(s || "").replace(/\s+/g, " ").trim();
|
|
259
|
+
|
|
260
|
+
// pages: [{ url, path?, title, description?, text? | sections? | html? }] → { llms, llmsFull }.
|
|
261
|
+
function build(pages, options) {
|
|
262
|
+
const o = options || {};
|
|
263
|
+
const list = pages.map((p) => {
|
|
264
|
+
if (p.html && !p.text && !p.sections) {
|
|
265
|
+
const info = readHtml(p.html, { content: o.content, exclude: o.remove });
|
|
266
|
+
return Object.assign({}, p, { title: p.title || info.title, description: p.description || info.description, sections: info.sections, text: info.text });
|
|
267
|
+
}
|
|
268
|
+
return p;
|
|
269
|
+
}).filter((p) => p && (p.text || (p.sections && p.sections.length)));
|
|
270
|
+
const home = list.find((p) => (p.path || "") === "/") || list[0] || {};
|
|
271
|
+
const title = oneLine(o.title || home.siteName || (home.docTitle || home.title || "").split(/\s+[|·–—-]\s+/).pop() || "Site");
|
|
272
|
+
const description = oneLine(o.description || home.description || "");
|
|
273
|
+
const link = (p) => p.url || (o.site ? joinUrl(o.site, p.path || "/") : p.path || "");
|
|
274
|
+
|
|
275
|
+
const index = ["# " + title, ""];
|
|
276
|
+
if (description) index.push("> " + description, "");
|
|
277
|
+
index.push("## " + (o.section || "Pages"), "");
|
|
278
|
+
for (const p of list) {
|
|
279
|
+
const d = oneLine(p.description);
|
|
280
|
+
index.push("- [" + oneLine(p.title).replace(/[[\]]/g, "") + "](" + link(p) + ")" + (d ? ": " + d : ""));
|
|
281
|
+
}
|
|
282
|
+
if (o.full !== false) index.push("", "## Optional", "", "- [Full text of every page](" + (o.site ? joinUrl(o.site, "/llms-full.txt") : "/llms-full.txt") + ")");
|
|
283
|
+
index.push("", MARK, "");
|
|
284
|
+
|
|
285
|
+
const full = [];
|
|
286
|
+
for (const p of list) {
|
|
287
|
+
const url = link(p);
|
|
288
|
+
const abs = /^https?:\/\//i.test(url);
|
|
289
|
+
full.push("# " + oneLine(p.title), "");
|
|
290
|
+
if (abs) full.push("Source: " + url, "");
|
|
291
|
+
const sections = p.sections || [{ heading: "", id: "", text: p.text }];
|
|
292
|
+
for (const s of sections) {
|
|
293
|
+
if (s.heading) {
|
|
294
|
+
full.push("## " + oneLine(s.heading), "");
|
|
295
|
+
if (abs && s.id) full.push("Source: " + url.replace(/#.*$/, "") + "#" + encodeURIComponent(s.id), "");
|
|
296
|
+
}
|
|
297
|
+
if (s.text) full.push(s.text, "");
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
full.push(MARK, "");
|
|
301
|
+
return { title, description, pages: list, llms: index.join("\n"), llmsFull: full.join("\n").replace(/\n{3,}/g, "\n\n") };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// A file we did not write (another plugin's llms.txt, or one written by hand) is left alone.
|
|
305
|
+
function writeOwn(file, text, force) {
|
|
306
|
+
if (!force && fs.existsSync(file) && !fs.readFileSync(file, "utf8").includes(MARK)) return false;
|
|
307
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
308
|
+
fs.writeFileSync(file, text);
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Scan a built site and write llms.txt and llms-full.txt into it.
|
|
313
|
+
function generate(dir, options) {
|
|
314
|
+
const o = options || {};
|
|
315
|
+
const pages = o.pages || scan(dir, o);
|
|
316
|
+
const result = build(pages, o);
|
|
317
|
+
const out = path.resolve(o.out || dir);
|
|
318
|
+
const written = [], skipped = [];
|
|
319
|
+
const put = (name, text) => (o.write === false ? null : (writeOwn(path.join(out, name), text, o.force) ? written : skipped).push(name));
|
|
320
|
+
put("llms.txt", result.llms);
|
|
321
|
+
if (o.full !== false) put("llms-full.txt", result.llmsFull);
|
|
322
|
+
return Object.assign(result, { written, skipped });
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const attr = (v) => String(v).replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
|
326
|
+
|
|
327
|
+
// The Ask AI script tag. Options become data-* attributes; `ref` names the plugin that added it.
|
|
328
|
+
function scriptTag(options) {
|
|
329
|
+
const o = options || {};
|
|
330
|
+
const names = ["key", "ref", "label", "placeholder", "position", "button", "color", "theme", "lang", "rules", "index"];
|
|
331
|
+
let tag = '<script async src="' + attr(o.src || SCRIPT) + '"';
|
|
332
|
+
for (const n of names) if (o[n] !== undefined && o[n] !== null && o[n] !== "") tag += " data-" + n + '="' + attr(o[n]) + '"';
|
|
333
|
+
return tag + "></script>";
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Add the Ask AI script tag before </body> of every page in a built site. Running it twice changes nothing.
|
|
337
|
+
function inject(dir, options) {
|
|
338
|
+
const root = path.resolve(dir);
|
|
339
|
+
const o = options || {};
|
|
340
|
+
const excludes = [].concat(o.exclude || []).map(globToRegExp);
|
|
341
|
+
const tag = scriptTag(o);
|
|
342
|
+
const changed = [];
|
|
343
|
+
for (const rel of walkHtml(root)) {
|
|
344
|
+
if (excludes.some((re) => re.test(rel))) continue;
|
|
345
|
+
const file = path.join(root, rel);
|
|
346
|
+
const html = fs.readFileSync(file, "utf8");
|
|
347
|
+
if (/<script[^>]+\/ask-docs\.js/i.test(html)) continue;
|
|
348
|
+
const at = html.search(/<\/body\s*>(?![\s\S]*<\/body\s*>)/i);
|
|
349
|
+
if (at === -1) continue;
|
|
350
|
+
fs.writeFileSync(file, html.slice(0, at) + tag + "\n" + html.slice(at));
|
|
351
|
+
changed.push(rel);
|
|
352
|
+
}
|
|
353
|
+
return changed;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
module.exports = { generate, scan, build, readHtml, inject, scriptTag, globToRegExp, MARK, SCRIPT };
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
export interface Section {
|
|
2
|
+
/** Text of the h2 that opens the section. Empty for the text before the first h2. */
|
|
3
|
+
heading: string;
|
|
4
|
+
/** The heading's id attribute, used for #anchor links. */
|
|
5
|
+
id: string;
|
|
6
|
+
/** Markdown text of the section. */
|
|
7
|
+
text: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface PageInfo {
|
|
11
|
+
title: string;
|
|
12
|
+
docTitle: string;
|
|
13
|
+
siteName: string;
|
|
14
|
+
description: string;
|
|
15
|
+
canonical: string;
|
|
16
|
+
noindex: boolean;
|
|
17
|
+
redirect: boolean;
|
|
18
|
+
lang: string;
|
|
19
|
+
sections: Section[];
|
|
20
|
+
text: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface Page {
|
|
24
|
+
/** Absolute address of the page. */
|
|
25
|
+
url?: string;
|
|
26
|
+
/** Path from the site root, e.g. "/guide/". */
|
|
27
|
+
path?: string;
|
|
28
|
+
/** File path relative to the scanned folder (set by scan). */
|
|
29
|
+
file?: string;
|
|
30
|
+
title: string;
|
|
31
|
+
description?: string;
|
|
32
|
+
/** Markdown text. Give text, sections or html. */
|
|
33
|
+
text?: string;
|
|
34
|
+
sections?: Section[];
|
|
35
|
+
/** Raw HTML of the page; build() reads it with readHtml(). */
|
|
36
|
+
html?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ReadOptions {
|
|
40
|
+
/** CSS selector (or list) of the main content. Default: main, [role=main], article, #content, .content, body. */
|
|
41
|
+
content?: string | string[];
|
|
42
|
+
/** Extra CSS selectors to drop from the content. */
|
|
43
|
+
exclude?: string | string[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface Options {
|
|
47
|
+
/** Public address of the site, e.g. "https://example.com". Makes every link absolute. */
|
|
48
|
+
site?: string;
|
|
49
|
+
/** Site title. Default: taken from the home page. */
|
|
50
|
+
title?: string;
|
|
51
|
+
/** One-line summary under the title. Default: the home page meta description. */
|
|
52
|
+
description?: string;
|
|
53
|
+
/** Heading of the page list in llms.txt. Default "Pages". */
|
|
54
|
+
section?: string;
|
|
55
|
+
/** CSS selector (or list) of the main content. */
|
|
56
|
+
content?: string | string[];
|
|
57
|
+
/** Extra CSS selectors to drop from every page. */
|
|
58
|
+
remove?: string | string[];
|
|
59
|
+
/** Globs of files to skip, relative to the folder, e.g. "tags/**". */
|
|
60
|
+
exclude?: string | string[];
|
|
61
|
+
/** Link /about instead of /about.html. */
|
|
62
|
+
cleanUrls?: boolean;
|
|
63
|
+
/** Pages with less text than this are skipped. Default 40 characters. */
|
|
64
|
+
minLength?: number;
|
|
65
|
+
/** false writes llms.txt only. */
|
|
66
|
+
full?: boolean;
|
|
67
|
+
/** Overwrite an llms.txt that this tool did not write. */
|
|
68
|
+
force?: boolean;
|
|
69
|
+
/** Write to this folder instead of the scanned one. */
|
|
70
|
+
out?: string;
|
|
71
|
+
/** false returns the text without writing files. */
|
|
72
|
+
write?: boolean;
|
|
73
|
+
/** Use these pages instead of scanning the folder. */
|
|
74
|
+
pages?: Page[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface Result {
|
|
78
|
+
title: string;
|
|
79
|
+
description: string;
|
|
80
|
+
pages: Page[];
|
|
81
|
+
/** Text of llms.txt. */
|
|
82
|
+
llms: string;
|
|
83
|
+
/** Text of llms-full.txt. */
|
|
84
|
+
llmsFull: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface GenerateResult extends Result {
|
|
88
|
+
written: string[];
|
|
89
|
+
skipped: string[];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface ScriptOptions {
|
|
93
|
+
/** OneTag AI site key. Public by design. */
|
|
94
|
+
key?: string;
|
|
95
|
+
/** Short name of the tool that added the tag. */
|
|
96
|
+
ref?: string;
|
|
97
|
+
label?: string;
|
|
98
|
+
placeholder?: string;
|
|
99
|
+
position?: "auto" | "float" | "left" | "none";
|
|
100
|
+
/** CSS selector of your own button that opens the box. */
|
|
101
|
+
button?: string;
|
|
102
|
+
/** Accent color as hex without "#". */
|
|
103
|
+
color?: string;
|
|
104
|
+
theme?: "light" | "dark";
|
|
105
|
+
lang?: string;
|
|
106
|
+
rules?: string;
|
|
107
|
+
index?: string;
|
|
108
|
+
src?: string;
|
|
109
|
+
/** Globs of files to leave alone (inject only). */
|
|
110
|
+
exclude?: string | string[];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Scan a built site and write llms.txt and llms-full.txt into it. */
|
|
114
|
+
export function generate(dir: string, options?: Options): GenerateResult;
|
|
115
|
+
/** Read every page of a built site. */
|
|
116
|
+
export function scan(dir: string, options?: Options): Page[];
|
|
117
|
+
/** Turn a list of pages into the text of both files. */
|
|
118
|
+
export function build(pages: Page[], options?: Options): Result;
|
|
119
|
+
/** Read one HTML document: title, description and main content as Markdown. */
|
|
120
|
+
export function readHtml(html: string, options?: ReadOptions): PageInfo;
|
|
121
|
+
/** Add the Ask AI script tag before </body> of every page. Returns the files changed. */
|
|
122
|
+
export function inject(dir: string, options?: ScriptOptions): string[];
|
|
123
|
+
/** The Ask AI script tag as a string. */
|
|
124
|
+
export function scriptTag(options?: ScriptOptions): string;
|
|
125
|
+
export function globToRegExp(glob: string): RegExp;
|
|
126
|
+
export const MARK: string;
|
|
127
|
+
export const SCRIPT: string;
|
|
128
|
+
declare const core: { generate: typeof generate; scan: typeof scan; build: typeof build; readHtml: typeof readHtml; inject: typeof inject; scriptTag: typeof scriptTag; globToRegExp: typeof globToRegExp; MARK: string; SCRIPT: string };
|
|
129
|
+
export default core;
|
package/index.mjs
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "static-llms-txt",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Write llms.txt and llms-full.txt for any static site from the HTML it already built. Works after Eleventy, Hugo, Jekyll, Astro, Gatsby, Docusaurus or plain HTML. No API key.",
|
|
5
|
+
"main": "./index.cjs",
|
|
6
|
+
"module": "./index.mjs",
|
|
7
|
+
"types": "./index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./index.d.ts",
|
|
11
|
+
"import": "./index.mjs",
|
|
12
|
+
"require": "./index.cjs"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"static-llms-txt": "./cli.cjs"
|
|
17
|
+
},
|
|
18
|
+
"files": ["index.cjs", "index.mjs", "index.d.ts", "cli.cjs", "README.md", "LICENSE"],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"test": "node --test test/*.test.mjs",
|
|
21
|
+
"prepublishOnly": "npm test"
|
|
22
|
+
},
|
|
23
|
+
"keywords": ["llms.txt", "llms-txt", "llms-full.txt", "llmstxt", "static-site", "html-to-markdown", "ssg", "eleventy", "hugo", "jekyll", "ai", "seo", "geo", "cli"],
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"author": "OneTag AI (https://onetagai.com)",
|
|
26
|
+
"homepage": "https://github.com/fengyiqicoder/onetagai/tree/main/packages/static-llms-txt#readme",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/fengyiqicoder/onetagai.git",
|
|
30
|
+
"directory": "packages/static-llms-txt"
|
|
31
|
+
},
|
|
32
|
+
"bugs": "https://github.com/fengyiqicoder/onetagai/issues",
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"node-html-parser": "^7.0.1"
|
|
38
|
+
}
|
|
39
|
+
}
|