synabun 2.0.0 → 2.0.5

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 (334) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/NOTICE +5 -0
  3. package/README.md +1 -1
  4. package/THIRD-PARTY-LICENSES.md +46 -0
  5. package/benchmarks/fixtures/browser/README.md +1 -1
  6. package/benchmarks/fixtures/desktop/README.md +1 -1
  7. package/docs/whatsapp.md +2 -2
  8. package/hooks/claude-code/pre-compact.mjs +11 -0
  9. package/hooks/claude-code/prompt-submit.mjs +60 -184
  10. package/hooks/claude-code/session-start.mjs +73 -16
  11. package/hooks/claude-code/shared.mjs +20 -0
  12. package/hooks/claude-code/stop.mjs +5 -5
  13. package/lib/start-launcher.js +88 -1
  14. package/mcp-server/package-lock.json +2 -2
  15. package/mcp-server/package.json +1 -3
  16. package/neural-interface/i18n/en.json +1500 -153
  17. package/neural-interface/i18n/pt-BR.json +1498 -152
  18. package/neural-interface/lib/assistant-api.js +62 -6
  19. package/neural-interface/lib/assistant-brains/claude.js +4 -0
  20. package/neural-interface/lib/assistant-catalog.js +179 -27
  21. package/neural-interface/lib/assistant-config.js +31 -2
  22. package/neural-interface/lib/assistant-dispatch.js +62 -13
  23. package/neural-interface/lib/assistant-hidden-models.js +393 -66
  24. package/neural-interface/lib/assistant-memory.js +24 -10
  25. package/neural-interface/lib/assistant-persona.js +6 -2
  26. package/neural-interface/lib/assistant-pricing.js +21 -6
  27. package/neural-interface/lib/assistant-runtime.js +181 -12
  28. package/neural-interface/lib/assistant-task-prompt.js +13 -3
  29. package/neural-interface/lib/assistant-test-density.js +142 -0
  30. package/neural-interface/lib/assistant-user-context-budgets.js +123 -0
  31. package/neural-interface/lib/assistant-user-context.js +908 -0
  32. package/neural-interface/lib/assistant-user-knowledge.js +328 -0
  33. package/neural-interface/lib/assistant-user-learn.js +404 -0
  34. package/neural-interface/lib/assistant-user-threads.js +226 -0
  35. package/neural-interface/lib/assistant-user-writer.js +181 -0
  36. package/neural-interface/lib/browser-background-tab.js +134 -0
  37. package/neural-interface/lib/browser-screenshot.js +678 -61
  38. package/neural-interface/lib/claude-agent-bridge.js +33 -6
  39. package/neural-interface/lib/claude-model-catalog.js +30 -2
  40. package/neural-interface/lib/claude-temporary.js +46 -7
  41. package/neural-interface/lib/cloudflared/install.js +533 -0
  42. package/neural-interface/lib/cloudflared/manager.js +545 -0
  43. package/neural-interface/lib/cloudflared/pins.js +115 -0
  44. package/neural-interface/lib/cloudflared/pins.json +63 -0
  45. package/neural-interface/lib/cloudflared/tunnel.js +265 -0
  46. package/neural-interface/lib/editor-completion-endpoint.js +264 -0
  47. package/neural-interface/lib/editor-completion-opencode.js +436 -0
  48. package/neural-interface/lib/editor-completion.js +714 -0
  49. package/neural-interface/lib/editor-config.js +149 -0
  50. package/neural-interface/lib/editor-files-api.js +268 -0
  51. package/neural-interface/lib/editor-git-api.js +134 -0
  52. package/neural-interface/lib/file-explorer-api.js +1039 -0
  53. package/neural-interface/lib/file-explorer-system.js +355 -0
  54. package/neural-interface/lib/file-explorer-trash.ps1 +85 -0
  55. package/neural-interface/lib/file-explorer-watch.js +239 -0
  56. package/neural-interface/lib/git-diff-api.js +103 -0
  57. package/neural-interface/lib/git-read.js +202 -0
  58. package/neural-interface/lib/greeting/api.js +89 -0
  59. package/neural-interface/lib/greeting/budgets.js +90 -0
  60. package/neural-interface/lib/greeting/engine.js +461 -0
  61. package/neural-interface/lib/greeting/facts.js +320 -0
  62. package/neural-interface/lib/greeting/sources.js +252 -0
  63. package/neural-interface/lib/greeting/writer.js +139 -0
  64. package/neural-interface/lib/http-guards.js +189 -13
  65. package/neural-interface/lib/machine-names.js +88 -0
  66. package/neural-interface/lib/memory-api.js +31 -7
  67. package/neural-interface/lib/native-loop-providers.js +10 -5
  68. package/neural-interface/lib/plugin-installer.js +4 -2
  69. package/neural-interface/lib/project-storage.js +5 -7
  70. package/neural-interface/lib/request-gate.js +589 -0
  71. package/neural-interface/lib/ui-blob-api.js +108 -0
  72. package/neural-interface/lib/ui-blob-store.js +68 -0
  73. package/neural-interface/package-lock.json +701 -2
  74. package/neural-interface/package.json +13 -2
  75. package/neural-interface/public/install-app.html +9 -6
  76. package/neural-interface/public/offline.html +3 -2
  77. package/neural-interface/public/onboarding.html +12 -1
  78. package/neural-interface/public/shared/agent-runtime-options.js +22 -6
  79. package/neural-interface/public/shared/assistant/asst-automation.js +286 -0
  80. package/neural-interface/public/shared/assistant/asst-brain-picker.js +312 -62
  81. package/neural-interface/public/shared/assistant/asst-composer.js +11 -1
  82. package/neural-interface/public/shared/assistant/asst-mascot.js +18 -58
  83. package/neural-interface/public/shared/assistant/asst-menu.js +12 -8
  84. package/neural-interface/public/shared/assistant/asst-panel.js +150 -28
  85. package/neural-interface/public/shared/assistant/asst-render.js +162 -19
  86. package/neural-interface/public/shared/assistant/asst-route-models.js +108 -0
  87. package/neural-interface/public/shared/assistant/asst-route.js +23 -329
  88. package/neural-interface/public/shared/assistant/asst-routes-editor.js +495 -0
  89. package/neural-interface/public/shared/assistant/asst-sanitize.js +8 -1
  90. package/neural-interface/public/shared/assistant/asst-sidepanel.js +31 -9
  91. package/neural-interface/public/shared/assistant/asst-state.js +185 -1
  92. package/neural-interface/public/shared/assistant/asst-styles.js +158 -4
  93. package/neural-interface/public/shared/blob-store.js +111 -22
  94. package/neural-interface/public/shared/cdx/cdx-panel.js +32 -11
  95. package/neural-interface/public/shared/cdx/cdx-render.js +25 -89
  96. package/neural-interface/public/shared/cdx/cdx-styles.js +36 -0
  97. package/neural-interface/public/shared/cdx/cdx-tabs.js +87 -4
  98. package/neural-interface/public/shared/constants.js +6 -1
  99. package/neural-interface/public/shared/cp/cp-rehydrate.js +2 -1
  100. package/neural-interface/public/shared/cp/cp-styles.js +15 -0
  101. package/neural-interface/public/shared/editor/editor-ai-logic.js +409 -0
  102. package/neural-interface/public/shared/editor/editor-ai.js +619 -0
  103. package/neural-interface/public/shared/editor/editor-commands.js +212 -0
  104. package/neural-interface/public/shared/editor/editor-compare.js +103 -0
  105. package/neural-interface/public/shared/editor/editor-completion-list.js +172 -0
  106. package/neural-interface/public/shared/editor/editor-doc.js +257 -0
  107. package/neural-interface/public/shared/editor/editor-git-logic.js +131 -0
  108. package/neural-interface/public/shared/editor/editor-git.js +95 -0
  109. package/neural-interface/public/shared/editor/editor-languages.js +156 -0
  110. package/neural-interface/public/shared/editor/editor-lint.js +229 -0
  111. package/neural-interface/public/shared/editor/editor-loader.js +51 -0
  112. package/neural-interface/public/shared/editor/editor-markdown.js +90 -0
  113. package/neural-interface/public/shared/editor/editor-menu.js +233 -0
  114. package/neural-interface/public/shared/editor/editor-minimap.js +371 -0
  115. package/neural-interface/public/shared/editor/editor-palette-logic.js +192 -0
  116. package/neural-interface/public/shared/editor/editor-palette.js +125 -0
  117. package/neural-interface/public/shared/editor/editor-panel-logic.js +288 -0
  118. package/neural-interface/public/shared/editor/editor-panel.js +2099 -0
  119. package/neural-interface/public/shared/editor/editor-prefs.js +224 -0
  120. package/neural-interface/public/shared/editor/editor-safety.js +500 -0
  121. package/neural-interface/public/shared/editor/editor-search.js +352 -0
  122. package/neural-interface/public/shared/editor/editor-statusbar.js +144 -0
  123. package/neural-interface/public/shared/editor/editor-tabs-logic.js +331 -0
  124. package/neural-interface/public/shared/editor/editor-tabs.js +233 -0
  125. package/neural-interface/public/shared/editor/editor-theme.js +244 -0
  126. package/neural-interface/public/shared/editor/editor-view.js +332 -0
  127. package/neural-interface/public/shared/explorer/fx-api.js +216 -0
  128. package/neural-interface/public/shared/explorer/fx-links.js +204 -0
  129. package/neural-interface/public/shared/explorer/fx-logic.js +537 -0
  130. package/neural-interface/public/shared/explorer/fx-main.js +865 -0
  131. package/neural-interface/public/shared/explorer/fx-names.js +87 -0
  132. package/neural-interface/public/shared/explorer/fx-ops.js +370 -0
  133. package/neural-interface/public/shared/explorer/fx-popups.js +490 -0
  134. package/neural-interface/public/shared/explorer/fx-tree.js +790 -0
  135. package/neural-interface/public/shared/file-links.js +339 -0
  136. package/neural-interface/public/shared/greeting/greeting-card.js +178 -0
  137. package/neural-interface/public/shared/greeting/greeting-view.js +148 -0
  138. package/neural-interface/public/shared/html-shell.js +160 -165
  139. package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +4 -0
  140. package/neural-interface/public/shared/ocp-v2/ocp-v2-greeting.js +47 -0
  141. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +99 -17
  142. package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +7 -1
  143. package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +5 -0
  144. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +40 -0
  145. package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +3 -2
  146. package/neural-interface/public/shared/pwa-install-page.js +29 -1
  147. package/neural-interface/public/shared/pwa-install.css +3 -3
  148. package/neural-interface/public/shared/pwa-install.js +100 -1
  149. package/neural-interface/public/shared/settings/settings-code-editor-pane.js +412 -0
  150. package/neural-interface/public/shared/settings/settings-code-editor.js +328 -0
  151. package/neural-interface/public/shared/settings/settings-greeting.js +133 -0
  152. package/neural-interface/public/shared/settings/settings-ia.js +18 -4
  153. package/neural-interface/public/shared/settings/settings-user-context.js +154 -0
  154. package/neural-interface/public/shared/start-bridge.js +238 -19
  155. package/neural-interface/public/shared/styles.css +1414 -721
  156. package/neural-interface/public/shared/synabun-mascot.js +20 -56
  157. package/neural-interface/public/shared/tunnel-view.js +167 -0
  158. package/neural-interface/public/shared/ui-claude-panel.js +157 -16
  159. package/neural-interface/public/shared/ui-detail.js +15 -0
  160. package/neural-interface/public/shared/ui-explorer.js +22 -0
  161. package/neural-interface/public/shared/ui-file-explorer.js +481 -2496
  162. package/neural-interface/public/shared/ui-file-links.js +103 -0
  163. package/neural-interface/public/shared/ui-help.js +26 -0
  164. package/neural-interface/public/shared/ui-invite.js +126 -116
  165. package/neural-interface/public/shared/ui-keybinds.js +4 -0
  166. package/neural-interface/public/shared/ui-loading.js +159 -15
  167. package/neural-interface/public/shared/ui-menubar.js +12 -30
  168. package/neural-interface/public/shared/ui-native-loop-router-state.js +11 -0
  169. package/neural-interface/public/shared/ui-native-loop-router.js +15 -2
  170. package/neural-interface/public/shared/ui-navbar.js +45 -21
  171. package/neural-interface/public/shared/ui-settings.js +455 -488
  172. package/neural-interface/public/shared/ui-sidepanel-runtime.js +2 -1
  173. package/neural-interface/public/shared/ui-sync.js +16 -0
  174. package/neural-interface/public/shared/ui-terminal.js +23 -5
  175. package/neural-interface/public/shared/ui-tunnel.js +130 -0
  176. package/neural-interface/public/shared/ui-tutorial-steps.js +6 -2
  177. package/neural-interface/public/shared/ui-whiteboard.js +12 -0
  178. package/neural-interface/public/shared/ui-worker-end.js +245 -0
  179. package/neural-interface/public/shared/ui-workspaces.js +41 -27
  180. package/neural-interface/public/shared/worker-end.js +251 -0
  181. package/neural-interface/public/sw.js +2 -2
  182. package/neural-interface/public/vendor/codemirror/chunks/apl-CSDKT72Z.js +1 -0
  183. package/neural-interface/public/vendor/codemirror/chunks/asciiarmor-JYYPU5BM.js +1 -0
  184. package/neural-interface/public/vendor/codemirror/chunks/asn1-NRWCZDA6.js +1 -0
  185. package/neural-interface/public/vendor/codemirror/chunks/asterisk-5OU6D4ZQ.js +1 -0
  186. package/neural-interface/public/vendor/codemirror/chunks/brainfuck-ID4N4SYC.js +1 -0
  187. package/neural-interface/public/vendor/codemirror/chunks/chunk-4RZWGCS2.js +1 -0
  188. package/neural-interface/public/vendor/codemirror/chunks/chunk-ACTJM3JR.js +11 -0
  189. package/neural-interface/public/vendor/codemirror/chunks/chunk-S6BMAXLK.js +1 -0
  190. package/neural-interface/public/vendor/codemirror/chunks/chunk-TO2RDOHQ.js +3 -0
  191. package/neural-interface/public/vendor/codemirror/chunks/chunk-US2GMZZQ.js +13 -0
  192. package/neural-interface/public/vendor/codemirror/chunks/chunk-WYYO5ZAJ.js +1 -0
  193. package/neural-interface/public/vendor/codemirror/chunks/chunk-XALZTDJC.js +1 -0
  194. package/neural-interface/public/vendor/codemirror/chunks/chunk-YRNUTRDV.js +1 -0
  195. package/neural-interface/public/vendor/codemirror/chunks/chunk-Z3QOKH62.js +1 -0
  196. package/neural-interface/public/vendor/codemirror/chunks/clike-OWQBSOGL.js +1 -0
  197. package/neural-interface/public/vendor/codemirror/chunks/clojure-KV75UQWC.js +1 -0
  198. package/neural-interface/public/vendor/codemirror/chunks/cmake-GTEP5Y3C.js +1 -0
  199. package/neural-interface/public/vendor/codemirror/chunks/cobol-WCZ7QEGJ.js +1 -0
  200. package/neural-interface/public/vendor/codemirror/chunks/coffeescript-TGLBOT6B.js +1 -0
  201. package/neural-interface/public/vendor/codemirror/chunks/commonlisp-WVVI432V.js +1 -0
  202. package/neural-interface/public/vendor/codemirror/chunks/crystal-S6R6HLKQ.js +1 -0
  203. package/neural-interface/public/vendor/codemirror/chunks/css-PDA2RINP.js +1 -0
  204. package/neural-interface/public/vendor/codemirror/chunks/cypher-HL5QPL4C.js +1 -0
  205. package/neural-interface/public/vendor/codemirror/chunks/d-HHJH2HAJ.js +1 -0
  206. package/neural-interface/public/vendor/codemirror/chunks/diff-MN3A35BY.js +1 -0
  207. package/neural-interface/public/vendor/codemirror/chunks/dist-25SQLBVS.js +3 -0
  208. package/neural-interface/public/vendor/codemirror/chunks/dist-27ZGGJJS.js +1 -0
  209. package/neural-interface/public/vendor/codemirror/chunks/dist-4E52OJIF.js +1 -0
  210. package/neural-interface/public/vendor/codemirror/chunks/dist-6FT73XDC.js +2 -0
  211. package/neural-interface/public/vendor/codemirror/chunks/dist-BE6YGRMW.js +1 -0
  212. package/neural-interface/public/vendor/codemirror/chunks/dist-CJSGKW2O.js +1 -0
  213. package/neural-interface/public/vendor/codemirror/chunks/dist-CRPH2WLI.js +1 -0
  214. package/neural-interface/public/vendor/codemirror/chunks/dist-CTG7JEQN.js +1 -0
  215. package/neural-interface/public/vendor/codemirror/chunks/dist-I3FEVELX.js +1 -0
  216. package/neural-interface/public/vendor/codemirror/chunks/dist-IM5BBSWZ.js +1 -0
  217. package/neural-interface/public/vendor/codemirror/chunks/dist-KERUKTIF.js +1 -0
  218. package/neural-interface/public/vendor/codemirror/chunks/dist-LXTMWZAM.js +1 -0
  219. package/neural-interface/public/vendor/codemirror/chunks/dist-MABASSET.js +1 -0
  220. package/neural-interface/public/vendor/codemirror/chunks/dist-MJVBUZLL.js +3 -0
  221. package/neural-interface/public/vendor/codemirror/chunks/dist-MNB3WZVU.js +1 -0
  222. package/neural-interface/public/vendor/codemirror/chunks/dist-QYWXOE3U.js +7 -0
  223. package/neural-interface/public/vendor/codemirror/chunks/dist-TEBOFWLZ.js +1 -0
  224. package/neural-interface/public/vendor/codemirror/chunks/dist-UNXFROM3.js +1 -0
  225. package/neural-interface/public/vendor/codemirror/chunks/dist-XL2SLQO6.js +1 -0
  226. package/neural-interface/public/vendor/codemirror/chunks/dist-XO42Y3AP.js +6 -0
  227. package/neural-interface/public/vendor/codemirror/chunks/dist-Y2WWDWJK.js +1 -0
  228. package/neural-interface/public/vendor/codemirror/chunks/dist-YFEZKWMU.js +1 -0
  229. package/neural-interface/public/vendor/codemirror/chunks/dist-ZY7SWQO6.js +1 -0
  230. package/neural-interface/public/vendor/codemirror/chunks/dockerfile-3Y4R4NCH.js +1 -0
  231. package/neural-interface/public/vendor/codemirror/chunks/dtd-5343GJ4P.js +1 -0
  232. package/neural-interface/public/vendor/codemirror/chunks/dylan-FI2CXSW2.js +1 -0
  233. package/neural-interface/public/vendor/codemirror/chunks/ebnf-GNQ77YWA.js +1 -0
  234. package/neural-interface/public/vendor/codemirror/chunks/ecl-MRPUAE2L.js +1 -0
  235. package/neural-interface/public/vendor/codemirror/chunks/eiffel-DZROES27.js +1 -0
  236. package/neural-interface/public/vendor/codemirror/chunks/elm-4KICRFKQ.js +1 -0
  237. package/neural-interface/public/vendor/codemirror/chunks/erlang-3IHYC263.js +1 -0
  238. package/neural-interface/public/vendor/codemirror/chunks/factor-C6D3E5UK.js +1 -0
  239. package/neural-interface/public/vendor/codemirror/chunks/fcl-VYTWUJMI.js +1 -0
  240. package/neural-interface/public/vendor/codemirror/chunks/forth-ZBO2EH5M.js +1 -0
  241. package/neural-interface/public/vendor/codemirror/chunks/fortran-5NDCRWIK.js +1 -0
  242. package/neural-interface/public/vendor/codemirror/chunks/gas-SKRMOXFP.js +1 -0
  243. package/neural-interface/public/vendor/codemirror/chunks/gherkin-L45YXZLW.js +1 -0
  244. package/neural-interface/public/vendor/codemirror/chunks/groovy-GLPTLN44.js +1 -0
  245. package/neural-interface/public/vendor/codemirror/chunks/haskell-E7NF7P4Y.js +1 -0
  246. package/neural-interface/public/vendor/codemirror/chunks/haxe-K2NVDOB6.js +1 -0
  247. package/neural-interface/public/vendor/codemirror/chunks/http-Q6DRQIJB.js +1 -0
  248. package/neural-interface/public/vendor/codemirror/chunks/idl-MZYNJVLN.js +1 -0
  249. package/neural-interface/public/vendor/codemirror/chunks/javascript-6YRYK2HR.js +1 -0
  250. package/neural-interface/public/vendor/codemirror/chunks/julia-HXEL2T5G.js +1 -0
  251. package/neural-interface/public/vendor/codemirror/chunks/livescript-MV7CI2JZ.js +1 -0
  252. package/neural-interface/public/vendor/codemirror/chunks/lua-E36NHOOD.js +1 -0
  253. package/neural-interface/public/vendor/codemirror/chunks/mathematica-3XOZZCMC.js +1 -0
  254. package/neural-interface/public/vendor/codemirror/chunks/mbox-ODYGFKS7.js +1 -0
  255. package/neural-interface/public/vendor/codemirror/chunks/mirc-WB43AWF5.js +1 -0
  256. package/neural-interface/public/vendor/codemirror/chunks/mllike-ZT6ET7ZH.js +1 -0
  257. package/neural-interface/public/vendor/codemirror/chunks/modelica-LLMZPFGA.js +1 -0
  258. package/neural-interface/public/vendor/codemirror/chunks/mscgen-2OPNNYFH.js +1 -0
  259. package/neural-interface/public/vendor/codemirror/chunks/mumps-6P3HWJH6.js +1 -0
  260. package/neural-interface/public/vendor/codemirror/chunks/nginx-TCGKQTRW.js +1 -0
  261. package/neural-interface/public/vendor/codemirror/chunks/nsis-ATIGIXTJ.js +1 -0
  262. package/neural-interface/public/vendor/codemirror/chunks/ntriples-5PLBI5OV.js +1 -0
  263. package/neural-interface/public/vendor/codemirror/chunks/octave-QF2S3NL6.js +1 -0
  264. package/neural-interface/public/vendor/codemirror/chunks/oz-JFIVFFH2.js +1 -0
  265. package/neural-interface/public/vendor/codemirror/chunks/pascal-JG6SW76V.js +1 -0
  266. package/neural-interface/public/vendor/codemirror/chunks/perl-4B3IWVVO.js +1 -0
  267. package/neural-interface/public/vendor/codemirror/chunks/pig-ABEBH4MI.js +1 -0
  268. package/neural-interface/public/vendor/codemirror/chunks/powershell-H32XE7TL.js +1 -0
  269. package/neural-interface/public/vendor/codemirror/chunks/properties-NN644MMA.js +1 -0
  270. package/neural-interface/public/vendor/codemirror/chunks/protobuf-YKWQK2J3.js +1 -0
  271. package/neural-interface/public/vendor/codemirror/chunks/pug-RIYXEKFW.js +1 -0
  272. package/neural-interface/public/vendor/codemirror/chunks/puppet-JZQ5WPEV.js +1 -0
  273. package/neural-interface/public/vendor/codemirror/chunks/python-EBCGGHAC.js +1 -0
  274. package/neural-interface/public/vendor/codemirror/chunks/q-5UMH2YQY.js +1 -0
  275. package/neural-interface/public/vendor/codemirror/chunks/r-7XMGG453.js +1 -0
  276. package/neural-interface/public/vendor/codemirror/chunks/rpm-IEV7TSLR.js +1 -0
  277. package/neural-interface/public/vendor/codemirror/chunks/ruby-T4SPFPNM.js +1 -0
  278. package/neural-interface/public/vendor/codemirror/chunks/sas-GEDX67ET.js +1 -0
  279. package/neural-interface/public/vendor/codemirror/chunks/scheme-FTLOWKPC.js +1 -0
  280. package/neural-interface/public/vendor/codemirror/chunks/shell-HUFRKFSJ.js +1 -0
  281. package/neural-interface/public/vendor/codemirror/chunks/sieve-P5QUDVYO.js +1 -0
  282. package/neural-interface/public/vendor/codemirror/chunks/smalltalk-2SORJNQI.js +1 -0
  283. package/neural-interface/public/vendor/codemirror/chunks/solr-M22TGAR5.js +1 -0
  284. package/neural-interface/public/vendor/codemirror/chunks/sparql-4M3KL752.js +1 -0
  285. package/neural-interface/public/vendor/codemirror/chunks/spreadsheet-3K23LA4N.js +1 -0
  286. package/neural-interface/public/vendor/codemirror/chunks/sql-VHRFMUBI.js +1 -0
  287. package/neural-interface/public/vendor/codemirror/chunks/stex-QZDAQUY2.js +1 -0
  288. package/neural-interface/public/vendor/codemirror/chunks/stylus-7ZDKAKBD.js +1 -0
  289. package/neural-interface/public/vendor/codemirror/chunks/swift-WCLCLGMR.js +1 -0
  290. package/neural-interface/public/vendor/codemirror/chunks/tcl-JLMU6WOK.js +1 -0
  291. package/neural-interface/public/vendor/codemirror/chunks/textile-4CHDKZ4T.js +1 -0
  292. package/neural-interface/public/vendor/codemirror/chunks/tiddlywiki-DKMCZZQ4.js +1 -0
  293. package/neural-interface/public/vendor/codemirror/chunks/tiki-F4JIG36Q.js +1 -0
  294. package/neural-interface/public/vendor/codemirror/chunks/toml-BHMHTIM2.js +1 -0
  295. package/neural-interface/public/vendor/codemirror/chunks/troff-YQ2KV5NT.js +1 -0
  296. package/neural-interface/public/vendor/codemirror/chunks/ttcn-ZHRCVFJY.js +1 -0
  297. package/neural-interface/public/vendor/codemirror/chunks/ttcn-cfg-KJOVTY4X.js +1 -0
  298. package/neural-interface/public/vendor/codemirror/chunks/turtle-535P5N6I.js +1 -0
  299. package/neural-interface/public/vendor/codemirror/chunks/vb-76UMQHOT.js +1 -0
  300. package/neural-interface/public/vendor/codemirror/chunks/vbscript-CGNDTK6Z.js +1 -0
  301. package/neural-interface/public/vendor/codemirror/chunks/velocity-VSJDO5T5.js +1 -0
  302. package/neural-interface/public/vendor/codemirror/chunks/verilog-4T6ETXIP.js +1 -0
  303. package/neural-interface/public/vendor/codemirror/chunks/vhdl-T7FNAXUN.js +1 -0
  304. package/neural-interface/public/vendor/codemirror/chunks/webidl-P6MJSJ6J.js +1 -0
  305. package/neural-interface/public/vendor/codemirror/chunks/xquery-UOVILRGN.js +1 -0
  306. package/neural-interface/public/vendor/codemirror/chunks/yacas-CV2APROL.js +1 -0
  307. package/neural-interface/public/vendor/codemirror/chunks/z80-3KFNVM4M.js +1 -0
  308. package/neural-interface/public/vendor/codemirror/codemirror-6.43.14-75f05e67.min.js +2 -0
  309. package/neural-interface/public/vendor/codemirror/manifest.json +60 -0
  310. package/neural-interface/scripts/build-vendor.mjs +78 -2
  311. package/neural-interface/scripts/vendor-codemirror-entry.js +121 -0
  312. package/neural-interface/server.js +873 -1028
  313. package/neural-interface/templates/rulesets/README.md +1 -1
  314. package/package.json +2 -8
  315. package/setup.js +44 -7
  316. package/benchmarks/browser-live-results.json +0 -79
  317. package/benchmarks/browser-v2-results.json +0 -258
  318. package/neural-interface/lib/codex-capabilities.d.ts +0 -41
  319. package/neural-interface/public/category-logos/claude.svg +0 -1
  320. package/neural-interface/public/category-logos/gemini-color.svg +0 -1
  321. package/neural-interface/public/category-logos/openai.svg +0 -1
  322. package/neural-interface/public/category-logos/synabun.png +0 -0
  323. package/neural-interface/public/claude.svg +0 -1
  324. package/neural-interface/public/gallery.svg +0 -6
  325. package/neural-interface/public/leonardoai.svg +0 -1
  326. package/neural-interface/public/mcp.svg +0 -4
  327. package/neural-interface/public/opencode-logo-dark-square.svg +0 -1
  328. package/neural-interface/public/pin-svgrepo-com.svg +0 -25
  329. package/neural-interface/public/sessionicon.svg +0 -8
  330. package/neural-interface/public/toolbar-album.svg +0 -8
  331. package/neural-interface/public/toolbar-question.svg +0 -6
  332. package/neural-interface/public/toolbar-sessions.svg +0 -6
  333. package/neural-interface/scripts/codex-protocol-fixture.mjs +0 -83
  334. package/skills/synabun/synabunicon.png +0 -0
@@ -0,0 +1,149 @@
1
+ // ── The code editor's preferences (DATA_HOME/data/editor-config.json) ──
2
+ // Word wrap, font size, indentation and the AI autocomplete settings of the
3
+ // File Explorer's editor. The defaults and the normaliser are the client's own
4
+ // (public/shared/editor/editor-prefs.js): one definition, so the file on disk
5
+ // and the page can never disagree about what a valid configuration is. Unknown
6
+ // keys are dropped and out-of-range values clamped on the way in and on the
7
+ // way out.
8
+ //
9
+ // The endpoint API key of AI autocomplete is not kept here: it is
10
+ // SYNABUN_EDITOR_AI_KEY in the environment, and only whether one is set
11
+ // (`hasEndpointKey`) ever reaches the page.
12
+
13
+ import { Router } from 'express';
14
+ import { mkdirSync, readFileSync, statSync } from 'node:fs';
15
+ import { dirname } from 'node:path';
16
+ import { replaceFileAtomic } from './confined-fs.js';
17
+ import { defaultEditorConfig, mergeEditorConfig, normalizeEditorConfig } from '../public/shared/editor/editor-prefs.js';
18
+
19
+ export { defaultEditorConfig, normalizeEditorConfig };
20
+ export const EDITOR_CONFIG_SYNC_TYPE = 'editor:config-changed';
21
+ /** What these routes add to the editor (GET /api/editor/capabilities). */
22
+ export const EDITOR_CONFIG_CAPABILITIES = Object.freeze(['config']);
23
+
24
+ /**
25
+ * The stored configuration. Read again when the file's modification time changes; written atomically.
26
+ * @param {{ path: string, log?: (tag: string, message: string) => void }} o
27
+ */
28
+ export function createEditorConfigStore({ path, log = () => {} } = {}) {
29
+ if (!path) throw new Error('createEditorConfigStore requires path');
30
+ let cached = null; // { mtimeMs, config }
31
+
32
+ const mtime = () => { try { return statSync(path).mtimeMs; } catch { return 0; } };
33
+
34
+ /** The configuration in effect: the file's, normalised, or the defaults when there is no (readable) file. */
35
+ function read() {
36
+ const m = mtime();
37
+ if (cached && cached.mtimeMs === m) return cached.config;
38
+ let config = defaultEditorConfig();
39
+ if (m) {
40
+ try { config = normalizeEditorConfig(JSON.parse(readFileSync(path, 'utf8'))); }
41
+ catch (error) { log('editor-config:unreadable', error?.message || String(error)); }
42
+ }
43
+ cached = { mtimeMs: m, config };
44
+ return config;
45
+ }
46
+
47
+ /**
48
+ * Apply a partial change and save.
49
+ * @returns {{ config: object, changed: boolean }}
50
+ */
51
+ function update(partial) {
52
+ const current = read();
53
+ const config = mergeEditorConfig(current, partial);
54
+ const changed = JSON.stringify(config) !== JSON.stringify(current);
55
+ if (changed) {
56
+ mkdirSync(dirname(path), { recursive: true });
57
+ replaceFileAtomic(path, `${JSON.stringify(config, null, 2)}\n`);
58
+ cached = { mtimeMs: mtime(), config };
59
+ }
60
+ return { config, changed };
61
+ }
62
+
63
+ return { read, update, path };
64
+ }
65
+
66
+ export const ENDPOINT_KEY_NAME = 'SYNABUN_EDITOR_AI_KEY';
67
+ // A bearer token as providers issue them. Nothing that could start a new line or a comment in a .env file.
68
+ const ENDPOINT_KEY_FORMAT = /^[A-Za-z0-9._~+/=:@-]{1,500}$/;
69
+
70
+ /**
71
+ * The API key of AI autocomplete's endpoint: SYNABUN_EDITOR_AI_KEY in the data home's .env, beside the other
72
+ * secrets, and in the environment of the running server. Written the way the TypeSafe key is (read the file,
73
+ * change one name, write the file).
74
+ * @param {object} deps
75
+ * @param {string} deps.envPath the .env file
76
+ * @param {(path: string) => object} deps.parseEnvFile server.js's reader
77
+ * @param {(path: string, vars: object) => void} deps.writeEnvFile server.js's writer
78
+ * @param {object} [deps.env] the running environment (process.env)
79
+ */
80
+ export function createEndpointKeyStore({ envPath, parseEnvFile, writeEnvFile, env = process.env } = {}) {
81
+ if (!envPath || typeof parseEnvFile !== 'function' || typeof writeEnvFile !== 'function') {
82
+ throw new Error('createEndpointKeyStore requires envPath, parseEnvFile and writeEnvFile');
83
+ }
84
+ const get = () => String(env[ENDPOINT_KEY_NAME] || '').trim();
85
+ return {
86
+ get,
87
+ has: () => !!get(),
88
+ /** Store a key, or remove it with ''. Throws on a value that is not a key. */
89
+ set(value) {
90
+ if (typeof value !== 'string') throw new Error('The endpoint key must be text');
91
+ const key = value.trim();
92
+ if (key && !ENDPOINT_KEY_FORMAT.test(key)) throw new Error('The endpoint key has an unexpected format');
93
+ const vars = parseEnvFile(envPath);
94
+ if (key) { vars[ENDPOINT_KEY_NAME] = key; env[ENDPOINT_KEY_NAME] = key; }
95
+ else { delete vars[ENDPOINT_KEY_NAME]; delete env[ENDPOINT_KEY_NAME]; }
96
+ writeEnvFile(envPath, vars);
97
+ return !!key;
98
+ },
99
+ };
100
+ }
101
+
102
+ /**
103
+ * GET / → { ok, config, hasEndpointKey }
104
+ * PUT / (a partial configuration) → the same, after the change; other windows are told when it changed anything.
105
+ * `endpointKey` in the body stores the endpoint's API key ('' removes it); it is never part of an answer.
106
+ * Mounted at /api/editor/config. The owner's only: `guard` refuses everybody else on the router itself.
107
+ * @param {object} deps
108
+ * @param {{ read: () => object, update: (partial: object) => { config: object, changed: boolean } }} deps.store
109
+ * @param {(message: object) => void} [deps.broadcastSync]
110
+ * @param {() => boolean} [deps.hasEndpointKey]
111
+ * @param {(key: string) => void} [deps.setEndpointKey] stores or removes the key; without it a key cannot be saved
112
+ * @param {(req: object, res: object, next: () => void) => void} [deps.guard] who may use these routes
113
+ * (lib/http-guards.js ownerRouteGuard); without one, anybody who reaches them
114
+ */
115
+ export function createEditorConfigRouter({ store, broadcastSync = () => {}, hasEndpointKey = () => false, setEndpointKey = null, guard = null } = {}) {
116
+ if (!store) throw new Error('createEditorConfigRouter requires a store');
117
+ const router = Router();
118
+ if (typeof guard === 'function') router.use(guard);
119
+ const view = (config) => ({ ok: true, config, hasEndpointKey: !!hasEndpointKey() });
120
+
121
+ router.get('/', (req, res) => {
122
+ try { res.json(view(store.read())); }
123
+ catch (error) { res.status(500).json({ ok: false, error: error.message }); }
124
+ });
125
+
126
+ router.put('/', (req, res) => {
127
+ if (!req.body || typeof req.body !== 'object' || Array.isArray(req.body)) {
128
+ return res.status(400).json({ ok: false, error: 'The body must be an object holding the preferences to change' });
129
+ }
130
+ const { endpointKey, ...partial } = req.body;
131
+ try {
132
+ let keyChanged = false;
133
+ if (endpointKey !== undefined) {
134
+ if (typeof setEndpointKey !== 'function') return res.status(400).json({ ok: false, error: 'Key storage is not wired in this server' });
135
+ const had = !!hasEndpointKey();
136
+ try { setEndpointKey(endpointKey); }
137
+ catch (error) { return res.status(400).json({ ok: false, error: error.message }); }
138
+ keyChanged = had !== !!hasEndpointKey();
139
+ }
140
+ const { config, changed } = store.update(partial);
141
+ if (changed || keyChanged) broadcastSync({ type: EDITOR_CONFIG_SYNC_TYPE, config, hasEndpointKey: !!hasEndpointKey() });
142
+ res.json(view(config));
143
+ } catch (error) {
144
+ res.status(500).json({ ok: false, error: error.message });
145
+ }
146
+ });
147
+
148
+ return router;
149
+ }
@@ -0,0 +1,268 @@
1
+ // ── The code editor's file routes ──
2
+ // Reading, writing, creating and removing the files the File Explorer's editor
3
+ // works on, plus what the editor asks the server before it uses anything newer
4
+ // than read and write (GET /api/editor/capabilities).
5
+ //
6
+ // A path is allowed when it is inside one of the roots the server registers:
7
+ // the data home, the registered projects, the plan folders, and one single
8
+ // file, the user's OpenCode configuration. "Inside" is judged as written first
9
+ // (403 "Path outside registered project roots"), then as the filesystem
10
+ // resolves it: every read and write goes through lib/confined-fs.js, so a link
11
+ // that leads out of its root is refused instead of followed.
12
+ //
13
+ // A read reports the file's version ({ mtimeMs, size }); a write that sends it
14
+ // back as `expect` is refused with 409 when the file changed in between. A
15
+ // write of a file that is expected not to be there (`expectAbsent`, with
16
+ // `create`) is refused the same way when it is: it is created exclusively.
17
+ //
18
+ // Who may ask is each route's own business: the `guard` it is handed (server.js:
19
+ // lib/http-guards.js ownerRouteGuard) refuses invite guests, a Host that is not
20
+ // this server's and a write from another site's page, on the route itself.
21
+ // Dependencies are injected: the router is tested without server.js.
22
+
23
+ import { Router } from 'express';
24
+ import { lstatSync, mkdirSync, readdirSync, rmdirSync, statSync } from 'node:fs';
25
+ import { basename, dirname, join, resolve, sep } from 'node:path';
26
+ import { PathConfineError, realPathOf } from './path-confine.js';
27
+ import { MAX_FILE_BYTES, canonicalInside, makeConfinedDir, readConfinedFile, removeConfinedEntry, writeConfinedFile } from './confined-fs.js';
28
+
29
+ /** What these routes add to the editor: the version check on save, and POST /api/file-stat. */
30
+ export const EDITOR_FILE_CAPABILITIES = Object.freeze(['file-version', 'file-stat']);
31
+ export const MAX_STAT_PATHS = 50;
32
+ /**
33
+ * The namespace of the UI blob store (lib/ui-blob-store.js, /api/ui-blobs) that holds the editor's copies of
34
+ * unsaved buffers. server.js allows it and keeps it for the owner; it announces `backups` when it does.
35
+ */
36
+ export const EDITOR_BACKUP_NAMESPACE = 'editor-backups';
37
+ export const EDITOR_BACKUP_CAPABILITIES = Object.freeze(['backups']);
38
+
39
+ const OUTSIDE_ROOTS = 'Path outside registered project roots';
40
+ const BINARY_PROBE_BYTES = 8192;
41
+ const UTF8_BOM = [0xEF, 0xBB, 0xBF];
42
+
43
+ const outsideRoots = () => new PathConfineError(403, OUTSIDE_ROOTS);
44
+ const slashes = (path) => path.replace(/\\/g, '/');
45
+ const versionOf = (st) => ({ mtimeMs: st.mtimeMs, size: st.size });
46
+
47
+ /**
48
+ * The roots a path is inside, as written.
49
+ * @param {string} path any path a request sent
50
+ * @param {{ dirs?: string[], files?: string[] }} roots folders, and single files allowed by their exact path
51
+ * @returns {{ target: string, matches: { base: string, path: string, isBase: boolean }[] }} `target` is the path
52
+ * resolved; each match is the folder to confine to and the path to use inside it. A single file is confined to
53
+ * the folder it really is in: it is allowed by name, wherever a link takes it.
54
+ */
55
+ export function matchFileRoots(path, roots = {}) {
56
+ const target = resolve(String(path));
57
+ const matches = [];
58
+ for (const dir of roots.dirs || []) {
59
+ const base = resolve(dir);
60
+ if (target === base) matches.push({ base, path: target, isBase: true });
61
+ else if (target.startsWith(base.endsWith(sep) ? base : base + sep)) matches.push({ base, path: target, isBase: false });
62
+ }
63
+ for (const file of roots.files || []) {
64
+ if (target !== resolve(file)) continue;
65
+ try {
66
+ const real = realPathOf(target);
67
+ matches.push({ base: dirname(real), path: real, isBase: false });
68
+ } catch { /* a link that cannot be resolved: not a file to open */ }
69
+ }
70
+ return { target, matches };
71
+ }
72
+
73
+ /**
74
+ * Run `use(match)` on the first root that really holds the path. A root the path leaves through a link is passed
75
+ * over; when every root is, the path is refused (403), as a path outside every root is.
76
+ * @param {string} path any path a request sent
77
+ * @param {{ dirs?: string[], files?: string[] }} roots
78
+ * @param {(match: { base: string, path: string, isBase: boolean, target: string }) => any} use
79
+ */
80
+ export function useFileRoot(path, roots, use) {
81
+ const { target, matches } = matchFileRoots(path, roots);
82
+ if (!matches.length) throw outsideRoots();
83
+ let refused = null;
84
+ for (const match of matches) {
85
+ try { return use({ ...match, target }); }
86
+ catch (error) {
87
+ if (!(error instanceof PathConfineError) || error.status !== 403) throw error;
88
+ refused = error;
89
+ }
90
+ }
91
+ throw refused;
92
+ }
93
+
94
+ /**
95
+ * @param {object} deps
96
+ * @param {() => { dirs: string[], files: string[] }} deps.fileRoots the roots validateProjectPath() allows
97
+ * @param {() => string[]} [deps.capabilities] what other editor routes of this server add (GET /api/editor/capabilities)
98
+ * @param {number} [deps.maxBytes] the largest file that is read
99
+ * @param {(req: object, res: object, next: () => void) => void} [deps.guard] who may use these routes; without
100
+ * one, anybody who reaches them
101
+ */
102
+ export function createEditorFilesRouter({ fileRoots, capabilities = () => [], maxBytes = MAX_FILE_BYTES, guard = null } = {}) {
103
+ if (typeof fileRoots !== 'function') throw new Error('createEditorFilesRouter requires fileRoots');
104
+ const router = Router();
105
+ // On each route, not on the router: it is mounted at the root, where a check of its own would run for the
106
+ // paths of every route registered after it.
107
+ const owner = typeof guard === 'function' ? guard : (req, res, next) => next();
108
+
109
+ const fail = (res, error) => {
110
+ if (error instanceof PathConfineError) return res.status(error.status).json({ error: error.message });
111
+ return res.status(500).json({ error: error?.message || String(error) });
112
+ };
113
+
114
+ const inRoots = (path, use) => useFileRoot(path, fileRoots(), use);
115
+
116
+ // The canonical path of a match, and what is there (null when nothing is).
117
+ function locate(match) {
118
+ const real = canonicalInside(match.base, match.path, { allowBase: true });
119
+ let st = null;
120
+ try { st = statSync(real); } catch { st = null; }
121
+ return { real, st };
122
+ }
123
+
124
+ // GET /api/file-content — read one file for the editor
125
+ router.get('/api/file-content', owner, (req, res) => {
126
+ try {
127
+ const filePath = req.query.path;
128
+ if (!filePath || typeof filePath !== 'string') return res.status(400).json({ error: 'Missing path parameter' });
129
+ const found = inRoots(filePath, (match) => {
130
+ let { real, st } = locate(match);
131
+ if (!st || !st.isFile()) return { status: 400, body: { error: 'Not a valid file' } };
132
+ if (st.size > maxBytes) {
133
+ return { status: 413, body: { error: `File too large (the editor opens up to ${Math.round(maxBytes / (1024 * 1024))} MB)`, maxBytes } };
134
+ }
135
+ let buf = readConfinedFile(match.base, real, { maxBytes });
136
+ // The version must be the one of the bytes that were read: a write in between is read again.
137
+ let after = statSync(real);
138
+ if (after.mtimeMs !== st.mtimeMs || after.size !== st.size) {
139
+ buf = readConfinedFile(match.base, real, { maxBytes });
140
+ after = statSync(real);
141
+ }
142
+ st = after;
143
+ const probe = Math.min(BINARY_PROBE_BYTES, buf.length);
144
+ for (let i = 0; i < probe; i++) {
145
+ if (buf[i] === 0x00) return { status: 415, body: { error: 'Binary file', reason: 'binary' } };
146
+ }
147
+ const bom = buf.length >= 3 && UTF8_BOM.every((byte, i) => buf[i] === byte);
148
+ let content;
149
+ try { content = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bom ? buf.subarray(3) : buf); }
150
+ catch { return { status: 415, body: { error: 'File is not valid UTF-8 text', reason: 'encoding' } }; }
151
+ return {
152
+ status: 200,
153
+ body: { ok: true, content, path: slashes(match.target), name: basename(match.target), size: st.size, version: versionOf(st), bom },
154
+ };
155
+ });
156
+ res.status(found.status).json(found.body);
157
+ } catch (error) { fail(res, error); }
158
+ });
159
+
160
+ // POST /api/file-content — write a file (an existing one, or a new one with `create: true`)
161
+ // expect: { mtimeMs, size } the version that was read: 409 { code: 'changed', current } when the file differs
162
+ // expectAbsent: true with `create`: the file is expected not to be there (the editor saving a tab whose
163
+ // file was deleted): 409 { code: 'changed', current } when it is
164
+ // force: true write whatever is there ("Overwrite")
165
+ router.post('/api/file-content', owner, (req, res) => {
166
+ try {
167
+ const { path: filePath, content, create, expect, expectAbsent, force } = req.body || {};
168
+ if (!filePath || typeof filePath !== 'string' || typeof content !== 'string') {
169
+ return res.status(400).json({ error: 'Missing path or content' });
170
+ }
171
+ const checked = expect !== undefined && expect !== null;
172
+ if (checked && !(Number.isFinite(expect?.mtimeMs) && Number.isFinite(expect?.size))) {
173
+ return res.status(400).json({ error: 'expect must be the version a read returned: { mtimeMs, size }' });
174
+ }
175
+ const absent = expectAbsent === true && !force;
176
+ const changed = (st) => ({ status: 409, body: { error: 'The file changed on disk', code: 'changed', current: versionOf(st) } });
177
+ const done = inRoots(filePath, (match) => {
178
+ const { real, st } = locate(match);
179
+ if (st && !st.isFile()) return { status: 400, body: { error: create ? 'Not a valid file' : 'File does not exist' } };
180
+ if (!st && !create) return { status: 400, body: { error: 'File does not exist' } };
181
+ if (st && absent) return changed(st);
182
+ if (st && checked && !force && (st.mtimeMs !== expect.mtimeMs || st.size !== expect.size)) return changed(st);
183
+ // In place: an existing file keeps its mode and its inode, a link to it stays a link. A file that is
184
+ // expected to be absent is created exclusively: one that appeared since the look above is not written over.
185
+ try {
186
+ writeConfinedFile(match.base, real, content, { exclusive: absent });
187
+ } catch (error) {
188
+ if (!(absent && error instanceof PathConfineError && error.status === 409)) throw error;
189
+ let now = null;
190
+ try { now = statSync(real); } catch { now = null; }
191
+ if (now) return changed(now);
192
+ throw error;
193
+ }
194
+ return { status: 200, body: { ok: true, version: versionOf(statSync(real)) } };
195
+ });
196
+ res.status(done.status).json(done.body);
197
+ } catch (error) { fail(res, error); }
198
+ });
199
+
200
+ // POST /api/file-mkdir — create a directory
201
+ router.post('/api/file-mkdir', owner, (req, res) => {
202
+ try {
203
+ const { path: dirPath } = req.body || {};
204
+ if (!dirPath || typeof dirPath !== 'string') return res.status(400).json({ error: 'Missing path' });
205
+ inRoots(dirPath, (match) => {
206
+ if (match.isBase) mkdirSync(match.path, { recursive: true });
207
+ else makeConfinedDir(match.base, match.path);
208
+ });
209
+ res.json({ ok: true });
210
+ } catch (error) { fail(res, error); }
211
+ });
212
+
213
+ // DELETE /api/file-content — delete a file or an empty directory
214
+ router.delete('/api/file-content', owner, (req, res) => {
215
+ try {
216
+ const filePath = req.query.path;
217
+ if (!filePath || typeof filePath !== 'string') return res.status(400).json({ error: 'Missing path parameter' });
218
+ const done = inRoots(filePath, (match) => {
219
+ if (match.isBase) return { status: 403, body: { error: 'A registered folder is not deleted from here' } };
220
+ // The folder that holds the entry must really be in the root; the entry itself is removed as it is
221
+ // (a link is unlinked, never followed).
222
+ const parent = canonicalInside(match.base, dirname(match.path), { allowBase: true });
223
+ const entry = join(parent, basename(match.path));
224
+ let st;
225
+ try { st = lstatSync(entry); } catch { return { status: 404, body: { error: 'Path does not exist' } }; }
226
+ if (st.isDirectory()) {
227
+ if (readdirSync(entry).length > 0) return { status: 400, body: { error: 'Directory is not empty' } };
228
+ rmdirSync(entry);
229
+ } else {
230
+ removeConfinedEntry(match.base, entry);
231
+ }
232
+ return { status: 200, body: { ok: true } };
233
+ });
234
+ res.status(done.status).json(done.body);
235
+ } catch (error) { fail(res, error); }
236
+ });
237
+
238
+ // POST /api/file-stat — the versions of up to 50 files, to see which changed on disk
239
+ router.post('/api/file-stat', owner, (req, res) => {
240
+ try {
241
+ const paths = req.body?.paths;
242
+ if (!Array.isArray(paths) || paths.length > MAX_STAT_PATHS || paths.some((p) => typeof p !== 'string' || !p)) {
243
+ return res.status(400).json({ error: `paths must be an array of at most ${MAX_STAT_PATHS} file paths` });
244
+ }
245
+ const files = {};
246
+ for (const path of paths) {
247
+ let entry = { exists: false };
248
+ try {
249
+ entry = inRoots(path, (match) => {
250
+ const { st } = locate(match);
251
+ return st && st.isFile() ? { exists: true, version: versionOf(st) } : { exists: false };
252
+ });
253
+ } catch { /* outside the roots, or out of one through a link: not a file the editor has */ }
254
+ files[path] = entry;
255
+ }
256
+ res.json({ ok: true, files });
257
+ } catch (error) { fail(res, error); }
258
+ });
259
+
260
+ // GET /api/editor/capabilities — what this server offers the editor beyond reading and writing
261
+ router.get('/api/editor/capabilities', owner, (req, res) => {
262
+ let extra = [];
263
+ try { extra = (capabilities() || []).filter((name) => typeof name === 'string'); } catch { extra = []; }
264
+ res.json({ ok: true, capabilities: [...new Set([...EDITOR_FILE_CAPABILITIES, ...extra])] });
265
+ });
266
+
267
+ return router;
268
+ }
@@ -0,0 +1,134 @@
1
+ // ── The code editor's view of git: the committed version of a file ──
2
+ // GET /api/file-head?path= answers with the text of a file as the last commit
3
+ // has it (`git show HEAD:<path>`). The editor marks the lines that differ from
4
+ // it and shows the two against each other.
5
+ //
6
+ // The path is checked like every other file path of the editor (inside a
7
+ // registered root as written, and as the filesystem resolves it). git is run
8
+ // with an argument array, never through a shell: a file name is a name,
9
+ // whatever characters it holds. Only commands that read are run here:
10
+ // `rev-parse --show-toplevel` to find the repository, `show` for the text.
11
+ // Nothing in the repository, its index or its working tree is changed, and
12
+ // nothing the repository's own configuration names is started: every call of
13
+ // runGit() is a read as lib/git-read.js runs one (no file system monitor
14
+ // program, no hook, no external diff or textconv, no fetch).
15
+ //
16
+ // A file git does not know, a folder that is not a repository and a machine
17
+ // without git all answer `tracked: false`: for the editor that is "no markers",
18
+ // not an error. Dependencies are injected: the router is tested without server.js.
19
+
20
+ import { Router } from 'express';
21
+ import { execFile } from 'node:child_process';
22
+ import { statSync } from 'node:fs';
23
+ import { dirname, isAbsolute, relative, sep } from 'node:path';
24
+ import { PathConfineError } from './path-confine.js';
25
+ import { MAX_FILE_BYTES, canonicalInside } from './confined-fs.js';
26
+ import { useFileRoot } from './editor-files-api.js';
27
+ import { gitRead } from './git-read.js';
28
+
29
+ /** What this route adds to the editor (GET /api/editor/capabilities). */
30
+ export const EDITOR_GIT_CAPABILITIES = Object.freeze(['git-head']);
31
+ export const GIT_TIMEOUT_MS = 5000;
32
+
33
+ const UTF8_BOM = [0xEF, 0xBB, 0xBF];
34
+ const BINARY_PROBE_BYTES = 8192;
35
+ // Variables that would point git at another repository than the one the file is in.
36
+ const REDIRECTING = ['GIT_DIR', 'GIT_WORK_TREE', 'GIT_INDEX_FILE', 'GIT_NAMESPACE', 'GIT_COMMON_DIR', 'GIT_OBJECT_DIRECTORY', 'GIT_ALTERNATE_OBJECT_DIRECTORIES', 'GIT_CEILING_DIRECTORIES'];
37
+
38
+ /**
39
+ * Run git for a read and collect what it prints. Never throws. The arguments are the command's own: what keeps a
40
+ * read from starting a program of the repository's is added here (lib/git-read.js).
41
+ * @param {string[]} args the command first
42
+ * @param {{ cwd: string, maxBuffer: number, timeoutMs?: number, binary?: string, env?: object }} o
43
+ * @returns {Promise<{ ok: boolean, stdout: Buffer, tooLarge: boolean }>} `tooLarge`: it printed more than `maxBuffer` bytes
44
+ */
45
+ export function runGit(args, { cwd, maxBuffer, timeoutMs = GIT_TIMEOUT_MS, binary = 'git', env = process.env } = {}) {
46
+ const clean = { ...env };
47
+ for (const name of REDIRECTING) delete clean[name];
48
+ const failed = { ok: false, stdout: Buffer.alloc(0), tooLarge: false };
49
+ return gitRead(cwd, args, { binary, env: clean }).then((read) => new Promise((done) => {
50
+ try {
51
+ execFile(binary, read.args, { cwd, maxBuffer, timeout: timeoutMs, encoding: 'buffer', windowsHide: true, env: read.env, shell: false }, (error, stdout) => {
52
+ const out = Buffer.isBuffer(stdout) ? stdout : Buffer.alloc(0);
53
+ if (!error) done({ ok: true, stdout: out, tooLarge: false });
54
+ else done({ ok: false, stdout: out, tooLarge: error.code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER' });
55
+ });
56
+ } catch {
57
+ done(failed);
58
+ }
59
+ })).catch(() => failed);
60
+ }
61
+
62
+ /**
63
+ * The committed text of one file.
64
+ * @param {string} real the file's canonical path (it may be gone from the disk)
65
+ * @param {{ maxBytes: number, git: (args: string[], o: object) => Promise<object> }} o
66
+ * @returns {Promise<{ tracked: boolean, content?: string, binary?: boolean, tooLarge?: boolean }>}
67
+ */
68
+ export async function readHeadVersion(real, { maxBytes, git }) {
69
+ const folder = dirname(real);
70
+ const top = await git(['rev-parse', '--show-toplevel'], { cwd: folder, maxBuffer: 64 * 1024 });
71
+ if (!top.ok) return { tracked: false };
72
+ const root = top.stdout.toString('utf8').replace(/\r?\n$/, '');
73
+ if (!root || !isAbsolute(root)) return { tracked: false };
74
+ // The file's name inside the repository, as git writes it. Both paths are canonical: git prints its top level
75
+ // resolved, and `real` is.
76
+ const inside = relative(root, real);
77
+ if (!inside || inside.startsWith('..') || isAbsolute(inside)) return { tracked: false };
78
+ const name = sep === '/' ? inside : inside.split(sep).join('/');
79
+ const shown = await git(['show', `HEAD:${name}`], { cwd: root, maxBuffer: maxBytes });
80
+ if (shown.tooLarge) return { tracked: true, tooLarge: true };
81
+ if (!shown.ok) return { tracked: false }; // not in the last commit, or no commit yet
82
+ const buf = shown.stdout;
83
+ const probe = Math.min(BINARY_PROBE_BYTES, buf.length);
84
+ for (let i = 0; i < probe; i++) if (buf[i] === 0x00) return { tracked: true, binary: true };
85
+ const bom = buf.length >= 3 && UTF8_BOM.every((byte, i) => buf[i] === byte);
86
+ try { return { tracked: true, content: new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bom ? buf.subarray(3) : buf) }; }
87
+ catch { return { tracked: true, binary: true }; }
88
+ }
89
+
90
+ /**
91
+ * GET /api/file-head?path=
92
+ * 200 { ok, tracked: false } git does not know the file, the folder is no repository, or there is no git
93
+ * 200 { ok, tracked: true, content } the file as HEAD has it: its own line endings, a byte-order mark taken off
94
+ * 200 { ok, tracked: true, binary: true } HEAD holds something that is not UTF-8 text
95
+ * 400 no path, or a folder · 403 outside the registered roots, or out of one through a link · 413 larger than `maxBytes`
96
+ * @param {object} deps
97
+ * @param {() => { dirs: string[], files: string[] }} deps.fileRoots the roots the editor's file routes allow
98
+ * @param {number} [deps.maxBytes]
99
+ * @param {(args: string[], o: object) => Promise<object>} [deps.git] runGit, or a stand-in
100
+ * @param {(req: object, res: object, next: () => void) => void} [deps.guard] who may use the route (the files
101
+ * router's own: lib/http-guards.js ownerRouteGuard); on the route itself, as the router is mounted at the root
102
+ */
103
+ export function createEditorGitRouter({ fileRoots, maxBytes = MAX_FILE_BYTES, git = runGit, guard = null } = {}) {
104
+ if (typeof fileRoots !== 'function') throw new Error('createEditorGitRouter requires fileRoots');
105
+ const router = Router();
106
+ const owner = typeof guard === 'function' ? guard : (req, res, next) => next();
107
+ const tooLarge = () => ({ status: 413, body: { error: `File too large (the editor opens up to ${Math.max(1, Math.round(maxBytes / (1024 * 1024)))} MB)`, maxBytes } });
108
+
109
+ router.get('/api/file-head', owner, async (req, res) => {
110
+ try {
111
+ const filePath = req.query.path;
112
+ if (!filePath || typeof filePath !== 'string') return res.status(400).json({ error: 'Missing path parameter' });
113
+ const found = useFileRoot(filePath, fileRoots(), (match) => {
114
+ const real = canonicalInside(match.base, match.path, { allowBase: true });
115
+ let st = null;
116
+ try { st = statSync(real); } catch { st = null; }
117
+ return { real, st };
118
+ });
119
+ // A file that is gone from the disk may still be in the last commit; a folder never is a file.
120
+ if (found.st && !found.st.isFile()) return res.status(400).json({ error: 'Not a valid file' });
121
+ if (found.st && found.st.size > maxBytes) { const refusal = tooLarge(); return res.status(refusal.status).json(refusal.body); }
122
+ const head = await readHeadVersion(found.real, { maxBytes, git });
123
+ if (head.tooLarge) { const refusal = tooLarge(); return res.status(refusal.status).json(refusal.body); }
124
+ if (!head.tracked) return res.json({ ok: true, tracked: false });
125
+ if (head.binary) return res.json({ ok: true, tracked: true, binary: true });
126
+ res.json({ ok: true, tracked: true, content: head.content });
127
+ } catch (error) {
128
+ if (error instanceof PathConfineError) return res.status(error.status).json({ error: error.message });
129
+ res.status(500).json({ error: error?.message || String(error) });
130
+ }
131
+ });
132
+
133
+ return router;
134
+ }