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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # blume
2
2
 
3
+ ## 1.7.3
4
+
5
+ ### Patch Changes
6
+
7
+ - afbe83b: Publish an AI Catalog / ARD (Agentic Resource Discovery) manifest at `/.well-known/ai-catalog.json`, mirrored at `/.well-known/ard.json`, so agent registries can index a site's agent-facing resources from its domain alone. The catalog is generated from what the site already publishes: the MCP server card, each agent skill, the JSON docs API's OpenAPI document, each rendered API reference, and `llms.txt`, each with a domain-anchored `urn:air:` identifier, media type, and representative queries for semantic search. It's on by default whenever a `deployment.site` is configured; `ai.catalog: false` turns it off, and `ai.catalog.queries` replaces the generated queries for any entry. The manifest is advertised through the homepage `Link` header, the `<link rel="ai-catalog">` and `<link rel="ard">` in every page's head, `llms.txt`, and `agent-readability.json`, and every `.well-known` discovery document (the catalog, the RFC 9727 API catalog, the MCP discovery files) now carries `Access-Control-Allow-Origin: *` on Netlify, Cloudflare, and Vercel so registries can fetch them cross-origin.
8
+ - 89b7618: Report Ask AI usage through the configured analytics providers, the way page feedback already is: `ask` when a question is sent, `ask_answer` when the answer finishes (with latency and length), and `ask_error` when the request fails, breaks mid-stream, or comes back empty (with the HTTP status, or `0` when no response exists). Events carry the page's served pathname and the question's length; the question text itself travels only on the `blume:track` DOM event, so a site decides where reader input goes. Custom chat UIs built on `useAskAI` report the same events, and a throwing analytics provider no longer starves the providers after it.
9
+ - c6189a3: A generated sidebar group whose folder has an `index` page now links its row to that page, the way an explicit sidebar group's `root` already did. Clicking the section name in the sidebar opens the section's landing page instead of only toggling the disclosure, in every display mode. The index page keeps its own row beneath the header; set `sidebar.hidden: true` on it to drop the duplicate label, and the header stays the link to it.
10
+ - 194e8f9: Style `<kbd>` as a bordered key badge, matching the search dialog's shortcut hints, everywhere in the content column. Keys inside `<Steps>`, `<Callout>`, `<Card>`, and other components previously rendered as plain text because the typography plugin skips them, and keys in ordinary prose used the plugin's near-black text and shadow, which disappeared in dark mode.
11
+ - ddf0a60: Write the Pagefind index files from the build itself instead of asking Pagefind to write them. Pagefind's service mode reports the files as written while the bytes are still buffered, and closing the backend right after could leave a truncated `pagefind-entry.json` or UI asset on a busy CI host, so search failed to load on the deployed site. The build now fetches the finished index and writes every file to disk before Pagefind shuts down.
12
+
13
+ ## 1.7.2
14
+
15
+ ### Patch Changes
16
+
17
+ - 93e31fc: Add `ai.ask.cors` to let listed origins (or `"*"`) call the generated Ask AI endpoint from another site. The route answers the browser's preflight and names a listed origin on every response, the streamed answer and error statuses alike, so a marketing page can embed an ask box without an external endpoint or an eject.
18
+ - 53d75a2: Add `ai.ask.reasoning` to set how much the Ask AI model reasons before answering, from `none` to `xhigh`. The level is sent as each backend's own reasoning-effort control: the AI SDK's `reasoning` option on the gateway, `reasoning.effort` on OpenRouter, and `reasoning_effort` on OpenAI-compatible endpoints. Leaving it unset keeps the model's default.
19
+ - 7f0c91e: Fix grouped sidebar sections 404ing on first open when header tabs are configured. On a page outside every tab, the sidebar hides the tab-owned sections, and that pruned view lost its groups' stable ids, so their deferred fragments were requested by a positional name no route serves. Untouched groups are now kept by identity (which also keeps their build-time render cache warm). A container that loses a nested tab section renders in full instead of deferring, since a shared fragment would show the section again.
20
+
3
21
  ## 1.7.1
4
22
 
5
23
  ### Patch Changes
@@ -0,0 +1,164 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ siteRoot,
4
+ absoluteUrl,
5
+ MCP_TOOLS
6
+ } from "./chunk-ct47dqpx.js";
7
+ import {
8
+ resolveReferences
9
+ } from "./chunk-7s8hm3b6.js";
10
+ import {
11
+ trimChar,
12
+ normalizeBasePath,
13
+ withBasePath
14
+ } from "./chunk-2mzebbbz.js";
15
+
16
+ // src/ai/api/paths.ts
17
+ var OPENAPI_PATH = "/openapi.json";
18
+ var API_BASE = "/api/docs";
19
+ var API_PAGES_PATH = `${API_BASE}/pages.json`;
20
+ var API_PAGE_PATH = `${API_BASE}/pages/{route}.json`;
21
+ var API_NAVIGATION_PATH = `${API_BASE}/navigation.json`;
22
+ var API_SEARCH_PATH = `${API_BASE}/search`;
23
+ var pageParam = (route) => route === "/" ? "index" : route.slice(1);
24
+ var pageJsonPath = (route) => `${API_BASE}/pages/${pageParam(route)}.json`;
25
+
26
+ // src/ai/api-catalog.ts
27
+ var API_CATALOG_PATH = "/.well-known/api-catalog";
28
+ var API_CATALOG_TYPE = "application/linkset+json";
29
+ var HTTP_URL = /^https?:\/\//u;
30
+ var linksetEntries = (config) => {
31
+ const site = config.deployment.site ?? null;
32
+ const deployBase = normalizeBasePath(config.deployment.base);
33
+ const abs = (path) => {
34
+ const based = withBasePath(deployBase, path);
35
+ return site ? absoluteUrl(site, based) : based;
36
+ };
37
+ const entries = [];
38
+ for (const reference of resolveReferences(config)) {
39
+ const docRoute = reference.renderer === "blume" ? withBasePath(reference.basePath, reference.route) : reference.route;
40
+ const entry = {
41
+ anchor: abs(docRoute),
42
+ "service-doc": [{ href: abs(docRoute), type: "text/html" }]
43
+ };
44
+ if (HTTP_URL.test(reference.spec)) {
45
+ entry["service-desc"] = [{ href: reference.spec }];
46
+ }
47
+ entries.push(entry);
48
+ }
49
+ if (config.ai.api) {
50
+ entries.push({
51
+ anchor: abs(API_BASE),
52
+ "service-desc": [{ href: abs(OPENAPI_PATH), type: "application/json" }],
53
+ "service-doc": [{ href: abs("/"), type: "text/html" }]
54
+ });
55
+ }
56
+ if (config.ai.mcp.enabled) {
57
+ entries.push({
58
+ anchor: abs(config.ai.mcp.route),
59
+ "service-desc": [
60
+ { href: abs("/.well-known/mcp.json"), type: "application/json" }
61
+ ],
62
+ "service-doc": [{ href: abs("/"), type: "text/html" }]
63
+ });
64
+ }
65
+ return entries;
66
+ };
67
+ var hasApiCatalog = (config) => linksetEntries(config).length > 0;
68
+
69
+ // src/ai/mcp/discovery.ts
70
+ var serverUrl = (input) => {
71
+ const path = withBasePath(input.base, input.route);
72
+ return input.site ? absoluteUrl(input.site, path) : path;
73
+ };
74
+ var buildMcpDiscovery = (input) => ({
75
+ servers: [
76
+ {
77
+ name: input.name,
78
+ transport: "streamable-http",
79
+ url: serverUrl(input)
80
+ }
81
+ ]
82
+ });
83
+ var SERVER_CARD_SCHEMA = "https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json";
84
+ var CARD_TEXT_MAX = 100;
85
+ var truncate = (text) => text.length > CARD_TEXT_MAX ? `${text.slice(0, CARD_TEXT_MAX - 1)}…` : text;
86
+ var NON_ASCII_SLUG = /[^a-z0-9]+/gu;
87
+ var COMBINING_MARKS = /\p{M}+/gu;
88
+ var asciiSlugify = (text) => trimChar(text.normalize("NFKD").replace(COMBINING_MARKS, "").toLowerCase().replace(NON_ASCII_SLUG, "-"), "-");
89
+ var reverseDnsName = (input) => {
90
+ let namespace = "localhost";
91
+ if (input.site) {
92
+ try {
93
+ namespace = new URL(input.site).hostname.split(".").toReversed().join(".");
94
+ } catch {}
95
+ }
96
+ return `${namespace}/${asciiSlugify(input.name) || "docs"}`;
97
+ };
98
+ var HTTP_URL2 = /^https?:\/\//u;
99
+ var buildMcpServerCard = (input) => {
100
+ const url = serverUrl(input);
101
+ const card = {
102
+ $schema: SERVER_CARD_SCHEMA,
103
+ capabilities: {
104
+ resources: { listChanged: false, subscribe: false },
105
+ tools: { listChanged: false }
106
+ },
107
+ description: truncate(`Model Context Protocol server for the ${input.name} documentation.`),
108
+ name: reverseDnsName(input),
109
+ serverInfo: { name: input.name, version: input.version },
110
+ title: truncate(input.name),
111
+ tools: MCP_TOOLS.map((tool) => ({
112
+ annotations: tool.annotations,
113
+ description: tool.description,
114
+ name: tool.name,
115
+ title: tool.title
116
+ })),
117
+ transport: "streamable-http",
118
+ transports: [{ endpoint: url, type: "streamable-http" }],
119
+ url,
120
+ version: input.version
121
+ };
122
+ if (HTTP_URL2.test(url)) {
123
+ card.remotes = [{ type: "streamable-http", url }];
124
+ }
125
+ if (input.site) {
126
+ card.websiteUrl = siteRoot(input.site);
127
+ }
128
+ return card;
129
+ };
130
+
131
+ // src/ai/skills.ts
132
+ import { join } from "pathe";
133
+
134
+ // src/ai/tar.ts
135
+ import { createTar } from "nanotar";
136
+ var encoder = new TextEncoder;
137
+
138
+ // src/ai/skills.ts
139
+ var AGENT_SKILLS_DIR = "/.well-known/agent-skills";
140
+ var AGENT_SKILLS_INDEX_PATH = "/.well-known/agent-skills/index.json";
141
+
142
+ // src/ai/ai-catalog.ts
143
+ var AI_CATALOG_PATH = "/.well-known/ai-catalog.json";
144
+ var ARD_MANIFEST_PATH = "/.well-known/ard.json";
145
+ var AI_CATALOG_TYPE = "application/ai-catalog+json";
146
+ var hasAiCatalog = (config) => config.ai.catalog.enabled && Boolean(config.deployment.site) && (config.ai.mcp.enabled || config.ai.api || config.ai.llmsTxt.enabled || Boolean(config.ai.skills) || resolveReferences(config).length > 0);
147
+ var crossOriginDiscoveryPaths = (config) => {
148
+ const paths = [];
149
+ if (hasAiCatalog(config)) {
150
+ paths.push(AI_CATALOG_PATH, ARD_MANIFEST_PATH);
151
+ }
152
+ if (hasApiCatalog(config)) {
153
+ paths.push(API_CATALOG_PATH);
154
+ }
155
+ if (config.ai.mcp.enabled) {
156
+ paths.push("/.well-known/mcp.json", "/.well-known/mcp/server-card.json");
157
+ }
158
+ return paths;
159
+ };
160
+
161
+ export { OPENAPI_PATH, API_PAGES_PATH, API_PAGE_PATH, API_NAVIGATION_PATH, API_SEARCH_PATH, pageJsonPath, API_CATALOG_PATH, API_CATALOG_TYPE, hasApiCatalog, buildMcpDiscovery, buildMcpServerCard, AGENT_SKILLS_DIR, AGENT_SKILLS_INDEX_PATH, AI_CATALOG_PATH, AI_CATALOG_TYPE, hasAiCatalog, crossOriginDiscoveryPaths };
162
+
163
+ //# debugId=26AE621DEDAFCC8A64756E2164756E21
164
+ //# sourceMappingURL=chunk-0qymqwzz.js.map
@@ -0,0 +1,15 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/ai/api/paths.ts", "../src/ai/api-catalog.ts", "../src/ai/mcp/discovery.ts", "../src/ai/skills.ts", "../src/ai/tar.ts", "../src/ai/ai-catalog.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * Where the JSON docs API and its OpenAPI description are served. Base-less\n * (like every route Blume emits); callers layer `deployment.base` on top.\n * Under `/api/` alongside the Ask AI endpoint (`/api/ask`) so the namespace a\n * Blume site reserves for live endpoints stays one prefix, and under its own\n * `docs` segment so a search provider's proxy at `/api/search` never collides.\n */\n\nexport const OPENAPI_PATH = \"/openapi.json\";\nexport const API_BASE = \"/api/docs\";\nexport const API_PAGES_PATH = `${API_BASE}/pages.json`;\nexport const API_PAGE_PATH = `${API_BASE}/pages/{route}.json`;\nexport const API_NAVIGATION_PATH = `${API_BASE}/navigation.json`;\nexport const API_SEARCH_PATH = `${API_BASE}/search`;\n\n/** The `pages/{route}.json` path segment for a route (`index` for home). */\nexport const pageParam = (route: string): string =>\n route === \"/\" ? \"index\" : route.slice(1);\n\n/** The base-less served path of a route's per-page JSON document. */\nexport const pageJsonPath = (route: string): string =>\n `${API_BASE}/pages/${pageParam(route)}.json`;\n",
6
+ "import { normalizeBasePath, withBasePath } from \"../core/base-path.ts\";\nimport type { ResolvedConfig } from \"../core/schema.ts\";\nimport { absoluteUrl } from \"../core/site-url.ts\";\nimport { resolveReferences } from \"../openapi/references.ts\";\nimport { API_BASE, OPENAPI_PATH } from \"./api/paths.ts\";\n\n/**\n * RFC 9727 API catalog: a linkset (RFC 9264) at `/.well-known/api-catalog`\n * enumerating the APIs a publisher offers, so agents can discover them from\n * the domain alone. Blume already knows the site's APIs — the configured\n * OpenAPI/AsyncAPI references and the hosted MCP server — so the catalog is\n * generated, never hand-written: one entry per API, anchored at its docs\n * route (the RFC's own examples anchor on developer-portal pages), with\n * `service-doc` pointing at the rendered reference and `service-desc` at the\n * spec when it lives at a fetchable URL. The site's own JSON docs API is an\n * entry too, described by its generated `/openapi.json`.\n */\n\nexport const API_CATALOG_PATH = \"/.well-known/api-catalog\";\nexport const API_CATALOG_TYPE = \"application/linkset+json\";\n\n/** An RFC 9264 linkset entry, restricted to the relations Blume emits. */\ninterface LinksetEntry {\n anchor: string;\n \"service-desc\"?: { href: string; type?: string }[];\n \"service-doc\"?: { href: string; type: string }[];\n}\n\nconst HTTP_URL = /^https?:\\/\\//u;\n\n/** The catalog's linkset entries; empty when the site publishes no APIs. */\nconst linksetEntries = (config: ResolvedConfig): LinksetEntry[] => {\n const site = config.deployment.site ?? null;\n const deployBase = normalizeBasePath(config.deployment.base);\n const abs = (path: string): string => {\n const based = withBasePath(deployBase, path);\n return site ? absoluteUrl(site, based) : based;\n };\n\n const entries: LinksetEntry[] = [];\n for (const reference of resolveReferences(config)) {\n // Blume-rendered pages mount under `basePath`; Scalar pages stay at the\n // raw route (see `referenceRoutes`).\n const docRoute =\n reference.renderer === \"blume\"\n ? withBasePath(reference.basePath, reference.route)\n : reference.route;\n const entry: LinksetEntry = {\n anchor: abs(docRoute),\n \"service-doc\": [{ href: abs(docRoute), type: \"text/html\" }],\n };\n // A local spec file is parsed into pages, not served, so only a remote\n // spec has a fetchable `service-desc` URL.\n if (HTTP_URL.test(reference.spec)) {\n entry[\"service-desc\"] = [{ href: reference.spec }];\n }\n entries.push(entry);\n }\n\n if (config.ai.api) {\n entries.push({\n anchor: abs(API_BASE),\n \"service-desc\": [{ href: abs(OPENAPI_PATH), type: \"application/json\" }],\n \"service-doc\": [{ href: abs(\"/\"), type: \"text/html\" }],\n });\n }\n\n if (config.ai.mcp.enabled) {\n entries.push({\n anchor: abs(config.ai.mcp.route),\n \"service-desc\": [\n { href: abs(\"/.well-known/mcp.json\"), type: \"application/json\" },\n ],\n \"service-doc\": [{ href: abs(\"/\"), type: \"text/html\" }],\n });\n }\n return entries;\n};\n\n/** Whether the site has anything to catalog — gates every emission surface. */\nexport const hasApiCatalog = (config: ResolvedConfig): boolean =>\n linksetEntries(config).length > 0;\n\n/** The `application/linkset+json` document, or null when there are no APIs. */\nexport const buildApiCatalog = (config: ResolvedConfig): string | null => {\n const linkset = linksetEntries(config);\n if (linkset.length === 0) {\n return null;\n }\n return `${JSON.stringify({ linkset }, null, 2)}\\n`;\n};\n",
7
+ "import { withBasePath } from \"../../core/base-path.ts\";\nimport { absoluteUrl, siteRoot } from \"../../core/site-url.ts\";\nimport { trimChar } from \"../../core/trim.ts\";\nimport { MCP_TOOLS } from \"./tools.ts\";\nimport type { McpToolMeta } from \"./tools.ts\";\n\n/** Inputs needed to describe the MCP server in discovery documents. */\nexport interface McpDiscoveryInput {\n /** Normalized `deployment.base` (`\"\"` or `/seg`); the route is base-less. */\n base: string;\n name: string;\n route: string;\n site: string | null;\n version: string;\n}\n\n/** The MCP server's address — absolute when a site is configured. */\nconst serverUrl = (input: McpDiscoveryInput): string => {\n // The endpoint is a generated Astro page, so it's served under\n // `deployment.base` like every other route (the sitemap/llms.txt convention).\n const path = withBasePath(input.base, input.route);\n // Concatenate rather than `new URL(path, site)` — a root-absolute path\n // would drop the base path of a subpath deployment (`acme.com/docs`).\n return input.site ? absoluteUrl(input.site, path) : path;\n};\n\n/**\n * The `/.well-known/mcp.json` discovery document: the minimal pointer agents use\n * to find the server and its transport.\n */\nexport const buildMcpDiscovery = (input: McpDiscoveryInput) => ({\n servers: [\n {\n name: input.name,\n transport: \"streamable-http\",\n url: serverUrl(input),\n },\n ],\n});\n\n/** The published `/v1/` Server Card schema (SEP-2127 extension). */\nconst SERVER_CARD_SCHEMA =\n \"https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json\";\n\n/** The schema caps `description` (and `title`) at 100 characters. */\nconst CARD_TEXT_MAX = 100;\n\nconst truncate = (text: string): string =>\n text.length > CARD_TEXT_MAX ? `${text.slice(0, CARD_TEXT_MAX - 1)}…` : text;\n\n// The server-card schema constrains `name` to `namespace/server` in ASCII\n// (`^[a-zA-Z0-9.-]+/[a-zA-Z0-9._-]+$`), so the display name cannot reuse the\n// route slugifier, which keeps Unicode letters. Decompose (NFKD) and drop\n// combining marks first so an accented name transliterates (`Café` → `cafe`)\n// instead of losing the letter.\nconst NON_ASCII_SLUG = /[^a-z0-9]+/gu;\nconst COMBINING_MARKS = /\\p{M}+/gu;\n\nexport const asciiSlugify = (text: string): string =>\n trimChar(\n text\n .normalize(\"NFKD\")\n .replace(COMBINING_MARKS, \"\")\n .toLowerCase()\n .replace(NON_ASCII_SLUG, \"-\"),\n \"-\"\n );\n\n/**\n * The card's identity in the schema's reverse-DNS `namespace/server` form:\n * the site hostname reversed (`useblume.dev` → `dev.useblume`), or\n * `localhost` when no site is configured, plus the slugged display name (an\n * entirely non-ASCII name falls back to `docs`).\n */\nconst reverseDnsName = (input: McpDiscoveryInput): string => {\n let namespace = \"localhost\";\n if (input.site) {\n try {\n namespace = new URL(input.site).hostname\n .split(\".\")\n .toReversed()\n .join(\".\");\n } catch {\n // Not a parsable URL; the local namespace is honest enough.\n }\n }\n return `${namespace}/${asciiSlugify(input.name) || \"docs\"}`;\n};\n\nconst HTTP_URL = /^https?:\\/\\//u;\n\n/**\n * The `/.well-known/mcp/server-card.json` document. The core follows the\n * SEP-2127 Server Card extension schema (`$schema`, reverse-DNS `name`,\n * `remotes` — which the schema requires to be absolute, so it appears only\n * when a `site` is configured). Alongside it ride initialize-shaped compat\n * fields (`serverInfo`, `capabilities`, `transports`) for scanners built\n * against the SEP's earlier revision, plus Blume's advertised tool set —\n * schema-legal extras (`additionalProperties` is open), and the tool list is\n * genuinely static for a docs server, unlike the dynamic servers the spec\n * excludes primitives for. Full input schemas are served live via\n * `tools/list`.\n */\nexport interface McpServerCard {\n $schema: string;\n capabilities: {\n resources: { listChanged: boolean; subscribe: boolean };\n tools: { listChanged: boolean };\n };\n description: string;\n name: string;\n /** Absolute endpoints only — present when a `site` is configured. */\n remotes?: { type: string; url: string }[];\n serverInfo: { name: string; version: string };\n title: string;\n tools: McpToolMeta[];\n transport: string;\n transports: { endpoint: string; type: string }[];\n url: string;\n version: string;\n websiteUrl?: string;\n}\n\nexport const buildMcpServerCard = (input: McpDiscoveryInput): McpServerCard => {\n const url = serverUrl(input);\n const card: McpServerCard = {\n $schema: SERVER_CARD_SCHEMA,\n // Resources mirror the page list (one `text/markdown` resource per page);\n // a rebuilt site replaces the server wholesale, so nothing changes live.\n capabilities: {\n resources: { listChanged: false, subscribe: false },\n tools: { listChanged: false },\n },\n description: truncate(\n `Model Context Protocol server for the ${input.name} documentation.`\n ),\n name: reverseDnsName(input),\n serverInfo: { name: input.name, version: input.version },\n title: truncate(input.name),\n tools: MCP_TOOLS.map((tool) => ({\n annotations: tool.annotations,\n description: tool.description,\n name: tool.name,\n title: tool.title,\n })),\n transport: \"streamable-http\",\n transports: [{ endpoint: url, type: \"streamable-http\" }],\n url,\n version: input.version,\n };\n if (HTTP_URL.test(url)) {\n card.remotes = [{ type: \"streamable-http\", url }];\n }\n if (input.site) {\n card.websiteUrl = siteRoot(input.site);\n }\n return card;\n};\n",
8
+ "import { createHash } from \"node:crypto\";\nimport { readdir, readFile, stat } from \"node:fs/promises\";\n\nimport { join } from \"pathe\";\n\nimport { normalizeBasePath } from \"../core/base-path.ts\";\nimport matter from \"../core/frontmatter.ts\";\nimport type { ResolvedConfig } from \"../core/schema.ts\";\nimport { buildTarGz } from \"./tar.ts\";\nimport type { TarEntry } from \"./tar.ts\";\n\n/**\n * Agent Skills discovery (Cloudflare's Agent Skills Discovery RFC v0.2.0):\n * the skills a site publishes are enumerated in an index at\n * `/.well-known/agent-skills/index.json`, each entry pointing at its artifact\n * with a SHA-256 digest. A skill that is only a `SKILL.md` publishes the file\n * verbatim (`type: \"skill-md\"`); a skill with supporting resources (scripts,\n * references, assets) is bundled into a deterministic `.tar.gz`\n * (`type: \"archive\"`) so its relative references resolve after unpacking.\n */\n\nexport const AGENT_SKILLS_DIR = \"/.well-known/agent-skills\";\nexport const AGENT_SKILLS_INDEX_PATH = \"/.well-known/agent-skills/index.json\";\n\nconst AGENT_SKILLS_SCHEMA =\n \"https://schemas.agentskills.io/discovery/0.2.0/schema.json\";\n\n/** Skill naming rule from the Agent Skills spec (1-64 chars enforced apart). */\nconst SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;\nconst SKILL_NAME_MAX = 64;\n/** The Agent Skills spec caps `description` at 1024 characters. */\nconst DESCRIPTION_MAX = 1024;\n\n/** One publishable skill artifact plus its index entry. */\nexport interface SkillArtifact {\n /** Raw bytes to publish (the digest is computed over exactly these). */\n content: Uint8Array;\n description: string;\n /** `sha256:{hex}` digest of `content`. */\n digest: string;\n name: string;\n /** Path under the agent-skills dir: `{name}/SKILL.md` or `{name}.tar.gz`. */\n path: string;\n type: \"archive\" | \"skill-md\";\n}\n\nexport interface CollectedSkills {\n skills: SkillArtifact[];\n /** Human-readable reasons for anything skipped. */\n warnings: string[];\n}\n\nconst sha256 = (content: Uint8Array): string =>\n `sha256:${createHash(\"sha256\").update(content).digest(\"hex\")}`;\n\n/**\n * Every regular file under a skill directory, as archive entries with\n * `/`-separated relative paths, dotfiles (`.DS_Store`, `.git`) excluded,\n * sorted for deterministic archives. The owner-execute bit is preserved so a\n * skill's scripts stay runnable after unpacking.\n */\nconst collectEntries = async (\n dir: string,\n prefix = \"\"\n): Promise<TarEntry[]> => {\n const entries: TarEntry[] = [];\n const items = await readdir(dir, { withFileTypes: true });\n for (const item of items.toSorted((a, b) => (a.name < b.name ? -1 : 1))) {\n if (item.name.startsWith(\".\")) {\n continue;\n }\n const path = join(dir, item.name);\n const relative = prefix ? `${prefix}/${item.name}` : item.name;\n if (item.isDirectory()) {\n // Sequential recursion keeps ordering deterministic.\n // oxlint-disable-next-line no-await-in-loop\n entries.push(...(await collectEntries(path, relative)));\n } else if (item.isFile()) {\n // oxlint-disable-next-line no-await-in-loop\n const [content, info] = await Promise.all([readFile(path), stat(path)]);\n entries.push({\n content: new Uint8Array(content),\n // oxlint-disable-next-line no-bitwise -- testing the owner-execute mode bit\n executable: (info.mode & 0o100) !== 0,\n path: relative,\n });\n }\n }\n return entries;\n};\n\n/** The two SKILL.md frontmatter fields the discovery index publishes. */\ninterface SkillMeta {\n description: string;\n name: string;\n}\n\ninterface SkillMetaResult {\n meta: SkillMeta | null;\n warning?: string;\n}\n\n/**\n * What js-yaml can put in a SKILL.md frontmatter field. Rich scalars (Dates)\n * ride along as the object arm; only strings are accepted below anyway.\n */\ntype FrontmatterField =\n | string\n | number\n | boolean\n | null\n | undefined\n | FrontmatterField[]\n | { [key: string]: FrontmatterField };\n\nconst isString = (value: FrontmatterField): value is string =>\n typeof value === \"string\";\n\n/** Frontmatter of a SKILL.md, or null with a warning when unusable. */\nconst skillMeta = (raw: string, dirName: string): SkillMetaResult => {\n let data: { description?: FrontmatterField; name?: FrontmatterField };\n try {\n ({ data } = matter(raw));\n } catch {\n return {\n meta: null,\n warning: `Skill \"${dirName}\" has unparsable SKILL.md frontmatter; skipped.`,\n };\n }\n const name = isString(data.name) ? data.name : \"\";\n const description = isString(data.description) ? data.description : \"\";\n if (!(name && description)) {\n return {\n meta: null,\n warning: `Skill \"${dirName}\" is missing the required \"name\"/\"description\" frontmatter; skipped.`,\n };\n }\n if (!SKILL_NAME.test(name) || name.length > SKILL_NAME_MAX) {\n return {\n meta: null,\n warning: `Skill \"${dirName}\" has an invalid name \"${name}\" (lowercase alphanumerics and single hyphens, max 64 chars); skipped.`,\n };\n }\n return {\n meta: { description: description.slice(0, DESCRIPTION_MAX), name },\n };\n};\n\n/**\n * Collect the publishable skills from a directory whose subdirectories each\n * hold a `SKILL.md` (the layout `npx skills add` consumes). Subdirectories\n * without one are ignored silently — the directory may hold other assets —\n * while a present-but-invalid skill earns a warning so it isn't dropped\n * behind the publisher's back.\n */\nexport const collectSkills = async (dir: string): Promise<CollectedSkills> => {\n const skills: SkillArtifact[] = [];\n const warnings: string[] = [];\n const items = await readdir(dir, { withFileTypes: true });\n for (const item of items.toSorted((a, b) => (a.name < b.name ? -1 : 1))) {\n if (!item.isDirectory() || item.name.startsWith(\".\")) {\n continue;\n }\n // oxlint-disable-next-line no-await-in-loop\n const entries = await collectEntries(join(dir, item.name));\n const skillMd = entries.find((entry) => entry.path === \"SKILL.md\");\n if (!skillMd) {\n continue;\n }\n const { meta, warning } = skillMeta(\n new TextDecoder().decode(skillMd.content),\n item.name\n );\n if (!meta) {\n if (warning) {\n warnings.push(warning);\n }\n continue;\n }\n // A lone SKILL.md ships verbatim; supporting resources ship as an\n // archive so the skill's relative references resolve after unpacking.\n const single = entries.length === 1;\n const content = single ? skillMd.content : buildTarGz(entries);\n skills.push({\n content,\n description: meta.description,\n digest: sha256(content),\n name: meta.name,\n path: single ? `${meta.name}/SKILL.md` : `${meta.name}.tar.gz`,\n type: single ? \"skill-md\" : \"archive\",\n });\n }\n return { skills, warnings };\n};\n\n/**\n * The discovery index (v0.2.0 schema). Artifact URLs are path-absolute under\n * `deployment.base` — the RFC resolves them against the index origin.\n */\nexport const buildSkillsIndex = (\n skills: readonly SkillArtifact[],\n config: ResolvedConfig\n): string => {\n const deployBase = normalizeBasePath(config.deployment.base);\n const index = {\n $schema: AGENT_SKILLS_SCHEMA,\n skills: skills.map((skill) => ({\n description: skill.description,\n digest: skill.digest,\n name: skill.name,\n type: skill.type,\n url: `${deployBase}${AGENT_SKILLS_DIR}/${skill.path}`,\n })),\n };\n return `${JSON.stringify(index, null, 2)}\\n`;\n};\n",
9
+ "import { gzipSync } from \"node:zlib\";\n\nimport { createTar } from \"nanotar\";\n\n/**\n * `.tar.gz` writer for agent-skill archives, on nanotar's ustar writer.\n * Deterministic by construction — fixed mtime/uid/gid/owner attrs,\n * caller-ordered entries, and Node's gzip header carries no timestamp — so a\n * skill's archive digest only changes when its content does. That holds per\n * machine: the tar bytes are portable, but zlib's compressed stream differs\n * across architectures, so gzip-layer digests are not comparable across\n * platforms.\n */\n\n/** One regular file to archive. Paths are `/`-separated, relative, no `..`. */\nexport interface TarEntry {\n /** Raw file bytes. */\n content: Uint8Array;\n /** Preserve the owner-execute bit (e.g. a skill's scripts). */\n executable?: boolean;\n /** Archive-relative path, e.g. `SKILL.md` or `references/FORMS.md`. */\n path: string;\n}\n\n/** ustar `name` field capacity; skill layouts are shallow, so no `prefix`. */\nconst NAME_MAX = 100;\n\nconst encoder = new TextEncoder();\n\n/**\n * Build a gzipped ustar archive of the given files, in the given order. Paths\n * longer than the ustar `name` field or escaping the archive root throw to\n * surface a programming error — nanotar would silently truncate an oversized\n * name into a corrupt archive, so the validation stays here.\n */\nexport const buildTarGz = (entries: readonly TarEntry[]): Uint8Array => {\n for (const entry of entries) {\n if (encoder.encode(entry.path).byteLength > NAME_MAX) {\n throw new Error(`tar path exceeds ${NAME_MAX} bytes: ${entry.path}`);\n }\n if (entry.path.startsWith(\"/\") || entry.path.split(\"/\").includes(\"..\")) {\n throw new Error(`tar path must be archive-relative: ${entry.path}`);\n }\n }\n const tar = createTar(\n entries.map((entry) => ({\n // Root-owned, epoch-mtime, empty owner names: every field a rebuild\n // could vary is pinned so the archive bytes are a function of content.\n attrs: {\n gid: 0,\n group: \"\",\n mode: entry.executable ? \"755\" : \"644\",\n mtime: 0,\n uid: 0,\n user: \"\",\n },\n data: entry.content,\n name: entry.path,\n }))\n );\n // Sync gzip with a pinned level; Node writes no timestamp into the header.\n return new Uint8Array(gzipSync(tar, { level: 9 }));\n};\n",
10
+ "import { normalizeBasePath, withBasePath } from \"../core/base-path.ts\";\nimport type { ResolvedConfig } from \"../core/schema.ts\";\nimport { absoluteUrl } from \"../core/site-url.ts\";\nimport { resolveReferences } from \"../openapi/references.ts\";\nimport { API_CATALOG_PATH, hasApiCatalog } from \"./api-catalog.ts\";\nimport { OPENAPI_PATH } from \"./api/paths.ts\";\nimport { asciiSlugify } from \"./mcp/discovery.ts\";\nimport { AGENT_SKILLS_DIR } from \"./skills.ts\";\nimport type { SkillArtifact } from \"./skills.ts\";\n\n/**\n * The AI Catalog (Agent-Card/ai-catalog data model, `specVersion` 1.0) at\n * `/.well-known/ai-catalog.json`, which is also the Agentic Resource\n * Discovery (ARD) manifest: one entry per agent-facing resource the site\n * publishes, each an `urn:air:<host>:<namespace>:<name>` identifier, a\n * display name, the artifact's media type, a URL, and the\n * `representativeQueries` registries embed for semantic search. Like the\n * RFC 9727 catalog next to it, everything is derived from `blume.config.ts`\n * and the build's published skills — nothing is hand-written.\n *\n * ARD v0.91 moved its manifest to `/.well-known/ard.json` and calls\n * `ai-catalog.json` the predecessor path that consumers only *may* consult,\n * while the ai-catalog spec and today's agent-readiness scanners key on\n * `ai-catalog.json`. The ARD manifest is just an `entries` array (other\n * members are ignored), so the one document is written to both paths.\n *\n * Served as plain `application/json`: the `.json` extension gets it from\n * every static host with no header rule, and the scanners that gate on the\n * file ask for exactly that type. The registered `application/ai-catalog+json`\n * is what the `Link`/`<link>` advertisements declare the document to be.\n */\n\nexport const AI_CATALOG_PATH = \"/.well-known/ai-catalog.json\";\nexport const ARD_MANIFEST_PATH = \"/.well-known/ard.json\";\nexport const AI_CATALOG_TYPE = \"application/ai-catalog+json\";\nconst SPEC_VERSION = \"1.0\";\n\n/** One catalog entry, restricted to the terms Blume emits. */\ninterface CatalogEntry {\n identifier: string;\n displayName: string;\n type: string;\n url: string;\n description?: string;\n capabilities?: string[];\n representativeQueries: string[];\n}\n\n/** An entry before its identifier and queries are resolved against the host. */\ninterface EntrySeed {\n /** `<namespace>:<name>` — the identifier's tail and the `queries` key. */\n key: string;\n displayName: string;\n type: string;\n url: string;\n description?: string;\n capabilities?: string[];\n /** Generated queries, replaced wholesale by a configured `queries[key]`. */\n queries: string[];\n}\n\nconst REFERENCE_KIND_LABEL = {\n asyncapi: \"AsyncAPI\",\n graphql: \"GraphQL\",\n openapi: \"OpenAPI\",\n} as const;\n\n/** The `urn:air` publisher: the configured site's hostname. */\nconst publisherHost = (site: string): string => new URL(site).hostname;\n\n/**\n * Whether the site publishes a catalog: the feature is on, a `deployment.site`\n * anchors the identifiers, and at least one entry exists. Every entry source\n * is a config flag, so the answer needs no build output — the same gate the\n * header rules, the `Link` header, and the head links read.\n */\nexport const hasAiCatalog = (config: ResolvedConfig): boolean =>\n config.ai.catalog.enabled &&\n Boolean(config.deployment.site) &&\n (config.ai.mcp.enabled ||\n config.ai.api ||\n config.ai.llmsTxt.enabled ||\n Boolean(config.ai.skills) ||\n resolveReferences(config).length > 0);\n\n/**\n * The `.well-known` documents agent registries fetch cross-origin (browser\n * agents, hosted registries reading through a page). Each needs\n * `Access-Control-Allow-Origin: *` on the static surface; the MCP endpoint\n * itself already sets it at runtime.\n */\nexport const crossOriginDiscoveryPaths = (config: ResolvedConfig): string[] => {\n const paths: string[] = [];\n if (hasAiCatalog(config)) {\n paths.push(AI_CATALOG_PATH, ARD_MANIFEST_PATH);\n }\n if (hasApiCatalog(config)) {\n paths.push(API_CATALOG_PATH);\n }\n if (config.ai.mcp.enabled) {\n paths.push(\"/.well-known/mcp.json\", \"/.well-known/mcp/server-card.json\");\n }\n return paths;\n};\n\nconst entrySeeds = (\n config: ResolvedConfig,\n skills: readonly SkillArtifact[],\n abs: (path: string) => string\n): EntrySeed[] => {\n const { title } = config;\n const seeds: EntrySeed[] = [];\n\n if (config.ai.mcp.enabled) {\n const name = config.ai.mcp.name ?? title;\n seeds.push({\n capabilities: [\"search_docs\", \"get_page\", \"list_pages\", \"get_navigation\"],\n description:\n config.ai.mcp.instructions ??\n `Model Context Protocol server over the ${title} documentation: full-text search, page Markdown, the page index, and the navigation tree.`,\n displayName: name,\n key: `mcp:${asciiSlugify(name) || \"docs\"}`,\n queries: [\n `search the ${title} documentation`,\n `get a ${title} docs page as Markdown`,\n `list every page in the ${title} docs`,\n ],\n type: \"application/mcp-server-card+json\",\n url: abs(\"/.well-known/mcp/server-card.json\"),\n });\n }\n\n for (const skill of skills) {\n seeds.push({\n description: skill.description,\n displayName: skill.name,\n key: `skill:${skill.name}`,\n queries: [\n `load the ${skill.name} agent skill`,\n `how do I use ${skill.name}`,\n ],\n type:\n skill.type === \"archive\"\n ? \"application/agent-skills+gzip\"\n : \"application/agent-skills+md\",\n url: abs(`${AGENT_SKILLS_DIR}/${skill.path}`),\n });\n }\n\n if (config.ai.api) {\n seeds.push({\n description: `REST API over the ${title} documentation: the page index, each page as JSON or Markdown, and the navigation tree, described by this OpenAPI document.`,\n displayName: `${title} docs API`,\n key: \"api:docs\",\n queries: [\n `fetch a ${title} docs page as JSON`,\n `list the pages in the ${title} docs`,\n `get the ${title} docs navigation tree`,\n ],\n type: \"application/vnd.oai.openapi+json\",\n url: abs(OPENAPI_PATH),\n });\n }\n\n for (const reference of resolveReferences(config)) {\n // Blume-rendered pages mount under `basePath`; Scalar pages stay at the\n // raw route (see `referenceRoutes`). The rendered reference is the\n // resource this publisher owns — the spec itself is catalogued by URL in\n // the RFC 9727 linkset.\n const docRoute =\n reference.renderer === \"blume\"\n ? withBasePath(reference.basePath, reference.route)\n : reference.route;\n seeds.push({\n description: `${reference.label}: rendered ${REFERENCE_KIND_LABEL[reference.kind]} reference in the ${title} documentation.`,\n displayName: reference.label,\n key: `reference:${reference.slug}`,\n queries: [\n `what operations does the ${reference.label} API expose`,\n `how do I call the ${reference.label} API`,\n ],\n type: \"text/html\",\n url: abs(docRoute),\n });\n }\n\n if (config.ai.llmsTxt.enabled) {\n seeds.push({\n description: `llms.txt index of the ${title} documentation: every page with a one-line summary, plus the agent-facing resources on this site.`,\n displayName: `${title} llms.txt`,\n key: \"docs:llms-txt\",\n queries: [`what is ${title}`, `overview of the ${title} documentation`],\n type: \"text/plain\",\n url: abs(\"/llms.txt\"),\n });\n }\n\n return seeds;\n};\n\n/** The catalog document, or null when the site publishes none. */\nexport const buildAiCatalog = (\n config: ResolvedConfig,\n skills: readonly SkillArtifact[]\n): string | null => {\n const site = config.deployment.site ?? null;\n if (!(site && hasAiCatalog(config))) {\n return null;\n }\n const deployBase = normalizeBasePath(config.deployment.base);\n const abs = (path: string): string =>\n absoluteUrl(site, withBasePath(deployBase, path));\n const host = publisherHost(site);\n const entries = entrySeeds(config, skills, abs).map((seed): CatalogEntry => {\n const entry: CatalogEntry = {\n displayName: seed.displayName,\n identifier: `urn:air:${host}:${seed.key}`,\n representativeQueries:\n config.ai.catalog.queries[seed.key] ?? seed.queries,\n type: seed.type,\n url: seed.url,\n };\n if (seed.description) {\n entry.description = seed.description;\n }\n if (seed.capabilities) {\n entry.capabilities = seed.capabilities;\n }\n return entry;\n });\n const catalog = {\n entries,\n host: {\n displayName: config.title,\n documentationUrl: abs(\"/\"),\n identifier: `did:web:${host}`,\n },\n specVersion: SPEC_VERSION,\n };\n return `${JSON.stringify(catalog, null, 2)}\\n`;\n};\n"
11
+ ],
12
+ "mappings": "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",
13
+ "debugId": "26AE621DEDAFCC8A64756E2164756E21",
14
+ "names": []
15
+ }
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  countBySeverity
4
- } from "./chunk-ey89bjj1.js";
4
+ } from "./index.js";
5
5
 
6
6
  // src/audit/report.ts
7
7
  import { colors } from "consola/utils";
@@ -949,4 +949,4 @@ var launchAgent = async (bin, prompt, platform = process.platform) => {
949
949
  export { CHECKS, finding, formatReport, reportJson, formatCatalog, AGENTS, writeAgentReport, fixPrompt, launchAgent };
950
950
 
951
951
  //# debugId=C3633A8B6C87D94F64756E2164756E21
952
- //# sourceMappingURL=chunk-8gnpdsn1.js.map
952
+ //# sourceMappingURL=chunk-0xjyb285.js.map