synthos 0.6.0 → 0.7.1
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 +33 -1
- package/default-pages/app_builder.html +40 -0
- package/default-pages/app_builder.json +1 -0
- package/default-pages/json_tools.html +89 -159
- package/default-pages/json_tools.json +1 -0
- package/default-pages/my_notes.html +33 -0
- package/default-pages/my_notes.json +12 -0
- package/default-pages/neon_asteroids.html +77 -0
- package/default-pages/neon_asteroids.json +12 -0
- package/default-pages/sidebar_builder.html +49 -0
- package/default-pages/sidebar_builder.json +1 -0
- package/default-pages/solar_explorer.html +1956 -0
- package/default-pages/solar_explorer.json +12 -0
- package/default-pages/solar_tutorial.html +476 -0
- package/default-pages/solar_tutorial.json +1 -0
- package/default-pages/two-panel_builder.html +66 -0
- package/default-pages/two-panel_builder.json +1 -0
- package/default-themes/nebula-dawn.css +682 -0
- package/default-themes/nebula-dawn.json +19 -0
- package/default-themes/nebula-dusk.css +674 -0
- package/default-themes/nebula-dusk.json +19 -0
- package/dist/connectors/index.d.ts +3 -0
- package/dist/connectors/index.d.ts.map +1 -0
- package/dist/connectors/index.js +6 -0
- package/dist/connectors/index.js.map +1 -0
- package/dist/connectors/registry.d.ts +3 -0
- package/dist/connectors/registry.d.ts.map +1 -0
- package/dist/connectors/registry.js +100 -0
- package/dist/connectors/registry.js.map +1 -0
- package/dist/connectors/types.d.ts +61 -0
- package/dist/connectors/types.d.ts.map +1 -0
- package/dist/connectors/types.js +3 -0
- package/dist/connectors/types.js.map +1 -0
- package/dist/files.d.ts +2 -0
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +12 -1
- package/dist/files.js.map +1 -1
- package/dist/init.d.ts +8 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +155 -3
- package/dist/init.js.map +1 -1
- package/dist/migrations.d.ts +11 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/migrations.js +281 -0
- package/dist/migrations.js.map +1 -0
- package/dist/models/index.d.ts +3 -0
- package/dist/models/index.d.ts.map +1 -0
- package/dist/models/index.js +10 -0
- package/dist/models/index.js.map +1 -0
- package/dist/models/providers.d.ts +7 -0
- package/dist/models/providers.d.ts.map +1 -0
- package/dist/models/providers.js +33 -0
- package/dist/models/providers.js.map +1 -0
- package/dist/models/types.d.ts +21 -0
- package/dist/models/types.d.ts.map +1 -0
- package/dist/models/types.js +3 -0
- package/dist/models/types.js.map +1 -0
- package/dist/pages.d.ts +21 -2
- package/dist/pages.d.ts.map +1 -1
- package/dist/pages.js +202 -23
- package/dist/pages.js.map +1 -1
- package/dist/scripts.js +2 -2
- package/dist/scripts.js.map +1 -1
- package/dist/service/createCompletePrompt.d.ts +3 -2
- package/dist/service/createCompletePrompt.d.ts.map +1 -1
- package/dist/service/createCompletePrompt.js +11 -16
- package/dist/service/createCompletePrompt.js.map +1 -1
- package/dist/service/debugLog.d.ts +11 -0
- package/dist/service/debugLog.d.ts.map +1 -0
- package/dist/service/debugLog.js +26 -0
- package/dist/service/debugLog.js.map +1 -0
- package/dist/service/modelInstructions.d.ts +7 -0
- package/dist/service/modelInstructions.d.ts.map +1 -0
- package/dist/service/modelInstructions.js +16 -0
- package/dist/service/modelInstructions.js.map +1 -0
- package/dist/service/requiresSettings.d.ts +2 -2
- package/dist/service/requiresSettings.d.ts.map +1 -1
- package/dist/service/requiresSettings.js.map +1 -1
- package/dist/service/server.d.ts.map +1 -1
- package/dist/service/server.js +15 -0
- package/dist/service/server.js.map +1 -1
- package/dist/service/transformPage.d.ts +81 -2
- package/dist/service/transformPage.d.ts.map +1 -1
- package/dist/service/transformPage.js +672 -82
- package/dist/service/transformPage.js.map +1 -1
- package/dist/service/useApiRoutes.d.ts.map +1 -1
- package/dist/service/useApiRoutes.js +579 -13
- package/dist/service/useApiRoutes.js.map +1 -1
- package/dist/service/useConnectorRoutes.d.ts +4 -0
- package/dist/service/useConnectorRoutes.d.ts.map +1 -0
- package/dist/service/useConnectorRoutes.js +389 -0
- package/dist/service/useConnectorRoutes.js.map +1 -0
- package/dist/service/useDataRoutes.d.ts.map +1 -1
- package/dist/service/useDataRoutes.js +83 -70
- package/dist/service/useDataRoutes.js.map +1 -1
- package/dist/service/usePageRoutes.d.ts.map +1 -1
- package/dist/service/usePageRoutes.js +243 -38
- package/dist/service/usePageRoutes.js.map +1 -1
- package/dist/settings.d.ts +33 -4
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +108 -15
- package/dist/settings.js.map +1 -1
- package/dist/synthos-cli.d.ts.map +1 -1
- package/dist/synthos-cli.js +11 -1
- package/dist/synthos-cli.js.map +1 -1
- package/dist/themes.d.ts +9 -0
- package/dist/themes.d.ts.map +1 -0
- package/dist/themes.js +64 -0
- package/dist/themes.js.map +1 -0
- package/package.json +6 -3
- package/required-pages/builder.html +74 -0
- package/required-pages/builder.json +1 -0
- package/required-pages/pages.html +169 -126
- package/required-pages/pages.json +1 -0
- package/required-pages/settings.html +812 -156
- package/required-pages/settings.json +1 -0
- package/required-pages/synthos_apis.html +272 -0
- package/required-pages/synthos_apis.json +1 -0
- package/required-pages/synthos_scripts.html +87 -0
- package/required-pages/synthos_scripts.json +1 -0
- package/src/connectors/index.ts +12 -0
- package/src/connectors/registry.ts +98 -0
- package/src/connectors/types.ts +68 -0
- package/src/files.ts +11 -0
- package/src/init.ts +151 -5
- package/src/migrations.ts +266 -0
- package/src/models/index.ts +2 -0
- package/src/models/providers.ts +33 -0
- package/src/models/types.ts +23 -0
- package/src/pages.ts +234 -26
- package/src/scripts.ts +2 -2
- package/src/service/createCompletePrompt.ts +14 -18
- package/src/service/debugLog.ts +17 -0
- package/src/service/modelInstructions.ts +14 -0
- package/src/service/requiresSettings.ts +3 -3
- package/src/service/server.ts +19 -2
- package/src/service/transformPage.ts +709 -88
- package/src/service/useApiRoutes.ts +632 -16
- package/src/service/useConnectorRoutes.ts +427 -0
- package/src/service/useDataRoutes.ts +87 -71
- package/src/service/usePageRoutes.ts +237 -44
- package/src/settings.ts +143 -20
- package/src/synthos-cli.ts +11 -1
- package/src/themes.ts +71 -0
- package/default-pages/[application].html +0 -95
- package/default-pages/[markdown].html +0 -271
- package/default-pages/[sidebar].html +0 -114
- package/default-pages/[split-application].html +0 -118
- package/default-pages/solar_system.html +0 -432
- package/default-pages/space_invaders.html +0 -617
- package/required-pages/apis.html +0 -362
- package/required-pages/home.html +0 -126
- package/required-pages/scripts.html +0 -350
|
@@ -1,14 +1,95 @@
|
|
|
1
|
-
import { loadPageState, normalizePageName, savePageState, updatePageState } from "../pages";
|
|
2
|
-
import { hasConfiguredSettings, loadSettings } from "../settings";
|
|
1
|
+
import { loadPageMetadata, loadPageState, normalizePageName, PAGE_VERSION, REQUIRED_PAGES, savePageMetadata, savePageState, updatePageState } from "../pages";
|
|
2
|
+
import { getModelEntry, hasConfiguredSettings, loadSettings } from "../settings";
|
|
3
3
|
import { Application } from 'express';
|
|
4
|
-
import { transformPage
|
|
4
|
+
import { transformPage } from "./transformPage";
|
|
5
|
+
import { getModelInstructions } from "./modelInstructions";
|
|
5
6
|
import { SynthOSConfig } from "../init";
|
|
6
7
|
import { createCompletePrompt } from "./createCompletePrompt";
|
|
7
8
|
import { completePrompt } from "agentm-core";
|
|
9
|
+
import { green, red, dim, estimateTokens } from "./debugLog";
|
|
10
|
+
import { loadThemeInfo } from "../themes";
|
|
11
|
+
import * as cheerio from 'cheerio';
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Required CDN imports that must be present on every v2 page.
|
|
15
|
+
* Each entry maps a detection selector to the script tag src to inject.
|
|
16
|
+
*/
|
|
17
|
+
const REQUIRED_IMPORTS: { selector: string; src: string }[] = [
|
|
18
|
+
{ selector: 'script[src*="marked"]', src: 'https://cdnjs.cloudflare.com/ajax/libs/marked/14.1.1/marked.min.js' },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Uses cheerio to ensure every required import is present in the page's <head>.
|
|
23
|
+
* Skips imports that already exist (detected via selector).
|
|
24
|
+
*/
|
|
25
|
+
function ensureRequiredImports(html: string, pageVersion: number): string {
|
|
26
|
+
if (pageVersion < 2) return html;
|
|
27
|
+
const $ = cheerio.load(html);
|
|
28
|
+
for (const imp of REQUIRED_IMPORTS) {
|
|
29
|
+
if ($(imp.selector).length === 0) {
|
|
30
|
+
$('head').append(`<script src="${imp.src}"></script>\n`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return $.html();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const HOME_PAGE_ROUTE = '/builder';
|
|
10
37
|
const PAGE_NOT_FOUND = 'Page not found';
|
|
11
38
|
|
|
39
|
+
function injectPageInfoScript(html: string, pageName: string): string {
|
|
40
|
+
const tag = `<script id="page-info" src="/api/page-info.js?page=${encodeURIComponent(pageName)}"></script>`;
|
|
41
|
+
|
|
42
|
+
// Replace any existing page-info script (may have a stale page name from the template)
|
|
43
|
+
const existing = html.match(/<script\s+id="page-info"[^>]*><\/script>/);
|
|
44
|
+
if (existing) {
|
|
45
|
+
return html.replace(existing[0], tag);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const idx = html.indexOf('</head>');
|
|
49
|
+
if (idx !== -1) {
|
|
50
|
+
return html.slice(0, idx) + tag + '\n' + html.slice(idx);
|
|
51
|
+
}
|
|
52
|
+
return tag + '\n' + html;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function injectPageHelpers(html: string, pageVersion: number): string {
|
|
56
|
+
if (pageVersion < 2) return html;
|
|
57
|
+
const tag = `<script id="page-helpers" src="/api/page-helpers.js?v=${pageVersion}"></script>`;
|
|
58
|
+
|
|
59
|
+
// Replace any existing page-helpers script (may be at wrong position from prior LLM output)
|
|
60
|
+
const existing = html.match(/<script\s+id="page-helpers"[^>]*><\/script>/);
|
|
61
|
+
if (existing) {
|
|
62
|
+
return html.replace(existing[0], tag);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Inject into <head> after page-info so helpers are available before inline body scripts
|
|
66
|
+
const pageInfo = html.indexOf('id="page-info"');
|
|
67
|
+
if (pageInfo !== -1) {
|
|
68
|
+
const closeTag = html.indexOf('</script>', pageInfo);
|
|
69
|
+
if (closeTag !== -1) {
|
|
70
|
+
const insertAt = closeTag + '</script>'.length;
|
|
71
|
+
return html.slice(0, insertAt) + '\n' + tag + html.slice(insertAt);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const idx = html.indexOf('</head>');
|
|
76
|
+
if (idx !== -1) {
|
|
77
|
+
return html.slice(0, idx) + tag + '\n' + html.slice(idx);
|
|
78
|
+
}
|
|
79
|
+
return tag + '\n' + html;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function injectPageScript(html: string, pageVersion: number): string {
|
|
83
|
+
if (pageVersion < 2) return html;
|
|
84
|
+
if (html.includes('id="page-script"')) return html;
|
|
85
|
+
const tag = `<script id="page-script" src="/api/page-script.js?v=${pageVersion}"></script>`;
|
|
86
|
+
const idx = html.indexOf('</body>');
|
|
87
|
+
if (idx !== -1) {
|
|
88
|
+
return html.slice(0, idx) + tag + '\n' + html.slice(idx);
|
|
89
|
+
}
|
|
90
|
+
return html + '\n' + tag;
|
|
91
|
+
}
|
|
92
|
+
|
|
12
93
|
export function usePageRoutes(config: SynthOSConfig, app: Application): void {
|
|
13
94
|
// Redirect / to /home page
|
|
14
95
|
app.get('/', (req, res) => res.redirect(HOME_PAGE_ROUTE));
|
|
@@ -19,7 +100,7 @@ export function usePageRoutes(config: SynthOSConfig, app: Application): void {
|
|
|
19
100
|
const { page } = req.params;
|
|
20
101
|
const isConfigured = await hasConfiguredSettings(config.pagesFolder);
|
|
21
102
|
if (!isConfigured && page !== 'settings') {
|
|
22
|
-
res.redirect('/settings');
|
|
103
|
+
res.redirect('/settings?firstRun=1');
|
|
23
104
|
return;
|
|
24
105
|
}
|
|
25
106
|
|
|
@@ -30,7 +111,21 @@ export function usePageRoutes(config: SynthOSConfig, app: Application): void {
|
|
|
30
111
|
return;
|
|
31
112
|
}
|
|
32
113
|
|
|
33
|
-
|
|
114
|
+
// Load page metadata for version-based script injection
|
|
115
|
+
const metadata = await loadPageMetadata(config.pagesFolder, page, config.requiredPagesFolder);
|
|
116
|
+
const pageVersion = metadata?.pageVersion ?? 0;
|
|
117
|
+
|
|
118
|
+
// Block outdated pages (redirect to /pages so user sees upgrade UI)
|
|
119
|
+
if (pageVersion < PAGE_VERSION && !REQUIRED_PAGES.includes(page)) {
|
|
120
|
+
res.redirect('/pages');
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let html = ensureRequiredImports(pageState, pageVersion);
|
|
125
|
+
html = injectPageInfoScript(html, page);
|
|
126
|
+
html = injectPageHelpers(html, pageVersion);
|
|
127
|
+
html = injectPageScript(html, pageVersion);
|
|
128
|
+
res.send(html);
|
|
34
129
|
});
|
|
35
130
|
|
|
36
131
|
// Page reset
|
|
@@ -39,7 +134,7 @@ export function usePageRoutes(config: SynthOSConfig, app: Application): void {
|
|
|
39
134
|
const { page } = req.params;
|
|
40
135
|
const isConfigured = await hasConfiguredSettings(config.pagesFolder);
|
|
41
136
|
if (!isConfigured) {
|
|
42
|
-
res.redirect('/settings');
|
|
137
|
+
res.redirect('/settings?firstRun=1');
|
|
43
138
|
return;
|
|
44
139
|
}
|
|
45
140
|
|
|
@@ -54,32 +149,82 @@ export function usePageRoutes(config: SynthOSConfig, app: Application): void {
|
|
|
54
149
|
});
|
|
55
150
|
|
|
56
151
|
// Page save
|
|
57
|
-
app.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
152
|
+
app.post('/:page/save', async (req, res) => {
|
|
153
|
+
try {
|
|
154
|
+
// Redirect if settings not configured
|
|
155
|
+
const { page } = req.params;
|
|
156
|
+
const isConfigured = await hasConfiguredSettings(config.pagesFolder);
|
|
157
|
+
if (!isConfigured) {
|
|
158
|
+
res.status(400).json({ error: 'Settings not configured' });
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
65
161
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
162
|
+
// Extract fields from JSON body
|
|
163
|
+
const { title, categories, greeting } = req.body;
|
|
164
|
+
if (!title || typeof title !== 'string') {
|
|
165
|
+
res.status(400).json({ error: 'title is required' });
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (!categories || !Array.isArray(categories) || categories.length === 0) {
|
|
169
|
+
res.status(400).json({ error: 'categories is required (array of strings)' });
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
72
172
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
173
|
+
// Normalize page name from title
|
|
174
|
+
const saveAs = normalizePageName(title);
|
|
175
|
+
if (!saveAs) {
|
|
176
|
+
res.status(400).json({ error: 'Invalid title — cannot derive a page name' });
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Reject reserved names
|
|
181
|
+
if (saveAs === 'builder') {
|
|
182
|
+
res.status(400).json({ error: '"Builder" is a reserved page name' });
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Load page state
|
|
187
|
+
let pageState = await loadPageWithFallback(page, config, false);
|
|
188
|
+
if (!pageState) {
|
|
189
|
+
res.status(404).json({ error: PAGE_NOT_FOUND });
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// If greeting is provided, process with cheerio
|
|
194
|
+
if (greeting && typeof greeting === 'string' && greeting.trim().length > 0) {
|
|
195
|
+
const $ = cheerio.load(pageState);
|
|
196
|
+
const messages = $('#chatMessages .chat-message');
|
|
197
|
+
// Keep only the first message, remove the rest
|
|
198
|
+
messages.slice(1).remove();
|
|
199
|
+
// Update the greeting text in the first message
|
|
200
|
+
const firstP = messages.first().find('p');
|
|
201
|
+
const strong = firstP.find('strong');
|
|
202
|
+
if (strong.length) {
|
|
203
|
+
firstP.html('<strong>Synthos:</strong> ' + greeting.trim());
|
|
204
|
+
}
|
|
205
|
+
pageState = $.html();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Save as new page
|
|
209
|
+
await savePageState(config.pagesFolder, saveAs, pageState, title, categories);
|
|
210
|
+
|
|
211
|
+
// Also update metadata with categories (in case page.json already existed)
|
|
212
|
+
await savePageMetadata(config.pagesFolder, saveAs, {
|
|
213
|
+
title,
|
|
214
|
+
categories,
|
|
215
|
+
pinned: false,
|
|
216
|
+
showInAll: true,
|
|
217
|
+
createdDate: new Date().toISOString(),
|
|
218
|
+
lastModified: new Date().toISOString(),
|
|
219
|
+
pageVersion: PAGE_VERSION,
|
|
220
|
+
mode: 'unlocked',
|
|
221
|
+
});
|
|
79
222
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
223
|
+
res.json({ redirect: `/${saveAs}` });
|
|
224
|
+
} catch (err: unknown) {
|
|
225
|
+
console.error(err);
|
|
226
|
+
res.status(500).json({ error: (err as Error).message });
|
|
227
|
+
}
|
|
83
228
|
});
|
|
84
229
|
|
|
85
230
|
// Page transformation
|
|
@@ -108,28 +253,76 @@ export function usePageRoutes(config: SynthOSConfig, app: Application): void {
|
|
|
108
253
|
}
|
|
109
254
|
|
|
110
255
|
// Create model instance
|
|
111
|
-
const innerCompletePrompt = await createCompletePrompt(config.pagesFolder, req.body.model);
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
256
|
+
const innerCompletePrompt = await createCompletePrompt(config.pagesFolder, 'builder', req.body.model);
|
|
257
|
+
const debugVerbose = config.debugPageUpdates;
|
|
258
|
+
let inputChars = 0;
|
|
259
|
+
let outputChars = 0;
|
|
260
|
+
const completePrompt: completePrompt = async (args) => {
|
|
261
|
+
if (debugVerbose) {
|
|
262
|
+
console.log(green(dim('\n ===== PAGE UPDATE REQUEST =====')));
|
|
263
|
+
console.log(green(` SYSTEM:\n${args.system?.content}`));
|
|
264
|
+
console.log(green(`\n PROMPT:\n${args.prompt.content}`));
|
|
265
|
+
}
|
|
266
|
+
inputChars += (args.system?.content?.length ?? 0) + (args.prompt.content?.length ?? 0);
|
|
267
|
+
const result = await innerCompletePrompt(args);
|
|
268
|
+
if (result.completed) {
|
|
269
|
+
outputChars += result.value?.length ?? 0;
|
|
270
|
+
}
|
|
271
|
+
if (debugVerbose) {
|
|
272
|
+
console.log(green(dim('\n ----- PAGE UPDATE RESPONSE -----')));
|
|
273
|
+
if (result.completed) {
|
|
274
|
+
console.log(green(` RESPONSE:\n${result.value}`));
|
|
275
|
+
} else {
|
|
276
|
+
console.log(red(` ERROR: ${result.error?.message}`));
|
|
277
|
+
}
|
|
278
|
+
console.log(green(dim(' ================================\n')));
|
|
279
|
+
}
|
|
280
|
+
return result;
|
|
116
281
|
}
|
|
117
282
|
|
|
118
|
-
|
|
119
|
-
// Transform and cache updated page
|
|
283
|
+
// Transform and cache updated page
|
|
120
284
|
const pagesFolder = config.pagesFolder;
|
|
121
|
-
const
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
285
|
+
const settings = await loadSettings(config.pagesFolder);
|
|
286
|
+
const builder = getModelEntry(settings, 'builder');
|
|
287
|
+
const { configuration, instructions } = builder;
|
|
288
|
+
const maxTokens = configuration.maxTokens;
|
|
289
|
+
const theme = settings.theme;
|
|
290
|
+
const themeInfo = await loadThemeInfo(theme ?? 'nebula-dusk', config);
|
|
291
|
+
const modelInstructions = getModelInstructions(builder.provider);
|
|
292
|
+
const configuredConnectors = settings.connectors;
|
|
293
|
+
const result = await transformPage({ pagesFolder, pageState, message, maxTokens, instructions, modelInstructions, completePrompt, themeInfo, configuredConnectors });
|
|
125
294
|
if (result.completed) {
|
|
126
|
-
|
|
127
|
-
|
|
295
|
+
const { html, changeCount } = result.value!;
|
|
296
|
+
if (config.debug) {
|
|
297
|
+
const inTokens = estimateTokens(inputChars).toLocaleString();
|
|
298
|
+
const outTokens = estimateTokens(outputChars).toLocaleString();
|
|
299
|
+
console.log(` page: ${page} | message: ${message.length} chars | changes: ${changeCount} ops | ~${inTokens} in / ~${outTokens} out tokens`);
|
|
300
|
+
}
|
|
301
|
+
updatePageState(page, html);
|
|
302
|
+
|
|
303
|
+
// Update lastModified timestamp in page metadata
|
|
304
|
+
const metadata = await loadPageMetadata(pagesFolder, page, config.requiredPagesFolder);
|
|
305
|
+
if (metadata) {
|
|
306
|
+
metadata.lastModified = new Date().toISOString();
|
|
307
|
+
await savePageMetadata(pagesFolder, page, metadata);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Inject required imports and page scripts (same as GET)
|
|
311
|
+
const pv = metadata?.pageVersion ?? 0;
|
|
312
|
+
let out = ensureRequiredImports(html, pv);
|
|
313
|
+
out = injectPageInfoScript(out, page);
|
|
314
|
+
out = injectPageHelpers(out, pv);
|
|
315
|
+
out = injectPageScript(out, pv);
|
|
316
|
+
res.send(out);
|
|
128
317
|
} else {
|
|
129
318
|
throw result.error;
|
|
130
319
|
}
|
|
131
320
|
} catch (err: unknown) {
|
|
132
|
-
|
|
321
|
+
if (config.debug) {
|
|
322
|
+
console.log(red(` ERROR: ${(err as Error).message}`));
|
|
323
|
+
} else {
|
|
324
|
+
console.error(err);
|
|
325
|
+
}
|
|
133
326
|
res.status(500).send((err as Error).message);
|
|
134
327
|
}
|
|
135
328
|
});
|
package/src/settings.ts
CHANGED
|
@@ -1,60 +1,183 @@
|
|
|
1
1
|
import {checkIfExists, loadFile, saveFile} from './files';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { ModelEntry, ProviderName, detectProvider } from './models';
|
|
3
4
|
|
|
4
|
-
let _settings: Partial<
|
|
5
|
+
let _settings: Partial<SettingsV2>|undefined;
|
|
5
6
|
|
|
6
|
-
export interface
|
|
7
|
+
export interface ServiceConfig {
|
|
8
|
+
apiKey: string;
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type ServicesConfig = { [serviceId: string]: ServiceConfig };
|
|
13
|
+
|
|
14
|
+
// Re-export ModelEntry from models so existing imports from settings still work
|
|
15
|
+
export type { ModelEntry } from './models';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* V1 settings shape (flat, single model). Used as migration source.
|
|
19
|
+
*/
|
|
20
|
+
export interface SettingsV1 {
|
|
7
21
|
serviceApiKey: string;
|
|
8
22
|
model: string;
|
|
9
23
|
maxTokens: number;
|
|
10
24
|
imageQuality: 'standard' | 'hd';
|
|
11
25
|
instructions?: string;
|
|
12
26
|
logCompletions?: boolean;
|
|
27
|
+
theme?: string;
|
|
28
|
+
services?: ServicesConfig;
|
|
13
29
|
}
|
|
14
30
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
31
|
+
/**
|
|
32
|
+
* V2 settings shape with versioned models array.
|
|
33
|
+
*/
|
|
34
|
+
export interface SettingsV2 {
|
|
35
|
+
version: 2;
|
|
36
|
+
theme: string;
|
|
37
|
+
models: ModelEntry[];
|
|
38
|
+
features: string[];
|
|
39
|
+
services?: ServicesConfig;
|
|
40
|
+
connectors?: ServicesConfig;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const DefaultSettings: SettingsV2 = {
|
|
44
|
+
version: 2,
|
|
45
|
+
theme: 'nebula-dawn',
|
|
46
|
+
models: [
|
|
47
|
+
{
|
|
48
|
+
use: 'builder',
|
|
49
|
+
provider: 'Anthropic',
|
|
50
|
+
configuration: { apiKey: '', model: '', maxTokens: 32000 },
|
|
51
|
+
imageQuality: 'standard',
|
|
52
|
+
instructions: '',
|
|
53
|
+
logCompletions: false,
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
use: 'chat',
|
|
57
|
+
provider: 'Anthropic',
|
|
58
|
+
configuration: { apiKey: '', model: '', maxTokens: 32000 },
|
|
59
|
+
imageQuality: 'standard',
|
|
60
|
+
instructions: '',
|
|
61
|
+
logCompletions: false,
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
features: [],
|
|
65
|
+
services: {},
|
|
66
|
+
connectors: {}
|
|
22
67
|
};
|
|
23
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Find a model entry by its `use` field. Falls back to default if not found.
|
|
71
|
+
*/
|
|
72
|
+
export function getModelEntry(settings: SettingsV2, use: 'builder' | 'chat'): ModelEntry {
|
|
73
|
+
const entry = settings.models.find(m => m.use === use);
|
|
74
|
+
if (entry) return entry;
|
|
75
|
+
return DefaultSettings.models.find(m => m.use === use)!;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Migrate a v1 settings object to v2 by detecting provider from model prefix
|
|
80
|
+
* and wrapping fields into the new configuration object.
|
|
81
|
+
*/
|
|
82
|
+
function migrateV1toV2(raw: Record<string, unknown>): SettingsV2 {
|
|
83
|
+
const v1 = raw as unknown as SettingsV1;
|
|
84
|
+
const rawModel = v1.model ?? '';
|
|
85
|
+
// Migrate retired model names
|
|
86
|
+
const model = rawModel === 'claude-opus-4-5' ? 'claude-opus-4-6' : rawModel;
|
|
87
|
+
const detected = detectProvider(model);
|
|
88
|
+
const provider: ProviderName = detected?.name ?? 'Anthropic';
|
|
89
|
+
|
|
90
|
+
// Default chat to claude-haiku-4-5 when migrating from an Anthropic model
|
|
91
|
+
const chatModel = provider === 'Anthropic' ? 'claude-haiku-4-5' : model;
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
version: 2,
|
|
95
|
+
theme: v1.theme ?? 'nebula-dusk',
|
|
96
|
+
models: [
|
|
97
|
+
{
|
|
98
|
+
use: 'builder',
|
|
99
|
+
provider,
|
|
100
|
+
configuration: {
|
|
101
|
+
apiKey: v1.serviceApiKey ?? '',
|
|
102
|
+
model,
|
|
103
|
+
maxTokens: v1.maxTokens ?? 32000,
|
|
104
|
+
},
|
|
105
|
+
imageQuality: v1.imageQuality ?? 'standard',
|
|
106
|
+
instructions: v1.instructions ?? '',
|
|
107
|
+
logCompletions: v1.logCompletions ?? false,
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
use: 'chat',
|
|
111
|
+
provider,
|
|
112
|
+
configuration: {
|
|
113
|
+
apiKey: v1.serviceApiKey ?? '',
|
|
114
|
+
model: chatModel,
|
|
115
|
+
maxTokens: v1.maxTokens ?? 32000,
|
|
116
|
+
},
|
|
117
|
+
imageQuality: v1.imageQuality ?? 'standard',
|
|
118
|
+
instructions: v1.instructions ?? '',
|
|
119
|
+
logCompletions: v1.logCompletions ?? false,
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
features: [],
|
|
123
|
+
services: v1.services,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
24
127
|
export async function hasConfiguredSettings(folder: string): Promise<boolean> {
|
|
25
128
|
const settings = await loadSettings(folder);
|
|
26
|
-
|
|
129
|
+
const builder = getModelEntry(settings, 'builder');
|
|
130
|
+
if (typeof builder.configuration.apiKey !== 'string' || builder.configuration.apiKey.length == 0) {
|
|
27
131
|
return false;
|
|
28
132
|
}
|
|
29
|
-
if (typeof
|
|
133
|
+
if (typeof builder.configuration.model !== 'string' || builder.configuration.model.length == 0) {
|
|
30
134
|
return false;
|
|
31
135
|
}
|
|
32
|
-
if (typeof
|
|
136
|
+
if (typeof builder.configuration.maxTokens !== 'number' || builder.configuration.maxTokens <= 0) {
|
|
33
137
|
return false;
|
|
34
138
|
}
|
|
35
139
|
|
|
36
|
-
return true;
|
|
140
|
+
return true;
|
|
37
141
|
}
|
|
38
142
|
|
|
39
|
-
export async function loadSettings(folder: string): Promise<
|
|
143
|
+
export async function loadSettings(folder: string): Promise<SettingsV2> {
|
|
40
144
|
if (_settings == undefined) {
|
|
41
|
-
// Check for file to exist
|
|
42
145
|
const filename = path.join(folder, 'settings.json');
|
|
43
146
|
if (await checkIfExists(filename)) {
|
|
44
147
|
try {
|
|
45
|
-
|
|
46
|
-
|
|
148
|
+
const raw = JSON.parse(await loadFile(filename));
|
|
149
|
+
if (!raw.version) {
|
|
150
|
+
// V1 file — migrate
|
|
151
|
+
console.log('Migrating settings.json from v1 to v2...');
|
|
152
|
+
const migrated = migrateV1toV2(raw);
|
|
153
|
+
_settings = migrated;
|
|
154
|
+
await saveFile(filename, JSON.stringify(migrated, null, 4));
|
|
155
|
+
} else {
|
|
156
|
+
_settings = raw as Partial<SettingsV2>;
|
|
157
|
+
}
|
|
47
158
|
} catch {
|
|
48
159
|
// Invalid JSON
|
|
49
160
|
}
|
|
50
161
|
}
|
|
51
162
|
}
|
|
52
163
|
|
|
53
|
-
|
|
54
|
-
|
|
164
|
+
const merged = {...DefaultSettings, ..._settings, models: _settings?.models ?? DefaultSettings.models};
|
|
165
|
+
|
|
166
|
+
// Auto-migrate: copy services into connectors if connectors is empty
|
|
167
|
+
const connectors = merged.connectors ?? {};
|
|
168
|
+
const services = merged.services ?? {};
|
|
169
|
+
if (Object.keys(connectors).length === 0 && Object.keys(services).length > 0) {
|
|
170
|
+
merged.connectors = { ...services };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return merged;
|
|
55
174
|
}
|
|
56
175
|
|
|
57
|
-
export async function saveSettings(folder: string, settings: Partial<
|
|
176
|
+
export async function saveSettings(folder: string, settings: Partial<SettingsV2>): Promise<void> {
|
|
58
177
|
_settings = {..._settings, ...settings};
|
|
178
|
+
if (settings.models) {
|
|
179
|
+
_settings.models = settings.models;
|
|
180
|
+
}
|
|
181
|
+
_settings.version = 2;
|
|
59
182
|
await saveFile(path.join(folder, 'settings.json'), JSON.stringify(_settings, null, 4));
|
|
60
183
|
}
|
package/src/synthos-cli.ts
CHANGED
|
@@ -20,9 +20,19 @@ export async function run() {
|
|
|
20
20
|
type: 'boolean',
|
|
21
21
|
default: true
|
|
22
22
|
})
|
|
23
|
+
.option('debug', {
|
|
24
|
+
describe: `Log all server requests with timing and page update summaries.`,
|
|
25
|
+
type: 'boolean',
|
|
26
|
+
default: false
|
|
27
|
+
})
|
|
28
|
+
.option('debug-page-updates', {
|
|
29
|
+
describe: `Log model input/output for page transformations to the console.`,
|
|
30
|
+
type: 'boolean',
|
|
31
|
+
default: false
|
|
32
|
+
})
|
|
23
33
|
.demandOption([]);
|
|
24
34
|
}, async (args) => {
|
|
25
|
-
const config = createConfig();
|
|
35
|
+
const config = createConfig('.synthos', { debug: args.debug, debugPageUpdates: args.debugPageUpdates });
|
|
26
36
|
await init(config, args.pages);
|
|
27
37
|
await server(config).listen(args.port, async () => {
|
|
28
38
|
console.log(`SynthOS server is running on http://localhost:${args.port}`);
|
package/src/themes.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { checkIfExists, listFiles, loadFile } from './files';
|
|
3
|
+
import { SynthOSConfig } from './init';
|
|
4
|
+
|
|
5
|
+
export interface ThemeInfo {
|
|
6
|
+
mode: 'light' | 'dark';
|
|
7
|
+
colors: Record<string, string>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function userThemesFolder(config: SynthOSConfig): string {
|
|
11
|
+
return path.join(config.pagesFolder, 'themes');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function loadThemeInfo(name: string, config: SynthOSConfig): Promise<ThemeInfo | undefined> {
|
|
15
|
+
// Check user's local themes first, then fall back to package defaults
|
|
16
|
+
const localPath = path.join(userThemesFolder(config), `${name}.json`);
|
|
17
|
+
if (await checkIfExists(localPath)) {
|
|
18
|
+
const raw = await loadFile(localPath);
|
|
19
|
+
return raw ? JSON.parse(raw) : undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const defaultPath = path.join(config.defaultThemesFolder, `${name}.json`);
|
|
23
|
+
if (await checkIfExists(defaultPath)) {
|
|
24
|
+
const raw = await loadFile(defaultPath);
|
|
25
|
+
return raw ? JSON.parse(raw) : undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function loadTheme(name: string, config: SynthOSConfig): Promise<string | undefined> {
|
|
32
|
+
// Check user's local themes first, then fall back to package defaults
|
|
33
|
+
const localPath = path.join(userThemesFolder(config), `${name}.css`);
|
|
34
|
+
if (await checkIfExists(localPath)) {
|
|
35
|
+
return await loadFile(localPath);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const defaultPath = path.join(config.defaultThemesFolder, `${name}.css`);
|
|
39
|
+
if (await checkIfExists(defaultPath)) {
|
|
40
|
+
return await loadFile(defaultPath);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function listThemes(config: SynthOSConfig): Promise<string[]> {
|
|
47
|
+
const names = new Set<string>();
|
|
48
|
+
|
|
49
|
+
// Collect from user's local themes folder
|
|
50
|
+
const localFolder = userThemesFolder(config);
|
|
51
|
+
if (await checkIfExists(localFolder)) {
|
|
52
|
+
const files = await listFiles(localFolder);
|
|
53
|
+
for (const f of files) {
|
|
54
|
+
if (f.endsWith('.css')) {
|
|
55
|
+
names.add(f.replace(/\.css$/, ''));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Collect from package defaults
|
|
61
|
+
if (await checkIfExists(config.defaultThemesFolder)) {
|
|
62
|
+
const files = await listFiles(config.defaultThemesFolder);
|
|
63
|
+
for (const f of files) {
|
|
64
|
+
if (f.endsWith('.css')) {
|
|
65
|
+
names.add(f.replace(/\.css$/, ''));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return Array.from(names).sort();
|
|
71
|
+
}
|