blume 0.0.0 → 0.1.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 (263) hide show
  1. package/bin/blume.mjs +18 -0
  2. package/dist/cli/index.js +11989 -0
  3. package/dist/cli/index.js.map +141 -0
  4. package/docs/01-quickstart.mdx +99 -0
  5. package/docs/02-deployment.mdx +129 -0
  6. package/docs/advanced/api-reference.mdx +114 -0
  7. package/docs/advanced/blog.mdx +121 -0
  8. package/docs/advanced/changelog.mdx +113 -0
  9. package/docs/advanced/custom-pages.mdx +187 -0
  10. package/docs/advanced/meta.ts +7 -0
  11. package/docs/changelog/v0-1-0.mdx +12 -0
  12. package/docs/changelog/v0-2-0.mdx +16 -0
  13. package/docs/configuration/ai.mdx +228 -0
  14. package/docs/configuration/analytics.mdx +98 -0
  15. package/docs/configuration/customization.mdx +91 -0
  16. package/docs/configuration/export.mdx +70 -0
  17. package/docs/configuration/index.mdx +271 -0
  18. package/docs/configuration/meta.ts +15 -0
  19. package/docs/configuration/search.mdx +172 -0
  20. package/docs/configuration/seo.mdx +196 -0
  21. package/docs/configuration/theming.mdx +178 -0
  22. package/docs/content/components.mdx +565 -0
  23. package/docs/content/i18n.mdx +205 -0
  24. package/docs/content/index.mdx +161 -0
  25. package/docs/content/islands.mdx +94 -0
  26. package/docs/content/meta.mdx +119 -0
  27. package/docs/content/meta.ts +15 -0
  28. package/docs/content/navigation.mdx +168 -0
  29. package/docs/content/sources.mdx +216 -0
  30. package/docs/content/syntax.mdx +445 -0
  31. package/docs/index.mdx +112 -0
  32. package/docs/reference/cli.mdx +43 -0
  33. package/docs/reference/frontmatter.mdx +74 -0
  34. package/docs/reference/meta.ts +7 -0
  35. package/package.json +140 -6
  36. package/src/ai/ask.ts +93 -0
  37. package/src/ai/llms.ts +65 -0
  38. package/src/ai/markdown.ts +31 -0
  39. package/src/ai/mcp/data.ts +74 -0
  40. package/src/ai/mcp/discovery.ts +49 -0
  41. package/src/ai/mcp/server.ts +225 -0
  42. package/src/ai/mcp/tools.ts +47 -0
  43. package/src/assets/icon.png +0 -0
  44. package/src/astro/generate.ts +878 -0
  45. package/src/astro/index.ts +4 -0
  46. package/src/astro/integration.ts +74 -0
  47. package/src/astro/islands.ts +131 -0
  48. package/src/astro/markdown-negotiation.ts +68 -0
  49. package/src/astro/pages.ts +28 -0
  50. package/src/astro/templates.ts +1199 -0
  51. package/src/cli/commands/add.ts +81 -0
  52. package/src/cli/commands/build.ts +103 -0
  53. package/src/cli/commands/dev.ts +108 -0
  54. package/src/cli/commands/doctor.ts +74 -0
  55. package/src/cli/commands/eject.ts +57 -0
  56. package/src/cli/commands/init.ts +98 -0
  57. package/src/cli/commands/migrate.ts +39 -0
  58. package/src/cli/commands/preview.ts +39 -0
  59. package/src/cli/commands/sync.ts +52 -0
  60. package/src/cli/commands/validate.ts +60 -0
  61. package/src/cli/index.ts +35 -0
  62. package/src/cli/log.ts +37 -0
  63. package/src/cli/prepare.ts +80 -0
  64. package/src/components/Icon.astro +99 -0
  65. package/src/components/content/Accordion.astro +8 -0
  66. package/src/components/content/AccordionItem.astro +121 -0
  67. package/src/components/content/AutoTypeTable.astro +51 -0
  68. package/src/components/content/Badge.astro +124 -0
  69. package/src/components/content/Callout.astro +73 -0
  70. package/src/components/content/Card.astro +104 -0
  71. package/src/components/content/CardGroup.astro +14 -0
  72. package/src/components/content/CodeGroup.astro +13 -0
  73. package/src/components/content/Color.astro +15 -0
  74. package/src/components/content/ColorItem.astro +87 -0
  75. package/src/components/content/ColorRow.astro +10 -0
  76. package/src/components/content/Column.astro +6 -0
  77. package/src/components/content/Columns.astro +9 -0
  78. package/src/components/content/Expandable.astro +11 -0
  79. package/src/components/content/FileTree.astro +8 -0
  80. package/src/components/content/Frame.astro +70 -0
  81. package/src/components/content/GithubInfo.astro +110 -0
  82. package/src/components/content/Math.astro +24 -0
  83. package/src/components/content/Panel.astro +20 -0
  84. package/src/components/content/Prompt.astro +129 -0
  85. package/src/components/content/Step.astro +34 -0
  86. package/src/components/content/Steps.astro +20 -0
  87. package/src/components/content/Tab.astro +40 -0
  88. package/src/components/content/Tabs.astro +273 -0
  89. package/src/components/content/Tile.astro +42 -0
  90. package/src/components/content/Tooltip.astro +68 -0
  91. package/src/components/content/Tree.astro +300 -0
  92. package/src/components/content/TreeFile.astro +15 -0
  93. package/src/components/content/TreeFolder.astro +62 -0
  94. package/src/components/content/TypeTable.astro +106 -0
  95. package/src/components/content/Update.astro +66 -0
  96. package/src/components/content/Visibility.astro +12 -0
  97. package/src/components/content/Warning.astro +9 -0
  98. package/src/components/content/auto-type-table.ts +141 -0
  99. package/src/components/content/github-info.ts +79 -0
  100. package/src/components/content/mermaid-element.ts +68 -0
  101. package/src/components/github-mark.ts +9 -0
  102. package/src/components/index.ts +14 -0
  103. package/src/components/islands/AskAI.astro +12 -0
  104. package/src/components/islands/ask-ai.tsx +156 -0
  105. package/src/components/layout/Analytics.astro +63 -0
  106. package/src/components/layout/Banner.astro +50 -0
  107. package/src/components/layout/Breadcrumbs.astro +31 -0
  108. package/src/components/layout/Favicon.astro +15 -0
  109. package/src/components/layout/Fonts.astro +14 -0
  110. package/src/components/layout/Header.astro +188 -0
  111. package/src/components/layout/LanguageSwitcher.astro +56 -0
  112. package/src/components/layout/NavTree.astro +462 -0
  113. package/src/components/layout/PageActions.astro +438 -0
  114. package/src/components/layout/PageFeedback.astro +58 -0
  115. package/src/components/layout/Pagination.astro +56 -0
  116. package/src/components/layout/ReferenceLayout.astro +102 -0
  117. package/src/components/layout/RootLayout.astro +533 -0
  118. package/src/components/layout/Search.astro +608 -0
  119. package/src/components/layout/TableOfContents.astro +68 -0
  120. package/src/components/layout/analytics-client.ts +38 -0
  121. package/src/components/layout/nav-utils.ts +87 -0
  122. package/src/components/layout/overrides.ts +32 -0
  123. package/src/components/layout/search/algolia.ts +43 -0
  124. package/src/components/layout/search/endpoint.ts +22 -0
  125. package/src/components/layout/search/flexsearch.ts +52 -0
  126. package/src/components/layout/search/orama-cloud.ts +41 -0
  127. package/src/components/layout/search/orama.ts +26 -0
  128. package/src/components/layout/search/pagefind.ts +43 -0
  129. package/src/components/layout/search/types.ts +163 -0
  130. package/src/components/layout/search/typesense.ts +60 -0
  131. package/src/components/layout/toc-element.ts +108 -0
  132. package/src/core/bridge.ts +92 -0
  133. package/src/core/config.ts +112 -0
  134. package/src/core/content.ts +50 -0
  135. package/src/core/define-components.ts +34 -0
  136. package/src/core/define-meta.ts +20 -0
  137. package/src/core/deployment-env.ts +73 -0
  138. package/src/core/diagnostics.ts +104 -0
  139. package/src/core/graph.ts +128 -0
  140. package/src/core/i18n-ui.ts +171 -0
  141. package/src/core/i18n.ts +169 -0
  142. package/src/core/last-modified.ts +88 -0
  143. package/src/core/links.ts +336 -0
  144. package/src/core/load-module.ts +15 -0
  145. package/src/core/manifest.ts +126 -0
  146. package/src/core/meta.ts +97 -0
  147. package/src/core/navigation.ts +392 -0
  148. package/src/core/package-root.ts +37 -0
  149. package/src/core/project-graph.ts +153 -0
  150. package/src/core/project.ts +56 -0
  151. package/src/core/schema.ts +1057 -0
  152. package/src/core/server-features.ts +23 -0
  153. package/src/core/sources/assets.ts +77 -0
  154. package/src/core/sources/cache.ts +122 -0
  155. package/src/core/sources/filesystem.ts +99 -0
  156. package/src/core/sources/mdx-remote.ts +216 -0
  157. package/src/core/sources/mintlify.ts +161 -0
  158. package/src/core/sources/normalize.ts +227 -0
  159. package/src/core/sources/notion.ts +440 -0
  160. package/src/core/sources/portable-text.ts +143 -0
  161. package/src/core/sources/read.ts +36 -0
  162. package/src/core/sources/resolve.ts +158 -0
  163. package/src/core/sources/sanity.ts +218 -0
  164. package/src/core/sources/types.ts +105 -0
  165. package/src/core/types.ts +261 -0
  166. package/src/core/ui-packs/ar.ts +47 -0
  167. package/src/core/ui-packs/bg.ts +47 -0
  168. package/src/core/ui-packs/bn.ts +47 -0
  169. package/src/core/ui-packs/ca.ts +47 -0
  170. package/src/core/ui-packs/cs.ts +47 -0
  171. package/src/core/ui-packs/da.ts +47 -0
  172. package/src/core/ui-packs/de.ts +47 -0
  173. package/src/core/ui-packs/el.ts +47 -0
  174. package/src/core/ui-packs/es.ts +47 -0
  175. package/src/core/ui-packs/fa.ts +47 -0
  176. package/src/core/ui-packs/fi.ts +47 -0
  177. package/src/core/ui-packs/fr.ts +47 -0
  178. package/src/core/ui-packs/he.ts +47 -0
  179. package/src/core/ui-packs/hi.ts +47 -0
  180. package/src/core/ui-packs/hr.ts +47 -0
  181. package/src/core/ui-packs/hu.ts +47 -0
  182. package/src/core/ui-packs/id.ts +47 -0
  183. package/src/core/ui-packs/index.ts +87 -0
  184. package/src/core/ui-packs/it.ts +47 -0
  185. package/src/core/ui-packs/ja.ts +47 -0
  186. package/src/core/ui-packs/ko.ts +47 -0
  187. package/src/core/ui-packs/nl.ts +47 -0
  188. package/src/core/ui-packs/no.ts +47 -0
  189. package/src/core/ui-packs/pl.ts +47 -0
  190. package/src/core/ui-packs/pt-br.ts +47 -0
  191. package/src/core/ui-packs/pt.ts +47 -0
  192. package/src/core/ui-packs/ro.ts +47 -0
  193. package/src/core/ui-packs/ru.ts +47 -0
  194. package/src/core/ui-packs/sk.ts +47 -0
  195. package/src/core/ui-packs/sr.ts +47 -0
  196. package/src/core/ui-packs/sv.ts +47 -0
  197. package/src/core/ui-packs/th.ts +47 -0
  198. package/src/core/ui-packs/tr.ts +47 -0
  199. package/src/core/ui-packs/uk.ts +47 -0
  200. package/src/core/ui-packs/vi.ts +47 -0
  201. package/src/core/ui-packs/zh-tw.ts +47 -0
  202. package/src/core/ui-packs/zh.ts +47 -0
  203. package/src/core/version.ts +23 -0
  204. package/src/deploy/robots.ts +20 -0
  205. package/src/deploy/rss.ts +128 -0
  206. package/src/deploy/sitemap.ts +28 -0
  207. package/src/index.ts +27 -0
  208. package/src/markdown/code-title.ts +71 -0
  209. package/src/markdown/directives.ts +83 -0
  210. package/src/markdown/heading-anchors.ts +137 -0
  211. package/src/markdown/index.ts +159 -0
  212. package/src/markdown/inline-code.ts +108 -0
  213. package/src/markdown/language-icon.ts +172 -0
  214. package/src/markdown/math.ts +32 -0
  215. package/src/markdown/mdast.ts +48 -0
  216. package/src/markdown/mermaid.ts +37 -0
  217. package/src/markdown/package-commands.ts +159 -0
  218. package/src/markdown/package-install.ts +40 -0
  219. package/src/migrate/fumadocs/config.ts +106 -0
  220. package/src/migrate/fumadocs/content.ts +365 -0
  221. package/src/migrate/fumadocs/frontmatter.ts +18 -0
  222. package/src/migrate/fumadocs/index.ts +252 -0
  223. package/src/migrate/fumadocs/meta.ts +114 -0
  224. package/src/migrate/migrate.ts +53 -0
  225. package/src/migrate/mintlify/config.ts +1040 -0
  226. package/src/migrate/mintlify/content.ts +98 -0
  227. package/src/migrate/mintlify/frontmatter.ts +126 -0
  228. package/src/migrate/mintlify/i18n.ts +51 -0
  229. package/src/migrate/mintlify/icons.ts +128 -0
  230. package/src/migrate/mintlify/index.ts +266 -0
  231. package/src/migrate/mintlify/snippets.ts +305 -0
  232. package/src/migrate/mintlify/transform.ts +81 -0
  233. package/src/migrate/nextra/content.ts +46 -0
  234. package/src/migrate/nextra/frontmatter.ts +40 -0
  235. package/src/migrate/nextra/index.ts +374 -0
  236. package/src/migrate/nextra/meta.ts +266 -0
  237. package/src/migrate/shared.ts +623 -0
  238. package/src/migrate/starlight/config.ts +459 -0
  239. package/src/migrate/starlight/content.ts +78 -0
  240. package/src/migrate/starlight/frontmatter.ts +111 -0
  241. package/src/migrate/starlight/i18n.ts +54 -0
  242. package/src/migrate/starlight/index.ts +131 -0
  243. package/src/og/card.ts +92 -0
  244. package/src/og/index.ts +2 -0
  245. package/src/openapi/scalar.ts +246 -0
  246. package/src/registry/eject.ts +263 -0
  247. package/src/registry/registry.ts +100 -0
  248. package/src/registry/rewrite-imports.ts +39 -0
  249. package/src/runtime/index.ts +14 -0
  250. package/src/search/build.ts +23 -0
  251. package/src/search/documents.ts +165 -0
  252. package/src/search/orama-index.ts +66 -0
  253. package/src/search/providers.ts +91 -0
  254. package/src/search/sync/algolia.ts +30 -0
  255. package/src/search/sync/index.ts +50 -0
  256. package/src/search/sync/orama-cloud.ts +40 -0
  257. package/src/search/sync/typesense.ts +65 -0
  258. package/src/seo/jsonld.ts +113 -0
  259. package/src/theme/entry.ts +608 -0
  260. package/src/theme/fonts.ts +198 -0
  261. package/src/theme/icons.ts +184 -0
  262. package/src/theme/palette.ts +143 -0
  263. package/src/theme/twoslash.ts +81 -0
package/package.json CHANGED
@@ -1,11 +1,145 @@
1
1
  {
2
2
  "name": "blume",
3
- "version": "0.0.0",
4
- "main": "index.js",
3
+ "version": "0.1.0",
4
+ "description": "Documentation that's fast, AI-ready, and zero-config.",
5
+ "keywords": [
6
+ "astro",
7
+ "docs",
8
+ "documentation",
9
+ "markdown",
10
+ "mdx",
11
+ "vite"
12
+ ],
13
+ "homepage": "https://github.com/haydenbleasel/blume#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/haydenbleasel/blume/issues"
16
+ },
17
+ "license": "MIT",
18
+ "author": "Hayden Bleasel",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/haydenbleasel/blume.git",
22
+ "directory": "packages/blume"
23
+ },
24
+ "bin": {
25
+ "blume": "./bin/blume.mjs"
26
+ },
27
+ "files": [
28
+ "dist",
29
+ "src",
30
+ "bin",
31
+ "docs"
32
+ ],
33
+ "type": "module",
34
+ "exports": {
35
+ ".": "./src/index.ts",
36
+ "./ai/*": "./src/ai/*",
37
+ "./schema": "./src/core/schema.ts",
38
+ "./runtime": "./src/runtime/index.ts",
39
+ "./astro": "./src/astro/index.ts",
40
+ "./og": "./src/og/index.ts",
41
+ "./markdown": "./src/markdown/index.ts",
42
+ "./components": "./src/components/index.ts",
43
+ "./components/*": "./src/components/*",
44
+ "./core/*": "./src/core/*",
45
+ "./sources/*": "./src/core/sources/*",
46
+ "./theme/*": "./src/theme/*",
47
+ "./package.json": "./package.json"
48
+ },
5
49
  "scripts": {
6
- "test": "echo \"Error: no test specified\" && exit 1"
50
+ "build": "bun run scripts/build.ts",
51
+ "bundle-docs": "node scripts/bundle-docs.mjs",
52
+ "prepack": "bun run scripts/build.ts && node scripts/bundle-docs.mjs",
53
+ "test": "bun test",
54
+ "test:coverage": "bun test --coverage",
55
+ "typecheck": "tsgo --noEmit && tsgo -p test/tsconfig.json --noEmit"
56
+ },
57
+ "dependencies": {
58
+ "@astrojs/markdown-satteri": "^0.3.2",
59
+ "@astrojs/mdx": "^7.0.0",
60
+ "@astrojs/node": "^11.0.0",
61
+ "@astrojs/react": "^6.0.0",
62
+ "@astrojs/vercel": "^11.0.0",
63
+ "@modelcontextprotocol/sdk": "^1.29.0",
64
+ "@orama/orama": "^3.1.18",
65
+ "@scalar/astro": "^0.4.5",
66
+ "@shikijs/transformers": "^4.2.0",
67
+ "@shikijs/twoslash": "^4.2.0",
68
+ "@tailwindcss/typography": "^0.5.20",
69
+ "@tailwindcss/vite": "^4",
70
+ "@takumi-rs/core": "^1.8.7",
71
+ "@takumi-rs/helpers": "^1.8.7",
72
+ "@vercel/analytics": "^2.0.1",
73
+ "ai": "^5.0.0",
74
+ "astro": "^7.0.2",
75
+ "citty": "^0.1.6",
76
+ "consola": "^3.4.0",
77
+ "deepmerge": "^4.3.1",
78
+ "epub-gen-memory": "^1.1.2",
79
+ "github-slugger": "^2.0.0",
80
+ "gray-matter": "^4.0.3",
81
+ "jiti": "^2.4.0",
82
+ "katex": "^0.17.0",
83
+ "mermaid": "^11.15.0",
84
+ "pagefind": "^1.3.0",
85
+ "pathe": "^2.0.0",
86
+ "react": "^19.0.0",
87
+ "react-dom": "^19.0.0",
88
+ "shiki": "^4.2.0",
89
+ "simple-icons": "^13.0.0",
90
+ "tailwindcss": "^4",
91
+ "tinyglobby": "^0.2.10",
92
+ "typescript": "^5.7.0",
93
+ "zod": "^3.24.0"
94
+ },
95
+ "devDependencies": {
96
+ "@types/node": "^22.10.0",
97
+ "@types/react": "^19.0.0",
98
+ "@types/react-dom": "^19.0.0",
99
+ "@typescript/native-preview": "^7.0.0-dev.20260626.1",
100
+ "bun-types": "^1.3.14"
101
+ },
102
+ "peerDependencies": {
103
+ "@ai-sdk/openai-compatible": "^1.0.41",
104
+ "@mixedbread/sdk": "^0.76.0",
105
+ "@notionhq/client": "^2.2.15",
106
+ "@openrouter/ai-sdk-provider": "^1.5.4",
107
+ "@oramacloud/client": "^2.1.0",
108
+ "@sanity/client": "^6.21.0",
109
+ "algoliasearch": "^5.55.0",
110
+ "flexsearch": "^0.8.0",
111
+ "typesense": "^3.0.0"
112
+ },
113
+ "peerDependenciesMeta": {
114
+ "@ai-sdk/openai-compatible": {
115
+ "optional": true
116
+ },
117
+ "@mixedbread/sdk": {
118
+ "optional": true
119
+ },
120
+ "@openrouter/ai-sdk-provider": {
121
+ "optional": true
122
+ },
123
+ "@notionhq/client": {
124
+ "optional": true
125
+ },
126
+ "@oramacloud/client": {
127
+ "optional": true
128
+ },
129
+ "@sanity/client": {
130
+ "optional": true
131
+ },
132
+ "algoliasearch": {
133
+ "optional": true
134
+ },
135
+ "flexsearch": {
136
+ "optional": true
137
+ },
138
+ "typesense": {
139
+ "optional": true
140
+ }
7
141
  },
8
- "author": "",
9
- "license": "ISC",
10
- "description": ""
142
+ "engines": {
143
+ "node": ">=22.12.0"
144
+ }
11
145
  }
package/src/ai/ask.ts ADDED
@@ -0,0 +1,93 @@
1
+ import type { AskAiConfig } from "../core/schema.ts";
2
+
3
+ /**
4
+ * The concrete backend the Ask AI endpoint is generated against. `gateway` uses
5
+ * the AI SDK core (`streamText({ model })`) and the Vercel AI Gateway;
6
+ * `openrouter` uses its dedicated provider; everything else streams through the
7
+ * AI SDK's OpenAI-compatible provider.
8
+ */
9
+ export type AskBackend =
10
+ | { kind: "gateway"; model: string }
11
+ | { apiKeyEnv: string; kind: "openrouter"; model: string }
12
+ | {
13
+ apiKeyEnv: string;
14
+ baseUrl: string;
15
+ kind: "openai-compatible";
16
+ model: string;
17
+ name: string;
18
+ };
19
+
20
+ interface AskPreset {
21
+ apiKeyEnv: string;
22
+ baseUrl?: string;
23
+ kind: "openai-compatible" | "openrouter";
24
+ name: string;
25
+ runtimeDep: string;
26
+ }
27
+
28
+ /**
29
+ * Connection presets for the named, non-gateway providers. OpenRouter has a
30
+ * dedicated AI SDK provider; LLMGateway and Inkeep are OpenAI-compatible
31
+ * endpoints with no dedicated provider, so they reuse `@ai-sdk/openai-compatible`.
32
+ */
33
+ const ASK_PRESETS: Record<string, AskPreset> = {
34
+ inkeep: {
35
+ apiKeyEnv: "INKEEP_API_KEY",
36
+ baseUrl: "https://api.inkeep.com/v1",
37
+ kind: "openai-compatible",
38
+ name: "inkeep",
39
+ runtimeDep: "@ai-sdk/openai-compatible",
40
+ },
41
+ llmgateway: {
42
+ apiKeyEnv: "LLMGATEWAY_API_KEY",
43
+ baseUrl: "https://api.llmgateway.io/v1",
44
+ kind: "openai-compatible",
45
+ name: "llmgateway",
46
+ runtimeDep: "@ai-sdk/openai-compatible",
47
+ },
48
+ openrouter: {
49
+ apiKeyEnv: "OPENROUTER_API_KEY",
50
+ kind: "openrouter",
51
+ name: "openrouter",
52
+ runtimeDep: "@openrouter/ai-sdk-provider",
53
+ },
54
+ };
55
+
56
+ const DEFAULT_MODEL = "openai/gpt-5.5";
57
+
58
+ /** Resolve the `ai.ask` config into the backend the endpoint is built against. */
59
+ export const resolveAskBackend = (ask?: AskAiConfig): AskBackend => {
60
+ const provider = ask?.provider ?? "gateway";
61
+ const model = ask?.model ?? DEFAULT_MODEL;
62
+ if (provider === "gateway") {
63
+ return { kind: "gateway", model };
64
+ }
65
+ const preset = ASK_PRESETS[provider];
66
+ const apiKeyEnv = ask?.apiKeyEnv ?? preset?.apiKeyEnv ?? "API_KEY";
67
+ if (provider === "openrouter") {
68
+ return { apiKeyEnv, kind: "openrouter", model };
69
+ }
70
+ // `llmgateway`, `inkeep`, and the generic `openai-compatible` provider all
71
+ // stream through the AI SDK's OpenAI-compatible provider. The schema requires
72
+ // `baseUrl` for the generic case; the named providers fall back to a preset.
73
+ return {
74
+ apiKeyEnv,
75
+ baseUrl: ask?.baseUrl ?? preset?.baseUrl ?? "",
76
+ kind: "openai-compatible",
77
+ model,
78
+ name: preset?.name ?? "openai-compatible",
79
+ };
80
+ };
81
+
82
+ /**
83
+ * The provider SDK a project must install for the configured backend, or
84
+ * `undefined` for `gateway` (which only needs the core `ai` package). Declared
85
+ * in the generated runtime so a project pulls in exactly the backend it uses.
86
+ */
87
+ export const askBackendRuntimeDep = (ask?: AskAiConfig): string | undefined => {
88
+ const provider = ask?.provider ?? "gateway";
89
+ if (provider === "gateway") {
90
+ return undefined;
91
+ }
92
+ return ASK_PRESETS[provider]?.runtimeDep ?? "@ai-sdk/openai-compatible";
93
+ };
package/src/ai/llms.ts ADDED
@@ -0,0 +1,65 @@
1
+ import matter from "gray-matter";
2
+
3
+ import type { BlumeProject } from "../core/project-graph.ts";
4
+ import { readEntryText } from "../core/sources/read.ts";
5
+ import type { PageRecord } from "../core/types.ts";
6
+
7
+ const pageUrl = (route: string, site?: string): string => {
8
+ if (!site) {
9
+ return route;
10
+ }
11
+ return `${site.replace(/\/$/u, "")}${route}`;
12
+ };
13
+
14
+ const orderedPages = (project: BlumeProject): PageRecord[] =>
15
+ [...project.graph.pages]
16
+ .filter((page) => !page.meta.draft)
17
+ .sort((a, b) => a.route.localeCompare(b.route));
18
+
19
+ /** Build the compact `llms.txt` index: title, summary, and links per page. */
20
+ const buildIndex = (project: BlumeProject): string => {
21
+ const { config } = project;
22
+ const { site } = config.deployment;
23
+ const lines = [`# ${config.title}`];
24
+ if (config.description) {
25
+ lines.push("", `> ${config.description}`);
26
+ }
27
+ lines.push("", "## Docs", "");
28
+
29
+ for (const page of orderedPages(project)) {
30
+ const url = pageUrl(page.route, site);
31
+ const summary = page.description ? `: ${page.description}` : "";
32
+ lines.push(`- [${page.title}](${url})${summary}`);
33
+ }
34
+
35
+ return `${lines.join("\n")}\n`;
36
+ };
37
+
38
+ /** Build `llms-full.txt`: the full Markdown body of every page. */
39
+ const buildFull = async (project: BlumeProject): Promise<string> => {
40
+ const { config } = project;
41
+ const pages = orderedPages(project);
42
+
43
+ const sections = await Promise.all(
44
+ pages.map(async (page) => {
45
+ const raw = await readEntryText(project, page);
46
+ const body = matter(raw).content.trim();
47
+ const url = pageUrl(page.route, config.deployment.site);
48
+ return [`# ${page.title}`, `Source: ${url}`, "", body].join("\n");
49
+ })
50
+ );
51
+
52
+ const header = config.description
53
+ ? `# ${config.title}\n\n> ${config.description}\n`
54
+ : `# ${config.title}\n`;
55
+
56
+ return `${header}\n${sections.join("\n\n---\n\n")}\n`;
57
+ };
58
+
59
+ /** Build both LLM text artifacts for a project. */
60
+ export const buildLlmsFiles = async (
61
+ project: BlumeProject
62
+ ): Promise<{ index: string; full: string }> => ({
63
+ full: await buildFull(project),
64
+ index: buildIndex(project),
65
+ });
@@ -0,0 +1,31 @@
1
+ import { readFile } from "node:fs/promises";
2
+
3
+ import type { BlumeProject } from "../core/project-graph.ts";
4
+ import { readEntryText } from "../core/sources/read.ts";
5
+ import type { RouteManifestEntry } from "../core/types.ts";
6
+
7
+ /**
8
+ * Map every route to its raw source Markdown. Powers the `<route>.md` and
9
+ * `<route>.mdx` endpoints, which serve the original source so AI tools — and
10
+ * readers — can fetch any page as plain Markdown.
11
+ */
12
+ export const buildRawMarkdown = async (
13
+ project: BlumeProject
14
+ ): Promise<Record<string, string>> => {
15
+ const pageById = new Map(project.graph.pages.map((page) => [page.id, page]));
16
+
17
+ const readRoute = async (route: RouteManifestEntry): Promise<string> => {
18
+ const page = pageById.get(route.id);
19
+ if (page) {
20
+ return await readEntryText(project, page);
21
+ }
22
+ return route.sourcePath ? await readFile(route.sourcePath, "utf-8") : "";
23
+ };
24
+
25
+ const entries = await Promise.all(
26
+ project.manifest.routes.map(
27
+ async (route) => [route.path, await readRoute(route)] as const
28
+ )
29
+ );
30
+ return Object.fromEntries(entries);
31
+ };
@@ -0,0 +1,74 @@
1
+ import type { BlumeProject } from "../../core/project-graph.ts";
2
+ import type { Navigation } from "../../core/types.ts";
3
+ import { buildSearchDocuments } from "../../search/documents.ts";
4
+ import type { OramaDoc } from "../../search/orama-index.ts";
5
+ import { buildRawMarkdown } from "../markdown.ts";
6
+
7
+ /** A page entry surfaced by the `list_pages` MCP tool. */
8
+ export interface McpRoute {
9
+ contentType: string;
10
+ description?: string;
11
+ indexable: boolean;
12
+ lastModified: string | null;
13
+ route: string;
14
+ title: string;
15
+ }
16
+
17
+ /**
18
+ * The self-contained snapshot the generated MCP endpoint serves. Bundles the
19
+ * search documents, raw page Markdown, route list, and navigation so the server
20
+ * works regardless of the configured search provider and needs no filesystem
21
+ * access at request time. Serialized to `generated/mcp-data.json`.
22
+ */
23
+ export interface McpData {
24
+ documents: OramaDoc[];
25
+ instructions?: string;
26
+ name: string;
27
+ navigation: Navigation;
28
+ pages: Record<string, string>;
29
+ routes: McpRoute[];
30
+ site: string | null;
31
+ version: string;
32
+ }
33
+
34
+ /** Build the MCP data snapshot from a resolved project. */
35
+ export const buildMcpData = async (project: BlumeProject): Promise<McpData> => {
36
+ const { config, graph, manifest } = project;
37
+ const [documents, pages] = await Promise.all([
38
+ // The MCP server is independent of on-page search, so index docs even when
39
+ // the search provider is `none`.
40
+ buildSearchDocuments(project, { includeWhenDisabled: true }),
41
+ buildRawMarkdown(project),
42
+ ]);
43
+
44
+ const descriptionById = new Map(
45
+ graph.pages.map((page) => [page.id, page.description])
46
+ );
47
+
48
+ const routes: McpRoute[] = manifest.routes
49
+ .filter((route) => !route.hidden)
50
+ .map((route) => ({
51
+ contentType: route.contentType,
52
+ description: descriptionById.get(route.id),
53
+ indexable: route.indexable,
54
+ lastModified: route.lastModified ?? null,
55
+ route: route.path,
56
+ title: route.title,
57
+ }));
58
+
59
+ return {
60
+ documents: documents.map((doc) => ({
61
+ content: doc.content,
62
+ description: doc.description,
63
+ route: doc.route,
64
+ title: doc.title,
65
+ })),
66
+ instructions: config.mcp.instructions,
67
+ name: config.mcp.name ?? config.title,
68
+ navigation: graph.navigation,
69
+ pages,
70
+ routes,
71
+ site: config.deployment.site ?? null,
72
+ version: manifest.blumeVersion,
73
+ };
74
+ };
@@ -0,0 +1,49 @@
1
+ import { MCP_TOOLS } from "./tools.ts";
2
+
3
+ /** Inputs needed to describe the MCP server in discovery documents. */
4
+ export interface McpDiscoveryInput {
5
+ name: string;
6
+ route: string;
7
+ site: string | null;
8
+ version: string;
9
+ }
10
+
11
+ /** The MCP server's address — absolute when a site is configured. */
12
+ const serverUrl = (input: McpDiscoveryInput): string =>
13
+ input.site ? new URL(input.route, input.site).href : input.route;
14
+
15
+ /**
16
+ * The `/.well-known/mcp.json` discovery document: the minimal pointer agents use
17
+ * to find the server and its transport.
18
+ */
19
+ export const buildMcpDiscovery = (
20
+ input: McpDiscoveryInput
21
+ ): Record<string, unknown> => ({
22
+ servers: [
23
+ {
24
+ name: input.name,
25
+ transport: "streamable-http",
26
+ url: serverUrl(input),
27
+ },
28
+ ],
29
+ });
30
+
31
+ /**
32
+ * The `/.well-known/mcp/server-card.json` document: richer metadata including
33
+ * the advertised tool set (full input schemas are served live via `tools/list`).
34
+ */
35
+ export const buildMcpServerCard = (
36
+ input: McpDiscoveryInput
37
+ ): Record<string, unknown> => ({
38
+ description: `Model Context Protocol server for the ${input.name} documentation.`,
39
+ name: input.name,
40
+ tools: MCP_TOOLS.map((tool) => ({
41
+ annotations: tool.annotations,
42
+ description: tool.description,
43
+ name: tool.name,
44
+ title: tool.title,
45
+ })),
46
+ transport: "streamable-http",
47
+ url: serverUrl(input),
48
+ version: input.version,
49
+ });
@@ -0,0 +1,225 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
3
+ import {
4
+ CallToolRequestSchema,
5
+ ListToolsRequestSchema,
6
+ } from "@modelcontextprotocol/sdk/types.js";
7
+
8
+ import { buildOramaIndex, queryOramaIndex } from "../../search/orama-index.ts";
9
+ import type { OramaDoc } from "../../search/orama-index.ts";
10
+ import type { McpData } from "./data.ts";
11
+ import { MCP_TOOLS } from "./tools.ts";
12
+
13
+ /**
14
+ * The low-level SDK `Server` is used (rather than the high-level `McpServer`)
15
+ * because the latter's `registerTool` is generic over the caller's Zod version;
16
+ * Blume pins Zod 3 while the SDK resolves Zod 4, so their types don't unify.
17
+ * Hand-written JSON Schema and the SDK's own request schemas avoid that entirely.
18
+ */
19
+
20
+ /** Default and maximum number of hits returned by `search_docs`. */
21
+ const DEFAULT_SEARCH_LIMIT = 8;
22
+ const MAX_SEARCH_LIMIT = 20;
23
+ /** Excerpt length when a page has no description. */
24
+ const EXCERPT_LENGTH = 200;
25
+
26
+ const CORS_HEADERS: Record<string, string> = {
27
+ "Access-Control-Allow-Headers":
28
+ "Content-Type, Mcp-Session-Id, Mcp-Protocol-Version",
29
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
30
+ "Access-Control-Allow-Origin": "*",
31
+ "Access-Control-Expose-Headers": "Mcp-Session-Id",
32
+ };
33
+
34
+ /** JSON Schema for each tool's input, keyed by tool name. */
35
+ const INPUT_SCHEMAS: Record<string, Record<string, unknown>> = {
36
+ get_navigation: { properties: {}, type: "object" },
37
+ get_page: {
38
+ properties: {
39
+ route: {
40
+ description: "The page route, e.g. `/guides/install`.",
41
+ type: "string",
42
+ },
43
+ },
44
+ required: ["route"],
45
+ type: "object",
46
+ },
47
+ list_pages: { properties: {}, type: "object" },
48
+ search_docs: {
49
+ properties: {
50
+ limit: {
51
+ description: `Maximum hits to return (default ${DEFAULT_SEARCH_LIMIT}).`,
52
+ maximum: MAX_SEARCH_LIMIT,
53
+ minimum: 1,
54
+ type: "integer",
55
+ },
56
+ query: { description: "The search query.", type: "string" },
57
+ },
58
+ required: ["query"],
59
+ type: "object",
60
+ },
61
+ };
62
+
63
+ /** The `tools/list` payload, derived from shared metadata + input schemas. */
64
+ const TOOL_DEFINITIONS = MCP_TOOLS.map((tool) => ({
65
+ annotations: tool.annotations,
66
+ description: tool.description,
67
+ inputSchema: INPUT_SCHEMAS[tool.name],
68
+ name: tool.name,
69
+ title: tool.title,
70
+ }));
71
+
72
+ const asString = (value: unknown): string =>
73
+ typeof value === "string" ? value : "";
74
+
75
+ const asLimit = (value: unknown): number => {
76
+ const num = typeof value === "number" ? value : Number(value);
77
+ if (!Number.isFinite(num)) {
78
+ return DEFAULT_SEARCH_LIMIT;
79
+ }
80
+ return Math.min(Math.max(Math.trunc(num), 1), MAX_SEARCH_LIMIT);
81
+ };
82
+
83
+ /** Normalize a user-supplied route to a `pages` key (`/`, `/a/b`, no suffix). */
84
+ const normalizeRoute = (input: string): string => {
85
+ const noTrailing = input.trim().replace(/\/+$/u, "");
86
+ const noSuffix = noTrailing.replace(/\.mdx?$/u, "");
87
+ const withSlash = noSuffix.startsWith("/") ? noSuffix : `/${noSuffix}`;
88
+ return withSlash === "" ? "/" : withSlash;
89
+ };
90
+
91
+ /** Build the absolute (or root-relative) URL for a route. */
92
+ const urlFor = (route: string, site: string | null): string =>
93
+ site ? new URL(route, site).href : route;
94
+
95
+ const text = (value: string, isError = false) => ({
96
+ content: [{ text: value, type: "text" as const }],
97
+ ...(isError ? { isError: true } : {}),
98
+ });
99
+
100
+ /** Construct a fresh MCP server with Blume's read-only docs tools registered. */
101
+ const buildServer = (
102
+ data: McpData,
103
+ index: () => Promise<Awaited<ReturnType<typeof buildOramaIndex>>>
104
+ ): Server => {
105
+ const server = new Server(
106
+ { name: data.name, version: data.version },
107
+ {
108
+ capabilities: { tools: {} },
109
+ ...(data.instructions ? { instructions: data.instructions } : {}),
110
+ }
111
+ );
112
+
113
+ server.setRequestHandler(ListToolsRequestSchema, () => ({
114
+ tools: TOOL_DEFINITIONS,
115
+ }));
116
+
117
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
118
+ const { arguments: args = {}, name } = request.params;
119
+
120
+ if (name === "search_docs") {
121
+ const db = await index();
122
+ const hits = await queryOramaIndex(
123
+ db,
124
+ asString(args.query),
125
+ asLimit(args.limit)
126
+ );
127
+ const results = hits.map((doc: OramaDoc) => ({
128
+ excerpt:
129
+ doc.description || `${doc.content.slice(0, EXCERPT_LENGTH)}…`.trim(),
130
+ title: doc.title,
131
+ url: urlFor(doc.route, data.site),
132
+ }));
133
+ return text(JSON.stringify(results, null, 2));
134
+ }
135
+
136
+ if (name === "get_page") {
137
+ const key = normalizeRoute(asString(args.route));
138
+ const markdown = data.pages[key];
139
+ if (markdown === undefined) {
140
+ return text(
141
+ `No page found at "${key}". Use list_pages or search_docs to find valid routes.`,
142
+ true
143
+ );
144
+ }
145
+ return text(markdown);
146
+ }
147
+
148
+ if (name === "list_pages") {
149
+ return text(
150
+ JSON.stringify(
151
+ data.routes.map((route) => ({
152
+ contentType: route.contentType,
153
+ description: route.description,
154
+ lastModified: route.lastModified,
155
+ route: route.route,
156
+ title: route.title,
157
+ url: urlFor(route.route, data.site),
158
+ })),
159
+ null,
160
+ 2
161
+ )
162
+ );
163
+ }
164
+
165
+ if (name === "get_navigation") {
166
+ return text(JSON.stringify(data.navigation, null, 2));
167
+ }
168
+
169
+ return text(`Unknown tool: ${name}`, true);
170
+ });
171
+
172
+ return server;
173
+ };
174
+
175
+ /**
176
+ * Build a stateless Streamable-HTTP MCP request handler from a data snapshot.
177
+ *
178
+ * The Orama index is built once and reused; a fresh `Server` and transport are
179
+ * created per request (required by the SDK's stateless mode, which skips session
180
+ * tracking). `enableJsonResponse` makes each call a plain request/response — no
181
+ * SSE — which suits read-only docs tools and runs on any adapter (Node, Vercel,
182
+ * Netlify, Cloudflare). CORS is added so browser-based connectors (e.g.
183
+ * claude.ai) can reach the endpoint.
184
+ */
185
+ export const createMcpFetchHandler = (
186
+ data: McpData
187
+ ): ((request: Request) => Promise<Response>) => {
188
+ let dbPromise: Promise<Awaited<ReturnType<typeof buildOramaIndex>>> | null =
189
+ null;
190
+ const index = () => {
191
+ dbPromise ??= buildOramaIndex(data.documents);
192
+ return dbPromise;
193
+ };
194
+
195
+ return async (request: Request): Promise<Response> => {
196
+ if (request.method === "OPTIONS") {
197
+ return new Response(null, { headers: CORS_HEADERS, status: 204 });
198
+ }
199
+ if (request.method === "GET") {
200
+ // No server-initiated streams are needed for read-only tools.
201
+ return new Response("Method Not Allowed", {
202
+ headers: { ...CORS_HEADERS, Allow: "POST, OPTIONS" },
203
+ status: 405,
204
+ });
205
+ }
206
+
207
+ const server = buildServer(data, index);
208
+ const transport = new WebStandardStreamableHTTPServerTransport({
209
+ enableJsonResponse: true,
210
+ sessionIdGenerator: undefined,
211
+ });
212
+ await server.connect(transport);
213
+ const response = await transport.handleRequest(request);
214
+
215
+ const headers = new Headers(response.headers);
216
+ for (const [key, value] of Object.entries(CORS_HEADERS)) {
217
+ headers.set(key, value);
218
+ }
219
+ return new Response(response.body, {
220
+ headers,
221
+ status: response.status,
222
+ statusText: response.statusText,
223
+ });
224
+ };
225
+ };