blume 1.7.1 → 1.7.3
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 +18 -0
- package/dist/cli/chunk-0qymqwzz.js +164 -0
- package/dist/cli/chunk-0qymqwzz.js.map +15 -0
- package/dist/cli/{chunk-8gnpdsn1.js → chunk-0xjyb285.js} +2 -2
- package/dist/cli/{chunk-12dzsn9b.js → chunk-1jefwnfs.js} +82 -81
- package/dist/cli/{chunk-12dzsn9b.js.map → chunk-1jefwnfs.js.map} +3 -3
- package/dist/cli/{chunk-27gtm2ym.js → chunk-2mzebbbz.js} +1 -1
- package/dist/cli/{chunk-s5e5jt53.js → chunk-2z47ypj8.js} +1 -1
- package/dist/cli/{chunk-jtb45atp.js → chunk-3r45185y.js} +10 -12
- package/dist/cli/{chunk-jtb45atp.js.map → chunk-3r45185y.js.map} +2 -2
- package/dist/cli/{chunk-6mq7qkve.js → chunk-4x36ddpw.js} +18 -24
- package/dist/cli/{chunk-6mq7qkve.js.map → chunk-4x36ddpw.js.map} +2 -2
- package/dist/cli/{chunk-he2zfgah.js → chunk-5093q3n7.js} +22 -30
- package/dist/cli/{chunk-he2zfgah.js.map → chunk-5093q3n7.js.map} +2 -2
- package/dist/cli/{chunk-k0v1f8bb.js → chunk-5g0w1e2c.js} +21 -28
- package/dist/cli/{chunk-k0v1f8bb.js.map → chunk-5g0w1e2c.js.map} +2 -2
- package/dist/cli/{chunk-5gfw0q4j.js → chunk-5qk08vmp.js} +26 -34
- package/dist/cli/{chunk-5gfw0q4j.js.map → chunk-5qk08vmp.js.map} +2 -2
- package/dist/cli/{chunk-r99hynxh.js → chunk-7s8hm3b6.js} +41 -9
- package/dist/cli/{chunk-r99hynxh.js.map → chunk-7s8hm3b6.js.map} +3 -3
- package/dist/cli/{chunk-vyqj481z.js → chunk-8cjtbafj.js} +68 -66
- package/dist/cli/chunk-8cjtbafj.js.map +13 -0
- package/dist/cli/{chunk-aqjvpd03.js → chunk-97r59kpr.js} +27 -33
- package/dist/cli/{chunk-aqjvpd03.js.map → chunk-97r59kpr.js.map} +2 -2
- package/dist/cli/{chunk-np8dmfb0.js → chunk-ahnw3kxw.js} +26 -33
- package/dist/cli/{chunk-np8dmfb0.js.map → chunk-ahnw3kxw.js.map} +2 -2
- package/dist/cli/{chunk-j5f2wrj5.js → chunk-b27xqwn9.js} +10 -15
- package/dist/cli/{chunk-j5f2wrj5.js.map → chunk-b27xqwn9.js.map} +2 -2
- package/dist/cli/{chunk-kmx2mydj.js → chunk-bf6bt1xt.js} +8 -8
- package/dist/cli/{chunk-kmx2mydj.js.map → chunk-bf6bt1xt.js.map} +1 -1
- package/dist/cli/{chunk-90pdhkpm.js → chunk-bvwwhd84.js} +23 -32
- package/dist/cli/{chunk-90pdhkpm.js.map → chunk-bvwwhd84.js.map} +2 -2
- package/dist/cli/{chunk-mfm4sjwx.js → chunk-cjtn640a.js} +32 -43
- package/dist/cli/{chunk-mfm4sjwx.js.map → chunk-cjtn640a.js.map} +2 -2
- package/dist/cli/{chunk-pxj10x8y.js → chunk-ct47dqpx.js} +14 -3
- package/dist/cli/{chunk-pxj10x8y.js.map → chunk-ct47dqpx.js.map} +4 -3
- package/dist/cli/{chunk-x66c5yjn.js → chunk-dwgcp5sm.js} +2 -2
- package/dist/cli/{chunk-4trphnvy.js → chunk-e7f42gdj.js} +10 -13
- package/dist/cli/{chunk-4trphnvy.js.map → chunk-e7f42gdj.js.map} +2 -2
- package/dist/cli/{chunk-82atea4k.js → chunk-esphfr8p.js} +14 -18
- package/dist/cli/{chunk-82atea4k.js.map → chunk-esphfr8p.js.map} +2 -2
- package/dist/cli/{chunk-q56730e0.js → chunk-ex56aa81.js} +53 -53
- package/dist/cli/chunk-ex56aa81.js.map +13 -0
- package/dist/cli/{chunk-ywn7t0pb.js → chunk-garjf5z9.js} +3 -3
- package/dist/cli/{chunk-ev67ycx0.js → chunk-jq5n4avg.js} +1 -1
- package/dist/cli/{chunk-ka5k7cz9.js → chunk-js7saxwm.js} +35 -39
- package/dist/cli/{chunk-ka5k7cz9.js.map → chunk-js7saxwm.js.map} +4 -6
- package/dist/cli/{chunk-x1wvw7a8.js → chunk-k79xp7av.js} +168 -208
- package/dist/cli/chunk-k79xp7av.js.map +39 -0
- package/dist/cli/{chunk-3r94j3tc.js → chunk-nn13znc2.js} +2 -2
- package/dist/cli/{chunk-4ae4f395.js → chunk-ps4m1xh4.js} +60 -35
- package/dist/cli/chunk-ps4m1xh4.js.map +15 -0
- package/dist/cli/{chunk-wd27zjcz.js → chunk-q4rae3bg.js} +1 -1
- package/dist/cli/{chunk-pdwg3q9g.js → chunk-rqy0s5wh.js} +21 -30
- package/dist/cli/{chunk-pdwg3q9g.js.map → chunk-rqy0s5wh.js.map} +2 -2
- package/dist/cli/{chunk-52cwcqvp.js → chunk-rz9jmfhz.js} +15 -24
- package/dist/cli/{chunk-52cwcqvp.js.map → chunk-rz9jmfhz.js.map} +2 -2
- package/dist/cli/{chunk-8p3xe5jv.js → chunk-vacwm2hv.js} +3 -3
- package/dist/cli/{chunk-cbjnx4s8.js → chunk-vh9w1sgp.js} +1 -1
- package/dist/cli/{chunk-sbdqrjbb.js → chunk-vrfp10qk.js} +1 -1
- package/dist/cli/{chunk-h9ekmtz7.js → chunk-yg63d42r.js} +28 -35
- package/dist/cli/{chunk-h9ekmtz7.js.map → chunk-yg63d42r.js.map} +2 -2
- package/dist/cli/{chunk-5hs6gb7n.js → chunk-yzhm0j9q.js} +1 -1
- package/dist/cli/index.js +397 -34
- package/dist/cli/index.js.map +12 -4
- package/dist/types/core/config-input.d.ts +59 -0
- package/dist/types/core/data.d.ts +2 -0
- package/dist/types/core/schema.d.ts +47 -3
- package/dist/types/core/types.d.ts +5 -0
- package/docs/configuration/ask-ai.mdx +61 -0
- package/docs/configuration/index.mdx +3 -1
- package/docs/content/navigation.mdx +3 -0
- package/docs/content/syntax.mdx +10 -0
- package/docs/discoverability/agent-discovery.mdx +82 -1
- package/docs/discoverability/index.mdx +1 -1
- package/docs/discoverability/llms-txt.mdx +1 -1
- package/docs/reference/frontmatter.mdx +2 -0
- package/package.json +1 -1
- package/src/ai/agent-readability.ts +5 -0
- package/src/ai/ai-catalog.ts +241 -0
- package/src/ai/cors.ts +87 -0
- package/src/ai/link-headers.ts +12 -0
- package/src/ai/llms.ts +6 -0
- package/src/ai/mcp/discovery.ts +1 -1
- package/src/astro/generate.ts +4 -0
- package/src/astro/templates.ts +88 -23
- package/src/cli/commands/build.ts +3 -1
- package/src/components/islands/hooks.ts +50 -1
- package/src/components/layout/NavTree.astro +75 -66
- package/src/components/layout/RootLayout.astro +28 -2
- package/src/components/layout/analytics-client.ts +36 -7
- package/src/components/layout/nav-utils.ts +17 -3
- package/src/core/adapter.ts +61 -0
- package/src/core/config-input.ts +60 -0
- package/src/core/data.ts +2 -0
- package/src/core/navigation.ts +22 -3
- package/src/core/schema.ts +88 -0
- package/src/core/types.ts +5 -0
- package/src/deploy/artifacts.ts +12 -1
- package/src/deploy/headers.ts +6 -0
- package/src/deploy/vercel-negotiation.ts +25 -2
- package/src/registry/eject.ts +2 -0
- package/src/search/build.ts +25 -3
- package/src/theme/entry.ts +23 -0
- package/dist/cli/chunk-2aj8ddew.js +0 -72
- package/dist/cli/chunk-2aj8ddew.js.map +0 -10
- package/dist/cli/chunk-4ae4f395.js.map +0 -15
- package/dist/cli/chunk-4xyggvgf.js +0 -21
- package/dist/cli/chunk-4xyggvgf.js.map +0 -10
- package/dist/cli/chunk-6kzzpsx8.js +0 -26
- package/dist/cli/chunk-6kzzpsx8.js.map +0 -10
- package/dist/cli/chunk-bcy492zc.js +0 -16
- package/dist/cli/chunk-bcy492zc.js.map +0 -10
- package/dist/cli/chunk-btfr9yvw.js +0 -41
- package/dist/cli/chunk-btfr9yvw.js.map +0 -10
- package/dist/cli/chunk-ey89bjj1.js +0 -209
- package/dist/cli/chunk-ey89bjj1.js.map +0 -11
- package/dist/cli/chunk-q56730e0.js.map +0 -13
- package/dist/cli/chunk-qvvpnwaz.js +0 -69
- package/dist/cli/chunk-qvvpnwaz.js.map +0 -11
- package/dist/cli/chunk-vt8fgygt.js +0 -23
- package/dist/cli/chunk-vt8fgygt.js.map +0 -10
- package/dist/cli/chunk-vxv4x1n8.js +0 -17
- package/dist/cli/chunk-vxv4x1n8.js.map +0 -10
- package/dist/cli/chunk-vyqj481z.js.map +0 -13
- package/dist/cli/chunk-x1wvw7a8.js.map +0 -40
- /package/dist/cli/{chunk-8gnpdsn1.js.map → chunk-0xjyb285.js.map} +0 -0
- /package/dist/cli/{chunk-27gtm2ym.js.map → chunk-2mzebbbz.js.map} +0 -0
- /package/dist/cli/{chunk-s5e5jt53.js.map → chunk-2z47ypj8.js.map} +0 -0
- /package/dist/cli/{chunk-x66c5yjn.js.map → chunk-dwgcp5sm.js.map} +0 -0
- /package/dist/cli/{chunk-ywn7t0pb.js.map → chunk-garjf5z9.js.map} +0 -0
- /package/dist/cli/{chunk-ev67ycx0.js.map → chunk-jq5n4avg.js.map} +0 -0
- /package/dist/cli/{chunk-3r94j3tc.js.map → chunk-nn13znc2.js.map} +0 -0
- /package/dist/cli/{chunk-wd27zjcz.js.map → chunk-q4rae3bg.js.map} +0 -0
- /package/dist/cli/{chunk-8p3xe5jv.js.map → chunk-vacwm2hv.js.map} +0 -0
- /package/dist/cli/{chunk-cbjnx4s8.js.map → chunk-vh9w1sgp.js.map} +0 -0
- /package/dist/cli/{chunk-sbdqrjbb.js.map → chunk-vrfp10qk.js.map} +0 -0
- /package/dist/cli/{chunk-5hs6gb7n.js.map → chunk-yzhm0j9q.js.map} +0 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { normalizeBasePath, withBasePath } from "../core/base-path.ts";
|
|
2
|
+
import type { ResolvedConfig } from "../core/schema.ts";
|
|
3
|
+
import { absoluteUrl } from "../core/site-url.ts";
|
|
4
|
+
import { resolveReferences } from "../openapi/references.ts";
|
|
5
|
+
import { API_CATALOG_PATH, hasApiCatalog } from "./api-catalog.ts";
|
|
6
|
+
import { OPENAPI_PATH } from "./api/paths.ts";
|
|
7
|
+
import { asciiSlugify } from "./mcp/discovery.ts";
|
|
8
|
+
import { AGENT_SKILLS_DIR } from "./skills.ts";
|
|
9
|
+
import type { SkillArtifact } from "./skills.ts";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The AI Catalog (Agent-Card/ai-catalog data model, `specVersion` 1.0) at
|
|
13
|
+
* `/.well-known/ai-catalog.json`, which is also the Agentic Resource
|
|
14
|
+
* Discovery (ARD) manifest: one entry per agent-facing resource the site
|
|
15
|
+
* publishes, each an `urn:air:<host>:<namespace>:<name>` identifier, a
|
|
16
|
+
* display name, the artifact's media type, a URL, and the
|
|
17
|
+
* `representativeQueries` registries embed for semantic search. Like the
|
|
18
|
+
* RFC 9727 catalog next to it, everything is derived from `blume.config.ts`
|
|
19
|
+
* and the build's published skills — nothing is hand-written.
|
|
20
|
+
*
|
|
21
|
+
* ARD v0.91 moved its manifest to `/.well-known/ard.json` and calls
|
|
22
|
+
* `ai-catalog.json` the predecessor path that consumers only *may* consult,
|
|
23
|
+
* while the ai-catalog spec and today's agent-readiness scanners key on
|
|
24
|
+
* `ai-catalog.json`. The ARD manifest is just an `entries` array (other
|
|
25
|
+
* members are ignored), so the one document is written to both paths.
|
|
26
|
+
*
|
|
27
|
+
* Served as plain `application/json`: the `.json` extension gets it from
|
|
28
|
+
* every static host with no header rule, and the scanners that gate on the
|
|
29
|
+
* file ask for exactly that type. The registered `application/ai-catalog+json`
|
|
30
|
+
* is what the `Link`/`<link>` advertisements declare the document to be.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
export const AI_CATALOG_PATH = "/.well-known/ai-catalog.json";
|
|
34
|
+
export const ARD_MANIFEST_PATH = "/.well-known/ard.json";
|
|
35
|
+
export const AI_CATALOG_TYPE = "application/ai-catalog+json";
|
|
36
|
+
const SPEC_VERSION = "1.0";
|
|
37
|
+
|
|
38
|
+
/** One catalog entry, restricted to the terms Blume emits. */
|
|
39
|
+
interface CatalogEntry {
|
|
40
|
+
identifier: string;
|
|
41
|
+
displayName: string;
|
|
42
|
+
type: string;
|
|
43
|
+
url: string;
|
|
44
|
+
description?: string;
|
|
45
|
+
capabilities?: string[];
|
|
46
|
+
representativeQueries: string[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** An entry before its identifier and queries are resolved against the host. */
|
|
50
|
+
interface EntrySeed {
|
|
51
|
+
/** `<namespace>:<name>` — the identifier's tail and the `queries` key. */
|
|
52
|
+
key: string;
|
|
53
|
+
displayName: string;
|
|
54
|
+
type: string;
|
|
55
|
+
url: string;
|
|
56
|
+
description?: string;
|
|
57
|
+
capabilities?: string[];
|
|
58
|
+
/** Generated queries, replaced wholesale by a configured `queries[key]`. */
|
|
59
|
+
queries: string[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const REFERENCE_KIND_LABEL = {
|
|
63
|
+
asyncapi: "AsyncAPI",
|
|
64
|
+
graphql: "GraphQL",
|
|
65
|
+
openapi: "OpenAPI",
|
|
66
|
+
} as const;
|
|
67
|
+
|
|
68
|
+
/** The `urn:air` publisher: the configured site's hostname. */
|
|
69
|
+
const publisherHost = (site: string): string => new URL(site).hostname;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Whether the site publishes a catalog: the feature is on, a `deployment.site`
|
|
73
|
+
* anchors the identifiers, and at least one entry exists. Every entry source
|
|
74
|
+
* is a config flag, so the answer needs no build output — the same gate the
|
|
75
|
+
* header rules, the `Link` header, and the head links read.
|
|
76
|
+
*/
|
|
77
|
+
export const hasAiCatalog = (config: ResolvedConfig): boolean =>
|
|
78
|
+
config.ai.catalog.enabled &&
|
|
79
|
+
Boolean(config.deployment.site) &&
|
|
80
|
+
(config.ai.mcp.enabled ||
|
|
81
|
+
config.ai.api ||
|
|
82
|
+
config.ai.llmsTxt.enabled ||
|
|
83
|
+
Boolean(config.ai.skills) ||
|
|
84
|
+
resolveReferences(config).length > 0);
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The `.well-known` documents agent registries fetch cross-origin (browser
|
|
88
|
+
* agents, hosted registries reading through a page). Each needs
|
|
89
|
+
* `Access-Control-Allow-Origin: *` on the static surface; the MCP endpoint
|
|
90
|
+
* itself already sets it at runtime.
|
|
91
|
+
*/
|
|
92
|
+
export const crossOriginDiscoveryPaths = (config: ResolvedConfig): string[] => {
|
|
93
|
+
const paths: string[] = [];
|
|
94
|
+
if (hasAiCatalog(config)) {
|
|
95
|
+
paths.push(AI_CATALOG_PATH, ARD_MANIFEST_PATH);
|
|
96
|
+
}
|
|
97
|
+
if (hasApiCatalog(config)) {
|
|
98
|
+
paths.push(API_CATALOG_PATH);
|
|
99
|
+
}
|
|
100
|
+
if (config.ai.mcp.enabled) {
|
|
101
|
+
paths.push("/.well-known/mcp.json", "/.well-known/mcp/server-card.json");
|
|
102
|
+
}
|
|
103
|
+
return paths;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const entrySeeds = (
|
|
107
|
+
config: ResolvedConfig,
|
|
108
|
+
skills: readonly SkillArtifact[],
|
|
109
|
+
abs: (path: string) => string
|
|
110
|
+
): EntrySeed[] => {
|
|
111
|
+
const { title } = config;
|
|
112
|
+
const seeds: EntrySeed[] = [];
|
|
113
|
+
|
|
114
|
+
if (config.ai.mcp.enabled) {
|
|
115
|
+
const name = config.ai.mcp.name ?? title;
|
|
116
|
+
seeds.push({
|
|
117
|
+
capabilities: ["search_docs", "get_page", "list_pages", "get_navigation"],
|
|
118
|
+
description:
|
|
119
|
+
config.ai.mcp.instructions ??
|
|
120
|
+
`Model Context Protocol server over the ${title} documentation: full-text search, page Markdown, the page index, and the navigation tree.`,
|
|
121
|
+
displayName: name,
|
|
122
|
+
key: `mcp:${asciiSlugify(name) || "docs"}`,
|
|
123
|
+
queries: [
|
|
124
|
+
`search the ${title} documentation`,
|
|
125
|
+
`get a ${title} docs page as Markdown`,
|
|
126
|
+
`list every page in the ${title} docs`,
|
|
127
|
+
],
|
|
128
|
+
type: "application/mcp-server-card+json",
|
|
129
|
+
url: abs("/.well-known/mcp/server-card.json"),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
for (const skill of skills) {
|
|
134
|
+
seeds.push({
|
|
135
|
+
description: skill.description,
|
|
136
|
+
displayName: skill.name,
|
|
137
|
+
key: `skill:${skill.name}`,
|
|
138
|
+
queries: [
|
|
139
|
+
`load the ${skill.name} agent skill`,
|
|
140
|
+
`how do I use ${skill.name}`,
|
|
141
|
+
],
|
|
142
|
+
type:
|
|
143
|
+
skill.type === "archive"
|
|
144
|
+
? "application/agent-skills+gzip"
|
|
145
|
+
: "application/agent-skills+md",
|
|
146
|
+
url: abs(`${AGENT_SKILLS_DIR}/${skill.path}`),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (config.ai.api) {
|
|
151
|
+
seeds.push({
|
|
152
|
+
description: `REST API over the ${title} documentation: the page index, each page as JSON or Markdown, and the navigation tree, described by this OpenAPI document.`,
|
|
153
|
+
displayName: `${title} docs API`,
|
|
154
|
+
key: "api:docs",
|
|
155
|
+
queries: [
|
|
156
|
+
`fetch a ${title} docs page as JSON`,
|
|
157
|
+
`list the pages in the ${title} docs`,
|
|
158
|
+
`get the ${title} docs navigation tree`,
|
|
159
|
+
],
|
|
160
|
+
type: "application/vnd.oai.openapi+json",
|
|
161
|
+
url: abs(OPENAPI_PATH),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
for (const reference of resolveReferences(config)) {
|
|
166
|
+
// Blume-rendered pages mount under `basePath`; Scalar pages stay at the
|
|
167
|
+
// raw route (see `referenceRoutes`). The rendered reference is the
|
|
168
|
+
// resource this publisher owns — the spec itself is catalogued by URL in
|
|
169
|
+
// the RFC 9727 linkset.
|
|
170
|
+
const docRoute =
|
|
171
|
+
reference.renderer === "blume"
|
|
172
|
+
? withBasePath(reference.basePath, reference.route)
|
|
173
|
+
: reference.route;
|
|
174
|
+
seeds.push({
|
|
175
|
+
description: `${reference.label}: rendered ${REFERENCE_KIND_LABEL[reference.kind]} reference in the ${title} documentation.`,
|
|
176
|
+
displayName: reference.label,
|
|
177
|
+
key: `reference:${reference.slug}`,
|
|
178
|
+
queries: [
|
|
179
|
+
`what operations does the ${reference.label} API expose`,
|
|
180
|
+
`how do I call the ${reference.label} API`,
|
|
181
|
+
],
|
|
182
|
+
type: "text/html",
|
|
183
|
+
url: abs(docRoute),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (config.ai.llmsTxt.enabled) {
|
|
188
|
+
seeds.push({
|
|
189
|
+
description: `llms.txt index of the ${title} documentation: every page with a one-line summary, plus the agent-facing resources on this site.`,
|
|
190
|
+
displayName: `${title} llms.txt`,
|
|
191
|
+
key: "docs:llms-txt",
|
|
192
|
+
queries: [`what is ${title}`, `overview of the ${title} documentation`],
|
|
193
|
+
type: "text/plain",
|
|
194
|
+
url: abs("/llms.txt"),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return seeds;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
/** The catalog document, or null when the site publishes none. */
|
|
202
|
+
export const buildAiCatalog = (
|
|
203
|
+
config: ResolvedConfig,
|
|
204
|
+
skills: readonly SkillArtifact[]
|
|
205
|
+
): string | null => {
|
|
206
|
+
const site = config.deployment.site ?? null;
|
|
207
|
+
if (!(site && hasAiCatalog(config))) {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
const deployBase = normalizeBasePath(config.deployment.base);
|
|
211
|
+
const abs = (path: string): string =>
|
|
212
|
+
absoluteUrl(site, withBasePath(deployBase, path));
|
|
213
|
+
const host = publisherHost(site);
|
|
214
|
+
const entries = entrySeeds(config, skills, abs).map((seed): CatalogEntry => {
|
|
215
|
+
const entry: CatalogEntry = {
|
|
216
|
+
displayName: seed.displayName,
|
|
217
|
+
identifier: `urn:air:${host}:${seed.key}`,
|
|
218
|
+
representativeQueries:
|
|
219
|
+
config.ai.catalog.queries[seed.key] ?? seed.queries,
|
|
220
|
+
type: seed.type,
|
|
221
|
+
url: seed.url,
|
|
222
|
+
};
|
|
223
|
+
if (seed.description) {
|
|
224
|
+
entry.description = seed.description;
|
|
225
|
+
}
|
|
226
|
+
if (seed.capabilities) {
|
|
227
|
+
entry.capabilities = seed.capabilities;
|
|
228
|
+
}
|
|
229
|
+
return entry;
|
|
230
|
+
});
|
|
231
|
+
const catalog = {
|
|
232
|
+
entries,
|
|
233
|
+
host: {
|
|
234
|
+
displayName: config.title,
|
|
235
|
+
documentationUrl: abs("/"),
|
|
236
|
+
identifier: `did:web:${host}`,
|
|
237
|
+
},
|
|
238
|
+
specVersion: SPEC_VERSION,
|
|
239
|
+
};
|
|
240
|
+
return `${JSON.stringify(catalog, null, 2)}\n`;
|
|
241
|
+
};
|
package/src/ai/cors.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CORS for the generated Ask AI route (`ai.ask.cors`).
|
|
3
|
+
*
|
|
4
|
+
* A browser only lets a page on another origin read a response that names
|
|
5
|
+
* that origin, and a JSON `POST` preflights first. `preflightResponse` answers
|
|
6
|
+
* the `OPTIONS`; `withCors` wraps the `POST` handler so every response it
|
|
7
|
+
* returns — the stream, a 400, a 500 — carries the headers. Wrapping once,
|
|
8
|
+
* rather than stamping each `return`, keeps the next return site added to the
|
|
9
|
+
* handler from shipping an opaque failure for that one status.
|
|
10
|
+
*
|
|
11
|
+
* `allowed` is the `ai.ask.cors` list: origins already reduced to their
|
|
12
|
+
* `scheme://host[:port]` form by the config schema, or the single entry `"*"`
|
|
13
|
+
* to admit every origin.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** The `ai.ask.cors` entry that admits every origin. */
|
|
17
|
+
export const ANY_ORIGIN = "*";
|
|
18
|
+
|
|
19
|
+
/** The headers a response carries for a cross-origin caller. */
|
|
20
|
+
export interface CorsHeaders {
|
|
21
|
+
"access-control-allow-origin"?: string;
|
|
22
|
+
vary?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The response headers that name the caller's origin when `allowed` lists it. */
|
|
26
|
+
export const corsHeaders = (
|
|
27
|
+
request: Request,
|
|
28
|
+
allowed: readonly string[]
|
|
29
|
+
): CorsHeaders => {
|
|
30
|
+
if (allowed.includes(ANY_ORIGIN)) {
|
|
31
|
+
// A wildcard answer is the same for every caller, so nothing to vary on.
|
|
32
|
+
return { "access-control-allow-origin": ANY_ORIGIN };
|
|
33
|
+
}
|
|
34
|
+
// `Vary` rides on both branches: the answer depends on `Origin` whether or
|
|
35
|
+
// not it was listed, so a shared cache never hands one origin's response
|
|
36
|
+
// (or the header-less one) to another.
|
|
37
|
+
const origin = request.headers.get("origin");
|
|
38
|
+
return origin && allowed.includes(origin)
|
|
39
|
+
? { "access-control-allow-origin": origin, vary: "origin" }
|
|
40
|
+
: { vary: "origin" };
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** Answer the browser's `OPTIONS` preflight for the route. */
|
|
44
|
+
export const preflightResponse = (
|
|
45
|
+
request: Request,
|
|
46
|
+
allowed: readonly string[]
|
|
47
|
+
): Response =>
|
|
48
|
+
new Response(null, {
|
|
49
|
+
headers: {
|
|
50
|
+
...corsHeaders(request, allowed),
|
|
51
|
+
// Reflect whatever the caller's fetch wrapper asks to send, falling back
|
|
52
|
+
// to the JSON POST's own `content-type`; a listed origin shouldn't need
|
|
53
|
+
// an eject to add a header of its own.
|
|
54
|
+
"access-control-allow-headers":
|
|
55
|
+
request.headers.get("access-control-request-headers") ?? "content-type",
|
|
56
|
+
"access-control-allow-methods": "POST",
|
|
57
|
+
"access-control-max-age": "86400",
|
|
58
|
+
},
|
|
59
|
+
status: 204,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
/** The slice of Astro's `APIContext` the wrapped handler reads. */
|
|
63
|
+
interface RequestContext {
|
|
64
|
+
request: Request;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Stamp the CORS headers on every response `handler` returns. */
|
|
68
|
+
export const withCors =
|
|
69
|
+
(
|
|
70
|
+
allowed: readonly string[],
|
|
71
|
+
handler: (context: RequestContext) => Promise<Response> | Response
|
|
72
|
+
): ((context: RequestContext) => Promise<Response>) =>
|
|
73
|
+
async (context) => {
|
|
74
|
+
const response = await handler(context);
|
|
75
|
+
for (const [key, value] of Object.entries(
|
|
76
|
+
corsHeaders(context.request, allowed)
|
|
77
|
+
)) {
|
|
78
|
+
// `Vary` accumulates (the handler may already vary on something), the
|
|
79
|
+
// rest replace.
|
|
80
|
+
if (key === "vary") {
|
|
81
|
+
response.headers.append(key, value);
|
|
82
|
+
} else {
|
|
83
|
+
response.headers.set(key, value);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return response;
|
|
87
|
+
};
|
package/src/ai/link-headers.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { normalizeBasePath } from "../core/base-path.ts";
|
|
2
2
|
import type { ResolvedConfig } from "../core/schema.ts";
|
|
3
|
+
import {
|
|
4
|
+
AI_CATALOG_PATH,
|
|
5
|
+
AI_CATALOG_TYPE,
|
|
6
|
+
hasAiCatalog,
|
|
7
|
+
} from "./ai-catalog.ts";
|
|
3
8
|
import { API_CATALOG_PATH, hasApiCatalog } from "./api-catalog.ts";
|
|
4
9
|
import { OPENAPI_PATH } from "./api/paths.ts";
|
|
5
10
|
|
|
@@ -37,6 +42,13 @@ export const buildHomeLinkHeader = (
|
|
|
37
42
|
`<${deployBase}${API_CATALOG_PATH}>; rel="api-catalog"; type="application/linkset+json"`
|
|
38
43
|
);
|
|
39
44
|
}
|
|
45
|
+
// The ai-catalog spec's own relation for its well-known document, the
|
|
46
|
+
// header form of the `<link rel="ai-catalog">` every page carries.
|
|
47
|
+
if (hasAiCatalog(config)) {
|
|
48
|
+
links.push(
|
|
49
|
+
`<${deployBase}${AI_CATALOG_PATH}>; rel="ai-catalog"; type="${AI_CATALOG_TYPE}"`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
40
52
|
// RFC 8631: `service-desc` is the relation for a machine-readable
|
|
41
53
|
// description of the service — the JSON docs API's OpenAPI document.
|
|
42
54
|
if (config.ai.api) {
|
package/src/ai/llms.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { absoluteUrl } from "../core/site-url.ts";
|
|
|
6
6
|
import { readExpandedEntryText } from "../core/sources/read.ts";
|
|
7
7
|
import type { NavNode, Navigation, PageRecord } from "../core/types.ts";
|
|
8
8
|
import { buildRssFeeds } from "../deploy/rss.ts";
|
|
9
|
+
import { AI_CATALOG_PATH, hasAiCatalog } from "./ai-catalog.ts";
|
|
9
10
|
import { API_CATALOG_PATH, hasApiCatalog } from "./api-catalog.ts";
|
|
10
11
|
import { API_PAGES_PATH, OPENAPI_PATH } from "./api/paths.ts";
|
|
11
12
|
import { downlevelComponents } from "./component-markdown.ts";
|
|
@@ -75,6 +76,11 @@ const agentResourceLines = (project: BlumeProject): string[] => {
|
|
|
75
76
|
`- [API catalog](${url(API_CATALOG_PATH)}): RFC 9727 linkset of the APIs documented here.`
|
|
76
77
|
);
|
|
77
78
|
}
|
|
79
|
+
if (hasAiCatalog(config)) {
|
|
80
|
+
lines.push(
|
|
81
|
+
`- [AI catalog](${url(AI_CATALOG_PATH)}): ARD manifest of the agent-facing resources on this site (MCP server, skills, APIs).`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
78
84
|
if (config.seo.agentReadability) {
|
|
79
85
|
lines.push(
|
|
80
86
|
`- [agent-readability.json](${url("/agent-readability.json")}): Manifest of every agent-facing artifact on this site.`
|
package/src/ai/mcp/discovery.ts
CHANGED
|
@@ -56,7 +56,7 @@ const truncate = (text: string): string =>
|
|
|
56
56
|
const NON_ASCII_SLUG = /[^a-z0-9]+/gu;
|
|
57
57
|
const COMBINING_MARKS = /\p{M}+/gu;
|
|
58
58
|
|
|
59
|
-
const asciiSlugify = (text: string): string =>
|
|
59
|
+
export const asciiSlugify = (text: string): string =>
|
|
60
60
|
trimChar(
|
|
61
61
|
text
|
|
62
62
|
.normalize("NFKD")
|
package/src/astro/generate.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
} from "pathe";
|
|
25
25
|
import { glob } from "tinyglobby";
|
|
26
26
|
|
|
27
|
+
import { hasAiCatalog } from "../ai/ai-catalog.ts";
|
|
27
28
|
import { OPENAPI_PATH } from "../ai/api/paths.ts";
|
|
28
29
|
import { buildApiSpec } from "../ai/api/spec.ts";
|
|
29
30
|
import { buildAskData } from "../ai/ask-data.ts";
|
|
@@ -1357,6 +1358,7 @@ export const buildRuntimeData = (project: BlumeProject): string => {
|
|
|
1357
1358
|
description: config.description,
|
|
1358
1359
|
discovery: {
|
|
1359
1360
|
agentReadability: config.seo.agentReadability,
|
|
1361
|
+
aiCatalog: hasAiCatalog(config),
|
|
1360
1362
|
api: config.ai.api,
|
|
1361
1363
|
llmsTxt: config.ai.llmsTxt.enabled,
|
|
1362
1364
|
// Mirrors `buildSitemapFiles`: no site, no sitemap.
|
|
@@ -1826,7 +1828,9 @@ const writeAskFiles = async (
|
|
|
1826
1828
|
await write(
|
|
1827
1829
|
join(srcDir, "pages", "api", "ask.ts"),
|
|
1828
1830
|
askEndpointTemplate(resolveAskBackend(ask), grounded, {
|
|
1831
|
+
cors: ask.cors,
|
|
1829
1832
|
instructions: ask.instructions,
|
|
1833
|
+
reasoning: ask.reasoning,
|
|
1830
1834
|
retrieval: ask.retrieval,
|
|
1831
1835
|
})
|
|
1832
1836
|
);
|
package/src/astro/templates.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type { AskBackend } from "../ai/ask.ts";
|
|
|
9
9
|
import { buildHomeLinkHeader } from "../ai/link-headers.ts";
|
|
10
10
|
import { normalizeBasePath } from "../core/base-path.ts";
|
|
11
11
|
import { TOC_HIDDEN_KEY } from "../core/heading-markers.ts";
|
|
12
|
-
import type { ResolvedConfig } from "../core/schema.ts";
|
|
12
|
+
import type { AskReasoning, ResolvedConfig } from "../core/schema.ts";
|
|
13
13
|
import { BLUME_IGNORE_DIRS } from "../core/sources/watch.ts";
|
|
14
14
|
import { trimChar } from "../core/trim.ts";
|
|
15
15
|
import type { ProjectContext } from "../core/types.ts";
|
|
@@ -1040,12 +1040,58 @@ const ASK_FALLBACK_PROMPT =
|
|
|
1040
1040
|
|
|
1041
1041
|
/** The `ai.ask` values the generated endpoint has to carry with it. */
|
|
1042
1042
|
export interface AskEndpointOptions {
|
|
1043
|
+
/** `ai.ask.cors` — origins allowed to call the route from another site. */
|
|
1044
|
+
cors?: string[];
|
|
1043
1045
|
/** `ai.ask.instructions` — extra system-prompt text. */
|
|
1044
1046
|
instructions?: string;
|
|
1047
|
+
/**
|
|
1048
|
+
* `ai.ask.reasoning` — how much the model reasons before answering, sent
|
|
1049
|
+
* as the backend's own reasoning-effort control.
|
|
1050
|
+
*/
|
|
1051
|
+
reasoning?: AskReasoning;
|
|
1045
1052
|
/** `ai.ask.retrieval` — how much documentation each question carries. */
|
|
1046
1053
|
retrieval?: AskRetrievalOptions;
|
|
1047
1054
|
}
|
|
1048
1055
|
|
|
1056
|
+
/** The pieces `askEndpointTemplate` splices in for `ai.ask.cors`. */
|
|
1057
|
+
interface AskCorsTemplate {
|
|
1058
|
+
/** The route's closing token: `});` when the POST is wrapped, `};` otherwise. */
|
|
1059
|
+
close: string;
|
|
1060
|
+
/** The runtime import, when anything is listed. */
|
|
1061
|
+
imports: string[];
|
|
1062
|
+
/** The opening of `export const POST: APIRoute = `. */
|
|
1063
|
+
open: string;
|
|
1064
|
+
/** The allow list and the `OPTIONS` handler, spliced after the provider setup. */
|
|
1065
|
+
setup: string;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
/**
|
|
1069
|
+
* `ai.ask.cors`: a browser only lets another origin read the stream when the
|
|
1070
|
+
* response names that origin, and a JSON POST preflights first, so the route
|
|
1071
|
+
* answers `OPTIONS` and wraps the `POST` in `withCors`, which stamps a listed
|
|
1072
|
+
* origin on every response — errors included, so a cross-origin caller can
|
|
1073
|
+
* tell a 400 from a 500 — without each `return` having to remember to.
|
|
1074
|
+
* Unlisted origins get no allow header and stay subject to the same-origin
|
|
1075
|
+
* rule. Left out entirely when nothing is listed, so the default route is
|
|
1076
|
+
* unchanged.
|
|
1077
|
+
*/
|
|
1078
|
+
const askCorsTemplate = (cors: readonly string[] = []): AskCorsTemplate =>
|
|
1079
|
+
cors.length > 0
|
|
1080
|
+
? {
|
|
1081
|
+
close: "});",
|
|
1082
|
+
imports: [
|
|
1083
|
+
'import { preflightResponse, withCors } from "blume/ai/cors.ts";',
|
|
1084
|
+
],
|
|
1085
|
+
open: "withCors(ALLOWED_ORIGINS, async ({ request }) => {",
|
|
1086
|
+
setup: `
|
|
1087
|
+
const ALLOWED_ORIGINS = ${JSON.stringify(cors)};
|
|
1088
|
+
|
|
1089
|
+
export const OPTIONS: APIRoute = ({ request }) =>
|
|
1090
|
+
preflightResponse(request, ALLOWED_ORIGINS);
|
|
1091
|
+
`,
|
|
1092
|
+
}
|
|
1093
|
+
: { close: "};", imports: [], open: "async ({ request }) => {", setup: "" };
|
|
1094
|
+
|
|
1049
1095
|
/**
|
|
1050
1096
|
* Generate the Ask AI server endpoint (`.blume/src/pages/api/ask.ts`).
|
|
1051
1097
|
*
|
|
@@ -1053,15 +1099,24 @@ export interface AskEndpointOptions {
|
|
|
1053
1099
|
* built-in prompt on every path: the grounded prompt via `createAskContext`,
|
|
1054
1100
|
* and the plain fallback here. `options.retrieval` (the `ai.ask.retrieval`
|
|
1055
1101
|
* config) is forwarded to `createAskContext` on the grounded path, where it
|
|
1056
|
-
* sizes retrieval.
|
|
1057
|
-
*
|
|
1102
|
+
* sizes retrieval. `options.reasoning` (the `ai.ask.reasoning` config)
|
|
1103
|
+
* reaches the model call on both paths. `options.cors` (the `ai.ask.cors`
|
|
1104
|
+
* config) adds a preflight handler and wraps the `POST` so every response
|
|
1105
|
+
* names a listed origin. All four travel in one options object so a new call
|
|
1106
|
+
* site can't silently drop one of them.
|
|
1058
1107
|
*/
|
|
1059
1108
|
export const askEndpointTemplate = (
|
|
1060
1109
|
backend: AskBackend,
|
|
1061
1110
|
grounded: boolean,
|
|
1062
1111
|
options?: AskEndpointOptions
|
|
1063
1112
|
): string => {
|
|
1064
|
-
const instructions = options
|
|
1113
|
+
const { instructions, reasoning, retrieval } = options ?? {};
|
|
1114
|
+
// `ai.ask.reasoning`. The gateway and OpenAI-compatible providers take it
|
|
1115
|
+
// from `streamText`'s top-level `reasoning` (the gateway maps it to the
|
|
1116
|
+
// model's own control, the OpenAI-compatible provider sends it as
|
|
1117
|
+
// `reasoning_effort`). OpenRouter's provider ignores that call option and
|
|
1118
|
+
// only reads its own model setting, so there the level rides on the model
|
|
1119
|
+
// as `reasoning.effort`. Omitted keeps the provider default on every path.
|
|
1065
1120
|
const fallbackPrompt = instructions
|
|
1066
1121
|
? `${ASK_FALLBACK_PROMPT}\n\n${instructions}`
|
|
1067
1122
|
: ASK_FALLBACK_PROMPT;
|
|
@@ -1097,7 +1152,10 @@ export const askEndpointTemplate = (
|
|
|
1097
1152
|
setup = `\nconst openrouter = createOpenRouter({
|
|
1098
1153
|
apiKey: getSecret(${JSON.stringify(backend.apiKeyEnv)}),${headersField}
|
|
1099
1154
|
});\n`;
|
|
1100
|
-
|
|
1155
|
+
const settings = reasoning
|
|
1156
|
+
? `, { reasoning: { effort: ${JSON.stringify(reasoning)} } }`
|
|
1157
|
+
: "";
|
|
1158
|
+
modelExpr = `openrouter(${JSON.stringify(backend.model)}${settings})`;
|
|
1101
1159
|
} else if (backend.kind === "openai-compatible") {
|
|
1102
1160
|
imports.push(
|
|
1103
1161
|
'import { createOpenAICompatible } from "@ai-sdk/openai-compatible";'
|
|
@@ -1120,13 +1178,15 @@ export const askEndpointTemplate = (
|
|
|
1120
1178
|
if (instructions) {
|
|
1121
1179
|
groundFields.push(`instructions: ${JSON.stringify(instructions)}`);
|
|
1122
1180
|
}
|
|
1123
|
-
if (
|
|
1124
|
-
groundFields.push(`retrieval: ${JSON.stringify(
|
|
1181
|
+
if (retrieval) {
|
|
1182
|
+
groundFields.push(`retrieval: ${JSON.stringify(retrieval)}`);
|
|
1125
1183
|
}
|
|
1126
1184
|
const groundOptions =
|
|
1127
1185
|
groundFields.length > 0 ? `, { ${groundFields.join(", ")} }` : "";
|
|
1128
1186
|
setup += `\nconst ground = createAskContext(askData${groundOptions});\n`;
|
|
1129
1187
|
}
|
|
1188
|
+
const cors = askCorsTemplate(options?.cors);
|
|
1189
|
+
imports.push(...cors.imports);
|
|
1130
1190
|
// Validate the client-supplied body and cap its size. The endpoint is
|
|
1131
1191
|
// unauthenticated, so bounding message count/length limits how much a caller
|
|
1132
1192
|
// can spend against the model per request, and restricting roles to
|
|
@@ -1183,24 +1243,29 @@ export const askEndpointTemplate = (
|
|
|
1183
1243
|
const onError = ` onError({ error }) {
|
|
1184
1244
|
console.error("Ask AI provider error:", error);
|
|
1185
1245
|
},`;
|
|
1246
|
+
// The `streamText` argument list, built once so the grounded and plain
|
|
1247
|
+
// paths can't drift: they differ only in where the instructions come from.
|
|
1248
|
+
const streamFields = [
|
|
1249
|
+
`model: ${modelExpr}`,
|
|
1250
|
+
grounded
|
|
1251
|
+
? "instructions"
|
|
1252
|
+
: `instructions:\n ${JSON.stringify(fallbackPrompt)}`,
|
|
1253
|
+
"messages",
|
|
1254
|
+
];
|
|
1255
|
+
if (reasoning && backend.kind !== "openrouter") {
|
|
1256
|
+
streamFields.push(`reasoning: ${JSON.stringify(reasoning)}`);
|
|
1257
|
+
}
|
|
1258
|
+
const call = ` const result = streamText({
|
|
1259
|
+
${streamFields.join(",\n ")},
|
|
1260
|
+
${onError}
|
|
1261
|
+
});`;
|
|
1186
1262
|
const stream = grounded
|
|
1187
1263
|
? ` const instructions =
|
|
1188
1264
|
(await ground(messages, body.page)) ??
|
|
1189
1265
|
${JSON.stringify(fallbackPrompt)};
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
messages,
|
|
1194
|
-
${onError}
|
|
1195
|
-
});`
|
|
1196
|
-
: ` const result = streamText({
|
|
1197
|
-
model: ${modelExpr},
|
|
1198
|
-
instructions:
|
|
1199
|
-
${JSON.stringify(fallbackPrompt)},
|
|
1200
|
-
messages,
|
|
1201
|
-
${onError}
|
|
1202
|
-
});`;
|
|
1203
|
-
const handler = `export const POST: APIRoute = async ({ request }) => {
|
|
1266
|
+
${call}`
|
|
1267
|
+
: call;
|
|
1268
|
+
const handler = `export const POST: APIRoute = ${cors.open}
|
|
1204
1269
|
${validate}
|
|
1205
1270
|
${keyCheck}
|
|
1206
1271
|
try {
|
|
@@ -1209,12 +1274,12 @@ ${stream}
|
|
|
1209
1274
|
} catch {
|
|
1210
1275
|
return new Response("Failed to generate a response.", { status: 500 });
|
|
1211
1276
|
}
|
|
1212
|
-
}
|
|
1277
|
+
${cors.close}`;
|
|
1213
1278
|
return `// Generated by Blume. Do not edit.
|
|
1214
1279
|
${imports.join("\n")}
|
|
1215
1280
|
|
|
1216
1281
|
export const prerender = false;
|
|
1217
|
-
${setup}
|
|
1282
|
+
${setup}${cors.setup}
|
|
1218
1283
|
${handler}
|
|
1219
1284
|
`;
|
|
1220
1285
|
};
|
|
@@ -5,6 +5,7 @@ import { build } from "astro";
|
|
|
5
5
|
import { defineCommand } from "citty";
|
|
6
6
|
import { join } from "pathe";
|
|
7
7
|
|
|
8
|
+
import { crossOriginDiscoveryPaths } from "../../ai/ai-catalog.ts";
|
|
8
9
|
import {
|
|
9
10
|
API_CATALOG_PATH,
|
|
10
11
|
API_CATALOG_TYPE,
|
|
@@ -128,7 +129,8 @@ const emitVercelNegotiation = async (
|
|
|
128
129
|
{
|
|
129
130
|
json: existsSync(join(staticDir, "404.json")),
|
|
130
131
|
markdown: existsSync(join(staticDir, "404.md")),
|
|
131
|
-
}
|
|
132
|
+
},
|
|
133
|
+
crossOriginDiscoveryPaths(config)
|
|
132
134
|
);
|
|
133
135
|
if (injected === null) {
|
|
134
136
|
logger.warn(
|