synthos 0.7.2 → 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 (262) 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 +8 -5
  173. package/page-scripts/helpers-v2.js +101 -0
  174. package/page-scripts/page-v2.js +47 -6
  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/anthropic.spec.ts +84 -0
  241. package/tests/chainOfThought.spec.ts +108 -0
  242. package/tests/ensureScripts.spec.ts +82 -0
  243. package/tests/files.spec.ts +233 -0
  244. package/tests/fireworksai.spec.ts +92 -0
  245. package/tests/logCompletePrompt.spec.ts +74 -0
  246. package/tests/migrations.spec.ts +79 -1
  247. package/tests/openai.spec.ts +71 -0
  248. package/tests/pages.spec.ts +226 -1
  249. package/tests/providers.spec.ts +144 -0
  250. package/tests/scripts.spec.ts +209 -0
  251. package/tests/transformPage.spec.ts +517 -0
  252. package/tests/types.spec.ts +23 -0
  253. package/default-pages/app_builder.json +0 -1
  254. package/default-pages/sidebar_builder.json +0 -1
  255. package/default-pages/two-panel_builder.json +0 -1
  256. package/images/home.png +0 -0
  257. package/images/page-management.png +0 -0
  258. package/images/settings.png +0 -0
  259. package/images/synthos-square.png +0 -0
  260. /package/default-pages/{app_builder.html → application.html} +0 -0
  261. /package/default-pages/{sidebar_builder.html → sidebar_page.html} +0 -0
  262. /package/default-pages/{two-panel_builder.html → two-panel_page.html} +0 -0
@@ -1,5 +1,20 @@
1
1
  import assert from 'assert';
2
- import { normalizePageName, parseMetadata } from '../src/pages';
2
+ import * as fs from 'fs/promises';
3
+ import * as os from 'os';
4
+ import path from 'path';
5
+ import {
6
+ normalizePageName,
7
+ parseMetadata,
8
+ savePageMetadata,
9
+ loadPageMetadata,
10
+ savePageState,
11
+ loadPageState,
12
+ updatePageState,
13
+ listPages,
14
+ deletePage,
15
+ copyPage,
16
+ PAGE_VERSION,
17
+ } from '../src/pages';
3
18
 
4
19
  // ---------------------------------------------------------------------------
5
20
  // normalizePageName
@@ -101,3 +116,213 @@ describe('parseMetadata', () => {
101
116
  assert.strictEqual(parseMetadata({ mode: 123 }).mode, 'unlocked');
102
117
  });
103
118
  });
119
+
120
+ // ---------------------------------------------------------------------------
121
+ // Page I/O (temp-dir tests)
122
+ // ---------------------------------------------------------------------------
123
+
124
+ describe('page I/O', () => {
125
+ let tmpDir: string;
126
+ let pagesFolder: string;
127
+ let fallbackFolder: string;
128
+
129
+ beforeEach(async () => {
130
+ tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'synthos-test-'));
131
+ pagesFolder = path.join(tmpDir, 'synthos');
132
+ fallbackFolder = path.join(tmpDir, 'fallback');
133
+ await fs.mkdir(pagesFolder, { recursive: true });
134
+ await fs.mkdir(fallbackFolder, { recursive: true });
135
+ });
136
+
137
+ afterEach(async () => {
138
+ await fs.rm(tmpDir, { recursive: true, force: true });
139
+ });
140
+
141
+ // -- savePageMetadata / loadPageMetadata ---------------------------------
142
+
143
+ describe('savePageMetadata / loadPageMetadata', () => {
144
+ it('roundtrips metadata correctly', async () => {
145
+ const meta = {
146
+ title: 'Test Page',
147
+ categories: ['Tools', 'Dev'],
148
+ pinned: true,
149
+ showInAll: true,
150
+ createdDate: '2025-01-01T00:00:00Z',
151
+ lastModified: '2025-06-01T00:00:00Z',
152
+ pageVersion: 2,
153
+ mode: 'locked' as const,
154
+ };
155
+ await savePageMetadata(pagesFolder, 'mypage', meta);
156
+ const loaded = await loadPageMetadata(pagesFolder, 'mypage');
157
+ assert.ok(loaded);
158
+ assert.strictEqual(loaded.title, 'Test Page');
159
+ assert.deepStrictEqual(loaded.categories, ['Tools', 'Dev']);
160
+ assert.strictEqual(loaded.pinned, true);
161
+ assert.strictEqual(loaded.pageVersion, 2);
162
+ assert.strictEqual(loaded.mode, 'locked');
163
+ });
164
+
165
+ it('returns undefined when no metadata file exists', async () => {
166
+ const loaded = await loadPageMetadata(pagesFolder, 'nonexistent');
167
+ assert.strictEqual(loaded, undefined);
168
+ });
169
+
170
+ it('falls back to fallbackFolder', async () => {
171
+ // Write a fallback JSON file
172
+ await fs.writeFile(
173
+ path.join(fallbackFolder, 'system-page.json'),
174
+ JSON.stringify({ title: 'System', categories: ['System'], pinned: true, pageVersion: 2, mode: 'locked' }),
175
+ );
176
+ const loaded = await loadPageMetadata(pagesFolder, 'system-page', fallbackFolder);
177
+ assert.ok(loaded);
178
+ assert.strictEqual(loaded.title, 'System');
179
+ });
180
+ });
181
+
182
+ // -- savePageState / loadPageState ---------------------------------------
183
+
184
+ describe('savePageState / loadPageState', () => {
185
+ it('roundtrips page HTML and creates metadata', async () => {
186
+ const html = '<html><body>Hello</body></html>';
187
+ await savePageState(pagesFolder, 'testpage', html, 'Test Title', ['Cat']);
188
+ const loaded = await loadPageState(pagesFolder, 'testpage', true);
189
+ assert.strictEqual(loaded, html);
190
+
191
+ // Metadata should have been created
192
+ const meta = await loadPageMetadata(pagesFolder, 'testpage');
193
+ assert.ok(meta);
194
+ assert.strictEqual(meta.pageVersion, PAGE_VERSION);
195
+ });
196
+
197
+ it('returns undefined for non-existent page', async () => {
198
+ const loaded = await loadPageState(pagesFolder, 'nope', false);
199
+ assert.strictEqual(loaded, undefined);
200
+ });
201
+
202
+ it('with reset=true re-reads from disk', async () => {
203
+ const html1 = '<html>v1</html>';
204
+ const html2 = '<html>v2</html>';
205
+ await savePageState(pagesFolder, 'resetpage', html1);
206
+ // First load
207
+ const first = await loadPageState(pagesFolder, 'resetpage', false);
208
+ assert.strictEqual(first, html1);
209
+
210
+ // Overwrite the file on disk directly
211
+ const pageHtmlPath = path.join(pagesFolder, 'pages', 'resetpage', 'page.html');
212
+ await fs.writeFile(pageHtmlPath, html2);
213
+
214
+ // Without reset, should return cached version
215
+ const cached = await loadPageState(pagesFolder, 'resetpage', false);
216
+ assert.strictEqual(cached, html1);
217
+
218
+ // With reset, should re-read
219
+ const refreshed = await loadPageState(pagesFolder, 'resetpage', true);
220
+ assert.strictEqual(refreshed, html2);
221
+ });
222
+ });
223
+
224
+ // -- updatePageState -----------------------------------------------------
225
+
226
+ describe('updatePageState', () => {
227
+ it('updates in-memory cache', async () => {
228
+ await savePageState(pagesFolder, 'cachepage', '<html>original</html>');
229
+ await loadPageState(pagesFolder, 'cachepage', false);
230
+
231
+ updatePageState('cachepage', '<html>updated</html>');
232
+
233
+ // Loading without reset should return updated cache
234
+ const loaded = await loadPageState(pagesFolder, 'cachepage', false);
235
+ assert.strictEqual(loaded, '<html>updated</html>');
236
+ });
237
+ });
238
+
239
+ // -- listPages -----------------------------------------------------------
240
+
241
+ describe('listPages', () => {
242
+ it('lists folder-based pages', async () => {
243
+ await savePageState(pagesFolder, 'alpha', '<html>A</html>');
244
+ await savePageState(pagesFolder, 'beta', '<html>B</html>');
245
+ const pages = await listPages(pagesFolder, fallbackFolder);
246
+ const names = pages.map(p => p.name);
247
+ assert.ok(names.includes('alpha'));
248
+ assert.ok(names.includes('beta'));
249
+ });
250
+
251
+ it('lists legacy flat HTML files', async () => {
252
+ await fs.writeFile(path.join(pagesFolder, 'legacy.html'), '<html>Legacy</html>');
253
+ const pages = await listPages(pagesFolder, fallbackFolder);
254
+ const legacy = pages.find(p => p.name === 'legacy');
255
+ assert.ok(legacy);
256
+ assert.strictEqual(legacy.pageVersion, 1);
257
+ });
258
+
259
+ it('lists fallback (required) pages', async () => {
260
+ await fs.writeFile(path.join(fallbackFolder, 'builder.html'), '<html>Builder</html>');
261
+ const pages = await listPages(pagesFolder, fallbackFolder);
262
+ const builder = pages.find(p => p.name === 'builder');
263
+ assert.ok(builder);
264
+ });
265
+
266
+ it('returns pages sorted alphabetically', async () => {
267
+ await savePageState(pagesFolder, 'zebra', '<html></html>');
268
+ await savePageState(pagesFolder, 'apple', '<html></html>');
269
+ const pages = await listPages(pagesFolder, fallbackFolder);
270
+ const names = pages.map(p => p.name);
271
+ const sortedNames = [...names].sort();
272
+ assert.deepStrictEqual(names, sortedNames);
273
+ });
274
+ });
275
+
276
+ // -- deletePage ----------------------------------------------------------
277
+
278
+ describe('deletePage', () => {
279
+ it('removes folder-based page and clears cache', async () => {
280
+ await savePageState(pagesFolder, 'doomed', '<html>bye</html>');
281
+ await loadPageState(pagesFolder, 'doomed', false);
282
+
283
+ await deletePage(pagesFolder, 'doomed');
284
+
285
+ // Folder should be gone
286
+ const folderExists = await fs.access(path.join(pagesFolder, 'pages', 'doomed'))
287
+ .then(() => true).catch(() => false);
288
+ assert.strictEqual(folderExists, false);
289
+
290
+ // Cache should be cleared — loading should return undefined
291
+ const loaded = await loadPageState(pagesFolder, 'doomed', true);
292
+ assert.strictEqual(loaded, undefined);
293
+ });
294
+ });
295
+
296
+ // -- copyPage ------------------------------------------------------------
297
+
298
+ describe('copyPage', () => {
299
+ it('copies HTML and creates metadata with correct title/categories', async () => {
300
+ await savePageState(pagesFolder, 'source', '<html>Source</html>');
301
+ await copyPage(pagesFolder, 'source', 'target', 'Copied Page', ['Copy'], fallbackFolder);
302
+
303
+ const html = await loadPageState(pagesFolder, 'target', true);
304
+ assert.strictEqual(html, '<html>Source</html>');
305
+
306
+ const meta = await loadPageMetadata(pagesFolder, 'target');
307
+ assert.ok(meta);
308
+ assert.strictEqual(meta.title, 'Copied Page');
309
+ assert.deepStrictEqual(meta.categories, ['Copy']);
310
+ assert.strictEqual(meta.pageVersion, PAGE_VERSION);
311
+ });
312
+
313
+ it('throws when source page does not exist', async () => {
314
+ await assert.rejects(
315
+ () => copyPage(pagesFolder, 'ghost', 'target', 'T', [], fallbackFolder),
316
+ /Source page "ghost" not found/,
317
+ );
318
+ });
319
+
320
+ it('copies from required pages folder as fallback', async () => {
321
+ await fs.writeFile(path.join(fallbackFolder, 'system.html'), '<html>System</html>');
322
+ await copyPage(pagesFolder, 'system', 'mycopy', 'My Copy', ['User'], fallbackFolder);
323
+
324
+ const html = await loadPageState(pagesFolder, 'mycopy', true);
325
+ assert.strictEqual(html, '<html>System</html>');
326
+ });
327
+ });
328
+ });
@@ -0,0 +1,144 @@
1
+ import assert from 'assert';
2
+ import {
3
+ getProvider,
4
+ detectProvider,
5
+ AnthropicProvider,
6
+ OpenAIProvider,
7
+ } from '../src/models/providers';
8
+ import { variableToString } from '../src/models/utils';
9
+ import { extractJSON } from '../src/models/fireworksai';
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // getProvider
13
+ // ---------------------------------------------------------------------------
14
+
15
+ describe('getProvider', () => {
16
+ it('returns Anthropic provider by name', () => {
17
+ const p = getProvider('Anthropic');
18
+ assert.strictEqual(p.name, 'Anthropic');
19
+ });
20
+
21
+ it('returns OpenAI provider by name', () => {
22
+ const p = getProvider('OpenAI');
23
+ assert.strictEqual(p.name, 'OpenAI');
24
+ });
25
+
26
+ it('returns FireworksAI provider by name', () => {
27
+ const p = getProvider('FireworksAI');
28
+ assert.strictEqual(p.name, 'FireworksAI');
29
+ });
30
+
31
+ it('throws on unknown provider name', () => {
32
+ assert.throws(() => getProvider('Unknown' as any), /Unknown provider/);
33
+ });
34
+ });
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // detectProvider
38
+ // ---------------------------------------------------------------------------
39
+
40
+ describe('detectProvider', () => {
41
+ it('detects Anthropic from claude- prefix', () => {
42
+ const p = detectProvider('claude-sonnet-4-5');
43
+ assert.strictEqual(p?.name, 'Anthropic');
44
+ });
45
+
46
+ it('detects OpenAI from gpt- prefix', () => {
47
+ const p = detectProvider('gpt-5-mini');
48
+ assert.strictEqual(p?.name, 'OpenAI');
49
+ });
50
+
51
+ it('detects FireworksAI from fireworks- prefix', () => {
52
+ const p = detectProvider('fireworks-glm-5');
53
+ assert.strictEqual(p?.name, 'FireworksAI');
54
+ });
55
+
56
+ it('returns undefined for unknown model', () => {
57
+ assert.strictEqual(detectProvider('llama-3'), undefined);
58
+ });
59
+ });
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // variableToString
63
+ // ---------------------------------------------------------------------------
64
+
65
+ describe('variableToString', () => {
66
+ it('returns empty string for null', () => {
67
+ assert.strictEqual(variableToString(null), '');
68
+ });
69
+
70
+ it('returns empty string for undefined', () => {
71
+ assert.strictEqual(variableToString(undefined), '');
72
+ });
73
+
74
+ it('passes through strings unchanged', () => {
75
+ assert.strictEqual(variableToString('hello'), 'hello');
76
+ });
77
+
78
+ it('serializes objects to JSON', () => {
79
+ assert.strictEqual(variableToString({ a: 1 }), '{"a":1}');
80
+ });
81
+
82
+ it('converts numbers via toString', () => {
83
+ assert.strictEqual(variableToString(42), '42');
84
+ });
85
+
86
+ it('converts booleans via toString', () => {
87
+ assert.strictEqual(variableToString(true), 'true');
88
+ });
89
+
90
+ it('serializes arrays to JSON', () => {
91
+ assert.strictEqual(variableToString([1, 2]), '[1,2]');
92
+ });
93
+ });
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // extractJSON
97
+ // ---------------------------------------------------------------------------
98
+
99
+ describe('extractJSON', () => {
100
+ it('extracts JSON from markdown code fences', () => {
101
+ const input = '```json\n{"key": "value"}\n```';
102
+ assert.strictEqual(extractJSON(input), '{"key": "value"}');
103
+ });
104
+
105
+ it('extracts JSON from fences without language tag', () => {
106
+ const input = '```\n[1, 2, 3]\n```';
107
+ assert.strictEqual(extractJSON(input), '[1, 2, 3]');
108
+ });
109
+
110
+ it('extracts a raw JSON object from surrounding text', () => {
111
+ const input = 'Here is the result: {"a": 1} end.';
112
+ const result = extractJSON(input);
113
+ assert.strictEqual(result, '{"a": 1}');
114
+ });
115
+
116
+ it('extracts a raw JSON array from surrounding text', () => {
117
+ const input = 'Result: [{"op":"update"}] done.';
118
+ const result = extractJSON(input);
119
+ assert.strictEqual(result, '[{"op":"update"}]');
120
+ });
121
+
122
+ it('handles nested braces correctly', () => {
123
+ const input = 'output: {"a": {"b": {"c": 1}}} end';
124
+ const result = extractJSON(input);
125
+ assert.strictEqual(result, '{"a": {"b": {"c": 1}}}');
126
+ });
127
+
128
+ it('handles escaped strings inside JSON', () => {
129
+ const input = '{"msg": "hello \\"world\\""}';
130
+ const result = extractJSON(input);
131
+ assert.strictEqual(result, '{"msg": "hello \\"world\\""}');
132
+ });
133
+
134
+ it('returns original text when no JSON found', () => {
135
+ const input = 'no json here at all';
136
+ assert.strictEqual(extractJSON(input), 'no json here at all');
137
+ });
138
+
139
+ it('handles braces inside JSON strings (not counting them)', () => {
140
+ const input = '{"text": "a { b } c"}';
141
+ const result = extractJSON(input);
142
+ assert.strictEqual(result, '{"text": "a { b } c"}');
143
+ });
144
+ });
@@ -0,0 +1,209 @@
1
+ import assert from 'assert';
2
+ import * as fs from 'fs/promises';
3
+ import * as os from 'os';
4
+ import path from 'path';
5
+ import { composeArguments } from '../src/scripts';
6
+ import { listScripts, clearCachedScripts, executeScript } from '../src/scripts';
7
+
8
+ let tmpDir: string;
9
+
10
+ beforeEach(async () => {
11
+ tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'synthos-scripts-test-'));
12
+ });
13
+
14
+ afterEach(async () => {
15
+ clearCachedScripts();
16
+ await fs.rm(tmpDir, { recursive: true, force: true });
17
+ });
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // composeArguments
21
+ // ---------------------------------------------------------------------------
22
+
23
+ describe('composeArguments', () => {
24
+ it('substitutes {{var}} with variable value', () => {
25
+ const result = composeArguments('echo {{name}}', { name: 'world' });
26
+ assert.strictEqual(result, 'echo world');
27
+ });
28
+
29
+ it('handles multiple variables', () => {
30
+ const result = composeArguments('{{a}} and {{b}}', { a: 'X', b: 'Y' });
31
+ assert.strictEqual(result, 'X and Y');
32
+ });
33
+
34
+ it('replaces missing vars with empty string', () => {
35
+ const result = composeArguments('hello {{missing}}!', {});
36
+ assert.strictEqual(result, 'hello !');
37
+ });
38
+
39
+ it('handles spaces inside braces', () => {
40
+ const result = composeArguments('{{ name }}', { name: 'trimmed' });
41
+ assert.strictEqual(result, 'trimmed');
42
+ });
43
+
44
+ it('converts object values to JSON strings', () => {
45
+ const result = composeArguments('data: {{obj}}', { obj: { key: 'val' } });
46
+ assert.strictEqual(result, 'data: {"key":"val"}');
47
+ });
48
+
49
+ it('converts number values to strings', () => {
50
+ const result = composeArguments('count: {{n}}', { n: 42 });
51
+ assert.strictEqual(result, 'count: 42');
52
+ });
53
+ });
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // listScripts
57
+ // ---------------------------------------------------------------------------
58
+
59
+ describe('listScripts', () => {
60
+ it('returns formatted script list from JSON files', async () => {
61
+ const scriptsDir = path.join(tmpDir, 'pages', 'scripts', 'scripts');
62
+ await fs.mkdir(scriptsDir, { recursive: true });
63
+
64
+ const script = {
65
+ id: 'test-script',
66
+ type: 'command',
67
+ command: 'echo hello',
68
+ description: 'A test script',
69
+ variables: 'name: string',
70
+ response: 'string',
71
+ };
72
+ await fs.writeFile(path.join(scriptsDir, 'test-script.json'), JSON.stringify(script));
73
+
74
+ const result = await listScripts(tmpDir);
75
+ assert.ok(result.includes('POST /api/scripts/test-script'));
76
+ assert.ok(result.includes('description: A test script'));
77
+ assert.ok(result.includes('request: name: string'));
78
+ assert.ok(result.includes('response: string'));
79
+ });
80
+
81
+ it('caches results on second call', async () => {
82
+ const scriptsDir = path.join(tmpDir, 'pages', 'scripts', 'scripts');
83
+ await fs.mkdir(scriptsDir, { recursive: true });
84
+
85
+ const script = { id: 'cached', type: 'command', command: 'echo cached' };
86
+ await fs.writeFile(path.join(scriptsDir, 'cached.json'), JSON.stringify(script));
87
+
88
+ const first = await listScripts(tmpDir);
89
+ // Add another file — should NOT appear due to caching
90
+ const script2 = { id: 'new', type: 'command', command: 'echo new' };
91
+ await fs.writeFile(path.join(scriptsDir, 'new.json'), JSON.stringify(script2));
92
+ const second = await listScripts(tmpDir);
93
+
94
+ assert.strictEqual(first, second);
95
+ assert.ok(!second.includes('new'));
96
+ });
97
+
98
+ it('returns empty string when scripts folder does not exist', async () => {
99
+ const result = await listScripts(tmpDir);
100
+ assert.strictEqual(result, '');
101
+ });
102
+ });
103
+
104
+ // ---------------------------------------------------------------------------
105
+ // clearCachedScripts
106
+ // ---------------------------------------------------------------------------
107
+
108
+ describe('clearCachedScripts', () => {
109
+ it('invalidates cache so next listScripts re-reads from disk', async () => {
110
+ const scriptsDir = path.join(tmpDir, 'pages', 'scripts', 'scripts');
111
+ await fs.mkdir(scriptsDir, { recursive: true });
112
+
113
+ const script = { id: 'original', type: 'command', command: 'echo original' };
114
+ await fs.writeFile(path.join(scriptsDir, 'original.json'), JSON.stringify(script));
115
+
116
+ const first = await listScripts(tmpDir);
117
+ assert.ok(first.includes('original'));
118
+
119
+ clearCachedScripts();
120
+
121
+ // Add a new script
122
+ const script2 = { id: 'added', type: 'command', command: 'echo added' };
123
+ await fs.writeFile(path.join(scriptsDir, 'added.json'), JSON.stringify(script2));
124
+
125
+ const second = await listScripts(tmpDir);
126
+ assert.ok(second.includes('added'));
127
+ });
128
+ });
129
+
130
+ // ---------------------------------------------------------------------------
131
+ // executeScript
132
+ // ---------------------------------------------------------------------------
133
+
134
+ describe('executeScript', () => {
135
+ it('executes a simple command and returns output', async () => {
136
+ const scriptsDir = path.join(tmpDir, 'pages', 'scripts', 'scripts');
137
+ await fs.mkdir(scriptsDir, { recursive: true });
138
+
139
+ const script = { id: 'hello', type: 'command', command: 'node -e "console.log(\'hello\')"' };
140
+ await fs.writeFile(path.join(scriptsDir, 'hello.json'), JSON.stringify(script));
141
+
142
+ const result = await executeScript({
143
+ pagesFolder: tmpDir,
144
+ scriptId: 'hello',
145
+ variables: {},
146
+ });
147
+
148
+ assert.strictEqual(result.completed, true);
149
+ assert.ok(result.value!.output.includes('hello'));
150
+ });
151
+
152
+ it('throws on missing script file', async () => {
153
+ await assert.rejects(
154
+ () => executeScript({
155
+ pagesFolder: tmpDir,
156
+ scriptId: 'nonexistent',
157
+ variables: {},
158
+ }),
159
+ /ENOENT/,
160
+ );
161
+ });
162
+
163
+ it('returns error for invalid JSON', async () => {
164
+ const scriptsDir = path.join(tmpDir, 'pages', 'scripts', 'scripts');
165
+ await fs.mkdir(scriptsDir, { recursive: true });
166
+ await fs.writeFile(path.join(scriptsDir, 'bad.json'), 'not valid json{{{');
167
+
168
+ const result = await executeScript({
169
+ pagesFolder: tmpDir,
170
+ scriptId: 'bad',
171
+ variables: {},
172
+ });
173
+ assert.strictEqual(result.completed, false);
174
+ assert.ok(result.error);
175
+ });
176
+
177
+ it('returns error for empty command', async () => {
178
+ const scriptsDir = path.join(tmpDir, 'pages', 'scripts', 'scripts');
179
+ await fs.mkdir(scriptsDir, { recursive: true });
180
+
181
+ const script = { id: 'empty', type: 'command', command: '' };
182
+ await fs.writeFile(path.join(scriptsDir, 'empty.json'), JSON.stringify(script));
183
+
184
+ const result = await executeScript({
185
+ pagesFolder: tmpDir,
186
+ scriptId: 'empty',
187
+ variables: {},
188
+ });
189
+ assert.strictEqual(result.completed, false);
190
+ assert.ok(result.error?.message.includes('No command'));
191
+ });
192
+
193
+ it('substitutes variables in the command', async () => {
194
+ const scriptsDir = path.join(tmpDir, 'pages', 'scripts', 'scripts');
195
+ await fs.mkdir(scriptsDir, { recursive: true });
196
+
197
+ const script = { id: 'greet', type: 'command', command: 'node -e "console.log(\'{{msg}}\')"' };
198
+ await fs.writeFile(path.join(scriptsDir, 'greet.json'), JSON.stringify(script));
199
+
200
+ const result = await executeScript({
201
+ pagesFolder: tmpDir,
202
+ scriptId: 'greet',
203
+ variables: { msg: 'hi there' },
204
+ });
205
+
206
+ assert.strictEqual(result.completed, true);
207
+ assert.ok(result.value!.output.includes('hi there'));
208
+ });
209
+ });