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.
- package/README.md +215 -65
- package/default-pages/application.json +1 -0
- package/default-pages/json_tools.json +1 -1
- package/default-pages/oregon_trail.html +321 -0
- package/default-pages/oregon_trail.json +12 -0
- package/default-pages/sidebar_page.json +1 -0
- package/default-pages/solar_explorer.html +10 -18
- package/default-pages/solar_explorer.json +2 -2
- package/default-pages/two-panel_page.json +1 -0
- package/default-pages/us_map.html +192 -0
- package/default-pages/us_map.json +12 -0
- package/default-pages/us_map_1850.html +325 -0
- package/default-pages/us_map_1850.json +12 -0
- package/default-pages/western_cities_1850.html +526 -0
- package/default-pages/western_cities_1850.json +12 -0
- package/default-themes/{nebula-dawn.css → nebula-dawn.v2.css} +24 -0
- package/default-themes/{nebula-dusk.css → nebula-dusk.v2.css} +24 -0
- package/dist/agents/a2a/a2aProvider.d.ts +3 -0
- package/dist/agents/a2a/a2aProvider.d.ts.map +1 -0
- package/dist/agents/a2a/a2aProvider.js +126 -0
- package/dist/agents/a2a/a2aProvider.js.map +1 -0
- package/dist/agents/discovery.d.ts +30 -0
- package/dist/agents/discovery.d.ts.map +1 -0
- package/dist/agents/discovery.js +52 -0
- package/dist/agents/discovery.js.map +1 -0
- package/dist/agents/index.d.ts +7 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/index.js +19 -0
- package/dist/agents/index.js.map +1 -0
- package/dist/agents/openclaw/gatewayManager.d.ts +113 -0
- package/dist/agents/openclaw/gatewayManager.d.ts.map +1 -0
- package/dist/agents/openclaw/gatewayManager.js +470 -0
- package/dist/agents/openclaw/gatewayManager.js.map +1 -0
- package/dist/agents/openclaw/openclawProvider.d.ts +3 -0
- package/dist/agents/openclaw/openclawProvider.d.ts.map +1 -0
- package/dist/agents/openclaw/openclawProvider.js +239 -0
- package/dist/agents/openclaw/openclawProvider.js.map +1 -0
- package/dist/agents/openclaw/sshTunnelManager.d.ts +23 -0
- package/dist/agents/openclaw/sshTunnelManager.d.ts.map +1 -0
- package/dist/agents/openclaw/sshTunnelManager.js +340 -0
- package/dist/agents/openclaw/sshTunnelManager.js.map +1 -0
- package/dist/agents/types.d.ts +64 -0
- package/dist/agents/types.d.ts.map +1 -0
- package/dist/agents/types.js +6 -0
- package/dist/agents/types.js.map +1 -0
- package/dist/connectors/airtable/connector.json +27 -0
- package/dist/connectors/alpha-vantage/connector.json +26 -0
- package/dist/connectors/brave-search/connector.json +26 -0
- package/dist/connectors/cloudinary/connector.json +27 -0
- package/dist/connectors/deepl/connector.json +28 -0
- package/dist/connectors/elevenlabs/connector.json +30 -0
- package/dist/connectors/giphy/connector.json +27 -0
- package/dist/connectors/github/connector.json +29 -0
- package/dist/connectors/huggingface/connector.json +27 -0
- package/dist/connectors/imgur/connector.json +29 -0
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.d.ts.map +1 -1
- package/dist/connectors/instagram/connector.json +43 -0
- package/dist/connectors/jira/connector.json +28 -0
- package/dist/connectors/mapbox/connector.json +26 -0
- package/dist/connectors/nasa/connector.json +27 -0
- package/dist/connectors/newsapi/connector.json +27 -0
- package/dist/connectors/notion/connector.json +28 -0
- package/dist/connectors/open-exchange-rates/connector.json +27 -0
- package/dist/connectors/openweathermap/connector.json +26 -0
- package/dist/connectors/pexels/connector.json +27 -0
- package/dist/connectors/registry.d.ts.map +1 -1
- package/dist/connectors/registry.js +42 -96
- package/dist/connectors/registry.js.map +1 -1
- package/dist/connectors/resend/connector.json +29 -0
- package/dist/connectors/rss2json/connector.json +27 -0
- package/dist/connectors/sendgrid/connector.json +27 -0
- package/dist/connectors/spoonacular/connector.json +28 -0
- package/dist/connectors/stability-ai/connector.json +27 -0
- package/dist/connectors/twilio/connector.json +28 -0
- package/dist/connectors/types.d.ts +23 -0
- package/dist/connectors/types.d.ts.map +1 -1
- package/dist/connectors/unsplash/connector.json +27 -0
- package/dist/connectors/wolfram-alpha/connector.json +26 -0
- package/dist/connectors/youtube-data/connector.json +30 -0
- package/dist/files.d.ts +1 -0
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +16 -1
- package/dist/files.js.map +1 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +28 -0
- package/dist/init.js.map +1 -1
- package/dist/migrations.d.ts +3 -2
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +122 -138
- package/dist/migrations.js.map +1 -1
- package/dist/models/anthropic.d.ts +22 -0
- package/dist/models/anthropic.d.ts.map +1 -0
- package/dist/models/anthropic.js +76 -0
- package/dist/models/anthropic.js.map +1 -0
- package/dist/models/chainOfThought.d.ts +12 -0
- package/dist/models/chainOfThought.d.ts.map +1 -0
- package/dist/models/chainOfThought.js +45 -0
- package/dist/models/chainOfThought.js.map +1 -0
- package/dist/models/fireworksai.d.ts +30 -0
- package/dist/models/fireworksai.d.ts.map +1 -0
- package/dist/models/fireworksai.js +133 -0
- package/dist/models/fireworksai.js.map +1 -0
- package/dist/models/index.d.ts +7 -1
- package/dist/models/index.d.ts.map +1 -1
- package/dist/models/index.js +19 -1
- package/dist/models/index.js.map +1 -1
- package/dist/models/logCompletePrompt.d.ts +3 -0
- package/dist/models/logCompletePrompt.d.ts.map +1 -0
- package/dist/models/logCompletePrompt.js +23 -0
- package/dist/models/logCompletePrompt.js.map +1 -0
- package/dist/models/openai.d.ts +24 -0
- package/dist/models/openai.d.ts.map +1 -0
- package/dist/models/openai.js +80 -0
- package/dist/models/openai.js.map +1 -0
- package/dist/models/providers.d.ts +1 -0
- package/dist/models/providers.d.ts.map +1 -1
- package/dist/models/providers.js +12 -4
- package/dist/models/providers.js.map +1 -1
- package/dist/models/types.d.ts +34 -2
- package/dist/models/types.d.ts.map +1 -1
- package/dist/models/types.js +16 -0
- package/dist/models/types.js.map +1 -1
- package/dist/models/utils.d.ts +6 -0
- package/dist/models/utils.d.ts.map +1 -0
- package/dist/models/utils.js +21 -0
- package/dist/models/utils.js.map +1 -0
- package/dist/scripts.d.ts +2 -1
- package/dist/scripts.d.ts.map +1 -1
- package/dist/scripts.js +4 -3
- package/dist/scripts.js.map +1 -1
- package/dist/service/createCompletePrompt.d.ts +1 -1
- package/dist/service/createCompletePrompt.d.ts.map +1 -1
- package/dist/service/createCompletePrompt.js +9 -6
- package/dist/service/createCompletePrompt.js.map +1 -1
- package/dist/service/generateImage.d.ts +1 -1
- package/dist/service/generateImage.d.ts.map +1 -1
- package/dist/service/generateImage.js +3 -3
- package/dist/service/generateImage.js.map +1 -1
- package/dist/service/server.d.ts.map +1 -1
- package/dist/service/server.js +3 -0
- package/dist/service/server.js.map +1 -1
- package/dist/service/transformPage.d.ts +4 -2
- package/dist/service/transformPage.d.ts.map +1 -1
- package/dist/service/transformPage.js +74 -6
- package/dist/service/transformPage.js.map +1 -1
- package/dist/service/useAgentRoutes.d.ts +4 -0
- package/dist/service/useAgentRoutes.d.ts.map +1 -0
- package/dist/service/useAgentRoutes.js +389 -0
- package/dist/service/useAgentRoutes.js.map +1 -0
- package/dist/service/useApiRoutes.d.ts.map +1 -1
- package/dist/service/useApiRoutes.js +157 -16
- package/dist/service/useApiRoutes.js.map +1 -1
- package/dist/service/useConnectorRoutes.d.ts.map +1 -1
- package/dist/service/useConnectorRoutes.js +14 -3
- package/dist/service/useConnectorRoutes.js.map +1 -1
- package/dist/service/useGatewayRoutes.d.ts +4 -0
- package/dist/service/useGatewayRoutes.d.ts.map +1 -0
- package/dist/service/useGatewayRoutes.js +168 -0
- package/dist/service/useGatewayRoutes.js.map +1 -0
- package/dist/service/usePageRoutes.d.ts.map +1 -1
- package/dist/service/usePageRoutes.js +16 -5
- package/dist/service/usePageRoutes.js.map +1 -1
- package/dist/settings.d.ts +2 -1
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +4 -8
- package/dist/settings.js.map +1 -1
- package/dist/themes.d.ts +14 -0
- package/dist/themes.d.ts.map +1 -1
- package/dist/themes.js +86 -13
- package/dist/themes.js.map +1 -1
- package/package.json +10 -5
- package/page-scripts/helpers-v2.js +222 -0
- package/page-scripts/page-v2.js +656 -0
- package/required-pages/builder.html +1 -27
- package/required-pages/pages.html +745 -22
- package/required-pages/settings.html +819 -21
- package/required-pages/synthos_apis.html +56 -1
- package/src/agents/a2a/a2aProvider.ts +110 -0
- package/src/agents/discovery.ts +74 -0
- package/src/agents/index.ts +6 -0
- package/src/agents/openclaw/gatewayManager.ts +559 -0
- package/src/agents/openclaw/openclawProvider.ts +261 -0
- package/src/agents/openclaw/sshTunnelManager.ts +385 -0
- package/src/agents/types.ts +82 -0
- package/src/connectors/airtable/connector.json +27 -0
- package/src/connectors/alpha-vantage/connector.json +26 -0
- package/src/connectors/brave-search/connector.json +26 -0
- package/src/connectors/cloudinary/connector.json +27 -0
- package/src/connectors/deepl/connector.json +28 -0
- package/src/connectors/elevenlabs/connector.json +30 -0
- package/src/connectors/giphy/connector.json +27 -0
- package/src/connectors/github/connector.json +29 -0
- package/src/connectors/huggingface/connector.json +27 -0
- package/src/connectors/imgur/connector.json +29 -0
- package/src/connectors/index.ts +2 -0
- package/src/connectors/instagram/connector.json +43 -0
- package/src/connectors/jira/connector.json +28 -0
- package/src/connectors/mapbox/connector.json +26 -0
- package/src/connectors/nasa/connector.json +27 -0
- package/src/connectors/newsapi/connector.json +27 -0
- package/src/connectors/notion/connector.json +28 -0
- package/src/connectors/open-exchange-rates/connector.json +27 -0
- package/src/connectors/openweathermap/connector.json +26 -0
- package/src/connectors/pexels/connector.json +27 -0
- package/src/connectors/registry.ts +21 -97
- package/src/connectors/resend/connector.json +29 -0
- package/src/connectors/rss2json/connector.json +27 -0
- package/src/connectors/sendgrid/connector.json +27 -0
- package/src/connectors/spoonacular/connector.json +28 -0
- package/src/connectors/stability-ai/connector.json +27 -0
- package/src/connectors/twilio/connector.json +28 -0
- package/src/connectors/types.ts +25 -0
- package/src/connectors/unsplash/connector.json +27 -0
- package/src/connectors/wolfram-alpha/connector.json +26 -0
- package/src/connectors/youtube-data/connector.json +30 -0
- package/src/files.ts +14 -0
- package/src/init.ts +27 -0
- package/src/migrations.ts +121 -138
- package/src/models/anthropic.ts +89 -0
- package/src/models/chainOfThought.ts +56 -0
- package/src/models/fireworksai.ts +136 -0
- package/src/models/index.ts +7 -1
- package/src/models/logCompletePrompt.ts +25 -0
- package/src/models/openai.ts +90 -0
- package/src/models/providers.ts +12 -3
- package/src/models/types.ts +67 -2
- package/src/models/utils.ts +16 -0
- package/src/scripts.ts +2 -2
- package/src/service/createCompletePrompt.ts +3 -1
- package/src/service/generateImage.ts +2 -2
- package/src/service/server.ts +4 -0
- package/src/service/transformPage.ts +81 -8
- package/src/service/useAgentRoutes.ts +423 -0
- package/src/service/useApiRoutes.ts +173 -18
- package/src/service/useConnectorRoutes.ts +14 -3
- package/src/service/usePageRoutes.ts +20 -6
- package/src/settings.ts +6 -10
- package/src/themes.ts +84 -12
- package/tests/README.md +12 -0
- package/tests/anthropic.spec.ts +84 -0
- package/tests/chainOfThought.spec.ts +108 -0
- package/tests/ensureScripts.spec.ts +82 -0
- package/tests/files.spec.ts +233 -0
- package/tests/fireworksai.spec.ts +92 -0
- package/tests/logCompletePrompt.spec.ts +74 -0
- package/tests/migrations.spec.ts +169 -0
- package/tests/openai.spec.ts +71 -0
- package/tests/pages.spec.ts +328 -0
- package/tests/providers.spec.ts +144 -0
- package/tests/scripts.spec.ts +209 -0
- package/tests/transformPage.spec.ts +931 -0
- package/tests/types.spec.ts +23 -0
- package/default-pages/app_builder.json +0 -1
- package/default-pages/sidebar_builder.json +0 -1
- package/default-pages/two-panel_builder.json +0 -1
- package/images/home.png +0 -0
- package/images/page-management.png +0 -0
- package/images/settings.png +0 -0
- package/images/synthos-square.png +0 -0
- /package/default-pages/{app_builder.html → application.html} +0 -0
- /package/default-pages/{sidebar_builder.html → sidebar_page.html} +0 -0
- /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 "
|
|
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,
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
//
|
|
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
|
}
|