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
@@ -23,161 +23,165 @@ var __importStar = (this && this.__importStar) || function (mod) {
23
23
  return result;
24
24
  };
25
25
  Object.defineProperty(exports, "__esModule", { value: true });
26
- exports.parseChangeList = exports.injectError = exports.applyChangeList = exports.ensureScriptsBeforeBodyClose = exports.deduplicateInlineScripts = exports.stripNodeIds = exports.assignNodeIds = exports.transformPage = void 0;
27
- const scripts_1 = require("../scripts");
26
+ exports.serverAPIs = exports.buildRouteHints = exports.DEFAULT_ROUTE_HINTS = exports.AGENT_API_REFERENCE = exports.getTransformInstr = exports.getMessageFormat = exports.parseChangeList = exports.applyChangeList = exports.ensureScriptsBeforeBodyClose = exports.deduplicateInlineScripts = exports.normalizedIndexOf = exports.stripNodeIds = exports.assignNodeIds = exports.simpleMarkdown = exports.transformPage = void 0;
28
27
  const cheerio = __importStar(require("cheerio"));
29
- const connectors_1 = require("../connectors");
30
28
  async function transformPage(args) {
31
- const { pagesFolder, pageState, message, completePrompt } = args;
29
+ const { message, builder, additionalSections } = args;
30
+ // 0. Strip the early error-capture script so the LLM never sees it
31
+ const pageState = stripErrorCapture(args.pageState);
32
32
  // 1. Assign data-node-id to every element
33
33
  const { html: annotatedHtml } = assignNodeIds(pageState);
34
34
  try {
35
- // 2. Build prompt
36
- const scripts = await (0, scripts_1.listScripts)(pagesFolder);
37
- const serverScripts = `<SERVER_SCRIPTS>\n${scripts || ''}`;
38
- const currentPage = `<CURRENT_PAGE>\n${annotatedHtml}`;
39
- // Build theme context block
40
- let themeBlock = '<THEME>\n';
41
- if (args.themeInfo) {
42
- const { mode, colors } = args.themeInfo;
43
- const colorList = Object.entries(colors)
44
- .map(([name, value]) => ` --${name}: ${value}`)
45
- .join('\n');
46
- 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.`;
47
- }
48
- // Build configured-connectors block
49
- let connectorsBlock = '';
50
- if (args.configuredConnectors) {
51
- const entries = Object.entries(args.configuredConnectors)
52
- .filter(([, cfg]) => cfg.enabled && cfg.apiKey);
53
- if (entries.length > 0) {
54
- const blocks = entries.map(([id, cfg]) => {
55
- const def = connectors_1.CONNECTOR_REGISTRY.find(d => d.id === id);
56
- if (!def)
57
- return `- ${id}`;
58
- let block = `- ${def.name} (id: "${id}", category: ${def.category})\n Base URL: ${def.baseUrl}`;
59
- if (def.hints) {
60
- block += `\n Usage:\n${def.hints.split('\n').map(l => ' ' + l).join('\n')}`;
61
- }
62
- // Append dynamic OAuth context
63
- if (def.authStrategy === 'oauth2') {
64
- const oauthCfg = cfg;
65
- block += '\n Auth: The proxy attaches the access token automatically. Do NOT pass access_token in body or query params.';
66
- if (oauthCfg.userId) {
67
- block += `\n User ID: ${oauthCfg.userId} — use this directly in API paths (e.g. /${oauthCfg.userId}/media).`;
68
- }
69
- else {
70
- 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.';
71
- }
72
- }
73
- return block;
74
- });
75
- 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.`;
76
- }
77
- }
78
- // Build configured-agents block (only enabled agents)
79
- let agentsBlock = '';
80
- const enabledAgents = (args.configuredAgents ?? []).filter(a => a.enabled);
81
- if (enabledAgents.length > 0) {
82
- const agentBlocks = enabledAgents.map(a => {
83
- let block = `- ${a.name} (id: "${a.id}", provider: ${a.provider})`;
84
- block += `\n Description: ${a.description}`;
85
- if (a.capabilities?.streaming) {
86
- block += `\n Supports streaming: yes`;
87
- }
88
- if (a.skills && a.skills.length > 0) {
89
- const skillList = a.skills.map(s => ` - ${s.name}: ${s.description}`).join('\n');
90
- block += `\n Skills:\n${skillList}`;
91
- }
92
- return block;
93
- });
94
- agentsBlock = `<CONFIGURED_AGENTS>\nThe user has configured these agents:\n\n${agentBlocks.join('\n\n')}\n\n${AGENT_API_REFERENCE}`;
95
- }
96
- const systemMessage = [currentPage, serverAPIs, serverScripts, connectorsBlock, agentsBlock, themeBlock, messageFormat].filter(s => s).join('\n\n');
97
- const system = {
98
- role: 'system',
99
- content: systemMessage
35
+ // 2. Build CURRENT_PAGE section
36
+ const currentPage = {
37
+ title: '<CURRENT_PAGE>',
38
+ content: annotatedHtml,
39
+ instructions: '',
100
40
  };
101
- const userInstr = args.instructions || '';
102
- const modelInstr = args.modelInstructions || '';
103
- const instructions = [userInstr, modelInstr, transformInstr].filter(s => s.trim() !== '').join('\n');
104
- const prompt = {
105
- role: 'user',
106
- content: `<USER_MESSAGE>\n${message}\n\n<INSTRUCTIONS>\n${instructions}`
107
- };
108
- // 3. Call model
109
- const result = await completePrompt({ prompt, system });
110
- if (!result.completed) {
111
- return { completed: false, error: result.error };
41
+ // 3. Determine newBuild: if isBuilder, count .chat-message in annotated HTML
42
+ let newBuild = false;
43
+ if (args.isBuilder) {
44
+ const $ = cheerio.load(annotatedHtml, { decodeEntities: false });
45
+ const messageCount = $('#chatMessages .chat-message').length;
46
+ newBuild = messageCount <= 1;
112
47
  }
113
- // 4. Parse JSON change list from response
114
- const changes = parseChangeList(result.value);
115
- // 5. Apply changes (first pass — with failure reporting)
116
- const firstPass = applyChangeListWithReport(annotatedHtml, changes);
117
- let finalHtml = firstPass.html;
118
- let successCount = changes.length - firstPass.failedOps.length;
119
- // 6. Repair pass — if any ops failed, make one follow-up LLM call
120
- if (firstPass.failedOps.length > 0) {
121
- console.warn(`transformPage: ${firstPass.failedOps.length} op(s) failed — attempting repair pass`);
122
- try {
123
- // Re-assign fresh node IDs on the partially-updated HTML
124
- const { html: reAnnotatedHtml } = assignNodeIds(stripNodeIds(firstPass.html));
125
- // Build compact repair prompt
126
- const failedSummary = firstPass.failedOps
127
- .map((f, i) => `${i + 1}. op="${f.op.op}" — ${f.reason}\n original: ${JSON.stringify(f.op)}`)
128
- .join('\n');
129
- const repairSystem = {
130
- role: 'system',
131
- content: `<CURRENT_PAGE>\n${reAnnotatedHtml}\n\n<FAILED_OPERATIONS>\n${failedSummary}`
132
- };
133
- const repairPrompt = {
134
- role: 'user',
135
- content: repairUSER_MESSAGE
136
- };
137
- const repairResult = await completePrompt({ prompt: repairPrompt, system: repairSystem });
138
- if (repairResult.completed) {
139
- const repairChanges = parseChangeList(repairResult.value);
140
- if (repairChanges.length > 0) {
141
- const repairPass = applyChangeListWithReport(reAnnotatedHtml, repairChanges);
142
- const repairSuccessCount = repairChanges.length - repairPass.failedOps.length;
143
- if (repairPass.failedOps.length > 0) {
144
- console.warn(`transformPage: repair pass had ${repairPass.failedOps.length} remaining failure(s) — keeping partial result`);
145
- }
146
- finalHtml = repairPass.html;
147
- successCount += repairSuccessCount;
148
- console.log(`transformPage: repair pass applied ${repairSuccessCount} fix(es)`);
149
- }
150
- else {
151
- console.log('transformPage: repair pass returned no changes (model deemed repairs unnecessary)');
152
- }
153
- }
154
- else {
155
- console.warn('transformPage: repair LLM call failed — keeping partial result from first pass');
156
- }
48
+ // 4. Call builder
49
+ const result = await builder.run(currentPage, additionalSections, message, newBuild, args.attachments);
50
+ // 5. Switch on result kind
51
+ switch (result.kind) {
52
+ case 'transforms': {
53
+ const applied = applyChangeList(annotatedHtml, result.changes);
54
+ const clean = stripNodeIds(applied);
55
+ const deduped = deduplicateInlineScripts(clean);
56
+ const safe = ensureScriptsBeforeBodyClose(deduped);
57
+ return { completed: true, value: { html: safe, changeCount: result.changes.length } };
58
+ }
59
+ case 'reply': {
60
+ const productName = args.productName ?? 'SynthOS';
61
+ const withReply = appendChatReply(annotatedHtml, message, result.text, productName);
62
+ const clean = stripNodeIds(withReply);
63
+ const deduped = deduplicateInlineScripts(clean);
64
+ const safe = ensureScriptsBeforeBodyClose(deduped);
65
+ return { completed: true, value: { html: safe, changeCount: -1 } };
157
66
  }
158
- catch (repairErr) {
159
- const msg = repairErr instanceof Error ? repairErr.message : String(repairErr);
160
- console.warn(`transformPage: repair pass error ${msg} — keeping partial result from first pass`);
67
+ case 'error': {
68
+ const productName = args.productName ?? 'SynthOS';
69
+ const errorHtml = appendChatError(annotatedHtml, message, result.error.message, productName);
70
+ const clean = stripNodeIds(errorHtml);
71
+ return { completed: true, value: { html: clean, changeCount: -1 } };
161
72
  }
162
73
  }
163
- // 7. Strip data-node-id attributes
164
- const cleanHtml = stripNodeIds(finalHtml);
165
- // 8. Remove duplicate inline scripts (LLM may insert instead of update)
166
- const dedupedHtml = deduplicateInlineScripts(cleanHtml);
167
- // 9. Ensure page-helpers and page-script are last in <body>
168
- const safeHtml = ensureScriptsBeforeBodyClose(dedupedHtml);
169
- return { completed: true, value: { html: safeHtml, changeCount: successCount } };
170
74
  }
171
75
  catch (err) {
172
- // On any error: return original page with error block injected
173
- const cleanOriginal = stripNodeIds(annotatedHtml);
76
+ // On any error: append error message to chat
77
+ const productName = args.productName ?? 'SynthOS';
174
78
  const errorMessage = err instanceof Error ? err.message : String(err);
175
- const errorHtml = injectError(cleanOriginal, 'Something went wrong try again', errorMessage);
176
- return { completed: true, value: { html: errorHtml, changeCount: 0 } };
79
+ const errorHtml = appendChatError(annotatedHtml, message, errorMessage, productName);
80
+ const clean = stripNodeIds(errorHtml);
81
+ return { completed: true, value: { html: clean, changeCount: -1 } };
177
82
  }
178
83
  }
179
84
  exports.transformPage = transformPage;
180
85
  // ---------------------------------------------------------------------------
86
+ // Chat reply helper
87
+ // ---------------------------------------------------------------------------
88
+ /**
89
+ * Append a user message and a reply to #chatMessages using cheerio.
90
+ */
91
+ function appendChatReply(annotatedHtml, userMessage, replyText, productName) {
92
+ const $ = cheerio.load(annotatedHtml, { decodeEntities: false });
93
+ const chatMessages = $('#chatMessages');
94
+ if (chatMessages.length > 0) {
95
+ chatMessages.append(`<div class="chat-message"><p><strong>User:</strong> ${escapeHtml(userMessage)}</p></div>`);
96
+ const replyHtml = simpleMarkdown(replyText);
97
+ chatMessages.append(`<div class="chat-message"><p><strong>${escapeHtml(productName)}:</strong> ${replyHtml}</p></div>`);
98
+ }
99
+ return $.html();
100
+ }
101
+ function escapeHtml(text) {
102
+ return text
103
+ .replace(/&/g, '&amp;')
104
+ .replace(/</g, '&lt;')
105
+ .replace(/>/g, '&gt;')
106
+ .replace(/"/g, '&quot;');
107
+ }
108
+ /**
109
+ * Lightweight markdown-to-HTML converter for chat reply text.
110
+ * Handles: code blocks, inline code, bold, italic, links, unordered/ordered lists, paragraphs.
111
+ */
112
+ function simpleMarkdown(text) {
113
+ // Extract fenced code blocks first to protect their contents
114
+ const codeBlocks = [];
115
+ let processed = text.replace(/```(\w*)\n([\s\S]*?)```/g, (_match, lang, code) => {
116
+ const idx = codeBlocks.length;
117
+ const escaped = escapeHtml(code.replace(/\n$/, ''));
118
+ const langAttr = lang ? ` class="language-${escapeHtml(lang)}"` : '';
119
+ codeBlocks.push(`<pre><code${langAttr}>${escaped}</code></pre>`);
120
+ return `\x00CODEBLOCK${idx}\x00`;
121
+ });
122
+ // Split into paragraphs by blank lines
123
+ const blocks = processed.split(/\n{2,}/);
124
+ const htmlBlocks = [];
125
+ for (const block of blocks) {
126
+ const trimmed = block.trim();
127
+ if (!trimmed)
128
+ continue;
129
+ // Code block placeholder
130
+ if (/^\x00CODEBLOCK\d+\x00$/.test(trimmed)) {
131
+ htmlBlocks.push(trimmed);
132
+ continue;
133
+ }
134
+ // Unordered list (lines starting with - or *)
135
+ if (/^[\-\*] /m.test(trimmed) && trimmed.split('\n').every(l => /^[\-\*] /.test(l.trim()) || l.trim() === '')) {
136
+ const items = trimmed.split('\n')
137
+ .map(l => l.trim())
138
+ .filter(l => l)
139
+ .map(l => `<li>${inlineMarkdown(l.replace(/^[\-\*] /, ''))}</li>`)
140
+ .join('');
141
+ htmlBlocks.push(`<ul>${items}</ul>`);
142
+ continue;
143
+ }
144
+ // Ordered list (lines starting with 1. 2. etc.)
145
+ if (/^\d+\. /m.test(trimmed) && trimmed.split('\n').every(l => /^\d+\. /.test(l.trim()) || l.trim() === '')) {
146
+ const items = trimmed.split('\n')
147
+ .map(l => l.trim())
148
+ .filter(l => l)
149
+ .map(l => `<li>${inlineMarkdown(l.replace(/^\d+\. /, ''))}</li>`)
150
+ .join('');
151
+ htmlBlocks.push(`<ol>${items}</ol>`);
152
+ continue;
153
+ }
154
+ // Regular paragraph
155
+ htmlBlocks.push(`<p>${inlineMarkdown(trimmed.replace(/\n/g, '<br>'))}</p>`);
156
+ }
157
+ // Restore code blocks
158
+ let result = htmlBlocks.join('');
159
+ result = result.replace(/\x00CODEBLOCK(\d+)\x00/g, (_m, idx) => codeBlocks[parseInt(idx)]);
160
+ return result;
161
+ }
162
+ exports.simpleMarkdown = simpleMarkdown;
163
+ /** Apply inline markdown formatting: bold, italic, inline code, links. */
164
+ function inlineMarkdown(text) {
165
+ // Inline code (protect from further processing)
166
+ const codes = [];
167
+ let result = text.replace(/`([^`]+)`/g, (_m, code) => {
168
+ const idx = codes.length;
169
+ codes.push(`<code>${escapeHtml(code)}</code>`);
170
+ return `\x00CODE${idx}\x00`;
171
+ });
172
+ // Bold (**text** or __text__)
173
+ result = result.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
174
+ result = result.replace(/__(.+?)__/g, '<strong>$1</strong>');
175
+ // Italic (*text* or _text_)
176
+ result = result.replace(/\*(.+?)\*/g, '<em>$1</em>');
177
+ result = result.replace(/(?<!\w)_(.+?)_(?!\w)/g, '<em>$1</em>');
178
+ // Links [text](url)
179
+ result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
180
+ // Restore inline code
181
+ result = result.replace(/\x00CODE(\d+)\x00/g, (_m, idx) => codes[parseInt(idx)]);
182
+ return result;
183
+ }
184
+ // ---------------------------------------------------------------------------
181
185
  // Internal helpers
182
186
  // ---------------------------------------------------------------------------
183
187
  /**
@@ -204,6 +208,65 @@ function stripNodeIds(html) {
204
208
  return $.html();
205
209
  }
206
210
  exports.stripNodeIds = stripNodeIds;
211
+ /**
212
+ * Remove the early error-capture script injected into <head> so the LLM
213
+ * never sees it during page transformation.
214
+ */
215
+ function stripErrorCapture(html) {
216
+ const id = 'synthos-error-capture';
217
+ if (!html.includes(id))
218
+ return html;
219
+ const $ = cheerio.load(html, { decodeEntities: false });
220
+ $(`#${id}`).remove();
221
+ return $.html();
222
+ }
223
+ /**
224
+ * Find `needle` in `haystack` using whitespace-normalized comparison.
225
+ * Collapses runs of whitespace (spaces, tabs, newlines) into a single space
226
+ * for comparison purposes but returns the position in the original string.
227
+ * Returns -1 if no match found.
228
+ */
229
+ function normalizedIndexOf(haystack, needle) {
230
+ // Build a mapping from normalized-string positions to original-string positions
231
+ const normChars = [];
232
+ const origPositions = []; // origPositions[i] = original index of normChars[i]
233
+ let inWhitespace = false;
234
+ for (let i = 0; i < haystack.length; i++) {
235
+ const ch = haystack[i];
236
+ if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') {
237
+ if (!inWhitespace) {
238
+ normChars.push(' ');
239
+ origPositions.push(i);
240
+ inWhitespace = true;
241
+ }
242
+ }
243
+ else {
244
+ normChars.push(ch);
245
+ origPositions.push(i);
246
+ inWhitespace = false;
247
+ }
248
+ }
249
+ const normHaystack = normChars.join('');
250
+ // Normalize the needle the same way
251
+ const normNeedle = needle.replace(/\s+/g, ' ');
252
+ const idx = normHaystack.indexOf(normNeedle);
253
+ if (idx === -1)
254
+ return null;
255
+ const start = origPositions[idx];
256
+ // The end position: find the original position of the last matched char, then go one past
257
+ const lastNormIdx = idx + normNeedle.length - 1;
258
+ const lastOrigPos = origPositions[lastNormIdx];
259
+ // Advance past any trailing whitespace that was collapsed in the original
260
+ let end = lastOrigPos + 1;
261
+ if (haystack[lastOrigPos] === ' ' || haystack[lastOrigPos] === '\t' || haystack[lastOrigPos] === '\n' || haystack[lastOrigPos] === '\r') {
262
+ // The last matched normalized char was a whitespace collapse — extend to include all original whitespace
263
+ while (end < haystack.length && (haystack[end] === ' ' || haystack[end] === '\t' || haystack[end] === '\n' || haystack[end] === '\r')) {
264
+ end++;
265
+ }
266
+ }
267
+ return { start, end };
268
+ }
269
+ exports.normalizedIndexOf = normalizedIndexOf;
207
270
  /**
208
271
  * Remove duplicate inline `<script>` blocks using a two-pass approach.
209
272
  *
@@ -331,6 +394,29 @@ exports.ensureScriptsBeforeBodyClose = ensureScriptsBeforeBodyClose;
331
394
  function isElementLocked(el, $) {
332
395
  return el.attr('data-locked') !== undefined;
333
396
  }
397
+ /**
398
+ * If the target element is a <script> or <style> and the html is wrapped in a
399
+ * redundant matching tag, strip the outer tag to avoid nesting (e.g.
400
+ * `<script>` inside `<script>`). Returns the inner content when unwrapped.
401
+ */
402
+ function unwrapRedundantTag(tagName, html) {
403
+ if (tagName !== 'script' && tagName !== 'style')
404
+ return html;
405
+ const re = new RegExp(`^\\s*<${tagName}[^>]*>([\\s\\S]*)</${tagName}>\\s*$`, 'i');
406
+ const match = html.match(re);
407
+ return match ? match[1] : html;
408
+ }
409
+ /**
410
+ * Strip any `<script>` or `<style>` tags from text that will be injected
411
+ * inside an existing script/style block (search-replace, search-insert).
412
+ */
413
+ function stripNestedBlockTags(tagName, text) {
414
+ if (tagName !== 'script' && tagName !== 'style')
415
+ return text;
416
+ return text
417
+ .replace(new RegExp(`<${tagName}[^>]*>`, 'gi'), '')
418
+ .replace(new RegExp(`</${tagName}>`, 'gi'), '');
419
+ }
334
420
  /**
335
421
  * Apply a list of CRUD operations to annotated HTML (elements must have `data-node-id`).
336
422
  */
@@ -344,7 +430,8 @@ function applyChangeList(html, changes) {
344
430
  console.warn(`applyChangeList: skipping update — node ${change.nodeId} not found (already removed?)`);
345
431
  break;
346
432
  }
347
- el.html(change.html);
433
+ const tag = el.prop('tagName')?.toLowerCase();
434
+ el.html(unwrapRedundantTag(tag, change.html));
348
435
  break;
349
436
  }
350
437
  case 'replace': {
@@ -357,7 +444,17 @@ function applyChangeList(html, changes) {
357
444
  console.warn(`applyChangeList: skipping replace — node ${change.nodeId} is data-locked`);
358
445
  break;
359
446
  }
360
- el.replaceWith(change.html);
447
+ // If the target is a <script> or <style> and the html doesn't
448
+ // include the outer tag (or wraps it redundantly), treat as an
449
+ // update (set inner content) instead of replacing the element.
450
+ const tagName = el.prop('tagName')?.toLowerCase();
451
+ const cleaned = unwrapRedundantTag(tagName, change.html);
452
+ if ((tagName === 'script' || tagName === 'style') && !cleaned.trimStart().startsWith('<')) {
453
+ el.html(cleaned);
454
+ }
455
+ else {
456
+ el.replaceWith(change.html);
457
+ }
361
458
  break;
362
459
  }
363
460
  case 'delete': {
@@ -375,22 +472,28 @@ function applyChangeList(html, changes) {
375
472
  }
376
473
  case 'insert': {
377
474
  const parent = $(`[data-node-id="${change.parentId}"]`);
378
- if (parent.length === 0)
379
- throw new Error(`insert: parent node ${change.parentId} not found`);
475
+ if (parent.length === 0) {
476
+ console.warn(`applyChangeList: skipping insert — parent node ${change.parentId} not found`);
477
+ break;
478
+ }
479
+ // Unwrap redundant tags when inserting into script/style
480
+ const parentTag = parent.prop('tagName')?.toLowerCase();
481
+ const insertHtml = unwrapRedundantTag(parentTag, change.html);
380
482
  switch (change.position) {
381
483
  case 'prepend':
382
- parent.prepend(change.html);
484
+ parent.prepend(insertHtml);
383
485
  break;
384
486
  case 'append':
385
- parent.append(change.html);
487
+ parent.append(insertHtml);
386
488
  break;
387
489
  case 'before':
388
- parent.before(change.html);
490
+ parent.before(insertHtml);
389
491
  break;
390
492
  case 'after':
391
- parent.after(change.html);
493
+ parent.after(insertHtml);
392
494
  break;
393
- default: throw new Error(`insert: unknown position "${change.position}"`);
495
+ default:
496
+ console.warn(`applyChangeList: skipping insert — unknown position "${change.position}"`);
394
497
  }
395
498
  break;
396
499
  }
@@ -407,134 +510,75 @@ function applyChangeList(html, changes) {
407
510
  el.attr('style', change.style);
408
511
  break;
409
512
  }
410
- default:
411
- throw new Error(`Unknown change op: "${change.op}"`);
412
- }
413
- }
414
- return $.html();
415
- }
416
- exports.applyChangeList = applyChangeList;
417
- /**
418
- * Apply a list of CRUD operations and report any ops that failed due to
419
- * missing nodes (instead of throwing). Unknown op types still throw.
420
- */
421
- function applyChangeListWithReport(html, changes) {
422
- const $ = cheerio.load(html, { decodeEntities: false });
423
- const failedOps = [];
424
- for (const change of changes) {
425
- switch (change.op) {
426
- case 'update': {
513
+ case 'search-replace': {
427
514
  const el = $(`[data-node-id="${change.nodeId}"]`);
428
515
  if (el.length === 0) {
429
- const reason = `node ${change.nodeId} not found (already removed?)`;
430
- console.warn(`applyChangeListWithReport: skipping update — ${reason}`);
431
- failedOps.push({ op: change, reason });
516
+ console.warn(`applyChangeList: skipping search-replace node ${change.nodeId} not found (already removed?)`);
432
517
  break;
433
518
  }
434
- el.html(change.html);
435
- break;
436
- }
437
- case 'replace': {
438
- const el = $(`[data-node-id="${change.nodeId}"]`);
439
- if (el.length === 0) {
440
- const reason = `node ${change.nodeId} not found (already removed?)`;
441
- console.warn(`applyChangeListWithReport: skipping replace — ${reason}`);
442
- failedOps.push({ op: change, reason });
443
- break;
519
+ const srTag = el.prop('tagName')?.toLowerCase();
520
+ const replaceText = stripNestedBlockTags(srTag, change.replace);
521
+ const content = el.html() ?? '';
522
+ const exactIdx = content.indexOf(change.search);
523
+ if (exactIdx !== -1) {
524
+ el.html(content.slice(0, exactIdx) + replaceText + content.slice(exactIdx + change.search.length));
444
525
  }
445
- if (isElementLocked(el, $)) {
446
- const reason = `node ${change.nodeId} is data-locked`;
447
- console.warn(`applyChangeListWithReport: skipping replace — ${reason}`);
448
- failedOps.push({ op: change, reason });
449
- break;
526
+ else {
527
+ const norm = normalizedIndexOf(content, change.search);
528
+ if (norm) {
529
+ el.html(content.slice(0, norm.start) + replaceText + content.slice(norm.end));
530
+ }
531
+ else {
532
+ console.warn(`applyChangeList: skipping search-replace — search text not found in node ${change.nodeId}`);
533
+ }
450
534
  }
451
- el.replaceWith(change.html);
452
535
  break;
453
536
  }
454
- case 'delete': {
537
+ case 'search-insert': {
455
538
  const el = $(`[data-node-id="${change.nodeId}"]`);
456
539
  if (el.length === 0) {
457
- const reason = `node ${change.nodeId} not found (already removed?)`;
458
- console.warn(`applyChangeListWithReport: skipping delete — ${reason}`);
459
- failedOps.push({ op: change, reason });
460
- break;
461
- }
462
- if (isElementLocked(el, $)) {
463
- const reason = `node ${change.nodeId} is data-locked`;
464
- console.warn(`applyChangeListWithReport: skipping delete — ${reason}`);
465
- failedOps.push({ op: change, reason });
466
- break;
467
- }
468
- el.remove();
469
- break;
470
- }
471
- case 'insert': {
472
- const parent = $(`[data-node-id="${change.parentId}"]`);
473
- if (parent.length === 0) {
474
- const reason = `parent node ${change.parentId} not found`;
475
- console.warn(`applyChangeListWithReport: skipping insert — ${reason}`);
476
- failedOps.push({ op: change, reason });
540
+ console.warn(`applyChangeList: skipping search-insert node ${change.nodeId} not found (already removed?)`);
477
541
  break;
478
542
  }
479
- switch (change.position) {
480
- case 'prepend':
481
- parent.prepend(change.html);
482
- break;
483
- case 'append':
484
- parent.append(change.html);
485
- break;
486
- case 'before':
487
- parent.before(change.html);
488
- break;
489
- case 'after':
490
- parent.after(change.html);
491
- break;
492
- default: throw new Error(`insert: unknown position "${change.position}"`);
493
- }
494
- break;
495
- }
496
- case 'style-element': {
497
- const el = $(`[data-node-id="${change.nodeId}"]`);
498
- if (el.length === 0) {
499
- const reason = `node ${change.nodeId} not found (already removed?)`;
500
- console.warn(`applyChangeListWithReport: skipping style-element — ${reason}`);
501
- failedOps.push({ op: change, reason });
502
- break;
543
+ const siTag = el.prop('tagName')?.toLowerCase();
544
+ const insertContent = stripNestedBlockTags(siTag, change.content);
545
+ const content = el.html() ?? '';
546
+ const exactIdx = content.indexOf(change.after);
547
+ if (exactIdx !== -1) {
548
+ const insertPos = exactIdx + change.after.length;
549
+ el.html(content.slice(0, insertPos) + insertContent + content.slice(insertPos));
503
550
  }
504
- if (isElementLocked(el, $)) {
505
- const reason = `node ${change.nodeId} is data-locked`;
506
- console.warn(`applyChangeListWithReport: skipping style-element — ${reason}`);
507
- failedOps.push({ op: change, reason });
508
- break;
551
+ else {
552
+ const norm = normalizedIndexOf(content, change.after);
553
+ if (norm) {
554
+ el.html(content.slice(0, norm.end) + insertContent + content.slice(norm.end));
555
+ }
556
+ else {
557
+ console.warn(`applyChangeList: skipping search-insert — after text not found in node ${change.nodeId}`);
558
+ }
509
559
  }
510
- el.attr('style', change.style);
511
560
  break;
512
561
  }
513
562
  default:
514
563
  throw new Error(`Unknown change op: "${change.op}"`);
515
564
  }
516
565
  }
517
- return { html: $.html(), failedOps };
566
+ return $.html();
518
567
  }
568
+ exports.applyChangeList = applyChangeList;
519
569
  /**
520
- * Inject an error script block into the page HTML.
570
+ * Append a user message and a styled error message to #chatMessages.
521
571
  */
522
- function injectError(html, message, details) {
572
+ function appendChatError(html, userMessage, errorDetails, productName) {
523
573
  const $ = cheerio.load(html, { decodeEntities: false });
524
- const errorPayload = JSON.stringify({ message, details });
525
- const scriptTag = `<script id="error" type="application/json">${errorPayload}</script>`;
526
- // Remove any existing error block first
527
- $('script#error').remove();
528
- // Inject before closing </body> or at end
529
- if ($('body').length > 0) {
530
- $('body').append(scriptTag);
531
- }
532
- else {
533
- return html + scriptTag;
574
+ const chatMessages = $('#chatMessages');
575
+ if (chatMessages.length > 0) {
576
+ chatMessages.append(`<div class="chat-message"><p><strong>User:</strong> ${escapeHtml(userMessage)}</p></div>`);
577
+ chatMessages.append(`<div class="chat-message chat-message-error"><p><strong>${escapeHtml(productName)}:</strong> Something went wrong \u2014 please try again.</p>`
578
+ + `<p class="chat-error-details">${escapeHtml(errorDetails)}</p></div>`);
534
579
  }
535
580
  return $.html();
536
581
  }
537
- exports.injectError = injectError;
538
582
  /**
539
583
  * Parse a JSON change list from the model's raw response text.
540
584
  * Handles responses that may include markdown fences or extra text around the JSON.
@@ -567,16 +611,20 @@ exports.parseChangeList = parseChangeList;
567
611
  // ---------------------------------------------------------------------------
568
612
  // Prompt constants
569
613
  // ---------------------------------------------------------------------------
570
- const messageFormat = `<MESSAGE_FORMAT>
571
- <div class="chat-message"><p><strong>{SynthOS: | User:}</strong> {message contents}</p></div>
614
+ function getMessageFormat(productName) {
615
+ return `<MESSAGE_FORMAT>
616
+ <div class="chat-message"><p><strong>{${productName}: | User:}</strong> {message contents}</p></div>
572
617
  `;
573
- const transformInstr = `Apply the users <USER_MESSAGE> to the .viewerPanel of the <CURRENT_PAGE> by generating a list of changes in JSON format.
618
+ }
619
+ exports.getMessageFormat = getMessageFormat;
620
+ function getTransformInstr(productName) {
621
+ return `Apply the users <USER_MESSAGE> to the .viewerPanel of the <CURRENT_PAGE> by generating a list of changes in JSON format.
574
622
  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.
575
623
 
576
- 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.
577
- If there's no <USER_MESSAGE> add a SynthOS: message to the chat with aasking the user what they would like to do.
578
- 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.
579
- 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.
624
+ 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.
625
+ If there's no <USER_MESSAGE> add a ${productName}: message to the chat with aasking the user what they would like to do.
626
+ 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.
627
+ 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.
580
628
  If a <USER_MESSAGE> is overly long, summarize the User: message.
581
629
 
582
630
  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.
@@ -600,30 +648,11 @@ Do not add duplicate script blocks with the same logic! Consolidate inline scrip
600
648
  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.
601
649
  If you're trying to assign an id to script or style block, use "replace" not "update".
602
650
  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.
603
- Return a JSON array of change operations to apply to the page. Do NOT return the full HTML page.
604
-
605
- Each operation must be one of:
606
- { "op": "update", "nodeId": "<data-node-id>", "html": "<new innerHTML>" }
607
- — replaces the innerHTML of the target element
608
-
609
- { "op": "replace", "nodeId": "<data-node-id>", "html": "<new outerHTML>" }
610
- — replaces the entire element (outerHTML) with new markup
611
-
612
- { "op": "delete", "nodeId": "<data-node-id>" }
613
- — removes the element from the page
614
-
615
- { "op": "insert", "parentId": "<data-node-id>", "position": "prepend"|"append"|"before"|"after", "html": "<new element HTML>" }
616
- — inserts new HTML relative to the parent element
617
651
 
618
- { "op": "style-element", "nodeId": "<data-node-id>", "style": "<css style string>" }
619
- — sets the style attribute of the target element (must be unlocked)
620
-
621
- Return ONLY the JSON array. Example:
622
- [
623
- { "op": "update", "nodeId": "5", "html": "<p>Hello world</p>" },
624
- { "op": "insert", "parentId": "3", "position": "append", "html": "<div class=\\"msg\\">New message</div>" }
625
- ]`;
626
- const AGENT_API_REFERENCE = `## Agent API
652
+ 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.`;
653
+ }
654
+ exports.getTransformInstr = getTransformInstr;
655
+ exports.AGENT_API_REFERENCE = `## Agent API
627
656
 
628
657
  Check availability first (required):
629
658
  const agents = await synthos.agents.list({ enabled: true });
@@ -655,7 +684,207 @@ Stream with attachments:
655
684
 
656
685
  IMPORTANT: Always check synthos.agents.list({ enabled: true }) before calling an agent.
657
686
  If no agents are configured, show the user a link to Settings > Agents (/settings?tab=agents).`;
658
- const serverAPIs = `<SERVER_APIS>
687
+ // ---------------------------------------------------------------------------
688
+ // Route hint blocks — keyed by feature group so they can be filtered
689
+ // ---------------------------------------------------------------------------
690
+ exports.DEFAULT_ROUTE_HINTS = new Map([
691
+ ['data', `GET /api/data/:page/:table
692
+ description: Retrieve all rows from a page-scoped table (tables are stored per-page). Supports pagination via query params.
693
+ query params: limit (number, optional) — max rows to return; offset (number, optional, default 0) — rows to skip
694
+ response (without limit): Array of JSON rows [{ id: string, ... }]
695
+ response (with limit): { items: [{ id: string, ... }], total: number, offset: number, limit: number, hasMore: boolean }
696
+
697
+ GET /api/data/:page/:table/:id
698
+ description: Retrieve a single row from a page-scoped table
699
+ response: JSON row { id: string, ... }
700
+
701
+ POST /api/data/:page/:table
702
+ description: Replaces or adds a single row to a page-scoped table and returns the row
703
+ request: JSON row { id?: string, ... }
704
+ response: { id: string, ... }
705
+
706
+ DELETE /api/data/:page/:table/:id
707
+ description: Delete a single row from a page-scoped table
708
+ response: { success: true }
709
+
710
+ 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 })
711
+ synthos.data.get(table, id) — GET /api/data/:page/:table/:id (auto-scoped to current page)
712
+ synthos.data.save(table, row) — POST /api/data/:page/:table (auto-scoped to current page)
713
+ synthos.data.remove(table, id) — DELETE /api/data/:page/:table/:id (auto-scoped to current page)`],
714
+ ['api', `POST /api/generate/image
715
+ description: Generate an image based on a prompt
716
+ request: { prompt: string, shape: 'square' | 'portrait' | 'landscape', style: 'vivid' | 'natural' }
717
+ response: { url: string }
718
+
719
+ POST /api/generate/completion
720
+ description: Generates a text completion based on a prompt
721
+ request: { prompt: string, temperature?: number }
722
+ response: { answer: string, explanation: string }
723
+
724
+ synthos.generate.image({ prompt, shape, style }) — POST /api/generate/image
725
+ synthos.generate.completion({ prompt, temperature? }) — POST /api/generate/completion`],
726
+ ['pages', `GET /api/pages
727
+ description: Retrieve a list of all pages with metadata
728
+ response: Array of { name: string, title: string, categories: string[], pinned: boolean, createdDate: string, lastModified: string, pageVersion: number, mode: 'unlocked' | 'locked' }
729
+
730
+ GET /api/pages/:name
731
+ description: Retrieve metadata for a single page
732
+ response: { title: string, categories: string[], pinned: boolean, createdDate: string, lastModified: string, pageVersion: number, mode: 'unlocked' | 'locked' }
733
+
734
+ POST /api/pages/:name
735
+ description: Update page metadata (merge semantics — send only fields to change; lastModified is auto-set)
736
+ request: { title?: string, categories?: string[], pinned?: boolean, mode?: 'unlocked' | 'locked' }
737
+ response: Full metadata object
738
+
739
+ DELETE /api/pages/:name
740
+ description: Delete a user page (cannot delete required/system pages)
741
+ response: { deleted: true }
742
+
743
+ POST /api/pages/:name/ask
744
+ description: Ask a question about a page with full HTML context
745
+ request: { question: string }
746
+ response: { answer: string }
747
+
748
+ synthos.pages.list() — GET /api/pages
749
+ synthos.pages.get(name) — GET /api/pages/:name
750
+ synthos.pages.update(name, metadata) — POST /api/pages/:name
751
+ synthos.pages.remove(name) — DELETE /api/pages/:name
752
+ synthos.pages.ask(name, question) — POST /api/pages/:name/ask`],
753
+ ['scripts', `POST /api/scripts/:id
754
+ description: Execute a script with the passed in variables
755
+ request: { [key: string]: string }
756
+ response: string
757
+
758
+ synthos.scripts.run(id, variables) — POST /api/scripts/:id`],
759
+ ['search', `POST /api/search/web
760
+ description: Search the web using Brave Search (must be enabled in Settings > Connectors)
761
+ request: { query: string, count?: number, country?: string, freshness?: string }
762
+ response: { results: [{ title: string, url: string, description: string }] }
763
+
764
+ synthos.search.web(query, opts?) — POST /api/search/web (opts: { count?, country?, freshness? })`],
765
+ ['agents', `GET /api/agents
766
+ description: List configured agents (A2A and OpenClaw). Supports ?enabled=true and ?provider=a2a|openclaw filters.
767
+ response: [{ id: string, name: string, description: string, url: string, enabled: boolean, provider: 'a2a'|'openclaw', capabilities?: object }]
768
+
769
+ POST /api/agents/:id/send
770
+ description: Send a text message to an agent (works for both A2A and OpenClaw protocols) and receive a normalized response
771
+ request: { message: string, attachments?: [{ fileName: string, mimeType: string, content: string }] }
772
+ response: { kind: 'message'|'task', text?: string, raw: object }
773
+
774
+ POST /api/agents/:id/stream
775
+ 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 }
776
+ request: { message: string, attachments?: [{ fileName: string, mimeType: string, content: string }] }
777
+ response: SSE stream
778
+
779
+ synthos.agents.list(opts?) — GET /api/agents (returns configured agents; opts: { enabled?, provider? }; returns [{ id, name, description, url, enabled, provider, capabilities }])
780
+ 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 }])
781
+ 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 }])
782
+ synthos.agents.isEnabled(agentId) — checks if an agent is enabled (returns Promise<boolean>)
783
+ synthos.agents.getCapabilities(agentId) — returns agent capabilities object (streaming, skills, etc.)`],
784
+ ['connectors', `GET /api/connectors
785
+ description: List available connectors (REST API proxies). Supports ?category=X and ?id=X filters.
786
+ response: [{ id: string, name: string, category: string, configured: boolean }]
787
+
788
+ GET /api/connectors/:id
789
+ description: Get full detail for a connector including its definition and configuration status
790
+ response: { id, name, category, description, baseUrl, authStrategy, authKey, fields, configured, enabled, hasKey }
791
+
792
+ POST /api/connectors (proxy call)
793
+ description: Proxy a request through a configured connector. The connector attaches auth automatically.
794
+ request: { connector: string, method: string, path: string, headers?: object, body?: any, query?: object }
795
+ response: Upstream API response (JSON or text)
796
+
797
+ synthos.connectors.call(connector, method, path, opts?) — POST /api/connectors (proxy call; opts: { headers?, body?, query? })
798
+ synthos.connectors.list(opts?) — GET /api/connectors (opts: { category?, id? })`],
799
+ ['files', `GET /api/files/:page
800
+ description: List files stored for a page (with sizes)
801
+ response: { files: [{ name: string, size: number }] }
802
+
803
+ POST /api/files/:page
804
+ description: Upload a file to a page's file storage (raw body + x-filename header)
805
+ request: Raw binary body with x-filename header
806
+ response: { name: string, size: number }
807
+
808
+ GET /api/files/:page/:filename
809
+ description: Download/serve a specific file from a page's file storage
810
+ response: File content (served with appropriate content-type)
811
+
812
+ DELETE /api/files/:page/:filename
813
+ description: Delete a file from a page's file storage
814
+ response: { deleted: true }
815
+
816
+ synthos.files.list() — GET /api/files/:page (auto-scoped to current page)
817
+ synthos.files.upload(filename, blob) — POST /api/files/:page (auto-scoped to current page; sends raw body with x-filename header)
818
+ synthos.files.url(filename) — returns URL string /api/files/:page/:filename (for <img src>, <a href>, etc.)
819
+ synthos.files.remove(filename) — DELETE /api/files/:page/:filename (auto-scoped to current page)`],
820
+ ['shared-data', `GET /api/shared/data/:table
821
+ description: Retrieve all rows from a shared (cross-page) table. Supports pagination via query params.
822
+ query params: limit (number, optional) — max rows to return; offset (number, optional, default 0) — rows to skip
823
+ response (without limit): Array of JSON rows [{ id: string, ... }]
824
+ response (with limit): { items: [{ id: string, ... }], total: number, offset: number, limit: number, hasMore: boolean }
825
+
826
+ GET /api/shared/data/:table/:id
827
+ description: Retrieve a single row from a shared table
828
+ response: JSON row { id: string, ... }
829
+
830
+ POST /api/shared/data/:table
831
+ description: Replaces or adds a single row to a shared table and returns the row
832
+ request: JSON row { id?: string, ... }
833
+ response: { id: string, ... }
834
+
835
+ DELETE /api/shared/data/:table/:id
836
+ description: Delete a single row from a shared table
837
+ response: { success: true }
838
+
839
+ synthos.shared.data.list(table, opts?) — GET /api/shared/data/:table (opts: { limit?, offset? } — when limit is set, returns { items, total, offset, limit, hasMore })
840
+ synthos.shared.data.get(table, id) — GET /api/shared/data/:table/:id
841
+ synthos.shared.data.save(table, row) — POST /api/shared/data/:table
842
+ synthos.shared.data.remove(table, id) — DELETE /api/shared/data/:table/:id`],
843
+ ['shared-files', `GET /api/shared/files
844
+ description: List files in shared (cross-page) file storage (with sizes)
845
+ response: { files: [{ name: string, size: number }] }
846
+
847
+ POST /api/shared/files
848
+ description: Upload a file to shared file storage (raw body + x-filename header)
849
+ request: Raw binary body with x-filename header
850
+ response: { name: string, size: number }
851
+
852
+ GET /api/shared/files/:filename
853
+ description: Download/serve a specific file from shared file storage
854
+ response: File content (served with appropriate content-type)
855
+
856
+ DELETE /api/shared/files/:filename
857
+ description: Delete a file from shared file storage
858
+ response: { deleted: true }
859
+
860
+ synthos.shared.files.list() — GET /api/shared/files
861
+ synthos.shared.files.upload(filename, blob) — POST /api/shared/files (sends raw body with x-filename header)
862
+ synthos.shared.files.url(filename) — returns URL string /api/shared/files/:filename (for <img src>, <a href>, etc.)
863
+ synthos.shared.files.remove(filename) — DELETE /api/shared/files/:filename`],
864
+ ]);
865
+ /**
866
+ * Assemble the <SERVER_APIS> prompt block, including only hints for enabled
867
+ * feature groups and any custom route hints from the Customizer.
868
+ */
869
+ function buildRouteHints(customizer) {
870
+ const blocks = ['<SERVER_APIS>'];
871
+ // Built-in hints — only include enabled groups
872
+ for (const [group, hints] of exports.DEFAULT_ROUTE_HINTS) {
873
+ if (customizer.isEnabled(group)) {
874
+ blocks.push(hints);
875
+ }
876
+ }
877
+ // Custom route hints from fork
878
+ for (const hint of customizer.getRouteHints()) {
879
+ blocks.push(hint);
880
+ }
881
+ blocks.push('PAGE HELPERS (available globally as window.synthos):');
882
+ blocks.push('All methods return Promises. Prefer these helpers over raw fetch().');
883
+ return blocks.join('\n\n');
884
+ }
885
+ exports.buildRouteHints = buildRouteHints;
886
+ // Backward-compatible full serverAPIs string (used when no Customizer is passed)
887
+ exports.serverAPIs = `<SERVER_APIS>
659
888
  GET /api/data/:page/:table
660
889
  description: Retrieve all rows from a page-scoped table (tables are stored per-page). Supports pagination via query params.
661
890
  query params: limit (number, optional) — max rows to return; offset (number, optional, default 0) — rows to skip
@@ -702,6 +931,11 @@ DELETE /api/pages/:name
702
931
  description: Delete a user page (cannot delete required/system pages)
703
932
  response: { deleted: true }
704
933
 
934
+ POST /api/pages/:name/ask
935
+ description: Ask a question about a page with full HTML context
936
+ request: { question: string }
937
+ response: { answer: string }
938
+
705
939
  POST /api/scripts/:id
706
940
  description: Execute a script with the passed in variables
707
941
  request: { [key: string]: string }
@@ -751,6 +985,7 @@ PAGE HELPERS (available globally as window.synthos):
751
985
  synthos.pages.get(name) — GET /api/pages/:name
752
986
  synthos.pages.update(name, metadata) — POST /api/pages/:name
753
987
  synthos.pages.remove(name) — DELETE /api/pages/:name
988
+ synthos.pages.ask(name, question) — POST /api/pages/:name/ask
754
989
  synthos.search.web(query, opts?) — POST /api/search/web (opts: { count?, country?, freshness? })
755
990
  synthos.connectors.call(connector, method, path, opts?) — POST /api/connectors (proxy call; opts: { headers?, body?, query? })
756
991
  synthos.connectors.list(opts?) — GET /api/connectors (opts: { category?, id? })
@@ -760,18 +995,4 @@ PAGE HELPERS (available globally as window.synthos):
760
995
  synthos.agents.isEnabled(agentId) — checks if an agent is enabled (returns Promise<boolean>)
761
996
  synthos.agents.getCapabilities(agentId) — returns agent capabilities object (streaming, skills, etc.)
762
997
  All methods return Promises. Prefer these helpers over raw fetch().`;
763
- const repairUSER_MESSAGE = `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).
764
-
765
- Below is the CURRENT state of the page after the successful operations were applied, followed by the list of operations that failed and why.
766
-
767
- 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.
768
- If a failed operation is no longer needed (e.g. the intended change was already accomplished by another op), omit it.
769
- Return an empty JSON array [] if no repairs are needed.
770
-
771
- Return ONLY a JSON array of change operations using the same format:
772
- { "op": "update", "nodeId": "<data-node-id>", "html": "<new innerHTML>" }
773
- { "op": "replace", "nodeId": "<data-node-id>", "html": "<new outerHTML>" }
774
- { "op": "delete", "nodeId": "<data-node-id>" }
775
- { "op": "insert", "parentId": "<data-node-id>", "position": "prepend"|"append"|"before"|"after", "html": "<new element HTML>" }
776
- { "op": "style-element", "nodeId": "<data-node-id>", "style": "<css style string>" }`;
777
998
  //# sourceMappingURL=transformPage.js.map