@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.
- package/LICENSE +21 -0
- package/README.md +338 -3
- package/SETUP.md +337 -0
- package/dist/bin/agent-ready.d.ts +23 -0
- package/dist/bin/agent-ready.js +350 -0
- package/dist/cli/init.d.ts +70 -0
- package/dist/cli/init.js +366 -0
- package/dist/core/accept.d.ts +8 -0
- package/dist/core/accept.js +76 -0
- package/dist/core/config.d.ts +145 -0
- package/dist/core/config.js +103 -0
- package/dist/core/convert.d.ts +22 -0
- package/dist/core/convert.js +334 -0
- package/dist/core/detect.d.ts +30 -0
- package/dist/core/detect.js +82 -0
- package/dist/core/easy.d.ts +19 -0
- package/dist/core/easy.js +117 -0
- package/dist/core/enrich.d.ts +30 -0
- package/dist/core/enrich.js +155 -0
- package/dist/core/llms.d.ts +11 -0
- package/dist/core/llms.js +71 -0
- package/dist/core/report.d.ts +15 -0
- package/dist/core/report.js +46 -0
- package/dist/core/resolve.d.ts +43 -0
- package/dist/core/resolve.js +204 -0
- package/dist/core/respond.d.ts +18 -0
- package/dist/core/respond.js +59 -0
- package/dist/core/sitemap.d.ts +31 -0
- package/dist/core/sitemap.js +140 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +76 -0
- package/dist/next/dashboard.d.ts +14 -0
- package/dist/next/dashboard.js +56 -0
- package/dist/next/index.d.ts +42 -0
- package/dist/next/index.js +100 -0
- package/dist/next/route.d.ts +26 -0
- package/dist/next/route.js +167 -0
- package/dist/node/index.d.ts +8 -0
- package/dist/node/index.js +191 -0
- package/dist/proof/audit.d.ts +47 -0
- package/dist/proof/audit.js +253 -0
- package/dist/proof/bots.d.ts +24 -0
- package/dist/proof/bots.js +60 -0
- package/dist/proof/dashboard.d.ts +8 -0
- package/dist/proof/dashboard.js +77 -0
- package/dist/proof/hits.d.ts +18 -0
- package/dist/proof/hits.js +51 -0
- package/dist/proof/jsonl.d.ts +5 -0
- package/dist/proof/jsonl.js +80 -0
- package/dist/proof/stats.d.ts +39 -0
- package/dist/proof/stats.js +127 -0
- package/dist/refine/check.d.ts +17 -0
- package/dist/refine/check.js +134 -0
- package/dist/refine/extract.d.ts +7 -0
- package/dist/refine/extract.js +120 -0
- package/dist/refine/file-store.d.ts +6 -0
- package/dist/refine/file-store.js +87 -0
- package/dist/refine/model.d.ts +30 -0
- package/dist/refine/model.js +105 -0
- package/dist/refine/prompts.d.ts +49 -0
- package/dist/refine/prompts.js +178 -0
- package/dist/refine/refine.d.ts +38 -0
- package/dist/refine/refine.js +75 -0
- package/dist/refine/run.d.ts +52 -0
- package/dist/refine/run.js +66 -0
- package/dist/refine/types.d.ts +17 -0
- package/dist/refine/types.js +8 -0
- package/dist/wp/relve-agent-ready/includes/class-relve-ar-admin.php +363 -0
- package/dist/wp/relve-agent-ready/includes/class-relve-ar-converter.php +762 -0
- package/dist/wp/relve-agent-ready/includes/class-relve-ar-llms.php +192 -0
- package/dist/wp/relve-agent-ready/includes/class-relve-ar-markdown-builder.php +447 -0
- package/dist/wp/relve-agent-ready/includes/class-relve-ar-negotiation.php +334 -0
- package/dist/wp/relve-agent-ready/includes/class-relve-ar-response.php +226 -0
- package/dist/wp/relve-agent-ready/includes/class-relve-ar-router.php +421 -0
- package/dist/wp/relve-agent-ready/includes/class-relve-ar-settings.php +115 -0
- package/dist/wp/relve-agent-ready/includes/class-relve-ar-store.php +543 -0
- package/dist/wp/relve-agent-ready/includes/index.php +2 -0
- package/dist/wp/relve-agent-ready/index.php +2 -0
- package/dist/wp/relve-agent-ready/readme.txt +75 -0
- package/dist/wp/relve-agent-ready/relve-agent-ready.php +70 -0
- package/dist/wp/relve-agent-ready/uninstall.php +24 -0
- package/package.json +64 -6
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { NextResponse, type NextFetchEvent, type NextRequest } from "next/server";
|
|
2
|
+
import { type AgentReadyConfig, type ResolvedAgentReadyConfig } from "../core/config";
|
|
3
|
+
import { type Detection } from "../core/detect";
|
|
4
|
+
export interface AgentReadyMatcher {
|
|
5
|
+
config: ResolvedAgentReadyConfig;
|
|
6
|
+
/** A rewrite to the internal Markdown route, or null to carry on as normal. */
|
|
7
|
+
match(req: NextRequest): NextResponse | null;
|
|
8
|
+
/** The decision without the response, for logging or tests. */
|
|
9
|
+
detect(req: NextRequest): Detection | null;
|
|
10
|
+
/** True for the package's own origin fetch (the internal header is present). */
|
|
11
|
+
isInternal(req: Request): boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare function agentReady(config: AgentReadyConfig | ResolvedAgentReadyConfig): AgentReadyMatcher;
|
|
14
|
+
/** Any Next.js middleware: may return a response, nothing, or a promise of either. */
|
|
15
|
+
export type Middleware = (req: NextRequest, event: NextFetchEvent) => Response | NextResponse | null | undefined | void | Promise<Response | NextResponse | null | undefined | void>;
|
|
16
|
+
/**
|
|
17
|
+
* The one-line middleware (changes/agent-ready-engine, "easy setup").
|
|
18
|
+
*
|
|
19
|
+
* // middleware.ts, no middleware of your own
|
|
20
|
+
* export default withAgentReady(agentReadyConfig);
|
|
21
|
+
*
|
|
22
|
+
* // middleware.ts, wrapping the one you have
|
|
23
|
+
* export default withAgentReady(agentReadyConfig, myMiddleware);
|
|
24
|
+
*
|
|
25
|
+
* A Markdown request is rewritten to the internal route; anything else goes
|
|
26
|
+
* to your middleware unchanged (or straight on, when there is none). Your
|
|
27
|
+
* middleware never sees Markdown requests, and the package's own origin fetch
|
|
28
|
+
* still reaches it as a normal request.
|
|
29
|
+
*/
|
|
30
|
+
export declare function withAgentReady(config: AgentReadyConfig | ResolvedAgentReadyConfig, next?: Middleware): Middleware;
|
|
31
|
+
/**
|
|
32
|
+
* The matcher entries a Markdown-serving middleware needs, for reference and
|
|
33
|
+
* for the init tool. Next.js reads `export const config = { matcher }`
|
|
34
|
+
* statically, so copy them into your file as literals (importing this array
|
|
35
|
+
* does not work there):
|
|
36
|
+
* "/((?!_next/|.*\\..*).*)" pages (no dot in the path)
|
|
37
|
+
* "/((?!_next/).+\\.md)" .md URLs
|
|
38
|
+
* "/llms.txt" the generated llms.txt
|
|
39
|
+
*/
|
|
40
|
+
export declare const AGENT_READY_MATCHER: readonly string[];
|
|
41
|
+
/** "/tool/x" -> "/__md/tool/x", "/" -> "/__md". */
|
|
42
|
+
export declare function internalPath(cfg: ResolvedAgentReadyConfig, path: string): string;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Next.js middleware adapter for @relvehq/agent-ready.
|
|
3
|
+
//
|
|
4
|
+
// const md = agentReady(config);
|
|
5
|
+
// export function middleware(req: NextRequest) {
|
|
6
|
+
// const hit = md.match(req);
|
|
7
|
+
// if (hit) return hit;
|
|
8
|
+
// ...
|
|
9
|
+
// }
|
|
10
|
+
//
|
|
11
|
+
// `match()` returns a NextResponse.rewrite to the internal route
|
|
12
|
+
// `/__md/<path>` (query preserved) when the request wants Markdown, or null.
|
|
13
|
+
// It returns null, and does no other work, when the package is disabled.
|
|
14
|
+
//
|
|
15
|
+
// The route handler lives in "@relvehq/agent-ready/next/route" (markdownRoute),
|
|
16
|
+
// NOT here: it pulls in linkedom/turndown, which must never be bundled into
|
|
17
|
+
// the Edge middleware.
|
|
18
|
+
//
|
|
19
|
+
// EDGE-SAFE.
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.AGENT_READY_MATCHER = void 0;
|
|
22
|
+
exports.agentReady = agentReady;
|
|
23
|
+
exports.withAgentReady = withAgentReady;
|
|
24
|
+
exports.internalPath = internalPath;
|
|
25
|
+
const server_1 = require("next/server");
|
|
26
|
+
const config_1 = require("../core/config");
|
|
27
|
+
const detect_1 = require("../core/detect");
|
|
28
|
+
function agentReady(config) {
|
|
29
|
+
const cfg = (0, config_1.defineAgentReady)(config);
|
|
30
|
+
const isInternal = (req) => req.headers.has(cfg.internalHeader);
|
|
31
|
+
const detect = (req) => {
|
|
32
|
+
if (req.method !== "GET" && req.method !== "HEAD")
|
|
33
|
+
return null;
|
|
34
|
+
if (isInternal(req))
|
|
35
|
+
return null;
|
|
36
|
+
const pathname = req.nextUrl.pathname;
|
|
37
|
+
if (pathname === cfg.internalPrefix || pathname.startsWith(cfg.internalPrefix + "/"))
|
|
38
|
+
return null;
|
|
39
|
+
if (!(0, config_1.isEnabled)(cfg, req))
|
|
40
|
+
return null;
|
|
41
|
+
const d = (0, detect_1.wantsMarkdown)(req, { llmsPath: cfg.llms ? cfg.llms.path : null });
|
|
42
|
+
if (!d.markdown)
|
|
43
|
+
return null;
|
|
44
|
+
if (!d.llms && (0, config_1.isExcluded)(cfg, d.path))
|
|
45
|
+
return null;
|
|
46
|
+
return d;
|
|
47
|
+
};
|
|
48
|
+
return {
|
|
49
|
+
config: cfg,
|
|
50
|
+
detect,
|
|
51
|
+
isInternal,
|
|
52
|
+
match(req) {
|
|
53
|
+
const d = detect(req);
|
|
54
|
+
if (!d)
|
|
55
|
+
return null;
|
|
56
|
+
const target = new URL(internalPath(cfg, d.path), req.url);
|
|
57
|
+
target.search = d.search;
|
|
58
|
+
const headers = new Headers(req.headers);
|
|
59
|
+
headers.set(config_1.VIA_HEADER, d.via ?? "direct");
|
|
60
|
+
return server_1.NextResponse.rewrite(target, { request: { headers } });
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The one-line middleware (changes/agent-ready-engine, "easy setup").
|
|
66
|
+
*
|
|
67
|
+
* // middleware.ts, no middleware of your own
|
|
68
|
+
* export default withAgentReady(agentReadyConfig);
|
|
69
|
+
*
|
|
70
|
+
* // middleware.ts, wrapping the one you have
|
|
71
|
+
* export default withAgentReady(agentReadyConfig, myMiddleware);
|
|
72
|
+
*
|
|
73
|
+
* A Markdown request is rewritten to the internal route; anything else goes
|
|
74
|
+
* to your middleware unchanged (or straight on, when there is none). Your
|
|
75
|
+
* middleware never sees Markdown requests, and the package's own origin fetch
|
|
76
|
+
* still reaches it as a normal request.
|
|
77
|
+
*/
|
|
78
|
+
function withAgentReady(config, next) {
|
|
79
|
+
const md = agentReady(config);
|
|
80
|
+
return (req, event) => {
|
|
81
|
+
const hit = md.match(req);
|
|
82
|
+
if (hit)
|
|
83
|
+
return hit;
|
|
84
|
+
return next ? next(req, event) : server_1.NextResponse.next();
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The matcher entries a Markdown-serving middleware needs, for reference and
|
|
89
|
+
* for the init tool. Next.js reads `export const config = { matcher }`
|
|
90
|
+
* statically, so copy them into your file as literals (importing this array
|
|
91
|
+
* does not work there):
|
|
92
|
+
* "/((?!_next/|.*\\..*).*)" pages (no dot in the path)
|
|
93
|
+
* "/((?!_next/).+\\.md)" .md URLs
|
|
94
|
+
* "/llms.txt" the generated llms.txt
|
|
95
|
+
*/
|
|
96
|
+
exports.AGENT_READY_MATCHER = ["/((?!_next/|.*\\..*).*)", "/((?!_next/).+\\.md)", "/llms.txt"];
|
|
97
|
+
/** "/tool/x" -> "/__md/tool/x", "/" -> "/__md". */
|
|
98
|
+
function internalPath(cfg, path) {
|
|
99
|
+
return path === "/" ? cfg.internalPrefix : `${cfg.internalPrefix}${path}`;
|
|
100
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type AgentReadyConfig, type ResolvedAgentReadyConfig } from "../core/config";
|
|
2
|
+
import { type ResolveDeps } from "../core/resolve";
|
|
3
|
+
type RouteContext = {
|
|
4
|
+
params?: {
|
|
5
|
+
path?: string[];
|
|
6
|
+
};
|
|
7
|
+
};
|
|
8
|
+
export declare function markdownRoute(config: AgentReadyConfig | ResolvedAgentReadyConfig, deps?: ResolveDeps): (req: Request, ctx?: RouteContext) => Promise<Response>;
|
|
9
|
+
/**
|
|
10
|
+
* The route file in one line (changes/agent-ready-engine, "easy setup"):
|
|
11
|
+
*
|
|
12
|
+
* // app/%5F%5Fmd/[[...path]]/route.ts
|
|
13
|
+
* export const { GET, HEAD } = createMarkdownRoute(config);
|
|
14
|
+
* export const runtime = "nodejs"; // literals: Next.js reads these
|
|
15
|
+
* export const dynamic = "force-dynamic"; // statically, not from a spread
|
|
16
|
+
*
|
|
17
|
+
* `runtime` and `dynamic` are returned too, for frameworks that read them at
|
|
18
|
+
* run time; the init tool writes the two literal lines for Next.js.
|
|
19
|
+
*/
|
|
20
|
+
export declare function createMarkdownRoute(config: AgentReadyConfig | ResolvedAgentReadyConfig, deps?: ResolveDeps): {
|
|
21
|
+
GET: (req: Request, ctx?: RouteContext) => Promise<Response>;
|
|
22
|
+
HEAD: (req: Request, ctx?: RouteContext) => Promise<Response>;
|
|
23
|
+
runtime: "nodejs";
|
|
24
|
+
dynamic: "force-dynamic";
|
|
25
|
+
};
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Next.js route handler for @relvehq/agent-ready.
|
|
3
|
+
//
|
|
4
|
+
// // app/%5F%5Fmd/[[...path]]/route.ts (serves the URL /__md/...)
|
|
5
|
+
// import { markdownRoute } from "@relvehq/agent-ready/next/route";
|
|
6
|
+
// export const GET = markdownRoute(config);
|
|
7
|
+
// export const HEAD = GET;
|
|
8
|
+
// export const runtime = "nodejs";
|
|
9
|
+
// export const dynamic = "force-dynamic";
|
|
10
|
+
//
|
|
11
|
+
// Runs resolve -> enrich -> respond, and serves llms.txt. Disabled -> 404.
|
|
12
|
+
// When no Markdown can be produced:
|
|
13
|
+
// - Accept-negotiated requests get the page's HTML (the origin response,
|
|
14
|
+
// passed through), so an agent is never worse off than a browser;
|
|
15
|
+
// - `.md` / `?format=md` requests get a plain-text 404 (or the origin's 4xx).
|
|
16
|
+
//
|
|
17
|
+
// NODE ONLY (the convert step uses linkedom/turndown).
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.markdownRoute = markdownRoute;
|
|
20
|
+
exports.createMarkdownRoute = createMarkdownRoute;
|
|
21
|
+
const config_1 = require("../core/config");
|
|
22
|
+
const detect_1 = require("../core/detect");
|
|
23
|
+
const enrich_1 = require("../core/enrich");
|
|
24
|
+
const llms_1 = require("../core/llms");
|
|
25
|
+
const respond_1 = require("../core/respond");
|
|
26
|
+
const resolve_1 = require("../core/resolve");
|
|
27
|
+
const VIAS = new Set(["accept", "suffix", "query", "llms", "direct"]);
|
|
28
|
+
function markdownRoute(config, deps = {}) {
|
|
29
|
+
const cfg = (0, config_1.defineAgentReady)(config);
|
|
30
|
+
return async function GET(req, ctx) {
|
|
31
|
+
const started = Date.now();
|
|
32
|
+
if (!(0, config_1.isEnabled)(cfg, req))
|
|
33
|
+
return plain(404, "Not found");
|
|
34
|
+
const url = new URL(req.url);
|
|
35
|
+
const path = pagePath(cfg, url.pathname, ctx);
|
|
36
|
+
const viaHeader = (req.headers.get(config_1.VIA_HEADER) ?? "direct");
|
|
37
|
+
const via = VIAS.has(viaHeader) ? viaHeader : "direct";
|
|
38
|
+
const hit = (status, source, tokens, bytes) => (0, config_1.emitHit)(cfg, {
|
|
39
|
+
ts: new Date().toISOString(),
|
|
40
|
+
host: req.headers.get("x-forwarded-host") ?? req.headers.get("host"),
|
|
41
|
+
path: path ?? url.pathname,
|
|
42
|
+
via,
|
|
43
|
+
source,
|
|
44
|
+
status,
|
|
45
|
+
tokens,
|
|
46
|
+
bytes,
|
|
47
|
+
userAgent: req.headers.get("user-agent")?.slice(0, 512) ?? null,
|
|
48
|
+
accept: req.headers.get("accept")?.slice(0, 1024) ?? null,
|
|
49
|
+
durationMs: Date.now() - started,
|
|
50
|
+
});
|
|
51
|
+
if (!path) {
|
|
52
|
+
hit(404, null, null, null);
|
|
53
|
+
return plain(404, "Not found");
|
|
54
|
+
}
|
|
55
|
+
// llms.txt
|
|
56
|
+
if (cfg.llms && path === cfg.llms.path) {
|
|
57
|
+
const entries = await Promise.resolve(cfg.llms.entries({ request: req })).catch(() => []);
|
|
58
|
+
const body = (0, llms_1.buildLlmsTxt)({
|
|
59
|
+
site: cfg.site,
|
|
60
|
+
title: cfg.llms.title,
|
|
61
|
+
summary: cfg.llms.summary,
|
|
62
|
+
details: cfg.llms.details,
|
|
63
|
+
entries,
|
|
64
|
+
});
|
|
65
|
+
const res = await (0, respond_1.markdownResponse)(body, {
|
|
66
|
+
request: req,
|
|
67
|
+
canonical: cfg.site + cfg.llms.path,
|
|
68
|
+
tokens: Math.ceil(body.length / 4),
|
|
69
|
+
cacheControl: cfg.cacheControl,
|
|
70
|
+
contentType: "text/plain; charset=utf-8",
|
|
71
|
+
headers: { "x-agent-ready-source": "llms" },
|
|
72
|
+
});
|
|
73
|
+
// llms.txt is meant to be found and read; it is not a duplicate of a page.
|
|
74
|
+
res.headers.delete("x-robots-tag");
|
|
75
|
+
res.headers.delete("link");
|
|
76
|
+
hit(res.status, "llms", Math.ceil(body.length / 4), (0, respond_1.byteLength)(body));
|
|
77
|
+
return res;
|
|
78
|
+
}
|
|
79
|
+
if ((0, config_1.isExcluded)(cfg, path)) {
|
|
80
|
+
hit(404, null, null, null);
|
|
81
|
+
return plain(404, "Not found");
|
|
82
|
+
}
|
|
83
|
+
// Keep the query (minus `format`) so query-driven pages convert as asked.
|
|
84
|
+
const params = new URLSearchParams(url.searchParams);
|
|
85
|
+
params.delete("format");
|
|
86
|
+
const qs = params.toString();
|
|
87
|
+
const search = qs ? `?${qs}` : "";
|
|
88
|
+
const resolved = await (0, resolve_1.resolveMarkdown)(cfg, { path, search, request: req }, deps);
|
|
89
|
+
const canonical = cfg.site + (path === "/" ? "" : path);
|
|
90
|
+
const cacheControl = via === "accept" ? cfg.negotiatedCacheControl : cfg.cacheControl;
|
|
91
|
+
if (resolved.source === null) {
|
|
92
|
+
if (via === "accept" && resolved.html) {
|
|
93
|
+
// Fall back to the HTML the browser would have got.
|
|
94
|
+
const { status, contentType, body } = resolved.html;
|
|
95
|
+
hit(status, null, null, (0, respond_1.byteLength)(body));
|
|
96
|
+
return new Response(req.method === "HEAD" ? null : body, {
|
|
97
|
+
status,
|
|
98
|
+
headers: {
|
|
99
|
+
"content-type": contentType,
|
|
100
|
+
vary: "Accept",
|
|
101
|
+
"cache-control": cfg.negotiatedCacheControl,
|
|
102
|
+
"x-agent-ready-source": "html-fallback",
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
const status = resolved.originStatus && resolved.originStatus >= 400 && resolved.originStatus < 500
|
|
107
|
+
? resolved.originStatus
|
|
108
|
+
: 404;
|
|
109
|
+
hit(status, null, null, null);
|
|
110
|
+
return plain(status, "No Markdown version of this page is available.", { vary: "Accept", link: `<${canonical}>; rel="canonical"` });
|
|
111
|
+
}
|
|
112
|
+
const { body, tokens } = (0, enrich_1.enrich)(resolved.doc, {
|
|
113
|
+
canonical: resolved.doc.canonical || canonical,
|
|
114
|
+
sourceUrl: resolved.sourceUrl,
|
|
115
|
+
source: resolved.source,
|
|
116
|
+
});
|
|
117
|
+
const res = await (0, respond_1.markdownResponse)(body, {
|
|
118
|
+
request: req,
|
|
119
|
+
canonical: resolved.doc.canonical || canonical,
|
|
120
|
+
tokens,
|
|
121
|
+
cacheControl,
|
|
122
|
+
headers: { "x-agent-ready-source": resolved.source },
|
|
123
|
+
});
|
|
124
|
+
hit(res.status, resolved.source, tokens, (0, respond_1.byteLength)(body));
|
|
125
|
+
return res;
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The route file in one line (changes/agent-ready-engine, "easy setup"):
|
|
130
|
+
*
|
|
131
|
+
* // app/%5F%5Fmd/[[...path]]/route.ts
|
|
132
|
+
* export const { GET, HEAD } = createMarkdownRoute(config);
|
|
133
|
+
* export const runtime = "nodejs"; // literals: Next.js reads these
|
|
134
|
+
* export const dynamic = "force-dynamic"; // statically, not from a spread
|
|
135
|
+
*
|
|
136
|
+
* `runtime` and `dynamic` are returned too, for frameworks that read them at
|
|
137
|
+
* run time; the init tool writes the two literal lines for Next.js.
|
|
138
|
+
*/
|
|
139
|
+
function createMarkdownRoute(config, deps = {}) {
|
|
140
|
+
const GET = markdownRoute(config, deps);
|
|
141
|
+
return { GET, HEAD: GET, runtime: "nodejs", dynamic: "force-dynamic" };
|
|
142
|
+
}
|
|
143
|
+
/** The page path behind an internal URL: "/__md/tool/x" -> "/tool/x", "/__md" -> "/". */
|
|
144
|
+
function pagePath(cfg, pathname, ctx) {
|
|
145
|
+
const prefix = cfg.internalPrefix;
|
|
146
|
+
let raw;
|
|
147
|
+
if (pathname === prefix || pathname.startsWith(prefix + "/")) {
|
|
148
|
+
raw = pathname.slice(prefix.length) || "/";
|
|
149
|
+
}
|
|
150
|
+
else if (ctx?.params) {
|
|
151
|
+
// In a production build (`next start`) the handler sees the ORIGINAL URL
|
|
152
|
+
// ("/index.md", "/pricing.md"), not the rewrite target. The catch-all
|
|
153
|
+
// params are the truth; no `path` param means the home page. Found in
|
|
154
|
+
// the fresh-app install test, 2026-10-21: /index.md answered 404.
|
|
155
|
+
raw = ctx.params.path?.length ? "/" + ctx.params.path.map(encodeURIComponent).join("/") : "/";
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
raw = (0, detect_1.stripMdSuffix)(pathname) ?? pathname;
|
|
159
|
+
}
|
|
160
|
+
return (0, detect_1.normalizePath)(raw);
|
|
161
|
+
}
|
|
162
|
+
function plain(status, text, extra = {}) {
|
|
163
|
+
return new Response(text, {
|
|
164
|
+
status,
|
|
165
|
+
headers: { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store", ...extra },
|
|
166
|
+
});
|
|
167
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import { type AgentReadyConfig, type ResolvedAgentReadyConfig } from "../core/config";
|
|
3
|
+
import { type ResolveDeps } from "../core/resolve";
|
|
4
|
+
/** Connect's `next`: no argument carries on, an argument is an error. */
|
|
5
|
+
export type NodeNext = (err?: unknown) => void;
|
|
6
|
+
/** A `(req, res, next)` middleware. Express's req/res extend these types. */
|
|
7
|
+
export type NodeMiddleware = (req: IncomingMessage, res: ServerResponse, next?: NodeNext) => void;
|
|
8
|
+
export declare function agentReadyNode(config: AgentReadyConfig | ResolvedAgentReadyConfig, deps?: ResolveDeps): NodeMiddleware;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Node adapter for @relvehq/agent-ready: one Connect-style middleware.
|
|
3
|
+
//
|
|
4
|
+
// // Express, Connect, Polka (Fastify: via @fastify/middie)
|
|
5
|
+
// import { createAgentReady } from "@relvehq/agent-ready";
|
|
6
|
+
// import { agentReadyNode } from "@relvehq/agent-ready/node";
|
|
7
|
+
// app.use(agentReadyNode(createAgentReady({ site: "https://your-site.com" })));
|
|
8
|
+
//
|
|
9
|
+
// // plain http
|
|
10
|
+
// const md = agentReadyNode(config);
|
|
11
|
+
// http.createServer((req, res) => md(req, res, () => app(req, res)));
|
|
12
|
+
//
|
|
13
|
+
// Mount it before static files and routes. Same detect -> resolve -> enrich
|
|
14
|
+
// -> respond path as the Next.js adapter and route, in one step (no rewrite):
|
|
15
|
+
// - not a Markdown request, disabled, excluded, not GET/HEAD, or the
|
|
16
|
+
// package's own origin fetch: next(), nothing written;
|
|
17
|
+
// - nothing resolves: an Accept request goes on to next() (the app serves
|
|
18
|
+
// its HTML), a `.md` / `?format=md` request gets a 404;
|
|
19
|
+
// - resolving throws: next() for Accept (the error never reaches the
|
|
20
|
+
// client), next(err) for `.md` / `?format=md`.
|
|
21
|
+
//
|
|
22
|
+
// NODE ONLY (the convert step uses linkedom/turndown).
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.agentReadyNode = agentReadyNode;
|
|
25
|
+
const config_1 = require("../core/config");
|
|
26
|
+
const detect_1 = require("../core/detect");
|
|
27
|
+
const enrich_1 = require("../core/enrich");
|
|
28
|
+
const llms_1 = require("../core/llms");
|
|
29
|
+
const respond_1 = require("../core/respond");
|
|
30
|
+
const resolve_1 = require("../core/resolve");
|
|
31
|
+
function agentReadyNode(config, deps = {}) {
|
|
32
|
+
const cfg = (0, config_1.defineAgentReady)(config);
|
|
33
|
+
return function agentReadyMiddleware(req, res, next) {
|
|
34
|
+
// Plain http with no next(): what would have been "carry on" is a 404.
|
|
35
|
+
const done = next ??
|
|
36
|
+
((err) => {
|
|
37
|
+
if (res.headersSent)
|
|
38
|
+
return void res.end();
|
|
39
|
+
res.statusCode = err ? 500 : 404;
|
|
40
|
+
res.setHeader("content-type", "text/plain; charset=utf-8");
|
|
41
|
+
res.end(err ? "Internal Server Error" : "Not found");
|
|
42
|
+
});
|
|
43
|
+
handle(cfg, deps, req, res, done).catch((err) => {
|
|
44
|
+
// Only reached when writing the response failed part-way.
|
|
45
|
+
if (!res.headersSent)
|
|
46
|
+
done(err);
|
|
47
|
+
else
|
|
48
|
+
res.destroy(err);
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
async function handle(cfg, deps, req, res, next) {
|
|
53
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
54
|
+
if (method !== "GET" && method !== "HEAD")
|
|
55
|
+
return next();
|
|
56
|
+
if (req.headers[cfg.internalHeader] !== undefined)
|
|
57
|
+
return next();
|
|
58
|
+
const request = toRequest(cfg, req, method);
|
|
59
|
+
const pathname = new URL(request.url).pathname;
|
|
60
|
+
if (pathname === cfg.internalPrefix || pathname.startsWith(cfg.internalPrefix + "/"))
|
|
61
|
+
return next();
|
|
62
|
+
if (!(0, config_1.isEnabled)(cfg, request))
|
|
63
|
+
return next();
|
|
64
|
+
const d = (0, detect_1.wantsMarkdown)(request, { llmsPath: cfg.llms ? cfg.llms.path : null });
|
|
65
|
+
if (!d.markdown)
|
|
66
|
+
return next();
|
|
67
|
+
if (!d.llms && (0, config_1.isExcluded)(cfg, d.path))
|
|
68
|
+
return next();
|
|
69
|
+
const started = Date.now();
|
|
70
|
+
const via = d.via ?? "direct";
|
|
71
|
+
const hit = (status, source, tokens, bytes) => (0, config_1.emitHit)(cfg, {
|
|
72
|
+
ts: new Date().toISOString(),
|
|
73
|
+
host: request.headers.get("x-forwarded-host") ?? request.headers.get("host"),
|
|
74
|
+
path: d.path,
|
|
75
|
+
via,
|
|
76
|
+
source,
|
|
77
|
+
status,
|
|
78
|
+
tokens,
|
|
79
|
+
bytes,
|
|
80
|
+
userAgent: request.headers.get("user-agent")?.slice(0, 512) ?? null,
|
|
81
|
+
accept: request.headers.get("accept")?.slice(0, 1024) ?? null,
|
|
82
|
+
durationMs: Date.now() - started,
|
|
83
|
+
});
|
|
84
|
+
// llms.txt
|
|
85
|
+
if (d.llms && cfg.llms) {
|
|
86
|
+
const entries = await Promise.resolve()
|
|
87
|
+
.then(() => cfg.llms.entries({ request }))
|
|
88
|
+
.catch(() => []);
|
|
89
|
+
const body = (0, llms_1.buildLlmsTxt)({
|
|
90
|
+
site: cfg.site,
|
|
91
|
+
title: cfg.llms.title,
|
|
92
|
+
summary: cfg.llms.summary,
|
|
93
|
+
details: cfg.llms.details,
|
|
94
|
+
entries,
|
|
95
|
+
});
|
|
96
|
+
const r = await (0, respond_1.markdownResponse)(body, {
|
|
97
|
+
request,
|
|
98
|
+
canonical: cfg.site + cfg.llms.path,
|
|
99
|
+
tokens: Math.ceil(body.length / 4),
|
|
100
|
+
cacheControl: cfg.cacheControl,
|
|
101
|
+
contentType: "text/plain; charset=utf-8",
|
|
102
|
+
headers: { "x-agent-ready-source": "llms" },
|
|
103
|
+
});
|
|
104
|
+
// llms.txt is meant to be found and read; it is not a duplicate of a page.
|
|
105
|
+
r.headers.delete("x-robots-tag");
|
|
106
|
+
r.headers.delete("link");
|
|
107
|
+
hit(r.status, "llms", Math.ceil(body.length / 4), (0, respond_1.byteLength)(body));
|
|
108
|
+
return send(res, r);
|
|
109
|
+
}
|
|
110
|
+
let resolved;
|
|
111
|
+
try {
|
|
112
|
+
resolved = await (0, resolve_1.resolveMarkdown)(cfg, { path: d.path, search: d.search, request }, deps);
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
// An agent asking by Accept is never worse off than a browser.
|
|
116
|
+
if (via === "accept")
|
|
117
|
+
return next();
|
|
118
|
+
return next(err);
|
|
119
|
+
}
|
|
120
|
+
const canonical = cfg.site + (d.path === "/" ? "" : d.path);
|
|
121
|
+
if (resolved.source === null) {
|
|
122
|
+
// Accept: the app serves its HTML (no hit: the status is the app's, not ours).
|
|
123
|
+
if (via === "accept")
|
|
124
|
+
return next();
|
|
125
|
+
const status = resolved.originStatus && resolved.originStatus >= 400 && resolved.originStatus < 500
|
|
126
|
+
? resolved.originStatus
|
|
127
|
+
: 404;
|
|
128
|
+
hit(status, null, null, null);
|
|
129
|
+
return send(res, plain(status, "No Markdown version of this page is available.", { vary: "Accept", link: `<${canonical}>; rel="canonical"` }), method);
|
|
130
|
+
}
|
|
131
|
+
const { body, tokens } = (0, enrich_1.enrich)(resolved.doc, {
|
|
132
|
+
canonical: resolved.doc.canonical || canonical,
|
|
133
|
+
sourceUrl: resolved.sourceUrl,
|
|
134
|
+
source: resolved.source,
|
|
135
|
+
});
|
|
136
|
+
const r = await (0, respond_1.markdownResponse)(body, {
|
|
137
|
+
request,
|
|
138
|
+
canonical: resolved.doc.canonical || canonical,
|
|
139
|
+
tokens,
|
|
140
|
+
cacheControl: via === "accept" ? cfg.negotiatedCacheControl : cfg.cacheControl,
|
|
141
|
+
headers: { "x-agent-ready-source": resolved.source },
|
|
142
|
+
});
|
|
143
|
+
hit(r.status, resolved.source, tokens, (0, respond_1.byteLength)(body));
|
|
144
|
+
return send(res, r);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* The Node request as a WHATWG Request (no body: only GET and HEAD get here).
|
|
148
|
+
* The origin is the Host the request came in on, so the convert step fetches
|
|
149
|
+
* this same server; without a Host header, the configured site.
|
|
150
|
+
*/
|
|
151
|
+
function toRequest(cfg, req, method) {
|
|
152
|
+
// Express rewrites req.url under a mount path; originalUrl is what was asked.
|
|
153
|
+
const raw = req.originalUrl ?? req.url ?? "/";
|
|
154
|
+
const host = req.headers.host;
|
|
155
|
+
const tls = req.socket?.encrypted === true;
|
|
156
|
+
const proto = (String(req.headers["x-forwarded-proto"] ?? "").split(",")[0].trim() || (tls ? "https" : "http"));
|
|
157
|
+
let url;
|
|
158
|
+
try {
|
|
159
|
+
url = host ? new URL(raw, `${proto}://${host}`) : new URL(raw, cfg.site);
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
url = new URL(cfg.site);
|
|
163
|
+
}
|
|
164
|
+
const headers = new Headers();
|
|
165
|
+
for (const [name, value] of Object.entries(req.headers)) {
|
|
166
|
+
if (value === undefined)
|
|
167
|
+
continue;
|
|
168
|
+
try {
|
|
169
|
+
headers.set(name, Array.isArray(value) ? value.join(", ") : value);
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
// A header value fetch won't accept: leave it out.
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return new Request(url, { method, headers });
|
|
176
|
+
}
|
|
177
|
+
/** Write a Response to the Node response: status, headers, then the body. */
|
|
178
|
+
async function send(res, r, method) {
|
|
179
|
+
res.statusCode = r.status;
|
|
180
|
+
r.headers.forEach((value, name) => res.setHeader(name, value));
|
|
181
|
+
const body = r.body && method !== "HEAD" ? Buffer.from(await r.arrayBuffer()) : null;
|
|
182
|
+
if (body)
|
|
183
|
+
res.setHeader("content-length", String(body.length));
|
|
184
|
+
res.end(body ?? undefined);
|
|
185
|
+
}
|
|
186
|
+
function plain(status, text, extra = {}) {
|
|
187
|
+
return new Response(text, {
|
|
188
|
+
status,
|
|
189
|
+
headers: { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store", ...extra },
|
|
190
|
+
});
|
|
191
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export type CheckStatus = "pass" | "warn" | "fail";
|
|
2
|
+
export interface AuditCheck {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
status: CheckStatus;
|
|
6
|
+
points: number;
|
|
7
|
+
detail: string;
|
|
8
|
+
/** What to do about it (empty on pass). */
|
|
9
|
+
fix: string;
|
|
10
|
+
}
|
|
11
|
+
export interface AuditReport {
|
|
12
|
+
url: string;
|
|
13
|
+
checkedAt: string;
|
|
14
|
+
ok: boolean;
|
|
15
|
+
/** Set when the page itself could not be loaded. */
|
|
16
|
+
error: string | null;
|
|
17
|
+
score: number;
|
|
18
|
+
grade: "A" | "B" | "C" | "D" | "F";
|
|
19
|
+
checks: AuditCheck[];
|
|
20
|
+
tokens: {
|
|
21
|
+
html: number | null;
|
|
22
|
+
/** Markdown the site served itself (Accept or .md). */
|
|
23
|
+
served: number | null;
|
|
24
|
+
/** What the package's converter would make of the page (when `convert` is given). */
|
|
25
|
+
converted: number | null;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export interface AuditOptions {
|
|
29
|
+
fetch?: typeof fetch;
|
|
30
|
+
timeoutMs?: number;
|
|
31
|
+
/** HTML -> Markdown, to show what the site could serve. */
|
|
32
|
+
convert?: (html: string, opts: {
|
|
33
|
+
url: string;
|
|
34
|
+
}) => {
|
|
35
|
+
markdown: string;
|
|
36
|
+
} | null;
|
|
37
|
+
now?: () => Date;
|
|
38
|
+
}
|
|
39
|
+
export declare function auditSite(rawUrl: string, opts?: AuditOptions): Promise<AuditReport>;
|
|
40
|
+
/**
|
|
41
|
+
* Bots from `tokens` that robots.txt shuts out of the whole site
|
|
42
|
+
* ("Disallow: /" in the group that applies to them, with no "Allow: /").
|
|
43
|
+
* A bot with its own group follows only that group; otherwise "*".
|
|
44
|
+
*/
|
|
45
|
+
export declare function blockedBots(robots: string, tokens: string[]): string[];
|
|
46
|
+
/** The report as terminal lines (`agent-ready check`). */
|
|
47
|
+
export declare function formatAudit(r: AuditReport): string;
|