@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
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env node
2
+
3
+ import net from 'net';
4
+ import assert from 'assert/strict';
5
+ import { createRemoteHub } from '../remote-hub.js';
6
+
7
+ function writeJsonLine(socket, payload) {
8
+ socket.write(`${JSON.stringify(payload)}\n`);
9
+ }
10
+
11
+ function wait(ms) {
12
+ return new Promise(resolve => setTimeout(resolve, ms));
13
+ }
14
+
15
+ async function waitFor(predicate, timeoutMs = 3000) {
16
+ const startedAt = Date.now();
17
+ while (Date.now() - startedAt < timeoutMs) {
18
+ const value = predicate();
19
+ if (value) {
20
+ return value;
21
+ }
22
+ await wait(25);
23
+ }
24
+
25
+ throw new Error('Timed out waiting for RemoteHub smoke condition.');
26
+ }
27
+
28
+ const hub = createRemoteHub({
29
+ env: {
30
+ MINDEXEC_REMOTE_HUB: '1',
31
+ REMOTE_HUB_HOST: '127.0.0.1',
32
+ REMOTE_HUB_PORT: '0',
33
+ REMOTE_HUB_PAIR_TOKEN: 'smoke-token'
34
+ }
35
+ });
36
+
37
+ try {
38
+ await hub.start();
39
+ const status = hub.getStatus({ includeSecrets: true });
40
+ assert.equal(status.started, true);
41
+ assert.equal(status.canvasPagination, 'none');
42
+
43
+ const socket = net.createConnection({ host: '127.0.0.1', port: status.port });
44
+ socket.setEncoding('utf8');
45
+ await new Promise((resolve, reject) => {
46
+ socket.once('connect', resolve);
47
+ socket.once('error', reject);
48
+ });
49
+
50
+ writeJsonLine(socket, {
51
+ type: 'hello',
52
+ pairToken: 'smoke-token',
53
+ deviceId: 'smoke-device',
54
+ deviceName: 'Smoke Device',
55
+ hostname: 'smoke-host',
56
+ platform: process.platform,
57
+ arch: process.arch,
58
+ pid: process.pid,
59
+ agentVersion: '0.0.0-smoke',
60
+ capabilities: {
61
+ status: true,
62
+ thumbnail: false,
63
+ control: false,
64
+ computerAgent: true,
65
+ taskDispatch: true
66
+ }
67
+ });
68
+ writeJsonLine(socket, {
69
+ type: 'status',
70
+ status: {
71
+ uptimeSec: 1,
72
+ totalMem: 2,
73
+ freeMem: 1
74
+ }
75
+ });
76
+
77
+ const devices = await waitFor(() => {
78
+ const current = hub.listDevices();
79
+ return current.length === 1 && current[0].status?.uptimeSec === 1 ? current : null;
80
+ });
81
+
82
+ assert.equal(devices[0].deviceId, 'smoke-device');
83
+ assert.equal(devices[0].connected, true);
84
+ assert.equal(hub.getStatus().deviceCount, 1);
85
+
86
+ const thumbnailCommand = hub.requestThumbnail('smoke-device', {
87
+ streamId: 'smoke-thumb',
88
+ maxWidth: 320,
89
+ maxHeight: 180,
90
+ quality: 50
91
+ });
92
+ assert.equal(thumbnailCommand.ok, true);
93
+
94
+ writeJsonLine(socket, {
95
+ type: 'thumbnail.frame',
96
+ commandId: thumbnailCommand.commandId,
97
+ streamId: 'smoke-thumb',
98
+ frameSeq: 1,
99
+ width: 2,
100
+ height: 1,
101
+ mimeType: 'image/png',
102
+ capturedAt: new Date().toISOString(),
103
+ data: 'iVBORw0KGgoAAAANSUhEUgAAAAIAAAABCAYAAAD0In+KAAAADElEQVR42mP8z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC'
104
+ });
105
+
106
+ const thumbnailDevice = await waitFor(() => {
107
+ const current = hub.listDevices();
108
+ return current[0]?.latestThumbnail?.frameSeq === 1 ? current[0] : null;
109
+ });
110
+ assert.equal(thumbnailDevice.latestThumbnail.streamId, 'smoke-thumb');
111
+ assert.equal(thumbnailDevice.counters.thumbnailFramesReceived, 1);
112
+
113
+ const taskCommand = hub.requestAgentTask('smoke-device', {
114
+ instruction: 'Summarize current desktop status for the manager.',
115
+ title: 'Smoke task'
116
+ });
117
+ assert.equal(taskCommand.ok, true);
118
+ writeJsonLine(socket, {
119
+ type: 'command.result',
120
+ commandId: taskCommand.commandId,
121
+ result: {
122
+ kind: 'agent.task',
123
+ taskId: taskCommand.taskId,
124
+ status: 'completed',
125
+ summary: 'Smoke task accepted.',
126
+ completedAt: new Date().toISOString()
127
+ }
128
+ });
129
+
130
+ const taskDevice = await waitFor(() => {
131
+ const current = hub.listDevices();
132
+ return current[0]?.latestTask?.taskId === taskCommand.taskId
133
+ && current[0]?.latestTask?.status === 'completed'
134
+ ? current[0]
135
+ : null;
136
+ });
137
+ assert.equal(taskDevice.latestTask.resultSummary, 'Smoke task accepted.');
138
+ assert.equal(taskDevice.counters.tasksQueued, 1);
139
+ assert.equal(taskDevice.counters.taskResultsReceived, 1);
140
+
141
+ socket.destroy();
142
+ await waitFor(() => hub.listDevices()[0]?.connected === false);
143
+ console.log('RemoteHub smoke OK');
144
+ } finally {
145
+ await hub.close();
146
+ }
package/server.js CHANGED
@@ -22,6 +22,7 @@ import chokidar from 'chokidar';
22
22
  import Parser from 'web-tree-sitter';
23
23
  import { fileURLToPath } from 'url';
24
24
  import { createCodexRuntime } from './codex-runtime.js';
25
+ import { createRemoteHub } from './remote-hub.js';
25
26
  import portGuard from './port-guard.cjs';
26
27
 
27
28
  const execAsync = promisify(exec);
@@ -1746,11 +1747,13 @@ const PROTECTED_BRIDGE_ROUTES = [
1746
1747
  { method: 'GET', prefix: '/api/codex/' },
1747
1748
  { method: 'POST', prefix: '/api/codex/' },
1748
1749
  { method: 'GET', prefix: '/api/shell/' },
1749
- { method: 'POST', prefix: '/api/shell/' },
1750
- { method: 'POST', prefix: '/project/' },
1751
- { method: 'POST', exact: '/agent/connect' },
1752
- { method: 'POST', exact: '/api/tool/trace' }
1753
- ];
1750
+ { method: 'POST', prefix: '/api/shell/' },
1751
+ { method: 'GET', prefix: '/api/remote/' },
1752
+ { method: 'POST', prefix: '/api/remote/' },
1753
+ { method: 'POST', prefix: '/project/' },
1754
+ { method: 'POST', exact: '/agent/connect' },
1755
+ { method: 'POST', exact: '/api/tool/trace' }
1756
+ ];
1754
1757
 
1755
1758
  function getBridgeTokenFromRequest(req) {
1756
1759
  const headerToken = String(req.get(bridgeTokenHeader) || '').trim();
@@ -2438,11 +2441,11 @@ function pushChangeFeedItem(item) {
2438
2441
  }
2439
2442
  }
2440
2443
 
2441
- function emitBridgeEvent(type, payload) {
2442
- if (wsClients.size === 0) return;
2443
- const message = JSON.stringify({
2444
- type,
2445
- timestamp: new Date().toISOString(),
2444
+ function emitBridgeEvent(type, payload) {
2445
+ if (wsClients.size === 0) return;
2446
+ const message = JSON.stringify({
2447
+ type,
2448
+ timestamp: new Date().toISOString(),
2446
2449
  payload
2447
2450
  });
2448
2451
  for (const client of wsClients) {
@@ -2452,11 +2455,18 @@ function emitBridgeEvent(type, payload) {
2452
2455
  } catch {
2453
2456
  // Ignore one-off client send errors
2454
2457
  }
2455
- }
2456
- }
2457
- }
2458
-
2459
- function trimShellOutput(value, maxLength) {
2458
+ }
2459
+ }
2460
+ }
2461
+
2462
+ const remoteHub = createRemoteHub({
2463
+ logEvent,
2464
+ logWarn,
2465
+ logError,
2466
+ emitEvent: emitBridgeEvent
2467
+ });
2468
+
2469
+ function trimShellOutput(value, maxLength) {
2460
2470
  const text = String(value || '');
2461
2471
  if (text.length <= maxLength) {
2462
2472
  return text;
@@ -6906,10 +6916,11 @@ app.get('/api/status', async (req, res) => {
6906
6916
  workspace: workspacePath,
6907
6917
  wsToken,
6908
6918
  wsPath: '/events',
6909
- bridgeToken,
6910
- bridgeTokenHeader,
6911
- bridgeAuthRequired,
6912
- shellJobsPath: '/api/shell/jobs',
6919
+ bridgeToken,
6920
+ bridgeTokenHeader,
6921
+ bridgeAuthRequired,
6922
+ remoteHub: remoteHub.getStatus({ includeSecrets: false }),
6923
+ shellJobsPath: '/api/shell/jobs',
6913
6924
  companyCore: {
6914
6925
  baseUrl: companyCoreBaseUrl,
6915
6926
  proxyPath: '/api/company-core',
@@ -6946,6 +6957,92 @@ app.get('/api/status', async (req, res) => {
6946
6957
  });
6947
6958
  });
6948
6959
 
6960
+ app.get('/api/remote/status', (req, res) => {
6961
+ res.setHeader('Cache-Control', 'no-store');
6962
+ res.json(remoteHub.getStatus({ includeSecrets: true }));
6963
+ });
6964
+
6965
+ app.get('/api/remote/devices', (req, res) => {
6966
+ res.setHeader('Cache-Control', 'no-store');
6967
+ const devices = remoteHub.listDevices();
6968
+ res.json({
6969
+ total: devices.length,
6970
+ pagination: 'none',
6971
+ canvasDeviceListMode: 'all-devices',
6972
+ devices
6973
+ });
6974
+ });
6975
+
6976
+ app.post('/api/remote/devices/:deviceId/disconnect', (req, res) => {
6977
+ const disconnected = remoteHub.disconnectDevice(req.params.deviceId, 'manager-request');
6978
+ res.json({ ok: disconnected });
6979
+ });
6980
+
6981
+ app.post('/api/remote/devices/:deviceId/ping', (req, res) => {
6982
+ res.json(remoteHub.sendCommand(req.params.deviceId, {
6983
+ command: 'ping',
6984
+ payload: {
6985
+ requestedAt: new Date().toISOString()
6986
+ }
6987
+ }));
6988
+ });
6989
+
6990
+ app.post('/api/remote/devices/:deviceId/tasks', (req, res) => {
6991
+ res.json(remoteHub.requestAgentTask(req.params.deviceId, {
6992
+ instruction: req.body?.instruction,
6993
+ title: req.body?.title,
6994
+ taskId: req.body?.taskId,
6995
+ commandId: req.body?.commandId
6996
+ }));
6997
+ });
6998
+
6999
+ app.post('/api/remote/tasks', (req, res) => {
7000
+ const requestedDeviceIds = Array.isArray(req.body?.deviceIds)
7001
+ ? req.body.deviceIds.map(value => String(value || '').trim()).filter(Boolean)
7002
+ : [];
7003
+ const allConnected = req.body?.allConnected !== false;
7004
+ const devices = remoteHub.listDevices();
7005
+ const targetIds = requestedDeviceIds.length > 0
7006
+ ? requestedDeviceIds
7007
+ : (allConnected ? devices.filter(device => device.connected).map(device => device.deviceId) : []);
7008
+ const uniqueTargetIds = [...new Set(targetIds)].slice(0, 500);
7009
+ const results = uniqueTargetIds.map(deviceId => ({
7010
+ deviceId,
7011
+ ...remoteHub.requestAgentTask(deviceId, {
7012
+ instruction: req.body?.instruction,
7013
+ title: req.body?.title
7014
+ })
7015
+ }));
7016
+ const queued = results.filter(result => result.ok === true).length;
7017
+ res.json({
7018
+ ok: queued > 0,
7019
+ total: uniqueTargetIds.length,
7020
+ queued,
7021
+ results,
7022
+ error: queued > 0 ? undefined : (uniqueTargetIds.length === 0 ? 'no-target-devices' : 'no-task-queued')
7023
+ });
7024
+ });
7025
+
7026
+ app.get('/api/remote/devices/:deviceId/thumbnail', (req, res) => {
7027
+ res.setHeader('Cache-Control', 'no-store');
7028
+ const thumbnail = remoteHub.getDeviceThumbnail(req.params.deviceId);
7029
+ if (!thumbnail) {
7030
+ res.status(404).json({ ok: false, error: 'thumbnail-not-available' });
7031
+ return;
7032
+ }
7033
+
7034
+ res.json({ ok: true, thumbnail });
7035
+ });
7036
+
7037
+ app.post('/api/remote/devices/:deviceId/thumbnail/request', (req, res) => {
7038
+ res.json(remoteHub.requestThumbnail(req.params.deviceId, {
7039
+ maxWidth: req.body?.maxWidth,
7040
+ maxHeight: req.body?.maxHeight,
7041
+ quality: req.body?.quality,
7042
+ streamId: req.body?.streamId
7043
+ }));
7044
+ });
7045
+
6949
7046
  app.get('/api/codex/capabilities', async (req, res) => {
6950
7047
  try {
6951
7048
  res.json(await getCodexRuntime().getCapabilities());
@@ -8347,6 +8444,12 @@ async function startBridgeServer() {
8347
8444
  process.exit(1);
8348
8445
  });
8349
8446
 
8447
+ try {
8448
+ await remoteHub.start();
8449
+ } catch (remoteHubError) {
8450
+ logError('remote', 'failed to start RemoteHub.', remoteHubError);
8451
+ }
8452
+
8350
8453
  httpServer.listen(PORT, '127.0.0.1', async () => {
8351
8454
  try {
8352
8455
  await ensureWorkspaceDataLayout();
@@ -8355,11 +8458,18 @@ async function startBridgeServer() {
8355
8458
  }
8356
8459
 
8357
8460
  const startupCodexRuntime = getCurrentCodexRuntime();
8461
+ const remoteHubStatus = remoteHub.getStatus({ includeSecrets: true });
8358
8462
  logSection('MindExec Local Bridge', [
8359
8463
  formatKeyValue('port', tone(PORT, 'accent')),
8360
8464
  formatKeyValue('workspace', tone(normalizePathForClient(workspacePath), 'path')),
8361
8465
  formatKeyValue('status', tone('ready', 'success')),
8362
8466
  formatKeyValue('exec', tone(formatCodexRuntime(startupCodexRuntime), 'accent')),
8467
+ formatKeyValue('remote', remoteHubStatus.started
8468
+ ? tone(`tcp://${remoteHubStatus.host}:${remoteHubStatus.port}`, 'accent')
8469
+ : tone(remoteHubStatus.enabled ? 'failed' : 'disabled', 'warn')),
8470
+ formatKeyValue('pair', remoteHubStatus.started
8471
+ ? tone(remoteHubStatus.pairTokenPreview, 'warn')
8472
+ : tone('-', 'muted')),
8363
8473
  tone('--------', 'muted'),
8364
8474
  WEB_APP_ROOT
8365
8475
  ? `${tone('app', 'muted')} ${tone(getLocalAppUrl(), 'path')}`
@@ -8396,15 +8506,21 @@ async function shutdownBridge(signal) {
8396
8506
  }
8397
8507
  }
8398
8508
 
8399
- try {
8400
- wss.close();
8401
- } catch {
8402
- // Ignore if already closed
8403
- }
8404
-
8405
- await new Promise((resolve) => {
8406
- try {
8407
- httpServer.close(() => resolve());
8509
+ try {
8510
+ wss.close();
8511
+ } catch {
8512
+ // Ignore if already closed
8513
+ }
8514
+
8515
+ try {
8516
+ await remoteHub.close();
8517
+ } catch {
8518
+ // Ignore RemoteHub close errors during shutdown
8519
+ }
8520
+
8521
+ await new Promise((resolve) => {
8522
+ try {
8523
+ httpServer.close(() => resolve());
8408
8524
  } catch {
8409
8525
  resolve();
8410
8526
  }
@@ -6272,6 +6272,16 @@ ${summaryLines.map(line => `<div>${escapeNodeFrameDebugHtml(line)}</div>`).join(
6272
6272
  if (!ready || !moduleInstance) return;
6273
6273
  await MindMapNodes.addNode(moduleInstance, m);
6274
6274
  },
6275
+ addRemoteFleetMonitor: async () => {
6276
+ const ready = await waitForMindMapReady();
6277
+ if (!ready || !moduleInstance?.dotNetHelper) {
6278
+ return { success: false, error: 'mind-map-not-ready' };
6279
+ }
6280
+
6281
+ const x = Number(moduleInstance.cursorPosition?.x ?? moduleInstance.camera?.position?.x ?? 0);
6282
+ const y = Number(moduleInstance.cursorPosition?.y ?? moduleInstance.camera?.position?.y ?? 0);
6283
+ return await moduleInstance.dotNetHelper.invokeMethodAsync('AddRemoteFleetMonitorNodeFromJs', x, y);
6284
+ },
6275
6285
  // ▼▼▼ [Perf] Expose nodeObjectsById for will-change optimization ▼▼▼
6276
6286
  getNodeObjectsById: () => moduleInstance?.nodeObjectsById || null,
6277
6287
  // ▲▲▲ [Perf] ▲▲▲
@@ -8032,6 +8042,7 @@ ${summaryLines.map(line => `<div>${escapeNodeFrameDebugHtml(line)}</div>`).join(
8032
8042
 
8033
8043
  // Backward-compatible alias: MindCanvas is the new page/internal name, but the underlying surface is the same.
8034
8044
  window.mindCanvas = window.mindMap;
8045
+ window.addRemoteFleetMonitorNode = () => window.mindMap?.addRemoteFleetMonitor?.();
8035
8046
  window.MindMapCoreBuildInfo = {
8036
8047
  build: MINDMAP_CORE_BUILD_ID,
8037
8048
  hasMovingUpdateDue: true,