@relvehq/agent-ready 0.0.0-stage → 0.2.1

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.
Files changed (82) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +338 -3
  3. package/SETUP.md +337 -0
  4. package/dist/bin/agent-ready.d.ts +23 -0
  5. package/dist/bin/agent-ready.js +350 -0
  6. package/dist/cli/init.d.ts +70 -0
  7. package/dist/cli/init.js +366 -0
  8. package/dist/core/accept.d.ts +8 -0
  9. package/dist/core/accept.js +76 -0
  10. package/dist/core/config.d.ts +145 -0
  11. package/dist/core/config.js +103 -0
  12. package/dist/core/convert.d.ts +22 -0
  13. package/dist/core/convert.js +334 -0
  14. package/dist/core/detect.d.ts +30 -0
  15. package/dist/core/detect.js +82 -0
  16. package/dist/core/easy.d.ts +19 -0
  17. package/dist/core/easy.js +117 -0
  18. package/dist/core/enrich.d.ts +30 -0
  19. package/dist/core/enrich.js +155 -0
  20. package/dist/core/llms.d.ts +11 -0
  21. package/dist/core/llms.js +71 -0
  22. package/dist/core/report.d.ts +15 -0
  23. package/dist/core/report.js +46 -0
  24. package/dist/core/resolve.d.ts +43 -0
  25. package/dist/core/resolve.js +204 -0
  26. package/dist/core/respond.d.ts +18 -0
  27. package/dist/core/respond.js +59 -0
  28. package/dist/core/sitemap.d.ts +31 -0
  29. package/dist/core/sitemap.js +140 -0
  30. package/dist/index.d.ts +14 -0
  31. package/dist/index.js +76 -0
  32. package/dist/next/dashboard.d.ts +14 -0
  33. package/dist/next/dashboard.js +56 -0
  34. package/dist/next/index.d.ts +42 -0
  35. package/dist/next/index.js +100 -0
  36. package/dist/next/route.d.ts +26 -0
  37. package/dist/next/route.js +167 -0
  38. package/dist/node/index.d.ts +8 -0
  39. package/dist/node/index.js +191 -0
  40. package/dist/proof/audit.d.ts +47 -0
  41. package/dist/proof/audit.js +253 -0
  42. package/dist/proof/bots.d.ts +24 -0
  43. package/dist/proof/bots.js +60 -0
  44. package/dist/proof/dashboard.d.ts +8 -0
  45. package/dist/proof/dashboard.js +77 -0
  46. package/dist/proof/hits.d.ts +18 -0
  47. package/dist/proof/hits.js +51 -0
  48. package/dist/proof/jsonl.d.ts +5 -0
  49. package/dist/proof/jsonl.js +80 -0
  50. package/dist/proof/stats.d.ts +39 -0
  51. package/dist/proof/stats.js +127 -0
  52. package/dist/refine/check.d.ts +17 -0
  53. package/dist/refine/check.js +134 -0
  54. package/dist/refine/extract.d.ts +7 -0
  55. package/dist/refine/extract.js +120 -0
  56. package/dist/refine/file-store.d.ts +6 -0
  57. package/dist/refine/file-store.js +87 -0
  58. package/dist/refine/model.d.ts +30 -0
  59. package/dist/refine/model.js +105 -0
  60. package/dist/refine/prompts.d.ts +49 -0
  61. package/dist/refine/prompts.js +178 -0
  62. package/dist/refine/refine.d.ts +38 -0
  63. package/dist/refine/refine.js +75 -0
  64. package/dist/refine/run.d.ts +52 -0
  65. package/dist/refine/run.js +66 -0
  66. package/dist/refine/types.d.ts +17 -0
  67. package/dist/refine/types.js +8 -0
  68. package/dist/wp/relve-agent-ready/includes/class-relve-ar-admin.php +363 -0
  69. package/dist/wp/relve-agent-ready/includes/class-relve-ar-converter.php +762 -0
  70. package/dist/wp/relve-agent-ready/includes/class-relve-ar-llms.php +192 -0
  71. package/dist/wp/relve-agent-ready/includes/class-relve-ar-markdown-builder.php +447 -0
  72. package/dist/wp/relve-agent-ready/includes/class-relve-ar-negotiation.php +334 -0
  73. package/dist/wp/relve-agent-ready/includes/class-relve-ar-response.php +226 -0
  74. package/dist/wp/relve-agent-ready/includes/class-relve-ar-router.php +421 -0
  75. package/dist/wp/relve-agent-ready/includes/class-relve-ar-settings.php +115 -0
  76. package/dist/wp/relve-agent-ready/includes/class-relve-ar-store.php +543 -0
  77. package/dist/wp/relve-agent-ready/includes/index.php +2 -0
  78. package/dist/wp/relve-agent-ready/index.php +2 -0
  79. package/dist/wp/relve-agent-ready/readme.txt +75 -0
  80. package/dist/wp/relve-agent-ready/relve-agent-ready.php +70 -0
  81. package/dist/wp/relve-agent-ready/uninstall.php +24 -0
  82. package/package.json +64 -6
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ // Configuration for @relvehq/agent-ready.
3
+ //
4
+ // `defineAgentReady()` fills in defaults once, so every other module can read
5
+ // a fully resolved config. Only Web-standard types (Request, Response) appear
6
+ // here: the core never depends on a framework.
7
+ //
8
+ // EDGE-SAFE. No Node APIs. Middleware bundles import this module.
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.VIA_HEADER = exports.DEFAULT_INTERNAL_HEADER = exports.DEFAULT_INTERNAL_PREFIX = exports.DEFAULT_NEGOTIATED_CACHE_CONTROL = exports.DEFAULT_CACHE_CONTROL = exports.ALWAYS_EXCLUDE = void 0;
11
+ exports.defineAgentReady = defineAgentReady;
12
+ exports.isEnabled = isEnabled;
13
+ exports.isExcluded = isExcluded;
14
+ exports.emitHit = emitHit;
15
+ /** Never served as Markdown, whatever the venture's config says. */
16
+ exports.ALWAYS_EXCLUDE = [
17
+ "/api",
18
+ "/_next",
19
+ "/admin",
20
+ "/super-admin",
21
+ "/auth",
22
+ "/login",
23
+ "/logout",
24
+ "/signup",
25
+ "/signin",
26
+ "/sign-in",
27
+ "/sign-up",
28
+ ];
29
+ exports.DEFAULT_CACHE_CONTROL = "public, s-maxage=300, stale-while-revalidate=86400";
30
+ exports.DEFAULT_NEGOTIATED_CACHE_CONTROL = "private, max-age=300";
31
+ exports.DEFAULT_INTERNAL_PREFIX = "/__md";
32
+ exports.DEFAULT_INTERNAL_HEADER = "x-agent-ready-internal";
33
+ /** Request header the middleware adapter sets on its rewrite: how Markdown was asked for. */
34
+ exports.VIA_HEADER = "x-agent-ready-via";
35
+ function defineAgentReady(config) {
36
+ if (config.__resolved)
37
+ return config;
38
+ const c = config;
39
+ if (!c || typeof c.site !== "string" || !/^https?:\/\//.test(c.site)) {
40
+ throw new Error("agent-ready: config.site must be an absolute http(s) origin");
41
+ }
42
+ const internalPrefix = trimSlash(c.internalPrefix ?? exports.DEFAULT_INTERNAL_PREFIX);
43
+ return {
44
+ __resolved: true,
45
+ site: new URL(c.site).origin,
46
+ exclude: [...exports.ALWAYS_EXCLUDE, internalPrefix, ...(c.exclude ?? [])],
47
+ templates: c.templates ?? {},
48
+ store: c.store ?? null,
49
+ fallback: c.fallback ?? "convert",
50
+ cacheControl: c.cacheControl ?? exports.DEFAULT_CACHE_CONTROL,
51
+ negotiatedCacheControl: c.negotiatedCacheControl ?? exports.DEFAULT_NEGOTIATED_CACHE_CONTROL,
52
+ llms: c.llms ? { ...c.llms, path: c.llms.path ?? "/llms.txt" } : null,
53
+ onHit: c.onHit ?? null,
54
+ enabled: c.enabled ?? false,
55
+ internalPrefix,
56
+ internalHeader: (c.internalHeader ?? exports.DEFAULT_INTERNAL_HEADER).toLowerCase(),
57
+ fetchOrigin: c.fetchOrigin ?? null,
58
+ fetchTimeoutMs: c.fetchTimeoutMs ?? 8000,
59
+ minChars: c.minChars ?? 200,
60
+ };
61
+ }
62
+ /** The master switch. A throwing `enabled` function counts as off. */
63
+ function isEnabled(cfg, request) {
64
+ if (typeof cfg.enabled === "boolean")
65
+ return cfg.enabled;
66
+ try {
67
+ return cfg.enabled(request) === true;
68
+ }
69
+ catch {
70
+ return false;
71
+ }
72
+ }
73
+ /** Segment-aware prefix match: "/admin" excludes "/admin" and "/admin/x", not "/administrator". */
74
+ function isExcluded(cfg, path) {
75
+ for (const rule of cfg.exclude) {
76
+ if (typeof rule === "string") {
77
+ const p = trimSlash(rule);
78
+ if (p === "" || p === "/")
79
+ continue;
80
+ if (path === p || path.startsWith(p + "/"))
81
+ return true;
82
+ }
83
+ else if (rule.test(path)) {
84
+ return true;
85
+ }
86
+ }
87
+ return false;
88
+ }
89
+ /** Fire-and-forget: never awaited by callers, never throws. */
90
+ function emitHit(cfg, event) {
91
+ if (!cfg.onHit)
92
+ return;
93
+ try {
94
+ void Promise.resolve(cfg.onHit(event)).catch(() => { });
95
+ }
96
+ catch {
97
+ // A synchronous throw from the logger must not change the response.
98
+ }
99
+ }
100
+ function trimSlash(p) {
101
+ const t = p.replace(/\/+$/, "");
102
+ return t.startsWith("/") ? t : `/${t}`;
103
+ }
@@ -0,0 +1,22 @@
1
+ import type { MarkdownDoc } from "./config";
2
+ export interface ConvertOptions {
3
+ /** Absolute URL of the page: base for relative links and images. */
4
+ url: string;
5
+ /** Quality gate in characters of plain text. Default 200. */
6
+ minChars?: number;
7
+ }
8
+ export interface ConvertResult extends MarkdownDoc {
9
+ markdown: string;
10
+ textLength: number;
11
+ strategy: "readability" | "main" | "article" | "body";
12
+ }
13
+ type El = any;
14
+ export declare function htmlToMarkdown(html: string, opts: ConvertOptions): ConvertResult | null;
15
+ /**
16
+ * Next.js App Router streams Suspense content after the shell as
17
+ * `<div hidden id="S:n">…</div>` plus a script that swaps it in for the
18
+ * fallback that follows `<template id="B:n">` up to the `<!--/$-->` comment.
19
+ * Scripts never run here, so do the swap ourselves before [hidden] is stripped.
20
+ */
21
+ export declare function unfoldNextStreaming(doc: El): void;
22
+ export {};
@@ -0,0 +1,334 @@
1
+ "use strict";
2
+ // Convert: a page's HTML -> clean Markdown.
3
+ //
4
+ // 1. Read metadata (title, description, lang, canonical, JSON-LD) from <head>.
5
+ // 2. Unfold Next.js streamed Suspense content (<div hidden id="S:n">) into
6
+ // its placeholder, so App Router pages convert with their real content.
7
+ // 3. Strip chrome: script/style/nav/footer/forms/dialogs/cookie banners.
8
+ // 4. Pick the main content: Mozilla Readability, unless it returns too little
9
+ // compared with <main>/<article>/<body>, which are used instead.
10
+ // 5. Absolutize links and images, flatten block-level links, give header-less
11
+ // tables a header row so GFM can render them.
12
+ // 6. Turndown + GFM (tables, strikethrough, task lists), then collapse whitespace.
13
+ // 7. Quality gate: under `minChars` of plain text -> null (caller serves HTML).
14
+ //
15
+ // NODE ONLY (linkedom, turndown). Never import this from middleware.
16
+ var __importDefault = (this && this.__importDefault) || function (mod) {
17
+ return (mod && mod.__esModule) ? mod : { "default": mod };
18
+ };
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.htmlToMarkdown = htmlToMarkdown;
21
+ exports.unfoldNextStreaming = unfoldNextStreaming;
22
+ const linkedom_1 = require("linkedom");
23
+ const readability_1 = require("@mozilla/readability");
24
+ const turndown_1 = __importDefault(require("turndown"));
25
+ const turndown_plugin_gfm_1 = require("turndown-plugin-gfm");
26
+ const JUNK = [
27
+ "script",
28
+ "style",
29
+ "noscript",
30
+ "template",
31
+ "iframe",
32
+ "object",
33
+ "embed",
34
+ "svg",
35
+ "canvas",
36
+ "video",
37
+ "audio",
38
+ "form",
39
+ "input",
40
+ "button",
41
+ "select",
42
+ "textarea",
43
+ "label",
44
+ "nav",
45
+ "footer",
46
+ "dialog",
47
+ "link",
48
+ "meta",
49
+ "[role=navigation]",
50
+ "[role=contentinfo]",
51
+ "[role=banner]",
52
+ "[role=dialog]",
53
+ "[role=alertdialog]",
54
+ "[role=search]",
55
+ "[aria-hidden=true]",
56
+ "[hidden]",
57
+ ].join(",");
58
+ const COOKIE_RE = /(^|[\s_-])(cookie|cookies|consent|gdpr|onetrust|cookiebot|cc-window|cc-banner|cmp)([\s_-]|$)/i;
59
+ const BLOCK = "div,p,section,article,header,ul,ol,li,table,h1,h2,h3,h4,h5,h6,blockquote,pre,figure";
60
+ let turndown = null;
61
+ function getTurndown() {
62
+ if (turndown)
63
+ return turndown;
64
+ const td = new turndown_1.default({
65
+ headingStyle: "atx",
66
+ codeBlockStyle: "fenced",
67
+ bulletListMarker: "-",
68
+ emDelimiter: "_",
69
+ strongDelimiter: "**",
70
+ linkStyle: "inlined",
71
+ });
72
+ td.use(turndown_plugin_gfm_1.gfm);
73
+ // Rules added later win (turndown unshifts them). Cells: one line each, pipes escaped,
74
+ // so a cell with paragraphs or <br> can't break the GFM table.
75
+ td.addRule("agentReadyCell", {
76
+ filter: ["th", "td"],
77
+ replacement(content, node) {
78
+ const parent = node.parentNode;
79
+ const index = parent ? Array.prototype.indexOf.call(parent.childNodes, node) : 0;
80
+ const firstCell = !parent || Array.prototype.slice.call(parent.childNodes, 0, index).every((n) => n.nodeName !== "TH" && n.nodeName !== "TD");
81
+ const text = content.replace(/\s*\n+\s*/g, " ").replace(/\|/g, "\\|").trim();
82
+ return (firstCell ? "| " : " ") + text + " |";
83
+ },
84
+ });
85
+ td.remove(["script", "style", "noscript", "iframe", "button", "form", "svg"]);
86
+ turndown = td;
87
+ return td;
88
+ }
89
+ function htmlToMarkdown(html, opts) {
90
+ if (typeof html !== "string" || !html.trim())
91
+ return null;
92
+ const minChars = opts.minChars ?? 200;
93
+ const pageUrl = opts.url;
94
+ const { document } = (0, linkedom_1.parseHTML)(html);
95
+ const meta = readMeta(document, pageUrl);
96
+ const firstH1 = textOf(document.querySelector("h1"));
97
+ unfoldNextStreaming(document);
98
+ stripChrome(document);
99
+ const root = document.querySelector("main") || document.querySelector("article") || document.body || document.documentElement;
100
+ const rootStrategy = document.querySelector("main")
101
+ ? "main"
102
+ : document.querySelector("article")
103
+ ? "article"
104
+ : "body";
105
+ const rootHtml = root ? root.innerHTML : "";
106
+ const rootText = textLength(root ? root.textContent : "");
107
+ let contentHtml = rootHtml;
108
+ let strategy = rootStrategy;
109
+ let readabilityTitle;
110
+ try {
111
+ const { document: rdoc } = (0, linkedom_1.parseHTML)(document.toString());
112
+ const article = new readability_1.Readability(rdoc, { charThreshold: minChars, keepClasses: false }).parse();
113
+ const articleText = textLength(article?.textContent ?? "");
114
+ // Readability is great on articles but can keep a fraction of a listing or
115
+ // profile page. Use it only when it kept most of what <main> holds.
116
+ if (article && article.content && articleText >= minChars && articleText >= rootText * 0.6) {
117
+ contentHtml = article.content;
118
+ strategy = "readability";
119
+ readabilityTitle = article.title || undefined;
120
+ }
121
+ }
122
+ catch {
123
+ // Readability failed on odd markup: the <main>/<body> fallback stands.
124
+ }
125
+ const { document: cdoc } = (0, linkedom_1.parseHTML)(`<!doctype html><html><head></head><body>${contentHtml}</body></html>`);
126
+ absolutize(cdoc, pageUrl);
127
+ flattenBlockLinks(cdoc);
128
+ headerRows(cdoc);
129
+ let markdown = getTurndown().turndown(cdoc.body.innerHTML);
130
+ markdown = tidy(markdown);
131
+ const heading = firstH1 || readabilityTitle || meta.title;
132
+ if (heading && !/^#\s/m.test(markdown))
133
+ markdown = `# ${oneLine(heading)}\n\n${markdown}`;
134
+ const textLen = plainTextLength(markdown);
135
+ if (!markdown || textLen < minChars)
136
+ return null;
137
+ return { markdown, textLength: textLen, strategy, ...meta };
138
+ }
139
+ // ── metadata ─────────────────────────────────────────────────────────────
140
+ function readMeta(doc, pageUrl) {
141
+ const metaContent = (sel) => {
142
+ const v = doc.querySelector(sel)?.getAttribute("content");
143
+ return v ? oneLine(v) : undefined;
144
+ };
145
+ const title = metaContent('meta[property="og:title"]') || oneLine(textOf(doc.querySelector("title"))) || undefined;
146
+ const description = metaContent('meta[name="description"]') || metaContent('meta[property="og:description"]');
147
+ const lang = doc.documentElement?.getAttribute("lang") || undefined;
148
+ const canonicalHref = doc.querySelector('link[rel="canonical"]')?.getAttribute("href");
149
+ const canonical = canonicalHref ? absolute(canonicalHref, pageUrl) ?? undefined : undefined;
150
+ const jsonLd = [];
151
+ for (const s of Array.from(doc.querySelectorAll('script[type="application/ld+json"]'))) {
152
+ try {
153
+ jsonLd.push(JSON.parse(s.textContent || ""));
154
+ }
155
+ catch {
156
+ // Broken JSON-LD is skipped, not fatal.
157
+ }
158
+ }
159
+ return { title, description, lang, canonical, jsonLd };
160
+ }
161
+ // ── cleaning ─────────────────────────────────────────────────────────────
162
+ /**
163
+ * Next.js App Router streams Suspense content after the shell as
164
+ * `<div hidden id="S:n">…</div>` plus a script that swaps it in for the
165
+ * fallback that follows `<template id="B:n">` up to the `<!--/$-->` comment.
166
+ * Scripts never run here, so do the swap ourselves before [hidden] is stripped.
167
+ */
168
+ function unfoldNextStreaming(doc) {
169
+ for (const segment of Array.from(doc.querySelectorAll('div[hidden][id^="S:"]'))) {
170
+ const n = segment.getAttribute("id").slice(2);
171
+ const placeholder = doc.getElementById ? doc.getElementById(`B:${n}`) : doc.querySelector(`[id="B:${n}"]`);
172
+ if (!placeholder || !placeholder.parentNode) {
173
+ segment.removeAttribute("hidden");
174
+ continue;
175
+ }
176
+ // Drop the fallback between the placeholder and the closing <!--/$-->.
177
+ let next = placeholder.nextSibling;
178
+ while (next && !(next.nodeType === 8 && String(next.data).trim() === "/$")) {
179
+ const after = next.nextSibling;
180
+ next.parentNode.removeChild(next);
181
+ next = after;
182
+ }
183
+ while (segment.firstChild)
184
+ placeholder.parentNode.insertBefore(segment.firstChild, placeholder);
185
+ placeholder.parentNode.removeChild(placeholder);
186
+ segment.parentNode.removeChild(segment);
187
+ }
188
+ }
189
+ function stripChrome(doc) {
190
+ for (const el of Array.from(doc.querySelectorAll(JUNK)))
191
+ remove(el);
192
+ // Page-level <header> is site chrome; an <article>'s own <header> is content.
193
+ for (const el of Array.from(doc.querySelectorAll("header"))) {
194
+ if (!el.closest || !el.closest("main, article"))
195
+ remove(el);
196
+ }
197
+ for (const el of Array.from(doc.querySelectorAll('[style*="display:none"], [style*="display: none"]'))) {
198
+ remove(el);
199
+ }
200
+ // Cookie / consent banners, by id or class. Small elements only, so a
201
+ // `cookie-consent-open` class on a page wrapper can't wipe the page.
202
+ for (const el of Array.from(doc.querySelectorAll("[id], [class]"))) {
203
+ const tag = String(el.nodeName).toLowerCase();
204
+ if (tag === "html" || tag === "body" || tag === "main")
205
+ continue;
206
+ const hay = `${el.getAttribute("id") || ""} ${el.getAttribute("class") || ""}`;
207
+ if (COOKIE_RE.test(hay) && textLength(el.textContent || "") < 1500)
208
+ remove(el);
209
+ }
210
+ }
211
+ function remove(el) {
212
+ if (el && el.parentNode)
213
+ el.parentNode.removeChild(el);
214
+ }
215
+ // ── content fixes ────────────────────────────────────────────────────────
216
+ function absolutize(doc, pageUrl) {
217
+ for (const a of Array.from(doc.querySelectorAll("a"))) {
218
+ const href = a.getAttribute("href");
219
+ const abs = href ? absolute(href, pageUrl) : null;
220
+ if (!abs || /^javascript:/i.test(abs)) {
221
+ unwrap(a);
222
+ continue;
223
+ }
224
+ a.setAttribute("href", abs);
225
+ a.removeAttribute("title");
226
+ }
227
+ for (const img of Array.from(doc.querySelectorAll("img"))) {
228
+ const raw = img.getAttribute("src") || img.getAttribute("data-src") || "";
229
+ let src = raw && !/^data:/i.test(raw) ? absolute(raw, pageUrl) : null;
230
+ // next/image: /_next/image?url=<original>&w=… -> the original asset.
231
+ if (src) {
232
+ try {
233
+ const u = new URL(src);
234
+ if (u.pathname === "/_next/image" && u.searchParams.get("url")) {
235
+ src = absolute(u.searchParams.get("url"), pageUrl);
236
+ }
237
+ }
238
+ catch {
239
+ // keep src as is
240
+ }
241
+ }
242
+ if (!src) {
243
+ remove(img);
244
+ continue;
245
+ }
246
+ img.setAttribute("src", src);
247
+ img.removeAttribute("srcset");
248
+ if (!img.getAttribute("alt"))
249
+ img.setAttribute("alt", "");
250
+ }
251
+ }
252
+ /**
253
+ * A card-style link wrapping a heading and a paragraph would come out of
254
+ * turndown as a multi-line `[...](url)`, which is not valid Markdown. Link
255
+ * the heading instead, or collapse the link to one line of text.
256
+ */
257
+ function flattenBlockLinks(doc) {
258
+ for (const a of Array.from(doc.querySelectorAll("a[href]"))) {
259
+ if (!a.querySelector(BLOCK))
260
+ continue;
261
+ const href = a.getAttribute("href");
262
+ const heading = a.querySelector("h1, h2, h3, h4, h5, h6");
263
+ if (heading) {
264
+ const link = doc.createElement("a");
265
+ link.setAttribute("href", href);
266
+ link.textContent = oneLine(heading.textContent || "");
267
+ heading.textContent = "";
268
+ heading.appendChild(link);
269
+ unwrap(a);
270
+ }
271
+ else {
272
+ a.textContent = oneLine(a.textContent || "").slice(0, 300);
273
+ }
274
+ }
275
+ }
276
+ /** GFM needs a header row; promote the first row of tables that have none. */
277
+ function headerRows(doc) {
278
+ for (const table of Array.from(doc.querySelectorAll("table"))) {
279
+ if (table.querySelector("thead") || table.querySelector("th"))
280
+ continue;
281
+ const row = table.querySelector("tr");
282
+ if (!row)
283
+ continue;
284
+ for (const td of Array.from(row.querySelectorAll("td"))) {
285
+ const th = doc.createElement("th");
286
+ while (td.firstChild)
287
+ th.appendChild(td.firstChild);
288
+ td.parentNode.replaceChild(th, td);
289
+ }
290
+ }
291
+ }
292
+ function unwrap(el) {
293
+ const parent = el.parentNode;
294
+ if (!parent)
295
+ return;
296
+ while (el.firstChild)
297
+ parent.insertBefore(el.firstChild, el);
298
+ parent.removeChild(el);
299
+ }
300
+ // ── text helpers ─────────────────────────────────────────────────────────
301
+ function tidy(md) {
302
+ return md
303
+ .replace(/\r\n?/g, "\n")
304
+ .replace(/ /g, " ")
305
+ .replace(/!?\[\s*\]\((?:[^()]|\([^)]*\))*\)/g, (m) => (m.startsWith("!") ? m : "")) // empty links
306
+ .replace(/[ \t]+$/gm, "")
307
+ .replace(/^[ \t]*[-*][ \t]*\n/gm, "") // empty bullets
308
+ .replace(/\n{3,}/g, "\n\n")
309
+ .trim();
310
+ }
311
+ function plainTextLength(md) {
312
+ const text = md
313
+ .replace(/!\[[^\]]*\]\([^)]*\)/g, " ")
314
+ .replace(/\]\([^)]*\)/g, "]")
315
+ .replace(/[#*_>|`[\]-]/g, " ");
316
+ return textLength(text);
317
+ }
318
+ function textLength(s) {
319
+ return (s ?? "").replace(/\s+/g, " ").trim().length;
320
+ }
321
+ function textOf(el) {
322
+ return el ? oneLine(el.textContent || "") : "";
323
+ }
324
+ function oneLine(s) {
325
+ return s.replace(/\s+/g, " ").trim();
326
+ }
327
+ function absolute(href, base) {
328
+ try {
329
+ return new URL(href.trim(), base).href;
330
+ }
331
+ catch {
332
+ return null;
333
+ }
334
+ }
@@ -0,0 +1,30 @@
1
+ import type { Via } from "./config";
2
+ export interface RequestLike {
3
+ url: string;
4
+ headers: {
5
+ get(name: string): string | null;
6
+ };
7
+ }
8
+ export interface Detection {
9
+ markdown: boolean;
10
+ /** Normalised page path without `.md`: leading slash, no trailing slash. */
11
+ path: string;
12
+ via: Exclude<Via, "direct"> | null;
13
+ llms: boolean;
14
+ /** The original query minus `format`, "" or "?a=b". */
15
+ search: string;
16
+ }
17
+ export interface DetectOptions {
18
+ /** Path llms.txt is served at, or null when the venture serves none. */
19
+ llmsPath?: string | null;
20
+ }
21
+ export declare function wantsMarkdown(request: RequestLike, opts?: DetectOptions): Detection;
22
+ /**
23
+ * Leading slash, no trailing slash, no query or hash. Null for anything that
24
+ * isn't a plain path (traversal, backslashes, absurd length).
25
+ */
26
+ export declare function normalizePath(raw: string | null | undefined): string | null;
27
+ /** "/tool/x.md" -> "/tool/x", "/index.md" -> "/"; null when not a .md URL. */
28
+ export declare function stripMdSuffix(pathname: string): string | null;
29
+ /** "/tool/x" -> "/tool/x.md", "/" -> "/index.md". */
30
+ export declare function mdUrlPath(path: string): string;
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ // Detect: does this request want Markdown, and for which page?
3
+ //
4
+ // Three ways to ask, in order of precedence:
5
+ // 1. a `.md` suffix /tool/notion.md, /index.md for "/"
6
+ // 2. `?format=md` /tool/notion?format=md (also format=markdown)
7
+ // 3. the Accept header text/markdown preferred over (or tied with)
8
+ // text/html; a bare */* is NOT Markdown
9
+ // plus the llms.txt path when one is configured.
10
+ //
11
+ // EDGE-SAFE. No Node APIs.
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.wantsMarkdown = wantsMarkdown;
14
+ exports.normalizePath = normalizePath;
15
+ exports.stripMdSuffix = stripMdSuffix;
16
+ exports.mdUrlPath = mdUrlPath;
17
+ const accept_1 = require("./accept");
18
+ const MAX_PATH_LENGTH = 1024;
19
+ function wantsMarkdown(request, opts = {}) {
20
+ let url;
21
+ try {
22
+ url = new URL(request.url);
23
+ }
24
+ catch {
25
+ return { markdown: false, path: "/", via: null, llms: false, search: "" };
26
+ }
27
+ const format = (url.searchParams.get("format") ?? "").toLowerCase();
28
+ const params = new URLSearchParams(url.searchParams);
29
+ params.delete("format");
30
+ const qs = params.toString();
31
+ const search = qs ? `?${qs}` : "";
32
+ if (opts.llmsPath && url.pathname === opts.llmsPath) {
33
+ return { markdown: true, path: opts.llmsPath, via: "llms", llms: true, search: "" };
34
+ }
35
+ const no = (path) => ({ markdown: false, path, via: null, llms: false, search: url.search });
36
+ const stripped = stripMdSuffix(url.pathname);
37
+ if (stripped !== null) {
38
+ const path = normalizePath(stripped);
39
+ return path ? { markdown: true, path, via: "suffix", llms: false, search } : no(url.pathname);
40
+ }
41
+ const path = normalizePath(url.pathname);
42
+ if (!path)
43
+ return no(url.pathname);
44
+ if (format === "md" || format === "markdown") {
45
+ return { markdown: true, path, via: "query", llms: false, search };
46
+ }
47
+ if ((0, accept_1.chooseFormat)(request.headers.get("accept")) === "markdown") {
48
+ // The query is kept whole: `format` was not used to ask.
49
+ return { markdown: true, path, via: "accept", llms: false, search: url.search };
50
+ }
51
+ return no(path);
52
+ }
53
+ /**
54
+ * Leading slash, no trailing slash, no query or hash. Null for anything that
55
+ * isn't a plain path (traversal, backslashes, absurd length).
56
+ */
57
+ function normalizePath(raw) {
58
+ if (typeof raw !== "string")
59
+ return null;
60
+ const path = raw.split("?")[0].split("#")[0];
61
+ if (!path.startsWith("/") || path.length > MAX_PATH_LENGTH)
62
+ return null;
63
+ // "//" is only allowed as a trailing run ("/a//" -> "/a"): "//host/x" would
64
+ // become a protocol-relative URL once joined to an origin.
65
+ if (path.includes("..") || path.includes("\\") || /\/\/./.test(path))
66
+ return null;
67
+ const trimmed = path.replace(/\/+$/, "");
68
+ return trimmed === "" ? "/" : trimmed;
69
+ }
70
+ /** "/tool/x.md" -> "/tool/x", "/index.md" -> "/"; null when not a .md URL. */
71
+ function stripMdSuffix(pathname) {
72
+ if (!pathname.toLowerCase().endsWith(".md"))
73
+ return null;
74
+ if (pathname.toLowerCase() === "/index.md")
75
+ return "/";
76
+ const base = pathname.slice(0, -".md".length);
77
+ return base === "" || base.endsWith("/") ? null : base; // "/.md", "/docs/.md" are dotfiles
78
+ }
79
+ /** "/tool/x" -> "/tool/x.md", "/" -> "/index.md". */
80
+ function mdUrlPath(path) {
81
+ return path === "/" ? "/index.md" : `${path}.md`;
82
+ }
@@ -0,0 +1,19 @@
1
+ import { type AgentReadyConfig, type LlmsConfig, type ResolvedAgentReadyConfig } from "./config";
2
+ import { type HitStore } from "../proof/hits";
3
+ /** Private pages most sites have, never served as Markdown by the easy config. */
4
+ export declare const COMMON_PRIVATE_PATHS: readonly string[];
5
+ export type Env = Record<string, string | undefined>;
6
+ export interface EasyOptions extends Omit<Partial<AgentReadyConfig>, "llms"> {
7
+ /** false: serve no llms.txt. An object: your own (as in `defineAgentReady`). Default: from the sitemap. */
8
+ llms?: LlmsConfig | false | null;
9
+ /** Add COMMON_PRIVATE_PATHS to the exclude list. Default true. */
10
+ excludeCommon?: boolean;
11
+ /** Record every visit here too (for the dashboard), alongside any reporter. */
12
+ hits?: HitStore | null;
13
+ }
14
+ /** The site's public origin from the usual env vars, or null. */
15
+ export declare function siteFromEnv(env: Env): string | null;
16
+ /** AGENT_READY_ENABLED is "1" or "true". */
17
+ export declare function enabledFromEnv(env: Env): boolean;
18
+ export declare function createAgentReady(options?: EasyOptions, env?: Env): ResolvedAgentReadyConfig;
19
+ export declare const NO_SITE_WARNING = "agent-ready: no site URL, so it stays off. Set NEXT_PUBLIC_SITE_URL (e.g. https://example.com) or pass createAgentReady({ site }).";