@nifrajs/cli 0.1.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +99 -0
  3. package/dist/check.d.ts +100 -0
  4. package/dist/check.d.ts.map +1 -0
  5. package/dist/check.js +690 -0
  6. package/dist/check.js.map +1 -0
  7. package/dist/cli.d.ts +3 -0
  8. package/dist/cli.d.ts.map +1 -0
  9. package/dist/cli.js +208 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/docs-search.d.ts +30 -0
  12. package/dist/docs-search.d.ts.map +1 -0
  13. package/dist/docs-search.js +94 -0
  14. package/dist/docs-search.js.map +1 -0
  15. package/dist/doctor.d.ts +52 -0
  16. package/dist/doctor.d.ts.map +1 -0
  17. package/dist/doctor.js +291 -0
  18. package/dist/doctor.js.map +1 -0
  19. package/dist/examples.d.ts +30 -0
  20. package/dist/examples.d.ts.map +1 -0
  21. package/dist/examples.js +70 -0
  22. package/dist/examples.js.map +1 -0
  23. package/dist/introspect.d.ts +64 -0
  24. package/dist/introspect.d.ts.map +1 -0
  25. package/dist/introspect.js +243 -0
  26. package/dist/introspect.js.map +1 -0
  27. package/dist/load.d.ts +51 -0
  28. package/dist/load.d.ts.map +1 -0
  29. package/dist/load.js +70 -0
  30. package/dist/load.js.map +1 -0
  31. package/dist/mcp-docs-tools.d.ts +13 -0
  32. package/dist/mcp-docs-tools.d.ts.map +1 -0
  33. package/dist/mcp-docs-tools.js +63 -0
  34. package/dist/mcp-docs-tools.js.map +1 -0
  35. package/dist/mcp-http.d.ts +43 -0
  36. package/dist/mcp-http.d.ts.map +1 -0
  37. package/dist/mcp-http.js +136 -0
  38. package/dist/mcp-http.js.map +1 -0
  39. package/dist/mcp-protocol.d.ts +97 -0
  40. package/dist/mcp-protocol.d.ts.map +1 -0
  41. package/dist/mcp-protocol.js +248 -0
  42. package/dist/mcp-protocol.js.map +1 -0
  43. package/dist/mcp-render.d.ts +17 -0
  44. package/dist/mcp-render.d.ts.map +1 -0
  45. package/dist/mcp-render.js +73 -0
  46. package/dist/mcp-render.js.map +1 -0
  47. package/dist/mcp-run.d.ts +24 -0
  48. package/dist/mcp-run.d.ts.map +1 -0
  49. package/dist/mcp-run.js +142 -0
  50. package/dist/mcp-run.js.map +1 -0
  51. package/dist/mcp-ws.d.ts +26 -0
  52. package/dist/mcp-ws.d.ts.map +1 -0
  53. package/dist/mcp-ws.js +258 -0
  54. package/dist/mcp-ws.js.map +1 -0
  55. package/dist/mcp.d.ts +42 -0
  56. package/dist/mcp.d.ts.map +1 -0
  57. package/dist/mcp.js +712 -0
  58. package/dist/mcp.js.map +1 -0
  59. package/dist/openapi-tool.d.ts +6 -0
  60. package/dist/openapi-tool.d.ts.map +1 -0
  61. package/dist/openapi-tool.js +48 -0
  62. package/dist/openapi-tool.js.map +1 -0
  63. package/dist/scaffold.d.ts +35 -0
  64. package/dist/scaffold.d.ts.map +1 -0
  65. package/dist/scaffold.js +140 -0
  66. package/dist/scaffold.js.map +1 -0
  67. package/dist/search-terms.d.ts +17 -0
  68. package/dist/search-terms.d.ts.map +1 -0
  69. package/dist/search-terms.js +82 -0
  70. package/dist/search-terms.js.map +1 -0
  71. package/dist/test-tool.d.ts +29 -0
  72. package/dist/test-tool.d.ts.map +1 -0
  73. package/dist/test-tool.js +150 -0
  74. package/dist/test-tool.js.map +1 -0
  75. package/docs/examples.json +314 -0
  76. package/docs/llms-full.txt +4325 -0
  77. package/package.json +58 -0
  78. package/src/check.ts +790 -0
  79. package/src/cli.ts +237 -0
  80. package/src/docs-search.ts +111 -0
  81. package/src/doctor.ts +349 -0
  82. package/src/examples.ts +89 -0
  83. package/src/introspect.ts +284 -0
  84. package/src/load.ts +129 -0
  85. package/src/mcp-docs-tools.ts +72 -0
  86. package/src/mcp-http.ts +155 -0
  87. package/src/mcp-protocol.ts +326 -0
  88. package/src/mcp-render.ts +78 -0
  89. package/src/mcp-run.ts +153 -0
  90. package/src/mcp-ws.ts +328 -0
  91. package/src/mcp.ts +863 -0
  92. package/src/openapi-tool.ts +51 -0
  93. package/src/scaffold.ts +164 -0
  94. package/src/search-terms.ts +89 -0
  95. package/src/test-tool.ts +181 -0
package/src/doctor.ts ADDED
@@ -0,0 +1,349 @@
1
+ /**
2
+ * `nifra doctor` — catches the Bun-workspace footgun the typed-client lint can't: a package that's
3
+ * IMPORTED in source but NOT declared in `package.json`. Bun resolves it at runtime (via hoisting or a
4
+ * workspace), so tests pass and `bun install` reports "no changes" — false confidence — yet `tsc` fails
5
+ * and a fresh or standalone install can't resolve it. doctor diffs every bare import specifier against
6
+ * the package's declared dependencies and flags the gap, with a stable `--json` shape for agents/CI.
7
+ *
8
+ * Scope is intentionally per-package: it checks the `package.json` at `cwd`, because a dependency must be
9
+ * declared by the package that imports it (for `tsc` and for that package to install on its own) even
10
+ * when a monorepo would hoist it. Relative paths, runtime builtins (node core, `node:`/`bun:`, `bun`),
11
+ * the package's own name, and tsconfig `paths` aliases are excluded — none of them are npm deps.
12
+ */
13
+ import { builtinModules } from "node:module"
14
+ import { dirname, join } from "node:path"
15
+ import { type SourceFinding, stripComments, walkSource } from "./check.ts"
16
+
17
+ // Runtime-provided modules that are never an npm dependency: Node core (bare + `node:` form) and Bun's
18
+ // own `bun` module. `node:`/`bun:`-prefixed specifiers are filtered in packageOf by prefix.
19
+ const BUILTINS: ReadonlySet<string> = new Set<string>([
20
+ ...builtinModules,
21
+ ...builtinModules.map((m) => `node:${m}`),
22
+ "bun",
23
+ ])
24
+
25
+ // The four specifier-bearing forms: static import/re-export with a source, side-effect import, dynamic
26
+ // import, and CJS require. Anchored with `(?<![.\w$])` so `myimport`/`.import`/`foorequire` never match.
27
+ // Comments are stripped before these run (see stripComments) — else a doc-comment usage example would be
28
+ // flagged as a real import.
29
+ const IMPORT_PATTERNS: readonly RegExp[] = [
30
+ /(?<![.\w$])(?:import|export)\b[^'"]*?\bfrom\s*['"]([^'"]+)['"]/g,
31
+ /(?<![.\w$])import\s+['"]([^'"]+)['"]/g,
32
+ /(?<![.\w$])import\s*\(\s*['"]([^'"]+)['"]/g,
33
+ /(?<![.\w$])require\s*\(\s*['"]([^'"]+)['"]/g,
34
+ ]
35
+
36
+ const NPM_PACKAGE_NAME = /^(?:@[A-Za-z0-9._-]+\/)?[A-Za-z0-9._-]+$/
37
+ const DEPENDENCY_FIELDS = [
38
+ "dependencies",
39
+ "devDependencies",
40
+ "peerDependencies",
41
+ "optionalDependencies",
42
+ ] as const
43
+
44
+ const isNpmPackageName = (name: string): boolean =>
45
+ NPM_PACKAGE_NAME.test(name) &&
46
+ name.split("/").every((part) => part !== "" && part !== "." && part !== "..")
47
+
48
+ /**
49
+ * Resolve an import specifier to the npm package name it would install as, or `undefined` when it isn't
50
+ * an npm dependency at all (a relative/absolute path, a runtime builtin, or a malformed scope like the
51
+ * `@/…` path-alias convention). `@scope/name/sub` → `@scope/name`; `name/sub` → `name`.
52
+ */
53
+ export function packageOf(spec: string): string | undefined {
54
+ if (spec === "" || spec.startsWith(".") || spec.startsWith("/")) return undefined
55
+ if (spec.startsWith("node:") || spec.startsWith("bun:")) return undefined
56
+ if (spec.startsWith("@")) {
57
+ const [scope, name] = spec.split("/")
58
+ if (!scope || scope === "@" || !name) return undefined // `@/alias`, `@foo` — not a real package
59
+ const pkg = `${scope}/${name}`
60
+ return isNpmPackageName(pkg) ? pkg : undefined
61
+ }
62
+ const name = spec.split("/")[0] as string
63
+ if (!isNpmPackageName(name)) return undefined
64
+ return BUILTINS.has(name) ? undefined : name
65
+ }
66
+
67
+ /** Build a predicate matching tsconfig `paths` aliases (e.g. `@/*`, `~/utils`), which resolve to local
68
+ * source, not npm packages — so doctor must not flag them. */
69
+ export function aliasMatcher(
70
+ paths: Readonly<Record<string, unknown>> | undefined,
71
+ ): (spec: string) => boolean {
72
+ const prefixes = Object.keys(paths ?? {})
73
+ .map((k) => k.replace(/\/?\*$/, ""))
74
+ .filter((p) => p !== "")
75
+ return (spec) => prefixes.some((p) => spec === p || spec.startsWith(`${p}/`))
76
+ }
77
+
78
+ /**
79
+ * Scan one file for bare imports whose resolved package is neither `declared` nor a path `alias`. Pure +
80
+ * line-accurate. Deduped per (package, line): the `snippet` carries the undeclared package name.
81
+ */
82
+ export function scanUndeclaredImports(
83
+ file: string,
84
+ content: string,
85
+ declared: ReadonlySet<string>,
86
+ isAlias: (spec: string) => boolean,
87
+ ): SourceFinding[] {
88
+ const out: SourceFinding[] = []
89
+ const seen = new Set<string>()
90
+ const code = stripComments(content) // so doc-comment usage examples aren't read as real imports
91
+ for (const rx of IMPORT_PATTERNS) {
92
+ rx.lastIndex = 0
93
+ for (let m = rx.exec(code); m !== null; m = rx.exec(code)) {
94
+ const spec = m[1] ?? ""
95
+ if (isAlias(spec)) continue
96
+ const pkg = packageOf(spec)
97
+ if (pkg === undefined || declared.has(pkg)) continue
98
+ const line = content.slice(0, m.index).split("\n").length
99
+ const key = `${pkg}@${line}`
100
+ if (seen.has(key)) continue
101
+ seen.add(key)
102
+ out.push({ file, line, snippet: pkg })
103
+ }
104
+ }
105
+ return out.sort((a, b) => a.line - b.line || a.snippet.localeCompare(b.snippet))
106
+ }
107
+
108
+ export interface DoctorFinding {
109
+ readonly file: string
110
+ readonly line: number
111
+ /** The undeclared package name — add it to `package.json` dependencies. */
112
+ readonly package: string
113
+ }
114
+
115
+ export interface DoctorResult {
116
+ readonly ok: boolean
117
+ /** `false` when no `package.json` was found at cwd — doctor can't run (reported, not a crash). */
118
+ readonly ran: boolean
119
+ readonly findings: readonly DoctorFinding[]
120
+ /** Dependencies written by `--auto-fix` / MCP `autoFix:true`. */
121
+ readonly fixed?: readonly DoctorAppliedFix[]
122
+ /** Findings that were safe to report but not safe to write automatically. */
123
+ readonly skippedFixes?: readonly DoctorSkippedFix[]
124
+ }
125
+
126
+ export interface DoctorAppliedFix {
127
+ readonly package: string
128
+ readonly field: "dependencies"
129
+ readonly version: string
130
+ readonly source: "ancestor-package-json" | "installed-package-json"
131
+ }
132
+
133
+ export interface DoctorSkippedFix {
134
+ readonly package: string
135
+ readonly reason: string
136
+ readonly command: readonly string[]
137
+ }
138
+
139
+ /** Read + parse a JSON file, or `undefined` if it's missing/unparseable (doctor degrades, never throws). */
140
+ async function readJson(path: string): Promise<Record<string, unknown> | undefined> {
141
+ try {
142
+ const text = await Bun.file(path).text()
143
+ const parsed = JSON.parse(text) as unknown
144
+ return typeof parsed === "object" && parsed !== null
145
+ ? (parsed as Record<string, unknown>)
146
+ : undefined
147
+ } catch {
148
+ return undefined
149
+ }
150
+ }
151
+
152
+ const depNames = (pkg: Record<string, unknown>, field: string): string[] => {
153
+ const deps = pkg[field]
154
+ return typeof deps === "object" && deps !== null
155
+ ? Object.keys(deps as Record<string, unknown>)
156
+ : []
157
+ }
158
+
159
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
160
+ typeof value === "object" && value !== null && !Array.isArray(value)
161
+
162
+ function depRecord(
163
+ pkg: Record<string, unknown>,
164
+ field: string,
165
+ ): Record<string, unknown> | undefined {
166
+ const deps = pkg[field]
167
+ return isRecord(deps) ? deps : undefined
168
+ }
169
+
170
+ function dependencySpec(pkg: Record<string, unknown>, name: string): string | undefined {
171
+ for (const field of DEPENDENCY_FIELDS) {
172
+ const spec = depRecord(pkg, field)?.[name]
173
+ if (typeof spec === "string" && spec.length > 0) return spec
174
+ }
175
+ return undefined
176
+ }
177
+
178
+ async function ancestorDependencySpec(cwd: string, name: string): Promise<string | undefined> {
179
+ for (let dir = dirname(cwd); ; dir = dirname(dir)) {
180
+ const pkg = await readJson(join(dir, "package.json"))
181
+ if (pkg !== undefined) {
182
+ const spec = dependencySpec(pkg, name)
183
+ if (spec !== undefined) return spec
184
+ }
185
+ const parent = dirname(dir)
186
+ if (parent === dir) return undefined
187
+ }
188
+ }
189
+
190
+ async function installedPackageSpec(cwd: string, name: string): Promise<string | undefined> {
191
+ if (!isNpmPackageName(name)) return undefined
192
+ const parts = name.split("/")
193
+ for (let dir = cwd; ; dir = dirname(dir)) {
194
+ const meta = await readJson(join(dir, "node_modules", ...parts, "package.json"))
195
+ if (typeof meta?.version === "string" && meta.version.length > 0) {
196
+ return `^${meta.version}`
197
+ }
198
+ const parent = dirname(dir)
199
+ if (parent === dir) return undefined
200
+ }
201
+ }
202
+
203
+ async function inferDependencyFix(
204
+ cwd: string,
205
+ name: string,
206
+ ): Promise<Omit<DoctorAppliedFix, "package" | "field"> | undefined> {
207
+ const ancestorSpec = await ancestorDependencySpec(cwd, name)
208
+ if (ancestorSpec !== undefined) {
209
+ return { version: ancestorSpec, source: "ancestor-package-json" }
210
+ }
211
+ const installedSpec = await installedPackageSpec(cwd, name)
212
+ return installedSpec === undefined
213
+ ? undefined
214
+ : { version: installedSpec, source: "installed-package-json" }
215
+ }
216
+
217
+ /** Run doctor against the project at `cwd`: diff source imports vs declared deps. */
218
+ export async function collectDoctorResult(cwd: string): Promise<DoctorResult> {
219
+ const pkg = await readJson(join(cwd, "package.json"))
220
+ if (pkg === undefined) return { ok: true, ran: false, findings: [] }
221
+
222
+ const declared = new Set<string>()
223
+ if (typeof pkg.name === "string") declared.add(pkg.name) // a package may import its own name (exports map)
224
+ for (const field of DEPENDENCY_FIELDS) {
225
+ for (const name of depNames(pkg, field)) declared.add(name)
226
+ }
227
+
228
+ const tsconfig = await readJson(join(cwd, "tsconfig.json"))
229
+ const compilerOptions = tsconfig?.compilerOptions as
230
+ | { paths?: Record<string, unknown> }
231
+ | undefined
232
+ const isAlias = aliasMatcher(compilerOptions?.paths)
233
+
234
+ const findings: DoctorFinding[] = []
235
+ await walkSource(cwd, (rel, content) => {
236
+ for (const f of scanUndeclaredImports(rel, content, declared, isAlias)) {
237
+ findings.push({ file: f.file, line: f.line, package: f.snippet })
238
+ }
239
+ })
240
+ findings.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line)
241
+ return { ok: findings.length === 0, ran: true, findings }
242
+ }
243
+
244
+ /** Safely add undeclared imports to package.json when the version can be inferred without network I/O. */
245
+ export async function applyDoctorAutoFix(cwd: string): Promise<DoctorResult> {
246
+ const before = await collectDoctorResult(cwd)
247
+ if (!before.ran || before.findings.length === 0) return before
248
+
249
+ const pkgPath = join(cwd, "package.json")
250
+ const pkg = await readJson(pkgPath)
251
+ if (pkg === undefined) return before
252
+ const dependencies = pkg.dependencies
253
+ if (dependencies !== undefined && !isRecord(dependencies)) {
254
+ const skippedFixes = [...new Set(before.findings.map((f) => f.package))].sort().map(
255
+ (name): DoctorSkippedFix => ({
256
+ package: name,
257
+ reason: "`dependencies` exists but is not an object; refusing to rewrite it automatically",
258
+ command: ["bun", "add", name],
259
+ }),
260
+ )
261
+ return { ...before, skippedFixes }
262
+ }
263
+
264
+ const deps = (dependencies ?? {}) as Record<string, unknown>
265
+ if (dependencies === undefined) pkg.dependencies = deps
266
+
267
+ const fixed: DoctorAppliedFix[] = []
268
+ const skippedFixes: DoctorSkippedFix[] = []
269
+ for (const name of [...new Set(before.findings.map((f) => f.package))].sort()) {
270
+ if (!isNpmPackageName(name)) {
271
+ skippedFixes.push({
272
+ package: name,
273
+ reason: "package name did not match npm package-name syntax",
274
+ command: ["bun", "add", name],
275
+ })
276
+ continue
277
+ }
278
+ const inferred = await inferDependencyFix(cwd, name)
279
+ if (inferred === undefined) {
280
+ skippedFixes.push({
281
+ package: name,
282
+ reason: "no declared ancestor version or installed package metadata was found locally",
283
+ command: ["bun", "add", name],
284
+ })
285
+ continue
286
+ }
287
+ deps[name] = inferred.version
288
+ fixed.push({ package: name, field: "dependencies", ...inferred })
289
+ }
290
+
291
+ if (fixed.length > 0) await Bun.write(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`)
292
+ const after = fixed.length > 0 ? await collectDoctorResult(cwd) : before
293
+ return {
294
+ ...after,
295
+ ...(fixed.length > 0 ? { fixed } : {}),
296
+ ...(skippedFixes.length > 0 ? { skippedFixes } : {}),
297
+ }
298
+ }
299
+
300
+ /** Run doctor; print a report (`--json` for machine output) and return whether it passed. */
301
+ export async function runDoctor(
302
+ cwd: string,
303
+ opts: { readonly json?: boolean; readonly autoFix?: boolean } = {},
304
+ ): Promise<boolean> {
305
+ const result = opts.autoFix ? await applyDoctorAutoFix(cwd) : await collectDoctorResult(cwd)
306
+ if (opts.json) {
307
+ console.log(JSON.stringify(result, null, 2))
308
+ return result.ok
309
+ }
310
+ console.log("nifra doctor\n")
311
+ if (!result.ran) {
312
+ console.log("• no package.json at this directory — nothing to check")
313
+ return true
314
+ }
315
+ if (result.fixed && result.fixed.length > 0) {
316
+ console.log("✓ updated package.json dependencies:")
317
+ for (const f of result.fixed) {
318
+ console.log(` ${f.package}@${f.version} (${f.source})`)
319
+ }
320
+ console.log("")
321
+ }
322
+ if (result.skippedFixes && result.skippedFixes.length > 0) {
323
+ console.log("• not auto-fixed:")
324
+ for (const f of result.skippedFixes) {
325
+ console.log(` ${f.package} — ${f.reason}; run \`${f.command.join(" ")}\``)
326
+ }
327
+ console.log("")
328
+ }
329
+ if (result.ok) {
330
+ console.log("✓ every imported package is declared in package.json")
331
+ return true
332
+ }
333
+ // Group by package so the fix ("add X to dependencies") is stated once with its import sites.
334
+ const byPkg = new Map<string, DoctorFinding[]>()
335
+ for (const f of result.findings) {
336
+ const list = byPkg.get(f.package) ?? []
337
+ list.push(f)
338
+ byPkg.set(f.package, list)
339
+ }
340
+ console.log(`✗ ${byPkg.size} package(s) imported but not declared in package.json:\n`)
341
+ for (const [pkg, sites] of [...byPkg.entries()].sort()) {
342
+ console.log(` ${pkg} — add to dependencies (\`bun add ${pkg}\`)`)
343
+ for (const s of sites) console.log(` ${s.file}:${s.line}`)
344
+ }
345
+ console.log(
346
+ "\nThese resolve at Bun runtime via hoisting/workspace but break `tsc` and a standalone install.",
347
+ )
348
+ return false
349
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * `nifra_example` — verified, copy-pasteable code examples for agents. The corpus is `examples.json`
3
+ * (written into this package by `bun run gen:llms`): the same doc snippets `check:docs` typechecks
4
+ * against the live `@nifrajs/*` API. So every example this returns is GUARANTEED to compile against the
5
+ * installed version — the antidote to an agent hallucinating a framework API from stale training.
6
+ *
7
+ * Keyword-scored over each example's name/topic/slug (curated identifiers, heavily weighted) and its
8
+ * code body. No query → a grouped index of what's available (the cheap discovery call).
9
+ */
10
+
11
+ import { countBodyHits, queryTermGroups, tokenize, tokenSetHas } from "./search-terms.ts"
12
+
13
+ export interface Example {
14
+ readonly name: string
15
+ /** The doc page's title, e.g. "Nifra — Auth & sessions". */
16
+ readonly topic: string
17
+ /** The doc page slug, e.g. "auth" — a short, matchable tag. */
18
+ readonly slug: string
19
+ readonly code: string
20
+ }
21
+
22
+ export interface ScoredExample extends Example {
23
+ readonly score: number
24
+ }
25
+
26
+ /** Score examples against the query: a term in the slug/topic/name (the curated, searchable labels)
27
+ * is worth far more than a mention in the code body; body hits cap per term so one long snippet can't
28
+ * outweigh a precisely-tagged one. */
29
+ export function searchExamples(
30
+ examples: readonly Example[],
31
+ query: string,
32
+ limit: number,
33
+ ): ScoredExample[] {
34
+ const terms = queryTermGroups(query)
35
+ if (terms.length === 0) return []
36
+ const scored: ScoredExample[] = []
37
+ for (const ex of examples) {
38
+ const labelTokens = new Set([...tokenize(ex.slug), ...tokenize(ex.topic), ...tokenize(ex.name)])
39
+ const codeLower = ex.code.toLowerCase()
40
+ let score = 0
41
+ for (const term of terms) {
42
+ if (tokenSetHas(labelTokens, term)) score += 8
43
+ score += countBodyHits(codeLower, term, 5)
44
+ }
45
+ if (score > 0) scored.push({ ...ex, score })
46
+ }
47
+ return scored.sort((a, b) => b.score - a.score).slice(0, limit)
48
+ }
49
+
50
+ /** Render the tool result: matched examples as fenced code blocks, or the grouped index when no query. */
51
+ export function renderExamplesResult(
52
+ examples: readonly Example[],
53
+ query: string | undefined,
54
+ limit: number,
55
+ ): string {
56
+ if (query === undefined || query.trim() === "") {
57
+ const byTopic = new Map<string, string[]>()
58
+ for (const ex of examples) {
59
+ const list = byTopic.get(ex.topic) ?? []
60
+ list.push(ex.name)
61
+ byTopic.set(ex.topic, list)
62
+ }
63
+ const index = [...byTopic.entries()]
64
+ .map(([topic, names]) => `- **${topic}**: ${names.join(", ")}`)
65
+ .join("\n")
66
+ return `# nifra examples — index (${examples.length} verified snippets; pass \`query\` to fetch code)\n\nEvery snippet is typechecked against the installed nifra version — prefer these over recalling an API.\n\n${index}`
67
+ }
68
+ const matches = searchExamples(examples, query, limit)
69
+ if (matches.length === 0) {
70
+ return `No examples matched ${JSON.stringify(query)}. Call without a query for the index, or try a broader term (e.g. "auth", "upload", "isr", "client").`
71
+ }
72
+ return matches
73
+ .map((m) => `## ${m.name} — ${m.topic}\n\n\`\`\`ts\n${m.code}\n\`\`\``)
74
+ .join("\n\n---\n\n")
75
+ }
76
+
77
+ /** Load the bundled example corpus. Resolves relative to this module so it works from `src/` (repo dev)
78
+ * and `dist/` (published) alike — `docs/examples.json` sits at the package root next to both. */
79
+ export async function loadExamplesCorpus(): Promise<Example[] | undefined> {
80
+ const url = new URL("../docs/examples.json", import.meta.url)
81
+ try {
82
+ const text = await Bun.file(url).text()
83
+ if (text.length === 0) return undefined
84
+ const parsed = JSON.parse(text) as Example[]
85
+ return Array.isArray(parsed) && parsed.length > 0 ? parsed : undefined
86
+ } catch {
87
+ return undefined
88
+ }
89
+ }