@nac3/forge-cli 0.2.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (561) hide show
  1. package/LICENSE +45 -0
  2. package/README.md +371 -0
  3. package/dist/bin/yf.d.ts +5 -0
  4. package/dist/bin/yf.d.ts.map +1 -0
  5. package/dist/bin/yf.js +86 -0
  6. package/dist/bin/yf.js.map +1 -0
  7. package/dist/chat/claude.d.ts +100 -0
  8. package/dist/chat/claude.d.ts.map +1 -0
  9. package/dist/chat/claude.js +228 -0
  10. package/dist/chat/claude.js.map +1 -0
  11. package/dist/chat/ingest_session.d.ts +97 -0
  12. package/dist/chat/ingest_session.d.ts.map +1 -0
  13. package/dist/chat/ingest_session.js +99 -0
  14. package/dist/chat/ingest_session.js.map +1 -0
  15. package/dist/chat/panel.d.ts +15 -0
  16. package/dist/chat/panel.d.ts.map +1 -0
  17. package/dist/chat/panel.js +1526 -0
  18. package/dist/chat/panel.js.map +1 -0
  19. package/dist/chat/persistence.d.ts +37 -0
  20. package/dist/chat/persistence.d.ts.map +1 -0
  21. package/dist/chat/persistence.js +91 -0
  22. package/dist/chat/persistence.js.map +1 -0
  23. package/dist/chat/server.d.ts +34 -0
  24. package/dist/chat/server.d.ts.map +1 -0
  25. package/dist/chat/server.js +1540 -0
  26. package/dist/chat/server.js.map +1 -0
  27. package/dist/chat/spec_extract.d.ts +35 -0
  28. package/dist/chat/spec_extract.d.ts.map +1 -0
  29. package/dist/chat/spec_extract.js +152 -0
  30. package/dist/chat/spec_extract.js.map +1 -0
  31. package/dist/chat/spec_plan.d.ts +65 -0
  32. package/dist/chat/spec_plan.d.ts.map +1 -0
  33. package/dist/chat/spec_plan.js +160 -0
  34. package/dist/chat/spec_plan.js.map +1 -0
  35. package/dist/chat/spec_scaffold.d.ts +95 -0
  36. package/dist/chat/spec_scaffold.d.ts.map +1 -0
  37. package/dist/chat/spec_scaffold.js +220 -0
  38. package/dist/chat/spec_scaffold.js.map +1 -0
  39. package/dist/chat/tools/git.d.ts +59 -0
  40. package/dist/chat/tools/git.d.ts.map +1 -0
  41. package/dist/chat/tools/git.js +313 -0
  42. package/dist/chat/tools/git.js.map +1 -0
  43. package/dist/chat/tools/github.d.ts +59 -0
  44. package/dist/chat/tools/github.d.ts.map +1 -0
  45. package/dist/chat/tools/github.js +310 -0
  46. package/dist/chat/tools/github.js.map +1 -0
  47. package/dist/chat/tools/lifecycle.d.ts +82 -0
  48. package/dist/chat/tools/lifecycle.d.ts.map +1 -0
  49. package/dist/chat/tools/lifecycle.js +295 -0
  50. package/dist/chat/tools/lifecycle.js.map +1 -0
  51. package/dist/chat/tools/manual.d.ts +26 -0
  52. package/dist/chat/tools/manual.d.ts.map +1 -0
  53. package/dist/chat/tools/manual.js +164 -0
  54. package/dist/chat/tools/manual.js.map +1 -0
  55. package/dist/chat/tools/reader.d.ts +80 -0
  56. package/dist/chat/tools/reader.d.ts.map +1 -0
  57. package/dist/chat/tools/reader.js +471 -0
  58. package/dist/chat/tools/reader.js.map +1 -0
  59. package/dist/chat/tools.d.ts +106 -0
  60. package/dist/chat/tools.d.ts.map +1 -0
  61. package/dist/chat/tools.js +587 -0
  62. package/dist/chat/tools.js.map +1 -0
  63. package/dist/codegen/e2e.d.ts +106 -0
  64. package/dist/codegen/e2e.d.ts.map +1 -0
  65. package/dist/codegen/e2e.js +931 -0
  66. package/dist/codegen/e2e.js.map +1 -0
  67. package/dist/codegen/v3_flow_emit.d.ts +70 -0
  68. package/dist/codegen/v3_flow_emit.d.ts.map +1 -0
  69. package/dist/codegen/v3_flow_emit.js +225 -0
  70. package/dist/codegen/v3_flow_emit.js.map +1 -0
  71. package/dist/commands/_stub.d.ts +2 -0
  72. package/dist/commands/_stub.d.ts.map +1 -0
  73. package/dist/commands/_stub.js +21 -0
  74. package/dist/commands/_stub.js.map +1 -0
  75. package/dist/commands/app.d.ts +31 -0
  76. package/dist/commands/app.d.ts.map +1 -0
  77. package/dist/commands/app.js +331 -0
  78. package/dist/commands/app.js.map +1 -0
  79. package/dist/commands/chat.d.ts +18 -0
  80. package/dist/commands/chat.d.ts.map +1 -0
  81. package/dist/commands/chat.js +76 -0
  82. package/dist/commands/chat.js.map +1 -0
  83. package/dist/commands/deploy.d.ts +21 -0
  84. package/dist/commands/deploy.d.ts.map +1 -0
  85. package/dist/commands/deploy.js +121 -0
  86. package/dist/commands/deploy.js.map +1 -0
  87. package/dist/commands/doctor.d.ts +14 -0
  88. package/dist/commands/doctor.d.ts.map +1 -0
  89. package/dist/commands/doctor.js +280 -0
  90. package/dist/commands/doctor.js.map +1 -0
  91. package/dist/commands/figma.d.ts +32 -0
  92. package/dist/commands/figma.d.ts.map +1 -0
  93. package/dist/commands/figma.js +141 -0
  94. package/dist/commands/figma.js.map +1 -0
  95. package/dist/commands/gen-flow-tests.d.ts +8 -0
  96. package/dist/commands/gen-flow-tests.d.ts.map +1 -0
  97. package/dist/commands/gen-flow-tests.js +78 -0
  98. package/dist/commands/gen-flow-tests.js.map +1 -0
  99. package/dist/commands/gen-tests.d.ts +9 -0
  100. package/dist/commands/gen-tests.d.ts.map +1 -0
  101. package/dist/commands/gen-tests.js +118 -0
  102. package/dist/commands/gen-tests.js.map +1 -0
  103. package/dist/commands/license.d.ts +14 -0
  104. package/dist/commands/license.d.ts.map +1 -0
  105. package/dist/commands/license.js +182 -0
  106. package/dist/commands/license.js.map +1 -0
  107. package/dist/commands/log.d.ts +19 -0
  108. package/dist/commands/log.d.ts.map +1 -0
  109. package/dist/commands/log.js +101 -0
  110. package/dist/commands/log.js.map +1 -0
  111. package/dist/commands/migrate.d.ts +118 -0
  112. package/dist/commands/migrate.d.ts.map +1 -0
  113. package/dist/commands/migrate.js +1410 -0
  114. package/dist/commands/migrate.js.map +1 -0
  115. package/dist/commands/mobile.d.ts +27 -0
  116. package/dist/commands/mobile.d.ts.map +1 -0
  117. package/dist/commands/mobile.js +90 -0
  118. package/dist/commands/mobile.js.map +1 -0
  119. package/dist/commands/new.d.ts +32 -0
  120. package/dist/commands/new.d.ts.map +1 -0
  121. package/dist/commands/new.js +107 -0
  122. package/dist/commands/new.js.map +1 -0
  123. package/dist/commands/pilot.d.ts +8 -0
  124. package/dist/commands/pilot.d.ts.map +1 -0
  125. package/dist/commands/pilot.js +104 -0
  126. package/dist/commands/pilot.js.map +1 -0
  127. package/dist/commands/projects.d.ts +21 -0
  128. package/dist/commands/projects.d.ts.map +1 -0
  129. package/dist/commands/projects.js +238 -0
  130. package/dist/commands/projects.js.map +1 -0
  131. package/dist/commands/publish.d.ts +35 -0
  132. package/dist/commands/publish.d.ts.map +1 -0
  133. package/dist/commands/publish.js +194 -0
  134. package/dist/commands/publish.js.map +1 -0
  135. package/dist/commands/repo.d.ts +59 -0
  136. package/dist/commands/repo.d.ts.map +1 -0
  137. package/dist/commands/repo.js +178 -0
  138. package/dist/commands/repo.js.map +1 -0
  139. package/dist/commands/review-screens.d.ts +28 -0
  140. package/dist/commands/review-screens.d.ts.map +1 -0
  141. package/dist/commands/review-screens.js +345 -0
  142. package/dist/commands/review-screens.js.map +1 -0
  143. package/dist/commands/scenarios.d.ts +23 -0
  144. package/dist/commands/scenarios.d.ts.map +1 -0
  145. package/dist/commands/scenarios.js +304 -0
  146. package/dist/commands/scenarios.js.map +1 -0
  147. package/dist/commands/ship.d.ts +18 -0
  148. package/dist/commands/ship.d.ts.map +1 -0
  149. package/dist/commands/ship.js +41 -0
  150. package/dist/commands/ship.js.map +1 -0
  151. package/dist/commands/test.d.ts +29 -0
  152. package/dist/commands/test.d.ts.map +1 -0
  153. package/dist/commands/test.js +62 -0
  154. package/dist/commands/test.js.map +1 -0
  155. package/dist/commands/tunnel.d.ts +22 -0
  156. package/dist/commands/tunnel.d.ts.map +1 -0
  157. package/dist/commands/tunnel.js +77 -0
  158. package/dist/commands/tunnel.js.map +1 -0
  159. package/dist/commands/validate.d.ts +14 -0
  160. package/dist/commands/validate.d.ts.map +1 -0
  161. package/dist/commands/validate.js +51 -0
  162. package/dist/commands/validate.js.map +1 -0
  163. package/dist/commands/vault.d.ts +32 -0
  164. package/dist/commands/vault.d.ts.map +1 -0
  165. package/dist/commands/vault.js +489 -0
  166. package/dist/commands/vault.js.map +1 -0
  167. package/dist/commands/voice.d.ts +16 -0
  168. package/dist/commands/voice.d.ts.map +1 -0
  169. package/dist/commands/voice.js +69 -0
  170. package/dist/commands/voice.js.map +1 -0
  171. package/dist/core/cascade_router.d.ts +90 -0
  172. package/dist/core/cascade_router.d.ts.map +1 -0
  173. package/dist/core/cascade_router.js +131 -0
  174. package/dist/core/cascade_router.js.map +1 -0
  175. package/dist/core/cf_tunnel.d.ts +52 -0
  176. package/dist/core/cf_tunnel.d.ts.map +1 -0
  177. package/dist/core/cf_tunnel.js +134 -0
  178. package/dist/core/cf_tunnel.js.map +1 -0
  179. package/dist/core/gha_dispatcher.d.ts +48 -0
  180. package/dist/core/gha_dispatcher.d.ts.map +1 -0
  181. package/dist/core/gha_dispatcher.js +198 -0
  182. package/dist/core/gha_dispatcher.js.map +1 -0
  183. package/dist/core/logger.d.ts +89 -0
  184. package/dist/core/logger.d.ts.map +1 -0
  185. package/dist/core/logger.js +245 -0
  186. package/dist/core/logger.js.map +1 -0
  187. package/dist/core/mode.d.ts +26 -0
  188. package/dist/core/mode.d.ts.map +1 -0
  189. package/dist/core/mode.js +122 -0
  190. package/dist/core/mode.js.map +1 -0
  191. package/dist/core/pairing.d.ts +40 -0
  192. package/dist/core/pairing.d.ts.map +1 -0
  193. package/dist/core/pairing.js +145 -0
  194. package/dist/core/pairing.js.map +1 -0
  195. package/dist/core/pilot_setup.d.ts +29 -0
  196. package/dist/core/pilot_setup.d.ts.map +1 -0
  197. package/dist/core/pilot_setup.js +119 -0
  198. package/dist/core/pilot_setup.js.map +1 -0
  199. package/dist/core/polar.d.ts +81 -0
  200. package/dist/core/polar.d.ts.map +1 -0
  201. package/dist/core/polar.js +175 -0
  202. package/dist/core/polar.js.map +1 -0
  203. package/dist/core/project_picker.d.ts +56 -0
  204. package/dist/core/project_picker.d.ts.map +1 -0
  205. package/dist/core/project_picker.js +86 -0
  206. package/dist/core/project_picker.js.map +1 -0
  207. package/dist/core/projects.d.ts +58 -0
  208. package/dist/core/projects.d.ts.map +1 -0
  209. package/dist/core/projects.js +146 -0
  210. package/dist/core/projects.js.map +1 -0
  211. package/dist/core/projects_sync.d.ts +80 -0
  212. package/dist/core/projects_sync.d.ts.map +1 -0
  213. package/dist/core/projects_sync.js +278 -0
  214. package/dist/core/projects_sync.js.map +1 -0
  215. package/dist/core/remote_runner.d.ts +70 -0
  216. package/dist/core/remote_runner.d.ts.map +1 -0
  217. package/dist/core/remote_runner.js +133 -0
  218. package/dist/core/remote_runner.js.map +1 -0
  219. package/dist/core/repo_state.d.ts +24 -0
  220. package/dist/core/repo_state.d.ts.map +1 -0
  221. package/dist/core/repo_state.js +109 -0
  222. package/dist/core/repo_state.js.map +1 -0
  223. package/dist/core/target.d.ts +31 -0
  224. package/dist/core/target.d.ts.map +1 -0
  225. package/dist/core/target.js +121 -0
  226. package/dist/core/target.js.map +1 -0
  227. package/dist/deploy/aws.d.ts +43 -0
  228. package/dist/deploy/aws.d.ts.map +1 -0
  229. package/dist/deploy/aws.js +173 -0
  230. package/dist/deploy/aws.js.map +1 -0
  231. package/dist/figma/api.d.ts +35 -0
  232. package/dist/figma/api.d.ts.map +1 -0
  233. package/dist/figma/api.js +40 -0
  234. package/dist/figma/api.js.map +1 -0
  235. package/dist/figma/decorator.d.ts +74 -0
  236. package/dist/figma/decorator.d.ts.map +1 -0
  237. package/dist/figma/decorator.js +210 -0
  238. package/dist/figma/decorator.js.map +1 -0
  239. package/dist/figma/heuristics.d.ts +29 -0
  240. package/dist/figma/heuristics.d.ts.map +1 -0
  241. package/dist/figma/heuristics.js +110 -0
  242. package/dist/figma/heuristics.js.map +1 -0
  243. package/dist/figma/normalize.d.ts +33 -0
  244. package/dist/figma/normalize.d.ts.map +1 -0
  245. package/dist/figma/normalize.js +101 -0
  246. package/dist/figma/normalize.js.map +1 -0
  247. package/dist/figma/tokens.d.ts +23 -0
  248. package/dist/figma/tokens.d.ts.map +1 -0
  249. package/dist/figma/tokens.js +111 -0
  250. package/dist/figma/tokens.js.map +1 -0
  251. package/dist/figma/types.d.ts +118 -0
  252. package/dist/figma/types.d.ts.map +1 -0
  253. package/dist/figma/types.js +12 -0
  254. package/dist/figma/types.js.map +1 -0
  255. package/dist/i18n/index.d.ts +48 -0
  256. package/dist/i18n/index.d.ts.map +1 -0
  257. package/dist/i18n/index.js +135 -0
  258. package/dist/i18n/index.js.map +1 -0
  259. package/dist/i18n/types.d.ts +52 -0
  260. package/dist/i18n/types.d.ts.map +1 -0
  261. package/dist/i18n/types.js +85 -0
  262. package/dist/i18n/types.js.map +1 -0
  263. package/dist/index.d.ts +12 -0
  264. package/dist/index.d.ts.map +1 -0
  265. package/dist/index.js +11 -0
  266. package/dist/index.js.map +1 -0
  267. package/dist/lan/mdns_packet.d.ts +74 -0
  268. package/dist/lan/mdns_packet.d.ts.map +1 -0
  269. package/dist/lan/mdns_packet.js +247 -0
  270. package/dist/lan/mdns_packet.js.map +1 -0
  271. package/dist/lan/mdns_service.d.ts +102 -0
  272. package/dist/lan/mdns_service.d.ts.map +1 -0
  273. package/dist/lan/mdns_service.js +206 -0
  274. package/dist/lan/mdns_service.js.map +1 -0
  275. package/dist/license/activate.d.ts +33 -0
  276. package/dist/license/activate.d.ts.map +1 -0
  277. package/dist/license/activate.js +135 -0
  278. package/dist/license/activate.js.map +1 -0
  279. package/dist/license/fingerprint.d.ts +2 -0
  280. package/dist/license/fingerprint.d.ts.map +1 -0
  281. package/dist/license/fingerprint.js +29 -0
  282. package/dist/license/fingerprint.js.map +1 -0
  283. package/dist/license/hito4_client.d.ts +24 -0
  284. package/dist/license/hito4_client.d.ts.map +1 -0
  285. package/dist/license/hito4_client.js +103 -0
  286. package/dist/license/hito4_client.js.map +1 -0
  287. package/dist/license/index.d.ts +22 -0
  288. package/dist/license/index.d.ts.map +1 -0
  289. package/dist/license/index.js +125 -0
  290. package/dist/license/index.js.map +1 -0
  291. package/dist/license/types.d.ts +38 -0
  292. package/dist/license/types.d.ts.map +1 -0
  293. package/dist/license/types.js +9 -0
  294. package/dist/license/types.js.map +1 -0
  295. package/dist/migrate/ai-apply.d.ts +198 -0
  296. package/dist/migrate/ai-apply.d.ts.map +1 -0
  297. package/dist/migrate/ai-apply.js +833 -0
  298. package/dist/migrate/ai-apply.js.map +1 -0
  299. package/dist/migrate/ai-decorator.d.ts +87 -0
  300. package/dist/migrate/ai-decorator.d.ts.map +1 -0
  301. package/dist/migrate/ai-decorator.js +203 -0
  302. package/dist/migrate/ai-decorator.js.map +1 -0
  303. package/dist/migrate/apply.d.ts +28 -0
  304. package/dist/migrate/apply.d.ts.map +1 -0
  305. package/dist/migrate/apply.js +119 -0
  306. package/dist/migrate/apply.js.map +1 -0
  307. package/dist/migrate/audit.d.ts +9 -0
  308. package/dist/migrate/audit.d.ts.map +1 -0
  309. package/dist/migrate/audit.js +197 -0
  310. package/dist/migrate/audit.js.map +1 -0
  311. package/dist/migrate/diff.d.ts +28 -0
  312. package/dist/migrate/diff.d.ts.map +1 -0
  313. package/dist/migrate/diff.js +154 -0
  314. package/dist/migrate/diff.js.map +1 -0
  315. package/dist/migrate/html-orchestrator.d.ts +81 -0
  316. package/dist/migrate/html-orchestrator.d.ts.map +1 -0
  317. package/dist/migrate/html-orchestrator.js +233 -0
  318. package/dist/migrate/html-orchestrator.js.map +1 -0
  319. package/dist/migrate/html-walker.d.ts +93 -0
  320. package/dist/migrate/html-walker.d.ts.map +1 -0
  321. package/dist/migrate/html-walker.js +288 -0
  322. package/dist/migrate/html-walker.js.map +1 -0
  323. package/dist/migrate/js-template-walker.d.ts +118 -0
  324. package/dist/migrate/js-template-walker.d.ts.map +1 -0
  325. package/dist/migrate/js-template-walker.js +644 -0
  326. package/dist/migrate/js-template-walker.js.map +1 -0
  327. package/dist/migrate/manifest-validator.d.ts +30 -0
  328. package/dist/migrate/manifest-validator.d.ts.map +1 -0
  329. package/dist/migrate/manifest-validator.js +261 -0
  330. package/dist/migrate/manifest-validator.js.map +1 -0
  331. package/dist/migrate/overrides.d.ts +58 -0
  332. package/dist/migrate/overrides.d.ts.map +1 -0
  333. package/dist/migrate/overrides.js +193 -0
  334. package/dist/migrate/overrides.js.map +1 -0
  335. package/dist/migrate/plugin-scope.d.ts +42 -0
  336. package/dist/migrate/plugin-scope.d.ts.map +1 -0
  337. package/dist/migrate/plugin-scope.js +94 -0
  338. package/dist/migrate/plugin-scope.js.map +1 -0
  339. package/dist/migrate/types.d.ts +45 -0
  340. package/dist/migrate/types.d.ts.map +1 -0
  341. package/dist/migrate/types.js +9 -0
  342. package/dist/migrate/types.js.map +1 -0
  343. package/dist/migrate/verb-inference.d.ts +37 -0
  344. package/dist/migrate/verb-inference.d.ts.map +1 -0
  345. package/dist/migrate/verb-inference.js +274 -0
  346. package/dist/migrate/verb-inference.js.map +1 -0
  347. package/dist/nac3/attrs.d.ts +87 -0
  348. package/dist/nac3/attrs.d.ts.map +1 -0
  349. package/dist/nac3/attrs.js +134 -0
  350. package/dist/nac3/attrs.js.map +1 -0
  351. package/dist/nac3/scenario_dsl.d.ts +71 -0
  352. package/dist/nac3/scenario_dsl.d.ts.map +1 -0
  353. package/dist/nac3/scenario_dsl.js +191 -0
  354. package/dist/nac3/scenario_dsl.js.map +1 -0
  355. package/dist/nac3/tokens.d.ts +126 -0
  356. package/dist/nac3/tokens.d.ts.map +1 -0
  357. package/dist/nac3/tokens.js +138 -0
  358. package/dist/nac3/tokens.js.map +1 -0
  359. package/dist/reader/parsers/csv.d.ts +42 -0
  360. package/dist/reader/parsers/csv.d.ts.map +1 -0
  361. package/dist/reader/parsers/csv.js +221 -0
  362. package/dist/reader/parsers/csv.js.map +1 -0
  363. package/dist/reader/parsers/docx.d.ts +31 -0
  364. package/dist/reader/parsers/docx.d.ts.map +1 -0
  365. package/dist/reader/parsers/docx.js +51 -0
  366. package/dist/reader/parsers/docx.js.map +1 -0
  367. package/dist/reader/parsers/epub.d.ts +39 -0
  368. package/dist/reader/parsers/epub.d.ts.map +1 -0
  369. package/dist/reader/parsers/epub.js +265 -0
  370. package/dist/reader/parsers/epub.js.map +1 -0
  371. package/dist/reader/parsers/html.d.ts +40 -0
  372. package/dist/reader/parsers/html.d.ts.map +1 -0
  373. package/dist/reader/parsers/html.js +386 -0
  374. package/dist/reader/parsers/html.js.map +1 -0
  375. package/dist/reader/parsers/md.d.ts +30 -0
  376. package/dist/reader/parsers/md.d.ts.map +1 -0
  377. package/dist/reader/parsers/md.js +199 -0
  378. package/dist/reader/parsers/md.js.map +1 -0
  379. package/dist/reader/parsers/pdf.d.ts +39 -0
  380. package/dist/reader/parsers/pdf.d.ts.map +1 -0
  381. package/dist/reader/parsers/pdf.js +220 -0
  382. package/dist/reader/parsers/pdf.js.map +1 -0
  383. package/dist/reader/parsers/rtf.d.ts +37 -0
  384. package/dist/reader/parsers/rtf.d.ts.map +1 -0
  385. package/dist/reader/parsers/rtf.js +347 -0
  386. package/dist/reader/parsers/rtf.js.map +1 -0
  387. package/dist/reader/parsers/source.d.ts +32 -0
  388. package/dist/reader/parsers/source.d.ts.map +1 -0
  389. package/dist/reader/parsers/source.js +122 -0
  390. package/dist/reader/parsers/source.js.map +1 -0
  391. package/dist/reader/parsers/txt.d.ts +25 -0
  392. package/dist/reader/parsers/txt.d.ts.map +1 -0
  393. package/dist/reader/parsers/txt.js +56 -0
  394. package/dist/reader/parsers/txt.js.map +1 -0
  395. package/dist/reader/parsers/xlsx.d.ts +33 -0
  396. package/dist/reader/parsers/xlsx.d.ts.map +1 -0
  397. package/dist/reader/parsers/xlsx.js +143 -0
  398. package/dist/reader/parsers/xlsx.js.map +1 -0
  399. package/dist/reader/registry.d.ts +39 -0
  400. package/dist/reader/registry.d.ts.map +1 -0
  401. package/dist/reader/registry.js +172 -0
  402. package/dist/reader/registry.js.map +1 -0
  403. package/dist/reader/search.d.ts +27 -0
  404. package/dist/reader/search.d.ts.map +1 -0
  405. package/dist/reader/search.js +77 -0
  406. package/dist/reader/search.js.map +1 -0
  407. package/dist/reader/state.d.ts +56 -0
  408. package/dist/reader/state.d.ts.map +1 -0
  409. package/dist/reader/state.js +179 -0
  410. package/dist/reader/state.js.map +1 -0
  411. package/dist/reader/types.d.ts +119 -0
  412. package/dist/reader/types.d.ts.map +1 -0
  413. package/dist/reader/types.js +23 -0
  414. package/dist/reader/types.js.map +1 -0
  415. package/dist/ship/run.d.ts +26 -0
  416. package/dist/ship/run.d.ts.map +1 -0
  417. package/dist/ship/run.js +123 -0
  418. package/dist/ship/run.js.map +1 -0
  419. package/dist/template/index.d.ts +50 -0
  420. package/dist/template/index.d.ts.map +1 -0
  421. package/dist/template/index.js +140 -0
  422. package/dist/template/index.js.map +1 -0
  423. package/dist/ui/colors.d.ts +13 -0
  424. package/dist/ui/colors.d.ts.map +1 -0
  425. package/dist/ui/colors.js +26 -0
  426. package/dist/ui/colors.js.map +1 -0
  427. package/dist/validate/index.d.ts +19 -0
  428. package/dist/validate/index.d.ts.map +1 -0
  429. package/dist/validate/index.js +181 -0
  430. package/dist/validate/index.js.map +1 -0
  431. package/dist/vault/catalog.d.ts +55 -0
  432. package/dist/vault/catalog.d.ts.map +1 -0
  433. package/dist/vault/catalog.js +424 -0
  434. package/dist/vault/catalog.js.map +1 -0
  435. package/dist/vault/crypto.d.ts +82 -0
  436. package/dist/vault/crypto.d.ts.map +1 -0
  437. package/dist/vault/crypto.js +173 -0
  438. package/dist/vault/crypto.js.map +1 -0
  439. package/dist/vault/git_askpass.d.ts +26 -0
  440. package/dist/vault/git_askpass.d.ts.map +1 -0
  441. package/dist/vault/git_askpass.js +104 -0
  442. package/dist/vault/git_askpass.js.map +1 -0
  443. package/dist/vault/migrator.d.ts +57 -0
  444. package/dist/vault/migrator.d.ts.map +1 -0
  445. package/dist/vault/migrator.js +204 -0
  446. package/dist/vault/migrator.js.map +1 -0
  447. package/dist/vault/redactor.d.ts +73 -0
  448. package/dist/vault/redactor.d.ts.map +1 -0
  449. package/dist/vault/redactor.js +182 -0
  450. package/dist/vault/redactor.js.map +1 -0
  451. package/dist/vault/store.d.ts +132 -0
  452. package/dist/vault/store.d.ts.map +1 -0
  453. package/dist/vault/store.js +335 -0
  454. package/dist/vault/store.js.map +1 -0
  455. package/dist/version.d.ts +8 -0
  456. package/dist/version.d.ts.map +1 -0
  457. package/dist/version.js +8 -0
  458. package/dist/version.js.map +1 -0
  459. package/dist/voice/chunker.d.ts +43 -0
  460. package/dist/voice/chunker.d.ts.map +1 -0
  461. package/dist/voice/chunker.js +133 -0
  462. package/dist/voice/chunker.js.map +1 -0
  463. package/dist/voice/config.d.ts +14 -0
  464. package/dist/voice/config.d.ts.map +1 -0
  465. package/dist/voice/config.js +51 -0
  466. package/dist/voice/config.js.map +1 -0
  467. package/dist/voice/intents.d.ts +71 -0
  468. package/dist/voice/intents.d.ts.map +1 -0
  469. package/dist/voice/intents.js +0 -0
  470. package/dist/voice/intents.js.map +1 -0
  471. package/dist/voice/providers/elevenlabs.d.ts +53 -0
  472. package/dist/voice/providers/elevenlabs.d.ts.map +1 -0
  473. package/dist/voice/providers/elevenlabs.js +159 -0
  474. package/dist/voice/providers/elevenlabs.js.map +1 -0
  475. package/dist/voice/providers/google.d.ts +56 -0
  476. package/dist/voice/providers/google.d.ts.map +1 -0
  477. package/dist/voice/providers/google.js +253 -0
  478. package/dist/voice/providers/google.js.map +1 -0
  479. package/dist/voice/providers/whisper.d.ts +44 -0
  480. package/dist/voice/providers/whisper.d.ts.map +1 -0
  481. package/dist/voice/providers/whisper.js +179 -0
  482. package/dist/voice/providers/whisper.js.map +1 -0
  483. package/dist/voice/registry.d.ts +35 -0
  484. package/dist/voice/registry.d.ts.map +1 -0
  485. package/dist/voice/registry.js +48 -0
  486. package/dist/voice/registry.js.map +1 -0
  487. package/dist/voice/router.d.ts +62 -0
  488. package/dist/voice/router.d.ts.map +1 -0
  489. package/dist/voice/router.js +175 -0
  490. package/dist/voice/router.js.map +1 -0
  491. package/dist/voice/types.d.ts +116 -0
  492. package/dist/voice/types.d.ts.map +1 -0
  493. package/dist/voice/types.js +22 -0
  494. package/dist/voice/types.js.map +1 -0
  495. package/dist/voice/voiceprint/enrollment.d.ts +36 -0
  496. package/dist/voice/voiceprint/enrollment.d.ts.map +1 -0
  497. package/dist/voice/voiceprint/enrollment.js +71 -0
  498. package/dist/voice/voiceprint/enrollment.js.map +1 -0
  499. package/dist/voice/voiceprint/identify.d.ts +16 -0
  500. package/dist/voice/voiceprint/identify.d.ts.map +1 -0
  501. package/dist/voice/voiceprint/identify.js +20 -0
  502. package/dist/voice/voiceprint/identify.js.map +1 -0
  503. package/dist/voice/voiceprint/liveness.d.ts +90 -0
  504. package/dist/voice/voiceprint/liveness.d.ts.map +1 -0
  505. package/dist/voice/voiceprint/liveness.js +251 -0
  506. package/dist/voice/voiceprint/liveness.js.map +1 -0
  507. package/dist/voice/voiceprint/match.d.ts +54 -0
  508. package/dist/voice/voiceprint/match.d.ts.map +1 -0
  509. package/dist/voice/voiceprint/match.js +88 -0
  510. package/dist/voice/voiceprint/match.js.map +1 -0
  511. package/dist/voice/voiceprint/providers/local-mfcc-stub.d.ts +44 -0
  512. package/dist/voice/voiceprint/providers/local-mfcc-stub.d.ts.map +1 -0
  513. package/dist/voice/voiceprint/providers/local-mfcc-stub.js +92 -0
  514. package/dist/voice/voiceprint/providers/local-mfcc-stub.js.map +1 -0
  515. package/dist/voice/voiceprint/store.d.ts +60 -0
  516. package/dist/voice/voiceprint/store.d.ts.map +1 -0
  517. package/dist/voice/voiceprint/store.js +155 -0
  518. package/dist/voice/voiceprint/store.js.map +1 -0
  519. package/dist/voice/voiceprint/trust.d.ts +90 -0
  520. package/dist/voice/voiceprint/trust.d.ts.map +1 -0
  521. package/dist/voice/voiceprint/trust.js +150 -0
  522. package/dist/voice/voiceprint/trust.js.map +1 -0
  523. package/dist/voice/voiceprint/types.d.ts +100 -0
  524. package/dist/voice/voiceprint/types.d.ts.map +1 -0
  525. package/dist/voice/voiceprint/types.js +23 -0
  526. package/dist/voice/voiceprint/types.js.map +1 -0
  527. package/dist/voice/wake.d.ts +64 -0
  528. package/dist/voice/wake.d.ts.map +1 -0
  529. package/dist/voice/wake.js +143 -0
  530. package/dist/voice/wake.js.map +1 -0
  531. package/docs/manuals/manual.ar.html +91 -0
  532. package/docs/manuals/manual.de.html +100 -0
  533. package/docs/manuals/manual.en.html +118 -0
  534. package/docs/manuals/manual.es.html +120 -0
  535. package/docs/manuals/manual.fr.html +102 -0
  536. package/docs/manuals/manual.hi.html +93 -0
  537. package/docs/manuals/manual.it.html +93 -0
  538. package/docs/manuals/manual.ja.html +97 -0
  539. package/docs/manuals/manual.pt.html +103 -0
  540. package/docs/manuals/manual.zh.html +89 -0
  541. package/package.json +94 -0
  542. package/src/i18n/catalogs/ar.json +86 -0
  543. package/src/i18n/catalogs/de.json +86 -0
  544. package/src/i18n/catalogs/en.json +86 -0
  545. package/src/i18n/catalogs/es.json +86 -0
  546. package/src/i18n/catalogs/fr.json +86 -0
  547. package/src/i18n/catalogs/hi.json +86 -0
  548. package/src/i18n/catalogs/it.json +86 -0
  549. package/src/i18n/catalogs/ja.json +86 -0
  550. package/src/i18n/catalogs/pt.json +86 -0
  551. package/src/i18n/catalogs/zh.json +86 -0
  552. package/templates/react-app/README.md +43 -0
  553. package/templates/react-app/index.html +12 -0
  554. package/templates/react-app/package.json +35 -0
  555. package/templates/react-app/src/App.tsx +106 -0
  556. package/templates/react-app/src/main.tsx +21 -0
  557. package/templates/react-app/src/nac/manifest.ts +46 -0
  558. package/templates/react-app/src/styles.css +68 -0
  559. package/templates/react-app/tsconfig.json +19 -0
  560. package/templates/react-app/vite.config.ts +12 -0
  561. package/templates/react-app/yujin.forge.json +15 -0
@@ -0,0 +1,1540 @@
1
+ /**
2
+ * Yujin Forge -- chat panel HTTP server.
3
+ *
4
+ * Routes (all on the local loopback port; CORS not needed):
5
+ *
6
+ * GET / serves the panel HTML
7
+ * GET /api/health { ok: true, version, project }
8
+ * POST /api/chat { messages } -> { ok, message }
9
+ *
10
+ * The server binds to 127.0.0.1 only -- never 0.0.0.0. Forge
11
+ * chat is a local-developer tool; exposing it on the LAN would
12
+ * leak the API key + the project source-aware system prompt.
13
+ */
14
+ import { createServer } from 'node:http';
15
+ import { promises as fs } from 'node:fs';
16
+ import path from 'node:path';
17
+ import { ClaudeClient, ConfigurationError, ClaudeApiError } from './claude.js';
18
+ import { renderPanelHtml } from './panel.js';
19
+ import { TranscriptStore } from './persistence.js';
20
+ import { FORGE_TOOL_SPECS, runForgeTool } from './tools.js';
21
+ import { Vault } from '../vault/store.js';
22
+ import { SLOT_CATALOG, SLOT_KINDS } from '../vault/catalog.js';
23
+ import { resolveMode, persistMode, modePromptSuffix, FORGE_MODES, } from '../core/mode.js';
24
+ import { resolveTarget, persistTarget, normaliseTarget, FORGE_TARGETS, } from '../core/target.js';
25
+ import { readPilotState, completePilotSetup, welcomePromptSuffix, shouldAutoComplete, } from '../core/pilot_setup.js';
26
+ import { readRegistry, upsertProject, setActive, activeSlug, deriveSlugFromPath, scanForProjects, } from '../core/projects.js';
27
+ import { configDir } from '../license/index.js';
28
+ import { VoiceRouter } from '../voice/router.js';
29
+ import { buildDefaultRegistry } from '../voice/registry.js';
30
+ import { matchReaderIntent } from '../voice/intents.js';
31
+ import { setRecapSummarizer } from './tools/reader.js';
32
+ import { parseDocument } from '../reader/registry.js';
33
+ import { SUPPORTED_LANGUAGES, LANGUAGE_DISPLAY_NAMES, BCP47, getCatalog, setLanguage, currentLanguage, normaliseLanguageTag, } from '../i18n/index.js';
34
+ import { listAvailableManuals } from './tools/manual.js';
35
+ import { recordIngest, getIngest, setExtraction, setPlan, setScaffold, clearScaffold, listIngest } from './ingest_session.js';
36
+ import { extractSpec } from './spec_extract.js';
37
+ import { generatePlan } from './spec_plan.js';
38
+ import { executeScaffold, rollbackScaffold } from './spec_scaffold.js';
39
+ import { VERSION } from '../version.js';
40
+ export async function startChatServer(opts) {
41
+ const projectName = await readProjectName(opts.projectRoot);
42
+ const claude = opts.claude ?? new ClaudeClient();
43
+ const voice = opts.voice ?? new VoiceRouter({
44
+ configDir: configDir(),
45
+ registry: buildDefaultRegistry(),
46
+ });
47
+ const store = new TranscriptStore(opts.projectRoot, VERSION);
48
+ /* V1.26b -- install the Claude-backed recap summariser so
49
+ forge.reader.recap({mode:'summary'}) can call it. The
50
+ reader still gates via env var YF_ENABLE_CLAUDE_RECAP=1
51
+ before actually invoking; this just makes the function
52
+ available. */
53
+ setRecapSummarizer(async (blocks, options) => {
54
+ const lang = options.language || 'es';
55
+ /* i18n-exempt: developer-facing system prompt that
56
+ instructs Claude in Spanish; Claude understands it and
57
+ is asked to respond in `lang`. Not surfaced to the user. */
58
+ const sys = 'Sos un resumidor breve. Te paso una lista de bloques de texto que el usuario acaba de escuchar via reader. '
59
+ + 'Devolves un resumen de 1 a 2 oraciones en idioma ' + lang + ', sin agregar informacion que no este en los bloques. '
60
+ + 'Sin vinetas. Solo prosa breve y clara.';
61
+ const reply = await claude.chat({
62
+ messages: [
63
+ { role: 'user', content: 'Resumi estos bloques:\n\n' + blocks.map((b, i) => '[' + (i + 1) + '] ' + b).join('\n') },
64
+ ],
65
+ system: sys,
66
+ maxTokens: 200,
67
+ });
68
+ return reply.text.trim();
69
+ });
70
+ const server = createServer(async (req, res) => {
71
+ // Every response carries a request-id (for client + server
72
+ // log correlation) + cache-control (we never want browsers
73
+ // to cache panel HTML / API responses since the panel is
74
+ // dynamic). Anti-debt headers requested in the 600-suite
75
+ // followup list -- see docs/CROSS_BROWSER_REPORT_600.md.
76
+ const requestId = generateRequestId();
77
+ res.setHeader('x-request-id', requestId);
78
+ res.setHeader('cache-control', 'no-store, must-revalidate');
79
+ res.setHeader('x-yujin-version', VERSION);
80
+ try {
81
+ await route(req, res, {
82
+ projectRoot: opts.projectRoot,
83
+ projectName,
84
+ port: opts.port,
85
+ claude,
86
+ voice,
87
+ store,
88
+ });
89
+ }
90
+ catch (err) {
91
+ sendJson(res, 500, {
92
+ ok: false,
93
+ error: err instanceof Error ? err.message : String(err),
94
+ request_id: requestId,
95
+ });
96
+ }
97
+ });
98
+ // Aggressive socket cleanup. Default keepAliveTimeout (5s)
99
+ // can keep sockets pending when the panel is in a test or
100
+ // headless context that does not close them gracefully. The
101
+ // 600-suite followup observed force-killed webkit workers
102
+ // when the suite ended -- shorter timeouts let Node drop the
103
+ // sockets cleanly so Playwright cleanup is immediate.
104
+ server.keepAliveTimeout = 1000;
105
+ server.headersTimeout = 5000;
106
+ server.requestTimeout = 30000;
107
+ await new Promise((resolve, reject) => {
108
+ server.once('error', reject);
109
+ server.listen(opts.port, '127.0.0.1', () => resolve());
110
+ });
111
+ const url = 'http://127.0.0.1:' + opts.port + '/';
112
+ return {
113
+ server,
114
+ url,
115
+ store,
116
+ close: () => new Promise((resolve) => {
117
+ server.close(() => resolve());
118
+ }),
119
+ };
120
+ }
121
+ async function route(req, res, ctx) {
122
+ const url = new URL(req.url ?? '/', 'http://127.0.0.1');
123
+ if (req.method === 'GET' && url.pathname === '/') {
124
+ // Stateless lang resolution: query string > cookie >
125
+ // server-wide setLanguage. This isolates per-request
126
+ // rendering so concurrent clients with different langs
127
+ // do not race on the shared current_language state.
128
+ const reqLang = resolveRequestLang(req, url);
129
+ res.statusCode = 200;
130
+ res.setHeader('content-type', 'text/html; charset=utf-8');
131
+ res.end(renderPanelHtml({
132
+ projectRoot: ctx.projectRoot,
133
+ projectName: ctx.projectName,
134
+ port: ctx.port,
135
+ lang: reqLang,
136
+ }));
137
+ return;
138
+ }
139
+ if (req.method === 'GET' && url.pathname === '/api/health') {
140
+ sendJson(res, 200, {
141
+ ok: true,
142
+ version: VERSION,
143
+ project: { name: ctx.projectName, root: ctx.projectRoot },
144
+ });
145
+ return;
146
+ }
147
+ if (req.method === 'POST' && url.pathname === '/api/chat') {
148
+ await handleChat(req, res, ctx);
149
+ return;
150
+ }
151
+ /* Vault surface (V0.4 of HITO 0). Same-origin: the server only
152
+ listens on 127.0.0.1 and the browser blocks cross-origin
153
+ fetches from other sites by default (no CORS headers set).
154
+ Plaintext NEVER returns through any of these endpoints --
155
+ only set accepts it, and only list/has/info return metadata. */
156
+ if (req.method === 'GET' && url.pathname === '/api/vault/list') {
157
+ await handleVaultList(res);
158
+ return;
159
+ }
160
+ if (req.method === 'POST' && url.pathname === '/api/vault/set') {
161
+ await handleVaultSet(req, res);
162
+ return;
163
+ }
164
+ if (req.method === 'POST' && url.pathname === '/api/vault/remove') {
165
+ await handleVaultRemove(req, res);
166
+ return;
167
+ }
168
+ if (req.method === 'GET' && url.pathname.startsWith('/api/vault/has/')) {
169
+ const slot = url.pathname.slice('/api/vault/has/'.length);
170
+ await handleVaultHas(res, slot);
171
+ return;
172
+ }
173
+ if (req.method === 'GET' && url.pathname === '/api/vault/catalog') {
174
+ await handleVaultCatalog(res, url);
175
+ return;
176
+ }
177
+ /* SQ 0.8 -- dual mode read/write. */
178
+ if (req.method === 'GET' && url.pathname === '/api/forge/mode') {
179
+ await handleModeGet(req, res, ctx);
180
+ return;
181
+ }
182
+ if (req.method === 'POST' && url.pathname === '/api/forge/mode') {
183
+ await handleModeSet(req, res, ctx);
184
+ return;
185
+ }
186
+ /* SQ 0.10 -- pre-dev target query + persistence. */
187
+ if (req.method === 'GET' && url.pathname === '/api/forge/target') {
188
+ await handleTargetGet(res, ctx);
189
+ return;
190
+ }
191
+ if (req.method === 'POST' && url.pathname === '/api/forge/target') {
192
+ await handleTargetSet(req, res, ctx);
193
+ return;
194
+ }
195
+ /* PLAN #10 -- project registry endpoints. */
196
+ if (req.method === 'GET' && url.pathname === '/api/forge/projects') {
197
+ await handleProjectsList(res);
198
+ return;
199
+ }
200
+ if (req.method === 'POST' && url.pathname === '/api/forge/projects/active') {
201
+ await handleProjectsSwitch(req, res);
202
+ return;
203
+ }
204
+ if (req.method === 'GET' && url.pathname === '/api/forge/projects/active') {
205
+ await handleProjectsActive(res);
206
+ return;
207
+ }
208
+ if (req.method === 'POST' && url.pathname === '/api/forge/projects/scan') {
209
+ await handleProjectsScan(req, res);
210
+ return;
211
+ }
212
+ if (req.method === 'POST' && url.pathname === '/api/forge/projects/add') {
213
+ await handleProjectsAdd(req, res);
214
+ return;
215
+ }
216
+ if (req.method === 'POST' && url.pathname.startsWith('/api/vault/test/')) {
217
+ const slot = url.pathname.slice('/api/vault/test/'.length);
218
+ await handleVaultTest(req, res, slot);
219
+ return;
220
+ }
221
+ /* Voice surface (V1.5 of HITO 1). Same-origin only (loopback +
222
+ no CORS). Audio bytes flow on /stt; JSON on /tts; the response
223
+ is binary audio. */
224
+ if (req.method === 'GET' && url.pathname === '/api/voice/config') {
225
+ await handleVoiceConfigGet(res, ctx);
226
+ return;
227
+ }
228
+ if (req.method === 'POST' && url.pathname === '/api/voice/config') {
229
+ await handleVoiceConfigSet(req, res, ctx);
230
+ return;
231
+ }
232
+ if (req.method === 'POST' && url.pathname === '/api/voice/stt') {
233
+ await handleVoiceStt(req, res, ctx);
234
+ return;
235
+ }
236
+ if (req.method === 'POST' && url.pathname === '/api/voice/tts') {
237
+ await handleVoiceTts(req, res, ctx);
238
+ return;
239
+ }
240
+ /* Direct reader-tool dispatch (V1.32). The voice intent matcher
241
+ surfaces a tool + args on the STT response; this endpoint
242
+ lets the client execute it without a Claude round-trip.
243
+ Whitelisted to forge.reader.* only -- write-class tools
244
+ (git_commit, git_push, run_app, create_github_repo, ...)
245
+ are NOT reachable here, they keep their approval flow
246
+ through /api/chat. */
247
+ if (req.method === 'POST' && url.pathname === '/api/forge/tool') {
248
+ await handleForgeToolDispatch(req, res, ctx);
249
+ return;
250
+ }
251
+ /* Spec doc ingest (V1.37 + V1.38 -- bloque 4.5 scaffolding).
252
+ Client uploads a spec file (PDF / DOCX / HTML / md / ...)
253
+ and the server parses it via the existing reader pipeline,
254
+ returning the NormalisedDocument shape. Real plan-building
255
+ + file scaffolding lands in V1.41..V1.46. */
256
+ if (req.method === 'POST' && url.pathname === '/api/forge/ingest') {
257
+ await handleForgeIngest(req, res, ctx);
258
+ return;
259
+ }
260
+ /* Spec extraction via Claude (V1.40). After an ingest, the
261
+ panel calls this to ask Claude for structured
262
+ requirements/entities/endpoints/components. */
263
+ if (req.method === 'POST' && url.pathname === '/api/forge/ingest/extract') {
264
+ await handleForgeIngestExtract(req, res, ctx);
265
+ return;
266
+ }
267
+ /* List active ingest sessions (V1.40 -- for debugging + the
268
+ panel's "previously ingested" picker, future slice). */
269
+ if (req.method === 'GET' && url.pathname === '/api/forge/ingest') {
270
+ sendJson(res, 200, { ok: true, sessions: listIngest() });
271
+ return;
272
+ }
273
+ /* Plan generation (V1.41). Asks Claude to convert the V1.40
274
+ extraction into a concrete file scaffold plan. */
275
+ if (req.method === 'POST' && url.pathname === '/api/forge/ingest/plan') {
276
+ await handleForgeIngestPlan(req, res, ctx);
277
+ return;
278
+ }
279
+ /* Scaffold approval + execution (V1.42 + V1.43). The body
280
+ must include {doc_id, approve:true}; only then does the
281
+ scaffolder touch the filesystem. */
282
+ if (req.method === 'POST' && url.pathname === '/api/forge/ingest/scaffold') {
283
+ await handleForgeIngestScaffold(req, res, ctx);
284
+ return;
285
+ }
286
+ /* Progress stream (V1.44). Server-sent events fire as the
287
+ scaffolder writes each file. The handler does the same
288
+ work as /scaffold but emits incremental updates. */
289
+ if (req.method === 'POST' && url.pathname === '/api/forge/ingest/scaffold/stream') {
290
+ await handleForgeIngestScaffoldStream(req, res, ctx);
291
+ return;
292
+ }
293
+ /* Rollback (V1.45). Undoes a scaffold via its recorded
294
+ rollback log. */
295
+ if (req.method === 'POST' && url.pathname === '/api/forge/ingest/rollback') {
296
+ await handleForgeIngestRollback(req, res, ctx);
297
+ return;
298
+ }
299
+ /* i18n surface (Fase F.3). The panel uses these to populate
300
+ the language selector + drive the current panel language. */
301
+ if (req.method === 'GET' && url.pathname === '/api/i18n/languages') {
302
+ const available = await listAvailableManuals();
303
+ sendJson(res, 200, {
304
+ ok: true,
305
+ current: currentLanguage(),
306
+ languages: SUPPORTED_LANGUAGES.map((l) => ({
307
+ code: l,
308
+ display: LANGUAGE_DISPLAY_NAMES[l],
309
+ bcp47: BCP47[l],
310
+ manual_available: available.includes(l),
311
+ })),
312
+ });
313
+ return;
314
+ }
315
+ if (req.method === 'GET' && url.pathname.startsWith('/api/i18n/catalog/')) {
316
+ const langRaw = url.pathname.slice('/api/i18n/catalog/'.length);
317
+ const lang = normaliseLanguageTag(langRaw);
318
+ sendJson(res, 200, {
319
+ ok: true,
320
+ lang,
321
+ catalog: getCatalog(lang),
322
+ });
323
+ return;
324
+ }
325
+ if (req.method === 'POST' && url.pathname === '/api/i18n/language') {
326
+ let body;
327
+ try {
328
+ body = JSON.parse(await readBody(req));
329
+ }
330
+ catch {
331
+ sendJson(res, 400, { ok: false, error: 'invalid JSON body' });
332
+ return;
333
+ }
334
+ if (!body || typeof body.lang !== 'string') {
335
+ sendJson(res, 400, { ok: false, error: 'lang (string) required' });
336
+ return;
337
+ }
338
+ const resolved = setLanguage(body.lang);
339
+ sendJson(res, 200, { ok: true, current: resolved });
340
+ return;
341
+ }
342
+ sendJson(res, 404, { ok: false, error: 'not found' });
343
+ }
344
+ const SUPPORTED_STT_FORMATS = new Set(['webm', 'ogg', 'wav', 'mp3', 'flac', 'mp4']);
345
+ const SUPPORTED_TTS_FORMATS = new Set(['mp3', 'wav', 'ogg', 'opus']);
346
+ /** Cap audio uploads at 25 MB. Larger payloads usually mean a
347
+ * recording loop got stuck; refuse loudly so the panel surfaces
348
+ * the problem instead of OOMing the server. */
349
+ const STT_MAX_AUDIO_BYTES = 25 * 1024 * 1024;
350
+ const TTS_AUDIO_MIME = {
351
+ mp3: 'audio/mpeg',
352
+ wav: 'audio/wav',
353
+ ogg: 'audio/ogg',
354
+ opus: 'audio/ogg',
355
+ };
356
+ async function handleVoiceConfigGet(res, ctx) {
357
+ try {
358
+ const cfg = await ctx.voice.loadConfig();
359
+ const providers = ctx.voice.registeredProviders();
360
+ sendJson(res, 200, { ok: true, config: cfg, providers });
361
+ }
362
+ catch (err) {
363
+ sendJson(res, 500, {
364
+ ok: false,
365
+ error: err instanceof Error ? err.message : String(err),
366
+ });
367
+ }
368
+ }
369
+ async function handleVoiceConfigSet(req, res, ctx) {
370
+ let body;
371
+ try {
372
+ body = JSON.parse(await readBody(req));
373
+ }
374
+ catch {
375
+ sendJson(res, 400, { ok: false, error: 'invalid JSON body' });
376
+ return;
377
+ }
378
+ if (!body || typeof body !== 'object') {
379
+ sendJson(res, 400, { ok: false, error: 'body must be a VoiceConfig object' });
380
+ return;
381
+ }
382
+ const cfg = body;
383
+ if (!cfg.stt || !cfg.tts) {
384
+ sendJson(res, 400, { ok: false, error: 'config must include stt + tts blocks' });
385
+ return;
386
+ }
387
+ try {
388
+ await ctx.voice.saveConfig(cfg);
389
+ sendJson(res, 200, { ok: true, config: cfg });
390
+ }
391
+ catch (err) {
392
+ sendJson(res, 400, {
393
+ ok: false,
394
+ error: err instanceof Error ? err.message : String(err),
395
+ });
396
+ }
397
+ }
398
+ async function handleVoiceStt(req, res, ctx) {
399
+ const format = String(req.headers['x-audio-format'] ?? '').toLowerCase();
400
+ if (!SUPPORTED_STT_FORMATS.has(format)) {
401
+ sendJson(res, 400, {
402
+ ok: false,
403
+ error: 'X-Audio-Format header missing or unsupported. Supported: '
404
+ + Array.from(SUPPORTED_STT_FORMATS).join(', '),
405
+ });
406
+ return;
407
+ }
408
+ const languageHint = typeof req.headers['x-audio-language'] === 'string'
409
+ ? String(req.headers['x-audio-language'])
410
+ : undefined;
411
+ let audio;
412
+ try {
413
+ audio = await readBinaryBody(req, STT_MAX_AUDIO_BYTES);
414
+ }
415
+ catch (err) {
416
+ sendJson(res, 413, {
417
+ ok: false,
418
+ error: err instanceof Error ? err.message : String(err),
419
+ });
420
+ return;
421
+ }
422
+ if (audio.length === 0) {
423
+ sendJson(res, 400, { ok: false, error: 'audio body is empty' });
424
+ return;
425
+ }
426
+ /* Optional active document hint (V1.31). Lets the voice intent
427
+ matcher resolve commands like "siguiente" or "buscar X" against
428
+ the currently-open reader session. Client sets this header to
429
+ the doc_id returned by the last forge.reader.open call. */
430
+ const activeDocId = typeof req.headers['x-active-doc-id'] === 'string'
431
+ ? String(req.headers['x-active-doc-id']).trim()
432
+ : '';
433
+ try {
434
+ const result = await ctx.voice.transcribe({
435
+ audio,
436
+ format: format,
437
+ ...(languageHint ? { languageHint } : {}),
438
+ });
439
+ /* Reader intent shortcut (V1.31). After successful STT, try
440
+ to match the transcript against the reader-intent
441
+ catalogue. On a hit, surface the proposed tool + args so
442
+ the client can dispatch directly without a Claude
443
+ round-trip. On a miss, matched_intent is null and the
444
+ client passes the transcript to Claude as before. */
445
+ const matched = matchReaderIntent(result.text, {
446
+ ...(activeDocId ? { active_doc_id: activeDocId } : {}),
447
+ });
448
+ sendJson(res, 200, {
449
+ ok: true,
450
+ transcript: result,
451
+ matched_intent: matched,
452
+ });
453
+ }
454
+ catch (err) {
455
+ sendJson(res, 502, {
456
+ ok: false,
457
+ error: err instanceof Error ? err.message : String(err),
458
+ });
459
+ }
460
+ }
461
+ /**
462
+ * Allow-list for the V1.32 direct-dispatch endpoint. Only
463
+ * read-only reader tools are exposed here. Any write-class
464
+ * tool (commit / push / run / repo create / branch switch /
465
+ * file write) MUST go through /api/chat so the approval flow
466
+ * runs.
467
+ */
468
+ const FORGE_TOOL_DIRECT_ALLOWLIST = new Set([
469
+ 'forge.reader.open',
470
+ 'forge.reader.list_documents',
471
+ 'forge.reader.read_section',
472
+ 'forge.reader.next_block',
473
+ 'forge.reader.search',
474
+ 'forge.reader.bookmark_set',
475
+ 'forge.reader.bookmark_jump',
476
+ 'forge.reader.recap',
477
+ /* Fase F.8 -- HTML user manuals in 10 languages. Read-only,
478
+ same safety profile as the reader tools. */
479
+ 'forge.manual.open',
480
+ ]);
481
+ async function handleForgeToolDispatch(req, res, ctx) {
482
+ let body;
483
+ try {
484
+ body = JSON.parse(await readBody(req));
485
+ }
486
+ catch {
487
+ sendJson(res, 400, { ok: false, error: 'invalid JSON body' });
488
+ return;
489
+ }
490
+ if (!body || typeof body.tool !== 'string' || typeof body.args !== 'object' || body.args === null) {
491
+ sendJson(res, 400, { ok: false, error: 'tool (string) + args (object) required' });
492
+ return;
493
+ }
494
+ if (!FORGE_TOOL_DIRECT_ALLOWLIST.has(body.tool)) {
495
+ /* Loud refusal. The caller picked a tool that requires
496
+ approval flow; route them to /api/chat instead. */
497
+ sendJson(res, 403, {
498
+ ok: false,
499
+ error: 'tool not allowed via direct dispatch (use /api/chat for approval-gated tools): ' + body.tool,
500
+ });
501
+ return;
502
+ }
503
+ try {
504
+ const r = await runForgeTool(body.tool, body.args, { projectRoot: ctx.projectRoot });
505
+ sendJson(res, 200, {
506
+ ok: true,
507
+ tool: body.tool,
508
+ result: r.result,
509
+ is_error: r.is_error ?? false,
510
+ });
511
+ }
512
+ catch (err) {
513
+ sendJson(res, 500, {
514
+ ok: false,
515
+ error: err instanceof Error ? err.message : String(err),
516
+ });
517
+ }
518
+ }
519
+ /** V1.37 + V1.38 -- ingest a spec doc via drag-drop / upload.
520
+ *
521
+ * Accepts POST with:
522
+ * - Header X-Filename: the original filename (used for
523
+ * extension-based format detection + slug).
524
+ * - Body: raw binary file contents (up to INGEST_MAX_BYTES).
525
+ *
526
+ * Pipeline:
527
+ * 1. Read the binary body.
528
+ * 2. detectFormat(filename, buffer) -> auto-detects format.
529
+ * 3. parseDocument({...}) -> NormalisedDocument.
530
+ * 4. Return summary: title, format, sections count, byte
531
+ * count, language, first 3 paragraph excerpts.
532
+ *
533
+ * Does NOT persist the file or open a reader session yet --
534
+ * that's V1.39+ (plan building + scaffold). For V1.37/V1.38
535
+ * the endpoint is a single-shot parse so the panel can confirm
536
+ * "yes, Forge can read this spec" before the user commits to
537
+ * the full ingest flow.
538
+ */
539
+ const INGEST_MAX_BYTES = 32 * 1024 * 1024;
540
+ async function handleForgeIngest(req, res, ctx) {
541
+ const filenameHdr = req.headers['x-filename'];
542
+ const filename = typeof filenameHdr === 'string' && filenameHdr.trim() !== ''
543
+ ? filenameHdr.trim()
544
+ : 'spec.bin';
545
+ /* Basic filename sanitisation: forbid path separators so an
546
+ attacker cannot use this endpoint to scribble outside any
547
+ directory we might later derive from the name. */
548
+ if (filename.includes('/') || filename.includes('\\') || filename.includes('..')) {
549
+ sendJson(res, 400, {
550
+ ok: false,
551
+ error: 'X-Filename must not contain path separators or "..".',
552
+ });
553
+ return;
554
+ }
555
+ let buffer;
556
+ try {
557
+ buffer = await readBinaryBody(req, INGEST_MAX_BYTES);
558
+ }
559
+ catch (err) {
560
+ sendJson(res, 413, {
561
+ ok: false,
562
+ error: err instanceof Error ? err.message : String(err),
563
+ });
564
+ return;
565
+ }
566
+ if (buffer.length === 0) {
567
+ sendJson(res, 400, { ok: false, error: 'body is empty' });
568
+ return;
569
+ }
570
+ try {
571
+ const doc = await parseDocument({ filename, buffer });
572
+ /* V1.40 -- record the session so /extract + /plan +
573
+ /scaffold can read it later. */
574
+ recordIngest(doc);
575
+ /* Compose a compact summary the panel can render. */
576
+ const firstParas = [];
577
+ for (const s of doc.sections) {
578
+ for (const b of s.blocks) {
579
+ if (b.kind === 'paragraph') {
580
+ firstParas.push(b.text);
581
+ if (firstParas.length >= 3)
582
+ break;
583
+ }
584
+ }
585
+ if (firstParas.length >= 3)
586
+ break;
587
+ }
588
+ sendJson(res, 200, {
589
+ ok: true,
590
+ ingested: {
591
+ doc_id: doc.id,
592
+ filename: doc.filename,
593
+ format: doc.format,
594
+ bytes: doc.bytes,
595
+ title: doc.title,
596
+ language: doc.language ?? null,
597
+ sections: doc.sections.length,
598
+ first_paragraphs: firstParas,
599
+ },
600
+ });
601
+ }
602
+ catch (err) {
603
+ sendJson(res, 422, {
604
+ ok: false,
605
+ error: 'parse failed: ' + (err instanceof Error ? err.message : String(err)),
606
+ });
607
+ }
608
+ }
609
+ /** V1.40 -- run Claude extraction on a previously ingested spec. */
610
+ async function handleForgeIngestExtract(req, res, ctx) {
611
+ let body;
612
+ try {
613
+ body = JSON.parse(await readBody(req));
614
+ }
615
+ catch {
616
+ sendJson(res, 400, { ok: false, error: 'invalid JSON body' });
617
+ return;
618
+ }
619
+ if (!body || typeof body.doc_id !== 'string' || body.doc_id.trim() === '') {
620
+ sendJson(res, 400, { ok: false, error: 'doc_id (non-empty string) required' });
621
+ return;
622
+ }
623
+ const session = getIngest(body.doc_id);
624
+ if (!session) {
625
+ sendJson(res, 404, { ok: false, error: 'no ingest session for doc_id ' + body.doc_id });
626
+ return;
627
+ }
628
+ /* If the extraction was already computed and the caller did
629
+ not pass force=true, return the cached value. */
630
+ if (session.extraction && body.force !== true) {
631
+ sendJson(res, 200, {
632
+ ok: true,
633
+ doc_id: session.doc_id,
634
+ extraction: session.extraction,
635
+ cached: true,
636
+ });
637
+ return;
638
+ }
639
+ try {
640
+ const opts = {
641
+ title: session.doc.title,
642
+ text: session.flat_text,
643
+ };
644
+ if (session.doc.language)
645
+ opts.language = session.doc.language;
646
+ const extraction = await extractSpec(ctx.claude, opts);
647
+ setExtraction(session.doc_id, extraction);
648
+ sendJson(res, 200, {
649
+ ok: true,
650
+ doc_id: session.doc_id,
651
+ extraction,
652
+ cached: false,
653
+ });
654
+ }
655
+ catch (err) {
656
+ sendJson(res, 502, {
657
+ ok: false,
658
+ error: err instanceof Error ? err.message : String(err),
659
+ });
660
+ }
661
+ }
662
+ /** V1.41 -- generate a file scaffold plan from a previously
663
+ * extracted spec. Requires the extraction (V1.40) to exist;
664
+ * if not, returns a clear 409 telling the caller to run
665
+ * extract first. */
666
+ async function handleForgeIngestPlan(req, res, ctx) {
667
+ let body;
668
+ try {
669
+ body = JSON.parse(await readBody(req));
670
+ }
671
+ catch {
672
+ sendJson(res, 400, { ok: false, error: 'invalid JSON body' });
673
+ return;
674
+ }
675
+ if (!body || typeof body.doc_id !== 'string' || body.doc_id.trim() === '') {
676
+ sendJson(res, 400, { ok: false, error: 'doc_id (non-empty string) required' });
677
+ return;
678
+ }
679
+ const session = getIngest(body.doc_id);
680
+ if (!session) {
681
+ sendJson(res, 404, { ok: false, error: 'no ingest session for doc_id ' + body.doc_id });
682
+ return;
683
+ }
684
+ if (!session.extraction) {
685
+ sendJson(res, 409, {
686
+ ok: false,
687
+ error: 'no extraction yet for doc_id ' + body.doc_id
688
+ + ' -- call POST /api/forge/ingest/extract first',
689
+ });
690
+ return;
691
+ }
692
+ if (session.plan && body.force !== true) {
693
+ sendJson(res, 200, {
694
+ ok: true,
695
+ doc_id: session.doc_id,
696
+ plan: session.plan,
697
+ cached: true,
698
+ });
699
+ return;
700
+ }
701
+ try {
702
+ const opts = {
703
+ extraction: session.extraction,
704
+ title: session.doc.title,
705
+ };
706
+ if (session.doc.language)
707
+ opts.language = session.doc.language;
708
+ const plan = await generatePlan(ctx.claude, opts);
709
+ setPlan(session.doc_id, plan);
710
+ sendJson(res, 200, {
711
+ ok: true,
712
+ doc_id: session.doc_id,
713
+ plan,
714
+ cached: false,
715
+ });
716
+ }
717
+ catch (err) {
718
+ sendJson(res, 502, {
719
+ ok: false,
720
+ error: err instanceof Error ? err.message : String(err),
721
+ });
722
+ }
723
+ }
724
+ /** V1.42 + V1.43 -- approve + execute the scaffold plan.
725
+ * The approve flag is required + must be true. */
726
+ async function handleForgeIngestScaffold(req, res, ctx) {
727
+ let body;
728
+ try {
729
+ body = JSON.parse(await readBody(req));
730
+ }
731
+ catch {
732
+ sendJson(res, 400, { ok: false, error: 'invalid JSON body' });
733
+ return;
734
+ }
735
+ if (!body || typeof body.doc_id !== 'string' || body.doc_id.trim() === '') {
736
+ sendJson(res, 400, { ok: false, error: 'doc_id (non-empty string) required' });
737
+ return;
738
+ }
739
+ if (body.approve !== true) {
740
+ sendJson(res, 403, {
741
+ ok: false,
742
+ error: 'scaffold requires explicit {approve: true} in the body. This step writes files to disk; deliberate gating prevents accidental scaffolds.',
743
+ });
744
+ return;
745
+ }
746
+ const session = getIngest(body.doc_id);
747
+ if (!session) {
748
+ sendJson(res, 404, { ok: false, error: 'no ingest session for doc_id ' + body.doc_id });
749
+ return;
750
+ }
751
+ if (!session.plan) {
752
+ sendJson(res, 409, {
753
+ ok: false,
754
+ error: 'no plan yet for doc_id ' + body.doc_id
755
+ + ' -- call POST /api/forge/ingest/plan first',
756
+ });
757
+ return;
758
+ }
759
+ try {
760
+ const force = body.force === true;
761
+ const report = await executeScaffold({
762
+ plan: session.plan,
763
+ projectRoot: ctx.projectRoot,
764
+ force,
765
+ });
766
+ setScaffold(session.doc_id, report);
767
+ sendJson(res, 200, {
768
+ ok: true,
769
+ doc_id: session.doc_id,
770
+ created: report.created,
771
+ overwritten: report.overwritten,
772
+ skipped: report.skipped,
773
+ errors: report.errors,
774
+ total_writes: report.rollback_log.length,
775
+ });
776
+ }
777
+ catch (err) {
778
+ sendJson(res, 500, {
779
+ ok: false,
780
+ error: err instanceof Error ? err.message : String(err),
781
+ });
782
+ }
783
+ }
784
+ /** V1.44 -- stream scaffold progress as Server-Sent Events.
785
+ * Same approval gate as /scaffold; each ProgressEvent fires
786
+ * as a separate SSE message so the panel can update a live
787
+ * progress bar. */
788
+ async function handleForgeIngestScaffoldStream(req, res, ctx) {
789
+ let body;
790
+ try {
791
+ body = JSON.parse(await readBody(req));
792
+ }
793
+ catch {
794
+ sendJson(res, 400, { ok: false, error: 'invalid JSON body' });
795
+ return;
796
+ }
797
+ if (!body || typeof body.doc_id !== 'string' || body.doc_id.trim() === '') {
798
+ sendJson(res, 400, { ok: false, error: 'doc_id (non-empty string) required' });
799
+ return;
800
+ }
801
+ if (body.approve !== true) {
802
+ sendJson(res, 403, {
803
+ ok: false,
804
+ error: 'scaffold/stream requires explicit {approve: true} in the body.',
805
+ });
806
+ return;
807
+ }
808
+ const session = getIngest(body.doc_id);
809
+ if (!session) {
810
+ sendJson(res, 404, { ok: false, error: 'no ingest session for doc_id ' + body.doc_id });
811
+ return;
812
+ }
813
+ if (!session.plan) {
814
+ sendJson(res, 409, {
815
+ ok: false,
816
+ error: 'no plan yet for doc_id ' + body.doc_id,
817
+ });
818
+ return;
819
+ }
820
+ /* Switch to SSE response. */
821
+ res.statusCode = 200;
822
+ res.setHeader('content-type', 'text/event-stream; charset=utf-8');
823
+ res.setHeader('cache-control', 'no-cache');
824
+ res.setHeader('connection', 'keep-alive');
825
+ const force = body.force === true;
826
+ try {
827
+ const report = await executeScaffold({
828
+ plan: session.plan,
829
+ projectRoot: ctx.projectRoot,
830
+ force,
831
+ onProgress: (ev) => {
832
+ try {
833
+ res.write('event: progress\ndata: ' + JSON.stringify(ev) + '\n\n');
834
+ }
835
+ catch {
836
+ /* connection closed mid-stream -- the scaffolder
837
+ keeps writing, we just stop emitting. */
838
+ }
839
+ },
840
+ });
841
+ setScaffold(session.doc_id, report);
842
+ res.write('event: complete\ndata: ' + JSON.stringify({
843
+ doc_id: session.doc_id,
844
+ created: report.created,
845
+ overwritten: report.overwritten,
846
+ skipped: report.skipped,
847
+ errors: report.errors,
848
+ total_writes: report.rollback_log.length,
849
+ }) + '\n\n');
850
+ res.end();
851
+ }
852
+ catch (err) {
853
+ const msg = err instanceof Error ? err.message : String(err);
854
+ try {
855
+ res.write('event: error\ndata: ' + JSON.stringify({ error: msg }) + '\n\n');
856
+ }
857
+ catch { /* connection already closed */ }
858
+ res.end();
859
+ }
860
+ }
861
+ /** V1.45 -- rollback a scaffold. */
862
+ async function handleForgeIngestRollback(req, res, ctx) {
863
+ /* ctx is needed for projectRoot validation in the future; */
864
+ /* unused for now keeps the signature parallel to siblings. */
865
+ void ctx;
866
+ let body;
867
+ try {
868
+ body = JSON.parse(await readBody(req));
869
+ }
870
+ catch {
871
+ sendJson(res, 400, { ok: false, error: 'invalid JSON body' });
872
+ return;
873
+ }
874
+ if (!body || typeof body.doc_id !== 'string' || body.doc_id.trim() === '') {
875
+ sendJson(res, 400, { ok: false, error: 'doc_id (non-empty string) required' });
876
+ return;
877
+ }
878
+ const session = getIngest(body.doc_id);
879
+ if (!session) {
880
+ sendJson(res, 404, { ok: false, error: 'no ingest session for doc_id ' + body.doc_id });
881
+ return;
882
+ }
883
+ if (!session.scaffold) {
884
+ sendJson(res, 409, {
885
+ ok: false,
886
+ error: 'no scaffold to rollback for doc_id ' + body.doc_id,
887
+ });
888
+ return;
889
+ }
890
+ try {
891
+ const result = await rollbackScaffold(session.scaffold.rollback_log);
892
+ clearScaffold(session.doc_id);
893
+ sendJson(res, 200, {
894
+ ok: true,
895
+ doc_id: session.doc_id,
896
+ undone: result.undone,
897
+ errors: result.errors,
898
+ });
899
+ }
900
+ catch (err) {
901
+ sendJson(res, 500, {
902
+ ok: false,
903
+ error: err instanceof Error ? err.message : String(err),
904
+ });
905
+ }
906
+ }
907
+ async function handleVoiceTts(req, res, ctx) {
908
+ let body;
909
+ try {
910
+ body = JSON.parse(await readBody(req));
911
+ }
912
+ catch {
913
+ sendJson(res, 400, { ok: false, error: 'invalid JSON body' });
914
+ return;
915
+ }
916
+ if (!body || typeof body.text !== 'string' || body.text.trim() === '') {
917
+ sendJson(res, 400, { ok: false, error: 'text (non-empty string) required' });
918
+ return;
919
+ }
920
+ if (body.format !== undefined && !SUPPORTED_TTS_FORMATS.has(body.format)) {
921
+ sendJson(res, 400, {
922
+ ok: false,
923
+ error: 'format unsupported. Supported: ' + Array.from(SUPPORTED_TTS_FORMATS).join(', '),
924
+ });
925
+ return;
926
+ }
927
+ try {
928
+ const result = await ctx.voice.synthesize({
929
+ text: body.text,
930
+ ...(body.voice ? { voice: String(body.voice) } : {}),
931
+ ...(typeof body.speed === 'number' ? { speed: body.speed } : {}),
932
+ ...(body.language ? { language: String(body.language) } : {}),
933
+ ...(body.format ? { format: body.format } : {}),
934
+ ...(body.ssml === true ? { ssml: true } : {}),
935
+ });
936
+ res.statusCode = 200;
937
+ res.setHeader('content-type', TTS_AUDIO_MIME[result.format]);
938
+ res.setHeader('content-length', String(result.audio.length));
939
+ res.setHeader('x-voice-provider', result.provider);
940
+ res.setHeader('x-voice-voice', result.voice);
941
+ res.setHeader('x-voice-latency-ms', String(result.latency_ms));
942
+ res.end(result.audio);
943
+ }
944
+ catch (err) {
945
+ sendJson(res, 502, {
946
+ ok: false,
947
+ error: err instanceof Error ? err.message : String(err),
948
+ });
949
+ }
950
+ }
951
+ async function handleVaultList(res) {
952
+ try {
953
+ const vault = await Vault.open({ configDir: configDir() });
954
+ sendJson(res, 200, { ok: true, slots: vault.list() });
955
+ }
956
+ catch (err) {
957
+ sendJson(res, 500, {
958
+ ok: false,
959
+ error: err instanceof Error ? err.message : String(err),
960
+ });
961
+ }
962
+ }
963
+ async function handleVaultSet(req, res) {
964
+ let body;
965
+ try {
966
+ body = JSON.parse(await readBody(req));
967
+ }
968
+ catch {
969
+ sendJson(res, 400, { ok: false, error: 'invalid JSON body' });
970
+ return;
971
+ }
972
+ if (typeof body.slot !== 'string' || typeof body.plaintext !== 'string') {
973
+ sendJson(res, 400, { ok: false, error: 'slot + plaintext (both strings) required' });
974
+ return;
975
+ }
976
+ /* Optional kind / expiry validation. */
977
+ let kindOpt;
978
+ if (body.kind !== undefined) {
979
+ if (typeof body.kind !== 'string' || !SLOT_KINDS.includes(body.kind)) {
980
+ sendJson(res, 400, { ok: false, error: 'invalid kind. Allowed: ' + SLOT_KINDS.join(', ') });
981
+ return;
982
+ }
983
+ kindOpt = body.kind;
984
+ }
985
+ let expiryOpt;
986
+ if (body.expiry === null) {
987
+ expiryOpt = null;
988
+ }
989
+ else if (body.expiry !== undefined) {
990
+ if (typeof body.expiry !== 'string') {
991
+ sendJson(res, 400, { ok: false, error: 'expiry must be a string ISO 8601 or null' });
992
+ return;
993
+ }
994
+ const d = new Date(body.expiry);
995
+ if (isNaN(d.getTime())) {
996
+ sendJson(res, 400, { ok: false, error: 'expiry is not a valid ISO 8601 timestamp' });
997
+ return;
998
+ }
999
+ expiryOpt = d.toISOString();
1000
+ }
1001
+ try {
1002
+ const vault = await Vault.open({ configDir: configDir() });
1003
+ await vault.set(body.slot, body.plaintext, {
1004
+ ...(kindOpt ? { kind: kindOpt } : {}),
1005
+ ...(expiryOpt !== undefined ? { expiry: expiryOpt } : {}),
1006
+ });
1007
+ /* Echo metadata only -- never the plaintext, never the
1008
+ ciphertext. The 4-char prefix on list() is the only
1009
+ visible confirmation. */
1010
+ const meta = vault.list().find((s) => s.name === body.slot);
1011
+ sendJson(res, 200, { ok: true, slot: meta });
1012
+ }
1013
+ catch (err) {
1014
+ sendJson(res, 400, {
1015
+ ok: false,
1016
+ error: err instanceof Error ? err.message : String(err),
1017
+ });
1018
+ }
1019
+ }
1020
+ async function handleVaultCatalog(res, url) {
1021
+ const kindFilter = url.searchParams.get('kind');
1022
+ let entries = SLOT_CATALOG;
1023
+ if (kindFilter && SLOT_KINDS.includes(kindFilter)) {
1024
+ entries = entries.filter((e) => e.kind === kindFilter);
1025
+ }
1026
+ sendJson(res, 200, {
1027
+ ok: true,
1028
+ slots: entries.map((e) => ({
1029
+ name: e.name, kind: e.kind, description: e.description,
1030
+ ...(e.obtainUrl ? { obtain_url: e.obtainUrl } : {}),
1031
+ has_probe: !!e.test,
1032
+ })),
1033
+ });
1034
+ }
1035
+ async function handleModeGet(req, res, ctx) {
1036
+ const mode = await resolveModeForRequest(req, ctx.projectRoot);
1037
+ const projectResolved = await resolveMode({ projectRoot: ctx.projectRoot });
1038
+ sendJson(res, 200, {
1039
+ ok: true,
1040
+ mode,
1041
+ source: projectResolved.source,
1042
+ available: FORGE_MODES,
1043
+ });
1044
+ }
1045
+ async function handleModeSet(req, res, ctx) {
1046
+ let body;
1047
+ try {
1048
+ body = JSON.parse(await readBody(req));
1049
+ }
1050
+ catch {
1051
+ sendJson(res, 400, { ok: false, error: 'invalid JSON body' });
1052
+ return;
1053
+ }
1054
+ if (typeof body.mode !== 'string' || !FORGE_MODES.includes(body.mode)) {
1055
+ sendJson(res, 400, { ok: false, error: 'mode must be one of: ' + FORGE_MODES.join(', ') });
1056
+ return;
1057
+ }
1058
+ const mode = body.mode;
1059
+ try {
1060
+ await persistMode(ctx.projectRoot, mode);
1061
+ }
1062
+ catch (err) {
1063
+ sendJson(res, 500, { ok: false,
1064
+ error: err instanceof Error ? err.message : String(err) });
1065
+ return;
1066
+ }
1067
+ /* Set cookie so the next request renders in the new mode
1068
+ even before the user reloads. The cookie is the source of
1069
+ truth for the running browser; the persisted file is the
1070
+ source of truth for the NEXT session. */
1071
+ res.setHeader('set-cookie', 'yf-mode=' + mode + '; Path=/; Max-Age=' + (60 * 60 * 24 * 365) + '; SameSite=Lax');
1072
+ sendJson(res, 200, { ok: true, mode });
1073
+ }
1074
+ async function handleProjectsList(res) {
1075
+ const reg = await readRegistry();
1076
+ sendJson(res, 200, { ok: true, registry: reg });
1077
+ }
1078
+ async function handleProjectsActive(res) {
1079
+ const slug = await activeSlug();
1080
+ const reg = await readRegistry();
1081
+ const entry = slug ? reg.projects.find((p) => p.slug === slug) : null;
1082
+ sendJson(res, 200, { ok: true, active: slug, project: entry ?? null });
1083
+ }
1084
+ async function handleProjectsSwitch(req, res) {
1085
+ let body;
1086
+ try {
1087
+ body = JSON.parse(await readBody(req));
1088
+ }
1089
+ catch {
1090
+ sendJson(res, 400, { ok: false, error: 'invalid JSON' });
1091
+ return;
1092
+ }
1093
+ if (typeof body.slug !== 'string') {
1094
+ sendJson(res, 400, { ok: false, error: 'slug (string) required' });
1095
+ return;
1096
+ }
1097
+ try {
1098
+ const reg = await setActive(body.slug);
1099
+ sendJson(res, 200, { ok: true, active: reg.active_slug, registry: reg });
1100
+ }
1101
+ catch (err) {
1102
+ sendJson(res, 404, { ok: false,
1103
+ error: err instanceof Error ? err.message : String(err) });
1104
+ }
1105
+ }
1106
+ async function handleProjectsScan(req, res) {
1107
+ let body;
1108
+ try {
1109
+ const raw = await readBody(req);
1110
+ body = raw ? JSON.parse(raw) : {};
1111
+ }
1112
+ catch {
1113
+ sendJson(res, 400, { ok: false, error: 'invalid JSON' });
1114
+ return;
1115
+ }
1116
+ const candidates = typeof body.root === 'string'
1117
+ ? [body.root]
1118
+ : [process.env.HOME ?? '', '/tmp'].filter((s) => s.length > 0);
1119
+ let found = 0;
1120
+ for (const root of candidates) {
1121
+ const matches = await scanForProjects(root, 4);
1122
+ for (const projPath of matches) {
1123
+ const slug = await deriveSlugFromPath(projPath);
1124
+ await upsertProject({ slug, path: projPath });
1125
+ found += 1;
1126
+ }
1127
+ }
1128
+ const reg = await readRegistry();
1129
+ sendJson(res, 200, { ok: true, found, registry: reg });
1130
+ }
1131
+ async function handleProjectsAdd(req, res) {
1132
+ let body;
1133
+ try {
1134
+ body = JSON.parse(await readBody(req));
1135
+ }
1136
+ catch {
1137
+ sendJson(res, 400, { ok: false, error: 'invalid JSON' });
1138
+ return;
1139
+ }
1140
+ if (typeof body.path !== 'string') {
1141
+ sendJson(res, 400, { ok: false, error: 'path (string) required' });
1142
+ return;
1143
+ }
1144
+ const slug = await deriveSlugFromPath(body.path);
1145
+ await upsertProject({ slug, path: body.path });
1146
+ const reg = await readRegistry();
1147
+ sendJson(res, 200, { ok: true, slug, registry: reg });
1148
+ }
1149
+ async function handleTargetGet(res, ctx) {
1150
+ const r = await resolveTarget(ctx.projectRoot);
1151
+ sendJson(res, 200, {
1152
+ ok: true,
1153
+ target: r.target,
1154
+ feature_split: r.feature_split,
1155
+ available: FORGE_TARGETS,
1156
+ pending_question: r.target === null, /* Pilot uses this to know whether to prompt */
1157
+ });
1158
+ }
1159
+ async function handleTargetSet(req, res, ctx) {
1160
+ let body;
1161
+ try {
1162
+ body = JSON.parse(await readBody(req));
1163
+ }
1164
+ catch {
1165
+ sendJson(res, 400, { ok: false, error: 'invalid JSON body' });
1166
+ return;
1167
+ }
1168
+ const target = normaliseTarget(body.target);
1169
+ if (!target) {
1170
+ sendJson(res, 400, {
1171
+ ok: false, error: 'target must be one of: ' + FORGE_TARGETS.join(', '),
1172
+ });
1173
+ return;
1174
+ }
1175
+ let featureSplitArg;
1176
+ if (body.feature_split !== undefined && body.feature_split !== null) {
1177
+ if (typeof body.feature_split !== 'object') {
1178
+ sendJson(res, 400, { ok: false, error: 'feature_split must be an object' });
1179
+ return;
1180
+ }
1181
+ featureSplitArg = body.feature_split;
1182
+ }
1183
+ try {
1184
+ await persistTarget(ctx.projectRoot, target, featureSplitArg);
1185
+ }
1186
+ catch (err) {
1187
+ sendJson(res, 500, { ok: false,
1188
+ error: err instanceof Error ? err.message : String(err) });
1189
+ return;
1190
+ }
1191
+ const r = await resolveTarget(ctx.projectRoot);
1192
+ sendJson(res, 200, { ok: true, target: r.target, feature_split: r.feature_split });
1193
+ }
1194
+ async function handleVaultTest(req, res, slot) {
1195
+ let timeoutMs = 5000;
1196
+ /* Optional body for { timeout_ms }. */
1197
+ try {
1198
+ const raw = await readBody(req);
1199
+ if (raw && raw.length > 0) {
1200
+ const body = JSON.parse(raw);
1201
+ if (typeof body.timeout_ms === 'number' && body.timeout_ms > 0 && body.timeout_ms <= 30000) {
1202
+ timeoutMs = body.timeout_ms;
1203
+ }
1204
+ }
1205
+ }
1206
+ catch { /* ignore -- use default */ }
1207
+ try {
1208
+ const vault = await Vault.open({ configDir: configDir() });
1209
+ if (!vault.has(slot)) {
1210
+ sendJson(res, 404, { ok: false, error: 'slot not found: ' + slot });
1211
+ return;
1212
+ }
1213
+ const result = await vault.test(slot, { timeoutMs });
1214
+ sendJson(res, 200, { ok: true, slot, result });
1215
+ }
1216
+ catch (err) {
1217
+ sendJson(res, 500, {
1218
+ ok: false,
1219
+ error: err instanceof Error ? err.message : String(err),
1220
+ });
1221
+ }
1222
+ }
1223
+ async function handleVaultRemove(req, res) {
1224
+ let body;
1225
+ try {
1226
+ body = JSON.parse(await readBody(req));
1227
+ }
1228
+ catch {
1229
+ sendJson(res, 400, { ok: false, error: 'invalid JSON body' });
1230
+ return;
1231
+ }
1232
+ if (typeof body.slot !== 'string') {
1233
+ sendJson(res, 400, { ok: false, error: 'slot (string) required' });
1234
+ return;
1235
+ }
1236
+ try {
1237
+ const vault = await Vault.open({ configDir: configDir() });
1238
+ const removed = await vault.remove(body.slot);
1239
+ sendJson(res, 200, { ok: true, removed });
1240
+ }
1241
+ catch (err) {
1242
+ sendJson(res, 500, {
1243
+ ok: false,
1244
+ error: err instanceof Error ? err.message : String(err),
1245
+ });
1246
+ }
1247
+ }
1248
+ async function handleVaultHas(res, slot) {
1249
+ try {
1250
+ const vault = await Vault.open({ configDir: configDir() });
1251
+ sendJson(res, 200, { ok: true, slot, present: vault.has(slot) });
1252
+ }
1253
+ catch (err) {
1254
+ sendJson(res, 500, {
1255
+ ok: false,
1256
+ error: err instanceof Error ? err.message : String(err),
1257
+ });
1258
+ }
1259
+ }
1260
+ async function handleChat(req, res, ctx) {
1261
+ const raw = await readBody(req);
1262
+ let body;
1263
+ try {
1264
+ body = JSON.parse(raw);
1265
+ }
1266
+ catch {
1267
+ sendJson(res, 400, { ok: false, error: 'invalid JSON body' });
1268
+ return;
1269
+ }
1270
+ const msgs = body.messages;
1271
+ if (!Array.isArray(msgs) || msgs.length === 0) {
1272
+ sendJson(res, 400, { ok: false, error: 'messages[] required' });
1273
+ return;
1274
+ }
1275
+ const normalized = [];
1276
+ for (const m of msgs) {
1277
+ if (typeof m !== 'object' || m === null)
1278
+ continue;
1279
+ const r = m['role'];
1280
+ const c = m['content'];
1281
+ if ((r === 'user' || r === 'assistant') && typeof c === 'string') {
1282
+ normalized.push({ role: r, content: c });
1283
+ }
1284
+ }
1285
+ if (normalized.length === 0) {
1286
+ sendJson(res, 400, { ok: false, error: 'no valid user/assistant messages' });
1287
+ return;
1288
+ }
1289
+ try {
1290
+ /* Resolve mode (didactico vs tecnico) per-request -- read
1291
+ from the cookie if set, else from yujin.forge.json,
1292
+ else default. */
1293
+ const reqMode = await resolveModeForRequest(req, ctx.projectRoot);
1294
+ /* Pilot first-run setup (SQ 0.7 + 0.10). If pilot has not
1295
+ run yet, inject a welcome suffix that asks the user
1296
+ target + mode in plain prose. Also schedule auto-complete
1297
+ after the reply. */
1298
+ const pilotState = await readPilotState(ctx.projectRoot);
1299
+ const lastUserMsg = normalized.filter((m) => m.role === 'user').slice(-1)[0];
1300
+ const lastUserText = typeof lastUserMsg?.content === 'string' ? lastUserMsg.content : '';
1301
+ const reply = await ctx.claude.chat({
1302
+ messages: normalized,
1303
+ system: buildSystemPrompt(ctx, reqMode, pilotState),
1304
+ maxTokens: 1024,
1305
+ tools: FORGE_TOOL_SPECS,
1306
+ runTool: async (name, input) => {
1307
+ const r = await runForgeTool(name, input, { projectRoot: ctx.projectRoot });
1308
+ return {
1309
+ result: r.result,
1310
+ ...(r.is_error ? { is_error: true } : {}),
1311
+ };
1312
+ },
1313
+ });
1314
+ // Append the latest user message + the assistant's reply to
1315
+ // the persistent transcript. Older messages may already be
1316
+ // present from previous turns; we only need to capture the
1317
+ // *new* turn so the on-disk file matches conversation order.
1318
+ const lastUser = normalized[normalized.length - 1];
1319
+ if (lastUser && lastUser.role === 'user') {
1320
+ // Only append if this user message isn't already the last
1321
+ // recorded one (prevents dup on retry).
1322
+ const recent = ctx.store.messages();
1323
+ const tail = recent[recent.length - 1];
1324
+ if (!tail || tail.role !== 'user' || tail.content !== lastUser.content) {
1325
+ ctx.store.append(lastUser);
1326
+ }
1327
+ }
1328
+ ctx.store.append({ role: 'assistant', content: reply.text });
1329
+ void ctx.store.flush();
1330
+ /* Pilot first-run auto-complete (SQ 0.10). If Pilot is
1331
+ not yet completed, decide based on the user's first
1332
+ message whether to mark setup done with defaults. */
1333
+ if (!pilotState.pilot_completed) {
1334
+ const decision = shouldAutoComplete(pilotState, lastUserText);
1335
+ if (decision.complete) {
1336
+ try {
1337
+ await completePilotSetup(ctx.projectRoot);
1338
+ }
1339
+ catch { /* non-fatal */ }
1340
+ }
1341
+ }
1342
+ sendJson(res, 200, {
1343
+ ok: true,
1344
+ message: { role: 'assistant', text: reply.text },
1345
+ tokens: { in: reply.tokensIn, out: reply.tokensOut },
1346
+ model: reply.model,
1347
+ /* Slice 4: surface the audit trail so the panel can render
1348
+ the action trace (which tools Claude called + with what
1349
+ args + what came back). Empty array when no tools fired. */
1350
+ tool_rounds: reply.toolRounds.map((r) => ({
1351
+ tool: r.tool,
1352
+ input: r.input,
1353
+ display: typeof r.result === 'object' && r.result !== null && 'display' in r.result
1354
+ ? r.result.display
1355
+ : undefined,
1356
+ is_error: r.is_error ?? false,
1357
+ })),
1358
+ });
1359
+ }
1360
+ catch (err) {
1361
+ if (err instanceof ConfigurationError) {
1362
+ sendJson(res, 503, {
1363
+ ok: false,
1364
+ code: 'no_api_key',
1365
+ error: err.message,
1366
+ });
1367
+ return;
1368
+ }
1369
+ if (err instanceof ClaudeApiError) {
1370
+ sendJson(res, 502, {
1371
+ ok: false,
1372
+ code: 'claude_api_error',
1373
+ error: err.message,
1374
+ });
1375
+ return;
1376
+ }
1377
+ sendJson(res, 502, {
1378
+ ok: false,
1379
+ error: err instanceof Error ? err.message : String(err),
1380
+ });
1381
+ }
1382
+ }
1383
+ function buildSystemPrompt(ctx, mode = 'didactico', pilotState = {
1384
+ pilot_completed: true, target_pending: false, mode_pending: false,
1385
+ }) {
1386
+ return [
1387
+ 'You are Yujin Forge -- a friendly assistant embedded in a developer\'s React project.',
1388
+ '',
1389
+ 'PRINCIPLES:',
1390
+ '- Reply in the user\'s language. Default to Spanish if unclear.',
1391
+ '- Keep replies short + conversational.',
1392
+ '- Ask one clarifying question at a time.',
1393
+ '- When proposing code changes, paste minimal diffs the user can apply manually.',
1394
+ ' Direct AST mutation lands when the write-class tools ship.',
1395
+ '',
1396
+ 'TOOLS:',
1397
+ '- forge.read_manifest: inspect the NAC-3 manifest in the project.',
1398
+ ' Use it BEFORE asking the user what is in their app.',
1399
+ '- forge.consult_nac_spec: search docs/SPEC.md for canonical',
1400
+ ' answers about NAC-3. Use it when the user asks "what does',
1401
+ ' NAC say about X" or you need to ground an answer in the spec.',
1402
+ '- forge.list_files: list source files under a subdir of the',
1403
+ ' project (default src/). Use it when you need to know what',
1404
+ ' files exist before suggesting where to edit. Filter with',
1405
+ ' the glob arg (e.g. "*.tsx") to narrow the result.',
1406
+ '- forge.read_file: read a specific source file by relative',
1407
+ ' path. Use AFTER forge.list_files when you need the actual',
1408
+ ' contents. Refuses binary files + caps at 64KB by default.',
1409
+ '- Tool calls are silent to the user. Summarise what you found',
1410
+ ' in plain language afterwards.',
1411
+ '',
1412
+ 'CONTEXT:',
1413
+ '- Project: ' + ctx.projectName,
1414
+ '- Root: ' + ctx.projectRoot,
1415
+ '- Forge: v' + VERSION,
1416
+ modePromptSuffix(mode),
1417
+ welcomePromptSuffix(pilotState),
1418
+ ].join('\n');
1419
+ }
1420
+ function sendJson(res, status, body) {
1421
+ res.statusCode = status;
1422
+ res.setHeader('content-type', 'application/json; charset=utf-8');
1423
+ res.end(JSON.stringify(body));
1424
+ }
1425
+ /** Resolve the active Forge mode for a single request.
1426
+ * Layer order: yf-mode cookie -> yujin.forge.json -> default.
1427
+ * Stays out of server-wide state for race-safe concurrent
1428
+ * rendering, mirroring the i18n stateless pattern. */
1429
+ async function resolveModeForRequest(req, projectRoot) {
1430
+ /* 1. Cookie. */
1431
+ const cookie = req.headers.cookie ?? '';
1432
+ for (const part of cookie.split(';')) {
1433
+ const eq = part.indexOf('=');
1434
+ if (eq < 0)
1435
+ continue;
1436
+ const k = part.slice(0, eq).trim();
1437
+ if (k !== 'yf-mode')
1438
+ continue;
1439
+ const v = part.slice(eq + 1).trim();
1440
+ if (FORGE_MODES.includes(v)) {
1441
+ return v;
1442
+ }
1443
+ }
1444
+ /* 2. Project config + default (handled by resolveMode). */
1445
+ const r = await resolveMode({ projectRoot });
1446
+ return r.mode;
1447
+ }
1448
+ /** Resolve the language for the current request without
1449
+ * mutating server-wide state. Priority chain:
1450
+ * 1. ?lang=xx query string
1451
+ * 2. yf-lang cookie
1452
+ * 3. server-wide setLanguage (legacy default)
1453
+ * Unknown / unsupported codes fall back to the server-wide
1454
+ * current_language. This keeps the lang switching UX working
1455
+ * for clients without cookie support while making concurrent
1456
+ * renders race-safe. */
1457
+ function resolveRequestLang(req, url) {
1458
+ const qp = url.searchParams.get('lang');
1459
+ if (qp && SUPPORTED_LANGUAGES.includes(qp)) {
1460
+ return qp;
1461
+ }
1462
+ const cookie = req.headers.cookie ?? '';
1463
+ for (const part of cookie.split(';')) {
1464
+ const eq = part.indexOf('=');
1465
+ if (eq < 0)
1466
+ continue;
1467
+ const k = part.slice(0, eq).trim();
1468
+ if (k !== 'yf-lang')
1469
+ continue;
1470
+ const v = part.slice(eq + 1).trim();
1471
+ if (SUPPORTED_LANGUAGES.includes(v)) {
1472
+ return v;
1473
+ }
1474
+ }
1475
+ return undefined; /* defer to server-wide current_language */
1476
+ }
1477
+ /** Short hex request id -- not a UUID, no dependency. Used in
1478
+ * the x-request-id response header so client and server logs
1479
+ * can be correlated. 16 hex chars give ~64 bits of entropy --
1480
+ * way more than enough for a local dev server. */
1481
+ function generateRequestId() {
1482
+ const bytes = new Uint8Array(8);
1483
+ if (typeof globalThis.crypto !== 'undefined' && globalThis.crypto.getRandomValues) {
1484
+ globalThis.crypto.getRandomValues(bytes);
1485
+ }
1486
+ else {
1487
+ for (let i = 0; i < bytes.length; i++)
1488
+ bytes[i] = Math.floor(Math.random() * 256);
1489
+ }
1490
+ let s = '';
1491
+ for (let i = 0; i < bytes.length; i++) {
1492
+ s += bytes[i].toString(16).padStart(2, '0');
1493
+ }
1494
+ return s;
1495
+ }
1496
+ async function readBody(req) {
1497
+ const chunks = [];
1498
+ for await (const chunk of req) {
1499
+ chunks.push(chunk);
1500
+ if (chunks.reduce((s, c) => s + c.length, 0) > 1_000_000) {
1501
+ throw new Error('request body too large (>1MB)');
1502
+ }
1503
+ }
1504
+ return Buffer.concat(chunks).toString('utf-8');
1505
+ }
1506
+ /** Read a raw binary body up to maxBytes. Used by /api/voice/stt
1507
+ * where the body is the audio recording (typically a few MB).
1508
+ * Throws loudly when the cap is exceeded so the panel surfaces
1509
+ * the size issue. */
1510
+ async function readBinaryBody(req, maxBytes) {
1511
+ const chunks = [];
1512
+ let total = 0;
1513
+ for await (const chunk of req) {
1514
+ const buf = chunk;
1515
+ total += buf.length;
1516
+ if (total > maxBytes) {
1517
+ throw new Error('request body too large (>' + maxBytes + ' bytes)');
1518
+ }
1519
+ chunks.push(buf);
1520
+ }
1521
+ return Buffer.concat(chunks);
1522
+ }
1523
+ async function readProjectName(projectRoot) {
1524
+ try {
1525
+ const raw = await fs.readFile(path.join(projectRoot, 'yujin.forge.json'), 'utf-8');
1526
+ const parsed = JSON.parse(raw);
1527
+ if (parsed && typeof parsed.project_name === 'string')
1528
+ return parsed.project_name;
1529
+ }
1530
+ catch { /* fall through */ }
1531
+ try {
1532
+ const raw = await fs.readFile(path.join(projectRoot, 'package.json'), 'utf-8');
1533
+ const parsed = JSON.parse(raw);
1534
+ if (parsed && typeof parsed.name === 'string')
1535
+ return parsed.name;
1536
+ }
1537
+ catch { /* fall through */ }
1538
+ return path.basename(projectRoot);
1539
+ }
1540
+ //# sourceMappingURL=server.js.map