ninja-terminals 2.4.3 → 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 +32 -14
- package/lib/runtime-session.js +21 -9
- package/mcp-server.js +2 -2
- package/package.json +1 -1
- package/server.js +1 -1
package/cli.js
CHANGED
|
@@ -92,7 +92,10 @@ if (hasFlag('--setup')) {
|
|
|
92
92
|
fs.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2) + '\n');
|
|
93
93
|
console.log(`✅ Added ninja-terminals to ${mcpPath}`);
|
|
94
94
|
|
|
95
|
-
// 3. Copy orchestrator prompt to CLAUDE.md
|
|
95
|
+
// 3. Copy orchestrator prompt to CLAUDE.md in the current directory.
|
|
96
|
+
// Wrapped in try/catch: running from a protected dir (e.g. an Admin shell
|
|
97
|
+
// sitting in C:\Windows\System32) makes this write fail with EPERM. Warn
|
|
98
|
+
// and guide instead of crashing — .mcp.json is already configured.
|
|
96
99
|
const claudeMd = path.join(process.cwd(), 'CLAUDE.md');
|
|
97
100
|
const orchestratorPrompt = path.join(npmRoot, 'ORCHESTRATOR-PROMPT.md');
|
|
98
101
|
|
|
@@ -100,19 +103,26 @@ if (hasFlag('--setup')) {
|
|
|
100
103
|
const prompt = fs.readFileSync(orchestratorPrompt, 'utf-8');
|
|
101
104
|
const marker = '<!-- NINJA TERMINALS ORCHESTRATOR -->';
|
|
102
105
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
try {
|
|
107
|
+
let claudeContent = '';
|
|
108
|
+
if (fs.existsSync(claudeMd)) {
|
|
109
|
+
claudeContent = fs.readFileSync(claudeMd, 'utf-8');
|
|
110
|
+
if (claudeContent.includes(marker)) {
|
|
111
|
+
console.log(`✅ Orchestrator prompt already in CLAUDE.md`);
|
|
112
|
+
} else {
|
|
113
|
+
fs.writeFileSync(claudeMd, claudeContent + `\n\n${marker}\n${prompt}`);
|
|
114
|
+
console.log(`✅ Added orchestrator prompt to ${claudeMd}`);
|
|
115
|
+
}
|
|
108
116
|
} else {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
console.log(`✅ Added orchestrator prompt to CLAUDE.md`);
|
|
117
|
+
fs.writeFileSync(claudeMd, `${marker}\n${prompt}`);
|
|
118
|
+
console.log(`✅ Created ${claudeMd}`);
|
|
112
119
|
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
console.log(
|
|
120
|
+
} catch (e) {
|
|
121
|
+
console.log(`⚠️ Couldn't write CLAUDE.md in ${process.cwd()} (${e.code || e.message}).`);
|
|
122
|
+
console.log(` This usually means you're in a protected folder (e.g. C:\\Windows\\System32).`);
|
|
123
|
+
console.log(` Re-run setup from your project folder, e.g.:`);
|
|
124
|
+
console.log(` cd <your-project> && npx ninja-terminals --setup`);
|
|
125
|
+
console.log(` (.mcp.json is already configured, so MCP tools will still load.)`);
|
|
116
126
|
}
|
|
117
127
|
}
|
|
118
128
|
|
|
@@ -246,9 +256,17 @@ function openBrowser(url) {
|
|
|
246
256
|
|
|
247
257
|
// Delay browser open until after the server listen callback fires.
|
|
248
258
|
// server.js calls server.listen() synchronously on require, so we
|
|
249
|
-
// 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).
|
|
250
262
|
setTimeout(() => {
|
|
251
|
-
|
|
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}`);
|
|
252
270
|
}, 1500);
|
|
253
271
|
|
|
254
272
|
// ── Start the server ─────────────────────────────────────────
|
package/lib/runtime-session.js
CHANGED
|
@@ -18,17 +18,29 @@ function ensureSessionDir() {
|
|
|
18
18
|
fs.mkdirSync(SESSION_DIR, { recursive: true, mode: 0o700 });
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
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(
|
|
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
|
|
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: '
|
|
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 = '
|
|
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 || '
|
|
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://
|
|
672
|
-
wsUrl: `ws://
|
|
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
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://
|
|
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;
|