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
package/dist/cli/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { createRequire } from "node:module";
|
|
|
3
3
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
4
4
|
|
|
5
5
|
// src/cli/index.ts
|
|
6
|
-
import { defineCommand as
|
|
6
|
+
import { defineCommand as defineCommand14, runMain } from "citty";
|
|
7
7
|
|
|
8
8
|
// src/core/version.ts
|
|
9
9
|
import { readFileSync } from "node:fs";
|
|
@@ -3090,7 +3090,12 @@ var routeSlug = (route) => slugify(trimChar(route, "/")) || "reference";
|
|
|
3090
3090
|
var sourcesOf = (block) => {
|
|
3091
3091
|
const sources = [...block.sources];
|
|
3092
3092
|
if (block.spec) {
|
|
3093
|
-
sources.unshift({
|
|
3093
|
+
sources.unshift({
|
|
3094
|
+
includeInLlms: true,
|
|
3095
|
+
includeInSearch: true,
|
|
3096
|
+
noindex: false,
|
|
3097
|
+
spec: block.spec
|
|
3098
|
+
});
|
|
3094
3099
|
}
|
|
3095
3100
|
return sources;
|
|
3096
3101
|
};
|
|
@@ -3114,8 +3119,11 @@ var referencesFor = (kind, block, defaultLabel, renderer, display, basePath) =>
|
|
|
3114
3119
|
return {
|
|
3115
3120
|
basePath,
|
|
3116
3121
|
display,
|
|
3122
|
+
includeInLlms: source.includeInLlms,
|
|
3123
|
+
includeInSearch: source.includeInSearch,
|
|
3117
3124
|
kind,
|
|
3118
3125
|
label,
|
|
3126
|
+
noindex: source.noindex,
|
|
3119
3127
|
renderer,
|
|
3120
3128
|
route,
|
|
3121
3129
|
scalar: block.scalar,
|
|
@@ -3417,7 +3425,10 @@ var WRANGLER_CONFIG_FILES = [
|
|
|
3417
3425
|
"wrangler.toml"
|
|
3418
3426
|
];
|
|
3419
3427
|
var resolveCloudflareAdapterArgs = (context) => {
|
|
3420
|
-
const args = [
|
|
3428
|
+
const args = [
|
|
3429
|
+
'prerenderEnvironment: "node"',
|
|
3430
|
+
'imageService: "compile"'
|
|
3431
|
+
];
|
|
3421
3432
|
const wranglerPath = WRANGLER_CONFIG_FILES.map((file) => join8(context.root, file)).find((file) => existsSync4(file));
|
|
3422
3433
|
if (wranglerPath) {
|
|
3423
3434
|
let configPath = relative5(context.outDir, wranglerPath);
|
|
@@ -3428,6 +3439,18 @@ var resolveCloudflareAdapterArgs = (context) => {
|
|
|
3428
3439
|
}
|
|
3429
3440
|
return `{ ${args.join(", ")} }`;
|
|
3430
3441
|
};
|
|
3442
|
+
var resolveSessionOption = (deployment) => deployment.output === "server" && deployment.adapter === "cloudflare" ? `
|
|
3443
|
+
session: { driver: sessionDrivers.memory() },` : "";
|
|
3444
|
+
var astroConfigImportLine = (options) => {
|
|
3445
|
+
const names = ["defineConfig"];
|
|
3446
|
+
if (options.hasFonts) {
|
|
3447
|
+
names.push("fontProviders");
|
|
3448
|
+
}
|
|
3449
|
+
if (options.hasSession) {
|
|
3450
|
+
names.push("sessionDrivers");
|
|
3451
|
+
}
|
|
3452
|
+
return `import { ${names.join(", ")} } from "astro/config";`;
|
|
3453
|
+
};
|
|
3431
3454
|
var runtimeDependencies = (options) => {
|
|
3432
3455
|
const { config, needsReact, needsSvelte, needsVue } = options;
|
|
3433
3456
|
const deps = ["@astrojs/mdx"];
|
|
@@ -3444,7 +3467,7 @@ var runtimeDependencies = (options) => {
|
|
|
3444
3467
|
deps.push("@scalar/astro");
|
|
3445
3468
|
}
|
|
3446
3469
|
deps.push(...searchProviderMeta(config.search.provider).runtimeDeps);
|
|
3447
|
-
if (config.ai.ask?.enabled) {
|
|
3470
|
+
if (config.ai.ask?.enabled && !config.ai.ask.endpoint) {
|
|
3448
3471
|
const askDep = askBackendRuntimeDep(config.ai.ask);
|
|
3449
3472
|
if (askDep) {
|
|
3450
3473
|
deps.push(askDep);
|
|
@@ -3487,6 +3510,29 @@ var devWatchOption = (outDir, contentWatchesRuntimeDir) => contentWatchesRuntime
|
|
|
3487
3510
|
watch: {
|
|
3488
3511
|
ignored: ${JSON.stringify([join8(outDir, ".astro", "**")])},
|
|
3489
3512
|
},` : "";
|
|
3513
|
+
var renderIntegrationBridge = (bridge) => {
|
|
3514
|
+
if (!bridge) {
|
|
3515
|
+
return {
|
|
3516
|
+
configSourceMarker: "",
|
|
3517
|
+
userConfigImports: "",
|
|
3518
|
+
userConfigSetup: "",
|
|
3519
|
+
userIntegrationSpread: ""
|
|
3520
|
+
};
|
|
3521
|
+
}
|
|
3522
|
+
return {
|
|
3523
|
+
configSourceMarker: bridge.sourceHash ? `// Blume config source SHA-256: ${bridge.sourceHash}
|
|
3524
|
+
` : "",
|
|
3525
|
+
userConfigImports: `import { dirname, resolve } from "node:path";
|
|
3526
|
+
import { fileURLToPath } from "node:url";
|
|
3527
|
+
import { createModuleLoader } from "blume/core/load-module.ts";
|
|
3528
|
+
`,
|
|
3529
|
+
userConfigSetup: `const loadBlumeConfig = createModuleLoader();
|
|
3530
|
+
const blumeConfig = await loadBlumeConfig(resolve(dirname(fileURLToPath(import.meta.url)), ${JSON.stringify(bridge.configFile)}));
|
|
3531
|
+
|
|
3532
|
+
`,
|
|
3533
|
+
userIntegrationSpread: ", ...(blumeConfig?.integrations ?? [])"
|
|
3534
|
+
};
|
|
3535
|
+
};
|
|
3490
3536
|
var astroConfigTemplate = (options) => {
|
|
3491
3537
|
const { context, config, needsReact, pages, dataPath, themePath } = options;
|
|
3492
3538
|
const {
|
|
@@ -3517,6 +3563,7 @@ var astroConfigTemplate = (options) => {
|
|
|
3517
3563
|
const adapterExpr = deployment.adapter === "vercel" ? `withAdapterRoot(adapter(${adapterArgs}), ${JSON.stringify(adapterRoot(context))})` : `adapter(${adapterArgs})`;
|
|
3518
3564
|
const adapterOption = server && deployment.adapter ? `
|
|
3519
3565
|
adapter: ${adapterExpr},` : "";
|
|
3566
|
+
const sessionOption = resolveSessionOption(deployment);
|
|
3520
3567
|
const siteOption = deployment.site ? `
|
|
3521
3568
|
site: ${JSON.stringify(deployment.site)},` : "";
|
|
3522
3569
|
const baseOption = deployment.base ? `
|
|
@@ -3538,7 +3585,10 @@ var astroConfigTemplate = (options) => {
|
|
|
3538
3585
|
const fontEntries = buildFontEntries(config.theme.fonts);
|
|
3539
3586
|
const fontsOption = fontEntries.length ? `
|
|
3540
3587
|
fonts: [${fontEntries.map((font) => `{ provider: fontProviders.google(), name: ${JSON.stringify(font.name)}, cssVariable: ${JSON.stringify(font.cssVariable)}, weights: ${JSON.stringify(font.weights)}, fallbacks: ${JSON.stringify(font.fallbacks)} }`).join(", ")}],` : "";
|
|
3541
|
-
const defineConfigImport =
|
|
3588
|
+
const defineConfigImport = astroConfigImportLine({
|
|
3589
|
+
hasFonts: fontEntries.length > 0,
|
|
3590
|
+
hasSession: sessionOption.length > 0
|
|
3591
|
+
});
|
|
3542
3592
|
const reactImport = needsReact ? `import react from "@astrojs/react";
|
|
3543
3593
|
` : "";
|
|
3544
3594
|
const vueImport = needsVue ? `import vue from "@astrojs/vue";
|
|
@@ -3574,19 +3624,25 @@ var astroConfigTemplate = (options) => {
|
|
|
3574
3624
|
}
|
|
3575
3625
|
integrations.push(`blumeIntegration(${JSON.stringify({ base: deployment.base, contentRoutes, pages })})`);
|
|
3576
3626
|
const watchOption = devWatchOption(context.outDir, options.contentWatchesRuntimeDir);
|
|
3627
|
+
const {
|
|
3628
|
+
configSourceMarker,
|
|
3629
|
+
userConfigImports,
|
|
3630
|
+
userConfigSetup,
|
|
3631
|
+
userIntegrationSpread
|
|
3632
|
+
} = renderIntegrationBridge(options.integrationBridge);
|
|
3577
3633
|
return `// Generated by Blume. Do not edit; this file is recreated on each run.
|
|
3578
|
-
${defineConfigImport}
|
|
3634
|
+
${configSourceMarker}${userConfigImports}${defineConfigImport}
|
|
3579
3635
|
import mdx from "@astrojs/mdx";
|
|
3580
3636
|
import tailwindcss from "@tailwindcss/vite";
|
|
3581
3637
|
import { blumeMarkdownProcessor, blumeMdxProcessor, blumeShikiTransformers, blumeTwoslashTransformer } from "blume/markdown";
|
|
3582
3638
|
${reactImport}${vueImport}${svelteImport}${blumeImport}${adapterImport}
|
|
3583
|
-
export default defineConfig({
|
|
3639
|
+
${userConfigSetup}export default defineConfig({
|
|
3584
3640
|
root: ${JSON.stringify(context.outDir)},
|
|
3585
3641
|
srcDir: ${JSON.stringify(`${context.outDir}/src`)},
|
|
3586
3642
|
outDir: ${JSON.stringify(astroOutDir(context))},
|
|
3587
3643
|
publicDir: ${JSON.stringify(`${context.root}/public`)},
|
|
3588
|
-
output: ${JSON.stringify(deployment.output)},${adapterOption}${siteOption}${baseOption}${redirectsOption}${i18nOption}${fontsOption}
|
|
3589
|
-
integrations: [${integrations.join(", ")}],
|
|
3644
|
+
output: ${JSON.stringify(deployment.output)},${adapterOption}${sessionOption}${siteOption}${baseOption}${redirectsOption}${i18nOption}${fontsOption}
|
|
3645
|
+
integrations: [${integrations.join(", ")}${userIntegrationSpread}],
|
|
3590
3646
|
markdown: {
|
|
3591
3647
|
processor: blumeMarkdownProcessor(${JSON.stringify({
|
|
3592
3648
|
basePath: config.basePath,
|
|
@@ -3606,18 +3662,25 @@ export default defineConfig({
|
|
|
3606
3662
|
devToolbar: { enabled: false },
|
|
3607
3663
|
vite: {
|
|
3608
3664
|
plugins: [tailwindcss(), prerenderDepsPlugin(), serverAppResolvePlugin()],
|
|
3609
|
-
//
|
|
3610
|
-
// CJS (\`dayjs/dayjs.min.js\`)
|
|
3611
|
-
//
|
|
3612
|
-
//
|
|
3613
|
-
//
|
|
3614
|
-
//
|
|
3615
|
-
//
|
|
3616
|
-
//
|
|
3617
|
-
//
|
|
3618
|
-
//
|
|
3619
|
-
//
|
|
3620
|
-
|
|
3665
|
+
// The lazy client-side imports both land on CJS/UMD files: mermaid (for
|
|
3666
|
+
// diagrams) statically imports dayjs as CJS (\`dayjs/dayjs.min.js\`), and
|
|
3667
|
+
// epub-gen-memory's browser bundle is a browserified UMD. In dev, an
|
|
3668
|
+
// un-pre-bundled dependency is served as raw ESM, where such a file
|
|
3669
|
+
// exposes no \`default\` export — mermaid throws on load and diagrams
|
|
3670
|
+
// render blank, and the EPUB export throws \`epub is not a function\`
|
|
3671
|
+
// (the UMD finds no \`exports\`/\`define\` and strands its callable on
|
|
3672
|
+
// \`window.epubGen\` instead). Forcing them through the dep optimizer
|
|
3673
|
+
// restores the CJS interop. In a standalone install these dynamic imports
|
|
3674
|
+
// live inside \`node_modules/blume\`, which Vite's optimizer scan doesn't
|
|
3675
|
+
// crawl, so neither is discovered on its own — hence the explicit
|
|
3676
|
+
// includes. They resolve through the \`blume\` package (they aren't direct
|
|
3677
|
+
// deps of the generated project), so the nested \`blume > x\` form is
|
|
3678
|
+
// required, and epub-gen-memory must name the \`/bundle\` subpath that is
|
|
3679
|
+
// actually imported: optimizing the package root leaves that entry out.
|
|
3680
|
+
// Production (Rollup) already handles the interop, so this only affects dev.
|
|
3681
|
+
optimizeDeps: {
|
|
3682
|
+
include: ["blume > mermaid", "blume > epub-gen-memory/bundle"],
|
|
3683
|
+
},
|
|
3621
3684
|
// Blume's render-time deps are forced external on both build environments so
|
|
3622
3685
|
// native bindings resolve at runtime and isolated linkers don't bundle
|
|
3623
3686
|
// symlinked store copies (which would surface their children as unresolvable
|
|
@@ -3811,7 +3874,11 @@ import data from "blume:data";
|
|
|
3811
3874
|
const { strings } = Astro.props;
|
|
3812
3875
|
---
|
|
3813
3876
|
|
|
3814
|
-
<AskAI
|
|
3877
|
+
<AskAI
|
|
3878
|
+
endpoint={data.config.ask?.endpoint ?? undefined}
|
|
3879
|
+
strings={strings ?? data.ui.ask}
|
|
3880
|
+
suggestions={data.config.ask?.suggestions ?? []}
|
|
3881
|
+
/>
|
|
3815
3882
|
` : `---
|
|
3816
3883
|
// Generated by Blume. Do not edit.
|
|
3817
3884
|
// Ask AI is off (\`ai.ask.enabled\`), so the header's Ask trigger renders nothing.
|
|
@@ -3836,10 +3903,10 @@ var searchClientImport = (module) => `import { createSearch as create } from "bl
|
|
|
3836
3903
|
`;
|
|
3837
3904
|
var SEARCH_BASE_IMPORT = `import { joinBase } from "blume/components/islands/base-path.ts";
|
|
3838
3905
|
`;
|
|
3839
|
-
var staticSearchClient = (module) => `${SEARCH_CLIENT_HEADER}${searchClientImport(module)}${SEARCH_BASE_IMPORT}
|
|
3906
|
+
var staticSearchClient = (module, locale) => `${SEARCH_CLIENT_HEADER}${searchClientImport(module)}${SEARCH_BASE_IMPORT}
|
|
3840
3907
|
const indexUrl = joinBase(import.meta.env.BASE_URL, "blume-search.json");
|
|
3841
3908
|
|
|
3842
|
-
export const createSearch = () => create({ indexUrl });
|
|
3909
|
+
export const createSearch = () => create({ indexUrl${locale ? `, locale: ${JSON.stringify(locale)}` : ""} });
|
|
3843
3910
|
`;
|
|
3844
3911
|
var hostedSearchClient = (module, options) => `${SEARCH_CLIENT_HEADER}${searchClientImport(module)}
|
|
3845
3912
|
export const createSearch = () => create(${JSON.stringify(options)});
|
|
@@ -3869,7 +3936,7 @@ var hostedSearchOptions = (search) => {
|
|
|
3869
3936
|
var searchClientTemplate = (config) => {
|
|
3870
3937
|
const { search } = config;
|
|
3871
3938
|
if (search.provider === "orama" || search.provider === "flexsearch") {
|
|
3872
|
-
return staticSearchClient(search.provider);
|
|
3939
|
+
return staticSearchClient(search.provider, search.provider === "orama" ? config.i18n?.defaultLocale : undefined);
|
|
3873
3940
|
}
|
|
3874
3941
|
const hosted = hostedSearchOptions(search);
|
|
3875
3942
|
if (hosted) {
|
|
@@ -4093,6 +4160,7 @@ const htmlLang = i18n ? i18n.defaultLocale : "en";
|
|
|
4093
4160
|
favicon={data.config.favicon}
|
|
4094
4161
|
appleIcon={data.config.appleIcon}
|
|
4095
4162
|
navigation={data.navigation}
|
|
4163
|
+
noindex={${options.noindex === true}}
|
|
4096
4164
|
pageTitle={${JSON.stringify(options.title)}}
|
|
4097
4165
|
route={${JSON.stringify(options.route)}}
|
|
4098
4166
|
searchEnabled={data.config.search.enabled}
|
|
@@ -7514,6 +7582,9 @@ var searchMetaSchema = z2.strictObject({
|
|
|
7514
7582
|
exclude: z2.boolean().default(false),
|
|
7515
7583
|
tags: z2.array(z2.string()).optional()
|
|
7516
7584
|
});
|
|
7585
|
+
var aiMetaSchema = z2.strictObject({
|
|
7586
|
+
exclude: z2.boolean().default(false)
|
|
7587
|
+
});
|
|
7517
7588
|
var changelogMetaSchema = z2.strictObject({
|
|
7518
7589
|
category: z2.string().optional(),
|
|
7519
7590
|
date: dateSchema.optional(),
|
|
@@ -7529,6 +7600,7 @@ var authorSchema = z2.union([
|
|
|
7529
7600
|
}).catchall(z2.unknown())
|
|
7530
7601
|
]);
|
|
7531
7602
|
var pageMetaBaseSchema = z2.strictObject({
|
|
7603
|
+
ai: aiMetaSchema.default({}),
|
|
7532
7604
|
authors: z2.union([authorSchema, z2.array(authorSchema)]).optional(),
|
|
7533
7605
|
changelog: changelogMetaSchema.optional(),
|
|
7534
7606
|
date: dateSchema.optional(),
|
|
@@ -7662,6 +7734,7 @@ var contentConfigSchema = z2.strictObject({
|
|
|
7662
7734
|
sources: z2.array(contentSourceSchema).optional()
|
|
7663
7735
|
});
|
|
7664
7736
|
var navTabSchema = z2.strictObject({
|
|
7737
|
+
href: z2.string().min(1).optional(),
|
|
7665
7738
|
icon: iconName.optional(),
|
|
7666
7739
|
items: z2.array(z2.strictObject({
|
|
7667
7740
|
description: z2.string().optional(),
|
|
@@ -7806,11 +7879,25 @@ var mcpConfigSchema = z2.strictObject({
|
|
|
7806
7879
|
name: z2.string().optional(),
|
|
7807
7880
|
route: z2.string().default("/mcp").transform(normalizeRoute)
|
|
7808
7881
|
});
|
|
7882
|
+
var askEndpointSchema = z2.string().trim().min(1).refine((value) => {
|
|
7883
|
+
if (value.startsWith("/") && !value.startsWith("//")) {
|
|
7884
|
+
return true;
|
|
7885
|
+
}
|
|
7886
|
+
try {
|
|
7887
|
+
const url = new URL(value);
|
|
7888
|
+
return url.protocol === "http:" || url.protocol === "https:";
|
|
7889
|
+
} catch {
|
|
7890
|
+
return false;
|
|
7891
|
+
}
|
|
7892
|
+
}, {
|
|
7893
|
+
message: "ai.ask.endpoint must be an HTTP(S) URL or a root-relative path."
|
|
7894
|
+
});
|
|
7809
7895
|
var aiConfigSchema = z2.strictObject({
|
|
7810
7896
|
ask: z2.strictObject({
|
|
7811
7897
|
apiKeyEnv: z2.string().optional(),
|
|
7812
7898
|
baseUrl: z2.string().url().optional(),
|
|
7813
7899
|
enabled: z2.boolean().default(false),
|
|
7900
|
+
endpoint: askEndpointSchema.optional(),
|
|
7814
7901
|
model: z2.string().default("openai/gpt-5.5"),
|
|
7815
7902
|
provider: z2.enum(askAiProviders).default("gateway"),
|
|
7816
7903
|
suggestions: z2.array(z2.strictObject({
|
|
@@ -7818,7 +7905,7 @@ var aiConfigSchema = z2.strictObject({
|
|
|
7818
7905
|
label: z2.string().min(1)
|
|
7819
7906
|
})).default([])
|
|
7820
7907
|
}).superRefine((value, ctx) => {
|
|
7821
|
-
if (value.provider === "openai-compatible" && !value.baseUrl) {
|
|
7908
|
+
if (value.provider === "openai-compatible" && !(value.baseUrl || value.endpoint)) {
|
|
7822
7909
|
ctx.addIssue({
|
|
7823
7910
|
code: z2.ZodIssueCode.custom,
|
|
7824
7911
|
message: 'ai.ask.baseUrl is required when provider is "openai-compatible".',
|
|
@@ -8046,7 +8133,10 @@ var reactConfigSchema = z2.strictObject({
|
|
|
8046
8133
|
compiler: z2.boolean().default(true)
|
|
8047
8134
|
});
|
|
8048
8135
|
var openapiSourceSchema = z2.strictObject({
|
|
8136
|
+
includeInLlms: z2.boolean().default(true),
|
|
8137
|
+
includeInSearch: z2.boolean().default(true),
|
|
8049
8138
|
label: z2.string().optional(),
|
|
8139
|
+
noindex: z2.boolean().default(false),
|
|
8050
8140
|
route: z2.string().optional(),
|
|
8051
8141
|
spec: z2.string()
|
|
8052
8142
|
});
|
|
@@ -8119,6 +8209,7 @@ var blumeConfigSchema = z2.strictObject({
|
|
|
8119
8209
|
frontmatter: frontmatterConfigSchema.default({}),
|
|
8120
8210
|
github: githubConfigSchema.optional(),
|
|
8121
8211
|
i18n: i18nConfigSchema.optional(),
|
|
8212
|
+
integrations: z2.array(z2.custom()).default([]),
|
|
8122
8213
|
lastModified: lastModifiedConfigSchema.default(false),
|
|
8123
8214
|
logo: logoConfigSchema.optional(),
|
|
8124
8215
|
markdown: markdownConfigSchema.default({}),
|
|
@@ -8794,7 +8885,7 @@ var resolveTabHref = (sidebar, path) => {
|
|
|
8794
8885
|
return walk(sidebar) ? path : first ?? path;
|
|
8795
8886
|
};
|
|
8796
8887
|
var withTabHrefs = (tabs, sidebar) => tabs.map((tab) => {
|
|
8797
|
-
const href = resolveTabHref(sidebar, tab.path);
|
|
8888
|
+
const href = tab.href ?? resolveTabHref(sidebar, tab.path);
|
|
8798
8889
|
return href === tab.path ? tab : { ...tab, href };
|
|
8799
8890
|
});
|
|
8800
8891
|
var buildNavigation = (pages, options) => {
|
|
@@ -8816,6 +8907,7 @@ var buildNavigation = (pages, options) => {
|
|
|
8816
8907
|
})) : options.selectors ?? [];
|
|
8817
8908
|
const tabs = basePath ? (options.tabs ?? []).map((tab) => ({
|
|
8818
8909
|
...tab,
|
|
8910
|
+
...tab.href ? { href: withBasePath(basePath, tab.href) } : {},
|
|
8819
8911
|
items: tab.items?.map(rebasePath),
|
|
8820
8912
|
path: withBasePath(basePath, tab.path)
|
|
8821
8913
|
})) : options.tabs ?? [];
|
|
@@ -8894,6 +8986,7 @@ var buildLocaleNavigation = (code, pages, fallback, fallbackByKey, options, i18n
|
|
|
8894
8986
|
const localizePath = (path) => path.startsWith("/") ? localizeRoute(path, code, i18n) : path;
|
|
8895
8987
|
const tabs = options.navigation.tabs?.map((tab) => ({
|
|
8896
8988
|
...tab,
|
|
8989
|
+
...tab.href ? { href: localizePath(tab.href) } : {},
|
|
8897
8990
|
items: tab.items?.map((item) => ({
|
|
8898
8991
|
...item,
|
|
8899
8992
|
path: localizePath(item.path)
|
|
@@ -9918,23 +10011,30 @@ var operationDescription = (spec, operation) => {
|
|
|
9918
10011
|
var withDescription = (description, component) => description.trim() ? `${mdxSafe(description.trim())}
|
|
9919
10012
|
|
|
9920
10013
|
${component}` : component;
|
|
9921
|
-
var operationMdx = (spec, operation) => {
|
|
10014
|
+
var operationMdx = (spec, operation, reference) => {
|
|
9922
10015
|
const method = operation.method.toUpperCase();
|
|
9923
10016
|
const title = operation.summary || `${method} ${operation.path}`;
|
|
9924
10017
|
const description = operation.description.trim() === operation.summary.trim() ? "" : operation.description;
|
|
9925
10018
|
return {
|
|
9926
10019
|
body: withDescription(description, `<Operation source="${spec.slug}" id="${operation.key}" />`),
|
|
9927
10020
|
data: {
|
|
10021
|
+
...reference?.includeInLlms === false ? { ai: { exclude: true } } : {},
|
|
9928
10022
|
...operation.deprecated ? { deprecated: true } : {},
|
|
9929
|
-
search: {
|
|
9930
|
-
|
|
10023
|
+
search: {
|
|
10024
|
+
...reference?.includeInSearch === false ? { exclude: true } : {},
|
|
10025
|
+
tags: [operation.tag, method]
|
|
10026
|
+
},
|
|
10027
|
+
seo: {
|
|
10028
|
+
description: operationDescription(spec, operation),
|
|
10029
|
+
...reference?.noindex ? { noindex: true } : {}
|
|
10030
|
+
},
|
|
9931
10031
|
sidebar: { badge: method, label: operation.summary || operation.path },
|
|
9932
10032
|
title,
|
|
9933
10033
|
type: "openapi-operation"
|
|
9934
10034
|
}
|
|
9935
10035
|
};
|
|
9936
10036
|
};
|
|
9937
|
-
var overviewMdx = (spec) => {
|
|
10037
|
+
var overviewMdx = (spec, reference) => {
|
|
9938
10038
|
const operations = Object.values(spec.operations);
|
|
9939
10039
|
const sections = [];
|
|
9940
10040
|
const known = new Set;
|
|
@@ -9976,8 +10076,11 @@ var overviewMdx = (spec) => {
|
|
|
9976
10076
|
|
|
9977
10077
|
`),
|
|
9978
10078
|
data: {
|
|
10079
|
+
...reference?.includeInLlms === false ? { ai: { exclude: true } } : {},
|
|
10080
|
+
...reference?.includeInSearch === false ? { search: { exclude: true } } : {},
|
|
9979
10081
|
seo: {
|
|
9980
|
-
description: clip(plainProse(spec.description), META_DESCRIPTION_MAX) || `${apiName(spec)} API reference
|
|
10082
|
+
description: clip(plainProse(spec.description), META_DESCRIPTION_MAX) || `${apiName(spec)} API reference.`,
|
|
10083
|
+
...reference?.noindex ? { noindex: true } : {}
|
|
9981
10084
|
},
|
|
9982
10085
|
sidebar: { label: "Overview" },
|
|
9983
10086
|
title: apiName(spec)
|
|
@@ -9999,10 +10102,10 @@ var toEntry = (rendered, ref) => {
|
|
|
9999
10102
|
ref
|
|
10000
10103
|
};
|
|
10001
10104
|
};
|
|
10002
|
-
var specEntries = (spec, operations) => {
|
|
10003
|
-
const entries = operations.map((operation) => toEntry(operationMdx(spec, operation), `${routeToRef(operation.route)}.mdx`));
|
|
10105
|
+
var specEntries = (spec, operations, reference) => {
|
|
10106
|
+
const entries = operations.map((operation) => toEntry(operationMdx(spec, operation, reference), `${routeToRef(operation.route)}.mdx`));
|
|
10004
10107
|
const base = routeToRef(spec.route);
|
|
10005
|
-
entries.push(toEntry(overviewMdx(spec), base ? `${base}/index.mdx` : "index.mdx"));
|
|
10108
|
+
entries.push(toEntry(overviewMdx(spec, reference), base ? `${base}/index.mdx` : "index.mdx"));
|
|
10006
10109
|
return entries;
|
|
10007
10110
|
};
|
|
10008
10111
|
var openApiSource = (references, ctx) => {
|
|
@@ -10056,7 +10159,7 @@ var openApiSource = (references, ctx) => {
|
|
|
10056
10159
|
}
|
|
10057
10160
|
] : []
|
|
10058
10161
|
],
|
|
10059
|
-
entries: specEntries(spec, operations),
|
|
10162
|
+
entries: specEntries(spec, operations, reference),
|
|
10060
10163
|
slug: reference.slug,
|
|
10061
10164
|
spec
|
|
10062
10165
|
};
|
|
@@ -11579,6 +11682,12 @@ var usagePolicy = (signals) => {
|
|
|
11579
11682
|
}
|
|
11580
11683
|
return Object.fromEntries(USAGE_TOKENS.map(([key, token]) => [token, signals[key]]));
|
|
11581
11684
|
};
|
|
11685
|
+
var askApiUrl = (endpoint, site, abs) => {
|
|
11686
|
+
if (!endpoint) {
|
|
11687
|
+
return abs("/api/ask");
|
|
11688
|
+
}
|
|
11689
|
+
return site && endpoint.startsWith("/") ? `${site.replace(/\/+$/u, "")}${endpoint}` : endpoint;
|
|
11690
|
+
};
|
|
11582
11691
|
var buildAgentReadability = (project) => {
|
|
11583
11692
|
const { config } = project;
|
|
11584
11693
|
if (!config.seo.agentReadability) {
|
|
@@ -11607,7 +11716,7 @@ var buildAgentReadability = (project) => {
|
|
|
11607
11716
|
};
|
|
11608
11717
|
}
|
|
11609
11718
|
if (config.ai.ask?.enabled) {
|
|
11610
|
-
artifacts.askApi =
|
|
11719
|
+
artifacts.askApi = askApiUrl(config.ai.ask.endpoint, site, abs);
|
|
11611
11720
|
}
|
|
11612
11721
|
if (site && config.seo.sitemap) {
|
|
11613
11722
|
artifacts.sitemap = abs("/sitemap.xml");
|
|
@@ -11950,7 +12059,7 @@ var pageUrl = (route, site, base = "") => {
|
|
|
11950
12059
|
const path = withBasePath(base, route);
|
|
11951
12060
|
return encodeURI(site ? `${site.replace(/\/$/u, "")}${path}` : path);
|
|
11952
12061
|
};
|
|
11953
|
-
var eligiblePages = (project) => project.graph.pages.filter((page) => !(page.meta.draft || page.meta.sidebar.hidden || page.meta.seo.noindex) && (project.config.ai.llmsTxt.openapi || page.source.name !== "openapi"));
|
|
12062
|
+
var eligiblePages = (project) => project.graph.pages.filter((page) => !(page.meta.ai.exclude || page.meta.draft || page.meta.sidebar.hidden || page.meta.seo.noindex && page.source.name !== "openapi") && (project.config.ai.llmsTxt.openapi || page.source.name !== "openapi"));
|
|
11954
12063
|
var indexedNavigations = (project) => {
|
|
11955
12064
|
const { i18n } = project.config;
|
|
11956
12065
|
if (i18n) {
|
|
@@ -12099,7 +12208,7 @@ var ensureGitignore = async (root, entries) => {
|
|
|
12099
12208
|
// src/core/server-features.ts
|
|
12100
12209
|
var serverFeatures = (config) => {
|
|
12101
12210
|
const features = [];
|
|
12102
|
-
if (config.ai.ask?.enabled) {
|
|
12211
|
+
if (config.ai.ask?.enabled && !config.ai.ask.endpoint) {
|
|
12103
12212
|
features.push("Ask AI");
|
|
12104
12213
|
}
|
|
12105
12214
|
if (config.ai.mcp.enabled) {
|
|
@@ -12649,6 +12758,7 @@ var refuseIfDevRunning = (root, action, options = {}) => {
|
|
|
12649
12758
|
};
|
|
12650
12759
|
|
|
12651
12760
|
// src/astro/generate.ts
|
|
12761
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
12652
12762
|
import { existsSync as existsSync14, readFileSync as readFileSync9, realpathSync } from "node:fs";
|
|
12653
12763
|
import {
|
|
12654
12764
|
lstat,
|
|
@@ -12674,6 +12784,7 @@ var buildAskData = async (project) => {
|
|
|
12674
12784
|
includeWhenDisabled: true
|
|
12675
12785
|
});
|
|
12676
12786
|
return {
|
|
12787
|
+
defaultLocale: project.config.i18n?.defaultLocale,
|
|
12677
12788
|
documents: documents.map((doc) => ({
|
|
12678
12789
|
content: doc.content,
|
|
12679
12790
|
description: doc.description,
|
|
@@ -12741,6 +12852,7 @@ var buildMcpData = async (project) => {
|
|
|
12741
12852
|
}
|
|
12742
12853
|
return {
|
|
12743
12854
|
base: normalizeBasePath(config.deployment.base),
|
|
12855
|
+
defaultLocale: config.i18n?.defaultLocale,
|
|
12744
12856
|
documents: documents.map((doc) => ({
|
|
12745
12857
|
content: doc.content,
|
|
12746
12858
|
description: doc.description,
|
|
@@ -13475,6 +13587,7 @@ var buildReferenceFiles = async (options) => {
|
|
|
13475
13587
|
...ref.scalar
|
|
13476
13588
|
},
|
|
13477
13589
|
dataImport: `${"../".repeat(depth + 1)}generated/data.json`,
|
|
13590
|
+
noindex: ref.noindex,
|
|
13478
13591
|
route: ref.route,
|
|
13479
13592
|
title: ref.label
|
|
13480
13593
|
}),
|
|
@@ -14765,6 +14878,16 @@ var detectUsesMath = async (root, staged = []) => {
|
|
|
14765
14878
|
const contents = await Promise.all(files.map((file) => readOptional(join26(root, file))));
|
|
14766
14879
|
return [...contents, ...staged].some(containsMath);
|
|
14767
14880
|
};
|
|
14881
|
+
var hashConfigSource = (source) => createHash2("sha256").update(source).digest("hex");
|
|
14882
|
+
var loadIntegrationBridge = async (config, context) => {
|
|
14883
|
+
if (config.integrations.length === 0 || !context.configFile) {
|
|
14884
|
+
return;
|
|
14885
|
+
}
|
|
14886
|
+
return {
|
|
14887
|
+
configFile: relative13(context.outDir, context.configFile),
|
|
14888
|
+
sourceHash: hashConfigSource(await readOptional(context.configFile))
|
|
14889
|
+
};
|
|
14890
|
+
};
|
|
14768
14891
|
var writeIfChanged = async (path, content) => {
|
|
14769
14892
|
let existing = null;
|
|
14770
14893
|
try {
|
|
@@ -14983,7 +15106,10 @@ var buildRuntimeData = (project) => {
|
|
|
14983
15106
|
config: {
|
|
14984
15107
|
analytics: config.analytics ?? null,
|
|
14985
15108
|
appleIcon: resolveAppleIcon(project),
|
|
14986
|
-
ask: config.ai.ask?.enabled ? {
|
|
15109
|
+
ask: config.ai.ask?.enabled ? {
|
|
15110
|
+
endpoint: config.ai.ask.endpoint ?? null,
|
|
15111
|
+
suggestions: config.ai.ask.suggestions
|
|
15112
|
+
} : null,
|
|
14987
15113
|
banner: resolveBanner(config),
|
|
14988
15114
|
basePath: config.basePath,
|
|
14989
15115
|
codeThemes: config.markdown.codeBlocks.theme,
|
|
@@ -15115,7 +15241,7 @@ var writeMcpFiles = async (project, plan, write) => {
|
|
|
15115
15241
|
};
|
|
15116
15242
|
var writeAskFiles = async (project, srcDir, write) => {
|
|
15117
15243
|
const { ask } = project.config.ai;
|
|
15118
|
-
if (!ask?.enabled) {
|
|
15244
|
+
if (!(ask?.enabled && !ask.endpoint)) {
|
|
15119
15245
|
return;
|
|
15120
15246
|
}
|
|
15121
15247
|
const grounded = ask.provider !== "inkeep";
|
|
@@ -15168,6 +15294,7 @@ var generateRuntime = async (project) => {
|
|
|
15168
15294
|
usesMath,
|
|
15169
15295
|
userTheme,
|
|
15170
15296
|
userExamplesCss,
|
|
15297
|
+
integrationBridge,
|
|
15171
15298
|
islandDiscovery,
|
|
15172
15299
|
exampleDiscovery,
|
|
15173
15300
|
componentSlots
|
|
@@ -15177,6 +15304,7 @@ var generateRuntime = async (project) => {
|
|
|
15177
15304
|
detectUsesMath(context.root, staged.values()),
|
|
15178
15305
|
readOptional(context.themeFile),
|
|
15179
15306
|
readOptional(examplesCssFile(context.root, config)),
|
|
15307
|
+
loadIntegrationBridge(config, context),
|
|
15180
15308
|
discoverIslands(context.root),
|
|
15181
15309
|
discoverExamples(context.root, config.examples.source),
|
|
15182
15310
|
buildComponentSlots(context.componentsFile)
|
|
@@ -15214,6 +15342,7 @@ var generateRuntime = async (project) => {
|
|
|
15214
15342
|
dataPath,
|
|
15215
15343
|
examplesPath,
|
|
15216
15344
|
examplesThemePath,
|
|
15345
|
+
integrationBridge,
|
|
15217
15346
|
needsReact,
|
|
15218
15347
|
needsSvelte,
|
|
15219
15348
|
needsVue,
|
|
@@ -15439,7 +15568,7 @@ var checkRequiredSecrets = (config) => {
|
|
|
15439
15568
|
suggestion: `Set ${env} in .env.local for local dev, or in your host's environment for production.`
|
|
15440
15569
|
});
|
|
15441
15570
|
};
|
|
15442
|
-
if (config.ai.ask?.enabled) {
|
|
15571
|
+
if (config.ai.ask?.enabled && !config.ai.ask.endpoint) {
|
|
15443
15572
|
const backend = resolveAskBackend(config.ai.ask);
|
|
15444
15573
|
if (backend.kind === "gateway") {
|
|
15445
15574
|
requireSecret("Ask AI (AI Gateway)", "AI_GATEWAY_API_KEY", "on Vercel the gateway can also authenticate via OIDC");
|
|
@@ -16196,7 +16325,15 @@ var ejectOpenApiData = (project) => {
|
|
|
16196
16325
|
};
|
|
16197
16326
|
var askFiles = async (project, srcDir, genDir) => {
|
|
16198
16327
|
const { ask } = project.config.ai;
|
|
16199
|
-
if (!ask?.enabled) {
|
|
16328
|
+
if (!(ask?.enabled && !ask.endpoint)) {
|
|
16329
|
+
const endpointPath = join31(srcDir, "pages", "api", "ask.ts");
|
|
16330
|
+
if (existsSync18(endpointPath)) {
|
|
16331
|
+
const content = await readFile15(endpointPath, "utf-8");
|
|
16332
|
+
if (content.startsWith("// Generated by Blume. Do not edit.")) {
|
|
16333
|
+
await rm3(endpointPath, { force: true });
|
|
16334
|
+
}
|
|
16335
|
+
}
|
|
16336
|
+
await rm3(join31(genDir, "ask-data.json"), { force: true });
|
|
16200
16337
|
return [];
|
|
16201
16338
|
}
|
|
16202
16339
|
const grounded = ask.provider !== "inkeep";
|
|
@@ -16279,6 +16416,7 @@ var examplesPreviewFiles = (srcDir, basePath, hasExamples) => hasExamples ? [
|
|
|
16279
16416
|
path: join31(srcDir, "pages", ...basePath.split("/").filter(Boolean), "blume-examples", "[...path].astro")
|
|
16280
16417
|
}
|
|
16281
16418
|
] : [];
|
|
16419
|
+
var ejectIntegrationBridge = (config, root, configFile) => config.integrations.length > 0 && configFile ? { configFile: toPosix(relative14(root, configFile)) } : undefined;
|
|
16282
16420
|
var eject = async (root) => {
|
|
16283
16421
|
const project = await scanProject(root, { mode: "build" });
|
|
16284
16422
|
const { context, config } = project;
|
|
@@ -16340,6 +16478,7 @@ var eject = async (root) => {
|
|
|
16340
16478
|
dataPath: "./src/generated/data.json",
|
|
16341
16479
|
examplesPath: "./src/generated/examples.ts",
|
|
16342
16480
|
examplesThemePath: "./src/generated/examples.css",
|
|
16481
|
+
integrationBridge: ejectIntegrationBridge(config, root, context.configFile),
|
|
16343
16482
|
needsReact,
|
|
16344
16483
|
needsSvelte,
|
|
16345
16484
|
needsVue,
|
|
@@ -16890,9 +17029,878 @@ The blume package remains importable.`);
|
|
|
16890
17029
|
}
|
|
16891
17030
|
});
|
|
16892
17031
|
|
|
17032
|
+
// src/cli/commands/eval.ts
|
|
17033
|
+
import { existsSync as existsSync20 } from "node:fs";
|
|
17034
|
+
import { defineCommand as defineCommand8 } from "citty";
|
|
17035
|
+
import { join as join37 } from "pathe";
|
|
17036
|
+
|
|
17037
|
+
// src/eval/prompts.ts
|
|
17038
|
+
var readerPrompt = (question) => `You are evaluating whether a product's documentation can answer a user's question.
|
|
17039
|
+
|
|
17040
|
+
Answer the question below using ONLY the connected documentation tools (search_docs, get_page, list_pages, get_navigation). Rules:
|
|
17041
|
+
- Do not use prior knowledge about the product. Do not guess.
|
|
17042
|
+
- Do not read files, run commands, or access the network.
|
|
17043
|
+
- Search first, then read the most relevant pages with get_page.
|
|
17044
|
+
- If the documentation does not contain the answer, say exactly what information is missing instead of inventing one.
|
|
17045
|
+
|
|
17046
|
+
Question: ${question.question}
|
|
17047
|
+
|
|
17048
|
+
Reply with a concise answer containing the specific facts the documentation provides. Plain text only.`;
|
|
17049
|
+
var judgePrompt = (question, answer) => {
|
|
17050
|
+
const facts = question.expected.map((fact) => `- ${fact}`).join(`
|
|
17051
|
+
`);
|
|
17052
|
+
return `You are grading an answer against expected facts. Do not use any tools.
|
|
17053
|
+
|
|
17054
|
+
Question: ${question.question}
|
|
17055
|
+
|
|
17056
|
+
Expected facts — each must be present in substance (paraphrase is fine, contradiction is not):
|
|
17057
|
+
${facts}
|
|
17058
|
+
|
|
17059
|
+
Answer to grade:
|
|
17060
|
+
"""
|
|
17061
|
+
${answer}
|
|
17062
|
+
"""
|
|
17063
|
+
|
|
17064
|
+
An answer that states the documentation lacks the information FAILS.
|
|
17065
|
+
|
|
17066
|
+
Reply with ONLY this JSON object on a single line, no markdown fences:
|
|
17067
|
+
{"pass": true|false, "score": 0.0-1.0, "missing": ["expected facts absent or contradicted"], "notes": "one sentence"}`;
|
|
17068
|
+
};
|
|
17069
|
+
var evalFixPrompt = (reportPath) => `Fix the documentation gaps found by \`blume eval\` in this project.
|
|
17070
|
+
|
|
17071
|
+
The full report is at ${reportPath}. It is JSON: each entry in \`eval.results\` with status "fail" is one question the documentation could not answer. Each carries the \`question\`, the \`expected\` facts, the judge's \`missing\` facts, and the reader agent's \`answer\` (what the docs currently convey). The matching \`diagnostics\` entry names the source \`file\` of the page that should answer it.
|
|
17072
|
+
|
|
17073
|
+
Work through every failed question:
|
|
17074
|
+
1. Read the page named in the finding (or choose the best page when none is named).
|
|
17075
|
+
2. Edit the documentation so it states the missing facts explicitly. Add prose, not filler; keep the page's voice.
|
|
17076
|
+
3. Never delete questions from the evals file or weaken expected facts.
|
|
17077
|
+
|
|
17078
|
+
When you are done, run \`blume eval\` to verify, and repeat until every question passes.`;
|
|
17079
|
+
var initPrompt = (evalsPath) => `Draft a starter evals file for \`blume eval\` in this documentation project.
|
|
17080
|
+
|
|
17081
|
+
Read the documentation source pages in this project and write ${evalsPath} with about 10 high-value questions a real user would ask — installation, configuration, deployment, and the project's headline features. For each question, list the expected facts a correct answer must state, grounded in what the documentation actually promises (never invent facts the docs don't state).
|
|
17082
|
+
|
|
17083
|
+
The file format is YAML:
|
|
17084
|
+
|
|
17085
|
+
questions:
|
|
17086
|
+
- id: kebab-case-slug
|
|
17087
|
+
question: One user question?
|
|
17088
|
+
expected:
|
|
17089
|
+
- a fact the answer must contain
|
|
17090
|
+
- another required fact
|
|
17091
|
+
routes:
|
|
17092
|
+
- /route/of/the/page/that/answers/it
|
|
17093
|
+
|
|
17094
|
+
Rules:
|
|
17095
|
+
- Every \`expected\` fact must be verifiable in the docs today.
|
|
17096
|
+
- Prefer questions whose answers live on one page; set \`routes\` to that page.
|
|
17097
|
+
- Keep ids unique and questions short.
|
|
17098
|
+
|
|
17099
|
+
When you are done, print the file and suggest running \`blume eval\` to try it.`;
|
|
17100
|
+
|
|
17101
|
+
// src/eval/report.ts
|
|
17102
|
+
import { mkdtemp as mkdtemp2, writeFile as writeFile12 } from "node:fs/promises";
|
|
17103
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
17104
|
+
import { join as join34, relative as relative17 } from "pathe";
|
|
17105
|
+
var ESC4 = String.fromCodePoint(27);
|
|
17106
|
+
var COLORS3 = {
|
|
17107
|
+
bold: `${ESC4}[1m`,
|
|
17108
|
+
cyan: `${ESC4}[36m`,
|
|
17109
|
+
dim: `${ESC4}[2m`,
|
|
17110
|
+
green: `${ESC4}[32m`,
|
|
17111
|
+
red: `${ESC4}[31m`,
|
|
17112
|
+
reset: `${ESC4}[0m`,
|
|
17113
|
+
yellow: `${ESC4}[33m`
|
|
17114
|
+
};
|
|
17115
|
+
var GLYPH2 = {
|
|
17116
|
+
error: "!",
|
|
17117
|
+
fail: "✖",
|
|
17118
|
+
pass: "✔",
|
|
17119
|
+
skip: "⊘"
|
|
17120
|
+
};
|
|
17121
|
+
var STATUS_COLOR = {
|
|
17122
|
+
error: COLORS3.yellow,
|
|
17123
|
+
fail: COLORS3.red,
|
|
17124
|
+
pass: COLORS3.green,
|
|
17125
|
+
skip: COLORS3.dim
|
|
17126
|
+
};
|
|
17127
|
+
var ID_PAD = 28;
|
|
17128
|
+
var seconds = (ms) => `${(ms / 1000).toFixed(1)}s`;
|
|
17129
|
+
var money = (cost) => cost === undefined ? "" : `$${cost.toFixed(2)}`;
|
|
17130
|
+
var duration = (ms) => {
|
|
17131
|
+
if (ms < 60000) {
|
|
17132
|
+
return seconds(ms);
|
|
17133
|
+
}
|
|
17134
|
+
const minutes = Math.floor(ms / 60000);
|
|
17135
|
+
const rest = Math.round(ms % 60000 / 1000);
|
|
17136
|
+
return `${minutes}m ${rest}s`;
|
|
17137
|
+
};
|
|
17138
|
+
var questionLine = (result) => {
|
|
17139
|
+
const color = STATUS_COLOR[result.status];
|
|
17140
|
+
const glyph = `${color}${GLYPH2[result.status]}${COLORS3.reset}`;
|
|
17141
|
+
const id2 = result.id.padEnd(ID_PAD);
|
|
17142
|
+
if (result.status === "skip") {
|
|
17143
|
+
return ` ${glyph} ${id2} ${COLORS3.dim}skipped${COLORS3.reset}`;
|
|
17144
|
+
}
|
|
17145
|
+
const score = result.score === undefined ? "" : result.score.toFixed(2);
|
|
17146
|
+
const cells = [
|
|
17147
|
+
`${color}${result.status}${COLORS3.reset}`,
|
|
17148
|
+
score,
|
|
17149
|
+
`${COLORS3.dim}${seconds(result.durationMs)}${COLORS3.reset}`,
|
|
17150
|
+
`${COLORS3.dim}${money(result.costUsd)}${COLORS3.reset}`
|
|
17151
|
+
].filter((cell) => cell !== "").join(" ");
|
|
17152
|
+
return ` ${glyph} ${id2} ${cells}`;
|
|
17153
|
+
};
|
|
17154
|
+
var questionDetails = (result, verbose) => {
|
|
17155
|
+
const lines = [];
|
|
17156
|
+
if (result.status === "fail") {
|
|
17157
|
+
for (const fact of result.missing) {
|
|
17158
|
+
lines.push(` ${COLORS3.dim}missing: ${fact}${COLORS3.reset}`);
|
|
17159
|
+
}
|
|
17160
|
+
}
|
|
17161
|
+
if (result.status === "error" && result.detail) {
|
|
17162
|
+
lines.push(` ${COLORS3.dim}${result.detail}${COLORS3.reset}`);
|
|
17163
|
+
}
|
|
17164
|
+
if (verbose && result.answer && result.status !== "pass") {
|
|
17165
|
+
lines.push(...result.answer.split(`
|
|
17166
|
+
`).map((line) => ` ${COLORS3.dim}> ${line}${COLORS3.reset}`));
|
|
17167
|
+
}
|
|
17168
|
+
return lines;
|
|
17169
|
+
};
|
|
17170
|
+
var summaryLine2 = (result) => {
|
|
17171
|
+
const { counts } = result;
|
|
17172
|
+
const parts = [
|
|
17173
|
+
`${counts.pass} passed`,
|
|
17174
|
+
counts.fail > 0 ? `${counts.fail} failed` : "",
|
|
17175
|
+
counts.error > 0 ? `${counts.error} errored` : "",
|
|
17176
|
+
counts.skip > 0 ? `${counts.skip} skipped` : "",
|
|
17177
|
+
duration(result.durationMs),
|
|
17178
|
+
money(result.costUsd)
|
|
17179
|
+
].filter((part) => part !== "");
|
|
17180
|
+
return parts.join(" · ");
|
|
17181
|
+
};
|
|
17182
|
+
var headerLine = (total, agent) => `${COLORS3.bold}blume eval${COLORS3.reset} ${total} question(s) · ${AGENTS[agent].name}`;
|
|
17183
|
+
var startLine = (id2, index, total) => ` ${COLORS3.dim}▸ ${id2} (${index + 1}/${total})${COLORS3.reset}`;
|
|
17184
|
+
var fixLines = (result, root) => result.diagnostics.filter((diagnostic) => diagnostic.code !== "BLUME_EVAL_ROUTE_UNKNOWN").map((finding2) => {
|
|
17185
|
+
const site = finding2.file ? `${relative17(root, finding2.file)}${finding2.line ? `:${finding2.line}` : ""}` : "";
|
|
17186
|
+
return ` ${COLORS3.cyan}fix:${COLORS3.reset} ${site} ${COLORS3.dim}${finding2.message}${COLORS3.reset}`;
|
|
17187
|
+
});
|
|
17188
|
+
var warningLines = (result, root) => result.diagnostics.filter((diagnostic) => diagnostic.code === "BLUME_EVAL_ROUTE_UNKNOWN").map((finding2) => {
|
|
17189
|
+
const site = finding2.file ? ` ${relative17(root, finding2.file)}${finding2.line ? `:${finding2.line}` : ""}` : "";
|
|
17190
|
+
return ` ${COLORS3.yellow}⚠${COLORS3.reset}${site} ${COLORS3.dim}${finding2.message}${COLORS3.reset}`;
|
|
17191
|
+
});
|
|
17192
|
+
var evalReportJson = (result, root, threshold) => {
|
|
17193
|
+
const diagnostics = result.diagnostics.map((diagnostic) => diagnostic.file ? { ...diagnostic, file: relative17(root, diagnostic.file) } : diagnostic);
|
|
17194
|
+
return `${JSON.stringify({
|
|
17195
|
+
diagnostics,
|
|
17196
|
+
eval: {
|
|
17197
|
+
agent: result.agent,
|
|
17198
|
+
costUsd: result.costUsd,
|
|
17199
|
+
counts: result.counts,
|
|
17200
|
+
durationMs: result.durationMs,
|
|
17201
|
+
results: result.results,
|
|
17202
|
+
threshold
|
|
17203
|
+
},
|
|
17204
|
+
summary: countBySeverity(result.diagnostics)
|
|
17205
|
+
}, null, 2)}
|
|
17206
|
+
`;
|
|
17207
|
+
};
|
|
17208
|
+
var writeEvalReport = async (result, root, threshold) => {
|
|
17209
|
+
const dir = await mkdtemp2(join34(tmpdir2(), "blume-eval-"));
|
|
17210
|
+
const path = join34(dir, "report.json");
|
|
17211
|
+
await writeFile12(path, evalReportJson(result, root, threshold));
|
|
17212
|
+
return path;
|
|
17213
|
+
};
|
|
17214
|
+
|
|
17215
|
+
// src/eval/run.ts
|
|
17216
|
+
import { mkdir as mkdir9, mkdtemp as mkdtemp3, writeFile as writeFile14 } from "node:fs/promises";
|
|
17217
|
+
import { tmpdir as tmpdir3 } from "node:os";
|
|
17218
|
+
import { join as join36 } from "pathe";
|
|
17219
|
+
|
|
17220
|
+
// src/eval/agents.ts
|
|
17221
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
17222
|
+
import { readFile as readFile17, writeFile as writeFile13 } from "node:fs/promises";
|
|
17223
|
+
import { join as join35 } from "pathe";
|
|
17224
|
+
import { z as z3 } from "zod";
|
|
17225
|
+
var KILL_GRACE_MS = 5000;
|
|
17226
|
+
var MCP_TOOL_NAMES = [
|
|
17227
|
+
"search_docs",
|
|
17228
|
+
"get_page",
|
|
17229
|
+
"list_pages",
|
|
17230
|
+
"get_navigation"
|
|
17231
|
+
];
|
|
17232
|
+
var MCP_SERVER_NAME = "docs";
|
|
17233
|
+
var DISALLOWED_TOOLS = [
|
|
17234
|
+
"Bash",
|
|
17235
|
+
"Read",
|
|
17236
|
+
"Glob",
|
|
17237
|
+
"Grep",
|
|
17238
|
+
"Write",
|
|
17239
|
+
"Edit",
|
|
17240
|
+
"NotebookEdit",
|
|
17241
|
+
"WebFetch",
|
|
17242
|
+
"WebSearch",
|
|
17243
|
+
"Task"
|
|
17244
|
+
];
|
|
17245
|
+
var runAgentHeadless = (bin, args, options) => new Promise((resolve9, reject) => {
|
|
17246
|
+
const platform = options.platform ?? process.platform;
|
|
17247
|
+
const child = spawn2(bin, args, {
|
|
17248
|
+
cwd: options.cwd,
|
|
17249
|
+
shell: platform === "win32",
|
|
17250
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
17251
|
+
});
|
|
17252
|
+
let stdout = "";
|
|
17253
|
+
let stderr = "";
|
|
17254
|
+
let timedOut = false;
|
|
17255
|
+
child.stdout.on("data", (chunk) => {
|
|
17256
|
+
stdout += chunk.toString("utf-8");
|
|
17257
|
+
});
|
|
17258
|
+
child.stderr.on("data", (chunk) => {
|
|
17259
|
+
stderr += chunk.toString("utf-8");
|
|
17260
|
+
});
|
|
17261
|
+
const deadline = setTimeout(() => {
|
|
17262
|
+
timedOut = true;
|
|
17263
|
+
child.kill("SIGTERM");
|
|
17264
|
+
const hardKill = setTimeout(() => child.kill("SIGKILL"), KILL_GRACE_MS);
|
|
17265
|
+
hardKill.unref();
|
|
17266
|
+
}, options.timeoutMs);
|
|
17267
|
+
deadline.unref();
|
|
17268
|
+
child.once("error", (error) => {
|
|
17269
|
+
clearTimeout(deadline);
|
|
17270
|
+
reject(error);
|
|
17271
|
+
});
|
|
17272
|
+
child.once("close", (code) => {
|
|
17273
|
+
clearTimeout(deadline);
|
|
17274
|
+
resolve9({ code: code ?? 1, stderr, stdout, timedOut });
|
|
17275
|
+
});
|
|
17276
|
+
child.once("exit", (code) => {
|
|
17277
|
+
if (timedOut) {
|
|
17278
|
+
clearTimeout(deadline);
|
|
17279
|
+
resolve9({ code: code ?? 1, stderr, stdout, timedOut });
|
|
17280
|
+
}
|
|
17281
|
+
});
|
|
17282
|
+
child.stdin.end(options.prompt);
|
|
17283
|
+
});
|
|
17284
|
+
var writeMcpConfig = async (dir, snapshotPath, launcher) => {
|
|
17285
|
+
const resolved = launcher ?? {
|
|
17286
|
+
args: [process.argv[1] ?? "", "mcp-stdio", "--data", snapshotPath],
|
|
17287
|
+
command: process.execPath
|
|
17288
|
+
};
|
|
17289
|
+
const configPath = join35(dir, "mcp-config.json");
|
|
17290
|
+
const config = {
|
|
17291
|
+
mcpServers: {
|
|
17292
|
+
[MCP_SERVER_NAME]: { args: resolved.args, command: resolved.command }
|
|
17293
|
+
}
|
|
17294
|
+
};
|
|
17295
|
+
await writeFile13(configPath, JSON.stringify(config, null, 2));
|
|
17296
|
+
return {
|
|
17297
|
+
configPath,
|
|
17298
|
+
serverArgs: resolved.args,
|
|
17299
|
+
serverCommand: resolved.command
|
|
17300
|
+
};
|
|
17301
|
+
};
|
|
17302
|
+
var CLAUDE_READER_MAX_TURNS = "25";
|
|
17303
|
+
var CLAUDE_JUDGE_MAX_TURNS = "1";
|
|
17304
|
+
var claudeArgs = (context) => {
|
|
17305
|
+
const base = ["-p", "--output-format", "json", "--strict-mcp-config"];
|
|
17306
|
+
if (context.mcp) {
|
|
17307
|
+
const allowed = MCP_TOOL_NAMES.map((tool) => `mcp__${MCP_SERVER_NAME}__${tool}`).join(",");
|
|
17308
|
+
return [
|
|
17309
|
+
...base,
|
|
17310
|
+
"--mcp-config",
|
|
17311
|
+
context.mcp.configPath,
|
|
17312
|
+
"--allowedTools",
|
|
17313
|
+
allowed,
|
|
17314
|
+
"--disallowedTools",
|
|
17315
|
+
DISALLOWED_TOOLS.join(","),
|
|
17316
|
+
"--max-turns",
|
|
17317
|
+
CLAUDE_READER_MAX_TURNS
|
|
17318
|
+
];
|
|
17319
|
+
}
|
|
17320
|
+
return [
|
|
17321
|
+
...base,
|
|
17322
|
+
"--disallowedTools",
|
|
17323
|
+
DISALLOWED_TOOLS.join(","),
|
|
17324
|
+
"--max-turns",
|
|
17325
|
+
CLAUDE_JUDGE_MAX_TURNS
|
|
17326
|
+
];
|
|
17327
|
+
};
|
|
17328
|
+
var codexArgs = (context) => {
|
|
17329
|
+
const base = [
|
|
17330
|
+
"exec",
|
|
17331
|
+
"--skip-git-repo-check",
|
|
17332
|
+
"--ignore-user-config",
|
|
17333
|
+
"--ephemeral",
|
|
17334
|
+
"--sandbox",
|
|
17335
|
+
"read-only",
|
|
17336
|
+
"--output-last-message",
|
|
17337
|
+
context.lastMessagePath
|
|
17338
|
+
];
|
|
17339
|
+
if (context.mcp) {
|
|
17340
|
+
return [
|
|
17341
|
+
...base,
|
|
17342
|
+
"-c",
|
|
17343
|
+
`mcp_servers.${MCP_SERVER_NAME}.command=${JSON.stringify(context.mcp.serverCommand)}`,
|
|
17344
|
+
"-c",
|
|
17345
|
+
`mcp_servers.${MCP_SERVER_NAME}.args=${JSON.stringify(context.mcp.serverArgs)}`,
|
|
17346
|
+
"-"
|
|
17347
|
+
];
|
|
17348
|
+
}
|
|
17349
|
+
return [...base, "-"];
|
|
17350
|
+
};
|
|
17351
|
+
var agentArgs = (kind, context) => kind === "claude" ? claudeArgs(context) : codexArgs(context);
|
|
17352
|
+
var claudeResultSchema = z3.object({
|
|
17353
|
+
is_error: z3.boolean().default(false),
|
|
17354
|
+
result: z3.string().default(""),
|
|
17355
|
+
total_cost_usd: z3.number().optional()
|
|
17356
|
+
});
|
|
17357
|
+
var tail = (value, max = 300) => {
|
|
17358
|
+
const trimmed = value.trim();
|
|
17359
|
+
return trimmed.length > max ? trimmed.slice(-max) : trimmed;
|
|
17360
|
+
};
|
|
17361
|
+
var readAgentOutput = async (kind, result, lastMessagePath) => {
|
|
17362
|
+
if (result.timedOut) {
|
|
17363
|
+
return { detail: "timed out", isError: true, text: "" };
|
|
17364
|
+
}
|
|
17365
|
+
if (result.code !== 0) {
|
|
17366
|
+
return {
|
|
17367
|
+
detail: tail(result.stderr) || `exited with code ${result.code}`,
|
|
17368
|
+
isError: true,
|
|
17369
|
+
text: ""
|
|
17370
|
+
};
|
|
17371
|
+
}
|
|
17372
|
+
if (kind === "claude") {
|
|
17373
|
+
let parsed;
|
|
17374
|
+
try {
|
|
17375
|
+
parsed = JSON.parse(result.stdout);
|
|
17376
|
+
} catch {
|
|
17377
|
+
return {
|
|
17378
|
+
detail: "unparseable --output-format json payload",
|
|
17379
|
+
isError: true,
|
|
17380
|
+
text: ""
|
|
17381
|
+
};
|
|
17382
|
+
}
|
|
17383
|
+
const payload = claudeResultSchema.safeParse(parsed);
|
|
17384
|
+
if (!payload.success) {
|
|
17385
|
+
return {
|
|
17386
|
+
detail: "unexpected --output-format json shape",
|
|
17387
|
+
isError: true,
|
|
17388
|
+
text: ""
|
|
17389
|
+
};
|
|
17390
|
+
}
|
|
17391
|
+
return {
|
|
17392
|
+
costUsd: payload.data.total_cost_usd,
|
|
17393
|
+
isError: payload.data.is_error,
|
|
17394
|
+
text: payload.data.result
|
|
17395
|
+
};
|
|
17396
|
+
}
|
|
17397
|
+
let text;
|
|
17398
|
+
try {
|
|
17399
|
+
const raw = await readFile17(lastMessagePath, "utf-8");
|
|
17400
|
+
text = raw.trim();
|
|
17401
|
+
} catch {
|
|
17402
|
+
return { detail: "no last message written", isError: true, text: "" };
|
|
17403
|
+
}
|
|
17404
|
+
if (text === "") {
|
|
17405
|
+
return { detail: "empty last message", isError: true, text: "" };
|
|
17406
|
+
}
|
|
17407
|
+
return { isError: false, text };
|
|
17408
|
+
};
|
|
17409
|
+
var verdictSchema = z3.object({
|
|
17410
|
+
missing: z3.array(z3.string()).default([]),
|
|
17411
|
+
notes: z3.string().default(""),
|
|
17412
|
+
pass: z3.boolean(),
|
|
17413
|
+
score: z3.number().min(0).max(1).optional()
|
|
17414
|
+
});
|
|
17415
|
+
var parseVerdict = (text) => {
|
|
17416
|
+
const start = text.indexOf("{");
|
|
17417
|
+
const end = text.lastIndexOf("}");
|
|
17418
|
+
if (start === -1 || end <= start) {
|
|
17419
|
+
return;
|
|
17420
|
+
}
|
|
17421
|
+
let parsed;
|
|
17422
|
+
try {
|
|
17423
|
+
parsed = JSON.parse(text.slice(start, end + 1));
|
|
17424
|
+
} catch {
|
|
17425
|
+
return;
|
|
17426
|
+
}
|
|
17427
|
+
const result = verdictSchema.safeParse(parsed);
|
|
17428
|
+
return result.success ? result.data : undefined;
|
|
17429
|
+
};
|
|
17430
|
+
|
|
17431
|
+
// src/eval/schema.ts
|
|
17432
|
+
import { readFile as readFile18 } from "node:fs/promises";
|
|
17433
|
+
import { load as load2 } from "js-yaml";
|
|
17434
|
+
import { z as z4 } from "zod";
|
|
17435
|
+
var ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/u;
|
|
17436
|
+
var questionSchema = z4.strictObject({
|
|
17437
|
+
expected: z4.array(z4.string().min(1)).min(1, "expected must list at least one fact"),
|
|
17438
|
+
id: z4.string().regex(ID_PATTERN, "id must be a kebab-case slug (a-z, 0-9, dashes)"),
|
|
17439
|
+
question: z4.string().min(1),
|
|
17440
|
+
routes: z4.union([z4.string(), z4.array(z4.string())]).default([]).transform((value) => typeof value === "string" ? [value] : value),
|
|
17441
|
+
severity: z4.enum(["error", "warning"]).default("error"),
|
|
17442
|
+
skip: z4.boolean().default(false)
|
|
17443
|
+
});
|
|
17444
|
+
var fullSchema = z4.strictObject({
|
|
17445
|
+
questions: z4.array(questionSchema).min(1),
|
|
17446
|
+
version: z4.literal(1).default(1)
|
|
17447
|
+
});
|
|
17448
|
+
var evalsFileSchema = fullSchema.superRefine((value, context) => {
|
|
17449
|
+
const seen = new Set;
|
|
17450
|
+
for (const question of value.questions) {
|
|
17451
|
+
if (seen.has(question.id)) {
|
|
17452
|
+
context.addIssue({
|
|
17453
|
+
code: z4.ZodIssueCode.custom,
|
|
17454
|
+
message: `duplicate question id "${question.id}"`,
|
|
17455
|
+
path: ["questions"]
|
|
17456
|
+
});
|
|
17457
|
+
}
|
|
17458
|
+
seen.add(question.id);
|
|
17459
|
+
}
|
|
17460
|
+
});
|
|
17461
|
+
|
|
17462
|
+
class EvalsFileError extends Error {
|
|
17463
|
+
path;
|
|
17464
|
+
constructor(path, message) {
|
|
17465
|
+
super(message);
|
|
17466
|
+
this.name = "EvalsFileError";
|
|
17467
|
+
this.path = path;
|
|
17468
|
+
}
|
|
17469
|
+
}
|
|
17470
|
+
var describeIssues = (error) => error.issues.map((issue) => {
|
|
17471
|
+
const at = issue.path.length > 0 ? ` at ${issue.path.join(".")}` : "";
|
|
17472
|
+
return `${issue.message}${at}`;
|
|
17473
|
+
}).join("; ");
|
|
17474
|
+
var loadEvalsFile = async (path) => {
|
|
17475
|
+
let raw;
|
|
17476
|
+
try {
|
|
17477
|
+
raw = await readFile18(path, "utf-8");
|
|
17478
|
+
} catch {
|
|
17479
|
+
throw new EvalsFileError(path, `No evals file found at ${path}. Run \`blume eval init\` to draft one.`);
|
|
17480
|
+
}
|
|
17481
|
+
let parsed;
|
|
17482
|
+
try {
|
|
17483
|
+
parsed = load2(raw);
|
|
17484
|
+
} catch (error) {
|
|
17485
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
17486
|
+
throw new EvalsFileError(path, `Invalid YAML in ${path}: ${detail}`);
|
|
17487
|
+
}
|
|
17488
|
+
const candidate = Array.isArray(parsed) ? { questions: parsed } : parsed;
|
|
17489
|
+
const result = evalsFileSchema.safeParse(candidate);
|
|
17490
|
+
if (!result.success) {
|
|
17491
|
+
throw new EvalsFileError(path, `Invalid evals file at ${path}: ${describeIssues(result.error)}`);
|
|
17492
|
+
}
|
|
17493
|
+
return { evals: result.data, raw };
|
|
17494
|
+
};
|
|
17495
|
+
var locateQuestion = (raw, id2) => {
|
|
17496
|
+
const pattern = new RegExp(`^\\s*-?\\s*id:\\s*["']?${id2}["']?\\s*$`, "u");
|
|
17497
|
+
const lines = raw.split(`
|
|
17498
|
+
`);
|
|
17499
|
+
for (const [index, line] of lines.entries()) {
|
|
17500
|
+
if (pattern.test(line)) {
|
|
17501
|
+
return index + 1;
|
|
17502
|
+
}
|
|
17503
|
+
}
|
|
17504
|
+
return;
|
|
17505
|
+
};
|
|
17506
|
+
|
|
17507
|
+
// src/eval/findings.ts
|
|
17508
|
+
var DOCS_URL = "https://useblume.dev/docs/reference/eval";
|
|
17509
|
+
var hintedRoute = (question, project) => {
|
|
17510
|
+
for (const hint of question.routes) {
|
|
17511
|
+
const route = project.manifest.routes.find((candidate) => candidate.path === hint);
|
|
17512
|
+
if (route) {
|
|
17513
|
+
return route;
|
|
17514
|
+
}
|
|
17515
|
+
}
|
|
17516
|
+
};
|
|
17517
|
+
var routeFindings = (question, project, anchor) => {
|
|
17518
|
+
const known = new Set(project.manifest.routes.map((route) => route.path));
|
|
17519
|
+
return question.routes.filter((hint) => !known.has(hint)).map((hint) => ({
|
|
17520
|
+
code: "BLUME_EVAL_ROUTE_UNKNOWN",
|
|
17521
|
+
docsUrl: DOCS_URL,
|
|
17522
|
+
file: anchor.path,
|
|
17523
|
+
line: locateQuestion(anchor.raw, question.id),
|
|
17524
|
+
message: `Question "${question.id}" hints at route "${hint}", which matches no page.`,
|
|
17525
|
+
severity: "warning",
|
|
17526
|
+
suggestion: "Update the question's `routes` to the page's current route, or remove the hint."
|
|
17527
|
+
}));
|
|
17528
|
+
};
|
|
17529
|
+
var questionFinding = (question, outcome, project, anchor) => {
|
|
17530
|
+
const route = hintedRoute(question, project);
|
|
17531
|
+
const site = route ? { file: route.sourcePath, url: route.path } : { file: anchor.path, line: locateQuestion(anchor.raw, question.id) };
|
|
17532
|
+
if (outcome.status === "error") {
|
|
17533
|
+
return {
|
|
17534
|
+
code: "BLUME_EVAL_QUESTION_ERROR",
|
|
17535
|
+
docsUrl: DOCS_URL,
|
|
17536
|
+
message: `Eval run failed for "${question.question}"${outcome.detail ? ` — ${outcome.detail}` : ""}.`,
|
|
17537
|
+
severity: question.severity,
|
|
17538
|
+
suggestion: "Rerun `blume eval`; if it persists, check the agent CLI installation and the failure detail.",
|
|
17539
|
+
...site
|
|
17540
|
+
};
|
|
17541
|
+
}
|
|
17542
|
+
const missing = outcome.missing.length > 0 ? ` — missing: ${outcome.missing.join("; ")}` : "";
|
|
17543
|
+
return {
|
|
17544
|
+
code: "BLUME_EVAL_QUESTION_FAILED",
|
|
17545
|
+
docsUrl: DOCS_URL,
|
|
17546
|
+
message: `Docs could not answer: "${question.question}"${missing}`,
|
|
17547
|
+
severity: question.severity,
|
|
17548
|
+
suggestion: "State the missing facts on this page, then rerun `blume eval`.",
|
|
17549
|
+
...site
|
|
17550
|
+
};
|
|
17551
|
+
};
|
|
17552
|
+
|
|
17553
|
+
// src/eval/run.ts
|
|
17554
|
+
var DEFAULT_READER_TIMEOUT_MS = 180000;
|
|
17555
|
+
var DEFAULT_JUDGE_TIMEOUT_MS = 60000;
|
|
17556
|
+
var errored = (question, detail, durationMs) => ({
|
|
17557
|
+
detail,
|
|
17558
|
+
durationMs,
|
|
17559
|
+
expected: question.expected,
|
|
17560
|
+
id: question.id,
|
|
17561
|
+
missing: [],
|
|
17562
|
+
question: question.question,
|
|
17563
|
+
routes: question.routes,
|
|
17564
|
+
status: "error"
|
|
17565
|
+
});
|
|
17566
|
+
var runQuestion = async (question, index, context) => {
|
|
17567
|
+
const started = performance.now();
|
|
17568
|
+
const elapsed = () => Math.round(performance.now() - started);
|
|
17569
|
+
const workDir = join36(context.dir, `work-${index}`);
|
|
17570
|
+
await mkdir9(workDir, { recursive: true });
|
|
17571
|
+
const answerPath = join36(workDir, "answer.txt");
|
|
17572
|
+
const reader = await context.run(context.bin, agentArgs(context.kind, { lastMessagePath: answerPath, mcp: context.mcp }), {
|
|
17573
|
+
cwd: workDir,
|
|
17574
|
+
prompt: readerPrompt(question),
|
|
17575
|
+
timeoutMs: context.readerTimeoutMs
|
|
17576
|
+
});
|
|
17577
|
+
const answer = await readAgentOutput(context.kind, reader, answerPath);
|
|
17578
|
+
if (answer.isError) {
|
|
17579
|
+
return {
|
|
17580
|
+
...errored(question, `reader ${answer.detail ?? "failed"}`, elapsed()),
|
|
17581
|
+
costUsd: answer.costUsd
|
|
17582
|
+
};
|
|
17583
|
+
}
|
|
17584
|
+
const verdictPath = join36(workDir, "verdict.txt");
|
|
17585
|
+
const judge = await context.run(context.bin, agentArgs(context.kind, { lastMessagePath: verdictPath }), {
|
|
17586
|
+
cwd: workDir,
|
|
17587
|
+
prompt: judgePrompt(question, answer.text),
|
|
17588
|
+
timeoutMs: context.judgeTimeoutMs
|
|
17589
|
+
});
|
|
17590
|
+
const graded = await readAgentOutput(context.kind, judge, verdictPath);
|
|
17591
|
+
const costUsd = answer.costUsd === undefined && graded.costUsd === undefined ? undefined : (answer.costUsd ?? 0) + (graded.costUsd ?? 0);
|
|
17592
|
+
if (graded.isError) {
|
|
17593
|
+
return {
|
|
17594
|
+
...errored(question, `judge ${graded.detail ?? "failed"}`, elapsed()),
|
|
17595
|
+
answer: answer.text,
|
|
17596
|
+
costUsd
|
|
17597
|
+
};
|
|
17598
|
+
}
|
|
17599
|
+
const verdict = parseVerdict(graded.text);
|
|
17600
|
+
if (!verdict) {
|
|
17601
|
+
return {
|
|
17602
|
+
...errored(question, "judge returned no parseable verdict", elapsed()),
|
|
17603
|
+
answer: answer.text,
|
|
17604
|
+
costUsd
|
|
17605
|
+
};
|
|
17606
|
+
}
|
|
17607
|
+
return {
|
|
17608
|
+
answer: answer.text,
|
|
17609
|
+
costUsd,
|
|
17610
|
+
durationMs: elapsed(),
|
|
17611
|
+
expected: question.expected,
|
|
17612
|
+
id: question.id,
|
|
17613
|
+
missing: verdict.missing,
|
|
17614
|
+
notes: verdict.notes || undefined,
|
|
17615
|
+
question: question.question,
|
|
17616
|
+
routes: question.routes,
|
|
17617
|
+
score: verdict.score,
|
|
17618
|
+
status: verdict.pass ? "pass" : "fail"
|
|
17619
|
+
};
|
|
17620
|
+
};
|
|
17621
|
+
var runEval = async (options) => {
|
|
17622
|
+
const started = performance.now();
|
|
17623
|
+
const kind = options.agent;
|
|
17624
|
+
const run = options.run ?? runAgentHeadless;
|
|
17625
|
+
const anchor = { path: options.evalsPath, raw: options.rawEvals };
|
|
17626
|
+
const dir = await mkdtemp3(join36(tmpdir3(), "blume-eval-"));
|
|
17627
|
+
const snapshotPath = join36(dir, "mcp-data.json");
|
|
17628
|
+
await writeFile14(snapshotPath, JSON.stringify(await buildMcpData(options.project)));
|
|
17629
|
+
const mcp = await writeMcpConfig(dir, snapshotPath);
|
|
17630
|
+
const context = {
|
|
17631
|
+
bin: AGENTS[kind].bin,
|
|
17632
|
+
dir,
|
|
17633
|
+
judgeTimeoutMs: options.judgeTimeoutMs ?? DEFAULT_JUDGE_TIMEOUT_MS,
|
|
17634
|
+
kind,
|
|
17635
|
+
mcp,
|
|
17636
|
+
readerTimeoutMs: options.readerTimeoutMs ?? DEFAULT_READER_TIMEOUT_MS,
|
|
17637
|
+
run
|
|
17638
|
+
};
|
|
17639
|
+
const diagnostics = [];
|
|
17640
|
+
const results = [];
|
|
17641
|
+
const { questions } = options.evals;
|
|
17642
|
+
for (const [index, question] of questions.entries()) {
|
|
17643
|
+
diagnostics.push(...routeFindings(question, options.project, anchor));
|
|
17644
|
+
if (question.skip) {
|
|
17645
|
+
results.push({
|
|
17646
|
+
durationMs: 0,
|
|
17647
|
+
expected: question.expected,
|
|
17648
|
+
id: question.id,
|
|
17649
|
+
missing: [],
|
|
17650
|
+
question: question.question,
|
|
17651
|
+
routes: question.routes,
|
|
17652
|
+
status: "skip"
|
|
17653
|
+
});
|
|
17654
|
+
continue;
|
|
17655
|
+
}
|
|
17656
|
+
options.onProgress?.({
|
|
17657
|
+
id: question.id,
|
|
17658
|
+
index,
|
|
17659
|
+
kind: "question-start",
|
|
17660
|
+
total: questions.length
|
|
17661
|
+
});
|
|
17662
|
+
const result = await runQuestion(question, index, context);
|
|
17663
|
+
results.push(result);
|
|
17664
|
+
if (result.status === "fail" || result.status === "error") {
|
|
17665
|
+
diagnostics.push(questionFinding(question, {
|
|
17666
|
+
detail: result.detail,
|
|
17667
|
+
missing: result.missing,
|
|
17668
|
+
status: result.status
|
|
17669
|
+
}, options.project, anchor));
|
|
17670
|
+
}
|
|
17671
|
+
options.onProgress?.({
|
|
17672
|
+
index,
|
|
17673
|
+
kind: "question-end",
|
|
17674
|
+
result,
|
|
17675
|
+
total: questions.length
|
|
17676
|
+
});
|
|
17677
|
+
}
|
|
17678
|
+
const counts = {
|
|
17679
|
+
error: 0,
|
|
17680
|
+
fail: 0,
|
|
17681
|
+
pass: 0,
|
|
17682
|
+
skip: 0
|
|
17683
|
+
};
|
|
17684
|
+
for (const result of results) {
|
|
17685
|
+
counts[result.status] += 1;
|
|
17686
|
+
}
|
|
17687
|
+
const costs = results.flatMap((result) => result.costUsd === undefined ? [] : [result.costUsd]);
|
|
17688
|
+
return {
|
|
17689
|
+
agent: kind,
|
|
17690
|
+
costUsd: costs.length > 0 ? costs.reduce((total, cost) => total + cost, 0) : undefined,
|
|
17691
|
+
counts,
|
|
17692
|
+
diagnostics,
|
|
17693
|
+
durationMs: Math.round(performance.now() - started),
|
|
17694
|
+
results
|
|
17695
|
+
};
|
|
17696
|
+
};
|
|
17697
|
+
|
|
17698
|
+
// src/cli/commands/eval.ts
|
|
17699
|
+
var DEFAULT_FILE = "evals.yaml";
|
|
17700
|
+
var DEFAULT_TIMEOUT_S = 180;
|
|
17701
|
+
var isAgentKind = (value) => (value in AGENTS);
|
|
17702
|
+
var launchAgentCode2 = async (bin, prompt) => {
|
|
17703
|
+
try {
|
|
17704
|
+
return await launchAgent(bin, prompt);
|
|
17705
|
+
} catch (error) {
|
|
17706
|
+
if (error?.code !== "ENOENT") {
|
|
17707
|
+
throw error;
|
|
17708
|
+
}
|
|
17709
|
+
return WINDOWS_COMMAND_NOT_FOUND;
|
|
17710
|
+
}
|
|
17711
|
+
};
|
|
17712
|
+
var notInstalled = (agent) => {
|
|
17713
|
+
const cli = AGENTS[agent];
|
|
17714
|
+
logger.error(`${cli.name} (\`${cli.bin}\`) was not found on PATH. Install it with \`${cli.install}\`.`);
|
|
17715
|
+
return process.exit(1);
|
|
17716
|
+
};
|
|
17717
|
+
var passFraction = (result) => {
|
|
17718
|
+
const ran = result.results.length - result.counts.skip;
|
|
17719
|
+
return ran === 0 ? 1 : result.counts.pass / ran;
|
|
17720
|
+
};
|
|
17721
|
+
var parseFlags = (args) => {
|
|
17722
|
+
if (!isAgentKind(args.agent)) {
|
|
17723
|
+
logger.error(`Invalid --agent "${args.agent}" (use claude | codex).`);
|
|
17724
|
+
process.exit(1);
|
|
17725
|
+
}
|
|
17726
|
+
if (args.action !== undefined && args.action !== "init") {
|
|
17727
|
+
logger.error(`Unknown action "${args.action}" (did you mean "init"?).`);
|
|
17728
|
+
process.exit(1);
|
|
17729
|
+
}
|
|
17730
|
+
if (args.json && args.fix) {
|
|
17731
|
+
logger.error("--json and --fix are mutually exclusive.");
|
|
17732
|
+
process.exit(1);
|
|
17733
|
+
}
|
|
17734
|
+
const threshold = args.threshold === undefined ? 1 : Number(args.threshold);
|
|
17735
|
+
if (!Number.isFinite(threshold) || threshold < 0 || threshold > 1) {
|
|
17736
|
+
logger.error(`Invalid --threshold "${args.threshold}" (use 0..1).`);
|
|
17737
|
+
process.exit(1);
|
|
17738
|
+
}
|
|
17739
|
+
const timeoutS = args.timeout === undefined ? DEFAULT_TIMEOUT_S : Number(args.timeout);
|
|
17740
|
+
if (!Number.isInteger(timeoutS) || timeoutS <= 0) {
|
|
17741
|
+
logger.error(`Invalid --timeout "${args.timeout}" (whole seconds).`);
|
|
17742
|
+
process.exit(1);
|
|
17743
|
+
}
|
|
17744
|
+
return { agent: args.agent, threshold, timeoutS };
|
|
17745
|
+
};
|
|
17746
|
+
var runFixHandoff = async (agent, result, root, threshold) => {
|
|
17747
|
+
const count = result.counts.fail + result.counts.error;
|
|
17748
|
+
if (count === 0) {
|
|
17749
|
+
return;
|
|
17750
|
+
}
|
|
17751
|
+
const cli = AGENTS[agent];
|
|
17752
|
+
const report = await writeEvalReport(result, root, threshold);
|
|
17753
|
+
process.stderr.write(` Handing ${count} failed question${count === 1 ? "" : "s"} to ${cli.name}…
|
|
17754
|
+
|
|
17755
|
+
`);
|
|
17756
|
+
const code = await launchAgentCode2(cli.bin, evalFixPrompt(report));
|
|
17757
|
+
if (code === WINDOWS_COMMAND_NOT_FOUND) {
|
|
17758
|
+
notInstalled(agent);
|
|
17759
|
+
}
|
|
17760
|
+
if (code !== 0) {
|
|
17761
|
+
process.exit(code);
|
|
17762
|
+
}
|
|
17763
|
+
};
|
|
17764
|
+
var runInit = async (agent, file) => {
|
|
17765
|
+
const path = join37(process.cwd(), file);
|
|
17766
|
+
if (existsSync20(path)) {
|
|
17767
|
+
logger.error(`${file} already exists — edit it directly, or pass --file to draft elsewhere.`);
|
|
17768
|
+
process.exit(1);
|
|
17769
|
+
}
|
|
17770
|
+
const code = await launchAgentCode2(AGENTS[agent].bin, initPrompt(file));
|
|
17771
|
+
if (code === WINDOWS_COMMAND_NOT_FOUND) {
|
|
17772
|
+
notInstalled(agent);
|
|
17773
|
+
}
|
|
17774
|
+
if (code !== 0) {
|
|
17775
|
+
process.exit(code);
|
|
17776
|
+
}
|
|
17777
|
+
};
|
|
17778
|
+
var evalCommand = defineCommand8({
|
|
17779
|
+
args: {
|
|
17780
|
+
action: {
|
|
17781
|
+
description: 'Optional action: "init" drafts a starter evals file.',
|
|
17782
|
+
required: false,
|
|
17783
|
+
type: "positional"
|
|
17784
|
+
},
|
|
17785
|
+
agent: {
|
|
17786
|
+
default: "claude",
|
|
17787
|
+
description: "Agent CLI that reads and grades the docs: claude | codex.",
|
|
17788
|
+
type: "string"
|
|
17789
|
+
},
|
|
17790
|
+
file: {
|
|
17791
|
+
default: DEFAULT_FILE,
|
|
17792
|
+
description: "The evals file to run.",
|
|
17793
|
+
type: "string"
|
|
17794
|
+
},
|
|
17795
|
+
fix: {
|
|
17796
|
+
description: "After a failing run, hand the report to the agent to fix the docs interactively.",
|
|
17797
|
+
type: "boolean"
|
|
17798
|
+
},
|
|
17799
|
+
json: {
|
|
17800
|
+
description: "Emit the report as JSON on stdout (for CI/editors).",
|
|
17801
|
+
type: "boolean"
|
|
17802
|
+
},
|
|
17803
|
+
threshold: {
|
|
17804
|
+
description: "Minimum passing fraction (0..1) before the run exits non-zero. Defaults to 1.",
|
|
17805
|
+
type: "string"
|
|
17806
|
+
},
|
|
17807
|
+
timeout: {
|
|
17808
|
+
description: `Reader time limit per question, in seconds. Defaults to ${DEFAULT_TIMEOUT_S}.`,
|
|
17809
|
+
type: "string"
|
|
17810
|
+
},
|
|
17811
|
+
verbose: {
|
|
17812
|
+
description: "Include the reader's full answer under each failure.",
|
|
17813
|
+
type: "boolean"
|
|
17814
|
+
}
|
|
17815
|
+
},
|
|
17816
|
+
meta: {
|
|
17817
|
+
description: "Test the docs: an agent answers your questions using only the documentation.",
|
|
17818
|
+
name: "eval"
|
|
17819
|
+
},
|
|
17820
|
+
async run({ args }) {
|
|
17821
|
+
const root = process.cwd();
|
|
17822
|
+
const { agent, threshold, timeoutS } = parseFlags(args);
|
|
17823
|
+
if (args.action === "init") {
|
|
17824
|
+
await runInit(agent, args.file);
|
|
17825
|
+
return;
|
|
17826
|
+
}
|
|
17827
|
+
let result;
|
|
17828
|
+
try {
|
|
17829
|
+
const project = await scanProject(root, { mode: "build" });
|
|
17830
|
+
const evalsPath = join37(root, args.file);
|
|
17831
|
+
const { evals, raw } = await loadEvalsFile(evalsPath);
|
|
17832
|
+
process.stderr.write(`${headerLine(evals.questions.length, agent)}
|
|
17833
|
+
|
|
17834
|
+
`);
|
|
17835
|
+
result = await runEval({
|
|
17836
|
+
agent,
|
|
17837
|
+
evals,
|
|
17838
|
+
evalsPath,
|
|
17839
|
+
onProgress: (event) => {
|
|
17840
|
+
if (event.kind === "question-start") {
|
|
17841
|
+
process.stderr.write(`${startLine(event.id, event.index, event.total)}
|
|
17842
|
+
`);
|
|
17843
|
+
return;
|
|
17844
|
+
}
|
|
17845
|
+
const lines = [
|
|
17846
|
+
questionLine(event.result),
|
|
17847
|
+
...questionDetails(event.result, Boolean(args.verbose))
|
|
17848
|
+
];
|
|
17849
|
+
process.stderr.write(`${lines.join(`
|
|
17850
|
+
`)}
|
|
17851
|
+
`);
|
|
17852
|
+
},
|
|
17853
|
+
project,
|
|
17854
|
+
rawEvals: raw,
|
|
17855
|
+
readerTimeoutMs: timeoutS * 1000
|
|
17856
|
+
});
|
|
17857
|
+
} catch (error) {
|
|
17858
|
+
if (error instanceof EvalsFileError) {
|
|
17859
|
+
logger.error(error.message);
|
|
17860
|
+
process.exit(1);
|
|
17861
|
+
}
|
|
17862
|
+
if (error instanceof BlumeError) {
|
|
17863
|
+
logger.error(error.diagnostic.message);
|
|
17864
|
+
process.exit(1);
|
|
17865
|
+
}
|
|
17866
|
+
if (error?.code === "ENOENT") {
|
|
17867
|
+
notInstalled(agent);
|
|
17868
|
+
}
|
|
17869
|
+
reportInternalError(error);
|
|
17870
|
+
process.exit(1);
|
|
17871
|
+
}
|
|
17872
|
+
const tail2 = [
|
|
17873
|
+
"",
|
|
17874
|
+
...warningLines(result, root),
|
|
17875
|
+
...fixLines(result, root),
|
|
17876
|
+
"",
|
|
17877
|
+
` ${summaryLine2(result)}`,
|
|
17878
|
+
""
|
|
17879
|
+
];
|
|
17880
|
+
process.stderr.write(tail2.join(`
|
|
17881
|
+
`));
|
|
17882
|
+
const failed = passFraction(result) < threshold;
|
|
17883
|
+
if (args.fix) {
|
|
17884
|
+
await runFixHandoff(agent, result, root, threshold);
|
|
17885
|
+
return;
|
|
17886
|
+
}
|
|
17887
|
+
if (args.json) {
|
|
17888
|
+
process.stdout.write(evalReportJson(result, root, threshold));
|
|
17889
|
+
if (failed) {
|
|
17890
|
+
await flushStdout();
|
|
17891
|
+
process.exit(1);
|
|
17892
|
+
}
|
|
17893
|
+
return;
|
|
17894
|
+
}
|
|
17895
|
+
if (failed) {
|
|
17896
|
+
process.exit(1);
|
|
17897
|
+
}
|
|
17898
|
+
}
|
|
17899
|
+
});
|
|
17900
|
+
|
|
16893
17901
|
// src/cli/commands/init.ts
|
|
16894
17902
|
import * as clack from "@clack/prompts";
|
|
16895
|
-
import { defineCommand as
|
|
17903
|
+
import { defineCommand as defineCommand9 } from "citty";
|
|
16896
17904
|
import { resolve as resolve10 } from "pathe";
|
|
16897
17905
|
|
|
16898
17906
|
// src/cli/init/questions.ts
|
|
@@ -17007,7 +18015,7 @@ var ejectScaffold = async (root, answers) => {
|
|
|
17007
18015
|
`);
|
|
17008
18016
|
}
|
|
17009
18017
|
};
|
|
17010
|
-
var initCommand =
|
|
18018
|
+
var initCommand = defineCommand9({
|
|
17011
18019
|
args: {
|
|
17012
18020
|
"content-dir": {
|
|
17013
18021
|
description: "Content directory.",
|
|
@@ -17083,7 +18091,11 @@ var initCommand = defineCommand8({
|
|
|
17083
18091
|
}
|
|
17084
18092
|
const sink = interactive ? clack.log : logger;
|
|
17085
18093
|
const { createdPackage } = await applyPlan(buildPlan(root, answers), sink);
|
|
17086
|
-
const ignored = await ensureGitignore(root, [
|
|
18094
|
+
const ignored = await ensureGitignore(root, [
|
|
18095
|
+
"node_modules/",
|
|
18096
|
+
".blume/",
|
|
18097
|
+
"dist/"
|
|
18098
|
+
]);
|
|
17087
18099
|
if (ignored.length > 0) {
|
|
17088
18100
|
sink.success(`Added ${ignored.join(", ")} to .gitignore`);
|
|
17089
18101
|
}
|
|
@@ -17101,12 +18113,251 @@ var initCommand = defineCommand8({
|
|
|
17101
18113
|
}
|
|
17102
18114
|
});
|
|
17103
18115
|
|
|
18116
|
+
// src/cli/commands/mcp-stdio.ts
|
|
18117
|
+
import { readFile as readFile19 } from "node:fs/promises";
|
|
18118
|
+
import { defineCommand as defineCommand10 } from "citty";
|
|
18119
|
+
|
|
18120
|
+
// src/ai/mcp/stdio.ts
|
|
18121
|
+
import { once } from "node:events";
|
|
18122
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
18123
|
+
|
|
18124
|
+
// src/ai/mcp/server.ts
|
|
18125
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
18126
|
+
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
18127
|
+
import {
|
|
18128
|
+
CallToolRequestSchema,
|
|
18129
|
+
ListToolsRequestSchema
|
|
18130
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
18131
|
+
|
|
18132
|
+
// src/search/orama-index.ts
|
|
18133
|
+
import { create, insertMultiple, search } from "@orama/orama";
|
|
18134
|
+
var SCHEMA = {
|
|
18135
|
+
content: "string",
|
|
18136
|
+
description: "string",
|
|
18137
|
+
locale: "enum",
|
|
18138
|
+
route: "string",
|
|
18139
|
+
title: "string"
|
|
18140
|
+
};
|
|
18141
|
+
var BOOST = { description: 2, title: 3 };
|
|
18142
|
+
var SEGMENTED_LANGUAGES = new Set(["ja", "ko", "th", "zh"]);
|
|
18143
|
+
var segmentingTokenizer = (locale) => {
|
|
18144
|
+
const language = locale?.toLowerCase().split(/[-_]/u)[0] ?? "";
|
|
18145
|
+
if (!SEGMENTED_LANGUAGES.has(language)) {
|
|
18146
|
+
return;
|
|
18147
|
+
}
|
|
18148
|
+
if (typeof Intl.Segmenter !== "function") {
|
|
18149
|
+
return;
|
|
18150
|
+
}
|
|
18151
|
+
const segmenter = new Intl.Segmenter(language, { granularity: "word" });
|
|
18152
|
+
return {
|
|
18153
|
+
language,
|
|
18154
|
+
normalizationCache: new Map,
|
|
18155
|
+
tokenize: (raw) => {
|
|
18156
|
+
const tokens = new Set;
|
|
18157
|
+
for (const segment of segmenter.segment(raw.toLowerCase())) {
|
|
18158
|
+
if (segment.isWordLike) {
|
|
18159
|
+
tokens.add(segment.segment);
|
|
18160
|
+
}
|
|
18161
|
+
}
|
|
18162
|
+
return [...tokens];
|
|
18163
|
+
}
|
|
18164
|
+
};
|
|
18165
|
+
};
|
|
18166
|
+
var buildOramaIndex = async (documents, locale) => {
|
|
18167
|
+
const tokenizer = segmentingTokenizer(locale);
|
|
18168
|
+
const db = create({
|
|
18169
|
+
schema: SCHEMA,
|
|
18170
|
+
...tokenizer ? { components: { tokenizer } } : {}
|
|
18171
|
+
});
|
|
18172
|
+
await insertMultiple(db, documents);
|
|
18173
|
+
return db;
|
|
18174
|
+
};
|
|
18175
|
+
var queryOramaIndex = async (db, term, limit, locale) => {
|
|
18176
|
+
const found = await search(db, {
|
|
18177
|
+
boost: BOOST,
|
|
18178
|
+
limit,
|
|
18179
|
+
properties: ["title", "description", "content"],
|
|
18180
|
+
term,
|
|
18181
|
+
...locale ? { where: { locale: { eq: locale } } } : {}
|
|
18182
|
+
});
|
|
18183
|
+
return found.hits.map((hit) => hit.document);
|
|
18184
|
+
};
|
|
18185
|
+
|
|
18186
|
+
// src/ai/mcp/server.ts
|
|
18187
|
+
var DEFAULT_SEARCH_LIMIT = 8;
|
|
18188
|
+
var MAX_SEARCH_LIMIT = 20;
|
|
18189
|
+
var EXCERPT_LENGTH = 200;
|
|
18190
|
+
var INPUT_SCHEMAS = {
|
|
18191
|
+
get_navigation: { properties: {}, type: "object" },
|
|
18192
|
+
get_page: {
|
|
18193
|
+
properties: {
|
|
18194
|
+
route: {
|
|
18195
|
+
description: "The page route, e.g. `/guides/install`.",
|
|
18196
|
+
type: "string"
|
|
18197
|
+
}
|
|
18198
|
+
},
|
|
18199
|
+
required: ["route"],
|
|
18200
|
+
type: "object"
|
|
18201
|
+
},
|
|
18202
|
+
list_pages: { properties: {}, type: "object" },
|
|
18203
|
+
search_docs: {
|
|
18204
|
+
properties: {
|
|
18205
|
+
limit: {
|
|
18206
|
+
description: `Maximum hits to return (default ${DEFAULT_SEARCH_LIMIT}).`,
|
|
18207
|
+
maximum: MAX_SEARCH_LIMIT,
|
|
18208
|
+
minimum: 1,
|
|
18209
|
+
type: "integer"
|
|
18210
|
+
},
|
|
18211
|
+
query: { description: "The search query.", type: "string" }
|
|
18212
|
+
},
|
|
18213
|
+
required: ["query"],
|
|
18214
|
+
type: "object"
|
|
18215
|
+
}
|
|
18216
|
+
};
|
|
18217
|
+
var TOOL_DEFINITIONS = MCP_TOOLS.map((tool) => ({
|
|
18218
|
+
annotations: tool.annotations,
|
|
18219
|
+
description: tool.description,
|
|
18220
|
+
inputSchema: INPUT_SCHEMAS[tool.name],
|
|
18221
|
+
name: tool.name,
|
|
18222
|
+
title: tool.title
|
|
18223
|
+
}));
|
|
18224
|
+
var asString2 = (value) => typeof value === "string" ? value : "";
|
|
18225
|
+
var asLimit = (value) => {
|
|
18226
|
+
const num = typeof value === "number" ? value : Number(value);
|
|
18227
|
+
if (!Number.isFinite(num)) {
|
|
18228
|
+
return DEFAULT_SEARCH_LIMIT;
|
|
18229
|
+
}
|
|
18230
|
+
return Math.min(Math.max(Math.trunc(num), 1), MAX_SEARCH_LIMIT);
|
|
18231
|
+
};
|
|
18232
|
+
var normalizeRoute2 = (input, data) => {
|
|
18233
|
+
let value = input.trim();
|
|
18234
|
+
if (/^https?:\/\//iu.test(value)) {
|
|
18235
|
+
try {
|
|
18236
|
+
value = new URL(value).pathname;
|
|
18237
|
+
} catch {}
|
|
18238
|
+
}
|
|
18239
|
+
try {
|
|
18240
|
+
value = decodeURI(value);
|
|
18241
|
+
} catch {}
|
|
18242
|
+
const noTrailing = value.replace(/\/+$/u, "");
|
|
18243
|
+
const noSuffix = noTrailing.replace(/\.mdx?$/u, "");
|
|
18244
|
+
const withSlash = noSuffix.startsWith("/") ? noSuffix : `/${noSuffix}`;
|
|
18245
|
+
const based = stripBasePath(data.base, withSlash);
|
|
18246
|
+
return based === "" ? "/" : based;
|
|
18247
|
+
};
|
|
18248
|
+
var urlFor = (route, data) => {
|
|
18249
|
+
const path = withBasePath(data.base, route);
|
|
18250
|
+
return data.site ? `${data.site.replace(/\/+$/u, "")}${path}` : path;
|
|
18251
|
+
};
|
|
18252
|
+
var excerptFor = (doc) => {
|
|
18253
|
+
if (doc.description) {
|
|
18254
|
+
return doc.description;
|
|
18255
|
+
}
|
|
18256
|
+
const head = doc.content.slice(0, EXCERPT_LENGTH).trim();
|
|
18257
|
+
return doc.content.length > EXCERPT_LENGTH ? `${head}…` : head;
|
|
18258
|
+
};
|
|
18259
|
+
var text = (value, isError = false) => ({
|
|
18260
|
+
content: [{ text: value, type: "text" }],
|
|
18261
|
+
...isError ? { isError: true } : {}
|
|
18262
|
+
});
|
|
18263
|
+
var createIndexProvider = (documents, locale) => {
|
|
18264
|
+
let dbPromise = null;
|
|
18265
|
+
return () => {
|
|
18266
|
+
dbPromise ??= buildOramaIndex(documents, locale);
|
|
18267
|
+
return dbPromise;
|
|
18268
|
+
};
|
|
18269
|
+
};
|
|
18270
|
+
var buildServer = (data, index) => {
|
|
18271
|
+
const server = new Server({ name: data.name, version: data.version }, {
|
|
18272
|
+
capabilities: { tools: {} },
|
|
18273
|
+
...data.instructions ? { instructions: data.instructions } : {}
|
|
18274
|
+
});
|
|
18275
|
+
server.setRequestHandler(ListToolsRequestSchema, () => ({
|
|
18276
|
+
tools: TOOL_DEFINITIONS
|
|
18277
|
+
}));
|
|
18278
|
+
server.setRequestHandler(CallToolRequestSchema, async (request2) => {
|
|
18279
|
+
const { arguments: args = {}, name } = request2.params;
|
|
18280
|
+
if (name === "search_docs") {
|
|
18281
|
+
const db = await index();
|
|
18282
|
+
const hits = await queryOramaIndex(db, asString2(args.query), asLimit(args.limit));
|
|
18283
|
+
const results = hits.map((doc) => ({
|
|
18284
|
+
excerpt: excerptFor(doc),
|
|
18285
|
+
route: doc.route,
|
|
18286
|
+
title: doc.title,
|
|
18287
|
+
url: urlFor(doc.route, data)
|
|
18288
|
+
}));
|
|
18289
|
+
return text(JSON.stringify(results, null, 2));
|
|
18290
|
+
}
|
|
18291
|
+
if (name === "get_page") {
|
|
18292
|
+
const key = normalizeRoute2(asString2(args.route), data);
|
|
18293
|
+
const markdown = data.pages[key];
|
|
18294
|
+
if (markdown === undefined) {
|
|
18295
|
+
return text(`No page found at "${key}". Use list_pages or search_docs to find valid routes.`, true);
|
|
18296
|
+
}
|
|
18297
|
+
return text(markdown);
|
|
18298
|
+
}
|
|
18299
|
+
if (name === "list_pages") {
|
|
18300
|
+
return text(JSON.stringify(data.routes.map((route) => ({
|
|
18301
|
+
contentType: route.contentType,
|
|
18302
|
+
description: route.description,
|
|
18303
|
+
lastModified: route.lastModified,
|
|
18304
|
+
route: route.route,
|
|
18305
|
+
title: route.title,
|
|
18306
|
+
url: urlFor(route.route, data)
|
|
18307
|
+
})), null, 2));
|
|
18308
|
+
}
|
|
18309
|
+
if (name === "get_navigation") {
|
|
18310
|
+
return text(JSON.stringify(data.navigation, null, 2));
|
|
18311
|
+
}
|
|
18312
|
+
return text(`Unknown tool: ${name}`, true);
|
|
18313
|
+
});
|
|
18314
|
+
return server;
|
|
18315
|
+
};
|
|
18316
|
+
|
|
18317
|
+
// src/ai/mcp/stdio.ts
|
|
18318
|
+
var serveMcpStdio = async (data, streams = {}) => {
|
|
18319
|
+
const stdin = streams.stdin ?? process.stdin;
|
|
18320
|
+
const stdout = streams.stdout ?? process.stdout;
|
|
18321
|
+
const transport = new StdioServerTransport(stdin, stdout);
|
|
18322
|
+
const server = buildServer(data, createIndexProvider(data.documents, data.defaultLocale));
|
|
18323
|
+
await server.connect(transport);
|
|
18324
|
+
await once(stdin, "end");
|
|
18325
|
+
await transport.close();
|
|
18326
|
+
};
|
|
18327
|
+
|
|
18328
|
+
// src/cli/commands/mcp-stdio.ts
|
|
18329
|
+
var mcpStdioCommand = defineCommand10({
|
|
18330
|
+
args: {
|
|
18331
|
+
data: {
|
|
18332
|
+
description: "Path to a serialized MCP data snapshot (JSON).",
|
|
18333
|
+
required: true,
|
|
18334
|
+
type: "string"
|
|
18335
|
+
}
|
|
18336
|
+
},
|
|
18337
|
+
meta: {
|
|
18338
|
+
description: "Serve an MCP data snapshot over stdio (internal, used by `blume eval`).",
|
|
18339
|
+
name: "mcp-stdio"
|
|
18340
|
+
},
|
|
18341
|
+
async run({ args }) {
|
|
18342
|
+
let data;
|
|
18343
|
+
try {
|
|
18344
|
+
data = JSON.parse(await readFile19(args.data, "utf-8"));
|
|
18345
|
+
} catch (error) {
|
|
18346
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
18347
|
+
process.stderr.write(`blume mcp-stdio: cannot load the snapshot at ${args.data}: ${detail}
|
|
18348
|
+
`);
|
|
18349
|
+
process.exit(1);
|
|
18350
|
+
}
|
|
18351
|
+
await serveMcpStdio(data);
|
|
18352
|
+
}
|
|
18353
|
+
});
|
|
18354
|
+
|
|
17104
18355
|
// src/cli/commands/preview.ts
|
|
17105
|
-
import { existsSync as
|
|
18356
|
+
import { existsSync as existsSync21 } from "node:fs";
|
|
17106
18357
|
import { preview } from "astro";
|
|
17107
|
-
import { defineCommand as
|
|
17108
|
-
import { join as
|
|
17109
|
-
var previewCommand =
|
|
18358
|
+
import { defineCommand as defineCommand11 } from "citty";
|
|
18359
|
+
import { join as join38 } from "pathe";
|
|
18360
|
+
var previewCommand = defineCommand11({
|
|
17110
18361
|
args: {
|
|
17111
18362
|
host: { description: "Network host to bind.", type: "string" },
|
|
17112
18363
|
port: { description: "Port to listen on.", type: "string" }
|
|
@@ -17119,7 +18370,7 @@ var previewCommand = defineCommand9({
|
|
|
17119
18370
|
const root = process.cwd();
|
|
17120
18371
|
const { config } = await loadConfig(root);
|
|
17121
18372
|
const context = resolveProjectContext(root, config);
|
|
17122
|
-
if (!
|
|
18373
|
+
if (!existsSync21(join38(context.outDir, "astro.config.mjs"))) {
|
|
17123
18374
|
logger.error("No build found. Run `blume build` first.");
|
|
17124
18375
|
process.exit(1);
|
|
17125
18376
|
}
|
|
@@ -17136,9 +18387,9 @@ var previewCommand = defineCommand9({
|
|
|
17136
18387
|
|
|
17137
18388
|
// src/cli/commands/sync.ts
|
|
17138
18389
|
import { rm as rm4 } from "node:fs/promises";
|
|
17139
|
-
import { defineCommand as
|
|
17140
|
-
import { join as
|
|
17141
|
-
var syncCommand =
|
|
18390
|
+
import { defineCommand as defineCommand12 } from "citty";
|
|
18391
|
+
import { join as join39 } from "pathe";
|
|
18392
|
+
var syncCommand = defineCommand12({
|
|
17142
18393
|
args: {
|
|
17143
18394
|
force: {
|
|
17144
18395
|
description: "Clear the source cache before refetching.",
|
|
@@ -17159,7 +18410,7 @@ var syncCommand = defineCommand10({
|
|
|
17159
18410
|
if (args.force) {
|
|
17160
18411
|
const { config } = await loadConfig(root);
|
|
17161
18412
|
const context = resolveProjectContext(root, config);
|
|
17162
|
-
await rm4(
|
|
18413
|
+
await rm4(join39(context.outDir, "cache"), { force: true, recursive: true });
|
|
17163
18414
|
logger.info("Cleared source cache.");
|
|
17164
18415
|
}
|
|
17165
18416
|
const lock = readDevLock(resolveRuntimeDir(root));
|
|
@@ -17177,13 +18428,13 @@ var syncCommand = defineCommand10({
|
|
|
17177
18428
|
});
|
|
17178
18429
|
|
|
17179
18430
|
// src/cli/commands/validate.ts
|
|
17180
|
-
import { existsSync as
|
|
17181
|
-
import { defineCommand as
|
|
17182
|
-
import { join as
|
|
18431
|
+
import { existsSync as existsSync23 } from "node:fs";
|
|
18432
|
+
import { defineCommand as defineCommand13 } from "citty";
|
|
18433
|
+
import { join as join41 } from "pathe";
|
|
17183
18434
|
|
|
17184
18435
|
// src/core/links.ts
|
|
17185
|
-
import { existsSync as
|
|
17186
|
-
import { basename as basename7, join as
|
|
18436
|
+
import { existsSync as existsSync22 } from "node:fs";
|
|
18437
|
+
import { basename as basename7, join as join40 } from "pathe";
|
|
17187
18438
|
var HTTP = /^https?:\/\//iu;
|
|
17188
18439
|
var PROTOCOL_RELATIVE = /^\/\//u;
|
|
17189
18440
|
var SCHEME = /^[a-z][a-z0-9+.-]*:/iu;
|
|
@@ -17196,7 +18447,7 @@ var decodePercent = (value) => {
|
|
|
17196
18447
|
};
|
|
17197
18448
|
var DOC_EXT = /\.(?:md|mdx)$/iu;
|
|
17198
18449
|
var FILE_EXT = /\.[a-z0-9]+$/iu;
|
|
17199
|
-
var assetIsPresent = (resolved, ctx) => ctx.publicDir !== null &&
|
|
18450
|
+
var assetIsPresent = (resolved, ctx) => ctx.publicDir !== null && existsSync22(join40(ctx.publicDir, resolved));
|
|
17200
18451
|
var NUMERIC_PREFIX3 = /^\d+[-_.]/u;
|
|
17201
18452
|
var isIndexPage = (page2) => /^index\.(?:md|mdx)$/iu.test(basename7(page2.navPath).replace(NUMERIC_PREFIX3, ""));
|
|
17202
18453
|
var applyRelativePart = (segments, part) => {
|
|
@@ -17369,7 +18620,7 @@ var validateLinks = async (graph, options) => {
|
|
|
17369
18620
|
};
|
|
17370
18621
|
|
|
17371
18622
|
// src/cli/commands/validate.ts
|
|
17372
|
-
var validateCommand =
|
|
18623
|
+
var validateCommand = defineCommand13({
|
|
17373
18624
|
args: {
|
|
17374
18625
|
external: {
|
|
17375
18626
|
description: "Check external (HTTP) links over the network.",
|
|
@@ -17407,12 +18658,12 @@ var validateCommand = defineCommand11({
|
|
|
17407
18658
|
});
|
|
17408
18659
|
extraRoutes.push(...manifest.routes.flatMap((route) => route.fallback ? [route.path] : []));
|
|
17409
18660
|
}
|
|
17410
|
-
const publicDir =
|
|
18661
|
+
const publicDir = join41(root, "public");
|
|
17411
18662
|
diagnostics.push(...await validateLinks(project.graph, {
|
|
17412
18663
|
basePath: project.config.basePath,
|
|
17413
18664
|
checkExternal: Boolean(args.external),
|
|
17414
18665
|
extraRoutes,
|
|
17415
|
-
publicDir:
|
|
18666
|
+
publicDir: existsSync23(publicDir) ? publicDir : null,
|
|
17416
18667
|
redirects: project.config.redirects
|
|
17417
18668
|
}));
|
|
17418
18669
|
} catch (error) {
|
|
@@ -17443,7 +18694,7 @@ var validateCommand = defineCommand11({
|
|
|
17443
18694
|
});
|
|
17444
18695
|
|
|
17445
18696
|
// src/cli/index.ts
|
|
17446
|
-
var main =
|
|
18697
|
+
var main = defineCommand14({
|
|
17447
18698
|
meta: {
|
|
17448
18699
|
description: "Markdown-first documentation powered by Astro and Vite.",
|
|
17449
18700
|
name: "blume",
|
|
@@ -17457,7 +18708,9 @@ var main = defineCommand12({
|
|
|
17457
18708
|
dev: devCommand,
|
|
17458
18709
|
doctor: doctorCommand,
|
|
17459
18710
|
eject: ejectCommand,
|
|
18711
|
+
eval: evalCommand,
|
|
17460
18712
|
init: initCommand,
|
|
18713
|
+
"mcp-stdio": mcpStdioCommand,
|
|
17461
18714
|
preview: previewCommand,
|
|
17462
18715
|
sync: syncCommand,
|
|
17463
18716
|
validate: validateCommand
|
|
@@ -17474,5 +18727,5 @@ process.on("unhandledRejection", (error) => {
|
|
|
17474
18727
|
});
|
|
17475
18728
|
runMain(main);
|
|
17476
18729
|
|
|
17477
|
-
//# debugId=
|
|
18730
|
+
//# debugId=0CA37F943036C96964756E2164756E21
|
|
17478
18731
|
//# sourceMappingURL=index.js.map
|