@nifrajs/cli 1.0.0-beta.3 → 1.0.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/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/introspect.d.ts.map +1 -1
- package/dist/introspect.js +5 -0
- package/dist/introspect.js.map +1 -1
- package/dist/mcp-docs-tools.d.ts +3 -2
- package/dist/mcp-docs-tools.d.ts.map +1 -1
- package/dist/mcp-docs-tools.js +34 -3
- package/dist/mcp-docs-tools.js.map +1 -1
- package/dist/mcp-http.d.ts +12 -15
- package/dist/mcp-http.d.ts.map +1 -1
- package/dist/mcp-http.js +15 -101
- package/dist/mcp-http.js.map +1 -1
- package/dist/mcp-protocol.d.ts +4 -94
- package/dist/mcp-protocol.d.ts.map +1 -1
- package/dist/mcp-protocol.js +4 -245
- package/dist/mcp-protocol.js.map +1 -1
- package/dist/mcp-run.d.ts.map +1 -1
- package/dist/mcp-run.js +7 -1
- package/dist/mcp-run.js.map +1 -1
- package/dist/mcp.d.ts +22 -0
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +10 -5
- package/dist/mcp.js.map +1 -1
- package/dist/types-search.d.ts +37 -0
- package/dist/types-search.d.ts.map +1 -0
- package/dist/types-search.js +90 -0
- package/dist/types-search.js.map +1 -0
- package/docs/examples.json +1 -1
- package/docs/llms-full.txt +78 -17
- package/docs/types.json +6386 -0
- package/package.json +7 -6
- package/src/cli.ts +1 -1
- package/src/introspect.ts +5 -0
- package/src/mcp-docs-tools.ts +37 -2
- package/src/mcp-http.ts +19 -109
- package/src/mcp-protocol.ts +4 -323
- package/src/mcp-run.ts +7 -1
- package/src/mcp.ts +10 -5
- package/src/types-search.ts +113 -0
package/src/mcp.ts
CHANGED
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
type McpToolContext,
|
|
47
47
|
rpcError,
|
|
48
48
|
} from "./mcp-protocol.ts"
|
|
49
|
+
import { loadTypesCorpus } from "./types-search.ts"
|
|
49
50
|
|
|
50
51
|
/** Path to a sibling child entry (`mcp-run` / `mcp-render` / `mcp-ws`), resolved next to this module (`.ts` in
|
|
51
52
|
* dev, `.js` once built). Each runs in a FRESH subprocess per call so the project's current code loads. */
|
|
@@ -124,8 +125,9 @@ type PipeSubprocess = ReturnType<typeof Bun.spawn> & {
|
|
|
124
125
|
* reused across newline-delimited `{ id, input }` requests, replying `{ id, output }`. The same machinery
|
|
125
126
|
* powers both `nifra_run warm` and `nifra_render warm` — `child` selects which engine, `label` shapes the
|
|
126
127
|
* cancellation message. The owning handler ({@link createWarmHandler}) fingerprints the source tree and
|
|
127
|
-
* replaces the worker when a file changes, so warm reuse never serves a stale result.
|
|
128
|
-
|
|
128
|
+
* replaces the worker when a file changes, so warm reuse never serves a stale result. Exported for the
|
|
129
|
+
* concurrency test that proves a single per-request cancel doesn't tear down the shared worker. */
|
|
130
|
+
export class WarmWorker {
|
|
129
131
|
private readonly proc: PipeSubprocess
|
|
130
132
|
private readonly pending = new Map<
|
|
131
133
|
number,
|
|
@@ -176,8 +178,11 @@ class WarmWorker {
|
|
|
176
178
|
const id = ++this.nextId
|
|
177
179
|
return new Promise((resolve, reject) => {
|
|
178
180
|
const abort = (): void => {
|
|
181
|
+
// Per-request cancel: drop just THIS id and resolve its cancellation. The worker is shared
|
|
182
|
+
// across concurrent requests (`pending` is id-keyed for exactly this reason), so killing the
|
|
183
|
+
// process here would reject every OTHER in-flight request via the `exited` handler and force a
|
|
184
|
+
// cold rebuild. Leave it hot — `createWarmHandler` already replaces it on file change.
|
|
179
185
|
this.pending.delete(id)
|
|
180
|
-
this.stop()
|
|
181
186
|
const reason = typeof signal?.reason === "string" ? `: ${signal.reason}` : ""
|
|
182
187
|
resolve(`${this.label} cancelled${reason}.`)
|
|
183
188
|
}
|
|
@@ -764,7 +769,7 @@ export function projectTools(
|
|
|
764
769
|
},
|
|
765
770
|
// nifra_docs + nifra_example are project-independent (corpus-backed); the shared factory keeps their
|
|
766
771
|
// definitions identical to the CLI HTTP server and the site's edge worker.
|
|
767
|
-
...docsTools(loadDocsCorpus, loadExamplesCorpus),
|
|
772
|
+
...docsTools(loadDocsCorpus, loadExamplesCorpus, loadTypesCorpus),
|
|
768
773
|
{
|
|
769
774
|
name: "nifra_scaffold",
|
|
770
775
|
description:
|
|
@@ -900,7 +905,7 @@ export async function runMcpServer(cwd: string, version: string): Promise<void>
|
|
|
900
905
|
allResources.push(...(ns.features.resources ?? []))
|
|
901
906
|
allPrompts.push(...(ns.features.prompts ?? []))
|
|
902
907
|
}
|
|
903
|
-
tools = [...docsTools(loadDocsCorpus, loadExamplesCorpus), ...allTools]
|
|
908
|
+
tools = [...docsTools(loadDocsCorpus, loadExamplesCorpus, loadTypesCorpus), ...allTools]
|
|
904
909
|
features = { resources: allResources, prompts: allPrompts }
|
|
905
910
|
} else {
|
|
906
911
|
const loadAppCached = createCachedAppLoader(cwd)
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `nifra_types` — the EXACT TypeScript of every exported `@nifrajs/*` symbol. The corpus is `types.json`
|
|
3
|
+
* (written into this package by `bun run gen:llms`): each entry's `signature` is the literal declaration
|
|
4
|
+
* parsed from the package's BUILT `.d.ts` with the TS compiler — a clean signature, never prose, never
|
|
5
|
+
* truncated. So an agent that needs the precise shape of a type (`RateLimitStore`, `RouteSchema`, a
|
|
6
|
+
* function signature) gets the authoritative source here and never has to read a `.d.ts` file.
|
|
7
|
+
*
|
|
8
|
+
* Lookup by `name` is exact (case-insensitive); `query` keyword-scores over the name (heavy) + the
|
|
9
|
+
* signature/doc body; no args → a per-package index of the available type names.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { countBodyHits, queryTermGroups, tokenize, tokenSetHas } from "./search-terms.ts"
|
|
13
|
+
|
|
14
|
+
export interface TypeEntry {
|
|
15
|
+
readonly name: string
|
|
16
|
+
readonly kind: "interface" | "type" | "class" | "function" | "enum" | "const"
|
|
17
|
+
readonly package: string
|
|
18
|
+
/** The literal declaration text from the `.d.ts` — a clean signature, no implementation. */
|
|
19
|
+
readonly signature: string
|
|
20
|
+
/** The declaration's JSDoc block, if any. */
|
|
21
|
+
readonly doc?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ScoredType extends TypeEntry {
|
|
25
|
+
readonly score: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Exact (case-insensitive) name lookup — returns every declaration with that name (a name can exist in
|
|
29
|
+
* more than one package). */
|
|
30
|
+
export function lookupType(types: readonly TypeEntry[], name: string): TypeEntry[] {
|
|
31
|
+
const wanted = name.trim().toLowerCase()
|
|
32
|
+
return types.filter((t) => t.name.toLowerCase() === wanted)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Keyword-score types: a term in the name is worth far more than one in the signature/doc body. */
|
|
36
|
+
export function searchTypes(
|
|
37
|
+
types: readonly TypeEntry[],
|
|
38
|
+
query: string,
|
|
39
|
+
limit: number,
|
|
40
|
+
): ScoredType[] {
|
|
41
|
+
const terms = queryTermGroups(query)
|
|
42
|
+
if (terms.length === 0) return []
|
|
43
|
+
const scored: ScoredType[] = []
|
|
44
|
+
for (const t of types) {
|
|
45
|
+
const nameTokens = new Set(tokenize(t.name))
|
|
46
|
+
const body = `${t.signature} ${t.doc ?? ""}`.toLowerCase()
|
|
47
|
+
let score = 0
|
|
48
|
+
for (const term of terms) {
|
|
49
|
+
if (tokenSetHas(nameTokens, term)) score += 8
|
|
50
|
+
score += countBodyHits(body, term, 4)
|
|
51
|
+
}
|
|
52
|
+
if (score > 0) scored.push({ ...t, score })
|
|
53
|
+
}
|
|
54
|
+
return scored.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name)).slice(0, limit)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function renderEntry(t: TypeEntry): string {
|
|
58
|
+
const head = `## ${t.name} — ${t.kind}, \`${t.package}\``
|
|
59
|
+
const doc = t.doc ? `${t.doc}\n` : ""
|
|
60
|
+
return `${head}\n\n\`\`\`ts\n${doc}${t.signature}\n\`\`\``
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Render the tool result. `name` → the exact declaration(s); else `query` → the top matches; else a
|
|
65
|
+
* per-package index of type names. The signatures are authoritative (generated from the built `.d.ts`),
|
|
66
|
+
* so this is the complete answer — there is no need to read a `.d.ts` file.
|
|
67
|
+
*/
|
|
68
|
+
export function renderTypesResult(
|
|
69
|
+
types: readonly TypeEntry[],
|
|
70
|
+
name: string | undefined,
|
|
71
|
+
query: string | undefined,
|
|
72
|
+
limit: number,
|
|
73
|
+
): string {
|
|
74
|
+
if (name !== undefined && name.trim() !== "") {
|
|
75
|
+
const hits = lookupType(types, name)
|
|
76
|
+
if (hits.length === 0) {
|
|
77
|
+
return `No exported type named ${JSON.stringify(name)} in @nifrajs/*. Call nifra_types with a \`query\` (e.g. "rate limit") or no args for the per-package index of names.`
|
|
78
|
+
}
|
|
79
|
+
return hits.map(renderEntry).join("\n\n---\n\n")
|
|
80
|
+
}
|
|
81
|
+
if (query !== undefined && query.trim() !== "") {
|
|
82
|
+
const matches = searchTypes(types, query, limit)
|
|
83
|
+
if (matches.length === 0) {
|
|
84
|
+
return `No types matched ${JSON.stringify(query)}. Try a broader term, pass an exact \`name\`, or call with no args for the index.`
|
|
85
|
+
}
|
|
86
|
+
return matches.map(renderEntry).join("\n\n---\n\n")
|
|
87
|
+
}
|
|
88
|
+
const byPkg = new Map<string, string[]>()
|
|
89
|
+
for (const t of types) {
|
|
90
|
+
const list = byPkg.get(t.package) ?? []
|
|
91
|
+
list.push(t.name)
|
|
92
|
+
byPkg.set(t.package, list)
|
|
93
|
+
}
|
|
94
|
+
const index = [...byPkg.entries()]
|
|
95
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
96
|
+
.map(([pkg, names]) => `- **${pkg}**: ${names.sort().join(", ")}`)
|
|
97
|
+
.join("\n")
|
|
98
|
+
return `# nifra types — index (${types.length} exported symbols; pass \`name\` for the exact declaration, or \`query\` to search)\n\nSignatures are generated from the built \`.d.ts\` — authoritative + complete. Use this instead of reading any \`.d.ts\` file.\n\n${index}`
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Load the bundled type corpus (`docs/types.json`), resolved relative to this module so it works from
|
|
102
|
+
* `src/` (dev) and `dist/` (published) alike. */
|
|
103
|
+
export async function loadTypesCorpus(): Promise<TypeEntry[] | undefined> {
|
|
104
|
+
const url = new URL("../docs/types.json", import.meta.url)
|
|
105
|
+
try {
|
|
106
|
+
const text = await Bun.file(url).text()
|
|
107
|
+
if (text.length === 0) return undefined
|
|
108
|
+
const parsed = JSON.parse(text) as TypeEntry[]
|
|
109
|
+
return Array.isArray(parsed) && parsed.length > 0 ? parsed : undefined
|
|
110
|
+
} catch {
|
|
111
|
+
return undefined
|
|
112
|
+
}
|
|
113
|
+
}
|