blume 1.1.4 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +1 -1
- package/dist/cli/index.js +1286 -63
- package/dist/cli/index.js.map +32 -21
- 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/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 +1 -1
- package/src/ai/agent-readability.ts +19 -1
- package/src/ai/llms.ts +9 -4
- package/src/ai/mcp/server.ts +19 -8
- package/src/ai/mcp/stdio.ts +35 -0
- package/src/astro/generate.ts +25 -2
- package/src/astro/templates.ts +114 -22
- 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 +37 -19
- package/src/components/layout/ReferenceLayout.astro +4 -0
- package/src/components/layout/RootLayout.astro +1 -1
- 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/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.
|
|
@@ -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,
|
|
@@ -13475,6 +13585,7 @@ var buildReferenceFiles = async (options) => {
|
|
|
13475
13585
|
...ref.scalar
|
|
13476
13586
|
},
|
|
13477
13587
|
dataImport: `${"../".repeat(depth + 1)}generated/data.json`,
|
|
13588
|
+
noindex: ref.noindex,
|
|
13478
13589
|
route: ref.route,
|
|
13479
13590
|
title: ref.label
|
|
13480
13591
|
}),
|
|
@@ -14765,6 +14876,16 @@ var detectUsesMath = async (root, staged = []) => {
|
|
|
14765
14876
|
const contents = await Promise.all(files.map((file) => readOptional(join26(root, file))));
|
|
14766
14877
|
return [...contents, ...staged].some(containsMath);
|
|
14767
14878
|
};
|
|
14879
|
+
var hashConfigSource = (source) => createHash2("sha256").update(source).digest("hex");
|
|
14880
|
+
var loadIntegrationBridge = async (config, context) => {
|
|
14881
|
+
if (config.integrations.length === 0 || !context.configFile) {
|
|
14882
|
+
return;
|
|
14883
|
+
}
|
|
14884
|
+
return {
|
|
14885
|
+
configFile: relative13(context.outDir, context.configFile),
|
|
14886
|
+
sourceHash: hashConfigSource(await readOptional(context.configFile))
|
|
14887
|
+
};
|
|
14888
|
+
};
|
|
14768
14889
|
var writeIfChanged = async (path, content) => {
|
|
14769
14890
|
let existing = null;
|
|
14770
14891
|
try {
|
|
@@ -14983,7 +15104,10 @@ var buildRuntimeData = (project) => {
|
|
|
14983
15104
|
config: {
|
|
14984
15105
|
analytics: config.analytics ?? null,
|
|
14985
15106
|
appleIcon: resolveAppleIcon(project),
|
|
14986
|
-
ask: config.ai.ask?.enabled ? {
|
|
15107
|
+
ask: config.ai.ask?.enabled ? {
|
|
15108
|
+
endpoint: config.ai.ask.endpoint ?? null,
|
|
15109
|
+
suggestions: config.ai.ask.suggestions
|
|
15110
|
+
} : null,
|
|
14987
15111
|
banner: resolveBanner(config),
|
|
14988
15112
|
basePath: config.basePath,
|
|
14989
15113
|
codeThemes: config.markdown.codeBlocks.theme,
|
|
@@ -15115,7 +15239,7 @@ var writeMcpFiles = async (project, plan, write) => {
|
|
|
15115
15239
|
};
|
|
15116
15240
|
var writeAskFiles = async (project, srcDir, write) => {
|
|
15117
15241
|
const { ask } = project.config.ai;
|
|
15118
|
-
if (!ask?.enabled) {
|
|
15242
|
+
if (!(ask?.enabled && !ask.endpoint)) {
|
|
15119
15243
|
return;
|
|
15120
15244
|
}
|
|
15121
15245
|
const grounded = ask.provider !== "inkeep";
|
|
@@ -15168,6 +15292,7 @@ var generateRuntime = async (project) => {
|
|
|
15168
15292
|
usesMath,
|
|
15169
15293
|
userTheme,
|
|
15170
15294
|
userExamplesCss,
|
|
15295
|
+
integrationBridge,
|
|
15171
15296
|
islandDiscovery,
|
|
15172
15297
|
exampleDiscovery,
|
|
15173
15298
|
componentSlots
|
|
@@ -15177,6 +15302,7 @@ var generateRuntime = async (project) => {
|
|
|
15177
15302
|
detectUsesMath(context.root, staged.values()),
|
|
15178
15303
|
readOptional(context.themeFile),
|
|
15179
15304
|
readOptional(examplesCssFile(context.root, config)),
|
|
15305
|
+
loadIntegrationBridge(config, context),
|
|
15180
15306
|
discoverIslands(context.root),
|
|
15181
15307
|
discoverExamples(context.root, config.examples.source),
|
|
15182
15308
|
buildComponentSlots(context.componentsFile)
|
|
@@ -15214,6 +15340,7 @@ var generateRuntime = async (project) => {
|
|
|
15214
15340
|
dataPath,
|
|
15215
15341
|
examplesPath,
|
|
15216
15342
|
examplesThemePath,
|
|
15343
|
+
integrationBridge,
|
|
15217
15344
|
needsReact,
|
|
15218
15345
|
needsSvelte,
|
|
15219
15346
|
needsVue,
|
|
@@ -15439,7 +15566,7 @@ var checkRequiredSecrets = (config) => {
|
|
|
15439
15566
|
suggestion: `Set ${env} in .env.local for local dev, or in your host's environment for production.`
|
|
15440
15567
|
});
|
|
15441
15568
|
};
|
|
15442
|
-
if (config.ai.ask?.enabled) {
|
|
15569
|
+
if (config.ai.ask?.enabled && !config.ai.ask.endpoint) {
|
|
15443
15570
|
const backend = resolveAskBackend(config.ai.ask);
|
|
15444
15571
|
if (backend.kind === "gateway") {
|
|
15445
15572
|
requireSecret("Ask AI (AI Gateway)", "AI_GATEWAY_API_KEY", "on Vercel the gateway can also authenticate via OIDC");
|
|
@@ -16196,7 +16323,15 @@ var ejectOpenApiData = (project) => {
|
|
|
16196
16323
|
};
|
|
16197
16324
|
var askFiles = async (project, srcDir, genDir) => {
|
|
16198
16325
|
const { ask } = project.config.ai;
|
|
16199
|
-
if (!ask?.enabled) {
|
|
16326
|
+
if (!(ask?.enabled && !ask.endpoint)) {
|
|
16327
|
+
const endpointPath = join31(srcDir, "pages", "api", "ask.ts");
|
|
16328
|
+
if (existsSync18(endpointPath)) {
|
|
16329
|
+
const content = await readFile15(endpointPath, "utf-8");
|
|
16330
|
+
if (content.startsWith("// Generated by Blume. Do not edit.")) {
|
|
16331
|
+
await rm3(endpointPath, { force: true });
|
|
16332
|
+
}
|
|
16333
|
+
}
|
|
16334
|
+
await rm3(join31(genDir, "ask-data.json"), { force: true });
|
|
16200
16335
|
return [];
|
|
16201
16336
|
}
|
|
16202
16337
|
const grounded = ask.provider !== "inkeep";
|
|
@@ -16279,6 +16414,7 @@ var examplesPreviewFiles = (srcDir, basePath, hasExamples) => hasExamples ? [
|
|
|
16279
16414
|
path: join31(srcDir, "pages", ...basePath.split("/").filter(Boolean), "blume-examples", "[...path].astro")
|
|
16280
16415
|
}
|
|
16281
16416
|
] : [];
|
|
16417
|
+
var ejectIntegrationBridge = (config, root, configFile) => config.integrations.length > 0 && configFile ? { configFile: toPosix(relative14(root, configFile)) } : undefined;
|
|
16282
16418
|
var eject = async (root) => {
|
|
16283
16419
|
const project = await scanProject(root, { mode: "build" });
|
|
16284
16420
|
const { context, config } = project;
|
|
@@ -16340,6 +16476,7 @@ var eject = async (root) => {
|
|
|
16340
16476
|
dataPath: "./src/generated/data.json",
|
|
16341
16477
|
examplesPath: "./src/generated/examples.ts",
|
|
16342
16478
|
examplesThemePath: "./src/generated/examples.css",
|
|
16479
|
+
integrationBridge: ejectIntegrationBridge(config, root, context.configFile),
|
|
16343
16480
|
needsReact,
|
|
16344
16481
|
needsSvelte,
|
|
16345
16482
|
needsVue,
|
|
@@ -16890,9 +17027,878 @@ The blume package remains importable.`);
|
|
|
16890
17027
|
}
|
|
16891
17028
|
});
|
|
16892
17029
|
|
|
17030
|
+
// src/cli/commands/eval.ts
|
|
17031
|
+
import { existsSync as existsSync20 } from "node:fs";
|
|
17032
|
+
import { defineCommand as defineCommand8 } from "citty";
|
|
17033
|
+
import { join as join37 } from "pathe";
|
|
17034
|
+
|
|
17035
|
+
// src/eval/prompts.ts
|
|
17036
|
+
var readerPrompt = (question) => `You are evaluating whether a product's documentation can answer a user's question.
|
|
17037
|
+
|
|
17038
|
+
Answer the question below using ONLY the connected documentation tools (search_docs, get_page, list_pages, get_navigation). Rules:
|
|
17039
|
+
- Do not use prior knowledge about the product. Do not guess.
|
|
17040
|
+
- Do not read files, run commands, or access the network.
|
|
17041
|
+
- Search first, then read the most relevant pages with get_page.
|
|
17042
|
+
- If the documentation does not contain the answer, say exactly what information is missing instead of inventing one.
|
|
17043
|
+
|
|
17044
|
+
Question: ${question.question}
|
|
17045
|
+
|
|
17046
|
+
Reply with a concise answer containing the specific facts the documentation provides. Plain text only.`;
|
|
17047
|
+
var judgePrompt = (question, answer) => {
|
|
17048
|
+
const facts = question.expected.map((fact) => `- ${fact}`).join(`
|
|
17049
|
+
`);
|
|
17050
|
+
return `You are grading an answer against expected facts. Do not use any tools.
|
|
17051
|
+
|
|
17052
|
+
Question: ${question.question}
|
|
17053
|
+
|
|
17054
|
+
Expected facts — each must be present in substance (paraphrase is fine, contradiction is not):
|
|
17055
|
+
${facts}
|
|
17056
|
+
|
|
17057
|
+
Answer to grade:
|
|
17058
|
+
"""
|
|
17059
|
+
${answer}
|
|
17060
|
+
"""
|
|
17061
|
+
|
|
17062
|
+
An answer that states the documentation lacks the information FAILS.
|
|
17063
|
+
|
|
17064
|
+
Reply with ONLY this JSON object on a single line, no markdown fences:
|
|
17065
|
+
{"pass": true|false, "score": 0.0-1.0, "missing": ["expected facts absent or contradicted"], "notes": "one sentence"}`;
|
|
17066
|
+
};
|
|
17067
|
+
var evalFixPrompt = (reportPath) => `Fix the documentation gaps found by \`blume eval\` in this project.
|
|
17068
|
+
|
|
17069
|
+
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.
|
|
17070
|
+
|
|
17071
|
+
Work through every failed question:
|
|
17072
|
+
1. Read the page named in the finding (or choose the best page when none is named).
|
|
17073
|
+
2. Edit the documentation so it states the missing facts explicitly. Add prose, not filler; keep the page's voice.
|
|
17074
|
+
3. Never delete questions from the evals file or weaken expected facts.
|
|
17075
|
+
|
|
17076
|
+
When you are done, run \`blume eval\` to verify, and repeat until every question passes.`;
|
|
17077
|
+
var initPrompt = (evalsPath) => `Draft a starter evals file for \`blume eval\` in this documentation project.
|
|
17078
|
+
|
|
17079
|
+
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).
|
|
17080
|
+
|
|
17081
|
+
The file format is YAML:
|
|
17082
|
+
|
|
17083
|
+
questions:
|
|
17084
|
+
- id: kebab-case-slug
|
|
17085
|
+
question: One user question?
|
|
17086
|
+
expected:
|
|
17087
|
+
- a fact the answer must contain
|
|
17088
|
+
- another required fact
|
|
17089
|
+
routes:
|
|
17090
|
+
- /route/of/the/page/that/answers/it
|
|
17091
|
+
|
|
17092
|
+
Rules:
|
|
17093
|
+
- Every \`expected\` fact must be verifiable in the docs today.
|
|
17094
|
+
- Prefer questions whose answers live on one page; set \`routes\` to that page.
|
|
17095
|
+
- Keep ids unique and questions short.
|
|
17096
|
+
|
|
17097
|
+
When you are done, print the file and suggest running \`blume eval\` to try it.`;
|
|
17098
|
+
|
|
17099
|
+
// src/eval/report.ts
|
|
17100
|
+
import { mkdtemp as mkdtemp2, writeFile as writeFile12 } from "node:fs/promises";
|
|
17101
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
17102
|
+
import { join as join34, relative as relative17 } from "pathe";
|
|
17103
|
+
var ESC4 = String.fromCodePoint(27);
|
|
17104
|
+
var COLORS3 = {
|
|
17105
|
+
bold: `${ESC4}[1m`,
|
|
17106
|
+
cyan: `${ESC4}[36m`,
|
|
17107
|
+
dim: `${ESC4}[2m`,
|
|
17108
|
+
green: `${ESC4}[32m`,
|
|
17109
|
+
red: `${ESC4}[31m`,
|
|
17110
|
+
reset: `${ESC4}[0m`,
|
|
17111
|
+
yellow: `${ESC4}[33m`
|
|
17112
|
+
};
|
|
17113
|
+
var GLYPH2 = {
|
|
17114
|
+
error: "!",
|
|
17115
|
+
fail: "✖",
|
|
17116
|
+
pass: "✔",
|
|
17117
|
+
skip: "⊘"
|
|
17118
|
+
};
|
|
17119
|
+
var STATUS_COLOR = {
|
|
17120
|
+
error: COLORS3.yellow,
|
|
17121
|
+
fail: COLORS3.red,
|
|
17122
|
+
pass: COLORS3.green,
|
|
17123
|
+
skip: COLORS3.dim
|
|
17124
|
+
};
|
|
17125
|
+
var ID_PAD = 28;
|
|
17126
|
+
var seconds = (ms) => `${(ms / 1000).toFixed(1)}s`;
|
|
17127
|
+
var money = (cost) => cost === undefined ? "" : `$${cost.toFixed(2)}`;
|
|
17128
|
+
var duration = (ms) => {
|
|
17129
|
+
if (ms < 60000) {
|
|
17130
|
+
return seconds(ms);
|
|
17131
|
+
}
|
|
17132
|
+
const minutes = Math.floor(ms / 60000);
|
|
17133
|
+
const rest = Math.round(ms % 60000 / 1000);
|
|
17134
|
+
return `${minutes}m ${rest}s`;
|
|
17135
|
+
};
|
|
17136
|
+
var questionLine = (result) => {
|
|
17137
|
+
const color = STATUS_COLOR[result.status];
|
|
17138
|
+
const glyph = `${color}${GLYPH2[result.status]}${COLORS3.reset}`;
|
|
17139
|
+
const id2 = result.id.padEnd(ID_PAD);
|
|
17140
|
+
if (result.status === "skip") {
|
|
17141
|
+
return ` ${glyph} ${id2} ${COLORS3.dim}skipped${COLORS3.reset}`;
|
|
17142
|
+
}
|
|
17143
|
+
const score = result.score === undefined ? "" : result.score.toFixed(2);
|
|
17144
|
+
const cells = [
|
|
17145
|
+
`${color}${result.status}${COLORS3.reset}`,
|
|
17146
|
+
score,
|
|
17147
|
+
`${COLORS3.dim}${seconds(result.durationMs)}${COLORS3.reset}`,
|
|
17148
|
+
`${COLORS3.dim}${money(result.costUsd)}${COLORS3.reset}`
|
|
17149
|
+
].filter((cell) => cell !== "").join(" ");
|
|
17150
|
+
return ` ${glyph} ${id2} ${cells}`;
|
|
17151
|
+
};
|
|
17152
|
+
var questionDetails = (result, verbose) => {
|
|
17153
|
+
const lines = [];
|
|
17154
|
+
if (result.status === "fail") {
|
|
17155
|
+
for (const fact of result.missing) {
|
|
17156
|
+
lines.push(` ${COLORS3.dim}missing: ${fact}${COLORS3.reset}`);
|
|
17157
|
+
}
|
|
17158
|
+
}
|
|
17159
|
+
if (result.status === "error" && result.detail) {
|
|
17160
|
+
lines.push(` ${COLORS3.dim}${result.detail}${COLORS3.reset}`);
|
|
17161
|
+
}
|
|
17162
|
+
if (verbose && result.answer && result.status !== "pass") {
|
|
17163
|
+
lines.push(...result.answer.split(`
|
|
17164
|
+
`).map((line) => ` ${COLORS3.dim}> ${line}${COLORS3.reset}`));
|
|
17165
|
+
}
|
|
17166
|
+
return lines;
|
|
17167
|
+
};
|
|
17168
|
+
var summaryLine2 = (result) => {
|
|
17169
|
+
const { counts } = result;
|
|
17170
|
+
const parts = [
|
|
17171
|
+
`${counts.pass} passed`,
|
|
17172
|
+
counts.fail > 0 ? `${counts.fail} failed` : "",
|
|
17173
|
+
counts.error > 0 ? `${counts.error} errored` : "",
|
|
17174
|
+
counts.skip > 0 ? `${counts.skip} skipped` : "",
|
|
17175
|
+
duration(result.durationMs),
|
|
17176
|
+
money(result.costUsd)
|
|
17177
|
+
].filter((part) => part !== "");
|
|
17178
|
+
return parts.join(" · ");
|
|
17179
|
+
};
|
|
17180
|
+
var headerLine = (total, agent) => `${COLORS3.bold}blume eval${COLORS3.reset} ${total} question(s) · ${AGENTS[agent].name}`;
|
|
17181
|
+
var startLine = (id2, index, total) => ` ${COLORS3.dim}▸ ${id2} (${index + 1}/${total})${COLORS3.reset}`;
|
|
17182
|
+
var fixLines = (result, root) => result.diagnostics.filter((diagnostic) => diagnostic.code !== "BLUME_EVAL_ROUTE_UNKNOWN").map((finding2) => {
|
|
17183
|
+
const site = finding2.file ? `${relative17(root, finding2.file)}${finding2.line ? `:${finding2.line}` : ""}` : "";
|
|
17184
|
+
return ` ${COLORS3.cyan}fix:${COLORS3.reset} ${site} ${COLORS3.dim}${finding2.message}${COLORS3.reset}`;
|
|
17185
|
+
});
|
|
17186
|
+
var warningLines = (result, root) => result.diagnostics.filter((diagnostic) => diagnostic.code === "BLUME_EVAL_ROUTE_UNKNOWN").map((finding2) => {
|
|
17187
|
+
const site = finding2.file ? ` ${relative17(root, finding2.file)}${finding2.line ? `:${finding2.line}` : ""}` : "";
|
|
17188
|
+
return ` ${COLORS3.yellow}⚠${COLORS3.reset}${site} ${COLORS3.dim}${finding2.message}${COLORS3.reset}`;
|
|
17189
|
+
});
|
|
17190
|
+
var evalReportJson = (result, root, threshold) => {
|
|
17191
|
+
const diagnostics = result.diagnostics.map((diagnostic) => diagnostic.file ? { ...diagnostic, file: relative17(root, diagnostic.file) } : diagnostic);
|
|
17192
|
+
return `${JSON.stringify({
|
|
17193
|
+
diagnostics,
|
|
17194
|
+
eval: {
|
|
17195
|
+
agent: result.agent,
|
|
17196
|
+
costUsd: result.costUsd,
|
|
17197
|
+
counts: result.counts,
|
|
17198
|
+
durationMs: result.durationMs,
|
|
17199
|
+
results: result.results,
|
|
17200
|
+
threshold
|
|
17201
|
+
},
|
|
17202
|
+
summary: countBySeverity(result.diagnostics)
|
|
17203
|
+
}, null, 2)}
|
|
17204
|
+
`;
|
|
17205
|
+
};
|
|
17206
|
+
var writeEvalReport = async (result, root, threshold) => {
|
|
17207
|
+
const dir = await mkdtemp2(join34(tmpdir2(), "blume-eval-"));
|
|
17208
|
+
const path = join34(dir, "report.json");
|
|
17209
|
+
await writeFile12(path, evalReportJson(result, root, threshold));
|
|
17210
|
+
return path;
|
|
17211
|
+
};
|
|
17212
|
+
|
|
17213
|
+
// src/eval/run.ts
|
|
17214
|
+
import { mkdir as mkdir9, mkdtemp as mkdtemp3, writeFile as writeFile14 } from "node:fs/promises";
|
|
17215
|
+
import { tmpdir as tmpdir3 } from "node:os";
|
|
17216
|
+
import { join as join36 } from "pathe";
|
|
17217
|
+
|
|
17218
|
+
// src/eval/agents.ts
|
|
17219
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
17220
|
+
import { readFile as readFile17, writeFile as writeFile13 } from "node:fs/promises";
|
|
17221
|
+
import { join as join35 } from "pathe";
|
|
17222
|
+
import { z as z3 } from "zod";
|
|
17223
|
+
var KILL_GRACE_MS = 5000;
|
|
17224
|
+
var MCP_TOOL_NAMES = [
|
|
17225
|
+
"search_docs",
|
|
17226
|
+
"get_page",
|
|
17227
|
+
"list_pages",
|
|
17228
|
+
"get_navigation"
|
|
17229
|
+
];
|
|
17230
|
+
var MCP_SERVER_NAME = "docs";
|
|
17231
|
+
var DISALLOWED_TOOLS = [
|
|
17232
|
+
"Bash",
|
|
17233
|
+
"Read",
|
|
17234
|
+
"Glob",
|
|
17235
|
+
"Grep",
|
|
17236
|
+
"Write",
|
|
17237
|
+
"Edit",
|
|
17238
|
+
"NotebookEdit",
|
|
17239
|
+
"WebFetch",
|
|
17240
|
+
"WebSearch",
|
|
17241
|
+
"Task"
|
|
17242
|
+
];
|
|
17243
|
+
var runAgentHeadless = (bin, args, options) => new Promise((resolve9, reject) => {
|
|
17244
|
+
const platform = options.platform ?? process.platform;
|
|
17245
|
+
const child = spawn2(bin, args, {
|
|
17246
|
+
cwd: options.cwd,
|
|
17247
|
+
shell: platform === "win32",
|
|
17248
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
17249
|
+
});
|
|
17250
|
+
let stdout = "";
|
|
17251
|
+
let stderr = "";
|
|
17252
|
+
let timedOut = false;
|
|
17253
|
+
child.stdout.on("data", (chunk) => {
|
|
17254
|
+
stdout += chunk.toString("utf-8");
|
|
17255
|
+
});
|
|
17256
|
+
child.stderr.on("data", (chunk) => {
|
|
17257
|
+
stderr += chunk.toString("utf-8");
|
|
17258
|
+
});
|
|
17259
|
+
const deadline = setTimeout(() => {
|
|
17260
|
+
timedOut = true;
|
|
17261
|
+
child.kill("SIGTERM");
|
|
17262
|
+
const hardKill = setTimeout(() => child.kill("SIGKILL"), KILL_GRACE_MS);
|
|
17263
|
+
hardKill.unref();
|
|
17264
|
+
}, options.timeoutMs);
|
|
17265
|
+
deadline.unref();
|
|
17266
|
+
child.once("error", (error) => {
|
|
17267
|
+
clearTimeout(deadline);
|
|
17268
|
+
reject(error);
|
|
17269
|
+
});
|
|
17270
|
+
child.once("close", (code) => {
|
|
17271
|
+
clearTimeout(deadline);
|
|
17272
|
+
resolve9({ code: code ?? 1, stderr, stdout, timedOut });
|
|
17273
|
+
});
|
|
17274
|
+
child.once("exit", (code) => {
|
|
17275
|
+
if (timedOut) {
|
|
17276
|
+
clearTimeout(deadline);
|
|
17277
|
+
resolve9({ code: code ?? 1, stderr, stdout, timedOut });
|
|
17278
|
+
}
|
|
17279
|
+
});
|
|
17280
|
+
child.stdin.end(options.prompt);
|
|
17281
|
+
});
|
|
17282
|
+
var writeMcpConfig = async (dir, snapshotPath, launcher) => {
|
|
17283
|
+
const resolved = launcher ?? {
|
|
17284
|
+
args: [process.argv[1] ?? "", "mcp-stdio", "--data", snapshotPath],
|
|
17285
|
+
command: process.execPath
|
|
17286
|
+
};
|
|
17287
|
+
const configPath = join35(dir, "mcp-config.json");
|
|
17288
|
+
const config = {
|
|
17289
|
+
mcpServers: {
|
|
17290
|
+
[MCP_SERVER_NAME]: { args: resolved.args, command: resolved.command }
|
|
17291
|
+
}
|
|
17292
|
+
};
|
|
17293
|
+
await writeFile13(configPath, JSON.stringify(config, null, 2));
|
|
17294
|
+
return {
|
|
17295
|
+
configPath,
|
|
17296
|
+
serverArgs: resolved.args,
|
|
17297
|
+
serverCommand: resolved.command
|
|
17298
|
+
};
|
|
17299
|
+
};
|
|
17300
|
+
var CLAUDE_READER_MAX_TURNS = "25";
|
|
17301
|
+
var CLAUDE_JUDGE_MAX_TURNS = "1";
|
|
17302
|
+
var claudeArgs = (context) => {
|
|
17303
|
+
const base = ["-p", "--output-format", "json", "--strict-mcp-config"];
|
|
17304
|
+
if (context.mcp) {
|
|
17305
|
+
const allowed = MCP_TOOL_NAMES.map((tool) => `mcp__${MCP_SERVER_NAME}__${tool}`).join(",");
|
|
17306
|
+
return [
|
|
17307
|
+
...base,
|
|
17308
|
+
"--mcp-config",
|
|
17309
|
+
context.mcp.configPath,
|
|
17310
|
+
"--allowedTools",
|
|
17311
|
+
allowed,
|
|
17312
|
+
"--disallowedTools",
|
|
17313
|
+
DISALLOWED_TOOLS.join(","),
|
|
17314
|
+
"--max-turns",
|
|
17315
|
+
CLAUDE_READER_MAX_TURNS
|
|
17316
|
+
];
|
|
17317
|
+
}
|
|
17318
|
+
return [
|
|
17319
|
+
...base,
|
|
17320
|
+
"--disallowedTools",
|
|
17321
|
+
DISALLOWED_TOOLS.join(","),
|
|
17322
|
+
"--max-turns",
|
|
17323
|
+
CLAUDE_JUDGE_MAX_TURNS
|
|
17324
|
+
];
|
|
17325
|
+
};
|
|
17326
|
+
var codexArgs = (context) => {
|
|
17327
|
+
const base = [
|
|
17328
|
+
"exec",
|
|
17329
|
+
"--skip-git-repo-check",
|
|
17330
|
+
"--ignore-user-config",
|
|
17331
|
+
"--ephemeral",
|
|
17332
|
+
"--sandbox",
|
|
17333
|
+
"read-only",
|
|
17334
|
+
"--output-last-message",
|
|
17335
|
+
context.lastMessagePath
|
|
17336
|
+
];
|
|
17337
|
+
if (context.mcp) {
|
|
17338
|
+
return [
|
|
17339
|
+
...base,
|
|
17340
|
+
"-c",
|
|
17341
|
+
`mcp_servers.${MCP_SERVER_NAME}.command=${JSON.stringify(context.mcp.serverCommand)}`,
|
|
17342
|
+
"-c",
|
|
17343
|
+
`mcp_servers.${MCP_SERVER_NAME}.args=${JSON.stringify(context.mcp.serverArgs)}`,
|
|
17344
|
+
"-"
|
|
17345
|
+
];
|
|
17346
|
+
}
|
|
17347
|
+
return [...base, "-"];
|
|
17348
|
+
};
|
|
17349
|
+
var agentArgs = (kind, context) => kind === "claude" ? claudeArgs(context) : codexArgs(context);
|
|
17350
|
+
var claudeResultSchema = z3.object({
|
|
17351
|
+
is_error: z3.boolean().default(false),
|
|
17352
|
+
result: z3.string().default(""),
|
|
17353
|
+
total_cost_usd: z3.number().optional()
|
|
17354
|
+
});
|
|
17355
|
+
var tail = (value, max = 300) => {
|
|
17356
|
+
const trimmed = value.trim();
|
|
17357
|
+
return trimmed.length > max ? trimmed.slice(-max) : trimmed;
|
|
17358
|
+
};
|
|
17359
|
+
var readAgentOutput = async (kind, result, lastMessagePath) => {
|
|
17360
|
+
if (result.timedOut) {
|
|
17361
|
+
return { detail: "timed out", isError: true, text: "" };
|
|
17362
|
+
}
|
|
17363
|
+
if (result.code !== 0) {
|
|
17364
|
+
return {
|
|
17365
|
+
detail: tail(result.stderr) || `exited with code ${result.code}`,
|
|
17366
|
+
isError: true,
|
|
17367
|
+
text: ""
|
|
17368
|
+
};
|
|
17369
|
+
}
|
|
17370
|
+
if (kind === "claude") {
|
|
17371
|
+
let parsed;
|
|
17372
|
+
try {
|
|
17373
|
+
parsed = JSON.parse(result.stdout);
|
|
17374
|
+
} catch {
|
|
17375
|
+
return {
|
|
17376
|
+
detail: "unparseable --output-format json payload",
|
|
17377
|
+
isError: true,
|
|
17378
|
+
text: ""
|
|
17379
|
+
};
|
|
17380
|
+
}
|
|
17381
|
+
const payload = claudeResultSchema.safeParse(parsed);
|
|
17382
|
+
if (!payload.success) {
|
|
17383
|
+
return {
|
|
17384
|
+
detail: "unexpected --output-format json shape",
|
|
17385
|
+
isError: true,
|
|
17386
|
+
text: ""
|
|
17387
|
+
};
|
|
17388
|
+
}
|
|
17389
|
+
return {
|
|
17390
|
+
costUsd: payload.data.total_cost_usd,
|
|
17391
|
+
isError: payload.data.is_error,
|
|
17392
|
+
text: payload.data.result
|
|
17393
|
+
};
|
|
17394
|
+
}
|
|
17395
|
+
let text;
|
|
17396
|
+
try {
|
|
17397
|
+
const raw = await readFile17(lastMessagePath, "utf-8");
|
|
17398
|
+
text = raw.trim();
|
|
17399
|
+
} catch {
|
|
17400
|
+
return { detail: "no last message written", isError: true, text: "" };
|
|
17401
|
+
}
|
|
17402
|
+
if (text === "") {
|
|
17403
|
+
return { detail: "empty last message", isError: true, text: "" };
|
|
17404
|
+
}
|
|
17405
|
+
return { isError: false, text };
|
|
17406
|
+
};
|
|
17407
|
+
var verdictSchema = z3.object({
|
|
17408
|
+
missing: z3.array(z3.string()).default([]),
|
|
17409
|
+
notes: z3.string().default(""),
|
|
17410
|
+
pass: z3.boolean(),
|
|
17411
|
+
score: z3.number().min(0).max(1).optional()
|
|
17412
|
+
});
|
|
17413
|
+
var parseVerdict = (text) => {
|
|
17414
|
+
const start = text.indexOf("{");
|
|
17415
|
+
const end = text.lastIndexOf("}");
|
|
17416
|
+
if (start === -1 || end <= start) {
|
|
17417
|
+
return;
|
|
17418
|
+
}
|
|
17419
|
+
let parsed;
|
|
17420
|
+
try {
|
|
17421
|
+
parsed = JSON.parse(text.slice(start, end + 1));
|
|
17422
|
+
} catch {
|
|
17423
|
+
return;
|
|
17424
|
+
}
|
|
17425
|
+
const result = verdictSchema.safeParse(parsed);
|
|
17426
|
+
return result.success ? result.data : undefined;
|
|
17427
|
+
};
|
|
17428
|
+
|
|
17429
|
+
// src/eval/schema.ts
|
|
17430
|
+
import { readFile as readFile18 } from "node:fs/promises";
|
|
17431
|
+
import { load as load2 } from "js-yaml";
|
|
17432
|
+
import { z as z4 } from "zod";
|
|
17433
|
+
var ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/u;
|
|
17434
|
+
var questionSchema = z4.strictObject({
|
|
17435
|
+
expected: z4.array(z4.string().min(1)).min(1, "expected must list at least one fact"),
|
|
17436
|
+
id: z4.string().regex(ID_PATTERN, "id must be a kebab-case slug (a-z, 0-9, dashes)"),
|
|
17437
|
+
question: z4.string().min(1),
|
|
17438
|
+
routes: z4.union([z4.string(), z4.array(z4.string())]).default([]).transform((value) => typeof value === "string" ? [value] : value),
|
|
17439
|
+
severity: z4.enum(["error", "warning"]).default("error"),
|
|
17440
|
+
skip: z4.boolean().default(false)
|
|
17441
|
+
});
|
|
17442
|
+
var fullSchema = z4.strictObject({
|
|
17443
|
+
questions: z4.array(questionSchema).min(1),
|
|
17444
|
+
version: z4.literal(1).default(1)
|
|
17445
|
+
});
|
|
17446
|
+
var evalsFileSchema = fullSchema.superRefine((value, context) => {
|
|
17447
|
+
const seen = new Set;
|
|
17448
|
+
for (const question of value.questions) {
|
|
17449
|
+
if (seen.has(question.id)) {
|
|
17450
|
+
context.addIssue({
|
|
17451
|
+
code: z4.ZodIssueCode.custom,
|
|
17452
|
+
message: `duplicate question id "${question.id}"`,
|
|
17453
|
+
path: ["questions"]
|
|
17454
|
+
});
|
|
17455
|
+
}
|
|
17456
|
+
seen.add(question.id);
|
|
17457
|
+
}
|
|
17458
|
+
});
|
|
17459
|
+
|
|
17460
|
+
class EvalsFileError extends Error {
|
|
17461
|
+
path;
|
|
17462
|
+
constructor(path, message) {
|
|
17463
|
+
super(message);
|
|
17464
|
+
this.name = "EvalsFileError";
|
|
17465
|
+
this.path = path;
|
|
17466
|
+
}
|
|
17467
|
+
}
|
|
17468
|
+
var describeIssues = (error) => error.issues.map((issue) => {
|
|
17469
|
+
const at = issue.path.length > 0 ? ` at ${issue.path.join(".")}` : "";
|
|
17470
|
+
return `${issue.message}${at}`;
|
|
17471
|
+
}).join("; ");
|
|
17472
|
+
var loadEvalsFile = async (path) => {
|
|
17473
|
+
let raw;
|
|
17474
|
+
try {
|
|
17475
|
+
raw = await readFile18(path, "utf-8");
|
|
17476
|
+
} catch {
|
|
17477
|
+
throw new EvalsFileError(path, `No evals file found at ${path}. Run \`blume eval init\` to draft one.`);
|
|
17478
|
+
}
|
|
17479
|
+
let parsed;
|
|
17480
|
+
try {
|
|
17481
|
+
parsed = load2(raw);
|
|
17482
|
+
} catch (error) {
|
|
17483
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
17484
|
+
throw new EvalsFileError(path, `Invalid YAML in ${path}: ${detail}`);
|
|
17485
|
+
}
|
|
17486
|
+
const candidate = Array.isArray(parsed) ? { questions: parsed } : parsed;
|
|
17487
|
+
const result = evalsFileSchema.safeParse(candidate);
|
|
17488
|
+
if (!result.success) {
|
|
17489
|
+
throw new EvalsFileError(path, `Invalid evals file at ${path}: ${describeIssues(result.error)}`);
|
|
17490
|
+
}
|
|
17491
|
+
return { evals: result.data, raw };
|
|
17492
|
+
};
|
|
17493
|
+
var locateQuestion = (raw, id2) => {
|
|
17494
|
+
const pattern = new RegExp(`^\\s*-?\\s*id:\\s*["']?${id2}["']?\\s*$`, "u");
|
|
17495
|
+
const lines = raw.split(`
|
|
17496
|
+
`);
|
|
17497
|
+
for (const [index, line] of lines.entries()) {
|
|
17498
|
+
if (pattern.test(line)) {
|
|
17499
|
+
return index + 1;
|
|
17500
|
+
}
|
|
17501
|
+
}
|
|
17502
|
+
return;
|
|
17503
|
+
};
|
|
17504
|
+
|
|
17505
|
+
// src/eval/findings.ts
|
|
17506
|
+
var DOCS_URL = "https://useblume.dev/docs/reference/eval";
|
|
17507
|
+
var hintedRoute = (question, project) => {
|
|
17508
|
+
for (const hint of question.routes) {
|
|
17509
|
+
const route = project.manifest.routes.find((candidate) => candidate.path === hint);
|
|
17510
|
+
if (route) {
|
|
17511
|
+
return route;
|
|
17512
|
+
}
|
|
17513
|
+
}
|
|
17514
|
+
};
|
|
17515
|
+
var routeFindings = (question, project, anchor) => {
|
|
17516
|
+
const known = new Set(project.manifest.routes.map((route) => route.path));
|
|
17517
|
+
return question.routes.filter((hint) => !known.has(hint)).map((hint) => ({
|
|
17518
|
+
code: "BLUME_EVAL_ROUTE_UNKNOWN",
|
|
17519
|
+
docsUrl: DOCS_URL,
|
|
17520
|
+
file: anchor.path,
|
|
17521
|
+
line: locateQuestion(anchor.raw, question.id),
|
|
17522
|
+
message: `Question "${question.id}" hints at route "${hint}", which matches no page.`,
|
|
17523
|
+
severity: "warning",
|
|
17524
|
+
suggestion: "Update the question's `routes` to the page's current route, or remove the hint."
|
|
17525
|
+
}));
|
|
17526
|
+
};
|
|
17527
|
+
var questionFinding = (question, outcome, project, anchor) => {
|
|
17528
|
+
const route = hintedRoute(question, project);
|
|
17529
|
+
const site = route ? { file: route.sourcePath, url: route.path } : { file: anchor.path, line: locateQuestion(anchor.raw, question.id) };
|
|
17530
|
+
if (outcome.status === "error") {
|
|
17531
|
+
return {
|
|
17532
|
+
code: "BLUME_EVAL_QUESTION_ERROR",
|
|
17533
|
+
docsUrl: DOCS_URL,
|
|
17534
|
+
message: `Eval run failed for "${question.question}"${outcome.detail ? ` — ${outcome.detail}` : ""}.`,
|
|
17535
|
+
severity: question.severity,
|
|
17536
|
+
suggestion: "Rerun `blume eval`; if it persists, check the agent CLI installation and the failure detail.",
|
|
17537
|
+
...site
|
|
17538
|
+
};
|
|
17539
|
+
}
|
|
17540
|
+
const missing = outcome.missing.length > 0 ? ` — missing: ${outcome.missing.join("; ")}` : "";
|
|
17541
|
+
return {
|
|
17542
|
+
code: "BLUME_EVAL_QUESTION_FAILED",
|
|
17543
|
+
docsUrl: DOCS_URL,
|
|
17544
|
+
message: `Docs could not answer: "${question.question}"${missing}`,
|
|
17545
|
+
severity: question.severity,
|
|
17546
|
+
suggestion: "State the missing facts on this page, then rerun `blume eval`.",
|
|
17547
|
+
...site
|
|
17548
|
+
};
|
|
17549
|
+
};
|
|
17550
|
+
|
|
17551
|
+
// src/eval/run.ts
|
|
17552
|
+
var DEFAULT_READER_TIMEOUT_MS = 180000;
|
|
17553
|
+
var DEFAULT_JUDGE_TIMEOUT_MS = 60000;
|
|
17554
|
+
var errored = (question, detail, durationMs) => ({
|
|
17555
|
+
detail,
|
|
17556
|
+
durationMs,
|
|
17557
|
+
expected: question.expected,
|
|
17558
|
+
id: question.id,
|
|
17559
|
+
missing: [],
|
|
17560
|
+
question: question.question,
|
|
17561
|
+
routes: question.routes,
|
|
17562
|
+
status: "error"
|
|
17563
|
+
});
|
|
17564
|
+
var runQuestion = async (question, index, context) => {
|
|
17565
|
+
const started = performance.now();
|
|
17566
|
+
const elapsed = () => Math.round(performance.now() - started);
|
|
17567
|
+
const workDir = join36(context.dir, `work-${index}`);
|
|
17568
|
+
await mkdir9(workDir, { recursive: true });
|
|
17569
|
+
const answerPath = join36(workDir, "answer.txt");
|
|
17570
|
+
const reader = await context.run(context.bin, agentArgs(context.kind, { lastMessagePath: answerPath, mcp: context.mcp }), {
|
|
17571
|
+
cwd: workDir,
|
|
17572
|
+
prompt: readerPrompt(question),
|
|
17573
|
+
timeoutMs: context.readerTimeoutMs
|
|
17574
|
+
});
|
|
17575
|
+
const answer = await readAgentOutput(context.kind, reader, answerPath);
|
|
17576
|
+
if (answer.isError) {
|
|
17577
|
+
return {
|
|
17578
|
+
...errored(question, `reader ${answer.detail ?? "failed"}`, elapsed()),
|
|
17579
|
+
costUsd: answer.costUsd
|
|
17580
|
+
};
|
|
17581
|
+
}
|
|
17582
|
+
const verdictPath = join36(workDir, "verdict.txt");
|
|
17583
|
+
const judge = await context.run(context.bin, agentArgs(context.kind, { lastMessagePath: verdictPath }), {
|
|
17584
|
+
cwd: workDir,
|
|
17585
|
+
prompt: judgePrompt(question, answer.text),
|
|
17586
|
+
timeoutMs: context.judgeTimeoutMs
|
|
17587
|
+
});
|
|
17588
|
+
const graded = await readAgentOutput(context.kind, judge, verdictPath);
|
|
17589
|
+
const costUsd = answer.costUsd === undefined && graded.costUsd === undefined ? undefined : (answer.costUsd ?? 0) + (graded.costUsd ?? 0);
|
|
17590
|
+
if (graded.isError) {
|
|
17591
|
+
return {
|
|
17592
|
+
...errored(question, `judge ${graded.detail ?? "failed"}`, elapsed()),
|
|
17593
|
+
answer: answer.text,
|
|
17594
|
+
costUsd
|
|
17595
|
+
};
|
|
17596
|
+
}
|
|
17597
|
+
const verdict = parseVerdict(graded.text);
|
|
17598
|
+
if (!verdict) {
|
|
17599
|
+
return {
|
|
17600
|
+
...errored(question, "judge returned no parseable verdict", elapsed()),
|
|
17601
|
+
answer: answer.text,
|
|
17602
|
+
costUsd
|
|
17603
|
+
};
|
|
17604
|
+
}
|
|
17605
|
+
return {
|
|
17606
|
+
answer: answer.text,
|
|
17607
|
+
costUsd,
|
|
17608
|
+
durationMs: elapsed(),
|
|
17609
|
+
expected: question.expected,
|
|
17610
|
+
id: question.id,
|
|
17611
|
+
missing: verdict.missing,
|
|
17612
|
+
notes: verdict.notes || undefined,
|
|
17613
|
+
question: question.question,
|
|
17614
|
+
routes: question.routes,
|
|
17615
|
+
score: verdict.score,
|
|
17616
|
+
status: verdict.pass ? "pass" : "fail"
|
|
17617
|
+
};
|
|
17618
|
+
};
|
|
17619
|
+
var runEval = async (options) => {
|
|
17620
|
+
const started = performance.now();
|
|
17621
|
+
const kind = options.agent;
|
|
17622
|
+
const run = options.run ?? runAgentHeadless;
|
|
17623
|
+
const anchor = { path: options.evalsPath, raw: options.rawEvals };
|
|
17624
|
+
const dir = await mkdtemp3(join36(tmpdir3(), "blume-eval-"));
|
|
17625
|
+
const snapshotPath = join36(dir, "mcp-data.json");
|
|
17626
|
+
await writeFile14(snapshotPath, JSON.stringify(await buildMcpData(options.project)));
|
|
17627
|
+
const mcp = await writeMcpConfig(dir, snapshotPath);
|
|
17628
|
+
const context = {
|
|
17629
|
+
bin: AGENTS[kind].bin,
|
|
17630
|
+
dir,
|
|
17631
|
+
judgeTimeoutMs: options.judgeTimeoutMs ?? DEFAULT_JUDGE_TIMEOUT_MS,
|
|
17632
|
+
kind,
|
|
17633
|
+
mcp,
|
|
17634
|
+
readerTimeoutMs: options.readerTimeoutMs ?? DEFAULT_READER_TIMEOUT_MS,
|
|
17635
|
+
run
|
|
17636
|
+
};
|
|
17637
|
+
const diagnostics = [];
|
|
17638
|
+
const results = [];
|
|
17639
|
+
const { questions } = options.evals;
|
|
17640
|
+
for (const [index, question] of questions.entries()) {
|
|
17641
|
+
diagnostics.push(...routeFindings(question, options.project, anchor));
|
|
17642
|
+
if (question.skip) {
|
|
17643
|
+
results.push({
|
|
17644
|
+
durationMs: 0,
|
|
17645
|
+
expected: question.expected,
|
|
17646
|
+
id: question.id,
|
|
17647
|
+
missing: [],
|
|
17648
|
+
question: question.question,
|
|
17649
|
+
routes: question.routes,
|
|
17650
|
+
status: "skip"
|
|
17651
|
+
});
|
|
17652
|
+
continue;
|
|
17653
|
+
}
|
|
17654
|
+
options.onProgress?.({
|
|
17655
|
+
id: question.id,
|
|
17656
|
+
index,
|
|
17657
|
+
kind: "question-start",
|
|
17658
|
+
total: questions.length
|
|
17659
|
+
});
|
|
17660
|
+
const result = await runQuestion(question, index, context);
|
|
17661
|
+
results.push(result);
|
|
17662
|
+
if (result.status === "fail" || result.status === "error") {
|
|
17663
|
+
diagnostics.push(questionFinding(question, {
|
|
17664
|
+
detail: result.detail,
|
|
17665
|
+
missing: result.missing,
|
|
17666
|
+
status: result.status
|
|
17667
|
+
}, options.project, anchor));
|
|
17668
|
+
}
|
|
17669
|
+
options.onProgress?.({
|
|
17670
|
+
index,
|
|
17671
|
+
kind: "question-end",
|
|
17672
|
+
result,
|
|
17673
|
+
total: questions.length
|
|
17674
|
+
});
|
|
17675
|
+
}
|
|
17676
|
+
const counts = {
|
|
17677
|
+
error: 0,
|
|
17678
|
+
fail: 0,
|
|
17679
|
+
pass: 0,
|
|
17680
|
+
skip: 0
|
|
17681
|
+
};
|
|
17682
|
+
for (const result of results) {
|
|
17683
|
+
counts[result.status] += 1;
|
|
17684
|
+
}
|
|
17685
|
+
const costs = results.flatMap((result) => result.costUsd === undefined ? [] : [result.costUsd]);
|
|
17686
|
+
return {
|
|
17687
|
+
agent: kind,
|
|
17688
|
+
costUsd: costs.length > 0 ? costs.reduce((total, cost) => total + cost, 0) : undefined,
|
|
17689
|
+
counts,
|
|
17690
|
+
diagnostics,
|
|
17691
|
+
durationMs: Math.round(performance.now() - started),
|
|
17692
|
+
results
|
|
17693
|
+
};
|
|
17694
|
+
};
|
|
17695
|
+
|
|
17696
|
+
// src/cli/commands/eval.ts
|
|
17697
|
+
var DEFAULT_FILE = "evals.yaml";
|
|
17698
|
+
var DEFAULT_TIMEOUT_S = 180;
|
|
17699
|
+
var isAgentKind = (value) => (value in AGENTS);
|
|
17700
|
+
var launchAgentCode2 = async (bin, prompt) => {
|
|
17701
|
+
try {
|
|
17702
|
+
return await launchAgent(bin, prompt);
|
|
17703
|
+
} catch (error) {
|
|
17704
|
+
if (error?.code !== "ENOENT") {
|
|
17705
|
+
throw error;
|
|
17706
|
+
}
|
|
17707
|
+
return WINDOWS_COMMAND_NOT_FOUND;
|
|
17708
|
+
}
|
|
17709
|
+
};
|
|
17710
|
+
var notInstalled = (agent) => {
|
|
17711
|
+
const cli = AGENTS[agent];
|
|
17712
|
+
logger.error(`${cli.name} (\`${cli.bin}\`) was not found on PATH. Install it with \`${cli.install}\`.`);
|
|
17713
|
+
return process.exit(1);
|
|
17714
|
+
};
|
|
17715
|
+
var passFraction = (result) => {
|
|
17716
|
+
const ran = result.results.length - result.counts.skip;
|
|
17717
|
+
return ran === 0 ? 1 : result.counts.pass / ran;
|
|
17718
|
+
};
|
|
17719
|
+
var parseFlags = (args) => {
|
|
17720
|
+
if (!isAgentKind(args.agent)) {
|
|
17721
|
+
logger.error(`Invalid --agent "${args.agent}" (use claude | codex).`);
|
|
17722
|
+
process.exit(1);
|
|
17723
|
+
}
|
|
17724
|
+
if (args.action !== undefined && args.action !== "init") {
|
|
17725
|
+
logger.error(`Unknown action "${args.action}" (did you mean "init"?).`);
|
|
17726
|
+
process.exit(1);
|
|
17727
|
+
}
|
|
17728
|
+
if (args.json && args.fix) {
|
|
17729
|
+
logger.error("--json and --fix are mutually exclusive.");
|
|
17730
|
+
process.exit(1);
|
|
17731
|
+
}
|
|
17732
|
+
const threshold = args.threshold === undefined ? 1 : Number(args.threshold);
|
|
17733
|
+
if (!Number.isFinite(threshold) || threshold < 0 || threshold > 1) {
|
|
17734
|
+
logger.error(`Invalid --threshold "${args.threshold}" (use 0..1).`);
|
|
17735
|
+
process.exit(1);
|
|
17736
|
+
}
|
|
17737
|
+
const timeoutS = args.timeout === undefined ? DEFAULT_TIMEOUT_S : Number(args.timeout);
|
|
17738
|
+
if (!Number.isInteger(timeoutS) || timeoutS <= 0) {
|
|
17739
|
+
logger.error(`Invalid --timeout "${args.timeout}" (whole seconds).`);
|
|
17740
|
+
process.exit(1);
|
|
17741
|
+
}
|
|
17742
|
+
return { agent: args.agent, threshold, timeoutS };
|
|
17743
|
+
};
|
|
17744
|
+
var runFixHandoff = async (agent, result, root, threshold) => {
|
|
17745
|
+
const count = result.counts.fail + result.counts.error;
|
|
17746
|
+
if (count === 0) {
|
|
17747
|
+
return;
|
|
17748
|
+
}
|
|
17749
|
+
const cli = AGENTS[agent];
|
|
17750
|
+
const report = await writeEvalReport(result, root, threshold);
|
|
17751
|
+
process.stderr.write(` Handing ${count} failed question${count === 1 ? "" : "s"} to ${cli.name}…
|
|
17752
|
+
|
|
17753
|
+
`);
|
|
17754
|
+
const code = await launchAgentCode2(cli.bin, evalFixPrompt(report));
|
|
17755
|
+
if (code === WINDOWS_COMMAND_NOT_FOUND) {
|
|
17756
|
+
notInstalled(agent);
|
|
17757
|
+
}
|
|
17758
|
+
if (code !== 0) {
|
|
17759
|
+
process.exit(code);
|
|
17760
|
+
}
|
|
17761
|
+
};
|
|
17762
|
+
var runInit = async (agent, file) => {
|
|
17763
|
+
const path = join37(process.cwd(), file);
|
|
17764
|
+
if (existsSync20(path)) {
|
|
17765
|
+
logger.error(`${file} already exists — edit it directly, or pass --file to draft elsewhere.`);
|
|
17766
|
+
process.exit(1);
|
|
17767
|
+
}
|
|
17768
|
+
const code = await launchAgentCode2(AGENTS[agent].bin, initPrompt(file));
|
|
17769
|
+
if (code === WINDOWS_COMMAND_NOT_FOUND) {
|
|
17770
|
+
notInstalled(agent);
|
|
17771
|
+
}
|
|
17772
|
+
if (code !== 0) {
|
|
17773
|
+
process.exit(code);
|
|
17774
|
+
}
|
|
17775
|
+
};
|
|
17776
|
+
var evalCommand = defineCommand8({
|
|
17777
|
+
args: {
|
|
17778
|
+
action: {
|
|
17779
|
+
description: 'Optional action: "init" drafts a starter evals file.',
|
|
17780
|
+
required: false,
|
|
17781
|
+
type: "positional"
|
|
17782
|
+
},
|
|
17783
|
+
agent: {
|
|
17784
|
+
default: "claude",
|
|
17785
|
+
description: "Agent CLI that reads and grades the docs: claude | codex.",
|
|
17786
|
+
type: "string"
|
|
17787
|
+
},
|
|
17788
|
+
file: {
|
|
17789
|
+
default: DEFAULT_FILE,
|
|
17790
|
+
description: "The evals file to run.",
|
|
17791
|
+
type: "string"
|
|
17792
|
+
},
|
|
17793
|
+
fix: {
|
|
17794
|
+
description: "After a failing run, hand the report to the agent to fix the docs interactively.",
|
|
17795
|
+
type: "boolean"
|
|
17796
|
+
},
|
|
17797
|
+
json: {
|
|
17798
|
+
description: "Emit the report as JSON on stdout (for CI/editors).",
|
|
17799
|
+
type: "boolean"
|
|
17800
|
+
},
|
|
17801
|
+
threshold: {
|
|
17802
|
+
description: "Minimum passing fraction (0..1) before the run exits non-zero. Defaults to 1.",
|
|
17803
|
+
type: "string"
|
|
17804
|
+
},
|
|
17805
|
+
timeout: {
|
|
17806
|
+
description: `Reader time limit per question, in seconds. Defaults to ${DEFAULT_TIMEOUT_S}.`,
|
|
17807
|
+
type: "string"
|
|
17808
|
+
},
|
|
17809
|
+
verbose: {
|
|
17810
|
+
description: "Include the reader's full answer under each failure.",
|
|
17811
|
+
type: "boolean"
|
|
17812
|
+
}
|
|
17813
|
+
},
|
|
17814
|
+
meta: {
|
|
17815
|
+
description: "Test the docs: an agent answers your questions using only the documentation.",
|
|
17816
|
+
name: "eval"
|
|
17817
|
+
},
|
|
17818
|
+
async run({ args }) {
|
|
17819
|
+
const root = process.cwd();
|
|
17820
|
+
const { agent, threshold, timeoutS } = parseFlags(args);
|
|
17821
|
+
if (args.action === "init") {
|
|
17822
|
+
await runInit(agent, args.file);
|
|
17823
|
+
return;
|
|
17824
|
+
}
|
|
17825
|
+
let result;
|
|
17826
|
+
try {
|
|
17827
|
+
const project = await scanProject(root, { mode: "build" });
|
|
17828
|
+
const evalsPath = join37(root, args.file);
|
|
17829
|
+
const { evals, raw } = await loadEvalsFile(evalsPath);
|
|
17830
|
+
process.stderr.write(`${headerLine(evals.questions.length, agent)}
|
|
17831
|
+
|
|
17832
|
+
`);
|
|
17833
|
+
result = await runEval({
|
|
17834
|
+
agent,
|
|
17835
|
+
evals,
|
|
17836
|
+
evalsPath,
|
|
17837
|
+
onProgress: (event) => {
|
|
17838
|
+
if (event.kind === "question-start") {
|
|
17839
|
+
process.stderr.write(`${startLine(event.id, event.index, event.total)}
|
|
17840
|
+
`);
|
|
17841
|
+
return;
|
|
17842
|
+
}
|
|
17843
|
+
const lines = [
|
|
17844
|
+
questionLine(event.result),
|
|
17845
|
+
...questionDetails(event.result, Boolean(args.verbose))
|
|
17846
|
+
];
|
|
17847
|
+
process.stderr.write(`${lines.join(`
|
|
17848
|
+
`)}
|
|
17849
|
+
`);
|
|
17850
|
+
},
|
|
17851
|
+
project,
|
|
17852
|
+
rawEvals: raw,
|
|
17853
|
+
readerTimeoutMs: timeoutS * 1000
|
|
17854
|
+
});
|
|
17855
|
+
} catch (error) {
|
|
17856
|
+
if (error instanceof EvalsFileError) {
|
|
17857
|
+
logger.error(error.message);
|
|
17858
|
+
process.exit(1);
|
|
17859
|
+
}
|
|
17860
|
+
if (error instanceof BlumeError) {
|
|
17861
|
+
logger.error(error.diagnostic.message);
|
|
17862
|
+
process.exit(1);
|
|
17863
|
+
}
|
|
17864
|
+
if (error?.code === "ENOENT") {
|
|
17865
|
+
notInstalled(agent);
|
|
17866
|
+
}
|
|
17867
|
+
reportInternalError(error);
|
|
17868
|
+
process.exit(1);
|
|
17869
|
+
}
|
|
17870
|
+
const tail2 = [
|
|
17871
|
+
"",
|
|
17872
|
+
...warningLines(result, root),
|
|
17873
|
+
...fixLines(result, root),
|
|
17874
|
+
"",
|
|
17875
|
+
` ${summaryLine2(result)}`,
|
|
17876
|
+
""
|
|
17877
|
+
];
|
|
17878
|
+
process.stderr.write(tail2.join(`
|
|
17879
|
+
`));
|
|
17880
|
+
const failed = passFraction(result) < threshold;
|
|
17881
|
+
if (args.fix) {
|
|
17882
|
+
await runFixHandoff(agent, result, root, threshold);
|
|
17883
|
+
return;
|
|
17884
|
+
}
|
|
17885
|
+
if (args.json) {
|
|
17886
|
+
process.stdout.write(evalReportJson(result, root, threshold));
|
|
17887
|
+
if (failed) {
|
|
17888
|
+
await flushStdout();
|
|
17889
|
+
process.exit(1);
|
|
17890
|
+
}
|
|
17891
|
+
return;
|
|
17892
|
+
}
|
|
17893
|
+
if (failed) {
|
|
17894
|
+
process.exit(1);
|
|
17895
|
+
}
|
|
17896
|
+
}
|
|
17897
|
+
});
|
|
17898
|
+
|
|
16893
17899
|
// src/cli/commands/init.ts
|
|
16894
17900
|
import * as clack from "@clack/prompts";
|
|
16895
|
-
import { defineCommand as
|
|
17901
|
+
import { defineCommand as defineCommand9 } from "citty";
|
|
16896
17902
|
import { resolve as resolve10 } from "pathe";
|
|
16897
17903
|
|
|
16898
17904
|
// src/cli/init/questions.ts
|
|
@@ -17007,7 +18013,7 @@ var ejectScaffold = async (root, answers) => {
|
|
|
17007
18013
|
`);
|
|
17008
18014
|
}
|
|
17009
18015
|
};
|
|
17010
|
-
var initCommand =
|
|
18016
|
+
var initCommand = defineCommand9({
|
|
17011
18017
|
args: {
|
|
17012
18018
|
"content-dir": {
|
|
17013
18019
|
description: "Content directory.",
|
|
@@ -17083,7 +18089,11 @@ var initCommand = defineCommand8({
|
|
|
17083
18089
|
}
|
|
17084
18090
|
const sink = interactive ? clack.log : logger;
|
|
17085
18091
|
const { createdPackage } = await applyPlan(buildPlan(root, answers), sink);
|
|
17086
|
-
const ignored = await ensureGitignore(root, [
|
|
18092
|
+
const ignored = await ensureGitignore(root, [
|
|
18093
|
+
"node_modules/",
|
|
18094
|
+
".blume/",
|
|
18095
|
+
"dist/"
|
|
18096
|
+
]);
|
|
17087
18097
|
if (ignored.length > 0) {
|
|
17088
18098
|
sink.success(`Added ${ignored.join(", ")} to .gitignore`);
|
|
17089
18099
|
}
|
|
@@ -17101,12 +18111,223 @@ var initCommand = defineCommand8({
|
|
|
17101
18111
|
}
|
|
17102
18112
|
});
|
|
17103
18113
|
|
|
18114
|
+
// src/cli/commands/mcp-stdio.ts
|
|
18115
|
+
import { readFile as readFile19 } from "node:fs/promises";
|
|
18116
|
+
import { defineCommand as defineCommand10 } from "citty";
|
|
18117
|
+
|
|
18118
|
+
// src/ai/mcp/stdio.ts
|
|
18119
|
+
import { once } from "node:events";
|
|
18120
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
18121
|
+
|
|
18122
|
+
// src/ai/mcp/server.ts
|
|
18123
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
18124
|
+
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
18125
|
+
import {
|
|
18126
|
+
CallToolRequestSchema,
|
|
18127
|
+
ListToolsRequestSchema
|
|
18128
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
18129
|
+
|
|
18130
|
+
// src/search/orama-index.ts
|
|
18131
|
+
import { create, insertMultiple, search } from "@orama/orama";
|
|
18132
|
+
var SCHEMA = {
|
|
18133
|
+
content: "string",
|
|
18134
|
+
description: "string",
|
|
18135
|
+
locale: "enum",
|
|
18136
|
+
route: "string",
|
|
18137
|
+
title: "string"
|
|
18138
|
+
};
|
|
18139
|
+
var BOOST = { description: 2, title: 3 };
|
|
18140
|
+
var buildOramaIndex = async (documents) => {
|
|
18141
|
+
const db = create({ schema: SCHEMA });
|
|
18142
|
+
await insertMultiple(db, documents);
|
|
18143
|
+
return db;
|
|
18144
|
+
};
|
|
18145
|
+
var queryOramaIndex = async (db, term, limit, locale) => {
|
|
18146
|
+
const found = await search(db, {
|
|
18147
|
+
boost: BOOST,
|
|
18148
|
+
limit,
|
|
18149
|
+
properties: ["title", "description", "content"],
|
|
18150
|
+
term,
|
|
18151
|
+
...locale ? { where: { locale: { eq: locale } } } : {}
|
|
18152
|
+
});
|
|
18153
|
+
return found.hits.map((hit) => hit.document);
|
|
18154
|
+
};
|
|
18155
|
+
|
|
18156
|
+
// src/ai/mcp/server.ts
|
|
18157
|
+
var DEFAULT_SEARCH_LIMIT = 8;
|
|
18158
|
+
var MAX_SEARCH_LIMIT = 20;
|
|
18159
|
+
var EXCERPT_LENGTH = 200;
|
|
18160
|
+
var INPUT_SCHEMAS = {
|
|
18161
|
+
get_navigation: { properties: {}, type: "object" },
|
|
18162
|
+
get_page: {
|
|
18163
|
+
properties: {
|
|
18164
|
+
route: {
|
|
18165
|
+
description: "The page route, e.g. `/guides/install`.",
|
|
18166
|
+
type: "string"
|
|
18167
|
+
}
|
|
18168
|
+
},
|
|
18169
|
+
required: ["route"],
|
|
18170
|
+
type: "object"
|
|
18171
|
+
},
|
|
18172
|
+
list_pages: { properties: {}, type: "object" },
|
|
18173
|
+
search_docs: {
|
|
18174
|
+
properties: {
|
|
18175
|
+
limit: {
|
|
18176
|
+
description: `Maximum hits to return (default ${DEFAULT_SEARCH_LIMIT}).`,
|
|
18177
|
+
maximum: MAX_SEARCH_LIMIT,
|
|
18178
|
+
minimum: 1,
|
|
18179
|
+
type: "integer"
|
|
18180
|
+
},
|
|
18181
|
+
query: { description: "The search query.", type: "string" }
|
|
18182
|
+
},
|
|
18183
|
+
required: ["query"],
|
|
18184
|
+
type: "object"
|
|
18185
|
+
}
|
|
18186
|
+
};
|
|
18187
|
+
var TOOL_DEFINITIONS = MCP_TOOLS.map((tool) => ({
|
|
18188
|
+
annotations: tool.annotations,
|
|
18189
|
+
description: tool.description,
|
|
18190
|
+
inputSchema: INPUT_SCHEMAS[tool.name],
|
|
18191
|
+
name: tool.name,
|
|
18192
|
+
title: tool.title
|
|
18193
|
+
}));
|
|
18194
|
+
var asString2 = (value) => typeof value === "string" ? value : "";
|
|
18195
|
+
var asLimit = (value) => {
|
|
18196
|
+
const num = typeof value === "number" ? value : Number(value);
|
|
18197
|
+
if (!Number.isFinite(num)) {
|
|
18198
|
+
return DEFAULT_SEARCH_LIMIT;
|
|
18199
|
+
}
|
|
18200
|
+
return Math.min(Math.max(Math.trunc(num), 1), MAX_SEARCH_LIMIT);
|
|
18201
|
+
};
|
|
18202
|
+
var normalizeRoute2 = (input, data) => {
|
|
18203
|
+
let value = input.trim();
|
|
18204
|
+
if (/^https?:\/\//iu.test(value)) {
|
|
18205
|
+
try {
|
|
18206
|
+
value = new URL(value).pathname;
|
|
18207
|
+
} catch {}
|
|
18208
|
+
}
|
|
18209
|
+
try {
|
|
18210
|
+
value = decodeURI(value);
|
|
18211
|
+
} catch {}
|
|
18212
|
+
const noTrailing = value.replace(/\/+$/u, "");
|
|
18213
|
+
const noSuffix = noTrailing.replace(/\.mdx?$/u, "");
|
|
18214
|
+
const withSlash = noSuffix.startsWith("/") ? noSuffix : `/${noSuffix}`;
|
|
18215
|
+
const based = stripBasePath(data.base, withSlash);
|
|
18216
|
+
return based === "" ? "/" : based;
|
|
18217
|
+
};
|
|
18218
|
+
var urlFor = (route, data) => {
|
|
18219
|
+
const path = withBasePath(data.base, route);
|
|
18220
|
+
return data.site ? `${data.site.replace(/\/+$/u, "")}${path}` : path;
|
|
18221
|
+
};
|
|
18222
|
+
var excerptFor = (doc) => {
|
|
18223
|
+
if (doc.description) {
|
|
18224
|
+
return doc.description;
|
|
18225
|
+
}
|
|
18226
|
+
const head = doc.content.slice(0, EXCERPT_LENGTH).trim();
|
|
18227
|
+
return doc.content.length > EXCERPT_LENGTH ? `${head}…` : head;
|
|
18228
|
+
};
|
|
18229
|
+
var text = (value, isError = false) => ({
|
|
18230
|
+
content: [{ text: value, type: "text" }],
|
|
18231
|
+
...isError ? { isError: true } : {}
|
|
18232
|
+
});
|
|
18233
|
+
var createIndexProvider = (documents) => {
|
|
18234
|
+
let dbPromise = null;
|
|
18235
|
+
return () => {
|
|
18236
|
+
dbPromise ??= buildOramaIndex(documents);
|
|
18237
|
+
return dbPromise;
|
|
18238
|
+
};
|
|
18239
|
+
};
|
|
18240
|
+
var buildServer = (data, index) => {
|
|
18241
|
+
const server = new Server({ name: data.name, version: data.version }, {
|
|
18242
|
+
capabilities: { tools: {} },
|
|
18243
|
+
...data.instructions ? { instructions: data.instructions } : {}
|
|
18244
|
+
});
|
|
18245
|
+
server.setRequestHandler(ListToolsRequestSchema, () => ({
|
|
18246
|
+
tools: TOOL_DEFINITIONS
|
|
18247
|
+
}));
|
|
18248
|
+
server.setRequestHandler(CallToolRequestSchema, async (request2) => {
|
|
18249
|
+
const { arguments: args = {}, name } = request2.params;
|
|
18250
|
+
if (name === "search_docs") {
|
|
18251
|
+
const db = await index();
|
|
18252
|
+
const hits = await queryOramaIndex(db, asString2(args.query), asLimit(args.limit));
|
|
18253
|
+
const results = hits.map((doc) => ({
|
|
18254
|
+
excerpt: excerptFor(doc),
|
|
18255
|
+
route: doc.route,
|
|
18256
|
+
title: doc.title,
|
|
18257
|
+
url: urlFor(doc.route, data)
|
|
18258
|
+
}));
|
|
18259
|
+
return text(JSON.stringify(results, null, 2));
|
|
18260
|
+
}
|
|
18261
|
+
if (name === "get_page") {
|
|
18262
|
+
const key = normalizeRoute2(asString2(args.route), data);
|
|
18263
|
+
const markdown = data.pages[key];
|
|
18264
|
+
if (markdown === undefined) {
|
|
18265
|
+
return text(`No page found at "${key}". Use list_pages or search_docs to find valid routes.`, true);
|
|
18266
|
+
}
|
|
18267
|
+
return text(markdown);
|
|
18268
|
+
}
|
|
18269
|
+
if (name === "list_pages") {
|
|
18270
|
+
return text(JSON.stringify(data.routes.map((route) => ({
|
|
18271
|
+
contentType: route.contentType,
|
|
18272
|
+
description: route.description,
|
|
18273
|
+
lastModified: route.lastModified,
|
|
18274
|
+
route: route.route,
|
|
18275
|
+
title: route.title,
|
|
18276
|
+
url: urlFor(route.route, data)
|
|
18277
|
+
})), null, 2));
|
|
18278
|
+
}
|
|
18279
|
+
if (name === "get_navigation") {
|
|
18280
|
+
return text(JSON.stringify(data.navigation, null, 2));
|
|
18281
|
+
}
|
|
18282
|
+
return text(`Unknown tool: ${name}`, true);
|
|
18283
|
+
});
|
|
18284
|
+
return server;
|
|
18285
|
+
};
|
|
18286
|
+
|
|
18287
|
+
// src/ai/mcp/stdio.ts
|
|
18288
|
+
var serveMcpStdio = async (data, streams = {}) => {
|
|
18289
|
+
const stdin = streams.stdin ?? process.stdin;
|
|
18290
|
+
const stdout = streams.stdout ?? process.stdout;
|
|
18291
|
+
const transport = new StdioServerTransport(stdin, stdout);
|
|
18292
|
+
const server = buildServer(data, createIndexProvider(data.documents));
|
|
18293
|
+
await server.connect(transport);
|
|
18294
|
+
await once(stdin, "end");
|
|
18295
|
+
await transport.close();
|
|
18296
|
+
};
|
|
18297
|
+
|
|
18298
|
+
// src/cli/commands/mcp-stdio.ts
|
|
18299
|
+
var mcpStdioCommand = defineCommand10({
|
|
18300
|
+
args: {
|
|
18301
|
+
data: {
|
|
18302
|
+
description: "Path to a serialized MCP data snapshot (JSON).",
|
|
18303
|
+
required: true,
|
|
18304
|
+
type: "string"
|
|
18305
|
+
}
|
|
18306
|
+
},
|
|
18307
|
+
meta: {
|
|
18308
|
+
description: "Serve an MCP data snapshot over stdio (internal, used by `blume eval`).",
|
|
18309
|
+
name: "mcp-stdio"
|
|
18310
|
+
},
|
|
18311
|
+
async run({ args }) {
|
|
18312
|
+
let data;
|
|
18313
|
+
try {
|
|
18314
|
+
data = JSON.parse(await readFile19(args.data, "utf-8"));
|
|
18315
|
+
} catch (error) {
|
|
18316
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
18317
|
+
process.stderr.write(`blume mcp-stdio: cannot load the snapshot at ${args.data}: ${detail}
|
|
18318
|
+
`);
|
|
18319
|
+
process.exit(1);
|
|
18320
|
+
}
|
|
18321
|
+
await serveMcpStdio(data);
|
|
18322
|
+
}
|
|
18323
|
+
});
|
|
18324
|
+
|
|
17104
18325
|
// src/cli/commands/preview.ts
|
|
17105
|
-
import { existsSync as
|
|
18326
|
+
import { existsSync as existsSync21 } from "node:fs";
|
|
17106
18327
|
import { preview } from "astro";
|
|
17107
|
-
import { defineCommand as
|
|
17108
|
-
import { join as
|
|
17109
|
-
var previewCommand =
|
|
18328
|
+
import { defineCommand as defineCommand11 } from "citty";
|
|
18329
|
+
import { join as join38 } from "pathe";
|
|
18330
|
+
var previewCommand = defineCommand11({
|
|
17110
18331
|
args: {
|
|
17111
18332
|
host: { description: "Network host to bind.", type: "string" },
|
|
17112
18333
|
port: { description: "Port to listen on.", type: "string" }
|
|
@@ -17119,7 +18340,7 @@ var previewCommand = defineCommand9({
|
|
|
17119
18340
|
const root = process.cwd();
|
|
17120
18341
|
const { config } = await loadConfig(root);
|
|
17121
18342
|
const context = resolveProjectContext(root, config);
|
|
17122
|
-
if (!
|
|
18343
|
+
if (!existsSync21(join38(context.outDir, "astro.config.mjs"))) {
|
|
17123
18344
|
logger.error("No build found. Run `blume build` first.");
|
|
17124
18345
|
process.exit(1);
|
|
17125
18346
|
}
|
|
@@ -17136,9 +18357,9 @@ var previewCommand = defineCommand9({
|
|
|
17136
18357
|
|
|
17137
18358
|
// src/cli/commands/sync.ts
|
|
17138
18359
|
import { rm as rm4 } from "node:fs/promises";
|
|
17139
|
-
import { defineCommand as
|
|
17140
|
-
import { join as
|
|
17141
|
-
var syncCommand =
|
|
18360
|
+
import { defineCommand as defineCommand12 } from "citty";
|
|
18361
|
+
import { join as join39 } from "pathe";
|
|
18362
|
+
var syncCommand = defineCommand12({
|
|
17142
18363
|
args: {
|
|
17143
18364
|
force: {
|
|
17144
18365
|
description: "Clear the source cache before refetching.",
|
|
@@ -17159,7 +18380,7 @@ var syncCommand = defineCommand10({
|
|
|
17159
18380
|
if (args.force) {
|
|
17160
18381
|
const { config } = await loadConfig(root);
|
|
17161
18382
|
const context = resolveProjectContext(root, config);
|
|
17162
|
-
await rm4(
|
|
18383
|
+
await rm4(join39(context.outDir, "cache"), { force: true, recursive: true });
|
|
17163
18384
|
logger.info("Cleared source cache.");
|
|
17164
18385
|
}
|
|
17165
18386
|
const lock = readDevLock(resolveRuntimeDir(root));
|
|
@@ -17177,13 +18398,13 @@ var syncCommand = defineCommand10({
|
|
|
17177
18398
|
});
|
|
17178
18399
|
|
|
17179
18400
|
// src/cli/commands/validate.ts
|
|
17180
|
-
import { existsSync as
|
|
17181
|
-
import { defineCommand as
|
|
17182
|
-
import { join as
|
|
18401
|
+
import { existsSync as existsSync23 } from "node:fs";
|
|
18402
|
+
import { defineCommand as defineCommand13 } from "citty";
|
|
18403
|
+
import { join as join41 } from "pathe";
|
|
17183
18404
|
|
|
17184
18405
|
// src/core/links.ts
|
|
17185
|
-
import { existsSync as
|
|
17186
|
-
import { basename as basename7, join as
|
|
18406
|
+
import { existsSync as existsSync22 } from "node:fs";
|
|
18407
|
+
import { basename as basename7, join as join40 } from "pathe";
|
|
17187
18408
|
var HTTP = /^https?:\/\//iu;
|
|
17188
18409
|
var PROTOCOL_RELATIVE = /^\/\//u;
|
|
17189
18410
|
var SCHEME = /^[a-z][a-z0-9+.-]*:/iu;
|
|
@@ -17196,7 +18417,7 @@ var decodePercent = (value) => {
|
|
|
17196
18417
|
};
|
|
17197
18418
|
var DOC_EXT = /\.(?:md|mdx)$/iu;
|
|
17198
18419
|
var FILE_EXT = /\.[a-z0-9]+$/iu;
|
|
17199
|
-
var assetIsPresent = (resolved, ctx) => ctx.publicDir !== null &&
|
|
18420
|
+
var assetIsPresent = (resolved, ctx) => ctx.publicDir !== null && existsSync22(join40(ctx.publicDir, resolved));
|
|
17200
18421
|
var NUMERIC_PREFIX3 = /^\d+[-_.]/u;
|
|
17201
18422
|
var isIndexPage = (page2) => /^index\.(?:md|mdx)$/iu.test(basename7(page2.navPath).replace(NUMERIC_PREFIX3, ""));
|
|
17202
18423
|
var applyRelativePart = (segments, part) => {
|
|
@@ -17369,7 +18590,7 @@ var validateLinks = async (graph, options) => {
|
|
|
17369
18590
|
};
|
|
17370
18591
|
|
|
17371
18592
|
// src/cli/commands/validate.ts
|
|
17372
|
-
var validateCommand =
|
|
18593
|
+
var validateCommand = defineCommand13({
|
|
17373
18594
|
args: {
|
|
17374
18595
|
external: {
|
|
17375
18596
|
description: "Check external (HTTP) links over the network.",
|
|
@@ -17407,12 +18628,12 @@ var validateCommand = defineCommand11({
|
|
|
17407
18628
|
});
|
|
17408
18629
|
extraRoutes.push(...manifest.routes.flatMap((route) => route.fallback ? [route.path] : []));
|
|
17409
18630
|
}
|
|
17410
|
-
const publicDir =
|
|
18631
|
+
const publicDir = join41(root, "public");
|
|
17411
18632
|
diagnostics.push(...await validateLinks(project.graph, {
|
|
17412
18633
|
basePath: project.config.basePath,
|
|
17413
18634
|
checkExternal: Boolean(args.external),
|
|
17414
18635
|
extraRoutes,
|
|
17415
|
-
publicDir:
|
|
18636
|
+
publicDir: existsSync23(publicDir) ? publicDir : null,
|
|
17416
18637
|
redirects: project.config.redirects
|
|
17417
18638
|
}));
|
|
17418
18639
|
} catch (error) {
|
|
@@ -17443,7 +18664,7 @@ var validateCommand = defineCommand11({
|
|
|
17443
18664
|
});
|
|
17444
18665
|
|
|
17445
18666
|
// src/cli/index.ts
|
|
17446
|
-
var main =
|
|
18667
|
+
var main = defineCommand14({
|
|
17447
18668
|
meta: {
|
|
17448
18669
|
description: "Markdown-first documentation powered by Astro and Vite.",
|
|
17449
18670
|
name: "blume",
|
|
@@ -17457,7 +18678,9 @@ var main = defineCommand12({
|
|
|
17457
18678
|
dev: devCommand,
|
|
17458
18679
|
doctor: doctorCommand,
|
|
17459
18680
|
eject: ejectCommand,
|
|
18681
|
+
eval: evalCommand,
|
|
17460
18682
|
init: initCommand,
|
|
18683
|
+
"mcp-stdio": mcpStdioCommand,
|
|
17461
18684
|
preview: previewCommand,
|
|
17462
18685
|
sync: syncCommand,
|
|
17463
18686
|
validate: validateCommand
|
|
@@ -17474,5 +18697,5 @@ process.on("unhandledRejection", (error) => {
|
|
|
17474
18697
|
});
|
|
17475
18698
|
runMain(main);
|
|
17476
18699
|
|
|
17477
|
-
//# debugId=
|
|
18700
|
+
//# debugId=E4DFD2C6131D546564756E2164756E21
|
|
17478
18701
|
//# sourceMappingURL=index.js.map
|