@farming-labs/farmjs 0.2.92
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/README.md +73 -0
- package/dist/api-reference.d.ts +3 -0
- package/dist/api-reference.d.ts.map +1 -0
- package/dist/api-reference.js +20 -0
- package/dist/api-reference.js.map +1 -0
- package/dist/config.d.ts +30 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +33 -0
- package/dist/config.js.map +1 -0
- package/dist/content.d.ts +56 -0
- package/dist/content.d.ts.map +1 -0
- package/dist/content.js +235 -0
- package/dist/content.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/react.d.ts +7 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +34 -0
- package/dist/react.js.map +1 -0
- package/dist/server.d.ts +43 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +1725 -0
- package/dist/server.js.map +1 -0
- package/dist/vite.d.ts +3 -0
- package/dist/vite.d.ts.map +1 -0
- package/dist/vite.js +147 -0
- package/dist/vite.js.map +1 -0
- package/package.json +90 -0
package/dist/server.js
ADDED
|
@@ -0,0 +1,1725 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import matter from "gray-matter";
|
|
4
|
+
import { applySidebarFolderIndexBehavior, buildDocsAskAIContext, buildDocsSearchFacets, buildDocsAgentDiscoverySpec, buildDocsConfigMap, buildDocsDiagnostics, createDocsContentChangeFeed, createDocsContentChangesHttpResponse, createDocsCacheableResponse, createDocsStandardsDiscoveryResponse, createDocsMarkdownResponse, createDocsRobotsResponse, createDocsSitemapResponse, createDocsAgentTraceContext, createDocsAgentTraceId, emitDocsAgentTraceEvent, emitDocsAnalyticsEvent, emitDocsTelemetryAgentSurfaceEvent, emitDocsTelemetryProjectEvent, formatDocsAskAIPackageHints, findDocsMarkdownPage, getDocsLlmsTxtMaxCharsIssue, getDocsAgentManifestLinkHeader, getDocsDiscoveryLinkHeader, isDocsAgentDiscoveryRequest, isDocsContentChangesRequest, isDocsAgentsRequest, isDocsConfigRequest, isDocsDiagnosticsRequest, isDocsSkillRequest, normalizeDocsRelated, normalizePageAgentFrontmatter, parseDocsAgentFeedbackData, performDocsSearch, performDocsSearchWithMetadata, renderDocsMarkdownDocument, renderDocsLlmsTxt, renderDocsAgentsDocument, renderDocsSkillDocument, readDocsSitemapManifestFromContentMap, stripGeneratedAgentProvenance, resolveDocsAudienceMdxContent, resolveDocsAgentFeedbackConfig, resolveDocsAgentFeedbackRequest, resolvePageSidebarFolderIndexBehavior, resolveAskAISearchRequestConfig, resolveDocsSearchAudience, resolveDocsSearchError, resolveDocsSearchRequest, resolveSearchRequestConfig, resolveDocsI18n, resolveDocsLlmsTxtRequest, resolveDocsLocale, resolveDocsMarkdownRequest, resolveDocsMetadataBaseUrl, resolveDocsContentChangesConfig, resolveDocsRequestApiRoute, resolveDocsRetrievalLastModified, resolveDocsPublishedAgentSkill, resolveDocsStandardsDiscoveryRequest, resolveDocsPath, resolvePageReadingTime, resolveReadingTimeOptions, resolveDocsSitemapPageLastmod, resolveDocsAgentsFormat, resolveDocsSkillFormat, inferDocsTelemetryAgentSurface, renderDocsPageStructuredDataJson, selectDocsLlmsTxtContent, validateDocsAgentFeedbackPayload, } from "@farming-labs/docs";
|
|
5
|
+
import { buildApiReferenceOpenApiDocumentAsync, createDocsMcpHttpHandler, readDocsSitemapManifest, resolveApiReferenceOpenApiDiscovery, resolveDocsMcpConfig, resolveConfiguredAgentSkills, } from "@farming-labs/docs/server";
|
|
6
|
+
import { loadDocsNavTree, loadDocsContent, flattenNavTree, normalizeDocsFrontmatterLastmod, } from "./content.js";
|
|
7
|
+
export { createFarmjsApiReference } from "./api-reference.js";
|
|
8
|
+
function isApiReferenceOpenApiRequest(url) {
|
|
9
|
+
return url.searchParams.get("format")?.trim() === "openapi";
|
|
10
|
+
}
|
|
11
|
+
function resolveAIModelAndProvider(aiConfig, requestedModelId) {
|
|
12
|
+
const raw = aiConfig.model;
|
|
13
|
+
const modelList = (typeof raw === "object" && raw?.models) || [];
|
|
14
|
+
let modelId = requestedModelId;
|
|
15
|
+
if (!modelId) {
|
|
16
|
+
if (typeof raw === "string")
|
|
17
|
+
modelId = raw;
|
|
18
|
+
else if (typeof raw === "object")
|
|
19
|
+
modelId = raw.defaultModel ?? raw.models?.[0]?.id;
|
|
20
|
+
if (!modelId)
|
|
21
|
+
modelId = "gpt-4o-mini";
|
|
22
|
+
}
|
|
23
|
+
const entry = modelList.find((model) => model.id === modelId);
|
|
24
|
+
const providerKey = entry?.provider;
|
|
25
|
+
const providerConfig = providerKey && aiConfig.providers?.[providerKey];
|
|
26
|
+
const baseUrl = ((providerConfig && providerConfig.baseUrl) ||
|
|
27
|
+
aiConfig.baseUrl ||
|
|
28
|
+
"https://api.openai.com/v1").replace(/\/$/, "");
|
|
29
|
+
const apiKey = (providerConfig && providerConfig.apiKey) || aiConfig.apiKey || process.env?.OPENAI_API_KEY;
|
|
30
|
+
return { model: modelId, baseUrl, apiKey };
|
|
31
|
+
}
|
|
32
|
+
function safeUrlOrigin(value) {
|
|
33
|
+
try {
|
|
34
|
+
return new URL(value).origin;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function resolvePreloadedContent(value) {
|
|
41
|
+
if (!value || typeof value !== "object")
|
|
42
|
+
return null;
|
|
43
|
+
const entries = Object.entries(value);
|
|
44
|
+
const normalized = {};
|
|
45
|
+
for (const [key, entryValue] of entries) {
|
|
46
|
+
if (typeof entryValue === "string") {
|
|
47
|
+
normalized[key] = entryValue;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (entryValue &&
|
|
51
|
+
typeof entryValue === "object" &&
|
|
52
|
+
typeof entryValue.default === "string") {
|
|
53
|
+
normalized[key] = entryValue.default;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
return normalized;
|
|
59
|
+
}
|
|
60
|
+
function stripMarkdownText(content) {
|
|
61
|
+
return content
|
|
62
|
+
.replace(/^(import|export)\s.*$/gm, "")
|
|
63
|
+
.replace(/<[^>]+\/>/g, "")
|
|
64
|
+
.replace(/<\/?[A-Z][^>]*>/g, "")
|
|
65
|
+
.replace(/<\/?[a-z][^>]*>/g, "")
|
|
66
|
+
.replace(/!\[([^\]]*)\]\([^)]+\)/g, "$1")
|
|
67
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
|
68
|
+
.replace(/^#{1,6}\s+/gm, "")
|
|
69
|
+
.replace(/(\*{1,3}|_{1,3})(.*?)\1/g, "$2")
|
|
70
|
+
.replace(/```[\s\S]*?```/g, "")
|
|
71
|
+
.replace(/`([^`]+)`/g, "$1")
|
|
72
|
+
.replace(/^>\s+/gm, "")
|
|
73
|
+
.replace(/^[-*_]{3,}\s*$/gm, "")
|
|
74
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
75
|
+
.trim();
|
|
76
|
+
}
|
|
77
|
+
function normalizePathSegment(value) {
|
|
78
|
+
return value.replace(/^\/+|\/+$/g, "");
|
|
79
|
+
}
|
|
80
|
+
function joinPathParts(...parts) {
|
|
81
|
+
return parts
|
|
82
|
+
.map((part) => normalizePathSegment(part))
|
|
83
|
+
.filter(Boolean)
|
|
84
|
+
.join("/");
|
|
85
|
+
}
|
|
86
|
+
function toPosixPath(value) {
|
|
87
|
+
return value.replace(/\\/g, "/");
|
|
88
|
+
}
|
|
89
|
+
function buildDirPrefix(contentDir, rootDir) {
|
|
90
|
+
const rel = path.isAbsolute(contentDir)
|
|
91
|
+
? toPosixPath(path.relative(rootDir, contentDir))
|
|
92
|
+
: toPosixPath(contentDir);
|
|
93
|
+
const normalized = normalizePathSegment(rel);
|
|
94
|
+
return normalized ? `/${normalized}/` : "/";
|
|
95
|
+
}
|
|
96
|
+
function toSourcePath(contentDir, relPath, rootDir) {
|
|
97
|
+
const base = path.isAbsolute(contentDir)
|
|
98
|
+
? toPosixPath(path.relative(rootDir, contentDir))
|
|
99
|
+
: toPosixPath(contentDir);
|
|
100
|
+
return `/${joinPathParts(base, toPosixPath(relPath))}`;
|
|
101
|
+
}
|
|
102
|
+
function navTreeFromMap(contentMap, dirPrefix, entry, ordering) {
|
|
103
|
+
const dirs = [];
|
|
104
|
+
for (const key of Object.keys(contentMap)) {
|
|
105
|
+
if (!key.startsWith(dirPrefix))
|
|
106
|
+
continue;
|
|
107
|
+
const rel = key.slice(dirPrefix.length);
|
|
108
|
+
const segments = rel.split("/");
|
|
109
|
+
const fileName = segments.pop();
|
|
110
|
+
const base = fileName.replace(/\.(md|mdx)$/, "");
|
|
111
|
+
if (base !== "page" && base !== "index")
|
|
112
|
+
continue;
|
|
113
|
+
const { data } = matter(contentMap[key]);
|
|
114
|
+
const dirParts = segments;
|
|
115
|
+
const slug = dirParts.join("/");
|
|
116
|
+
const url = slug ? `/${entry}/${slug}` : `/${entry}`;
|
|
117
|
+
const fallbackTitle = dirParts.length > 0
|
|
118
|
+
? dirParts[dirParts.length - 1]
|
|
119
|
+
.replace(/-/g, " ")
|
|
120
|
+
.replace(/\b\w/g, (char) => char.toUpperCase())
|
|
121
|
+
: "Documentation";
|
|
122
|
+
dirs.push({
|
|
123
|
+
parts: dirParts,
|
|
124
|
+
title: data.title ?? fallbackTitle,
|
|
125
|
+
url,
|
|
126
|
+
icon: data.icon,
|
|
127
|
+
folderIndexBehavior: resolvePageSidebarFolderIndexBehavior(data.sidebar),
|
|
128
|
+
order: typeof data.order === "number" ? data.order : Infinity,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
dirs.sort((a, b) => {
|
|
132
|
+
if (a.parts.length !== b.parts.length)
|
|
133
|
+
return a.parts.length - b.parts.length;
|
|
134
|
+
return a.parts.join("/").localeCompare(b.parts.join("/"));
|
|
135
|
+
});
|
|
136
|
+
const children = [];
|
|
137
|
+
const rootInfo = dirs.find((dir) => dir.parts.length === 0);
|
|
138
|
+
if (rootInfo) {
|
|
139
|
+
children.push({
|
|
140
|
+
type: "page",
|
|
141
|
+
name: rootInfo.title,
|
|
142
|
+
url: rootInfo.url,
|
|
143
|
+
icon: rootInfo.icon,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
function findSlugOrder(parentParts) {
|
|
147
|
+
if (!Array.isArray(ordering))
|
|
148
|
+
return undefined;
|
|
149
|
+
let items = ordering;
|
|
150
|
+
for (const part of parentParts) {
|
|
151
|
+
const found = items.find((item) => item.slug === part);
|
|
152
|
+
if (!found?.children)
|
|
153
|
+
return undefined;
|
|
154
|
+
items = found.children;
|
|
155
|
+
}
|
|
156
|
+
return items;
|
|
157
|
+
}
|
|
158
|
+
function buildLevel(parentParts) {
|
|
159
|
+
const depth = parentParts.length;
|
|
160
|
+
const directChildren = dirs.filter((dir) => {
|
|
161
|
+
if (dir.parts.length !== depth + 1)
|
|
162
|
+
return false;
|
|
163
|
+
for (let index = 0; index < depth; index++) {
|
|
164
|
+
if (dir.parts[index] !== parentParts[index])
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
return true;
|
|
168
|
+
});
|
|
169
|
+
const slugOrder = findSlugOrder(parentParts);
|
|
170
|
+
if (slugOrder) {
|
|
171
|
+
const slugMap = new Set(slugOrder.map((item) => item.slug));
|
|
172
|
+
const ordered = [];
|
|
173
|
+
for (const item of slugOrder) {
|
|
174
|
+
const match = directChildren.find((child) => child.parts[depth] === item.slug);
|
|
175
|
+
if (match)
|
|
176
|
+
ordered.push(match);
|
|
177
|
+
}
|
|
178
|
+
for (const child of directChildren) {
|
|
179
|
+
if (!slugMap.has(child.parts[depth]))
|
|
180
|
+
ordered.push(child);
|
|
181
|
+
}
|
|
182
|
+
return ordered.map((child) => {
|
|
183
|
+
const hasGrandChildren = dirs.some((dir) => {
|
|
184
|
+
if (dir.parts.length <= child.parts.length)
|
|
185
|
+
return false;
|
|
186
|
+
return child.parts.every((part, index) => dir.parts[index] === part);
|
|
187
|
+
});
|
|
188
|
+
if (hasGrandChildren) {
|
|
189
|
+
return {
|
|
190
|
+
type: "folder",
|
|
191
|
+
name: child.title,
|
|
192
|
+
icon: child.icon,
|
|
193
|
+
index: { type: "page", name: child.title, url: child.url, icon: child.icon },
|
|
194
|
+
folderIndexBehavior: child.folderIndexBehavior,
|
|
195
|
+
children: buildLevel(child.parts),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
type: "page",
|
|
200
|
+
name: child.title,
|
|
201
|
+
url: child.url,
|
|
202
|
+
icon: child.icon,
|
|
203
|
+
};
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
if (ordering === "numeric") {
|
|
207
|
+
directChildren.sort((a, b) => a.order - b.order);
|
|
208
|
+
}
|
|
209
|
+
return directChildren.map((child) => {
|
|
210
|
+
const hasGrandChildren = dirs.some((dir) => {
|
|
211
|
+
if (dir.parts.length <= child.parts.length)
|
|
212
|
+
return false;
|
|
213
|
+
return child.parts.every((part, index) => dir.parts[index] === part);
|
|
214
|
+
});
|
|
215
|
+
if (hasGrandChildren) {
|
|
216
|
+
return {
|
|
217
|
+
type: "folder",
|
|
218
|
+
name: child.title,
|
|
219
|
+
icon: child.icon,
|
|
220
|
+
index: { type: "page", name: child.title, url: child.url, icon: child.icon },
|
|
221
|
+
folderIndexBehavior: child.folderIndexBehavior,
|
|
222
|
+
children: buildLevel(child.parts),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
type: "page",
|
|
227
|
+
name: child.title,
|
|
228
|
+
url: child.url,
|
|
229
|
+
icon: child.icon,
|
|
230
|
+
};
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
children.push(...buildLevel([]));
|
|
234
|
+
return { name: "Docs", children };
|
|
235
|
+
}
|
|
236
|
+
function searchIndexFromMap(contentMap, dirPrefix, entry, locale, sitemapManifest) {
|
|
237
|
+
const pages = [];
|
|
238
|
+
for (const key of Object.keys(contentMap)) {
|
|
239
|
+
if (!key.startsWith(dirPrefix))
|
|
240
|
+
continue;
|
|
241
|
+
const rel = key.slice(dirPrefix.length);
|
|
242
|
+
const segments = rel.split("/");
|
|
243
|
+
const fileName = segments.pop();
|
|
244
|
+
if (fileName === "agent.md")
|
|
245
|
+
continue;
|
|
246
|
+
const base = fileName.replace(/\.(md|mdx)$/, "");
|
|
247
|
+
const isIdx = base === "page" || base === "index";
|
|
248
|
+
const raw = contentMap[key];
|
|
249
|
+
const { data, content } = matter(raw);
|
|
250
|
+
const humanRawContent = resolveDocsAudienceMdxContent(content, "human");
|
|
251
|
+
const pageAgentRawContent = resolveDocsAudienceMdxContent(content, "agent");
|
|
252
|
+
const related = normalizeDocsRelated(data.related);
|
|
253
|
+
const slug = isIdx ? segments.join("/") : [...segments, base].join("/");
|
|
254
|
+
const url = slug ? `/${entry}/${slug}` : `/${entry}`;
|
|
255
|
+
const manifestLastmod = (locale
|
|
256
|
+
? resolveDocsSitemapPageLastmod(sitemapManifest, `${url}?lang=${encodeURIComponent(locale)}`)
|
|
257
|
+
: undefined) ?? resolveDocsSitemapPageLastmod(sitemapManifest, url);
|
|
258
|
+
const agentDoc = isIdx ? readAgentDocFromMap(contentMap, dirPrefix, slug) : undefined;
|
|
259
|
+
const fallbackTitle = isIdx
|
|
260
|
+
? segments.length > 0
|
|
261
|
+
? segments[segments.length - 1]
|
|
262
|
+
.replace(/-/g, " ")
|
|
263
|
+
.replace(/\b\w/g, (char) => char.toUpperCase())
|
|
264
|
+
: "Documentation"
|
|
265
|
+
: base.replace(/-/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
|
|
266
|
+
pages.push({
|
|
267
|
+
slug,
|
|
268
|
+
url,
|
|
269
|
+
title: data.title ?? fallbackTitle,
|
|
270
|
+
description: data.description,
|
|
271
|
+
...(related.length > 0 ? { related } : {}),
|
|
272
|
+
agent: normalizePageAgentFrontmatter(data.agent),
|
|
273
|
+
icon: data.icon,
|
|
274
|
+
lastmod: normalizeDocsFrontmatterLastmod(data.lastmod),
|
|
275
|
+
locale: typeof data.locale === "string" ? data.locale : undefined,
|
|
276
|
+
framework: typeof data.framework === "string" ? data.framework : undefined,
|
|
277
|
+
version: typeof data.version === "string" ? data.version : undefined,
|
|
278
|
+
tags: Array.isArray(data.tags)
|
|
279
|
+
? data.tags.filter((tag) => typeof tag === "string")
|
|
280
|
+
: undefined,
|
|
281
|
+
content: stripMarkdownText(humanRawContent),
|
|
282
|
+
rawContent: humanRawContent,
|
|
283
|
+
...(manifestLastmod ? { lastModified: `${manifestLastmod}T00:00:00.000Z` } : {}),
|
|
284
|
+
...(pageAgentRawContent !== humanRawContent
|
|
285
|
+
? {
|
|
286
|
+
agentFallbackContent: stripMarkdownText(pageAgentRawContent),
|
|
287
|
+
agentFallbackRawContent: pageAgentRawContent,
|
|
288
|
+
}
|
|
289
|
+
: {}),
|
|
290
|
+
...agentDoc,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
return pages;
|
|
294
|
+
}
|
|
295
|
+
function readAgentDocFromMap(contentMap, dirPrefix, slug) {
|
|
296
|
+
const key = slug ? `${dirPrefix}${slug}/agent.md` : `${dirPrefix}agent.md`;
|
|
297
|
+
const raw = contentMap[key];
|
|
298
|
+
if (!raw)
|
|
299
|
+
return undefined;
|
|
300
|
+
const { content } = matter(stripGeneratedAgentProvenance(raw));
|
|
301
|
+
const agentRawContent = resolveDocsAudienceMdxContent(content, "agent");
|
|
302
|
+
return {
|
|
303
|
+
agentContent: stripMarkdownText(agentRawContent),
|
|
304
|
+
agentRawContent,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
function findPageInMap(contentMap, dirPrefix, slug) {
|
|
308
|
+
const candidates = slug
|
|
309
|
+
? [
|
|
310
|
+
`${dirPrefix}${slug}.md`,
|
|
311
|
+
`${dirPrefix}${slug}.mdx`,
|
|
312
|
+
`${dirPrefix}${slug}/page.md`,
|
|
313
|
+
`${dirPrefix}${slug}/page.mdx`,
|
|
314
|
+
`${dirPrefix}${slug}/index.md`,
|
|
315
|
+
`${dirPrefix}${slug}/index.mdx`,
|
|
316
|
+
]
|
|
317
|
+
: [
|
|
318
|
+
`${dirPrefix}page.md`,
|
|
319
|
+
`${dirPrefix}page.mdx`,
|
|
320
|
+
`${dirPrefix}index.md`,
|
|
321
|
+
`${dirPrefix}index.mdx`,
|
|
322
|
+
];
|
|
323
|
+
for (const key of candidates) {
|
|
324
|
+
const raw = contentMap[key];
|
|
325
|
+
if (raw) {
|
|
326
|
+
return {
|
|
327
|
+
raw,
|
|
328
|
+
relPath: key.slice(dirPrefix.length),
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
function readRootSkillDocument(contentMap, rootDir) {
|
|
335
|
+
if (contentMap) {
|
|
336
|
+
for (const key of ["/skill.md", "skill.md", "./skill.md"]) {
|
|
337
|
+
const raw = contentMap[key];
|
|
338
|
+
if (typeof raw === "string")
|
|
339
|
+
return raw;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
const candidate = path.join(rootDir, "skill.md");
|
|
343
|
+
try {
|
|
344
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
|
|
345
|
+
return fs.readFileSync(candidate, "utf-8");
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
function readRootAgentsDocument(contentMap, rootDir) {
|
|
354
|
+
if (contentMap) {
|
|
355
|
+
for (const key of [
|
|
356
|
+
"/AGENTS.md",
|
|
357
|
+
"AGENTS.md",
|
|
358
|
+
"./AGENTS.md",
|
|
359
|
+
"/AGENT.md",
|
|
360
|
+
"AGENT.md",
|
|
361
|
+
"./AGENT.md",
|
|
362
|
+
]) {
|
|
363
|
+
const raw = contentMap[key];
|
|
364
|
+
if (typeof raw === "string")
|
|
365
|
+
return raw;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
for (const fileName of ["AGENTS.md", "AGENT.md"]) {
|
|
369
|
+
const candidate = path.join(rootDir, fileName);
|
|
370
|
+
try {
|
|
371
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
|
|
372
|
+
return fs.readFileSync(candidate, "utf-8");
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
export function createDocsServer(config) {
|
|
382
|
+
const entry = config.entry ?? "docs";
|
|
383
|
+
const analytics = config.analytics;
|
|
384
|
+
const observability = config.observability;
|
|
385
|
+
const ordering = config.ordering;
|
|
386
|
+
const contentDirBase = config.contentDir ?? entry;
|
|
387
|
+
const rootDir = path.resolve(config.rootDir ?? process.cwd());
|
|
388
|
+
let publishedAgentSkills;
|
|
389
|
+
function getPublishedAgentSkills() {
|
|
390
|
+
publishedAgentSkills ??= Array.isArray(config._preloadedAgentSkills)
|
|
391
|
+
? Promise.resolve(config._preloadedAgentSkills)
|
|
392
|
+
: resolveConfiguredAgentSkills(config.agent?.skills, { rootDir });
|
|
393
|
+
return publishedAgentSkills;
|
|
394
|
+
}
|
|
395
|
+
const preloaded = resolvePreloadedContent(config._preloadedContent);
|
|
396
|
+
const preloadedSitemapManifest = readDocsSitemapManifestFromContentMap(preloaded);
|
|
397
|
+
const i18n = resolveDocsI18n(config.i18n);
|
|
398
|
+
const githubRaw = config.github;
|
|
399
|
+
const githubRepo = typeof githubRaw === "string"
|
|
400
|
+
? githubRaw.replace(/\/$/, "")
|
|
401
|
+
: githubRaw?.url.replace(/\/$/, "");
|
|
402
|
+
const githubBranch = typeof githubRaw === "object" ? (githubRaw.branch ?? "main") : "main";
|
|
403
|
+
const githubContentPath = typeof githubRaw === "object" ? githubRaw.directory?.replace(/^\/|\/$/g, "") : undefined;
|
|
404
|
+
const readingTimeOptions = resolveReadingTimeOptions(config.readingTime);
|
|
405
|
+
const aiConfig = { enabled: false, ...config.ai };
|
|
406
|
+
if (config.apiKey && !aiConfig.apiKey) {
|
|
407
|
+
aiConfig.apiKey = config.apiKey;
|
|
408
|
+
}
|
|
409
|
+
function normalizePathname(pathname) {
|
|
410
|
+
const trimmed = pathname.replace(/\/+$/, "");
|
|
411
|
+
return trimmed || "/";
|
|
412
|
+
}
|
|
413
|
+
function resolveContentDirRel(locale) {
|
|
414
|
+
if (!locale)
|
|
415
|
+
return contentDirBase;
|
|
416
|
+
if (path.isAbsolute(contentDirBase))
|
|
417
|
+
return path.join(contentDirBase, locale);
|
|
418
|
+
return joinPathParts(contentDirBase, locale);
|
|
419
|
+
}
|
|
420
|
+
function resolveContextFromPath(pathname, locale) {
|
|
421
|
+
const match = resolveDocsPath(normalizePathname(pathname), entry);
|
|
422
|
+
const contentDirRel = resolveContentDirRel(locale);
|
|
423
|
+
return {
|
|
424
|
+
...match,
|
|
425
|
+
locale,
|
|
426
|
+
contentDirRel,
|
|
427
|
+
contentDirAbs: path.isAbsolute(contentDirRel)
|
|
428
|
+
? contentDirRel
|
|
429
|
+
: path.resolve(rootDir, contentDirRel),
|
|
430
|
+
dirPrefix: buildDirPrefix(contentDirRel, rootDir),
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
function resolveLocaleFromRequest(request) {
|
|
434
|
+
if (!i18n)
|
|
435
|
+
return undefined;
|
|
436
|
+
const url = new URL(request.url);
|
|
437
|
+
const direct = resolveDocsLocale(url.searchParams, i18n);
|
|
438
|
+
if (direct)
|
|
439
|
+
return direct;
|
|
440
|
+
const referrer = request.headers.get("referer") ?? request.headers.get("referrer");
|
|
441
|
+
if (referrer) {
|
|
442
|
+
try {
|
|
443
|
+
const refUrl = new URL(referrer);
|
|
444
|
+
const fromRef = resolveDocsLocale(refUrl.searchParams, i18n);
|
|
445
|
+
if (fromRef)
|
|
446
|
+
return fromRef;
|
|
447
|
+
}
|
|
448
|
+
catch {
|
|
449
|
+
// ignore malformed referrer URLs
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return i18n.defaultLocale;
|
|
453
|
+
}
|
|
454
|
+
function resolveContextFromRequest(request) {
|
|
455
|
+
const locale = resolveLocaleFromRequest(request);
|
|
456
|
+
const url = new URL(request.url);
|
|
457
|
+
const pathnameParam = url.searchParams.get("pathname");
|
|
458
|
+
const referrer = request.headers.get("referer") ?? request.headers.get("referrer");
|
|
459
|
+
let refPath;
|
|
460
|
+
if (referrer) {
|
|
461
|
+
try {
|
|
462
|
+
refPath = new URL(referrer).pathname;
|
|
463
|
+
}
|
|
464
|
+
catch {
|
|
465
|
+
refPath = undefined;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
const pathname = pathnameParam ?? refPath ?? `/${entry}`;
|
|
469
|
+
return resolveContextFromPath(pathname, locale);
|
|
470
|
+
}
|
|
471
|
+
async function load({ pathname, locale }) {
|
|
472
|
+
const resolvedLocale = (i18n && locale && i18n.locales.includes(locale) ? locale : undefined) ?? i18n?.defaultLocale;
|
|
473
|
+
const ctx = resolveContextFromPath(pathname, resolvedLocale);
|
|
474
|
+
const tree = applySidebarFolderIndexBehavior(preloaded
|
|
475
|
+
? navTreeFromMap(preloaded, ctx.dirPrefix, entry, ordering)
|
|
476
|
+
: loadDocsNavTree(ctx.contentDirAbs, entry, ordering), {
|
|
477
|
+
sidebar: config.sidebar,
|
|
478
|
+
defaultBehavior: "link",
|
|
479
|
+
});
|
|
480
|
+
const flatPages = flattenNavTree(tree);
|
|
481
|
+
const slug = ctx.slug;
|
|
482
|
+
const isIndex = slug === "";
|
|
483
|
+
const currentUrl = isIndex ? `/${entry}` : `/${entry}/${slug}`;
|
|
484
|
+
let raw;
|
|
485
|
+
let relPath;
|
|
486
|
+
let lastModified;
|
|
487
|
+
let lastModifiedIso;
|
|
488
|
+
if (preloaded) {
|
|
489
|
+
const result = findPageInMap(preloaded, ctx.dirPrefix, slug);
|
|
490
|
+
if (!result) {
|
|
491
|
+
const error = new Error(`Page not found: /${entry}/${slug}`);
|
|
492
|
+
error.status = 404;
|
|
493
|
+
throw error;
|
|
494
|
+
}
|
|
495
|
+
raw = result.raw;
|
|
496
|
+
relPath = result.relPath;
|
|
497
|
+
const manifestLastmod = resolveDocsSitemapPageLastmod(preloadedSitemapManifest, currentUrl);
|
|
498
|
+
const lastModifiedDate = manifestLastmod
|
|
499
|
+
? new Date(`${manifestLastmod}T00:00:00`)
|
|
500
|
+
: new Date();
|
|
501
|
+
lastModified = lastModifiedDate.toLocaleDateString("en-US", {
|
|
502
|
+
year: "numeric",
|
|
503
|
+
month: "long",
|
|
504
|
+
day: "numeric",
|
|
505
|
+
});
|
|
506
|
+
lastModifiedIso = manifestLastmod ? `${manifestLastmod}T00:00:00.000Z` : undefined;
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
let filePath = null;
|
|
510
|
+
relPath = "";
|
|
511
|
+
if (isIndex) {
|
|
512
|
+
for (const name of ["page.md", "page.mdx", "index.md", "index.mdx"]) {
|
|
513
|
+
const candidate = path.join(ctx.contentDirAbs, name);
|
|
514
|
+
if (fs.existsSync(candidate)) {
|
|
515
|
+
filePath = candidate;
|
|
516
|
+
relPath = name;
|
|
517
|
+
break;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
else {
|
|
522
|
+
const candidates = [
|
|
523
|
+
path.join(ctx.contentDirAbs, `${slug}.md`),
|
|
524
|
+
path.join(ctx.contentDirAbs, `${slug}.mdx`),
|
|
525
|
+
path.join(ctx.contentDirAbs, slug, "page.md"),
|
|
526
|
+
path.join(ctx.contentDirAbs, slug, "page.mdx"),
|
|
527
|
+
path.join(ctx.contentDirAbs, slug, "index.md"),
|
|
528
|
+
path.join(ctx.contentDirAbs, slug, "index.mdx"),
|
|
529
|
+
];
|
|
530
|
+
for (const candidate of candidates) {
|
|
531
|
+
if (fs.existsSync(candidate)) {
|
|
532
|
+
filePath = candidate;
|
|
533
|
+
relPath = path.relative(ctx.contentDirAbs, candidate);
|
|
534
|
+
break;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
if (!filePath) {
|
|
539
|
+
const error = new Error(`Page not found: /${entry}/${slug}`);
|
|
540
|
+
error.status = 404;
|
|
541
|
+
throw error;
|
|
542
|
+
}
|
|
543
|
+
raw = fs.readFileSync(filePath, "utf-8");
|
|
544
|
+
const stat = fs.statSync(filePath);
|
|
545
|
+
lastModifiedIso = stat.mtime.toISOString();
|
|
546
|
+
lastModified = stat.mtime.toLocaleDateString("en-US", {
|
|
547
|
+
year: "numeric",
|
|
548
|
+
month: "long",
|
|
549
|
+
day: "numeric",
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
const { data, content } = matter(raw);
|
|
553
|
+
const humanRawContent = resolveDocsAudienceMdxContent(content, "human");
|
|
554
|
+
const readingTime = resolvePageReadingTime(data, humanRawContent, {
|
|
555
|
+
enabledByDefault: readingTimeOptions.enabled,
|
|
556
|
+
wordsPerMinute: readingTimeOptions.wordsPerMinute,
|
|
557
|
+
includeCode: readingTimeOptions.includeCode,
|
|
558
|
+
});
|
|
559
|
+
const currentIndex = flatPages.findIndex((page) => page.url === currentUrl);
|
|
560
|
+
const previousPage = currentIndex > 0 ? flatPages[currentIndex - 1] : null;
|
|
561
|
+
const nextPage = currentIndex < flatPages.length - 1 ? flatPages[currentIndex + 1] : null;
|
|
562
|
+
let editOnGithub;
|
|
563
|
+
if (githubRepo && githubContentPath) {
|
|
564
|
+
const trimmed = githubContentPath.replace(/\/+$/, "");
|
|
565
|
+
const localePrefix = ctx.locale ? `${ctx.locale}/` : "";
|
|
566
|
+
editOnGithub = `${githubRepo}/blob/${githubBranch}/${trimmed}/${localePrefix}${toPosixPath(relPath)}`;
|
|
567
|
+
}
|
|
568
|
+
const fallbackTitle = isIndex
|
|
569
|
+
? "Documentation"
|
|
570
|
+
: (slug.split("/").pop()?.replace(/-/g, " ") ?? "Documentation");
|
|
571
|
+
const title = data.title ?? fallbackTitle;
|
|
572
|
+
const description = data.description;
|
|
573
|
+
const structuredData = renderDocsPageStructuredDataJson({
|
|
574
|
+
title,
|
|
575
|
+
description,
|
|
576
|
+
url: currentUrl,
|
|
577
|
+
baseUrl: resolveDocsMetadataBaseUrl(config),
|
|
578
|
+
entry,
|
|
579
|
+
dateModified: lastModifiedIso,
|
|
580
|
+
agent: normalizePageAgentFrontmatter(data.agent),
|
|
581
|
+
});
|
|
582
|
+
return {
|
|
583
|
+
tree,
|
|
584
|
+
flatPages,
|
|
585
|
+
url: currentUrl,
|
|
586
|
+
title,
|
|
587
|
+
description,
|
|
588
|
+
rawContent: humanRawContent,
|
|
589
|
+
readingTime,
|
|
590
|
+
readingTimeFormat: readingTimeOptions.format,
|
|
591
|
+
sourcePath: toSourcePath(ctx.contentDirRel, relPath, rootDir),
|
|
592
|
+
entry,
|
|
593
|
+
locale: ctx.locale,
|
|
594
|
+
...(isIndex ? {} : { slug }),
|
|
595
|
+
previousPage,
|
|
596
|
+
nextPage,
|
|
597
|
+
editOnGithub,
|
|
598
|
+
lastModified,
|
|
599
|
+
structuredData,
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
const searchIndexByEntry = new Map();
|
|
603
|
+
function getSearchIndex(ctx) {
|
|
604
|
+
const key = ctx.locale ?? "__default__";
|
|
605
|
+
const cached = searchIndexByEntry.get(key);
|
|
606
|
+
if (cached)
|
|
607
|
+
return cached;
|
|
608
|
+
const index = preloaded
|
|
609
|
+
? searchIndexFromMap(preloaded, ctx.dirPrefix, entry, ctx.locale, preloadedSitemapManifest)
|
|
610
|
+
: loadDocsContent(ctx.contentDirAbs, entry);
|
|
611
|
+
searchIndexByEntry.set(key, index);
|
|
612
|
+
return index;
|
|
613
|
+
}
|
|
614
|
+
const llmsSiteTitle = typeof config.nav === "object" && typeof config.nav?.title === "string"
|
|
615
|
+
? config.nav.title
|
|
616
|
+
: "Documentation";
|
|
617
|
+
const llmsTxtConfig = config.llmsTxt;
|
|
618
|
+
const llmsBaseUrl = typeof llmsTxtConfig === "object" ? (llmsTxtConfig.baseUrl ?? "") : "";
|
|
619
|
+
const markdownMetadataBaseUrl = resolveDocsMetadataBaseUrl(config);
|
|
620
|
+
const contentChangesConfig = resolveDocsContentChangesConfig(config.agent?.contentChanges, {
|
|
621
|
+
staticExport: config.staticExport === true,
|
|
622
|
+
});
|
|
623
|
+
const contentChangeFeed = createDocsContentChangeFeed(config.agent?.contentChanges);
|
|
624
|
+
const llmsTitle = typeof llmsTxtConfig === "object" ? (llmsTxtConfig.siteTitle ?? llmsSiteTitle) : llmsSiteTitle;
|
|
625
|
+
const llmsDesc = typeof llmsTxtConfig === "object" ? llmsTxtConfig.siteDescription : undefined;
|
|
626
|
+
const llmsEnabled = llmsTxtConfig !== false &&
|
|
627
|
+
!(llmsTxtConfig && typeof llmsTxtConfig === "object" && llmsTxtConfig.enabled === false);
|
|
628
|
+
const apiCatalogEnabled = typeof llmsTxtConfig !== "object" || llmsTxtConfig.apiCatalog !== false;
|
|
629
|
+
const configuredApiRoute = typeof config.cloud?.apiRoute === "string" && config.cloud.apiRoute.trim()
|
|
630
|
+
? config.cloud.apiRoute
|
|
631
|
+
: undefined;
|
|
632
|
+
const discoveryLinkHeader = getDocsDiscoveryLinkHeader({
|
|
633
|
+
includeApiCatalog: apiCatalogEnabled,
|
|
634
|
+
includeAgentCard: Boolean(config.agent?.a2a),
|
|
635
|
+
});
|
|
636
|
+
const agentManifestLinkHeader = getDocsAgentManifestLinkHeader(discoveryLinkHeader);
|
|
637
|
+
const openapiDiscovery = resolveApiReferenceOpenApiDiscovery(config.apiReference);
|
|
638
|
+
const mcpConfig = resolveDocsMcpConfig(config.mcp, {
|
|
639
|
+
defaultName: llmsTitle,
|
|
640
|
+
});
|
|
641
|
+
const agentFeedbackConfig = resolveDocsAgentFeedbackConfig(config.feedback);
|
|
642
|
+
const agentFeedbackDiscovery = {
|
|
643
|
+
enabled: agentFeedbackConfig.enabled,
|
|
644
|
+
route: "/api/docs?feedback=agent",
|
|
645
|
+
schemaRoute: "/api/docs?feedback=agent&schema=1",
|
|
646
|
+
};
|
|
647
|
+
const llmsCache = new Map();
|
|
648
|
+
function trackTelemetryRequest(request) {
|
|
649
|
+
emitDocsTelemetryProjectEvent(config, {
|
|
650
|
+
framework: "farmjs",
|
|
651
|
+
request,
|
|
652
|
+
});
|
|
653
|
+
const surface = inferDocsTelemetryAgentSurface(request, {
|
|
654
|
+
entry,
|
|
655
|
+
llmsTxt: config.llmsTxt,
|
|
656
|
+
feedback: config.feedback,
|
|
657
|
+
});
|
|
658
|
+
if (!surface)
|
|
659
|
+
return;
|
|
660
|
+
emitDocsTelemetryAgentSurfaceEvent(config, {
|
|
661
|
+
framework: "farmjs",
|
|
662
|
+
request,
|
|
663
|
+
surface,
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
function getLlmsContent(ctx, apiRoute) {
|
|
667
|
+
const cached = llmsCache.get(ctx.locale);
|
|
668
|
+
if (cached?.apiRoute === apiRoute)
|
|
669
|
+
return cached.content;
|
|
670
|
+
const next = renderDocsLlmsTxt(getSearchIndex(ctx), {
|
|
671
|
+
siteTitle: llmsTitle,
|
|
672
|
+
siteDescription: llmsDesc,
|
|
673
|
+
baseUrl: llmsBaseUrl,
|
|
674
|
+
apiRoute,
|
|
675
|
+
maxChars: typeof llmsTxtConfig === "object" ? llmsTxtConfig.maxChars : undefined,
|
|
676
|
+
sections: typeof llmsTxtConfig === "object" ? llmsTxtConfig.sections : undefined,
|
|
677
|
+
openapi: openapiDiscovery,
|
|
678
|
+
apiCatalog: apiCatalogEnabled,
|
|
679
|
+
});
|
|
680
|
+
llmsCache.set(ctx.locale, { apiRoute, content: next });
|
|
681
|
+
return next;
|
|
682
|
+
}
|
|
683
|
+
function resolveLocaleForMcp(locale) {
|
|
684
|
+
if (!i18n)
|
|
685
|
+
return undefined;
|
|
686
|
+
if (locale && i18n.locales.includes(locale))
|
|
687
|
+
return locale;
|
|
688
|
+
return i18n.defaultLocale;
|
|
689
|
+
}
|
|
690
|
+
function getMarkdownRepresentation(ctx, requestedPath, origin) {
|
|
691
|
+
const page = findDocsMarkdownPage(entry, getSearchIndex(ctx), requestedPath);
|
|
692
|
+
return page
|
|
693
|
+
? {
|
|
694
|
+
document: renderDocsMarkdownDocument(page, { origin, sitemap: config.sitemap }),
|
|
695
|
+
lastModified: resolveDocsRetrievalLastModified(page, "agent"),
|
|
696
|
+
}
|
|
697
|
+
: null;
|
|
698
|
+
}
|
|
699
|
+
function createDiscoveryOptions(origin, apiRoute) {
|
|
700
|
+
return {
|
|
701
|
+
origin,
|
|
702
|
+
entry,
|
|
703
|
+
apiRoute,
|
|
704
|
+
docsPath: config.docsPath,
|
|
705
|
+
apiCatalog: apiCatalogEnabled,
|
|
706
|
+
i18n,
|
|
707
|
+
search: config.search,
|
|
708
|
+
contentChanges: contentChangesConfig.enabled,
|
|
709
|
+
mcp: mcpConfig,
|
|
710
|
+
feedback: agentFeedbackDiscovery,
|
|
711
|
+
llms: {
|
|
712
|
+
enabled: llmsEnabled,
|
|
713
|
+
apiCatalog: apiCatalogEnabled,
|
|
714
|
+
baseUrl: llmsBaseUrl || undefined,
|
|
715
|
+
siteTitle: llmsTitle,
|
|
716
|
+
siteDescription: llmsDesc,
|
|
717
|
+
maxChars: typeof llmsTxtConfig === "object" ? llmsTxtConfig.maxChars : undefined,
|
|
718
|
+
sections: typeof llmsTxtConfig === "object" ? llmsTxtConfig.sections : undefined,
|
|
719
|
+
},
|
|
720
|
+
sitemap: config.sitemap,
|
|
721
|
+
robots: config.robots,
|
|
722
|
+
openapi: openapiDiscovery,
|
|
723
|
+
markdown: {
|
|
724
|
+
acceptHeader: true,
|
|
725
|
+
},
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
async function resolveStandardsResponse(request, url, discoveryOptions) {
|
|
729
|
+
const resolved = resolveDocsStandardsDiscoveryRequest(url, {
|
|
730
|
+
apiRoute: discoveryOptions.apiRoute,
|
|
731
|
+
});
|
|
732
|
+
if (!resolved)
|
|
733
|
+
return null;
|
|
734
|
+
const method = request.method.toUpperCase();
|
|
735
|
+
const needsSkill = (method === "GET" || method === "HEAD") && resolved.kind !== "api-catalog";
|
|
736
|
+
return createDocsStandardsDiscoveryResponse({
|
|
737
|
+
request,
|
|
738
|
+
...discoveryOptions,
|
|
739
|
+
preferredSkillDocument: needsSkill ? readRootSkillDocument(preloaded, rootDir) : null,
|
|
740
|
+
fallbackSkillDocument: needsSkill ? renderDocsSkillDocument(discoveryOptions) : "",
|
|
741
|
+
publishedSkills: needsSkill ? await getPublishedAgentSkills() : [],
|
|
742
|
+
agentCard: config.agent?.a2a,
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
async function GET(event) {
|
|
746
|
+
trackTelemetryRequest(event.request);
|
|
747
|
+
const ctx = resolveContextFromRequest(event.request);
|
|
748
|
+
const url = new URL(event.request.url);
|
|
749
|
+
const isHeadRequest = event.request.method.toUpperCase() === "HEAD";
|
|
750
|
+
const requestApiRoute = resolveDocsRequestApiRoute(url, configuredApiRoute);
|
|
751
|
+
if (isDocsConfigRequest(url)) {
|
|
752
|
+
return new Response(JSON.stringify(buildDocsConfigMap(config), null, 2), {
|
|
753
|
+
headers: {
|
|
754
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
755
|
+
"Cache-Control": "public, max-age=0, s-maxage=3600",
|
|
756
|
+
"X-Robots-Tag": "noindex",
|
|
757
|
+
},
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
if (isDocsDiagnosticsRequest(url)) {
|
|
761
|
+
return new Response(JSON.stringify(buildDocsDiagnostics(config, {
|
|
762
|
+
adapter: "farmjs",
|
|
763
|
+
entry,
|
|
764
|
+
apiRoute: requestApiRoute,
|
|
765
|
+
i18n,
|
|
766
|
+
mcp: mcpConfig,
|
|
767
|
+
feedback: agentFeedbackDiscovery,
|
|
768
|
+
openapi: openapiDiscovery,
|
|
769
|
+
}), null, 2), {
|
|
770
|
+
headers: {
|
|
771
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
772
|
+
"Cache-Control": "public, max-age=0, s-maxage=3600",
|
|
773
|
+
"X-Robots-Tag": "noindex",
|
|
774
|
+
},
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
const discoveryApiRoute = requestApiRoute;
|
|
778
|
+
const discoveryOptions = createDiscoveryOptions(url.origin, discoveryApiRoute);
|
|
779
|
+
const standardsDiscoveryResponse = await resolveStandardsResponse(event.request, url, discoveryOptions);
|
|
780
|
+
if (standardsDiscoveryResponse)
|
|
781
|
+
return standardsDiscoveryResponse;
|
|
782
|
+
if (isDocsAgentDiscoveryRequest(url, { apiRoute: discoveryApiRoute })) {
|
|
783
|
+
const content = `${JSON.stringify(buildDocsAgentDiscoverySpec({
|
|
784
|
+
...discoveryOptions,
|
|
785
|
+
publishedSkills: [
|
|
786
|
+
await resolveDocsPublishedAgentSkill({
|
|
787
|
+
preferredDocument: readRootSkillDocument(preloaded, rootDir),
|
|
788
|
+
fallbackDocument: renderDocsSkillDocument(discoveryOptions),
|
|
789
|
+
}),
|
|
790
|
+
...(await getPublishedAgentSkills()),
|
|
791
|
+
],
|
|
792
|
+
agentCard: config.agent?.a2a,
|
|
793
|
+
}), null, 2)}\n`;
|
|
794
|
+
return createDocsCacheableResponse({
|
|
795
|
+
request: event.request,
|
|
796
|
+
content,
|
|
797
|
+
headers: {
|
|
798
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
799
|
+
"Cache-Control": "public, max-age=0, s-maxage=3600",
|
|
800
|
+
"X-Robots-Tag": "noindex",
|
|
801
|
+
Link: agentManifestLinkHeader,
|
|
802
|
+
},
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
if (isDocsContentChangesRequest(url)) {
|
|
806
|
+
if (!contentChangesConfig.enabled) {
|
|
807
|
+
return new Response("Not Found", {
|
|
808
|
+
status: 404,
|
|
809
|
+
headers: { "Content-Type": "text/plain; charset=utf-8", "X-Robots-Tag": "noindex" },
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
return createDocsContentChangesHttpResponse({
|
|
813
|
+
request: event.request,
|
|
814
|
+
feed: contentChangeFeed,
|
|
815
|
+
pages: getSearchIndex(ctx),
|
|
816
|
+
search: config.search,
|
|
817
|
+
locale: ctx.locale,
|
|
818
|
+
baseUrl: markdownMetadataBaseUrl || url.origin,
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
if (isApiReferenceOpenApiRequest(url)) {
|
|
822
|
+
if (!openapiDiscovery.enabled) {
|
|
823
|
+
return new Response("Not Found", {
|
|
824
|
+
status: 404,
|
|
825
|
+
headers: {
|
|
826
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
827
|
+
"X-Robots-Tag": "noindex",
|
|
828
|
+
},
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
if (isHeadRequest) {
|
|
832
|
+
return new Response(null, {
|
|
833
|
+
headers: {
|
|
834
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
835
|
+
"Cache-Control": "public, max-age=0, s-maxage=3600",
|
|
836
|
+
"X-Robots-Tag": "noindex",
|
|
837
|
+
},
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
const document = await buildApiReferenceOpenApiDocumentAsync(config, {
|
|
841
|
+
framework: "farmjs",
|
|
842
|
+
rootDir,
|
|
843
|
+
baseUrl: url.origin,
|
|
844
|
+
});
|
|
845
|
+
return new Response(JSON.stringify(document, null, 2), {
|
|
846
|
+
headers: {
|
|
847
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
848
|
+
"Cache-Control": "public, max-age=0, s-maxage=3600",
|
|
849
|
+
"X-Robots-Tag": "noindex",
|
|
850
|
+
},
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
const agentFeedbackRequest = resolveDocsAgentFeedbackRequest(url, agentFeedbackConfig);
|
|
854
|
+
if (agentFeedbackRequest) {
|
|
855
|
+
if (agentFeedbackRequest.kind === "submit") {
|
|
856
|
+
return new Response(JSON.stringify({ error: "Method Not Allowed" }), {
|
|
857
|
+
status: 405,
|
|
858
|
+
headers: {
|
|
859
|
+
Allow: "POST",
|
|
860
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
861
|
+
},
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
return new Response(isHeadRequest ? null : JSON.stringify(agentFeedbackConfig.schema, null, 2), {
|
|
865
|
+
headers: {
|
|
866
|
+
"Content-Type": "application/schema+json; charset=utf-8",
|
|
867
|
+
"Cache-Control": "public, max-age=0, s-maxage=3600",
|
|
868
|
+
"X-Robots-Tag": "noindex",
|
|
869
|
+
},
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
if (isDocsAgentsRequest(url, { apiRoute: discoveryApiRoute }) ||
|
|
873
|
+
resolveDocsAgentsFormat(url) === "agents") {
|
|
874
|
+
const content = readRootAgentsDocument(preloaded, rootDir) ?? renderDocsAgentsDocument(discoveryOptions);
|
|
875
|
+
return createDocsCacheableResponse({
|
|
876
|
+
request: event.request,
|
|
877
|
+
content,
|
|
878
|
+
headers: {
|
|
879
|
+
"Content-Type": "text/markdown; charset=utf-8",
|
|
880
|
+
"Cache-Control": "public, max-age=0, s-maxage=3600",
|
|
881
|
+
"X-Robots-Tag": "noindex",
|
|
882
|
+
Link: discoveryLinkHeader,
|
|
883
|
+
},
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
if (isDocsSkillRequest(url, { apiRoute: discoveryApiRoute }) ||
|
|
887
|
+
resolveDocsSkillFormat(url) === "skill") {
|
|
888
|
+
const content = readRootSkillDocument(preloaded, rootDir) ?? renderDocsSkillDocument(discoveryOptions);
|
|
889
|
+
return createDocsCacheableResponse({
|
|
890
|
+
request: event.request,
|
|
891
|
+
content,
|
|
892
|
+
headers: {
|
|
893
|
+
"Content-Type": "text/markdown; charset=utf-8",
|
|
894
|
+
"Cache-Control": "public, max-age=0, s-maxage=3600",
|
|
895
|
+
"X-Robots-Tag": "noindex",
|
|
896
|
+
Link: discoveryLinkHeader,
|
|
897
|
+
},
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
const sitemapResponse = createDocsSitemapResponse({
|
|
901
|
+
request: event.request,
|
|
902
|
+
apiRoute: discoveryApiRoute,
|
|
903
|
+
sitemap: config.sitemap,
|
|
904
|
+
entry,
|
|
905
|
+
siteTitle: llmsTitle,
|
|
906
|
+
baseUrl: llmsBaseUrl || url.origin,
|
|
907
|
+
pages: getSearchIndex(ctx),
|
|
908
|
+
manifest: preloadedSitemapManifest ?? readDocsSitemapManifest(rootDir, config.sitemap),
|
|
909
|
+
});
|
|
910
|
+
if (sitemapResponse)
|
|
911
|
+
return sitemapResponse;
|
|
912
|
+
const robotsResponse = createDocsRobotsResponse({
|
|
913
|
+
agentCard: Boolean(config.agent?.a2a),
|
|
914
|
+
request: event.request,
|
|
915
|
+
entry,
|
|
916
|
+
apiCatalog: apiCatalogEnabled,
|
|
917
|
+
sitemap: config.sitemap,
|
|
918
|
+
baseUrl: llmsBaseUrl || url.origin,
|
|
919
|
+
robots: config.robots,
|
|
920
|
+
});
|
|
921
|
+
if (robotsResponse)
|
|
922
|
+
return robotsResponse;
|
|
923
|
+
const markdownRequest = resolveDocsMarkdownRequest(entry, url, event.request, {
|
|
924
|
+
apiRoute: discoveryApiRoute,
|
|
925
|
+
});
|
|
926
|
+
if (markdownRequest) {
|
|
927
|
+
const markdownOrigin = markdownMetadataBaseUrl || url.origin;
|
|
928
|
+
const representation = getMarkdownRepresentation(ctx, markdownRequest.requestedPath, markdownOrigin);
|
|
929
|
+
return createDocsMarkdownResponse({
|
|
930
|
+
request: event.request,
|
|
931
|
+
apiRoute: discoveryApiRoute,
|
|
932
|
+
document: representation?.document ?? null,
|
|
933
|
+
entry,
|
|
934
|
+
requestedPath: markdownRequest.requestedPath,
|
|
935
|
+
origin: markdownOrigin,
|
|
936
|
+
locale: ctx.locale,
|
|
937
|
+
lastModified: representation?.lastModified,
|
|
938
|
+
pages: getSearchIndex(ctx),
|
|
939
|
+
sitemap: config.sitemap,
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
const llmsRequest = resolveDocsLlmsTxtRequest(url, llmsTxtConfig, entry, {
|
|
943
|
+
apiRoute: discoveryApiRoute,
|
|
944
|
+
});
|
|
945
|
+
if (llmsRequest) {
|
|
946
|
+
if (!llmsEnabled) {
|
|
947
|
+
return new Response("Not Found", {
|
|
948
|
+
status: 404,
|
|
949
|
+
headers: {
|
|
950
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
951
|
+
"X-Robots-Tag": "noindex",
|
|
952
|
+
},
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
const selected = selectDocsLlmsTxtContent(getLlmsContent(ctx, requestApiRoute), llmsRequest);
|
|
956
|
+
if (!selected) {
|
|
957
|
+
return new Response("Not Found", {
|
|
958
|
+
status: 404,
|
|
959
|
+
headers: {
|
|
960
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
961
|
+
"X-Robots-Tag": "noindex",
|
|
962
|
+
},
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
const budgetIssue = getDocsLlmsTxtMaxCharsIssue(selected.label, selected.content, selected.maxChars);
|
|
966
|
+
if (budgetIssue?.mode === "error") {
|
|
967
|
+
return new Response(budgetIssue.message, {
|
|
968
|
+
status: 500,
|
|
969
|
+
headers: {
|
|
970
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
971
|
+
"X-Robots-Tag": "noindex",
|
|
972
|
+
},
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
if (budgetIssue?.mode === "warn") {
|
|
976
|
+
console.warn(`[docs] ${budgetIssue.message}`);
|
|
977
|
+
}
|
|
978
|
+
return createDocsCacheableResponse({
|
|
979
|
+
request: event.request,
|
|
980
|
+
content: selected.content,
|
|
981
|
+
headers: {
|
|
982
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
983
|
+
"Cache-Control": "public, max-age=3600",
|
|
984
|
+
},
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
const query = url.searchParams.get("query")?.trim();
|
|
988
|
+
const audience = resolveDocsSearchAudience(url.searchParams.get("audience"));
|
|
989
|
+
let searchRequest;
|
|
990
|
+
try {
|
|
991
|
+
searchRequest = resolveDocsSearchRequest(url.searchParams);
|
|
992
|
+
}
|
|
993
|
+
catch (error) {
|
|
994
|
+
const searchError = resolveDocsSearchError(error);
|
|
995
|
+
if (!searchError)
|
|
996
|
+
throw error;
|
|
997
|
+
return Response.json({ error: searchError }, { status: 400 });
|
|
998
|
+
}
|
|
999
|
+
const { filters, structured, explain, facets, facet, cursor, limit } = searchRequest;
|
|
1000
|
+
if (!query && !structured && !facets) {
|
|
1001
|
+
return new Response("[]", {
|
|
1002
|
+
headers: { "Content-Type": "application/json" },
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
const searchOptions = {
|
|
1006
|
+
pages: getSearchIndex(ctx),
|
|
1007
|
+
query: query ?? "",
|
|
1008
|
+
search: resolveSearchRequestConfig(config.search, event.request.url, {
|
|
1009
|
+
localMcp: config.mcp,
|
|
1010
|
+
}),
|
|
1011
|
+
audience,
|
|
1012
|
+
filters,
|
|
1013
|
+
explain,
|
|
1014
|
+
locale: ctx.locale,
|
|
1015
|
+
pathname: url.searchParams.get("pathname") ?? undefined,
|
|
1016
|
+
siteTitle: llmsTitle,
|
|
1017
|
+
baseUrl: markdownMetadataBaseUrl || url.origin,
|
|
1018
|
+
syncBaseUrl: markdownMetadataBaseUrl ?? null,
|
|
1019
|
+
facet,
|
|
1020
|
+
cursor,
|
|
1021
|
+
limit,
|
|
1022
|
+
};
|
|
1023
|
+
const searchStartedAt = Date.now();
|
|
1024
|
+
let searchResponse;
|
|
1025
|
+
try {
|
|
1026
|
+
searchResponse = facets
|
|
1027
|
+
? await buildDocsSearchFacets(searchOptions)
|
|
1028
|
+
: structured
|
|
1029
|
+
? await performDocsSearchWithMetadata(searchOptions)
|
|
1030
|
+
: query
|
|
1031
|
+
? await performDocsSearch(searchOptions)
|
|
1032
|
+
: [];
|
|
1033
|
+
}
|
|
1034
|
+
catch (error) {
|
|
1035
|
+
const searchError = resolveDocsSearchError(error);
|
|
1036
|
+
if (!searchError)
|
|
1037
|
+
throw error;
|
|
1038
|
+
return Response.json({ error: searchError }, { status: 400 });
|
|
1039
|
+
}
|
|
1040
|
+
if (facets) {
|
|
1041
|
+
return Response.json(searchResponse, {
|
|
1042
|
+
headers: { "Cache-Control": "public, max-age=60, stale-while-revalidate=300" },
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
if (!query) {
|
|
1046
|
+
return Response.json(searchResponse);
|
|
1047
|
+
}
|
|
1048
|
+
const resultCount = Array.isArray(searchResponse)
|
|
1049
|
+
? searchResponse.length
|
|
1050
|
+
: "resultCount" in searchResponse
|
|
1051
|
+
? searchResponse.resultCount
|
|
1052
|
+
: searchResponse.matchedPageCount;
|
|
1053
|
+
await emitDocsAnalyticsEvent(analytics, {
|
|
1054
|
+
type: "api_search",
|
|
1055
|
+
source: "server",
|
|
1056
|
+
url: `${url.origin}${url.pathname}`,
|
|
1057
|
+
path: url.pathname,
|
|
1058
|
+
locale: ctx.locale,
|
|
1059
|
+
input: { query },
|
|
1060
|
+
properties: {
|
|
1061
|
+
queryLength: query.length,
|
|
1062
|
+
audience,
|
|
1063
|
+
resultCount,
|
|
1064
|
+
pathname: url.searchParams.get("pathname") ?? undefined,
|
|
1065
|
+
durationMs: Math.max(0, Date.now() - searchStartedAt),
|
|
1066
|
+
},
|
|
1067
|
+
});
|
|
1068
|
+
return new Response(JSON.stringify(searchResponse), {
|
|
1069
|
+
headers: { "Content-Type": "application/json" },
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
const projectName = typeof config.nav?.title === "string" ? config.nav.title : null;
|
|
1073
|
+
const packageName = aiConfig.packageName;
|
|
1074
|
+
const docsUrl = aiConfig.docsUrl;
|
|
1075
|
+
function buildDefaultSystemPrompt() {
|
|
1076
|
+
const lines = [
|
|
1077
|
+
`You are a helpful documentation assistant${projectName ? ` for ${projectName}` : ""}.`,
|
|
1078
|
+
"Answer only from the provided documentation context.",
|
|
1079
|
+
"Prefer exact code/config snippets from the context when the question asks how to implement something.",
|
|
1080
|
+
"Cite the relevant documentation URL when you use a source.",
|
|
1081
|
+
"Use only URLs exactly as they appear in the context; do not invent placeholder domains.",
|
|
1082
|
+
'Never use placeholder package names or imports such as "your-auth-library", "your-package", "your-sdk", "replace-me", or "example-library". If the exact package or import is not in the context, do not include an import snippet.',
|
|
1083
|
+
"Be concise and accurate. If the answer is not in the context, say so honestly.",
|
|
1084
|
+
"Use markdown formatting for code examples and links.",
|
|
1085
|
+
];
|
|
1086
|
+
if (packageName) {
|
|
1087
|
+
lines.push(`When showing import examples, use "${packageName}" as the package name and prefer exact imports copied from the documentation context.`);
|
|
1088
|
+
}
|
|
1089
|
+
if (docsUrl) {
|
|
1090
|
+
lines.push(`When linking to documentation pages, use "${docsUrl}" as the base URL (e.g. ${docsUrl}/docs/get-started).`);
|
|
1091
|
+
}
|
|
1092
|
+
return lines.join(" ");
|
|
1093
|
+
}
|
|
1094
|
+
const DEFAULT_SYSTEM_PROMPT = buildDefaultSystemPrompt();
|
|
1095
|
+
async function POST(event) {
|
|
1096
|
+
trackTelemetryRequest(event.request);
|
|
1097
|
+
const requestUrl = new URL(event.request.url);
|
|
1098
|
+
const standardsDiscoveryResponse = await resolveStandardsResponse(event.request, requestUrl, createDiscoveryOptions(requestUrl.origin, resolveDocsRequestApiRoute(requestUrl, configuredApiRoute)));
|
|
1099
|
+
if (standardsDiscoveryResponse)
|
|
1100
|
+
return standardsDiscoveryResponse;
|
|
1101
|
+
const agentFeedbackRequest = resolveDocsAgentFeedbackRequest(requestUrl, agentFeedbackConfig);
|
|
1102
|
+
if (agentFeedbackRequest) {
|
|
1103
|
+
if (agentFeedbackRequest.kind === "schema") {
|
|
1104
|
+
return new Response(JSON.stringify({ error: "Method Not Allowed" }), {
|
|
1105
|
+
status: 405,
|
|
1106
|
+
headers: {
|
|
1107
|
+
Allow: "GET",
|
|
1108
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
1109
|
+
},
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
const parsed = await parseDocsAgentFeedbackData(event.request);
|
|
1113
|
+
if (!parsed.ok)
|
|
1114
|
+
return parsed.response;
|
|
1115
|
+
const payloadError = validateDocsAgentFeedbackPayload(parsed.data.payload, agentFeedbackConfig.payloadSchema);
|
|
1116
|
+
if (payloadError)
|
|
1117
|
+
return Response.json({ error: payloadError }, { status: 400 });
|
|
1118
|
+
if (!agentFeedbackConfig.onFeedback) {
|
|
1119
|
+
return Response.json({ ok: true, handled: false }, { status: 202 });
|
|
1120
|
+
}
|
|
1121
|
+
await agentFeedbackConfig.onFeedback(parsed.data);
|
|
1122
|
+
return Response.json({ ok: true, handled: true }, { status: 201 });
|
|
1123
|
+
}
|
|
1124
|
+
const requestStartedAt = Date.now();
|
|
1125
|
+
const trace = createDocsAgentTraceContext("ask-ai");
|
|
1126
|
+
const runSpanId = createDocsAgentTraceId("span");
|
|
1127
|
+
const traceBase = {
|
|
1128
|
+
source: "server",
|
|
1129
|
+
traceId: trace.traceId,
|
|
1130
|
+
url: event.request.url,
|
|
1131
|
+
path: requestUrl.pathname,
|
|
1132
|
+
};
|
|
1133
|
+
async function emitTrace(traceEvent) {
|
|
1134
|
+
await emitDocsAgentTraceEvent(observability, {
|
|
1135
|
+
...traceBase,
|
|
1136
|
+
...traceEvent,
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
async function emitRunError(reason, outputPreview = {}) {
|
|
1140
|
+
const endedAt = new Date().toISOString();
|
|
1141
|
+
const elapsed = Math.max(0, Date.now() - requestStartedAt);
|
|
1142
|
+
const common = {
|
|
1143
|
+
name: "ask-ai",
|
|
1144
|
+
startedAt: trace.startedAt,
|
|
1145
|
+
endedAt,
|
|
1146
|
+
durationMs: elapsed,
|
|
1147
|
+
status: "error",
|
|
1148
|
+
outputPreview: {
|
|
1149
|
+
reason,
|
|
1150
|
+
...outputPreview,
|
|
1151
|
+
},
|
|
1152
|
+
metadata: { reason },
|
|
1153
|
+
};
|
|
1154
|
+
await emitTrace({ ...common, type: "error", parentSpanId: runSpanId });
|
|
1155
|
+
await emitTrace({ ...common, type: "run.error", spanId: runSpanId });
|
|
1156
|
+
await emitTrace({ ...common, type: "run.end", spanId: runSpanId });
|
|
1157
|
+
}
|
|
1158
|
+
await emitTrace({
|
|
1159
|
+
type: "run.start",
|
|
1160
|
+
name: "ask-ai",
|
|
1161
|
+
spanId: runSpanId,
|
|
1162
|
+
startedAt: trace.startedAt,
|
|
1163
|
+
durationMs: 0,
|
|
1164
|
+
status: "started",
|
|
1165
|
+
inputPreview: {
|
|
1166
|
+
method: event.request.method,
|
|
1167
|
+
path: requestUrl.pathname,
|
|
1168
|
+
},
|
|
1169
|
+
});
|
|
1170
|
+
if (!aiConfig.enabled) {
|
|
1171
|
+
await emitDocsAnalyticsEvent(analytics, {
|
|
1172
|
+
type: "api_ai_error",
|
|
1173
|
+
source: "server",
|
|
1174
|
+
url: event.request.url,
|
|
1175
|
+
path: requestUrl.pathname,
|
|
1176
|
+
properties: {
|
|
1177
|
+
reason: "disabled",
|
|
1178
|
+
},
|
|
1179
|
+
});
|
|
1180
|
+
await emitRunError("disabled", { status: 404 });
|
|
1181
|
+
return new Response(JSON.stringify({
|
|
1182
|
+
error: "AI is not enabled. Set `ai: { enabled: true }` in your docs config to enable it.",
|
|
1183
|
+
}), { status: 404, headers: { "Content-Type": "application/json" } });
|
|
1184
|
+
}
|
|
1185
|
+
const resolvedKey = aiConfig.apiKey ?? process.env?.OPENAI_API_KEY;
|
|
1186
|
+
if (!resolvedKey) {
|
|
1187
|
+
await emitDocsAnalyticsEvent(analytics, {
|
|
1188
|
+
type: "api_ai_error",
|
|
1189
|
+
source: "server",
|
|
1190
|
+
url: event.request.url,
|
|
1191
|
+
path: requestUrl.pathname,
|
|
1192
|
+
properties: {
|
|
1193
|
+
reason: "missing_api_key",
|
|
1194
|
+
durationMs: Math.max(0, Date.now() - requestStartedAt),
|
|
1195
|
+
},
|
|
1196
|
+
});
|
|
1197
|
+
await emitRunError("missing_api_key", { status: 500 });
|
|
1198
|
+
return new Response(JSON.stringify({
|
|
1199
|
+
error: "AI is enabled but no API key was found. Set `apiKey` in your docs config `ai` section or add OPENAI_API_KEY to your environment.",
|
|
1200
|
+
}), { status: 500, headers: { "Content-Type": "application/json" } });
|
|
1201
|
+
}
|
|
1202
|
+
const ctx = resolveContextFromRequest(event.request);
|
|
1203
|
+
let body;
|
|
1204
|
+
try {
|
|
1205
|
+
body = await event.request.json();
|
|
1206
|
+
}
|
|
1207
|
+
catch {
|
|
1208
|
+
await emitDocsAnalyticsEvent(analytics, {
|
|
1209
|
+
type: "api_ai_error",
|
|
1210
|
+
source: "server",
|
|
1211
|
+
url: event.request.url,
|
|
1212
|
+
path: requestUrl.pathname,
|
|
1213
|
+
locale: ctx.locale,
|
|
1214
|
+
properties: {
|
|
1215
|
+
reason: "invalid_json",
|
|
1216
|
+
durationMs: Math.max(0, Date.now() - requestStartedAt),
|
|
1217
|
+
},
|
|
1218
|
+
});
|
|
1219
|
+
await emitRunError("invalid_json", { status: 400, locale: ctx.locale });
|
|
1220
|
+
return new Response(JSON.stringify({ error: "Invalid JSON body. Expected { messages: [...] }" }), { status: 400, headers: { "Content-Type": "application/json" } });
|
|
1221
|
+
}
|
|
1222
|
+
const messages = body.messages;
|
|
1223
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
1224
|
+
await emitDocsAnalyticsEvent(analytics, {
|
|
1225
|
+
type: "api_ai_error",
|
|
1226
|
+
source: "server",
|
|
1227
|
+
url: event.request.url,
|
|
1228
|
+
path: requestUrl.pathname,
|
|
1229
|
+
locale: ctx.locale,
|
|
1230
|
+
properties: {
|
|
1231
|
+
reason: "missing_messages",
|
|
1232
|
+
durationMs: Math.max(0, Date.now() - requestStartedAt),
|
|
1233
|
+
},
|
|
1234
|
+
});
|
|
1235
|
+
await emitRunError("missing_messages", { status: 400, locale: ctx.locale });
|
|
1236
|
+
return new Response(JSON.stringify({ error: "messages array is required and must not be empty." }), { status: 400, headers: { "Content-Type": "application/json" } });
|
|
1237
|
+
}
|
|
1238
|
+
const lastUserMessage = [...messages].reverse().find((message) => message.role === "user");
|
|
1239
|
+
if (!lastUserMessage) {
|
|
1240
|
+
await emitDocsAnalyticsEvent(analytics, {
|
|
1241
|
+
type: "api_ai_error",
|
|
1242
|
+
source: "server",
|
|
1243
|
+
url: event.request.url,
|
|
1244
|
+
path: requestUrl.pathname,
|
|
1245
|
+
locale: ctx.locale,
|
|
1246
|
+
properties: {
|
|
1247
|
+
reason: "missing_user_message",
|
|
1248
|
+
messageCount: messages.length,
|
|
1249
|
+
durationMs: Math.max(0, Date.now() - requestStartedAt),
|
|
1250
|
+
},
|
|
1251
|
+
});
|
|
1252
|
+
await emitRunError("missing_user_message", {
|
|
1253
|
+
status: 400,
|
|
1254
|
+
locale: ctx.locale,
|
|
1255
|
+
messageCount: messages.length,
|
|
1256
|
+
});
|
|
1257
|
+
return new Response(JSON.stringify({ error: "At least one user message is required." }), {
|
|
1258
|
+
status: 400,
|
|
1259
|
+
headers: { "Content-Type": "application/json" },
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1262
|
+
const maxResults = aiConfig.maxResults ?? 5;
|
|
1263
|
+
await emitTrace({
|
|
1264
|
+
type: "user.input",
|
|
1265
|
+
name: "ask-ai",
|
|
1266
|
+
parentSpanId: runSpanId,
|
|
1267
|
+
startedAt: new Date().toISOString(),
|
|
1268
|
+
endedAt: new Date().toISOString(),
|
|
1269
|
+
durationMs: 0,
|
|
1270
|
+
status: "success",
|
|
1271
|
+
locale: ctx.locale,
|
|
1272
|
+
inputPreview: {
|
|
1273
|
+
messageCount: messages.length,
|
|
1274
|
+
questionLength: lastUserMessage.content.length,
|
|
1275
|
+
requestedModel: typeof body.model === "string" && body.model.trim().length > 0
|
|
1276
|
+
? body.model.trim()
|
|
1277
|
+
: undefined,
|
|
1278
|
+
},
|
|
1279
|
+
});
|
|
1280
|
+
const retrievalStartedAt = Date.now();
|
|
1281
|
+
const retrievalStartedAtIso = new Date().toISOString();
|
|
1282
|
+
const retrievalSpanId = createDocsAgentTraceId("span");
|
|
1283
|
+
await emitTrace({
|
|
1284
|
+
type: "retrieval.query",
|
|
1285
|
+
name: "docs-index",
|
|
1286
|
+
spanId: retrievalSpanId,
|
|
1287
|
+
parentSpanId: runSpanId,
|
|
1288
|
+
startedAt: retrievalStartedAtIso,
|
|
1289
|
+
status: "started",
|
|
1290
|
+
locale: ctx.locale,
|
|
1291
|
+
inputPreview: {
|
|
1292
|
+
queryLength: lastUserMessage.content.length,
|
|
1293
|
+
maxResults,
|
|
1294
|
+
},
|
|
1295
|
+
});
|
|
1296
|
+
const retrieval = await buildDocsAskAIContext({
|
|
1297
|
+
pages: getSearchIndex(ctx),
|
|
1298
|
+
query: lastUserMessage.content,
|
|
1299
|
+
search: resolveAskAISearchRequestConfig({
|
|
1300
|
+
search: config.search,
|
|
1301
|
+
useMcp: aiConfig.useMcp,
|
|
1302
|
+
mcpEndpoint: mcpConfig.route,
|
|
1303
|
+
mcpEnabled: mcpConfig.enabled,
|
|
1304
|
+
mcpSearchEnabled: mcpConfig.tools.searchDocs,
|
|
1305
|
+
requestUrl: event.request.url,
|
|
1306
|
+
}),
|
|
1307
|
+
locale: ctx.locale,
|
|
1308
|
+
pathname: requestUrl.searchParams.get("pathname") ?? undefined,
|
|
1309
|
+
siteTitle: llmsTitle,
|
|
1310
|
+
baseUrl: markdownMetadataBaseUrl || requestUrl.origin,
|
|
1311
|
+
syncBaseUrl: markdownMetadataBaseUrl ?? null,
|
|
1312
|
+
limit: maxResults,
|
|
1313
|
+
});
|
|
1314
|
+
const scored = retrieval.results;
|
|
1315
|
+
await emitTrace({
|
|
1316
|
+
type: "retrieval.result",
|
|
1317
|
+
name: "docs-index",
|
|
1318
|
+
parentSpanId: retrievalSpanId,
|
|
1319
|
+
startedAt: retrievalStartedAtIso,
|
|
1320
|
+
endedAt: new Date().toISOString(),
|
|
1321
|
+
durationMs: Math.max(0, Date.now() - retrievalStartedAt),
|
|
1322
|
+
status: "success",
|
|
1323
|
+
locale: ctx.locale,
|
|
1324
|
+
outputPreview: {
|
|
1325
|
+
resultCount: scored.length,
|
|
1326
|
+
urls: scored.slice(0, 5).map((doc) => doc.url),
|
|
1327
|
+
},
|
|
1328
|
+
metadata: { maxResults },
|
|
1329
|
+
});
|
|
1330
|
+
const promptStartedAt = Date.now();
|
|
1331
|
+
const promptStartedAtIso = new Date().toISOString();
|
|
1332
|
+
const promptSpanId = createDocsAgentTraceId("span");
|
|
1333
|
+
const context = retrieval.context;
|
|
1334
|
+
const systemPrompt = aiConfig.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
1335
|
+
const packageHintsPrompt = formatDocsAskAIPackageHints(retrieval.packageHints, packageName);
|
|
1336
|
+
const fullSystemPrompt = [systemPrompt, packageHintsPrompt].filter(Boolean).join("\n\n");
|
|
1337
|
+
const systemMessage = {
|
|
1338
|
+
role: "system",
|
|
1339
|
+
content: context
|
|
1340
|
+
? `${fullSystemPrompt}\n\n---\n\nDocumentation context:\n\n${context}`
|
|
1341
|
+
: fullSystemPrompt,
|
|
1342
|
+
};
|
|
1343
|
+
const llmMessages = [
|
|
1344
|
+
systemMessage,
|
|
1345
|
+
...messages.filter((message) => message.role !== "system"),
|
|
1346
|
+
];
|
|
1347
|
+
await emitTrace({
|
|
1348
|
+
type: "prompt.build",
|
|
1349
|
+
name: "ask-ai.prompt",
|
|
1350
|
+
spanId: promptSpanId,
|
|
1351
|
+
parentSpanId: runSpanId,
|
|
1352
|
+
startedAt: promptStartedAtIso,
|
|
1353
|
+
endedAt: new Date().toISOString(),
|
|
1354
|
+
durationMs: Math.max(0, Date.now() - promptStartedAt),
|
|
1355
|
+
status: "success",
|
|
1356
|
+
locale: ctx.locale,
|
|
1357
|
+
inputPreview: {
|
|
1358
|
+
messageCount: messages.length,
|
|
1359
|
+
retrievedCount: scored.length,
|
|
1360
|
+
},
|
|
1361
|
+
outputPreview: {
|
|
1362
|
+
llmMessageCount: llmMessages.length,
|
|
1363
|
+
contextChars: context.length,
|
|
1364
|
+
systemMessageChars: systemMessage.content.length,
|
|
1365
|
+
},
|
|
1366
|
+
});
|
|
1367
|
+
const requestedModel = typeof body.model === "string" && body.model.trim().length > 0
|
|
1368
|
+
? body.model.trim()
|
|
1369
|
+
: undefined;
|
|
1370
|
+
const resolved = resolveAIModelAndProvider(aiConfig, requestedModel);
|
|
1371
|
+
const finalKey = resolved.apiKey ?? resolvedKey;
|
|
1372
|
+
await emitDocsAnalyticsEvent(analytics, {
|
|
1373
|
+
type: "api_ai_request",
|
|
1374
|
+
source: "server",
|
|
1375
|
+
url: event.request.url,
|
|
1376
|
+
path: requestUrl.pathname,
|
|
1377
|
+
locale: ctx.locale,
|
|
1378
|
+
input: { question: lastUserMessage.content },
|
|
1379
|
+
properties: {
|
|
1380
|
+
messageCount: messages.length,
|
|
1381
|
+
questionLength: lastUserMessage.content.length,
|
|
1382
|
+
retrievedCount: scored.length,
|
|
1383
|
+
model: resolved.model,
|
|
1384
|
+
},
|
|
1385
|
+
});
|
|
1386
|
+
const modelStartedAt = Date.now();
|
|
1387
|
+
const modelStartedAtIso = new Date().toISOString();
|
|
1388
|
+
const modelSpanId = createDocsAgentTraceId("span");
|
|
1389
|
+
const providerOrigin = safeUrlOrigin(resolved.baseUrl);
|
|
1390
|
+
await emitTrace({
|
|
1391
|
+
type: "model.call",
|
|
1392
|
+
name: resolved.model,
|
|
1393
|
+
spanId: modelSpanId,
|
|
1394
|
+
parentSpanId: runSpanId,
|
|
1395
|
+
startedAt: modelStartedAtIso,
|
|
1396
|
+
status: "started",
|
|
1397
|
+
locale: ctx.locale,
|
|
1398
|
+
inputPreview: {
|
|
1399
|
+
messageCount: llmMessages.length,
|
|
1400
|
+
stream: true,
|
|
1401
|
+
providerOrigin,
|
|
1402
|
+
},
|
|
1403
|
+
metadata: { model: resolved.model },
|
|
1404
|
+
});
|
|
1405
|
+
let llmResponse;
|
|
1406
|
+
try {
|
|
1407
|
+
llmResponse = await fetch(`${resolved.baseUrl}/chat/completions`, {
|
|
1408
|
+
method: "POST",
|
|
1409
|
+
headers: {
|
|
1410
|
+
"Content-Type": "application/json",
|
|
1411
|
+
Authorization: `Bearer ${finalKey}`,
|
|
1412
|
+
},
|
|
1413
|
+
body: JSON.stringify({ model: resolved.model, stream: true, messages: llmMessages }),
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
1416
|
+
catch (error) {
|
|
1417
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1418
|
+
await emitTrace({
|
|
1419
|
+
type: "model.error",
|
|
1420
|
+
name: resolved.model,
|
|
1421
|
+
parentSpanId: modelSpanId,
|
|
1422
|
+
startedAt: modelStartedAtIso,
|
|
1423
|
+
endedAt: new Date().toISOString(),
|
|
1424
|
+
durationMs: Math.max(0, Date.now() - modelStartedAt),
|
|
1425
|
+
status: "error",
|
|
1426
|
+
locale: ctx.locale,
|
|
1427
|
+
outputPreview: { message },
|
|
1428
|
+
metadata: { model: resolved.model, providerOrigin },
|
|
1429
|
+
});
|
|
1430
|
+
await emitDocsAnalyticsEvent(analytics, {
|
|
1431
|
+
type: "api_ai_error",
|
|
1432
|
+
source: "server",
|
|
1433
|
+
url: event.request.url,
|
|
1434
|
+
path: requestUrl.pathname,
|
|
1435
|
+
locale: ctx.locale,
|
|
1436
|
+
input: { question: lastUserMessage.content },
|
|
1437
|
+
properties: {
|
|
1438
|
+
reason: "llm_fetch_error",
|
|
1439
|
+
messageCount: messages.length,
|
|
1440
|
+
questionLength: lastUserMessage.content.length,
|
|
1441
|
+
retrievedCount: scored.length,
|
|
1442
|
+
model: resolved.model,
|
|
1443
|
+
durationMs: Math.max(0, Date.now() - requestStartedAt),
|
|
1444
|
+
},
|
|
1445
|
+
});
|
|
1446
|
+
await emitRunError("llm_fetch_error", {
|
|
1447
|
+
status: 502,
|
|
1448
|
+
locale: ctx.locale,
|
|
1449
|
+
messageCount: messages.length,
|
|
1450
|
+
questionLength: lastUserMessage.content.length,
|
|
1451
|
+
retrievedCount: scored.length,
|
|
1452
|
+
model: resolved.model,
|
|
1453
|
+
});
|
|
1454
|
+
return new Response(JSON.stringify({ error: "LLM API request failed." }), {
|
|
1455
|
+
status: 502,
|
|
1456
|
+
headers: { "Content-Type": "application/json" },
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1459
|
+
if (!llmResponse.ok) {
|
|
1460
|
+
const errText = await llmResponse.text().catch(() => "Unknown error");
|
|
1461
|
+
await emitTrace({
|
|
1462
|
+
type: "model.error",
|
|
1463
|
+
name: resolved.model,
|
|
1464
|
+
parentSpanId: modelSpanId,
|
|
1465
|
+
startedAt: modelStartedAtIso,
|
|
1466
|
+
endedAt: new Date().toISOString(),
|
|
1467
|
+
durationMs: Math.max(0, Date.now() - modelStartedAt),
|
|
1468
|
+
status: "error",
|
|
1469
|
+
locale: ctx.locale,
|
|
1470
|
+
outputPreview: {
|
|
1471
|
+
status: llmResponse.status,
|
|
1472
|
+
errorChars: errText.length,
|
|
1473
|
+
},
|
|
1474
|
+
metadata: { model: resolved.model, providerOrigin },
|
|
1475
|
+
});
|
|
1476
|
+
await emitDocsAnalyticsEvent(analytics, {
|
|
1477
|
+
type: "api_ai_error",
|
|
1478
|
+
source: "server",
|
|
1479
|
+
url: event.request.url,
|
|
1480
|
+
path: requestUrl.pathname,
|
|
1481
|
+
locale: ctx.locale,
|
|
1482
|
+
input: { question: lastUserMessage.content },
|
|
1483
|
+
properties: {
|
|
1484
|
+
reason: "llm_error",
|
|
1485
|
+
status: llmResponse.status,
|
|
1486
|
+
messageCount: messages.length,
|
|
1487
|
+
questionLength: lastUserMessage.content.length,
|
|
1488
|
+
retrievedCount: scored.length,
|
|
1489
|
+
model: resolved.model,
|
|
1490
|
+
durationMs: Math.max(0, Date.now() - requestStartedAt),
|
|
1491
|
+
},
|
|
1492
|
+
});
|
|
1493
|
+
await emitRunError("llm_error", {
|
|
1494
|
+
status: 502,
|
|
1495
|
+
modelStatus: llmResponse.status,
|
|
1496
|
+
locale: ctx.locale,
|
|
1497
|
+
messageCount: messages.length,
|
|
1498
|
+
questionLength: lastUserMessage.content.length,
|
|
1499
|
+
retrievedCount: scored.length,
|
|
1500
|
+
model: resolved.model,
|
|
1501
|
+
});
|
|
1502
|
+
return new Response(JSON.stringify({ error: `LLM API error (${llmResponse.status}): ${errText}` }), { status: 502, headers: { "Content-Type": "application/json" } });
|
|
1503
|
+
}
|
|
1504
|
+
await emitDocsAnalyticsEvent(analytics, {
|
|
1505
|
+
type: "api_ai_response",
|
|
1506
|
+
source: "server",
|
|
1507
|
+
url: event.request.url,
|
|
1508
|
+
path: requestUrl.pathname,
|
|
1509
|
+
locale: ctx.locale,
|
|
1510
|
+
input: { question: lastUserMessage.content },
|
|
1511
|
+
properties: {
|
|
1512
|
+
messageCount: messages.length,
|
|
1513
|
+
questionLength: lastUserMessage.content.length,
|
|
1514
|
+
retrievedCount: scored.length,
|
|
1515
|
+
model: resolved.model,
|
|
1516
|
+
durationMs: Math.max(0, Date.now() - requestStartedAt),
|
|
1517
|
+
},
|
|
1518
|
+
});
|
|
1519
|
+
const responseEndedAt = new Date().toISOString();
|
|
1520
|
+
const modelDurationMs = Math.max(0, Date.now() - modelStartedAt);
|
|
1521
|
+
await emitTrace({
|
|
1522
|
+
type: "model.response",
|
|
1523
|
+
name: resolved.model,
|
|
1524
|
+
parentSpanId: modelSpanId,
|
|
1525
|
+
startedAt: modelStartedAtIso,
|
|
1526
|
+
endedAt: responseEndedAt,
|
|
1527
|
+
durationMs: modelDurationMs,
|
|
1528
|
+
status: "success",
|
|
1529
|
+
locale: ctx.locale,
|
|
1530
|
+
outputPreview: {
|
|
1531
|
+
status: llmResponse.status,
|
|
1532
|
+
stream: true,
|
|
1533
|
+
contentType: llmResponse.headers.get("content-type") ?? undefined,
|
|
1534
|
+
},
|
|
1535
|
+
metadata: { model: resolved.model, providerOrigin },
|
|
1536
|
+
});
|
|
1537
|
+
await emitTrace({
|
|
1538
|
+
type: "model.stream",
|
|
1539
|
+
name: resolved.model,
|
|
1540
|
+
parentSpanId: modelSpanId,
|
|
1541
|
+
startedAt: modelStartedAtIso,
|
|
1542
|
+
endedAt: responseEndedAt,
|
|
1543
|
+
durationMs: modelDurationMs,
|
|
1544
|
+
status: "success",
|
|
1545
|
+
locale: ctx.locale,
|
|
1546
|
+
outputPreview: { stream: true },
|
|
1547
|
+
metadata: { model: resolved.model },
|
|
1548
|
+
});
|
|
1549
|
+
const runDurationMs = Math.max(0, Date.now() - requestStartedAt);
|
|
1550
|
+
await emitTrace({
|
|
1551
|
+
type: "agent.final",
|
|
1552
|
+
name: "ask-ai",
|
|
1553
|
+
parentSpanId: runSpanId,
|
|
1554
|
+
startedAt: trace.startedAt,
|
|
1555
|
+
endedAt: new Date().toISOString(),
|
|
1556
|
+
durationMs: runDurationMs,
|
|
1557
|
+
status: "success",
|
|
1558
|
+
locale: ctx.locale,
|
|
1559
|
+
outputPreview: {
|
|
1560
|
+
stream: true,
|
|
1561
|
+
retrievedCount: scored.length,
|
|
1562
|
+
},
|
|
1563
|
+
metadata: { model: resolved.model },
|
|
1564
|
+
});
|
|
1565
|
+
await emitTrace({
|
|
1566
|
+
type: "run.end",
|
|
1567
|
+
name: "ask-ai",
|
|
1568
|
+
spanId: runSpanId,
|
|
1569
|
+
startedAt: trace.startedAt,
|
|
1570
|
+
endedAt: new Date().toISOString(),
|
|
1571
|
+
durationMs: runDurationMs,
|
|
1572
|
+
status: "success",
|
|
1573
|
+
locale: ctx.locale,
|
|
1574
|
+
outputPreview: {
|
|
1575
|
+
stream: true,
|
|
1576
|
+
retrievedCount: scored.length,
|
|
1577
|
+
},
|
|
1578
|
+
metadata: { model: resolved.model },
|
|
1579
|
+
});
|
|
1580
|
+
return new Response(llmResponse.body, {
|
|
1581
|
+
headers: {
|
|
1582
|
+
"Content-Type": "text/event-stream",
|
|
1583
|
+
"Cache-Control": "no-cache",
|
|
1584
|
+
Connection: "keep-alive",
|
|
1585
|
+
},
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
const mcpSiteTitle = typeof config.nav?.title === "string" ? config.nav.title : "Documentation";
|
|
1589
|
+
const MCP = createDocsMcpHttpHandler({
|
|
1590
|
+
source: {
|
|
1591
|
+
entry,
|
|
1592
|
+
siteTitle: mcpSiteTitle,
|
|
1593
|
+
baseUrl: markdownMetadataBaseUrl || undefined,
|
|
1594
|
+
resolveLocale: resolveLocaleForMcp,
|
|
1595
|
+
getPages(locale) {
|
|
1596
|
+
const ctx = resolveContextFromPath(`/${entry}`, resolveLocaleForMcp(locale));
|
|
1597
|
+
return getSearchIndex(ctx);
|
|
1598
|
+
},
|
|
1599
|
+
getNavigation(locale) {
|
|
1600
|
+
const ctx = resolveContextFromPath(`/${entry}`, resolveLocaleForMcp(locale));
|
|
1601
|
+
return applySidebarFolderIndexBehavior(preloaded
|
|
1602
|
+
? navTreeFromMap(preloaded, ctx.dirPrefix, entry, ordering)
|
|
1603
|
+
: loadDocsNavTree(ctx.contentDirAbs, entry, ordering), {
|
|
1604
|
+
sidebar: config.sidebar,
|
|
1605
|
+
defaultBehavior: "link",
|
|
1606
|
+
});
|
|
1607
|
+
},
|
|
1608
|
+
async getSkills(context) {
|
|
1609
|
+
const origin = context?.request
|
|
1610
|
+
? new URL(context.request.url).origin
|
|
1611
|
+
: llmsBaseUrl || "http://localhost";
|
|
1612
|
+
const skillOptions = {
|
|
1613
|
+
origin,
|
|
1614
|
+
entry,
|
|
1615
|
+
apiRoute: configuredApiRoute,
|
|
1616
|
+
docsPath: config.docsPath,
|
|
1617
|
+
apiCatalog: apiCatalogEnabled,
|
|
1618
|
+
i18n,
|
|
1619
|
+
search: config.search,
|
|
1620
|
+
mcp: mcpConfig,
|
|
1621
|
+
feedback: agentFeedbackDiscovery,
|
|
1622
|
+
llms: {
|
|
1623
|
+
enabled: llmsEnabled,
|
|
1624
|
+
apiCatalog: apiCatalogEnabled,
|
|
1625
|
+
baseUrl: llmsBaseUrl || undefined,
|
|
1626
|
+
siteTitle: llmsTitle,
|
|
1627
|
+
siteDescription: llmsDesc,
|
|
1628
|
+
},
|
|
1629
|
+
sitemap: config.sitemap,
|
|
1630
|
+
robots: config.robots,
|
|
1631
|
+
openapi: openapiDiscovery,
|
|
1632
|
+
};
|
|
1633
|
+
const rootSkill = await resolveDocsPublishedAgentSkill({
|
|
1634
|
+
preferredDocument: readRootSkillDocument(preloaded, rootDir),
|
|
1635
|
+
fallbackDocument: renderDocsSkillDocument(skillOptions),
|
|
1636
|
+
});
|
|
1637
|
+
return [rootSkill, ...(await getPublishedAgentSkills())];
|
|
1638
|
+
},
|
|
1639
|
+
},
|
|
1640
|
+
mcp: config.mcp,
|
|
1641
|
+
contentChanges: config.agent?.contentChanges,
|
|
1642
|
+
evaluations: config.agent?.evaluations,
|
|
1643
|
+
contentChangeFeed,
|
|
1644
|
+
analytics,
|
|
1645
|
+
telemetry: config.telemetry,
|
|
1646
|
+
telemetryFramework: "farmjs",
|
|
1647
|
+
observability,
|
|
1648
|
+
defaultName: mcpSiteTitle,
|
|
1649
|
+
});
|
|
1650
|
+
async function HEAD(event) {
|
|
1651
|
+
const response = await GET(event);
|
|
1652
|
+
return new Response(null, {
|
|
1653
|
+
status: response.status,
|
|
1654
|
+
statusText: response.statusText,
|
|
1655
|
+
headers: response.headers,
|
|
1656
|
+
});
|
|
1657
|
+
}
|
|
1658
|
+
async function handle(request) {
|
|
1659
|
+
const method = request.method.toUpperCase();
|
|
1660
|
+
const url = new URL(request.url);
|
|
1661
|
+
const pathname = url.pathname === "/" ? "/" : url.pathname.replace(/\/+$/, "");
|
|
1662
|
+
const mcpRoutes = new Set([mcpConfig.route, "/mcp", "/.well-known/mcp"].map((route) => route === "/" ? route : route.replace(/\/+$/, "")));
|
|
1663
|
+
const docsPath = `/${String(entry).replace(/^\/+|\/+$/g, "")}`;
|
|
1664
|
+
const apiPath = `/${String(configuredApiRoute ?? "api/docs").replace(/^\/+|\/+$/g, "")}`;
|
|
1665
|
+
const configuredApiReferencePath = config.apiReference &&
|
|
1666
|
+
typeof config.apiReference === "object" &&
|
|
1667
|
+
typeof config.apiReference.path === "string"
|
|
1668
|
+
? config.apiReference.path
|
|
1669
|
+
: "api-reference";
|
|
1670
|
+
const apiReferencePath = `/${configuredApiReferencePath.replace(/^\/+|\/+$/g, "")}`;
|
|
1671
|
+
const publicDocsPaths = new Set([
|
|
1672
|
+
"/llms.txt",
|
|
1673
|
+
"/llms-full.txt",
|
|
1674
|
+
"/sitemap.xml",
|
|
1675
|
+
"/sitemap.md",
|
|
1676
|
+
"/robots.txt",
|
|
1677
|
+
"/skill.md",
|
|
1678
|
+
"/AGENTS.md",
|
|
1679
|
+
"/agent.md",
|
|
1680
|
+
"/.well-known/api-catalog",
|
|
1681
|
+
"/.well-known/agent.json",
|
|
1682
|
+
"/.well-known/agent",
|
|
1683
|
+
"/.well-known/agent-card.json",
|
|
1684
|
+
"/.well-known/agent-skills/index.json",
|
|
1685
|
+
"/.well-known/skills/index.json",
|
|
1686
|
+
"/.well-known/skill.md",
|
|
1687
|
+
"/.well-known/sitemap.md",
|
|
1688
|
+
"/.well-known/AGENTS.md",
|
|
1689
|
+
]);
|
|
1690
|
+
const isDocsRequest = pathname === docsPath ||
|
|
1691
|
+
pathname === `${docsPath}.md` ||
|
|
1692
|
+
pathname.startsWith(`${docsPath}/`) ||
|
|
1693
|
+
pathname === apiPath ||
|
|
1694
|
+
pathname.startsWith(`${apiPath}/`) ||
|
|
1695
|
+
pathname === apiReferencePath ||
|
|
1696
|
+
publicDocsPaths.has(pathname) ||
|
|
1697
|
+
pathname.startsWith("/.well-known/agent-skills/");
|
|
1698
|
+
if (mcpConfig.enabled && mcpRoutes.has(pathname)) {
|
|
1699
|
+
if (method === "GET" || method === "POST" || method === "DELETE" || method === "OPTIONS") {
|
|
1700
|
+
return MCP[method]({ request });
|
|
1701
|
+
}
|
|
1702
|
+
return new Response("Method Not Allowed", {
|
|
1703
|
+
status: 405,
|
|
1704
|
+
headers: { Allow: "GET, POST, DELETE, OPTIONS" },
|
|
1705
|
+
});
|
|
1706
|
+
}
|
|
1707
|
+
if (!isDocsRequest)
|
|
1708
|
+
return null;
|
|
1709
|
+
if (method === "GET") {
|
|
1710
|
+
const response = await GET({ request });
|
|
1711
|
+
return response.status === 404 ? null : response;
|
|
1712
|
+
}
|
|
1713
|
+
if (method === "HEAD") {
|
|
1714
|
+
const response = await HEAD({ request });
|
|
1715
|
+
return response.status === 404 ? null : response;
|
|
1716
|
+
}
|
|
1717
|
+
if (method === "POST") {
|
|
1718
|
+
const response = await POST({ request });
|
|
1719
|
+
return response.status === 404 ? null : response;
|
|
1720
|
+
}
|
|
1721
|
+
return null;
|
|
1722
|
+
}
|
|
1723
|
+
return { load, GET, HEAD, POST, MCP, handle };
|
|
1724
|
+
}
|
|
1725
|
+
//# sourceMappingURL=server.js.map
|