synthos 0.9.0 → 0.10.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 (114) 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/customizer/Customizer.ts +12 -0
  96. package/src/index.ts +2 -1
  97. package/src/init.ts +78 -42
  98. package/src/pages.ts +98 -67
  99. package/src/service/createCompletePrompt.ts +3 -2
  100. package/src/service/requiresSettings.ts +4 -3
  101. package/src/service/useAgentRoutes.ts +12 -12
  102. package/src/service/useApiRoutes.ts +76 -61
  103. package/src/service/useConnectorRoutes.ts +11 -11
  104. package/src/service/useDataRoutes.ts +13 -10
  105. package/src/service/useFileRoutes.ts +14 -13
  106. package/src/service/usePageRoutes.ts +42 -40
  107. package/src/service/useSharedDataRoutes.ts +13 -10
  108. package/src/service/useSharedFileRoutes.ts +14 -13
  109. package/src/settings.ts +12 -10
  110. package/src/storage/FsStorageProvider.ts +87 -0
  111. package/src/storage/StorageProvider.ts +34 -0
  112. package/src/storage/index.ts +2 -0
  113. package/src/themes.ts +44 -18
  114. package/tests/pages.spec.ts +54 -84
package/src/init.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import * as fs from 'fs/promises';
2
2
  import path from "path";
3
- import { checkIfExists, copyFile, copyFolderRecursive, deleteFile, ensureFolderExists, findFileInFolders, listFiles, listFolders, saveFile } from "./files";
3
+ import { checkIfExists, findFileInFolders, listFolders } from "./files";
4
4
  import { PAGE_VERSION, getRequiredPages } from "./pages";
5
5
  import { DefaultSettings } from "./settings";
6
6
  import { Customizer } from './customizer';
7
+ import { StorageProvider, FsStorageProvider } from './storage';
7
8
 
8
9
  export interface SynthOSConfig {
9
10
  localFolder: string;
@@ -15,6 +16,7 @@ export interface SynthOSConfig {
15
16
  staticFilesFolders: string[];
16
17
  serviceConnectorsFolders: string[];
17
18
  requiredPages: string[];
19
+ storageProvider: StorageProvider;
18
20
  debug: boolean;
19
21
  debugPageUpdates: boolean;
20
22
  }
@@ -62,14 +64,17 @@ export async function createConfig(
62
64
  path.join(__dirname, '../service-connectors')
63
65
  ),
64
66
  requiredPages,
67
+ storageProvider: customizer?.storageProvider ?? new FsStorageProvider(),
65
68
  debug: options?.debug ?? false,
66
69
  debugPageUpdates: options?.debugPageUpdates ?? false
67
70
  };
68
71
  }
69
72
 
70
73
  export async function init(config: SynthOSConfig, includeDefaultPages: boolean = true): Promise<boolean> {
74
+ const sp = config.storageProvider;
75
+
71
76
  // Check for existing folder
72
- if (await checkIfExists(config.pagesFolder)) {
77
+ if (await sp.checkIfExists(config.pagesFolder)) {
73
78
  await repairMissingFolders(config);
74
79
  return false;
75
80
  }
@@ -77,17 +82,17 @@ export async function init(config: SynthOSConfig, includeDefaultPages: boolean =
77
82
  console.log(`Initializing ${config.localFolder} folder...`);
78
83
 
79
84
  // Create pages folder
80
- await ensureFolderExists(config.pagesFolder);
85
+ await sp.ensureFolderExists(config.pagesFolder);
81
86
 
82
87
  // Create mandatory files
83
- await saveFile(path.join(config.pagesFolder, '.gitignore'), 'settings.json\n');
84
- await saveFile(path.join(config.pagesFolder, 'settings.json'), JSON.stringify(DefaultSettings, null, 4));
85
- await saveFile(path.join(config.pagesFolder, 'settings.json.example'), JSON.stringify(DefaultSettings, null, 4));
88
+ await sp.saveFile(path.join(config.pagesFolder, '.gitignore'), 'settings.json\n');
89
+ await sp.saveFile(path.join(config.pagesFolder, 'settings.json'), JSON.stringify(DefaultSettings, null, 4));
90
+ await sp.saveFile(path.join(config.pagesFolder, 'settings.json.example'), JSON.stringify(DefaultSettings, null, 4));
86
91
 
87
92
  // Setup default scripts
88
93
  console.log(`Copying default scripts to ${config.localFolder} folder...`);
89
94
  const scriptsFolder = path.join(config.pagesFolder, 'scripts');
90
- await ensureFolderExists(scriptsFolder);
95
+ await sp.ensureFolderExists(scriptsFolder);
91
96
  const scriptFilename = ({
92
97
  win32: 'windows-terminal.json',
93
98
  darwin: 'mac-terminal.json',
@@ -95,30 +100,34 @@ export async function init(config: SynthOSConfig, includeDefaultPages: boolean =
95
100
  } as Record<string, string>)[process.platform] ?? 'linux-terminal.json';
96
101
  const scriptSrc = await findFileInFolders(config.defaultScriptsFolders, scriptFilename);
97
102
  if (scriptSrc) {
98
- await copyFile(scriptSrc, scriptsFolder);
103
+ // Read from package (local fs), write to user storage (provider)
104
+ const data = await fs.readFile(scriptSrc);
105
+ await sp.saveBuffer(path.join(scriptsFolder, path.basename(scriptSrc)), data);
99
106
  }
100
107
 
101
- await saveFile(path.join(scriptsFolder, 'example.sh'), `#!/bin/bash\n\n# This is an example script\n\n# You can run this script using the following command:\n# sh ${config.localFolder}/scripts/example.sh\n\n# This script will print "Hello, World!" to the console\n\necho "Hello, World!"\n`);
108
+ await sp.saveFile(path.join(scriptsFolder, 'example.sh'), `#!/bin/bash\n\n# This is an example script\n\n# You can run this script using the following command:\n# sh ${config.localFolder}/scripts/example.sh\n\n# This script will print "Hello, World!" to the console\n\necho "Hello, World!"\n`);
102
109
 
103
110
  // Create empty themes folder — default themes are served directly from
104
111
  // defaultThemesFolders; users can add custom themes here.
105
- await ensureFolderExists(path.join(config.pagesFolder, 'themes'));
112
+ await sp.ensureFolderExists(path.join(config.pagesFolder, 'themes'));
106
113
 
107
114
  // Copy pages
108
115
  if (includeDefaultPages) {
109
116
  console.log(`Copying default pages to ${config.localFolder} folder...`);
110
- await copyDefaultPages(config.defaultPagesFolders, config.pagesFolder);
117
+ await copyDefaultPages(config, config.defaultPagesFolders);
111
118
  }
112
119
 
113
120
  return true;
114
121
  }
115
122
 
116
123
  async function repairMissingFolders(config: SynthOSConfig): Promise<void> {
124
+ const sp = config.storageProvider;
125
+
117
126
  // Rebuild scripts folder from defaults if missing
118
127
  const scriptsFolder = path.join(config.pagesFolder, 'scripts');
119
- if (!await checkIfExists(scriptsFolder)) {
128
+ if (!await sp.checkIfExists(scriptsFolder)) {
120
129
  console.log(`Restoring default scripts to ${config.localFolder} folder...`);
121
- await ensureFolderExists(scriptsFolder);
130
+ await sp.ensureFolderExists(scriptsFolder);
122
131
  const scriptFilename = ({
123
132
  win32: 'windows-terminal.json',
124
133
  darwin: 'mac-terminal.json',
@@ -126,30 +135,31 @@ async function repairMissingFolders(config: SynthOSConfig): Promise<void> {
126
135
  } as Record<string, string>)[process.platform] ?? 'linux-terminal.json';
127
136
  const scriptSrc = await findFileInFolders(config.defaultScriptsFolders, scriptFilename);
128
137
  if (scriptSrc) {
129
- await copyFile(scriptSrc, scriptsFolder);
138
+ const data = await fs.readFile(scriptSrc);
139
+ await sp.saveBuffer(path.join(scriptsFolder, path.basename(scriptSrc)), data);
130
140
  }
131
- await saveFile(path.join(scriptsFolder, 'example.sh'), `#!/bin/bash\n\n# This is an example script\n\n# You can run this script using the following command:\n# sh ${config.localFolder}/scripts/example.sh\n\n# This script will print "Hello, World!" to the console\n\necho "Hello, World!"\n`);
141
+ await sp.saveFile(path.join(scriptsFolder, 'example.sh'), `#!/bin/bash\n\n# This is an example script\n\n# You can run this script using the following command:\n# sh ${config.localFolder}/scripts/example.sh\n\n# This script will print "Hello, World!" to the console\n\necho "Hello, World!"\n`);
132
142
  }
133
143
 
134
144
  // Ensure themes folder exists — default themes are served directly from
135
145
  // defaultThemesFolders; this folder is for user-added custom themes only.
136
- await ensureFolderExists(path.join(config.pagesFolder, 'themes'));
146
+ await sp.ensureFolderExists(path.join(config.pagesFolder, 'themes'));
137
147
 
138
148
  // Ensure pages/ subfolder exists
139
149
  const pagesSubdir = path.join(config.pagesFolder, 'pages');
140
- if (!await checkIfExists(pagesSubdir)) {
150
+ if (!await sp.checkIfExists(pagesSubdir)) {
141
151
  // No pages folder and no flat files — rebuild from defaults
142
- const htmlFiles = (await listFiles(config.pagesFolder)).filter(f => f.endsWith('.html'));
152
+ const htmlFiles = (await sp.listFiles(config.pagesFolder)).filter(f => f.endsWith('.html'));
143
153
  if (htmlFiles.length === 0) {
144
154
  console.log(`Restoring default pages to ${config.localFolder}/pages/ folder...`);
145
- await copyDefaultPages(config.defaultPagesFolders, config.pagesFolder);
155
+ await copyDefaultPages(config, config.defaultPagesFolders);
146
156
  } else {
147
- await ensureFolderExists(pagesSubdir);
157
+ await sp.ensureFolderExists(pagesSubdir);
148
158
  }
149
159
  }
150
160
 
151
161
  // Migrate any stray flat .html files from root into pages/<name>/
152
- await migrateFlatPages(config.pagesFolder, config.localFolder);
162
+ await migrateFlatPages(config);
153
163
  }
154
164
 
155
165
  function toTitleCase(name: string): string {
@@ -160,13 +170,15 @@ function toTitleCase(name: string): string {
160
170
  .replace(/\b\w/g, c => c.toUpperCase());
161
171
  }
162
172
 
163
- async function migrateFlatPages(pagesFolder: string, localFolder: string): Promise<void> {
173
+ async function migrateFlatPages(config: SynthOSConfig): Promise<void> {
174
+ const sp = config.storageProvider;
175
+ const pagesFolder = config.pagesFolder;
164
176
  const pagesSubdir = path.join(pagesFolder, 'pages');
165
- const htmlFiles = (await listFiles(pagesFolder)).filter(f => f.endsWith('.html'));
177
+ const htmlFiles = (await sp.listFiles(pagesFolder)).filter(f => f.endsWith('.html'));
166
178
  if (htmlFiles.length === 0) return;
167
179
 
168
- console.log(`Migrating ${htmlFiles.length} page(s) to ${localFolder}/pages/ folder...`);
169
- await ensureFolderExists(pagesSubdir);
180
+ console.log(`Migrating ${htmlFiles.length} page(s) to ${config.localFolder}/pages/ folder...`);
181
+ await sp.ensureFolderExists(pagesSubdir);
170
182
  const now = new Date().toISOString();
171
183
 
172
184
  for (const file of htmlFiles) {
@@ -174,12 +186,11 @@ async function migrateFlatPages(pagesFolder: string, localFolder: string): Promi
174
186
  const category = pageName.startsWith('[') ? 'Builder' : 'Pages';
175
187
  const title = toTitleCase(pageName);
176
188
  const pageFolder = path.join(pagesSubdir, pageName);
177
- await ensureFolderExists(pageFolder);
178
- await fs.copyFile(
179
- path.join(pagesFolder, file),
180
- path.join(pageFolder, 'page.html')
181
- );
182
- await saveFile(
189
+ await sp.ensureFolderExists(pageFolder);
190
+ // Both source and destination are user storage
191
+ const content = await sp.loadFile(path.join(pagesFolder, file));
192
+ await sp.saveFile(path.join(pageFolder, 'page.html'), content);
193
+ await sp.saveFile(
183
194
  path.join(pageFolder, 'page.json'),
184
195
  JSON.stringify({
185
196
  title,
@@ -192,19 +203,20 @@ async function migrateFlatPages(pagesFolder: string, localFolder: string): Promi
192
203
  mode: 'unlocked',
193
204
  }, null, 4)
194
205
  );
195
- await deleteFile(path.join(pagesFolder, file));
206
+ await sp.deleteFile(path.join(pagesFolder, file));
196
207
  }
197
208
  }
198
209
 
199
- async function copyDefaultPages(srcFolders: string[], destFolder: string): Promise<void> {
200
- const pagesDir = path.join(destFolder, 'pages');
201
- await ensureFolderExists(pagesDir);
210
+ async function copyDefaultPages(config: SynthOSConfig, srcFolders: string[]): Promise<void> {
211
+ const sp = config.storageProvider;
212
+ const pagesDir = path.join(config.pagesFolder, 'pages');
213
+ await sp.ensureFolderExists(pagesDir);
202
214
  const now = new Date().toISOString();
203
215
  const seen = new Set<string>();
204
216
 
205
217
  for (const srcFolder of srcFolders) {
206
- if (!await checkIfExists(srcFolder)) continue;
207
- const dirs = await listFolders(srcFolder);
218
+ if (!await checkIfExists(srcFolder)) continue; // source is always local fs
219
+ const dirs = await listFolders(srcFolder); // source is always local fs
208
220
  for (const dir of dirs) {
209
221
  const srcPageDir = path.join(srcFolder, dir);
210
222
  if (!await checkIfExists(path.join(srcPageDir, 'page.html'))) continue;
@@ -212,8 +224,11 @@ async function copyDefaultPages(srcFolders: string[], destFolder: string): Promi
212
224
  seen.add(dir);
213
225
 
214
226
  const pageFolder = path.join(pagesDir, dir);
215
- await ensureFolderExists(pageFolder);
216
- await fs.copyFile(path.join(srcPageDir, 'page.html'), path.join(pageFolder, 'page.html'));
227
+ await sp.ensureFolderExists(pageFolder);
228
+
229
+ // Read from local fs, write via provider
230
+ const htmlData = await fs.readFile(path.join(srcPageDir, 'page.html'));
231
+ await sp.saveBuffer(path.join(pageFolder, 'page.html'), htmlData);
217
232
 
218
233
  // Read companion page.json metadata from source folder, fall back to defaults
219
234
  let metadata: Record<string, unknown> = {};
@@ -237,17 +252,38 @@ async function copyDefaultPages(srcFolders: string[], destFolder: string): Promi
237
252
  : typeof metadata.uxVersion === 'number' ? metadata.uxVersion : PAGE_VERSION,
238
253
  mode: metadata.mode === 'locked' ? 'locked' : 'unlocked',
239
254
  };
240
- await saveFile(path.join(pageFolder, 'page.json'), JSON.stringify(fullMetadata, null, 4));
255
+ await sp.saveFile(path.join(pageFolder, 'page.json'), JSON.stringify(fullMetadata, null, 4));
241
256
 
242
257
  // Copy data subfolders (anything that isn't page.html/page.json)
258
+ // Read from local fs, write via provider using cross-provider copy
243
259
  const subEntries = await fs.readdir(srcPageDir, { withFileTypes: true });
244
260
  for (const entry of subEntries) {
245
261
  if (!entry.isDirectory()) continue;
246
- await copyFolderRecursive(
262
+ await crossProviderCopyFolder(
247
263
  path.join(srcPageDir, entry.name),
248
- path.join(pageFolder, entry.name)
264
+ path.join(pageFolder, entry.name),
265
+ sp
249
266
  );
250
267
  }
251
268
  }
252
269
  }
253
270
  }
271
+
272
+ /**
273
+ * Copy a folder from local filesystem to the storage provider.
274
+ * Reads from local fs, writes via provider.
275
+ */
276
+ async function crossProviderCopyFolder(srcFolder: string, destFolder: string, sp: StorageProvider): Promise<void> {
277
+ await sp.ensureFolderExists(destFolder);
278
+ const entries = await fs.readdir(srcFolder, { withFileTypes: true });
279
+ for (const entry of entries) {
280
+ const srcPath = path.join(srcFolder, entry.name);
281
+ const destPath = path.join(destFolder, entry.name);
282
+ if (entry.isDirectory()) {
283
+ await crossProviderCopyFolder(srcPath, destPath, sp);
284
+ } else {
285
+ const data = await fs.readFile(srcPath);
286
+ await sp.saveBuffer(destPath, data);
287
+ }
288
+ }
289
+ }
package/src/pages.ts CHANGED
@@ -1,5 +1,6 @@
1
- import {checkIfExists, copyFolderRecursive, deleteFile, deleteFolder, ensureFolderExists, listFiles, listFolders, loadFile, saveFile} from './files';
1
+ import {checkIfExists, listFolders, loadFile} from './files';
2
2
  import path from 'path';
3
+ import { SynthOSConfig } from './init';
3
4
 
4
5
  /**
5
6
  * Derive the list of required page names by scanning *.html files
@@ -38,12 +39,15 @@ export interface PageInfo {
38
39
 
39
40
  export type PageMetadata = Omit<PageInfo, 'name'>;
40
41
 
41
- export async function loadPageMetadata(pagesFolder: string, name: string, fallbackFolders?: string[]): Promise<PageMetadata | undefined> {
42
+ export async function loadPageMetadata(config: SynthOSConfig, name: string, fallbackFolders?: string[]): Promise<PageMetadata | undefined> {
43
+ const pagesFolder = config.pagesFolder;
44
+ const sp = config.storageProvider;
45
+
42
46
  // 1. Try user override: <localFolder>/pages/<name>/page.json
43
47
  const metadataPath = path.join(pagesFolder, 'pages', name, 'page.json');
44
- if (await checkIfExists(metadataPath)) {
48
+ if (await sp.checkIfExists(metadataPath)) {
45
49
  try {
46
- const raw = await loadFile(metadataPath);
50
+ const raw = await sp.loadFile(metadataPath);
47
51
  const parsed = JSON.parse(raw);
48
52
  return parseMetadata(parsed);
49
53
  } catch {
@@ -51,7 +55,7 @@ export async function loadPageMetadata(pagesFolder: string, name: string, fallba
51
55
  }
52
56
  }
53
57
 
54
- // 2. Try fallback folders: fallbackFolder/<name>/page.json
58
+ // 2. Try fallback folders: fallbackFolder/<name>/page.json (package content, always local fs)
55
59
  if (fallbackFolders) {
56
60
  for (const folder of fallbackFolders) {
57
61
  const candidate = path.join(folder, name, 'page.json');
@@ -84,10 +88,11 @@ export function parseMetadata(parsed: Record<string, unknown>): PageMetadata {
84
88
  };
85
89
  }
86
90
 
87
- export async function savePageMetadata(pagesFolder: string, name: string, metadata: PageMetadata): Promise<void> {
88
- const pageFolder = path.join(pagesFolder, 'pages', name);
89
- await ensureFolderExists(pageFolder);
90
- await saveFile(path.join(pageFolder, 'page.json'), JSON.stringify(metadata, null, 4));
91
+ export async function savePageMetadata(config: SynthOSConfig, name: string, metadata: PageMetadata): Promise<void> {
92
+ const sp = config.storageProvider;
93
+ const pageFolder = path.join(config.pagesFolder, 'pages', name);
94
+ await sp.ensureFolderExists(pageFolder);
95
+ await sp.saveFile(path.join(pageFolder, 'page.json'), JSON.stringify(metadata, null, 4));
91
96
  }
92
97
 
93
98
  const DEFAULT_METADATA: PageMetadata = {
@@ -101,16 +106,18 @@ const DEFAULT_METADATA: PageMetadata = {
101
106
  mode: 'unlocked',
102
107
  };
103
108
 
104
- export async function listPages(pagesFolder: string, fallbackPagesFolders: string[]): Promise<PageInfo[]> {
109
+ export async function listPages(config: SynthOSConfig, fallbackPagesFolders: string[]): Promise<PageInfo[]> {
110
+ const pagesFolder = config.pagesFolder;
111
+ const sp = config.storageProvider;
105
112
  const pageMap = new Map<string, PageInfo>();
106
113
 
107
114
  // Folder-based pages under pages/ subdirectory
108
115
  const pagesSubdir = path.join(pagesFolder, 'pages');
109
- if (await checkIfExists(pagesSubdir)) {
110
- const folders = await listFolders(pagesSubdir);
116
+ if (await sp.checkIfExists(pagesSubdir)) {
117
+ const folders = await sp.listFolders(pagesSubdir);
111
118
  for (const folder of folders) {
112
- if (await checkIfExists(path.join(pagesSubdir, folder, 'page.html'))) {
113
- const metadata = await loadPageMetadata(pagesFolder, folder);
119
+ if (await sp.checkIfExists(path.join(pagesSubdir, folder, 'page.html'))) {
120
+ const metadata = await loadPageMetadata(config, folder);
114
121
  pageMap.set(folder, {
115
122
  name: folder,
116
123
  ...(metadata ?? DEFAULT_METADATA),
@@ -120,7 +127,7 @@ export async function listPages(pagesFolder: string, fallbackPagesFolders: strin
120
127
  }
121
128
 
122
129
  // Legacy flat .html files in root (v1 pages)
123
- const flatFiles = (await listFiles(pagesFolder)).filter(file => file.endsWith('.html'));
130
+ const flatFiles = (await sp.listFiles(pagesFolder)).filter(file => file.endsWith('.html'));
124
131
  for (const file of flatFiles) {
125
132
  const name = file.replace(/\.html$/, '');
126
133
  if (!pageMap.has(name)) {
@@ -146,7 +153,7 @@ export async function listPages(pagesFolder: string, fallbackPagesFolders: strin
146
153
  }
147
154
  }
148
155
 
149
- // Add pages from fallback (required) pages folders
156
+ // Add pages from fallback (required) pages folders (package content, always local fs)
150
157
  for (const folder of fallbackPagesFolders) {
151
158
  if (!await checkIfExists(folder)) continue;
152
159
  const dirs = await listFolders(folder);
@@ -154,7 +161,7 @@ export async function listPages(pagesFolder: string, fallbackPagesFolders: strin
154
161
  if (pageMap.has(name)) continue;
155
162
  if (!await checkIfExists(path.join(folder, name, 'page.html'))) continue;
156
163
  // System page not yet in map — check for user override, then fallback page.json
157
- const metadata = await loadPageMetadata(pagesFolder, name, fallbackPagesFolders);
164
+ const metadata = await loadPageMetadata(config, name, fallbackPagesFolders);
158
165
  pageMap.set(name, {
159
166
  name,
160
167
  title: metadata?.title ?? '',
@@ -176,11 +183,14 @@ export async function listPages(pagesFolder: string, fallbackPagesFolders: strin
176
183
  return entries;
177
184
  }
178
185
 
179
- export async function loadPageState(pagesFolder: string, name: string): Promise<string|undefined> {
186
+ export async function loadPageState(config: SynthOSConfig, name: string): Promise<string|undefined> {
187
+ const pagesFolder = config.pagesFolder;
188
+ const sp = config.storageProvider;
189
+
180
190
  // Check for working-state version files first
181
- const latestVersion = await getLatestVersion(pagesFolder, name);
191
+ const latestVersion = await getLatestVersion(config, name);
182
192
  if (latestVersion > 0) {
183
- const versionHtml = await loadPageVersion(pagesFolder, name, latestVersion);
193
+ const versionHtml = await loadPageVersion(config, name, latestVersion);
184
194
  if (versionHtml) return versionHtml;
185
195
  }
186
196
 
@@ -189,12 +199,12 @@ export async function loadPageState(pagesFolder: string, name: string): Promise<
189
199
  const directFolderPath = path.join(pagesFolder, name, 'page.html');
190
200
  const flatPath = path.join(pagesFolder, `${name}.html`);
191
201
 
192
- if (await checkIfExists(folderPath)) {
193
- return loadFile(folderPath);
194
- } else if (await checkIfExists(directFolderPath)) {
195
- return loadFile(directFolderPath);
196
- } else if (await checkIfExists(flatPath)) {
197
- return loadFile(flatPath);
202
+ if (await sp.checkIfExists(folderPath)) {
203
+ return sp.loadFile(folderPath);
204
+ } else if (await sp.checkIfExists(directFolderPath)) {
205
+ return sp.loadFile(directFolderPath);
206
+ } else if (await sp.checkIfExists(flatPath)) {
207
+ return sp.loadFile(flatPath);
198
208
  }
199
209
  return undefined;
200
210
  }
@@ -203,14 +213,15 @@ export function normalizePageName(name: string|undefined): string|undefined {
203
213
  return typeof name == 'string' && name.length > 0 ? name.replace(/[^a-z0-9\-_\[\]\(\)\{\}@#\$%&]/gi, '_').toLowerCase() : undefined;
204
214
  }
205
215
 
206
- export async function savePageState(pagesFolder: string, name: string, content: string, title?: string, categories?: string[]): Promise<void> {
207
- const pageFolder = path.join(pagesFolder, 'pages', name);
208
- await ensureFolderExists(pageFolder);
209
- await saveFile(path.join(pageFolder, 'page.html'), content);
216
+ export async function savePageState(config: SynthOSConfig, name: string, content: string, title?: string, categories?: string[]): Promise<void> {
217
+ const sp = config.storageProvider;
218
+ const pageFolder = path.join(config.pagesFolder, 'pages', name);
219
+ await sp.ensureFolderExists(pageFolder);
220
+ await sp.saveFile(path.join(pageFolder, 'page.html'), content);
210
221
 
211
222
  // Create page.json with full metadata if it doesn't exist
212
223
  const metadataPath = path.join(pageFolder, 'page.json');
213
- if (!(await checkIfExists(metadataPath))) {
224
+ if (!(await sp.checkIfExists(metadataPath))) {
214
225
  const now = new Date().toISOString();
215
226
  const metadata: PageMetadata = {
216
227
  title: title ?? '',
@@ -222,21 +233,24 @@ export async function savePageState(pagesFolder: string, name: string, content:
222
233
  pageVersion: PAGE_VERSION,
223
234
  mode: 'unlocked',
224
235
  };
225
- await saveFile(metadataPath, JSON.stringify(metadata, null, 4));
236
+ await sp.saveFile(metadataPath, JSON.stringify(metadata, null, 4));
226
237
  }
227
238
  }
228
239
 
229
- export async function deletePage(pagesFolder: string, name: string): Promise<void> {
240
+ export async function deletePage(config: SynthOSConfig, name: string): Promise<void> {
241
+ const pagesFolder = config.pagesFolder;
242
+ const sp = config.storageProvider;
243
+
230
244
  // Delete folder-based page: <pagesFolder>/pages/<name>/
231
245
  const folderPath = path.join(pagesFolder, 'pages', name);
232
- if (await checkIfExists(folderPath)) {
233
- await deleteFolder(folderPath);
246
+ if (await sp.checkIfExists(folderPath)) {
247
+ await sp.deleteFolder(folderPath);
234
248
  }
235
249
 
236
250
  // Delete legacy flat file: <pagesFolder>/<name>.html
237
251
  const flatPath = path.join(pagesFolder, `${name}.html`);
238
- if (await checkIfExists(flatPath)) {
239
- await deleteFile(flatPath);
252
+ if (await sp.checkIfExists(flatPath)) {
253
+ await sp.deleteFile(flatPath);
240
254
  }
241
255
 
242
256
  }
@@ -248,28 +262,31 @@ export async function deletePage(pagesFolder: string, name: string): Promise<voi
248
262
  /**
249
263
  * Save a version snapshot: <pagesFolder>/pages/<name>/page.v<version>.html
250
264
  */
251
- export async function savePageVersion(pagesFolder: string, name: string, version: number, html: string): Promise<void> {
252
- const pageFolder = path.join(pagesFolder, 'pages', name);
253
- await ensureFolderExists(pageFolder);
254
- await saveFile(path.join(pageFolder, `page.v${version}.html`), html);
265
+ export async function savePageVersion(config: SynthOSConfig, name: string, version: number, html: string): Promise<void> {
266
+ const sp = config.storageProvider;
267
+ const pageFolder = path.join(config.pagesFolder, 'pages', name);
268
+ await sp.ensureFolderExists(pageFolder);
269
+ await sp.saveFile(path.join(pageFolder, `page.v${version}.html`), html);
255
270
  }
256
271
 
257
272
  /**
258
273
  * Load a version snapshot (returns undefined if the file doesn't exist).
259
274
  */
260
- export async function loadPageVersion(pagesFolder: string, name: string, version: number): Promise<string | undefined> {
261
- const filePath = path.join(pagesFolder, 'pages', name, `page.v${version}.html`);
262
- if (!await checkIfExists(filePath)) return undefined;
263
- return loadFile(filePath);
275
+ export async function loadPageVersion(config: SynthOSConfig, name: string, version: number): Promise<string | undefined> {
276
+ const sp = config.storageProvider;
277
+ const filePath = path.join(config.pagesFolder, 'pages', name, `page.v${version}.html`);
278
+ if (!await sp.checkIfExists(filePath)) return undefined;
279
+ return sp.loadFile(filePath);
264
280
  }
265
281
 
266
282
  /**
267
283
  * Scan page.v*.html files and return the highest version number (0 if none).
268
284
  */
269
- export async function getLatestVersion(pagesFolder: string, name: string): Promise<number> {
270
- const pageFolder = path.join(pagesFolder, 'pages', name);
271
- if (!await checkIfExists(pageFolder)) return 0;
272
- const files = await listFiles(pageFolder);
285
+ export async function getLatestVersion(config: SynthOSConfig, name: string): Promise<number> {
286
+ const sp = config.storageProvider;
287
+ const pageFolder = path.join(config.pagesFolder, 'pages', name);
288
+ if (!await sp.checkIfExists(pageFolder)) return 0;
289
+ const files = await sp.listFiles(pageFolder);
273
290
  let max = 0;
274
291
  for (const file of files) {
275
292
  const match = file.match(/^page\.v(\d+)\.html$/);
@@ -284,13 +301,14 @@ export async function getLatestVersion(pagesFolder: string, name: string): Promi
284
301
  /**
285
302
  * Delete all page.v*.html version files for a page.
286
303
  */
287
- export async function clearVersions(pagesFolder: string, name: string): Promise<void> {
288
- const pageFolder = path.join(pagesFolder, 'pages', name);
289
- if (!await checkIfExists(pageFolder)) return;
290
- const files = await listFiles(pageFolder);
304
+ export async function clearVersions(config: SynthOSConfig, name: string): Promise<void> {
305
+ const sp = config.storageProvider;
306
+ const pageFolder = path.join(config.pagesFolder, 'pages', name);
307
+ if (!await sp.checkIfExists(pageFolder)) return;
308
+ const files = await sp.listFiles(pageFolder);
291
309
  for (const file of files) {
292
310
  if (/^page\.v\d+\.html$/.test(file)) {
293
- await deleteFile(path.join(pageFolder, file));
311
+ await sp.deleteFile(path.join(pageFolder, file));
294
312
  }
295
313
  }
296
314
  }
@@ -301,7 +319,7 @@ export interface CopyPageOptions {
301
319
  }
302
320
 
303
321
  export async function copyPage(
304
- pagesFolder: string,
322
+ config: SynthOSConfig,
305
323
  sourceName: string,
306
324
  targetName: string,
307
325
  title: string,
@@ -309,13 +327,15 @@ export async function copyPage(
309
327
  requiredPagesFolders: string[],
310
328
  options?: CopyPageOptions
311
329
  ): Promise<void> {
330
+ const pagesFolder = config.pagesFolder;
331
+ const sp = config.storageProvider;
312
332
  const copyTables = options?.copyTables ?? false;
313
- const copyFiles = options?.copyFiles ?? true;
333
+ const cpFiles = options?.copyFiles ?? true;
314
334
 
315
335
  // Resolve source page folder: user pages first, then required pages
316
336
  let sourceFolder: string | undefined;
317
337
  const userSourceFolder = path.join(pagesFolder, 'pages', sourceName);
318
- if (await checkIfExists(path.join(userSourceFolder, 'page.html'))) {
338
+ if (await sp.checkIfExists(path.join(userSourceFolder, 'page.html'))) {
319
339
  sourceFolder = userSourceFolder;
320
340
  } else {
321
341
  for (const folder of requiredPagesFolders) {
@@ -330,12 +350,17 @@ export async function copyPage(
330
350
  // Load source HTML
331
351
  let html: string | undefined;
332
352
  if (sourceFolder) {
333
- html = await loadFile(path.join(sourceFolder, 'page.html'));
353
+ // Source could be user storage or package — try user first, fall back to local fs
354
+ if (sourceFolder === userSourceFolder) {
355
+ html = await sp.loadFile(path.join(sourceFolder, 'page.html'));
356
+ } else {
357
+ html = await loadFile(path.join(sourceFolder, 'page.html'));
358
+ }
334
359
  } else {
335
360
  // Try legacy flat file
336
361
  const flatPath = path.join(pagesFolder, `${sourceName}.html`);
337
- if (await checkIfExists(flatPath)) {
338
- html = await loadFile(flatPath);
362
+ if (await sp.checkIfExists(flatPath)) {
363
+ html = await sp.loadFile(flatPath);
339
364
  }
340
365
  }
341
366
 
@@ -344,7 +369,7 @@ export async function copyPage(
344
369
  }
345
370
 
346
371
  // Save HTML to target (creates folder + page.html + page.json)
347
- await savePageState(pagesFolder, targetName, html, title);
372
+ await savePageState(config, targetName, html, title);
348
373
 
349
374
  // Overwrite the generated metadata with provided title + categories
350
375
  const now = new Date().toISOString();
@@ -358,27 +383,33 @@ export async function copyPage(
358
383
  pageVersion: PAGE_VERSION,
359
384
  mode: 'unlocked',
360
385
  };
361
- await savePageMetadata(pagesFolder, targetName, metadata);
386
+ await savePageMetadata(config, targetName, metadata);
362
387
 
363
388
  // Copy additional content from source if a folder was resolved
364
389
  if (sourceFolder) {
365
390
  const targetFolder = path.join(pagesFolder, 'pages', targetName);
366
391
 
367
392
  if (copyTables) {
368
- const entries = await listFolders(sourceFolder);
393
+ // Source could be user or package folder — use appropriate listing
394
+ const entries = sourceFolder === userSourceFolder
395
+ ? await sp.listFolders(sourceFolder)
396
+ : await listFolders(sourceFolder);
369
397
  for (const entry of entries) {
370
398
  if (entry === 'files') continue; // handled separately
371
- await copyFolderRecursive(
399
+ await sp.copyFolderRecursive(
372
400
  path.join(sourceFolder, entry),
373
401
  path.join(targetFolder, entry)
374
402
  );
375
403
  }
376
404
  }
377
405
 
378
- if (copyFiles) {
406
+ if (cpFiles) {
379
407
  const filesDir = path.join(sourceFolder, 'files');
380
- if (await checkIfExists(filesDir)) {
381
- await copyFolderRecursive(filesDir, path.join(targetFolder, 'files'));
408
+ const filesDirExists = sourceFolder === userSourceFolder
409
+ ? await sp.checkIfExists(filesDir)
410
+ : await checkIfExists(filesDir);
411
+ if (filesDirExists) {
412
+ await sp.copyFolderRecursive(filesDir, path.join(targetFolder, 'files'));
382
413
  }
383
414
  }
384
415
  }
@@ -1,10 +1,11 @@
1
1
  import { anthropic, completePrompt, fireworksai, logCompletePrompt, openai } from '../models';
2
2
  import { getModelEntry, loadSettings } from '../settings';
3
3
  import { PROVIDERS } from '../models';
4
+ import { SynthOSConfig } from '../init';
4
5
 
5
- export async function createCompletePrompt(pagesFolder: string, use: 'builder' | 'chat', modelOverride?: string): Promise<completePrompt> {
6
+ export async function createCompletePrompt(config: SynthOSConfig, use: 'builder' | 'chat', modelOverride?: string): Promise<completePrompt> {
6
7
  // Get configuration settings
7
- const settings = await loadSettings(pagesFolder);
8
+ const settings = await loadSettings(config);
8
9
  const entry = getModelEntry(settings, use);
9
10
 
10
11
  if (!entry.configuration.apiKey) {
@@ -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);