blume 1.4.3 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/dist/cli/index.js +1621 -576
- package/dist/cli/index.js.map +109 -104
- package/dist/types/ai/component-markdown.d.ts +14 -4
- package/dist/types/core/config-input.d.ts +79 -27
- package/dist/types/core/config.d.ts +2 -1
- package/dist/types/core/data.d.ts +16 -1
- package/dist/types/core/diagnostics.d.ts +5 -1
- package/dist/types/core/i18n-ui.d.ts +12 -0
- package/dist/types/core/schema.d.ts +112 -15
- package/dist/types/core/sources/types.d.ts +3 -1
- package/dist/types/core/standard-schema.d.ts +7 -3
- package/dist/types/core/types.d.ts +43 -2
- package/dist/types/core/ui-packs/index.d.ts +9 -1
- package/dist/types/openapi/references.d.ts +6 -5
- package/dist/types/seo/x-handle.d.ts +3 -2
- package/docs/advanced/api-reference.mdx +8 -6
- package/docs/configuration/search.mdx +2 -0
- package/docs/configuration/seo.mdx +1 -1
- package/docs/content/i18n.mdx +1 -1
- package/docs/content/meta.mdx +2 -1
- package/docs/content/meta.ts +1 -0
- package/docs/content/navigation.mdx +35 -1
- package/docs/content/versioning.mdx +106 -0
- package/docs/reference/cli.mdx +1 -0
- package/docs/reference/frontmatter.mdx +3 -0
- package/package.json +3 -1
- package/skills/blume-migrate/SKILL.md +2 -2
- package/skills/blume-migrate/references/docusaurus.md +1 -1
- package/skills/blume-migrate/references/fumadocs.md +1 -1
- package/skills/blume-migrate/references/mintlify.md +1 -1
- package/src/ai/agent-readability.ts +37 -10
- package/src/ai/ask-context.ts +5 -1
- package/src/ai/ask.ts +10 -1
- package/src/ai/component-markdown.ts +80 -43
- package/src/ai/llms.ts +40 -16
- package/src/ai/mcp/data.ts +48 -12
- package/src/ai/mcp/discovery.ts +28 -11
- package/src/ai/mcp/server.ts +183 -38
- package/src/ai/mcp/tools.ts +3 -3
- package/src/ai/skills.ts +32 -9
- package/src/ai/visibility.ts +2 -2
- package/src/astro/component-slots.ts +2 -0
- package/src/astro/examples.ts +6 -2
- package/src/astro/generate.ts +54 -29
- package/src/astro/integration.ts +13 -2
- package/src/astro/islands.ts +16 -9
- package/src/astro/templates.ts +152 -33
- package/src/audit/agent.ts +2 -2
- package/src/audit/checks/content.ts +26 -11
- package/src/audit/checks/dns-aid.ts +3 -0
- package/src/audit/checks/indexability.ts +24 -6
- package/src/audit/checks/llms.ts +9 -4
- package/src/audit/checks/network.ts +2 -0
- package/src/audit/checks/social.ts +18 -10
- package/src/audit/crawl.ts +37 -9
- package/src/audit/report.ts +20 -19
- package/src/audit/run.ts +5 -2
- package/src/audit/snapshot.ts +2 -4
- package/src/audit/types.ts +25 -3
- package/src/blume-modules.d.ts +5 -1
- package/src/cli/commands/audit.ts +9 -4
- package/src/cli/commands/build.ts +15 -9
- package/src/cli/commands/dev.ts +2 -0
- package/src/cli/commands/doctor.ts +2 -0
- package/src/cli/commands/eval.ts +7 -3
- package/src/cli/commands/init.ts +9 -9
- package/src/cli/commands/mcp-stdio.ts +3 -0
- package/src/cli/commands/translate.ts +14 -3
- package/src/cli/commands/version.ts +85 -0
- package/src/cli/dev-lock.ts +31 -10
- package/src/cli/eject-scripts.ts +17 -2
- package/src/cli/index.ts +2 -0
- package/src/cli/init/questions.ts +1 -1
- package/src/cli/init/scaffold.ts +22 -15
- package/src/cli/internal-error.ts +1 -0
- package/src/components/content/auto-type-table.ts +3 -0
- package/src/components/content/diff.ts +9 -5
- package/src/components/content/github-info.ts +2 -0
- package/src/components/islands/ask-ai.tsx +33 -25
- package/src/components/islands/hooks.ts +5 -1
- package/src/components/islands/webmcp.ts +49 -12
- package/src/components/layout/Header.astro +25 -1
- package/src/components/layout/NavSelector.astro +11 -2
- package/src/components/layout/NavTree.astro +4 -2
- package/src/components/layout/RootLayout.astro +18 -0
- package/src/components/layout/Search.astro +77 -13
- package/src/components/layout/VersionBanner.astro +39 -0
- package/src/components/layout/analytics-client.ts +8 -5
- package/src/components/layout/hydration-hint.ts +1 -1
- package/src/components/layout/nav-utils.ts +1 -4
- package/src/components/layout/overrides.ts +25 -12
- package/src/components/layout/search/algolia.ts +18 -5
- package/src/components/layout/search/endpoint.ts +3 -0
- package/src/components/layout/search/flexsearch.ts +23 -7
- package/src/components/layout/search/orama-cloud.ts +1 -1
- package/src/components/layout/search/orama.ts +4 -1
- package/src/components/layout/search/pagefind.ts +2 -0
- package/src/components/layout/search/types.ts +13 -1
- package/src/components/layout/search/typesense.ts +19 -3
- package/src/components/openapi/ApiOverview.astro +32 -6
- package/src/components/openapi/AsyncApiOperation.astro +237 -0
- package/src/components/openapi/Bindings.astro +89 -0
- package/src/components/openapi/MethodBadge.astro +3 -0
- package/src/components/openapi/Operation.astro +7 -2
- package/src/components/openapi/PanelTabs.astro +131 -0
- package/src/components/openapi/ParametersTable.astro +2 -0
- package/src/components/openapi/RequestPanel.astro +12 -119
- package/src/components/openapi/async-snippets.ts +174 -0
- package/src/components/openapi/async.ts +348 -0
- package/src/components/openapi/helpers.ts +52 -20
- package/src/components/openapi/security.ts +102 -29
- package/src/components/openapi/snippets.ts +11 -11
- package/src/core/component-overrides.ts +28 -23
- package/src/core/config-input.ts +88 -27
- package/src/core/config.ts +20 -7
- package/src/core/content.ts +3 -1
- package/src/core/data.ts +16 -1
- package/src/core/define-components.ts +5 -0
- package/src/core/diagnostics.ts +46 -38
- package/src/core/frontmatter.ts +33 -7
- package/src/core/graph.ts +137 -53
- package/src/core/i18n-ui.ts +15 -0
- package/src/core/i18n.ts +16 -8
- package/src/core/load-module.ts +1 -0
- package/src/core/manifest.ts +92 -3
- package/src/core/meta.ts +44 -14
- package/src/core/nav-diagnostics.ts +3 -3
- package/src/core/navigation.ts +247 -67
- package/src/core/project-graph.ts +15 -3
- package/src/core/schema.ts +213 -67
- package/src/core/sources/assets.ts +2 -0
- package/src/core/sources/cache.ts +6 -0
- package/src/core/sources/github-releases.ts +39 -31
- package/src/core/sources/mdx-remote.ts +4 -0
- package/src/core/sources/normalize.ts +67 -20
- package/src/core/sources/notion.ts +49 -17
- package/src/core/sources/portable-text.ts +32 -11
- package/src/core/sources/sanity.ts +68 -14
- package/src/core/sources/types.ts +4 -0
- package/src/core/sources/watch.ts +1 -1
- package/src/core/standard-schema.ts +9 -3
- package/src/core/text-width.ts +26 -0
- package/src/core/tsconfig-aliases.ts +9 -5
- package/src/core/types.ts +45 -2
- package/src/core/ui-packs/index.ts +9 -1
- package/src/core/version-cut.ts +301 -0
- package/src/core/version.ts +2 -0
- package/src/core/versions.ts +170 -0
- package/src/deploy/adapter-output.ts +5 -2
- package/src/deploy/cloudflare-negotiation.ts +25 -10
- package/src/deploy/sitemap.ts +33 -1
- package/src/deploy/vercel-negotiation.ts +11 -4
- package/src/eval/report.ts +4 -4
- package/src/eval/run.ts +2 -2
- package/src/eval/schema.ts +1 -1
- package/src/markdown/base-links.ts +6 -6
- package/src/markdown/directives.ts +7 -1
- package/src/markdown/heading-anchors.ts +17 -6
- package/src/markdown/index.ts +73 -24
- package/src/markdown/inline-code.ts +14 -2
- package/src/markdown/language-icon.ts +6 -2
- package/src/markdown/mdast.ts +18 -4
- package/src/markdown/package-commands.ts +6 -8
- package/src/markdown/table-wrap.ts +4 -1
- package/src/markdown/twoslash.ts +2 -0
- package/src/og/card.ts +30 -11
- package/src/og/derive.ts +43 -27
- package/src/openapi/asyncapi.ts +366 -0
- package/src/openapi/model.ts +126 -57
- package/src/openapi/parse.ts +97 -5
- package/src/openapi/references.ts +12 -10
- package/src/openapi/render-mdx.ts +73 -34
- package/src/openapi/scalar.ts +6 -8
- package/src/openapi/source.ts +98 -28
- package/src/registry/eject.ts +7 -2
- package/src/search/documents.ts +25 -5
- package/src/search/facets.ts +7 -5
- package/src/search/orama-index.ts +66 -20
- package/src/search/popular.ts +10 -5
- package/src/search/providers.ts +2 -2
- package/src/search/sync/index.ts +2 -0
- package/src/search/sync/typesense.ts +4 -2
- package/src/seo/jsonld.ts +24 -6
- package/src/seo/x-handle.ts +8 -3
- package/src/theme/chrome-icons.ts +7 -2
- package/src/theme/fonts.ts +8 -4
- package/src/theme/icons.ts +4 -2
- package/src/theme/palette.ts +22 -14
- package/src/translate/meta.ts +15 -6
- package/src/translate/report.ts +9 -5
- package/src/translate/run.ts +10 -4
- package/src/translate/validate.ts +52 -17
- package/src/translate/work-list.ts +0 -0
package/src/ai/mcp/server.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import type { ServerOptions } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
3
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
3
4
|
import {
|
|
4
5
|
CallToolRequestSchema,
|
|
@@ -33,7 +34,7 @@ const MAX_SEARCH_LIMIT = 20;
|
|
|
33
34
|
/** Excerpt length when a page has no description. */
|
|
34
35
|
const EXCERPT_LENGTH = 200;
|
|
35
36
|
|
|
36
|
-
const CORS_HEADERS
|
|
37
|
+
const CORS_HEADERS = {
|
|
37
38
|
"Access-Control-Allow-Headers":
|
|
38
39
|
"Content-Type, Mcp-Session-Id, Mcp-Protocol-Version",
|
|
39
40
|
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
@@ -65,11 +66,15 @@ const contentTypesField = z.preprocess((value) => {
|
|
|
65
66
|
* The optional facet filter `search_docs` and `list_pages` share. Only
|
|
66
67
|
* string-valued entries survive; an empty `{}` means "no filter".
|
|
67
68
|
*/
|
|
69
|
+
/** Accepts any plain object, so the string-valued entries can be sifted out. */
|
|
70
|
+
const looseFacetObject = z.record(z.string(), z.unknown());
|
|
71
|
+
|
|
68
72
|
const filtersField = z.preprocess((value) => {
|
|
69
|
-
|
|
73
|
+
const candidate = looseFacetObject.safeParse(value);
|
|
74
|
+
if (!candidate.success) {
|
|
70
75
|
return;
|
|
71
76
|
}
|
|
72
|
-
const entries = Object.entries(
|
|
77
|
+
const entries = Object.entries(candidate.data).filter(
|
|
73
78
|
(entry): entry is [string, string] => typeof entry[1] === "string"
|
|
74
79
|
);
|
|
75
80
|
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
|
|
@@ -78,7 +83,9 @@ const filtersField = z.preprocess((value) => {
|
|
|
78
83
|
/** Clamped into range rather than rejected; non-numeric means the default. */
|
|
79
84
|
const limitField = z.preprocess(
|
|
80
85
|
(value) => {
|
|
81
|
-
|
|
86
|
+
// `Number` is the identity on numbers, so one conversion covers both the
|
|
87
|
+
// well-typed call and a numeric string.
|
|
88
|
+
const num = Number(value);
|
|
82
89
|
return Number.isFinite(num)
|
|
83
90
|
? Math.min(Math.max(Math.trunc(num), 1), MAX_SEARCH_LIMIT)
|
|
84
91
|
: undefined;
|
|
@@ -93,26 +100,55 @@ const limitField = z.preprocess(
|
|
|
93
100
|
|
|
94
101
|
/** A required text field; a missing or non-string value coerces to "". */
|
|
95
102
|
const textField = (description: string) =>
|
|
96
|
-
z.preprocess(
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
);
|
|
103
|
+
z.preprocess((value) => {
|
|
104
|
+
const parsed = z.string().safeParse(value);
|
|
105
|
+
return parsed.success ? parsed.data : "";
|
|
106
|
+
}, z.string().describe(description));
|
|
107
|
+
|
|
108
|
+
/** An optional trimmed text field; blank or non-string means "absent". */
|
|
109
|
+
const optionalTextField = (description: string) =>
|
|
110
|
+
z.preprocess((value) => {
|
|
111
|
+
const parsed = z.string().safeParse(value);
|
|
112
|
+
const trimmed = parsed.success ? parsed.data.trim() : "";
|
|
113
|
+
return trimmed || undefined;
|
|
114
|
+
}, z.string().optional().describe(description));
|
|
115
|
+
|
|
116
|
+
/** The optional locale filter `search_docs` and `list_pages` share. */
|
|
117
|
+
const localeField = optionalTextField(
|
|
118
|
+
"Only include pages in this locale (e.g. `fr`). Omit for every language."
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
/** The optional docs-version scope `search_docs` and `list_pages` share. */
|
|
122
|
+
const versionField = optionalTextField(
|
|
123
|
+
'Docs version to scope to on a versioned site: `"latest"` (the default — current docs only), `"all"` (every version), or an archived version id (e.g. `"v1.0"`). Ignored when the site is unversioned.'
|
|
124
|
+
);
|
|
100
125
|
|
|
101
126
|
/** Every tool's input schema — the runtime parse and tools/list source. */
|
|
102
127
|
const TOOL_INPUTS = {
|
|
103
|
-
get_navigation: z.object({
|
|
128
|
+
get_navigation: z.object({
|
|
129
|
+
locale: optionalTextField(
|
|
130
|
+
"Locale whose navigation tree to return (defaults to the default locale)."
|
|
131
|
+
),
|
|
132
|
+
version: optionalTextField(
|
|
133
|
+
"Archived version id whose tree to return (defaults to the current docs)."
|
|
134
|
+
),
|
|
135
|
+
}),
|
|
104
136
|
get_page: z.object({
|
|
105
137
|
route: textField("The page route, e.g. `/guides/install`."),
|
|
106
138
|
}),
|
|
107
139
|
list_pages: z.object({
|
|
108
140
|
contentTypes: contentTypesField,
|
|
109
141
|
filters: filtersField,
|
|
142
|
+
locale: localeField,
|
|
143
|
+
version: versionField,
|
|
110
144
|
}),
|
|
111
145
|
search_docs: z.object({
|
|
112
146
|
contentTypes: contentTypesField,
|
|
113
147
|
filters: filtersField,
|
|
114
148
|
limit: limitField,
|
|
149
|
+
locale: localeField,
|
|
115
150
|
query: textField("The search query."),
|
|
151
|
+
version: versionField,
|
|
116
152
|
}),
|
|
117
153
|
};
|
|
118
154
|
|
|
@@ -122,7 +158,7 @@ const TOOL_INPUTS = {
|
|
|
122
158
|
* runtime strips unknown keys rather than rejecting them, and the advertised
|
|
123
159
|
* schema shouldn't promise stricter validation than the server performs.
|
|
124
160
|
*/
|
|
125
|
-
const inputSchemaFor = (schema: z.ZodType)
|
|
161
|
+
const inputSchemaFor = (schema: z.ZodType) => {
|
|
126
162
|
const {
|
|
127
163
|
$schema: _dialect,
|
|
128
164
|
additionalProperties: _closed,
|
|
@@ -136,12 +172,37 @@ const TOOL_DEFINITIONS = MCP_TOOLS.map((tool) => ({
|
|
|
136
172
|
annotations: tool.annotations,
|
|
137
173
|
description: tool.description,
|
|
138
174
|
inputSchema: inputSchemaFor(
|
|
175
|
+
// SAFETY: TOOL_INPUTS declares a schema for every MCP_TOOLS name; the two
|
|
176
|
+
// lists are maintained together so names and descriptions never drift.
|
|
139
177
|
TOOL_INPUTS[tool.name as keyof typeof TOOL_INPUTS]
|
|
140
178
|
),
|
|
141
179
|
name: tool.name,
|
|
142
180
|
title: tool.title,
|
|
143
181
|
}));
|
|
144
182
|
|
|
183
|
+
/** One `search_docs` result entry; `version` only appears on versioned sites. */
|
|
184
|
+
interface SearchHitPayload {
|
|
185
|
+
contentType: string | undefined;
|
|
186
|
+
excerpt: string;
|
|
187
|
+
facets: Record<string, string> | undefined;
|
|
188
|
+
route: string;
|
|
189
|
+
title: string;
|
|
190
|
+
url: string;
|
|
191
|
+
version?: string;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** One `list_pages` entry; `version` only appears on versioned sites. */
|
|
195
|
+
interface PageListingPayload {
|
|
196
|
+
contentType: string;
|
|
197
|
+
description: string | undefined;
|
|
198
|
+
facets: Record<string, string> | undefined;
|
|
199
|
+
lastModified: string | null;
|
|
200
|
+
route: string;
|
|
201
|
+
title: string;
|
|
202
|
+
url: string;
|
|
203
|
+
version?: string;
|
|
204
|
+
}
|
|
205
|
+
|
|
145
206
|
/** Whether a page's facet values satisfy every requested filter entry. */
|
|
146
207
|
const matchesFacets = (
|
|
147
208
|
facets: Record<string, string> | undefined,
|
|
@@ -149,6 +210,47 @@ const matchesFacets = (
|
|
|
149
210
|
): boolean =>
|
|
150
211
|
Object.entries(filters).every(([key, value]) => facets?.[key] === value);
|
|
151
212
|
|
|
213
|
+
/**
|
|
214
|
+
* Resolve the `version` scope on a versioned site: `undefined` disables the
|
|
215
|
+
* filter (`"all"`), `""` is the current docs (the default — agents almost
|
|
216
|
+
* always want the live documentation), and anything else is an archived id
|
|
217
|
+
* (an unknown id simply matches nothing). On an unversioned site the input is
|
|
218
|
+
* ignored entirely. The input arrives pre-trimmed (blank coerced to absent)
|
|
219
|
+
* from the tool's input schema.
|
|
220
|
+
*/
|
|
221
|
+
const asVersionScope = (
|
|
222
|
+
value: string | undefined,
|
|
223
|
+
data: McpData
|
|
224
|
+
): string | undefined => {
|
|
225
|
+
if (!data.archivedVersions) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (value === "all") {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (value === undefined || value === "latest" || value === "current") {
|
|
232
|
+
return "";
|
|
233
|
+
}
|
|
234
|
+
return value;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Error message for a `get_navigation` version id that isn't a configured
|
|
239
|
+
* archived version, or `null` when the id is valid (or the site is
|
|
240
|
+
* unversioned, where the id is ignored like the other tools' scopes). Unlike
|
|
241
|
+
* `asVersionScope`'s match-nothing filters, a bad id here would otherwise
|
|
242
|
+
* silently return the *current* tree posing as the requested snapshot.
|
|
243
|
+
*/
|
|
244
|
+
const unknownVersionError = (
|
|
245
|
+
versionId: string | undefined,
|
|
246
|
+
data: McpData
|
|
247
|
+
): string | null =>
|
|
248
|
+
versionId &&
|
|
249
|
+
data.archivedVersions &&
|
|
250
|
+
!data.archivedVersions.includes(versionId)
|
|
251
|
+
? `Unknown version "${versionId}". Archived versions: ${data.archivedVersions.join(", ")}.`
|
|
252
|
+
: null;
|
|
253
|
+
|
|
152
254
|
/**
|
|
153
255
|
* Normalize a user-supplied route to a `pages` key (`/`, `/a/b`, no suffix).
|
|
154
256
|
* Accepts a full URL too — `search_docs` hits and llms.txt entries carry
|
|
@@ -196,10 +298,11 @@ const excerptFor = (doc: OramaDoc): string => {
|
|
|
196
298
|
return doc.content.length > EXCERPT_LENGTH ? `${head}…` : head;
|
|
197
299
|
};
|
|
198
300
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
301
|
+
/** A tool call's text result, marked as an error when `isError` is set. */
|
|
302
|
+
const text = (value: string, isError = false) => {
|
|
303
|
+
const content = [{ text: value, type: "text" as const }];
|
|
304
|
+
return isError ? { content, isError: true } : { content };
|
|
305
|
+
};
|
|
203
306
|
|
|
204
307
|
/** Lazily builds the Orama index over a snapshot's documents, once. */
|
|
205
308
|
export type OramaIndexProvider = () => Promise<
|
|
@@ -227,12 +330,12 @@ export const buildServer = (
|
|
|
227
330
|
data: McpData,
|
|
228
331
|
index: OramaIndexProvider
|
|
229
332
|
): Server => {
|
|
333
|
+
const serverOptions: ServerOptions = data.instructions
|
|
334
|
+
? { capabilities: { tools: {} }, instructions: data.instructions }
|
|
335
|
+
: { capabilities: { tools: {} } };
|
|
230
336
|
const server = new Server(
|
|
231
337
|
{ name: data.name, version: data.version },
|
|
232
|
-
|
|
233
|
-
capabilities: { tools: {} },
|
|
234
|
-
...(data.instructions ? { instructions: data.instructions } : {}),
|
|
235
|
-
}
|
|
338
|
+
serverOptions
|
|
236
339
|
);
|
|
237
340
|
|
|
238
341
|
server.setRequestHandler(ListToolsRequestSchema, () => ({
|
|
@@ -252,18 +355,26 @@ export const buildServer = (
|
|
|
252
355
|
{
|
|
253
356
|
contentTypes: input.contentTypes,
|
|
254
357
|
facets: input.filters,
|
|
358
|
+
locale: input.locale,
|
|
359
|
+
version: asVersionScope(input.version, data),
|
|
255
360
|
}
|
|
256
361
|
);
|
|
257
362
|
// `route` is the key `get_page` takes (the tool descriptions promise
|
|
258
363
|
// it); `url` is where the page is served.
|
|
259
|
-
const results = hits.map((doc: OramaDoc) =>
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
364
|
+
const results = hits.map((doc: OramaDoc) => {
|
|
365
|
+
const hit: SearchHitPayload = {
|
|
366
|
+
contentType: doc.contentType,
|
|
367
|
+
excerpt: excerptFor(doc),
|
|
368
|
+
facets: doc.facets,
|
|
369
|
+
route: doc.route,
|
|
370
|
+
title: doc.title,
|
|
371
|
+
url: urlFor(doc.route, data),
|
|
372
|
+
};
|
|
373
|
+
if (data.archivedVersions) {
|
|
374
|
+
hit.version = doc.version ?? "";
|
|
375
|
+
}
|
|
376
|
+
return hit;
|
|
377
|
+
});
|
|
267
378
|
return text(JSON.stringify(results, null, 2));
|
|
268
379
|
}
|
|
269
380
|
|
|
@@ -281,23 +392,33 @@ export const buildServer = (
|
|
|
281
392
|
}
|
|
282
393
|
|
|
283
394
|
if (name === "list_pages") {
|
|
284
|
-
const
|
|
395
|
+
const input = TOOL_INPUTS.list_pages.parse(args);
|
|
396
|
+
const { contentTypes, filters, locale } = input;
|
|
397
|
+
const versionScope = asVersionScope(input.version, data);
|
|
285
398
|
const routes = data.routes.filter(
|
|
286
399
|
(route) =>
|
|
287
400
|
(!contentTypes || contentTypes.includes(route.contentType)) &&
|
|
288
|
-
(!filters || matchesFacets(route.facets, filters))
|
|
401
|
+
(!filters || matchesFacets(route.facets, filters)) &&
|
|
402
|
+
(!locale || route.locale === locale) &&
|
|
403
|
+
(versionScope === undefined || route.version === versionScope)
|
|
289
404
|
);
|
|
290
405
|
return text(
|
|
291
406
|
JSON.stringify(
|
|
292
|
-
routes.map((route) =>
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
407
|
+
routes.map((route) => {
|
|
408
|
+
const listing: PageListingPayload = {
|
|
409
|
+
contentType: route.contentType,
|
|
410
|
+
description: route.description,
|
|
411
|
+
facets: route.facets,
|
|
412
|
+
lastModified: route.lastModified,
|
|
413
|
+
route: route.route,
|
|
414
|
+
title: route.title,
|
|
415
|
+
url: urlFor(route.route, data),
|
|
416
|
+
};
|
|
417
|
+
if (data.archivedVersions) {
|
|
418
|
+
listing.version = route.version;
|
|
419
|
+
}
|
|
420
|
+
return listing;
|
|
421
|
+
}),
|
|
301
422
|
null,
|
|
302
423
|
2
|
|
303
424
|
)
|
|
@@ -305,7 +426,31 @@ export const buildServer = (
|
|
|
305
426
|
}
|
|
306
427
|
|
|
307
428
|
if (name === "get_navigation") {
|
|
308
|
-
|
|
429
|
+
// A version id selects the snapshot's tree; a locale selects its
|
|
430
|
+
// language (falling back through the default locale to any tree the
|
|
431
|
+
// snapshot has). Without a version, a locale selects the current docs'
|
|
432
|
+
// localized tree. An unknown id on a versioned site is an error — the
|
|
433
|
+
// current tree would silently masquerade as the requested snapshot.
|
|
434
|
+
const { locale, version: versionId } =
|
|
435
|
+
TOOL_INPUTS.get_navigation.parse(args);
|
|
436
|
+
const unknownVersion = unknownVersionError(versionId, data);
|
|
437
|
+
if (unknownVersion) {
|
|
438
|
+
return text(unknownVersion, true);
|
|
439
|
+
}
|
|
440
|
+
let { navigation } = data;
|
|
441
|
+
const byLocale = versionId
|
|
442
|
+
? data.navigationByVersion?.[versionId]
|
|
443
|
+
: undefined;
|
|
444
|
+
if (byLocale) {
|
|
445
|
+
navigation =
|
|
446
|
+
(locale ? byLocale[locale] : undefined) ??
|
|
447
|
+
byLocale[data.defaultLocale ?? ""] ??
|
|
448
|
+
Object.values(byLocale)[0] ??
|
|
449
|
+
navigation;
|
|
450
|
+
} else if (locale && data.navigationByLocale?.[locale]) {
|
|
451
|
+
navigation = data.navigationByLocale[locale];
|
|
452
|
+
}
|
|
453
|
+
return text(JSON.stringify(navigation, null, 2));
|
|
309
454
|
}
|
|
310
455
|
|
|
311
456
|
return text(`Unknown tool: ${name}`, true);
|
package/src/ai/mcp/tools.ts
CHANGED
|
@@ -19,7 +19,7 @@ export const MCP_TOOLS: McpToolMeta[] = [
|
|
|
19
19
|
{
|
|
20
20
|
annotations: READ_ONLY,
|
|
21
21
|
description:
|
|
22
|
-
'Full-text search across the documentation. Returns matching pages with their title, route, content type, and a short excerpt; pass `contentTypes` to search only pages of certain types (e.g. `rfc`, `changelog`),
|
|
22
|
+
'Full-text search across the documentation. Returns matching pages with their title, route, content type, and a short excerpt; pass `contentTypes` to search only pages of certain types (e.g. `rfc`, `changelog`), `filters` to require facet values the site declares per type (e.g. `{"status": "enforced"}`), and `locale` to search one language. On a versioned site results default to the current docs — pass `version` to search an archived version (e.g. `"v1.0"`) or `"all"` for every version. Use this first to discover relevant pages, then `get_page` to read one in full.',
|
|
23
23
|
name: "search_docs",
|
|
24
24
|
title: "Search documentation",
|
|
25
25
|
},
|
|
@@ -33,14 +33,14 @@ export const MCP_TOOLS: McpToolMeta[] = [
|
|
|
33
33
|
{
|
|
34
34
|
annotations: READ_ONLY,
|
|
35
35
|
description:
|
|
36
|
-
|
|
36
|
+
'List every documentation page with its route, title, description, content type, and any declared facet values; pass `contentTypes`, `filters`, and/or `locale` to narrow the list. On a versioned site the list defaults to the current docs — pass `version` for an archived version or `"all"`. Useful for enumerating the docs, discovering the types and facets in use, or finding a page when search is too narrow.',
|
|
37
37
|
name: "list_pages",
|
|
38
38
|
title: "List pages",
|
|
39
39
|
},
|
|
40
40
|
{
|
|
41
41
|
annotations: READ_ONLY,
|
|
42
42
|
description:
|
|
43
|
-
"Return the documentation navigation tree (header tabs and the sidebar hierarchy), reflecting how the docs are organized for readers.",
|
|
43
|
+
"Return the documentation navigation tree (header tabs and the sidebar hierarchy), reflecting how the docs are organized for readers. Pass `locale` for a language's tree and, on a versioned site, `version` for an archived snapshot's tree.",
|
|
44
44
|
name: "get_navigation",
|
|
45
45
|
title: "Get navigation",
|
|
46
46
|
},
|
package/src/ai/skills.ts
CHANGED
|
@@ -89,23 +89,46 @@ const collectEntries = async (
|
|
|
89
89
|
return entries;
|
|
90
90
|
};
|
|
91
91
|
|
|
92
|
+
/** The two SKILL.md frontmatter fields the discovery index publishes. */
|
|
93
|
+
interface SkillMeta {
|
|
94
|
+
description: string;
|
|
95
|
+
name: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
interface SkillMetaResult {
|
|
99
|
+
meta: SkillMeta | null;
|
|
100
|
+
warning?: string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* What js-yaml can put in a SKILL.md frontmatter field. Rich scalars (Dates)
|
|
105
|
+
* ride along as the object arm; only strings are accepted below anyway.
|
|
106
|
+
*/
|
|
107
|
+
type FrontmatterField =
|
|
108
|
+
| string
|
|
109
|
+
| number
|
|
110
|
+
| boolean
|
|
111
|
+
| null
|
|
112
|
+
| undefined
|
|
113
|
+
| FrontmatterField[]
|
|
114
|
+
| { [key: string]: FrontmatterField };
|
|
115
|
+
|
|
116
|
+
const isString = (value: FrontmatterField): value is string =>
|
|
117
|
+
typeof value === "string";
|
|
118
|
+
|
|
92
119
|
/** Frontmatter of a SKILL.md, or null with a warning when unusable. */
|
|
93
|
-
const skillMeta = (
|
|
94
|
-
|
|
95
|
-
dirName: string
|
|
96
|
-
): { meta: { description: string; name: string } | null; warning?: string } => {
|
|
97
|
-
let data: Record<string, unknown>;
|
|
120
|
+
const skillMeta = (raw: string, dirName: string): SkillMetaResult => {
|
|
121
|
+
let data: { description?: FrontmatterField; name?: FrontmatterField };
|
|
98
122
|
try {
|
|
99
|
-
({ data } = matter(raw)
|
|
123
|
+
({ data } = matter(raw));
|
|
100
124
|
} catch {
|
|
101
125
|
return {
|
|
102
126
|
meta: null,
|
|
103
127
|
warning: `Skill "${dirName}" has unparsable SKILL.md frontmatter; skipped.`,
|
|
104
128
|
};
|
|
105
129
|
}
|
|
106
|
-
const name =
|
|
107
|
-
const description =
|
|
108
|
-
typeof data.description === "string" ? data.description : "";
|
|
130
|
+
const name = isString(data.name) ? data.name : "";
|
|
131
|
+
const description = isString(data.description) ? data.description : "";
|
|
109
132
|
if (!(name && description)) {
|
|
110
133
|
return {
|
|
111
134
|
meta: null,
|
package/src/ai/visibility.ts
CHANGED
|
@@ -19,10 +19,10 @@ const visibilityBlock = (audience: VisibilityAudience): RegExp =>
|
|
|
19
19
|
"gu"
|
|
20
20
|
);
|
|
21
21
|
|
|
22
|
-
const BLOCKS
|
|
22
|
+
const BLOCKS = {
|
|
23
23
|
agents: visibilityBlock("agents"),
|
|
24
24
|
web: visibilityBlock("web"),
|
|
25
|
-
}
|
|
25
|
+
} satisfies Record<VisibilityAudience, RegExp>;
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* Resolve `<Visibility>` blocks for one audience: blocks addressed to the
|
|
@@ -77,6 +77,8 @@ const importClause = (variable: string, name: string, path: string): string =>
|
|
|
77
77
|
|
|
78
78
|
/** A wrapper `.astro` that statically imports a component and hydrates it. */
|
|
79
79
|
const wrapperContent = (override: NormalizedOverride): string => {
|
|
80
|
+
// SAFETY: the only caller guards `if (!source)` and bails before invoking
|
|
81
|
+
// this, so the override always carries a resolved source here.
|
|
80
82
|
const { name, path } = override.source as NonNullable<
|
|
81
83
|
NormalizedOverride["source"]
|
|
82
84
|
>;
|
package/src/astro/examples.ts
CHANGED
|
@@ -33,7 +33,11 @@ export interface ExampleDiscovery {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/** Example extensions mapped to the framework that renders them. */
|
|
36
|
-
|
|
36
|
+
interface FrameworkByExtension {
|
|
37
|
+
[extension: string]: ExampleFramework;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const FRAMEWORK_BY_EXT: FrameworkByExtension = {
|
|
37
41
|
astro: "astro",
|
|
38
42
|
jsx: "react",
|
|
39
43
|
svelte: "svelte",
|
|
@@ -62,7 +66,7 @@ const GLOB_MAGIC = /[!*?[\]{}]/u;
|
|
|
62
66
|
* discovered files can be keyed relative to that prefix (e.g.
|
|
63
67
|
* `registry/x/**\/examples/*` → `{ base: "registry/x", rest: "**\/examples/*" }`).
|
|
64
68
|
*/
|
|
65
|
-
const splitGlobBase = (pattern: string)
|
|
69
|
+
const splitGlobBase = (pattern: string) => {
|
|
66
70
|
const segments = pattern.split("/");
|
|
67
71
|
// Only called when the pattern contains glob magic (see the caller), and `/`
|
|
68
72
|
// is never magic, so the magic char always lands in a segment — `findIndex`
|
package/src/astro/generate.ts
CHANGED
|
@@ -489,12 +489,14 @@ export const ensureDepsLink = async (
|
|
|
489
489
|
* (`<build.server>/.prerender/`) output — so it fires for exactly that build.
|
|
490
490
|
* Inert in dev, where there is no build and `writeBundle` never runs.
|
|
491
491
|
*/
|
|
492
|
-
export
|
|
493
|
-
pkgDir: string = packageRoot()
|
|
494
|
-
): {
|
|
492
|
+
export interface PrerenderDepsPlugin {
|
|
495
493
|
name: string;
|
|
496
494
|
writeBundle: (options: { dir?: string }) => Promise<void>;
|
|
497
|
-
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
export const prerenderDepsPlugin = (
|
|
498
|
+
pkgDir: string = packageRoot()
|
|
499
|
+
): PrerenderDepsPlugin => ({
|
|
498
500
|
name: "blume:prerender-deps",
|
|
499
501
|
writeBundle: async (options) => {
|
|
500
502
|
if (!options.dir || basename(options.dir) !== ".prerender") {
|
|
@@ -528,14 +530,16 @@ interface ServerAppResolveContext {
|
|
|
528
530
|
* Stripping the spurious `.js` and delegating back to Astro's resolver lets the
|
|
529
531
|
* reload complete cleanly, so the renamed route resolves without a restart.
|
|
530
532
|
*/
|
|
531
|
-
export
|
|
533
|
+
export interface ServerAppResolvePlugin {
|
|
532
534
|
enforce: "pre";
|
|
533
535
|
name: string;
|
|
534
536
|
resolveId: (
|
|
535
537
|
this: ServerAppResolveContext,
|
|
536
538
|
id: string
|
|
537
539
|
) => Promise<string | null>;
|
|
538
|
-
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
export const serverAppResolvePlugin = (): ServerAppResolvePlugin => ({
|
|
539
543
|
enforce: "pre",
|
|
540
544
|
name: "blume:server-app-resolve",
|
|
541
545
|
async resolveId(id) {
|
|
@@ -548,20 +552,20 @@ export const serverAppResolvePlugin = (): {
|
|
|
548
552
|
});
|
|
549
553
|
|
|
550
554
|
/** Astro integration package each non-React island framework needs installed. */
|
|
551
|
-
const ISLAND_FRAMEWORK_DEPS
|
|
552
|
-
svelte
|
|
553
|
-
vue
|
|
554
|
-
|
|
555
|
+
const ISLAND_FRAMEWORK_DEPS = new Map([
|
|
556
|
+
["svelte", "@astrojs/svelte"],
|
|
557
|
+
["vue", "@astrojs/vue"],
|
|
558
|
+
]);
|
|
555
559
|
|
|
556
560
|
/**
|
|
557
561
|
* Adapter package the project must install itself for each deployment
|
|
558
562
|
* platform whose adapter Blume doesn't ship. Node and Vercel ship with Blume,
|
|
559
563
|
* so they never need this.
|
|
560
564
|
*/
|
|
561
|
-
const DEPLOYMENT_ADAPTER_DEPS
|
|
562
|
-
cloudflare
|
|
563
|
-
netlify
|
|
564
|
-
|
|
565
|
+
const DEPLOYMENT_ADAPTER_DEPS = new Map([
|
|
566
|
+
["cloudflare", "@astrojs/cloudflare"],
|
|
567
|
+
["netlify", "@astrojs/netlify"],
|
|
568
|
+
]);
|
|
565
569
|
|
|
566
570
|
/**
|
|
567
571
|
* Warn when a Vue/Svelte island is present but its Astro integration isn't
|
|
@@ -574,7 +578,7 @@ const islandFrameworkWarnings = (
|
|
|
574
578
|
): string[] => {
|
|
575
579
|
const warnings: string[] = [];
|
|
576
580
|
for (const framework of frameworks) {
|
|
577
|
-
const dep = ISLAND_FRAMEWORK_DEPS
|
|
581
|
+
const dep = ISLAND_FRAMEWORK_DEPS.get(framework);
|
|
578
582
|
if (dep && !canResolveFrom(root, dep)) {
|
|
579
583
|
warnings.push(
|
|
580
584
|
`Islands use ${framework}, which needs "${dep}". Install it (e.g. \`npm install ${dep} ${framework}\`).`
|
|
@@ -599,7 +603,7 @@ const deploymentAdapterWarnings = (
|
|
|
599
603
|
): string[] => {
|
|
600
604
|
const dep =
|
|
601
605
|
deployment.output === "server" && deployment.adapter
|
|
602
|
-
? DEPLOYMENT_ADAPTER_DEPS
|
|
606
|
+
? DEPLOYMENT_ADAPTER_DEPS.get(deployment.adapter)
|
|
603
607
|
: undefined;
|
|
604
608
|
if (
|
|
605
609
|
dep &&
|
|
@@ -906,6 +910,10 @@ const readLogoSvg = (
|
|
|
906
910
|
return file ? readFileSync(file, "utf-8") : undefined;
|
|
907
911
|
};
|
|
908
912
|
|
|
913
|
+
/** Narrows a config union's string shorthand from its object form. */
|
|
914
|
+
const isStringShorthand = <T>(value: T | string): value is string =>
|
|
915
|
+
typeof value === "string";
|
|
916
|
+
|
|
909
917
|
/**
|
|
910
918
|
* Resolve the configured logo. A single SVG is read and inlined so a
|
|
911
919
|
* `currentColor` logo follows the theme; other images keep their URL for an
|
|
@@ -916,12 +924,12 @@ const resolveLogo = (project: BlumeProject): BlumeLogo | null => {
|
|
|
916
924
|
if (!logo) {
|
|
917
925
|
return null;
|
|
918
926
|
}
|
|
919
|
-
const config =
|
|
927
|
+
const config = isStringShorthand(logo) ? { image: logo } : logo;
|
|
920
928
|
// `text` is passed through verbatim: `undefined` lets the brand fall back to
|
|
921
929
|
// the site title, `""` renders the mark alone (a logo with the wordmark baked
|
|
922
930
|
// in).
|
|
923
931
|
const { href, image: source, text } = config;
|
|
924
|
-
const image =
|
|
932
|
+
const image = isStringShorthand(source) ? { light: source } : source;
|
|
925
933
|
const light = image?.light ?? image?.dark;
|
|
926
934
|
const dark = image?.dark ?? image?.light;
|
|
927
935
|
const alt = image?.alt ?? "";
|
|
@@ -957,18 +965,18 @@ const FAVICON_CANDIDATES = [
|
|
|
957
965
|
];
|
|
958
966
|
|
|
959
967
|
/** `<link type>` MIME for the favicon extensions we recognize. */
|
|
960
|
-
const FAVICON_TYPES
|
|
961
|
-
ico
|
|
962
|
-
jpeg
|
|
963
|
-
jpg
|
|
964
|
-
png
|
|
965
|
-
svg
|
|
966
|
-
|
|
968
|
+
const FAVICON_TYPES = new Map([
|
|
969
|
+
["ico", "image/x-icon"],
|
|
970
|
+
["jpeg", "image/jpeg"],
|
|
971
|
+
["jpg", "image/jpeg"],
|
|
972
|
+
["png", "image/png"],
|
|
973
|
+
["svg", "image/svg+xml"],
|
|
974
|
+
]);
|
|
967
975
|
|
|
968
976
|
/** Infer the `<link type>` MIME from a filename, when we recognize the extension. */
|
|
969
977
|
const faviconType = (name: string): string | undefined => {
|
|
970
978
|
const ext = name.split(".").pop()?.toLowerCase();
|
|
971
|
-
return ext ? FAVICON_TYPES
|
|
979
|
+
return ext ? FAVICON_TYPES.get(ext) : undefined;
|
|
972
980
|
};
|
|
973
981
|
|
|
974
982
|
/** Read a file and encode it as a `data:` URI of the given MIME type. */
|
|
@@ -1041,7 +1049,7 @@ const resolveBanner = (config: ResolvedConfig): BlumeBanner | null => {
|
|
|
1041
1049
|
if (!banner) {
|
|
1042
1050
|
return null;
|
|
1043
1051
|
}
|
|
1044
|
-
if (
|
|
1052
|
+
if (isStringShorthand(banner)) {
|
|
1045
1053
|
return { content: banner, dismissible: false, key: banner };
|
|
1046
1054
|
}
|
|
1047
1055
|
return {
|
|
@@ -1234,6 +1242,7 @@ export const buildRuntimeData = (project: BlumeProject): string => {
|
|
|
1234
1242
|
theme: config.theme,
|
|
1235
1243
|
title: config.title,
|
|
1236
1244
|
toc: config.toc,
|
|
1245
|
+
versions: config.versions ?? null,
|
|
1237
1246
|
webmcp: {
|
|
1238
1247
|
enabled: config.ai.webmcp,
|
|
1239
1248
|
llms: config.ai.llmsTxt.enabled,
|
|
@@ -1250,6 +1259,19 @@ export const buildRuntimeData = (project: BlumeProject): string => {
|
|
|
1250
1259
|
navigation: withRepoUrl(graph.navigation),
|
|
1251
1260
|
// Per-locale navigation; the catch-all selects the active locale's tree.
|
|
1252
1261
|
navigationByLocale,
|
|
1262
|
+
// Per-archived-version navigation; the catch-all selects by the route's
|
|
1263
|
+
// version, then locale.
|
|
1264
|
+
navigationByVersion: Object.fromEntries(
|
|
1265
|
+
Object.entries(graph.navigationByVersion).map(([id, byLocale]) => [
|
|
1266
|
+
id,
|
|
1267
|
+
Object.fromEntries(
|
|
1268
|
+
Object.entries(byLocale).map(([code, nav]) => [
|
|
1269
|
+
code,
|
|
1270
|
+
withRepoUrl(nav),
|
|
1271
|
+
])
|
|
1272
|
+
),
|
|
1273
|
+
])
|
|
1274
|
+
),
|
|
1253
1275
|
routes: manifest.routes.map((route) => ({
|
|
1254
1276
|
alternates: route.alternates,
|
|
1255
1277
|
collection: route.collection,
|
|
@@ -1264,6 +1286,8 @@ export const buildRuntimeData = (project: BlumeProject): string => {
|
|
|
1264
1286
|
locale: route.locale,
|
|
1265
1287
|
path: route.path,
|
|
1266
1288
|
title: route.title,
|
|
1289
|
+
version: route.version,
|
|
1290
|
+
versionAlternates: route.versionAlternates,
|
|
1267
1291
|
})),
|
|
1268
1292
|
// Default-locale chrome strings (English baseline when not under i18n).
|
|
1269
1293
|
ui: defaultUi,
|
|
@@ -1861,8 +1885,9 @@ export const generateRuntime = async (
|
|
|
1861
1885
|
]);
|
|
1862
1886
|
}
|
|
1863
1887
|
|
|
1864
|
-
// API/AsyncAPI reference pages (
|
|
1865
|
-
// mounted on its configured route and
|
|
1888
|
+
// Scalar-rendered API/AsyncAPI reference pages (`renderer: "scalar"`). One
|
|
1889
|
+
// self-contained page per source, mounted on its configured route and
|
|
1890
|
+
// regenerated each run.
|
|
1866
1891
|
const warnings: string[] = [
|
|
1867
1892
|
...(depsLinkWarning ? [depsLinkWarning] : []),
|
|
1868
1893
|
...reactCompilerWarnings(config, needsReact, reactCompilerPath),
|