blume 1.6.1 → 1.6.3

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
  });
@@ -19601,7 +20288,6 @@ var resolveSearchPopular = (popular, basePath) => popular.map(({ href, icon, lab
19601
20288
 
19602
20289
  // src/theme/code-block-padding.ts
19603
20290
  var CODE_PADDING_BLOCK_REM = 1;
19604
- var FLUSH_CODE_PADDING_TOP_REM = 2.5;
19605
20291
 
19606
20292
  // src/theme/entry.ts
19607
20293
  var DARK_VARIANT = `/* Dark mode is driven by data-theme on the <html> element. */
@@ -20177,27 +20863,19 @@ blume-tabs pre[data-language],
20177
20863
  padding-top: ${CODE_PADDING_BLOCK_REM}rem;
20178
20864
  }
20179
20865
 
20180
- /* Room for the copy button. The docs layout injects it into every \`.prose pre\`
20181
- absolutely positioned against the pre's border box, 1.875rem tall, at
20182
- \`top-2.5\` in the flush contexts above and \`top-2\` elsewhere and it is
20183
- unconditional, so wherever no language bar exists to hold it, it painted
20184
- over the first line of code: any line long enough to reach the button's
20185
- strip (a curl invocation, an install command, an import path) went under
20186
- it. Keyed on the attribute the docs layout stamps on <body>, so the strip
20187
- only appears where the injector runs (PageLayout pages run none), and on
20188
- \`.astro-code\` so the playground's response pre created after the
20189
- injector ran, never given a button keeps its plain inset. The first
20190
- selector is the injector's own flush predicate; the second covers the
20191
- remaining bar-less Shiki case, an untitled <CodeBlock> in plain prose, from
20192
- first paint. The third covers a raw \`<pre><code>\` written in prose — no
20193
- \`.astro-code\`, no language bar, but the injector gives it a button all
20194
- the same — keyed on that injected button, which is exactly what separates
20195
- it from the playground's pre. API panel blocks match, but their own
20196
- !important padding wins and they hide the injected button. */
20197
- [data-blume-code-copy] :is(blume-tabs, .not-prose) pre.astro-code,
20198
- [data-blume-code-copy] .prose pre.astro-code:not([data-language]),
20199
- [data-blume-code-copy] .prose pre:not([data-language]):has(> [data-blume-copy]) {
20200
- padding-top: ${FLUSH_CODE_PADDING_TOP_REM}rem;
20866
+ /* Where no language bar holds the copy button a flush block in tabs or a
20867
+ not-prose component, a bar-less block in prose the docs layout still pins
20868
+ one over the block's top-right corner, so a first line long enough to reach
20869
+ it would end underneath. A code switcher (CodeGroup, ts2js) hosts the button
20870
+ in its tab strip instead and needs nothing here; for every block that keeps
20871
+ an overlay button, give the scrolling code element enough end padding that
20872
+ the line's tail clears the button at the end of its scroll. Keyed on the
20873
+ injected button itself (which the playground's client-created response pre
20874
+ never gets), and \`.prose\`-scoped to outrank the base \`:where(pre code)\`
20875
+ inset. Not a vertical insetthat reserved a strip above every one-line
20876
+ command and read as a rendering bug. */
20877
+ .prose :is(blume-tabs pre, .not-prose pre, pre:not([data-language])):not(.twoslash, .twoslash pre, blume-panel-tabs *):has(> [data-blume-copy]) > code {
20878
+ padding-inline-end: 3.5rem;
20201
20879
  }
20202
20880
 
20203
20881
  blume-tabs pre[data-language]::before,
@@ -21311,6 +21989,7 @@ var buildRuntimeData = (project) => {
21311
21989
  description: config.description,
21312
21990
  discovery: {
21313
21991
  agentReadability: config.seo.agentReadability,
21992
+ api: config.ai.api,
21314
21993
  llmsTxt: config.ai.llmsTxt.enabled,
21315
21994
  sitemap: config.seo.sitemap && Boolean(config.deployment.site)
21316
21995
  },
@@ -21439,12 +22118,18 @@ var planMcp = (project, srcDir, userPages) => {
21439
22118
  enabled: true
21440
22119
  };
21441
22120
  };
21442
- var writeMcpFiles = async (project, plan, write, modules) => {
21443
- if (!plan.enabled) {
21444
- return;
22121
+ var publishAgentData = async (project, plans, modules) => {
22122
+ if (!(plans.mcp.enabled || plans.api.enabled)) {
22123
+ return null;
21445
22124
  }
21446
22125
  const data = await buildMcpData(project);
21447
22126
  modules.set("blume:mcp-data", JSON.stringify(data));
22127
+ return data;
22128
+ };
22129
+ var writeMcpFiles = async (plan, write, data) => {
22130
+ if (!(plan.enabled && data)) {
22131
+ return;
22132
+ }
21448
22133
  const discoveryInput = {
21449
22134
  base: data.base,
21450
22135
  name: data.name,
@@ -21458,6 +22143,52 @@ var writeMcpFiles = async (project, plan, write, modules) => {
21458
22143
  write(join31(plan.dir, "server-card.ts"), staticJsonEndpointTemplate(buildMcpServerCard(discoveryInput)))
21459
22144
  ]);
21460
22145
  };
22146
+ var ownsApiRest = (page) => page.pattern.startsWith("/api/[");
22147
+ var contentUnderApi = (page) => page.route === "/api" || page.route.startsWith("/api/");
22148
+ var planApi = (project, srcDir, userPages) => {
22149
+ const { config, context } = project;
22150
+ const server = config.deployment.output === "server";
22151
+ return {
22152
+ catchAll: server && !userPages.some(ownsApiRest) && !project.graph.pages.some(contentUnderApi),
22153
+ enabled: config.ai.api,
22154
+ server,
22155
+ spec: !routeIsTaken(userPages, project.graph.pages, OPENAPI_PATH) && !existsSync17(join31(context.root, "public", "openapi.json")),
22156
+ srcDir
22157
+ };
22158
+ };
22159
+ var writeApiFiles = async (project, plan, write, data, mcp) => {
22160
+ if (!(plan.enabled && data)) {
22161
+ return;
22162
+ }
22163
+ const { config } = project;
22164
+ const mcpRoute = mcp.enabled ? mcp.route : null;
22165
+ const apiDir = join31(plan.srcDir, "pages", "api");
22166
+ const writes = [
22167
+ write(join31(apiDir, "docs", "pages.json.ts"), apiPagesIndexTemplate()),
22168
+ write(join31(apiDir, "docs", "pages", "[...route].json.ts"), apiPageTemplate()),
22169
+ write(join31(apiDir, "docs", "navigation.json.ts"), apiNavigationTemplate())
22170
+ ];
22171
+ if (plan.server) {
22172
+ writes.push(write(join31(apiDir, "docs", "search.ts"), apiSearchTemplate()));
22173
+ }
22174
+ if (plan.catchAll) {
22175
+ writes.push(write(join31(apiDir, "[...path].ts"), apiNotFoundTemplate({ base: data.base, site: data.site })));
22176
+ }
22177
+ if (plan.spec) {
22178
+ writes.push(write(join31(plan.srcDir, "pages", "openapi.json.ts"), staticJsonEndpointTemplate(buildApiSpec({
22179
+ agentReadability: config.seo.agentReadability,
22180
+ base: data.base,
22181
+ description: config.description,
22182
+ llmsTxt: config.ai.llmsTxt.enabled,
22183
+ mcpRoute,
22184
+ name: config.title,
22185
+ search: plan.server,
22186
+ site: data.site,
22187
+ version: data.version
22188
+ }))));
22189
+ }
22190
+ await Promise.all(writes);
22191
+ };
21461
22192
  var planPlaygroundProxy = (config, srcDir) => ({
21462
22193
  enabled: needsPlaygroundProxy(config),
21463
22194
  entrypoint: join31(srcDir, "blume-openapi", "api-proxy.ts"),
@@ -21514,7 +22245,8 @@ var writeNotFoundPage = async (write, srcDir, pages, contentPages) => {
21514
22245
  }
21515
22246
  await Promise.all([
21516
22247
  write(join31(srcDir, "pages", "404.astro"), notFoundPageTemplate()),
21517
- write(join31(srcDir, "pages", "404.md.ts"), notFoundMarkdownTemplate())
22248
+ write(join31(srcDir, "pages", "404.md.ts"), notFoundMarkdownTemplate()),
22249
+ write(join31(srcDir, "pages", "404.json.ts"), notFoundJsonTemplate())
21518
22250
  ]);
21519
22251
  };
21520
22252
  var diagnosticWarning = (diagnostic) => diagnostic.suggestion ? `${diagnostic.message} ${diagnostic.suggestion}` : diagnostic.message;
@@ -21604,6 +22336,8 @@ var generateRuntime = async (project) => {
21604
22336
  const changelogIndex = hasGeneratedChangelog(project, pages);
21605
22337
  const mcp = planMcp(project, srcDir, pages);
21606
22338
  pages.push(...mcp.discoveryPages);
22339
+ const api = planApi(project, srcDir, pages);
22340
+ const agentData = await publishAgentData(project, { api, mcp }, modules);
21607
22341
  const openApiSource2 = project.sources.find(isOpenApiSource);
21608
22342
  const openApiData = openApiSource2 ? openApiSource2.openApiData() : {};
21609
22343
  const playgroundProxy = planPlaygroundProxy(config, srcDir);
@@ -21676,7 +22410,8 @@ var generateRuntime = async (project) => {
21676
22410
  Promise.all(slotPlan.wrappers.map((wrapper) => write(join31(srcDir, "generated", "component-slots", `${wrapper.name}.astro`), wrapper.content))),
21677
22411
  Promise.all(exampleDiscovery.examples.map((example) => write(join31(srcDir, "generated", "examples", `${exampleSlug(example.path)}.astro`), exampleWrapperTemplate(example)))),
21678
22412
  writeAskFiles(project, srcDir, write, modules),
21679
- writeMcpFiles(project, mcp, write, modules),
22413
+ writeMcpFiles(mcp, write, agentData),
22414
+ writeApiFiles(project, api, write, agentData, mcp),
21680
22415
  playgroundProxy.enabled ? write(playgroundProxy.entrypoint, playgroundProxyTemplate(proxyOrigins)) : Promise.resolve(false)
21681
22416
  ]);
21682
22417
  if (config.seo.og.enabled) {
@@ -21988,8 +22723,11 @@ var emitVercelNegotiation = async (project, routePaths, root) => {
21988
22723
  }
21989
22724
  const rawMarkdown = await buildRawMarkdown(project);
21990
22725
  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);
22726
+ const staticDir = join33(root, ".vercel", "output", "static");
22727
+ const injected = injectNegotiationRoutes(await readFile20(configPath, "utf-8"), routePaths, buildHomeLinkHeader(config2, routePaths), overrides, home ? markdownTokenCount(agentMarkdown(home)) : undefined, {
22728
+ json: existsSync19(join33(staticDir, "404.json")),
22729
+ markdown: existsSync19(join33(staticDir, "404.md"))
22730
+ });
21993
22731
  if (injected === null) {
21994
22732
  logger.warn("Could not wire Accept: text/markdown negotiation into .vercel/output/config.json — raw Markdown stays available at the .md URLs.");
21995
22733
  return;
@@ -24489,6 +25227,9 @@ import {
24489
25227
  McpError,
24490
25228
  ReadResourceRequestSchema
24491
25229
  } from "@modelcontextprotocol/sdk/types.js";
25230
+ import { z as z8 } from "zod";
25231
+
25232
+ // src/ai/mcp/query.ts
24492
25233
  import { z as z7 } from "zod";
24493
25234
 
24494
25235
  // src/search/orama-index.ts
@@ -24632,13 +25373,10 @@ var queryOramaIndex = async (db, term, limit, filters) => {
24632
25373
  return found.hits.map((hit) => hit.document);
24633
25374
  };
24634
25375
 
24635
- // src/ai/mcp/server.ts
25376
+ // src/ai/mcp/query.ts
24636
25377
  var DEFAULT_SEARCH_LIMIT = 8;
24637
25378
  var MAX_SEARCH_LIMIT = 20;
24638
25379
  var EXCERPT_LENGTH = 200;
24639
- var RESOURCE_MIME_TYPE = "text/markdown";
24640
- var RESOURCE_NOT_FOUND = -32002;
24641
- var LOCAL_RESOURCE_SCHEME = "blume:";
24642
25380
  var contentTypesField = z7.preprocess((value) => {
24643
25381
  const list2 = (Array.isArray(value) ? value : [value]).filter((entry) => typeof entry === "string");
24644
25382
  return list2.length > 0 ? list2 : undefined;
@@ -24690,21 +25428,6 @@ var TOOL_INPUTS = {
24690
25428
  version: versionField
24691
25429
  })
24692
25430
  };
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
25431
  var matchesFacets = (facets, filters) => Object.entries(filters).every(([key, value]) => facets?.[key] === value);
24709
25432
  var asVersionScope = (value, data) => {
24710
25433
  if (!data.archivedVersions) {
@@ -24736,8 +25459,6 @@ var urlFor = (route, data) => {
24736
25459
  const path = withBasePath(data.base, route);
24737
25460
  return data.site ? absoluteUrl(data.site, path) : path;
24738
25461
  };
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
25462
  var excerptFor = (doc) => {
24742
25463
  if (doc.description) {
24743
25464
  return doc.description;
@@ -24745,10 +25466,6 @@ var excerptFor = (doc) => {
24745
25466
  const head = doc.content.slice(0, EXCERPT_LENGTH).trim();
24746
25467
  return doc.content.length > EXCERPT_LENGTH ? `${head}…` : head;
24747
25468
  };
24748
- var text = (value, isError = false) => {
24749
- const content = [{ text: value, type: "text" }];
24750
- return isError ? { content, isError: true } : { content };
24751
- };
24752
25469
  var createIndexProvider = (documents, locale) => {
24753
25470
  let dbPromise = null;
24754
25471
  return function provideIndex() {
@@ -24756,6 +25473,90 @@ var createIndexProvider = (documents, locale) => {
24756
25473
  return dbPromise;
24757
25474
  };
24758
25475
  };
25476
+ var searchDocs = async (data, index, input) => {
25477
+ const db = await index();
25478
+ const hits = await queryOramaIndex(db, input.query, input.limit ?? DEFAULT_SEARCH_LIMIT, {
25479
+ contentTypes: input.contentTypes,
25480
+ facets: input.filters,
25481
+ locale: input.locale,
25482
+ version: asVersionScope(input.version, data)
25483
+ });
25484
+ return hits.map((doc) => {
25485
+ const hit = {
25486
+ contentType: doc.contentType,
25487
+ excerpt: excerptFor(doc),
25488
+ facets: doc.facets,
25489
+ route: doc.route,
25490
+ title: doc.title,
25491
+ url: urlFor(doc.route, data)
25492
+ };
25493
+ if (data.archivedVersions) {
25494
+ hit.version = doc.version ?? "";
25495
+ }
25496
+ return hit;
25497
+ });
25498
+ };
25499
+ var getPageMarkdown = (data, route) => data.pages[route];
25500
+ var listPages = (data, input) => {
25501
+ const { contentTypes, filters, locale } = input;
25502
+ const versionScope = asVersionScope(input.version, data);
25503
+ 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) => {
25504
+ const listing = {
25505
+ contentType: route.contentType,
25506
+ description: route.description,
25507
+ facets: route.facets,
25508
+ lastModified: route.lastModified,
25509
+ route: route.route,
25510
+ title: route.title,
25511
+ url: urlFor(route.route, data)
25512
+ };
25513
+ if (data.archivedVersions) {
25514
+ listing.version = route.version;
25515
+ }
25516
+ return listing;
25517
+ });
25518
+ };
25519
+ var getNavigation = (data, input) => {
25520
+ const { locale, version: versionId } = input;
25521
+ const unknownVersion = unknownVersionError(versionId, data);
25522
+ if (unknownVersion) {
25523
+ return { error: unknownVersion };
25524
+ }
25525
+ let { navigation } = data;
25526
+ const byLocale = versionId ? data.navigationByVersion?.[versionId] : undefined;
25527
+ if (byLocale) {
25528
+ navigation = (locale ? byLocale[locale] : undefined) ?? byLocale[data.defaultLocale ?? ""] ?? Object.values(byLocale)[0] ?? navigation;
25529
+ } else if (locale && data.navigationByLocale?.[locale]) {
25530
+ navigation = data.navigationByLocale[locale];
25531
+ }
25532
+ return { navigation };
25533
+ };
25534
+
25535
+ // src/ai/mcp/server.ts
25536
+ var RESOURCE_MIME_TYPE = "text/markdown";
25537
+ var RESOURCE_NOT_FOUND = -32002;
25538
+ var LOCAL_RESOURCE_SCHEME = "blume:";
25539
+ var inputSchemaFor = (schema) => {
25540
+ const {
25541
+ $schema: _dialect,
25542
+ additionalProperties: _closed,
25543
+ ...rest
25544
+ } = z8.toJSONSchema(schema);
25545
+ return rest;
25546
+ };
25547
+ var TOOL_DEFINITIONS = MCP_TOOLS.map((tool) => ({
25548
+ annotations: tool.annotations,
25549
+ description: tool.description,
25550
+ inputSchema: inputSchemaFor(TOOL_INPUTS[tool.name]),
25551
+ name: tool.name,
25552
+ title: tool.title
25553
+ }));
25554
+ var resourceUri = (route, data) => data.site ? urlFor(route, data) : `${LOCAL_RESOURCE_SCHEME}${withBasePath(data.base, route)}`;
25555
+ var resourceRoute = (uri, data) => normalizeRoute2(uri.startsWith(LOCAL_RESOURCE_SCHEME) ? uri.slice(LOCAL_RESOURCE_SCHEME.length) : uri, data);
25556
+ var text = (value, isError = false) => {
25557
+ const content = [{ text: value, type: "text" }];
25558
+ return isError ? { content, isError: true } : { content };
25559
+ };
24759
25560
  var buildServer = (data, index) => {
24760
25561
  const capabilities = { resources: {}, tools: {} };
24761
25562
  const serverOptions = data.instructions ? { capabilities, instructions: data.instructions } : { capabilities };
@@ -24779,7 +25580,7 @@ var buildServer = (data, index) => {
24779
25580
  }));
24780
25581
  server.setRequestHandler(ReadResourceRequestSchema, (request2) => {
24781
25582
  const { uri } = request2.params;
24782
- const markdown = data.pages[resourceRoute(uri, data)];
25583
+ const markdown = getPageMarkdown(data, resourceRoute(uri, data));
24783
25584
  if (markdown === undefined) {
24784
25585
  throw new McpError(RESOURCE_NOT_FOUND, `No page found at "${uri}". Use resources/list or list_pages to find valid URIs.`);
24785
25586
  }
@@ -24790,74 +25591,28 @@ var buildServer = (data, index) => {
24790
25591
  server.setRequestHandler(CallToolRequestSchema, async (request2) => {
24791
25592
  const { arguments: args = {}, name } = request2.params;
24792
25593
  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
- });
25594
+ const results = await searchDocs(data, index, TOOL_INPUTS.search_docs.parse(args));
24815
25595
  return text(JSON.stringify(results, null, 2));
24816
25596
  }
24817
25597
  if (name === "get_page") {
24818
25598
  const input = TOOL_INPUTS.get_page.parse(args);
24819
25599
  const key = normalizeRoute2(input.route, data);
24820
- const markdown = data.pages[key];
25600
+ const markdown = getPageMarkdown(data, key);
24821
25601
  if (markdown === undefined) {
24822
25602
  return text(`No page found at "${key}". Use list_pages or search_docs to find valid routes.`, true);
24823
25603
  }
24824
25604
  return text(markdown);
24825
25605
  }
24826
25606
  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));
25607
+ const listing = listPages(data, TOOL_INPUTS.list_pages.parse(args));
25608
+ return text(JSON.stringify(listing, null, 2));
24846
25609
  }
24847
25610
  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);
25611
+ const result = getNavigation(data, TOOL_INPUTS.get_navigation.parse(args));
25612
+ if ("error" in result) {
25613
+ return text(result.error, true);
24852
25614
  }
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));
25615
+ return text(JSON.stringify(result.navigation, null, 2));
24861
25616
  }
24862
25617
  return text(`Unknown tool: ${name}`, true);
24863
25618
  });
@@ -25009,11 +25764,11 @@ var translateAgentArgs = (kind, lastMessagePath) => kind === "claude" ? claudeAr
25009
25764
  import { createHash as createHash5 } from "node:crypto";
25010
25765
  import { readFile as readFile26 } from "node:fs/promises";
25011
25766
  import { join as join45 } from "pathe";
25012
- import { z as z8 } from "zod";
25767
+ import { z as z9 } from "zod";
25013
25768
  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)
25769
+ var ledgerSchema = z9.object({
25770
+ files: z9.record(z9.string(), z9.record(z9.string(), z9.string())),
25771
+ version: z9.literal(1)
25017
25772
  });
25018
25773
  var emptyLedger = () => ({
25019
25774
  files: {},
@@ -26150,18 +26905,18 @@ var checkPathLink = (resolved, fragment, page2, link, site, ctx, via = "") => {
26150
26905
  };
26151
26906
  };
26152
26907
  var checkExternalLinks = async (refs) => {
26153
- const results = await probeAll(refs.map((ref) => ref.url));
26908
+ const results = await probeAll(refs.map((ref2) => ref2.url));
26154
26909
  const diagnostics = [];
26155
- for (const ref of refs) {
26156
- const result = results.get(ref.url);
26910
+ for (const ref2 of refs) {
26911
+ const result = results.get(ref2.url);
26157
26912
  const grade = result ? gradeExternal(result) : null;
26158
26913
  if (grade) {
26159
26914
  diagnostics.push({
26160
26915
  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}).`,
26916
+ column: ref2.column,
26917
+ file: ref2.file,
26918
+ line: ref2.line,
26919
+ message: `External link ${ref2.url} is unreachable (${grade.detail}).`,
26165
26920
  severity: grade.severity
26166
26921
  });
26167
26922
  }
@@ -26215,7 +26970,7 @@ var validateLinks = async (graph, options) => {
26215
26970
  let uncheckedAssets = 0;
26216
26971
  for (const page2 of graph.pages) {
26217
26972
  for (const link of page2.links) {
26218
- const result = classifyLink(page2, link, ctx, (ref) => external.push(ref));
26973
+ const result = classifyLink(page2, link, ctx, (ref2) => external.push(ref2));
26219
26974
  if (result === "asset-unchecked") {
26220
26975
  uncheckedAssets += 1;
26221
26976
  } else if (result) {
@@ -26613,5 +27368,5 @@ process.on("unhandledRejection", (error) => {
26613
27368
  });
26614
27369
  runMain(main, { rawArgs: normalizeHostArgs(process.argv.slice(2)) });
26615
27370
 
26616
- //# debugId=2A8DB3E67F0908D564756E2164756E21
27371
+ //# debugId=91C1BFDA6463CA2664756E2164756E21
26617
27372
  //# sourceMappingURL=index.js.map