ninja-terminals 2.4.4 → 2.4.5

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.
package/cli.js CHANGED
@@ -256,9 +256,17 @@ function openBrowser(url) {
256
256
 
257
257
  // Delay browser open until after the server listen callback fires.
258
258
  // server.js calls server.listen() synchronously on require, so we
259
- // schedule the open after the current tick stack clears.
259
+ // schedule the open after the current tick stack clears. Read the actual
260
+ // selected port from the session file — the server may have rolled to a
261
+ // different port if the preferred one was occupied (on either IP stack).
260
262
  setTimeout(() => {
261
- openBrowser(`http://localhost:${port}`);
263
+ let openPort = port;
264
+ try {
265
+ const sessionPath = require('path').join(require('os').homedir(), '.ninja', 'session.json');
266
+ const s = JSON.parse(require('fs').readFileSync(sessionPath, 'utf8'));
267
+ if (s && s.port) openPort = s.port;
268
+ } catch { /* fall back to requested port */ }
269
+ openBrowser(`http://127.0.0.1:${openPort}`);
262
270
  }, 1500);
263
271
 
264
272
  // ── Start the server ─────────────────────────────────────────
@@ -18,17 +18,29 @@ function ensureSessionDir() {
18
18
  fs.mkdirSync(SESSION_DIR, { recursive: true, mode: 0o700 });
19
19
  }
20
20
 
21
- function isPortAvailable(port, host = '127.0.0.1') {
21
+ // Probe a single host: 'free' (bindable), 'inuse' (EADDRINUSE), or
22
+ // 'unavailable' (stack absent / other error — don't treat as a collision).
23
+ function probePort(port, host) {
22
24
  return new Promise((resolve) => {
23
25
  const server = net.createServer();
24
- server.once('error', () => resolve(false));
25
- server.once('listening', () => {
26
- server.close(() => resolve(true));
27
- });
26
+ server.once('error', (err) => resolve(err.code === 'EADDRINUSE' ? 'inuse' : 'unavailable'));
27
+ server.once('listening', () => server.close(() => resolve('free')));
28
28
  server.listen(port, host);
29
29
  });
30
30
  }
31
31
 
32
+ // A port is available only if it's NOT in use on EITHER IP stack. A listener
33
+ // on IPv6 wildcard (e.g. *:3300, common with other local MCP servers) leaves
34
+ // IPv4 127.0.0.1 genuinely free, so an IPv4-only check would miss it and the
35
+ // browser (which resolves localhost -> IPv6 first) would hit the wrong server.
36
+ async function isPortAvailable(port) {
37
+ const [v4, v6] = await Promise.all([
38
+ probePort(port, '127.0.0.1'),
39
+ probePort(port, '::1'),
40
+ ]);
41
+ return v4 !== 'inuse' && v6 !== 'inuse';
42
+ }
43
+
32
44
  async function findAvailablePort(preferredPort, host = '127.0.0.1', maxAttempts = 50) {
33
45
  const start = Number.parseInt(preferredPort, 10);
34
46
  if (!Number.isInteger(start) || start < 1 || start > 65535) {
@@ -36,7 +48,7 @@ async function findAvailablePort(preferredPort, host = '127.0.0.1', maxAttempts
36
48
  }
37
49
 
38
50
  for (let port = start; port < start + maxAttempts && port <= 65535; port++) {
39
- if (await isPortAvailable(port, host)) return port;
51
+ if (await isPortAvailable(port)) return port;
40
52
  }
41
53
 
42
54
  throw new Error(`No available port found from ${start} to ${Math.min(start + maxAttempts - 1, 65535)}`);
@@ -47,7 +59,7 @@ function writeRuntimeSession(session) {
47
59
  const payload = {
48
60
  version: 1,
49
61
  pid: process.pid,
50
- host: 'localhost',
62
+ host: '127.0.0.1',
51
63
  createdAt: new Date().toISOString(),
52
64
  ...session,
53
65
  };
@@ -90,7 +102,7 @@ function readAuthToken() {
90
102
  }
91
103
  }
92
104
 
93
- function requestJson({ host = 'localhost', port, path: reqPath, token, method = 'GET', body = null, timeoutMs = 3000 }) {
105
+ function requestJson({ host = '127.0.0.1', port, path: reqPath, token, method = 'GET', body = null, timeoutMs = 3000 }) {
94
106
  return new Promise((resolve, reject) => {
95
107
  const payload = body ? JSON.stringify(body) : null;
96
108
  const headers = {};
@@ -134,7 +146,7 @@ async function healthCheckSession(session, timeoutMs = 3000) {
134
146
  if (!session || !session.port) return { ok: false, error: 'No runtime session port' };
135
147
  try {
136
148
  const res = await requestJson({
137
- host: session.host || 'localhost',
149
+ host: session.host || '127.0.0.1',
138
150
  port: session.port,
139
151
  path: '/health',
140
152
  timeoutMs,
package/mcp-server.js CHANGED
@@ -668,8 +668,8 @@ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
668
668
  label: terminal.label,
669
669
  status: terminal.status,
670
670
  cwd: terminal.cwd,
671
- webUrl: `http://localhost:${HTTP_PORT}`,
672
- wsUrl: `ws://localhost:${HTTP_PORT}/ws/${terminal.id}`,
671
+ webUrl: `http://127.0.0.1:${HTTP_PORT}`,
672
+ wsUrl: `ws://127.0.0.1:${HTTP_PORT}/ws/${terminal.id}`,
673
673
  }, null, 2),
674
674
  }],
675
675
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ninja-terminals",
3
- "version": "2.4.4",
3
+ "version": "2.4.5",
4
4
  "description": "MCP server for multi-terminal Claude Code orchestration with DAG task management, parallel execution, and self-improvement",
5
5
  "main": "server.js",
6
6
  "bin": {
package/server.js CHANGED
@@ -1311,7 +1311,7 @@ async function startServer() {
1311
1311
  }
1312
1312
 
1313
1313
  server.listen(selectedPort, BIND_HOST, () => {
1314
- const url = `http://localhost:${selectedPort}`;
1314
+ const url = `http://127.0.0.1:${selectedPort}`;
1315
1315
  console.log(`[bind] Listening on ${BIND_HOST}:${selectedPort}`);
1316
1316
  const fleetConfig = FLEET_MODES[FLEET_MODE] || FLEET_MODES.claude;
1317
1317
  const terminalCount = DEFAULT_TERMINALS > 0 ? Math.min(DEFAULT_TERMINALS, fleetConfig.length) : fleetConfig.length;