agent-relay 3.0.2 → 3.1.1

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 (266) hide show
  1. package/README.md +8 -0
  2. package/bin/agent-relay-broker-linux-x64 +0 -0
  3. package/dist/index.cjs +273 -56
  4. package/dist/src/cli/commands/core.d.ts +2 -0
  5. package/dist/src/cli/commands/core.d.ts.map +1 -1
  6. package/dist/src/cli/commands/core.js +9 -2
  7. package/dist/src/cli/commands/core.js.map +1 -1
  8. package/dist/src/cli/lib/broker-lifecycle.d.ts.map +1 -1
  9. package/dist/src/cli/lib/broker-lifecycle.js +87 -28
  10. package/dist/src/cli/lib/broker-lifecycle.js.map +1 -1
  11. package/package.json +9 -9
  12. package/packages/acp-bridge/README.md +50 -67
  13. package/packages/acp-bridge/package.json +2 -2
  14. package/packages/config/package.json +1 -1
  15. package/packages/hooks/package.json +4 -4
  16. package/packages/memory/package.json +2 -2
  17. package/packages/openclaw/README.md +78 -0
  18. package/packages/openclaw/bin/relay-openclaw.mjs +2 -0
  19. package/packages/openclaw/bridge/bridge.mjs +305 -0
  20. package/packages/openclaw/dist/__tests__/gateway-threads.test.d.ts +2 -0
  21. package/packages/openclaw/dist/__tests__/gateway-threads.test.d.ts.map +1 -0
  22. package/packages/openclaw/dist/__tests__/gateway-threads.test.js +320 -0
  23. package/packages/openclaw/dist/__tests__/gateway-threads.test.js.map +1 -0
  24. package/packages/openclaw/dist/__tests__/naming.test.d.ts +2 -0
  25. package/packages/openclaw/dist/__tests__/naming.test.d.ts.map +1 -0
  26. package/packages/openclaw/dist/__tests__/naming.test.js +21 -0
  27. package/packages/openclaw/dist/__tests__/naming.test.js.map +1 -0
  28. package/packages/openclaw/dist/__tests__/spawn-manager.test.d.ts +2 -0
  29. package/packages/openclaw/dist/__tests__/spawn-manager.test.d.ts.map +1 -0
  30. package/packages/openclaw/dist/__tests__/spawn-manager.test.js +126 -0
  31. package/packages/openclaw/dist/__tests__/spawn-manager.test.js.map +1 -0
  32. package/packages/openclaw/dist/auth/converter.d.ts +28 -0
  33. package/packages/openclaw/dist/auth/converter.d.ts.map +1 -0
  34. package/packages/openclaw/dist/auth/converter.js +64 -0
  35. package/packages/openclaw/dist/auth/converter.js.map +1 -0
  36. package/packages/openclaw/dist/cli.d.ts +2 -0
  37. package/packages/openclaw/dist/cli.d.ts.map +1 -0
  38. package/packages/openclaw/dist/cli.js +230 -0
  39. package/packages/openclaw/dist/cli.js.map +1 -0
  40. package/packages/openclaw/dist/config.d.ts +27 -0
  41. package/packages/openclaw/dist/config.d.ts.map +1 -0
  42. package/packages/openclaw/dist/config.js +97 -0
  43. package/packages/openclaw/dist/config.js.map +1 -0
  44. package/packages/openclaw/dist/control.d.ts +22 -0
  45. package/packages/openclaw/dist/control.d.ts.map +1 -0
  46. package/packages/openclaw/dist/control.js +58 -0
  47. package/packages/openclaw/dist/control.js.map +1 -0
  48. package/packages/openclaw/dist/gateway.d.ts +71 -0
  49. package/packages/openclaw/dist/gateway.d.ts.map +1 -0
  50. package/packages/openclaw/dist/gateway.js +785 -0
  51. package/packages/openclaw/dist/gateway.js.map +1 -0
  52. package/packages/openclaw/dist/identity/contract.d.ts +11 -0
  53. package/packages/openclaw/dist/identity/contract.d.ts.map +1 -0
  54. package/packages/openclaw/dist/identity/contract.js +40 -0
  55. package/packages/openclaw/dist/identity/contract.js.map +1 -0
  56. package/packages/openclaw/dist/identity/files.d.ts +33 -0
  57. package/packages/openclaw/dist/identity/files.d.ts.map +1 -0
  58. package/packages/openclaw/dist/identity/files.js +145 -0
  59. package/packages/openclaw/dist/identity/files.js.map +1 -0
  60. package/packages/openclaw/dist/identity/model.d.ts +11 -0
  61. package/packages/openclaw/dist/identity/model.d.ts.map +1 -0
  62. package/packages/openclaw/dist/identity/model.js +28 -0
  63. package/packages/openclaw/dist/identity/model.js.map +1 -0
  64. package/packages/openclaw/dist/identity/naming.d.ts +5 -0
  65. package/packages/openclaw/dist/identity/naming.d.ts.map +1 -0
  66. package/packages/openclaw/dist/identity/naming.js +7 -0
  67. package/packages/openclaw/dist/identity/naming.js.map +1 -0
  68. package/packages/openclaw/dist/index.d.ts +20 -0
  69. package/packages/openclaw/dist/index.d.ts.map +1 -0
  70. package/packages/openclaw/dist/index.js +27 -0
  71. package/packages/openclaw/dist/index.js.map +1 -0
  72. package/packages/openclaw/dist/inject.d.ts +14 -0
  73. package/packages/openclaw/dist/inject.d.ts.map +1 -0
  74. package/packages/openclaw/dist/inject.js +66 -0
  75. package/packages/openclaw/dist/inject.js.map +1 -0
  76. package/packages/openclaw/dist/mcp/server.d.ts +8 -0
  77. package/packages/openclaw/dist/mcp/server.d.ts.map +1 -0
  78. package/packages/openclaw/dist/mcp/server.js +105 -0
  79. package/packages/openclaw/dist/mcp/server.js.map +1 -0
  80. package/packages/openclaw/dist/mcp/tools.d.ts +17 -0
  81. package/packages/openclaw/dist/mcp/tools.d.ts.map +1 -0
  82. package/packages/openclaw/dist/mcp/tools.js +145 -0
  83. package/packages/openclaw/dist/mcp/tools.js.map +1 -0
  84. package/packages/openclaw/dist/runtime/openclaw-config.d.ts +20 -0
  85. package/packages/openclaw/dist/runtime/openclaw-config.d.ts.map +1 -0
  86. package/packages/openclaw/dist/runtime/openclaw-config.js +50 -0
  87. package/packages/openclaw/dist/runtime/openclaw-config.js.map +1 -0
  88. package/packages/openclaw/dist/runtime/patch.d.ts +24 -0
  89. package/packages/openclaw/dist/runtime/patch.d.ts.map +1 -0
  90. package/packages/openclaw/dist/runtime/patch.js +92 -0
  91. package/packages/openclaw/dist/runtime/patch.js.map +1 -0
  92. package/packages/openclaw/dist/runtime/setup.d.ts +26 -0
  93. package/packages/openclaw/dist/runtime/setup.d.ts.map +1 -0
  94. package/packages/openclaw/dist/runtime/setup.js +58 -0
  95. package/packages/openclaw/dist/runtime/setup.js.map +1 -0
  96. package/packages/openclaw/dist/setup.d.ts +29 -0
  97. package/packages/openclaw/dist/setup.d.ts.map +1 -0
  98. package/packages/openclaw/dist/setup.js +300 -0
  99. package/packages/openclaw/dist/setup.js.map +1 -0
  100. package/packages/openclaw/dist/spawn/docker.d.ts +58 -0
  101. package/packages/openclaw/dist/spawn/docker.d.ts.map +1 -0
  102. package/packages/openclaw/dist/spawn/docker.js +222 -0
  103. package/packages/openclaw/dist/spawn/docker.js.map +1 -0
  104. package/packages/openclaw/dist/spawn/manager.d.ts +45 -0
  105. package/packages/openclaw/dist/spawn/manager.d.ts.map +1 -0
  106. package/packages/openclaw/dist/spawn/manager.js +140 -0
  107. package/packages/openclaw/dist/spawn/manager.js.map +1 -0
  108. package/packages/openclaw/dist/spawn/process.d.ts +16 -0
  109. package/packages/openclaw/dist/spawn/process.d.ts.map +1 -0
  110. package/packages/openclaw/dist/spawn/process.js +241 -0
  111. package/packages/openclaw/dist/spawn/process.js.map +1 -0
  112. package/packages/openclaw/dist/spawn/types.d.ts +42 -0
  113. package/packages/openclaw/dist/spawn/types.d.ts.map +1 -0
  114. package/packages/openclaw/dist/spawn/types.js +2 -0
  115. package/packages/openclaw/dist/spawn/types.js.map +1 -0
  116. package/packages/openclaw/dist/types.d.ts +37 -0
  117. package/packages/openclaw/dist/types.d.ts.map +1 -0
  118. package/packages/openclaw/dist/types.js +2 -0
  119. package/packages/openclaw/dist/types.js.map +1 -0
  120. package/packages/openclaw/package.json +63 -0
  121. package/packages/openclaw/skill/SKILL.md +194 -0
  122. package/packages/openclaw/src/__tests__/gateway-threads.test.ts +384 -0
  123. package/packages/openclaw/src/__tests__/naming.test.ts +24 -0
  124. package/packages/openclaw/src/__tests__/spawn-manager.test.ts +152 -0
  125. package/packages/openclaw/src/auth/converter.ts +90 -0
  126. package/packages/openclaw/src/cli.ts +269 -0
  127. package/packages/openclaw/src/config.ts +124 -0
  128. package/packages/openclaw/src/control.ts +100 -0
  129. package/packages/openclaw/src/gateway.ts +941 -0
  130. package/packages/openclaw/src/identity/contract.ts +44 -0
  131. package/packages/openclaw/src/identity/files.ts +198 -0
  132. package/packages/openclaw/src/identity/model.ts +27 -0
  133. package/packages/openclaw/src/identity/naming.ts +6 -0
  134. package/packages/openclaw/src/index.ts +59 -0
  135. package/packages/openclaw/src/inject.ts +77 -0
  136. package/packages/openclaw/src/mcp/server.ts +121 -0
  137. package/packages/openclaw/src/mcp/tools.ts +174 -0
  138. package/packages/openclaw/src/runtime/openclaw-config.ts +64 -0
  139. package/packages/openclaw/src/runtime/patch.ts +103 -0
  140. package/packages/openclaw/src/runtime/setup.ts +89 -0
  141. package/packages/openclaw/src/setup.ts +336 -0
  142. package/packages/openclaw/src/spawn/docker.ts +261 -0
  143. package/packages/openclaw/src/spawn/manager.ts +181 -0
  144. package/packages/openclaw/src/spawn/process.ts +272 -0
  145. package/packages/openclaw/src/spawn/types.ts +43 -0
  146. package/packages/openclaw/src/types.ts +38 -0
  147. package/packages/openclaw/templates/SOUL.md.template +34 -0
  148. package/packages/openclaw/tsconfig.json +12 -0
  149. package/packages/policy/package.json +2 -2
  150. package/packages/sdk/README.md +169 -64
  151. package/packages/sdk/dist/__tests__/contract-fixtures.test.js +76 -9
  152. package/packages/sdk/dist/__tests__/contract-fixtures.test.js.map +1 -1
  153. package/packages/sdk/dist/__tests__/integration.test.js +5 -4
  154. package/packages/sdk/dist/__tests__/integration.test.js.map +1 -1
  155. package/packages/sdk/dist/client.d.ts +34 -3
  156. package/packages/sdk/dist/client.d.ts.map +1 -1
  157. package/packages/sdk/dist/client.js +120 -10
  158. package/packages/sdk/dist/client.js.map +1 -1
  159. package/packages/sdk/dist/protocol.d.ts +7 -1
  160. package/packages/sdk/dist/protocol.d.ts.map +1 -1
  161. package/packages/sdk/dist/relay.d.ts +47 -11
  162. package/packages/sdk/dist/relay.d.ts.map +1 -1
  163. package/packages/sdk/dist/relay.js +114 -23
  164. package/packages/sdk/dist/relay.js.map +1 -1
  165. package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
  166. package/packages/sdk/dist/workflows/runner.js +71 -36
  167. package/packages/sdk/dist/workflows/runner.js.map +1 -1
  168. package/packages/sdk/dist/workflows/types.d.ts +1 -1
  169. package/packages/sdk/dist/workflows/types.d.ts.map +1 -1
  170. package/packages/sdk/package.json +2 -2
  171. package/packages/sdk/src/__tests__/contract-fixtures.test.ts +88 -9
  172. package/packages/sdk/src/__tests__/error-scenarios.test.ts +1 -1
  173. package/packages/sdk/src/__tests__/idle-nudge.test.ts +205 -257
  174. package/packages/sdk/src/__tests__/integration.test.ts +5 -4
  175. package/packages/sdk/src/__tests__/orchestration-upgrades.test.ts +277 -13
  176. package/packages/sdk/src/__tests__/swarm-coordinator.test.ts +1 -0
  177. package/packages/sdk/src/__tests__/workflow-runner.test.ts +67 -7
  178. package/packages/sdk/src/__tests__/workflow-trajectory.test.ts +4 -5
  179. package/packages/sdk/src/client.ts +171 -14
  180. package/packages/sdk/src/examples/workflows/runner-idle-refactor.yaml +306 -0
  181. package/packages/sdk/src/protocol.ts +7 -2
  182. package/packages/sdk/src/relay.ts +196 -34
  183. package/packages/sdk/src/workflows/runner.ts +73 -42
  184. package/packages/sdk/src/workflows/schema.json +1 -1
  185. package/packages/sdk/src/workflows/types.ts +1 -1
  186. package/packages/sdk/vitest.config.ts +1 -0
  187. package/packages/sdk-py/README.md +89 -102
  188. package/packages/sdk-py/agent_relay/__init__.py +16 -19
  189. package/packages/sdk-py/pyproject.toml +5 -1
  190. package/packages/sdk-py/src/agent_relay/__init__.py +35 -1
  191. package/packages/sdk-py/src/agent_relay/client.py +776 -0
  192. package/packages/sdk-py/src/agent_relay/models.py +27 -0
  193. package/packages/sdk-py/src/agent_relay/protocol.py +114 -0
  194. package/packages/sdk-py/src/agent_relay/relay.py +860 -0
  195. package/packages/sdk-py/tests/test_relay_lifecycle_hooks.py +250 -0
  196. package/packages/telemetry/package.json +1 -1
  197. package/packages/trajectory/package.json +2 -2
  198. package/packages/user-directory/package.json +2 -2
  199. package/packages/utils/package.json +2 -2
  200. package/bin/agent-relay-broker-darwin-arm64 +0 -0
  201. package/bin/agent-relay-broker-darwin-x64 +0 -0
  202. package/bin/agent-relay-broker-linux-arm64 +0 -0
  203. package/packages/sdk/.trajectories/active/traj_1771875803391_84ca57b2.json +0 -50
  204. package/packages/sdk/.trajectories/active/traj_1771891934534_06504121.json +0 -50
  205. package/packages/sdk/.trajectories/active/traj_1771891957929_211afc4e.json +0 -50
  206. package/packages/sdk/.trajectories/active/traj_1771891982509_38c84638.json +0 -50
  207. package/packages/sdk/.trajectories/completed/traj_1771875803188_cd6d181c.json +0 -80
  208. package/packages/sdk/.trajectories/completed/traj_1771875803204_f2aeb8c8.json +0 -80
  209. package/packages/sdk/.trajectories/completed/traj_1771875803210_d65f3f1a.json +0 -80
  210. package/packages/sdk/.trajectories/completed/traj_1771875803218_e454a25d.json +0 -80
  211. package/packages/sdk/.trajectories/completed/traj_1771875803223_d7a64815.json +0 -80
  212. package/packages/sdk/.trajectories/completed/traj_1771875803227_7e56da5b.json +0 -80
  213. package/packages/sdk/.trajectories/completed/traj_1771875803235_4fbf93b4.json +0 -80
  214. package/packages/sdk/.trajectories/completed/traj_1771875803243_47931c71.json +0 -80
  215. package/packages/sdk/.trajectories/completed/traj_1771875803258_3816f3fe.json +0 -80
  216. package/packages/sdk/.trajectories/completed/traj_1771875803268_8061140e.json +0 -80
  217. package/packages/sdk/.trajectories/completed/traj_1771875803326_ae6f9c78.json +0 -80
  218. package/packages/sdk/.trajectories/completed/traj_1771875808396_cbde0a6c.json +0 -91
  219. package/packages/sdk/.trajectories/completed/traj_1771875812026_aa2442bb.json +0 -91
  220. package/packages/sdk/.trajectories/completed/traj_1771875815431_c2c656c5.json +0 -91
  221. package/packages/sdk/.trajectories/completed/traj_1771875818645_3a4dbf02.json +0 -91
  222. package/packages/sdk/.trajectories/completed/traj_1771891934403_24923c03.json +0 -80
  223. package/packages/sdk/.trajectories/completed/traj_1771891934421_dca16e24.json +0 -80
  224. package/packages/sdk/.trajectories/completed/traj_1771891934430_057706f7.json +0 -80
  225. package/packages/sdk/.trajectories/completed/traj_1771891934442_faf97382.json +0 -80
  226. package/packages/sdk/.trajectories/completed/traj_1771891934454_5542ecd5.json +0 -80
  227. package/packages/sdk/.trajectories/completed/traj_1771891934464_12202a08.json +0 -80
  228. package/packages/sdk/.trajectories/completed/traj_1771891934487_94378275.json +0 -80
  229. package/packages/sdk/.trajectories/completed/traj_1771891934503_ca728c13.json +0 -80
  230. package/packages/sdk/.trajectories/completed/traj_1771891934519_100af69a.json +0 -80
  231. package/packages/sdk/.trajectories/completed/traj_1771891934536_62ad39d9.json +0 -80
  232. package/packages/sdk/.trajectories/completed/traj_1771891934553_d6798a52.json +0 -80
  233. package/packages/sdk/.trajectories/completed/traj_1771891939537_541c8096.json +0 -91
  234. package/packages/sdk/.trajectories/completed/traj_1771891942985_36ab9a4d.json +0 -91
  235. package/packages/sdk/.trajectories/completed/traj_1771891946453_e8a6e05f.json +0 -91
  236. package/packages/sdk/.trajectories/completed/traj_1771891949838_5de0de84.json +0 -91
  237. package/packages/sdk/.trajectories/completed/traj_1771891957807_0ecfb4f4.json +0 -80
  238. package/packages/sdk/.trajectories/completed/traj_1771891957827_c4539239.json +0 -80
  239. package/packages/sdk/.trajectories/completed/traj_1771891957836_91168b48.json +0 -80
  240. package/packages/sdk/.trajectories/completed/traj_1771891957848_8c5cad0b.json +0 -80
  241. package/packages/sdk/.trajectories/completed/traj_1771891957857_0986b293.json +0 -80
  242. package/packages/sdk/.trajectories/completed/traj_1771891957872_8a3113af.json +0 -80
  243. package/packages/sdk/.trajectories/completed/traj_1771891957884_0bb85208.json +0 -80
  244. package/packages/sdk/.trajectories/completed/traj_1771891957892_86c75e2e.json +0 -80
  245. package/packages/sdk/.trajectories/completed/traj_1771891957907_98ca0e6f.json +0 -80
  246. package/packages/sdk/.trajectories/completed/traj_1771891957918_d9091231.json +0 -80
  247. package/packages/sdk/.trajectories/completed/traj_1771891957931_dcaf77ed.json +0 -80
  248. package/packages/sdk/.trajectories/completed/traj_1771891962931_eb1fdee2.json +0 -91
  249. package/packages/sdk/.trajectories/completed/traj_1771891966262_9061a93f.json +0 -91
  250. package/packages/sdk/.trajectories/completed/traj_1771891969915_1adaba19.json +0 -91
  251. package/packages/sdk/.trajectories/completed/traj_1771891973588_f08b79e9.json +0 -91
  252. package/packages/sdk/.trajectories/completed/traj_1771891982421_f1985bce.json +0 -80
  253. package/packages/sdk/.trajectories/completed/traj_1771891982432_e7a84163.json +0 -80
  254. package/packages/sdk/.trajectories/completed/traj_1771891982447_369b842a.json +0 -80
  255. package/packages/sdk/.trajectories/completed/traj_1771891982469_5fc45199.json +0 -80
  256. package/packages/sdk/.trajectories/completed/traj_1771891982495_454c7cb3.json +0 -80
  257. package/packages/sdk/.trajectories/completed/traj_1771891982514_08098e03.json +0 -80
  258. package/packages/sdk/.trajectories/completed/traj_1771891982526_b351d778.json +0 -80
  259. package/packages/sdk/.trajectories/completed/traj_1771891982533_fa542d83.json +0 -80
  260. package/packages/sdk/.trajectories/completed/traj_1771891982540_18ab24dc.json +0 -80
  261. package/packages/sdk/.trajectories/completed/traj_1771891982544_5b4fa163.json +0 -80
  262. package/packages/sdk/.trajectories/completed/traj_1771891982548_c13f089a.json +0 -80
  263. package/packages/sdk/.trajectories/completed/traj_1771891987510_23f6da1f.json +0 -91
  264. package/packages/sdk/.trajectories/completed/traj_1771891991466_912c2e04.json +0 -91
  265. package/packages/sdk/.trajectories/completed/traj_1771891994891_60604be2.json +0 -91
  266. package/packages/sdk/.trajectories/completed/traj_1771891998370_cfaf9b8b.json +0 -91
@@ -0,0 +1,785 @@
1
+ import { createHash, generateKeyPairSync, sign } from 'node:crypto';
2
+ import { createServer } from 'node:http';
3
+ import { RelayCast } from '@relaycast/sdk';
4
+ import WebSocket from 'ws';
5
+ import { SpawnManager } from './spawn/manager.js';
6
+ function normalizeChannelName(channel) {
7
+ return channel.startsWith('#') ? channel.slice(1) : channel;
8
+ }
9
+ function generateDeviceIdentity() {
10
+ const { publicKey, privateKey } = generateKeyPairSync('ed25519');
11
+ // Extract raw 32-byte public key from SPKI DER (12-byte header for Ed25519)
12
+ const rawPublicBytes = publicKey.export({ type: 'spki', format: 'der' }).subarray(12);
13
+ const deviceId = createHash('sha256').update(rawPublicBytes).digest('hex');
14
+ const publicKeyB64 = Buffer.from(rawPublicBytes).toString('base64url');
15
+ return {
16
+ publicKeyB64,
17
+ privateKeyObj: privateKey,
18
+ deviceId,
19
+ };
20
+ }
21
+ function signConnectPayload(device, params) {
22
+ // v3 payload format: v3|deviceId|clientId|clientMode|role|scopes|signedAtMs|token|nonce|platform|deviceFamily
23
+ const payload = [
24
+ 'v3',
25
+ device.deviceId,
26
+ params.clientId,
27
+ params.clientMode,
28
+ params.role,
29
+ params.scopes.join(','),
30
+ String(params.signedAt),
31
+ params.token || '',
32
+ params.nonce,
33
+ params.platform,
34
+ params.deviceFamily,
35
+ ].join('|');
36
+ const payloadBytes = Buffer.from(payload, 'utf-8');
37
+ // Ed25519 sign — no hash algorithm needed (null), it's built into Ed25519
38
+ const signature = sign(null, payloadBytes, device.privateKeyObj);
39
+ return Buffer.from(signature).toString('base64url');
40
+ }
41
+ class OpenClawGatewayClient {
42
+ ws = null;
43
+ authenticated = false;
44
+ device;
45
+ token;
46
+ port;
47
+ pendingRpcs = new Map();
48
+ rpcIdCounter = 0;
49
+ reconnectTimer = null;
50
+ stopped = false;
51
+ connectPromise = null;
52
+ connectResolve = null;
53
+ connectReject = null;
54
+ connectTimeout = null;
55
+ /** Default timeout for initial connection (30 seconds). */
56
+ static CONNECT_TIMEOUT_MS = 30_000;
57
+ constructor(token, port) {
58
+ this.token = token;
59
+ this.port = port;
60
+ this.device = generateDeviceIdentity();
61
+ }
62
+ /** Connect and authenticate. Resolves when chat.send is ready, rejects on timeout or error. */
63
+ async connect() {
64
+ if (this.authenticated && this.ws?.readyState === WebSocket.OPEN)
65
+ return;
66
+ // Cancel any pending reconnect timer to prevent orphaned WebSocket connections
67
+ if (this.reconnectTimer) {
68
+ clearTimeout(this.reconnectTimer);
69
+ this.reconnectTimer = null;
70
+ }
71
+ this.connectPromise = new Promise((resolve, reject) => {
72
+ this.connectResolve = resolve;
73
+ this.connectReject = reject;
74
+ // Set up timeout to prevent indefinite hanging
75
+ this.connectTimeout = setTimeout(() => {
76
+ this.connectTimeout = null;
77
+ if (!this.authenticated) {
78
+ const err = new Error(`Connection to OpenClaw gateway timed out after ${OpenClawGatewayClient.CONNECT_TIMEOUT_MS}ms`);
79
+ this.connectReject?.(err);
80
+ this.connectReject = null;
81
+ this.connectResolve = null;
82
+ }
83
+ }, OpenClawGatewayClient.CONNECT_TIMEOUT_MS);
84
+ });
85
+ this.doConnect();
86
+ return this.connectPromise;
87
+ }
88
+ clearConnectTimeout() {
89
+ if (this.connectTimeout) {
90
+ clearTimeout(this.connectTimeout);
91
+ this.connectTimeout = null;
92
+ }
93
+ }
94
+ doConnect() {
95
+ if (this.stopped)
96
+ return;
97
+ try {
98
+ this.ws = new WebSocket(`ws://127.0.0.1:${this.port}`);
99
+ }
100
+ catch (err) {
101
+ console.warn(`[openclaw-ws] Connection failed: ${err instanceof Error ? err.message : String(err)}`);
102
+ this.scheduleReconnect();
103
+ return;
104
+ }
105
+ this.ws.on('open', () => {
106
+ console.log('[openclaw-ws] Connected to OpenClaw gateway');
107
+ });
108
+ this.ws.on('message', (data) => {
109
+ this.handleMessage(data.toString());
110
+ });
111
+ this.ws.on('close', (code, reason) => {
112
+ console.warn(`[openclaw-ws] Disconnected: ${code} ${reason.toString()}`);
113
+ const wasAuthenticated = this.authenticated;
114
+ this.authenticated = false;
115
+ // Reject all pending RPCs
116
+ for (const [id, pending] of this.pendingRpcs) {
117
+ clearTimeout(pending.timer);
118
+ pending.resolve(false);
119
+ this.pendingRpcs.delete(id);
120
+ }
121
+ // If we weren't authenticated yet, reject the connect promise
122
+ if (!wasAuthenticated && this.connectReject) {
123
+ this.clearConnectTimeout();
124
+ const err = new Error(`WebSocket closed before authentication (code=${code})`);
125
+ this.connectReject(err);
126
+ this.connectReject = null;
127
+ this.connectResolve = null;
128
+ }
129
+ if (!this.stopped) {
130
+ this.scheduleReconnect();
131
+ }
132
+ });
133
+ this.ws.on('error', (err) => {
134
+ console.warn(`[openclaw-ws] Error: ${err.message}`);
135
+ // If we weren't authenticated yet, reject the connect promise
136
+ if (!this.authenticated && this.connectReject) {
137
+ this.clearConnectTimeout();
138
+ this.connectReject(err);
139
+ this.connectReject = null;
140
+ this.connectResolve = null;
141
+ }
142
+ });
143
+ }
144
+ handleMessage(raw) {
145
+ let msg;
146
+ try {
147
+ msg = JSON.parse(raw);
148
+ }
149
+ catch {
150
+ return;
151
+ }
152
+ // Handle connect.challenge — sign and respond
153
+ if (msg.type === 'event' && msg.event === 'connect.challenge') {
154
+ const payload = msg.payload;
155
+ console.log('[openclaw-ws] Received connect.challenge, signing...');
156
+ const signedAt = Date.now();
157
+ const clientId = 'cli';
158
+ const clientMode = 'cli';
159
+ const platform = process.platform === 'darwin' ? 'macos' : 'linux';
160
+ const deviceFamily = 'cli';
161
+ const role = 'operator';
162
+ const scopes = ['operator.read', 'operator.write'];
163
+ const signature = signConnectPayload(this.device, {
164
+ clientId,
165
+ clientMode,
166
+ platform,
167
+ deviceFamily,
168
+ role,
169
+ scopes,
170
+ signedAt,
171
+ token: this.token,
172
+ nonce: payload.nonce,
173
+ });
174
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
175
+ console.warn('[openclaw-ws] WebSocket not open when trying to send connect');
176
+ return;
177
+ }
178
+ this.ws.send(JSON.stringify({
179
+ type: 'req',
180
+ id: 'connect-1',
181
+ method: 'connect',
182
+ params: {
183
+ minProtocol: 3,
184
+ maxProtocol: 3,
185
+ client: {
186
+ id: clientId,
187
+ version: '1.0.0',
188
+ platform,
189
+ mode: clientMode,
190
+ deviceFamily,
191
+ },
192
+ role,
193
+ scopes,
194
+ caps: [],
195
+ commands: [],
196
+ permissions: {},
197
+ auth: { token: this.token },
198
+ locale: 'en-US',
199
+ userAgent: 'relaycast-gateway/1.0.0',
200
+ device: {
201
+ id: this.device.deviceId,
202
+ publicKey: this.device.publicKeyB64,
203
+ signature,
204
+ signedAt,
205
+ nonce: payload.nonce,
206
+ },
207
+ },
208
+ }));
209
+ return;
210
+ }
211
+ // Handle connect response
212
+ if (msg.type === 'res' && msg.id === 'connect-1') {
213
+ this.clearConnectTimeout();
214
+ if (msg.ok) {
215
+ console.log('[openclaw-ws] Authenticated successfully');
216
+ this.authenticated = true;
217
+ this.connectResolve?.();
218
+ this.connectResolve = null;
219
+ this.connectReject = null;
220
+ }
221
+ else {
222
+ console.warn(`[openclaw-ws] Auth rejected: ${JSON.stringify(msg.error ?? msg)}`);
223
+ // Reject the connect promise on auth failure
224
+ const errMsg = msg.error ? JSON.stringify(msg.error) : 'Authentication rejected';
225
+ this.connectReject?.(new Error(`OpenClaw gateway auth failed: ${errMsg}`));
226
+ this.connectReject = null;
227
+ this.connectResolve = null;
228
+ }
229
+ return;
230
+ }
231
+ // Handle RPC responses
232
+ const id = msg.id;
233
+ if (id && this.pendingRpcs.has(id)) {
234
+ const pending = this.pendingRpcs.get(id);
235
+ clearTimeout(pending.timer);
236
+ this.pendingRpcs.delete(id);
237
+ if (msg.ok === false || msg.error) {
238
+ console.warn(`[openclaw-ws] RPC ${id} error: ${JSON.stringify(msg.error ?? msg)}`);
239
+ pending.resolve(false);
240
+ }
241
+ else {
242
+ const result = msg.payload;
243
+ console.log(`[openclaw-ws] RPC ${id} ok: runId=${result?.runId ?? 'n/a'} status=${result?.status ?? 'n/a'}`);
244
+ pending.resolve(true);
245
+ }
246
+ return;
247
+ }
248
+ // Log other events at debug level
249
+ if (msg.type === 'event') {
250
+ // chat events, tick events, etc. — ignore silently
251
+ }
252
+ }
253
+ /** Send a chat.send RPC. Returns true if accepted. */
254
+ async sendChatMessage(text, idempotencyKey) {
255
+ if (!this.authenticated || !this.ws || this.ws.readyState !== WebSocket.OPEN) {
256
+ // Try to reconnect
257
+ try {
258
+ await this.connect();
259
+ }
260
+ catch {
261
+ return false;
262
+ }
263
+ if (!this.authenticated)
264
+ return false;
265
+ }
266
+ const id = `chat-${++this.rpcIdCounter}-${Date.now()}`;
267
+ return new Promise((resolve) => {
268
+ const timer = setTimeout(() => {
269
+ console.warn(`[openclaw-ws] chat.send ${id} timed out`);
270
+ this.pendingRpcs.delete(id);
271
+ resolve(false);
272
+ }, 15_000);
273
+ this.pendingRpcs.set(id, { resolve, timer });
274
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
275
+ clearTimeout(timer);
276
+ this.pendingRpcs.delete(id);
277
+ resolve(false);
278
+ return;
279
+ }
280
+ this.ws.send(JSON.stringify({
281
+ type: 'req',
282
+ id,
283
+ method: 'chat.send',
284
+ params: {
285
+ sessionKey: 'agent:main:main',
286
+ message: text,
287
+ ...(idempotencyKey ? { idempotencyKey } : {}),
288
+ },
289
+ }));
290
+ });
291
+ }
292
+ scheduleReconnect() {
293
+ if (this.stopped || this.reconnectTimer)
294
+ return;
295
+ console.log('[openclaw-ws] Reconnecting in 3s...');
296
+ this.reconnectTimer = setTimeout(() => {
297
+ this.reconnectTimer = null;
298
+ this.doConnect();
299
+ }, 3_000);
300
+ }
301
+ async disconnect() {
302
+ this.stopped = true;
303
+ this.clearConnectTimeout();
304
+ if (this.reconnectTimer) {
305
+ clearTimeout(this.reconnectTimer);
306
+ this.reconnectTimer = null;
307
+ }
308
+ for (const [id, pending] of this.pendingRpcs) {
309
+ clearTimeout(pending.timer);
310
+ pending.resolve(false);
311
+ this.pendingRpcs.delete(id);
312
+ }
313
+ if (this.ws) {
314
+ try {
315
+ this.ws.close();
316
+ }
317
+ catch { }
318
+ this.ws = null;
319
+ }
320
+ this.authenticated = false;
321
+ // Clear any pending connect promise
322
+ this.connectReject = null;
323
+ this.connectResolve = null;
324
+ }
325
+ }
326
+ // ---------------------------------------------------------------------------
327
+ // InboundGateway
328
+ // ---------------------------------------------------------------------------
329
+ export class InboundGateway {
330
+ relaySender;
331
+ relayAgentClient = null;
332
+ relaycast;
333
+ config;
334
+ fallbackPollMs;
335
+ dedupeTtlMs;
336
+ running = false;
337
+ pollTimer = null;
338
+ unsubscribeHandlers = [];
339
+ seenMessageIds = new Map();
340
+ processingMessageIds = new Set();
341
+ channelCursor = new Map();
342
+ /** Persistent WebSocket client for the local OpenClaw gateway. */
343
+ openclawClient = null;
344
+ /** Spawn manager — lives in the gateway so spawned processes survive MCP server restarts. */
345
+ spawnManager;
346
+ /** HTTP control server for spawn/list/release commands. */
347
+ controlServer = null;
348
+ /** Port the control server listens on. */
349
+ controlPort = 0;
350
+ /** Default control port for the gateway's spawn API. */
351
+ static DEFAULT_CONTROL_PORT = 18790;
352
+ constructor(options) {
353
+ this.config = {
354
+ ...options.config,
355
+ channels: options.config.channels.map(normalizeChannelName),
356
+ };
357
+ this.relaySender = options.relaySender ?? null;
358
+ this.relaycast = new RelayCast({
359
+ apiKey: this.config.apiKey,
360
+ baseUrl: this.config.baseUrl,
361
+ });
362
+ const fallbackPollMs = Number(process.env.RELAYCAST_FALLBACK_POLL_MS ?? 15000);
363
+ this.fallbackPollMs = Number.isFinite(fallbackPollMs) && fallbackPollMs >= 1000
364
+ ? Math.floor(fallbackPollMs)
365
+ : 15000;
366
+ const dedupeTtlMs = Number(process.env.RELAYCAST_DEDUPE_TTL_MS ?? 15 * 60 * 1000);
367
+ this.dedupeTtlMs = Number.isFinite(dedupeTtlMs) && dedupeTtlMs >= 1000
368
+ ? Math.floor(dedupeTtlMs)
369
+ : 15 * 60 * 1000;
370
+ const parentDepth = Number(process.env.OPENCLAW_SPAWN_DEPTH || 0);
371
+ this.spawnManager = new SpawnManager({ spawnDepth: parentDepth + 1 });
372
+ }
373
+ /** Start the gateway — register agent, subscribe for realtime events, and run fallback polling. */
374
+ async start() {
375
+ if (this.running)
376
+ return;
377
+ this.running = true;
378
+ // Connect to the local OpenClaw gateway WebSocket (persistent connection)
379
+ const token = this.config.openclawGatewayToken ?? process.env.OPENCLAW_GATEWAY_TOKEN;
380
+ const port = this.config.openclawGatewayPort ?? 18789;
381
+ if (token) {
382
+ this.openclawClient = new OpenClawGatewayClient(token, port);
383
+ try {
384
+ await this.openclawClient.connect();
385
+ console.log('[gateway] OpenClaw gateway WebSocket client ready');
386
+ }
387
+ catch (err) {
388
+ console.warn(`[gateway] OpenClaw gateway WS failed (will retry per message): ${err instanceof Error ? err.message : String(err)}`);
389
+ }
390
+ }
391
+ else {
392
+ console.warn('[gateway] No OPENCLAW_GATEWAY_TOKEN — local delivery disabled');
393
+ }
394
+ // Register with a viewer- prefixed name so we don't collide with the
395
+ // container broker's agent registration (which uses the bare clawName).
396
+ const viewerName = `viewer-${this.config.clawName}`;
397
+ const registered = await this.relaycast.agents.registerOrGet({
398
+ name: viewerName,
399
+ type: 'agent',
400
+ persona: 'Relaycast inbound gateway for OpenClaw',
401
+ });
402
+ this.relayAgentClient = this.relaycast.as(registered.token);
403
+ // Connect first, then register handlers. The SDK requires connect()
404
+ // before subscribe() can be called.
405
+ this.relayAgentClient.connect();
406
+ this.unsubscribeHandlers.push(this.relayAgentClient.on.connected(() => {
407
+ console.log(`[gateway] Relaycast WebSocket connected, subscribing to channels: ${this.config.channels.join(', ')}`);
408
+ this.relayAgentClient?.subscribe(this.config.channels);
409
+ }));
410
+ this.unsubscribeHandlers.push(this.relayAgentClient.on.messageCreated((event) => {
411
+ console.log(`[gateway] Realtime message from @${event.message?.agentName} in #${event.channel}`);
412
+ void this.handleRealtimeMessage(event);
413
+ }));
414
+ this.unsubscribeHandlers.push(this.relayAgentClient.on.threadReply((event) => {
415
+ console.log(`[gateway] Thread reply from @${event.message?.agentName} in #${event.channel} (parent: ${event.parentId})`);
416
+ void this.handleRealtimeThreadReply(event);
417
+ }));
418
+ this.unsubscribeHandlers.push(this.relayAgentClient.on.reconnecting((attempt) => {
419
+ console.warn(`[gateway] Relaycast reconnecting (attempt ${attempt})`);
420
+ }));
421
+ this.unsubscribeHandlers.push(this.relayAgentClient.on.disconnected(() => {
422
+ console.warn(`[gateway] Relaycast disconnected`);
423
+ }));
424
+ this.unsubscribeHandlers.push(this.relayAgentClient.on.error(() => {
425
+ console.warn(`[gateway] Relaycast socket error`);
426
+ }));
427
+ await this.ensureChannelMembership();
428
+ // Also subscribe explicitly in case the `connected` event already fired
429
+ // before we registered the handler above.
430
+ try {
431
+ this.relayAgentClient.subscribe(this.config.channels);
432
+ }
433
+ catch {
434
+ // Will subscribe on next connected event
435
+ }
436
+ // Initial catch-up in case messages arrived before realtime subscription was active.
437
+ await this.pollMessages();
438
+ // Keep a low-frequency poll as recovery/backfill only.
439
+ this.pollTimer = setInterval(() => {
440
+ void this.pollMessages();
441
+ }, this.fallbackPollMs);
442
+ console.log(`[gateway] Realtime listening on channels: ${this.config.channels.join(', ')}`);
443
+ // Start spawn control HTTP server
444
+ await this.startControlServer();
445
+ }
446
+ /** Stop the gateway — clean up websocket and relay clients. */
447
+ async stop() {
448
+ this.running = false;
449
+ if (this.pollTimer) {
450
+ clearInterval(this.pollTimer);
451
+ this.pollTimer = null;
452
+ }
453
+ for (const unsubscribe of this.unsubscribeHandlers) {
454
+ try {
455
+ unsubscribe();
456
+ }
457
+ catch {
458
+ // Best effort
459
+ }
460
+ }
461
+ this.unsubscribeHandlers = [];
462
+ if (this.relayAgentClient) {
463
+ try {
464
+ await this.relayAgentClient.disconnect();
465
+ }
466
+ catch {
467
+ // Best effort
468
+ }
469
+ this.relayAgentClient = null;
470
+ }
471
+ if (this.openclawClient) {
472
+ await this.openclawClient.disconnect();
473
+ this.openclawClient = null;
474
+ }
475
+ // Stop control server and release all spawns
476
+ if (this.controlServer) {
477
+ this.controlServer.close();
478
+ this.controlServer = null;
479
+ }
480
+ await this.spawnManager.releaseAll();
481
+ this.processingMessageIds.clear();
482
+ this.channelCursor.clear();
483
+ this.seenMessageIds.clear();
484
+ }
485
+ cleanupSeenMap(nowMs) {
486
+ for (const [id, seenAt] of this.seenMessageIds.entries()) {
487
+ if (nowMs - seenAt > this.dedupeTtlMs) {
488
+ this.seenMessageIds.delete(id);
489
+ }
490
+ }
491
+ }
492
+ isSeen(messageId) {
493
+ const nowMs = Date.now();
494
+ this.cleanupSeenMap(nowMs);
495
+ return this.seenMessageIds.has(messageId);
496
+ }
497
+ markSeen(messageId) {
498
+ const nowMs = Date.now();
499
+ this.cleanupSeenMap(nowMs);
500
+ this.seenMessageIds.set(messageId, nowMs);
501
+ }
502
+ async ensureChannelMembership() {
503
+ if (!this.relayAgentClient)
504
+ return;
505
+ for (const channel of this.config.channels) {
506
+ try {
507
+ await this.relayAgentClient.channels.join(channel);
508
+ }
509
+ catch {
510
+ try {
511
+ await this.relayAgentClient.channels.create({ name: channel });
512
+ await this.relayAgentClient.channels.join(channel);
513
+ }
514
+ catch {
515
+ // Non-fatal
516
+ }
517
+ }
518
+ }
519
+ }
520
+ async handleRealtimeMessage(event) {
521
+ const channel = normalizeChannelName(event.channel);
522
+ if (!this.config.channels.includes(channel))
523
+ return;
524
+ const messageId = event.message?.id;
525
+ if (!messageId)
526
+ return;
527
+ const inbound = {
528
+ id: messageId,
529
+ channel,
530
+ from: event.message.agentName,
531
+ text: event.message.text,
532
+ timestamp: new Date().toISOString(),
533
+ };
534
+ await this.handleInbound(inbound);
535
+ }
536
+ async handleRealtimeThreadReply(event) {
537
+ const channel = normalizeChannelName(event.channel);
538
+ if (!this.config.channels.includes(channel))
539
+ return;
540
+ const messageId = event.message?.id;
541
+ if (!messageId)
542
+ return;
543
+ const inbound = {
544
+ id: messageId,
545
+ channel,
546
+ from: event.message.agentName,
547
+ text: event.message.text,
548
+ timestamp: new Date().toISOString(),
549
+ threadParentId: event.parentId,
550
+ };
551
+ await this.handleInbound(inbound);
552
+ }
553
+ normalizePolledMessage(channel, message) {
554
+ return {
555
+ id: message.id,
556
+ channel,
557
+ from: message.agentName,
558
+ text: message.text,
559
+ timestamp: message.createdAt,
560
+ };
561
+ }
562
+ /** Poll channels for catch-up/recovery only. */
563
+ async pollMessages() {
564
+ if (!this.running)
565
+ return;
566
+ for (const channel of this.config.channels) {
567
+ try {
568
+ const after = this.channelCursor.get(channel);
569
+ const query = { limit: 50 };
570
+ if (after) {
571
+ query.after = after;
572
+ }
573
+ const messages = await this.relaycast.messages.list(channel, query);
574
+ const ordered = [...messages].sort((a, b) => a.createdAt.localeCompare(b.createdAt));
575
+ for (const message of ordered) {
576
+ await this.handleInbound(this.normalizePolledMessage(channel, message));
577
+ }
578
+ }
579
+ catch (err) {
580
+ console.warn(`[gateway] Poll error for #${channel}: ${err instanceof Error ? err.message : String(err)}`);
581
+ }
582
+ }
583
+ }
584
+ async handleInbound(message) {
585
+ if (!this.running)
586
+ return;
587
+ if (this.processingMessageIds.has(message.id) || this.isSeen(message.id))
588
+ return;
589
+ // Avoid echo loops — skip messages from this claw or its viewer identity.
590
+ const viewerName = `viewer-${this.config.clawName}`;
591
+ if (message.from === this.config.clawName || message.from === viewerName) {
592
+ this.channelCursor.set(normalizeChannelName(message.channel), message.id);
593
+ this.markSeen(message.id);
594
+ return;
595
+ }
596
+ // Mark as seen immediately to prevent duplicate delivery from concurrent
597
+ // realtime + poll paths processing the same message.
598
+ this.markSeen(message.id);
599
+ this.processingMessageIds.add(message.id);
600
+ console.log(`[gateway] Delivering message ${message.id} from @${message.from}: "${message.text}"`);
601
+ try {
602
+ const result = await this.onMessage(message);
603
+ console.log(`[gateway] Delivery result: ${result.method} ok=${result.ok}${result.error ? ' error=' + result.error : ''}`);
604
+ this.channelCursor.set(normalizeChannelName(message.channel), message.id);
605
+ }
606
+ finally {
607
+ this.processingMessageIds.delete(message.id);
608
+ }
609
+ }
610
+ /** Format delivery text with channel, sender, and optional thread prefix. */
611
+ formatDeliveryText(message) {
612
+ const threadPrefix = message.threadParentId ? '[thread] ' : '';
613
+ return `${threadPrefix}[relaycast:${message.channel}] @${message.from}: ${message.text}`;
614
+ }
615
+ /** Handle an inbound Relaycast message. */
616
+ async onMessage(message) {
617
+ // Try primary delivery via the shared relay sender (no extra broker spawned).
618
+ if (this.relaySender) {
619
+ const ok = await this.deliverViaRelaySender(message);
620
+ if (ok) {
621
+ return { ok: true, method: 'relay_sdk' };
622
+ }
623
+ }
624
+ // Deliver via persistent OpenClaw gateway WebSocket connection
625
+ if (this.openclawClient) {
626
+ const text = this.formatDeliveryText(message);
627
+ const ok = await this.openclawClient.sendChatMessage(text, message.id);
628
+ if (ok) {
629
+ return { ok: true, method: 'gateway_ws' };
630
+ }
631
+ }
632
+ console.warn(`[gateway] Failed to deliver message ${message.id} from @${message.from}`);
633
+ return { ok: false, method: 'failed', error: 'All delivery methods failed' };
634
+ }
635
+ /** Deliver via the caller-provided relay sender (shared broker). */
636
+ async deliverViaRelaySender(message) {
637
+ if (!this.relaySender)
638
+ return false;
639
+ const input = {
640
+ to: this.config.clawName,
641
+ text: this.formatDeliveryText(message),
642
+ from: message.from,
643
+ data: {
644
+ source: 'relaycast',
645
+ channel: message.channel,
646
+ messageId: message.id,
647
+ },
648
+ };
649
+ try {
650
+ const result = await this.relaySender.sendMessage(input);
651
+ return Boolean(result.event_id) && result.event_id !== 'unsupported_operation';
652
+ }
653
+ catch {
654
+ return false;
655
+ }
656
+ }
657
+ // -------------------------------------------------------------------------
658
+ // Spawn control HTTP server
659
+ // -------------------------------------------------------------------------
660
+ async startControlServer() {
661
+ const port = Number(process.env.RELAYCAST_CONTROL_PORT) || InboundGateway.DEFAULT_CONTROL_PORT;
662
+ this.controlServer = createServer((req, res) => {
663
+ void this.handleControlRequest(req, res);
664
+ });
665
+ return new Promise((resolve) => {
666
+ this.controlServer.listen(port, '127.0.0.1', () => {
667
+ this.controlPort = port;
668
+ console.log(`[gateway] Spawn control API listening on http://127.0.0.1:${port}`);
669
+ resolve();
670
+ });
671
+ this.controlServer.on('error', (err) => {
672
+ console.warn(`[gateway] Control server failed to start on port ${port}: ${err.message}`);
673
+ this.controlServer = null;
674
+ resolve(); // Non-fatal
675
+ });
676
+ });
677
+ }
678
+ async handleControlRequest(req, res) {
679
+ const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
680
+ const path = url.pathname;
681
+ // CORS for local callers
682
+ res.setHeader('Content-Type', 'application/json');
683
+ if (req.method === 'GET' && path === '/health') {
684
+ res.writeHead(200);
685
+ res.end(JSON.stringify({
686
+ ok: true,
687
+ status: 'running',
688
+ active: this.spawnManager.size,
689
+ uptime: process.uptime(),
690
+ }));
691
+ return;
692
+ }
693
+ if (req.method === 'POST' && path === '/spawn') {
694
+ const body = await readBody(req);
695
+ try {
696
+ const args = JSON.parse(body);
697
+ const name = args.name;
698
+ if (!name) {
699
+ res.writeHead(400);
700
+ res.end(JSON.stringify({ ok: false, error: '"name" is required' }));
701
+ return;
702
+ }
703
+ const relayApiKey = this.config.apiKey;
704
+ const spawnOpts = {
705
+ name,
706
+ relayApiKey,
707
+ role: args.role || undefined,
708
+ model: args.model || undefined,
709
+ channels: args.channels || undefined,
710
+ systemPrompt: args.system_prompt || undefined,
711
+ relayBaseUrl: this.config.baseUrl,
712
+ workspaceId: args.workspace_id || process.env.OPENCLAW_WORKSPACE_ID,
713
+ };
714
+ const handle = await this.spawnManager.spawn(spawnOpts);
715
+ res.writeHead(200);
716
+ res.end(JSON.stringify({
717
+ ok: true,
718
+ name: handle.displayName,
719
+ agentName: handle.agentName,
720
+ id: handle.id,
721
+ gatewayPort: handle.gatewayPort,
722
+ active: this.spawnManager.size,
723
+ }));
724
+ }
725
+ catch (err) {
726
+ res.writeHead(500);
727
+ res.end(JSON.stringify({ ok: false, error: err instanceof Error ? err.message : String(err) }));
728
+ }
729
+ return;
730
+ }
731
+ if (req.method === 'GET' && path === '/list') {
732
+ const handles = this.spawnManager.list();
733
+ res.writeHead(200);
734
+ res.end(JSON.stringify({
735
+ ok: true,
736
+ active: handles.length,
737
+ claws: handles.map(h => ({
738
+ name: h.displayName,
739
+ agentName: h.agentName,
740
+ id: h.id,
741
+ gatewayPort: h.gatewayPort,
742
+ })),
743
+ }));
744
+ return;
745
+ }
746
+ if (req.method === 'POST' && path === '/release') {
747
+ const body = await readBody(req);
748
+ try {
749
+ const args = JSON.parse(body);
750
+ const name = args.name;
751
+ const id = args.id;
752
+ if (!name && !id) {
753
+ res.writeHead(400);
754
+ res.end(JSON.stringify({ ok: false, error: 'Provide "name" or "id"' }));
755
+ return;
756
+ }
757
+ let released = false;
758
+ if (id) {
759
+ released = await this.spawnManager.release(id);
760
+ }
761
+ else if (name) {
762
+ released = await this.spawnManager.releaseByName(name);
763
+ }
764
+ res.writeHead(200);
765
+ res.end(JSON.stringify({ ok: released, active: this.spawnManager.size }));
766
+ }
767
+ catch (err) {
768
+ res.writeHead(500);
769
+ res.end(JSON.stringify({ ok: false, error: err instanceof Error ? err.message : String(err) }));
770
+ }
771
+ return;
772
+ }
773
+ res.writeHead(404);
774
+ res.end(JSON.stringify({ error: 'Not found' }));
775
+ }
776
+ }
777
+ function readBody(req) {
778
+ return new Promise((resolve, reject) => {
779
+ const chunks = [];
780
+ req.on('data', (chunk) => chunks.push(chunk));
781
+ req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
782
+ req.on('error', (err) => reject(err));
783
+ });
784
+ }
785
+ //# sourceMappingURL=gateway.js.map