synthos 0.7.2 → 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 (380) hide show
  1. package/README.md +215 -65
  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/page.html +323 -0
  27. package/default-pages/oregon_trail/page.json +12 -0
  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_builder.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} +24 -29
  33. package/default-pages/{solar_explorer.json → solar_explorer/page.json} +4 -4
  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_builder.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/page.html +193 -0
  39. package/default-pages/us_map/page.json +12 -0
  40. package/default-pages/us_map_1850/page.html +326 -0
  41. package/default-pages/us_map_1850/page.json +12 -0
  42. package/default-pages/western_cities_1850/page.html +527 -0
  43. package/default-pages/western_cities_1850/page.json +12 -0
  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.css → nebula-dawn.v2.css} +134 -0
  57. package/default-themes/nebula-dawn.v3.css +199 -0
  58. package/default-themes/{nebula-dusk.css → nebula-dusk.v2.css} +128 -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/a2a/a2aProvider.d.ts.map +1 -0
  65. package/dist/agents/a2a/a2aProvider.js +126 -0
  66. package/dist/agents/a2a/a2aProvider.js.map +1 -0
  67. package/dist/agents/discovery.d.ts.map +1 -0
  68. package/dist/agents/discovery.js +52 -0
  69. package/dist/agents/discovery.js.map +1 -0
  70. package/dist/agents/index.d.ts +7 -0
  71. package/dist/agents/index.d.ts.map +1 -0
  72. package/dist/agents/index.js +20 -0
  73. package/dist/agents/index.js.map +1 -0
  74. package/dist/agents/openclaw/gatewayManager.d.ts +117 -0
  75. package/dist/agents/openclaw/gatewayManager.d.ts.map +1 -0
  76. package/dist/agents/openclaw/gatewayManager.js +486 -0
  77. package/dist/agents/openclaw/gatewayManager.js.map +1 -0
  78. package/dist/agents/openclaw/openclawProvider.d.ts.map +1 -0
  79. package/dist/agents/openclaw/openclawProvider.js +237 -0
  80. package/dist/agents/openclaw/openclawProvider.js.map +1 -0
  81. package/dist/agents/openclaw/sshTunnelManager.d.ts +25 -0
  82. package/dist/agents/openclaw/sshTunnelManager.d.ts.map +1 -0
  83. package/dist/agents/openclaw/sshTunnelManager.js +359 -0
  84. package/dist/agents/openclaw/sshTunnelManager.js.map +1 -0
  85. package/dist/agents/types.d.ts.map +1 -0
  86. package/dist/agents/types.js +6 -0
  87. package/dist/agents/types.js.map +1 -0
  88. package/dist/builders/anthropic.d.ts +31 -0
  89. package/dist/builders/anthropic.d.ts.map +1 -0
  90. package/dist/builders/anthropic.js +227 -0
  91. package/dist/builders/anthropic.js.map +1 -0
  92. package/dist/builders/fireworksai.d.ts +9 -0
  93. package/dist/builders/fireworksai.d.ts.map +1 -0
  94. package/dist/builders/fireworksai.js +57 -0
  95. package/dist/builders/fireworksai.js.map +1 -0
  96. package/dist/builders/index.d.ts +13 -0
  97. package/dist/builders/index.d.ts.map +1 -0
  98. package/dist/builders/index.js +31 -0
  99. package/dist/builders/index.js.map +1 -0
  100. package/dist/builders/openai.d.ts +8 -0
  101. package/dist/builders/openai.d.ts.map +1 -0
  102. package/dist/builders/openai.js +87 -0
  103. package/dist/builders/openai.js.map +1 -0
  104. package/dist/builders/types.d.ts +54 -0
  105. package/dist/builders/types.d.ts.map +1 -0
  106. package/dist/builders/types.js +211 -0
  107. package/dist/builders/types.js.map +1 -0
  108. package/dist/connectors/index.d.ts.map +1 -1
  109. package/dist/connectors/index.js +3 -2
  110. package/dist/connectors/index.js.map +1 -1
  111. package/dist/connectors/registry.d.ts +2 -1
  112. package/dist/connectors/registry.d.ts.map +1 -1
  113. package/dist/connectors/registry.js +65 -96
  114. package/dist/connectors/registry.js.map +1 -1
  115. package/dist/connectors/types.d.ts.map +1 -1
  116. package/dist/customizer/Customizer.d.ts +57 -0
  117. package/dist/customizer/Customizer.d.ts.map +1 -0
  118. package/dist/customizer/Customizer.js +124 -0
  119. package/dist/customizer/Customizer.js.map +1 -0
  120. package/dist/customizer/index.d.ts.map +1 -0
  121. package/dist/customizer/index.js +9 -0
  122. package/dist/customizer/index.js.map +1 -0
  123. package/dist/files.d.ts +17 -0
  124. package/dist/files.d.ts.map +1 -1
  125. package/dist/files.js +75 -1
  126. package/dist/files.js.map +1 -1
  127. package/dist/index.d.ts.map +1 -1
  128. package/dist/index.js +1 -0
  129. package/dist/index.js.map +1 -1
  130. package/dist/init.d.ts +10 -6
  131. package/dist/init.d.ts.map +1 -1
  132. package/dist/init.js +97 -86
  133. package/dist/init.js.map +1 -1
  134. package/dist/migrations.d.ts.map +1 -1
  135. package/dist/migrations.js +142 -145
  136. package/dist/migrations.js.map +1 -1
  137. package/dist/models/anthropic.d.ts +24 -0
  138. package/dist/models/anthropic.d.ts.map +1 -0
  139. package/dist/models/anthropic.js +103 -0
  140. package/dist/models/anthropic.js.map +1 -0
  141. package/dist/models/chainOfThought.d.ts.map +1 -0
  142. package/dist/models/chainOfThought.js +45 -0
  143. package/dist/models/chainOfThought.js.map +1 -0
  144. package/dist/models/fireworksai.d.ts.map +1 -0
  145. package/dist/models/fireworksai.js +141 -0
  146. package/dist/models/fireworksai.js.map +1 -0
  147. package/dist/models/index.d.ts +7 -1
  148. package/dist/models/index.d.ts.map +1 -1
  149. package/dist/models/index.js +20 -1
  150. package/dist/models/index.js.map +1 -1
  151. package/dist/models/logCompletePrompt.d.ts.map +1 -0
  152. package/dist/models/logCompletePrompt.js +23 -0
  153. package/dist/models/logCompletePrompt.js.map +1 -0
  154. package/dist/models/openai.d.ts +24 -0
  155. package/dist/models/openai.d.ts.map +1 -0
  156. package/dist/models/openai.js +101 -0
  157. package/dist/models/openai.js.map +1 -0
  158. package/dist/models/providers.d.ts.map +1 -1
  159. package/dist/models/providers.js +12 -4
  160. package/dist/models/providers.js.map +1 -1
  161. package/dist/models/types.d.ts +53 -2
  162. package/dist/models/types.d.ts.map +1 -1
  163. package/dist/models/types.js +21 -0
  164. package/dist/models/types.js.map +1 -1
  165. package/dist/models/utils.d.ts.map +1 -0
  166. package/dist/models/utils.js +21 -0
  167. package/dist/models/utils.js.map +1 -0
  168. package/dist/pages.d.ts +30 -7
  169. package/dist/pages.d.ts.map +1 -1
  170. package/dist/pages.js +177 -55
  171. package/dist/pages.js.map +1 -1
  172. package/dist/scripts.d.ts.map +1 -1
  173. package/dist/scripts.js +4 -3
  174. package/dist/scripts.js.map +1 -1
  175. package/dist/service/createCompletePrompt.d.ts.map +1 -1
  176. package/dist/service/createCompletePrompt.js +9 -6
  177. package/dist/service/createCompletePrompt.js.map +1 -1
  178. package/dist/service/generateImage.d.ts.map +1 -1
  179. package/dist/service/generateImage.js +3 -3
  180. package/dist/service/generateImage.js.map +1 -1
  181. package/dist/service/server.d.ts.map +1 -1
  182. package/dist/service/server.js +39 -7
  183. package/dist/service/server.js.map +1 -1
  184. package/dist/service/transformPage.d.ts +47 -18
  185. package/dist/service/transformPage.d.ts.map +1 -1
  186. package/dist/service/transformPage.js +559 -270
  187. package/dist/service/transformPage.js.map +1 -1
  188. package/dist/service/useAgentRoutes.d.ts +5 -0
  189. package/dist/service/useAgentRoutes.d.ts.map +1 -0
  190. package/dist/service/useAgentRoutes.js +392 -0
  191. package/dist/service/useAgentRoutes.js.map +1 -0
  192. package/dist/service/useApiRoutes.d.ts.map +1 -1
  193. package/dist/service/useApiRoutes.js +380 -138
  194. package/dist/service/useApiRoutes.js.map +1 -1
  195. package/dist/service/useConnectorRoutes.d.ts.map +1 -1
  196. package/dist/service/useConnectorRoutes.js +20 -9
  197. package/dist/service/useConnectorRoutes.js.map +1 -1
  198. package/dist/service/useFileRoutes.d.ts +4 -0
  199. package/dist/service/useFileRoutes.d.ts.map +1 -0
  200. package/dist/service/useFileRoutes.js +122 -0
  201. package/dist/service/useFileRoutes.js.map +1 -0
  202. package/dist/service/usePageRoutes.d.ts.map +1 -1
  203. package/dist/service/usePageRoutes.js +660 -68
  204. package/dist/service/usePageRoutes.js.map +1 -1
  205. package/dist/service/useSharedDataRoutes.d.ts +4 -0
  206. package/dist/service/useSharedDataRoutes.d.ts.map +1 -0
  207. package/dist/service/useSharedDataRoutes.js +104 -0
  208. package/dist/service/useSharedDataRoutes.js.map +1 -0
  209. package/dist/service/useSharedFileRoutes.d.ts +4 -0
  210. package/dist/service/useSharedFileRoutes.d.ts.map +1 -0
  211. package/dist/service/useSharedFileRoutes.js +121 -0
  212. package/dist/service/useSharedFileRoutes.js.map +1 -0
  213. package/dist/settings.d.ts +3 -1
  214. package/dist/settings.d.ts.map +1 -1
  215. package/dist/settings.js +5 -8
  216. package/dist/settings.js.map +1 -1
  217. package/dist/synthos-cli.d.ts.map +1 -1
  218. package/dist/synthos-cli.js +4 -3
  219. package/dist/synthos-cli.js.map +1 -1
  220. package/dist/themes.d.ts +15 -0
  221. package/dist/themes.d.ts.map +1 -1
  222. package/dist/themes.js +106 -20
  223. package/dist/themes.js.map +1 -1
  224. package/migration-rules/v1-to-v2.md +193 -0
  225. package/migration-rules/v2-to-v3.md +481 -0
  226. package/package.json +15 -11
  227. package/required-pages/builder/page.html +43 -0
  228. package/required-pages/builder/page.json +10 -0
  229. package/required-pages/pages/page.html +924 -0
  230. package/required-pages/pages/page.json +10 -0
  231. package/required-pages/settings/page.html +1753 -0
  232. package/required-pages/settings/page.json +10 -0
  233. package/required-pages/synthos_apis/page.html +846 -0
  234. package/required-pages/synthos_apis/page.json +10 -0
  235. package/required-pages/{synthos_scripts.html → synthos_scripts/page.html} +13 -11
  236. package/required-pages/synthos_scripts/page.json +10 -0
  237. package/service-connectors/airtable/connector.json +27 -0
  238. package/service-connectors/alpha-vantage/connector.json +26 -0
  239. package/service-connectors/brave-search/connector.json +26 -0
  240. package/service-connectors/cloudinary/connector.json +27 -0
  241. package/service-connectors/deepl/connector.json +28 -0
  242. package/service-connectors/elevenlabs/connector.json +30 -0
  243. package/service-connectors/giphy/connector.json +27 -0
  244. package/service-connectors/github/connector.json +29 -0
  245. package/service-connectors/huggingface/connector.json +27 -0
  246. package/service-connectors/imgur/connector.json +29 -0
  247. package/service-connectors/instagram/connector.json +43 -0
  248. package/service-connectors/jira/connector.json +28 -0
  249. package/service-connectors/mapbox/connector.json +26 -0
  250. package/service-connectors/nasa/connector.json +27 -0
  251. package/service-connectors/newsapi/connector.json +27 -0
  252. package/service-connectors/notion/connector.json +28 -0
  253. package/service-connectors/open-exchange-rates/connector.json +27 -0
  254. package/service-connectors/openweathermap/connector.json +26 -0
  255. package/service-connectors/pexels/connector.json +27 -0
  256. package/service-connectors/resend/connector.json +29 -0
  257. package/service-connectors/rss2json/connector.json +27 -0
  258. package/service-connectors/sendgrid/connector.json +27 -0
  259. package/service-connectors/spoonacular/connector.json +28 -0
  260. package/service-connectors/stability-ai/connector.json +27 -0
  261. package/service-connectors/twilio/connector.json +28 -0
  262. package/service-connectors/unsplash/connector.json +27 -0
  263. package/service-connectors/wolfram-alpha/connector.json +26 -0
  264. package/service-connectors/youtube-data/connector.json +30 -0
  265. package/src/agents/a2a/a2aProvider.ts +110 -0
  266. package/src/agents/discovery.ts +74 -0
  267. package/src/agents/index.ts +6 -0
  268. package/src/agents/openclaw/gatewayManager.ts +570 -0
  269. package/src/agents/openclaw/openclawProvider.ts +259 -0
  270. package/src/agents/openclaw/sshTunnelManager.ts +393 -0
  271. package/src/agents/types.ts +82 -0
  272. package/src/builders/anthropic.ts +283 -0
  273. package/src/builders/fireworksai.ts +59 -0
  274. package/src/builders/index.ts +33 -0
  275. package/src/builders/openai.ts +89 -0
  276. package/src/builders/types.ts +261 -0
  277. package/src/connectors/index.ts +3 -1
  278. package/src/connectors/registry.ts +40 -96
  279. package/src/connectors/types.ts +25 -0
  280. package/src/customizer/Customizer.ts +151 -0
  281. package/src/customizer/index.ts +5 -0
  282. package/src/files.ts +71 -0
  283. package/src/index.ts +2 -1
  284. package/src/init.ts +138 -97
  285. package/src/migrations.ts +148 -145
  286. package/src/models/anthropic.ts +119 -0
  287. package/src/models/chainOfThought.ts +56 -0
  288. package/src/models/fireworksai.ts +143 -0
  289. package/src/models/index.ts +7 -1
  290. package/src/models/logCompletePrompt.ts +25 -0
  291. package/src/models/openai.ts +110 -0
  292. package/src/models/providers.ts +12 -3
  293. package/src/models/types.ts +97 -2
  294. package/src/models/utils.ts +16 -0
  295. package/src/pages.ts +176 -54
  296. package/src/scripts.ts +2 -2
  297. package/src/service/createCompletePrompt.ts +3 -1
  298. package/src/service/generateImage.ts +2 -2
  299. package/src/service/server.ts +39 -8
  300. package/src/service/transformPage.ts +605 -301
  301. package/src/service/useAgentRoutes.ts +428 -0
  302. package/src/service/useApiRoutes.ts +309 -45
  303. package/src/service/useConnectorRoutes.ts +21 -10
  304. package/src/service/useFileRoutes.ts +127 -0
  305. package/src/service/usePageRoutes.ts +736 -75
  306. package/src/service/useSharedDataRoutes.ts +106 -0
  307. package/src/service/useSharedFileRoutes.ts +126 -0
  308. package/src/settings.ts +8 -10
  309. package/src/synthos-cli.ts +4 -3
  310. package/src/themes.ts +103 -20
  311. package/static-files/favicon.svg +12 -0
  312. package/static-files/fluentlm-instructions.llmd +868 -0
  313. package/static-files/fluentlm-instructions.md +1595 -0
  314. package/static-files/fluentlm.css +4844 -0
  315. package/static-files/fluentlm.js +3602 -0
  316. package/static-files/fluentlm.min.css +1 -0
  317. package/static-files/fluentlm.min.js +1 -0
  318. package/static-files/helpers.v3.js +304 -0
  319. package/static-files/page.v3.js +1290 -0
  320. package/static-files/recommended-frameworks.llmd +81 -0
  321. package/static-files/recommended-frameworks.md +137 -0
  322. package/static-files/retro-game.js +877 -0
  323. package/static-files/shell.css +797 -0
  324. package/static-files/theme-dark.css +169 -0
  325. package/static-files/theme-light.css +169 -0
  326. package/tests/anthropic.spec.ts +84 -0
  327. package/tests/builders.spec.ts +139 -0
  328. package/tests/chainOfThought.spec.ts +108 -0
  329. package/tests/ensureScripts.spec.ts +82 -0
  330. package/tests/files.spec.ts +233 -0
  331. package/tests/fireworksai.spec.ts +92 -0
  332. package/tests/logCompletePrompt.spec.ts +74 -0
  333. package/tests/migrations.spec.ts +79 -1
  334. package/tests/openai.spec.ts +71 -0
  335. package/tests/pages.spec.ts +226 -1
  336. package/tests/providers.spec.ts +144 -0
  337. package/tests/scripts.spec.ts +209 -0
  338. package/tests/transformPage.spec.ts +456 -0
  339. package/tests/types.spec.ts +23 -0
  340. package/default-pages/app_builder.html +0 -40
  341. package/default-pages/app_builder.json +0 -1
  342. package/default-pages/json_tools.json +0 -1
  343. package/default-pages/my_notes.html +0 -33
  344. package/default-pages/neon_asteroids.html +0 -77
  345. package/default-pages/sidebar_builder.json +0 -1
  346. package/default-pages/solar_tutorial.json +0 -1
  347. package/default-pages/two-panel_builder.json +0 -1
  348. package/dist/connectors/index.d.ts +0 -3
  349. package/dist/connectors/types.d.ts +0 -61
  350. package/dist/index.d.ts +0 -7
  351. package/dist/migrations.d.ts +0 -11
  352. package/dist/models/providers.d.ts +0 -7
  353. package/dist/scripts.d.ts +0 -14
  354. package/dist/service/createCompletePrompt.d.ts +0 -5
  355. package/dist/service/debugLog.d.ts +0 -11
  356. package/dist/service/generateImage.d.ts +0 -32
  357. package/dist/service/index.d.ts +0 -8
  358. package/dist/service/modelInstructions.d.ts +0 -7
  359. package/dist/service/requiresSettings.d.ts +0 -3
  360. package/dist/service/server.d.ts +0 -4
  361. package/dist/service/useApiRoutes.d.ts +0 -4
  362. package/dist/service/useConnectorRoutes.d.ts +0 -4
  363. package/dist/service/useDataRoutes.d.ts +0 -4
  364. package/dist/service/usePageRoutes.d.ts +0 -5
  365. package/dist/synthos-cli.d.ts +0 -2
  366. package/images/home.png +0 -0
  367. package/images/page-management.png +0 -0
  368. package/images/settings.png +0 -0
  369. package/images/synthos-square.png +0 -0
  370. package/page-scripts/helpers-v2.js +0 -121
  371. package/page-scripts/page-v2.js +0 -615
  372. package/required-pages/builder.html +0 -74
  373. package/required-pages/builder.json +0 -1
  374. package/required-pages/pages.html +0 -196
  375. package/required-pages/pages.json +0 -1
  376. package/required-pages/settings.html +0 -841
  377. package/required-pages/settings.json +0 -1
  378. package/required-pages/synthos_apis.html +0 -272
  379. package/required-pages/synthos_apis.json +0 -1
  380. package/required-pages/synthos_scripts.json +0 -1
@@ -1,17 +1,21 @@
1
1
  import path from "path";
2
- import { listPages, loadPageMetadata, PageMetadata, savePageMetadata, REQUIRED_PAGES, deletePage, copyPage, loadPageState, savePageState, PAGE_VERSION } from "../pages";
3
- import { checkIfExists, copyFile, deleteFile, loadFile } from "../files";
2
+ import fs from "fs/promises";
3
+ import AdmZip from "adm-zip";
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";
4
6
  import {getModelEntry, loadSettings, saveSettings, ServicesConfig } from "../settings";
5
7
  import { Application } from 'express';
8
+ import express from 'express';
6
9
  import { SynthOSConfig } from "../init";
7
10
  import { createCompletePrompt, PROVIDERS } from "./createCompletePrompt";
8
11
  import { generateDefaultImage, generateImage } from "./generateImage";
9
- import { chainOfThought } from "agentm-core";
12
+ import { chainOfThought } from "../models";
10
13
  import { requiresSettings } from "./requiresSettings";
11
14
  import { executeScript } from "../scripts";
12
- import { listThemes, loadTheme, loadThemeInfo } from "../themes";
15
+ import { listThemes, loadTheme, loadThemeInfo, loadThemeVersion } from "../themes";
13
16
  import { migratePage } from "../migrations";
14
17
  import { loadPageWithFallback } from "./usePageRoutes";
18
+ import { Customizer } from "../customizer";
15
19
 
16
20
  // ---------------------------------------------------------------------------
17
21
  // Service registry
@@ -45,18 +49,108 @@ const SERVICE_REGISTRY: ServiceDefinition[] = [
45
49
  }
46
50
  ];
47
51
 
48
- export function useApiRoutes(config: SynthOSConfig, app: Application): void {
52
+ export function useApiRoutes(config: SynthOSConfig, app: Application, customizer?: Customizer): void {
49
53
  // List pages
50
54
  app.get('/api/pages', async (req, res) => {
51
- const pages = await listPages(config.pagesFolder, config.requiredPagesFolder);
55
+ const pages = await listPages(config.pagesFolder, config.requiredPagesFolders);
52
56
  res.json(pages);
53
57
  });
54
58
 
59
+ // Import a page from a zip file
60
+ app.post('/api/pages/import', express.raw({ type: 'application/zip', limit: '50mb' }), async (req, res) => {
61
+ try {
62
+ const zipBuffer = req.body as Buffer;
63
+ if (!zipBuffer || zipBuffer.length === 0) {
64
+ res.status(400).json({ error: 'Empty request body' });
65
+ return;
66
+ }
67
+
68
+ let zip: AdmZip;
69
+ try {
70
+ zip = new AdmZip(zipBuffer);
71
+ } catch {
72
+ res.status(400).json({ error: 'Invalid zip file' });
73
+ return;
74
+ }
75
+
76
+ const entries = zip.getEntries();
77
+ if (entries.length === 0) {
78
+ res.status(400).json({ error: 'Zip file is empty' });
79
+ return;
80
+ }
81
+
82
+ // Determine top-level folder name and validate structure
83
+ const firstEntry = entries[0].entryName;
84
+ const topFolder = firstEntry.split('/')[0];
85
+ const hasPageHtml = entries.some(e => e.entryName === `${topFolder}/page.html`);
86
+ if (!hasPageHtml) {
87
+ res.status(400).json({ error: 'Zip must contain a <folder>/page.html entry' });
88
+ return;
89
+ }
90
+
91
+ // Sanitize page name from folder name
92
+ let pageName = topFolder.toLowerCase().replace(/[^a-z0-9_-]/g, '');
93
+ if (!pageName) {
94
+ res.status(400).json({ error: 'Could not derive a valid page name from zip contents' });
95
+ return;
96
+ }
97
+
98
+ // Auto-append _1, _2, etc. on name conflicts
99
+ let finalName = pageName;
100
+ let suffix = 0;
101
+ while (await checkIfExists(path.join(config.pagesFolder, 'pages', finalName))) {
102
+ suffix++;
103
+ finalName = `${pageName}_${suffix}`;
104
+ }
105
+
106
+ const targetDir = path.join(config.pagesFolder, 'pages', finalName);
107
+ await ensureFolderExists(targetDir);
108
+
109
+ // Extract entries with path traversal protection
110
+ for (const entry of entries) {
111
+ if (entry.isDirectory) continue;
112
+
113
+ // Strip the top-level folder prefix to get relative path
114
+ const relativePath = entry.entryName.substring(topFolder.length + 1);
115
+ if (!relativePath) continue;
116
+
117
+ const resolvedPath = path.resolve(targetDir, relativePath);
118
+ if (!resolvedPath.startsWith(path.resolve(targetDir))) {
119
+ // Path traversal — skip this entry
120
+ continue;
121
+ }
122
+
123
+ await ensureFolderExists(path.dirname(resolvedPath));
124
+ await fs.writeFile(resolvedPath, entry.getData());
125
+ }
126
+
127
+ // Update metadata: set createdDate and lastModified to now
128
+ const now = new Date().toISOString();
129
+ const existingMeta = await loadPageMetadata(config.pagesFolder, finalName);
130
+ const metadata: PageMetadata = {
131
+ title: existingMeta?.title ?? '',
132
+ categories: existingMeta?.categories ?? [],
133
+ pinned: existingMeta?.pinned ?? false,
134
+ showInAll: existingMeta?.showInAll ?? true,
135
+ createdDate: now,
136
+ lastModified: now,
137
+ pageVersion: existingMeta?.pageVersion ?? PAGE_VERSION,
138
+ mode: existingMeta?.mode ?? 'unlocked',
139
+ };
140
+ await savePageMetadata(config.pagesFolder, finalName, metadata);
141
+
142
+ res.status(201).json({ name: finalName, title: metadata.title });
143
+ } catch (err: unknown) {
144
+ console.error(err);
145
+ res.status(500).json({ error: (err as Error).message });
146
+ }
147
+ });
148
+
55
149
  // Get page metadata
56
150
  app.get('/api/pages/:name', async (req, res) => {
57
151
  try {
58
152
  const { name } = req.params;
59
- const metadata = await loadPageMetadata(config.pagesFolder, name, config.requiredPagesFolder);
153
+ const metadata = await loadPageMetadata(config.pagesFolder, name, config.requiredPagesFolders);
60
154
  if (metadata) {
61
155
  res.json(metadata);
62
156
  } else {
@@ -107,7 +201,7 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
107
201
  }
108
202
 
109
203
  // Load existing metadata (or defaults)
110
- const existing = await loadPageMetadata(config.pagesFolder, name, config.requiredPagesFolder);
204
+ const existing = await loadPageMetadata(config.pagesFolder, name, config.requiredPagesFolders);
111
205
  const metadata: PageMetadata = {
112
206
  title: existing?.title ?? '',
113
207
  categories: existing?.categories ?? [],
@@ -133,7 +227,11 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
133
227
  if (metadata.mode !== 'locked') {
134
228
  const userPagePath = path.join(config.pagesFolder, 'pages', name, 'page.html');
135
229
  if (!(await checkIfExists(userPagePath))) {
136
- 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
+ }
137
235
  if (html) {
138
236
  await savePageState(config.pagesFolder, name, html);
139
237
  }
@@ -159,7 +257,7 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
159
257
  }
160
258
 
161
259
  // Load existing metadata (user override → fallback .json → defaults)
162
- let metadata = await loadPageMetadata(config.pagesFolder, name, config.requiredPagesFolder);
260
+ let metadata = await loadPageMetadata(config.pagesFolder, name, config.requiredPagesFolders);
163
261
  if (!metadata) {
164
262
  metadata = {
165
263
  title: '',
@@ -188,7 +286,7 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
188
286
  const { name } = req.params;
189
287
 
190
288
  // Cannot delete required pages
191
- if (REQUIRED_PAGES.includes(name)) {
289
+ if (config.requiredPages.includes(name)) {
192
290
  res.status(400).json({ error: `Cannot delete required page "${name}"` });
193
291
  return;
194
292
  }
@@ -210,11 +308,56 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
210
308
  }
211
309
  });
212
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
+
213
356
  // Copy a page to a new name
214
357
  app.post('/api/pages/:name/copy', async (req, res) => {
215
358
  try {
216
359
  const sourceName = req.params.name;
217
- const { name: targetName, title, categories } = req.body;
360
+ const { name: targetName, title, categories, copyTables, copyFiles } = req.body;
218
361
 
219
362
  // Validate target name
220
363
  if (!targetName || typeof targetName !== 'string') {
@@ -236,10 +379,14 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
236
379
  // Check source exists (user pages → required pages)
237
380
  const sourceFolderPath = path.join(config.pagesFolder, 'pages', sourceName, 'page.html');
238
381
  const sourceFlatPath = path.join(config.pagesFolder, `${sourceName}.html`);
239
- 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
+ }
240
387
  const sourceExists = await checkIfExists(sourceFolderPath)
241
388
  || await checkIfExists(sourceFlatPath)
242
- || await checkIfExists(sourceRequiredPath);
389
+ || !!sourceRequiredPath;
243
390
  if (!sourceExists) {
244
391
  res.status(404).json({ error: `Source page "${sourceName}" not found` });
245
392
  return;
@@ -259,7 +406,11 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
259
406
  targetName,
260
407
  typeof title === 'string' ? title : '',
261
408
  Array.isArray(categories) ? categories : [],
262
- config.requiredPagesFolder
409
+ config.requiredPagesFolders,
410
+ {
411
+ copyTables: copyTables === true,
412
+ copyFiles: copyFiles !== false, // default true
413
+ }
263
414
  );
264
415
 
265
416
  // Return the new page metadata
@@ -286,9 +437,6 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
286
437
  if (Array.isArray(settings.models)) {
287
438
  for (const entry of settings.models) {
288
439
  if (entry.configuration) {
289
- if (typeof entry.configuration.maxTokens === 'string') {
290
- entry.configuration.maxTokens = parseInt(entry.configuration.maxTokens);
291
- }
292
440
  }
293
441
  if (typeof entry.logCompletions === 'string') {
294
442
  entry.logCompletions = entry.logCompletions === 'true';
@@ -326,9 +474,8 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
326
474
  app.post('/api/generate/completion', async (req, res) => {
327
475
  await requiresSettings(res, config.pagesFolder, async (settings) => {
328
476
  const { prompt, temperature } = req.body;
329
- const maxTokens = getModelEntry(settings, 'chat').configuration.maxTokens;
330
477
  const completePrompt = await createCompletePrompt(config.pagesFolder, 'chat', req.body.model);
331
- const response = await chainOfThought({ question: prompt, temperature, maxTokens, completePrompt });
478
+ const response = await chainOfThought({ question: prompt, temperature, completePrompt });
332
479
  if (response.completed) {
333
480
  res.json(response.value ?? {});
334
481
  } else {
@@ -339,31 +486,42 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
339
486
  });
340
487
 
341
488
  // Brainstorm endpoint
489
+ if (!customizer || customizer.isEnabled('brainstorm'))
342
490
  app.post('/api/brainstorm', async (req, res) => {
343
491
  await requiresSettings(res, config.pagesFolder, async (settings) => {
344
492
  const { context, messages } = req.body;
345
- const maxTokens = getModelEntry(settings, 'chat').configuration.maxTokens;
346
493
  const completePrompt = await createCompletePrompt(config.pagesFolder, 'chat');
347
494
 
495
+ const productName = customizer?.productName ?? 'SynthOS';
348
496
  const system: { role: 'system'; content: string } = {
349
497
  role: 'system',
350
- content: `You are a creative brainstorming assistant for SynthOS, a tool that builds web pages through conversation. The user is brainstorming — exploring ideas before building. Be concise, creative, and collaborative. Suggest concrete approaches when you can.
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.
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.
502
+ The goal is to help them generate a prompt for the builder that captures their vision, along with suggestions for next steps.
503
+ Suggest concrete approaches when you can, not complex visions for some ellaborate app.
504
+ Just help expand their thoughts into a great next prompt.
505
+
506
+ <CONTEXT>
507
+ ${context}
508
+
509
+ <INSTRUCTIONS>
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.
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.
512
+
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.
351
514
 
352
515
  You MUST return your response as a JSON object with exactly these fields:
353
516
  {
354
517
  "response": "Your conversational reply — explanations, options, suggestions. Markdown OK.",
355
- "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.",
356
519
  "suggestions": ["Short clickable option A", "Short clickable option B", "Short clickable option C"]
357
520
  }
358
521
 
359
522
  suggestions — 2-4 short phrases the user can click to continue the conversation. These are next-step options: directions to explore, questions to answer, or choices to make. Keep each under 60 characters. Always provide suggestions.
360
523
 
361
- Return ONLY the JSON object. No markdown fences.
362
-
363
- <CONTEXT>
364
- ${context}
365
- </CONTEXT>`
366
- };
524
+ Return ONLY the JSON object.`};
367
525
 
368
526
  // Format multi-turn conversation into a single prompt
369
527
  const formatted = (messages as { role: string; content: string }[]).map(m =>
@@ -372,7 +530,7 @@ ${context}
372
530
 
373
531
  const prompt: { role: 'user'; content: string } = { role: 'user', content: formatted };
374
532
 
375
- const result = await completePrompt({ prompt, system, maxTokens, jsonMode: true });
533
+ const result = await completePrompt({ prompt, system, jsonMode: true });
376
534
  if (result.completed) {
377
535
  let response = result.value || '';
378
536
  let brainstormPrompt = '';
@@ -397,6 +555,7 @@ ${context}
397
555
  });
398
556
 
399
557
  // Define a route for running configured scripts
558
+ if (!customizer || customizer.isEnabled('scripts'))
400
559
  app.post('/api/scripts/:id', async (req, res) => {
401
560
  await requiresSettings(res, config.pagesFolder, async (settings) => {
402
561
  const { id } = req.params;
@@ -425,7 +584,13 @@ ${context}
425
584
  res.status(404).send(`// Theme info for "${themeName}" not found`);
426
585
  return;
427
586
  }
428
- 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')});`;
429
594
  res.set('Content-Type', 'application/javascript');
430
595
  res.send(js);
431
596
  } catch (err: unknown) {
@@ -442,21 +607,18 @@ ${context}
442
607
  res.status(400).send('// Missing page query parameter');
443
608
  return;
444
609
  }
445
- const metadata = await loadPageMetadata(config.pagesFolder, page, config.requiredPagesFolder);
610
+ const metadata = await loadPageMetadata(config.pagesFolder, page, config.requiredPagesFolders);
446
611
  const mode = metadata?.mode ?? 'unlocked';
447
612
  const title = metadata?.title ?? '';
448
613
  const categories = metadata?.categories ?? [];
449
- 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 });
450
617
  const js = [
451
618
  `window.pageInfo=${info};`,
452
619
  `if(window.pageInfo.mode==="locked"){`,
453
620
  `document.addEventListener("DOMContentLoaded",function(){`,
454
621
  `var f=document.getElementById("chatForm");if(f)f.style.display="none";`,
455
- `var s=document.getElementById("saveLink");if(s)s.textContent="Copy";`,
456
- `var r=document.getElementById("resetLink");if(r){`,
457
- `var c=r.cloneNode(true);c.textContent="Reload";`,
458
- `c.addEventListener("click",function(e){e.preventDefault();window.location.href=window.location.pathname;});`,
459
- `r.parentNode.replaceChild(c,r);}`,
460
622
  `});`,
461
623
  `}`,
462
624
  ].join('');
@@ -506,8 +668,8 @@ ${context}
506
668
  res.status(400).send('// Invalid version parameter');
507
669
  return;
508
670
  }
509
- const scriptPath = path.join(config.pageScriptsFolder, `page-v${v}.js`);
510
- if (!(await checkIfExists(scriptPath))) {
671
+ const scriptPath = await findFileInFolders(config.staticFilesFolders, `page.v${v}.js`);
672
+ if (!scriptPath) {
511
673
  res.status(404).send(`// page-v${v}.js not found`);
512
674
  return;
513
675
  }
@@ -529,8 +691,8 @@ ${context}
529
691
  res.status(400).send('// Invalid version parameter');
530
692
  return;
531
693
  }
532
- const scriptPath = path.join(config.pageScriptsFolder, `helpers-v${v}.js`);
533
- if (!(await checkIfExists(scriptPath))) {
694
+ const scriptPath = await findFileInFolders(config.staticFilesFolders, `helpers.v${v}.js`);
695
+ if (!scriptPath) {
534
696
  res.status(404).send(`// helpers-v${v}.js not found`);
535
697
  return;
536
698
  }
@@ -609,6 +771,7 @@ ${context}
609
771
  // Web Search (Brave Search API)
610
772
  // -----------------------------------------------------------------------
611
773
 
774
+ if (!customizer || customizer.isEnabled('search'))
612
775
  app.post('/api/search/web', async (req, res) => {
613
776
  try {
614
777
  const { query, count, country, freshness } = req.body;
@@ -663,7 +826,7 @@ ${context}
663
826
  const { name } = req.params;
664
827
 
665
828
  // Load current metadata
666
- const metadata = await loadPageMetadata(config.pagesFolder, name, config.requiredPagesFolder);
829
+ const metadata = await loadPageMetadata(config.pagesFolder, name, config.requiredPagesFolders);
667
830
  if (!metadata) {
668
831
  res.status(404).json({ error: `Page "${name}" not found` });
669
832
  return;
@@ -689,14 +852,25 @@ ${context}
689
852
  // Save upgraded HTML to v2 folder structure
690
853
  await savePageState(config.pagesFolder, name, migratedHtml);
691
854
 
692
- // Move legacy flat file to .migrated folder instead of deleting
855
+ // Backup original page to .migrated/ before overwriting
856
+ const migratedFolder = path.join(config.pagesFolder, '.migrated');
857
+
858
+ // Handle legacy flat file (<localFolder>/pagename.html)
693
859
  const flatPath = path.join(config.pagesFolder, `${name}.html`);
694
860
  if (await checkIfExists(flatPath)) {
695
- const migratedFolder = path.join(config.pagesFolder, '.migrated');
696
861
  await copyFile(flatPath, migratedFolder);
697
862
  await deleteFile(flatPath);
698
863
  }
699
864
 
865
+ // Handle folder-based page (<localFolder>/pages/name/)
866
+ const folderPath = path.join(config.pagesFolder, 'pages', name);
867
+ if (await checkIfExists(folderPath)) {
868
+ await copyFolderRecursive(folderPath, path.join(migratedFolder, name));
869
+ }
870
+
871
+ // Clear stale version files (undo snapshots from the old page version)
872
+ await clearVersions(config.pagesFolder, name);
873
+
700
874
  // Update metadata
701
875
  metadata.pageVersion = PAGE_VERSION;
702
876
  metadata.lastModified = new Date().toISOString();
@@ -708,4 +882,94 @@ ${context}
708
882
  res.status(500).json({ error: (err as Error).message });
709
883
  }
710
884
  });
885
+
886
+ // Export a page as a zip file
887
+ app.get('/api/pages/:name/export', async (req, res) => {
888
+ try {
889
+ const { name } = req.params;
890
+
891
+ // Try user pages folder first, then required pages
892
+ const userPageDir = path.join(config.pagesFolder, 'pages', name);
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
+ }
900
+ let sourceDir: string | null = null;
901
+
902
+ if (await checkIfExists(path.join(userPageDir, 'page.html'))) {
903
+ sourceDir = userPageDir;
904
+ } else if (requiredPageDir) {
905
+ // For required pages, create a temp-like zip with just the HTML
906
+ const zip = new AdmZip();
907
+ const html = await loadFile(path.join(requiredPageDir, 'page.html'));
908
+ zip.addFile(`${name}/page.html`, Buffer.from(html, 'utf-8'));
909
+
910
+ // Include page.json if it exists
911
+ const metaPath = path.join(requiredPageDir, 'page.json');
912
+ if (await checkIfExists(metaPath)) {
913
+ const meta = await loadFile(metaPath);
914
+ zip.addFile(`${name}/page.json`, Buffer.from(meta, 'utf-8'));
915
+ }
916
+
917
+ const zipBuffer = zip.toBuffer();
918
+ res.set('Content-Type', 'application/zip');
919
+ res.set('Content-Disposition', `attachment; filename="${name}.zip"`);
920
+ res.send(zipBuffer);
921
+ return;
922
+ } else {
923
+ res.status(404).json({ error: `Page "${name}" not found` });
924
+ return;
925
+ }
926
+
927
+ // Zip the entire page folder
928
+ const zip = new AdmZip();
929
+ zip.addLocalFolder(sourceDir, name);
930
+ const zipBuffer = zip.toBuffer();
931
+
932
+ res.set('Content-Type', 'application/zip');
933
+ res.set('Content-Disposition', `attachment; filename="${name}.zip"`);
934
+ res.send(zipBuffer);
935
+ } catch (err: unknown) {
936
+ console.error(err);
937
+ res.status(500).json({ error: (err as Error).message });
938
+ }
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
+ });
711
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;
@@ -35,9 +35,11 @@ export function useConnectorRoutes(config: SynthOSConfig, app: Application): voi
35
35
  id: def.id,
36
36
  name: def.name,
37
37
  category: def.category,
38
+ description: def.description,
38
39
  configured: isOAuth
39
40
  ? !!oauthCfg && oauthCfg.enabled && !!oauthCfg.accessToken
40
- : !!cfg && cfg.enabled && !!cfg.apiKey
41
+ : !!cfg && cfg.enabled && !!cfg.apiKey,
42
+ enabled: !!cfg?.enabled
41
43
  };
42
44
  });
43
45
 
@@ -52,7 +54,7 @@ export function useConnectorRoutes(config: SynthOSConfig, app: Application): voi
52
54
  app.get('/api/connectors/:id', async (req, res) => {
53
55
  try {
54
56
  const { id } = req.params;
55
- const def = CONNECTOR_REGISTRY.find(d => d.id === id);
57
+ const def = getConnectorRegistry(config.serviceConnectorsFolders).find(d => d.id === id);
56
58
  if (!def) {
57
59
  res.status(404).json({ error: `Connector "${id}" not found` });
58
60
  return;
@@ -86,7 +88,7 @@ export function useConnectorRoutes(config: SynthOSConfig, app: Application): voi
86
88
  app.post('/api/connectors/:id', async (req, res) => {
87
89
  try {
88
90
  const { id } = req.params;
89
- const def = CONNECTOR_REGISTRY.find(d => d.id === id);
91
+ const def = getConnectorRegistry(config.serviceConnectorsFolders).find(d => d.id === id);
90
92
  if (!def) {
91
93
  res.status(404).json({ error: `Connector "${id}" not found` });
92
94
  return;
@@ -162,7 +164,7 @@ export function useConnectorRoutes(config: SynthOSConfig, app: Application): voi
162
164
  app.get('/api/connectors/:id/authorize', async (req, res) => {
163
165
  try {
164
166
  const { id } = req.params;
165
- const def = CONNECTOR_REGISTRY.find(d => d.id === id);
167
+ const def = getConnectorRegistry(config.serviceConnectorsFolders).find(d => d.id === id);
166
168
  if (!def || def.authStrategy !== 'oauth2') {
167
169
  res.status(400).json({ error: `Connector "${id}" is not an OAuth2 connector` });
168
170
  return;
@@ -212,7 +214,7 @@ export function useConnectorRoutes(config: SynthOSConfig, app: Application): voi
212
214
  const state = JSON.parse(stateRaw) as { connector: string };
213
215
  const connectorId = state.connector;
214
216
 
215
- const def = CONNECTOR_REGISTRY.find(d => d.id === connectorId);
217
+ const def = getConnectorRegistry(config.serviceConnectorsFolders).find(d => d.id === connectorId);
216
218
  if (!def || def.authStrategy !== 'oauth2') {
217
219
  res.status(400).json({ error: `Unknown OAuth2 connector: ${connectorId}` });
218
220
  return;
@@ -324,7 +326,7 @@ export function useConnectorRoutes(config: SynthOSConfig, app: Application): voi
324
326
  return;
325
327
  }
326
328
 
327
- const def = CONNECTOR_REGISTRY.find(d => d.id === request.connector);
329
+ const def = getConnectorRegistry(config.serviceConnectorsFolders).find(d => d.id === request.connector);
328
330
  if (!def) {
329
331
  res.status(404).json({ error: `Connector "${request.connector}" not found` });
330
332
  return;
@@ -352,11 +354,20 @@ export function useConnectorRoutes(config: SynthOSConfig, app: Application): voi
352
354
  }
353
355
 
354
356
  // Build URL — join baseUrl path with request path to avoid
355
- // absolute paths (e.g. "/me/accounts") replacing the base path
357
+ // absolute paths (e.g. "/me/accounts") replacing the base path.
358
+ // Split path from inline query string first — assigning a '?' to
359
+ // URL.pathname encodes it as %3F, which breaks upstream APIs.
360
+ const [reqPath, reqQS] = request.path.split('?');
356
361
  const base = new URL(def.baseUrl);
357
- const joinedPath = base.pathname.replace(/\/+$/, '') + '/' + request.path.replace(/^\/+/, '');
362
+ const joinedPath = base.pathname.replace(/\/+$/, '') + '/' + reqPath.replace(/^\/+/, '');
358
363
  base.pathname = joinedPath;
359
364
  const url = base;
365
+ if (reqQS) {
366
+ const inline = new URLSearchParams(reqQS);
367
+ for (const [key, value] of inline.entries()) {
368
+ url.searchParams.set(key, value);
369
+ }
370
+ }
360
371
  if (request.query) {
361
372
  for (const [key, value] of Object.entries(request.query)) {
362
373
  url.searchParams.set(key, value);