ninja-terminals 2.4.4 → 2.4.6

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
@@ -225,6 +225,14 @@ process.env.HTTP_PORT = String(port);
225
225
  process.env.DEFAULT_TERMINALS = String(terminals);
226
226
  process.env.DEFAULT_CWD = cwd;
227
227
 
228
+ // Single browser-open path: server.js opens the tab in its listen callback
229
+ // (knows the actual selected port, uses 127.0.0.1). Enable it by default for
230
+ // CLI launches; opt out with --no-open. This replaces cli.js's own opener,
231
+ // which previously fired a second, redundant tab.
232
+ if (!hasFlag('--no-open') && !process.env.NINJA_OPEN_BROWSER) {
233
+ process.env.NINJA_OPEN_BROWSER = '1';
234
+ }
235
+
228
236
  // Auth env vars
229
237
  if (token) {
230
238
  process.env.NINJA_AUTH_TOKEN = token;
@@ -233,33 +241,9 @@ if (offline) {
233
241
  process.env.NINJA_OFFLINE = '1';
234
242
  }
235
243
 
236
- // ── Auto-open browser ────────────────────────────────────────
237
-
238
- function openBrowser(url) {
239
- const { spawn } = require('child_process');
240
- const platform = process.platform;
241
- let cmd, cmdArgs;
242
-
243
- if (platform === 'darwin') {
244
- cmd = 'open';
245
- cmdArgs = [url];
246
- } else if (platform === 'win32') {
247
- cmd = 'cmd';
248
- cmdArgs = ['/c', 'start', url];
249
- } else {
250
- cmd = 'xdg-open';
251
- cmdArgs = [url];
252
- }
253
-
254
- spawn(cmd, cmdArgs, { stdio: 'ignore', detached: true }).unref();
255
- }
256
-
257
- // Delay browser open until after the server listen callback fires.
258
- // server.js calls server.listen() synchronously on require, so we
259
- // schedule the open after the current tick stack clears.
260
- setTimeout(() => {
261
- openBrowser(`http://localhost:${port}`);
262
- }, 1500);
244
+ // Browser auto-open is handled by server.js's listen callback (single path,
245
+ // gated by NINJA_OPEN_BROWSER which we set above). cli.js no longer opens its
246
+ // own tab — that produced a duplicate.
263
247
 
264
248
  // ── Start the server ─────────────────────────────────────────
265
249
 
@@ -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.6",
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;