@mindexec/cli 0.2.1 → 0.2.3

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 (26) hide show
  1. package/README.md +38 -0
  2. package/package.json +6 -4
  3. package/remote-hub.js +571 -0
  4. package/scripts/remote-hub-smoke.mjs +117 -0
  5. package/server.js +108 -28
  6. package/wwwroot/_content/MindExecution.Shared/css/mind-map-overrides.css +11 -0
  7. package/wwwroot/_content/MindExecution.Shared/js/mind-map-core.js +76 -1
  8. package/wwwroot/_content/MindExecution.Shared/js/mind-map-css3d-manager.js +629 -5
  9. package/wwwroot/_content/MindExecution.Shared/js/mind-map-lod-renderer.js +16 -0
  10. package/wwwroot/_content/MindExecution.Shared/js/mind-map-nodes.js +16 -5
  11. package/wwwroot/_framework/MindExecution.Core.5luow1xgjs.dll +0 -0
  12. package/wwwroot/_framework/{MindExecution.Kernel.gwwc40sc45.dll → MindExecution.Kernel.mot9nj6bzm.dll} +0 -0
  13. package/wwwroot/_framework/{MindExecution.Plugins.Admin.0jgrn1sckv.dll → MindExecution.Plugins.Admin.x9v2drg2f7.dll} +0 -0
  14. package/wwwroot/_framework/{MindExecution.Plugins.Business.13mme2qcag.dll → MindExecution.Plugins.Business.b0kjoyx31x.dll} +0 -0
  15. package/wwwroot/_framework/{MindExecution.Plugins.Concept.dfp2mdt45q.dll → MindExecution.Plugins.Concept.6tojojgh1a.dll} +0 -0
  16. package/wwwroot/_framework/{MindExecution.Plugins.Directory.3w4t6n3se0.dll → MindExecution.Plugins.Directory.fqtbuqadsx.dll} +0 -0
  17. package/wwwroot/_framework/{MindExecution.Plugins.PlanMaster.s0qpntz420.dll → MindExecution.Plugins.PlanMaster.j7llfeae6l.dll} +0 -0
  18. package/wwwroot/_framework/{MindExecution.Plugins.YouTube.iu11fq8d16.dll → MindExecution.Plugins.YouTube.yo5fwdhugr.dll} +0 -0
  19. package/wwwroot/_framework/{MindExecution.Shared.7j27dcqnrc.dll → MindExecution.Shared.0qi7vbn9a4.dll} +0 -0
  20. package/wwwroot/_framework/MindExecution.Web.6cv7ad7rik.dll +0 -0
  21. package/wwwroot/_framework/blazor.boot.json +21 -21
  22. package/wwwroot/index.html +3 -3
  23. package/wwwroot/service-worker-assets.js +28 -28
  24. package/wwwroot/service-worker.js +1 -1
  25. package/wwwroot/_framework/MindExecution.Core.1q1trifbuu.dll +0 -0
  26. package/wwwroot/_framework/MindExecution.Web.pq1ty8ov2v.dll +0 -0
@@ -0,0 +1,117 @@
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: false
65
+ }
66
+ });
67
+ writeJsonLine(socket, {
68
+ type: 'status',
69
+ status: {
70
+ uptimeSec: 1,
71
+ totalMem: 2,
72
+ freeMem: 1
73
+ }
74
+ });
75
+
76
+ const devices = await waitFor(() => {
77
+ const current = hub.listDevices();
78
+ return current.length === 1 && current[0].status?.uptimeSec === 1 ? current : null;
79
+ });
80
+
81
+ assert.equal(devices[0].deviceId, 'smoke-device');
82
+ assert.equal(devices[0].connected, true);
83
+ assert.equal(hub.getStatus().deviceCount, 1);
84
+
85
+ const thumbnailCommand = hub.requestThumbnail('smoke-device', {
86
+ streamId: 'smoke-thumb',
87
+ maxWidth: 320,
88
+ maxHeight: 180,
89
+ quality: 50
90
+ });
91
+ assert.equal(thumbnailCommand.ok, true);
92
+
93
+ writeJsonLine(socket, {
94
+ type: 'thumbnail.frame',
95
+ commandId: thumbnailCommand.commandId,
96
+ streamId: 'smoke-thumb',
97
+ frameSeq: 1,
98
+ width: 2,
99
+ height: 1,
100
+ mimeType: 'image/png',
101
+ capturedAt: new Date().toISOString(),
102
+ data: 'iVBORw0KGgoAAAANSUhEUgAAAAIAAAABCAYAAAD0In+KAAAADElEQVR42mP8z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC'
103
+ });
104
+
105
+ const thumbnailDevice = await waitFor(() => {
106
+ const current = hub.listDevices();
107
+ return current[0]?.latestThumbnail?.frameSeq === 1 ? current[0] : null;
108
+ });
109
+ assert.equal(thumbnailDevice.latestThumbnail.streamId, 'smoke-thumb');
110
+ assert.equal(thumbnailDevice.counters.thumbnailFramesReceived, 1);
111
+
112
+ socket.destroy();
113
+ await waitFor(() => hub.listDevices()[0]?.connected === false);
114
+ console.log('RemoteHub smoke OK');
115
+ } finally {
116
+ await hub.close();
117
+ }
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,56 @@ 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.get('/api/remote/devices/:deviceId/thumbnail', (req, res) => {
6991
+ res.setHeader('Cache-Control', 'no-store');
6992
+ const thumbnail = remoteHub.getDeviceThumbnail(req.params.deviceId);
6993
+ if (!thumbnail) {
6994
+ res.status(404).json({ ok: false, error: 'thumbnail-not-available' });
6995
+ return;
6996
+ }
6997
+
6998
+ res.json({ ok: true, thumbnail });
6999
+ });
7000
+
7001
+ app.post('/api/remote/devices/:deviceId/thumbnail/request', (req, res) => {
7002
+ res.json(remoteHub.requestThumbnail(req.params.deviceId, {
7003
+ maxWidth: req.body?.maxWidth,
7004
+ maxHeight: req.body?.maxHeight,
7005
+ quality: req.body?.quality,
7006
+ streamId: req.body?.streamId
7007
+ }));
7008
+ });
7009
+
6949
7010
  app.get('/api/codex/capabilities', async (req, res) => {
6950
7011
  try {
6951
7012
  res.json(await getCodexRuntime().getCapabilities());
@@ -8347,6 +8408,12 @@ async function startBridgeServer() {
8347
8408
  process.exit(1);
8348
8409
  });
8349
8410
 
8411
+ try {
8412
+ await remoteHub.start();
8413
+ } catch (remoteHubError) {
8414
+ logError('remote', 'failed to start RemoteHub.', remoteHubError);
8415
+ }
8416
+
8350
8417
  httpServer.listen(PORT, '127.0.0.1', async () => {
8351
8418
  try {
8352
8419
  await ensureWorkspaceDataLayout();
@@ -8355,11 +8422,18 @@ async function startBridgeServer() {
8355
8422
  }
8356
8423
 
8357
8424
  const startupCodexRuntime = getCurrentCodexRuntime();
8425
+ const remoteHubStatus = remoteHub.getStatus({ includeSecrets: true });
8358
8426
  logSection('MindExec Local Bridge', [
8359
8427
  formatKeyValue('port', tone(PORT, 'accent')),
8360
8428
  formatKeyValue('workspace', tone(normalizePathForClient(workspacePath), 'path')),
8361
8429
  formatKeyValue('status', tone('ready', 'success')),
8362
8430
  formatKeyValue('exec', tone(formatCodexRuntime(startupCodexRuntime), 'accent')),
8431
+ formatKeyValue('remote', remoteHubStatus.started
8432
+ ? tone(`tcp://${remoteHubStatus.host}:${remoteHubStatus.port}`, 'accent')
8433
+ : tone(remoteHubStatus.enabled ? 'failed' : 'disabled', 'warn')),
8434
+ formatKeyValue('pair', remoteHubStatus.started
8435
+ ? tone(remoteHubStatus.pairTokenPreview, 'warn')
8436
+ : tone('-', 'muted')),
8363
8437
  tone('--------', 'muted'),
8364
8438
  WEB_APP_ROOT
8365
8439
  ? `${tone('app', 'muted')} ${tone(getLocalAppUrl(), 'path')}`
@@ -8396,15 +8470,21 @@ async function shutdownBridge(signal) {
8396
8470
  }
8397
8471
  }
8398
8472
 
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());
8473
+ try {
8474
+ wss.close();
8475
+ } catch {
8476
+ // Ignore if already closed
8477
+ }
8478
+
8479
+ try {
8480
+ await remoteHub.close();
8481
+ } catch {
8482
+ // Ignore RemoteHub close errors during shutdown
8483
+ }
8484
+
8485
+ await new Promise((resolve) => {
8486
+ try {
8487
+ httpServer.close(() => resolve());
8408
8488
  } catch {
8409
8489
  resolve();
8410
8490
  }
@@ -36,6 +36,17 @@
36
36
  contain-intrinsic-size: auto !important;
37
37
  }
38
38
 
39
+ .css3d-resolution-wrapper.node-type-templatelauncher,
40
+ .css3d-resolution-wrapper.node-type-templatelauncher > .map-node,
41
+ .css3d-resolution-wrapper.node-type-templatelauncher > .map-node-template-card {
42
+ contain: layout style !important;
43
+ content-visibility: visible !important;
44
+ contain-intrinsic-size: auto !important;
45
+ isolation: isolate !important;
46
+ -webkit-backface-visibility: visible !important;
47
+ backface-visibility: visible !important;
48
+ }
49
+
39
50
  .css3d-resolution-wrapper.node-type-image > .map-node,
40
51
  .css3d-resolution-wrapper.node-type-video > .map-node,
41
52
  .css3d-resolution-wrapper.node-type-embed > .map-node {
@@ -5,7 +5,7 @@
5
5
  const DEBUG = false;
6
6
  const FPS_DEBUG = false;
7
7
  const FRAME_PERF_DEBUG = false;
8
- const MINDMAP_CORE_BUILD_ID = '20260609-midfar-idle-resident-wake-v204';
8
+ const MINDMAP_CORE_BUILD_ID = '20260610-template-board-ready-v205';
9
9
  const CanvasPhase = Object.freeze({
10
10
  Booting: 'booting',
11
11
  BoardFileLoading: 'board-file-loading',
@@ -170,6 +170,52 @@
170
170
  let moduleInstance = null;
171
171
  let pendingAuthToken = null;
172
172
  let pendingSettings = null;
173
+ let _lastCompletedBoardLoadId = null;
174
+ let _lastCompletedBoardLoadAt = 0;
175
+
176
+ function normalizeBoardIdValue(boardId) {
177
+ return String(boardId ?? '').trim().toLowerCase();
178
+ }
179
+
180
+ function invalidateCurrentBoardLoadCompletion(reason = 'unknown') {
181
+ _lastCompletedBoardLoadId = null;
182
+ _lastCompletedBoardLoadAt = 0;
183
+ emitMindCanvasTrace('board.load.pending', {
184
+ boardId: _currentBoardId,
185
+ reason
186
+ });
187
+ }
188
+
189
+ function isCurrentBoardLoadComplete() {
190
+ const activeBoardId = normalizeBoardIdValue(_currentBoardId);
191
+ return !!activeBoardId && activeBoardId === _lastCompletedBoardLoadId;
192
+ }
193
+
194
+ function markBoardLoadComplete(boardId, reason = 'unknown') {
195
+ const expectedBoardId = normalizeBoardIdValue(boardId);
196
+ const activeBoardId = normalizeBoardIdValue(_currentBoardId);
197
+ if (!expectedBoardId || expectedBoardId !== activeBoardId) {
198
+ emitMindCanvasTrace('board.staleMutationBlocked', {
199
+ mutation: 'markBoardLoadComplete',
200
+ expectedBoardId,
201
+ activeBoardId,
202
+ reason
203
+ });
204
+ return false;
205
+ }
206
+
207
+ _lastCompletedBoardLoadId = expectedBoardId;
208
+ _lastCompletedBoardLoadAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
209
+ emitMindCanvasTrace('board.load.complete', {
210
+ boardId: _currentBoardId,
211
+ reason,
212
+ nodeCount: moduleInstance?.nodeObjectsById?.size || 0,
213
+ isLoading: moduleInstance?.isLoading === true,
214
+ phase: moduleInstance?.canvasPhase || null
215
+ });
216
+ return true;
217
+ }
218
+
173
219
  function isDocumentHidden() {
174
220
  return typeof document !== 'undefined' && document.visibilityState === 'hidden';
175
221
  }
@@ -1997,7 +2043,11 @@ ${summaryLines.map(line => `<div>${escapeNodeFrameDebugHtml(line)}</div>`).join(
1997
2043
  let _currentBoardId = null;
1998
2044
 
1999
2045
  function setActiveBoardIdRuntime(boardId, reason = 'unknown') {
2046
+ const previousBoardId = _currentBoardId;
2000
2047
  _currentBoardId = boardId || null;
2048
+ if (normalizeBoardIdValue(previousBoardId) !== normalizeBoardIdValue(_currentBoardId)) {
2049
+ invalidateCurrentBoardLoadCompletion(reason);
2050
+ }
2001
2051
  window.MindCanvasBuildInfo?.setRuntime?.('activeBoardId', _currentBoardId);
2002
2052
  emitMindCanvasTrace('board.active', {
2003
2053
  boardId: _currentBoardId,
@@ -6222,6 +6272,16 @@ ${summaryLines.map(line => `<div>${escapeNodeFrameDebugHtml(line)}</div>`).join(
6222
6272
  if (!ready || !moduleInstance) return;
6223
6273
  await MindMapNodes.addNode(moduleInstance, m);
6224
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
+ },
6225
6285
  // ▼▼▼ [Perf] Expose nodeObjectsById for will-change optimization ▼▼▼
6226
6286
  getNodeObjectsById: () => moduleInstance?.nodeObjectsById || null,
6227
6287
  // ▲▲▲ [Perf] ▲▲▲
@@ -7258,6 +7318,7 @@ ${summaryLines.map(line => `<div>${escapeNodeFrameDebugHtml(line)}</div>`).join(
7258
7318
  if (boardId) {
7259
7319
  setActiveBoardIdRuntime(boardId, 'resetBoard');
7260
7320
  }
7321
+ invalidateCurrentBoardLoadCompletion('resetBoard');
7261
7322
  // ▲▲▲ [Fix] ▲▲▲
7262
7323
  const resetPhase = moduleInstance.canvasPhase === CanvasPhase.Booting
7263
7324
  ? CanvasPhase.BoardFileLoading
@@ -7369,9 +7430,22 @@ ${summaryLines.map(line => `<div>${escapeNodeFrameDebugHtml(line)}</div>`).join(
7369
7430
  // ▼▼▼ [Fix] Expose activeBoardId for async guard in addNode ▼▼▼
7370
7431
  get activeBoardId() { return _currentBoardId; },
7371
7432
  setActiveBoardId: (boardId) => { setActiveBoardIdRuntime(boardId, 'external-setActiveBoardId'); },
7433
+ markBoardLoadCompleteForBoard: (boardId) => markBoardLoadComplete(boardId, 'csharp-board-load-complete'),
7434
+ getActiveBoardRuntimeState: () => ({
7435
+ activeBoardId: _currentBoardId,
7436
+ activeBoardIdNormalized: normalizeBoardIdValue(_currentBoardId),
7437
+ isLoading: moduleInstance?.isLoading === true,
7438
+ canvasPhase: moduleInstance?.canvasPhase || null,
7439
+ nodeCount: moduleInstance?.nodeObjectsById?.size || 0,
7440
+ boardLoadComplete: isCurrentBoardLoadComplete(),
7441
+ boardLoadCompletedAt: _lastCompletedBoardLoadAt || 0
7442
+ }),
7372
7443
  getRuntimeDiagnostics: () => ({
7373
7444
  activeBoardId: _currentBoardId,
7374
7445
  canvasPhase: moduleInstance?.canvasPhase || null,
7446
+ isLoading: moduleInstance?.isLoading === true,
7447
+ boardLoadComplete: isCurrentBoardLoadComplete(),
7448
+ nodeCount: moduleInstance?.nodeObjectsById?.size || 0,
7375
7449
  buildInfo: window.MindCanvasBuildInfo?.dump?.() || null,
7376
7450
  guardCounts: window.MindCanvasDevGuards?.getCounts?.() || {},
7377
7451
  trace: {
@@ -7968,6 +8042,7 @@ ${summaryLines.map(line => `<div>${escapeNodeFrameDebugHtml(line)}</div>`).join(
7968
8042
 
7969
8043
  // Backward-compatible alias: MindCanvas is the new page/internal name, but the underlying surface is the same.
7970
8044
  window.mindCanvas = window.mindMap;
8045
+ window.addRemoteFleetMonitorNode = () => window.mindMap?.addRemoteFleetMonitor?.();
7971
8046
  window.MindMapCoreBuildInfo = {
7972
8047
  build: MINDMAP_CORE_BUILD_ID,
7973
8048
  hasMovingUpdateDue: true,