synthos 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (368) hide show
  1. package/README.md +1 -1
  2. package/default-pages/application/page.html +42 -0
  3. package/default-pages/application/page.json +10 -0
  4. package/default-pages/elevenlabs_effects_studio/page.html +1363 -0
  5. package/default-pages/elevenlabs_effects_studio/page.json +11 -0
  6. package/default-pages/elevenlabs_voice_studio/page.html +801 -0
  7. package/default-pages/elevenlabs_voice_studio/page.json +11 -0
  8. package/default-pages/{json_tools.html → json_tools/page.html} +13 -11
  9. package/default-pages/json_tools/page.json +10 -0
  10. package/default-pages/my_notes/notes/a1b2c3d4-e5f6-7890-abcd-ef1234567890.json +5 -0
  11. package/default-pages/my_notes/page.html +132 -0
  12. package/default-pages/{my_notes.json → my_notes/page.json} +2 -2
  13. package/default-pages/neon_asteroids/files/Ambient_Space.mp3 +0 -0
  14. package/default-pages/neon_asteroids/files/Ambient_Space2.mp3 +0 -0
  15. package/default-pages/neon_asteroids/files/Ambient_Space3.mp3 +0 -0
  16. package/default-pages/neon_asteroids/files/Asteroid_Explosion.mp3 +0 -0
  17. package/default-pages/neon_asteroids/files/Hyperspace_Jump.mp3 +0 -0
  18. package/default-pages/neon_asteroids/files/Laser_Fire.mp3 +0 -0
  19. package/default-pages/neon_asteroids/files/Menu_Navigate.mp3 +0 -0
  20. package/default-pages/neon_asteroids/files/Power_Up_Collect.mp3 +0 -0
  21. package/default-pages/neon_asteroids/files/Saucer_Alert.mp3 +0 -0
  22. package/default-pages/neon_asteroids/files/Ship_Thrust.mp3 +0 -0
  23. package/default-pages/neon_asteroids/files/effects.json +74 -0
  24. package/default-pages/neon_asteroids/page.html +1803 -0
  25. package/default-pages/{neon_asteroids.json → neon_asteroids/page.json} +3 -3
  26. package/default-pages/{oregon_trail.html → oregon_trail/page.html} +16 -30
  27. package/default-pages/{oregon_trail.json → oregon_trail/page.json} +2 -2
  28. package/default-pages/retro_game_starter/page.html +1308 -0
  29. package/default-pages/retro_game_starter/page.json +12 -0
  30. package/default-pages/{sidebar_page.html → sidebar_page/page.html} +12 -10
  31. package/default-pages/sidebar_page/page.json +10 -0
  32. package/default-pages/{solar_explorer.html → solar_explorer/page.html} +15 -12
  33. package/default-pages/{solar_explorer.json → solar_explorer/page.json} +2 -2
  34. package/default-pages/{solar_tutorial.html → solar_tutorial/page.html} +12 -10
  35. package/default-pages/solar_tutorial/page.json +10 -0
  36. package/default-pages/{two-panel_page.html → two-panel_page/page.html} +13 -11
  37. package/default-pages/two-panel_page/page.json +10 -0
  38. package/default-pages/{us_map.html → us_map/page.html} +193 -192
  39. package/default-pages/{us_map.json → us_map/page.json} +12 -12
  40. package/default-pages/{us_map_1850.html → us_map_1850/page.html} +326 -325
  41. package/default-pages/{us_map_1850.json → us_map_1850/page.json} +12 -12
  42. package/default-pages/{western_cities_1850.html → western_cities_1850/page.html} +527 -526
  43. package/default-pages/{western_cities_1850.json → western_cities_1850/page.json} +12 -12
  44. package/default-themes/aurora-dawn.json +19 -0
  45. package/default-themes/aurora-dawn.v3.css +198 -0
  46. package/default-themes/aurora-dusk.json +19 -0
  47. package/default-themes/aurora-dusk.v3.css +200 -0
  48. package/default-themes/cosmos-dawn.json +19 -0
  49. package/default-themes/cosmos-dawn.v3.css +198 -0
  50. package/default-themes/cosmos-dusk.json +19 -0
  51. package/default-themes/cosmos-dusk.v3.css +200 -0
  52. package/default-themes/high-contrast-dark.json +19 -0
  53. package/default-themes/high-contrast-dark.v3.css +200 -0
  54. package/default-themes/high-contrast-light.json +19 -0
  55. package/default-themes/high-contrast-light.v3.css +198 -0
  56. package/default-themes/nebula-dawn.v2.css +110 -0
  57. package/default-themes/nebula-dawn.v3.css +199 -0
  58. package/default-themes/nebula-dusk.v2.css +104 -0
  59. package/default-themes/nebula-dusk.v3.css +201 -0
  60. package/default-themes/solar-flare-dawn.json +19 -0
  61. package/default-themes/solar-flare-dawn.v3.css +198 -0
  62. package/default-themes/solar-flare-dusk.json +19 -0
  63. package/default-themes/solar-flare-dusk.v3.css +200 -0
  64. package/dist/agents/index.d.ts +1 -1
  65. package/dist/agents/index.d.ts.map +1 -1
  66. package/dist/agents/index.js +2 -1
  67. package/dist/agents/index.js.map +1 -1
  68. package/dist/agents/openclaw/gatewayManager.d.ts +4 -0
  69. package/dist/agents/openclaw/gatewayManager.d.ts.map +1 -1
  70. package/dist/agents/openclaw/gatewayManager.js +27 -11
  71. package/dist/agents/openclaw/gatewayManager.js.map +1 -1
  72. package/dist/agents/openclaw/openclawProvider.d.ts.map +1 -1
  73. package/dist/agents/openclaw/openclawProvider.js +2 -4
  74. package/dist/agents/openclaw/openclawProvider.js.map +1 -1
  75. package/dist/agents/openclaw/sshTunnelManager.d.ts +2 -0
  76. package/dist/agents/openclaw/sshTunnelManager.d.ts.map +1 -1
  77. package/dist/agents/openclaw/sshTunnelManager.js +31 -12
  78. package/dist/agents/openclaw/sshTunnelManager.js.map +1 -1
  79. package/dist/builders/anthropic.d.ts +31 -0
  80. package/dist/builders/anthropic.d.ts.map +1 -0
  81. package/dist/builders/anthropic.js +227 -0
  82. package/dist/builders/anthropic.js.map +1 -0
  83. package/dist/builders/fireworksai.d.ts +9 -0
  84. package/dist/builders/fireworksai.d.ts.map +1 -0
  85. package/dist/builders/fireworksai.js +57 -0
  86. package/dist/builders/fireworksai.js.map +1 -0
  87. package/dist/builders/index.d.ts +13 -0
  88. package/dist/builders/index.d.ts.map +1 -0
  89. package/dist/builders/index.js +31 -0
  90. package/dist/builders/index.js.map +1 -0
  91. package/dist/builders/openai.d.ts +8 -0
  92. package/dist/builders/openai.d.ts.map +1 -0
  93. package/dist/builders/openai.js +87 -0
  94. package/dist/builders/openai.js.map +1 -0
  95. package/dist/builders/types.d.ts +54 -0
  96. package/dist/builders/types.d.ts.map +1 -0
  97. package/dist/builders/types.js +211 -0
  98. package/dist/builders/types.js.map +1 -0
  99. package/dist/connectors/index.d.ts +1 -1
  100. package/dist/connectors/index.d.ts.map +1 -1
  101. package/dist/connectors/index.js +3 -2
  102. package/dist/connectors/index.js.map +1 -1
  103. package/dist/connectors/registry.d.ts +2 -1
  104. package/dist/connectors/registry.d.ts.map +1 -1
  105. package/dist/connectors/registry.js +31 -8
  106. package/dist/connectors/registry.js.map +1 -1
  107. package/dist/customizer/Customizer.d.ts +62 -0
  108. package/dist/customizer/Customizer.d.ts.map +1 -0
  109. package/dist/customizer/Customizer.js +134 -0
  110. package/dist/customizer/Customizer.js.map +1 -0
  111. package/dist/customizer/index.d.ts +4 -0
  112. package/dist/customizer/index.d.ts.map +1 -0
  113. package/dist/customizer/index.js +9 -0
  114. package/dist/customizer/index.js.map +1 -0
  115. package/dist/files.d.ts +16 -0
  116. package/dist/files.d.ts.map +1 -1
  117. package/dist/files.js +60 -1
  118. package/dist/files.js.map +1 -1
  119. package/dist/index.d.ts +2 -0
  120. package/dist/index.d.ts.map +1 -1
  121. package/dist/index.js +2 -0
  122. package/dist/index.js.map +1 -1
  123. package/dist/init.d.ts +12 -6
  124. package/dist/init.d.ts.map +1 -1
  125. package/dist/init.js +150 -133
  126. package/dist/init.js.map +1 -1
  127. package/dist/migrations.d.ts.map +1 -1
  128. package/dist/migrations.js +23 -10
  129. package/dist/migrations.js.map +1 -1
  130. package/dist/models/anthropic.d.ts +4 -2
  131. package/dist/models/anthropic.d.ts.map +1 -1
  132. package/dist/models/anthropic.js +33 -6
  133. package/dist/models/anthropic.js.map +1 -1
  134. package/dist/models/fireworksai.d.ts.map +1 -1
  135. package/dist/models/fireworksai.js +9 -1
  136. package/dist/models/fireworksai.js.map +1 -1
  137. package/dist/models/index.d.ts +1 -1
  138. package/dist/models/index.d.ts.map +1 -1
  139. package/dist/models/index.js +2 -1
  140. package/dist/models/index.js.map +1 -1
  141. package/dist/models/openai.d.ts +1 -1
  142. package/dist/models/openai.d.ts.map +1 -1
  143. package/dist/models/openai.js +24 -3
  144. package/dist/models/openai.js.map +1 -1
  145. package/dist/models/types.d.ts +20 -1
  146. package/dist/models/types.d.ts.map +1 -1
  147. package/dist/models/types.js +6 -1
  148. package/dist/models/types.js.map +1 -1
  149. package/dist/pages.d.ts +34 -10
  150. package/dist/pages.d.ts.map +1 -1
  151. package/dist/pages.js +229 -79
  152. package/dist/pages.js.map +1 -1
  153. package/dist/service/createCompletePrompt.d.ts +2 -1
  154. package/dist/service/createCompletePrompt.d.ts.map +1 -1
  155. package/dist/service/createCompletePrompt.js +2 -2
  156. package/dist/service/createCompletePrompt.js.map +1 -1
  157. package/dist/service/requiresSettings.d.ts +2 -1
  158. package/dist/service/requiresSettings.d.ts.map +1 -1
  159. package/dist/service/requiresSettings.js +3 -3
  160. package/dist/service/requiresSettings.js.map +1 -1
  161. package/dist/service/server.d.ts +2 -1
  162. package/dist/service/server.d.ts.map +1 -1
  163. package/dist/service/server.js +37 -8
  164. package/dist/service/server.js.map +1 -1
  165. package/dist/service/transformPage.d.ts +47 -20
  166. package/dist/service/transformPage.d.ts.map +1 -1
  167. package/dist/service/transformPage.js +514 -293
  168. package/dist/service/transformPage.js.map +1 -1
  169. package/dist/service/useAgentRoutes.d.ts +2 -1
  170. package/dist/service/useAgentRoutes.d.ts.map +1 -1
  171. package/dist/service/useAgentRoutes.js +17 -14
  172. package/dist/service/useAgentRoutes.js.map +1 -1
  173. package/dist/service/useApiRoutes.d.ts +2 -1
  174. package/dist/service/useApiRoutes.d.ts.map +1 -1
  175. package/dist/service/useApiRoutes.js +287 -172
  176. package/dist/service/useApiRoutes.js.map +1 -1
  177. package/dist/service/useConnectorRoutes.js +17 -17
  178. package/dist/service/useConnectorRoutes.js.map +1 -1
  179. package/dist/service/useDataRoutes.d.ts.map +1 -1
  180. package/dist/service/useDataRoutes.js +13 -10
  181. package/dist/service/useDataRoutes.js.map +1 -1
  182. package/dist/service/useFileRoutes.d.ts +4 -0
  183. package/dist/service/useFileRoutes.d.ts.map +1 -0
  184. package/dist/service/useFileRoutes.js +122 -0
  185. package/dist/service/useFileRoutes.js.map +1 -0
  186. package/dist/service/usePageRoutes.d.ts +2 -1
  187. package/dist/service/usePageRoutes.d.ts.map +1 -1
  188. package/dist/service/usePageRoutes.js +671 -74
  189. package/dist/service/usePageRoutes.js.map +1 -1
  190. package/dist/service/useSharedDataRoutes.d.ts +4 -0
  191. package/dist/service/useSharedDataRoutes.d.ts.map +1 -0
  192. package/dist/service/useSharedDataRoutes.js +107 -0
  193. package/dist/service/useSharedDataRoutes.js.map +1 -0
  194. package/dist/service/useSharedFileRoutes.d.ts +4 -0
  195. package/dist/service/useSharedFileRoutes.d.ts.map +1 -0
  196. package/dist/service/useSharedFileRoutes.js +121 -0
  197. package/dist/service/useSharedFileRoutes.js.map +1 -0
  198. package/dist/settings.d.ts +5 -3
  199. package/dist/settings.d.ts.map +1 -1
  200. package/dist/settings.js +12 -10
  201. package/dist/settings.js.map +1 -1
  202. package/dist/storage/FsStorageProvider.d.ts +25 -0
  203. package/dist/storage/FsStorageProvider.d.ts.map +1 -0
  204. package/dist/storage/FsStorageProvider.js +103 -0
  205. package/dist/storage/FsStorageProvider.js.map +1 -0
  206. package/dist/storage/StorageProvider.d.ts +31 -0
  207. package/dist/storage/StorageProvider.d.ts.map +1 -0
  208. package/dist/storage/StorageProvider.js +3 -0
  209. package/dist/storage/StorageProvider.js.map +1 -0
  210. package/dist/storage/index.d.ts +3 -0
  211. package/dist/storage/index.d.ts.map +1 -0
  212. package/dist/storage/index.js +6 -0
  213. package/dist/storage/index.js.map +1 -0
  214. package/dist/synthos-cli.d.ts.map +1 -1
  215. package/dist/synthos-cli.js +4 -3
  216. package/dist/synthos-cli.js.map +1 -1
  217. package/dist/themes.d.ts +1 -0
  218. package/dist/themes.d.ts.map +1 -1
  219. package/dist/themes.js +65 -28
  220. package/dist/themes.js.map +1 -1
  221. package/migration-rules/v1-to-v2.md +193 -0
  222. package/migration-rules/v2-to-v3.md +481 -0
  223. package/package.json +11 -10
  224. package/required-pages/builder/page.html +43 -0
  225. package/required-pages/builder/page.json +10 -0
  226. package/required-pages/{pages.html → pages/page.html} +238 -233
  227. package/required-pages/pages/page.json +10 -0
  228. package/required-pages/{settings.html → settings/page.html} +389 -275
  229. package/required-pages/settings/page.json +10 -0
  230. package/required-pages/synthos_apis/page.html +846 -0
  231. package/required-pages/synthos_apis/page.json +10 -0
  232. package/required-pages/{synthos_scripts.html → synthos_scripts/page.html} +13 -11
  233. package/required-pages/synthos_scripts/page.json +10 -0
  234. package/src/agents/index.ts +1 -1
  235. package/src/agents/openclaw/gatewayManager.ts +22 -11
  236. package/src/agents/openclaw/openclawProvider.ts +2 -4
  237. package/src/agents/openclaw/sshTunnelManager.ts +19 -11
  238. package/src/builders/anthropic.ts +283 -0
  239. package/src/builders/fireworksai.ts +59 -0
  240. package/src/builders/index.ts +33 -0
  241. package/src/builders/openai.ts +89 -0
  242. package/src/builders/types.ts +261 -0
  243. package/src/connectors/index.ts +1 -1
  244. package/src/connectors/registry.ts +28 -8
  245. package/src/customizer/Customizer.ts +163 -0
  246. package/src/customizer/index.ts +5 -0
  247. package/src/files.ts +57 -0
  248. package/src/index.ts +3 -1
  249. package/src/init.ts +195 -145
  250. package/src/migrations.ts +30 -10
  251. package/src/models/anthropic.ts +40 -10
  252. package/src/models/fireworksai.ts +9 -2
  253. package/src/models/index.ts +1 -1
  254. package/src/models/openai.ts +26 -6
  255. package/src/models/types.ts +31 -1
  256. package/src/pages.ts +230 -77
  257. package/src/service/createCompletePrompt.ts +3 -2
  258. package/src/service/requiresSettings.ts +4 -3
  259. package/src/service/server.ts +36 -9
  260. package/src/service/transformPage.ts +557 -326
  261. package/src/service/useAgentRoutes.ts +19 -14
  262. package/src/service/useApiRoutes.ts +208 -84
  263. package/src/service/useConnectorRoutes.ts +18 -18
  264. package/src/service/useDataRoutes.ts +13 -10
  265. package/src/service/useFileRoutes.ts +128 -0
  266. package/src/service/usePageRoutes.ts +730 -81
  267. package/src/service/useSharedDataRoutes.ts +109 -0
  268. package/src/service/useSharedFileRoutes.ts +127 -0
  269. package/src/settings.ts +14 -10
  270. package/src/storage/FsStorageProvider.ts +87 -0
  271. package/src/storage/StorageProvider.ts +34 -0
  272. package/src/storage/index.ts +2 -0
  273. package/src/synthos-cli.ts +4 -3
  274. package/src/themes.ts +64 -27
  275. package/static-files/favicon.svg +12 -0
  276. package/static-files/fluentlm-instructions.llmd +868 -0
  277. package/static-files/fluentlm-instructions.md +1595 -0
  278. package/static-files/fluentlm.css +4844 -0
  279. package/static-files/fluentlm.js +3602 -0
  280. package/static-files/fluentlm.min.css +1 -0
  281. package/static-files/fluentlm.min.js +1 -0
  282. package/{page-scripts/helpers-v2.js → static-files/helpers.v3.js} +82 -0
  283. package/static-files/page.v3.js +1290 -0
  284. package/static-files/recommended-frameworks.llmd +81 -0
  285. package/static-files/recommended-frameworks.md +137 -0
  286. package/static-files/retro-game.js +877 -0
  287. package/static-files/shell.css +797 -0
  288. package/static-files/theme-dark.css +169 -0
  289. package/static-files/theme-light.css +169 -0
  290. package/tests/builders.spec.ts +139 -0
  291. package/tests/pages.spec.ts +54 -84
  292. package/tests/transformPage.spec.ts +299 -360
  293. package/default-pages/application.html +0 -40
  294. package/default-pages/application.json +0 -1
  295. package/default-pages/json_tools.json +0 -1
  296. package/default-pages/my_notes.html +0 -33
  297. package/default-pages/neon_asteroids.html +0 -77
  298. package/default-pages/sidebar_page.json +0 -1
  299. package/default-pages/solar_tutorial.json +0 -1
  300. package/default-pages/two-panel_page.json +0 -1
  301. package/dist/service/useGatewayRoutes.d.ts +0 -4
  302. package/dist/service/useGatewayRoutes.d.ts.map +0 -1
  303. package/dist/service/useGatewayRoutes.js +0 -168
  304. package/dist/service/useGatewayRoutes.js.map +0 -1
  305. package/page-scripts/page-v2.js +0 -656
  306. package/required-pages/builder.html +0 -48
  307. package/required-pages/builder.json +0 -1
  308. package/required-pages/pages.json +0 -1
  309. package/required-pages/settings.json +0 -1
  310. package/required-pages/synthos_apis.html +0 -327
  311. package/required-pages/synthos_apis.json +0 -1
  312. package/required-pages/synthos_scripts.json +0 -1
  313. package/src/connectors/airtable/connector.json +0 -27
  314. package/src/connectors/alpha-vantage/connector.json +0 -26
  315. package/src/connectors/brave-search/connector.json +0 -26
  316. package/src/connectors/cloudinary/connector.json +0 -27
  317. package/src/connectors/deepl/connector.json +0 -28
  318. package/src/connectors/elevenlabs/connector.json +0 -30
  319. package/src/connectors/giphy/connector.json +0 -27
  320. package/src/connectors/github/connector.json +0 -29
  321. package/src/connectors/huggingface/connector.json +0 -27
  322. package/src/connectors/imgur/connector.json +0 -29
  323. package/src/connectors/instagram/connector.json +0 -43
  324. package/src/connectors/jira/connector.json +0 -28
  325. package/src/connectors/mapbox/connector.json +0 -26
  326. package/src/connectors/nasa/connector.json +0 -27
  327. package/src/connectors/newsapi/connector.json +0 -27
  328. package/src/connectors/notion/connector.json +0 -28
  329. package/src/connectors/open-exchange-rates/connector.json +0 -27
  330. package/src/connectors/openweathermap/connector.json +0 -26
  331. package/src/connectors/pexels/connector.json +0 -27
  332. package/src/connectors/resend/connector.json +0 -29
  333. package/src/connectors/rss2json/connector.json +0 -27
  334. package/src/connectors/sendgrid/connector.json +0 -27
  335. package/src/connectors/spoonacular/connector.json +0 -28
  336. package/src/connectors/stability-ai/connector.json +0 -27
  337. package/src/connectors/twilio/connector.json +0 -28
  338. package/src/connectors/unsplash/connector.json +0 -27
  339. package/src/connectors/wolfram-alpha/connector.json +0 -26
  340. package/src/connectors/youtube-data/connector.json +0 -30
  341. /package/{dist/connectors → service-connectors}/airtable/connector.json +0 -0
  342. /package/{dist/connectors → service-connectors}/alpha-vantage/connector.json +0 -0
  343. /package/{dist/connectors → service-connectors}/brave-search/connector.json +0 -0
  344. /package/{dist/connectors → service-connectors}/cloudinary/connector.json +0 -0
  345. /package/{dist/connectors → service-connectors}/deepl/connector.json +0 -0
  346. /package/{dist/connectors → service-connectors}/elevenlabs/connector.json +0 -0
  347. /package/{dist/connectors → service-connectors}/giphy/connector.json +0 -0
  348. /package/{dist/connectors → service-connectors}/github/connector.json +0 -0
  349. /package/{dist/connectors → service-connectors}/huggingface/connector.json +0 -0
  350. /package/{dist/connectors → service-connectors}/imgur/connector.json +0 -0
  351. /package/{dist/connectors → service-connectors}/instagram/connector.json +0 -0
  352. /package/{dist/connectors → service-connectors}/jira/connector.json +0 -0
  353. /package/{dist/connectors → service-connectors}/mapbox/connector.json +0 -0
  354. /package/{dist/connectors → service-connectors}/nasa/connector.json +0 -0
  355. /package/{dist/connectors → service-connectors}/newsapi/connector.json +0 -0
  356. /package/{dist/connectors → service-connectors}/notion/connector.json +0 -0
  357. /package/{dist/connectors → service-connectors}/open-exchange-rates/connector.json +0 -0
  358. /package/{dist/connectors → service-connectors}/openweathermap/connector.json +0 -0
  359. /package/{dist/connectors → service-connectors}/pexels/connector.json +0 -0
  360. /package/{dist/connectors → service-connectors}/resend/connector.json +0 -0
  361. /package/{dist/connectors → service-connectors}/rss2json/connector.json +0 -0
  362. /package/{dist/connectors → service-connectors}/sendgrid/connector.json +0 -0
  363. /package/{dist/connectors → service-connectors}/spoonacular/connector.json +0 -0
  364. /package/{dist/connectors → service-connectors}/stability-ai/connector.json +0 -0
  365. /package/{dist/connectors → service-connectors}/twilio/connector.json +0 -0
  366. /package/{dist/connectors → service-connectors}/unsplash/connector.json +0 -0
  367. /package/{dist/connectors → service-connectors}/wolfram-alpha/connector.json +0 -0
  368. /package/{dist/connectors → service-connectors}/youtube-data/connector.json +0 -0
@@ -1,5 +1,5 @@
1
1
  import Anthropic from '@anthropic-ai/sdk';
2
- import { AgentCompletion, completePrompt, PromptCompletionArgs, RequestError } from './types';
2
+ import { AgentCompletion, completePrompt, PromptCompletionArgs, RequestError, isMultimodalContent } from './types';
3
3
 
4
4
  export interface AnthropicArgs {
5
5
  apiKey: string;
@@ -14,25 +14,44 @@ export interface AnthropicArgs {
14
14
  * Pure function — no SDK dependency.
15
15
  */
16
16
  export function buildAnthropicRequest(args: PromptCompletionArgs, defaultTemp: number): {
17
- messages: { role: string; content: string }[];
18
- system: string | undefined;
17
+ messages: { role: string; content: string | Anthropic.ContentBlockParam[] }[];
18
+ system: string | Anthropic.TextBlockParam[] | undefined;
19
19
  temperature: number;
20
+ outputConfig?: Anthropic.OutputConfig;
20
21
  } {
21
22
  const reqTemp = args.temperature ?? defaultTemp;
22
23
 
23
- const messages: { role: string; content: string }[] = [];
24
+ const messages: { role: string; content: string | Anthropic.ContentBlockParam[] }[] = [];
24
25
  if (args.history) {
25
26
  for (const msg of args.history) {
26
27
  messages.push({ role: msg.role, content: msg.content });
27
28
  }
28
29
  }
29
30
 
30
- const useJsonPrefill = args.jsonMode || args.jsonSchema;
31
+ // Build user content multimodal when ContentBlock[] is provided
32
+ const promptContent = args.prompt.content;
33
+ let userContent: string | Anthropic.ContentBlockParam[];
34
+ if (isMultimodalContent(promptContent)) {
35
+ userContent = promptContent.map(block => {
36
+ if (block.type === 'text') {
37
+ return { type: 'text' as const, text: block.text };
38
+ }
39
+ return {
40
+ type: 'image' as const,
41
+ source: { type: 'base64' as const, media_type: block.mediaType as 'image/jpeg' | 'image/png' | 'image/gif' | 'image/webp', data: block.data },
42
+ };
43
+ });
44
+ } else {
45
+ userContent = promptContent;
46
+ }
47
+
48
+ // Structured output via output_config is incompatible with prefilling
49
+ const useJsonPrefill = !args.outputSchema && (args.jsonMode || args.jsonSchema);
31
50
  if (useJsonPrefill) {
32
- messages.push({ role: 'user', content: args.prompt.content });
51
+ messages.push({ role: 'user', content: userContent });
33
52
  messages.push({ role: 'assistant', content: '{' });
34
53
  } else {
35
- messages.push({ role: 'user', content: args.prompt.content });
54
+ messages.push({ role: 'user', content: userContent });
36
55
  }
37
56
 
38
57
  let system = args.system?.content;
@@ -41,7 +60,17 @@ export function buildAnthropicRequest(args: PromptCompletionArgs, defaultTemp: n
41
60
  system = system ? system + schemaInstruction : schemaInstruction;
42
61
  }
43
62
 
44
- return { messages, system, temperature: reqTemp };
63
+ // Wrap system content with cache_control for prompt caching
64
+ const finalSystem: string | Anthropic.TextBlockParam[] | undefined = (system && args.cacheSystem)
65
+ ? [{ type: 'text' as const, text: system, cache_control: { type: 'ephemeral' as const } }]
66
+ : system;
67
+
68
+ // Structured output config for constrained decoding
69
+ const outputConfig: Anthropic.OutputConfig | undefined = args.outputSchema
70
+ ? { format: { type: 'json_schema', schema: args.outputSchema } }
71
+ : undefined;
72
+
73
+ return { messages, system: finalSystem, temperature: reqTemp, outputConfig };
45
74
  }
46
75
 
47
76
  export function anthropic(args: AnthropicArgs): completePrompt {
@@ -50,9 +79,9 @@ export function anthropic(args: AnthropicArgs): completePrompt {
50
79
  const client = new Anthropic({ apiKey, baseURL, maxRetries });
51
80
 
52
81
  return async (completionArgs: PromptCompletionArgs): Promise<AgentCompletion<string>> => {
53
- const { messages, system: systemContent, temperature: reqTemp } = buildAnthropicRequest(completionArgs, temperature);
82
+ const { messages, system: systemContent, temperature: reqTemp, outputConfig } = buildAnthropicRequest(completionArgs, temperature);
54
83
 
55
- const useJsonPrefill = completionArgs.jsonMode || completionArgs.jsonSchema;
84
+ const useJsonPrefill = !completionArgs.outputSchema && (completionArgs.jsonMode || completionArgs.jsonSchema);
56
85
 
57
86
  try {
58
87
  const stream = await client.messages.create({
@@ -62,6 +91,7 @@ export function anthropic(args: AnthropicArgs): completePrompt {
62
91
  system: systemContent,
63
92
  messages: messages as Anthropic.MessageParam[],
64
93
  stream: true,
94
+ ...(outputConfig && { output_config: outputConfig }),
65
95
  });
66
96
 
67
97
  let text = '';
@@ -1,5 +1,5 @@
1
1
  import OpenAI from 'openai';
2
- import { AgentCompletion, completePrompt, PromptCompletionArgs, RequestError } from './types';
2
+ import { AgentCompletion, completePrompt, PromptCompletionArgs, RequestError, isMultimodalContent } from './types';
3
3
 
4
4
  export interface FireworksAIArgs {
5
5
  apiKey: string;
@@ -83,7 +83,14 @@ export function buildFireworksRequest(args: PromptCompletionArgs, defaultTemp: n
83
83
  }
84
84
  }
85
85
 
86
- let userContent = args.prompt.content;
86
+ // Strip images — FireworksAI has no vision support; keep text only
87
+ const promptContent = args.prompt.content;
88
+ let userContent: string;
89
+ if (isMultimodalContent(promptContent)) {
90
+ userContent = promptContent.filter(b => b.type === 'text').map(b => (b as { text: string }).text).join('\n');
91
+ } else {
92
+ userContent = promptContent;
93
+ }
87
94
  if (useJson) {
88
95
  userContent += '\n\nRespond with valid JSON only. No markdown fences.';
89
96
  }
@@ -1,4 +1,4 @@
1
- export { ProviderName, ProviderConfig, ModelEntry, Provider, SystemMessage, UserMessage, Message, AgentCompletion, completePrompt, PromptCompletionArgs, AgentArgs, RequestError } from './types';
1
+ export { ProviderName, ProviderConfig, ModelEntry, Provider, SystemMessage, UserMessage, Message, AgentCompletion, completePrompt, PromptCompletionArgs, AgentArgs, RequestError, TextBlock, ImageBlock, ContentBlock, MessageContent, isMultimodalContent } from './types';
2
2
  export { AnthropicProvider, OpenAIProvider, PROVIDERS, getProvider, detectProvider } from './providers';
3
3
  export { anthropic, AnthropicArgs, buildAnthropicRequest } from './anthropic';
4
4
  export { openai, OpenaiArgs, buildOpenAIRequest } from './openai';
@@ -1,5 +1,5 @@
1
1
  import OpenAI from 'openai';
2
- import { AgentCompletion, completePrompt, PromptCompletionArgs, RequestError } from './types';
2
+ import { AgentCompletion, completePrompt, PromptCompletionArgs, RequestError, isMultimodalContent } from './types';
3
3
 
4
4
  export interface OpenaiArgs {
5
5
  apiKey: string;
@@ -15,23 +15,43 @@ export interface OpenaiArgs {
15
15
  * Pure function — no SDK dependency.
16
16
  */
17
17
  export function buildOpenAIRequest(args: PromptCompletionArgs): {
18
- input: { role: string; content: string }[];
18
+ input: { role: string; content: string | any[] }[];
19
19
  text?: { format: any };
20
20
  } {
21
- const input: { role: string; content: string }[] = [];
21
+ const input: { role: string; content: string | any[] }[] = [];
22
22
  if (args.history) {
23
23
  for (const msg of args.history) {
24
24
  input.push({ role: msg.role, content: msg.content });
25
25
  }
26
26
  }
27
- input.push({ role: 'user', content: args.prompt.content });
27
+
28
+ // Build user content — multimodal when ContentBlock[] is provided
29
+ const promptContent = args.prompt.content;
30
+ if (isMultimodalContent(promptContent)) {
31
+ const parts: any[] = promptContent.map(block => {
32
+ if (block.type === 'text') {
33
+ return { type: 'input_text', text: block.text };
34
+ }
35
+ return {
36
+ type: 'input_image',
37
+ image_url: `data:${block.mediaType};base64,${block.data}`,
38
+ };
39
+ });
40
+ input.push({ role: 'user', content: parts });
41
+ } else {
42
+ input.push({ role: 'user', content: promptContent });
43
+ }
28
44
 
29
45
  if (args.jsonMode || args.jsonSchema) {
30
- const inputText = input.map(m => m.content).join(' ');
46
+ const inputText = input.map(m => typeof m.content === 'string' ? m.content : '').join(' ');
31
47
  if (!/json/i.test(inputText)) {
32
48
  const last = input[input.length - 1];
33
49
  if (last) {
34
- last.content += '\nReturn JSON.';
50
+ if (typeof last.content === 'string') {
51
+ last.content += '\nReturn JSON.';
52
+ } else if (Array.isArray(last.content)) {
53
+ last.content.push({ type: 'input_text', text: '\nReturn JSON.' });
54
+ }
35
55
  }
36
56
  }
37
57
  }
@@ -25,6 +25,30 @@ export interface Provider {
25
25
  detectModel(model: string): boolean;
26
26
  }
27
27
 
28
+ // ---------------------------------------------------------------------------
29
+ // Multimodal content
30
+ // ---------------------------------------------------------------------------
31
+
32
+ export interface TextBlock {
33
+ type: 'text';
34
+ text: string;
35
+ }
36
+
37
+ export interface ImageBlock {
38
+ type: 'image';
39
+ mediaType: string;
40
+ data: string;
41
+ }
42
+
43
+ export type ContentBlock = TextBlock | ImageBlock;
44
+
45
+ export type MessageContent = string | ContentBlock[];
46
+
47
+ /** Type guard: returns true when content is a multimodal ContentBlock array. */
48
+ export function isMultimodalContent(content: MessageContent): content is ContentBlock[] {
49
+ return Array.isArray(content);
50
+ }
51
+
28
52
  // ---------------------------------------------------------------------------
29
53
  // Messages
30
54
  // ---------------------------------------------------------------------------
@@ -39,8 +63,10 @@ export interface SystemMessage extends Message {
39
63
  role: 'system';
40
64
  }
41
65
 
42
- export interface UserMessage extends Message {
66
+ export interface UserMessage {
43
67
  role: 'user';
68
+ content: MessageContent;
69
+ name?: string;
44
70
  }
45
71
 
46
72
  // ---------------------------------------------------------------------------
@@ -61,6 +87,10 @@ export interface PromptCompletionArgs {
61
87
  jsonMode?: boolean;
62
88
  /** JSON schema for structured output. When provided, the model is asked to return JSON conforming to this schema. */
63
89
  jsonSchema?: Record<string, unknown>;
90
+ /** When true, system content is wrapped with cache_control for Anthropic prompt caching. */
91
+ cacheSystem?: boolean;
92
+ /** JSON schema for structured output via constrained decoding (Anthropic output_config). */
93
+ outputSchema?: Record<string, unknown>;
64
94
  }
65
95
 
66
96
  export type completePrompt<TValue = string> = (args: PromptCompletionArgs) => Promise<AgentCompletion<TValue>>;
package/src/pages.ts CHANGED
@@ -1,12 +1,29 @@
1
- import {checkIfExists, deleteFile, deleteFolder, ensureFolderExists, listFiles, listFolders, loadFile, saveFile} from './files';
1
+ import {checkIfExists, listFolders, loadFile} from './files';
2
2
  import path from 'path';
3
+ import { SynthOSConfig } from './init';
3
4
 
4
- // Page State Cache
5
- const _pages: { [name: string]: string } = {};
6
-
7
- export const REQUIRED_PAGES = ['builder', 'pages', 'settings', 'apis', 'scripts'];
5
+ /**
6
+ * Derive the list of required page names by scanning *.html files
7
+ * across one or more requiredPages folders.
8
+ */
9
+ export async function getRequiredPages(requiredPagesFolders: string[]): Promise<string[]> {
10
+ const result: string[] = [];
11
+ const seen = new Set<string>();
12
+ for (const folder of requiredPagesFolders) {
13
+ if (!await checkIfExists(folder)) continue;
14
+ const entries = await listFolders(folder);
15
+ for (const entry of entries) {
16
+ if (seen.has(entry)) continue;
17
+ if (await checkIfExists(path.join(folder, entry, 'page.html'))) {
18
+ seen.add(entry);
19
+ result.push(entry);
20
+ }
21
+ }
22
+ }
23
+ return result;
24
+ }
8
25
 
9
- export const PAGE_VERSION = 2;
26
+ export const PAGE_VERSION = 3;
10
27
 
11
28
  export interface PageInfo {
12
29
  name: string;
@@ -22,12 +39,15 @@ export interface PageInfo {
22
39
 
23
40
  export type PageMetadata = Omit<PageInfo, 'name'>;
24
41
 
25
- export async function loadPageMetadata(pagesFolder: string, name: string, fallbackFolder?: string): Promise<PageMetadata | undefined> {
26
- // 1. Try user override: .synthos/pages/<name>/page.json
42
+ export async function loadPageMetadata(config: SynthOSConfig, name: string, fallbackFolders?: string[]): Promise<PageMetadata | undefined> {
43
+ const pagesFolder = config.pagesFolder;
44
+ const sp = config.storageProvider;
45
+
46
+ // 1. Try user override: <localFolder>/pages/<name>/page.json
27
47
  const metadataPath = path.join(pagesFolder, 'pages', name, 'page.json');
28
- if (await checkIfExists(metadataPath)) {
48
+ if (await sp.checkIfExists(metadataPath)) {
29
49
  try {
30
- const raw = await loadFile(metadataPath);
50
+ const raw = await sp.loadFile(metadataPath);
31
51
  const parsed = JSON.parse(raw);
32
52
  return parseMetadata(parsed);
33
53
  } catch {
@@ -35,16 +55,18 @@ export async function loadPageMetadata(pagesFolder: string, name: string, fallba
35
55
  }
36
56
  }
37
57
 
38
- // 2. Try fallback: fallbackFolder/<name>.json
39
- if (fallbackFolder) {
40
- const fallbackPath = path.join(fallbackFolder, `${name}.json`);
41
- if (await checkIfExists(fallbackPath)) {
42
- try {
43
- const raw = await loadFile(fallbackPath);
44
- const parsed = JSON.parse(raw);
45
- return parseMetadata(parsed);
46
- } catch {
47
- // fall through
58
+ // 2. Try fallback folders: fallbackFolder/<name>/page.json (package content, always local fs)
59
+ if (fallbackFolders) {
60
+ for (const folder of fallbackFolders) {
61
+ const candidate = path.join(folder, name, 'page.json');
62
+ if (await checkIfExists(candidate)) {
63
+ try {
64
+ const raw = await loadFile(candidate);
65
+ const parsed = JSON.parse(raw);
66
+ return parseMetadata(parsed);
67
+ } catch {
68
+ // fall through
69
+ }
48
70
  }
49
71
  }
50
72
  }
@@ -66,10 +88,11 @@ export function parseMetadata(parsed: Record<string, unknown>): PageMetadata {
66
88
  };
67
89
  }
68
90
 
69
- export async function savePageMetadata(pagesFolder: string, name: string, metadata: PageMetadata): Promise<void> {
70
- const pageFolder = path.join(pagesFolder, 'pages', name);
71
- await ensureFolderExists(pageFolder);
72
- await saveFile(path.join(pageFolder, 'page.json'), JSON.stringify(metadata, null, 4));
91
+ export async function savePageMetadata(config: SynthOSConfig, name: string, metadata: PageMetadata): Promise<void> {
92
+ const sp = config.storageProvider;
93
+ const pageFolder = path.join(config.pagesFolder, 'pages', name);
94
+ await sp.ensureFolderExists(pageFolder);
95
+ await sp.saveFile(path.join(pageFolder, 'page.json'), JSON.stringify(metadata, null, 4));
73
96
  }
74
97
 
75
98
  const DEFAULT_METADATA: PageMetadata = {
@@ -83,16 +106,18 @@ const DEFAULT_METADATA: PageMetadata = {
83
106
  mode: 'unlocked',
84
107
  };
85
108
 
86
- export async function listPages(pagesFolder: string, fallbackPagesFolder: string): Promise<PageInfo[]> {
109
+ export async function listPages(config: SynthOSConfig, fallbackPagesFolders: string[]): Promise<PageInfo[]> {
110
+ const pagesFolder = config.pagesFolder;
111
+ const sp = config.storageProvider;
87
112
  const pageMap = new Map<string, PageInfo>();
88
113
 
89
114
  // Folder-based pages under pages/ subdirectory
90
115
  const pagesSubdir = path.join(pagesFolder, 'pages');
91
- if (await checkIfExists(pagesSubdir)) {
92
- const folders = await listFolders(pagesSubdir);
116
+ if (await sp.checkIfExists(pagesSubdir)) {
117
+ const folders = await sp.listFolders(pagesSubdir);
93
118
  for (const folder of folders) {
94
- if (await checkIfExists(path.join(pagesSubdir, folder, 'page.html'))) {
95
- const metadata = await loadPageMetadata(pagesFolder, folder);
119
+ if (await sp.checkIfExists(path.join(pagesSubdir, folder, 'page.html'))) {
120
+ const metadata = await loadPageMetadata(config, folder);
96
121
  pageMap.set(folder, {
97
122
  name: folder,
98
123
  ...(metadata ?? DEFAULT_METADATA),
@@ -102,7 +127,7 @@ export async function listPages(pagesFolder: string, fallbackPagesFolder: string
102
127
  }
103
128
 
104
129
  // Legacy flat .html files in root (v1 pages)
105
- const flatFiles = (await listFiles(pagesFolder)).filter(file => file.endsWith('.html'));
130
+ const flatFiles = (await sp.listFiles(pagesFolder)).filter(file => file.endsWith('.html'));
106
131
  for (const file of flatFiles) {
107
132
  const name = file.replace(/\.html$/, '');
108
133
  if (!pageMap.has(name)) {
@@ -128,13 +153,15 @@ export async function listPages(pagesFolder: string, fallbackPagesFolder: string
128
153
  }
129
154
  }
130
155
 
131
- // Add pages from fallback (required) pages folder
132
- const fallbackFiles = (await listFiles(fallbackPagesFolder)).filter(file => file.endsWith('.html'));
133
- for (const file of fallbackFiles) {
134
- const name = file.replace(/\.html$/, '');
135
- if (!pageMap.has(name)) {
136
- // System page not yet in map — check for user override, then fallback .json
137
- const metadata = await loadPageMetadata(pagesFolder, name, fallbackPagesFolder);
156
+ // Add pages from fallback (required) pages folders (package content, always local fs)
157
+ for (const folder of fallbackPagesFolders) {
158
+ if (!await checkIfExists(folder)) continue;
159
+ const dirs = await listFolders(folder);
160
+ for (const name of dirs) {
161
+ if (pageMap.has(name)) continue;
162
+ if (!await checkIfExists(path.join(folder, name, 'page.html'))) continue;
163
+ // System page not yet in map — check for user override, then fallback page.json
164
+ const metadata = await loadPageMetadata(config, name, fallbackPagesFolders);
138
165
  pageMap.set(name, {
139
166
  name,
140
167
  title: metadata?.title ?? '',
@@ -156,37 +183,45 @@ export async function listPages(pagesFolder: string, fallbackPagesFolder: string
156
183
  return entries;
157
184
  }
158
185
 
159
- export async function loadPageState(pagesFolder: string, name: string, reset: boolean): Promise<string|undefined> {
160
- if (!_pages[name] || reset) {
161
- // Try folder-based path under pages/ first, then fall back to flat file
162
- const folderPath = path.join(pagesFolder, 'pages', name, 'page.html');
163
- const flatPath = path.join(pagesFolder, `${name}.html`);
186
+ export async function loadPageState(config: SynthOSConfig, name: string): Promise<string|undefined> {
187
+ const pagesFolder = config.pagesFolder;
188
+ const sp = config.storageProvider;
164
189
 
165
- if (await checkIfExists(folderPath)) {
166
- _pages[name] = await loadFile(folderPath);
167
- } else if (await checkIfExists(flatPath)) {
168
- _pages[name] = await loadFile(flatPath);
169
- } else {
170
- return undefined;
171
- }
190
+ // Check for working-state version files first
191
+ const latestVersion = await getLatestVersion(config, name);
192
+ if (latestVersion > 0) {
193
+ const versionHtml = await loadPageVersion(config, name, latestVersion);
194
+ if (versionHtml) return versionHtml;
172
195
  }
173
196
 
174
- return _pages[name];
197
+ // Fall back to saved baseline
198
+ const folderPath = path.join(pagesFolder, 'pages', name, 'page.html');
199
+ const directFolderPath = path.join(pagesFolder, name, 'page.html');
200
+ const flatPath = path.join(pagesFolder, `${name}.html`);
201
+
202
+ if (await sp.checkIfExists(folderPath)) {
203
+ return sp.loadFile(folderPath);
204
+ } else if (await sp.checkIfExists(directFolderPath)) {
205
+ return sp.loadFile(directFolderPath);
206
+ } else if (await sp.checkIfExists(flatPath)) {
207
+ return sp.loadFile(flatPath);
208
+ }
209
+ return undefined;
175
210
  }
176
211
 
177
212
  export function normalizePageName(name: string|undefined): string|undefined {
178
213
  return typeof name == 'string' && name.length > 0 ? name.replace(/[^a-z0-9\-_\[\]\(\)\{\}@#\$%&]/gi, '_').toLowerCase() : undefined;
179
214
  }
180
215
 
181
- export async function savePageState(pagesFolder: string, name: string, content: string, title?: string, categories?: string[]): Promise<void> {
182
- _pages[name] = content;
183
- const pageFolder = path.join(pagesFolder, 'pages', name);
184
- await ensureFolderExists(pageFolder);
185
- await saveFile(path.join(pageFolder, 'page.html'), content);
216
+ export async function savePageState(config: SynthOSConfig, name: string, content: string, title?: string, categories?: string[]): Promise<void> {
217
+ const sp = config.storageProvider;
218
+ const pageFolder = path.join(config.pagesFolder, 'pages', name);
219
+ await sp.ensureFolderExists(pageFolder);
220
+ await sp.saveFile(path.join(pageFolder, 'page.html'), content);
186
221
 
187
222
  // Create page.json with full metadata if it doesn't exist
188
223
  const metadataPath = path.join(pageFolder, 'page.json');
189
- if (!(await checkIfExists(metadataPath))) {
224
+ if (!(await sp.checkIfExists(metadataPath))) {
190
225
  const now = new Date().toISOString();
191
226
  const metadata: PageMetadata = {
192
227
  title: title ?? '',
@@ -198,45 +233,134 @@ export async function savePageState(pagesFolder: string, name: string, content:
198
233
  pageVersion: PAGE_VERSION,
199
234
  mode: 'unlocked',
200
235
  };
201
- await saveFile(metadataPath, JSON.stringify(metadata, null, 4));
236
+ await sp.saveFile(metadataPath, JSON.stringify(metadata, null, 4));
202
237
  }
203
238
  }
204
239
 
205
- export function updatePageState(name: string, content: string): void {
206
- _pages[name] = content;
207
- }
240
+ export async function deletePage(config: SynthOSConfig, name: string): Promise<void> {
241
+ const pagesFolder = config.pagesFolder;
242
+ const sp = config.storageProvider;
208
243
 
209
- export async function deletePage(pagesFolder: string, name: string): Promise<void> {
210
244
  // Delete folder-based page: <pagesFolder>/pages/<name>/
211
245
  const folderPath = path.join(pagesFolder, 'pages', name);
212
- if (await checkIfExists(folderPath)) {
213
- await deleteFolder(folderPath);
246
+ if (await sp.checkIfExists(folderPath)) {
247
+ await sp.deleteFolder(folderPath);
214
248
  }
215
249
 
216
250
  // Delete legacy flat file: <pagesFolder>/<name>.html
217
251
  const flatPath = path.join(pagesFolder, `${name}.html`);
218
- if (await checkIfExists(flatPath)) {
219
- await deleteFile(flatPath);
252
+ if (await sp.checkIfExists(flatPath)) {
253
+ await sp.deleteFile(flatPath);
254
+ }
255
+
256
+ }
257
+
258
+ // ---------------------------------------------------------------------------
259
+ // Page versioning — per-edit version snapshots for undo support
260
+ // ---------------------------------------------------------------------------
261
+
262
+ /**
263
+ * Save a version snapshot: <pagesFolder>/pages/<name>/page.v<version>.html
264
+ */
265
+ export async function savePageVersion(config: SynthOSConfig, name: string, version: number, html: string): Promise<void> {
266
+ const sp = config.storageProvider;
267
+ const pageFolder = path.join(config.pagesFolder, 'pages', name);
268
+ await sp.ensureFolderExists(pageFolder);
269
+ await sp.saveFile(path.join(pageFolder, `page.v${version}.html`), html);
270
+ }
271
+
272
+ /**
273
+ * Load a version snapshot (returns undefined if the file doesn't exist).
274
+ */
275
+ export async function loadPageVersion(config: SynthOSConfig, name: string, version: number): Promise<string | undefined> {
276
+ const sp = config.storageProvider;
277
+ const filePath = path.join(config.pagesFolder, 'pages', name, `page.v${version}.html`);
278
+ if (!await sp.checkIfExists(filePath)) return undefined;
279
+ return sp.loadFile(filePath);
280
+ }
281
+
282
+ /**
283
+ * Scan page.v*.html files and return the highest version number (0 if none).
284
+ */
285
+ export async function getLatestVersion(config: SynthOSConfig, name: string): Promise<number> {
286
+ const sp = config.storageProvider;
287
+ const pageFolder = path.join(config.pagesFolder, 'pages', name);
288
+ if (!await sp.checkIfExists(pageFolder)) return 0;
289
+ const files = await sp.listFiles(pageFolder);
290
+ let max = 0;
291
+ for (const file of files) {
292
+ const match = file.match(/^page\.v(\d+)\.html$/);
293
+ if (match) {
294
+ const v = parseInt(match[1], 10);
295
+ if (v > max) max = v;
296
+ }
297
+ }
298
+ return max;
299
+ }
300
+
301
+ /**
302
+ * Delete all page.v*.html version files for a page.
303
+ */
304
+ export async function clearVersions(config: SynthOSConfig, name: string): Promise<void> {
305
+ const sp = config.storageProvider;
306
+ const pageFolder = path.join(config.pagesFolder, 'pages', name);
307
+ if (!await sp.checkIfExists(pageFolder)) return;
308
+ const files = await sp.listFiles(pageFolder);
309
+ for (const file of files) {
310
+ if (/^page\.v\d+\.html$/.test(file)) {
311
+ await sp.deleteFile(path.join(pageFolder, file));
312
+ }
220
313
  }
314
+ }
221
315
 
222
- // Clear in-memory cache
223
- delete _pages[name];
316
+ export interface CopyPageOptions {
317
+ copyTables?: boolean; // default false
318
+ copyFiles?: boolean; // default true
224
319
  }
225
320
 
226
321
  export async function copyPage(
227
- pagesFolder: string,
322
+ config: SynthOSConfig,
228
323
  sourceName: string,
229
324
  targetName: string,
230
325
  title: string,
231
326
  categories: string[],
232
- requiredPagesFolder: string
327
+ requiredPagesFolders: string[],
328
+ options?: CopyPageOptions
233
329
  ): Promise<void> {
234
- // Load source HTML from user folder, then try required folder as fallback
235
- let html = await loadPageState(pagesFolder, sourceName, true);
236
- if (!html) {
237
- const requiredPath = path.join(requiredPagesFolder, `${sourceName}.html`);
238
- if (await checkIfExists(requiredPath)) {
239
- html = await loadFile(requiredPath);
330
+ const pagesFolder = config.pagesFolder;
331
+ const sp = config.storageProvider;
332
+ const copyTables = options?.copyTables ?? false;
333
+ const cpFiles = options?.copyFiles ?? true;
334
+
335
+ // Resolve source page folder: user pages first, then required pages
336
+ let sourceFolder: string | undefined;
337
+ const userSourceFolder = path.join(pagesFolder, 'pages', sourceName);
338
+ if (await sp.checkIfExists(path.join(userSourceFolder, 'page.html'))) {
339
+ sourceFolder = userSourceFolder;
340
+ } else {
341
+ for (const folder of requiredPagesFolders) {
342
+ const candidate = path.join(folder, sourceName);
343
+ if (await checkIfExists(path.join(candidate, 'page.html'))) {
344
+ sourceFolder = candidate;
345
+ break;
346
+ }
347
+ }
348
+ }
349
+
350
+ // Load source HTML
351
+ let html: string | undefined;
352
+ if (sourceFolder) {
353
+ // Source could be user storage or package — try user first, fall back to local fs
354
+ if (sourceFolder === userSourceFolder) {
355
+ html = await sp.loadFile(path.join(sourceFolder, 'page.html'));
356
+ } else {
357
+ html = await loadFile(path.join(sourceFolder, 'page.html'));
358
+ }
359
+ } else {
360
+ // Try legacy flat file
361
+ const flatPath = path.join(pagesFolder, `${sourceName}.html`);
362
+ if (await sp.checkIfExists(flatPath)) {
363
+ html = await sp.loadFile(flatPath);
240
364
  }
241
365
  }
242
366
 
@@ -245,7 +369,7 @@ export async function copyPage(
245
369
  }
246
370
 
247
371
  // Save HTML to target (creates folder + page.html + page.json)
248
- await savePageState(pagesFolder, targetName, html, title);
372
+ await savePageState(config, targetName, html, title);
249
373
 
250
374
  // Overwrite the generated metadata with provided title + categories
251
375
  const now = new Date().toISOString();
@@ -259,5 +383,34 @@ export async function copyPage(
259
383
  pageVersion: PAGE_VERSION,
260
384
  mode: 'unlocked',
261
385
  };
262
- await savePageMetadata(pagesFolder, targetName, metadata);
386
+ await savePageMetadata(config, targetName, metadata);
387
+
388
+ // Copy additional content from source if a folder was resolved
389
+ if (sourceFolder) {
390
+ const targetFolder = path.join(pagesFolder, 'pages', targetName);
391
+
392
+ if (copyTables) {
393
+ // Source could be user or package folder — use appropriate listing
394
+ const entries = sourceFolder === userSourceFolder
395
+ ? await sp.listFolders(sourceFolder)
396
+ : await listFolders(sourceFolder);
397
+ for (const entry of entries) {
398
+ if (entry === 'files') continue; // handled separately
399
+ await sp.copyFolderRecursive(
400
+ path.join(sourceFolder, entry),
401
+ path.join(targetFolder, entry)
402
+ );
403
+ }
404
+ }
405
+
406
+ if (cpFiles) {
407
+ const filesDir = path.join(sourceFolder, 'files');
408
+ const filesDirExists = sourceFolder === userSourceFolder
409
+ ? await sp.checkIfExists(filesDir)
410
+ : await checkIfExists(filesDir);
411
+ if (filesDirExists) {
412
+ await sp.copyFolderRecursive(filesDir, path.join(targetFolder, 'files'));
413
+ }
414
+ }
415
+ }
263
416
  }