synthos 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (368) hide show
  1. package/README.md +1 -1
  2. package/default-pages/application/page.html +42 -0
  3. package/default-pages/application/page.json +10 -0
  4. package/default-pages/elevenlabs_effects_studio/page.html +1363 -0
  5. package/default-pages/elevenlabs_effects_studio/page.json +11 -0
  6. package/default-pages/elevenlabs_voice_studio/page.html +801 -0
  7. package/default-pages/elevenlabs_voice_studio/page.json +11 -0
  8. package/default-pages/{json_tools.html → json_tools/page.html} +13 -11
  9. package/default-pages/json_tools/page.json +10 -0
  10. package/default-pages/my_notes/notes/a1b2c3d4-e5f6-7890-abcd-ef1234567890.json +5 -0
  11. package/default-pages/my_notes/page.html +132 -0
  12. package/default-pages/{my_notes.json → my_notes/page.json} +2 -2
  13. package/default-pages/neon_asteroids/files/Ambient_Space.mp3 +0 -0
  14. package/default-pages/neon_asteroids/files/Ambient_Space2.mp3 +0 -0
  15. package/default-pages/neon_asteroids/files/Ambient_Space3.mp3 +0 -0
  16. package/default-pages/neon_asteroids/files/Asteroid_Explosion.mp3 +0 -0
  17. package/default-pages/neon_asteroids/files/Hyperspace_Jump.mp3 +0 -0
  18. package/default-pages/neon_asteroids/files/Laser_Fire.mp3 +0 -0
  19. package/default-pages/neon_asteroids/files/Menu_Navigate.mp3 +0 -0
  20. package/default-pages/neon_asteroids/files/Power_Up_Collect.mp3 +0 -0
  21. package/default-pages/neon_asteroids/files/Saucer_Alert.mp3 +0 -0
  22. package/default-pages/neon_asteroids/files/Ship_Thrust.mp3 +0 -0
  23. package/default-pages/neon_asteroids/files/effects.json +74 -0
  24. package/default-pages/neon_asteroids/page.html +1803 -0
  25. package/default-pages/{neon_asteroids.json → neon_asteroids/page.json} +3 -3
  26. package/default-pages/{oregon_trail.html → oregon_trail/page.html} +16 -30
  27. package/default-pages/{oregon_trail.json → oregon_trail/page.json} +2 -2
  28. package/default-pages/retro_game_starter/page.html +1308 -0
  29. package/default-pages/retro_game_starter/page.json +12 -0
  30. package/default-pages/{sidebar_page.html → sidebar_page/page.html} +12 -10
  31. package/default-pages/sidebar_page/page.json +10 -0
  32. package/default-pages/{solar_explorer.html → solar_explorer/page.html} +15 -12
  33. package/default-pages/{solar_explorer.json → solar_explorer/page.json} +2 -2
  34. package/default-pages/{solar_tutorial.html → solar_tutorial/page.html} +12 -10
  35. package/default-pages/solar_tutorial/page.json +10 -0
  36. package/default-pages/{two-panel_page.html → two-panel_page/page.html} +13 -11
  37. package/default-pages/two-panel_page/page.json +10 -0
  38. package/default-pages/{us_map.html → us_map/page.html} +193 -192
  39. package/default-pages/{us_map.json → us_map/page.json} +12 -12
  40. package/default-pages/{us_map_1850.html → us_map_1850/page.html} +326 -325
  41. package/default-pages/{us_map_1850.json → us_map_1850/page.json} +12 -12
  42. package/default-pages/{western_cities_1850.html → western_cities_1850/page.html} +527 -526
  43. package/default-pages/{western_cities_1850.json → western_cities_1850/page.json} +12 -12
  44. package/default-themes/aurora-dawn.json +19 -0
  45. package/default-themes/aurora-dawn.v3.css +198 -0
  46. package/default-themes/aurora-dusk.json +19 -0
  47. package/default-themes/aurora-dusk.v3.css +200 -0
  48. package/default-themes/cosmos-dawn.json +19 -0
  49. package/default-themes/cosmos-dawn.v3.css +198 -0
  50. package/default-themes/cosmos-dusk.json +19 -0
  51. package/default-themes/cosmos-dusk.v3.css +200 -0
  52. package/default-themes/high-contrast-dark.json +19 -0
  53. package/default-themes/high-contrast-dark.v3.css +200 -0
  54. package/default-themes/high-contrast-light.json +19 -0
  55. package/default-themes/high-contrast-light.v3.css +198 -0
  56. package/default-themes/nebula-dawn.v2.css +110 -0
  57. package/default-themes/nebula-dawn.v3.css +199 -0
  58. package/default-themes/nebula-dusk.v2.css +104 -0
  59. package/default-themes/nebula-dusk.v3.css +201 -0
  60. package/default-themes/solar-flare-dawn.json +19 -0
  61. package/default-themes/solar-flare-dawn.v3.css +198 -0
  62. package/default-themes/solar-flare-dusk.json +19 -0
  63. package/default-themes/solar-flare-dusk.v3.css +200 -0
  64. package/dist/agents/index.d.ts +1 -1
  65. package/dist/agents/index.d.ts.map +1 -1
  66. package/dist/agents/index.js +2 -1
  67. package/dist/agents/index.js.map +1 -1
  68. package/dist/agents/openclaw/gatewayManager.d.ts +4 -0
  69. package/dist/agents/openclaw/gatewayManager.d.ts.map +1 -1
  70. package/dist/agents/openclaw/gatewayManager.js +27 -11
  71. package/dist/agents/openclaw/gatewayManager.js.map +1 -1
  72. package/dist/agents/openclaw/openclawProvider.d.ts.map +1 -1
  73. package/dist/agents/openclaw/openclawProvider.js +2 -4
  74. package/dist/agents/openclaw/openclawProvider.js.map +1 -1
  75. package/dist/agents/openclaw/sshTunnelManager.d.ts +2 -0
  76. package/dist/agents/openclaw/sshTunnelManager.d.ts.map +1 -1
  77. package/dist/agents/openclaw/sshTunnelManager.js +31 -12
  78. package/dist/agents/openclaw/sshTunnelManager.js.map +1 -1
  79. package/dist/builders/anthropic.d.ts +31 -0
  80. package/dist/builders/anthropic.d.ts.map +1 -0
  81. package/dist/builders/anthropic.js +227 -0
  82. package/dist/builders/anthropic.js.map +1 -0
  83. package/dist/builders/fireworksai.d.ts +9 -0
  84. package/dist/builders/fireworksai.d.ts.map +1 -0
  85. package/dist/builders/fireworksai.js +57 -0
  86. package/dist/builders/fireworksai.js.map +1 -0
  87. package/dist/builders/index.d.ts +13 -0
  88. package/dist/builders/index.d.ts.map +1 -0
  89. package/dist/builders/index.js +31 -0
  90. package/dist/builders/index.js.map +1 -0
  91. package/dist/builders/openai.d.ts +8 -0
  92. package/dist/builders/openai.d.ts.map +1 -0
  93. package/dist/builders/openai.js +87 -0
  94. package/dist/builders/openai.js.map +1 -0
  95. package/dist/builders/types.d.ts +54 -0
  96. package/dist/builders/types.d.ts.map +1 -0
  97. package/dist/builders/types.js +211 -0
  98. package/dist/builders/types.js.map +1 -0
  99. package/dist/connectors/index.d.ts +1 -1
  100. package/dist/connectors/index.d.ts.map +1 -1
  101. package/dist/connectors/index.js +3 -2
  102. package/dist/connectors/index.js.map +1 -1
  103. package/dist/connectors/registry.d.ts +2 -1
  104. package/dist/connectors/registry.d.ts.map +1 -1
  105. package/dist/connectors/registry.js +31 -8
  106. package/dist/connectors/registry.js.map +1 -1
  107. package/dist/customizer/Customizer.d.ts +62 -0
  108. package/dist/customizer/Customizer.d.ts.map +1 -0
  109. package/dist/customizer/Customizer.js +134 -0
  110. package/dist/customizer/Customizer.js.map +1 -0
  111. package/dist/customizer/index.d.ts +4 -0
  112. package/dist/customizer/index.d.ts.map +1 -0
  113. package/dist/customizer/index.js +9 -0
  114. package/dist/customizer/index.js.map +1 -0
  115. package/dist/files.d.ts +16 -0
  116. package/dist/files.d.ts.map +1 -1
  117. package/dist/files.js +60 -1
  118. package/dist/files.js.map +1 -1
  119. package/dist/index.d.ts +2 -0
  120. package/dist/index.d.ts.map +1 -1
  121. package/dist/index.js +2 -0
  122. package/dist/index.js.map +1 -1
  123. package/dist/init.d.ts +12 -6
  124. package/dist/init.d.ts.map +1 -1
  125. package/dist/init.js +150 -133
  126. package/dist/init.js.map +1 -1
  127. package/dist/migrations.d.ts.map +1 -1
  128. package/dist/migrations.js +23 -10
  129. package/dist/migrations.js.map +1 -1
  130. package/dist/models/anthropic.d.ts +4 -2
  131. package/dist/models/anthropic.d.ts.map +1 -1
  132. package/dist/models/anthropic.js +33 -6
  133. package/dist/models/anthropic.js.map +1 -1
  134. package/dist/models/fireworksai.d.ts.map +1 -1
  135. package/dist/models/fireworksai.js +9 -1
  136. package/dist/models/fireworksai.js.map +1 -1
  137. package/dist/models/index.d.ts +1 -1
  138. package/dist/models/index.d.ts.map +1 -1
  139. package/dist/models/index.js +2 -1
  140. package/dist/models/index.js.map +1 -1
  141. package/dist/models/openai.d.ts +1 -1
  142. package/dist/models/openai.d.ts.map +1 -1
  143. package/dist/models/openai.js +24 -3
  144. package/dist/models/openai.js.map +1 -1
  145. package/dist/models/types.d.ts +20 -1
  146. package/dist/models/types.d.ts.map +1 -1
  147. package/dist/models/types.js +6 -1
  148. package/dist/models/types.js.map +1 -1
  149. package/dist/pages.d.ts +34 -10
  150. package/dist/pages.d.ts.map +1 -1
  151. package/dist/pages.js +229 -79
  152. package/dist/pages.js.map +1 -1
  153. package/dist/service/createCompletePrompt.d.ts +2 -1
  154. package/dist/service/createCompletePrompt.d.ts.map +1 -1
  155. package/dist/service/createCompletePrompt.js +2 -2
  156. package/dist/service/createCompletePrompt.js.map +1 -1
  157. package/dist/service/requiresSettings.d.ts +2 -1
  158. package/dist/service/requiresSettings.d.ts.map +1 -1
  159. package/dist/service/requiresSettings.js +3 -3
  160. package/dist/service/requiresSettings.js.map +1 -1
  161. package/dist/service/server.d.ts +2 -1
  162. package/dist/service/server.d.ts.map +1 -1
  163. package/dist/service/server.js +37 -8
  164. package/dist/service/server.js.map +1 -1
  165. package/dist/service/transformPage.d.ts +47 -20
  166. package/dist/service/transformPage.d.ts.map +1 -1
  167. package/dist/service/transformPage.js +514 -293
  168. package/dist/service/transformPage.js.map +1 -1
  169. package/dist/service/useAgentRoutes.d.ts +2 -1
  170. package/dist/service/useAgentRoutes.d.ts.map +1 -1
  171. package/dist/service/useAgentRoutes.js +17 -14
  172. package/dist/service/useAgentRoutes.js.map +1 -1
  173. package/dist/service/useApiRoutes.d.ts +2 -1
  174. package/dist/service/useApiRoutes.d.ts.map +1 -1
  175. package/dist/service/useApiRoutes.js +287 -172
  176. package/dist/service/useApiRoutes.js.map +1 -1
  177. package/dist/service/useConnectorRoutes.js +17 -17
  178. package/dist/service/useConnectorRoutes.js.map +1 -1
  179. package/dist/service/useDataRoutes.d.ts.map +1 -1
  180. package/dist/service/useDataRoutes.js +13 -10
  181. package/dist/service/useDataRoutes.js.map +1 -1
  182. package/dist/service/useFileRoutes.d.ts +4 -0
  183. package/dist/service/useFileRoutes.d.ts.map +1 -0
  184. package/dist/service/useFileRoutes.js +122 -0
  185. package/dist/service/useFileRoutes.js.map +1 -0
  186. package/dist/service/usePageRoutes.d.ts +2 -1
  187. package/dist/service/usePageRoutes.d.ts.map +1 -1
  188. package/dist/service/usePageRoutes.js +671 -74
  189. package/dist/service/usePageRoutes.js.map +1 -1
  190. package/dist/service/useSharedDataRoutes.d.ts +4 -0
  191. package/dist/service/useSharedDataRoutes.d.ts.map +1 -0
  192. package/dist/service/useSharedDataRoutes.js +107 -0
  193. package/dist/service/useSharedDataRoutes.js.map +1 -0
  194. package/dist/service/useSharedFileRoutes.d.ts +4 -0
  195. package/dist/service/useSharedFileRoutes.d.ts.map +1 -0
  196. package/dist/service/useSharedFileRoutes.js +121 -0
  197. package/dist/service/useSharedFileRoutes.js.map +1 -0
  198. package/dist/settings.d.ts +5 -3
  199. package/dist/settings.d.ts.map +1 -1
  200. package/dist/settings.js +12 -10
  201. package/dist/settings.js.map +1 -1
  202. package/dist/storage/FsStorageProvider.d.ts +25 -0
  203. package/dist/storage/FsStorageProvider.d.ts.map +1 -0
  204. package/dist/storage/FsStorageProvider.js +103 -0
  205. package/dist/storage/FsStorageProvider.js.map +1 -0
  206. package/dist/storage/StorageProvider.d.ts +31 -0
  207. package/dist/storage/StorageProvider.d.ts.map +1 -0
  208. package/dist/storage/StorageProvider.js +3 -0
  209. package/dist/storage/StorageProvider.js.map +1 -0
  210. package/dist/storage/index.d.ts +3 -0
  211. package/dist/storage/index.d.ts.map +1 -0
  212. package/dist/storage/index.js +6 -0
  213. package/dist/storage/index.js.map +1 -0
  214. package/dist/synthos-cli.d.ts.map +1 -1
  215. package/dist/synthos-cli.js +4 -3
  216. package/dist/synthos-cli.js.map +1 -1
  217. package/dist/themes.d.ts +1 -0
  218. package/dist/themes.d.ts.map +1 -1
  219. package/dist/themes.js +65 -28
  220. package/dist/themes.js.map +1 -1
  221. package/migration-rules/v1-to-v2.md +193 -0
  222. package/migration-rules/v2-to-v3.md +481 -0
  223. package/package.json +11 -10
  224. package/required-pages/builder/page.html +43 -0
  225. package/required-pages/builder/page.json +10 -0
  226. package/required-pages/{pages.html → pages/page.html} +238 -233
  227. package/required-pages/pages/page.json +10 -0
  228. package/required-pages/{settings.html → settings/page.html} +389 -275
  229. package/required-pages/settings/page.json +10 -0
  230. package/required-pages/synthos_apis/page.html +846 -0
  231. package/required-pages/synthos_apis/page.json +10 -0
  232. package/required-pages/{synthos_scripts.html → synthos_scripts/page.html} +13 -11
  233. package/required-pages/synthos_scripts/page.json +10 -0
  234. package/src/agents/index.ts +1 -1
  235. package/src/agents/openclaw/gatewayManager.ts +22 -11
  236. package/src/agents/openclaw/openclawProvider.ts +2 -4
  237. package/src/agents/openclaw/sshTunnelManager.ts +19 -11
  238. package/src/builders/anthropic.ts +283 -0
  239. package/src/builders/fireworksai.ts +59 -0
  240. package/src/builders/index.ts +33 -0
  241. package/src/builders/openai.ts +89 -0
  242. package/src/builders/types.ts +261 -0
  243. package/src/connectors/index.ts +1 -1
  244. package/src/connectors/registry.ts +28 -8
  245. package/src/customizer/Customizer.ts +163 -0
  246. package/src/customizer/index.ts +5 -0
  247. package/src/files.ts +57 -0
  248. package/src/index.ts +3 -1
  249. package/src/init.ts +195 -145
  250. package/src/migrations.ts +30 -10
  251. package/src/models/anthropic.ts +40 -10
  252. package/src/models/fireworksai.ts +9 -2
  253. package/src/models/index.ts +1 -1
  254. package/src/models/openai.ts +26 -6
  255. package/src/models/types.ts +31 -1
  256. package/src/pages.ts +230 -77
  257. package/src/service/createCompletePrompt.ts +3 -2
  258. package/src/service/requiresSettings.ts +4 -3
  259. package/src/service/server.ts +36 -9
  260. package/src/service/transformPage.ts +557 -326
  261. package/src/service/useAgentRoutes.ts +19 -14
  262. package/src/service/useApiRoutes.ts +208 -84
  263. package/src/service/useConnectorRoutes.ts +18 -18
  264. package/src/service/useDataRoutes.ts +13 -10
  265. package/src/service/useFileRoutes.ts +128 -0
  266. package/src/service/usePageRoutes.ts +730 -81
  267. package/src/service/useSharedDataRoutes.ts +109 -0
  268. package/src/service/useSharedFileRoutes.ts +127 -0
  269. package/src/settings.ts +14 -10
  270. package/src/storage/FsStorageProvider.ts +87 -0
  271. package/src/storage/StorageProvider.ts +34 -0
  272. package/src/storage/index.ts +2 -0
  273. package/src/synthos-cli.ts +4 -3
  274. package/src/themes.ts +64 -27
  275. package/static-files/favicon.svg +12 -0
  276. package/static-files/fluentlm-instructions.llmd +868 -0
  277. package/static-files/fluentlm-instructions.md +1595 -0
  278. package/static-files/fluentlm.css +4844 -0
  279. package/static-files/fluentlm.js +3602 -0
  280. package/static-files/fluentlm.min.css +1 -0
  281. package/static-files/fluentlm.min.js +1 -0
  282. package/{page-scripts/helpers-v2.js → static-files/helpers.v3.js} +82 -0
  283. package/static-files/page.v3.js +1290 -0
  284. package/static-files/recommended-frameworks.llmd +81 -0
  285. package/static-files/recommended-frameworks.md +137 -0
  286. package/static-files/retro-game.js +877 -0
  287. package/static-files/shell.css +797 -0
  288. package/static-files/theme-dark.css +169 -0
  289. package/static-files/theme-light.css +169 -0
  290. package/tests/builders.spec.ts +139 -0
  291. package/tests/pages.spec.ts +54 -84
  292. package/tests/transformPage.spec.ts +299 -360
  293. package/default-pages/application.html +0 -40
  294. package/default-pages/application.json +0 -1
  295. package/default-pages/json_tools.json +0 -1
  296. package/default-pages/my_notes.html +0 -33
  297. package/default-pages/neon_asteroids.html +0 -77
  298. package/default-pages/sidebar_page.json +0 -1
  299. package/default-pages/solar_tutorial.json +0 -1
  300. package/default-pages/two-panel_page.json +0 -1
  301. package/dist/service/useGatewayRoutes.d.ts +0 -4
  302. package/dist/service/useGatewayRoutes.d.ts.map +0 -1
  303. package/dist/service/useGatewayRoutes.js +0 -168
  304. package/dist/service/useGatewayRoutes.js.map +0 -1
  305. package/page-scripts/page-v2.js +0 -656
  306. package/required-pages/builder.html +0 -48
  307. package/required-pages/builder.json +0 -1
  308. package/required-pages/pages.json +0 -1
  309. package/required-pages/settings.json +0 -1
  310. package/required-pages/synthos_apis.html +0 -327
  311. package/required-pages/synthos_apis.json +0 -1
  312. package/required-pages/synthos_scripts.json +0 -1
  313. package/src/connectors/airtable/connector.json +0 -27
  314. package/src/connectors/alpha-vantage/connector.json +0 -26
  315. package/src/connectors/brave-search/connector.json +0 -26
  316. package/src/connectors/cloudinary/connector.json +0 -27
  317. package/src/connectors/deepl/connector.json +0 -28
  318. package/src/connectors/elevenlabs/connector.json +0 -30
  319. package/src/connectors/giphy/connector.json +0 -27
  320. package/src/connectors/github/connector.json +0 -29
  321. package/src/connectors/huggingface/connector.json +0 -27
  322. package/src/connectors/imgur/connector.json +0 -29
  323. package/src/connectors/instagram/connector.json +0 -43
  324. package/src/connectors/jira/connector.json +0 -28
  325. package/src/connectors/mapbox/connector.json +0 -26
  326. package/src/connectors/nasa/connector.json +0 -27
  327. package/src/connectors/newsapi/connector.json +0 -27
  328. package/src/connectors/notion/connector.json +0 -28
  329. package/src/connectors/open-exchange-rates/connector.json +0 -27
  330. package/src/connectors/openweathermap/connector.json +0 -26
  331. package/src/connectors/pexels/connector.json +0 -27
  332. package/src/connectors/resend/connector.json +0 -29
  333. package/src/connectors/rss2json/connector.json +0 -27
  334. package/src/connectors/sendgrid/connector.json +0 -27
  335. package/src/connectors/spoonacular/connector.json +0 -28
  336. package/src/connectors/stability-ai/connector.json +0 -27
  337. package/src/connectors/twilio/connector.json +0 -28
  338. package/src/connectors/unsplash/connector.json +0 -27
  339. package/src/connectors/wolfram-alpha/connector.json +0 -26
  340. package/src/connectors/youtube-data/connector.json +0 -30
  341. /package/{dist/connectors → service-connectors}/airtable/connector.json +0 -0
  342. /package/{dist/connectors → service-connectors}/alpha-vantage/connector.json +0 -0
  343. /package/{dist/connectors → service-connectors}/brave-search/connector.json +0 -0
  344. /package/{dist/connectors → service-connectors}/cloudinary/connector.json +0 -0
  345. /package/{dist/connectors → service-connectors}/deepl/connector.json +0 -0
  346. /package/{dist/connectors → service-connectors}/elevenlabs/connector.json +0 -0
  347. /package/{dist/connectors → service-connectors}/giphy/connector.json +0 -0
  348. /package/{dist/connectors → service-connectors}/github/connector.json +0 -0
  349. /package/{dist/connectors → service-connectors}/huggingface/connector.json +0 -0
  350. /package/{dist/connectors → service-connectors}/imgur/connector.json +0 -0
  351. /package/{dist/connectors → service-connectors}/instagram/connector.json +0 -0
  352. /package/{dist/connectors → service-connectors}/jira/connector.json +0 -0
  353. /package/{dist/connectors → service-connectors}/mapbox/connector.json +0 -0
  354. /package/{dist/connectors → service-connectors}/nasa/connector.json +0 -0
  355. /package/{dist/connectors → service-connectors}/newsapi/connector.json +0 -0
  356. /package/{dist/connectors → service-connectors}/notion/connector.json +0 -0
  357. /package/{dist/connectors → service-connectors}/open-exchange-rates/connector.json +0 -0
  358. /package/{dist/connectors → service-connectors}/openweathermap/connector.json +0 -0
  359. /package/{dist/connectors → service-connectors}/pexels/connector.json +0 -0
  360. /package/{dist/connectors → service-connectors}/resend/connector.json +0 -0
  361. /package/{dist/connectors → service-connectors}/rss2json/connector.json +0 -0
  362. /package/{dist/connectors → service-connectors}/sendgrid/connector.json +0 -0
  363. /package/{dist/connectors → service-connectors}/spoonacular/connector.json +0 -0
  364. /package/{dist/connectors → service-connectors}/stability-ai/connector.json +0 -0
  365. /package/{dist/connectors → service-connectors}/twilio/connector.json +0 -0
  366. /package/{dist/connectors → service-connectors}/unsplash/connector.json +0 -0
  367. /package/{dist/connectors → service-connectors}/wolfram-alpha/connector.json +0 -0
  368. /package/{dist/connectors → service-connectors}/youtube-data/connector.json +0 -0
@@ -1,29 +1,24 @@
1
- import { AgentArgs, AgentCompletion, SystemMessage, UserMessage } from "../models";
2
- import { listScripts } from "../scripts";
1
+ import { AgentCompletion } from "../models";
3
2
  import * as cheerio from "cheerio";
4
- import { ThemeInfo } from "../themes";
5
- import { CONNECTOR_REGISTRY, ConnectorsConfig, ConnectorOAuthConfig } from "../connectors";
6
- import { AgentConfig } from "../agents";
3
+ import { Customizer } from "../customizer";
4
+ import { Attachment, Builder, ContextSection } from "../builders/types";
7
5
 
8
6
  // ---------------------------------------------------------------------------
9
7
  // Types
10
8
  // ---------------------------------------------------------------------------
11
9
 
12
- export interface TransformPageArgs extends AgentArgs {
13
- pagesFolder: string;
10
+ export interface TransformPageArgs {
14
11
  pageState: string;
15
12
  message: string;
16
13
  instructions?: string;
17
- /** Provider-specific formatting instructions injected into the prompt. */
18
- modelInstructions?: string;
19
- /** Active theme metadata for theme-aware page generation. */
20
- themeInfo?: ThemeInfo;
21
- /** Page mode. */
22
- mode?: 'unlocked' | 'locked';
23
- /** User's configured connectors (from settings). */
24
- configuredConnectors?: ConnectorsConfig;
25
- /** User's configured A2A agents (from settings). */
26
- configuredAgents?: AgentConfig[];
14
+ builder: Builder;
15
+ additionalSections: ContextSection[];
16
+ /** True when this is the builder page (has chat panel). */
17
+ isBuilder?: boolean;
18
+ /** Product name for branding in prompts (defaults to 'SynthOS'). */
19
+ productName?: string;
20
+ /** Optional image attachments sent alongside the user message. */
21
+ attachments?: Attachment[];
27
22
  }
28
23
 
29
24
  export type ChangeOp =
@@ -31,20 +26,12 @@ export type ChangeOp =
31
26
  | { op: "replace"; nodeId: string; html: string }
32
27
  | { op: "delete"; nodeId: string }
33
28
  | { op: "insert"; parentId: string; position: "prepend" | "append" | "before" | "after"; html: string }
34
- | { op: "style-element"; nodeId: string; style: string };
29
+ | { op: "style-element"; nodeId: string; style: string }
30
+ | { op: "search-replace"; nodeId: string; search: string; replace: string }
31
+ | { op: "search-insert"; nodeId: string; after: string; content: string };
35
32
 
36
33
  export type ChangeList = ChangeOp[];
37
34
 
38
- interface FailedOp {
39
- op: ChangeOp;
40
- reason: string;
41
- }
42
-
43
- interface ApplyResult {
44
- html: string;
45
- failedOps: FailedOp[];
46
- }
47
-
48
35
  // ---------------------------------------------------------------------------
49
36
  // Public entry point
50
37
  // ---------------------------------------------------------------------------
@@ -55,167 +42,179 @@ export interface TransformPageResult {
55
42
  }
56
43
 
57
44
  export async function transformPage(args: TransformPageArgs): Promise<AgentCompletion<TransformPageResult>> {
58
- const { pagesFolder, pageState, message, completePrompt } = args;
45
+ const { message, builder, additionalSections } = args;
46
+
47
+ // 0. Strip the early error-capture script so the LLM never sees it
48
+ const pageState = stripErrorCapture(args.pageState);
59
49
 
60
50
  // 1. Assign data-node-id to every element
61
51
  const { html: annotatedHtml } = assignNodeIds(pageState);
62
52
 
63
53
  try {
64
- // 2. Build prompt
65
- const scripts = await listScripts(pagesFolder);
66
- const serverScripts = `<SERVER_SCRIPTS>\n${scripts || ''}`;
67
- const currentPage = `<CURRENT_PAGE>\n${annotatedHtml}`;
68
-
69
- // Build theme context block
70
- let themeBlock = '<THEME>\n';
71
- if (args.themeInfo) {
72
- const { mode, colors } = args.themeInfo;
73
- const colorList = Object.entries(colors)
74
- .map(([name, value]) => ` --${name}: ${value}`)
75
- .join('\n');
76
- themeBlock += `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 .link-group — Navigation links row (Save, Pages, Reset)\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 - Save/Reset link handlers (#saveLink, #resetLink)\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, .link-group, #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.`;
54
+ // 2. Build CURRENT_PAGE section
55
+ const currentPage: ContextSection = {
56
+ title: '<CURRENT_PAGE>',
57
+ content: annotatedHtml,
58
+ instructions: '',
59
+ };
60
+
61
+ // 3. Determine newBuild: if isBuilder, count .chat-message in annotated HTML
62
+ let newBuild = false;
63
+ if (args.isBuilder) {
64
+ const $ = cheerio.load(annotatedHtml, { decodeEntities: false });
65
+ const messageCount = $('#chatMessages .chat-message').length;
66
+ newBuild = messageCount <= 1;
77
67
  }
78
68
 
79
- // Build configured-connectors block
80
- let connectorsBlock = '';
81
- if (args.configuredConnectors) {
82
- const entries = Object.entries(args.configuredConnectors)
83
- .filter(([, cfg]) => cfg.enabled && cfg.apiKey);
84
- if (entries.length > 0) {
85
- const blocks = entries.map(([id, cfg]) => {
86
- const def = CONNECTOR_REGISTRY.find(d => d.id === id);
87
- if (!def) return `- ${id}`;
88
- let block = `- ${def.name} (id: "${id}", category: ${def.category})\n Base URL: ${def.baseUrl}`;
89
- if (def.hints) {
90
- block += `\n Usage:\n${def.hints.split('\n').map(l => ' ' + l).join('\n')}`;
91
- }
92
- // Append dynamic OAuth context
93
- if (def.authStrategy === 'oauth2') {
94
- const oauthCfg = cfg as ConnectorOAuthConfig;
95
- block += '\n Auth: The proxy attaches the access token automatically. Do NOT pass access_token in body or query params.';
96
- if (oauthCfg.userId) {
97
- block += `\n User ID: ${oauthCfg.userId} use this directly in API paths (e.g. /${oauthCfg.userId}/media).`;
98
- } else {
99
- 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.';
100
- }
101
- }
102
- return block;
103
- });
104
- connectorsBlock = `<CONFIGURED_CONNECTORS>\nThe 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.`;
69
+ // 4. Call builder
70
+ const result = await builder.run(currentPage, additionalSections, message, newBuild, args.attachments);
71
+
72
+ // 5. Switch on result kind
73
+ switch (result.kind) {
74
+ case 'transforms': {
75
+ const applied = applyChangeList(annotatedHtml, result.changes);
76
+ const clean = stripNodeIds(applied);
77
+ const deduped = deduplicateInlineScripts(clean);
78
+ const safe = ensureScriptsBeforeBodyClose(deduped);
79
+ return { completed: true, value: { html: safe, changeCount: result.changes.length } };
80
+ }
81
+ case 'reply': {
82
+ const productName = args.productName ?? 'SynthOS';
83
+ const withReply = appendChatReply(annotatedHtml, message, result.text, productName);
84
+ const clean = stripNodeIds(withReply);
85
+ const deduped = deduplicateInlineScripts(clean);
86
+ const safe = ensureScriptsBeforeBodyClose(deduped);
87
+ return { completed: true, value: { html: safe, changeCount: -1 } };
88
+ }
89
+ case 'error': {
90
+ const productName = args.productName ?? 'SynthOS';
91
+ const errorHtml = appendChatError(annotatedHtml, message, result.error.message, productName);
92
+ const clean = stripNodeIds(errorHtml);
93
+ return { completed: true, value: { html: clean, changeCount: -1 } };
105
94
  }
106
95
  }
96
+ } catch (err: unknown) {
97
+ // On any error: append error message to chat
98
+ const productName = args.productName ?? 'SynthOS';
99
+ const errorMessage = err instanceof Error ? err.message : String(err);
100
+ const errorHtml = appendChatError(annotatedHtml, message, errorMessage, productName);
101
+ const clean = stripNodeIds(errorHtml);
102
+ return { completed: true, value: { html: clean, changeCount: -1 } };
103
+ }
104
+ }
107
105
 
108
- // Build configured-agents block (only enabled agents)
109
- let agentsBlock = '';
110
- const enabledAgents = (args.configuredAgents ?? []).filter(a => a.enabled);
111
- if (enabledAgents.length > 0) {
112
- const agentBlocks = enabledAgents.map(a => {
113
- let block = `- ${a.name} (id: "${a.id}", provider: ${a.provider})`;
114
- block += `\n Description: ${a.description}`;
115
- if (a.capabilities?.streaming) {
116
- block += `\n Supports streaming: yes`;
117
- }
118
- if (a.skills && a.skills.length > 0) {
119
- const skillList = a.skills.map(s => ` - ${s.name}: ${s.description}`).join('\n');
120
- block += `\n Skills:\n${skillList}`;
121
- }
122
- return block;
123
- });
124
- agentsBlock = `<CONFIGURED_AGENTS>\nThe user has configured these agents:\n\n${agentBlocks.join('\n\n')}\n\n${AGENT_API_REFERENCE}`;
125
- }
106
+ // ---------------------------------------------------------------------------
107
+ // Chat reply helper
108
+ // ---------------------------------------------------------------------------
126
109
 
127
- const systemMessage = [currentPage, serverAPIs, serverScripts, connectorsBlock, agentsBlock, themeBlock, messageFormat].filter(s => s).join('\n\n');
128
- const system: SystemMessage = {
129
- role: 'system',
130
- content: systemMessage
131
- };
110
+ /**
111
+ * Append a user message and a reply to #chatMessages using cheerio.
112
+ */
113
+ function appendChatReply(annotatedHtml: string, userMessage: string, replyText: string, productName: string): string {
114
+ const $ = cheerio.load(annotatedHtml, { decodeEntities: false });
115
+ const chatMessages = $('#chatMessages');
116
+ if (chatMessages.length > 0) {
117
+ chatMessages.append(`<div class="chat-message"><p><strong>User:</strong> ${escapeHtml(userMessage)}</p></div>`);
118
+ const replyHtml = simpleMarkdown(replyText);
119
+ chatMessages.append(`<div class="chat-message"><p><strong>${escapeHtml(productName)}:</strong> ${replyHtml}</p></div>`);
120
+ }
121
+ return $.html();
122
+ }
132
123
 
133
- const userInstr = args.instructions || '';
134
- const modelInstr = args.modelInstructions || '';
135
- const instructions = [userInstr, modelInstr, transformInstr].filter(s => s.trim() !== '').join('\n');
136
- const prompt: UserMessage = {
137
- role: 'user',
138
- content: `<USER_MESSAGE>\n${message}\n\n<INSTRUCTIONS>\n${instructions}`
139
- };
124
+ function escapeHtml(text: string): string {
125
+ return text
126
+ .replace(/&/g, '&amp;')
127
+ .replace(/</g, '&lt;')
128
+ .replace(/>/g, '&gt;')
129
+ .replace(/"/g, '&quot;');
130
+ }
131
+
132
+ /**
133
+ * Lightweight markdown-to-HTML converter for chat reply text.
134
+ * Handles: code blocks, inline code, bold, italic, links, unordered/ordered lists, paragraphs.
135
+ */
136
+ export function simpleMarkdown(text: string): string {
137
+ // Extract fenced code blocks first to protect their contents
138
+ const codeBlocks: string[] = [];
139
+ let processed = text.replace(/```(\w*)\n([\s\S]*?)```/g, (_match, lang, code) => {
140
+ const idx = codeBlocks.length;
141
+ const escaped = escapeHtml(code.replace(/\n$/, ''));
142
+ const langAttr = lang ? ` class="language-${escapeHtml(lang)}"` : '';
143
+ codeBlocks.push(`<pre><code${langAttr}>${escaped}</code></pre>`);
144
+ return `\x00CODEBLOCK${idx}\x00`;
145
+ });
140
146
 
141
- // 3. Call model
142
- const result = await completePrompt({ prompt, system });
143
- if (!result.completed) {
144
- return { completed: false, error: result.error };
147
+ // Split into paragraphs by blank lines
148
+ const blocks = processed.split(/\n{2,}/);
149
+ const htmlBlocks: string[] = [];
150
+
151
+ for (const block of blocks) {
152
+ const trimmed = block.trim();
153
+ if (!trimmed) continue;
154
+
155
+ // Code block placeholder
156
+ if (/^\x00CODEBLOCK\d+\x00$/.test(trimmed)) {
157
+ htmlBlocks.push(trimmed);
158
+ continue;
145
159
  }
146
160
 
147
- // 4. Parse JSON change list from response
148
- const changes = parseChangeList(result.value);
149
-
150
- // 5. Apply changes (first pass — with failure reporting)
151
- const firstPass = applyChangeListWithReport(annotatedHtml, changes);
152
- let finalHtml = firstPass.html;
153
- let successCount = changes.length - firstPass.failedOps.length;
154
-
155
- // 6. Repair pass — if any ops failed, make one follow-up LLM call
156
- if (firstPass.failedOps.length > 0) {
157
- console.warn(`transformPage: ${firstPass.failedOps.length} op(s) failed — attempting repair pass`);
158
- try {
159
- // Re-assign fresh node IDs on the partially-updated HTML
160
- const { html: reAnnotatedHtml } = assignNodeIds(stripNodeIds(firstPass.html));
161
-
162
- // Build compact repair prompt
163
- const failedSummary = firstPass.failedOps
164
- .map((f, i) => `${i + 1}. op="${f.op.op}" — ${f.reason}\n original: ${JSON.stringify(f.op)}`)
165
- .join('\n');
166
-
167
- const repairSystem: SystemMessage = {
168
- role: 'system',
169
- content: `<CURRENT_PAGE>\n${reAnnotatedHtml}\n\n<FAILED_OPERATIONS>\n${failedSummary}`
170
- };
171
-
172
- const repairPrompt: UserMessage = {
173
- role: 'user',
174
- content: repairUSER_MESSAGE
175
- };
176
-
177
- const repairResult = await completePrompt({ prompt: repairPrompt, system: repairSystem });
178
-
179
- if (repairResult.completed) {
180
- const repairChanges = parseChangeList(repairResult.value);
181
- if (repairChanges.length > 0) {
182
- const repairPass = applyChangeListWithReport(reAnnotatedHtml, repairChanges);
183
- const repairSuccessCount = repairChanges.length - repairPass.failedOps.length;
184
- if (repairPass.failedOps.length > 0) {
185
- console.warn(`transformPage: repair pass had ${repairPass.failedOps.length} remaining failure(s) — keeping partial result`);
186
- }
187
- finalHtml = repairPass.html;
188
- successCount += repairSuccessCount;
189
- console.log(`transformPage: repair pass applied ${repairSuccessCount} fix(es)`);
190
- } else {
191
- console.log('transformPage: repair pass returned no changes (model deemed repairs unnecessary)');
192
- }
193
- } else {
194
- console.warn('transformPage: repair LLM call failed — keeping partial result from first pass');
195
- }
196
- } catch (repairErr: unknown) {
197
- const msg = repairErr instanceof Error ? repairErr.message : String(repairErr);
198
- console.warn(`transformPage: repair pass error — ${msg} — keeping partial result from first pass`);
199
- }
161
+ // Unordered list (lines starting with - or *)
162
+ if (/^[\-\*] /m.test(trimmed) && trimmed.split('\n').every(l => /^[\-\*] /.test(l.trim()) || l.trim() === '')) {
163
+ const items = trimmed.split('\n')
164
+ .map(l => l.trim())
165
+ .filter(l => l)
166
+ .map(l => `<li>${inlineMarkdown(l.replace(/^[\-\*] /, ''))}</li>`)
167
+ .join('');
168
+ htmlBlocks.push(`<ul>${items}</ul>`);
169
+ continue;
200
170
  }
201
171
 
202
- // 7. Strip data-node-id attributes
203
- const cleanHtml = stripNodeIds(finalHtml);
172
+ // Ordered list (lines starting with 1. 2. etc.)
173
+ if (/^\d+\. /m.test(trimmed) && trimmed.split('\n').every(l => /^\d+\. /.test(l.trim()) || l.trim() === '')) {
174
+ const items = trimmed.split('\n')
175
+ .map(l => l.trim())
176
+ .filter(l => l)
177
+ .map(l => `<li>${inlineMarkdown(l.replace(/^\d+\. /, ''))}</li>`)
178
+ .join('');
179
+ htmlBlocks.push(`<ol>${items}</ol>`);
180
+ continue;
181
+ }
204
182
 
205
- // 8. Remove duplicate inline scripts (LLM may insert instead of update)
206
- const dedupedHtml = deduplicateInlineScripts(cleanHtml);
183
+ // Regular paragraph
184
+ htmlBlocks.push(`<p>${inlineMarkdown(trimmed.replace(/\n/g, '<br>'))}</p>`);
185
+ }
207
186
 
208
- // 9. Ensure page-helpers and page-script are last in <body>
209
- const safeHtml = ensureScriptsBeforeBodyClose(dedupedHtml);
187
+ // Restore code blocks
188
+ let result = htmlBlocks.join('');
189
+ result = result.replace(/\x00CODEBLOCK(\d+)\x00/g, (_m, idx) => codeBlocks[parseInt(idx)]);
190
+ return result;
191
+ }
210
192
 
211
- return { completed: true, value: { html: safeHtml, changeCount: successCount } };
212
- } catch (err: unknown) {
213
- // On any error: return original page with error block injected
214
- const cleanOriginal = stripNodeIds(annotatedHtml);
215
- const errorMessage = err instanceof Error ? err.message : String(err);
216
- const errorHtml = injectError(cleanOriginal, 'Something went wrong try again', errorMessage);
217
- return { completed: true, value: { html: errorHtml, changeCount: 0 } };
218
- }
193
+ /** Apply inline markdown formatting: bold, italic, inline code, links. */
194
+ function inlineMarkdown(text: string): string {
195
+ // Inline code (protect from further processing)
196
+ const codes: string[] = [];
197
+ let result = text.replace(/`([^`]+)`/g, (_m, code) => {
198
+ const idx = codes.length;
199
+ codes.push(`<code>${escapeHtml(code)}</code>`);
200
+ return `\x00CODE${idx}\x00`;
201
+ });
202
+
203
+ // Bold (**text** or __text__)
204
+ result = result.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
205
+ result = result.replace(/__(.+?)__/g, '<strong>$1</strong>');
206
+
207
+ // Italic (*text* or _text_)
208
+ result = result.replace(/\*(.+?)\*/g, '<em>$1</em>');
209
+ result = result.replace(/(?<!\w)_(.+?)_(?!\w)/g, '<em>$1</em>');
210
+
211
+ // Links [text](url)
212
+ result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
213
+
214
+ // Restore inline code
215
+ result = result.replace(/\x00CODE(\d+)\x00/g, (_m, idx) => codes[parseInt(idx)]);
216
+
217
+ return result;
219
218
  }
220
219
 
221
220
  // ---------------------------------------------------------------------------
@@ -247,6 +246,69 @@ export function stripNodeIds(html: string): string {
247
246
  return $.html();
248
247
  }
249
248
 
249
+ /**
250
+ * Remove the early error-capture script injected into <head> so the LLM
251
+ * never sees it during page transformation.
252
+ */
253
+ function stripErrorCapture(html: string): string {
254
+ const id = 'synthos-error-capture';
255
+ if (!html.includes(id)) return html;
256
+ const $ = cheerio.load(html, { decodeEntities: false });
257
+ $(`#${id}`).remove();
258
+ return $.html();
259
+ }
260
+
261
+ /**
262
+ * Find `needle` in `haystack` using whitespace-normalized comparison.
263
+ * Collapses runs of whitespace (spaces, tabs, newlines) into a single space
264
+ * for comparison purposes but returns the position in the original string.
265
+ * Returns -1 if no match found.
266
+ */
267
+ export function normalizedIndexOf(haystack: string, needle: string): { start: number; end: number } | null {
268
+ // Build a mapping from normalized-string positions to original-string positions
269
+ const normChars: string[] = [];
270
+ const origPositions: number[] = []; // origPositions[i] = original index of normChars[i]
271
+ let inWhitespace = false;
272
+
273
+ for (let i = 0; i < haystack.length; i++) {
274
+ const ch = haystack[i];
275
+ if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') {
276
+ if (!inWhitespace) {
277
+ normChars.push(' ');
278
+ origPositions.push(i);
279
+ inWhitespace = true;
280
+ }
281
+ } else {
282
+ normChars.push(ch);
283
+ origPositions.push(i);
284
+ inWhitespace = false;
285
+ }
286
+ }
287
+
288
+ const normHaystack = normChars.join('');
289
+
290
+ // Normalize the needle the same way
291
+ const normNeedle = needle.replace(/\s+/g, ' ');
292
+
293
+ const idx = normHaystack.indexOf(normNeedle);
294
+ if (idx === -1) return null;
295
+
296
+ const start = origPositions[idx];
297
+ // The end position: find the original position of the last matched char, then go one past
298
+ const lastNormIdx = idx + normNeedle.length - 1;
299
+ const lastOrigPos = origPositions[lastNormIdx];
300
+ // Advance past any trailing whitespace that was collapsed in the original
301
+ let end = lastOrigPos + 1;
302
+ if (haystack[lastOrigPos] === ' ' || haystack[lastOrigPos] === '\t' || haystack[lastOrigPos] === '\n' || haystack[lastOrigPos] === '\r') {
303
+ // The last matched normalized char was a whitespace collapse — extend to include all original whitespace
304
+ while (end < haystack.length && (haystack[end] === ' ' || haystack[end] === '\t' || haystack[end] === '\n' || haystack[end] === '\r')) {
305
+ end++;
306
+ }
307
+ }
308
+
309
+ return { start, end };
310
+ }
311
+
250
312
  /**
251
313
  * Remove duplicate inline `<script>` blocks using a two-pass approach.
252
314
  *
@@ -385,6 +447,29 @@ function isElementLocked(el: cheerio.Cheerio, $: cheerio.Root): boolean {
385
447
  return el.attr('data-locked') !== undefined;
386
448
  }
387
449
 
450
+ /**
451
+ * If the target element is a <script> or <style> and the html is wrapped in a
452
+ * redundant matching tag, strip the outer tag to avoid nesting (e.g.
453
+ * `<script>` inside `<script>`). Returns the inner content when unwrapped.
454
+ */
455
+ function unwrapRedundantTag(tagName: string | undefined, html: string): string {
456
+ if (tagName !== 'script' && tagName !== 'style') return html;
457
+ const re = new RegExp(`^\\s*<${tagName}[^>]*>([\\s\\S]*)</${tagName}>\\s*$`, 'i');
458
+ const match = html.match(re);
459
+ return match ? match[1] : html;
460
+ }
461
+
462
+ /**
463
+ * Strip any `<script>` or `<style>` tags from text that will be injected
464
+ * inside an existing script/style block (search-replace, search-insert).
465
+ */
466
+ function stripNestedBlockTags(tagName: string | undefined, text: string): string {
467
+ if (tagName !== 'script' && tagName !== 'style') return text;
468
+ return text
469
+ .replace(new RegExp(`<${tagName}[^>]*>`, 'gi'), '')
470
+ .replace(new RegExp(`</${tagName}>`, 'gi'), '');
471
+ }
472
+
388
473
  /**
389
474
  * Apply a list of CRUD operations to annotated HTML (elements must have `data-node-id`).
390
475
  */
@@ -399,7 +484,8 @@ export function applyChangeList(html: string, changes: ChangeList): string {
399
484
  console.warn(`applyChangeList: skipping update — node ${change.nodeId} not found (already removed?)`);
400
485
  break;
401
486
  }
402
- el.html(change.html);
487
+ const tag = el.prop('tagName')?.toLowerCase();
488
+ el.html(unwrapRedundantTag(tag, change.html));
403
489
  break;
404
490
  }
405
491
  case 'replace': {
@@ -412,7 +498,16 @@ export function applyChangeList(html: string, changes: ChangeList): string {
412
498
  console.warn(`applyChangeList: skipping replace — node ${change.nodeId} is data-locked`);
413
499
  break;
414
500
  }
415
- el.replaceWith(change.html);
501
+ // If the target is a <script> or <style> and the html doesn't
502
+ // include the outer tag (or wraps it redundantly), treat as an
503
+ // update (set inner content) instead of replacing the element.
504
+ const tagName = el.prop('tagName')?.toLowerCase();
505
+ const cleaned = unwrapRedundantTag(tagName, change.html);
506
+ if ((tagName === 'script' || tagName === 'style') && !cleaned.trimStart().startsWith('<')) {
507
+ el.html(cleaned);
508
+ } else {
509
+ el.replaceWith(change.html);
510
+ }
416
511
  break;
417
512
  }
418
513
  case 'delete': {
@@ -430,13 +525,20 @@ export function applyChangeList(html: string, changes: ChangeList): string {
430
525
  }
431
526
  case 'insert': {
432
527
  const parent = $(`[data-node-id="${change.parentId}"]`);
433
- if (parent.length === 0) throw new Error(`insert: parent node ${change.parentId} not found`);
528
+ if (parent.length === 0) {
529
+ console.warn(`applyChangeList: skipping insert — parent node ${change.parentId} not found`);
530
+ break;
531
+ }
532
+ // Unwrap redundant tags when inserting into script/style
533
+ const parentTag = parent.prop('tagName')?.toLowerCase();
534
+ const insertHtml = unwrapRedundantTag(parentTag, change.html);
434
535
  switch (change.position) {
435
- case 'prepend': parent.prepend(change.html); break;
436
- case 'append': parent.append(change.html); break;
437
- case 'before': parent.before(change.html); break;
438
- case 'after': parent.after(change.html); break;
439
- default: throw new Error(`insert: unknown position "${(change as any).position}"`);
536
+ case 'prepend': parent.prepend(insertHtml); break;
537
+ case 'append': parent.append(insertHtml); break;
538
+ case 'before': parent.before(insertHtml); break;
539
+ case 'after': parent.after(insertHtml); break;
540
+ default:
541
+ console.warn(`applyChangeList: skipping insert — unknown position "${(change as any).position}"`);
440
542
  }
441
543
  break;
442
544
  }
@@ -453,101 +555,49 @@ export function applyChangeList(html: string, changes: ChangeList): string {
453
555
  el.attr('style', change.style);
454
556
  break;
455
557
  }
456
- default:
457
- throw new Error(`Unknown change op: "${(change as any).op}"`);
458
- }
459
- }
460
-
461
- return $.html();
462
- }
463
-
464
- /**
465
- * Apply a list of CRUD operations and report any ops that failed due to
466
- * missing nodes (instead of throwing). Unknown op types still throw.
467
- */
468
- function applyChangeListWithReport(html: string, changes: ChangeList): ApplyResult {
469
- const $ = cheerio.load(html, { decodeEntities: false });
470
- const failedOps: FailedOp[] = [];
471
-
472
- for (const change of changes) {
473
- switch (change.op) {
474
- case 'update': {
475
- const el = $(`[data-node-id="${change.nodeId}"]`);
476
- if (el.length === 0) {
477
- const reason = `node ${change.nodeId} not found (already removed?)`;
478
- console.warn(`applyChangeListWithReport: skipping update — ${reason}`);
479
- failedOps.push({ op: change, reason });
480
- break;
481
- }
482
- el.html(change.html);
483
- break;
484
- }
485
- case 'replace': {
486
- const el = $(`[data-node-id="${change.nodeId}"]`);
487
- if (el.length === 0) {
488
- const reason = `node ${change.nodeId} not found (already removed?)`;
489
- console.warn(`applyChangeListWithReport: skipping replace — ${reason}`);
490
- failedOps.push({ op: change, reason });
491
- break;
492
- }
493
- if (isElementLocked(el, $)) {
494
- const reason = `node ${change.nodeId} is data-locked`;
495
- console.warn(`applyChangeListWithReport: skipping replace — ${reason}`);
496
- failedOps.push({ op: change, reason });
497
- break;
498
- }
499
- el.replaceWith(change.html);
500
- break;
501
- }
502
- case 'delete': {
558
+ case 'search-replace': {
503
559
  const el = $(`[data-node-id="${change.nodeId}"]`);
504
560
  if (el.length === 0) {
505
- const reason = `node ${change.nodeId} not found (already removed?)`;
506
- console.warn(`applyChangeListWithReport: skipping delete — ${reason}`);
507
- failedOps.push({ op: change, reason });
508
- break;
509
- }
510
- if (isElementLocked(el, $)) {
511
- const reason = `node ${change.nodeId} is data-locked`;
512
- console.warn(`applyChangeListWithReport: skipping delete — ${reason}`);
513
- failedOps.push({ op: change, reason });
514
- break;
515
- }
516
- el.remove();
517
- break;
518
- }
519
- case 'insert': {
520
- const parent = $(`[data-node-id="${change.parentId}"]`);
521
- if (parent.length === 0) {
522
- const reason = `parent node ${change.parentId} not found`;
523
- console.warn(`applyChangeListWithReport: skipping insert — ${reason}`);
524
- failedOps.push({ op: change, reason });
561
+ console.warn(`applyChangeList: skipping search-replace node ${change.nodeId} not found (already removed?)`);
525
562
  break;
526
563
  }
527
- switch (change.position) {
528
- case 'prepend': parent.prepend(change.html); break;
529
- case 'append': parent.append(change.html); break;
530
- case 'before': parent.before(change.html); break;
531
- case 'after': parent.after(change.html); break;
532
- default: throw new Error(`insert: unknown position "${(change as any).position}"`);
564
+ const srTag = el.prop('tagName')?.toLowerCase();
565
+ const replaceText = stripNestedBlockTags(srTag, change.replace);
566
+ const content = el.html() ?? '';
567
+ const exactIdx = content.indexOf(change.search);
568
+ if (exactIdx !== -1) {
569
+ el.html(content.slice(0, exactIdx) + replaceText + content.slice(exactIdx + change.search.length));
570
+ } else {
571
+ const norm = normalizedIndexOf(content, change.search);
572
+ if (norm) {
573
+ el.html(content.slice(0, norm.start) + replaceText + content.slice(norm.end));
574
+ } else {
575
+ console.warn(`applyChangeList: skipping search-replace — search text not found in node ${change.nodeId}`);
576
+ }
533
577
  }
534
578
  break;
535
579
  }
536
- case 'style-element': {
580
+ case 'search-insert': {
537
581
  const el = $(`[data-node-id="${change.nodeId}"]`);
538
582
  if (el.length === 0) {
539
- const reason = `node ${change.nodeId} not found (already removed?)`;
540
- console.warn(`applyChangeListWithReport: skipping style-element — ${reason}`);
541
- failedOps.push({ op: change, reason });
583
+ console.warn(`applyChangeList: skipping search-insert node ${change.nodeId} not found (already removed?)`);
542
584
  break;
543
585
  }
544
- if (isElementLocked(el, $)) {
545
- const reason = `node ${change.nodeId} is data-locked`;
546
- console.warn(`applyChangeListWithReport: skipping style-element — ${reason}`);
547
- failedOps.push({ op: change, reason });
548
- break;
586
+ const siTag = el.prop('tagName')?.toLowerCase();
587
+ const insertContent = stripNestedBlockTags(siTag, change.content);
588
+ const content = el.html() ?? '';
589
+ const exactIdx = content.indexOf(change.after);
590
+ if (exactIdx !== -1) {
591
+ const insertPos = exactIdx + change.after.length;
592
+ el.html(content.slice(0, insertPos) + insertContent + content.slice(insertPos));
593
+ } else {
594
+ const norm = normalizedIndexOf(content, change.after);
595
+ if (norm) {
596
+ el.html(content.slice(0, norm.end) + insertContent + content.slice(norm.end));
597
+ } else {
598
+ console.warn(`applyChangeList: skipping search-insert — after text not found in node ${change.nodeId}`);
599
+ }
549
600
  }
550
- el.attr('style', change.style);
551
601
  break;
552
602
  }
553
603
  default:
@@ -555,27 +605,24 @@ function applyChangeListWithReport(html: string, changes: ChangeList): ApplyResu
555
605
  }
556
606
  }
557
607
 
558
- return { html: $.html(), failedOps };
608
+ return $.html();
559
609
  }
560
610
 
561
611
  /**
562
- * Inject an error script block into the page HTML.
612
+ * Append a user message and a styled error message to #chatMessages.
563
613
  */
564
- export function injectError(html: string, message: string, details: string): string {
614
+ function appendChatError(html: string, userMessage: string, errorDetails: string, productName: string): string {
565
615
  const $ = cheerio.load(html, { decodeEntities: false });
566
- const errorPayload = JSON.stringify({ message, details });
567
- const scriptTag = `<script id="error" type="application/json">${errorPayload}</script>`;
568
-
569
- // Remove any existing error block first
570
- $('script#error').remove();
571
-
572
- // Inject before closing </body> or at end
573
- if ($('body').length > 0) {
574
- $('body').append(scriptTag);
575
- } else {
576
- return html + scriptTag;
616
+ const chatMessages = $('#chatMessages');
617
+ if (chatMessages.length > 0) {
618
+ chatMessages.append(
619
+ `<div class="chat-message"><p><strong>User:</strong> ${escapeHtml(userMessage)}</p></div>`
620
+ );
621
+ chatMessages.append(
622
+ `<div class="chat-message chat-message-error"><p><strong>${escapeHtml(productName)}:</strong> Something went wrong \u2014 please try again.</p>`
623
+ + `<p class="chat-error-details">${escapeHtml(errorDetails)}</p></div>`
624
+ );
577
625
  }
578
-
579
626
  return $.html();
580
627
  }
581
628
 
@@ -610,19 +657,20 @@ export function parseChangeList(response: string): ChangeList {
610
657
  // Prompt constants
611
658
  // ---------------------------------------------------------------------------
612
659
 
613
- const messageFormat =
614
- `<MESSAGE_FORMAT>
615
- <div class="chat-message"><p><strong>{SynthOS: | User:}</strong> {message contents}</p></div>
616
- `
660
+ export function getMessageFormat(productName: string): string {
661
+ return `<MESSAGE_FORMAT>
662
+ <div class="chat-message"><p><strong>{${productName}: | User:}</strong> {message contents}</p></div>
663
+ `;
664
+ }
617
665
 
618
- const transformInstr =
619
- `Apply the users <USER_MESSAGE> to the .viewerPanel of the <CURRENT_PAGE> by generating a list of changes in JSON format.
666
+ export function getTransformInstr(productName: string): string {
667
+ return `Apply the users <USER_MESSAGE> to the .viewerPanel of the <CURRENT_PAGE> by generating a list of changes in JSON format.
620
668
  Never remove any element that has a data-locked attribute. You may modfiy the inner text of a data-locked element or any of its unlocked child elements.
621
669
 
622
- If the <USER_MESSAGE> involves clearning the chat history, remove all .chat-message elements inside the #chatMessages container except for the first SynthOS: message. You may modify that message contents if requested.
623
- If there's no <USER_MESSAGE> add a SynthOS: message to the chat with aasking the user what they would like to do.
624
- If there is a <USER_MESSAGE> but the intent is unclear, add a User: message with the <USER_MESSAGE> to the chat and add a SynthOS: message asking the user for clarification on their intent.
625
- If there is a <USER_MESSAGE> with clear intent, add a User: message with the <USER_MESSAGE> to the chat and add a SynthOS: message explaining your change or answering their question.
670
+ If the <USER_MESSAGE> involves clearning the chat history, remove all .chat-message elements inside the #chatMessages container except for the first ${productName}: message. You may modify that message contents if requested.
671
+ If there's no <USER_MESSAGE> add a ${productName}: message to the chat with aasking the user what they would like to do.
672
+ If there is a <USER_MESSAGE> but the intent is unclear, add a User: message with the <USER_MESSAGE> to the chat and add a ${productName}: message asking the user for clarification on their intent.
673
+ If there is a <USER_MESSAGE> with clear intent, add a User: message with the <USER_MESSAGE> to the chat and add a ${productName}: message explaining your change or answering their question.
626
674
  If a <USER_MESSAGE> is overly long, summarize the User: message.
627
675
 
628
676
  When updating the .viewerPanel you may alse add/remove/update style blocks to the header unless they're data-locked. Use inline styles if you need to modify the .viewerPanel itself.
@@ -646,31 +694,11 @@ Do not add duplicate script blocks with the same logic! Consolidate inline scrip
646
694
  Each element in the CURRENT_PAGE has a data-node-id attribute. Don't use the id attribute for targeting nodes (reserve it for scripts and styles) — use data-node-id.
647
695
  If you're trying to assign an id to script or style block, use "replace" not "update".
648
696
  Your first operation should always be an update to your thoughts block, where you can reason through the user's request and plan your changes before applying them to the page.
649
- Return a JSON array of change operations to apply to the page. Do NOT return the full HTML page.
650
-
651
- Each operation must be one of:
652
- { "op": "update", "nodeId": "<data-node-id>", "html": "<new innerHTML>" }
653
- — replaces the innerHTML of the target element
654
697
 
655
- { "op": "replace", "nodeId": "<data-node-id>", "html": "<new outerHTML>" }
656
- — replaces the entire element (outerHTML) with new markup
657
-
658
- { "op": "delete", "nodeId": "<data-node-id>" }
659
- — removes the element from the page
660
-
661
- { "op": "insert", "parentId": "<data-node-id>", "position": "prepend"|"append"|"before"|"after", "html": "<new element HTML>" }
662
- — inserts new HTML relative to the parent element
663
-
664
- { "op": "style-element", "nodeId": "<data-node-id>", "style": "<css style string>" }
665
- — sets the style attribute of the target element (must be unlocked)
666
-
667
- Return ONLY the JSON array. Example:
668
- [
669
- { "op": "update", "nodeId": "5", "html": "<p>Hello world</p>" },
670
- { "op": "insert", "parentId": "3", "position": "append", "html": "<div class=\\"msg\\">New message</div>" }
671
- ]`;
698
+ CRITICAL FluentLM components: If a <FLUENTLM_COMPONENTS> section is present, you MUST use those components for all standard UI elements (buttons, inputs, selects, dialogs, tabs, cards, toggles, etc.). Never create custom CSS classes for UI controls that have a FluentLM equivalent. Refer to <FLUENTLM_COMPONENTS> for the exact class names and markup patterns.`;
699
+ }
672
700
 
673
- const AGENT_API_REFERENCE =
701
+ export const AGENT_API_REFERENCE =
674
702
  `## Agent API
675
703
 
676
704
  Check availability first (required):
@@ -704,7 +732,221 @@ Stream with attachments:
704
732
  IMPORTANT: Always check synthos.agents.list({ enabled: true }) before calling an agent.
705
733
  If no agents are configured, show the user a link to Settings > Agents (/settings?tab=agents).`;
706
734
 
707
- const serverAPIs =
735
+ // ---------------------------------------------------------------------------
736
+ // Route hint blocks — keyed by feature group so they can be filtered
737
+ // ---------------------------------------------------------------------------
738
+
739
+ export const DEFAULT_ROUTE_HINTS = new Map<string, string>([
740
+ ['data', `GET /api/data/:page/:table
741
+ description: Retrieve all rows from a page-scoped table (tables are stored per-page). Supports pagination via query params.
742
+ query params: limit (number, optional) — max rows to return; offset (number, optional, default 0) — rows to skip
743
+ response (without limit): Array of JSON rows [{ id: string, ... }]
744
+ response (with limit): { items: [{ id: string, ... }], total: number, offset: number, limit: number, hasMore: boolean }
745
+
746
+ GET /api/data/:page/:table/:id
747
+ description: Retrieve a single row from a page-scoped table
748
+ response: JSON row { id: string, ... }
749
+
750
+ POST /api/data/:page/:table
751
+ description: Replaces or adds a single row to a page-scoped table and returns the row
752
+ request: JSON row { id?: string, ... }
753
+ response: { id: string, ... }
754
+
755
+ DELETE /api/data/:page/:table/:id
756
+ description: Delete a single row from a page-scoped table
757
+ response: { success: true }
758
+
759
+ synthos.data.list(table, opts?) — GET /api/data/:page/:table (auto-scoped to current page; opts: { limit?, offset? } — when limit is set, returns { items, total, offset, limit, hasMore })
760
+ synthos.data.get(table, id) — GET /api/data/:page/:table/:id (auto-scoped to current page)
761
+ synthos.data.save(table, row) — POST /api/data/:page/:table (auto-scoped to current page)
762
+ synthos.data.remove(table, id) — DELETE /api/data/:page/:table/:id (auto-scoped to current page)`],
763
+
764
+ ['api', `POST /api/generate/image
765
+ description: Generate an image based on a prompt
766
+ request: { prompt: string, shape: 'square' | 'portrait' | 'landscape', style: 'vivid' | 'natural' }
767
+ response: { url: string }
768
+
769
+ POST /api/generate/completion
770
+ description: Generates a text completion based on a prompt
771
+ request: { prompt: string, temperature?: number }
772
+ response: { answer: string, explanation: string }
773
+
774
+ synthos.generate.image({ prompt, shape, style }) — POST /api/generate/image
775
+ synthos.generate.completion({ prompt, temperature? }) — POST /api/generate/completion`],
776
+
777
+ ['pages', `GET /api/pages
778
+ description: Retrieve a list of all pages with metadata
779
+ response: Array of { name: string, title: string, categories: string[], pinned: boolean, createdDate: string, lastModified: string, pageVersion: number, mode: 'unlocked' | 'locked' }
780
+
781
+ GET /api/pages/:name
782
+ description: Retrieve metadata for a single page
783
+ response: { title: string, categories: string[], pinned: boolean, createdDate: string, lastModified: string, pageVersion: number, mode: 'unlocked' | 'locked' }
784
+
785
+ POST /api/pages/:name
786
+ description: Update page metadata (merge semantics — send only fields to change; lastModified is auto-set)
787
+ request: { title?: string, categories?: string[], pinned?: boolean, mode?: 'unlocked' | 'locked' }
788
+ response: Full metadata object
789
+
790
+ DELETE /api/pages/:name
791
+ description: Delete a user page (cannot delete required/system pages)
792
+ response: { deleted: true }
793
+
794
+ POST /api/pages/:name/ask
795
+ description: Ask a question about a page with full HTML context
796
+ request: { question: string }
797
+ response: { answer: string }
798
+
799
+ synthos.pages.list() — GET /api/pages
800
+ synthos.pages.get(name) — GET /api/pages/:name
801
+ synthos.pages.update(name, metadata) — POST /api/pages/:name
802
+ synthos.pages.remove(name) — DELETE /api/pages/:name
803
+ synthos.pages.ask(name, question) — POST /api/pages/:name/ask`],
804
+
805
+ ['scripts', `POST /api/scripts/:id
806
+ description: Execute a script with the passed in variables
807
+ request: { [key: string]: string }
808
+ response: string
809
+
810
+ synthos.scripts.run(id, variables) — POST /api/scripts/:id`],
811
+
812
+ ['search', `POST /api/search/web
813
+ description: Search the web using Brave Search (must be enabled in Settings > Connectors)
814
+ request: { query: string, count?: number, country?: string, freshness?: string }
815
+ response: { results: [{ title: string, url: string, description: string }] }
816
+
817
+ synthos.search.web(query, opts?) — POST /api/search/web (opts: { count?, country?, freshness? })`],
818
+
819
+ ['agents', `GET /api/agents
820
+ description: List configured agents (A2A and OpenClaw). Supports ?enabled=true and ?provider=a2a|openclaw filters.
821
+ response: [{ id: string, name: string, description: string, url: string, enabled: boolean, provider: 'a2a'|'openclaw', capabilities?: object }]
822
+
823
+ POST /api/agents/:id/send
824
+ description: Send a text message to an agent (works for both A2A and OpenClaw protocols) and receive a normalized response
825
+ request: { message: string, attachments?: [{ fileName: string, mimeType: string, content: string }] }
826
+ response: { kind: 'message'|'task', text?: string, raw: object }
827
+
828
+ POST /api/agents/:id/stream
829
+ description: Send a message and receive a streaming SSE response (text/event-stream). Each event is JSON: { kind: 'text'|'status'|'artifact'|'done'|'error', data: any }
830
+ request: { message: string, attachments?: [{ fileName: string, mimeType: string, content: string }] }
831
+ response: SSE stream
832
+
833
+ synthos.agents.list(opts?) — GET /api/agents (returns configured agents; opts: { enabled?, provider? }; returns [{ id, name, description, url, enabled, provider, capabilities }])
834
+ synthos.agents.send(agentId, message, attachments?) — POST /api/agents/:id/send (sends a text message to any agent, returns normalized { kind, text, raw }; attachments: [{ fileName, mimeType, content }])
835
+ synthos.agents.sendStream(agentId, message, onEvent, attachments?) — POST /api/agents/:id/stream (SSE streaming; onEvent receives { kind, data }; returns { close() } handle; attachments: [{ fileName, mimeType, content }])
836
+ synthos.agents.isEnabled(agentId) — checks if an agent is enabled (returns Promise<boolean>)
837
+ synthos.agents.getCapabilities(agentId) — returns agent capabilities object (streaming, skills, etc.)`],
838
+
839
+ ['connectors', `GET /api/connectors
840
+ description: List available connectors (REST API proxies). Supports ?category=X and ?id=X filters.
841
+ response: [{ id: string, name: string, category: string, configured: boolean }]
842
+
843
+ GET /api/connectors/:id
844
+ description: Get full detail for a connector including its definition and configuration status
845
+ response: { id, name, category, description, baseUrl, authStrategy, authKey, fields, configured, enabled, hasKey }
846
+
847
+ POST /api/connectors (proxy call)
848
+ description: Proxy a request through a configured connector. The connector attaches auth automatically.
849
+ request: { connector: string, method: string, path: string, headers?: object, body?: any, query?: object }
850
+ response: Upstream API response (JSON or text)
851
+
852
+ synthos.connectors.call(connector, method, path, opts?) — POST /api/connectors (proxy call; opts: { headers?, body?, query? })
853
+ synthos.connectors.list(opts?) — GET /api/connectors (opts: { category?, id? })`],
854
+
855
+ ['files', `GET /api/files/:page
856
+ description: List files stored for a page (with sizes)
857
+ response: { files: [{ name: string, size: number }] }
858
+
859
+ POST /api/files/:page
860
+ description: Upload a file to a page's file storage (raw body + x-filename header)
861
+ request: Raw binary body with x-filename header
862
+ response: { name: string, size: number }
863
+
864
+ GET /api/files/:page/:filename
865
+ description: Download/serve a specific file from a page's file storage
866
+ response: File content (served with appropriate content-type)
867
+
868
+ DELETE /api/files/:page/:filename
869
+ description: Delete a file from a page's file storage
870
+ response: { deleted: true }
871
+
872
+ synthos.files.list() — GET /api/files/:page (auto-scoped to current page)
873
+ synthos.files.upload(filename, blob) — POST /api/files/:page (auto-scoped to current page; sends raw body with x-filename header)
874
+ synthos.files.url(filename) — returns URL string /api/files/:page/:filename (for <img src>, <a href>, etc.)
875
+ synthos.files.remove(filename) — DELETE /api/files/:page/:filename (auto-scoped to current page)`],
876
+
877
+ ['shared-data', `GET /api/shared/data/:table
878
+ description: Retrieve all rows from a shared (cross-page) table. Supports pagination via query params.
879
+ query params: limit (number, optional) — max rows to return; offset (number, optional, default 0) — rows to skip
880
+ response (without limit): Array of JSON rows [{ id: string, ... }]
881
+ response (with limit): { items: [{ id: string, ... }], total: number, offset: number, limit: number, hasMore: boolean }
882
+
883
+ GET /api/shared/data/:table/:id
884
+ description: Retrieve a single row from a shared table
885
+ response: JSON row { id: string, ... }
886
+
887
+ POST /api/shared/data/:table
888
+ description: Replaces or adds a single row to a shared table and returns the row
889
+ request: JSON row { id?: string, ... }
890
+ response: { id: string, ... }
891
+
892
+ DELETE /api/shared/data/:table/:id
893
+ description: Delete a single row from a shared table
894
+ response: { success: true }
895
+
896
+ synthos.shared.data.list(table, opts?) — GET /api/shared/data/:table (opts: { limit?, offset? } — when limit is set, returns { items, total, offset, limit, hasMore })
897
+ synthos.shared.data.get(table, id) — GET /api/shared/data/:table/:id
898
+ synthos.shared.data.save(table, row) — POST /api/shared/data/:table
899
+ synthos.shared.data.remove(table, id) — DELETE /api/shared/data/:table/:id`],
900
+
901
+ ['shared-files', `GET /api/shared/files
902
+ description: List files in shared (cross-page) file storage (with sizes)
903
+ response: { files: [{ name: string, size: number }] }
904
+
905
+ POST /api/shared/files
906
+ description: Upload a file to shared file storage (raw body + x-filename header)
907
+ request: Raw binary body with x-filename header
908
+ response: { name: string, size: number }
909
+
910
+ GET /api/shared/files/:filename
911
+ description: Download/serve a specific file from shared file storage
912
+ response: File content (served with appropriate content-type)
913
+
914
+ DELETE /api/shared/files/:filename
915
+ description: Delete a file from shared file storage
916
+ response: { deleted: true }
917
+
918
+ synthos.shared.files.list() — GET /api/shared/files
919
+ synthos.shared.files.upload(filename, blob) — POST /api/shared/files (sends raw body with x-filename header)
920
+ synthos.shared.files.url(filename) — returns URL string /api/shared/files/:filename (for <img src>, <a href>, etc.)
921
+ synthos.shared.files.remove(filename) — DELETE /api/shared/files/:filename`],
922
+ ]);
923
+
924
+ /**
925
+ * Assemble the <SERVER_APIS> prompt block, including only hints for enabled
926
+ * feature groups and any custom route hints from the Customizer.
927
+ */
928
+ export function buildRouteHints(customizer: Customizer): string {
929
+ const blocks: string[] = ['<SERVER_APIS>'];
930
+
931
+ // Built-in hints — only include enabled groups
932
+ for (const [group, hints] of DEFAULT_ROUTE_HINTS) {
933
+ if (customizer.isEnabled(group)) {
934
+ blocks.push(hints);
935
+ }
936
+ }
937
+
938
+ // Custom route hints from fork
939
+ for (const hint of customizer.getRouteHints()) {
940
+ blocks.push(hint);
941
+ }
942
+
943
+ blocks.push('PAGE HELPERS (available globally as window.synthos):');
944
+ blocks.push('All methods return Promises. Prefer these helpers over raw fetch().');
945
+ return blocks.join('\n\n');
946
+ }
947
+
948
+ // Backward-compatible full serverAPIs string (used when no Customizer is passed)
949
+ export const serverAPIs =
708
950
  `<SERVER_APIS>
709
951
  GET /api/data/:page/:table
710
952
  description: Retrieve all rows from a page-scoped table (tables are stored per-page). Supports pagination via query params.
@@ -752,6 +994,11 @@ DELETE /api/pages/:name
752
994
  description: Delete a user page (cannot delete required/system pages)
753
995
  response: { deleted: true }
754
996
 
997
+ POST /api/pages/:name/ask
998
+ description: Ask a question about a page with full HTML context
999
+ request: { question: string }
1000
+ response: { answer: string }
1001
+
755
1002
  POST /api/scripts/:id
756
1003
  description: Execute a script with the passed in variables
757
1004
  request: { [key: string]: string }
@@ -801,6 +1048,7 @@ PAGE HELPERS (available globally as window.synthos):
801
1048
  synthos.pages.get(name) — GET /api/pages/:name
802
1049
  synthos.pages.update(name, metadata) — POST /api/pages/:name
803
1050
  synthos.pages.remove(name) — DELETE /api/pages/:name
1051
+ synthos.pages.ask(name, question) — POST /api/pages/:name/ask
804
1052
  synthos.search.web(query, opts?) — POST /api/search/web (opts: { count?, country?, freshness? })
805
1053
  synthos.connectors.call(connector, method, path, opts?) — POST /api/connectors (proxy call; opts: { headers?, body?, query? })
806
1054
  synthos.connectors.list(opts?) — GET /api/connectors (opts: { category?, id? })
@@ -810,20 +1058,3 @@ PAGE HELPERS (available globally as window.synthos):
810
1058
  synthos.agents.isEnabled(agentId) — checks if an agent is enabled (returns Promise<boolean>)
811
1059
  synthos.agents.getCapabilities(agentId) — returns agent capabilities object (streaming, skills, etc.)
812
1060
  All methods return Promises. Prefer these helpers over raw fetch().`;
813
-
814
- const repairUSER_MESSAGE =
815
- `Some change operations from the previous response failed because the target nodes no longer exist in the page (they were removed or replaced by earlier operations in the same batch).
816
-
817
- Below is the CURRENT state of the page after the successful operations were applied, followed by the list of operations that failed and why.
818
-
819
- Re-generate corrected versions of ONLY the failed operations, targeting nodes that actually exist in the current page. Each element has a data-node-id attribute you can reference.
820
- If a failed operation is no longer needed (e.g. the intended change was already accomplished by another op), omit it.
821
- Return an empty JSON array [] if no repairs are needed.
822
-
823
- Return ONLY a JSON array of change operations using the same format:
824
- { "op": "update", "nodeId": "<data-node-id>", "html": "<new innerHTML>" }
825
- { "op": "replace", "nodeId": "<data-node-id>", "html": "<new outerHTML>" }
826
- { "op": "delete", "nodeId": "<data-node-id>" }
827
- { "op": "insert", "parentId": "<data-node-id>", "position": "prepend"|"append"|"before"|"after", "html": "<new element HTML>" }
828
- { "op": "style-element", "nodeId": "<data-node-id>", "style": "<css style string>" }`;
829
-