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
@@ -0,0 +1,109 @@
1
+ import { Application, Response } from 'express';
2
+ import { SynthOSConfig } from "../init";
3
+ import path from "path";
4
+ import { v4 } from "uuid";
5
+
6
+ export function useSharedDataRoutes(config: SynthOSConfig, app: Application): void {
7
+ app.get('/api/shared/data/:table', (req, res) => handleList(config, req.params.table, req.query, res));
8
+ app.get('/api/shared/data/:table/:id', (req, res) => handleGet(config, req.params.table, req.params.id, res));
9
+ app.post('/api/shared/data/:table', (req, res) => handleUpsert(config, req.params.table, req.body, res));
10
+ app.delete('/api/shared/data/:table/:id', (req, res) => handleDelete(config, req.params.table, req.params.id, res));
11
+ }
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Route handlers
15
+ // ---------------------------------------------------------------------------
16
+
17
+ async function handleList(config: SynthOSConfig, table: string, query: Record<string, any>, res: Response): Promise<void> {
18
+ const sp = config.storageProvider;
19
+ const folder = sharedTableFolder(config, table);
20
+ if (!(await sp.checkIfExists(folder))) {
21
+ res.status(404).json({ error: 'table_not_found', table });
22
+ return;
23
+ }
24
+
25
+ const ids = (await sp.listFiles(folder)).filter(f => f.endsWith('.json')).map(f => f.replace('.json', ''));
26
+
27
+ const rows: Record<string, any>[] = [];
28
+ for (const id of ids) {
29
+ const file = recordFile(folder, id);
30
+ try {
31
+ const row = JSON.parse(await sp.loadFile(file));
32
+ row.id = id;
33
+ rows.push(row);
34
+ } catch (err: unknown) {
35
+ console.error(err);
36
+ }
37
+ }
38
+
39
+ // Paginate when limit is provided
40
+ const limitParam = typeof query.limit === 'string' ? parseInt(query.limit, 10) : NaN;
41
+ if (!isNaN(limitParam) && limitParam > 0) {
42
+ const offset = Math.max(0, typeof query.offset === 'string' ? parseInt(query.offset, 10) || 0 : 0);
43
+ const items = rows.slice(offset, offset + limitParam);
44
+ res.json({ items, total: rows.length, offset, limit: limitParam, hasMore: offset + limitParam < rows.length });
45
+ } else {
46
+ res.json(rows);
47
+ }
48
+ }
49
+
50
+ async function handleGet(config: SynthOSConfig, table: string, id: string, res: Response): Promise<void> {
51
+ const sp = config.storageProvider;
52
+ const folder = sharedTableFolder(config, table);
53
+ if (!(await sp.checkIfExists(folder))) {
54
+ res.status(404).json({ error: 'table_not_found', table });
55
+ return;
56
+ }
57
+
58
+ const file = recordFile(folder, id);
59
+ try {
60
+ const row = JSON.parse(await sp.loadFile(file));
61
+ row.id = id;
62
+ res.json(row);
63
+ } catch (err: unknown) {
64
+ res.json({});
65
+ }
66
+ }
67
+
68
+ async function handleUpsert(config: SynthOSConfig, table: string, body: any, res: Response): Promise<void> {
69
+ const sp = config.storageProvider;
70
+ const id = body.id ?? v4();
71
+ const folder = sharedTableFolder(config, table);
72
+ const file = recordFile(folder, id);
73
+ try {
74
+ const row = { ...body, id };
75
+ await sp.ensureFolderExists(folder);
76
+ await sp.saveFile(file, JSON.stringify(row, null, 4));
77
+ res.json(row);
78
+ } catch (err: unknown) {
79
+ console.error(err);
80
+ res.status(500).send((err as Error).message);
81
+ }
82
+ }
83
+
84
+ async function handleDelete(config: SynthOSConfig, table: string, id: string, res: Response): Promise<void> {
85
+ const sp = config.storageProvider;
86
+ const folder = sharedTableFolder(config, table);
87
+ const file = recordFile(folder, id);
88
+ try {
89
+ if (await sp.checkIfExists(file)) {
90
+ await sp.deleteFile(file);
91
+ }
92
+ res.json({ success: true });
93
+ } catch (err: unknown) {
94
+ console.error(err);
95
+ res.status(500).send((err as Error).message);
96
+ }
97
+ }
98
+
99
+ // ---------------------------------------------------------------------------
100
+ // Helpers
101
+ // ---------------------------------------------------------------------------
102
+
103
+ function sharedTableFolder(config: SynthOSConfig, table: string): string {
104
+ return path.join(config.pagesFolder, 'shared', table);
105
+ }
106
+
107
+ function recordFile(folder: string, id: string): string {
108
+ return path.join(folder, `${id}.json`);
109
+ }
@@ -0,0 +1,127 @@
1
+ import { Application } from 'express';
2
+ import express from 'express';
3
+ import path from 'path';
4
+ import { SynthOSConfig } from '../init';
5
+
6
+ export function useSharedFileRoutes(config: SynthOSConfig, app: Application): void {
7
+ const sp = config.storageProvider;
8
+
9
+ // List files in the shared files folder
10
+ app.get('/api/shared/files', async (req, res) => {
11
+ try {
12
+ const folder = sharedFilesFolder(config);
13
+ if (!(await sp.checkIfExists(folder))) {
14
+ res.json({ files: [] });
15
+ return;
16
+ }
17
+
18
+ const entries = await sp.listFiles(folder);
19
+ const files: { name: string; size: number }[] = [];
20
+ for (const entry of entries) {
21
+ const stat = await sp.stat(path.join(folder, entry));
22
+ if (stat.isFile) {
23
+ files.push({ name: entry, size: stat.size });
24
+ }
25
+ }
26
+ res.json({ files });
27
+ } catch (err: unknown) {
28
+ console.error(err);
29
+ res.status(500).json({ error: (err as Error).message });
30
+ }
31
+ });
32
+
33
+ // Download/serve a specific shared file
34
+ app.get('/api/shared/files/:filename', async (req, res) => {
35
+ try {
36
+ const filePath = safeFilePath(config, req.params.filename);
37
+ if (!filePath) {
38
+ res.status(400).json({ error: 'Invalid filename' });
39
+ return;
40
+ }
41
+
42
+ if (!(await sp.checkIfExists(filePath))) {
43
+ res.status(404).json({ error: 'File not found' });
44
+ return;
45
+ }
46
+
47
+ res.type(path.extname(req.params.filename));
48
+ sp.createReadStream(filePath).pipe(res);
49
+ } catch (err: unknown) {
50
+ console.error(err);
51
+ res.status(500).json({ error: (err as Error).message });
52
+ }
53
+ });
54
+
55
+ // Upload a shared file (raw body + x-filename header)
56
+ app.post('/api/shared/files', express.raw({ type: '*/*', limit: '50mb' }), async (req, res) => {
57
+ try {
58
+ const filename = req.headers['x-filename'] as string | undefined;
59
+ if (!filename || filename.trim().length === 0) {
60
+ res.status(400).json({ error: 'x-filename header is required' });
61
+ return;
62
+ }
63
+
64
+ const filePath = safeFilePath(config, filename);
65
+ if (!filePath) {
66
+ res.status(400).json({ error: 'Invalid filename' });
67
+ return;
68
+ }
69
+
70
+ const folder = sharedFilesFolder(config);
71
+ await sp.ensureFolderExists(folder);
72
+ await sp.saveBuffer(filePath, req.body as Buffer);
73
+
74
+ const stat = await sp.stat(filePath);
75
+ res.status(201).json({ name: filename, size: stat.size });
76
+ } catch (err: unknown) {
77
+ console.error(err);
78
+ res.status(500).json({ error: (err as Error).message });
79
+ }
80
+ });
81
+
82
+ // Delete a shared file
83
+ app.delete('/api/shared/files/:filename', async (req, res) => {
84
+ try {
85
+ const filePath = safeFilePath(config, req.params.filename);
86
+ if (!filePath) {
87
+ res.status(400).json({ error: 'Invalid filename' });
88
+ return;
89
+ }
90
+
91
+ if (!(await sp.checkIfExists(filePath))) {
92
+ res.status(404).json({ error: 'File not found' });
93
+ return;
94
+ }
95
+
96
+ await sp.deleteFile(filePath);
97
+ res.json({ deleted: true });
98
+ } catch (err: unknown) {
99
+ console.error(err);
100
+ res.status(500).json({ error: (err as Error).message });
101
+ }
102
+ });
103
+ }
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // Helpers
107
+ // ---------------------------------------------------------------------------
108
+
109
+ function sharedFilesFolder(config: SynthOSConfig): string {
110
+ return path.join(config.pagesFolder, 'shared', 'files');
111
+ }
112
+
113
+ /**
114
+ * Resolve a filename inside the shared files folder with path-traversal protection.
115
+ * Returns the absolute path if safe, or null if the filename is invalid.
116
+ */
117
+ function safeFilePath(config: SynthOSConfig, filename: string): string | null {
118
+ if (!filename || filename.includes('..') || filename.includes('/') || filename.includes('\\')) {
119
+ return null;
120
+ }
121
+ const folder = sharedFilesFolder(config);
122
+ const resolved = path.resolve(folder, filename);
123
+ if (!resolved.startsWith(path.resolve(folder))) {
124
+ return null;
125
+ }
126
+ return resolved;
127
+ }
package/src/settings.ts CHANGED
@@ -1,7 +1,7 @@
1
- import {checkIfExists, loadFile, saveFile} from './files';
2
1
  import path from 'path';
3
2
  import { ModelEntry, ProviderName, detectProvider } from './models';
4
3
  import { AgentConfig } from './agents';
4
+ import { SynthOSConfig } from './init';
5
5
 
6
6
  let _settings: Partial<SettingsV2>|undefined;
7
7
 
@@ -39,6 +39,7 @@ export interface SettingsV2 {
39
39
  services?: ServicesConfig;
40
40
  connectors?: ServicesConfig;
41
41
  agents?: AgentConfig[];
42
+ toolbarPosition?: 'left' | 'right';
42
43
  }
43
44
 
44
45
  export const DefaultSettings: SettingsV2 = {
@@ -66,6 +67,7 @@ export const DefaultSettings: SettingsV2 = {
66
67
  services: {},
67
68
  connectors: {},
68
69
  agents: [],
70
+ toolbarPosition: 'left',
69
71
  };
70
72
 
71
73
  /**
@@ -124,8 +126,8 @@ function migrateV1toV2(raw: Record<string, unknown>): SettingsV2 {
124
126
  };
125
127
  }
126
128
 
127
- export async function hasConfiguredSettings(folder: string): Promise<boolean> {
128
- const settings = await loadSettings(folder);
129
+ export async function hasConfiguredSettings(config: SynthOSConfig): Promise<boolean> {
130
+ const settings = await loadSettings(config);
129
131
  const builder = getModelEntry(settings, 'builder');
130
132
  if (typeof builder.configuration.apiKey !== 'string' || builder.configuration.apiKey.length == 0) {
131
133
  return false;
@@ -136,18 +138,19 @@ export async function hasConfiguredSettings(folder: string): Promise<boolean> {
136
138
  return true;
137
139
  }
138
140
 
139
- export async function loadSettings(folder: string): Promise<SettingsV2> {
141
+ export async function loadSettings(config: SynthOSConfig): Promise<SettingsV2> {
142
+ const sp = config.storageProvider;
140
143
  if (_settings == undefined) {
141
- const filename = path.join(folder, 'settings.json');
142
- if (await checkIfExists(filename)) {
144
+ const filename = path.join(config.pagesFolder, 'settings.json');
145
+ if (await sp.checkIfExists(filename)) {
143
146
  try {
144
- const raw = JSON.parse(await loadFile(filename));
147
+ const raw = JSON.parse(await sp.loadFile(filename));
145
148
  if (!raw.version) {
146
149
  // V1 file — migrate
147
150
  console.log('Migrating settings.json from v1 to v2...');
148
151
  const migrated = migrateV1toV2(raw);
149
152
  _settings = migrated;
150
- await saveFile(filename, JSON.stringify(migrated, null, 4));
153
+ await sp.saveFile(filename, JSON.stringify(migrated, null, 4));
151
154
  } else {
152
155
  _settings = raw as Partial<SettingsV2>;
153
156
  }
@@ -169,11 +172,12 @@ export async function loadSettings(folder: string): Promise<SettingsV2> {
169
172
  return merged;
170
173
  }
171
174
 
172
- export async function saveSettings(folder: string, settings: Partial<SettingsV2>): Promise<void> {
175
+ export async function saveSettings(config: SynthOSConfig, settings: Partial<SettingsV2>): Promise<void> {
176
+ const sp = config.storageProvider;
173
177
  _settings = {..._settings, ...settings};
174
178
  if (settings.models) {
175
179
  _settings.models = settings.models;
176
180
  }
177
181
  _settings.version = 2;
178
- await saveFile(path.join(folder, 'settings.json'), JSON.stringify(_settings, null, 4));
182
+ await sp.saveFile(path.join(config.pagesFolder, 'settings.json'), JSON.stringify(_settings, null, 4));
179
183
  }
@@ -0,0 +1,87 @@
1
+ import * as fs from 'fs/promises';
2
+ import { createReadStream } from 'fs';
3
+ import path from 'path';
4
+ import { StorageProvider } from './StorageProvider';
5
+
6
+ /**
7
+ * Default StorageProvider backed by the local filesystem via `fs/promises`.
8
+ * Mirrors the existing logic in `files.ts`.
9
+ */
10
+ export class FsStorageProvider implements StorageProvider {
11
+ // --- Text file operations ---
12
+
13
+ async checkIfExists(filePath: string): Promise<boolean> {
14
+ try {
15
+ await fs.access(filePath);
16
+ return true;
17
+ } catch {
18
+ return false;
19
+ }
20
+ }
21
+
22
+ async loadFile(filePath: string): Promise<string> {
23
+ return await fs.readFile(filePath, 'utf8');
24
+ }
25
+
26
+ async saveFile(filePath: string, content: string): Promise<void> {
27
+ await fs.writeFile(filePath, content, 'utf8');
28
+ }
29
+
30
+ async deleteFile(filePath: string): Promise<void> {
31
+ await fs.unlink(filePath);
32
+ }
33
+
34
+ // --- Binary file operations ---
35
+
36
+ async saveBuffer(filePath: string, data: Buffer): Promise<void> {
37
+ await fs.writeFile(filePath, data);
38
+ }
39
+
40
+ async loadBuffer(filePath: string): Promise<Buffer> {
41
+ return await fs.readFile(filePath);
42
+ }
43
+
44
+ async stat(filePath: string): Promise<{ size: number; isFile: boolean }> {
45
+ const s = await fs.stat(filePath);
46
+ return { size: s.size, isFile: s.isFile() };
47
+ }
48
+
49
+ createReadStream(filePath: string): NodeJS.ReadableStream {
50
+ return createReadStream(filePath);
51
+ }
52
+
53
+ // --- Directory operations ---
54
+
55
+ async listFiles(dirPath: string): Promise<string[]> {
56
+ return (await fs.readdir(dirPath)).filter(file => !file.startsWith('.') && file.includes('.'));
57
+ }
58
+
59
+ async listFolders(dirPath: string): Promise<string[]> {
60
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
61
+ return entries
62
+ .filter(entry => entry.isDirectory() && !entry.name.startsWith('.'))
63
+ .map(entry => entry.name);
64
+ }
65
+
66
+ async ensureFolderExists(dirPath: string): Promise<void> {
67
+ await fs.mkdir(dirPath, { recursive: true });
68
+ }
69
+
70
+ async deleteFolder(dirPath: string): Promise<void> {
71
+ await fs.rm(dirPath, { recursive: true });
72
+ }
73
+
74
+ async copyFolderRecursive(srcPath: string, destPath: string): Promise<void> {
75
+ await this.ensureFolderExists(destPath);
76
+ const entries = await fs.readdir(srcPath, { withFileTypes: true });
77
+ for (const entry of entries) {
78
+ const src = path.join(srcPath, entry.name);
79
+ const dest = path.join(destPath, entry.name);
80
+ if (entry.isDirectory()) {
81
+ await this.copyFolderRecursive(src, dest);
82
+ } else {
83
+ await fs.copyFile(src, dest);
84
+ }
85
+ }
86
+ }
87
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Abstraction over user-mutable storage (pages, settings, themes, data, uploads).
3
+ *
4
+ * All paths are absolute. Callers build paths with `path.join(config.pagesFolder, ...)`
5
+ * exactly as they do today — the provider simply executes the I/O against
6
+ * whatever backend it wraps (local filesystem, blob storage, etc.).
7
+ *
8
+ * Package/read-only content (required-pages, default-themes, static-files,
9
+ * service-connectors) stays on local filesystem and is NOT routed through
10
+ * this interface.
11
+ */
12
+ export interface StorageProvider {
13
+ // --- Text file operations ---
14
+
15
+ checkIfExists(filePath: string): Promise<boolean>;
16
+ loadFile(filePath: string): Promise<string>;
17
+ saveFile(filePath: string, content: string): Promise<void>;
18
+ deleteFile(filePath: string): Promise<void>;
19
+
20
+ // --- Binary file operations (uploads / images) ---
21
+
22
+ saveBuffer(filePath: string, data: Buffer): Promise<void>;
23
+ loadBuffer(filePath: string): Promise<Buffer>;
24
+ stat(filePath: string): Promise<{ size: number; isFile: boolean }>;
25
+ createReadStream(filePath: string): NodeJS.ReadableStream;
26
+
27
+ // --- Directory operations ---
28
+
29
+ listFiles(dirPath: string): Promise<string[]>;
30
+ listFolders(dirPath: string): Promise<string[]>;
31
+ ensureFolderExists(dirPath: string): Promise<void>;
32
+ deleteFolder(dirPath: string): Promise<void>;
33
+ copyFolderRecursive(srcPath: string, destPath: string): Promise<void>;
34
+ }
@@ -0,0 +1,2 @@
1
+ export { StorageProvider } from './StorageProvider';
2
+ export { FsStorageProvider } from './FsStorageProvider';
@@ -2,6 +2,7 @@ import yargs from "yargs";
2
2
  import { hideBin } from "yargs/helpers";
3
3
  import { server } from "./service";
4
4
  import { createConfig, init } from "./init";
5
+ import { customizer } from "./customizer";
5
6
 
6
7
  const dynamicImport = new Function('specifier', `return import(specifier)`);
7
8
 
@@ -16,7 +17,7 @@ export async function run() {
16
17
  default: 4242
17
18
  })
18
19
  .option('pages', {
19
- describe: `Include default pages when initializing a new .synthos folder.`,
20
+ describe: `Include default pages when initializing a new local folder.`,
20
21
  type: 'boolean',
21
22
  default: true
22
23
  })
@@ -32,9 +33,9 @@ export async function run() {
32
33
  })
33
34
  .demandOption([]);
34
35
  }, async (args) => {
35
- const config = createConfig('.synthos', { debug: args.debug, debugPageUpdates: args.debugPageUpdates });
36
+ const config = await createConfig(customizer.localFolder, { debug: args.debug, debugPageUpdates: args.debugPageUpdates }, customizer);
36
37
  await init(config, args.pages);
37
- await server(config).listen(args.port, async () => {
38
+ await server(config, customizer).listen(args.port, async () => {
38
39
  console.log(`SynthOS server is running on http://localhost:${args.port}`);
39
40
 
40
41
  // Open using default browser
package/src/themes.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import path from 'path';
2
- import { checkIfExists, listFiles, loadFile } from './files';
2
+ import { checkIfExists, findFileInFolders, listFiles, listFilesFromFolders, loadFile } from './files';
3
3
  import { SynthOSConfig } from './init';
4
4
 
5
5
  export const THEME_VERSION = 2;
@@ -28,10 +28,29 @@ export function parseThemeFilename(filename: string): { name: string; version: n
28
28
  }
29
29
 
30
30
  /**
31
- * Find the CSS file for a theme by name in a folder.
31
+ * Find the CSS file for a theme by name in a user folder (via storageProvider).
32
32
  * Prefers the highest-versioned file (e.g. name.v2.css over name.css).
33
33
  */
34
- async function findThemeCssFile(folder: string, name: string): Promise<{ path: string; version: number } | undefined> {
34
+ async function findUserThemeCssFile(config: SynthOSConfig, folder: string, name: string): Promise<{ path: string; version: number } | undefined> {
35
+ const sp = config.storageProvider;
36
+ if (!await sp.checkIfExists(folder)) return undefined;
37
+ const files = await sp.listFiles(folder);
38
+ let best: { path: string; version: number } | undefined;
39
+ for (const f of files) {
40
+ const parsed = parseThemeFilename(f);
41
+ if (parsed && parsed.name === name) {
42
+ if (!best || parsed.version > best.version) {
43
+ best = { path: path.join(folder, f), version: parsed.version };
44
+ }
45
+ }
46
+ }
47
+ return best;
48
+ }
49
+
50
+ /**
51
+ * Find the CSS file for a theme by name in a local-fs folder (package defaults).
52
+ */
53
+ async function findDefaultThemeCssFile(folder: string, name: string): Promise<{ path: string; version: number } | undefined> {
35
54
  if (!await checkIfExists(folder)) return undefined;
36
55
  const files = await listFiles(folder);
37
56
  let best: { path: string; version: number } | undefined;
@@ -46,16 +65,29 @@ async function findThemeCssFile(folder: string, name: string): Promise<{ path: s
46
65
  return best;
47
66
  }
48
67
 
68
+ export async function loadThemeVersion(name: string, config: SynthOSConfig): Promise<number> {
69
+ const local = await findUserThemeCssFile(config, userThemesFolder(config), name);
70
+ if (local) return local.version;
71
+ for (const folder of config.defaultThemesFolders) {
72
+ const def = await findDefaultThemeCssFile(folder, name);
73
+ if (def) return def.version;
74
+ }
75
+ return 1;
76
+ }
77
+
49
78
  export async function loadThemeInfo(name: string, config: SynthOSConfig): Promise<ThemeInfo | undefined> {
50
- // Check user's local themes first, then fall back to package defaults
79
+ const sp = config.storageProvider;
80
+
81
+ // Check user's local themes first (user storage)
51
82
  const localPath = path.join(userThemesFolder(config), `${name}.json`);
52
- if (await checkIfExists(localPath)) {
53
- const raw = await loadFile(localPath);
83
+ if (await sp.checkIfExists(localPath)) {
84
+ const raw = await sp.loadFile(localPath);
54
85
  return raw ? JSON.parse(raw) : undefined;
55
86
  }
56
87
 
57
- const defaultPath = path.join(config.defaultThemesFolder, `${name}.json`);
58
- if (await checkIfExists(defaultPath)) {
88
+ // Fall back to package defaults (local fs)
89
+ const defaultPath = await findFileInFolders(config.defaultThemesFolders, `${name}.json`);
90
+ if (defaultPath) {
59
91
  const raw = await loadFile(defaultPath);
60
92
  return raw ? JSON.parse(raw) : undefined;
61
93
  }
@@ -64,40 +96,44 @@ export async function loadThemeInfo(name: string, config: SynthOSConfig): Promis
64
96
  }
65
97
 
66
98
  export async function loadTheme(name: string, config: SynthOSConfig): Promise<string | undefined> {
67
- // Check user's local themes first, then fall back to package defaults
68
- const local = await findThemeCssFile(userThemesFolder(config), name);
99
+ const sp = config.storageProvider;
100
+
101
+ // Check user's local themes first (user storage)
102
+ const local = await findUserThemeCssFile(config, userThemesFolder(config), name);
69
103
  if (local) {
70
- return await loadFile(local.path);
104
+ return await sp.loadFile(local.path);
71
105
  }
72
106
 
73
- const def = await findThemeCssFile(config.defaultThemesFolder, name);
74
- if (def) {
75
- return await loadFile(def.path);
107
+ // Search all default theme folders (local fs)
108
+ for (const folder of config.defaultThemesFolders) {
109
+ const def = await findDefaultThemeCssFile(folder, name);
110
+ if (def) {
111
+ return await loadFile(def.path);
112
+ }
76
113
  }
77
114
 
78
115
  return undefined;
79
116
  }
80
117
 
81
118
  export async function listThemes(config: SynthOSConfig): Promise<string[]> {
119
+ const sp = config.storageProvider;
82
120
  const names = new Set<string>();
83
121
 
84
- // Collect from user's local themes folder
122
+ // Collect from user's local themes folder (user storage)
85
123
  const localFolder = userThemesFolder(config);
86
- if (await checkIfExists(localFolder)) {
87
- const files = await listFiles(localFolder);
124
+ if (await sp.checkIfExists(localFolder)) {
125
+ const files = await sp.listFiles(localFolder);
88
126
  for (const f of files) {
89
127
  const parsed = parseThemeFilename(f);
90
128
  if (parsed) names.add(parsed.name);
91
129
  }
92
130
  }
93
131
 
94
- // Collect from package defaults
95
- if (await checkIfExists(config.defaultThemesFolder)) {
96
- const files = await listFiles(config.defaultThemesFolder);
97
- for (const f of files) {
98
- const parsed = parseThemeFilename(f);
99
- if (parsed) names.add(parsed.name);
100
- }
132
+ // Collect from all default theme folders (local fs)
133
+ const defaultFiles = await listFilesFromFolders(config.defaultThemesFolders);
134
+ for (const f of defaultFiles) {
135
+ const parsed = parseThemeFilename(f);
136
+ if (parsed) names.add(parsed.name);
101
137
  }
102
138
 
103
139
  return Array.from(names).sort();
@@ -107,11 +143,12 @@ export async function listThemes(config: SynthOSConfig): Promise<string[]> {
107
143
  * Compare local theme versions against defaults and return themes that need upgrading.
108
144
  */
109
145
  export async function getOutdatedThemes(config: SynthOSConfig): Promise<string[]> {
146
+ const sp = config.storageProvider;
110
147
  const localFolder = userThemesFolder(config);
111
- if (!await checkIfExists(localFolder)) return [];
148
+ if (!await sp.checkIfExists(localFolder)) return [];
112
149
 
113
- const defaultFiles = await listFiles(config.defaultThemesFolder);
114
- const localFiles = await listFiles(localFolder);
150
+ const defaultFiles = await listFilesFromFolders(config.defaultThemesFolders);
151
+ const localFiles = await sp.listFiles(localFolder);
115
152
 
116
153
  // Build maps: theme name → highest version
117
154
  const defaultVersions = new Map<string, number>();
@@ -0,0 +1,12 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
2
+ <defs>
3
+ <linearGradient id="g" x1="0%" y1="0%" x2="100%" y2="100%">
4
+ <stop offset="0%" stop-color="#a855f7"/>
5
+ <stop offset="100%" stop-color="#3b82f6"/>
6
+ </linearGradient>
7
+ </defs>
8
+ <rect width="32" height="32" rx="7" fill="#0f0f13"/>
9
+ <path d="M9 11 C9 8.8 10.8 7 13 7 L20 7 C22.2 7 24 8.8 24 11 C24 13.2 22.2 15 20 15 L13 15 C10.8 15 9 16.8 9 19 C9 21.2 10.8 23 13 23 L20 23" stroke="url(#g)" stroke-width="3" stroke-linecap="round" fill="none"/>
10
+ <circle cx="9" cy="11" r="2" fill="#a855f7"/>
11
+ <circle cx="23" cy="23" r="2" fill="#3b82f6"/>
12
+ </svg>