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,559 @@
1
+ import { startTunnel, stopTunnel, SshTunnelConfig } from './sshTunnelManager';
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Types
5
+ // ---------------------------------------------------------------------------
6
+
7
+ /** Internal gateway config for WebSocket connections (not exported). */
8
+ export interface GatewayConfig {
9
+ id: string;
10
+ name: string;
11
+ url: string;
12
+ token: string;
13
+ enabled: boolean;
14
+ role: 'operator';
15
+ scopes: string[];
16
+ }
17
+
18
+ interface PendingRequest {
19
+ resolve: (payload: unknown) => void;
20
+ reject: (err: Error) => void;
21
+ timer: ReturnType<typeof setTimeout>;
22
+ /** When true, skip resolution on intermediate { status: "accepted" } responses. */
23
+ expectFinal?: boolean;
24
+ }
25
+
26
+ export interface GatewayConnection {
27
+ gatewayId: string;
28
+ ws: WebSocket | null;
29
+ config: GatewayConfig;
30
+ connected: boolean;
31
+ authenticated: boolean;
32
+ /** Granted scopes and role from the hello-ok response */
33
+ grantedAuth?: { role: string; scopes: string[]; deviceToken?: string };
34
+ pendingRequests: Map<string, PendingRequest>;
35
+ eventListeners: Map<string, Array<(data: unknown) => void>>;
36
+ lastTick: number;
37
+ tickIntervalMs: number;
38
+ tickTimer: ReturnType<typeof setInterval> | null;
39
+ reconnectTimer: ReturnType<typeof setTimeout> | null;
40
+ reconnectAttempts: number;
41
+ /** Set to true when disconnect() is called explicitly — suppresses auto-reconnect */
42
+ intentionalClose: boolean;
43
+ }
44
+
45
+ const RPC_TIMEOUT_MS = 30_000;
46
+ const AUTH_TIMEOUT_MS = 15_000;
47
+ const MAX_RECONNECT_DELAY_MS = 60_000;
48
+ const BASE_RECONNECT_DELAY_MS = 1_000;
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // Module-level connection pool
52
+ // ---------------------------------------------------------------------------
53
+
54
+ const connections = new Map<string, GatewayConnection>();
55
+
56
+ let _nextId = 1;
57
+ function nextRequestId(): string {
58
+ return `req-${_nextId++}`;
59
+ }
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // Public API
63
+ // ---------------------------------------------------------------------------
64
+
65
+ /**
66
+ * Connect to an OpenClaw gateway. Resolves once the auth handshake completes.
67
+ *
68
+ * Protocol (from https://docs.openclaw.ai/gateway/protocol):
69
+ * 1. Client opens WebSocket
70
+ * 2. Server sends {type:"event", event:"connect.challenge", payload:{nonce,ts}}
71
+ * 3. Client sends {type:"req", id, method:"connect", params:{auth:{token}, role, scopes, ...}}
72
+ * 4. Server sends {type:"res", id, ok:true, payload:{type:"hello-ok", policy:{tickIntervalMs}, auth:{...}}}
73
+ */
74
+ export async function connect(config: GatewayConfig): Promise<GatewayConnection> {
75
+ const existing = connections.get(config.id);
76
+ if (existing?.connected && existing.authenticated) {
77
+ return existing;
78
+ }
79
+
80
+ const conn: GatewayConnection = existing ?? {
81
+ gatewayId: config.id,
82
+ ws: null,
83
+ config,
84
+ connected: false,
85
+ authenticated: false,
86
+ pendingRequests: new Map(),
87
+ eventListeners: new Map(),
88
+ lastTick: Date.now(),
89
+ tickIntervalMs: 0,
90
+ tickTimer: null,
91
+ reconnectTimer: null,
92
+ reconnectAttempts: 0,
93
+ intentionalClose: false,
94
+ };
95
+
96
+ // Update config in case it changed
97
+ conn.config = config;
98
+ conn.intentionalClose = false;
99
+ connections.set(config.id, conn);
100
+
101
+ return new Promise<GatewayConnection>((resolve, reject) => {
102
+ let settled = false;
103
+
104
+ function settle(err?: Error): void {
105
+ if (settled) return;
106
+ settled = true;
107
+ clearTimeout(authTimeout);
108
+ if (err) reject(err);
109
+ else resolve(conn);
110
+ }
111
+
112
+ const authTimeout = setTimeout(() => {
113
+ settle(new Error(`Gateway auth timed out after ${AUTH_TIMEOUT_MS}ms: ${config.url}`));
114
+ conn.ws?.close();
115
+ }, AUTH_TIMEOUT_MS);
116
+
117
+ try {
118
+ const ws = new WebSocket(config.url);
119
+ conn.ws = ws;
120
+
121
+ ws.addEventListener('open', () => {
122
+ conn.connected = true;
123
+ conn.reconnectAttempts = 0;
124
+ // Wait for connect.challenge event from server
125
+ });
126
+
127
+ ws.addEventListener('message', (rawEvent) => {
128
+ const data = typeof rawEvent.data === 'string' ? rawEvent.data : rawEvent.data.toString();
129
+ let msg: Record<string, unknown>;
130
+ try {
131
+ msg = JSON.parse(data);
132
+ } catch {
133
+ return;
134
+ }
135
+
136
+ handleMessage(conn, msg, () => settle());
137
+ });
138
+
139
+ ws.addEventListener('close', (ev) => {
140
+ const wasAuthenticated = conn.authenticated;
141
+ conn.connected = false;
142
+ conn.authenticated = false;
143
+ stopTickWatch(conn);
144
+
145
+ // Reject all pending requests
146
+ for (const [, pending] of conn.pendingRequests) {
147
+ clearTimeout(pending.timer);
148
+ pending.reject(new Error('Gateway connection closed'));
149
+ }
150
+ conn.pendingRequests.clear();
151
+
152
+ // If we never finished auth, reject the connect promise
153
+ settle(new Error(`Gateway WebSocket closed (code ${ev.code}) before auth completed`));
154
+
155
+ // Auto-reconnect only if: was previously authenticated, not intentionally closed, and config is enabled
156
+ if (wasAuthenticated && !conn.intentionalClose && config.enabled) {
157
+ scheduleReconnect(conn);
158
+ }
159
+ });
160
+
161
+ ws.addEventListener('error', () => {
162
+ // The close event will fire after this, so we don't settle here.
163
+ // Just log for diagnostics.
164
+ console.error(`[OpenClaw] WebSocket error on gateway ${config.name}`);
165
+ });
166
+ } catch (err) {
167
+ settle(err instanceof Error ? err : new Error(String(err)));
168
+ }
169
+ });
170
+ }
171
+
172
+ /**
173
+ * Disconnect from a gateway.
174
+ */
175
+ export function disconnect(gatewayId: string): void {
176
+ const conn = connections.get(gatewayId);
177
+ if (!conn) return;
178
+
179
+ conn.intentionalClose = true;
180
+
181
+ if (conn.reconnectTimer) {
182
+ clearTimeout(conn.reconnectTimer);
183
+ conn.reconnectTimer = null;
184
+ }
185
+
186
+ stopTickWatch(conn);
187
+
188
+ if (conn.ws) {
189
+ conn.ws.close();
190
+ conn.ws = null;
191
+ }
192
+
193
+ conn.connected = false;
194
+ conn.authenticated = false;
195
+ connections.delete(gatewayId);
196
+ }
197
+
198
+ /**
199
+ * Get an existing connection (connects on demand if not yet connected).
200
+ */
201
+ export async function getConnection(config: GatewayConfig): Promise<GatewayConnection> {
202
+ const existing = connections.get(config.id);
203
+ if (existing?.connected && existing.authenticated) {
204
+ return existing;
205
+ }
206
+ return connect(config);
207
+ }
208
+
209
+ /**
210
+ * Get connection status without connecting.
211
+ */
212
+ export function getConnectionStatus(gatewayId: string): { connected: boolean; authenticated: boolean } {
213
+ const conn = connections.get(gatewayId);
214
+ return {
215
+ connected: conn?.connected ?? false,
216
+ authenticated: conn?.authenticated ?? false,
217
+ };
218
+ }
219
+
220
+ /**
221
+ * Send an RPC request to the gateway and wait for the correlated response.
222
+ */
223
+ export function request(conn: GatewayConnection, method: string, params?: unknown, opts?: { expectFinal?: boolean }): Promise<unknown> {
224
+ return new Promise<unknown>((resolve, reject) => {
225
+ if (!conn.ws || !conn.connected || !conn.authenticated) {
226
+ reject(new Error('Gateway not connected or not authenticated'));
227
+ return;
228
+ }
229
+
230
+ const id = nextRequestId();
231
+
232
+ const timer = setTimeout(() => {
233
+ conn.pendingRequests.delete(id);
234
+ reject(new Error(`RPC timeout for method "${method}" (id: ${id})`));
235
+ }, RPC_TIMEOUT_MS);
236
+
237
+ conn.pendingRequests.set(id, { resolve, reject, timer, expectFinal: opts?.expectFinal });
238
+
239
+ const msg: Record<string, unknown> = { type: 'req', id, method };
240
+ if (params !== undefined) {
241
+ msg.params = params;
242
+ }
243
+
244
+ console.log(`[OpenClaw] → req: ${id} ${method}`);
245
+ conn.ws.send(JSON.stringify(msg));
246
+ });
247
+ }
248
+
249
+ /**
250
+ * Subscribe to gateway events by event type.
251
+ */
252
+ export function onEvent(conn: GatewayConnection, eventType: string, listener: (data: unknown) => void): void {
253
+ if (!conn.eventListeners.has(eventType)) {
254
+ conn.eventListeners.set(eventType, []);
255
+ }
256
+ conn.eventListeners.get(eventType)!.push(listener);
257
+ }
258
+
259
+ /**
260
+ * Remove a specific event listener.
261
+ */
262
+ export function offEvent(conn: GatewayConnection, eventType: string, listener: (data: unknown) => void): void {
263
+ const listeners = conn.eventListeners.get(eventType);
264
+ if (!listeners) return;
265
+ const idx = listeners.indexOf(listener);
266
+ if (idx !== -1) listeners.splice(idx, 1);
267
+ }
268
+
269
+ // ---------------------------------------------------------------------------
270
+ // Agent-level helpers (bridge from AgentConfig to internal GatewayConfig)
271
+ // ---------------------------------------------------------------------------
272
+
273
+ /**
274
+ * Convert an http(s) URL to a ws(s) URL for WebSocket connections.
275
+ */
276
+ function wsUrl(url: string): string {
277
+ return url
278
+ .replace(/^https:\/\//, 'wss://')
279
+ .replace(/^http:\/\//, 'ws://')
280
+ .replace(/\/+$/, '');
281
+ }
282
+
283
+ /**
284
+ * Connect an OpenClaw agent via WebSocket.
285
+ * Converts the agent's HTTP URL to a WS URL and uses default operator role/scopes.
286
+ */
287
+ export async function connectAgent(agent: {
288
+ id: string;
289
+ name: string;
290
+ url: string;
291
+ token: string;
292
+ sshTunnel?: { enabled: boolean; command: string; password: string };
293
+ }): Promise<GatewayConnection> {
294
+ // Start SSH tunnel first if configured
295
+ if (agent.sshTunnel?.enabled && agent.sshTunnel.command) {
296
+ const tunnelConfig: SshTunnelConfig = {
297
+ command: agent.sshTunnel.command,
298
+ password: agent.sshTunnel.password,
299
+ };
300
+ await startTunnel(agent.id, tunnelConfig);
301
+ }
302
+
303
+ const gwConfig: GatewayConfig = {
304
+ id: agent.id,
305
+ name: agent.name,
306
+ url: wsUrl(agent.url),
307
+ token: agent.token,
308
+ enabled: true,
309
+ role: 'operator',
310
+ scopes: ['operator.read', 'operator.write', 'operator.approvals'],
311
+ };
312
+ return connect(gwConfig);
313
+ }
314
+
315
+ /**
316
+ * Disconnect an agent's WebSocket connection.
317
+ */
318
+ export function disconnectAgent(agentId: string): void {
319
+ disconnect(agentId);
320
+ stopTunnel(agentId);
321
+ }
322
+
323
+ /**
324
+ * Get an agent's WebSocket connection status without connecting.
325
+ */
326
+ export function getAgentStatus(agentId: string): { connected: boolean; authenticated: boolean } {
327
+ return getConnectionStatus(agentId);
328
+ }
329
+
330
+ // ---------------------------------------------------------------------------
331
+ // Derive the HTTP base URL from the WebSocket URL
332
+ // ---------------------------------------------------------------------------
333
+
334
+ /**
335
+ * Convert a ws:// or wss:// URL to http:// or https:// for the HTTP API.
336
+ * Also passes through http/https URLs unchanged.
337
+ */
338
+ export function httpBaseUrl(config: { url: string }): string {
339
+ return config.url
340
+ .replace(/^wss:\/\//, 'https://')
341
+ .replace(/^ws:\/\//, 'http://')
342
+ .replace(/\/+$/, '');
343
+ }
344
+
345
+ // ---------------------------------------------------------------------------
346
+ // Internal helpers
347
+ // ---------------------------------------------------------------------------
348
+
349
+ function handleMessage(
350
+ conn: GatewayConnection,
351
+ msg: Record<string, unknown>,
352
+ onAuthenticated: () => void
353
+ ): void {
354
+ const type = msg.type as string;
355
+
356
+ switch (type) {
357
+ // --- Events from the server ---
358
+ case 'event': {
359
+ const eventName = msg.event as string;
360
+
361
+ if (eventName === 'tick' || eventName === 'health') {
362
+ if (eventName === 'tick') conn.lastTick = Date.now();
363
+ break;
364
+ }
365
+
366
+ // Skip noisy per-token agent events
367
+ if (eventName === 'agent') {
368
+ // Still dispatch to listeners below, just don't log
369
+ } else {
370
+ console.log(`[OpenClaw] ← event: ${eventName}`);
371
+ if (eventName === 'chat') {
372
+ console.log(`[OpenClaw] ← ${eventName} payload:\n${JSON.stringify(msg.payload, null, 2)}`);
373
+ }
374
+ }
375
+
376
+ if (eventName === 'connect.challenge') {
377
+ sendConnectRequest(conn);
378
+ break;
379
+ }
380
+
381
+ // Dispatch to registered event listeners
382
+ const listeners = conn.eventListeners.get(eventName);
383
+ if (listeners) {
384
+ for (const listener of listeners) {
385
+ try { listener(msg.payload); } catch (err) {
386
+ console.error(`[OpenClaw] Event listener error (${eventName}):`, err);
387
+ }
388
+ }
389
+ }
390
+ // Wildcard listeners
391
+ const wildcardListeners = conn.eventListeners.get('*');
392
+ if (wildcardListeners) {
393
+ for (const listener of wildcardListeners) {
394
+ try { listener(msg); } catch (err) {
395
+ console.error(`[OpenClaw] Wildcard listener error:`, err);
396
+ }
397
+ }
398
+ }
399
+ break;
400
+ }
401
+
402
+ // --- RPC responses (including connect handshake) ---
403
+ case 'res': {
404
+ const id = msg.id as string;
405
+ const ok = msg.ok as boolean;
406
+ const payload = (msg.payload ?? msg.result ?? msg.data ?? {}) as Record<string, unknown>;
407
+ const error = msg.error as Record<string, unknown> | string | undefined;
408
+
409
+ const payloadType = (payload as Record<string, unknown>).type as string | undefined;
410
+ console.log(`[OpenClaw] ← res: ${id} ${ok ? (payloadType ?? 'ok') : 'ERROR'}`);
411
+
412
+ if (!ok || error) {
413
+ console.error(`[OpenClaw] ← error (id: ${id}):\n${JSON.stringify(msg, null, 2)}`);
414
+ }
415
+
416
+ // Check if this is the hello-ok response to our connect request
417
+ if (payload.type === 'hello-ok') {
418
+ conn.authenticated = true;
419
+
420
+ const policy = payload.policy as Record<string, unknown> | undefined;
421
+ if (policy?.tickIntervalMs && typeof policy.tickIntervalMs === 'number') {
422
+ conn.tickIntervalMs = policy.tickIntervalMs;
423
+ conn.lastTick = Date.now();
424
+ startTickWatch(conn);
425
+ }
426
+
427
+ const auth = payload.auth as Record<string, unknown> | undefined;
428
+ if (auth) {
429
+ conn.grantedAuth = {
430
+ role: (auth.role as string) ?? conn.config.role,
431
+ scopes: (auth.scopes as string[]) ?? conn.config.scopes,
432
+ deviceToken: auth.deviceToken as string | undefined,
433
+ };
434
+ }
435
+
436
+ onAuthenticated();
437
+
438
+ // Also resolve the pending request if tracked
439
+ const pending = conn.pendingRequests.get(id);
440
+ if (pending) {
441
+ clearTimeout(pending.timer);
442
+ conn.pendingRequests.delete(id);
443
+ pending.resolve(payload);
444
+ }
445
+ break;
446
+ }
447
+
448
+ // Regular RPC response
449
+ const pending = conn.pendingRequests.get(id);
450
+ if (pending) {
451
+ // Skip intermediate "accepted" acks for long-running requests
452
+ if (pending.expectFinal && payload.status === 'accepted') {
453
+ break;
454
+ }
455
+ clearTimeout(pending.timer);
456
+ conn.pendingRequests.delete(id);
457
+ if (!ok || error) {
458
+ const errMsg = typeof error === 'string' ? error
459
+ : (error as Record<string, unknown>)?.message as string ?? JSON.stringify(error ?? 'Unknown error');
460
+ pending.reject(new Error(errMsg));
461
+ } else {
462
+ pending.resolve(payload);
463
+ }
464
+ }
465
+ break;
466
+ }
467
+ }
468
+ }
469
+
470
+ /**
471
+ * Send the `connect` RPC request after receiving the server challenge.
472
+ *
473
+ * OpenClaw protocol requires:
474
+ * - client: { id: 'gateway-client', version, platform, mode: 'backend' }
475
+ * - device: { id, publicKey, signature, signedAt, nonce }
476
+ * - auth: { token }
477
+ *
478
+ * Device signature payload (pipe-delimited):
479
+ * v2 (with nonce): "v2|deviceId|clientId|clientMode|role|scopes|signedAt|token|nonce"
480
+ * v1 (no nonce): "v1|deviceId|clientId|clientMode|role|scopes|signedAt|token"
481
+ */
482
+ function sendConnectRequest(conn: GatewayConnection): void {
483
+ const id = nextRequestId();
484
+
485
+ const connectMsg: Record<string, unknown> = {
486
+ type: 'req',
487
+ id,
488
+ method: 'connect',
489
+ params: {
490
+ minProtocol: 3,
491
+ maxProtocol: 3,
492
+ client: {
493
+ id: 'gateway-client',
494
+ version: '1.0.0',
495
+ platform: process.platform,
496
+ mode: 'backend',
497
+ },
498
+ role: conn.config.role,
499
+ scopes: conn.config.scopes,
500
+ caps: [],
501
+ commands: [],
502
+ permissions: {},
503
+ auth: { token: conn.config.token },
504
+ locale: 'en-US',
505
+ userAgent: 'SynthOS/1.0',
506
+ },
507
+ };
508
+
509
+ // Track as a pending request so we can correlate the response
510
+ conn.pendingRequests.set(id, {
511
+ resolve: () => { /* handled in handleMessage via hello-ok check */ },
512
+ reject: (err: Error) => {
513
+ console.error(`[OpenClaw] Connect request failed for ${conn.config.name}: ${err.message}`);
514
+ },
515
+ timer: setTimeout(() => {
516
+ conn.pendingRequests.delete(id);
517
+ }, AUTH_TIMEOUT_MS),
518
+ });
519
+
520
+ conn.ws?.send(JSON.stringify(connectMsg));
521
+ }
522
+
523
+ /** Monitor server tick events — close if stalled (2x interval with no tick). */
524
+ function startTickWatch(conn: GatewayConnection): void {
525
+ stopTickWatch(conn);
526
+ if (conn.tickIntervalMs <= 0) return;
527
+ conn.tickTimer = setInterval(() => {
528
+ const elapsed = Date.now() - conn.lastTick;
529
+ if (elapsed > conn.tickIntervalMs * 2) {
530
+ conn.ws?.close(4000, 'tick stall');
531
+ }
532
+ }, conn.tickIntervalMs);
533
+ }
534
+
535
+ function stopTickWatch(conn: GatewayConnection): void {
536
+ if (conn.tickTimer) {
537
+ clearInterval(conn.tickTimer);
538
+ conn.tickTimer = null;
539
+ }
540
+ }
541
+
542
+ function scheduleReconnect(conn: GatewayConnection): void {
543
+ if (conn.reconnectTimer || conn.intentionalClose) return;
544
+
545
+ conn.reconnectAttempts++;
546
+ const delay = Math.min(
547
+ BASE_RECONNECT_DELAY_MS * Math.pow(2, conn.reconnectAttempts - 1),
548
+ MAX_RECONNECT_DELAY_MS
549
+ );
550
+
551
+ conn.reconnectTimer = setTimeout(async () => {
552
+ conn.reconnectTimer = null;
553
+ try {
554
+ await connect(conn.config);
555
+ } catch (err) {
556
+ console.error(`[OpenClaw] Reconnect failed for ${conn.config.name}:`, err instanceof Error ? err.message : err);
557
+ }
558
+ }, delay);
559
+ }