@relvehq/agent-ready 0.0.0-stage → 0.3.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.
Files changed (85) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +364 -3
  3. package/SETUP.md +354 -0
  4. package/dist/bin/agent-ready.d.ts +23 -0
  5. package/dist/bin/agent-ready.js +444 -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/remote.d.ts +57 -0
  65. package/dist/refine/remote.js +238 -0
  66. package/dist/refine/run.d.ts +52 -0
  67. package/dist/refine/run.js +66 -0
  68. package/dist/refine/types.d.ts +17 -0
  69. package/dist/refine/types.js +8 -0
  70. package/dist/wp/relve-agent-ready/includes/class-relve-ar-admin.php +550 -0
  71. package/dist/wp/relve-agent-ready/includes/class-relve-ar-converter.php +762 -0
  72. package/dist/wp/relve-agent-ready/includes/class-relve-ar-llms.php +192 -0
  73. package/dist/wp/relve-agent-ready/includes/class-relve-ar-markdown-builder.php +455 -0
  74. package/dist/wp/relve-agent-ready/includes/class-relve-ar-negotiation.php +334 -0
  75. package/dist/wp/relve-agent-ready/includes/class-relve-ar-refiner.php +894 -0
  76. package/dist/wp/relve-agent-ready/includes/class-relve-ar-response.php +226 -0
  77. package/dist/wp/relve-agent-ready/includes/class-relve-ar-router.php +421 -0
  78. package/dist/wp/relve-agent-ready/includes/class-relve-ar-settings.php +201 -0
  79. package/dist/wp/relve-agent-ready/includes/class-relve-ar-store.php +583 -0
  80. package/dist/wp/relve-agent-ready/includes/index.php +2 -0
  81. package/dist/wp/relve-agent-ready/index.php +2 -0
  82. package/dist/wp/relve-agent-ready/readme.txt +108 -0
  83. package/dist/wp/relve-agent-ready/relve-agent-ready.php +81 -0
  84. package/dist/wp/relve-agent-ready/uninstall.php +35 -0
  85. package/package.json +64 -6
@@ -0,0 +1,366 @@
1
+ "use strict";
2
+ // `agent-ready init`: set a site up in one command
3
+ // (changes/agent-ready-engine, "easy setup"; changes/agent-ready-onboarding,
4
+ // "universal init").
5
+ //
6
+ // detectStack(fs) picks the path (`--stack` skips it):
7
+ // next App Router folder + `next` in package.json (or an app/ folder
8
+ // and no Node server framework, as before): planInit below
9
+ // wordpress wp-config.php or wp-content/: planWordPress copies the plugin
10
+ // bundled in the package to wp-content/plugins/relve-agent-ready/
11
+ // node express, connect, polka, fastify, @nestjs/core, h3 or
12
+ // nitropack in package.json: nothing written, formatNode prints
13
+ // the lines to add
14
+ // null none of these: formatUnknown, exit 1
15
+ //
16
+ // The Next.js path, unchanged:
17
+ // Two steps, kept apart so each can be tested on its own:
18
+ // planInit(fs) look at the project, decide every action (no writes)
19
+ // applyInit(plan, fs) carry the plan out
20
+ //
21
+ // What it sets up (App Router; `src/` layout or not; TS or JS):
22
+ // agent-ready.config.ts createAgentReady() with the defaults
23
+ // middleware.ts created when there is none; an existing one is
24
+ // NEVER edited: the plan prints the two lines to add
25
+ // app/%5F%5Fmd/[[...path]]/route.ts the internal Markdown route
26
+ // .env.local AGENT_READY_ENABLED=1 (appended once)
27
+ // Re-running is safe: whatever exists is skipped.
28
+ //
29
+ // NODE ONLY through the FS it is given (the planner itself is pure).
30
+ Object.defineProperty(exports, "__esModule", { value: true });
31
+ exports.WP_PLUGIN_PATH = exports.DETECTION_RULES = exports.STACKS = exports.ONBOARDING_URL = void 0;
32
+ exports.planInit = planInit;
33
+ exports.applyInit = applyInit;
34
+ exports.formatPlan = formatPlan;
35
+ exports.configTemplate = configTemplate;
36
+ exports.middlewareTemplate = middlewareTemplate;
37
+ exports.middlewarePatch = middlewarePatch;
38
+ exports.routeTemplate = routeTemplate;
39
+ exports.nodeFramework = nodeFramework;
40
+ exports.detectStack = detectStack;
41
+ exports.planWordPress = planWordPress;
42
+ exports.formatWordPress = formatWordPress;
43
+ exports.nodeSnippet = nodeSnippet;
44
+ exports.formatNode = formatNode;
45
+ exports.formatUnknown = formatUnknown;
46
+ const join = (...parts) => parts.filter(Boolean).join("/");
47
+ function planInit(fs, opts = {}) {
48
+ const pkg = opts.pkg ?? "@relvehq/agent-ready";
49
+ // Where the App Router lives decides where everything else goes.
50
+ const base = fs.exists("src/app") ? "src" : fs.exists("app") ? "" : null;
51
+ if (base === null) {
52
+ return {
53
+ ok: false,
54
+ error: "No App Router folder found (app/ or src/app/). Run this in a Next.js project's root.",
55
+ actions: [],
56
+ };
57
+ }
58
+ const ext = fs.exists("tsconfig.json") ? "ts" : "js";
59
+ const configFile = join(base, `agent-ready.config.${ext}`);
60
+ const routeFile = join(base, "app", "%5F%5Fmd", "[[...path]]", `route.${ext}`);
61
+ const middlewareFile = [join(base, "middleware.ts"), join(base, "middleware.js")].find((p) => fs.exists(p)) ?? null;
62
+ const actions = [];
63
+ // A middleware that already imports the package was set up (by hand or by
64
+ // an earlier run): its config lives wherever that file says.
65
+ const wired = middlewareFile !== null && /agent-ready/.test(fs.read(middlewareFile));
66
+ // 1. The config.
67
+ actions.push(fs.exists(configFile)
68
+ ? { kind: "skip", path: configFile, note: "config already there" }
69
+ : wired
70
+ ? { kind: "skip", path: configFile, note: "your middleware already has an agent-ready config" }
71
+ : { kind: "create", path: configFile, content: configTemplate(pkg), note: "settings (smart defaults; add `exclude` if needed)" });
72
+ // 2. The middleware: create, keep, or tell the person what to add.
73
+ if (!middlewareFile) {
74
+ const path = join(base, `middleware.${ext}`);
75
+ actions.push({ kind: "create", path, content: middlewareTemplate(pkg), note: "middleware that serves Markdown to agents" });
76
+ }
77
+ else if (wired) {
78
+ actions.push({ kind: "skip", path: middlewareFile, note: "middleware already uses agent-ready" });
79
+ }
80
+ else {
81
+ actions.push({ kind: "manual", path: middlewareFile, content: middlewarePatch(pkg), note: "you have a middleware: wrap it (2 lines) and add 2 matcher entries" });
82
+ }
83
+ // 3. The internal route.
84
+ actions.push(fs.exists(routeFile)
85
+ ? { kind: "skip", path: routeFile, note: "Markdown route already there" }
86
+ : { kind: "create", path: routeFile, content: routeTemplate(pkg), note: "internal route that builds the Markdown" });
87
+ // 4. The switch, and the site URL (createAgentReady stays off without one).
88
+ const envFile = ".env.local";
89
+ const envText = [".env.local", ".env"].filter((f) => fs.exists(f)).map((f) => fs.read(f)).join("\n");
90
+ const has = (name) => new RegExp(`^\\s*${name}\\s*=`, "m").test(envText);
91
+ const lines = [
92
+ has("AGENT_READY_ENABLED") ? null : "AGENT_READY_ENABLED=1",
93
+ has("NEXT_PUBLIC_SITE_URL") ? null : "NEXT_PUBLIC_SITE_URL=http://localhost:3000",
94
+ ].filter((l) => !!l);
95
+ if (!lines.length) {
96
+ actions.push({ kind: "skip", path: envFile, note: "AGENT_READY_ENABLED and NEXT_PUBLIC_SITE_URL already set" });
97
+ }
98
+ else if (!fs.exists(envFile)) {
99
+ actions.push({ kind: "create", path: envFile, content: lines.join("\n") + "\n", note: `turns it on locally (${lines.map((l) => l.split("=")[0]).join(", ")})` });
100
+ }
101
+ else {
102
+ const text = fs.read(envFile);
103
+ actions.push({
104
+ kind: "append",
105
+ path: envFile,
106
+ content: (text.endsWith("\n") || text === "" ? "" : "\n") + lines.join("\n") + "\n",
107
+ note: `turns it on locally (${lines.map((l) => l.split("=")[0]).join(", ")})`,
108
+ });
109
+ }
110
+ return { ok: true, actions };
111
+ }
112
+ /** Write what the plan says. Manual and skip actions are left to the person. */
113
+ function applyInit(plan, fs) {
114
+ const done = [];
115
+ if (!plan.ok)
116
+ return done;
117
+ for (const a of plan.actions) {
118
+ if (a.kind === "create" && a.content !== undefined) {
119
+ fs.write(a.path, a.content);
120
+ done.push(a.path);
121
+ }
122
+ else if (a.kind === "append" && a.content !== undefined) {
123
+ fs.append(a.path, a.content);
124
+ done.push(a.path);
125
+ }
126
+ else if (a.kind === "copy" && a.from !== undefined) {
127
+ if (!fs.copy)
128
+ throw new Error("agent-ready init: this file system cannot copy files");
129
+ fs.copy(a.from, a.path);
130
+ done.push(a.path);
131
+ }
132
+ }
133
+ return done;
134
+ }
135
+ /** A plan as lines for the terminal. */
136
+ function formatPlan(plan, dryRun) {
137
+ if (!plan.ok)
138
+ return `agent-ready init: ${plan.error}`;
139
+ const mark = {
140
+ create: dryRun ? "would create" : "created",
141
+ append: dryRun ? "would update" : "updated",
142
+ skip: "kept",
143
+ manual: "TODO",
144
+ copy: dryRun ? "would copy" : "copied",
145
+ };
146
+ const lines = plan.actions.map((a) => ` ${mark[a.kind].padEnd(12)} ${a.path} (${a.note})`);
147
+ const manual = plan.actions.filter((a) => a.kind === "manual");
148
+ const out = ["agent-ready init", ...lines];
149
+ for (const m of manual)
150
+ out.push("", `Add to ${m.path}:`, "", m.content ?? "");
151
+ out.push("", "Check it (dev server running):", ' curl -i -H "Accept: text/markdown" http://localhost:3000/', " curl -i http://localhost:3000/index.md", " curl -i http://localhost:3000/llms.txt", "", "Production: set AGENT_READY_ENABLED=1 (and NEXT_PUBLIC_SITE_URL) in your host's env.", "Dashboard (optional): AGENT_READY_KEY + AGENT_READY_ENDPOINT.");
152
+ return out.join("\n");
153
+ }
154
+ /* ── templates ──────────────────────────────────────────────────────── */
155
+ function configTemplate(pkg) {
156
+ return `// Agent-Ready: a clean Markdown version of every page for AI agents.
157
+ // Defaults: site from NEXT_PUBLIC_SITE_URL, on with AGENT_READY_ENABLED=1,
158
+ // common private pages skipped, /llms.txt built from your sitemap.xml.
159
+ import { createAgentReady } from "${pkg}";
160
+
161
+ export default createAgentReady({
162
+ // exclude: ["/members"], // pages never served as Markdown
163
+ });
164
+ `;
165
+ }
166
+ function middlewareTemplate(pkg) {
167
+ return `import { withAgentReady } from "${pkg}/next";
168
+ import agentReady from "./agent-ready.config";
169
+
170
+ // Markdown requests (Accept: text/markdown, .md URLs, /llms.txt) go to the
171
+ // agent-ready route; everything else carries on as normal.
172
+ export default withAgentReady(agentReady);
173
+
174
+ export const config = {
175
+ matcher: ["/((?!_next/|.*\\\\..*).*)", "/((?!_next/).+\\\\.md)", "/llms.txt"],
176
+ };
177
+ `;
178
+ }
179
+ function middlewarePatch(pkg) {
180
+ return `// 1. Wrap your middleware:
181
+ import { withAgentReady } from "${pkg}/next";
182
+ import agentReady from "./agent-ready.config";
183
+ export default withAgentReady(agentReady, yourMiddleware); // instead of exporting yourMiddleware
184
+
185
+ // 2. Add these two entries to your matcher:
186
+ // "/((?!_next/).+\\\\.md)", "/llms.txt"`;
187
+ }
188
+ function routeTemplate(pkg) {
189
+ return `// The internal route agent-ready rewrites Markdown requests to (/__md/...).
190
+ // "%5F" is "_": a folder starting with "_" is private in the App Router.
191
+ import { createMarkdownRoute } from "${pkg}/next/route";
192
+ import agentReady from "../../../agent-ready.config";
193
+
194
+ export const { GET, HEAD } = createMarkdownRoute(agentReady);
195
+ export const runtime = "nodejs";
196
+ export const dynamic = "force-dynamic";
197
+ `;
198
+ }
199
+ /* ── universal init: which stack is this? ───────────────────────────── */
200
+ /** Where a venture starts: the onboarding page. */
201
+ exports.ONBOARDING_URL = "https://relvehq.com/agent-ready";
202
+ exports.STACKS = ["next", "node", "wordpress"];
203
+ /** package.json dependency -> framework. First match wins (Nest and Nitro bring express / h3 along). */
204
+ const NODE_DEPS = [
205
+ ["@nestjs/core", "nest"],
206
+ ["nitropack", "nitro"],
207
+ ["h3", "h3"],
208
+ ["fastify", "fastify"],
209
+ ["express", "express"],
210
+ ["polka", "polka"],
211
+ ["connect", "connect"],
212
+ ];
213
+ /** What detection looks for, in order (printed when nothing matches). */
214
+ exports.DETECTION_RULES = [
215
+ "Next.js: app/ or src/app/ with `next` in package.json",
216
+ "WordPress: wp-config.php or a wp-content/ folder",
217
+ "Node server: express, connect, polka, fastify, @nestjs/core, h3 or nitropack in package.json",
218
+ ];
219
+ /** dependencies + devDependencies of ./package.json; empty when missing or unreadable. */
220
+ function packageDeps(fs) {
221
+ if (!fs.exists("package.json"))
222
+ return new Set();
223
+ try {
224
+ const pkg = JSON.parse(fs.read("package.json"));
225
+ const names = (o) => (o && typeof o === "object" ? Object.keys(o) : []);
226
+ return new Set([...names(pkg.dependencies), ...names(pkg.devDependencies)]);
227
+ }
228
+ catch {
229
+ return new Set();
230
+ }
231
+ }
232
+ /** The Node server framework package.json names, or null. */
233
+ function nodeFramework(fs) {
234
+ const deps = packageDeps(fs);
235
+ return NODE_DEPS.find(([dep]) => deps.has(dep))?.[1] ?? null;
236
+ }
237
+ function detectStack(fs) {
238
+ const deps = packageDeps(fs);
239
+ const appDir = fs.exists("src/app") ? "src/app/" : fs.exists("app") ? "app/" : null;
240
+ const framework = nodeFramework(fs);
241
+ if (appDir && deps.has("next"))
242
+ return { stack: "next", why: `${appDir} and next in package.json` };
243
+ if (fs.exists("wp-config.php"))
244
+ return { stack: "wordpress", why: "wp-config.php" };
245
+ if (fs.exists("wp-content"))
246
+ return { stack: "wordpress", why: "wp-content/" };
247
+ if (framework)
248
+ return { stack: "node", framework, why: `${NODE_DEPS.find(([, f]) => f === framework)[0]} in package.json` };
249
+ // Before universal init, any app/ folder meant Next.js: keep that.
250
+ if (appDir)
251
+ return { stack: "next", why: appDir };
252
+ // Next.js without an App Router folder: planInit says what is missing.
253
+ if (deps.has("next"))
254
+ return { stack: "next", why: "next in package.json" };
255
+ return { stack: null, why: "nothing recognised" };
256
+ }
257
+ /* ── WordPress: copy the bundled plugin ─────────────────────────────── */
258
+ exports.WP_PLUGIN_PATH = "wp-content/plugins/relve-agent-ready";
259
+ function planWordPress(fs, plugin, opts = {}) {
260
+ // The plugin's own tests never go to a site.
261
+ const files = (plugin?.files ?? []).filter((f) => !/^tests(\/|$)/.test(f));
262
+ if (!plugin || !files.includes("relve-agent-ready.php")) {
263
+ return {
264
+ ok: false,
265
+ error: `the WordPress plugin is missing from this copy of the package. Download the zip from ${exports.ONBOARDING_URL}`,
266
+ actions: [],
267
+ };
268
+ }
269
+ const installed = fs.exists(exports.WP_PLUGIN_PATH);
270
+ if (installed && !opts.force) {
271
+ return {
272
+ ok: true,
273
+ actions: [{ kind: "skip", path: exports.WP_PLUGIN_PATH, note: "already there, nothing copied; run with --force to replace it" }],
274
+ };
275
+ }
276
+ const dir = plugin.dir.replace(/\\/g, "/").replace(/\/+$/, "");
277
+ return {
278
+ ok: true,
279
+ actions: files.map((f) => ({
280
+ kind: "copy",
281
+ path: `${exports.WP_PLUGIN_PATH}/${f}`,
282
+ from: `${dir}/${f}`,
283
+ note: installed ? "replaced (--force)" : "plugin file",
284
+ })),
285
+ };
286
+ }
287
+ function formatWordPress(plan, dryRun) {
288
+ if (!plan.ok)
289
+ return `agent-ready init (WordPress): ${plan.error}`;
290
+ const mark = (k) => (k === "copy" ? (dryRun ? "would copy" : "copied") : "kept");
291
+ const copies = plan.actions.filter((a) => a.kind === "copy").length;
292
+ const out = ["agent-ready init (WordPress)", ...plan.actions.map((a) => ` ${mark(a.kind).padEnd(12)} ${a.path} (${a.note})`)];
293
+ if (copies)
294
+ out.push("", `${copies} file(s) ${dryRun ? "would be copied (dry run: nothing written)" : "copied"}.`);
295
+ out.push("", "Then, in WordPress admin:", ' 1. Plugins: activate "Relve Agent Ready" (it is installed switched off).', " 2. Settings > Agent Ready: tick Enabled, then Save.", "", "Check it (swap in a real post URL):", ' curl -i -H "Accept: text/markdown" https://your-site.com/your-post/', ' curl -i "https://your-site.com/your-post/?format=md"', "", `Guide: ${exports.ONBOARDING_URL}`);
296
+ return out.join("\n");
297
+ }
298
+ /* ── Node server: print the lines, write nothing ────────────────────── */
299
+ const FRAMEWORK_NAME = {
300
+ express: "Express",
301
+ connect: "Connect",
302
+ polka: "Polka",
303
+ fastify: "Fastify",
304
+ nest: "NestJS",
305
+ h3: "h3",
306
+ nitro: "Nitro",
307
+ };
308
+ /** The exact lines to add for a framework (null: the Express form, which also fits Connect, Polka and Nest). */
309
+ function nodeSnippet(framework, pkg = "@relvehq/agent-ready") {
310
+ const imports = [`import { createAgentReady } from "${pkg}";`, `import { agentReadyNode } from "${pkg}/node";`];
311
+ const mw = "agentReadyNode(createAgentReady({ site: process.env.SITE_URL }))";
312
+ switch (framework) {
313
+ case "fastify":
314
+ return [
315
+ 'import middie from "@fastify/middie";',
316
+ ...imports,
317
+ "",
318
+ "await fastify.register(middie);",
319
+ `fastify.use(${mw});`,
320
+ ].join("\n");
321
+ case "nitro":
322
+ return [
323
+ "// server/middleware/agent-ready.ts",
324
+ 'import { fromNodeMiddleware } from "h3";',
325
+ ...imports,
326
+ "",
327
+ `export default fromNodeMiddleware(${mw});`,
328
+ ].join("\n");
329
+ case "h3":
330
+ return ['import { fromNodeMiddleware } from "h3";', ...imports, "", `app.use(fromNodeMiddleware(${mw}));`].join("\n");
331
+ case "nest":
332
+ return [...imports, "", "// main.ts, after NestFactory.create(...) and before app.listen(...):", `app.use(${mw});`].join("\n");
333
+ default:
334
+ return [...imports, "", `app.use(${mw});`].join("\n");
335
+ }
336
+ }
337
+ function formatNode(framework, pkg = "@relvehq/agent-ready") {
338
+ const name = framework ? FRAMEWORK_NAME[framework] : "Node server";
339
+ const install = framework === "fastify" ? `npm install ${pkg} @fastify/middie` : `npm install ${pkg}`;
340
+ const indent = (s) => s.split("\n").map((l) => (l ? " " + l : l)).join("\n");
341
+ const out = [
342
+ `agent-ready init (${name}): nothing written. Add these lines yourself.`,
343
+ "",
344
+ "1. Install:",
345
+ ` ${install}`,
346
+ "",
347
+ "2. Mount it before static files and routes:",
348
+ "",
349
+ indent(nodeSnippet(framework, pkg)),
350
+ ];
351
+ if (!framework) {
352
+ out.push("", " Express, Connect, Polka and NestJS use that app.use(...) line.", " Fastify: register @fastify/middie first, then fastify.use(...) with the same middleware.", ' Nitro / h3: wrap it, fromNodeMiddleware(agentReadyNode(...)) from "h3".');
353
+ }
354
+ out.push("", "3. Set these env vars (.env locally, your host's settings in production):", " AGENT_READY_ENABLED=1", " SITE_URL=https://your-site.com", "", "Check it (server running; change the port to yours):", ' curl -i -H "Accept: text/markdown" http://localhost:3000/', " curl -i http://localhost:3000/index.md", " curl -i http://localhost:3000/llms.txt", "", `Guide: ${exports.ONBOARDING_URL}`);
355
+ return out.join("\n");
356
+ }
357
+ /* ── nothing recognised ─────────────────────────────────────────────── */
358
+ function formatUnknown() {
359
+ return [
360
+ "agent-ready init: no supported stack found in this folder. Looked for:",
361
+ ...exports.DETECTION_RULES.map((r) => ` - ${r}`),
362
+ "",
363
+ "Run it in your project's root, or pick one with --stack next|node|wordpress.",
364
+ `Other stacks (Hono, Bun, Deno, Workers...): ${exports.ONBOARDING_URL}`,
365
+ ].join("\n");
366
+ }
@@ -0,0 +1,8 @@
1
+ export type Format = "markdown" | "html";
2
+ export interface MediaRange {
3
+ type: string;
4
+ subtype: string;
5
+ q: number;
6
+ }
7
+ export declare function chooseFormat(accept: string | null | undefined): Format;
8
+ export declare function parseAccept(accept: string): MediaRange[];
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ // Accept-header negotiation: does this request prefer Markdown over HTML?
3
+ //
4
+ // Ported unchanged in behaviour from the Relve Markdown POC
5
+ // (src/lib/mdPoc/accept.ts, checked by scripts/verify-md-poc-accept.mjs).
6
+ // The POC keeps its own copy so its verify script stays independent; the
7
+ // cases are mirrored in packages/agent-ready/test/detect.test.ts.
8
+ //
9
+ // Rule: serve Markdown only when `text/markdown` is listed explicitly and its
10
+ // q-value is greater than or equal to the q-value that applies to `text/html`.
11
+ // Ties go to Markdown (Claude Code, Copilot CLI and Microsoft Copilot send
12
+ // `text/markdown, text/html, */*` at equal weight). Wildcards never select
13
+ // Markdown on their own, but they do count towards HTML's q-value.
14
+ //
15
+ // The user agent is never an input: choosing the format by user agent is the
16
+ // cloaking pattern search engines warn about.
17
+ //
18
+ // EDGE-SAFE. No Node APIs.
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.chooseFormat = chooseFormat;
21
+ exports.parseAccept = parseAccept;
22
+ // Longer headers are treated as absent (HTML), so a hostile header can't cost
23
+ // much CPU. Real clients send well under 1 KB.
24
+ const MAX_ACCEPT_LENGTH = 4096;
25
+ function chooseFormat(accept) {
26
+ if (!accept || accept.length > MAX_ACCEPT_LENGTH)
27
+ return "html";
28
+ const ranges = parseAccept(accept);
29
+ const markdownQ = explicitQ(ranges, "text", "markdown");
30
+ if (markdownQ === undefined || markdownQ <= 0)
31
+ return "html";
32
+ return markdownQ >= htmlQ(ranges) ? "markdown" : "html";
33
+ }
34
+ function parseAccept(accept) {
35
+ const ranges = [];
36
+ for (const part of accept.split(",")) {
37
+ const [mediaType = "", ...params] = part.split(";");
38
+ const [type, subtype, ...rest] = mediaType.trim().toLowerCase().split("/");
39
+ if (!type || !subtype || rest.length > 0)
40
+ continue;
41
+ let q = 1;
42
+ for (const param of params) {
43
+ const [name = "", value = ""] = param.split("=");
44
+ if (name.trim().toLowerCase() !== "q")
45
+ continue;
46
+ q = parseQ(value.trim());
47
+ }
48
+ ranges.push({ type, subtype, q });
49
+ }
50
+ return ranges;
51
+ }
52
+ // RFC 9110 §12.4.2: qvalue = ( "0" [ "." 0*3DIGIT ] ) / ( "1" [ "." 0*3("0") ] ).
53
+ // A malformed q-value makes the range unacceptable (q=0) rather than
54
+ // preferred, so a broken header can never tip the result towards Markdown.
55
+ function parseQ(value) {
56
+ if (!/^(0(\.\d{0,3})?|1(\.0{0,3})?)$/.test(value))
57
+ return 0;
58
+ return Number(value);
59
+ }
60
+ // The highest q given to exactly type/subtype, or undefined if not listed.
61
+ function explicitQ(ranges, type, subtype) {
62
+ let best;
63
+ for (const r of ranges) {
64
+ if (r.type === type && r.subtype === subtype)
65
+ best = Math.max(best ?? 0, r.q);
66
+ }
67
+ return best;
68
+ }
69
+ // The q that applies to text/html: the most specific matching range wins
70
+ // (text/html, then text/*, then */*). Unlisted means 0.
71
+ function htmlQ(ranges) {
72
+ return (explicitQ(ranges, "text", "html") ??
73
+ explicitQ(ranges, "text", "*") ??
74
+ explicitQ(ranges, "*", "*") ??
75
+ 0);
76
+ }
@@ -0,0 +1,145 @@
1
+ export type Awaitable<T> = T | Promise<T>;
2
+ /** A Markdown document plus the facts the front matter is built from. */
3
+ export interface MarkdownDoc {
4
+ markdown: string;
5
+ title?: string;
6
+ description?: string;
7
+ lang?: string;
8
+ /** Absolute canonical URL of the HTML page; defaults to site + path. */
9
+ canonical?: string;
10
+ /** JSON-LD objects found on the page (convert fills this in). */
11
+ jsonLd?: unknown[];
12
+ }
13
+ /** What a template or store may return. A plain string is the Markdown body. */
14
+ export type DocResult = string | MarkdownDoc | null | undefined;
15
+ export interface TemplateContext {
16
+ /** Normalised page path, no `.md`, e.g. "/tool/notion". */
17
+ path: string;
18
+ /** Named segments from the pattern, e.g. { slug: "notion" } for "/tool/:slug". */
19
+ params: Record<string, string>;
20
+ /** Query string of the original request (without `format`), "" or "?a=b". */
21
+ search: string;
22
+ request: Request;
23
+ }
24
+ export type TemplateFn = (ctx: TemplateContext) => Awaitable<DocResult>;
25
+ export interface MarkdownStore {
26
+ get(path: string, ctx: {
27
+ request: Request;
28
+ }): Awaitable<DocResult>;
29
+ }
30
+ export interface LlmsEntry {
31
+ title: string;
32
+ /** Absolute, or a path resolved against `site`. */
33
+ url: string;
34
+ description?: string;
35
+ /** H2 section the link is listed under. Defaults to "Pages". */
36
+ section?: string;
37
+ /** Listed under "## Optional" (llms.txt convention: skippable context). */
38
+ optional?: boolean;
39
+ /** Link the `.md` variant of this same-site page. Default true; false for pages the package doesn't serve. */
40
+ markdown?: boolean;
41
+ }
42
+ export interface LlmsConfig {
43
+ /** Path the file is served at. Default "/llms.txt". */
44
+ path?: string;
45
+ /** H1 of the file: the site or project name. */
46
+ title: string;
47
+ /** Blockquote summary under the H1. */
48
+ summary?: string;
49
+ /** Free Markdown paragraphs between the summary and the link lists. */
50
+ details?: string;
51
+ entries: (ctx: {
52
+ request: Request;
53
+ }) => Awaitable<LlmsEntry[]>;
54
+ }
55
+ export type Via = "accept" | "suffix" | "query" | "llms" | "direct";
56
+ export type Source = "native" | "stored" | "convert" | "llms";
57
+ /** One served (or refused) Markdown request, passed to `onHit`. */
58
+ export interface HitEvent {
59
+ ts: string;
60
+ host: string | null;
61
+ path: string;
62
+ via: Via;
63
+ source: Source | null;
64
+ status: number;
65
+ tokens: number | null;
66
+ bytes: number | null;
67
+ userAgent: string | null;
68
+ accept: string | null;
69
+ durationMs: number;
70
+ }
71
+ export interface AgentReadyConfig {
72
+ /** Public origin of the site, e.g. "https://relvehq.com". Used for canonicals and links. */
73
+ site: string;
74
+ /** Extra path prefixes (segment-aware) or RegExps never served as Markdown. */
75
+ exclude?: Array<string | RegExp>;
76
+ /** Native templates by route pattern ("/tool/:slug", "/docs/*"). First match wins. */
77
+ templates?: Record<string, TemplateFn>;
78
+ /** Stored Markdown lookup, tried after templates. */
79
+ store?: MarkdownStore | null;
80
+ /** What to do when no template or stored doc exists. Default "convert". */
81
+ fallback?: "convert" | "none";
82
+ /**
83
+ * Cache-Control for Markdown served at its own URL (`.md`, `?format=md`,
84
+ * the internal path, llms.txt). Default "public, s-maxage=300, stale-while-revalidate=86400".
85
+ */
86
+ cacheControl?: string;
87
+ /**
88
+ * Cache-Control for Markdown negotiated by Accept on the page's own URL.
89
+ * Default "private, max-age=300": most CDNs (CloudFront included) do not
90
+ * key on Accept, so a shared cache must never store this variant or it
91
+ * would be served to browsers. See README "Caching".
92
+ */
93
+ negotiatedCacheControl?: string;
94
+ /** llms.txt generation. Null/undefined = the package does not serve one. */
95
+ llms?: LlmsConfig | null;
96
+ /** Fire-and-forget logger. Errors are swallowed. */
97
+ onHit?: (event: HitEvent) => Awaitable<void>;
98
+ /** Master switch. Default false: nothing happens until a venture opts in. */
99
+ enabled?: boolean | ((request: Request) => boolean);
100
+ /** Internal rewrite prefix. Default "/__md". */
101
+ internalPrefix?: string;
102
+ /** Header marking the package's own origin fetch. Default "x-agent-ready-internal". */
103
+ internalHeader?: string;
104
+ /** Origin the converter fetches HTML from. Default: derived from the request's host. */
105
+ fetchOrigin?: (request: Request) => string;
106
+ /** Origin fetch timeout. Default 8000 ms. */
107
+ fetchTimeoutMs?: number;
108
+ /** Quality gate: converted Markdown with less plain text than this is discarded. Default 200. */
109
+ minChars?: number;
110
+ }
111
+ export interface ResolvedAgentReadyConfig {
112
+ readonly __resolved: true;
113
+ site: string;
114
+ exclude: Array<string | RegExp>;
115
+ templates: Record<string, TemplateFn>;
116
+ store: MarkdownStore | null;
117
+ fallback: "convert" | "none";
118
+ cacheControl: string;
119
+ negotiatedCacheControl: string;
120
+ llms: (LlmsConfig & {
121
+ path: string;
122
+ }) | null;
123
+ onHit: ((event: HitEvent) => Awaitable<void>) | null;
124
+ enabled: boolean | ((request: Request) => boolean);
125
+ internalPrefix: string;
126
+ internalHeader: string;
127
+ fetchOrigin: ((request: Request) => string) | null;
128
+ fetchTimeoutMs: number;
129
+ minChars: number;
130
+ }
131
+ /** Never served as Markdown, whatever the venture's config says. */
132
+ export declare const ALWAYS_EXCLUDE: readonly string[];
133
+ export declare const DEFAULT_CACHE_CONTROL = "public, s-maxage=300, stale-while-revalidate=86400";
134
+ export declare const DEFAULT_NEGOTIATED_CACHE_CONTROL = "private, max-age=300";
135
+ export declare const DEFAULT_INTERNAL_PREFIX = "/__md";
136
+ export declare const DEFAULT_INTERNAL_HEADER = "x-agent-ready-internal";
137
+ /** Request header the middleware adapter sets on its rewrite: how Markdown was asked for. */
138
+ export declare const VIA_HEADER = "x-agent-ready-via";
139
+ export declare function defineAgentReady(config: AgentReadyConfig | ResolvedAgentReadyConfig): ResolvedAgentReadyConfig;
140
+ /** The master switch. A throwing `enabled` function counts as off. */
141
+ export declare function isEnabled(cfg: ResolvedAgentReadyConfig, request: Request): boolean;
142
+ /** Segment-aware prefix match: "/admin" excludes "/admin" and "/admin/x", not "/administrator". */
143
+ export declare function isExcluded(cfg: ResolvedAgentReadyConfig, path: string): boolean;
144
+ /** Fire-and-forget: never awaited by callers, never throws. */
145
+ export declare function emitHit(cfg: ResolvedAgentReadyConfig, event: HitEvent): void;