blume 1.4.2 → 1.5.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.
- package/CHANGELOG.md +71 -0
- package/dist/cli/index.js +2260 -1100
- package/dist/cli/index.js.map +123 -117
- package/dist/types/ai/component-markdown.d.ts +14 -4
- package/dist/types/core/base-path.d.ts +8 -0
- package/dist/types/core/config-input.d.ts +87 -27
- package/dist/types/core/config.d.ts +2 -1
- package/dist/types/core/data.d.ts +16 -1
- package/dist/types/core/diagnostics.d.ts +5 -1
- package/dist/types/core/i18n-ui.d.ts +12 -0
- package/dist/types/core/schema.d.ts +116 -15
- package/dist/types/core/sources/types.d.ts +11 -1
- package/dist/types/core/standard-schema.d.ts +7 -3
- package/dist/types/core/types.d.ts +43 -2
- package/dist/types/core/ui-packs/index.d.ts +9 -1
- package/dist/types/openapi/references.d.ts +14 -7
- package/dist/types/seo/x-handle.d.ts +3 -2
- package/docs/advanced/api-reference.mdx +8 -6
- package/docs/configuration/ai.mdx +26 -8
- package/docs/configuration/search.mdx +2 -0
- package/docs/configuration/seo.mdx +1 -1
- package/docs/content/i18n.mdx +1 -1
- package/docs/content/meta.mdx +2 -1
- package/docs/content/meta.ts +1 -0
- package/docs/content/navigation.mdx +35 -1
- package/docs/content/sources.mdx +1 -1
- package/docs/content/versioning.mdx +106 -0
- package/docs/reference/cli.mdx +1 -0
- package/docs/reference/frontmatter.mdx +3 -0
- package/package.json +13 -1
- package/skills/blume-migrate/SKILL.md +2 -2
- package/skills/blume-migrate/references/docusaurus.md +1 -1
- package/skills/blume-migrate/references/fumadocs.md +1 -1
- package/skills/blume-migrate/references/mintlify.md +1 -1
- package/src/ai/agent-readability.ts +40 -12
- package/src/ai/api-catalog.ts +2 -2
- package/src/ai/ask-context.ts +49 -12
- package/src/ai/ask.ts +10 -1
- package/src/ai/component-markdown.ts +80 -43
- package/src/ai/llms.ts +42 -17
- package/src/ai/mcp/data.ts +48 -12
- package/src/ai/mcp/discovery.ts +52 -16
- package/src/ai/mcp/server.ts +280 -125
- package/src/ai/mcp/tools.ts +3 -3
- package/src/ai/skills.ts +32 -9
- package/src/ai/tar.ts +29 -70
- package/src/ai/visibility.ts +2 -2
- package/src/astro/component-slots.ts +2 -0
- package/src/astro/examples.ts +13 -5
- package/src/astro/generate.ts +113 -63
- package/src/astro/integration.ts +13 -2
- package/src/astro/islands.ts +23 -12
- package/src/astro/templates.ts +185 -41
- package/src/audit/agent.ts +16 -31
- package/src/audit/checks/content.ts +26 -11
- package/src/audit/checks/dns-aid.ts +3 -0
- package/src/audit/checks/indexability.ts +24 -6
- package/src/audit/checks/llms.ts +9 -4
- package/src/audit/checks/network.ts +2 -0
- package/src/audit/checks/social.ts +18 -10
- package/src/audit/crawl.ts +78 -25
- package/src/audit/report.ts +20 -19
- package/src/audit/run.ts +15 -5
- package/src/audit/snapshot.ts +29 -6
- package/src/audit/types.ts +25 -3
- package/src/blume-modules.d.ts +5 -1
- package/src/cli/commands/audit.ts +21 -21
- package/src/cli/commands/build.ts +30 -16
- package/src/cli/commands/dev.ts +15 -15
- package/src/cli/commands/doctor.ts +2 -0
- package/src/cli/commands/eject.ts +4 -4
- package/src/cli/commands/eval.ts +24 -30
- package/src/cli/commands/init.ts +9 -9
- package/src/cli/commands/mcp-stdio.ts +3 -0
- package/src/cli/commands/translate.ts +14 -3
- package/src/cli/commands/version.ts +85 -0
- package/src/cli/dev-lock.ts +31 -10
- package/src/cli/eject-scripts.ts +17 -2
- package/src/cli/env.ts +13 -30
- package/src/cli/index.ts +2 -0
- package/src/cli/init/questions.ts +1 -1
- package/src/cli/init/scaffold.ts +41 -13
- package/src/cli/internal-error.ts +1 -0
- package/src/cli/report-format.ts +22 -0
- package/src/components/content/AccordionItem.astro +2 -9
- package/src/components/content/ColorItem.astro +5 -13
- package/src/components/content/Component.astro +12 -8
- package/src/components/content/Frame.astro +2 -12
- package/src/components/content/Prompt.astro +12 -31
- package/src/components/content/Tab.astro +2 -9
- package/src/components/content/Tooltip.astro +1 -9
- package/src/components/content/Update.astro +2 -9
- package/src/components/content/auto-type-table.ts +3 -0
- package/src/components/content/diff.ts +9 -5
- package/src/components/content/github-info.ts +2 -0
- package/src/components/content/inline-markdown.ts +28 -0
- package/src/components/copy-feedback.ts +96 -0
- package/src/components/islands/ask-ai.tsx +111 -34
- package/src/components/islands/hooks.ts +5 -1
- package/src/components/islands/webmcp.ts +49 -12
- package/src/components/layout/Header.astro +25 -1
- package/src/components/layout/NavSelector.astro +11 -2
- package/src/components/layout/NavTree.astro +4 -2
- package/src/components/layout/PageActions.astro +20 -32
- package/src/components/layout/PageLayout.astro +8 -28
- package/src/components/layout/RootLayout.astro +24 -48
- package/src/components/layout/Search.astro +133 -22
- package/src/components/layout/VersionBanner.astro +39 -0
- package/src/components/layout/analytics-client.ts +8 -5
- package/src/components/layout/drawer-inert.ts +31 -0
- package/src/components/layout/hydration-hint.ts +1 -1
- package/src/components/layout/nav-utils.ts +1 -4
- package/src/components/layout/overrides.ts +25 -12
- package/src/components/layout/search/algolia.ts +18 -5
- package/src/components/layout/search/endpoint.ts +3 -0
- package/src/components/layout/search/flexsearch.ts +23 -7
- package/src/components/layout/search/orama-cloud.ts +1 -1
- package/src/components/layout/search/orama.ts +4 -1
- package/src/components/layout/search/pagefind.ts +8 -5
- package/src/components/layout/search/types.ts +45 -1
- package/src/components/layout/search/typesense.ts +19 -3
- package/src/components/openapi/ApiOverview.astro +32 -6
- package/src/components/openapi/AsyncApiOperation.astro +237 -0
- package/src/components/openapi/Bindings.astro +89 -0
- package/src/components/openapi/MethodBadge.astro +3 -0
- package/src/components/openapi/Operation.astro +7 -2
- package/src/components/openapi/PanelTabs.astro +131 -0
- package/src/components/openapi/ParametersTable.astro +2 -0
- package/src/components/openapi/RequestPanel.astro +12 -119
- package/src/components/openapi/async-snippets.ts +174 -0
- package/src/components/openapi/async.ts +348 -0
- package/src/components/openapi/helpers.ts +52 -20
- package/src/components/openapi/panel.ts +11 -8
- package/src/components/openapi/security.ts +102 -29
- package/src/components/openapi/snippets.ts +11 -11
- package/src/components/raf-throttle.ts +21 -0
- package/src/components/slug.ts +14 -0
- package/src/core/base-path.ts +18 -1
- package/src/core/component-overrides.ts +28 -23
- package/src/core/config-input.ts +96 -27
- package/src/core/config.ts +20 -7
- package/src/core/content.ts +3 -1
- package/src/core/data.ts +16 -1
- package/src/core/define-components.ts +5 -0
- package/src/core/diagnostics.ts +46 -38
- package/src/core/frontmatter.ts +74 -4
- package/src/core/graph.ts +137 -53
- package/src/core/i18n-ui.ts +15 -0
- package/src/core/i18n.ts +16 -8
- package/src/core/load-module.ts +1 -0
- package/src/core/manifest.ts +92 -3
- package/src/core/meta.ts +44 -14
- package/src/core/nav-diagnostics.ts +3 -3
- package/src/core/navigation.ts +247 -67
- package/src/core/probe.ts +7 -19
- package/src/core/project-graph.ts +27 -4
- package/src/core/schema.ts +219 -67
- package/src/core/site-url.ts +27 -0
- package/src/core/sources/assets.ts +2 -0
- package/src/core/sources/cache.ts +16 -8
- package/src/core/sources/github-releases.ts +39 -11
- package/src/core/sources/mdx-remote.ts +4 -0
- package/src/core/sources/normalize.ts +93 -22
- package/src/core/sources/notion.ts +76 -22
- package/src/core/sources/portable-text.ts +48 -12
- package/src/core/sources/resolve.ts +1 -0
- package/src/core/sources/sanity.ts +68 -14
- package/src/core/sources/types.ts +17 -1
- package/src/core/sources/watch.ts +1 -1
- package/src/core/standard-schema.ts +9 -3
- package/src/core/text-width.ts +26 -0
- package/src/core/tsconfig-aliases.ts +9 -5
- package/src/core/types.ts +45 -2
- package/src/core/ui-packs/index.ts +9 -1
- package/src/core/version-cut.ts +301 -0
- package/src/core/version.ts +2 -0
- package/src/core/versions.ts +170 -0
- package/src/deploy/adapter-output.ts +5 -2
- package/src/deploy/cloudflare-negotiation.ts +40 -11
- package/src/deploy/robots.ts +2 -1
- package/src/deploy/rss.ts +2 -1
- package/src/deploy/sitemap.ts +89 -8
- package/src/deploy/vercel-negotiation.ts +11 -4
- package/src/eval/agents.ts +13 -10
- package/src/eval/report.ts +5 -18
- package/src/eval/run.ts +2 -2
- package/src/eval/schema.ts +1 -1
- package/src/markdown/base-links.ts +6 -6
- package/src/markdown/directives.ts +7 -1
- package/src/markdown/heading-anchors.ts +17 -6
- package/src/markdown/index.ts +73 -24
- package/src/markdown/inline-code.ts +14 -2
- package/src/markdown/language-icon.ts +6 -2
- package/src/markdown/mdast.ts +18 -4
- package/src/markdown/package-commands.ts +63 -58
- package/src/markdown/table-wrap.ts +4 -1
- package/src/markdown/twoslash.ts +2 -0
- package/src/og/card.ts +50 -33
- package/src/og/derive.ts +43 -27
- package/src/openapi/asyncapi.ts +366 -0
- package/src/openapi/model.ts +135 -66
- package/src/openapi/parse.ts +166 -33
- package/src/openapi/references.ts +47 -22
- package/src/openapi/render-mdx.ts +137 -59
- package/src/openapi/scalar.ts +8 -10
- package/src/openapi/source.ts +126 -29
- package/src/registry/eject.ts +7 -2
- package/src/search/documents.ts +103 -39
- package/src/search/facets.ts +7 -5
- package/src/search/orama-index.ts +117 -32
- package/src/search/popular.ts +10 -5
- package/src/search/providers.ts +2 -2
- package/src/search/sync/index.ts +2 -0
- package/src/search/sync/typesense.ts +4 -2
- package/src/seo/jsonld.ts +24 -6
- package/src/seo/x-handle.ts +8 -3
- package/src/theme/chrome-icons.ts +7 -2
- package/src/theme/fonts.ts +8 -4
- package/src/theme/icons.ts +4 -2
- package/src/theme/palette.ts +27 -15
- package/src/translate/ledger.ts +4 -2
- package/src/translate/meta.ts +15 -6
- package/src/translate/report.ts +10 -19
- package/src/translate/run.ts +29 -38
- package/src/translate/validate.ts +52 -17
- package/src/translate/work-list.ts +0 -0
- package/src/cli/coalesce.ts +0 -43
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { normalizeBasePath, withBasePath } from "../core/base-path.ts";
|
|
2
2
|
import type { BlumeProject } from "../core/project-graph.ts";
|
|
3
3
|
import type { ContentSignalPolicy, ContentSignals } from "../core/schema.ts";
|
|
4
|
+
import { absoluteUrl } from "../core/site-url.ts";
|
|
4
5
|
import { buildRssFeeds } from "../deploy/rss.ts";
|
|
5
6
|
import { hasApiCatalog } from "./api-catalog.ts";
|
|
6
7
|
|
|
@@ -42,16 +43,45 @@ const askApiUrl = (
|
|
|
42
43
|
return abs("/api/ask");
|
|
43
44
|
}
|
|
44
45
|
return site && endpoint.startsWith("/")
|
|
45
|
-
?
|
|
46
|
+
? absoluteUrl(site, endpoint)
|
|
46
47
|
: endpoint;
|
|
47
48
|
};
|
|
48
49
|
|
|
50
|
+
/** The `.well-known` discovery URLs a site can publish. */
|
|
51
|
+
interface WellKnownArtifacts {
|
|
52
|
+
httpMessageSignaturesDirectory?: string;
|
|
53
|
+
apiCatalog?: string;
|
|
54
|
+
agentSkills?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The agent-facing artifact index the manifest publishes. */
|
|
58
|
+
interface AgentArtifacts extends WellKnownArtifacts {
|
|
59
|
+
markdown: { contentNegotiation?: string; pattern: string };
|
|
60
|
+
llmsFullTxt?: string;
|
|
61
|
+
llmsTxt?: string;
|
|
62
|
+
mcp?: { discovery: string; url: string };
|
|
63
|
+
askApi?: string;
|
|
64
|
+
sitemap?: string;
|
|
65
|
+
feeds?: string[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The published `agent-readability.json` document. */
|
|
69
|
+
export interface AgentReadabilityManifest {
|
|
70
|
+
artifacts: AgentArtifacts;
|
|
71
|
+
description?: string;
|
|
72
|
+
generator?: string;
|
|
73
|
+
name: string;
|
|
74
|
+
site: string | null;
|
|
75
|
+
contentUsage?: Record<string, boolean>;
|
|
76
|
+
repository?: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
49
79
|
/** The `.well-known` discovery artifacts the site publishes, if any. */
|
|
50
80
|
const wellKnownArtifacts = (
|
|
51
81
|
config: BlumeProject["config"],
|
|
52
82
|
abs: (path: string) => string
|
|
53
|
-
):
|
|
54
|
-
const artifacts:
|
|
83
|
+
): WellKnownArtifacts => {
|
|
84
|
+
const artifacts: WellKnownArtifacts = {};
|
|
55
85
|
if (config.ai.webBotAuth.keys.length > 0) {
|
|
56
86
|
artifacts.httpMessageSignaturesDirectory = abs(
|
|
57
87
|
"/.well-known/http-message-signatures-directory"
|
|
@@ -76,7 +106,7 @@ const wellKnownArtifacts = (
|
|
|
76
106
|
*/
|
|
77
107
|
export const buildAgentReadability = (
|
|
78
108
|
project: BlumeProject
|
|
79
|
-
):
|
|
109
|
+
): AgentReadabilityManifest | null => {
|
|
80
110
|
const { config } = project;
|
|
81
111
|
if (!config.seo.agentReadability) {
|
|
82
112
|
return null;
|
|
@@ -88,7 +118,7 @@ export const buildAgentReadability = (
|
|
|
88
118
|
const deployBase = normalizeBasePath(config.deployment.base);
|
|
89
119
|
const abs = (path: string): string => {
|
|
90
120
|
const based = withBasePath(deployBase, path);
|
|
91
|
-
return site ?
|
|
121
|
+
return site ? absoluteUrl(site, based) : based;
|
|
92
122
|
};
|
|
93
123
|
|
|
94
124
|
// Advertise `Accept: text/markdown` negotiation only where the deployed site
|
|
@@ -102,12 +132,10 @@ export const buildAgentReadability = (
|
|
|
102
132
|
config.deployment.output === "server" &&
|
|
103
133
|
(config.deployment.adapter === "vercel" ||
|
|
104
134
|
config.deployment.adapter === "cloudflare");
|
|
105
|
-
const
|
|
106
|
-
markdown: {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
},
|
|
110
|
-
};
|
|
135
|
+
const markdown: AgentArtifacts["markdown"] = negotiates
|
|
136
|
+
? { contentNegotiation: "text/markdown", pattern: abs("/{route}.md") }
|
|
137
|
+
: { pattern: abs("/{route}.md") };
|
|
138
|
+
const artifacts: AgentArtifacts = { markdown };
|
|
111
139
|
if (config.ai.llmsTxt.enabled) {
|
|
112
140
|
artifacts.llmsFullTxt = abs("/llms-full.txt");
|
|
113
141
|
artifacts.llmsTxt = abs("/llms.txt");
|
|
@@ -134,7 +162,7 @@ export const buildAgentReadability = (
|
|
|
134
162
|
}
|
|
135
163
|
|
|
136
164
|
const version = project.manifest?.blumeVersion;
|
|
137
|
-
const manifest:
|
|
165
|
+
const manifest: AgentReadabilityManifest = {
|
|
138
166
|
artifacts,
|
|
139
167
|
description: config.description,
|
|
140
168
|
generator: version ? `blume@${version}` : undefined,
|
package/src/ai/api-catalog.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { normalizeBasePath, withBasePath } from "../core/base-path.ts";
|
|
2
2
|
import type { ResolvedConfig } from "../core/schema.ts";
|
|
3
|
-
import {
|
|
3
|
+
import { absoluteUrl } from "../core/site-url.ts";
|
|
4
4
|
import { resolveReferences } from "../openapi/references.ts";
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -32,7 +32,7 @@ const linksetEntries = (config: ResolvedConfig): LinksetEntry[] => {
|
|
|
32
32
|
const deployBase = normalizeBasePath(config.deployment.base);
|
|
33
33
|
const abs = (path: string): string => {
|
|
34
34
|
const based = withBasePath(deployBase, path);
|
|
35
|
-
return site ?
|
|
35
|
+
return site ? absoluteUrl(site, based) : based;
|
|
36
36
|
};
|
|
37
37
|
|
|
38
38
|
const entries: LinksetEntry[] = [];
|
package/src/ai/ask-context.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizeRoute } from "../core/base-path.ts";
|
|
1
2
|
import { buildOramaIndex, queryOramaIndex } from "../search/orama-index.ts";
|
|
2
3
|
import type { OramaDoc } from "../search/orama-index.ts";
|
|
3
4
|
|
|
@@ -90,11 +91,43 @@ const STOPWORDS = new Set([
|
|
|
90
91
|
"your",
|
|
91
92
|
]);
|
|
92
93
|
|
|
94
|
+
/** A run of letters, combining marks and digits inside a word-like segment. */
|
|
95
|
+
const TERM = /[\p{L}\p{M}\p{N}]+/gu;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Word-shaped pieces of a query, NFC-normalized and lowercased. Languages
|
|
99
|
+
* written without spaces (the CJK/Thai sites the Orama tokenizer goes out of
|
|
100
|
+
* its way to support) have no delimiter for a regex to split on, so the query
|
|
101
|
+
* is cut with `Intl.Segmenter` where available — otherwise every excerpt
|
|
102
|
+
* window silently degrades to the head of the page. The regex fallback covers
|
|
103
|
+
* runtimes without the segmenter and still handles spaced scripts correctly.
|
|
104
|
+
*/
|
|
105
|
+
const hasSegmenter = (
|
|
106
|
+
segmenter: typeof Intl.Segmenter | undefined
|
|
107
|
+
): segmenter is typeof Intl.Segmenter => typeof segmenter === "function";
|
|
108
|
+
|
|
109
|
+
const segmentQuery = (query: string): string[] => {
|
|
110
|
+
const lowered = query.normalize("NFC").toLowerCase();
|
|
111
|
+
if (!hasSegmenter(Intl.Segmenter)) {
|
|
112
|
+
return lowered.match(TERM) ?? [];
|
|
113
|
+
}
|
|
114
|
+
const pieces: string[] = [];
|
|
115
|
+
const segmenter = new Intl.Segmenter(undefined, { granularity: "word" });
|
|
116
|
+
for (const segment of segmenter.segment(lowered)) {
|
|
117
|
+
if (segment.isWordLike) {
|
|
118
|
+
pieces.push(segment.segment);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return pieces;
|
|
122
|
+
};
|
|
123
|
+
|
|
93
124
|
/** Distinct, meaningful lowercase terms from a query (drops stopwords). */
|
|
94
|
-
const queryTerms = (query: string): string[] =>
|
|
95
|
-
|
|
125
|
+
const queryTerms = (query: string): string[] => {
|
|
126
|
+
const terms = segmentQuery(query).flatMap((piece) => piece.match(TERM) ?? []);
|
|
127
|
+
return [...new Set(terms)].filter(
|
|
96
128
|
(term) => term.length >= 2 && !STOPWORDS.has(term)
|
|
97
129
|
);
|
|
130
|
+
};
|
|
98
131
|
|
|
99
132
|
/**
|
|
100
133
|
* The grounding preamble. The model is told to answer strictly from the injected
|
|
@@ -104,13 +137,6 @@ const queryTerms = (query: string): string[] =>
|
|
|
104
137
|
const BASE_INSTRUCTION =
|
|
105
138
|
"You are a helpful documentation assistant for this project. Answer the user's question using ONLY the documentation excerpts below. Each excerpt is headed by its page as `## Page Title (/route)`. If the answer is not covered by the excerpts, say you don't know and suggest where in the docs to look — do not invent details. Always cite the pages you drew from, and write every citation as a Markdown link to that page using its route, e.g. [Page Title](/route).";
|
|
106
139
|
|
|
107
|
-
/** Normalize a page path to a document `route` (`/`, `/a/b`, no trailing slash). */
|
|
108
|
-
const normalizeRoute = (input: string): string => {
|
|
109
|
-
const noTrailing = input.trim().replace(/\/+$/u, "");
|
|
110
|
-
const withSlash = noTrailing.startsWith("/") ? noTrailing : `/${noTrailing}`;
|
|
111
|
-
return withSlash === "" ? "/" : withSlash;
|
|
112
|
-
};
|
|
113
|
-
|
|
114
140
|
/** The most recent non-empty user message, used as the retrieval query. */
|
|
115
141
|
const lastUserMessage = (messages: AskMessage[]): string => {
|
|
116
142
|
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
@@ -137,7 +163,9 @@ export const relevantExcerpt = (
|
|
|
137
163
|
query: string,
|
|
138
164
|
max: number
|
|
139
165
|
): string => {
|
|
140
|
-
|
|
166
|
+
// NFC to match the normalized query terms; positions are computed on (and
|
|
167
|
+
// sliced from) this same string, so offsets stay aligned.
|
|
168
|
+
const trimmed = content.normalize("NFC").trim();
|
|
141
169
|
if (trimmed.length <= max) {
|
|
142
170
|
return trimmed;
|
|
143
171
|
}
|
|
@@ -199,9 +227,15 @@ export const relevantExcerpt = (
|
|
|
199
227
|
* grounded system prompt — the retrieved excerpts plus the page the user is
|
|
200
228
|
* viewing — or `undefined` when there is nothing to ground on, so the endpoint
|
|
201
229
|
* can fall back to its plain prompt.
|
|
230
|
+
*
|
|
231
|
+
* `options.instructions` (the `ai.ask.instructions` config) is appended after
|
|
232
|
+
* the base instruction rather than replacing it: the base carries the
|
|
233
|
+
* functional contract (answer only from the excerpts, cite pages as Markdown
|
|
234
|
+
* links) that the panel's citation rendering depends on.
|
|
202
235
|
*/
|
|
203
236
|
export const createAskContext = (
|
|
204
|
-
data: AskData
|
|
237
|
+
data: AskData,
|
|
238
|
+
options?: { instructions?: string }
|
|
205
239
|
): ((
|
|
206
240
|
messages: AskMessage[],
|
|
207
241
|
page?: AskPage
|
|
@@ -213,6 +247,9 @@ export const createAskContext = (
|
|
|
213
247
|
return dbPromise;
|
|
214
248
|
};
|
|
215
249
|
const byRoute = new Map(data.documents.map((doc) => [doc.route, doc]));
|
|
250
|
+
const instruction = options?.instructions
|
|
251
|
+
? `${BASE_INSTRUCTION}\n\n${options.instructions}`
|
|
252
|
+
: BASE_INSTRUCTION;
|
|
216
253
|
|
|
217
254
|
return async (messages, page) => {
|
|
218
255
|
const list = Array.isArray(messages) ? messages : [];
|
|
@@ -257,6 +294,6 @@ export const createAskContext = (
|
|
|
257
294
|
if (sections.length === 0) {
|
|
258
295
|
return;
|
|
259
296
|
}
|
|
260
|
-
return `${
|
|
297
|
+
return `${instruction}\n\n<docs>\n${sections.join("\n\n")}\n</docs>`;
|
|
261
298
|
};
|
|
262
299
|
};
|
package/src/ai/ask.ts
CHANGED
|
@@ -35,7 +35,16 @@ const OPENAI_COMPATIBLE = "openai-compatible";
|
|
|
35
35
|
/** The AI SDK provider package the OpenAI-compatible providers install. */
|
|
36
36
|
const OPENAI_COMPATIBLE_DEP = "@ai-sdk/openai-compatible";
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Connection presets keyed by provider name. Open-keyed on purpose: lookups
|
|
40
|
+
* use the configured `ai.ask.provider`, which includes values with no preset
|
|
41
|
+
* (the generic `openai-compatible`).
|
|
42
|
+
*/
|
|
43
|
+
interface AskPresetRegistry {
|
|
44
|
+
[provider: string]: AskPreset;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const ASK_PRESETS: AskPresetRegistry = {
|
|
39
48
|
inkeep: {
|
|
40
49
|
apiKeyEnv: "INKEEP_API_KEY",
|
|
41
50
|
baseUrl: "https://api.inkeep.com/v1",
|
|
@@ -47,10 +47,31 @@ interface Splice {
|
|
|
47
47
|
text: string;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* A statically-recovered data value. Parsed front matter and evaluated
|
|
52
|
+
* attribute literals are both plain data — scalars, dates, arrays, and
|
|
53
|
+
* nested maps — never functions or class instances.
|
|
54
|
+
*/
|
|
55
|
+
export type EvaluatedValue =
|
|
56
|
+
| string
|
|
57
|
+
| number
|
|
58
|
+
| boolean
|
|
59
|
+
| null
|
|
60
|
+
| undefined
|
|
61
|
+
| Date
|
|
62
|
+
| EvaluatedValue[]
|
|
63
|
+
| { [key: string]: EvaluatedValue };
|
|
64
|
+
|
|
65
|
+
const isString = <Value>(value: Value): value is Value & string =>
|
|
66
|
+
typeof value === "string";
|
|
67
|
+
|
|
68
|
+
const isNumber = <Value>(value: Value): value is Value & number =>
|
|
69
|
+
typeof value === "number";
|
|
70
|
+
|
|
50
71
|
/** Evaluated props plus whether any attribute resisted static evaluation. */
|
|
51
72
|
interface EvaluatedProps {
|
|
52
73
|
lossy: boolean;
|
|
53
|
-
props: Record<string,
|
|
74
|
+
props: Record<string, EvaluatedValue>;
|
|
54
75
|
}
|
|
55
76
|
|
|
56
77
|
/** A child component extracted by name (e.g. each `<Step>` under `<Steps>`). */
|
|
@@ -70,7 +91,7 @@ export interface ComponentMarkdownContext extends EvaluatedProps {
|
|
|
70
91
|
* serializer read page metadata directly, even when a prop expression is
|
|
71
92
|
* not statically evaluable.
|
|
72
93
|
*/
|
|
73
|
-
frontmatter: Record<string,
|
|
94
|
+
frontmatter: Record<string, EvaluatedValue>;
|
|
74
95
|
}
|
|
75
96
|
|
|
76
97
|
/**
|
|
@@ -93,15 +114,16 @@ export type ComponentMarkdown = (
|
|
|
93
114
|
*/
|
|
94
115
|
const evaluateExpression = (
|
|
95
116
|
raw: string,
|
|
96
|
-
frontmatter: Record<string,
|
|
97
|
-
)
|
|
117
|
+
frontmatter: Record<string, EvaluatedValue> | undefined
|
|
118
|
+
) => {
|
|
98
119
|
try {
|
|
99
120
|
// Build-time eval of the author's own attribute literals; a throw falls
|
|
100
121
|
// back to leaving the JSX verbatim.
|
|
101
122
|
// oxlint-disable-next-line no-new-func
|
|
102
|
-
const value = new Function(
|
|
103
|
-
frontmatter
|
|
104
|
-
|
|
123
|
+
const value: EvaluatedValue = new Function(
|
|
124
|
+
"frontmatter",
|
|
125
|
+
`"use strict"; return (${raw});`
|
|
126
|
+
)(frontmatter);
|
|
105
127
|
return { ok: true, value };
|
|
106
128
|
} catch {
|
|
107
129
|
return { ok: false, value: undefined };
|
|
@@ -111,9 +133,9 @@ const evaluateExpression = (
|
|
|
111
133
|
/** Evaluate an element's attributes into a plain props object. */
|
|
112
134
|
const readProps = (
|
|
113
135
|
node: MdastNode,
|
|
114
|
-
frontmatter: Record<string,
|
|
136
|
+
frontmatter: Record<string, EvaluatedValue> | undefined
|
|
115
137
|
): EvaluatedProps => {
|
|
116
|
-
const props: Record<string,
|
|
138
|
+
const props: Record<string, EvaluatedValue> = {};
|
|
117
139
|
let lossy = false;
|
|
118
140
|
for (const attribute of node.attributes ?? []) {
|
|
119
141
|
// A spread ({...props}) can't be recovered statically.
|
|
@@ -124,7 +146,7 @@ const readProps = (
|
|
|
124
146
|
if (attribute.value === null || attribute.value === undefined) {
|
|
125
147
|
// Boolean shorthand: `<Steps compact>`.
|
|
126
148
|
props[attribute.name] = true;
|
|
127
|
-
} else if (
|
|
149
|
+
} else if (isString(attribute.value)) {
|
|
128
150
|
props[attribute.name] = attribute.value;
|
|
129
151
|
} else {
|
|
130
152
|
const result = evaluateExpression(attribute.value.value, frontmatter);
|
|
@@ -198,35 +220,49 @@ const isJsxElement = (node: MdastNode): boolean =>
|
|
|
198
220
|
node.type === "mdxJsxFlowElement" || node.type === "mdxJsxTextElement";
|
|
199
221
|
|
|
200
222
|
/** Flatten a value to a single Markdown table cell (pipes escaped). */
|
|
201
|
-
const cellText = (value:
|
|
223
|
+
const cellText = (value: EvaluatedValue): string =>
|
|
202
224
|
String(value ?? "")
|
|
203
225
|
.replaceAll(/\s*\n\s*/gu, " ")
|
|
204
226
|
.replaceAll("|", "\\|")
|
|
205
227
|
.trim();
|
|
206
228
|
|
|
207
229
|
/** A cell rendered as inline code, unless the value itself uses backticks. */
|
|
208
|
-
const cellCode = (value:
|
|
230
|
+
const cellCode = (value: EvaluatedValue): string => {
|
|
209
231
|
const text = cellText(value);
|
|
210
232
|
return text && !text.includes("`") ? `\`${text}\`` : text;
|
|
211
233
|
};
|
|
212
234
|
|
|
213
|
-
/**
|
|
235
|
+
/**
|
|
236
|
+
* One `<TypeTable type={{...}}>` entry, matching the component's props. The
|
|
237
|
+
* index signature keeps the interface interchangeable with the evaluated
|
|
238
|
+
* data-value maps it is narrowed from.
|
|
239
|
+
*/
|
|
214
240
|
interface TypeEntry {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
241
|
+
[field: string]: EvaluatedValue;
|
|
242
|
+
default?: EvaluatedValue;
|
|
243
|
+
description?: EvaluatedValue;
|
|
244
|
+
required?: EvaluatedValue;
|
|
245
|
+
type?: EvaluatedValue;
|
|
246
|
+
typeDescription?: EvaluatedValue;
|
|
247
|
+
typeDescriptionLink?: EvaluatedValue;
|
|
221
248
|
}
|
|
222
249
|
|
|
250
|
+
/**
|
|
251
|
+
* The `type` data prop's entry map. Structural only: each entry's fields are
|
|
252
|
+
* rendered through cellText/cellCode, which stringify any value.
|
|
253
|
+
*/
|
|
254
|
+
const isTypeEntryMap = (
|
|
255
|
+
value: EvaluatedValue
|
|
256
|
+
): value is Record<string, TypeEntry> =>
|
|
257
|
+
typeof value === "object" && value !== null;
|
|
258
|
+
|
|
223
259
|
const typeTable: ComponentMarkdown = ({ children, props }) => {
|
|
224
260
|
const { type } = props;
|
|
225
|
-
if (type
|
|
261
|
+
if (!isTypeEntryMap(type)) {
|
|
226
262
|
// The data prop is missing or wasn't statically evaluable.
|
|
227
263
|
return null;
|
|
228
264
|
}
|
|
229
|
-
const entries = Object.entries(type
|
|
265
|
+
const entries = Object.entries(type);
|
|
230
266
|
const rows = entries.map(([name, info]) => {
|
|
231
267
|
const prop = cellCode(`${name}${info.required ? "" : "?"}`);
|
|
232
268
|
const typeCell = info.typeDescriptionLink
|
|
@@ -236,7 +272,7 @@ const typeTable: ComponentMarkdown = ({ children, props }) => {
|
|
|
236
272
|
info.default === undefined ? "-" : cellCode(info.default);
|
|
237
273
|
const description = cellText(
|
|
238
274
|
[info.description, info.typeDescription]
|
|
239
|
-
.filter((part) =>
|
|
275
|
+
.filter((part) => isString(part) && part !== "")
|
|
240
276
|
.join(" ")
|
|
241
277
|
);
|
|
242
278
|
return [prop, typeCell, defaultCell, description];
|
|
@@ -254,9 +290,9 @@ const typeTable: ComponentMarkdown = ({ children, props }) => {
|
|
|
254
290
|
};
|
|
255
291
|
|
|
256
292
|
const callout: ComponentMarkdown = ({ children, props }) => {
|
|
257
|
-
const type =
|
|
293
|
+
const type = isString(props.type) ? props.type : "info";
|
|
258
294
|
const label =
|
|
259
|
-
|
|
295
|
+
isString(props.title) && props.title !== ""
|
|
260
296
|
? props.title
|
|
261
297
|
: type.charAt(0).toUpperCase() + type.slice(1);
|
|
262
298
|
if (!children) {
|
|
@@ -291,7 +327,7 @@ const steps: ComponentMarkdown = ({ childComponents, children }) => {
|
|
|
291
327
|
return items
|
|
292
328
|
.map((step, index) => {
|
|
293
329
|
const title =
|
|
294
|
-
|
|
330
|
+
isString(step.props.title) && step.props.title !== ""
|
|
295
331
|
? `**${step.props.title}**`
|
|
296
332
|
: "";
|
|
297
333
|
const content = [title, step.children].filter(Boolean).join("\n\n");
|
|
@@ -308,7 +344,7 @@ const tabs: ComponentMarkdown = ({ childComponents, children }) => {
|
|
|
308
344
|
return items
|
|
309
345
|
.map((tab, index) => {
|
|
310
346
|
const title =
|
|
311
|
-
|
|
347
|
+
isString(tab.props.title) && tab.props.title !== ""
|
|
312
348
|
? tab.props.title
|
|
313
349
|
: `Tab ${index + 1}`;
|
|
314
350
|
return tab.children ? `**${title}**\n\n${tab.children}` : `**${title}**`;
|
|
@@ -318,9 +354,9 @@ const tabs: ComponentMarkdown = ({ childComponents, children }) => {
|
|
|
318
354
|
|
|
319
355
|
const youtube: ComponentMarkdown = ({ props }) => {
|
|
320
356
|
let input = "";
|
|
321
|
-
if (
|
|
357
|
+
if (isString(props.id)) {
|
|
322
358
|
input = props.id;
|
|
323
|
-
} else if (
|
|
359
|
+
} else if (isString(props.url)) {
|
|
324
360
|
input = props.url;
|
|
325
361
|
}
|
|
326
362
|
const videoId = parseYouTubeId(input);
|
|
@@ -328,11 +364,11 @@ const youtube: ComponentMarkdown = ({ props }) => {
|
|
|
328
364
|
return null;
|
|
329
365
|
}
|
|
330
366
|
const start =
|
|
331
|
-
|
|
367
|
+
isNumber(props.start) && props.start > 0
|
|
332
368
|
? `&t=${Math.floor(props.start)}s`
|
|
333
369
|
: "";
|
|
334
370
|
const title =
|
|
335
|
-
|
|
371
|
+
isString(props.title) && props.title !== ""
|
|
336
372
|
? props.title
|
|
337
373
|
: "Watch on YouTube";
|
|
338
374
|
return `[${title}](https://www.youtube.com/watch?v=${videoId}${start})`;
|
|
@@ -355,28 +391,27 @@ const fencedBlock = (lang: string, code: string): string => {
|
|
|
355
391
|
* the JSX verbatim, mirroring the "no example found" note the component renders
|
|
356
392
|
* on the page.
|
|
357
393
|
*/
|
|
358
|
-
export const exampleComponentSerializers = (
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
},
|
|
366
|
-
});
|
|
394
|
+
export const exampleComponentSerializers = (examples: ExampleLookup) =>
|
|
395
|
+
({
|
|
396
|
+
Component: ({ props }) => {
|
|
397
|
+
const path = isString(props.path) ? props.path : undefined;
|
|
398
|
+
const example = path === undefined ? undefined : examples[path];
|
|
399
|
+
return example ? fencedBlock(example.lang, example.source) : null;
|
|
400
|
+
},
|
|
401
|
+
}) satisfies Record<string, ComponentMarkdown>;
|
|
367
402
|
|
|
368
403
|
/**
|
|
369
404
|
* The built-in serializer registry, keyed by JSX name. `Step` and `Tab` are
|
|
370
405
|
* intentionally absent: they only carry meaning inside their containers,
|
|
371
406
|
* which extract them via `childComponents`; a stray one stays verbatim.
|
|
372
407
|
*/
|
|
373
|
-
const SERIALIZERS
|
|
408
|
+
const SERIALIZERS = {
|
|
374
409
|
Callout: callout,
|
|
375
410
|
Steps: steps,
|
|
376
411
|
Tabs: tabs,
|
|
377
412
|
TypeTable: typeTable,
|
|
378
413
|
YouTube: youtube,
|
|
379
|
-
}
|
|
414
|
+
} satisfies Record<string, ComponentMarkdown>;
|
|
380
415
|
|
|
381
416
|
const escapeRegExp = (value: string): string =>
|
|
382
417
|
value.replaceAll(/[$()*+.?[\\\]^{|}]/gu, String.raw`\$&`);
|
|
@@ -394,7 +429,7 @@ const BUILT_IN_HINT = componentHint(SERIALIZERS);
|
|
|
394
429
|
|
|
395
430
|
/** One downlevel pass's inputs: the source, registry, and page metadata. */
|
|
396
431
|
interface Walk {
|
|
397
|
-
frontmatter: Record<string,
|
|
432
|
+
frontmatter: Record<string, EvaluatedValue> | undefined;
|
|
398
433
|
registry: Record<string, ComponentMarkdown>;
|
|
399
434
|
source: string;
|
|
400
435
|
}
|
|
@@ -494,7 +529,7 @@ const collectSplices = (
|
|
|
494
529
|
export const downlevelComponents = (
|
|
495
530
|
source: string,
|
|
496
531
|
components?: Record<string, ComponentMarkdown>,
|
|
497
|
-
frontmatter?: Record<string,
|
|
532
|
+
frontmatter?: Record<string, EvaluatedValue>
|
|
498
533
|
): string => {
|
|
499
534
|
const custom = components && Object.keys(components).length > 0;
|
|
500
535
|
const registry = custom ? { ...SERIALIZERS, ...components } : SERIALIZERS;
|
|
@@ -504,6 +539,8 @@ export const downlevelComponents = (
|
|
|
504
539
|
}
|
|
505
540
|
let tree: MdastNode;
|
|
506
541
|
try {
|
|
542
|
+
// SAFETY: MdastNode is a structural subset of Satteri's mdast output —
|
|
543
|
+
// every node carries `type`, and the walk reads only optional fields.
|
|
507
544
|
tree = mdxToMdast(source) as MdastNode;
|
|
508
545
|
} catch {
|
|
509
546
|
return source;
|
package/src/ai/llms.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { normalizeBasePath, withBasePath } from "../core/base-path.ts";
|
|
|
2
2
|
import { rewriteRelativeImages } from "../core/content-assets.ts";
|
|
3
3
|
import matter from "../core/frontmatter.ts";
|
|
4
4
|
import type { BlumeProject } from "../core/project-graph.ts";
|
|
5
|
+
import { absoluteUrl } from "../core/site-url.ts";
|
|
5
6
|
import { readEntryText } from "../core/sources/read.ts";
|
|
6
7
|
import type { NavNode, Navigation, PageRecord } from "../core/types.ts";
|
|
7
8
|
import { buildRssFeeds } from "../deploy/rss.ts";
|
|
@@ -17,20 +18,26 @@ import { applyAgentVisibility } from "./visibility.ts";
|
|
|
17
18
|
// spaces or non-ASCII must still yield a valid Markdown link.
|
|
18
19
|
const pageUrl = (route: string, site?: string, base = ""): string => {
|
|
19
20
|
const path = withBasePath(base, route);
|
|
20
|
-
return encodeURI(site ?
|
|
21
|
+
return encodeURI(site ? absoluteUrl(site, path) : path);
|
|
21
22
|
};
|
|
22
23
|
|
|
23
24
|
// Drafts, hidden, and ordinary `noindex` pages are excluded. Generated API
|
|
24
25
|
// references keep crawler visibility (`noindex`) separate from LLM visibility
|
|
25
26
|
// (`ai.exclude`), and are excluded wholesale when `ai.llmsTxt.openapi` is off.
|
|
26
|
-
|
|
27
|
+
// `versions: "current"` additionally drops archived-snapshot pages — the flat
|
|
28
|
+
// llms-full.txt dump serves agents the live docs, not every frozen copy.
|
|
29
|
+
const eligiblePages = (
|
|
30
|
+
project: BlumeProject,
|
|
31
|
+
options: { versions?: "all" | "current" } = {}
|
|
32
|
+
): PageRecord[] =>
|
|
27
33
|
project.graph.pages.filter(
|
|
28
34
|
(page) =>
|
|
29
35
|
!(
|
|
30
36
|
page.meta.ai.exclude ||
|
|
31
37
|
page.meta.draft ||
|
|
32
38
|
page.meta.sidebar.hidden ||
|
|
33
|
-
(page.meta.seo.noindex && page.source.name !== "openapi")
|
|
39
|
+
(page.meta.seo.noindex && page.source.name !== "openapi") ||
|
|
40
|
+
(options.versions === "current" && page.version !== "")
|
|
34
41
|
) &&
|
|
35
42
|
(project.config.ai.llmsTxt.openapi || page.source.name !== "openapi")
|
|
36
43
|
);
|
|
@@ -38,22 +45,40 @@ const eligiblePages = (project: BlumeProject): PageRecord[] =>
|
|
|
38
45
|
/**
|
|
39
46
|
* The navigation trees the index mirrors: the site tree, or one per locale
|
|
40
47
|
* under i18n (each labeled with the locale except the default, so sections
|
|
41
|
-
* don't repeat ambiguously).
|
|
48
|
+
* don't repeat ambiguously). On a versioned site each archived snapshot's
|
|
49
|
+
* trees follow the current ones, labeled with the version (and locale) so an
|
|
50
|
+
* agent reading the index knows which docs are frozen.
|
|
42
51
|
*/
|
|
43
52
|
const indexedNavigations = (
|
|
44
53
|
project: BlumeProject
|
|
45
54
|
): { label?: string; nav: Navigation }[] => {
|
|
46
|
-
const { i18n } = project.config;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
55
|
+
const { i18n, versions } = project.config;
|
|
56
|
+
const current: { label?: string; nav: Navigation }[] = i18n
|
|
57
|
+
? i18n.locales.flatMap(({ code, label }) => {
|
|
58
|
+
const nav = project.graph.navigationByLocale[code];
|
|
59
|
+
if (!nav) {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
return [
|
|
63
|
+
{ label: code === i18n.defaultLocale ? undefined : label, nav },
|
|
64
|
+
];
|
|
65
|
+
})
|
|
66
|
+
: [{ nav: project.graph.navigation }];
|
|
67
|
+
|
|
68
|
+
const archived = (versions?.archived ?? []).flatMap((version) => {
|
|
69
|
+
const byLocale = project.graph.navigationByVersion[version.id] ?? {};
|
|
70
|
+
const versionLabel = `${version.label ?? version.id} (archived)`;
|
|
71
|
+
return Object.entries(byLocale).flatMap(([code, nav]) => {
|
|
72
|
+
const locale = i18n?.locales.find((entry) => entry.code === code);
|
|
73
|
+
const label =
|
|
74
|
+
locale && code !== i18n?.defaultLocale
|
|
75
|
+
? `${locale.label} — ${versionLabel}`
|
|
76
|
+
: versionLabel;
|
|
77
|
+
return [{ label, nav }];
|
|
54
78
|
});
|
|
55
|
-
}
|
|
56
|
-
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return [...current, ...archived];
|
|
57
82
|
};
|
|
58
83
|
|
|
59
84
|
/**
|
|
@@ -168,11 +193,11 @@ export const buildLlmsIndex = (project: BlumeProject): string => {
|
|
|
168
193
|
return `${[header, ...blocks].join("\n\n")}\n`;
|
|
169
194
|
};
|
|
170
195
|
|
|
171
|
-
/** Build `llms-full.txt`: the full Markdown body of every page. */
|
|
196
|
+
/** Build `llms-full.txt`: the full Markdown body of every current-docs page. */
|
|
172
197
|
const buildFull = async (project: BlumeProject): Promise<string> => {
|
|
173
198
|
const { config } = project;
|
|
174
|
-
const pages = eligiblePages(project).toSorted(
|
|
175
|
-
a.route.localeCompare(b.route)
|
|
199
|
+
const pages = eligiblePages(project, { versions: "current" }).toSorted(
|
|
200
|
+
(a, b) => a.route.localeCompare(b.route)
|
|
176
201
|
);
|
|
177
202
|
// Downlevel `<Component>` to its example's source; a same-name user
|
|
178
203
|
// `markdownComponents` entry is spread last and still wins.
|