synthos 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (312) hide show
  1. package/README.md +5 -5
  2. package/default-pages/elevenlabs_effects_studio/chat-history.json +1 -0
  3. package/default-pages/elevenlabs_effects_studio/page.html +1345 -1363
  4. package/default-pages/elevenlabs_effects_studio/page.json +13 -11
  5. package/default-pages/elevenlabs_voice_studio/chat-history.json +1 -0
  6. package/default-pages/elevenlabs_voice_studio/page.html +782 -801
  7. package/default-pages/elevenlabs_voice_studio/page.json +13 -11
  8. package/default-pages/json_tools/chat-history.json +1 -0
  9. package/default-pages/json_tools/page.html +70 -90
  10. package/default-pages/json_tools/page.json +12 -10
  11. package/default-pages/my_notes/chat-history.json +1 -0
  12. package/default-pages/my_notes/page.html +115 -131
  13. package/default-pages/my_notes/page.json +14 -12
  14. package/default-pages/neon_asteroids/chat-history.json +1 -0
  15. package/default-pages/neon_asteroids/page.html +1777 -1803
  16. package/default-pages/neon_asteroids/page.json +14 -12
  17. package/default-pages/oregon_trail/chat-history.json +1 -0
  18. package/default-pages/oregon_trail/page.html +290 -307
  19. package/default-pages/oregon_trail/page.json +14 -12
  20. package/default-pages/solar_explorer/chat-history.json +1 -0
  21. package/default-pages/solar_explorer/page.html +1929 -1951
  22. package/default-pages/solar_explorer/page.json +14 -12
  23. package/default-pages/solar_tutorial/chat-history.json +1 -0
  24. package/default-pages/solar_tutorial/page.html +464 -478
  25. package/default-pages/solar_tutorial/page.json +12 -10
  26. package/default-pages/us_map/chat-history.json +1 -0
  27. package/default-pages/us_map/page.html +170 -193
  28. package/default-pages/us_map/page.json +14 -12
  29. package/default-pages/us_map/page.light.png +0 -0
  30. package/default-pages/us_map_1850/chat-history.json +1 -0
  31. package/default-pages/us_map_1850/page.html +302 -326
  32. package/default-pages/us_map_1850/page.json +14 -12
  33. package/default-pages/western_cities_1850/chat-history.json +1 -0
  34. package/default-pages/western_cities_1850/page.html +503 -527
  35. package/default-pages/western_cities_1850/page.json +14 -12
  36. package/default-themes/aurora-dawn.v3.css +15 -14
  37. package/default-themes/aurora-dusk.v3.css +26 -26
  38. package/default-themes/cosmos-dawn.v3.css +15 -14
  39. package/default-themes/cosmos-dusk.v3.css +26 -26
  40. package/default-themes/elemental-dawn.v3.css +200 -0
  41. package/default-themes/nebula-dawn.v3.css +15 -14
  42. package/default-themes/nebula-dusk.v3.css +24 -24
  43. package/default-themes/solar-flare-dawn.v3.css +15 -14
  44. package/default-themes/solar-flare-dusk.v3.css +26 -26
  45. package/dist/builders/anthropic.d.ts +26 -2
  46. package/dist/builders/anthropic.d.ts.map +1 -1
  47. package/dist/builders/anthropic.js +132 -31
  48. package/dist/builders/anthropic.js.map +1 -1
  49. package/dist/builders/claudecode.d.ts +13 -0
  50. package/dist/builders/claudecode.d.ts.map +1 -0
  51. package/dist/builders/claudecode.js +253 -0
  52. package/dist/builders/claudecode.js.map +1 -0
  53. package/dist/builders/index.d.ts +2 -1
  54. package/dist/builders/index.d.ts.map +1 -1
  55. package/dist/builders/index.js +8 -1
  56. package/dist/builders/index.js.map +1 -1
  57. package/dist/builders/openai.js +2 -1
  58. package/dist/builders/openai.js.map +1 -1
  59. package/dist/builders/types.d.ts +31 -7
  60. package/dist/builders/types.d.ts.map +1 -1
  61. package/dist/builders/types.js +60 -28
  62. package/dist/builders/types.js.map +1 -1
  63. package/dist/connectors/types.d.ts +8 -0
  64. package/dist/connectors/types.d.ts.map +1 -1
  65. package/dist/init.d.ts.map +1 -1
  66. package/dist/init.js +13 -6
  67. package/dist/init.js.map +1 -1
  68. package/dist/migrations.d.ts.map +1 -1
  69. package/dist/migrations.js +161 -14
  70. package/dist/migrations.js.map +1 -1
  71. package/dist/models/anthropic.d.ts +1 -0
  72. package/dist/models/anthropic.d.ts.map +1 -1
  73. package/dist/models/anthropic.js +129 -29
  74. package/dist/models/anthropic.js.map +1 -1
  75. package/dist/models/chainOfThought.d.ts.map +1 -1
  76. package/dist/models/chainOfThought.js +32 -19
  77. package/dist/models/chainOfThought.js.map +1 -1
  78. package/dist/models/index.d.ts +2 -2
  79. package/dist/models/index.d.ts.map +1 -1
  80. package/dist/models/index.js +2 -1
  81. package/dist/models/index.js.map +1 -1
  82. package/dist/models/providers.d.ts +1 -0
  83. package/dist/models/providers.d.ts.map +1 -1
  84. package/dist/models/providers.js +12 -4
  85. package/dist/models/providers.js.map +1 -1
  86. package/dist/models/types.d.ts +15 -1
  87. package/dist/models/types.d.ts.map +1 -1
  88. package/dist/models/types.js.map +1 -1
  89. package/dist/pages.d.ts +57 -8
  90. package/dist/pages.d.ts.map +1 -1
  91. package/dist/pages.js +258 -45
  92. package/dist/pages.js.map +1 -1
  93. package/dist/service/createCompletePrompt.d.ts.map +1 -1
  94. package/dist/service/createCompletePrompt.js +5 -0
  95. package/dist/service/createCompletePrompt.js.map +1 -1
  96. package/dist/service/mediaCache.d.ts +36 -0
  97. package/dist/service/mediaCache.d.ts.map +1 -0
  98. package/dist/service/mediaCache.js +182 -0
  99. package/dist/service/mediaCache.js.map +1 -0
  100. package/dist/service/pageValidator.d.ts +25 -0
  101. package/dist/service/pageValidator.d.ts.map +1 -0
  102. package/dist/service/pageValidator.js +315 -0
  103. package/dist/service/pageValidator.js.map +1 -0
  104. package/dist/service/server.d.ts.map +1 -1
  105. package/dist/service/server.js +4 -0
  106. package/dist/service/server.js.map +1 -1
  107. package/dist/service/sharedTableSchema.d.ts +73 -0
  108. package/dist/service/sharedTableSchema.d.ts.map +1 -0
  109. package/dist/service/sharedTableSchema.js +206 -0
  110. package/dist/service/sharedTableSchema.js.map +1 -0
  111. package/dist/service/transformPage.d.ts +49 -11
  112. package/dist/service/transformPage.d.ts.map +1 -1
  113. package/dist/service/transformPage.js +354 -241
  114. package/dist/service/transformPage.js.map +1 -1
  115. package/dist/service/useApiRoutes.d.ts.map +1 -1
  116. package/dist/service/useApiRoutes.js +288 -34
  117. package/dist/service/useApiRoutes.js.map +1 -1
  118. package/dist/service/useConnectorRoutes.d.ts.map +1 -1
  119. package/dist/service/useConnectorRoutes.js +170 -32
  120. package/dist/service/useConnectorRoutes.js.map +1 -1
  121. package/dist/service/useDataRoutes.d.ts.map +1 -1
  122. package/dist/service/useDataRoutes.js +59 -2
  123. package/dist/service/useDataRoutes.js.map +1 -1
  124. package/dist/service/useExtractRoutes.d.ts +4 -0
  125. package/dist/service/useExtractRoutes.d.ts.map +1 -0
  126. package/dist/service/useExtractRoutes.js +304 -0
  127. package/dist/service/useExtractRoutes.js.map +1 -0
  128. package/dist/service/usePageRoutes.d.ts +17 -0
  129. package/dist/service/usePageRoutes.d.ts.map +1 -1
  130. package/dist/service/usePageRoutes.js +1385 -483
  131. package/dist/service/usePageRoutes.js.map +1 -1
  132. package/dist/service/useSharedDataRoutes.d.ts.map +1 -1
  133. package/dist/service/useSharedDataRoutes.js +54 -2
  134. package/dist/service/useSharedDataRoutes.js.map +1 -1
  135. package/dist/settings.d.ts +27 -0
  136. package/dist/settings.d.ts.map +1 -1
  137. package/dist/settings.js +40 -1
  138. package/dist/settings.js.map +1 -1
  139. package/dist/themes.d.ts +0 -5
  140. package/dist/themes.d.ts.map +1 -1
  141. package/dist/themes.js +3 -95
  142. package/dist/themes.js.map +1 -1
  143. package/migration-rules/v2-to-v3.md +277 -119
  144. package/package.json +5 -1
  145. package/{default-pages/application → required-pages/_shell}/page.html +56 -42
  146. package/required-pages/_shell/page.json +14 -0
  147. package/required-pages/_starters/page.html +534 -0
  148. package/required-pages/_starters/page.json +12 -0
  149. package/required-pages/builder/page.html +353 -43
  150. package/required-pages/builder/page.json +12 -10
  151. package/required-pages/pages/page.html +697 -924
  152. package/required-pages/pages/page.json +12 -10
  153. package/required-pages/settings/page.html +1879 -1753
  154. package/required-pages/settings/page.json +12 -10
  155. package/required-pages/synthos_apis/page.html +834 -845
  156. package/required-pages/synthos_apis/page.json +12 -10
  157. package/required-pages/synthos_scripts/page.html +74 -88
  158. package/required-pages/synthos_scripts/page.json +12 -10
  159. package/scripts/append-instructions.py +90 -0
  160. package/scripts/audit-instructions.py +76 -0
  161. package/scripts/cleanup-shell-markup.mjs +112 -0
  162. package/service-connectors/buffer/connector.json +46 -0
  163. package/service-connectors/canva/connector.json +67 -0
  164. package/service-connectors/elevenlabs/connector.json +1 -1
  165. package/src/builders/anthropic.ts +155 -30
  166. package/src/builders/claudecode.ts +310 -0
  167. package/src/builders/index.ts +7 -1
  168. package/src/builders/openai.ts +2 -1
  169. package/src/builders/types.ts +93 -32
  170. package/src/connectors/types.ts +8 -0
  171. package/src/init.ts +13 -7
  172. package/src/migrations.ts +187 -16
  173. package/src/models/anthropic.ts +140 -30
  174. package/src/models/chainOfThought.ts +33 -18
  175. package/src/models/index.ts +2 -2
  176. package/src/models/providers.ts +12 -3
  177. package/src/models/types.ts +21 -1
  178. package/src/pages.ts +271 -35
  179. package/src/service/createCompletePrompt.ts +6 -0
  180. package/src/service/mediaCache.ts +206 -0
  181. package/src/service/pageValidator.ts +337 -0
  182. package/src/service/server.ts +4 -0
  183. package/src/service/sharedTableSchema.ts +236 -0
  184. package/src/service/transformPage.ts +370 -260
  185. package/src/service/useApiRoutes.ts +282 -32
  186. package/src/service/useConnectorRoutes.ts +189 -34
  187. package/src/service/useDataRoutes.ts +198 -116
  188. package/src/service/useExtractRoutes.ts +331 -0
  189. package/src/service/usePageRoutes.ts +1411 -394
  190. package/src/service/useSharedDataRoutes.ts +184 -109
  191. package/src/settings.ts +65 -0
  192. package/src/themes.ts +78 -180
  193. package/starters/blank_starter/chat-history.json +1 -0
  194. package/starters/blank_starter/page.dark.png +0 -0
  195. package/starters/blank_starter/page.html +47 -0
  196. package/starters/blank_starter/page.json +13 -0
  197. package/starters/blank_starter/page.light.png +0 -0
  198. package/starters/calculator_starter/chat-history.json +1 -0
  199. package/starters/calculator_starter/page.dark.png +0 -0
  200. package/starters/calculator_starter/page.html +232 -0
  201. package/starters/calculator_starter/page.json +13 -0
  202. package/starters/calculator_starter/page.light.png +0 -0
  203. package/starters/calendar_starter/chat-history.json +1 -0
  204. package/starters/calendar_starter/page.dark.png +0 -0
  205. package/starters/calendar_starter/page.html +495 -0
  206. package/starters/calendar_starter/page.json +13 -0
  207. package/starters/calendar_starter/page.light.png +0 -0
  208. package/starters/chat_starter/chat-history.json +1 -0
  209. package/starters/chat_starter/page.dark.png +0 -0
  210. package/starters/chat_starter/page.html +351 -0
  211. package/starters/chat_starter/page.json +13 -0
  212. package/starters/chat_starter/page.light.png +0 -0
  213. package/starters/checklist_starter/chat-history.json +1 -0
  214. package/starters/checklist_starter/page.dark.png +0 -0
  215. package/starters/checklist_starter/page.html +437 -0
  216. package/starters/checklist_starter/page.json +13 -0
  217. package/starters/checklist_starter/page.light.png +0 -0
  218. package/starters/dashboard_starter/chat-history.json +1 -0
  219. package/starters/dashboard_starter/page.dark.png +0 -0
  220. package/starters/dashboard_starter/page.html +195 -0
  221. package/starters/dashboard_starter/page.json +13 -0
  222. package/starters/dashboard_starter/page.light.png +0 -0
  223. package/starters/form_starter/chat-history.json +1 -0
  224. package/starters/form_starter/page.dark.png +0 -0
  225. package/starters/form_starter/page.html +313 -0
  226. package/starters/form_starter/page.json +13 -0
  227. package/starters/form_starter/page.light.png +0 -0
  228. package/starters/gallery_starter/chat-history.json +1 -0
  229. package/starters/gallery_starter/page.dark.png +0 -0
  230. package/starters/gallery_starter/page.html +418 -0
  231. package/starters/gallery_starter/page.json +13 -0
  232. package/starters/gallery_starter/page.light.png +0 -0
  233. package/starters/generator_starter/chat-history.json +1 -0
  234. package/starters/generator_starter/page.dark.png +0 -0
  235. package/starters/generator_starter/page.html +261 -0
  236. package/starters/generator_starter/page.json +13 -0
  237. package/starters/generator_starter/page.light.png +0 -0
  238. package/starters/index.html +538 -0
  239. package/starters/kanban_starter/chat-history.json +1 -0
  240. package/starters/kanban_starter/page.dark.png +0 -0
  241. package/starters/kanban_starter/page.html +432 -0
  242. package/starters/kanban_starter/page.json +13 -0
  243. package/starters/kanban_starter/page.light.png +0 -0
  244. package/starters/presentation_builder/chat-history.json +1 -0
  245. package/starters/presentation_builder/page.dark.png +0 -0
  246. package/starters/presentation_builder/page.html +970 -0
  247. package/starters/presentation_builder/page.json +15 -0
  248. package/starters/presentation_builder/page.light.png +0 -0
  249. package/starters/presentation_builder/presentation_voice/voice_config.json +9 -0
  250. package/starters/pulse_starter/chat-history.json +1 -0
  251. package/starters/pulse_starter/page.dark.png +0 -0
  252. package/starters/pulse_starter/page.html +698 -0
  253. package/starters/pulse_starter/page.json +13 -0
  254. package/starters/pulse_starter/page.light.png +0 -0
  255. package/starters/quiz_starter/chat-history.json +1 -0
  256. package/starters/quiz_starter/page.dark.png +0 -0
  257. package/starters/quiz_starter/page.html +292 -0
  258. package/starters/quiz_starter/page.json +13 -0
  259. package/starters/quiz_starter/page.light.png +0 -0
  260. package/starters/reference_starter/chat-history.json +1 -0
  261. package/starters/reference_starter/page.dark.png +0 -0
  262. package/starters/reference_starter/page.html +250 -0
  263. package/starters/reference_starter/page.json +13 -0
  264. package/starters/reference_starter/page.light.png +0 -0
  265. package/starters/retro_game_starter/chat-history.json +1 -0
  266. package/starters/retro_game_starter/page.dark.png +0 -0
  267. package/{default-pages → starters}/retro_game_starter/page.html +1281 -1308
  268. package/starters/retro_game_starter/page.json +15 -0
  269. package/starters/retro_game_starter/page.light.png +0 -0
  270. package/starters/roster_starter/chat-history.json +1 -0
  271. package/starters/roster_starter/page.dark.png +0 -0
  272. package/starters/roster_starter/page.html +600 -0
  273. package/starters/roster_starter/page.json +13 -0
  274. package/starters/roster_starter/page.light.png +0 -0
  275. package/starters/server.js +182 -0
  276. package/starters/start.cmd +1 -0
  277. package/starters/timeline_starter/chat-history.json +1 -0
  278. package/starters/timeline_starter/page.dark.png +0 -0
  279. package/starters/timeline_starter/page.html +446 -0
  280. package/starters/timeline_starter/page.json +13 -0
  281. package/starters/timeline_starter/page.light.png +0 -0
  282. package/starters/tutorial_starter/chat-history.json +1 -0
  283. package/starters/tutorial_starter/page.dark.png +0 -0
  284. package/starters/tutorial_starter/page.html +283 -0
  285. package/starters/tutorial_starter/page.json +13 -0
  286. package/starters/tutorial_starter/page.light.png +0 -0
  287. package/static-files/agent.v3.js +122 -0
  288. package/static-files/connector.v3.js +48 -0
  289. package/static-files/extract.v3.js +188 -0
  290. package/static-files/helpers.v3.js +50 -6
  291. package/static-files/page-bridge.js +114 -0
  292. package/static-files/page.v3.js +1292 -1290
  293. package/static-files/script.v3.js +32 -0
  294. package/static-files/server.v3.js +89 -0
  295. package/static-files/shell-bridge.v3.js +174 -0
  296. package/static-files/shell-modals.v3.js +521 -0
  297. package/static-files/{shell.css → shell.v3.css} +271 -22
  298. package/static-files/shell.v3.js +1865 -0
  299. package/static-files/storage.v3.js +176 -0
  300. package/tests/anthropic.spec.ts +42 -7
  301. package/tests/builders.spec.ts +72 -4
  302. package/tests/pageValidator.spec.ts +548 -0
  303. package/tests/profiles.spec.ts +122 -0
  304. package/tests/providers.spec.ts +1 -1
  305. package/tests/sharedTableSchema.spec.ts +242 -0
  306. package/tests/transformPage.spec.ts +62 -81
  307. package/default-pages/application/page.json +0 -10
  308. package/default-pages/retro_game_starter/page.json +0 -12
  309. package/default-pages/sidebar_page/page.html +0 -51
  310. package/default-pages/sidebar_page/page.json +0 -10
  311. package/default-pages/two-panel_page/page.html +0 -68
  312. package/default-pages/two-panel_page/page.json +0 -10
@@ -23,169 +23,85 @@ var __importStar = (this && this.__importStar) || function (mod) {
23
23
  return result;
24
24
  };
25
25
  Object.defineProperty(exports, "__esModule", { value: true });
26
- exports.serverAPIs = exports.buildRouteHints = exports.DEFAULT_ROUTE_HINTS = exports.AGENT_API_REFERENCE = exports.getTransformInstr = exports.getMessageFormat = exports.parseChangeList = exports.applyChangeList = exports.ensureScriptsBeforeBodyClose = exports.deduplicateInlineScripts = exports.normalizedIndexOf = exports.stripNodeIds = exports.assignNodeIds = exports.simpleMarkdown = exports.transformPage = void 0;
26
+ exports.serverAPIs = exports.buildRouteHints = exports.DEFAULT_ROUTE_HINTS = exports.AGENT_API_REFERENCE = exports.getTransformInstr = exports.parseChangeList = exports.validateChangeOps = exports.applyChangeListWithReport = exports.applyChangeList = exports.ensureScriptsBeforeBodyClose = exports.deduplicateInlineScripts = exports.normalizedIndexOf = exports.stripNodeIds = exports.assignNodeIds = exports.transformPage = void 0;
27
27
  const cheerio = __importStar(require("cheerio"));
28
28
  async function transformPage(args) {
29
29
  const { message, builder, additionalSections } = args;
30
30
  // 0. Strip the early error-capture script so the LLM never sees it
31
31
  const pageState = stripErrorCapture(args.pageState);
32
- // 1. Assign data-node-id to every element
32
+ // 1. Assign data-nid to every element
33
33
  const { html: annotatedHtml } = assignNodeIds(pageState);
34
+ // 1b. Strip HTML comments from the annotated copy sent to the LLM (saves tokens).
35
+ // The original pageState is preserved — changes are applied to the annotated copy
36
+ // and then node IDs are stripped, so comments survive in the saved page.
37
+ const llmHtml = annotatedHtml.replace(/<!--[\s\S]*?-->/g, '');
34
38
  try {
35
39
  // 2. Build CURRENT_PAGE section
36
40
  const currentPage = {
37
41
  title: '<CURRENT_PAGE>',
38
- content: annotatedHtml,
42
+ content: llmHtml,
43
+ sketch: null,
44
+ mode: 'always-full',
39
45
  instructions: '',
40
46
  };
41
- // 3. Determine newBuild: if isBuilder, count .chat-message in annotated HTML
47
+ // 3. Determine newBuild: if isBuilder, check chat history length
42
48
  let newBuild = false;
43
49
  if (args.isBuilder) {
44
- const $ = cheerio.load(annotatedHtml, { decodeEntities: false });
45
- const messageCount = $('#chatMessages .chat-message').length;
46
- newBuild = messageCount <= 1;
50
+ if (args.history) {
51
+ // Shell provides explicit history — empty or greeting-only means new build
52
+ newBuild = args.history.length <= 1;
53
+ }
54
+ else {
55
+ // Fallback: count .chat-message in annotated HTML (legacy pages without chat-history.json)
56
+ const $ = cheerio.load(annotatedHtml, { decodeEntities: false });
57
+ const messageCount = $('#chatMessages .chat-message').length;
58
+ newBuild = messageCount <= 1;
59
+ }
47
60
  }
48
- // 4. Call builder
49
- const result = await builder.run(currentPage, additionalSections, message, newBuild, args.attachments);
61
+ // 4. Call builder (with timeout guard)
62
+ const TRANSFORM_TIMEOUT_MS = 600_000; // 10 minutes
63
+ const result = await Promise.race([
64
+ builder.run(currentPage, additionalSections, message, newBuild, args.attachments, args.tools, args.toolHandlers, args.onToolCall),
65
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Page transform timed out — the AI took too long to respond. Please try again.')), TRANSFORM_TIMEOUT_MS)),
66
+ ]);
50
67
  // 5. Switch on result kind
51
68
  switch (result.kind) {
52
69
  case 'transforms': {
53
- const applied = applyChangeList(annotatedHtml, result.changes);
54
- const clean = stripNodeIds(applied);
55
- const deduped = deduplicateInlineScripts(clean);
56
- const safe = ensureScriptsBeforeBodyClose(deduped);
57
- return { completed: true, value: { html: safe, changeCount: result.changes.length } };
70
+ const report = applyChangeListWithReport(annotatedHtml, result.changes);
71
+ const safe = postProcessHtml(report.html);
72
+ return { completed: true, value: {
73
+ html: safe,
74
+ changeCount: result.changes.length,
75
+ replyText: result.message,
76
+ skippedOps: report.skippedOps,
77
+ skipReasons: report.skipReasons,
78
+ } };
58
79
  }
59
80
  case 'reply': {
60
- const productName = args.productName ?? 'SynthOS';
61
- const withReply = appendChatReply(annotatedHtml, message, result.text, productName);
62
- const clean = stripNodeIds(withReply);
63
- const deduped = deduplicateInlineScripts(clean);
64
- const safe = ensureScriptsBeforeBodyClose(deduped);
65
- return { completed: true, value: { html: safe, changeCount: -1 } };
81
+ const safe = postProcessHtml(annotatedHtml);
82
+ return { completed: true, value: { html: safe, changeCount: -1, replyText: result.text } };
66
83
  }
67
84
  case 'error': {
68
- const productName = args.productName ?? 'SynthOS';
69
- const errorHtml = appendChatError(annotatedHtml, message, result.error.message, productName);
70
- const clean = stripNodeIds(errorHtml);
71
- return { completed: true, value: { html: clean, changeCount: -1 } };
85
+ const clean = stripNodeIds(annotatedHtml);
86
+ return { completed: true, value: { html: clean, changeCount: -1, errorText: result.error.message } };
72
87
  }
73
88
  }
74
89
  }
75
90
  catch (err) {
76
- // On any error: append error message to chat
77
- const productName = args.productName ?? 'SynthOS';
78
91
  const errorMessage = err instanceof Error ? err.message : String(err);
79
- const errorHtml = appendChatError(annotatedHtml, message, errorMessage, productName);
80
- const clean = stripNodeIds(errorHtml);
81
- return { completed: true, value: { html: clean, changeCount: -1 } };
92
+ const clean = stripNodeIds(annotatedHtml);
93
+ return { completed: true, value: { html: clean, changeCount: -1, errorText: errorMessage } };
82
94
  }
83
95
  }
84
96
  exports.transformPage = transformPage;
85
97
  // ---------------------------------------------------------------------------
86
- // Chat reply helper
87
- // ---------------------------------------------------------------------------
88
- /**
89
- * Append a user message and a reply to #chatMessages using cheerio.
90
- */
91
- function appendChatReply(annotatedHtml, userMessage, replyText, productName) {
92
- const $ = cheerio.load(annotatedHtml, { decodeEntities: false });
93
- const chatMessages = $('#chatMessages');
94
- if (chatMessages.length > 0) {
95
- chatMessages.append(`<div class="chat-message"><p><strong>User:</strong> ${escapeHtml(userMessage)}</p></div>`);
96
- const replyHtml = simpleMarkdown(replyText);
97
- chatMessages.append(`<div class="chat-message"><p><strong>${escapeHtml(productName)}:</strong> ${replyHtml}</p></div>`);
98
- }
99
- return $.html();
100
- }
101
- function escapeHtml(text) {
102
- return text
103
- .replace(/&/g, '&amp;')
104
- .replace(/</g, '&lt;')
105
- .replace(/>/g, '&gt;')
106
- .replace(/"/g, '&quot;');
107
- }
108
- /**
109
- * Lightweight markdown-to-HTML converter for chat reply text.
110
- * Handles: code blocks, inline code, bold, italic, links, unordered/ordered lists, paragraphs.
111
- */
112
- function simpleMarkdown(text) {
113
- // Extract fenced code blocks first to protect their contents
114
- const codeBlocks = [];
115
- let processed = text.replace(/```(\w*)\n([\s\S]*?)```/g, (_match, lang, code) => {
116
- const idx = codeBlocks.length;
117
- const escaped = escapeHtml(code.replace(/\n$/, ''));
118
- const langAttr = lang ? ` class="language-${escapeHtml(lang)}"` : '';
119
- codeBlocks.push(`<pre><code${langAttr}>${escaped}</code></pre>`);
120
- return `\x00CODEBLOCK${idx}\x00`;
121
- });
122
- // Split into paragraphs by blank lines
123
- const blocks = processed.split(/\n{2,}/);
124
- const htmlBlocks = [];
125
- for (const block of blocks) {
126
- const trimmed = block.trim();
127
- if (!trimmed)
128
- continue;
129
- // Code block placeholder
130
- if (/^\x00CODEBLOCK\d+\x00$/.test(trimmed)) {
131
- htmlBlocks.push(trimmed);
132
- continue;
133
- }
134
- // Unordered list (lines starting with - or *)
135
- if (/^[\-\*] /m.test(trimmed) && trimmed.split('\n').every(l => /^[\-\*] /.test(l.trim()) || l.trim() === '')) {
136
- const items = trimmed.split('\n')
137
- .map(l => l.trim())
138
- .filter(l => l)
139
- .map(l => `<li>${inlineMarkdown(l.replace(/^[\-\*] /, ''))}</li>`)
140
- .join('');
141
- htmlBlocks.push(`<ul>${items}</ul>`);
142
- continue;
143
- }
144
- // Ordered list (lines starting with 1. 2. etc.)
145
- if (/^\d+\. /m.test(trimmed) && trimmed.split('\n').every(l => /^\d+\. /.test(l.trim()) || l.trim() === '')) {
146
- const items = trimmed.split('\n')
147
- .map(l => l.trim())
148
- .filter(l => l)
149
- .map(l => `<li>${inlineMarkdown(l.replace(/^\d+\. /, ''))}</li>`)
150
- .join('');
151
- htmlBlocks.push(`<ol>${items}</ol>`);
152
- continue;
153
- }
154
- // Regular paragraph
155
- htmlBlocks.push(`<p>${inlineMarkdown(trimmed.replace(/\n/g, '<br>'))}</p>`);
156
- }
157
- // Restore code blocks
158
- let result = htmlBlocks.join('');
159
- result = result.replace(/\x00CODEBLOCK(\d+)\x00/g, (_m, idx) => codeBlocks[parseInt(idx)]);
160
- return result;
161
- }
162
- exports.simpleMarkdown = simpleMarkdown;
163
- /** Apply inline markdown formatting: bold, italic, inline code, links. */
164
- function inlineMarkdown(text) {
165
- // Inline code (protect from further processing)
166
- const codes = [];
167
- let result = text.replace(/`([^`]+)`/g, (_m, code) => {
168
- const idx = codes.length;
169
- codes.push(`<code>${escapeHtml(code)}</code>`);
170
- return `\x00CODE${idx}\x00`;
171
- });
172
- // Bold (**text** or __text__)
173
- result = result.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
174
- result = result.replace(/__(.+?)__/g, '<strong>$1</strong>');
175
- // Italic (*text* or _text_)
176
- result = result.replace(/\*(.+?)\*/g, '<em>$1</em>');
177
- result = result.replace(/(?<!\w)_(.+?)_(?!\w)/g, '<em>$1</em>');
178
- // Links [text](url)
179
- result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
180
- // Restore inline code
181
- result = result.replace(/\x00CODE(\d+)\x00/g, (_m, idx) => codes[parseInt(idx)]);
182
- return result;
183
- }
184
- // ---------------------------------------------------------------------------
185
98
  // Internal helpers
186
99
  // ---------------------------------------------------------------------------
100
+ /** Tags the LLM will never target — skip annotation to save tokens. */
101
+ const SKIP_ANNOTATION_TAGS = new Set(['br', 'wbr', 'col', 'source']);
187
102
  /**
188
- * Assign sequential `data-node-id` to every element in the HTML.
103
+ * Assign sequential `data-nid` to every element in the HTML.
104
+ * Skips trivial elements (br, wbr, col, source) that the LLM never targets.
189
105
  */
190
106
  function assignNodeIds(html) {
191
107
  const $ = cheerio.load(html, { decodeEntities: false });
@@ -193,21 +109,116 @@ function assignNodeIds(html) {
193
109
  $('*').each(function () {
194
110
  const el = $(this);
195
111
  if (this.type === 'tag' || this.type === 'script' || this.type === 'style') {
196
- el.attr('data-node-id', String(counter++));
112
+ const tag = this.tagName?.toLowerCase() ?? this.name?.toLowerCase();
113
+ if (SKIP_ANNOTATION_TAGS.has(tag))
114
+ return;
115
+ el.attr('data-nid', String(counter++));
197
116
  }
198
117
  });
199
118
  return { html: $.html(), nodeCount: counter };
200
119
  }
201
120
  exports.assignNodeIds = assignNodeIds;
202
121
  /**
203
- * Remove all `data-node-id` attributes from the HTML.
122
+ * Remove all `data-nid` attributes from the HTML.
204
123
  */
205
124
  function stripNodeIds(html) {
206
125
  const $ = cheerio.load(html, { decodeEntities: false });
207
- $('[data-node-id]').removeAttr('data-node-id');
126
+ $('[data-nid]').removeAttr('data-nid');
208
127
  return $.html();
209
128
  }
210
129
  exports.stripNodeIds = stripNodeIds;
130
+ /**
131
+ * Consolidated post-processing: strip node IDs, deduplicate scripts, and
132
+ * ensure script ordering — all in a single cheerio parse/serialize cycle.
133
+ */
134
+ function postProcessHtml(html) {
135
+ const $ = cheerio.load(html, { decodeEntities: false });
136
+ // Strip data-nid attributes
137
+ $('[data-nid]').removeAttr('data-nid');
138
+ // Deduplicate inline scripts (ID-based pass)
139
+ const SYSTEM_IDS = new Set(['page-info', 'page-helpers', 'page-script', 'error', 'shell-v3', 'server-v3', 'storage-v3', 'script-v3', 'connector-v3', 'agent-v3']);
140
+ const idGroups = new Map();
141
+ $('script').each(function (_, rawEl) {
142
+ const el = $(rawEl);
143
+ if (el.attr('src'))
144
+ return;
145
+ const id = el.attr('id');
146
+ if (!id || SYSTEM_IDS.has(id))
147
+ return;
148
+ if (!idGroups.has(id))
149
+ idGroups.set(id, []);
150
+ idGroups.get(id).push(el);
151
+ });
152
+ for (const [id, group] of idGroups) {
153
+ if (group.length < 2)
154
+ continue;
155
+ for (let i = 0; i < group.length - 1; i++) {
156
+ console.log(`deduplicateInlineScripts: removing duplicate script id="${id}" (keeping last of ${group.length})`);
157
+ group[i].remove();
158
+ }
159
+ }
160
+ // Deduplicate inline scripts (declaration-overlap pass for id-less scripts)
161
+ const declPattern = /(?:^|;|\n)\s*(?:let|const|var|function|class)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g;
162
+ const scripts = [];
163
+ $('script').each(function (_, rawEl) {
164
+ const el = $(rawEl);
165
+ if (el.attr('src'))
166
+ return;
167
+ if (el.attr('id'))
168
+ return;
169
+ if ((el.attr('type') ?? '').toLowerCase() === 'application/json')
170
+ return;
171
+ const code = (el.html() ?? '').trim();
172
+ if (!code)
173
+ return;
174
+ const declarations = new Set();
175
+ let m;
176
+ declPattern.lastIndex = 0;
177
+ while ((m = declPattern.exec(code)) !== null)
178
+ declarations.add(m[1]);
179
+ scripts.push({ el, declarations });
180
+ });
181
+ const toRemove = new Set();
182
+ for (let i = 0; i < scripts.length; i++) {
183
+ if (toRemove.has(i))
184
+ continue;
185
+ for (let j = i + 1; j < scripts.length; j++) {
186
+ if (toRemove.has(j))
187
+ continue;
188
+ const a = scripts[i].declarations, b = scripts[j].declarations;
189
+ if (a.size < 2 || b.size < 2)
190
+ continue;
191
+ let overlap = 0;
192
+ for (const name of a)
193
+ if (b.has(name))
194
+ overlap++;
195
+ if (overlap / Math.min(a.size, b.size) >= 0.6) {
196
+ console.log(`deduplicateInlineScripts: removing duplicate script (${overlap}/${Math.min(a.size, b.size)} declaration overlap)`);
197
+ toRemove.add(i);
198
+ break;
199
+ }
200
+ }
201
+ }
202
+ for (const idx of toRemove)
203
+ scripts[idx].el.remove();
204
+ // Ensure page-helpers and page-script are last children of <body>
205
+ const body = $('body');
206
+ if (body.length > 0) {
207
+ const helpers = $('script#page-helpers');
208
+ const pageScript = $('script#page-script');
209
+ const helpersHtml = helpers.length > 0 ? $.html(helpers.first()) : '';
210
+ const pageScriptHtml = pageScript.length > 0 ? $.html(pageScript.first()) : '';
211
+ if (helpers.length > 0)
212
+ helpers.remove();
213
+ if (pageScript.length > 0)
214
+ pageScript.remove();
215
+ if (helpersHtml)
216
+ body.append(helpersHtml);
217
+ if (pageScriptHtml)
218
+ body.append(pageScriptHtml);
219
+ }
220
+ return $.html();
221
+ }
211
222
  /**
212
223
  * Remove the early error-capture script injected into <head> so the LLM
213
224
  * never sees it during page transformation.
@@ -282,7 +293,7 @@ exports.normalizedIndexOf = normalizedIndexOf;
282
293
  */
283
294
  function deduplicateInlineScripts(html) {
284
295
  const $ = cheerio.load(html, { decodeEntities: false });
285
- const SYSTEM_IDS = new Set(['page-info', 'page-helpers', 'page-script', 'error']);
296
+ const SYSTEM_IDS = new Set(['page-info', 'page-helpers', 'page-script', 'error', 'shell-v3', 'server-v3', 'storage-v3', 'script-v3', 'connector-v3', 'agent-v3']);
286
297
  // ── Pass 1: ID-based dedup ──────────────────────────────────────────
287
298
  const idGroups = new Map();
288
299
  $('script').each(function (_, rawEl) {
@@ -374,8 +385,8 @@ function ensureScriptsBeforeBodyClose(html) {
374
385
  // Capture outer HTML before removing so we can re-append
375
386
  const helpers = $('script#page-helpers');
376
387
  const pageScript = $('script#page-script');
377
- const helpersHtml = helpers.length > 0 ? $.html(helpers) : '';
378
- const pageScriptHtml = pageScript.length > 0 ? $.html(pageScript) : '';
388
+ const helpersHtml = helpers.length > 0 ? $.html(helpers.first()) : '';
389
+ const pageScriptHtml = pageScript.length > 0 ? $.html(pageScript.first()) : '';
379
390
  // Remove from current position and re-append at end of <body>
380
391
  if (helpers.length > 0)
381
392
  helpers.remove();
@@ -395,16 +406,40 @@ function isElementLocked(el, $) {
395
406
  return el.attr('data-locked') !== undefined;
396
407
  }
397
408
  /**
398
- * If the target element is a <script> or <style> and the html is wrapped in a
399
- * redundant matching tag, strip the outer tag to avoid nesting (e.g.
400
- * `<script>` inside `<script>`). Returns the inner content when unwrapped.
409
+ * If the provided html is wrapped in a redundant tag that matches the target
410
+ * element, strip the outer tag to avoid nesting (e.g. `<div id="x">` inside
411
+ * the existing `<div id="x">`). For script/style elements the tag name alone
412
+ * is sufficient; for other elements we require an id or class match to avoid
413
+ * false positives.
401
414
  */
402
- function unwrapRedundantTag(tagName, html) {
403
- if (tagName !== 'script' && tagName !== 'style')
415
+ function unwrapRedundantTag(tagName, html, targetId, targetClass) {
416
+ if (!tagName)
404
417
  return html;
405
- const re = new RegExp(`^\\s*<${tagName}[^>]*>([\\s\\S]*)</${tagName}>\\s*$`, 'i');
418
+ const re = new RegExp(`^\\s*<${tagName}\\b([^>]*)>([\\s\\S]*)</${tagName}>\\s*$`, 'i');
406
419
  const match = html.match(re);
407
- return match ? match[1] : html;
420
+ if (!match)
421
+ return html;
422
+ // For script/style, always unwrap (original behaviour)
423
+ if (tagName === 'script' || tagName === 'style') {
424
+ return match[2];
425
+ }
426
+ // For other elements, require an id or class match to avoid false positives
427
+ const outerAttrs = match[1];
428
+ if (targetId) {
429
+ const idMatch = outerAttrs.match(/\bid=["']([^"']*)["']/);
430
+ if (idMatch && idMatch[1] === targetId)
431
+ return match[2];
432
+ }
433
+ if (targetClass) {
434
+ const classMatch = outerAttrs.match(/\bclass=["']([^"']*)["']/);
435
+ if (classMatch) {
436
+ const targetClasses = targetClass.split(/\s+/);
437
+ const outerClasses = classMatch[1].split(/\s+/);
438
+ if (targetClasses.some(c => outerClasses.includes(c)))
439
+ return match[2];
440
+ }
441
+ }
442
+ return html;
408
443
  }
409
444
  /**
410
445
  * Strip any `<script>` or `<style>` tags from text that will be injected
@@ -418,30 +453,47 @@ function stripNestedBlockTags(tagName, text) {
418
453
  .replace(new RegExp(`</${tagName}>`, 'gi'), '');
419
454
  }
420
455
  /**
421
- * Apply a list of CRUD operations to annotated HTML (elements must have `data-node-id`).
456
+ * Apply a list of CRUD operations to annotated HTML (elements must have `data-nid`).
457
+ * Returns just the resulting HTML string. For diagnostic info about skipped ops,
458
+ * use `applyChangeListWithReport` instead.
422
459
  */
423
460
  function applyChangeList(html, changes) {
461
+ return applyChangeListWithReport(html, changes).html;
462
+ }
463
+ exports.applyChangeList = applyChangeList;
464
+ /**
465
+ * Apply a list of CRUD operations and return the resulting HTML plus a report
466
+ * of any ops that were silently skipped (missing target node, locked element,
467
+ * search text not found, etc.). Callers can surface the skip report to the
468
+ * user so that partial/broken builds do not fail silently.
469
+ */
470
+ function applyChangeListWithReport(html, changes) {
424
471
  const $ = cheerio.load(html, { decodeEntities: false });
472
+ const skipReasons = [];
473
+ const recordSkip = (reason) => {
474
+ console.warn(`applyChangeList: ${reason}`);
475
+ skipReasons.push(reason);
476
+ };
425
477
  for (const change of changes) {
426
478
  switch (change.op) {
427
479
  case 'update': {
428
- const el = $(`[data-node-id="${change.nodeId}"]`);
480
+ const el = $(`[data-nid="${change.nodeId}"]`);
429
481
  if (el.length === 0) {
430
- console.warn(`applyChangeList: skipping update — node ${change.nodeId} not found (already removed?)`);
482
+ recordSkip(`skipping update — node ${change.nodeId} not found (already removed?)`);
431
483
  break;
432
484
  }
433
485
  const tag = el.prop('tagName')?.toLowerCase();
434
- el.html(unwrapRedundantTag(tag, change.html));
486
+ el.html(unwrapRedundantTag(tag, change.html, el.attr('id'), el.attr('class')));
435
487
  break;
436
488
  }
437
489
  case 'replace': {
438
- const el = $(`[data-node-id="${change.nodeId}"]`);
490
+ const el = $(`[data-nid="${change.nodeId}"]`);
439
491
  if (el.length === 0) {
440
- console.warn(`applyChangeList: skipping replace — node ${change.nodeId} not found (already removed?)`);
492
+ recordSkip(`skipping replace — node ${change.nodeId} not found (already removed?)`);
441
493
  break;
442
494
  }
443
495
  if (isElementLocked(el, $)) {
444
- console.warn(`applyChangeList: skipping replace — node ${change.nodeId} is data-locked`);
496
+ recordSkip(`skipping replace — node ${change.nodeId} is data-locked`);
445
497
  break;
446
498
  }
447
499
  // If the target is a <script> or <style> and the html doesn't
@@ -458,22 +510,22 @@ function applyChangeList(html, changes) {
458
510
  break;
459
511
  }
460
512
  case 'delete': {
461
- const el = $(`[data-node-id="${change.nodeId}"]`);
513
+ const el = $(`[data-nid="${change.nodeId}"]`);
462
514
  if (el.length === 0) {
463
- console.warn(`applyChangeList: skipping delete — node ${change.nodeId} not found (already removed?)`);
515
+ recordSkip(`skipping delete — node ${change.nodeId} not found (already removed?)`);
464
516
  break;
465
517
  }
466
518
  if (isElementLocked(el, $)) {
467
- console.warn(`applyChangeList: skipping delete — node ${change.nodeId} is data-locked`);
519
+ recordSkip(`skipping delete — node ${change.nodeId} is data-locked`);
468
520
  break;
469
521
  }
470
522
  el.remove();
471
523
  break;
472
524
  }
473
525
  case 'insert': {
474
- const parent = $(`[data-node-id="${change.parentId}"]`);
526
+ const parent = $(`[data-nid="${change.parentId}"]`);
475
527
  if (parent.length === 0) {
476
- console.warn(`applyChangeList: skipping insert — parent node ${change.parentId} not found`);
528
+ recordSkip(`skipping insert — parent node ${change.parentId} not found`);
477
529
  break;
478
530
  }
479
531
  // Unwrap redundant tags when inserting into script/style
@@ -493,27 +545,27 @@ function applyChangeList(html, changes) {
493
545
  parent.after(insertHtml);
494
546
  break;
495
547
  default:
496
- console.warn(`applyChangeList: skipping insert — unknown position "${change.position}"`);
548
+ recordSkip(`skipping insert — unknown position "${change.position}"`);
497
549
  }
498
550
  break;
499
551
  }
500
552
  case 'style-element': {
501
- const el = $(`[data-node-id="${change.nodeId}"]`);
553
+ const el = $(`[data-nid="${change.nodeId}"]`);
502
554
  if (el.length === 0) {
503
- console.warn(`applyChangeList: skipping style-element — node ${change.nodeId} not found (already removed?)`);
555
+ recordSkip(`skipping style-element — node ${change.nodeId} not found (already removed?)`);
504
556
  break;
505
557
  }
506
558
  if (isElementLocked(el, $)) {
507
- console.warn(`applyChangeList: skipping style-element — node ${change.nodeId} is data-locked`);
559
+ recordSkip(`skipping style-element — node ${change.nodeId} is data-locked`);
508
560
  break;
509
561
  }
510
562
  el.attr('style', change.style);
511
563
  break;
512
564
  }
513
565
  case 'search-replace': {
514
- const el = $(`[data-node-id="${change.nodeId}"]`);
566
+ const el = $(`[data-nid="${change.nodeId}"]`);
515
567
  if (el.length === 0) {
516
- console.warn(`applyChangeList: skipping search-replace — node ${change.nodeId} not found (already removed?)`);
568
+ recordSkip(`skipping search-replace — node ${change.nodeId} not found (already removed?)`);
517
569
  break;
518
570
  }
519
571
  const srTag = el.prop('tagName')?.toLowerCase();
@@ -529,15 +581,15 @@ function applyChangeList(html, changes) {
529
581
  el.html(content.slice(0, norm.start) + replaceText + content.slice(norm.end));
530
582
  }
531
583
  else {
532
- console.warn(`applyChangeList: skipping search-replace — search text not found in node ${change.nodeId}`);
584
+ recordSkip(`skipping search-replace — search text not found in node ${change.nodeId}`);
533
585
  }
534
586
  }
535
587
  break;
536
588
  }
537
589
  case 'search-insert': {
538
- const el = $(`[data-node-id="${change.nodeId}"]`);
590
+ const el = $(`[data-nid="${change.nodeId}"]`);
539
591
  if (el.length === 0) {
540
- console.warn(`applyChangeList: skipping search-insert — node ${change.nodeId} not found (already removed?)`);
592
+ recordSkip(`skipping search-insert — node ${change.nodeId} not found (already removed?)`);
541
593
  break;
542
594
  }
543
595
  const siTag = el.prop('tagName')?.toLowerCase();
@@ -554,7 +606,7 @@ function applyChangeList(html, changes) {
554
606
  el.html(content.slice(0, norm.end) + insertContent + content.slice(norm.end));
555
607
  }
556
608
  else {
557
- console.warn(`applyChangeList: skipping search-insert — after text not found in node ${change.nodeId}`);
609
+ recordSkip(`skipping search-insert — after text not found in node ${change.nodeId}`);
558
610
  }
559
611
  }
560
612
  break;
@@ -563,32 +615,73 @@ function applyChangeList(html, changes) {
563
615
  throw new Error(`Unknown change op: "${change.op}"`);
564
616
  }
565
617
  }
566
- return $.html();
618
+ return { html: $.html(), skippedOps: skipReasons.length, skipReasons };
567
619
  }
568
- exports.applyChangeList = applyChangeList;
620
+ exports.applyChangeListWithReport = applyChangeListWithReport;
621
+ /** Known op types and their required fields (beyond `op`). */
622
+ const CHANGE_OP_REQUIRED_FIELDS = {
623
+ 'update': ['nodeId', 'html'],
624
+ 'replace': ['nodeId', 'html'],
625
+ 'delete': ['nodeId'],
626
+ 'insert': ['parentId', 'position', 'html'],
627
+ 'style-element': ['nodeId', 'style'],
628
+ 'search-replace': ['nodeId', 'search', 'replace'],
629
+ 'search-insert': ['nodeId', 'after', 'content'],
630
+ };
631
+ const VALID_INSERT_POSITIONS = new Set(['prepend', 'append', 'before', 'after']);
569
632
  /**
570
- * Append a user message and a styled error message to #chatMessages.
633
+ * Validate and filter a raw parsed array into a well-formed ChangeList.
634
+ * Invalid ops are logged as warnings and dropped rather than crashing.
571
635
  */
572
- function appendChatError(html, userMessage, errorDetails, productName) {
573
- const $ = cheerio.load(html, { decodeEntities: false });
574
- const chatMessages = $('#chatMessages');
575
- if (chatMessages.length > 0) {
576
- chatMessages.append(`<div class="chat-message"><p><strong>User:</strong> ${escapeHtml(userMessage)}</p></div>`);
577
- chatMessages.append(`<div class="chat-message chat-message-error"><p><strong>${escapeHtml(productName)}:</strong> Something went wrong \u2014 please try again.</p>`
578
- + `<p class="chat-error-details">${escapeHtml(errorDetails)}</p></div>`);
636
+ function validateChangeOps(raw) {
637
+ const valid = [];
638
+ for (let i = 0; i < raw.length; i++) {
639
+ const item = raw[i];
640
+ if (!item || typeof item !== 'object') {
641
+ console.warn(`validateChangeOps: skipping op[${i}] not an object`);
642
+ continue;
643
+ }
644
+ const op = item.op;
645
+ if (typeof op !== 'string') {
646
+ console.warn(`validateChangeOps: skipping op[${i}] — missing or non-string 'op' field`);
647
+ continue;
648
+ }
649
+ const requiredFields = CHANGE_OP_REQUIRED_FIELDS[op];
650
+ if (!requiredFields) {
651
+ console.warn(`validateChangeOps: skipping op[${i}] — unknown op type '${op}'`);
652
+ continue;
653
+ }
654
+ let missingField = false;
655
+ for (const field of requiredFields) {
656
+ if (typeof item[field] !== 'string') {
657
+ console.warn(`validateChangeOps: skipping op[${i}] (${op}) — missing or non-string '${field}' field`);
658
+ missingField = true;
659
+ break;
660
+ }
661
+ }
662
+ if (missingField)
663
+ continue;
664
+ // Extra validation for insert position
665
+ if (op === 'insert' && !VALID_INSERT_POSITIONS.has(item.position)) {
666
+ console.warn(`validateChangeOps: skipping op[${i}] (insert) — invalid position '${item.position}'`);
667
+ continue;
668
+ }
669
+ valid.push(item);
579
670
  }
580
- return $.html();
671
+ return valid;
581
672
  }
673
+ exports.validateChangeOps = validateChangeOps;
582
674
  /**
583
675
  * Parse a JSON change list from the model's raw response text.
584
676
  * Handles responses that may include markdown fences or extra text around the JSON.
677
+ * Invalid ops are logged as warnings and filtered out.
585
678
  */
586
679
  function parseChangeList(response) {
587
680
  // Try direct parse first
588
681
  try {
589
682
  const parsed = JSON.parse(response);
590
683
  if (Array.isArray(parsed))
591
- return parsed;
684
+ return validateChangeOps(parsed);
592
685
  }
593
686
  catch {
594
687
  // fall through to extraction
@@ -599,7 +692,7 @@ function parseChangeList(response) {
599
692
  try {
600
693
  const parsed = JSON.parse(match[0]);
601
694
  if (Array.isArray(parsed))
602
- return parsed;
695
+ return validateChangeOps(parsed);
603
696
  }
604
697
  catch {
605
698
  // fall through
@@ -611,43 +704,55 @@ exports.parseChangeList = parseChangeList;
611
704
  // ---------------------------------------------------------------------------
612
705
  // Prompt constants
613
706
  // ---------------------------------------------------------------------------
614
- function getMessageFormat(productName) {
615
- return `<MESSAGE_FORMAT>
616
- <div class="chat-message"><p><strong>{${productName}: | User:}</strong> {message contents}</p></div>
617
- `;
618
- }
619
- exports.getMessageFormat = getMessageFormat;
620
707
  function getTransformInstr(productName) {
621
708
  return `Apply the users <USER_MESSAGE> to the .viewerPanel of the <CURRENT_PAGE> by generating a list of changes in JSON format.
622
- 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.
709
+ 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.
710
+ 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.
623
711
 
624
- 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.
625
- If there's no <USER_MESSAGE> add a ${productName}: message to the chat with aasking the user what they would like to do.
626
- 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.
627
- 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.
628
- If a <USER_MESSAGE> is overly long, summarize the User: message.
712
+ 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.
629
713
 
630
- 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.
714
+ 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.
631
715
  You may add/remove new script blocks to the body but all script & style blocks should have a unique id.
632
716
  You may modify the contents of a data-locked script block but may not remove it.
633
717
 
634
718
  Every <CURRENT_PAGE> has hidden data-locked "thoughts" and "instructions" divs.
635
- 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.
719
+ 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.
636
720
  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.
637
721
  If the <USER_MESSAGE> indicates that a change didn't work, use your thoughts to diagnose the problem before fixing the issue.
722
+ 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.
723
+
724
+ 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.
725
+ Examples: data-notes on .slide elements (speaker notes), data-duration on .slide elements (slide timing), data-label on interactive elements (accessible names).
638
726
 
639
- 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.
640
727
  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().
641
- 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).
642
- 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.
643
- 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).
644
- 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.
728
+ 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).
729
+ The <THEME> section provides details on the current theme's color scheme to help you generate theme-aware pages.
730
+ 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).
731
+ 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.
645
732
 
646
- 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.
733
+ Visual Design Guide CRITICAL: Follow these rules exactly so pages look polished. Pages that ignore spacing rules look broken.
734
+ - SPACING TOKENS: --spacingS2: 4px, --spacingS1: 8px, --spacingM: 16px, --spacingL1: 20px, --spacingL2: 32px.
735
+ - 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.
736
+ - 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.
737
+ - 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.
738
+ - CARD/PANEL PADDING: Cards and panels MUST have at least padding: var(--spacingM) (16px) inside. Use var(--spacingL1) for larger content cards.
739
+ - TAB BARS (flm-pivot): Tab container should have padding: var(--spacingM) var(--spacingL1) — never --spacingS1 which makes tabs look squished.
740
+ - 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.
741
+ - 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.
742
+ - ELEVATION: var(--elevation4) for cards, var(--elevation8) for dropdowns, var(--elevation16) for modals.
743
+ - BORDER RADIUS: 4px on containers, 2px on badges/chips, 4px on buttons.
744
+ - 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).
745
+ - LAYOUT: Use flm-stack / flm-stack--horizontal with gap via spacing tokens. CSS grid with gap: var(--spacingM). Use max-width on wide layouts.
746
+ - VISUAL WEIGHT: var(--themePrimary) for interactive elements only, not large backgrounds. var(--neutralPrimary) for body text, var(--neutralSecondary) for secondary text.
747
+ - EMPTY STATES: Center the message vertically and horizontally with an icon. Add generous padding (at least var(--spacingL2)) around empty state messages.
647
748
 
648
- 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.
749
+ 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.
750
+ 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.
751
+ 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.
752
+ 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.
753
+
754
+ 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.
649
755
  If you're trying to assign an id to script or style block, use "replace" not "update".
650
- 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.
651
756
 
652
757
  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.`;
653
758
  }
@@ -655,19 +760,19 @@ exports.getTransformInstr = getTransformInstr;
655
760
  exports.AGENT_API_REFERENCE = `## Agent API
656
761
 
657
762
  Check availability first (required):
658
- const agents = await synthos.agents.list({ enabled: true });
763
+ const agents = await synthos.agent.list({ enabled: true });
659
764
 
660
765
  Send a message (returns full response):
661
- const result = await synthos.agents.send(agentId, message);
766
+ const result = await synthos.agent.send(agentId, message);
662
767
  // result: { kind: 'message', text: 'response text', raw: {...} }
663
768
 
664
769
  Send with file/image attachments:
665
- const result = await synthos.agents.send(agentId, message, [
770
+ const result = await synthos.agent.send(agentId, message, [
666
771
  { fileName: 'photo.jpg', mimeType: 'image/jpeg', content: '<base64-string>' }
667
772
  ]);
668
773
 
669
774
  Stream a response (token-by-token deltas):
670
- const handle = synthos.agents.sendStream(agentId, message, function(event) {
775
+ const handle = synthos.agent.sendStream(agentId, message, function(event) {
671
776
  switch (event.kind) {
672
777
  case 'text': // event.data = text delta string — append to output
673
778
  case 'status': // event.data = status info object
@@ -678,11 +783,11 @@ Stream a response (token-by-token deltas):
678
783
  handle.close(); // call to abort the stream early
679
784
 
680
785
  Stream with attachments:
681
- synthos.agents.sendStream(agentId, message, onEvent, [
786
+ synthos.agent.sendStream(agentId, message, onEvent, [
682
787
  { fileName: 'doc.pdf', mimeType: 'application/pdf', content: '<base64>' }
683
788
  ]);
684
789
 
685
- IMPORTANT: Always check synthos.agents.list({ enabled: true }) before calling an agent.
790
+ IMPORTANT: Always check synthos.agent.list({ enabled: true }) before calling an agent.
686
791
  If no agents are configured, show the user a link to Settings > Agents (/settings?tab=agents).`;
687
792
  // ---------------------------------------------------------------------------
688
793
  // Route hint blocks — keyed by feature group so they can be filtered
@@ -717,12 +822,19 @@ request: { prompt: string, shape: 'square' | 'portrait' | 'landscape', style: 'v
717
822
  response: { url: string }
718
823
 
719
824
  POST /api/generate/completion
720
- description: Generates a text completion based on a prompt
721
- request: { prompt: string, temperature?: number }
722
- response: { answer: string, explanation: string }
825
+ 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.
826
+ request: { prompt: string, temperature?: number, schema?: object (JSON schema) }
827
+ response (no schema): { answer: string }
828
+ response (with schema): the parsed JSON object matching \`schema\` (e.g. \`{ items: [...] }\`)
829
+
830
+ Schema notes:
831
+ - Use this when you need structured data — never tell the model "return JSON with these fields" via the prompt and parse \`answer\`.
832
+ - Top-level schema must be \`type: 'object'\`. To return a list, wrap it: \`{ type: 'object', additionalProperties: false, required: ['items'], properties: { items: { type: 'array', items: {...} } } }\`.
833
+ - Every nested \`type: 'object'\` MUST set \`additionalProperties: false\` — Anthropic structured-output rejects schemas that omit it.
834
+ - Mark every required field in \`required: [...]\` so the model is forced to emit them.
723
835
 
724
- synthos.generate.image({ prompt, shape, style }) — POST /api/generate/image
725
- synthos.generate.completion({ prompt, temperature? }) — POST /api/generate/completion`],
836
+ synthos.generate.image({ prompt, shape, style }) — POST /api/generate/image
837
+ synthos.generate.completion({ prompt, temperature?, schema? }) — POST /api/generate/completion`],
726
838
  ['pages', `GET /api/pages
727
839
  description: Retrieve a list of all pages with metadata
728
840
  response: Array of { name: string, title: string, categories: string[], pinned: boolean, createdDate: string, lastModified: string, pageVersion: number, mode: 'unlocked' | 'locked' }
@@ -745,17 +857,17 @@ description: Ask a question about a page with full HTML context
745
857
  request: { question: string }
746
858
  response: { answer: string }
747
859
 
748
- synthos.pages.list() — GET /api/pages
749
- synthos.pages.get(name) — GET /api/pages/:name
750
- synthos.pages.update(name, metadata) — POST /api/pages/:name
751
- synthos.pages.remove(name) — DELETE /api/pages/:name
752
- synthos.pages.ask(name, question) — POST /api/pages/:name/ask`],
860
+ synthos.page.list() — GET /api/pages
861
+ synthos.page.get(name) — GET /api/pages/:name
862
+ synthos.page.update(name, metadata) — POST /api/pages/:name
863
+ synthos.page.remove(name) — DELETE /api/pages/:name
864
+ synthos.page.ask(name, question) — POST /api/pages/:name/ask`],
753
865
  ['scripts', `POST /api/scripts/:id
754
866
  description: Execute a script with the passed in variables
755
867
  request: { [key: string]: string }
756
868
  response: string
757
869
 
758
- synthos.scripts.run(id, variables) — POST /api/scripts/:id`],
870
+ synthos.script.run(id, variables) — POST /api/scripts/:id`],
759
871
  ['search', `POST /api/search/web
760
872
  description: Search the web using Brave Search (must be enabled in Settings > Connectors)
761
873
  request: { query: string, count?: number, country?: string, freshness?: string }
@@ -776,11 +888,11 @@ description: Send a message and receive a streaming SSE response (text/event-str
776
888
  request: { message: string, attachments?: [{ fileName: string, mimeType: string, content: string }] }
777
889
  response: SSE stream
778
890
 
779
- synthos.agents.list(opts?) — GET /api/agents (returns configured agents; opts: { enabled?, provider? }; returns [{ id, name, description, url, enabled, provider, capabilities }])
780
- 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 }])
781
- 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 }])
782
- synthos.agents.isEnabled(agentId) — checks if an agent is enabled (returns Promise<boolean>)
783
- synthos.agents.getCapabilities(agentId) — returns agent capabilities object (streaming, skills, etc.)`],
891
+ synthos.agent.list(opts?) — GET /api/agents (returns configured agents; opts: { enabled?, provider? }; returns [{ id, name, description, url, enabled, provider, capabilities }])
892
+ 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 }])
893
+ 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 }])
894
+ synthos.agent.isEnabled(agentId) — checks if an agent is enabled (returns Promise<boolean>)
895
+ synthos.agent.getCapabilities(agentId) — returns agent capabilities object (streaming, skills, etc.)`],
784
896
  ['connectors', `GET /api/connectors
785
897
  description: List available connectors (REST API proxies). Supports ?category=X and ?id=X filters.
786
898
  response: [{ id: string, name: string, category: string, configured: boolean }]
@@ -794,8 +906,8 @@ description: Proxy a request through a configured connector. The connector attac
794
906
  request: { connector: string, method: string, path: string, headers?: object, body?: any, query?: object }
795
907
  response: Upstream API response (JSON or text)
796
908
 
797
- synthos.connectors.call(connector, method, path, opts?) — POST /api/connectors (proxy call; opts: { headers?, body?, query? })
798
- synthos.connectors.list(opts?) — GET /api/connectors (opts: { category?, id? })`],
909
+ synthos.connector.call(connector, method, path, opts?) — POST /api/connectors (proxy call; opts: { headers?, body?, query? })
910
+ synthos.connector.list(opts?) — GET /api/connectors (opts: { category?, id? })`],
799
911
  ['files', `GET /api/files/:page
800
912
  description: List files stored for a page (with sizes)
801
913
  response: { files: [{ name: string, size: number }] }
@@ -910,9 +1022,10 @@ request: { prompt: string, shape: 'square' | 'portrait' | 'landscape', style: 'v
910
1022
  response: { url: string }
911
1023
 
912
1024
  POST /api/generate/completion
913
- description: Generates a text completion based on a prompt
914
- request: { prompt: string, temperature?: number }
915
- response: { answer: string, explanation: string }
1025
+ 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.
1026
+ request: { prompt: string, temperature?: number, schema?: object (JSON schema) }
1027
+ response (no schema): { answer: string }
1028
+ response (with schema): the parsed JSON object matching \`schema\`. Top-level must be \`type: 'object'\`; every nested object MUST set \`additionalProperties: false\`.
916
1029
 
917
1030
  GET /api/pages
918
1031
  description: Retrieve a list of all pages with metadata
@@ -978,21 +1091,21 @@ PAGE HELPERS (available globally as window.synthos):
978
1091
  synthos.data.get(table, id) — GET /api/data/:page/:table/:id (auto-scoped to current page)
979
1092
  synthos.data.save(table, row) — POST /api/data/:page/:table (auto-scoped to current page)
980
1093
  synthos.data.remove(table, id) — DELETE /api/data/:page/:table/:id (auto-scoped to current page)
981
- synthos.generate.image({ prompt, shape, style }) — POST /api/generate/image
982
- synthos.generate.completion({ prompt, temperature? }) — POST /api/generate/completion
983
- synthos.scripts.run(id, variables) — POST /api/scripts/:id
984
- synthos.pages.list() — GET /api/pages
985
- synthos.pages.get(name) — GET /api/pages/:name
986
- synthos.pages.update(name, metadata) — POST /api/pages/:name
987
- synthos.pages.remove(name) — DELETE /api/pages/:name
988
- synthos.pages.ask(name, question) — POST /api/pages/:name/ask
1094
+ synthos.generate.image({ prompt, shape, style }) — POST /api/generate/image
1095
+ synthos.generate.completion({ prompt, temperature?, schema? }) — POST /api/generate/completion (schema: optional JSON schema for structured output; returns parsed object)
1096
+ synthos.script.run(id, variables) — POST /api/scripts/:id
1097
+ synthos.page.list() — GET /api/pages
1098
+ synthos.page.get(name) — GET /api/pages/:name
1099
+ synthos.page.update(name, metadata) — POST /api/pages/:name
1100
+ synthos.page.remove(name) — DELETE /api/pages/:name
1101
+ synthos.page.ask(name, question) — POST /api/pages/:name/ask
989
1102
  synthos.search.web(query, opts?) — POST /api/search/web (opts: { count?, country?, freshness? })
990
- synthos.connectors.call(connector, method, path, opts?) — POST /api/connectors (proxy call; opts: { headers?, body?, query? })
991
- synthos.connectors.list(opts?) — GET /api/connectors (opts: { category?, id? })
992
- synthos.agents.list(opts?) — GET /api/agents (returns configured agents; opts: { enabled?, provider? }; returns [{ id, name, description, url, enabled, provider, capabilities }])
993
- 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 }])
994
- 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 }])
995
- synthos.agents.isEnabled(agentId) — checks if an agent is enabled (returns Promise<boolean>)
996
- synthos.agents.getCapabilities(agentId) — returns agent capabilities object (streaming, skills, etc.)
1103
+ synthos.connector.call(connector, method, path, opts?) — POST /api/connectors (proxy call; opts: { headers?, body?, query? })
1104
+ synthos.connector.list(opts?) — GET /api/connectors (opts: { category?, id? })
1105
+ synthos.agent.list(opts?) — GET /api/agents (returns configured agents; opts: { enabled?, provider? }; returns [{ id, name, description, url, enabled, provider, capabilities }])
1106
+ 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 }])
1107
+ 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 }])
1108
+ synthos.agent.isEnabled(agentId) — checks if an agent is enabled (returns Promise<boolean>)
1109
+ synthos.agent.getCapabilities(agentId) — returns agent capabilities object (streaming, skills, etc.)
997
1110
  All methods return Promises. Prefer these helpers over raw fetch().`;
998
1111
  //# sourceMappingURL=transformPage.js.map