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
package/src/files.ts CHANGED
@@ -72,3 +72,60 @@ export async function copyFolderRecursive(srcFolder: string, destFolder: string)
72
72
  export async function deleteFolder(dirPath: string): Promise<void> {
73
73
  await fs.rm(dirPath, { recursive: true });
74
74
  }
75
+
76
+ // --- Multi-folder helpers ---
77
+
78
+ /**
79
+ * Search folders in order, return the full path to the first existing match
80
+ * for the given filename.
81
+ */
82
+ export async function findFileInFolders(folders: string[], filename: string): Promise<string | undefined> {
83
+ for (const folder of folders) {
84
+ const candidate = path.join(folder, filename);
85
+ if (await checkIfExists(candidate)) {
86
+ return candidate;
87
+ }
88
+ }
89
+ return undefined;
90
+ }
91
+
92
+ /**
93
+ * Merge file listings from multiple folders. First folder takes priority on
94
+ * name collisions (earlier occurrence wins).
95
+ */
96
+ export async function listFilesFromFolders(folders: string[]): Promise<string[]> {
97
+ const seen = new Set<string>();
98
+ const result: string[] = [];
99
+ for (const folder of folders) {
100
+ if (!await checkIfExists(folder)) continue;
101
+ const files = await listFiles(folder);
102
+ for (const f of files) {
103
+ if (!seen.has(f)) {
104
+ seen.add(f);
105
+ result.push(f);
106
+ }
107
+ }
108
+ }
109
+ return result;
110
+ }
111
+
112
+ /**
113
+ * Copy files from multiple source folders into a single destination.
114
+ * First folder takes priority on duplicate filenames (copy is skipped
115
+ * if the file already exists in dest from an earlier folder).
116
+ */
117
+ export async function copyFilesFromFolders(folders: string[], destFolder: string): Promise<void> {
118
+ await ensureFolderExists(destFolder);
119
+ const copied = new Set<string>();
120
+ for (const folder of folders) {
121
+ if (!await checkIfExists(folder)) continue;
122
+ const files = await fs.readdir(folder);
123
+ for (const file of files) {
124
+ if (copied.has(file)) continue;
125
+ copied.add(file);
126
+ const srcPath = path.join(folder, file);
127
+ const destPath = path.join(destFolder, file);
128
+ await fs.copyFile(srcPath, destPath);
129
+ }
130
+ }
131
+ }
package/src/index.ts CHANGED
@@ -3,4 +3,5 @@ export * from './files';
3
3
  export * from './init';
4
4
  export * from './pages';
5
5
  export * from './scripts';
6
- export * from './settings';
6
+ export * from './settings';
7
+ export * from './customizer';
package/src/init.ts CHANGED
@@ -1,29 +1,67 @@
1
1
  import * as fs from 'fs/promises';
2
2
  import path from "path";
3
- import { checkIfExists, copyFile, copyFiles, deleteFile, ensureFolderExists, listFiles, saveFile } from "./files";
4
- import { PAGE_VERSION } from "./pages";
3
+ import { checkIfExists, copyFile, copyFolderRecursive, deleteFile, ensureFolderExists, findFileInFolders, listFiles, listFolders, saveFile } from "./files";
4
+ import { PAGE_VERSION, getRequiredPages } from "./pages";
5
5
  import { DefaultSettings } from "./settings";
6
- import { getOutdatedThemes, parseThemeFilename } from "./themes";
6
+ import { Customizer } from './customizer';
7
7
 
8
8
  export interface SynthOSConfig {
9
+ localFolder: string;
9
10
  pagesFolder: string;
10
- requiredPagesFolder: string;
11
- defaultPagesFolder: string;
12
- defaultScriptsFolder: string;
13
- defaultThemesFolder: string;
14
- pageScriptsFolder: string;
11
+ requiredPagesFolders: string[];
12
+ defaultPagesFolders: string[];
13
+ defaultScriptsFolders: string[];
14
+ defaultThemesFolders: string[];
15
+ staticFilesFolders: string[];
16
+ serviceConnectorsFolders: string[];
17
+ requiredPages: string[];
15
18
  debug: boolean;
16
19
  debugPageUpdates: boolean;
17
20
  }
18
21
 
19
- export function createConfig(pagesFolder = '.synthos', options?: { debug?: boolean; debugPageUpdates?: boolean }): SynthOSConfig {
22
+ /**
23
+ * Resolve a folder array from a Customizer getter: replace the `'default'`
24
+ * sentinel with the built-in SynthOS path.
25
+ */
26
+ function resolveFolders(folders: string[], builtInPath: string): string[] {
27
+ return folders.map(f => f === 'default' ? builtInPath : f);
28
+ }
29
+
30
+ export async function createConfig(
31
+ pagesFolder = '.synthos',
32
+ options?: { debug?: boolean; debugPageUpdates?: boolean },
33
+ customizer?: Customizer
34
+ ): Promise<SynthOSConfig> {
35
+ const requiredPagesFolders = resolveFolders(
36
+ customizer?.requiredPagesFolders ?? ['default'],
37
+ path.join(__dirname, '../required-pages')
38
+ );
39
+ const requiredPages = await getRequiredPages(requiredPagesFolders);
20
40
  return {
41
+ localFolder: pagesFolder,
21
42
  pagesFolder: path.join(process.cwd(), pagesFolder),
22
- requiredPagesFolder: path.join(__dirname, '../required-pages'),
23
- defaultPagesFolder: path.join(__dirname, '../default-pages'),
24
- defaultScriptsFolder: path.join(__dirname, '../default-scripts'),
25
- defaultThemesFolder: path.join(__dirname, '../default-themes'),
26
- pageScriptsFolder: path.join(__dirname, '../page-scripts'),
43
+ requiredPagesFolders,
44
+ defaultPagesFolders: resolveFolders(
45
+ customizer?.defaultPagesFolders ?? ['default'],
46
+ path.join(__dirname, '../default-pages')
47
+ ),
48
+ defaultScriptsFolders: resolveFolders(
49
+ customizer?.defaultScriptsFolders ?? ['default'],
50
+ path.join(__dirname, '../default-scripts')
51
+ ),
52
+ defaultThemesFolders: resolveFolders(
53
+ customizer?.defaultThemesFolders ?? ['default'],
54
+ path.join(__dirname, '../default-themes')
55
+ ),
56
+ staticFilesFolders: resolveFolders(
57
+ customizer?.staticFilesFolders ?? ['default'],
58
+ path.join(__dirname, '../static-files')
59
+ ),
60
+ serviceConnectorsFolders: resolveFolders(
61
+ customizer?.serviceConnectorsFolders ?? ['default'],
62
+ path.join(__dirname, '../service-connectors')
63
+ ),
64
+ requiredPages,
27
65
  debug: options?.debug ?? false,
28
66
  debugPageUpdates: options?.debugPageUpdates ?? false
29
67
  };
@@ -36,7 +74,7 @@ export async function init(config: SynthOSConfig, includeDefaultPages: boolean =
36
74
  return false;
37
75
  }
38
76
 
39
- console.log(`Initializing .synthos folder...`);
77
+ console.log(`Initializing ${config.localFolder} folder...`);
40
78
 
41
79
  // Create pages folder
42
80
  await ensureFolderExists(config.pagesFolder);
@@ -47,37 +85,29 @@ export async function init(config: SynthOSConfig, includeDefaultPages: boolean =
47
85
  await saveFile(path.join(config.pagesFolder, 'settings.json.example'), JSON.stringify(DefaultSettings, null, 4));
48
86
 
49
87
  // Setup default scripts
50
- console.log(`Copying default scripts to .synthos folder...`);
88
+ console.log(`Copying default scripts to ${config.localFolder} folder...`);
51
89
  const scriptsFolder = path.join(config.pagesFolder, 'scripts');
52
90
  await ensureFolderExists(scriptsFolder);
53
- switch (process.platform) {
54
- case 'win32':
55
- await copyFile(path.join(config.defaultScriptsFolder, 'windows-terminal.json'), scriptsFolder);
56
- break;
57
- case 'darwin':
58
- await copyFile(path.join(config.defaultScriptsFolder, 'mac-terminal.json'), scriptsFolder);
59
- break;
60
- case 'android':
61
- await copyFile(path.join(config.defaultScriptsFolder, 'android-terminal.json'), scriptsFolder);
62
- break;
63
- case 'linux':
64
- default:
65
- await copyFile(path.join(config.defaultScriptsFolder, 'linux-terminal.json'), scriptsFolder);
66
- break;
67
- }
68
-
69
- await saveFile(path.join(scriptsFolder, 'example.sh'), '#!/bin/bash\n\n# This is an example script\n\n# You can run this script using the following command:\n# sh .synthos/scripts/example.sh\n\n# This script will print "Hello, World!" to the console\n\necho "Hello, World!"\n');
70
-
71
- // Setup default themes
72
- console.log(`Copying default themes to .synthos folder...`);
73
- const themesFolder = path.join(config.pagesFolder, 'themes');
74
- await ensureFolderExists(themesFolder);
75
- await copyFiles(config.defaultThemesFolder, themesFolder);
91
+ const scriptFilename = ({
92
+ win32: 'windows-terminal.json',
93
+ darwin: 'mac-terminal.json',
94
+ android: 'android-terminal.json',
95
+ } as Record<string, string>)[process.platform] ?? 'linux-terminal.json';
96
+ const scriptSrc = await findFileInFolders(config.defaultScriptsFolders, scriptFilename);
97
+ if (scriptSrc) {
98
+ await copyFile(scriptSrc, scriptsFolder);
99
+ }
100
+
101
+ await saveFile(path.join(scriptsFolder, 'example.sh'), `#!/bin/bash\n\n# This is an example script\n\n# You can run this script using the following command:\n# sh ${config.localFolder}/scripts/example.sh\n\n# This script will print "Hello, World!" to the console\n\necho "Hello, World!"\n`);
102
+
103
+ // Create empty themes folder — default themes are served directly from
104
+ // defaultThemesFolders; users can add custom themes here.
105
+ await ensureFolderExists(path.join(config.pagesFolder, 'themes'));
76
106
 
77
107
  // Copy pages
78
108
  if (includeDefaultPages) {
79
- console.log(`Copying default pages to .synthos folder...`);
80
- await copyDefaultPages(config.defaultPagesFolder, config.pagesFolder);
109
+ console.log(`Copying default pages to ${config.localFolder} folder...`);
110
+ await copyDefaultPages(config.defaultPagesFolders, config.pagesFolder);
81
111
  }
82
112
 
83
113
  return true;
@@ -87,57 +117,23 @@ async function repairMissingFolders(config: SynthOSConfig): Promise<void> {
87
117
  // Rebuild scripts folder from defaults if missing
88
118
  const scriptsFolder = path.join(config.pagesFolder, 'scripts');
89
119
  if (!await checkIfExists(scriptsFolder)) {
90
- console.log(`Restoring default scripts to .synthos folder...`);
120
+ console.log(`Restoring default scripts to ${config.localFolder} folder...`);
91
121
  await ensureFolderExists(scriptsFolder);
92
- switch (process.platform) {
93
- case 'win32':
94
- await copyFile(path.join(config.defaultScriptsFolder, 'windows-terminal.json'), scriptsFolder);
95
- break;
96
- case 'darwin':
97
- await copyFile(path.join(config.defaultScriptsFolder, 'mac-terminal.json'), scriptsFolder);
98
- break;
99
- case 'android':
100
- await copyFile(path.join(config.defaultScriptsFolder, 'android-terminal.json'), scriptsFolder);
101
- break;
102
- case 'linux':
103
- default:
104
- await copyFile(path.join(config.defaultScriptsFolder, 'linux-terminal.json'), scriptsFolder);
105
- break;
122
+ const scriptFilename = ({
123
+ win32: 'windows-terminal.json',
124
+ darwin: 'mac-terminal.json',
125
+ android: 'android-terminal.json',
126
+ } as Record<string, string>)[process.platform] ?? 'linux-terminal.json';
127
+ const scriptSrc = await findFileInFolders(config.defaultScriptsFolders, scriptFilename);
128
+ if (scriptSrc) {
129
+ await copyFile(scriptSrc, scriptsFolder);
106
130
  }
107
- await saveFile(path.join(scriptsFolder, 'example.sh'), '#!/bin/bash\n\n# This is an example script\n\n# You can run this script using the following command:\n# sh .synthos/scripts/example.sh\n\n# This script will print "Hello, World!" to the console\n\necho "Hello, World!"\n');
131
+ await saveFile(path.join(scriptsFolder, 'example.sh'), `#!/bin/bash\n\n# This is an example script\n\n# You can run this script using the following command:\n# sh ${config.localFolder}/scripts/example.sh\n\n# This script will print "Hello, World!" to the console\n\necho "Hello, World!"\n`);
108
132
  }
109
133
 
110
- // Rebuild themes folder from defaults if missing
111
- const themesFolder = path.join(config.pagesFolder, 'themes');
112
- if (!await checkIfExists(themesFolder)) {
113
- console.log(`Restoring default themes to .synthos folder...`);
114
- await ensureFolderExists(themesFolder);
115
- await copyFiles(config.defaultThemesFolder, themesFolder);
116
- } else {
117
- // Upgrade outdated themes — copy newer versioned CSS from defaults
118
- const outdated = await getOutdatedThemes(config);
119
- if (outdated.length > 0) {
120
- console.log(`Upgrading ${outdated.length} theme(s): ${outdated.join(', ')}...`);
121
- const defaultFiles = await listFiles(config.defaultThemesFolder);
122
- for (const themeName of outdated) {
123
- // Remove old versioned CSS files for this theme
124
- const localFiles = await listFiles(themesFolder);
125
- for (const f of localFiles) {
126
- const parsed = parseThemeFilename(f);
127
- if (parsed && parsed.name === themeName) {
128
- await deleteFile(path.join(themesFolder, f));
129
- }
130
- }
131
- // Copy the new versioned CSS from defaults
132
- for (const f of defaultFiles) {
133
- const parsed = parseThemeFilename(f);
134
- if (parsed && parsed.name === themeName) {
135
- await copyFile(path.join(config.defaultThemesFolder, f), themesFolder);
136
- }
137
- }
138
- }
139
- }
140
- }
134
+ // Ensure themes folder exists default themes are served directly from
135
+ // defaultThemesFolders; this folder is for user-added custom themes only.
136
+ await ensureFolderExists(path.join(config.pagesFolder, 'themes'));
141
137
 
142
138
  // Ensure pages/ subfolder exists
143
139
  const pagesSubdir = path.join(config.pagesFolder, 'pages');
@@ -145,15 +141,15 @@ async function repairMissingFolders(config: SynthOSConfig): Promise<void> {
145
141
  // No pages folder and no flat files — rebuild from defaults
146
142
  const htmlFiles = (await listFiles(config.pagesFolder)).filter(f => f.endsWith('.html'));
147
143
  if (htmlFiles.length === 0) {
148
- console.log(`Restoring default pages to .synthos/pages/ folder...`);
149
- await copyDefaultPages(config.defaultPagesFolder, config.pagesFolder);
144
+ console.log(`Restoring default pages to ${config.localFolder}/pages/ folder...`);
145
+ await copyDefaultPages(config.defaultPagesFolders, config.pagesFolder);
150
146
  } else {
151
147
  await ensureFolderExists(pagesSubdir);
152
148
  }
153
149
  }
154
150
 
155
151
  // Migrate any stray flat .html files from root into pages/<name>/
156
- await migrateFlatPages(config.pagesFolder);
152
+ await migrateFlatPages(config.pagesFolder, config.localFolder);
157
153
  }
158
154
 
159
155
  function toTitleCase(name: string): string {
@@ -164,12 +160,12 @@ function toTitleCase(name: string): string {
164
160
  .replace(/\b\w/g, c => c.toUpperCase());
165
161
  }
166
162
 
167
- async function migrateFlatPages(pagesFolder: string): Promise<void> {
163
+ async function migrateFlatPages(pagesFolder: string, localFolder: string): Promise<void> {
168
164
  const pagesSubdir = path.join(pagesFolder, 'pages');
169
165
  const htmlFiles = (await listFiles(pagesFolder)).filter(f => f.endsWith('.html'));
170
166
  if (htmlFiles.length === 0) return;
171
167
 
172
- console.log(`Migrating ${htmlFiles.length} page(s) to .synthos/pages/ folder...`);
168
+ console.log(`Migrating ${htmlFiles.length} page(s) to ${localFolder}/pages/ folder...`);
173
169
  await ensureFolderExists(pagesSubdir);
174
170
  const now = new Date().toISOString();
175
171
 
@@ -200,40 +196,58 @@ async function migrateFlatPages(pagesFolder: string): Promise<void> {
200
196
  }
201
197
  }
202
198
 
203
- async function copyDefaultPages(srcFolder: string, destFolder: string): Promise<void> {
199
+ async function copyDefaultPages(srcFolders: string[], destFolder: string): Promise<void> {
204
200
  const pagesDir = path.join(destFolder, 'pages');
205
201
  await ensureFolderExists(pagesDir);
206
- const files = await fs.readdir(srcFolder);
207
202
  const now = new Date().toISOString();
208
- for (const file of files) {
209
- if (!file.endsWith('.html')) continue;
210
- const pageName = file.replace(/\.html$/, '');
211
- const pageFolder = path.join(pagesDir, pageName);
212
- await ensureFolderExists(pageFolder);
213
- await fs.copyFile(path.join(srcFolder, file), path.join(pageFolder, 'page.html'));
214
-
215
- // Read companion .json metadata from source folder, fall back to defaults
216
- let metadata: Record<string, unknown> = {};
217
- const jsonPath = path.join(srcFolder, `${pageName}.json`);
218
- if (await checkIfExists(jsonPath)) {
219
- try {
220
- const raw = await fs.readFile(jsonPath, 'utf-8');
221
- metadata = JSON.parse(raw);
222
- } catch {
223
- // use defaults
203
+ const seen = new Set<string>();
204
+
205
+ for (const srcFolder of srcFolders) {
206
+ if (!await checkIfExists(srcFolder)) continue;
207
+ const dirs = await listFolders(srcFolder);
208
+ for (const dir of dirs) {
209
+ const srcPageDir = path.join(srcFolder, dir);
210
+ if (!await checkIfExists(path.join(srcPageDir, 'page.html'))) continue;
211
+ if (seen.has(dir)) continue; // first folder wins
212
+ seen.add(dir);
213
+
214
+ const pageFolder = path.join(pagesDir, dir);
215
+ await ensureFolderExists(pageFolder);
216
+ await fs.copyFile(path.join(srcPageDir, 'page.html'), path.join(pageFolder, 'page.html'));
217
+
218
+ // Read companion page.json metadata from source folder, fall back to defaults
219
+ let metadata: Record<string, unknown> = {};
220
+ const jsonPath = path.join(srcPageDir, 'page.json');
221
+ if (await checkIfExists(jsonPath)) {
222
+ try {
223
+ const raw = await fs.readFile(jsonPath, 'utf-8');
224
+ metadata = JSON.parse(raw);
225
+ } catch {
226
+ // use defaults
227
+ }
228
+ }
229
+ const fullMetadata = {
230
+ title: typeof metadata.title === 'string' ? metadata.title : '',
231
+ categories: Array.isArray(metadata.categories) ? metadata.categories : [],
232
+ pinned: typeof metadata.pinned === 'boolean' ? metadata.pinned : false,
233
+ showInAll: typeof metadata.showInAll === 'boolean' ? metadata.showInAll : true,
234
+ createdDate: now,
235
+ lastModified: now,
236
+ pageVersion: typeof metadata.pageVersion === 'number' ? metadata.pageVersion
237
+ : typeof metadata.uxVersion === 'number' ? metadata.uxVersion : PAGE_VERSION,
238
+ mode: metadata.mode === 'locked' ? 'locked' : 'unlocked',
239
+ };
240
+ await saveFile(path.join(pageFolder, 'page.json'), JSON.stringify(fullMetadata, null, 4));
241
+
242
+ // Copy data subfolders (anything that isn't page.html/page.json)
243
+ const subEntries = await fs.readdir(srcPageDir, { withFileTypes: true });
244
+ for (const entry of subEntries) {
245
+ if (!entry.isDirectory()) continue;
246
+ await copyFolderRecursive(
247
+ path.join(srcPageDir, entry.name),
248
+ path.join(pageFolder, entry.name)
249
+ );
224
250
  }
225
251
  }
226
- const fullMetadata = {
227
- title: typeof metadata.title === 'string' ? metadata.title : '',
228
- categories: Array.isArray(metadata.categories) ? metadata.categories : [],
229
- pinned: typeof metadata.pinned === 'boolean' ? metadata.pinned : false,
230
- showInAll: typeof metadata.showInAll === 'boolean' ? metadata.showInAll : true,
231
- createdDate: now,
232
- lastModified: now,
233
- pageVersion: typeof metadata.pageVersion === 'number' ? metadata.pageVersion
234
- : typeof metadata.uxVersion === 'number' ? metadata.uxVersion : PAGE_VERSION,
235
- mode: metadata.mode === 'locked' ? 'locked' : 'unlocked',
236
- };
237
- await saveFile(path.join(pageFolder, 'page.json'), JSON.stringify(fullMetadata, null, 4));
238
252
  }
239
253
  }
package/src/migrations.ts CHANGED
@@ -10,6 +10,7 @@ import { deduplicateInlineScripts } from './service/transformPage';
10
10
  */
11
11
  const migrations: Record<number, (html: string, completePrompt: completePrompt) => Promise<string>> = {
12
12
  1: migrateV1toV2,
13
+ 2: migrateV2toV3,
13
14
  };
14
15
 
15
16
  /**
@@ -36,7 +37,6 @@ const SHARED_CSS_SELECTORS = [
36
37
  '.chat-panel', '.chat-header', '.chat-messages',
37
38
  '.chat-message', '.chat-message p', '.chat-message p strong', '.chat-message p code',
38
39
  '.chat-message strong', '.chat-message pre', '.chat-message code', '.chat-message a',
39
- '.link-group', '.link-group a', '.link-group a:hover',
40
40
  'form',
41
41
  '.chat-input', '.chat-input:focus', '.chat-input::placeholder', '.chat-input:disabled',
42
42
  '.chat-submit', '.chat-submit:hover', '.chat-submit:active', '.chat-submit:disabled',
@@ -80,14 +80,8 @@ const DEFAULT_CHAT_PANEL = `
80
80
  <div class="chat-messages" id="chatMessages">
81
81
  <div class="chat-message"><p>Welcome! How can I help you?</p></div>
82
82
  </div>
83
- <div class="link-group">
84
- <a href="#" id="saveLink">Save</a>
85
- <a href="/pages" id="pagesLink">Pages</a>
86
- <a href="#" id="resetLink">Reset</a>
87
- </div>
88
83
  <form action="/" method="POST" id="chatForm">
89
- <input type="text" class="chat-input" id="chatInput" name="message" placeholder="Type a message...">
90
- <button type="submit" class="chat-submit">Send</button>
84
+ <textarea class="chat-input" id="chatInput" name="message" rows="2" placeholder="Type a message..."></textarea>
91
85
  </form>
92
86
  </div>`;
93
87
 
@@ -125,6 +119,33 @@ async function migrateV1toV2(html: string, completePrompt: completePrompt): Prom
125
119
  return migrated;
126
120
  }
127
121
 
122
+ /**
123
+ * v2 -> v3: Cheerio-based migration (no LLM).
124
+ * - Removes .link-group div
125
+ * - Converts chat <input> to <textarea>
126
+ * - Removes .chat-submit button (v3 creates it dynamically)
127
+ */
128
+ async function migrateV2toV3(html: string, _completePrompt: completePrompt): Promise<string> {
129
+ const $ = cheerio.load(html, { decodeEntities: false });
130
+
131
+ // 1. Remove .link-group div and its children
132
+ $('.link-group').remove();
133
+
134
+ // 2. Replace <input type="text" id="chatInput" ...> with <textarea>
135
+ const chatInput = $('input#chatInput, input.chat-input');
136
+ if (chatInput.length > 0) {
137
+ chatInput.replaceWith(
138
+ '<textarea class="chat-input" id="chatInput" name="message" rows="2" placeholder="Type a message..."></textarea>'
139
+ );
140
+ }
141
+
142
+ // 3. Remove .chat-submit button inside #chatForm (or anywhere)
143
+ $('button.chat-submit').remove();
144
+
145
+ // Run through postProcessV2 to ensure structural integrity
146
+ return postProcessV2($.html());
147
+ }
148
+
128
149
  /**
129
150
  * Cheerio-based post-processing to verify the LLM output meets v2 requirements.
130
151
  * Uses the original HTML as a fallback source for critical elements.
@@ -146,8 +167,7 @@ export function postProcessV2(html: string, originalHtml?: string): string {
146
167
  // Append default form
147
168
  $('.chat-panel').append(`
148
169
  <form action="/" method="POST" id="chatForm">
149
- <input type="text" class="chat-input" id="chatInput" name="message" placeholder="Type a message...">
150
- <button type="submit" class="chat-submit">Send</button>
170
+ <textarea class="chat-input" id="chatInput" name="message" rows="2" placeholder="Type a message..."></textarea>
151
171
  </form>`);
152
172
  }
153
173
  } else {
@@ -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';