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