ninja-terminals 2.4.0 → 2.4.1
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 +3 -1
- package/cli.js +34 -118
- package/lib/ninja-request.js +247 -0
- package/lib/settings-gen.js +0 -13
- package/mcp-server.js +7 -33
- 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 +15 -6
- 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,10 +35,10 @@ 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 --
|
|
41
|
+
const CLAUDE_CMD = process.env.CLAUDE_CMD || process.env.CLAUDE_CHROME_CMD || 'claude --chrome --model claude-opus-4-5-20251101';
|
|
42
42
|
const SHELL = process.env.SHELL || '/bin/zsh';
|
|
43
43
|
const PROJECT_DIR = __dirname;
|
|
44
44
|
const DEFAULT_CWD = process.env.DEFAULT_CWD || null; // Set to target project path to avoid cross-project prompts
|
|
@@ -47,12 +47,17 @@ const INJECT_GUIDANCE = process.env.INJECT_GUIDANCE !== 'false'; // Default tru
|
|
|
47
47
|
// Fleet modes — preset terminal configurations
|
|
48
48
|
const FLEET_MODES = {
|
|
49
49
|
claude: ['claude', 'claude', 'claude', 'claude'],
|
|
50
|
+
codex: ['codex', 'codex', 'codex', 'codex'],
|
|
51
|
+
opencode: ['opencode', 'opencode', 'opencode', 'opencode'],
|
|
50
52
|
teams: ['claude', 'opencode', 'codex', 'shell'],
|
|
51
53
|
mixed: ['claude', 'claude', 'opencode', 'shell'],
|
|
52
54
|
shell: ['shell', 'shell', 'shell', 'shell'],
|
|
53
55
|
duo: ['claude', 'opencode'],
|
|
54
56
|
};
|
|
55
57
|
const FLEET_MODE = process.env.NINJA_MODE || 'claude';
|
|
58
|
+
// Resolve the claude binary from PATH by default so it works on any machine.
|
|
59
|
+
// Override with CLAUDE_CMD (full command) or CLAUDE_BIN (binary path) if needed.
|
|
60
|
+
const CLAUDE_BIN = process.env.CLAUDE_BIN || 'claude';
|
|
56
61
|
|
|
57
62
|
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
|
58
63
|
|
|
@@ -152,7 +157,7 @@ function getTerminalRules(terminalId) {
|
|
|
152
157
|
// ── Terminal Spawning ───────────────────────────────────────
|
|
153
158
|
|
|
154
159
|
const AGENT_COMMANDS = {
|
|
155
|
-
claude: process.env.CLAUDE_CMD ||
|
|
160
|
+
claude: process.env.CLAUDE_CMD || `${CLAUDE_BIN} --dangerously-skip-permissions`,
|
|
156
161
|
opencode: 'opencode',
|
|
157
162
|
codex: 'codex',
|
|
158
163
|
shell: null, // No command — just shell
|
|
@@ -197,6 +202,9 @@ function spawnTerminal(label, scope = [], cwd = null, tier = 'pro', agentType =
|
|
|
197
202
|
env: {
|
|
198
203
|
...cleanEnv,
|
|
199
204
|
TERM: 'xterm-256color',
|
|
205
|
+
COLORTERM: 'truecolor',
|
|
206
|
+
FORCE_COLOR: '1',
|
|
207
|
+
CLICOLOR_FORCE: '1',
|
|
200
208
|
HOME: require('os').homedir(),
|
|
201
209
|
PATH: `${require('os').homedir()}/.local/bin:/opt/homebrew/bin:${process.env.PATH || ''}`,
|
|
202
210
|
SHELL_SESSIONS_DISABLE: '1',
|
|
@@ -1292,13 +1300,16 @@ async function startServer() {
|
|
|
1292
1300
|
server.listen(selectedPort, BIND_HOST, () => {
|
|
1293
1301
|
const url = `http://localhost:${selectedPort}`;
|
|
1294
1302
|
console.log(`[bind] Listening on ${BIND_HOST}:${selectedPort}`);
|
|
1303
|
+
const fleetConfig = FLEET_MODES[FLEET_MODE] || FLEET_MODES.claude;
|
|
1304
|
+
const terminalCount = DEFAULT_TERMINALS > 0 ? Math.min(DEFAULT_TERMINALS, fleetConfig.length) : fleetConfig.length;
|
|
1295
1305
|
const session = writeRuntimeSession({
|
|
1296
1306
|
port: selectedPort,
|
|
1297
1307
|
host: BIND_HOST,
|
|
1298
1308
|
url,
|
|
1299
1309
|
cwd: DEFAULT_CWD || process.cwd(),
|
|
1300
|
-
terminals:
|
|
1310
|
+
terminals: terminalCount,
|
|
1301
1311
|
command: 'ninja-terminals',
|
|
1312
|
+
launchConfig: { mode: FLEET_MODE, terminalCount },
|
|
1302
1313
|
});
|
|
1303
1314
|
|
|
1304
1315
|
console.log(`Ninja Terminals v2 running on ${url}`);
|
|
@@ -1314,8 +1325,6 @@ async function startServer() {
|
|
|
1314
1325
|
startSessionHeartbeat(sessionCache, handleSessionInvalidation, 5 * 60 * 1000);
|
|
1315
1326
|
|
|
1316
1327
|
// 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
1328
|
const agentLabels = { claude: 'Claude', opencode: 'OpenCode', codex: 'Codex', shell: 'Shell' };
|
|
1320
1329
|
|
|
1321
1330
|
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
|
-
});
|