blume 1.6.1 → 1.6.2

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/dist/cli/index.js CHANGED
@@ -3267,6 +3267,14 @@ var builtinProxyKinds = (config) => {
3267
3267
  };
3268
3268
  var needsPlaygroundProxy = (config) => builtinProxyKinds(config).length > 0;
3269
3269
 
3270
+ // src/ai/api/paths.ts
3271
+ var OPENAPI_PATH = "/openapi.json";
3272
+ var API_BASE = "/api/docs";
3273
+ var API_PAGES_PATH = `${API_BASE}/pages.json`;
3274
+ var API_PAGE_PATH = `${API_BASE}/pages/{route}.json`;
3275
+ var API_NAVIGATION_PATH = `${API_BASE}/navigation.json`;
3276
+ var API_SEARCH_PATH = `${API_BASE}/search`;
3277
+
3270
3278
  // src/ai/api-catalog.ts
3271
3279
  var API_CATALOG_PATH = "/.well-known/api-catalog";
3272
3280
  var API_CATALOG_TYPE = "application/linkset+json";
@@ -3290,6 +3298,13 @@ var linksetEntries = (config) => {
3290
3298
  }
3291
3299
  entries.push(entry);
3292
3300
  }
3301
+ if (config.ai.api) {
3302
+ entries.push({
3303
+ anchor: abs(API_BASE),
3304
+ "service-desc": [{ href: abs(OPENAPI_PATH), type: "application/json" }],
3305
+ "service-doc": [{ href: abs("/"), type: "text/html" }]
3306
+ });
3307
+ }
3293
3308
  if (config.ai.mcp.enabled) {
3294
3309
  entries.push({
3295
3310
  anchor: abs(config.ai.mcp.route),
@@ -3318,6 +3333,9 @@ var buildHomeLinkHeader = (config, routePaths) => {
3318
3333
  if (hasApiCatalog(config)) {
3319
3334
  links.push(`<${deployBase}${API_CATALOG_PATH}>; rel="api-catalog"; type="application/linkset+json"`);
3320
3335
  }
3336
+ if (config.ai.api) {
3337
+ links.push(`<${deployBase}${OPENAPI_PATH}>; rel="service-desc"; type="application/json"`);
3338
+ }
3321
3339
  if (config.seo.agentReadability) {
3322
3340
  links.push(`<${deployBase}/agent-readability.json>; rel="describedby"; type="application/json"`);
3323
3341
  }
@@ -5662,6 +5680,9 @@ const links = [
5662
5680
  ...(data.config.discovery.llmsTxt
5663
5681
  ? [{ href: href("/llms.txt"), label: nf.llms }]
5664
5682
  : []),
5683
+ ...(data.config.discovery.api
5684
+ ? [{ href: href("/openapi.json"), label: nf.api }]
5685
+ : []),
5665
5686
  ];
5666
5687
 
5667
5688
  const body = [
@@ -5685,6 +5706,114 @@ export function GET() {
5685
5706
  });
5686
5707
  }
5687
5708
  `;
5709
+ var notFoundJsonTemplate = () => `// Generated by Blume. Do not edit. Override by adding \`pages/404.astro\`.
5710
+ import { problem } from "blume/ai/api/problem.ts";
5711
+ import { withBase } from "blume/components/islands/base-path.ts";
5712
+ import { absoluteUrl } from "blume/core/site-url.ts";
5713
+ import data from "blume:data";
5714
+
5715
+ export const prerender = true;
5716
+
5717
+ const nf = data.ui.notFound;
5718
+
5719
+ // Absolute for internal routes when the site is known; an external tab href
5720
+ // passes through untouched.
5721
+ const href = (path: string): string => {
5722
+ const based = withBase(path);
5723
+ return data.config.site && based.startsWith("/") && !based.startsWith("//")
5724
+ ? absoluteUrl(data.config.site, based)
5725
+ : based;
5726
+ };
5727
+
5728
+ // The recovery set of 404.astro: home, every top-level section (a tab links to
5729
+ // its resolved target), then the machine-readable indexes that exist.
5730
+ const links = [
5731
+ { href: href("/"), label: nf.home },
5732
+ ...data.navigation.tabs.map((tab) => ({
5733
+ href: href(tab.href ?? tab.path),
5734
+ label: tab.label,
5735
+ })),
5736
+ ...(data.config.discovery.sitemap
5737
+ ? [{ href: href("/sitemap.xml"), label: nf.sitemap }]
5738
+ : []),
5739
+ ...(data.config.discovery.llmsTxt
5740
+ ? [{ href: href("/llms.txt"), label: nf.llms }]
5741
+ : []),
5742
+ ...(data.config.discovery.api
5743
+ ? [{ href: href("/openapi.json"), label: nf.api }]
5744
+ : []),
5745
+ ];
5746
+
5747
+ const body = problem({
5748
+ code: "PAGE_NOT_FOUND",
5749
+ detail: nf.description,
5750
+ links,
5751
+ resolution: nf.suggestions + ": " + links.map((link) => link.href).join(", "),
5752
+ status: 404,
5753
+ title: nf.title,
5754
+ });
5755
+
5756
+ export function GET() {
5757
+ return new Response(JSON.stringify(body, null, 2) + "\\n", {
5758
+ headers: { "Content-Type": "application/problem+json; charset=utf-8" },
5759
+ });
5760
+ }
5761
+ `;
5762
+ var apiPagesIndexTemplate = () => `// Generated by Blume. Do not edit.
5763
+ import { pagesIndexResponse } from "blume/ai/api/handlers.ts";
5764
+ import data from "blume:mcp-data";
5765
+
5766
+ export const prerender = true;
5767
+
5768
+ export function GET() {
5769
+ return pagesIndexResponse(data);
5770
+ }
5771
+ `;
5772
+ var apiPageTemplate = () => `// Generated by Blume. Do not edit.
5773
+ import { pageParams, pageResponse } from "blume/ai/api/handlers.ts";
5774
+ import data from "blume:mcp-data";
5775
+
5776
+ export const prerender = true;
5777
+
5778
+ export function getStaticPaths() {
5779
+ return pageParams(data);
5780
+ }
5781
+
5782
+ export function GET({ props }: { props: { route: string } }) {
5783
+ return pageResponse(data, props.route);
5784
+ }
5785
+ `;
5786
+ var apiNavigationTemplate = () => `// Generated by Blume. Do not edit.
5787
+ import { navigationResponse } from "blume/ai/api/handlers.ts";
5788
+ import data from "blume:mcp-data";
5789
+
5790
+ export const prerender = true;
5791
+
5792
+ export function GET() {
5793
+ return navigationResponse(data);
5794
+ }
5795
+ `;
5796
+ var apiSearchTemplate = () => `// Generated by Blume. Do not edit.
5797
+ import type { APIRoute } from "astro";
5798
+ import { createSearchHandler } from "blume/ai/api/handlers.ts";
5799
+ import data from "blume:mcp-data";
5800
+
5801
+ export const prerender = false;
5802
+
5803
+ const handler = createSearchHandler(data);
5804
+
5805
+ export const GET: APIRoute = ({ request }) => handler(request);
5806
+ `;
5807
+ var apiNotFoundTemplate = (context) => `// Generated by Blume. Do not edit.
5808
+ import type { APIRoute } from "astro";
5809
+ import { apiNotFoundResponse } from "blume/ai/api/handlers.ts";
5810
+
5811
+ export const prerender = false;
5812
+
5813
+ const context = ${JSON.stringify(context)};
5814
+
5815
+ export const ALL: APIRoute = ({ request }) => apiNotFoundResponse(request, context);
5816
+ `;
5688
5817
  var islandDirective = (spec) => spec.client === "only" ? `client:only="${spec.framework}"` : `client:${spec.client}`;
5689
5818
  var wrapperPropsType = (name) => `type Props = typeof ${name} extends (
5690
5819
  props: infer P extends object,
@@ -10091,6 +10220,7 @@ var uiStringsObject = z.object({
10091
10220
  toggleTheme: z.string().default("Toggle color theme")
10092
10221
  }).prefault({}),
10093
10222
  notFound: z.object({
10223
+ api: z.string().default("JSON API description (openapi.json)"),
10094
10224
  description: z.string().default("We couldn't find the page you're looking for."),
10095
10225
  home: z.string().default("Back to home"),
10096
10226
  llms: z.string().default("Docs index for AI agents (llms.txt)"),
@@ -10597,6 +10727,7 @@ var llmsTxtObjectSchema = z2.strictObject({
10597
10727
  openapi: z2.boolean().default(true)
10598
10728
  });
10599
10729
  var aiConfigSchema = z2.strictObject({
10730
+ api: z2.boolean().default(true),
10600
10731
  ask: z2.strictObject({
10601
10732
  apiKeyEnv: z2.string().optional(),
10602
10733
  baseUrl: z2.url().optional(),
@@ -16381,6 +16512,23 @@ var askApiUrl = (endpoint, site, abs) => {
16381
16512
  }
16382
16513
  return site && endpoint.startsWith("/") ? absoluteUrl(site, endpoint) : endpoint;
16383
16514
  };
16515
+ var markdownArtifact = (config, abs) => {
16516
+ const negotiates = config.deployment.output === "server" && (config.deployment.adapter === "vercel" || config.deployment.adapter === "cloudflare");
16517
+ return negotiates ? { contentNegotiation: "text/markdown", pattern: abs("/{route}.md") } : { pattern: abs("/{route}.md") };
16518
+ };
16519
+ var apiArtifact = (config, abs) => {
16520
+ if (!config.ai.api) {
16521
+ return null;
16522
+ }
16523
+ const api = {
16524
+ openapi: abs(OPENAPI_PATH),
16525
+ pages: abs(API_PAGES_PATH)
16526
+ };
16527
+ if (config.deployment.output === "server") {
16528
+ api.search = abs(API_SEARCH_PATH);
16529
+ }
16530
+ return api;
16531
+ };
16384
16532
  var wellKnownArtifacts = (config, abs) => {
16385
16533
  const artifacts = {};
16386
16534
  if (config.ai.webBotAuth.keys.length > 0) {
@@ -16405,9 +16553,13 @@ var buildAgentReadability = (project) => {
16405
16553
  const based = withBasePath(deployBase, path);
16406
16554
  return site ? absoluteUrl(site, based) : based;
16407
16555
  };
16408
- const negotiates = config.deployment.output === "server" && (config.deployment.adapter === "vercel" || config.deployment.adapter === "cloudflare");
16409
- const markdown = negotiates ? { contentNegotiation: "text/markdown", pattern: abs("/{route}.md") } : { pattern: abs("/{route}.md") };
16410
- const artifacts = { markdown };
16556
+ const artifacts = {
16557
+ markdown: markdownArtifact(config, abs)
16558
+ };
16559
+ const api = apiArtifact(config, abs);
16560
+ if (api) {
16561
+ artifacts.api = api;
16562
+ }
16411
16563
  if (config.ai.llmsTxt.enabled) {
16412
16564
  artifacts.llmsFullTxt = abs("/llms-full.txt");
16413
16565
  artifacts.llmsTxt = abs("/llms.txt");
@@ -17123,6 +17275,9 @@ var agentResourceLines = (project) => {
17123
17275
  `- [llms-full.txt](${url("/llms-full.txt")}): The full Markdown of every page in one file.`,
17124
17276
  `- [Page Markdown](${url("/index.md")}): Append \`.md\` to any page URL to fetch that page as raw Markdown.`
17125
17277
  ];
17278
+ if (config.ai.api) {
17279
+ lines.push(`- [JSON API](${url(API_PAGES_PATH)}): Page index of the JSON docs API; each entry links the page's JSON and Markdown forms. Described by the OpenAPI document at ${url(OPENAPI_PATH)}.`);
17280
+ }
17126
17281
  if (config.ai.mcp.enabled) {
17127
17282
  lines.push(`- [MCP server](${url(config.ai.mcp.route)}): Streamable HTTP Model Context Protocol server with search_docs, get_page, list_pages, and get_navigation tools, plus every page as a resource. Discovery document: ${url("/.well-known/mcp.json")}`);
17128
17283
  }
@@ -18062,12 +18217,17 @@ ${references.join(`
18062
18217
 
18063
18218
  // src/deploy/vercel-negotiation.ts
18064
18219
  var ACCEPT_MARKDOWN_HEADER_VALUE = "(.*,)?\\s*text/(x-)?markdown(\\s*[;,].*)?$";
18220
+ var ACCEPT_JSON_HEADER_VALUE = "(.*,)?\\s*application/(problem\\+)?json(\\s*[;,].*)?$";
18065
18221
  var isString6 = (value) => typeof value === "string";
18066
18222
  var ACCEPT_MARKDOWN_CONDITION = [
18067
18223
  { key: "accept", type: "header", value: ACCEPT_MARKDOWN_HEADER_VALUE }
18068
18224
  ];
18225
+ var ACCEPT_JSON_CONDITION = [
18226
+ { key: "accept", type: "header", value: ACCEPT_JSON_HEADER_VALUE }
18227
+ ];
18069
18228
  var VARY_ACCEPT = { vary: "Accept" };
18070
18229
  var NOT_FOUND_MARKDOWN_DEST = "/404.md";
18230
+ var NOT_FOUND_JSON_DEST = "/404.json";
18071
18231
  var NOT_FOUND_HTML_DEST = "/404.html";
18072
18232
  var NOT_FOUND_MARKDOWN_ROUTES = [
18073
18233
  {
@@ -18079,6 +18239,16 @@ var NOT_FOUND_MARKDOWN_ROUTES = [
18079
18239
  },
18080
18240
  { dest: NOT_FOUND_MARKDOWN_DEST, src: "^/.*\\.mdx?$", status: 404 }
18081
18241
  ];
18242
+ var NOT_FOUND_JSON_ROUTES = [
18243
+ {
18244
+ dest: NOT_FOUND_JSON_DEST,
18245
+ has: ACCEPT_JSON_CONDITION,
18246
+ headers: VARY_ACCEPT,
18247
+ src: "^/.*$",
18248
+ status: 404
18249
+ },
18250
+ { dest: NOT_FOUND_JSON_DEST, src: "^/.*\\.json$", status: 404 }
18251
+ ];
18082
18252
  var MAX_ALTERNATION_LENGTH = 3900;
18083
18253
  var REGEX_SPECIALS = /[$()*+.?[\]^{|}\\]/gu;
18084
18254
  var routePattern = (route) => encodeURI(route).replace(REGEX_SPECIALS, "\\$&");
@@ -18134,8 +18304,8 @@ var TRAILING_SLASH_REDIRECT = {
18134
18304
  src: "^/(.+)/$",
18135
18305
  status: 308
18136
18306
  };
18137
- var isNegotiationRoute = (route) => route.has?.some((condition) => condition.value === ACCEPT_MARKDOWN_HEADER_VALUE) === true || route.dest === NOT_FOUND_MARKDOWN_DEST && route.status === 404 || route.continue === true && route.headers?.vary === "Accept" && isString6(route.src) && Object.keys(route).length === 3 || route.continue === true && isString6(route.headers?.link) && route.src === HOME_SRC && Object.keys(route).length === 3 || route.status === TRAILING_SLASH_REDIRECT.status && route.src === TRAILING_SLASH_REDIRECT.src;
18138
- var injectNegotiationRoutes = (configText, routePaths, homeLinkHeader, contentTypeOverrides, homeTokens, notFoundMarkdown = false) => {
18307
+ var isNegotiationRoute = (route) => route.has?.some((condition) => condition.value === ACCEPT_MARKDOWN_HEADER_VALUE) === true || route.dest === NOT_FOUND_MARKDOWN_DEST && route.status === 404 || route.dest === NOT_FOUND_JSON_DEST && route.status === 404 || route.continue === true && route.headers?.vary === "Accept" && isString6(route.src) && Object.keys(route).length === 3 || route.continue === true && isString6(route.headers?.link) && route.src === HOME_SRC && Object.keys(route).length === 3 || route.status === TRAILING_SLASH_REDIRECT.status && route.src === TRAILING_SLASH_REDIRECT.src;
18308
+ var injectNegotiationRoutes = (configText, routePaths, homeLinkHeader, contentTypeOverrides, homeTokens, notFound = {}) => {
18139
18309
  const overrideEntries = Object.entries(contentTypeOverrides ?? {});
18140
18310
  let config;
18141
18311
  try {
@@ -18163,10 +18333,14 @@ var injectNegotiationRoutes = (configText, routePaths, homeLinkHeader, contentTy
18163
18333
  });
18164
18334
  }
18165
18335
  routes.splice(filesystemIndex, 0, ...headerRoutes, ...rewriteRoutes, TRAILING_SLASH_REDIRECT);
18166
- if (notFoundMarkdown) {
18336
+ const notFoundRoutes = [
18337
+ ...notFound.markdown ? NOT_FOUND_MARKDOWN_ROUTES : [],
18338
+ ...notFound.json ? NOT_FOUND_JSON_ROUTES : []
18339
+ ];
18340
+ if (notFoundRoutes.length > 0) {
18167
18341
  const fallbackIndex = routes.findIndex((route) => route.status === 404 && route.dest === NOT_FOUND_HTML_DEST);
18168
18342
  if (fallbackIndex !== -1) {
18169
- routes.splice(fallbackIndex, 0, ...NOT_FOUND_MARKDOWN_ROUTES);
18343
+ routes.splice(fallbackIndex, 0, ...notFoundRoutes);
18170
18344
  }
18171
18345
  }
18172
18346
  config.routes = routes;
@@ -18731,6 +18905,519 @@ import {
18731
18905
  } from "pathe";
18732
18906
  import { glob as glob7 } from "tinyglobby";
18733
18907
 
18908
+ // src/ai/api/problem.ts
18909
+ var PROBLEM_TYPE = "application/problem+json";
18910
+
18911
+ // src/ai/api/spec.ts
18912
+ var JSON_TYPE = "application/json";
18913
+ var MARKDOWN_TYPE = "text/markdown";
18914
+ var TEXT_TYPE = "text/plain";
18915
+ var ref = (name) => ({
18916
+ $ref: `#/components/schemas/${name}`
18917
+ });
18918
+ var jsonResponse = (description, schema) => ({
18919
+ content: { [JSON_TYPE]: { schema: ref(schema) } },
18920
+ description
18921
+ });
18922
+ var problemResponse = (description) => ({
18923
+ content: { [PROBLEM_TYPE]: { schema: ref("Problem") } },
18924
+ description
18925
+ });
18926
+ var textResponse = (description, type) => ({
18927
+ content: { [type]: { schema: { type: "string" } } },
18928
+ description
18929
+ });
18930
+ var string = (description) => ({ description, type: "string" });
18931
+ var facetsSchema = {
18932
+ additionalProperties: { type: "string" },
18933
+ description: "Facet values the site declares for the page's content type (`content.types.<type>.facets`), key → value.",
18934
+ type: "object"
18935
+ };
18936
+ var versionProperty = {
18937
+ description: 'Docs version the page belongs to on a versioned site: `""` for the current docs, else an archived version id. Absent on unversioned sites.',
18938
+ type: "string"
18939
+ };
18940
+ var searchParameters = [
18941
+ {
18942
+ description: "The search query.",
18943
+ in: "query",
18944
+ name: "q",
18945
+ required: true,
18946
+ schema: { minLength: 1, type: "string" }
18947
+ },
18948
+ {
18949
+ description: "Maximum hits to return (default 8, at most 20).",
18950
+ in: "query",
18951
+ name: "limit",
18952
+ required: false,
18953
+ schema: { default: 8, maximum: 20, minimum: 1, type: "integer" }
18954
+ },
18955
+ {
18956
+ description: "Only include pages of these content types (frontmatter `type`, e.g. `doc`, `rfc`). Comma-separated or repeated. Omit for every type.",
18957
+ explode: false,
18958
+ in: "query",
18959
+ name: "contentTypes",
18960
+ required: false,
18961
+ schema: { items: { type: "string" }, type: "array" },
18962
+ style: "form"
18963
+ },
18964
+ {
18965
+ description: "Only include pages in this locale (e.g. `fr`). Omit for every language.",
18966
+ in: "query",
18967
+ name: "locale",
18968
+ required: false,
18969
+ schema: { type: "string" }
18970
+ },
18971
+ {
18972
+ description: "Docs version to scope to on a versioned site: `latest` (the default — current docs only), `all`, or an archived version id. Ignored when the site is unversioned.",
18973
+ in: "query",
18974
+ name: "version",
18975
+ required: false,
18976
+ schema: { type: "string" }
18977
+ },
18978
+ {
18979
+ description: "Only include pages matching every facet, as `filters[key]=value` pairs (e.g. `filters[status]=enforced`). Facets are metadata the site declares per content type; the page index shows each page's values.",
18980
+ explode: true,
18981
+ in: "query",
18982
+ name: "filters",
18983
+ required: false,
18984
+ schema: { additionalProperties: { type: "string" }, type: "object" },
18985
+ style: "deepObject"
18986
+ }
18987
+ ];
18988
+ var schemas = {
18989
+ JsonRpcRequest: {
18990
+ description: "A JSON-RPC 2.0 request, as the Model Context Protocol sends.",
18991
+ properties: {
18992
+ id: { description: "Request id (absent on notifications)." },
18993
+ jsonrpc: { const: "2.0", type: "string" },
18994
+ method: string("The MCP method, e.g. `initialize` or `tools/call`."),
18995
+ params: { description: "Method parameters.", type: "object" }
18996
+ },
18997
+ required: ["jsonrpc", "method"],
18998
+ type: "object"
18999
+ },
19000
+ JsonRpcResponse: {
19001
+ description: "A JSON-RPC 2.0 response: a `result` or an `error`.",
19002
+ properties: {
19003
+ error: {
19004
+ properties: {
19005
+ code: { type: "integer" },
19006
+ data: {},
19007
+ message: { type: "string" }
19008
+ },
19009
+ required: ["code", "message"],
19010
+ type: "object"
19011
+ },
19012
+ id: {},
19013
+ jsonrpc: { const: "2.0", type: "string" },
19014
+ result: { type: "object" }
19015
+ },
19016
+ required: ["jsonrpc"],
19017
+ type: "object"
19018
+ },
19019
+ NavLink: {
19020
+ properties: {
19021
+ href: string("Link target — an internal route or an external URL."),
19022
+ icon: string("Optional icon name."),
19023
+ label: { type: "string" }
19024
+ },
19025
+ required: ["href", "label"],
19026
+ type: "object"
19027
+ },
19028
+ NavNode: {
19029
+ description: "A sidebar entry: a page (linking to its route) or a group holding further nodes.",
19030
+ oneOf: [
19031
+ {
19032
+ properties: {
19033
+ badge: { type: "string" },
19034
+ deprecated: { type: "boolean" },
19035
+ description: { type: "string" },
19036
+ icon: { type: "string" },
19037
+ kind: { const: "page", type: "string" },
19038
+ label: { type: "string" },
19039
+ pageId: string("The page's stable content id."),
19040
+ route: string("The page's route.")
19041
+ },
19042
+ required: ["kind", "label", "pageId", "route"],
19043
+ type: "object"
19044
+ },
19045
+ {
19046
+ properties: {
19047
+ badge: { type: "string" },
19048
+ children: { items: ref("NavNode"), type: "array" },
19049
+ collapsed: { type: "boolean" },
19050
+ icon: { type: "string" },
19051
+ kind: { const: "group", type: "string" },
19052
+ label: { type: "string" },
19053
+ path: string("The group's route prefix (not necessarily a page)."),
19054
+ route: string("The group's index page route, when it has one.")
19055
+ },
19056
+ required: ["children", "kind", "label"],
19057
+ type: "object"
19058
+ }
19059
+ ]
19060
+ },
19061
+ NavSelector: {
19062
+ description: "A top-level partition selector (products, versions, languages).",
19063
+ properties: {
19064
+ items: {
19065
+ items: {
19066
+ properties: {
19067
+ description: { type: "string" },
19068
+ label: { type: "string" },
19069
+ path: { type: "string" },
19070
+ tag: { type: "string" }
19071
+ },
19072
+ required: ["label", "path"],
19073
+ type: "object"
19074
+ },
19075
+ type: "array"
19076
+ },
19077
+ kind: { type: "string" },
19078
+ label: { type: "string" }
19079
+ },
19080
+ required: ["items", "kind", "label"],
19081
+ type: "object"
19082
+ },
19083
+ NavTab: {
19084
+ properties: {
19085
+ href: string("The clickable target when it differs from `path` (the section's first page)."),
19086
+ icon: { type: "string" },
19087
+ label: { type: "string" },
19088
+ path: string("The tab's section prefix.")
19089
+ },
19090
+ required: ["label", "path"],
19091
+ type: "object"
19092
+ },
19093
+ Navigation: {
19094
+ description: "The docs navigation model: header tabs, the sidebar tree, partition selectors, and pinned links.",
19095
+ properties: {
19096
+ actions: { items: ref("NavLink"), type: "array" },
19097
+ cta: { oneOf: [ref("NavLink"), { type: "null" }] },
19098
+ featured: { items: ref("NavLink"), type: "array" },
19099
+ repoUrl: { type: ["string", "null"] },
19100
+ root: string("The tree root's route (`/`, or a locale/version prefix)."),
19101
+ selectors: { items: ref("NavSelector"), type: "array" },
19102
+ sidebar: { items: ref("NavNode"), type: "array" },
19103
+ tabs: { items: ref("NavTab"), type: "array" }
19104
+ },
19105
+ required: ["featured", "selectors", "sidebar", "tabs"],
19106
+ type: "object"
19107
+ },
19108
+ Page: {
19109
+ allOf: [
19110
+ ref("PageSummary"),
19111
+ {
19112
+ properties: {
19113
+ markdown: string("The page as agent Markdown: frontmatter included, components downleveled to plain Markdown.")
19114
+ },
19115
+ required: ["markdown"],
19116
+ type: "object"
19117
+ }
19118
+ ],
19119
+ description: "A page's index entry plus its full Markdown body."
19120
+ },
19121
+ PageSummary: {
19122
+ properties: {
19123
+ contentType: string("The page's content type (frontmatter `type`)."),
19124
+ description: { type: "string" },
19125
+ facets: facetsSchema,
19126
+ json: {
19127
+ description: "This page's JSON representation (the `getPage` operation).",
19128
+ format: "uri-reference",
19129
+ type: "string"
19130
+ },
19131
+ lastModified: {
19132
+ description: "ISO 8601 last-modified date, when known.",
19133
+ type: ["string", "null"]
19134
+ },
19135
+ locale: string("The page's locale code."),
19136
+ markdownUrl: {
19137
+ description: "The page's raw-Markdown mirror (the `getPageMarkdown` operation).",
19138
+ format: "uri-reference",
19139
+ type: "string"
19140
+ },
19141
+ route: string("The page's route (`/guides/install`); the key every other operation takes."),
19142
+ title: { type: "string" },
19143
+ url: {
19144
+ description: "Where the rendered page is served.",
19145
+ format: "uri-reference",
19146
+ type: "string"
19147
+ },
19148
+ version: versionProperty
19149
+ },
19150
+ required: [
19151
+ "contentType",
19152
+ "json",
19153
+ "lastModified",
19154
+ "locale",
19155
+ "markdownUrl",
19156
+ "route",
19157
+ "title",
19158
+ "url"
19159
+ ],
19160
+ type: "object"
19161
+ },
19162
+ PagesIndex: {
19163
+ properties: {
19164
+ count: { type: "integer" },
19165
+ generator: string("The Blume version that built the site."),
19166
+ pages: { items: ref("PageSummary"), type: "array" },
19167
+ site: { type: ["string", "null"] }
19168
+ },
19169
+ required: ["count", "generator", "pages", "site"],
19170
+ type: "object"
19171
+ },
19172
+ Problem: {
19173
+ description: "RFC 9457 problem details, with a stable code and a resolution hint.",
19174
+ properties: {
19175
+ code: string("Stable error code (e.g. `PAGE_NOT_FOUND`)."),
19176
+ detail: string("Human-readable explanation of this occurrence."),
19177
+ instance: string("The request path the problem occurred on."),
19178
+ links: {
19179
+ description: "Recovery links, when there is somewhere useful to go.",
19180
+ items: {
19181
+ properties: { href: { type: "string" }, label: { type: "string" } },
19182
+ required: ["href", "label"],
19183
+ type: "object"
19184
+ },
19185
+ type: "array"
19186
+ },
19187
+ resolution: string("What to do next."),
19188
+ status: { type: "integer" },
19189
+ title: { type: "string" },
19190
+ type: string("Problem type URI; `about:blank` by default.")
19191
+ },
19192
+ required: ["code", "detail", "resolution", "status", "title", "type"],
19193
+ type: "object"
19194
+ },
19195
+ SearchHit: {
19196
+ properties: {
19197
+ contentType: { type: "string" },
19198
+ excerpt: string("The page description, else the start of its content."),
19199
+ facets: facetsSchema,
19200
+ route: string("The page's route; pass it to `getPage`."),
19201
+ title: { type: "string" },
19202
+ url: { format: "uri-reference", type: "string" },
19203
+ version: versionProperty
19204
+ },
19205
+ required: ["excerpt", "route", "title", "url"],
19206
+ type: "object"
19207
+ },
19208
+ SearchResponse: {
19209
+ properties: {
19210
+ count: { type: "integer" },
19211
+ query: { type: "string" },
19212
+ results: { items: ref("SearchHit"), type: "array" }
19213
+ },
19214
+ required: ["count", "query", "results"],
19215
+ type: "object"
19216
+ }
19217
+ };
19218
+ var ROUTE_PARAM = {
19219
+ description: "The page route without its leading slash (`guides/install`), or `index` for the home page. May contain slashes.",
19220
+ in: "path",
19221
+ name: "route",
19222
+ required: true,
19223
+ schema: { type: "string" }
19224
+ };
19225
+ var serverUrl = (input) => {
19226
+ if (input.site) {
19227
+ return input.base ? absoluteUrl(input.site, input.base) : siteRoot(input.site);
19228
+ }
19229
+ return input.base || "/";
19230
+ };
19231
+ var buildApiSpec = (input) => {
19232
+ const pathEntries = [
19233
+ [
19234
+ API_PAGES_PATH,
19235
+ {
19236
+ get: {
19237
+ description: "Every documentation page with its route, title, description, content type, locale, facets, and the URLs of its rendered, Markdown, and JSON forms. Unfiltered; on a versioned site every version is listed with its `version`. Use it to enumerate the docs or to find a page when search is too narrow.",
19238
+ operationId: "listPages",
19239
+ responses: {
19240
+ "200": jsonResponse("The page index.", "PagesIndex"),
19241
+ default: problemResponse("An error, as problem details.")
19242
+ },
19243
+ summary: "List every page",
19244
+ tags: ["Pages"]
19245
+ }
19246
+ }
19247
+ ],
19248
+ [
19249
+ API_PAGE_PATH,
19250
+ {
19251
+ get: {
19252
+ description: "A single page as JSON: its index entry plus the page's agent Markdown (frontmatter included, components downleveled to plain Markdown). Take `route` from `listPages` or `searchDocs`.",
19253
+ operationId: "getPage",
19254
+ parameters: [ROUTE_PARAM],
19255
+ responses: {
19256
+ "200": jsonResponse("The page.", "Page"),
19257
+ "404": problemResponse("No page has that route."),
19258
+ default: problemResponse("An error, as problem details.")
19259
+ },
19260
+ summary: "Get a page as JSON",
19261
+ tags: ["Pages"]
19262
+ }
19263
+ }
19264
+ ],
19265
+ [
19266
+ API_NAVIGATION_PATH,
19267
+ {
19268
+ get: {
19269
+ description: "The navigation tree (header tabs and the sidebar hierarchy) as readers see it, for the default locale and the current docs.",
19270
+ operationId: "getNavigation",
19271
+ responses: {
19272
+ "200": jsonResponse("The navigation tree.", "Navigation"),
19273
+ default: problemResponse("An error, as problem details.")
19274
+ },
19275
+ summary: "Get the navigation tree",
19276
+ tags: ["Navigation"]
19277
+ }
19278
+ }
19279
+ ]
19280
+ ];
19281
+ if (input.search) {
19282
+ pathEntries.push([
19283
+ API_SEARCH_PATH,
19284
+ {
19285
+ get: {
19286
+ description: "Full-text search across the documentation. Returns matching pages with their title, route, content type, and a short excerpt; narrow by content type, locale, version, or facet. Use it first to discover relevant pages, then `getPage` to read one in full.",
19287
+ operationId: "searchDocs",
19288
+ parameters: searchParameters,
19289
+ responses: {
19290
+ "200": jsonResponse("The matching pages, best first.", "SearchResponse"),
19291
+ "400": problemResponse("The query was missing or blank."),
19292
+ default: problemResponse("An error, as problem details.")
19293
+ },
19294
+ summary: "Search the docs",
19295
+ tags: ["Search"]
19296
+ }
19297
+ }
19298
+ ]);
19299
+ }
19300
+ pathEntries.push([
19301
+ "/{route}.md",
19302
+ {
19303
+ get: {
19304
+ description: "A page's raw-Markdown mirror: append `.md` to any page URL. Components are downleveled to plain Markdown; `.mdx` serves the source as written. The same body the `getPage` operation carries in its `markdown` field.",
19305
+ operationId: "getPageMarkdown",
19306
+ parameters: [ROUTE_PARAM],
19307
+ responses: {
19308
+ "200": textResponse("The page as Markdown.", MARKDOWN_TYPE),
19309
+ "404": textResponse("No page has that route; the body lists where to look next.", MARKDOWN_TYPE)
19310
+ },
19311
+ summary: "Get a page as Markdown",
19312
+ tags: ["Markdown"]
19313
+ }
19314
+ }
19315
+ ]);
19316
+ if (input.llmsTxt) {
19317
+ pathEntries.push([
19318
+ "/llms.txt",
19319
+ {
19320
+ get: {
19321
+ description: "The llms.txt index: the site's summary, when to use it, and every page with a one-line description, grouped by section.",
19322
+ operationId: "getLlmsTxt",
19323
+ responses: {
19324
+ "200": textResponse("The index.", TEXT_TYPE)
19325
+ },
19326
+ summary: "Get llms.txt",
19327
+ tags: ["Markdown"]
19328
+ }
19329
+ }
19330
+ ], [
19331
+ "/llms-full.txt",
19332
+ {
19333
+ get: {
19334
+ description: "The full Markdown of every current-docs page in one file.",
19335
+ operationId: "getLlmsFullTxt",
19336
+ responses: {
19337
+ "200": textResponse("Every page's Markdown.", TEXT_TYPE)
19338
+ },
19339
+ summary: "Get llms-full.txt",
19340
+ tags: ["Markdown"]
19341
+ }
19342
+ }
19343
+ ]);
19344
+ }
19345
+ if (input.agentReadability) {
19346
+ pathEntries.push([
19347
+ "/agent-readability.json",
19348
+ {
19349
+ get: {
19350
+ description: "A manifest indexing every agent-facing artifact the site publishes — this API, the Markdown mirrors, llms.txt, the MCP server, feeds, and the sitemap.",
19351
+ operationId: "getAgentReadability",
19352
+ responses: {
19353
+ "200": {
19354
+ content: { [JSON_TYPE]: { schema: { type: "object" } } },
19355
+ description: "The manifest."
19356
+ }
19357
+ },
19358
+ summary: "Get the agent-readability manifest",
19359
+ tags: ["Discovery"]
19360
+ }
19361
+ }
19362
+ ]);
19363
+ }
19364
+ if (input.mcpRoute) {
19365
+ pathEntries.push([
19366
+ input.mcpRoute,
19367
+ {
19368
+ post: {
19369
+ description: "The Model Context Protocol server (Streamable HTTP, stateless, JSON responses). Tools: `search_docs`, `get_page`, `list_pages`, `get_navigation` — the same operations this API exposes — plus every page as a `text/markdown` resource. Discovery document at `/.well-known/mcp.json`.",
19370
+ operationId: "mcp",
19371
+ requestBody: {
19372
+ content: { [JSON_TYPE]: { schema: ref("JsonRpcRequest") } },
19373
+ required: true
19374
+ },
19375
+ responses: {
19376
+ "200": jsonResponse("The JSON-RPC response.", "JsonRpcResponse")
19377
+ },
19378
+ summary: "Call the MCP server",
19379
+ tags: ["MCP"]
19380
+ }
19381
+ }
19382
+ ]);
19383
+ }
19384
+ const tags = [
19385
+ { description: "Enumerate and read documentation pages.", name: "Pages" },
19386
+ ...input.search ? [{ description: "Full-text search over the docs.", name: "Search" }] : [],
19387
+ { description: "How the docs are organized.", name: "Navigation" },
19388
+ { description: "Plain-text and Markdown surfaces.", name: "Markdown" },
19389
+ ...input.agentReadability ? [{ description: "Agent discovery documents.", name: "Discovery" }] : [],
19390
+ ...input.mcpRoute ? [{ description: "The Model Context Protocol endpoint.", name: "MCP" }] : []
19391
+ ];
19392
+ const info = {
19393
+ description: [
19394
+ `Read-only JSON API over the ${input.name} documentation${input.description ? `: ${input.description}` : "."}`,
19395
+ "Every operation is public and needs no authentication. Errors are RFC 9457 problem details (`application/problem+json`) with a stable `code`, a `detail`, and a `resolution` hint."
19396
+ ].join(`
19397
+
19398
+ `),
19399
+ title: `${input.name} API`,
19400
+ version: input.version,
19401
+ "x-generator": `blume@${input.version}`
19402
+ };
19403
+ const document = {
19404
+ components: { schemas },
19405
+ info,
19406
+ openapi: "3.1.0",
19407
+ paths: Object.fromEntries(pathEntries),
19408
+ security: [],
19409
+ servers: [{ description: input.name, url: serverUrl(input) }],
19410
+ tags
19411
+ };
19412
+ if (input.site) {
19413
+ document.externalDocs = {
19414
+ description: `${input.name} documentation`,
19415
+ url: absoluteUrl(input.site, withBasePath(input.base, "/"))
19416
+ };
19417
+ }
19418
+ return document;
19419
+ };
19420
+
18734
19421
  // src/ai/ask-data.ts
18735
19422
  var buildAskData = async (project) => {
18736
19423
  const documents = await buildSearchDocuments(project, {
@@ -18854,7 +19541,7 @@ var MCP_TOOLS = [
18854
19541
  ];
18855
19542
 
18856
19543
  // src/ai/mcp/discovery.ts
18857
- var serverUrl = (input) => {
19544
+ var serverUrl2 = (input) => {
18858
19545
  const path = withBasePath(input.base, input.route);
18859
19546
  return input.site ? absoluteUrl(input.site, path) : path;
18860
19547
  };
@@ -18863,7 +19550,7 @@ var buildMcpDiscovery = (input) => ({
18863
19550
  {
18864
19551
  name: input.name,
18865
19552
  transport: "streamable-http",
18866
- url: serverUrl(input)
19553
+ url: serverUrl2(input)
18867
19554
  }
18868
19555
  ]
18869
19556
  });
@@ -18884,7 +19571,7 @@ var reverseDnsName = (input) => {
18884
19571
  };
18885
19572
  var HTTP_URL2 = /^https?:\/\//u;
18886
19573
  var buildMcpServerCard = (input) => {
18887
- const url = serverUrl(input);
19574
+ const url = serverUrl2(input);
18888
19575
  const card2 = {
18889
19576
  $schema: SERVER_CARD_SCHEMA,
18890
19577
  capabilities: {
@@ -19534,52 +20221,52 @@ var specConfiguration = async (spec, root) => {
19534
20221
  };
19535
20222
  }
19536
20223
  };
19537
- var acceptScalarReference = (ref, seen, contentRoutes, warnings) => {
19538
- if (ref.renderer !== "scalar") {
20224
+ var acceptScalarReference = (ref2, seen, contentRoutes, warnings) => {
20225
+ if (ref2.renderer !== "scalar") {
19539
20226
  return null;
19540
20227
  }
19541
- if (seen.has(ref.route)) {
19542
- warnings.push(`Two API reference sources resolve to ${ref.route}; keeping the first.`);
20228
+ if (seen.has(ref2.route)) {
20229
+ warnings.push(`Two API reference sources resolve to ${ref2.route}; keeping the first.`);
19543
20230
  return null;
19544
20231
  }
19545
- if (contentRoutes.has(ref.route)) {
19546
- warnings.push(`API reference route ${ref.route} collides with a content page; skipping the reference there.`);
20232
+ if (contentRoutes.has(ref2.route)) {
20233
+ warnings.push(`API reference route ${ref2.route} collides with a content page; skipping the reference there.`);
19547
20234
  return null;
19548
20235
  }
19549
- seen.add(ref.route);
19550
- return ref;
20236
+ seen.add(ref2.route);
20237
+ return ref2;
19551
20238
  };
19552
20239
  var buildReferenceFiles = async (options) => {
19553
20240
  const { config, root, contentRoutes } = options;
19554
20241
  const warnings = [];
19555
20242
  const seen = new Set;
19556
20243
  const accepted = [];
19557
- for (const ref of resolveReferences(config)) {
19558
- const next = acceptScalarReference(ref, seen, contentRoutes, warnings);
20244
+ for (const ref2 of resolveReferences(config)) {
20245
+ const next = acceptScalarReference(ref2, seen, contentRoutes, warnings);
19559
20246
  if (next) {
19560
20247
  accepted.push(next);
19561
20248
  }
19562
20249
  }
19563
- const built = await Promise.all(accepted.map(async (ref) => ({
19564
- ref,
19565
- spec: await specConfiguration(ref.spec, root)
20250
+ const built = await Promise.all(accepted.map(async (ref2) => ({
20251
+ ref: ref2,
20252
+ spec: await specConfiguration(ref2.spec, root)
19566
20253
  })));
19567
20254
  const files = [];
19568
- for (const { ref, spec } of built) {
20255
+ for (const { ref: ref2, spec } of built) {
19569
20256
  if (spec.warning) {
19570
20257
  warnings.push(spec.warning);
19571
20258
  }
19572
- const pagePath = referencePagePath(ref.route);
20259
+ const pagePath = referencePagePath(ref2.route);
19573
20260
  files.push({
19574
20261
  content: scalarReferenceTemplate({
19575
20262
  configuration: {
19576
20263
  ...spec.config,
19577
- ...themeConfiguration(config, ref.theme),
19578
- ...ref.scalar
20264
+ ...themeConfiguration(config, ref2.theme),
20265
+ ...ref2.scalar
19579
20266
  },
19580
- noindex: ref.noindex,
19581
- route: ref.route,
19582
- title: ref.label
20267
+ noindex: ref2.noindex,
20268
+ route: ref2.route,
20269
+ title: ref2.label
19583
20270
  }),
19584
20271
  pagePath
19585
20272
  });
@@ -21311,6 +21998,7 @@ var buildRuntimeData = (project) => {
21311
21998
  description: config.description,
21312
21999
  discovery: {
21313
22000
  agentReadability: config.seo.agentReadability,
22001
+ api: config.ai.api,
21314
22002
  llmsTxt: config.ai.llmsTxt.enabled,
21315
22003
  sitemap: config.seo.sitemap && Boolean(config.deployment.site)
21316
22004
  },
@@ -21439,12 +22127,18 @@ var planMcp = (project, srcDir, userPages) => {
21439
22127
  enabled: true
21440
22128
  };
21441
22129
  };
21442
- var writeMcpFiles = async (project, plan, write, modules) => {
21443
- if (!plan.enabled) {
21444
- return;
22130
+ var publishAgentData = async (project, plans, modules) => {
22131
+ if (!(plans.mcp.enabled || plans.api.enabled)) {
22132
+ return null;
21445
22133
  }
21446
22134
  const data = await buildMcpData(project);
21447
22135
  modules.set("blume:mcp-data", JSON.stringify(data));
22136
+ return data;
22137
+ };
22138
+ var writeMcpFiles = async (plan, write, data) => {
22139
+ if (!(plan.enabled && data)) {
22140
+ return;
22141
+ }
21448
22142
  const discoveryInput = {
21449
22143
  base: data.base,
21450
22144
  name: data.name,
@@ -21458,6 +22152,52 @@ var writeMcpFiles = async (project, plan, write, modules) => {
21458
22152
  write(join31(plan.dir, "server-card.ts"), staticJsonEndpointTemplate(buildMcpServerCard(discoveryInput)))
21459
22153
  ]);
21460
22154
  };
22155
+ var ownsApiRest = (page) => page.pattern.startsWith("/api/[");
22156
+ var contentUnderApi = (page) => page.route === "/api" || page.route.startsWith("/api/");
22157
+ var planApi = (project, srcDir, userPages) => {
22158
+ const { config, context } = project;
22159
+ const server = config.deployment.output === "server";
22160
+ return {
22161
+ catchAll: server && !userPages.some(ownsApiRest) && !project.graph.pages.some(contentUnderApi),
22162
+ enabled: config.ai.api,
22163
+ server,
22164
+ spec: !routeIsTaken(userPages, project.graph.pages, OPENAPI_PATH) && !existsSync17(join31(context.root, "public", "openapi.json")),
22165
+ srcDir
22166
+ };
22167
+ };
22168
+ var writeApiFiles = async (project, plan, write, data, mcp) => {
22169
+ if (!(plan.enabled && data)) {
22170
+ return;
22171
+ }
22172
+ const { config } = project;
22173
+ const mcpRoute = mcp.enabled ? mcp.route : null;
22174
+ const apiDir = join31(plan.srcDir, "pages", "api");
22175
+ const writes = [
22176
+ write(join31(apiDir, "docs", "pages.json.ts"), apiPagesIndexTemplate()),
22177
+ write(join31(apiDir, "docs", "pages", "[...route].json.ts"), apiPageTemplate()),
22178
+ write(join31(apiDir, "docs", "navigation.json.ts"), apiNavigationTemplate())
22179
+ ];
22180
+ if (plan.server) {
22181
+ writes.push(write(join31(apiDir, "docs", "search.ts"), apiSearchTemplate()));
22182
+ }
22183
+ if (plan.catchAll) {
22184
+ writes.push(write(join31(apiDir, "[...path].ts"), apiNotFoundTemplate({ base: data.base, site: data.site })));
22185
+ }
22186
+ if (plan.spec) {
22187
+ writes.push(write(join31(plan.srcDir, "pages", "openapi.json.ts"), staticJsonEndpointTemplate(buildApiSpec({
22188
+ agentReadability: config.seo.agentReadability,
22189
+ base: data.base,
22190
+ description: config.description,
22191
+ llmsTxt: config.ai.llmsTxt.enabled,
22192
+ mcpRoute,
22193
+ name: config.title,
22194
+ search: plan.server,
22195
+ site: data.site,
22196
+ version: data.version
22197
+ }))));
22198
+ }
22199
+ await Promise.all(writes);
22200
+ };
21461
22201
  var planPlaygroundProxy = (config, srcDir) => ({
21462
22202
  enabled: needsPlaygroundProxy(config),
21463
22203
  entrypoint: join31(srcDir, "blume-openapi", "api-proxy.ts"),
@@ -21514,7 +22254,8 @@ var writeNotFoundPage = async (write, srcDir, pages, contentPages) => {
21514
22254
  }
21515
22255
  await Promise.all([
21516
22256
  write(join31(srcDir, "pages", "404.astro"), notFoundPageTemplate()),
21517
- write(join31(srcDir, "pages", "404.md.ts"), notFoundMarkdownTemplate())
22257
+ write(join31(srcDir, "pages", "404.md.ts"), notFoundMarkdownTemplate()),
22258
+ write(join31(srcDir, "pages", "404.json.ts"), notFoundJsonTemplate())
21518
22259
  ]);
21519
22260
  };
21520
22261
  var diagnosticWarning = (diagnostic) => diagnostic.suggestion ? `${diagnostic.message} ${diagnostic.suggestion}` : diagnostic.message;
@@ -21604,6 +22345,8 @@ var generateRuntime = async (project) => {
21604
22345
  const changelogIndex = hasGeneratedChangelog(project, pages);
21605
22346
  const mcp = planMcp(project, srcDir, pages);
21606
22347
  pages.push(...mcp.discoveryPages);
22348
+ const api = planApi(project, srcDir, pages);
22349
+ const agentData = await publishAgentData(project, { api, mcp }, modules);
21607
22350
  const openApiSource2 = project.sources.find(isOpenApiSource);
21608
22351
  const openApiData = openApiSource2 ? openApiSource2.openApiData() : {};
21609
22352
  const playgroundProxy = planPlaygroundProxy(config, srcDir);
@@ -21676,7 +22419,8 @@ var generateRuntime = async (project) => {
21676
22419
  Promise.all(slotPlan.wrappers.map((wrapper) => write(join31(srcDir, "generated", "component-slots", `${wrapper.name}.astro`), wrapper.content))),
21677
22420
  Promise.all(exampleDiscovery.examples.map((example) => write(join31(srcDir, "generated", "examples", `${exampleSlug(example.path)}.astro`), exampleWrapperTemplate(example)))),
21678
22421
  writeAskFiles(project, srcDir, write, modules),
21679
- writeMcpFiles(project, mcp, write, modules),
22422
+ writeMcpFiles(mcp, write, agentData),
22423
+ writeApiFiles(project, api, write, agentData, mcp),
21680
22424
  playgroundProxy.enabled ? write(playgroundProxy.entrypoint, playgroundProxyTemplate(proxyOrigins)) : Promise.resolve(false)
21681
22425
  ]);
21682
22426
  if (config.seo.og.enabled) {
@@ -21988,8 +22732,11 @@ var emitVercelNegotiation = async (project, routePaths, root) => {
21988
22732
  }
21989
22733
  const rawMarkdown = await buildRawMarkdown(project);
21990
22734
  const home = rawMarkdown["/"];
21991
- const notFoundMarkdown = existsSync19(join33(root, ".vercel", "output", "static", "404.md"));
21992
- const injected = injectNegotiationRoutes(await readFile20(configPath, "utf-8"), routePaths, buildHomeLinkHeader(config2, routePaths), overrides, home ? markdownTokenCount(agentMarkdown(home)) : undefined, notFoundMarkdown);
22735
+ const staticDir = join33(root, ".vercel", "output", "static");
22736
+ const injected = injectNegotiationRoutes(await readFile20(configPath, "utf-8"), routePaths, buildHomeLinkHeader(config2, routePaths), overrides, home ? markdownTokenCount(agentMarkdown(home)) : undefined, {
22737
+ json: existsSync19(join33(staticDir, "404.json")),
22738
+ markdown: existsSync19(join33(staticDir, "404.md"))
22739
+ });
21993
22740
  if (injected === null) {
21994
22741
  logger.warn("Could not wire Accept: text/markdown negotiation into .vercel/output/config.json — raw Markdown stays available at the .md URLs.");
21995
22742
  return;
@@ -24489,6 +25236,9 @@ import {
24489
25236
  McpError,
24490
25237
  ReadResourceRequestSchema
24491
25238
  } from "@modelcontextprotocol/sdk/types.js";
25239
+ import { z as z8 } from "zod";
25240
+
25241
+ // src/ai/mcp/query.ts
24492
25242
  import { z as z7 } from "zod";
24493
25243
 
24494
25244
  // src/search/orama-index.ts
@@ -24632,13 +25382,10 @@ var queryOramaIndex = async (db, term, limit, filters) => {
24632
25382
  return found.hits.map((hit) => hit.document);
24633
25383
  };
24634
25384
 
24635
- // src/ai/mcp/server.ts
25385
+ // src/ai/mcp/query.ts
24636
25386
  var DEFAULT_SEARCH_LIMIT = 8;
24637
25387
  var MAX_SEARCH_LIMIT = 20;
24638
25388
  var EXCERPT_LENGTH = 200;
24639
- var RESOURCE_MIME_TYPE = "text/markdown";
24640
- var RESOURCE_NOT_FOUND = -32002;
24641
- var LOCAL_RESOURCE_SCHEME = "blume:";
24642
25389
  var contentTypesField = z7.preprocess((value) => {
24643
25390
  const list2 = (Array.isArray(value) ? value : [value]).filter((entry) => typeof entry === "string");
24644
25391
  return list2.length > 0 ? list2 : undefined;
@@ -24690,21 +25437,6 @@ var TOOL_INPUTS = {
24690
25437
  version: versionField
24691
25438
  })
24692
25439
  };
24693
- var inputSchemaFor = (schema) => {
24694
- const {
24695
- $schema: _dialect,
24696
- additionalProperties: _closed,
24697
- ...rest
24698
- } = z7.toJSONSchema(schema);
24699
- return rest;
24700
- };
24701
- var TOOL_DEFINITIONS = MCP_TOOLS.map((tool) => ({
24702
- annotations: tool.annotations,
24703
- description: tool.description,
24704
- inputSchema: inputSchemaFor(TOOL_INPUTS[tool.name]),
24705
- name: tool.name,
24706
- title: tool.title
24707
- }));
24708
25440
  var matchesFacets = (facets, filters) => Object.entries(filters).every(([key, value]) => facets?.[key] === value);
24709
25441
  var asVersionScope = (value, data) => {
24710
25442
  if (!data.archivedVersions) {
@@ -24736,8 +25468,6 @@ var urlFor = (route, data) => {
24736
25468
  const path = withBasePath(data.base, route);
24737
25469
  return data.site ? absoluteUrl(data.site, path) : path;
24738
25470
  };
24739
- var resourceUri = (route, data) => data.site ? urlFor(route, data) : `${LOCAL_RESOURCE_SCHEME}${withBasePath(data.base, route)}`;
24740
- var resourceRoute = (uri, data) => normalizeRoute2(uri.startsWith(LOCAL_RESOURCE_SCHEME) ? uri.slice(LOCAL_RESOURCE_SCHEME.length) : uri, data);
24741
25471
  var excerptFor = (doc) => {
24742
25472
  if (doc.description) {
24743
25473
  return doc.description;
@@ -24745,10 +25475,6 @@ var excerptFor = (doc) => {
24745
25475
  const head = doc.content.slice(0, EXCERPT_LENGTH).trim();
24746
25476
  return doc.content.length > EXCERPT_LENGTH ? `${head}…` : head;
24747
25477
  };
24748
- var text = (value, isError = false) => {
24749
- const content = [{ text: value, type: "text" }];
24750
- return isError ? { content, isError: true } : { content };
24751
- };
24752
25478
  var createIndexProvider = (documents, locale) => {
24753
25479
  let dbPromise = null;
24754
25480
  return function provideIndex() {
@@ -24756,6 +25482,90 @@ var createIndexProvider = (documents, locale) => {
24756
25482
  return dbPromise;
24757
25483
  };
24758
25484
  };
25485
+ var searchDocs = async (data, index, input) => {
25486
+ const db = await index();
25487
+ const hits = await queryOramaIndex(db, input.query, input.limit ?? DEFAULT_SEARCH_LIMIT, {
25488
+ contentTypes: input.contentTypes,
25489
+ facets: input.filters,
25490
+ locale: input.locale,
25491
+ version: asVersionScope(input.version, data)
25492
+ });
25493
+ return hits.map((doc) => {
25494
+ const hit = {
25495
+ contentType: doc.contentType,
25496
+ excerpt: excerptFor(doc),
25497
+ facets: doc.facets,
25498
+ route: doc.route,
25499
+ title: doc.title,
25500
+ url: urlFor(doc.route, data)
25501
+ };
25502
+ if (data.archivedVersions) {
25503
+ hit.version = doc.version ?? "";
25504
+ }
25505
+ return hit;
25506
+ });
25507
+ };
25508
+ var getPageMarkdown = (data, route) => data.pages[route];
25509
+ var listPages = (data, input) => {
25510
+ const { contentTypes, filters, locale } = input;
25511
+ const versionScope = asVersionScope(input.version, data);
25512
+ return data.routes.filter((route) => (!contentTypes || contentTypes.includes(route.contentType)) && (!filters || matchesFacets(route.facets, filters)) && (!locale || route.locale === locale) && (versionScope === undefined || route.version === versionScope)).map((route) => {
25513
+ const listing = {
25514
+ contentType: route.contentType,
25515
+ description: route.description,
25516
+ facets: route.facets,
25517
+ lastModified: route.lastModified,
25518
+ route: route.route,
25519
+ title: route.title,
25520
+ url: urlFor(route.route, data)
25521
+ };
25522
+ if (data.archivedVersions) {
25523
+ listing.version = route.version;
25524
+ }
25525
+ return listing;
25526
+ });
25527
+ };
25528
+ var getNavigation = (data, input) => {
25529
+ const { locale, version: versionId } = input;
25530
+ const unknownVersion = unknownVersionError(versionId, data);
25531
+ if (unknownVersion) {
25532
+ return { error: unknownVersion };
25533
+ }
25534
+ let { navigation } = data;
25535
+ const byLocale = versionId ? data.navigationByVersion?.[versionId] : undefined;
25536
+ if (byLocale) {
25537
+ navigation = (locale ? byLocale[locale] : undefined) ?? byLocale[data.defaultLocale ?? ""] ?? Object.values(byLocale)[0] ?? navigation;
25538
+ } else if (locale && data.navigationByLocale?.[locale]) {
25539
+ navigation = data.navigationByLocale[locale];
25540
+ }
25541
+ return { navigation };
25542
+ };
25543
+
25544
+ // src/ai/mcp/server.ts
25545
+ var RESOURCE_MIME_TYPE = "text/markdown";
25546
+ var RESOURCE_NOT_FOUND = -32002;
25547
+ var LOCAL_RESOURCE_SCHEME = "blume:";
25548
+ var inputSchemaFor = (schema) => {
25549
+ const {
25550
+ $schema: _dialect,
25551
+ additionalProperties: _closed,
25552
+ ...rest
25553
+ } = z8.toJSONSchema(schema);
25554
+ return rest;
25555
+ };
25556
+ var TOOL_DEFINITIONS = MCP_TOOLS.map((tool) => ({
25557
+ annotations: tool.annotations,
25558
+ description: tool.description,
25559
+ inputSchema: inputSchemaFor(TOOL_INPUTS[tool.name]),
25560
+ name: tool.name,
25561
+ title: tool.title
25562
+ }));
25563
+ var resourceUri = (route, data) => data.site ? urlFor(route, data) : `${LOCAL_RESOURCE_SCHEME}${withBasePath(data.base, route)}`;
25564
+ var resourceRoute = (uri, data) => normalizeRoute2(uri.startsWith(LOCAL_RESOURCE_SCHEME) ? uri.slice(LOCAL_RESOURCE_SCHEME.length) : uri, data);
25565
+ var text = (value, isError = false) => {
25566
+ const content = [{ text: value, type: "text" }];
25567
+ return isError ? { content, isError: true } : { content };
25568
+ };
24759
25569
  var buildServer = (data, index) => {
24760
25570
  const capabilities = { resources: {}, tools: {} };
24761
25571
  const serverOptions = data.instructions ? { capabilities, instructions: data.instructions } : { capabilities };
@@ -24779,7 +25589,7 @@ var buildServer = (data, index) => {
24779
25589
  }));
24780
25590
  server.setRequestHandler(ReadResourceRequestSchema, (request2) => {
24781
25591
  const { uri } = request2.params;
24782
- const markdown = data.pages[resourceRoute(uri, data)];
25592
+ const markdown = getPageMarkdown(data, resourceRoute(uri, data));
24783
25593
  if (markdown === undefined) {
24784
25594
  throw new McpError(RESOURCE_NOT_FOUND, `No page found at "${uri}". Use resources/list or list_pages to find valid URIs.`);
24785
25595
  }
@@ -24790,74 +25600,28 @@ var buildServer = (data, index) => {
24790
25600
  server.setRequestHandler(CallToolRequestSchema, async (request2) => {
24791
25601
  const { arguments: args = {}, name } = request2.params;
24792
25602
  if (name === "search_docs") {
24793
- const input = TOOL_INPUTS.search_docs.parse(args);
24794
- const db = await index();
24795
- const hits = await queryOramaIndex(db, input.query, input.limit ?? DEFAULT_SEARCH_LIMIT, {
24796
- contentTypes: input.contentTypes,
24797
- facets: input.filters,
24798
- locale: input.locale,
24799
- version: asVersionScope(input.version, data)
24800
- });
24801
- const results = hits.map((doc) => {
24802
- const hit = {
24803
- contentType: doc.contentType,
24804
- excerpt: excerptFor(doc),
24805
- facets: doc.facets,
24806
- route: doc.route,
24807
- title: doc.title,
24808
- url: urlFor(doc.route, data)
24809
- };
24810
- if (data.archivedVersions) {
24811
- hit.version = doc.version ?? "";
24812
- }
24813
- return hit;
24814
- });
25603
+ const results = await searchDocs(data, index, TOOL_INPUTS.search_docs.parse(args));
24815
25604
  return text(JSON.stringify(results, null, 2));
24816
25605
  }
24817
25606
  if (name === "get_page") {
24818
25607
  const input = TOOL_INPUTS.get_page.parse(args);
24819
25608
  const key = normalizeRoute2(input.route, data);
24820
- const markdown = data.pages[key];
25609
+ const markdown = getPageMarkdown(data, key);
24821
25610
  if (markdown === undefined) {
24822
25611
  return text(`No page found at "${key}". Use list_pages or search_docs to find valid routes.`, true);
24823
25612
  }
24824
25613
  return text(markdown);
24825
25614
  }
24826
25615
  if (name === "list_pages") {
24827
- const input = TOOL_INPUTS.list_pages.parse(args);
24828
- const { contentTypes, filters, locale } = input;
24829
- const versionScope = asVersionScope(input.version, data);
24830
- const routes = data.routes.filter((route) => (!contentTypes || contentTypes.includes(route.contentType)) && (!filters || matchesFacets(route.facets, filters)) && (!locale || route.locale === locale) && (versionScope === undefined || route.version === versionScope));
24831
- return text(JSON.stringify(routes.map((route) => {
24832
- const listing = {
24833
- contentType: route.contentType,
24834
- description: route.description,
24835
- facets: route.facets,
24836
- lastModified: route.lastModified,
24837
- route: route.route,
24838
- title: route.title,
24839
- url: urlFor(route.route, data)
24840
- };
24841
- if (data.archivedVersions) {
24842
- listing.version = route.version;
24843
- }
24844
- return listing;
24845
- }), null, 2));
25616
+ const listing = listPages(data, TOOL_INPUTS.list_pages.parse(args));
25617
+ return text(JSON.stringify(listing, null, 2));
24846
25618
  }
24847
25619
  if (name === "get_navigation") {
24848
- const { locale, version: versionId } = TOOL_INPUTS.get_navigation.parse(args);
24849
- const unknownVersion = unknownVersionError(versionId, data);
24850
- if (unknownVersion) {
24851
- return text(unknownVersion, true);
25620
+ const result = getNavigation(data, TOOL_INPUTS.get_navigation.parse(args));
25621
+ if ("error" in result) {
25622
+ return text(result.error, true);
24852
25623
  }
24853
- let { navigation } = data;
24854
- const byLocale = versionId ? data.navigationByVersion?.[versionId] : undefined;
24855
- if (byLocale) {
24856
- navigation = (locale ? byLocale[locale] : undefined) ?? byLocale[data.defaultLocale ?? ""] ?? Object.values(byLocale)[0] ?? navigation;
24857
- } else if (locale && data.navigationByLocale?.[locale]) {
24858
- navigation = data.navigationByLocale[locale];
24859
- }
24860
- return text(JSON.stringify(navigation, null, 2));
25624
+ return text(JSON.stringify(result.navigation, null, 2));
24861
25625
  }
24862
25626
  return text(`Unknown tool: ${name}`, true);
24863
25627
  });
@@ -25009,11 +25773,11 @@ var translateAgentArgs = (kind, lastMessagePath) => kind === "claude" ? claudeAr
25009
25773
  import { createHash as createHash5 } from "node:crypto";
25010
25774
  import { readFile as readFile26 } from "node:fs/promises";
25011
25775
  import { join as join45 } from "pathe";
25012
- import { z as z8 } from "zod";
25776
+ import { z as z9 } from "zod";
25013
25777
  var LEDGER_FILE = "blume.translations.json";
25014
- var ledgerSchema = z8.object({
25015
- files: z8.record(z8.string(), z8.record(z8.string(), z8.string())),
25016
- version: z8.literal(1)
25778
+ var ledgerSchema = z9.object({
25779
+ files: z9.record(z9.string(), z9.record(z9.string(), z9.string())),
25780
+ version: z9.literal(1)
25017
25781
  });
25018
25782
  var emptyLedger = () => ({
25019
25783
  files: {},
@@ -26150,18 +26914,18 @@ var checkPathLink = (resolved, fragment, page2, link, site, ctx, via = "") => {
26150
26914
  };
26151
26915
  };
26152
26916
  var checkExternalLinks = async (refs) => {
26153
- const results = await probeAll(refs.map((ref) => ref.url));
26917
+ const results = await probeAll(refs.map((ref2) => ref2.url));
26154
26918
  const diagnostics = [];
26155
- for (const ref of refs) {
26156
- const result = results.get(ref.url);
26919
+ for (const ref2 of refs) {
26920
+ const result = results.get(ref2.url);
26157
26921
  const grade = result ? gradeExternal(result) : null;
26158
26922
  if (grade) {
26159
26923
  diagnostics.push({
26160
26924
  code: "BLUME_DEAD_LINK",
26161
- column: ref.column,
26162
- file: ref.file,
26163
- line: ref.line,
26164
- message: `External link ${ref.url} is unreachable (${grade.detail}).`,
26925
+ column: ref2.column,
26926
+ file: ref2.file,
26927
+ line: ref2.line,
26928
+ message: `External link ${ref2.url} is unreachable (${grade.detail}).`,
26165
26929
  severity: grade.severity
26166
26930
  });
26167
26931
  }
@@ -26215,7 +26979,7 @@ var validateLinks = async (graph, options) => {
26215
26979
  let uncheckedAssets = 0;
26216
26980
  for (const page2 of graph.pages) {
26217
26981
  for (const link of page2.links) {
26218
- const result = classifyLink(page2, link, ctx, (ref) => external.push(ref));
26982
+ const result = classifyLink(page2, link, ctx, (ref2) => external.push(ref2));
26219
26983
  if (result === "asset-unchecked") {
26220
26984
  uncheckedAssets += 1;
26221
26985
  } else if (result) {
@@ -26613,5 +27377,5 @@ process.on("unhandledRejection", (error) => {
26613
27377
  });
26614
27378
  runMain(main, { rawArgs: normalizeHostArgs(process.argv.slice(2)) });
26615
27379
 
26616
- //# debugId=2A8DB3E67F0908D564756E2164756E21
27380
+ //# debugId=9AB8E5F1D71CCEF764756E2164756E21
26617
27381
  //# sourceMappingURL=index.js.map