blume 1.1.3 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +1 -1
  3. package/dist/cli/index.js +1473 -149
  4. package/dist/cli/index.js.map +47 -36
  5. package/dist/types/core/config-input.d.ts +18 -0
  6. package/dist/types/core/config.d.ts +4 -0
  7. package/dist/types/core/data.d.ts +3 -0
  8. package/dist/types/core/schema.d.ts +132 -17
  9. package/dist/types/core/types.d.ts +5 -3
  10. package/dist/types/openapi/references.d.ts +6 -0
  11. package/docs/advanced/api-reference.mdx +27 -0
  12. package/docs/advanced/changelog.mdx +10 -0
  13. package/docs/configuration/ai.mdx +38 -2
  14. package/docs/configuration/customization.mdx +27 -0
  15. package/docs/configuration/index.mdx +5 -0
  16. package/docs/content/navigation.mdx +12 -0
  17. package/docs/reference/cli.mdx +17 -13
  18. package/docs/reference/eval.mdx +106 -0
  19. package/docs/reference/meta.ts +1 -1
  20. package/package.json +1 -1
  21. package/src/ai/agent-readability.ts +19 -1
  22. package/src/ai/llms.ts +9 -4
  23. package/src/ai/mcp/server.ts +48 -14
  24. package/src/ai/mcp/stdio.ts +35 -0
  25. package/src/astro/generate.ts +119 -48
  26. package/src/astro/templates.ts +173 -37
  27. package/src/audit/checks/duplicates.ts +15 -6
  28. package/src/audit/checks/indexability.ts +11 -2
  29. package/src/audit/checks/network.ts +22 -8
  30. package/src/audit/checks/sitemap.ts +42 -16
  31. package/src/audit/redirects.ts +12 -1
  32. package/src/audit/run.ts +13 -3
  33. package/src/audit/url.ts +21 -2
  34. package/src/cli/commands/audit.ts +21 -6
  35. package/src/cli/commands/dev.ts +19 -2
  36. package/src/cli/commands/eval.ts +291 -0
  37. package/src/cli/commands/init.ts +9 -4
  38. package/src/cli/commands/mcp-stdio.ts +36 -0
  39. package/src/cli/index.ts +4 -0
  40. package/src/cli/required-secrets.ts +1 -1
  41. package/src/components/content/AccordionItem.astro +2 -2
  42. package/src/components/content/Frame.astro +4 -1
  43. package/src/components/content/Prompt.astro +4 -1
  44. package/src/components/content/Tooltip.astro +4 -1
  45. package/src/components/content/TreeFolder.astro +1 -2
  46. package/src/components/content/Update.astro +45 -0
  47. package/src/components/islands/AskAI.astro +9 -2
  48. package/src/components/islands/ask-ai.tsx +23 -4
  49. package/src/components/islands/hooks.ts +48 -15
  50. package/src/components/layout/NavTree.astro +37 -19
  51. package/src/components/layout/ReferenceLayout.astro +4 -0
  52. package/src/components/layout/RootLayout.astro +14 -3
  53. package/src/components/layout/Search.astro +5 -1
  54. package/src/components/layout/head-scripts.ts +22 -5
  55. package/src/components/openapi/SchemaProperty.astro +3 -3
  56. package/src/core/config-input.ts +18 -0
  57. package/src/core/config.ts +4 -0
  58. package/src/core/data.ts +3 -0
  59. package/src/core/deployment-env.ts +7 -2
  60. package/src/core/graph.ts +8 -1
  61. package/src/core/i18n.ts +10 -2
  62. package/src/core/navigation.ts +16 -5
  63. package/src/core/schema.ts +51 -4
  64. package/src/core/server-features.ts +1 -1
  65. package/src/core/sources/normalize.ts +69 -8
  66. package/src/core/sources/notion.ts +4 -2
  67. package/src/core/sources/sanity.ts +5 -3
  68. package/src/core/types.ts +5 -3
  69. package/src/eval/agents.ts +340 -0
  70. package/src/eval/findings.ts +103 -0
  71. package/src/eval/prompts.ts +78 -0
  72. package/src/eval/report.ts +214 -0
  73. package/src/eval/run.ts +290 -0
  74. package/src/eval/schema.ts +124 -0
  75. package/src/markdown/code-title.ts +7 -1
  76. package/src/openapi/model.ts +31 -2
  77. package/src/openapi/references.ts +23 -2
  78. package/src/openapi/render-mdx.ts +39 -11
  79. package/src/openapi/scalar.ts +1 -0
  80. package/src/openapi/source.ts +11 -4
  81. package/src/registry/eject.ts +23 -1
  82. package/src/search/build.ts +4 -3
package/dist/cli/index.js CHANGED
@@ -3,7 +3,7 @@ import { createRequire } from "node:module";
3
3
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
4
 
5
5
  // src/cli/index.ts
6
- import { defineCommand as defineCommand12, runMain } from "citty";
6
+ import { defineCommand as defineCommand14, runMain } from "citty";
7
7
 
8
8
  // src/core/version.ts
9
9
  import { readFileSync } from "node:fs";
@@ -1581,6 +1581,13 @@ var normalizePath = (path) => {
1581
1581
  const trimmed = path.replace(/\/+$/u, "");
1582
1582
  return trimmed === "" ? "/" : trimmed;
1583
1583
  };
1584
+ var decodePath = (path) => {
1585
+ try {
1586
+ return decodeURI(path);
1587
+ } catch {
1588
+ return path;
1589
+ }
1590
+ };
1584
1591
  var siteOrigin = (site) => {
1585
1592
  if (!site) {
1586
1593
  return null;
@@ -1611,7 +1618,7 @@ var resolveHref = (pageUrl, href, origin, deployBase = "") => {
1611
1618
  return {
1612
1619
  hash: parsed.hash.slice(1),
1613
1620
  kind: "self-origin",
1614
- path: normalizePath(stripBasePath(deployBase, parsed.pathname))
1621
+ path: normalizePath(stripBasePath(deployBase, decodePath(parsed.pathname)))
1615
1622
  };
1616
1623
  }
1617
1624
  return { kind: "external", url: parsed.toString() };
@@ -1626,7 +1633,7 @@ var resolveHref = (pageUrl, href, origin, deployBase = "") => {
1626
1633
  return {
1627
1634
  hash: resolved.hash.slice(1),
1628
1635
  kind: "internal",
1629
- path: normalizePath(stripBasePath(deployBase, resolved.pathname))
1636
+ path: normalizePath(stripBasePath(deployBase, decodePath(resolved.pathname)))
1630
1637
  };
1631
1638
  };
1632
1639
 
@@ -1825,17 +1832,20 @@ var contentChecks = {
1825
1832
  };
1826
1833
 
1827
1834
  // src/audit/checks/duplicates.ts
1828
- var isNonCanonical = (page) => {
1835
+ var isNonCanonical = (page, deployBase) => {
1829
1836
  if (!page.canonical) {
1830
1837
  return false;
1831
1838
  }
1832
1839
  try {
1833
- return new URL(page.canonical).pathname.replace(/\/$/u, "") !== page.url.replace(/\/$/u, "");
1840
+ return stripBasePath(deployBase, decodePath(new URL(page.canonical).pathname)).replace(/\/$/u, "") !== page.url.replace(/\/$/u, "");
1834
1841
  } catch {
1835
1842
  return false;
1836
1843
  }
1837
1844
  };
1838
- var comparable = (context) => context.pages.filter((page) => page.indexable && !page.route?.fallback && !isNonCanonical(page));
1845
+ var comparable = (context) => {
1846
+ const deployBase = normalizeBasePath(context.project.config.deployment.base);
1847
+ return context.pages.filter((page) => page.indexable && !page.route?.fallback && !isNonCanonical(page, deployBase));
1848
+ };
1839
1849
  var reportGroups = (context, pages, id, key, describe, frontmatterKey) => {
1840
1850
  const groups = new Map;
1841
1851
  for (const page of pages) {
@@ -2009,12 +2019,12 @@ var PLATFORMS = [
2009
2019
  {
2010
2020
  adapter: "vercel",
2011
2021
  detect: (env) => Boolean(env.VERCEL),
2012
- site: (env) => toUrl(env.VERCEL_PROJECT_PRODUCTION_URL ?? env.VERCEL_URL)
2022
+ site: (env) => toUrl(env.VERCEL_PROJECT_PRODUCTION_URL) ?? toUrl(env.VERCEL_URL)
2013
2023
  },
2014
2024
  {
2015
2025
  adapter: "netlify",
2016
2026
  detect: (env) => Boolean(env.NETLIFY),
2017
- site: (env) => toUrl(env.URL ?? env.DEPLOY_PRIME_URL ?? env.DEPLOY_URL)
2027
+ site: (env) => toUrl(env.URL) ?? toUrl(env.DEPLOY_PRIME_URL) ?? toUrl(env.DEPLOY_URL)
2018
2028
  },
2019
2029
  {
2020
2030
  adapter: "cloudflare",
@@ -2073,7 +2083,7 @@ var canonicalChecks = (context, page) => {
2073
2083
  }
2074
2084
  return found;
2075
2085
  }
2076
- const target = normalizePath(canonical.pathname);
2086
+ const target = normalizePath(stripBasePath(normalizeBasePath(context.project.config.deployment.base), decodePath(canonical.pathname)));
2077
2087
  if (target === normalizePath(page.url)) {
2078
2088
  return found;
2079
2089
  }
@@ -2401,7 +2411,7 @@ var probeAll = async (urls, options = {}) => {
2401
2411
  var CLIENT_ERROR = 400;
2402
2412
  var SERVER_ERROR = 500;
2403
2413
  var SLOW_MS = 1500;
2404
- var liveUrl = (origin, page) => new URL(page.url, origin).toString();
2414
+ var liveUrl = (origin, page, deployBase) => new URL(`${deployBase}${page.url}`, origin).toString();
2405
2415
  var badResponse = (context, page, result) => {
2406
2416
  const site = pageSite(context, page);
2407
2417
  if (result.timedOut) {
@@ -2445,12 +2455,13 @@ var networkChecks = {
2445
2455
  return [];
2446
2456
  }
2447
2457
  const found = [];
2448
- const targets = context.pages.map((page) => liveUrl(origin, page));
2449
- const robotsUrl = new URL("/robots.txt", origin).toString();
2450
- const sitemapUrl = new URL("/sitemap.xml", origin).toString();
2458
+ const deployBase = normalizeBasePath(context.project.config.deployment.base);
2459
+ const targets = context.pages.map((page) => liveUrl(origin, page, deployBase));
2460
+ const robotsUrl = new URL(`${deployBase}/robots.txt`, origin).toString();
2461
+ const sitemapUrl = new URL(`${deployBase}/sitemap.xml`, origin).toString();
2451
2462
  const results = await probeAll([...targets, robotsUrl, sitemapUrl]);
2452
2463
  for (const page of context.pages) {
2453
- const result = results.get(liveUrl(origin, page));
2464
+ const result = results.get(liveUrl(origin, page, deployBase));
2454
2465
  if (!result) {
2455
2466
  continue;
2456
2467
  }
@@ -2717,23 +2728,23 @@ var MAX_SITEMAP_BYTES = 50 * 1024 * 1024;
2717
2728
  var MAX_SITEMAP_URLS = 50000;
2718
2729
  var LASTMOD_SLACK_MS = 24 * 60 * 60 * 1000;
2719
2730
  var ERROR_ROUTES2 = new Set(["/404", "/500"]);
2720
- var sitemapPaths = (context) => {
2731
+ var sitemapPaths = (context, deployBase) => {
2721
2732
  const paths = new Map;
2722
2733
  for (const loc of context.sitemap?.urls ?? []) {
2723
2734
  try {
2724
- paths.set(normalizePath(new URL(loc).pathname), loc);
2735
+ paths.set(normalizePath(stripBasePath(deployBase, decodePath(new URL(loc).pathname))), loc);
2725
2736
  } catch {}
2726
2737
  }
2727
2738
  return paths;
2728
2739
  };
2729
- var canonicalPath = (canonical) => {
2740
+ var canonicalPath = (canonical, deployBase) => {
2730
2741
  try {
2731
- return normalizePath(new URL(canonical).pathname);
2742
+ return normalizePath(stripBasePath(deployBase, decodePath(new URL(canonical).pathname)));
2732
2743
  } catch {
2733
2744
  return null;
2734
2745
  }
2735
2746
  };
2736
- var checkListedUrl = (context, loc, origin, file) => {
2747
+ var checkListedUrl = (context, loc, origin, file, deployBase) => {
2737
2748
  let parsed;
2738
2749
  try {
2739
2750
  parsed = new URL(loc);
@@ -2747,7 +2758,7 @@ var checkListedUrl = (context, loc, origin, file) => {
2747
2758
  finding("BLUME_AUDIT_SITEMAP_OUT_OF_SCOPE", { file, url: loc }, `sitemap.xml lists ${loc}, which is on another origin.`)
2748
2759
  ];
2749
2760
  }
2750
- const path = normalizePath(stripBasePath(normalizeBasePath(context.project.config.deployment.base), parsed.pathname));
2761
+ const path = normalizePath(stripBasePath(deployBase, decodePath(parsed.pathname)));
2751
2762
  const page = context.byUrl.get(path);
2752
2763
  if (!page) {
2753
2764
  const redirect = context.redirects.find((entry) => normalizePath(entry.from) === path);
@@ -2759,7 +2770,7 @@ var checkListedUrl = (context, loc, origin, file) => {
2759
2770
  if (!page.indexable) {
2760
2771
  found.push(finding("BLUME_AUDIT_NOINDEX_IN_SITEMAP", pageSite(context, page, ["noindex"]), `${path} is in the sitemap but declares robots "${page.robots}".`));
2761
2772
  }
2762
- const canonical = page.canonical && canonicalPath(page.canonical);
2773
+ const canonical = page.canonical && canonicalPath(page.canonical, deployBase);
2763
2774
  if (canonical && canonical !== path) {
2764
2775
  found.push(finding("BLUME_AUDIT_NON_CANONICAL_IN_SITEMAP", pageSite(context, page, ["seo", "canonical"]), `${path} is in the sitemap but canonicalizes to ${canonical}.`));
2765
2776
  }
@@ -2795,9 +2806,10 @@ var sitemapChecks = {
2795
2806
  }
2796
2807
  }
2797
2808
  const origin = siteOrigin(site);
2798
- const listed = sitemapPaths(context);
2809
+ const deployBase = normalizeBasePath(context.project.config.deployment.base);
2810
+ const listed = sitemapPaths(context, deployBase);
2799
2811
  for (const loc of sitemap.urls) {
2800
- found.push(...checkListedUrl(context, loc, origin, sitemap.file));
2812
+ found.push(...checkListedUrl(context, loc, origin, sitemap.file, deployBase));
2801
2813
  }
2802
2814
  for (const page of context.pages) {
2803
2815
  if (!page.indexable || ERROR_ROUTES2.has(page.url) || listed.has(normalizePath(page.url))) {
@@ -3078,7 +3090,12 @@ var routeSlug = (route) => slugify(trimChar(route, "/")) || "reference";
3078
3090
  var sourcesOf = (block) => {
3079
3091
  const sources = [...block.sources];
3080
3092
  if (block.spec) {
3081
- sources.unshift({ spec: block.spec });
3093
+ sources.unshift({
3094
+ includeInLlms: true,
3095
+ includeInSearch: true,
3096
+ noindex: false,
3097
+ spec: block.spec
3098
+ });
3082
3099
  }
3083
3100
  return sources;
3084
3101
  };
@@ -3102,8 +3119,11 @@ var referencesFor = (kind, block, defaultLabel, renderer, display, basePath) =>
3102
3119
  return {
3103
3120
  basePath,
3104
3121
  display,
3122
+ includeInLlms: source.includeInLlms,
3123
+ includeInSearch: source.includeInSearch,
3105
3124
  kind,
3106
3125
  label,
3126
+ noindex: source.noindex,
3107
3127
  renderer,
3108
3128
  route,
3109
3129
  scalar: block.scalar,
@@ -3405,7 +3425,10 @@ var WRANGLER_CONFIG_FILES = [
3405
3425
  "wrangler.toml"
3406
3426
  ];
3407
3427
  var resolveCloudflareAdapterArgs = (context) => {
3408
- const args = ['prerenderEnvironment: "node"'];
3428
+ const args = [
3429
+ 'prerenderEnvironment: "node"',
3430
+ 'imageService: "compile"'
3431
+ ];
3409
3432
  const wranglerPath = WRANGLER_CONFIG_FILES.map((file) => join8(context.root, file)).find((file) => existsSync4(file));
3410
3433
  if (wranglerPath) {
3411
3434
  let configPath = relative5(context.outDir, wranglerPath);
@@ -3416,6 +3439,18 @@ var resolveCloudflareAdapterArgs = (context) => {
3416
3439
  }
3417
3440
  return `{ ${args.join(", ")} }`;
3418
3441
  };
3442
+ var resolveSessionOption = (deployment) => deployment.output === "server" && deployment.adapter === "cloudflare" ? `
3443
+ session: { driver: sessionDrivers.memory() },` : "";
3444
+ var astroConfigImportLine = (options) => {
3445
+ const names = ["defineConfig"];
3446
+ if (options.hasFonts) {
3447
+ names.push("fontProviders");
3448
+ }
3449
+ if (options.hasSession) {
3450
+ names.push("sessionDrivers");
3451
+ }
3452
+ return `import { ${names.join(", ")} } from "astro/config";`;
3453
+ };
3419
3454
  var runtimeDependencies = (options) => {
3420
3455
  const { config, needsReact, needsSvelte, needsVue } = options;
3421
3456
  const deps = ["@astrojs/mdx"];
@@ -3432,7 +3467,7 @@ var runtimeDependencies = (options) => {
3432
3467
  deps.push("@scalar/astro");
3433
3468
  }
3434
3469
  deps.push(...searchProviderMeta(config.search.provider).runtimeDeps);
3435
- if (config.ai.ask?.enabled) {
3470
+ if (config.ai.ask?.enabled && !config.ai.ask.endpoint) {
3436
3471
  const askDep = askBackendRuntimeDep(config.ai.ask);
3437
3472
  if (askDep) {
3438
3473
  deps.push(askDep);
@@ -3468,6 +3503,36 @@ var astroOutDir = (context) => context.distDir ?? `${context.root}/dist`;
3468
3503
  var adapterRoot = (context) => dirname5(astroOutDir(context));
3469
3504
  var REACT_EXCLUDE = String.raw`exclude: [/\/node_modules\/\.vite\//]`;
3470
3505
  var reactIntegration = (compilerPath) => compilerPath ? `react({ babel: { plugins: [[${JSON.stringify(compilerPath)}, { target: "19" }]] }, ${REACT_EXCLUDE} })` : `react({ ${REACT_EXCLUDE} })`;
3506
+ var devWatchOption = (outDir, contentWatchesRuntimeDir) => contentWatchesRuntimeDir ? `
3507
+ // Astro's cache dir sits inside the docs collection, whose watcher would
3508
+ // otherwise churn (and can loop) on Astro's own writes. Trade-off: .md
3509
+ // body edits need a dev-server restart in this layout.
3510
+ watch: {
3511
+ ignored: ${JSON.stringify([join8(outDir, ".astro", "**")])},
3512
+ },` : "";
3513
+ var renderIntegrationBridge = (bridge) => {
3514
+ if (!bridge) {
3515
+ return {
3516
+ configSourceMarker: "",
3517
+ userConfigImports: "",
3518
+ userConfigSetup: "",
3519
+ userIntegrationSpread: ""
3520
+ };
3521
+ }
3522
+ return {
3523
+ configSourceMarker: bridge.sourceHash ? `// Blume config source SHA-256: ${bridge.sourceHash}
3524
+ ` : "",
3525
+ userConfigImports: `import { dirname, resolve } from "node:path";
3526
+ import { fileURLToPath } from "node:url";
3527
+ import { createModuleLoader } from "blume/core/load-module.ts";
3528
+ `,
3529
+ userConfigSetup: `const loadBlumeConfig = createModuleLoader();
3530
+ const blumeConfig = await loadBlumeConfig(resolve(dirname(fileURLToPath(import.meta.url)), ${JSON.stringify(bridge.configFile)}));
3531
+
3532
+ `,
3533
+ userIntegrationSpread: ", ...(blumeConfig?.integrations ?? [])"
3534
+ };
3535
+ };
3471
3536
  var astroConfigTemplate = (options) => {
3472
3537
  const { context, config, needsReact, pages, dataPath, themePath } = options;
3473
3538
  const {
@@ -3498,6 +3563,7 @@ var astroConfigTemplate = (options) => {
3498
3563
  const adapterExpr = deployment.adapter === "vercel" ? `withAdapterRoot(adapter(${adapterArgs}), ${JSON.stringify(adapterRoot(context))})` : `adapter(${adapterArgs})`;
3499
3564
  const adapterOption = server && deployment.adapter ? `
3500
3565
  adapter: ${adapterExpr},` : "";
3566
+ const sessionOption = resolveSessionOption(deployment);
3501
3567
  const siteOption = deployment.site ? `
3502
3568
  site: ${JSON.stringify(deployment.site)},` : "";
3503
3569
  const baseOption = deployment.base ? `
@@ -3519,7 +3585,10 @@ var astroConfigTemplate = (options) => {
3519
3585
  const fontEntries = buildFontEntries(config.theme.fonts);
3520
3586
  const fontsOption = fontEntries.length ? `
3521
3587
  fonts: [${fontEntries.map((font) => `{ provider: fontProviders.google(), name: ${JSON.stringify(font.name)}, cssVariable: ${JSON.stringify(font.cssVariable)}, weights: ${JSON.stringify(font.weights)}, fallbacks: ${JSON.stringify(font.fallbacks)} }`).join(", ")}],` : "";
3522
- const defineConfigImport = fontEntries.length ? `import { defineConfig, fontProviders } from "astro/config";` : `import { defineConfig } from "astro/config";`;
3588
+ const defineConfigImport = astroConfigImportLine({
3589
+ hasFonts: fontEntries.length > 0,
3590
+ hasSession: sessionOption.length > 0
3591
+ });
3523
3592
  const reactImport = needsReact ? `import react from "@astrojs/react";
3524
3593
  ` : "";
3525
3594
  const vueImport = needsVue ? `import vue from "@astrojs/vue";
@@ -3554,19 +3623,26 @@ var astroConfigTemplate = (options) => {
3554
3623
  integrations.push("svelte()");
3555
3624
  }
3556
3625
  integrations.push(`blumeIntegration(${JSON.stringify({ base: deployment.base, contentRoutes, pages })})`);
3626
+ const watchOption = devWatchOption(context.outDir, options.contentWatchesRuntimeDir);
3627
+ const {
3628
+ configSourceMarker,
3629
+ userConfigImports,
3630
+ userConfigSetup,
3631
+ userIntegrationSpread
3632
+ } = renderIntegrationBridge(options.integrationBridge);
3557
3633
  return `// Generated by Blume. Do not edit; this file is recreated on each run.
3558
- ${defineConfigImport}
3634
+ ${configSourceMarker}${userConfigImports}${defineConfigImport}
3559
3635
  import mdx from "@astrojs/mdx";
3560
3636
  import tailwindcss from "@tailwindcss/vite";
3561
3637
  import { blumeMarkdownProcessor, blumeMdxProcessor, blumeShikiTransformers, blumeTwoslashTransformer } from "blume/markdown";
3562
3638
  ${reactImport}${vueImport}${svelteImport}${blumeImport}${adapterImport}
3563
- export default defineConfig({
3639
+ ${userConfigSetup}export default defineConfig({
3564
3640
  root: ${JSON.stringify(context.outDir)},
3565
3641
  srcDir: ${JSON.stringify(`${context.outDir}/src`)},
3566
3642
  outDir: ${JSON.stringify(astroOutDir(context))},
3567
3643
  publicDir: ${JSON.stringify(`${context.root}/public`)},
3568
- output: ${JSON.stringify(deployment.output)},${adapterOption}${siteOption}${baseOption}${redirectsOption}${i18nOption}${fontsOption}
3569
- integrations: [${integrations.join(", ")}],
3644
+ output: ${JSON.stringify(deployment.output)},${adapterOption}${sessionOption}${siteOption}${baseOption}${redirectsOption}${i18nOption}${fontsOption}
3645
+ integrations: [${integrations.join(", ")}${userIntegrationSpread}],
3570
3646
  markdown: {
3571
3647
  processor: blumeMarkdownProcessor(${JSON.stringify({
3572
3648
  basePath: config.basePath,
@@ -3586,18 +3662,25 @@ export default defineConfig({
3586
3662
  devToolbar: { enabled: false },
3587
3663
  vite: {
3588
3664
  plugins: [tailwindcss(), prerenderDepsPlugin(), serverAppResolvePlugin()],
3589
- // Mermaid (lazy-loaded client-side for diagrams) statically imports dayjs as
3590
- // CJS (\`dayjs/dayjs.min.js\`). In dev, an un-pre-bundled dependency is served
3591
- // as raw ESM, and that UMD file exposes no \`default\` export, so mermaid
3592
- // throws on load and diagrams render blank. Forcing mermaid through the dep
3593
- // optimizer bundles dayjs with correct CJS interop. In a standalone install
3594
- // Blume's dynamic \`import("mermaid")\` lives inside \`node_modules/blume\`,
3595
- // which Vite's optimizer scan doesn't crawl, so mermaid is never discovered
3596
- // on its own hence the explicit include. mermaid resolves through the
3597
- // \`blume\` package (it isn't a direct dep of the generated project), so the
3598
- // nested \`blume > mermaid\` form is required. Production (Rollup) already
3599
- // handles the interop, so this only affects dev.
3600
- optimizeDeps: { include: ["blume > mermaid"] },
3665
+ // The lazy client-side imports both land on CJS/UMD files: mermaid (for
3666
+ // diagrams) statically imports dayjs as CJS (\`dayjs/dayjs.min.js\`), and
3667
+ // epub-gen-memory's browser bundle is a browserified UMD. In dev, an
3668
+ // un-pre-bundled dependency is served as raw ESM, where such a file
3669
+ // exposes no \`default\` export mermaid throws on load and diagrams
3670
+ // render blank, and the EPUB export throws \`epub is not a function\`
3671
+ // (the UMD finds no \`exports\`/\`define\` and strands its callable on
3672
+ // \`window.epubGen\` instead). Forcing them through the dep optimizer
3673
+ // restores the CJS interop. In a standalone install these dynamic imports
3674
+ // live inside \`node_modules/blume\`, which Vite's optimizer scan doesn't
3675
+ // crawl, so neither is discovered on its own — hence the explicit
3676
+ // includes. They resolve through the \`blume\` package (they aren't direct
3677
+ // deps of the generated project), so the nested \`blume > x\` form is
3678
+ // required, and epub-gen-memory must name the \`/bundle\` subpath that is
3679
+ // actually imported: optimizing the package root leaves that entry out.
3680
+ // Production (Rollup) already handles the interop, so this only affects dev.
3681
+ optimizeDeps: {
3682
+ include: ["blume > mermaid", "blume > epub-gen-memory/bundle"],
3683
+ },
3601
3684
  // Blume's render-time deps are forced external on both build environments so
3602
3685
  // native bindings resolve at runtime and isolated linkers don't bundle
3603
3686
  // symlinked store copies (which would surface their children as unresolvable
@@ -3628,22 +3711,17 @@ export default defineConfig({
3628
3711
  server: {
3629
3712
  fs: {
3630
3713
  allow: ${JSON.stringify(fsAllow)},
3631
- },
3632
- // Keep the file watcher out of Astro's own cache dir. In a migrated
3633
- // (root-rooted) project the docs collection is rooted at the project dir,
3634
- // so its glob-loader watcher would otherwise fire on every write Astro
3635
- // makes under .blume/.astro (data-store.json, content module manifests,
3636
- // self-hosted fonts) -- pure noise the loader logs as "No entry type
3637
- // found". Vite appends this to its default ignores.
3638
- watch: {
3639
- ignored: ${JSON.stringify([join8(context.outDir, ".astro", "**")])},
3640
- },
3714
+ },${watchOption}
3641
3715
  },
3642
3716
  },
3643
3717
  });
3644
3718
  `;
3645
3719
  };
3646
3720
  var stagedContentDir = (outDir) => join8(outDir, "content");
3721
+ var runtimeDirWithin = (base, outDir) => {
3722
+ const rel = relative5(base, outDir);
3723
+ return rel && !rel.startsWith("..") && !isAbsolute(rel) ? rel : null;
3724
+ };
3647
3725
  var astroGlobBase = (base) => isAbsolute(base) ? pathToFileURL(base).href : base;
3648
3726
  var contentConfigTemplate = (options) => {
3649
3727
  const { context, config } = options;
@@ -3651,8 +3729,8 @@ var contentConfigTemplate = (options) => {
3651
3729
  const collectionBase = options.collection?.base ?? context.contentRoot;
3652
3730
  const includeGlobs = options.collection?.include ?? config.content.include;
3653
3731
  const excludeGlobs = options.collection?.exclude ?? config.content.exclude;
3654
- const outDirRel = relative5(collectionBase, context.outDir);
3655
- const outDirIgnore = outDirRel && !outDirRel.startsWith("..") && !isAbsolute(outDirRel) ? [`!${outDirRel}/**`] : [];
3732
+ const outDirRel = runtimeDirWithin(collectionBase, context.outDir);
3733
+ const outDirIgnore = outDirRel ? [`!${outDirRel}/**`] : [];
3656
3734
  const filesystem = options.filesystem ?? true;
3657
3735
  const docsPattern = filesystem ? [
3658
3736
  ...includeGlobs,
@@ -3796,7 +3874,11 @@ import data from "blume:data";
3796
3874
  const { strings } = Astro.props;
3797
3875
  ---
3798
3876
 
3799
- <AskAI strings={strings ?? data.ui.ask} suggestions={data.config.ask?.suggestions ?? []} />
3877
+ <AskAI
3878
+ endpoint={data.config.ask?.endpoint ?? undefined}
3879
+ strings={strings ?? data.ui.ask}
3880
+ suggestions={data.config.ask?.suggestions ?? []}
3881
+ />
3800
3882
  ` : `---
3801
3883
  // Generated by Blume. Do not edit.
3802
3884
  // Ask AI is off (\`ai.ask.enabled\`), so the header's Ask trigger renders nothing.
@@ -4078,6 +4160,7 @@ const htmlLang = i18n ? i18n.defaultLocale : "en";
4078
4160
  favicon={data.config.favicon}
4079
4161
  appleIcon={data.config.appleIcon}
4080
4162
  navigation={data.navigation}
4163
+ noindex={${options.noindex === true}}
4081
4164
  pageTitle={${JSON.stringify(options.title)}}
4082
4165
  route={${JSON.stringify(options.route)}}
4083
4166
  searchEnabled={data.config.search.enabled}
@@ -5125,6 +5208,10 @@ var crawlStaticDir = async (options) => {
5125
5208
  };
5126
5209
 
5127
5210
  // src/audit/redirects.ts
5211
+ var pathOnly = (value) => {
5212
+ const cut = value.search(/[?#]/u);
5213
+ return cut === -1 ? value : value.slice(0, cut);
5214
+ };
5128
5215
  var resolveRedirects = (redirects, pageUrls) => {
5129
5216
  const byFrom = new Map;
5130
5217
  for (const redirect of redirects) {
@@ -5140,7 +5227,7 @@ var resolveRedirects = (redirects, pageUrls) => {
5140
5227
  chain.push(current);
5141
5228
  break;
5142
5229
  }
5143
- const next = normalizePath(current);
5230
+ const next = normalizePath(pathOnly(current));
5144
5231
  if (seen.has(next)) {
5145
5232
  chain.push(next);
5146
5233
  return {
@@ -5222,8 +5309,9 @@ var matches = (id, terms) => {
5222
5309
  var runAudit = async (options) => {
5223
5310
  const { project } = options;
5224
5311
  const staticDir = deployStaticDir(project.config, project.context);
5312
+ const basePath = normalizeBasePath(project.config.basePath);
5225
5313
  const crawl = await crawlStaticDir({
5226
- basePath: normalizeBasePath(project.config.basePath),
5314
+ basePath,
5227
5315
  manifest: project.manifest,
5228
5316
  staticDir
5229
5317
  });
@@ -5240,7 +5328,11 @@ var runAudit = async (options) => {
5240
5328
  origin,
5241
5329
  pages: crawl.pages,
5242
5330
  project,
5243
- redirects: resolveRedirects(project.config.redirects, new Set([...byUrl.keys(), ...crawl.files.keys()].map((path) => normalizePath(path)))),
5331
+ redirects: resolveRedirects(project.config.redirects.map((redirect) => ({
5332
+ ...redirect,
5333
+ from: withBasePath(basePath, redirect.from),
5334
+ to: withBasePath(basePath, redirect.to)
5335
+ })), new Set([...byUrl.keys(), ...crawl.files.keys()].map((path) => normalizePath(path)))),
5244
5336
  robots: crawl.robots,
5245
5337
  sitemap: crawl.sitemap,
5246
5338
  sources: await readSources(crawl.pages),
@@ -7490,6 +7582,9 @@ var searchMetaSchema = z2.strictObject({
7490
7582
  exclude: z2.boolean().default(false),
7491
7583
  tags: z2.array(z2.string()).optional()
7492
7584
  });
7585
+ var aiMetaSchema = z2.strictObject({
7586
+ exclude: z2.boolean().default(false)
7587
+ });
7493
7588
  var changelogMetaSchema = z2.strictObject({
7494
7589
  category: z2.string().optional(),
7495
7590
  date: dateSchema.optional(),
@@ -7505,6 +7600,7 @@ var authorSchema = z2.union([
7505
7600
  }).catchall(z2.unknown())
7506
7601
  ]);
7507
7602
  var pageMetaBaseSchema = z2.strictObject({
7603
+ ai: aiMetaSchema.default({}),
7508
7604
  authors: z2.union([authorSchema, z2.array(authorSchema)]).optional(),
7509
7605
  changelog: changelogMetaSchema.optional(),
7510
7606
  date: dateSchema.optional(),
@@ -7638,6 +7734,7 @@ var contentConfigSchema = z2.strictObject({
7638
7734
  sources: z2.array(contentSourceSchema).optional()
7639
7735
  });
7640
7736
  var navTabSchema = z2.strictObject({
7737
+ href: z2.string().min(1).optional(),
7641
7738
  icon: iconName.optional(),
7642
7739
  items: z2.array(z2.strictObject({
7643
7740
  description: z2.string().optional(),
@@ -7782,11 +7879,25 @@ var mcpConfigSchema = z2.strictObject({
7782
7879
  name: z2.string().optional(),
7783
7880
  route: z2.string().default("/mcp").transform(normalizeRoute)
7784
7881
  });
7882
+ var askEndpointSchema = z2.string().trim().min(1).refine((value) => {
7883
+ if (value.startsWith("/") && !value.startsWith("//")) {
7884
+ return true;
7885
+ }
7886
+ try {
7887
+ const url = new URL(value);
7888
+ return url.protocol === "http:" || url.protocol === "https:";
7889
+ } catch {
7890
+ return false;
7891
+ }
7892
+ }, {
7893
+ message: "ai.ask.endpoint must be an HTTP(S) URL or a root-relative path."
7894
+ });
7785
7895
  var aiConfigSchema = z2.strictObject({
7786
7896
  ask: z2.strictObject({
7787
7897
  apiKeyEnv: z2.string().optional(),
7788
7898
  baseUrl: z2.string().url().optional(),
7789
7899
  enabled: z2.boolean().default(false),
7900
+ endpoint: askEndpointSchema.optional(),
7790
7901
  model: z2.string().default("openai/gpt-5.5"),
7791
7902
  provider: z2.enum(askAiProviders).default("gateway"),
7792
7903
  suggestions: z2.array(z2.strictObject({
@@ -7794,7 +7905,7 @@ var aiConfigSchema = z2.strictObject({
7794
7905
  label: z2.string().min(1)
7795
7906
  })).default([])
7796
7907
  }).superRefine((value, ctx) => {
7797
- if (value.provider === "openai-compatible" && !value.baseUrl) {
7908
+ if (value.provider === "openai-compatible" && !(value.baseUrl || value.endpoint)) {
7798
7909
  ctx.addIssue({
7799
7910
  code: z2.ZodIssueCode.custom,
7800
7911
  message: 'ai.ask.baseUrl is required when provider is "openai-compatible".',
@@ -8022,7 +8133,10 @@ var reactConfigSchema = z2.strictObject({
8022
8133
  compiler: z2.boolean().default(true)
8023
8134
  });
8024
8135
  var openapiSourceSchema = z2.strictObject({
8136
+ includeInLlms: z2.boolean().default(true),
8137
+ includeInSearch: z2.boolean().default(true),
8025
8138
  label: z2.string().optional(),
8139
+ noindex: z2.boolean().default(false),
8026
8140
  route: z2.string().optional(),
8027
8141
  spec: z2.string()
8028
8142
  });
@@ -8095,6 +8209,7 @@ var blumeConfigSchema = z2.strictObject({
8095
8209
  frontmatter: frontmatterConfigSchema.default({}),
8096
8210
  github: githubConfigSchema.optional(),
8097
8211
  i18n: i18nConfigSchema.optional(),
8212
+ integrations: z2.array(z2.custom()).default([]),
8098
8213
  lastModified: lastModifiedConfigSchema.default(false),
8099
8214
  logo: logoConfigSchema.optional(),
8100
8215
  markdown: markdownConfigSchema.default({}),
@@ -8191,9 +8306,10 @@ var detectLocale = (parts, i18n) => {
8191
8306
  var localePlacement = (rel, ext, i18n) => {
8192
8307
  const base = rel.slice(0, rel.length - ext.length);
8193
8308
  if (base.endsWith(".$")) {
8309
+ const shared = `${base.slice(0, -2)}${ext}`;
8194
8310
  return {
8195
8311
  locales: i18n.locales.map((locale2) => locale2.code),
8196
- navPath: `${base.slice(0, -2)}${ext}`
8312
+ navPath: i18n.parser === "dir" ? detectLocale(shared.split("/"), i18n).rest.join("/") : shared
8197
8313
  };
8198
8314
  }
8199
8315
  if (i18n.parser === "dot") {
@@ -8673,7 +8789,8 @@ var normalizeRef = (ref) => {
8673
8789
  return "/";
8674
8790
  }
8675
8791
  const withSlash = ref.startsWith("/") ? ref : `/${ref}`;
8676
- const trimmed = withSlash.endsWith("/index") ? withSlash.slice(0, -"/index".length) : withSlash;
8792
+ const noTrailing = withSlash.replace(/\/+$/u, "");
8793
+ const trimmed = noTrailing.endsWith("/index") ? noTrailing.slice(0, -"/index".length) : noTrailing;
8677
8794
  return trimmed === "" ? "/" : trimmed;
8678
8795
  };
8679
8796
  var routeForRef = (ref, byRoute, basePath) => {
@@ -8768,7 +8885,7 @@ var resolveTabHref = (sidebar, path) => {
8768
8885
  return walk(sidebar) ? path : first ?? path;
8769
8886
  };
8770
8887
  var withTabHrefs = (tabs, sidebar) => tabs.map((tab) => {
8771
- const href = resolveTabHref(sidebar, tab.path);
8888
+ const href = tab.href ?? resolveTabHref(sidebar, tab.path);
8772
8889
  return href === tab.path ? tab : { ...tab, href };
8773
8890
  });
8774
8891
  var buildNavigation = (pages, options) => {
@@ -8790,6 +8907,7 @@ var buildNavigation = (pages, options) => {
8790
8907
  })) : options.selectors ?? [];
8791
8908
  const tabs = basePath ? (options.tabs ?? []).map((tab) => ({
8792
8909
  ...tab,
8910
+ ...tab.href ? { href: withBasePath(basePath, tab.href) } : {},
8793
8911
  items: tab.items?.map(rebasePath),
8794
8912
  path: withBasePath(basePath, tab.path)
8795
8913
  })) : options.tabs ?? [];
@@ -8868,6 +8986,7 @@ var buildLocaleNavigation = (code, pages, fallback, fallbackByKey, options, i18n
8868
8986
  const localizePath = (path) => path.startsWith("/") ? localizeRoute(path, code, i18n) : path;
8869
8987
  const tabs = options.navigation.tabs?.map((tab) => ({
8870
8988
  ...tab,
8989
+ ...tab.href ? { href: localizePath(tab.href) } : {},
8871
8990
  items: tab.items?.map((item) => ({
8872
8991
  ...item,
8873
8992
  path: localizePath(item.path)
@@ -8880,7 +8999,10 @@ var buildLocaleNavigation = (code, pages, fallback, fallbackByKey, options, i18n
8880
8999
  basePath: options.basePath ?? "",
8881
9000
  diagnostics,
8882
9001
  display: options.navigation.sidebar.display,
8883
- featured: options.navigation.featured,
9002
+ featured: options.navigation.featured?.map((link) => ({
9003
+ ...link,
9004
+ href: localizePath(link.href)
9005
+ })),
8884
9006
  folderMeta: options.folderMeta,
8885
9007
  localizedRoot: localizeRoute("/", code, i18n),
8886
9008
  metaPrefix: i18n.parser === "dir" && code !== i18n.defaultLocale ? code : "",
@@ -9176,6 +9298,7 @@ var WORD_SPLIT2 = /[-_]/u;
9176
9298
  var stripNumericPrefix = (segment) => segment.replace(NUMERIC_PREFIX2, "");
9177
9299
  var groupLabel = (segment) => segment.match(GROUP_FOLDER2)?.groups?.label ?? null;
9178
9300
  var slugify2 = (text) => text.toLowerCase().trim().replaceAll(/[^\w\s-]/gu, "").replaceAll(/[\s_]+/gu, "-").replaceAll(/-+/gu, "-").replaceAll(/^-|-$/gu, "");
9301
+ var slugifyPath = (text) => text.split("/").map(slugify2).filter(Boolean).join("/");
9179
9302
  var titleCase = (value) => value.split(WORD_SPLIT2).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
9180
9303
  var addRouteSegment = (part, segments, groups) => {
9181
9304
  if (part === "") {
@@ -9205,11 +9328,18 @@ var mapRoute = (relativePath) => {
9205
9328
  };
9206
9329
  var CODE_FENCE = /^(?<delimiter>```|~~~)/u;
9207
9330
  var nextFenceState = (line, fence) => {
9208
- const delimiter = line.trimStart().match(CODE_FENCE)?.groups?.delimiter;
9331
+ const trimmed = line.trimStart();
9332
+ const delimiter = trimmed.match(CODE_FENCE)?.groups?.delimiter;
9209
9333
  if (delimiter === undefined) {
9210
9334
  return fence;
9211
9335
  }
9212
9336
  if (fence === null) {
9337
+ if (delimiter === "```") {
9338
+ const run = trimmed.match(/^`+/u)?.[0].length ?? 0;
9339
+ if (trimmed.slice(run).includes("`")) {
9340
+ return fence;
9341
+ }
9342
+ }
9213
9343
  return delimiter;
9214
9344
  }
9215
9345
  return fence === delimiter ? null : fence;
@@ -9227,6 +9357,9 @@ var linesWithoutFrontMatter = (body) => {
9227
9357
  if (!/^-{3}\s*$/u.test(lines[0] ?? "")) {
9228
9358
  return lines;
9229
9359
  }
9360
+ if ((lines[1] ?? "").trim() === "") {
9361
+ return lines;
9362
+ }
9230
9363
  const close = lines.findIndex((line, index) => index > 0 && FRONT_MATTER_CLOSE.test(line));
9231
9364
  return close === -1 ? lines : lines.slice(close + 1);
9232
9365
  };
@@ -9306,8 +9439,10 @@ var extractHeadings = (body) => {
9306
9439
  }
9307
9440
  return headings;
9308
9441
  };
9309
- var MD_LINK = /\[[^\]]*\]\((?<target>[^)\s]+)(?:\s+"[^"]*")?\)/gu;
9442
+ var MD_LINK = /\[(?<label>(?:[^[\]]|\[[^\]]*\])*)\]\((?<target>(?:[^()\s]|\([^()\s]*\))+)(?<title>\s+"[^"]*")?\)/gu;
9443
+ var MD_IMAGE = /!\[[^\]]*\]\((?<target>(?:[^()\s]|\([^()\s]*\))+)(?<title>\s+"[^"]*")?\)/gu;
9310
9444
  var INLINE_CODE = /`[^`]*`/gu;
9445
+ var targetOffsetIn = (matched, target, title) => matched.length - 1 - (title?.length ?? 0) - target.length;
9311
9446
  var scanLinkLine = (line, lineNumber, fence, links) => {
9312
9447
  const next = nextFenceState(line, fence);
9313
9448
  if (fence !== null || next !== null) {
@@ -9319,12 +9454,24 @@ var scanLinkLine = (line, lineNumber, fence, links) => {
9319
9454
  if (target === undefined || match.index === undefined) {
9320
9455
  continue;
9321
9456
  }
9322
- const targetOffset = match.index + match[0].indexOf("](") + "](".length;
9457
+ const targetOffset = targetOffsetIn(match[0], target, match.groups?.title);
9323
9458
  links.push({
9324
- column: targetOffset + 1,
9459
+ column: match.index + targetOffset + 1,
9325
9460
  line: lineNumber,
9326
9461
  target
9327
9462
  });
9463
+ const label = match[0].slice(0, targetOffset - "](".length);
9464
+ for (const image of label.matchAll(MD_IMAGE)) {
9465
+ const imageTarget = image.groups?.target;
9466
+ if (imageTarget === undefined || image.index === undefined) {
9467
+ continue;
9468
+ }
9469
+ links.push({
9470
+ column: match.index + image.index + targetOffsetIn(image[0], imageTarget, image.groups?.title) + 1,
9471
+ line: lineNumber,
9472
+ target: imageTarget
9473
+ });
9474
+ }
9328
9475
  }
9329
9476
  return next;
9330
9477
  };
@@ -9598,6 +9745,24 @@ var operationKey = (method, path, operationId) => {
9598
9745
  return fromId || slugify3(`${method}-${path}`);
9599
9746
  };
9600
9747
  var isOperation = (value) => typeof value === "object" && value !== null;
9748
+ var tagSlugger = () => {
9749
+ const assigned = new Map;
9750
+ const taken = new Set;
9751
+ return (name) => {
9752
+ const existing = assigned.get(name);
9753
+ if (existing) {
9754
+ return existing;
9755
+ }
9756
+ const base = slugify3(name) || "operations";
9757
+ let slug = base;
9758
+ for (let suffix = 2;taken.has(slug); suffix += 1) {
9759
+ slug = `${base}-${suffix}`;
9760
+ }
9761
+ taken.add(slug);
9762
+ assigned.set(name, slug);
9763
+ return slug;
9764
+ };
9765
+ };
9601
9766
  var extractOperations = (document, baseRoute) => {
9602
9767
  const operations = [];
9603
9768
  const tagOrder = [];
@@ -9605,6 +9770,7 @@ var extractOperations = (document, baseRoute) => {
9605
9770
  const tagMeta = new Map((document.tags ?? []).map((tag) => [tag.name, tag.description ?? ""]));
9606
9771
  const seen = new Set;
9607
9772
  const warnings = [];
9773
+ const slugForTag = tagSlugger();
9608
9774
  for (const [path, rawItem] of Object.entries(document.paths ?? {})) {
9609
9775
  const item = rawItem;
9610
9776
  if (!item) {
@@ -9620,7 +9786,7 @@ var extractOperations = (document, baseRoute) => {
9620
9786
  continue;
9621
9787
  }
9622
9788
  const tag = operation.tags?.[0] ?? UNTAGGED;
9623
- const tagSlug = slugify3(tag) || "operations";
9789
+ const tagSlug = slugForTag(tag);
9624
9790
  if (!tagsSeen.has(tag)) {
9625
9791
  tagsSeen.add(tag);
9626
9792
  tagOrder.push(tag);
@@ -9647,7 +9813,7 @@ var extractOperations = (document, baseRoute) => {
9647
9813
  const tags = tagOrder.map((name) => ({
9648
9814
  description: tagMeta.get(name) ?? "",
9649
9815
  name,
9650
- slug: slugify3(name) || "operations"
9816
+ slug: slugForTag(name)
9651
9817
  }));
9652
9818
  return { operations, tags, warnings };
9653
9819
  };
@@ -9797,15 +9963,14 @@ var parseSpec = async (spec, root, options = {}) => {
9797
9963
  };
9798
9964
 
9799
9965
  // src/openapi/render-mdx.ts
9800
- var MDX_UNSAFE = /[<>{}]/gu;
9966
+ var MDX_UNSAFE = /[<{}]/gu;
9801
9967
  var ENTITIES = {
9802
9968
  "<": "&lt;",
9803
- ">": "&gt;",
9804
9969
  "{": "&#123;",
9805
9970
  "}": "&#125;"
9806
9971
  };
9807
9972
  var MDX_ESM_KEYWORD = /^(?<keyword>import|export)\b/gmu;
9808
- var BACKTICK_CODE = /(?<bt>`+)[\s\S]*?\k<bt>/gu;
9973
+ var BACKTICK_CODE = /(?<!`)(?<bt>`+)(?!`)[\s\S]*?(?<!`)\k<bt>(?!`)/gu;
9809
9974
  var escapeProse = (text) => text.replace(MDX_UNSAFE, (char) => ENTITIES[char] ?? char).replace(MDX_ESM_KEYWORD, (keyword) => `&#${keyword.codePointAt(0)};${keyword.slice(1)}`);
9810
9975
  var mdxSafe = (text) => {
9811
9976
  let out = "";
@@ -9846,23 +10011,30 @@ var operationDescription = (spec, operation) => {
9846
10011
  var withDescription = (description, component) => description.trim() ? `${mdxSafe(description.trim())}
9847
10012
 
9848
10013
  ${component}` : component;
9849
- var operationMdx = (spec, operation) => {
10014
+ var operationMdx = (spec, operation, reference) => {
9850
10015
  const method = operation.method.toUpperCase();
9851
10016
  const title = operation.summary || `${method} ${operation.path}`;
9852
10017
  const description = operation.description.trim() === operation.summary.trim() ? "" : operation.description;
9853
10018
  return {
9854
10019
  body: withDescription(description, `<Operation source="${spec.slug}" id="${operation.key}" />`),
9855
10020
  data: {
10021
+ ...reference?.includeInLlms === false ? { ai: { exclude: true } } : {},
9856
10022
  ...operation.deprecated ? { deprecated: true } : {},
9857
- search: { tags: [operation.tag, method] },
9858
- seo: { description: operationDescription(spec, operation) },
10023
+ search: {
10024
+ ...reference?.includeInSearch === false ? { exclude: true } : {},
10025
+ tags: [operation.tag, method]
10026
+ },
10027
+ seo: {
10028
+ description: operationDescription(spec, operation),
10029
+ ...reference?.noindex ? { noindex: true } : {}
10030
+ },
9859
10031
  sidebar: { badge: method, label: operation.summary || operation.path },
9860
10032
  title,
9861
10033
  type: "openapi-operation"
9862
10034
  }
9863
10035
  };
9864
10036
  };
9865
- var overviewMdx = (spec) => {
10037
+ var overviewMdx = (spec, reference) => {
9866
10038
  const operations = Object.values(spec.operations);
9867
10039
  const sections = [];
9868
10040
  const known = new Set;
@@ -9904,8 +10076,11 @@ var overviewMdx = (spec) => {
9904
10076
 
9905
10077
  `),
9906
10078
  data: {
10079
+ ...reference?.includeInLlms === false ? { ai: { exclude: true } } : {},
10080
+ ...reference?.includeInSearch === false ? { search: { exclude: true } } : {},
9907
10081
  seo: {
9908
- description: clip(plainProse(spec.description), META_DESCRIPTION_MAX) || `${apiName(spec)} API reference.`
10082
+ description: clip(plainProse(spec.description), META_DESCRIPTION_MAX) || `${apiName(spec)} API reference.`,
10083
+ ...reference?.noindex ? { noindex: true } : {}
9909
10084
  },
9910
10085
  sidebar: { label: "Overview" },
9911
10086
  title: apiName(spec)
@@ -9927,10 +10102,10 @@ var toEntry = (rendered, ref) => {
9927
10102
  ref
9928
10103
  };
9929
10104
  };
9930
- var specEntries = (spec, operations) => {
9931
- const entries = operations.map((operation) => toEntry(operationMdx(spec, operation), `${routeToRef(operation.route)}.mdx`));
10105
+ var specEntries = (spec, operations, reference) => {
10106
+ const entries = operations.map((operation) => toEntry(operationMdx(spec, operation, reference), `${routeToRef(operation.route)}.mdx`));
9932
10107
  const base = routeToRef(spec.route);
9933
- entries.push(toEntry(overviewMdx(spec), base ? `${base}/index.mdx` : "index.mdx"));
10108
+ entries.push(toEntry(overviewMdx(spec, reference), base ? `${base}/index.mdx` : "index.mdx"));
9934
10109
  return entries;
9935
10110
  };
9936
10111
  var openApiSource = (references, ctx) => {
@@ -9984,7 +10159,7 @@ var openApiSource = (references, ctx) => {
9984
10159
  }
9985
10160
  ] : []
9986
10161
  ],
9987
- entries: specEntries(spec, operations),
10162
+ entries: specEntries(spec, operations, reference),
9988
10163
  slug: reference.slug,
9989
10164
  spec
9990
10165
  };
@@ -10417,7 +10592,7 @@ import { join as join15 } from "pathe";
10417
10592
  // src/core/sources/assets.ts
10418
10593
  import { mkdir as mkdir5, writeFile as writeFile5 } from "node:fs/promises";
10419
10594
  import { extname as extname4, join as join14 } from "pathe";
10420
- var MD_IMAGE = /!\[(?<alt>[^\]]*)\]\((?<url>[^)\s]+)\)/gu;
10595
+ var MD_IMAGE2 = /!\[(?<alt>[^\]]*)\]\((?<url>[^)\s]+)\)/gu;
10421
10596
  var REMOTE = /^https?:\/\//u;
10422
10597
  var SAFE_EXT = /^\.[a-z0-9]+$/iu;
10423
10598
  var CODE_FENCE_BLOCK = /^(?<fence>`{3,}|~{3,})[^\n]*\n[\s\S]*?^\k<fence>[^\n]*(?=\n|$)/gmu;
@@ -10436,7 +10611,7 @@ var materializeAssets = async (markdown, ctx) => {
10436
10611
  return `\x00blume-fence-${fences.length - 1}\x00`;
10437
10612
  });
10438
10613
  const urls = new Set;
10439
- for (const match of masked.matchAll(MD_IMAGE)) {
10614
+ for (const match of masked.matchAll(MD_IMAGE2)) {
10440
10615
  const url = match.groups?.url;
10441
10616
  if (url && REMOTE.test(url)) {
10442
10617
  urls.add(url);
@@ -10462,7 +10637,7 @@ var materializeAssets = async (markdown, ctx) => {
10462
10637
  });
10463
10638
  }
10464
10639
  }));
10465
- const rewritten = masked.replaceAll(MD_IMAGE, (match, alt, url) => {
10640
+ const rewritten = masked.replaceAll(MD_IMAGE2, (match, alt, url) => {
10466
10641
  const local = rewrites.get(url);
10467
10642
  return local ? `![${alt}](${local})` : match;
10468
10643
  }).replaceAll(FENCE_TOKEN, (token, index) => fences[Number(index)] ?? token);
@@ -10697,7 +10872,7 @@ ${nested}`;
10697
10872
  data.sidebar = { order };
10698
10873
  }
10699
10874
  const slugProp = richToMarkdown(page.properties[props.slug ?? "Slug"]?.rich_text);
10700
- const slug = slugify2(slugProp || title) || page.id;
10875
+ const slug = slugifyPath(slugProp || title) || page.id;
10701
10876
  return { data, slug };
10702
10877
  };
10703
10878
  const toEntry2 = async (client, page) => {
@@ -10904,7 +11079,7 @@ var sanitySource = (options, ctx) => {
10904
11079
  let snapshot = new Map;
10905
11080
  const toEntry2 = (doc) => {
10906
11081
  const slugValue = asString(getPath(doc, fields.slug ?? "slug.current")) ?? asString(doc._id) ?? "untitled";
10907
- const slug = slugify2(slugValue) || slugify2(asString(doc._id) ?? "") || "untitled";
11082
+ const slug = slugifyPath(slugValue) || slugify2(asString(doc._id) ?? "") || "untitled";
10908
11083
  const data = {};
10909
11084
  const title = asString(getPath(doc, fields.title ?? "title"));
10910
11085
  const description = asString(getPath(doc, fields.description ?? "description"));
@@ -11264,6 +11439,16 @@ var shouldFail = (result, gate) => {
11264
11439
  const failing = failingSeverities(gate);
11265
11440
  return result.diagnostics.some((d) => failing.has(d.severity));
11266
11441
  };
11442
+ var launchAgentCode = async (bin, prompt) => {
11443
+ try {
11444
+ return await launchAgent(bin, prompt);
11445
+ } catch (error) {
11446
+ if (error?.code !== "ENOENT") {
11447
+ throw error;
11448
+ }
11449
+ return WINDOWS_COMMAND_NOT_FOUND;
11450
+ }
11451
+ };
11267
11452
  var auditCommand = defineCommand2({
11268
11453
  args: {
11269
11454
  claude: {
@@ -11369,12 +11554,7 @@ var auditCommand = defineCommand2({
11369
11554
  process.stderr.write(` Handing ${count} finding${count === 1 ? "" : "s"} to ${cli.name}…
11370
11555
 
11371
11556
  `);
11372
- let code;
11373
- try {
11374
- code = await launchAgent(cli.bin, fixPrompt(report));
11375
- } catch {
11376
- code = WINDOWS_COMMAND_NOT_FOUND;
11377
- }
11557
+ const code = await launchAgentCode(cli.bin, fixPrompt(report));
11378
11558
  if (code === WINDOWS_COMMAND_NOT_FOUND) {
11379
11559
  logger.error(`${cli.name} (\`${cli.bin}\`) was not found on PATH. Install it with \`${cli.install}\`.`);
11380
11560
  process.exit(1);
@@ -11502,6 +11682,12 @@ var usagePolicy = (signals) => {
11502
11682
  }
11503
11683
  return Object.fromEntries(USAGE_TOKENS.map(([key, token]) => [token, signals[key]]));
11504
11684
  };
11685
+ var askApiUrl = (endpoint, site, abs) => {
11686
+ if (!endpoint) {
11687
+ return abs("/api/ask");
11688
+ }
11689
+ return site && endpoint.startsWith("/") ? `${site.replace(/\/+$/u, "")}${endpoint}` : endpoint;
11690
+ };
11505
11691
  var buildAgentReadability = (project) => {
11506
11692
  const { config } = project;
11507
11693
  if (!config.seo.agentReadability) {
@@ -11530,7 +11716,7 @@ var buildAgentReadability = (project) => {
11530
11716
  };
11531
11717
  }
11532
11718
  if (config.ai.ask?.enabled) {
11533
- artifacts.askApi = abs("/api/ask");
11719
+ artifacts.askApi = askApiUrl(config.ai.ask.endpoint, site, abs);
11534
11720
  }
11535
11721
  if (site && config.seo.sitemap) {
11536
11722
  artifacts.sitemap = abs("/sitemap.xml");
@@ -11873,7 +12059,7 @@ var pageUrl = (route, site, base = "") => {
11873
12059
  const path = withBasePath(base, route);
11874
12060
  return encodeURI(site ? `${site.replace(/\/$/u, "")}${path}` : path);
11875
12061
  };
11876
- var eligiblePages = (project) => project.graph.pages.filter((page) => !(page.meta.draft || page.meta.sidebar.hidden || page.meta.seo.noindex) && (project.config.ai.llmsTxt.openapi || page.source.name !== "openapi"));
12062
+ var eligiblePages = (project) => project.graph.pages.filter((page) => !(page.meta.ai.exclude || page.meta.draft || page.meta.sidebar.hidden || page.meta.seo.noindex && page.source.name !== "openapi") && (project.config.ai.llmsTxt.openapi || page.source.name !== "openapi"));
11877
12063
  var indexedNavigations = (project) => {
11878
12064
  const { i18n } = project.config;
11879
12065
  if (i18n) {
@@ -12022,7 +12208,7 @@ var ensureGitignore = async (root, entries) => {
12022
12208
  // src/core/server-features.ts
12023
12209
  var serverFeatures = (config) => {
12024
12210
  const features = [];
12025
- if (config.ai.ask?.enabled) {
12211
+ if (config.ai.ask?.enabled && !config.ai.ask.endpoint) {
12026
12212
  features.push("Ask AI");
12027
12213
  }
12028
12214
  if (config.ai.mcp.enabled) {
@@ -12572,11 +12758,13 @@ var refuseIfDevRunning = (root, action, options = {}) => {
12572
12758
  };
12573
12759
 
12574
12760
  // src/astro/generate.ts
12761
+ import { createHash as createHash2 } from "node:crypto";
12575
12762
  import { existsSync as existsSync14, readFileSync as readFileSync9, realpathSync } from "node:fs";
12576
12763
  import {
12577
12764
  lstat,
12578
12765
  mkdir as mkdir6,
12579
12766
  readFile as readFile14,
12767
+ readlink,
12580
12768
  realpath,
12581
12769
  rename,
12582
12770
  rm as rm2,
@@ -12585,7 +12773,7 @@ import {
12585
12773
  } from "node:fs/promises";
12586
12774
  import { createRequire as createRequire5 } from "node:module";
12587
12775
  import { pathToFileURL as pathToFileURL3 } from "node:url";
12588
- import { basename as basename3, dirname as dirname9, join as join26, normalize as normalize3, relative as relative13 } from "pathe";
12776
+ import { basename as basename3, dirname as dirname9, join as join26, normalize as normalize3, relative as relative13, resolve as resolve7 } from "pathe";
12589
12777
  import { glob as glob7 } from "tinyglobby";
12590
12778
 
12591
12779
  // src/ai/ask-data.ts
@@ -13397,6 +13585,7 @@ var buildReferenceFiles = async (options) => {
13397
13585
  ...ref.scalar
13398
13586
  },
13399
13587
  dataImport: `${"../".repeat(depth + 1)}generated/data.json`,
13588
+ noindex: ref.noindex,
13400
13589
  route: ref.route,
13401
13590
  title: ref.label
13402
13591
  }),
@@ -14485,12 +14674,12 @@ var canResolveFrom = (fromDir, spec) => {
14485
14674
  return false;
14486
14675
  }
14487
14676
  };
14488
- var resolveReactCompiler = (config, needsReact) => {
14677
+ var resolveReactCompiler = (config, needsReact, pkgDir = packageRoot()) => {
14489
14678
  if (!(needsReact && config.react.compiler)) {
14490
14679
  return null;
14491
14680
  }
14492
14681
  try {
14493
- return createRequire5(pathToFileURL3(join26(packageRoot(), "_.js")).href).resolve("babel-plugin-react-compiler");
14682
+ return createRequire5(pathToFileURL3(join26(pkgDir, "_.js")).href).resolve("babel-plugin-react-compiler");
14494
14683
  } catch {
14495
14684
  return null;
14496
14685
  }
@@ -14543,6 +14732,11 @@ var linkDepsJunction = async (link, depsDir) => {
14543
14732
  if (!existing.isSymbolicLink()) {
14544
14733
  return;
14545
14734
  }
14735
+ try {
14736
+ if (resolve7(dirname9(link), await readlink(link)) === resolve7(depsDir)) {
14737
+ return;
14738
+ }
14739
+ } catch {}
14546
14740
  await rm2(link, { force: true });
14547
14741
  }
14548
14742
  await mkdir6(dirname9(link), { recursive: true });
@@ -14634,6 +14828,15 @@ var deploymentAdapterWarnings = (deployment, root) => {
14634
14828
  }
14635
14829
  return [];
14636
14830
  };
14831
+ var searchProviderWarnings = (provider, root, pkgDir = packageRoot()) => {
14832
+ const warnings = [];
14833
+ for (const dep of searchProviderMeta(provider).runtimeDeps) {
14834
+ if (!(canResolveFrom(root, dep) || canResolveFrom(pkgDir, dep))) {
14835
+ warnings.push(`Search provider "${provider}" needs "${dep}", which isn't installed. Run \`npm install ${dep}\` (or your package manager's equivalent).`);
14836
+ }
14837
+ }
14838
+ return warnings;
14839
+ };
14637
14840
  var examplesCssFile = (root, config) => config.examples.css ? join26(root, config.examples.css) : null;
14638
14841
  var writeExamplesPreview = async (options) => {
14639
14842
  const { config, hasExamples, root, srcDir, write } = options;
@@ -14673,6 +14876,16 @@ var detectUsesMath = async (root, staged = []) => {
14673
14876
  const contents = await Promise.all(files.map((file) => readOptional(join26(root, file))));
14674
14877
  return [...contents, ...staged].some(containsMath);
14675
14878
  };
14879
+ var hashConfigSource = (source) => createHash2("sha256").update(source).digest("hex");
14880
+ var loadIntegrationBridge = async (config, context) => {
14881
+ if (config.integrations.length === 0 || !context.configFile) {
14882
+ return;
14883
+ }
14884
+ return {
14885
+ configFile: relative13(context.outDir, context.configFile),
14886
+ sourceHash: hashConfigSource(await readOptional(context.configFile))
14887
+ };
14888
+ };
14676
14889
  var writeIfChanged = async (path, content) => {
14677
14890
  let existing = null;
14678
14891
  try {
@@ -14891,11 +15104,15 @@ var buildRuntimeData = (project) => {
14891
15104
  config: {
14892
15105
  analytics: config.analytics ?? null,
14893
15106
  appleIcon: resolveAppleIcon(project),
14894
- ask: config.ai.ask?.enabled ? { suggestions: config.ai.ask.suggestions } : null,
15107
+ ask: config.ai.ask?.enabled ? {
15108
+ endpoint: config.ai.ask.endpoint ?? null,
15109
+ suggestions: config.ai.ask.suggestions
15110
+ } : null,
14895
15111
  banner: resolveBanner(config),
14896
15112
  basePath: config.basePath,
14897
15113
  codeThemes: config.markdown.codeBlocks.theme,
14898
15114
  codeWrap: config.markdown.code.wrap,
15115
+ dateFormat: config.dateFormat,
14899
15116
  description: config.description,
14900
15117
  favicon: resolveFavicon(project),
14901
15118
  feedback: config.feedback,
@@ -15022,7 +15239,7 @@ var writeMcpFiles = async (project, plan, write) => {
15022
15239
  };
15023
15240
  var writeAskFiles = async (project, srcDir, write) => {
15024
15241
  const { ask } = project.config.ai;
15025
- if (!ask?.enabled) {
15242
+ if (!(ask?.enabled && !ask.endpoint)) {
15026
15243
  return;
15027
15244
  }
15028
15245
  const grounded = ask.provider !== "inkeep";
@@ -15038,6 +15255,7 @@ var writeNotFoundPage = async (write, srcDir, pages, contentPages) => {
15038
15255
  }
15039
15256
  await write(join26(srcDir, "pages", "404.astro"), notFoundPageTemplate());
15040
15257
  };
15258
+ var diagnosticWarning = (diagnostic) => diagnostic.suggestion ? `${diagnostic.message} ${diagnostic.suggestion}` : diagnostic.message;
15041
15259
  var buildComponentSlots = async (componentsFile) => {
15042
15260
  const analysis = componentsFile ? analyzeComponentOverrides(await readFile14(componentsFile, "utf-8"), componentsFile) : null;
15043
15261
  return {
@@ -15046,6 +15264,7 @@ var buildComponentSlots = async (componentsFile) => {
15046
15264
  warnings: analysis ? analysis.warnings : []
15047
15265
  };
15048
15266
  };
15267
+ var contentWatchesRuntimeDir = (hasFilesystemSource, collectionBase, context) => hasFilesystemSource && runtimeDirWithin(collectionBase, context.outDir) !== null;
15049
15268
  var generateRuntime = async (project) => {
15050
15269
  const { context, config } = project;
15051
15270
  const out = context.outDir;
@@ -15073,6 +15292,7 @@ var generateRuntime = async (project) => {
15073
15292
  usesMath,
15074
15293
  userTheme,
15075
15294
  userExamplesCss,
15295
+ integrationBridge,
15076
15296
  islandDiscovery,
15077
15297
  exampleDiscovery,
15078
15298
  componentSlots
@@ -15082,6 +15302,7 @@ var generateRuntime = async (project) => {
15082
15302
  detectUsesMath(context.root, staged.values()),
15083
15303
  readOptional(context.themeFile),
15084
15304
  readOptional(examplesCssFile(context.root, config)),
15305
+ loadIntegrationBridge(config, context),
15085
15306
  discoverIslands(context.root),
15086
15307
  discoverExamples(context.root, config.examples.source),
15087
15308
  buildComponentSlots(context.componentsFile)
@@ -15106,6 +15327,7 @@ var generateRuntime = async (project) => {
15106
15327
  pages.push(...mcp.discoveryPages);
15107
15328
  const hasStaged = staged.size > 0;
15108
15329
  const hasFilesystemSource = project.sources.some((source) => !source.staged);
15330
+ const docsCollection = resolveDocsCollection(config, context);
15109
15331
  const [structural] = await Promise.all([
15110
15332
  Promise.all([
15111
15333
  write(join26(out, "astro.config.mjs"), astroConfigTemplate({
@@ -15113,10 +15335,12 @@ var generateRuntime = async (project) => {
15113
15335
  askPath,
15114
15336
  config,
15115
15337
  contentRoutes: project.manifest.routes.map((route) => route.path),
15338
+ contentWatchesRuntimeDir: contentWatchesRuntimeDir(hasFilesystemSource, docsCollection.base, context),
15116
15339
  context,
15117
15340
  dataPath,
15118
15341
  examplesPath,
15119
15342
  examplesThemePath,
15343
+ integrationBridge,
15120
15344
  needsReact,
15121
15345
  needsSvelte,
15122
15346
  needsVue,
@@ -15130,7 +15354,7 @@ var generateRuntime = async (project) => {
15130
15354
  write(join26(out, "tsconfig.json"), runtimeTsconfigTemplate()),
15131
15355
  write(join26(srcDir, "env.d.ts"), envTemplate()),
15132
15356
  write(join26(srcDir, "content.config.ts"), contentConfigTemplate({
15133
- collection: resolveDocsCollection(config, context),
15357
+ collection: docsCollection,
15134
15358
  config,
15135
15359
  context,
15136
15360
  filesystem: hasFilesystemSource,
@@ -15234,18 +15458,12 @@ var generateRuntime = async (project) => {
15234
15458
  warnings.push(...[
15235
15459
  ...validateNavTargets(project.graph.navigation, navTargetRoutes),
15236
15460
  ...validateSearchPopularIcons(config.search.popular)
15237
- ].map((diagnostic) => diagnostic.suggestion ? `${diagnostic.message} ${diagnostic.suggestion}` : diagnostic.message));
15461
+ ].map(diagnosticWarning));
15238
15462
  const knownComponentTags = new Set([
15239
15463
  ...islandDiscovery.islands.map((island) => island.name),
15240
15464
  ...overrideTags
15241
15465
  ]);
15242
- warnings.push(...validateUsedComponents(project.graph.pages, knownComponentTags, new Set(registry.map((item) => item.name))).map((diagnostic) => diagnostic.suggestion ? `${diagnostic.message} ${diagnostic.suggestion}` : diagnostic.message));
15243
- for (const dep of searchProviderMeta(config.search.provider).runtimeDeps) {
15244
- if (!(canResolveFrom(context.root, dep) || canResolveFrom(packageRoot(), dep))) {
15245
- warnings.push(`Search provider "${config.search.provider}" needs "${dep}", which isn't installed. Run \`npm install ${dep}\` (or your package manager's equivalent).`);
15246
- }
15247
- }
15248
- warnings.push(...deploymentAdapterWarnings(config.deployment, context.root), ...islandFrameworkWarnings(frameworks, context.root));
15466
+ warnings.push(...validateUsedComponents(project.graph.pages, knownComponentTags, new Set(registry.map((item) => item.name))).map(diagnosticWarning), ...searchProviderWarnings(config.search.provider, context.root), ...deploymentAdapterWarnings(config.deployment, context.root), ...islandFrameworkWarnings(frameworks, context.root));
15249
15467
  if (hasScalarReferences(config)) {
15250
15468
  const references = await buildReferenceFiles({
15251
15469
  config,
@@ -15270,7 +15488,7 @@ var generateRuntime = async (project) => {
15270
15488
 
15271
15489
  // src/cli/env.ts
15272
15490
  import { existsSync as existsSync15, readFileSync as readFileSync10 } from "node:fs";
15273
- import { dirname as dirname10, join as join27, resolve as resolve7 } from "pathe";
15491
+ import { dirname as dirname10, join as join27, resolve as resolve8 } from "pathe";
15274
15492
  var ENV_LINE = /^\s*(?:export\s+)?(?<key>[A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?<value>.*?)\s*$/u;
15275
15493
  var DOUBLE_QUOTED2 = /^"(?<body>[\s\S]*)"$/u;
15276
15494
  var SINGLE_QUOTED = /^'(?<body>[\s\S]*)'$/u;
@@ -15322,7 +15540,7 @@ var loadFile = (path) => {
15322
15540
  } catch {}
15323
15541
  };
15324
15542
  var loadEnvFiles = (startDir) => {
15325
- let dir = resolve7(startDir);
15543
+ let dir = resolve8(startDir);
15326
15544
  let done = false;
15327
15545
  while (!done) {
15328
15546
  loadFile(join27(dir, ".env.local"));
@@ -15348,7 +15566,7 @@ var checkRequiredSecrets = (config) => {
15348
15566
  suggestion: `Set ${env} in .env.local for local dev, or in your host's environment for production.`
15349
15567
  });
15350
15568
  };
15351
- if (config.ai.ask?.enabled) {
15569
+ if (config.ai.ask?.enabled && !config.ai.ask.endpoint) {
15352
15570
  const backend = resolveAskBackend(config.ai.ask);
15353
15571
  if (backend.kind === "gateway") {
15354
15572
  requireSecret("Ask AI (AI Gateway)", "AI_GATEWAY_API_KEY", "on Vercel the gateway can also authenticate via OIDC");
@@ -15764,6 +15982,7 @@ var checkCommand = defineCommand4({
15764
15982
  import { watch } from "node:fs";
15765
15983
  import { dev } from "astro";
15766
15984
  import { defineCommand as defineCommand5 } from "citty";
15985
+ import { basename as basename4, dirname as dirname11 } from "pathe";
15767
15986
 
15768
15987
  // src/astro/integration.ts
15769
15988
  var overlayServer = null;
@@ -15942,17 +16161,26 @@ var devCommand = defineCommand5({
15942
16161
  if (boundPort !== port) {
15943
16162
  runRegenerate();
15944
16163
  }
16164
+ const dirTargets = [project.context.pagesRoot].filter((target) => target !== null);
15945
16165
  const fileTargets = [
15946
- project.context.pagesRoot,
15947
16166
  project.context.configFile,
15948
16167
  project.context.themeFile,
15949
16168
  project.context.componentsFile
15950
16169
  ].filter((target) => target !== null);
15951
16170
  const disposers = [
15952
16171
  ...project.sources.map((source) => source.watch?.(regenerate)),
15953
- ...fileTargets.map((target) => {
16172
+ ...dirTargets.map((target) => {
15954
16173
  const watcher = watch(target, { recursive: true }, regenerate);
15955
16174
  return () => watcher.close();
16175
+ }),
16176
+ ...fileTargets.map((target) => {
16177
+ const name = basename4(target);
16178
+ const watcher = watch(dirname11(target), (_event, filename) => {
16179
+ if (!filename || filename === name) {
16180
+ regenerate();
16181
+ }
16182
+ });
16183
+ return () => watcher.close();
15956
16184
  })
15957
16185
  ].filter((dispose) => dispose !== undefined);
15958
16186
  const shutdown = async () => {
@@ -16095,7 +16323,15 @@ var ejectOpenApiData = (project) => {
16095
16323
  };
16096
16324
  var askFiles = async (project, srcDir, genDir) => {
16097
16325
  const { ask } = project.config.ai;
16098
- if (!ask?.enabled) {
16326
+ if (!(ask?.enabled && !ask.endpoint)) {
16327
+ const endpointPath = join31(srcDir, "pages", "api", "ask.ts");
16328
+ if (existsSync18(endpointPath)) {
16329
+ const content = await readFile15(endpointPath, "utf-8");
16330
+ if (content.startsWith("// Generated by Blume. Do not edit.")) {
16331
+ await rm3(endpointPath, { force: true });
16332
+ }
16333
+ }
16334
+ await rm3(join31(genDir, "ask-data.json"), { force: true });
16099
16335
  return [];
16100
16336
  }
16101
16337
  const grounded = ask.provider !== "inkeep";
@@ -16178,6 +16414,7 @@ var examplesPreviewFiles = (srcDir, basePath, hasExamples) => hasExamples ? [
16178
16414
  path: join31(srcDir, "pages", ...basePath.split("/").filter(Boolean), "blume-examples", "[...path].astro")
16179
16415
  }
16180
16416
  ] : [];
16417
+ var ejectIntegrationBridge = (config, root, configFile) => config.integrations.length > 0 && configFile ? { configFile: toPosix(relative14(root, configFile)) } : undefined;
16181
16418
  var eject = async (root) => {
16182
16419
  const project = await scanProject(root, { mode: "build" });
16183
16420
  const { context, config } = project;
@@ -16239,6 +16476,7 @@ var eject = async (root) => {
16239
16476
  dataPath: "./src/generated/data.json",
16240
16477
  examplesPath: "./src/generated/examples.ts",
16241
16478
  examplesThemePath: "./src/generated/examples.css",
16479
+ integrationBridge: ejectIntegrationBridge(config, root, context.configFile),
16242
16480
  needsReact,
16243
16481
  needsSvelte,
16244
16482
  needsVue,
@@ -16473,7 +16711,7 @@ var updatePackageScripts = async (root) => {
16473
16711
  // src/cli/init/scaffold.ts
16474
16712
  import { existsSync as existsSync19 } from "node:fs";
16475
16713
  import { mkdir as mkdir8, writeFile as writeFile11 } from "node:fs/promises";
16476
- import { basename as basename4, dirname as dirname11, isAbsolute as isAbsolute9, join as join33, relative as relative15 } from "pathe";
16714
+ import { basename as basename5, dirname as dirname12, isAbsolute as isAbsolute9, join as join33, relative as relative15 } from "pathe";
16477
16715
 
16478
16716
  // src/core/package-json.ts
16479
16717
  var toPackageName = (raw) => raw.toLowerCase().replaceAll(/[^a-z0-9._-]+/gu, "-").replaceAll(/^[-_.]+|[-_.]+$/gu, "") || "docs";
@@ -16683,7 +16921,7 @@ var extraDepsFor = (sources) => ({
16683
16921
  var buildPlan = (root, answers) => {
16684
16922
  const files = [
16685
16923
  {
16686
- content: blumePackageJson(toPackageName(basename4(root)), extraDepsFor(answers.sources)),
16924
+ content: blumePackageJson(toPackageName(basename5(root)), extraDepsFor(answers.sources)),
16687
16925
  path: join33(root, "package.json")
16688
16926
  },
16689
16927
  { content: buildConfig(answers), path: join33(root, "blume.config.ts") }
@@ -16698,14 +16936,14 @@ var writeFileSafe = async (file, log) => {
16698
16936
  log.info(`Skipped existing ${file.path}`);
16699
16937
  return false;
16700
16938
  }
16701
- await mkdir8(dirname11(file.path), { recursive: true });
16939
+ await mkdir8(dirname12(file.path), { recursive: true });
16702
16940
  await writeFile11(file.path, file.content, "utf-8");
16703
16941
  log.success(`Created ${file.path}`);
16704
16942
  return true;
16705
16943
  };
16706
16944
  var applyPlan = async (files, log) => {
16707
16945
  const created = await Promise.all(files.map((file) => writeFileSafe(file, log)));
16708
- const createdPackage = files.some((file, index) => created[index] && basename4(file.path) === "package.json");
16946
+ const createdPackage = files.some((file, index) => created[index] && basename5(file.path) === "package.json");
16709
16947
  return { createdPackage };
16710
16948
  };
16711
16949
  var envVarsFor = (sources) => [
@@ -16789,13 +17027,882 @@ The blume package remains importable.`);
16789
17027
  }
16790
17028
  });
16791
17029
 
17030
+ // src/cli/commands/eval.ts
17031
+ import { existsSync as existsSync20 } from "node:fs";
17032
+ import { defineCommand as defineCommand8 } from "citty";
17033
+ import { join as join37 } from "pathe";
17034
+
17035
+ // src/eval/prompts.ts
17036
+ var readerPrompt = (question) => `You are evaluating whether a product's documentation can answer a user's question.
17037
+
17038
+ Answer the question below using ONLY the connected documentation tools (search_docs, get_page, list_pages, get_navigation). Rules:
17039
+ - Do not use prior knowledge about the product. Do not guess.
17040
+ - Do not read files, run commands, or access the network.
17041
+ - Search first, then read the most relevant pages with get_page.
17042
+ - If the documentation does not contain the answer, say exactly what information is missing instead of inventing one.
17043
+
17044
+ Question: ${question.question}
17045
+
17046
+ Reply with a concise answer containing the specific facts the documentation provides. Plain text only.`;
17047
+ var judgePrompt = (question, answer) => {
17048
+ const facts = question.expected.map((fact) => `- ${fact}`).join(`
17049
+ `);
17050
+ return `You are grading an answer against expected facts. Do not use any tools.
17051
+
17052
+ Question: ${question.question}
17053
+
17054
+ Expected facts — each must be present in substance (paraphrase is fine, contradiction is not):
17055
+ ${facts}
17056
+
17057
+ Answer to grade:
17058
+ """
17059
+ ${answer}
17060
+ """
17061
+
17062
+ An answer that states the documentation lacks the information FAILS.
17063
+
17064
+ Reply with ONLY this JSON object on a single line, no markdown fences:
17065
+ {"pass": true|false, "score": 0.0-1.0, "missing": ["expected facts absent or contradicted"], "notes": "one sentence"}`;
17066
+ };
17067
+ var evalFixPrompt = (reportPath) => `Fix the documentation gaps found by \`blume eval\` in this project.
17068
+
17069
+ The full report is at ${reportPath}. It is JSON: each entry in \`eval.results\` with status "fail" is one question the documentation could not answer. Each carries the \`question\`, the \`expected\` facts, the judge's \`missing\` facts, and the reader agent's \`answer\` (what the docs currently convey). The matching \`diagnostics\` entry names the source \`file\` of the page that should answer it.
17070
+
17071
+ Work through every failed question:
17072
+ 1. Read the page named in the finding (or choose the best page when none is named).
17073
+ 2. Edit the documentation so it states the missing facts explicitly. Add prose, not filler; keep the page's voice.
17074
+ 3. Never delete questions from the evals file or weaken expected facts.
17075
+
17076
+ When you are done, run \`blume eval\` to verify, and repeat until every question passes.`;
17077
+ var initPrompt = (evalsPath) => `Draft a starter evals file for \`blume eval\` in this documentation project.
17078
+
17079
+ Read the documentation source pages in this project and write ${evalsPath} with about 10 high-value questions a real user would ask — installation, configuration, deployment, and the project's headline features. For each question, list the expected facts a correct answer must state, grounded in what the documentation actually promises (never invent facts the docs don't state).
17080
+
17081
+ The file format is YAML:
17082
+
17083
+ questions:
17084
+ - id: kebab-case-slug
17085
+ question: One user question?
17086
+ expected:
17087
+ - a fact the answer must contain
17088
+ - another required fact
17089
+ routes:
17090
+ - /route/of/the/page/that/answers/it
17091
+
17092
+ Rules:
17093
+ - Every \`expected\` fact must be verifiable in the docs today.
17094
+ - Prefer questions whose answers live on one page; set \`routes\` to that page.
17095
+ - Keep ids unique and questions short.
17096
+
17097
+ When you are done, print the file and suggest running \`blume eval\` to try it.`;
17098
+
17099
+ // src/eval/report.ts
17100
+ import { mkdtemp as mkdtemp2, writeFile as writeFile12 } from "node:fs/promises";
17101
+ import { tmpdir as tmpdir2 } from "node:os";
17102
+ import { join as join34, relative as relative17 } from "pathe";
17103
+ var ESC4 = String.fromCodePoint(27);
17104
+ var COLORS3 = {
17105
+ bold: `${ESC4}[1m`,
17106
+ cyan: `${ESC4}[36m`,
17107
+ dim: `${ESC4}[2m`,
17108
+ green: `${ESC4}[32m`,
17109
+ red: `${ESC4}[31m`,
17110
+ reset: `${ESC4}[0m`,
17111
+ yellow: `${ESC4}[33m`
17112
+ };
17113
+ var GLYPH2 = {
17114
+ error: "!",
17115
+ fail: "✖",
17116
+ pass: "✔",
17117
+ skip: "⊘"
17118
+ };
17119
+ var STATUS_COLOR = {
17120
+ error: COLORS3.yellow,
17121
+ fail: COLORS3.red,
17122
+ pass: COLORS3.green,
17123
+ skip: COLORS3.dim
17124
+ };
17125
+ var ID_PAD = 28;
17126
+ var seconds = (ms) => `${(ms / 1000).toFixed(1)}s`;
17127
+ var money = (cost) => cost === undefined ? "" : `$${cost.toFixed(2)}`;
17128
+ var duration = (ms) => {
17129
+ if (ms < 60000) {
17130
+ return seconds(ms);
17131
+ }
17132
+ const minutes = Math.floor(ms / 60000);
17133
+ const rest = Math.round(ms % 60000 / 1000);
17134
+ return `${minutes}m ${rest}s`;
17135
+ };
17136
+ var questionLine = (result) => {
17137
+ const color = STATUS_COLOR[result.status];
17138
+ const glyph = `${color}${GLYPH2[result.status]}${COLORS3.reset}`;
17139
+ const id2 = result.id.padEnd(ID_PAD);
17140
+ if (result.status === "skip") {
17141
+ return ` ${glyph} ${id2} ${COLORS3.dim}skipped${COLORS3.reset}`;
17142
+ }
17143
+ const score = result.score === undefined ? "" : result.score.toFixed(2);
17144
+ const cells = [
17145
+ `${color}${result.status}${COLORS3.reset}`,
17146
+ score,
17147
+ `${COLORS3.dim}${seconds(result.durationMs)}${COLORS3.reset}`,
17148
+ `${COLORS3.dim}${money(result.costUsd)}${COLORS3.reset}`
17149
+ ].filter((cell) => cell !== "").join(" ");
17150
+ return ` ${glyph} ${id2} ${cells}`;
17151
+ };
17152
+ var questionDetails = (result, verbose) => {
17153
+ const lines = [];
17154
+ if (result.status === "fail") {
17155
+ for (const fact of result.missing) {
17156
+ lines.push(` ${COLORS3.dim}missing: ${fact}${COLORS3.reset}`);
17157
+ }
17158
+ }
17159
+ if (result.status === "error" && result.detail) {
17160
+ lines.push(` ${COLORS3.dim}${result.detail}${COLORS3.reset}`);
17161
+ }
17162
+ if (verbose && result.answer && result.status !== "pass") {
17163
+ lines.push(...result.answer.split(`
17164
+ `).map((line) => ` ${COLORS3.dim}> ${line}${COLORS3.reset}`));
17165
+ }
17166
+ return lines;
17167
+ };
17168
+ var summaryLine2 = (result) => {
17169
+ const { counts } = result;
17170
+ const parts = [
17171
+ `${counts.pass} passed`,
17172
+ counts.fail > 0 ? `${counts.fail} failed` : "",
17173
+ counts.error > 0 ? `${counts.error} errored` : "",
17174
+ counts.skip > 0 ? `${counts.skip} skipped` : "",
17175
+ duration(result.durationMs),
17176
+ money(result.costUsd)
17177
+ ].filter((part) => part !== "");
17178
+ return parts.join(" · ");
17179
+ };
17180
+ var headerLine = (total, agent) => `${COLORS3.bold}blume eval${COLORS3.reset} ${total} question(s) · ${AGENTS[agent].name}`;
17181
+ var startLine = (id2, index, total) => ` ${COLORS3.dim}▸ ${id2} (${index + 1}/${total})${COLORS3.reset}`;
17182
+ var fixLines = (result, root) => result.diagnostics.filter((diagnostic) => diagnostic.code !== "BLUME_EVAL_ROUTE_UNKNOWN").map((finding2) => {
17183
+ const site = finding2.file ? `${relative17(root, finding2.file)}${finding2.line ? `:${finding2.line}` : ""}` : "";
17184
+ return ` ${COLORS3.cyan}fix:${COLORS3.reset} ${site} ${COLORS3.dim}${finding2.message}${COLORS3.reset}`;
17185
+ });
17186
+ var warningLines = (result, root) => result.diagnostics.filter((diagnostic) => diagnostic.code === "BLUME_EVAL_ROUTE_UNKNOWN").map((finding2) => {
17187
+ const site = finding2.file ? ` ${relative17(root, finding2.file)}${finding2.line ? `:${finding2.line}` : ""}` : "";
17188
+ return ` ${COLORS3.yellow}⚠${COLORS3.reset}${site} ${COLORS3.dim}${finding2.message}${COLORS3.reset}`;
17189
+ });
17190
+ var evalReportJson = (result, root, threshold) => {
17191
+ const diagnostics = result.diagnostics.map((diagnostic) => diagnostic.file ? { ...diagnostic, file: relative17(root, diagnostic.file) } : diagnostic);
17192
+ return `${JSON.stringify({
17193
+ diagnostics,
17194
+ eval: {
17195
+ agent: result.agent,
17196
+ costUsd: result.costUsd,
17197
+ counts: result.counts,
17198
+ durationMs: result.durationMs,
17199
+ results: result.results,
17200
+ threshold
17201
+ },
17202
+ summary: countBySeverity(result.diagnostics)
17203
+ }, null, 2)}
17204
+ `;
17205
+ };
17206
+ var writeEvalReport = async (result, root, threshold) => {
17207
+ const dir = await mkdtemp2(join34(tmpdir2(), "blume-eval-"));
17208
+ const path = join34(dir, "report.json");
17209
+ await writeFile12(path, evalReportJson(result, root, threshold));
17210
+ return path;
17211
+ };
17212
+
17213
+ // src/eval/run.ts
17214
+ import { mkdir as mkdir9, mkdtemp as mkdtemp3, writeFile as writeFile14 } from "node:fs/promises";
17215
+ import { tmpdir as tmpdir3 } from "node:os";
17216
+ import { join as join36 } from "pathe";
17217
+
17218
+ // src/eval/agents.ts
17219
+ import { spawn as spawn2 } from "node:child_process";
17220
+ import { readFile as readFile17, writeFile as writeFile13 } from "node:fs/promises";
17221
+ import { join as join35 } from "pathe";
17222
+ import { z as z3 } from "zod";
17223
+ var KILL_GRACE_MS = 5000;
17224
+ var MCP_TOOL_NAMES = [
17225
+ "search_docs",
17226
+ "get_page",
17227
+ "list_pages",
17228
+ "get_navigation"
17229
+ ];
17230
+ var MCP_SERVER_NAME = "docs";
17231
+ var DISALLOWED_TOOLS = [
17232
+ "Bash",
17233
+ "Read",
17234
+ "Glob",
17235
+ "Grep",
17236
+ "Write",
17237
+ "Edit",
17238
+ "NotebookEdit",
17239
+ "WebFetch",
17240
+ "WebSearch",
17241
+ "Task"
17242
+ ];
17243
+ var runAgentHeadless = (bin, args, options) => new Promise((resolve9, reject) => {
17244
+ const platform = options.platform ?? process.platform;
17245
+ const child = spawn2(bin, args, {
17246
+ cwd: options.cwd,
17247
+ shell: platform === "win32",
17248
+ stdio: ["pipe", "pipe", "pipe"]
17249
+ });
17250
+ let stdout = "";
17251
+ let stderr = "";
17252
+ let timedOut = false;
17253
+ child.stdout.on("data", (chunk) => {
17254
+ stdout += chunk.toString("utf-8");
17255
+ });
17256
+ child.stderr.on("data", (chunk) => {
17257
+ stderr += chunk.toString("utf-8");
17258
+ });
17259
+ const deadline = setTimeout(() => {
17260
+ timedOut = true;
17261
+ child.kill("SIGTERM");
17262
+ const hardKill = setTimeout(() => child.kill("SIGKILL"), KILL_GRACE_MS);
17263
+ hardKill.unref();
17264
+ }, options.timeoutMs);
17265
+ deadline.unref();
17266
+ child.once("error", (error) => {
17267
+ clearTimeout(deadline);
17268
+ reject(error);
17269
+ });
17270
+ child.once("close", (code) => {
17271
+ clearTimeout(deadline);
17272
+ resolve9({ code: code ?? 1, stderr, stdout, timedOut });
17273
+ });
17274
+ child.once("exit", (code) => {
17275
+ if (timedOut) {
17276
+ clearTimeout(deadline);
17277
+ resolve9({ code: code ?? 1, stderr, stdout, timedOut });
17278
+ }
17279
+ });
17280
+ child.stdin.end(options.prompt);
17281
+ });
17282
+ var writeMcpConfig = async (dir, snapshotPath, launcher) => {
17283
+ const resolved = launcher ?? {
17284
+ args: [process.argv[1] ?? "", "mcp-stdio", "--data", snapshotPath],
17285
+ command: process.execPath
17286
+ };
17287
+ const configPath = join35(dir, "mcp-config.json");
17288
+ const config = {
17289
+ mcpServers: {
17290
+ [MCP_SERVER_NAME]: { args: resolved.args, command: resolved.command }
17291
+ }
17292
+ };
17293
+ await writeFile13(configPath, JSON.stringify(config, null, 2));
17294
+ return {
17295
+ configPath,
17296
+ serverArgs: resolved.args,
17297
+ serverCommand: resolved.command
17298
+ };
17299
+ };
17300
+ var CLAUDE_READER_MAX_TURNS = "25";
17301
+ var CLAUDE_JUDGE_MAX_TURNS = "1";
17302
+ var claudeArgs = (context) => {
17303
+ const base = ["-p", "--output-format", "json", "--strict-mcp-config"];
17304
+ if (context.mcp) {
17305
+ const allowed = MCP_TOOL_NAMES.map((tool) => `mcp__${MCP_SERVER_NAME}__${tool}`).join(",");
17306
+ return [
17307
+ ...base,
17308
+ "--mcp-config",
17309
+ context.mcp.configPath,
17310
+ "--allowedTools",
17311
+ allowed,
17312
+ "--disallowedTools",
17313
+ DISALLOWED_TOOLS.join(","),
17314
+ "--max-turns",
17315
+ CLAUDE_READER_MAX_TURNS
17316
+ ];
17317
+ }
17318
+ return [
17319
+ ...base,
17320
+ "--disallowedTools",
17321
+ DISALLOWED_TOOLS.join(","),
17322
+ "--max-turns",
17323
+ CLAUDE_JUDGE_MAX_TURNS
17324
+ ];
17325
+ };
17326
+ var codexArgs = (context) => {
17327
+ const base = [
17328
+ "exec",
17329
+ "--skip-git-repo-check",
17330
+ "--ignore-user-config",
17331
+ "--ephemeral",
17332
+ "--sandbox",
17333
+ "read-only",
17334
+ "--output-last-message",
17335
+ context.lastMessagePath
17336
+ ];
17337
+ if (context.mcp) {
17338
+ return [
17339
+ ...base,
17340
+ "-c",
17341
+ `mcp_servers.${MCP_SERVER_NAME}.command=${JSON.stringify(context.mcp.serverCommand)}`,
17342
+ "-c",
17343
+ `mcp_servers.${MCP_SERVER_NAME}.args=${JSON.stringify(context.mcp.serverArgs)}`,
17344
+ "-"
17345
+ ];
17346
+ }
17347
+ return [...base, "-"];
17348
+ };
17349
+ var agentArgs = (kind, context) => kind === "claude" ? claudeArgs(context) : codexArgs(context);
17350
+ var claudeResultSchema = z3.object({
17351
+ is_error: z3.boolean().default(false),
17352
+ result: z3.string().default(""),
17353
+ total_cost_usd: z3.number().optional()
17354
+ });
17355
+ var tail = (value, max = 300) => {
17356
+ const trimmed = value.trim();
17357
+ return trimmed.length > max ? trimmed.slice(-max) : trimmed;
17358
+ };
17359
+ var readAgentOutput = async (kind, result, lastMessagePath) => {
17360
+ if (result.timedOut) {
17361
+ return { detail: "timed out", isError: true, text: "" };
17362
+ }
17363
+ if (result.code !== 0) {
17364
+ return {
17365
+ detail: tail(result.stderr) || `exited with code ${result.code}`,
17366
+ isError: true,
17367
+ text: ""
17368
+ };
17369
+ }
17370
+ if (kind === "claude") {
17371
+ let parsed;
17372
+ try {
17373
+ parsed = JSON.parse(result.stdout);
17374
+ } catch {
17375
+ return {
17376
+ detail: "unparseable --output-format json payload",
17377
+ isError: true,
17378
+ text: ""
17379
+ };
17380
+ }
17381
+ const payload = claudeResultSchema.safeParse(parsed);
17382
+ if (!payload.success) {
17383
+ return {
17384
+ detail: "unexpected --output-format json shape",
17385
+ isError: true,
17386
+ text: ""
17387
+ };
17388
+ }
17389
+ return {
17390
+ costUsd: payload.data.total_cost_usd,
17391
+ isError: payload.data.is_error,
17392
+ text: payload.data.result
17393
+ };
17394
+ }
17395
+ let text;
17396
+ try {
17397
+ const raw = await readFile17(lastMessagePath, "utf-8");
17398
+ text = raw.trim();
17399
+ } catch {
17400
+ return { detail: "no last message written", isError: true, text: "" };
17401
+ }
17402
+ if (text === "") {
17403
+ return { detail: "empty last message", isError: true, text: "" };
17404
+ }
17405
+ return { isError: false, text };
17406
+ };
17407
+ var verdictSchema = z3.object({
17408
+ missing: z3.array(z3.string()).default([]),
17409
+ notes: z3.string().default(""),
17410
+ pass: z3.boolean(),
17411
+ score: z3.number().min(0).max(1).optional()
17412
+ });
17413
+ var parseVerdict = (text) => {
17414
+ const start = text.indexOf("{");
17415
+ const end = text.lastIndexOf("}");
17416
+ if (start === -1 || end <= start) {
17417
+ return;
17418
+ }
17419
+ let parsed;
17420
+ try {
17421
+ parsed = JSON.parse(text.slice(start, end + 1));
17422
+ } catch {
17423
+ return;
17424
+ }
17425
+ const result = verdictSchema.safeParse(parsed);
17426
+ return result.success ? result.data : undefined;
17427
+ };
17428
+
17429
+ // src/eval/schema.ts
17430
+ import { readFile as readFile18 } from "node:fs/promises";
17431
+ import { load as load2 } from "js-yaml";
17432
+ import { z as z4 } from "zod";
17433
+ var ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/u;
17434
+ var questionSchema = z4.strictObject({
17435
+ expected: z4.array(z4.string().min(1)).min(1, "expected must list at least one fact"),
17436
+ id: z4.string().regex(ID_PATTERN, "id must be a kebab-case slug (a-z, 0-9, dashes)"),
17437
+ question: z4.string().min(1),
17438
+ routes: z4.union([z4.string(), z4.array(z4.string())]).default([]).transform((value) => typeof value === "string" ? [value] : value),
17439
+ severity: z4.enum(["error", "warning"]).default("error"),
17440
+ skip: z4.boolean().default(false)
17441
+ });
17442
+ var fullSchema = z4.strictObject({
17443
+ questions: z4.array(questionSchema).min(1),
17444
+ version: z4.literal(1).default(1)
17445
+ });
17446
+ var evalsFileSchema = fullSchema.superRefine((value, context) => {
17447
+ const seen = new Set;
17448
+ for (const question of value.questions) {
17449
+ if (seen.has(question.id)) {
17450
+ context.addIssue({
17451
+ code: z4.ZodIssueCode.custom,
17452
+ message: `duplicate question id "${question.id}"`,
17453
+ path: ["questions"]
17454
+ });
17455
+ }
17456
+ seen.add(question.id);
17457
+ }
17458
+ });
17459
+
17460
+ class EvalsFileError extends Error {
17461
+ path;
17462
+ constructor(path, message) {
17463
+ super(message);
17464
+ this.name = "EvalsFileError";
17465
+ this.path = path;
17466
+ }
17467
+ }
17468
+ var describeIssues = (error) => error.issues.map((issue) => {
17469
+ const at = issue.path.length > 0 ? ` at ${issue.path.join(".")}` : "";
17470
+ return `${issue.message}${at}`;
17471
+ }).join("; ");
17472
+ var loadEvalsFile = async (path) => {
17473
+ let raw;
17474
+ try {
17475
+ raw = await readFile18(path, "utf-8");
17476
+ } catch {
17477
+ throw new EvalsFileError(path, `No evals file found at ${path}. Run \`blume eval init\` to draft one.`);
17478
+ }
17479
+ let parsed;
17480
+ try {
17481
+ parsed = load2(raw);
17482
+ } catch (error) {
17483
+ const detail = error instanceof Error ? error.message : String(error);
17484
+ throw new EvalsFileError(path, `Invalid YAML in ${path}: ${detail}`);
17485
+ }
17486
+ const candidate = Array.isArray(parsed) ? { questions: parsed } : parsed;
17487
+ const result = evalsFileSchema.safeParse(candidate);
17488
+ if (!result.success) {
17489
+ throw new EvalsFileError(path, `Invalid evals file at ${path}: ${describeIssues(result.error)}`);
17490
+ }
17491
+ return { evals: result.data, raw };
17492
+ };
17493
+ var locateQuestion = (raw, id2) => {
17494
+ const pattern = new RegExp(`^\\s*-?\\s*id:\\s*["']?${id2}["']?\\s*$`, "u");
17495
+ const lines = raw.split(`
17496
+ `);
17497
+ for (const [index, line] of lines.entries()) {
17498
+ if (pattern.test(line)) {
17499
+ return index + 1;
17500
+ }
17501
+ }
17502
+ return;
17503
+ };
17504
+
17505
+ // src/eval/findings.ts
17506
+ var DOCS_URL = "https://useblume.dev/docs/reference/eval";
17507
+ var hintedRoute = (question, project) => {
17508
+ for (const hint of question.routes) {
17509
+ const route = project.manifest.routes.find((candidate) => candidate.path === hint);
17510
+ if (route) {
17511
+ return route;
17512
+ }
17513
+ }
17514
+ };
17515
+ var routeFindings = (question, project, anchor) => {
17516
+ const known = new Set(project.manifest.routes.map((route) => route.path));
17517
+ return question.routes.filter((hint) => !known.has(hint)).map((hint) => ({
17518
+ code: "BLUME_EVAL_ROUTE_UNKNOWN",
17519
+ docsUrl: DOCS_URL,
17520
+ file: anchor.path,
17521
+ line: locateQuestion(anchor.raw, question.id),
17522
+ message: `Question "${question.id}" hints at route "${hint}", which matches no page.`,
17523
+ severity: "warning",
17524
+ suggestion: "Update the question's `routes` to the page's current route, or remove the hint."
17525
+ }));
17526
+ };
17527
+ var questionFinding = (question, outcome, project, anchor) => {
17528
+ const route = hintedRoute(question, project);
17529
+ const site = route ? { file: route.sourcePath, url: route.path } : { file: anchor.path, line: locateQuestion(anchor.raw, question.id) };
17530
+ if (outcome.status === "error") {
17531
+ return {
17532
+ code: "BLUME_EVAL_QUESTION_ERROR",
17533
+ docsUrl: DOCS_URL,
17534
+ message: `Eval run failed for "${question.question}"${outcome.detail ? ` — ${outcome.detail}` : ""}.`,
17535
+ severity: question.severity,
17536
+ suggestion: "Rerun `blume eval`; if it persists, check the agent CLI installation and the failure detail.",
17537
+ ...site
17538
+ };
17539
+ }
17540
+ const missing = outcome.missing.length > 0 ? ` — missing: ${outcome.missing.join("; ")}` : "";
17541
+ return {
17542
+ code: "BLUME_EVAL_QUESTION_FAILED",
17543
+ docsUrl: DOCS_URL,
17544
+ message: `Docs could not answer: "${question.question}"${missing}`,
17545
+ severity: question.severity,
17546
+ suggestion: "State the missing facts on this page, then rerun `blume eval`.",
17547
+ ...site
17548
+ };
17549
+ };
17550
+
17551
+ // src/eval/run.ts
17552
+ var DEFAULT_READER_TIMEOUT_MS = 180000;
17553
+ var DEFAULT_JUDGE_TIMEOUT_MS = 60000;
17554
+ var errored = (question, detail, durationMs) => ({
17555
+ detail,
17556
+ durationMs,
17557
+ expected: question.expected,
17558
+ id: question.id,
17559
+ missing: [],
17560
+ question: question.question,
17561
+ routes: question.routes,
17562
+ status: "error"
17563
+ });
17564
+ var runQuestion = async (question, index, context) => {
17565
+ const started = performance.now();
17566
+ const elapsed = () => Math.round(performance.now() - started);
17567
+ const workDir = join36(context.dir, `work-${index}`);
17568
+ await mkdir9(workDir, { recursive: true });
17569
+ const answerPath = join36(workDir, "answer.txt");
17570
+ const reader = await context.run(context.bin, agentArgs(context.kind, { lastMessagePath: answerPath, mcp: context.mcp }), {
17571
+ cwd: workDir,
17572
+ prompt: readerPrompt(question),
17573
+ timeoutMs: context.readerTimeoutMs
17574
+ });
17575
+ const answer = await readAgentOutput(context.kind, reader, answerPath);
17576
+ if (answer.isError) {
17577
+ return {
17578
+ ...errored(question, `reader ${answer.detail ?? "failed"}`, elapsed()),
17579
+ costUsd: answer.costUsd
17580
+ };
17581
+ }
17582
+ const verdictPath = join36(workDir, "verdict.txt");
17583
+ const judge = await context.run(context.bin, agentArgs(context.kind, { lastMessagePath: verdictPath }), {
17584
+ cwd: workDir,
17585
+ prompt: judgePrompt(question, answer.text),
17586
+ timeoutMs: context.judgeTimeoutMs
17587
+ });
17588
+ const graded = await readAgentOutput(context.kind, judge, verdictPath);
17589
+ const costUsd = answer.costUsd === undefined && graded.costUsd === undefined ? undefined : (answer.costUsd ?? 0) + (graded.costUsd ?? 0);
17590
+ if (graded.isError) {
17591
+ return {
17592
+ ...errored(question, `judge ${graded.detail ?? "failed"}`, elapsed()),
17593
+ answer: answer.text,
17594
+ costUsd
17595
+ };
17596
+ }
17597
+ const verdict = parseVerdict(graded.text);
17598
+ if (!verdict) {
17599
+ return {
17600
+ ...errored(question, "judge returned no parseable verdict", elapsed()),
17601
+ answer: answer.text,
17602
+ costUsd
17603
+ };
17604
+ }
17605
+ return {
17606
+ answer: answer.text,
17607
+ costUsd,
17608
+ durationMs: elapsed(),
17609
+ expected: question.expected,
17610
+ id: question.id,
17611
+ missing: verdict.missing,
17612
+ notes: verdict.notes || undefined,
17613
+ question: question.question,
17614
+ routes: question.routes,
17615
+ score: verdict.score,
17616
+ status: verdict.pass ? "pass" : "fail"
17617
+ };
17618
+ };
17619
+ var runEval = async (options) => {
17620
+ const started = performance.now();
17621
+ const kind = options.agent;
17622
+ const run = options.run ?? runAgentHeadless;
17623
+ const anchor = { path: options.evalsPath, raw: options.rawEvals };
17624
+ const dir = await mkdtemp3(join36(tmpdir3(), "blume-eval-"));
17625
+ const snapshotPath = join36(dir, "mcp-data.json");
17626
+ await writeFile14(snapshotPath, JSON.stringify(await buildMcpData(options.project)));
17627
+ const mcp = await writeMcpConfig(dir, snapshotPath);
17628
+ const context = {
17629
+ bin: AGENTS[kind].bin,
17630
+ dir,
17631
+ judgeTimeoutMs: options.judgeTimeoutMs ?? DEFAULT_JUDGE_TIMEOUT_MS,
17632
+ kind,
17633
+ mcp,
17634
+ readerTimeoutMs: options.readerTimeoutMs ?? DEFAULT_READER_TIMEOUT_MS,
17635
+ run
17636
+ };
17637
+ const diagnostics = [];
17638
+ const results = [];
17639
+ const { questions } = options.evals;
17640
+ for (const [index, question] of questions.entries()) {
17641
+ diagnostics.push(...routeFindings(question, options.project, anchor));
17642
+ if (question.skip) {
17643
+ results.push({
17644
+ durationMs: 0,
17645
+ expected: question.expected,
17646
+ id: question.id,
17647
+ missing: [],
17648
+ question: question.question,
17649
+ routes: question.routes,
17650
+ status: "skip"
17651
+ });
17652
+ continue;
17653
+ }
17654
+ options.onProgress?.({
17655
+ id: question.id,
17656
+ index,
17657
+ kind: "question-start",
17658
+ total: questions.length
17659
+ });
17660
+ const result = await runQuestion(question, index, context);
17661
+ results.push(result);
17662
+ if (result.status === "fail" || result.status === "error") {
17663
+ diagnostics.push(questionFinding(question, {
17664
+ detail: result.detail,
17665
+ missing: result.missing,
17666
+ status: result.status
17667
+ }, options.project, anchor));
17668
+ }
17669
+ options.onProgress?.({
17670
+ index,
17671
+ kind: "question-end",
17672
+ result,
17673
+ total: questions.length
17674
+ });
17675
+ }
17676
+ const counts = {
17677
+ error: 0,
17678
+ fail: 0,
17679
+ pass: 0,
17680
+ skip: 0
17681
+ };
17682
+ for (const result of results) {
17683
+ counts[result.status] += 1;
17684
+ }
17685
+ const costs = results.flatMap((result) => result.costUsd === undefined ? [] : [result.costUsd]);
17686
+ return {
17687
+ agent: kind,
17688
+ costUsd: costs.length > 0 ? costs.reduce((total, cost) => total + cost, 0) : undefined,
17689
+ counts,
17690
+ diagnostics,
17691
+ durationMs: Math.round(performance.now() - started),
17692
+ results
17693
+ };
17694
+ };
17695
+
17696
+ // src/cli/commands/eval.ts
17697
+ var DEFAULT_FILE = "evals.yaml";
17698
+ var DEFAULT_TIMEOUT_S = 180;
17699
+ var isAgentKind = (value) => (value in AGENTS);
17700
+ var launchAgentCode2 = async (bin, prompt) => {
17701
+ try {
17702
+ return await launchAgent(bin, prompt);
17703
+ } catch (error) {
17704
+ if (error?.code !== "ENOENT") {
17705
+ throw error;
17706
+ }
17707
+ return WINDOWS_COMMAND_NOT_FOUND;
17708
+ }
17709
+ };
17710
+ var notInstalled = (agent) => {
17711
+ const cli = AGENTS[agent];
17712
+ logger.error(`${cli.name} (\`${cli.bin}\`) was not found on PATH. Install it with \`${cli.install}\`.`);
17713
+ return process.exit(1);
17714
+ };
17715
+ var passFraction = (result) => {
17716
+ const ran = result.results.length - result.counts.skip;
17717
+ return ran === 0 ? 1 : result.counts.pass / ran;
17718
+ };
17719
+ var parseFlags = (args) => {
17720
+ if (!isAgentKind(args.agent)) {
17721
+ logger.error(`Invalid --agent "${args.agent}" (use claude | codex).`);
17722
+ process.exit(1);
17723
+ }
17724
+ if (args.action !== undefined && args.action !== "init") {
17725
+ logger.error(`Unknown action "${args.action}" (did you mean "init"?).`);
17726
+ process.exit(1);
17727
+ }
17728
+ if (args.json && args.fix) {
17729
+ logger.error("--json and --fix are mutually exclusive.");
17730
+ process.exit(1);
17731
+ }
17732
+ const threshold = args.threshold === undefined ? 1 : Number(args.threshold);
17733
+ if (!Number.isFinite(threshold) || threshold < 0 || threshold > 1) {
17734
+ logger.error(`Invalid --threshold "${args.threshold}" (use 0..1).`);
17735
+ process.exit(1);
17736
+ }
17737
+ const timeoutS = args.timeout === undefined ? DEFAULT_TIMEOUT_S : Number(args.timeout);
17738
+ if (!Number.isInteger(timeoutS) || timeoutS <= 0) {
17739
+ logger.error(`Invalid --timeout "${args.timeout}" (whole seconds).`);
17740
+ process.exit(1);
17741
+ }
17742
+ return { agent: args.agent, threshold, timeoutS };
17743
+ };
17744
+ var runFixHandoff = async (agent, result, root, threshold) => {
17745
+ const count = result.counts.fail + result.counts.error;
17746
+ if (count === 0) {
17747
+ return;
17748
+ }
17749
+ const cli = AGENTS[agent];
17750
+ const report = await writeEvalReport(result, root, threshold);
17751
+ process.stderr.write(` Handing ${count} failed question${count === 1 ? "" : "s"} to ${cli.name}…
17752
+
17753
+ `);
17754
+ const code = await launchAgentCode2(cli.bin, evalFixPrompt(report));
17755
+ if (code === WINDOWS_COMMAND_NOT_FOUND) {
17756
+ notInstalled(agent);
17757
+ }
17758
+ if (code !== 0) {
17759
+ process.exit(code);
17760
+ }
17761
+ };
17762
+ var runInit = async (agent, file) => {
17763
+ const path = join37(process.cwd(), file);
17764
+ if (existsSync20(path)) {
17765
+ logger.error(`${file} already exists — edit it directly, or pass --file to draft elsewhere.`);
17766
+ process.exit(1);
17767
+ }
17768
+ const code = await launchAgentCode2(AGENTS[agent].bin, initPrompt(file));
17769
+ if (code === WINDOWS_COMMAND_NOT_FOUND) {
17770
+ notInstalled(agent);
17771
+ }
17772
+ if (code !== 0) {
17773
+ process.exit(code);
17774
+ }
17775
+ };
17776
+ var evalCommand = defineCommand8({
17777
+ args: {
17778
+ action: {
17779
+ description: 'Optional action: "init" drafts a starter evals file.',
17780
+ required: false,
17781
+ type: "positional"
17782
+ },
17783
+ agent: {
17784
+ default: "claude",
17785
+ description: "Agent CLI that reads and grades the docs: claude | codex.",
17786
+ type: "string"
17787
+ },
17788
+ file: {
17789
+ default: DEFAULT_FILE,
17790
+ description: "The evals file to run.",
17791
+ type: "string"
17792
+ },
17793
+ fix: {
17794
+ description: "After a failing run, hand the report to the agent to fix the docs interactively.",
17795
+ type: "boolean"
17796
+ },
17797
+ json: {
17798
+ description: "Emit the report as JSON on stdout (for CI/editors).",
17799
+ type: "boolean"
17800
+ },
17801
+ threshold: {
17802
+ description: "Minimum passing fraction (0..1) before the run exits non-zero. Defaults to 1.",
17803
+ type: "string"
17804
+ },
17805
+ timeout: {
17806
+ description: `Reader time limit per question, in seconds. Defaults to ${DEFAULT_TIMEOUT_S}.`,
17807
+ type: "string"
17808
+ },
17809
+ verbose: {
17810
+ description: "Include the reader's full answer under each failure.",
17811
+ type: "boolean"
17812
+ }
17813
+ },
17814
+ meta: {
17815
+ description: "Test the docs: an agent answers your questions using only the documentation.",
17816
+ name: "eval"
17817
+ },
17818
+ async run({ args }) {
17819
+ const root = process.cwd();
17820
+ const { agent, threshold, timeoutS } = parseFlags(args);
17821
+ if (args.action === "init") {
17822
+ await runInit(agent, args.file);
17823
+ return;
17824
+ }
17825
+ let result;
17826
+ try {
17827
+ const project = await scanProject(root, { mode: "build" });
17828
+ const evalsPath = join37(root, args.file);
17829
+ const { evals, raw } = await loadEvalsFile(evalsPath);
17830
+ process.stderr.write(`${headerLine(evals.questions.length, agent)}
17831
+
17832
+ `);
17833
+ result = await runEval({
17834
+ agent,
17835
+ evals,
17836
+ evalsPath,
17837
+ onProgress: (event) => {
17838
+ if (event.kind === "question-start") {
17839
+ process.stderr.write(`${startLine(event.id, event.index, event.total)}
17840
+ `);
17841
+ return;
17842
+ }
17843
+ const lines = [
17844
+ questionLine(event.result),
17845
+ ...questionDetails(event.result, Boolean(args.verbose))
17846
+ ];
17847
+ process.stderr.write(`${lines.join(`
17848
+ `)}
17849
+ `);
17850
+ },
17851
+ project,
17852
+ rawEvals: raw,
17853
+ readerTimeoutMs: timeoutS * 1000
17854
+ });
17855
+ } catch (error) {
17856
+ if (error instanceof EvalsFileError) {
17857
+ logger.error(error.message);
17858
+ process.exit(1);
17859
+ }
17860
+ if (error instanceof BlumeError) {
17861
+ logger.error(error.diagnostic.message);
17862
+ process.exit(1);
17863
+ }
17864
+ if (error?.code === "ENOENT") {
17865
+ notInstalled(agent);
17866
+ }
17867
+ reportInternalError(error);
17868
+ process.exit(1);
17869
+ }
17870
+ const tail2 = [
17871
+ "",
17872
+ ...warningLines(result, root),
17873
+ ...fixLines(result, root),
17874
+ "",
17875
+ ` ${summaryLine2(result)}`,
17876
+ ""
17877
+ ];
17878
+ process.stderr.write(tail2.join(`
17879
+ `));
17880
+ const failed = passFraction(result) < threshold;
17881
+ if (args.fix) {
17882
+ await runFixHandoff(agent, result, root, threshold);
17883
+ return;
17884
+ }
17885
+ if (args.json) {
17886
+ process.stdout.write(evalReportJson(result, root, threshold));
17887
+ if (failed) {
17888
+ await flushStdout();
17889
+ process.exit(1);
17890
+ }
17891
+ return;
17892
+ }
17893
+ if (failed) {
17894
+ process.exit(1);
17895
+ }
17896
+ }
17897
+ });
17898
+
16792
17899
  // src/cli/commands/init.ts
16793
17900
  import * as clack from "@clack/prompts";
16794
- import { defineCommand as defineCommand8 } from "citty";
16795
- import { resolve as resolve9 } from "pathe";
17901
+ import { defineCommand as defineCommand9 } from "citty";
17902
+ import { resolve as resolve10 } from "pathe";
16796
17903
 
16797
17904
  // src/cli/init/questions.ts
16798
- import { basename as basename5, resolve as resolve8 } from "pathe";
17905
+ import { basename as basename6, resolve as resolve9 } from "pathe";
16799
17906
  var cancelled = (value) => typeof value === "symbol";
16800
17907
  var collectAnswers = async (prompter, flags, defaults) => {
16801
17908
  const directory = flags.directory ?? await prompter.text({
@@ -16806,9 +17913,9 @@ var collectAnswers = async (prompter, flags, defaults) => {
16806
17913
  if (cancelled(directory)) {
16807
17914
  return null;
16808
17915
  }
16809
- const root = resolve8(defaults.cwd, directory);
17916
+ const root = resolve9(defaults.cwd, directory);
16810
17917
  const title = await prompter.text({
16811
- initialValue: titleize(basename5(root)),
17918
+ initialValue: titleize(basename6(root)),
16812
17919
  message: "What's your docs site called?",
16813
17920
  validate: (value) => value?.trim() ? undefined : "Give your docs site a name."
16814
17921
  });
@@ -16906,7 +18013,7 @@ var ejectScaffold = async (root, answers) => {
16906
18013
  `);
16907
18014
  }
16908
18015
  };
16909
- var initCommand = defineCommand8({
18016
+ var initCommand = defineCommand9({
16910
18017
  args: {
16911
18018
  "content-dir": {
16912
18019
  description: "Content directory.",
@@ -16975,14 +18082,18 @@ var initCommand = defineCommand8({
16975
18082
  title: "My Docs"
16976
18083
  };
16977
18084
  }
16978
- const root = resolve9(cwd, answers.directory);
18085
+ const root = resolve10(cwd, answers.directory);
16979
18086
  if (validateContentDir(root, answers.contentDir) !== undefined) {
16980
18087
  logger.error(`Invalid --content-dir "${answers.contentDir}" (must be a path inside the project).`);
16981
18088
  process.exit(1);
16982
18089
  }
16983
18090
  const sink = interactive ? clack.log : logger;
16984
18091
  const { createdPackage } = await applyPlan(buildPlan(root, answers), sink);
16985
- const ignored = await ensureGitignore(root, [".blume/", "dist/"]);
18092
+ const ignored = await ensureGitignore(root, [
18093
+ "node_modules/",
18094
+ ".blume/",
18095
+ "dist/"
18096
+ ]);
16986
18097
  if (ignored.length > 0) {
16987
18098
  sink.success(`Added ${ignored.join(", ")} to .gitignore`);
16988
18099
  }
@@ -17000,12 +18111,223 @@ var initCommand = defineCommand8({
17000
18111
  }
17001
18112
  });
17002
18113
 
18114
+ // src/cli/commands/mcp-stdio.ts
18115
+ import { readFile as readFile19 } from "node:fs/promises";
18116
+ import { defineCommand as defineCommand10 } from "citty";
18117
+
18118
+ // src/ai/mcp/stdio.ts
18119
+ import { once } from "node:events";
18120
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
18121
+
18122
+ // src/ai/mcp/server.ts
18123
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
18124
+ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
18125
+ import {
18126
+ CallToolRequestSchema,
18127
+ ListToolsRequestSchema
18128
+ } from "@modelcontextprotocol/sdk/types.js";
18129
+
18130
+ // src/search/orama-index.ts
18131
+ import { create, insertMultiple, search } from "@orama/orama";
18132
+ var SCHEMA = {
18133
+ content: "string",
18134
+ description: "string",
18135
+ locale: "enum",
18136
+ route: "string",
18137
+ title: "string"
18138
+ };
18139
+ var BOOST = { description: 2, title: 3 };
18140
+ var buildOramaIndex = async (documents) => {
18141
+ const db = create({ schema: SCHEMA });
18142
+ await insertMultiple(db, documents);
18143
+ return db;
18144
+ };
18145
+ var queryOramaIndex = async (db, term, limit, locale) => {
18146
+ const found = await search(db, {
18147
+ boost: BOOST,
18148
+ limit,
18149
+ properties: ["title", "description", "content"],
18150
+ term,
18151
+ ...locale ? { where: { locale: { eq: locale } } } : {}
18152
+ });
18153
+ return found.hits.map((hit) => hit.document);
18154
+ };
18155
+
18156
+ // src/ai/mcp/server.ts
18157
+ var DEFAULT_SEARCH_LIMIT = 8;
18158
+ var MAX_SEARCH_LIMIT = 20;
18159
+ var EXCERPT_LENGTH = 200;
18160
+ var INPUT_SCHEMAS = {
18161
+ get_navigation: { properties: {}, type: "object" },
18162
+ get_page: {
18163
+ properties: {
18164
+ route: {
18165
+ description: "The page route, e.g. `/guides/install`.",
18166
+ type: "string"
18167
+ }
18168
+ },
18169
+ required: ["route"],
18170
+ type: "object"
18171
+ },
18172
+ list_pages: { properties: {}, type: "object" },
18173
+ search_docs: {
18174
+ properties: {
18175
+ limit: {
18176
+ description: `Maximum hits to return (default ${DEFAULT_SEARCH_LIMIT}).`,
18177
+ maximum: MAX_SEARCH_LIMIT,
18178
+ minimum: 1,
18179
+ type: "integer"
18180
+ },
18181
+ query: { description: "The search query.", type: "string" }
18182
+ },
18183
+ required: ["query"],
18184
+ type: "object"
18185
+ }
18186
+ };
18187
+ var TOOL_DEFINITIONS = MCP_TOOLS.map((tool) => ({
18188
+ annotations: tool.annotations,
18189
+ description: tool.description,
18190
+ inputSchema: INPUT_SCHEMAS[tool.name],
18191
+ name: tool.name,
18192
+ title: tool.title
18193
+ }));
18194
+ var asString2 = (value) => typeof value === "string" ? value : "";
18195
+ var asLimit = (value) => {
18196
+ const num = typeof value === "number" ? value : Number(value);
18197
+ if (!Number.isFinite(num)) {
18198
+ return DEFAULT_SEARCH_LIMIT;
18199
+ }
18200
+ return Math.min(Math.max(Math.trunc(num), 1), MAX_SEARCH_LIMIT);
18201
+ };
18202
+ var normalizeRoute2 = (input, data) => {
18203
+ let value = input.trim();
18204
+ if (/^https?:\/\//iu.test(value)) {
18205
+ try {
18206
+ value = new URL(value).pathname;
18207
+ } catch {}
18208
+ }
18209
+ try {
18210
+ value = decodeURI(value);
18211
+ } catch {}
18212
+ const noTrailing = value.replace(/\/+$/u, "");
18213
+ const noSuffix = noTrailing.replace(/\.mdx?$/u, "");
18214
+ const withSlash = noSuffix.startsWith("/") ? noSuffix : `/${noSuffix}`;
18215
+ const based = stripBasePath(data.base, withSlash);
18216
+ return based === "" ? "/" : based;
18217
+ };
18218
+ var urlFor = (route, data) => {
18219
+ const path = withBasePath(data.base, route);
18220
+ return data.site ? `${data.site.replace(/\/+$/u, "")}${path}` : path;
18221
+ };
18222
+ var excerptFor = (doc) => {
18223
+ if (doc.description) {
18224
+ return doc.description;
18225
+ }
18226
+ const head = doc.content.slice(0, EXCERPT_LENGTH).trim();
18227
+ return doc.content.length > EXCERPT_LENGTH ? `${head}…` : head;
18228
+ };
18229
+ var text = (value, isError = false) => ({
18230
+ content: [{ text: value, type: "text" }],
18231
+ ...isError ? { isError: true } : {}
18232
+ });
18233
+ var createIndexProvider = (documents) => {
18234
+ let dbPromise = null;
18235
+ return () => {
18236
+ dbPromise ??= buildOramaIndex(documents);
18237
+ return dbPromise;
18238
+ };
18239
+ };
18240
+ var buildServer = (data, index) => {
18241
+ const server = new Server({ name: data.name, version: data.version }, {
18242
+ capabilities: { tools: {} },
18243
+ ...data.instructions ? { instructions: data.instructions } : {}
18244
+ });
18245
+ server.setRequestHandler(ListToolsRequestSchema, () => ({
18246
+ tools: TOOL_DEFINITIONS
18247
+ }));
18248
+ server.setRequestHandler(CallToolRequestSchema, async (request2) => {
18249
+ const { arguments: args = {}, name } = request2.params;
18250
+ if (name === "search_docs") {
18251
+ const db = await index();
18252
+ const hits = await queryOramaIndex(db, asString2(args.query), asLimit(args.limit));
18253
+ const results = hits.map((doc) => ({
18254
+ excerpt: excerptFor(doc),
18255
+ route: doc.route,
18256
+ title: doc.title,
18257
+ url: urlFor(doc.route, data)
18258
+ }));
18259
+ return text(JSON.stringify(results, null, 2));
18260
+ }
18261
+ if (name === "get_page") {
18262
+ const key = normalizeRoute2(asString2(args.route), data);
18263
+ const markdown = data.pages[key];
18264
+ if (markdown === undefined) {
18265
+ return text(`No page found at "${key}". Use list_pages or search_docs to find valid routes.`, true);
18266
+ }
18267
+ return text(markdown);
18268
+ }
18269
+ if (name === "list_pages") {
18270
+ return text(JSON.stringify(data.routes.map((route) => ({
18271
+ contentType: route.contentType,
18272
+ description: route.description,
18273
+ lastModified: route.lastModified,
18274
+ route: route.route,
18275
+ title: route.title,
18276
+ url: urlFor(route.route, data)
18277
+ })), null, 2));
18278
+ }
18279
+ if (name === "get_navigation") {
18280
+ return text(JSON.stringify(data.navigation, null, 2));
18281
+ }
18282
+ return text(`Unknown tool: ${name}`, true);
18283
+ });
18284
+ return server;
18285
+ };
18286
+
18287
+ // src/ai/mcp/stdio.ts
18288
+ var serveMcpStdio = async (data, streams = {}) => {
18289
+ const stdin = streams.stdin ?? process.stdin;
18290
+ const stdout = streams.stdout ?? process.stdout;
18291
+ const transport = new StdioServerTransport(stdin, stdout);
18292
+ const server = buildServer(data, createIndexProvider(data.documents));
18293
+ await server.connect(transport);
18294
+ await once(stdin, "end");
18295
+ await transport.close();
18296
+ };
18297
+
18298
+ // src/cli/commands/mcp-stdio.ts
18299
+ var mcpStdioCommand = defineCommand10({
18300
+ args: {
18301
+ data: {
18302
+ description: "Path to a serialized MCP data snapshot (JSON).",
18303
+ required: true,
18304
+ type: "string"
18305
+ }
18306
+ },
18307
+ meta: {
18308
+ description: "Serve an MCP data snapshot over stdio (internal, used by `blume eval`).",
18309
+ name: "mcp-stdio"
18310
+ },
18311
+ async run({ args }) {
18312
+ let data;
18313
+ try {
18314
+ data = JSON.parse(await readFile19(args.data, "utf-8"));
18315
+ } catch (error) {
18316
+ const detail = error instanceof Error ? error.message : String(error);
18317
+ process.stderr.write(`blume mcp-stdio: cannot load the snapshot at ${args.data}: ${detail}
18318
+ `);
18319
+ process.exit(1);
18320
+ }
18321
+ await serveMcpStdio(data);
18322
+ }
18323
+ });
18324
+
17003
18325
  // src/cli/commands/preview.ts
17004
- import { existsSync as existsSync20 } from "node:fs";
18326
+ import { existsSync as existsSync21 } from "node:fs";
17005
18327
  import { preview } from "astro";
17006
- import { defineCommand as defineCommand9 } from "citty";
17007
- import { join as join34 } from "pathe";
17008
- var previewCommand = defineCommand9({
18328
+ import { defineCommand as defineCommand11 } from "citty";
18329
+ import { join as join38 } from "pathe";
18330
+ var previewCommand = defineCommand11({
17009
18331
  args: {
17010
18332
  host: { description: "Network host to bind.", type: "string" },
17011
18333
  port: { description: "Port to listen on.", type: "string" }
@@ -17018,7 +18340,7 @@ var previewCommand = defineCommand9({
17018
18340
  const root = process.cwd();
17019
18341
  const { config } = await loadConfig(root);
17020
18342
  const context = resolveProjectContext(root, config);
17021
- if (!existsSync20(join34(context.outDir, "astro.config.mjs"))) {
18343
+ if (!existsSync21(join38(context.outDir, "astro.config.mjs"))) {
17022
18344
  logger.error("No build found. Run `blume build` first.");
17023
18345
  process.exit(1);
17024
18346
  }
@@ -17035,9 +18357,9 @@ var previewCommand = defineCommand9({
17035
18357
 
17036
18358
  // src/cli/commands/sync.ts
17037
18359
  import { rm as rm4 } from "node:fs/promises";
17038
- import { defineCommand as defineCommand10 } from "citty";
17039
- import { join as join35 } from "pathe";
17040
- var syncCommand = defineCommand10({
18360
+ import { defineCommand as defineCommand12 } from "citty";
18361
+ import { join as join39 } from "pathe";
18362
+ var syncCommand = defineCommand12({
17041
18363
  args: {
17042
18364
  force: {
17043
18365
  description: "Clear the source cache before refetching.",
@@ -17058,7 +18380,7 @@ var syncCommand = defineCommand10({
17058
18380
  if (args.force) {
17059
18381
  const { config } = await loadConfig(root);
17060
18382
  const context = resolveProjectContext(root, config);
17061
- await rm4(join35(context.outDir, "cache"), { force: true, recursive: true });
18383
+ await rm4(join39(context.outDir, "cache"), { force: true, recursive: true });
17062
18384
  logger.info("Cleared source cache.");
17063
18385
  }
17064
18386
  const lock = readDevLock(resolveRuntimeDir(root));
@@ -17076,13 +18398,13 @@ var syncCommand = defineCommand10({
17076
18398
  });
17077
18399
 
17078
18400
  // src/cli/commands/validate.ts
17079
- import { existsSync as existsSync22 } from "node:fs";
17080
- import { defineCommand as defineCommand11 } from "citty";
17081
- import { join as join37 } from "pathe";
18401
+ import { existsSync as existsSync23 } from "node:fs";
18402
+ import { defineCommand as defineCommand13 } from "citty";
18403
+ import { join as join41 } from "pathe";
17082
18404
 
17083
18405
  // src/core/links.ts
17084
- import { existsSync as existsSync21 } from "node:fs";
17085
- import { basename as basename6, join as join36 } from "pathe";
18406
+ import { existsSync as existsSync22 } from "node:fs";
18407
+ import { basename as basename7, join as join40 } from "pathe";
17086
18408
  var HTTP = /^https?:\/\//iu;
17087
18409
  var PROTOCOL_RELATIVE = /^\/\//u;
17088
18410
  var SCHEME = /^[a-z][a-z0-9+.-]*:/iu;
@@ -17095,9 +18417,9 @@ var decodePercent = (value) => {
17095
18417
  };
17096
18418
  var DOC_EXT = /\.(?:md|mdx)$/iu;
17097
18419
  var FILE_EXT = /\.[a-z0-9]+$/iu;
17098
- var assetIsPresent = (resolved, ctx) => ctx.publicDir !== null && existsSync21(join36(ctx.publicDir, resolved));
18420
+ var assetIsPresent = (resolved, ctx) => ctx.publicDir !== null && existsSync22(join40(ctx.publicDir, resolved));
17099
18421
  var NUMERIC_PREFIX3 = /^\d+[-_.]/u;
17100
- var isIndexPage = (page2) => /^index\.(?:md|mdx)$/iu.test(basename6(page2.navPath).replace(NUMERIC_PREFIX3, ""));
18422
+ var isIndexPage = (page2) => /^index\.(?:md|mdx)$/iu.test(basename7(page2.navPath).replace(NUMERIC_PREFIX3, ""));
17101
18423
  var applyRelativePart = (segments, part) => {
17102
18424
  if (part === "" || part === ".") {
17103
18425
  return;
@@ -17268,7 +18590,7 @@ var validateLinks = async (graph, options) => {
17268
18590
  };
17269
18591
 
17270
18592
  // src/cli/commands/validate.ts
17271
- var validateCommand = defineCommand11({
18593
+ var validateCommand = defineCommand13({
17272
18594
  args: {
17273
18595
  external: {
17274
18596
  description: "Check external (HTTP) links over the network.",
@@ -17306,12 +18628,12 @@ var validateCommand = defineCommand11({
17306
18628
  });
17307
18629
  extraRoutes.push(...manifest.routes.flatMap((route) => route.fallback ? [route.path] : []));
17308
18630
  }
17309
- const publicDir = join37(root, "public");
18631
+ const publicDir = join41(root, "public");
17310
18632
  diagnostics.push(...await validateLinks(project.graph, {
17311
18633
  basePath: project.config.basePath,
17312
18634
  checkExternal: Boolean(args.external),
17313
18635
  extraRoutes,
17314
- publicDir: existsSync22(publicDir) ? publicDir : null,
18636
+ publicDir: existsSync23(publicDir) ? publicDir : null,
17315
18637
  redirects: project.config.redirects
17316
18638
  }));
17317
18639
  } catch (error) {
@@ -17342,7 +18664,7 @@ var validateCommand = defineCommand11({
17342
18664
  });
17343
18665
 
17344
18666
  // src/cli/index.ts
17345
- var main = defineCommand12({
18667
+ var main = defineCommand14({
17346
18668
  meta: {
17347
18669
  description: "Markdown-first documentation powered by Astro and Vite.",
17348
18670
  name: "blume",
@@ -17356,7 +18678,9 @@ var main = defineCommand12({
17356
18678
  dev: devCommand,
17357
18679
  doctor: doctorCommand,
17358
18680
  eject: ejectCommand,
18681
+ eval: evalCommand,
17359
18682
  init: initCommand,
18683
+ "mcp-stdio": mcpStdioCommand,
17360
18684
  preview: previewCommand,
17361
18685
  sync: syncCommand,
17362
18686
  validate: validateCommand
@@ -17373,5 +18697,5 @@ process.on("unhandledRejection", (error) => {
17373
18697
  });
17374
18698
  runMain(main);
17375
18699
 
17376
- //# debugId=74F150F28E9B859A64756E2164756E21
18700
+ //# debugId=E4DFD2C6131D546564756E2164756E21
17377
18701
  //# sourceMappingURL=index.js.map