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