@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.
Files changed (425) hide show
  1. package/README.md +42 -3
  2. package/dist/app-files.d.ts +11 -0
  3. package/dist/app-files.d.ts.map +1 -0
  4. package/dist/app-files.js +34 -0
  5. package/dist/app-files.js.map +1 -0
  6. package/dist/assure-hydration.d.ts +7 -1
  7. package/dist/assure-hydration.d.ts.map +1 -1
  8. package/dist/assure-hydration.js +128 -43
  9. package/dist/assure-hydration.js.map +1 -1
  10. package/dist/assure.d.ts.map +1 -1
  11. package/dist/assure.js +16 -2
  12. package/dist/assure.js.map +1 -1
  13. package/dist/capabilities-tool.d.ts +5 -1
  14. package/dist/capabilities-tool.d.ts.map +1 -1
  15. package/dist/capabilities-tool.js +32 -10
  16. package/dist/capabilities-tool.js.map +1 -1
  17. package/dist/cdn-check.d.ts +41 -0
  18. package/dist/cdn-check.d.ts.map +1 -0
  19. package/dist/cdn-check.js +207 -0
  20. package/dist/cdn-check.js.map +1 -0
  21. package/dist/check-diagnostics.d.ts +7 -15
  22. package/dist/check-diagnostics.d.ts.map +1 -1
  23. package/dist/check-diagnostics.js +162 -713
  24. package/dist/check-diagnostics.js.map +1 -1
  25. package/dist/check-scan.d.ts +58 -7
  26. package/dist/check-scan.d.ts.map +1 -1
  27. package/dist/check-scan.js +323 -62
  28. package/dist/check-scan.js.map +1 -1
  29. package/dist/check.d.ts +1 -0
  30. package/dist/check.d.ts.map +1 -1
  31. package/dist/check.js +145 -54
  32. package/dist/check.js.map +1 -1
  33. package/dist/cli.d.ts +42 -16
  34. package/dist/cli.d.ts.map +1 -1
  35. package/dist/cli.js +306 -87
  36. package/dist/cli.js.map +1 -1
  37. package/dist/command-catalog.d.ts +60 -5
  38. package/dist/command-catalog.d.ts.map +1 -1
  39. package/dist/command-catalog.js +298 -33
  40. package/dist/command-catalog.js.map +1 -1
  41. package/dist/config-target.d.ts +8 -0
  42. package/dist/config-target.d.ts.map +1 -0
  43. package/dist/config-target.js +45 -0
  44. package/dist/config-target.js.map +1 -0
  45. package/dist/contracts.d.ts +3 -2
  46. package/dist/contracts.d.ts.map +1 -1
  47. package/dist/contracts.js +56 -16
  48. package/dist/contracts.js.map +1 -1
  49. package/dist/db-audit.d.ts +37 -0
  50. package/dist/db-audit.d.ts.map +1 -0
  51. package/dist/db-audit.js +99 -0
  52. package/dist/db-audit.js.map +1 -0
  53. package/dist/db-child.d.ts +64 -0
  54. package/dist/db-child.d.ts.map +1 -0
  55. package/dist/db-child.js +213 -0
  56. package/dist/db-child.js.map +1 -0
  57. package/dist/db-config.d.ts +60 -0
  58. package/dist/db-config.d.ts.map +1 -0
  59. package/dist/db-config.js +176 -0
  60. package/dist/db-config.js.map +1 -0
  61. package/dist/db-tool.d.ts +84 -0
  62. package/dist/db-tool.d.ts.map +1 -0
  63. package/dist/db-tool.js +537 -0
  64. package/dist/db-tool.js.map +1 -0
  65. package/dist/dev-bun-config.d.ts +20 -13
  66. package/dist/dev-bun-config.d.ts.map +1 -1
  67. package/dist/dev-bun-config.js +98 -41
  68. package/dist/dev-bun-config.js.map +1 -1
  69. package/dist/dev-feed-tool.d.ts +106 -0
  70. package/dist/dev-feed-tool.d.ts.map +1 -0
  71. package/dist/dev-feed-tool.js +713 -0
  72. package/dist/dev-feed-tool.js.map +1 -0
  73. package/dist/dev-server-client.d.ts +67 -0
  74. package/dist/dev-server-client.d.ts.map +1 -0
  75. package/dist/dev-server-client.js +195 -0
  76. package/dist/dev-server-client.js.map +1 -0
  77. package/dist/diagnostics.d.ts +86 -0
  78. package/dist/diagnostics.d.ts.map +1 -1
  79. package/dist/diagnostics.js +85 -0
  80. package/dist/diagnostics.js.map +1 -1
  81. package/dist/diff-tool.d.ts +3 -3
  82. package/dist/diff-tool.d.ts.map +1 -1
  83. package/dist/diff-tool.js +6 -5
  84. package/dist/diff-tool.js.map +1 -1
  85. package/dist/docs-search.d.ts.map +1 -1
  86. package/dist/docs-search.js +12 -4
  87. package/dist/docs-search.js.map +1 -1
  88. package/dist/doctor.d.ts +17 -0
  89. package/dist/doctor.d.ts.map +1 -1
  90. package/dist/doctor.js +76 -15
  91. package/dist/doctor.js.map +1 -1
  92. package/dist/env-file.d.ts +15 -0
  93. package/dist/env-file.d.ts.map +1 -1
  94. package/dist/env-file.js +73 -1
  95. package/dist/env-file.js.map +1 -1
  96. package/dist/fix-recipes.d.ts.map +1 -1
  97. package/dist/fix-recipes.js +41 -21
  98. package/dist/fix-recipes.js.map +1 -1
  99. package/dist/frontend-guidance.js +5 -5
  100. package/dist/frontend-guidance.js.map +1 -1
  101. package/dist/i18n-check.d.ts +16 -0
  102. package/dist/i18n-check.d.ts.map +1 -0
  103. package/dist/i18n-check.js +154 -0
  104. package/dist/i18n-check.js.map +1 -0
  105. package/dist/init-agents.d.ts +49 -13
  106. package/dist/init-agents.d.ts.map +1 -1
  107. package/dist/init-agents.js +239 -34
  108. package/dist/init-agents.js.map +1 -1
  109. package/dist/internal/code-unit-order.d.ts +4 -0
  110. package/dist/internal/code-unit-order.d.ts.map +1 -0
  111. package/dist/internal/code-unit-order.js +6 -0
  112. package/dist/internal/code-unit-order.js.map +1 -0
  113. package/dist/internal/script-kind.d.ts +7 -0
  114. package/dist/internal/script-kind.d.ts.map +1 -0
  115. package/dist/internal/script-kind.js +12 -0
  116. package/dist/internal/script-kind.js.map +1 -0
  117. package/dist/internal/source-facts.d.ts +5 -0
  118. package/dist/internal/source-facts.d.ts.map +1 -1
  119. package/dist/internal/source-facts.js +61 -5
  120. package/dist/internal/source-facts.js.map +1 -1
  121. package/dist/internal/typescript-import.d.ts +49 -24
  122. package/dist/internal/typescript-import.d.ts.map +1 -1
  123. package/dist/internal/typescript-import.js +507 -67
  124. package/dist/internal/typescript-import.js.map +1 -1
  125. package/dist/introspect.d.ts +6 -4
  126. package/dist/introspect.d.ts.map +1 -1
  127. package/dist/introspect.js +93 -74
  128. package/dist/introspect.js.map +1 -1
  129. package/dist/learn.js +3 -3
  130. package/dist/learn.js.map +1 -1
  131. package/dist/levels-tool.d.ts +10 -0
  132. package/dist/levels-tool.d.ts.map +1 -1
  133. package/dist/levels-tool.js +33 -3
  134. package/dist/levels-tool.js.map +1 -1
  135. package/dist/load.d.ts +71 -16
  136. package/dist/load.d.ts.map +1 -1
  137. package/dist/load.js +78 -18
  138. package/dist/load.js.map +1 -1
  139. package/dist/manifest-tool.d.ts.map +1 -1
  140. package/dist/manifest-tool.js +3 -2
  141. package/dist/manifest-tool.js.map +1 -1
  142. package/dist/mcp-app-tool.d.ts.map +1 -1
  143. package/dist/mcp-app-tool.js +2 -0
  144. package/dist/mcp-app-tool.js.map +1 -1
  145. package/dist/mcp-context.d.ts.map +1 -1
  146. package/dist/mcp-context.js +5 -6
  147. package/dist/mcp-context.js.map +1 -1
  148. package/dist/mcp-delegate.d.ts +57 -0
  149. package/dist/mcp-delegate.d.ts.map +1 -0
  150. package/dist/mcp-delegate.js +130 -0
  151. package/dist/mcp-delegate.js.map +1 -0
  152. package/dist/mcp-docs-tools.d.ts.map +1 -1
  153. package/dist/mcp-docs-tools.js +23 -5
  154. package/dist/mcp-docs-tools.js.map +1 -1
  155. package/dist/mcp-exec.d.ts +3 -0
  156. package/dist/mcp-exec.d.ts.map +1 -1
  157. package/dist/mcp-exec.js +263 -131
  158. package/dist/mcp-exec.js.map +1 -1
  159. package/dist/mcp-http.d.ts.map +1 -1
  160. package/dist/mcp-http.js +20 -5
  161. package/dist/mcp-http.js.map +1 -1
  162. package/dist/mcp-io.d.ts +26 -0
  163. package/dist/mcp-io.d.ts.map +1 -1
  164. package/dist/mcp-io.js +113 -0
  165. package/dist/mcp-io.js.map +1 -1
  166. package/dist/mcp-isolate.d.ts +48 -0
  167. package/dist/mcp-isolate.d.ts.map +1 -0
  168. package/dist/mcp-isolate.js +222 -0
  169. package/dist/mcp-isolate.js.map +1 -0
  170. package/dist/mcp-path.d.ts +10 -0
  171. package/dist/mcp-path.d.ts.map +1 -0
  172. package/dist/mcp-path.js +51 -0
  173. package/dist/mcp-path.js.map +1 -0
  174. package/dist/mcp-project-child.d.ts +74 -0
  175. package/dist/mcp-project-child.d.ts.map +1 -0
  176. package/dist/mcp-project-child.js +130 -0
  177. package/dist/mcp-project-child.js.map +1 -0
  178. package/dist/mcp-reflect.d.ts.map +1 -1
  179. package/dist/mcp-reflect.js +2 -2
  180. package/dist/mcp-reflect.js.map +1 -1
  181. package/dist/mcp-render.d.ts.map +1 -1
  182. package/dist/mcp-render.js +64 -45
  183. package/dist/mcp-render.js.map +1 -1
  184. package/dist/mcp-root.d.ts +37 -9
  185. package/dist/mcp-root.d.ts.map +1 -1
  186. package/dist/mcp-root.js +124 -25
  187. package/dist/mcp-root.js.map +1 -1
  188. package/dist/mcp-run.d.ts +18 -1
  189. package/dist/mcp-run.d.ts.map +1 -1
  190. package/dist/mcp-run.js +159 -55
  191. package/dist/mcp-run.js.map +1 -1
  192. package/dist/mcp-ws.js +4 -1
  193. package/dist/mcp-ws.js.map +1 -1
  194. package/dist/mcp.d.ts +18 -5
  195. package/dist/mcp.d.ts.map +1 -1
  196. package/dist/mcp.js +165 -82
  197. package/dist/mcp.js.map +1 -1
  198. package/dist/migrate-layout.d.ts +44 -0
  199. package/dist/migrate-layout.d.ts.map +1 -0
  200. package/dist/migrate-layout.js +982 -0
  201. package/dist/migrate-layout.js.map +1 -0
  202. package/dist/openapi-tool.d.ts +2 -2
  203. package/dist/openapi-tool.d.ts.map +1 -1
  204. package/dist/openapi-tool.js.map +1 -1
  205. package/dist/openapi-types.d.ts +2 -1
  206. package/dist/openapi-types.d.ts.map +1 -1
  207. package/dist/openapi-types.js +240 -12
  208. package/dist/openapi-types.js.map +1 -1
  209. package/dist/pipeline-report.d.ts +3 -2
  210. package/dist/pipeline-report.d.ts.map +1 -1
  211. package/dist/pipeline-report.js +19 -16
  212. package/dist/pipeline-report.js.map +1 -1
  213. package/dist/port.d.ts +5 -4
  214. package/dist/port.d.ts.map +1 -1
  215. package/dist/port.js +46 -29
  216. package/dist/port.js.map +1 -1
  217. package/dist/project-facts.d.ts +28 -4
  218. package/dist/project-facts.d.ts.map +1 -1
  219. package/dist/project-facts.js +12 -2
  220. package/dist/project-facts.js.map +1 -1
  221. package/dist/recipes/4.0.0.d.ts +9 -0
  222. package/dist/recipes/4.0.0.d.ts.map +1 -0
  223. package/dist/recipes/4.0.0.js +34 -0
  224. package/dist/recipes/4.0.0.js.map +1 -0
  225. package/dist/recipes/index.d.ts.map +1 -1
  226. package/dist/recipes/index.js +2 -0
  227. package/dist/recipes/index.js.map +1 -1
  228. package/dist/release-verification.d.ts +11 -1
  229. package/dist/release-verification.d.ts.map +1 -1
  230. package/dist/release-verification.js +47 -4
  231. package/dist/release-verification.js.map +1 -1
  232. package/dist/review.d.ts +21 -0
  233. package/dist/review.d.ts.map +1 -0
  234. package/dist/review.js +823 -0
  235. package/dist/review.js.map +1 -0
  236. package/dist/route-types.d.ts +28 -0
  237. package/dist/route-types.d.ts.map +1 -0
  238. package/dist/route-types.js +106 -0
  239. package/dist/route-types.js.map +1 -0
  240. package/dist/rules/codes.d.ts +9 -0
  241. package/dist/rules/codes.d.ts.map +1 -1
  242. package/dist/rules/codes.js +9 -0
  243. package/dist/rules/codes.js.map +1 -1
  244. package/dist/rules/data-guard.d.ts +12 -0
  245. package/dist/rules/data-guard.d.ts.map +1 -0
  246. package/dist/rules/data-guard.js +213 -0
  247. package/dist/rules/data-guard.js.map +1 -0
  248. package/dist/rules/index.d.ts +3 -0
  249. package/dist/rules/index.d.ts.map +1 -1
  250. package/dist/rules/index.js +17 -5
  251. package/dist/rules/index.js.map +1 -1
  252. package/dist/rules/islands.d.ts.map +1 -1
  253. package/dist/rules/islands.js +7 -4
  254. package/dist/rules/islands.js.map +1 -1
  255. package/dist/rules/legacy.d.ts +3 -1
  256. package/dist/rules/legacy.d.ts.map +1 -1
  257. package/dist/rules/legacy.js +829 -34
  258. package/dist/rules/legacy.js.map +1 -1
  259. package/dist/rules/nano.d.ts.map +1 -1
  260. package/dist/rules/nano.js +15 -6
  261. package/dist/rules/nano.js.map +1 -1
  262. package/dist/rules/pages.d.ts +15 -0
  263. package/dist/rules/pages.d.ts.map +1 -0
  264. package/dist/rules/pages.js +88 -0
  265. package/dist/rules/pages.js.map +1 -0
  266. package/dist/rules/routes.d.ts +11 -0
  267. package/dist/rules/routes.d.ts.map +1 -1
  268. package/dist/rules/routes.js +180 -2
  269. package/dist/rules/routes.js.map +1 -1
  270. package/dist/rules/secrets.d.ts +10 -0
  271. package/dist/rules/secrets.d.ts.map +1 -0
  272. package/dist/rules/secrets.js +169 -0
  273. package/dist/rules/secrets.js.map +1 -0
  274. package/dist/rules/security.d.ts +7 -0
  275. package/dist/rules/security.d.ts.map +1 -1
  276. package/dist/rules/security.js +171 -65
  277. package/dist/rules/security.js.map +1 -1
  278. package/dist/rules/typescript.d.ts +16 -0
  279. package/dist/rules/typescript.d.ts.map +1 -0
  280. package/dist/rules/typescript.js +59 -0
  281. package/dist/rules/typescript.js.map +1 -0
  282. package/dist/rules/zones.d.ts +17 -0
  283. package/dist/rules/zones.d.ts.map +1 -0
  284. package/dist/rules/zones.js +174 -0
  285. package/dist/rules/zones.js.map +1 -0
  286. package/dist/scaffold.d.ts +25 -12
  287. package/dist/scaffold.d.ts.map +1 -1
  288. package/dist/scaffold.js +154 -90
  289. package/dist/scaffold.js.map +1 -1
  290. package/dist/sdk.d.ts +17 -3
  291. package/dist/sdk.d.ts.map +1 -1
  292. package/dist/sdk.js +941 -153
  293. package/dist/sdk.js.map +1 -1
  294. package/dist/search-terms.d.ts +2 -0
  295. package/dist/search-terms.d.ts.map +1 -1
  296. package/dist/search-terms.js +23 -8
  297. package/dist/search-terms.js.map +1 -1
  298. package/dist/smoke.d.ts +79 -0
  299. package/dist/smoke.d.ts.map +1 -0
  300. package/dist/smoke.js +494 -0
  301. package/dist/smoke.js.map +1 -0
  302. package/dist/sync-manifest.d.ts.map +1 -1
  303. package/dist/sync-manifest.js +2 -1
  304. package/dist/sync-manifest.js.map +1 -1
  305. package/dist/sync-routes.d.ts +4 -3
  306. package/dist/sync-routes.d.ts.map +1 -1
  307. package/dist/sync-routes.js +4 -3
  308. package/dist/sync-routes.js.map +1 -1
  309. package/dist/test-tool.d.ts.map +1 -1
  310. package/dist/test-tool.js +30 -11
  311. package/dist/test-tool.js.map +1 -1
  312. package/dist/types-search.d.ts.map +1 -1
  313. package/dist/types-search.js +11 -5
  314. package/dist/types-search.js.map +1 -1
  315. package/dist/upgrade.d.ts +36 -6
  316. package/dist/upgrade.d.ts.map +1 -1
  317. package/dist/upgrade.js +167 -29
  318. package/dist/upgrade.js.map +1 -1
  319. package/dist/verification-plan.d.ts.map +1 -1
  320. package/dist/verification-plan.js +8 -0
  321. package/dist/verification-plan.js.map +1 -1
  322. package/dist/verification.d.ts.map +1 -1
  323. package/dist/verification.js +45 -12
  324. package/dist/verification.js.map +1 -1
  325. package/dist/web-app-options.d.ts +19 -0
  326. package/dist/web-app-options.d.ts.map +1 -0
  327. package/dist/web-app-options.js +49 -0
  328. package/dist/web-app-options.js.map +1 -0
  329. package/dist/work-graph.d.ts +3 -0
  330. package/dist/work-graph.d.ts.map +1 -1
  331. package/dist/work-graph.js +33 -16
  332. package/dist/work-graph.js.map +1 -1
  333. package/docs/examples.json +185 -29
  334. package/docs/llms-full.txt +4123 -548
  335. package/docs/types.json +14391 -10060
  336. package/package.json +19 -11
  337. package/src/app-files.ts +40 -0
  338. package/src/assure-hydration.ts +139 -46
  339. package/src/assure.ts +17 -2
  340. package/src/capabilities-tool.ts +35 -9
  341. package/src/cdn-check.ts +268 -0
  342. package/src/check-diagnostics.ts +199 -797
  343. package/src/check-scan.ts +394 -63
  344. package/src/check.ts +146 -58
  345. package/src/cli.ts +347 -92
  346. package/src/command-catalog.ts +407 -45
  347. package/src/config-target.ts +47 -0
  348. package/src/contracts.ts +66 -15
  349. package/src/db-audit.ts +128 -0
  350. package/src/db-child.ts +285 -0
  351. package/src/db-config.ts +224 -0
  352. package/src/db-tool.ts +690 -0
  353. package/src/dev-bun-config.ts +116 -40
  354. package/src/dev-feed-tool.ts +912 -0
  355. package/src/dev-server-client.ts +267 -0
  356. package/src/diagnostics.ts +179 -0
  357. package/src/diff-tool.ts +6 -5
  358. package/src/docs-search.ts +10 -3
  359. package/src/doctor.ts +98 -15
  360. package/src/env-file.ts +78 -1
  361. package/src/fix-recipes.ts +44 -31
  362. package/src/frontend-guidance.ts +5 -5
  363. package/src/i18n-check.ts +179 -0
  364. package/src/init-agents.ts +301 -38
  365. package/src/internal/code-unit-order.ts +5 -0
  366. package/src/internal/script-kind.ts +11 -0
  367. package/src/internal/source-facts.ts +96 -22
  368. package/src/internal/typescript-import.ts +663 -68
  369. package/src/introspect.ts +93 -82
  370. package/src/learn.ts +3 -3
  371. package/src/levels-tool.ts +47 -2
  372. package/src/load.ts +144 -32
  373. package/src/manifest-tool.ts +3 -2
  374. package/src/mcp-app-tool.ts +2 -0
  375. package/src/mcp-context.ts +5 -7
  376. package/src/mcp-delegate.ts +162 -0
  377. package/src/mcp-docs-tools.ts +23 -5
  378. package/src/mcp-exec.ts +301 -161
  379. package/src/mcp-http.ts +22 -5
  380. package/src/mcp-io.ts +130 -0
  381. package/src/mcp-isolate.ts +287 -0
  382. package/src/mcp-path.ts +50 -0
  383. package/src/mcp-project-child.ts +182 -0
  384. package/src/mcp-reflect.ts +3 -2
  385. package/src/mcp-render.ts +72 -42
  386. package/src/mcp-root.ts +130 -24
  387. package/src/mcp-run.ts +190 -56
  388. package/src/mcp-ws.ts +4 -1
  389. package/src/mcp.ts +193 -91
  390. package/src/migrate-layout.ts +1121 -0
  391. package/src/openapi-tool.ts +7 -3
  392. package/src/openapi-types.ts +315 -13
  393. package/src/pipeline-report.ts +22 -18
  394. package/src/port.ts +53 -30
  395. package/src/project-facts.ts +42 -8
  396. package/src/recipes/4.0.0.ts +35 -0
  397. package/src/recipes/index.ts +2 -0
  398. package/src/release-verification.ts +74 -5
  399. package/src/review.ts +988 -0
  400. package/src/route-types.ts +133 -0
  401. package/src/rules/codes.ts +9 -0
  402. package/src/rules/data-guard.ts +230 -0
  403. package/src/rules/index.ts +18 -5
  404. package/src/rules/islands.ts +12 -4
  405. package/src/rules/legacy.ts +947 -35
  406. package/src/rules/nano.ts +18 -6
  407. package/src/rules/pages.ts +99 -0
  408. package/src/rules/routes.ts +181 -2
  409. package/src/rules/secrets.ts +174 -0
  410. package/src/rules/security.ts +198 -70
  411. package/src/rules/typescript.ts +77 -0
  412. package/src/rules/zones.ts +195 -0
  413. package/src/scaffold.ts +175 -98
  414. package/src/sdk.ts +1215 -158
  415. package/src/search-terms.ts +24 -7
  416. package/src/smoke.ts +607 -0
  417. package/src/sync-manifest.ts +2 -1
  418. package/src/sync-routes.ts +4 -3
  419. package/src/test-tool.ts +32 -11
  420. package/src/types-search.ts +11 -4
  421. package/src/upgrade.ts +205 -25
  422. package/src/verification-plan.ts +27 -0
  423. package/src/verification.ts +50 -12
  424. package/src/web-app-options.ts +57 -0
  425. package/src/work-graph.ts +40 -16
package/src/mcp.ts CHANGED
@@ -20,19 +20,32 @@
20
20
  * - `nifra_levels` - the cumulative verification ladder (L0 contract → L4 invariants): what the
21
21
  * project actually proves, and why each level it misses does not hold.
22
22
  * - `nifra_doctor` - package.json dependency drift detector, with safe local-version auto-fix.
23
- * - `nifra_explain` - resolve an error (pasted, or the dev server's last) into a structured
23
+ * - `nifra_errors` - the running dev server's errors (SSR, loader, API, build, browser, hydration,
24
+ * crash), each a structured diagnostic tagged with its request; `since` cursors, stale flags.
25
+ * - `nifra_logs` - the dev server's console output, server and browser, tagged by request.
26
+ * - `nifra_db_schema` / `nifra_db_query` / `nifra_db_role` - the development database declared as
27
+ * `devDatabase` in nifra.config.ts: its schema, one read-only query (or plan) per call in a fresh
28
+ * subprocess killed at the deadline, and the SQL for a read-only role. Separate tools, so a client
29
+ * can allow reading the schema without allowing queries.
30
+ * - `nifra_explain` - resolve an error (pasted, or the dev server's latest) into a structured
24
31
  * diagnostic: stable code, a codeframe in the user's source, and the recognised cause + fix.
25
32
  * - `nifra_inspect` - read the running dev server's recent request traces (method/path/status/
26
- * duration/ISR) from the DevTools plugin: what your requests ACTUALLY did, not a guess.
33
+ * duration/ISR/errors): what your requests ACTUALLY did, not a guess.
27
34
  *
28
35
  * Wire it into a client (e.g. Claude Desktop / Cursor) as: command `nifra`, args `["mcp"]` (or
29
36
  * `["mcp", "<dir>"]` to pin the project directory explicitly). The server does NOT silently trust its
30
37
  * spawn directory: the root is resolved via `./mcp-root.ts` (marker walk-up, the client's MCP `roots`,
31
38
  * fail-closed tools when no nifra project is found) and announced in `initialize` + on every project
32
- * tool result. The protocol is hand-rolled (newline-delimited JSON-RPC 2.0 over stdio), including
39
+ * tool result. A project that installs its own `@nifrajs/cli` at a different version gets the session
40
+ * handed to that CLI; when that is impossible the version-sensitive tools refuse rather than answer
41
+ * for the wrong release (see `./mcp-delegate.ts`). The protocol is hand-rolled (newline-delimited JSON-RPC 2.0 over stdio), including
33
42
  * standard MCP progress notifications and request cancellation - no SDK dependency, the same
34
43
  * minimal-surface choice as the rest of nifra. The pure dispatch lives in `./mcp-protocol.ts`; this
35
44
  * module is the I/O shell (stdin loop, tool wiring, the run subprocess).
45
+ *
46
+ * This process runs no project code and holds none of the project's `.env`: the config, the backend
47
+ * and everything they import run in a fresh subprocess per call (`./mcp-isolate.ts`), and a session
48
+ * whose process Bun started with `.env` values moves to a copy that never loads them (`./cli.ts`).
36
49
  */
37
50
 
38
51
  import { stat } from "node:fs/promises"
@@ -40,11 +53,14 @@ import { resolve } from "node:path"
40
53
  import { loadDocsCorpus } from "./docs-search.ts"
41
54
  import { loadExamplesCorpus } from "./examples.ts"
42
55
  import type { LoadedApp } from "./load.ts"
43
- import { detectMonorepo, loadMonorepoApps } from "./load.ts"
56
+ import { loadMonorepoApps } from "./load.ts"
57
+ import { delegateToProjectCli, refuseVersionSensitive } from "./mcp-delegate.ts"
44
58
  import { docsTools } from "./mcp-docs-tools.ts"
45
59
  import {
46
60
  createMcpProtocolState,
47
61
  handleRpc,
62
+ isJsonRpcRequest,
63
+ isJsonRpcResponse,
48
64
  type JsonRpcNotification,
49
65
  type JsonRpcRequest,
50
66
  type JsonRpcResponse,
@@ -54,11 +70,6 @@ import {
54
70
  type McpTool,
55
71
  rpcError,
56
72
  } from "./mcp-protocol.ts"
57
- import {
58
- extractBackendPrompts,
59
- extractBackendResources,
60
- extractBackendTools,
61
- } from "./mcp-reflect.ts"
62
73
  import { loadTypesCorpus } from "./types-search.ts"
63
74
 
64
75
  export {
@@ -80,21 +91,31 @@ import {
80
91
  type ToolingDrift,
81
92
  } from "./mcp-root.ts"
82
93
 
83
- export type { BoundedOutput } from "./mcp-io.ts"
94
+ export type { BoundedLine, BoundedOutput } from "./mcp-io.ts"
84
95
  export {
96
+ CHILD_INPUT_MAX_BYTES,
85
97
  CHILD_OUTPUT_MAX_BYTES,
86
98
  CHILD_TIMEOUT_MS,
87
99
  LOCAL_TOOL_FETCH_TIMEOUT_MS,
88
100
  LOCAL_TOOL_MAX_RESPONSE_BYTES,
89
101
  notNifraResponse,
102
+ readBoundedLines,
90
103
  readBoundedResponse,
91
104
  readBoundedStream,
105
+ serializeBoundedJson,
92
106
  timeoutMessage,
93
107
  validateLocalPort,
94
108
  } from "./mcp-io.ts"
95
109
  export { clientSupportsRoots, guardTools } from "./mcp-root.ts"
96
110
 
97
- import { createCachedAppLoader, projectTools } from "./mcp-exec.ts"
111
+ import { projectTools } from "./mcp-exec.ts"
112
+ import {
113
+ createAppSurface,
114
+ detectMonorepoIsolated,
115
+ type IsolatedSurface,
116
+ isolateResources,
117
+ isolateTools,
118
+ } from "./mcp-isolate.ts"
98
119
 
99
120
  export type { CachedAppLoaderOptions } from "./mcp-exec.ts"
100
121
  export {
@@ -110,7 +131,7 @@ export {
110
131
  wsHandler,
111
132
  } from "./mcp-exec.ts"
112
133
 
113
- import { namespaceForApp, projectFeatures } from "./mcp-context.ts"
134
+ import { namespaceForApp, projectPrompts, projectResources } from "./mcp-context.ts"
114
135
 
115
136
  export type { CommandMcpToolOptions } from "./mcp-context.ts"
116
137
  export {
@@ -122,76 +143,82 @@ export {
122
143
  resolveProjectDir,
123
144
  } from "./mcp-context.ts"
124
145
 
125
- async function backendFeatures(
126
- loader: () => Promise<LoadedApp>,
127
- base: McpServerFeatures,
128
- ): Promise<McpServerFeatures> {
129
- const resources = [...(base.resources ?? [])]
130
- const prompts = [...(base.prompts ?? [])]
131
- try {
132
- const app = await loader()
133
- resources.push(...extractBackendResources(app.backend))
134
- prompts.push(...extractBackendPrompts(app.backend))
135
- } catch {
136
- // Not loadable here (no web config, or the config itself throws). The server still serves.
137
- }
138
- return { resources, prompts }
139
- }
146
+ /**
147
+ * The loader the server builds its tool and resource lists with. It never loads: every handler that
148
+ * reaches the app runs in a project subprocess (`./mcp-isolate.ts`), so one that tried here fails.
149
+ */
150
+ const noAppInServer = (): Promise<LoadedApp> =>
151
+ Promise.reject(
152
+ new Error("[nifra] the MCP server does not load the app; project code runs in a subprocess"),
153
+ )
154
+
155
+ const isolatedToolsByDir = new Map<string, McpTool[]>()
140
156
 
141
157
  /**
142
- * The MCP tools the app itself declares via `app.tool(...)`, or none when the app cannot be loaded.
143
- *
144
- * Same reasoning as {@link backendFeatures}, on the hotter path: this ran on EVERY JSON-RPC message,
145
- * so an unloadable project failed `initialize` itself with a `-32603` and the session never opened.
146
- * An app's own tools are an extension of the built-in set, so their absence must not withdraw the
147
- * built-ins - a backend-only project still gets docs, examples, types, check, doctor, levels and test.
158
+ * The project's tools, each forwarded to a project subprocess unless it runs no project code. Built
159
+ * once per directory: `nifra_run`/`nifra_render` hold their warm worker in the tool, so a list rebuilt
160
+ * per message started a fresh worker for every `warm: true` call.
148
161
  */
149
- async function appDeclaredTools(loader: () => Promise<LoadedApp>): Promise<McpTool[]> {
150
- try {
151
- return extractBackendTools((await loader()).backend)
152
- } catch {
153
- return []
162
+ function isolatedProjectTools(cwd: string): McpTool[] {
163
+ let tools = isolatedToolsByDir.get(cwd)
164
+ if (tools === undefined) {
165
+ tools = isolateTools(cwd, projectTools(cwd, noAppInServer))
166
+ isolatedToolsByDir.set(cwd, tools)
167
+ }
168
+ return tools
169
+ }
170
+
171
+ /** The project's resources and prompts, with the ones the app declares. */
172
+ function appFeatures(cwd: string, surface: IsolatedSurface): McpServerFeatures {
173
+ return {
174
+ resources: [
175
+ ...isolateResources(cwd, projectResources(cwd, noAppInServer)),
176
+ ...surface.resources,
177
+ ],
178
+ prompts: [...projectPrompts(), ...surface.prompts],
154
179
  }
155
180
  }
156
181
 
157
182
  const ROOTS_REQUEST_ID = "nifra:roots/list"
183
+ const MAX_STDIO_MESSAGE_BYTES = 8 * 1024 * 1024
184
+ const STDIO_ENCODER = new TextEncoder()
185
+
186
+ interface ProjectApp {
187
+ readonly name: string
188
+ readonly cwd: string
189
+ readonly surface: () => Promise<IsolatedSurface>
190
+ }
158
191
 
159
192
  /** Everything derived from the project root - rebuilt wholesale when the root changes (adoption of a
160
193
  * client workspace root), so no per-tool state can keep pointing at the old directory. */
161
194
  interface ProjectContext {
162
- readonly monorepo: Awaited<ReturnType<typeof detectMonorepo>>
195
+ /** A monorepo root's apps; `undefined` when the root is one app. */
196
+ readonly apps: readonly ProjectApp[] | undefined
163
197
  readonly features: McpServerFeatures
164
- readonly loadAppCached: () => Promise<LoadedApp>
198
+ readonly surface: () => Promise<IsolatedSurface>
165
199
  }
166
200
 
167
201
  async function createProjectContext(root: string): Promise<ProjectContext> {
168
- const monorepo = await detectMonorepo(root)
202
+ const monorepo = await detectMonorepoIsolated(root)
169
203
  if (monorepo) {
170
- const appEntries = await loadMonorepoApps(root, monorepo)
204
+ const apps = (await loadMonorepoApps(root, monorepo)).map(
205
+ ({ name, cwd }): ProjectApp => ({ name, cwd, surface: createAppSurface(cwd) }),
206
+ )
171
207
  const allResources: McpResource[] = []
172
208
  const allPrompts: McpPrompt[] = []
173
- for (const { name, cwd: appCwd } of appEntries) {
174
- const loader = createCachedAppLoader(appCwd)
175
- const ns = namespaceForApp(
176
- name,
177
- [],
178
- await backendFeatures(loader, projectFeatures(appCwd, loader)),
179
- )
209
+ for (const { name, cwd: appCwd, surface } of apps) {
210
+ const ns = namespaceForApp(name, [], appFeatures(appCwd, await surface()))
180
211
  allResources.push(...(ns.features.resources ?? []))
181
212
  allPrompts.push(...(ns.features.prompts ?? []))
182
213
  }
183
214
  return {
184
- monorepo,
215
+ apps,
185
216
  features: { resources: allResources, prompts: allPrompts },
186
- loadAppCached: createCachedAppLoader(root),
217
+ surface: createAppSurface(root),
187
218
  }
188
219
  }
189
- const loadAppCached = createCachedAppLoader(root)
190
- return {
191
- monorepo,
192
- features: await backendFeatures(loadAppCached, projectFeatures(root, loadAppCached)),
193
- loadAppCached,
194
- }
220
+ const surface = createAppSurface(root)
221
+ return { apps: undefined, features: appFeatures(root, await surface()), surface }
195
222
  }
196
223
 
197
224
  /**
@@ -210,22 +237,49 @@ export async function runMcpServer(
210
237
  throw new Error(`nifra mcp: directory not found: ${requested}`)
211
238
  }
212
239
  let rootState = await resolveRootState(requested, explicitDir !== undefined)
240
+ if (rootState.isProject) {
241
+ // Before anything reads stdin: a hand-off child inherits the descriptor and owns the session.
242
+ const exitCode = await delegateToProjectCli({
243
+ cwd,
244
+ root: rootState.root,
245
+ version,
246
+ args: explicitDir === undefined ? [] : [requested],
247
+ })
248
+ if (exitCode !== undefined) {
249
+ process.exitCode = exitCode
250
+ return
251
+ }
252
+ }
213
253
  let ctx = await createProjectContext(rootState.root)
214
254
  let drift: ToolingDrift | undefined = await detectToolingDrift(rootState.root, version)
215
255
  const serverInfo = { name: "nifra", version }
216
256
  const state = createMcpProtocolState()
217
257
  const send = (message: JsonRpcResponse | JsonRpcNotification): void => {
218
- process.stdout.write(`${JSON.stringify(message)}\n`)
258
+ let encoded: string
259
+ try {
260
+ encoded = JSON.stringify(message)
261
+ } catch {
262
+ encoded = JSON.stringify(
263
+ rpcError(responseIdOf(message), -32603, "MCP response is not serializable"),
264
+ )
265
+ }
266
+ if (STDIO_ENCODER.encode(encoded).byteLength > MAX_STDIO_MESSAGE_BYTES)
267
+ encoded = JSON.stringify(
268
+ rpcError(responseIdOf(message), -32603, "MCP response exceeded the size limit"),
269
+ )
270
+ process.stdout.write(`${encoded}\n`)
219
271
  }
220
272
  let rootsSupported = false
273
+ let rootsRequestPending = false
221
274
  const requestRoots = (): void => {
222
- if (!rootsSupported) return
275
+ if (!rootsSupported || rootsRequestPending) return
276
+ rootsRequestPending = true
223
277
  process.stdout.write(
224
278
  `${JSON.stringify({ jsonrpc: "2.0", id: ROOTS_REQUEST_ID, method: "roots/list" })}\n`,
225
279
  )
226
280
  }
227
- const onRootsAnswer = async (message: JsonRpcRequest): Promise<void> => {
228
- const result = (message as { result?: unknown }).result
281
+ const onRootsAnswer = async (message: JsonRpcResponse): Promise<void> => {
282
+ const result = "result" in message ? message.result : undefined
229
283
  // An error response (or a malformed one) carries no roots: keep the current state - the absence
230
284
  // of workspace data is not a mismatch, and never a reason to move the root.
231
285
  if (result === undefined) return
@@ -241,35 +295,42 @@ export async function runMcpServer(
241
295
  // must see the adopted root, even though dispatches themselves run concurrently.
242
296
  let rootsUpdate: Promise<void> = Promise.resolve()
243
297
 
298
+ const dispatchRootsAnswer = (message: JsonRpcResponse): void => {
299
+ // The roots answer is a response to a request initiated by this server, not a client RPC request.
300
+ // Serialize adoption with normal dispatch so a following request cannot observe a half-updated root.
301
+ if (!rootsRequestPending) return
302
+ rootsRequestPending = false
303
+ rootsUpdate = onRootsAnswer(message).catch(() => {})
304
+ }
305
+
244
306
  const dispatch = async (message: JsonRpcRequest): Promise<void> => {
245
- // The client's answer to OUR `roots/list` request - a response (id, no method), which the pure
246
- // dispatch would reject as an unknown method. Intercept it before handleRpc ever sees it.
247
- if (message.method === undefined && message.id === ROOTS_REQUEST_ID) {
248
- // Swallow failures: a bad answer must not poison the chain and 500 every later dispatch.
249
- rootsUpdate = onRootsAnswer(message).catch(() => {})
250
- await rootsUpdate
251
- return
252
- }
253
307
  await rootsUpdate
254
308
  if (message.method === "initialize") {
255
309
  rootsSupported = clientSupportsRoots(message.params)
256
310
  }
257
311
 
258
312
  let activeTools: McpTool[]
259
- if (ctx.monorepo) {
260
- const appEntries = await loadMonorepoApps(rootState.root, ctx.monorepo)
313
+ if (ctx.apps) {
261
314
  const allTools: McpTool[] = []
262
- for (const { name, cwd: appCwd } of appEntries) {
263
- const loader = createCachedAppLoader(appCwd)
264
- const tools = [...projectTools(appCwd, loader), ...(await appDeclaredTools(loader))]
315
+ for (const { name, cwd: appCwd, surface } of ctx.apps) {
316
+ const tools = [
317
+ ...refuseVersionSensitive(isolatedProjectTools(appCwd), drift),
318
+ ...(await surface()).tools,
319
+ ]
265
320
  const ns = namespaceForApp(name, tools, { resources: [], prompts: [] })
266
321
  allTools.push(...ns.tools)
267
322
  }
268
- activeTools = [...docsTools(loadDocsCorpus, loadExamplesCorpus, loadTypesCorpus), ...allTools]
323
+ activeTools = [
324
+ ...refuseVersionSensitive(
325
+ docsTools(loadDocsCorpus, loadExamplesCorpus, loadTypesCorpus),
326
+ drift,
327
+ ),
328
+ ...allTools,
329
+ ]
269
330
  } else {
270
331
  activeTools = [
271
- ...projectTools(rootState.root, ctx.loadAppCached),
272
- ...(await appDeclaredTools(ctx.loadAppCached)),
332
+ ...refuseVersionSensitive(isolatedProjectTools(rootState.root), drift),
333
+ ...(await ctx.surface()).tools,
273
334
  ]
274
335
  }
275
336
  const verdict = await rootVerdict(rootState)
@@ -287,6 +348,8 @@ export async function runMcpServer(
287
348
  const response = await handleRpc(message, activeTools, serverInfo, features, {
288
349
  state,
289
350
  sendNotification: send,
351
+ // The stdio caller is the developer's own agent: a failure it cannot read it cannot fix.
352
+ exposeToolErrors: true,
290
353
  })
291
354
  if (response) send(response)
292
355
  // Ask for the client's workspace roots once the handshake completes, and again whenever the
@@ -301,25 +364,64 @@ export async function runMcpServer(
301
364
 
302
365
  const decoder = new TextDecoder()
303
366
  let buffer = ""
367
+ let bufferBytes = 0
368
+ let discardingOversized = false
369
+ const processLine = (rawLine: string): void => {
370
+ const line = rawLine.trim()
371
+ if (line === "") return
372
+ if (STDIO_ENCODER.encode(rawLine).byteLength > MAX_STDIO_MESSAGE_BYTES) {
373
+ send(rpcError(null, -32600, "MCP message exceeded the size limit"))
374
+ return
375
+ }
376
+ let parsed: unknown
377
+ try {
378
+ parsed = JSON.parse(line) as unknown
379
+ } catch {
380
+ send(rpcError(null, -32700, "parse error"))
381
+ return
382
+ }
383
+ if (isJsonRpcRequest(parsed)) {
384
+ void dispatch(parsed).catch((err) => {
385
+ const msg = err instanceof Error ? err.message : String(err)
386
+ send(rpcError(parsed.id ?? null, -32603, msg))
387
+ })
388
+ } else if (isJsonRpcResponse(parsed) && parsed.id === ROOTS_REQUEST_ID) {
389
+ dispatchRootsAnswer(parsed)
390
+ } else {
391
+ // Do not let null, arrays, scalars, responses for unknown ids, or malformed envelopes reach the
392
+ // dispatch shell. In particular, reading `.id` from a hostile `null` must never crash the loop.
393
+ send(rpcError(null, -32600, "Invalid Request"))
394
+ }
395
+ }
304
396
  for await (const chunk of Bun.stdin.stream()) {
305
- buffer += decoder.decode(chunk as Uint8Array, { stream: true })
397
+ let text = decoder.decode(chunk as Uint8Array, { stream: true })
398
+ if (discardingOversized) {
399
+ const newline = text.indexOf("\n")
400
+ if (newline === -1) continue
401
+ text = text.slice(newline + 1)
402
+ discardingOversized = false
403
+ }
404
+ buffer += text
405
+ bufferBytes += STDIO_ENCODER.encode(text).byteLength
306
406
  let nl = buffer.indexOf("\n")
307
407
  while (nl !== -1) {
308
- const line = buffer.slice(0, nl).trim()
408
+ const line = buffer.slice(0, nl)
309
409
  buffer = buffer.slice(nl + 1)
410
+ bufferBytes = Math.max(0, bufferBytes - STDIO_ENCODER.encode(line).byteLength - 1)
310
411
  nl = buffer.indexOf("\n")
311
- if (line === "") continue
312
- let message: JsonRpcRequest
313
- try {
314
- message = JSON.parse(line)
315
- } catch {
316
- send(rpcError(null, -32700, "parse error"))
317
- continue
318
- }
319
- void dispatch(message).catch((err) => {
320
- const msg = err instanceof Error ? err.message : String(err)
321
- send(rpcError(message.id ?? null, -32603, msg))
322
- })
412
+ processLine(line)
413
+ }
414
+ if (bufferBytes > MAX_STDIO_MESSAGE_BYTES) {
415
+ buffer = ""
416
+ bufferBytes = 0
417
+ discardingOversized = true
418
+ send(rpcError(null, -32600, "MCP message exceeded the size limit"))
323
419
  }
324
420
  }
325
421
  }
422
+
423
+ function responseIdOf(message: JsonRpcResponse | JsonRpcNotification): string | number | null {
424
+ if (!("id" in message)) return null
425
+ const id = message.id
426
+ return id === null || typeof id === "string" || typeof id === "number" ? id : null
427
+ }