@nifrajs/cli 2.2.0 → 2.4.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/README.md +25 -25
- package/dist/assure.d.ts +2 -2
- package/dist/assure.d.ts.map +1 -1
- package/dist/assure.js +17 -10
- package/dist/assure.js.map +1 -1
- package/dist/capabilities-tool.d.ts +7 -0
- package/dist/capabilities-tool.d.ts.map +1 -1
- package/dist/capabilities-tool.js +68 -16
- package/dist/capabilities-tool.js.map +1 -1
- package/dist/check.d.ts +61 -22
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +336 -87
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts +30 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +148 -39
- package/dist/cli.js.map +1 -1
- package/dist/diff-tool.d.ts +3 -3
- package/dist/diff-tool.js +7 -7
- package/dist/docs-search.d.ts +3 -3
- package/dist/docs-search.js +6 -6
- package/dist/doctor.d.ts +3 -3
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +12 -8
- package/dist/doctor.js.map +1 -1
- package/dist/examples.d.ts +5 -5
- package/dist/examples.js +5 -5
- package/dist/init-agents.d.ts +14 -14
- package/dist/init-agents.js +14 -14
- package/dist/internal/typescript-import.d.ts +24 -0
- package/dist/internal/typescript-import.d.ts.map +1 -0
- package/dist/internal/typescript-import.js +32 -0
- package/dist/internal/typescript-import.js.map +1 -0
- package/dist/introspect.d.ts +12 -12
- package/dist/introspect.js +24 -24
- package/dist/learn.d.ts +32 -0
- package/dist/learn.d.ts.map +1 -0
- package/dist/learn.js +127 -0
- package/dist/learn.js.map +1 -0
- package/dist/levels-tool.d.ts +8 -8
- package/dist/levels-tool.d.ts.map +1 -1
- package/dist/levels-tool.js +33 -30
- package/dist/levels-tool.js.map +1 -1
- package/dist/load.d.ts +6 -6
- package/dist/load.d.ts.map +1 -1
- package/dist/load.js +15 -7
- package/dist/load.js.map +1 -1
- package/dist/manifest-tool.d.ts +1 -1
- package/dist/manifest-tool.d.ts.map +1 -1
- package/dist/manifest-tool.js +4 -2
- package/dist/manifest-tool.js.map +1 -1
- package/dist/mcp-app-tool.d.ts +14 -0
- package/dist/mcp-app-tool.d.ts.map +1 -0
- package/dist/mcp-app-tool.js +80 -0
- package/dist/mcp-app-tool.js.map +1 -0
- package/dist/mcp-docs-tools.d.ts +1 -1
- package/dist/mcp-docs-tools.d.ts.map +1 -1
- package/dist/mcp-docs-tools.js +25 -9
- package/dist/mcp-docs-tools.js.map +1 -1
- package/dist/mcp-http.d.ts +14 -10
- package/dist/mcp-http.d.ts.map +1 -1
- package/dist/mcp-http.js +30 -16
- package/dist/mcp-http.js.map +1 -1
- package/dist/mcp-protocol.d.ts +2 -2
- package/dist/mcp-protocol.js +2 -2
- package/dist/mcp-render.d.ts +3 -3
- package/dist/mcp-render.js +6 -6
- package/dist/mcp-run.d.ts +2 -2
- package/dist/mcp-run.js +2 -2
- package/dist/mcp.d.ts +28 -14
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +263 -45
- package/dist/mcp.js.map +1 -1
- package/dist/openapi-tool.d.ts +1 -1
- package/dist/openapi-tool.js +2 -2
- package/dist/pipeline-guard.d.ts +1 -1
- package/dist/pipeline-guard.js +1 -1
- package/dist/port.d.ts +9 -9
- package/dist/port.js +27 -27
- package/dist/recipes/1.8.0.d.ts +2 -2
- package/dist/recipes/1.8.0.js +5 -5
- package/dist/recipes/index.d.ts +2 -2
- package/dist/recipes/index.js +1 -1
- package/dist/scaffold.d.ts +3 -3
- package/dist/scaffold.js +8 -8
- package/dist/sync-routes.d.ts +28 -0
- package/dist/sync-routes.d.ts.map +1 -0
- package/dist/sync-routes.js +55 -0
- package/dist/sync-routes.js.map +1 -0
- package/dist/types-search.d.ts +5 -5
- package/dist/types-search.js +7 -7
- package/dist/upgrade.d.ts +20 -9
- package/dist/upgrade.d.ts.map +1 -1
- package/dist/upgrade.js +117 -19
- package/dist/upgrade.js.map +1 -1
- package/dist/verification.d.ts +51 -0
- package/dist/verification.d.ts.map +1 -0
- package/dist/verification.js +80 -0
- package/dist/verification.js.map +1 -0
- package/docs/examples.json +174 -90
- package/docs/llms-full.txt +1771 -748
- package/docs/types.json +2027 -3089
- package/package.json +22 -11
- package/src/assure.ts +17 -18
- package/src/capabilities-tool.ts +82 -16
- package/src/check.ts +410 -104
- package/src/cli.ts +161 -41
- package/src/diff-tool.ts +8 -8
- package/src/docs-search.ts +6 -6
- package/src/doctor.ts +18 -10
- package/src/examples.ts +7 -7
- package/src/init-agents.ts +17 -17
- package/src/internal/typescript-import.ts +33 -0
- package/src/introspect.ts +28 -28
- package/src/learn.ts +152 -0
- package/src/levels-tool.ts +37 -31
- package/src/load.ts +21 -12
- package/src/manifest-tool.ts +4 -2
- package/src/mcp-app-tool.ts +86 -0
- package/src/mcp-docs-tools.ts +26 -9
- package/src/mcp-http.ts +31 -18
- package/src/mcp-protocol.ts +2 -2
- package/src/mcp-render.ts +6 -6
- package/src/mcp-run.ts +2 -2
- package/src/mcp.ts +319 -45
- package/src/openapi-tool.ts +2 -2
- package/src/pipeline-guard.ts +1 -1
- package/src/port.ts +31 -31
- package/src/recipes/1.8.0.ts +5 -5
- package/src/recipes/index.ts +2 -2
- package/src/scaffold.ts +8 -8
- package/src/sync-routes.ts +69 -0
- package/src/types-search.ts +8 -8
- package/src/upgrade.ts +137 -19
- package/src/verification.ts +123 -0
package/src/levels-tool.ts
CHANGED
|
@@ -1,36 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `nifra levels`
|
|
2
|
+
* `nifra levels` - the verification ladder, computed from gates that already exist:
|
|
3
3
|
*
|
|
4
|
-
* L0 typed contract
|
|
5
|
-
* L1 route assurance
|
|
6
|
-
* L2 capability lockfile
|
|
7
|
-
* L3 route manifest
|
|
8
|
-
* L4 invariant-tested
|
|
4
|
+
* L0 typed contract - `nifra check` passes (compiler-enforced frontend↔backend contract)
|
|
5
|
+
* L1 route assurance - `nifra.assurance.ts` present and every route satisfies its policy
|
|
6
|
+
* L2 capability lockfile - capability/effect assurance passes and the lockfile is in sync
|
|
7
|
+
* L3 route manifest - the emitted manifest hash-verifies and matches the current app
|
|
8
|
+
* L4 invariant-tested - every route passes through an explicitly isolated executor
|
|
9
9
|
*
|
|
10
|
-
* A level counts only when every level below it holds
|
|
10
|
+
* A level counts only when every level below it holds - the ladder is cumulative, so "L3" is one
|
|
11
11
|
* honest word for a whole posture. Levels are computed, never self-declared.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { existsSync } from "node:fs"
|
|
15
15
|
import { resolve } from "node:path"
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
16
|
+
import type { AssuranceReport } from "@nifrajs/core/assurance"
|
|
17
|
+
import { snapshotCapabilities } from "@nifrajs/core/capabilities"
|
|
18
18
|
import { buildNifraManifest, parseNifraManifest } from "@nifrajs/core/manifest"
|
|
19
19
|
import { type ContractTestApp, runAdversarialContract } from "@nifrajs/testing"
|
|
20
|
-
import { loadAssuranceConfig } from "./assure.ts"
|
|
21
20
|
import {
|
|
22
|
-
|
|
21
|
+
type CapabilityProjectReport,
|
|
23
22
|
diffCapabilitySnapshots,
|
|
24
23
|
parseCapabilityLockfile,
|
|
25
24
|
} from "./capabilities-tool.ts"
|
|
26
|
-
import { collectCheckResult } from "./check.ts"
|
|
27
25
|
import { DEFAULT_MANIFEST_FILE } from "./manifest-tool.ts"
|
|
28
26
|
|
|
29
27
|
export interface VerificationLevelStatus {
|
|
30
28
|
readonly level: number
|
|
31
29
|
readonly name: string
|
|
32
30
|
readonly ok: boolean
|
|
33
|
-
/** Why the level does not hold
|
|
31
|
+
/** Why the level does not hold - a gate failure or a missing prerequisite. Empty when ok. */
|
|
34
32
|
readonly reasons: readonly string[]
|
|
35
33
|
}
|
|
36
34
|
|
|
@@ -51,10 +49,18 @@ export async function collectVerificationLevels(
|
|
|
51
49
|
cwd: string,
|
|
52
50
|
options: CollectLevelsOptions = {},
|
|
53
51
|
): Promise<VerificationLevelsResult> {
|
|
52
|
+
// The ladder is the fullest view over the one project verification: L0 is the check view, L1/L2 read
|
|
53
|
+
// the route-assurance + capability evidence the same collector already computed (no second walk), and
|
|
54
|
+
// only the ladder-specific L3 (manifest) and L4 (invariants) are evaluated here.
|
|
55
|
+
const { collectProjectVerification } = await import("./verification.ts")
|
|
56
|
+
const verification = await collectProjectVerification(cwd, {
|
|
57
|
+
...(options.config !== undefined ? { config: options.config } : {}),
|
|
58
|
+
maxDiagnostics: 5,
|
|
59
|
+
})
|
|
54
60
|
const statuses: { level: number; name: string; ok: boolean; reasons: string[] }[] = []
|
|
55
61
|
|
|
56
|
-
// L0
|
|
57
|
-
const check = await
|
|
62
|
+
// L0 - typed contract (`nifra check`).
|
|
63
|
+
const check = await verification.check()
|
|
58
64
|
statuses.push({
|
|
59
65
|
level: 0,
|
|
60
66
|
name: "typed contract",
|
|
@@ -63,11 +69,11 @@ export async function collectVerificationLevels(
|
|
|
63
69
|
})
|
|
64
70
|
|
|
65
71
|
// L1-L4 all hang off the assurance config; without it the ladder stops at L0 by definition.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
72
|
+
if (verification.config === undefined) {
|
|
73
|
+
const reason =
|
|
74
|
+
verification.configError instanceof Error
|
|
75
|
+
? verification.configError.message
|
|
76
|
+
: String(verification.configError)
|
|
71
77
|
for (const [level, name] of [
|
|
72
78
|
[1, "route assurance"],
|
|
73
79
|
[2, "capability lockfile"],
|
|
@@ -78,9 +84,10 @@ export async function collectVerificationLevels(
|
|
|
78
84
|
}
|
|
79
85
|
return finalize(statuses)
|
|
80
86
|
}
|
|
87
|
+
const config = verification.config
|
|
81
88
|
|
|
82
|
-
// L1
|
|
83
|
-
const assurance =
|
|
89
|
+
// L1 - route assurance.
|
|
90
|
+
const assurance = verification.routeAssurance as AssuranceReport
|
|
84
91
|
statuses.push({
|
|
85
92
|
level: 1,
|
|
86
93
|
name: "route assurance",
|
|
@@ -88,14 +95,13 @@ export async function collectVerificationLevels(
|
|
|
88
95
|
reasons: assurance.findings.map((f) => f.message).slice(0, 5),
|
|
89
96
|
})
|
|
90
97
|
|
|
91
|
-
// L2
|
|
98
|
+
// L2 - capability assurance + lockfile in sync.
|
|
92
99
|
const l2Reasons: string[] = []
|
|
93
|
-
|
|
100
|
+
const capabilityReport = verification.capability?.report
|
|
94
101
|
if (config.capabilities === undefined) {
|
|
95
102
|
l2Reasons.push("nifra.assurance.ts declares no capabilities policy")
|
|
96
103
|
} else {
|
|
97
|
-
const project =
|
|
98
|
-
capabilityReport = project.report
|
|
104
|
+
const project = verification.capability as CapabilityProjectReport
|
|
99
105
|
if (!project.report.ok) {
|
|
100
106
|
l2Reasons.push(...project.report.findings.map((f) => f.message).slice(0, 5))
|
|
101
107
|
}
|
|
@@ -115,11 +121,11 @@ export async function collectVerificationLevels(
|
|
|
115
121
|
reasons: l2Reasons,
|
|
116
122
|
})
|
|
117
123
|
|
|
118
|
-
// L3
|
|
124
|
+
// L3 - emitted manifest hash-verifies AND matches the current app.
|
|
119
125
|
const l3Reasons: string[] = []
|
|
120
126
|
const manifestPath = resolve(cwd, config.manifest?.path ?? DEFAULT_MANIFEST_FILE)
|
|
121
127
|
if (!existsSync(manifestPath)) {
|
|
122
|
-
l3Reasons.push(`manifest missing (${manifestPath})
|
|
128
|
+
l3Reasons.push(`manifest missing (${manifestPath}) - run \`nifra manifest emit\``)
|
|
123
129
|
} else {
|
|
124
130
|
try {
|
|
125
131
|
const recorded = await parseNifraManifest(await Bun.file(manifestPath).text(), manifestPath)
|
|
@@ -130,7 +136,7 @@ export async function collectVerificationLevels(
|
|
|
130
136
|
...(capabilityReport !== undefined ? { capabilities: capabilityReport } : {}),
|
|
131
137
|
})
|
|
132
138
|
if (current.contentHash !== recorded.contentHash) {
|
|
133
|
-
l3Reasons.push("manifest is stale
|
|
139
|
+
l3Reasons.push("manifest is stale - the app changed since it was emitted")
|
|
134
140
|
}
|
|
135
141
|
} else {
|
|
136
142
|
l3Reasons.push("manifest cannot be validated while assurance is failing")
|
|
@@ -146,11 +152,11 @@ export async function collectVerificationLevels(
|
|
|
146
152
|
reasons: l3Reasons,
|
|
147
153
|
})
|
|
148
154
|
|
|
149
|
-
// L4
|
|
155
|
+
// L4 - contract-generated invariants against an explicitly isolated executor.
|
|
150
156
|
const l4Reasons: string[] = []
|
|
151
157
|
if (config.invariants === undefined) {
|
|
152
158
|
l4Reasons.push(
|
|
153
|
-
"no isolated invariant executor configured
|
|
159
|
+
"no isolated invariant executor configured - add `invariants.executor` in nifra.assurance.ts",
|
|
154
160
|
)
|
|
155
161
|
} else {
|
|
156
162
|
const invariants = await runAdversarialContract(config.source as ContractTestApp, {
|
package/src/load.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Zero-config app discovery. The CLI reads a few conventions from the project root:
|
|
3
|
-
* - `routes/`
|
|
4
|
-
* - `nifra.config.ts`
|
|
3
|
+
* - `routes/` - the file-based routes (per `@nifrajs/web/fs`).
|
|
4
|
+
* - `nifra.config.ts` - the CLI's framework wiring (see {@link NifraFramework}); `framework.ts` is
|
|
5
5
|
* used as a fallback. One of the two is required.
|
|
6
|
-
* - `backend.ts`
|
|
6
|
+
* - `backend.ts` - exports `backend` (a `@nifrajs/core` server, the contract). Optional.
|
|
7
7
|
*
|
|
8
8
|
* Why two filenames: a multi-target app's edge entry (`_worker.ts`) imports `framework.ts` for the
|
|
9
|
-
* render adapter, so `framework.ts` must stay edge-bundlable
|
|
9
|
+
* render adapter, so `framework.ts` must stay edge-bundlable - it can't reference Vite plugins or the
|
|
10
10
|
* SFC compiler (Bun would pull them into the `target:"browser"` worker and fail on `child_process`).
|
|
11
11
|
* `nifra.config.ts` holds that build/dev tooling instead: it's imported ONLY by this CLI (which runs on
|
|
12
12
|
* Bun), so its Vite plugin + SFC-compiler imports (`@vitejs/plugin-vue`, `@nifrajs/web-vue/plugin`) never
|
|
@@ -17,9 +17,9 @@ import { resolve } from "node:path"
|
|
|
17
17
|
import { checkPipelineSeparation } from "./pipeline-guard.ts"
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
|
-
* A plugin list
|
|
20
|
+
* A plugin list - either an array, or a **thunk** that returns one (optionally async). Both forms are
|
|
21
21
|
* accepted; {@link resolvePlugins} normalizes them. The thunk is just a convenience for deferring a
|
|
22
|
-
* plugin's construction
|
|
22
|
+
* plugin's construction - it does NOT make a plugin edge-safe (Bun.build follows a dynamic `import()`
|
|
23
23
|
* even from an unused export, so a Vite/compiler import in an edge-bundled file fails regardless). Edge
|
|
24
24
|
* safety comes from WHERE the config lives: keep these fields in `nifra.config.ts` (CLI-only), not in
|
|
25
25
|
* the edge-imported `framework.ts`. See the module header.
|
|
@@ -29,7 +29,7 @@ export type PluginsField =
|
|
|
29
29
|
| (() => readonly unknown[] | Promise<readonly unknown[]>)
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
|
-
* The CLI's framework wiring
|
|
32
|
+
* The CLI's framework wiring - exported from `nifra.config.ts` (or `framework.ts` as a fallback).
|
|
33
33
|
* `create-nifra` generates it. Only `adapter` + `clientModule` are required; the plugin/condition
|
|
34
34
|
* fields are framework-specific extras (Vue/Svelte/Solid need them; React/Preact don't).
|
|
35
35
|
*/
|
|
@@ -43,7 +43,7 @@ export interface NifraFramework {
|
|
|
43
43
|
/** Bun build plugins that compile routes for the CLIENT bundle (`nifra build`), e.g.
|
|
44
44
|
* `[vueBunPlugin("dom")]`. React/Preact JSX is Bun-native → none. */
|
|
45
45
|
readonly clientPlugins?: PluginsField
|
|
46
|
-
/** Bun runtime plugins that compile routes for SSR
|
|
46
|
+
/** Bun runtime plugins that compile routes for SSR - `nifra dev` (Bun-native route import) and
|
|
47
47
|
* `nifra start` register them via `Bun.plugin`, e.g. `[vueBunPlugin("ssr")]`. React/Preact → none. */
|
|
48
48
|
readonly serverPlugins?: PluginsField
|
|
49
49
|
/** Extra Bun.build / Vite resolve conditions, e.g. Solid's `["solid"]`. */
|
|
@@ -109,7 +109,7 @@ export interface LoadAppOptions {
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
/**
|
|
112
|
-
* Root-level monorepo config
|
|
112
|
+
* Root-level monorepo config - exported from `nifra.config.ts` at the workspace root.
|
|
113
113
|
* Each key in `apps` is the short name used to namespace MCP tools (`nifra_<name>_context` etc.);
|
|
114
114
|
* each value is a path relative to the root that contains its own `nifra.config.ts` / `framework.ts`.
|
|
115
115
|
*
|
|
@@ -145,7 +145,7 @@ export async function detectMonorepo(
|
|
|
145
145
|
|
|
146
146
|
/**
|
|
147
147
|
* Load all apps declared in a monorepo config. Returns `{ name, cwd, app }[]` in declaration order.
|
|
148
|
-
* Individual app load failures throw
|
|
148
|
+
* Individual app load failures throw - each app must be valid.
|
|
149
149
|
*/
|
|
150
150
|
export async function loadMonorepoApps(
|
|
151
151
|
rootCwd: string,
|
|
@@ -187,7 +187,7 @@ export async function loadApp(
|
|
|
187
187
|
}
|
|
188
188
|
if (!existsSync(resolve(cwd, "routes"))) {
|
|
189
189
|
throw new Error(
|
|
190
|
-
`[nifra] no routes/ directory in ${cwd}
|
|
190
|
+
`[nifra] no routes/ directory in ${cwd} - nifra apps are file-routed under routes/.`,
|
|
191
191
|
)
|
|
192
192
|
}
|
|
193
193
|
const fw = (await importWithQuery(configPath, options.importQuery)) as Partial<NifraFramework>
|
|
@@ -196,6 +196,15 @@ export async function loadApp(
|
|
|
196
196
|
`[nifra] ${configFile} must export \`adapter\` (a render adapter object) and \`clientModule\` (a string).`,
|
|
197
197
|
)
|
|
198
198
|
}
|
|
199
|
+
// A `clientModule` given as a RELATIVE path (a local client entry, e.g. `./src/client.tsx`) is
|
|
200
|
+
// resolved to absolute here. It is embedded verbatim as an import specifier in the generated client
|
|
201
|
+
// entry, and `nifra dev` and `nifra build` write that entry into DIFFERENT directories - so a relative
|
|
202
|
+
// specifier would resolve against different bases and load in one phase but not the other. A bare or
|
|
203
|
+
// package specifier (`@nifrajs/web-react/client`) is location-independent and passes through unchanged.
|
|
204
|
+
const clientModule =
|
|
205
|
+
fw.clientModule.startsWith("./") || fw.clientModule.startsWith("../")
|
|
206
|
+
? resolve(cwd, fw.clientModule)
|
|
207
|
+
: fw.clientModule
|
|
199
208
|
const [vitePlugins, clientPlugins, serverPlugins] = await Promise.all([
|
|
200
209
|
resolvePlugins(fw.vitePlugins),
|
|
201
210
|
resolvePlugins(fw.clientPlugins),
|
|
@@ -215,7 +224,7 @@ export async function loadApp(
|
|
|
215
224
|
cwd,
|
|
216
225
|
routesDir: resolve(cwd, "routes"),
|
|
217
226
|
outDir: resolve(cwd, outDirName),
|
|
218
|
-
framework: fw as NifraFramework,
|
|
227
|
+
framework: { ...fw, clientModule } as NifraFramework,
|
|
219
228
|
resolvedPlugins,
|
|
220
229
|
backend,
|
|
221
230
|
}
|
package/src/manifest-tool.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** `nifra manifest`
|
|
1
|
+
/** `nifra manifest` - emit/sign/diff the deployable route trust artifact. */
|
|
2
2
|
|
|
3
3
|
import { existsSync } from "node:fs"
|
|
4
4
|
import { resolve } from "node:path"
|
|
@@ -46,7 +46,9 @@ export async function runManifestEmit(
|
|
|
46
46
|
options: { readonly config?: string; readonly out?: string; readonly sign?: string } = {},
|
|
47
47
|
): Promise<boolean> {
|
|
48
48
|
const config = await loadAssuranceConfig(cwd, options.config)
|
|
49
|
-
const assurance = evaluateRouteAssurance(config.source, config.policy
|
|
49
|
+
const assurance = evaluateRouteAssurance(config.source, config.policy, {
|
|
50
|
+
...(config.capabilities !== undefined ? { definitions: config.capabilities.definitions } : {}),
|
|
51
|
+
})
|
|
50
52
|
const capabilityProject =
|
|
51
53
|
config.capabilities === undefined
|
|
52
54
|
? undefined
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `nifra_examples_app` MCP tool: nifra's verified code examples as an MCP Apps widget. It returns the
|
|
3
|
+
* examples as `structuredContent`, rendered by the {@link examplesWidget} `ui://` widget in MCP Apps hosts
|
|
4
|
+
* (ChatGPT Apps, MCPJam, Goose); text-only hosts still get the example names as plain text. Part of the
|
|
5
|
+
* public docs-MCP tool set (served by `handleMcpHttp`), so every self-host gets it from one definition.
|
|
6
|
+
* `loadExamples` is injected, so the same tool reads the corpus from disk (CLI) or a cached fetch (edge).
|
|
7
|
+
*/
|
|
8
|
+
import { defineMcpTool, defineMcpWidget, type McpTool } from "@nifrajs/mcp"
|
|
9
|
+
import type { Example } from "./examples.ts"
|
|
10
|
+
|
|
11
|
+
/** The widget: renders `structuredContent.examples` as a filterable list of example cards. */
|
|
12
|
+
export const examplesWidget = defineMcpWidget({
|
|
13
|
+
uri: "ui://nifra/examples",
|
|
14
|
+
name: "nifra examples",
|
|
15
|
+
description: "Interactive browser of verified, copy-pasteable nifra code examples.",
|
|
16
|
+
head: `<style>
|
|
17
|
+
:root { color-scheme: light dark; font: 14px/1.5 system-ui, sans-serif; }
|
|
18
|
+
body { margin: 0; padding: 16px; }
|
|
19
|
+
h1 { font-size: 16px; margin: 0 0 4px; }
|
|
20
|
+
#q { width: 100%; box-sizing: border-box; padding: 6px 10px; margin: 8px 0 14px; border-radius: 6px; border: 1px solid color-mix(in srgb, currentColor 25%, transparent); background: transparent; color: inherit; font: inherit; }
|
|
21
|
+
.card { border: 1px solid color-mix(in srgb, currentColor 15%, transparent); border-radius: 10px; padding: 10px 12px; margin-bottom: 10px; }
|
|
22
|
+
.topic { font-size: 12px; opacity: .6; }
|
|
23
|
+
.name { font-weight: 600; margin: 2px 0 6px; }
|
|
24
|
+
pre { margin: 0; padding: 8px 10px; border-radius: 6px; background: color-mix(in srgb, currentColor 8%, transparent); overflow-x: auto; font: 12px/1.5 ui-monospace, monospace; }
|
|
25
|
+
</style>`,
|
|
26
|
+
html: `
|
|
27
|
+
<h1>nifra examples</h1>
|
|
28
|
+
<input id="q" placeholder="Filter examples…" />
|
|
29
|
+
<div id="list"></div>
|
|
30
|
+
<script>
|
|
31
|
+
var ALL = [];
|
|
32
|
+
function esc(s){ return String(s).replace(/[&<>]/g, function(c){ return c==="&"?"&":c==="<"?"<":">"; }); }
|
|
33
|
+
function paint(items){
|
|
34
|
+
document.getElementById("list").innerHTML = items.map(function(e){
|
|
35
|
+
var code = e.code.length > 600 ? e.code.slice(0, 600) + "\\n…" : e.code;
|
|
36
|
+
return "<div class='card'><div class='topic'>" + esc(e.topic) + "</div><div class='name'>" + esc(e.name) +
|
|
37
|
+
"</div><pre>" + esc(code) + "</pre></div>";
|
|
38
|
+
}).join("") || "<p style='opacity:.6'>No matching examples.</p>";
|
|
39
|
+
}
|
|
40
|
+
function render(data){ ALL = (data && data.examples) || []; paint(ALL); }
|
|
41
|
+
mcpApp.onData(render);
|
|
42
|
+
document.getElementById("q").addEventListener("input", function(ev){
|
|
43
|
+
var q = ev.target.value.toLowerCase();
|
|
44
|
+
paint(ALL.filter(function(e){ return (e.topic + e.name + e.slug).toLowerCase().indexOf(q) !== -1; }));
|
|
45
|
+
});
|
|
46
|
+
</script>
|
|
47
|
+
`,
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
/** The widget-backed tool. `loadExamples` is injected (disk on the CLI, cached fetch on the edge). */
|
|
51
|
+
export function examplesAppTool(loadExamples: () => Promise<Example[] | undefined>): McpTool {
|
|
52
|
+
return defineMcpTool({
|
|
53
|
+
name: "nifra_examples_app",
|
|
54
|
+
description:
|
|
55
|
+
"Browse nifra's verified code examples as an interactive, filterable list (MCP Apps widget). Pass query to pre-filter; the widget also filters client-side.",
|
|
56
|
+
inputSchema: {
|
|
57
|
+
type: "object",
|
|
58
|
+
properties: { query: { type: "string", description: "Pre-filter examples by keyword." } },
|
|
59
|
+
additionalProperties: false,
|
|
60
|
+
},
|
|
61
|
+
widget: examplesWidget,
|
|
62
|
+
handler: async (args) => {
|
|
63
|
+
const all = (await loadExamples()) ?? []
|
|
64
|
+
const q = ((args.query as string) ?? "").toLowerCase()
|
|
65
|
+
const matched = (
|
|
66
|
+
q
|
|
67
|
+
? all.filter((e) => `${e.topic} ${e.name} ${e.slug} ${e.code}`.toLowerCase().includes(q))
|
|
68
|
+
: all
|
|
69
|
+
).slice(0, 30)
|
|
70
|
+
return {
|
|
71
|
+
text:
|
|
72
|
+
matched.length === 0
|
|
73
|
+
? "No matching examples."
|
|
74
|
+
: `${matched.length} example(s): ${matched.map((e) => e.name).join(", ")}.`,
|
|
75
|
+
structuredContent: {
|
|
76
|
+
examples: matched.map((e) => ({
|
|
77
|
+
name: e.name,
|
|
78
|
+
topic: e.topic,
|
|
79
|
+
slug: e.slug,
|
|
80
|
+
code: e.code,
|
|
81
|
+
})),
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
})
|
|
86
|
+
}
|
package/src/mcp-docs-tools.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
2
|
+
* The project-INDEPENDENT MCP tools - `nifra_docs`, `nifra_example`, `nifra_types`, `nifra_learn` - as a single factory shared
|
|
3
3
|
* by every transport: the project stdio server ({@link projectTools}), the CLI HTTP server
|
|
4
4
|
* (`nifra docs-mcp`), and the site's Cloudflare-Pages worker (`/mcp`). The corpus source is injected as
|
|
5
5
|
* loaders, so the SAME tool definitions work whether the corpus comes from disk (`Bun.file`, on the
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { renderDocsResult } from "./docs-search.ts"
|
|
11
11
|
import { type Example, renderExamplesResult } from "./examples.ts"
|
|
12
|
+
import { renderLearnResult } from "./learn.ts"
|
|
12
13
|
import type { McpTool } from "./mcp-protocol.ts"
|
|
13
14
|
import { renderTypesResult, type TypeEntry } from "./types-search.ts"
|
|
14
15
|
|
|
@@ -25,7 +26,7 @@ export function docsTools(
|
|
|
25
26
|
{
|
|
26
27
|
name: "nifra_docs",
|
|
27
28
|
description:
|
|
28
|
-
"Search nifra's framework documentation and get back ONLY the matching sections
|
|
29
|
+
"Search nifra's framework documentation and get back ONLY the matching sections - auth, uploads, ISR, WebSockets, loaders, deployment, anything. Call with no query for the cheap section index; pass query (e.g. \"isr revalidate\") for the top matching sections. Use this instead of reading llms-full.txt (~150 KB) whole. For the EXACT TypeScript of a type/interface/function, call nifra_types instead (don't read .d.ts files).",
|
|
29
30
|
inputSchema: {
|
|
30
31
|
type: "object",
|
|
31
32
|
properties: {
|
|
@@ -40,7 +41,7 @@ export function docsTools(
|
|
|
40
41
|
handler: async (args) => {
|
|
41
42
|
const corpus = await loadDocs()
|
|
42
43
|
if (corpus === undefined) {
|
|
43
|
-
return "docs corpus missing
|
|
44
|
+
return "docs corpus missing - run `bun run gen:llms` in the nifra repo (published builds ship it)."
|
|
44
45
|
}
|
|
45
46
|
const { query, limit } = args as { query?: string; limit?: number }
|
|
46
47
|
return renderDocsResult(corpus, query, clamp(limit, 1, 5, 3))
|
|
@@ -49,7 +50,7 @@ export function docsTools(
|
|
|
49
50
|
{
|
|
50
51
|
name: "nifra_example",
|
|
51
52
|
description:
|
|
52
|
-
'Get a VERIFIED, copy-pasteable nifra code example for a task
|
|
53
|
+
'Get a VERIFIED, copy-pasteable nifra code example for a task - auth route, file upload, ISR page, loader/action, typed client, SSE, deployment, etc. Every snippet is typechecked against the installed nifra version, so it compiles as-is. PREFER THIS over writing nifra code from memory (training data drifts). Call with no query for the grouped index; pass query (e.g. "protected route", "upload", "isr revalidate") for matching snippets.',
|
|
53
54
|
inputSchema: {
|
|
54
55
|
type: "object",
|
|
55
56
|
properties: {
|
|
@@ -64,7 +65,7 @@ export function docsTools(
|
|
|
64
65
|
handler: async (args) => {
|
|
65
66
|
const corpus = await loadExamples()
|
|
66
67
|
if (corpus === undefined) {
|
|
67
|
-
return "examples corpus missing
|
|
68
|
+
return "examples corpus missing - run `bun run gen:llms` in the nifra repo (published builds ship it)."
|
|
68
69
|
}
|
|
69
70
|
const { query, limit } = args as { query?: string; limit?: number }
|
|
70
71
|
return renderExamplesResult(corpus, query, clamp(limit, 1, 5, 3))
|
|
@@ -73,14 +74,14 @@ export function docsTools(
|
|
|
73
74
|
{
|
|
74
75
|
name: "nifra_types",
|
|
75
76
|
description:
|
|
76
|
-
'Get the EXACT TypeScript declaration of any exported @nifrajs/* symbol
|
|
77
|
+
'Get the EXACT TypeScript declaration of any exported @nifrajs/* symbol - interface, type, class, function, const. Each signature is generated from the package\'s built .d.ts, so it is the LITERAL declaration: complete, authoritative, never prose and never truncated. THIS IS THE SOURCE OF TRUTH for nifra\'s types - do NOT read node_modules/@nifrajs/**/*.d.ts; call this instead. Pass `name` for an exact symbol (e.g. "RateLimitStore", "RouteSchema", "Context", "rateLimit"); pass `query` to search by keyword; omit both for the per-package index of names. A `name` lookup is always the complete declaration; a `query` returns a one-line summary plus the signature, collapsing an oversized body - pass `full: true` to override.',
|
|
77
78
|
inputSchema: {
|
|
78
79
|
type: "object",
|
|
79
80
|
properties: {
|
|
80
81
|
name: {
|
|
81
82
|
type: "string",
|
|
82
83
|
description:
|
|
83
|
-
"Exact symbol name
|
|
84
|
+
"Exact symbol name - returns its literal declaration (e.g. RateLimitStore).",
|
|
84
85
|
},
|
|
85
86
|
query: {
|
|
86
87
|
type: "string",
|
|
@@ -94,7 +95,7 @@ export function docsTools(
|
|
|
94
95
|
full: {
|
|
95
96
|
type: "boolean",
|
|
96
97
|
description:
|
|
97
|
-
"Query mode only: return whole declarations instead of collapsed ones. Off by default
|
|
98
|
+
"Query mode only: return whole declarations instead of collapsed ones. Off by default - a search is for picking a symbol, and one match can be tens of thousands of characters.",
|
|
98
99
|
},
|
|
99
100
|
},
|
|
100
101
|
additionalProperties: false,
|
|
@@ -102,7 +103,7 @@ export function docsTools(
|
|
|
102
103
|
handler: async (args) => {
|
|
103
104
|
const corpus = await loadTypes()
|
|
104
105
|
if (corpus === undefined) {
|
|
105
|
-
return "types corpus missing
|
|
106
|
+
return "types corpus missing - run `bun run build && bun run gen:llms` in the nifra repo (published builds ship it)."
|
|
106
107
|
}
|
|
107
108
|
const { name, query, limit, full } = args as {
|
|
108
109
|
name?: string
|
|
@@ -113,5 +114,21 @@ export function docsTools(
|
|
|
113
114
|
return renderTypesResult(corpus, name, query, clamp(limit, 1, 8, 5), full === true)
|
|
114
115
|
},
|
|
115
116
|
},
|
|
117
|
+
{
|
|
118
|
+
name: "nifra_learn",
|
|
119
|
+
description:
|
|
120
|
+
"Walk the guided path to build a nifra app end to end - an ORDERED sequence (create → page route → loader → typed API → typed client → auth → background jobs → deploy), not the random-access search of nifra_docs/nifra_example. Call with no args for the step index; pass `step: N` for that step's goal, how to do it (which tool emits the correct artifact), and how to verify it. Use it when scaffolding a new app or learning nifra's flow - each step composes the other nifra_* tools.",
|
|
121
|
+
inputSchema: {
|
|
122
|
+
type: "object",
|
|
123
|
+
properties: {
|
|
124
|
+
step: {
|
|
125
|
+
type: "number",
|
|
126
|
+
description: "1-based step number to expand (omit for the index).",
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
additionalProperties: false,
|
|
130
|
+
},
|
|
131
|
+
handler: async (args) => renderLearnResult((args as { step?: number }).step),
|
|
132
|
+
},
|
|
116
133
|
]
|
|
117
134
|
}
|
package/src/mcp-http.ts
CHANGED
|
@@ -1,39 +1,49 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The PUBLIC nifra docs MCP
|
|
3
|
-
* (`nifra_docs`
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* The PUBLIC nifra docs MCP - Streamable-HTTP transport exposing the project-INDEPENDENT tools
|
|
3
|
+
* (`nifra_docs`, `nifra_example`, `nifra_types`, `nifra_learn` + the `nifra_examples_app` MCP Apps widget)
|
|
4
|
+
* so any remote AI agent can learn nifra without a local checkout. They read the bundled corpus
|
|
5
|
+
* (llms-full.txt / examples.json / types.json shipped in this package) and ignore `cwd`, so they reuse the
|
|
6
|
+
* exact definitions from {@link projectTools} - one source, no drift.
|
|
6
7
|
*
|
|
7
8
|
* The transport itself (body cap, CORS, JSON-RPC dispatch) lives in `@nifrajs/mcp/http`; this module is
|
|
8
9
|
* the docs-specific layer over it: it supplies the bundled corpus tools and the `nifra-docs` server info.
|
|
9
|
-
* {@link handleMcpHttp} self-hosts ANYWHERE Bun runs
|
|
10
|
-
* same handler runs on Cloudflare/Vercel edge (there inline the corpus as string imports
|
|
10
|
+
* {@link handleMcpHttp} self-hosts ANYWHERE Bun runs - the simplest path is `nifra docs-mcp` on a VPS; the
|
|
11
|
+
* same handler runs on Cloudflare/Vercel edge (there inline the corpus as string imports - a Worker has no
|
|
11
12
|
* filesystem). `export default { port, fetch }` also lets `bun run mcp-http.ts` serve it directly.
|
|
12
13
|
*/
|
|
13
14
|
|
|
14
15
|
import { type McpHttpOptions, respondMcpHttp as respondMcpHttpCore } from "@nifrajs/mcp/http"
|
|
15
|
-
import type
|
|
16
|
+
import { type McpTool, UI_MIME } from "@nifrajs/mcp/protocol"
|
|
16
17
|
import { loadDocsCorpus } from "./docs-search.ts"
|
|
17
18
|
import { loadExamplesCorpus } from "./examples.ts"
|
|
19
|
+
import { examplesAppTool, examplesWidget } from "./mcp-app-tool.ts"
|
|
18
20
|
import { docsTools } from "./mcp-docs-tools.ts"
|
|
19
21
|
import { loadTypesCorpus } from "./types-search.ts"
|
|
20
22
|
|
|
21
23
|
export type { McpHttpOptions } from "@nifrajs/mcp/http"
|
|
22
24
|
export type { Example } from "./examples.ts"
|
|
23
|
-
// Re-exported so a self-host
|
|
24
|
-
//
|
|
25
|
+
// Re-exported so a self-host gets the corpus-injectable tool factories + the transport core from one
|
|
26
|
+
// entry: `import { respondMcpHttp, docsTools, examplesAppTool } from "@nifrajs/cli/mcp"`.
|
|
27
|
+
export { examplesAppTool, examplesWidget } from "./mcp-app-tool.ts"
|
|
25
28
|
export { docsTools } from "./mcp-docs-tools.ts"
|
|
26
29
|
export type { TypeEntry } from "./types-search.ts"
|
|
27
30
|
|
|
28
31
|
// Kept in lockstep with packages/cli/package.json by check:publish's version-consistency gate.
|
|
29
|
-
const VERSION = "2.
|
|
32
|
+
const VERSION = "2.4.0"
|
|
30
33
|
const SERVER_INFO = { name: "nifra-docs", version: VERSION }
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
// Derive the GET/health tool list from the tools actually served, so the line can never drift from them.
|
|
35
|
+
const docsHealth = (tools: McpTool[]): string =>
|
|
36
|
+
`nifra docs MCP - POST JSON-RPC 2.0 here (methods: initialize, tools/list, tools/call). Tools: ${tools
|
|
37
|
+
.map((tool) => tool.name)
|
|
38
|
+
.join(", ")}.`
|
|
33
39
|
|
|
34
|
-
/** The
|
|
40
|
+
/** The project-independent tools, reading the package's bundled corpus from disk (CLI use): the text
|
|
41
|
+
* docs tools plus the `nifra_examples_app` MCP Apps widget tool. */
|
|
35
42
|
export function publicDocsTools(): McpTool[] {
|
|
36
|
-
return
|
|
43
|
+
return [
|
|
44
|
+
...docsTools(loadDocsCorpus, loadExamplesCorpus, loadTypesCorpus),
|
|
45
|
+
examplesAppTool(loadExamplesCorpus),
|
|
46
|
+
]
|
|
37
47
|
}
|
|
38
48
|
|
|
39
49
|
/**
|
|
@@ -46,18 +56,21 @@ export function respondMcpHttp(
|
|
|
46
56
|
tools: McpTool[],
|
|
47
57
|
options: McpHttpOptions = {},
|
|
48
58
|
): Promise<Response> {
|
|
49
|
-
return respondMcpHttpCore(request, tools, SERVER_INFO, { health:
|
|
59
|
+
return respondMcpHttpCore(request, tools, SERVER_INFO, { health: docsHealth(tools), ...options })
|
|
50
60
|
}
|
|
51
61
|
|
|
52
|
-
/** The CLI HTTP handler: serves the disk-backed corpus tools
|
|
62
|
+
/** The CLI HTTP handler: serves the disk-backed corpus tools + registers the examples widget's `ui://`
|
|
63
|
+
* resource so MCP Apps hosts can render `nifra_examples_app`. (`nifra docs-mcp` / `bun run` this file.) */
|
|
53
64
|
export function handleMcpHttp(request: Request): Promise<Response> {
|
|
54
|
-
return respondMcpHttp(request, publicDocsTools()
|
|
65
|
+
return respondMcpHttp(request, publicDocsTools(), {
|
|
66
|
+
features: { resources: [examplesWidget.resource], ui: { mimeTypes: [UI_MIME] } },
|
|
67
|
+
})
|
|
55
68
|
}
|
|
56
69
|
|
|
57
70
|
/**
|
|
58
71
|
* Worker/edge + local entry. `export default { fetch }` is the universal server shape: Cloudflare /
|
|
59
72
|
* Vercel edge / Deno deploy use `fetch` (and ignore `port`); `bun run mcp-http.ts` auto-serves it on
|
|
60
|
-
* `port` (PORT env, default 8787)
|
|
73
|
+
* `port` (PORT env, default 8787) - Bun serves a module's default-exported server, so NO manual
|
|
61
74
|
* `Bun.serve` here (that would double-bind the port).
|
|
62
75
|
*/
|
|
63
76
|
export default {
|
package/src/mcp-protocol.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The MCP JSON-RPC dispatch now lives in `@nifrajs/mcp` (shared with nifra apps that expose their own
|
|
3
|
-
* MCP server / MCP Apps widgets). This module is a thin re-export so the CLI's existing imports
|
|
4
|
-
* the `@nifrajs/cli/mcp` self-host surface
|
|
3
|
+
* MCP server / MCP Apps widgets). This module is a thin re-export so the CLI's existing imports - and
|
|
4
|
+
* the `@nifrajs/cli/mcp` self-host surface - keep resolving here unchanged.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
export * from "@nifrajs/mcp/protocol"
|
package/src/mcp-render.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The `nifra_render` engine + its child-process entry
|
|
2
|
+
* The `nifra_render` engine + its child-process entry - the page half of `nifra_run`. Where `nifra_run`
|
|
3
3
|
* runs requests through the BACKEND, this SSRs page routes through the WEB app (`createWebApp`), so an
|
|
4
4
|
* agent can verify a page renders and its loader ran after an edit. Spawned fresh per call (`bun
|
|
5
5
|
* mcp-render.ts <cwd>`) so the project's CURRENT route/loader code is loaded.
|
|
6
6
|
*
|
|
7
|
-
* No build required: a placeholder `clientEntry` is injected (the SSR HTML renders regardless
|
|
7
|
+
* No build required: a placeholder `clientEntry` is injected (the SSR HTML renders regardless - the
|
|
8
8
|
* client bundle only matters for hydration in the browser, not for "did this page render server-side").
|
|
9
|
-
* {@link renderPages} never throws
|
|
9
|
+
* {@link renderPages} never throws - every failure (no config, a loader error, a render throw) becomes
|
|
10
10
|
* `{ error }` so the agent gets the actionable message.
|
|
11
11
|
*/
|
|
12
12
|
|
|
@@ -19,7 +19,7 @@ import { loadApp, resolvePlugins } from "./load.ts"
|
|
|
19
19
|
const errString = (err: unknown): string =>
|
|
20
20
|
err instanceof Error ? `${err.name}: ${err.message}` : String(err)
|
|
21
21
|
|
|
22
|
-
/** Load the project, register its SSR plugins, and build the SSR `createWebApp` once
|
|
22
|
+
/** Load the project, register its SSR plugins, and build the SSR `createWebApp` once - the cost the warm
|
|
23
23
|
* worker amortizes across calls. Never throws (every failure → `{ error }`). Factored out of
|
|
24
24
|
* {@link renderPages} so the cold path (fresh process per call) and the warm worker share one builder. */
|
|
25
25
|
async function buildWebApp(cwd: string): Promise<{ app: AppLike } | { error: string }> {
|
|
@@ -92,7 +92,7 @@ function redirectConsoleToStderr(): void {
|
|
|
92
92
|
/**
|
|
93
93
|
* Warm-worker loop (`--worker`): build the web app ONCE, then SSR each `{ id, input: { requests } }` line
|
|
94
94
|
* against that hot app and reply `{ id, output }`. The parent (`mcp.ts`'s warm-render handler) spawns this,
|
|
95
|
-
* fingerprints the source tree, and restarts the worker when a file changes
|
|
95
|
+
* fingerprints the source tree, and restarts the worker when a file changes - so warm reuse never serves a
|
|
96
96
|
* stale render. Mirrors `mcp-run.ts`'s worker (single backend entry → no per-call entry key needed here).
|
|
97
97
|
*/
|
|
98
98
|
async function runWorker(cwd: string): Promise<void> {
|
|
@@ -148,7 +148,7 @@ if (import.meta.main) {
|
|
|
148
148
|
// The parent (mcp.ts) spawns `bun mcp-render.ts <cwd>` WITHOUT setting the subprocess `cwd` option, so
|
|
149
149
|
// this process inherits the MCP server's working dir, not the app's. SSR resolves `react-dom/server`
|
|
150
150
|
// from `process.cwd()` to share ONE React core with the app's route components (the dual-React fix in
|
|
151
|
-
// @nifrajs/web-react)
|
|
151
|
+
// @nifrajs/web-react) - so we must chdir to the app root before any render. Best-effort: a bad path
|
|
152
152
|
// surfaces later as a load error rather than crashing the child here.
|
|
153
153
|
if (cwd !== process.cwd()) {
|
|
154
154
|
try {
|
package/src/mcp-run.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The `nifra_run` engine + its child-process entry. The MCP server spawns this file fresh on every
|
|
3
|
-
* `nifra_run` call (`bun mcp-run.ts <cwd>`) so the project's CURRENT backend code is loaded
|
|
3
|
+
* `nifra_run` call (`bun mcp-run.ts <cwd>`) so the project's CURRENT backend code is loaded - picking
|
|
4
4
|
* up the agent's latest edits. {@link runBackend} is the pure-ish core (resolve entry → import → run),
|
|
5
5
|
* exported so it's unit-testable in-process; the entry below wires it to stdin/stdout.
|
|
6
6
|
*/
|
|
@@ -48,7 +48,7 @@ export async function loadBackend(
|
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
50
|
* Resolve the backend entry under `cwd`, import it, and run `requests` through its exported app. Never
|
|
51
|
-
* throws
|
|
51
|
+
* throws - every failure (missing/invalid entry, import/compile/runtime error) becomes `{ error }`, so
|
|
52
52
|
* the caller (and the agent) always gets actionable output. An import error here IS the failure the
|
|
53
53
|
* agent needs to see and fix.
|
|
54
54
|
*/
|