@astroanywhere/agent 0.1.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 (203) hide show
  1. package/LICENSE +76 -0
  2. package/README.md +178 -0
  3. package/dist/cli.d.ts +15 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +401 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/commands/index.d.ts +9 -0
  8. package/dist/commands/index.d.ts.map +1 -0
  9. package/dist/commands/index.js +9 -0
  10. package/dist/commands/index.js.map +1 -0
  11. package/dist/commands/mcp.d.ts +16 -0
  12. package/dist/commands/mcp.d.ts.map +1 -0
  13. package/dist/commands/mcp.js +19 -0
  14. package/dist/commands/mcp.js.map +1 -0
  15. package/dist/commands/setup.d.ts +20 -0
  16. package/dist/commands/setup.d.ts.map +1 -0
  17. package/dist/commands/setup.js +585 -0
  18. package/dist/commands/setup.js.map +1 -0
  19. package/dist/commands/start.d.ts +16 -0
  20. package/dist/commands/start.d.ts.map +1 -0
  21. package/dist/commands/start.js +638 -0
  22. package/dist/commands/start.js.map +1 -0
  23. package/dist/commands/status.d.ts +5 -0
  24. package/dist/commands/status.d.ts.map +1 -0
  25. package/dist/commands/status.js +63 -0
  26. package/dist/commands/status.js.map +1 -0
  27. package/dist/commands/stop.d.ts +5 -0
  28. package/dist/commands/stop.d.ts.map +1 -0
  29. package/dist/commands/stop.js +85 -0
  30. package/dist/commands/stop.js.map +1 -0
  31. package/dist/execution/direct-strategy.d.ts +18 -0
  32. package/dist/execution/direct-strategy.d.ts.map +1 -0
  33. package/dist/execution/direct-strategy.js +156 -0
  34. package/dist/execution/direct-strategy.js.map +1 -0
  35. package/dist/execution/docker-strategy.d.ts +26 -0
  36. package/dist/execution/docker-strategy.d.ts.map +1 -0
  37. package/dist/execution/docker-strategy.js +222 -0
  38. package/dist/execution/docker-strategy.js.map +1 -0
  39. package/dist/execution/index.d.ts +14 -0
  40. package/dist/execution/index.d.ts.map +1 -0
  41. package/dist/execution/index.js +13 -0
  42. package/dist/execution/index.js.map +1 -0
  43. package/dist/execution/kubernetes-exec-strategy.d.ts +23 -0
  44. package/dist/execution/kubernetes-exec-strategy.d.ts.map +1 -0
  45. package/dist/execution/kubernetes-exec-strategy.js +232 -0
  46. package/dist/execution/kubernetes-exec-strategy.js.map +1 -0
  47. package/dist/execution/registry.d.ts +41 -0
  48. package/dist/execution/registry.d.ts.map +1 -0
  49. package/dist/execution/registry.js +84 -0
  50. package/dist/execution/registry.js.map +1 -0
  51. package/dist/execution/slurm-strategy.d.ts +22 -0
  52. package/dist/execution/slurm-strategy.d.ts.map +1 -0
  53. package/dist/execution/slurm-strategy.js +219 -0
  54. package/dist/execution/slurm-strategy.js.map +1 -0
  55. package/dist/execution/types.d.ts +72 -0
  56. package/dist/execution/types.d.ts.map +1 -0
  57. package/dist/execution/types.js +10 -0
  58. package/dist/execution/types.js.map +1 -0
  59. package/dist/index.d.ts +22 -0
  60. package/dist/index.d.ts.map +1 -0
  61. package/dist/index.js +22 -0
  62. package/dist/index.js.map +1 -0
  63. package/dist/lib/api-client.d.ts +35 -0
  64. package/dist/lib/api-client.d.ts.map +1 -0
  65. package/dist/lib/api-client.js +126 -0
  66. package/dist/lib/api-client.js.map +1 -0
  67. package/dist/lib/config.d.ts +174 -0
  68. package/dist/lib/config.d.ts.map +1 -0
  69. package/dist/lib/config.js +399 -0
  70. package/dist/lib/config.js.map +1 -0
  71. package/dist/lib/copy-worktree.d.ts +73 -0
  72. package/dist/lib/copy-worktree.d.ts.map +1 -0
  73. package/dist/lib/copy-worktree.js +374 -0
  74. package/dist/lib/copy-worktree.js.map +1 -0
  75. package/dist/lib/git-pr.d.ts +63 -0
  76. package/dist/lib/git-pr.d.ts.map +1 -0
  77. package/dist/lib/git-pr.js +224 -0
  78. package/dist/lib/git-pr.js.map +1 -0
  79. package/dist/lib/hardware-id.d.ts +25 -0
  80. package/dist/lib/hardware-id.d.ts.map +1 -0
  81. package/dist/lib/hardware-id.js +186 -0
  82. package/dist/lib/hardware-id.js.map +1 -0
  83. package/dist/lib/hpc-context.d.ts +35 -0
  84. package/dist/lib/hpc-context.d.ts.map +1 -0
  85. package/dist/lib/hpc-context.js +167 -0
  86. package/dist/lib/hpc-context.js.map +1 -0
  87. package/dist/lib/prompt-templates.d.ts +195 -0
  88. package/dist/lib/prompt-templates.d.ts.map +1 -0
  89. package/dist/lib/prompt-templates.js +353 -0
  90. package/dist/lib/prompt-templates.js.map +1 -0
  91. package/dist/lib/providers.d.ts +27 -0
  92. package/dist/lib/providers.d.ts.map +1 -0
  93. package/dist/lib/providers.js +372 -0
  94. package/dist/lib/providers.js.map +1 -0
  95. package/dist/lib/repo-context.d.ts +18 -0
  96. package/dist/lib/repo-context.d.ts.map +1 -0
  97. package/dist/lib/repo-context.js +61 -0
  98. package/dist/lib/repo-context.js.map +1 -0
  99. package/dist/lib/repo-utils.d.ts +35 -0
  100. package/dist/lib/repo-utils.d.ts.map +1 -0
  101. package/dist/lib/repo-utils.js +222 -0
  102. package/dist/lib/repo-utils.js.map +1 -0
  103. package/dist/lib/resources.d.ts +17 -0
  104. package/dist/lib/resources.d.ts.map +1 -0
  105. package/dist/lib/resources.js +227 -0
  106. package/dist/lib/resources.js.map +1 -0
  107. package/dist/lib/slurm-detect.d.ts +15 -0
  108. package/dist/lib/slurm-detect.d.ts.map +1 -0
  109. package/dist/lib/slurm-detect.js +148 -0
  110. package/dist/lib/slurm-detect.js.map +1 -0
  111. package/dist/lib/slurm-executor.d.ts +70 -0
  112. package/dist/lib/slurm-executor.d.ts.map +1 -0
  113. package/dist/lib/slurm-executor.js +402 -0
  114. package/dist/lib/slurm-executor.js.map +1 -0
  115. package/dist/lib/slurm-job-monitor.d.ts +52 -0
  116. package/dist/lib/slurm-job-monitor.d.ts.map +1 -0
  117. package/dist/lib/slurm-job-monitor.js +212 -0
  118. package/dist/lib/slurm-job-monitor.js.map +1 -0
  119. package/dist/lib/ssh-discovery.d.ts +17 -0
  120. package/dist/lib/ssh-discovery.d.ts.map +1 -0
  121. package/dist/lib/ssh-discovery.js +287 -0
  122. package/dist/lib/ssh-discovery.js.map +1 -0
  123. package/dist/lib/ssh-installer.d.ts +69 -0
  124. package/dist/lib/ssh-installer.d.ts.map +1 -0
  125. package/dist/lib/ssh-installer.js +230 -0
  126. package/dist/lib/ssh-installer.js.map +1 -0
  127. package/dist/lib/streaming-prompt.d.ts +48 -0
  128. package/dist/lib/streaming-prompt.d.ts.map +1 -0
  129. package/dist/lib/streaming-prompt.js +91 -0
  130. package/dist/lib/streaming-prompt.js.map +1 -0
  131. package/dist/lib/task-executor.d.ts +114 -0
  132. package/dist/lib/task-executor.d.ts.map +1 -0
  133. package/dist/lib/task-executor.js +753 -0
  134. package/dist/lib/task-executor.js.map +1 -0
  135. package/dist/lib/websocket-client.d.ts +200 -0
  136. package/dist/lib/websocket-client.d.ts.map +1 -0
  137. package/dist/lib/websocket-client.js +781 -0
  138. package/dist/lib/websocket-client.js.map +1 -0
  139. package/dist/lib/workdir-safety.d.ts +63 -0
  140. package/dist/lib/workdir-safety.d.ts.map +1 -0
  141. package/dist/lib/workdir-safety.js +247 -0
  142. package/dist/lib/workdir-safety.js.map +1 -0
  143. package/dist/lib/worktree-include.d.ts +14 -0
  144. package/dist/lib/worktree-include.d.ts.map +1 -0
  145. package/dist/lib/worktree-include.js +68 -0
  146. package/dist/lib/worktree-include.js.map +1 -0
  147. package/dist/lib/worktree-setup.d.ts +18 -0
  148. package/dist/lib/worktree-setup.d.ts.map +1 -0
  149. package/dist/lib/worktree-setup.js +60 -0
  150. package/dist/lib/worktree-setup.js.map +1 -0
  151. package/dist/lib/worktree.d.ts +37 -0
  152. package/dist/lib/worktree.d.ts.map +1 -0
  153. package/dist/lib/worktree.js +411 -0
  154. package/dist/lib/worktree.js.map +1 -0
  155. package/dist/mcp/index.d.ts +8 -0
  156. package/dist/mcp/index.d.ts.map +1 -0
  157. package/dist/mcp/index.js +8 -0
  158. package/dist/mcp/index.js.map +1 -0
  159. package/dist/mcp/server.d.ts +45 -0
  160. package/dist/mcp/server.d.ts.map +1 -0
  161. package/dist/mcp/server.js +153 -0
  162. package/dist/mcp/server.js.map +1 -0
  163. package/dist/mcp/session-bridge.d.ts +87 -0
  164. package/dist/mcp/session-bridge.d.ts.map +1 -0
  165. package/dist/mcp/session-bridge.js +317 -0
  166. package/dist/mcp/session-bridge.js.map +1 -0
  167. package/dist/mcp/tools.d.ts +70 -0
  168. package/dist/mcp/tools.d.ts.map +1 -0
  169. package/dist/mcp/tools.js +234 -0
  170. package/dist/mcp/tools.js.map +1 -0
  171. package/dist/mcp/types.d.ts +197 -0
  172. package/dist/mcp/types.d.ts.map +1 -0
  173. package/dist/mcp/types.js +16 -0
  174. package/dist/mcp/types.js.map +1 -0
  175. package/dist/providers/base-adapter.d.ts +56 -0
  176. package/dist/providers/base-adapter.d.ts.map +1 -0
  177. package/dist/providers/base-adapter.js +5 -0
  178. package/dist/providers/base-adapter.js.map +1 -0
  179. package/dist/providers/claude-code-adapter.d.ts +27 -0
  180. package/dist/providers/claude-code-adapter.d.ts.map +1 -0
  181. package/dist/providers/claude-code-adapter.js +298 -0
  182. package/dist/providers/claude-code-adapter.js.map +1 -0
  183. package/dist/providers/claude-sdk-adapter.d.ts +60 -0
  184. package/dist/providers/claude-sdk-adapter.d.ts.map +1 -0
  185. package/dist/providers/claude-sdk-adapter.js +632 -0
  186. package/dist/providers/claude-sdk-adapter.js.map +1 -0
  187. package/dist/providers/codex-adapter.d.ts +21 -0
  188. package/dist/providers/codex-adapter.d.ts.map +1 -0
  189. package/dist/providers/codex-adapter.js +197 -0
  190. package/dist/providers/codex-adapter.js.map +1 -0
  191. package/dist/providers/index.d.ts +26 -0
  192. package/dist/providers/index.d.ts.map +1 -0
  193. package/dist/providers/index.js +58 -0
  194. package/dist/providers/index.js.map +1 -0
  195. package/dist/providers/slurm-adapter.d.ts +26 -0
  196. package/dist/providers/slurm-adapter.d.ts.map +1 -0
  197. package/dist/providers/slurm-adapter.js +146 -0
  198. package/dist/providers/slurm-adapter.js.map +1 -0
  199. package/dist/types.d.ts +592 -0
  200. package/dist/types.d.ts.map +1 -0
  201. package/dist/types.js +5 -0
  202. package/dist/types.js.map +1 -0
  203. package/package.json +77 -0
@@ -0,0 +1,781 @@
1
+ /**
2
+ * WebSocket client with automatic reconnection and heartbeat
3
+ */
4
+ import WebSocket from 'ws';
5
+ import jwt from 'jsonwebtoken';
6
+ import { getMachineResources } from './resources.js';
7
+ import { config as configManager } from './config.js';
8
+ const DEFAULT_CONFIG = {
9
+ relayUrl: 'wss://relay.astro.dev',
10
+ maxConcurrentTasks: 4,
11
+ heartbeatInterval: 30000, // 30 seconds
12
+ reconnectMaxRetries: -1, // Infinite retries
13
+ reconnectBaseDelay: 1000, // 1 second
14
+ reconnectMaxDelay: 60000, // 1 minute
15
+ taskTimeout: 3600000, // 1 hour
16
+ logLevel: 'info',
17
+ };
18
+ export class WebSocketClient {
19
+ ws = null;
20
+ config;
21
+ runnerId;
22
+ machineId;
23
+ providers;
24
+ executionStrategies;
25
+ version;
26
+ wsToken;
27
+ reconnectAttempts = 0;
28
+ reconnectTimeout = null;
29
+ heartbeatInterval = null;
30
+ isConnecting = false;
31
+ shouldReconnect = true;
32
+ activeTasks = new Set();
33
+ pendingApprovals = new Map();
34
+ onEvent;
35
+ onTaskDispatch;
36
+ onTaskCancel;
37
+ onTaskSteer;
38
+ onTaskSafetyDecision;
39
+ onFileList;
40
+ onRepoSetup;
41
+ onSlashCommands;
42
+ onRepoDetect;
43
+ onGitInit;
44
+ constructor(options) {
45
+ this.runnerId = options.runnerId;
46
+ this.machineId = options.machineId;
47
+ this.providers = options.providers;
48
+ this.executionStrategies = options.executionStrategies;
49
+ this.version = options.version ?? '0.1.0';
50
+ this.wsToken = options.wsToken;
51
+ this.config = { ...DEFAULT_CONFIG, ...options.config };
52
+ this.onEvent = options.onEvent;
53
+ this.onTaskDispatch = options.onTaskDispatch;
54
+ this.onTaskCancel = options.onTaskCancel;
55
+ this.onTaskSteer = options.onTaskSteer;
56
+ this.onTaskSafetyDecision = options.onTaskSafetyDecision;
57
+ this.onFileList = options.onFileList;
58
+ this.onRepoSetup = options.onRepoSetup;
59
+ this.onSlashCommands = options.onSlashCommands;
60
+ this.onRepoDetect = options.onRepoDetect;
61
+ this.onGitInit = options.onGitInit;
62
+ }
63
+ /**
64
+ * Check if access token is expired or expiring soon
65
+ */
66
+ isTokenExpiring(token, bufferSeconds = 5 * 60) {
67
+ try {
68
+ const decoded = jwt.decode(token);
69
+ if (!decoded || !decoded.exp) {
70
+ return true; // Assume expired if we can't decode
71
+ }
72
+ const now = Math.floor(Date.now() / 1000);
73
+ return decoded.exp < now + bufferSeconds;
74
+ }
75
+ catch {
76
+ return true; // Assume expired on error
77
+ }
78
+ }
79
+ /**
80
+ * Refresh the access token and WebSocket token using the refresh token
81
+ */
82
+ async refreshAccessToken() {
83
+ const refreshToken = configManager.getRefreshToken();
84
+ if (!refreshToken) {
85
+ throw new Error('No refresh token available. Please run setup again to re-authenticate.');
86
+ }
87
+ const apiUrl = configManager.getApiUrl();
88
+ const response = await fetch(`${apiUrl}/api/device/refresh`, {
89
+ method: 'POST',
90
+ headers: { 'Content-Type': 'application/json' },
91
+ body: JSON.stringify({
92
+ refreshToken,
93
+ grantType: 'refresh_token',
94
+ }),
95
+ });
96
+ if (!response.ok) {
97
+ const error = await response.json().catch(() => ({ error: 'Unknown error' }));
98
+ throw new Error(`Token refresh failed: ${error.error || response.statusText}`);
99
+ }
100
+ const data = await response.json();
101
+ // Update stored tokens
102
+ configManager.setAccessToken(data.accessToken);
103
+ if (data.refreshToken) {
104
+ configManager.setRefreshToken(data.refreshToken);
105
+ }
106
+ if (data.wsToken) {
107
+ configManager.setWsToken(data.wsToken);
108
+ }
109
+ // Return wsToken for WebSocket connections (signed with RELAY_JWT_SECRET)
110
+ // Falls back to accessToken if server didn't return a wsToken
111
+ return data.wsToken || data.accessToken;
112
+ }
113
+ /**
114
+ * Ensure we have a valid WebSocket token before connecting.
115
+ * Returns null in dev mode when no token is configured (allows unauthenticated connections).
116
+ */
117
+ async ensureValidToken() {
118
+ let token = this.wsToken || configManager.getWsToken();
119
+ // In dev mode (non-wss relay), skip auth entirely
120
+ if (this.config.relayUrl.startsWith('ws://')) {
121
+ if (!token) {
122
+ return null;
123
+ }
124
+ // Even with a stale token, dev mode doesn't need auth
125
+ if (this.isTokenExpiring(token)) {
126
+ return null;
127
+ }
128
+ }
129
+ if (!token) {
130
+ throw new Error('No access token configured. Please run setup to authenticate.');
131
+ }
132
+ // Check if token is expired or expiring soon
133
+ if (this.isTokenExpiring(token)) {
134
+ console.log('[ws-client] Access token expired or expiring soon, refreshing...');
135
+ try {
136
+ token = await this.refreshAccessToken();
137
+ this.wsToken = token; // Update instance token
138
+ console.log('[ws-client] ✅ Access token refreshed successfully');
139
+ }
140
+ catch (error) {
141
+ console.error('[ws-client] ❌ Failed to refresh token:', error instanceof Error ? error.message : String(error));
142
+ throw new Error(`Authentication failed: ${error instanceof Error ? error.message : 'Token refresh failed'}. Please run setup again.`);
143
+ }
144
+ }
145
+ return token;
146
+ }
147
+ /**
148
+ * Connect to the relay server
149
+ */
150
+ async connect() {
151
+ if (this.isConnecting || this.ws?.readyState === WebSocket.OPEN) {
152
+ return;
153
+ }
154
+ this.isConnecting = true;
155
+ this.shouldReconnect = true;
156
+ // Ensure we have a valid token before connecting (null = dev mode, no auth)
157
+ let token;
158
+ try {
159
+ token = await this.ensureValidToken();
160
+ }
161
+ catch (error) {
162
+ this.isConnecting = false;
163
+ throw error;
164
+ }
165
+ return new Promise((resolve, reject) => {
166
+ try {
167
+ const headers = {
168
+ 'X-Runner-Id': this.runnerId,
169
+ 'X-Machine-Id': this.machineId,
170
+ };
171
+ if (token) {
172
+ headers['Authorization'] = `Bearer ${token}`;
173
+ }
174
+ this.ws = new WebSocket(this.config.relayUrl, { headers });
175
+ this.ws.on('open', async () => {
176
+ this.isConnecting = false;
177
+ this.reconnectAttempts = 0;
178
+ await this.handleOpen();
179
+ resolve();
180
+ });
181
+ this.ws.on('message', (data) => {
182
+ this.handleMessage(data);
183
+ });
184
+ const thisSocket = this.ws;
185
+ this.ws.on('close', (code, reason) => {
186
+ // Only handle close if this is still the active socket.
187
+ // When the server closes a stale connection after re-registration,
188
+ // the old socket's close handler fires — ignore it to prevent a reconnect loop.
189
+ if (this.ws !== thisSocket) {
190
+ return;
191
+ }
192
+ this.handleClose(code, reason.toString());
193
+ });
194
+ this.ws.on('error', (error) => {
195
+ this.isConnecting = false;
196
+ if (this.reconnectAttempts === 0) {
197
+ reject(error);
198
+ }
199
+ this.emitEvent({ type: 'error', error: error });
200
+ });
201
+ this.ws.on('pong', () => {
202
+ // Server responded to ping, connection is alive
203
+ });
204
+ }
205
+ catch (error) {
206
+ this.isConnecting = false;
207
+ reject(error);
208
+ }
209
+ });
210
+ }
211
+ /**
212
+ * Disconnect from the relay server
213
+ */
214
+ disconnect() {
215
+ this.shouldReconnect = false;
216
+ this.cleanup();
217
+ if (this.ws) {
218
+ this.ws.close(1000, 'Client disconnect');
219
+ this.ws = null;
220
+ }
221
+ }
222
+ /**
223
+ * Send a task status update
224
+ */
225
+ sendTaskStatus(taskId, status, progress, message) {
226
+ const msg = {
227
+ type: 'task_status',
228
+ timestamp: new Date().toISOString(),
229
+ payload: { taskId, status, progress, message },
230
+ };
231
+ this.send(msg);
232
+ }
233
+ /**
234
+ * Send task output (stdout/stderr)
235
+ */
236
+ sendTaskOutput(taskId, stream, data, sequence) {
237
+ const msg = {
238
+ type: 'task_output',
239
+ timestamp: new Date().toISOString(),
240
+ payload: { taskId, stream, data, sequence },
241
+ };
242
+ this.send(msg);
243
+ }
244
+ /**
245
+ * Send task result
246
+ */
247
+ sendTaskResult(result) {
248
+ const msg = {
249
+ type: 'task_result',
250
+ timestamp: new Date().toISOString(),
251
+ payload: result,
252
+ };
253
+ this.send(msg);
254
+ // Remove from active tasks
255
+ this.activeTasks.delete(result.taskId);
256
+ }
257
+ /**
258
+ * Send tool trace
259
+ */
260
+ sendToolTrace(taskId, toolName, toolInput, toolResult, success) {
261
+ const msg = {
262
+ type: 'task_tool_trace',
263
+ timestamp: new Date().toISOString(),
264
+ payload: { taskId, toolName, toolInput, toolResult, success },
265
+ };
266
+ this.send(msg);
267
+ }
268
+ /**
269
+ * Send structured text (bypasses stdout throttle on relay)
270
+ */
271
+ sendTaskText(taskId, text, sequence) {
272
+ const msg = {
273
+ type: 'task_text',
274
+ timestamp: new Date().toISOString(),
275
+ payload: { taskId, text, sequence },
276
+ };
277
+ this.send(msg);
278
+ }
279
+ /**
280
+ * Send structured tool use event
281
+ */
282
+ sendTaskToolUse(taskId, toolName, toolInput) {
283
+ const msg = {
284
+ type: 'task_tool_use',
285
+ timestamp: new Date().toISOString(),
286
+ payload: { taskId, toolName, toolInput },
287
+ };
288
+ this.send(msg);
289
+ }
290
+ /**
291
+ * Send structured tool result event
292
+ */
293
+ sendTaskToolResult(taskId, toolName, result, success) {
294
+ const msg = {
295
+ type: 'task_tool_result',
296
+ timestamp: new Date().toISOString(),
297
+ payload: { taskId, toolName, result, success },
298
+ };
299
+ this.send(msg);
300
+ }
301
+ /**
302
+ * Send structured file change event
303
+ */
304
+ sendTaskFileChange(taskId, path, action, linesAdded, linesRemoved, diff) {
305
+ const msg = {
306
+ type: 'task_file_change',
307
+ timestamp: new Date().toISOString(),
308
+ payload: { taskId, path, action, linesAdded, linesRemoved, diff },
309
+ };
310
+ this.send(msg);
311
+ }
312
+ /**
313
+ * Send structured session init event
314
+ */
315
+ sendTaskSessionInit(taskId, sessionId, model) {
316
+ const msg = {
317
+ type: 'task_session_init',
318
+ timestamp: new Date().toISOString(),
319
+ payload: { taskId, sessionId, model },
320
+ };
321
+ this.send(msg);
322
+ }
323
+ /**
324
+ * Send steer acknowledgment
325
+ */
326
+ sendSteerAck(taskId, accepted, message, interrupted) {
327
+ const msg = {
328
+ type: 'task_steer_ack',
329
+ timestamp: new Date().toISOString(),
330
+ payload: { taskId, accepted, message },
331
+ };
332
+ if (interrupted) {
333
+ msg.payload.interrupted = true;
334
+ }
335
+ this.send(msg);
336
+ }
337
+ /**
338
+ * Send approval request and wait for response (following Cyrus pattern)
339
+ * Returns a promise that resolves when the user responds or rejects on timeout
340
+ */
341
+ sendApprovalRequest(taskId, question, options) {
342
+ const requestId = `${taskId}-${Date.now()}`;
343
+ return new Promise((resolve, reject) => {
344
+ // Store the promise handlers
345
+ this.pendingApprovals.set(requestId, { resolve, reject });
346
+ // Send the approval request message
347
+ const msg = {
348
+ type: 'task_approval_request',
349
+ timestamp: new Date().toISOString(),
350
+ payload: { taskId, requestId, question, options },
351
+ };
352
+ this.send(msg);
353
+ console.log(`[ws-client] Sent approval request ${requestId} for task ${taskId}`);
354
+ // Set timeout (60 seconds for user to respond)
355
+ setTimeout(() => {
356
+ const pending = this.pendingApprovals.get(requestId);
357
+ if (pending) {
358
+ this.pendingApprovals.delete(requestId);
359
+ pending.resolve({ answered: false, message: 'Approval request timed out (60s)' });
360
+ }
361
+ }, 60000);
362
+ });
363
+ }
364
+ /**
365
+ * Send safety prompt to user
366
+ */
367
+ sendSafetyPrompt(taskId, safetyTier, warning, options) {
368
+ const msg = {
369
+ type: 'task_safety_prompt',
370
+ timestamp: new Date().toISOString(),
371
+ payload: {
372
+ taskId,
373
+ safetyTier,
374
+ warning,
375
+ blockReason: undefined,
376
+ options,
377
+ },
378
+ };
379
+ this.send(msg);
380
+ console.log(`[ws-client] Sent safety prompt for task ${taskId} (tier: ${safetyTier})`);
381
+ }
382
+ /**
383
+ * Send resource update
384
+ */
385
+ async sendResourceUpdate() {
386
+ const resources = await getMachineResources();
387
+ const msg = {
388
+ type: 'resource_update',
389
+ timestamp: new Date().toISOString(),
390
+ payload: { runnerId: this.runnerId, resources },
391
+ };
392
+ this.send(msg);
393
+ }
394
+ /**
395
+ * Check if connected
396
+ */
397
+ isConnected() {
398
+ return this.ws?.readyState === WebSocket.OPEN;
399
+ }
400
+ /**
401
+ * Get current configuration
402
+ */
403
+ getConfig() {
404
+ return { ...this.config };
405
+ }
406
+ /**
407
+ * Get active task count
408
+ */
409
+ getActiveTaskCount() {
410
+ return this.activeTasks.size;
411
+ }
412
+ /**
413
+ * Add task to active set
414
+ */
415
+ addActiveTask(taskId) {
416
+ this.activeTasks.add(taskId);
417
+ }
418
+ /**
419
+ * Remove task from active set
420
+ */
421
+ removeActiveTask(taskId) {
422
+ this.activeTasks.delete(taskId);
423
+ }
424
+ // ============================================================================
425
+ // Private Methods
426
+ // ============================================================================
427
+ async handleOpen() {
428
+ // Send registration message
429
+ const resources = await getMachineResources();
430
+ // Allow config to override the display name (e.g., SSH alias like "nebius-2")
431
+ const machineName = configManager.getMachineName();
432
+ const registerMsg = {
433
+ type: 'register',
434
+ timestamp: new Date().toISOString(),
435
+ payload: {
436
+ runnerId: this.runnerId,
437
+ machineId: this.machineId,
438
+ ...(machineName ? { name: machineName } : {}),
439
+ providers: this.providers,
440
+ executionStrategies: this.executionStrategies,
441
+ resources,
442
+ version: this.version,
443
+ },
444
+ };
445
+ this.send(registerMsg);
446
+ // Start heartbeat
447
+ this.startHeartbeat();
448
+ this.emitEvent({ type: 'connected' });
449
+ }
450
+ handleMessage(data) {
451
+ try {
452
+ const raw = JSON.parse(data.toString());
453
+ // Handle task.steer (dot notation from relay) by normalizing to task_steer
454
+ if (raw.type === 'task.steer') {
455
+ // Relay sends: { type: 'task.steer', taskId, message, action, interrupt }
456
+ // Normalize to agent-runner format: { type: 'task_steer', payload: { taskId, message, action, interrupt } }
457
+ const steerMsg = {
458
+ type: 'task_steer',
459
+ timestamp: raw.timestamp ?? new Date().toISOString(),
460
+ payload: {
461
+ taskId: raw.taskId,
462
+ message: raw.message,
463
+ action: raw.action,
464
+ interrupt: raw.interrupt,
465
+ },
466
+ };
467
+ this.handleTaskSteer(steerMsg);
468
+ return;
469
+ }
470
+ // Handle file_list.request (dot notation from relay)
471
+ if (raw.type === 'file_list.request') {
472
+ const fileListMsg = {
473
+ type: 'file_list_request',
474
+ timestamp: raw.timestamp ?? new Date().toISOString(),
475
+ payload: {
476
+ path: raw.path,
477
+ correlationId: raw.correlationId,
478
+ },
479
+ };
480
+ this.handleFileListRequest(fileListMsg);
481
+ return;
482
+ }
483
+ // Handle slash_commands.request (dot notation from relay)
484
+ if (raw.type === 'slash_commands.request') {
485
+ const slashMsg = {
486
+ type: 'slash_commands_request',
487
+ timestamp: raw.timestamp ?? new Date().toISOString(),
488
+ payload: {
489
+ correlationId: raw.correlationId,
490
+ workingDirectory: raw.workingDirectory,
491
+ },
492
+ };
493
+ this.handleSlashCommandsRequest(slashMsg);
494
+ return;
495
+ }
496
+ // Handle repo_setup.request (dot notation from relay)
497
+ if (raw.type === 'repo_setup.request') {
498
+ const repoMsg = {
499
+ type: 'repo_setup_request',
500
+ timestamp: raw.timestamp ?? new Date().toISOString(),
501
+ payload: raw.payload,
502
+ };
503
+ this.handleRepoSetupRequest(repoMsg);
504
+ return;
505
+ }
506
+ // Handle repo_detect.request (dot notation from relay)
507
+ if (raw.type === 'repo_detect.request') {
508
+ const detectMsg = {
509
+ type: 'repo_detect_request',
510
+ timestamp: raw.timestamp ?? new Date().toISOString(),
511
+ payload: raw.payload,
512
+ };
513
+ this.handleRepoDetectRequest(detectMsg);
514
+ return;
515
+ }
516
+ // Handle git_init.request (dot notation from relay)
517
+ if (raw.type === 'git_init.request') {
518
+ const gitInitMsg = {
519
+ type: 'git_init_request',
520
+ timestamp: raw.timestamp ?? new Date().toISOString(),
521
+ payload: raw.payload,
522
+ };
523
+ this.handleGitInitRequest(gitInitMsg);
524
+ return;
525
+ }
526
+ const message = raw;
527
+ this.routeMessage(message);
528
+ }
529
+ catch (error) {
530
+ this.emitEvent({ type: 'error', error: error });
531
+ }
532
+ }
533
+ routeMessage(message) {
534
+ switch (message.type) {
535
+ case 'registered':
536
+ this.handleRegistered(message);
537
+ break;
538
+ case 'heartbeat_ack':
539
+ // Heartbeat acknowledged, nothing to do
540
+ break;
541
+ case 'task_dispatch':
542
+ this.handleTaskDispatch(message);
543
+ break;
544
+ case 'task_cancel':
545
+ this.handleTaskCancel(message);
546
+ break;
547
+ case 'task_steer':
548
+ this.handleTaskSteer(message);
549
+ break;
550
+ case 'task_approval_response':
551
+ this.handleApprovalResponse(message);
552
+ break;
553
+ case 'task_safety_decision':
554
+ this.handleSafetyDecision(message);
555
+ break;
556
+ case 'config_update':
557
+ this.handleConfigUpdate(message);
558
+ break;
559
+ case 'file_list_request':
560
+ this.handleFileListRequest(message);
561
+ break;
562
+ case 'repo_setup_request':
563
+ this.handleRepoSetupRequest(message);
564
+ break;
565
+ case 'slash_commands_request':
566
+ this.handleSlashCommandsRequest(message);
567
+ break;
568
+ case 'repo_detect_request':
569
+ this.handleRepoDetectRequest(message);
570
+ break;
571
+ case 'git_init_request':
572
+ this.handleGitInitRequest(message);
573
+ break;
574
+ case 'error':
575
+ this.handleError(message);
576
+ break;
577
+ }
578
+ }
579
+ handleRegistered(message) {
580
+ // Apply server-provided configuration
581
+ if (message.payload.config) {
582
+ this.config = { ...this.config, ...message.payload.config };
583
+ }
584
+ }
585
+ handleTaskDispatch(message) {
586
+ const task = message.payload;
587
+ // Check if we can accept more tasks
588
+ if (this.activeTasks.size >= this.config.maxConcurrentTasks) {
589
+ this.sendTaskStatus(task.id, 'queued', 0, 'Waiting for available slot');
590
+ return;
591
+ }
592
+ this.activeTasks.add(task.id);
593
+ this.emitEvent({ type: 'task_received', task });
594
+ this.onTaskDispatch?.(task);
595
+ }
596
+ handleTaskCancel(message) {
597
+ const { taskId } = message.payload;
598
+ this.activeTasks.delete(taskId);
599
+ this.emitEvent({ type: 'task_cancelled', taskId });
600
+ this.onTaskCancel?.(taskId);
601
+ }
602
+ handleTaskSteer(message) {
603
+ const { taskId, message: steerMessage, action, interrupt } = message.payload;
604
+ this.onTaskSteer?.(taskId, steerMessage, action, interrupt);
605
+ }
606
+ handleSafetyDecision(message) {
607
+ const { taskId, decision } = message.payload;
608
+ console.log(`[ws-client] Received safety decision for ${taskId}: ${decision}`);
609
+ this.onTaskSafetyDecision?.(taskId, decision);
610
+ }
611
+ handleApprovalResponse(message) {
612
+ const { requestId, answered, answer, message: responseMessage } = message.payload;
613
+ const pending = this.pendingApprovals.get(requestId);
614
+ if (pending) {
615
+ console.log(`[ws-client] Received approval response for ${requestId}: answered=${answered}, answer=${answer}`);
616
+ this.pendingApprovals.delete(requestId);
617
+ pending.resolve({ answered, answer, message: responseMessage });
618
+ }
619
+ else {
620
+ console.warn(`[ws-client] Received approval response for unknown request ${requestId}`);
621
+ }
622
+ }
623
+ handleConfigUpdate(message) {
624
+ this.config = { ...this.config, ...message.payload };
625
+ // Restart heartbeat with new interval if changed
626
+ if (message.payload.heartbeatInterval) {
627
+ this.stopHeartbeat();
628
+ this.startHeartbeat();
629
+ }
630
+ }
631
+ handleError(message) {
632
+ const code = message.payload?.code ?? 'UNKNOWN';
633
+ const msg = message.payload?.message ?? 'Unknown error';
634
+ const error = new Error(`${code}: ${msg}`);
635
+ this.emitEvent({ type: 'error', error });
636
+ }
637
+ handleFileListRequest(message) {
638
+ const { path, correlationId } = message.payload;
639
+ this.onFileList?.(path, correlationId);
640
+ }
641
+ /**
642
+ * Send file list response
643
+ */
644
+ sendFileListResponse(correlationId, files) {
645
+ const msg = {
646
+ type: 'file_list_response',
647
+ timestamp: new Date().toISOString(),
648
+ payload: { correlationId, files },
649
+ };
650
+ this.send(msg);
651
+ }
652
+ handleRepoSetupRequest(message) {
653
+ this.onRepoSetup?.(message.payload);
654
+ }
655
+ handleSlashCommandsRequest(message) {
656
+ const { correlationId, workingDirectory } = message.payload;
657
+ this.onSlashCommands?.(correlationId, workingDirectory);
658
+ }
659
+ /**
660
+ * Send slash commands response
661
+ */
662
+ sendSlashCommandsResponse(correlationId, commands) {
663
+ const msg = {
664
+ type: 'slash_commands_response',
665
+ timestamp: new Date().toISOString(),
666
+ payload: { correlationId, commands },
667
+ };
668
+ this.send(msg);
669
+ }
670
+ /**
671
+ * Send repo setup response
672
+ */
673
+ sendRepoSetupResponse(correlationId, result) {
674
+ const msg = {
675
+ type: 'repo_setup_response',
676
+ timestamp: new Date().toISOString(),
677
+ payload: { correlationId, ...result },
678
+ };
679
+ this.send(msg);
680
+ }
681
+ handleRepoDetectRequest(message) {
682
+ this.onRepoDetect?.(message.payload);
683
+ }
684
+ /**
685
+ * Send repo detect response
686
+ */
687
+ sendRepoDetectResponse(correlationId, result) {
688
+ const msg = {
689
+ type: 'repo_detect_response',
690
+ timestamp: new Date().toISOString(),
691
+ payload: { correlationId, ...result },
692
+ };
693
+ this.send(msg);
694
+ }
695
+ handleGitInitRequest(message) {
696
+ this.onGitInit?.(message.payload);
697
+ }
698
+ /**
699
+ * Send git init response
700
+ */
701
+ sendGitInitResponse(correlationId, result) {
702
+ const msg = {
703
+ type: 'git_init_response',
704
+ timestamp: new Date().toISOString(),
705
+ payload: { correlationId, ...result },
706
+ };
707
+ this.send(msg);
708
+ }
709
+ handleClose(code, reason) {
710
+ this.cleanup();
711
+ this.emitEvent({ type: 'disconnected', reason: `${code}: ${reason}` });
712
+ // Custom close code 4001 means this connection was replaced by another
713
+ // process with the same machineId — do not reconnect.
714
+ if (code === 4001) {
715
+ console.error('[ws-client] Connection replaced by another process with the same machineId. Not reconnecting.');
716
+ this.shouldReconnect = false;
717
+ return;
718
+ }
719
+ if (this.shouldReconnect) {
720
+ this.scheduleReconnect();
721
+ }
722
+ }
723
+ send(message) {
724
+ if (this.ws?.readyState === WebSocket.OPEN) {
725
+ this.ws.send(JSON.stringify(message));
726
+ }
727
+ }
728
+ startHeartbeat() {
729
+ this.heartbeatInterval = setInterval(async () => {
730
+ if (this.ws?.readyState === WebSocket.OPEN) {
731
+ // Send application-level heartbeat
732
+ const resources = await getMachineResources();
733
+ const heartbeat = {
734
+ type: 'heartbeat',
735
+ timestamp: new Date().toISOString(),
736
+ payload: {
737
+ runnerId: this.runnerId,
738
+ activeTasks: Array.from(this.activeTasks),
739
+ resources,
740
+ },
741
+ };
742
+ this.send(heartbeat);
743
+ // Also send WebSocket ping for connection health
744
+ this.ws.ping();
745
+ }
746
+ }, this.config.heartbeatInterval);
747
+ }
748
+ stopHeartbeat() {
749
+ if (this.heartbeatInterval) {
750
+ clearInterval(this.heartbeatInterval);
751
+ this.heartbeatInterval = null;
752
+ }
753
+ }
754
+ scheduleReconnect() {
755
+ const maxRetries = this.config.reconnectMaxRetries;
756
+ if (maxRetries >= 0 && this.reconnectAttempts >= maxRetries) {
757
+ return;
758
+ }
759
+ this.reconnectAttempts++;
760
+ // Exponential backoff with jitter
761
+ const delay = Math.min(this.config.reconnectBaseDelay * Math.pow(2, this.reconnectAttempts - 1) +
762
+ Math.random() * 1000, this.config.reconnectMaxDelay);
763
+ this.emitEvent({ type: 'reconnecting', attempt: this.reconnectAttempts });
764
+ this.reconnectTimeout = setTimeout(() => {
765
+ this.connect().catch(() => {
766
+ // Error already handled in connect()
767
+ });
768
+ }, delay);
769
+ }
770
+ cleanup() {
771
+ this.stopHeartbeat();
772
+ if (this.reconnectTimeout) {
773
+ clearTimeout(this.reconnectTimeout);
774
+ this.reconnectTimeout = null;
775
+ }
776
+ }
777
+ emitEvent(event) {
778
+ this.onEvent?.(event);
779
+ }
780
+ }
781
+ //# sourceMappingURL=websocket-client.js.map