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.
- package/README.md +5 -5
- package/default-pages/elevenlabs_effects_studio/chat-history.json +1 -0
- package/default-pages/elevenlabs_effects_studio/page.html +1345 -1363
- package/default-pages/elevenlabs_effects_studio/page.json +13 -11
- package/default-pages/elevenlabs_voice_studio/chat-history.json +1 -0
- package/default-pages/elevenlabs_voice_studio/page.html +782 -801
- package/default-pages/elevenlabs_voice_studio/page.json +13 -11
- package/default-pages/json_tools/chat-history.json +1 -0
- package/default-pages/json_tools/page.html +70 -90
- package/default-pages/json_tools/page.json +12 -10
- package/default-pages/my_notes/chat-history.json +1 -0
- package/default-pages/my_notes/page.html +115 -131
- package/default-pages/my_notes/page.json +14 -12
- package/default-pages/neon_asteroids/chat-history.json +1 -0
- package/default-pages/neon_asteroids/page.html +1777 -1803
- package/default-pages/neon_asteroids/page.json +14 -12
- package/default-pages/oregon_trail/chat-history.json +1 -0
- package/default-pages/oregon_trail/page.html +290 -307
- package/default-pages/oregon_trail/page.json +14 -12
- package/default-pages/solar_explorer/chat-history.json +1 -0
- package/default-pages/solar_explorer/page.html +1929 -1951
- package/default-pages/solar_explorer/page.json +14 -12
- package/default-pages/solar_tutorial/chat-history.json +1 -0
- package/default-pages/solar_tutorial/page.html +464 -478
- package/default-pages/solar_tutorial/page.json +12 -10
- package/default-pages/us_map/chat-history.json +1 -0
- package/default-pages/us_map/page.html +170 -193
- package/default-pages/us_map/page.json +14 -12
- package/default-pages/us_map/page.light.png +0 -0
- package/default-pages/us_map_1850/chat-history.json +1 -0
- package/default-pages/us_map_1850/page.html +302 -326
- package/default-pages/us_map_1850/page.json +14 -12
- package/default-pages/western_cities_1850/chat-history.json +1 -0
- package/default-pages/western_cities_1850/page.html +503 -527
- package/default-pages/western_cities_1850/page.json +14 -12
- package/default-themes/aurora-dawn.v3.css +15 -14
- package/default-themes/aurora-dusk.v3.css +26 -26
- package/default-themes/cosmos-dawn.v3.css +15 -14
- package/default-themes/cosmos-dusk.v3.css +26 -26
- package/default-themes/elemental-dawn.v3.css +200 -0
- package/default-themes/nebula-dawn.v3.css +15 -14
- package/default-themes/nebula-dusk.v3.css +24 -24
- package/default-themes/solar-flare-dawn.v3.css +15 -14
- package/default-themes/solar-flare-dusk.v3.css +26 -26
- package/dist/builders/anthropic.d.ts +26 -2
- package/dist/builders/anthropic.d.ts.map +1 -1
- package/dist/builders/anthropic.js +132 -31
- package/dist/builders/anthropic.js.map +1 -1
- package/dist/builders/claudecode.d.ts +13 -0
- package/dist/builders/claudecode.d.ts.map +1 -0
- package/dist/builders/claudecode.js +253 -0
- package/dist/builders/claudecode.js.map +1 -0
- package/dist/builders/index.d.ts +2 -1
- package/dist/builders/index.d.ts.map +1 -1
- package/dist/builders/index.js +8 -1
- package/dist/builders/index.js.map +1 -1
- package/dist/builders/openai.js +2 -1
- package/dist/builders/openai.js.map +1 -1
- package/dist/builders/types.d.ts +31 -7
- package/dist/builders/types.d.ts.map +1 -1
- package/dist/builders/types.js +60 -28
- package/dist/builders/types.js.map +1 -1
- package/dist/connectors/types.d.ts +8 -0
- package/dist/connectors/types.d.ts.map +1 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +13 -6
- package/dist/init.js.map +1 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +161 -14
- package/dist/migrations.js.map +1 -1
- package/dist/models/anthropic.d.ts +1 -0
- package/dist/models/anthropic.d.ts.map +1 -1
- package/dist/models/anthropic.js +129 -29
- package/dist/models/anthropic.js.map +1 -1
- package/dist/models/chainOfThought.d.ts.map +1 -1
- package/dist/models/chainOfThought.js +32 -19
- package/dist/models/chainOfThought.js.map +1 -1
- package/dist/models/index.d.ts +2 -2
- package/dist/models/index.d.ts.map +1 -1
- package/dist/models/index.js +2 -1
- package/dist/models/index.js.map +1 -1
- package/dist/models/providers.d.ts +1 -0
- package/dist/models/providers.d.ts.map +1 -1
- package/dist/models/providers.js +12 -4
- package/dist/models/providers.js.map +1 -1
- package/dist/models/types.d.ts +15 -1
- package/dist/models/types.d.ts.map +1 -1
- package/dist/models/types.js.map +1 -1
- package/dist/pages.d.ts +57 -8
- package/dist/pages.d.ts.map +1 -1
- package/dist/pages.js +258 -45
- package/dist/pages.js.map +1 -1
- package/dist/service/createCompletePrompt.d.ts.map +1 -1
- package/dist/service/createCompletePrompt.js +5 -0
- package/dist/service/createCompletePrompt.js.map +1 -1
- package/dist/service/mediaCache.d.ts +36 -0
- package/dist/service/mediaCache.d.ts.map +1 -0
- package/dist/service/mediaCache.js +182 -0
- package/dist/service/mediaCache.js.map +1 -0
- package/dist/service/pageValidator.d.ts +25 -0
- package/dist/service/pageValidator.d.ts.map +1 -0
- package/dist/service/pageValidator.js +315 -0
- package/dist/service/pageValidator.js.map +1 -0
- package/dist/service/server.d.ts.map +1 -1
- package/dist/service/server.js +4 -0
- package/dist/service/server.js.map +1 -1
- package/dist/service/sharedTableSchema.d.ts +73 -0
- package/dist/service/sharedTableSchema.d.ts.map +1 -0
- package/dist/service/sharedTableSchema.js +206 -0
- package/dist/service/sharedTableSchema.js.map +1 -0
- package/dist/service/transformPage.d.ts +49 -11
- package/dist/service/transformPage.d.ts.map +1 -1
- package/dist/service/transformPage.js +354 -241
- package/dist/service/transformPage.js.map +1 -1
- package/dist/service/useApiRoutes.d.ts.map +1 -1
- package/dist/service/useApiRoutes.js +288 -34
- package/dist/service/useApiRoutes.js.map +1 -1
- package/dist/service/useConnectorRoutes.d.ts.map +1 -1
- package/dist/service/useConnectorRoutes.js +170 -32
- package/dist/service/useConnectorRoutes.js.map +1 -1
- package/dist/service/useDataRoutes.d.ts.map +1 -1
- package/dist/service/useDataRoutes.js +59 -2
- package/dist/service/useDataRoutes.js.map +1 -1
- package/dist/service/useExtractRoutes.d.ts +4 -0
- package/dist/service/useExtractRoutes.d.ts.map +1 -0
- package/dist/service/useExtractRoutes.js +304 -0
- package/dist/service/useExtractRoutes.js.map +1 -0
- package/dist/service/usePageRoutes.d.ts +17 -0
- package/dist/service/usePageRoutes.d.ts.map +1 -1
- package/dist/service/usePageRoutes.js +1385 -483
- package/dist/service/usePageRoutes.js.map +1 -1
- package/dist/service/useSharedDataRoutes.d.ts.map +1 -1
- package/dist/service/useSharedDataRoutes.js +54 -2
- package/dist/service/useSharedDataRoutes.js.map +1 -1
- package/dist/settings.d.ts +27 -0
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +40 -1
- package/dist/settings.js.map +1 -1
- package/dist/themes.d.ts +0 -5
- package/dist/themes.d.ts.map +1 -1
- package/dist/themes.js +3 -95
- package/dist/themes.js.map +1 -1
- package/migration-rules/v2-to-v3.md +277 -119
- package/package.json +5 -1
- package/{default-pages/application → required-pages/_shell}/page.html +56 -42
- package/required-pages/_shell/page.json +14 -0
- package/required-pages/_starters/page.html +534 -0
- package/required-pages/_starters/page.json +12 -0
- package/required-pages/builder/page.html +353 -43
- package/required-pages/builder/page.json +12 -10
- package/required-pages/pages/page.html +697 -924
- package/required-pages/pages/page.json +12 -10
- package/required-pages/settings/page.html +1879 -1753
- package/required-pages/settings/page.json +12 -10
- package/required-pages/synthos_apis/page.html +834 -845
- package/required-pages/synthos_apis/page.json +12 -10
- package/required-pages/synthos_scripts/page.html +74 -88
- package/required-pages/synthos_scripts/page.json +12 -10
- package/scripts/append-instructions.py +90 -0
- package/scripts/audit-instructions.py +76 -0
- package/scripts/cleanup-shell-markup.mjs +112 -0
- package/service-connectors/buffer/connector.json +46 -0
- package/service-connectors/canva/connector.json +67 -0
- package/service-connectors/elevenlabs/connector.json +1 -1
- package/src/builders/anthropic.ts +155 -30
- package/src/builders/claudecode.ts +310 -0
- package/src/builders/index.ts +7 -1
- package/src/builders/openai.ts +2 -1
- package/src/builders/types.ts +93 -32
- package/src/connectors/types.ts +8 -0
- package/src/init.ts +13 -7
- package/src/migrations.ts +187 -16
- package/src/models/anthropic.ts +140 -30
- package/src/models/chainOfThought.ts +33 -18
- package/src/models/index.ts +2 -2
- package/src/models/providers.ts +12 -3
- package/src/models/types.ts +21 -1
- package/src/pages.ts +271 -35
- package/src/service/createCompletePrompt.ts +6 -0
- package/src/service/mediaCache.ts +206 -0
- package/src/service/pageValidator.ts +337 -0
- package/src/service/server.ts +4 -0
- package/src/service/sharedTableSchema.ts +236 -0
- package/src/service/transformPage.ts +370 -260
- package/src/service/useApiRoutes.ts +282 -32
- package/src/service/useConnectorRoutes.ts +189 -34
- package/src/service/useDataRoutes.ts +198 -116
- package/src/service/useExtractRoutes.ts +331 -0
- package/src/service/usePageRoutes.ts +1411 -394
- package/src/service/useSharedDataRoutes.ts +184 -109
- package/src/settings.ts +65 -0
- package/src/themes.ts +78 -180
- package/starters/blank_starter/chat-history.json +1 -0
- package/starters/blank_starter/page.dark.png +0 -0
- package/starters/blank_starter/page.html +47 -0
- package/starters/blank_starter/page.json +13 -0
- package/starters/blank_starter/page.light.png +0 -0
- package/starters/calculator_starter/chat-history.json +1 -0
- package/starters/calculator_starter/page.dark.png +0 -0
- package/starters/calculator_starter/page.html +232 -0
- package/starters/calculator_starter/page.json +13 -0
- package/starters/calculator_starter/page.light.png +0 -0
- package/starters/calendar_starter/chat-history.json +1 -0
- package/starters/calendar_starter/page.dark.png +0 -0
- package/starters/calendar_starter/page.html +495 -0
- package/starters/calendar_starter/page.json +13 -0
- package/starters/calendar_starter/page.light.png +0 -0
- package/starters/chat_starter/chat-history.json +1 -0
- package/starters/chat_starter/page.dark.png +0 -0
- package/starters/chat_starter/page.html +351 -0
- package/starters/chat_starter/page.json +13 -0
- package/starters/chat_starter/page.light.png +0 -0
- package/starters/checklist_starter/chat-history.json +1 -0
- package/starters/checklist_starter/page.dark.png +0 -0
- package/starters/checklist_starter/page.html +437 -0
- package/starters/checklist_starter/page.json +13 -0
- package/starters/checklist_starter/page.light.png +0 -0
- package/starters/dashboard_starter/chat-history.json +1 -0
- package/starters/dashboard_starter/page.dark.png +0 -0
- package/starters/dashboard_starter/page.html +195 -0
- package/starters/dashboard_starter/page.json +13 -0
- package/starters/dashboard_starter/page.light.png +0 -0
- package/starters/form_starter/chat-history.json +1 -0
- package/starters/form_starter/page.dark.png +0 -0
- package/starters/form_starter/page.html +313 -0
- package/starters/form_starter/page.json +13 -0
- package/starters/form_starter/page.light.png +0 -0
- package/starters/gallery_starter/chat-history.json +1 -0
- package/starters/gallery_starter/page.dark.png +0 -0
- package/starters/gallery_starter/page.html +418 -0
- package/starters/gallery_starter/page.json +13 -0
- package/starters/gallery_starter/page.light.png +0 -0
- package/starters/generator_starter/chat-history.json +1 -0
- package/starters/generator_starter/page.dark.png +0 -0
- package/starters/generator_starter/page.html +261 -0
- package/starters/generator_starter/page.json +13 -0
- package/starters/generator_starter/page.light.png +0 -0
- package/starters/index.html +538 -0
- package/starters/kanban_starter/chat-history.json +1 -0
- package/starters/kanban_starter/page.dark.png +0 -0
- package/starters/kanban_starter/page.html +432 -0
- package/starters/kanban_starter/page.json +13 -0
- package/starters/kanban_starter/page.light.png +0 -0
- package/starters/presentation_builder/chat-history.json +1 -0
- package/starters/presentation_builder/page.dark.png +0 -0
- package/starters/presentation_builder/page.html +970 -0
- package/starters/presentation_builder/page.json +15 -0
- package/starters/presentation_builder/page.light.png +0 -0
- package/starters/presentation_builder/presentation_voice/voice_config.json +9 -0
- package/starters/pulse_starter/chat-history.json +1 -0
- package/starters/pulse_starter/page.dark.png +0 -0
- package/starters/pulse_starter/page.html +698 -0
- package/starters/pulse_starter/page.json +13 -0
- package/starters/pulse_starter/page.light.png +0 -0
- package/starters/quiz_starter/chat-history.json +1 -0
- package/starters/quiz_starter/page.dark.png +0 -0
- package/starters/quiz_starter/page.html +292 -0
- package/starters/quiz_starter/page.json +13 -0
- package/starters/quiz_starter/page.light.png +0 -0
- package/starters/reference_starter/chat-history.json +1 -0
- package/starters/reference_starter/page.dark.png +0 -0
- package/starters/reference_starter/page.html +250 -0
- package/starters/reference_starter/page.json +13 -0
- package/starters/reference_starter/page.light.png +0 -0
- package/starters/retro_game_starter/chat-history.json +1 -0
- package/starters/retro_game_starter/page.dark.png +0 -0
- package/{default-pages → starters}/retro_game_starter/page.html +1281 -1308
- package/starters/retro_game_starter/page.json +15 -0
- package/starters/retro_game_starter/page.light.png +0 -0
- package/starters/roster_starter/chat-history.json +1 -0
- package/starters/roster_starter/page.dark.png +0 -0
- package/starters/roster_starter/page.html +600 -0
- package/starters/roster_starter/page.json +13 -0
- package/starters/roster_starter/page.light.png +0 -0
- package/starters/server.js +182 -0
- package/starters/start.cmd +1 -0
- package/starters/timeline_starter/chat-history.json +1 -0
- package/starters/timeline_starter/page.dark.png +0 -0
- package/starters/timeline_starter/page.html +446 -0
- package/starters/timeline_starter/page.json +13 -0
- package/starters/timeline_starter/page.light.png +0 -0
- package/starters/tutorial_starter/chat-history.json +1 -0
- package/starters/tutorial_starter/page.dark.png +0 -0
- package/starters/tutorial_starter/page.html +283 -0
- package/starters/tutorial_starter/page.json +13 -0
- package/starters/tutorial_starter/page.light.png +0 -0
- package/static-files/agent.v3.js +122 -0
- package/static-files/connector.v3.js +48 -0
- package/static-files/extract.v3.js +188 -0
- package/static-files/helpers.v3.js +50 -6
- package/static-files/page-bridge.js +114 -0
- package/static-files/page.v3.js +1292 -1290
- package/static-files/script.v3.js +32 -0
- package/static-files/server.v3.js +89 -0
- package/static-files/shell-bridge.v3.js +174 -0
- package/static-files/shell-modals.v3.js +521 -0
- package/static-files/{shell.css → shell.v3.css} +271 -22
- package/static-files/shell.v3.js +1865 -0
- package/static-files/storage.v3.js +176 -0
- package/tests/anthropic.spec.ts +42 -7
- package/tests/builders.spec.ts +72 -4
- package/tests/pageValidator.spec.ts +548 -0
- package/tests/profiles.spec.ts +122 -0
- package/tests/providers.spec.ts +1 -1
- package/tests/sharedTableSchema.spec.ts +242 -0
- package/tests/transformPage.spec.ts +62 -81
- package/default-pages/application/page.json +0 -10
- package/default-pages/retro_game_starter/page.json +0 -12
- package/default-pages/sidebar_page/page.html +0 -51
- package/default-pages/sidebar_page/page.json +0 -10
- package/default-pages/two-panel_page/page.html +0 -68
- 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.
|
|
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"
|
|
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-
|
|
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
|
-
//
|
|
140
|
-
|
|
141
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
247
|
-
if (classification === 'medium-change') return 'claude-sonnet-4-
|
|
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
|
|
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
|
}
|