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
@@ -9,11 +9,12 @@ import {
9
9
  parseChangeList,
10
10
  injectError,
11
11
  deduplicateInlineScripts,
12
+ normalizedIndexOf,
12
13
  transformPage,
13
14
  ChangeList,
14
15
  TransformPageArgs,
15
16
  } from '../src/service/transformPage';
16
- import { AgentCompletion, PromptCompletionArgs } from '../src/models/types';
17
+ import { Builder, BuilderResult, ContextSection } from '../src/builders/types';
17
18
 
18
19
  // ---------------------------------------------------------------------------
19
20
  // assignNodeIds
@@ -456,12 +457,142 @@ describe('deduplicateInlineScripts — ID-based dedup', () => {
456
457
  });
457
458
 
458
459
  // ---------------------------------------------------------------------------
459
- // transformPage (integration with stub completePrompt)
460
+ // normalizedIndexOf
460
461
  // ---------------------------------------------------------------------------
461
462
 
462
- describe('transformPage', () => {
463
- let tmpDir: string;
463
+ describe('normalizedIndexOf', () => {
464
+ it('returns null when needle is not found', () => {
465
+ assert.strictEqual(normalizedIndexOf('hello world', 'xyz'), null);
466
+ });
467
+
468
+ it('finds exact match and returns correct positions', () => {
469
+ const result = normalizedIndexOf('let x = 1;', 'x = 1');
470
+ assert.ok(result !== null);
471
+ assert.strictEqual(result.start, 4);
472
+ assert.strictEqual(result.end, 9);
473
+ });
474
+
475
+ it('matches despite whitespace differences', () => {
476
+ const haystack = 'let x =\n 1;';
477
+ const needle = 'x = 1;';
478
+ const result = normalizedIndexOf(haystack, needle);
479
+ assert.ok(result !== null);
480
+ // Should span from 'x' to end of ';'
481
+ const matched = haystack.slice(result.start, result.end);
482
+ assert.ok(matched.includes('x'));
483
+ assert.ok(matched.includes('1;'));
484
+ });
485
+
486
+ it('handles newlines vs spaces', () => {
487
+ const haystack = 'function foo() {\n return 1;\n}';
488
+ const needle = 'foo() { return 1; }';
489
+ const result = normalizedIndexOf(haystack, needle);
490
+ assert.ok(result !== null);
491
+ });
492
+ });
493
+
494
+ // ---------------------------------------------------------------------------
495
+ // applyChangeList — search-replace / search-insert ops
496
+ // ---------------------------------------------------------------------------
464
497
 
498
+ describe('applyChangeList — search-replace / search-insert ops', () => {
499
+ const scriptHtml = '<html><head></head><body>' +
500
+ '<script data-node-id="5">let a = 1;\nlet b = 2;\nlet c = 3;\nlet d = 4;\nlet e = 5;</script>' +
501
+ '</body></html>';
502
+
503
+ it('search-replace replaces exact text match', () => {
504
+ const changes: ChangeList = [
505
+ { op: 'search-replace', nodeId: '5', search: 'let b = 2;', replace: 'let b = 20;' },
506
+ ];
507
+ const result = applyChangeList(scriptHtml, changes);
508
+ assert.ok(result.includes('let b = 20;'));
509
+ assert.ok(!result.includes('let b = 2;'));
510
+ assert.ok(result.includes('let a = 1;'));
511
+ assert.ok(result.includes('let c = 3;'));
512
+ });
513
+
514
+ it('search-replace with empty replace deletes text', () => {
515
+ const changes: ChangeList = [
516
+ { op: 'search-replace', nodeId: '5', search: 'let c = 3;\n', replace: '' },
517
+ ];
518
+ const result = applyChangeList(scriptHtml, changes);
519
+ assert.ok(!result.includes('let c = 3;'));
520
+ assert.ok(result.includes('let b = 2;'));
521
+ assert.ok(result.includes('let d = 4;'));
522
+ });
523
+
524
+ it('search-replace falls back to normalized match', () => {
525
+ // Script has single spaces, search has different whitespace
526
+ const html = '<html><head></head><body>' +
527
+ '<script data-node-id="5">function foo() {\n return 1;\n}</script>' +
528
+ '</body></html>';
529
+ const changes: ChangeList = [
530
+ { op: 'search-replace', nodeId: '5', search: 'function foo() { return 1; }', replace: 'function foo() { return 2; }' },
531
+ ];
532
+ const result = applyChangeList(html, changes);
533
+ assert.ok(result.includes('return 2;'));
534
+ });
535
+
536
+ it('search-replace warns on no match found', () => {
537
+ const changes: ChangeList = [
538
+ { op: 'search-replace', nodeId: '5', search: 'nonexistent text', replace: 'replacement' },
539
+ ];
540
+ // Should not throw
541
+ const result = applyChangeList(scriptHtml, changes);
542
+ // Original content preserved
543
+ assert.ok(result.includes('let a = 1;'));
544
+ assert.ok(!result.includes('replacement'));
545
+ });
546
+
547
+ it('search-replace works on style blocks', () => {
548
+ const styleHtml = '<html><head><style data-node-id="3">.a { color: red; }\n.b { color: blue; }\n.c { color: green; }</style></head><body></body></html>';
549
+ const changes: ChangeList = [
550
+ { op: 'search-replace', nodeId: '3', search: '.b { color: blue; }', replace: '.b { color: purple; }' },
551
+ ];
552
+ const result = applyChangeList(styleHtml, changes);
553
+ assert.ok(result.includes('.b { color: purple; }'));
554
+ assert.ok(!result.includes('color: blue'));
555
+ });
556
+
557
+ it('search-insert inserts after matched text', () => {
558
+ const changes: ChangeList = [
559
+ { op: 'search-insert', nodeId: '5', after: 'let b = 2;', content: '\nlet inserted = true;' },
560
+ ];
561
+ const result = applyChangeList(scriptHtml, changes);
562
+ assert.ok(result.includes('let inserted = true;'));
563
+ const idxB = result.indexOf('let b = 2;');
564
+ const idxInserted = result.indexOf('let inserted = true;');
565
+ const idxC = result.indexOf('let c = 3;');
566
+ assert.ok(idxB < idxInserted);
567
+ assert.ok(idxInserted < idxC);
568
+ });
569
+
570
+ it('search-insert warns on no match found', () => {
571
+ const changes: ChangeList = [
572
+ { op: 'search-insert', nodeId: '5', after: 'nonexistent text', content: '\nnew stuff' },
573
+ ];
574
+ const result = applyChangeList(scriptHtml, changes);
575
+ assert.ok(result.includes('let a = 1;'));
576
+ assert.ok(!result.includes('new stuff'));
577
+ });
578
+
579
+ it('warns but does not throw on missing node for search ops', () => {
580
+ const ops: ChangeList = [
581
+ { op: 'search-replace', nodeId: '999', search: 'x', replace: 'y' },
582
+ { op: 'search-insert', nodeId: '999', after: 'x', content: 'y' },
583
+ ];
584
+ for (const change of ops) {
585
+ const result = applyChangeList(scriptHtml, [change]);
586
+ assert.ok(result.includes('let a = 1;'));
587
+ }
588
+ });
589
+ });
590
+
591
+ // ---------------------------------------------------------------------------
592
+ // transformPage (integration with stub Builder)
593
+ // ---------------------------------------------------------------------------
594
+
595
+ describe('transformPage', () => {
465
596
  // Minimal page with a viewer-panel and thoughts div
466
597
  const testPage = `<html><head></head><body>
467
598
  <div class="chat-panel" data-locked>
@@ -471,14 +602,6 @@ describe('transformPage', () => {
471
602
  <div id="thoughts" style="display: none;"></div>
472
603
  </body></html>`;
473
604
 
474
- beforeEach(async () => {
475
- tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'synthos-tp-test-'));
476
- });
477
-
478
- afterEach(async () => {
479
- await fs.rm(tmpDir, { recursive: true, force: true });
480
- });
481
-
482
605
  /** Extract the data-node-id for an element identified by a CSS-style attribute (e.g. id="content") from annotated HTML. */
483
606
  function findNodeId(annotatedHtml: string, idAttr: string): string {
484
607
  // Match a tag that contains both data-node-id="X" and the target id, in either order
@@ -488,28 +611,30 @@ describe('transformPage', () => {
488
611
  return m ? m[1] : '99999';
489
612
  }
490
613
 
491
- function makeArgs(stub: (args: PromptCompletionArgs) => Promise<AgentCompletion<string>>, pageState?: string): TransformPageArgs {
614
+ /** Create a stub builder that calls the given handler to produce a BuilderResult. */
615
+ function makeBuilder(handler: (currentPage: ContextSection, additionalSections: ContextSection[], userMessage: string, newBuild: boolean) => Promise<BuilderResult>): Builder {
616
+ return { run: handler };
617
+ }
618
+
619
+ function makeArgs(builder: Builder, pageState?: string): TransformPageArgs {
492
620
  return {
493
- completePrompt: stub,
494
- pagesFolder: tmpDir,
621
+ builder,
622
+ additionalSections: [],
495
623
  pageState: pageState ?? testPage,
496
624
  message: 'Change the content',
497
625
  };
498
626
  }
499
627
 
500
628
  it('happy path — applies valid change list and returns transformed HTML', async () => {
501
- const stub = async (args: PromptCompletionArgs): Promise<AgentCompletion<string>> => {
502
- const sys = args.system?.content ?? '';
503
- const nodeId = findNodeId(sys, 'id="content"');
629
+ const builder = makeBuilder(async (currentPage) => {
630
+ const nodeId = findNodeId(currentPage.content, 'id="content"');
504
631
  return {
505
- completed: true,
506
- value: JSON.stringify([
507
- { op: 'update', nodeId, html: 'Updated content' },
508
- ]),
632
+ kind: 'transforms',
633
+ changes: [{ op: 'update', nodeId, html: 'Updated content' }],
509
634
  };
510
- };
635
+ });
511
636
 
512
- const result = await transformPage(makeArgs(stub));
637
+ const result = await transformPage(makeArgs(builder));
513
638
  assert.strictEqual(result.completed, true);
514
639
  assert.ok(result.value);
515
640
  assert.ok(result.value.html.includes('Updated content'));
@@ -518,239 +643,72 @@ describe('transformPage', () => {
518
643
  assert.ok(!result.value.html.includes('data-node-id'));
519
644
  });
520
645
 
521
- it('returns error when completePrompt fails', async () => {
522
- const stub = async (): Promise<AgentCompletion<string>> => ({
523
- completed: false,
646
+ it('returns error HTML when builder returns error', async () => {
647
+ const builder = makeBuilder(async () => ({
648
+ kind: 'error',
524
649
  error: new Error('API quota exceeded'),
525
- });
650
+ }));
526
651
 
527
- const result = await transformPage(makeArgs(stub));
528
- assert.strictEqual(result.completed, false);
529
- assert.strictEqual(result.error?.message, 'API quota exceeded');
530
- });
531
-
532
- it('injects error block when response is not valid JSON', async () => {
533
- const stub = async (): Promise<AgentCompletion<string>> => ({
534
- completed: true,
535
- value: 'I cannot help with that request.',
536
- });
537
-
538
- const result = await transformPage(makeArgs(stub));
539
- // Should still complete (error is injected into HTML, not thrown)
652
+ const result = await transformPage(makeArgs(builder));
540
653
  assert.strictEqual(result.completed, true);
541
654
  assert.ok(result.value);
542
- assert.strictEqual(result.value.changeCount, 0);
543
655
  assert.ok(result.value.html.includes('id="error"'));
656
+ assert.strictEqual(result.value.changeCount, 0);
544
657
  });
545
658
 
546
- it('handles failed ops and triggers repair pass', async () => {
547
- let callCount = 0;
548
- const stub = async (args: PromptCompletionArgs): Promise<AgentCompletion<string>> => {
549
- callCount++;
550
- const sys = args.system?.content ?? '';
551
- if (callCount === 1) {
552
- const contentNodeId = findNodeId(sys, 'id="content"');
553
- return {
554
- completed: true,
555
- value: JSON.stringify([
556
- { op: 'update', nodeId: contentNodeId, html: 'First pass change' },
557
- { op: 'update', nodeId: '9999', html: 'Ghost node' }, // will fail
558
- ]),
559
- };
560
- } else {
561
- // Repair call: target an element that exists in re-annotated HTML
562
- const thoughtsNodeId = findNodeId(sys, 'id="thoughts"');
563
- return {
564
- completed: true,
565
- value: JSON.stringify([
566
- { op: 'update', nodeId: thoughtsNodeId, html: 'Repaired content' },
567
- ]),
568
- };
569
- }
570
- };
571
-
572
- const result = await transformPage(makeArgs(stub));
573
- assert.strictEqual(result.completed, true);
574
- assert.ok(result.value);
575
- // Should have made 2 calls (initial + repair)
576
- assert.strictEqual(callCount, 2);
577
- // changeCount should be 2 (1 success from first pass + 1 from repair)
578
- assert.strictEqual(result.value.changeCount, 2);
579
- });
580
-
581
- it('keeps partial result when repair pass LLM call fails', async () => {
582
- let callCount = 0;
583
- const stub = async (args: PromptCompletionArgs): Promise<AgentCompletion<string>> => {
584
- callCount++;
585
- if (callCount === 1) {
586
- const sys = args.system?.content ?? '';
587
- const contentNodeId = findNodeId(sys, 'id="content"');
588
- return {
589
- completed: true,
590
- value: JSON.stringify([
591
- { op: 'update', nodeId: contentNodeId, html: 'Partial update' },
592
- { op: 'update', nodeId: '9999', html: 'Ghost' },
593
- ]),
594
- };
595
- } else {
596
- // Repair call fails
597
- return { completed: false, error: new Error('Repair failed') };
598
- }
599
- };
659
+ it('handles reply result by appending chat messages', async () => {
660
+ const builder = makeBuilder(async () => ({
661
+ kind: 'reply',
662
+ text: 'I cannot help with that.',
663
+ }));
600
664
 
601
- const result = await transformPage(makeArgs(stub));
665
+ const result = await transformPage({
666
+ ...makeArgs(builder),
667
+ productName: 'SynthOS',
668
+ });
602
669
  assert.strictEqual(result.completed, true);
603
670
  assert.ok(result.value);
604
- assert.ok(result.value.html.includes('Partial update'));
605
- assert.strictEqual(result.value.changeCount, 1);
606
- });
607
-
608
- it('handles repair pass returning empty array (no repairs needed)', async () => {
609
- let callCount = 0;
610
- const stub = async (args: PromptCompletionArgs): Promise<AgentCompletion<string>> => {
611
- callCount++;
612
- if (callCount === 1) {
613
- return {
614
- completed: true,
615
- value: JSON.stringify([
616
- { op: 'update', nodeId: '9999', html: 'Ghost' },
617
- ]),
618
- };
619
- } else {
620
- return { completed: true, value: '[]' };
621
- }
622
- };
623
-
624
- const result = await transformPage(makeArgs(stub));
625
- assert.strictEqual(result.completed, true);
626
- assert.strictEqual(callCount, 2);
627
- });
628
-
629
- it('includes instructions and modelInstructions in prompt', async () => {
630
- let capturedPrompt: string | undefined;
631
- const stub = async (args: PromptCompletionArgs): Promise<AgentCompletion<string>> => {
632
- capturedPrompt = args.prompt.content;
633
- return { completed: true, value: '[]' };
634
- };
635
-
636
- await transformPage({
637
- ...makeArgs(stub),
638
- instructions: 'Be creative',
639
- modelInstructions: 'Return concise JSON',
640
- });
641
-
642
- assert.ok(capturedPrompt);
643
- assert.ok(capturedPrompt.includes('Be creative'));
644
- assert.ok(capturedPrompt.includes('Return concise JSON'));
645
- });
646
-
647
- it('includes theme info in system prompt when provided', async () => {
648
- let capturedSystem: string | undefined;
649
- const stub = async (args: PromptCompletionArgs): Promise<AgentCompletion<string>> => {
650
- capturedSystem = args.system?.content;
651
- return { completed: true, value: '[]' };
652
- };
653
-
654
- await transformPage({
655
- ...makeArgs(stub),
656
- themeInfo: {
657
- mode: 'dark',
658
- colors: { 'accent-primary': '#ff0000', 'text-primary': '#ffffff' },
659
- },
660
- });
661
-
662
- assert.ok(capturedSystem);
663
- assert.ok(capturedSystem.includes('Mode: dark'));
664
- assert.ok(capturedSystem.includes('--accent-primary: #ff0000'));
665
- });
666
-
667
- it('includes connector info in system prompt when provided', async () => {
668
- let capturedSystem: string | undefined;
669
- const stub = async (args: PromptCompletionArgs): Promise<AgentCompletion<string>> => {
670
- capturedSystem = args.system?.content;
671
- return { completed: true, value: '[]' };
672
- };
673
-
674
- await transformPage({
675
- ...makeArgs(stub),
676
- configuredConnectors: {
677
- 'brave-search': { enabled: true, apiKey: 'test-key' } as any,
678
- },
679
- });
680
-
681
- assert.ok(capturedSystem);
682
- assert.ok(capturedSystem.includes('CONFIGURED_CONNECTORS'));
683
- });
684
-
685
- it('includes agent info in system prompt when provided', async () => {
686
- let capturedSystem: string | undefined;
687
- const stub = async (args: PromptCompletionArgs): Promise<AgentCompletion<string>> => {
688
- capturedSystem = args.system?.content;
689
- return { completed: true, value: '[]' };
690
- };
691
-
692
- await transformPage({
693
- ...makeArgs(stub),
694
- configuredAgents: [{
695
- id: 'test-agent',
696
- name: 'Test Agent',
697
- description: 'A test agent',
698
- url: 'http://localhost:3000',
699
- enabled: true,
700
- provider: 'a2a' as any,
701
- }],
702
- });
703
-
704
- assert.ok(capturedSystem);
705
- assert.ok(capturedSystem.includes('CONFIGURED_AGENTS'));
706
- assert.ok(capturedSystem.includes('Test Agent'));
671
+ assert.ok(result.value.html.includes('User:'));
672
+ assert.ok(result.value.html.includes('SynthOS:'));
673
+ assert.ok(result.value.html.includes('I cannot help with that.'));
674
+ assert.strictEqual(result.value.changeCount, 0);
707
675
  });
708
676
 
709
- it('includes agent capabilities and skills in system prompt', async () => {
710
- let capturedSystem: string | undefined;
711
- const stub = async (args: PromptCompletionArgs): Promise<AgentCompletion<string>> => {
712
- capturedSystem = args.system?.content;
713
- return { completed: true, value: '[]' };
714
- };
715
-
716
- await transformPage({
717
- ...makeArgs(stub),
718
- configuredAgents: [{
719
- id: 'agent-1',
720
- name: 'Streaming Agent',
721
- description: 'Agent with streaming',
722
- url: 'http://localhost:3000',
723
- enabled: true,
724
- provider: 'a2a',
725
- capabilities: { streaming: true },
726
- skills: [
727
- { id: 'summarize', name: 'summarize', description: 'Summarizes text', tags: [] },
677
+ it('handles missing nodes gracefully (no repair pass)', async () => {
678
+ const builder = makeBuilder(async (currentPage) => {
679
+ const contentNodeId = findNodeId(currentPage.content, 'id="content"');
680
+ return {
681
+ kind: 'transforms',
682
+ changes: [
683
+ { op: 'update', nodeId: contentNodeId, html: 'First pass change' },
684
+ { op: 'update', nodeId: '9999', html: 'Ghost node' }, // will be skipped
728
685
  ],
729
- }],
686
+ };
730
687
  });
731
688
 
732
- assert.ok(capturedSystem);
733
- assert.ok(capturedSystem.includes('Supports streaming: yes'));
734
- assert.ok(capturedSystem.includes('summarize'));
735
- assert.ok(capturedSystem.includes('Summarizes text'));
689
+ const result = await transformPage(makeArgs(builder));
690
+ assert.strictEqual(result.completed, true);
691
+ assert.ok(result.value);
692
+ assert.ok(result.value.html.includes('First pass change'));
693
+ // changeCount counts all ops, including skipped ones
694
+ assert.strictEqual(result.value.changeCount, 2);
736
695
  });
737
696
 
738
697
  it('exercises replace, delete, insert, and style-element ops through pipeline', async () => {
739
- const stub = async (args: PromptCompletionArgs): Promise<AgentCompletion<string>> => {
740
- const sys = args.system?.content ?? '';
741
- const contentNodeId = findNodeId(sys, 'id="content"');
742
- const viewerNodeId = findNodeId(sys, 'class="viewer-panel"');
698
+ const builder = makeBuilder(async (currentPage) => {
699
+ const contentNodeId = findNodeId(currentPage.content, 'id="content"');
700
+ const viewerNodeId = findNodeId(currentPage.content, 'class="viewer-panel"');
743
701
  return {
744
- completed: true,
745
- value: JSON.stringify([
702
+ kind: 'transforms',
703
+ changes: [
746
704
  { op: 'replace', nodeId: contentNodeId, html: '<p id="content">Replaced</p>' },
747
705
  { op: 'insert', parentId: viewerNodeId, position: 'append', html: '<span>Appended</span>' },
748
706
  { op: 'style-element', nodeId: viewerNodeId, style: 'background: blue' },
749
- ]),
707
+ ],
750
708
  };
751
- };
709
+ });
752
710
 
753
- const result = await transformPage(makeArgs(stub));
711
+ const result = await transformPage(makeArgs(builder));
754
712
  assert.strictEqual(result.completed, true);
755
713
  assert.ok(result.value);
756
714
  assert.ok(result.value.html.includes('Replaced'));
@@ -759,8 +717,7 @@ describe('transformPage', () => {
759
717
  assert.strictEqual(result.value.changeCount, 3);
760
718
  });
761
719
 
762
- it('reports failed replace on locked element and triggers repair', async () => {
763
- // Use a page where the content is data-locked
720
+ it('skips replace on locked element', async () => {
764
721
  const lockedPage = `<html><head></head><body>
765
722
  <div class="chat-panel" data-locked>
766
723
  <div id="chatMessages"><div class="chat-message"><p><strong>SynthOS:</strong> Welcome!</p></div></div>
@@ -769,77 +726,20 @@ describe('transformPage', () => {
769
726
  <div id="thoughts" style="display: none;"></div>
770
727
  </body></html>`;
771
728
 
772
- let callCount = 0;
773
- const stub = async (args: PromptCompletionArgs): Promise<AgentCompletion<string>> => {
774
- callCount++;
775
- const sys = args.system?.content ?? '';
776
- if (callCount === 1) {
777
- const contentNodeId = findNodeId(sys, 'id="content"');
778
- return {
779
- completed: true,
780
- value: JSON.stringify([
781
- { op: 'replace', nodeId: contentNodeId, html: '<p>Should fail</p>' },
782
- ]),
783
- };
784
- } else {
785
- // Repair: return empty array (nothing to fix)
786
- return { completed: true, value: '[]' };
787
- }
788
- };
729
+ const builder = makeBuilder(async (currentPage) => {
730
+ const contentNodeId = findNodeId(currentPage.content, 'id="content"');
731
+ return {
732
+ kind: 'transforms',
733
+ changes: [{ op: 'replace', nodeId: contentNodeId, html: '<p>Should fail</p>' }],
734
+ };
735
+ });
789
736
 
790
- const result = await transformPage(makeArgs(stub, lockedPage));
737
+ const result = await transformPage(makeArgs(builder, lockedPage));
791
738
  assert.strictEqual(result.completed, true);
792
- assert.strictEqual(callCount, 2);
793
- // Original locked content should still be present
794
739
  assert.ok(result.value!.html.includes('Locked content'));
795
740
  });
796
741
 
797
- it('handles repair pass that throws an error', async () => {
798
- let callCount = 0;
799
- const stub = async (args: PromptCompletionArgs): Promise<AgentCompletion<string>> => {
800
- callCount++;
801
- if (callCount === 1) {
802
- return {
803
- completed: true,
804
- value: JSON.stringify([
805
- { op: 'update', nodeId: '9999', html: 'Ghost' },
806
- ]),
807
- };
808
- } else {
809
- // Repair call returns invalid JSON that will cause parseChangeList to throw
810
- return { completed: true, value: 'not valid json at all' };
811
- }
812
- };
813
-
814
- const result = await transformPage(makeArgs(stub));
815
- assert.strictEqual(result.completed, true);
816
- assert.strictEqual(callCount, 2);
817
- // Should have kept partial result from first pass
818
- assert.ok(result.value);
819
- });
820
-
821
- it('reports failed insert on missing parent through pipeline', async () => {
822
- let callCount = 0;
823
- const stub = async (args: PromptCompletionArgs): Promise<AgentCompletion<string>> => {
824
- callCount++;
825
- if (callCount === 1) {
826
- return {
827
- completed: true,
828
- value: JSON.stringify([
829
- { op: 'insert', parentId: '9999', position: 'append', html: '<span>Ghost</span>' },
830
- ]),
831
- };
832
- } else {
833
- return { completed: true, value: '[]' };
834
- }
835
- };
836
-
837
- const result = await transformPage(makeArgs(stub));
838
- assert.strictEqual(result.completed, true);
839
- assert.strictEqual(callCount, 2);
840
- });
841
-
842
- it('reports failed delete on locked element through pipeline', async () => {
742
+ it('skips delete on locked element', async () => {
843
743
  const lockedPage = `<html><head></head><body>
844
744
  <div class="chat-panel" data-locked>
845
745
  <div id="chatMessages"><div class="chat-message"><p><strong>SynthOS:</strong> Welcome!</p></div></div>
@@ -848,29 +748,20 @@ describe('transformPage', () => {
848
748
  <div id="thoughts" style="display: none;"></div>
849
749
  </body></html>`;
850
750
 
851
- let callCount = 0;
852
- const stub = async (args: PromptCompletionArgs): Promise<AgentCompletion<string>> => {
853
- callCount++;
854
- const sys = args.system?.content ?? '';
855
- if (callCount === 1) {
856
- const contentNodeId = findNodeId(sys, 'id="content"');
857
- return {
858
- completed: true,
859
- value: JSON.stringify([
860
- { op: 'delete', nodeId: contentNodeId },
861
- ]),
862
- };
863
- } else {
864
- return { completed: true, value: '[]' };
865
- }
866
- };
751
+ const builder = makeBuilder(async (currentPage) => {
752
+ const contentNodeId = findNodeId(currentPage.content, 'id="content"');
753
+ return {
754
+ kind: 'transforms',
755
+ changes: [{ op: 'delete', nodeId: contentNodeId }],
756
+ };
757
+ });
867
758
 
868
- const result = await transformPage(makeArgs(stub, lockedPage));
759
+ const result = await transformPage(makeArgs(builder, lockedPage));
869
760
  assert.strictEqual(result.completed, true);
870
761
  assert.ok(result.value!.html.includes('Locked'));
871
762
  });
872
763
 
873
- it('reports failed style-element on locked element through pipeline', async () => {
764
+ it('skips style-element on locked element', async () => {
874
765
  const lockedPage = `<html><head></head><body>
875
766
  <div class="chat-panel" data-locked>
876
767
  <div id="chatMessages"><div class="chat-message"><p><strong>SynthOS:</strong> Welcome!</p></div></div>
@@ -879,53 +770,101 @@ describe('transformPage', () => {
879
770
  <div id="thoughts" style="display: none;"></div>
880
771
  </body></html>`;
881
772
 
882
- let callCount = 0;
883
- const stub = async (args: PromptCompletionArgs): Promise<AgentCompletion<string>> => {
884
- callCount++;
885
- const sys = args.system?.content ?? '';
886
- if (callCount === 1) {
887
- const contentNodeId = findNodeId(sys, 'id="content"');
888
- return {
889
- completed: true,
890
- value: JSON.stringify([
891
- { op: 'style-element', nodeId: contentNodeId, style: 'color: red' },
892
- ]),
893
- };
894
- } else {
895
- return { completed: true, value: '[]' };
896
- }
897
- };
773
+ const builder = makeBuilder(async (currentPage) => {
774
+ const contentNodeId = findNodeId(currentPage.content, 'id="content"');
775
+ return {
776
+ kind: 'transforms',
777
+ changes: [{ op: 'style-element', nodeId: contentNodeId, style: 'color: red' }],
778
+ };
779
+ });
898
780
 
899
- const result = await transformPage(makeArgs(stub, lockedPage));
781
+ const result = await transformPage(makeArgs(builder, lockedPage));
900
782
  assert.strictEqual(result.completed, true);
901
- // Locked element should not have the style applied
902
783
  assert.ok(!result.value!.html.includes('color: red'));
903
784
  });
904
785
 
905
- it('repair pass with remaining failures keeps partial result', async () => {
906
- let callCount = 0;
907
- const stub = async (args: PromptCompletionArgs): Promise<AgentCompletion<string>> => {
908
- callCount++;
909
- if (callCount === 1) {
910
- return {
911
- completed: true,
912
- value: JSON.stringify([
913
- { op: 'update', nodeId: '9999', html: 'Ghost' },
914
- ]),
915
- };
916
- } else {
917
- // Repair also fails targets non-existent node
918
- return {
919
- completed: true,
920
- value: JSON.stringify([
921
- { op: 'update', nodeId: '8888', html: 'Still ghost' },
922
- ]),
923
- };
924
- }
925
- };
786
+ it('catches exceptions from builder and injects error', async () => {
787
+ const builder = makeBuilder(async () => {
788
+ throw new Error('Unexpected builder crash');
789
+ });
790
+
791
+ const result = await transformPage(makeArgs(builder));
792
+ assert.strictEqual(result.completed, true);
793
+ assert.ok(result.value);
794
+ assert.ok(result.value.html.includes('id="error"'));
795
+ assert.strictEqual(result.value.changeCount, 0);
796
+ });
797
+
798
+ it('detects newBuild when isBuilder is true and only one chat message', async () => {
799
+ let capturedNewBuild: boolean | undefined;
800
+ const builder = makeBuilder(async (_cp, _as, _msg, newBuild) => {
801
+ capturedNewBuild = newBuild;
802
+ return { kind: 'transforms', changes: [] };
803
+ });
804
+
805
+ await transformPage({
806
+ ...makeArgs(builder),
807
+ isBuilder: true,
808
+ });
809
+
810
+ assert.strictEqual(capturedNewBuild, true);
811
+ });
812
+
813
+ it('detects existing build when multiple chat messages present', async () => {
814
+ const multiMessagePage = `<html><head></head><body>
815
+ <div class="chat-panel" data-locked>
816
+ <div id="chatMessages">
817
+ <div class="chat-message"><p><strong>SynthOS:</strong> Welcome!</p></div>
818
+ <div class="chat-message"><p><strong>User:</strong> Build me a todo app</p></div>
819
+ <div class="chat-message"><p><strong>SynthOS:</strong> Here you go!</p></div>
820
+ </div>
821
+ </div>
822
+ <div class="viewer-panel"><p>Content</p></div>
823
+ </body></html>`;
824
+
825
+ let capturedNewBuild: boolean | undefined;
826
+ const builder = makeBuilder(async (_cp, _as, _msg, newBuild) => {
827
+ capturedNewBuild = newBuild;
828
+ return { kind: 'transforms', changes: [] };
829
+ });
830
+
831
+ await transformPage({
832
+ ...makeArgs(builder, multiMessagePage),
833
+ isBuilder: true,
834
+ });
835
+
836
+ assert.strictEqual(capturedNewBuild, false);
837
+ });
926
838
 
927
- const result = await transformPage(makeArgs(stub));
839
+ it('applies search-replace through full pipeline', async () => {
840
+ const pageWithScript = `<html><head></head><body>
841
+ <div class="chat-panel" data-locked>
842
+ <div id="chatMessages"><div class="chat-message"><p><strong>SynthOS:</strong> Welcome!</p></div></div>
843
+ </div>
844
+ <div class="viewer-panel"><p id="content">Hello</p></div>
845
+ <script id="page-script">let count = 0;\nlet name = "test";\nfunction init() { return count; }</script>
846
+ </body></html>`;
847
+
848
+ const builder = makeBuilder(async (currentPage) => {
849
+ // The current page should NOT have line numbers
850
+ assert.ok(!currentPage.content.includes('01:'), 'currentPage should not contain line numbers');
851
+ // Find the script node id
852
+ const scriptNodeId = findNodeId(currentPage.content, 'id="page-script"');
853
+ return {
854
+ kind: 'transforms',
855
+ changes: [
856
+ { op: 'search-replace', nodeId: scriptNodeId, search: 'let count = 0;', replace: 'let count = 42;' },
857
+ ],
858
+ };
859
+ });
860
+
861
+ const result = await transformPage(makeArgs(builder, pageWithScript));
928
862
  assert.strictEqual(result.completed, true);
929
- assert.strictEqual(callCount, 2);
863
+ assert.ok(result.value);
864
+ // Edit should be applied
865
+ assert.ok(result.value.html.includes('let count = 42;'));
866
+ // Node ids should be stripped
867
+ assert.ok(!result.value.html.includes('data-node-id'));
868
+ assert.strictEqual(result.value.changeCount, 1);
930
869
  });
931
870
  });