synthos 0.8.0 → 0.9.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 (359) 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 +1822 -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} +14 -12
  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} +14 -11
  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.map +1 -1
  100. package/dist/connectors/index.js +3 -2
  101. package/dist/connectors/index.js.map +1 -1
  102. package/dist/connectors/registry.d.ts +2 -1
  103. package/dist/connectors/registry.d.ts.map +1 -1
  104. package/dist/connectors/registry.js +31 -8
  105. package/dist/connectors/registry.js.map +1 -1
  106. package/dist/customizer/Customizer.d.ts +57 -0
  107. package/dist/customizer/Customizer.d.ts.map +1 -0
  108. package/dist/customizer/Customizer.js +124 -0
  109. package/dist/customizer/Customizer.js.map +1 -0
  110. package/dist/customizer/index.d.ts.map +1 -0
  111. package/dist/customizer/index.js +9 -0
  112. package/dist/customizer/index.js.map +1 -0
  113. package/dist/files.d.ts +16 -0
  114. package/dist/files.d.ts.map +1 -1
  115. package/dist/files.js +60 -1
  116. package/dist/files.js.map +1 -1
  117. package/dist/index.d.ts.map +1 -1
  118. package/dist/index.js +1 -0
  119. package/dist/index.js.map +1 -1
  120. package/dist/init.d.ts +10 -6
  121. package/dist/init.d.ts.map +1 -1
  122. package/dist/init.js +96 -113
  123. package/dist/init.js.map +1 -1
  124. package/dist/migrations.d.ts.map +1 -1
  125. package/dist/migrations.js +23 -10
  126. package/dist/migrations.js.map +1 -1
  127. package/dist/models/anthropic.d.ts +4 -2
  128. package/dist/models/anthropic.d.ts.map +1 -1
  129. package/dist/models/anthropic.js +33 -6
  130. package/dist/models/anthropic.js.map +1 -1
  131. package/dist/models/fireworksai.d.ts.map +1 -1
  132. package/dist/models/fireworksai.js +9 -1
  133. package/dist/models/fireworksai.js.map +1 -1
  134. package/dist/models/index.d.ts +1 -1
  135. package/dist/models/index.d.ts.map +1 -1
  136. package/dist/models/index.js +2 -1
  137. package/dist/models/index.js.map +1 -1
  138. package/dist/models/openai.d.ts +1 -1
  139. package/dist/models/openai.d.ts.map +1 -1
  140. package/dist/models/openai.js +24 -3
  141. package/dist/models/openai.js.map +1 -1
  142. package/dist/models/types.d.ts +20 -1
  143. package/dist/models/types.d.ts.map +1 -1
  144. package/dist/models/types.js +6 -1
  145. package/dist/models/types.js.map +1 -1
  146. package/dist/pages.d.ts +30 -7
  147. package/dist/pages.d.ts.map +1 -1
  148. package/dist/pages.js +177 -55
  149. package/dist/pages.js.map +1 -1
  150. package/dist/service/server.d.ts.map +1 -1
  151. package/dist/service/server.js +37 -8
  152. package/dist/service/server.js.map +1 -1
  153. package/dist/service/transformPage.d.ts +47 -20
  154. package/dist/service/transformPage.d.ts.map +1 -1
  155. package/dist/service/transformPage.js +514 -293
  156. package/dist/service/transformPage.js.map +1 -1
  157. package/dist/service/useAgentRoutes.d.ts +2 -1
  158. package/dist/service/useAgentRoutes.d.ts.map +1 -1
  159. package/dist/service/useAgentRoutes.js +5 -2
  160. package/dist/service/useAgentRoutes.js.map +1 -1
  161. package/dist/service/useApiRoutes.d.ts.map +1 -1
  162. package/dist/service/useApiRoutes.js +237 -136
  163. package/dist/service/useApiRoutes.js.map +1 -1
  164. package/dist/service/useConnectorRoutes.js +6 -6
  165. package/dist/service/useConnectorRoutes.js.map +1 -1
  166. package/dist/service/useFileRoutes.d.ts +4 -0
  167. package/dist/service/useFileRoutes.d.ts.map +1 -0
  168. package/dist/service/useFileRoutes.js +122 -0
  169. package/dist/service/useFileRoutes.js.map +1 -0
  170. package/dist/service/usePageRoutes.d.ts.map +1 -1
  171. package/dist/service/usePageRoutes.js +648 -67
  172. package/dist/service/usePageRoutes.js.map +1 -1
  173. package/dist/service/useSharedDataRoutes.d.ts +4 -0
  174. package/dist/service/useSharedDataRoutes.d.ts.map +1 -0
  175. package/dist/service/useSharedDataRoutes.js +104 -0
  176. package/dist/service/useSharedDataRoutes.js.map +1 -0
  177. package/dist/service/useSharedFileRoutes.d.ts +4 -0
  178. package/dist/service/useSharedFileRoutes.d.ts.map +1 -0
  179. package/dist/service/useSharedFileRoutes.js +121 -0
  180. package/dist/service/useSharedFileRoutes.js.map +1 -0
  181. package/dist/settings.d.ts +1 -0
  182. package/dist/settings.d.ts.map +1 -1
  183. package/dist/settings.js +1 -0
  184. package/dist/settings.js.map +1 -1
  185. package/dist/synthos-cli.d.ts.map +1 -1
  186. package/dist/synthos-cli.js +4 -3
  187. package/dist/synthos-cli.js.map +1 -1
  188. package/dist/themes.d.ts +1 -0
  189. package/dist/themes.d.ts.map +1 -1
  190. package/dist/themes.js +28 -15
  191. package/dist/themes.js.map +1 -1
  192. package/migration-rules/v1-to-v2.md +193 -0
  193. package/migration-rules/v2-to-v3.md +481 -0
  194. package/package.json +11 -10
  195. package/required-pages/builder/page.html +43 -0
  196. package/required-pages/builder/page.json +10 -0
  197. package/required-pages/{pages.html → pages/page.html} +238 -233
  198. package/required-pages/pages/page.json +10 -0
  199. package/required-pages/{settings.html → settings/page.html} +389 -275
  200. package/required-pages/settings/page.json +10 -0
  201. package/required-pages/synthos_apis/page.html +846 -0
  202. package/required-pages/synthos_apis/page.json +10 -0
  203. package/required-pages/{synthos_scripts.html → synthos_scripts/page.html} +13 -11
  204. package/required-pages/synthos_scripts/page.json +10 -0
  205. package/src/agents/index.ts +1 -1
  206. package/src/agents/openclaw/gatewayManager.ts +22 -11
  207. package/src/agents/openclaw/openclawProvider.ts +2 -4
  208. package/src/agents/openclaw/sshTunnelManager.ts +19 -11
  209. package/src/builders/anthropic.ts +283 -0
  210. package/src/builders/fireworksai.ts +59 -0
  211. package/src/builders/index.ts +33 -0
  212. package/src/builders/openai.ts +89 -0
  213. package/src/builders/types.ts +261 -0
  214. package/src/connectors/index.ts +1 -1
  215. package/src/connectors/registry.ts +28 -8
  216. package/src/customizer/Customizer.ts +151 -0
  217. package/src/customizer/index.ts +5 -0
  218. package/src/files.ts +57 -0
  219. package/src/index.ts +2 -1
  220. package/src/init.ts +137 -123
  221. package/src/migrations.ts +30 -10
  222. package/src/models/anthropic.ts +40 -10
  223. package/src/models/fireworksai.ts +9 -2
  224. package/src/models/index.ts +1 -1
  225. package/src/models/openai.ts +26 -6
  226. package/src/models/types.ts +31 -1
  227. package/src/pages.ts +176 -54
  228. package/src/service/server.ts +36 -9
  229. package/src/service/transformPage.ts +557 -326
  230. package/src/service/useAgentRoutes.ts +7 -2
  231. package/src/service/useApiRoutes.ts +150 -41
  232. package/src/service/useConnectorRoutes.ts +7 -7
  233. package/src/service/useFileRoutes.ts +127 -0
  234. package/src/service/usePageRoutes.ts +720 -73
  235. package/src/service/useSharedDataRoutes.ts +106 -0
  236. package/src/service/useSharedFileRoutes.ts +126 -0
  237. package/src/settings.ts +2 -0
  238. package/src/synthos-cli.ts +4 -3
  239. package/src/themes.ts +25 -14
  240. package/static-files/favicon.svg +12 -0
  241. package/static-files/fluentlm-instructions.llmd +868 -0
  242. package/static-files/fluentlm-instructions.md +1595 -0
  243. package/static-files/fluentlm.css +4844 -0
  244. package/static-files/fluentlm.js +3602 -0
  245. package/static-files/fluentlm.min.css +1 -0
  246. package/static-files/fluentlm.min.js +1 -0
  247. package/{page-scripts/helpers-v2.js → static-files/helpers.v3.js} +82 -0
  248. package/static-files/page.v3.js +1290 -0
  249. package/static-files/recommended-frameworks.llmd +81 -0
  250. package/static-files/recommended-frameworks.md +137 -0
  251. package/static-files/retro-game.js +877 -0
  252. package/static-files/shell.css +797 -0
  253. package/static-files/theme-dark.css +169 -0
  254. package/static-files/theme-light.css +169 -0
  255. package/tests/builders.spec.ts +139 -0
  256. package/tests/pages.spec.ts +8 -8
  257. package/tests/transformPage.spec.ts +299 -360
  258. package/default-pages/application.html +0 -40
  259. package/default-pages/application.json +0 -1
  260. package/default-pages/json_tools.json +0 -1
  261. package/default-pages/my_notes.html +0 -33
  262. package/default-pages/neon_asteroids.html +0 -77
  263. package/default-pages/sidebar_page.json +0 -1
  264. package/default-pages/solar_tutorial.json +0 -1
  265. package/default-pages/two-panel_page.json +0 -1
  266. package/dist/agents/a2a/a2aProvider.d.ts +0 -3
  267. package/dist/agents/discovery.d.ts +0 -30
  268. package/dist/agents/openclaw/openclawProvider.d.ts +0 -3
  269. package/dist/agents/types.d.ts +0 -64
  270. package/dist/connectors/index.d.ts +0 -3
  271. package/dist/connectors/types.d.ts +0 -84
  272. package/dist/index.d.ts +0 -7
  273. package/dist/migrations.d.ts +0 -12
  274. package/dist/models/chainOfThought.d.ts +0 -12
  275. package/dist/models/fireworksai.d.ts +0 -30
  276. package/dist/models/logCompletePrompt.d.ts +0 -3
  277. package/dist/models/providers.d.ts +0 -8
  278. package/dist/models/utils.d.ts +0 -6
  279. package/dist/scripts.d.ts +0 -15
  280. package/dist/service/createCompletePrompt.d.ts +0 -5
  281. package/dist/service/debugLog.d.ts +0 -11
  282. package/dist/service/generateImage.d.ts +0 -32
  283. package/dist/service/index.d.ts +0 -8
  284. package/dist/service/modelInstructions.d.ts +0 -7
  285. package/dist/service/requiresSettings.d.ts +0 -3
  286. package/dist/service/server.d.ts +0 -4
  287. package/dist/service/useApiRoutes.d.ts +0 -4
  288. package/dist/service/useConnectorRoutes.d.ts +0 -4
  289. package/dist/service/useDataRoutes.d.ts +0 -4
  290. package/dist/service/useGatewayRoutes.d.ts +0 -4
  291. package/dist/service/useGatewayRoutes.d.ts.map +0 -1
  292. package/dist/service/useGatewayRoutes.js +0 -168
  293. package/dist/service/useGatewayRoutes.js.map +0 -1
  294. package/dist/service/usePageRoutes.d.ts +0 -5
  295. package/dist/synthos-cli.d.ts +0 -2
  296. package/page-scripts/page-v2.js +0 -656
  297. package/required-pages/builder.html +0 -48
  298. package/required-pages/builder.json +0 -1
  299. package/required-pages/pages.json +0 -1
  300. package/required-pages/settings.json +0 -1
  301. package/required-pages/synthos_apis.html +0 -327
  302. package/required-pages/synthos_apis.json +0 -1
  303. package/required-pages/synthos_scripts.json +0 -1
  304. package/src/connectors/airtable/connector.json +0 -27
  305. package/src/connectors/alpha-vantage/connector.json +0 -26
  306. package/src/connectors/brave-search/connector.json +0 -26
  307. package/src/connectors/cloudinary/connector.json +0 -27
  308. package/src/connectors/deepl/connector.json +0 -28
  309. package/src/connectors/elevenlabs/connector.json +0 -30
  310. package/src/connectors/giphy/connector.json +0 -27
  311. package/src/connectors/github/connector.json +0 -29
  312. package/src/connectors/huggingface/connector.json +0 -27
  313. package/src/connectors/imgur/connector.json +0 -29
  314. package/src/connectors/instagram/connector.json +0 -43
  315. package/src/connectors/jira/connector.json +0 -28
  316. package/src/connectors/mapbox/connector.json +0 -26
  317. package/src/connectors/nasa/connector.json +0 -27
  318. package/src/connectors/newsapi/connector.json +0 -27
  319. package/src/connectors/notion/connector.json +0 -28
  320. package/src/connectors/open-exchange-rates/connector.json +0 -27
  321. package/src/connectors/openweathermap/connector.json +0 -26
  322. package/src/connectors/pexels/connector.json +0 -27
  323. package/src/connectors/resend/connector.json +0 -29
  324. package/src/connectors/rss2json/connector.json +0 -27
  325. package/src/connectors/sendgrid/connector.json +0 -27
  326. package/src/connectors/spoonacular/connector.json +0 -28
  327. package/src/connectors/stability-ai/connector.json +0 -27
  328. package/src/connectors/twilio/connector.json +0 -28
  329. package/src/connectors/unsplash/connector.json +0 -27
  330. package/src/connectors/wolfram-alpha/connector.json +0 -26
  331. package/src/connectors/youtube-data/connector.json +0 -30
  332. /package/{dist/connectors → service-connectors}/airtable/connector.json +0 -0
  333. /package/{dist/connectors → service-connectors}/alpha-vantage/connector.json +0 -0
  334. /package/{dist/connectors → service-connectors}/brave-search/connector.json +0 -0
  335. /package/{dist/connectors → service-connectors}/cloudinary/connector.json +0 -0
  336. /package/{dist/connectors → service-connectors}/deepl/connector.json +0 -0
  337. /package/{dist/connectors → service-connectors}/elevenlabs/connector.json +0 -0
  338. /package/{dist/connectors → service-connectors}/giphy/connector.json +0 -0
  339. /package/{dist/connectors → service-connectors}/github/connector.json +0 -0
  340. /package/{dist/connectors → service-connectors}/huggingface/connector.json +0 -0
  341. /package/{dist/connectors → service-connectors}/imgur/connector.json +0 -0
  342. /package/{dist/connectors → service-connectors}/instagram/connector.json +0 -0
  343. /package/{dist/connectors → service-connectors}/jira/connector.json +0 -0
  344. /package/{dist/connectors → service-connectors}/mapbox/connector.json +0 -0
  345. /package/{dist/connectors → service-connectors}/nasa/connector.json +0 -0
  346. /package/{dist/connectors → service-connectors}/newsapi/connector.json +0 -0
  347. /package/{dist/connectors → service-connectors}/notion/connector.json +0 -0
  348. /package/{dist/connectors → service-connectors}/open-exchange-rates/connector.json +0 -0
  349. /package/{dist/connectors → service-connectors}/openweathermap/connector.json +0 -0
  350. /package/{dist/connectors → service-connectors}/pexels/connector.json +0 -0
  351. /package/{dist/connectors → service-connectors}/resend/connector.json +0 -0
  352. /package/{dist/connectors → service-connectors}/rss2json/connector.json +0 -0
  353. /package/{dist/connectors → service-connectors}/sendgrid/connector.json +0 -0
  354. /package/{dist/connectors → service-connectors}/spoonacular/connector.json +0 -0
  355. /package/{dist/connectors → service-connectors}/stability-ai/connector.json +0 -0
  356. /package/{dist/connectors → service-connectors}/twilio/connector.json +0 -0
  357. /package/{dist/connectors → service-connectors}/unsplash/connector.json +0 -0
  358. /package/{dist/connectors → service-connectors}/wolfram-alpha/connector.json +0 -0
  359. /package/{dist/connectors → service-connectors}/youtube-data/connector.json +0 -0
@@ -12,10 +12,14 @@ import {
12
12
  disconnectAgent,
13
13
  getAgentStatus,
14
14
  getTunnelStatus,
15
+ setOpenClawDebug,
15
16
  } from '../agents';
16
17
  import { v4 as uuidv4 } from 'uuid';
18
+ import { Customizer } from '../customizer';
17
19
 
18
- export function useAgentRoutes(config: SynthOSConfig, app: Application): void {
20
+ export function useAgentRoutes(config: SynthOSConfig, app: Application, customizer?: Customizer): void {
21
+ // Enable OpenClaw debug logging only when --debug is passed
22
+ setOpenClawDebug(config.debug);
19
23
 
20
24
  /** Strip the token and sshTunnel.password fields, add connection/tunnel status for agent responses. */
21
25
  function toClientAgent(agent: AgentConfig): Record<string, unknown> {
@@ -41,6 +45,7 @@ export function useAgentRoutes(config: SynthOSConfig, app: Application): void {
41
45
  url: agent.url,
42
46
  token: agent.token,
43
47
  sshTunnel: agent.sshTunnel,
48
+ productName: customizer?.productName,
44
49
  })
45
50
  .then(() => console.log(`[Agents] Auto-connected OpenClaw agent "${agent.name}"`))
46
51
  .catch(err => console.warn(`[Agents] Auto-connect failed for "${agent.name}": ${err instanceof Error ? err.message : err}`));
@@ -241,7 +246,7 @@ export function useAgentRoutes(config: SynthOSConfig, app: Application): void {
241
246
  return;
242
247
  }
243
248
 
244
- await connectAgent({ id: agent.id, name: agent.name, url: agent.url, token: agent.token, sshTunnel: agent.sshTunnel });
249
+ await connectAgent({ id: agent.id, name: agent.name, url: agent.url, token: agent.token, sshTunnel: agent.sshTunnel, productName: customizer?.productName });
245
250
  const status = getAgentStatus(agent.id);
246
251
  res.json({ connected: status.connected, authenticated: status.authenticated });
247
252
  } catch (err: unknown) {
@@ -1,8 +1,8 @@
1
1
  import path from "path";
2
2
  import fs from "fs/promises";
3
3
  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";
4
+ import { listPages, loadPageMetadata, PageMetadata, savePageMetadata, deletePage, copyPage, loadPageState, savePageState, clearVersions, PAGE_VERSION } from "../pages";
5
+ import { checkIfExists, copyFile, copyFolderRecursive, deleteFile, ensureFolderExists, findFileInFolders, listFolders, loadFile } from "../files";
6
6
  import {getModelEntry, loadSettings, saveSettings, ServicesConfig } from "../settings";
7
7
  import { Application } from 'express';
8
8
  import express from 'express';
@@ -12,9 +12,10 @@ import { generateDefaultImage, generateImage } from "./generateImage";
12
12
  import { chainOfThought } from "../models";
13
13
  import { requiresSettings } from "./requiresSettings";
14
14
  import { executeScript } from "../scripts";
15
- import { listThemes, loadTheme, loadThemeInfo } from "../themes";
15
+ import { listThemes, loadTheme, loadThemeInfo, loadThemeVersion } from "../themes";
16
16
  import { migratePage } from "../migrations";
17
17
  import { loadPageWithFallback } from "./usePageRoutes";
18
+ import { Customizer } from "../customizer";
18
19
 
19
20
  // ---------------------------------------------------------------------------
20
21
  // Service registry
@@ -48,10 +49,10 @@ const SERVICE_REGISTRY: ServiceDefinition[] = [
48
49
  }
49
50
  ];
50
51
 
51
- export function useApiRoutes(config: SynthOSConfig, app: Application): void {
52
+ export function useApiRoutes(config: SynthOSConfig, app: Application, customizer?: Customizer): void {
52
53
  // List pages
53
54
  app.get('/api/pages', async (req, res) => {
54
- const pages = await listPages(config.pagesFolder, config.requiredPagesFolder);
55
+ const pages = await listPages(config.pagesFolder, config.requiredPagesFolders);
55
56
  res.json(pages);
56
57
  });
57
58
 
@@ -149,7 +150,7 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
149
150
  app.get('/api/pages/:name', async (req, res) => {
150
151
  try {
151
152
  const { name } = req.params;
152
- const metadata = await loadPageMetadata(config.pagesFolder, name, config.requiredPagesFolder);
153
+ const metadata = await loadPageMetadata(config.pagesFolder, name, config.requiredPagesFolders);
153
154
  if (metadata) {
154
155
  res.json(metadata);
155
156
  } else {
@@ -200,7 +201,7 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
200
201
  }
201
202
 
202
203
  // Load existing metadata (or defaults)
203
- const existing = await loadPageMetadata(config.pagesFolder, name, config.requiredPagesFolder);
204
+ const existing = await loadPageMetadata(config.pagesFolder, name, config.requiredPagesFolders);
204
205
  const metadata: PageMetadata = {
205
206
  title: existing?.title ?? '',
206
207
  categories: existing?.categories ?? [],
@@ -226,7 +227,11 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
226
227
  if (metadata.mode !== 'locked') {
227
228
  const userPagePath = path.join(config.pagesFolder, 'pages', name, 'page.html');
228
229
  if (!(await checkIfExists(userPagePath))) {
229
- const html = await loadPageState(config.requiredPagesFolder, name, false);
230
+ let html: string | undefined;
231
+ for (const folder of config.requiredPagesFolders) {
232
+ html = await loadPageState(folder, name);
233
+ if (html) break;
234
+ }
230
235
  if (html) {
231
236
  await savePageState(config.pagesFolder, name, html);
232
237
  }
@@ -252,7 +257,7 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
252
257
  }
253
258
 
254
259
  // Load existing metadata (user override → fallback .json → defaults)
255
- let metadata = await loadPageMetadata(config.pagesFolder, name, config.requiredPagesFolder);
260
+ let metadata = await loadPageMetadata(config.pagesFolder, name, config.requiredPagesFolders);
256
261
  if (!metadata) {
257
262
  metadata = {
258
263
  title: '',
@@ -281,7 +286,7 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
281
286
  const { name } = req.params;
282
287
 
283
288
  // Cannot delete required pages
284
- if (REQUIRED_PAGES.includes(name)) {
289
+ if (config.requiredPages.includes(name)) {
285
290
  res.status(400).json({ error: `Cannot delete required page "${name}"` });
286
291
  return;
287
292
  }
@@ -303,11 +308,56 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
303
308
  }
304
309
  });
305
310
 
311
+ // Discover what a page contains (tables + files)
312
+ app.get('/api/pages/:name/contents', async (req, res) => {
313
+ try {
314
+ const { name } = req.params;
315
+
316
+ // Resolve page folder: user pages first, then required pages
317
+ let pageFolder: string | undefined;
318
+ const userFolder = path.join(config.pagesFolder, 'pages', name);
319
+ if (await checkIfExists(path.join(userFolder, 'page.html'))) {
320
+ pageFolder = userFolder;
321
+ } else {
322
+ for (const folder of config.requiredPagesFolders) {
323
+ const candidate = path.join(folder, name);
324
+ if (await checkIfExists(path.join(candidate, 'page.html'))) {
325
+ pageFolder = candidate;
326
+ break;
327
+ }
328
+ }
329
+ }
330
+
331
+ if (!pageFolder) {
332
+ res.status(404).json({ error: `Page "${name}" not found` });
333
+ return;
334
+ }
335
+
336
+ // List subdirectories, filtering out non-table entries
337
+ const EXCLUDED = new Set(['files']);
338
+ const subdirs = await listFolders(pageFolder);
339
+ const tables = subdirs.filter(d => !EXCLUDED.has(d));
340
+
341
+ // Check if files/ exists and has entries
342
+ const filesDir = path.join(pageFolder, 'files');
343
+ let hasFiles = false;
344
+ if (await checkIfExists(filesDir)) {
345
+ const entries = await fs.readdir(filesDir);
346
+ hasFiles = entries.length > 0;
347
+ }
348
+
349
+ res.json({ tables, hasFiles });
350
+ } catch (err: unknown) {
351
+ console.error(err);
352
+ res.status(500).json({ error: (err as Error).message });
353
+ }
354
+ });
355
+
306
356
  // Copy a page to a new name
307
357
  app.post('/api/pages/:name/copy', async (req, res) => {
308
358
  try {
309
359
  const sourceName = req.params.name;
310
- const { name: targetName, title, categories } = req.body;
360
+ const { name: targetName, title, categories, copyTables, copyFiles } = req.body;
311
361
 
312
362
  // Validate target name
313
363
  if (!targetName || typeof targetName !== 'string') {
@@ -329,10 +379,14 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
329
379
  // Check source exists (user pages → required pages)
330
380
  const sourceFolderPath = path.join(config.pagesFolder, 'pages', sourceName, 'page.html');
331
381
  const sourceFlatPath = path.join(config.pagesFolder, `${sourceName}.html`);
332
- const sourceRequiredPath = path.join(config.requiredPagesFolder, `${sourceName}.html`);
382
+ let sourceRequiredPath: string | undefined;
383
+ for (const folder of config.requiredPagesFolders) {
384
+ const candidate = path.join(folder, sourceName, 'page.html');
385
+ if (await checkIfExists(candidate)) { sourceRequiredPath = candidate; break; }
386
+ }
333
387
  const sourceExists = await checkIfExists(sourceFolderPath)
334
388
  || await checkIfExists(sourceFlatPath)
335
- || await checkIfExists(sourceRequiredPath);
389
+ || !!sourceRequiredPath;
336
390
  if (!sourceExists) {
337
391
  res.status(404).json({ error: `Source page "${sourceName}" not found` });
338
392
  return;
@@ -352,7 +406,11 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
352
406
  targetName,
353
407
  typeof title === 'string' ? title : '',
354
408
  Array.isArray(categories) ? categories : [],
355
- config.requiredPagesFolder
409
+ config.requiredPagesFolders,
410
+ {
411
+ copyTables: copyTables === true,
412
+ copyFiles: copyFiles !== false, // default true
413
+ }
356
414
  );
357
415
 
358
416
  // Return the new page metadata
@@ -428,16 +486,18 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
428
486
  });
429
487
 
430
488
  // Brainstorm endpoint
489
+ if (!customizer || customizer.isEnabled('brainstorm'))
431
490
  app.post('/api/brainstorm', async (req, res) => {
432
491
  await requiresSettings(res, config.pagesFolder, async (settings) => {
433
492
  const { context, messages } = req.body;
434
493
  const completePrompt = await createCompletePrompt(config.pagesFolder, 'chat');
435
494
 
495
+ const productName = customizer?.productName ?? 'SynthOS';
436
496
  const system: { role: 'system'; content: string } = {
437
497
  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.
498
+ content: `You are a creative brainstorming assistant for ${productName}, a tool that builds pages through conversation.
499
+ ${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.
500
+ The user is brainstorming — exploring ideas before building. Be concise, creative, and collaborative.
441
501
  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
502
  The goal is to help them generate a prompt for the builder that captures their vision, along with suggestions for next steps.
443
503
  Suggest concrete approaches when you can, not complex visions for some ellaborate app.
@@ -448,14 +508,14 @@ ${context}
448
508
 
449
509
  <INSTRUCTIONS>
450
510
  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.
511
+ 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
512
 
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.
513
+ ${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
514
 
455
515
  You MUST return your response as a JSON object with exactly these fields:
456
516
  {
457
517
  "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.",
518
+ "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
519
  "suggestions": ["Short clickable option A", "Short clickable option B", "Short clickable option C"]
460
520
  }
461
521
 
@@ -495,6 +555,7 @@ Return ONLY the JSON object.`};
495
555
  });
496
556
 
497
557
  // Define a route for running configured scripts
558
+ if (!customizer || customizer.isEnabled('scripts'))
498
559
  app.post('/api/scripts/:id', async (req, res) => {
499
560
  await requiresSettings(res, config.pagesFolder, async (settings) => {
500
561
  const { id } = req.params;
@@ -523,7 +584,13 @@ Return ONLY the JSON object.`};
523
584
  res.status(404).send(`// Theme info for "${themeName}" not found`);
524
585
  return;
525
586
  }
526
- const js = `window.themeInfo=${JSON.stringify(info)};document.documentElement.classList.add(window.themeInfo.mode+"-mode");`;
587
+ const themeVersion = await loadThemeVersion(themeName, config);
588
+ const payload = { ...info, name: themeName, version: themeVersion };
589
+ let js = `window.themeInfo=${JSON.stringify(payload)};document.documentElement.classList.add(window.themeInfo.mode+"-mode");`;
590
+ if (themeVersion >= 3) {
591
+ js += `document.documentElement.classList.add(${JSON.stringify(themeName)});`;
592
+ }
593
+ js += `document.documentElement.setAttribute("data-toolbar",${JSON.stringify(settings.toolbarPosition || 'left')});`;
527
594
  res.set('Content-Type', 'application/javascript');
528
595
  res.send(js);
529
596
  } catch (err: unknown) {
@@ -540,21 +607,18 @@ Return ONLY the JSON object.`};
540
607
  res.status(400).send('// Missing page query parameter');
541
608
  return;
542
609
  }
543
- const metadata = await loadPageMetadata(config.pagesFolder, page, config.requiredPagesFolder);
610
+ const metadata = await loadPageMetadata(config.pagesFolder, page, config.requiredPagesFolders);
544
611
  const mode = metadata?.mode ?? 'unlocked';
545
612
  const title = metadata?.title ?? '';
546
613
  const categories = metadata?.categories ?? [];
547
- const info = JSON.stringify({ name: page, mode, latestPageVersion: PAGE_VERSION, title, categories });
614
+ const isRequiredPage = config.requiredPages.includes(page);
615
+ const productName = customizer?.productName ?? 'SynthOS';
616
+ const info = JSON.stringify({ name: page, mode, latestPageVersion: PAGE_VERSION, title, categories, isRequiredPage, productName });
548
617
  const js = [
549
618
  `window.pageInfo=${info};`,
550
619
  `if(window.pageInfo.mode==="locked"){`,
551
620
  `document.addEventListener("DOMContentLoaded",function(){`,
552
621
  `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
622
  `});`,
559
623
  `}`,
560
624
  ].join('');
@@ -604,8 +668,8 @@ Return ONLY the JSON object.`};
604
668
  res.status(400).send('// Invalid version parameter');
605
669
  return;
606
670
  }
607
- const scriptPath = path.join(config.pageScriptsFolder, `page-v${v}.js`);
608
- if (!(await checkIfExists(scriptPath))) {
671
+ const scriptPath = await findFileInFolders(config.staticFilesFolders, `page.v${v}.js`);
672
+ if (!scriptPath) {
609
673
  res.status(404).send(`// page-v${v}.js not found`);
610
674
  return;
611
675
  }
@@ -627,8 +691,8 @@ Return ONLY the JSON object.`};
627
691
  res.status(400).send('// Invalid version parameter');
628
692
  return;
629
693
  }
630
- const scriptPath = path.join(config.pageScriptsFolder, `helpers-v${v}.js`);
631
- if (!(await checkIfExists(scriptPath))) {
694
+ const scriptPath = await findFileInFolders(config.staticFilesFolders, `helpers.v${v}.js`);
695
+ if (!scriptPath) {
632
696
  res.status(404).send(`// helpers-v${v}.js not found`);
633
697
  return;
634
698
  }
@@ -707,6 +771,7 @@ Return ONLY the JSON object.`};
707
771
  // Web Search (Brave Search API)
708
772
  // -----------------------------------------------------------------------
709
773
 
774
+ if (!customizer || customizer.isEnabled('search'))
710
775
  app.post('/api/search/web', async (req, res) => {
711
776
  try {
712
777
  const { query, count, country, freshness } = req.body;
@@ -761,7 +826,7 @@ Return ONLY the JSON object.`};
761
826
  const { name } = req.params;
762
827
 
763
828
  // Load current metadata
764
- const metadata = await loadPageMetadata(config.pagesFolder, name, config.requiredPagesFolder);
829
+ const metadata = await loadPageMetadata(config.pagesFolder, name, config.requiredPagesFolders);
765
830
  if (!metadata) {
766
831
  res.status(404).json({ error: `Page "${name}" not found` });
767
832
  return;
@@ -790,19 +855,22 @@ Return ONLY the JSON object.`};
790
855
  // Backup original page to .migrated/ before overwriting
791
856
  const migratedFolder = path.join(config.pagesFolder, '.migrated');
792
857
 
793
- // Handle legacy flat file (.synthos/pagename.html)
858
+ // Handle legacy flat file (<localFolder>/pagename.html)
794
859
  const flatPath = path.join(config.pagesFolder, `${name}.html`);
795
860
  if (await checkIfExists(flatPath)) {
796
861
  await copyFile(flatPath, migratedFolder);
797
862
  await deleteFile(flatPath);
798
863
  }
799
864
 
800
- // Handle folder-based page (.synthos/pages/name/)
865
+ // Handle folder-based page (<localFolder>/pages/name/)
801
866
  const folderPath = path.join(config.pagesFolder, 'pages', name);
802
867
  if (await checkIfExists(folderPath)) {
803
868
  await copyFolderRecursive(folderPath, path.join(migratedFolder, name));
804
869
  }
805
870
 
871
+ // Clear stale version files (undo snapshots from the old page version)
872
+ await clearVersions(config.pagesFolder, name);
873
+
806
874
  // Update metadata
807
875
  metadata.pageVersion = PAGE_VERSION;
808
876
  metadata.lastModified = new Date().toISOString();
@@ -822,21 +890,27 @@ Return ONLY the JSON object.`};
822
890
 
823
891
  // Try user pages folder first, then required pages
824
892
  const userPageDir = path.join(config.pagesFolder, 'pages', name);
825
- const requiredPageFile = path.join(config.requiredPagesFolder, `${name}.html`);
893
+ let requiredPageDir: string | undefined;
894
+ for (const folder of config.requiredPagesFolders) {
895
+ if (await checkIfExists(path.join(folder, name, 'page.html'))) {
896
+ requiredPageDir = path.join(folder, name);
897
+ break;
898
+ }
899
+ }
826
900
  let sourceDir: string | null = null;
827
901
 
828
902
  if (await checkIfExists(path.join(userPageDir, 'page.html'))) {
829
903
  sourceDir = userPageDir;
830
- } else if (await checkIfExists(requiredPageFile)) {
904
+ } else if (requiredPageDir) {
831
905
  // For required pages, create a temp-like zip with just the HTML
832
906
  const zip = new AdmZip();
833
- const html = await loadFile(requiredPageFile);
907
+ const html = await loadFile(path.join(requiredPageDir, 'page.html'));
834
908
  zip.addFile(`${name}/page.html`, Buffer.from(html, 'utf-8'));
835
909
 
836
910
  // 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);
911
+ const metaPath = path.join(requiredPageDir, 'page.json');
912
+ if (await checkIfExists(metaPath)) {
913
+ const meta = await loadFile(metaPath);
840
914
  zip.addFile(`${name}/page.json`, Buffer.from(meta, 'utf-8'));
841
915
  }
842
916
 
@@ -863,4 +937,39 @@ Return ONLY the JSON object.`};
863
937
  res.status(500).json({ error: (err as Error).message });
864
938
  }
865
939
  });
940
+
941
+ // Ask a question about a page (with full page HTML context)
942
+ app.post('/api/pages/:name/ask', async (req, res) => {
943
+ await requiresSettings(res, config.pagesFolder, async (settings) => {
944
+ const { name } = req.params;
945
+ const { question } = req.body;
946
+ if (typeof question !== 'string' || !question.trim()) {
947
+ res.status(400).json({ error: 'question is required' });
948
+ return;
949
+ }
950
+
951
+ // Load the page HTML
952
+ const html = await loadPageWithFallback(name, config, false);
953
+ if (!html) {
954
+ res.status(404).json({ error: `Page "${name}" not found` });
955
+ return;
956
+ }
957
+
958
+ // Create completion (uses 'chat' model, not 'builder')
959
+ const complete = await createCompletePrompt(config.pagesFolder, 'chat', req.body.model);
960
+
961
+ const system = {
962
+ role: 'system' as const,
963
+ 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}`
964
+ };
965
+ const prompt = { role: 'user' as const, content: question };
966
+
967
+ const result = await complete({ system, prompt });
968
+ if (result.completed) {
969
+ res.json({ answer: result.value });
970
+ } else {
971
+ res.status(500).json({ error: result.error?.message || 'Completion failed' });
972
+ }
973
+ });
974
+ });
866
975
  }
@@ -2,7 +2,7 @@ import { Application } from 'express';
2
2
  import { SynthOSConfig } from '../init';
3
3
  import { loadSettings, saveSettings } from '../settings';
4
4
  import {
5
- CONNECTOR_REGISTRY,
5
+ getConnectorRegistry,
6
6
  ConnectorSummary,
7
7
  ConnectorDetail,
8
8
  ConnectorCallRequest,
@@ -21,7 +21,7 @@ export function useConnectorRoutes(config: SynthOSConfig, app: Application): voi
21
21
  const categoryFilter = req.query.category as string | undefined;
22
22
  const idFilter = req.query.id as string | undefined;
23
23
 
24
- const list: ConnectorSummary[] = CONNECTOR_REGISTRY
24
+ const list: ConnectorSummary[] = getConnectorRegistry(config.serviceConnectorsFolders)
25
25
  .filter(def => {
26
26
  if (categoryFilter && def.category !== categoryFilter) return false;
27
27
  if (idFilter && def.id !== idFilter) return false;
@@ -54,7 +54,7 @@ export function useConnectorRoutes(config: SynthOSConfig, app: Application): voi
54
54
  app.get('/api/connectors/:id', async (req, res) => {
55
55
  try {
56
56
  const { id } = req.params;
57
- const def = CONNECTOR_REGISTRY.find(d => d.id === id);
57
+ const def = getConnectorRegistry(config.serviceConnectorsFolders).find(d => d.id === id);
58
58
  if (!def) {
59
59
  res.status(404).json({ error: `Connector "${id}" not found` });
60
60
  return;
@@ -88,7 +88,7 @@ export function useConnectorRoutes(config: SynthOSConfig, app: Application): voi
88
88
  app.post('/api/connectors/:id', async (req, res) => {
89
89
  try {
90
90
  const { id } = req.params;
91
- const def = CONNECTOR_REGISTRY.find(d => d.id === id);
91
+ const def = getConnectorRegistry(config.serviceConnectorsFolders).find(d => d.id === id);
92
92
  if (!def) {
93
93
  res.status(404).json({ error: `Connector "${id}" not found` });
94
94
  return;
@@ -164,7 +164,7 @@ export function useConnectorRoutes(config: SynthOSConfig, app: Application): voi
164
164
  app.get('/api/connectors/:id/authorize', async (req, res) => {
165
165
  try {
166
166
  const { id } = req.params;
167
- const def = CONNECTOR_REGISTRY.find(d => d.id === id);
167
+ const def = getConnectorRegistry(config.serviceConnectorsFolders).find(d => d.id === id);
168
168
  if (!def || def.authStrategy !== 'oauth2') {
169
169
  res.status(400).json({ error: `Connector "${id}" is not an OAuth2 connector` });
170
170
  return;
@@ -214,7 +214,7 @@ export function useConnectorRoutes(config: SynthOSConfig, app: Application): voi
214
214
  const state = JSON.parse(stateRaw) as { connector: string };
215
215
  const connectorId = state.connector;
216
216
 
217
- const def = CONNECTOR_REGISTRY.find(d => d.id === connectorId);
217
+ const def = getConnectorRegistry(config.serviceConnectorsFolders).find(d => d.id === connectorId);
218
218
  if (!def || def.authStrategy !== 'oauth2') {
219
219
  res.status(400).json({ error: `Unknown OAuth2 connector: ${connectorId}` });
220
220
  return;
@@ -326,7 +326,7 @@ export function useConnectorRoutes(config: SynthOSConfig, app: Application): voi
326
326
  return;
327
327
  }
328
328
 
329
- const def = CONNECTOR_REGISTRY.find(d => d.id === request.connector);
329
+ const def = getConnectorRegistry(config.serviceConnectorsFolders).find(d => d.id === request.connector);
330
330
  if (!def) {
331
331
  res.status(404).json({ error: `Connector "${request.connector}" not found` });
332
332
  return;
@@ -0,0 +1,127 @@
1
+ import { Application } from 'express';
2
+ import express from 'express';
3
+ import fs from 'fs/promises';
4
+ import path from 'path';
5
+ import { SynthOSConfig } from '../init';
6
+ import { checkIfExists, ensureFolderExists } from '../files';
7
+
8
+ export function useFileRoutes(config: SynthOSConfig, app: Application): void {
9
+ // List files in a page's files/ folder
10
+ app.get('/api/files/:page', async (req, res) => {
11
+ try {
12
+ const folder = filesFolder(config, req.params.page);
13
+ if (!(await checkIfExists(folder))) {
14
+ res.json({ files: [] });
15
+ return;
16
+ }
17
+
18
+ const entries = await fs.readdir(folder);
19
+ const files: { name: string; size: number }[] = [];
20
+ for (const entry of entries) {
21
+ const stat = await fs.stat(path.join(folder, entry));
22
+ if (stat.isFile()) {
23
+ files.push({ name: entry, size: stat.size });
24
+ }
25
+ }
26
+ res.json({ files });
27
+ } catch (err: unknown) {
28
+ console.error(err);
29
+ res.status(500).json({ error: (err as Error).message });
30
+ }
31
+ });
32
+
33
+ // Download/serve a specific file
34
+ app.get('/api/files/:page/:filename', async (req, res) => {
35
+ try {
36
+ const filePath = safeFilePath(config, req.params.page, req.params.filename);
37
+ if (!filePath) {
38
+ res.status(400).json({ error: 'Invalid filename' });
39
+ return;
40
+ }
41
+
42
+ if (!(await checkIfExists(filePath))) {
43
+ res.status(404).json({ error: 'File not found' });
44
+ return;
45
+ }
46
+
47
+ res.sendFile(filePath);
48
+ } catch (err: unknown) {
49
+ console.error(err);
50
+ res.status(500).json({ error: (err as Error).message });
51
+ }
52
+ });
53
+
54
+ // Upload a file (raw body + x-filename header)
55
+ app.post('/api/files/:page', express.raw({ type: '*/*', limit: '50mb' }), async (req, res) => {
56
+ try {
57
+ const filename = req.headers['x-filename'] as string | undefined;
58
+ if (!filename || filename.trim().length === 0) {
59
+ res.status(400).json({ error: 'x-filename header is required' });
60
+ return;
61
+ }
62
+
63
+ const filePath = safeFilePath(config, req.params.page, filename);
64
+ if (!filePath) {
65
+ res.status(400).json({ error: 'Invalid filename' });
66
+ return;
67
+ }
68
+
69
+ const folder = filesFolder(config, req.params.page);
70
+ await ensureFolderExists(folder);
71
+ await fs.writeFile(filePath, req.body as Buffer);
72
+
73
+ const stat = await fs.stat(filePath);
74
+ res.status(201).json({ name: filename, size: stat.size });
75
+ } catch (err: unknown) {
76
+ console.error(err);
77
+ res.status(500).json({ error: (err as Error).message });
78
+ }
79
+ });
80
+
81
+ // Delete a file
82
+ app.delete('/api/files/:page/:filename', async (req, res) => {
83
+ try {
84
+ const filePath = safeFilePath(config, req.params.page, req.params.filename);
85
+ if (!filePath) {
86
+ res.status(400).json({ error: 'Invalid filename' });
87
+ return;
88
+ }
89
+
90
+ if (!(await checkIfExists(filePath))) {
91
+ res.status(404).json({ error: 'File not found' });
92
+ return;
93
+ }
94
+
95
+ await fs.unlink(filePath);
96
+ res.json({ deleted: true });
97
+ } catch (err: unknown) {
98
+ console.error(err);
99
+ res.status(500).json({ error: (err as Error).message });
100
+ }
101
+ });
102
+ }
103
+
104
+ // ---------------------------------------------------------------------------
105
+ // Helpers
106
+ // ---------------------------------------------------------------------------
107
+
108
+ function filesFolder(config: SynthOSConfig, page: string): string {
109
+ return path.join(config.pagesFolder, 'pages', page, 'files');
110
+ }
111
+
112
+ /**
113
+ * Resolve a filename inside the page's files/ folder with path-traversal protection.
114
+ * Returns the absolute path if safe, or null if the filename is invalid.
115
+ */
116
+ function safeFilePath(config: SynthOSConfig, page: string, filename: string): string | null {
117
+ // Reject obviously bad filenames
118
+ if (!filename || filename.includes('..') || filename.includes('/') || filename.includes('\\')) {
119
+ return null;
120
+ }
121
+ const folder = filesFolder(config, page);
122
+ const resolved = path.resolve(folder, filename);
123
+ if (!resolved.startsWith(path.resolve(folder))) {
124
+ return null;
125
+ }
126
+ return resolved;
127
+ }