synthos 0.6.0 → 0.7.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 (149) hide show
  1. package/README.md +33 -1
  2. package/default-pages/app_builder.html +40 -0
  3. package/default-pages/app_builder.json +1 -0
  4. package/default-pages/json_tools.html +89 -159
  5. package/default-pages/json_tools.json +1 -0
  6. package/default-pages/my_notes.html +33 -0
  7. package/default-pages/my_notes.json +12 -0
  8. package/default-pages/neon_asteroids.html +77 -0
  9. package/default-pages/neon_asteroids.json +12 -0
  10. package/default-pages/sidebar_builder.html +49 -0
  11. package/default-pages/sidebar_builder.json +1 -0
  12. package/default-pages/solar_explorer.html +1956 -0
  13. package/default-pages/solar_explorer.json +12 -0
  14. package/default-pages/solar_tutorial.html +476 -0
  15. package/default-pages/solar_tutorial.json +1 -0
  16. package/default-pages/two-panel_builder.html +66 -0
  17. package/default-pages/two-panel_builder.json +1 -0
  18. package/dist/connectors/index.d.ts +3 -0
  19. package/dist/connectors/index.d.ts.map +1 -0
  20. package/dist/connectors/index.js +6 -0
  21. package/dist/connectors/index.js.map +1 -0
  22. package/dist/connectors/registry.d.ts +3 -0
  23. package/dist/connectors/registry.d.ts.map +1 -0
  24. package/dist/connectors/registry.js +100 -0
  25. package/dist/connectors/registry.js.map +1 -0
  26. package/dist/connectors/types.d.ts +61 -0
  27. package/dist/connectors/types.d.ts.map +1 -0
  28. package/dist/connectors/types.js +3 -0
  29. package/dist/connectors/types.js.map +1 -0
  30. package/dist/files.d.ts +2 -0
  31. package/dist/files.d.ts.map +1 -1
  32. package/dist/files.js +12 -1
  33. package/dist/files.js.map +1 -1
  34. package/dist/init.d.ts +8 -1
  35. package/dist/init.d.ts.map +1 -1
  36. package/dist/init.js +155 -3
  37. package/dist/init.js.map +1 -1
  38. package/dist/migrations.d.ts +11 -0
  39. package/dist/migrations.d.ts.map +1 -0
  40. package/dist/migrations.js +281 -0
  41. package/dist/migrations.js.map +1 -0
  42. package/dist/models/index.d.ts +3 -0
  43. package/dist/models/index.d.ts.map +1 -0
  44. package/dist/models/index.js +10 -0
  45. package/dist/models/index.js.map +1 -0
  46. package/dist/models/providers.d.ts +7 -0
  47. package/dist/models/providers.d.ts.map +1 -0
  48. package/dist/models/providers.js +33 -0
  49. package/dist/models/providers.js.map +1 -0
  50. package/dist/models/types.d.ts +21 -0
  51. package/dist/models/types.d.ts.map +1 -0
  52. package/dist/models/types.js +3 -0
  53. package/dist/models/types.js.map +1 -0
  54. package/dist/pages.d.ts +21 -2
  55. package/dist/pages.d.ts.map +1 -1
  56. package/dist/pages.js +202 -23
  57. package/dist/pages.js.map +1 -1
  58. package/dist/scripts.js +2 -2
  59. package/dist/scripts.js.map +1 -1
  60. package/dist/service/createCompletePrompt.d.ts +3 -2
  61. package/dist/service/createCompletePrompt.d.ts.map +1 -1
  62. package/dist/service/createCompletePrompt.js +11 -16
  63. package/dist/service/createCompletePrompt.js.map +1 -1
  64. package/dist/service/debugLog.d.ts +11 -0
  65. package/dist/service/debugLog.d.ts.map +1 -0
  66. package/dist/service/debugLog.js +26 -0
  67. package/dist/service/debugLog.js.map +1 -0
  68. package/dist/service/modelInstructions.d.ts +7 -0
  69. package/dist/service/modelInstructions.d.ts.map +1 -0
  70. package/dist/service/modelInstructions.js +16 -0
  71. package/dist/service/modelInstructions.js.map +1 -0
  72. package/dist/service/requiresSettings.d.ts +2 -2
  73. package/dist/service/requiresSettings.d.ts.map +1 -1
  74. package/dist/service/requiresSettings.js.map +1 -1
  75. package/dist/service/server.d.ts.map +1 -1
  76. package/dist/service/server.js +15 -0
  77. package/dist/service/server.js.map +1 -1
  78. package/dist/service/transformPage.d.ts +81 -2
  79. package/dist/service/transformPage.d.ts.map +1 -1
  80. package/dist/service/transformPage.js +672 -82
  81. package/dist/service/transformPage.js.map +1 -1
  82. package/dist/service/useApiRoutes.d.ts.map +1 -1
  83. package/dist/service/useApiRoutes.js +579 -13
  84. package/dist/service/useApiRoutes.js.map +1 -1
  85. package/dist/service/useConnectorRoutes.d.ts +4 -0
  86. package/dist/service/useConnectorRoutes.d.ts.map +1 -0
  87. package/dist/service/useConnectorRoutes.js +389 -0
  88. package/dist/service/useConnectorRoutes.js.map +1 -0
  89. package/dist/service/useDataRoutes.d.ts.map +1 -1
  90. package/dist/service/useDataRoutes.js +83 -70
  91. package/dist/service/useDataRoutes.js.map +1 -1
  92. package/dist/service/usePageRoutes.d.ts.map +1 -1
  93. package/dist/service/usePageRoutes.js +243 -38
  94. package/dist/service/usePageRoutes.js.map +1 -1
  95. package/dist/settings.d.ts +33 -4
  96. package/dist/settings.d.ts.map +1 -1
  97. package/dist/settings.js +108 -15
  98. package/dist/settings.js.map +1 -1
  99. package/dist/synthos-cli.d.ts.map +1 -1
  100. package/dist/synthos-cli.js +11 -1
  101. package/dist/synthos-cli.js.map +1 -1
  102. package/dist/themes.d.ts +9 -0
  103. package/dist/themes.d.ts.map +1 -0
  104. package/dist/themes.js +64 -0
  105. package/dist/themes.js.map +1 -0
  106. package/package.json +5 -3
  107. package/required-pages/builder.html +74 -0
  108. package/required-pages/builder.json +1 -0
  109. package/required-pages/pages.html +169 -126
  110. package/required-pages/pages.json +1 -0
  111. package/required-pages/settings.html +812 -156
  112. package/required-pages/settings.json +1 -0
  113. package/required-pages/synthos_apis.html +272 -0
  114. package/required-pages/synthos_apis.json +1 -0
  115. package/required-pages/synthos_scripts.html +87 -0
  116. package/required-pages/synthos_scripts.json +1 -0
  117. package/src/connectors/index.ts +12 -0
  118. package/src/connectors/registry.ts +98 -0
  119. package/src/connectors/types.ts +68 -0
  120. package/src/files.ts +11 -0
  121. package/src/init.ts +151 -5
  122. package/src/migrations.ts +266 -0
  123. package/src/models/index.ts +2 -0
  124. package/src/models/providers.ts +33 -0
  125. package/src/models/types.ts +23 -0
  126. package/src/pages.ts +234 -26
  127. package/src/scripts.ts +2 -2
  128. package/src/service/createCompletePrompt.ts +14 -18
  129. package/src/service/debugLog.ts +17 -0
  130. package/src/service/modelInstructions.ts +14 -0
  131. package/src/service/requiresSettings.ts +3 -3
  132. package/src/service/server.ts +19 -2
  133. package/src/service/transformPage.ts +709 -88
  134. package/src/service/useApiRoutes.ts +632 -16
  135. package/src/service/useConnectorRoutes.ts +427 -0
  136. package/src/service/useDataRoutes.ts +87 -71
  137. package/src/service/usePageRoutes.ts +237 -44
  138. package/src/settings.ts +143 -20
  139. package/src/synthos-cli.ts +11 -1
  140. package/src/themes.ts +71 -0
  141. package/default-pages/[application].html +0 -95
  142. package/default-pages/[markdown].html +0 -271
  143. package/default-pages/[sidebar].html +0 -114
  144. package/default-pages/[split-application].html +0 -118
  145. package/default-pages/solar_system.html +0 -432
  146. package/default-pages/space_invaders.html +0 -617
  147. package/required-pages/apis.html +0 -362
  148. package/required-pages/home.html +0 -126
  149. package/required-pages/scripts.html +0 -350
@@ -1,5 +1,12 @@
1
- import { AgentArgs, AgentCompletion, generateObject, JsonSchema, SystemMessage, UserMessage } from "agentm-core";
1
+ import { AgentArgs, AgentCompletion, SystemMessage, UserMessage } from "agentm-core";
2
2
  import { listScripts } from "../scripts";
3
+ import * as cheerio from "cheerio";
4
+ import { ThemeInfo } from "../themes";
5
+ import { CONNECTOR_REGISTRY, ConnectorsConfig, ConnectorOAuthConfig } from "../connectors";
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Types
9
+ // ---------------------------------------------------------------------------
3
10
 
4
11
  export interface TransformPageArgs extends AgentArgs {
5
12
  pagesFolder: string;
@@ -7,112 +14,661 @@ export interface TransformPageArgs extends AgentArgs {
7
14
  message: string;
8
15
  maxTokens: number;
9
16
  instructions?: string;
17
+ /** Provider-specific formatting instructions injected into the prompt. */
18
+ modelInstructions?: string;
19
+ /** Active theme metadata for theme-aware page generation. */
20
+ themeInfo?: ThemeInfo;
21
+ /** Page mode. */
22
+ mode?: 'unlocked' | 'locked';
23
+ /** User's configured connectors (from settings). */
24
+ configuredConnectors?: ConnectorsConfig;
25
+ }
26
+
27
+ export type ChangeOp =
28
+ | { op: "update"; nodeId: string; html: string }
29
+ | { op: "replace"; nodeId: string; html: string }
30
+ | { op: "delete"; nodeId: string }
31
+ | { op: "insert"; parentId: string; position: "prepend" | "append" | "before" | "after"; html: string }
32
+ | { op: "style-element"; nodeId: string; style: string };
33
+
34
+ export type ChangeList = ChangeOp[];
35
+
36
+ interface FailedOp {
37
+ op: ChangeOp;
38
+ reason: string;
10
39
  }
11
40
 
12
- export async function transformPage(args: TransformPageArgs): Promise<AgentCompletion<string>> {
13
- // Get list of registered scripts
14
- const scripts = await listScripts(args.pagesFolder);
15
- const serverScripts = scripts.length > 0 ? `<SERVER_SCRIPTS>\n${scripts}\n\n` : '';
16
-
17
- // Define system message
18
- const { pageState, message, maxTokens, completePrompt } = args;
19
- const system: SystemMessage = {
20
- role: 'system',
21
- content: `<CURRENT_PAGE>\n${pageState}\n\n<SERVER_APIS>\n${serverAPIs}\n\n${serverScripts}<USER_MESSAGE>\n${message}`
22
- };
23
-
24
- // Create prompt
25
- const instructions = args.instructions ? `\n\n<INSTRUCTIONS>\n${args.instructions}` : '';
26
- const prompt: UserMessage = {
27
- role: 'user',
28
- content: `${goal}${instructions}`
29
- };
30
-
31
- // Complete prompt
32
- const result = await completePrompt({ prompt, system, maxTokens });
33
- if (result.completed) {
34
- // Find html content
35
- let start = result.value.indexOf(`<!DOCTYPE`);
36
- start = start >= 0 ? start : result.value.indexOf('<html');
37
- const end = result.value.lastIndexOf('</html>');
38
- if (start >= 0 && end >= start) {
39
- const value = result.value.substring(start, end + 7);
40
- return { completed: true, value };
41
- } else {
42
- return { completed: false, error: new Error('Failed to find html content') };
41
+ interface ApplyResult {
42
+ html: string;
43
+ failedOps: FailedOp[];
44
+ }
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Public entry point
48
+ // ---------------------------------------------------------------------------
49
+
50
+ export interface TransformPageResult {
51
+ html: string;
52
+ changeCount: number;
53
+ }
54
+
55
+ export async function transformPage(args: TransformPageArgs): Promise<AgentCompletion<TransformPageResult>> {
56
+ const { pagesFolder, pageState, message, maxTokens, completePrompt } = args;
57
+
58
+ // 1. Assign data-node-id to every element
59
+ const { html: annotatedHtml } = assignNodeIds(pageState);
60
+
61
+ try {
62
+ // 2. Build prompt
63
+ const scripts = await listScripts(pagesFolder);
64
+ const serverScripts = `<SERVER_SCRIPTS>\n${scripts || ''}`;
65
+ const currentPage = `<CURRENT_PAGE>\n${annotatedHtml}`;
66
+
67
+ // Build theme context block
68
+ let themeBlock = '<THEME>\n';
69
+ if (args.themeInfo) {
70
+ const { mode, colors } = args.themeInfo;
71
+ const colorList = Object.entries(colors)
72
+ .map(([name, value]) => ` --${name}: ${value}`)
73
+ .join('\n');
74
+ themeBlock += `Mode: ${mode}\nCSS custom properties (use instead of hardcoded values):\n${colorList}\n\nShared shell classes (pre-styled by theme, do not redefine):\n .chat-panel — Left sidebar container (30% width)\n .chat-header — Chat panel title bar\n .chat-messages — Scrollable message container\n .chat-message — Individual message wrapper\n .link-group — Navigation links row (Save, Pages, Reset)\n .chat-input — Message text input\n .chat-submit — Send button\n .viewer-panel — Right content area (70% width)\n .loading-overlay — Full-screen loading overlay\n .spinner — Animated loading spinner\n\nPage title bars: To align with the chat header, apply these styles:\n min-height: var(--header-min-height);\n padding: var(--header-padding-vertical) var(--header-padding-horizontal);\n line-height: var(--header-line-height);\n display: flex; align-items: center; justify-content: center; box-sizing: border-box;\n\nFull-viewer mode: For games, animations, or full-screen content, add class "full-viewer" to the viewer-panel element to remove its padding.\n\nChat panel behaviours (auto-injected via page script — do NOT recreate in page code):\n The server injects page-v2.js after transformation. It provides:\n - Form submit handler: sets action to window.location.pathname, shows #loadingOverlay, disables inputs\n - Save/Reset link handlers (#saveLink, #resetLink)\n - Chat scroll to bottom (#chatMessages)\n - Chat toggle button (.chat-toggle) — created dynamically if not in markup\n - .chat-input-wrapper — wraps #chatInput with a brainstorm icon button\n - Brainstorm modal (#brainstormModal) — LLM-powered brainstorm UI, created dynamically\n - Focus management — keeps keyboard input directed to #chatInput\n\n Do NOT:\n - Create your own form submit handler, toggle button, or input wrapper\n - Modify or replace .chat-panel, .chat-header, .link-group, #chatForm, or .chat-toggle\n - INSERT new <script> blocks that duplicate existing ones — when fixing JavaScript, UPDATE or REPLACE the existing script's nodeId instead. Always give inline scripts a unique id attribute.\n - Set the form action attribute (page-v2.js sets it dynamically)\n - Include these CSS rules (in the theme): #loadingOverlay position, .chat-submit:disabled, .chat-input:disabled\n\n To add chat messages: use insert with parentId of #chatMessages and position "append".\n #chatMessages is the only unlocked element inside .chat-panel.\n\nThe <html> element has class "${mode}-mode". Always add .light-mode CSS overrides for any page-specific styles so the page works in both light and dark themes, unless the user has explicitly requested a very specific color scheme.`;
43
75
  }
44
- } else {
45
- return { completed: false, error: result.error };
76
+
77
+ // Build configured-connectors block
78
+ let connectorsBlock = '';
79
+ if (args.configuredConnectors) {
80
+ const entries = Object.entries(args.configuredConnectors)
81
+ .filter(([, cfg]) => cfg.enabled && cfg.apiKey);
82
+ if (entries.length > 0) {
83
+ const blocks = entries.map(([id, cfg]) => {
84
+ const def = CONNECTOR_REGISTRY.find(d => d.id === id);
85
+ if (!def) return `- ${id}`;
86
+ let block = `- ${def.name} (id: "${id}", category: ${def.category})\n Base URL: ${def.baseUrl}`;
87
+ if (def.hints) {
88
+ block += `\n Usage:\n${def.hints.split('\n').map(l => ' ' + l).join('\n')}`;
89
+ }
90
+ // Append dynamic OAuth context
91
+ if (def.authStrategy === 'oauth2') {
92
+ const oauthCfg = cfg as ConnectorOAuthConfig;
93
+ block += '\n Auth: The proxy attaches the access token automatically. Do NOT pass access_token in body or query params.';
94
+ if (oauthCfg.userId) {
95
+ block += `\n User ID: ${oauthCfg.userId} — use this directly in API paths (e.g. /${oauthCfg.userId}/media).`;
96
+ } else {
97
+ block += '\n User ID: Not yet resolved. Call GET /me/accounts to discover it, then GET /{page-id}?fields=instagram_business_account to get the IG user ID.';
98
+ }
99
+ }
100
+ return block;
101
+ });
102
+ connectorsBlock = `<CONFIGURED_CONNECTORS>\nThe user has configured and enabled these connectors:\n${blocks.join('\n\n')}\n\nYou may use synthos.connectors.call(connector, method, path, opts) to call them.\nIMPORTANT: Before making any connector call, ALWAYS check that the connector is configured first using synthos.connectors.list(). If the connector is not configured, show the user a friendly message with a link to the Settings > Connectors page (/settings?tab=connectors) so they can set it up.\nDo NOT hardcode API keys. The connector proxy attaches authentication automatically.`;
103
+ }
104
+ }
105
+
106
+ const systemMessage = [currentPage, serverAPIs, serverScripts, connectorsBlock, themeBlock, messageFormat].filter(s => s).join('\n\n');
107
+ const system: SystemMessage = {
108
+ role: 'system',
109
+ content: systemMessage
110
+ };
111
+
112
+ const userInstr = args.instructions || '';
113
+ const modelInstr = args.modelInstructions || '';
114
+ const instructions = [userInstr, modelInstr, transformInstr].filter(s => s.trim() !== '').join('\n');
115
+ const prompt: UserMessage = {
116
+ role: 'user',
117
+ content: `<USER_MESSAGE>\n${message}\n\n<INSTRUCTIONS>\n${instructions}`
118
+ };
119
+
120
+ // 3. Call model
121
+ const result = await completePrompt({ prompt, system, maxTokens });
122
+ if (!result.completed) {
123
+ return { completed: false, error: result.error };
124
+ }
125
+
126
+ // 4. Parse JSON change list from response
127
+ const changes = parseChangeList(result.value);
128
+
129
+ // 5. Apply changes (first pass — with failure reporting)
130
+ const firstPass = applyChangeListWithReport(annotatedHtml, changes);
131
+ let finalHtml = firstPass.html;
132
+ let successCount = changes.length - firstPass.failedOps.length;
133
+
134
+ // 6. Repair pass — if any ops failed, make one follow-up LLM call
135
+ if (firstPass.failedOps.length > 0) {
136
+ console.warn(`transformPage: ${firstPass.failedOps.length} op(s) failed — attempting repair pass`);
137
+ try {
138
+ // Re-assign fresh node IDs on the partially-updated HTML
139
+ const { html: reAnnotatedHtml } = assignNodeIds(stripNodeIds(firstPass.html));
140
+
141
+ // Build compact repair prompt
142
+ const failedSummary = firstPass.failedOps
143
+ .map((f, i) => `${i + 1}. op="${f.op.op}" — ${f.reason}\n original: ${JSON.stringify(f.op)}`)
144
+ .join('\n');
145
+
146
+ const repairSystem: SystemMessage = {
147
+ role: 'system',
148
+ content: `<CURRENT_PAGE>\n${reAnnotatedHtml}\n\n<FAILED_OPERATIONS>\n${failedSummary}`
149
+ };
150
+
151
+ const repairPrompt: UserMessage = {
152
+ role: 'user',
153
+ content: repairUSER_MESSAGE
154
+ };
155
+
156
+ const repairMaxTokens = Math.min(maxTokens, 4096);
157
+ const repairResult = await completePrompt({ prompt: repairPrompt, system: repairSystem, maxTokens: repairMaxTokens });
158
+
159
+ if (repairResult.completed) {
160
+ const repairChanges = parseChangeList(repairResult.value);
161
+ if (repairChanges.length > 0) {
162
+ const repairPass = applyChangeListWithReport(reAnnotatedHtml, repairChanges);
163
+ const repairSuccessCount = repairChanges.length - repairPass.failedOps.length;
164
+ if (repairPass.failedOps.length > 0) {
165
+ console.warn(`transformPage: repair pass had ${repairPass.failedOps.length} remaining failure(s) — keeping partial result`);
166
+ }
167
+ finalHtml = repairPass.html;
168
+ successCount += repairSuccessCount;
169
+ console.log(`transformPage: repair pass applied ${repairSuccessCount} fix(es)`);
170
+ } else {
171
+ console.log('transformPage: repair pass returned no changes (model deemed repairs unnecessary)');
172
+ }
173
+ } else {
174
+ console.warn('transformPage: repair LLM call failed — keeping partial result from first pass');
175
+ }
176
+ } catch (repairErr: unknown) {
177
+ const msg = repairErr instanceof Error ? repairErr.message : String(repairErr);
178
+ console.warn(`transformPage: repair pass error — ${msg} — keeping partial result from first pass`);
179
+ }
180
+ }
181
+
182
+ // 7. Strip data-node-id attributes
183
+ const cleanHtml = stripNodeIds(finalHtml);
184
+
185
+ // 8. Remove duplicate inline scripts (LLM may insert instead of update)
186
+ const dedupedHtml = deduplicateInlineScripts(cleanHtml);
187
+
188
+ // 9. Ensure page-helpers and page-script are last in <body>
189
+ const safeHtml = ensureScriptsBeforeBodyClose(dedupedHtml);
190
+
191
+ return { completed: true, value: { html: safeHtml, changeCount: successCount } };
192
+ } catch (err: unknown) {
193
+ // On any error: return original page with error block injected
194
+ const cleanOriginal = stripNodeIds(annotatedHtml);
195
+ const errorMessage = err instanceof Error ? err.message : String(err);
196
+ const errorHtml = injectError(cleanOriginal, 'Something went wrong try again', errorMessage);
197
+ return { completed: true, value: { html: errorHtml, changeCount: 0 } };
46
198
  }
47
199
  }
48
200
 
49
- export async function transformPageAsObject(args: TransformPageArgs): Promise<AgentCompletion<string>> {
50
- // Get list of registered scripts
51
- const scripts = await listScripts(args.pagesFolder);
52
- const serverScripts = scripts.length > 0 ? `<SERVER_SCRIPTS>\n${scripts}\n\n` : '';
201
+ // ---------------------------------------------------------------------------
202
+ // Internal helpers
203
+ // ---------------------------------------------------------------------------
53
204
 
54
- // Provide additional context
55
- const { pageState, message, maxTokens, instructions, completePrompt, shouldContinue } = args;
56
- const context = `<CURRENT_PAGE>\n${pageState}\n\n<SERVER_APIS>\n${serverAPIs}\n\n${serverScripts}<USER_MESSAGE>\n${message}`;
205
+ /**
206
+ * Assign sequential `data-node-id` to every element in the HTML.
207
+ */
208
+ export function assignNodeIds(html: string): { html: string; nodeCount: number } {
209
+ const $ = cheerio.load(html, { decodeEntities: false });
57
210
 
58
- // Generate next page
59
- const result = await generateObject<HtmlPage>({ goal, jsonSchema, maxTokens, context, instructions, completePrompt, shouldContinue });
60
- if (result.completed) {
61
- return { completed: true, value: result.value?.content! };
62
- } else {
63
- return { completed: false, error: result.error };
211
+ let counter = 0;
212
+ $('*').each(function (this: cheerio.Element) {
213
+ const el = $(this);
214
+ if (this.type === 'tag' || this.type === 'script' || this.type === 'style') {
215
+ el.attr('data-node-id', String(counter++));
216
+ }
217
+ });
218
+ return { html: $.html(), nodeCount: counter };
219
+ }
220
+
221
+ /**
222
+ * Remove all `data-node-id` attributes from the HTML.
223
+ */
224
+ export function stripNodeIds(html: string): string {
225
+ const $ = cheerio.load(html, { decodeEntities: false });
226
+ $('[data-node-id]').removeAttr('data-node-id');
227
+ return $.html();
228
+ }
229
+
230
+ /**
231
+ * Remove duplicate inline `<script>` blocks using a two-pass approach.
232
+ *
233
+ * **Pass 1 — ID-based dedup (deterministic):**
234
+ * Groups inline scripts by their `id` attribute (skipping system IDs:
235
+ * page-info, page-helpers, page-script, error and scripts with `src`).
236
+ * If any group has 2+ scripts with the same id, all but the **last** are removed.
237
+ *
238
+ * **Pass 2 — Declaration-overlap dedup (heuristic fallback):**
239
+ * For scripts with no `id`, no `src`, and no `type="application/json"`,
240
+ * compares top-level declaration names. When overlap >= 60% of the smaller
241
+ * set (minimum 2 declarations each), the **first** script is removed.
242
+ */
243
+ export function deduplicateInlineScripts(html: string): string {
244
+ const $ = cheerio.load(html, { decodeEntities: false });
245
+
246
+ const SYSTEM_IDS = new Set(['page-info', 'page-helpers', 'page-script', 'error']);
247
+
248
+ // ── Pass 1: ID-based dedup ──────────────────────────────────────────
249
+ const idGroups = new Map<string, cheerio.Cheerio[]>();
250
+ $('script').each(function (_, rawEl) {
251
+ const el = $(rawEl);
252
+ if (el.attr('src')) return;
253
+ const id = el.attr('id');
254
+ if (!id || SYSTEM_IDS.has(id)) return;
255
+
256
+ if (!idGroups.has(id)) {
257
+ idGroups.set(id, []);
258
+ }
259
+ idGroups.get(id)!.push(el);
260
+ });
261
+
262
+ for (const [id, group] of idGroups) {
263
+ if (group.length < 2) continue;
264
+ for (let i = 0; i < group.length - 1; i++) {
265
+ console.log(`deduplicateInlineScripts: removing duplicate script id="${id}" (keeping last of ${group.length})`);
266
+ group[i].remove();
267
+ }
268
+ }
269
+
270
+ // ── Pass 2: Declaration-overlap dedup (fallback for id-less scripts) ─
271
+ interface ScriptInfo {
272
+ el: cheerio.Cheerio;
273
+ declarations: Set<string>;
274
+ }
275
+
276
+ const declPattern = /(?:^|;|\n)\s*(?:let|const|var|function|class)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g;
277
+
278
+ const scripts: ScriptInfo[] = [];
279
+ $('script').each(function (_, rawEl) {
280
+ const el = $(rawEl);
281
+ if (el.attr('src')) return;
282
+ if (el.attr('id')) return;
283
+ if ((el.attr('type') ?? '').toLowerCase() === 'application/json') return;
284
+
285
+ const code = (el.html() ?? '').trim();
286
+ if (!code) return;
287
+
288
+ const declarations = new Set<string>();
289
+ let m: RegExpExecArray | null;
290
+ declPattern.lastIndex = 0;
291
+ while ((m = declPattern.exec(code)) !== null) {
292
+ declarations.add(m[1]);
293
+ }
294
+
295
+ scripts.push({ el, declarations });
296
+ });
297
+
298
+ // Compare each pair; mark earlier script for removal when overlap is high
299
+ const toRemove = new Set<number>();
300
+ for (let i = 0; i < scripts.length; i++) {
301
+ if (toRemove.has(i)) continue;
302
+ for (let j = i + 1; j < scripts.length; j++) {
303
+ if (toRemove.has(j)) continue;
304
+
305
+ const a = scripts[i].declarations;
306
+ const b = scripts[j].declarations;
307
+
308
+ // Both must have at least 2 declarations
309
+ if (a.size < 2 || b.size < 2) continue;
310
+
311
+ // Count overlap
312
+ let overlap = 0;
313
+ for (const name of a) {
314
+ if (b.has(name)) overlap++;
315
+ }
316
+
317
+ const smallerSize = Math.min(a.size, b.size);
318
+ if (overlap / smallerSize >= 0.6) {
319
+ // Remove the first (older) script, keep the last (LLM fix)
320
+ console.log(`deduplicateInlineScripts: removing duplicate script (${overlap}/${smallerSize} declaration overlap)`);
321
+ toRemove.add(i);
322
+ break; // script i is already marked, move on
323
+ }
324
+ }
64
325
  }
326
+
327
+ // Remove marked scripts
328
+ for (const idx of toRemove) {
329
+ scripts[idx].el.remove();
330
+ }
331
+
332
+ return $.html();
65
333
  }
66
334
 
67
- // Define output shape
68
- interface HtmlPage {
69
- content: string;
335
+ /**
336
+ * Ensure `#page-helpers` and `#page-script` are the last children of <body>,
337
+ * in that order. The LLM may move them during transformation; this guarantees
338
+ * they always execute after the DOM is fully parsed.
339
+ */
340
+ export function ensureScriptsBeforeBodyClose(html: string): string {
341
+ const $ = cheerio.load(html, { decodeEntities: false });
342
+ const body = $('body');
343
+ if (body.length === 0) return html;
344
+
345
+ // Capture outer HTML before removing so we can re-append
346
+ const helpers = $('script#page-helpers');
347
+ const pageScript = $('script#page-script');
348
+
349
+ const helpersHtml = helpers.length > 0 ? $.html(helpers) : '';
350
+ const pageScriptHtml = pageScript.length > 0 ? $.html(pageScript) : '';
351
+
352
+ // Remove from current position and re-append at end of <body>
353
+ if (helpers.length > 0) helpers.remove();
354
+ if (pageScript.length > 0) pageScript.remove();
355
+ if (helpersHtml) body.append(helpersHtml);
356
+ if (pageScriptHtml) body.append(pageScriptHtml);
357
+
358
+ return $.html();
359
+ }
360
+
361
+ /**
362
+ * Check whether an element or any of its ancestors has the `data-locked` attribute.
363
+ */
364
+ function isElementLocked(el: cheerio.Cheerio, $: cheerio.Root): boolean {
365
+ return el.attr('data-locked') !== undefined;
70
366
  }
71
367
 
72
- const jsonSchema: JsonSchema = {
73
- name: 'HtmlPage',
74
- schema: {
75
- type: 'object',
76
- properties: {
77
- content: {
78
- type: 'string',
79
- description: 'html page content to return'
368
+ /**
369
+ * Apply a list of CRUD operations to annotated HTML (elements must have `data-node-id`).
370
+ */
371
+ export function applyChangeList(html: string, changes: ChangeList): string {
372
+ const $ = cheerio.load(html, { decodeEntities: false });
373
+
374
+ for (const change of changes) {
375
+ switch (change.op) {
376
+ case 'update': {
377
+ const el = $(`[data-node-id="${change.nodeId}"]`);
378
+ if (el.length === 0) {
379
+ console.warn(`applyChangeList: skipping update — node ${change.nodeId} not found (already removed?)`);
380
+ break;
381
+ }
382
+ el.html(change.html);
383
+ break;
384
+ }
385
+ case 'replace': {
386
+ const el = $(`[data-node-id="${change.nodeId}"]`);
387
+ if (el.length === 0) {
388
+ console.warn(`applyChangeList: skipping replace — node ${change.nodeId} not found (already removed?)`);
389
+ break;
390
+ }
391
+ if (isElementLocked(el, $)) {
392
+ console.warn(`applyChangeList: skipping replace — node ${change.nodeId} is data-locked`);
393
+ break;
394
+ }
395
+ el.replaceWith(change.html);
396
+ break;
397
+ }
398
+ case 'delete': {
399
+ const el = $(`[data-node-id="${change.nodeId}"]`);
400
+ if (el.length === 0) {
401
+ console.warn(`applyChangeList: skipping delete — node ${change.nodeId} not found (already removed?)`);
402
+ break;
403
+ }
404
+ if (isElementLocked(el, $)) {
405
+ console.warn(`applyChangeList: skipping delete — node ${change.nodeId} is data-locked`);
406
+ break;
407
+ }
408
+ el.remove();
409
+ break;
410
+ }
411
+ case 'insert': {
412
+ const parent = $(`[data-node-id="${change.parentId}"]`);
413
+ if (parent.length === 0) throw new Error(`insert: parent node ${change.parentId} not found`);
414
+ switch (change.position) {
415
+ case 'prepend': parent.prepend(change.html); break;
416
+ case 'append': parent.append(change.html); break;
417
+ case 'before': parent.before(change.html); break;
418
+ case 'after': parent.after(change.html); break;
419
+ default: throw new Error(`insert: unknown position "${(change as any).position}"`);
420
+ }
421
+ break;
422
+ }
423
+ case 'style-element': {
424
+ const el = $(`[data-node-id="${change.nodeId}"]`);
425
+ if (el.length === 0) {
426
+ console.warn(`applyChangeList: skipping style-element — node ${change.nodeId} not found (already removed?)`);
427
+ break;
428
+ }
429
+ if (isElementLocked(el, $)) {
430
+ console.warn(`applyChangeList: skipping style-element — node ${change.nodeId} is data-locked`);
431
+ break;
432
+ }
433
+ el.attr('style', change.style);
434
+ break;
435
+ }
436
+ default:
437
+ throw new Error(`Unknown change op: "${(change as any).op}"`);
438
+ }
439
+ }
440
+
441
+ return $.html();
442
+ }
443
+
444
+ /**
445
+ * Apply a list of CRUD operations and report any ops that failed due to
446
+ * missing nodes (instead of throwing). Unknown op types still throw.
447
+ */
448
+ function applyChangeListWithReport(html: string, changes: ChangeList): ApplyResult {
449
+ const $ = cheerio.load(html, { decodeEntities: false });
450
+ const failedOps: FailedOp[] = [];
451
+
452
+ for (const change of changes) {
453
+ switch (change.op) {
454
+ case 'update': {
455
+ const el = $(`[data-node-id="${change.nodeId}"]`);
456
+ if (el.length === 0) {
457
+ const reason = `node ${change.nodeId} not found (already removed?)`;
458
+ console.warn(`applyChangeListWithReport: skipping update — ${reason}`);
459
+ failedOps.push({ op: change, reason });
460
+ break;
461
+ }
462
+ el.html(change.html);
463
+ break;
80
464
  }
81
- },
82
- required: ['content'],
83
- additionalProperties: false
84
- },
85
- strict: true
86
- };
87
-
88
- const goal =
89
- `Generate a new web page that represents the next state of the chat based on the users message.
90
- Append the the users message and a brief response from the AI to the chat panel.
91
- Maintain the full conversation history in the chat panel unless asked to clear it.
92
- Any details or visualizations should be rendered to the viewer panel.
93
- The basic layout structure of the page needs to be maintained.
94
- You're free to write any additional CSS or JavaScript to enhance the page.
95
- Write an explication of your reasoning or any hidden thoughts to the thoughts div.
96
- If the user asks to create something like an app, tool, game, or ui create it in the viewer panel.
97
- If the user asks to draw something use canvas to draw it in the viewer panel.
98
- Always return the full html content of the page.`;
465
+ case 'replace': {
466
+ const el = $(`[data-node-id="${change.nodeId}"]`);
467
+ if (el.length === 0) {
468
+ const reason = `node ${change.nodeId} not found (already removed?)`;
469
+ console.warn(`applyChangeListWithReport: skipping replace — ${reason}`);
470
+ failedOps.push({ op: change, reason });
471
+ break;
472
+ }
473
+ if (isElementLocked(el, $)) {
474
+ const reason = `node ${change.nodeId} is data-locked`;
475
+ console.warn(`applyChangeListWithReport: skipping replace ${reason}`);
476
+ failedOps.push({ op: change, reason });
477
+ break;
478
+ }
479
+ el.replaceWith(change.html);
480
+ break;
481
+ }
482
+ case 'delete': {
483
+ const el = $(`[data-node-id="${change.nodeId}"]`);
484
+ if (el.length === 0) {
485
+ const reason = `node ${change.nodeId} not found (already removed?)`;
486
+ console.warn(`applyChangeListWithReport: skipping delete — ${reason}`);
487
+ failedOps.push({ op: change, reason });
488
+ break;
489
+ }
490
+ if (isElementLocked(el, $)) {
491
+ const reason = `node ${change.nodeId} is data-locked`;
492
+ console.warn(`applyChangeListWithReport: skipping delete — ${reason}`);
493
+ failedOps.push({ op: change, reason });
494
+ break;
495
+ }
496
+ el.remove();
497
+ break;
498
+ }
499
+ case 'insert': {
500
+ const parent = $(`[data-node-id="${change.parentId}"]`);
501
+ if (parent.length === 0) {
502
+ const reason = `parent node ${change.parentId} not found`;
503
+ console.warn(`applyChangeListWithReport: skipping insert — ${reason}`);
504
+ failedOps.push({ op: change, reason });
505
+ break;
506
+ }
507
+ switch (change.position) {
508
+ case 'prepend': parent.prepend(change.html); break;
509
+ case 'append': parent.append(change.html); break;
510
+ case 'before': parent.before(change.html); break;
511
+ case 'after': parent.after(change.html); break;
512
+ default: throw new Error(`insert: unknown position "${(change as any).position}"`);
513
+ }
514
+ break;
515
+ }
516
+ case 'style-element': {
517
+ const el = $(`[data-node-id="${change.nodeId}"]`);
518
+ if (el.length === 0) {
519
+ const reason = `node ${change.nodeId} not found (already removed?)`;
520
+ console.warn(`applyChangeListWithReport: skipping style-element — ${reason}`);
521
+ failedOps.push({ op: change, reason });
522
+ break;
523
+ }
524
+ if (isElementLocked(el, $)) {
525
+ const reason = `node ${change.nodeId} is data-locked`;
526
+ console.warn(`applyChangeListWithReport: skipping style-element — ${reason}`);
527
+ failedOps.push({ op: change, reason });
528
+ break;
529
+ }
530
+ el.attr('style', change.style);
531
+ break;
532
+ }
533
+ default:
534
+ throw new Error(`Unknown change op: "${(change as any).op}"`);
535
+ }
536
+ }
537
+
538
+ return { html: $.html(), failedOps };
539
+ }
540
+
541
+ /**
542
+ * Inject an error script block into the page HTML.
543
+ */
544
+ export function injectError(html: string, message: string, details: string): string {
545
+ const $ = cheerio.load(html, { decodeEntities: false });
546
+ const errorPayload = JSON.stringify({ message, details });
547
+ const scriptTag = `<script id="error" type="application/json">${errorPayload}</script>`;
548
+
549
+ // Remove any existing error block first
550
+ $('script#error').remove();
551
+
552
+ // Inject before closing </body> or at end
553
+ if ($('body').length > 0) {
554
+ $('body').append(scriptTag);
555
+ } else {
556
+ return html + scriptTag;
557
+ }
558
+
559
+ return $.html();
560
+ }
561
+
562
+ /**
563
+ * Parse a JSON change list from the model's raw response text.
564
+ * Handles responses that may include markdown fences or extra text around the JSON.
565
+ */
566
+ export function parseChangeList(response: string): ChangeList {
567
+ // Try direct parse first
568
+ try {
569
+ const parsed = JSON.parse(response);
570
+ if (Array.isArray(parsed)) return parsed as ChangeList;
571
+ } catch {
572
+ // fall through to extraction
573
+ }
574
+
575
+ // Try to extract JSON array from the response
576
+ const match = response.match(/\[[\s\S]*\]/);
577
+ if (match) {
578
+ try {
579
+ const parsed = JSON.parse(match[0]);
580
+ if (Array.isArray(parsed)) return parsed as ChangeList;
581
+ } catch {
582
+ // fall through
583
+ }
584
+ }
585
+
586
+ throw new Error('Failed to parse change list from model response');
587
+ }
588
+
589
+ // ---------------------------------------------------------------------------
590
+ // Prompt constants
591
+ // ---------------------------------------------------------------------------
592
+
593
+ const messageFormat =
594
+ `<MESSAGE_FORMAT>
595
+ <div class="chat-message"><p><strong>{SynthOS: | User:}</strong> {message contents}</p></div>
596
+ `
597
+
598
+ const transformInstr =
599
+ `Apply the users <USER_MESSAGE> to the .viewerPanel of the <CURRENT_PAGE> by generating a list of changes in JSON format.
600
+ 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.
601
+
602
+ If the <USER_MESSAGE> involves clearning the chat history, remove all .chat-message elements inside the #chatMessages container except for the first SynthOS: message. You may modify that message contents if requested.
603
+ If there's no <USER_MESSAGE> add a SynthOS: message to the chat with aasking the user what they would like to do.
604
+ 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 SynthOS: message asking the user for clarification on their intent.
605
+ If there is a <USER_MESSAGE> with clear intent, add a User: message with the <USER_MESSAGE> to the chat and add a SynthOS: message explaining your change or answering their question.
606
+ If a <USER_MESSAGE> is overly long, summarize the User: message.
607
+
608
+ 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.
609
+ You may add/remove new script blocks to the body but all script & style blocks should have a unique id.
610
+ You may modify the contents of a data-locked script block but may not remove it.
611
+
612
+ Every <CURRENT_PAGE> has hidden data-locked "thoughts" and "instructions" divs.
613
+ 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.
614
+ 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.
615
+ If the <USER_MESSAGE> indicates that a change didn't work, use your thoughts to diagnose the problem before fixing the issue.
616
+
617
+ 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.
618
+ 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().
619
+ 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).
620
+ 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.
621
+ 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).
622
+ 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.
623
+
624
+ 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.
625
+
626
+ 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.
627
+ If you're trying to assign an id to script or style block, use "replace" not "update".
628
+ 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.
629
+ Return a JSON array of change operations to apply to the page. Do NOT return the full HTML page.
630
+
631
+ Each operation must be one of:
632
+ { "op": "update", "nodeId": "<data-node-id>", "html": "<new innerHTML>" }
633
+ — replaces the innerHTML of the target element
634
+
635
+ { "op": "replace", "nodeId": "<data-node-id>", "html": "<new outerHTML>" }
636
+ — replaces the entire element (outerHTML) with new markup
637
+
638
+ { "op": "delete", "nodeId": "<data-node-id>" }
639
+ — removes the element from the page
640
+
641
+ { "op": "insert", "parentId": "<data-node-id>", "position": "prepend"|"append"|"before"|"after", "html": "<new element HTML>" }
642
+ — inserts new HTML relative to the parent element
643
+
644
+ { "op": "style-element", "nodeId": "<data-node-id>", "style": "<css style string>" }
645
+ — sets the style attribute of the target element (must be unlocked)
646
+
647
+ Return ONLY the JSON array. Example:
648
+ [
649
+ { "op": "update", "nodeId": "5", "html": "<p>Hello world</p>" },
650
+ { "op": "insert", "parentId": "3", "position": "append", "html": "<div class=\\"msg\\">New message</div>" }
651
+ ]`;
99
652
 
100
653
  const serverAPIs =
101
- `GET /api/data/:table
102
- description: Retrieve all rows from a table
103
- response: Array of JSON rows [{ id: string, ... }]
654
+ `<SERVER_APIS>
655
+ GET /api/data/:page/:table
656
+ description: Retrieve all rows from a page-scoped table (tables are stored per-page). Supports pagination via query params.
657
+ query params: limit (number, optional) — max rows to return; offset (number, optional, default 0) — rows to skip
658
+ response (without limit): Array of JSON rows [{ id: string, ... }]
659
+ response (with limit): { items: [{ id: string, ... }], total: number, offset: number, limit: number, hasMore: boolean }
104
660
 
105
- GET /api/data/:table/:id
106
- description: Retrieve a single row from a table
661
+ GET /api/data/:page/:table/:id
662
+ description: Retrieve a single row from a page-scoped table
107
663
  response: JSON row { id: string, ... }
108
664
 
109
- POST /api/data/:table
110
- description: Replaces or adds a single row to a table and returns the row
665
+ POST /api/data/:page/:table
666
+ description: Replaces or adds a single row to a page-scoped table and returns the row
111
667
  request: JSON row { id?: string, ... }
112
668
  response: { id: string, ... }
113
669
 
114
- DELETE /api/data/:table/:id
115
- description: Delete a single row from a table
670
+ DELETE /api/data/:page/:table/:id
671
+ description: Delete a single row from a page-scoped table
116
672
  response: { success: true }
117
673
 
118
674
  POST /api/generate/image
@@ -126,10 +682,75 @@ request: { prompt: string, temperature?: number }
126
682
  response: { answer: string, explanation: string }
127
683
 
128
684
  GET /api/pages
129
- description: Retrieve a list of all pages
130
- response: Array of page names [string]
685
+ description: Retrieve a list of all pages with metadata
686
+ response: Array of { name: string, title: string, categories: string[], pinned: boolean, createdDate: string, lastModified: string, pageVersion: number, mode: 'unlocked' | 'locked' }
687
+
688
+ GET /api/pages/:name
689
+ description: Retrieve metadata for a single page
690
+ response: { title: string, categories: string[], pinned: boolean, createdDate: string, lastModified: string, pageVersion: number, mode: 'unlocked' | 'locked' }
691
+
692
+ POST /api/pages/:name
693
+ description: Update page metadata (merge semantics — send only fields to change; lastModified is auto-set)
694
+ request: { title?: string, categories?: string[], pinned?: boolean, mode?: 'unlocked' | 'locked' }
695
+ response: Full metadata object
696
+
697
+ DELETE /api/pages/:name
698
+ description: Delete a user page (cannot delete required/system pages)
699
+ response: { deleted: true }
131
700
 
132
701
  POST /api/scripts/:id
133
702
  description: Execute a script with the passed in variables
134
703
  request: { [key: string]: string }
135
- response: string`;
704
+ response: string
705
+
706
+ POST /api/search/web
707
+ description: Search the web using Brave Search (must be enabled in Settings > Connectors)
708
+ request: { query: string, count?: number, country?: string, freshness?: string }
709
+ response: { results: [{ title: string, url: string, description: string }] }
710
+
711
+ GET /api/connectors
712
+ description: List available connectors (REST API proxies). Supports ?category=X and ?id=X filters.
713
+ response: [{ id: string, name: string, category: string, configured: boolean }]
714
+
715
+ GET /api/connectors/:id
716
+ description: Get full detail for a connector including its definition and configuration status
717
+ response: { id, name, category, description, baseUrl, authStrategy, authKey, fields, configured, enabled, hasKey }
718
+
719
+ POST /api/connectors (proxy call)
720
+ description: Proxy a request through a configured connector. The connector attaches auth automatically.
721
+ request: { connector: string, method: string, path: string, headers?: object, body?: any, query?: object }
722
+ response: Upstream API response (JSON or text)
723
+
724
+ PAGE HELPERS (available globally as window.synthos):
725
+ synthos.data.list(table, opts?) — GET /api/data/:page/:table (auto-scoped to current page; opts: { limit?, offset? } — when limit is set, returns { items, total, offset, limit, hasMore })
726
+ synthos.data.get(table, id) — GET /api/data/:page/:table/:id (auto-scoped to current page)
727
+ synthos.data.save(table, row) — POST /api/data/:page/:table (auto-scoped to current page)
728
+ synthos.data.remove(table, id) — DELETE /api/data/:page/:table/:id (auto-scoped to current page)
729
+ synthos.generate.image({ prompt, shape, style }) — POST /api/generate/image
730
+ synthos.generate.completion({ prompt, temperature? }) — POST /api/generate/completion
731
+ synthos.scripts.run(id, variables) — POST /api/scripts/:id
732
+ synthos.pages.list() — GET /api/pages
733
+ synthos.pages.get(name) — GET /api/pages/:name
734
+ synthos.pages.update(name, metadata) — POST /api/pages/:name
735
+ synthos.pages.remove(name) — DELETE /api/pages/:name
736
+ synthos.search.web(query, opts?) — POST /api/search/web (opts: { count?, country?, freshness? })
737
+ synthos.connectors.call(connector, method, path, opts?) — POST /api/connectors (proxy call; opts: { headers?, body?, query? })
738
+ synthos.connectors.list(opts?) — GET /api/connectors (opts: { category?, id? })
739
+ All methods return Promises. Prefer these helpers over raw fetch().`;
740
+
741
+ const repairUSER_MESSAGE =
742
+ `Some change operations from the previous response failed because the target nodes no longer exist in the page (they were removed or replaced by earlier operations in the same batch).
743
+
744
+ Below is the CURRENT state of the page after the successful operations were applied, followed by the list of operations that failed and why.
745
+
746
+ Re-generate corrected versions of ONLY the failed operations, targeting nodes that actually exist in the current page. Each element has a data-node-id attribute you can reference.
747
+ If a failed operation is no longer needed (e.g. the intended change was already accomplished by another op), omit it.
748
+ Return an empty JSON array [] if no repairs are needed.
749
+
750
+ Return ONLY a JSON array of change operations using the same format:
751
+ { "op": "update", "nodeId": "<data-node-id>", "html": "<new innerHTML>" }
752
+ { "op": "replace", "nodeId": "<data-node-id>", "html": "<new outerHTML>" }
753
+ { "op": "delete", "nodeId": "<data-node-id>" }
754
+ { "op": "insert", "parentId": "<data-node-id>", "position": "prepend"|"append"|"before"|"after", "html": "<new element HTML>" }
755
+ { "op": "style-element", "nodeId": "<data-node-id>", "style": "<css style string>" }`;
756
+