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
@@ -1,13 +1,19 @@
1
- import { loadPageMetadata, loadPageState, normalizePageName, PAGE_VERSION, REQUIRED_PAGES, savePageMetadata, savePageState, updatePageState } from "../pages";
1
+ import { loadPageMetadata, loadPageState, normalizePageName, PAGE_VERSION, savePageMetadata, savePageState, savePageVersion, loadPageVersion, getLatestVersion, clearVersions } from "../pages";
2
2
  import { getModelEntry, hasConfiguredSettings, loadSettings } from "../settings";
3
3
  import { Application } from 'express';
4
- import { transformPage } from "./transformPage";
5
- import { getModelInstructions } from "./modelInstructions";
4
+ import { transformPage, buildRouteHints, serverAPIs, AGENT_API_REFERENCE } from "./transformPage";
6
5
  import { SynthOSConfig } from "../init";
7
6
  import { createCompletePrompt } from "./createCompletePrompt";
8
7
  import { completePrompt } from "../models";
9
8
  import { green, red, dim, estimateTokens } from "./debugLog";
10
- import { loadThemeInfo } from "../themes";
9
+ import { loadThemeInfo, loadThemeVersion, ThemeInfo } from "../themes";
10
+ import { Customizer } from "../customizer";
11
+ import { createBuilder, ContextSection, Attachment } from "../builders";
12
+ import { getConnectorRegistry, ConnectorsConfig, ConnectorOAuthConfig } from "../connectors";
13
+ import { AgentConfig } from "../agents";
14
+ import { listScripts } from "../scripts";
15
+ import path from 'path';
16
+ import { checkIfExists, copyFolderRecursive, deleteFile as deleteFileFromDisk, findFileInFolders, loadFile } from "../files";
11
17
  import * as cheerio from 'cheerio';
12
18
 
13
19
  /**
@@ -16,6 +22,7 @@ import * as cheerio from 'cheerio';
16
22
  */
17
23
  const REQUIRED_IMPORTS: { selector: string; src: string }[] = [
18
24
  { selector: 'script[src*="marked"]', src: 'https://cdnjs.cloudflare.com/ajax/libs/marked/14.1.1/marked.min.js' },
25
+ { selector: 'script[src*="html2canvas"]', src: 'https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js' },
19
26
  ];
20
27
 
21
28
  /**
@@ -91,10 +98,359 @@ function injectPageScript(html: string, pageVersion: number): string {
91
98
  return html + '\n' + tag;
92
99
  }
93
100
 
94
- export function usePageRoutes(config: SynthOSConfig, app: Application): void {
101
+ /**
102
+ * Wrap each inline <script> body in an IIFE so that top-level const/let
103
+ * declarations become function-scoped. This prevents "Identifier … has
104
+ * already been declared" errors when the page is reloaded via
105
+ * document.write() (which reuses the Window's global lexical environment).
106
+ *
107
+ * Skips: external scripts (src=), JSON data blocks, already-wrapped IIFEs,
108
+ * and empty bodies.
109
+ */
110
+ function wrapInlineScriptsInIIFE(html: string): string {
111
+ return html.replace(/<script([^>]*)>([\s\S]*?)<\/script>/gi, (match, attrs: string, body: string) => {
112
+ if (/\bsrc\s*=/i.test(attrs)) return match;
113
+ if (/\btype\s*=\s*["']application\/json["']/i.test(attrs)) return match;
114
+ const trimmed = body.trim();
115
+ if (!trimmed) return match;
116
+ // Already wrapped — don't double-wrap
117
+ if (trimmed.startsWith('(function(){') || trimmed.startsWith('(function ()')) return match;
118
+ return `<script${attrs}>(function(){${body}})();</script>`;
119
+ });
120
+ }
121
+
122
+ /**
123
+ * Move any external <script src="..."> tags found in <body> up to the end
124
+ * of <head>, preserving their relative order. Library scripts (d3, Chart.js,
125
+ * marked, etc.) only define globals and don't touch the DOM, so executing
126
+ * them in <head> is safe and guarantees they are available before any inline
127
+ * <body> scripts run. This also eliminates browser "parser-blocking cross
128
+ * site script invoked via document.write" warnings.
129
+ */
130
+ function hoistExternalScriptsToHead(html: string): string {
131
+ const $ = cheerio.load(html, { decodeEntities: false });
132
+ const hoisted: string[] = [];
133
+ $('body script[src]').each((_, el) => {
134
+ const script = $(el);
135
+ // Don't move data-locked system scripts (page-script, page-helpers, etc.)
136
+ if (script.attr('data-locked') !== undefined) return;
137
+ hoisted.push($.html(script));
138
+ script.remove();
139
+ });
140
+ if (hoisted.length > 0) {
141
+ $('head').append(hoisted.join('\n') + '\n');
142
+ }
143
+ return $.html();
144
+ }
145
+
146
+ /**
147
+ * Inline error-capture script injected as the first child of <head> so it
148
+ * registers window.onerror / unhandledrejection *before* any page scripts run.
149
+ * Stripped before page transformation so the LLM never sees it.
150
+ */
151
+ const ERROR_CAPTURE_ID = 'synthos-error-capture';
152
+
153
+ const ERROR_CAPTURE_SCRIPT = `<script id="${ERROR_CAPTURE_ID}">
154
+ (function(){
155
+ var E=window.__synthOSErrors=[];
156
+ window.onerror=function(m,s,l,c,e){
157
+ var entry=m+' at '+(s||'?')+':'+(l||'?')+':'+(c||'?');
158
+ if(e&&e.stack)entry+='\\n'+e.stack;
159
+ E.push(entry);showErr();return false;
160
+ };
161
+ window.addEventListener('unhandledrejection',function(ev){
162
+ var r=ev.reason;
163
+ E.push('Unhandled rejection: '+(r&&r.stack?r.stack:String(r)));showErr();
164
+ });
165
+ function showErr(){
166
+ var cm=document.getElementById('chatMessages');if(!cm)return;
167
+ if(showErr._p)return;showErr._p=true;
168
+ setTimeout(function(){
169
+ showErr._p=false;
170
+ var d=document.createElement('div');d.className='chat-message';
171
+ var p=document.createElement('p');
172
+ var pn=(window.pageInfo&&window.pageInfo.productName)||'SynthOS';
173
+ p.innerHTML='<strong>'+pn+':</strong> I noticed a JavaScript error on this page. '+
174
+ '<a href="#" style="color:var(--accent-primary,#a78bfa);text-decoration:underline;cursor:pointer" '+
175
+ 'onclick="(function(e){e.preventDefault();var ci=document.getElementById(\\'chatInput\\');'+
176
+ 'var f=document.getElementById(\\'chatForm\\');if(!ci||!f)return;'+
177
+ 'ci.value=\\'Fix the following JavaScript errors on this page:\\\\n\\\\nCONSOLE_ERRORS:\\\\n\\'+window.__synthOSErrors.join(\\'\\\\n---\\\\n\\');'+
178
+ 'window.__synthOSErrors=[];f.requestSubmit?f.requestSubmit():f.submit();})(event)">'+
179
+ 'Let me try to fix it</a>';
180
+ d.appendChild(p);cm.appendChild(d);
181
+ cm.scrollTo({top:cm.scrollHeight,behavior:'smooth'});
182
+ },500);
183
+ }
184
+ })();
185
+ </script>`;
186
+
187
+ function injectErrorCapture(html: string, pageVersion: number): string {
188
+ if (pageVersion < 2) return html;
189
+ if (html.includes(`id="${ERROR_CAPTURE_ID}"`)) return html;
190
+ const $ = cheerio.load(html, { decodeEntities: false });
191
+ $('head').prepend(ERROR_CAPTURE_SCRIPT + '\n');
192
+ return $.html();
193
+ }
194
+
195
+
196
+ /**
197
+ * Inject shell.css (always) and FluentLM base CSS/JS (v3 themes only).
198
+ * For v3 themes also adds the theme name class to <html> so scoped rules apply.
199
+ */
200
+ function injectShellAssets(html: string, themeName: string, themeVersion: number, toolbarPosition?: string): string {
201
+ const $ = cheerio.load(html, { decodeEntities: false });
202
+ const themeLink = $('link#theme-css');
203
+
204
+ // Favicon
205
+ if ($('link#synthos-favicon').length === 0) {
206
+ $('head').prepend('<link id="synthos-favicon" rel="icon" type="image/svg+xml" href="/static/favicon.svg">\n');
207
+ }
208
+
209
+ // shell.css — always injected (provides toolbar + layout chrome)
210
+ if ($('link#shell-css').length === 0) {
211
+ const shellLink = '<link id="shell-css" rel="stylesheet" href="/static/shell.css">';
212
+ if (themeLink.length > 0) {
213
+ themeLink.before(shellLink + '\n');
214
+ } else {
215
+ $('head').append(shellLink + '\n');
216
+ }
217
+ }
218
+
219
+ // FluentLM assets — only for v3 themes
220
+ if (themeVersion >= 3) {
221
+ // Add theme name class to <html> (e.g. "nebula-dusk")
222
+ $('html').addClass(themeName);
223
+
224
+ // Inject CSS: fluentlm.min.css before shell.css (load order matters)
225
+ if ($('link#fluentlm-css').length === 0) {
226
+ const fluentLink = '<link id="fluentlm-css" rel="stylesheet" href="/static/fluentlm.min.css">';
227
+ const shellCss = $('link#shell-css');
228
+ if (shellCss.length > 0) {
229
+ shellCss.before(fluentLink + '\n');
230
+ } else if (themeLink.length > 0) {
231
+ themeLink.before(fluentLink + '\n');
232
+ } else {
233
+ $('head').append(fluentLink + '\n');
234
+ }
235
+ }
236
+
237
+ // Inject FluentLM JS before </body>
238
+ if ($('script#fluentlm-js').length === 0) {
239
+ const fluentScript = '<script id="fluentlm-js" src="/static/fluentlm.min.js"></script>';
240
+ $('body').append(fluentScript + '\n');
241
+ }
242
+ }
243
+
244
+ $('html').attr('data-toolbar', toolbarPosition || 'left');
245
+
246
+ return $.html();
247
+ }
248
+
249
+ // ---------------------------------------------------------------------------
250
+ // Context section builders — assemble ContextSections from enabled features
251
+ // ---------------------------------------------------------------------------
252
+
253
+ function buildContextSection(): ContextSection {
254
+ const now = new Date();
255
+ const dateTime = now.toLocaleString('en-US', {
256
+ weekday: 'long', year: 'numeric', month: 'long', day: 'numeric',
257
+ hour: 'numeric', minute: '2-digit', hour12: true,
258
+ });
259
+ return {
260
+ title: '<CONTEXT>',
261
+ content: `Current date and time: ${dateTime}`,
262
+ instructions: '',
263
+ };
264
+ }
265
+
266
+ function buildServerApisSection(customizer?: Customizer): ContextSection {
267
+ const content = customizer ? buildRouteHints(customizer) : serverAPIs;
268
+ return {
269
+ title: '<SERVER_APIS>',
270
+ content: content.replace(/^<SERVER_APIS>\n?/, ''),
271
+ instructions: 'provides a list of available server APIs and helper functions you can call from injected scripts. Use synthos.* helpers instead of raw fetch().',
272
+ };
273
+ }
274
+
275
+ async function buildServerScriptsSection(pagesFolder: string): Promise<ContextSection> {
276
+ const scripts = await listScripts(pagesFolder);
277
+ return {
278
+ title: '<SERVER_SCRIPTS>',
279
+ content: scripts || '',
280
+ instructions: 'provides a list of available scripts callable via synthos.scripts.run(id, variables).',
281
+ };
282
+ }
283
+
284
+ function buildConnectorsSection(configuredConnectors?: ConnectorsConfig): ContextSection | undefined {
285
+ if (!configuredConnectors) return undefined;
286
+
287
+ const entries = Object.entries(configuredConnectors)
288
+ .filter(([, cfg]) => cfg.enabled && cfg.apiKey);
289
+ if (entries.length === 0) return undefined;
290
+
291
+ const blocks = entries.map(([id, cfg]) => {
292
+ const def = getConnectorRegistry().find(d => d.id === id);
293
+ if (!def) return `- ${id}`;
294
+ let block = `- ${def.name} (id: "${id}", category: ${def.category})\n Base URL: ${def.baseUrl}`;
295
+ if (def.hints) {
296
+ block += `\n Usage:\n${def.hints.split('\n').map(l => ' ' + l).join('\n')}`;
297
+ }
298
+ // Append dynamic OAuth context
299
+ if (def.authStrategy === 'oauth2') {
300
+ const oauthCfg = cfg as ConnectorOAuthConfig;
301
+ block += '\n Auth: The proxy attaches the access token automatically. Do NOT pass access_token in body or query params.';
302
+ if (oauthCfg.userId) {
303
+ block += `\n User ID: ${oauthCfg.userId} — use this directly in API paths (e.g. /${oauthCfg.userId}/media).`;
304
+ } else {
305
+ block += '\n User ID: Not yet resolved. Call GET /me/accounts to discover it, then GET /{page-id}?fields=instagram_business_account to get the IG user ID.';
306
+ }
307
+ }
308
+ return block;
309
+ });
310
+
311
+ const content = `The user has configured and enabled these connectors:\n${blocks.join('\n\n')}\n\nYou may use synthos.connectors.call(connector, method, path, opts) to call them.\nIMPORTANT: Before making any connector call, ALWAYS check that the connector is configured first using synthos.connectors.list(). If the connector is not configured, show the user a friendly message with a link to the Settings > Connectors page (/settings?tab=connectors) so they can set it up.\nDo NOT hardcode API keys. The connector proxy attaches authentication automatically.`;
312
+
313
+ return {
314
+ title: '<CONFIGURED_CONNECTORS>',
315
+ content,
316
+ instructions: '',
317
+ };
318
+ }
319
+
320
+ function buildAgentsSection(configuredAgents?: AgentConfig[]): ContextSection | undefined {
321
+ const enabledAgents = (configuredAgents ?? []).filter(a => a.enabled);
322
+ if (enabledAgents.length === 0) return undefined;
323
+
324
+ const agentBlocks = enabledAgents.map(a => {
325
+ let block = `- ${a.name} (id: "${a.id}", provider: ${a.provider})`;
326
+ block += `\n Description: ${a.description}`;
327
+ if (a.capabilities?.streaming) {
328
+ block += `\n Supports streaming: yes`;
329
+ }
330
+ if (a.skills && a.skills.length > 0) {
331
+ const skillList = a.skills.map(s => ` - ${s.name}: ${s.description}`).join('\n');
332
+ block += `\n Skills:\n${skillList}`;
333
+ }
334
+ return block;
335
+ });
336
+
337
+ return {
338
+ title: '<CONFIGURED_AGENTS>',
339
+ content: `The user has configured these agents:\n\n${agentBlocks.join('\n\n')}\n\n${AGENT_API_REFERENCE}`,
340
+ instructions: '',
341
+ };
342
+ }
343
+
344
+ function buildThemeSection(themeInfo?: ThemeInfo): ContextSection {
345
+ let content = '';
346
+ if (themeInfo) {
347
+ const { mode, colors } = themeInfo;
348
+ const colorList = Object.entries(colors)
349
+ .map(([name, value]) => ` --${name}: ${value}`)
350
+ .join('\n');
351
+ content = `Mode: ${mode}\nCSS custom properties (use instead of hardcoded values):\n${colorList}\n\nShared shell classes (pre-styled by theme, do not redefine):\n .chat-panel — Left sidebar container (30% width)\n .chat-header — Chat panel title bar\n .chat-messages — Scrollable message container\n .chat-message — Individual message wrapper\n .chat-input — Message text input\n .chat-submit — Send button\n .viewer-panel — Right content area (70% width)\n .loading-overlay — Full-screen loading overlay\n .spinner — Animated loading spinner\n .modal-overlay — Full-screen modal backdrop (position:fixed, z-index:2000, backdrop-filter:blur). Add class "show" to display.\n .modal-content — Centered modal container\n .modal-header — Gradient header bar\n .modal-body — Modal content area\n .modal-footer — Bottom action bar (flex, space-between)\n .modal-footer-right — Right-aligned button group\n\nModals and popups: ALWAYS use the theme\'s .modal-overlay class for any modal or popup overlay. Do NOT create custom overlay classes with position:fixed and z-index. Structure:\n <div class="modal-overlay" id="myModal">\n <div class="modal-content">\n <div class="modal-header">Title</div>\n <div class="modal-body">Content</div>\n <div class="modal-footer"><div class="modal-footer-right"><button>OK</button></div></div>\n </div>\n </div>\nShow/hide by toggling the "show" class: el.classList.add(\'show\') / el.classList.remove(\'show\'). This ensures correct z-index layering above the chat toggle and other UI elements.\n\nPage title bars: To align with the chat header, apply these styles:\n min-height: var(--header-min-height);\n padding: var(--header-padding-vertical) var(--header-padding-horizontal);\n line-height: var(--header-line-height);\n display: flex; align-items: center; justify-content: center; box-sizing: border-box;\n\nFull-viewer mode: For games, animations, or full-screen content, add class "full-viewer" to the viewer-panel element to remove its padding.\n\nChat panel behaviours (auto-injected via page script — do NOT recreate in page code):\n The server injects page-v2.js after transformation. It provides:\n - Form submit handler: sets action to window.location.pathname, shows #loadingOverlay, disables inputs\n - Chat scroll to bottom (#chatMessages)\n - Chat toggle button (.chat-toggle) — created dynamically if not in markup\n - .chat-input-wrapper — wraps #chatInput with a brainstorm icon button\n - Brainstorm modal (#brainstormModal) — LLM-powered brainstorm UI, created dynamically\n - Focus management — keeps keyboard input directed to #chatInput\n\n Do NOT:\n - Create your own form submit handler, toggle button, or input wrapper\n - Modify or replace .chat-panel, .chat-header, #chatForm, or .chat-toggle\n - INSERT new <script> blocks that duplicate existing ones — when fixing JavaScript, UPDATE or REPLACE the existing script's nodeId instead. Always give inline scripts a unique id attribute.\n - Set the form action attribute (page-v2.js sets it dynamically)\n - Include these CSS rules (in the theme): #loadingOverlay position, .chat-submit:disabled, .chat-input:disabled\n\n To add chat messages: use insert with parentId of #chatMessages and position "append".\n #chatMessages is the only unlocked element inside .chat-panel.\n\nThe <html> element has class "${mode}-mode". Always add .light-mode CSS overrides for any page-specific styles so the page works in both light and dark themes, unless the user has explicitly requested a very specific color scheme.`;
352
+ }
353
+
354
+ return {
355
+ title: '<THEME>',
356
+ content,
357
+ instructions: 'provides details on the current theme\'s color scheme and shared shell classes to help you generate theme-aware pages that fit seamlessly into the user experience.',
358
+ };
359
+ }
360
+
361
+ const LLMD_READING_GUIDE = `Content below is LLMD v0.2 — a compressed, token-optimized format. Read it as follows:
362
+
363
+ Line types (each non-empty line starts with exactly one prefix, or none for prose):
364
+
365
+ - @name — scope. Sets the current topic. All following lines belong to this scope
366
+ until the next @. Hierarchy is flattened: @Auth after @API means separate scopes,
367
+ not nested. Reconstruct context from scope names.
368
+ - :k=v k2=v2 — attributes. Key-value facts about the current scope. ¦ (broken bar,
369
+ U+00A6) separates multiple values (e.g., methods=oauth2¦apikey). Multiple pairs
370
+ may appear on one line, space-separated. Parse each pair by splitting on the
371
+ first = (keys never contain =).
372
+ - plain text (no prefix) — prose about the current scope.
373
+ - -item — list item. Nested depth uses dots: -. child, -.. grandchild.
374
+ - →Node — relation. Current scope depends on Node. ←Node is reverse. =Node is
375
+ equivalence. Trailing ? means optional (e.g., →Cache?).
376
+ - ::lang followed by <<<...>>> — literal block. Code or data preserved exactly,
377
+ not compressed.
378
+ - ~k=v — file metadata. Optional, appears at top of file.
379
+
380
+ Reserved meta-attributes (compiler-generated, prefixed with _):
381
+
382
+ - :_col=<header> — column header for a 2-column property table.
383
+ - :_cols=c1¦c2¦c3 — column headers for a multi-column table.
384
+ - :_pfx=<prefix> — common prefix extracted from subsequent keys. Prepend it to
385
+ restore full key names (e.g., :_pfx=flm-text-- then :secondary=... means the
386
+ full key is flm-text--secondary).
387
+
388
+ Compression artifacts (content may be shortened — infer original phrasing):
389
+
390
+ - Common words (the, a, is, are, of, etc.) may be removed from prose and list items.
391
+ - Long phrases replaced with short forms (e.g., "in order to" → "to",
392
+ "application programming interface" → "API", "specification" → "spec").
393
+ - Units shortened (e.g., "1000 requests per minute" → "1000/m",
394
+ "seconds" → "s", "megabytes" → "MB").
395
+ - Boolean values compressed (Yes/No → Y/N, true/false → T/F,
396
+ enabled/disabled → Y/N).
397
+ - Trailing periods stripped from prose and list items.
398
+ - Negation (no, not, never) and modals (must, should, may, always) are always
399
+ preserved.`;
400
+
401
+ function buildLlmdReadingGuideSection(): ContextSection {
402
+ return {
403
+ title: '<LLMD_READING_GUIDE>',
404
+ content: LLMD_READING_GUIDE,
405
+ instructions: '',
406
+ };
407
+ }
408
+
409
+ async function buildFluentLMSection(config: SynthOSConfig): Promise<ContextSection | undefined> {
410
+ const filePath = await findFileInFolders(config.staticFilesFolders, 'fluentlm-instructions.md');
411
+ if (!filePath) return undefined;
412
+ try {
413
+ const content = await loadFile(filePath);
414
+ return {
415
+ title: '<FLUENTLM_COMPONENTS>',
416
+ content,
417
+ instructions: `<FLUENTLM_COMPONENTS> is the component library available on every page. You MUST use FluentLM components instead of writing custom HTML/CSS for standard UI elements.
418
+ REQUIRED: Use flm-button for buttons, flm-textfield for inputs, flm-dropdown for selects, flm-dialog/flm-panel/flm-modal for overlays, flm-pivot for tabs, flm-nav for navigation, flm-toggle for switches, flm-card for cards, flm-callout for tooltips, flm-messagebar for alerts, and all other components listed in <FLUENTLM_COMPONENTS>.
419
+ FORBIDDEN: Do NOT create custom CSS classes for buttons (e.g. .my-btn, .okr-btn), inputs, modals, cards, tabs, dropdowns, or any UI element that has a FluentLM equivalent. Do NOT use raw <button>, <input>, or <select> elements without FluentLM classes.
420
+ Apply FluentLM utility classes (flm-text--secondary, flm-stack, etc.) for layout and typography instead of custom CSS where possible.`,
421
+ };
422
+ } catch {
423
+ return undefined;
424
+ }
425
+ }
426
+
427
+ async function buildRecommendedFrameworksSection(config: SynthOSConfig): Promise<ContextSection | undefined> {
428
+ const filePath = await findFileInFolders(config.staticFilesFolders, 'recommended-frameworks.llmd');
429
+ if (!filePath) return undefined;
430
+ try {
431
+ const content = await loadFile(filePath);
432
+ return {
433
+ title: '<RECOMMENDED_FRAMEWORKS>',
434
+ content,
435
+ instructions: 'lists recommended third-party frameworks with CDN URLs. When a page needs a framework from this list, load it via <script> (or <link> for CSS) tags at the end of the <head> block. Always use the version shown in the CDN URL (it is the latest approved version).',
436
+ };
437
+ } catch {
438
+ return undefined;
439
+ }
440
+ }
441
+
442
+ function buildMessageFormatSection(productName: string): ContextSection {
443
+ return {
444
+ title: '<MESSAGE_FORMAT>',
445
+ content: `<div class="chat-message"><p><strong>{${productName}: | User:}</strong> {message contents}</p></div>`,
446
+ instructions: 'provides the HTML structure for chat messages in the chat panel.',
447
+ };
448
+ }
449
+
450
+ export function usePageRoutes(config: SynthOSConfig, app: Application, customizer?: Customizer): void {
95
451
  // Redirect / to /home page
96
452
  app.get('/', (req, res) => res.redirect(HOME_PAGE_ROUTE));
97
-
453
+
98
454
  // Page retrieval
99
455
  app.get('/:page', async (req, res) => {
100
456
  // Redirect if settings not configured
@@ -105,48 +461,53 @@ export function usePageRoutes(config: SynthOSConfig, app: Application): void {
105
461
  return;
106
462
  }
107
463
 
108
- // Ensure page exists
109
- const pageState = await loadPageWithFallback(page, config, false);
464
+ // Ensure page exists — force fresh disk read for required pages
465
+ const isRequiredPage = config.requiredPages.includes(page);
466
+ const pageState = await loadPageWithFallback(page, config, isRequiredPage);
110
467
  if (!pageState) {
111
468
  res.status(404).send(PAGE_NOT_FOUND);
112
469
  return;
113
470
  }
114
471
 
115
472
  // Load page metadata for version-based script injection
116
- const metadata = await loadPageMetadata(config.pagesFolder, page, config.requiredPagesFolder);
473
+ const metadata = await loadPageMetadata(config.pagesFolder, page, config.requiredPagesFolders);
117
474
  const pageVersion = metadata?.pageVersion ?? 0;
118
475
 
119
- // Block outdated pages (redirect to /pages so user sees upgrade UI)
120
- if (pageVersion < PAGE_VERSION && !REQUIRED_PAGES.includes(page)) {
121
- res.redirect('/pages');
476
+ // Block outdated pages (redirect to tabs list so user sees upgrade UI)
477
+ if (pageVersion < PAGE_VERSION && !config.requiredPages.includes(page)) {
478
+ res.redirect(customizer?.tabsListRoute ?? '/pages');
122
479
  return;
123
480
  }
124
481
 
482
+ // Load settings to determine theme version for FluentLM base injection
483
+ const settings = await loadSettings(config.pagesFolder);
484
+ const themeName = settings.theme ?? 'nebula-dusk';
485
+ const themeVersion = await loadThemeVersion(themeName, config);
486
+
125
487
  let html = ensureRequiredImports(pageState, pageVersion);
488
+ html = injectErrorCapture(html, pageVersion);
489
+ html = injectShellAssets(html, themeName, themeVersion, settings.toolbarPosition);
126
490
  html = injectPageInfoScript(html, page);
127
491
  html = injectPageHelpers(html, pageVersion);
128
492
  html = injectPageScript(html, pageVersion);
129
- res.send(html);
130
- });
131
493
 
132
- // Page reset
133
- app.get('/:page/reset', async (req, res) => {
134
- // Redirect if settings not configured
135
- const { page } = req.params;
136
- const isConfigured = await hasConfiguredSettings(config.pagesFolder);
137
- if (!isConfigured) {
138
- res.redirect('/settings?firstRun=1');
139
- return;
494
+ // Inject version meta tag so undo/try-again links appear on page load
495
+ {
496
+ const latestVersion = await getLatestVersion(config.pagesFolder, page);
497
+ if (latestVersion > 0) {
498
+ html = html.replace('</head>', `<meta name="synthos-version" content="${latestVersion}">\n</head>`);
499
+ }
140
500
  }
141
501
 
142
- // Ensure page exists
143
- const pageState = await loadPageWithFallback(page, config, true);
144
- if (!pageState) {
145
- res.status(404).send(PAGE_NOT_FOUND);
146
- return;
502
+ html = hoistExternalScriptsToHead(html);
503
+
504
+ // Replace branding for white-label forks
505
+ const productName = customizer?.productName ?? 'SynthOS';
506
+ if (productName !== 'SynthOS') {
507
+ html = html.replace(/SynthOS/g, productName);
147
508
  }
148
509
 
149
- res.redirect(`/${page}`);
510
+ res.send(html);
150
511
  });
151
512
 
152
513
  // Page save
@@ -191,23 +552,26 @@ export function usePageRoutes(config: SynthOSConfig, app: Application): void {
191
552
  return;
192
553
  }
193
554
 
194
- // If greeting is provided, process with cheerio
195
- if (greeting && typeof greeting === 'string' && greeting.trim().length > 0) {
555
+ // Always trim chat to the first message on save and remove undo links
556
+ {
196
557
  const $ = cheerio.load(pageState);
197
558
  const messages = $('#chatMessages .chat-message');
198
- // Keep only the first message, remove the rest
199
559
  messages.slice(1).remove();
200
- // Update the greeting text in the first message
201
- const firstP = messages.first().find('p');
202
- const strong = firstP.find('strong');
203
- if (strong.length) {
204
- firstP.html('<strong>Synthos:</strong> ' + greeting.trim());
560
+ // Remove any undo links
561
+ $('#chatMessages .synthos-undo-link').remove();
562
+ // Update greeting text if provided
563
+ if (greeting && typeof greeting === 'string' && greeting.trim().length > 0) {
564
+ const firstP = messages.first().find('p');
565
+ const strong = firstP.find('strong');
566
+ if (strong.length) {
567
+ firstP.html('<strong>Synthos:</strong> ' + greeting.trim());
568
+ }
205
569
  }
206
570
  pageState = $.html();
207
571
  }
208
572
 
209
573
  // Inject save-line marker at the end of chat messages (skip for locked pages)
210
- const sourceMetadata = await loadPageMetadata(config.pagesFolder, page, config.requiredPagesFolder);
574
+ const sourceMetadata = await loadPageMetadata(config.pagesFolder, page, config.requiredPagesFolders);
211
575
  if (sourceMetadata?.mode !== 'locked') {
212
576
  const $ = cheerio.load(pageState);
213
577
  // Remove any existing save-line first
@@ -222,6 +586,31 @@ export function usePageRoutes(config: SynthOSConfig, app: Application): void {
222
586
  // Save as new page
223
587
  await savePageState(config.pagesFolder, saveAs, pageState, title, categories);
224
588
 
589
+ // Copy files (sound effects, etc.) from source page when saving as a different name
590
+ if (page !== saveAs) {
591
+ // Check source page's files/ dir in user pages, then fallback folders
592
+ let sourceFilesDir: string | undefined;
593
+ const userFilesDir = path.join(config.pagesFolder, 'pages', page, 'files');
594
+ if (await checkIfExists(userFilesDir)) {
595
+ sourceFilesDir = userFilesDir;
596
+ } else {
597
+ for (const folder of config.requiredPagesFolders) {
598
+ const candidate = path.join(folder, page, 'files');
599
+ if (await checkIfExists(candidate)) {
600
+ sourceFilesDir = candidate;
601
+ break;
602
+ }
603
+ }
604
+ }
605
+ if (sourceFilesDir) {
606
+ const targetFilesDir = path.join(config.pagesFolder, 'pages', saveAs, 'files');
607
+ await copyFolderRecursive(sourceFilesDir, targetFilesDir);
608
+ }
609
+ }
610
+
611
+ // Clear version files after saving (fresh baseline)
612
+ await clearVersions(config.pagesFolder, saveAs);
613
+
225
614
  // Also update metadata with categories (in case page.json already existed)
226
615
  await savePageMetadata(config.pagesFolder, saveAs, {
227
616
  title,
@@ -241,6 +630,74 @@ export function usePageRoutes(config: SynthOSConfig, app: Application): void {
241
630
  }
242
631
  });
243
632
 
633
+ // Page undo — roll back to the previous version
634
+ app.post('/:page/undo', async (req, res) => {
635
+ try {
636
+ const { page } = req.params;
637
+ const pagesFolder = config.pagesFolder;
638
+ const cv = await getLatestVersion(pagesFolder, page);
639
+ if (cv <= 0) {
640
+ res.status(400).send('Nothing to undo');
641
+ return;
642
+ }
643
+
644
+ // Delete the current version file
645
+ const pageFolder = path.join(pagesFolder, 'pages', page);
646
+ const versionFile = path.join(pageFolder, `page.v${cv}.html`);
647
+ if (await checkIfExists(versionFile)) {
648
+ await deleteFileFromDisk(versionFile);
649
+ }
650
+
651
+ // Load previous version (v{cv-1}.html, or page.html if rolling back to v0)
652
+ const prevVersion = cv - 1;
653
+ let previousHtml: string | undefined;
654
+ if (prevVersion > 0) {
655
+ previousHtml = await loadPageVersion(pagesFolder, page, prevVersion);
656
+ } else {
657
+ // v0 = the saved page.html baseline (user folder → required folders fallback)
658
+ previousHtml = await loadPageWithFallback(page, config, true);
659
+ }
660
+
661
+ if (!previousHtml) {
662
+ res.status(500).send('Could not load previous version');
663
+ return;
664
+ }
665
+
666
+ // Inject shell assets (same as GET handler)
667
+ const settings = await loadSettings(config.pagesFolder);
668
+ const metadata = await loadPageMetadata(pagesFolder, page, config.requiredPagesFolders);
669
+ const pv = metadata?.pageVersion ?? 0;
670
+ const themeName = settings.theme ?? 'nebula-dusk';
671
+ const themeVersion = await loadThemeVersion(themeName, config);
672
+ let out = ensureRequiredImports(previousHtml, pv);
673
+ out = injectErrorCapture(out, pv);
674
+ out = injectShellAssets(out, themeName, themeVersion, settings.toolbarPosition);
675
+ out = injectPageInfoScript(out, page);
676
+ out = injectPageHelpers(out, pv);
677
+ out = injectPageScript(out, pv);
678
+
679
+ // Inject version meta tag if still have versions
680
+ if (prevVersion > 0) {
681
+ out = out.replace('</head>', `<meta name="synthos-version" content="${prevVersion}">\n</head>`);
682
+ }
683
+
684
+ // Same hardening as POST handler (undo also loads via document.write)
685
+ out = hoistExternalScriptsToHead(out);
686
+ out = wrapInlineScriptsInIIFE(out);
687
+
688
+ // Replace branding for white-label forks
689
+ const productName = customizer?.productName ?? 'SynthOS';
690
+ if (productName !== 'SynthOS') {
691
+ out = out.replace(/SynthOS/g, productName);
692
+ }
693
+
694
+ res.send(out);
695
+ } catch (err: unknown) {
696
+ console.error(err);
697
+ res.status(500).send((err as Error).message);
698
+ }
699
+ });
700
+
244
701
  // Page transformation
245
702
  app.post('/:page', async (req, res) => {
246
703
  try {
@@ -259,6 +716,13 @@ export function usePageRoutes(config: SynthOSConfig, app: Application): void {
259
716
  return;
260
717
  }
261
718
 
719
+ // Reject modifications to locked pages
720
+ const lockMetadata = await loadPageMetadata(config.pagesFolder, page, config.requiredPagesFolders);
721
+ if (lockMetadata?.mode === 'locked') {
722
+ res.status(403).send('This page is locked and cannot be modified');
723
+ return;
724
+ }
725
+
262
726
  // Get required and optional parameters
263
727
  const { message } = req.body; // Extract the message from the request body
264
728
  if (typeof message !== 'string') {
@@ -266,56 +730,211 @@ export function usePageRoutes(config: SynthOSConfig, app: Application): void {
266
730
  return;
267
731
  }
268
732
 
733
+ // Extract and validate optional attachments
734
+ let attachments: Attachment[] | undefined;
735
+ if (Array.isArray(req.body.attachments)) {
736
+ attachments = [];
737
+ for (const att of req.body.attachments) {
738
+ if (att && typeof att.mediaType === 'string' && att.mediaType.startsWith('image/') && typeof att.data === 'string') {
739
+ attachments.push({ mediaType: att.mediaType, data: att.data, name: typeof att.name === 'string' ? att.name : undefined });
740
+ }
741
+ }
742
+ if (attachments.length === 0) attachments = undefined;
743
+ }
744
+
269
745
  // Create model instance
270
746
  const innerCompletePrompt = await createCompletePrompt(config.pagesFolder, 'builder', req.body.model);
271
747
  const debugVerbose = config.debugPageUpdates;
272
748
  let inputChars = 0;
273
749
  let outputChars = 0;
274
- const completePrompt: completePrompt = async (args) => {
275
- if (debugVerbose) {
276
- console.log(green(dim('\n ===== PAGE UPDATE REQUEST =====')));
277
- console.log(green(` SYSTEM:\n${args.system?.content}`));
278
- console.log(green(`\n PROMPT:\n${args.prompt.content}`));
279
- }
280
- inputChars += (args.system?.content?.length ?? 0) + (args.prompt.content?.length ?? 0);
281
- const result = await innerCompletePrompt(args);
282
- if (result.completed) {
283
- outputChars += result.value?.length ?? 0;
284
- }
285
- if (debugVerbose) {
286
- console.log(green(dim('\n ----- PAGE UPDATE RESPONSE -----')));
750
+ const wrapModel = (inner: completePrompt): completePrompt => {
751
+ return async (args) => {
752
+ if (debugVerbose) {
753
+ console.log(green(dim('\n ===== PAGE UPDATE REQUEST =====')));
754
+ console.log(green(` SYSTEM:\n${args.system?.content}`));
755
+ console.log(green(`\n PROMPT:\n${args.prompt.content}`));
756
+ }
757
+ inputChars += (args.system?.content?.length ?? 0) + (args.prompt.content?.length ?? 0);
758
+ const result = await inner(args);
287
759
  if (result.completed) {
288
- console.log(green(` RESPONSE:\n${result.value}`));
289
- } else {
290
- console.log(red(` ERROR: ${result.error?.message}`));
760
+ outputChars += result.value?.length ?? 0;
291
761
  }
292
- console.log(green(dim(' ================================\n')));
293
- }
294
- return result;
295
- }
762
+ if (debugVerbose) {
763
+ console.log(green(dim('\n ----- PAGE UPDATE RESPONSE -----')));
764
+ if (result.completed) {
765
+ console.log(green(` RESPONSE:\n${result.value}`));
766
+ } else {
767
+ console.log(red(` ERROR: ${result.error?.message}`));
768
+ }
769
+ console.log(green(dim(' ================================\n')));
770
+ }
771
+ return result;
772
+ };
773
+ };
774
+ const wrappedCompletePrompt = wrapModel(innerCompletePrompt);
296
775
 
297
- // Transform and cache updated page
776
+ // Load settings and build context
298
777
  const pagesFolder = config.pagesFolder;
299
778
  const settings = await loadSettings(config.pagesFolder);
300
- const builder = getModelEntry(settings, 'builder');
301
- const { instructions } = builder;
779
+ const entry = getModelEntry(settings, 'builder');
780
+ const { instructions } = entry;
781
+ const productName = customizer?.productName ?? 'SynthOS';
782
+
783
+ // Build context sections from enabled features
784
+ const featureSections: ContextSection[] = [];
785
+
786
+ // CONTEXT section — always first so the model knows the current date/time
787
+ featureSections.push(buildContextSection());
788
+
789
+ // LLMD_READING_GUIDE section (first — tells model how to read compressed content)
790
+ featureSections.push(buildLlmdReadingGuideSection());
791
+
792
+ // SERVER_APIS section
793
+ featureSections.push(buildServerApisSection(customizer));
794
+
795
+ // SERVER_SCRIPTS section
796
+ featureSections.push(await buildServerScriptsSection(pagesFolder));
797
+
798
+ // CONFIGURED_CONNECTORS section
799
+ const connectorsSection = buildConnectorsSection(settings.connectors);
800
+ if (connectorsSection) featureSections.push(connectorsSection);
801
+
802
+ // CONFIGURED_AGENTS section
803
+ const agentsSection = buildAgentsSection(settings.agents);
804
+ if (agentsSection) featureSections.push(agentsSection);
805
+
806
+ // THEME section
302
807
  const theme = settings.theme;
303
808
  const themeInfo = await loadThemeInfo(theme ?? 'nebula-dusk', config);
304
- const modelInstructions = getModelInstructions(builder.provider);
305
- const configuredConnectors = settings.connectors;
306
- const configuredAgents = settings.agents;
307
- const result = await transformPage({ pagesFolder, pageState, message, instructions, modelInstructions, completePrompt, themeInfo, configuredConnectors, configuredAgents });
809
+ featureSections.push(buildThemeSection(themeInfo));
810
+
811
+ // FLUENTLM_COMPONENTS section
812
+ const fluentLMSection = await buildFluentLMSection(config);
813
+ if (fluentLMSection) featureSections.push(fluentLMSection);
814
+
815
+ // RECOMMENDED_FRAMEWORKS section
816
+ const frameworksSection = await buildRecommendedFrameworksSection(config);
817
+ if (frameworksSection) featureSections.push(frameworksSection);
818
+
819
+ // MESSAGE_FORMAT section
820
+ featureSections.push(buildMessageFormatSection(productName));
821
+
822
+ // Custom transform instructions as a section (backward compat)
823
+ const customTransformInstructions = customizer ? customizer.getTransformInstructions() : undefined;
824
+ if (customTransformInstructions && customTransformInstructions.length > 0) {
825
+ featureSections.push({
826
+ title: '<CUSTOM_INSTRUCTIONS>',
827
+ content: customTransformInstructions.join('\n'),
828
+ instructions: '',
829
+ });
830
+ }
831
+
832
+ // Custom context sections from Customizer (appended last)
833
+ if (customizer) {
834
+ featureSections.push(...customizer.getContextSections());
835
+ }
836
+
837
+ // Detect first edit (v0→v1) for saved pages
838
+ const isRequiredPage = config.requiredPages.includes(page);
839
+ const pageFolder = path.join(pagesFolder, 'pages', page);
840
+ const pageFileExists = await checkIfExists(path.join(pageFolder, 'page.html'));
841
+ let currentVersion = await getLatestVersion(pagesFolder, page);
842
+ const isFirstEdit = !isRequiredPage && pageFileExists && currentVersion === 0;
843
+
844
+ // Try again — roll back to previous version before re-running the transform
845
+ const tryAgain = req.body.tryAgain === true;
846
+ let transformInput = pageState;
847
+ if (tryAgain && currentVersion > 0) {
848
+ // Delete current version file
849
+ const versionFile = path.join(pageFolder, `page.v${currentVersion}.html`);
850
+ if (await checkIfExists(versionFile)) {
851
+ await deleteFileFromDisk(versionFile);
852
+ }
853
+ // Load previous version
854
+ const prevVersion = currentVersion - 1;
855
+ if (prevVersion > 0) {
856
+ transformInput = await loadPageVersion(pagesFolder, page, prevVersion) ?? pageState;
857
+ } else {
858
+ transformInput = await loadPageWithFallback(page, config, true) ?? pageState;
859
+ }
860
+ currentVersion = prevVersion;
861
+ }
862
+
863
+ // Create builder
864
+ const builder = createBuilder(entry.provider, wrappedCompletePrompt, instructions, productName, {
865
+ apiKey: entry.configuration.apiKey,
866
+ model: entry.configuration.model,
867
+ wrapModel,
868
+ isFirstEdit,
869
+ tryAgain,
870
+ });
871
+
872
+ // Inject save-line before transform if not already present, so new
873
+ // messages appended by the LLM land after the marker.
874
+ {
875
+ const $pre = cheerio.load(transformInput, { decodeEntities: false });
876
+ if ($pre('#chatMessages').length > 0 && $pre('#chatMessages .save-line').length === 0) {
877
+ $pre('#chatMessages').append(
878
+ '<div class="save-line" data-locked="true"><span class="save-line-label">Saved</span></div>'
879
+ );
880
+ transformInput = $pre.html();
881
+ }
882
+ }
883
+
884
+ // Transform page
885
+ const result = await transformPage({
886
+ pageState: transformInput,
887
+ message,
888
+ instructions,
889
+ builder,
890
+ additionalSections: featureSections,
891
+ isBuilder: page === 'builder',
892
+ productName,
893
+ attachments,
894
+ });
895
+
308
896
  if (result.completed) {
309
- const { html, changeCount } = result.value!;
897
+ let { html, changeCount } = result.value!;
310
898
  if (config.debug) {
311
899
  const inTokens = estimateTokens(inputChars).toLocaleString();
312
900
  const outTokens = estimateTokens(outputChars).toLocaleString();
313
901
  console.log(` page: ${page} | message: ${message.length} chars | changes: ${changeCount} ops | ~${inTokens} in / ~${outTokens} out tokens`);
314
902
  }
315
- updatePageState(page, html);
903
+
904
+ // Handle 0-ops from transforms: model returned no page changes.
905
+ // Append the user's message + a "try again" prompt with undo link.
906
+ // Still save as a version so undo logic stays simple.
907
+ // changeCount -1 means error/reply path already handled messaging.
908
+ if (changeCount === 0) {
909
+ const $ = cheerio.load(pageState, { decodeEntities: false });
910
+ const chatMessages = $('#chatMessages');
911
+ if (chatMessages.length > 0) {
912
+ const escapedMsg = message.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
913
+ chatMessages.append(
914
+ `<div class="chat-message"><p><strong>User:</strong> ${escapedMsg}</p></div>`
915
+ );
916
+
917
+ const tryAgainLink = `<a href="#" style="color:var(--themePrimary);text-decoration:underline;cursor:pointer" `
918
+ + `onclick="(function(e){e.preventDefault();var ci=document.getElementById('chatInput');if(ci){ci.focus();ci.value='';}})(event)">try again</a>`;
919
+ const undoLink = `<a href="#" style="color:var(--themePrimary);text-decoration:underline;cursor:pointer" `
920
+ + `onclick="(function(e){e.preventDefault();var o=document.getElementById('loadingOverlay');if(o)o.style.display='flex';`
921
+ + `fetch(window.location.pathname+'/undo',{method:'POST',headers:{'Content-Type':'application/json'},body:'{}'})`
922
+ + `.then(function(r){return r.text()}).then(function(h){document.open();document.write(h);document.close()})`
923
+ + `.catch(function(){if(o)o.style.display='none'});})(event)">undo</a>`;
924
+
925
+ chatMessages.append(
926
+ `<div class="chat-message"><p><strong>${productName}:</strong> Sorry, I wasn\u2019t able to make changes for that request. Please ${tryAgainLink} or ${undoLink}.</p></div>`
927
+ );
928
+ }
929
+ html = $.html();
930
+ }
931
+
932
+ // Save version snapshot (working state for all pages, undo support for saved pages)
933
+ const nextVersion = currentVersion + 1;
934
+ await savePageVersion(pagesFolder, page, nextVersion, html);
316
935
 
317
936
  // Update lastModified timestamp in page metadata
318
- const metadata = await loadPageMetadata(pagesFolder, page, config.requiredPagesFolder);
937
+ const metadata = await loadPageMetadata(pagesFolder, page, config.requiredPagesFolders);
319
938
  if (metadata) {
320
939
  metadata.lastModified = new Date().toISOString();
321
940
  await savePageMetadata(pagesFolder, page, metadata);
@@ -323,10 +942,31 @@ export function usePageRoutes(config: SynthOSConfig, app: Application): void {
323
942
 
324
943
  // Inject required imports and page scripts (same as GET)
325
944
  const pv = metadata?.pageVersion ?? 0;
945
+ const themeName = settings.theme ?? 'nebula-dusk';
946
+ const themeVersion = await loadThemeVersion(themeName, config);
326
947
  let out = ensureRequiredImports(html, pv);
948
+ out = injectErrorCapture(out, pv);
949
+ out = injectShellAssets(out, themeName, themeVersion, settings.toolbarPosition);
327
950
  out = injectPageInfoScript(out, page);
328
951
  out = injectPageHelpers(out, pv);
329
952
  out = injectPageScript(out, pv);
953
+
954
+ // Inject version meta tag for client-side undo support
955
+ if (nextVersion > 0) {
956
+ out = out.replace('</head>', `<meta name="synthos-version" content="${nextVersion}">\n</head>`);
957
+ }
958
+
959
+ // Hoist external scripts to <head> so libs load before
960
+ // inline body scripts, then wrap inlines in IIFEs to
961
+ // avoid const/let redeclaration via document.write().
962
+ out = hoistExternalScriptsToHead(out);
963
+ out = wrapInlineScriptsInIIFE(out);
964
+
965
+ // Replace branding for white-label forks
966
+ if (productName !== 'SynthOS') {
967
+ out = out.replace(/SynthOS/g, productName);
968
+ }
969
+
330
970
  res.send(out);
331
971
  } else {
332
972
  throw result.error;
@@ -343,12 +983,19 @@ export function usePageRoutes(config: SynthOSConfig, app: Application): void {
343
983
  }
344
984
 
345
985
  export async function loadPageWithFallback(page: string, config: SynthOSConfig, reset: boolean): Promise<string|undefined> {
346
- // Try primary pages folder first
347
- const pageState = await loadPageState(config.pagesFolder, page, reset);
348
- if (pageState) {
349
- return pageState;
986
+ if (reset) {
987
+ // Clear working-state versions so we get the fresh template
988
+ await clearVersions(config.pagesFolder, page);
350
989
  }
351
990
 
352
- // Try fallback pages folder second
353
- return loadPageState(config.requiredPagesFolder, page, reset);
991
+ // Try primary pages folder first
992
+ const pageState = await loadPageState(config.pagesFolder, page);
993
+ if (pageState) return pageState;
994
+
995
+ // Try fallback pages folders
996
+ for (const folder of config.requiredPagesFolders) {
997
+ const state = await loadPageState(folder, page);
998
+ if (state) return state;
999
+ }
1000
+ return undefined;
354
1001
  }