blume 1.7.1 → 1.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/cli/chunk-0qymqwzz.js +164 -0
  3. package/dist/cli/chunk-0qymqwzz.js.map +15 -0
  4. package/dist/cli/{chunk-8gnpdsn1.js → chunk-0xjyb285.js} +2 -2
  5. package/dist/cli/{chunk-12dzsn9b.js → chunk-1jefwnfs.js} +82 -81
  6. package/dist/cli/{chunk-12dzsn9b.js.map → chunk-1jefwnfs.js.map} +3 -3
  7. package/dist/cli/{chunk-27gtm2ym.js → chunk-2mzebbbz.js} +1 -1
  8. package/dist/cli/{chunk-s5e5jt53.js → chunk-2z47ypj8.js} +1 -1
  9. package/dist/cli/{chunk-jtb45atp.js → chunk-3r45185y.js} +10 -12
  10. package/dist/cli/{chunk-jtb45atp.js.map → chunk-3r45185y.js.map} +2 -2
  11. package/dist/cli/{chunk-6mq7qkve.js → chunk-4x36ddpw.js} +18 -24
  12. package/dist/cli/{chunk-6mq7qkve.js.map → chunk-4x36ddpw.js.map} +2 -2
  13. package/dist/cli/{chunk-he2zfgah.js → chunk-5093q3n7.js} +22 -30
  14. package/dist/cli/{chunk-he2zfgah.js.map → chunk-5093q3n7.js.map} +2 -2
  15. package/dist/cli/{chunk-k0v1f8bb.js → chunk-5g0w1e2c.js} +21 -28
  16. package/dist/cli/{chunk-k0v1f8bb.js.map → chunk-5g0w1e2c.js.map} +2 -2
  17. package/dist/cli/{chunk-5gfw0q4j.js → chunk-5qk08vmp.js} +26 -34
  18. package/dist/cli/{chunk-5gfw0q4j.js.map → chunk-5qk08vmp.js.map} +2 -2
  19. package/dist/cli/{chunk-r99hynxh.js → chunk-7s8hm3b6.js} +41 -9
  20. package/dist/cli/{chunk-r99hynxh.js.map → chunk-7s8hm3b6.js.map} +3 -3
  21. package/dist/cli/{chunk-vyqj481z.js → chunk-8cjtbafj.js} +68 -66
  22. package/dist/cli/chunk-8cjtbafj.js.map +13 -0
  23. package/dist/cli/{chunk-aqjvpd03.js → chunk-97r59kpr.js} +27 -33
  24. package/dist/cli/{chunk-aqjvpd03.js.map → chunk-97r59kpr.js.map} +2 -2
  25. package/dist/cli/{chunk-np8dmfb0.js → chunk-ahnw3kxw.js} +26 -33
  26. package/dist/cli/{chunk-np8dmfb0.js.map → chunk-ahnw3kxw.js.map} +2 -2
  27. package/dist/cli/{chunk-j5f2wrj5.js → chunk-b27xqwn9.js} +10 -15
  28. package/dist/cli/{chunk-j5f2wrj5.js.map → chunk-b27xqwn9.js.map} +2 -2
  29. package/dist/cli/{chunk-kmx2mydj.js → chunk-bf6bt1xt.js} +8 -8
  30. package/dist/cli/{chunk-kmx2mydj.js.map → chunk-bf6bt1xt.js.map} +1 -1
  31. package/dist/cli/{chunk-90pdhkpm.js → chunk-bvwwhd84.js} +23 -32
  32. package/dist/cli/{chunk-90pdhkpm.js.map → chunk-bvwwhd84.js.map} +2 -2
  33. package/dist/cli/{chunk-mfm4sjwx.js → chunk-cjtn640a.js} +32 -43
  34. package/dist/cli/{chunk-mfm4sjwx.js.map → chunk-cjtn640a.js.map} +2 -2
  35. package/dist/cli/{chunk-pxj10x8y.js → chunk-ct47dqpx.js} +14 -3
  36. package/dist/cli/{chunk-pxj10x8y.js.map → chunk-ct47dqpx.js.map} +4 -3
  37. package/dist/cli/{chunk-x66c5yjn.js → chunk-dwgcp5sm.js} +2 -2
  38. package/dist/cli/{chunk-4trphnvy.js → chunk-e7f42gdj.js} +10 -13
  39. package/dist/cli/{chunk-4trphnvy.js.map → chunk-e7f42gdj.js.map} +2 -2
  40. package/dist/cli/{chunk-82atea4k.js → chunk-esphfr8p.js} +14 -18
  41. package/dist/cli/{chunk-82atea4k.js.map → chunk-esphfr8p.js.map} +2 -2
  42. package/dist/cli/{chunk-q56730e0.js → chunk-ex56aa81.js} +53 -53
  43. package/dist/cli/chunk-ex56aa81.js.map +13 -0
  44. package/dist/cli/{chunk-ywn7t0pb.js → chunk-garjf5z9.js} +3 -3
  45. package/dist/cli/{chunk-ev67ycx0.js → chunk-jq5n4avg.js} +1 -1
  46. package/dist/cli/{chunk-ka5k7cz9.js → chunk-js7saxwm.js} +35 -39
  47. package/dist/cli/{chunk-ka5k7cz9.js.map → chunk-js7saxwm.js.map} +4 -6
  48. package/dist/cli/{chunk-x1wvw7a8.js → chunk-k79xp7av.js} +168 -208
  49. package/dist/cli/chunk-k79xp7av.js.map +39 -0
  50. package/dist/cli/{chunk-3r94j3tc.js → chunk-nn13znc2.js} +2 -2
  51. package/dist/cli/{chunk-4ae4f395.js → chunk-ps4m1xh4.js} +60 -35
  52. package/dist/cli/chunk-ps4m1xh4.js.map +15 -0
  53. package/dist/cli/{chunk-wd27zjcz.js → chunk-q4rae3bg.js} +1 -1
  54. package/dist/cli/{chunk-pdwg3q9g.js → chunk-rqy0s5wh.js} +21 -30
  55. package/dist/cli/{chunk-pdwg3q9g.js.map → chunk-rqy0s5wh.js.map} +2 -2
  56. package/dist/cli/{chunk-52cwcqvp.js → chunk-rz9jmfhz.js} +15 -24
  57. package/dist/cli/{chunk-52cwcqvp.js.map → chunk-rz9jmfhz.js.map} +2 -2
  58. package/dist/cli/{chunk-8p3xe5jv.js → chunk-vacwm2hv.js} +3 -3
  59. package/dist/cli/{chunk-cbjnx4s8.js → chunk-vh9w1sgp.js} +1 -1
  60. package/dist/cli/{chunk-sbdqrjbb.js → chunk-vrfp10qk.js} +1 -1
  61. package/dist/cli/{chunk-h9ekmtz7.js → chunk-yg63d42r.js} +28 -35
  62. package/dist/cli/{chunk-h9ekmtz7.js.map → chunk-yg63d42r.js.map} +2 -2
  63. package/dist/cli/{chunk-5hs6gb7n.js → chunk-yzhm0j9q.js} +1 -1
  64. package/dist/cli/index.js +397 -34
  65. package/dist/cli/index.js.map +12 -4
  66. package/dist/types/core/config-input.d.ts +59 -0
  67. package/dist/types/core/data.d.ts +2 -0
  68. package/dist/types/core/schema.d.ts +47 -3
  69. package/dist/types/core/types.d.ts +5 -0
  70. package/docs/configuration/ask-ai.mdx +61 -0
  71. package/docs/configuration/index.mdx +3 -1
  72. package/docs/content/navigation.mdx +3 -0
  73. package/docs/content/syntax.mdx +10 -0
  74. package/docs/discoverability/agent-discovery.mdx +82 -1
  75. package/docs/discoverability/index.mdx +1 -1
  76. package/docs/discoverability/llms-txt.mdx +1 -1
  77. package/docs/reference/frontmatter.mdx +2 -0
  78. package/package.json +1 -1
  79. package/src/ai/agent-readability.ts +5 -0
  80. package/src/ai/ai-catalog.ts +241 -0
  81. package/src/ai/cors.ts +87 -0
  82. package/src/ai/link-headers.ts +12 -0
  83. package/src/ai/llms.ts +6 -0
  84. package/src/ai/mcp/discovery.ts +1 -1
  85. package/src/astro/generate.ts +4 -0
  86. package/src/astro/templates.ts +88 -23
  87. package/src/cli/commands/build.ts +3 -1
  88. package/src/components/islands/hooks.ts +50 -1
  89. package/src/components/layout/NavTree.astro +75 -66
  90. package/src/components/layout/RootLayout.astro +28 -2
  91. package/src/components/layout/analytics-client.ts +36 -7
  92. package/src/components/layout/nav-utils.ts +17 -3
  93. package/src/core/adapter.ts +61 -0
  94. package/src/core/config-input.ts +60 -0
  95. package/src/core/data.ts +2 -0
  96. package/src/core/navigation.ts +22 -3
  97. package/src/core/schema.ts +88 -0
  98. package/src/core/types.ts +5 -0
  99. package/src/deploy/artifacts.ts +12 -1
  100. package/src/deploy/headers.ts +6 -0
  101. package/src/deploy/vercel-negotiation.ts +25 -2
  102. package/src/registry/eject.ts +2 -0
  103. package/src/search/build.ts +25 -3
  104. package/src/theme/entry.ts +23 -0
  105. package/dist/cli/chunk-2aj8ddew.js +0 -72
  106. package/dist/cli/chunk-2aj8ddew.js.map +0 -10
  107. package/dist/cli/chunk-4ae4f395.js.map +0 -15
  108. package/dist/cli/chunk-4xyggvgf.js +0 -21
  109. package/dist/cli/chunk-4xyggvgf.js.map +0 -10
  110. package/dist/cli/chunk-6kzzpsx8.js +0 -26
  111. package/dist/cli/chunk-6kzzpsx8.js.map +0 -10
  112. package/dist/cli/chunk-bcy492zc.js +0 -16
  113. package/dist/cli/chunk-bcy492zc.js.map +0 -10
  114. package/dist/cli/chunk-btfr9yvw.js +0 -41
  115. package/dist/cli/chunk-btfr9yvw.js.map +0 -10
  116. package/dist/cli/chunk-ey89bjj1.js +0 -209
  117. package/dist/cli/chunk-ey89bjj1.js.map +0 -11
  118. package/dist/cli/chunk-q56730e0.js.map +0 -13
  119. package/dist/cli/chunk-qvvpnwaz.js +0 -69
  120. package/dist/cli/chunk-qvvpnwaz.js.map +0 -11
  121. package/dist/cli/chunk-vt8fgygt.js +0 -23
  122. package/dist/cli/chunk-vt8fgygt.js.map +0 -10
  123. package/dist/cli/chunk-vxv4x1n8.js +0 -17
  124. package/dist/cli/chunk-vxv4x1n8.js.map +0 -10
  125. package/dist/cli/chunk-vyqj481z.js.map +0 -13
  126. package/dist/cli/chunk-x1wvw7a8.js.map +0 -40
  127. /package/dist/cli/{chunk-8gnpdsn1.js.map → chunk-0xjyb285.js.map} +0 -0
  128. /package/dist/cli/{chunk-27gtm2ym.js.map → chunk-2mzebbbz.js.map} +0 -0
  129. /package/dist/cli/{chunk-s5e5jt53.js.map → chunk-2z47ypj8.js.map} +0 -0
  130. /package/dist/cli/{chunk-x66c5yjn.js.map → chunk-dwgcp5sm.js.map} +0 -0
  131. /package/dist/cli/{chunk-ywn7t0pb.js.map → chunk-garjf5z9.js.map} +0 -0
  132. /package/dist/cli/{chunk-ev67ycx0.js.map → chunk-jq5n4avg.js.map} +0 -0
  133. /package/dist/cli/{chunk-3r94j3tc.js.map → chunk-nn13znc2.js.map} +0 -0
  134. /package/dist/cli/{chunk-wd27zjcz.js.map → chunk-q4rae3bg.js.map} +0 -0
  135. /package/dist/cli/{chunk-8p3xe5jv.js.map → chunk-vacwm2hv.js.map} +0 -0
  136. /package/dist/cli/{chunk-cbjnx4s8.js.map → chunk-vh9w1sgp.js.map} +0 -0
  137. /package/dist/cli/{chunk-sbdqrjbb.js.map → chunk-vrfp10qk.js.map} +0 -0
  138. /package/dist/cli/{chunk-5hs6gb7n.js.map → chunk-yzhm0j9q.js.map} +0 -0
@@ -767,6 +767,19 @@ const searchConfigSchema = z
767
767
  }
768
768
  });
769
769
 
770
+ /**
771
+ * The `ai.ask.reasoning` levels: the AI SDK's top-level `reasoning` values
772
+ * minus `provider-default`, which is what omitting the field means.
773
+ */
774
+ export const askReasoningLevels = [
775
+ "none",
776
+ "minimal",
777
+ "low",
778
+ "medium",
779
+ "high",
780
+ "xhigh",
781
+ ] as const;
782
+
770
783
  /** Ask AI backends. `gateway` (default) routes through the Vercel AI Gateway. */
771
784
  export const askAiProviders = [
772
785
  "gateway",
@@ -856,6 +869,23 @@ const llmsTxtObjectSchema = z.strictObject({
856
869
 
857
870
  type LlmsTxtResolved = z.output<typeof llmsTxtObjectSchema>;
858
871
 
872
+ /** The object form of `ai.catalog`; a bare boolean normalizes onto it. */
873
+ const aiCatalogObjectSchema = z.strictObject({
874
+ enabled: z.boolean().default(true),
875
+ /**
876
+ * Representative queries per catalog entry, keyed by the entry's
877
+ * `<namespace>:<name>` (the identifier minus its `urn:air:<host>:` prefix,
878
+ * e.g. `mcp:docs`, `skill:blume`, `api:docs`). Replaces the generated
879
+ * defaults for that entry; 2–5 short natural-language questions the
880
+ * resource can answer, which registries embed for semantic search.
881
+ */
882
+ queries: z
883
+ .record(z.string().min(1), z.array(z.string().trim().min(1)).min(1))
884
+ .default({}),
885
+ });
886
+
887
+ type AiCatalogResolved = z.output<typeof aiCatalogObjectSchema>;
888
+
859
889
  const aiConfigSchema = z.strictObject({
860
890
  /**
861
891
  * The JSON docs API: the page index, per-page JSON, and navigation under
@@ -872,6 +902,21 @@ const aiConfigSchema = z.strictObject({
872
902
  // Base URL of the backend. Required for `openai-compatible` only when no
873
903
  // external endpoint is supplied; for named providers it overrides the preset.
874
904
  baseUrl: z.url().optional(),
905
+ // Origins allowed to call the generated `/api/ask` from another site (a
906
+ // marketing page that embeds an ask box, say), or `"*"` for every
907
+ // origin. Each URL is reduced to its origin so a trailing slash or path
908
+ // can't defeat the exact match the route performs. Read by the
909
+ // generated route only; an external `endpoint` owns its own CORS.
910
+ cors: z
911
+ .array(
912
+ z.union([
913
+ z.literal("*"),
914
+ z
915
+ .url({ protocol: /^https?$/u })
916
+ .transform((value) => new URL(value).origin),
917
+ ])
918
+ )
919
+ .optional(),
875
920
  enabled: z.boolean().default(false),
876
921
  // Optional external endpoint for projects that keep their docs static
877
922
  // and host Ask AI in an existing backend. Absolute URLs and root-relative
@@ -888,6 +933,12 @@ const aiConfigSchema = z.strictObject({
888
933
  instructions: z.string().trim().min(1).optional(),
889
934
  model: z.string().default("openai/gpt-5.5"),
890
935
  provider: z.enum(askAiProviders).default("gateway"),
936
+ // How much the model reasons before answering, sent as the backend's
937
+ // own reasoning-effort control (see `askEndpointTemplate`). Omitted
938
+ // keeps the provider's default; `none` is the fastest and cheapest for
939
+ // grounded docs Q&A, where the excerpts carry the answer. Not for
940
+ // Inkeep, which has no such control (refined below).
941
+ reasoning: z.enum(askReasoningLevels).optional(),
891
942
  // How much documentation each question carries. Injected characters are
892
943
  // the dominant term in time-to-first-token on a self-hosted backend, so
893
944
  // these trade recall for latency. No zod defaults here: only what the
@@ -926,8 +977,44 @@ const aiConfigSchema = z.strictObject({
926
977
  path: ["baseUrl"],
927
978
  });
928
979
  }
980
+ // `cors` configures the generated route, which an external `endpoint`
981
+ // replaces; accepting both would silently do nothing.
982
+ if (value.cors && value.endpoint) {
983
+ ctx.addIssue({
984
+ code: z.ZodIssueCode.custom,
985
+ message:
986
+ "ai.ask.cors only applies to the generated route; with ai.ask.endpoint, CORS is that backend's job.",
987
+ path: ["cors"],
988
+ });
989
+ }
990
+ // Inkeep runs its own QA pipeline behind an OpenAI-compatible endpoint
991
+ // with no reasoning control; a level would only reach it as an
992
+ // unsupported `reasoning_effort`, so refuse it up front.
993
+ if (value.provider === "inkeep" && value.reasoning) {
994
+ ctx.addIssue({
995
+ code: z.ZodIssueCode.custom,
996
+ message:
997
+ 'ai.ask.reasoning is not supported when provider is "inkeep".',
998
+ path: ["reasoning"],
999
+ });
1000
+ }
929
1001
  })
930
1002
  .optional(),
1003
+ /**
1004
+ * The AI Catalog / ARD manifest at `/.well-known/ai-catalog.json` (mirrored
1005
+ * at `/.well-known/ard.json`): one entry per agent-facing resource the site
1006
+ * publishes — the MCP server card, each agent skill, the JSON docs API's
1007
+ * OpenAPI document, each rendered API reference, and llms.txt — so agent
1008
+ * registries can index the site from its domain alone. Needs a
1009
+ * `deployment.site` (identifiers are domain-anchored URNs). On by default;
1010
+ * the object form overrides the generated representative queries.
1011
+ */
1012
+ catalog: z
1013
+ .union([z.boolean(), aiCatalogObjectSchema])
1014
+ .default(true)
1015
+ .transform((value): AiCatalogResolved =>
1016
+ isBoolean(value) ? { enabled: value, queries: {} } : value
1017
+ ),
931
1018
  /**
932
1019
  * `llms.txt`/`llms-full.txt` emission. A bare boolean toggles it; the object
933
1020
  * form adds `openapi: false` to keep generated API reference pages out of
@@ -1078,6 +1165,7 @@ const navigationConfigSchema = z.strictObject({
1078
1165
  });
1079
1166
 
1080
1167
  export type AskAiProvider = (typeof askAiProviders)[number];
1168
+ export type AskReasoning = (typeof askReasoningLevels)[number];
1081
1169
  export type AskAiConfig = NonNullable<z.infer<typeof aiConfigSchema>["ask"]>;
1082
1170
  export { openInChatProviders } from "./open-in-chat.ts";
1083
1171
  export type { OpenInChatProvider } from "./open-in-chat.ts";
package/src/core/types.ts CHANGED
@@ -220,6 +220,11 @@ export type NavNode =
220
220
  */
221
221
  display: SidebarDisplay;
222
222
  icon?: string;
223
+ /**
224
+ * The group row's link: an explicit-config group's `root`, or the
225
+ * generated folder's index page route. Absent when there is no page at
226
+ * the group's own path, so the row never links to a 404.
227
+ */
223
228
  route?: string;
224
229
  /**
225
230
  * The group's URL path (its folder route prefix), even when the folder
@@ -4,6 +4,11 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
4
4
  import { dirname, join, resolve } from "pathe";
5
5
 
6
6
  import { buildAgentReadability } from "../ai/agent-readability.ts";
7
+ import {
8
+ AI_CATALOG_PATH,
9
+ ARD_MANIFEST_PATH,
10
+ buildAiCatalog,
11
+ } from "../ai/ai-catalog.ts";
7
12
  import { API_CATALOG_PATH, buildApiCatalog } from "../ai/api-catalog.ts";
8
13
  import { buildHomeLinkHeader } from "../ai/link-headers.ts";
9
14
  import { buildLlmsFiles } from "../ai/llms.ts";
@@ -234,8 +239,12 @@ const emitAgentSkills = async (
234
239
  const emitWellKnownFiles = async (
235
240
  config: ResolvedConfig,
236
241
  distDir: string,
242
+ skills: readonly SkillArtifact[],
237
243
  logger: ArtifactLogger
238
244
  ): Promise<void> => {
245
+ // One document, two paths: the ai-catalog spec's well-known URI and the
246
+ // ARD v0.91 one (see `ai/ai-catalog.ts`).
247
+ const aiCatalog = buildAiCatalog(config, skills);
239
248
  const files = [
240
249
  {
241
250
  content: buildSignaturesDirectory(config),
@@ -247,6 +256,8 @@ const emitWellKnownFiles = async (
247
256
  label: "RFC 9727",
248
257
  path: API_CATALOG_PATH,
249
258
  },
259
+ { content: aiCatalog, label: "AI Catalog", path: AI_CATALOG_PATH },
260
+ { content: aiCatalog, label: "ARD manifest", path: ARD_MANIFEST_PATH },
250
261
  ];
251
262
  for (const file of files) {
252
263
  const target = join(distDir, file.path.slice(1));
@@ -362,7 +373,7 @@ export const publishBuildArtifacts = async (
362
373
  logger.info("Generated agent-readability.json");
363
374
  }
364
375
 
365
- await emitWellKnownFiles(project.config, distDir, logger);
376
+ await emitWellKnownFiles(project.config, distDir, skills, logger);
366
377
  await emitAgentSkills(project, distDir, skills, logger);
367
378
 
368
379
  await emitRedirectFiles(project.config, distDir, logger);
@@ -1,3 +1,4 @@
1
+ import { crossOriginDiscoveryPaths } from "../ai/ai-catalog.ts";
1
2
  import {
2
3
  API_CATALOG_PATH,
3
4
  API_CATALOG_TYPE,
@@ -95,6 +96,11 @@ export const buildNetlifyHeaders = (
95
96
  `${deployBase}${SIGNATURES_DIRECTORY_PATH}\n Content-Type: ${SIGNATURES_DIRECTORY_TYPE}`
96
97
  );
97
98
  }
99
+ // Agent registries fetch the discovery documents cross-origin; a static
100
+ // host sends no CORS header unless told to.
101
+ for (const path of crossOriginDiscoveryPaths(config)) {
102
+ rules.push(`${deployBase}${path}\n Access-Control-Allow-Origin: *`);
103
+ }
98
104
  // Published skills live at the deployment base, outside `basePath` — the
99
105
  // `.md` charset rule above misses them whenever a basePath is set, and the
100
106
  // RFC wants archives served as application/gzip explicitly.
@@ -235,6 +235,26 @@ export const buildNegotiationRoutes = (
235
235
  /** The `src` of the injected homepage `Link` header route. */
236
236
  const HOME_SRC = "^/$";
237
237
 
238
+ const ALLOW_ANY_ORIGIN = "*";
239
+
240
+ /**
241
+ * A main-phase `continue` route stamping `Access-Control-Allow-Origin: *` on
242
+ * one static discovery document (see `crossOriginDiscoveryPaths`), so a
243
+ * registry reading it from another origin isn't refused by the browser.
244
+ */
245
+ const corsRoute = (path: string): VercelRoute => ({
246
+ continue: true,
247
+ headers: { "access-control-allow-origin": ALLOW_ANY_ORIGIN },
248
+ src: `^${routePattern(path)}$`,
249
+ });
250
+
251
+ /** Whether a route is a `corsRoute` — the same three-field shape test as the others. */
252
+ const isCorsRoute = (route: VercelRoute): boolean =>
253
+ route.continue === true &&
254
+ route.headers?.["access-control-allow-origin"] === ALLOW_ANY_ORIGIN &&
255
+ isString(route.src) &&
256
+ Object.keys(route).length === 3;
257
+
238
258
  /**
239
259
  * Whether a route is one this module previously injected, so re-injection
240
260
  * replaces rather than duplicates. Rewrites are identified by their `accept`
@@ -258,7 +278,8 @@ const isNegotiationRoute = (route: VercelRoute): boolean =>
258
278
  (route.continue === true &&
259
279
  isString(route.headers?.link) &&
260
280
  route.src === HOME_SRC &&
261
- Object.keys(route).length === 3);
281
+ Object.keys(route).length === 3) ||
282
+ isCorsRoute(route);
262
283
 
263
284
  /**
264
285
  * Splice the negotiation routes into a Build Output `config.json`, plus — when
@@ -287,7 +308,8 @@ export const injectNegotiationRoutes = (
287
308
  homeLinkHeader?: string | null,
288
309
  contentTypeOverrides?: Record<string, string>,
289
310
  homeTokens?: number,
290
- notFound: NotFoundVariants = {}
311
+ notFound: NotFoundVariants = {},
312
+ corsPaths: readonly string[] = []
291
313
  ): string | null => {
292
314
  const overrideEntries = Object.entries(contentTypeOverrides ?? {});
293
315
  let config: {
@@ -325,6 +347,7 @@ export const injectNegotiationRoutes = (
325
347
  src: HOME_SRC,
326
348
  });
327
349
  }
350
+ headerRoutes.push(...corsPaths.map(corsRoute));
328
351
  // Headers first: `continue` routes accumulate, so a request the rewrite
329
352
  // route then terminates (Markdown negotiation on the homepage) still carries
330
353
  // the Link header.
@@ -137,7 +137,9 @@ const askFiles = async (
137
137
  const files = [
138
138
  {
139
139
  content: askEndpointTemplate(resolveAskBackend(ask), grounded, {
140
+ cors: ask.cors,
140
141
  instructions: ask.instructions,
142
+ reasoning: ask.reasoning,
141
143
  retrieval: ask.retrieval,
142
144
  }),
143
145
  path: join(srcDir, "pages", "api", "ask.ts"),
@@ -1,4 +1,6 @@
1
- import { join } from "pathe";
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+
3
+ import { dirname, join } from "pathe";
2
4
 
3
5
  /**
4
6
  * Build a local Pagefind search index over the built site. Pagefind indexes
@@ -6,6 +8,15 @@ import { join } from "pathe";
6
8
  * `data-pagefind-ignore`, which Blume stamps on non-indexable pages
7
9
  * (search-excluded, or hidden without the opt-in), so those stay out.
8
10
  *
11
+ * The index files are fetched with `getFiles()` and written here rather than
12
+ * with Pagefind's `writeFiles()`: in service mode Pagefind acknowledges
13
+ * `writeFiles()` once tokio has buffered the writes, not once they are on
14
+ * disk, and `close()` kills the backend, so a loaded CI host can ship a
15
+ * truncated `pagefind-entry.json` (Pagefind/pagefind#1271). Writing the bytes
16
+ * ourselves puts them on disk before the backend goes away. Fixed upstream in
17
+ * Pagefind/pagefind#1272; switch back to `writeFiles()` once a release ships
18
+ * that fix.
19
+ *
9
20
  * Returns the number of pages indexed.
10
21
  */
11
22
  export const buildSearchIndex = async (outDir: string): Promise<number> => {
@@ -17,11 +28,22 @@ export const buildSearchIndex = async (outDir: string): Promise<number> => {
17
28
  }
18
29
 
19
30
  // These awaits are strictly ordered, not independent: the directory must be
20
- // indexed before its files are written, and the index closed only after.
31
+ // indexed before its files are read, and the index closed only after.
21
32
  // oxlint-disable-next-line react-doctor/async-parallel
22
33
  const result = await index.addDirectory({ path: outDir });
23
- await index.writeFiles({ outputPath: join(outDir, "pagefind") });
34
+ const { files } = await index.getFiles();
24
35
  await pagefind.close();
25
36
 
37
+ const searchDir = join(outDir, "pagefind");
38
+ const directories = new Set(
39
+ files.map((file) => dirname(join(searchDir, file.path)))
40
+ );
41
+ await Promise.all(
42
+ [...directories].map((directory) => mkdir(directory, { recursive: true }))
43
+ );
44
+ await Promise.all(
45
+ files.map((file) => writeFile(join(searchDir, file.path), file.content))
46
+ );
47
+
26
48
  return result.page_count;
27
49
  };
@@ -298,6 +298,11 @@ ${THEME_MAPPING}
298
298
  --tw-prose-pre-bg: var(--blume-code-background);
299
299
  --tw-prose-th-borders: var(--blume-border);
300
300
  --tw-prose-td-borders: var(--blume-border);
301
+ /* Neutralized: the plugin defaults are near-black text and shadow with no
302
+ dark counterpart (Blume never applies prose-invert), and the kbd rule
303
+ below draws the badge with Blume tokens instead. */
304
+ --tw-prose-kbd: inherit;
305
+ --tw-prose-kbd-shadows: transparent;
301
306
  color: var(--blume-muted-foreground);
302
307
  font-size: 0.875rem;
303
308
  line-height: 1.7;
@@ -827,6 +832,24 @@ pre:has(.line.focused):hover .line:not(.focused) {
827
832
  content: none;
828
833
  }
829
834
 
835
+ /* Keyboard keys (<kbd>) render as the same bordered badge the search dialog
836
+ uses for its shortcut hints. Typography's own kbd rule is scoped with
837
+ :not(.not-prose *), so it never reaches a key inside <Steps>, <Callout>,
838
+ <Card>, or any other not-prose component; this rule is deliberately
839
+ unscoped from that exclusion so a key looks the same everywhere in the
840
+ content column. Unlayered, so it outranks the plugin's layered rule. */
841
+ .prose kbd {
842
+ background: var(--blume-muted);
843
+ border: 1px solid var(--blume-border);
844
+ border-radius: 0.25rem;
845
+ box-shadow: none;
846
+ color: inherit;
847
+ font-family: var(--font-mono);
848
+ font-size: 0.85em;
849
+ font-weight: 500;
850
+ padding: 0.125rem 0.25rem;
851
+ }
852
+
830
853
  /* Inline code highlighting: Shiki colors the tokens of a \`code\`{:lang} snippet
831
854
  via the same dual-theme CSS variables as fenced blocks, keeping the inline
832
855
  pill background. Always on — it only fires on the trailing {:lang} marker. */
@@ -1,72 +0,0 @@
1
- #!/usr/bin/env node
2
- import { createRequire } from "node:module";
3
- var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
-
5
- // src/cli/command-meta.ts
6
- var commandMeta = {
7
- add: {
8
- description: "Install a source component or template from the registry.",
9
- name: "add"
10
- },
11
- audit: {
12
- description: "Audit the built site for SEO and site-health issues.",
13
- name: "audit"
14
- },
15
- build: {
16
- description: "Build the docs site for production.",
17
- name: "build"
18
- },
19
- check: {
20
- description: "Type-check the docs site with astro check.",
21
- name: "check"
22
- },
23
- dev: {
24
- description: "Start the Blume development server.",
25
- name: "dev"
26
- },
27
- doctor: {
28
- description: "Diagnose common configuration and content problems.",
29
- name: "doctor"
30
- },
31
- eject: {
32
- description: "Promote the generated runtime into an owned Astro project.",
33
- name: "eject"
34
- },
35
- eval: {
36
- description: "Test the docs: an agent answers your questions using only the documentation.",
37
- name: "eval"
38
- },
39
- init: {
40
- description: "Scaffold a minimal Blume project.",
41
- name: "init"
42
- },
43
- "mcp-stdio": {
44
- description: "Serve an MCP data snapshot over stdio (internal, used by `blume eval`).",
45
- name: "mcp-stdio"
46
- },
47
- preview: {
48
- description: "Preview the last production build.",
49
- name: "preview"
50
- },
51
- sync: {
52
- description: "Re-fetch remote content sources and regenerate the runtime.",
53
- name: "sync"
54
- },
55
- translate: {
56
- description: "Translate docs into the configured locales with a local agent CLI.",
57
- name: "translate"
58
- },
59
- validate: {
60
- description: "Validate internal, anchor, asset, and external links.",
61
- name: "validate"
62
- },
63
- version: {
64
- description: "Freeze the current docs as an archived version.",
65
- name: "version"
66
- }
67
- };
68
-
69
- export { __require, commandMeta };
70
-
71
- //# debugId=3DF60EB79641093164756E2164756E21
72
- //# sourceMappingURL=chunk-2aj8ddew.js.map
@@ -1,10 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../src/cli/command-meta.ts"],
4
- "sourcesContent": [
5
- "import type { CommandMeta } from \"citty\";\n\n/**\n * Every command's `meta`, held apart from the command modules themselves.\n *\n * The CLI entry loads each command lazily (see `lazy-command.ts`), but citty\n * still reads every subcommand's `meta` to render `blume --help` and to match\n * an unknown name against aliases. Keeping that table here lets those paths\n * run without importing a single command module — `dev` alone drags in Astro,\n * `mcp-stdio` the MCP SDK. The command modules read their `meta` from this\n * table too, so the entry and the command can't drift.\n */\nexport const commandMeta = {\n add: {\n description: \"Install a source component or template from the registry.\",\n name: \"add\",\n },\n audit: {\n description: \"Audit the built site for SEO and site-health issues.\",\n name: \"audit\",\n },\n build: {\n description: \"Build the docs site for production.\",\n name: \"build\",\n },\n check: {\n description: \"Type-check the docs site with astro check.\",\n name: \"check\",\n },\n dev: {\n description: \"Start the Blume development server.\",\n name: \"dev\",\n },\n doctor: {\n description: \"Diagnose common configuration and content problems.\",\n name: \"doctor\",\n },\n eject: {\n description: \"Promote the generated runtime into an owned Astro project.\",\n name: \"eject\",\n },\n eval: {\n description:\n \"Test the docs: an agent answers your questions using only the documentation.\",\n name: \"eval\",\n },\n init: {\n description: \"Scaffold a minimal Blume project.\",\n name: \"init\",\n },\n \"mcp-stdio\": {\n description:\n \"Serve an MCP data snapshot over stdio (internal, used by `blume eval`).\",\n name: \"mcp-stdio\",\n },\n preview: {\n description: \"Preview the last production build.\",\n name: \"preview\",\n },\n sync: {\n description: \"Re-fetch remote content sources and regenerate the runtime.\",\n name: \"sync\",\n },\n translate: {\n description:\n \"Translate docs into the configured locales with a local agent CLI.\",\n name: \"translate\",\n },\n validate: {\n description: \"Validate internal, anchor, asset, and external links.\",\n name: \"validate\",\n },\n version: {\n description: \"Freeze the current docs as an archived version.\",\n name: \"version\",\n },\n} satisfies Record<string, CommandMeta>;\n"
6
- ],
7
- "mappings": ";;;;;AAYO,IAAM,cAAc;AAAA,EACzB,KAAK;AAAA,IACH,aAAa;AAAA,IACb,MAAM;AAAA,EACR;AAAA,EACA,OAAO;AAAA,IACL,aAAa;AAAA,IACb,MAAM;AAAA,EACR;AAAA,EACA,OAAO;AAAA,IACL,aAAa;AAAA,IACb,MAAM;AAAA,EACR;AAAA,EACA,OAAO;AAAA,IACL,aAAa;AAAA,IACb,MAAM;AAAA,EACR;AAAA,EACA,KAAK;AAAA,IACH,aAAa;AAAA,IACb,MAAM;AAAA,EACR;AAAA,EACA,QAAQ;AAAA,IACN,aAAa;AAAA,IACb,MAAM;AAAA,EACR;AAAA,EACA,OAAO;AAAA,IACL,aAAa;AAAA,IACb,MAAM;AAAA,EACR;AAAA,EACA,MAAM;AAAA,IACJ,aACE;AAAA,IACF,MAAM;AAAA,EACR;AAAA,EACA,MAAM;AAAA,IACJ,aAAa;AAAA,IACb,MAAM;AAAA,EACR;AAAA,EACA,aAAa;AAAA,IACX,aACE;AAAA,IACF,MAAM;AAAA,EACR;AAAA,EACA,SAAS;AAAA,IACP,aAAa;AAAA,IACb,MAAM;AAAA,EACR;AAAA,EACA,MAAM;AAAA,IACJ,aAAa;AAAA,IACb,MAAM;AAAA,EACR;AAAA,EACA,WAAW;AAAA,IACT,aACE;AAAA,IACF,MAAM;AAAA,EACR;AAAA,EACA,UAAU;AAAA,IACR,aAAa;AAAA,IACb,MAAM;AAAA,EACR;AAAA,EACA,SAAS;AAAA,IACP,aAAa;AAAA,IACb,MAAM;AAAA,EACR;AACF;",
8
- "debugId": "3DF60EB79641093164756E2164756E21",
9
- "names": []
10
- }