synthos 0.7.1 → 0.8.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 +215 -65
- package/default-pages/application.json +1 -0
- package/default-pages/json_tools.json +1 -1
- package/default-pages/oregon_trail.html +321 -0
- package/default-pages/oregon_trail.json +12 -0
- package/default-pages/sidebar_page.json +1 -0
- package/default-pages/solar_explorer.html +10 -18
- package/default-pages/solar_explorer.json +2 -2
- package/default-pages/two-panel_page.json +1 -0
- package/default-pages/us_map.html +192 -0
- package/default-pages/us_map.json +12 -0
- package/default-pages/us_map_1850.html +325 -0
- package/default-pages/us_map_1850.json +12 -0
- package/default-pages/western_cities_1850.html +526 -0
- package/default-pages/western_cities_1850.json +12 -0
- package/default-themes/{nebula-dawn.css → nebula-dawn.v2.css} +24 -0
- package/default-themes/{nebula-dusk.css → nebula-dusk.v2.css} +24 -0
- package/dist/agents/a2a/a2aProvider.d.ts +3 -0
- package/dist/agents/a2a/a2aProvider.d.ts.map +1 -0
- package/dist/agents/a2a/a2aProvider.js +126 -0
- package/dist/agents/a2a/a2aProvider.js.map +1 -0
- package/dist/agents/discovery.d.ts +30 -0
- package/dist/agents/discovery.d.ts.map +1 -0
- package/dist/agents/discovery.js +52 -0
- package/dist/agents/discovery.js.map +1 -0
- package/dist/agents/index.d.ts +7 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/index.js +19 -0
- package/dist/agents/index.js.map +1 -0
- package/dist/agents/openclaw/gatewayManager.d.ts +113 -0
- package/dist/agents/openclaw/gatewayManager.d.ts.map +1 -0
- package/dist/agents/openclaw/gatewayManager.js +470 -0
- package/dist/agents/openclaw/gatewayManager.js.map +1 -0
- package/dist/agents/openclaw/openclawProvider.d.ts +3 -0
- package/dist/agents/openclaw/openclawProvider.d.ts.map +1 -0
- package/dist/agents/openclaw/openclawProvider.js +239 -0
- package/dist/agents/openclaw/openclawProvider.js.map +1 -0
- package/dist/agents/openclaw/sshTunnelManager.d.ts +23 -0
- package/dist/agents/openclaw/sshTunnelManager.d.ts.map +1 -0
- package/dist/agents/openclaw/sshTunnelManager.js +340 -0
- package/dist/agents/openclaw/sshTunnelManager.js.map +1 -0
- package/dist/agents/types.d.ts +64 -0
- package/dist/agents/types.d.ts.map +1 -0
- package/dist/agents/types.js +6 -0
- package/dist/agents/types.js.map +1 -0
- package/dist/connectors/airtable/connector.json +27 -0
- package/dist/connectors/alpha-vantage/connector.json +26 -0
- package/dist/connectors/brave-search/connector.json +26 -0
- package/dist/connectors/cloudinary/connector.json +27 -0
- package/dist/connectors/deepl/connector.json +28 -0
- package/dist/connectors/elevenlabs/connector.json +30 -0
- package/dist/connectors/giphy/connector.json +27 -0
- package/dist/connectors/github/connector.json +29 -0
- package/dist/connectors/huggingface/connector.json +27 -0
- package/dist/connectors/imgur/connector.json +29 -0
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.d.ts.map +1 -1
- package/dist/connectors/instagram/connector.json +43 -0
- package/dist/connectors/jira/connector.json +28 -0
- package/dist/connectors/mapbox/connector.json +26 -0
- package/dist/connectors/nasa/connector.json +27 -0
- package/dist/connectors/newsapi/connector.json +27 -0
- package/dist/connectors/notion/connector.json +28 -0
- package/dist/connectors/open-exchange-rates/connector.json +27 -0
- package/dist/connectors/openweathermap/connector.json +26 -0
- package/dist/connectors/pexels/connector.json +27 -0
- package/dist/connectors/registry.d.ts.map +1 -1
- package/dist/connectors/registry.js +42 -96
- package/dist/connectors/registry.js.map +1 -1
- package/dist/connectors/resend/connector.json +29 -0
- package/dist/connectors/rss2json/connector.json +27 -0
- package/dist/connectors/sendgrid/connector.json +27 -0
- package/dist/connectors/spoonacular/connector.json +28 -0
- package/dist/connectors/stability-ai/connector.json +27 -0
- package/dist/connectors/twilio/connector.json +28 -0
- package/dist/connectors/types.d.ts +23 -0
- package/dist/connectors/types.d.ts.map +1 -1
- package/dist/connectors/unsplash/connector.json +27 -0
- package/dist/connectors/wolfram-alpha/connector.json +26 -0
- package/dist/connectors/youtube-data/connector.json +30 -0
- package/dist/files.d.ts +1 -0
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +16 -1
- package/dist/files.js.map +1 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +28 -0
- package/dist/init.js.map +1 -1
- package/dist/migrations.d.ts +3 -2
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +122 -138
- package/dist/migrations.js.map +1 -1
- package/dist/models/anthropic.d.ts +22 -0
- package/dist/models/anthropic.d.ts.map +1 -0
- package/dist/models/anthropic.js +76 -0
- package/dist/models/anthropic.js.map +1 -0
- package/dist/models/chainOfThought.d.ts +12 -0
- package/dist/models/chainOfThought.d.ts.map +1 -0
- package/dist/models/chainOfThought.js +45 -0
- package/dist/models/chainOfThought.js.map +1 -0
- package/dist/models/fireworksai.d.ts +30 -0
- package/dist/models/fireworksai.d.ts.map +1 -0
- package/dist/models/fireworksai.js +133 -0
- package/dist/models/fireworksai.js.map +1 -0
- package/dist/models/index.d.ts +7 -1
- package/dist/models/index.d.ts.map +1 -1
- package/dist/models/index.js +19 -1
- package/dist/models/index.js.map +1 -1
- package/dist/models/logCompletePrompt.d.ts +3 -0
- package/dist/models/logCompletePrompt.d.ts.map +1 -0
- package/dist/models/logCompletePrompt.js +23 -0
- package/dist/models/logCompletePrompt.js.map +1 -0
- package/dist/models/openai.d.ts +24 -0
- package/dist/models/openai.d.ts.map +1 -0
- package/dist/models/openai.js +80 -0
- package/dist/models/openai.js.map +1 -0
- 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 +34 -2
- package/dist/models/types.d.ts.map +1 -1
- package/dist/models/types.js +16 -0
- package/dist/models/types.js.map +1 -1
- package/dist/models/utils.d.ts +6 -0
- package/dist/models/utils.d.ts.map +1 -0
- package/dist/models/utils.js +21 -0
- package/dist/models/utils.js.map +1 -0
- package/dist/scripts.d.ts +2 -1
- package/dist/scripts.d.ts.map +1 -1
- package/dist/scripts.js +4 -3
- package/dist/scripts.js.map +1 -1
- package/dist/service/createCompletePrompt.d.ts +1 -1
- package/dist/service/createCompletePrompt.d.ts.map +1 -1
- package/dist/service/createCompletePrompt.js +9 -6
- package/dist/service/createCompletePrompt.js.map +1 -1
- package/dist/service/generateImage.d.ts +1 -1
- package/dist/service/generateImage.d.ts.map +1 -1
- package/dist/service/generateImage.js +3 -3
- package/dist/service/generateImage.js.map +1 -1
- package/dist/service/server.d.ts.map +1 -1
- package/dist/service/server.js +3 -0
- package/dist/service/server.js.map +1 -1
- package/dist/service/transformPage.d.ts +4 -2
- package/dist/service/transformPage.d.ts.map +1 -1
- package/dist/service/transformPage.js +74 -6
- package/dist/service/transformPage.js.map +1 -1
- package/dist/service/useAgentRoutes.d.ts +4 -0
- package/dist/service/useAgentRoutes.d.ts.map +1 -0
- package/dist/service/useAgentRoutes.js +389 -0
- package/dist/service/useAgentRoutes.js.map +1 -0
- package/dist/service/useApiRoutes.d.ts.map +1 -1
- package/dist/service/useApiRoutes.js +157 -16
- package/dist/service/useApiRoutes.js.map +1 -1
- package/dist/service/useConnectorRoutes.d.ts.map +1 -1
- package/dist/service/useConnectorRoutes.js +14 -3
- package/dist/service/useConnectorRoutes.js.map +1 -1
- package/dist/service/useGatewayRoutes.d.ts +4 -0
- package/dist/service/useGatewayRoutes.d.ts.map +1 -0
- package/dist/service/useGatewayRoutes.js +168 -0
- package/dist/service/useGatewayRoutes.js.map +1 -0
- package/dist/service/usePageRoutes.d.ts.map +1 -1
- package/dist/service/usePageRoutes.js +16 -5
- package/dist/service/usePageRoutes.js.map +1 -1
- package/dist/settings.d.ts +2 -1
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +4 -8
- package/dist/settings.js.map +1 -1
- package/dist/themes.d.ts +14 -0
- package/dist/themes.d.ts.map +1 -1
- package/dist/themes.js +86 -13
- package/dist/themes.js.map +1 -1
- package/package.json +10 -5
- package/page-scripts/helpers-v2.js +222 -0
- package/page-scripts/page-v2.js +656 -0
- package/required-pages/builder.html +1 -27
- package/required-pages/pages.html +745 -22
- package/required-pages/settings.html +819 -21
- package/required-pages/synthos_apis.html +56 -1
- package/src/agents/a2a/a2aProvider.ts +110 -0
- package/src/agents/discovery.ts +74 -0
- package/src/agents/index.ts +6 -0
- package/src/agents/openclaw/gatewayManager.ts +559 -0
- package/src/agents/openclaw/openclawProvider.ts +261 -0
- package/src/agents/openclaw/sshTunnelManager.ts +385 -0
- package/src/agents/types.ts +82 -0
- package/src/connectors/airtable/connector.json +27 -0
- package/src/connectors/alpha-vantage/connector.json +26 -0
- package/src/connectors/brave-search/connector.json +26 -0
- package/src/connectors/cloudinary/connector.json +27 -0
- package/src/connectors/deepl/connector.json +28 -0
- package/src/connectors/elevenlabs/connector.json +30 -0
- package/src/connectors/giphy/connector.json +27 -0
- package/src/connectors/github/connector.json +29 -0
- package/src/connectors/huggingface/connector.json +27 -0
- package/src/connectors/imgur/connector.json +29 -0
- package/src/connectors/index.ts +2 -0
- package/src/connectors/instagram/connector.json +43 -0
- package/src/connectors/jira/connector.json +28 -0
- package/src/connectors/mapbox/connector.json +26 -0
- package/src/connectors/nasa/connector.json +27 -0
- package/src/connectors/newsapi/connector.json +27 -0
- package/src/connectors/notion/connector.json +28 -0
- package/src/connectors/open-exchange-rates/connector.json +27 -0
- package/src/connectors/openweathermap/connector.json +26 -0
- package/src/connectors/pexels/connector.json +27 -0
- package/src/connectors/registry.ts +21 -97
- package/src/connectors/resend/connector.json +29 -0
- package/src/connectors/rss2json/connector.json +27 -0
- package/src/connectors/sendgrid/connector.json +27 -0
- package/src/connectors/spoonacular/connector.json +28 -0
- package/src/connectors/stability-ai/connector.json +27 -0
- package/src/connectors/twilio/connector.json +28 -0
- package/src/connectors/types.ts +25 -0
- package/src/connectors/unsplash/connector.json +27 -0
- package/src/connectors/wolfram-alpha/connector.json +26 -0
- package/src/connectors/youtube-data/connector.json +30 -0
- package/src/files.ts +14 -0
- package/src/init.ts +27 -0
- package/src/migrations.ts +121 -138
- package/src/models/anthropic.ts +89 -0
- package/src/models/chainOfThought.ts +56 -0
- package/src/models/fireworksai.ts +136 -0
- package/src/models/index.ts +7 -1
- package/src/models/logCompletePrompt.ts +25 -0
- package/src/models/openai.ts +90 -0
- package/src/models/providers.ts +12 -3
- package/src/models/types.ts +67 -2
- package/src/models/utils.ts +16 -0
- package/src/scripts.ts +2 -2
- package/src/service/createCompletePrompt.ts +3 -1
- package/src/service/generateImage.ts +2 -2
- package/src/service/server.ts +4 -0
- package/src/service/transformPage.ts +81 -8
- package/src/service/useAgentRoutes.ts +423 -0
- package/src/service/useApiRoutes.ts +173 -18
- package/src/service/useConnectorRoutes.ts +14 -3
- package/src/service/usePageRoutes.ts +20 -6
- package/src/settings.ts +6 -10
- package/src/themes.ts +84 -12
- package/tests/README.md +12 -0
- package/tests/anthropic.spec.ts +84 -0
- package/tests/chainOfThought.spec.ts +108 -0
- package/tests/ensureScripts.spec.ts +82 -0
- package/tests/files.spec.ts +233 -0
- package/tests/fireworksai.spec.ts +92 -0
- package/tests/logCompletePrompt.spec.ts +74 -0
- package/tests/migrations.spec.ts +169 -0
- package/tests/openai.spec.ts +71 -0
- package/tests/pages.spec.ts +328 -0
- package/tests/providers.spec.ts +144 -0
- package/tests/scripts.spec.ts +209 -0
- package/tests/transformPage.spec.ts +931 -0
- package/tests/types.spec.ts +23 -0
- package/default-pages/app_builder.json +0 -1
- package/default-pages/sidebar_builder.json +0 -1
- package/default-pages/two-panel_builder.json +0 -1
- package/images/home.png +0 -0
- package/images/page-management.png +0 -0
- package/images/settings.png +0 -0
- package/images/synthos-square.png +0 -0
- /package/default-pages/{app_builder.html → application.html} +0 -0
- /package/default-pages/{sidebar_builder.html → sidebar_page.html} +0 -0
- /package/default-pages/{two-panel_builder.html → two-panel_page.html} +0 -0
|
@@ -35,9 +35,11 @@ export function useConnectorRoutes(config: SynthOSConfig, app: Application): voi
|
|
|
35
35
|
id: def.id,
|
|
36
36
|
name: def.name,
|
|
37
37
|
category: def.category,
|
|
38
|
+
description: def.description,
|
|
38
39
|
configured: isOAuth
|
|
39
40
|
? !!oauthCfg && oauthCfg.enabled && !!oauthCfg.accessToken
|
|
40
|
-
: !!cfg && cfg.enabled && !!cfg.apiKey
|
|
41
|
+
: !!cfg && cfg.enabled && !!cfg.apiKey,
|
|
42
|
+
enabled: !!cfg?.enabled
|
|
41
43
|
};
|
|
42
44
|
});
|
|
43
45
|
|
|
@@ -352,11 +354,20 @@ export function useConnectorRoutes(config: SynthOSConfig, app: Application): voi
|
|
|
352
354
|
}
|
|
353
355
|
|
|
354
356
|
// Build URL — join baseUrl path with request path to avoid
|
|
355
|
-
// absolute paths (e.g. "/me/accounts") replacing the base path
|
|
357
|
+
// absolute paths (e.g. "/me/accounts") replacing the base path.
|
|
358
|
+
// Split path from inline query string first — assigning a '?' to
|
|
359
|
+
// URL.pathname encodes it as %3F, which breaks upstream APIs.
|
|
360
|
+
const [reqPath, reqQS] = request.path.split('?');
|
|
356
361
|
const base = new URL(def.baseUrl);
|
|
357
|
-
const joinedPath = base.pathname.replace(/\/+$/, '') + '/' +
|
|
362
|
+
const joinedPath = base.pathname.replace(/\/+$/, '') + '/' + reqPath.replace(/^\/+/, '');
|
|
358
363
|
base.pathname = joinedPath;
|
|
359
364
|
const url = base;
|
|
365
|
+
if (reqQS) {
|
|
366
|
+
const inline = new URLSearchParams(reqQS);
|
|
367
|
+
for (const [key, value] of inline.entries()) {
|
|
368
|
+
url.searchParams.set(key, value);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
360
371
|
if (request.query) {
|
|
361
372
|
for (const [key, value] of Object.entries(request.query)) {
|
|
362
373
|
url.searchParams.set(key, value);
|
|
@@ -5,7 +5,7 @@ import { transformPage } from "./transformPage";
|
|
|
5
5
|
import { getModelInstructions } from "./modelInstructions";
|
|
6
6
|
import { SynthOSConfig } from "../init";
|
|
7
7
|
import { createCompletePrompt } from "./createCompletePrompt";
|
|
8
|
-
import { completePrompt } from "
|
|
8
|
+
import { completePrompt } from "../models";
|
|
9
9
|
import { green, red, dim, estimateTokens } from "./debugLog";
|
|
10
10
|
import { loadThemeInfo } from "../themes";
|
|
11
11
|
import * as cheerio from 'cheerio';
|
|
@@ -56,10 +56,11 @@ function injectPageHelpers(html: string, pageVersion: number): string {
|
|
|
56
56
|
if (pageVersion < 2) return html;
|
|
57
57
|
const tag = `<script id="page-helpers" src="/api/page-helpers.js?v=${pageVersion}"></script>`;
|
|
58
58
|
|
|
59
|
-
//
|
|
59
|
+
// Remove any existing page-helpers script (may be at wrong position from prior LLM output)
|
|
60
|
+
// so it gets re-injected at the correct position below.
|
|
60
61
|
const existing = html.match(/<script\s+id="page-helpers"[^>]*><\/script>/);
|
|
61
62
|
if (existing) {
|
|
62
|
-
|
|
63
|
+
html = html.replace(existing[0], '');
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
// Inject into <head> after page-info so helpers are available before inline body scripts
|
|
@@ -205,6 +206,19 @@ export function usePageRoutes(config: SynthOSConfig, app: Application): void {
|
|
|
205
206
|
pageState = $.html();
|
|
206
207
|
}
|
|
207
208
|
|
|
209
|
+
// Inject save-line marker at the end of chat messages (skip for locked pages)
|
|
210
|
+
const sourceMetadata = await loadPageMetadata(config.pagesFolder, page, config.requiredPagesFolder);
|
|
211
|
+
if (sourceMetadata?.mode !== 'locked') {
|
|
212
|
+
const $ = cheerio.load(pageState);
|
|
213
|
+
// Remove any existing save-line first
|
|
214
|
+
$('#chatMessages .save-line').remove();
|
|
215
|
+
// Append new save-line
|
|
216
|
+
$('#chatMessages').append(
|
|
217
|
+
'<div class="save-line" data-locked="true"><span class="save-line-label">Saved</span></div>'
|
|
218
|
+
);
|
|
219
|
+
pageState = $.html();
|
|
220
|
+
}
|
|
221
|
+
|
|
208
222
|
// Save as new page
|
|
209
223
|
await savePageState(config.pagesFolder, saveAs, pageState, title, categories);
|
|
210
224
|
|
|
@@ -284,13 +298,13 @@ export function usePageRoutes(config: SynthOSConfig, app: Application): void {
|
|
|
284
298
|
const pagesFolder = config.pagesFolder;
|
|
285
299
|
const settings = await loadSettings(config.pagesFolder);
|
|
286
300
|
const builder = getModelEntry(settings, 'builder');
|
|
287
|
-
const {
|
|
288
|
-
const maxTokens = configuration.maxTokens;
|
|
301
|
+
const { instructions } = builder;
|
|
289
302
|
const theme = settings.theme;
|
|
290
303
|
const themeInfo = await loadThemeInfo(theme ?? 'nebula-dusk', config);
|
|
291
304
|
const modelInstructions = getModelInstructions(builder.provider);
|
|
292
305
|
const configuredConnectors = settings.connectors;
|
|
293
|
-
const
|
|
306
|
+
const configuredAgents = settings.agents;
|
|
307
|
+
const result = await transformPage({ pagesFolder, pageState, message, instructions, modelInstructions, completePrompt, themeInfo, configuredConnectors, configuredAgents });
|
|
294
308
|
if (result.completed) {
|
|
295
309
|
const { html, changeCount } = result.value!;
|
|
296
310
|
if (config.debug) {
|
package/src/settings.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {checkIfExists, loadFile, saveFile} from './files';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { ModelEntry, ProviderName, detectProvider } from './models';
|
|
4
|
+
import { AgentConfig } from './agents';
|
|
4
5
|
|
|
5
6
|
let _settings: Partial<SettingsV2>|undefined;
|
|
6
7
|
|
|
@@ -20,7 +21,6 @@ export type { ModelEntry } from './models';
|
|
|
20
21
|
export interface SettingsV1 {
|
|
21
22
|
serviceApiKey: string;
|
|
22
23
|
model: string;
|
|
23
|
-
maxTokens: number;
|
|
24
24
|
imageQuality: 'standard' | 'hd';
|
|
25
25
|
instructions?: string;
|
|
26
26
|
logCompletions?: boolean;
|
|
@@ -38,6 +38,7 @@ export interface SettingsV2 {
|
|
|
38
38
|
features: string[];
|
|
39
39
|
services?: ServicesConfig;
|
|
40
40
|
connectors?: ServicesConfig;
|
|
41
|
+
agents?: AgentConfig[];
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
export const DefaultSettings: SettingsV2 = {
|
|
@@ -47,7 +48,7 @@ export const DefaultSettings: SettingsV2 = {
|
|
|
47
48
|
{
|
|
48
49
|
use: 'builder',
|
|
49
50
|
provider: 'Anthropic',
|
|
50
|
-
configuration: { apiKey: '', model: ''
|
|
51
|
+
configuration: { apiKey: '', model: '' },
|
|
51
52
|
imageQuality: 'standard',
|
|
52
53
|
instructions: '',
|
|
53
54
|
logCompletions: false,
|
|
@@ -55,7 +56,7 @@ export const DefaultSettings: SettingsV2 = {
|
|
|
55
56
|
{
|
|
56
57
|
use: 'chat',
|
|
57
58
|
provider: 'Anthropic',
|
|
58
|
-
configuration: { apiKey: '', model: ''
|
|
59
|
+
configuration: { apiKey: '', model: '' },
|
|
59
60
|
imageQuality: 'standard',
|
|
60
61
|
instructions: '',
|
|
61
62
|
logCompletions: false,
|
|
@@ -63,7 +64,8 @@ export const DefaultSettings: SettingsV2 = {
|
|
|
63
64
|
],
|
|
64
65
|
features: [],
|
|
65
66
|
services: {},
|
|
66
|
-
connectors: {}
|
|
67
|
+
connectors: {},
|
|
68
|
+
agents: [],
|
|
67
69
|
};
|
|
68
70
|
|
|
69
71
|
/**
|
|
@@ -100,7 +102,6 @@ function migrateV1toV2(raw: Record<string, unknown>): SettingsV2 {
|
|
|
100
102
|
configuration: {
|
|
101
103
|
apiKey: v1.serviceApiKey ?? '',
|
|
102
104
|
model,
|
|
103
|
-
maxTokens: v1.maxTokens ?? 32000,
|
|
104
105
|
},
|
|
105
106
|
imageQuality: v1.imageQuality ?? 'standard',
|
|
106
107
|
instructions: v1.instructions ?? '',
|
|
@@ -112,7 +113,6 @@ function migrateV1toV2(raw: Record<string, unknown>): SettingsV2 {
|
|
|
112
113
|
configuration: {
|
|
113
114
|
apiKey: v1.serviceApiKey ?? '',
|
|
114
115
|
model: chatModel,
|
|
115
|
-
maxTokens: v1.maxTokens ?? 32000,
|
|
116
116
|
},
|
|
117
117
|
imageQuality: v1.imageQuality ?? 'standard',
|
|
118
118
|
instructions: v1.instructions ?? '',
|
|
@@ -133,10 +133,6 @@ export async function hasConfiguredSettings(folder: string): Promise<boolean> {
|
|
|
133
133
|
if (typeof builder.configuration.model !== 'string' || builder.configuration.model.length == 0) {
|
|
134
134
|
return false;
|
|
135
135
|
}
|
|
136
|
-
if (typeof builder.configuration.maxTokens !== 'number' || builder.configuration.maxTokens <= 0) {
|
|
137
|
-
return false;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
136
|
return true;
|
|
141
137
|
}
|
|
142
138
|
|
package/src/themes.ts
CHANGED
|
@@ -2,6 +2,8 @@ import path from 'path';
|
|
|
2
2
|
import { checkIfExists, listFiles, loadFile } from './files';
|
|
3
3
|
import { SynthOSConfig } from './init';
|
|
4
4
|
|
|
5
|
+
export const THEME_VERSION = 2;
|
|
6
|
+
|
|
5
7
|
export interface ThemeInfo {
|
|
6
8
|
mode: 'light' | 'dark';
|
|
7
9
|
colors: Record<string, string>;
|
|
@@ -11,6 +13,39 @@ function userThemesFolder(config: SynthOSConfig): string {
|
|
|
11
13
|
return path.join(config.pagesFolder, 'themes');
|
|
12
14
|
}
|
|
13
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Extract the base theme name and version from a CSS filename.
|
|
18
|
+
* e.g. "nebula-dusk.v2.css" → { name: "nebula-dusk", version: 2 }
|
|
19
|
+
* "nebula-dusk.css" → { name: "nebula-dusk", version: 1 }
|
|
20
|
+
*/
|
|
21
|
+
export function parseThemeFilename(filename: string): { name: string; version: number } | undefined {
|
|
22
|
+
if (!filename.endsWith('.css')) return undefined;
|
|
23
|
+
const versionedMatch = filename.match(/^(.+)\.v(\d+)\.css$/);
|
|
24
|
+
if (versionedMatch) {
|
|
25
|
+
return { name: versionedMatch[1], version: parseInt(versionedMatch[2], 10) };
|
|
26
|
+
}
|
|
27
|
+
return { name: filename.replace(/\.css$/, ''), version: 1 };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Find the CSS file for a theme by name in a folder.
|
|
32
|
+
* Prefers the highest-versioned file (e.g. name.v2.css over name.css).
|
|
33
|
+
*/
|
|
34
|
+
async function findThemeCssFile(folder: string, name: string): Promise<{ path: string; version: number } | undefined> {
|
|
35
|
+
if (!await checkIfExists(folder)) return undefined;
|
|
36
|
+
const files = await listFiles(folder);
|
|
37
|
+
let best: { path: string; version: number } | undefined;
|
|
38
|
+
for (const f of files) {
|
|
39
|
+
const parsed = parseThemeFilename(f);
|
|
40
|
+
if (parsed && parsed.name === name) {
|
|
41
|
+
if (!best || parsed.version > best.version) {
|
|
42
|
+
best = { path: path.join(folder, f), version: parsed.version };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return best;
|
|
47
|
+
}
|
|
48
|
+
|
|
14
49
|
export async function loadThemeInfo(name: string, config: SynthOSConfig): Promise<ThemeInfo | undefined> {
|
|
15
50
|
// Check user's local themes first, then fall back to package defaults
|
|
16
51
|
const localPath = path.join(userThemesFolder(config), `${name}.json`);
|
|
@@ -30,14 +65,14 @@ export async function loadThemeInfo(name: string, config: SynthOSConfig): Promis
|
|
|
30
65
|
|
|
31
66
|
export async function loadTheme(name: string, config: SynthOSConfig): Promise<string | undefined> {
|
|
32
67
|
// Check user's local themes first, then fall back to package defaults
|
|
33
|
-
const
|
|
34
|
-
if (
|
|
35
|
-
return await loadFile(
|
|
68
|
+
const local = await findThemeCssFile(userThemesFolder(config), name);
|
|
69
|
+
if (local) {
|
|
70
|
+
return await loadFile(local.path);
|
|
36
71
|
}
|
|
37
72
|
|
|
38
|
-
const
|
|
39
|
-
if (
|
|
40
|
-
return await loadFile(
|
|
73
|
+
const def = await findThemeCssFile(config.defaultThemesFolder, name);
|
|
74
|
+
if (def) {
|
|
75
|
+
return await loadFile(def.path);
|
|
41
76
|
}
|
|
42
77
|
|
|
43
78
|
return undefined;
|
|
@@ -51,9 +86,8 @@ export async function listThemes(config: SynthOSConfig): Promise<string[]> {
|
|
|
51
86
|
if (await checkIfExists(localFolder)) {
|
|
52
87
|
const files = await listFiles(localFolder);
|
|
53
88
|
for (const f of files) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
89
|
+
const parsed = parseThemeFilename(f);
|
|
90
|
+
if (parsed) names.add(parsed.name);
|
|
57
91
|
}
|
|
58
92
|
}
|
|
59
93
|
|
|
@@ -61,11 +95,49 @@ export async function listThemes(config: SynthOSConfig): Promise<string[]> {
|
|
|
61
95
|
if (await checkIfExists(config.defaultThemesFolder)) {
|
|
62
96
|
const files = await listFiles(config.defaultThemesFolder);
|
|
63
97
|
for (const f of files) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
98
|
+
const parsed = parseThemeFilename(f);
|
|
99
|
+
if (parsed) names.add(parsed.name);
|
|
67
100
|
}
|
|
68
101
|
}
|
|
69
102
|
|
|
70
103
|
return Array.from(names).sort();
|
|
71
104
|
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Compare local theme versions against defaults and return themes that need upgrading.
|
|
108
|
+
*/
|
|
109
|
+
export async function getOutdatedThemes(config: SynthOSConfig): Promise<string[]> {
|
|
110
|
+
const localFolder = userThemesFolder(config);
|
|
111
|
+
if (!await checkIfExists(localFolder)) return [];
|
|
112
|
+
|
|
113
|
+
const defaultFiles = await listFiles(config.defaultThemesFolder);
|
|
114
|
+
const localFiles = await listFiles(localFolder);
|
|
115
|
+
|
|
116
|
+
// Build maps: theme name → highest version
|
|
117
|
+
const defaultVersions = new Map<string, number>();
|
|
118
|
+
for (const f of defaultFiles) {
|
|
119
|
+
const parsed = parseThemeFilename(f);
|
|
120
|
+
if (parsed) {
|
|
121
|
+
const cur = defaultVersions.get(parsed.name) ?? 0;
|
|
122
|
+
if (parsed.version > cur) defaultVersions.set(parsed.name, parsed.version);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const localVersions = new Map<string, number>();
|
|
127
|
+
for (const f of localFiles) {
|
|
128
|
+
const parsed = parseThemeFilename(f);
|
|
129
|
+
if (parsed) {
|
|
130
|
+
const cur = localVersions.get(parsed.name) ?? 0;
|
|
131
|
+
if (parsed.version > cur) localVersions.set(parsed.name, parsed.version);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const outdated: string[] = [];
|
|
136
|
+
for (const [name, defVer] of defaultVersions) {
|
|
137
|
+
const localVer = localVersions.get(name) ?? 0;
|
|
138
|
+
if (localVer < defVer) {
|
|
139
|
+
outdated.push(name);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return outdated;
|
|
143
|
+
}
|
package/tests/README.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# SynthOS Tests
|
|
2
|
+
|
|
3
|
+
## Tier 1 (implemented)
|
|
4
|
+
- `transformPage.spec.ts` — assignNodeIds, stripNodeIds, applyChangeList, parseChangeList, injectError
|
|
5
|
+
- `pages.spec.ts` — normalizePageName, parseMetadata
|
|
6
|
+
- `migrations.spec.ts` — postProcessV2
|
|
7
|
+
|
|
8
|
+
## Tier 2 (TODO)
|
|
9
|
+
- `scripts.spec.ts` — listScripts, loadScripts, saveScripts
|
|
10
|
+
- `modelInstructions.spec.ts` — getModelInstructions provider routing
|
|
11
|
+
- `debugLog.spec.ts` — log formatting and filtering
|
|
12
|
+
- `init.spec.ts` — folder creation and default file copying
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import assert from 'assert';
|
|
2
|
+
import { buildAnthropicRequest } from '../src/models/anthropic';
|
|
3
|
+
import { PromptCompletionArgs } from '../src/models/types';
|
|
4
|
+
|
|
5
|
+
describe('buildAnthropicRequest', () => {
|
|
6
|
+
const baseArgs: PromptCompletionArgs = {
|
|
7
|
+
prompt: { role: 'user', content: 'Hello' },
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
it('builds messages from history + prompt', () => {
|
|
11
|
+
const args: PromptCompletionArgs = {
|
|
12
|
+
...baseArgs,
|
|
13
|
+
history: [
|
|
14
|
+
{ role: 'user', content: 'Hi' },
|
|
15
|
+
{ role: 'assistant', content: 'Hey' },
|
|
16
|
+
],
|
|
17
|
+
};
|
|
18
|
+
const { messages } = buildAnthropicRequest(args, 0.0);
|
|
19
|
+
assert.strictEqual(messages.length, 3);
|
|
20
|
+
assert.strictEqual(messages[0].role, 'user');
|
|
21
|
+
assert.strictEqual(messages[0].content, 'Hi');
|
|
22
|
+
assert.strictEqual(messages[1].role, 'assistant');
|
|
23
|
+
assert.strictEqual(messages[1].content, 'Hey');
|
|
24
|
+
assert.strictEqual(messages[2].role, 'user');
|
|
25
|
+
assert.strictEqual(messages[2].content, 'Hello');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('appends assistant prefill "{" in jsonMode', () => {
|
|
29
|
+
const args: PromptCompletionArgs = { ...baseArgs, jsonMode: true };
|
|
30
|
+
const { messages } = buildAnthropicRequest(args, 0.0);
|
|
31
|
+
const last = messages[messages.length - 1];
|
|
32
|
+
assert.strictEqual(last.role, 'assistant');
|
|
33
|
+
assert.strictEqual(last.content, '{');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('appends assistant prefill "{" when jsonSchema is provided', () => {
|
|
37
|
+
const args: PromptCompletionArgs = { ...baseArgs, jsonSchema: { type: 'object' } };
|
|
38
|
+
const { messages } = buildAnthropicRequest(args, 0.0);
|
|
39
|
+
const last = messages[messages.length - 1];
|
|
40
|
+
assert.strictEqual(last.role, 'assistant');
|
|
41
|
+
assert.strictEqual(last.content, '{');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('does not prefill in plain text mode', () => {
|
|
45
|
+
const { messages } = buildAnthropicRequest(baseArgs, 0.0);
|
|
46
|
+
const last = messages[messages.length - 1];
|
|
47
|
+
assert.strictEqual(last.role, 'user');
|
|
48
|
+
assert.strictEqual(last.content, 'Hello');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('injects jsonSchema into system content', () => {
|
|
52
|
+
const args: PromptCompletionArgs = {
|
|
53
|
+
...baseArgs,
|
|
54
|
+
system: { role: 'system', content: 'Be helpful.' },
|
|
55
|
+
jsonSchema: { type: 'object', properties: { name: { type: 'string' } } },
|
|
56
|
+
};
|
|
57
|
+
const { system } = buildAnthropicRequest(args, 0.0);
|
|
58
|
+
assert.ok(system);
|
|
59
|
+
assert.ok(system.includes('Be helpful.'));
|
|
60
|
+
assert.ok(system.includes('JSON conforming to this schema'));
|
|
61
|
+
assert.ok(system.includes('"type":"object"'));
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('creates system content from schema alone when no system message', () => {
|
|
65
|
+
const args: PromptCompletionArgs = {
|
|
66
|
+
...baseArgs,
|
|
67
|
+
jsonSchema: { type: 'object' },
|
|
68
|
+
};
|
|
69
|
+
const { system } = buildAnthropicRequest(args, 0.0);
|
|
70
|
+
assert.ok(system);
|
|
71
|
+
assert.ok(system.includes('JSON conforming to this schema'));
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('uses default temperature when none specified', () => {
|
|
75
|
+
const { temperature } = buildAnthropicRequest(baseArgs, 0.7);
|
|
76
|
+
assert.strictEqual(temperature, 0.7);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('uses args temperature when specified', () => {
|
|
80
|
+
const args: PromptCompletionArgs = { ...baseArgs, temperature: 0.3 };
|
|
81
|
+
const { temperature } = buildAnthropicRequest(args, 0.7);
|
|
82
|
+
assert.strictEqual(temperature, 0.3);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import assert from 'assert';
|
|
2
|
+
import { chainOfThought, ChainOfThoughtArgs } from '../src/models/chainOfThought';
|
|
3
|
+
import { AgentCompletion, PromptCompletionArgs } from '../src/models/types';
|
|
4
|
+
|
|
5
|
+
/** Helper: build a stub completePrompt that returns the given result. */
|
|
6
|
+
function stubCompletePrompt(result: AgentCompletion<any>) {
|
|
7
|
+
return async (_args: PromptCompletionArgs) => result;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Helper: build a stub that captures the args it was called with. */
|
|
11
|
+
function capturingStub(result: AgentCompletion<any>) {
|
|
12
|
+
let captured: PromptCompletionArgs | undefined;
|
|
13
|
+
const fn = async (args: PromptCompletionArgs) => { captured = args; return result; };
|
|
14
|
+
return { fn, getCaptured: () => captured };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('chainOfThought', () => {
|
|
18
|
+
it('returns parsed {explanation, answer} from a valid JSON string response', async () => {
|
|
19
|
+
const stub = stubCompletePrompt({
|
|
20
|
+
completed: true,
|
|
21
|
+
value: '{"explanation": "because", "answer": "42"}',
|
|
22
|
+
});
|
|
23
|
+
const result = await chainOfThought({ completePrompt: stub, question: 'What?' });
|
|
24
|
+
assert.strictEqual(result.completed, true);
|
|
25
|
+
assert.deepStrictEqual(result.value, { explanation: 'because', answer: '42' });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('returns parsed result when completePrompt returns a pre-parsed object', async () => {
|
|
29
|
+
const stub = stubCompletePrompt({
|
|
30
|
+
completed: true,
|
|
31
|
+
value: { explanation: 'reason', answer: 'yes' },
|
|
32
|
+
});
|
|
33
|
+
const result = await chainOfThought({ completePrompt: stub, question: 'Q?' });
|
|
34
|
+
assert.strictEqual(result.completed, true);
|
|
35
|
+
assert.deepStrictEqual(result.value, { explanation: 'reason', answer: 'yes' });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('strips markdown code fences before parsing', async () => {
|
|
39
|
+
const stub = stubCompletePrompt({
|
|
40
|
+
completed: true,
|
|
41
|
+
value: '```json\n{"explanation": "x", "answer": "y"}\n```',
|
|
42
|
+
});
|
|
43
|
+
const result = await chainOfThought({ completePrompt: stub, question: 'Q?' });
|
|
44
|
+
assert.strictEqual(result.completed, true);
|
|
45
|
+
assert.deepStrictEqual(result.value, { explanation: 'x', answer: 'y' });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('extracts JSON from text with surrounding prose', async () => {
|
|
49
|
+
const stub = stubCompletePrompt({
|
|
50
|
+
completed: true,
|
|
51
|
+
value: 'Here is my answer: {"explanation": "e", "answer": "a"} hope that helps!',
|
|
52
|
+
});
|
|
53
|
+
const result = await chainOfThought({ completePrompt: stub, question: 'Q?' });
|
|
54
|
+
assert.strictEqual(result.completed, true);
|
|
55
|
+
assert.deepStrictEqual(result.value, { explanation: 'e', answer: 'a' });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('returns {completed: false} when completePrompt fails', async () => {
|
|
59
|
+
const stub = stubCompletePrompt({
|
|
60
|
+
completed: false,
|
|
61
|
+
error: new Error('API error'),
|
|
62
|
+
});
|
|
63
|
+
const result = await chainOfThought({ completePrompt: stub, question: 'Q?' });
|
|
64
|
+
assert.strictEqual(result.completed, false);
|
|
65
|
+
assert.strictEqual(result.error?.message, 'API error');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('returns parse-error when response is not valid JSON', async () => {
|
|
69
|
+
const stub = stubCompletePrompt({
|
|
70
|
+
completed: true,
|
|
71
|
+
value: 'this is not json at all',
|
|
72
|
+
});
|
|
73
|
+
const result = await chainOfThought({ completePrompt: stub, question: 'Q?' });
|
|
74
|
+
assert.strictEqual(result.completed, false);
|
|
75
|
+
assert.ok(result.error?.message.includes('parse'));
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('includes custom instructions in system prompt when provided', async () => {
|
|
79
|
+
const { fn, getCaptured } = capturingStub({
|
|
80
|
+
completed: true,
|
|
81
|
+
value: '{"explanation": "", "answer": ""}',
|
|
82
|
+
});
|
|
83
|
+
await chainOfThought({ completePrompt: fn, question: 'Q?', instructions: 'Be concise.' });
|
|
84
|
+
const system = getCaptured()!.system!;
|
|
85
|
+
assert.ok(system.content.startsWith('Be concise.'));
|
|
86
|
+
assert.ok(system.content.includes('JSON object'));
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('uses default system prompt when no instructions given', async () => {
|
|
90
|
+
const { fn, getCaptured } = capturingStub({
|
|
91
|
+
completed: true,
|
|
92
|
+
value: '{"explanation": "", "answer": ""}',
|
|
93
|
+
});
|
|
94
|
+
await chainOfThought({ completePrompt: fn, question: 'Q?' });
|
|
95
|
+
const system = getCaptured()!.system!;
|
|
96
|
+
assert.ok(system.content.startsWith('You must return'));
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('defaults missing explanation/answer to empty strings', async () => {
|
|
100
|
+
const stub = stubCompletePrompt({
|
|
101
|
+
completed: true,
|
|
102
|
+
value: '{}',
|
|
103
|
+
});
|
|
104
|
+
const result = await chainOfThought({ completePrompt: stub, question: 'Q?' });
|
|
105
|
+
assert.strictEqual(result.completed, true);
|
|
106
|
+
assert.deepStrictEqual(result.value, { explanation: '', answer: '' });
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import assert from 'assert';
|
|
2
|
+
import { ensureScriptsBeforeBodyClose } from '../src/service/transformPage';
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// ensureScriptsBeforeBodyClose
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
describe('ensureScriptsBeforeBodyClose', () => {
|
|
9
|
+
it('moves page-helpers and page-script to end of body in correct order', () => {
|
|
10
|
+
const html =
|
|
11
|
+
'<html><head></head><body>' +
|
|
12
|
+
'<script id="page-script" src="/api/page-script.js?v=2"></script>' +
|
|
13
|
+
'<div>content</div>' +
|
|
14
|
+
'<script id="page-helpers" src="/api/page-helpers.js?v=2"></script>' +
|
|
15
|
+
'</body></html>';
|
|
16
|
+
|
|
17
|
+
const result = ensureScriptsBeforeBodyClose(html);
|
|
18
|
+
|
|
19
|
+
// Both scripts should be at the end of body
|
|
20
|
+
const bodyContent = result.match(/<body>([\s\S]*)<\/body>/)?.[1] ?? '';
|
|
21
|
+
const helpersIdx = bodyContent.lastIndexOf('id="page-helpers"');
|
|
22
|
+
const scriptIdx = bodyContent.lastIndexOf('id="page-script"');
|
|
23
|
+
const contentIdx = bodyContent.indexOf('<div>content</div>');
|
|
24
|
+
|
|
25
|
+
// Content should come before both scripts
|
|
26
|
+
assert.ok(contentIdx < helpersIdx, 'content should be before page-helpers');
|
|
27
|
+
// Helpers should come before page-script
|
|
28
|
+
assert.ok(helpersIdx < scriptIdx, 'page-helpers should be before page-script');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('handles missing page-helpers gracefully', () => {
|
|
32
|
+
const html =
|
|
33
|
+
'<html><head></head><body>' +
|
|
34
|
+
'<div>content</div>' +
|
|
35
|
+
'<script id="page-script" src="/api/page-script.js?v=2"></script>' +
|
|
36
|
+
'</body></html>';
|
|
37
|
+
|
|
38
|
+
const result = ensureScriptsBeforeBodyClose(html);
|
|
39
|
+
assert.ok(result.includes('id="page-script"'));
|
|
40
|
+
assert.ok(result.includes('<div>content</div>'));
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('handles missing page-script gracefully', () => {
|
|
44
|
+
const html =
|
|
45
|
+
'<html><head></head><body>' +
|
|
46
|
+
'<div>content</div>' +
|
|
47
|
+
'<script id="page-helpers" src="/api/page-helpers.js?v=2"></script>' +
|
|
48
|
+
'</body></html>';
|
|
49
|
+
|
|
50
|
+
const result = ensureScriptsBeforeBodyClose(html);
|
|
51
|
+
assert.ok(result.includes('id="page-helpers"'));
|
|
52
|
+
assert.ok(result.includes('<div>content</div>'));
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('handles both scripts missing gracefully', () => {
|
|
56
|
+
const html = '<html><head></head><body><div>content</div></body></html>';
|
|
57
|
+
const result = ensureScriptsBeforeBodyClose(html);
|
|
58
|
+
assert.ok(result.includes('<div>content</div>'));
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('returns HTML unchanged when no body tag', () => {
|
|
62
|
+
const html = '<div>no body tag</div>';
|
|
63
|
+
const result = ensureScriptsBeforeBodyClose(html);
|
|
64
|
+
// cheerio wraps in html/body, so just check the content is preserved
|
|
65
|
+
assert.ok(result.includes('no body tag'));
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('does nothing when scripts are already at end of body', () => {
|
|
69
|
+
const html =
|
|
70
|
+
'<html><head></head><body>' +
|
|
71
|
+
'<div>content</div>' +
|
|
72
|
+
'<script id="page-helpers" src="/api/page-helpers.js?v=2"></script>' +
|
|
73
|
+
'<script id="page-script" src="/api/page-script.js?v=2"></script>' +
|
|
74
|
+
'</body></html>';
|
|
75
|
+
|
|
76
|
+
const result = ensureScriptsBeforeBodyClose(html);
|
|
77
|
+
const bodyContent = result.match(/<body>([\s\S]*)<\/body>/)?.[1] ?? '';
|
|
78
|
+
const helpersIdx = bodyContent.lastIndexOf('id="page-helpers"');
|
|
79
|
+
const scriptIdx = bodyContent.lastIndexOf('id="page-script"');
|
|
80
|
+
assert.ok(helpersIdx < scriptIdx);
|
|
81
|
+
});
|
|
82
|
+
});
|