blume 1.1.3 → 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 +54 -0
- package/README.md +1 -1
- package/dist/cli/index.js +1473 -149
- package/dist/cli/index.js.map +47 -36
- 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 +3 -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 +48 -14
- package/src/ai/mcp/stdio.ts +35 -0
- package/src/astro/generate.ts +119 -48
- package/src/astro/templates.ts +173 -37
- package/src/audit/checks/duplicates.ts +15 -6
- package/src/audit/checks/indexability.ts +11 -2
- package/src/audit/checks/network.ts +22 -8
- package/src/audit/checks/sitemap.ts +42 -16
- package/src/audit/redirects.ts +12 -1
- package/src/audit/run.ts +13 -3
- package/src/audit/url.ts +21 -2
- package/src/cli/commands/audit.ts +21 -6
- package/src/cli/commands/dev.ts +19 -2
- 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/Frame.astro +4 -1
- package/src/components/content/Prompt.astro +4 -1
- package/src/components/content/Tooltip.astro +4 -1
- package/src/components/content/TreeFolder.astro +1 -2
- package/src/components/content/Update.astro +45 -0
- package/src/components/islands/AskAI.astro +9 -2
- package/src/components/islands/ask-ai.tsx +23 -4
- package/src/components/islands/hooks.ts +48 -15
- package/src/components/layout/NavTree.astro +37 -19
- package/src/components/layout/ReferenceLayout.astro +4 -0
- package/src/components/layout/RootLayout.astro +14 -3
- package/src/components/layout/Search.astro +5 -1
- package/src/components/layout/head-scripts.ts +22 -5
- 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 +3 -0
- package/src/core/deployment-env.ts +7 -2
- package/src/core/graph.ts +8 -1
- package/src/core/i18n.ts +10 -2
- package/src/core/navigation.ts +16 -5
- package/src/core/schema.ts +51 -4
- package/src/core/server-features.ts +1 -1
- package/src/core/sources/normalize.ts +69 -8
- package/src/core/sources/notion.ts +4 -2
- package/src/core/sources/sanity.ts +5 -3
- 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/markdown/code-title.ts +7 -1
- package/src/openapi/model.ts +31 -2
- package/src/openapi/references.ts +23 -2
- package/src/openapi/render-mdx.ts +39 -11
- 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";
|
|
@@ -1581,6 +1581,13 @@ var normalizePath = (path) => {
|
|
|
1581
1581
|
const trimmed = path.replace(/\/+$/u, "");
|
|
1582
1582
|
return trimmed === "" ? "/" : trimmed;
|
|
1583
1583
|
};
|
|
1584
|
+
var decodePath = (path) => {
|
|
1585
|
+
try {
|
|
1586
|
+
return decodeURI(path);
|
|
1587
|
+
} catch {
|
|
1588
|
+
return path;
|
|
1589
|
+
}
|
|
1590
|
+
};
|
|
1584
1591
|
var siteOrigin = (site) => {
|
|
1585
1592
|
if (!site) {
|
|
1586
1593
|
return null;
|
|
@@ -1611,7 +1618,7 @@ var resolveHref = (pageUrl, href, origin, deployBase = "") => {
|
|
|
1611
1618
|
return {
|
|
1612
1619
|
hash: parsed.hash.slice(1),
|
|
1613
1620
|
kind: "self-origin",
|
|
1614
|
-
path: normalizePath(stripBasePath(deployBase, parsed.pathname))
|
|
1621
|
+
path: normalizePath(stripBasePath(deployBase, decodePath(parsed.pathname)))
|
|
1615
1622
|
};
|
|
1616
1623
|
}
|
|
1617
1624
|
return { kind: "external", url: parsed.toString() };
|
|
@@ -1626,7 +1633,7 @@ var resolveHref = (pageUrl, href, origin, deployBase = "") => {
|
|
|
1626
1633
|
return {
|
|
1627
1634
|
hash: resolved.hash.slice(1),
|
|
1628
1635
|
kind: "internal",
|
|
1629
|
-
path: normalizePath(stripBasePath(deployBase, resolved.pathname))
|
|
1636
|
+
path: normalizePath(stripBasePath(deployBase, decodePath(resolved.pathname)))
|
|
1630
1637
|
};
|
|
1631
1638
|
};
|
|
1632
1639
|
|
|
@@ -1825,17 +1832,20 @@ var contentChecks = {
|
|
|
1825
1832
|
};
|
|
1826
1833
|
|
|
1827
1834
|
// src/audit/checks/duplicates.ts
|
|
1828
|
-
var isNonCanonical = (page) => {
|
|
1835
|
+
var isNonCanonical = (page, deployBase) => {
|
|
1829
1836
|
if (!page.canonical) {
|
|
1830
1837
|
return false;
|
|
1831
1838
|
}
|
|
1832
1839
|
try {
|
|
1833
|
-
return new URL(page.canonical).pathname.replace(/\/$/u, "") !== page.url.replace(/\/$/u, "");
|
|
1840
|
+
return stripBasePath(deployBase, decodePath(new URL(page.canonical).pathname)).replace(/\/$/u, "") !== page.url.replace(/\/$/u, "");
|
|
1834
1841
|
} catch {
|
|
1835
1842
|
return false;
|
|
1836
1843
|
}
|
|
1837
1844
|
};
|
|
1838
|
-
var comparable = (context) =>
|
|
1845
|
+
var comparable = (context) => {
|
|
1846
|
+
const deployBase = normalizeBasePath(context.project.config.deployment.base);
|
|
1847
|
+
return context.pages.filter((page) => page.indexable && !page.route?.fallback && !isNonCanonical(page, deployBase));
|
|
1848
|
+
};
|
|
1839
1849
|
var reportGroups = (context, pages, id, key, describe, frontmatterKey) => {
|
|
1840
1850
|
const groups = new Map;
|
|
1841
1851
|
for (const page of pages) {
|
|
@@ -2009,12 +2019,12 @@ var PLATFORMS = [
|
|
|
2009
2019
|
{
|
|
2010
2020
|
adapter: "vercel",
|
|
2011
2021
|
detect: (env) => Boolean(env.VERCEL),
|
|
2012
|
-
site: (env) => toUrl(env.VERCEL_PROJECT_PRODUCTION_URL ?? env.VERCEL_URL)
|
|
2022
|
+
site: (env) => toUrl(env.VERCEL_PROJECT_PRODUCTION_URL) ?? toUrl(env.VERCEL_URL)
|
|
2013
2023
|
},
|
|
2014
2024
|
{
|
|
2015
2025
|
adapter: "netlify",
|
|
2016
2026
|
detect: (env) => Boolean(env.NETLIFY),
|
|
2017
|
-
site: (env) => toUrl(env.URL ?? env.DEPLOY_PRIME_URL ?? env.DEPLOY_URL)
|
|
2027
|
+
site: (env) => toUrl(env.URL) ?? toUrl(env.DEPLOY_PRIME_URL) ?? toUrl(env.DEPLOY_URL)
|
|
2018
2028
|
},
|
|
2019
2029
|
{
|
|
2020
2030
|
adapter: "cloudflare",
|
|
@@ -2073,7 +2083,7 @@ var canonicalChecks = (context, page) => {
|
|
|
2073
2083
|
}
|
|
2074
2084
|
return found;
|
|
2075
2085
|
}
|
|
2076
|
-
const target = normalizePath(canonical.pathname);
|
|
2086
|
+
const target = normalizePath(stripBasePath(normalizeBasePath(context.project.config.deployment.base), decodePath(canonical.pathname)));
|
|
2077
2087
|
if (target === normalizePath(page.url)) {
|
|
2078
2088
|
return found;
|
|
2079
2089
|
}
|
|
@@ -2401,7 +2411,7 @@ var probeAll = async (urls, options = {}) => {
|
|
|
2401
2411
|
var CLIENT_ERROR = 400;
|
|
2402
2412
|
var SERVER_ERROR = 500;
|
|
2403
2413
|
var SLOW_MS = 1500;
|
|
2404
|
-
var liveUrl = (origin, page) => new URL(page.url
|
|
2414
|
+
var liveUrl = (origin, page, deployBase) => new URL(`${deployBase}${page.url}`, origin).toString();
|
|
2405
2415
|
var badResponse = (context, page, result) => {
|
|
2406
2416
|
const site = pageSite(context, page);
|
|
2407
2417
|
if (result.timedOut) {
|
|
@@ -2445,12 +2455,13 @@ var networkChecks = {
|
|
|
2445
2455
|
return [];
|
|
2446
2456
|
}
|
|
2447
2457
|
const found = [];
|
|
2448
|
-
const
|
|
2449
|
-
const
|
|
2450
|
-
const
|
|
2458
|
+
const deployBase = normalizeBasePath(context.project.config.deployment.base);
|
|
2459
|
+
const targets = context.pages.map((page) => liveUrl(origin, page, deployBase));
|
|
2460
|
+
const robotsUrl = new URL(`${deployBase}/robots.txt`, origin).toString();
|
|
2461
|
+
const sitemapUrl = new URL(`${deployBase}/sitemap.xml`, origin).toString();
|
|
2451
2462
|
const results = await probeAll([...targets, robotsUrl, sitemapUrl]);
|
|
2452
2463
|
for (const page of context.pages) {
|
|
2453
|
-
const result = results.get(liveUrl(origin, page));
|
|
2464
|
+
const result = results.get(liveUrl(origin, page, deployBase));
|
|
2454
2465
|
if (!result) {
|
|
2455
2466
|
continue;
|
|
2456
2467
|
}
|
|
@@ -2717,23 +2728,23 @@ var MAX_SITEMAP_BYTES = 50 * 1024 * 1024;
|
|
|
2717
2728
|
var MAX_SITEMAP_URLS = 50000;
|
|
2718
2729
|
var LASTMOD_SLACK_MS = 24 * 60 * 60 * 1000;
|
|
2719
2730
|
var ERROR_ROUTES2 = new Set(["/404", "/500"]);
|
|
2720
|
-
var sitemapPaths = (context) => {
|
|
2731
|
+
var sitemapPaths = (context, deployBase) => {
|
|
2721
2732
|
const paths = new Map;
|
|
2722
2733
|
for (const loc of context.sitemap?.urls ?? []) {
|
|
2723
2734
|
try {
|
|
2724
|
-
paths.set(normalizePath(new URL(loc).pathname), loc);
|
|
2735
|
+
paths.set(normalizePath(stripBasePath(deployBase, decodePath(new URL(loc).pathname))), loc);
|
|
2725
2736
|
} catch {}
|
|
2726
2737
|
}
|
|
2727
2738
|
return paths;
|
|
2728
2739
|
};
|
|
2729
|
-
var canonicalPath = (canonical) => {
|
|
2740
|
+
var canonicalPath = (canonical, deployBase) => {
|
|
2730
2741
|
try {
|
|
2731
|
-
return normalizePath(new URL(canonical).pathname);
|
|
2742
|
+
return normalizePath(stripBasePath(deployBase, decodePath(new URL(canonical).pathname)));
|
|
2732
2743
|
} catch {
|
|
2733
2744
|
return null;
|
|
2734
2745
|
}
|
|
2735
2746
|
};
|
|
2736
|
-
var checkListedUrl = (context, loc, origin, file) => {
|
|
2747
|
+
var checkListedUrl = (context, loc, origin, file, deployBase) => {
|
|
2737
2748
|
let parsed;
|
|
2738
2749
|
try {
|
|
2739
2750
|
parsed = new URL(loc);
|
|
@@ -2747,7 +2758,7 @@ var checkListedUrl = (context, loc, origin, file) => {
|
|
|
2747
2758
|
finding("BLUME_AUDIT_SITEMAP_OUT_OF_SCOPE", { file, url: loc }, `sitemap.xml lists ${loc}, which is on another origin.`)
|
|
2748
2759
|
];
|
|
2749
2760
|
}
|
|
2750
|
-
const path = normalizePath(stripBasePath(
|
|
2761
|
+
const path = normalizePath(stripBasePath(deployBase, decodePath(parsed.pathname)));
|
|
2751
2762
|
const page = context.byUrl.get(path);
|
|
2752
2763
|
if (!page) {
|
|
2753
2764
|
const redirect = context.redirects.find((entry) => normalizePath(entry.from) === path);
|
|
@@ -2759,7 +2770,7 @@ var checkListedUrl = (context, loc, origin, file) => {
|
|
|
2759
2770
|
if (!page.indexable) {
|
|
2760
2771
|
found.push(finding("BLUME_AUDIT_NOINDEX_IN_SITEMAP", pageSite(context, page, ["noindex"]), `${path} is in the sitemap but declares robots "${page.robots}".`));
|
|
2761
2772
|
}
|
|
2762
|
-
const canonical = page.canonical && canonicalPath(page.canonical);
|
|
2773
|
+
const canonical = page.canonical && canonicalPath(page.canonical, deployBase);
|
|
2763
2774
|
if (canonical && canonical !== path) {
|
|
2764
2775
|
found.push(finding("BLUME_AUDIT_NON_CANONICAL_IN_SITEMAP", pageSite(context, page, ["seo", "canonical"]), `${path} is in the sitemap but canonicalizes to ${canonical}.`));
|
|
2765
2776
|
}
|
|
@@ -2795,9 +2806,10 @@ var sitemapChecks = {
|
|
|
2795
2806
|
}
|
|
2796
2807
|
}
|
|
2797
2808
|
const origin = siteOrigin(site);
|
|
2798
|
-
const
|
|
2809
|
+
const deployBase = normalizeBasePath(context.project.config.deployment.base);
|
|
2810
|
+
const listed = sitemapPaths(context, deployBase);
|
|
2799
2811
|
for (const loc of sitemap.urls) {
|
|
2800
|
-
found.push(...checkListedUrl(context, loc, origin, sitemap.file));
|
|
2812
|
+
found.push(...checkListedUrl(context, loc, origin, sitemap.file, deployBase));
|
|
2801
2813
|
}
|
|
2802
2814
|
for (const page of context.pages) {
|
|
2803
2815
|
if (!page.indexable || ERROR_ROUTES2.has(page.url) || listed.has(normalizePath(page.url))) {
|
|
@@ -3078,7 +3090,12 @@ var routeSlug = (route) => slugify(trimChar(route, "/")) || "reference";
|
|
|
3078
3090
|
var sourcesOf = (block) => {
|
|
3079
3091
|
const sources = [...block.sources];
|
|
3080
3092
|
if (block.spec) {
|
|
3081
|
-
sources.unshift({
|
|
3093
|
+
sources.unshift({
|
|
3094
|
+
includeInLlms: true,
|
|
3095
|
+
includeInSearch: true,
|
|
3096
|
+
noindex: false,
|
|
3097
|
+
spec: block.spec
|
|
3098
|
+
});
|
|
3082
3099
|
}
|
|
3083
3100
|
return sources;
|
|
3084
3101
|
};
|
|
@@ -3102,8 +3119,11 @@ var referencesFor = (kind, block, defaultLabel, renderer, display, basePath) =>
|
|
|
3102
3119
|
return {
|
|
3103
3120
|
basePath,
|
|
3104
3121
|
display,
|
|
3122
|
+
includeInLlms: source.includeInLlms,
|
|
3123
|
+
includeInSearch: source.includeInSearch,
|
|
3105
3124
|
kind,
|
|
3106
3125
|
label,
|
|
3126
|
+
noindex: source.noindex,
|
|
3107
3127
|
renderer,
|
|
3108
3128
|
route,
|
|
3109
3129
|
scalar: block.scalar,
|
|
@@ -3405,7 +3425,10 @@ var WRANGLER_CONFIG_FILES = [
|
|
|
3405
3425
|
"wrangler.toml"
|
|
3406
3426
|
];
|
|
3407
3427
|
var resolveCloudflareAdapterArgs = (context) => {
|
|
3408
|
-
const args = [
|
|
3428
|
+
const args = [
|
|
3429
|
+
'prerenderEnvironment: "node"',
|
|
3430
|
+
'imageService: "compile"'
|
|
3431
|
+
];
|
|
3409
3432
|
const wranglerPath = WRANGLER_CONFIG_FILES.map((file) => join8(context.root, file)).find((file) => existsSync4(file));
|
|
3410
3433
|
if (wranglerPath) {
|
|
3411
3434
|
let configPath = relative5(context.outDir, wranglerPath);
|
|
@@ -3416,6 +3439,18 @@ var resolveCloudflareAdapterArgs = (context) => {
|
|
|
3416
3439
|
}
|
|
3417
3440
|
return `{ ${args.join(", ")} }`;
|
|
3418
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
|
+
};
|
|
3419
3454
|
var runtimeDependencies = (options) => {
|
|
3420
3455
|
const { config, needsReact, needsSvelte, needsVue } = options;
|
|
3421
3456
|
const deps = ["@astrojs/mdx"];
|
|
@@ -3432,7 +3467,7 @@ var runtimeDependencies = (options) => {
|
|
|
3432
3467
|
deps.push("@scalar/astro");
|
|
3433
3468
|
}
|
|
3434
3469
|
deps.push(...searchProviderMeta(config.search.provider).runtimeDeps);
|
|
3435
|
-
if (config.ai.ask?.enabled) {
|
|
3470
|
+
if (config.ai.ask?.enabled && !config.ai.ask.endpoint) {
|
|
3436
3471
|
const askDep = askBackendRuntimeDep(config.ai.ask);
|
|
3437
3472
|
if (askDep) {
|
|
3438
3473
|
deps.push(askDep);
|
|
@@ -3468,6 +3503,36 @@ var astroOutDir = (context) => context.distDir ?? `${context.root}/dist`;
|
|
|
3468
3503
|
var adapterRoot = (context) => dirname5(astroOutDir(context));
|
|
3469
3504
|
var REACT_EXCLUDE = String.raw`exclude: [/\/node_modules\/\.vite\//]`;
|
|
3470
3505
|
var reactIntegration = (compilerPath) => compilerPath ? `react({ babel: { plugins: [[${JSON.stringify(compilerPath)}, { target: "19" }]] }, ${REACT_EXCLUDE} })` : `react({ ${REACT_EXCLUDE} })`;
|
|
3506
|
+
var devWatchOption = (outDir, contentWatchesRuntimeDir) => contentWatchesRuntimeDir ? `
|
|
3507
|
+
// Astro's cache dir sits inside the docs collection, whose watcher would
|
|
3508
|
+
// otherwise churn (and can loop) on Astro's own writes. Trade-off: .md
|
|
3509
|
+
// body edits need a dev-server restart in this layout.
|
|
3510
|
+
watch: {
|
|
3511
|
+
ignored: ${JSON.stringify([join8(outDir, ".astro", "**")])},
|
|
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
|
+
};
|
|
3471
3536
|
var astroConfigTemplate = (options) => {
|
|
3472
3537
|
const { context, config, needsReact, pages, dataPath, themePath } = options;
|
|
3473
3538
|
const {
|
|
@@ -3498,6 +3563,7 @@ var astroConfigTemplate = (options) => {
|
|
|
3498
3563
|
const adapterExpr = deployment.adapter === "vercel" ? `withAdapterRoot(adapter(${adapterArgs}), ${JSON.stringify(adapterRoot(context))})` : `adapter(${adapterArgs})`;
|
|
3499
3564
|
const adapterOption = server && deployment.adapter ? `
|
|
3500
3565
|
adapter: ${adapterExpr},` : "";
|
|
3566
|
+
const sessionOption = resolveSessionOption(deployment);
|
|
3501
3567
|
const siteOption = deployment.site ? `
|
|
3502
3568
|
site: ${JSON.stringify(deployment.site)},` : "";
|
|
3503
3569
|
const baseOption = deployment.base ? `
|
|
@@ -3519,7 +3585,10 @@ var astroConfigTemplate = (options) => {
|
|
|
3519
3585
|
const fontEntries = buildFontEntries(config.theme.fonts);
|
|
3520
3586
|
const fontsOption = fontEntries.length ? `
|
|
3521
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(", ")}],` : "";
|
|
3522
|
-
const defineConfigImport =
|
|
3588
|
+
const defineConfigImport = astroConfigImportLine({
|
|
3589
|
+
hasFonts: fontEntries.length > 0,
|
|
3590
|
+
hasSession: sessionOption.length > 0
|
|
3591
|
+
});
|
|
3523
3592
|
const reactImport = needsReact ? `import react from "@astrojs/react";
|
|
3524
3593
|
` : "";
|
|
3525
3594
|
const vueImport = needsVue ? `import vue from "@astrojs/vue";
|
|
@@ -3554,19 +3623,26 @@ var astroConfigTemplate = (options) => {
|
|
|
3554
3623
|
integrations.push("svelte()");
|
|
3555
3624
|
}
|
|
3556
3625
|
integrations.push(`blumeIntegration(${JSON.stringify({ base: deployment.base, contentRoutes, pages })})`);
|
|
3626
|
+
const watchOption = devWatchOption(context.outDir, options.contentWatchesRuntimeDir);
|
|
3627
|
+
const {
|
|
3628
|
+
configSourceMarker,
|
|
3629
|
+
userConfigImports,
|
|
3630
|
+
userConfigSetup,
|
|
3631
|
+
userIntegrationSpread
|
|
3632
|
+
} = renderIntegrationBridge(options.integrationBridge);
|
|
3557
3633
|
return `// Generated by Blume. Do not edit; this file is recreated on each run.
|
|
3558
|
-
${defineConfigImport}
|
|
3634
|
+
${configSourceMarker}${userConfigImports}${defineConfigImport}
|
|
3559
3635
|
import mdx from "@astrojs/mdx";
|
|
3560
3636
|
import tailwindcss from "@tailwindcss/vite";
|
|
3561
3637
|
import { blumeMarkdownProcessor, blumeMdxProcessor, blumeShikiTransformers, blumeTwoslashTransformer } from "blume/markdown";
|
|
3562
3638
|
${reactImport}${vueImport}${svelteImport}${blumeImport}${adapterImport}
|
|
3563
|
-
export default defineConfig({
|
|
3639
|
+
${userConfigSetup}export default defineConfig({
|
|
3564
3640
|
root: ${JSON.stringify(context.outDir)},
|
|
3565
3641
|
srcDir: ${JSON.stringify(`${context.outDir}/src`)},
|
|
3566
3642
|
outDir: ${JSON.stringify(astroOutDir(context))},
|
|
3567
3643
|
publicDir: ${JSON.stringify(`${context.root}/public`)},
|
|
3568
|
-
output: ${JSON.stringify(deployment.output)},${adapterOption}${siteOption}${baseOption}${redirectsOption}${i18nOption}${fontsOption}
|
|
3569
|
-
integrations: [${integrations.join(", ")}],
|
|
3644
|
+
output: ${JSON.stringify(deployment.output)},${adapterOption}${sessionOption}${siteOption}${baseOption}${redirectsOption}${i18nOption}${fontsOption}
|
|
3645
|
+
integrations: [${integrations.join(", ")}${userIntegrationSpread}],
|
|
3570
3646
|
markdown: {
|
|
3571
3647
|
processor: blumeMarkdownProcessor(${JSON.stringify({
|
|
3572
3648
|
basePath: config.basePath,
|
|
@@ -3586,18 +3662,25 @@ export default defineConfig({
|
|
|
3586
3662
|
devToolbar: { enabled: false },
|
|
3587
3663
|
vite: {
|
|
3588
3664
|
plugins: [tailwindcss(), prerenderDepsPlugin(), serverAppResolvePlugin()],
|
|
3589
|
-
//
|
|
3590
|
-
// CJS (\`dayjs/dayjs.min.js\`)
|
|
3591
|
-
//
|
|
3592
|
-
//
|
|
3593
|
-
//
|
|
3594
|
-
//
|
|
3595
|
-
//
|
|
3596
|
-
//
|
|
3597
|
-
//
|
|
3598
|
-
//
|
|
3599
|
-
//
|
|
3600
|
-
|
|
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
|
+
},
|
|
3601
3684
|
// Blume's render-time deps are forced external on both build environments so
|
|
3602
3685
|
// native bindings resolve at runtime and isolated linkers don't bundle
|
|
3603
3686
|
// symlinked store copies (which would surface their children as unresolvable
|
|
@@ -3628,22 +3711,17 @@ export default defineConfig({
|
|
|
3628
3711
|
server: {
|
|
3629
3712
|
fs: {
|
|
3630
3713
|
allow: ${JSON.stringify(fsAllow)},
|
|
3631
|
-
}
|
|
3632
|
-
// Keep the file watcher out of Astro's own cache dir. In a migrated
|
|
3633
|
-
// (root-rooted) project the docs collection is rooted at the project dir,
|
|
3634
|
-
// so its glob-loader watcher would otherwise fire on every write Astro
|
|
3635
|
-
// makes under .blume/.astro (data-store.json, content module manifests,
|
|
3636
|
-
// self-hosted fonts) -- pure noise the loader logs as "No entry type
|
|
3637
|
-
// found". Vite appends this to its default ignores.
|
|
3638
|
-
watch: {
|
|
3639
|
-
ignored: ${JSON.stringify([join8(context.outDir, ".astro", "**")])},
|
|
3640
|
-
},
|
|
3714
|
+
},${watchOption}
|
|
3641
3715
|
},
|
|
3642
3716
|
},
|
|
3643
3717
|
});
|
|
3644
3718
|
`;
|
|
3645
3719
|
};
|
|
3646
3720
|
var stagedContentDir = (outDir) => join8(outDir, "content");
|
|
3721
|
+
var runtimeDirWithin = (base, outDir) => {
|
|
3722
|
+
const rel = relative5(base, outDir);
|
|
3723
|
+
return rel && !rel.startsWith("..") && !isAbsolute(rel) ? rel : null;
|
|
3724
|
+
};
|
|
3647
3725
|
var astroGlobBase = (base) => isAbsolute(base) ? pathToFileURL(base).href : base;
|
|
3648
3726
|
var contentConfigTemplate = (options) => {
|
|
3649
3727
|
const { context, config } = options;
|
|
@@ -3651,8 +3729,8 @@ var contentConfigTemplate = (options) => {
|
|
|
3651
3729
|
const collectionBase = options.collection?.base ?? context.contentRoot;
|
|
3652
3730
|
const includeGlobs = options.collection?.include ?? config.content.include;
|
|
3653
3731
|
const excludeGlobs = options.collection?.exclude ?? config.content.exclude;
|
|
3654
|
-
const outDirRel =
|
|
3655
|
-
const outDirIgnore = outDirRel
|
|
3732
|
+
const outDirRel = runtimeDirWithin(collectionBase, context.outDir);
|
|
3733
|
+
const outDirIgnore = outDirRel ? [`!${outDirRel}/**`] : [];
|
|
3656
3734
|
const filesystem = options.filesystem ?? true;
|
|
3657
3735
|
const docsPattern = filesystem ? [
|
|
3658
3736
|
...includeGlobs,
|
|
@@ -3796,7 +3874,11 @@ import data from "blume:data";
|
|
|
3796
3874
|
const { strings } = Astro.props;
|
|
3797
3875
|
---
|
|
3798
3876
|
|
|
3799
|
-
<AskAI
|
|
3877
|
+
<AskAI
|
|
3878
|
+
endpoint={data.config.ask?.endpoint ?? undefined}
|
|
3879
|
+
strings={strings ?? data.ui.ask}
|
|
3880
|
+
suggestions={data.config.ask?.suggestions ?? []}
|
|
3881
|
+
/>
|
|
3800
3882
|
` : `---
|
|
3801
3883
|
// Generated by Blume. Do not edit.
|
|
3802
3884
|
// Ask AI is off (\`ai.ask.enabled\`), so the header's Ask trigger renders nothing.
|
|
@@ -4078,6 +4160,7 @@ const htmlLang = i18n ? i18n.defaultLocale : "en";
|
|
|
4078
4160
|
favicon={data.config.favicon}
|
|
4079
4161
|
appleIcon={data.config.appleIcon}
|
|
4080
4162
|
navigation={data.navigation}
|
|
4163
|
+
noindex={${options.noindex === true}}
|
|
4081
4164
|
pageTitle={${JSON.stringify(options.title)}}
|
|
4082
4165
|
route={${JSON.stringify(options.route)}}
|
|
4083
4166
|
searchEnabled={data.config.search.enabled}
|
|
@@ -5125,6 +5208,10 @@ var crawlStaticDir = async (options) => {
|
|
|
5125
5208
|
};
|
|
5126
5209
|
|
|
5127
5210
|
// src/audit/redirects.ts
|
|
5211
|
+
var pathOnly = (value) => {
|
|
5212
|
+
const cut = value.search(/[?#]/u);
|
|
5213
|
+
return cut === -1 ? value : value.slice(0, cut);
|
|
5214
|
+
};
|
|
5128
5215
|
var resolveRedirects = (redirects, pageUrls) => {
|
|
5129
5216
|
const byFrom = new Map;
|
|
5130
5217
|
for (const redirect of redirects) {
|
|
@@ -5140,7 +5227,7 @@ var resolveRedirects = (redirects, pageUrls) => {
|
|
|
5140
5227
|
chain.push(current);
|
|
5141
5228
|
break;
|
|
5142
5229
|
}
|
|
5143
|
-
const next = normalizePath(current);
|
|
5230
|
+
const next = normalizePath(pathOnly(current));
|
|
5144
5231
|
if (seen.has(next)) {
|
|
5145
5232
|
chain.push(next);
|
|
5146
5233
|
return {
|
|
@@ -5222,8 +5309,9 @@ var matches = (id, terms) => {
|
|
|
5222
5309
|
var runAudit = async (options) => {
|
|
5223
5310
|
const { project } = options;
|
|
5224
5311
|
const staticDir = deployStaticDir(project.config, project.context);
|
|
5312
|
+
const basePath = normalizeBasePath(project.config.basePath);
|
|
5225
5313
|
const crawl = await crawlStaticDir({
|
|
5226
|
-
basePath
|
|
5314
|
+
basePath,
|
|
5227
5315
|
manifest: project.manifest,
|
|
5228
5316
|
staticDir
|
|
5229
5317
|
});
|
|
@@ -5240,7 +5328,11 @@ var runAudit = async (options) => {
|
|
|
5240
5328
|
origin,
|
|
5241
5329
|
pages: crawl.pages,
|
|
5242
5330
|
project,
|
|
5243
|
-
redirects: resolveRedirects(project.config.redirects
|
|
5331
|
+
redirects: resolveRedirects(project.config.redirects.map((redirect) => ({
|
|
5332
|
+
...redirect,
|
|
5333
|
+
from: withBasePath(basePath, redirect.from),
|
|
5334
|
+
to: withBasePath(basePath, redirect.to)
|
|
5335
|
+
})), new Set([...byUrl.keys(), ...crawl.files.keys()].map((path) => normalizePath(path)))),
|
|
5244
5336
|
robots: crawl.robots,
|
|
5245
5337
|
sitemap: crawl.sitemap,
|
|
5246
5338
|
sources: await readSources(crawl.pages),
|
|
@@ -7490,6 +7582,9 @@ var searchMetaSchema = z2.strictObject({
|
|
|
7490
7582
|
exclude: z2.boolean().default(false),
|
|
7491
7583
|
tags: z2.array(z2.string()).optional()
|
|
7492
7584
|
});
|
|
7585
|
+
var aiMetaSchema = z2.strictObject({
|
|
7586
|
+
exclude: z2.boolean().default(false)
|
|
7587
|
+
});
|
|
7493
7588
|
var changelogMetaSchema = z2.strictObject({
|
|
7494
7589
|
category: z2.string().optional(),
|
|
7495
7590
|
date: dateSchema.optional(),
|
|
@@ -7505,6 +7600,7 @@ var authorSchema = z2.union([
|
|
|
7505
7600
|
}).catchall(z2.unknown())
|
|
7506
7601
|
]);
|
|
7507
7602
|
var pageMetaBaseSchema = z2.strictObject({
|
|
7603
|
+
ai: aiMetaSchema.default({}),
|
|
7508
7604
|
authors: z2.union([authorSchema, z2.array(authorSchema)]).optional(),
|
|
7509
7605
|
changelog: changelogMetaSchema.optional(),
|
|
7510
7606
|
date: dateSchema.optional(),
|
|
@@ -7638,6 +7734,7 @@ var contentConfigSchema = z2.strictObject({
|
|
|
7638
7734
|
sources: z2.array(contentSourceSchema).optional()
|
|
7639
7735
|
});
|
|
7640
7736
|
var navTabSchema = z2.strictObject({
|
|
7737
|
+
href: z2.string().min(1).optional(),
|
|
7641
7738
|
icon: iconName.optional(),
|
|
7642
7739
|
items: z2.array(z2.strictObject({
|
|
7643
7740
|
description: z2.string().optional(),
|
|
@@ -7782,11 +7879,25 @@ var mcpConfigSchema = z2.strictObject({
|
|
|
7782
7879
|
name: z2.string().optional(),
|
|
7783
7880
|
route: z2.string().default("/mcp").transform(normalizeRoute)
|
|
7784
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
|
+
});
|
|
7785
7895
|
var aiConfigSchema = z2.strictObject({
|
|
7786
7896
|
ask: z2.strictObject({
|
|
7787
7897
|
apiKeyEnv: z2.string().optional(),
|
|
7788
7898
|
baseUrl: z2.string().url().optional(),
|
|
7789
7899
|
enabled: z2.boolean().default(false),
|
|
7900
|
+
endpoint: askEndpointSchema.optional(),
|
|
7790
7901
|
model: z2.string().default("openai/gpt-5.5"),
|
|
7791
7902
|
provider: z2.enum(askAiProviders).default("gateway"),
|
|
7792
7903
|
suggestions: z2.array(z2.strictObject({
|
|
@@ -7794,7 +7905,7 @@ var aiConfigSchema = z2.strictObject({
|
|
|
7794
7905
|
label: z2.string().min(1)
|
|
7795
7906
|
})).default([])
|
|
7796
7907
|
}).superRefine((value, ctx) => {
|
|
7797
|
-
if (value.provider === "openai-compatible" && !value.baseUrl) {
|
|
7908
|
+
if (value.provider === "openai-compatible" && !(value.baseUrl || value.endpoint)) {
|
|
7798
7909
|
ctx.addIssue({
|
|
7799
7910
|
code: z2.ZodIssueCode.custom,
|
|
7800
7911
|
message: 'ai.ask.baseUrl is required when provider is "openai-compatible".',
|
|
@@ -8022,7 +8133,10 @@ var reactConfigSchema = z2.strictObject({
|
|
|
8022
8133
|
compiler: z2.boolean().default(true)
|
|
8023
8134
|
});
|
|
8024
8135
|
var openapiSourceSchema = z2.strictObject({
|
|
8136
|
+
includeInLlms: z2.boolean().default(true),
|
|
8137
|
+
includeInSearch: z2.boolean().default(true),
|
|
8025
8138
|
label: z2.string().optional(),
|
|
8139
|
+
noindex: z2.boolean().default(false),
|
|
8026
8140
|
route: z2.string().optional(),
|
|
8027
8141
|
spec: z2.string()
|
|
8028
8142
|
});
|
|
@@ -8095,6 +8209,7 @@ var blumeConfigSchema = z2.strictObject({
|
|
|
8095
8209
|
frontmatter: frontmatterConfigSchema.default({}),
|
|
8096
8210
|
github: githubConfigSchema.optional(),
|
|
8097
8211
|
i18n: i18nConfigSchema.optional(),
|
|
8212
|
+
integrations: z2.array(z2.custom()).default([]),
|
|
8098
8213
|
lastModified: lastModifiedConfigSchema.default(false),
|
|
8099
8214
|
logo: logoConfigSchema.optional(),
|
|
8100
8215
|
markdown: markdownConfigSchema.default({}),
|
|
@@ -8191,9 +8306,10 @@ var detectLocale = (parts, i18n) => {
|
|
|
8191
8306
|
var localePlacement = (rel, ext, i18n) => {
|
|
8192
8307
|
const base = rel.slice(0, rel.length - ext.length);
|
|
8193
8308
|
if (base.endsWith(".$")) {
|
|
8309
|
+
const shared = `${base.slice(0, -2)}${ext}`;
|
|
8194
8310
|
return {
|
|
8195
8311
|
locales: i18n.locales.map((locale2) => locale2.code),
|
|
8196
|
-
navPath:
|
|
8312
|
+
navPath: i18n.parser === "dir" ? detectLocale(shared.split("/"), i18n).rest.join("/") : shared
|
|
8197
8313
|
};
|
|
8198
8314
|
}
|
|
8199
8315
|
if (i18n.parser === "dot") {
|
|
@@ -8673,7 +8789,8 @@ var normalizeRef = (ref) => {
|
|
|
8673
8789
|
return "/";
|
|
8674
8790
|
}
|
|
8675
8791
|
const withSlash = ref.startsWith("/") ? ref : `/${ref}`;
|
|
8676
|
-
const
|
|
8792
|
+
const noTrailing = withSlash.replace(/\/+$/u, "");
|
|
8793
|
+
const trimmed = noTrailing.endsWith("/index") ? noTrailing.slice(0, -"/index".length) : noTrailing;
|
|
8677
8794
|
return trimmed === "" ? "/" : trimmed;
|
|
8678
8795
|
};
|
|
8679
8796
|
var routeForRef = (ref, byRoute, basePath) => {
|
|
@@ -8768,7 +8885,7 @@ var resolveTabHref = (sidebar, path) => {
|
|
|
8768
8885
|
return walk(sidebar) ? path : first ?? path;
|
|
8769
8886
|
};
|
|
8770
8887
|
var withTabHrefs = (tabs, sidebar) => tabs.map((tab) => {
|
|
8771
|
-
const href = resolveTabHref(sidebar, tab.path);
|
|
8888
|
+
const href = tab.href ?? resolveTabHref(sidebar, tab.path);
|
|
8772
8889
|
return href === tab.path ? tab : { ...tab, href };
|
|
8773
8890
|
});
|
|
8774
8891
|
var buildNavigation = (pages, options) => {
|
|
@@ -8790,6 +8907,7 @@ var buildNavigation = (pages, options) => {
|
|
|
8790
8907
|
})) : options.selectors ?? [];
|
|
8791
8908
|
const tabs = basePath ? (options.tabs ?? []).map((tab) => ({
|
|
8792
8909
|
...tab,
|
|
8910
|
+
...tab.href ? { href: withBasePath(basePath, tab.href) } : {},
|
|
8793
8911
|
items: tab.items?.map(rebasePath),
|
|
8794
8912
|
path: withBasePath(basePath, tab.path)
|
|
8795
8913
|
})) : options.tabs ?? [];
|
|
@@ -8868,6 +8986,7 @@ var buildLocaleNavigation = (code, pages, fallback, fallbackByKey, options, i18n
|
|
|
8868
8986
|
const localizePath = (path) => path.startsWith("/") ? localizeRoute(path, code, i18n) : path;
|
|
8869
8987
|
const tabs = options.navigation.tabs?.map((tab) => ({
|
|
8870
8988
|
...tab,
|
|
8989
|
+
...tab.href ? { href: localizePath(tab.href) } : {},
|
|
8871
8990
|
items: tab.items?.map((item) => ({
|
|
8872
8991
|
...item,
|
|
8873
8992
|
path: localizePath(item.path)
|
|
@@ -8880,7 +8999,10 @@ var buildLocaleNavigation = (code, pages, fallback, fallbackByKey, options, i18n
|
|
|
8880
8999
|
basePath: options.basePath ?? "",
|
|
8881
9000
|
diagnostics,
|
|
8882
9001
|
display: options.navigation.sidebar.display,
|
|
8883
|
-
featured: options.navigation.featured
|
|
9002
|
+
featured: options.navigation.featured?.map((link) => ({
|
|
9003
|
+
...link,
|
|
9004
|
+
href: localizePath(link.href)
|
|
9005
|
+
})),
|
|
8884
9006
|
folderMeta: options.folderMeta,
|
|
8885
9007
|
localizedRoot: localizeRoute("/", code, i18n),
|
|
8886
9008
|
metaPrefix: i18n.parser === "dir" && code !== i18n.defaultLocale ? code : "",
|
|
@@ -9176,6 +9298,7 @@ var WORD_SPLIT2 = /[-_]/u;
|
|
|
9176
9298
|
var stripNumericPrefix = (segment) => segment.replace(NUMERIC_PREFIX2, "");
|
|
9177
9299
|
var groupLabel = (segment) => segment.match(GROUP_FOLDER2)?.groups?.label ?? null;
|
|
9178
9300
|
var slugify2 = (text) => text.toLowerCase().trim().replaceAll(/[^\w\s-]/gu, "").replaceAll(/[\s_]+/gu, "-").replaceAll(/-+/gu, "-").replaceAll(/^-|-$/gu, "");
|
|
9301
|
+
var slugifyPath = (text) => text.split("/").map(slugify2).filter(Boolean).join("/");
|
|
9179
9302
|
var titleCase = (value) => value.split(WORD_SPLIT2).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
9180
9303
|
var addRouteSegment = (part, segments, groups) => {
|
|
9181
9304
|
if (part === "") {
|
|
@@ -9205,11 +9328,18 @@ var mapRoute = (relativePath) => {
|
|
|
9205
9328
|
};
|
|
9206
9329
|
var CODE_FENCE = /^(?<delimiter>```|~~~)/u;
|
|
9207
9330
|
var nextFenceState = (line, fence) => {
|
|
9208
|
-
const
|
|
9331
|
+
const trimmed = line.trimStart();
|
|
9332
|
+
const delimiter = trimmed.match(CODE_FENCE)?.groups?.delimiter;
|
|
9209
9333
|
if (delimiter === undefined) {
|
|
9210
9334
|
return fence;
|
|
9211
9335
|
}
|
|
9212
9336
|
if (fence === null) {
|
|
9337
|
+
if (delimiter === "```") {
|
|
9338
|
+
const run = trimmed.match(/^`+/u)?.[0].length ?? 0;
|
|
9339
|
+
if (trimmed.slice(run).includes("`")) {
|
|
9340
|
+
return fence;
|
|
9341
|
+
}
|
|
9342
|
+
}
|
|
9213
9343
|
return delimiter;
|
|
9214
9344
|
}
|
|
9215
9345
|
return fence === delimiter ? null : fence;
|
|
@@ -9227,6 +9357,9 @@ var linesWithoutFrontMatter = (body) => {
|
|
|
9227
9357
|
if (!/^-{3}\s*$/u.test(lines[0] ?? "")) {
|
|
9228
9358
|
return lines;
|
|
9229
9359
|
}
|
|
9360
|
+
if ((lines[1] ?? "").trim() === "") {
|
|
9361
|
+
return lines;
|
|
9362
|
+
}
|
|
9230
9363
|
const close = lines.findIndex((line, index) => index > 0 && FRONT_MATTER_CLOSE.test(line));
|
|
9231
9364
|
return close === -1 ? lines : lines.slice(close + 1);
|
|
9232
9365
|
};
|
|
@@ -9306,8 +9439,10 @@ var extractHeadings = (body) => {
|
|
|
9306
9439
|
}
|
|
9307
9440
|
return headings;
|
|
9308
9441
|
};
|
|
9309
|
-
var MD_LINK = /\[[^\]]*\]\((?<target>[^)\s]+)(
|
|
9442
|
+
var MD_LINK = /\[(?<label>(?:[^[\]]|\[[^\]]*\])*)\]\((?<target>(?:[^()\s]|\([^()\s]*\))+)(?<title>\s+"[^"]*")?\)/gu;
|
|
9443
|
+
var MD_IMAGE = /!\[[^\]]*\]\((?<target>(?:[^()\s]|\([^()\s]*\))+)(?<title>\s+"[^"]*")?\)/gu;
|
|
9310
9444
|
var INLINE_CODE = /`[^`]*`/gu;
|
|
9445
|
+
var targetOffsetIn = (matched, target, title) => matched.length - 1 - (title?.length ?? 0) - target.length;
|
|
9311
9446
|
var scanLinkLine = (line, lineNumber, fence, links) => {
|
|
9312
9447
|
const next = nextFenceState(line, fence);
|
|
9313
9448
|
if (fence !== null || next !== null) {
|
|
@@ -9319,12 +9454,24 @@ var scanLinkLine = (line, lineNumber, fence, links) => {
|
|
|
9319
9454
|
if (target === undefined || match.index === undefined) {
|
|
9320
9455
|
continue;
|
|
9321
9456
|
}
|
|
9322
|
-
const targetOffset = match
|
|
9457
|
+
const targetOffset = targetOffsetIn(match[0], target, match.groups?.title);
|
|
9323
9458
|
links.push({
|
|
9324
|
-
column: targetOffset + 1,
|
|
9459
|
+
column: match.index + targetOffset + 1,
|
|
9325
9460
|
line: lineNumber,
|
|
9326
9461
|
target
|
|
9327
9462
|
});
|
|
9463
|
+
const label = match[0].slice(0, targetOffset - "](".length);
|
|
9464
|
+
for (const image of label.matchAll(MD_IMAGE)) {
|
|
9465
|
+
const imageTarget = image.groups?.target;
|
|
9466
|
+
if (imageTarget === undefined || image.index === undefined) {
|
|
9467
|
+
continue;
|
|
9468
|
+
}
|
|
9469
|
+
links.push({
|
|
9470
|
+
column: match.index + image.index + targetOffsetIn(image[0], imageTarget, image.groups?.title) + 1,
|
|
9471
|
+
line: lineNumber,
|
|
9472
|
+
target: imageTarget
|
|
9473
|
+
});
|
|
9474
|
+
}
|
|
9328
9475
|
}
|
|
9329
9476
|
return next;
|
|
9330
9477
|
};
|
|
@@ -9598,6 +9745,24 @@ var operationKey = (method, path, operationId) => {
|
|
|
9598
9745
|
return fromId || slugify3(`${method}-${path}`);
|
|
9599
9746
|
};
|
|
9600
9747
|
var isOperation = (value) => typeof value === "object" && value !== null;
|
|
9748
|
+
var tagSlugger = () => {
|
|
9749
|
+
const assigned = new Map;
|
|
9750
|
+
const taken = new Set;
|
|
9751
|
+
return (name) => {
|
|
9752
|
+
const existing = assigned.get(name);
|
|
9753
|
+
if (existing) {
|
|
9754
|
+
return existing;
|
|
9755
|
+
}
|
|
9756
|
+
const base = slugify3(name) || "operations";
|
|
9757
|
+
let slug = base;
|
|
9758
|
+
for (let suffix = 2;taken.has(slug); suffix += 1) {
|
|
9759
|
+
slug = `${base}-${suffix}`;
|
|
9760
|
+
}
|
|
9761
|
+
taken.add(slug);
|
|
9762
|
+
assigned.set(name, slug);
|
|
9763
|
+
return slug;
|
|
9764
|
+
};
|
|
9765
|
+
};
|
|
9601
9766
|
var extractOperations = (document, baseRoute) => {
|
|
9602
9767
|
const operations = [];
|
|
9603
9768
|
const tagOrder = [];
|
|
@@ -9605,6 +9770,7 @@ var extractOperations = (document, baseRoute) => {
|
|
|
9605
9770
|
const tagMeta = new Map((document.tags ?? []).map((tag) => [tag.name, tag.description ?? ""]));
|
|
9606
9771
|
const seen = new Set;
|
|
9607
9772
|
const warnings = [];
|
|
9773
|
+
const slugForTag = tagSlugger();
|
|
9608
9774
|
for (const [path, rawItem] of Object.entries(document.paths ?? {})) {
|
|
9609
9775
|
const item = rawItem;
|
|
9610
9776
|
if (!item) {
|
|
@@ -9620,7 +9786,7 @@ var extractOperations = (document, baseRoute) => {
|
|
|
9620
9786
|
continue;
|
|
9621
9787
|
}
|
|
9622
9788
|
const tag = operation.tags?.[0] ?? UNTAGGED;
|
|
9623
|
-
const tagSlug =
|
|
9789
|
+
const tagSlug = slugForTag(tag);
|
|
9624
9790
|
if (!tagsSeen.has(tag)) {
|
|
9625
9791
|
tagsSeen.add(tag);
|
|
9626
9792
|
tagOrder.push(tag);
|
|
@@ -9647,7 +9813,7 @@ var extractOperations = (document, baseRoute) => {
|
|
|
9647
9813
|
const tags = tagOrder.map((name) => ({
|
|
9648
9814
|
description: tagMeta.get(name) ?? "",
|
|
9649
9815
|
name,
|
|
9650
|
-
slug:
|
|
9816
|
+
slug: slugForTag(name)
|
|
9651
9817
|
}));
|
|
9652
9818
|
return { operations, tags, warnings };
|
|
9653
9819
|
};
|
|
@@ -9797,15 +9963,14 @@ var parseSpec = async (spec, root, options = {}) => {
|
|
|
9797
9963
|
};
|
|
9798
9964
|
|
|
9799
9965
|
// src/openapi/render-mdx.ts
|
|
9800
|
-
var MDX_UNSAFE = /[
|
|
9966
|
+
var MDX_UNSAFE = /[<{}]/gu;
|
|
9801
9967
|
var ENTITIES = {
|
|
9802
9968
|
"<": "<",
|
|
9803
|
-
">": ">",
|
|
9804
9969
|
"{": "{",
|
|
9805
9970
|
"}": "}"
|
|
9806
9971
|
};
|
|
9807
9972
|
var MDX_ESM_KEYWORD = /^(?<keyword>import|export)\b/gmu;
|
|
9808
|
-
var BACKTICK_CODE = /(?<bt>`+)[\s\S]
|
|
9973
|
+
var BACKTICK_CODE = /(?<!`)(?<bt>`+)(?!`)[\s\S]*?(?<!`)\k<bt>(?!`)/gu;
|
|
9809
9974
|
var escapeProse = (text) => text.replace(MDX_UNSAFE, (char) => ENTITIES[char] ?? char).replace(MDX_ESM_KEYWORD, (keyword) => `&#${keyword.codePointAt(0)};${keyword.slice(1)}`);
|
|
9810
9975
|
var mdxSafe = (text) => {
|
|
9811
9976
|
let out = "";
|
|
@@ -9846,23 +10011,30 @@ var operationDescription = (spec, operation) => {
|
|
|
9846
10011
|
var withDescription = (description, component) => description.trim() ? `${mdxSafe(description.trim())}
|
|
9847
10012
|
|
|
9848
10013
|
${component}` : component;
|
|
9849
|
-
var operationMdx = (spec, operation) => {
|
|
10014
|
+
var operationMdx = (spec, operation, reference) => {
|
|
9850
10015
|
const method = operation.method.toUpperCase();
|
|
9851
10016
|
const title = operation.summary || `${method} ${operation.path}`;
|
|
9852
10017
|
const description = operation.description.trim() === operation.summary.trim() ? "" : operation.description;
|
|
9853
10018
|
return {
|
|
9854
10019
|
body: withDescription(description, `<Operation source="${spec.slug}" id="${operation.key}" />`),
|
|
9855
10020
|
data: {
|
|
10021
|
+
...reference?.includeInLlms === false ? { ai: { exclude: true } } : {},
|
|
9856
10022
|
...operation.deprecated ? { deprecated: true } : {},
|
|
9857
|
-
search: {
|
|
9858
|
-
|
|
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
|
+
},
|
|
9859
10031
|
sidebar: { badge: method, label: operation.summary || operation.path },
|
|
9860
10032
|
title,
|
|
9861
10033
|
type: "openapi-operation"
|
|
9862
10034
|
}
|
|
9863
10035
|
};
|
|
9864
10036
|
};
|
|
9865
|
-
var overviewMdx = (spec) => {
|
|
10037
|
+
var overviewMdx = (spec, reference) => {
|
|
9866
10038
|
const operations = Object.values(spec.operations);
|
|
9867
10039
|
const sections = [];
|
|
9868
10040
|
const known = new Set;
|
|
@@ -9904,8 +10076,11 @@ var overviewMdx = (spec) => {
|
|
|
9904
10076
|
|
|
9905
10077
|
`),
|
|
9906
10078
|
data: {
|
|
10079
|
+
...reference?.includeInLlms === false ? { ai: { exclude: true } } : {},
|
|
10080
|
+
...reference?.includeInSearch === false ? { search: { exclude: true } } : {},
|
|
9907
10081
|
seo: {
|
|
9908
|
-
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 } : {}
|
|
9909
10084
|
},
|
|
9910
10085
|
sidebar: { label: "Overview" },
|
|
9911
10086
|
title: apiName(spec)
|
|
@@ -9927,10 +10102,10 @@ var toEntry = (rendered, ref) => {
|
|
|
9927
10102
|
ref
|
|
9928
10103
|
};
|
|
9929
10104
|
};
|
|
9930
|
-
var specEntries = (spec, operations) => {
|
|
9931
|
-
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`));
|
|
9932
10107
|
const base = routeToRef(spec.route);
|
|
9933
|
-
entries.push(toEntry(overviewMdx(spec), base ? `${base}/index.mdx` : "index.mdx"));
|
|
10108
|
+
entries.push(toEntry(overviewMdx(spec, reference), base ? `${base}/index.mdx` : "index.mdx"));
|
|
9934
10109
|
return entries;
|
|
9935
10110
|
};
|
|
9936
10111
|
var openApiSource = (references, ctx) => {
|
|
@@ -9984,7 +10159,7 @@ var openApiSource = (references, ctx) => {
|
|
|
9984
10159
|
}
|
|
9985
10160
|
] : []
|
|
9986
10161
|
],
|
|
9987
|
-
entries: specEntries(spec, operations),
|
|
10162
|
+
entries: specEntries(spec, operations, reference),
|
|
9988
10163
|
slug: reference.slug,
|
|
9989
10164
|
spec
|
|
9990
10165
|
};
|
|
@@ -10417,7 +10592,7 @@ import { join as join15 } from "pathe";
|
|
|
10417
10592
|
// src/core/sources/assets.ts
|
|
10418
10593
|
import { mkdir as mkdir5, writeFile as writeFile5 } from "node:fs/promises";
|
|
10419
10594
|
import { extname as extname4, join as join14 } from "pathe";
|
|
10420
|
-
var
|
|
10595
|
+
var MD_IMAGE2 = /!\[(?<alt>[^\]]*)\]\((?<url>[^)\s]+)\)/gu;
|
|
10421
10596
|
var REMOTE = /^https?:\/\//u;
|
|
10422
10597
|
var SAFE_EXT = /^\.[a-z0-9]+$/iu;
|
|
10423
10598
|
var CODE_FENCE_BLOCK = /^(?<fence>`{3,}|~{3,})[^\n]*\n[\s\S]*?^\k<fence>[^\n]*(?=\n|$)/gmu;
|
|
@@ -10436,7 +10611,7 @@ var materializeAssets = async (markdown, ctx) => {
|
|
|
10436
10611
|
return `\x00blume-fence-${fences.length - 1}\x00`;
|
|
10437
10612
|
});
|
|
10438
10613
|
const urls = new Set;
|
|
10439
|
-
for (const match of masked.matchAll(
|
|
10614
|
+
for (const match of masked.matchAll(MD_IMAGE2)) {
|
|
10440
10615
|
const url = match.groups?.url;
|
|
10441
10616
|
if (url && REMOTE.test(url)) {
|
|
10442
10617
|
urls.add(url);
|
|
@@ -10462,7 +10637,7 @@ var materializeAssets = async (markdown, ctx) => {
|
|
|
10462
10637
|
});
|
|
10463
10638
|
}
|
|
10464
10639
|
}));
|
|
10465
|
-
const rewritten = masked.replaceAll(
|
|
10640
|
+
const rewritten = masked.replaceAll(MD_IMAGE2, (match, alt, url) => {
|
|
10466
10641
|
const local = rewrites.get(url);
|
|
10467
10642
|
return local ? `` : match;
|
|
10468
10643
|
}).replaceAll(FENCE_TOKEN, (token, index) => fences[Number(index)] ?? token);
|
|
@@ -10697,7 +10872,7 @@ ${nested}`;
|
|
|
10697
10872
|
data.sidebar = { order };
|
|
10698
10873
|
}
|
|
10699
10874
|
const slugProp = richToMarkdown(page.properties[props.slug ?? "Slug"]?.rich_text);
|
|
10700
|
-
const slug =
|
|
10875
|
+
const slug = slugifyPath(slugProp || title) || page.id;
|
|
10701
10876
|
return { data, slug };
|
|
10702
10877
|
};
|
|
10703
10878
|
const toEntry2 = async (client, page) => {
|
|
@@ -10904,7 +11079,7 @@ var sanitySource = (options, ctx) => {
|
|
|
10904
11079
|
let snapshot = new Map;
|
|
10905
11080
|
const toEntry2 = (doc) => {
|
|
10906
11081
|
const slugValue = asString(getPath(doc, fields.slug ?? "slug.current")) ?? asString(doc._id) ?? "untitled";
|
|
10907
|
-
const slug =
|
|
11082
|
+
const slug = slugifyPath(slugValue) || slugify2(asString(doc._id) ?? "") || "untitled";
|
|
10908
11083
|
const data = {};
|
|
10909
11084
|
const title = asString(getPath(doc, fields.title ?? "title"));
|
|
10910
11085
|
const description = asString(getPath(doc, fields.description ?? "description"));
|
|
@@ -11264,6 +11439,16 @@ var shouldFail = (result, gate) => {
|
|
|
11264
11439
|
const failing = failingSeverities(gate);
|
|
11265
11440
|
return result.diagnostics.some((d) => failing.has(d.severity));
|
|
11266
11441
|
};
|
|
11442
|
+
var launchAgentCode = async (bin, prompt) => {
|
|
11443
|
+
try {
|
|
11444
|
+
return await launchAgent(bin, prompt);
|
|
11445
|
+
} catch (error) {
|
|
11446
|
+
if (error?.code !== "ENOENT") {
|
|
11447
|
+
throw error;
|
|
11448
|
+
}
|
|
11449
|
+
return WINDOWS_COMMAND_NOT_FOUND;
|
|
11450
|
+
}
|
|
11451
|
+
};
|
|
11267
11452
|
var auditCommand = defineCommand2({
|
|
11268
11453
|
args: {
|
|
11269
11454
|
claude: {
|
|
@@ -11369,12 +11554,7 @@ var auditCommand = defineCommand2({
|
|
|
11369
11554
|
process.stderr.write(` Handing ${count} finding${count === 1 ? "" : "s"} to ${cli.name}…
|
|
11370
11555
|
|
|
11371
11556
|
`);
|
|
11372
|
-
|
|
11373
|
-
try {
|
|
11374
|
-
code = await launchAgent(cli.bin, fixPrompt(report));
|
|
11375
|
-
} catch {
|
|
11376
|
-
code = WINDOWS_COMMAND_NOT_FOUND;
|
|
11377
|
-
}
|
|
11557
|
+
const code = await launchAgentCode(cli.bin, fixPrompt(report));
|
|
11378
11558
|
if (code === WINDOWS_COMMAND_NOT_FOUND) {
|
|
11379
11559
|
logger.error(`${cli.name} (\`${cli.bin}\`) was not found on PATH. Install it with \`${cli.install}\`.`);
|
|
11380
11560
|
process.exit(1);
|
|
@@ -11502,6 +11682,12 @@ var usagePolicy = (signals) => {
|
|
|
11502
11682
|
}
|
|
11503
11683
|
return Object.fromEntries(USAGE_TOKENS.map(([key, token]) => [token, signals[key]]));
|
|
11504
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
|
+
};
|
|
11505
11691
|
var buildAgentReadability = (project) => {
|
|
11506
11692
|
const { config } = project;
|
|
11507
11693
|
if (!config.seo.agentReadability) {
|
|
@@ -11530,7 +11716,7 @@ var buildAgentReadability = (project) => {
|
|
|
11530
11716
|
};
|
|
11531
11717
|
}
|
|
11532
11718
|
if (config.ai.ask?.enabled) {
|
|
11533
|
-
artifacts.askApi =
|
|
11719
|
+
artifacts.askApi = askApiUrl(config.ai.ask.endpoint, site, abs);
|
|
11534
11720
|
}
|
|
11535
11721
|
if (site && config.seo.sitemap) {
|
|
11536
11722
|
artifacts.sitemap = abs("/sitemap.xml");
|
|
@@ -11873,7 +12059,7 @@ var pageUrl = (route, site, base = "") => {
|
|
|
11873
12059
|
const path = withBasePath(base, route);
|
|
11874
12060
|
return encodeURI(site ? `${site.replace(/\/$/u, "")}${path}` : path);
|
|
11875
12061
|
};
|
|
11876
|
-
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"));
|
|
11877
12063
|
var indexedNavigations = (project) => {
|
|
11878
12064
|
const { i18n } = project.config;
|
|
11879
12065
|
if (i18n) {
|
|
@@ -12022,7 +12208,7 @@ var ensureGitignore = async (root, entries) => {
|
|
|
12022
12208
|
// src/core/server-features.ts
|
|
12023
12209
|
var serverFeatures = (config) => {
|
|
12024
12210
|
const features = [];
|
|
12025
|
-
if (config.ai.ask?.enabled) {
|
|
12211
|
+
if (config.ai.ask?.enabled && !config.ai.ask.endpoint) {
|
|
12026
12212
|
features.push("Ask AI");
|
|
12027
12213
|
}
|
|
12028
12214
|
if (config.ai.mcp.enabled) {
|
|
@@ -12572,11 +12758,13 @@ var refuseIfDevRunning = (root, action, options = {}) => {
|
|
|
12572
12758
|
};
|
|
12573
12759
|
|
|
12574
12760
|
// src/astro/generate.ts
|
|
12761
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
12575
12762
|
import { existsSync as existsSync14, readFileSync as readFileSync9, realpathSync } from "node:fs";
|
|
12576
12763
|
import {
|
|
12577
12764
|
lstat,
|
|
12578
12765
|
mkdir as mkdir6,
|
|
12579
12766
|
readFile as readFile14,
|
|
12767
|
+
readlink,
|
|
12580
12768
|
realpath,
|
|
12581
12769
|
rename,
|
|
12582
12770
|
rm as rm2,
|
|
@@ -12585,7 +12773,7 @@ import {
|
|
|
12585
12773
|
} from "node:fs/promises";
|
|
12586
12774
|
import { createRequire as createRequire5 } from "node:module";
|
|
12587
12775
|
import { pathToFileURL as pathToFileURL3 } from "node:url";
|
|
12588
|
-
import { basename as basename3, dirname as dirname9, join as join26, normalize as normalize3, relative as relative13 } from "pathe";
|
|
12776
|
+
import { basename as basename3, dirname as dirname9, join as join26, normalize as normalize3, relative as relative13, resolve as resolve7 } from "pathe";
|
|
12589
12777
|
import { glob as glob7 } from "tinyglobby";
|
|
12590
12778
|
|
|
12591
12779
|
// src/ai/ask-data.ts
|
|
@@ -13397,6 +13585,7 @@ var buildReferenceFiles = async (options) => {
|
|
|
13397
13585
|
...ref.scalar
|
|
13398
13586
|
},
|
|
13399
13587
|
dataImport: `${"../".repeat(depth + 1)}generated/data.json`,
|
|
13588
|
+
noindex: ref.noindex,
|
|
13400
13589
|
route: ref.route,
|
|
13401
13590
|
title: ref.label
|
|
13402
13591
|
}),
|
|
@@ -14485,12 +14674,12 @@ var canResolveFrom = (fromDir, spec) => {
|
|
|
14485
14674
|
return false;
|
|
14486
14675
|
}
|
|
14487
14676
|
};
|
|
14488
|
-
var resolveReactCompiler = (config, needsReact) => {
|
|
14677
|
+
var resolveReactCompiler = (config, needsReact, pkgDir = packageRoot()) => {
|
|
14489
14678
|
if (!(needsReact && config.react.compiler)) {
|
|
14490
14679
|
return null;
|
|
14491
14680
|
}
|
|
14492
14681
|
try {
|
|
14493
|
-
return createRequire5(pathToFileURL3(join26(
|
|
14682
|
+
return createRequire5(pathToFileURL3(join26(pkgDir, "_.js")).href).resolve("babel-plugin-react-compiler");
|
|
14494
14683
|
} catch {
|
|
14495
14684
|
return null;
|
|
14496
14685
|
}
|
|
@@ -14543,6 +14732,11 @@ var linkDepsJunction = async (link, depsDir) => {
|
|
|
14543
14732
|
if (!existing.isSymbolicLink()) {
|
|
14544
14733
|
return;
|
|
14545
14734
|
}
|
|
14735
|
+
try {
|
|
14736
|
+
if (resolve7(dirname9(link), await readlink(link)) === resolve7(depsDir)) {
|
|
14737
|
+
return;
|
|
14738
|
+
}
|
|
14739
|
+
} catch {}
|
|
14546
14740
|
await rm2(link, { force: true });
|
|
14547
14741
|
}
|
|
14548
14742
|
await mkdir6(dirname9(link), { recursive: true });
|
|
@@ -14634,6 +14828,15 @@ var deploymentAdapterWarnings = (deployment, root) => {
|
|
|
14634
14828
|
}
|
|
14635
14829
|
return [];
|
|
14636
14830
|
};
|
|
14831
|
+
var searchProviderWarnings = (provider, root, pkgDir = packageRoot()) => {
|
|
14832
|
+
const warnings = [];
|
|
14833
|
+
for (const dep of searchProviderMeta(provider).runtimeDeps) {
|
|
14834
|
+
if (!(canResolveFrom(root, dep) || canResolveFrom(pkgDir, dep))) {
|
|
14835
|
+
warnings.push(`Search provider "${provider}" needs "${dep}", which isn't installed. Run \`npm install ${dep}\` (or your package manager's equivalent).`);
|
|
14836
|
+
}
|
|
14837
|
+
}
|
|
14838
|
+
return warnings;
|
|
14839
|
+
};
|
|
14637
14840
|
var examplesCssFile = (root, config) => config.examples.css ? join26(root, config.examples.css) : null;
|
|
14638
14841
|
var writeExamplesPreview = async (options) => {
|
|
14639
14842
|
const { config, hasExamples, root, srcDir, write } = options;
|
|
@@ -14673,6 +14876,16 @@ var detectUsesMath = async (root, staged = []) => {
|
|
|
14673
14876
|
const contents = await Promise.all(files.map((file) => readOptional(join26(root, file))));
|
|
14674
14877
|
return [...contents, ...staged].some(containsMath);
|
|
14675
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
|
+
};
|
|
14676
14889
|
var writeIfChanged = async (path, content) => {
|
|
14677
14890
|
let existing = null;
|
|
14678
14891
|
try {
|
|
@@ -14891,11 +15104,15 @@ var buildRuntimeData = (project) => {
|
|
|
14891
15104
|
config: {
|
|
14892
15105
|
analytics: config.analytics ?? null,
|
|
14893
15106
|
appleIcon: resolveAppleIcon(project),
|
|
14894
|
-
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,
|
|
14895
15111
|
banner: resolveBanner(config),
|
|
14896
15112
|
basePath: config.basePath,
|
|
14897
15113
|
codeThemes: config.markdown.codeBlocks.theme,
|
|
14898
15114
|
codeWrap: config.markdown.code.wrap,
|
|
15115
|
+
dateFormat: config.dateFormat,
|
|
14899
15116
|
description: config.description,
|
|
14900
15117
|
favicon: resolveFavicon(project),
|
|
14901
15118
|
feedback: config.feedback,
|
|
@@ -15022,7 +15239,7 @@ var writeMcpFiles = async (project, plan, write) => {
|
|
|
15022
15239
|
};
|
|
15023
15240
|
var writeAskFiles = async (project, srcDir, write) => {
|
|
15024
15241
|
const { ask } = project.config.ai;
|
|
15025
|
-
if (!ask?.enabled) {
|
|
15242
|
+
if (!(ask?.enabled && !ask.endpoint)) {
|
|
15026
15243
|
return;
|
|
15027
15244
|
}
|
|
15028
15245
|
const grounded = ask.provider !== "inkeep";
|
|
@@ -15038,6 +15255,7 @@ var writeNotFoundPage = async (write, srcDir, pages, contentPages) => {
|
|
|
15038
15255
|
}
|
|
15039
15256
|
await write(join26(srcDir, "pages", "404.astro"), notFoundPageTemplate());
|
|
15040
15257
|
};
|
|
15258
|
+
var diagnosticWarning = (diagnostic) => diagnostic.suggestion ? `${diagnostic.message} ${diagnostic.suggestion}` : diagnostic.message;
|
|
15041
15259
|
var buildComponentSlots = async (componentsFile) => {
|
|
15042
15260
|
const analysis = componentsFile ? analyzeComponentOverrides(await readFile14(componentsFile, "utf-8"), componentsFile) : null;
|
|
15043
15261
|
return {
|
|
@@ -15046,6 +15264,7 @@ var buildComponentSlots = async (componentsFile) => {
|
|
|
15046
15264
|
warnings: analysis ? analysis.warnings : []
|
|
15047
15265
|
};
|
|
15048
15266
|
};
|
|
15267
|
+
var contentWatchesRuntimeDir = (hasFilesystemSource, collectionBase, context) => hasFilesystemSource && runtimeDirWithin(collectionBase, context.outDir) !== null;
|
|
15049
15268
|
var generateRuntime = async (project) => {
|
|
15050
15269
|
const { context, config } = project;
|
|
15051
15270
|
const out = context.outDir;
|
|
@@ -15073,6 +15292,7 @@ var generateRuntime = async (project) => {
|
|
|
15073
15292
|
usesMath,
|
|
15074
15293
|
userTheme,
|
|
15075
15294
|
userExamplesCss,
|
|
15295
|
+
integrationBridge,
|
|
15076
15296
|
islandDiscovery,
|
|
15077
15297
|
exampleDiscovery,
|
|
15078
15298
|
componentSlots
|
|
@@ -15082,6 +15302,7 @@ var generateRuntime = async (project) => {
|
|
|
15082
15302
|
detectUsesMath(context.root, staged.values()),
|
|
15083
15303
|
readOptional(context.themeFile),
|
|
15084
15304
|
readOptional(examplesCssFile(context.root, config)),
|
|
15305
|
+
loadIntegrationBridge(config, context),
|
|
15085
15306
|
discoverIslands(context.root),
|
|
15086
15307
|
discoverExamples(context.root, config.examples.source),
|
|
15087
15308
|
buildComponentSlots(context.componentsFile)
|
|
@@ -15106,6 +15327,7 @@ var generateRuntime = async (project) => {
|
|
|
15106
15327
|
pages.push(...mcp.discoveryPages);
|
|
15107
15328
|
const hasStaged = staged.size > 0;
|
|
15108
15329
|
const hasFilesystemSource = project.sources.some((source) => !source.staged);
|
|
15330
|
+
const docsCollection = resolveDocsCollection(config, context);
|
|
15109
15331
|
const [structural] = await Promise.all([
|
|
15110
15332
|
Promise.all([
|
|
15111
15333
|
write(join26(out, "astro.config.mjs"), astroConfigTemplate({
|
|
@@ -15113,10 +15335,12 @@ var generateRuntime = async (project) => {
|
|
|
15113
15335
|
askPath,
|
|
15114
15336
|
config,
|
|
15115
15337
|
contentRoutes: project.manifest.routes.map((route) => route.path),
|
|
15338
|
+
contentWatchesRuntimeDir: contentWatchesRuntimeDir(hasFilesystemSource, docsCollection.base, context),
|
|
15116
15339
|
context,
|
|
15117
15340
|
dataPath,
|
|
15118
15341
|
examplesPath,
|
|
15119
15342
|
examplesThemePath,
|
|
15343
|
+
integrationBridge,
|
|
15120
15344
|
needsReact,
|
|
15121
15345
|
needsSvelte,
|
|
15122
15346
|
needsVue,
|
|
@@ -15130,7 +15354,7 @@ var generateRuntime = async (project) => {
|
|
|
15130
15354
|
write(join26(out, "tsconfig.json"), runtimeTsconfigTemplate()),
|
|
15131
15355
|
write(join26(srcDir, "env.d.ts"), envTemplate()),
|
|
15132
15356
|
write(join26(srcDir, "content.config.ts"), contentConfigTemplate({
|
|
15133
|
-
collection:
|
|
15357
|
+
collection: docsCollection,
|
|
15134
15358
|
config,
|
|
15135
15359
|
context,
|
|
15136
15360
|
filesystem: hasFilesystemSource,
|
|
@@ -15234,18 +15458,12 @@ var generateRuntime = async (project) => {
|
|
|
15234
15458
|
warnings.push(...[
|
|
15235
15459
|
...validateNavTargets(project.graph.navigation, navTargetRoutes),
|
|
15236
15460
|
...validateSearchPopularIcons(config.search.popular)
|
|
15237
|
-
].map(
|
|
15461
|
+
].map(diagnosticWarning));
|
|
15238
15462
|
const knownComponentTags = new Set([
|
|
15239
15463
|
...islandDiscovery.islands.map((island) => island.name),
|
|
15240
15464
|
...overrideTags
|
|
15241
15465
|
]);
|
|
15242
|
-
warnings.push(...validateUsedComponents(project.graph.pages, knownComponentTags, new Set(registry.map((item) => item.name))).map(
|
|
15243
|
-
for (const dep of searchProviderMeta(config.search.provider).runtimeDeps) {
|
|
15244
|
-
if (!(canResolveFrom(context.root, dep) || canResolveFrom(packageRoot(), dep))) {
|
|
15245
|
-
warnings.push(`Search provider "${config.search.provider}" needs "${dep}", which isn't installed. Run \`npm install ${dep}\` (or your package manager's equivalent).`);
|
|
15246
|
-
}
|
|
15247
|
-
}
|
|
15248
|
-
warnings.push(...deploymentAdapterWarnings(config.deployment, context.root), ...islandFrameworkWarnings(frameworks, context.root));
|
|
15466
|
+
warnings.push(...validateUsedComponents(project.graph.pages, knownComponentTags, new Set(registry.map((item) => item.name))).map(diagnosticWarning), ...searchProviderWarnings(config.search.provider, context.root), ...deploymentAdapterWarnings(config.deployment, context.root), ...islandFrameworkWarnings(frameworks, context.root));
|
|
15249
15467
|
if (hasScalarReferences(config)) {
|
|
15250
15468
|
const references = await buildReferenceFiles({
|
|
15251
15469
|
config,
|
|
@@ -15270,7 +15488,7 @@ var generateRuntime = async (project) => {
|
|
|
15270
15488
|
|
|
15271
15489
|
// src/cli/env.ts
|
|
15272
15490
|
import { existsSync as existsSync15, readFileSync as readFileSync10 } from "node:fs";
|
|
15273
|
-
import { dirname as dirname10, join as join27, resolve as
|
|
15491
|
+
import { dirname as dirname10, join as join27, resolve as resolve8 } from "pathe";
|
|
15274
15492
|
var ENV_LINE = /^\s*(?:export\s+)?(?<key>[A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?<value>.*?)\s*$/u;
|
|
15275
15493
|
var DOUBLE_QUOTED2 = /^"(?<body>[\s\S]*)"$/u;
|
|
15276
15494
|
var SINGLE_QUOTED = /^'(?<body>[\s\S]*)'$/u;
|
|
@@ -15322,7 +15540,7 @@ var loadFile = (path) => {
|
|
|
15322
15540
|
} catch {}
|
|
15323
15541
|
};
|
|
15324
15542
|
var loadEnvFiles = (startDir) => {
|
|
15325
|
-
let dir =
|
|
15543
|
+
let dir = resolve8(startDir);
|
|
15326
15544
|
let done = false;
|
|
15327
15545
|
while (!done) {
|
|
15328
15546
|
loadFile(join27(dir, ".env.local"));
|
|
@@ -15348,7 +15566,7 @@ var checkRequiredSecrets = (config) => {
|
|
|
15348
15566
|
suggestion: `Set ${env} in .env.local for local dev, or in your host's environment for production.`
|
|
15349
15567
|
});
|
|
15350
15568
|
};
|
|
15351
|
-
if (config.ai.ask?.enabled) {
|
|
15569
|
+
if (config.ai.ask?.enabled && !config.ai.ask.endpoint) {
|
|
15352
15570
|
const backend = resolveAskBackend(config.ai.ask);
|
|
15353
15571
|
if (backend.kind === "gateway") {
|
|
15354
15572
|
requireSecret("Ask AI (AI Gateway)", "AI_GATEWAY_API_KEY", "on Vercel the gateway can also authenticate via OIDC");
|
|
@@ -15764,6 +15982,7 @@ var checkCommand = defineCommand4({
|
|
|
15764
15982
|
import { watch } from "node:fs";
|
|
15765
15983
|
import { dev } from "astro";
|
|
15766
15984
|
import { defineCommand as defineCommand5 } from "citty";
|
|
15985
|
+
import { basename as basename4, dirname as dirname11 } from "pathe";
|
|
15767
15986
|
|
|
15768
15987
|
// src/astro/integration.ts
|
|
15769
15988
|
var overlayServer = null;
|
|
@@ -15942,17 +16161,26 @@ var devCommand = defineCommand5({
|
|
|
15942
16161
|
if (boundPort !== port) {
|
|
15943
16162
|
runRegenerate();
|
|
15944
16163
|
}
|
|
16164
|
+
const dirTargets = [project.context.pagesRoot].filter((target) => target !== null);
|
|
15945
16165
|
const fileTargets = [
|
|
15946
|
-
project.context.pagesRoot,
|
|
15947
16166
|
project.context.configFile,
|
|
15948
16167
|
project.context.themeFile,
|
|
15949
16168
|
project.context.componentsFile
|
|
15950
16169
|
].filter((target) => target !== null);
|
|
15951
16170
|
const disposers = [
|
|
15952
16171
|
...project.sources.map((source) => source.watch?.(regenerate)),
|
|
15953
|
-
...
|
|
16172
|
+
...dirTargets.map((target) => {
|
|
15954
16173
|
const watcher = watch(target, { recursive: true }, regenerate);
|
|
15955
16174
|
return () => watcher.close();
|
|
16175
|
+
}),
|
|
16176
|
+
...fileTargets.map((target) => {
|
|
16177
|
+
const name = basename4(target);
|
|
16178
|
+
const watcher = watch(dirname11(target), (_event, filename) => {
|
|
16179
|
+
if (!filename || filename === name) {
|
|
16180
|
+
regenerate();
|
|
16181
|
+
}
|
|
16182
|
+
});
|
|
16183
|
+
return () => watcher.close();
|
|
15956
16184
|
})
|
|
15957
16185
|
].filter((dispose) => dispose !== undefined);
|
|
15958
16186
|
const shutdown = async () => {
|
|
@@ -16095,7 +16323,15 @@ var ejectOpenApiData = (project) => {
|
|
|
16095
16323
|
};
|
|
16096
16324
|
var askFiles = async (project, srcDir, genDir) => {
|
|
16097
16325
|
const { ask } = project.config.ai;
|
|
16098
|
-
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 });
|
|
16099
16335
|
return [];
|
|
16100
16336
|
}
|
|
16101
16337
|
const grounded = ask.provider !== "inkeep";
|
|
@@ -16178,6 +16414,7 @@ var examplesPreviewFiles = (srcDir, basePath, hasExamples) => hasExamples ? [
|
|
|
16178
16414
|
path: join31(srcDir, "pages", ...basePath.split("/").filter(Boolean), "blume-examples", "[...path].astro")
|
|
16179
16415
|
}
|
|
16180
16416
|
] : [];
|
|
16417
|
+
var ejectIntegrationBridge = (config, root, configFile) => config.integrations.length > 0 && configFile ? { configFile: toPosix(relative14(root, configFile)) } : undefined;
|
|
16181
16418
|
var eject = async (root) => {
|
|
16182
16419
|
const project = await scanProject(root, { mode: "build" });
|
|
16183
16420
|
const { context, config } = project;
|
|
@@ -16239,6 +16476,7 @@ var eject = async (root) => {
|
|
|
16239
16476
|
dataPath: "./src/generated/data.json",
|
|
16240
16477
|
examplesPath: "./src/generated/examples.ts",
|
|
16241
16478
|
examplesThemePath: "./src/generated/examples.css",
|
|
16479
|
+
integrationBridge: ejectIntegrationBridge(config, root, context.configFile),
|
|
16242
16480
|
needsReact,
|
|
16243
16481
|
needsSvelte,
|
|
16244
16482
|
needsVue,
|
|
@@ -16473,7 +16711,7 @@ var updatePackageScripts = async (root) => {
|
|
|
16473
16711
|
// src/cli/init/scaffold.ts
|
|
16474
16712
|
import { existsSync as existsSync19 } from "node:fs";
|
|
16475
16713
|
import { mkdir as mkdir8, writeFile as writeFile11 } from "node:fs/promises";
|
|
16476
|
-
import { basename as
|
|
16714
|
+
import { basename as basename5, dirname as dirname12, isAbsolute as isAbsolute9, join as join33, relative as relative15 } from "pathe";
|
|
16477
16715
|
|
|
16478
16716
|
// src/core/package-json.ts
|
|
16479
16717
|
var toPackageName = (raw) => raw.toLowerCase().replaceAll(/[^a-z0-9._-]+/gu, "-").replaceAll(/^[-_.]+|[-_.]+$/gu, "") || "docs";
|
|
@@ -16683,7 +16921,7 @@ var extraDepsFor = (sources) => ({
|
|
|
16683
16921
|
var buildPlan = (root, answers) => {
|
|
16684
16922
|
const files = [
|
|
16685
16923
|
{
|
|
16686
|
-
content: blumePackageJson(toPackageName(
|
|
16924
|
+
content: blumePackageJson(toPackageName(basename5(root)), extraDepsFor(answers.sources)),
|
|
16687
16925
|
path: join33(root, "package.json")
|
|
16688
16926
|
},
|
|
16689
16927
|
{ content: buildConfig(answers), path: join33(root, "blume.config.ts") }
|
|
@@ -16698,14 +16936,14 @@ var writeFileSafe = async (file, log) => {
|
|
|
16698
16936
|
log.info(`Skipped existing ${file.path}`);
|
|
16699
16937
|
return false;
|
|
16700
16938
|
}
|
|
16701
|
-
await mkdir8(
|
|
16939
|
+
await mkdir8(dirname12(file.path), { recursive: true });
|
|
16702
16940
|
await writeFile11(file.path, file.content, "utf-8");
|
|
16703
16941
|
log.success(`Created ${file.path}`);
|
|
16704
16942
|
return true;
|
|
16705
16943
|
};
|
|
16706
16944
|
var applyPlan = async (files, log) => {
|
|
16707
16945
|
const created = await Promise.all(files.map((file) => writeFileSafe(file, log)));
|
|
16708
|
-
const createdPackage = files.some((file, index) => created[index] &&
|
|
16946
|
+
const createdPackage = files.some((file, index) => created[index] && basename5(file.path) === "package.json");
|
|
16709
16947
|
return { createdPackage };
|
|
16710
16948
|
};
|
|
16711
16949
|
var envVarsFor = (sources) => [
|
|
@@ -16789,13 +17027,882 @@ The blume package remains importable.`);
|
|
|
16789
17027
|
}
|
|
16790
17028
|
});
|
|
16791
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
|
+
|
|
16792
17899
|
// src/cli/commands/init.ts
|
|
16793
17900
|
import * as clack from "@clack/prompts";
|
|
16794
|
-
import { defineCommand as
|
|
16795
|
-
import { resolve as
|
|
17901
|
+
import { defineCommand as defineCommand9 } from "citty";
|
|
17902
|
+
import { resolve as resolve10 } from "pathe";
|
|
16796
17903
|
|
|
16797
17904
|
// src/cli/init/questions.ts
|
|
16798
|
-
import { basename as
|
|
17905
|
+
import { basename as basename6, resolve as resolve9 } from "pathe";
|
|
16799
17906
|
var cancelled = (value) => typeof value === "symbol";
|
|
16800
17907
|
var collectAnswers = async (prompter, flags, defaults) => {
|
|
16801
17908
|
const directory = flags.directory ?? await prompter.text({
|
|
@@ -16806,9 +17913,9 @@ var collectAnswers = async (prompter, flags, defaults) => {
|
|
|
16806
17913
|
if (cancelled(directory)) {
|
|
16807
17914
|
return null;
|
|
16808
17915
|
}
|
|
16809
|
-
const root =
|
|
17916
|
+
const root = resolve9(defaults.cwd, directory);
|
|
16810
17917
|
const title = await prompter.text({
|
|
16811
|
-
initialValue: titleize(
|
|
17918
|
+
initialValue: titleize(basename6(root)),
|
|
16812
17919
|
message: "What's your docs site called?",
|
|
16813
17920
|
validate: (value) => value?.trim() ? undefined : "Give your docs site a name."
|
|
16814
17921
|
});
|
|
@@ -16906,7 +18013,7 @@ var ejectScaffold = async (root, answers) => {
|
|
|
16906
18013
|
`);
|
|
16907
18014
|
}
|
|
16908
18015
|
};
|
|
16909
|
-
var initCommand =
|
|
18016
|
+
var initCommand = defineCommand9({
|
|
16910
18017
|
args: {
|
|
16911
18018
|
"content-dir": {
|
|
16912
18019
|
description: "Content directory.",
|
|
@@ -16975,14 +18082,18 @@ var initCommand = defineCommand8({
|
|
|
16975
18082
|
title: "My Docs"
|
|
16976
18083
|
};
|
|
16977
18084
|
}
|
|
16978
|
-
const root =
|
|
18085
|
+
const root = resolve10(cwd, answers.directory);
|
|
16979
18086
|
if (validateContentDir(root, answers.contentDir) !== undefined) {
|
|
16980
18087
|
logger.error(`Invalid --content-dir "${answers.contentDir}" (must be a path inside the project).`);
|
|
16981
18088
|
process.exit(1);
|
|
16982
18089
|
}
|
|
16983
18090
|
const sink = interactive ? clack.log : logger;
|
|
16984
18091
|
const { createdPackage } = await applyPlan(buildPlan(root, answers), sink);
|
|
16985
|
-
const ignored = await ensureGitignore(root, [
|
|
18092
|
+
const ignored = await ensureGitignore(root, [
|
|
18093
|
+
"node_modules/",
|
|
18094
|
+
".blume/",
|
|
18095
|
+
"dist/"
|
|
18096
|
+
]);
|
|
16986
18097
|
if (ignored.length > 0) {
|
|
16987
18098
|
sink.success(`Added ${ignored.join(", ")} to .gitignore`);
|
|
16988
18099
|
}
|
|
@@ -17000,12 +18111,223 @@ var initCommand = defineCommand8({
|
|
|
17000
18111
|
}
|
|
17001
18112
|
});
|
|
17002
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
|
+
|
|
17003
18325
|
// src/cli/commands/preview.ts
|
|
17004
|
-
import { existsSync as
|
|
18326
|
+
import { existsSync as existsSync21 } from "node:fs";
|
|
17005
18327
|
import { preview } from "astro";
|
|
17006
|
-
import { defineCommand as
|
|
17007
|
-
import { join as
|
|
17008
|
-
var previewCommand =
|
|
18328
|
+
import { defineCommand as defineCommand11 } from "citty";
|
|
18329
|
+
import { join as join38 } from "pathe";
|
|
18330
|
+
var previewCommand = defineCommand11({
|
|
17009
18331
|
args: {
|
|
17010
18332
|
host: { description: "Network host to bind.", type: "string" },
|
|
17011
18333
|
port: { description: "Port to listen on.", type: "string" }
|
|
@@ -17018,7 +18340,7 @@ var previewCommand = defineCommand9({
|
|
|
17018
18340
|
const root = process.cwd();
|
|
17019
18341
|
const { config } = await loadConfig(root);
|
|
17020
18342
|
const context = resolveProjectContext(root, config);
|
|
17021
|
-
if (!
|
|
18343
|
+
if (!existsSync21(join38(context.outDir, "astro.config.mjs"))) {
|
|
17022
18344
|
logger.error("No build found. Run `blume build` first.");
|
|
17023
18345
|
process.exit(1);
|
|
17024
18346
|
}
|
|
@@ -17035,9 +18357,9 @@ var previewCommand = defineCommand9({
|
|
|
17035
18357
|
|
|
17036
18358
|
// src/cli/commands/sync.ts
|
|
17037
18359
|
import { rm as rm4 } from "node:fs/promises";
|
|
17038
|
-
import { defineCommand as
|
|
17039
|
-
import { join as
|
|
17040
|
-
var syncCommand =
|
|
18360
|
+
import { defineCommand as defineCommand12 } from "citty";
|
|
18361
|
+
import { join as join39 } from "pathe";
|
|
18362
|
+
var syncCommand = defineCommand12({
|
|
17041
18363
|
args: {
|
|
17042
18364
|
force: {
|
|
17043
18365
|
description: "Clear the source cache before refetching.",
|
|
@@ -17058,7 +18380,7 @@ var syncCommand = defineCommand10({
|
|
|
17058
18380
|
if (args.force) {
|
|
17059
18381
|
const { config } = await loadConfig(root);
|
|
17060
18382
|
const context = resolveProjectContext(root, config);
|
|
17061
|
-
await rm4(
|
|
18383
|
+
await rm4(join39(context.outDir, "cache"), { force: true, recursive: true });
|
|
17062
18384
|
logger.info("Cleared source cache.");
|
|
17063
18385
|
}
|
|
17064
18386
|
const lock = readDevLock(resolveRuntimeDir(root));
|
|
@@ -17076,13 +18398,13 @@ var syncCommand = defineCommand10({
|
|
|
17076
18398
|
});
|
|
17077
18399
|
|
|
17078
18400
|
// src/cli/commands/validate.ts
|
|
17079
|
-
import { existsSync as
|
|
17080
|
-
import { defineCommand as
|
|
17081
|
-
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";
|
|
17082
18404
|
|
|
17083
18405
|
// src/core/links.ts
|
|
17084
|
-
import { existsSync as
|
|
17085
|
-
import { basename as
|
|
18406
|
+
import { existsSync as existsSync22 } from "node:fs";
|
|
18407
|
+
import { basename as basename7, join as join40 } from "pathe";
|
|
17086
18408
|
var HTTP = /^https?:\/\//iu;
|
|
17087
18409
|
var PROTOCOL_RELATIVE = /^\/\//u;
|
|
17088
18410
|
var SCHEME = /^[a-z][a-z0-9+.-]*:/iu;
|
|
@@ -17095,9 +18417,9 @@ var decodePercent = (value) => {
|
|
|
17095
18417
|
};
|
|
17096
18418
|
var DOC_EXT = /\.(?:md|mdx)$/iu;
|
|
17097
18419
|
var FILE_EXT = /\.[a-z0-9]+$/iu;
|
|
17098
|
-
var assetIsPresent = (resolved, ctx) => ctx.publicDir !== null &&
|
|
18420
|
+
var assetIsPresent = (resolved, ctx) => ctx.publicDir !== null && existsSync22(join40(ctx.publicDir, resolved));
|
|
17099
18421
|
var NUMERIC_PREFIX3 = /^\d+[-_.]/u;
|
|
17100
|
-
var isIndexPage = (page2) => /^index\.(?:md|mdx)$/iu.test(
|
|
18422
|
+
var isIndexPage = (page2) => /^index\.(?:md|mdx)$/iu.test(basename7(page2.navPath).replace(NUMERIC_PREFIX3, ""));
|
|
17101
18423
|
var applyRelativePart = (segments, part) => {
|
|
17102
18424
|
if (part === "" || part === ".") {
|
|
17103
18425
|
return;
|
|
@@ -17268,7 +18590,7 @@ var validateLinks = async (graph, options) => {
|
|
|
17268
18590
|
};
|
|
17269
18591
|
|
|
17270
18592
|
// src/cli/commands/validate.ts
|
|
17271
|
-
var validateCommand =
|
|
18593
|
+
var validateCommand = defineCommand13({
|
|
17272
18594
|
args: {
|
|
17273
18595
|
external: {
|
|
17274
18596
|
description: "Check external (HTTP) links over the network.",
|
|
@@ -17306,12 +18628,12 @@ var validateCommand = defineCommand11({
|
|
|
17306
18628
|
});
|
|
17307
18629
|
extraRoutes.push(...manifest.routes.flatMap((route) => route.fallback ? [route.path] : []));
|
|
17308
18630
|
}
|
|
17309
|
-
const publicDir =
|
|
18631
|
+
const publicDir = join41(root, "public");
|
|
17310
18632
|
diagnostics.push(...await validateLinks(project.graph, {
|
|
17311
18633
|
basePath: project.config.basePath,
|
|
17312
18634
|
checkExternal: Boolean(args.external),
|
|
17313
18635
|
extraRoutes,
|
|
17314
|
-
publicDir:
|
|
18636
|
+
publicDir: existsSync23(publicDir) ? publicDir : null,
|
|
17315
18637
|
redirects: project.config.redirects
|
|
17316
18638
|
}));
|
|
17317
18639
|
} catch (error) {
|
|
@@ -17342,7 +18664,7 @@ var validateCommand = defineCommand11({
|
|
|
17342
18664
|
});
|
|
17343
18665
|
|
|
17344
18666
|
// src/cli/index.ts
|
|
17345
|
-
var main =
|
|
18667
|
+
var main = defineCommand14({
|
|
17346
18668
|
meta: {
|
|
17347
18669
|
description: "Markdown-first documentation powered by Astro and Vite.",
|
|
17348
18670
|
name: "blume",
|
|
@@ -17356,7 +18678,9 @@ var main = defineCommand12({
|
|
|
17356
18678
|
dev: devCommand,
|
|
17357
18679
|
doctor: doctorCommand,
|
|
17358
18680
|
eject: ejectCommand,
|
|
18681
|
+
eval: evalCommand,
|
|
17359
18682
|
init: initCommand,
|
|
18683
|
+
"mcp-stdio": mcpStdioCommand,
|
|
17360
18684
|
preview: previewCommand,
|
|
17361
18685
|
sync: syncCommand,
|
|
17362
18686
|
validate: validateCommand
|
|
@@ -17373,5 +18697,5 @@ process.on("unhandledRejection", (error) => {
|
|
|
17373
18697
|
});
|
|
17374
18698
|
runMain(main);
|
|
17375
18699
|
|
|
17376
|
-
//# debugId=
|
|
18700
|
+
//# debugId=E4DFD2C6131D546564756E2164756E21
|
|
17377
18701
|
//# sourceMappingURL=index.js.map
|