synthos 0.7.2 → 0.9.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/page.html +42 -0
- package/default-pages/application/page.json +10 -0
- package/default-pages/elevenlabs_effects_studio/page.html +1363 -0
- package/default-pages/elevenlabs_effects_studio/page.json +11 -0
- package/default-pages/elevenlabs_voice_studio/page.html +801 -0
- package/default-pages/elevenlabs_voice_studio/page.json +11 -0
- package/default-pages/{json_tools.html → json_tools/page.html} +13 -11
- package/default-pages/json_tools/page.json +10 -0
- package/default-pages/my_notes/notes/a1b2c3d4-e5f6-7890-abcd-ef1234567890.json +5 -0
- package/default-pages/my_notes/page.html +132 -0
- package/default-pages/{my_notes.json → my_notes/page.json} +2 -2
- package/default-pages/neon_asteroids/files/Ambient_Space.mp3 +0 -0
- package/default-pages/neon_asteroids/files/Ambient_Space2.mp3 +0 -0
- package/default-pages/neon_asteroids/files/Ambient_Space3.mp3 +0 -0
- package/default-pages/neon_asteroids/files/Asteroid_Explosion.mp3 +0 -0
- package/default-pages/neon_asteroids/files/Hyperspace_Jump.mp3 +0 -0
- package/default-pages/neon_asteroids/files/Laser_Fire.mp3 +0 -0
- package/default-pages/neon_asteroids/files/Menu_Navigate.mp3 +0 -0
- package/default-pages/neon_asteroids/files/Power_Up_Collect.mp3 +0 -0
- package/default-pages/neon_asteroids/files/Saucer_Alert.mp3 +0 -0
- package/default-pages/neon_asteroids/files/Ship_Thrust.mp3 +0 -0
- package/default-pages/neon_asteroids/files/effects.json +74 -0
- package/default-pages/neon_asteroids/page.html +1822 -0
- package/default-pages/{neon_asteroids.json → neon_asteroids/page.json} +3 -3
- package/default-pages/oregon_trail/page.html +323 -0
- package/default-pages/oregon_trail/page.json +12 -0
- package/default-pages/retro_game_starter/page.html +1308 -0
- package/default-pages/retro_game_starter/page.json +12 -0
- package/default-pages/{sidebar_builder.html → sidebar_page/page.html} +12 -10
- package/default-pages/sidebar_page/page.json +10 -0
- package/default-pages/{solar_explorer.html → solar_explorer/page.html} +24 -29
- package/default-pages/{solar_explorer.json → solar_explorer/page.json} +4 -4
- package/default-pages/{solar_tutorial.html → solar_tutorial/page.html} +12 -10
- package/default-pages/solar_tutorial/page.json +10 -0
- package/default-pages/{two-panel_builder.html → two-panel_page/page.html} +13 -11
- package/default-pages/two-panel_page/page.json +10 -0
- package/default-pages/us_map/page.html +193 -0
- package/default-pages/us_map/page.json +12 -0
- package/default-pages/us_map_1850/page.html +326 -0
- package/default-pages/us_map_1850/page.json +12 -0
- package/default-pages/western_cities_1850/page.html +527 -0
- package/default-pages/western_cities_1850/page.json +12 -0
- package/default-themes/aurora-dawn.json +19 -0
- package/default-themes/aurora-dawn.v3.css +198 -0
- package/default-themes/aurora-dusk.json +19 -0
- package/default-themes/aurora-dusk.v3.css +200 -0
- package/default-themes/cosmos-dawn.json +19 -0
- package/default-themes/cosmos-dawn.v3.css +198 -0
- package/default-themes/cosmos-dusk.json +19 -0
- package/default-themes/cosmos-dusk.v3.css +200 -0
- package/default-themes/high-contrast-dark.json +19 -0
- package/default-themes/high-contrast-dark.v3.css +200 -0
- package/default-themes/high-contrast-light.json +19 -0
- package/default-themes/high-contrast-light.v3.css +198 -0
- package/default-themes/{nebula-dawn.css → nebula-dawn.v2.css} +134 -0
- package/default-themes/nebula-dawn.v3.css +199 -0
- package/default-themes/{nebula-dusk.css → nebula-dusk.v2.css} +128 -0
- package/default-themes/nebula-dusk.v3.css +201 -0
- package/default-themes/solar-flare-dawn.json +19 -0
- package/default-themes/solar-flare-dawn.v3.css +198 -0
- package/default-themes/solar-flare-dusk.json +19 -0
- package/default-themes/solar-flare-dusk.v3.css +200 -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.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 +20 -0
- package/dist/agents/index.js.map +1 -0
- package/dist/agents/openclaw/gatewayManager.d.ts +117 -0
- package/dist/agents/openclaw/gatewayManager.d.ts.map +1 -0
- package/dist/agents/openclaw/gatewayManager.js +486 -0
- package/dist/agents/openclaw/gatewayManager.js.map +1 -0
- package/dist/agents/openclaw/openclawProvider.d.ts.map +1 -0
- package/dist/agents/openclaw/openclawProvider.js +237 -0
- package/dist/agents/openclaw/openclawProvider.js.map +1 -0
- package/dist/agents/openclaw/sshTunnelManager.d.ts +25 -0
- package/dist/agents/openclaw/sshTunnelManager.d.ts.map +1 -0
- package/dist/agents/openclaw/sshTunnelManager.js +359 -0
- package/dist/agents/openclaw/sshTunnelManager.js.map +1 -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/builders/anthropic.d.ts +31 -0
- package/dist/builders/anthropic.d.ts.map +1 -0
- package/dist/builders/anthropic.js +227 -0
- package/dist/builders/anthropic.js.map +1 -0
- package/dist/builders/fireworksai.d.ts +9 -0
- package/dist/builders/fireworksai.d.ts.map +1 -0
- package/dist/builders/fireworksai.js +57 -0
- package/dist/builders/fireworksai.js.map +1 -0
- package/dist/builders/index.d.ts +13 -0
- package/dist/builders/index.d.ts.map +1 -0
- package/dist/builders/index.js +31 -0
- package/dist/builders/index.js.map +1 -0
- package/dist/builders/openai.d.ts +8 -0
- package/dist/builders/openai.d.ts.map +1 -0
- package/dist/builders/openai.js +87 -0
- package/dist/builders/openai.js.map +1 -0
- package/dist/builders/types.d.ts +54 -0
- package/dist/builders/types.d.ts.map +1 -0
- package/dist/builders/types.js +211 -0
- package/dist/builders/types.js.map +1 -0
- package/dist/connectors/index.d.ts.map +1 -1
- package/dist/connectors/index.js +3 -2
- package/dist/connectors/index.js.map +1 -1
- package/dist/connectors/registry.d.ts +2 -1
- package/dist/connectors/registry.d.ts.map +1 -1
- package/dist/connectors/registry.js +65 -96
- package/dist/connectors/registry.js.map +1 -1
- package/dist/connectors/types.d.ts.map +1 -1
- package/dist/customizer/Customizer.d.ts +57 -0
- package/dist/customizer/Customizer.d.ts.map +1 -0
- package/dist/customizer/Customizer.js +124 -0
- package/dist/customizer/Customizer.js.map +1 -0
- package/dist/customizer/index.d.ts.map +1 -0
- package/dist/customizer/index.js +9 -0
- package/dist/customizer/index.js.map +1 -0
- package/dist/files.d.ts +17 -0
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +75 -1
- package/dist/files.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +10 -6
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +97 -86
- package/dist/init.js.map +1 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +142 -145
- package/dist/migrations.js.map +1 -1
- package/dist/models/anthropic.d.ts +24 -0
- package/dist/models/anthropic.d.ts.map +1 -0
- package/dist/models/anthropic.js +103 -0
- package/dist/models/anthropic.js.map +1 -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.map +1 -0
- package/dist/models/fireworksai.js +141 -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 +20 -1
- package/dist/models/index.js.map +1 -1
- 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 +101 -0
- package/dist/models/openai.js.map +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 +53 -2
- package/dist/models/types.d.ts.map +1 -1
- package/dist/models/types.js +21 -0
- package/dist/models/types.js.map +1 -1
- 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/pages.d.ts +30 -7
- package/dist/pages.d.ts.map +1 -1
- package/dist/pages.js +177 -55
- package/dist/pages.js.map +1 -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.map +1 -1
- package/dist/service/createCompletePrompt.js +9 -6
- package/dist/service/createCompletePrompt.js.map +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 +39 -7
- package/dist/service/server.js.map +1 -1
- package/dist/service/transformPage.d.ts +47 -18
- package/dist/service/transformPage.d.ts.map +1 -1
- package/dist/service/transformPage.js +559 -270
- package/dist/service/transformPage.js.map +1 -1
- package/dist/service/useAgentRoutes.d.ts +5 -0
- package/dist/service/useAgentRoutes.d.ts.map +1 -0
- package/dist/service/useAgentRoutes.js +392 -0
- package/dist/service/useAgentRoutes.js.map +1 -0
- package/dist/service/useApiRoutes.d.ts.map +1 -1
- package/dist/service/useApiRoutes.js +380 -138
- package/dist/service/useApiRoutes.js.map +1 -1
- package/dist/service/useConnectorRoutes.d.ts.map +1 -1
- package/dist/service/useConnectorRoutes.js +20 -9
- package/dist/service/useConnectorRoutes.js.map +1 -1
- package/dist/service/useFileRoutes.d.ts +4 -0
- package/dist/service/useFileRoutes.d.ts.map +1 -0
- package/dist/service/useFileRoutes.js +122 -0
- package/dist/service/useFileRoutes.js.map +1 -0
- package/dist/service/usePageRoutes.d.ts.map +1 -1
- package/dist/service/usePageRoutes.js +660 -68
- package/dist/service/usePageRoutes.js.map +1 -1
- package/dist/service/useSharedDataRoutes.d.ts +4 -0
- package/dist/service/useSharedDataRoutes.d.ts.map +1 -0
- package/dist/service/useSharedDataRoutes.js +104 -0
- package/dist/service/useSharedDataRoutes.js.map +1 -0
- package/dist/service/useSharedFileRoutes.d.ts +4 -0
- package/dist/service/useSharedFileRoutes.d.ts.map +1 -0
- package/dist/service/useSharedFileRoutes.js +121 -0
- package/dist/service/useSharedFileRoutes.js.map +1 -0
- package/dist/settings.d.ts +3 -1
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +5 -8
- package/dist/settings.js.map +1 -1
- package/dist/synthos-cli.d.ts.map +1 -1
- package/dist/synthos-cli.js +4 -3
- package/dist/synthos-cli.js.map +1 -1
- package/dist/themes.d.ts +15 -0
- package/dist/themes.d.ts.map +1 -1
- package/dist/themes.js +106 -20
- package/dist/themes.js.map +1 -1
- package/migration-rules/v1-to-v2.md +193 -0
- package/migration-rules/v2-to-v3.md +481 -0
- package/package.json +15 -11
- package/required-pages/builder/page.html +43 -0
- package/required-pages/builder/page.json +10 -0
- package/required-pages/pages/page.html +924 -0
- package/required-pages/pages/page.json +10 -0
- package/required-pages/settings/page.html +1753 -0
- package/required-pages/settings/page.json +10 -0
- package/required-pages/synthos_apis/page.html +846 -0
- package/required-pages/synthos_apis/page.json +10 -0
- package/required-pages/{synthos_scripts.html → synthos_scripts/page.html} +13 -11
- package/required-pages/synthos_scripts/page.json +10 -0
- package/service-connectors/airtable/connector.json +27 -0
- package/service-connectors/alpha-vantage/connector.json +26 -0
- package/service-connectors/brave-search/connector.json +26 -0
- package/service-connectors/cloudinary/connector.json +27 -0
- package/service-connectors/deepl/connector.json +28 -0
- package/service-connectors/elevenlabs/connector.json +30 -0
- package/service-connectors/giphy/connector.json +27 -0
- package/service-connectors/github/connector.json +29 -0
- package/service-connectors/huggingface/connector.json +27 -0
- package/service-connectors/imgur/connector.json +29 -0
- package/service-connectors/instagram/connector.json +43 -0
- package/service-connectors/jira/connector.json +28 -0
- package/service-connectors/mapbox/connector.json +26 -0
- package/service-connectors/nasa/connector.json +27 -0
- package/service-connectors/newsapi/connector.json +27 -0
- package/service-connectors/notion/connector.json +28 -0
- package/service-connectors/open-exchange-rates/connector.json +27 -0
- package/service-connectors/openweathermap/connector.json +26 -0
- package/service-connectors/pexels/connector.json +27 -0
- package/service-connectors/resend/connector.json +29 -0
- package/service-connectors/rss2json/connector.json +27 -0
- package/service-connectors/sendgrid/connector.json +27 -0
- package/service-connectors/spoonacular/connector.json +28 -0
- package/service-connectors/stability-ai/connector.json +27 -0
- package/service-connectors/twilio/connector.json +28 -0
- package/service-connectors/unsplash/connector.json +27 -0
- package/service-connectors/wolfram-alpha/connector.json +26 -0
- package/service-connectors/youtube-data/connector.json +30 -0
- 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 +570 -0
- package/src/agents/openclaw/openclawProvider.ts +259 -0
- package/src/agents/openclaw/sshTunnelManager.ts +393 -0
- package/src/agents/types.ts +82 -0
- package/src/builders/anthropic.ts +283 -0
- package/src/builders/fireworksai.ts +59 -0
- package/src/builders/index.ts +33 -0
- package/src/builders/openai.ts +89 -0
- package/src/builders/types.ts +261 -0
- package/src/connectors/index.ts +3 -1
- package/src/connectors/registry.ts +40 -96
- package/src/connectors/types.ts +25 -0
- package/src/customizer/Customizer.ts +151 -0
- package/src/customizer/index.ts +5 -0
- package/src/files.ts +71 -0
- package/src/index.ts +2 -1
- package/src/init.ts +138 -97
- package/src/migrations.ts +148 -145
- package/src/models/anthropic.ts +119 -0
- package/src/models/chainOfThought.ts +56 -0
- package/src/models/fireworksai.ts +143 -0
- package/src/models/index.ts +7 -1
- package/src/models/logCompletePrompt.ts +25 -0
- package/src/models/openai.ts +110 -0
- package/src/models/providers.ts +12 -3
- package/src/models/types.ts +97 -2
- package/src/models/utils.ts +16 -0
- package/src/pages.ts +176 -54
- 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 +39 -8
- package/src/service/transformPage.ts +605 -301
- package/src/service/useAgentRoutes.ts +428 -0
- package/src/service/useApiRoutes.ts +309 -45
- package/src/service/useConnectorRoutes.ts +21 -10
- package/src/service/useFileRoutes.ts +127 -0
- package/src/service/usePageRoutes.ts +736 -75
- package/src/service/useSharedDataRoutes.ts +106 -0
- package/src/service/useSharedFileRoutes.ts +126 -0
- package/src/settings.ts +8 -10
- package/src/synthos-cli.ts +4 -3
- package/src/themes.ts +103 -20
- package/static-files/favicon.svg +12 -0
- package/static-files/fluentlm-instructions.llmd +868 -0
- package/static-files/fluentlm-instructions.md +1595 -0
- package/static-files/fluentlm.css +4844 -0
- package/static-files/fluentlm.js +3602 -0
- package/static-files/fluentlm.min.css +1 -0
- package/static-files/fluentlm.min.js +1 -0
- package/static-files/helpers.v3.js +304 -0
- package/static-files/page.v3.js +1290 -0
- package/static-files/recommended-frameworks.llmd +81 -0
- package/static-files/recommended-frameworks.md +137 -0
- package/static-files/retro-game.js +877 -0
- package/static-files/shell.css +797 -0
- package/static-files/theme-dark.css +169 -0
- package/static-files/theme-light.css +169 -0
- package/tests/anthropic.spec.ts +84 -0
- package/tests/builders.spec.ts +139 -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 +79 -1
- package/tests/openai.spec.ts +71 -0
- package/tests/pages.spec.ts +226 -1
- package/tests/providers.spec.ts +144 -0
- package/tests/scripts.spec.ts +209 -0
- package/tests/transformPage.spec.ts +456 -0
- package/tests/types.spec.ts +23 -0
- package/default-pages/app_builder.html +0 -40
- package/default-pages/app_builder.json +0 -1
- package/default-pages/json_tools.json +0 -1
- package/default-pages/my_notes.html +0 -33
- package/default-pages/neon_asteroids.html +0 -77
- package/default-pages/sidebar_builder.json +0 -1
- package/default-pages/solar_tutorial.json +0 -1
- package/default-pages/two-panel_builder.json +0 -1
- package/dist/connectors/index.d.ts +0 -3
- package/dist/connectors/types.d.ts +0 -61
- package/dist/index.d.ts +0 -7
- package/dist/migrations.d.ts +0 -11
- package/dist/models/providers.d.ts +0 -7
- package/dist/scripts.d.ts +0 -14
- package/dist/service/createCompletePrompt.d.ts +0 -5
- package/dist/service/debugLog.d.ts +0 -11
- package/dist/service/generateImage.d.ts +0 -32
- package/dist/service/index.d.ts +0 -8
- package/dist/service/modelInstructions.d.ts +0 -7
- package/dist/service/requiresSettings.d.ts +0 -3
- package/dist/service/server.d.ts +0 -4
- package/dist/service/useApiRoutes.d.ts +0 -4
- package/dist/service/useConnectorRoutes.d.ts +0 -4
- package/dist/service/useDataRoutes.d.ts +0 -4
- package/dist/service/usePageRoutes.d.ts +0 -5
- package/dist/synthos-cli.d.ts +0 -2
- 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/page-scripts/helpers-v2.js +0 -121
- package/page-scripts/page-v2.js +0 -615
- package/required-pages/builder.html +0 -74
- package/required-pages/builder.json +0 -1
- package/required-pages/pages.html +0 -196
- package/required-pages/pages.json +0 -1
- package/required-pages/settings.html +0 -841
- package/required-pages/settings.json +0 -1
- package/required-pages/synthos_apis.html +0 -272
- package/required-pages/synthos_apis.json +0 -1
- package/required-pages/synthos_scripts.json +0 -1
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
import path from "path";
|
|
2
|
-
import
|
|
3
|
-
import
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
import AdmZip from "adm-zip";
|
|
4
|
+
import { listPages, loadPageMetadata, PageMetadata, savePageMetadata, deletePage, copyPage, loadPageState, savePageState, clearVersions, PAGE_VERSION } from "../pages";
|
|
5
|
+
import { checkIfExists, copyFile, copyFolderRecursive, deleteFile, ensureFolderExists, findFileInFolders, listFolders, loadFile } from "../files";
|
|
4
6
|
import {getModelEntry, loadSettings, saveSettings, ServicesConfig } from "../settings";
|
|
5
7
|
import { Application } from 'express';
|
|
8
|
+
import express from 'express';
|
|
6
9
|
import { SynthOSConfig } from "../init";
|
|
7
10
|
import { createCompletePrompt, PROVIDERS } from "./createCompletePrompt";
|
|
8
11
|
import { generateDefaultImage, generateImage } from "./generateImage";
|
|
9
|
-
import { chainOfThought } from "
|
|
12
|
+
import { chainOfThought } from "../models";
|
|
10
13
|
import { requiresSettings } from "./requiresSettings";
|
|
11
14
|
import { executeScript } from "../scripts";
|
|
12
|
-
import { listThemes, loadTheme, loadThemeInfo } from "../themes";
|
|
15
|
+
import { listThemes, loadTheme, loadThemeInfo, loadThemeVersion } from "../themes";
|
|
13
16
|
import { migratePage } from "../migrations";
|
|
14
17
|
import { loadPageWithFallback } from "./usePageRoutes";
|
|
18
|
+
import { Customizer } from "../customizer";
|
|
15
19
|
|
|
16
20
|
// ---------------------------------------------------------------------------
|
|
17
21
|
// Service registry
|
|
@@ -45,18 +49,108 @@ const SERVICE_REGISTRY: ServiceDefinition[] = [
|
|
|
45
49
|
}
|
|
46
50
|
];
|
|
47
51
|
|
|
48
|
-
export function useApiRoutes(config: SynthOSConfig, app: Application): void {
|
|
52
|
+
export function useApiRoutes(config: SynthOSConfig, app: Application, customizer?: Customizer): void {
|
|
49
53
|
// List pages
|
|
50
54
|
app.get('/api/pages', async (req, res) => {
|
|
51
|
-
const pages = await listPages(config.pagesFolder, config.
|
|
55
|
+
const pages = await listPages(config.pagesFolder, config.requiredPagesFolders);
|
|
52
56
|
res.json(pages);
|
|
53
57
|
});
|
|
54
58
|
|
|
59
|
+
// Import a page from a zip file
|
|
60
|
+
app.post('/api/pages/import', express.raw({ type: 'application/zip', limit: '50mb' }), async (req, res) => {
|
|
61
|
+
try {
|
|
62
|
+
const zipBuffer = req.body as Buffer;
|
|
63
|
+
if (!zipBuffer || zipBuffer.length === 0) {
|
|
64
|
+
res.status(400).json({ error: 'Empty request body' });
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let zip: AdmZip;
|
|
69
|
+
try {
|
|
70
|
+
zip = new AdmZip(zipBuffer);
|
|
71
|
+
} catch {
|
|
72
|
+
res.status(400).json({ error: 'Invalid zip file' });
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const entries = zip.getEntries();
|
|
77
|
+
if (entries.length === 0) {
|
|
78
|
+
res.status(400).json({ error: 'Zip file is empty' });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Determine top-level folder name and validate structure
|
|
83
|
+
const firstEntry = entries[0].entryName;
|
|
84
|
+
const topFolder = firstEntry.split('/')[0];
|
|
85
|
+
const hasPageHtml = entries.some(e => e.entryName === `${topFolder}/page.html`);
|
|
86
|
+
if (!hasPageHtml) {
|
|
87
|
+
res.status(400).json({ error: 'Zip must contain a <folder>/page.html entry' });
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Sanitize page name from folder name
|
|
92
|
+
let pageName = topFolder.toLowerCase().replace(/[^a-z0-9_-]/g, '');
|
|
93
|
+
if (!pageName) {
|
|
94
|
+
res.status(400).json({ error: 'Could not derive a valid page name from zip contents' });
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Auto-append _1, _2, etc. on name conflicts
|
|
99
|
+
let finalName = pageName;
|
|
100
|
+
let suffix = 0;
|
|
101
|
+
while (await checkIfExists(path.join(config.pagesFolder, 'pages', finalName))) {
|
|
102
|
+
suffix++;
|
|
103
|
+
finalName = `${pageName}_${suffix}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const targetDir = path.join(config.pagesFolder, 'pages', finalName);
|
|
107
|
+
await ensureFolderExists(targetDir);
|
|
108
|
+
|
|
109
|
+
// Extract entries with path traversal protection
|
|
110
|
+
for (const entry of entries) {
|
|
111
|
+
if (entry.isDirectory) continue;
|
|
112
|
+
|
|
113
|
+
// Strip the top-level folder prefix to get relative path
|
|
114
|
+
const relativePath = entry.entryName.substring(topFolder.length + 1);
|
|
115
|
+
if (!relativePath) continue;
|
|
116
|
+
|
|
117
|
+
const resolvedPath = path.resolve(targetDir, relativePath);
|
|
118
|
+
if (!resolvedPath.startsWith(path.resolve(targetDir))) {
|
|
119
|
+
// Path traversal — skip this entry
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
await ensureFolderExists(path.dirname(resolvedPath));
|
|
124
|
+
await fs.writeFile(resolvedPath, entry.getData());
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Update metadata: set createdDate and lastModified to now
|
|
128
|
+
const now = new Date().toISOString();
|
|
129
|
+
const existingMeta = await loadPageMetadata(config.pagesFolder, finalName);
|
|
130
|
+
const metadata: PageMetadata = {
|
|
131
|
+
title: existingMeta?.title ?? '',
|
|
132
|
+
categories: existingMeta?.categories ?? [],
|
|
133
|
+
pinned: existingMeta?.pinned ?? false,
|
|
134
|
+
showInAll: existingMeta?.showInAll ?? true,
|
|
135
|
+
createdDate: now,
|
|
136
|
+
lastModified: now,
|
|
137
|
+
pageVersion: existingMeta?.pageVersion ?? PAGE_VERSION,
|
|
138
|
+
mode: existingMeta?.mode ?? 'unlocked',
|
|
139
|
+
};
|
|
140
|
+
await savePageMetadata(config.pagesFolder, finalName, metadata);
|
|
141
|
+
|
|
142
|
+
res.status(201).json({ name: finalName, title: metadata.title });
|
|
143
|
+
} catch (err: unknown) {
|
|
144
|
+
console.error(err);
|
|
145
|
+
res.status(500).json({ error: (err as Error).message });
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
55
149
|
// Get page metadata
|
|
56
150
|
app.get('/api/pages/:name', async (req, res) => {
|
|
57
151
|
try {
|
|
58
152
|
const { name } = req.params;
|
|
59
|
-
const metadata = await loadPageMetadata(config.pagesFolder, name, config.
|
|
153
|
+
const metadata = await loadPageMetadata(config.pagesFolder, name, config.requiredPagesFolders);
|
|
60
154
|
if (metadata) {
|
|
61
155
|
res.json(metadata);
|
|
62
156
|
} else {
|
|
@@ -107,7 +201,7 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
|
|
|
107
201
|
}
|
|
108
202
|
|
|
109
203
|
// Load existing metadata (or defaults)
|
|
110
|
-
const existing = await loadPageMetadata(config.pagesFolder, name, config.
|
|
204
|
+
const existing = await loadPageMetadata(config.pagesFolder, name, config.requiredPagesFolders);
|
|
111
205
|
const metadata: PageMetadata = {
|
|
112
206
|
title: existing?.title ?? '',
|
|
113
207
|
categories: existing?.categories ?? [],
|
|
@@ -133,7 +227,11 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
|
|
|
133
227
|
if (metadata.mode !== 'locked') {
|
|
134
228
|
const userPagePath = path.join(config.pagesFolder, 'pages', name, 'page.html');
|
|
135
229
|
if (!(await checkIfExists(userPagePath))) {
|
|
136
|
-
|
|
230
|
+
let html: string | undefined;
|
|
231
|
+
for (const folder of config.requiredPagesFolders) {
|
|
232
|
+
html = await loadPageState(folder, name);
|
|
233
|
+
if (html) break;
|
|
234
|
+
}
|
|
137
235
|
if (html) {
|
|
138
236
|
await savePageState(config.pagesFolder, name, html);
|
|
139
237
|
}
|
|
@@ -159,7 +257,7 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
|
|
|
159
257
|
}
|
|
160
258
|
|
|
161
259
|
// Load existing metadata (user override → fallback .json → defaults)
|
|
162
|
-
let metadata = await loadPageMetadata(config.pagesFolder, name, config.
|
|
260
|
+
let metadata = await loadPageMetadata(config.pagesFolder, name, config.requiredPagesFolders);
|
|
163
261
|
if (!metadata) {
|
|
164
262
|
metadata = {
|
|
165
263
|
title: '',
|
|
@@ -188,7 +286,7 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
|
|
|
188
286
|
const { name } = req.params;
|
|
189
287
|
|
|
190
288
|
// Cannot delete required pages
|
|
191
|
-
if (
|
|
289
|
+
if (config.requiredPages.includes(name)) {
|
|
192
290
|
res.status(400).json({ error: `Cannot delete required page "${name}"` });
|
|
193
291
|
return;
|
|
194
292
|
}
|
|
@@ -210,11 +308,56 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
|
|
|
210
308
|
}
|
|
211
309
|
});
|
|
212
310
|
|
|
311
|
+
// Discover what a page contains (tables + files)
|
|
312
|
+
app.get('/api/pages/:name/contents', async (req, res) => {
|
|
313
|
+
try {
|
|
314
|
+
const { name } = req.params;
|
|
315
|
+
|
|
316
|
+
// Resolve page folder: user pages first, then required pages
|
|
317
|
+
let pageFolder: string | undefined;
|
|
318
|
+
const userFolder = path.join(config.pagesFolder, 'pages', name);
|
|
319
|
+
if (await checkIfExists(path.join(userFolder, 'page.html'))) {
|
|
320
|
+
pageFolder = userFolder;
|
|
321
|
+
} else {
|
|
322
|
+
for (const folder of config.requiredPagesFolders) {
|
|
323
|
+
const candidate = path.join(folder, name);
|
|
324
|
+
if (await checkIfExists(path.join(candidate, 'page.html'))) {
|
|
325
|
+
pageFolder = candidate;
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (!pageFolder) {
|
|
332
|
+
res.status(404).json({ error: `Page "${name}" not found` });
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// List subdirectories, filtering out non-table entries
|
|
337
|
+
const EXCLUDED = new Set(['files']);
|
|
338
|
+
const subdirs = await listFolders(pageFolder);
|
|
339
|
+
const tables = subdirs.filter(d => !EXCLUDED.has(d));
|
|
340
|
+
|
|
341
|
+
// Check if files/ exists and has entries
|
|
342
|
+
const filesDir = path.join(pageFolder, 'files');
|
|
343
|
+
let hasFiles = false;
|
|
344
|
+
if (await checkIfExists(filesDir)) {
|
|
345
|
+
const entries = await fs.readdir(filesDir);
|
|
346
|
+
hasFiles = entries.length > 0;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
res.json({ tables, hasFiles });
|
|
350
|
+
} catch (err: unknown) {
|
|
351
|
+
console.error(err);
|
|
352
|
+
res.status(500).json({ error: (err as Error).message });
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
|
|
213
356
|
// Copy a page to a new name
|
|
214
357
|
app.post('/api/pages/:name/copy', async (req, res) => {
|
|
215
358
|
try {
|
|
216
359
|
const sourceName = req.params.name;
|
|
217
|
-
const { name: targetName, title, categories } = req.body;
|
|
360
|
+
const { name: targetName, title, categories, copyTables, copyFiles } = req.body;
|
|
218
361
|
|
|
219
362
|
// Validate target name
|
|
220
363
|
if (!targetName || typeof targetName !== 'string') {
|
|
@@ -236,10 +379,14 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
|
|
|
236
379
|
// Check source exists (user pages → required pages)
|
|
237
380
|
const sourceFolderPath = path.join(config.pagesFolder, 'pages', sourceName, 'page.html');
|
|
238
381
|
const sourceFlatPath = path.join(config.pagesFolder, `${sourceName}.html`);
|
|
239
|
-
|
|
382
|
+
let sourceRequiredPath: string | undefined;
|
|
383
|
+
for (const folder of config.requiredPagesFolders) {
|
|
384
|
+
const candidate = path.join(folder, sourceName, 'page.html');
|
|
385
|
+
if (await checkIfExists(candidate)) { sourceRequiredPath = candidate; break; }
|
|
386
|
+
}
|
|
240
387
|
const sourceExists = await checkIfExists(sourceFolderPath)
|
|
241
388
|
|| await checkIfExists(sourceFlatPath)
|
|
242
|
-
||
|
|
389
|
+
|| !!sourceRequiredPath;
|
|
243
390
|
if (!sourceExists) {
|
|
244
391
|
res.status(404).json({ error: `Source page "${sourceName}" not found` });
|
|
245
392
|
return;
|
|
@@ -259,7 +406,11 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
|
|
|
259
406
|
targetName,
|
|
260
407
|
typeof title === 'string' ? title : '',
|
|
261
408
|
Array.isArray(categories) ? categories : [],
|
|
262
|
-
config.
|
|
409
|
+
config.requiredPagesFolders,
|
|
410
|
+
{
|
|
411
|
+
copyTables: copyTables === true,
|
|
412
|
+
copyFiles: copyFiles !== false, // default true
|
|
413
|
+
}
|
|
263
414
|
);
|
|
264
415
|
|
|
265
416
|
// Return the new page metadata
|
|
@@ -286,9 +437,6 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
|
|
|
286
437
|
if (Array.isArray(settings.models)) {
|
|
287
438
|
for (const entry of settings.models) {
|
|
288
439
|
if (entry.configuration) {
|
|
289
|
-
if (typeof entry.configuration.maxTokens === 'string') {
|
|
290
|
-
entry.configuration.maxTokens = parseInt(entry.configuration.maxTokens);
|
|
291
|
-
}
|
|
292
440
|
}
|
|
293
441
|
if (typeof entry.logCompletions === 'string') {
|
|
294
442
|
entry.logCompletions = entry.logCompletions === 'true';
|
|
@@ -326,9 +474,8 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
|
|
|
326
474
|
app.post('/api/generate/completion', async (req, res) => {
|
|
327
475
|
await requiresSettings(res, config.pagesFolder, async (settings) => {
|
|
328
476
|
const { prompt, temperature } = req.body;
|
|
329
|
-
const maxTokens = getModelEntry(settings, 'chat').configuration.maxTokens;
|
|
330
477
|
const completePrompt = await createCompletePrompt(config.pagesFolder, 'chat', req.body.model);
|
|
331
|
-
const response = await chainOfThought({ question: prompt, temperature,
|
|
478
|
+
const response = await chainOfThought({ question: prompt, temperature, completePrompt });
|
|
332
479
|
if (response.completed) {
|
|
333
480
|
res.json(response.value ?? {});
|
|
334
481
|
} else {
|
|
@@ -339,31 +486,42 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
|
|
|
339
486
|
});
|
|
340
487
|
|
|
341
488
|
// Brainstorm endpoint
|
|
489
|
+
if (!customizer || customizer.isEnabled('brainstorm'))
|
|
342
490
|
app.post('/api/brainstorm', async (req, res) => {
|
|
343
491
|
await requiresSettings(res, config.pagesFolder, async (settings) => {
|
|
344
492
|
const { context, messages } = req.body;
|
|
345
|
-
const maxTokens = getModelEntry(settings, 'chat').configuration.maxTokens;
|
|
346
493
|
const completePrompt = await createCompletePrompt(config.pagesFolder, 'chat');
|
|
347
494
|
|
|
495
|
+
const productName = customizer?.productName ?? 'SynthOS';
|
|
348
496
|
const system: { role: 'system'; content: string } = {
|
|
349
497
|
role: 'system',
|
|
350
|
-
content: `You are a creative brainstorming assistant for
|
|
498
|
+
content: `You are a creative brainstorming assistant for ${productName}, a tool that builds pages through conversation.
|
|
499
|
+
${productName} is like a WIKI for vibe coding. Each page has a chat panel and a viewer panel. They are vibe coding what's displayed in that viewer panel. They can then save that as a page.
|
|
500
|
+
The user is brainstorming — exploring ideas before building. Be concise, creative, and collaborative.
|
|
501
|
+
They may say that they want to build an app or page that does XYZ but they're talking about what they expect to see in the viewer panel.
|
|
502
|
+
The goal is to help them generate a prompt for the builder that captures their vision, along with suggestions for next steps.
|
|
503
|
+
Suggest concrete approaches when you can, not complex visions for some ellaborate app.
|
|
504
|
+
Just help expand their thoughts into a great next prompt.
|
|
505
|
+
|
|
506
|
+
<CONTEXT>
|
|
507
|
+
${context}
|
|
508
|
+
|
|
509
|
+
<INSTRUCTIONS>
|
|
510
|
+
Look at the <CHAT_HISTORY> and if it's empty it's the start of a new idea. Simply greet them and ask them what they're thinking of building. Suggestions could be help me decide, etc.
|
|
511
|
+
If you see a conversation between ${productName} and the User. Asses what they're building and ask them what they'd like help with. Maybe offer a few good next steps.
|
|
512
|
+
|
|
513
|
+
${productName} exposes table storage and chat completion api's that every page can use. If the user wants to store something or use AI, your prompt should suggest using table storage or make llm calls.
|
|
351
514
|
|
|
352
515
|
You MUST return your response as a JSON object with exactly these fields:
|
|
353
516
|
{
|
|
354
517
|
"response": "Your conversational reply — explanations, options, suggestions. Markdown OK.",
|
|
355
|
-
"prompt": "A clean, actionable instruction ready to paste into
|
|
518
|
+
"prompt": "A clean, actionable instruction ready to paste into ${productName} chat to build what was discussed. Update this each exchange to reflect the latest brainstorm state.",
|
|
356
519
|
"suggestions": ["Short clickable option A", "Short clickable option B", "Short clickable option C"]
|
|
357
520
|
}
|
|
358
521
|
|
|
359
522
|
suggestions — 2-4 short phrases the user can click to continue the conversation. These are next-step options: directions to explore, questions to answer, or choices to make. Keep each under 60 characters. Always provide suggestions.
|
|
360
523
|
|
|
361
|
-
Return ONLY the JSON object
|
|
362
|
-
|
|
363
|
-
<CONTEXT>
|
|
364
|
-
${context}
|
|
365
|
-
</CONTEXT>`
|
|
366
|
-
};
|
|
524
|
+
Return ONLY the JSON object.`};
|
|
367
525
|
|
|
368
526
|
// Format multi-turn conversation into a single prompt
|
|
369
527
|
const formatted = (messages as { role: string; content: string }[]).map(m =>
|
|
@@ -372,7 +530,7 @@ ${context}
|
|
|
372
530
|
|
|
373
531
|
const prompt: { role: 'user'; content: string } = { role: 'user', content: formatted };
|
|
374
532
|
|
|
375
|
-
const result = await completePrompt({ prompt, system,
|
|
533
|
+
const result = await completePrompt({ prompt, system, jsonMode: true });
|
|
376
534
|
if (result.completed) {
|
|
377
535
|
let response = result.value || '';
|
|
378
536
|
let brainstormPrompt = '';
|
|
@@ -397,6 +555,7 @@ ${context}
|
|
|
397
555
|
});
|
|
398
556
|
|
|
399
557
|
// Define a route for running configured scripts
|
|
558
|
+
if (!customizer || customizer.isEnabled('scripts'))
|
|
400
559
|
app.post('/api/scripts/:id', async (req, res) => {
|
|
401
560
|
await requiresSettings(res, config.pagesFolder, async (settings) => {
|
|
402
561
|
const { id } = req.params;
|
|
@@ -425,7 +584,13 @@ ${context}
|
|
|
425
584
|
res.status(404).send(`// Theme info for "${themeName}" not found`);
|
|
426
585
|
return;
|
|
427
586
|
}
|
|
428
|
-
const
|
|
587
|
+
const themeVersion = await loadThemeVersion(themeName, config);
|
|
588
|
+
const payload = { ...info, name: themeName, version: themeVersion };
|
|
589
|
+
let js = `window.themeInfo=${JSON.stringify(payload)};document.documentElement.classList.add(window.themeInfo.mode+"-mode");`;
|
|
590
|
+
if (themeVersion >= 3) {
|
|
591
|
+
js += `document.documentElement.classList.add(${JSON.stringify(themeName)});`;
|
|
592
|
+
}
|
|
593
|
+
js += `document.documentElement.setAttribute("data-toolbar",${JSON.stringify(settings.toolbarPosition || 'left')});`;
|
|
429
594
|
res.set('Content-Type', 'application/javascript');
|
|
430
595
|
res.send(js);
|
|
431
596
|
} catch (err: unknown) {
|
|
@@ -442,21 +607,18 @@ ${context}
|
|
|
442
607
|
res.status(400).send('// Missing page query parameter');
|
|
443
608
|
return;
|
|
444
609
|
}
|
|
445
|
-
const metadata = await loadPageMetadata(config.pagesFolder, page, config.
|
|
610
|
+
const metadata = await loadPageMetadata(config.pagesFolder, page, config.requiredPagesFolders);
|
|
446
611
|
const mode = metadata?.mode ?? 'unlocked';
|
|
447
612
|
const title = metadata?.title ?? '';
|
|
448
613
|
const categories = metadata?.categories ?? [];
|
|
449
|
-
const
|
|
614
|
+
const isRequiredPage = config.requiredPages.includes(page);
|
|
615
|
+
const productName = customizer?.productName ?? 'SynthOS';
|
|
616
|
+
const info = JSON.stringify({ name: page, mode, latestPageVersion: PAGE_VERSION, title, categories, isRequiredPage, productName });
|
|
450
617
|
const js = [
|
|
451
618
|
`window.pageInfo=${info};`,
|
|
452
619
|
`if(window.pageInfo.mode==="locked"){`,
|
|
453
620
|
`document.addEventListener("DOMContentLoaded",function(){`,
|
|
454
621
|
`var f=document.getElementById("chatForm");if(f)f.style.display="none";`,
|
|
455
|
-
`var s=document.getElementById("saveLink");if(s)s.textContent="Copy";`,
|
|
456
|
-
`var r=document.getElementById("resetLink");if(r){`,
|
|
457
|
-
`var c=r.cloneNode(true);c.textContent="Reload";`,
|
|
458
|
-
`c.addEventListener("click",function(e){e.preventDefault();window.location.href=window.location.pathname;});`,
|
|
459
|
-
`r.parentNode.replaceChild(c,r);}`,
|
|
460
622
|
`});`,
|
|
461
623
|
`}`,
|
|
462
624
|
].join('');
|
|
@@ -506,8 +668,8 @@ ${context}
|
|
|
506
668
|
res.status(400).send('// Invalid version parameter');
|
|
507
669
|
return;
|
|
508
670
|
}
|
|
509
|
-
const scriptPath =
|
|
510
|
-
if (!
|
|
671
|
+
const scriptPath = await findFileInFolders(config.staticFilesFolders, `page.v${v}.js`);
|
|
672
|
+
if (!scriptPath) {
|
|
511
673
|
res.status(404).send(`// page-v${v}.js not found`);
|
|
512
674
|
return;
|
|
513
675
|
}
|
|
@@ -529,8 +691,8 @@ ${context}
|
|
|
529
691
|
res.status(400).send('// Invalid version parameter');
|
|
530
692
|
return;
|
|
531
693
|
}
|
|
532
|
-
const scriptPath =
|
|
533
|
-
if (!
|
|
694
|
+
const scriptPath = await findFileInFolders(config.staticFilesFolders, `helpers.v${v}.js`);
|
|
695
|
+
if (!scriptPath) {
|
|
534
696
|
res.status(404).send(`// helpers-v${v}.js not found`);
|
|
535
697
|
return;
|
|
536
698
|
}
|
|
@@ -609,6 +771,7 @@ ${context}
|
|
|
609
771
|
// Web Search (Brave Search API)
|
|
610
772
|
// -----------------------------------------------------------------------
|
|
611
773
|
|
|
774
|
+
if (!customizer || customizer.isEnabled('search'))
|
|
612
775
|
app.post('/api/search/web', async (req, res) => {
|
|
613
776
|
try {
|
|
614
777
|
const { query, count, country, freshness } = req.body;
|
|
@@ -663,7 +826,7 @@ ${context}
|
|
|
663
826
|
const { name } = req.params;
|
|
664
827
|
|
|
665
828
|
// Load current metadata
|
|
666
|
-
const metadata = await loadPageMetadata(config.pagesFolder, name, config.
|
|
829
|
+
const metadata = await loadPageMetadata(config.pagesFolder, name, config.requiredPagesFolders);
|
|
667
830
|
if (!metadata) {
|
|
668
831
|
res.status(404).json({ error: `Page "${name}" not found` });
|
|
669
832
|
return;
|
|
@@ -689,14 +852,25 @@ ${context}
|
|
|
689
852
|
// Save upgraded HTML to v2 folder structure
|
|
690
853
|
await savePageState(config.pagesFolder, name, migratedHtml);
|
|
691
854
|
|
|
692
|
-
//
|
|
855
|
+
// Backup original page to .migrated/ before overwriting
|
|
856
|
+
const migratedFolder = path.join(config.pagesFolder, '.migrated');
|
|
857
|
+
|
|
858
|
+
// Handle legacy flat file (<localFolder>/pagename.html)
|
|
693
859
|
const flatPath = path.join(config.pagesFolder, `${name}.html`);
|
|
694
860
|
if (await checkIfExists(flatPath)) {
|
|
695
|
-
const migratedFolder = path.join(config.pagesFolder, '.migrated');
|
|
696
861
|
await copyFile(flatPath, migratedFolder);
|
|
697
862
|
await deleteFile(flatPath);
|
|
698
863
|
}
|
|
699
864
|
|
|
865
|
+
// Handle folder-based page (<localFolder>/pages/name/)
|
|
866
|
+
const folderPath = path.join(config.pagesFolder, 'pages', name);
|
|
867
|
+
if (await checkIfExists(folderPath)) {
|
|
868
|
+
await copyFolderRecursive(folderPath, path.join(migratedFolder, name));
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// Clear stale version files (undo snapshots from the old page version)
|
|
872
|
+
await clearVersions(config.pagesFolder, name);
|
|
873
|
+
|
|
700
874
|
// Update metadata
|
|
701
875
|
metadata.pageVersion = PAGE_VERSION;
|
|
702
876
|
metadata.lastModified = new Date().toISOString();
|
|
@@ -708,4 +882,94 @@ ${context}
|
|
|
708
882
|
res.status(500).json({ error: (err as Error).message });
|
|
709
883
|
}
|
|
710
884
|
});
|
|
885
|
+
|
|
886
|
+
// Export a page as a zip file
|
|
887
|
+
app.get('/api/pages/:name/export', async (req, res) => {
|
|
888
|
+
try {
|
|
889
|
+
const { name } = req.params;
|
|
890
|
+
|
|
891
|
+
// Try user pages folder first, then required pages
|
|
892
|
+
const userPageDir = path.join(config.pagesFolder, 'pages', name);
|
|
893
|
+
let requiredPageDir: string | undefined;
|
|
894
|
+
for (const folder of config.requiredPagesFolders) {
|
|
895
|
+
if (await checkIfExists(path.join(folder, name, 'page.html'))) {
|
|
896
|
+
requiredPageDir = path.join(folder, name);
|
|
897
|
+
break;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
let sourceDir: string | null = null;
|
|
901
|
+
|
|
902
|
+
if (await checkIfExists(path.join(userPageDir, 'page.html'))) {
|
|
903
|
+
sourceDir = userPageDir;
|
|
904
|
+
} else if (requiredPageDir) {
|
|
905
|
+
// For required pages, create a temp-like zip with just the HTML
|
|
906
|
+
const zip = new AdmZip();
|
|
907
|
+
const html = await loadFile(path.join(requiredPageDir, 'page.html'));
|
|
908
|
+
zip.addFile(`${name}/page.html`, Buffer.from(html, 'utf-8'));
|
|
909
|
+
|
|
910
|
+
// Include page.json if it exists
|
|
911
|
+
const metaPath = path.join(requiredPageDir, 'page.json');
|
|
912
|
+
if (await checkIfExists(metaPath)) {
|
|
913
|
+
const meta = await loadFile(metaPath);
|
|
914
|
+
zip.addFile(`${name}/page.json`, Buffer.from(meta, 'utf-8'));
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
const zipBuffer = zip.toBuffer();
|
|
918
|
+
res.set('Content-Type', 'application/zip');
|
|
919
|
+
res.set('Content-Disposition', `attachment; filename="${name}.zip"`);
|
|
920
|
+
res.send(zipBuffer);
|
|
921
|
+
return;
|
|
922
|
+
} else {
|
|
923
|
+
res.status(404).json({ error: `Page "${name}" not found` });
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// Zip the entire page folder
|
|
928
|
+
const zip = new AdmZip();
|
|
929
|
+
zip.addLocalFolder(sourceDir, name);
|
|
930
|
+
const zipBuffer = zip.toBuffer();
|
|
931
|
+
|
|
932
|
+
res.set('Content-Type', 'application/zip');
|
|
933
|
+
res.set('Content-Disposition', `attachment; filename="${name}.zip"`);
|
|
934
|
+
res.send(zipBuffer);
|
|
935
|
+
} catch (err: unknown) {
|
|
936
|
+
console.error(err);
|
|
937
|
+
res.status(500).json({ error: (err as Error).message });
|
|
938
|
+
}
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
// Ask a question about a page (with full page HTML context)
|
|
942
|
+
app.post('/api/pages/:name/ask', async (req, res) => {
|
|
943
|
+
await requiresSettings(res, config.pagesFolder, async (settings) => {
|
|
944
|
+
const { name } = req.params;
|
|
945
|
+
const { question } = req.body;
|
|
946
|
+
if (typeof question !== 'string' || !question.trim()) {
|
|
947
|
+
res.status(400).json({ error: 'question is required' });
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// Load the page HTML
|
|
952
|
+
const html = await loadPageWithFallback(name, config, false);
|
|
953
|
+
if (!html) {
|
|
954
|
+
res.status(404).json({ error: `Page "${name}" not found` });
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// Create completion (uses 'chat' model, not 'builder')
|
|
959
|
+
const complete = await createCompletePrompt(config.pagesFolder, 'chat', req.body.model);
|
|
960
|
+
|
|
961
|
+
const system = {
|
|
962
|
+
role: 'system' as const,
|
|
963
|
+
content: `You are a helpful assistant. The user will ask questions about a web page. Answer based on the page content provided.\n\n<PAGE_HTML>\n${html}`
|
|
964
|
+
};
|
|
965
|
+
const prompt = { role: 'user' as const, content: question };
|
|
966
|
+
|
|
967
|
+
const result = await complete({ system, prompt });
|
|
968
|
+
if (result.completed) {
|
|
969
|
+
res.json({ answer: result.value });
|
|
970
|
+
} else {
|
|
971
|
+
res.status(500).json({ error: result.error?.message || 'Completion failed' });
|
|
972
|
+
}
|
|
973
|
+
});
|
|
974
|
+
});
|
|
711
975
|
}
|
|
@@ -2,7 +2,7 @@ import { Application } from 'express';
|
|
|
2
2
|
import { SynthOSConfig } from '../init';
|
|
3
3
|
import { loadSettings, saveSettings } from '../settings';
|
|
4
4
|
import {
|
|
5
|
-
|
|
5
|
+
getConnectorRegistry,
|
|
6
6
|
ConnectorSummary,
|
|
7
7
|
ConnectorDetail,
|
|
8
8
|
ConnectorCallRequest,
|
|
@@ -21,7 +21,7 @@ export function useConnectorRoutes(config: SynthOSConfig, app: Application): voi
|
|
|
21
21
|
const categoryFilter = req.query.category as string | undefined;
|
|
22
22
|
const idFilter = req.query.id as string | undefined;
|
|
23
23
|
|
|
24
|
-
const list: ConnectorSummary[] =
|
|
24
|
+
const list: ConnectorSummary[] = getConnectorRegistry(config.serviceConnectorsFolders)
|
|
25
25
|
.filter(def => {
|
|
26
26
|
if (categoryFilter && def.category !== categoryFilter) return false;
|
|
27
27
|
if (idFilter && def.id !== idFilter) return false;
|
|
@@ -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
|
|
|
@@ -52,7 +54,7 @@ export function useConnectorRoutes(config: SynthOSConfig, app: Application): voi
|
|
|
52
54
|
app.get('/api/connectors/:id', async (req, res) => {
|
|
53
55
|
try {
|
|
54
56
|
const { id } = req.params;
|
|
55
|
-
const def =
|
|
57
|
+
const def = getConnectorRegistry(config.serviceConnectorsFolders).find(d => d.id === id);
|
|
56
58
|
if (!def) {
|
|
57
59
|
res.status(404).json({ error: `Connector "${id}" not found` });
|
|
58
60
|
return;
|
|
@@ -86,7 +88,7 @@ export function useConnectorRoutes(config: SynthOSConfig, app: Application): voi
|
|
|
86
88
|
app.post('/api/connectors/:id', async (req, res) => {
|
|
87
89
|
try {
|
|
88
90
|
const { id } = req.params;
|
|
89
|
-
const def =
|
|
91
|
+
const def = getConnectorRegistry(config.serviceConnectorsFolders).find(d => d.id === id);
|
|
90
92
|
if (!def) {
|
|
91
93
|
res.status(404).json({ error: `Connector "${id}" not found` });
|
|
92
94
|
return;
|
|
@@ -162,7 +164,7 @@ export function useConnectorRoutes(config: SynthOSConfig, app: Application): voi
|
|
|
162
164
|
app.get('/api/connectors/:id/authorize', async (req, res) => {
|
|
163
165
|
try {
|
|
164
166
|
const { id } = req.params;
|
|
165
|
-
const def =
|
|
167
|
+
const def = getConnectorRegistry(config.serviceConnectorsFolders).find(d => d.id === id);
|
|
166
168
|
if (!def || def.authStrategy !== 'oauth2') {
|
|
167
169
|
res.status(400).json({ error: `Connector "${id}" is not an OAuth2 connector` });
|
|
168
170
|
return;
|
|
@@ -212,7 +214,7 @@ export function useConnectorRoutes(config: SynthOSConfig, app: Application): voi
|
|
|
212
214
|
const state = JSON.parse(stateRaw) as { connector: string };
|
|
213
215
|
const connectorId = state.connector;
|
|
214
216
|
|
|
215
|
-
const def =
|
|
217
|
+
const def = getConnectorRegistry(config.serviceConnectorsFolders).find(d => d.id === connectorId);
|
|
216
218
|
if (!def || def.authStrategy !== 'oauth2') {
|
|
217
219
|
res.status(400).json({ error: `Unknown OAuth2 connector: ${connectorId}` });
|
|
218
220
|
return;
|
|
@@ -324,7 +326,7 @@ export function useConnectorRoutes(config: SynthOSConfig, app: Application): voi
|
|
|
324
326
|
return;
|
|
325
327
|
}
|
|
326
328
|
|
|
327
|
-
const def =
|
|
329
|
+
const def = getConnectorRegistry(config.serviceConnectorsFolders).find(d => d.id === request.connector);
|
|
328
330
|
if (!def) {
|
|
329
331
|
res.status(404).json({ error: `Connector "${request.connector}" not found` });
|
|
330
332
|
return;
|
|
@@ -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);
|