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,6 +1,5 @@
1
1
  import { Application, Response } from 'express';
2
2
  import { SynthOSConfig } from "../init";
3
- import { checkIfExists, deleteFile, ensureFolderExists, listFiles, loadFile, saveFile } from "../files";
4
3
  import path from "path";
5
4
  import { v4 } from "uuid";
6
5
  import { clearCachedScripts } from '../scripts';
@@ -17,19 +16,20 @@ export function useDataRoutes(config: SynthOSConfig, app: Application): void {
17
16
  // ---------------------------------------------------------------------------
18
17
 
19
18
  async function handleList(config: SynthOSConfig, page: string, table: string, query: Record<string, any>, res: Response): Promise<void> {
19
+ const sp = config.storageProvider;
20
20
  const folder = tableFolder(config, page, table);
21
- if (!(await checkIfExists(folder))) {
21
+ if (!(await sp.checkIfExists(folder))) {
22
22
  res.status(404).json({ error: 'table_not_found', page, table });
23
23
  return;
24
24
  }
25
25
 
26
- const ids = (await listFiles(folder)).filter(f => f.endsWith('.json')).map(f => f.replace('.json', ''));
26
+ const ids = (await sp.listFiles(folder)).filter(f => f.endsWith('.json')).map(f => f.replace('.json', ''));
27
27
 
28
28
  const rows: Record<string, any>[] = [];
29
29
  for (const id of ids) {
30
30
  const file = recordFile(folder, id);
31
31
  try {
32
- const row = JSON.parse(await loadFile(file));
32
+ const row = JSON.parse(await sp.loadFile(file));
33
33
  row.id = id;
34
34
  rows.push(row);
35
35
  } catch (err: unknown) {
@@ -49,15 +49,16 @@ async function handleList(config: SynthOSConfig, page: string, table: string, qu
49
49
  }
50
50
 
51
51
  async function handleGet(config: SynthOSConfig, page: string, table: string, id: string, res: Response): Promise<void> {
52
+ const sp = config.storageProvider;
52
53
  const folder = tableFolder(config, page, table);
53
- if (!(await checkIfExists(folder))) {
54
+ if (!(await sp.checkIfExists(folder))) {
54
55
  res.status(404).json({ error: 'table_not_found', page, table });
55
56
  return;
56
57
  }
57
58
 
58
59
  const file = recordFile(folder, id);
59
60
  try {
60
- const row = JSON.parse(await loadFile(file));
61
+ const row = JSON.parse(await sp.loadFile(file));
61
62
  row.id = id;
62
63
  res.json(row);
63
64
  } catch (err: unknown) {
@@ -66,13 +67,14 @@ async function handleGet(config: SynthOSConfig, page: string, table: string, id:
66
67
  }
67
68
 
68
69
  async function handleUpsert(config: SynthOSConfig, page: string, table: string, body: any, res: Response): Promise<void> {
70
+ const sp = config.storageProvider;
69
71
  const id = body.id ?? v4();
70
72
  const folder = tableFolder(config, page, table);
71
73
  const file = recordFile(folder, id);
72
74
  try {
73
75
  const row = { ...body, id };
74
- await ensureFolderExists(folder);
75
- await saveFile(file, JSON.stringify(row, null, 4));
76
+ await sp.ensureFolderExists(folder);
77
+ await sp.saveFile(file, JSON.stringify(row, null, 4));
76
78
  if (table === 'scripts') {
77
79
  clearCachedScripts();
78
80
  }
@@ -84,11 +86,12 @@ async function handleUpsert(config: SynthOSConfig, page: string, table: string,
84
86
  }
85
87
 
86
88
  async function handleDelete(config: SynthOSConfig, page: string, table: string, id: string, res: Response): Promise<void> {
89
+ const sp = config.storageProvider;
87
90
  const folder = tableFolder(config, page, table);
88
91
  const file = recordFile(folder, id);
89
92
  try {
90
- if (await checkIfExists(file)) {
91
- await deleteFile(file);
93
+ if (await sp.checkIfExists(file)) {
94
+ await sp.deleteFile(file);
92
95
  if (table === 'scripts') {
93
96
  clearCachedScripts();
94
97
  }
@@ -1,25 +1,25 @@
1
1
  import { Application } from 'express';
2
2
  import express from 'express';
3
- import fs from 'fs/promises';
4
3
  import path from 'path';
5
4
  import { SynthOSConfig } from '../init';
6
- import { checkIfExists, ensureFolderExists } from '../files';
7
5
 
8
6
  export function useFileRoutes(config: SynthOSConfig, app: Application): void {
7
+ const sp = config.storageProvider;
8
+
9
9
  // List files in a page's files/ folder
10
10
  app.get('/api/files/:page', async (req, res) => {
11
11
  try {
12
12
  const folder = filesFolder(config, req.params.page);
13
- if (!(await checkIfExists(folder))) {
13
+ if (!(await sp.checkIfExists(folder))) {
14
14
  res.json({ files: [] });
15
15
  return;
16
16
  }
17
17
 
18
- const entries = await fs.readdir(folder);
18
+ const entries = await sp.listFiles(folder);
19
19
  const files: { name: string; size: number }[] = [];
20
20
  for (const entry of entries) {
21
- const stat = await fs.stat(path.join(folder, entry));
22
- if (stat.isFile()) {
21
+ const stat = await sp.stat(path.join(folder, entry));
22
+ if (stat.isFile) {
23
23
  files.push({ name: entry, size: stat.size });
24
24
  }
25
25
  }
@@ -39,12 +39,13 @@ export function useFileRoutes(config: SynthOSConfig, app: Application): void {
39
39
  return;
40
40
  }
41
41
 
42
- if (!(await checkIfExists(filePath))) {
42
+ if (!(await sp.checkIfExists(filePath))) {
43
43
  res.status(404).json({ error: 'File not found' });
44
44
  return;
45
45
  }
46
46
 
47
- res.sendFile(filePath);
47
+ res.type(path.extname(req.params.filename));
48
+ sp.createReadStream(filePath).pipe(res);
48
49
  } catch (err: unknown) {
49
50
  console.error(err);
50
51
  res.status(500).json({ error: (err as Error).message });
@@ -67,10 +68,10 @@ export function useFileRoutes(config: SynthOSConfig, app: Application): void {
67
68
  }
68
69
 
69
70
  const folder = filesFolder(config, req.params.page);
70
- await ensureFolderExists(folder);
71
- await fs.writeFile(filePath, req.body as Buffer);
71
+ await sp.ensureFolderExists(folder);
72
+ await sp.saveBuffer(filePath, req.body as Buffer);
72
73
 
73
- const stat = await fs.stat(filePath);
74
+ const stat = await sp.stat(filePath);
74
75
  res.status(201).json({ name: filename, size: stat.size });
75
76
  } catch (err: unknown) {
76
77
  console.error(err);
@@ -87,12 +88,12 @@ export function useFileRoutes(config: SynthOSConfig, app: Application): void {
87
88
  return;
88
89
  }
89
90
 
90
- if (!(await checkIfExists(filePath))) {
91
+ if (!(await sp.checkIfExists(filePath))) {
91
92
  res.status(404).json({ error: 'File not found' });
92
93
  return;
93
94
  }
94
95
 
95
- await fs.unlink(filePath);
96
+ await sp.deleteFile(filePath);
96
97
  res.json({ deleted: true });
97
98
  } catch (err: unknown) {
98
99
  console.error(err);
@@ -13,7 +13,7 @@ import { getConnectorRegistry, ConnectorsConfig, ConnectorOAuthConfig } from "..
13
13
  import { AgentConfig } from "../agents";
14
14
  import { listScripts } from "../scripts";
15
15
  import path from 'path';
16
- import { checkIfExists, copyFolderRecursive, deleteFile as deleteFileFromDisk, findFileInFolders, loadFile } from "../files";
16
+ import { checkIfExists, findFileInFolders, loadFile } from "../files";
17
17
  import * as cheerio from 'cheerio';
18
18
 
19
19
  /**
@@ -455,7 +455,7 @@ export function usePageRoutes(config: SynthOSConfig, app: Application, customize
455
455
  app.get('/:page', async (req, res) => {
456
456
  // Redirect if settings not configured
457
457
  const { page } = req.params;
458
- const isConfigured = await hasConfiguredSettings(config.pagesFolder);
458
+ const isConfigured = await hasConfiguredSettings(config);
459
459
  if (!isConfigured && page !== 'settings') {
460
460
  res.redirect('/settings?firstRun=1');
461
461
  return;
@@ -470,7 +470,7 @@ export function usePageRoutes(config: SynthOSConfig, app: Application, customize
470
470
  }
471
471
 
472
472
  // Load page metadata for version-based script injection
473
- const metadata = await loadPageMetadata(config.pagesFolder, page, config.requiredPagesFolders);
473
+ const metadata = await loadPageMetadata(config, page, config.requiredPagesFolders);
474
474
  const pageVersion = metadata?.pageVersion ?? 0;
475
475
 
476
476
  // Block outdated pages (redirect to tabs list so user sees upgrade UI)
@@ -480,7 +480,7 @@ export function usePageRoutes(config: SynthOSConfig, app: Application, customize
480
480
  }
481
481
 
482
482
  // Load settings to determine theme version for FluentLM base injection
483
- const settings = await loadSettings(config.pagesFolder);
483
+ const settings = await loadSettings(config);
484
484
  const themeName = settings.theme ?? 'nebula-dusk';
485
485
  const themeVersion = await loadThemeVersion(themeName, config);
486
486
 
@@ -493,7 +493,7 @@ export function usePageRoutes(config: SynthOSConfig, app: Application, customize
493
493
 
494
494
  // Inject version meta tag so undo/try-again links appear on page load
495
495
  {
496
- const latestVersion = await getLatestVersion(config.pagesFolder, page);
496
+ const latestVersion = await getLatestVersion(config, page);
497
497
  if (latestVersion > 0) {
498
498
  html = html.replace('</head>', `<meta name="synthos-version" content="${latestVersion}">\n</head>`);
499
499
  }
@@ -504,7 +504,7 @@ export function usePageRoutes(config: SynthOSConfig, app: Application, customize
504
504
  // Replace branding for white-label forks
505
505
  const productName = customizer?.productName ?? 'SynthOS';
506
506
  if (productName !== 'SynthOS') {
507
- html = html.replace(/SynthOS/g, productName);
507
+ html = html.replace(/synthos/gi, productName);
508
508
  }
509
509
 
510
510
  res.send(html);
@@ -515,7 +515,7 @@ export function usePageRoutes(config: SynthOSConfig, app: Application, customize
515
515
  try {
516
516
  // Redirect if settings not configured
517
517
  const { page } = req.params;
518
- const isConfigured = await hasConfiguredSettings(config.pagesFolder);
518
+ const isConfigured = await hasConfiguredSettings(config);
519
519
  if (!isConfigured) {
520
520
  res.status(400).json({ error: 'Settings not configured' });
521
521
  return;
@@ -571,7 +571,7 @@ export function usePageRoutes(config: SynthOSConfig, app: Application, customize
571
571
  }
572
572
 
573
573
  // Inject save-line marker at the end of chat messages (skip for locked pages)
574
- const sourceMetadata = await loadPageMetadata(config.pagesFolder, page, config.requiredPagesFolders);
574
+ const sourceMetadata = await loadPageMetadata(config, page, config.requiredPagesFolders);
575
575
  if (sourceMetadata?.mode !== 'locked') {
576
576
  const $ = cheerio.load(pageState);
577
577
  // Remove any existing save-line first
@@ -584,14 +584,14 @@ export function usePageRoutes(config: SynthOSConfig, app: Application, customize
584
584
  }
585
585
 
586
586
  // Save as new page
587
- await savePageState(config.pagesFolder, saveAs, pageState, title, categories);
587
+ await savePageState(config, saveAs, pageState, title, categories);
588
588
 
589
589
  // Copy files (sound effects, etc.) from source page when saving as a different name
590
590
  if (page !== saveAs) {
591
591
  // Check source page's files/ dir in user pages, then fallback folders
592
592
  let sourceFilesDir: string | undefined;
593
593
  const userFilesDir = path.join(config.pagesFolder, 'pages', page, 'files');
594
- if (await checkIfExists(userFilesDir)) {
594
+ if (await config.storageProvider.checkIfExists(userFilesDir)) {
595
595
  sourceFilesDir = userFilesDir;
596
596
  } else {
597
597
  for (const folder of config.requiredPagesFolders) {
@@ -604,15 +604,15 @@ export function usePageRoutes(config: SynthOSConfig, app: Application, customize
604
604
  }
605
605
  if (sourceFilesDir) {
606
606
  const targetFilesDir = path.join(config.pagesFolder, 'pages', saveAs, 'files');
607
- await copyFolderRecursive(sourceFilesDir, targetFilesDir);
607
+ await config.storageProvider.copyFolderRecursive(sourceFilesDir, targetFilesDir);
608
608
  }
609
609
  }
610
610
 
611
611
  // Clear version files after saving (fresh baseline)
612
- await clearVersions(config.pagesFolder, saveAs);
612
+ await clearVersions(config, saveAs);
613
613
 
614
614
  // Also update metadata with categories (in case page.json already existed)
615
- await savePageMetadata(config.pagesFolder, saveAs, {
615
+ await savePageMetadata(config, saveAs, {
616
616
  title,
617
617
  categories,
618
618
  pinned: false,
@@ -634,25 +634,25 @@ export function usePageRoutes(config: SynthOSConfig, app: Application, customize
634
634
  app.post('/:page/undo', async (req, res) => {
635
635
  try {
636
636
  const { page } = req.params;
637
- const pagesFolder = config.pagesFolder;
638
- const cv = await getLatestVersion(pagesFolder, page);
637
+ const sp = config.storageProvider;
638
+ const cv = await getLatestVersion(config, page);
639
639
  if (cv <= 0) {
640
640
  res.status(400).send('Nothing to undo');
641
641
  return;
642
642
  }
643
643
 
644
644
  // Delete the current version file
645
- const pageFolder = path.join(pagesFolder, 'pages', page);
645
+ const pageFolder = path.join(config.pagesFolder, 'pages', page);
646
646
  const versionFile = path.join(pageFolder, `page.v${cv}.html`);
647
- if (await checkIfExists(versionFile)) {
648
- await deleteFileFromDisk(versionFile);
647
+ if (await sp.checkIfExists(versionFile)) {
648
+ await sp.deleteFile(versionFile);
649
649
  }
650
650
 
651
651
  // Load previous version (v{cv-1}.html, or page.html if rolling back to v0)
652
652
  const prevVersion = cv - 1;
653
653
  let previousHtml: string | undefined;
654
654
  if (prevVersion > 0) {
655
- previousHtml = await loadPageVersion(pagesFolder, page, prevVersion);
655
+ previousHtml = await loadPageVersion(config, page, prevVersion);
656
656
  } else {
657
657
  // v0 = the saved page.html baseline (user folder → required folders fallback)
658
658
  previousHtml = await loadPageWithFallback(page, config, true);
@@ -664,8 +664,8 @@ export function usePageRoutes(config: SynthOSConfig, app: Application, customize
664
664
  }
665
665
 
666
666
  // Inject shell assets (same as GET handler)
667
- const settings = await loadSettings(config.pagesFolder);
668
- const metadata = await loadPageMetadata(pagesFolder, page, config.requiredPagesFolders);
667
+ const settings = await loadSettings(config);
668
+ const metadata = await loadPageMetadata(config, page, config.requiredPagesFolders);
669
669
  const pv = metadata?.pageVersion ?? 0;
670
670
  const themeName = settings.theme ?? 'nebula-dusk';
671
671
  const themeVersion = await loadThemeVersion(themeName, config);
@@ -688,7 +688,7 @@ export function usePageRoutes(config: SynthOSConfig, app: Application, customize
688
688
  // Replace branding for white-label forks
689
689
  const productName = customizer?.productName ?? 'SynthOS';
690
690
  if (productName !== 'SynthOS') {
691
- out = out.replace(/SynthOS/g, productName);
691
+ out = out.replace(/synthos/gi, productName);
692
692
  }
693
693
 
694
694
  res.send(out);
@@ -703,7 +703,7 @@ export function usePageRoutes(config: SynthOSConfig, app: Application, customize
703
703
  try {
704
704
  // Ensure settings configured
705
705
  const { page } = req.params;
706
- const isConfigured = await hasConfiguredSettings(config.pagesFolder);
706
+ const isConfigured = await hasConfiguredSettings(config);
707
707
  if (!isConfigured) {
708
708
  res.status(400).send('Settings not configured');
709
709
  return;
@@ -717,7 +717,7 @@ export function usePageRoutes(config: SynthOSConfig, app: Application, customize
717
717
  }
718
718
 
719
719
  // Reject modifications to locked pages
720
- const lockMetadata = await loadPageMetadata(config.pagesFolder, page, config.requiredPagesFolders);
720
+ const lockMetadata = await loadPageMetadata(config, page, config.requiredPagesFolders);
721
721
  if (lockMetadata?.mode === 'locked') {
722
722
  res.status(403).send('This page is locked and cannot be modified');
723
723
  return;
@@ -743,7 +743,7 @@ export function usePageRoutes(config: SynthOSConfig, app: Application, customize
743
743
  }
744
744
 
745
745
  // Create model instance
746
- const innerCompletePrompt = await createCompletePrompt(config.pagesFolder, 'builder', req.body.model);
746
+ const innerCompletePrompt = await createCompletePrompt(config, 'builder', req.body.model);
747
747
  const debugVerbose = config.debugPageUpdates;
748
748
  let inputChars = 0;
749
749
  let outputChars = 0;
@@ -775,7 +775,7 @@ export function usePageRoutes(config: SynthOSConfig, app: Application, customize
775
775
 
776
776
  // Load settings and build context
777
777
  const pagesFolder = config.pagesFolder;
778
- const settings = await loadSettings(config.pagesFolder);
778
+ const settings = await loadSettings(config);
779
779
  const entry = getModelEntry(settings, 'builder');
780
780
  const { instructions } = entry;
781
781
  const productName = customizer?.productName ?? 'SynthOS';
@@ -837,8 +837,8 @@ export function usePageRoutes(config: SynthOSConfig, app: Application, customize
837
837
  // Detect first edit (v0→v1) for saved pages
838
838
  const isRequiredPage = config.requiredPages.includes(page);
839
839
  const pageFolder = path.join(pagesFolder, 'pages', page);
840
- const pageFileExists = await checkIfExists(path.join(pageFolder, 'page.html'));
841
- let currentVersion = await getLatestVersion(pagesFolder, page);
840
+ const pageFileExists = await config.storageProvider.checkIfExists(path.join(pageFolder, 'page.html'));
841
+ let currentVersion = await getLatestVersion(config, page);
842
842
  const isFirstEdit = !isRequiredPage && pageFileExists && currentVersion === 0;
843
843
 
844
844
  // Try again — roll back to previous version before re-running the transform
@@ -847,13 +847,13 @@ export function usePageRoutes(config: SynthOSConfig, app: Application, customize
847
847
  if (tryAgain && currentVersion > 0) {
848
848
  // Delete current version file
849
849
  const versionFile = path.join(pageFolder, `page.v${currentVersion}.html`);
850
- if (await checkIfExists(versionFile)) {
851
- await deleteFileFromDisk(versionFile);
850
+ if (await config.storageProvider.checkIfExists(versionFile)) {
851
+ await config.storageProvider.deleteFile(versionFile);
852
852
  }
853
853
  // Load previous version
854
854
  const prevVersion = currentVersion - 1;
855
855
  if (prevVersion > 0) {
856
- transformInput = await loadPageVersion(pagesFolder, page, prevVersion) ?? pageState;
856
+ transformInput = await loadPageVersion(config, page, prevVersion) ?? pageState;
857
857
  } else {
858
858
  transformInput = await loadPageWithFallback(page, config, true) ?? pageState;
859
859
  }
@@ -931,13 +931,13 @@ export function usePageRoutes(config: SynthOSConfig, app: Application, customize
931
931
 
932
932
  // Save version snapshot (working state for all pages, undo support for saved pages)
933
933
  const nextVersion = currentVersion + 1;
934
- await savePageVersion(pagesFolder, page, nextVersion, html);
934
+ await savePageVersion(config, page, nextVersion, html);
935
935
 
936
936
  // Update lastModified timestamp in page metadata
937
- const metadata = await loadPageMetadata(pagesFolder, page, config.requiredPagesFolders);
937
+ const metadata = await loadPageMetadata(config, page, config.requiredPagesFolders);
938
938
  if (metadata) {
939
939
  metadata.lastModified = new Date().toISOString();
940
- await savePageMetadata(pagesFolder, page, metadata);
940
+ await savePageMetadata(config, page, metadata);
941
941
  }
942
942
 
943
943
  // Inject required imports and page scripts (same as GET)
@@ -964,7 +964,7 @@ export function usePageRoutes(config: SynthOSConfig, app: Application, customize
964
964
 
965
965
  // Replace branding for white-label forks
966
966
  if (productName !== 'SynthOS') {
967
- out = out.replace(/SynthOS/g, productName);
967
+ out = out.replace(/synthos/gi, productName);
968
968
  }
969
969
 
970
970
  res.send(out);
@@ -985,17 +985,19 @@ export function usePageRoutes(config: SynthOSConfig, app: Application, customize
985
985
  export async function loadPageWithFallback(page: string, config: SynthOSConfig, reset: boolean): Promise<string|undefined> {
986
986
  if (reset) {
987
987
  // Clear working-state versions so we get the fresh template
988
- await clearVersions(config.pagesFolder, page);
988
+ await clearVersions(config, page);
989
989
  }
990
990
 
991
991
  // Try primary pages folder first
992
- const pageState = await loadPageState(config.pagesFolder, page);
992
+ const pageState = await loadPageState(config, page);
993
993
  if (pageState) return pageState;
994
994
 
995
- // Try fallback pages folders
995
+ // Try fallback pages folders (package content, always local fs)
996
996
  for (const folder of config.requiredPagesFolders) {
997
- const state = await loadPageState(folder, page);
998
- if (state) return state;
997
+ const candidate = path.join(folder, page, 'page.html');
998
+ if (await checkIfExists(candidate)) {
999
+ return await loadFile(candidate);
1000
+ }
999
1001
  }
1000
1002
  return undefined;
1001
1003
  }
@@ -1,6 +1,5 @@
1
1
  import { Application, Response } from 'express';
2
2
  import { SynthOSConfig } from "../init";
3
- import { checkIfExists, deleteFile, ensureFolderExists, listFiles, loadFile, saveFile } from "../files";
4
3
  import path from "path";
5
4
  import { v4 } from "uuid";
6
5
 
@@ -16,19 +15,20 @@ export function useSharedDataRoutes(config: SynthOSConfig, app: Application): vo
16
15
  // ---------------------------------------------------------------------------
17
16
 
18
17
  async function handleList(config: SynthOSConfig, table: string, query: Record<string, any>, res: Response): Promise<void> {
18
+ const sp = config.storageProvider;
19
19
  const folder = sharedTableFolder(config, table);
20
- if (!(await checkIfExists(folder))) {
20
+ if (!(await sp.checkIfExists(folder))) {
21
21
  res.status(404).json({ error: 'table_not_found', table });
22
22
  return;
23
23
  }
24
24
 
25
- const ids = (await listFiles(folder)).filter(f => f.endsWith('.json')).map(f => f.replace('.json', ''));
25
+ const ids = (await sp.listFiles(folder)).filter(f => f.endsWith('.json')).map(f => f.replace('.json', ''));
26
26
 
27
27
  const rows: Record<string, any>[] = [];
28
28
  for (const id of ids) {
29
29
  const file = recordFile(folder, id);
30
30
  try {
31
- const row = JSON.parse(await loadFile(file));
31
+ const row = JSON.parse(await sp.loadFile(file));
32
32
  row.id = id;
33
33
  rows.push(row);
34
34
  } catch (err: unknown) {
@@ -48,15 +48,16 @@ async function handleList(config: SynthOSConfig, table: string, query: Record<st
48
48
  }
49
49
 
50
50
  async function handleGet(config: SynthOSConfig, table: string, id: string, res: Response): Promise<void> {
51
+ const sp = config.storageProvider;
51
52
  const folder = sharedTableFolder(config, table);
52
- if (!(await checkIfExists(folder))) {
53
+ if (!(await sp.checkIfExists(folder))) {
53
54
  res.status(404).json({ error: 'table_not_found', table });
54
55
  return;
55
56
  }
56
57
 
57
58
  const file = recordFile(folder, id);
58
59
  try {
59
- const row = JSON.parse(await loadFile(file));
60
+ const row = JSON.parse(await sp.loadFile(file));
60
61
  row.id = id;
61
62
  res.json(row);
62
63
  } catch (err: unknown) {
@@ -65,13 +66,14 @@ async function handleGet(config: SynthOSConfig, table: string, id: string, res:
65
66
  }
66
67
 
67
68
  async function handleUpsert(config: SynthOSConfig, table: string, body: any, res: Response): Promise<void> {
69
+ const sp = config.storageProvider;
68
70
  const id = body.id ?? v4();
69
71
  const folder = sharedTableFolder(config, table);
70
72
  const file = recordFile(folder, id);
71
73
  try {
72
74
  const row = { ...body, id };
73
- await ensureFolderExists(folder);
74
- await saveFile(file, JSON.stringify(row, null, 4));
75
+ await sp.ensureFolderExists(folder);
76
+ await sp.saveFile(file, JSON.stringify(row, null, 4));
75
77
  res.json(row);
76
78
  } catch (err: unknown) {
77
79
  console.error(err);
@@ -80,11 +82,12 @@ async function handleUpsert(config: SynthOSConfig, table: string, body: any, res
80
82
  }
81
83
 
82
84
  async function handleDelete(config: SynthOSConfig, table: string, id: string, res: Response): Promise<void> {
85
+ const sp = config.storageProvider;
83
86
  const folder = sharedTableFolder(config, table);
84
87
  const file = recordFile(folder, id);
85
88
  try {
86
- if (await checkIfExists(file)) {
87
- await deleteFile(file);
89
+ if (await sp.checkIfExists(file)) {
90
+ await sp.deleteFile(file);
88
91
  }
89
92
  res.json({ success: true });
90
93
  } catch (err: unknown) {
@@ -1,25 +1,25 @@
1
1
  import { Application } from 'express';
2
2
  import express from 'express';
3
- import fs from 'fs/promises';
4
3
  import path from 'path';
5
4
  import { SynthOSConfig } from '../init';
6
- import { checkIfExists, ensureFolderExists } from '../files';
7
5
 
8
6
  export function useSharedFileRoutes(config: SynthOSConfig, app: Application): void {
7
+ const sp = config.storageProvider;
8
+
9
9
  // List files in the shared files folder
10
10
  app.get('/api/shared/files', async (req, res) => {
11
11
  try {
12
12
  const folder = sharedFilesFolder(config);
13
- if (!(await checkIfExists(folder))) {
13
+ if (!(await sp.checkIfExists(folder))) {
14
14
  res.json({ files: [] });
15
15
  return;
16
16
  }
17
17
 
18
- const entries = await fs.readdir(folder);
18
+ const entries = await sp.listFiles(folder);
19
19
  const files: { name: string; size: number }[] = [];
20
20
  for (const entry of entries) {
21
- const stat = await fs.stat(path.join(folder, entry));
22
- if (stat.isFile()) {
21
+ const stat = await sp.stat(path.join(folder, entry));
22
+ if (stat.isFile) {
23
23
  files.push({ name: entry, size: stat.size });
24
24
  }
25
25
  }
@@ -39,12 +39,13 @@ export function useSharedFileRoutes(config: SynthOSConfig, app: Application): vo
39
39
  return;
40
40
  }
41
41
 
42
- if (!(await checkIfExists(filePath))) {
42
+ if (!(await sp.checkIfExists(filePath))) {
43
43
  res.status(404).json({ error: 'File not found' });
44
44
  return;
45
45
  }
46
46
 
47
- res.sendFile(filePath);
47
+ res.type(path.extname(req.params.filename));
48
+ sp.createReadStream(filePath).pipe(res);
48
49
  } catch (err: unknown) {
49
50
  console.error(err);
50
51
  res.status(500).json({ error: (err as Error).message });
@@ -67,10 +68,10 @@ export function useSharedFileRoutes(config: SynthOSConfig, app: Application): vo
67
68
  }
68
69
 
69
70
  const folder = sharedFilesFolder(config);
70
- await ensureFolderExists(folder);
71
- await fs.writeFile(filePath, req.body as Buffer);
71
+ await sp.ensureFolderExists(folder);
72
+ await sp.saveBuffer(filePath, req.body as Buffer);
72
73
 
73
- const stat = await fs.stat(filePath);
74
+ const stat = await sp.stat(filePath);
74
75
  res.status(201).json({ name: filename, size: stat.size });
75
76
  } catch (err: unknown) {
76
77
  console.error(err);
@@ -87,12 +88,12 @@ export function useSharedFileRoutes(config: SynthOSConfig, app: Application): vo
87
88
  return;
88
89
  }
89
90
 
90
- if (!(await checkIfExists(filePath))) {
91
+ if (!(await sp.checkIfExists(filePath))) {
91
92
  res.status(404).json({ error: 'File not found' });
92
93
  return;
93
94
  }
94
95
 
95
- await fs.unlink(filePath);
96
+ await sp.deleteFile(filePath);
96
97
  res.json({ deleted: true });
97
98
  } catch (err: unknown) {
98
99
  console.error(err);
package/src/settings.ts CHANGED
@@ -1,7 +1,7 @@
1
- import {checkIfExists, loadFile, saveFile} from './files';
2
1
  import path from 'path';
3
2
  import { ModelEntry, ProviderName, detectProvider } from './models';
4
3
  import { AgentConfig } from './agents';
4
+ import { SynthOSConfig } from './init';
5
5
 
6
6
  let _settings: Partial<SettingsV2>|undefined;
7
7
 
@@ -126,8 +126,8 @@ function migrateV1toV2(raw: Record<string, unknown>): SettingsV2 {
126
126
  };
127
127
  }
128
128
 
129
- export async function hasConfiguredSettings(folder: string): Promise<boolean> {
130
- const settings = await loadSettings(folder);
129
+ export async function hasConfiguredSettings(config: SynthOSConfig): Promise<boolean> {
130
+ const settings = await loadSettings(config);
131
131
  const builder = getModelEntry(settings, 'builder');
132
132
  if (typeof builder.configuration.apiKey !== 'string' || builder.configuration.apiKey.length == 0) {
133
133
  return false;
@@ -138,18 +138,19 @@ export async function hasConfiguredSettings(folder: string): Promise<boolean> {
138
138
  return true;
139
139
  }
140
140
 
141
- export async function loadSettings(folder: string): Promise<SettingsV2> {
141
+ export async function loadSettings(config: SynthOSConfig): Promise<SettingsV2> {
142
+ const sp = config.storageProvider;
142
143
  if (_settings == undefined) {
143
- const filename = path.join(folder, 'settings.json');
144
- if (await checkIfExists(filename)) {
144
+ const filename = path.join(config.pagesFolder, 'settings.json');
145
+ if (await sp.checkIfExists(filename)) {
145
146
  try {
146
- const raw = JSON.parse(await loadFile(filename));
147
+ const raw = JSON.parse(await sp.loadFile(filename));
147
148
  if (!raw.version) {
148
149
  // V1 file — migrate
149
150
  console.log('Migrating settings.json from v1 to v2...');
150
151
  const migrated = migrateV1toV2(raw);
151
152
  _settings = migrated;
152
- await saveFile(filename, JSON.stringify(migrated, null, 4));
153
+ await sp.saveFile(filename, JSON.stringify(migrated, null, 4));
153
154
  } else {
154
155
  _settings = raw as Partial<SettingsV2>;
155
156
  }
@@ -171,11 +172,12 @@ export async function loadSettings(folder: string): Promise<SettingsV2> {
171
172
  return merged;
172
173
  }
173
174
 
174
- export async function saveSettings(folder: string, settings: Partial<SettingsV2>): Promise<void> {
175
+ export async function saveSettings(config: SynthOSConfig, settings: Partial<SettingsV2>): Promise<void> {
176
+ const sp = config.storageProvider;
175
177
  _settings = {..._settings, ...settings};
176
178
  if (settings.models) {
177
179
  _settings.models = settings.models;
178
180
  }
179
181
  _settings.version = 2;
180
- await saveFile(path.join(folder, 'settings.json'), JSON.stringify(_settings, null, 4));
182
+ await sp.saveFile(path.join(config.pagesFolder, 'settings.json'), JSON.stringify(_settings, null, 4));
181
183
  }