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
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { Application, Response } from 'express';
|
|
2
|
+
import { SynthOSConfig } from "../init";
|
|
3
|
+
import { checkIfExists, deleteFile, ensureFolderExists, listFiles, loadFile, saveFile } from "../files";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { v4 } from "uuid";
|
|
6
|
+
|
|
7
|
+
export function useSharedDataRoutes(config: SynthOSConfig, app: Application): void {
|
|
8
|
+
app.get('/api/shared/data/:table', (req, res) => handleList(config, req.params.table, req.query, res));
|
|
9
|
+
app.get('/api/shared/data/:table/:id', (req, res) => handleGet(config, req.params.table, req.params.id, res));
|
|
10
|
+
app.post('/api/shared/data/:table', (req, res) => handleUpsert(config, req.params.table, req.body, res));
|
|
11
|
+
app.delete('/api/shared/data/:table/:id', (req, res) => handleDelete(config, req.params.table, req.params.id, res));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Route handlers
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
async function handleList(config: SynthOSConfig, table: string, query: Record<string, any>, res: Response): Promise<void> {
|
|
19
|
+
const folder = sharedTableFolder(config, table);
|
|
20
|
+
if (!(await checkIfExists(folder))) {
|
|
21
|
+
res.status(404).json({ error: 'table_not_found', table });
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const ids = (await listFiles(folder)).filter(f => f.endsWith('.json')).map(f => f.replace('.json', ''));
|
|
26
|
+
|
|
27
|
+
const rows: Record<string, any>[] = [];
|
|
28
|
+
for (const id of ids) {
|
|
29
|
+
const file = recordFile(folder, id);
|
|
30
|
+
try {
|
|
31
|
+
const row = JSON.parse(await loadFile(file));
|
|
32
|
+
row.id = id;
|
|
33
|
+
rows.push(row);
|
|
34
|
+
} catch (err: unknown) {
|
|
35
|
+
console.error(err);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Paginate when limit is provided
|
|
40
|
+
const limitParam = typeof query.limit === 'string' ? parseInt(query.limit, 10) : NaN;
|
|
41
|
+
if (!isNaN(limitParam) && limitParam > 0) {
|
|
42
|
+
const offset = Math.max(0, typeof query.offset === 'string' ? parseInt(query.offset, 10) || 0 : 0);
|
|
43
|
+
const items = rows.slice(offset, offset + limitParam);
|
|
44
|
+
res.json({ items, total: rows.length, offset, limit: limitParam, hasMore: offset + limitParam < rows.length });
|
|
45
|
+
} else {
|
|
46
|
+
res.json(rows);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function handleGet(config: SynthOSConfig, table: string, id: string, res: Response): Promise<void> {
|
|
51
|
+
const folder = sharedTableFolder(config, table);
|
|
52
|
+
if (!(await checkIfExists(folder))) {
|
|
53
|
+
res.status(404).json({ error: 'table_not_found', table });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const file = recordFile(folder, id);
|
|
58
|
+
try {
|
|
59
|
+
const row = JSON.parse(await loadFile(file));
|
|
60
|
+
row.id = id;
|
|
61
|
+
res.json(row);
|
|
62
|
+
} catch (err: unknown) {
|
|
63
|
+
res.json({});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function handleUpsert(config: SynthOSConfig, table: string, body: any, res: Response): Promise<void> {
|
|
68
|
+
const id = body.id ?? v4();
|
|
69
|
+
const folder = sharedTableFolder(config, table);
|
|
70
|
+
const file = recordFile(folder, id);
|
|
71
|
+
try {
|
|
72
|
+
const row = { ...body, id };
|
|
73
|
+
await ensureFolderExists(folder);
|
|
74
|
+
await saveFile(file, JSON.stringify(row, null, 4));
|
|
75
|
+
res.json(row);
|
|
76
|
+
} catch (err: unknown) {
|
|
77
|
+
console.error(err);
|
|
78
|
+
res.status(500).send((err as Error).message);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function handleDelete(config: SynthOSConfig, table: string, id: string, res: Response): Promise<void> {
|
|
83
|
+
const folder = sharedTableFolder(config, table);
|
|
84
|
+
const file = recordFile(folder, id);
|
|
85
|
+
try {
|
|
86
|
+
if (await checkIfExists(file)) {
|
|
87
|
+
await deleteFile(file);
|
|
88
|
+
}
|
|
89
|
+
res.json({ success: true });
|
|
90
|
+
} catch (err: unknown) {
|
|
91
|
+
console.error(err);
|
|
92
|
+
res.status(500).send((err as Error).message);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// Helpers
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
function sharedTableFolder(config: SynthOSConfig, table: string): string {
|
|
101
|
+
return path.join(config.pagesFolder, 'shared', table);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function recordFile(folder: string, id: string): string {
|
|
105
|
+
return path.join(folder, `${id}.json`);
|
|
106
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
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 useSharedFileRoutes(config: SynthOSConfig, app: Application): void {
|
|
9
|
+
// List files in the shared files folder
|
|
10
|
+
app.get('/api/shared/files', async (req, res) => {
|
|
11
|
+
try {
|
|
12
|
+
const folder = sharedFilesFolder(config);
|
|
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 shared file
|
|
34
|
+
app.get('/api/shared/files/:filename', async (req, res) => {
|
|
35
|
+
try {
|
|
36
|
+
const filePath = safeFilePath(config, 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 shared file (raw body + x-filename header)
|
|
55
|
+
app.post('/api/shared/files', 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, filename);
|
|
64
|
+
if (!filePath) {
|
|
65
|
+
res.status(400).json({ error: 'Invalid filename' });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const folder = sharedFilesFolder(config);
|
|
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 shared file
|
|
82
|
+
app.delete('/api/shared/files/:filename', async (req, res) => {
|
|
83
|
+
try {
|
|
84
|
+
const filePath = safeFilePath(config, 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 sharedFilesFolder(config: SynthOSConfig): string {
|
|
109
|
+
return path.join(config.pagesFolder, 'shared', 'files');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Resolve a filename inside the shared 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, filename: string): string | null {
|
|
117
|
+
if (!filename || filename.includes('..') || filename.includes('/') || filename.includes('\\')) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
const folder = sharedFilesFolder(config);
|
|
121
|
+
const resolved = path.resolve(folder, filename);
|
|
122
|
+
if (!resolved.startsWith(path.resolve(folder))) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
return resolved;
|
|
126
|
+
}
|
package/src/settings.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {checkIfExists, loadFile, saveFile} from './files';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { ModelEntry, ProviderName, detectProvider } from './models';
|
|
4
|
+
import { AgentConfig } from './agents';
|
|
4
5
|
|
|
5
6
|
let _settings: Partial<SettingsV2>|undefined;
|
|
6
7
|
|
|
@@ -20,7 +21,6 @@ export type { ModelEntry } from './models';
|
|
|
20
21
|
export interface SettingsV1 {
|
|
21
22
|
serviceApiKey: string;
|
|
22
23
|
model: string;
|
|
23
|
-
maxTokens: number;
|
|
24
24
|
imageQuality: 'standard' | 'hd';
|
|
25
25
|
instructions?: string;
|
|
26
26
|
logCompletions?: boolean;
|
|
@@ -38,6 +38,8 @@ export interface SettingsV2 {
|
|
|
38
38
|
features: string[];
|
|
39
39
|
services?: ServicesConfig;
|
|
40
40
|
connectors?: ServicesConfig;
|
|
41
|
+
agents?: AgentConfig[];
|
|
42
|
+
toolbarPosition?: 'left' | 'right';
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
export const DefaultSettings: SettingsV2 = {
|
|
@@ -47,7 +49,7 @@ export const DefaultSettings: SettingsV2 = {
|
|
|
47
49
|
{
|
|
48
50
|
use: 'builder',
|
|
49
51
|
provider: 'Anthropic',
|
|
50
|
-
configuration: { apiKey: '', model: ''
|
|
52
|
+
configuration: { apiKey: '', model: '' },
|
|
51
53
|
imageQuality: 'standard',
|
|
52
54
|
instructions: '',
|
|
53
55
|
logCompletions: false,
|
|
@@ -55,7 +57,7 @@ export const DefaultSettings: SettingsV2 = {
|
|
|
55
57
|
{
|
|
56
58
|
use: 'chat',
|
|
57
59
|
provider: 'Anthropic',
|
|
58
|
-
configuration: { apiKey: '', model: ''
|
|
60
|
+
configuration: { apiKey: '', model: '' },
|
|
59
61
|
imageQuality: 'standard',
|
|
60
62
|
instructions: '',
|
|
61
63
|
logCompletions: false,
|
|
@@ -63,7 +65,9 @@ export const DefaultSettings: SettingsV2 = {
|
|
|
63
65
|
],
|
|
64
66
|
features: [],
|
|
65
67
|
services: {},
|
|
66
|
-
connectors: {}
|
|
68
|
+
connectors: {},
|
|
69
|
+
agents: [],
|
|
70
|
+
toolbarPosition: 'left',
|
|
67
71
|
};
|
|
68
72
|
|
|
69
73
|
/**
|
|
@@ -100,7 +104,6 @@ function migrateV1toV2(raw: Record<string, unknown>): SettingsV2 {
|
|
|
100
104
|
configuration: {
|
|
101
105
|
apiKey: v1.serviceApiKey ?? '',
|
|
102
106
|
model,
|
|
103
|
-
maxTokens: v1.maxTokens ?? 32000,
|
|
104
107
|
},
|
|
105
108
|
imageQuality: v1.imageQuality ?? 'standard',
|
|
106
109
|
instructions: v1.instructions ?? '',
|
|
@@ -112,7 +115,6 @@ function migrateV1toV2(raw: Record<string, unknown>): SettingsV2 {
|
|
|
112
115
|
configuration: {
|
|
113
116
|
apiKey: v1.serviceApiKey ?? '',
|
|
114
117
|
model: chatModel,
|
|
115
|
-
maxTokens: v1.maxTokens ?? 32000,
|
|
116
118
|
},
|
|
117
119
|
imageQuality: v1.imageQuality ?? 'standard',
|
|
118
120
|
instructions: v1.instructions ?? '',
|
|
@@ -133,10 +135,6 @@ export async function hasConfiguredSettings(folder: string): Promise<boolean> {
|
|
|
133
135
|
if (typeof builder.configuration.model !== 'string' || builder.configuration.model.length == 0) {
|
|
134
136
|
return false;
|
|
135
137
|
}
|
|
136
|
-
if (typeof builder.configuration.maxTokens !== 'number' || builder.configuration.maxTokens <= 0) {
|
|
137
|
-
return false;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
138
|
return true;
|
|
141
139
|
}
|
|
142
140
|
|
package/src/synthos-cli.ts
CHANGED
|
@@ -2,6 +2,7 @@ import yargs from "yargs";
|
|
|
2
2
|
import { hideBin } from "yargs/helpers";
|
|
3
3
|
import { server } from "./service";
|
|
4
4
|
import { createConfig, init } from "./init";
|
|
5
|
+
import { customizer } from "./customizer";
|
|
5
6
|
|
|
6
7
|
const dynamicImport = new Function('specifier', `return import(specifier)`);
|
|
7
8
|
|
|
@@ -16,7 +17,7 @@ export async function run() {
|
|
|
16
17
|
default: 4242
|
|
17
18
|
})
|
|
18
19
|
.option('pages', {
|
|
19
|
-
describe: `Include default pages when initializing a new
|
|
20
|
+
describe: `Include default pages when initializing a new local folder.`,
|
|
20
21
|
type: 'boolean',
|
|
21
22
|
default: true
|
|
22
23
|
})
|
|
@@ -32,9 +33,9 @@ export async function run() {
|
|
|
32
33
|
})
|
|
33
34
|
.demandOption([]);
|
|
34
35
|
}, async (args) => {
|
|
35
|
-
const config = createConfig(
|
|
36
|
+
const config = await createConfig(customizer.localFolder, { debug: args.debug, debugPageUpdates: args.debugPageUpdates }, customizer);
|
|
36
37
|
await init(config, args.pages);
|
|
37
|
-
await server(config).listen(args.port, async () => {
|
|
38
|
+
await server(config, customizer).listen(args.port, async () => {
|
|
38
39
|
console.log(`SynthOS server is running on http://localhost:${args.port}`);
|
|
39
40
|
|
|
40
41
|
// Open using default browser
|
package/src/themes.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
|
-
import { checkIfExists, listFiles, loadFile } from './files';
|
|
2
|
+
import { checkIfExists, findFileInFolders, listFiles, listFilesFromFolders, loadFile } from './files';
|
|
3
3
|
import { SynthOSConfig } from './init';
|
|
4
4
|
|
|
5
|
+
export const THEME_VERSION = 2;
|
|
6
|
+
|
|
5
7
|
export interface ThemeInfo {
|
|
6
8
|
mode: 'light' | 'dark';
|
|
7
9
|
colors: Record<string, string>;
|
|
@@ -11,6 +13,49 @@ function userThemesFolder(config: SynthOSConfig): string {
|
|
|
11
13
|
return path.join(config.pagesFolder, 'themes');
|
|
12
14
|
}
|
|
13
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Extract the base theme name and version from a CSS filename.
|
|
18
|
+
* e.g. "nebula-dusk.v2.css" → { name: "nebula-dusk", version: 2 }
|
|
19
|
+
* "nebula-dusk.css" → { name: "nebula-dusk", version: 1 }
|
|
20
|
+
*/
|
|
21
|
+
export function parseThemeFilename(filename: string): { name: string; version: number } | undefined {
|
|
22
|
+
if (!filename.endsWith('.css')) return undefined;
|
|
23
|
+
const versionedMatch = filename.match(/^(.+)\.v(\d+)\.css$/);
|
|
24
|
+
if (versionedMatch) {
|
|
25
|
+
return { name: versionedMatch[1], version: parseInt(versionedMatch[2], 10) };
|
|
26
|
+
}
|
|
27
|
+
return { name: filename.replace(/\.css$/, ''), version: 1 };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Find the CSS file for a theme by name in a folder.
|
|
32
|
+
* Prefers the highest-versioned file (e.g. name.v2.css over name.css).
|
|
33
|
+
*/
|
|
34
|
+
async function findThemeCssFile(folder: string, name: string): Promise<{ path: string; version: number } | undefined> {
|
|
35
|
+
if (!await checkIfExists(folder)) return undefined;
|
|
36
|
+
const files = await listFiles(folder);
|
|
37
|
+
let best: { path: string; version: number } | undefined;
|
|
38
|
+
for (const f of files) {
|
|
39
|
+
const parsed = parseThemeFilename(f);
|
|
40
|
+
if (parsed && parsed.name === name) {
|
|
41
|
+
if (!best || parsed.version > best.version) {
|
|
42
|
+
best = { path: path.join(folder, f), version: parsed.version };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return best;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function loadThemeVersion(name: string, config: SynthOSConfig): Promise<number> {
|
|
50
|
+
const local = await findThemeCssFile(userThemesFolder(config), name);
|
|
51
|
+
if (local) return local.version;
|
|
52
|
+
for (const folder of config.defaultThemesFolders) {
|
|
53
|
+
const def = await findThemeCssFile(folder, name);
|
|
54
|
+
if (def) return def.version;
|
|
55
|
+
}
|
|
56
|
+
return 1;
|
|
57
|
+
}
|
|
58
|
+
|
|
14
59
|
export async function loadThemeInfo(name: string, config: SynthOSConfig): Promise<ThemeInfo | undefined> {
|
|
15
60
|
// Check user's local themes first, then fall back to package defaults
|
|
16
61
|
const localPath = path.join(userThemesFolder(config), `${name}.json`);
|
|
@@ -19,8 +64,8 @@ export async function loadThemeInfo(name: string, config: SynthOSConfig): Promis
|
|
|
19
64
|
return raw ? JSON.parse(raw) : undefined;
|
|
20
65
|
}
|
|
21
66
|
|
|
22
|
-
const defaultPath =
|
|
23
|
-
if (
|
|
67
|
+
const defaultPath = await findFileInFolders(config.defaultThemesFolders, `${name}.json`);
|
|
68
|
+
if (defaultPath) {
|
|
24
69
|
const raw = await loadFile(defaultPath);
|
|
25
70
|
return raw ? JSON.parse(raw) : undefined;
|
|
26
71
|
}
|
|
@@ -30,14 +75,17 @@ export async function loadThemeInfo(name: string, config: SynthOSConfig): Promis
|
|
|
30
75
|
|
|
31
76
|
export async function loadTheme(name: string, config: SynthOSConfig): Promise<string | undefined> {
|
|
32
77
|
// Check user's local themes first, then fall back to package defaults
|
|
33
|
-
const
|
|
34
|
-
if (
|
|
35
|
-
return await loadFile(
|
|
78
|
+
const local = await findThemeCssFile(userThemesFolder(config), name);
|
|
79
|
+
if (local) {
|
|
80
|
+
return await loadFile(local.path);
|
|
36
81
|
}
|
|
37
82
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
83
|
+
// Search all default theme folders
|
|
84
|
+
for (const folder of config.defaultThemesFolders) {
|
|
85
|
+
const def = await findThemeCssFile(folder, name);
|
|
86
|
+
if (def) {
|
|
87
|
+
return await loadFile(def.path);
|
|
88
|
+
}
|
|
41
89
|
}
|
|
42
90
|
|
|
43
91
|
return undefined;
|
|
@@ -51,21 +99,56 @@ export async function listThemes(config: SynthOSConfig): Promise<string[]> {
|
|
|
51
99
|
if (await checkIfExists(localFolder)) {
|
|
52
100
|
const files = await listFiles(localFolder);
|
|
53
101
|
for (const f of files) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
102
|
+
const parsed = parseThemeFilename(f);
|
|
103
|
+
if (parsed) names.add(parsed.name);
|
|
57
104
|
}
|
|
58
105
|
}
|
|
59
106
|
|
|
60
|
-
// Collect from
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
names.add(f.replace(/\.css$/, ''));
|
|
66
|
-
}
|
|
67
|
-
}
|
|
107
|
+
// Collect from all default theme folders
|
|
108
|
+
const defaultFiles = await listFilesFromFolders(config.defaultThemesFolders);
|
|
109
|
+
for (const f of defaultFiles) {
|
|
110
|
+
const parsed = parseThemeFilename(f);
|
|
111
|
+
if (parsed) names.add(parsed.name);
|
|
68
112
|
}
|
|
69
113
|
|
|
70
114
|
return Array.from(names).sort();
|
|
71
115
|
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Compare local theme versions against defaults and return themes that need upgrading.
|
|
119
|
+
*/
|
|
120
|
+
export async function getOutdatedThemes(config: SynthOSConfig): Promise<string[]> {
|
|
121
|
+
const localFolder = userThemesFolder(config);
|
|
122
|
+
if (!await checkIfExists(localFolder)) return [];
|
|
123
|
+
|
|
124
|
+
const defaultFiles = await listFilesFromFolders(config.defaultThemesFolders);
|
|
125
|
+
const localFiles = await listFiles(localFolder);
|
|
126
|
+
|
|
127
|
+
// Build maps: theme name → highest version
|
|
128
|
+
const defaultVersions = new Map<string, number>();
|
|
129
|
+
for (const f of defaultFiles) {
|
|
130
|
+
const parsed = parseThemeFilename(f);
|
|
131
|
+
if (parsed) {
|
|
132
|
+
const cur = defaultVersions.get(parsed.name) ?? 0;
|
|
133
|
+
if (parsed.version > cur) defaultVersions.set(parsed.name, parsed.version);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const localVersions = new Map<string, number>();
|
|
138
|
+
for (const f of localFiles) {
|
|
139
|
+
const parsed = parseThemeFilename(f);
|
|
140
|
+
if (parsed) {
|
|
141
|
+
const cur = localVersions.get(parsed.name) ?? 0;
|
|
142
|
+
if (parsed.version > cur) localVersions.set(parsed.name, parsed.version);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const outdated: string[] = [];
|
|
147
|
+
for (const [name, defVer] of defaultVersions) {
|
|
148
|
+
const localVer = localVersions.get(name) ?? 0;
|
|
149
|
+
if (localVer < defVer) {
|
|
150
|
+
outdated.push(name);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return outdated;
|
|
154
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="g" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
4
|
+
<stop offset="0%" stop-color="#a855f7"/>
|
|
5
|
+
<stop offset="100%" stop-color="#3b82f6"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
<rect width="32" height="32" rx="7" fill="#0f0f13"/>
|
|
9
|
+
<path d="M9 11 C9 8.8 10.8 7 13 7 L20 7 C22.2 7 24 8.8 24 11 C24 13.2 22.2 15 20 15 L13 15 C10.8 15 9 16.8 9 19 C9 21.2 10.8 23 13 23 L20 23" stroke="url(#g)" stroke-width="3" stroke-linecap="round" fill="none"/>
|
|
10
|
+
<circle cx="9" cy="11" r="2" fill="#a855f7"/>
|
|
11
|
+
<circle cx="23" cy="23" r="2" fill="#3b82f6"/>
|
|
12
|
+
</svg>
|