@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.
- package/LICENSE +21 -0
- package/README.md +99 -0
- package/dist/check.d.ts +100 -0
- package/dist/check.d.ts.map +1 -0
- package/dist/check.js +690 -0
- package/dist/check.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +208 -0
- package/dist/cli.js.map +1 -0
- package/dist/docs-search.d.ts +30 -0
- package/dist/docs-search.d.ts.map +1 -0
- package/dist/docs-search.js +94 -0
- package/dist/docs-search.js.map +1 -0
- package/dist/doctor.d.ts +52 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +291 -0
- package/dist/doctor.js.map +1 -0
- package/dist/examples.d.ts +30 -0
- package/dist/examples.d.ts.map +1 -0
- package/dist/examples.js +70 -0
- package/dist/examples.js.map +1 -0
- package/dist/introspect.d.ts +64 -0
- package/dist/introspect.d.ts.map +1 -0
- package/dist/introspect.js +243 -0
- package/dist/introspect.js.map +1 -0
- package/dist/load.d.ts +51 -0
- package/dist/load.d.ts.map +1 -0
- package/dist/load.js +70 -0
- package/dist/load.js.map +1 -0
- package/dist/mcp-docs-tools.d.ts +13 -0
- package/dist/mcp-docs-tools.d.ts.map +1 -0
- package/dist/mcp-docs-tools.js +63 -0
- package/dist/mcp-docs-tools.js.map +1 -0
- package/dist/mcp-http.d.ts +43 -0
- package/dist/mcp-http.d.ts.map +1 -0
- package/dist/mcp-http.js +136 -0
- package/dist/mcp-http.js.map +1 -0
- package/dist/mcp-protocol.d.ts +97 -0
- package/dist/mcp-protocol.d.ts.map +1 -0
- package/dist/mcp-protocol.js +248 -0
- package/dist/mcp-protocol.js.map +1 -0
- package/dist/mcp-render.d.ts +17 -0
- package/dist/mcp-render.d.ts.map +1 -0
- package/dist/mcp-render.js +73 -0
- package/dist/mcp-render.js.map +1 -0
- package/dist/mcp-run.d.ts +24 -0
- package/dist/mcp-run.d.ts.map +1 -0
- package/dist/mcp-run.js +142 -0
- package/dist/mcp-run.js.map +1 -0
- package/dist/mcp-ws.d.ts +26 -0
- package/dist/mcp-ws.d.ts.map +1 -0
- package/dist/mcp-ws.js +258 -0
- package/dist/mcp-ws.js.map +1 -0
- package/dist/mcp.d.ts +42 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +712 -0
- package/dist/mcp.js.map +1 -0
- package/dist/openapi-tool.d.ts +6 -0
- package/dist/openapi-tool.d.ts.map +1 -0
- package/dist/openapi-tool.js +48 -0
- package/dist/openapi-tool.js.map +1 -0
- package/dist/scaffold.d.ts +35 -0
- package/dist/scaffold.d.ts.map +1 -0
- package/dist/scaffold.js +140 -0
- package/dist/scaffold.js.map +1 -0
- package/dist/search-terms.d.ts +17 -0
- package/dist/search-terms.d.ts.map +1 -0
- package/dist/search-terms.js +82 -0
- package/dist/search-terms.js.map +1 -0
- package/dist/test-tool.d.ts +29 -0
- package/dist/test-tool.d.ts.map +1 -0
- package/dist/test-tool.js +150 -0
- package/dist/test-tool.js.map +1 -0
- package/docs/examples.json +314 -0
- package/docs/llms-full.txt +4325 -0
- package/package.json +58 -0
- package/src/check.ts +790 -0
- package/src/cli.ts +237 -0
- package/src/docs-search.ts +111 -0
- package/src/doctor.ts +349 -0
- package/src/examples.ts +89 -0
- package/src/introspect.ts +284 -0
- package/src/load.ts +129 -0
- package/src/mcp-docs-tools.ts +72 -0
- package/src/mcp-http.ts +155 -0
- package/src/mcp-protocol.ts +326 -0
- package/src/mcp-render.ts +78 -0
- package/src/mcp-run.ts +153 -0
- package/src/mcp-ws.ts +328 -0
- package/src/mcp.ts +863 -0
- package/src/openapi-tool.ts +51 -0
- package/src/scaffold.ts +164 -0
- package/src/search-terms.ts +89 -0
- package/src/test-tool.ts +181 -0
package/src/cli.ts
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { inProcessClient } from "@nifrajs/client"
|
|
3
|
+
import { createWebApp, type RenderAdapter } from "@nifrajs/web"
|
|
4
|
+
import { discoverRoutes } from "@nifrajs/web/fs"
|
|
5
|
+
/**
|
|
6
|
+
* `nifra` — the zero-config CLI for a nifra app. Reads `framework.ts` + `backend.ts` + `routes/` from
|
|
7
|
+
* the project root (see {@link loadApp}) and wires the right `@nifrajs/web` entrypoint:
|
|
8
|
+
*
|
|
9
|
+
* nifra dev true-HMR dev server (Vite middleware + nifra SSR) — @nifrajs/web/vite
|
|
10
|
+
* nifra build bundle the client (content-hashed) + write manifest.json — @nifrajs/web/build
|
|
11
|
+
* nifra start serve the built client + SSR on Bun.serve — @nifrajs/web
|
|
12
|
+
*
|
|
13
|
+
* Bun-only (it runs the framework's TS + Bun plugins directly). The *output* runs anywhere.
|
|
14
|
+
*/
|
|
15
|
+
import type { BunPlugin } from "bun"
|
|
16
|
+
import { describeProject } from "./introspect.ts"
|
|
17
|
+
import { type LoadedApp, loadApp, resolvePlugins } from "./load.ts"
|
|
18
|
+
|
|
19
|
+
interface Flags {
|
|
20
|
+
readonly port: number
|
|
21
|
+
readonly out: string
|
|
22
|
+
readonly poll: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const HELP = `nifra — zero-config dev/build/start for a nifra app
|
|
26
|
+
|
|
27
|
+
Usage:
|
|
28
|
+
nifra dev [--port <n>] [--poll] Start the true-HMR dev server (Vite).
|
|
29
|
+
nifra build [--out <dir>] Bundle the client + write manifest.json.
|
|
30
|
+
nifra start [--port <n>] [--out <dir>] Serve the built app (client + SSR) on Bun.
|
|
31
|
+
nifra context Print this project's API + page routes + conventions
|
|
32
|
+
(for piping into an AI coding agent's prompt).
|
|
33
|
+
nifra mcp Start an MCP server (stdio) exposing this project to a coding
|
|
34
|
+
agent: nifra_context, nifra_routes (routes+schemas as JSON),
|
|
35
|
+
nifra_run (backend), nifra_render (SSR a page), nifra_docs,
|
|
36
|
+
nifra_example (verified snippets), nifra_scaffold (route→file),
|
|
37
|
+
nifra_check (drift gate + fixes), nifra_doctor (undeclared deps).
|
|
38
|
+
nifra docs-mcp [--port <n>] Serve the PUBLIC docs MCP over HTTP (nifra_docs + nifra_example) —
|
|
39
|
+
self-host on a VPS so any remote agent can learn nifra. Default :8787.
|
|
40
|
+
nifra check [--json] [--lints-only] Gate: typecheck + lints (hand-rolled fetch(), untyped client("…"),
|
|
41
|
+
server-only imports in routes/). Run as "done"; --json for agents;
|
|
42
|
+
--lints-only skips tsc for a near-instant inner-loop pass.
|
|
43
|
+
nifra doctor [--json] [--auto-fix] Flag packages imported in source but missing from package.json
|
|
44
|
+
(resolve at Bun runtime, break tsc + standalone install);
|
|
45
|
+
--auto-fix writes safe local-version dependency fixes.
|
|
46
|
+
|
|
47
|
+
Reads nifra.config.ts (adapter + clientModule + plugins; or framework.ts), backend.ts (optional), and
|
|
48
|
+
routes/ from the current directory. Run from your project root.`
|
|
49
|
+
|
|
50
|
+
const CLI_VERSION = "0.1.0-alpha.1"
|
|
51
|
+
|
|
52
|
+
// A render adapter + nifra server are opaque to the CLI (it just forwards them); cast at the seam.
|
|
53
|
+
const asAdapter = (v: unknown): RenderAdapter => v as RenderAdapter
|
|
54
|
+
const asBunPlugins = (v: readonly unknown[]): BunPlugin[] => v as BunPlugin[]
|
|
55
|
+
const apiOf = (backend: unknown): { api?: unknown } =>
|
|
56
|
+
backend === undefined ? {} : { api: inProcessClient(backend as never) }
|
|
57
|
+
|
|
58
|
+
async function dev(app: LoadedApp, flags: Flags): Promise<void> {
|
|
59
|
+
const { plugin } = await import("bun")
|
|
60
|
+
const { createViteDevServer } = await import("@nifrajs/web/vite")
|
|
61
|
+
const { framework: fw, routesDir, cwd, backend } = app
|
|
62
|
+
// nifra SSRs each request by importing the route modules through Bun (not Vite's ssrLoadModule), so
|
|
63
|
+
// `.vue`/`.svelte`/Solid routes need their SSR compile plugin registered on Bun's runtime first —
|
|
64
|
+
// the dev analog of `nifra start`. React/Preact JSX is Bun-native → no server plugins → no-op.
|
|
65
|
+
for (const p of asBunPlugins(await resolvePlugins(fw.serverPlugins))) plugin(p)
|
|
66
|
+
const server = await createViteDevServer({
|
|
67
|
+
root: cwd,
|
|
68
|
+
routesDir,
|
|
69
|
+
clientModule: fw.clientModule,
|
|
70
|
+
plugins: await resolvePlugins(fw.vitePlugins),
|
|
71
|
+
poll: flags.poll,
|
|
72
|
+
port: flags.port,
|
|
73
|
+
...(fw.conditions ? { conditions: fw.conditions } : {}),
|
|
74
|
+
...(fw.define ? { define: fw.define } : {}),
|
|
75
|
+
createApp: (clientEntry, importQuery) =>
|
|
76
|
+
createWebApp({
|
|
77
|
+
adapter: asAdapter(fw.adapter),
|
|
78
|
+
manifest: discoverRoutes(routesDir, { importQuery }),
|
|
79
|
+
clientEntry,
|
|
80
|
+
...apiOf(backend),
|
|
81
|
+
}),
|
|
82
|
+
})
|
|
83
|
+
console.log(`nifra dev → http://localhost:${server.port}`)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function build(app: LoadedApp): Promise<void> {
|
|
87
|
+
const { buildClient } = await import("@nifrajs/web/build")
|
|
88
|
+
const { framework: fw, routesDir, outDir } = app
|
|
89
|
+
const manifest = await buildClient({
|
|
90
|
+
routesDir,
|
|
91
|
+
outDir,
|
|
92
|
+
clientModule: fw.clientModule,
|
|
93
|
+
plugins: asBunPlugins(await resolvePlugins(fw.clientPlugins)),
|
|
94
|
+
define: { "process.env.NODE_ENV": '"production"', ...(fw.define ?? {}) },
|
|
95
|
+
...(fw.conditions ? { conditions: fw.conditions } : {}),
|
|
96
|
+
})
|
|
97
|
+
console.log(`nifra build → ${outDir} (entry ${manifest.entry}${manifest.css ? ", + css" : ""})`)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
interface BuiltManifest {
|
|
101
|
+
readonly entry: string
|
|
102
|
+
readonly routes?: Readonly<Record<string, readonly string[]>>
|
|
103
|
+
readonly css?: readonly string[]
|
|
104
|
+
readonly routeStyles?: Readonly<Record<string, readonly string[]>>
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function start(app: LoadedApp, flags: Flags): Promise<void> {
|
|
108
|
+
const { plugin } = await import("bun")
|
|
109
|
+
const { framework: fw, routesDir, outDir, backend } = app
|
|
110
|
+
// Register the framework's SSR Bun plugins so `.vue`/`.svelte`/Solid route files compile on the
|
|
111
|
+
// server's runtime import (React/Preact JSX is Bun-native → none).
|
|
112
|
+
for (const p of asBunPlugins(await resolvePlugins(fw.serverPlugins))) plugin(p)
|
|
113
|
+
|
|
114
|
+
const manifestFile = Bun.file(`${outDir}/manifest.json`)
|
|
115
|
+
if (!(await manifestFile.exists())) {
|
|
116
|
+
throw new Error(`[nifra] no ${outDir}/manifest.json — run \`nifra build\` first.`)
|
|
117
|
+
}
|
|
118
|
+
const manifest = JSON.parse(await manifestFile.text()) as BuiltManifest
|
|
119
|
+
|
|
120
|
+
const server = createWebApp({
|
|
121
|
+
adapter: asAdapter(fw.adapter),
|
|
122
|
+
manifest: discoverRoutes(routesDir),
|
|
123
|
+
clientEntry: manifest.entry,
|
|
124
|
+
...(manifest.routes ? { routePreload: manifest.routes } : {}),
|
|
125
|
+
...(manifest.css ? { styles: manifest.css } : {}),
|
|
126
|
+
...(manifest.routeStyles ? { routeStyles: manifest.routeStyles } : {}),
|
|
127
|
+
...apiOf(backend),
|
|
128
|
+
})
|
|
129
|
+
// Serve the content-hashed bundle (immutable). `.css` → text/css, else JS. Name is path-validated.
|
|
130
|
+
server.get("/assets/*", async (c) => {
|
|
131
|
+
const name = new URL(c.req.url).pathname.slice("/assets/".length)
|
|
132
|
+
if (!/^[A-Za-z0-9._-]+$/.test(name)) return new Response("bad request", { status: 400 })
|
|
133
|
+
const file = Bun.file(`${outDir}/${name}`)
|
|
134
|
+
if (!(await file.exists())) return new Response("Not Found", { status: 404 })
|
|
135
|
+
const type = name.endsWith(".css") ? "text/css" : "text/javascript"
|
|
136
|
+
return new Response(file, {
|
|
137
|
+
headers: {
|
|
138
|
+
"content-type": `${type}; charset=utf-8`,
|
|
139
|
+
"cache-control": "public, max-age=31536000, immutable",
|
|
140
|
+
},
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
const running = server.listen(flags.port)
|
|
144
|
+
console.log(`nifra start → http://localhost:${running.port}`)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function parseFlags(args: readonly string[]): Flags {
|
|
148
|
+
let port = Number(Bun.env.PORT ?? 3000)
|
|
149
|
+
let out = "dist"
|
|
150
|
+
let poll = Bun.env.CHOKIDAR_USEPOLLING === "1"
|
|
151
|
+
for (let i = 0; i < args.length; i++) {
|
|
152
|
+
const a = args[i]
|
|
153
|
+
if ((a === "--port" || a === "-p") && args[i + 1]) port = Number(args[++i])
|
|
154
|
+
else if (a === "--out" && args[i + 1]) out = args[++i] as string
|
|
155
|
+
else if (a === "--poll") poll = true
|
|
156
|
+
}
|
|
157
|
+
if (!Number.isFinite(port) || port < 0 || port > 65535) {
|
|
158
|
+
throw new Error(`[nifra] invalid --port: ${port}`)
|
|
159
|
+
}
|
|
160
|
+
return { port, out, poll }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function main(): Promise<void> {
|
|
164
|
+
const argv = Bun.argv.slice(2)
|
|
165
|
+
const command = argv[0]
|
|
166
|
+
if (command === undefined || command === "--help" || command === "-h" || command === "help") {
|
|
167
|
+
console.log(HELP)
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
if (command === "--version" || command === "-v") {
|
|
171
|
+
console.log(`nifra (@nifrajs/cli) ${CLI_VERSION}`)
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
// `mcp` runs a long-lived stdio server and loads the project lazily per-tool — it must not go through
|
|
175
|
+
// the eager `loadApp` below (which would fail fast on a project that's API-only / not yet built).
|
|
176
|
+
if (command === "mcp") {
|
|
177
|
+
const { runMcpServer } = await import("./mcp.ts")
|
|
178
|
+
await runMcpServer(process.cwd(), CLI_VERSION)
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
// `docs-mcp` runs the PUBLIC docs MCP over HTTP — project-independent (serves the bundled corpus), so
|
|
182
|
+
// it self-hosts anywhere Bun runs (a VPS behind a reverse proxy, a container). Long-lived; no loadApp.
|
|
183
|
+
if (command === "docs-mcp") {
|
|
184
|
+
const { handleMcpHttp } = await import("./mcp-http.ts")
|
|
185
|
+
const portArg = argv[argv.indexOf("--port") + 1]
|
|
186
|
+
const port = Number(portArg && argv.includes("--port") ? portArg : (Bun.env.PORT ?? 8787))
|
|
187
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
188
|
+
throw new Error(`[nifra] invalid --port: ${port}`)
|
|
189
|
+
}
|
|
190
|
+
const server = Bun.serve({ port, fetch: handleMcpHttp })
|
|
191
|
+
console.log(`nifra docs MCP (HTTP) → ${server.url}`)
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
// `check` is a pure cwd-based gate (typecheck + lint) — it must run even when the project doesn't
|
|
195
|
+
// load (API-only, not built yet), so it dispatches before the eager `loadApp` below.
|
|
196
|
+
if (command === "check") {
|
|
197
|
+
const { runCheck } = await import("./check.ts")
|
|
198
|
+
if (
|
|
199
|
+
!(await runCheck(process.cwd(), {
|
|
200
|
+
json: argv.includes("--json"),
|
|
201
|
+
lintsOnly: argv.includes("--lints-only"),
|
|
202
|
+
}))
|
|
203
|
+
)
|
|
204
|
+
process.exitCode = 1
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
// `doctor` is a pure cwd check (imports vs declared deps) — like `check`, dispatch before `loadApp`
|
|
208
|
+
// so it runs on an API-only / not-yet-built project.
|
|
209
|
+
if (command === "doctor") {
|
|
210
|
+
const { runDoctor } = await import("./doctor.ts")
|
|
211
|
+
if (
|
|
212
|
+
!(await runDoctor(process.cwd(), {
|
|
213
|
+
json: argv.includes("--json"),
|
|
214
|
+
autoFix: argv.includes("--auto-fix") || argv.includes("--fix"),
|
|
215
|
+
}))
|
|
216
|
+
)
|
|
217
|
+
process.exitCode = 1
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
if (command !== "dev" && command !== "build" && command !== "start" && command !== "context") {
|
|
221
|
+
console.error(`[nifra] unknown command: ${command}\n`)
|
|
222
|
+
console.error(HELP)
|
|
223
|
+
process.exitCode = 1
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
const flags = parseFlags(argv.slice(1))
|
|
227
|
+
const app = await loadApp(process.cwd(), flags.out)
|
|
228
|
+
if (command === "dev") await dev(app, flags)
|
|
229
|
+
else if (command === "build") await build(app)
|
|
230
|
+
else if (command === "context") console.log(describeProject(app))
|
|
231
|
+
else await start(app, flags)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
main().catch((err) => {
|
|
235
|
+
console.error(err instanceof Error ? err.message : String(err))
|
|
236
|
+
process.exitCode = 1
|
|
237
|
+
})
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `nifra_docs` — searchable framework documentation for agents. The corpus is the generated
|
|
3
|
+
* `llms-full.txt` (written into this package by `bun run gen:llms`, so it can't drift from the
|
|
4
|
+
* source it's generated from). Instead of dumping the whole ~150 KB into an agent's context, the
|
|
5
|
+
* tool keyword-scores the document's `##` sections and returns only the top matches — or, with no
|
|
6
|
+
* query, just the section index (the cheap discovery call).
|
|
7
|
+
*
|
|
8
|
+
* Deliberately NOT a vector search: the corpus is one curated document with descriptive headings;
|
|
9
|
+
* weighted keyword scoring over sections is deterministic, dependency-free, and good enough that
|
|
10
|
+
* embedding infra would be complexity without a payoff.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { countBodyHits, queryTermGroups, tokenize, tokenSetHas } from "./search-terms.ts"
|
|
14
|
+
|
|
15
|
+
export interface DocSection {
|
|
16
|
+
readonly heading: string
|
|
17
|
+
readonly body: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Split the generated doc into its `##` sections (the `# title` intro becomes "Overview"). */
|
|
21
|
+
export function splitSections(markdown: string): DocSection[] {
|
|
22
|
+
const out: DocSection[] = []
|
|
23
|
+
const lines = markdown.split("\n")
|
|
24
|
+
let heading = "Overview"
|
|
25
|
+
let buf: string[] = []
|
|
26
|
+
const flush = (): void => {
|
|
27
|
+
const body = buf.join("\n").trim()
|
|
28
|
+
if (body.length > 0) out.push({ heading, body })
|
|
29
|
+
buf = []
|
|
30
|
+
}
|
|
31
|
+
for (const line of lines) {
|
|
32
|
+
if (line.startsWith("## ")) {
|
|
33
|
+
flush()
|
|
34
|
+
heading = line.slice(3).trim()
|
|
35
|
+
} else {
|
|
36
|
+
buf.push(line)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
flush()
|
|
40
|
+
return out
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ScoredSection extends DocSection {
|
|
44
|
+
readonly score: number
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Score sections against the query: a term in the HEADING is worth far more than body mentions
|
|
48
|
+
* (headings are curated names for exactly these lookups); body occurrences cap per term so one
|
|
49
|
+
* spammy section can't drown a precisely-titled one. */
|
|
50
|
+
export function searchSections(
|
|
51
|
+
sections: readonly DocSection[],
|
|
52
|
+
query: string,
|
|
53
|
+
limit: number,
|
|
54
|
+
): ScoredSection[] {
|
|
55
|
+
const terms = queryTermGroups(query)
|
|
56
|
+
if (terms.length === 0) return []
|
|
57
|
+
const scored: ScoredSection[] = []
|
|
58
|
+
for (const section of sections) {
|
|
59
|
+
const headingTokens = new Set(tokenize(section.heading))
|
|
60
|
+
const bodyLower = section.body.toLowerCase()
|
|
61
|
+
let score = 0
|
|
62
|
+
for (const term of terms) {
|
|
63
|
+
if (tokenSetHas(headingTokens, term)) score += 8
|
|
64
|
+
score += countBodyHits(bodyLower, term, 5)
|
|
65
|
+
}
|
|
66
|
+
if (score > 0) scored.push({ ...section, score })
|
|
67
|
+
}
|
|
68
|
+
return scored.sort((a, b) => b.score - a.score).slice(0, limit)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Cap a section body for the tool result — whole sections can be pages long; the agent can ask
|
|
72
|
+
* again with a narrower query if the trim cuts what it needed. */
|
|
73
|
+
const MAX_SECTION_CHARS = 2800
|
|
74
|
+
|
|
75
|
+
/** Render the tool result: matched sections, or the index when no query was given. */
|
|
76
|
+
export function renderDocsResult(
|
|
77
|
+
markdown: string,
|
|
78
|
+
query: string | undefined,
|
|
79
|
+
limit: number,
|
|
80
|
+
): string {
|
|
81
|
+
const sections = splitSections(markdown)
|
|
82
|
+
if (query === undefined || query.trim() === "") {
|
|
83
|
+
const index = sections.map((s) => `- ${s.heading}`).join("\n")
|
|
84
|
+
return `# nifra docs — section index (pass \`query\` to fetch matching sections)\n\n${index}`
|
|
85
|
+
}
|
|
86
|
+
const matches = searchSections(sections, query, limit)
|
|
87
|
+
if (matches.length === 0) {
|
|
88
|
+
return `No docs sections matched ${JSON.stringify(query)}. Call without a query for the section index.`
|
|
89
|
+
}
|
|
90
|
+
return matches
|
|
91
|
+
.map((m) => {
|
|
92
|
+
const body =
|
|
93
|
+
m.body.length > MAX_SECTION_CHARS
|
|
94
|
+
? `${m.body.slice(0, MAX_SECTION_CHARS)}\n…(trimmed — narrow the query for the rest)`
|
|
95
|
+
: m.body
|
|
96
|
+
return `## ${m.heading}\n\n${body}`
|
|
97
|
+
})
|
|
98
|
+
.join("\n\n---\n\n")
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Load the bundled corpus. Resolves relative to this module so it works from `src/` (repo dev)
|
|
102
|
+
* and `dist/` (published) alike — `docs/llms-full.txt` sits at the package root next to both. */
|
|
103
|
+
export async function loadDocsCorpus(): Promise<string | undefined> {
|
|
104
|
+
const url = new URL("../docs/llms-full.txt", import.meta.url)
|
|
105
|
+
try {
|
|
106
|
+
const text = await Bun.file(url).text()
|
|
107
|
+
return text.length > 0 ? text : undefined
|
|
108
|
+
} catch {
|
|
109
|
+
return undefined
|
|
110
|
+
}
|
|
111
|
+
}
|