synthos 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (368) hide show
  1. package/README.md +1 -1
  2. package/default-pages/application/page.html +42 -0
  3. package/default-pages/application/page.json +10 -0
  4. package/default-pages/elevenlabs_effects_studio/page.html +1363 -0
  5. package/default-pages/elevenlabs_effects_studio/page.json +11 -0
  6. package/default-pages/elevenlabs_voice_studio/page.html +801 -0
  7. package/default-pages/elevenlabs_voice_studio/page.json +11 -0
  8. package/default-pages/{json_tools.html → json_tools/page.html} +13 -11
  9. package/default-pages/json_tools/page.json +10 -0
  10. package/default-pages/my_notes/notes/a1b2c3d4-e5f6-7890-abcd-ef1234567890.json +5 -0
  11. package/default-pages/my_notes/page.html +132 -0
  12. package/default-pages/{my_notes.json → my_notes/page.json} +2 -2
  13. package/default-pages/neon_asteroids/files/Ambient_Space.mp3 +0 -0
  14. package/default-pages/neon_asteroids/files/Ambient_Space2.mp3 +0 -0
  15. package/default-pages/neon_asteroids/files/Ambient_Space3.mp3 +0 -0
  16. package/default-pages/neon_asteroids/files/Asteroid_Explosion.mp3 +0 -0
  17. package/default-pages/neon_asteroids/files/Hyperspace_Jump.mp3 +0 -0
  18. package/default-pages/neon_asteroids/files/Laser_Fire.mp3 +0 -0
  19. package/default-pages/neon_asteroids/files/Menu_Navigate.mp3 +0 -0
  20. package/default-pages/neon_asteroids/files/Power_Up_Collect.mp3 +0 -0
  21. package/default-pages/neon_asteroids/files/Saucer_Alert.mp3 +0 -0
  22. package/default-pages/neon_asteroids/files/Ship_Thrust.mp3 +0 -0
  23. package/default-pages/neon_asteroids/files/effects.json +74 -0
  24. package/default-pages/neon_asteroids/page.html +1803 -0
  25. package/default-pages/{neon_asteroids.json → neon_asteroids/page.json} +3 -3
  26. package/default-pages/{oregon_trail.html → oregon_trail/page.html} +16 -30
  27. package/default-pages/{oregon_trail.json → oregon_trail/page.json} +2 -2
  28. package/default-pages/retro_game_starter/page.html +1308 -0
  29. package/default-pages/retro_game_starter/page.json +12 -0
  30. package/default-pages/{sidebar_page.html → sidebar_page/page.html} +12 -10
  31. package/default-pages/sidebar_page/page.json +10 -0
  32. package/default-pages/{solar_explorer.html → solar_explorer/page.html} +15 -12
  33. package/default-pages/{solar_explorer.json → solar_explorer/page.json} +2 -2
  34. package/default-pages/{solar_tutorial.html → solar_tutorial/page.html} +12 -10
  35. package/default-pages/solar_tutorial/page.json +10 -0
  36. package/default-pages/{two-panel_page.html → two-panel_page/page.html} +13 -11
  37. package/default-pages/two-panel_page/page.json +10 -0
  38. package/default-pages/{us_map.html → us_map/page.html} +193 -192
  39. package/default-pages/{us_map.json → us_map/page.json} +12 -12
  40. package/default-pages/{us_map_1850.html → us_map_1850/page.html} +326 -325
  41. package/default-pages/{us_map_1850.json → us_map_1850/page.json} +12 -12
  42. package/default-pages/{western_cities_1850.html → western_cities_1850/page.html} +527 -526
  43. package/default-pages/{western_cities_1850.json → western_cities_1850/page.json} +12 -12
  44. package/default-themes/aurora-dawn.json +19 -0
  45. package/default-themes/aurora-dawn.v3.css +198 -0
  46. package/default-themes/aurora-dusk.json +19 -0
  47. package/default-themes/aurora-dusk.v3.css +200 -0
  48. package/default-themes/cosmos-dawn.json +19 -0
  49. package/default-themes/cosmos-dawn.v3.css +198 -0
  50. package/default-themes/cosmos-dusk.json +19 -0
  51. package/default-themes/cosmos-dusk.v3.css +200 -0
  52. package/default-themes/high-contrast-dark.json +19 -0
  53. package/default-themes/high-contrast-dark.v3.css +200 -0
  54. package/default-themes/high-contrast-light.json +19 -0
  55. package/default-themes/high-contrast-light.v3.css +198 -0
  56. package/default-themes/nebula-dawn.v2.css +110 -0
  57. package/default-themes/nebula-dawn.v3.css +199 -0
  58. package/default-themes/nebula-dusk.v2.css +104 -0
  59. package/default-themes/nebula-dusk.v3.css +201 -0
  60. package/default-themes/solar-flare-dawn.json +19 -0
  61. package/default-themes/solar-flare-dawn.v3.css +198 -0
  62. package/default-themes/solar-flare-dusk.json +19 -0
  63. package/default-themes/solar-flare-dusk.v3.css +200 -0
  64. package/dist/agents/index.d.ts +1 -1
  65. package/dist/agents/index.d.ts.map +1 -1
  66. package/dist/agents/index.js +2 -1
  67. package/dist/agents/index.js.map +1 -1
  68. package/dist/agents/openclaw/gatewayManager.d.ts +4 -0
  69. package/dist/agents/openclaw/gatewayManager.d.ts.map +1 -1
  70. package/dist/agents/openclaw/gatewayManager.js +27 -11
  71. package/dist/agents/openclaw/gatewayManager.js.map +1 -1
  72. package/dist/agents/openclaw/openclawProvider.d.ts.map +1 -1
  73. package/dist/agents/openclaw/openclawProvider.js +2 -4
  74. package/dist/agents/openclaw/openclawProvider.js.map +1 -1
  75. package/dist/agents/openclaw/sshTunnelManager.d.ts +2 -0
  76. package/dist/agents/openclaw/sshTunnelManager.d.ts.map +1 -1
  77. package/dist/agents/openclaw/sshTunnelManager.js +31 -12
  78. package/dist/agents/openclaw/sshTunnelManager.js.map +1 -1
  79. package/dist/builders/anthropic.d.ts +31 -0
  80. package/dist/builders/anthropic.d.ts.map +1 -0
  81. package/dist/builders/anthropic.js +227 -0
  82. package/dist/builders/anthropic.js.map +1 -0
  83. package/dist/builders/fireworksai.d.ts +9 -0
  84. package/dist/builders/fireworksai.d.ts.map +1 -0
  85. package/dist/builders/fireworksai.js +57 -0
  86. package/dist/builders/fireworksai.js.map +1 -0
  87. package/dist/builders/index.d.ts +13 -0
  88. package/dist/builders/index.d.ts.map +1 -0
  89. package/dist/builders/index.js +31 -0
  90. package/dist/builders/index.js.map +1 -0
  91. package/dist/builders/openai.d.ts +8 -0
  92. package/dist/builders/openai.d.ts.map +1 -0
  93. package/dist/builders/openai.js +87 -0
  94. package/dist/builders/openai.js.map +1 -0
  95. package/dist/builders/types.d.ts +54 -0
  96. package/dist/builders/types.d.ts.map +1 -0
  97. package/dist/builders/types.js +211 -0
  98. package/dist/builders/types.js.map +1 -0
  99. package/dist/connectors/index.d.ts +1 -1
  100. package/dist/connectors/index.d.ts.map +1 -1
  101. package/dist/connectors/index.js +3 -2
  102. package/dist/connectors/index.js.map +1 -1
  103. package/dist/connectors/registry.d.ts +2 -1
  104. package/dist/connectors/registry.d.ts.map +1 -1
  105. package/dist/connectors/registry.js +31 -8
  106. package/dist/connectors/registry.js.map +1 -1
  107. package/dist/customizer/Customizer.d.ts +62 -0
  108. package/dist/customizer/Customizer.d.ts.map +1 -0
  109. package/dist/customizer/Customizer.js +134 -0
  110. package/dist/customizer/Customizer.js.map +1 -0
  111. package/dist/customizer/index.d.ts +4 -0
  112. package/dist/customizer/index.d.ts.map +1 -0
  113. package/dist/customizer/index.js +9 -0
  114. package/dist/customizer/index.js.map +1 -0
  115. package/dist/files.d.ts +16 -0
  116. package/dist/files.d.ts.map +1 -1
  117. package/dist/files.js +60 -1
  118. package/dist/files.js.map +1 -1
  119. package/dist/index.d.ts +2 -0
  120. package/dist/index.d.ts.map +1 -1
  121. package/dist/index.js +2 -0
  122. package/dist/index.js.map +1 -1
  123. package/dist/init.d.ts +12 -6
  124. package/dist/init.d.ts.map +1 -1
  125. package/dist/init.js +150 -133
  126. package/dist/init.js.map +1 -1
  127. package/dist/migrations.d.ts.map +1 -1
  128. package/dist/migrations.js +23 -10
  129. package/dist/migrations.js.map +1 -1
  130. package/dist/models/anthropic.d.ts +4 -2
  131. package/dist/models/anthropic.d.ts.map +1 -1
  132. package/dist/models/anthropic.js +33 -6
  133. package/dist/models/anthropic.js.map +1 -1
  134. package/dist/models/fireworksai.d.ts.map +1 -1
  135. package/dist/models/fireworksai.js +9 -1
  136. package/dist/models/fireworksai.js.map +1 -1
  137. package/dist/models/index.d.ts +1 -1
  138. package/dist/models/index.d.ts.map +1 -1
  139. package/dist/models/index.js +2 -1
  140. package/dist/models/index.js.map +1 -1
  141. package/dist/models/openai.d.ts +1 -1
  142. package/dist/models/openai.d.ts.map +1 -1
  143. package/dist/models/openai.js +24 -3
  144. package/dist/models/openai.js.map +1 -1
  145. package/dist/models/types.d.ts +20 -1
  146. package/dist/models/types.d.ts.map +1 -1
  147. package/dist/models/types.js +6 -1
  148. package/dist/models/types.js.map +1 -1
  149. package/dist/pages.d.ts +34 -10
  150. package/dist/pages.d.ts.map +1 -1
  151. package/dist/pages.js +229 -79
  152. package/dist/pages.js.map +1 -1
  153. package/dist/service/createCompletePrompt.d.ts +2 -1
  154. package/dist/service/createCompletePrompt.d.ts.map +1 -1
  155. package/dist/service/createCompletePrompt.js +2 -2
  156. package/dist/service/createCompletePrompt.js.map +1 -1
  157. package/dist/service/requiresSettings.d.ts +2 -1
  158. package/dist/service/requiresSettings.d.ts.map +1 -1
  159. package/dist/service/requiresSettings.js +3 -3
  160. package/dist/service/requiresSettings.js.map +1 -1
  161. package/dist/service/server.d.ts +2 -1
  162. package/dist/service/server.d.ts.map +1 -1
  163. package/dist/service/server.js +37 -8
  164. package/dist/service/server.js.map +1 -1
  165. package/dist/service/transformPage.d.ts +47 -20
  166. package/dist/service/transformPage.d.ts.map +1 -1
  167. package/dist/service/transformPage.js +514 -293
  168. package/dist/service/transformPage.js.map +1 -1
  169. package/dist/service/useAgentRoutes.d.ts +2 -1
  170. package/dist/service/useAgentRoutes.d.ts.map +1 -1
  171. package/dist/service/useAgentRoutes.js +17 -14
  172. package/dist/service/useAgentRoutes.js.map +1 -1
  173. package/dist/service/useApiRoutes.d.ts +2 -1
  174. package/dist/service/useApiRoutes.d.ts.map +1 -1
  175. package/dist/service/useApiRoutes.js +287 -172
  176. package/dist/service/useApiRoutes.js.map +1 -1
  177. package/dist/service/useConnectorRoutes.js +17 -17
  178. package/dist/service/useConnectorRoutes.js.map +1 -1
  179. package/dist/service/useDataRoutes.d.ts.map +1 -1
  180. package/dist/service/useDataRoutes.js +13 -10
  181. package/dist/service/useDataRoutes.js.map +1 -1
  182. package/dist/service/useFileRoutes.d.ts +4 -0
  183. package/dist/service/useFileRoutes.d.ts.map +1 -0
  184. package/dist/service/useFileRoutes.js +122 -0
  185. package/dist/service/useFileRoutes.js.map +1 -0
  186. package/dist/service/usePageRoutes.d.ts +2 -1
  187. package/dist/service/usePageRoutes.d.ts.map +1 -1
  188. package/dist/service/usePageRoutes.js +671 -74
  189. package/dist/service/usePageRoutes.js.map +1 -1
  190. package/dist/service/useSharedDataRoutes.d.ts +4 -0
  191. package/dist/service/useSharedDataRoutes.d.ts.map +1 -0
  192. package/dist/service/useSharedDataRoutes.js +107 -0
  193. package/dist/service/useSharedDataRoutes.js.map +1 -0
  194. package/dist/service/useSharedFileRoutes.d.ts +4 -0
  195. package/dist/service/useSharedFileRoutes.d.ts.map +1 -0
  196. package/dist/service/useSharedFileRoutes.js +121 -0
  197. package/dist/service/useSharedFileRoutes.js.map +1 -0
  198. package/dist/settings.d.ts +5 -3
  199. package/dist/settings.d.ts.map +1 -1
  200. package/dist/settings.js +12 -10
  201. package/dist/settings.js.map +1 -1
  202. package/dist/storage/FsStorageProvider.d.ts +25 -0
  203. package/dist/storage/FsStorageProvider.d.ts.map +1 -0
  204. package/dist/storage/FsStorageProvider.js +103 -0
  205. package/dist/storage/FsStorageProvider.js.map +1 -0
  206. package/dist/storage/StorageProvider.d.ts +31 -0
  207. package/dist/storage/StorageProvider.d.ts.map +1 -0
  208. package/dist/storage/StorageProvider.js +3 -0
  209. package/dist/storage/StorageProvider.js.map +1 -0
  210. package/dist/storage/index.d.ts +3 -0
  211. package/dist/storage/index.d.ts.map +1 -0
  212. package/dist/storage/index.js +6 -0
  213. package/dist/storage/index.js.map +1 -0
  214. package/dist/synthos-cli.d.ts.map +1 -1
  215. package/dist/synthos-cli.js +4 -3
  216. package/dist/synthos-cli.js.map +1 -1
  217. package/dist/themes.d.ts +1 -0
  218. package/dist/themes.d.ts.map +1 -1
  219. package/dist/themes.js +65 -28
  220. package/dist/themes.js.map +1 -1
  221. package/migration-rules/v1-to-v2.md +193 -0
  222. package/migration-rules/v2-to-v3.md +481 -0
  223. package/package.json +11 -10
  224. package/required-pages/builder/page.html +43 -0
  225. package/required-pages/builder/page.json +10 -0
  226. package/required-pages/{pages.html → pages/page.html} +238 -233
  227. package/required-pages/pages/page.json +10 -0
  228. package/required-pages/{settings.html → settings/page.html} +389 -275
  229. package/required-pages/settings/page.json +10 -0
  230. package/required-pages/synthos_apis/page.html +846 -0
  231. package/required-pages/synthos_apis/page.json +10 -0
  232. package/required-pages/{synthos_scripts.html → synthos_scripts/page.html} +13 -11
  233. package/required-pages/synthos_scripts/page.json +10 -0
  234. package/src/agents/index.ts +1 -1
  235. package/src/agents/openclaw/gatewayManager.ts +22 -11
  236. package/src/agents/openclaw/openclawProvider.ts +2 -4
  237. package/src/agents/openclaw/sshTunnelManager.ts +19 -11
  238. package/src/builders/anthropic.ts +283 -0
  239. package/src/builders/fireworksai.ts +59 -0
  240. package/src/builders/index.ts +33 -0
  241. package/src/builders/openai.ts +89 -0
  242. package/src/builders/types.ts +261 -0
  243. package/src/connectors/index.ts +1 -1
  244. package/src/connectors/registry.ts +28 -8
  245. package/src/customizer/Customizer.ts +163 -0
  246. package/src/customizer/index.ts +5 -0
  247. package/src/files.ts +57 -0
  248. package/src/index.ts +3 -1
  249. package/src/init.ts +195 -145
  250. package/src/migrations.ts +30 -10
  251. package/src/models/anthropic.ts +40 -10
  252. package/src/models/fireworksai.ts +9 -2
  253. package/src/models/index.ts +1 -1
  254. package/src/models/openai.ts +26 -6
  255. package/src/models/types.ts +31 -1
  256. package/src/pages.ts +230 -77
  257. package/src/service/createCompletePrompt.ts +3 -2
  258. package/src/service/requiresSettings.ts +4 -3
  259. package/src/service/server.ts +36 -9
  260. package/src/service/transformPage.ts +557 -326
  261. package/src/service/useAgentRoutes.ts +19 -14
  262. package/src/service/useApiRoutes.ts +208 -84
  263. package/src/service/useConnectorRoutes.ts +18 -18
  264. package/src/service/useDataRoutes.ts +13 -10
  265. package/src/service/useFileRoutes.ts +128 -0
  266. package/src/service/usePageRoutes.ts +730 -81
  267. package/src/service/useSharedDataRoutes.ts +109 -0
  268. package/src/service/useSharedFileRoutes.ts +127 -0
  269. package/src/settings.ts +14 -10
  270. package/src/storage/FsStorageProvider.ts +87 -0
  271. package/src/storage/StorageProvider.ts +34 -0
  272. package/src/storage/index.ts +2 -0
  273. package/src/synthos-cli.ts +4 -3
  274. package/src/themes.ts +64 -27
  275. package/static-files/favicon.svg +12 -0
  276. package/static-files/fluentlm-instructions.llmd +868 -0
  277. package/static-files/fluentlm-instructions.md +1595 -0
  278. package/static-files/fluentlm.css +4844 -0
  279. package/static-files/fluentlm.js +3602 -0
  280. package/static-files/fluentlm.min.css +1 -0
  281. package/static-files/fluentlm.min.js +1 -0
  282. package/{page-scripts/helpers-v2.js → static-files/helpers.v3.js} +82 -0
  283. package/static-files/page.v3.js +1290 -0
  284. package/static-files/recommended-frameworks.llmd +81 -0
  285. package/static-files/recommended-frameworks.md +137 -0
  286. package/static-files/retro-game.js +877 -0
  287. package/static-files/shell.css +797 -0
  288. package/static-files/theme-dark.css +169 -0
  289. package/static-files/theme-light.css +169 -0
  290. package/tests/builders.spec.ts +139 -0
  291. package/tests/pages.spec.ts +54 -84
  292. package/tests/transformPage.spec.ts +299 -360
  293. package/default-pages/application.html +0 -40
  294. package/default-pages/application.json +0 -1
  295. package/default-pages/json_tools.json +0 -1
  296. package/default-pages/my_notes.html +0 -33
  297. package/default-pages/neon_asteroids.html +0 -77
  298. package/default-pages/sidebar_page.json +0 -1
  299. package/default-pages/solar_tutorial.json +0 -1
  300. package/default-pages/two-panel_page.json +0 -1
  301. package/dist/service/useGatewayRoutes.d.ts +0 -4
  302. package/dist/service/useGatewayRoutes.d.ts.map +0 -1
  303. package/dist/service/useGatewayRoutes.js +0 -168
  304. package/dist/service/useGatewayRoutes.js.map +0 -1
  305. package/page-scripts/page-v2.js +0 -656
  306. package/required-pages/builder.html +0 -48
  307. package/required-pages/builder.json +0 -1
  308. package/required-pages/pages.json +0 -1
  309. package/required-pages/settings.json +0 -1
  310. package/required-pages/synthos_apis.html +0 -327
  311. package/required-pages/synthos_apis.json +0 -1
  312. package/required-pages/synthos_scripts.json +0 -1
  313. package/src/connectors/airtable/connector.json +0 -27
  314. package/src/connectors/alpha-vantage/connector.json +0 -26
  315. package/src/connectors/brave-search/connector.json +0 -26
  316. package/src/connectors/cloudinary/connector.json +0 -27
  317. package/src/connectors/deepl/connector.json +0 -28
  318. package/src/connectors/elevenlabs/connector.json +0 -30
  319. package/src/connectors/giphy/connector.json +0 -27
  320. package/src/connectors/github/connector.json +0 -29
  321. package/src/connectors/huggingface/connector.json +0 -27
  322. package/src/connectors/imgur/connector.json +0 -29
  323. package/src/connectors/instagram/connector.json +0 -43
  324. package/src/connectors/jira/connector.json +0 -28
  325. package/src/connectors/mapbox/connector.json +0 -26
  326. package/src/connectors/nasa/connector.json +0 -27
  327. package/src/connectors/newsapi/connector.json +0 -27
  328. package/src/connectors/notion/connector.json +0 -28
  329. package/src/connectors/open-exchange-rates/connector.json +0 -27
  330. package/src/connectors/openweathermap/connector.json +0 -26
  331. package/src/connectors/pexels/connector.json +0 -27
  332. package/src/connectors/resend/connector.json +0 -29
  333. package/src/connectors/rss2json/connector.json +0 -27
  334. package/src/connectors/sendgrid/connector.json +0 -27
  335. package/src/connectors/spoonacular/connector.json +0 -28
  336. package/src/connectors/stability-ai/connector.json +0 -27
  337. package/src/connectors/twilio/connector.json +0 -28
  338. package/src/connectors/unsplash/connector.json +0 -27
  339. package/src/connectors/wolfram-alpha/connector.json +0 -26
  340. package/src/connectors/youtube-data/connector.json +0 -30
  341. /package/{dist/connectors → service-connectors}/airtable/connector.json +0 -0
  342. /package/{dist/connectors → service-connectors}/alpha-vantage/connector.json +0 -0
  343. /package/{dist/connectors → service-connectors}/brave-search/connector.json +0 -0
  344. /package/{dist/connectors → service-connectors}/cloudinary/connector.json +0 -0
  345. /package/{dist/connectors → service-connectors}/deepl/connector.json +0 -0
  346. /package/{dist/connectors → service-connectors}/elevenlabs/connector.json +0 -0
  347. /package/{dist/connectors → service-connectors}/giphy/connector.json +0 -0
  348. /package/{dist/connectors → service-connectors}/github/connector.json +0 -0
  349. /package/{dist/connectors → service-connectors}/huggingface/connector.json +0 -0
  350. /package/{dist/connectors → service-connectors}/imgur/connector.json +0 -0
  351. /package/{dist/connectors → service-connectors}/instagram/connector.json +0 -0
  352. /package/{dist/connectors → service-connectors}/jira/connector.json +0 -0
  353. /package/{dist/connectors → service-connectors}/mapbox/connector.json +0 -0
  354. /package/{dist/connectors → service-connectors}/nasa/connector.json +0 -0
  355. /package/{dist/connectors → service-connectors}/newsapi/connector.json +0 -0
  356. /package/{dist/connectors → service-connectors}/notion/connector.json +0 -0
  357. /package/{dist/connectors → service-connectors}/open-exchange-rates/connector.json +0 -0
  358. /package/{dist/connectors → service-connectors}/openweathermap/connector.json +0 -0
  359. /package/{dist/connectors → service-connectors}/pexels/connector.json +0 -0
  360. /package/{dist/connectors → service-connectors}/resend/connector.json +0 -0
  361. /package/{dist/connectors → service-connectors}/rss2json/connector.json +0 -0
  362. /package/{dist/connectors → service-connectors}/sendgrid/connector.json +0 -0
  363. /package/{dist/connectors → service-connectors}/spoonacular/connector.json +0 -0
  364. /package/{dist/connectors → service-connectors}/stability-ai/connector.json +0 -0
  365. /package/{dist/connectors → service-connectors}/twilio/connector.json +0 -0
  366. /package/{dist/connectors → service-connectors}/unsplash/connector.json +0 -0
  367. /package/{dist/connectors → service-connectors}/wolfram-alpha/connector.json +0 -0
  368. /package/{dist/connectors → service-connectors}/youtube-data/connector.json +0 -0
@@ -1,8 +1,7 @@
1
1
  import path from "path";
2
- import fs from "fs/promises";
3
2
  import AdmZip from "adm-zip";
4
- import { listPages, loadPageMetadata, PageMetadata, savePageMetadata, REQUIRED_PAGES, deletePage, copyPage, loadPageState, savePageState, PAGE_VERSION } from "../pages";
5
- import { checkIfExists, copyFile, copyFolderRecursive, deleteFile, ensureFolderExists, loadFile } from "../files";
3
+ import { listPages, loadPageMetadata, PageMetadata, savePageMetadata, deletePage, copyPage, savePageState, clearVersions, PAGE_VERSION } from "../pages";
4
+ import { checkIfExists, findFileInFolders, listFiles, listFolders, loadFile } from "../files";
6
5
  import {getModelEntry, loadSettings, saveSettings, ServicesConfig } from "../settings";
7
6
  import { Application } from 'express';
8
7
  import express from 'express';
@@ -12,9 +11,10 @@ import { generateDefaultImage, generateImage } from "./generateImage";
12
11
  import { chainOfThought } from "../models";
13
12
  import { requiresSettings } from "./requiresSettings";
14
13
  import { executeScript } from "../scripts";
15
- import { listThemes, loadTheme, loadThemeInfo } from "../themes";
14
+ import { listThemes, loadTheme, loadThemeInfo, loadThemeVersion } from "../themes";
16
15
  import { migratePage } from "../migrations";
17
16
  import { loadPageWithFallback } from "./usePageRoutes";
17
+ import { Customizer } from "../customizer";
18
18
 
19
19
  // ---------------------------------------------------------------------------
20
20
  // Service registry
@@ -48,10 +48,12 @@ const SERVICE_REGISTRY: ServiceDefinition[] = [
48
48
  }
49
49
  ];
50
50
 
51
- export function useApiRoutes(config: SynthOSConfig, app: Application): void {
51
+ export function useApiRoutes(config: SynthOSConfig, app: Application, customizer?: Customizer): void {
52
+ const sp = config.storageProvider;
53
+
52
54
  // List pages
53
55
  app.get('/api/pages', async (req, res) => {
54
- const pages = await listPages(config.pagesFolder, config.requiredPagesFolder);
56
+ const pages = await listPages(config, config.requiredPagesFolders);
55
57
  res.json(pages);
56
58
  });
57
59
 
@@ -97,13 +99,13 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
97
99
  // Auto-append _1, _2, etc. on name conflicts
98
100
  let finalName = pageName;
99
101
  let suffix = 0;
100
- while (await checkIfExists(path.join(config.pagesFolder, 'pages', finalName))) {
102
+ while (await sp.checkIfExists(path.join(config.pagesFolder, 'pages', finalName))) {
101
103
  suffix++;
102
104
  finalName = `${pageName}_${suffix}`;
103
105
  }
104
106
 
105
107
  const targetDir = path.join(config.pagesFolder, 'pages', finalName);
106
- await ensureFolderExists(targetDir);
108
+ await sp.ensureFolderExists(targetDir);
107
109
 
108
110
  // Extract entries with path traversal protection
109
111
  for (const entry of entries) {
@@ -119,13 +121,13 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
119
121
  continue;
120
122
  }
121
123
 
122
- await ensureFolderExists(path.dirname(resolvedPath));
123
- await fs.writeFile(resolvedPath, entry.getData());
124
+ await sp.ensureFolderExists(path.dirname(resolvedPath));
125
+ await sp.saveBuffer(resolvedPath, entry.getData());
124
126
  }
125
127
 
126
128
  // Update metadata: set createdDate and lastModified to now
127
129
  const now = new Date().toISOString();
128
- const existingMeta = await loadPageMetadata(config.pagesFolder, finalName);
130
+ const existingMeta = await loadPageMetadata(config, finalName);
129
131
  const metadata: PageMetadata = {
130
132
  title: existingMeta?.title ?? '',
131
133
  categories: existingMeta?.categories ?? [],
@@ -136,7 +138,7 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
136
138
  pageVersion: existingMeta?.pageVersion ?? PAGE_VERSION,
137
139
  mode: existingMeta?.mode ?? 'unlocked',
138
140
  };
139
- await savePageMetadata(config.pagesFolder, finalName, metadata);
141
+ await savePageMetadata(config, finalName, metadata);
140
142
 
141
143
  res.status(201).json({ name: finalName, title: metadata.title });
142
144
  } catch (err: unknown) {
@@ -149,7 +151,7 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
149
151
  app.get('/api/pages/:name', async (req, res) => {
150
152
  try {
151
153
  const { name } = req.params;
152
- const metadata = await loadPageMetadata(config.pagesFolder, name, config.requiredPagesFolder);
154
+ const metadata = await loadPageMetadata(config, name, config.requiredPagesFolders);
153
155
  if (metadata) {
154
156
  res.json(metadata);
155
157
  } else {
@@ -200,7 +202,7 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
200
202
  }
201
203
 
202
204
  // Load existing metadata (or defaults)
203
- const existing = await loadPageMetadata(config.pagesFolder, name, config.requiredPagesFolder);
205
+ const existing = await loadPageMetadata(config, name, config.requiredPagesFolders);
204
206
  const metadata: PageMetadata = {
205
207
  title: existing?.title ?? '',
206
208
  categories: existing?.categories ?? [],
@@ -225,15 +227,23 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
225
227
  // Promote required page to user folder if being unlocked/designed
226
228
  if (metadata.mode !== 'locked') {
227
229
  const userPagePath = path.join(config.pagesFolder, 'pages', name, 'page.html');
228
- if (!(await checkIfExists(userPagePath))) {
229
- const html = await loadPageState(config.requiredPagesFolder, name, false);
230
+ if (!(await sp.checkIfExists(userPagePath))) {
231
+ // Read from required pages (package content, always local fs)
232
+ let html: string | undefined;
233
+ for (const folder of config.requiredPagesFolders) {
234
+ const candidate = path.join(folder, name, 'page.html');
235
+ if (await checkIfExists(candidate)) {
236
+ html = await loadFile(candidate);
237
+ break;
238
+ }
239
+ }
230
240
  if (html) {
231
- await savePageState(config.pagesFolder, name, html);
241
+ await savePageState(config, name, html);
232
242
  }
233
243
  }
234
244
  }
235
245
 
236
- await savePageMetadata(config.pagesFolder, name, metadata);
246
+ await savePageMetadata(config, name, metadata);
237
247
  res.json(metadata);
238
248
  } catch (err: unknown) {
239
249
  console.error(err);
@@ -252,7 +262,7 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
252
262
  }
253
263
 
254
264
  // Load existing metadata (user override → fallback .json → defaults)
255
- let metadata = await loadPageMetadata(config.pagesFolder, name, config.requiredPagesFolder);
265
+ let metadata = await loadPageMetadata(config, name, config.requiredPagesFolders);
256
266
  if (!metadata) {
257
267
  metadata = {
258
268
  title: '',
@@ -267,7 +277,7 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
267
277
  }
268
278
 
269
279
  metadata.pinned = pinned;
270
- await savePageMetadata(config.pagesFolder, name, metadata);
280
+ await savePageMetadata(config, name, metadata);
271
281
  res.json(metadata);
272
282
  } catch (err: unknown) {
273
283
  console.error(err);
@@ -281,7 +291,7 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
281
291
  const { name } = req.params;
282
292
 
283
293
  // Cannot delete required pages
284
- if (REQUIRED_PAGES.includes(name)) {
294
+ if (config.requiredPages.includes(name)) {
285
295
  res.status(400).json({ error: `Cannot delete required page "${name}"` });
286
296
  return;
287
297
  }
@@ -289,13 +299,13 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
289
299
  // Check if page exists (folder-based or legacy flat file)
290
300
  const folderPath = path.join(config.pagesFolder, 'pages', name, 'page.html');
291
301
  const flatPath = path.join(config.pagesFolder, `${name}.html`);
292
- const exists = await checkIfExists(folderPath) || await checkIfExists(flatPath);
302
+ const exists = await sp.checkIfExists(folderPath) || await sp.checkIfExists(flatPath);
293
303
  if (!exists) {
294
304
  res.status(404).json({ error: `Page "${name}" not found` });
295
305
  return;
296
306
  }
297
307
 
298
- await deletePage(config.pagesFolder, name);
308
+ await deletePage(config, name);
299
309
  res.json({ deleted: true });
300
310
  } catch (err: unknown) {
301
311
  console.error(err);
@@ -303,11 +313,65 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
303
313
  }
304
314
  });
305
315
 
316
+ // Discover what a page contains (tables + files)
317
+ app.get('/api/pages/:name/contents', async (req, res) => {
318
+ try {
319
+ const { name } = req.params;
320
+
321
+ // Resolve page folder: user pages first, then required pages
322
+ let pageFolder: string | undefined;
323
+ const userFolder = path.join(config.pagesFolder, 'pages', name);
324
+ if (await sp.checkIfExists(path.join(userFolder, 'page.html'))) {
325
+ pageFolder = userFolder;
326
+ } else {
327
+ for (const folder of config.requiredPagesFolders) {
328
+ const candidate = path.join(folder, name);
329
+ if (await checkIfExists(path.join(candidate, 'page.html'))) {
330
+ pageFolder = candidate;
331
+ break;
332
+ }
333
+ }
334
+ }
335
+
336
+ if (!pageFolder) {
337
+ res.status(404).json({ error: `Page "${name}" not found` });
338
+ return;
339
+ }
340
+
341
+ // List subdirectories, filtering out non-table entries
342
+ const EXCLUDED = new Set(['files']);
343
+ // Use provider for user folder, local fs for required pages
344
+ const isUserFolder = pageFolder === userFolder;
345
+ const subdirs = isUserFolder
346
+ ? await sp.listFolders(pageFolder)
347
+ : await listFolders(pageFolder);
348
+ const tables = subdirs.filter(d => !EXCLUDED.has(d));
349
+
350
+ // Check if files/ exists and has entries
351
+ const filesDir = path.join(pageFolder, 'files');
352
+ let hasFiles = false;
353
+ const filesDirExists = isUserFolder
354
+ ? await sp.checkIfExists(filesDir)
355
+ : await checkIfExists(filesDir);
356
+ if (filesDirExists) {
357
+ const entries = isUserFolder
358
+ ? await sp.listFiles(filesDir)
359
+ : await listFiles(filesDir);
360
+ hasFiles = entries.length > 0;
361
+ }
362
+
363
+ res.json({ tables, hasFiles });
364
+ } catch (err: unknown) {
365
+ console.error(err);
366
+ res.status(500).json({ error: (err as Error).message });
367
+ }
368
+ });
369
+
306
370
  // Copy a page to a new name
307
371
  app.post('/api/pages/:name/copy', async (req, res) => {
308
372
  try {
309
373
  const sourceName = req.params.name;
310
- const { name: targetName, title, categories } = req.body;
374
+ const { name: targetName, title, categories, copyTables, copyFiles } = req.body;
311
375
 
312
376
  // Validate target name
313
377
  if (!targetName || typeof targetName !== 'string') {
@@ -329,10 +393,14 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
329
393
  // Check source exists (user pages → required pages)
330
394
  const sourceFolderPath = path.join(config.pagesFolder, 'pages', sourceName, 'page.html');
331
395
  const sourceFlatPath = path.join(config.pagesFolder, `${sourceName}.html`);
332
- const sourceRequiredPath = path.join(config.requiredPagesFolder, `${sourceName}.html`);
333
- const sourceExists = await checkIfExists(sourceFolderPath)
334
- || await checkIfExists(sourceFlatPath)
335
- || await checkIfExists(sourceRequiredPath);
396
+ let sourceRequiredPath: string | undefined;
397
+ for (const folder of config.requiredPagesFolders) {
398
+ const candidate = path.join(folder, sourceName, 'page.html');
399
+ if (await checkIfExists(candidate)) { sourceRequiredPath = candidate; break; }
400
+ }
401
+ const sourceExists = await sp.checkIfExists(sourceFolderPath)
402
+ || await sp.checkIfExists(sourceFlatPath)
403
+ || !!sourceRequiredPath;
336
404
  if (!sourceExists) {
337
405
  res.status(404).json({ error: `Source page "${sourceName}" not found` });
338
406
  return;
@@ -341,22 +409,26 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
341
409
  // Check target doesn't already exist
342
410
  const targetFolderPath = path.join(config.pagesFolder, 'pages', targetName, 'page.html');
343
411
  const targetFlatPath = path.join(config.pagesFolder, `${targetName}.html`);
344
- if (await checkIfExists(targetFolderPath) || await checkIfExists(targetFlatPath)) {
412
+ if (await sp.checkIfExists(targetFolderPath) || await sp.checkIfExists(targetFlatPath)) {
345
413
  res.status(409).json({ error: `Page "${targetName}" already exists` });
346
414
  return;
347
415
  }
348
416
 
349
417
  await copyPage(
350
- config.pagesFolder,
418
+ config,
351
419
  sourceName,
352
420
  targetName,
353
421
  typeof title === 'string' ? title : '',
354
422
  Array.isArray(categories) ? categories : [],
355
- config.requiredPagesFolder
423
+ config.requiredPagesFolders,
424
+ {
425
+ copyTables: copyTables === true,
426
+ copyFiles: copyFiles !== false, // default true
427
+ }
356
428
  );
357
429
 
358
430
  // Return the new page metadata
359
- const metadata = await loadPageMetadata(config.pagesFolder, targetName);
431
+ const metadata = await loadPageMetadata(config, targetName);
360
432
  res.status(201).json({ name: targetName, ...metadata });
361
433
  } catch (err: unknown) {
362
434
  console.error(err);
@@ -366,7 +438,7 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
366
438
 
367
439
  // Define a route to return settings
368
440
  app.get('/api/settings', async (req, res) => {
369
- const settings = await loadSettings(config.pagesFolder);
441
+ const settings = await loadSettings(config);
370
442
  const providers = PROVIDERS.map(p => ({ name: p.name, builderModels: p.builderModels, chatModels: p.chatModels }));
371
443
  res.json({...settings, providers});
372
444
  });
@@ -387,7 +459,7 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
387
459
  }
388
460
 
389
461
  // Save settings
390
- await saveSettings(config.pagesFolder, settings);
462
+ await saveSettings(config, settings);
391
463
  res.redirect('/builder');
392
464
  } catch (err: unknown) {
393
465
  console.error(err);
@@ -397,7 +469,7 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
397
469
 
398
470
  // Define a route to generate an image
399
471
  app.post('/api/generate/image', async (req, res) => {
400
- await requiresSettings(res, config.pagesFolder, async (settings) => {
472
+ await requiresSettings(res, config, async (settings) => {
401
473
  const { prompt, shape, style } = req.body;
402
474
  const builder = getModelEntry(settings, 'builder');
403
475
  const { configuration, imageQuality, provider } = builder;
@@ -414,9 +486,9 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
414
486
 
415
487
  // Define a route to generate a completion using chain-of-thought
416
488
  app.post('/api/generate/completion', async (req, res) => {
417
- await requiresSettings(res, config.pagesFolder, async (settings) => {
489
+ await requiresSettings(res, config, async (settings) => {
418
490
  const { prompt, temperature } = req.body;
419
- const completePrompt = await createCompletePrompt(config.pagesFolder, 'chat', req.body.model);
491
+ const completePrompt = await createCompletePrompt(config, 'chat', req.body.model);
420
492
  const response = await chainOfThought({ question: prompt, temperature, completePrompt });
421
493
  if (response.completed) {
422
494
  res.json(response.value ?? {});
@@ -428,16 +500,18 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
428
500
  });
429
501
 
430
502
  // Brainstorm endpoint
503
+ if (!customizer || customizer.isEnabled('brainstorm'))
431
504
  app.post('/api/brainstorm', async (req, res) => {
432
- await requiresSettings(res, config.pagesFolder, async (settings) => {
505
+ await requiresSettings(res, config, async (settings) => {
433
506
  const { context, messages } = req.body;
434
- const completePrompt = await createCompletePrompt(config.pagesFolder, 'chat');
507
+ const completePrompt = await createCompletePrompt(config, 'chat');
435
508
 
509
+ const productName = customizer?.productName ?? 'SynthOS';
436
510
  const system: { role: 'system'; content: string } = {
437
511
  role: 'system',
438
- content: `You are a creative brainstorming assistant for SynthOS, a tool that builds pages through conversation.
439
- SynthOS is like a WIKI for vibe coding. Each page has a chat panel and a viewer panel. They are vibe coding what's displayed in that viewer panel. They can then save that as a page.
440
- The user is brainstorming — exploring ideas before building. Be concise, creative, and collaborative.
512
+ content: `You are a creative brainstorming assistant for ${productName}, a tool that builds pages through conversation.
513
+ ${productName} is like a WIKI for vibe coding. Each page has a chat panel and a viewer panel. They are vibe coding what's displayed in that viewer panel. They can then save that as a page.
514
+ The user is brainstorming — exploring ideas before building. Be concise, creative, and collaborative.
441
515
  They may say that they want to build an app or page that does XYZ but they're talking about what they expect to see in the viewer panel.
442
516
  The goal is to help them generate a prompt for the builder that captures their vision, along with suggestions for next steps.
443
517
  Suggest concrete approaches when you can, not complex visions for some ellaborate app.
@@ -448,14 +522,14 @@ ${context}
448
522
 
449
523
  <INSTRUCTIONS>
450
524
  Look at the <CHAT_HISTORY> and if it's empty it's the start of a new idea. Simply greet them and ask them what they're thinking of building. Suggestions could be help me decide, etc.
451
- If you see a conversation between SynthOS and the User. Asses what they're building and ask them what they'd like help with. Maybe offer a few good next steps.
525
+ If you see a conversation between ${productName} and the User. Asses what they're building and ask them what they'd like help with. Maybe offer a few good next steps.
452
526
 
453
- SynthOS exposes table storage and chat completion api's that every page can use. If the user wants to store something or use AI, your prompt should suggest using table storage or make llm calls.
527
+ ${productName} exposes table storage and chat completion api's that every page can use. If the user wants to store something or use AI, your prompt should suggest using table storage or make llm calls.
454
528
 
455
529
  You MUST return your response as a JSON object with exactly these fields:
456
530
  {
457
531
  "response": "Your conversational reply — explanations, options, suggestions. Markdown OK.",
458
- "prompt": "A clean, actionable instruction ready to paste into SynthOS chat to build what was discussed. Update this each exchange to reflect the latest brainstorm state.",
532
+ "prompt": "A clean, actionable instruction ready to paste into ${productName} chat to build what was discussed. Update this each exchange to reflect the latest brainstorm state.",
459
533
  "suggestions": ["Short clickable option A", "Short clickable option B", "Short clickable option C"]
460
534
  }
461
535
 
@@ -495,12 +569,12 @@ Return ONLY the JSON object.`};
495
569
  });
496
570
 
497
571
  // Define a route for running configured scripts
572
+ if (!customizer || customizer.isEnabled('scripts'))
498
573
  app.post('/api/scripts/:id', async (req, res) => {
499
- await requiresSettings(res, config.pagesFolder, async (settings) => {
574
+ await requiresSettings(res, config, async (settings) => {
500
575
  const { id } = req.params;
501
- const pagesFolder = config.pagesFolder;
502
576
  const scriptId = id;
503
- const response = await executeScript({ pagesFolder, scriptId, variables: req.body });
577
+ const response = await executeScript({ pagesFolder: config.pagesFolder, scriptId, variables: req.body });
504
578
  if (response.completed) {
505
579
  // Return the response as text
506
580
  const value = (response.value?.output ?? (response.value?.errors ?? []).join('\n')).trim();
@@ -516,14 +590,20 @@ Return ONLY the JSON object.`};
516
590
  // Return theme info as a self-executing JS script
517
591
  app.get('/api/theme-info.js', async (req, res) => {
518
592
  try {
519
- const settings = await loadSettings(config.pagesFolder);
593
+ const settings = await loadSettings(config);
520
594
  const themeName = settings.theme ?? 'nebula-dusk';
521
595
  const info = await loadThemeInfo(themeName, config);
522
596
  if (!info) {
523
597
  res.status(404).send(`// Theme info for "${themeName}" not found`);
524
598
  return;
525
599
  }
526
- const js = `window.themeInfo=${JSON.stringify(info)};document.documentElement.classList.add(window.themeInfo.mode+"-mode");`;
600
+ const themeVersion = await loadThemeVersion(themeName, config);
601
+ const payload = { ...info, name: themeName, version: themeVersion };
602
+ let js = `window.themeInfo=${JSON.stringify(payload)};document.documentElement.classList.add(window.themeInfo.mode+"-mode");`;
603
+ if (themeVersion >= 3) {
604
+ js += `document.documentElement.classList.add(${JSON.stringify(themeName)});`;
605
+ }
606
+ js += `document.documentElement.setAttribute("data-toolbar",${JSON.stringify(settings.toolbarPosition || 'left')});`;
527
607
  res.set('Content-Type', 'application/javascript');
528
608
  res.send(js);
529
609
  } catch (err: unknown) {
@@ -540,21 +620,18 @@ Return ONLY the JSON object.`};
540
620
  res.status(400).send('// Missing page query parameter');
541
621
  return;
542
622
  }
543
- const metadata = await loadPageMetadata(config.pagesFolder, page, config.requiredPagesFolder);
623
+ const metadata = await loadPageMetadata(config, page, config.requiredPagesFolders);
544
624
  const mode = metadata?.mode ?? 'unlocked';
545
625
  const title = metadata?.title ?? '';
546
626
  const categories = metadata?.categories ?? [];
547
- const info = JSON.stringify({ name: page, mode, latestPageVersion: PAGE_VERSION, title, categories });
627
+ const isRequiredPage = config.requiredPages.includes(page);
628
+ const productName = customizer?.productName ?? 'SynthOS';
629
+ const info = JSON.stringify({ name: page, mode, latestPageVersion: PAGE_VERSION, title, categories, isRequiredPage, productName });
548
630
  const js = [
549
631
  `window.pageInfo=${info};`,
550
632
  `if(window.pageInfo.mode==="locked"){`,
551
633
  `document.addEventListener("DOMContentLoaded",function(){`,
552
634
  `var f=document.getElementById("chatForm");if(f)f.style.display="none";`,
553
- `var s=document.getElementById("saveLink");if(s)s.textContent="Copy";`,
554
- `var r=document.getElementById("resetLink");if(r){`,
555
- `var c=r.cloneNode(true);c.textContent="Reload";`,
556
- `c.addEventListener("click",function(e){e.preventDefault();window.location.href=window.location.pathname;});`,
557
- `r.parentNode.replaceChild(c,r);}`,
558
635
  `});`,
559
636
  `}`,
560
637
  ].join('');
@@ -570,7 +647,7 @@ Return ONLY the JSON object.`};
570
647
  // Return the current theme as CSS
571
648
  app.get('/api/theme.css', async (req, res) => {
572
649
  try {
573
- const settings = await loadSettings(config.pagesFolder);
650
+ const settings = await loadSettings(config);
574
651
  const themeName = settings.theme ?? 'nebula-dusk';
575
652
  const css = await loadTheme(themeName, config);
576
653
  if (!css) {
@@ -604,8 +681,8 @@ Return ONLY the JSON object.`};
604
681
  res.status(400).send('// Invalid version parameter');
605
682
  return;
606
683
  }
607
- const scriptPath = path.join(config.pageScriptsFolder, `page-v${v}.js`);
608
- if (!(await checkIfExists(scriptPath))) {
684
+ const scriptPath = await findFileInFolders(config.staticFilesFolders, `page.v${v}.js`);
685
+ if (!scriptPath) {
609
686
  res.status(404).send(`// page-v${v}.js not found`);
610
687
  return;
611
688
  }
@@ -627,8 +704,8 @@ Return ONLY the JSON object.`};
627
704
  res.status(400).send('// Invalid version parameter');
628
705
  return;
629
706
  }
630
- const scriptPath = path.join(config.pageScriptsFolder, `helpers-v${v}.js`);
631
- if (!(await checkIfExists(scriptPath))) {
707
+ const scriptPath = await findFileInFolders(config.staticFilesFolders, `helpers.v${v}.js`);
708
+ if (!scriptPath) {
632
709
  res.status(404).send(`// helpers-v${v}.js not found`);
633
710
  return;
634
711
  }
@@ -654,7 +731,7 @@ Return ONLY the JSON object.`};
654
731
  // Return user's configured services (API keys masked)
655
732
  app.get('/api/services', async (_req, res) => {
656
733
  try {
657
- const settings = await loadSettings(config.pagesFolder);
734
+ const settings = await loadSettings(config);
658
735
  const services = settings.services ?? {};
659
736
  const masked: Record<string, { enabled: boolean; hasKey: boolean }> = {};
660
737
  for (const [id, cfg] of Object.entries(services)) {
@@ -674,7 +751,7 @@ Return ONLY the JSON object.`};
674
751
  app.post('/api/services', async (req, res) => {
675
752
  try {
676
753
  const incoming = req.body as ServicesConfig;
677
- const settings = await loadSettings(config.pagesFolder);
754
+ const settings = await loadSettings(config);
678
755
  const existing = settings.services ?? {};
679
756
 
680
757
  // Build merged config — empty apiKey means "keep existing"
@@ -695,7 +772,7 @@ Return ONLY the JSON object.`};
695
772
  }
696
773
  }
697
774
 
698
- await saveSettings(config.pagesFolder, { services: merged });
775
+ await saveSettings(config, { services: merged });
699
776
  res.json({ saved: true });
700
777
  } catch (err: unknown) {
701
778
  console.error(err);
@@ -707,6 +784,7 @@ Return ONLY the JSON object.`};
707
784
  // Web Search (Brave Search API)
708
785
  // -----------------------------------------------------------------------
709
786
 
787
+ if (!customizer || customizer.isEnabled('search'))
710
788
  app.post('/api/search/web', async (req, res) => {
711
789
  try {
712
790
  const { query, count, country, freshness } = req.body;
@@ -715,7 +793,7 @@ Return ONLY the JSON object.`};
715
793
  return;
716
794
  }
717
795
 
718
- const settings = await loadSettings(config.pagesFolder);
796
+ const settings = await loadSettings(config);
719
797
  const braveConfig = settings.connectors?.['brave-search'] ?? settings.services?.['brave-search'];
720
798
  if (!braveConfig || !braveConfig.enabled || !braveConfig.apiKey) {
721
799
  res.status(400).json({ error: 'Brave Search is not configured or not enabled. Add your API key in Settings > Services.' });
@@ -761,7 +839,7 @@ Return ONLY the JSON object.`};
761
839
  const { name } = req.params;
762
840
 
763
841
  // Load current metadata
764
- const metadata = await loadPageMetadata(config.pagesFolder, name, config.requiredPagesFolder);
842
+ const metadata = await loadPageMetadata(config, name, config.requiredPagesFolders);
765
843
  if (!metadata) {
766
844
  res.status(404).json({ error: `Page "${name}" not found` });
767
845
  return;
@@ -781,32 +859,37 @@ Return ONLY the JSON object.`};
781
859
  }
782
860
 
783
861
  // Run LLM-based migration
784
- const completePrompt = await createCompletePrompt(config.pagesFolder, 'builder');
862
+ const completePrompt = await createCompletePrompt(config, 'builder');
785
863
  const migratedHtml = await migratePage(html, currentVersion, PAGE_VERSION, completePrompt);
786
864
 
787
865
  // Save upgraded HTML to v2 folder structure
788
- await savePageState(config.pagesFolder, name, migratedHtml);
866
+ await savePageState(config, name, migratedHtml);
789
867
 
790
868
  // Backup original page to .migrated/ before overwriting
791
869
  const migratedFolder = path.join(config.pagesFolder, '.migrated');
792
870
 
793
- // Handle legacy flat file (.synthos/pagename.html)
871
+ // Handle legacy flat file (<localFolder>/pagename.html)
794
872
  const flatPath = path.join(config.pagesFolder, `${name}.html`);
795
- if (await checkIfExists(flatPath)) {
796
- await copyFile(flatPath, migratedFolder);
797
- await deleteFile(flatPath);
873
+ if (await sp.checkIfExists(flatPath)) {
874
+ await sp.ensureFolderExists(migratedFolder);
875
+ const flatData = await sp.loadBuffer(flatPath);
876
+ await sp.saveBuffer(path.join(migratedFolder, `${name}.html`), flatData);
877
+ await sp.deleteFile(flatPath);
798
878
  }
799
879
 
800
- // Handle folder-based page (.synthos/pages/name/)
880
+ // Handle folder-based page (<localFolder>/pages/name/)
801
881
  const folderPath = path.join(config.pagesFolder, 'pages', name);
802
- if (await checkIfExists(folderPath)) {
803
- await copyFolderRecursive(folderPath, path.join(migratedFolder, name));
882
+ if (await sp.checkIfExists(folderPath)) {
883
+ await sp.copyFolderRecursive(folderPath, path.join(migratedFolder, name));
804
884
  }
805
885
 
886
+ // Clear stale version files (undo snapshots from the old page version)
887
+ await clearVersions(config, name);
888
+
806
889
  // Update metadata
807
890
  metadata.pageVersion = PAGE_VERSION;
808
891
  metadata.lastModified = new Date().toISOString();
809
- await savePageMetadata(config.pagesFolder, name, metadata);
892
+ await savePageMetadata(config, name, metadata);
810
893
 
811
894
  res.json({ upgraded: true, fromVersion: currentVersion, toVersion: PAGE_VERSION });
812
895
  } catch (err: unknown) {
@@ -822,21 +905,27 @@ Return ONLY the JSON object.`};
822
905
 
823
906
  // Try user pages folder first, then required pages
824
907
  const userPageDir = path.join(config.pagesFolder, 'pages', name);
825
- const requiredPageFile = path.join(config.requiredPagesFolder, `${name}.html`);
908
+ let requiredPageDir: string | undefined;
909
+ for (const folder of config.requiredPagesFolders) {
910
+ if (await checkIfExists(path.join(folder, name, 'page.html'))) { // package content, local fs
911
+ requiredPageDir = path.join(folder, name);
912
+ break;
913
+ }
914
+ }
826
915
  let sourceDir: string | null = null;
827
916
 
828
- if (await checkIfExists(path.join(userPageDir, 'page.html'))) {
917
+ if (await sp.checkIfExists(path.join(userPageDir, 'page.html'))) {
829
918
  sourceDir = userPageDir;
830
- } else if (await checkIfExists(requiredPageFile)) {
919
+ } else if (requiredPageDir) {
831
920
  // For required pages, create a temp-like zip with just the HTML
832
921
  const zip = new AdmZip();
833
- const html = await loadFile(requiredPageFile);
922
+ const html = await loadFile(path.join(requiredPageDir, 'page.html'));
834
923
  zip.addFile(`${name}/page.html`, Buffer.from(html, 'utf-8'));
835
924
 
836
925
  // Include page.json if it exists
837
- const requiredMetaFile = path.join(config.requiredPagesFolder, `${name}.json`);
838
- if (await checkIfExists(requiredMetaFile)) {
839
- const meta = await loadFile(requiredMetaFile);
926
+ const metaPath = path.join(requiredPageDir, 'page.json');
927
+ if (await checkIfExists(metaPath)) {
928
+ const meta = await loadFile(metaPath);
840
929
  zip.addFile(`${name}/page.json`, Buffer.from(meta, 'utf-8'));
841
930
  }
842
931
 
@@ -863,4 +952,39 @@ Return ONLY the JSON object.`};
863
952
  res.status(500).json({ error: (err as Error).message });
864
953
  }
865
954
  });
955
+
956
+ // Ask a question about a page (with full page HTML context)
957
+ app.post('/api/pages/:name/ask', async (req, res) => {
958
+ await requiresSettings(res, config, async (settings) => {
959
+ const { name } = req.params;
960
+ const { question } = req.body;
961
+ if (typeof question !== 'string' || !question.trim()) {
962
+ res.status(400).json({ error: 'question is required' });
963
+ return;
964
+ }
965
+
966
+ // Load the page HTML
967
+ const html = await loadPageWithFallback(name, config, false);
968
+ if (!html) {
969
+ res.status(404).json({ error: `Page "${name}" not found` });
970
+ return;
971
+ }
972
+
973
+ // Create completion (uses 'chat' model, not 'builder')
974
+ const complete = await createCompletePrompt(config, 'chat', req.body.model);
975
+
976
+ const system = {
977
+ role: 'system' as const,
978
+ content: `You are a helpful assistant. The user will ask questions about a web page. Answer based on the page content provided.\n\n<PAGE_HTML>\n${html}`
979
+ };
980
+ const prompt = { role: 'user' as const, content: question };
981
+
982
+ const result = await complete({ system, prompt });
983
+ if (result.completed) {
984
+ res.json({ answer: result.value });
985
+ } else {
986
+ res.status(500).json({ error: result.error?.message || 'Completion failed' });
987
+ }
988
+ });
989
+ });
866
990
  }