synthos 0.9.0 → 0.10.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.
Files changed (118) hide show
  1. package/default-pages/neon_asteroids/page.html +2 -21
  2. package/default-pages/oregon_trail/page.html +3 -19
  3. package/default-pages/solar_explorer/page.html +1 -1
  4. package/default-pages/western_cities_1850/page.html +2 -2
  5. package/dist/agents/a2a/a2aProvider.d.ts +3 -0
  6. package/dist/agents/discovery.d.ts +30 -0
  7. package/dist/agents/openclaw/openclawProvider.d.ts +3 -0
  8. package/dist/agents/types.d.ts +64 -0
  9. package/dist/connectors/index.d.ts +3 -0
  10. package/dist/connectors/types.d.ts +84 -0
  11. package/dist/customizer/Customizer.d.ts +5 -0
  12. package/dist/customizer/Customizer.d.ts.map +1 -1
  13. package/dist/customizer/Customizer.js +10 -0
  14. package/dist/customizer/Customizer.js.map +1 -1
  15. package/dist/customizer/index.d.ts +4 -0
  16. package/dist/index.d.ts +9 -0
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +1 -0
  19. package/dist/index.js.map +1 -1
  20. package/dist/init.d.ts +2 -0
  21. package/dist/init.d.ts.map +1 -1
  22. package/dist/init.js +71 -37
  23. package/dist/init.js.map +1 -1
  24. package/dist/migrations.d.ts +12 -0
  25. package/dist/models/chainOfThought.d.ts +12 -0
  26. package/dist/models/fireworksai.d.ts +30 -0
  27. package/dist/models/logCompletePrompt.d.ts +3 -0
  28. package/dist/models/providers.d.ts +8 -0
  29. package/dist/models/utils.d.ts +6 -0
  30. package/dist/pages.d.ts +12 -11
  31. package/dist/pages.d.ts.map +1 -1
  32. package/dist/pages.js +94 -66
  33. package/dist/pages.js.map +1 -1
  34. package/dist/scripts.d.ts +15 -0
  35. package/dist/service/createCompletePrompt.d.ts +6 -0
  36. package/dist/service/createCompletePrompt.d.ts.map +1 -1
  37. package/dist/service/createCompletePrompt.js +2 -2
  38. package/dist/service/createCompletePrompt.js.map +1 -1
  39. package/dist/service/debugLog.d.ts +11 -0
  40. package/dist/service/generateImage.d.ts +32 -0
  41. package/dist/service/index.d.ts +8 -0
  42. package/dist/service/modelInstructions.d.ts +7 -0
  43. package/dist/service/requiresSettings.d.ts +4 -0
  44. package/dist/service/requiresSettings.d.ts.map +1 -1
  45. package/dist/service/requiresSettings.js +3 -3
  46. package/dist/service/requiresSettings.js.map +1 -1
  47. package/dist/service/server.d.ts +5 -0
  48. package/dist/service/useAgentRoutes.js +12 -12
  49. package/dist/service/useAgentRoutes.js.map +1 -1
  50. package/dist/service/useApiRoutes.d.ts +5 -0
  51. package/dist/service/useApiRoutes.d.ts.map +1 -1
  52. package/dist/service/useApiRoutes.js +74 -60
  53. package/dist/service/useApiRoutes.js.map +1 -1
  54. package/dist/service/useConnectorRoutes.d.ts +4 -0
  55. package/dist/service/useConnectorRoutes.js +11 -11
  56. package/dist/service/useConnectorRoutes.js.map +1 -1
  57. package/dist/service/useDataRoutes.d.ts +4 -0
  58. package/dist/service/useDataRoutes.d.ts.map +1 -1
  59. package/dist/service/useDataRoutes.js +13 -10
  60. package/dist/service/useDataRoutes.js.map +1 -1
  61. package/dist/service/useFileRoutes.d.ts.map +1 -1
  62. package/dist/service/useFileRoutes.js +13 -13
  63. package/dist/service/useFileRoutes.js.map +1 -1
  64. package/dist/service/usePageRoutes.d.ts +6 -0
  65. package/dist/service/usePageRoutes.d.ts.map +1 -1
  66. package/dist/service/usePageRoutes.js +54 -38
  67. package/dist/service/usePageRoutes.js.map +1 -1
  68. package/dist/service/useSharedDataRoutes.d.ts.map +1 -1
  69. package/dist/service/useSharedDataRoutes.js +13 -10
  70. package/dist/service/useSharedDataRoutes.js.map +1 -1
  71. package/dist/service/useSharedFileRoutes.d.ts.map +1 -1
  72. package/dist/service/useSharedFileRoutes.js +13 -13
  73. package/dist/service/useSharedFileRoutes.js.map +1 -1
  74. package/dist/settings.d.ts +4 -3
  75. package/dist/settings.d.ts.map +1 -1
  76. package/dist/settings.js +11 -10
  77. package/dist/settings.js.map +1 -1
  78. package/dist/storage/FsStorageProvider.d.ts +25 -0
  79. package/dist/storage/FsStorageProvider.d.ts.map +1 -0
  80. package/dist/storage/FsStorageProvider.js +103 -0
  81. package/dist/storage/FsStorageProvider.js.map +1 -0
  82. package/dist/storage/StorageProvider.d.ts +31 -0
  83. package/dist/storage/StorageProvider.d.ts.map +1 -0
  84. package/dist/storage/StorageProvider.js +3 -0
  85. package/dist/storage/StorageProvider.js.map +1 -0
  86. package/dist/storage/index.d.ts +3 -0
  87. package/dist/storage/index.d.ts.map +1 -0
  88. package/dist/storage/index.js +6 -0
  89. package/dist/storage/index.js.map +1 -0
  90. package/dist/synthos-cli.d.ts +2 -0
  91. package/dist/themes.d.ts.map +1 -1
  92. package/dist/themes.js +42 -18
  93. package/dist/themes.js.map +1 -1
  94. package/package.json +1 -1
  95. package/src/builders/anthropic.ts +5 -5
  96. package/src/customizer/Customizer.ts +12 -0
  97. package/src/index.ts +2 -1
  98. package/src/init.ts +78 -42
  99. package/src/models/providers.ts +2 -2
  100. package/src/pages.ts +98 -67
  101. package/src/service/createCompletePrompt.ts +3 -2
  102. package/src/service/requiresSettings.ts +4 -3
  103. package/src/service/useAgentRoutes.ts +12 -12
  104. package/src/service/useApiRoutes.ts +76 -61
  105. package/src/service/useConnectorRoutes.ts +11 -11
  106. package/src/service/useDataRoutes.ts +13 -10
  107. package/src/service/useFileRoutes.ts +14 -13
  108. package/src/service/usePageRoutes.ts +42 -40
  109. package/src/service/useSharedDataRoutes.ts +13 -10
  110. package/src/service/useSharedFileRoutes.ts +14 -13
  111. package/src/settings.ts +12 -10
  112. package/src/storage/FsStorageProvider.ts +87 -0
  113. package/src/storage/StorageProvider.ts +34 -0
  114. package/src/storage/index.ts +2 -0
  115. package/src/themes.ts +44 -18
  116. package/tests/builders.spec.ts +2 -2
  117. package/tests/pages.spec.ts +54 -84
  118. package/tests/providers.spec.ts +1 -1
@@ -1,17 +1,18 @@
1
1
  import { hasConfiguredSettings, loadSettings, SettingsV2 } from "../settings";
2
+ import { SynthOSConfig } from "../init";
2
3
 
3
4
 
4
- export async function requiresSettings(res: any, folder: string, cb: (settings: SettingsV2) => Promise<void>) {
5
+ export async function requiresSettings(res: any, config: SynthOSConfig, cb: (settings: SettingsV2) => Promise<void>) {
5
6
  try {
6
7
  // Ensure settings configured
7
- const isConfigured = await hasConfiguredSettings(folder);
8
+ const isConfigured = await hasConfiguredSettings(config);
8
9
  if (!isConfigured) {
9
10
  res.status(400).send('Settings not configured');
10
11
  return;
11
12
  }
12
13
 
13
14
  // Load settings
14
- const settings = await loadSettings(folder);
15
+ const settings = await loadSettings(config);
15
16
 
16
17
  // Call the callback
17
18
  await cb(settings);
@@ -54,7 +54,7 @@ export function useAgentRoutes(config: SynthOSConfig, app: Application, customiz
54
54
  // Auto-connect all enabled OpenClaw agents on startup
55
55
  (async () => {
56
56
  try {
57
- const settings = await loadSettings(config.pagesFolder);
57
+ const settings = await loadSettings(config);
58
58
  for (const agent of settings.agents ?? []) {
59
59
  tryConnectAgent(agent);
60
60
  }
@@ -66,7 +66,7 @@ export function useAgentRoutes(config: SynthOSConfig, app: Application, customiz
66
66
  // GET /api/agents — List configured agents (with optional filters)
67
67
  app.get('/api/agents', async (req, res) => {
68
68
  try {
69
- const settings = await loadSettings(config.pagesFolder);
69
+ const settings = await loadSettings(config);
70
70
  let agents = settings.agents ?? [];
71
71
 
72
72
  // Filter by enabled
@@ -122,7 +122,7 @@ export function useAgentRoutes(config: SynthOSConfig, app: Application, customiz
122
122
  return;
123
123
  }
124
124
 
125
- const settings = await loadSettings(config.pagesFolder);
125
+ const settings = await loadSettings(config);
126
126
  const agents = settings.agents ? [...settings.agents] : [];
127
127
 
128
128
  const agentConfig: AgentConfig = {
@@ -159,7 +159,7 @@ export function useAgentRoutes(config: SynthOSConfig, app: Application, customiz
159
159
  agents.push(agentConfig);
160
160
  }
161
161
 
162
- await saveSettings(config.pagesFolder, { agents });
162
+ await saveSettings(config, { agents });
163
163
 
164
164
  // Auto-connect OpenClaw agents after save
165
165
  tryConnectAgent(agentConfig);
@@ -175,7 +175,7 @@ export function useAgentRoutes(config: SynthOSConfig, app: Application, customiz
175
175
  app.patch('/api/agents/:id', async (req, res) => {
176
176
  try {
177
177
  const { id } = req.params;
178
- const settings = await loadSettings(config.pagesFolder);
178
+ const settings = await loadSettings(config);
179
179
  const agents = settings.agents ? [...settings.agents] : [];
180
180
  const idx = agents.findIndex(a => a.id === id);
181
181
  if (idx === -1) {
@@ -188,7 +188,7 @@ export function useAgentRoutes(config: SynthOSConfig, app: Application, customiz
188
188
  if (typeof body.name === 'string') agents[idx].name = body.name;
189
189
  if (typeof body.description === 'string') agents[idx].description = body.description;
190
190
 
191
- await saveSettings(config.pagesFolder, { agents });
191
+ await saveSettings(config, { agents });
192
192
 
193
193
  // Connect or disconnect based on enabled state
194
194
  if (agents[idx].provider === 'openclaw') {
@@ -210,7 +210,7 @@ export function useAgentRoutes(config: SynthOSConfig, app: Application, customiz
210
210
  app.delete('/api/agents/:id', async (req, res) => {
211
211
  try {
212
212
  const { id } = req.params;
213
- const settings = await loadSettings(config.pagesFolder);
213
+ const settings = await loadSettings(config);
214
214
  const agent = (settings.agents ?? []).find(a => a.id === id);
215
215
 
216
216
  // Disconnect if OpenClaw
@@ -219,7 +219,7 @@ export function useAgentRoutes(config: SynthOSConfig, app: Application, customiz
219
219
  }
220
220
 
221
221
  const agents = (settings.agents ?? []).filter(a => a.id !== id);
222
- await saveSettings(config.pagesFolder, { agents });
222
+ await saveSettings(config, { agents });
223
223
  res.json({ deleted: true });
224
224
  } catch (err: unknown) {
225
225
  console.error(err);
@@ -231,7 +231,7 @@ export function useAgentRoutes(config: SynthOSConfig, app: Application, customiz
231
231
  app.post('/api/agents/:id/connect', async (req, res) => {
232
232
  try {
233
233
  const { id } = req.params;
234
- const settings = await loadSettings(config.pagesFolder);
234
+ const settings = await loadSettings(config);
235
235
  const agent = (settings.agents ?? []).find(a => a.id === id);
236
236
  if (!agent) {
237
237
  res.status(404).json({ error: `Agent "${id}" not found` });
@@ -277,7 +277,7 @@ export function useAgentRoutes(config: SynthOSConfig, app: Application, customiz
277
277
  return;
278
278
  }
279
279
 
280
- const settings = await loadSettings(config.pagesFolder);
280
+ const settings = await loadSettings(config);
281
281
  const agent = (settings.agents ?? []).find(a => a.id === id);
282
282
  if (!agent) {
283
283
  res.status(404).json({ error: `Agent "${id}" not found` });
@@ -307,7 +307,7 @@ export function useAgentRoutes(config: SynthOSConfig, app: Application, customiz
307
307
  res: import('express').Response,
308
308
  fn: (agent: AgentConfig, provider: AgentProvider) => Promise<void>,
309
309
  ): Promise<void> {
310
- const settings = await loadSettings(config.pagesFolder);
310
+ const settings = await loadSettings(config);
311
311
  const agent = (settings.agents ?? []).find(a => a.id === id);
312
312
  if (!agent) {
313
313
  res.status(404).json({ error: `Agent "${id}" not found` });
@@ -386,7 +386,7 @@ export function useAgentRoutes(config: SynthOSConfig, app: Application, customiz
386
386
  return;
387
387
  }
388
388
 
389
- const settings = await loadSettings(config.pagesFolder);
389
+ const settings = await loadSettings(config);
390
390
  const agent = (settings.agents ?? []).find(a => a.id === id);
391
391
  if (!agent) {
392
392
  res.status(404).json({ error: `Agent "${id}" not found` });
@@ -1,8 +1,7 @@
1
1
  import path from "path";
2
- import fs from "fs/promises";
3
2
  import AdmZip from "adm-zip";
4
- import { listPages, loadPageMetadata, PageMetadata, savePageMetadata, deletePage, copyPage, loadPageState, savePageState, clearVersions, PAGE_VERSION } from "../pages";
5
- import { checkIfExists, copyFile, copyFolderRecursive, deleteFile, ensureFolderExists, findFileInFolders, listFolders, loadFile } from "../files";
3
+ import { listPages, loadPageMetadata, PageMetadata, savePageMetadata, deletePage, copyPage, savePageState, clearVersions, PAGE_VERSION } from "../pages";
4
+ import { checkIfExists, findFileInFolders, listFiles, listFolders, loadFile } from "../files";
6
5
  import {getModelEntry, loadSettings, saveSettings, ServicesConfig } from "../settings";
7
6
  import { Application } from 'express';
8
7
  import express from 'express';
@@ -50,9 +49,11 @@ const SERVICE_REGISTRY: ServiceDefinition[] = [
50
49
  ];
51
50
 
52
51
  export function useApiRoutes(config: SynthOSConfig, app: Application, customizer?: Customizer): void {
52
+ const sp = config.storageProvider;
53
+
53
54
  // List pages
54
55
  app.get('/api/pages', async (req, res) => {
55
- const pages = await listPages(config.pagesFolder, config.requiredPagesFolders);
56
+ const pages = await listPages(config, config.requiredPagesFolders);
56
57
  res.json(pages);
57
58
  });
58
59
 
@@ -98,13 +99,13 @@ export function useApiRoutes(config: SynthOSConfig, app: Application, customizer
98
99
  // Auto-append _1, _2, etc. on name conflicts
99
100
  let finalName = pageName;
100
101
  let suffix = 0;
101
- while (await checkIfExists(path.join(config.pagesFolder, 'pages', finalName))) {
102
+ while (await sp.checkIfExists(path.join(config.pagesFolder, 'pages', finalName))) {
102
103
  suffix++;
103
104
  finalName = `${pageName}_${suffix}`;
104
105
  }
105
106
 
106
107
  const targetDir = path.join(config.pagesFolder, 'pages', finalName);
107
- await ensureFolderExists(targetDir);
108
+ await sp.ensureFolderExists(targetDir);
108
109
 
109
110
  // Extract entries with path traversal protection
110
111
  for (const entry of entries) {
@@ -120,13 +121,13 @@ export function useApiRoutes(config: SynthOSConfig, app: Application, customizer
120
121
  continue;
121
122
  }
122
123
 
123
- await ensureFolderExists(path.dirname(resolvedPath));
124
- await fs.writeFile(resolvedPath, entry.getData());
124
+ await sp.ensureFolderExists(path.dirname(resolvedPath));
125
+ await sp.saveBuffer(resolvedPath, entry.getData());
125
126
  }
126
127
 
127
128
  // Update metadata: set createdDate and lastModified to now
128
129
  const now = new Date().toISOString();
129
- const existingMeta = await loadPageMetadata(config.pagesFolder, finalName);
130
+ const existingMeta = await loadPageMetadata(config, finalName);
130
131
  const metadata: PageMetadata = {
131
132
  title: existingMeta?.title ?? '',
132
133
  categories: existingMeta?.categories ?? [],
@@ -137,7 +138,7 @@ export function useApiRoutes(config: SynthOSConfig, app: Application, customizer
137
138
  pageVersion: existingMeta?.pageVersion ?? PAGE_VERSION,
138
139
  mode: existingMeta?.mode ?? 'unlocked',
139
140
  };
140
- await savePageMetadata(config.pagesFolder, finalName, metadata);
141
+ await savePageMetadata(config, finalName, metadata);
141
142
 
142
143
  res.status(201).json({ name: finalName, title: metadata.title });
143
144
  } catch (err: unknown) {
@@ -150,7 +151,7 @@ export function useApiRoutes(config: SynthOSConfig, app: Application, customizer
150
151
  app.get('/api/pages/:name', async (req, res) => {
151
152
  try {
152
153
  const { name } = req.params;
153
- const metadata = await loadPageMetadata(config.pagesFolder, name, config.requiredPagesFolders);
154
+ const metadata = await loadPageMetadata(config, name, config.requiredPagesFolders);
154
155
  if (metadata) {
155
156
  res.json(metadata);
156
157
  } else {
@@ -201,7 +202,7 @@ export function useApiRoutes(config: SynthOSConfig, app: Application, customizer
201
202
  }
202
203
 
203
204
  // Load existing metadata (or defaults)
204
- const existing = await loadPageMetadata(config.pagesFolder, name, config.requiredPagesFolders);
205
+ const existing = await loadPageMetadata(config, name, config.requiredPagesFolders);
205
206
  const metadata: PageMetadata = {
206
207
  title: existing?.title ?? '',
207
208
  categories: existing?.categories ?? [],
@@ -226,19 +227,23 @@ export function useApiRoutes(config: SynthOSConfig, app: Application, customizer
226
227
  // Promote required page to user folder if being unlocked/designed
227
228
  if (metadata.mode !== 'locked') {
228
229
  const userPagePath = path.join(config.pagesFolder, 'pages', name, 'page.html');
229
- if (!(await checkIfExists(userPagePath))) {
230
+ if (!(await sp.checkIfExists(userPagePath))) {
231
+ // Read from required pages (package content, always local fs)
230
232
  let html: string | undefined;
231
233
  for (const folder of config.requiredPagesFolders) {
232
- html = await loadPageState(folder, name);
233
- if (html) break;
234
+ const candidate = path.join(folder, name, 'page.html');
235
+ if (await checkIfExists(candidate)) {
236
+ html = await loadFile(candidate);
237
+ break;
238
+ }
234
239
  }
235
240
  if (html) {
236
- await savePageState(config.pagesFolder, name, html);
241
+ await savePageState(config, name, html);
237
242
  }
238
243
  }
239
244
  }
240
245
 
241
- await savePageMetadata(config.pagesFolder, name, metadata);
246
+ await savePageMetadata(config, name, metadata);
242
247
  res.json(metadata);
243
248
  } catch (err: unknown) {
244
249
  console.error(err);
@@ -257,7 +262,7 @@ export function useApiRoutes(config: SynthOSConfig, app: Application, customizer
257
262
  }
258
263
 
259
264
  // Load existing metadata (user override → fallback .json → defaults)
260
- let metadata = await loadPageMetadata(config.pagesFolder, name, config.requiredPagesFolders);
265
+ let metadata = await loadPageMetadata(config, name, config.requiredPagesFolders);
261
266
  if (!metadata) {
262
267
  metadata = {
263
268
  title: '',
@@ -272,7 +277,7 @@ export function useApiRoutes(config: SynthOSConfig, app: Application, customizer
272
277
  }
273
278
 
274
279
  metadata.pinned = pinned;
275
- await savePageMetadata(config.pagesFolder, name, metadata);
280
+ await savePageMetadata(config, name, metadata);
276
281
  res.json(metadata);
277
282
  } catch (err: unknown) {
278
283
  console.error(err);
@@ -294,13 +299,13 @@ export function useApiRoutes(config: SynthOSConfig, app: Application, customizer
294
299
  // Check if page exists (folder-based or legacy flat file)
295
300
  const folderPath = path.join(config.pagesFolder, 'pages', name, 'page.html');
296
301
  const flatPath = path.join(config.pagesFolder, `${name}.html`);
297
- const exists = await checkIfExists(folderPath) || await checkIfExists(flatPath);
302
+ const exists = await sp.checkIfExists(folderPath) || await sp.checkIfExists(flatPath);
298
303
  if (!exists) {
299
304
  res.status(404).json({ error: `Page "${name}" not found` });
300
305
  return;
301
306
  }
302
307
 
303
- await deletePage(config.pagesFolder, name);
308
+ await deletePage(config, name);
304
309
  res.json({ deleted: true });
305
310
  } catch (err: unknown) {
306
311
  console.error(err);
@@ -316,7 +321,7 @@ export function useApiRoutes(config: SynthOSConfig, app: Application, customizer
316
321
  // Resolve page folder: user pages first, then required pages
317
322
  let pageFolder: string | undefined;
318
323
  const userFolder = path.join(config.pagesFolder, 'pages', name);
319
- if (await checkIfExists(path.join(userFolder, 'page.html'))) {
324
+ if (await sp.checkIfExists(path.join(userFolder, 'page.html'))) {
320
325
  pageFolder = userFolder;
321
326
  } else {
322
327
  for (const folder of config.requiredPagesFolders) {
@@ -335,14 +340,23 @@ export function useApiRoutes(config: SynthOSConfig, app: Application, customizer
335
340
 
336
341
  // List subdirectories, filtering out non-table entries
337
342
  const EXCLUDED = new Set(['files']);
338
- const subdirs = await listFolders(pageFolder);
343
+ // Use provider for user folder, local fs for required pages
344
+ const isUserFolder = pageFolder === userFolder;
345
+ const subdirs = isUserFolder
346
+ ? await sp.listFolders(pageFolder)
347
+ : await listFolders(pageFolder);
339
348
  const tables = subdirs.filter(d => !EXCLUDED.has(d));
340
349
 
341
350
  // Check if files/ exists and has entries
342
351
  const filesDir = path.join(pageFolder, 'files');
343
352
  let hasFiles = false;
344
- if (await checkIfExists(filesDir)) {
345
- const entries = await fs.readdir(filesDir);
353
+ const filesDirExists = isUserFolder
354
+ ? await sp.checkIfExists(filesDir)
355
+ : await checkIfExists(filesDir);
356
+ if (filesDirExists) {
357
+ const entries = isUserFolder
358
+ ? await sp.listFiles(filesDir)
359
+ : await listFiles(filesDir);
346
360
  hasFiles = entries.length > 0;
347
361
  }
348
362
 
@@ -384,8 +398,8 @@ export function useApiRoutes(config: SynthOSConfig, app: Application, customizer
384
398
  const candidate = path.join(folder, sourceName, 'page.html');
385
399
  if (await checkIfExists(candidate)) { sourceRequiredPath = candidate; break; }
386
400
  }
387
- const sourceExists = await checkIfExists(sourceFolderPath)
388
- || await checkIfExists(sourceFlatPath)
401
+ const sourceExists = await sp.checkIfExists(sourceFolderPath)
402
+ || await sp.checkIfExists(sourceFlatPath)
389
403
  || !!sourceRequiredPath;
390
404
  if (!sourceExists) {
391
405
  res.status(404).json({ error: `Source page "${sourceName}" not found` });
@@ -395,13 +409,13 @@ export function useApiRoutes(config: SynthOSConfig, app: Application, customizer
395
409
  // Check target doesn't already exist
396
410
  const targetFolderPath = path.join(config.pagesFolder, 'pages', targetName, 'page.html');
397
411
  const targetFlatPath = path.join(config.pagesFolder, `${targetName}.html`);
398
- if (await checkIfExists(targetFolderPath) || await checkIfExists(targetFlatPath)) {
412
+ if (await sp.checkIfExists(targetFolderPath) || await sp.checkIfExists(targetFlatPath)) {
399
413
  res.status(409).json({ error: `Page "${targetName}" already exists` });
400
414
  return;
401
415
  }
402
416
 
403
417
  await copyPage(
404
- config.pagesFolder,
418
+ config,
405
419
  sourceName,
406
420
  targetName,
407
421
  typeof title === 'string' ? title : '',
@@ -414,7 +428,7 @@ export function useApiRoutes(config: SynthOSConfig, app: Application, customizer
414
428
  );
415
429
 
416
430
  // Return the new page metadata
417
- const metadata = await loadPageMetadata(config.pagesFolder, targetName);
431
+ const metadata = await loadPageMetadata(config, targetName);
418
432
  res.status(201).json({ name: targetName, ...metadata });
419
433
  } catch (err: unknown) {
420
434
  console.error(err);
@@ -424,7 +438,7 @@ export function useApiRoutes(config: SynthOSConfig, app: Application, customizer
424
438
 
425
439
  // Define a route to return settings
426
440
  app.get('/api/settings', async (req, res) => {
427
- const settings = await loadSettings(config.pagesFolder);
441
+ const settings = await loadSettings(config);
428
442
  const providers = PROVIDERS.map(p => ({ name: p.name, builderModels: p.builderModels, chatModels: p.chatModels }));
429
443
  res.json({...settings, providers});
430
444
  });
@@ -445,7 +459,7 @@ export function useApiRoutes(config: SynthOSConfig, app: Application, customizer
445
459
  }
446
460
 
447
461
  // Save settings
448
- await saveSettings(config.pagesFolder, settings);
462
+ await saveSettings(config, settings);
449
463
  res.redirect('/builder');
450
464
  } catch (err: unknown) {
451
465
  console.error(err);
@@ -455,7 +469,7 @@ export function useApiRoutes(config: SynthOSConfig, app: Application, customizer
455
469
 
456
470
  // Define a route to generate an image
457
471
  app.post('/api/generate/image', async (req, res) => {
458
- await requiresSettings(res, config.pagesFolder, async (settings) => {
472
+ await requiresSettings(res, config, async (settings) => {
459
473
  const { prompt, shape, style } = req.body;
460
474
  const builder = getModelEntry(settings, 'builder');
461
475
  const { configuration, imageQuality, provider } = builder;
@@ -472,9 +486,9 @@ export function useApiRoutes(config: SynthOSConfig, app: Application, customizer
472
486
 
473
487
  // Define a route to generate a completion using chain-of-thought
474
488
  app.post('/api/generate/completion', async (req, res) => {
475
- await requiresSettings(res, config.pagesFolder, async (settings) => {
489
+ await requiresSettings(res, config, async (settings) => {
476
490
  const { prompt, temperature } = req.body;
477
- const completePrompt = await createCompletePrompt(config.pagesFolder, 'chat', req.body.model);
491
+ const completePrompt = await createCompletePrompt(config, 'chat', req.body.model);
478
492
  const response = await chainOfThought({ question: prompt, temperature, completePrompt });
479
493
  if (response.completed) {
480
494
  res.json(response.value ?? {});
@@ -488,9 +502,9 @@ export function useApiRoutes(config: SynthOSConfig, app: Application, customizer
488
502
  // Brainstorm endpoint
489
503
  if (!customizer || customizer.isEnabled('brainstorm'))
490
504
  app.post('/api/brainstorm', async (req, res) => {
491
- await requiresSettings(res, config.pagesFolder, async (settings) => {
505
+ await requiresSettings(res, config, async (settings) => {
492
506
  const { context, messages } = req.body;
493
- const completePrompt = await createCompletePrompt(config.pagesFolder, 'chat');
507
+ const completePrompt = await createCompletePrompt(config, 'chat');
494
508
 
495
509
  const productName = customizer?.productName ?? 'SynthOS';
496
510
  const system: { role: 'system'; content: string } = {
@@ -557,11 +571,10 @@ Return ONLY the JSON object.`};
557
571
  // Define a route for running configured scripts
558
572
  if (!customizer || customizer.isEnabled('scripts'))
559
573
  app.post('/api/scripts/:id', async (req, res) => {
560
- await requiresSettings(res, config.pagesFolder, async (settings) => {
574
+ await requiresSettings(res, config, async (settings) => {
561
575
  const { id } = req.params;
562
- const pagesFolder = config.pagesFolder;
563
576
  const scriptId = id;
564
- const response = await executeScript({ pagesFolder, scriptId, variables: req.body });
577
+ const response = await executeScript({ pagesFolder: config.pagesFolder, scriptId, variables: req.body });
565
578
  if (response.completed) {
566
579
  // Return the response as text
567
580
  const value = (response.value?.output ?? (response.value?.errors ?? []).join('\n')).trim();
@@ -577,7 +590,7 @@ Return ONLY the JSON object.`};
577
590
  // Return theme info as a self-executing JS script
578
591
  app.get('/api/theme-info.js', async (req, res) => {
579
592
  try {
580
- const settings = await loadSettings(config.pagesFolder);
593
+ const settings = await loadSettings(config);
581
594
  const themeName = settings.theme ?? 'nebula-dusk';
582
595
  const info = await loadThemeInfo(themeName, config);
583
596
  if (!info) {
@@ -607,7 +620,7 @@ Return ONLY the JSON object.`};
607
620
  res.status(400).send('// Missing page query parameter');
608
621
  return;
609
622
  }
610
- const metadata = await loadPageMetadata(config.pagesFolder, page, config.requiredPagesFolders);
623
+ const metadata = await loadPageMetadata(config, page, config.requiredPagesFolders);
611
624
  const mode = metadata?.mode ?? 'unlocked';
612
625
  const title = metadata?.title ?? '';
613
626
  const categories = metadata?.categories ?? [];
@@ -634,7 +647,7 @@ Return ONLY the JSON object.`};
634
647
  // Return the current theme as CSS
635
648
  app.get('/api/theme.css', async (req, res) => {
636
649
  try {
637
- const settings = await loadSettings(config.pagesFolder);
650
+ const settings = await loadSettings(config);
638
651
  const themeName = settings.theme ?? 'nebula-dusk';
639
652
  const css = await loadTheme(themeName, config);
640
653
  if (!css) {
@@ -718,7 +731,7 @@ Return ONLY the JSON object.`};
718
731
  // Return user's configured services (API keys masked)
719
732
  app.get('/api/services', async (_req, res) => {
720
733
  try {
721
- const settings = await loadSettings(config.pagesFolder);
734
+ const settings = await loadSettings(config);
722
735
  const services = settings.services ?? {};
723
736
  const masked: Record<string, { enabled: boolean; hasKey: boolean }> = {};
724
737
  for (const [id, cfg] of Object.entries(services)) {
@@ -738,7 +751,7 @@ Return ONLY the JSON object.`};
738
751
  app.post('/api/services', async (req, res) => {
739
752
  try {
740
753
  const incoming = req.body as ServicesConfig;
741
- const settings = await loadSettings(config.pagesFolder);
754
+ const settings = await loadSettings(config);
742
755
  const existing = settings.services ?? {};
743
756
 
744
757
  // Build merged config — empty apiKey means "keep existing"
@@ -759,7 +772,7 @@ Return ONLY the JSON object.`};
759
772
  }
760
773
  }
761
774
 
762
- await saveSettings(config.pagesFolder, { services: merged });
775
+ await saveSettings(config, { services: merged });
763
776
  res.json({ saved: true });
764
777
  } catch (err: unknown) {
765
778
  console.error(err);
@@ -780,7 +793,7 @@ Return ONLY the JSON object.`};
780
793
  return;
781
794
  }
782
795
 
783
- const settings = await loadSettings(config.pagesFolder);
796
+ const settings = await loadSettings(config);
784
797
  const braveConfig = settings.connectors?.['brave-search'] ?? settings.services?.['brave-search'];
785
798
  if (!braveConfig || !braveConfig.enabled || !braveConfig.apiKey) {
786
799
  res.status(400).json({ error: 'Brave Search is not configured or not enabled. Add your API key in Settings > Services.' });
@@ -826,7 +839,7 @@ Return ONLY the JSON object.`};
826
839
  const { name } = req.params;
827
840
 
828
841
  // Load current metadata
829
- const metadata = await loadPageMetadata(config.pagesFolder, name, config.requiredPagesFolders);
842
+ const metadata = await loadPageMetadata(config, name, config.requiredPagesFolders);
830
843
  if (!metadata) {
831
844
  res.status(404).json({ error: `Page "${name}" not found` });
832
845
  return;
@@ -846,35 +859,37 @@ Return ONLY the JSON object.`};
846
859
  }
847
860
 
848
861
  // Run LLM-based migration
849
- const completePrompt = await createCompletePrompt(config.pagesFolder, 'builder');
862
+ const completePrompt = await createCompletePrompt(config, 'builder');
850
863
  const migratedHtml = await migratePage(html, currentVersion, PAGE_VERSION, completePrompt);
851
864
 
852
865
  // Save upgraded HTML to v2 folder structure
853
- await savePageState(config.pagesFolder, name, migratedHtml);
866
+ await savePageState(config, name, migratedHtml);
854
867
 
855
868
  // Backup original page to .migrated/ before overwriting
856
869
  const migratedFolder = path.join(config.pagesFolder, '.migrated');
857
870
 
858
871
  // Handle legacy flat file (<localFolder>/pagename.html)
859
872
  const flatPath = path.join(config.pagesFolder, `${name}.html`);
860
- if (await checkIfExists(flatPath)) {
861
- await copyFile(flatPath, migratedFolder);
862
- await deleteFile(flatPath);
873
+ if (await sp.checkIfExists(flatPath)) {
874
+ await sp.ensureFolderExists(migratedFolder);
875
+ const flatData = await sp.loadBuffer(flatPath);
876
+ await sp.saveBuffer(path.join(migratedFolder, `${name}.html`), flatData);
877
+ await sp.deleteFile(flatPath);
863
878
  }
864
879
 
865
880
  // Handle folder-based page (<localFolder>/pages/name/)
866
881
  const folderPath = path.join(config.pagesFolder, 'pages', name);
867
- if (await checkIfExists(folderPath)) {
868
- await copyFolderRecursive(folderPath, path.join(migratedFolder, name));
882
+ if (await sp.checkIfExists(folderPath)) {
883
+ await sp.copyFolderRecursive(folderPath, path.join(migratedFolder, name));
869
884
  }
870
885
 
871
886
  // Clear stale version files (undo snapshots from the old page version)
872
- await clearVersions(config.pagesFolder, name);
887
+ await clearVersions(config, name);
873
888
 
874
889
  // Update metadata
875
890
  metadata.pageVersion = PAGE_VERSION;
876
891
  metadata.lastModified = new Date().toISOString();
877
- await savePageMetadata(config.pagesFolder, name, metadata);
892
+ await savePageMetadata(config, name, metadata);
878
893
 
879
894
  res.json({ upgraded: true, fromVersion: currentVersion, toVersion: PAGE_VERSION });
880
895
  } catch (err: unknown) {
@@ -892,14 +907,14 @@ Return ONLY the JSON object.`};
892
907
  const userPageDir = path.join(config.pagesFolder, 'pages', name);
893
908
  let requiredPageDir: string | undefined;
894
909
  for (const folder of config.requiredPagesFolders) {
895
- if (await checkIfExists(path.join(folder, name, 'page.html'))) {
910
+ if (await checkIfExists(path.join(folder, name, 'page.html'))) { // package content, local fs
896
911
  requiredPageDir = path.join(folder, name);
897
912
  break;
898
913
  }
899
914
  }
900
915
  let sourceDir: string | null = null;
901
916
 
902
- if (await checkIfExists(path.join(userPageDir, 'page.html'))) {
917
+ if (await sp.checkIfExists(path.join(userPageDir, 'page.html'))) {
903
918
  sourceDir = userPageDir;
904
919
  } else if (requiredPageDir) {
905
920
  // For required pages, create a temp-like zip with just the HTML
@@ -940,7 +955,7 @@ Return ONLY the JSON object.`};
940
955
 
941
956
  // Ask a question about a page (with full page HTML context)
942
957
  app.post('/api/pages/:name/ask', async (req, res) => {
943
- await requiresSettings(res, config.pagesFolder, async (settings) => {
958
+ await requiresSettings(res, config, async (settings) => {
944
959
  const { name } = req.params;
945
960
  const { question } = req.body;
946
961
  if (typeof question !== 'string' || !question.trim()) {
@@ -956,7 +971,7 @@ Return ONLY the JSON object.`};
956
971
  }
957
972
 
958
973
  // Create completion (uses 'chat' model, not 'builder')
959
- const complete = await createCompletePrompt(config.pagesFolder, 'chat', req.body.model);
974
+ const complete = await createCompletePrompt(config, 'chat', req.body.model);
960
975
 
961
976
  const system = {
962
977
  role: 'system' as const,
@@ -15,7 +15,7 @@ export function useConnectorRoutes(config: SynthOSConfig, app: Application): voi
15
15
  // Also handles POST /api/connectors — Proxy call (see below)
16
16
  app.get('/api/connectors', async (req, res) => {
17
17
  try {
18
- const settings = await loadSettings(config.pagesFolder);
18
+ const settings = await loadSettings(config);
19
19
  const connectors = settings.connectors ?? {};
20
20
 
21
21
  const categoryFilter = req.query.category as string | undefined;
@@ -60,7 +60,7 @@ export function useConnectorRoutes(config: SynthOSConfig, app: Application): voi
60
60
  return;
61
61
  }
62
62
 
63
- const settings = await loadSettings(config.pagesFolder);
63
+ const settings = await loadSettings(config);
64
64
  const cfg = (settings.connectors ?? {})[id];
65
65
  const isOAuth = def.authStrategy === 'oauth2';
66
66
  const oauthCfg = cfg as ConnectorOAuthConfig | undefined;
@@ -94,7 +94,7 @@ export function useConnectorRoutes(config: SynthOSConfig, app: Application): voi
94
94
  return;
95
95
  }
96
96
 
97
- const settings = await loadSettings(config.pagesFolder);
97
+ const settings = await loadSettings(config);
98
98
  const existing = settings.connectors ?? {};
99
99
 
100
100
  if (def.authStrategy === 'oauth2') {
@@ -121,7 +121,7 @@ export function useConnectorRoutes(config: SynthOSConfig, app: Application): voi
121
121
  }
122
122
 
123
123
  const updated = { ...existing, [id]: entry };
124
- await saveSettings(config.pagesFolder, { connectors: updated });
124
+ await saveSettings(config, { connectors: updated });
125
125
  } else {
126
126
  const { apiKey, enabled } = req.body;
127
127
  const resolvedKey = (typeof apiKey === 'string' && apiKey.length > 0)
@@ -132,7 +132,7 @@ export function useConnectorRoutes(config: SynthOSConfig, app: Application): voi
132
132
  ...existing,
133
133
  [id]: { apiKey: resolvedKey, enabled: !!enabled }
134
134
  };
135
- await saveSettings(config.pagesFolder, { connectors: updated });
135
+ await saveSettings(config, { connectors: updated });
136
136
  }
137
137
 
138
138
  res.json({ saved: true });
@@ -146,13 +146,13 @@ export function useConnectorRoutes(config: SynthOSConfig, app: Application): voi
146
146
  app.delete('/api/connectors/:id', async (req, res) => {
147
147
  try {
148
148
  const { id } = req.params;
149
- const settings = await loadSettings(config.pagesFolder);
149
+ const settings = await loadSettings(config);
150
150
  const existing = settings.connectors ?? {};
151
151
 
152
152
  const updated = { ...existing };
153
153
  delete updated[id];
154
154
 
155
- await saveSettings(config.pagesFolder, { connectors: updated });
155
+ await saveSettings(config, { connectors: updated });
156
156
  res.json({ deleted: true });
157
157
  } catch (err: unknown) {
158
158
  console.error(err);
@@ -170,7 +170,7 @@ export function useConnectorRoutes(config: SynthOSConfig, app: Application): voi
170
170
  return;
171
171
  }
172
172
 
173
- const settings = await loadSettings(config.pagesFolder);
173
+ const settings = await loadSettings(config);
174
174
  const cfg = (settings.connectors ?? {})[id] as ConnectorOAuthConfig | undefined;
175
175
  if (!cfg?.clientId || !cfg?.clientSecret) {
176
176
  res.status(400).json({ error: 'Client ID and Client Secret must be saved before authorizing' });
@@ -220,7 +220,7 @@ export function useConnectorRoutes(config: SynthOSConfig, app: Application): voi
220
220
  return;
221
221
  }
222
222
 
223
- const settings = await loadSettings(config.pagesFolder);
223
+ const settings = await loadSettings(config);
224
224
  const cfg = (settings.connectors ?? {})[connectorId] as ConnectorOAuthConfig | undefined;
225
225
  if (!cfg?.clientId || !cfg?.clientSecret) {
226
226
  res.status(400).json({ error: 'Client credentials not found' });
@@ -307,7 +307,7 @@ export function useConnectorRoutes(config: SynthOSConfig, app: Application): voi
307
307
  enabled: true
308
308
  }
309
309
  };
310
- await saveSettings(config.pagesFolder, { connectors: updated });
310
+ await saveSettings(config, { connectors: updated });
311
311
 
312
312
  res.redirect(`/settings?tab=connectors&connected=${encodeURIComponent(connectorId)}`);
313
313
  } catch (err: unknown) {
@@ -332,7 +332,7 @@ export function useConnectorRoutes(config: SynthOSConfig, app: Application): voi
332
332
  return;
333
333
  }
334
334
 
335
- const settings = await loadSettings(config.pagesFolder);
335
+ const settings = await loadSettings(config);
336
336
  const cfg = (settings.connectors ?? {})[request.connector];
337
337
 
338
338
  if (def.authStrategy === 'oauth2') {