synthos 0.8.0 → 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 +1 -1
- 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.html → oregon_trail/page.html} +14 -12
- package/default-pages/{oregon_trail.json → oregon_trail/page.json} +2 -2
- package/default-pages/retro_game_starter/page.html +1308 -0
- package/default-pages/retro_game_starter/page.json +12 -0
- package/default-pages/{sidebar_page.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} +14 -11
- package/default-pages/{solar_explorer.json → solar_explorer/page.json} +2 -2
- 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_page.html → two-panel_page/page.html} +13 -11
- package/default-pages/two-panel_page/page.json +10 -0
- package/default-pages/{us_map.html → us_map/page.html} +193 -192
- package/default-pages/{us_map.json → us_map/page.json} +12 -12
- package/default-pages/{us_map_1850.html → us_map_1850/page.html} +326 -325
- package/default-pages/{us_map_1850.json → us_map_1850/page.json} +12 -12
- package/default-pages/{western_cities_1850.html → western_cities_1850/page.html} +527 -526
- package/default-pages/{western_cities_1850.json → western_cities_1850/page.json} +12 -12
- 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.v2.css +110 -0
- package/default-themes/nebula-dawn.v3.css +199 -0
- package/default-themes/nebula-dusk.v2.css +104 -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/index.d.ts +1 -1
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/index.js +2 -1
- package/dist/agents/index.js.map +1 -1
- package/dist/agents/openclaw/gatewayManager.d.ts +4 -0
- package/dist/agents/openclaw/gatewayManager.d.ts.map +1 -1
- package/dist/agents/openclaw/gatewayManager.js +27 -11
- package/dist/agents/openclaw/gatewayManager.js.map +1 -1
- package/dist/agents/openclaw/openclawProvider.d.ts.map +1 -1
- package/dist/agents/openclaw/openclawProvider.js +2 -4
- package/dist/agents/openclaw/openclawProvider.js.map +1 -1
- package/dist/agents/openclaw/sshTunnelManager.d.ts +2 -0
- package/dist/agents/openclaw/sshTunnelManager.d.ts.map +1 -1
- package/dist/agents/openclaw/sshTunnelManager.js +31 -12
- package/dist/agents/openclaw/sshTunnelManager.js.map +1 -1
- 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 +31 -8
- package/dist/connectors/registry.js.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 +16 -0
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +60 -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 +96 -113
- package/dist/init.js.map +1 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +23 -10
- package/dist/migrations.js.map +1 -1
- package/dist/models/anthropic.d.ts +4 -2
- package/dist/models/anthropic.d.ts.map +1 -1
- package/dist/models/anthropic.js +33 -6
- package/dist/models/anthropic.js.map +1 -1
- package/dist/models/fireworksai.d.ts.map +1 -1
- package/dist/models/fireworksai.js +9 -1
- package/dist/models/fireworksai.js.map +1 -1
- package/dist/models/index.d.ts +1 -1
- package/dist/models/index.d.ts.map +1 -1
- package/dist/models/index.js +2 -1
- package/dist/models/index.js.map +1 -1
- package/dist/models/openai.d.ts +1 -1
- package/dist/models/openai.d.ts.map +1 -1
- package/dist/models/openai.js +24 -3
- package/dist/models/openai.js.map +1 -1
- package/dist/models/types.d.ts +20 -1
- package/dist/models/types.d.ts.map +1 -1
- package/dist/models/types.js +6 -1
- package/dist/models/types.js.map +1 -1
- 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/service/server.d.ts.map +1 -1
- package/dist/service/server.js +37 -8
- package/dist/service/server.js.map +1 -1
- package/dist/service/transformPage.d.ts +47 -20
- package/dist/service/transformPage.d.ts.map +1 -1
- package/dist/service/transformPage.js +514 -293
- package/dist/service/transformPage.js.map +1 -1
- package/dist/service/useAgentRoutes.d.ts +2 -1
- package/dist/service/useAgentRoutes.d.ts.map +1 -1
- package/dist/service/useAgentRoutes.js +5 -2
- package/dist/service/useAgentRoutes.js.map +1 -1
- package/dist/service/useApiRoutes.d.ts.map +1 -1
- package/dist/service/useApiRoutes.js +237 -136
- package/dist/service/useApiRoutes.js.map +1 -1
- package/dist/service/useConnectorRoutes.js +6 -6
- 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 +648 -67
- 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 +1 -0
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +1 -0
- 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 +1 -0
- package/dist/themes.d.ts.map +1 -1
- package/dist/themes.js +28 -15
- 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 +11 -10
- package/required-pages/builder/page.html +43 -0
- package/required-pages/builder/page.json +10 -0
- package/required-pages/{pages.html → pages/page.html} +238 -233
- package/required-pages/pages/page.json +10 -0
- package/required-pages/{settings.html → settings/page.html} +389 -275
- 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/src/agents/index.ts +1 -1
- package/src/agents/openclaw/gatewayManager.ts +22 -11
- package/src/agents/openclaw/openclawProvider.ts +2 -4
- package/src/agents/openclaw/sshTunnelManager.ts +19 -11
- 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 +1 -1
- package/src/connectors/registry.ts +28 -8
- package/src/customizer/Customizer.ts +151 -0
- package/src/customizer/index.ts +5 -0
- package/src/files.ts +57 -0
- package/src/index.ts +2 -1
- package/src/init.ts +137 -123
- package/src/migrations.ts +30 -10
- package/src/models/anthropic.ts +40 -10
- package/src/models/fireworksai.ts +9 -2
- package/src/models/index.ts +1 -1
- package/src/models/openai.ts +26 -6
- package/src/models/types.ts +31 -1
- package/src/pages.ts +176 -54
- package/src/service/server.ts +36 -9
- package/src/service/transformPage.ts +557 -326
- package/src/service/useAgentRoutes.ts +7 -2
- package/src/service/useApiRoutes.ts +150 -41
- package/src/service/useConnectorRoutes.ts +7 -7
- package/src/service/useFileRoutes.ts +127 -0
- package/src/service/usePageRoutes.ts +720 -73
- package/src/service/useSharedDataRoutes.ts +106 -0
- package/src/service/useSharedFileRoutes.ts +126 -0
- package/src/settings.ts +2 -0
- package/src/synthos-cli.ts +4 -3
- package/src/themes.ts +25 -14
- 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/{page-scripts/helpers-v2.js → static-files/helpers.v3.js} +82 -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/builders.spec.ts +139 -0
- package/tests/pages.spec.ts +8 -8
- package/tests/transformPage.spec.ts +299 -360
- package/default-pages/application.html +0 -40
- package/default-pages/application.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_page.json +0 -1
- package/default-pages/solar_tutorial.json +0 -1
- package/default-pages/two-panel_page.json +0 -1
- package/dist/agents/a2a/a2aProvider.d.ts +0 -3
- package/dist/agents/discovery.d.ts +0 -30
- package/dist/agents/openclaw/openclawProvider.d.ts +0 -3
- package/dist/agents/types.d.ts +0 -64
- package/dist/connectors/index.d.ts +0 -3
- package/dist/connectors/types.d.ts +0 -84
- package/dist/index.d.ts +0 -7
- package/dist/migrations.d.ts +0 -12
- package/dist/models/chainOfThought.d.ts +0 -12
- package/dist/models/fireworksai.d.ts +0 -30
- package/dist/models/logCompletePrompt.d.ts +0 -3
- package/dist/models/providers.d.ts +0 -8
- package/dist/models/utils.d.ts +0 -6
- package/dist/scripts.d.ts +0 -15
- 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/useGatewayRoutes.d.ts +0 -4
- package/dist/service/useGatewayRoutes.d.ts.map +0 -1
- package/dist/service/useGatewayRoutes.js +0 -168
- package/dist/service/useGatewayRoutes.js.map +0 -1
- package/dist/service/usePageRoutes.d.ts +0 -5
- package/dist/synthos-cli.d.ts +0 -2
- package/page-scripts/page-v2.js +0 -656
- package/required-pages/builder.html +0 -48
- package/required-pages/builder.json +0 -1
- package/required-pages/pages.json +0 -1
- package/required-pages/settings.json +0 -1
- package/required-pages/synthos_apis.html +0 -327
- package/required-pages/synthos_apis.json +0 -1
- package/required-pages/synthos_scripts.json +0 -1
- package/src/connectors/airtable/connector.json +0 -27
- package/src/connectors/alpha-vantage/connector.json +0 -26
- package/src/connectors/brave-search/connector.json +0 -26
- package/src/connectors/cloudinary/connector.json +0 -27
- package/src/connectors/deepl/connector.json +0 -28
- package/src/connectors/elevenlabs/connector.json +0 -30
- package/src/connectors/giphy/connector.json +0 -27
- package/src/connectors/github/connector.json +0 -29
- package/src/connectors/huggingface/connector.json +0 -27
- package/src/connectors/imgur/connector.json +0 -29
- package/src/connectors/instagram/connector.json +0 -43
- package/src/connectors/jira/connector.json +0 -28
- package/src/connectors/mapbox/connector.json +0 -26
- package/src/connectors/nasa/connector.json +0 -27
- package/src/connectors/newsapi/connector.json +0 -27
- package/src/connectors/notion/connector.json +0 -28
- package/src/connectors/open-exchange-rates/connector.json +0 -27
- package/src/connectors/openweathermap/connector.json +0 -26
- package/src/connectors/pexels/connector.json +0 -27
- package/src/connectors/resend/connector.json +0 -29
- package/src/connectors/rss2json/connector.json +0 -27
- package/src/connectors/sendgrid/connector.json +0 -27
- package/src/connectors/spoonacular/connector.json +0 -28
- package/src/connectors/stability-ai/connector.json +0 -27
- package/src/connectors/twilio/connector.json +0 -28
- package/src/connectors/unsplash/connector.json +0 -27
- package/src/connectors/wolfram-alpha/connector.json +0 -26
- package/src/connectors/youtube-data/connector.json +0 -30
- /package/{dist/connectors → service-connectors}/airtable/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/alpha-vantage/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/brave-search/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/cloudinary/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/deepl/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/elevenlabs/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/giphy/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/github/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/huggingface/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/imgur/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/instagram/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/jira/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/mapbox/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/nasa/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/newsapi/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/notion/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/open-exchange-rates/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/openweathermap/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/pexels/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/resend/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/rss2json/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/sendgrid/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/spoonacular/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/stability-ai/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/twilio/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/unsplash/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/wolfram-alpha/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/youtube-data/connector.json +0 -0
|
@@ -12,10 +12,14 @@ import {
|
|
|
12
12
|
disconnectAgent,
|
|
13
13
|
getAgentStatus,
|
|
14
14
|
getTunnelStatus,
|
|
15
|
+
setOpenClawDebug,
|
|
15
16
|
} from '../agents';
|
|
16
17
|
import { v4 as uuidv4 } from 'uuid';
|
|
18
|
+
import { Customizer } from '../customizer';
|
|
17
19
|
|
|
18
|
-
export function useAgentRoutes(config: SynthOSConfig, app: Application): void {
|
|
20
|
+
export function useAgentRoutes(config: SynthOSConfig, app: Application, customizer?: Customizer): void {
|
|
21
|
+
// Enable OpenClaw debug logging only when --debug is passed
|
|
22
|
+
setOpenClawDebug(config.debug);
|
|
19
23
|
|
|
20
24
|
/** Strip the token and sshTunnel.password fields, add connection/tunnel status for agent responses. */
|
|
21
25
|
function toClientAgent(agent: AgentConfig): Record<string, unknown> {
|
|
@@ -41,6 +45,7 @@ export function useAgentRoutes(config: SynthOSConfig, app: Application): void {
|
|
|
41
45
|
url: agent.url,
|
|
42
46
|
token: agent.token,
|
|
43
47
|
sshTunnel: agent.sshTunnel,
|
|
48
|
+
productName: customizer?.productName,
|
|
44
49
|
})
|
|
45
50
|
.then(() => console.log(`[Agents] Auto-connected OpenClaw agent "${agent.name}"`))
|
|
46
51
|
.catch(err => console.warn(`[Agents] Auto-connect failed for "${agent.name}": ${err instanceof Error ? err.message : err}`));
|
|
@@ -241,7 +246,7 @@ export function useAgentRoutes(config: SynthOSConfig, app: Application): void {
|
|
|
241
246
|
return;
|
|
242
247
|
}
|
|
243
248
|
|
|
244
|
-
await connectAgent({ id: agent.id, name: agent.name, url: agent.url, token: agent.token, sshTunnel: agent.sshTunnel });
|
|
249
|
+
await connectAgent({ id: agent.id, name: agent.name, url: agent.url, token: agent.token, sshTunnel: agent.sshTunnel, productName: customizer?.productName });
|
|
245
250
|
const status = getAgentStatus(agent.id);
|
|
246
251
|
res.json({ connected: status.connected, authenticated: status.authenticated });
|
|
247
252
|
} catch (err: unknown) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import fs from "fs/promises";
|
|
3
3
|
import AdmZip from "adm-zip";
|
|
4
|
-
import { listPages, loadPageMetadata, PageMetadata, savePageMetadata,
|
|
5
|
-
import { checkIfExists, copyFile, copyFolderRecursive, deleteFile, ensureFolderExists, loadFile } from "../files";
|
|
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";
|
|
6
6
|
import {getModelEntry, loadSettings, saveSettings, ServicesConfig } from "../settings";
|
|
7
7
|
import { Application } from 'express';
|
|
8
8
|
import express from 'express';
|
|
@@ -12,9 +12,10 @@ import { generateDefaultImage, generateImage } from "./generateImage";
|
|
|
12
12
|
import { chainOfThought } from "../models";
|
|
13
13
|
import { requiresSettings } from "./requiresSettings";
|
|
14
14
|
import { executeScript } from "../scripts";
|
|
15
|
-
import { listThemes, loadTheme, loadThemeInfo } from "../themes";
|
|
15
|
+
import { listThemes, loadTheme, loadThemeInfo, loadThemeVersion } from "../themes";
|
|
16
16
|
import { migratePage } from "../migrations";
|
|
17
17
|
import { loadPageWithFallback } from "./usePageRoutes";
|
|
18
|
+
import { Customizer } from "../customizer";
|
|
18
19
|
|
|
19
20
|
// ---------------------------------------------------------------------------
|
|
20
21
|
// Service registry
|
|
@@ -48,10 +49,10 @@ const SERVICE_REGISTRY: ServiceDefinition[] = [
|
|
|
48
49
|
}
|
|
49
50
|
];
|
|
50
51
|
|
|
51
|
-
export function useApiRoutes(config: SynthOSConfig, app: Application): void {
|
|
52
|
+
export function useApiRoutes(config: SynthOSConfig, app: Application, customizer?: Customizer): void {
|
|
52
53
|
// List pages
|
|
53
54
|
app.get('/api/pages', async (req, res) => {
|
|
54
|
-
const pages = await listPages(config.pagesFolder, config.
|
|
55
|
+
const pages = await listPages(config.pagesFolder, config.requiredPagesFolders);
|
|
55
56
|
res.json(pages);
|
|
56
57
|
});
|
|
57
58
|
|
|
@@ -149,7 +150,7 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
|
|
|
149
150
|
app.get('/api/pages/:name', async (req, res) => {
|
|
150
151
|
try {
|
|
151
152
|
const { name } = req.params;
|
|
152
|
-
const metadata = await loadPageMetadata(config.pagesFolder, name, config.
|
|
153
|
+
const metadata = await loadPageMetadata(config.pagesFolder, name, config.requiredPagesFolders);
|
|
153
154
|
if (metadata) {
|
|
154
155
|
res.json(metadata);
|
|
155
156
|
} else {
|
|
@@ -200,7 +201,7 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
|
|
|
200
201
|
}
|
|
201
202
|
|
|
202
203
|
// Load existing metadata (or defaults)
|
|
203
|
-
const existing = await loadPageMetadata(config.pagesFolder, name, config.
|
|
204
|
+
const existing = await loadPageMetadata(config.pagesFolder, name, config.requiredPagesFolders);
|
|
204
205
|
const metadata: PageMetadata = {
|
|
205
206
|
title: existing?.title ?? '',
|
|
206
207
|
categories: existing?.categories ?? [],
|
|
@@ -226,7 +227,11 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
|
|
|
226
227
|
if (metadata.mode !== 'locked') {
|
|
227
228
|
const userPagePath = path.join(config.pagesFolder, 'pages', name, 'page.html');
|
|
228
229
|
if (!(await checkIfExists(userPagePath))) {
|
|
229
|
-
|
|
230
|
+
let html: string | undefined;
|
|
231
|
+
for (const folder of config.requiredPagesFolders) {
|
|
232
|
+
html = await loadPageState(folder, name);
|
|
233
|
+
if (html) break;
|
|
234
|
+
}
|
|
230
235
|
if (html) {
|
|
231
236
|
await savePageState(config.pagesFolder, name, html);
|
|
232
237
|
}
|
|
@@ -252,7 +257,7 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
|
|
|
252
257
|
}
|
|
253
258
|
|
|
254
259
|
// Load existing metadata (user override → fallback .json → defaults)
|
|
255
|
-
let metadata = await loadPageMetadata(config.pagesFolder, name, config.
|
|
260
|
+
let metadata = await loadPageMetadata(config.pagesFolder, name, config.requiredPagesFolders);
|
|
256
261
|
if (!metadata) {
|
|
257
262
|
metadata = {
|
|
258
263
|
title: '',
|
|
@@ -281,7 +286,7 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
|
|
|
281
286
|
const { name } = req.params;
|
|
282
287
|
|
|
283
288
|
// Cannot delete required pages
|
|
284
|
-
if (
|
|
289
|
+
if (config.requiredPages.includes(name)) {
|
|
285
290
|
res.status(400).json({ error: `Cannot delete required page "${name}"` });
|
|
286
291
|
return;
|
|
287
292
|
}
|
|
@@ -303,11 +308,56 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
|
|
|
303
308
|
}
|
|
304
309
|
});
|
|
305
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
|
+
|
|
306
356
|
// Copy a page to a new name
|
|
307
357
|
app.post('/api/pages/:name/copy', async (req, res) => {
|
|
308
358
|
try {
|
|
309
359
|
const sourceName = req.params.name;
|
|
310
|
-
const { name: targetName, title, categories } = req.body;
|
|
360
|
+
const { name: targetName, title, categories, copyTables, copyFiles } = req.body;
|
|
311
361
|
|
|
312
362
|
// Validate target name
|
|
313
363
|
if (!targetName || typeof targetName !== 'string') {
|
|
@@ -329,10 +379,14 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
|
|
|
329
379
|
// Check source exists (user pages → required pages)
|
|
330
380
|
const sourceFolderPath = path.join(config.pagesFolder, 'pages', sourceName, 'page.html');
|
|
331
381
|
const sourceFlatPath = path.join(config.pagesFolder, `${sourceName}.html`);
|
|
332
|
-
|
|
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
|
+
}
|
|
333
387
|
const sourceExists = await checkIfExists(sourceFolderPath)
|
|
334
388
|
|| await checkIfExists(sourceFlatPath)
|
|
335
|
-
||
|
|
389
|
+
|| !!sourceRequiredPath;
|
|
336
390
|
if (!sourceExists) {
|
|
337
391
|
res.status(404).json({ error: `Source page "${sourceName}" not found` });
|
|
338
392
|
return;
|
|
@@ -352,7 +406,11 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
|
|
|
352
406
|
targetName,
|
|
353
407
|
typeof title === 'string' ? title : '',
|
|
354
408
|
Array.isArray(categories) ? categories : [],
|
|
355
|
-
config.
|
|
409
|
+
config.requiredPagesFolders,
|
|
410
|
+
{
|
|
411
|
+
copyTables: copyTables === true,
|
|
412
|
+
copyFiles: copyFiles !== false, // default true
|
|
413
|
+
}
|
|
356
414
|
);
|
|
357
415
|
|
|
358
416
|
// Return the new page metadata
|
|
@@ -428,16 +486,18 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
|
|
|
428
486
|
});
|
|
429
487
|
|
|
430
488
|
// Brainstorm endpoint
|
|
489
|
+
if (!customizer || customizer.isEnabled('brainstorm'))
|
|
431
490
|
app.post('/api/brainstorm', async (req, res) => {
|
|
432
491
|
await requiresSettings(res, config.pagesFolder, async (settings) => {
|
|
433
492
|
const { context, messages } = req.body;
|
|
434
493
|
const completePrompt = await createCompletePrompt(config.pagesFolder, 'chat');
|
|
435
494
|
|
|
495
|
+
const productName = customizer?.productName ?? 'SynthOS';
|
|
436
496
|
const system: { role: 'system'; content: string } = {
|
|
437
497
|
role: 'system',
|
|
438
|
-
content: `You are a creative brainstorming assistant for
|
|
439
|
-
|
|
440
|
-
The user is brainstorming — exploring ideas before building. Be concise, creative, and collaborative.
|
|
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.
|
|
441
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.
|
|
442
502
|
The goal is to help them generate a prompt for the builder that captures their vision, along with suggestions for next steps.
|
|
443
503
|
Suggest concrete approaches when you can, not complex visions for some ellaborate app.
|
|
@@ -448,14 +508,14 @@ ${context}
|
|
|
448
508
|
|
|
449
509
|
<INSTRUCTIONS>
|
|
450
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.
|
|
451
|
-
If you see a conversation between
|
|
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.
|
|
452
512
|
|
|
453
|
-
|
|
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.
|
|
454
514
|
|
|
455
515
|
You MUST return your response as a JSON object with exactly these fields:
|
|
456
516
|
{
|
|
457
517
|
"response": "Your conversational reply — explanations, options, suggestions. Markdown OK.",
|
|
458
|
-
"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.",
|
|
459
519
|
"suggestions": ["Short clickable option A", "Short clickable option B", "Short clickable option C"]
|
|
460
520
|
}
|
|
461
521
|
|
|
@@ -495,6 +555,7 @@ Return ONLY the JSON object.`};
|
|
|
495
555
|
});
|
|
496
556
|
|
|
497
557
|
// Define a route for running configured scripts
|
|
558
|
+
if (!customizer || customizer.isEnabled('scripts'))
|
|
498
559
|
app.post('/api/scripts/:id', async (req, res) => {
|
|
499
560
|
await requiresSettings(res, config.pagesFolder, async (settings) => {
|
|
500
561
|
const { id } = req.params;
|
|
@@ -523,7 +584,13 @@ Return ONLY the JSON object.`};
|
|
|
523
584
|
res.status(404).send(`// Theme info for "${themeName}" not found`);
|
|
524
585
|
return;
|
|
525
586
|
}
|
|
526
|
-
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')});`;
|
|
527
594
|
res.set('Content-Type', 'application/javascript');
|
|
528
595
|
res.send(js);
|
|
529
596
|
} catch (err: unknown) {
|
|
@@ -540,21 +607,18 @@ Return ONLY the JSON object.`};
|
|
|
540
607
|
res.status(400).send('// Missing page query parameter');
|
|
541
608
|
return;
|
|
542
609
|
}
|
|
543
|
-
const metadata = await loadPageMetadata(config.pagesFolder, page, config.
|
|
610
|
+
const metadata = await loadPageMetadata(config.pagesFolder, page, config.requiredPagesFolders);
|
|
544
611
|
const mode = metadata?.mode ?? 'unlocked';
|
|
545
612
|
const title = metadata?.title ?? '';
|
|
546
613
|
const categories = metadata?.categories ?? [];
|
|
547
|
-
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 });
|
|
548
617
|
const js = [
|
|
549
618
|
`window.pageInfo=${info};`,
|
|
550
619
|
`if(window.pageInfo.mode==="locked"){`,
|
|
551
620
|
`document.addEventListener("DOMContentLoaded",function(){`,
|
|
552
621
|
`var f=document.getElementById("chatForm");if(f)f.style.display="none";`,
|
|
553
|
-
`var s=document.getElementById("saveLink");if(s)s.textContent="Copy";`,
|
|
554
|
-
`var r=document.getElementById("resetLink");if(r){`,
|
|
555
|
-
`var c=r.cloneNode(true);c.textContent="Reload";`,
|
|
556
|
-
`c.addEventListener("click",function(e){e.preventDefault();window.location.href=window.location.pathname;});`,
|
|
557
|
-
`r.parentNode.replaceChild(c,r);}`,
|
|
558
622
|
`});`,
|
|
559
623
|
`}`,
|
|
560
624
|
].join('');
|
|
@@ -604,8 +668,8 @@ Return ONLY the JSON object.`};
|
|
|
604
668
|
res.status(400).send('// Invalid version parameter');
|
|
605
669
|
return;
|
|
606
670
|
}
|
|
607
|
-
const scriptPath =
|
|
608
|
-
if (!
|
|
671
|
+
const scriptPath = await findFileInFolders(config.staticFilesFolders, `page.v${v}.js`);
|
|
672
|
+
if (!scriptPath) {
|
|
609
673
|
res.status(404).send(`// page-v${v}.js not found`);
|
|
610
674
|
return;
|
|
611
675
|
}
|
|
@@ -627,8 +691,8 @@ Return ONLY the JSON object.`};
|
|
|
627
691
|
res.status(400).send('// Invalid version parameter');
|
|
628
692
|
return;
|
|
629
693
|
}
|
|
630
|
-
const scriptPath =
|
|
631
|
-
if (!
|
|
694
|
+
const scriptPath = await findFileInFolders(config.staticFilesFolders, `helpers.v${v}.js`);
|
|
695
|
+
if (!scriptPath) {
|
|
632
696
|
res.status(404).send(`// helpers-v${v}.js not found`);
|
|
633
697
|
return;
|
|
634
698
|
}
|
|
@@ -707,6 +771,7 @@ Return ONLY the JSON object.`};
|
|
|
707
771
|
// Web Search (Brave Search API)
|
|
708
772
|
// -----------------------------------------------------------------------
|
|
709
773
|
|
|
774
|
+
if (!customizer || customizer.isEnabled('search'))
|
|
710
775
|
app.post('/api/search/web', async (req, res) => {
|
|
711
776
|
try {
|
|
712
777
|
const { query, count, country, freshness } = req.body;
|
|
@@ -761,7 +826,7 @@ Return ONLY the JSON object.`};
|
|
|
761
826
|
const { name } = req.params;
|
|
762
827
|
|
|
763
828
|
// Load current metadata
|
|
764
|
-
const metadata = await loadPageMetadata(config.pagesFolder, name, config.
|
|
829
|
+
const metadata = await loadPageMetadata(config.pagesFolder, name, config.requiredPagesFolders);
|
|
765
830
|
if (!metadata) {
|
|
766
831
|
res.status(404).json({ error: `Page "${name}" not found` });
|
|
767
832
|
return;
|
|
@@ -790,19 +855,22 @@ Return ONLY the JSON object.`};
|
|
|
790
855
|
// Backup original page to .migrated/ before overwriting
|
|
791
856
|
const migratedFolder = path.join(config.pagesFolder, '.migrated');
|
|
792
857
|
|
|
793
|
-
// Handle legacy flat file (
|
|
858
|
+
// Handle legacy flat file (<localFolder>/pagename.html)
|
|
794
859
|
const flatPath = path.join(config.pagesFolder, `${name}.html`);
|
|
795
860
|
if (await checkIfExists(flatPath)) {
|
|
796
861
|
await copyFile(flatPath, migratedFolder);
|
|
797
862
|
await deleteFile(flatPath);
|
|
798
863
|
}
|
|
799
864
|
|
|
800
|
-
// Handle folder-based page (
|
|
865
|
+
// Handle folder-based page (<localFolder>/pages/name/)
|
|
801
866
|
const folderPath = path.join(config.pagesFolder, 'pages', name);
|
|
802
867
|
if (await checkIfExists(folderPath)) {
|
|
803
868
|
await copyFolderRecursive(folderPath, path.join(migratedFolder, name));
|
|
804
869
|
}
|
|
805
870
|
|
|
871
|
+
// Clear stale version files (undo snapshots from the old page version)
|
|
872
|
+
await clearVersions(config.pagesFolder, name);
|
|
873
|
+
|
|
806
874
|
// Update metadata
|
|
807
875
|
metadata.pageVersion = PAGE_VERSION;
|
|
808
876
|
metadata.lastModified = new Date().toISOString();
|
|
@@ -822,21 +890,27 @@ Return ONLY the JSON object.`};
|
|
|
822
890
|
|
|
823
891
|
// Try user pages folder first, then required pages
|
|
824
892
|
const userPageDir = path.join(config.pagesFolder, 'pages', name);
|
|
825
|
-
|
|
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
|
+
}
|
|
826
900
|
let sourceDir: string | null = null;
|
|
827
901
|
|
|
828
902
|
if (await checkIfExists(path.join(userPageDir, 'page.html'))) {
|
|
829
903
|
sourceDir = userPageDir;
|
|
830
|
-
} else if (
|
|
904
|
+
} else if (requiredPageDir) {
|
|
831
905
|
// For required pages, create a temp-like zip with just the HTML
|
|
832
906
|
const zip = new AdmZip();
|
|
833
|
-
const html = await loadFile(
|
|
907
|
+
const html = await loadFile(path.join(requiredPageDir, 'page.html'));
|
|
834
908
|
zip.addFile(`${name}/page.html`, Buffer.from(html, 'utf-8'));
|
|
835
909
|
|
|
836
910
|
// Include page.json if it exists
|
|
837
|
-
const
|
|
838
|
-
if (await checkIfExists(
|
|
839
|
-
const meta = await loadFile(
|
|
911
|
+
const metaPath = path.join(requiredPageDir, 'page.json');
|
|
912
|
+
if (await checkIfExists(metaPath)) {
|
|
913
|
+
const meta = await loadFile(metaPath);
|
|
840
914
|
zip.addFile(`${name}/page.json`, Buffer.from(meta, 'utf-8'));
|
|
841
915
|
}
|
|
842
916
|
|
|
@@ -863,4 +937,39 @@ Return ONLY the JSON object.`};
|
|
|
863
937
|
res.status(500).json({ error: (err as Error).message });
|
|
864
938
|
}
|
|
865
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
|
+
});
|
|
866
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;
|
|
@@ -54,7 +54,7 @@ export function useConnectorRoutes(config: SynthOSConfig, app: Application): voi
|
|
|
54
54
|
app.get('/api/connectors/:id', async (req, res) => {
|
|
55
55
|
try {
|
|
56
56
|
const { id } = req.params;
|
|
57
|
-
const def =
|
|
57
|
+
const def = getConnectorRegistry(config.serviceConnectorsFolders).find(d => d.id === id);
|
|
58
58
|
if (!def) {
|
|
59
59
|
res.status(404).json({ error: `Connector "${id}" not found` });
|
|
60
60
|
return;
|
|
@@ -88,7 +88,7 @@ export function useConnectorRoutes(config: SynthOSConfig, app: Application): voi
|
|
|
88
88
|
app.post('/api/connectors/:id', async (req, res) => {
|
|
89
89
|
try {
|
|
90
90
|
const { id } = req.params;
|
|
91
|
-
const def =
|
|
91
|
+
const def = getConnectorRegistry(config.serviceConnectorsFolders).find(d => d.id === id);
|
|
92
92
|
if (!def) {
|
|
93
93
|
res.status(404).json({ error: `Connector "${id}" not found` });
|
|
94
94
|
return;
|
|
@@ -164,7 +164,7 @@ export function useConnectorRoutes(config: SynthOSConfig, app: Application): voi
|
|
|
164
164
|
app.get('/api/connectors/:id/authorize', async (req, res) => {
|
|
165
165
|
try {
|
|
166
166
|
const { id } = req.params;
|
|
167
|
-
const def =
|
|
167
|
+
const def = getConnectorRegistry(config.serviceConnectorsFolders).find(d => d.id === id);
|
|
168
168
|
if (!def || def.authStrategy !== 'oauth2') {
|
|
169
169
|
res.status(400).json({ error: `Connector "${id}" is not an OAuth2 connector` });
|
|
170
170
|
return;
|
|
@@ -214,7 +214,7 @@ export function useConnectorRoutes(config: SynthOSConfig, app: Application): voi
|
|
|
214
214
|
const state = JSON.parse(stateRaw) as { connector: string };
|
|
215
215
|
const connectorId = state.connector;
|
|
216
216
|
|
|
217
|
-
const def =
|
|
217
|
+
const def = getConnectorRegistry(config.serviceConnectorsFolders).find(d => d.id === connectorId);
|
|
218
218
|
if (!def || def.authStrategy !== 'oauth2') {
|
|
219
219
|
res.status(400).json({ error: `Unknown OAuth2 connector: ${connectorId}` });
|
|
220
220
|
return;
|
|
@@ -326,7 +326,7 @@ export function useConnectorRoutes(config: SynthOSConfig, app: Application): voi
|
|
|
326
326
|
return;
|
|
327
327
|
}
|
|
328
328
|
|
|
329
|
-
const def =
|
|
329
|
+
const def = getConnectorRegistry(config.serviceConnectorsFolders).find(d => d.id === request.connector);
|
|
330
330
|
if (!def) {
|
|
331
331
|
res.status(404).json({ error: `Connector "${request.connector}" not found` });
|
|
332
332
|
return;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { Application } from 'express';
|
|
2
|
+
import express from 'express';
|
|
3
|
+
import fs from 'fs/promises';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { SynthOSConfig } from '../init';
|
|
6
|
+
import { checkIfExists, ensureFolderExists } from '../files';
|
|
7
|
+
|
|
8
|
+
export function useFileRoutes(config: SynthOSConfig, app: Application): void {
|
|
9
|
+
// List files in a page's files/ folder
|
|
10
|
+
app.get('/api/files/:page', async (req, res) => {
|
|
11
|
+
try {
|
|
12
|
+
const folder = filesFolder(config, req.params.page);
|
|
13
|
+
if (!(await checkIfExists(folder))) {
|
|
14
|
+
res.json({ files: [] });
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const entries = await fs.readdir(folder);
|
|
19
|
+
const files: { name: string; size: number }[] = [];
|
|
20
|
+
for (const entry of entries) {
|
|
21
|
+
const stat = await fs.stat(path.join(folder, entry));
|
|
22
|
+
if (stat.isFile()) {
|
|
23
|
+
files.push({ name: entry, size: stat.size });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
res.json({ files });
|
|
27
|
+
} catch (err: unknown) {
|
|
28
|
+
console.error(err);
|
|
29
|
+
res.status(500).json({ error: (err as Error).message });
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Download/serve a specific file
|
|
34
|
+
app.get('/api/files/:page/:filename', async (req, res) => {
|
|
35
|
+
try {
|
|
36
|
+
const filePath = safeFilePath(config, req.params.page, req.params.filename);
|
|
37
|
+
if (!filePath) {
|
|
38
|
+
res.status(400).json({ error: 'Invalid filename' });
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!(await checkIfExists(filePath))) {
|
|
43
|
+
res.status(404).json({ error: 'File not found' });
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
res.sendFile(filePath);
|
|
48
|
+
} catch (err: unknown) {
|
|
49
|
+
console.error(err);
|
|
50
|
+
res.status(500).json({ error: (err as Error).message });
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Upload a file (raw body + x-filename header)
|
|
55
|
+
app.post('/api/files/:page', express.raw({ type: '*/*', limit: '50mb' }), async (req, res) => {
|
|
56
|
+
try {
|
|
57
|
+
const filename = req.headers['x-filename'] as string | undefined;
|
|
58
|
+
if (!filename || filename.trim().length === 0) {
|
|
59
|
+
res.status(400).json({ error: 'x-filename header is required' });
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const filePath = safeFilePath(config, req.params.page, filename);
|
|
64
|
+
if (!filePath) {
|
|
65
|
+
res.status(400).json({ error: 'Invalid filename' });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const folder = filesFolder(config, req.params.page);
|
|
70
|
+
await ensureFolderExists(folder);
|
|
71
|
+
await fs.writeFile(filePath, req.body as Buffer);
|
|
72
|
+
|
|
73
|
+
const stat = await fs.stat(filePath);
|
|
74
|
+
res.status(201).json({ name: filename, size: stat.size });
|
|
75
|
+
} catch (err: unknown) {
|
|
76
|
+
console.error(err);
|
|
77
|
+
res.status(500).json({ error: (err as Error).message });
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Delete a file
|
|
82
|
+
app.delete('/api/files/:page/:filename', async (req, res) => {
|
|
83
|
+
try {
|
|
84
|
+
const filePath = safeFilePath(config, req.params.page, req.params.filename);
|
|
85
|
+
if (!filePath) {
|
|
86
|
+
res.status(400).json({ error: 'Invalid filename' });
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!(await checkIfExists(filePath))) {
|
|
91
|
+
res.status(404).json({ error: 'File not found' });
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
await fs.unlink(filePath);
|
|
96
|
+
res.json({ deleted: true });
|
|
97
|
+
} catch (err: unknown) {
|
|
98
|
+
console.error(err);
|
|
99
|
+
res.status(500).json({ error: (err as Error).message });
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// Helpers
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
function filesFolder(config: SynthOSConfig, page: string): string {
|
|
109
|
+
return path.join(config.pagesFolder, 'pages', page, 'files');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Resolve a filename inside the page's files/ folder with path-traversal protection.
|
|
114
|
+
* Returns the absolute path if safe, or null if the filename is invalid.
|
|
115
|
+
*/
|
|
116
|
+
function safeFilePath(config: SynthOSConfig, page: string, filename: string): string | null {
|
|
117
|
+
// Reject obviously bad filenames
|
|
118
|
+
if (!filename || filename.includes('..') || filename.includes('/') || filename.includes('\\')) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
const folder = filesFolder(config, page);
|
|
122
|
+
const resolved = path.resolve(folder, filename);
|
|
123
|
+
if (!resolved.startsWith(path.resolve(folder))) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
return resolved;
|
|
127
|
+
}
|