blume 1.1.4 → 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 (60) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +1 -1
  3. package/dist/cli/index.js +1286 -63
  4. package/dist/cli/index.js.map +32 -21
  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 +1 -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 +19 -8
  24. package/src/ai/mcp/stdio.ts +35 -0
  25. package/src/astro/generate.ts +25 -2
  26. package/src/astro/templates.ts +114 -22
  27. package/src/cli/commands/eval.ts +291 -0
  28. package/src/cli/commands/init.ts +9 -4
  29. package/src/cli/commands/mcp-stdio.ts +36 -0
  30. package/src/cli/index.ts +4 -0
  31. package/src/cli/required-secrets.ts +1 -1
  32. package/src/components/content/AccordionItem.astro +2 -2
  33. package/src/components/content/TreeFolder.astro +1 -2
  34. package/src/components/islands/AskAI.astro +9 -2
  35. package/src/components/islands/ask-ai.tsx +4 -2
  36. package/src/components/islands/hooks.ts +10 -4
  37. package/src/components/layout/NavTree.astro +37 -19
  38. package/src/components/layout/ReferenceLayout.astro +4 -0
  39. package/src/components/layout/RootLayout.astro +1 -1
  40. package/src/components/openapi/SchemaProperty.astro +3 -3
  41. package/src/core/config-input.ts +18 -0
  42. package/src/core/config.ts +4 -0
  43. package/src/core/data.ts +1 -0
  44. package/src/core/graph.ts +1 -0
  45. package/src/core/navigation.ts +9 -2
  46. package/src/core/schema.ts +51 -4
  47. package/src/core/server-features.ts +1 -1
  48. package/src/core/types.ts +5 -3
  49. package/src/eval/agents.ts +340 -0
  50. package/src/eval/findings.ts +103 -0
  51. package/src/eval/prompts.ts +78 -0
  52. package/src/eval/report.ts +214 -0
  53. package/src/eval/run.ts +290 -0
  54. package/src/eval/schema.ts +124 -0
  55. package/src/openapi/references.ts +23 -2
  56. package/src/openapi/render-mdx.ts +27 -4
  57. package/src/openapi/scalar.ts +1 -0
  58. package/src/openapi/source.ts +11 -4
  59. package/src/registry/eject.ts +23 -1
  60. 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";
@@ -3090,7 +3090,12 @@ var routeSlug = (route) => slugify(trimChar(route, "/")) || "reference";
3090
3090
  var sourcesOf = (block) => {
3091
3091
  const sources = [...block.sources];
3092
3092
  if (block.spec) {
3093
- sources.unshift({ spec: block.spec });
3093
+ sources.unshift({
3094
+ includeInLlms: true,
3095
+ includeInSearch: true,
3096
+ noindex: false,
3097
+ spec: block.spec
3098
+ });
3094
3099
  }
3095
3100
  return sources;
3096
3101
  };
@@ -3114,8 +3119,11 @@ var referencesFor = (kind, block, defaultLabel, renderer, display, basePath) =>
3114
3119
  return {
3115
3120
  basePath,
3116
3121
  display,
3122
+ includeInLlms: source.includeInLlms,
3123
+ includeInSearch: source.includeInSearch,
3117
3124
  kind,
3118
3125
  label,
3126
+ noindex: source.noindex,
3119
3127
  renderer,
3120
3128
  route,
3121
3129
  scalar: block.scalar,
@@ -3417,7 +3425,10 @@ var WRANGLER_CONFIG_FILES = [
3417
3425
  "wrangler.toml"
3418
3426
  ];
3419
3427
  var resolveCloudflareAdapterArgs = (context) => {
3420
- const args = ['prerenderEnvironment: "node"'];
3428
+ const args = [
3429
+ 'prerenderEnvironment: "node"',
3430
+ 'imageService: "compile"'
3431
+ ];
3421
3432
  const wranglerPath = WRANGLER_CONFIG_FILES.map((file) => join8(context.root, file)).find((file) => existsSync4(file));
3422
3433
  if (wranglerPath) {
3423
3434
  let configPath = relative5(context.outDir, wranglerPath);
@@ -3428,6 +3439,18 @@ var resolveCloudflareAdapterArgs = (context) => {
3428
3439
  }
3429
3440
  return `{ ${args.join(", ")} }`;
3430
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
+ };
3431
3454
  var runtimeDependencies = (options) => {
3432
3455
  const { config, needsReact, needsSvelte, needsVue } = options;
3433
3456
  const deps = ["@astrojs/mdx"];
@@ -3444,7 +3467,7 @@ var runtimeDependencies = (options) => {
3444
3467
  deps.push("@scalar/astro");
3445
3468
  }
3446
3469
  deps.push(...searchProviderMeta(config.search.provider).runtimeDeps);
3447
- if (config.ai.ask?.enabled) {
3470
+ if (config.ai.ask?.enabled && !config.ai.ask.endpoint) {
3448
3471
  const askDep = askBackendRuntimeDep(config.ai.ask);
3449
3472
  if (askDep) {
3450
3473
  deps.push(askDep);
@@ -3487,6 +3510,29 @@ var devWatchOption = (outDir, contentWatchesRuntimeDir) => contentWatchesRuntime
3487
3510
  watch: {
3488
3511
  ignored: ${JSON.stringify([join8(outDir, ".astro", "**")])},
3489
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
+ };
3490
3536
  var astroConfigTemplate = (options) => {
3491
3537
  const { context, config, needsReact, pages, dataPath, themePath } = options;
3492
3538
  const {
@@ -3517,6 +3563,7 @@ var astroConfigTemplate = (options) => {
3517
3563
  const adapterExpr = deployment.adapter === "vercel" ? `withAdapterRoot(adapter(${adapterArgs}), ${JSON.stringify(adapterRoot(context))})` : `adapter(${adapterArgs})`;
3518
3564
  const adapterOption = server && deployment.adapter ? `
3519
3565
  adapter: ${adapterExpr},` : "";
3566
+ const sessionOption = resolveSessionOption(deployment);
3520
3567
  const siteOption = deployment.site ? `
3521
3568
  site: ${JSON.stringify(deployment.site)},` : "";
3522
3569
  const baseOption = deployment.base ? `
@@ -3538,7 +3585,10 @@ var astroConfigTemplate = (options) => {
3538
3585
  const fontEntries = buildFontEntries(config.theme.fonts);
3539
3586
  const fontsOption = fontEntries.length ? `
3540
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(", ")}],` : "";
3541
- 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
+ });
3542
3592
  const reactImport = needsReact ? `import react from "@astrojs/react";
3543
3593
  ` : "";
3544
3594
  const vueImport = needsVue ? `import vue from "@astrojs/vue";
@@ -3574,19 +3624,25 @@ var astroConfigTemplate = (options) => {
3574
3624
  }
3575
3625
  integrations.push(`blumeIntegration(${JSON.stringify({ base: deployment.base, contentRoutes, pages })})`);
3576
3626
  const watchOption = devWatchOption(context.outDir, options.contentWatchesRuntimeDir);
3627
+ const {
3628
+ configSourceMarker,
3629
+ userConfigImports,
3630
+ userConfigSetup,
3631
+ userIntegrationSpread
3632
+ } = renderIntegrationBridge(options.integrationBridge);
3577
3633
  return `// Generated by Blume. Do not edit; this file is recreated on each run.
3578
- ${defineConfigImport}
3634
+ ${configSourceMarker}${userConfigImports}${defineConfigImport}
3579
3635
  import mdx from "@astrojs/mdx";
3580
3636
  import tailwindcss from "@tailwindcss/vite";
3581
3637
  import { blumeMarkdownProcessor, blumeMdxProcessor, blumeShikiTransformers, blumeTwoslashTransformer } from "blume/markdown";
3582
3638
  ${reactImport}${vueImport}${svelteImport}${blumeImport}${adapterImport}
3583
- export default defineConfig({
3639
+ ${userConfigSetup}export default defineConfig({
3584
3640
  root: ${JSON.stringify(context.outDir)},
3585
3641
  srcDir: ${JSON.stringify(`${context.outDir}/src`)},
3586
3642
  outDir: ${JSON.stringify(astroOutDir(context))},
3587
3643
  publicDir: ${JSON.stringify(`${context.root}/public`)},
3588
- output: ${JSON.stringify(deployment.output)},${adapterOption}${siteOption}${baseOption}${redirectsOption}${i18nOption}${fontsOption}
3589
- integrations: [${integrations.join(", ")}],
3644
+ output: ${JSON.stringify(deployment.output)},${adapterOption}${sessionOption}${siteOption}${baseOption}${redirectsOption}${i18nOption}${fontsOption}
3645
+ integrations: [${integrations.join(", ")}${userIntegrationSpread}],
3590
3646
  markdown: {
3591
3647
  processor: blumeMarkdownProcessor(${JSON.stringify({
3592
3648
  basePath: config.basePath,
@@ -3606,18 +3662,25 @@ export default defineConfig({
3606
3662
  devToolbar: { enabled: false },
3607
3663
  vite: {
3608
3664
  plugins: [tailwindcss(), prerenderDepsPlugin(), serverAppResolvePlugin()],
3609
- // Mermaid (lazy-loaded client-side for diagrams) statically imports dayjs as
3610
- // CJS (\`dayjs/dayjs.min.js\`). In dev, an un-pre-bundled dependency is served
3611
- // as raw ESM, and that UMD file exposes no \`default\` export, so mermaid
3612
- // throws on load and diagrams render blank. Forcing mermaid through the dep
3613
- // optimizer bundles dayjs with correct CJS interop. In a standalone install
3614
- // Blume's dynamic \`import("mermaid")\` lives inside \`node_modules/blume\`,
3615
- // which Vite's optimizer scan doesn't crawl, so mermaid is never discovered
3616
- // on its own hence the explicit include. mermaid resolves through the
3617
- // \`blume\` package (it isn't a direct dep of the generated project), so the
3618
- // nested \`blume > mermaid\` form is required. Production (Rollup) already
3619
- // handles the interop, so this only affects dev.
3620
- 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
+ },
3621
3684
  // Blume's render-time deps are forced external on both build environments so
3622
3685
  // native bindings resolve at runtime and isolated linkers don't bundle
3623
3686
  // symlinked store copies (which would surface their children as unresolvable
@@ -3811,7 +3874,11 @@ import data from "blume:data";
3811
3874
  const { strings } = Astro.props;
3812
3875
  ---
3813
3876
 
3814
- <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
+ />
3815
3882
  ` : `---
3816
3883
  // Generated by Blume. Do not edit.
3817
3884
  // Ask AI is off (\`ai.ask.enabled\`), so the header's Ask trigger renders nothing.
@@ -4093,6 +4160,7 @@ const htmlLang = i18n ? i18n.defaultLocale : "en";
4093
4160
  favicon={data.config.favicon}
4094
4161
  appleIcon={data.config.appleIcon}
4095
4162
  navigation={data.navigation}
4163
+ noindex={${options.noindex === true}}
4096
4164
  pageTitle={${JSON.stringify(options.title)}}
4097
4165
  route={${JSON.stringify(options.route)}}
4098
4166
  searchEnabled={data.config.search.enabled}
@@ -7514,6 +7582,9 @@ var searchMetaSchema = z2.strictObject({
7514
7582
  exclude: z2.boolean().default(false),
7515
7583
  tags: z2.array(z2.string()).optional()
7516
7584
  });
7585
+ var aiMetaSchema = z2.strictObject({
7586
+ exclude: z2.boolean().default(false)
7587
+ });
7517
7588
  var changelogMetaSchema = z2.strictObject({
7518
7589
  category: z2.string().optional(),
7519
7590
  date: dateSchema.optional(),
@@ -7529,6 +7600,7 @@ var authorSchema = z2.union([
7529
7600
  }).catchall(z2.unknown())
7530
7601
  ]);
7531
7602
  var pageMetaBaseSchema = z2.strictObject({
7603
+ ai: aiMetaSchema.default({}),
7532
7604
  authors: z2.union([authorSchema, z2.array(authorSchema)]).optional(),
7533
7605
  changelog: changelogMetaSchema.optional(),
7534
7606
  date: dateSchema.optional(),
@@ -7662,6 +7734,7 @@ var contentConfigSchema = z2.strictObject({
7662
7734
  sources: z2.array(contentSourceSchema).optional()
7663
7735
  });
7664
7736
  var navTabSchema = z2.strictObject({
7737
+ href: z2.string().min(1).optional(),
7665
7738
  icon: iconName.optional(),
7666
7739
  items: z2.array(z2.strictObject({
7667
7740
  description: z2.string().optional(),
@@ -7806,11 +7879,25 @@ var mcpConfigSchema = z2.strictObject({
7806
7879
  name: z2.string().optional(),
7807
7880
  route: z2.string().default("/mcp").transform(normalizeRoute)
7808
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
+ });
7809
7895
  var aiConfigSchema = z2.strictObject({
7810
7896
  ask: z2.strictObject({
7811
7897
  apiKeyEnv: z2.string().optional(),
7812
7898
  baseUrl: z2.string().url().optional(),
7813
7899
  enabled: z2.boolean().default(false),
7900
+ endpoint: askEndpointSchema.optional(),
7814
7901
  model: z2.string().default("openai/gpt-5.5"),
7815
7902
  provider: z2.enum(askAiProviders).default("gateway"),
7816
7903
  suggestions: z2.array(z2.strictObject({
@@ -7818,7 +7905,7 @@ var aiConfigSchema = z2.strictObject({
7818
7905
  label: z2.string().min(1)
7819
7906
  })).default([])
7820
7907
  }).superRefine((value, ctx) => {
7821
- if (value.provider === "openai-compatible" && !value.baseUrl) {
7908
+ if (value.provider === "openai-compatible" && !(value.baseUrl || value.endpoint)) {
7822
7909
  ctx.addIssue({
7823
7910
  code: z2.ZodIssueCode.custom,
7824
7911
  message: 'ai.ask.baseUrl is required when provider is "openai-compatible".',
@@ -8046,7 +8133,10 @@ var reactConfigSchema = z2.strictObject({
8046
8133
  compiler: z2.boolean().default(true)
8047
8134
  });
8048
8135
  var openapiSourceSchema = z2.strictObject({
8136
+ includeInLlms: z2.boolean().default(true),
8137
+ includeInSearch: z2.boolean().default(true),
8049
8138
  label: z2.string().optional(),
8139
+ noindex: z2.boolean().default(false),
8050
8140
  route: z2.string().optional(),
8051
8141
  spec: z2.string()
8052
8142
  });
@@ -8119,6 +8209,7 @@ var blumeConfigSchema = z2.strictObject({
8119
8209
  frontmatter: frontmatterConfigSchema.default({}),
8120
8210
  github: githubConfigSchema.optional(),
8121
8211
  i18n: i18nConfigSchema.optional(),
8212
+ integrations: z2.array(z2.custom()).default([]),
8122
8213
  lastModified: lastModifiedConfigSchema.default(false),
8123
8214
  logo: logoConfigSchema.optional(),
8124
8215
  markdown: markdownConfigSchema.default({}),
@@ -8794,7 +8885,7 @@ var resolveTabHref = (sidebar, path) => {
8794
8885
  return walk(sidebar) ? path : first ?? path;
8795
8886
  };
8796
8887
  var withTabHrefs = (tabs, sidebar) => tabs.map((tab) => {
8797
- const href = resolveTabHref(sidebar, tab.path);
8888
+ const href = tab.href ?? resolveTabHref(sidebar, tab.path);
8798
8889
  return href === tab.path ? tab : { ...tab, href };
8799
8890
  });
8800
8891
  var buildNavigation = (pages, options) => {
@@ -8816,6 +8907,7 @@ var buildNavigation = (pages, options) => {
8816
8907
  })) : options.selectors ?? [];
8817
8908
  const tabs = basePath ? (options.tabs ?? []).map((tab) => ({
8818
8909
  ...tab,
8910
+ ...tab.href ? { href: withBasePath(basePath, tab.href) } : {},
8819
8911
  items: tab.items?.map(rebasePath),
8820
8912
  path: withBasePath(basePath, tab.path)
8821
8913
  })) : options.tabs ?? [];
@@ -8894,6 +8986,7 @@ var buildLocaleNavigation = (code, pages, fallback, fallbackByKey, options, i18n
8894
8986
  const localizePath = (path) => path.startsWith("/") ? localizeRoute(path, code, i18n) : path;
8895
8987
  const tabs = options.navigation.tabs?.map((tab) => ({
8896
8988
  ...tab,
8989
+ ...tab.href ? { href: localizePath(tab.href) } : {},
8897
8990
  items: tab.items?.map((item) => ({
8898
8991
  ...item,
8899
8992
  path: localizePath(item.path)
@@ -9918,23 +10011,30 @@ var operationDescription = (spec, operation) => {
9918
10011
  var withDescription = (description, component) => description.trim() ? `${mdxSafe(description.trim())}
9919
10012
 
9920
10013
  ${component}` : component;
9921
- var operationMdx = (spec, operation) => {
10014
+ var operationMdx = (spec, operation, reference) => {
9922
10015
  const method = operation.method.toUpperCase();
9923
10016
  const title = operation.summary || `${method} ${operation.path}`;
9924
10017
  const description = operation.description.trim() === operation.summary.trim() ? "" : operation.description;
9925
10018
  return {
9926
10019
  body: withDescription(description, `<Operation source="${spec.slug}" id="${operation.key}" />`),
9927
10020
  data: {
10021
+ ...reference?.includeInLlms === false ? { ai: { exclude: true } } : {},
9928
10022
  ...operation.deprecated ? { deprecated: true } : {},
9929
- search: { tags: [operation.tag, method] },
9930
- 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
+ },
9931
10031
  sidebar: { badge: method, label: operation.summary || operation.path },
9932
10032
  title,
9933
10033
  type: "openapi-operation"
9934
10034
  }
9935
10035
  };
9936
10036
  };
9937
- var overviewMdx = (spec) => {
10037
+ var overviewMdx = (spec, reference) => {
9938
10038
  const operations = Object.values(spec.operations);
9939
10039
  const sections = [];
9940
10040
  const known = new Set;
@@ -9976,8 +10076,11 @@ var overviewMdx = (spec) => {
9976
10076
 
9977
10077
  `),
9978
10078
  data: {
10079
+ ...reference?.includeInLlms === false ? { ai: { exclude: true } } : {},
10080
+ ...reference?.includeInSearch === false ? { search: { exclude: true } } : {},
9979
10081
  seo: {
9980
- 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 } : {}
9981
10084
  },
9982
10085
  sidebar: { label: "Overview" },
9983
10086
  title: apiName(spec)
@@ -9999,10 +10102,10 @@ var toEntry = (rendered, ref) => {
9999
10102
  ref
10000
10103
  };
10001
10104
  };
10002
- var specEntries = (spec, operations) => {
10003
- 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`));
10004
10107
  const base = routeToRef(spec.route);
10005
- entries.push(toEntry(overviewMdx(spec), base ? `${base}/index.mdx` : "index.mdx"));
10108
+ entries.push(toEntry(overviewMdx(spec, reference), base ? `${base}/index.mdx` : "index.mdx"));
10006
10109
  return entries;
10007
10110
  };
10008
10111
  var openApiSource = (references, ctx) => {
@@ -10056,7 +10159,7 @@ var openApiSource = (references, ctx) => {
10056
10159
  }
10057
10160
  ] : []
10058
10161
  ],
10059
- entries: specEntries(spec, operations),
10162
+ entries: specEntries(spec, operations, reference),
10060
10163
  slug: reference.slug,
10061
10164
  spec
10062
10165
  };
@@ -11579,6 +11682,12 @@ var usagePolicy = (signals) => {
11579
11682
  }
11580
11683
  return Object.fromEntries(USAGE_TOKENS.map(([key, token]) => [token, signals[key]]));
11581
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
+ };
11582
11691
  var buildAgentReadability = (project) => {
11583
11692
  const { config } = project;
11584
11693
  if (!config.seo.agentReadability) {
@@ -11607,7 +11716,7 @@ var buildAgentReadability = (project) => {
11607
11716
  };
11608
11717
  }
11609
11718
  if (config.ai.ask?.enabled) {
11610
- artifacts.askApi = abs("/api/ask");
11719
+ artifacts.askApi = askApiUrl(config.ai.ask.endpoint, site, abs);
11611
11720
  }
11612
11721
  if (site && config.seo.sitemap) {
11613
11722
  artifacts.sitemap = abs("/sitemap.xml");
@@ -11950,7 +12059,7 @@ var pageUrl = (route, site, base = "") => {
11950
12059
  const path = withBasePath(base, route);
11951
12060
  return encodeURI(site ? `${site.replace(/\/$/u, "")}${path}` : path);
11952
12061
  };
11953
- 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"));
11954
12063
  var indexedNavigations = (project) => {
11955
12064
  const { i18n } = project.config;
11956
12065
  if (i18n) {
@@ -12099,7 +12208,7 @@ var ensureGitignore = async (root, entries) => {
12099
12208
  // src/core/server-features.ts
12100
12209
  var serverFeatures = (config) => {
12101
12210
  const features = [];
12102
- if (config.ai.ask?.enabled) {
12211
+ if (config.ai.ask?.enabled && !config.ai.ask.endpoint) {
12103
12212
  features.push("Ask AI");
12104
12213
  }
12105
12214
  if (config.ai.mcp.enabled) {
@@ -12649,6 +12758,7 @@ var refuseIfDevRunning = (root, action, options = {}) => {
12649
12758
  };
12650
12759
 
12651
12760
  // src/astro/generate.ts
12761
+ import { createHash as createHash2 } from "node:crypto";
12652
12762
  import { existsSync as existsSync14, readFileSync as readFileSync9, realpathSync } from "node:fs";
12653
12763
  import {
12654
12764
  lstat,
@@ -13475,6 +13585,7 @@ var buildReferenceFiles = async (options) => {
13475
13585
  ...ref.scalar
13476
13586
  },
13477
13587
  dataImport: `${"../".repeat(depth + 1)}generated/data.json`,
13588
+ noindex: ref.noindex,
13478
13589
  route: ref.route,
13479
13590
  title: ref.label
13480
13591
  }),
@@ -14765,6 +14876,16 @@ var detectUsesMath = async (root, staged = []) => {
14765
14876
  const contents = await Promise.all(files.map((file) => readOptional(join26(root, file))));
14766
14877
  return [...contents, ...staged].some(containsMath);
14767
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
+ };
14768
14889
  var writeIfChanged = async (path, content) => {
14769
14890
  let existing = null;
14770
14891
  try {
@@ -14983,7 +15104,10 @@ var buildRuntimeData = (project) => {
14983
15104
  config: {
14984
15105
  analytics: config.analytics ?? null,
14985
15106
  appleIcon: resolveAppleIcon(project),
14986
- 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,
14987
15111
  banner: resolveBanner(config),
14988
15112
  basePath: config.basePath,
14989
15113
  codeThemes: config.markdown.codeBlocks.theme,
@@ -15115,7 +15239,7 @@ var writeMcpFiles = async (project, plan, write) => {
15115
15239
  };
15116
15240
  var writeAskFiles = async (project, srcDir, write) => {
15117
15241
  const { ask } = project.config.ai;
15118
- if (!ask?.enabled) {
15242
+ if (!(ask?.enabled && !ask.endpoint)) {
15119
15243
  return;
15120
15244
  }
15121
15245
  const grounded = ask.provider !== "inkeep";
@@ -15168,6 +15292,7 @@ var generateRuntime = async (project) => {
15168
15292
  usesMath,
15169
15293
  userTheme,
15170
15294
  userExamplesCss,
15295
+ integrationBridge,
15171
15296
  islandDiscovery,
15172
15297
  exampleDiscovery,
15173
15298
  componentSlots
@@ -15177,6 +15302,7 @@ var generateRuntime = async (project) => {
15177
15302
  detectUsesMath(context.root, staged.values()),
15178
15303
  readOptional(context.themeFile),
15179
15304
  readOptional(examplesCssFile(context.root, config)),
15305
+ loadIntegrationBridge(config, context),
15180
15306
  discoverIslands(context.root),
15181
15307
  discoverExamples(context.root, config.examples.source),
15182
15308
  buildComponentSlots(context.componentsFile)
@@ -15214,6 +15340,7 @@ var generateRuntime = async (project) => {
15214
15340
  dataPath,
15215
15341
  examplesPath,
15216
15342
  examplesThemePath,
15343
+ integrationBridge,
15217
15344
  needsReact,
15218
15345
  needsSvelte,
15219
15346
  needsVue,
@@ -15439,7 +15566,7 @@ var checkRequiredSecrets = (config) => {
15439
15566
  suggestion: `Set ${env} in .env.local for local dev, or in your host's environment for production.`
15440
15567
  });
15441
15568
  };
15442
- if (config.ai.ask?.enabled) {
15569
+ if (config.ai.ask?.enabled && !config.ai.ask.endpoint) {
15443
15570
  const backend = resolveAskBackend(config.ai.ask);
15444
15571
  if (backend.kind === "gateway") {
15445
15572
  requireSecret("Ask AI (AI Gateway)", "AI_GATEWAY_API_KEY", "on Vercel the gateway can also authenticate via OIDC");
@@ -16196,7 +16323,15 @@ var ejectOpenApiData = (project) => {
16196
16323
  };
16197
16324
  var askFiles = async (project, srcDir, genDir) => {
16198
16325
  const { ask } = project.config.ai;
16199
- 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 });
16200
16335
  return [];
16201
16336
  }
16202
16337
  const grounded = ask.provider !== "inkeep";
@@ -16279,6 +16414,7 @@ var examplesPreviewFiles = (srcDir, basePath, hasExamples) => hasExamples ? [
16279
16414
  path: join31(srcDir, "pages", ...basePath.split("/").filter(Boolean), "blume-examples", "[...path].astro")
16280
16415
  }
16281
16416
  ] : [];
16417
+ var ejectIntegrationBridge = (config, root, configFile) => config.integrations.length > 0 && configFile ? { configFile: toPosix(relative14(root, configFile)) } : undefined;
16282
16418
  var eject = async (root) => {
16283
16419
  const project = await scanProject(root, { mode: "build" });
16284
16420
  const { context, config } = project;
@@ -16340,6 +16476,7 @@ var eject = async (root) => {
16340
16476
  dataPath: "./src/generated/data.json",
16341
16477
  examplesPath: "./src/generated/examples.ts",
16342
16478
  examplesThemePath: "./src/generated/examples.css",
16479
+ integrationBridge: ejectIntegrationBridge(config, root, context.configFile),
16343
16480
  needsReact,
16344
16481
  needsSvelte,
16345
16482
  needsVue,
@@ -16890,9 +17027,878 @@ The blume package remains importable.`);
16890
17027
  }
16891
17028
  });
16892
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
+
16893
17899
  // src/cli/commands/init.ts
16894
17900
  import * as clack from "@clack/prompts";
16895
- import { defineCommand as defineCommand8 } from "citty";
17901
+ import { defineCommand as defineCommand9 } from "citty";
16896
17902
  import { resolve as resolve10 } from "pathe";
16897
17903
 
16898
17904
  // src/cli/init/questions.ts
@@ -17007,7 +18013,7 @@ var ejectScaffold = async (root, answers) => {
17007
18013
  `);
17008
18014
  }
17009
18015
  };
17010
- var initCommand = defineCommand8({
18016
+ var initCommand = defineCommand9({
17011
18017
  args: {
17012
18018
  "content-dir": {
17013
18019
  description: "Content directory.",
@@ -17083,7 +18089,11 @@ var initCommand = defineCommand8({
17083
18089
  }
17084
18090
  const sink = interactive ? clack.log : logger;
17085
18091
  const { createdPackage } = await applyPlan(buildPlan(root, answers), sink);
17086
- const ignored = await ensureGitignore(root, [".blume/", "dist/"]);
18092
+ const ignored = await ensureGitignore(root, [
18093
+ "node_modules/",
18094
+ ".blume/",
18095
+ "dist/"
18096
+ ]);
17087
18097
  if (ignored.length > 0) {
17088
18098
  sink.success(`Added ${ignored.join(", ")} to .gitignore`);
17089
18099
  }
@@ -17101,12 +18111,223 @@ var initCommand = defineCommand8({
17101
18111
  }
17102
18112
  });
17103
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
+
17104
18325
  // src/cli/commands/preview.ts
17105
- import { existsSync as existsSync20 } from "node:fs";
18326
+ import { existsSync as existsSync21 } from "node:fs";
17106
18327
  import { preview } from "astro";
17107
- import { defineCommand as defineCommand9 } from "citty";
17108
- import { join as join34 } from "pathe";
17109
- var previewCommand = defineCommand9({
18328
+ import { defineCommand as defineCommand11 } from "citty";
18329
+ import { join as join38 } from "pathe";
18330
+ var previewCommand = defineCommand11({
17110
18331
  args: {
17111
18332
  host: { description: "Network host to bind.", type: "string" },
17112
18333
  port: { description: "Port to listen on.", type: "string" }
@@ -17119,7 +18340,7 @@ var previewCommand = defineCommand9({
17119
18340
  const root = process.cwd();
17120
18341
  const { config } = await loadConfig(root);
17121
18342
  const context = resolveProjectContext(root, config);
17122
- if (!existsSync20(join34(context.outDir, "astro.config.mjs"))) {
18343
+ if (!existsSync21(join38(context.outDir, "astro.config.mjs"))) {
17123
18344
  logger.error("No build found. Run `blume build` first.");
17124
18345
  process.exit(1);
17125
18346
  }
@@ -17136,9 +18357,9 @@ var previewCommand = defineCommand9({
17136
18357
 
17137
18358
  // src/cli/commands/sync.ts
17138
18359
  import { rm as rm4 } from "node:fs/promises";
17139
- import { defineCommand as defineCommand10 } from "citty";
17140
- import { join as join35 } from "pathe";
17141
- var syncCommand = defineCommand10({
18360
+ import { defineCommand as defineCommand12 } from "citty";
18361
+ import { join as join39 } from "pathe";
18362
+ var syncCommand = defineCommand12({
17142
18363
  args: {
17143
18364
  force: {
17144
18365
  description: "Clear the source cache before refetching.",
@@ -17159,7 +18380,7 @@ var syncCommand = defineCommand10({
17159
18380
  if (args.force) {
17160
18381
  const { config } = await loadConfig(root);
17161
18382
  const context = resolveProjectContext(root, config);
17162
- await rm4(join35(context.outDir, "cache"), { force: true, recursive: true });
18383
+ await rm4(join39(context.outDir, "cache"), { force: true, recursive: true });
17163
18384
  logger.info("Cleared source cache.");
17164
18385
  }
17165
18386
  const lock = readDevLock(resolveRuntimeDir(root));
@@ -17177,13 +18398,13 @@ var syncCommand = defineCommand10({
17177
18398
  });
17178
18399
 
17179
18400
  // src/cli/commands/validate.ts
17180
- import { existsSync as existsSync22 } from "node:fs";
17181
- import { defineCommand as defineCommand11 } from "citty";
17182
- 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";
17183
18404
 
17184
18405
  // src/core/links.ts
17185
- import { existsSync as existsSync21 } from "node:fs";
17186
- import { basename as basename7, join as join36 } from "pathe";
18406
+ import { existsSync as existsSync22 } from "node:fs";
18407
+ import { basename as basename7, join as join40 } from "pathe";
17187
18408
  var HTTP = /^https?:\/\//iu;
17188
18409
  var PROTOCOL_RELATIVE = /^\/\//u;
17189
18410
  var SCHEME = /^[a-z][a-z0-9+.-]*:/iu;
@@ -17196,7 +18417,7 @@ var decodePercent = (value) => {
17196
18417
  };
17197
18418
  var DOC_EXT = /\.(?:md|mdx)$/iu;
17198
18419
  var FILE_EXT = /\.[a-z0-9]+$/iu;
17199
- 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));
17200
18421
  var NUMERIC_PREFIX3 = /^\d+[-_.]/u;
17201
18422
  var isIndexPage = (page2) => /^index\.(?:md|mdx)$/iu.test(basename7(page2.navPath).replace(NUMERIC_PREFIX3, ""));
17202
18423
  var applyRelativePart = (segments, part) => {
@@ -17369,7 +18590,7 @@ var validateLinks = async (graph, options) => {
17369
18590
  };
17370
18591
 
17371
18592
  // src/cli/commands/validate.ts
17372
- var validateCommand = defineCommand11({
18593
+ var validateCommand = defineCommand13({
17373
18594
  args: {
17374
18595
  external: {
17375
18596
  description: "Check external (HTTP) links over the network.",
@@ -17407,12 +18628,12 @@ var validateCommand = defineCommand11({
17407
18628
  });
17408
18629
  extraRoutes.push(...manifest.routes.flatMap((route) => route.fallback ? [route.path] : []));
17409
18630
  }
17410
- const publicDir = join37(root, "public");
18631
+ const publicDir = join41(root, "public");
17411
18632
  diagnostics.push(...await validateLinks(project.graph, {
17412
18633
  basePath: project.config.basePath,
17413
18634
  checkExternal: Boolean(args.external),
17414
18635
  extraRoutes,
17415
- publicDir: existsSync22(publicDir) ? publicDir : null,
18636
+ publicDir: existsSync23(publicDir) ? publicDir : null,
17416
18637
  redirects: project.config.redirects
17417
18638
  }));
17418
18639
  } catch (error) {
@@ -17443,7 +18664,7 @@ var validateCommand = defineCommand11({
17443
18664
  });
17444
18665
 
17445
18666
  // src/cli/index.ts
17446
- var main = defineCommand12({
18667
+ var main = defineCommand14({
17447
18668
  meta: {
17448
18669
  description: "Markdown-first documentation powered by Astro and Vite.",
17449
18670
  name: "blume",
@@ -17457,7 +18678,9 @@ var main = defineCommand12({
17457
18678
  dev: devCommand,
17458
18679
  doctor: doctorCommand,
17459
18680
  eject: ejectCommand,
18681
+ eval: evalCommand,
17460
18682
  init: initCommand,
18683
+ "mcp-stdio": mcpStdioCommand,
17461
18684
  preview: previewCommand,
17462
18685
  sync: syncCommand,
17463
18686
  validate: validateCommand
@@ -17474,5 +18697,5 @@ process.on("unhandledRejection", (error) => {
17474
18697
  });
17475
18698
  runMain(main);
17476
18699
 
17477
- //# debugId=5D342B2E16EE6E0464756E2164756E21
18700
+ //# debugId=E4DFD2C6131D546564756E2164756E21
17478
18701
  //# sourceMappingURL=index.js.map