synthos 0.10.0 → 0.11.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 (312) 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 +288 -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 +1385 -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 +1879 -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 +155 -30
  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 +12 -3
  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 +282 -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 +1411 -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 +72 -4
  302. package/tests/pageValidator.spec.ts +548 -0
  303. package/tests/profiles.spec.ts +122 -0
  304. package/tests/providers.spec.ts +1 -1
  305. package/tests/sharedTableSchema.spec.ts +242 -0
  306. package/tests/transformPage.spec.ts +62 -81
  307. package/default-pages/application/page.json +0 -10
  308. package/default-pages/retro_game_starter/page.json +0 -12
  309. package/default-pages/sidebar_page/page.html +0 -51
  310. package/default-pages/sidebar_page/page.json +0 -10
  311. package/default-pages/two-panel_page/page.html +0 -68
  312. package/default-pages/two-panel_page/page.json +0 -10
@@ -1,6 +1,7 @@
1
1
  import path from "path";
2
+ import * as fs from "fs/promises";
2
3
  import AdmZip from "adm-zip";
3
- import { listPages, loadPageMetadata, PageMetadata, savePageMetadata, deletePage, copyPage, savePageState, clearVersions, PAGE_VERSION } from "../pages";
4
+ import { listPages, loadPageMetadata, PageMetadata, savePageMetadata, deletePage, copyPage, savePageState, clearChanges, PAGE_VERSION } from "../pages";
4
5
  import { checkIfExists, findFileInFolders, listFiles, listFolders, loadFile } from "../files";
5
6
  import {getModelEntry, loadSettings, saveSettings, ServicesConfig } from "../settings";
6
7
  import { Application } from 'express';
@@ -8,7 +9,7 @@ import express from 'express';
8
9
  import { SynthOSConfig } from "../init";
9
10
  import { createCompletePrompt, PROVIDERS } from "./createCompletePrompt";
10
11
  import { generateDefaultImage, generateImage } from "./generateImage";
11
- import { chainOfThought } from "../models";
12
+ import { createMediaCache } from "./mediaCache";
12
13
  import { requiresSettings } from "./requiresSettings";
13
14
  import { executeScript } from "../scripts";
14
15
  import { listThemes, loadTheme, loadThemeInfo, loadThemeVersion } from "../themes";
@@ -35,6 +36,25 @@ interface ServiceDefinition {
35
36
  exclusive?: string;
36
37
  }
37
38
 
39
+ /**
40
+ * Greeting text applied to any page upgraded by `/api/pages/:name/upgrade`
41
+ * when the page does not already carry a user-authored greeting. Shown as the
42
+ * first assistant message when the migrated page loads.
43
+ */
44
+ const MIGRATED_PAGE_GREETING =
45
+ "This page was just migrated to v3. If anything looks off or stopped working, let me know what you see and I'll help fix it.";
46
+
47
+ const BRAINSTORM_SCHEMA = {
48
+ type: 'object',
49
+ additionalProperties: false,
50
+ required: ['response', 'prompt', 'suggestions'],
51
+ properties: {
52
+ response: { type: 'string' },
53
+ prompt: { type: 'string' },
54
+ suggestions: { type: 'array', items: { type: 'string' } },
55
+ },
56
+ } as const;
57
+
38
58
  const SERVICE_REGISTRY: ServiceDefinition[] = [
39
59
  {
40
60
  id: 'brave-search',
@@ -50,6 +70,10 @@ const SERVICE_REGISTRY: ServiceDefinition[] = [
50
70
 
51
71
  export function useApiRoutes(config: SynthOSConfig, app: Application, customizer?: Customizer): void {
52
72
  const sp = config.storageProvider;
73
+ const mediaCache = createMediaCache({
74
+ storage: sp,
75
+ cacheRoot: path.join(config.pagesFolder, 'cache'),
76
+ });
53
77
 
54
78
  // List pages
55
79
  app.get('/api/pages', async (req, res) => {
@@ -130,6 +154,7 @@ export function useApiRoutes(config: SynthOSConfig, app: Application, customizer
130
154
  const existingMeta = await loadPageMetadata(config, finalName);
131
155
  const metadata: PageMetadata = {
132
156
  title: existingMeta?.title ?? '',
157
+ description: existingMeta?.description ?? '',
133
158
  categories: existingMeta?.categories ?? [],
134
159
  pinned: existingMeta?.pinned ?? false,
135
160
  showInAll: existingMeta?.showInAll ?? true,
@@ -137,6 +162,8 @@ export function useApiRoutes(config: SynthOSConfig, app: Application, customizer
137
162
  lastModified: now,
138
163
  pageVersion: existingMeta?.pageVersion ?? PAGE_VERSION,
139
164
  mode: existingMeta?.mode ?? 'unlocked',
165
+ greeting: existingMeta?.greeting ?? '',
166
+ firstRunGreeting: existingMeta?.firstRunGreeting ?? '',
140
167
  };
141
168
  await savePageMetadata(config, finalName, metadata);
142
169
 
@@ -157,6 +184,7 @@ export function useApiRoutes(config: SynthOSConfig, app: Application, customizer
157
184
  } else {
158
185
  const defaults: PageMetadata = {
159
186
  title: '',
187
+ description: '',
160
188
  categories: [],
161
189
  pinned: false,
162
190
  showInAll: true,
@@ -164,6 +192,8 @@ export function useApiRoutes(config: SynthOSConfig, app: Application, customizer
164
192
  lastModified: '',
165
193
  pageVersion: 0,
166
194
  mode: 'unlocked',
195
+ greeting: '',
196
+ firstRunGreeting: '',
167
197
  };
168
198
  res.json(defaults);
169
199
  }
@@ -184,6 +214,10 @@ export function useApiRoutes(config: SynthOSConfig, app: Application, customizer
184
214
  res.status(400).json({ error: 'title must be a string' });
185
215
  return;
186
216
  }
217
+ if ('description' in body && typeof body.description !== 'string') {
218
+ res.status(400).json({ error: 'description must be a string' });
219
+ return;
220
+ }
187
221
  if ('categories' in body && !Array.isArray(body.categories)) {
188
222
  res.status(400).json({ error: 'categories must be an array' });
189
223
  return;
@@ -205,6 +239,7 @@ export function useApiRoutes(config: SynthOSConfig, app: Application, customizer
205
239
  const existing = await loadPageMetadata(config, name, config.requiredPagesFolders);
206
240
  const metadata: PageMetadata = {
207
241
  title: existing?.title ?? '',
242
+ description: existing?.description ?? '',
208
243
  categories: existing?.categories ?? [],
209
244
  pinned: existing?.pinned ?? false,
210
245
  showInAll: existing?.showInAll ?? true,
@@ -212,10 +247,13 @@ export function useApiRoutes(config: SynthOSConfig, app: Application, customizer
212
247
  lastModified: existing?.lastModified ?? '',
213
248
  pageVersion: existing?.pageVersion ?? 0,
214
249
  mode: existing?.mode ?? 'unlocked',
250
+ greeting: existing?.greeting ?? '',
251
+ firstRunGreeting: existing?.firstRunGreeting ?? '',
215
252
  };
216
253
 
217
254
  // Overlay provided fields
218
255
  if ('title' in body) metadata.title = body.title;
256
+ if ('description' in body) metadata.description = body.description;
219
257
  if ('categories' in body) metadata.categories = body.categories;
220
258
  if ('pinned' in body) metadata.pinned = body.pinned;
221
259
  if ('showInAll' in body) metadata.showInAll = body.showInAll;
@@ -266,6 +304,7 @@ export function useApiRoutes(config: SynthOSConfig, app: Application, customizer
266
304
  if (!metadata) {
267
305
  metadata = {
268
306
  title: '',
307
+ description: '',
269
308
  categories: [],
270
309
  pinned: false,
271
310
  showInAll: true,
@@ -273,6 +312,8 @@ export function useApiRoutes(config: SynthOSConfig, app: Application, customizer
273
312
  lastModified: '',
274
313
  pageVersion: 0,
275
314
  mode: 'unlocked',
315
+ greeting: '',
316
+ firstRunGreeting: '',
276
317
  };
277
318
  }
278
319
 
@@ -473,10 +514,46 @@ export function useApiRoutes(config: SynthOSConfig, app: Application, customizer
473
514
  const { prompt, shape, style } = req.body;
474
515
  const builder = getModelEntry(settings, 'builder');
475
516
  const { configuration, imageQuality, provider } = builder;
476
- const response = provider === 'OpenAI' ?
477
- await generateImage({ apiKey: configuration.apiKey, prompt, shape, quality: imageQuality, style }) :
478
- await generateDefaultImage();
517
+
518
+ // Only cache OpenAI image generations
519
+ if (provider !== 'OpenAI') {
520
+ const response = await generateDefaultImage();
521
+ if (response.completed) {
522
+ res.json(response.value);
523
+ } else {
524
+ res.status(500).send(response.error?.message);
525
+ }
526
+ return;
527
+ }
528
+
529
+ const cacheEnabled = settings.cache?.enabled !== false;
530
+ const noCache = req.headers['x-no-cache'] === 'true' || req.query.nocache === '1';
531
+ const cacheKey = `image:v1:${prompt}:${shape}:${imageQuality}:${style}`;
532
+
533
+ // Check cache
534
+ if (cacheEnabled && !noCache) {
535
+ const cached = await mediaCache.get('images', cacheKey);
536
+ if (cached.hit) {
537
+ const base64 = cached.buffer.toString('base64');
538
+ res.json({ url: `data:${cached.contentType};base64,${base64}` });
539
+ return;
540
+ }
541
+ }
542
+
543
+ // Generate fresh
544
+ const response = await generateImage({ apiKey: configuration.apiKey, prompt, shape, quality: imageQuality, style });
479
545
  if (response.completed) {
546
+ // Cache the result
547
+ if (cacheEnabled) {
548
+ const dataUrl = response.value!.url;
549
+ const base64Data = dataUrl.split(',')[1];
550
+ if (base64Data) {
551
+ const buffer = Buffer.from(base64Data, 'base64');
552
+ await mediaCache.put('images', cacheKey, buffer, 'image/png', {
553
+ prompt, shape, quality: imageQuality, style,
554
+ });
555
+ }
556
+ }
480
557
  res.json(response.value);
481
558
  } else {
482
559
  res.status(500).send(response.error?.message);
@@ -484,18 +561,57 @@ export function useApiRoutes(config: SynthOSConfig, app: Application, customizer
484
561
  });
485
562
  });
486
563
 
487
- // Define a route to generate a completion using chain-of-thought
564
+ // Define a route to generate a completion. When `schema` is provided, the model
565
+ // is constrained to emit JSON conforming to it (Anthropic output_config /
566
+ // OpenAI json_schema) and the parsed object is returned directly. Otherwise a
567
+ // plain text completion is returned as { answer: string }.
488
568
  app.post('/api/generate/completion', async (req, res) => {
489
- await requiresSettings(res, config, async (settings) => {
490
- const { prompt, temperature } = req.body;
569
+ await requiresSettings(res, config, async () => {
570
+ const { prompt, temperature, schema } = req.body;
571
+ if (typeof prompt !== 'string' || !prompt.trim()) {
572
+ res.status(400).json({ error: 'prompt is required' });
573
+ return;
574
+ }
575
+ if (schema !== undefined && (typeof schema !== 'object' || schema === null || Array.isArray(schema))) {
576
+ res.status(400).json({ error: 'schema must be a JSON schema object' });
577
+ return;
578
+ }
491
579
  const completePrompt = await createCompletePrompt(config, 'chat', req.body.model);
492
- const response = await chainOfThought({ question: prompt, temperature, completePrompt });
493
- if (response.completed) {
494
- res.json(response.value ?? {});
495
- } else {
496
- console.error(response.error);
497
- res.status(500).send(response.error?.message);
580
+ const userMessage: { role: 'user'; content: string } = { role: 'user', content: prompt };
581
+ const result = await completePrompt({
582
+ prompt: userMessage,
583
+ temperature,
584
+ ...(schema ? { outputSchema: schema, jsonSchema: schema } : {}),
585
+ });
586
+ if (!result.completed) {
587
+ console.error(result.error);
588
+ res.status(500).send(result.error?.message);
589
+ return;
590
+ }
591
+ const text = typeof result.value === 'string' ? result.value : '';
592
+ if (schema) {
593
+ let parsed: unknown;
594
+ if (typeof result.value === 'object' && result.value !== null) {
595
+ parsed = result.value;
596
+ } else {
597
+ let candidate = text.replace(/^```(?:json)?\s*/i, '').replace(/\s*```\s*$/, '').trim();
598
+ const start = candidate.indexOf('{');
599
+ const end = candidate.lastIndexOf('}');
600
+ if (start !== -1 && end > start) {
601
+ candidate = candidate.substring(start, end + 1);
602
+ }
603
+ try {
604
+ parsed = JSON.parse(candidate);
605
+ } catch {
606
+ console.error('completion: schema-mode response was not parseable JSON:', text);
607
+ res.status(502).json({ error: 'Model did not return JSON conforming to schema', raw: text });
608
+ return;
609
+ }
610
+ }
611
+ res.json(parsed);
612
+ return;
498
613
  }
614
+ res.json({ answer: text });
499
615
  });
500
616
  });
501
617
 
@@ -526,16 +642,10 @@ If you see a conversation between ${productName} and the User. Asses what they'r
526
642
 
527
643
  ${productName} exposes table storage and chat completion api's that every page can use. If the user wants to store something or use AI, your prompt should suggest using table storage or make llm calls.
528
644
 
529
- You MUST return your response as a JSON object with exactly these fields:
530
- {
531
- "response": "Your conversational reply explanations, options, suggestions. Markdown OK.",
532
- "prompt": "A clean, actionable instruction ready to paste into ${productName} chat to build what was discussed. Update this each exchange to reflect the latest brainstorm state.",
533
- "suggestions": ["Short clickable option A", "Short clickable option B", "Short clickable option C"]
534
- }
535
-
536
- suggestions — 2-4 short phrases the user can click to continue the conversation. These are next-step options: directions to explore, questions to answer, or choices to make. Keep each under 60 characters. Always provide suggestions.
537
-
538
- Return ONLY the JSON object.`};
645
+ Field guidance:
646
+ - response — Your conversational reply: explanations, options, suggestions. Markdown OK.
647
+ - promptA clean, actionable instruction ready to paste into ${productName} chat to build what was discussed. Update this each exchange to reflect the latest brainstorm state. Empty string is fine before the user has a clear direction.
648
+ - suggestions 2-4 short phrases (under 60 characters each) the user can click to continue the conversation. Always provide suggestions.`};
539
649
 
540
650
  // Format multi-turn conversation into a single prompt
541
651
  const formatted = (messages as { role: string; content: string }[]).map(m =>
@@ -544,15 +654,35 @@ Return ONLY the JSON object.`};
544
654
 
545
655
  const prompt: { role: 'user'; content: string } = { role: 'user', content: formatted };
546
656
 
547
- const result = await completePrompt({ prompt, system, jsonMode: true });
657
+ const result = await completePrompt({
658
+ prompt,
659
+ system,
660
+ outputSchema: BRAINSTORM_SCHEMA,
661
+ jsonSchema: BRAINSTORM_SCHEMA,
662
+ });
548
663
  if (result.completed) {
549
- let response = result.value || '';
664
+ let response = '';
550
665
  let brainstormPrompt = '';
551
666
  let suggestions: string[] = [];
552
- // jsonMode returns an already-parsed object from agentm-core
553
- const parsed = (typeof result.value === 'object' && result.value !== null)
554
- ? result.value as Record<string, unknown>
555
- : (() => { try { return JSON.parse(result.value as string); } catch { return null; } })();
667
+ let parsed: Record<string, unknown> | null = null;
668
+ if (typeof result.value === 'object' && result.value !== null) {
669
+ parsed = result.value as Record<string, unknown>;
670
+ } else if (typeof result.value === 'string') {
671
+ let candidate = result.value
672
+ .replace(/^```(?:json)?\s*/i, '')
673
+ .replace(/\s*```\s*$/, '')
674
+ .trim();
675
+ const start = candidate.indexOf('{');
676
+ const end = candidate.lastIndexOf('}');
677
+ if (start !== -1 && end > start) {
678
+ candidate = candidate.substring(start, end + 1);
679
+ }
680
+ try {
681
+ parsed = JSON.parse(candidate) as Record<string, unknown>;
682
+ } catch {
683
+ console.error('brainstorm: response was not parseable JSON:', result.value);
684
+ }
685
+ }
556
686
  if (parsed) {
557
687
  if (typeof parsed.response === 'string') response = parsed.response;
558
688
  if (typeof parsed.prompt === 'string') brainstormPrompt = parsed.prompt;
@@ -883,12 +1013,20 @@ Return ONLY the JSON object.`};
883
1013
  await sp.copyFolderRecursive(folderPath, path.join(migratedFolder, name));
884
1014
  }
885
1015
 
886
- // Clear stale version files (undo snapshots from the old page version)
887
- await clearVersions(config, name);
1016
+ // Clear stale change files (undo snapshots from the old page version)
1017
+ await clearChanges(config, name);
888
1018
 
889
1019
  // Update metadata
890
1020
  metadata.pageVersion = PAGE_VERSION;
891
1021
  metadata.lastModified = new Date().toISOString();
1022
+
1023
+ // Assign a generic greeting so the shell shows a helpful first
1024
+ // message explaining this page was auto-migrated. Only set if the
1025
+ // page does not already have a user-authored greeting.
1026
+ if (!metadata.greeting || metadata.greeting.trim().length === 0) {
1027
+ metadata.greeting = MIGRATED_PAGE_GREETING;
1028
+ }
1029
+
892
1030
  await savePageMetadata(config, name, metadata);
893
1031
 
894
1032
  res.json({ upgraded: true, fromVersion: currentVersion, toVersion: PAGE_VERSION });
@@ -987,4 +1125,116 @@ Return ONLY the JSON object.`};
987
1125
  }
988
1126
  });
989
1127
  });
1128
+
1129
+ // -----------------------------------------------------------------------
1130
+ // Cache management routes
1131
+ // -----------------------------------------------------------------------
1132
+
1133
+ app.get('/api/cache/stats', async (_req, res) => {
1134
+ try {
1135
+ const cacheStats = await mediaCache.stats();
1136
+ res.json(cacheStats);
1137
+ } catch (err: unknown) {
1138
+ res.status(500).json({ error: (err as Error).message });
1139
+ }
1140
+ });
1141
+
1142
+ app.delete('/api/cache/images', async (_req, res) => {
1143
+ try {
1144
+ await mediaCache.clearCategory('images');
1145
+ res.json({ cleared: 'images' });
1146
+ } catch (err: unknown) {
1147
+ res.status(500).json({ error: (err as Error).message });
1148
+ }
1149
+ });
1150
+
1151
+ app.delete('/api/cache/audio', async (_req, res) => {
1152
+ try {
1153
+ await mediaCache.clearCategory('audio');
1154
+ res.json({ cleared: 'audio' });
1155
+ } catch (err: unknown) {
1156
+ res.status(500).json({ error: (err as Error).message });
1157
+ }
1158
+ });
1159
+
1160
+ app.delete('/api/cache', async (_req, res) => {
1161
+ try {
1162
+ await mediaCache.clearAll();
1163
+ res.json({ cleared: 'all' });
1164
+ } catch (err: unknown) {
1165
+ res.status(500).json({ error: (err as Error).message });
1166
+ }
1167
+ });
1168
+
1169
+ // Serve a saved starter screenshot (page.light.png / page.dark.png) from
1170
+ // the top-level starters/<name>/ folder so the carousel can display them.
1171
+ app.get('/api/starters/:name/screenshot/:variant', async (req, res) => {
1172
+ try {
1173
+ const { name, variant } = req.params;
1174
+ if (variant !== 'light' && variant !== 'dark') {
1175
+ res.status(400).json({ error: 'variant must be "light" or "dark"' });
1176
+ return;
1177
+ }
1178
+ if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
1179
+ res.status(400).json({ error: 'invalid starter name' });
1180
+ return;
1181
+ }
1182
+ const target = path.join(process.cwd(), 'starters', name, `page.${variant}.png`);
1183
+ try {
1184
+ await fs.access(target);
1185
+ } catch {
1186
+ res.status(404).json({ error: 'screenshot not found' });
1187
+ return;
1188
+ }
1189
+ res.sendFile(target, { headers: { 'Cache-Control': 'public, max-age=300' } });
1190
+ } catch (err: unknown) {
1191
+ console.error(err);
1192
+ res.status(500).json({ error: (err as Error).message });
1193
+ }
1194
+ });
1195
+
1196
+ // Save a captured screenshot for a starter page into the top-level
1197
+ // starters/<name>/ folder so the image lives alongside the snapshot.
1198
+ app.post(
1199
+ '/api/starters/:name/screenshot/:variant',
1200
+ express.raw({ type: 'image/png', limit: '20mb' }),
1201
+ async (req, res) => {
1202
+ try {
1203
+ const { name, variant } = req.params;
1204
+
1205
+ if (variant !== 'light' && variant !== 'dark') {
1206
+ res.status(400).json({ error: 'variant must be "light" or "dark"' });
1207
+ return;
1208
+ }
1209
+
1210
+ if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
1211
+ res.status(400).json({ error: 'invalid starter name' });
1212
+ return;
1213
+ }
1214
+
1215
+ const png = req.body as Buffer;
1216
+ if (!png || png.length === 0) {
1217
+ res.status(400).json({ error: 'empty image body' });
1218
+ return;
1219
+ }
1220
+ // PNG signature: 89 50 4E 47 0D 0A 1A 0A
1221
+ if (png.length < 8 ||
1222
+ png[0] !== 0x89 || png[1] !== 0x50 || png[2] !== 0x4E || png[3] !== 0x47) {
1223
+ res.status(400).json({ error: 'body is not a PNG' });
1224
+ return;
1225
+ }
1226
+
1227
+ const targetFolder = path.join(process.cwd(), 'starters', name);
1228
+ await fs.mkdir(targetFolder, { recursive: true });
1229
+
1230
+ const target = path.join(targetFolder, `page.${variant}.png`);
1231
+ await fs.writeFile(target, png);
1232
+
1233
+ res.json({ ok: true, paths: [target], bytes: png.length });
1234
+ } catch (err: unknown) {
1235
+ console.error(err);
1236
+ res.status(500).json({ error: (err as Error).message });
1237
+ }
1238
+ }
1239
+ );
990
1240
  }