synthos 0.10.1 → 0.11.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 (311) hide show
  1. package/README.md +5 -5
  2. package/default-pages/elevenlabs_effects_studio/chat-history.json +1 -0
  3. package/default-pages/elevenlabs_effects_studio/page.html +1345 -1363
  4. package/default-pages/elevenlabs_effects_studio/page.json +13 -11
  5. package/default-pages/elevenlabs_voice_studio/chat-history.json +1 -0
  6. package/default-pages/elevenlabs_voice_studio/page.html +782 -801
  7. package/default-pages/elevenlabs_voice_studio/page.json +13 -11
  8. package/default-pages/json_tools/chat-history.json +1 -0
  9. package/default-pages/json_tools/page.html +70 -90
  10. package/default-pages/json_tools/page.json +12 -10
  11. package/default-pages/my_notes/chat-history.json +1 -0
  12. package/default-pages/my_notes/page.html +115 -131
  13. package/default-pages/my_notes/page.json +14 -12
  14. package/default-pages/neon_asteroids/chat-history.json +1 -0
  15. package/default-pages/neon_asteroids/page.html +1777 -1803
  16. package/default-pages/neon_asteroids/page.json +14 -12
  17. package/default-pages/oregon_trail/chat-history.json +1 -0
  18. package/default-pages/oregon_trail/page.html +290 -307
  19. package/default-pages/oregon_trail/page.json +14 -12
  20. package/default-pages/solar_explorer/chat-history.json +1 -0
  21. package/default-pages/solar_explorer/page.html +1929 -1951
  22. package/default-pages/solar_explorer/page.json +14 -12
  23. package/default-pages/solar_tutorial/chat-history.json +1 -0
  24. package/default-pages/solar_tutorial/page.html +464 -478
  25. package/default-pages/solar_tutorial/page.json +12 -10
  26. package/default-pages/us_map/chat-history.json +1 -0
  27. package/default-pages/us_map/page.html +170 -193
  28. package/default-pages/us_map/page.json +14 -12
  29. package/default-pages/us_map/page.light.png +0 -0
  30. package/default-pages/us_map_1850/chat-history.json +1 -0
  31. package/default-pages/us_map_1850/page.html +302 -326
  32. package/default-pages/us_map_1850/page.json +14 -12
  33. package/default-pages/western_cities_1850/chat-history.json +1 -0
  34. package/default-pages/western_cities_1850/page.html +503 -527
  35. package/default-pages/western_cities_1850/page.json +14 -12
  36. package/default-themes/aurora-dawn.v3.css +15 -14
  37. package/default-themes/aurora-dusk.v3.css +26 -26
  38. package/default-themes/cosmos-dawn.v3.css +15 -14
  39. package/default-themes/cosmos-dusk.v3.css +26 -26
  40. package/default-themes/elemental-dawn.v3.css +200 -0
  41. package/default-themes/nebula-dawn.v3.css +15 -14
  42. package/default-themes/nebula-dusk.v3.css +24 -24
  43. package/default-themes/solar-flare-dawn.v3.css +15 -14
  44. package/default-themes/solar-flare-dusk.v3.css +26 -26
  45. package/dist/builders/anthropic.d.ts +26 -2
  46. package/dist/builders/anthropic.d.ts.map +1 -1
  47. package/dist/builders/anthropic.js +132 -31
  48. package/dist/builders/anthropic.js.map +1 -1
  49. package/dist/builders/claudecode.d.ts +13 -0
  50. package/dist/builders/claudecode.d.ts.map +1 -0
  51. package/dist/builders/claudecode.js +253 -0
  52. package/dist/builders/claudecode.js.map +1 -0
  53. package/dist/builders/index.d.ts +2 -1
  54. package/dist/builders/index.d.ts.map +1 -1
  55. package/dist/builders/index.js +8 -1
  56. package/dist/builders/index.js.map +1 -1
  57. package/dist/builders/openai.js +2 -1
  58. package/dist/builders/openai.js.map +1 -1
  59. package/dist/builders/types.d.ts +31 -7
  60. package/dist/builders/types.d.ts.map +1 -1
  61. package/dist/builders/types.js +60 -28
  62. package/dist/builders/types.js.map +1 -1
  63. package/dist/connectors/types.d.ts +8 -0
  64. package/dist/connectors/types.d.ts.map +1 -1
  65. package/dist/init.d.ts.map +1 -1
  66. package/dist/init.js +13 -6
  67. package/dist/init.js.map +1 -1
  68. package/dist/migrations.d.ts.map +1 -1
  69. package/dist/migrations.js +161 -14
  70. package/dist/migrations.js.map +1 -1
  71. package/dist/models/anthropic.d.ts +1 -0
  72. package/dist/models/anthropic.d.ts.map +1 -1
  73. package/dist/models/anthropic.js +129 -29
  74. package/dist/models/anthropic.js.map +1 -1
  75. package/dist/models/chainOfThought.d.ts.map +1 -1
  76. package/dist/models/chainOfThought.js +32 -19
  77. package/dist/models/chainOfThought.js.map +1 -1
  78. package/dist/models/index.d.ts +2 -2
  79. package/dist/models/index.d.ts.map +1 -1
  80. package/dist/models/index.js +2 -1
  81. package/dist/models/index.js.map +1 -1
  82. package/dist/models/providers.d.ts +1 -0
  83. package/dist/models/providers.d.ts.map +1 -1
  84. package/dist/models/providers.js +12 -4
  85. package/dist/models/providers.js.map +1 -1
  86. package/dist/models/types.d.ts +15 -1
  87. package/dist/models/types.d.ts.map +1 -1
  88. package/dist/models/types.js.map +1 -1
  89. package/dist/pages.d.ts +57 -8
  90. package/dist/pages.d.ts.map +1 -1
  91. package/dist/pages.js +258 -45
  92. package/dist/pages.js.map +1 -1
  93. package/dist/service/createCompletePrompt.d.ts.map +1 -1
  94. package/dist/service/createCompletePrompt.js +5 -0
  95. package/dist/service/createCompletePrompt.js.map +1 -1
  96. package/dist/service/mediaCache.d.ts +36 -0
  97. package/dist/service/mediaCache.d.ts.map +1 -0
  98. package/dist/service/mediaCache.js +182 -0
  99. package/dist/service/mediaCache.js.map +1 -0
  100. package/dist/service/pageValidator.d.ts +25 -0
  101. package/dist/service/pageValidator.d.ts.map +1 -0
  102. package/dist/service/pageValidator.js +315 -0
  103. package/dist/service/pageValidator.js.map +1 -0
  104. package/dist/service/server.d.ts.map +1 -1
  105. package/dist/service/server.js +4 -0
  106. package/dist/service/server.js.map +1 -1
  107. package/dist/service/sharedTableSchema.d.ts +73 -0
  108. package/dist/service/sharedTableSchema.d.ts.map +1 -0
  109. package/dist/service/sharedTableSchema.js +206 -0
  110. package/dist/service/sharedTableSchema.js.map +1 -0
  111. package/dist/service/transformPage.d.ts +49 -11
  112. package/dist/service/transformPage.d.ts.map +1 -1
  113. package/dist/service/transformPage.js +354 -241
  114. package/dist/service/transformPage.js.map +1 -1
  115. package/dist/service/useApiRoutes.d.ts.map +1 -1
  116. package/dist/service/useApiRoutes.js +285 -34
  117. package/dist/service/useApiRoutes.js.map +1 -1
  118. package/dist/service/useConnectorRoutes.d.ts.map +1 -1
  119. package/dist/service/useConnectorRoutes.js +170 -32
  120. package/dist/service/useConnectorRoutes.js.map +1 -1
  121. package/dist/service/useDataRoutes.d.ts.map +1 -1
  122. package/dist/service/useDataRoutes.js +59 -2
  123. package/dist/service/useDataRoutes.js.map +1 -1
  124. package/dist/service/useExtractRoutes.d.ts +4 -0
  125. package/dist/service/useExtractRoutes.d.ts.map +1 -0
  126. package/dist/service/useExtractRoutes.js +304 -0
  127. package/dist/service/useExtractRoutes.js.map +1 -0
  128. package/dist/service/usePageRoutes.d.ts +17 -0
  129. package/dist/service/usePageRoutes.d.ts.map +1 -1
  130. package/dist/service/usePageRoutes.js +1388 -483
  131. package/dist/service/usePageRoutes.js.map +1 -1
  132. package/dist/service/useSharedDataRoutes.d.ts.map +1 -1
  133. package/dist/service/useSharedDataRoutes.js +54 -2
  134. package/dist/service/useSharedDataRoutes.js.map +1 -1
  135. package/dist/settings.d.ts +27 -0
  136. package/dist/settings.d.ts.map +1 -1
  137. package/dist/settings.js +40 -1
  138. package/dist/settings.js.map +1 -1
  139. package/dist/themes.d.ts +0 -5
  140. package/dist/themes.d.ts.map +1 -1
  141. package/dist/themes.js +3 -95
  142. package/dist/themes.js.map +1 -1
  143. package/migration-rules/v2-to-v3.md +277 -119
  144. package/package.json +5 -1
  145. package/{default-pages/application → required-pages/_shell}/page.html +56 -42
  146. package/required-pages/_shell/page.json +14 -0
  147. package/required-pages/_starters/page.html +534 -0
  148. package/required-pages/_starters/page.json +12 -0
  149. package/required-pages/builder/page.html +353 -43
  150. package/required-pages/builder/page.json +12 -10
  151. package/required-pages/pages/page.html +697 -924
  152. package/required-pages/pages/page.json +12 -10
  153. package/required-pages/settings/page.html +1888 -1753
  154. package/required-pages/settings/page.json +12 -10
  155. package/required-pages/synthos_apis/page.html +834 -845
  156. package/required-pages/synthos_apis/page.json +12 -10
  157. package/required-pages/synthos_scripts/page.html +74 -88
  158. package/required-pages/synthos_scripts/page.json +12 -10
  159. package/scripts/append-instructions.py +90 -0
  160. package/scripts/audit-instructions.py +76 -0
  161. package/scripts/cleanup-shell-markup.mjs +112 -0
  162. package/service-connectors/buffer/connector.json +46 -0
  163. package/service-connectors/canva/connector.json +67 -0
  164. package/service-connectors/elevenlabs/connector.json +1 -1
  165. package/src/builders/anthropic.ts +150 -25
  166. package/src/builders/claudecode.ts +310 -0
  167. package/src/builders/index.ts +7 -1
  168. package/src/builders/openai.ts +2 -1
  169. package/src/builders/types.ts +93 -32
  170. package/src/connectors/types.ts +8 -0
  171. package/src/init.ts +13 -7
  172. package/src/migrations.ts +187 -16
  173. package/src/models/anthropic.ts +140 -30
  174. package/src/models/chainOfThought.ts +33 -18
  175. package/src/models/index.ts +2 -2
  176. package/src/models/providers.ts +10 -1
  177. package/src/models/types.ts +21 -1
  178. package/src/pages.ts +271 -35
  179. package/src/service/createCompletePrompt.ts +6 -0
  180. package/src/service/mediaCache.ts +206 -0
  181. package/src/service/pageValidator.ts +337 -0
  182. package/src/service/server.ts +4 -0
  183. package/src/service/sharedTableSchema.ts +236 -0
  184. package/src/service/transformPage.ts +370 -260
  185. package/src/service/useApiRoutes.ts +283 -32
  186. package/src/service/useConnectorRoutes.ts +189 -34
  187. package/src/service/useDataRoutes.ts +198 -116
  188. package/src/service/useExtractRoutes.ts +331 -0
  189. package/src/service/usePageRoutes.ts +1414 -394
  190. package/src/service/useSharedDataRoutes.ts +184 -109
  191. package/src/settings.ts +65 -0
  192. package/src/themes.ts +78 -180
  193. package/starters/blank_starter/chat-history.json +1 -0
  194. package/starters/blank_starter/page.dark.png +0 -0
  195. package/starters/blank_starter/page.html +47 -0
  196. package/starters/blank_starter/page.json +13 -0
  197. package/starters/blank_starter/page.light.png +0 -0
  198. package/starters/calculator_starter/chat-history.json +1 -0
  199. package/starters/calculator_starter/page.dark.png +0 -0
  200. package/starters/calculator_starter/page.html +232 -0
  201. package/starters/calculator_starter/page.json +13 -0
  202. package/starters/calculator_starter/page.light.png +0 -0
  203. package/starters/calendar_starter/chat-history.json +1 -0
  204. package/starters/calendar_starter/page.dark.png +0 -0
  205. package/starters/calendar_starter/page.html +495 -0
  206. package/starters/calendar_starter/page.json +13 -0
  207. package/starters/calendar_starter/page.light.png +0 -0
  208. package/starters/chat_starter/chat-history.json +1 -0
  209. package/starters/chat_starter/page.dark.png +0 -0
  210. package/starters/chat_starter/page.html +351 -0
  211. package/starters/chat_starter/page.json +13 -0
  212. package/starters/chat_starter/page.light.png +0 -0
  213. package/starters/checklist_starter/chat-history.json +1 -0
  214. package/starters/checklist_starter/page.dark.png +0 -0
  215. package/starters/checklist_starter/page.html +437 -0
  216. package/starters/checklist_starter/page.json +13 -0
  217. package/starters/checklist_starter/page.light.png +0 -0
  218. package/starters/dashboard_starter/chat-history.json +1 -0
  219. package/starters/dashboard_starter/page.dark.png +0 -0
  220. package/starters/dashboard_starter/page.html +195 -0
  221. package/starters/dashboard_starter/page.json +13 -0
  222. package/starters/dashboard_starter/page.light.png +0 -0
  223. package/starters/form_starter/chat-history.json +1 -0
  224. package/starters/form_starter/page.dark.png +0 -0
  225. package/starters/form_starter/page.html +313 -0
  226. package/starters/form_starter/page.json +13 -0
  227. package/starters/form_starter/page.light.png +0 -0
  228. package/starters/gallery_starter/chat-history.json +1 -0
  229. package/starters/gallery_starter/page.dark.png +0 -0
  230. package/starters/gallery_starter/page.html +418 -0
  231. package/starters/gallery_starter/page.json +13 -0
  232. package/starters/gallery_starter/page.light.png +0 -0
  233. package/starters/generator_starter/chat-history.json +1 -0
  234. package/starters/generator_starter/page.dark.png +0 -0
  235. package/starters/generator_starter/page.html +261 -0
  236. package/starters/generator_starter/page.json +13 -0
  237. package/starters/generator_starter/page.light.png +0 -0
  238. package/starters/index.html +538 -0
  239. package/starters/kanban_starter/chat-history.json +1 -0
  240. package/starters/kanban_starter/page.dark.png +0 -0
  241. package/starters/kanban_starter/page.html +432 -0
  242. package/starters/kanban_starter/page.json +13 -0
  243. package/starters/kanban_starter/page.light.png +0 -0
  244. package/starters/presentation_builder/chat-history.json +1 -0
  245. package/starters/presentation_builder/page.dark.png +0 -0
  246. package/starters/presentation_builder/page.html +970 -0
  247. package/starters/presentation_builder/page.json +15 -0
  248. package/starters/presentation_builder/page.light.png +0 -0
  249. package/starters/presentation_builder/presentation_voice/voice_config.json +9 -0
  250. package/starters/pulse_starter/chat-history.json +1 -0
  251. package/starters/pulse_starter/page.dark.png +0 -0
  252. package/starters/pulse_starter/page.html +698 -0
  253. package/starters/pulse_starter/page.json +13 -0
  254. package/starters/pulse_starter/page.light.png +0 -0
  255. package/starters/quiz_starter/chat-history.json +1 -0
  256. package/starters/quiz_starter/page.dark.png +0 -0
  257. package/starters/quiz_starter/page.html +292 -0
  258. package/starters/quiz_starter/page.json +13 -0
  259. package/starters/quiz_starter/page.light.png +0 -0
  260. package/starters/reference_starter/chat-history.json +1 -0
  261. package/starters/reference_starter/page.dark.png +0 -0
  262. package/starters/reference_starter/page.html +250 -0
  263. package/starters/reference_starter/page.json +13 -0
  264. package/starters/reference_starter/page.light.png +0 -0
  265. package/starters/retro_game_starter/chat-history.json +1 -0
  266. package/starters/retro_game_starter/page.dark.png +0 -0
  267. package/{default-pages → starters}/retro_game_starter/page.html +1281 -1308
  268. package/starters/retro_game_starter/page.json +15 -0
  269. package/starters/retro_game_starter/page.light.png +0 -0
  270. package/starters/roster_starter/chat-history.json +1 -0
  271. package/starters/roster_starter/page.dark.png +0 -0
  272. package/starters/roster_starter/page.html +600 -0
  273. package/starters/roster_starter/page.json +13 -0
  274. package/starters/roster_starter/page.light.png +0 -0
  275. package/starters/server.js +182 -0
  276. package/starters/start.cmd +1 -0
  277. package/starters/timeline_starter/chat-history.json +1 -0
  278. package/starters/timeline_starter/page.dark.png +0 -0
  279. package/starters/timeline_starter/page.html +446 -0
  280. package/starters/timeline_starter/page.json +13 -0
  281. package/starters/timeline_starter/page.light.png +0 -0
  282. package/starters/tutorial_starter/chat-history.json +1 -0
  283. package/starters/tutorial_starter/page.dark.png +0 -0
  284. package/starters/tutorial_starter/page.html +283 -0
  285. package/starters/tutorial_starter/page.json +13 -0
  286. package/starters/tutorial_starter/page.light.png +0 -0
  287. package/static-files/agent.v3.js +122 -0
  288. package/static-files/connector.v3.js +48 -0
  289. package/static-files/extract.v3.js +188 -0
  290. package/static-files/helpers.v3.js +50 -6
  291. package/static-files/page-bridge.js +114 -0
  292. package/static-files/page.v3.js +1292 -1290
  293. package/static-files/script.v3.js +32 -0
  294. package/static-files/server.v3.js +89 -0
  295. package/static-files/shell-bridge.v3.js +174 -0
  296. package/static-files/shell-modals.v3.js +521 -0
  297. package/static-files/{shell.css → shell.v3.css} +271 -22
  298. package/static-files/shell.v3.js +1865 -0
  299. package/static-files/storage.v3.js +176 -0
  300. package/tests/anthropic.spec.ts +42 -7
  301. package/tests/builders.spec.ts +70 -2
  302. package/tests/pageValidator.spec.ts +548 -0
  303. package/tests/profiles.spec.ts +122 -0
  304. package/tests/sharedTableSchema.spec.ts +242 -0
  305. package/tests/transformPage.spec.ts +62 -81
  306. package/default-pages/application/page.json +0 -10
  307. package/default-pages/retro_game_starter/page.json +0 -12
  308. package/default-pages/sidebar_page/page.html +0 -51
  309. package/default-pages/sidebar_page/page.json +0 -10
  310. package/default-pages/two-panel_page/page.html +0 -68
  311. package/default-pages/two-panel_page/page.json +0 -10
package/src/pages.ts CHANGED
@@ -1,6 +1,13 @@
1
1
  import {checkIfExists, listFolders, loadFile} from './files';
2
2
  import path from 'path';
3
3
  import { SynthOSConfig } from './init';
4
+ import { SectionMode } from './builders/types';
5
+
6
+ const VALID_SECTION_MODES: ReadonlySet<SectionMode> = new Set<SectionMode>([
7
+ 'always-full',
8
+ 'classifier-decides',
9
+ 'always-omit-when-empty',
10
+ ]);
4
11
 
5
12
  /**
6
13
  * Derive the list of required page names by scanning *.html files
@@ -28,6 +35,7 @@ export const PAGE_VERSION = 3;
28
35
  export interface PageInfo {
29
36
  name: string;
30
37
  title: string;
38
+ description: string; // one-line summary of what the page does
31
39
  categories: string[];
32
40
  pinned: boolean;
33
41
  showInAll: boolean; // true = visible in "All" filter, false = only in category filters
@@ -35,6 +43,14 @@ export interface PageInfo {
35
43
  lastModified: string; // ISO 8601, empty string if unknown
36
44
  pageVersion: number; // integer, 0 = pre-versioning
37
45
  mode: 'unlocked' | 'locked';
46
+ greeting: string; // initial greeting shown when page loads
47
+ firstRunGreeting: string; // greeting shown when ?firstRun=true (e.g. builder first launch)
48
+ /**
49
+ * Per-page overrides for builder context-section modes. Maps a section
50
+ * title (e.g. "<FLUENTLM_COMPONENTS>") to a SectionMode that overrides
51
+ * the producer-declared default. Optional and rarely needed.
52
+ */
53
+ sectionModes?: Record<string, SectionMode>;
38
54
  }
39
55
 
40
56
  export type PageMetadata = Omit<PageInfo, 'name'>;
@@ -75,8 +91,9 @@ export async function loadPageMetadata(config: SynthOSConfig, name: string, fall
75
91
  }
76
92
 
77
93
  export function parseMetadata(parsed: Record<string, unknown>): PageMetadata {
78
- return {
94
+ const result: PageMetadata = {
79
95
  title: typeof parsed.title === 'string' ? parsed.title : '',
96
+ description: typeof parsed.description === 'string' ? parsed.description : '',
80
97
  categories: Array.isArray(parsed.categories) ? parsed.categories : [],
81
98
  pinned: typeof parsed.pinned === 'boolean' ? parsed.pinned : false,
82
99
  showInAll: typeof parsed.showInAll === 'boolean' ? parsed.showInAll : true,
@@ -85,7 +102,19 @@ export function parseMetadata(parsed: Record<string, unknown>): PageMetadata {
85
102
  pageVersion: typeof parsed.pageVersion === 'number' ? parsed.pageVersion
86
103
  : typeof parsed.uxVersion === 'number' ? parsed.uxVersion : 0,
87
104
  mode: parsed.mode === 'locked' ? 'locked' : 'unlocked',
105
+ greeting: typeof parsed.greeting === 'string' ? parsed.greeting : '',
106
+ firstRunGreeting: typeof parsed.firstRunGreeting === 'string' ? parsed.firstRunGreeting : '',
88
107
  };
108
+ if (parsed.sectionModes && typeof parsed.sectionModes === 'object' && !Array.isArray(parsed.sectionModes)) {
109
+ const out: Record<string, SectionMode> = {};
110
+ for (const [title, mode] of Object.entries(parsed.sectionModes as Record<string, unknown>)) {
111
+ if (typeof mode === 'string' && VALID_SECTION_MODES.has(mode as SectionMode)) {
112
+ out[title] = mode as SectionMode;
113
+ }
114
+ }
115
+ if (Object.keys(out).length > 0) result.sectionModes = out;
116
+ }
117
+ return result;
89
118
  }
90
119
 
91
120
  export async function savePageMetadata(config: SynthOSConfig, name: string, metadata: PageMetadata): Promise<void> {
@@ -97,6 +126,7 @@ export async function savePageMetadata(config: SynthOSConfig, name: string, meta
97
126
 
98
127
  const DEFAULT_METADATA: PageMetadata = {
99
128
  title: '',
129
+ description: '',
100
130
  categories: [],
101
131
  pinned: false,
102
132
  showInAll: true,
@@ -104,6 +134,8 @@ const DEFAULT_METADATA: PageMetadata = {
104
134
  lastModified: '',
105
135
  pageVersion: 0,
106
136
  mode: 'unlocked',
137
+ greeting: '',
138
+ firstRunGreeting: '',
107
139
  };
108
140
 
109
141
  export async function listPages(config: SynthOSConfig, fallbackPagesFolders: string[]): Promise<PageInfo[]> {
@@ -142,6 +174,7 @@ export async function listPages(config: SynthOSConfig, fallbackPagesFolders: str
142
174
  pageMap.set(name, {
143
175
  name,
144
176
  title,
177
+ description: '',
145
178
  categories,
146
179
  pinned: false,
147
180
  showInAll: true,
@@ -149,6 +182,8 @@ export async function listPages(config: SynthOSConfig, fallbackPagesFolders: str
149
182
  lastModified: '',
150
183
  pageVersion: 1,
151
184
  mode: 'unlocked',
185
+ greeting: '',
186
+ firstRunGreeting: '',
152
187
  });
153
188
  }
154
189
  }
@@ -165,6 +200,7 @@ export async function listPages(config: SynthOSConfig, fallbackPagesFolders: str
165
200
  pageMap.set(name, {
166
201
  name,
167
202
  title: metadata?.title ?? '',
203
+ description: metadata?.description ?? '',
168
204
  categories: metadata?.categories ?? ['System'],
169
205
  pinned: metadata?.pinned ?? true,
170
206
  showInAll: metadata?.showInAll ?? true,
@@ -172,6 +208,8 @@ export async function listPages(config: SynthOSConfig, fallbackPagesFolders: str
172
208
  lastModified: metadata?.lastModified ?? '',
173
209
  pageVersion: metadata?.pageVersion ?? 0,
174
210
  mode: metadata?.mode ?? 'unlocked',
211
+ greeting: metadata?.greeting ?? '',
212
+ firstRunGreeting: metadata?.firstRunGreeting ?? '',
175
213
  });
176
214
  }
177
215
  }
@@ -187,11 +225,14 @@ export async function loadPageState(config: SynthOSConfig, name: string): Promis
187
225
  const pagesFolder = config.pagesFolder;
188
226
  const sp = config.storageProvider;
189
227
 
190
- // Check for working-state version files first
191
- const latestVersion = await getLatestVersion(config, name);
192
- if (latestVersion > 0) {
193
- const versionHtml = await loadPageVersion(config, name, latestVersion);
194
- if (versionHtml) return versionHtml;
228
+ // Migrate any legacy v-scheme files on first read, then look for the
229
+ // highest-numbered page.c<N>.html as the active working state.
230
+ await migrateLegacyChangeFiles(config, name);
231
+
232
+ const latestChange = await getLatestChangeHtmlNumber(config, name);
233
+ if (latestChange > 0) {
234
+ const html = await loadChangeHtml(config, name, latestChange);
235
+ if (html) return html;
195
236
  }
196
237
 
197
238
  // Fall back to saved baseline
@@ -225,6 +266,7 @@ export async function savePageState(config: SynthOSConfig, name: string, content
225
266
  const now = new Date().toISOString();
226
267
  const metadata: PageMetadata = {
227
268
  title: title ?? '',
269
+ description: '',
228
270
  categories: categories ?? [],
229
271
  pinned: false,
230
272
  showInAll: true,
@@ -232,6 +274,8 @@ export async function savePageState(config: SynthOSConfig, name: string, content
232
274
  lastModified: now,
233
275
  pageVersion: PAGE_VERSION,
234
276
  mode: 'unlocked',
277
+ greeting: '',
278
+ firstRunGreeting: '',
235
279
  };
236
280
  await sp.saveFile(metadataPath, JSON.stringify(metadata, null, 4));
237
281
  }
@@ -256,61 +300,250 @@ export async function deletePage(config: SynthOSConfig, name: string): Promise<v
256
300
  }
257
301
 
258
302
  // ---------------------------------------------------------------------------
259
- // Page versioningper-edit version snapshots for undo support
303
+ // Per-change journaldurable HTML + JSON pairs per user turn
260
304
  // ---------------------------------------------------------------------------
305
+ //
306
+ // Every user message creates a new page.c<N>.json (user msg first, assistant
307
+ // reply or error appended). On a successful transform, page.c<N>.html is also
308
+ // written. Errors only append to the current JSON — no HTML is written — so
309
+ // the HTML and JSON sequence numbers may diverge (gaps allowed).
310
+ //
311
+ // Active page = highest N where page.c<N>.html exists (else baseline).
312
+ // Save collapses the chain back to page.html and deletes all c-files.
313
+
314
+ export interface ChatMessage {
315
+ role: 'user' | 'assistant';
316
+ content: string;
317
+ }
318
+
319
+ const CHANGE_HTML_RE = /^page\.c(\d+)\.html$/;
320
+ const CHANGE_JSON_RE = /^page\.c(\d+)\.json$/;
321
+
322
+ function pageFolderPath(config: SynthOSConfig, name: string): string {
323
+ return path.join(config.pagesFolder, 'pages', name);
324
+ }
261
325
 
262
326
  /**
263
- * Save a version snapshot: <pagesFolder>/pages/<name>/page.v<version>.html
327
+ * Save the HTML produced by change <n>: page.c<n>.html
264
328
  */
265
- export async function savePageVersion(config: SynthOSConfig, name: string, version: number, html: string): Promise<void> {
329
+ export async function saveChangeHtml(config: SynthOSConfig, name: string, n: number, html: string): Promise<void> {
266
330
  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);
331
+ const folder = pageFolderPath(config, name);
332
+ await sp.ensureFolderExists(folder);
333
+ await sp.saveFile(path.join(folder, `page.c${n}.html`), html);
270
334
  }
271
335
 
272
336
  /**
273
- * Load a version snapshot (returns undefined if the file doesn't exist).
337
+ * Load the HTML produced by change <n> (undefined if missing).
274
338
  */
275
- export async function loadPageVersion(config: SynthOSConfig, name: string, version: number): Promise<string | undefined> {
339
+ export async function loadChangeHtml(config: SynthOSConfig, name: string, n: number): Promise<string | undefined> {
276
340
  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);
341
+ const file = path.join(pageFolderPath(config, name), `page.c${n}.html`);
342
+ if (!await sp.checkIfExists(file)) return undefined;
343
+ return sp.loadFile(file);
280
344
  }
281
345
 
282
346
  /**
283
- * Scan page.v*.html files and return the highest version number (0 if none).
347
+ * Highest N such that page.c<N>.html exists (0 if none).
284
348
  */
285
- export async function getLatestVersion(config: SynthOSConfig, name: string): Promise<number> {
349
+ export async function getLatestChangeHtmlNumber(config: SynthOSConfig, name: string): Promise<number> {
350
+ const nums = await listChangeNumbers(config, name, CHANGE_HTML_RE);
351
+ return nums.length > 0 ? nums[nums.length - 1] : 0;
352
+ }
353
+
354
+ /**
355
+ * Highest N across both .html and .json files (0 if none) — used when
356
+ * assigning the next change number so errors (JSON only) still advance.
357
+ */
358
+ export async function getLatestChangeNumber(config: SynthOSConfig, name: string): Promise<number> {
359
+ const htmlNums = await listChangeNumbers(config, name, CHANGE_HTML_RE);
360
+ const jsonNums = await listChangeNumbers(config, name, CHANGE_JSON_RE);
361
+ const all = [...htmlNums, ...jsonNums];
362
+ return all.length > 0 ? Math.max(...all) : 0;
363
+ }
364
+
365
+ /**
366
+ * List all change numbers matching a pattern, sorted ascending.
367
+ */
368
+ async function listChangeNumbers(config: SynthOSConfig, name: string, pattern: RegExp): Promise<number[]> {
286
369
  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);
290
- let max = 0;
370
+ const folder = pageFolderPath(config, name);
371
+ if (!await sp.checkIfExists(folder)) return [];
372
+ const files = await sp.listFiles(folder);
373
+ const nums: number[] = [];
291
374
  for (const file of files) {
292
- const match = file.match(/^page\.v(\d+)\.html$/);
293
- if (match) {
294
- const v = parseInt(match[1], 10);
295
- if (v > max) max = v;
375
+ const match = file.match(pattern);
376
+ if (match) nums.push(parseInt(match[1], 10));
377
+ }
378
+ nums.sort((a, b) => a - b);
379
+ return nums;
380
+ }
381
+
382
+ /**
383
+ * Save the messages list for change <n>: page.c<n>.json
384
+ */
385
+ export async function saveChangeMessages(config: SynthOSConfig, name: string, n: number, messages: ChatMessage[]): Promise<void> {
386
+ const sp = config.storageProvider;
387
+ const folder = pageFolderPath(config, name);
388
+ await sp.ensureFolderExists(folder);
389
+ await sp.saveFile(path.join(folder, `page.c${n}.json`), JSON.stringify({ messages }, null, 2));
390
+ }
391
+
392
+ /**
393
+ * Load the messages list for change <n>. Returns [] if the file doesn't
394
+ * exist or is corrupted.
395
+ */
396
+ export async function loadChangeMessages(config: SynthOSConfig, name: string, n: number): Promise<ChatMessage[]> {
397
+ const sp = config.storageProvider;
398
+ const file = path.join(pageFolderPath(config, name), `page.c${n}.json`);
399
+ if (!await sp.checkIfExists(file)) return [];
400
+ try {
401
+ const raw = await sp.loadFile(file);
402
+ const parsed = JSON.parse(raw);
403
+ if (parsed && Array.isArray(parsed.messages)) return parsed.messages;
404
+ // Legacy shape: top-level array
405
+ if (Array.isArray(parsed)) return parsed;
406
+ } catch {
407
+ // corrupted — fall through
408
+ }
409
+ return [];
410
+ }
411
+
412
+ /**
413
+ * Append a single message to the current page.c<n>.json (creating it if
414
+ * missing). Used for errors — always targets the highest existing N, or
415
+ * creates c1 if none exist.
416
+ */
417
+ export async function appendMessageToCurrentChange(config: SynthOSConfig, name: string, message: ChatMessage): Promise<number> {
418
+ let n = await getLatestChangeNumber(config, name);
419
+ if (n === 0) n = 1;
420
+ const existing = await loadChangeMessages(config, name, n);
421
+ existing.push(message);
422
+ await saveChangeMessages(config, name, n, existing);
423
+ return n;
424
+ }
425
+
426
+ /**
427
+ * Flat concat of all page.c<n>.json messages in numeric order.
428
+ */
429
+ export async function loadAllChangeMessages(config: SynthOSConfig, name: string): Promise<ChatMessage[]> {
430
+ const nums = await listChangeNumbers(config, name, CHANGE_JSON_RE);
431
+ const out: ChatMessage[] = [];
432
+ for (const n of nums) {
433
+ const msgs = await loadChangeMessages(config, name, n);
434
+ out.push(...msgs);
435
+ }
436
+ return out;
437
+ }
438
+
439
+ /**
440
+ * Delete a single change's html + json pair (either may be missing).
441
+ */
442
+ export async function deleteChange(config: SynthOSConfig, name: string, n: number): Promise<void> {
443
+ const sp = config.storageProvider;
444
+ const folder = pageFolderPath(config, name);
445
+ const html = path.join(folder, `page.c${n}.html`);
446
+ const json = path.join(folder, `page.c${n}.json`);
447
+ if (await sp.checkIfExists(html)) await sp.deleteFile(html);
448
+ if (await sp.checkIfExists(json)) await sp.deleteFile(json);
449
+ }
450
+
451
+ /**
452
+ * Delete all page.c*.html and page.c*.json files for a page.
453
+ */
454
+ export async function clearChanges(config: SynthOSConfig, name: string): Promise<void> {
455
+ const sp = config.storageProvider;
456
+ const folder = pageFolderPath(config, name);
457
+ if (!await sp.checkIfExists(folder)) return;
458
+ const files = await sp.listFiles(folder);
459
+ for (const file of files) {
460
+ if (CHANGE_HTML_RE.test(file) || CHANGE_JSON_RE.test(file)) {
461
+ await sp.deleteFile(path.join(folder, file));
296
462
  }
297
463
  }
298
- return max;
299
464
  }
300
465
 
301
466
  /**
302
- * Delete all page.v*.html version files for a page.
467
+ * One-shot lazy migration: on first touch of a page, rename any
468
+ * legacy page.v<N>.html to page.c<N>.html and split any legacy
469
+ * chat-history.json into page.c<N>.json files grouped by user turn.
470
+ * Idempotent — does nothing if no legacy files remain.
303
471
  */
304
- export async function clearVersions(config: SynthOSConfig, name: string): Promise<void> {
472
+ export async function migrateLegacyChangeFiles(config: SynthOSConfig, name: string): Promise<void> {
305
473
  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);
474
+ const folder = pageFolderPath(config, name);
475
+ if (!await sp.checkIfExists(folder)) return;
476
+
477
+ const files = await sp.listFiles(folder);
478
+
479
+ // 1. Rename page.v<N>.html → page.c<N>.html if target doesn't already exist.
309
480
  for (const file of files) {
310
- if (/^page\.v\d+\.html$/.test(file)) {
311
- await sp.deleteFile(path.join(pageFolder, file));
481
+ const match = file.match(/^page\.v(\d+)\.html$/);
482
+ if (!match) continue;
483
+ const n = match[1];
484
+ const src = path.join(folder, file);
485
+ const dst = path.join(folder, `page.c${n}.html`);
486
+ if (await sp.checkIfExists(dst)) {
487
+ // c-file already present — drop the legacy v copy to avoid dual state
488
+ await sp.deleteFile(src);
489
+ continue;
312
490
  }
491
+ const html = await sp.loadFile(src);
492
+ await sp.saveFile(dst, html);
493
+ await sp.deleteFile(src);
313
494
  }
495
+
496
+ // 2. Split legacy chat-history.json into per-turn page.c<N>.json files,
497
+ // but only if no page.c*.json files already exist (avoid clobbering).
498
+ const chatHistoryPath = path.join(folder, 'chat-history.json');
499
+ if (!await sp.checkIfExists(chatHistoryPath)) return;
500
+
501
+ const existingJson = files.some(f => CHANGE_JSON_RE.test(f));
502
+ if (existingJson) {
503
+ // Already migrated or mid-flight — drop the legacy file to converge.
504
+ await sp.deleteFile(chatHistoryPath);
505
+ return;
506
+ }
507
+
508
+ let parsed: unknown;
509
+ try {
510
+ parsed = JSON.parse(await sp.loadFile(chatHistoryPath));
511
+ } catch {
512
+ await sp.deleteFile(chatHistoryPath);
513
+ return;
514
+ }
515
+ if (!Array.isArray(parsed)) {
516
+ await sp.deleteFile(chatHistoryPath);
517
+ return;
518
+ }
519
+
520
+ // Group messages by user turn. The legacy file starts with a greeting
521
+ // (assistant) followed by user/assistant pairs. Drop the leading greeting
522
+ // (shell rebuilds it from metadata), then chunk on every user message.
523
+ const msgs = parsed as ChatMessage[];
524
+ const groups: ChatMessage[][] = [];
525
+ let current: ChatMessage[] = [];
526
+ let sawFirstUser = false;
527
+ for (const m of msgs) {
528
+ if (!m || typeof m.role !== 'string' || typeof m.content !== 'string') continue;
529
+ if (m.role === 'user') {
530
+ if (current.length > 0) groups.push(current);
531
+ current = [m];
532
+ sawFirstUser = true;
533
+ } else if (sawFirstUser) {
534
+ current.push(m);
535
+ }
536
+ // assistant messages before the first user msg (greeting) are skipped
537
+ }
538
+ if (current.length > 0) groups.push(current);
539
+
540
+ for (let i = 0; i < groups.length; i++) {
541
+ const n = i + 1;
542
+ const target = path.join(folder, `page.c${n}.json`);
543
+ await sp.saveFile(target, JSON.stringify({ messages: groups[i] }, null, 2));
544
+ }
545
+
546
+ await sp.deleteFile(chatHistoryPath);
314
547
  }
315
548
 
316
549
  export interface CopyPageOptions {
@@ -375,6 +608,7 @@ export async function copyPage(
375
608
  const now = new Date().toISOString();
376
609
  const metadata: PageMetadata = {
377
610
  title,
611
+ description: '',
378
612
  categories,
379
613
  pinned: false,
380
614
  showInAll: true,
@@ -382,6 +616,8 @@ export async function copyPage(
382
616
  lastModified: now,
383
617
  pageVersion: PAGE_VERSION,
384
618
  mode: 'unlocked',
619
+ greeting: '',
620
+ firstRunGreeting: '',
385
621
  };
386
622
  await savePageMetadata(config, targetName, metadata);
387
623
 
@@ -8,6 +8,12 @@ export async function createCompletePrompt(config: SynthOSConfig, use: 'builder'
8
8
  const settings = await loadSettings(config);
9
9
  const entry = getModelEntry(settings, use);
10
10
 
11
+ // ClaudeCode provider uses its own CLI subprocess — no API key or completePrompt needed.
12
+ // Return a no-op stub; the ClaudeCode builder ignores it entirely.
13
+ if (entry.provider === 'ClaudeCode') {
14
+ return async () => ({ completed: false, error: new Error('ClaudeCode provider does not use completePrompt') });
15
+ }
16
+
11
17
  if (!entry.configuration.apiKey) {
12
18
  throw new Error('API key not configured');
13
19
  }
@@ -0,0 +1,206 @@
1
+ import { createHash } from 'crypto';
2
+ import path from 'path';
3
+ import { StorageProvider } from '../storage/StorageProvider';
4
+
5
+ export interface MediaCacheOptions {
6
+ storage: StorageProvider;
7
+ cacheRoot: string;
8
+ }
9
+
10
+ export interface CacheEntry {
11
+ hit: true;
12
+ buffer: Buffer;
13
+ contentType: string;
14
+ }
15
+
16
+ export interface CacheMiss {
17
+ hit: false;
18
+ }
19
+
20
+ export type CacheLookup = CacheEntry | CacheMiss;
21
+
22
+ export interface CacheStats {
23
+ images: { count: number; totalBytes: number };
24
+ audio: { count: number; totalBytes: number };
25
+ }
26
+
27
+ function hashKey(key: string): string {
28
+ return createHash('sha256').update(key).digest('hex');
29
+ }
30
+
31
+ const MAX_ENTRIES = 500;
32
+ const CATEGORIES = ['images', 'audio'] as const;
33
+
34
+ export function createMediaCache(options: MediaCacheOptions) {
35
+ const { storage, cacheRoot } = options;
36
+
37
+ function categoryDir(category: string): string {
38
+ return path.join(cacheRoot, category);
39
+ }
40
+
41
+ async function ensureCategory(category: string): Promise<void> {
42
+ await storage.ensureFolderExists(categoryDir(category));
43
+ }
44
+
45
+ async function get(category: string, key: string): Promise<CacheLookup> {
46
+ const hash = hashKey(key);
47
+ const dir = categoryDir(category);
48
+ const metaPath = path.join(dir, `${hash}.meta.json`);
49
+
50
+ if (!(await storage.checkIfExists(metaPath))) {
51
+ return { hit: false };
52
+ }
53
+
54
+ const meta = JSON.parse(await storage.loadFile(metaPath));
55
+ const dataPath = path.join(dir, `${hash}${meta.extension}`);
56
+ try {
57
+ const buffer = await storage.loadBuffer(dataPath);
58
+
59
+ // Update last access time for eviction tracking
60
+ meta.lastAccessedAt = new Date().toISOString();
61
+ await storage.saveFile(metaPath, JSON.stringify(meta, null, 2));
62
+
63
+ return { hit: true, buffer, contentType: meta.contentType };
64
+ } catch {
65
+ return { hit: false };
66
+ }
67
+ }
68
+
69
+ async function put(
70
+ category: string,
71
+ key: string,
72
+ data: Buffer,
73
+ contentType: string,
74
+ meta: Record<string, unknown>
75
+ ): Promise<void> {
76
+ await ensureCategory(category);
77
+ const hash = hashKey(key);
78
+ const dir = categoryDir(category);
79
+ const extension = extensionForContentType(contentType);
80
+ const dataPath = path.join(dir, `${hash}${extension}`);
81
+ const metaPath = path.join(dir, `${hash}.meta.json`);
82
+
83
+ const now = new Date().toISOString();
84
+ await storage.saveBuffer(dataPath, data);
85
+ await storage.saveFile(metaPath, JSON.stringify({
86
+ ...meta,
87
+ contentType,
88
+ extension,
89
+ createdAt: now,
90
+ lastAccessedAt: now,
91
+ }, null, 2));
92
+
93
+ // Run eviction after writing the new entry
94
+ await evict();
95
+ }
96
+
97
+ async function evict(): Promise<void> {
98
+ // Collect all entries across categories
99
+ const entries: { category: string; hash: string; lastAccessedAt: string; size: number; metaPath: string; extension: string }[] = [];
100
+
101
+ for (const category of CATEGORIES) {
102
+ const dir = categoryDir(category);
103
+ if (!(await storage.checkIfExists(dir))) continue;
104
+
105
+ const files = await storage.listFiles(dir);
106
+ for (const file of files) {
107
+ if (!file.endsWith('.meta.json')) continue;
108
+ const metaPath = path.join(dir, file);
109
+ try {
110
+ const meta = JSON.parse(await storage.loadFile(metaPath));
111
+ const hash = file.replace('.meta.json', '');
112
+ const dataPath = path.join(dir, `${hash}${meta.extension}`);
113
+ let size = 0;
114
+ try {
115
+ const info = await storage.stat(dataPath);
116
+ size = info.size;
117
+ } catch { /* data file missing, size stays 0 */ }
118
+ entries.push({
119
+ category,
120
+ hash,
121
+ lastAccessedAt: meta.lastAccessedAt || meta.createdAt || '1970-01-01T00:00:00.000Z',
122
+ size,
123
+ metaPath,
124
+ extension: meta.extension,
125
+ });
126
+ } catch { /* skip unreadable meta */ }
127
+ }
128
+ }
129
+
130
+ if (entries.length <= MAX_ENTRIES) return;
131
+
132
+ // Sort by lastAccessedAt descending — most recent first
133
+ entries.sort((a, b) => b.lastAccessedAt.localeCompare(a.lastAccessedAt));
134
+
135
+ // Evict everything beyond the first 500
136
+ const toEvict = entries.slice(MAX_ENTRIES);
137
+ for (const entry of toEvict) {
138
+ const dir = categoryDir(entry.category);
139
+ const dataPath = path.join(dir, `${entry.hash}${entry.extension}`);
140
+ try { await storage.deleteFile(dataPath); } catch { /* ignore */ }
141
+ try { await storage.deleteFile(entry.metaPath); } catch { /* ignore */ }
142
+ }
143
+ }
144
+
145
+ async function invalidate(category: string, key: string): Promise<void> {
146
+ const hash = hashKey(key);
147
+ const dir = categoryDir(category);
148
+ const metaPath = path.join(dir, `${hash}.meta.json`);
149
+
150
+ if (await storage.checkIfExists(metaPath)) {
151
+ const meta = JSON.parse(await storage.loadFile(metaPath));
152
+ const dataPath = path.join(dir, `${hash}${meta.extension}`);
153
+ try { await storage.deleteFile(dataPath); } catch { /* ignore */ }
154
+ try { await storage.deleteFile(metaPath); } catch { /* ignore */ }
155
+ }
156
+ }
157
+
158
+ async function clearCategory(category: string): Promise<void> {
159
+ const dir = categoryDir(category);
160
+ if (await storage.checkIfExists(dir)) {
161
+ await storage.deleteFolder(dir);
162
+ }
163
+ }
164
+
165
+ async function clearAll(): Promise<void> {
166
+ await clearCategory('images');
167
+ await clearCategory('audio');
168
+ }
169
+
170
+ async function stats(): Promise<CacheStats> {
171
+ const result: CacheStats = {
172
+ images: { count: 0, totalBytes: 0 },
173
+ audio: { count: 0, totalBytes: 0 },
174
+ };
175
+
176
+ for (const category of CATEGORIES) {
177
+ const dir = categoryDir(category);
178
+ if (!(await storage.checkIfExists(dir))) continue;
179
+ const files = await storage.listFiles(dir);
180
+ for (const file of files) {
181
+ if (file.endsWith('.meta.json')) continue;
182
+ try {
183
+ const info = await storage.stat(path.join(dir, file));
184
+ result[category].count++;
185
+ result[category].totalBytes += info.size;
186
+ } catch { /* skip missing */ }
187
+ }
188
+ }
189
+
190
+ return result;
191
+ }
192
+
193
+ return { get, put, invalidate, clearCategory, clearAll, stats, evict };
194
+ }
195
+
196
+ function extensionForContentType(contentType: string): string {
197
+ if (contentType.includes('png')) return '.png';
198
+ if (contentType.includes('jpeg') || contentType.includes('jpg')) return '.jpg';
199
+ if (contentType.includes('webp')) return '.webp';
200
+ if (contentType.includes('mpeg') || contentType.includes('mp3')) return '.mp3';
201
+ if (contentType.includes('wav')) return '.wav';
202
+ if (contentType.includes('ogg')) return '.ogg';
203
+ return '.bin';
204
+ }
205
+
206
+ export type MediaCache = ReturnType<typeof createMediaCache>;