@mindexec/cli 0.2.92 → 0.2.94

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 (3) hide show
  1. package/package.json +2 -2
  2. package/remote-hub.js +87 -7
  3. package/server.js +86 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindexec/cli",
3
- "version": "0.2.92",
3
+ "version": "0.2.94",
4
4
  "description": "MindExec local runtime and bridge CLI",
5
5
  "main": "server.js",
6
6
  "type": "module",
@@ -47,7 +47,7 @@
47
47
  "node": ">=20"
48
48
  },
49
49
  "dependencies": {
50
- "@mindexec/remote": "^0.1.14",
50
+ "@mindexec/remote": "^0.1.15",
51
51
  "@openai/codex-sdk": "^0.137.0",
52
52
  "chokidar": "^3.6.0",
53
53
  "cors": "^2.8.5",
package/remote-hub.js CHANGED
@@ -77,10 +77,72 @@ function isPrivateIPv4(value) {
77
77
  || (parts[0] === 192 && parts[1] === 168);
78
78
  }
79
79
 
80
- function getReachableIPv4Candidates() {
80
+ function isPreferredPhysicalInterfaceName(value) {
81
+ const name = String(value || '').toLowerCase();
82
+ return /(^|[\s_\-()])((wi[\s_\-]?fi)|wifi|wireless|wlan|ethernet|lan|이더넷|en\d+|eth\d+)(?=$|[\s_\-()])/i.test(name);
83
+ }
84
+
85
+ function isVirtualInterfaceName(value) {
86
+ const name = String(value || '').toLowerCase();
87
+ return /virtual|vethernet|hyper-v|hyperv|vmware|virtualbox|vbox|docker|wsl|container|bluetooth|loopback|npcap|isatap|teredo/i.test(name);
88
+ }
89
+
90
+ function getIPv4LastOctet(value) {
91
+ const parts = String(value || '').split('.').map(part => Number(part));
92
+ if (parts.length !== 4 || parts.some(part => !Number.isInteger(part))) {
93
+ return -1;
94
+ }
95
+
96
+ return parts[3];
97
+ }
98
+
99
+ function scoreReachableIPv4Candidate(candidate) {
100
+ const address = String(candidate?.address || '');
101
+ const name = String(candidate?.name || '');
102
+ let score = 0;
103
+
104
+ if (isPrivateIPv4(address)) {
105
+ score += 1000;
106
+ }
107
+
108
+ if (/^192\.168\./.test(address)) {
109
+ score += 90;
110
+ } else if (/^10\./.test(address)) {
111
+ score += 80;
112
+ } else if (/^172\.(1[6-9]|2\d|3[0-1])\./.test(address)) {
113
+ score += 70;
114
+ }
115
+
116
+ if (isPreferredPhysicalInterfaceName(name)) {
117
+ score += 500;
118
+ }
119
+
120
+ if (isVirtualInterfaceName(name)) {
121
+ score -= 1200;
122
+ }
123
+
124
+ if (/^169\.254\./.test(address)) {
125
+ score -= 2000;
126
+ }
127
+
128
+ const lastOctet = getIPv4LastOctet(address);
129
+ if (lastOctet === 1) {
130
+ score -= 60;
131
+ } else if (lastOctet === 0 || lastOctet === 255) {
132
+ score -= 100;
133
+ }
134
+
135
+ if (candidate?.mac && candidate.mac === '00:00:00:00:00:00') {
136
+ score -= 50;
137
+ }
138
+
139
+ return score;
140
+ }
141
+
142
+ function getReachableIPv4CandidateEntries() {
81
143
  const candidates = [];
82
144
  const interfaces = os.networkInterfaces();
83
- for (const entries of Object.values(interfaces)) {
145
+ for (const [name, entries] of Object.entries(interfaces)) {
84
146
  for (const entry of entries || []) {
85
147
  if (entry?.family !== 'IPv4' || entry.internal) {
86
148
  continue;
@@ -91,13 +153,28 @@ function getReachableIPv4Candidates() {
91
153
  continue;
92
154
  }
93
155
 
94
- candidates.push(address);
156
+ candidates.push({
157
+ address,
158
+ name: safeString(name, 128),
159
+ mac: safeString(entry.mac, 32),
160
+ score: 0,
161
+ virtual: isVirtualInterfaceName(name),
162
+ preferred: isPreferredPhysicalInterfaceName(name)
163
+ });
95
164
  }
96
165
  }
97
166
 
98
167
  return candidates
99
- .sort((left, right) => Number(isPrivateIPv4(right)) - Number(isPrivateIPv4(left)))
100
- .filter((address, index, all) => all.indexOf(address) === index);
168
+ .map(candidate => ({
169
+ ...candidate,
170
+ score: scoreReachableIPv4Candidate(candidate)
171
+ }))
172
+ .sort((left, right) => right.score - left.score || left.address.localeCompare(right.address))
173
+ .filter((candidate, index, all) => all.findIndex(item => item.address === candidate.address) === index);
174
+ }
175
+
176
+ function getReachableIPv4Candidates() {
177
+ return getReachableIPv4CandidateEntries().map(candidate => candidate.address);
101
178
  }
102
179
 
103
180
  function getReachableIPv4Address() {
@@ -409,7 +486,8 @@ export function createRemoteHub(options = {}) {
409
486
  }
410
487
 
411
488
  function getAgentEndpointRouteInfo() {
412
- const candidates = getReachableIPv4Candidates();
489
+ const candidateDetails = getReachableIPv4CandidateEntries();
490
+ const candidates = candidateDetails.map(candidate => candidate.address);
413
491
  const endpoint = getAgentEndpoint();
414
492
  const wildcardWithoutReachableAddress =
415
493
  !publicEndpoint
@@ -424,7 +502,8 @@ export function createRemoteHub(options = {}) {
424
502
  port: parsed?.port || 0,
425
503
  accountRouteReady: reason === 'ok',
426
504
  reason,
427
- candidates
505
+ candidates,
506
+ candidateDetails
428
507
  };
429
508
  }
430
509
 
@@ -574,6 +653,7 @@ export function createRemoteHub(options = {}) {
574
653
  agentEndpointAccountRouteReady: routeInfo.accountRouteReady,
575
654
  agentEndpointRouteReason: routeInfo.reason,
576
655
  agentEndpointCandidates: routeInfo.candidates,
656
+ agentEndpointCandidateDetails: routeInfo.candidateDetails,
577
657
  pairToken: includeSecrets ? pairToken : undefined,
578
658
  pairTokenPreview: maskToken(pairToken),
579
659
  deviceCount: devices.size,
package/server.js CHANGED
@@ -2732,6 +2732,7 @@ const remoteHub = createRemoteHub({
2732
2732
  });
2733
2733
 
2734
2734
  const REMOTE_AGENT_STDIO_TAIL_CHARS = 12000;
2735
+ const REMOTE_AGENT_EARLY_EXIT_MS = 1200;
2735
2736
  const REMOTE_AGENT_DEFAULT_ENGINE = 'auto';
2736
2737
  let remoteAgentState = createRemoteAgentIdleState();
2737
2738
 
@@ -2778,6 +2779,71 @@ function appendRemoteAgentOutput(stream, chunk) {
2778
2779
  remoteAgentState.updatedAt = new Date().toISOString();
2779
2780
  }
2780
2781
 
2782
+ function summarizeRemoteAgentOutput(value, maxLength = 700) {
2783
+ const text = String(value || '').replace(/\s+/g, ' ').trim();
2784
+ if (text.length <= maxLength) {
2785
+ return text;
2786
+ }
2787
+
2788
+ return `...${text.slice(text.length - maxLength + 3)}`;
2789
+ }
2790
+
2791
+ function formatRemoteAgentFailureSummary(agent = remoteAgentState) {
2792
+ const details = [];
2793
+ if (agent.lastError) {
2794
+ details.push(agent.lastError);
2795
+ } else if (agent.signal) {
2796
+ details.push(`signal:${agent.signal}`);
2797
+ } else if (Number.isFinite(agent.exitCode)) {
2798
+ details.push(`exit:${agent.exitCode}`);
2799
+ }
2800
+
2801
+ const stderr = summarizeRemoteAgentOutput(agent.stderrTail);
2802
+ if (stderr) {
2803
+ details.push(`stderr=${stderr}`);
2804
+ }
2805
+
2806
+ const stdout = summarizeRemoteAgentOutput(agent.stdoutTail);
2807
+ if (stdout) {
2808
+ details.push(`stdout=${stdout}`);
2809
+ }
2810
+
2811
+ return details.join(' | ') || 'remote-agent-failed';
2812
+ }
2813
+
2814
+ function waitForRemoteAgentEarlyExit(child, settleMs = REMOTE_AGENT_EARLY_EXIT_MS) {
2815
+ return new Promise(resolve => {
2816
+ if (!child || child.exitCode !== null || child.killed) {
2817
+ resolve(true);
2818
+ return;
2819
+ }
2820
+
2821
+ let settled = false;
2822
+ let timer = null;
2823
+ const cleanup = () => {
2824
+ if (timer) {
2825
+ clearTimeout(timer);
2826
+ }
2827
+ child.off('exit', onExit);
2828
+ child.off('error', onError);
2829
+ };
2830
+ const finish = value => {
2831
+ if (settled) {
2832
+ return;
2833
+ }
2834
+ settled = true;
2835
+ cleanup();
2836
+ resolve(value);
2837
+ };
2838
+ const onExit = () => finish(true);
2839
+ const onError = () => finish(true);
2840
+
2841
+ child.once('exit', onExit);
2842
+ child.once('error', onError);
2843
+ timer = setTimeout(() => finish(false), settleMs);
2844
+ });
2845
+ }
2846
+
2781
2847
  function normalizeRemoteAgentEngine(value) {
2782
2848
  const engine = String(value || REMOTE_AGENT_DEFAULT_ENGINE).trim().toLowerCase();
2783
2849
  if (engine === 'fast' || engine === 'csharp' || engine === 'c#') {
@@ -2888,10 +2954,12 @@ async function startRemoteAgentConnection(options = {}) {
2888
2954
  const engine = normalizeRemoteAgentEngine(options.engine);
2889
2955
 
2890
2956
  if (!manager) {
2957
+ logWarn('remote', 'managed RemoteAgent connect rejected: invalid manager endpoint');
2891
2958
  return { ok: false, error: 'invalid-manager-endpoint', agent: serializeRemoteAgentState() };
2892
2959
  }
2893
2960
 
2894
2961
  if (!pairToken) {
2962
+ logWarn('remote', `managed RemoteAgent connect rejected: missing pair token ${formatKeyValue('manager', manager)}`);
2895
2963
  return { ok: false, error: 'missing-pair-token', agent: serializeRemoteAgentState() };
2896
2964
  }
2897
2965
 
@@ -2961,6 +3029,7 @@ async function startRemoteAgentConnection(options = {}) {
2961
3029
  remoteAgentState.status = 'failed';
2962
3030
  remoteAgentState.lastError = err?.message || String(err);
2963
3031
  remoteAgentState.updatedAt = new Date().toISOString();
3032
+ logWarn('remote', `managed RemoteAgent failed: ${formatRemoteAgentFailureSummary()}`);
2964
3033
  emitBridgeEvent('RemoteAgentFailed', serializeRemoteAgentState());
2965
3034
  });
2966
3035
  child.once('exit', (code, signal) => {
@@ -2972,6 +3041,11 @@ async function startRemoteAgentConnection(options = {}) {
2972
3041
  if (code !== 0 && !remoteAgentState.lastError) {
2973
3042
  remoteAgentState.lastError = signal ? `signal:${signal}` : `exit:${code}`;
2974
3043
  }
3044
+ if (remoteAgentState.status === 'failed') {
3045
+ logWarn('remote', `managed RemoteAgent failed: ${formatRemoteAgentFailureSummary()}`);
3046
+ } else {
3047
+ logEvent('remote', `managed RemoteAgent exited ${formatKeyValue('code', code ?? 0)}`, 'remote');
3048
+ }
2975
3049
  emitBridgeEvent(remoteAgentState.status === 'exited' ? 'RemoteAgentExited' : 'RemoteAgentFailed', serializeRemoteAgentState());
2976
3050
  });
2977
3051
 
@@ -2981,6 +3055,13 @@ async function startRemoteAgentConnection(options = {}) {
2981
3055
  'remote');
2982
3056
  emitBridgeEvent('RemoteAgentStarted', serializeRemoteAgentState());
2983
3057
 
3058
+ const exitedEarly = await waitForRemoteAgentEarlyExit(child);
3059
+ if (exitedEarly) {
3060
+ const error = formatRemoteAgentFailureSummary();
3061
+ logWarn('remote', `managed RemoteAgent exited during startup ${formatKeyValue('manager', manager)} ${formatKeyValue('error', error)}`);
3062
+ return { ok: false, error, agent: serializeRemoteAgentState() };
3063
+ }
3064
+
2984
3065
  return { ok: true, alreadyRunning: false, agent: serializeRemoteAgentState() };
2985
3066
  }
2986
3067
 
@@ -7537,14 +7618,18 @@ app.get('/api/remote/agent/status', (req, res) => {
7537
7618
  app.post('/api/remote/agent/connect', async (req, res) => {
7538
7619
  res.setHeader('Cache-Control', 'no-store');
7539
7620
  try {
7621
+ const requestedManager = normalizeRemoteManagerEndpoint(req.body?.manager || req.body?.managerEndpoint);
7540
7622
  const result = await startRemoteAgentConnection({
7541
- manager: req.body?.manager || req.body?.managerEndpoint,
7623
+ manager: requestedManager,
7542
7624
  pairToken: req.body?.pairToken || req.body?.pair,
7543
7625
  leaseId: req.body?.leaseId,
7544
7626
  nodeId: req.body?.nodeId,
7545
7627
  engine: req.body?.engine,
7546
7628
  source: req.body?.source || 'registry'
7547
7629
  });
7630
+ if (!result.ok) {
7631
+ logWarn('remote', `managed RemoteAgent connect failed ${formatKeyValue('manager', requestedManager || 'invalid')} ${formatKeyValue('error', result.error || 'unknown')}`);
7632
+ }
7548
7633
  res.status(result.ok ? 200 : 400).json(result);
7549
7634
  } catch (err) {
7550
7635
  logError('remote', 'managed RemoteAgent connect failed.', err);