blume 1.1.4 → 1.2.1
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 +27 -0
- package/README.md +1 -1
- package/dist/cli/index.js +1319 -66
- package/dist/cli/index.js.map +34 -23
- package/dist/types/core/config-input.d.ts +18 -0
- package/dist/types/core/config.d.ts +4 -0
- package/dist/types/core/data.d.ts +1 -0
- package/dist/types/core/schema.d.ts +132 -17
- package/dist/types/core/types.d.ts +5 -3
- package/dist/types/openapi/references.d.ts +6 -0
- package/docs/advanced/api-reference.mdx +27 -0
- package/docs/advanced/changelog.mdx +10 -0
- package/docs/configuration/ai.mdx +38 -2
- package/docs/configuration/customization.mdx +27 -0
- package/docs/configuration/index.mdx +5 -0
- package/docs/configuration/search.mdx +15 -0
- package/docs/content/navigation.mdx +12 -0
- package/docs/reference/cli.mdx +17 -13
- package/docs/reference/eval.mdx +106 -0
- package/docs/reference/meta.ts +1 -1
- package/package.json +2 -2
- package/src/ai/agent-readability.ts +19 -1
- package/src/ai/ask-context.ts +7 -1
- package/src/ai/ask-data.ts +1 -0
- package/src/ai/llms.ts +9 -4
- package/src/ai/mcp/data.ts +7 -0
- package/src/ai/mcp/server.ts +24 -8
- package/src/ai/mcp/stdio.ts +38 -0
- package/src/astro/generate.ts +25 -2
- package/src/astro/templates.ts +129 -26
- package/src/cli/commands/eval.ts +291 -0
- package/src/cli/commands/init.ts +9 -4
- package/src/cli/commands/mcp-stdio.ts +36 -0
- package/src/cli/index.ts +4 -0
- package/src/cli/required-secrets.ts +1 -1
- package/src/components/content/AccordionItem.astro +2 -2
- package/src/components/content/TreeFolder.astro +1 -2
- package/src/components/islands/AskAI.astro +9 -2
- package/src/components/islands/ask-ai.tsx +4 -2
- package/src/components/islands/hooks.ts +10 -4
- package/src/components/layout/NavTree.astro +38 -20
- package/src/components/layout/ReferenceLayout.astro +4 -0
- package/src/components/layout/RootLayout.astro +2 -2
- package/src/components/layout/search/orama.ts +5 -2
- package/src/components/openapi/SchemaProperty.astro +3 -3
- package/src/core/config-input.ts +18 -0
- package/src/core/config.ts +4 -0
- package/src/core/data.ts +1 -0
- package/src/core/graph.ts +1 -0
- package/src/core/navigation.ts +9 -2
- package/src/core/schema.ts +51 -4
- package/src/core/server-features.ts +1 -1
- package/src/core/types.ts +5 -3
- package/src/eval/agents.ts +340 -0
- package/src/eval/findings.ts +103 -0
- package/src/eval/prompts.ts +78 -0
- package/src/eval/report.ts +214 -0
- package/src/eval/run.ts +290 -0
- package/src/eval/schema.ts +124 -0
- package/src/openapi/references.ts +23 -2
- package/src/openapi/render-mdx.ts +27 -4
- package/src/openapi/scalar.ts +1 -0
- package/src/openapi/source.ts +11 -4
- package/src/registry/eject.ts +23 -1
- package/src/search/build.ts +4 -3
- package/src/search/orama-index.ts +55 -5
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ApiOperationRef, ApiSpecData } from "./model.ts";
|
|
2
|
+
import type { ReferenceSource } from "./references.ts";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Lower a parsed spec into MDX for the staged content source. Each operation and
|
|
@@ -126,7 +127,11 @@ const withDescription = (description: string, component: string): string =>
|
|
|
126
127
|
|
|
127
128
|
export const operationMdx = (
|
|
128
129
|
spec: ApiSpecData,
|
|
129
|
-
operation: ApiOperationRef
|
|
130
|
+
operation: ApiOperationRef,
|
|
131
|
+
reference?: Pick<
|
|
132
|
+
ReferenceSource,
|
|
133
|
+
"includeInLlms" | "includeInSearch" | "noindex"
|
|
134
|
+
>
|
|
130
135
|
): RenderedPage => {
|
|
131
136
|
const method = operation.method.toUpperCase();
|
|
132
137
|
const title = operation.summary || `${method} ${operation.path}`;
|
|
@@ -142,9 +147,16 @@ export const operationMdx = (
|
|
|
142
147
|
`<Operation source="${spec.slug}" id="${operation.key}" />`
|
|
143
148
|
),
|
|
144
149
|
data: {
|
|
150
|
+
...(reference?.includeInLlms === false ? { ai: { exclude: true } } : {}),
|
|
145
151
|
...(operation.deprecated ? { deprecated: true } : {}),
|
|
146
|
-
search: {
|
|
147
|
-
|
|
152
|
+
search: {
|
|
153
|
+
...(reference?.includeInSearch === false ? { exclude: true } : {}),
|
|
154
|
+
tags: [operation.tag, method],
|
|
155
|
+
},
|
|
156
|
+
seo: {
|
|
157
|
+
description: operationDescription(spec, operation),
|
|
158
|
+
...(reference?.noindex ? { noindex: true } : {}),
|
|
159
|
+
},
|
|
148
160
|
sidebar: { badge: method, label: operation.summary || operation.path },
|
|
149
161
|
title,
|
|
150
162
|
// Signals the two-column API layout (request panel instead of the TOC).
|
|
@@ -153,7 +165,13 @@ export const operationMdx = (
|
|
|
153
165
|
};
|
|
154
166
|
};
|
|
155
167
|
|
|
156
|
-
export const overviewMdx = (
|
|
168
|
+
export const overviewMdx = (
|
|
169
|
+
spec: ApiSpecData,
|
|
170
|
+
reference?: Pick<
|
|
171
|
+
ReferenceSource,
|
|
172
|
+
"includeInLlms" | "includeInSearch" | "noindex"
|
|
173
|
+
>
|
|
174
|
+
): RenderedPage => {
|
|
157
175
|
// Tag sections: declared tags in spec order, then any tag an operation
|
|
158
176
|
// references that isn't declared under `tags`. The section headings are
|
|
159
177
|
// emitted as real markdown `##` (not markup inside a component) so the
|
|
@@ -205,10 +223,15 @@ export const overviewMdx = (spec: ApiSpecData): RenderedPage => {
|
|
|
205
223
|
...tagSections,
|
|
206
224
|
].join("\n\n"),
|
|
207
225
|
data: {
|
|
226
|
+
...(reference?.includeInLlms === false ? { ai: { exclude: true } } : {}),
|
|
227
|
+
...(reference?.includeInSearch === false
|
|
228
|
+
? { search: { exclude: true } }
|
|
229
|
+
: {}),
|
|
208
230
|
seo: {
|
|
209
231
|
description:
|
|
210
232
|
clip(plainProse(spec.description), META_DESCRIPTION_MAX) ||
|
|
211
233
|
`${apiName(spec)} API reference.`,
|
|
234
|
+
...(reference?.noindex ? { noindex: true } : {}),
|
|
212
235
|
},
|
|
213
236
|
sidebar: { label: "Overview" },
|
|
214
237
|
title: apiName(spec),
|
package/src/openapi/scalar.ts
CHANGED
package/src/openapi/source.ts
CHANGED
|
@@ -53,17 +53,24 @@ const toEntry = (rendered: RenderedPage, ref: string): SourceEntry => {
|
|
|
53
53
|
/** All staged entries for one spec: operations first, overview last. */
|
|
54
54
|
const specEntries = (
|
|
55
55
|
spec: ApiSpecData,
|
|
56
|
-
operations: ApiOperationRef[]
|
|
56
|
+
operations: ApiOperationRef[],
|
|
57
|
+
reference: ReferenceSource
|
|
57
58
|
): SourceEntry[] => {
|
|
58
59
|
const entries = operations.map((operation) =>
|
|
59
|
-
toEntry(
|
|
60
|
+
toEntry(
|
|
61
|
+
operationMdx(spec, operation, reference),
|
|
62
|
+
`${routeToRef(operation.route)}.mdx`
|
|
63
|
+
)
|
|
60
64
|
);
|
|
61
65
|
// Overview last so an operation sets the section's routePath before the index
|
|
62
66
|
// page is inserted (the group's routePath is derived from its first child).
|
|
63
67
|
// A root-mounted reference refs `index.mdx`, not `/index.mdx`.
|
|
64
68
|
const base = routeToRef(spec.route);
|
|
65
69
|
entries.push(
|
|
66
|
-
toEntry(
|
|
70
|
+
toEntry(
|
|
71
|
+
overviewMdx(spec, reference),
|
|
72
|
+
base ? `${base}/index.mdx` : "index.mdx"
|
|
73
|
+
)
|
|
67
74
|
);
|
|
68
75
|
return entries;
|
|
69
76
|
};
|
|
@@ -149,7 +156,7 @@ export const openApiSource = (
|
|
|
149
156
|
]
|
|
150
157
|
: []),
|
|
151
158
|
],
|
|
152
|
-
entries: specEntries(spec, operations),
|
|
159
|
+
entries: specEntries(spec, operations, reference),
|
|
153
160
|
slug: reference.slug,
|
|
154
161
|
spec,
|
|
155
162
|
};
|
package/src/registry/eject.ts
CHANGED
|
@@ -112,7 +112,15 @@ const askFiles = async (
|
|
|
112
112
|
genDir: string
|
|
113
113
|
): Promise<{ content: string; path: string }[]> => {
|
|
114
114
|
const { ask } = project.config.ai;
|
|
115
|
-
if (!ask?.enabled) {
|
|
115
|
+
if (!(ask?.enabled && !ask.endpoint)) {
|
|
116
|
+
const endpointPath = join(srcDir, "pages", "api", "ask.ts");
|
|
117
|
+
if (existsSync(endpointPath)) {
|
|
118
|
+
const content = await readFile(endpointPath, "utf-8");
|
|
119
|
+
if (content.startsWith("// Generated by Blume. Do not edit.")) {
|
|
120
|
+
await rm(endpointPath, { force: true });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
await rm(join(genDir, "ask-data.json"), { force: true });
|
|
116
124
|
return [];
|
|
117
125
|
}
|
|
118
126
|
const grounded = ask.provider !== "inkeep";
|
|
@@ -271,6 +279,15 @@ const examplesPreviewFiles = (
|
|
|
271
279
|
]
|
|
272
280
|
: [];
|
|
273
281
|
|
|
282
|
+
const ejectIntegrationBridge = (
|
|
283
|
+
config: BlumeProject["config"],
|
|
284
|
+
root: string,
|
|
285
|
+
configFile: string | null
|
|
286
|
+
): Parameters<typeof astroConfigTemplate>[0]["integrationBridge"] =>
|
|
287
|
+
config.integrations.length > 0 && configFile
|
|
288
|
+
? { configFile: toPosix(relative(root, configFile)) }
|
|
289
|
+
: undefined;
|
|
290
|
+
|
|
274
291
|
/**
|
|
275
292
|
* Promote the generated runtime into the project as an owned Astro app. After
|
|
276
293
|
* eject the project has a normal `astro.config.mjs` and `src/`, the `blume` CLI
|
|
@@ -367,6 +384,11 @@ export const eject = async (
|
|
|
367
384
|
dataPath: "./src/generated/data.json",
|
|
368
385
|
examplesPath: "./src/generated/examples.ts",
|
|
369
386
|
examplesThemePath: "./src/generated/examples.css",
|
|
387
|
+
integrationBridge: ejectIntegrationBridge(
|
|
388
|
+
config,
|
|
389
|
+
root,
|
|
390
|
+
context.configFile
|
|
391
|
+
),
|
|
370
392
|
needsReact,
|
|
371
393
|
needsSvelte,
|
|
372
394
|
needsVue,
|
package/src/search/build.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { join } from "pathe";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Build a local Pagefind search index over the built site. Pagefind
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Build a local Pagefind search index over the built site. Pagefind indexes
|
|
5
|
+
* every rendered page except those whose `<html>` carries
|
|
6
|
+
* `data-pagefind-ignore`, which Blume stamps on non-indexable pages
|
|
7
|
+
* (search-excluded, or hidden without the opt-in), so those stay out.
|
|
7
8
|
*
|
|
8
9
|
* Returns the number of pages indexed.
|
|
9
10
|
*/
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { create, insertMultiple, search } from "@orama/orama";
|
|
2
|
-
import type { AnyOrama } from "@orama/orama";
|
|
2
|
+
import type { AnyOrama, Tokenizer } from "@orama/orama";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* The minimal document shape both the client-side search dialog and the
|
|
@@ -31,15 +31,65 @@ const SCHEMA = {
|
|
|
31
31
|
/** Title and description outrank body text, matching the search dialog. */
|
|
32
32
|
const BOOST = { description: 2, title: 3 };
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Scripts written without spaces between words. Orama's default tokenizer
|
|
36
|
+
* splits on a Latin-centric delimiter class, so text in these languages
|
|
37
|
+
* collapses to zero tokens and every query silently returns no hits. Keyed by
|
|
38
|
+
* the primary language subtag of `i18n.defaultLocale`.
|
|
39
|
+
*/
|
|
40
|
+
const SEGMENTED_LANGUAGES = new Set(["ja", "ko", "th", "zh"]);
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A word-segmenting tokenizer for languages the default splitter can't handle,
|
|
44
|
+
* built on `Intl.Segmenter` (the same engine `@orama/tokenizers` wraps).
|
|
45
|
+
* Input is lowercased before segmenting — unlike the upstream tokenizers —
|
|
46
|
+
* so Latin terms ("GDPR", English pages on a mixed-locale site) still match
|
|
47
|
+
* case-insensitively. Returns `undefined` for languages the default tokenizer
|
|
48
|
+
* already serves, and on runtimes without `Intl.Segmenter`, where the caller
|
|
49
|
+
* falls back to Orama's default.
|
|
50
|
+
*/
|
|
51
|
+
const segmentingTokenizer = (locale?: string): Tokenizer | undefined => {
|
|
52
|
+
const language = locale?.toLowerCase().split(/[-_]/u)[0] ?? "";
|
|
53
|
+
if (!SEGMENTED_LANGUAGES.has(language)) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (typeof Intl.Segmenter !== "function") {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const segmenter = new Intl.Segmenter(language, { granularity: "word" });
|
|
60
|
+
return {
|
|
61
|
+
language,
|
|
62
|
+
normalizationCache: new Map(),
|
|
63
|
+
tokenize: (raw: string): string[] => {
|
|
64
|
+
const tokens = new Set<string>();
|
|
65
|
+
for (const segment of segmenter.segment(raw.toLowerCase())) {
|
|
66
|
+
if (segment.isWordLike) {
|
|
67
|
+
tokens.add(segment.segment);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return [...tokens];
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
|
|
34
75
|
/**
|
|
35
76
|
* Build an in-memory Orama full-text index from search documents. Shared by the
|
|
36
|
-
* Orama client loader (browser)
|
|
37
|
-
* identical wherever docs are queried.
|
|
77
|
+
* Orama client loader (browser), the MCP server, and Ask AI grounding (Node),
|
|
78
|
+
* so ranking is identical wherever docs are queried. `locale` — the site's
|
|
79
|
+
* `i18n.defaultLocale` — swaps in a word-segmenting tokenizer for languages
|
|
80
|
+
* written without spaces (Japanese, Chinese, Korean, Thai); the tokenizer
|
|
81
|
+
* belongs to the database, so on a mixed-locale site it applies to every
|
|
82
|
+
* document, which is safe because Latin words survive segmentation intact.
|
|
38
83
|
*/
|
|
39
84
|
export const buildOramaIndex = async (
|
|
40
|
-
documents: OramaDoc[]
|
|
85
|
+
documents: OramaDoc[],
|
|
86
|
+
locale?: string
|
|
41
87
|
): Promise<AnyOrama> => {
|
|
42
|
-
const
|
|
88
|
+
const tokenizer = segmentingTokenizer(locale);
|
|
89
|
+
const db = create({
|
|
90
|
+
schema: SCHEMA,
|
|
91
|
+
...(tokenizer ? { components: { tokenizer } } : {}),
|
|
92
|
+
});
|
|
43
93
|
await insertMultiple(db, documents);
|
|
44
94
|
return db;
|
|
45
95
|
};
|