synthos 0.10.1 → 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 (311) 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 +150 -25
  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 +10 -1
  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 +70 -2
  302. package/tests/pageValidator.spec.ts +548 -0
  303. package/tests/profiles.spec.ts +122 -0
  304. package/tests/sharedTableSchema.spec.ts +242 -0
  305. package/tests/transformPage.spec.ts +62 -81
  306. package/default-pages/application/page.json +0 -10
  307. package/default-pages/retro_game_starter/page.json +0 -12
  308. package/default-pages/sidebar_page/page.html +0 -51
  309. package/default-pages/sidebar_page/page.json +0 -10
  310. package/default-pages/two-panel_page/page.html +0 -68
  311. package/default-pages/two-panel_page/page.json +0 -10
@@ -1,7 +1,9 @@
1
1
  import { AgentCompletion } from "../models";
2
+ import { ToolDefinition, ToolHandler } from "../models/types";
2
3
  import * as cheerio from "cheerio";
3
4
  import { Customizer } from "../customizer";
4
5
  import { Attachment, Builder, ContextSection } from "../builders/types";
6
+ import { PageValidationResult } from "./pageValidator";
5
7
 
6
8
  // ---------------------------------------------------------------------------
7
9
  // Types
@@ -19,6 +21,14 @@ export interface TransformPageArgs {
19
21
  productName?: string;
20
22
  /** Optional image attachments sent alongside the user message. */
21
23
  attachments?: Attachment[];
24
+ /** Chat history from the shell — used for newBuild detection instead of counting DOM elements. */
25
+ history?: { role: string; content: string }[];
26
+ /** Optional tool definitions exposed to the builder (on-demand context loading). */
27
+ tools?: ToolDefinition[];
28
+ /** Executors keyed by tool name. Required when `tools` is provided. */
29
+ toolHandlers?: Record<string, ToolHandler>;
30
+ /** Fired once per tool-use iteration with the names of tools about to execute. */
31
+ onToolCall?: (names: string[]) => void;
22
32
  }
23
33
 
24
34
  export type ChangeOp =
@@ -39,6 +49,22 @@ export type ChangeList = ChangeOp[];
39
49
  export interface TransformPageResult {
40
50
  html: string;
41
51
  changeCount: number;
52
+ /** For 'reply' results — the assistant's text response (shell displays it). */
53
+ replyText?: string;
54
+ /** For 'error' results — the error message (shell displays it). */
55
+ errorText?: string;
56
+ /** Page validation results (undefined if validation was skipped). */
57
+ validation?: PageValidationResult;
58
+ /** Number of change ops that were silently skipped (missing target, locked, etc.). */
59
+ skippedOps?: number;
60
+ /** Short human-readable reasons for each skipped op, in order. */
61
+ skipReasons?: string[];
62
+ }
63
+
64
+ export interface ApplyChangeListReport {
65
+ html: string;
66
+ skippedOps: number;
67
+ skipReasons: string[];
42
68
  }
43
69
 
44
70
  export async function transformPage(args: TransformPageArgs): Promise<AgentCompletion<TransformPageResult>> {
@@ -47,182 +73,86 @@ export async function transformPage(args: TransformPageArgs): Promise<AgentCompl
47
73
  // 0. Strip the early error-capture script so the LLM never sees it
48
74
  const pageState = stripErrorCapture(args.pageState);
49
75
 
50
- // 1. Assign data-node-id to every element
76
+ // 1. Assign data-nid to every element
51
77
  const { html: annotatedHtml } = assignNodeIds(pageState);
52
78
 
79
+ // 1b. Strip HTML comments from the annotated copy sent to the LLM (saves tokens).
80
+ // The original pageState is preserved — changes are applied to the annotated copy
81
+ // and then node IDs are stripped, so comments survive in the saved page.
82
+ const llmHtml = annotatedHtml.replace(/<!--[\s\S]*?-->/g, '');
83
+
53
84
  try {
54
85
  // 2. Build CURRENT_PAGE section
55
86
  const currentPage: ContextSection = {
56
87
  title: '<CURRENT_PAGE>',
57
- content: annotatedHtml,
88
+ content: llmHtml,
89
+ sketch: null,
90
+ mode: 'always-full',
58
91
  instructions: '',
59
92
  };
60
93
 
61
- // 3. Determine newBuild: if isBuilder, count .chat-message in annotated HTML
94
+ // 3. Determine newBuild: if isBuilder, check chat history length
62
95
  let newBuild = false;
63
96
  if (args.isBuilder) {
64
- const $ = cheerio.load(annotatedHtml, { decodeEntities: false });
65
- const messageCount = $('#chatMessages .chat-message').length;
66
- newBuild = messageCount <= 1;
97
+ if (args.history) {
98
+ // Shell provides explicit history — empty or greeting-only means new build
99
+ newBuild = args.history.length <= 1;
100
+ } else {
101
+ // Fallback: count .chat-message in annotated HTML (legacy pages without chat-history.json)
102
+ const $ = cheerio.load(annotatedHtml, { decodeEntities: false });
103
+ const messageCount = $('#chatMessages .chat-message').length;
104
+ newBuild = messageCount <= 1;
105
+ }
67
106
  }
68
107
 
69
- // 4. Call builder
70
- const result = await builder.run(currentPage, additionalSections, message, newBuild, args.attachments);
108
+ // 4. Call builder (with timeout guard)
109
+ const TRANSFORM_TIMEOUT_MS = 600_000; // 10 minutes
110
+ const result = await Promise.race([
111
+ builder.run(currentPage, additionalSections, message, newBuild, args.attachments, args.tools, args.toolHandlers, args.onToolCall),
112
+ new Promise<never>((_, reject) =>
113
+ setTimeout(() => reject(new Error('Page transform timed out — the AI took too long to respond. Please try again.')), TRANSFORM_TIMEOUT_MS)
114
+ ),
115
+ ]);
71
116
 
72
117
  // 5. Switch on result kind
73
118
  switch (result.kind) {
74
119
  case 'transforms': {
75
- const applied = applyChangeList(annotatedHtml, result.changes);
76
- const clean = stripNodeIds(applied);
77
- const deduped = deduplicateInlineScripts(clean);
78
- const safe = ensureScriptsBeforeBodyClose(deduped);
79
- return { completed: true, value: { html: safe, changeCount: result.changes.length } };
120
+ const report = applyChangeListWithReport(annotatedHtml, result.changes);
121
+ const safe = postProcessHtml(report.html);
122
+ return { completed: true, value: {
123
+ html: safe,
124
+ changeCount: result.changes.length,
125
+ replyText: result.message,
126
+ skippedOps: report.skippedOps,
127
+ skipReasons: report.skipReasons,
128
+ } };
80
129
  }
81
130
  case 'reply': {
82
- const productName = args.productName ?? 'SynthOS';
83
- const withReply = appendChatReply(annotatedHtml, message, result.text, productName);
84
- const clean = stripNodeIds(withReply);
85
- const deduped = deduplicateInlineScripts(clean);
86
- const safe = ensureScriptsBeforeBodyClose(deduped);
87
- return { completed: true, value: { html: safe, changeCount: -1 } };
131
+ const safe = postProcessHtml(annotatedHtml);
132
+ return { completed: true, value: { html: safe, changeCount: -1, replyText: result.text } };
88
133
  }
89
134
  case 'error': {
90
- const productName = args.productName ?? 'SynthOS';
91
- const errorHtml = appendChatError(annotatedHtml, message, result.error.message, productName);
92
- const clean = stripNodeIds(errorHtml);
93
- return { completed: true, value: { html: clean, changeCount: -1 } };
135
+ const clean = stripNodeIds(annotatedHtml);
136
+ return { completed: true, value: { html: clean, changeCount: -1, errorText: result.error.message } };
94
137
  }
95
138
  }
96
139
  } catch (err: unknown) {
97
- // On any error: append error message to chat
98
- const productName = args.productName ?? 'SynthOS';
99
140
  const errorMessage = err instanceof Error ? err.message : String(err);
100
- const errorHtml = appendChatError(annotatedHtml, message, errorMessage, productName);
101
- const clean = stripNodeIds(errorHtml);
102
- return { completed: true, value: { html: clean, changeCount: -1 } };
141
+ const clean = stripNodeIds(annotatedHtml);
142
+ return { completed: true, value: { html: clean, changeCount: -1, errorText: errorMessage } };
103
143
  }
104
144
  }
105
145
 
106
- // ---------------------------------------------------------------------------
107
- // Chat reply helper
108
- // ---------------------------------------------------------------------------
109
-
110
- /**
111
- * Append a user message and a reply to #chatMessages using cheerio.
112
- */
113
- function appendChatReply(annotatedHtml: string, userMessage: string, replyText: string, productName: string): string {
114
- const $ = cheerio.load(annotatedHtml, { decodeEntities: false });
115
- const chatMessages = $('#chatMessages');
116
- if (chatMessages.length > 0) {
117
- chatMessages.append(`<div class="chat-message"><p><strong>User:</strong> ${escapeHtml(userMessage)}</p></div>`);
118
- const replyHtml = simpleMarkdown(replyText);
119
- chatMessages.append(`<div class="chat-message"><p><strong>${escapeHtml(productName)}:</strong> ${replyHtml}</p></div>`);
120
- }
121
- return $.html();
122
- }
123
-
124
- function escapeHtml(text: string): string {
125
- return text
126
- .replace(/&/g, '&amp;')
127
- .replace(/</g, '&lt;')
128
- .replace(/>/g, '&gt;')
129
- .replace(/"/g, '&quot;');
130
- }
131
-
132
- /**
133
- * Lightweight markdown-to-HTML converter for chat reply text.
134
- * Handles: code blocks, inline code, bold, italic, links, unordered/ordered lists, paragraphs.
135
- */
136
- export function simpleMarkdown(text: string): string {
137
- // Extract fenced code blocks first to protect their contents
138
- const codeBlocks: string[] = [];
139
- let processed = text.replace(/```(\w*)\n([\s\S]*?)```/g, (_match, lang, code) => {
140
- const idx = codeBlocks.length;
141
- const escaped = escapeHtml(code.replace(/\n$/, ''));
142
- const langAttr = lang ? ` class="language-${escapeHtml(lang)}"` : '';
143
- codeBlocks.push(`<pre><code${langAttr}>${escaped}</code></pre>`);
144
- return `\x00CODEBLOCK${idx}\x00`;
145
- });
146
-
147
- // Split into paragraphs by blank lines
148
- const blocks = processed.split(/\n{2,}/);
149
- const htmlBlocks: string[] = [];
150
-
151
- for (const block of blocks) {
152
- const trimmed = block.trim();
153
- if (!trimmed) continue;
154
-
155
- // Code block placeholder
156
- if (/^\x00CODEBLOCK\d+\x00$/.test(trimmed)) {
157
- htmlBlocks.push(trimmed);
158
- continue;
159
- }
160
-
161
- // Unordered list (lines starting with - or *)
162
- if (/^[\-\*] /m.test(trimmed) && trimmed.split('\n').every(l => /^[\-\*] /.test(l.trim()) || l.trim() === '')) {
163
- const items = trimmed.split('\n')
164
- .map(l => l.trim())
165
- .filter(l => l)
166
- .map(l => `<li>${inlineMarkdown(l.replace(/^[\-\*] /, ''))}</li>`)
167
- .join('');
168
- htmlBlocks.push(`<ul>${items}</ul>`);
169
- continue;
170
- }
171
-
172
- // Ordered list (lines starting with 1. 2. etc.)
173
- if (/^\d+\. /m.test(trimmed) && trimmed.split('\n').every(l => /^\d+\. /.test(l.trim()) || l.trim() === '')) {
174
- const items = trimmed.split('\n')
175
- .map(l => l.trim())
176
- .filter(l => l)
177
- .map(l => `<li>${inlineMarkdown(l.replace(/^\d+\. /, ''))}</li>`)
178
- .join('');
179
- htmlBlocks.push(`<ol>${items}</ol>`);
180
- continue;
181
- }
182
-
183
- // Regular paragraph
184
- htmlBlocks.push(`<p>${inlineMarkdown(trimmed.replace(/\n/g, '<br>'))}</p>`);
185
- }
186
-
187
- // Restore code blocks
188
- let result = htmlBlocks.join('');
189
- result = result.replace(/\x00CODEBLOCK(\d+)\x00/g, (_m, idx) => codeBlocks[parseInt(idx)]);
190
- return result;
191
- }
192
-
193
- /** Apply inline markdown formatting: bold, italic, inline code, links. */
194
- function inlineMarkdown(text: string): string {
195
- // Inline code (protect from further processing)
196
- const codes: string[] = [];
197
- let result = text.replace(/`([^`]+)`/g, (_m, code) => {
198
- const idx = codes.length;
199
- codes.push(`<code>${escapeHtml(code)}</code>`);
200
- return `\x00CODE${idx}\x00`;
201
- });
202
-
203
- // Bold (**text** or __text__)
204
- result = result.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
205
- result = result.replace(/__(.+?)__/g, '<strong>$1</strong>');
206
-
207
- // Italic (*text* or _text_)
208
- result = result.replace(/\*(.+?)\*/g, '<em>$1</em>');
209
- result = result.replace(/(?<!\w)_(.+?)_(?!\w)/g, '<em>$1</em>');
210
-
211
- // Links [text](url)
212
- result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
213
-
214
- // Restore inline code
215
- result = result.replace(/\x00CODE(\d+)\x00/g, (_m, idx) => codes[parseInt(idx)]);
216
-
217
- return result;
218
- }
219
-
220
146
  // ---------------------------------------------------------------------------
221
147
  // Internal helpers
222
148
  // ---------------------------------------------------------------------------
223
149
 
150
+ /** Tags the LLM will never target — skip annotation to save tokens. */
151
+ const SKIP_ANNOTATION_TAGS = new Set(['br', 'wbr', 'col', 'source']);
152
+
224
153
  /**
225
- * Assign sequential `data-node-id` to every element in the HTML.
154
+ * Assign sequential `data-nid` to every element in the HTML.
155
+ * Skips trivial elements (br, wbr, col, source) that the LLM never targets.
226
156
  */
227
157
  export function assignNodeIds(html: string): { html: string; nodeCount: number } {
228
158
  const $ = cheerio.load(html, { decodeEntities: false });
@@ -231,18 +161,100 @@ export function assignNodeIds(html: string): { html: string; nodeCount: number }
231
161
  $('*').each(function (this: cheerio.Element) {
232
162
  const el = $(this);
233
163
  if (this.type === 'tag' || this.type === 'script' || this.type === 'style') {
234
- el.attr('data-node-id', String(counter++));
164
+ const tag = (this as any).tagName?.toLowerCase() ?? (this as any).name?.toLowerCase();
165
+ if (SKIP_ANNOTATION_TAGS.has(tag)) return;
166
+ el.attr('data-nid', String(counter++));
235
167
  }
236
168
  });
237
169
  return { html: $.html(), nodeCount: counter };
238
170
  }
239
171
 
240
172
  /**
241
- * Remove all `data-node-id` attributes from the HTML.
173
+ * Remove all `data-nid` attributes from the HTML.
242
174
  */
243
175
  export function stripNodeIds(html: string): string {
244
176
  const $ = cheerio.load(html, { decodeEntities: false });
245
- $('[data-node-id]').removeAttr('data-node-id');
177
+ $('[data-nid]').removeAttr('data-nid');
178
+ return $.html();
179
+ }
180
+
181
+ /**
182
+ * Consolidated post-processing: strip node IDs, deduplicate scripts, and
183
+ * ensure script ordering — all in a single cheerio parse/serialize cycle.
184
+ */
185
+ function postProcessHtml(html: string): string {
186
+ const $ = cheerio.load(html, { decodeEntities: false });
187
+
188
+ // Strip data-nid attributes
189
+ $('[data-nid]').removeAttr('data-nid');
190
+
191
+ // Deduplicate inline scripts (ID-based pass)
192
+ const SYSTEM_IDS = new Set(['page-info', 'page-helpers', 'page-script', 'error', 'shell-v3', 'server-v3', 'storage-v3', 'script-v3', 'connector-v3', 'agent-v3']);
193
+ const idGroups = new Map<string, cheerio.Cheerio[]>();
194
+ $('script').each(function (_, rawEl) {
195
+ const el = $(rawEl);
196
+ if (el.attr('src')) return;
197
+ const id = el.attr('id');
198
+ if (!id || SYSTEM_IDS.has(id)) return;
199
+ if (!idGroups.has(id)) idGroups.set(id, []);
200
+ idGroups.get(id)!.push(el);
201
+ });
202
+ for (const [id, group] of idGroups) {
203
+ if (group.length < 2) continue;
204
+ for (let i = 0; i < group.length - 1; i++) {
205
+ console.log(`deduplicateInlineScripts: removing duplicate script id="${id}" (keeping last of ${group.length})`);
206
+ group[i].remove();
207
+ }
208
+ }
209
+
210
+ // Deduplicate inline scripts (declaration-overlap pass for id-less scripts)
211
+ const declPattern = /(?:^|;|\n)\s*(?:let|const|var|function|class)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g;
212
+ interface ScriptInfo { el: cheerio.Cheerio; declarations: Set<string>; }
213
+ const scripts: ScriptInfo[] = [];
214
+ $('script').each(function (_, rawEl) {
215
+ const el = $(rawEl);
216
+ if (el.attr('src')) return;
217
+ if (el.attr('id')) return;
218
+ if ((el.attr('type') ?? '').toLowerCase() === 'application/json') return;
219
+ const code = (el.html() ?? '').trim();
220
+ if (!code) return;
221
+ const declarations = new Set<string>();
222
+ let m: RegExpExecArray | null;
223
+ declPattern.lastIndex = 0;
224
+ while ((m = declPattern.exec(code)) !== null) declarations.add(m[1]);
225
+ scripts.push({ el, declarations });
226
+ });
227
+ const toRemove = new Set<number>();
228
+ for (let i = 0; i < scripts.length; i++) {
229
+ if (toRemove.has(i)) continue;
230
+ for (let j = i + 1; j < scripts.length; j++) {
231
+ if (toRemove.has(j)) continue;
232
+ const a = scripts[i].declarations, b = scripts[j].declarations;
233
+ if (a.size < 2 || b.size < 2) continue;
234
+ let overlap = 0;
235
+ for (const name of a) if (b.has(name)) overlap++;
236
+ if (overlap / Math.min(a.size, b.size) >= 0.6) {
237
+ console.log(`deduplicateInlineScripts: removing duplicate script (${overlap}/${Math.min(a.size, b.size)} declaration overlap)`);
238
+ toRemove.add(i);
239
+ break;
240
+ }
241
+ }
242
+ }
243
+ for (const idx of toRemove) scripts[idx].el.remove();
244
+
245
+ // Ensure page-helpers and page-script are last children of <body>
246
+ const body = $('body');
247
+ if (body.length > 0) {
248
+ const helpers = $('script#page-helpers');
249
+ const pageScript = $('script#page-script');
250
+ const helpersHtml = helpers.length > 0 ? $.html(helpers.first()) : '';
251
+ const pageScriptHtml = pageScript.length > 0 ? $.html(pageScript.first()) : '';
252
+ if (helpers.length > 0) helpers.remove();
253
+ if (pageScript.length > 0) pageScript.remove();
254
+ if (helpersHtml) body.append(helpersHtml);
255
+ if (pageScriptHtml) body.append(pageScriptHtml);
256
+ }
257
+
246
258
  return $.html();
247
259
  }
248
260
 
@@ -325,7 +337,7 @@ export function normalizedIndexOf(haystack: string, needle: string): { start: nu
325
337
  export function deduplicateInlineScripts(html: string): string {
326
338
  const $ = cheerio.load(html, { decodeEntities: false });
327
339
 
328
- const SYSTEM_IDS = new Set(['page-info', 'page-helpers', 'page-script', 'error']);
340
+ const SYSTEM_IDS = new Set(['page-info', 'page-helpers', 'page-script', 'error', 'shell-v3', 'server-v3', 'storage-v3', 'script-v3', 'connector-v3', 'agent-v3']);
329
341
 
330
342
  // ── Pass 1: ID-based dedup ──────────────────────────────────────────
331
343
  const idGroups = new Map<string, cheerio.Cheerio[]>();
@@ -428,8 +440,8 @@ export function ensureScriptsBeforeBodyClose(html: string): string {
428
440
  const helpers = $('script#page-helpers');
429
441
  const pageScript = $('script#page-script');
430
442
 
431
- const helpersHtml = helpers.length > 0 ? $.html(helpers) : '';
432
- const pageScriptHtml = pageScript.length > 0 ? $.html(pageScript) : '';
443
+ const helpersHtml = helpers.length > 0 ? $.html(helpers.first()) : '';
444
+ const pageScriptHtml = pageScript.length > 0 ? $.html(pageScript.first()) : '';
433
445
 
434
446
  // Remove from current position and re-append at end of <body>
435
447
  if (helpers.length > 0) helpers.remove();
@@ -448,15 +460,39 @@ function isElementLocked(el: cheerio.Cheerio, $: cheerio.Root): boolean {
448
460
  }
449
461
 
450
462
  /**
451
- * If the target element is a <script> or <style> and the html is wrapped in a
452
- * redundant matching tag, strip the outer tag to avoid nesting (e.g.
453
- * `<script>` inside `<script>`). Returns the inner content when unwrapped.
463
+ * If the provided html is wrapped in a redundant tag that matches the target
464
+ * element, strip the outer tag to avoid nesting (e.g. `<div id="x">` inside
465
+ * the existing `<div id="x">`). For script/style elements the tag name alone
466
+ * is sufficient; for other elements we require an id or class match to avoid
467
+ * false positives.
454
468
  */
455
- function unwrapRedundantTag(tagName: string | undefined, html: string): string {
456
- if (tagName !== 'script' && tagName !== 'style') return html;
457
- const re = new RegExp(`^\\s*<${tagName}[^>]*>([\\s\\S]*)</${tagName}>\\s*$`, 'i');
469
+ function unwrapRedundantTag(tagName: string | undefined, html: string, targetId?: string, targetClass?: string): string {
470
+ if (!tagName) return html;
471
+ const re = new RegExp(`^\\s*<${tagName}\\b([^>]*)>([\\s\\S]*)</${tagName}>\\s*$`, 'i');
458
472
  const match = html.match(re);
459
- return match ? match[1] : html;
473
+ if (!match) return html;
474
+
475
+ // For script/style, always unwrap (original behaviour)
476
+ if (tagName === 'script' || tagName === 'style') {
477
+ return match[2];
478
+ }
479
+
480
+ // For other elements, require an id or class match to avoid false positives
481
+ const outerAttrs = match[1];
482
+ if (targetId) {
483
+ const idMatch = outerAttrs.match(/\bid=["']([^"']*)["']/);
484
+ if (idMatch && idMatch[1] === targetId) return match[2];
485
+ }
486
+ if (targetClass) {
487
+ const classMatch = outerAttrs.match(/\bclass=["']([^"']*)["']/);
488
+ if (classMatch) {
489
+ const targetClasses = targetClass.split(/\s+/);
490
+ const outerClasses = classMatch[1].split(/\s+/);
491
+ if (targetClasses.some(c => outerClasses.includes(c))) return match[2];
492
+ }
493
+ }
494
+
495
+ return html;
460
496
  }
461
497
 
462
498
  /**
@@ -471,31 +507,48 @@ function stripNestedBlockTags(tagName: string | undefined, text: string): string
471
507
  }
472
508
 
473
509
  /**
474
- * Apply a list of CRUD operations to annotated HTML (elements must have `data-node-id`).
510
+ * Apply a list of CRUD operations to annotated HTML (elements must have `data-nid`).
511
+ * Returns just the resulting HTML string. For diagnostic info about skipped ops,
512
+ * use `applyChangeListWithReport` instead.
475
513
  */
476
514
  export function applyChangeList(html: string, changes: ChangeList): string {
515
+ return applyChangeListWithReport(html, changes).html;
516
+ }
517
+
518
+ /**
519
+ * Apply a list of CRUD operations and return the resulting HTML plus a report
520
+ * of any ops that were silently skipped (missing target node, locked element,
521
+ * search text not found, etc.). Callers can surface the skip report to the
522
+ * user so that partial/broken builds do not fail silently.
523
+ */
524
+ export function applyChangeListWithReport(html: string, changes: ChangeList): ApplyChangeListReport {
477
525
  const $ = cheerio.load(html, { decodeEntities: false });
526
+ const skipReasons: string[] = [];
527
+ const recordSkip = (reason: string) => {
528
+ console.warn(`applyChangeList: ${reason}`);
529
+ skipReasons.push(reason);
530
+ };
478
531
 
479
532
  for (const change of changes) {
480
533
  switch (change.op) {
481
534
  case 'update': {
482
- const el = $(`[data-node-id="${change.nodeId}"]`);
535
+ const el = $(`[data-nid="${change.nodeId}"]`);
483
536
  if (el.length === 0) {
484
- console.warn(`applyChangeList: skipping update — node ${change.nodeId} not found (already removed?)`);
537
+ recordSkip(`skipping update — node ${change.nodeId} not found (already removed?)`);
485
538
  break;
486
539
  }
487
540
  const tag = el.prop('tagName')?.toLowerCase();
488
- el.html(unwrapRedundantTag(tag, change.html));
541
+ el.html(unwrapRedundantTag(tag, change.html, el.attr('id'), el.attr('class')));
489
542
  break;
490
543
  }
491
544
  case 'replace': {
492
- const el = $(`[data-node-id="${change.nodeId}"]`);
545
+ const el = $(`[data-nid="${change.nodeId}"]`);
493
546
  if (el.length === 0) {
494
- console.warn(`applyChangeList: skipping replace — node ${change.nodeId} not found (already removed?)`);
547
+ recordSkip(`skipping replace — node ${change.nodeId} not found (already removed?)`);
495
548
  break;
496
549
  }
497
550
  if (isElementLocked(el, $)) {
498
- console.warn(`applyChangeList: skipping replace — node ${change.nodeId} is data-locked`);
551
+ recordSkip(`skipping replace — node ${change.nodeId} is data-locked`);
499
552
  break;
500
553
  }
501
554
  // If the target is a <script> or <style> and the html doesn't
@@ -511,22 +564,22 @@ export function applyChangeList(html: string, changes: ChangeList): string {
511
564
  break;
512
565
  }
513
566
  case 'delete': {
514
- const el = $(`[data-node-id="${change.nodeId}"]`);
567
+ const el = $(`[data-nid="${change.nodeId}"]`);
515
568
  if (el.length === 0) {
516
- console.warn(`applyChangeList: skipping delete — node ${change.nodeId} not found (already removed?)`);
569
+ recordSkip(`skipping delete — node ${change.nodeId} not found (already removed?)`);
517
570
  break;
518
571
  }
519
572
  if (isElementLocked(el, $)) {
520
- console.warn(`applyChangeList: skipping delete — node ${change.nodeId} is data-locked`);
573
+ recordSkip(`skipping delete — node ${change.nodeId} is data-locked`);
521
574
  break;
522
575
  }
523
576
  el.remove();
524
577
  break;
525
578
  }
526
579
  case 'insert': {
527
- const parent = $(`[data-node-id="${change.parentId}"]`);
580
+ const parent = $(`[data-nid="${change.parentId}"]`);
528
581
  if (parent.length === 0) {
529
- console.warn(`applyChangeList: skipping insert — parent node ${change.parentId} not found`);
582
+ recordSkip(`skipping insert — parent node ${change.parentId} not found`);
530
583
  break;
531
584
  }
532
585
  // Unwrap redundant tags when inserting into script/style
@@ -538,27 +591,27 @@ export function applyChangeList(html: string, changes: ChangeList): string {
538
591
  case 'before': parent.before(insertHtml); break;
539
592
  case 'after': parent.after(insertHtml); break;
540
593
  default:
541
- console.warn(`applyChangeList: skipping insert — unknown position "${(change as any).position}"`);
594
+ recordSkip(`skipping insert — unknown position "${(change as any).position}"`);
542
595
  }
543
596
  break;
544
597
  }
545
598
  case 'style-element': {
546
- const el = $(`[data-node-id="${change.nodeId}"]`);
599
+ const el = $(`[data-nid="${change.nodeId}"]`);
547
600
  if (el.length === 0) {
548
- console.warn(`applyChangeList: skipping style-element — node ${change.nodeId} not found (already removed?)`);
601
+ recordSkip(`skipping style-element — node ${change.nodeId} not found (already removed?)`);
549
602
  break;
550
603
  }
551
604
  if (isElementLocked(el, $)) {
552
- console.warn(`applyChangeList: skipping style-element — node ${change.nodeId} is data-locked`);
605
+ recordSkip(`skipping style-element — node ${change.nodeId} is data-locked`);
553
606
  break;
554
607
  }
555
608
  el.attr('style', change.style);
556
609
  break;
557
610
  }
558
611
  case 'search-replace': {
559
- const el = $(`[data-node-id="${change.nodeId}"]`);
612
+ const el = $(`[data-nid="${change.nodeId}"]`);
560
613
  if (el.length === 0) {
561
- console.warn(`applyChangeList: skipping search-replace — node ${change.nodeId} not found (already removed?)`);
614
+ recordSkip(`skipping search-replace — node ${change.nodeId} not found (already removed?)`);
562
615
  break;
563
616
  }
564
617
  const srTag = el.prop('tagName')?.toLowerCase();
@@ -572,15 +625,15 @@ export function applyChangeList(html: string, changes: ChangeList): string {
572
625
  if (norm) {
573
626
  el.html(content.slice(0, norm.start) + replaceText + content.slice(norm.end));
574
627
  } else {
575
- console.warn(`applyChangeList: skipping search-replace — search text not found in node ${change.nodeId}`);
628
+ recordSkip(`skipping search-replace — search text not found in node ${change.nodeId}`);
576
629
  }
577
630
  }
578
631
  break;
579
632
  }
580
633
  case 'search-insert': {
581
- const el = $(`[data-node-id="${change.nodeId}"]`);
634
+ const el = $(`[data-nid="${change.nodeId}"]`);
582
635
  if (el.length === 0) {
583
- console.warn(`applyChangeList: skipping search-insert — node ${change.nodeId} not found (already removed?)`);
636
+ recordSkip(`skipping search-insert — node ${change.nodeId} not found (already removed?)`);
584
637
  break;
585
638
  }
586
639
  const siTag = el.prop('tagName')?.toLowerCase();
@@ -595,7 +648,7 @@ export function applyChangeList(html: string, changes: ChangeList): string {
595
648
  if (norm) {
596
649
  el.html(content.slice(0, norm.end) + insertContent + content.slice(norm.end));
597
650
  } else {
598
- console.warn(`applyChangeList: skipping search-insert — after text not found in node ${change.nodeId}`);
651
+ recordSkip(`skipping search-insert — after text not found in node ${change.nodeId}`);
599
652
  }
600
653
  }
601
654
  break;
@@ -605,36 +658,73 @@ export function applyChangeList(html: string, changes: ChangeList): string {
605
658
  }
606
659
  }
607
660
 
608
- return $.html();
661
+ return { html: $.html(), skippedOps: skipReasons.length, skipReasons };
609
662
  }
610
663
 
664
+ /** Known op types and their required fields (beyond `op`). */
665
+ const CHANGE_OP_REQUIRED_FIELDS: Record<string, string[]> = {
666
+ 'update': ['nodeId', 'html'],
667
+ 'replace': ['nodeId', 'html'],
668
+ 'delete': ['nodeId'],
669
+ 'insert': ['parentId', 'position', 'html'],
670
+ 'style-element': ['nodeId', 'style'],
671
+ 'search-replace': ['nodeId', 'search', 'replace'],
672
+ 'search-insert': ['nodeId', 'after', 'content'],
673
+ };
674
+
675
+ const VALID_INSERT_POSITIONS = new Set(['prepend', 'append', 'before', 'after']);
676
+
611
677
  /**
612
- * Append a user message and a styled error message to #chatMessages.
678
+ * Validate and filter a raw parsed array into a well-formed ChangeList.
679
+ * Invalid ops are logged as warnings and dropped rather than crashing.
613
680
  */
614
- function appendChatError(html: string, userMessage: string, errorDetails: string, productName: string): string {
615
- const $ = cheerio.load(html, { decodeEntities: false });
616
- const chatMessages = $('#chatMessages');
617
- if (chatMessages.length > 0) {
618
- chatMessages.append(
619
- `<div class="chat-message"><p><strong>User:</strong> ${escapeHtml(userMessage)}</p></div>`
620
- );
621
- chatMessages.append(
622
- `<div class="chat-message chat-message-error"><p><strong>${escapeHtml(productName)}:</strong> Something went wrong \u2014 please try again.</p>`
623
- + `<p class="chat-error-details">${escapeHtml(errorDetails)}</p></div>`
624
- );
681
+ export function validateChangeOps(raw: unknown[]): ChangeList {
682
+ const valid: ChangeList = [];
683
+ for (let i = 0; i < raw.length; i++) {
684
+ const item = raw[i] as Record<string, unknown>;
685
+ if (!item || typeof item !== 'object') {
686
+ console.warn(`validateChangeOps: skipping op[${i}] — not an object`);
687
+ continue;
688
+ }
689
+ const op = item.op;
690
+ if (typeof op !== 'string') {
691
+ console.warn(`validateChangeOps: skipping op[${i}] — missing or non-string 'op' field`);
692
+ continue;
693
+ }
694
+ const requiredFields = CHANGE_OP_REQUIRED_FIELDS[op];
695
+ if (!requiredFields) {
696
+ console.warn(`validateChangeOps: skipping op[${i}] — unknown op type '${op}'`);
697
+ continue;
698
+ }
699
+ let missingField = false;
700
+ for (const field of requiredFields) {
701
+ if (typeof item[field] !== 'string') {
702
+ console.warn(`validateChangeOps: skipping op[${i}] (${op}) — missing or non-string '${field}' field`);
703
+ missingField = true;
704
+ break;
705
+ }
706
+ }
707
+ if (missingField) continue;
708
+ // Extra validation for insert position
709
+ if (op === 'insert' && !VALID_INSERT_POSITIONS.has(item.position as string)) {
710
+ console.warn(`validateChangeOps: skipping op[${i}] (insert) — invalid position '${item.position}'`);
711
+ continue;
712
+ }
713
+ valid.push(item as unknown as ChangeOp);
625
714
  }
626
- return $.html();
715
+ return valid;
627
716
  }
628
717
 
629
718
  /**
630
719
  * Parse a JSON change list from the model's raw response text.
631
720
  * Handles responses that may include markdown fences or extra text around the JSON.
721
+ * Invalid ops are logged as warnings and filtered out.
632
722
  */
633
723
  export function parseChangeList(response: string): ChangeList {
634
724
  // Try direct parse first
635
725
  try {
636
726
  const parsed = JSON.parse(response);
637
- if (Array.isArray(parsed)) return parsed as ChangeList;
727
+ if (Array.isArray(parsed)) return validateChangeOps(parsed);
638
728
  } catch {
639
729
  // fall through to extraction
640
730
  }
@@ -644,7 +734,7 @@ export function parseChangeList(response: string): ChangeList {
644
734
  if (match) {
645
735
  try {
646
736
  const parsed = JSON.parse(match[0]);
647
- if (Array.isArray(parsed)) return parsed as ChangeList;
737
+ if (Array.isArray(parsed)) return validateChangeOps(parsed);
648
738
  } catch {
649
739
  // fall through
650
740
  }
@@ -657,43 +747,55 @@ export function parseChangeList(response: string): ChangeList {
657
747
  // Prompt constants
658
748
  // ---------------------------------------------------------------------------
659
749
 
660
- export function getMessageFormat(productName: string): string {
661
- return `<MESSAGE_FORMAT>
662
- <div class="chat-message"><p><strong>{${productName}: | User:}</strong> {message contents}</p></div>
663
- `;
664
- }
665
-
666
750
  export function getTransformInstr(productName: string): string {
667
751
  return `Apply the users <USER_MESSAGE> to the .viewerPanel of the <CURRENT_PAGE> by generating a list of changes in JSON format.
668
- Never remove any element that has a data-locked attribute. You may modfiy the inner text of a data-locked element or any of its unlocked child elements.
752
+ Your response is a JSON object with two fields: "message" and "changes". The "message" field is REQUIRED on every response and must be a brief (1 sentence) message written directly to the user explaining what you did — it is shown in the chat feed as your reply. Do not describe the JSON; speak to the user (e.g. "Added a dark-mode toggle to the header."). The "changes" field is the array of change operations.
753
+ Never remove any element that has a data-locked attribute. You may modify the inner text of a data-locked element or any of its unlocked child elements.
669
754
 
670
- If the <USER_MESSAGE> involves clearning the chat history, remove all .chat-message elements inside the #chatMessages container except for the first ${productName}: message. You may modify that message contents if requested.
671
- If there's no <USER_MESSAGE> add a ${productName}: message to the chat with aasking the user what they would like to do.
672
- If there is a <USER_MESSAGE> but the intent is unclear, add a User: message with the <USER_MESSAGE> to the chat and add a ${productName}: message asking the user for clarification on their intent.
673
- If there is a <USER_MESSAGE> with clear intent, add a User: message with the <USER_MESSAGE> to the chat and add a ${productName}: message explaining your change or answering their question.
674
- If a <USER_MESSAGE> is overly long, summarize the User: message.
755
+ Your page runs inside an iframe. The chat panel, toolbar, and all shell chrome are in the parent frame they are NOT part of your page HTML. Do not generate chat messages, chat-message divs, or any shell markup. Focus only on the page content inside .viewerPanel.
675
756
 
676
- When updating the .viewerPanel you may alse add/remove/update style blocks to the header unless they're data-locked. Use inline styles if you need to modify the .viewerPanel itself.
757
+ When updating the .viewerPanel you may also add/remove/update style blocks in the head unless they're data-locked. Use inline styles if you need to modify the .viewerPanel itself.
677
758
  You may add/remove new script blocks to the body but all script & style blocks should have a unique id.
678
759
  You may modify the contents of a data-locked script block but may not remove it.
679
760
 
680
761
  Every <CURRENT_PAGE> has hidden data-locked "thoughts" and "instructions" divs.
681
- The instruction div, if pressent, contains custom <INSTRUCTIONS> for that page that should be followed in addition to these general instructions. You may modify the instructions div if needed (e.g. to add new instructions or update existing ones), but do not remove it. Add it if it's missing though.
762
+ The instructions div, if present, contains custom <INSTRUCTIONS> for that page that should be followed in addition to these general instructions. You may modify the instructions div if needed (e.g. to add new instructions or update existing ones), but do not remove it. Add it if it's missing.
682
763
  The thoughts block is for your internal use only — you can write anything in there to help you reason through the user's request, but it is not visible to the user. You can also use it to keep track of any relevant state or information that may be useful across multiple turns.
683
764
  If the <USER_MESSAGE> indicates that a change didn't work, use your thoughts to diagnose the problem before fixing the issue.
765
+ Your first operation should always be an update to your thoughts block, where you can reason through the user's request and plan your changes before applying them to the page.
684
766
 
685
- The <MESSAGE_FORMAT> section provides the HTML structure for chat messages in the chat panel. Use this format when generating new messages to ensure they display correctly.
686
- The <SERVER_APIS> section provides a list of available server APIs and helper functions you can call from injected scripts. You should use the synthos.* helper functions for any server API calls instead of raw fetch().
687
- The <SERVER_SCRIPTS> section provides a list of available scripts you can call from injected scripts. These are user-created scripts stored on the server that can be executed by calling synthos.scripts.run(id, variables).
688
- The <THEME> section provides details on the current theme's color scheme and shared shell classes to help you generate theme-aware pages that fit seamlessly into the user experience.
689
- The viewer panel can be resized by the user, so for animations, games, and presentations should always add the ",full-viewer" class to the viewer-panel element and ensure content stays centered and uses the maximum available space (use 100% width/height, flexbox centering, or viewport-relative sizing as appropriate).
690
- window.themeInfo is available and has a structure like this: { mode: 'light' | 'dark', colors: { primary: '#hex', secondary: '#hex', background: '#hex', text: '#hex', ... } }. Use these colors instead of hardcoded values to ensure your page works with the user's selected theme and any custom themes they may have. You can also use the shared shell classes defined in the theme info for consistent styling of common elements like the chat panel and header.
691
-
692
- Do not add duplicate script blocks with the same logic! Consolidate inline scrips if needed and double check that variables and functions are defined in the correct order.
767
+ Patchable Data Convention When a page stores editable metadata that should survive round-trips and be independently updatable, use data-* attributes on identifiable elements (elements with an id or a unique, stable CSS selector). This allows the patch API to update individual values without a full page re-render. Do NOT store patchable data as text content inside elements.
768
+ Examples: data-notes on .slide elements (speaker notes), data-duration on .slide elements (slide timing), data-label on interactive elements (accessible names).
693
769
 
694
- Each element in the CURRENT_PAGE has a data-node-id attribute. Don't use the id attribute for targeting nodes (reserve it for scripts and styles) — use data-node-id.
770
+ The <SERVER_APIS> section provides a list of available server APIs and helper functions you can call from injected scripts. You should use the synthos.* helper functions for any server API calls instead of raw fetch().
771
+ The <SERVER_SCRIPTS> section provides a list of available scripts you can call from injected scripts. These are user-created scripts stored on the server that can be executed by calling synthos.script.run(id, variables).
772
+ The <THEME> section provides details on the current theme's color scheme to help you generate theme-aware pages.
773
+ The viewer panel can be resized by the user, so for animations, games, and presentations should always add the "full-viewer" class to the viewer-panel element and ensure content stays centered and uses the maximum available space (use 100% width/height, flexbox centering, or viewport-relative sizing as appropriate).
774
+ window.themeInfo is available and has a structure like this: { mode: 'light' | 'dark', colors: { primary: '#hex', secondary: '#hex', background: '#hex', text: '#hex', ... } }. Use these colors instead of hardcoded values to ensure your page works with the user's selected theme and any custom themes they may have.
775
+
776
+ Visual Design Guide — CRITICAL: Follow these rules exactly so pages look polished. Pages that ignore spacing rules look broken.
777
+ - SPACING TOKENS: --spacingS2: 4px, --spacingS1: 8px, --spacingM: 16px, --spacingL1: 20px, --spacingL2: 32px.
778
+ - PAGE HEADER: padding MUST be at least var(--spacingM) vertically and var(--spacingL1) horizontally (e.g. padding: var(--spacingM) var(--spacingL1)). Never use --spacingS1 for page header padding — it looks cramped.
779
+ - CONTENT AREAS: Main content sections (tab panels, card bodies, scroll areas) MUST have at least padding: var(--spacingL1) (20px). Use var(--spacingL2) (32px) for primary page content padding when there is room.
780
+ - SECTION GAPS: Leave at least var(--spacingL1) (20px) gap between major sections (header-to-tabs, tabs-to-content, between card groups). Use var(--spacingM) (16px) minimum between related items within a section.
781
+ - CARD/PANEL PADDING: Cards and panels MUST have at least padding: var(--spacingM) (16px) inside. Use var(--spacingL1) for larger content cards.
782
+ - TAB BARS (flm-pivot): Tab container should have padding: var(--spacingM) var(--spacingL1) — never --spacingS1 which makes tabs look squished.
783
+ - TYPOGRAPHY HIERARCHY: Use FluentLM text classes — flm-text--xxLarge or flm-text--xLarge + flm-text--semibold for page titles, flm-text--large for section headings, flm-text (default 14px) for body, flm-text--small + flm-text--secondary for captions. Always apply flm-text--block on block-level text.
784
+ - SURFACES & CARDS: Use theme color variables only: background var(--white) with border 1px solid var(--neutralLight), or box-shadow var(--elevation4) for elevated cards. Section backgrounds: var(--neutralLighterAlt). NEVER use custom variables like --bg-secondary, --border-color, --text-primary — these do not exist. Always use the real theme variables: --white, --black, --neutralLight, --neutralLighter, --neutralLighterAlt, --neutralPrimary, --neutralSecondary, --themePrimary, --themeLight, --themeLighter, --themeLighterAlt, --themeDark, --themeDarker.
785
+ - ELEVATION: var(--elevation4) for cards, var(--elevation8) for dropdowns, var(--elevation16) for modals.
786
+ - BORDER RADIUS: 4px on containers, 2px on badges/chips, 4px on buttons.
787
+ - DATA TABLES: Headers: background var(--neutralLighterAlt), font-weight 600, padding var(--spacingS1) var(--spacingM). Rows: border-bottom 1px solid var(--neutralLight), cell padding var(--spacingS1) var(--spacingM).
788
+ - LAYOUT: Use flm-stack / flm-stack--horizontal with gap via spacing tokens. CSS grid with gap: var(--spacingM). Use max-width on wide layouts.
789
+ - VISUAL WEIGHT: var(--themePrimary) for interactive elements only, not large backgrounds. var(--neutralPrimary) for body text, var(--neutralSecondary) for secondary text.
790
+ - EMPTY STATES: Center the message vertically and horizontally with an icon. Add generous padding (at least var(--spacingL2)) around empty state messages.
791
+
792
+ Do not add duplicate script blocks with the same logic! Consolidate inline scripts if needed and double check that variables and functions are defined in the correct order.
793
+ Prefer a single inline <script> block for all of your page's JavaScript. If you must split logic across two blocks, that's acceptable, but one is strongly preferred.
794
+ Inline <script> blocks on your page are automatically wrapped in an IIFE (except scripts with src, data-locked scripts, or scripts already wrapped in (function(){ ... })()). This means top-level \`function\`, \`const\`, \`let\`, and \`var\` declarations inside a script are LOCAL to that script and cannot be called from another script. Put functions that need to call each other in the same <script> block, or explicitly attach them to \`window\` (e.g. \`window.myFunc = function(){...}\`) if they must be shared across blocks.
795
+ Place all <script> blocks at the end of <body>, never in <head> — scripts in <head> run before the DOM is parsed and will crash if they reference body elements.
796
+
797
+ Each element in the CURRENT_PAGE has a data-nid attribute. Don't use the id attribute for targeting nodes (reserve it for scripts and styles) — use data-nid.
695
798
  If you're trying to assign an id to script or style block, use "replace" not "update".
696
- Your first operation should always be an update to your thoughts block, where you can reason through the user's request and plan your changes before applying them to the page.
697
799
 
698
800
  CRITICAL — FluentLM components: If a <FLUENTLM_COMPONENTS> section is present, you MUST use those components for all standard UI elements (buttons, inputs, selects, dialogs, tabs, cards, toggles, etc.). Never create custom CSS classes for UI controls that have a FluentLM equivalent. Refer to <FLUENTLM_COMPONENTS> for the exact class names and markup patterns.`;
699
801
  }
@@ -702,19 +804,19 @@ export const AGENT_API_REFERENCE =
702
804
  `## Agent API
703
805
 
704
806
  Check availability first (required):
705
- const agents = await synthos.agents.list({ enabled: true });
807
+ const agents = await synthos.agent.list({ enabled: true });
706
808
 
707
809
  Send a message (returns full response):
708
- const result = await synthos.agents.send(agentId, message);
810
+ const result = await synthos.agent.send(agentId, message);
709
811
  // result: { kind: 'message', text: 'response text', raw: {...} }
710
812
 
711
813
  Send with file/image attachments:
712
- const result = await synthos.agents.send(agentId, message, [
814
+ const result = await synthos.agent.send(agentId, message, [
713
815
  { fileName: 'photo.jpg', mimeType: 'image/jpeg', content: '<base64-string>' }
714
816
  ]);
715
817
 
716
818
  Stream a response (token-by-token deltas):
717
- const handle = synthos.agents.sendStream(agentId, message, function(event) {
819
+ const handle = synthos.agent.sendStream(agentId, message, function(event) {
718
820
  switch (event.kind) {
719
821
  case 'text': // event.data = text delta string — append to output
720
822
  case 'status': // event.data = status info object
@@ -725,11 +827,11 @@ Stream a response (token-by-token deltas):
725
827
  handle.close(); // call to abort the stream early
726
828
 
727
829
  Stream with attachments:
728
- synthos.agents.sendStream(agentId, message, onEvent, [
830
+ synthos.agent.sendStream(agentId, message, onEvent, [
729
831
  { fileName: 'doc.pdf', mimeType: 'application/pdf', content: '<base64>' }
730
832
  ]);
731
833
 
732
- IMPORTANT: Always check synthos.agents.list({ enabled: true }) before calling an agent.
834
+ IMPORTANT: Always check synthos.agent.list({ enabled: true }) before calling an agent.
733
835
  If no agents are configured, show the user a link to Settings > Agents (/settings?tab=agents).`;
734
836
 
735
837
  // ---------------------------------------------------------------------------
@@ -767,12 +869,19 @@ request: { prompt: string, shape: 'square' | 'portrait' | 'landscape', style: 'v
767
869
  response: { url: string }
768
870
 
769
871
  POST /api/generate/completion
770
- description: Generates a text completion based on a prompt
771
- request: { prompt: string, temperature?: number }
772
- response: { answer: string, explanation: string }
872
+ description: Generates a completion based on a prompt. When \`schema\` is provided, the model is constrained to emit JSON conforming to it (structured output) and the parsed object is returned directly. Without \`schema\`, returns plain text.
873
+ request: { prompt: string, temperature?: number, schema?: object (JSON schema) }
874
+ response (no schema): { answer: string }
875
+ response (with schema): the parsed JSON object matching \`schema\` (e.g. \`{ items: [...] }\`)
876
+
877
+ Schema notes:
878
+ - Use this when you need structured data — never tell the model "return JSON with these fields" via the prompt and parse \`answer\`.
879
+ - Top-level schema must be \`type: 'object'\`. To return a list, wrap it: \`{ type: 'object', additionalProperties: false, required: ['items'], properties: { items: { type: 'array', items: {...} } } }\`.
880
+ - Every nested \`type: 'object'\` MUST set \`additionalProperties: false\` — Anthropic structured-output rejects schemas that omit it.
881
+ - Mark every required field in \`required: [...]\` so the model is forced to emit them.
773
882
 
774
- synthos.generate.image({ prompt, shape, style }) — POST /api/generate/image
775
- synthos.generate.completion({ prompt, temperature? }) — POST /api/generate/completion`],
883
+ synthos.generate.image({ prompt, shape, style }) — POST /api/generate/image
884
+ synthos.generate.completion({ prompt, temperature?, schema? }) — POST /api/generate/completion`],
776
885
 
777
886
  ['pages', `GET /api/pages
778
887
  description: Retrieve a list of all pages with metadata
@@ -796,18 +905,18 @@ description: Ask a question about a page with full HTML context
796
905
  request: { question: string }
797
906
  response: { answer: string }
798
907
 
799
- synthos.pages.list() — GET /api/pages
800
- synthos.pages.get(name) — GET /api/pages/:name
801
- synthos.pages.update(name, metadata) — POST /api/pages/:name
802
- synthos.pages.remove(name) — DELETE /api/pages/:name
803
- synthos.pages.ask(name, question) — POST /api/pages/:name/ask`],
908
+ synthos.page.list() — GET /api/pages
909
+ synthos.page.get(name) — GET /api/pages/:name
910
+ synthos.page.update(name, metadata) — POST /api/pages/:name
911
+ synthos.page.remove(name) — DELETE /api/pages/:name
912
+ synthos.page.ask(name, question) — POST /api/pages/:name/ask`],
804
913
 
805
914
  ['scripts', `POST /api/scripts/:id
806
915
  description: Execute a script with the passed in variables
807
916
  request: { [key: string]: string }
808
917
  response: string
809
918
 
810
- synthos.scripts.run(id, variables) — POST /api/scripts/:id`],
919
+ synthos.script.run(id, variables) — POST /api/scripts/:id`],
811
920
 
812
921
  ['search', `POST /api/search/web
813
922
  description: Search the web using Brave Search (must be enabled in Settings > Connectors)
@@ -830,11 +939,11 @@ description: Send a message and receive a streaming SSE response (text/event-str
830
939
  request: { message: string, attachments?: [{ fileName: string, mimeType: string, content: string }] }
831
940
  response: SSE stream
832
941
 
833
- synthos.agents.list(opts?) — GET /api/agents (returns configured agents; opts: { enabled?, provider? }; returns [{ id, name, description, url, enabled, provider, capabilities }])
834
- synthos.agents.send(agentId, message, attachments?) — POST /api/agents/:id/send (sends a text message to any agent, returns normalized { kind, text, raw }; attachments: [{ fileName, mimeType, content }])
835
- synthos.agents.sendStream(agentId, message, onEvent, attachments?) — POST /api/agents/:id/stream (SSE streaming; onEvent receives { kind, data }; returns { close() } handle; attachments: [{ fileName, mimeType, content }])
836
- synthos.agents.isEnabled(agentId) — checks if an agent is enabled (returns Promise<boolean>)
837
- synthos.agents.getCapabilities(agentId) — returns agent capabilities object (streaming, skills, etc.)`],
942
+ synthos.agent.list(opts?) — GET /api/agents (returns configured agents; opts: { enabled?, provider? }; returns [{ id, name, description, url, enabled, provider, capabilities }])
943
+ synthos.agent.send(agentId, message, attachments?) — POST /api/agents/:id/send (sends a text message to any agent, returns normalized { kind, text, raw }; attachments: [{ fileName, mimeType, content }])
944
+ synthos.agent.sendStream(agentId, message, onEvent, attachments?) — POST /api/agents/:id/stream (SSE streaming; onEvent receives { kind, data }; returns { close() } handle; attachments: [{ fileName, mimeType, content }])
945
+ synthos.agent.isEnabled(agentId) — checks if an agent is enabled (returns Promise<boolean>)
946
+ synthos.agent.getCapabilities(agentId) — returns agent capabilities object (streaming, skills, etc.)`],
838
947
 
839
948
  ['connectors', `GET /api/connectors
840
949
  description: List available connectors (REST API proxies). Supports ?category=X and ?id=X filters.
@@ -849,8 +958,8 @@ description: Proxy a request through a configured connector. The connector attac
849
958
  request: { connector: string, method: string, path: string, headers?: object, body?: any, query?: object }
850
959
  response: Upstream API response (JSON or text)
851
960
 
852
- synthos.connectors.call(connector, method, path, opts?) — POST /api/connectors (proxy call; opts: { headers?, body?, query? })
853
- synthos.connectors.list(opts?) — GET /api/connectors (opts: { category?, id? })`],
961
+ synthos.connector.call(connector, method, path, opts?) — POST /api/connectors (proxy call; opts: { headers?, body?, query? })
962
+ synthos.connector.list(opts?) — GET /api/connectors (opts: { category?, id? })`],
854
963
 
855
964
  ['files', `GET /api/files/:page
856
965
  description: List files stored for a page (with sizes)
@@ -973,9 +1082,10 @@ request: { prompt: string, shape: 'square' | 'portrait' | 'landscape', style: 'v
973
1082
  response: { url: string }
974
1083
 
975
1084
  POST /api/generate/completion
976
- description: Generates a text completion based on a prompt
977
- request: { prompt: string, temperature?: number }
978
- response: { answer: string, explanation: string }
1085
+ description: Generates a completion based on a prompt. When \`schema\` is provided, the model is constrained to emit JSON conforming to it and the parsed object is returned directly.
1086
+ request: { prompt: string, temperature?: number, schema?: object (JSON schema) }
1087
+ response (no schema): { answer: string }
1088
+ response (with schema): the parsed JSON object matching \`schema\`. Top-level must be \`type: 'object'\`; every nested object MUST set \`additionalProperties: false\`.
979
1089
 
980
1090
  GET /api/pages
981
1091
  description: Retrieve a list of all pages with metadata
@@ -1041,20 +1151,20 @@ PAGE HELPERS (available globally as window.synthos):
1041
1151
  synthos.data.get(table, id) — GET /api/data/:page/:table/:id (auto-scoped to current page)
1042
1152
  synthos.data.save(table, row) — POST /api/data/:page/:table (auto-scoped to current page)
1043
1153
  synthos.data.remove(table, id) — DELETE /api/data/:page/:table/:id (auto-scoped to current page)
1044
- synthos.generate.image({ prompt, shape, style }) — POST /api/generate/image
1045
- synthos.generate.completion({ prompt, temperature? }) — POST /api/generate/completion
1046
- synthos.scripts.run(id, variables) — POST /api/scripts/:id
1047
- synthos.pages.list() — GET /api/pages
1048
- synthos.pages.get(name) — GET /api/pages/:name
1049
- synthos.pages.update(name, metadata) — POST /api/pages/:name
1050
- synthos.pages.remove(name) — DELETE /api/pages/:name
1051
- synthos.pages.ask(name, question) — POST /api/pages/:name/ask
1154
+ synthos.generate.image({ prompt, shape, style }) — POST /api/generate/image
1155
+ synthos.generate.completion({ prompt, temperature?, schema? }) — POST /api/generate/completion (schema: optional JSON schema for structured output; returns parsed object)
1156
+ synthos.script.run(id, variables) — POST /api/scripts/:id
1157
+ synthos.page.list() — GET /api/pages
1158
+ synthos.page.get(name) — GET /api/pages/:name
1159
+ synthos.page.update(name, metadata) — POST /api/pages/:name
1160
+ synthos.page.remove(name) — DELETE /api/pages/:name
1161
+ synthos.page.ask(name, question) — POST /api/pages/:name/ask
1052
1162
  synthos.search.web(query, opts?) — POST /api/search/web (opts: { count?, country?, freshness? })
1053
- synthos.connectors.call(connector, method, path, opts?) — POST /api/connectors (proxy call; opts: { headers?, body?, query? })
1054
- synthos.connectors.list(opts?) — GET /api/connectors (opts: { category?, id? })
1055
- synthos.agents.list(opts?) — GET /api/agents (returns configured agents; opts: { enabled?, provider? }; returns [{ id, name, description, url, enabled, provider, capabilities }])
1056
- synthos.agents.send(agentId, message, attachments?) — POST /api/agents/:id/send (sends a text message to any agent, returns normalized { kind, text, raw }; attachments: [{ fileName, mimeType, content }])
1057
- synthos.agents.sendStream(agentId, message, onEvent, attachments?) — POST /api/agents/:id/stream (SSE streaming; onEvent receives { kind, data }; returns { close() } handle; attachments: [{ fileName, mimeType, content }])
1058
- synthos.agents.isEnabled(agentId) — checks if an agent is enabled (returns Promise<boolean>)
1059
- synthos.agents.getCapabilities(agentId) — returns agent capabilities object (streaming, skills, etc.)
1163
+ synthos.connector.call(connector, method, path, opts?) — POST /api/connectors (proxy call; opts: { headers?, body?, query? })
1164
+ synthos.connector.list(opts?) — GET /api/connectors (opts: { category?, id? })
1165
+ synthos.agent.list(opts?) — GET /api/agents (returns configured agents; opts: { enabled?, provider? }; returns [{ id, name, description, url, enabled, provider, capabilities }])
1166
+ synthos.agent.send(agentId, message, attachments?) — POST /api/agents/:id/send (sends a text message to any agent, returns normalized { kind, text, raw }; attachments: [{ fileName, mimeType, content }])
1167
+ synthos.agent.sendStream(agentId, message, onEvent, attachments?) — POST /api/agents/:id/stream (SSE streaming; onEvent receives { kind, data }; returns { close() } handle; attachments: [{ fileName, mimeType, content }])
1168
+ synthos.agent.isEnabled(agentId) — checks if an agent is enabled (returns Promise<boolean>)
1169
+ synthos.agent.getCapabilities(agentId) — returns agent capabilities object (streaming, skills, etc.)
1060
1170
  All methods return Promises. Prefer these helpers over raw fetch().`;