synthos 0.10.0 → 0.11.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 (312) hide show
  1. package/README.md +5 -5
  2. package/default-pages/elevenlabs_effects_studio/chat-history.json +1 -0
  3. package/default-pages/elevenlabs_effects_studio/page.html +1345 -1363
  4. package/default-pages/elevenlabs_effects_studio/page.json +13 -11
  5. package/default-pages/elevenlabs_voice_studio/chat-history.json +1 -0
  6. package/default-pages/elevenlabs_voice_studio/page.html +782 -801
  7. package/default-pages/elevenlabs_voice_studio/page.json +13 -11
  8. package/default-pages/json_tools/chat-history.json +1 -0
  9. package/default-pages/json_tools/page.html +70 -90
  10. package/default-pages/json_tools/page.json +12 -10
  11. package/default-pages/my_notes/chat-history.json +1 -0
  12. package/default-pages/my_notes/page.html +115 -131
  13. package/default-pages/my_notes/page.json +14 -12
  14. package/default-pages/neon_asteroids/chat-history.json +1 -0
  15. package/default-pages/neon_asteroids/page.html +1777 -1803
  16. package/default-pages/neon_asteroids/page.json +14 -12
  17. package/default-pages/oregon_trail/chat-history.json +1 -0
  18. package/default-pages/oregon_trail/page.html +290 -307
  19. package/default-pages/oregon_trail/page.json +14 -12
  20. package/default-pages/solar_explorer/chat-history.json +1 -0
  21. package/default-pages/solar_explorer/page.html +1929 -1951
  22. package/default-pages/solar_explorer/page.json +14 -12
  23. package/default-pages/solar_tutorial/chat-history.json +1 -0
  24. package/default-pages/solar_tutorial/page.html +464 -478
  25. package/default-pages/solar_tutorial/page.json +12 -10
  26. package/default-pages/us_map/chat-history.json +1 -0
  27. package/default-pages/us_map/page.html +170 -193
  28. package/default-pages/us_map/page.json +14 -12
  29. package/default-pages/us_map/page.light.png +0 -0
  30. package/default-pages/us_map_1850/chat-history.json +1 -0
  31. package/default-pages/us_map_1850/page.html +302 -326
  32. package/default-pages/us_map_1850/page.json +14 -12
  33. package/default-pages/western_cities_1850/chat-history.json +1 -0
  34. package/default-pages/western_cities_1850/page.html +503 -527
  35. package/default-pages/western_cities_1850/page.json +14 -12
  36. package/default-themes/aurora-dawn.v3.css +15 -14
  37. package/default-themes/aurora-dusk.v3.css +26 -26
  38. package/default-themes/cosmos-dawn.v3.css +15 -14
  39. package/default-themes/cosmos-dusk.v3.css +26 -26
  40. package/default-themes/elemental-dawn.v3.css +200 -0
  41. package/default-themes/nebula-dawn.v3.css +15 -14
  42. package/default-themes/nebula-dusk.v3.css +24 -24
  43. package/default-themes/solar-flare-dawn.v3.css +15 -14
  44. package/default-themes/solar-flare-dusk.v3.css +26 -26
  45. package/dist/builders/anthropic.d.ts +26 -2
  46. package/dist/builders/anthropic.d.ts.map +1 -1
  47. package/dist/builders/anthropic.js +132 -31
  48. package/dist/builders/anthropic.js.map +1 -1
  49. package/dist/builders/claudecode.d.ts +13 -0
  50. package/dist/builders/claudecode.d.ts.map +1 -0
  51. package/dist/builders/claudecode.js +253 -0
  52. package/dist/builders/claudecode.js.map +1 -0
  53. package/dist/builders/index.d.ts +2 -1
  54. package/dist/builders/index.d.ts.map +1 -1
  55. package/dist/builders/index.js +8 -1
  56. package/dist/builders/index.js.map +1 -1
  57. package/dist/builders/openai.js +2 -1
  58. package/dist/builders/openai.js.map +1 -1
  59. package/dist/builders/types.d.ts +31 -7
  60. package/dist/builders/types.d.ts.map +1 -1
  61. package/dist/builders/types.js +60 -28
  62. package/dist/builders/types.js.map +1 -1
  63. package/dist/connectors/types.d.ts +8 -0
  64. package/dist/connectors/types.d.ts.map +1 -1
  65. package/dist/init.d.ts.map +1 -1
  66. package/dist/init.js +13 -6
  67. package/dist/init.js.map +1 -1
  68. package/dist/migrations.d.ts.map +1 -1
  69. package/dist/migrations.js +161 -14
  70. package/dist/migrations.js.map +1 -1
  71. package/dist/models/anthropic.d.ts +1 -0
  72. package/dist/models/anthropic.d.ts.map +1 -1
  73. package/dist/models/anthropic.js +129 -29
  74. package/dist/models/anthropic.js.map +1 -1
  75. package/dist/models/chainOfThought.d.ts.map +1 -1
  76. package/dist/models/chainOfThought.js +32 -19
  77. package/dist/models/chainOfThought.js.map +1 -1
  78. package/dist/models/index.d.ts +2 -2
  79. package/dist/models/index.d.ts.map +1 -1
  80. package/dist/models/index.js +2 -1
  81. package/dist/models/index.js.map +1 -1
  82. package/dist/models/providers.d.ts +1 -0
  83. package/dist/models/providers.d.ts.map +1 -1
  84. package/dist/models/providers.js +12 -4
  85. package/dist/models/providers.js.map +1 -1
  86. package/dist/models/types.d.ts +15 -1
  87. package/dist/models/types.d.ts.map +1 -1
  88. package/dist/models/types.js.map +1 -1
  89. package/dist/pages.d.ts +57 -8
  90. package/dist/pages.d.ts.map +1 -1
  91. package/dist/pages.js +258 -45
  92. package/dist/pages.js.map +1 -1
  93. package/dist/service/createCompletePrompt.d.ts.map +1 -1
  94. package/dist/service/createCompletePrompt.js +5 -0
  95. package/dist/service/createCompletePrompt.js.map +1 -1
  96. package/dist/service/mediaCache.d.ts +36 -0
  97. package/dist/service/mediaCache.d.ts.map +1 -0
  98. package/dist/service/mediaCache.js +182 -0
  99. package/dist/service/mediaCache.js.map +1 -0
  100. package/dist/service/pageValidator.d.ts +25 -0
  101. package/dist/service/pageValidator.d.ts.map +1 -0
  102. package/dist/service/pageValidator.js +315 -0
  103. package/dist/service/pageValidator.js.map +1 -0
  104. package/dist/service/server.d.ts.map +1 -1
  105. package/dist/service/server.js +4 -0
  106. package/dist/service/server.js.map +1 -1
  107. package/dist/service/sharedTableSchema.d.ts +73 -0
  108. package/dist/service/sharedTableSchema.d.ts.map +1 -0
  109. package/dist/service/sharedTableSchema.js +206 -0
  110. package/dist/service/sharedTableSchema.js.map +1 -0
  111. package/dist/service/transformPage.d.ts +49 -11
  112. package/dist/service/transformPage.d.ts.map +1 -1
  113. package/dist/service/transformPage.js +354 -241
  114. package/dist/service/transformPage.js.map +1 -1
  115. package/dist/service/useApiRoutes.d.ts.map +1 -1
  116. package/dist/service/useApiRoutes.js +288 -34
  117. package/dist/service/useApiRoutes.js.map +1 -1
  118. package/dist/service/useConnectorRoutes.d.ts.map +1 -1
  119. package/dist/service/useConnectorRoutes.js +170 -32
  120. package/dist/service/useConnectorRoutes.js.map +1 -1
  121. package/dist/service/useDataRoutes.d.ts.map +1 -1
  122. package/dist/service/useDataRoutes.js +59 -2
  123. package/dist/service/useDataRoutes.js.map +1 -1
  124. package/dist/service/useExtractRoutes.d.ts +4 -0
  125. package/dist/service/useExtractRoutes.d.ts.map +1 -0
  126. package/dist/service/useExtractRoutes.js +304 -0
  127. package/dist/service/useExtractRoutes.js.map +1 -0
  128. package/dist/service/usePageRoutes.d.ts +17 -0
  129. package/dist/service/usePageRoutes.d.ts.map +1 -1
  130. package/dist/service/usePageRoutes.js +1385 -483
  131. package/dist/service/usePageRoutes.js.map +1 -1
  132. package/dist/service/useSharedDataRoutes.d.ts.map +1 -1
  133. package/dist/service/useSharedDataRoutes.js +54 -2
  134. package/dist/service/useSharedDataRoutes.js.map +1 -1
  135. package/dist/settings.d.ts +27 -0
  136. package/dist/settings.d.ts.map +1 -1
  137. package/dist/settings.js +40 -1
  138. package/dist/settings.js.map +1 -1
  139. package/dist/themes.d.ts +0 -5
  140. package/dist/themes.d.ts.map +1 -1
  141. package/dist/themes.js +3 -95
  142. package/dist/themes.js.map +1 -1
  143. package/migration-rules/v2-to-v3.md +277 -119
  144. package/package.json +5 -1
  145. package/{default-pages/application → required-pages/_shell}/page.html +56 -42
  146. package/required-pages/_shell/page.json +14 -0
  147. package/required-pages/_starters/page.html +534 -0
  148. package/required-pages/_starters/page.json +12 -0
  149. package/required-pages/builder/page.html +353 -43
  150. package/required-pages/builder/page.json +12 -10
  151. package/required-pages/pages/page.html +697 -924
  152. package/required-pages/pages/page.json +12 -10
  153. package/required-pages/settings/page.html +1879 -1753
  154. package/required-pages/settings/page.json +12 -10
  155. package/required-pages/synthos_apis/page.html +834 -845
  156. package/required-pages/synthos_apis/page.json +12 -10
  157. package/required-pages/synthos_scripts/page.html +74 -88
  158. package/required-pages/synthos_scripts/page.json +12 -10
  159. package/scripts/append-instructions.py +90 -0
  160. package/scripts/audit-instructions.py +76 -0
  161. package/scripts/cleanup-shell-markup.mjs +112 -0
  162. package/service-connectors/buffer/connector.json +46 -0
  163. package/service-connectors/canva/connector.json +67 -0
  164. package/service-connectors/elevenlabs/connector.json +1 -1
  165. package/src/builders/anthropic.ts +155 -30
  166. package/src/builders/claudecode.ts +310 -0
  167. package/src/builders/index.ts +7 -1
  168. package/src/builders/openai.ts +2 -1
  169. package/src/builders/types.ts +93 -32
  170. package/src/connectors/types.ts +8 -0
  171. package/src/init.ts +13 -7
  172. package/src/migrations.ts +187 -16
  173. package/src/models/anthropic.ts +140 -30
  174. package/src/models/chainOfThought.ts +33 -18
  175. package/src/models/index.ts +2 -2
  176. package/src/models/providers.ts +12 -3
  177. package/src/models/types.ts +21 -1
  178. package/src/pages.ts +271 -35
  179. package/src/service/createCompletePrompt.ts +6 -0
  180. package/src/service/mediaCache.ts +206 -0
  181. package/src/service/pageValidator.ts +337 -0
  182. package/src/service/server.ts +4 -0
  183. package/src/service/sharedTableSchema.ts +236 -0
  184. package/src/service/transformPage.ts +370 -260
  185. package/src/service/useApiRoutes.ts +282 -32
  186. package/src/service/useConnectorRoutes.ts +189 -34
  187. package/src/service/useDataRoutes.ts +198 -116
  188. package/src/service/useExtractRoutes.ts +331 -0
  189. package/src/service/usePageRoutes.ts +1411 -394
  190. package/src/service/useSharedDataRoutes.ts +184 -109
  191. package/src/settings.ts +65 -0
  192. package/src/themes.ts +78 -180
  193. package/starters/blank_starter/chat-history.json +1 -0
  194. package/starters/blank_starter/page.dark.png +0 -0
  195. package/starters/blank_starter/page.html +47 -0
  196. package/starters/blank_starter/page.json +13 -0
  197. package/starters/blank_starter/page.light.png +0 -0
  198. package/starters/calculator_starter/chat-history.json +1 -0
  199. package/starters/calculator_starter/page.dark.png +0 -0
  200. package/starters/calculator_starter/page.html +232 -0
  201. package/starters/calculator_starter/page.json +13 -0
  202. package/starters/calculator_starter/page.light.png +0 -0
  203. package/starters/calendar_starter/chat-history.json +1 -0
  204. package/starters/calendar_starter/page.dark.png +0 -0
  205. package/starters/calendar_starter/page.html +495 -0
  206. package/starters/calendar_starter/page.json +13 -0
  207. package/starters/calendar_starter/page.light.png +0 -0
  208. package/starters/chat_starter/chat-history.json +1 -0
  209. package/starters/chat_starter/page.dark.png +0 -0
  210. package/starters/chat_starter/page.html +351 -0
  211. package/starters/chat_starter/page.json +13 -0
  212. package/starters/chat_starter/page.light.png +0 -0
  213. package/starters/checklist_starter/chat-history.json +1 -0
  214. package/starters/checklist_starter/page.dark.png +0 -0
  215. package/starters/checklist_starter/page.html +437 -0
  216. package/starters/checklist_starter/page.json +13 -0
  217. package/starters/checklist_starter/page.light.png +0 -0
  218. package/starters/dashboard_starter/chat-history.json +1 -0
  219. package/starters/dashboard_starter/page.dark.png +0 -0
  220. package/starters/dashboard_starter/page.html +195 -0
  221. package/starters/dashboard_starter/page.json +13 -0
  222. package/starters/dashboard_starter/page.light.png +0 -0
  223. package/starters/form_starter/chat-history.json +1 -0
  224. package/starters/form_starter/page.dark.png +0 -0
  225. package/starters/form_starter/page.html +313 -0
  226. package/starters/form_starter/page.json +13 -0
  227. package/starters/form_starter/page.light.png +0 -0
  228. package/starters/gallery_starter/chat-history.json +1 -0
  229. package/starters/gallery_starter/page.dark.png +0 -0
  230. package/starters/gallery_starter/page.html +418 -0
  231. package/starters/gallery_starter/page.json +13 -0
  232. package/starters/gallery_starter/page.light.png +0 -0
  233. package/starters/generator_starter/chat-history.json +1 -0
  234. package/starters/generator_starter/page.dark.png +0 -0
  235. package/starters/generator_starter/page.html +261 -0
  236. package/starters/generator_starter/page.json +13 -0
  237. package/starters/generator_starter/page.light.png +0 -0
  238. package/starters/index.html +538 -0
  239. package/starters/kanban_starter/chat-history.json +1 -0
  240. package/starters/kanban_starter/page.dark.png +0 -0
  241. package/starters/kanban_starter/page.html +432 -0
  242. package/starters/kanban_starter/page.json +13 -0
  243. package/starters/kanban_starter/page.light.png +0 -0
  244. package/starters/presentation_builder/chat-history.json +1 -0
  245. package/starters/presentation_builder/page.dark.png +0 -0
  246. package/starters/presentation_builder/page.html +970 -0
  247. package/starters/presentation_builder/page.json +15 -0
  248. package/starters/presentation_builder/page.light.png +0 -0
  249. package/starters/presentation_builder/presentation_voice/voice_config.json +9 -0
  250. package/starters/pulse_starter/chat-history.json +1 -0
  251. package/starters/pulse_starter/page.dark.png +0 -0
  252. package/starters/pulse_starter/page.html +698 -0
  253. package/starters/pulse_starter/page.json +13 -0
  254. package/starters/pulse_starter/page.light.png +0 -0
  255. package/starters/quiz_starter/chat-history.json +1 -0
  256. package/starters/quiz_starter/page.dark.png +0 -0
  257. package/starters/quiz_starter/page.html +292 -0
  258. package/starters/quiz_starter/page.json +13 -0
  259. package/starters/quiz_starter/page.light.png +0 -0
  260. package/starters/reference_starter/chat-history.json +1 -0
  261. package/starters/reference_starter/page.dark.png +0 -0
  262. package/starters/reference_starter/page.html +250 -0
  263. package/starters/reference_starter/page.json +13 -0
  264. package/starters/reference_starter/page.light.png +0 -0
  265. package/starters/retro_game_starter/chat-history.json +1 -0
  266. package/starters/retro_game_starter/page.dark.png +0 -0
  267. package/{default-pages → starters}/retro_game_starter/page.html +1281 -1308
  268. package/starters/retro_game_starter/page.json +15 -0
  269. package/starters/retro_game_starter/page.light.png +0 -0
  270. package/starters/roster_starter/chat-history.json +1 -0
  271. package/starters/roster_starter/page.dark.png +0 -0
  272. package/starters/roster_starter/page.html +600 -0
  273. package/starters/roster_starter/page.json +13 -0
  274. package/starters/roster_starter/page.light.png +0 -0
  275. package/starters/server.js +182 -0
  276. package/starters/start.cmd +1 -0
  277. package/starters/timeline_starter/chat-history.json +1 -0
  278. package/starters/timeline_starter/page.dark.png +0 -0
  279. package/starters/timeline_starter/page.html +446 -0
  280. package/starters/timeline_starter/page.json +13 -0
  281. package/starters/timeline_starter/page.light.png +0 -0
  282. package/starters/tutorial_starter/chat-history.json +1 -0
  283. package/starters/tutorial_starter/page.dark.png +0 -0
  284. package/starters/tutorial_starter/page.html +283 -0
  285. package/starters/tutorial_starter/page.json +13 -0
  286. package/starters/tutorial_starter/page.light.png +0 -0
  287. package/static-files/agent.v3.js +122 -0
  288. package/static-files/connector.v3.js +48 -0
  289. package/static-files/extract.v3.js +188 -0
  290. package/static-files/helpers.v3.js +50 -6
  291. package/static-files/page-bridge.js +114 -0
  292. package/static-files/page.v3.js +1292 -1290
  293. package/static-files/script.v3.js +32 -0
  294. package/static-files/server.v3.js +89 -0
  295. package/static-files/shell-bridge.v3.js +174 -0
  296. package/static-files/shell-modals.v3.js +521 -0
  297. package/static-files/{shell.css → shell.v3.css} +271 -22
  298. package/static-files/shell.v3.js +1865 -0
  299. package/static-files/storage.v3.js +176 -0
  300. package/tests/anthropic.spec.ts +42 -7
  301. package/tests/builders.spec.ts +72 -4
  302. package/tests/pageValidator.spec.ts +548 -0
  303. package/tests/profiles.spec.ts +122 -0
  304. package/tests/providers.spec.ts +1 -1
  305. package/tests/sharedTableSchema.spec.ts +242 -0
  306. package/tests/transformPage.spec.ts +62 -81
  307. package/default-pages/application/page.json +0 -10
  308. package/default-pages/retro_game_starter/page.json +0 -12
  309. package/default-pages/sidebar_page/page.html +0 -51
  310. package/default-pages/sidebar_page/page.json +0 -10
  311. package/default-pages/two-panel_page/page.html +0 -68
  312. package/default-pages/two-panel_page/page.json +0 -10
@@ -0,0 +1,67 @@
1
+ {
2
+ "id": "canva",
3
+ "name": "Canva",
4
+ "category": "Design",
5
+ "description": "Create, autofill, export, and manage Canva designs, brand templates, and assets via the Canva Connect API.",
6
+ "baseUrl": "https://api.canva.com/rest/v1",
7
+ "authStrategy": "oauth2",
8
+ "authKey": "Authorization",
9
+ "authorizationUrl": "https://www.canva.com/api/oauth/authorize",
10
+ "tokenUrl": "https://api.canva.com/rest/v1/oauth/token",
11
+ "usePkce": true,
12
+ "scopeSeparator": " ",
13
+ "scopes": [
14
+ "profile:read",
15
+ "design:meta:read",
16
+ "design:content:read",
17
+ "design:content:write",
18
+ "asset:read",
19
+ "asset:write",
20
+ "brandtemplate:meta:read",
21
+ "brandtemplate:content:read",
22
+ "folder:read"
23
+ ],
24
+ "fields": [
25
+ { "name": "clientId", "label": "Client ID", "type": "text" },
26
+ { "name": "clientSecret", "label": "Client Secret", "type": "password" }
27
+ ],
28
+ "hints": [
29
+ "REST API. Auth is OAuth 2.0 with PKCE (S256) — required by Canva for ALL clients (public AND confidential). The proxy handles the PKCE handshake automatically.",
30
+ "User profile: GET /users/me/profile — returns the authenticated user's display_name and user_id. Use this as a connection smoke test.",
31
+ "List designs: GET /designs?query=&continuation=&ownership=any&sort_by=relevance",
32
+ " Cursor pagination via `continuation` (opaque string). Pass back the value from the previous response's `continuation` field to fetch the next page.",
33
+ "Get a design: GET /designs/{designId} — returns title, urls (edit_url, view_url), thumbnail, page_count.",
34
+ "Create a blank design: POST /designs Body: { design_type: { type: \"preset\", name: \"presentation\" } } Other preset names: doc, whiteboard, presentation, instagrampost, instagramstory, facebookcover, etc. Or use { type: \"custom\", width, height } for custom dimensions.",
35
+ "Export a design (PNG, PDF, JPG, MP4, GIF, PPTX) — async two-step:",
36
+ " 1) POST /exports Body: { design_id, format: { type: \"png\" } } Response: { job: { id, status: \"in_progress\" } }",
37
+ " 2) Poll GET /exports/{jobId} until status === \"success\". Response then includes `urls` (signed download URLs, expire after a short window — fetch immediately).",
38
+ " Format options: png (no compression flag), jpg (quality 1-100), pdf (size, pages), mp4 (quality), gif, pptx.",
39
+ "Asset upload — binary only, no URL pass-through. Two-step async:",
40
+ " 1) POST /asset-uploads Headers: Content-Type: application/octet-stream, Asset-Upload-Metadata: <base64url JSON {name_base64, tags?}> Body: raw bytes.",
41
+ " 2) Poll GET /asset-uploads/{jobId} until status === \"success\". Response then includes asset.id (use in autofill / design references).",
42
+ " Pexels URLs cannot be passed directly — fetch the image, then upload the bytes here. Max ~25MB per asset.",
43
+ "List brand templates: GET /brand-templates?query=&continuation= Returns templates the user has access to (Canva Enterprise required for full brand template access).",
44
+ "Get template dataset (autofill schema): GET /brand-templates/{brandTemplateId}/dataset Returns the named text fields and image placeholders the template accepts.",
45
+ "Autofill a brand template — async two-step (very high-value for programmatic design generation):",
46
+ " 1) POST /autofills Body: { brand_template_id, data: { <field_name>: { type: \"text\" | \"image\" | \"chart\", text?: \"...\", asset_id?: \"...\" } } } Response: { job: { id, status: \"in_progress\" } }",
47
+ " 2) Poll GET /autofills/{jobId} until status === \"success\". Response includes design_id of the new design — then fetch via /designs/{designId} for edit_url/thumbnail or export it.",
48
+ " Image fields require an asset_id from a prior /asset-uploads — you cannot pass a URL.",
49
+ "Comments: GET/POST /designs/{designId}/comments and /designs/{designId}/comments/{commentId}/replies (requires comment:read / comment:write scopes — not in default scope list above; add if needed).",
50
+ "Folders: GET /folders/{folderId}, GET /folders/{folderId}/items?continuation= — list designs/templates within a folder.",
51
+ "Async job pattern (used by exports, asset uploads, autofills): poll the GET endpoint with a small backoff (start 500ms, cap ~5s). Status values: in_progress, success, failed. On failed the response includes an `error` object with `code` and `message`.",
52
+ "Error shape: { code: \"<machine_code>\", message: \"...\" } with appropriate HTTP status (400/401/403/404/429/5xx). Common codes: invalid_request, unauthorized, forbidden, not_found, rate_limit_exceeded, internal_error.",
53
+ "Rate limits: per-token request budgets. On 429, honor the Retry-After response header (seconds).",
54
+ "Refresh tokens: Canva rotates refresh tokens — every refresh returns a new refresh_token and invalidates the old one. The proxy stores the latest automatically; do not cache refresh tokens client-side.",
55
+ "IMPORTANT: Do NOT include access_token in body or query params — the proxy attaches the Authorization: Bearer header automatically."
56
+ ],
57
+ "onboarding": {
58
+ "url": "https://www.canva.com/developers/",
59
+ "steps": [
60
+ "Sign in to Canva and go to canva.com/developers",
61
+ "Create a new integration (Connect API) and choose 'Public' or 'Team' depending on who will use it",
62
+ "Add the redirect URI shown in Synthos (typically http://localhost:<port>/api/connectors/callback) to the integration's allowed redirect URIs",
63
+ "Enable the scopes the integration needs: profile:read, design:meta:read, design:content:read, design:content:write, asset:read, asset:write, brandtemplate:meta:read, brandtemplate:content:read, folder:read",
64
+ "Copy the Client ID and Client Secret into the fields below"
65
+ ]
66
+ }
67
+ }
@@ -15,7 +15,7 @@
15
15
  " Request headers: Accept: audio/mpeg, Content-Type: application/json",
16
16
  " Request body: { text: string, model_id: \"eleven_multilingual_v2\" }",
17
17
  " Response: raw audio/mpeg binary — use resp.arrayBuffer() then new Blob([buf], {type:\"audio/mpeg\"}) and URL.createObjectURL() to play",
18
- " IMPORTANT: The proxy returns raw binary, NOT JSON. Call fetch(\"/api/connectors\", ...) directly instead of synthos.connectors.call() for TTS, since the helper parses JSON.",
18
+ " IMPORTANT: The proxy returns raw binary, NOT JSON. Call fetch(\"/api/connectors\", ...) directly instead of synthos.connector.call() for TTS, since the helper parses JSON.",
19
19
  "Default voice: \"Rachel\" (voice_id: 21m00Tcm4TlvDq8ikWAM) is a good general-purpose voice.",
20
20
  "Max text length: 5000 characters per request."
21
21
  ],
@@ -1,4 +1,5 @@
1
1
  import { anthropic as createAnthropicModel, completePrompt, ContentBlock } from '../models';
2
+ import { ToolDefinition, ToolHandler } from '../models/types';
2
3
  import { parseChangeList, getTransformInstr } from '../service/transformPage';
3
4
  import { Attachment, Builder, BuilderResult, CHANGE_OPS_SCHEMA, CHANGE_OPS_SCHEMA_NO_RANGED, ContextSection } from './types';
4
5
 
@@ -15,6 +16,10 @@ export interface AnthropicBuilderOptions {
15
16
  isFirstEdit?: boolean;
16
17
  /** When true, retry the last edit — forces Opus + no ranged writes, skips classification. */
17
18
  tryAgain?: boolean;
19
+ /** Number of messages in the chat history (used for early-conversation bypass). */
20
+ historyLength?: number;
21
+ /** Pre-computed classification result — skips re-classifying in the builder. */
22
+ preClassified?: ClassifyResult;
18
23
  }
19
24
 
20
25
  // ---------------------------------------------------------------------------
@@ -23,13 +28,23 @@ export interface AnthropicBuilderOptions {
23
28
 
24
29
  export type Classification = 'question' | 'easy-change' | 'medium-change' | 'hard-change' | 're-write';
25
30
 
31
+ /** Sketches passed to the classifier so it can decide which sections to expand. */
32
+ export interface ClassifierSection {
33
+ /** Section title (e.g. "<CONFIGURED_AGENTS>"). */
34
+ title: string;
35
+ /** Compact one-line summary the classifier reads. */
36
+ sketch: string;
37
+ }
38
+
26
39
  export interface ClassifyResult {
27
40
  classification: Classification;
28
41
  /** When classification is "question", this contains the answer text. */
29
42
  answer?: string;
43
+ /** Section titles the classifier picked to render in full. Empty = render all sketches. */
44
+ expand: string[];
30
45
  }
31
46
 
32
- const CLASSIFIER_SYSTEM_PROMPT = `You classify user messages for a web page builder. Default to a change request. Only classify as "question" when the user is purely asking for information with zero implication that anything should change.
47
+ const CLASSIFIER_SYSTEM_PROMPT = `You classify user messages for a web page builder and pick which context sections the builder needs. Default to a change request. Only classify as "question" when the user is purely asking for information with zero implication that anything should change.
33
48
 
34
49
  <DECISION_RULES>
35
50
  Step 1 — Is this a pure information question with zero change implication?
@@ -43,40 +58,91 @@ Step 2 — Classify complexity:
43
58
  "hard-change": Complex new features, significant new JS, games, animations, restructuring layout, forms with validation, multi-step work.
44
59
  "re-write": Complete page overhaul, fundamental restructure, "start over", "rebuild", "redo the whole page".
45
60
 
61
+ Step 3 — Which context sections does the model need to fulfill this request?
62
+ Read the AVAILABLE_SECTIONS list in the user message — each line is "<SECTION_TITLE>: <one-line sketch>" describing what's in the section.
63
+ Return an "expand" array of section titles (matching exactly, including the angle brackets) the model needs in full.
64
+ Include a title only if the page currently uses that feature OR the user's request will likely require it.
65
+ Sections you don't expand will still be visible to the model as their sketch — so the model knows the section exists, just not the full content.
66
+ Omit sections that are clearly unrelated to this request.
67
+
46
68
  <OUTPUT_FORMAT>
47
69
  Return only JSON. No other text.
48
- - Change: { "classification": "easy-change" } or { "classification": "medium-change" } or { "classification": "hard-change" } or { "classification": "re-write" }
49
- - Question: { "classification": "question", "answer": "<brief answer>" }`;
70
+ - Change: { "classification": "easy-change", "expand": ["<FLUENTLM_COMPONENTS>"] }
71
+ - Question: { "classification": "question", "answer": "<brief answer>", "expand": [] }`;
50
72
 
51
73
  export async function classifyRequest(
52
74
  apiKey: string,
53
75
  pageHtml: string,
54
- userMessage: string
76
+ userMessage: string,
77
+ sections?: ClassifierSection[],
55
78
  ): Promise<ClassifyResult> {
56
79
  try {
57
- const sonnet = createAnthropicModel({ apiKey, model: 'claude-sonnet-4-5' });
80
+ const sonnet = createAnthropicModel({ apiKey, model: 'claude-sonnet-4-6' });
81
+ const sectionList = (sections ?? [])
82
+ .map(s => `${s.title}: ${s.sketch}`)
83
+ .join('\n');
84
+ const sectionsBlock = sectionList
85
+ ? `\n\n<AVAILABLE_SECTIONS>\n${sectionList}`
86
+ : '';
58
87
  const result = await sonnet({
59
88
  system: { role: 'system', content: CLASSIFIER_SYSTEM_PROMPT },
60
- prompt: { role: 'user', content: `<PAGE_HTML>\n${pageHtml}\n\n<USER_MESSAGE>\n${userMessage}` },
89
+ prompt: { role: 'user', content: `<PAGE_HTML>\n${pageHtml}\n\n<USER_MESSAGE>\n${userMessage}${sectionsBlock}` },
61
90
  jsonMode: true,
62
91
  });
63
92
 
64
93
  if (!result.completed || !result.value) {
65
- return { classification: 'hard-change' };
94
+ return { classification: 'hard-change', expand: [] };
66
95
  }
67
96
 
68
97
  const parsed = JSON.parse(result.value);
69
98
  const c = parsed.classification;
99
+ const allowed = new Set((sections ?? []).map(s => s.title));
100
+ const expand = parseExpand(parsed.expand, allowed);
101
+
70
102
  if (c === 'question') {
71
- return { classification: 'question', answer: typeof parsed.answer === 'string' ? parsed.answer : '' };
103
+ return { classification: 'question', answer: typeof parsed.answer === 'string' ? parsed.answer : '', expand };
72
104
  }
73
105
  if (c === 'easy-change' || c === 'medium-change' || c === 'hard-change' || c === 're-write') {
74
- return { classification: c };
106
+ return { classification: c, expand };
75
107
  }
76
- return { classification: 'hard-change' };
108
+ return { classification: 'hard-change', expand };
77
109
  } catch {
78
- return { classification: 'hard-change' };
110
+ return { classification: 'hard-change', expand: [] };
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Validate the classifier's expand[] against the set of titles it was offered.
116
+ * Unknown / non-string entries are dropped silently.
117
+ */
118
+ function parseExpand(raw: unknown, allowed: Set<string>): string[] {
119
+ if (!Array.isArray(raw)) return [];
120
+ const out: string[] = [];
121
+ for (const item of raw) {
122
+ if (typeof item === 'string' && allowed.has(item)) {
123
+ out.push(item);
124
+ }
79
125
  }
126
+ return out;
127
+ }
128
+
129
+ // ---------------------------------------------------------------------------
130
+ // Lightweight question heuristic — replaces Sonnet classifier for Q&A detection
131
+ // ---------------------------------------------------------------------------
132
+
133
+ const QUESTION_WORDS = /^(?:what|how|why|where|when|which|is|are|does|do|can|could|should|would|will)\b/i;
134
+ const ACTION_VERBS = /\b(?:add|change|make|create|build|fix|update|remove|delete|move|replace|resize|rewrite|redo|redesign)\b/i;
135
+
136
+ /**
137
+ * Returns true when the message is very likely a pure information question
138
+ * (no intent to modify the page). Used to bypass the full Sonnet classifier.
139
+ */
140
+ export function isLikelyQuestion(message: string): boolean {
141
+ const trimmed = message.trim();
142
+ if (!trimmed.endsWith('?')) return false;
143
+ if (!QUESTION_WORDS.test(trimmed)) return false;
144
+ if (ACTION_VERBS.test(trimmed)) return false;
145
+ return true;
80
146
  }
81
147
 
82
148
  // ---------------------------------------------------------------------------
@@ -100,32 +166,32 @@ export function createAnthropicBuilder(
100
166
  const name = productName ?? 'SynthOS';
101
167
 
102
168
  return {
103
- async run(currentPage, additionalSections, userMessage, newBuild, attachments?): Promise<BuilderResult> {
169
+ async run(currentPage, additionalSections, userMessage, newBuild, attachments?, tools?, toolHandlers?, onToolCall?): Promise<BuilderResult> {
104
170
  try {
105
171
  const isOpus = options?.model?.startsWith('claude-opus-');
106
172
  const noRanged = CHANGE_OPS_SCHEMA_NO_RANGED;
107
173
 
108
174
  // Non-Opus models or missing apiKey: existing behavior
109
175
  if (!isOpus || !options?.apiKey) {
110
- return buildWithModel(complete, currentPage, additionalSections, userMessage, userInstructions, name, attachments);
176
+ return buildWithModel(complete, currentPage, additionalSections, userMessage, userInstructions, name, attachments, undefined, tools, toolHandlers, onToolCall);
111
177
  }
112
178
 
113
179
  // Console errors bypass classification — always route to Opus with no ranged writes
114
180
  if (userMessage.includes('CONSOLE_ERRORS:')) {
115
181
  console.log('classifyRequest: console errors detected → routing to ' + options.model! + ' (no ranged)');
116
- return buildWithModel(complete, currentPage, additionalSections, userMessage, userInstructions, name, attachments, noRanged);
182
+ return buildWithModel(complete, currentPage, additionalSections, userMessage, userInstructions, name, attachments, noRanged, tools, toolHandlers, onToolCall);
117
183
  }
118
184
 
119
185
  // New builds always use Opus with no ranged writes
120
186
  if (newBuild) {
121
187
  console.log('classifyRequest: new build → routing to ' + options.model! + ' (no ranged)');
122
- return buildWithModel(complete, currentPage, additionalSections, userMessage, userInstructions, name, attachments, noRanged);
188
+ return buildWithModel(complete, currentPage, additionalSections, userMessage, userInstructions, name, attachments, noRanged, tools, toolHandlers, onToolCall);
123
189
  }
124
190
 
125
191
  // First edit to a saved page (v0→v1) — force Opus with no ranged writes
126
192
  if (options?.isFirstEdit) {
127
193
  console.log('classifyRequest: first edit (v0→v1) → routing to ' + options.model! + ' (no ranged)');
128
- return buildWithModel(complete, currentPage, additionalSections, userMessage, userInstructions, name, attachments, noRanged);
194
+ return buildWithModel(complete, currentPage, additionalSections, userMessage, userInstructions, name, attachments, noRanged, tools, toolHandlers, onToolCall);
129
195
  }
130
196
 
131
197
  // Try again — force Opus with no ranged writes, skip classification
@@ -133,12 +199,54 @@ export function createAnthropicBuilder(
133
199
  console.log('classifyRequest: try again → routing to claude-opus-4-6 (no ranged)');
134
200
  let opus: completePrompt = createAnthropicModel({ apiKey: options.apiKey!, model: 'claude-opus-4-6' });
135
201
  if (options.wrapModel) opus = options.wrapModel(opus);
136
- return buildWithModel(opus, currentPage, additionalSections, userMessage, userInstructions, name, attachments, noRanged);
202
+ return buildWithModel(opus, currentPage, additionalSections, userMessage, userInstructions, name, attachments, noRanged, tools, toolHandlers, onToolCall);
137
203
  }
138
204
 
139
- // Classify the request using Sonnet
140
- const classifyResult = await classifyRequest(options.apiKey, currentPage.content, userMessage);
141
- console.log(`classifyRequest: "${classifyResult.classification}" routing to ${routeLabel(classifyResult.classification, options.model!)}`);
205
+ // --- Opt-1: Smart classification bypass ---
206
+
207
+ // Small pages (<5KB) Opus cost delta is negligible; classifier cost exceeds savings
208
+ if (currentPage.content.length < 5_000) {
209
+ console.log('classifyRequest: small page (<5KB) → routing to ' + options.model! + ' (no ranged, bypass)');
210
+ return buildWithModel(complete, currentPage, additionalSections, userMessage, userInstructions, name, attachments, noRanged, tools, toolHandlers, onToolCall);
211
+ }
212
+
213
+ // Image attachments — always structural changes; classifier can't inspect images
214
+ if (attachments && attachments.length > 0) {
215
+ console.log('classifyRequest: image attachments → routing to ' + options.model! + ' (no ranged, bypass)');
216
+ return buildWithModel(complete, currentPage, additionalSections, userMessage, userInstructions, name, attachments, noRanged, tools, toolHandlers, onToolCall);
217
+ }
218
+
219
+ // Early conversations (history <= 2) — almost always builds/edits
220
+ if (typeof options.historyLength === 'number' && options.historyLength <= 2) {
221
+ console.log('classifyRequest: early conversation (history <= 2) → routing to ' + options.model! + ' (no ranged, bypass)');
222
+ return buildWithModel(complete, currentPage, additionalSections, userMessage, userInstructions, name, attachments, noRanged, tools, toolHandlers, onToolCall);
223
+ }
224
+
225
+ // Keyword-based Q&A detection — lightweight heuristic replaces full-page Sonnet call
226
+ if (isLikelyQuestion(userMessage)) {
227
+ console.log('classifyRequest: likely question (heuristic) → routing to claude-sonnet-4-6 (Q&A, no page HTML)');
228
+ let sonnet: completePrompt = createAnthropicModel({ apiKey: options.apiKey, model: 'claude-sonnet-4-6' });
229
+ if (options.wrapModel) sonnet = options.wrapModel(sonnet);
230
+ // Lightweight Q&A call — send only user message + page summary, not full HTML
231
+ const pageTitle = currentPage.content.match(/<title[^>]*>([^<]*)<\/title>/i)?.[1] ?? 'untitled';
232
+ const elementCount = (currentPage.content.match(/<[a-z]/gi) ?? []).length;
233
+ const pageSummary = `Page: "${pageTitle}" (${elementCount} elements)`;
234
+ const qaResult = await sonnet({
235
+ system: { role: 'system', content: 'You are a helpful assistant for a web page builder. Answer the user\'s question briefly and accurately based on the page summary provided.' },
236
+ prompt: { role: 'user', content: `${pageSummary}\n\n${userMessage}` },
237
+ });
238
+ if (qaResult.completed && qaResult.value) {
239
+ return { kind: 'reply', text: qaResult.value };
240
+ }
241
+ // Fallback to classifier if Q&A call fails
242
+ }
243
+
244
+ // Classify the request — use pre-computed result if available, otherwise call Sonnet
245
+ const classifyResult = options.preClassified
246
+ ?? await classifyRequest(options.apiKey, currentPage.content, userMessage);
247
+ if (!options.preClassified) {
248
+ console.log(`classifyRequest: "${classifyResult.classification}" → routing to ${routeLabel(classifyResult.classification, options.model!)}`);
249
+ }
142
250
 
143
251
  // Questions — answer was already provided by the classifier
144
252
  if (classifyResult.classification === 'question') {
@@ -147,20 +255,20 @@ export function createAnthropicBuilder(
147
255
 
148
256
  // Easy changes use Sonnet with default schema (ranged allowed)
149
257
  if (classifyResult.classification === 'easy-change') {
150
- let sonnet: completePrompt = createAnthropicModel({ apiKey: options.apiKey, model: 'claude-sonnet-4-5' });
258
+ let sonnet: completePrompt = createAnthropicModel({ apiKey: options.apiKey, model: 'claude-sonnet-4-6' });
151
259
  if (options.wrapModel) sonnet = options.wrapModel(sonnet);
152
- return buildWithModel(sonnet, currentPage, additionalSections, userMessage, userInstructions, name, attachments);
260
+ return buildWithModel(sonnet, currentPage, additionalSections, userMessage, userInstructions, name, attachments, undefined, tools, toolHandlers, onToolCall);
153
261
  }
154
262
 
155
263
  // Medium changes use Sonnet with no ranged writes
156
264
  if (classifyResult.classification === 'medium-change') {
157
- let sonnet: completePrompt = createAnthropicModel({ apiKey: options.apiKey, model: 'claude-sonnet-4-5' });
265
+ let sonnet: completePrompt = createAnthropicModel({ apiKey: options.apiKey, model: 'claude-sonnet-4-6' });
158
266
  if (options.wrapModel) sonnet = options.wrapModel(sonnet);
159
- return buildWithModel(sonnet, currentPage, additionalSections, userMessage, userInstructions, name, attachments, noRanged);
267
+ return buildWithModel(sonnet, currentPage, additionalSections, userMessage, userInstructions, name, attachments, noRanged, tools, toolHandlers, onToolCall);
160
268
  }
161
269
 
162
270
  // Hard changes and re-writes use Opus with no ranged writes
163
- return buildWithModel(complete, currentPage, additionalSections, userMessage, userInstructions, name, attachments, noRanged);
271
+ return buildWithModel(complete, currentPage, additionalSections, userMessage, userInstructions, name, attachments, noRanged, tools, toolHandlers, onToolCall);
164
272
  } catch (err: unknown) {
165
273
  return { kind: 'error', error: err instanceof Error ? err : new Error(String(err)) };
166
274
  }
@@ -172,6 +280,12 @@ export function createAnthropicBuilder(
172
280
  // Build flow — shared prompt construction + model call + parsing
173
281
  // ---------------------------------------------------------------------------
174
282
 
283
+ /**
284
+ * Appended to <INSTRUCTIONS> when tools are bound so the model knows it may
285
+ * fetch additional context on demand.
286
+ */
287
+ export const TOOLS_INSTRUCTION_HINT = 'You may call the provided tools to load additional context only if you need it. You may call multiple tools in parallel in a single turn.';
288
+
175
289
  export async function buildWithModel(
176
290
  model: completePrompt,
177
291
  currentPage: ContextSection,
@@ -180,7 +294,10 @@ export async function buildWithModel(
180
294
  userInstructions: string | undefined,
181
295
  productName: string,
182
296
  attachments?: Attachment[],
183
- outputSchema?: Record<string, unknown>
297
+ outputSchema?: Record<string, unknown>,
298
+ tools?: ToolDefinition[],
299
+ toolHandlers?: Record<string, ToolHandler>,
300
+ onToolCall?: (names: string[]) => void,
184
301
  ): Promise<BuilderResult> {
185
302
  // -- System message: all static content (cacheable) --
186
303
  const systemParts: string[] = [];
@@ -200,6 +317,9 @@ export async function buildWithModel(
200
317
  }
201
318
  }
202
319
  instructionParts.push(getTransformInstr(productName));
320
+ if (tools && tools.length > 0) {
321
+ instructionParts.push(TOOLS_INSTRUCTION_HINT);
322
+ }
203
323
  const instructions = instructionParts.filter(s => s.trim() !== '').join('\n');
204
324
  systemParts.push(`<INSTRUCTIONS>\n${instructions}`);
205
325
 
@@ -227,6 +347,7 @@ export async function buildWithModel(
227
347
  prompt: { role: 'user', content: promptContent },
228
348
  cacheSystem: true,
229
349
  outputSchema: outputSchema ?? CHANGE_OPS_SCHEMA,
350
+ ...(tools && tools.length > 0 && { tools, toolHandlers: toolHandlers ?? {}, onToolCall }),
230
351
  });
231
352
 
232
353
  if (!result.completed) {
@@ -243,8 +364,8 @@ export async function buildWithModel(
243
364
 
244
365
  function routeLabel(classification: Classification, configuredModel: string): string {
245
366
  if (classification === 'question') return 'classifier (answered inline)';
246
- if (classification === 'easy-change') return 'claude-sonnet-4-5';
247
- if (classification === 'medium-change') return 'claude-sonnet-4-5 (no ranged)';
367
+ if (classification === 'easy-change') return 'claude-sonnet-4-6';
368
+ if (classification === 'medium-change') return 'claude-sonnet-4-6 (no ranged)';
248
369
  if (classification === 're-write') return configuredModel + ' (re-write)';
249
370
  return configuredModel + ' (no ranged)';
250
371
  }
@@ -254,10 +375,14 @@ function routeLabel(classification: Classification, configuredModel: string): st
254
375
  // ---------------------------------------------------------------------------
255
376
 
256
377
  export function parseBuilderResponse(raw: string): BuilderResult {
257
- // Try parsing as a JSON object with a kind discriminator
378
+ // Try parsing as a JSON object with a changes array + optional message
258
379
  try {
259
380
  const parsed = JSON.parse(raw);
260
381
  if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
382
+ if (Array.isArray(parsed.changes)) {
383
+ const message = typeof parsed.message === 'string' ? parsed.message : undefined;
384
+ return { kind: 'transforms', changes: parsed.changes, message };
385
+ }
261
386
  if (parsed.kind === 'transforms' && Array.isArray(parsed.changes)) {
262
387
  return { kind: 'transforms', changes: parsed.changes };
263
388
  }
@@ -265,7 +390,7 @@ export function parseBuilderResponse(raw: string): BuilderResult {
265
390
  return { kind: 'reply', text: parsed.text };
266
391
  }
267
392
  }
268
- // Bare array — backward compat
393
+ // Bare array — backward compat (no message field available)
269
394
  if (Array.isArray(parsed)) {
270
395
  return { kind: 'transforms', changes: parsed };
271
396
  }