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
@@ -0,0 +1,423 @@
1
+ import { Application } from 'express';
2
+ import { SynthOSConfig } from '../init';
3
+ import { loadSettings, saveSettings } from '../settings';
4
+ import {
5
+ AgentConfig,
6
+ AgentProvider,
7
+ a2aProvider,
8
+ openclawProvider,
9
+ discoverA2AAgent,
10
+ discoverOpenClawAgent,
11
+ connectAgent,
12
+ disconnectAgent,
13
+ getAgentStatus,
14
+ getTunnelStatus,
15
+ } from '../agents';
16
+ import { v4 as uuidv4 } from 'uuid';
17
+
18
+ export function useAgentRoutes(config: SynthOSConfig, app: Application): void {
19
+
20
+ /** Strip the token and sshTunnel.password fields, add connection/tunnel status for agent responses. */
21
+ function toClientAgent(agent: AgentConfig): Record<string, unknown> {
22
+ const { token: _, sshTunnel, ...rest } = agent;
23
+ const status = agent.provider === 'openclaw' ? getAgentStatus(agent.id) : undefined;
24
+ const tunnelStatus = sshTunnel?.enabled ? getTunnelStatus(agent.id) : undefined;
25
+ // Expose sshTunnel config without the password
26
+ const sshTunnelClient = sshTunnel ? { enabled: sshTunnel.enabled, command: sshTunnel.command } : undefined;
27
+ return {
28
+ ...rest,
29
+ ...(sshTunnelClient ? { sshTunnel: sshTunnelClient } : {}),
30
+ ...(status ? { connected: status.connected && status.authenticated } : {}),
31
+ ...(tunnelStatus ? { tunnelStatus } : {}),
32
+ };
33
+ }
34
+
35
+ /** Try to connect an OpenClaw agent (fire-and-forget, logs errors). */
36
+ function tryConnectAgent(agent: AgentConfig): void {
37
+ if (agent.provider !== 'openclaw' || !agent.token || !agent.enabled) return;
38
+ connectAgent({
39
+ id: agent.id,
40
+ name: agent.name,
41
+ url: agent.url,
42
+ token: agent.token,
43
+ sshTunnel: agent.sshTunnel,
44
+ })
45
+ .then(() => console.log(`[Agents] Auto-connected OpenClaw agent "${agent.name}"`))
46
+ .catch(err => console.warn(`[Agents] Auto-connect failed for "${agent.name}": ${err instanceof Error ? err.message : err}`));
47
+ }
48
+
49
+ // Auto-connect all enabled OpenClaw agents on startup
50
+ (async () => {
51
+ try {
52
+ const settings = await loadSettings(config.pagesFolder);
53
+ for (const agent of settings.agents ?? []) {
54
+ tryConnectAgent(agent);
55
+ }
56
+ } catch (err) {
57
+ console.warn('[Agents] Failed to auto-connect agents on startup:', err);
58
+ }
59
+ })();
60
+
61
+ // GET /api/agents — List configured agents (with optional filters)
62
+ app.get('/api/agents', async (req, res) => {
63
+ try {
64
+ const settings = await loadSettings(config.pagesFolder);
65
+ let agents = settings.agents ?? [];
66
+
67
+ // Filter by enabled
68
+ if (req.query.enabled === 'true') {
69
+ agents = agents.filter(a => a.enabled);
70
+ }
71
+
72
+ // Filter by provider
73
+ if (typeof req.query.provider === 'string') {
74
+ agents = agents.filter(a => a.provider === req.query.provider);
75
+ }
76
+
77
+ res.json(agents.map(toClientAgent));
78
+ } catch (err: unknown) {
79
+ console.error(err);
80
+ res.status(500).json({ error: (err as Error).message });
81
+ }
82
+ });
83
+
84
+ // POST /api/agents/discover — Discover agent by URL + type
85
+ app.post('/api/agents/discover', async (req, res) => {
86
+ try {
87
+ const { url, type, token } = req.body;
88
+ if (!url || typeof url !== 'string') {
89
+ res.status(400).json({ error: 'url is required' });
90
+ return;
91
+ }
92
+
93
+ if (type === 'openclaw') {
94
+ if (!token || typeof token !== 'string') {
95
+ res.status(400).json({ error: 'token is required for OpenClaw discovery' });
96
+ return;
97
+ }
98
+ const result = await discoverOpenClawAgent(url, token);
99
+ res.json(result);
100
+ } else {
101
+ // Default to A2A
102
+ const card = await discoverA2AAgent(url);
103
+ res.json(card);
104
+ }
105
+ } catch (err: unknown) {
106
+ console.error(err);
107
+ res.status(502).json({ error: `Failed to discover agent: ${(err as Error).message}` });
108
+ }
109
+ });
110
+
111
+ // POST /api/agents — Upsert agent config
112
+ app.post('/api/agents', async (req, res) => {
113
+ try {
114
+ const body = req.body as Partial<AgentConfig>;
115
+ if (!body.url || !body.name) {
116
+ res.status(400).json({ error: 'url and name are required' });
117
+ return;
118
+ }
119
+
120
+ const settings = await loadSettings(config.pagesFolder);
121
+ const agents = settings.agents ? [...settings.agents] : [];
122
+
123
+ const agentConfig: AgentConfig = {
124
+ id: body.id || uuidv4(),
125
+ url: body.url,
126
+ name: body.name,
127
+ description: body.description || '',
128
+ iconUrl: body.iconUrl,
129
+ enabled: body.enabled ?? true,
130
+ provider: body.provider ?? 'a2a',
131
+ token: body.token,
132
+ sessionKey: body.sessionKey,
133
+ capabilities: body.capabilities,
134
+ skills: body.skills,
135
+ sshTunnel: body.sshTunnel,
136
+ };
137
+
138
+ // Upsert: replace if same id exists, otherwise append
139
+ const idx = agents.findIndex(a => a.id === agentConfig.id);
140
+ if (idx !== -1) {
141
+ // Preserve existing token/sessionKey if not provided in update
142
+ if (!agentConfig.token && agents[idx].token) {
143
+ agentConfig.token = agents[idx].token;
144
+ }
145
+ if (!agentConfig.sessionKey && agents[idx].sessionKey) {
146
+ agentConfig.sessionKey = agents[idx].sessionKey;
147
+ }
148
+ // Preserve existing sshTunnel if not provided in update
149
+ if (!agentConfig.sshTunnel && agents[idx].sshTunnel) {
150
+ agentConfig.sshTunnel = agents[idx].sshTunnel;
151
+ }
152
+ agents[idx] = agentConfig;
153
+ } else {
154
+ agents.push(agentConfig);
155
+ }
156
+
157
+ await saveSettings(config.pagesFolder, { agents });
158
+
159
+ // Auto-connect OpenClaw agents after save
160
+ tryConnectAgent(agentConfig);
161
+
162
+ res.json(toClientAgent(agentConfig));
163
+ } catch (err: unknown) {
164
+ console.error(err);
165
+ res.status(500).json({ error: (err as Error).message });
166
+ }
167
+ });
168
+
169
+ // PATCH /api/agents/:id — Toggle enabled/disabled or partial update
170
+ app.patch('/api/agents/:id', async (req, res) => {
171
+ try {
172
+ const { id } = req.params;
173
+ const settings = await loadSettings(config.pagesFolder);
174
+ const agents = settings.agents ? [...settings.agents] : [];
175
+ const idx = agents.findIndex(a => a.id === id);
176
+ if (idx === -1) {
177
+ res.status(404).json({ error: `Agent "${id}" not found` });
178
+ return;
179
+ }
180
+
181
+ const body = req.body as Partial<AgentConfig>;
182
+ if (typeof body.enabled === 'boolean') agents[idx].enabled = body.enabled;
183
+ if (typeof body.name === 'string') agents[idx].name = body.name;
184
+ if (typeof body.description === 'string') agents[idx].description = body.description;
185
+
186
+ await saveSettings(config.pagesFolder, { agents });
187
+
188
+ // Connect or disconnect based on enabled state
189
+ if (agents[idx].provider === 'openclaw') {
190
+ if (agents[idx].enabled) {
191
+ tryConnectAgent(agents[idx]);
192
+ } else {
193
+ disconnectAgent(agents[idx].id);
194
+ }
195
+ }
196
+
197
+ res.json(toClientAgent(agents[idx]));
198
+ } catch (err: unknown) {
199
+ console.error(err);
200
+ res.status(500).json({ error: (err as Error).message });
201
+ }
202
+ });
203
+
204
+ // DELETE /api/agents/:id — Remove agent
205
+ app.delete('/api/agents/:id', async (req, res) => {
206
+ try {
207
+ const { id } = req.params;
208
+ const settings = await loadSettings(config.pagesFolder);
209
+ const agent = (settings.agents ?? []).find(a => a.id === id);
210
+
211
+ // Disconnect if OpenClaw
212
+ if (agent?.provider === 'openclaw') {
213
+ disconnectAgent(id);
214
+ }
215
+
216
+ const agents = (settings.agents ?? []).filter(a => a.id !== id);
217
+ await saveSettings(config.pagesFolder, { agents });
218
+ res.json({ deleted: true });
219
+ } catch (err: unknown) {
220
+ console.error(err);
221
+ res.status(500).json({ error: (err as Error).message });
222
+ }
223
+ });
224
+
225
+ // POST /api/agents/:id/connect — Manually trigger WebSocket connection
226
+ app.post('/api/agents/:id/connect', async (req, res) => {
227
+ try {
228
+ const { id } = req.params;
229
+ const settings = await loadSettings(config.pagesFolder);
230
+ const agent = (settings.agents ?? []).find(a => a.id === id);
231
+ if (!agent) {
232
+ res.status(404).json({ error: `Agent "${id}" not found` });
233
+ return;
234
+ }
235
+ if (agent.provider !== 'openclaw') {
236
+ res.status(400).json({ error: 'Only OpenClaw agents support WebSocket connections' });
237
+ return;
238
+ }
239
+ if (!agent.token) {
240
+ res.status(400).json({ error: 'Agent has no token configured' });
241
+ return;
242
+ }
243
+
244
+ await connectAgent({ id: agent.id, name: agent.name, url: agent.url, token: agent.token, sshTunnel: agent.sshTunnel });
245
+ const status = getAgentStatus(agent.id);
246
+ res.json({ connected: status.connected, authenticated: status.authenticated });
247
+ } catch (err: unknown) {
248
+ console.error(err);
249
+ res.status(500).json({ error: (err as Error).message });
250
+ }
251
+ });
252
+
253
+ // POST /api/agents/:id/disconnect — Disconnect WebSocket
254
+ app.post('/api/agents/:id/disconnect', async (req, res) => {
255
+ try {
256
+ const { id } = req.params;
257
+ disconnectAgent(id);
258
+ res.json({ disconnected: true });
259
+ } catch (err: unknown) {
260
+ console.error(err);
261
+ res.status(500).json({ error: (err as Error).message });
262
+ }
263
+ });
264
+
265
+ // POST /api/agents/:id/send — Send message to agent (dispatches by provider)
266
+ app.post('/api/agents/:id/send', async (req, res) => {
267
+ try {
268
+ const { id } = req.params;
269
+ const { message, attachments } = req.body;
270
+ if (!message || typeof message !== 'string') {
271
+ res.status(400).json({ error: 'message is required' });
272
+ return;
273
+ }
274
+
275
+ const settings = await loadSettings(config.pagesFolder);
276
+ const agent = (settings.agents ?? []).find(a => a.id === id);
277
+ if (!agent) {
278
+ res.status(404).json({ error: `Agent "${id}" not found` });
279
+ return;
280
+ }
281
+
282
+ if (!agent.enabled) {
283
+ res.status(400).json({ error: `Agent "${agent.name}" is disabled` });
284
+ return;
285
+ }
286
+
287
+ // Dispatch to the correct provider
288
+ const provider = agent.provider === 'openclaw' ? openclawProvider : a2aProvider;
289
+ const result = await provider.send(agent, message, attachments);
290
+ res.json(result);
291
+ } catch (err: unknown) {
292
+ console.error(err);
293
+ res.status(500).json({ error: (err as Error).message });
294
+ }
295
+ });
296
+
297
+ // -----------------------------------------------------------------------
298
+ // Helper: load agent + resolve provider, return 404/400 on failure
299
+ // -----------------------------------------------------------------------
300
+ async function withAgent(
301
+ id: string,
302
+ res: import('express').Response,
303
+ fn: (agent: AgentConfig, provider: AgentProvider) => Promise<void>,
304
+ ): Promise<void> {
305
+ const settings = await loadSettings(config.pagesFolder);
306
+ const agent = (settings.agents ?? []).find(a => a.id === id);
307
+ if (!agent) {
308
+ res.status(404).json({ error: `Agent "${id}" not found` });
309
+ return;
310
+ }
311
+ if (!agent.enabled) {
312
+ res.status(400).json({ error: `Agent "${agent.name}" is disabled` });
313
+ return;
314
+ }
315
+ const provider: AgentProvider = agent.provider === 'openclaw' ? openclawProvider : a2aProvider;
316
+ await fn(agent, provider);
317
+ }
318
+
319
+ // -----------------------------------------------------------------------
320
+ // Chat lifecycle routes (Phase 1)
321
+ // -----------------------------------------------------------------------
322
+
323
+ // POST /api/agents/:id/chat/history — Get chat history
324
+ app.post('/api/agents/:id/chat/history', async (req, res) => {
325
+ try {
326
+ await withAgent(req.params.id, res, async (agent, provider) => {
327
+ if (!provider.getHistory) {
328
+ res.status(501).json({ error: 'Operation not supported by this agent provider' });
329
+ return;
330
+ }
331
+ const messages = await provider.getHistory(agent);
332
+ res.json({ messages });
333
+ });
334
+ } catch (err: unknown) {
335
+ console.error(err);
336
+ res.status(500).json({ error: (err as Error).message });
337
+ }
338
+ });
339
+
340
+ // POST /api/agents/:id/chat/abort — Abort in-flight chat
341
+ app.post('/api/agents/:id/chat/abort', async (req, res) => {
342
+ try {
343
+ await withAgent(req.params.id, res, async (agent, provider) => {
344
+ if (!provider.abortChat) {
345
+ res.status(501).json({ error: 'Operation not supported by this agent provider' });
346
+ return;
347
+ }
348
+ await provider.abortChat(agent);
349
+ res.json({ ok: true });
350
+ });
351
+ } catch (err: unknown) {
352
+ console.error(err);
353
+ res.status(500).json({ error: (err as Error).message });
354
+ }
355
+ });
356
+
357
+ // POST /api/agents/:id/chat/clear — Clear/reset chat session
358
+ app.post('/api/agents/:id/chat/clear', async (req, res) => {
359
+ try {
360
+ await withAgent(req.params.id, res, async (agent, provider) => {
361
+ if (!provider.clearSession) {
362
+ res.status(501).json({ error: 'Operation not supported by this agent provider' });
363
+ return;
364
+ }
365
+ await provider.clearSession(agent);
366
+ res.json({ ok: true });
367
+ });
368
+ } catch (err: unknown) {
369
+ console.error(err);
370
+ res.status(500).json({ error: (err as Error).message });
371
+ }
372
+ });
373
+
374
+ // POST /api/agents/:id/stream — SSE streaming endpoint
375
+ app.post('/api/agents/:id/stream', async (req, res) => {
376
+ try {
377
+ const { id } = req.params;
378
+ const { message, attachments } = req.body;
379
+ if (!message || typeof message !== 'string') {
380
+ res.status(400).json({ error: 'message is required' });
381
+ return;
382
+ }
383
+
384
+ const settings = await loadSettings(config.pagesFolder);
385
+ const agent = (settings.agents ?? []).find(a => a.id === id);
386
+ if (!agent) {
387
+ res.status(404).json({ error: `Agent "${id}" not found` });
388
+ return;
389
+ }
390
+
391
+ if (!agent.enabled) {
392
+ res.status(400).json({ error: `Agent "${agent.name}" is disabled` });
393
+ return;
394
+ }
395
+
396
+ // Set up SSE headers
397
+ res.setHeader('Content-Type', 'text/event-stream');
398
+ res.setHeader('Cache-Control', 'no-cache');
399
+ res.setHeader('Connection', 'keep-alive');
400
+ res.flushHeaders();
401
+
402
+ // Dispatch to the correct provider's streaming method
403
+ const provider = agent.provider === 'openclaw' ? openclawProvider : a2aProvider;
404
+
405
+ for await (const event of provider.sendStream(agent, message, attachments)) {
406
+ res.write(`data: ${JSON.stringify(event)}\n\n`);
407
+ if (event.kind === 'done' || event.kind === 'error') break;
408
+ }
409
+
410
+ res.write('data: [DONE]\n\n');
411
+ res.end();
412
+ } catch (err: unknown) {
413
+ console.error(err);
414
+ // If headers already sent, just end the stream
415
+ if (res.headersSent) {
416
+ res.write(`data: ${JSON.stringify({ kind: 'error', data: (err as Error).message })}\n\n`);
417
+ res.end();
418
+ } else {
419
+ res.status(500).json({ error: (err as Error).message });
420
+ }
421
+ }
422
+ });
423
+ }
@@ -1,12 +1,15 @@
1
1
  import path from "path";
2
+ import fs from "fs/promises";
3
+ import AdmZip from "adm-zip";
2
4
  import { listPages, loadPageMetadata, PageMetadata, savePageMetadata, REQUIRED_PAGES, deletePage, copyPage, loadPageState, savePageState, PAGE_VERSION } from "../pages";
3
- import { checkIfExists, copyFile, deleteFile, loadFile } from "../files";
5
+ import { checkIfExists, copyFile, copyFolderRecursive, deleteFile, ensureFolderExists, loadFile } from "../files";
4
6
  import {getModelEntry, loadSettings, saveSettings, ServicesConfig } from "../settings";
5
7
  import { Application } from 'express';
8
+ import express from 'express';
6
9
  import { SynthOSConfig } from "../init";
7
10
  import { createCompletePrompt, PROVIDERS } from "./createCompletePrompt";
8
11
  import { generateDefaultImage, generateImage } from "./generateImage";
9
- import { chainOfThought } from "agentm-core";
12
+ import { chainOfThought } from "../models";
10
13
  import { requiresSettings } from "./requiresSettings";
11
14
  import { executeScript } from "../scripts";
12
15
  import { listThemes, loadTheme, loadThemeInfo } from "../themes";
@@ -52,6 +55,96 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
52
55
  res.json(pages);
53
56
  });
54
57
 
58
+ // Import a page from a zip file
59
+ app.post('/api/pages/import', express.raw({ type: 'application/zip', limit: '50mb' }), async (req, res) => {
60
+ try {
61
+ const zipBuffer = req.body as Buffer;
62
+ if (!zipBuffer || zipBuffer.length === 0) {
63
+ res.status(400).json({ error: 'Empty request body' });
64
+ return;
65
+ }
66
+
67
+ let zip: AdmZip;
68
+ try {
69
+ zip = new AdmZip(zipBuffer);
70
+ } catch {
71
+ res.status(400).json({ error: 'Invalid zip file' });
72
+ return;
73
+ }
74
+
75
+ const entries = zip.getEntries();
76
+ if (entries.length === 0) {
77
+ res.status(400).json({ error: 'Zip file is empty' });
78
+ return;
79
+ }
80
+
81
+ // Determine top-level folder name and validate structure
82
+ const firstEntry = entries[0].entryName;
83
+ const topFolder = firstEntry.split('/')[0];
84
+ const hasPageHtml = entries.some(e => e.entryName === `${topFolder}/page.html`);
85
+ if (!hasPageHtml) {
86
+ res.status(400).json({ error: 'Zip must contain a <folder>/page.html entry' });
87
+ return;
88
+ }
89
+
90
+ // Sanitize page name from folder name
91
+ let pageName = topFolder.toLowerCase().replace(/[^a-z0-9_-]/g, '');
92
+ if (!pageName) {
93
+ res.status(400).json({ error: 'Could not derive a valid page name from zip contents' });
94
+ return;
95
+ }
96
+
97
+ // Auto-append _1, _2, etc. on name conflicts
98
+ let finalName = pageName;
99
+ let suffix = 0;
100
+ while (await checkIfExists(path.join(config.pagesFolder, 'pages', finalName))) {
101
+ suffix++;
102
+ finalName = `${pageName}_${suffix}`;
103
+ }
104
+
105
+ const targetDir = path.join(config.pagesFolder, 'pages', finalName);
106
+ await ensureFolderExists(targetDir);
107
+
108
+ // Extract entries with path traversal protection
109
+ for (const entry of entries) {
110
+ if (entry.isDirectory) continue;
111
+
112
+ // Strip the top-level folder prefix to get relative path
113
+ const relativePath = entry.entryName.substring(topFolder.length + 1);
114
+ if (!relativePath) continue;
115
+
116
+ const resolvedPath = path.resolve(targetDir, relativePath);
117
+ if (!resolvedPath.startsWith(path.resolve(targetDir))) {
118
+ // Path traversal — skip this entry
119
+ continue;
120
+ }
121
+
122
+ await ensureFolderExists(path.dirname(resolvedPath));
123
+ await fs.writeFile(resolvedPath, entry.getData());
124
+ }
125
+
126
+ // Update metadata: set createdDate and lastModified to now
127
+ const now = new Date().toISOString();
128
+ const existingMeta = await loadPageMetadata(config.pagesFolder, finalName);
129
+ const metadata: PageMetadata = {
130
+ title: existingMeta?.title ?? '',
131
+ categories: existingMeta?.categories ?? [],
132
+ pinned: existingMeta?.pinned ?? false,
133
+ showInAll: existingMeta?.showInAll ?? true,
134
+ createdDate: now,
135
+ lastModified: now,
136
+ pageVersion: existingMeta?.pageVersion ?? PAGE_VERSION,
137
+ mode: existingMeta?.mode ?? 'unlocked',
138
+ };
139
+ await savePageMetadata(config.pagesFolder, finalName, metadata);
140
+
141
+ res.status(201).json({ name: finalName, title: metadata.title });
142
+ } catch (err: unknown) {
143
+ console.error(err);
144
+ res.status(500).json({ error: (err as Error).message });
145
+ }
146
+ });
147
+
55
148
  // Get page metadata
56
149
  app.get('/api/pages/:name', async (req, res) => {
57
150
  try {
@@ -286,9 +379,6 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
286
379
  if (Array.isArray(settings.models)) {
287
380
  for (const entry of settings.models) {
288
381
  if (entry.configuration) {
289
- if (typeof entry.configuration.maxTokens === 'string') {
290
- entry.configuration.maxTokens = parseInt(entry.configuration.maxTokens);
291
- }
292
382
  }
293
383
  if (typeof entry.logCompletions === 'string') {
294
384
  entry.logCompletions = entry.logCompletions === 'true';
@@ -326,9 +416,8 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
326
416
  app.post('/api/generate/completion', async (req, res) => {
327
417
  await requiresSettings(res, config.pagesFolder, async (settings) => {
328
418
  const { prompt, temperature } = req.body;
329
- const maxTokens = getModelEntry(settings, 'chat').configuration.maxTokens;
330
419
  const completePrompt = await createCompletePrompt(config.pagesFolder, 'chat', req.body.model);
331
- const response = await chainOfThought({ question: prompt, temperature, maxTokens, completePrompt });
420
+ const response = await chainOfThought({ question: prompt, temperature, completePrompt });
332
421
  if (response.completed) {
333
422
  res.json(response.value ?? {});
334
423
  } else {
@@ -342,12 +431,26 @@ export function useApiRoutes(config: SynthOSConfig, app: Application): void {
342
431
  app.post('/api/brainstorm', async (req, res) => {
343
432
  await requiresSettings(res, config.pagesFolder, async (settings) => {
344
433
  const { context, messages } = req.body;
345
- const maxTokens = getModelEntry(settings, 'chat').configuration.maxTokens;
346
434
  const completePrompt = await createCompletePrompt(config.pagesFolder, 'chat');
347
435
 
348
436
  const system: { role: 'system'; content: string } = {
349
437
  role: 'system',
350
- content: `You are a creative brainstorming assistant for SynthOS, a tool that builds web pages through conversation. The user is brainstorming — exploring ideas before building. Be concise, creative, and collaborative. Suggest concrete approaches when you can.
438
+ content: `You are a creative brainstorming assistant for SynthOS, a tool that builds pages through conversation.
439
+ SynthOS is like a WIKI for vibe coding. Each page has a chat panel and a viewer panel. They are vibe coding what's displayed in that viewer panel. They can then save that as a page.
440
+ The user is brainstorming — exploring ideas before building. Be concise, creative, and collaborative.
441
+ They may say that they want to build an app or page that does XYZ but they're talking about what they expect to see in the viewer panel.
442
+ The goal is to help them generate a prompt for the builder that captures their vision, along with suggestions for next steps.
443
+ Suggest concrete approaches when you can, not complex visions for some ellaborate app.
444
+ Just help expand their thoughts into a great next prompt.
445
+
446
+ <CONTEXT>
447
+ ${context}
448
+
449
+ <INSTRUCTIONS>
450
+ Look at the <CHAT_HISTORY> and if it's empty it's the start of a new idea. Simply greet them and ask them what they're thinking of building. Suggestions could be help me decide, etc.
451
+ If you see a conversation between SynthOS and the User. Asses what they're building and ask them what they'd like help with. Maybe offer a few good next steps.
452
+
453
+ SynthOS 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.
351
454
 
352
455
  You MUST return your response as a JSON object with exactly these fields:
353
456
  {
@@ -358,12 +461,7 @@ You MUST return your response as a JSON object with exactly these fields:
358
461
 
359
462
  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.
360
463
 
361
- Return ONLY the JSON object. No markdown fences.
362
-
363
- <CONTEXT>
364
- ${context}
365
- </CONTEXT>`
366
- };
464
+ Return ONLY the JSON object.`};
367
465
 
368
466
  // Format multi-turn conversation into a single prompt
369
467
  const formatted = (messages as { role: string; content: string }[]).map(m =>
@@ -372,7 +470,7 @@ ${context}
372
470
 
373
471
  const prompt: { role: 'user'; content: string } = { role: 'user', content: formatted };
374
472
 
375
- const result = await completePrompt({ prompt, system, maxTokens, jsonMode: true });
473
+ const result = await completePrompt({ prompt, system, jsonMode: true });
376
474
  if (result.completed) {
377
475
  let response = result.value || '';
378
476
  let brainstormPrompt = '';
@@ -689,14 +787,22 @@ ${context}
689
787
  // Save upgraded HTML to v2 folder structure
690
788
  await savePageState(config.pagesFolder, name, migratedHtml);
691
789
 
692
- // Move legacy flat file to .migrated folder instead of deleting
790
+ // Backup original page to .migrated/ before overwriting
791
+ const migratedFolder = path.join(config.pagesFolder, '.migrated');
792
+
793
+ // Handle legacy flat file (.synthos/pagename.html)
693
794
  const flatPath = path.join(config.pagesFolder, `${name}.html`);
694
795
  if (await checkIfExists(flatPath)) {
695
- const migratedFolder = path.join(config.pagesFolder, '.migrated');
696
796
  await copyFile(flatPath, migratedFolder);
697
797
  await deleteFile(flatPath);
698
798
  }
699
799
 
800
+ // Handle folder-based page (.synthos/pages/name/)
801
+ const folderPath = path.join(config.pagesFolder, 'pages', name);
802
+ if (await checkIfExists(folderPath)) {
803
+ await copyFolderRecursive(folderPath, path.join(migratedFolder, name));
804
+ }
805
+
700
806
  // Update metadata
701
807
  metadata.pageVersion = PAGE_VERSION;
702
808
  metadata.lastModified = new Date().toISOString();
@@ -708,4 +814,53 @@ ${context}
708
814
  res.status(500).json({ error: (err as Error).message });
709
815
  }
710
816
  });
817
+
818
+ // Export a page as a zip file
819
+ app.get('/api/pages/:name/export', async (req, res) => {
820
+ try {
821
+ const { name } = req.params;
822
+
823
+ // Try user pages folder first, then required pages
824
+ const userPageDir = path.join(config.pagesFolder, 'pages', name);
825
+ const requiredPageFile = path.join(config.requiredPagesFolder, `${name}.html`);
826
+ let sourceDir: string | null = null;
827
+
828
+ if (await checkIfExists(path.join(userPageDir, 'page.html'))) {
829
+ sourceDir = userPageDir;
830
+ } else if (await checkIfExists(requiredPageFile)) {
831
+ // For required pages, create a temp-like zip with just the HTML
832
+ const zip = new AdmZip();
833
+ const html = await loadFile(requiredPageFile);
834
+ zip.addFile(`${name}/page.html`, Buffer.from(html, 'utf-8'));
835
+
836
+ // Include page.json if it exists
837
+ const requiredMetaFile = path.join(config.requiredPagesFolder, `${name}.json`);
838
+ if (await checkIfExists(requiredMetaFile)) {
839
+ const meta = await loadFile(requiredMetaFile);
840
+ zip.addFile(`${name}/page.json`, Buffer.from(meta, 'utf-8'));
841
+ }
842
+
843
+ const zipBuffer = zip.toBuffer();
844
+ res.set('Content-Type', 'application/zip');
845
+ res.set('Content-Disposition', `attachment; filename="${name}.zip"`);
846
+ res.send(zipBuffer);
847
+ return;
848
+ } else {
849
+ res.status(404).json({ error: `Page "${name}" not found` });
850
+ return;
851
+ }
852
+
853
+ // Zip the entire page folder
854
+ const zip = new AdmZip();
855
+ zip.addLocalFolder(sourceDir, name);
856
+ const zipBuffer = zip.toBuffer();
857
+
858
+ res.set('Content-Type', 'application/zip');
859
+ res.set('Content-Disposition', `attachment; filename="${name}.zip"`);
860
+ res.send(zipBuffer);
861
+ } catch (err: unknown) {
862
+ console.error(err);
863
+ res.status(500).json({ error: (err as Error).message });
864
+ }
865
+ });
711
866
  }