synthos 0.7.1 → 0.8.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 (263) hide show
  1. package/README.md +215 -65
  2. package/default-pages/application.json +1 -0
  3. package/default-pages/json_tools.json +1 -1
  4. package/default-pages/oregon_trail.html +321 -0
  5. package/default-pages/oregon_trail.json +12 -0
  6. package/default-pages/sidebar_page.json +1 -0
  7. package/default-pages/solar_explorer.html +10 -18
  8. package/default-pages/solar_explorer.json +2 -2
  9. package/default-pages/two-panel_page.json +1 -0
  10. package/default-pages/us_map.html +192 -0
  11. package/default-pages/us_map.json +12 -0
  12. package/default-pages/us_map_1850.html +325 -0
  13. package/default-pages/us_map_1850.json +12 -0
  14. package/default-pages/western_cities_1850.html +526 -0
  15. package/default-pages/western_cities_1850.json +12 -0
  16. package/default-themes/{nebula-dawn.css → nebula-dawn.v2.css} +24 -0
  17. package/default-themes/{nebula-dusk.css → nebula-dusk.v2.css} +24 -0
  18. package/dist/agents/a2a/a2aProvider.d.ts +3 -0
  19. package/dist/agents/a2a/a2aProvider.d.ts.map +1 -0
  20. package/dist/agents/a2a/a2aProvider.js +126 -0
  21. package/dist/agents/a2a/a2aProvider.js.map +1 -0
  22. package/dist/agents/discovery.d.ts +30 -0
  23. package/dist/agents/discovery.d.ts.map +1 -0
  24. package/dist/agents/discovery.js +52 -0
  25. package/dist/agents/discovery.js.map +1 -0
  26. package/dist/agents/index.d.ts +7 -0
  27. package/dist/agents/index.d.ts.map +1 -0
  28. package/dist/agents/index.js +19 -0
  29. package/dist/agents/index.js.map +1 -0
  30. package/dist/agents/openclaw/gatewayManager.d.ts +113 -0
  31. package/dist/agents/openclaw/gatewayManager.d.ts.map +1 -0
  32. package/dist/agents/openclaw/gatewayManager.js +470 -0
  33. package/dist/agents/openclaw/gatewayManager.js.map +1 -0
  34. package/dist/agents/openclaw/openclawProvider.d.ts +3 -0
  35. package/dist/agents/openclaw/openclawProvider.d.ts.map +1 -0
  36. package/dist/agents/openclaw/openclawProvider.js +239 -0
  37. package/dist/agents/openclaw/openclawProvider.js.map +1 -0
  38. package/dist/agents/openclaw/sshTunnelManager.d.ts +23 -0
  39. package/dist/agents/openclaw/sshTunnelManager.d.ts.map +1 -0
  40. package/dist/agents/openclaw/sshTunnelManager.js +340 -0
  41. package/dist/agents/openclaw/sshTunnelManager.js.map +1 -0
  42. package/dist/agents/types.d.ts +64 -0
  43. package/dist/agents/types.d.ts.map +1 -0
  44. package/dist/agents/types.js +6 -0
  45. package/dist/agents/types.js.map +1 -0
  46. package/dist/connectors/airtable/connector.json +27 -0
  47. package/dist/connectors/alpha-vantage/connector.json +26 -0
  48. package/dist/connectors/brave-search/connector.json +26 -0
  49. package/dist/connectors/cloudinary/connector.json +27 -0
  50. package/dist/connectors/deepl/connector.json +28 -0
  51. package/dist/connectors/elevenlabs/connector.json +30 -0
  52. package/dist/connectors/giphy/connector.json +27 -0
  53. package/dist/connectors/github/connector.json +29 -0
  54. package/dist/connectors/huggingface/connector.json +27 -0
  55. package/dist/connectors/imgur/connector.json +29 -0
  56. package/dist/connectors/index.d.ts +1 -1
  57. package/dist/connectors/index.d.ts.map +1 -1
  58. package/dist/connectors/instagram/connector.json +43 -0
  59. package/dist/connectors/jira/connector.json +28 -0
  60. package/dist/connectors/mapbox/connector.json +26 -0
  61. package/dist/connectors/nasa/connector.json +27 -0
  62. package/dist/connectors/newsapi/connector.json +27 -0
  63. package/dist/connectors/notion/connector.json +28 -0
  64. package/dist/connectors/open-exchange-rates/connector.json +27 -0
  65. package/dist/connectors/openweathermap/connector.json +26 -0
  66. package/dist/connectors/pexels/connector.json +27 -0
  67. package/dist/connectors/registry.d.ts.map +1 -1
  68. package/dist/connectors/registry.js +42 -96
  69. package/dist/connectors/registry.js.map +1 -1
  70. package/dist/connectors/resend/connector.json +29 -0
  71. package/dist/connectors/rss2json/connector.json +27 -0
  72. package/dist/connectors/sendgrid/connector.json +27 -0
  73. package/dist/connectors/spoonacular/connector.json +28 -0
  74. package/dist/connectors/stability-ai/connector.json +27 -0
  75. package/dist/connectors/twilio/connector.json +28 -0
  76. package/dist/connectors/types.d.ts +23 -0
  77. package/dist/connectors/types.d.ts.map +1 -1
  78. package/dist/connectors/unsplash/connector.json +27 -0
  79. package/dist/connectors/wolfram-alpha/connector.json +26 -0
  80. package/dist/connectors/youtube-data/connector.json +30 -0
  81. package/dist/files.d.ts +1 -0
  82. package/dist/files.d.ts.map +1 -1
  83. package/dist/files.js +16 -1
  84. package/dist/files.js.map +1 -1
  85. package/dist/init.d.ts.map +1 -1
  86. package/dist/init.js +28 -0
  87. package/dist/init.js.map +1 -1
  88. package/dist/migrations.d.ts +3 -2
  89. package/dist/migrations.d.ts.map +1 -1
  90. package/dist/migrations.js +122 -138
  91. package/dist/migrations.js.map +1 -1
  92. package/dist/models/anthropic.d.ts +22 -0
  93. package/dist/models/anthropic.d.ts.map +1 -0
  94. package/dist/models/anthropic.js +76 -0
  95. package/dist/models/anthropic.js.map +1 -0
  96. package/dist/models/chainOfThought.d.ts +12 -0
  97. package/dist/models/chainOfThought.d.ts.map +1 -0
  98. package/dist/models/chainOfThought.js +45 -0
  99. package/dist/models/chainOfThought.js.map +1 -0
  100. package/dist/models/fireworksai.d.ts +30 -0
  101. package/dist/models/fireworksai.d.ts.map +1 -0
  102. package/dist/models/fireworksai.js +133 -0
  103. package/dist/models/fireworksai.js.map +1 -0
  104. package/dist/models/index.d.ts +7 -1
  105. package/dist/models/index.d.ts.map +1 -1
  106. package/dist/models/index.js +19 -1
  107. package/dist/models/index.js.map +1 -1
  108. package/dist/models/logCompletePrompt.d.ts +3 -0
  109. package/dist/models/logCompletePrompt.d.ts.map +1 -0
  110. package/dist/models/logCompletePrompt.js +23 -0
  111. package/dist/models/logCompletePrompt.js.map +1 -0
  112. package/dist/models/openai.d.ts +24 -0
  113. package/dist/models/openai.d.ts.map +1 -0
  114. package/dist/models/openai.js +80 -0
  115. package/dist/models/openai.js.map +1 -0
  116. package/dist/models/providers.d.ts +1 -0
  117. package/dist/models/providers.d.ts.map +1 -1
  118. package/dist/models/providers.js +12 -4
  119. package/dist/models/providers.js.map +1 -1
  120. package/dist/models/types.d.ts +34 -2
  121. package/dist/models/types.d.ts.map +1 -1
  122. package/dist/models/types.js +16 -0
  123. package/dist/models/types.js.map +1 -1
  124. package/dist/models/utils.d.ts +6 -0
  125. package/dist/models/utils.d.ts.map +1 -0
  126. package/dist/models/utils.js +21 -0
  127. package/dist/models/utils.js.map +1 -0
  128. package/dist/scripts.d.ts +2 -1
  129. package/dist/scripts.d.ts.map +1 -1
  130. package/dist/scripts.js +4 -3
  131. package/dist/scripts.js.map +1 -1
  132. package/dist/service/createCompletePrompt.d.ts +1 -1
  133. package/dist/service/createCompletePrompt.d.ts.map +1 -1
  134. package/dist/service/createCompletePrompt.js +9 -6
  135. package/dist/service/createCompletePrompt.js.map +1 -1
  136. package/dist/service/generateImage.d.ts +1 -1
  137. package/dist/service/generateImage.d.ts.map +1 -1
  138. package/dist/service/generateImage.js +3 -3
  139. package/dist/service/generateImage.js.map +1 -1
  140. package/dist/service/server.d.ts.map +1 -1
  141. package/dist/service/server.js +3 -0
  142. package/dist/service/server.js.map +1 -1
  143. package/dist/service/transformPage.d.ts +4 -2
  144. package/dist/service/transformPage.d.ts.map +1 -1
  145. package/dist/service/transformPage.js +74 -6
  146. package/dist/service/transformPage.js.map +1 -1
  147. package/dist/service/useAgentRoutes.d.ts +4 -0
  148. package/dist/service/useAgentRoutes.d.ts.map +1 -0
  149. package/dist/service/useAgentRoutes.js +389 -0
  150. package/dist/service/useAgentRoutes.js.map +1 -0
  151. package/dist/service/useApiRoutes.d.ts.map +1 -1
  152. package/dist/service/useApiRoutes.js +157 -16
  153. package/dist/service/useApiRoutes.js.map +1 -1
  154. package/dist/service/useConnectorRoutes.d.ts.map +1 -1
  155. package/dist/service/useConnectorRoutes.js +14 -3
  156. package/dist/service/useConnectorRoutes.js.map +1 -1
  157. package/dist/service/useGatewayRoutes.d.ts +4 -0
  158. package/dist/service/useGatewayRoutes.d.ts.map +1 -0
  159. package/dist/service/useGatewayRoutes.js +168 -0
  160. package/dist/service/useGatewayRoutes.js.map +1 -0
  161. package/dist/service/usePageRoutes.d.ts.map +1 -1
  162. package/dist/service/usePageRoutes.js +16 -5
  163. package/dist/service/usePageRoutes.js.map +1 -1
  164. package/dist/settings.d.ts +2 -1
  165. package/dist/settings.d.ts.map +1 -1
  166. package/dist/settings.js +4 -8
  167. package/dist/settings.js.map +1 -1
  168. package/dist/themes.d.ts +14 -0
  169. package/dist/themes.d.ts.map +1 -1
  170. package/dist/themes.js +86 -13
  171. package/dist/themes.js.map +1 -1
  172. package/package.json +10 -5
  173. package/page-scripts/helpers-v2.js +222 -0
  174. package/page-scripts/page-v2.js +656 -0
  175. package/required-pages/builder.html +1 -27
  176. package/required-pages/pages.html +745 -22
  177. package/required-pages/settings.html +819 -21
  178. package/required-pages/synthos_apis.html +56 -1
  179. package/src/agents/a2a/a2aProvider.ts +110 -0
  180. package/src/agents/discovery.ts +74 -0
  181. package/src/agents/index.ts +6 -0
  182. package/src/agents/openclaw/gatewayManager.ts +559 -0
  183. package/src/agents/openclaw/openclawProvider.ts +261 -0
  184. package/src/agents/openclaw/sshTunnelManager.ts +385 -0
  185. package/src/agents/types.ts +82 -0
  186. package/src/connectors/airtable/connector.json +27 -0
  187. package/src/connectors/alpha-vantage/connector.json +26 -0
  188. package/src/connectors/brave-search/connector.json +26 -0
  189. package/src/connectors/cloudinary/connector.json +27 -0
  190. package/src/connectors/deepl/connector.json +28 -0
  191. package/src/connectors/elevenlabs/connector.json +30 -0
  192. package/src/connectors/giphy/connector.json +27 -0
  193. package/src/connectors/github/connector.json +29 -0
  194. package/src/connectors/huggingface/connector.json +27 -0
  195. package/src/connectors/imgur/connector.json +29 -0
  196. package/src/connectors/index.ts +2 -0
  197. package/src/connectors/instagram/connector.json +43 -0
  198. package/src/connectors/jira/connector.json +28 -0
  199. package/src/connectors/mapbox/connector.json +26 -0
  200. package/src/connectors/nasa/connector.json +27 -0
  201. package/src/connectors/newsapi/connector.json +27 -0
  202. package/src/connectors/notion/connector.json +28 -0
  203. package/src/connectors/open-exchange-rates/connector.json +27 -0
  204. package/src/connectors/openweathermap/connector.json +26 -0
  205. package/src/connectors/pexels/connector.json +27 -0
  206. package/src/connectors/registry.ts +21 -97
  207. package/src/connectors/resend/connector.json +29 -0
  208. package/src/connectors/rss2json/connector.json +27 -0
  209. package/src/connectors/sendgrid/connector.json +27 -0
  210. package/src/connectors/spoonacular/connector.json +28 -0
  211. package/src/connectors/stability-ai/connector.json +27 -0
  212. package/src/connectors/twilio/connector.json +28 -0
  213. package/src/connectors/types.ts +25 -0
  214. package/src/connectors/unsplash/connector.json +27 -0
  215. package/src/connectors/wolfram-alpha/connector.json +26 -0
  216. package/src/connectors/youtube-data/connector.json +30 -0
  217. package/src/files.ts +14 -0
  218. package/src/init.ts +27 -0
  219. package/src/migrations.ts +121 -138
  220. package/src/models/anthropic.ts +89 -0
  221. package/src/models/chainOfThought.ts +56 -0
  222. package/src/models/fireworksai.ts +136 -0
  223. package/src/models/index.ts +7 -1
  224. package/src/models/logCompletePrompt.ts +25 -0
  225. package/src/models/openai.ts +90 -0
  226. package/src/models/providers.ts +12 -3
  227. package/src/models/types.ts +67 -2
  228. package/src/models/utils.ts +16 -0
  229. package/src/scripts.ts +2 -2
  230. package/src/service/createCompletePrompt.ts +3 -1
  231. package/src/service/generateImage.ts +2 -2
  232. package/src/service/server.ts +4 -0
  233. package/src/service/transformPage.ts +81 -8
  234. package/src/service/useAgentRoutes.ts +423 -0
  235. package/src/service/useApiRoutes.ts +173 -18
  236. package/src/service/useConnectorRoutes.ts +14 -3
  237. package/src/service/usePageRoutes.ts +20 -6
  238. package/src/settings.ts +6 -10
  239. package/src/themes.ts +84 -12
  240. package/tests/README.md +12 -0
  241. package/tests/anthropic.spec.ts +84 -0
  242. package/tests/chainOfThought.spec.ts +108 -0
  243. package/tests/ensureScripts.spec.ts +82 -0
  244. package/tests/files.spec.ts +233 -0
  245. package/tests/fireworksai.spec.ts +92 -0
  246. package/tests/logCompletePrompt.spec.ts +74 -0
  247. package/tests/migrations.spec.ts +169 -0
  248. package/tests/openai.spec.ts +71 -0
  249. package/tests/pages.spec.ts +328 -0
  250. package/tests/providers.spec.ts +144 -0
  251. package/tests/scripts.spec.ts +209 -0
  252. package/tests/transformPage.spec.ts +931 -0
  253. package/tests/types.spec.ts +23 -0
  254. package/default-pages/app_builder.json +0 -1
  255. package/default-pages/sidebar_builder.json +0 -1
  256. package/default-pages/two-panel_builder.json +0 -1
  257. package/images/home.png +0 -0
  258. package/images/page-management.png +0 -0
  259. package/images/settings.png +0 -0
  260. package/images/synthos-square.png +0 -0
  261. /package/default-pages/{app_builder.html → application.html} +0 -0
  262. /package/default-pages/{sidebar_builder.html → sidebar_page.html} +0 -0
  263. /package/default-pages/{two-panel_builder.html → two-panel_page.html} +0 -0
@@ -35,9 +35,11 @@ export function useConnectorRoutes(config: SynthOSConfig, app: Application): voi
35
35
  id: def.id,
36
36
  name: def.name,
37
37
  category: def.category,
38
+ description: def.description,
38
39
  configured: isOAuth
39
40
  ? !!oauthCfg && oauthCfg.enabled && !!oauthCfg.accessToken
40
- : !!cfg && cfg.enabled && !!cfg.apiKey
41
+ : !!cfg && cfg.enabled && !!cfg.apiKey,
42
+ enabled: !!cfg?.enabled
41
43
  };
42
44
  });
43
45
 
@@ -352,11 +354,20 @@ export function useConnectorRoutes(config: SynthOSConfig, app: Application): voi
352
354
  }
353
355
 
354
356
  // Build URL — join baseUrl path with request path to avoid
355
- // absolute paths (e.g. "/me/accounts") replacing the base path
357
+ // absolute paths (e.g. "/me/accounts") replacing the base path.
358
+ // Split path from inline query string first — assigning a '?' to
359
+ // URL.pathname encodes it as %3F, which breaks upstream APIs.
360
+ const [reqPath, reqQS] = request.path.split('?');
356
361
  const base = new URL(def.baseUrl);
357
- const joinedPath = base.pathname.replace(/\/+$/, '') + '/' + request.path.replace(/^\/+/, '');
362
+ const joinedPath = base.pathname.replace(/\/+$/, '') + '/' + reqPath.replace(/^\/+/, '');
358
363
  base.pathname = joinedPath;
359
364
  const url = base;
365
+ if (reqQS) {
366
+ const inline = new URLSearchParams(reqQS);
367
+ for (const [key, value] of inline.entries()) {
368
+ url.searchParams.set(key, value);
369
+ }
370
+ }
360
371
  if (request.query) {
361
372
  for (const [key, value] of Object.entries(request.query)) {
362
373
  url.searchParams.set(key, value);
@@ -5,7 +5,7 @@ import { transformPage } from "./transformPage";
5
5
  import { getModelInstructions } from "./modelInstructions";
6
6
  import { SynthOSConfig } from "../init";
7
7
  import { createCompletePrompt } from "./createCompletePrompt";
8
- import { completePrompt } from "agentm-core";
8
+ import { completePrompt } from "../models";
9
9
  import { green, red, dim, estimateTokens } from "./debugLog";
10
10
  import { loadThemeInfo } from "../themes";
11
11
  import * as cheerio from 'cheerio';
@@ -56,10 +56,11 @@ function injectPageHelpers(html: string, pageVersion: number): string {
56
56
  if (pageVersion < 2) return html;
57
57
  const tag = `<script id="page-helpers" src="/api/page-helpers.js?v=${pageVersion}"></script>`;
58
58
 
59
- // Replace any existing page-helpers script (may be at wrong position from prior LLM output)
59
+ // Remove any existing page-helpers script (may be at wrong position from prior LLM output)
60
+ // so it gets re-injected at the correct position below.
60
61
  const existing = html.match(/<script\s+id="page-helpers"[^>]*><\/script>/);
61
62
  if (existing) {
62
- return html.replace(existing[0], tag);
63
+ html = html.replace(existing[0], '');
63
64
  }
64
65
 
65
66
  // Inject into <head> after page-info so helpers are available before inline body scripts
@@ -205,6 +206,19 @@ export function usePageRoutes(config: SynthOSConfig, app: Application): void {
205
206
  pageState = $.html();
206
207
  }
207
208
 
209
+ // Inject save-line marker at the end of chat messages (skip for locked pages)
210
+ const sourceMetadata = await loadPageMetadata(config.pagesFolder, page, config.requiredPagesFolder);
211
+ if (sourceMetadata?.mode !== 'locked') {
212
+ const $ = cheerio.load(pageState);
213
+ // Remove any existing save-line first
214
+ $('#chatMessages .save-line').remove();
215
+ // Append new save-line
216
+ $('#chatMessages').append(
217
+ '<div class="save-line" data-locked="true"><span class="save-line-label">Saved</span></div>'
218
+ );
219
+ pageState = $.html();
220
+ }
221
+
208
222
  // Save as new page
209
223
  await savePageState(config.pagesFolder, saveAs, pageState, title, categories);
210
224
 
@@ -284,13 +298,13 @@ export function usePageRoutes(config: SynthOSConfig, app: Application): void {
284
298
  const pagesFolder = config.pagesFolder;
285
299
  const settings = await loadSettings(config.pagesFolder);
286
300
  const builder = getModelEntry(settings, 'builder');
287
- const { configuration, instructions } = builder;
288
- const maxTokens = configuration.maxTokens;
301
+ const { instructions } = builder;
289
302
  const theme = settings.theme;
290
303
  const themeInfo = await loadThemeInfo(theme ?? 'nebula-dusk', config);
291
304
  const modelInstructions = getModelInstructions(builder.provider);
292
305
  const configuredConnectors = settings.connectors;
293
- const result = await transformPage({ pagesFolder, pageState, message, maxTokens, instructions, modelInstructions, completePrompt, themeInfo, configuredConnectors });
306
+ const configuredAgents = settings.agents;
307
+ const result = await transformPage({ pagesFolder, pageState, message, instructions, modelInstructions, completePrompt, themeInfo, configuredConnectors, configuredAgents });
294
308
  if (result.completed) {
295
309
  const { html, changeCount } = result.value!;
296
310
  if (config.debug) {
package/src/settings.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import {checkIfExists, loadFile, saveFile} from './files';
2
2
  import path from 'path';
3
3
  import { ModelEntry, ProviderName, detectProvider } from './models';
4
+ import { AgentConfig } from './agents';
4
5
 
5
6
  let _settings: Partial<SettingsV2>|undefined;
6
7
 
@@ -20,7 +21,6 @@ export type { ModelEntry } from './models';
20
21
  export interface SettingsV1 {
21
22
  serviceApiKey: string;
22
23
  model: string;
23
- maxTokens: number;
24
24
  imageQuality: 'standard' | 'hd';
25
25
  instructions?: string;
26
26
  logCompletions?: boolean;
@@ -38,6 +38,7 @@ export interface SettingsV2 {
38
38
  features: string[];
39
39
  services?: ServicesConfig;
40
40
  connectors?: ServicesConfig;
41
+ agents?: AgentConfig[];
41
42
  }
42
43
 
43
44
  export const DefaultSettings: SettingsV2 = {
@@ -47,7 +48,7 @@ export const DefaultSettings: SettingsV2 = {
47
48
  {
48
49
  use: 'builder',
49
50
  provider: 'Anthropic',
50
- configuration: { apiKey: '', model: '', maxTokens: 32000 },
51
+ configuration: { apiKey: '', model: '' },
51
52
  imageQuality: 'standard',
52
53
  instructions: '',
53
54
  logCompletions: false,
@@ -55,7 +56,7 @@ export const DefaultSettings: SettingsV2 = {
55
56
  {
56
57
  use: 'chat',
57
58
  provider: 'Anthropic',
58
- configuration: { apiKey: '', model: '', maxTokens: 32000 },
59
+ configuration: { apiKey: '', model: '' },
59
60
  imageQuality: 'standard',
60
61
  instructions: '',
61
62
  logCompletions: false,
@@ -63,7 +64,8 @@ export const DefaultSettings: SettingsV2 = {
63
64
  ],
64
65
  features: [],
65
66
  services: {},
66
- connectors: {}
67
+ connectors: {},
68
+ agents: [],
67
69
  };
68
70
 
69
71
  /**
@@ -100,7 +102,6 @@ function migrateV1toV2(raw: Record<string, unknown>): SettingsV2 {
100
102
  configuration: {
101
103
  apiKey: v1.serviceApiKey ?? '',
102
104
  model,
103
- maxTokens: v1.maxTokens ?? 32000,
104
105
  },
105
106
  imageQuality: v1.imageQuality ?? 'standard',
106
107
  instructions: v1.instructions ?? '',
@@ -112,7 +113,6 @@ function migrateV1toV2(raw: Record<string, unknown>): SettingsV2 {
112
113
  configuration: {
113
114
  apiKey: v1.serviceApiKey ?? '',
114
115
  model: chatModel,
115
- maxTokens: v1.maxTokens ?? 32000,
116
116
  },
117
117
  imageQuality: v1.imageQuality ?? 'standard',
118
118
  instructions: v1.instructions ?? '',
@@ -133,10 +133,6 @@ export async function hasConfiguredSettings(folder: string): Promise<boolean> {
133
133
  if (typeof builder.configuration.model !== 'string' || builder.configuration.model.length == 0) {
134
134
  return false;
135
135
  }
136
- if (typeof builder.configuration.maxTokens !== 'number' || builder.configuration.maxTokens <= 0) {
137
- return false;
138
- }
139
-
140
136
  return true;
141
137
  }
142
138
 
package/src/themes.ts CHANGED
@@ -2,6 +2,8 @@ import path from 'path';
2
2
  import { checkIfExists, listFiles, loadFile } from './files';
3
3
  import { SynthOSConfig } from './init';
4
4
 
5
+ export const THEME_VERSION = 2;
6
+
5
7
  export interface ThemeInfo {
6
8
  mode: 'light' | 'dark';
7
9
  colors: Record<string, string>;
@@ -11,6 +13,39 @@ function userThemesFolder(config: SynthOSConfig): string {
11
13
  return path.join(config.pagesFolder, 'themes');
12
14
  }
13
15
 
16
+ /**
17
+ * Extract the base theme name and version from a CSS filename.
18
+ * e.g. "nebula-dusk.v2.css" → { name: "nebula-dusk", version: 2 }
19
+ * "nebula-dusk.css" → { name: "nebula-dusk", version: 1 }
20
+ */
21
+ export function parseThemeFilename(filename: string): { name: string; version: number } | undefined {
22
+ if (!filename.endsWith('.css')) return undefined;
23
+ const versionedMatch = filename.match(/^(.+)\.v(\d+)\.css$/);
24
+ if (versionedMatch) {
25
+ return { name: versionedMatch[1], version: parseInt(versionedMatch[2], 10) };
26
+ }
27
+ return { name: filename.replace(/\.css$/, ''), version: 1 };
28
+ }
29
+
30
+ /**
31
+ * Find the CSS file for a theme by name in a folder.
32
+ * Prefers the highest-versioned file (e.g. name.v2.css over name.css).
33
+ */
34
+ async function findThemeCssFile(folder: string, name: string): Promise<{ path: string; version: number } | undefined> {
35
+ if (!await checkIfExists(folder)) return undefined;
36
+ const files = await listFiles(folder);
37
+ let best: { path: string; version: number } | undefined;
38
+ for (const f of files) {
39
+ const parsed = parseThemeFilename(f);
40
+ if (parsed && parsed.name === name) {
41
+ if (!best || parsed.version > best.version) {
42
+ best = { path: path.join(folder, f), version: parsed.version };
43
+ }
44
+ }
45
+ }
46
+ return best;
47
+ }
48
+
14
49
  export async function loadThemeInfo(name: string, config: SynthOSConfig): Promise<ThemeInfo | undefined> {
15
50
  // Check user's local themes first, then fall back to package defaults
16
51
  const localPath = path.join(userThemesFolder(config), `${name}.json`);
@@ -30,14 +65,14 @@ export async function loadThemeInfo(name: string, config: SynthOSConfig): Promis
30
65
 
31
66
  export async function loadTheme(name: string, config: SynthOSConfig): Promise<string | undefined> {
32
67
  // Check user's local themes first, then fall back to package defaults
33
- const localPath = path.join(userThemesFolder(config), `${name}.css`);
34
- if (await checkIfExists(localPath)) {
35
- return await loadFile(localPath);
68
+ const local = await findThemeCssFile(userThemesFolder(config), name);
69
+ if (local) {
70
+ return await loadFile(local.path);
36
71
  }
37
72
 
38
- const defaultPath = path.join(config.defaultThemesFolder, `${name}.css`);
39
- if (await checkIfExists(defaultPath)) {
40
- return await loadFile(defaultPath);
73
+ const def = await findThemeCssFile(config.defaultThemesFolder, name);
74
+ if (def) {
75
+ return await loadFile(def.path);
41
76
  }
42
77
 
43
78
  return undefined;
@@ -51,9 +86,8 @@ export async function listThemes(config: SynthOSConfig): Promise<string[]> {
51
86
  if (await checkIfExists(localFolder)) {
52
87
  const files = await listFiles(localFolder);
53
88
  for (const f of files) {
54
- if (f.endsWith('.css')) {
55
- names.add(f.replace(/\.css$/, ''));
56
- }
89
+ const parsed = parseThemeFilename(f);
90
+ if (parsed) names.add(parsed.name);
57
91
  }
58
92
  }
59
93
 
@@ -61,11 +95,49 @@ export async function listThemes(config: SynthOSConfig): Promise<string[]> {
61
95
  if (await checkIfExists(config.defaultThemesFolder)) {
62
96
  const files = await listFiles(config.defaultThemesFolder);
63
97
  for (const f of files) {
64
- if (f.endsWith('.css')) {
65
- names.add(f.replace(/\.css$/, ''));
66
- }
98
+ const parsed = parseThemeFilename(f);
99
+ if (parsed) names.add(parsed.name);
67
100
  }
68
101
  }
69
102
 
70
103
  return Array.from(names).sort();
71
104
  }
105
+
106
+ /**
107
+ * Compare local theme versions against defaults and return themes that need upgrading.
108
+ */
109
+ export async function getOutdatedThemes(config: SynthOSConfig): Promise<string[]> {
110
+ const localFolder = userThemesFolder(config);
111
+ if (!await checkIfExists(localFolder)) return [];
112
+
113
+ const defaultFiles = await listFiles(config.defaultThemesFolder);
114
+ const localFiles = await listFiles(localFolder);
115
+
116
+ // Build maps: theme name → highest version
117
+ const defaultVersions = new Map<string, number>();
118
+ for (const f of defaultFiles) {
119
+ const parsed = parseThemeFilename(f);
120
+ if (parsed) {
121
+ const cur = defaultVersions.get(parsed.name) ?? 0;
122
+ if (parsed.version > cur) defaultVersions.set(parsed.name, parsed.version);
123
+ }
124
+ }
125
+
126
+ const localVersions = new Map<string, number>();
127
+ for (const f of localFiles) {
128
+ const parsed = parseThemeFilename(f);
129
+ if (parsed) {
130
+ const cur = localVersions.get(parsed.name) ?? 0;
131
+ if (parsed.version > cur) localVersions.set(parsed.name, parsed.version);
132
+ }
133
+ }
134
+
135
+ const outdated: string[] = [];
136
+ for (const [name, defVer] of defaultVersions) {
137
+ const localVer = localVersions.get(name) ?? 0;
138
+ if (localVer < defVer) {
139
+ outdated.push(name);
140
+ }
141
+ }
142
+ return outdated;
143
+ }
@@ -0,0 +1,12 @@
1
+ # SynthOS Tests
2
+
3
+ ## Tier 1 (implemented)
4
+ - `transformPage.spec.ts` — assignNodeIds, stripNodeIds, applyChangeList, parseChangeList, injectError
5
+ - `pages.spec.ts` — normalizePageName, parseMetadata
6
+ - `migrations.spec.ts` — postProcessV2
7
+
8
+ ## Tier 2 (TODO)
9
+ - `scripts.spec.ts` — listScripts, loadScripts, saveScripts
10
+ - `modelInstructions.spec.ts` — getModelInstructions provider routing
11
+ - `debugLog.spec.ts` — log formatting and filtering
12
+ - `init.spec.ts` — folder creation and default file copying
@@ -0,0 +1,84 @@
1
+ import assert from 'assert';
2
+ import { buildAnthropicRequest } from '../src/models/anthropic';
3
+ import { PromptCompletionArgs } from '../src/models/types';
4
+
5
+ describe('buildAnthropicRequest', () => {
6
+ const baseArgs: PromptCompletionArgs = {
7
+ prompt: { role: 'user', content: 'Hello' },
8
+ };
9
+
10
+ it('builds messages from history + prompt', () => {
11
+ const args: PromptCompletionArgs = {
12
+ ...baseArgs,
13
+ history: [
14
+ { role: 'user', content: 'Hi' },
15
+ { role: 'assistant', content: 'Hey' },
16
+ ],
17
+ };
18
+ const { messages } = buildAnthropicRequest(args, 0.0);
19
+ assert.strictEqual(messages.length, 3);
20
+ assert.strictEqual(messages[0].role, 'user');
21
+ assert.strictEqual(messages[0].content, 'Hi');
22
+ assert.strictEqual(messages[1].role, 'assistant');
23
+ assert.strictEqual(messages[1].content, 'Hey');
24
+ assert.strictEqual(messages[2].role, 'user');
25
+ assert.strictEqual(messages[2].content, 'Hello');
26
+ });
27
+
28
+ it('appends assistant prefill "{" in jsonMode', () => {
29
+ const args: PromptCompletionArgs = { ...baseArgs, jsonMode: true };
30
+ const { messages } = buildAnthropicRequest(args, 0.0);
31
+ const last = messages[messages.length - 1];
32
+ assert.strictEqual(last.role, 'assistant');
33
+ assert.strictEqual(last.content, '{');
34
+ });
35
+
36
+ it('appends assistant prefill "{" when jsonSchema is provided', () => {
37
+ const args: PromptCompletionArgs = { ...baseArgs, jsonSchema: { type: 'object' } };
38
+ const { messages } = buildAnthropicRequest(args, 0.0);
39
+ const last = messages[messages.length - 1];
40
+ assert.strictEqual(last.role, 'assistant');
41
+ assert.strictEqual(last.content, '{');
42
+ });
43
+
44
+ it('does not prefill in plain text mode', () => {
45
+ const { messages } = buildAnthropicRequest(baseArgs, 0.0);
46
+ const last = messages[messages.length - 1];
47
+ assert.strictEqual(last.role, 'user');
48
+ assert.strictEqual(last.content, 'Hello');
49
+ });
50
+
51
+ it('injects jsonSchema into system content', () => {
52
+ const args: PromptCompletionArgs = {
53
+ ...baseArgs,
54
+ system: { role: 'system', content: 'Be helpful.' },
55
+ jsonSchema: { type: 'object', properties: { name: { type: 'string' } } },
56
+ };
57
+ const { system } = buildAnthropicRequest(args, 0.0);
58
+ assert.ok(system);
59
+ assert.ok(system.includes('Be helpful.'));
60
+ assert.ok(system.includes('JSON conforming to this schema'));
61
+ assert.ok(system.includes('"type":"object"'));
62
+ });
63
+
64
+ it('creates system content from schema alone when no system message', () => {
65
+ const args: PromptCompletionArgs = {
66
+ ...baseArgs,
67
+ jsonSchema: { type: 'object' },
68
+ };
69
+ const { system } = buildAnthropicRequest(args, 0.0);
70
+ assert.ok(system);
71
+ assert.ok(system.includes('JSON conforming to this schema'));
72
+ });
73
+
74
+ it('uses default temperature when none specified', () => {
75
+ const { temperature } = buildAnthropicRequest(baseArgs, 0.7);
76
+ assert.strictEqual(temperature, 0.7);
77
+ });
78
+
79
+ it('uses args temperature when specified', () => {
80
+ const args: PromptCompletionArgs = { ...baseArgs, temperature: 0.3 };
81
+ const { temperature } = buildAnthropicRequest(args, 0.7);
82
+ assert.strictEqual(temperature, 0.3);
83
+ });
84
+ });
@@ -0,0 +1,108 @@
1
+ import assert from 'assert';
2
+ import { chainOfThought, ChainOfThoughtArgs } from '../src/models/chainOfThought';
3
+ import { AgentCompletion, PromptCompletionArgs } from '../src/models/types';
4
+
5
+ /** Helper: build a stub completePrompt that returns the given result. */
6
+ function stubCompletePrompt(result: AgentCompletion<any>) {
7
+ return async (_args: PromptCompletionArgs) => result;
8
+ }
9
+
10
+ /** Helper: build a stub that captures the args it was called with. */
11
+ function capturingStub(result: AgentCompletion<any>) {
12
+ let captured: PromptCompletionArgs | undefined;
13
+ const fn = async (args: PromptCompletionArgs) => { captured = args; return result; };
14
+ return { fn, getCaptured: () => captured };
15
+ }
16
+
17
+ describe('chainOfThought', () => {
18
+ it('returns parsed {explanation, answer} from a valid JSON string response', async () => {
19
+ const stub = stubCompletePrompt({
20
+ completed: true,
21
+ value: '{"explanation": "because", "answer": "42"}',
22
+ });
23
+ const result = await chainOfThought({ completePrompt: stub, question: 'What?' });
24
+ assert.strictEqual(result.completed, true);
25
+ assert.deepStrictEqual(result.value, { explanation: 'because', answer: '42' });
26
+ });
27
+
28
+ it('returns parsed result when completePrompt returns a pre-parsed object', async () => {
29
+ const stub = stubCompletePrompt({
30
+ completed: true,
31
+ value: { explanation: 'reason', answer: 'yes' },
32
+ });
33
+ const result = await chainOfThought({ completePrompt: stub, question: 'Q?' });
34
+ assert.strictEqual(result.completed, true);
35
+ assert.deepStrictEqual(result.value, { explanation: 'reason', answer: 'yes' });
36
+ });
37
+
38
+ it('strips markdown code fences before parsing', async () => {
39
+ const stub = stubCompletePrompt({
40
+ completed: true,
41
+ value: '```json\n{"explanation": "x", "answer": "y"}\n```',
42
+ });
43
+ const result = await chainOfThought({ completePrompt: stub, question: 'Q?' });
44
+ assert.strictEqual(result.completed, true);
45
+ assert.deepStrictEqual(result.value, { explanation: 'x', answer: 'y' });
46
+ });
47
+
48
+ it('extracts JSON from text with surrounding prose', async () => {
49
+ const stub = stubCompletePrompt({
50
+ completed: true,
51
+ value: 'Here is my answer: {"explanation": "e", "answer": "a"} hope that helps!',
52
+ });
53
+ const result = await chainOfThought({ completePrompt: stub, question: 'Q?' });
54
+ assert.strictEqual(result.completed, true);
55
+ assert.deepStrictEqual(result.value, { explanation: 'e', answer: 'a' });
56
+ });
57
+
58
+ it('returns {completed: false} when completePrompt fails', async () => {
59
+ const stub = stubCompletePrompt({
60
+ completed: false,
61
+ error: new Error('API error'),
62
+ });
63
+ const result = await chainOfThought({ completePrompt: stub, question: 'Q?' });
64
+ assert.strictEqual(result.completed, false);
65
+ assert.strictEqual(result.error?.message, 'API error');
66
+ });
67
+
68
+ it('returns parse-error when response is not valid JSON', async () => {
69
+ const stub = stubCompletePrompt({
70
+ completed: true,
71
+ value: 'this is not json at all',
72
+ });
73
+ const result = await chainOfThought({ completePrompt: stub, question: 'Q?' });
74
+ assert.strictEqual(result.completed, false);
75
+ assert.ok(result.error?.message.includes('parse'));
76
+ });
77
+
78
+ it('includes custom instructions in system prompt when provided', async () => {
79
+ const { fn, getCaptured } = capturingStub({
80
+ completed: true,
81
+ value: '{"explanation": "", "answer": ""}',
82
+ });
83
+ await chainOfThought({ completePrompt: fn, question: 'Q?', instructions: 'Be concise.' });
84
+ const system = getCaptured()!.system!;
85
+ assert.ok(system.content.startsWith('Be concise.'));
86
+ assert.ok(system.content.includes('JSON object'));
87
+ });
88
+
89
+ it('uses default system prompt when no instructions given', async () => {
90
+ const { fn, getCaptured } = capturingStub({
91
+ completed: true,
92
+ value: '{"explanation": "", "answer": ""}',
93
+ });
94
+ await chainOfThought({ completePrompt: fn, question: 'Q?' });
95
+ const system = getCaptured()!.system!;
96
+ assert.ok(system.content.startsWith('You must return'));
97
+ });
98
+
99
+ it('defaults missing explanation/answer to empty strings', async () => {
100
+ const stub = stubCompletePrompt({
101
+ completed: true,
102
+ value: '{}',
103
+ });
104
+ const result = await chainOfThought({ completePrompt: stub, question: 'Q?' });
105
+ assert.strictEqual(result.completed, true);
106
+ assert.deepStrictEqual(result.value, { explanation: '', answer: '' });
107
+ });
108
+ });
@@ -0,0 +1,82 @@
1
+ import assert from 'assert';
2
+ import { ensureScriptsBeforeBodyClose } from '../src/service/transformPage';
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // ensureScriptsBeforeBodyClose
6
+ // ---------------------------------------------------------------------------
7
+
8
+ describe('ensureScriptsBeforeBodyClose', () => {
9
+ it('moves page-helpers and page-script to end of body in correct order', () => {
10
+ const html =
11
+ '<html><head></head><body>' +
12
+ '<script id="page-script" src="/api/page-script.js?v=2"></script>' +
13
+ '<div>content</div>' +
14
+ '<script id="page-helpers" src="/api/page-helpers.js?v=2"></script>' +
15
+ '</body></html>';
16
+
17
+ const result = ensureScriptsBeforeBodyClose(html);
18
+
19
+ // Both scripts should be at the end of body
20
+ const bodyContent = result.match(/<body>([\s\S]*)<\/body>/)?.[1] ?? '';
21
+ const helpersIdx = bodyContent.lastIndexOf('id="page-helpers"');
22
+ const scriptIdx = bodyContent.lastIndexOf('id="page-script"');
23
+ const contentIdx = bodyContent.indexOf('<div>content</div>');
24
+
25
+ // Content should come before both scripts
26
+ assert.ok(contentIdx < helpersIdx, 'content should be before page-helpers');
27
+ // Helpers should come before page-script
28
+ assert.ok(helpersIdx < scriptIdx, 'page-helpers should be before page-script');
29
+ });
30
+
31
+ it('handles missing page-helpers gracefully', () => {
32
+ const html =
33
+ '<html><head></head><body>' +
34
+ '<div>content</div>' +
35
+ '<script id="page-script" src="/api/page-script.js?v=2"></script>' +
36
+ '</body></html>';
37
+
38
+ const result = ensureScriptsBeforeBodyClose(html);
39
+ assert.ok(result.includes('id="page-script"'));
40
+ assert.ok(result.includes('<div>content</div>'));
41
+ });
42
+
43
+ it('handles missing page-script gracefully', () => {
44
+ const html =
45
+ '<html><head></head><body>' +
46
+ '<div>content</div>' +
47
+ '<script id="page-helpers" src="/api/page-helpers.js?v=2"></script>' +
48
+ '</body></html>';
49
+
50
+ const result = ensureScriptsBeforeBodyClose(html);
51
+ assert.ok(result.includes('id="page-helpers"'));
52
+ assert.ok(result.includes('<div>content</div>'));
53
+ });
54
+
55
+ it('handles both scripts missing gracefully', () => {
56
+ const html = '<html><head></head><body><div>content</div></body></html>';
57
+ const result = ensureScriptsBeforeBodyClose(html);
58
+ assert.ok(result.includes('<div>content</div>'));
59
+ });
60
+
61
+ it('returns HTML unchanged when no body tag', () => {
62
+ const html = '<div>no body tag</div>';
63
+ const result = ensureScriptsBeforeBodyClose(html);
64
+ // cheerio wraps in html/body, so just check the content is preserved
65
+ assert.ok(result.includes('no body tag'));
66
+ });
67
+
68
+ it('does nothing when scripts are already at end of body', () => {
69
+ const html =
70
+ '<html><head></head><body>' +
71
+ '<div>content</div>' +
72
+ '<script id="page-helpers" src="/api/page-helpers.js?v=2"></script>' +
73
+ '<script id="page-script" src="/api/page-script.js?v=2"></script>' +
74
+ '</body></html>';
75
+
76
+ const result = ensureScriptsBeforeBodyClose(html);
77
+ const bodyContent = result.match(/<body>([\s\S]*)<\/body>/)?.[1] ?? '';
78
+ const helpersIdx = bodyContent.lastIndexOf('id="page-helpers"');
79
+ const scriptIdx = bodyContent.lastIndexOf('id="page-script"');
80
+ assert.ok(helpersIdx < scriptIdx);
81
+ });
82
+ });