ninja-terminals 2.4.0 → 2.4.2
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/CLAUDE.md +2 -379
- package/README.md +5 -1
- package/cli.js +34 -118
- package/lib/ninja-request.js +247 -0
- package/lib/settings-gen.js +0 -13
- package/mcp-server.js +22 -37
- package/ninja-ensure.js +92 -16
- package/package.json +2 -3
- package/public/app.js +55 -308
- package/public/index.html +15 -44
- package/public/style.css +78 -6
- package/server.js +31 -12
- package/hooks/ninja-common.js +0 -46
- package/hooks/ninja-prompt-submit.js +0 -33
- package/hooks/ninja-startup.js +0 -95
package/server.js
CHANGED
|
@@ -35,11 +35,14 @@ const {
|
|
|
35
35
|
} = require('./lib/runtime-session');
|
|
36
36
|
|
|
37
37
|
// ── Config ──────────────────────────────────────────────────
|
|
38
|
-
const PREFERRED_PORT = parseInt(process.env.PORT || '3300', 10);
|
|
38
|
+
const PREFERRED_PORT = parseInt(process.env.PORT || process.env.HTTP_PORT || '3300', 10);
|
|
39
39
|
const BIND_HOST = process.env.NINJA_BIND_HOST || '127.0.0.1';
|
|
40
40
|
const DEFAULT_TERMINALS = parseInt(process.env.DEFAULT_TERMINALS || '4', 10);
|
|
41
|
-
const CLAUDE_CMD = process.env.CLAUDE_CMD || 'claude --
|
|
42
|
-
|
|
41
|
+
const CLAUDE_CMD = process.env.CLAUDE_CMD || process.env.CLAUDE_CHROME_CMD || 'claude --chrome --model claude-opus-4-5-20251101';
|
|
42
|
+
// Windows has no $SHELL / /bin/zsh — default to PowerShell (handles drive
|
|
43
|
+
// changes via `cd` and emits clean ANSI for status detection).
|
|
44
|
+
const IS_WIN = process.platform === 'win32';
|
|
45
|
+
const SHELL = IS_WIN ? (process.env.NINJA_SHELL || 'powershell.exe') : (process.env.SHELL || '/bin/zsh');
|
|
43
46
|
const PROJECT_DIR = __dirname;
|
|
44
47
|
const DEFAULT_CWD = process.env.DEFAULT_CWD || null; // Set to target project path to avoid cross-project prompts
|
|
45
48
|
const INJECT_GUIDANCE = process.env.INJECT_GUIDANCE !== 'false'; // Default true, set INJECT_GUIDANCE=false to disable
|
|
@@ -47,12 +50,17 @@ const INJECT_GUIDANCE = process.env.INJECT_GUIDANCE !== 'false'; // Default tru
|
|
|
47
50
|
// Fleet modes — preset terminal configurations
|
|
48
51
|
const FLEET_MODES = {
|
|
49
52
|
claude: ['claude', 'claude', 'claude', 'claude'],
|
|
53
|
+
codex: ['codex', 'codex', 'codex', 'codex'],
|
|
54
|
+
opencode: ['opencode', 'opencode', 'opencode', 'opencode'],
|
|
50
55
|
teams: ['claude', 'opencode', 'codex', 'shell'],
|
|
51
56
|
mixed: ['claude', 'claude', 'opencode', 'shell'],
|
|
52
57
|
shell: ['shell', 'shell', 'shell', 'shell'],
|
|
53
58
|
duo: ['claude', 'opencode'],
|
|
54
59
|
};
|
|
55
60
|
const FLEET_MODE = process.env.NINJA_MODE || 'claude';
|
|
61
|
+
// Resolve the claude binary from PATH by default so it works on any machine.
|
|
62
|
+
// Override with CLAUDE_CMD (full command) or CLAUDE_BIN (binary path) if needed.
|
|
63
|
+
const CLAUDE_BIN = process.env.CLAUDE_BIN || 'claude';
|
|
56
64
|
|
|
57
65
|
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
|
58
66
|
|
|
@@ -152,7 +160,7 @@ function getTerminalRules(terminalId) {
|
|
|
152
160
|
// ── Terminal Spawning ───────────────────────────────────────
|
|
153
161
|
|
|
154
162
|
const AGENT_COMMANDS = {
|
|
155
|
-
claude: process.env.CLAUDE_CMD ||
|
|
163
|
+
claude: process.env.CLAUDE_CMD || `${CLAUDE_BIN} --dangerously-skip-permissions`,
|
|
156
164
|
opencode: 'opencode',
|
|
157
165
|
codex: 'codex',
|
|
158
166
|
shell: null, // No command — just shell
|
|
@@ -189,7 +197,7 @@ function spawnTerminal(label, scope = [], cwd = null, tier = 'pro', agentType =
|
|
|
189
197
|
}
|
|
190
198
|
}
|
|
191
199
|
|
|
192
|
-
const ptyProcess = pty.spawn(SHELL, ['-l'], {
|
|
200
|
+
const ptyProcess = pty.spawn(SHELL, IS_WIN ? [] : ['-l'], {
|
|
193
201
|
name: 'xterm-256color',
|
|
194
202
|
cols,
|
|
195
203
|
rows,
|
|
@@ -197,8 +205,16 @@ function spawnTerminal(label, scope = [], cwd = null, tier = 'pro', agentType =
|
|
|
197
205
|
env: {
|
|
198
206
|
...cleanEnv,
|
|
199
207
|
TERM: 'xterm-256color',
|
|
208
|
+
COLORTERM: 'truecolor',
|
|
209
|
+
FORCE_COLOR: '1',
|
|
210
|
+
CLICOLOR_FORCE: '1',
|
|
200
211
|
HOME: require('os').homedir(),
|
|
201
|
-
PATH
|
|
212
|
+
// On Windows, keep the native PATH untouched (mac/linux bin dirs would
|
|
213
|
+
// corrupt it via the ';' delimiter). On POSIX, prepend the usual bins.
|
|
214
|
+
PATH: IS_WIN
|
|
215
|
+
? (process.env.PATH || '')
|
|
216
|
+
: [`${require('os').homedir()}/.local/bin`, '/opt/homebrew/bin', process.env.PATH || '']
|
|
217
|
+
.filter(Boolean).join(path.delimiter),
|
|
202
218
|
SHELL_SESSIONS_DISABLE: '1',
|
|
203
219
|
NINJA_TERMINAL_ID: String(id),
|
|
204
220
|
},
|
|
@@ -206,11 +222,13 @@ function spawnTerminal(label, scope = [], cwd = null, tier = 'pro', agentType =
|
|
|
206
222
|
|
|
207
223
|
// After shell starts, cd to work dir and launch agent (if not shell)
|
|
208
224
|
const agentCmd = AGENT_COMMANDS[agentType] || null;
|
|
225
|
+
// Two separate writes (cd, then command) instead of `&&` — works across
|
|
226
|
+
// bash/zsh/PowerShell/cmd, which disagree on chaining (`&&` is unsupported
|
|
227
|
+
// in Windows PowerShell 5).
|
|
209
228
|
setTimeout(() => {
|
|
229
|
+
ptyProcess.write(`cd "${workDir}"\r`);
|
|
210
230
|
if (agentCmd) {
|
|
211
|
-
ptyProcess.write(
|
|
212
|
-
} else {
|
|
213
|
-
ptyProcess.write(`cd "${workDir}"\r`);
|
|
231
|
+
setTimeout(() => ptyProcess.write(`${agentCmd}\r`), 250);
|
|
214
232
|
}
|
|
215
233
|
}, 500);
|
|
216
234
|
|
|
@@ -1292,13 +1310,16 @@ async function startServer() {
|
|
|
1292
1310
|
server.listen(selectedPort, BIND_HOST, () => {
|
|
1293
1311
|
const url = `http://localhost:${selectedPort}`;
|
|
1294
1312
|
console.log(`[bind] Listening on ${BIND_HOST}:${selectedPort}`);
|
|
1313
|
+
const fleetConfig = FLEET_MODES[FLEET_MODE] || FLEET_MODES.claude;
|
|
1314
|
+
const terminalCount = DEFAULT_TERMINALS > 0 ? Math.min(DEFAULT_TERMINALS, fleetConfig.length) : fleetConfig.length;
|
|
1295
1315
|
const session = writeRuntimeSession({
|
|
1296
1316
|
port: selectedPort,
|
|
1297
1317
|
host: BIND_HOST,
|
|
1298
1318
|
url,
|
|
1299
1319
|
cwd: DEFAULT_CWD || process.cwd(),
|
|
1300
|
-
terminals:
|
|
1320
|
+
terminals: terminalCount,
|
|
1301
1321
|
command: 'ninja-terminals',
|
|
1322
|
+
launchConfig: { mode: FLEET_MODE, terminalCount },
|
|
1302
1323
|
});
|
|
1303
1324
|
|
|
1304
1325
|
console.log(`Ninja Terminals v2 running on ${url}`);
|
|
@@ -1314,8 +1335,6 @@ async function startServer() {
|
|
|
1314
1335
|
startSessionHeartbeat(sessionCache, handleSessionInvalidation, 5 * 60 * 1000);
|
|
1315
1336
|
|
|
1316
1337
|
// Auto-spawn terminals based on fleet mode
|
|
1317
|
-
const fleetConfig = FLEET_MODES[FLEET_MODE] || FLEET_MODES.claude;
|
|
1318
|
-
const terminalCount = DEFAULT_TERMINALS > 0 ? Math.min(DEFAULT_TERMINALS, fleetConfig.length) : fleetConfig.length;
|
|
1319
1338
|
const agentLabels = { claude: 'Claude', opencode: 'OpenCode', codex: 'Codex', shell: 'Shell' };
|
|
1320
1339
|
|
|
1321
1340
|
console.log(`Auto-spawning ${terminalCount} terminals (mode: ${FLEET_MODE})...`);
|
package/hooks/ninja-common.js
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
const fs = require('fs');
|
|
5
|
-
const path = require('path');
|
|
6
|
-
const os = require('os');
|
|
7
|
-
|
|
8
|
-
const NINJA_DIR = path.join(os.homedir(), '.ninja');
|
|
9
|
-
const REQUEST_FILE = path.join(NINJA_DIR, 'ninja-request.json');
|
|
10
|
-
|
|
11
|
-
const NINJA_PATTERNS = [
|
|
12
|
-
/ninja\s*terminal/i,
|
|
13
|
-
/use\s+ninja/i,
|
|
14
|
-
/build\s+with\s+ninja/i,
|
|
15
|
-
/orchestrat/i,
|
|
16
|
-
/\bPRD\b.*\bbuild\b/i,
|
|
17
|
-
/\bbuild\b.*\bPRD\b/i,
|
|
18
|
-
];
|
|
19
|
-
|
|
20
|
-
function isNinjaRequest(prompt) {
|
|
21
|
-
if (!prompt) return false;
|
|
22
|
-
return NINJA_PATTERNS.some(pattern => pattern.test(prompt));
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function writeNinjaRequest(cwd, promptPreview) {
|
|
26
|
-
try {
|
|
27
|
-
fs.mkdirSync(NINJA_DIR, { recursive: true, mode: 0o700 });
|
|
28
|
-
const data = {
|
|
29
|
-
timestamp: new Date().toISOString(),
|
|
30
|
-
cwd: cwd || process.cwd(),
|
|
31
|
-
promptPreview: (promptPreview || '').slice(0, 200),
|
|
32
|
-
};
|
|
33
|
-
fs.writeFileSync(REQUEST_FILE, JSON.stringify(data, null, 2) + '\n', { mode: 0o600 });
|
|
34
|
-
return data;
|
|
35
|
-
} catch (err) {
|
|
36
|
-
console.error(`Warning: Could not write ninja request: ${err.message}`);
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
module.exports = {
|
|
42
|
-
NINJA_DIR,
|
|
43
|
-
REQUEST_FILE,
|
|
44
|
-
isNinjaRequest,
|
|
45
|
-
writeNinjaRequest,
|
|
46
|
-
};
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
const { isNinjaRequest, writeNinjaRequest } = require('./ninja-common');
|
|
5
|
-
|
|
6
|
-
function main() {
|
|
7
|
-
let input = '';
|
|
8
|
-
process.stdin.setEncoding('utf8');
|
|
9
|
-
process.stdin.on('data', chunk => { input += chunk; });
|
|
10
|
-
process.stdin.on('end', () => {
|
|
11
|
-
try {
|
|
12
|
-
const event = JSON.parse(input);
|
|
13
|
-
const prompt = event.prompt || '';
|
|
14
|
-
const cwd = event.cwd || process.cwd();
|
|
15
|
-
|
|
16
|
-
if (isNinjaRequest(prompt)) {
|
|
17
|
-
writeNinjaRequest(cwd, prompt);
|
|
18
|
-
const output = {
|
|
19
|
-
result: 'continue',
|
|
20
|
-
message: `NINJA REQUEST DETECTED. Follow ORCHESTRATOR-PROMPT.md workflow.`,
|
|
21
|
-
};
|
|
22
|
-
console.log(JSON.stringify(output));
|
|
23
|
-
} else {
|
|
24
|
-
console.log(JSON.stringify({ result: 'continue' }));
|
|
25
|
-
}
|
|
26
|
-
} catch (err) {
|
|
27
|
-
console.error(`Warning: ninja-prompt-submit hook error: ${err.message}`);
|
|
28
|
-
console.log(JSON.stringify({ result: 'continue' }));
|
|
29
|
-
}
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
main();
|
package/hooks/ninja-startup.js
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
const http = require('http');
|
|
5
|
-
const { execSync } = require('child_process');
|
|
6
|
-
|
|
7
|
-
const PORT = parseInt(process.env.HTTP_PORT || '3300', 10);
|
|
8
|
-
const MAX_WAIT_MS = 10000;
|
|
9
|
-
const POLL_INTERVAL_MS = 500;
|
|
10
|
-
|
|
11
|
-
function log(msg) {
|
|
12
|
-
process.stderr.write(`[ninja-startup] ${msg}\n`);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function healthCheck(port) {
|
|
16
|
-
return new Promise((resolve) => {
|
|
17
|
-
const req = http.get(`http://localhost:${port}/health`, (res) => {
|
|
18
|
-
let data = '';
|
|
19
|
-
res.on('data', chunk => data += chunk);
|
|
20
|
-
res.on('end', () => {
|
|
21
|
-
try {
|
|
22
|
-
const json = JSON.parse(data);
|
|
23
|
-
resolve(json.status === 'ok');
|
|
24
|
-
} catch {
|
|
25
|
-
resolve(false);
|
|
26
|
-
}
|
|
27
|
-
});
|
|
28
|
-
});
|
|
29
|
-
req.on('error', () => resolve(false));
|
|
30
|
-
req.setTimeout(2000, () => {
|
|
31
|
-
req.destroy();
|
|
32
|
-
resolve(false);
|
|
33
|
-
});
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async function waitForHealth(port, maxWait = MAX_WAIT_MS) {
|
|
38
|
-
const start = Date.now();
|
|
39
|
-
while (Date.now() - start < maxWait) {
|
|
40
|
-
if (await healthCheck(port)) return true;
|
|
41
|
-
await new Promise(r => setTimeout(r, POLL_INTERVAL_MS));
|
|
42
|
-
}
|
|
43
|
-
return false;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function openInBrowser(url) {
|
|
47
|
-
try {
|
|
48
|
-
if (process.platform === 'darwin') {
|
|
49
|
-
execSync(`open "${url}"`, { stdio: 'ignore' });
|
|
50
|
-
} else if (process.platform === 'linux') {
|
|
51
|
-
execSync(`xdg-open "${url}"`, { stdio: 'ignore' });
|
|
52
|
-
} else if (process.platform === 'win32') {
|
|
53
|
-
execSync(`start "" "${url}"`, { stdio: 'ignore', shell: true });
|
|
54
|
-
}
|
|
55
|
-
} catch {
|
|
56
|
-
log(`Could not auto-open ${url}`);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async function main() {
|
|
61
|
-
// MCP server should already be running (started by Claude Code via mcpServers config)
|
|
62
|
-
// Just wait for it to be healthy
|
|
63
|
-
log(`Checking Ninja Terminal server on port ${PORT}...`);
|
|
64
|
-
|
|
65
|
-
const healthy = await waitForHealth(PORT);
|
|
66
|
-
|
|
67
|
-
if (!healthy) {
|
|
68
|
-
// Server not running - tell user to check MCP config
|
|
69
|
-
console.log(JSON.stringify({
|
|
70
|
-
status: 'error',
|
|
71
|
-
message: `Ninja Terminal server not responding on port ${PORT}. Check MCP config or run: npx ninja-terminals`,
|
|
72
|
-
}));
|
|
73
|
-
process.exit(0); // Don't fail the hook, just report
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const mainUrl = `http://localhost:${PORT}/`;
|
|
77
|
-
const logViewerUrl = `http://localhost:${PORT}/log-viewer.html`;
|
|
78
|
-
|
|
79
|
-
log('Opening Ninja UI in browser...');
|
|
80
|
-
openInBrowser(mainUrl);
|
|
81
|
-
|
|
82
|
-
await new Promise(r => setTimeout(r, 300));
|
|
83
|
-
openInBrowser(logViewerUrl);
|
|
84
|
-
|
|
85
|
-
console.log(JSON.stringify({
|
|
86
|
-
status: 'ready',
|
|
87
|
-
port: PORT,
|
|
88
|
-
urls: { main: mainUrl, logViewer: logViewerUrl },
|
|
89
|
-
}));
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
main().catch(err => {
|
|
93
|
-
log(`Error: ${err.message}`);
|
|
94
|
-
process.exit(0); // Don't fail session start
|
|
95
|
-
});
|