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
@@ -0,0 +1,87 @@
1
+ import * as fs from 'fs/promises';
2
+ import { createReadStream } from 'fs';
3
+ import path from 'path';
4
+ import { StorageProvider } from './StorageProvider';
5
+
6
+ /**
7
+ * Default StorageProvider backed by the local filesystem via `fs/promises`.
8
+ * Mirrors the existing logic in `files.ts`.
9
+ */
10
+ export class FsStorageProvider implements StorageProvider {
11
+ // --- Text file operations ---
12
+
13
+ async checkIfExists(filePath: string): Promise<boolean> {
14
+ try {
15
+ await fs.access(filePath);
16
+ return true;
17
+ } catch {
18
+ return false;
19
+ }
20
+ }
21
+
22
+ async loadFile(filePath: string): Promise<string> {
23
+ return await fs.readFile(filePath, 'utf8');
24
+ }
25
+
26
+ async saveFile(filePath: string, content: string): Promise<void> {
27
+ await fs.writeFile(filePath, content, 'utf8');
28
+ }
29
+
30
+ async deleteFile(filePath: string): Promise<void> {
31
+ await fs.unlink(filePath);
32
+ }
33
+
34
+ // --- Binary file operations ---
35
+
36
+ async saveBuffer(filePath: string, data: Buffer): Promise<void> {
37
+ await fs.writeFile(filePath, data);
38
+ }
39
+
40
+ async loadBuffer(filePath: string): Promise<Buffer> {
41
+ return await fs.readFile(filePath);
42
+ }
43
+
44
+ async stat(filePath: string): Promise<{ size: number; isFile: boolean }> {
45
+ const s = await fs.stat(filePath);
46
+ return { size: s.size, isFile: s.isFile() };
47
+ }
48
+
49
+ createReadStream(filePath: string): NodeJS.ReadableStream {
50
+ return createReadStream(filePath);
51
+ }
52
+
53
+ // --- Directory operations ---
54
+
55
+ async listFiles(dirPath: string): Promise<string[]> {
56
+ return (await fs.readdir(dirPath)).filter(file => !file.startsWith('.') && file.includes('.'));
57
+ }
58
+
59
+ async listFolders(dirPath: string): Promise<string[]> {
60
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
61
+ return entries
62
+ .filter(entry => entry.isDirectory() && !entry.name.startsWith('.'))
63
+ .map(entry => entry.name);
64
+ }
65
+
66
+ async ensureFolderExists(dirPath: string): Promise<void> {
67
+ await fs.mkdir(dirPath, { recursive: true });
68
+ }
69
+
70
+ async deleteFolder(dirPath: string): Promise<void> {
71
+ await fs.rm(dirPath, { recursive: true });
72
+ }
73
+
74
+ async copyFolderRecursive(srcPath: string, destPath: string): Promise<void> {
75
+ await this.ensureFolderExists(destPath);
76
+ const entries = await fs.readdir(srcPath, { withFileTypes: true });
77
+ for (const entry of entries) {
78
+ const src = path.join(srcPath, entry.name);
79
+ const dest = path.join(destPath, entry.name);
80
+ if (entry.isDirectory()) {
81
+ await this.copyFolderRecursive(src, dest);
82
+ } else {
83
+ await fs.copyFile(src, dest);
84
+ }
85
+ }
86
+ }
87
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Abstraction over user-mutable storage (pages, settings, themes, data, uploads).
3
+ *
4
+ * All paths are absolute. Callers build paths with `path.join(config.pagesFolder, ...)`
5
+ * exactly as they do today — the provider simply executes the I/O against
6
+ * whatever backend it wraps (local filesystem, blob storage, etc.).
7
+ *
8
+ * Package/read-only content (required-pages, default-themes, static-files,
9
+ * service-connectors) stays on local filesystem and is NOT routed through
10
+ * this interface.
11
+ */
12
+ export interface StorageProvider {
13
+ // --- Text file operations ---
14
+
15
+ checkIfExists(filePath: string): Promise<boolean>;
16
+ loadFile(filePath: string): Promise<string>;
17
+ saveFile(filePath: string, content: string): Promise<void>;
18
+ deleteFile(filePath: string): Promise<void>;
19
+
20
+ // --- Binary file operations (uploads / images) ---
21
+
22
+ saveBuffer(filePath: string, data: Buffer): Promise<void>;
23
+ loadBuffer(filePath: string): Promise<Buffer>;
24
+ stat(filePath: string): Promise<{ size: number; isFile: boolean }>;
25
+ createReadStream(filePath: string): NodeJS.ReadableStream;
26
+
27
+ // --- Directory operations ---
28
+
29
+ listFiles(dirPath: string): Promise<string[]>;
30
+ listFolders(dirPath: string): Promise<string[]>;
31
+ ensureFolderExists(dirPath: string): Promise<void>;
32
+ deleteFolder(dirPath: string): Promise<void>;
33
+ copyFolderRecursive(srcPath: string, destPath: string): Promise<void>;
34
+ }
@@ -0,0 +1,2 @@
1
+ export { StorageProvider } from './StorageProvider';
2
+ export { FsStorageProvider } from './FsStorageProvider';
package/src/themes.ts CHANGED
@@ -28,10 +28,29 @@ export function parseThemeFilename(filename: string): { name: string; version: n
28
28
  }
29
29
 
30
30
  /**
31
- * Find the CSS file for a theme by name in a folder.
31
+ * Find the CSS file for a theme by name in a user folder (via storageProvider).
32
32
  * Prefers the highest-versioned file (e.g. name.v2.css over name.css).
33
33
  */
34
- async function findThemeCssFile(folder: string, name: string): Promise<{ path: string; version: number } | undefined> {
34
+ async function findUserThemeCssFile(config: SynthOSConfig, folder: string, name: string): Promise<{ path: string; version: number } | undefined> {
35
+ const sp = config.storageProvider;
36
+ if (!await sp.checkIfExists(folder)) return undefined;
37
+ const files = await sp.listFiles(folder);
38
+ let best: { path: string; version: number } | undefined;
39
+ for (const f of files) {
40
+ const parsed = parseThemeFilename(f);
41
+ if (parsed && parsed.name === name) {
42
+ if (!best || parsed.version > best.version) {
43
+ best = { path: path.join(folder, f), version: parsed.version };
44
+ }
45
+ }
46
+ }
47
+ return best;
48
+ }
49
+
50
+ /**
51
+ * Find the CSS file for a theme by name in a local-fs folder (package defaults).
52
+ */
53
+ async function findDefaultThemeCssFile(folder: string, name: string): Promise<{ path: string; version: number } | undefined> {
35
54
  if (!await checkIfExists(folder)) return undefined;
36
55
  const files = await listFiles(folder);
37
56
  let best: { path: string; version: number } | undefined;
@@ -47,23 +66,26 @@ async function findThemeCssFile(folder: string, name: string): Promise<{ path: s
47
66
  }
48
67
 
49
68
  export async function loadThemeVersion(name: string, config: SynthOSConfig): Promise<number> {
50
- const local = await findThemeCssFile(userThemesFolder(config), name);
69
+ const local = await findUserThemeCssFile(config, userThemesFolder(config), name);
51
70
  if (local) return local.version;
52
71
  for (const folder of config.defaultThemesFolders) {
53
- const def = await findThemeCssFile(folder, name);
72
+ const def = await findDefaultThemeCssFile(folder, name);
54
73
  if (def) return def.version;
55
74
  }
56
75
  return 1;
57
76
  }
58
77
 
59
78
  export async function loadThemeInfo(name: string, config: SynthOSConfig): Promise<ThemeInfo | undefined> {
60
- // Check user's local themes first, then fall back to package defaults
79
+ const sp = config.storageProvider;
80
+
81
+ // Check user's local themes first (user storage)
61
82
  const localPath = path.join(userThemesFolder(config), `${name}.json`);
62
- if (await checkIfExists(localPath)) {
63
- const raw = await loadFile(localPath);
83
+ if (await sp.checkIfExists(localPath)) {
84
+ const raw = await sp.loadFile(localPath);
64
85
  return raw ? JSON.parse(raw) : undefined;
65
86
  }
66
87
 
88
+ // Fall back to package defaults (local fs)
67
89
  const defaultPath = await findFileInFolders(config.defaultThemesFolders, `${name}.json`);
68
90
  if (defaultPath) {
69
91
  const raw = await loadFile(defaultPath);
@@ -74,15 +96,17 @@ export async function loadThemeInfo(name: string, config: SynthOSConfig): Promis
74
96
  }
75
97
 
76
98
  export async function loadTheme(name: string, config: SynthOSConfig): Promise<string | undefined> {
77
- // Check user's local themes first, then fall back to package defaults
78
- const local = await findThemeCssFile(userThemesFolder(config), name);
99
+ const sp = config.storageProvider;
100
+
101
+ // Check user's local themes first (user storage)
102
+ const local = await findUserThemeCssFile(config, userThemesFolder(config), name);
79
103
  if (local) {
80
- return await loadFile(local.path);
104
+ return await sp.loadFile(local.path);
81
105
  }
82
106
 
83
- // Search all default theme folders
107
+ // Search all default theme folders (local fs)
84
108
  for (const folder of config.defaultThemesFolders) {
85
- const def = await findThemeCssFile(folder, name);
109
+ const def = await findDefaultThemeCssFile(folder, name);
86
110
  if (def) {
87
111
  return await loadFile(def.path);
88
112
  }
@@ -92,19 +116,20 @@ export async function loadTheme(name: string, config: SynthOSConfig): Promise<st
92
116
  }
93
117
 
94
118
  export async function listThemes(config: SynthOSConfig): Promise<string[]> {
119
+ const sp = config.storageProvider;
95
120
  const names = new Set<string>();
96
121
 
97
- // Collect from user's local themes folder
122
+ // Collect from user's local themes folder (user storage)
98
123
  const localFolder = userThemesFolder(config);
99
- if (await checkIfExists(localFolder)) {
100
- const files = await listFiles(localFolder);
124
+ if (await sp.checkIfExists(localFolder)) {
125
+ const files = await sp.listFiles(localFolder);
101
126
  for (const f of files) {
102
127
  const parsed = parseThemeFilename(f);
103
128
  if (parsed) names.add(parsed.name);
104
129
  }
105
130
  }
106
131
 
107
- // Collect from all default theme folders
132
+ // Collect from all default theme folders (local fs)
108
133
  const defaultFiles = await listFilesFromFolders(config.defaultThemesFolders);
109
134
  for (const f of defaultFiles) {
110
135
  const parsed = parseThemeFilename(f);
@@ -118,11 +143,12 @@ export async function listThemes(config: SynthOSConfig): Promise<string[]> {
118
143
  * Compare local theme versions against defaults and return themes that need upgrading.
119
144
  */
120
145
  export async function getOutdatedThemes(config: SynthOSConfig): Promise<string[]> {
146
+ const sp = config.storageProvider;
121
147
  const localFolder = userThemesFolder(config);
122
- if (!await checkIfExists(localFolder)) return [];
148
+ if (!await sp.checkIfExists(localFolder)) return [];
123
149
 
124
150
  const defaultFiles = await listFilesFromFolders(config.defaultThemesFolders);
125
- const localFiles = await listFiles(localFolder);
151
+ const localFiles = await sp.listFiles(localFolder);
126
152
 
127
153
  // Build maps: theme name → highest version
128
154
  const defaultVersions = new Map<string, number>();
@@ -46,7 +46,7 @@ describe('Anthropic Builder — Haiku Classifier Routing', () => {
46
46
 
47
47
  const builder = createAnthropicBuilder(tracker.fn, undefined, 'SynthOS', {
48
48
  apiKey: 'test-key',
49
- model: 'claude-sonnet-4-5-20250514',
49
+ model: 'claude-sonnet-4-6-20250514',
50
50
  });
51
51
 
52
52
  const result = await builder.run(currentPage, additionalSections, 'change heading', false);
@@ -113,7 +113,7 @@ describe('Anthropic Builder — Haiku Classifier Routing', () => {
113
113
  // Non-Opus model should just work without classification
114
114
  const builder = createBuilder('Anthropic', tracker.fn, undefined, 'SynthOS', {
115
115
  apiKey: 'key',
116
- model: 'claude-sonnet-4-5-20250514',
116
+ model: 'claude-sonnet-4-6-20250514',
117
117
  });
118
118
 
119
119
  const result = await builder.run(currentPage, additionalSections, 'test', false);
@@ -9,12 +9,31 @@ import {
9
9
  loadPageMetadata,
10
10
  savePageState,
11
11
  loadPageState,
12
- updatePageState,
13
12
  listPages,
14
13
  deletePage,
15
14
  copyPage,
16
15
  PAGE_VERSION,
17
16
  } from '../src/pages';
17
+ import { SynthOSConfig } from '../src/init';
18
+ import { FsStorageProvider } from '../src/storage';
19
+
20
+ /** Create a minimal SynthOSConfig for testing. */
21
+ function makeConfig(pagesFolder: string): SynthOSConfig {
22
+ return {
23
+ localFolder: '.synthos',
24
+ pagesFolder,
25
+ requiredPagesFolders: [],
26
+ defaultPagesFolders: [],
27
+ defaultScriptsFolders: [],
28
+ defaultThemesFolders: [],
29
+ staticFilesFolders: [],
30
+ serviceConnectorsFolders: [],
31
+ requiredPages: [],
32
+ storageProvider: new FsStorageProvider(),
33
+ debug: false,
34
+ debugPageUpdates: false,
35
+ };
36
+ }
18
37
 
19
38
  // ---------------------------------------------------------------------------
20
39
  // normalizePageName
@@ -123,15 +142,16 @@ describe('parseMetadata', () => {
123
142
 
124
143
  describe('page I/O', () => {
125
144
  let tmpDir: string;
126
- let pagesFolder: string;
145
+ let config: SynthOSConfig;
127
146
  let fallbackFolder: string;
128
147
 
129
148
  beforeEach(async () => {
130
149
  tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'synthos-test-'));
131
- pagesFolder = path.join(tmpDir, 'synthos');
150
+ const pagesFolder = path.join(tmpDir, 'synthos');
132
151
  fallbackFolder = path.join(tmpDir, 'fallback');
133
152
  await fs.mkdir(pagesFolder, { recursive: true });
134
153
  await fs.mkdir(fallbackFolder, { recursive: true });
154
+ config = makeConfig(pagesFolder);
135
155
  });
136
156
 
137
157
  afterEach(async () => {
@@ -152,8 +172,8 @@ describe('page I/O', () => {
152
172
  pageVersion: 2,
153
173
  mode: 'locked' as const,
154
174
  };
155
- await savePageMetadata(pagesFolder, 'mypage', meta);
156
- const loaded = await loadPageMetadata(pagesFolder, 'mypage');
175
+ await savePageMetadata(config, 'mypage', meta);
176
+ const loaded = await loadPageMetadata(config, 'mypage');
157
177
  assert.ok(loaded);
158
178
  assert.strictEqual(loaded.title, 'Test Page');
159
179
  assert.deepStrictEqual(loaded.categories, ['Tools', 'Dev']);
@@ -163,17 +183,19 @@ describe('page I/O', () => {
163
183
  });
164
184
 
165
185
  it('returns undefined when no metadata file exists', async () => {
166
- const loaded = await loadPageMetadata(pagesFolder, 'nonexistent');
186
+ const loaded = await loadPageMetadata(config, 'nonexistent');
167
187
  assert.strictEqual(loaded, undefined);
168
188
  });
169
189
 
170
190
  it('falls back to fallbackFolder', async () => {
171
- // Write a fallback JSON file
191
+ // Write a fallback JSON file in folder-based structure
192
+ const fallbackPageDir = path.join(fallbackFolder, 'system-page');
193
+ await fs.mkdir(fallbackPageDir, { recursive: true });
172
194
  await fs.writeFile(
173
- path.join(fallbackFolder, 'system-page.json'),
195
+ path.join(fallbackPageDir, 'page.json'),
174
196
  JSON.stringify({ title: 'System', categories: ['System'], pinned: true, pageVersion: 2, mode: 'locked' }),
175
197
  );
176
- const loaded = await loadPageMetadata(pagesFolder, 'system-page', [fallbackFolder]);
198
+ const loaded = await loadPageMetadata(config, 'system-page', [fallbackFolder]);
177
199
  assert.ok(loaded);
178
200
  assert.strictEqual(loaded.title, 'System');
179
201
  });
@@ -184,89 +206,46 @@ describe('page I/O', () => {
184
206
  describe('savePageState / loadPageState', () => {
185
207
  it('roundtrips page HTML and creates metadata', async () => {
186
208
  const html = '<html><body>Hello</body></html>';
187
- await savePageState(pagesFolder, 'testpage', html, 'Test Title', ['Cat']);
188
- const loaded = await loadPageState(pagesFolder, 'testpage', true);
209
+ await savePageState(config, 'testpage', html, 'Test Title', ['Cat']);
210
+ const loaded = await loadPageState(config, 'testpage');
189
211
  assert.strictEqual(loaded, html);
190
212
 
191
213
  // Metadata should have been created
192
- const meta = await loadPageMetadata(pagesFolder, 'testpage');
214
+ const meta = await loadPageMetadata(config, 'testpage');
193
215
  assert.ok(meta);
194
216
  assert.strictEqual(meta.pageVersion, PAGE_VERSION);
195
217
  });
196
218
 
197
219
  it('returns undefined for non-existent page', async () => {
198
- const loaded = await loadPageState(pagesFolder, 'nope', false);
220
+ const loaded = await loadPageState(config, 'nope');
199
221
  assert.strictEqual(loaded, undefined);
200
222
  });
201
-
202
- it('with reset=true re-reads from disk', async () => {
203
- const html1 = '<html>v1</html>';
204
- const html2 = '<html>v2</html>';
205
- await savePageState(pagesFolder, 'resetpage', html1);
206
- // First load
207
- const first = await loadPageState(pagesFolder, 'resetpage', false);
208
- assert.strictEqual(first, html1);
209
-
210
- // Overwrite the file on disk directly
211
- const pageHtmlPath = path.join(pagesFolder, 'pages', 'resetpage', 'page.html');
212
- await fs.writeFile(pageHtmlPath, html2);
213
-
214
- // Without reset, should return cached version
215
- const cached = await loadPageState(pagesFolder, 'resetpage', false);
216
- assert.strictEqual(cached, html1);
217
-
218
- // With reset, should re-read
219
- const refreshed = await loadPageState(pagesFolder, 'resetpage', true);
220
- assert.strictEqual(refreshed, html2);
221
- });
222
- });
223
-
224
- // -- updatePageState -----------------------------------------------------
225
-
226
- describe('updatePageState', () => {
227
- it('updates in-memory cache', async () => {
228
- await savePageState(pagesFolder, 'cachepage', '<html>original</html>');
229
- await loadPageState(pagesFolder, 'cachepage', false);
230
-
231
- updatePageState('cachepage', '<html>updated</html>');
232
-
233
- // Loading without reset should return updated cache
234
- const loaded = await loadPageState(pagesFolder, 'cachepage', false);
235
- assert.strictEqual(loaded, '<html>updated</html>');
236
- });
237
223
  });
238
224
 
239
225
  // -- listPages -----------------------------------------------------------
240
226
 
241
227
  describe('listPages', () => {
242
228
  it('lists folder-based pages', async () => {
243
- await savePageState(pagesFolder, 'alpha', '<html>A</html>');
244
- await savePageState(pagesFolder, 'beta', '<html>B</html>');
245
- const pages = await listPages(pagesFolder, [fallbackFolder]);
229
+ await savePageState(config, 'alpha', '<html>A</html>');
230
+ await savePageState(config, 'beta', '<html>B</html>');
231
+ const pages = await listPages(config, [fallbackFolder]);
246
232
  const names = pages.map(p => p.name);
247
233
  assert.ok(names.includes('alpha'));
248
234
  assert.ok(names.includes('beta'));
249
235
  });
250
236
 
251
237
  it('lists legacy flat HTML files', async () => {
252
- await fs.writeFile(path.join(pagesFolder, 'legacy.html'), '<html>Legacy</html>');
253
- const pages = await listPages(pagesFolder, [fallbackFolder]);
238
+ await fs.writeFile(path.join(config.pagesFolder, 'legacy.html'), '<html>Legacy</html>');
239
+ const pages = await listPages(config, [fallbackFolder]);
254
240
  const legacy = pages.find(p => p.name === 'legacy');
255
241
  assert.ok(legacy);
256
242
  assert.strictEqual(legacy.pageVersion, 1);
257
243
  });
258
244
 
259
- it('lists fallback (required) pages', async () => {
260
- await fs.writeFile(path.join(fallbackFolder, 'builder.html'), '<html>Builder</html>');
261
- const pages = await listPages(pagesFolder, [fallbackFolder]);
262
- const builder = pages.find(p => p.name === 'builder');
263
- assert.ok(builder);
264
- });
265
-
266
245
  it('returns pages sorted alphabetically', async () => {
267
- await savePageState(pagesFolder, 'zebra', '<html></html>');
268
- await savePageState(pagesFolder, 'apple', '<html></html>');
269
- const pages = await listPages(pagesFolder, [fallbackFolder]);
246
+ await savePageState(config, 'zebra', '<html></html>');
247
+ await savePageState(config, 'apple', '<html></html>');
248
+ const pages = await listPages(config, [fallbackFolder]);
270
249
  const names = pages.map(p => p.name);
271
250
  const sortedNames = [...names].sort();
272
251
  assert.deepStrictEqual(names, sortedNames);
@@ -276,19 +255,18 @@ describe('page I/O', () => {
276
255
  // -- deletePage ----------------------------------------------------------
277
256
 
278
257
  describe('deletePage', () => {
279
- it('removes folder-based page and clears cache', async () => {
280
- await savePageState(pagesFolder, 'doomed', '<html>bye</html>');
281
- await loadPageState(pagesFolder, 'doomed', false);
258
+ it('removes folder-based page', async () => {
259
+ await savePageState(config, 'doomed', '<html>bye</html>');
282
260
 
283
- await deletePage(pagesFolder, 'doomed');
261
+ await deletePage(config, 'doomed');
284
262
 
285
263
  // Folder should be gone
286
- const folderExists = await fs.access(path.join(pagesFolder, 'pages', 'doomed'))
264
+ const folderExists = await fs.access(path.join(config.pagesFolder, 'pages', 'doomed'))
287
265
  .then(() => true).catch(() => false);
288
266
  assert.strictEqual(folderExists, false);
289
267
 
290
- // Cache should be cleared — loading should return undefined
291
- const loaded = await loadPageState(pagesFolder, 'doomed', true);
268
+ // Loading should return undefined
269
+ const loaded = await loadPageState(config, 'doomed');
292
270
  assert.strictEqual(loaded, undefined);
293
271
  });
294
272
  });
@@ -297,13 +275,13 @@ describe('page I/O', () => {
297
275
 
298
276
  describe('copyPage', () => {
299
277
  it('copies HTML and creates metadata with correct title/categories', async () => {
300
- await savePageState(pagesFolder, 'source', '<html>Source</html>');
301
- await copyPage(pagesFolder, 'source', 'target', 'Copied Page', ['Copy'], [fallbackFolder]);
278
+ await savePageState(config, 'source', '<html>Source</html>');
279
+ await copyPage(config, 'source', 'target', 'Copied Page', ['Copy'], [fallbackFolder]);
302
280
 
303
- const html = await loadPageState(pagesFolder, 'target', true);
281
+ const html = await loadPageState(config, 'target');
304
282
  assert.strictEqual(html, '<html>Source</html>');
305
283
 
306
- const meta = await loadPageMetadata(pagesFolder, 'target');
284
+ const meta = await loadPageMetadata(config, 'target');
307
285
  assert.ok(meta);
308
286
  assert.strictEqual(meta.title, 'Copied Page');
309
287
  assert.deepStrictEqual(meta.categories, ['Copy']);
@@ -312,17 +290,9 @@ describe('page I/O', () => {
312
290
 
313
291
  it('throws when source page does not exist', async () => {
314
292
  await assert.rejects(
315
- () => copyPage(pagesFolder, 'ghost', 'target', 'T', [], [fallbackFolder]),
293
+ () => copyPage(config, 'ghost', 'target', 'T', [], [fallbackFolder]),
316
294
  /Source page "ghost" not found/,
317
295
  );
318
296
  });
319
-
320
- it('copies from required pages folder as fallback', async () => {
321
- await fs.writeFile(path.join(fallbackFolder, 'system.html'), '<html>System</html>');
322
- await copyPage(pagesFolder, 'system', 'mycopy', 'My Copy', ['User'], [fallbackFolder]);
323
-
324
- const html = await loadPageState(pagesFolder, 'mycopy', true);
325
- assert.strictEqual(html, '<html>System</html>');
326
- });
327
297
  });
328
298
  });
@@ -39,7 +39,7 @@ describe('getProvider', () => {
39
39
 
40
40
  describe('detectProvider', () => {
41
41
  it('detects Anthropic from claude- prefix', () => {
42
- const p = detectProvider('claude-sonnet-4-5');
42
+ const p = detectProvider('claude-sonnet-4-6');
43
43
  assert.strictEqual(p?.name, 'Anthropic');
44
44
  });
45
45