synthos 0.5.0 → 0.7.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 +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/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 +5 -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,12 +1,49 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { listPages, loadPageMetadata, PageMetadata, savePageMetadata, REQUIRED_PAGES, deletePage, copyPage, loadPageState, savePageState, PAGE_VERSION } from "../pages";
|
|
3
|
+
import { checkIfExists, copyFile, deleteFile, loadFile } from "../files";
|
|
4
|
+
import {getModelEntry, loadSettings, saveSettings, ServicesConfig } from "../settings";
|
|
3
5
|
import { Application } from 'express';
|
|
4
6
|
import { SynthOSConfig } from "../init";
|
|
5
|
-
import {
|
|
7
|
+
import { createCompletePrompt, PROVIDERS } from "./createCompletePrompt";
|
|
6
8
|
import { generateDefaultImage, generateImage } from "./generateImage";
|
|
7
9
|
import { chainOfThought } from "agentm-core";
|
|
8
10
|
import { requiresSettings } from "./requiresSettings";
|
|
9
11
|
import { executeScript } from "../scripts";
|
|
12
|
+
import { listThemes, loadTheme, loadThemeInfo } from "../themes";
|
|
13
|
+
import { migratePage } from "../migrations";
|
|
14
|
+
import { loadPageWithFallback } from "./usePageRoutes";
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Service registry
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
interface ServiceField {
|
|
21
|
+
name: string;
|
|
22
|
+
label: string;
|
|
23
|
+
type: 'password' | 'text';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface ServiceDefinition {
|
|
27
|
+
id: string;
|
|
28
|
+
name: string;
|
|
29
|
+
category: string;
|
|
30
|
+
description: string;
|
|
31
|
+
fields: ServiceField[];
|
|
32
|
+
exclusive?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const SERVICE_REGISTRY: ServiceDefinition[] = [
|
|
36
|
+
{
|
|
37
|
+
id: 'brave-search',
|
|
38
|
+
name: 'Brave Search',
|
|
39
|
+
category: 'Search',
|
|
40
|
+
description: 'Web search powered by the Brave Search API. Provides real-time search results from the web.',
|
|
41
|
+
fields: [
|
|
42
|
+
{ name: 'apiKey', label: 'API Key', type: 'password' }
|
|
43
|
+
],
|
|
44
|
+
exclusive: 'search'
|
|
45
|
+
}
|
|
46
|
+
];
|
|
10
47
|
|
|
11
48
|
export function useApiRoutes(config: SynthOSConfig, app: Application): void {
|
|
12
49
|
// List pages
|
|
@@ -15,27 +52,253 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
|
|
|
15
52
|
res.json(pages);
|
|
16
53
|
});
|
|
17
54
|
|
|
55
|
+
// Get page metadata
|
|
56
|
+
app.get('/api/pages/:name', async (req, res) => {
|
|
57
|
+
try {
|
|
58
|
+
const { name } = req.params;
|
|
59
|
+
const metadata = await loadPageMetadata(config.pagesFolder, name, config.requiredPagesFolder);
|
|
60
|
+
if (metadata) {
|
|
61
|
+
res.json(metadata);
|
|
62
|
+
} else {
|
|
63
|
+
const defaults: PageMetadata = {
|
|
64
|
+
title: '',
|
|
65
|
+
categories: [],
|
|
66
|
+
pinned: false,
|
|
67
|
+
showInAll: true,
|
|
68
|
+
createdDate: '',
|
|
69
|
+
lastModified: '',
|
|
70
|
+
pageVersion: 0,
|
|
71
|
+
mode: 'unlocked',
|
|
72
|
+
};
|
|
73
|
+
res.json(defaults);
|
|
74
|
+
}
|
|
75
|
+
} catch (err: unknown) {
|
|
76
|
+
console.error(err);
|
|
77
|
+
res.status(500).send((err as Error).message);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Save page metadata (merge semantics)
|
|
82
|
+
app.post('/api/pages/:name', async (req, res) => {
|
|
83
|
+
try {
|
|
84
|
+
const { name } = req.params;
|
|
85
|
+
const body = req.body;
|
|
86
|
+
|
|
87
|
+
// Validate provided fields only
|
|
88
|
+
if ('title' in body && typeof body.title !== 'string') {
|
|
89
|
+
res.status(400).json({ error: 'title must be a string' });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if ('categories' in body && !Array.isArray(body.categories)) {
|
|
93
|
+
res.status(400).json({ error: 'categories must be an array' });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if ('pinned' in body && typeof body.pinned !== 'boolean') {
|
|
97
|
+
res.status(400).json({ error: 'pinned must be a boolean' });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if ('mode' in body && body.mode !== 'unlocked' && body.mode !== 'locked') {
|
|
101
|
+
res.status(400).json({ error: 'mode must be "unlocked" or "locked"' });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if ('showInAll' in body && typeof body.showInAll !== 'boolean') {
|
|
105
|
+
res.status(400).json({ error: 'showInAll must be a boolean' });
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Load existing metadata (or defaults)
|
|
110
|
+
const existing = await loadPageMetadata(config.pagesFolder, name, config.requiredPagesFolder);
|
|
111
|
+
const metadata: PageMetadata = {
|
|
112
|
+
title: existing?.title ?? '',
|
|
113
|
+
categories: existing?.categories ?? [],
|
|
114
|
+
pinned: existing?.pinned ?? false,
|
|
115
|
+
showInAll: existing?.showInAll ?? true,
|
|
116
|
+
createdDate: existing?.createdDate ?? '',
|
|
117
|
+
lastModified: existing?.lastModified ?? '',
|
|
118
|
+
pageVersion: existing?.pageVersion ?? 0,
|
|
119
|
+
mode: existing?.mode ?? 'unlocked',
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// Overlay provided fields
|
|
123
|
+
if ('title' in body) metadata.title = body.title;
|
|
124
|
+
if ('categories' in body) metadata.categories = body.categories;
|
|
125
|
+
if ('pinned' in body) metadata.pinned = body.pinned;
|
|
126
|
+
if ('showInAll' in body) metadata.showInAll = body.showInAll;
|
|
127
|
+
if ('mode' in body) metadata.mode = body.mode;
|
|
128
|
+
|
|
129
|
+
// Auto-set lastModified
|
|
130
|
+
metadata.lastModified = new Date().toISOString();
|
|
131
|
+
|
|
132
|
+
// Promote required page to user folder if being unlocked/designed
|
|
133
|
+
if (metadata.mode !== 'locked') {
|
|
134
|
+
const userPagePath = path.join(config.pagesFolder, 'pages', name, 'page.html');
|
|
135
|
+
if (!(await checkIfExists(userPagePath))) {
|
|
136
|
+
const html = await loadPageState(config.requiredPagesFolder, name, false);
|
|
137
|
+
if (html) {
|
|
138
|
+
await savePageState(config.pagesFolder, name, html);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
await savePageMetadata(config.pagesFolder, name, metadata);
|
|
144
|
+
res.json(metadata);
|
|
145
|
+
} catch (err: unknown) {
|
|
146
|
+
console.error(err);
|
|
147
|
+
res.status(500).send((err as Error).message);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Pin/unpin a page
|
|
152
|
+
app.post('/api/pages/:name/pin', async (req, res) => {
|
|
153
|
+
try {
|
|
154
|
+
const { name } = req.params;
|
|
155
|
+
const { pinned } = req.body;
|
|
156
|
+
if (typeof pinned !== 'boolean') {
|
|
157
|
+
res.status(400).json({ error: 'pinned must be a boolean' });
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Load existing metadata (user override → fallback .json → defaults)
|
|
162
|
+
let metadata = await loadPageMetadata(config.pagesFolder, name, config.requiredPagesFolder);
|
|
163
|
+
if (!metadata) {
|
|
164
|
+
metadata = {
|
|
165
|
+
title: '',
|
|
166
|
+
categories: [],
|
|
167
|
+
pinned: false,
|
|
168
|
+
showInAll: true,
|
|
169
|
+
createdDate: '',
|
|
170
|
+
lastModified: '',
|
|
171
|
+
pageVersion: 0,
|
|
172
|
+
mode: 'unlocked',
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
metadata.pinned = pinned;
|
|
177
|
+
await savePageMetadata(config.pagesFolder, name, metadata);
|
|
178
|
+
res.json(metadata);
|
|
179
|
+
} catch (err: unknown) {
|
|
180
|
+
console.error(err);
|
|
181
|
+
res.status(500).send((err as Error).message);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Delete a page
|
|
186
|
+
app.delete('/api/pages/:name', async (req, res) => {
|
|
187
|
+
try {
|
|
188
|
+
const { name } = req.params;
|
|
189
|
+
|
|
190
|
+
// Cannot delete required pages
|
|
191
|
+
if (REQUIRED_PAGES.includes(name)) {
|
|
192
|
+
res.status(400).json({ error: `Cannot delete required page "${name}"` });
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Check if page exists (folder-based or legacy flat file)
|
|
197
|
+
const folderPath = path.join(config.pagesFolder, 'pages', name, 'page.html');
|
|
198
|
+
const flatPath = path.join(config.pagesFolder, `${name}.html`);
|
|
199
|
+
const exists = await checkIfExists(folderPath) || await checkIfExists(flatPath);
|
|
200
|
+
if (!exists) {
|
|
201
|
+
res.status(404).json({ error: `Page "${name}" not found` });
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
await deletePage(config.pagesFolder, name);
|
|
206
|
+
res.json({ deleted: true });
|
|
207
|
+
} catch (err: unknown) {
|
|
208
|
+
console.error(err);
|
|
209
|
+
res.status(500).send((err as Error).message);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// Copy a page to a new name
|
|
214
|
+
app.post('/api/pages/:name/copy', async (req, res) => {
|
|
215
|
+
try {
|
|
216
|
+
const sourceName = req.params.name;
|
|
217
|
+
const { name: targetName, title, categories } = req.body;
|
|
218
|
+
|
|
219
|
+
// Validate target name
|
|
220
|
+
if (!targetName || typeof targetName !== 'string') {
|
|
221
|
+
res.status(400).json({ error: 'name is required' });
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(targetName)) {
|
|
226
|
+
res.status(400).json({ error: 'name can only contain letters, numbers, hyphens, and underscores' });
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Can't copy over yourself
|
|
231
|
+
if (targetName === sourceName) {
|
|
232
|
+
res.status(400).json({ error: 'Cannot copy a page to itself' });
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Check source exists (user pages → required pages)
|
|
237
|
+
const sourceFolderPath = path.join(config.pagesFolder, 'pages', sourceName, 'page.html');
|
|
238
|
+
const sourceFlatPath = path.join(config.pagesFolder, `${sourceName}.html`);
|
|
239
|
+
const sourceRequiredPath = path.join(config.requiredPagesFolder, `${sourceName}.html`);
|
|
240
|
+
const sourceExists = await checkIfExists(sourceFolderPath)
|
|
241
|
+
|| await checkIfExists(sourceFlatPath)
|
|
242
|
+
|| await checkIfExists(sourceRequiredPath);
|
|
243
|
+
if (!sourceExists) {
|
|
244
|
+
res.status(404).json({ error: `Source page "${sourceName}" not found` });
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Check target doesn't already exist
|
|
249
|
+
const targetFolderPath = path.join(config.pagesFolder, 'pages', targetName, 'page.html');
|
|
250
|
+
const targetFlatPath = path.join(config.pagesFolder, `${targetName}.html`);
|
|
251
|
+
if (await checkIfExists(targetFolderPath) || await checkIfExists(targetFlatPath)) {
|
|
252
|
+
res.status(409).json({ error: `Page "${targetName}" already exists` });
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
await copyPage(
|
|
257
|
+
config.pagesFolder,
|
|
258
|
+
sourceName,
|
|
259
|
+
targetName,
|
|
260
|
+
typeof title === 'string' ? title : '',
|
|
261
|
+
Array.isArray(categories) ? categories : [],
|
|
262
|
+
config.requiredPagesFolder
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
// Return the new page metadata
|
|
266
|
+
const metadata = await loadPageMetadata(config.pagesFolder, targetName);
|
|
267
|
+
res.status(201).json({ name: targetName, ...metadata });
|
|
268
|
+
} catch (err: unknown) {
|
|
269
|
+
console.error(err);
|
|
270
|
+
res.status(500).send((err as Error).message);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
|
|
18
274
|
// Define a route to return settings
|
|
19
275
|
app.get('/api/settings', async (req, res) => {
|
|
20
276
|
const settings = await loadSettings(config.pagesFolder);
|
|
21
|
-
|
|
277
|
+
const providers = PROVIDERS.map(p => ({ name: p.name, builderModels: p.builderModels, chatModels: p.chatModels }));
|
|
278
|
+
res.json({...settings, providers});
|
|
22
279
|
});
|
|
23
280
|
|
|
24
281
|
// Define a route to save settings
|
|
25
282
|
app.post('/api/settings', async (req, res) => {
|
|
26
283
|
try {
|
|
27
|
-
//
|
|
284
|
+
// Coerce non-string values inside models array
|
|
28
285
|
const settings = req.body as Record<string, any>;
|
|
29
|
-
if (
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
286
|
+
if (Array.isArray(settings.models)) {
|
|
287
|
+
for (const entry of settings.models) {
|
|
288
|
+
if (entry.configuration) {
|
|
289
|
+
if (typeof entry.configuration.maxTokens === 'string') {
|
|
290
|
+
entry.configuration.maxTokens = parseInt(entry.configuration.maxTokens);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (typeof entry.logCompletions === 'string') {
|
|
294
|
+
entry.logCompletions = entry.logCompletions === 'true';
|
|
295
|
+
}
|
|
296
|
+
}
|
|
34
297
|
}
|
|
35
298
|
|
|
36
299
|
// Save settings
|
|
37
300
|
await saveSettings(config.pagesFolder, settings);
|
|
38
|
-
res.redirect('/
|
|
301
|
+
res.redirect('/builder');
|
|
39
302
|
} catch (err: unknown) {
|
|
40
303
|
console.error(err);
|
|
41
304
|
res.status(500).send((err as Error).message);
|
|
@@ -46,9 +309,10 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
|
|
|
46
309
|
app.post('/api/generate/image', async (req, res) => {
|
|
47
310
|
await requiresSettings(res, config.pagesFolder, async (settings) => {
|
|
48
311
|
const { prompt, shape, style } = req.body;
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
|
|
312
|
+
const builder = getModelEntry(settings, 'builder');
|
|
313
|
+
const { configuration, imageQuality, provider } = builder;
|
|
314
|
+
const response = provider === 'OpenAI' ?
|
|
315
|
+
await generateImage({ apiKey: configuration.apiKey, prompt, shape, quality: imageQuality, style }) :
|
|
52
316
|
await generateDefaultImage();
|
|
53
317
|
if (response.completed) {
|
|
54
318
|
res.json(response.value);
|
|
@@ -62,8 +326,8 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
|
|
|
62
326
|
app.post('/api/generate/completion', async (req, res) => {
|
|
63
327
|
await requiresSettings(res, config.pagesFolder, async (settings) => {
|
|
64
328
|
const { prompt, temperature } = req.body;
|
|
65
|
-
const
|
|
66
|
-
const completePrompt = await createCompletePrompt(config.pagesFolder, req.body.model);
|
|
329
|
+
const maxTokens = getModelEntry(settings, 'chat').configuration.maxTokens;
|
|
330
|
+
const completePrompt = await createCompletePrompt(config.pagesFolder, 'chat', req.body.model);
|
|
67
331
|
const response = await chainOfThought({ question: prompt, temperature, maxTokens, completePrompt });
|
|
68
332
|
if (response.completed) {
|
|
69
333
|
res.json(response.value ?? {});
|
|
@@ -74,6 +338,64 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
|
|
|
74
338
|
});
|
|
75
339
|
});
|
|
76
340
|
|
|
341
|
+
// Brainstorm endpoint
|
|
342
|
+
app.post('/api/brainstorm', async (req, res) => {
|
|
343
|
+
await requiresSettings(res, config.pagesFolder, async (settings) => {
|
|
344
|
+
const { context, messages } = req.body;
|
|
345
|
+
const maxTokens = getModelEntry(settings, 'chat').configuration.maxTokens;
|
|
346
|
+
const completePrompt = await createCompletePrompt(config.pagesFolder, 'chat');
|
|
347
|
+
|
|
348
|
+
const system: { role: 'system'; content: string } = {
|
|
349
|
+
role: 'system',
|
|
350
|
+
content: `You are a creative brainstorming assistant for SynthOS, a tool that builds web pages through conversation. The user is brainstorming — exploring ideas before building. Be concise, creative, and collaborative. Suggest concrete approaches when you can.
|
|
351
|
+
|
|
352
|
+
You MUST return your response as a JSON object with exactly these fields:
|
|
353
|
+
{
|
|
354
|
+
"response": "Your conversational reply — explanations, options, suggestions. Markdown OK.",
|
|
355
|
+
"prompt": "A clean, actionable instruction ready to paste into SynthOS chat to build what was discussed. Update this each exchange to reflect the latest brainstorm state.",
|
|
356
|
+
"suggestions": ["Short clickable option A", "Short clickable option B", "Short clickable option C"]
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
suggestions — 2-4 short phrases the user can click to continue the conversation. These are next-step options: directions to explore, questions to answer, or choices to make. Keep each under 60 characters. Always provide suggestions.
|
|
360
|
+
|
|
361
|
+
Return ONLY the JSON object. No markdown fences.
|
|
362
|
+
|
|
363
|
+
<CONTEXT>
|
|
364
|
+
${context}
|
|
365
|
+
</CONTEXT>`
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
// Format multi-turn conversation into a single prompt
|
|
369
|
+
const formatted = (messages as { role: string; content: string }[]).map(m =>
|
|
370
|
+
`${m.role === 'user' ? 'User' : 'Assistant'}: ${m.content}`
|
|
371
|
+
).join('\n\n');
|
|
372
|
+
|
|
373
|
+
const prompt: { role: 'user'; content: string } = { role: 'user', content: formatted };
|
|
374
|
+
|
|
375
|
+
const result = await completePrompt({ prompt, system, maxTokens, jsonMode: true });
|
|
376
|
+
if (result.completed) {
|
|
377
|
+
let response = result.value || '';
|
|
378
|
+
let brainstormPrompt = '';
|
|
379
|
+
let suggestions: string[] = [];
|
|
380
|
+
// jsonMode returns an already-parsed object from agentm-core
|
|
381
|
+
const parsed = (typeof result.value === 'object' && result.value !== null)
|
|
382
|
+
? result.value as Record<string, unknown>
|
|
383
|
+
: (() => { try { return JSON.parse(result.value as string); } catch { return null; } })();
|
|
384
|
+
if (parsed) {
|
|
385
|
+
if (typeof parsed.response === 'string') response = parsed.response;
|
|
386
|
+
if (typeof parsed.prompt === 'string') brainstormPrompt = parsed.prompt;
|
|
387
|
+
if (Array.isArray(parsed.suggestions)) {
|
|
388
|
+
suggestions = parsed.suggestions.filter((s: unknown) => typeof s === 'string');
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
res.json({ response, prompt: brainstormPrompt, suggestions });
|
|
392
|
+
} else {
|
|
393
|
+
console.error(result.error);
|
|
394
|
+
res.status(500).send(result.error?.message);
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
|
|
77
399
|
// Define a route for running configured scripts
|
|
78
400
|
app.post('/api/scripts/:id', async (req, res) => {
|
|
79
401
|
await requiresSettings(res, config.pagesFolder, async (settings) => {
|
|
@@ -92,4 +414,298 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
|
|
|
92
414
|
}
|
|
93
415
|
});
|
|
94
416
|
});
|
|
417
|
+
|
|
418
|
+
// Return theme info as a self-executing JS script
|
|
419
|
+
app.get('/api/theme-info.js', async (req, res) => {
|
|
420
|
+
try {
|
|
421
|
+
const settings = await loadSettings(config.pagesFolder);
|
|
422
|
+
const themeName = settings.theme ?? 'nebula-dusk';
|
|
423
|
+
const info = await loadThemeInfo(themeName, config);
|
|
424
|
+
if (!info) {
|
|
425
|
+
res.status(404).send(`// Theme info for "${themeName}" not found`);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
const js = `window.themeInfo=${JSON.stringify(info)};document.documentElement.classList.add(window.themeInfo.mode+"-mode");`;
|
|
429
|
+
res.set('Content-Type', 'application/javascript');
|
|
430
|
+
res.send(js);
|
|
431
|
+
} catch (err: unknown) {
|
|
432
|
+
console.error(err);
|
|
433
|
+
res.status(500).send(`// ${(err as Error).message}`);
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// Return page info as a self-executing JS script
|
|
438
|
+
app.get('/api/page-info.js', async (req, res) => {
|
|
439
|
+
try {
|
|
440
|
+
const page = req.query.page as string;
|
|
441
|
+
if (!page) {
|
|
442
|
+
res.status(400).send('// Missing page query parameter');
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
const metadata = await loadPageMetadata(config.pagesFolder, page, config.requiredPagesFolder);
|
|
446
|
+
const mode = metadata?.mode ?? 'unlocked';
|
|
447
|
+
const title = metadata?.title ?? '';
|
|
448
|
+
const categories = metadata?.categories ?? [];
|
|
449
|
+
const info = JSON.stringify({ name: page, mode, latestPageVersion: PAGE_VERSION, title, categories });
|
|
450
|
+
const js = [
|
|
451
|
+
`window.pageInfo=${info};`,
|
|
452
|
+
`if(window.pageInfo.mode==="locked"){`,
|
|
453
|
+
`document.addEventListener("DOMContentLoaded",function(){`,
|
|
454
|
+
`var f=document.getElementById("chatForm");if(f)f.style.display="none";`,
|
|
455
|
+
`var s=document.getElementById("saveLink");if(s)s.textContent="Copy";`,
|
|
456
|
+
`var r=document.getElementById("resetLink");if(r){`,
|
|
457
|
+
`var c=r.cloneNode(true);c.textContent="Reload";`,
|
|
458
|
+
`c.addEventListener("click",function(e){e.preventDefault();window.location.href=window.location.pathname;});`,
|
|
459
|
+
`r.parentNode.replaceChild(c,r);}`,
|
|
460
|
+
`});`,
|
|
461
|
+
`}`,
|
|
462
|
+
].join('');
|
|
463
|
+
res.set('Content-Type', 'application/javascript');
|
|
464
|
+
res.set('Cache-Control', 'no-store');
|
|
465
|
+
res.send(js);
|
|
466
|
+
} catch (err: unknown) {
|
|
467
|
+
console.error(err);
|
|
468
|
+
res.status(500).send(`// ${(err as Error).message}`);
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
// Return the current theme as CSS
|
|
473
|
+
app.get('/api/theme.css', async (req, res) => {
|
|
474
|
+
try {
|
|
475
|
+
const settings = await loadSettings(config.pagesFolder);
|
|
476
|
+
const themeName = settings.theme ?? 'nebula-dusk';
|
|
477
|
+
const css = await loadTheme(themeName, config);
|
|
478
|
+
if (!css) {
|
|
479
|
+
res.status(404).send(`Theme "${themeName}" not found`);
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
res.set('Content-Type', 'text/css');
|
|
483
|
+
res.send(css);
|
|
484
|
+
} catch (err: unknown) {
|
|
485
|
+
console.error(err);
|
|
486
|
+
res.status(500).send((err as Error).message);
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
// List available themes
|
|
491
|
+
app.get('/api/themes', async (req, res) => {
|
|
492
|
+
try {
|
|
493
|
+
const themes = await listThemes(config);
|
|
494
|
+
res.json(themes);
|
|
495
|
+
} catch (err: unknown) {
|
|
496
|
+
console.error(err);
|
|
497
|
+
res.status(500).send((err as Error).message);
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
// Return a versioned page script
|
|
502
|
+
app.get('/api/page-script.js', async (req, res) => {
|
|
503
|
+
try {
|
|
504
|
+
const v = parseInt(req.query.v as string, 10);
|
|
505
|
+
if (isNaN(v) || v < 1) {
|
|
506
|
+
res.status(400).send('// Invalid version parameter');
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
const scriptPath = path.join(config.pageScriptsFolder, `page-v${v}.js`);
|
|
510
|
+
if (!(await checkIfExists(scriptPath))) {
|
|
511
|
+
res.status(404).send(`// page-v${v}.js not found`);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
const js = await loadFile(scriptPath);
|
|
515
|
+
res.set('Content-Type', 'application/javascript');
|
|
516
|
+
res.set('Cache-Control', 'public, max-age=3600');
|
|
517
|
+
res.send(js);
|
|
518
|
+
} catch (err: unknown) {
|
|
519
|
+
console.error(err);
|
|
520
|
+
res.status(500).send(`// ${(err as Error).message}`);
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
// Return versioned page helpers
|
|
525
|
+
app.get('/api/page-helpers.js', async (req, res) => {
|
|
526
|
+
try {
|
|
527
|
+
const v = parseInt(req.query.v as string, 10);
|
|
528
|
+
if (isNaN(v) || v < 1) {
|
|
529
|
+
res.status(400).send('// Invalid version parameter');
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
const scriptPath = path.join(config.pageScriptsFolder, `helpers-v${v}.js`);
|
|
533
|
+
if (!(await checkIfExists(scriptPath))) {
|
|
534
|
+
res.status(404).send(`// helpers-v${v}.js not found`);
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
const js = await loadFile(scriptPath);
|
|
538
|
+
res.set('Content-Type', 'application/javascript');
|
|
539
|
+
res.set('Cache-Control', 'public, max-age=3600');
|
|
540
|
+
res.send(js);
|
|
541
|
+
} catch (err: unknown) {
|
|
542
|
+
console.error(err);
|
|
543
|
+
res.status(500).send(`// ${(err as Error).message}`);
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
// -----------------------------------------------------------------------
|
|
548
|
+
// Services
|
|
549
|
+
// -----------------------------------------------------------------------
|
|
550
|
+
|
|
551
|
+
// Return the service registry (available service definitions)
|
|
552
|
+
app.get('/api/services/registry', (_req, res) => {
|
|
553
|
+
res.json(SERVICE_REGISTRY);
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
// Return user's configured services (API keys masked)
|
|
557
|
+
app.get('/api/services', async (_req, res) => {
|
|
558
|
+
try {
|
|
559
|
+
const settings = await loadSettings(config.pagesFolder);
|
|
560
|
+
const services = settings.services ?? {};
|
|
561
|
+
const masked: Record<string, { enabled: boolean; hasKey: boolean }> = {};
|
|
562
|
+
for (const [id, cfg] of Object.entries(services)) {
|
|
563
|
+
masked[id] = {
|
|
564
|
+
enabled: cfg.enabled,
|
|
565
|
+
hasKey: typeof cfg.apiKey === 'string' && cfg.apiKey.length > 0
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
res.json(masked);
|
|
569
|
+
} catch (err: unknown) {
|
|
570
|
+
console.error(err);
|
|
571
|
+
res.status(500).send((err as Error).message);
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
// Save services config (enforces exclusive groups)
|
|
576
|
+
app.post('/api/services', async (req, res) => {
|
|
577
|
+
try {
|
|
578
|
+
const incoming = req.body as ServicesConfig;
|
|
579
|
+
const settings = await loadSettings(config.pagesFolder);
|
|
580
|
+
const existing = settings.services ?? {};
|
|
581
|
+
|
|
582
|
+
// Build merged config — empty apiKey means "keep existing"
|
|
583
|
+
const merged: ServicesConfig = {};
|
|
584
|
+
for (const [id, cfg] of Object.entries(incoming)) {
|
|
585
|
+
const apiKey = (cfg.apiKey && cfg.apiKey.length > 0) ? cfg.apiKey : (existing[id]?.apiKey ?? '');
|
|
586
|
+
merged[id] = { apiKey, enabled: cfg.enabled };
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Enforce exclusive groups: only one enabled per group
|
|
590
|
+
for (const def of SERVICE_REGISTRY) {
|
|
591
|
+
if (!def.exclusive) continue;
|
|
592
|
+
if (!merged[def.id]?.enabled) continue;
|
|
593
|
+
for (const other of SERVICE_REGISTRY) {
|
|
594
|
+
if (other.id !== def.id && other.exclusive === def.exclusive && merged[other.id]) {
|
|
595
|
+
merged[other.id].enabled = false;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
await saveSettings(config.pagesFolder, { services: merged });
|
|
601
|
+
res.json({ saved: true });
|
|
602
|
+
} catch (err: unknown) {
|
|
603
|
+
console.error(err);
|
|
604
|
+
res.status(500).send((err as Error).message);
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
// -----------------------------------------------------------------------
|
|
609
|
+
// Web Search (Brave Search API)
|
|
610
|
+
// -----------------------------------------------------------------------
|
|
611
|
+
|
|
612
|
+
app.post('/api/search/web', async (req, res) => {
|
|
613
|
+
try {
|
|
614
|
+
const { query, count, country, freshness } = req.body;
|
|
615
|
+
if (!query || typeof query !== 'string') {
|
|
616
|
+
res.status(400).json({ error: 'query is required' });
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const settings = await loadSettings(config.pagesFolder);
|
|
621
|
+
const braveConfig = settings.connectors?.['brave-search'] ?? settings.services?.['brave-search'];
|
|
622
|
+
if (!braveConfig || !braveConfig.enabled || !braveConfig.apiKey) {
|
|
623
|
+
res.status(400).json({ error: 'Brave Search is not configured or not enabled. Add your API key in Settings > Services.' });
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const params = new URLSearchParams({ q: query });
|
|
628
|
+
if (count) params.set('count', String(Math.min(Number(count) || 5, 20)));
|
|
629
|
+
if (country) params.set('country', country);
|
|
630
|
+
if (freshness) params.set('freshness', freshness);
|
|
631
|
+
|
|
632
|
+
const response = await fetch(`https://api.search.brave.com/res/v1/web/search?${params.toString()}`, {
|
|
633
|
+
headers: {
|
|
634
|
+
'Accept': 'application/json',
|
|
635
|
+
'Accept-Encoding': 'gzip',
|
|
636
|
+
'X-Subscription-Token': braveConfig.apiKey
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
if (!response.ok) {
|
|
641
|
+
const text = await response.text();
|
|
642
|
+
res.status(response.status).json({ error: `Brave Search API error: ${text}` });
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const data = await response.json() as { web?: { results?: Array<{ title: string; url: string; description: string }> } };
|
|
647
|
+
const results = (data.web?.results ?? []).map(r => ({
|
|
648
|
+
title: r.title,
|
|
649
|
+
url: r.url,
|
|
650
|
+
description: r.description
|
|
651
|
+
}));
|
|
652
|
+
|
|
653
|
+
res.json({ results });
|
|
654
|
+
} catch (err: unknown) {
|
|
655
|
+
console.error(err);
|
|
656
|
+
res.status(500).json({ error: (err as Error).message });
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
// Upgrade a page to the latest version
|
|
661
|
+
app.post('/api/pages/:name/upgrade', async (req, res) => {
|
|
662
|
+
try {
|
|
663
|
+
const { name } = req.params;
|
|
664
|
+
|
|
665
|
+
// Load current metadata
|
|
666
|
+
const metadata = await loadPageMetadata(config.pagesFolder, name, config.requiredPagesFolder);
|
|
667
|
+
if (!metadata) {
|
|
668
|
+
res.status(404).json({ error: `Page "${name}" not found` });
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
const currentVersion = metadata.pageVersion;
|
|
673
|
+
if (currentVersion >= PAGE_VERSION) {
|
|
674
|
+
res.json({ upgraded: false, currentVersion });
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Load the page HTML
|
|
679
|
+
const html = await loadPageWithFallback(name, config, false);
|
|
680
|
+
if (!html) {
|
|
681
|
+
res.status(404).json({ error: `Page HTML for "${name}" not found` });
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Run LLM-based migration
|
|
686
|
+
const completePrompt = await createCompletePrompt(config.pagesFolder, 'builder');
|
|
687
|
+
const migratedHtml = await migratePage(html, currentVersion, PAGE_VERSION, completePrompt);
|
|
688
|
+
|
|
689
|
+
// Save upgraded HTML to v2 folder structure
|
|
690
|
+
await savePageState(config.pagesFolder, name, migratedHtml);
|
|
691
|
+
|
|
692
|
+
// Move legacy flat file to .migrated folder instead of deleting
|
|
693
|
+
const flatPath = path.join(config.pagesFolder, `${name}.html`);
|
|
694
|
+
if (await checkIfExists(flatPath)) {
|
|
695
|
+
const migratedFolder = path.join(config.pagesFolder, '.migrated');
|
|
696
|
+
await copyFile(flatPath, migratedFolder);
|
|
697
|
+
await deleteFile(flatPath);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Update metadata
|
|
701
|
+
metadata.pageVersion = PAGE_VERSION;
|
|
702
|
+
metadata.lastModified = new Date().toISOString();
|
|
703
|
+
await savePageMetadata(config.pagesFolder, name, metadata);
|
|
704
|
+
|
|
705
|
+
res.json({ upgraded: true, fromVersion: currentVersion, toVersion: PAGE_VERSION });
|
|
706
|
+
} catch (err: unknown) {
|
|
707
|
+
console.error(err);
|
|
708
|
+
res.status(500).json({ error: (err as Error).message });
|
|
709
|
+
}
|
|
710
|
+
});
|
|
95
711
|
}
|