@nifrajs/cli 3.3.0 → 4.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/README.md +42 -3
- package/dist/app-files.d.ts +11 -0
- package/dist/app-files.d.ts.map +1 -0
- package/dist/app-files.js +34 -0
- package/dist/app-files.js.map +1 -0
- package/dist/assure-hydration.d.ts +7 -1
- package/dist/assure-hydration.d.ts.map +1 -1
- package/dist/assure-hydration.js +128 -43
- package/dist/assure-hydration.js.map +1 -1
- package/dist/assure.d.ts.map +1 -1
- package/dist/assure.js +16 -2
- package/dist/assure.js.map +1 -1
- package/dist/capabilities-tool.d.ts +5 -1
- package/dist/capabilities-tool.d.ts.map +1 -1
- package/dist/capabilities-tool.js +32 -10
- package/dist/capabilities-tool.js.map +1 -1
- package/dist/cdn-check.d.ts +41 -0
- package/dist/cdn-check.d.ts.map +1 -0
- package/dist/cdn-check.js +207 -0
- package/dist/cdn-check.js.map +1 -0
- package/dist/check-diagnostics.d.ts +7 -15
- package/dist/check-diagnostics.d.ts.map +1 -1
- package/dist/check-diagnostics.js +162 -713
- package/dist/check-diagnostics.js.map +1 -1
- package/dist/check-scan.d.ts +58 -7
- package/dist/check-scan.d.ts.map +1 -1
- package/dist/check-scan.js +323 -62
- package/dist/check-scan.js.map +1 -1
- package/dist/check.d.ts +1 -0
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +145 -54
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts +42 -16
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +306 -87
- package/dist/cli.js.map +1 -1
- package/dist/command-catalog.d.ts +60 -5
- package/dist/command-catalog.d.ts.map +1 -1
- package/dist/command-catalog.js +298 -33
- package/dist/command-catalog.js.map +1 -1
- package/dist/config-target.d.ts +8 -0
- package/dist/config-target.d.ts.map +1 -0
- package/dist/config-target.js +45 -0
- package/dist/config-target.js.map +1 -0
- package/dist/contracts.d.ts +3 -2
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +56 -16
- package/dist/contracts.js.map +1 -1
- package/dist/db-audit.d.ts +37 -0
- package/dist/db-audit.d.ts.map +1 -0
- package/dist/db-audit.js +99 -0
- package/dist/db-audit.js.map +1 -0
- package/dist/db-child.d.ts +64 -0
- package/dist/db-child.d.ts.map +1 -0
- package/dist/db-child.js +213 -0
- package/dist/db-child.js.map +1 -0
- package/dist/db-config.d.ts +60 -0
- package/dist/db-config.d.ts.map +1 -0
- package/dist/db-config.js +176 -0
- package/dist/db-config.js.map +1 -0
- package/dist/db-tool.d.ts +84 -0
- package/dist/db-tool.d.ts.map +1 -0
- package/dist/db-tool.js +537 -0
- package/dist/db-tool.js.map +1 -0
- package/dist/dev-bun-config.d.ts +20 -13
- package/dist/dev-bun-config.d.ts.map +1 -1
- package/dist/dev-bun-config.js +98 -41
- package/dist/dev-bun-config.js.map +1 -1
- package/dist/dev-feed-tool.d.ts +106 -0
- package/dist/dev-feed-tool.d.ts.map +1 -0
- package/dist/dev-feed-tool.js +713 -0
- package/dist/dev-feed-tool.js.map +1 -0
- package/dist/dev-server-client.d.ts +67 -0
- package/dist/dev-server-client.d.ts.map +1 -0
- package/dist/dev-server-client.js +195 -0
- package/dist/dev-server-client.js.map +1 -0
- package/dist/diagnostics.d.ts +86 -0
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +85 -0
- package/dist/diagnostics.js.map +1 -1
- package/dist/diff-tool.d.ts +3 -3
- package/dist/diff-tool.d.ts.map +1 -1
- package/dist/diff-tool.js +6 -5
- package/dist/diff-tool.js.map +1 -1
- package/dist/docs-search.d.ts.map +1 -1
- package/dist/docs-search.js +12 -4
- package/dist/docs-search.js.map +1 -1
- package/dist/doctor.d.ts +17 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +76 -15
- package/dist/doctor.js.map +1 -1
- package/dist/env-file.d.ts +15 -0
- package/dist/env-file.d.ts.map +1 -1
- package/dist/env-file.js +73 -1
- package/dist/env-file.js.map +1 -1
- package/dist/fix-recipes.d.ts.map +1 -1
- package/dist/fix-recipes.js +41 -21
- package/dist/fix-recipes.js.map +1 -1
- package/dist/frontend-guidance.js +5 -5
- package/dist/frontend-guidance.js.map +1 -1
- package/dist/i18n-check.d.ts +16 -0
- package/dist/i18n-check.d.ts.map +1 -0
- package/dist/i18n-check.js +154 -0
- package/dist/i18n-check.js.map +1 -0
- package/dist/init-agents.d.ts +49 -13
- package/dist/init-agents.d.ts.map +1 -1
- package/dist/init-agents.js +239 -34
- package/dist/init-agents.js.map +1 -1
- package/dist/internal/code-unit-order.d.ts +4 -0
- package/dist/internal/code-unit-order.d.ts.map +1 -0
- package/dist/internal/code-unit-order.js +6 -0
- package/dist/internal/code-unit-order.js.map +1 -0
- package/dist/internal/script-kind.d.ts +7 -0
- package/dist/internal/script-kind.d.ts.map +1 -0
- package/dist/internal/script-kind.js +12 -0
- package/dist/internal/script-kind.js.map +1 -0
- package/dist/internal/source-facts.d.ts +5 -0
- package/dist/internal/source-facts.d.ts.map +1 -1
- package/dist/internal/source-facts.js +61 -5
- package/dist/internal/source-facts.js.map +1 -1
- package/dist/internal/typescript-import.d.ts +49 -24
- package/dist/internal/typescript-import.d.ts.map +1 -1
- package/dist/internal/typescript-import.js +507 -67
- package/dist/internal/typescript-import.js.map +1 -1
- package/dist/introspect.d.ts +6 -4
- package/dist/introspect.d.ts.map +1 -1
- package/dist/introspect.js +93 -74
- package/dist/introspect.js.map +1 -1
- package/dist/learn.js +3 -3
- package/dist/learn.js.map +1 -1
- package/dist/levels-tool.d.ts +10 -0
- package/dist/levels-tool.d.ts.map +1 -1
- package/dist/levels-tool.js +33 -3
- package/dist/levels-tool.js.map +1 -1
- package/dist/load.d.ts +71 -16
- package/dist/load.d.ts.map +1 -1
- package/dist/load.js +78 -18
- package/dist/load.js.map +1 -1
- package/dist/manifest-tool.d.ts.map +1 -1
- package/dist/manifest-tool.js +3 -2
- package/dist/manifest-tool.js.map +1 -1
- package/dist/mcp-app-tool.d.ts.map +1 -1
- package/dist/mcp-app-tool.js +2 -0
- package/dist/mcp-app-tool.js.map +1 -1
- package/dist/mcp-context.d.ts.map +1 -1
- package/dist/mcp-context.js +5 -6
- package/dist/mcp-context.js.map +1 -1
- package/dist/mcp-delegate.d.ts +57 -0
- package/dist/mcp-delegate.d.ts.map +1 -0
- package/dist/mcp-delegate.js +130 -0
- package/dist/mcp-delegate.js.map +1 -0
- package/dist/mcp-docs-tools.d.ts.map +1 -1
- package/dist/mcp-docs-tools.js +23 -5
- package/dist/mcp-docs-tools.js.map +1 -1
- package/dist/mcp-exec.d.ts +3 -0
- package/dist/mcp-exec.d.ts.map +1 -1
- package/dist/mcp-exec.js +263 -131
- package/dist/mcp-exec.js.map +1 -1
- package/dist/mcp-http.d.ts.map +1 -1
- package/dist/mcp-http.js +20 -5
- package/dist/mcp-http.js.map +1 -1
- package/dist/mcp-io.d.ts +26 -0
- package/dist/mcp-io.d.ts.map +1 -1
- package/dist/mcp-io.js +113 -0
- package/dist/mcp-io.js.map +1 -1
- package/dist/mcp-isolate.d.ts +48 -0
- package/dist/mcp-isolate.d.ts.map +1 -0
- package/dist/mcp-isolate.js +222 -0
- package/dist/mcp-isolate.js.map +1 -0
- package/dist/mcp-path.d.ts +10 -0
- package/dist/mcp-path.d.ts.map +1 -0
- package/dist/mcp-path.js +51 -0
- package/dist/mcp-path.js.map +1 -0
- package/dist/mcp-project-child.d.ts +74 -0
- package/dist/mcp-project-child.d.ts.map +1 -0
- package/dist/mcp-project-child.js +130 -0
- package/dist/mcp-project-child.js.map +1 -0
- package/dist/mcp-reflect.d.ts.map +1 -1
- package/dist/mcp-reflect.js +2 -2
- package/dist/mcp-reflect.js.map +1 -1
- package/dist/mcp-render.d.ts.map +1 -1
- package/dist/mcp-render.js +64 -45
- package/dist/mcp-render.js.map +1 -1
- package/dist/mcp-root.d.ts +37 -9
- package/dist/mcp-root.d.ts.map +1 -1
- package/dist/mcp-root.js +124 -25
- package/dist/mcp-root.js.map +1 -1
- package/dist/mcp-run.d.ts +18 -1
- package/dist/mcp-run.d.ts.map +1 -1
- package/dist/mcp-run.js +159 -55
- package/dist/mcp-run.js.map +1 -1
- package/dist/mcp-ws.js +4 -1
- package/dist/mcp-ws.js.map +1 -1
- package/dist/mcp.d.ts +18 -5
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +165 -82
- package/dist/mcp.js.map +1 -1
- package/dist/migrate-layout.d.ts +44 -0
- package/dist/migrate-layout.d.ts.map +1 -0
- package/dist/migrate-layout.js +982 -0
- package/dist/migrate-layout.js.map +1 -0
- package/dist/openapi-tool.d.ts +2 -2
- package/dist/openapi-tool.d.ts.map +1 -1
- package/dist/openapi-tool.js.map +1 -1
- package/dist/openapi-types.d.ts +2 -1
- package/dist/openapi-types.d.ts.map +1 -1
- package/dist/openapi-types.js +240 -12
- package/dist/openapi-types.js.map +1 -1
- package/dist/pipeline-report.d.ts +3 -2
- package/dist/pipeline-report.d.ts.map +1 -1
- package/dist/pipeline-report.js +19 -16
- package/dist/pipeline-report.js.map +1 -1
- package/dist/port.d.ts +5 -4
- package/dist/port.d.ts.map +1 -1
- package/dist/port.js +46 -29
- package/dist/port.js.map +1 -1
- package/dist/project-facts.d.ts +28 -4
- package/dist/project-facts.d.ts.map +1 -1
- package/dist/project-facts.js +12 -2
- package/dist/project-facts.js.map +1 -1
- package/dist/recipes/4.0.0.d.ts +9 -0
- package/dist/recipes/4.0.0.d.ts.map +1 -0
- package/dist/recipes/4.0.0.js +34 -0
- package/dist/recipes/4.0.0.js.map +1 -0
- package/dist/recipes/index.d.ts.map +1 -1
- package/dist/recipes/index.js +2 -0
- package/dist/recipes/index.js.map +1 -1
- package/dist/release-verification.d.ts +11 -1
- package/dist/release-verification.d.ts.map +1 -1
- package/dist/release-verification.js +47 -4
- package/dist/release-verification.js.map +1 -1
- package/dist/review.d.ts +21 -0
- package/dist/review.d.ts.map +1 -0
- package/dist/review.js +823 -0
- package/dist/review.js.map +1 -0
- package/dist/route-types.d.ts +28 -0
- package/dist/route-types.d.ts.map +1 -0
- package/dist/route-types.js +106 -0
- package/dist/route-types.js.map +1 -0
- package/dist/rules/codes.d.ts +9 -0
- package/dist/rules/codes.d.ts.map +1 -1
- package/dist/rules/codes.js +9 -0
- package/dist/rules/codes.js.map +1 -1
- package/dist/rules/data-guard.d.ts +12 -0
- package/dist/rules/data-guard.d.ts.map +1 -0
- package/dist/rules/data-guard.js +213 -0
- package/dist/rules/data-guard.js.map +1 -0
- package/dist/rules/index.d.ts +3 -0
- package/dist/rules/index.d.ts.map +1 -1
- package/dist/rules/index.js +17 -5
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/islands.d.ts.map +1 -1
- package/dist/rules/islands.js +7 -4
- package/dist/rules/islands.js.map +1 -1
- package/dist/rules/legacy.d.ts +3 -1
- package/dist/rules/legacy.d.ts.map +1 -1
- package/dist/rules/legacy.js +829 -34
- package/dist/rules/legacy.js.map +1 -1
- package/dist/rules/nano.d.ts.map +1 -1
- package/dist/rules/nano.js +15 -6
- package/dist/rules/nano.js.map +1 -1
- package/dist/rules/pages.d.ts +15 -0
- package/dist/rules/pages.d.ts.map +1 -0
- package/dist/rules/pages.js +88 -0
- package/dist/rules/pages.js.map +1 -0
- package/dist/rules/routes.d.ts +11 -0
- package/dist/rules/routes.d.ts.map +1 -1
- package/dist/rules/routes.js +180 -2
- package/dist/rules/routes.js.map +1 -1
- package/dist/rules/secrets.d.ts +10 -0
- package/dist/rules/secrets.d.ts.map +1 -0
- package/dist/rules/secrets.js +169 -0
- package/dist/rules/secrets.js.map +1 -0
- package/dist/rules/security.d.ts +7 -0
- package/dist/rules/security.d.ts.map +1 -1
- package/dist/rules/security.js +171 -65
- package/dist/rules/security.js.map +1 -1
- package/dist/rules/typescript.d.ts +16 -0
- package/dist/rules/typescript.d.ts.map +1 -0
- package/dist/rules/typescript.js +59 -0
- package/dist/rules/typescript.js.map +1 -0
- package/dist/rules/zones.d.ts +17 -0
- package/dist/rules/zones.d.ts.map +1 -0
- package/dist/rules/zones.js +174 -0
- package/dist/rules/zones.js.map +1 -0
- package/dist/scaffold.d.ts +25 -12
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +154 -90
- package/dist/scaffold.js.map +1 -1
- package/dist/sdk.d.ts +17 -3
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +941 -153
- package/dist/sdk.js.map +1 -1
- package/dist/search-terms.d.ts +2 -0
- package/dist/search-terms.d.ts.map +1 -1
- package/dist/search-terms.js +23 -8
- package/dist/search-terms.js.map +1 -1
- package/dist/smoke.d.ts +79 -0
- package/dist/smoke.d.ts.map +1 -0
- package/dist/smoke.js +494 -0
- package/dist/smoke.js.map +1 -0
- package/dist/sync-manifest.d.ts.map +1 -1
- package/dist/sync-manifest.js +2 -1
- package/dist/sync-manifest.js.map +1 -1
- package/dist/sync-routes.d.ts +4 -3
- package/dist/sync-routes.d.ts.map +1 -1
- package/dist/sync-routes.js +4 -3
- package/dist/sync-routes.js.map +1 -1
- package/dist/test-tool.d.ts.map +1 -1
- package/dist/test-tool.js +30 -11
- package/dist/test-tool.js.map +1 -1
- package/dist/types-search.d.ts.map +1 -1
- package/dist/types-search.js +11 -5
- package/dist/types-search.js.map +1 -1
- package/dist/upgrade.d.ts +36 -6
- package/dist/upgrade.d.ts.map +1 -1
- package/dist/upgrade.js +167 -29
- package/dist/upgrade.js.map +1 -1
- package/dist/verification-plan.d.ts.map +1 -1
- package/dist/verification-plan.js +8 -0
- package/dist/verification-plan.js.map +1 -1
- package/dist/verification.d.ts.map +1 -1
- package/dist/verification.js +45 -12
- package/dist/verification.js.map +1 -1
- package/dist/web-app-options.d.ts +19 -0
- package/dist/web-app-options.d.ts.map +1 -0
- package/dist/web-app-options.js +49 -0
- package/dist/web-app-options.js.map +1 -0
- package/dist/work-graph.d.ts +3 -0
- package/dist/work-graph.d.ts.map +1 -1
- package/dist/work-graph.js +33 -16
- package/dist/work-graph.js.map +1 -1
- package/docs/examples.json +185 -29
- package/docs/llms-full.txt +4123 -548
- package/docs/types.json +14391 -10060
- package/package.json +19 -11
- package/src/app-files.ts +40 -0
- package/src/assure-hydration.ts +139 -46
- package/src/assure.ts +17 -2
- package/src/capabilities-tool.ts +35 -9
- package/src/cdn-check.ts +268 -0
- package/src/check-diagnostics.ts +199 -797
- package/src/check-scan.ts +394 -63
- package/src/check.ts +146 -58
- package/src/cli.ts +347 -92
- package/src/command-catalog.ts +407 -45
- package/src/config-target.ts +47 -0
- package/src/contracts.ts +66 -15
- package/src/db-audit.ts +128 -0
- package/src/db-child.ts +285 -0
- package/src/db-config.ts +224 -0
- package/src/db-tool.ts +690 -0
- package/src/dev-bun-config.ts +116 -40
- package/src/dev-feed-tool.ts +912 -0
- package/src/dev-server-client.ts +267 -0
- package/src/diagnostics.ts +179 -0
- package/src/diff-tool.ts +6 -5
- package/src/docs-search.ts +10 -3
- package/src/doctor.ts +98 -15
- package/src/env-file.ts +78 -1
- package/src/fix-recipes.ts +44 -31
- package/src/frontend-guidance.ts +5 -5
- package/src/i18n-check.ts +179 -0
- package/src/init-agents.ts +301 -38
- package/src/internal/code-unit-order.ts +5 -0
- package/src/internal/script-kind.ts +11 -0
- package/src/internal/source-facts.ts +96 -22
- package/src/internal/typescript-import.ts +663 -68
- package/src/introspect.ts +93 -82
- package/src/learn.ts +3 -3
- package/src/levels-tool.ts +47 -2
- package/src/load.ts +144 -32
- package/src/manifest-tool.ts +3 -2
- package/src/mcp-app-tool.ts +2 -0
- package/src/mcp-context.ts +5 -7
- package/src/mcp-delegate.ts +162 -0
- package/src/mcp-docs-tools.ts +23 -5
- package/src/mcp-exec.ts +301 -161
- package/src/mcp-http.ts +22 -5
- package/src/mcp-io.ts +130 -0
- package/src/mcp-isolate.ts +287 -0
- package/src/mcp-path.ts +50 -0
- package/src/mcp-project-child.ts +182 -0
- package/src/mcp-reflect.ts +3 -2
- package/src/mcp-render.ts +72 -42
- package/src/mcp-root.ts +130 -24
- package/src/mcp-run.ts +190 -56
- package/src/mcp-ws.ts +4 -1
- package/src/mcp.ts +193 -91
- package/src/migrate-layout.ts +1121 -0
- package/src/openapi-tool.ts +7 -3
- package/src/openapi-types.ts +315 -13
- package/src/pipeline-report.ts +22 -18
- package/src/port.ts +53 -30
- package/src/project-facts.ts +42 -8
- package/src/recipes/4.0.0.ts +35 -0
- package/src/recipes/index.ts +2 -0
- package/src/release-verification.ts +74 -5
- package/src/review.ts +988 -0
- package/src/route-types.ts +133 -0
- package/src/rules/codes.ts +9 -0
- package/src/rules/data-guard.ts +230 -0
- package/src/rules/index.ts +18 -5
- package/src/rules/islands.ts +12 -4
- package/src/rules/legacy.ts +947 -35
- package/src/rules/nano.ts +18 -6
- package/src/rules/pages.ts +99 -0
- package/src/rules/routes.ts +181 -2
- package/src/rules/secrets.ts +174 -0
- package/src/rules/security.ts +198 -70
- package/src/rules/typescript.ts +77 -0
- package/src/rules/zones.ts +195 -0
- package/src/scaffold.ts +175 -98
- package/src/sdk.ts +1215 -158
- package/src/search-terms.ts +24 -7
- package/src/smoke.ts +607 -0
- package/src/sync-manifest.ts +2 -1
- package/src/sync-routes.ts +4 -3
- package/src/test-tool.ts +32 -11
- package/src/types-search.ts +11 -4
- package/src/upgrade.ts +205 -25
- package/src/verification-plan.ts +27 -0
- package/src/verification.ts +50 -12
- package/src/web-app-options.ts +57 -0
- package/src/work-graph.ts +40 -16
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The subprocess `nifra mcp` runs project code in. The server (`mcp-isolate.ts`) spawns a fresh one
|
|
3
|
+
* per call in the project's directory, so Bun loads that directory's `.env` files here, and the
|
|
4
|
+
* config, the backend and everything they import run here and exit with the process. The tools,
|
|
5
|
+
* resources and prompts answered here are the same ones the server lists: the project tools, and the
|
|
6
|
+
* ones the app declares on its backend.
|
|
7
|
+
*
|
|
8
|
+
* Protocol: one JSON request on stdin; on stdout, progress lines and then one answer line, each
|
|
9
|
+
* prefixed with the request's token, so anything project code prints cannot be read as an answer.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { detectMonorepo } from "./load.ts"
|
|
13
|
+
import { projectResources } from "./mcp-context.ts"
|
|
14
|
+
import { createCachedAppLoader, projectTools } from "./mcp-exec.ts"
|
|
15
|
+
import { CHILD_INPUT_MAX_BYTES, readBoundedStream } from "./mcp-io.ts"
|
|
16
|
+
import type {
|
|
17
|
+
McpPrompt,
|
|
18
|
+
McpPromptMessage,
|
|
19
|
+
McpResource,
|
|
20
|
+
McpTool,
|
|
21
|
+
McpToolResult,
|
|
22
|
+
} from "./mcp-protocol.ts"
|
|
23
|
+
import {
|
|
24
|
+
extractBackendPrompts,
|
|
25
|
+
extractBackendResources,
|
|
26
|
+
extractBackendTools,
|
|
27
|
+
} from "./mcp-reflect.ts"
|
|
28
|
+
|
|
29
|
+
export type ProjectChildRequest =
|
|
30
|
+
| { readonly op: "monorepo" }
|
|
31
|
+
| { readonly op: "surface" }
|
|
32
|
+
| { readonly op: "tool"; readonly name: string; readonly args: Record<string, unknown> }
|
|
33
|
+
| { readonly op: "resource"; readonly uri: string }
|
|
34
|
+
| { readonly op: "prompt"; readonly name: string; readonly args: Record<string, unknown> }
|
|
35
|
+
|
|
36
|
+
/** What the server lists for the tools, resources and prompts the app declares on its backend. */
|
|
37
|
+
export interface AppSurface {
|
|
38
|
+
readonly tools: readonly Omit<McpTool, "handler">[]
|
|
39
|
+
readonly resources: readonly Omit<McpResource, "read">[]
|
|
40
|
+
readonly prompts: readonly Omit<McpPrompt, "handler">[]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type ProjectChildAnswer =
|
|
44
|
+
| { readonly ok: true; readonly apps: Readonly<Record<string, string>> | null }
|
|
45
|
+
| { readonly ok: true; readonly surface: AppSurface }
|
|
46
|
+
| { readonly ok: true; readonly result: string | McpToolResult }
|
|
47
|
+
| { readonly ok: true; readonly resource: { readonly text: string; readonly mimeType?: string } }
|
|
48
|
+
| { readonly ok: true; readonly messages: readonly McpPromptMessage[] }
|
|
49
|
+
| { readonly ok: false; readonly message: string }
|
|
50
|
+
|
|
51
|
+
export type ProjectChildMessage =
|
|
52
|
+
| { readonly type: "progress"; readonly progress: number; readonly total?: number }
|
|
53
|
+
| { readonly type: "answer"; readonly answer: ProjectChildAnswer }
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Answer `request` for the project at `cwd`. A tool, resource or prompt that throws answers
|
|
57
|
+
* `ok: false` with its message; an app that cannot be loaded declares nothing, as in the server.
|
|
58
|
+
*/
|
|
59
|
+
export async function answerProjectRequest(
|
|
60
|
+
cwd: string,
|
|
61
|
+
request: ProjectChildRequest,
|
|
62
|
+
reportProgress: (progress: number, total?: number) => void = () => {},
|
|
63
|
+
): Promise<ProjectChildAnswer> {
|
|
64
|
+
try {
|
|
65
|
+
if (request.op === "monorepo") {
|
|
66
|
+
return { ok: true, apps: (await detectMonorepo(cwd))?.apps ?? null }
|
|
67
|
+
}
|
|
68
|
+
const loadAppCached = createCachedAppLoader(cwd)
|
|
69
|
+
const backend = (): Promise<unknown> =>
|
|
70
|
+
loadAppCached().then(
|
|
71
|
+
(app) => app.backend,
|
|
72
|
+
() => undefined,
|
|
73
|
+
)
|
|
74
|
+
if (request.op === "surface") {
|
|
75
|
+
const declared = await backend()
|
|
76
|
+
return {
|
|
77
|
+
ok: true,
|
|
78
|
+
surface: {
|
|
79
|
+
tools: extractBackendTools(declared),
|
|
80
|
+
resources: extractBackendResources(declared),
|
|
81
|
+
prompts: extractBackendPrompts(declared),
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (request.op === "tool") {
|
|
86
|
+
const tool =
|
|
87
|
+
projectTools(cwd, loadAppCached).find((t) => t.name === request.name) ??
|
|
88
|
+
extractBackendTools(await backend()).find((t) => t.name === request.name)
|
|
89
|
+
if (tool === undefined) return { ok: false, message: `unknown tool: ${request.name}` }
|
|
90
|
+
const result = await tool.handler(request.args, {
|
|
91
|
+
signal: new AbortController().signal,
|
|
92
|
+
requestId: null,
|
|
93
|
+
reportProgress,
|
|
94
|
+
})
|
|
95
|
+
return { ok: true, result }
|
|
96
|
+
}
|
|
97
|
+
if (request.op === "resource") {
|
|
98
|
+
const resource =
|
|
99
|
+
projectResources(cwd, loadAppCached).find((r) => r.uri === request.uri) ??
|
|
100
|
+
extractBackendResources(await backend()).find((r) => r.uri === request.uri)
|
|
101
|
+
if (resource === undefined) return { ok: false, message: `unknown resource: ${request.uri}` }
|
|
102
|
+
return { ok: true, resource: await resource.read() }
|
|
103
|
+
}
|
|
104
|
+
const prompt = extractBackendPrompts(await backend()).find((p) => p.name === request.name)
|
|
105
|
+
if (prompt === undefined) return { ok: false, message: `unknown prompt: ${request.name}` }
|
|
106
|
+
return { ok: true, messages: await prompt.handler(request.args) }
|
|
107
|
+
} catch (error) {
|
|
108
|
+
return { ok: false, message: error instanceof Error ? error.message : String(error) }
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const isArgs = (value: unknown): value is Record<string, unknown> =>
|
|
113
|
+
typeof value === "object" && value !== null && !Array.isArray(value)
|
|
114
|
+
|
|
115
|
+
function parseRequest(text: string): { token: string; request: ProjectChildRequest } | undefined {
|
|
116
|
+
let value: unknown
|
|
117
|
+
try {
|
|
118
|
+
value = JSON.parse(text)
|
|
119
|
+
} catch {
|
|
120
|
+
return undefined
|
|
121
|
+
}
|
|
122
|
+
if (!isArgs(value)) return undefined
|
|
123
|
+
const { token, op, name, uri, args } = value
|
|
124
|
+
if (typeof token !== "string" || !/^[0-9a-f-]{16,64}$/.test(token)) return undefined
|
|
125
|
+
if (op === "monorepo" || op === "surface") return { token, request: { op } }
|
|
126
|
+
if (op === "resource" && typeof uri === "string") return { token, request: { op, uri } }
|
|
127
|
+
if ((op === "tool" || op === "prompt") && typeof name === "string" && isArgs(args)) {
|
|
128
|
+
return { token, request: { op, name, args } }
|
|
129
|
+
}
|
|
130
|
+
return undefined
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Serve one request: read it from `input`, answer it and write each message through `write`,
|
|
135
|
+
* prefixed with the request's token. Returns false (writing nothing) for a malformed request.
|
|
136
|
+
*/
|
|
137
|
+
export async function serveProjectChild(
|
|
138
|
+
cwd: string,
|
|
139
|
+
input: ReadableStream<Uint8Array>,
|
|
140
|
+
write: (line: string) => Promise<void>,
|
|
141
|
+
): Promise<boolean> {
|
|
142
|
+
const text = await readBoundedStream(input, CHILD_INPUT_MAX_BYTES)
|
|
143
|
+
const parsed = text.truncated ? undefined : parseRequest(text.text)
|
|
144
|
+
if (parsed === undefined) return false
|
|
145
|
+
const send = (message: ProjectChildMessage): Promise<void> =>
|
|
146
|
+
write(`${parsed.token} ${JSON.stringify(message)}\n`)
|
|
147
|
+
const answer = await answerProjectRequest(cwd, parsed.request, (progress, total) => {
|
|
148
|
+
void send(
|
|
149
|
+
total === undefined ? { type: "progress", progress } : { type: "progress", progress, total },
|
|
150
|
+
)
|
|
151
|
+
})
|
|
152
|
+
let line: string
|
|
153
|
+
try {
|
|
154
|
+
line = JSON.stringify({ type: "answer", answer } satisfies ProjectChildMessage)
|
|
155
|
+
} catch {
|
|
156
|
+
line = JSON.stringify({
|
|
157
|
+
type: "answer",
|
|
158
|
+
answer: { ok: false, message: "the answer is not serializable" },
|
|
159
|
+
} satisfies ProjectChildMessage)
|
|
160
|
+
}
|
|
161
|
+
await write(`${parsed.token} ${line}\n`)
|
|
162
|
+
return true
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (import.meta.main) {
|
|
166
|
+
// Bound before project code loads, so nothing it patches can reach the protocol channel.
|
|
167
|
+
const stdout = process.stdout.write.bind(process.stdout)
|
|
168
|
+
const toStderr = (...args: unknown[]): void => {
|
|
169
|
+
process.stderr.write(`${args.map((arg) => String(arg)).join(" ")}\n`)
|
|
170
|
+
}
|
|
171
|
+
console.log = toStderr
|
|
172
|
+
console.info = toStderr
|
|
173
|
+
console.debug = toStderr
|
|
174
|
+
const served = await serveProjectChild(
|
|
175
|
+
process.argv[2] ?? process.cwd(),
|
|
176
|
+
Bun.stdin.stream(),
|
|
177
|
+
(line) => new Promise((done) => stdout(line, () => done())),
|
|
178
|
+
)
|
|
179
|
+
if (!served) process.stderr.write("nifra mcp: invalid project request\n")
|
|
180
|
+
// Project code may leave a handle open (a pool, an interval); the answer is written, so stop here.
|
|
181
|
+
process.exit(served ? 0 : 2)
|
|
182
|
+
}
|
package/src/mcp-reflect.ts
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* about stdio, HTTP, root trust, or project loading, so the reflection seam is independently testable.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { reflectedRoutesFromEvidence, snapshotProjectEvidence } from "@nifrajs/core/evidence"
|
|
9
|
+
import type { ReflectedRoute } from "@nifrajs/core/reflection"
|
|
9
10
|
import type {
|
|
10
11
|
McpPrompt,
|
|
11
12
|
McpPromptMessage,
|
|
@@ -24,7 +25,7 @@ type ToolBackend = {
|
|
|
24
25
|
export function extractBackendTools(backend: unknown): McpTool[] {
|
|
25
26
|
const b = backend as ToolBackend | null
|
|
26
27
|
if (!b || typeof b.routes !== "function") return []
|
|
27
|
-
const routes =
|
|
28
|
+
const routes = reflectedRoutesFromEvidence(snapshotProjectEvidence(b))
|
|
28
29
|
|
|
29
30
|
return routes
|
|
30
31
|
.filter(
|
package/src/mcp-render.ts
CHANGED
|
@@ -10,11 +10,18 @@
|
|
|
10
10
|
* `{ error }` so the agent gets the actionable message.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { inProcessClient } from "@nifrajs/client"
|
|
14
13
|
import { type AppLike, runApp } from "@nifrajs/runner"
|
|
15
14
|
import { createWebApp, type RenderAdapter } from "@nifrajs/web"
|
|
16
15
|
import { discoverRoutes } from "@nifrajs/web/fs"
|
|
17
16
|
import { loadApp, resolvePlugins } from "./load.ts"
|
|
17
|
+
import {
|
|
18
|
+
CHILD_INPUT_MAX_BYTES,
|
|
19
|
+
CHILD_OUTPUT_MAX_BYTES,
|
|
20
|
+
readBoundedLines,
|
|
21
|
+
readBoundedStream,
|
|
22
|
+
serializeBoundedJson,
|
|
23
|
+
} from "./mcp-io.ts"
|
|
24
|
+
import { frameworkWebAppOptions } from "./web-app-options.ts"
|
|
18
25
|
|
|
19
26
|
const errString = (err: unknown): string =>
|
|
20
27
|
err instanceof Error ? `${err.name}: ${err.message}` : String(err)
|
|
@@ -47,7 +54,7 @@ async function buildWebApp(cwd: string): Promise<{ app: AppLike } | { error: str
|
|
|
47
54
|
adapter: app.framework.adapter as RenderAdapter,
|
|
48
55
|
manifest: discoverRoutes(app.routesDir),
|
|
49
56
|
clientEntry: "/_nifra-render-only.js", // placeholder; SSR HTML does not need the built client
|
|
50
|
-
...(app.
|
|
57
|
+
...frameworkWebAppOptions(app.framework, app.backend),
|
|
51
58
|
}) as unknown as AppLike
|
|
52
59
|
return { app: webApp }
|
|
53
60
|
} catch (err) {
|
|
@@ -89,6 +96,12 @@ function redirectConsoleToStderr(): void {
|
|
|
89
96
|
console.error = (...args: unknown[]) => write("error", args)
|
|
90
97
|
}
|
|
91
98
|
|
|
99
|
+
function safeResponseId(value: unknown): number | string | null {
|
|
100
|
+
if (typeof value === "number" && Number.isSafeInteger(value)) return value
|
|
101
|
+
if (typeof value === "string" && value.length <= 128) return value
|
|
102
|
+
return null
|
|
103
|
+
}
|
|
104
|
+
|
|
92
105
|
/**
|
|
93
106
|
* Warm-worker loop (`--worker`): build the web app ONCE, then SSR each `{ id, input: { requests } }` line
|
|
94
107
|
* against that hot app and reply `{ id, output }`. The parent (`mcp.ts`'s warm-render handler) spawns this,
|
|
@@ -100,43 +113,51 @@ async function runWorker(cwd: string): Promise<void> {
|
|
|
100
113
|
// Build once; the same `{ error }` is returned for every request if the build failed, so an agent sees
|
|
101
114
|
// the actionable message and the parent restarts the worker on the next file change.
|
|
102
115
|
const built = await buildWebApp(cwd)
|
|
103
|
-
const decoder = new TextDecoder()
|
|
104
|
-
let buffer = ""
|
|
105
116
|
const send = (id: unknown, output: unknown): void => {
|
|
106
|
-
|
|
117
|
+
const responseId = safeResponseId(id)
|
|
118
|
+
const encoded = serializeBoundedJson({ id: responseId, output }, CHILD_OUTPUT_MAX_BYTES - 1)
|
|
119
|
+
process.stdout.write(
|
|
120
|
+
`${encoded ?? JSON.stringify({ id: responseId, output: { error: "worker output exceeded the size limit" } })}\n`,
|
|
121
|
+
)
|
|
107
122
|
}
|
|
108
|
-
for await (const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (line === "") continue
|
|
123
|
+
for await (const item of readBoundedLines(Bun.stdin.stream(), CHILD_INPUT_MAX_BYTES)) {
|
|
124
|
+
if (item.kind === "too-large") {
|
|
125
|
+
send(null, { error: `worker input exceeded ${CHILD_INPUT_MAX_BYTES} bytes` })
|
|
126
|
+
continue
|
|
127
|
+
}
|
|
128
|
+
const line = item.text.trim()
|
|
129
|
+
if (line === "") continue
|
|
116
130
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
131
|
+
let message: unknown
|
|
132
|
+
try {
|
|
133
|
+
message = JSON.parse(line) as unknown
|
|
134
|
+
} catch {
|
|
135
|
+
send(null, { error: "invalid worker input: expected JSON line" })
|
|
136
|
+
continue
|
|
137
|
+
}
|
|
138
|
+
const record =
|
|
139
|
+
message !== null && typeof message === "object" && !Array.isArray(message)
|
|
140
|
+
? (message as RenderWorkerMessage)
|
|
141
|
+
: undefined
|
|
142
|
+
const input =
|
|
143
|
+
record?.input !== null && typeof record?.input === "object" && !Array.isArray(record.input)
|
|
144
|
+
? record.input
|
|
145
|
+
: undefined
|
|
146
|
+
const requests = input?.requests
|
|
147
|
+
if (!Array.isArray(requests)) {
|
|
148
|
+
send(record?.id, { error: "expected { requests: [...] }" })
|
|
149
|
+
continue
|
|
150
|
+
}
|
|
151
|
+
if ("error" in built) {
|
|
152
|
+
send(record?.id, built)
|
|
153
|
+
continue
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
send(record?.id, {
|
|
157
|
+
results: await runApp(built.app, requests as Parameters<typeof runApp>[1]),
|
|
158
|
+
})
|
|
159
|
+
} catch (err) {
|
|
160
|
+
send(record?.id, { error: errString(err) })
|
|
140
161
|
}
|
|
141
162
|
}
|
|
142
163
|
}
|
|
@@ -163,13 +184,22 @@ if (import.meta.main) {
|
|
|
163
184
|
process.exit(0)
|
|
164
185
|
}
|
|
165
186
|
let output: unknown
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
output =
|
|
169
|
-
}
|
|
170
|
-
|
|
187
|
+
const input = await readBoundedStream(Bun.stdin.stream(), CHILD_INPUT_MAX_BYTES)
|
|
188
|
+
if (input.truncated) {
|
|
189
|
+
output = { error: `input exceeded ${CHILD_INPUT_MAX_BYTES} bytes` }
|
|
190
|
+
} else {
|
|
191
|
+
try {
|
|
192
|
+
const { requests } = JSON.parse(input.text) as { requests: unknown }
|
|
193
|
+
output = await renderPages(cwd, requests)
|
|
194
|
+
} catch {
|
|
195
|
+
output = { error: "invalid input: expected JSON { requests: [...] }" }
|
|
196
|
+
}
|
|
171
197
|
}
|
|
172
|
-
await Bun.write(
|
|
198
|
+
await Bun.write(
|
|
199
|
+
Bun.stdout,
|
|
200
|
+
serializeBoundedJson(output, CHILD_OUTPUT_MAX_BYTES, 2) ??
|
|
201
|
+
JSON.stringify({ error: `output exceeded ${CHILD_OUTPUT_MAX_BYTES} bytes` }),
|
|
202
|
+
)
|
|
173
203
|
// Exit explicitly, exactly as the `--worker` branch above already does.
|
|
174
204
|
//
|
|
175
205
|
// Loading the app runs its module side effects, and a real app opens things: a database pool, a
|
package/src/mcp-root.ts
CHANGED
|
@@ -5,9 +5,12 @@
|
|
|
5
5
|
* 1. An explicit `nifra mcp <dir>` argument is taken literally (human configuration wins).
|
|
6
6
|
* 2. A cwd guess walks UP to the nearest nifra marker (a `package.json` depending on `@nifrajs/*`,
|
|
7
7
|
* or a `nifra.config.ts` monorepo root), so running from a subdirectory still finds the project.
|
|
8
|
+
* When nothing above is a project but the spawn directory is a package-manager workspace root
|
|
9
|
+
* (`package.json` `workspaces`) with exactly one nifra member, the server adopts that member.
|
|
8
10
|
* 3. The client's MCP `roots` (its workspace folders) are requested after the handshake. When the
|
|
9
11
|
* guess found no project - or found one DISJOINT from every workspace root - and exactly one
|
|
10
|
-
* workspace root is a nifra project, the server
|
|
12
|
+
* workspace root (or one member of a workspace-root folder) is a nifra project, the server
|
|
13
|
+
* adopts it.
|
|
11
14
|
* 4. What cannot be resolved fails CLOSED: project-scoped tools refuse with a remediation message
|
|
12
15
|
* instead of describing whatever directory the server happened to start in.
|
|
13
16
|
*
|
|
@@ -15,12 +18,13 @@
|
|
|
15
18
|
*/
|
|
16
19
|
|
|
17
20
|
import { readFile } from "node:fs/promises"
|
|
18
|
-
import { dirname, isAbsolute, relative, resolve, sep } from "node:path"
|
|
21
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path"
|
|
19
22
|
import { fileURLToPath } from "node:url"
|
|
20
23
|
|
|
21
24
|
/** How the current root was chosen. `arg` = explicit `nifra mcp <dir>`; `cwd` = the spawn directory;
|
|
22
|
-
* `parent` = a marker found walking up from cwd; `
|
|
23
|
-
|
|
25
|
+
* `parent` = a marker found walking up from cwd; `workspace` = the one nifra member of the spawn
|
|
26
|
+
* directory's package-manager workspaces; `client-root` = adopted from the client's MCP roots. */
|
|
27
|
+
export type McpRootSource = "arg" | "cwd" | "parent" | "workspace" | "client-root"
|
|
24
28
|
|
|
25
29
|
export interface McpRootState {
|
|
26
30
|
/** Absolute directory every project-scoped tool operates on. */
|
|
@@ -31,8 +35,18 @@ export interface McpRootState {
|
|
|
31
35
|
/** The client's workspace roots (absolute paths), or `null` until (unless) the client answers
|
|
32
36
|
* `roots/list`. `null` disables mismatch detection - no data is not a mismatch. */
|
|
33
37
|
readonly clientRoots: readonly string[] | null
|
|
38
|
+
/** Nifra members of the spawn directory's workspaces when there were several to choose from - the
|
|
39
|
+
* remediation list, since adopting one of them would be a guess. */
|
|
40
|
+
readonly workspaceCandidates?: readonly string[]
|
|
34
41
|
}
|
|
35
42
|
|
|
43
|
+
const MAX_CLIENT_ROOTS = 64
|
|
44
|
+
const MAX_ROOT_URI_LENGTH = 16_384
|
|
45
|
+
const MAX_ROOT_PATH_LENGTH = 4_096
|
|
46
|
+
/** Workspace members probed per workspace root. A monorepo larger than this is pointed at explicitly
|
|
47
|
+
* (`nifra mcp <dir>`) rather than scanned on every server start. */
|
|
48
|
+
const MAX_WORKSPACE_MEMBERS = 256
|
|
49
|
+
|
|
36
50
|
/** A directory is a nifra project when its `package.json` depends on any `@nifrajs/*` package, or it
|
|
37
51
|
* is a `nifra.config.ts` monorepo root. Backend-only projects (no web config, no `routes/`) count -
|
|
38
52
|
* the marker is the dependency, not the app shape. */
|
|
@@ -65,8 +79,56 @@ export async function findNifraRoot(start: string): Promise<string | null> {
|
|
|
65
79
|
}
|
|
66
80
|
}
|
|
67
81
|
|
|
82
|
+
/** The `workspaces` globs a `package.json` declares (the array form, or Yarn's `{ packages }`), or
|
|
83
|
+
* `null` when the directory is not a workspace root. Negated patterns are dropped: they only exclude. */
|
|
84
|
+
async function workspacePatterns(dir: string): Promise<readonly string[] | null> {
|
|
85
|
+
let field: unknown
|
|
86
|
+
try {
|
|
87
|
+
field = (
|
|
88
|
+
JSON.parse(await readFile(join(dir, "package.json"), "utf8")) as Record<string, unknown>
|
|
89
|
+
).workspaces
|
|
90
|
+
} catch {
|
|
91
|
+
return null
|
|
92
|
+
}
|
|
93
|
+
const list = Array.isArray(field)
|
|
94
|
+
? field
|
|
95
|
+
: typeof field === "object" && field !== null
|
|
96
|
+
? (field as { packages?: unknown }).packages
|
|
97
|
+
: undefined
|
|
98
|
+
if (!Array.isArray(list)) return null
|
|
99
|
+
return list.filter(
|
|
100
|
+
(pattern): pattern is string =>
|
|
101
|
+
typeof pattern === "string" && pattern !== "" && !pattern.startsWith("!"),
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The nifra projects among `dir`'s package-manager workspace members, in declaration order. Empty when
|
|
107
|
+
* `dir` declares no `workspaces`. A member is a directory a pattern matches that holds a `package.json`;
|
|
108
|
+
* `node_modules` is never a member.
|
|
109
|
+
*/
|
|
110
|
+
export async function workspaceProjects(dir: string): Promise<string[]> {
|
|
111
|
+
const patterns = await workspacePatterns(dir)
|
|
112
|
+
if (patterns === null) return []
|
|
113
|
+
const members = new Set<string>()
|
|
114
|
+
let probed = 0
|
|
115
|
+
for (const pattern of patterns) {
|
|
116
|
+
const glob = new Bun.Glob(`${pattern.replace(/\/+$/, "")}/package.json`)
|
|
117
|
+
for await (const match of glob.scan({ cwd: dir, onlyFiles: true })) {
|
|
118
|
+
if (match.split(/[\\/]/).includes("node_modules")) continue
|
|
119
|
+
if (++probed > MAX_WORKSPACE_MEMBERS) break
|
|
120
|
+
members.add(dirname(join(dir, match)))
|
|
121
|
+
}
|
|
122
|
+
if (probed > MAX_WORKSPACE_MEMBERS) break
|
|
123
|
+
}
|
|
124
|
+
const projects: string[] = []
|
|
125
|
+
for (const member of members) if (await isNifraProjectDir(member)) projects.push(member)
|
|
126
|
+
return projects
|
|
127
|
+
}
|
|
128
|
+
|
|
68
129
|
/** Initial root state. An explicit dir is taken literally (no walk-up - the human named it); a cwd
|
|
69
|
-
* guess walks up so a subdirectory spawn still lands on the project
|
|
130
|
+
* guess walks up so a subdirectory spawn still lands on the project, then tries the spawn directory's
|
|
131
|
+
* workspace members so a monorepo root finds its one nifra app. */
|
|
70
132
|
export async function resolveRootState(
|
|
71
133
|
requested: string,
|
|
72
134
|
explicit: boolean,
|
|
@@ -88,7 +150,17 @@ export async function resolveRootState(
|
|
|
88
150
|
clientRoots: null,
|
|
89
151
|
}
|
|
90
152
|
}
|
|
91
|
-
|
|
153
|
+
const members = await workspaceProjects(requested)
|
|
154
|
+
if (members.length === 1) {
|
|
155
|
+
return { root: members[0] as string, source: "workspace", isProject: true, clientRoots: null }
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
root: requested,
|
|
159
|
+
source: "cwd",
|
|
160
|
+
isProject: false,
|
|
161
|
+
clientRoots: null,
|
|
162
|
+
...(members.length > 1 ? { workspaceCandidates: members } : {}),
|
|
163
|
+
}
|
|
92
164
|
}
|
|
93
165
|
|
|
94
166
|
/** `file://` roots from a `roots/list` result, as absolute paths. Non-file and malformed URIs are
|
|
@@ -98,9 +170,12 @@ export function pathsFromRootsResult(result: unknown): string[] {
|
|
|
98
170
|
const roots = (result as { roots?: unknown }).roots
|
|
99
171
|
if (!Array.isArray(roots)) return []
|
|
100
172
|
const paths: string[] = []
|
|
173
|
+
const seen = new Set<string>()
|
|
101
174
|
for (const entry of roots) {
|
|
175
|
+
if (paths.length >= MAX_CLIENT_ROOTS) break
|
|
102
176
|
const uri = (entry as { uri?: unknown })?.uri
|
|
103
|
-
if (typeof uri !== "string" || !uri.startsWith("file://"))
|
|
177
|
+
if (typeof uri !== "string" || uri.length > MAX_ROOT_URI_LENGTH || !uri.startsWith("file://"))
|
|
178
|
+
continue
|
|
104
179
|
try {
|
|
105
180
|
const parsed = new URL(uri)
|
|
106
181
|
// A remote file host is a UNC path on Windows, not a local workspace root. MCP roots are
|
|
@@ -111,7 +186,11 @@ export function pathsFromRootsResult(result: unknown): string[] {
|
|
|
111
186
|
(parsed.hostname !== "" && parsed.hostname !== "localhost")
|
|
112
187
|
)
|
|
113
188
|
continue
|
|
114
|
-
|
|
189
|
+
const path = fileURLToPath(parsed)
|
|
190
|
+
if (path.length === 0 || path.length > MAX_ROOT_PATH_LENGTH || path.includes("\0")) continue
|
|
191
|
+
if (seen.has(path)) continue
|
|
192
|
+
seen.add(path)
|
|
193
|
+
paths.push(path)
|
|
115
194
|
} catch {
|
|
116
195
|
// Malformed URI - skip.
|
|
117
196
|
}
|
|
@@ -140,9 +219,9 @@ export function rootMismatch(state: McpRootState): boolean {
|
|
|
140
219
|
/**
|
|
141
220
|
* Fold the client's workspace roots into the state, adopting one of them when the current root is
|
|
142
221
|
* wrong and the correction is unambiguous: the root was GUESSED (never an explicit `nifra mcp <dir>`),
|
|
143
|
-
* it is not a project - or is one disjoint from the whole workspace - and exactly ONE
|
|
144
|
-
*
|
|
145
|
-
* message lists the candidates instead.
|
|
222
|
+
* it is not a project - or is one disjoint from the whole workspace - and exactly ONE candidate exists
|
|
223
|
+
* (see {@link nifraCandidates}). Ambiguity (zero or several candidates) adopts nothing; the guard's
|
|
224
|
+
* remediation message lists the candidates instead.
|
|
146
225
|
*/
|
|
147
226
|
export async function applyClientRoots(
|
|
148
227
|
state: McpRootState,
|
|
@@ -151,8 +230,7 @@ export async function applyClientRoots(
|
|
|
151
230
|
const next: McpRootState = { ...state, clientRoots: paths }
|
|
152
231
|
if (state.source === "arg") return next
|
|
153
232
|
if (next.isProject && !rootMismatch(next)) return next
|
|
154
|
-
const candidates
|
|
155
|
-
for (const path of paths) if (await isNifraProjectDir(path)) candidates.push(path)
|
|
233
|
+
const candidates = await nifraCandidates(paths)
|
|
156
234
|
if (candidates.length !== 1) return next
|
|
157
235
|
return {
|
|
158
236
|
root: candidates[0] as string,
|
|
@@ -162,11 +240,15 @@ export async function applyClientRoots(
|
|
|
162
240
|
}
|
|
163
241
|
}
|
|
164
242
|
|
|
165
|
-
/** The nifra
|
|
166
|
-
*
|
|
243
|
+
/** The nifra projects the client's workspace roots name: each root that is a project itself, and
|
|
244
|
+
* otherwise the nifra members of a root that is a package-manager workspace. The remediation list
|
|
245
|
+
* when adoption was ambiguous. */
|
|
167
246
|
export async function nifraCandidates(paths: readonly string[]): Promise<string[]> {
|
|
168
247
|
const candidates: string[] = []
|
|
169
|
-
for (const path of paths)
|
|
248
|
+
for (const path of paths) {
|
|
249
|
+
if (await isNifraProjectDir(path)) candidates.push(path)
|
|
250
|
+
else for (const member of await workspaceProjects(path)) candidates.push(member)
|
|
251
|
+
}
|
|
170
252
|
return candidates
|
|
171
253
|
}
|
|
172
254
|
|
|
@@ -187,10 +269,15 @@ export async function rootVerdict(state: McpRootState): Promise<McpRootVerdict>
|
|
|
187
269
|
? ` Client workspace roots: ${state.clientRoots.join(", ")}.`
|
|
188
270
|
: ""
|
|
189
271
|
if (!state.isProject) {
|
|
190
|
-
const candidates =
|
|
272
|
+
const candidates = [
|
|
273
|
+
...new Set([
|
|
274
|
+
...(state.workspaceCandidates ?? []),
|
|
275
|
+
...(state.clientRoots !== null ? await nifraCandidates(state.clientRoots) : []),
|
|
276
|
+
]),
|
|
277
|
+
]
|
|
191
278
|
const hint =
|
|
192
279
|
candidates.length > 1
|
|
193
|
-
? ` Several workspace
|
|
280
|
+
? ` Several workspace folders are nifra projects (${candidates.join(", ")}) - pass one: \`nifra mcp <dir>\`.`
|
|
194
281
|
: ""
|
|
195
282
|
return {
|
|
196
283
|
blocked:
|
|
@@ -239,7 +326,8 @@ export interface ToolingDrift {
|
|
|
239
326
|
export const driftNote = (drift: ToolingDrift): string =>
|
|
240
327
|
`WARNING: this server runs nifra CLI ${drift.cli}, but the project installs ${drift.package} ${drift.project}. ` +
|
|
241
328
|
"Types, checks, and docs may describe a different version than your code builds with. " +
|
|
242
|
-
|
|
329
|
+
`Fix: re-pin the project's MCP launch to the nifra it installs with \`${syncMcpCommand(drift.cli)}\` (run in the project directory), then restart the agent. ` +
|
|
330
|
+
"If this server is not launched from the project's .mcp.json, run the project's own CLI instead (`bunx --bun nifra mcp` from the project directory, or point the client at ./node_modules/.bin/nifra)."
|
|
243
331
|
|
|
244
332
|
/** Feature-level agreement. Patch drift is not reported: it never changes the surface being described. */
|
|
245
333
|
const featureVersion = (version: string): string => version.split(".").slice(0, 2).join(".")
|
|
@@ -274,16 +362,34 @@ export async function detectToolingDrift(
|
|
|
274
362
|
root: string,
|
|
275
363
|
cliVersion: string,
|
|
276
364
|
): Promise<ToolingDrift | undefined> {
|
|
365
|
+
const installed = await installedNifraVersion(root)
|
|
366
|
+
if (installed === undefined) return undefined
|
|
367
|
+
return featureVersion(installed.version) === featureVersion(cliVersion)
|
|
368
|
+
? undefined
|
|
369
|
+
: { cli: cliVersion, project: installed.version, package: installed.package }
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* The nifra the project builds with: its `@nifrajs/cli` when installed, else its `@nifrajs/core`
|
|
374
|
+
* (`fixed` versioning keeps the two in lockstep, so either names the CLI that matches the project).
|
|
375
|
+
* The version drift detection compares against and the version `init-agents --sync-mcp` pins to.
|
|
376
|
+
*/
|
|
377
|
+
export async function installedNifraVersion(
|
|
378
|
+
root: string,
|
|
379
|
+
): Promise<{ readonly package: string; readonly version: string } | undefined> {
|
|
277
380
|
for (const name of ["@nifrajs/cli", "@nifrajs/core"]) {
|
|
278
|
-
const
|
|
279
|
-
if (
|
|
280
|
-
return featureVersion(project) === featureVersion(cliVersion)
|
|
281
|
-
? undefined
|
|
282
|
-
: { cli: cliVersion, project, package: name }
|
|
381
|
+
const version = await installedVersion(root, name)
|
|
382
|
+
if (version !== undefined) return { package: name, version }
|
|
283
383
|
}
|
|
284
384
|
return undefined
|
|
285
385
|
}
|
|
286
386
|
|
|
387
|
+
/** The command that re-pins a project's MCP launch to the nifra it installs. Named with the running
|
|
388
|
+
* CLI's own exact version: the binary printing this is the one known to have `--sync-mcp`, and it pins
|
|
389
|
+
* to the project's version, not its own. */
|
|
390
|
+
export const syncMcpCommand = (cliVersion: string): string =>
|
|
391
|
+
`bunx @nifrajs/cli@${cliVersion} init-agents --sync-mcp`
|
|
392
|
+
|
|
287
393
|
/** Tool names served from the bundled corpus; they do not depend on the project root. */
|
|
288
394
|
const PROJECT_FREE_TOOLS = new Set(["nifra_docs", "nifra_example", "nifra_types", "nifra_learn"])
|
|
289
395
|
|