@mindexec/cli 0.2.2 → 0.2.4

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 (24) hide show
  1. package/README.md +50 -0
  2. package/package.json +6 -4
  3. package/remote-hub.js +737 -0
  4. package/scripts/remote-hub-smoke.mjs +146 -0
  5. package/server.js +144 -28
  6. package/wwwroot/_content/MindExecution.Shared/js/mind-map-core.js +11 -0
  7. package/wwwroot/_content/MindExecution.Shared/js/mind-map-css3d-manager.js +732 -2
  8. package/wwwroot/_content/MindExecution.Shared/js/mind-map-nodes.js +3 -1
  9. package/wwwroot/_framework/MindExecution.Core.rydw4mhsbd.dll +0 -0
  10. package/wwwroot/_framework/{MindExecution.Kernel.gwwc40sc45.dll → MindExecution.Kernel.8sz1fl3k6s.dll} +0 -0
  11. package/wwwroot/_framework/{MindExecution.Plugins.Admin.0jgrn1sckv.dll → MindExecution.Plugins.Admin.iltai5c3i9.dll} +0 -0
  12. package/wwwroot/_framework/{MindExecution.Plugins.Business.13mme2qcag.dll → MindExecution.Plugins.Business.mscgb1gwpf.dll} +0 -0
  13. package/wwwroot/_framework/{MindExecution.Plugins.Concept.9al2g3v3f9.dll → MindExecution.Plugins.Concept.s888y8snr4.dll} +0 -0
  14. package/wwwroot/_framework/{MindExecution.Plugins.Directory.3w4t6n3se0.dll → MindExecution.Plugins.Directory.281klijdzl.dll} +0 -0
  15. package/wwwroot/_framework/{MindExecution.Plugins.PlanMaster.vfmfbygv5y.dll → MindExecution.Plugins.PlanMaster.2gy2ozelqp.dll} +0 -0
  16. package/wwwroot/_framework/{MindExecution.Plugins.YouTube.32jyiqs383.dll → MindExecution.Plugins.YouTube.1v8o9nnlzq.dll} +0 -0
  17. package/wwwroot/_framework/{MindExecution.Shared.7ttmykvopx.dll → MindExecution.Shared.04anisxh35.dll} +0 -0
  18. package/wwwroot/_framework/MindExecution.Web.0qdidsf6sl.dll +0 -0
  19. package/wwwroot/_framework/blazor.boot.json +21 -21
  20. package/wwwroot/index.html +1 -1
  21. package/wwwroot/service-worker-assets.js +26 -26
  22. package/wwwroot/service-worker.js +1 -1
  23. package/wwwroot/_framework/MindExecution.Core.1q1trifbuu.dll +0 -0
  24. package/wwwroot/_framework/MindExecution.Web.ozzcqp30uy.dll +0 -0
package/remote-hub.js ADDED
@@ -0,0 +1,737 @@
1
+ import net from 'net';
2
+ import os from 'os';
3
+ import crypto from 'crypto';
4
+
5
+ const DEFAULT_REMOTE_HUB_PORT = 5197;
6
+ const DEFAULT_REMOTE_HUB_HOST = '127.0.0.1';
7
+ const DEFAULT_HEARTBEAT_MS = 5000;
8
+ const MAX_LINE_CHARS = 4 * 1024 * 1024;
9
+ const MAX_THUMBNAIL_BASE64_CHARS = 3 * 1024 * 1024;
10
+ const MAX_AGENT_TASK_CHARS = 4000;
11
+ const MAX_AGENT_TASK_RESULT_CHARS = 3000;
12
+ const RECENT_TASK_LIMIT = 12;
13
+ const REMOTE_PROTOCOL_VERSION = 1;
14
+
15
+ function isEnabledValue(value, fallback = true) {
16
+ if (value === undefined || value === null || value === '') {
17
+ return fallback;
18
+ }
19
+
20
+ return /^(1|true|yes|on)$/i.test(String(value).trim());
21
+ }
22
+
23
+ function isDisabledValue(value) {
24
+ return /^(1|true|yes|on)$/i.test(String(value || '').trim());
25
+ }
26
+
27
+ function clampNumber(value, min, max, fallback) {
28
+ const number = Number(value);
29
+ if (!Number.isFinite(number)) {
30
+ return fallback;
31
+ }
32
+
33
+ return Math.max(min, Math.min(max, Math.floor(number)));
34
+ }
35
+
36
+ function normalizePort(value) {
37
+ return clampNumber(value, 0, 65535, DEFAULT_REMOTE_HUB_PORT);
38
+ }
39
+
40
+ function safeString(value, maxLength = 200) {
41
+ return String(value ?? '').replace(/[\r\n\t]/g, ' ').trim().slice(0, maxLength);
42
+ }
43
+
44
+ function safeText(value, maxLength = 1000) {
45
+ return String(value ?? '').replace(/\0/g, '').trim().slice(0, maxLength);
46
+ }
47
+
48
+ function normalizeDeviceId(value) {
49
+ const id = safeString(value, 128).replace(/[^a-zA-Z0-9_.:-]/g, '-');
50
+ return id || crypto.randomUUID();
51
+ }
52
+
53
+ function maskToken(token) {
54
+ const value = String(token || '');
55
+ if (value.length <= 8) {
56
+ return value ? '****' : '';
57
+ }
58
+
59
+ return `${value.slice(0, 4)}...${value.slice(-4)}`;
60
+ }
61
+
62
+ function timingSafeStringEqual(left, right) {
63
+ const a = Buffer.from(String(left || ''), 'utf8');
64
+ const b = Buffer.from(String(right || ''), 'utf8');
65
+ if (a.length !== b.length) {
66
+ return false;
67
+ }
68
+
69
+ return crypto.timingSafeEqual(a, b);
70
+ }
71
+
72
+ function parseJsonLine(line) {
73
+ const text = String(line || '').trim();
74
+ if (!text) {
75
+ return null;
76
+ }
77
+
78
+ return JSON.parse(text);
79
+ }
80
+
81
+ function serializeDevice(device) {
82
+ if (!device) {
83
+ return null;
84
+ }
85
+
86
+ return {
87
+ deviceId: device.deviceId,
88
+ sessionId: device.sessionId,
89
+ deviceName: device.deviceName,
90
+ hostname: device.hostname,
91
+ platform: device.platform,
92
+ arch: device.arch,
93
+ pid: device.pid,
94
+ agentVersion: device.agentVersion,
95
+ capabilities: { ...device.capabilities },
96
+ connected: device.connected,
97
+ connectedAt: device.connectedAt,
98
+ disconnectedAt: device.disconnectedAt,
99
+ lastSeenAt: device.lastSeenAt,
100
+ lastStatusAt: device.lastStatusAt,
101
+ lastDisconnectReason: device.lastDisconnectReason,
102
+ remoteAddress: device.remoteAddress,
103
+ remotePort: device.remotePort,
104
+ latestThumbnail: device.latestThumbnail ? { ...device.latestThumbnail } : null,
105
+ latestTask: device.latestTask ? { ...device.latestTask } : null,
106
+ recentTasks: Array.isArray(device.recentTasks)
107
+ ? device.recentTasks.map(task => ({ ...task }))
108
+ : [],
109
+ status: { ...device.status },
110
+ counters: { ...device.counters }
111
+ };
112
+ }
113
+
114
+ function writeJsonLine(socket, payload) {
115
+ if (!socket || socket.destroyed) {
116
+ return false;
117
+ }
118
+
119
+ socket.write(`${JSON.stringify(payload)}\n`);
120
+ return true;
121
+ }
122
+
123
+ export function createRemoteHub(options = {}) {
124
+ const env = options.env || process.env;
125
+ const logEvent = options.logEvent || (() => {});
126
+ const logWarn = options.logWarn || (() => {});
127
+ const emitEvent = options.emitEvent || (() => {});
128
+
129
+ const enabled = isEnabledValue(env.MINDEXEC_REMOTE_HUB ?? env.REMOTE_HUB_ENABLED, true)
130
+ && !isDisabledValue(env.REMOTE_HUB_DISABLED);
131
+ const host = safeString(env.REMOTE_HUB_HOST || DEFAULT_REMOTE_HUB_HOST, 128);
132
+ const requestedPort = normalizePort(env.REMOTE_HUB_PORT || DEFAULT_REMOTE_HUB_PORT);
133
+ const heartbeatMs = clampNumber(env.REMOTE_HUB_HEARTBEAT_MS, 1000, 60000, DEFAULT_HEARTBEAT_MS);
134
+ const pairToken = safeString(
135
+ env.REMOTE_HUB_PAIR_TOKEN || env.MINDEXEC_REMOTE_PAIR_TOKEN || crypto.randomBytes(6).toString('hex'),
136
+ 256);
137
+
138
+ const devices = new Map();
139
+ const sockets = new Map();
140
+ const allSockets = new Set();
141
+ let server = null;
142
+ let started = false;
143
+ let boundPort = requestedPort;
144
+ let lastError = '';
145
+
146
+ function getStatus({ includeSecrets = false } = {}) {
147
+ const connectedDevices = [...devices.values()].filter(device => device.connected).length;
148
+ return {
149
+ enabled,
150
+ started,
151
+ host,
152
+ port: boundPort || requestedPort,
153
+ protocol: 'tcp-jsonl',
154
+ protocolVersion: REMOTE_PROTOCOL_VERSION,
155
+ heartbeatMs,
156
+ agentPackage: '@mindexec/remote',
157
+ agentEndpoint: `${host}:${boundPort || requestedPort}`,
158
+ pairToken: includeSecrets ? pairToken : undefined,
159
+ pairTokenPreview: maskToken(pairToken),
160
+ deviceCount: devices.size,
161
+ connectedDeviceCount: connectedDevices,
162
+ canvasDeviceListMode: 'all-devices',
163
+ canvasPagination: 'none',
164
+ externalExposure: host === '0.0.0.0' || host === '::',
165
+ lastError
166
+ };
167
+ }
168
+
169
+ function listDevices() {
170
+ return [...devices.values()]
171
+ .map(serializeDevice)
172
+ .filter(Boolean)
173
+ .sort((a, b) => {
174
+ const nameCompare = String(a.deviceName || '').localeCompare(String(b.deviceName || ''));
175
+ if (nameCompare !== 0) {
176
+ return nameCompare;
177
+ }
178
+
179
+ return String(a.deviceId).localeCompare(String(b.deviceId));
180
+ });
181
+ }
182
+
183
+ function emitRemoteEvent(type, device = null, extra = {}) {
184
+ emitEvent(type, {
185
+ ...extra,
186
+ remoteHub: getStatus({ includeSecrets: false }),
187
+ device: serializeDevice(device)
188
+ });
189
+ }
190
+
191
+ function closeExistingDeviceSocket(deviceId, nextSessionId) {
192
+ const existing = devices.get(deviceId);
193
+ if (!existing?.socket || existing.socket.destroyed) {
194
+ return;
195
+ }
196
+
197
+ existing.lastDisconnectReason = 'replaced-by-new-session';
198
+ writeJsonLine(existing.socket, {
199
+ type: 'disconnect',
200
+ reason: 'replaced-by-new-session',
201
+ nextSessionId
202
+ });
203
+ existing.socket.destroy();
204
+ }
205
+
206
+ function attachDevice(socket, hello) {
207
+ const now = new Date().toISOString();
208
+ const sessionId = crypto.randomUUID();
209
+ const deviceId = normalizeDeviceId(hello.deviceId);
210
+
211
+ closeExistingDeviceSocket(deviceId, sessionId);
212
+
213
+ const device = {
214
+ socket,
215
+ deviceId,
216
+ sessionId,
217
+ deviceName: safeString(hello.deviceName || hello.hostname || deviceId, 120),
218
+ hostname: safeString(hello.hostname, 120),
219
+ platform: safeString(hello.platform, 80),
220
+ arch: safeString(hello.arch, 40),
221
+ pid: Number.isFinite(Number(hello.pid)) ? Number(hello.pid) : 0,
222
+ agentVersion: safeString(hello.agentVersion, 40),
223
+ capabilities: typeof hello.capabilities === 'object' && hello.capabilities
224
+ ? { ...hello.capabilities }
225
+ : {},
226
+ connected: true,
227
+ connectedAt: now,
228
+ disconnectedAt: '',
229
+ lastSeenAt: now,
230
+ lastStatusAt: '',
231
+ lastDisconnectReason: '',
232
+ remoteAddress: socket.remoteAddress || '',
233
+ remotePort: socket.remotePort || 0,
234
+ status: {},
235
+ latestThumbnail: null,
236
+ latestTask: null,
237
+ recentTasks: [],
238
+ pendingTaskCommands: new Map(),
239
+ counters: {
240
+ messagesReceived: 1,
241
+ statusReceived: 0,
242
+ commandsSent: 0,
243
+ commandResultsReceived: 0,
244
+ thumbnailFramesReceived: 0,
245
+ thumbnailFramesDropped: 0,
246
+ tasksQueued: 0,
247
+ taskResultsReceived: 0,
248
+ taskResultsFailed: 0
249
+ }
250
+ };
251
+
252
+ devices.set(deviceId, device);
253
+ sockets.set(socket, deviceId);
254
+ writeJsonLine(socket, {
255
+ type: 'welcome',
256
+ protocolVersion: REMOTE_PROTOCOL_VERSION,
257
+ sessionId,
258
+ deviceId,
259
+ heartbeatMs,
260
+ serverTime: now
261
+ });
262
+
263
+ logEvent('remote', `device connected ${device.deviceName} (${deviceId})`, 'success');
264
+ emitRemoteEvent('RemoteDeviceConnected', device);
265
+ return device;
266
+ }
267
+
268
+ function detachSocket(socket, reason = 'socket-closed') {
269
+ const deviceId = sockets.get(socket);
270
+ sockets.delete(socket);
271
+ if (!deviceId) {
272
+ return;
273
+ }
274
+
275
+ const device = devices.get(deviceId);
276
+ if (!device || device.socket !== socket) {
277
+ return;
278
+ }
279
+
280
+ device.connected = false;
281
+ device.socket = null;
282
+ device.disconnectedAt = new Date().toISOString();
283
+ device.lastDisconnectReason = reason;
284
+ logWarn('remote', `device disconnected ${device.deviceName} (${deviceId}): ${reason}`);
285
+ emitRemoteEvent('RemoteDeviceDisconnected', device, { reason });
286
+ }
287
+
288
+ function rememberDeviceTask(device, task) {
289
+ if (!device || !task) {
290
+ return null;
291
+ }
292
+
293
+ const existingIndex = device.recentTasks.findIndex(item =>
294
+ item.taskId === task.taskId || item.commandId === task.commandId);
295
+ if (existingIndex >= 0) {
296
+ device.recentTasks.splice(existingIndex, 1);
297
+ }
298
+
299
+ device.recentTasks.unshift(task);
300
+ if (device.recentTasks.length > RECENT_TASK_LIMIT) {
301
+ device.recentTasks.length = RECENT_TASK_LIMIT;
302
+ }
303
+
304
+ device.latestTask = task;
305
+ if (task.commandId) {
306
+ device.pendingTaskCommands.set(task.commandId, task);
307
+ }
308
+
309
+ return task;
310
+ }
311
+
312
+ function summarizeTaskResult(result) {
313
+ if (result && typeof result === 'object') {
314
+ return safeText(
315
+ result.summary
316
+ || result.message
317
+ || result.output
318
+ || result.result
319
+ || JSON.stringify(result),
320
+ MAX_AGENT_TASK_RESULT_CHARS);
321
+ }
322
+
323
+ return safeText(result ?? '', MAX_AGENT_TASK_RESULT_CHARS);
324
+ }
325
+
326
+ function applyTaskResult(device, commandId, result, error) {
327
+ if (!device || !commandId) {
328
+ return null;
329
+ }
330
+
331
+ const resultTaskId = result && typeof result === 'object'
332
+ ? safeString(result.taskId, 128)
333
+ : '';
334
+ const task = resultTaskId
335
+ ? device.recentTasks.find(item => item.taskId === resultTaskId)
336
+ : device.pendingTaskCommands.get(commandId);
337
+ if (!task) {
338
+ return null;
339
+ }
340
+
341
+ const now = device.lastSeenAt || new Date().toISOString();
342
+ const status = error
343
+ ? 'failed'
344
+ : safeString(result?.status, 40) || 'completed';
345
+ task.status = status;
346
+ task.updatedAt = now;
347
+ task.completedAt = safeString(result?.completedAt, 80) || now;
348
+ task.error = error;
349
+ task.resultSummary = error || summarizeTaskResult(result);
350
+ task.resultKind = safeString(result?.kind || result?.mode || 'agent-task', 80);
351
+ device.latestTask = task;
352
+ device.pendingTaskCommands.delete(commandId);
353
+ device.counters.taskResultsReceived += 1;
354
+ if (error) {
355
+ device.counters.taskResultsFailed += 1;
356
+ }
357
+
358
+ return task;
359
+ }
360
+
361
+ function handleAgentMessage(socket, state, message) {
362
+ if (!message || typeof message !== 'object') {
363
+ return;
364
+ }
365
+
366
+ if (!state.authenticated) {
367
+ if (message.type !== 'hello') {
368
+ writeJsonLine(socket, { type: 'error', error: 'hello-required' });
369
+ socket.destroy();
370
+ return;
371
+ }
372
+
373
+ if (!timingSafeStringEqual(message.pairToken, pairToken)) {
374
+ writeJsonLine(socket, { type: 'error', error: 'invalid-pair-token' });
375
+ socket.destroy();
376
+ return;
377
+ }
378
+
379
+ state.authenticated = true;
380
+ state.device = attachDevice(socket, message);
381
+ return;
382
+ }
383
+
384
+ const device = state.device;
385
+ if (!device) {
386
+ socket.destroy();
387
+ return;
388
+ }
389
+
390
+ device.counters.messagesReceived += 1;
391
+ device.lastSeenAt = new Date().toISOString();
392
+
393
+ switch (message.type) {
394
+ case 'heartbeat':
395
+ case 'status':
396
+ device.status = typeof message.status === 'object' && message.status
397
+ ? { ...message.status }
398
+ : {};
399
+ device.lastStatusAt = device.lastSeenAt;
400
+ device.counters.statusReceived += 1;
401
+ emitRemoteEvent('RemoteDeviceStatus', device);
402
+ break;
403
+ case 'command.result':
404
+ device.counters.commandResultsReceived += 1;
405
+ {
406
+ const commandId = safeString(message.commandId, 128);
407
+ const error = safeString(message.error, 500);
408
+ const task = applyTaskResult(device, commandId, message.result ?? null, error);
409
+ if (task) {
410
+ emitRemoteEvent('RemoteTaskResult', device, {
411
+ commandId,
412
+ taskId: task.taskId,
413
+ status: task.status,
414
+ error: task.error
415
+ });
416
+ }
417
+ }
418
+ emitRemoteEvent('RemoteCommandResult', device, {
419
+ commandId: safeString(message.commandId, 128),
420
+ result: message.result ?? null,
421
+ error: safeString(message.error, 500)
422
+ });
423
+ break;
424
+ case 'thumbnail.frame': {
425
+ const frameData = safeString(message.data, MAX_THUMBNAIL_BASE64_CHARS + 1);
426
+ const frameSeq = Number(message.frameSeq);
427
+ if (!frameData || frameData.length > MAX_THUMBNAIL_BASE64_CHARS || !Number.isFinite(frameSeq)) {
428
+ device.counters.thumbnailFramesDropped += 1;
429
+ emitRemoteEvent('RemoteFrameDropped', device, {
430
+ reason: 'invalid-thumbnail-frame',
431
+ frameSeq: Number.isFinite(frameSeq) ? frameSeq : null
432
+ });
433
+ break;
434
+ }
435
+
436
+ const mimeType = safeString(message.mimeType || message.format || 'image/jpeg', 80) || 'image/jpeg';
437
+ const capturedAt = safeString(message.capturedAt, 80) || device.lastSeenAt;
438
+ device.latestThumbnail = {
439
+ streamId: safeString(message.streamId, 128) || 'thumbnail',
440
+ frameSeq,
441
+ commandId: safeString(message.commandId, 128),
442
+ width: Number.isFinite(Number(message.width)) ? Number(message.width) : 0,
443
+ height: Number.isFinite(Number(message.height)) ? Number(message.height) : 0,
444
+ mimeType,
445
+ format: mimeType,
446
+ capturedAt,
447
+ receivedAt: device.lastSeenAt,
448
+ byteLength: Math.floor(frameData.length * 3 / 4),
449
+ dataUrl: frameData.startsWith('data:')
450
+ ? frameData
451
+ : `data:${mimeType};base64,${frameData}`
452
+ };
453
+ device.counters.thumbnailFramesReceived += 1;
454
+ emitRemoteEvent('RemoteFrameReceived', device, {
455
+ streamId: device.latestThumbnail.streamId,
456
+ frameSeq,
457
+ width: device.latestThumbnail.width,
458
+ height: device.latestThumbnail.height
459
+ });
460
+ break;
461
+ }
462
+ default:
463
+ emitRemoteEvent('RemoteAgentMessageIgnored', device, {
464
+ messageType: safeString(message.type, 80)
465
+ });
466
+ break;
467
+ }
468
+ }
469
+
470
+ function handleSocket(socket) {
471
+ allSockets.add(socket);
472
+ socket.setEncoding('utf8');
473
+ socket.setNoDelay(true);
474
+ socket.setKeepAlive(true, heartbeatMs);
475
+
476
+ const state = {
477
+ authenticated: false,
478
+ device: null,
479
+ buffer: ''
480
+ };
481
+
482
+ const helloTimer = setTimeout(() => {
483
+ if (!state.authenticated) {
484
+ writeJsonLine(socket, { type: 'error', error: 'hello-timeout' });
485
+ socket.destroy();
486
+ }
487
+ }, 10000);
488
+
489
+ socket.on('data', chunk => {
490
+ state.buffer += chunk;
491
+ if (state.buffer.length > MAX_LINE_CHARS) {
492
+ writeJsonLine(socket, { type: 'error', error: 'message-too-large' });
493
+ socket.destroy();
494
+ return;
495
+ }
496
+
497
+ let newlineIndex = state.buffer.indexOf('\n');
498
+ while (newlineIndex >= 0) {
499
+ const line = state.buffer.slice(0, newlineIndex);
500
+ state.buffer = state.buffer.slice(newlineIndex + 1);
501
+
502
+ try {
503
+ const message = parseJsonLine(line);
504
+ handleAgentMessage(socket, state, message);
505
+ } catch (err) {
506
+ writeJsonLine(socket, { type: 'error', error: 'invalid-json' });
507
+ logWarn('remote', `invalid agent message: ${err?.message || err}`);
508
+ }
509
+
510
+ newlineIndex = state.buffer.indexOf('\n');
511
+ }
512
+ });
513
+
514
+ socket.on('close', () => {
515
+ allSockets.delete(socket);
516
+ clearTimeout(helloTimer);
517
+ detachSocket(socket);
518
+ });
519
+ socket.on('error', err => {
520
+ allSockets.delete(socket);
521
+ clearTimeout(helloTimer);
522
+ detachSocket(socket, err?.message || 'socket-error');
523
+ });
524
+ }
525
+
526
+ async function start() {
527
+ if (!enabled || started) {
528
+ return getStatus({ includeSecrets: false });
529
+ }
530
+
531
+ await new Promise((resolve, reject) => {
532
+ server = net.createServer(handleSocket);
533
+ server.once('error', err => {
534
+ lastError = err?.message || String(err);
535
+ server = null;
536
+ reject(err);
537
+ });
538
+ server.listen(requestedPort, host, () => {
539
+ started = true;
540
+ boundPort = server.address()?.port || requestedPort;
541
+ lastError = '';
542
+ logEvent('remote', `RemoteHub listening on tcp://${host}:${boundPort}`, 'success');
543
+ if (host === '0.0.0.0' || host === '::') {
544
+ logWarn('remote', 'RemoteHub is externally reachable. Use a strong pairing token and trusted network.');
545
+ }
546
+ emitRemoteEvent('RemoteHubStarted', null);
547
+ resolve();
548
+ });
549
+ });
550
+
551
+ return getStatus({ includeSecrets: false });
552
+ }
553
+
554
+ async function close() {
555
+ for (const device of devices.values()) {
556
+ if (device.socket && !device.socket.destroyed) {
557
+ writeJsonLine(device.socket, { type: 'disconnect', reason: 'hub-shutdown' });
558
+ device.socket.destroy();
559
+ }
560
+ }
561
+
562
+ for (const socket of allSockets) {
563
+ if (!socket.destroyed) {
564
+ socket.destroy();
565
+ }
566
+ }
567
+
568
+ sockets.clear();
569
+ if (!server) {
570
+ started = false;
571
+ return;
572
+ }
573
+
574
+ await new Promise(resolve => {
575
+ let settled = false;
576
+ const settle = () => {
577
+ if (settled) {
578
+ return;
579
+ }
580
+
581
+ settled = true;
582
+ clearTimeout(closeTimer);
583
+ resolve();
584
+ };
585
+
586
+ const closeTimer = setTimeout(settle, 1000);
587
+ closeTimer.unref?.();
588
+
589
+ try {
590
+ server.close(settle);
591
+ } catch {
592
+ settle();
593
+ }
594
+ });
595
+
596
+ server = null;
597
+ started = false;
598
+ }
599
+
600
+ function disconnectDevice(deviceId, reason = 'manager-disconnect') {
601
+ const device = devices.get(String(deviceId || ''));
602
+ if (!device?.socket || device.socket.destroyed) {
603
+ return false;
604
+ }
605
+
606
+ writeJsonLine(device.socket, { type: 'disconnect', reason });
607
+ device.socket.destroy();
608
+ return true;
609
+ }
610
+
611
+ function sendCommand(deviceId, command) {
612
+ const device = devices.get(String(deviceId || ''));
613
+ if (!device?.socket || device.socket.destroyed || !device.connected) {
614
+ return { ok: false, error: 'device-not-connected' };
615
+ }
616
+
617
+ const commandId = safeString(command?.commandId, 128) || crypto.randomUUID();
618
+ const payload = {
619
+ type: 'command',
620
+ commandId,
621
+ command: safeString(command?.command || 'ping', 80),
622
+ payload: command?.payload ?? null,
623
+ issuedAt: new Date().toISOString()
624
+ };
625
+
626
+ writeJsonLine(device.socket, payload);
627
+ device.counters.commandsSent += 1;
628
+ emitRemoteEvent('RemoteCommandQueued', device, {
629
+ commandId,
630
+ command: payload.command
631
+ });
632
+ return { ok: true, commandId };
633
+ }
634
+
635
+ function requestAgentTask(deviceId, options = {}) {
636
+ const device = devices.get(String(deviceId || ''));
637
+ if (!device?.socket || device.socket.destroyed || !device.connected) {
638
+ return { ok: false, error: 'device-not-connected' };
639
+ }
640
+
641
+ const instruction = safeText(options.instruction, MAX_AGENT_TASK_CHARS);
642
+ if (!instruction) {
643
+ return { ok: false, error: 'missing-instruction' };
644
+ }
645
+
646
+ const now = new Date().toISOString();
647
+ const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
648
+ const taskId = safeString(options.taskId, 128) || crypto.randomUUID();
649
+ const title = safeString(options.title, 120)
650
+ || safeString(instruction.split(/\r?\n/)[0], 120)
651
+ || 'Remote task';
652
+ const task = {
653
+ taskId,
654
+ commandId,
655
+ title,
656
+ instructionPreview: safeText(instruction, 320),
657
+ status: 'queued',
658
+ approvalLevel: 'task-only',
659
+ requestedAt: now,
660
+ sentAt: now,
661
+ updatedAt: now,
662
+ completedAt: '',
663
+ error: '',
664
+ resultKind: '',
665
+ resultSummary: ''
666
+ };
667
+
668
+ const sent = writeJsonLine(device.socket, {
669
+ type: 'command',
670
+ commandId,
671
+ command: 'agent.task',
672
+ payload: {
673
+ taskId,
674
+ title,
675
+ instruction,
676
+ approvalLevel: 'task-only',
677
+ requestedAt: now
678
+ },
679
+ issuedAt: now
680
+ });
681
+
682
+ if (!sent) {
683
+ return { ok: false, error: 'device-not-connected' };
684
+ }
685
+
686
+ device.counters.commandsSent += 1;
687
+ device.counters.tasksQueued += 1;
688
+ rememberDeviceTask(device, task);
689
+ emitRemoteEvent('RemoteTaskQueued', device, {
690
+ commandId,
691
+ taskId,
692
+ title
693
+ });
694
+ return { ok: true, commandId, taskId };
695
+ }
696
+
697
+ function requestThumbnail(deviceId, options = {}) {
698
+ return sendCommand(deviceId, {
699
+ command: 'thumbnail.capture',
700
+ commandId: safeString(options.commandId, 128) || crypto.randomUUID(),
701
+ payload: {
702
+ streamId: safeString(options.streamId, 128) || `thumb-${Date.now()}`,
703
+ maxWidth: clampNumber(options.maxWidth, 160, 1920, 360),
704
+ maxHeight: clampNumber(options.maxHeight, 90, 1080, 220),
705
+ quality: clampNumber(options.quality, 20, 95, 55),
706
+ requestedAt: new Date().toISOString()
707
+ }
708
+ });
709
+ }
710
+
711
+ function getDeviceThumbnail(deviceId) {
712
+ const device = devices.get(String(deviceId || ''));
713
+ return device?.latestThumbnail ? { ...device.latestThumbnail } : null;
714
+ }
715
+
716
+ return {
717
+ start,
718
+ close,
719
+ getStatus,
720
+ listDevices,
721
+ disconnectDevice,
722
+ sendCommand,
723
+ requestAgentTask,
724
+ requestThumbnail,
725
+ getDeviceThumbnail,
726
+ getPairToken: () => pairToken
727
+ };
728
+ }
729
+
730
+ export function getDefaultRemoteAgentDeviceInfo() {
731
+ return {
732
+ hostname: os.hostname(),
733
+ platform: os.platform(),
734
+ arch: os.arch(),
735
+ pid: process.pid
736
+ };
737
+ }