ninja-terminals 2.3.9 → 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 -22
- package/README.md +3 -1
- package/cli.js +34 -52
- 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 +1 -1
- package/public/app.js +55 -308
- package/public/index.html +15 -44
- package/public/style.css +78 -6
- package/server.js +15 -6
package/CLAUDE.md
CHANGED
|
@@ -5,28 +5,8 @@ You are a Claude Code worker instance running inside Ninja Terminals, a multi-te
|
|
|
5
5
|
## When User Says "Use Ninja Terminal"
|
|
6
6
|
|
|
7
7
|
If the user asks you to orchestrate via Ninja Terminals, you MUST:
|
|
8
|
-
1.
|
|
9
|
-
2.
|
|
10
|
-
3. Follow the rules there — visible PTY workflow is primary; use `ninja-dispatch`/API for reliable visible dispatch, browser paste as manual fallback, MCP/API/browser for monitoring
|
|
11
|
-
|
|
12
|
-
## Startup Protocol
|
|
13
|
-
|
|
14
|
-
Before any orchestration, run this command to ensure server is running and UIs are open:
|
|
15
|
-
|
|
16
|
-
```bash
|
|
17
|
-
node .claude/hooks/ninja-startup.js
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
This will:
|
|
21
|
-
- Start the MCP server on port 3300 if not running
|
|
22
|
-
- Health check the server
|
|
23
|
-
- Open Main UI (`http://localhost:3300/`) in browser
|
|
24
|
-
- Open Log Viewer (`http://localhost:3300/log-viewer.html`) in browser
|
|
25
|
-
- Output JSON confirmation with URLs
|
|
26
|
-
|
|
27
|
-
**Do not proceed with orchestration until startup outputs `"status": "ready"`.**
|
|
28
|
-
|
|
29
|
-
If startup fails, report: `STATUS: ERROR — Ninja server failed to start. Check if port 3300 is in use.`
|
|
8
|
+
1. Read `ORCHESTRATOR-PROMPT.md` FIRST
|
|
9
|
+
2. Follow the rules there — visible PTY workflow is primary; use `ninja-dispatch`/API for reliable visible dispatch, browser paste as manual fallback, MCP/API/browser for monitoring
|
|
30
10
|
|
|
31
11
|
## Identity
|
|
32
12
|
- You are ONE of 4 Claude Code terminals running simultaneously
|
package/README.md
CHANGED
package/cli.js
CHANGED
|
@@ -30,7 +30,7 @@ USAGE
|
|
|
30
30
|
npx ninja-terminals [options]
|
|
31
31
|
|
|
32
32
|
OPTIONS
|
|
33
|
-
--setup
|
|
33
|
+
--setup Run the one-time setup flow for Claude Code
|
|
34
34
|
--port <number> Port to listen on (default: 3300)
|
|
35
35
|
--terminals <number> Number of terminals to spawn (default: 4)
|
|
36
36
|
--cwd <path> Working directory for terminals (default: current dir)
|
|
@@ -38,6 +38,7 @@ OPTIONS
|
|
|
38
38
|
--help, -h Show this help message
|
|
39
39
|
|
|
40
40
|
EXAMPLES
|
|
41
|
+
npx --yes ninja-terminals --setup
|
|
41
42
|
npx ninja-terminals
|
|
42
43
|
npx ninja-terminals --port 3301 --terminals 2
|
|
43
44
|
npx ninja-terminals --cwd /path/to/my-project
|
|
@@ -54,43 +55,24 @@ if (hasFlag('--version') || hasFlag('-v')) {
|
|
|
54
55
|
|
|
55
56
|
// ── Setup command ───────────────────────────────────────────
|
|
56
57
|
if (hasFlag('--setup')) {
|
|
57
|
-
runSetup().then(() => process.exit(0)).catch(e => { console.error(e); process.exit(1); });
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async function runSetup() {
|
|
61
58
|
const fs = require('fs');
|
|
62
59
|
const path = require('path');
|
|
63
60
|
const os = require('os');
|
|
64
61
|
|
|
65
62
|
console.log('\n🥷 NINJA TERMINALS SETUP\n');
|
|
66
63
|
|
|
67
|
-
// 1. Find or create .
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
const globalSettings = path.join(globalClaudeDir, 'settings.json');
|
|
72
|
-
|
|
73
|
-
// Prefer project-level if .claude dir exists, else use global
|
|
74
|
-
let settingsPath, claudeDir;
|
|
75
|
-
if (fs.existsSync(projectClaudeDir)) {
|
|
76
|
-
settingsPath = projectSettings;
|
|
77
|
-
claudeDir = projectClaudeDir;
|
|
78
|
-
} else {
|
|
79
|
-
settingsPath = globalSettings;
|
|
80
|
-
claudeDir = globalClaudeDir;
|
|
81
|
-
// Create global .claude dir if needed
|
|
82
|
-
if (!fs.existsSync(claudeDir)) {
|
|
83
|
-
fs.mkdirSync(claudeDir, { recursive: true });
|
|
84
|
-
}
|
|
85
|
-
}
|
|
64
|
+
// 1. Find or create .mcp.json
|
|
65
|
+
const projectMcp = path.join(process.cwd(), '.mcp.json');
|
|
66
|
+
const globalMcp = path.join(os.homedir(), '.mcp.json');
|
|
67
|
+
const mcpPath = fs.existsSync(projectMcp) ? projectMcp : globalMcp;
|
|
86
68
|
|
|
87
69
|
let mcpConfig = { mcpServers: {} };
|
|
88
|
-
if (fs.existsSync(
|
|
70
|
+
if (fs.existsSync(mcpPath)) {
|
|
89
71
|
try {
|
|
90
|
-
mcpConfig = JSON.parse(fs.readFileSync(
|
|
72
|
+
mcpConfig = JSON.parse(fs.readFileSync(mcpPath, 'utf-8'));
|
|
91
73
|
if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
|
|
92
74
|
} catch (e) {
|
|
93
|
-
console.log(`⚠️ Could not parse ${
|
|
75
|
+
console.log(`⚠️ Could not parse ${mcpPath}, creating new config`);
|
|
94
76
|
}
|
|
95
77
|
}
|
|
96
78
|
|
|
@@ -100,29 +82,19 @@ async function runSetup() {
|
|
|
100
82
|
args: ['ninja-terminals-mcp'],
|
|
101
83
|
env: {
|
|
102
84
|
NINJA_TERMINAL_COUNT: '4',
|
|
103
|
-
NINJA_LOG_LEVEL: 'info'
|
|
104
|
-
HTTP_PORT: '3300'
|
|
85
|
+
NINJA_LOG_LEVEL: 'info'
|
|
105
86
|
}
|
|
106
87
|
};
|
|
107
88
|
|
|
108
|
-
// Get npm root for copying orchestrator prompt
|
|
109
|
-
|
|
110
|
-
try {
|
|
111
|
-
npmRoot = path.dirname(require.resolve('ninja-terminals/package.json'));
|
|
112
|
-
} catch {
|
|
113
|
-
npmRoot = __dirname; // Dev mode fallback
|
|
114
|
-
}
|
|
89
|
+
// Get npm root for copying orchestrator prompt
|
|
90
|
+
const npmRoot = path.dirname(require.resolve('ninja-terminals/package.json'));
|
|
115
91
|
|
|
116
|
-
fs.writeFileSync(
|
|
117
|
-
console.log(`✅ Added ninja-terminals to ${
|
|
92
|
+
fs.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2) + '\n');
|
|
93
|
+
console.log(`✅ Added ninja-terminals to ${mcpPath}`);
|
|
118
94
|
|
|
119
95
|
// 3. Copy orchestrator prompt to CLAUDE.md
|
|
120
96
|
const claudeMd = path.join(process.cwd(), 'CLAUDE.md');
|
|
121
|
-
|
|
122
|
-
let orchestratorPrompt = path.join(npmRoot, 'ORCHESTRATOR-PROMPT.md');
|
|
123
|
-
if (!fs.existsSync(orchestratorPrompt)) {
|
|
124
|
-
orchestratorPrompt = path.join(npmRoot, 'prompts', 'orchestrator.md');
|
|
125
|
-
}
|
|
97
|
+
const orchestratorPrompt = path.join(npmRoot, 'ORCHESTRATOR-PROMPT.md');
|
|
126
98
|
|
|
127
99
|
if (fs.existsSync(orchestratorPrompt)) {
|
|
128
100
|
const prompt = fs.readFileSync(orchestratorPrompt, 'utf-8');
|
|
@@ -168,7 +140,7 @@ async function runSetup() {
|
|
|
168
140
|
}
|
|
169
141
|
|
|
170
142
|
// Save updated config with all MCPs
|
|
171
|
-
fs.writeFileSync(
|
|
143
|
+
fs.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2) + '\n');
|
|
172
144
|
|
|
173
145
|
// 5. Check for Claude in Chrome (optional but recommended)
|
|
174
146
|
const chromeExt = mcpConfig.mcpServers['claude-in-chrome'];
|
|
@@ -182,17 +154,28 @@ async function runSetup() {
|
|
|
182
154
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
183
155
|
✨ Setup complete!
|
|
184
156
|
|
|
185
|
-
|
|
186
|
-
|
|
157
|
+
MCPs configured:
|
|
158
|
+
• ninja-terminals - orchestrates parallel Claude Code instances
|
|
159
|
+
• playwright - browser automation (screenshots, clicks, reading)
|
|
160
|
+
• fetch - API calls to /api/terminals
|
|
161
|
+
|
|
162
|
+
Next steps:
|
|
163
|
+
1. Restart Claude Code to load MCP servers
|
|
164
|
+
2. Run: npx ninja-terminals
|
|
165
|
+
3. Or use MCP tools directly in Claude Code
|
|
166
|
+
|
|
167
|
+
MCP tools available after restart:
|
|
168
|
+
mcp__ninja-terminals__spawn_terminal
|
|
169
|
+
mcp__ninja-terminals__send_input
|
|
170
|
+
mcp__ninja-terminals__list_terminals
|
|
171
|
+
... and 9 more
|
|
187
172
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
188
173
|
`);
|
|
174
|
+
process.exit(0);
|
|
189
175
|
}
|
|
190
176
|
|
|
191
|
-
// If setup mode was requested, we're done (setup function calls process.exit)
|
|
192
|
-
if (!hasFlag('--setup')) {
|
|
193
|
-
|
|
194
177
|
const port = parseInt(getArg('--port', '3300'), 10);
|
|
195
|
-
const terminals = parseInt(getArg('--terminals', '
|
|
178
|
+
const terminals = parseInt(getArg('--terminals', '2'), 10); // Free tier default
|
|
196
179
|
const cwd = getArg('--cwd', process.cwd());
|
|
197
180
|
const token = getArg('--token', null);
|
|
198
181
|
const offline = hasFlag('--offline');
|
|
@@ -228,6 +211,7 @@ console.log(`
|
|
|
228
211
|
// must be set here before the require call.
|
|
229
212
|
|
|
230
213
|
process.env.PORT = String(port);
|
|
214
|
+
process.env.HTTP_PORT = String(port);
|
|
231
215
|
process.env.DEFAULT_TERMINALS = String(terminals);
|
|
232
216
|
process.env.DEFAULT_CWD = cwd;
|
|
233
217
|
|
|
@@ -270,5 +254,3 @@ setTimeout(() => {
|
|
|
270
254
|
// ── Start the server ─────────────────────────────────────────
|
|
271
255
|
|
|
272
256
|
require('./server.js');
|
|
273
|
-
|
|
274
|
-
} // end if (!hasFlag('--setup'))
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
|
|
7
|
+
const NINJA_DIR = path.join(os.homedir(), '.ninja');
|
|
8
|
+
const REQUEST_FILE = path.join(NINJA_DIR, 'ninja-request.json');
|
|
9
|
+
const LEDGER_FILE = path.join(NINJA_DIR, 'dispatch-ledger.ndjson');
|
|
10
|
+
const VERIFICATION_LEDGER_FILE = path.join(NINJA_DIR, 'verification-ledger.ndjson');
|
|
11
|
+
const VISUAL_LEDGER_FILE = path.join(NINJA_DIR, 'visual-ledger.ndjson');
|
|
12
|
+
|
|
13
|
+
const NINJA_PATTERNS = [
|
|
14
|
+
/(?:^|\b)(?:open|start|launch|spawn|bring up)\s+(?:the\s+)?ninja\s*terminals?\b/i,
|
|
15
|
+
/(?:^|\b)ninja\s*terminals?\s+(?:please|now)\b/i,
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const MODE_PATTERNS = [
|
|
19
|
+
{ mode: 'shell', pattern: /(?:terminals?|shell)\s+only/i, defaultCount: 4 },
|
|
20
|
+
{ mode: 'claude', pattern: /claude\s+only/i, defaultCount: 4 },
|
|
21
|
+
{ mode: 'codex', pattern: /codex\s+only/i, defaultCount: 4 },
|
|
22
|
+
{ mode: 'opencode', pattern: /(?:open\s*code|opencode)\s+only/i, defaultCount: 4 },
|
|
23
|
+
{ mode: 'mixed', pattern: /\bmixed\b/i, defaultCount: 4 },
|
|
24
|
+
{ mode: 'duo', pattern: /\bduo\b/i, defaultCount: 2 },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const NUMBER_WORDS = new Map([
|
|
28
|
+
['one', 1],
|
|
29
|
+
['two', 2],
|
|
30
|
+
['three', 3],
|
|
31
|
+
['four', 4],
|
|
32
|
+
['five', 5],
|
|
33
|
+
['six', 6],
|
|
34
|
+
['seven', 7],
|
|
35
|
+
['eight', 8],
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
function isNinjaRequest(prompt) {
|
|
39
|
+
if (!prompt) return false;
|
|
40
|
+
return NINJA_PATTERNS.some(pattern => pattern.test(prompt));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function inferNinjaLaunchConfig(prompt) {
|
|
44
|
+
const text = String(prompt || '');
|
|
45
|
+
const modeEntry = MODE_PATTERNS.find(entry => entry.pattern.test(text));
|
|
46
|
+
const mode = modeEntry ? modeEntry.mode : 'claude';
|
|
47
|
+
const defaultCount = modeEntry ? modeEntry.defaultCount : 4;
|
|
48
|
+
|
|
49
|
+
const numericCount = text.match(/\b(\d+)\s*(?:terminals?|instances?)\b/i);
|
|
50
|
+
const wordCount = text.match(/\b(one|two|three|four|five|six|seven|eight)\s*(?:terminals?|instances?)\b/i);
|
|
51
|
+
const requestedCount = numericCount
|
|
52
|
+
? parseInt(numericCount[1], 10)
|
|
53
|
+
: wordCount
|
|
54
|
+
? NUMBER_WORDS.get(wordCount[1].toLowerCase())
|
|
55
|
+
: null;
|
|
56
|
+
|
|
57
|
+
const terminalCount = Number.isInteger(requestedCount) && requestedCount > 0
|
|
58
|
+
? requestedCount
|
|
59
|
+
: defaultCount;
|
|
60
|
+
|
|
61
|
+
return { mode, terminalCount };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function writeNinjaRequest(cwd, promptPreview) {
|
|
65
|
+
try {
|
|
66
|
+
fs.mkdirSync(NINJA_DIR, { recursive: true, mode: 0o700 });
|
|
67
|
+
const launchConfig = inferNinjaLaunchConfig(promptPreview);
|
|
68
|
+
const data = {
|
|
69
|
+
timestamp: new Date().toISOString(),
|
|
70
|
+
cwd: cwd || process.cwd(),
|
|
71
|
+
promptPreview: (promptPreview || '').slice(0, 200),
|
|
72
|
+
launchConfig,
|
|
73
|
+
};
|
|
74
|
+
fs.writeFileSync(REQUEST_FILE, JSON.stringify(data, null, 2) + '\n', { mode: 0o600 });
|
|
75
|
+
return data;
|
|
76
|
+
} catch (err) {
|
|
77
|
+
console.error(`Warning: Could not write ninja request: ${err.message}`);
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function readNinjaRequest() {
|
|
83
|
+
try {
|
|
84
|
+
const raw = fs.readFileSync(REQUEST_FILE, 'utf8');
|
|
85
|
+
return JSON.parse(raw);
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function clearNinjaRequest() {
|
|
92
|
+
try {
|
|
93
|
+
fs.unlinkSync(REQUEST_FILE);
|
|
94
|
+
} catch {
|
|
95
|
+
// already absent
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function readLedgerAfter(afterTimestamp) {
|
|
100
|
+
try {
|
|
101
|
+
const raw = fs.readFileSync(LEDGER_FILE, 'utf8');
|
|
102
|
+
const lines = raw.trim().split('\n').filter(Boolean);
|
|
103
|
+
const entries = [];
|
|
104
|
+
for (const line of lines) {
|
|
105
|
+
try {
|
|
106
|
+
const entry = JSON.parse(line);
|
|
107
|
+
if (entry.timestamp && entry.timestamp > afterTimestamp) {
|
|
108
|
+
entries.push(entry);
|
|
109
|
+
}
|
|
110
|
+
} catch {
|
|
111
|
+
// skip malformed
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return entries;
|
|
115
|
+
} catch {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function hasSuccessfulDispatchAfter(afterTimestamp) {
|
|
121
|
+
const entries = readLedgerAfter(afterTimestamp);
|
|
122
|
+
return entries.some(e => e.success === true);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function readVerificationLedgerAfter(afterTimestamp) {
|
|
126
|
+
try {
|
|
127
|
+
const raw = fs.readFileSync(VERIFICATION_LEDGER_FILE, 'utf8');
|
|
128
|
+
const lines = raw.trim().split('\n').filter(Boolean);
|
|
129
|
+
const entries = [];
|
|
130
|
+
for (const line of lines) {
|
|
131
|
+
try {
|
|
132
|
+
const entry = JSON.parse(line);
|
|
133
|
+
if (entry.timestamp && entry.timestamp > afterTimestamp) {
|
|
134
|
+
entries.push(entry);
|
|
135
|
+
}
|
|
136
|
+
} catch {
|
|
137
|
+
// skip malformed
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return entries;
|
|
141
|
+
} catch {
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function hasVerificationAfter(afterTimestamp) {
|
|
147
|
+
const entries = readVerificationLedgerAfter(afterTimestamp);
|
|
148
|
+
return entries.length > 0;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function getLastSuccessfulDispatchTimestamp(afterTimestamp) {
|
|
152
|
+
const entries = readLedgerAfter(afterTimestamp);
|
|
153
|
+
const successful = entries.filter(e => e.success === true);
|
|
154
|
+
if (successful.length === 0) return null;
|
|
155
|
+
return successful[successful.length - 1].timestamp;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function getLastVerificationTimestamp(afterTimestamp) {
|
|
159
|
+
const entries = readVerificationLedgerAfter(afterTimestamp);
|
|
160
|
+
if (entries.length === 0) return null;
|
|
161
|
+
return entries[entries.length - 1].timestamp;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function readVisualLedgerAfter(afterTimestamp) {
|
|
165
|
+
try {
|
|
166
|
+
const raw = fs.readFileSync(VISUAL_LEDGER_FILE, 'utf8');
|
|
167
|
+
const lines = raw.trim().split('\n').filter(Boolean);
|
|
168
|
+
const entries = [];
|
|
169
|
+
for (const line of lines) {
|
|
170
|
+
try {
|
|
171
|
+
const entry = JSON.parse(line);
|
|
172
|
+
if (entry.timestamp && entry.timestamp > afterTimestamp) {
|
|
173
|
+
entries.push(entry);
|
|
174
|
+
}
|
|
175
|
+
} catch {
|
|
176
|
+
// skip malformed
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return entries;
|
|
180
|
+
} catch {
|
|
181
|
+
return [];
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function readVisualLedgerBetween(startTs, endTs) {
|
|
186
|
+
try {
|
|
187
|
+
const raw = fs.readFileSync(VISUAL_LEDGER_FILE, 'utf8');
|
|
188
|
+
const lines = raw.trim().split('\n').filter(Boolean);
|
|
189
|
+
const entries = [];
|
|
190
|
+
for (const line of lines) {
|
|
191
|
+
try {
|
|
192
|
+
const entry = JSON.parse(line);
|
|
193
|
+
if (entry.timestamp && entry.timestamp > startTs && entry.timestamp < endTs) {
|
|
194
|
+
entries.push(entry);
|
|
195
|
+
}
|
|
196
|
+
} catch {
|
|
197
|
+
// skip malformed
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return entries;
|
|
201
|
+
} catch {
|
|
202
|
+
return [];
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function hasVisualBetween(startTs, endTs, options = {}) {
|
|
207
|
+
const entries = readVisualLedgerBetween(startTs, endTs);
|
|
208
|
+
return entries.some(e => {
|
|
209
|
+
if (options.stage && e.stage !== options.stage) return false;
|
|
210
|
+
if (options.stages && !options.stages.includes(e.stage)) return false;
|
|
211
|
+
if (options.source && e.source !== options.source) return false;
|
|
212
|
+
return true;
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function hasVisualAfter(afterTimestamp, options = {}) {
|
|
217
|
+
const entries = readVisualLedgerAfter(afterTimestamp);
|
|
218
|
+
return entries.some(e => {
|
|
219
|
+
if (options.stage && e.stage !== options.stage) return false;
|
|
220
|
+
if (options.stages && !options.stages.includes(e.stage)) return false;
|
|
221
|
+
if (options.source && e.source !== options.source) return false;
|
|
222
|
+
return true;
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
module.exports = {
|
|
227
|
+
NINJA_DIR,
|
|
228
|
+
REQUEST_FILE,
|
|
229
|
+
LEDGER_FILE,
|
|
230
|
+
VERIFICATION_LEDGER_FILE,
|
|
231
|
+
VISUAL_LEDGER_FILE,
|
|
232
|
+
isNinjaRequest,
|
|
233
|
+
inferNinjaLaunchConfig,
|
|
234
|
+
writeNinjaRequest,
|
|
235
|
+
readNinjaRequest,
|
|
236
|
+
clearNinjaRequest,
|
|
237
|
+
readLedgerAfter,
|
|
238
|
+
hasSuccessfulDispatchAfter,
|
|
239
|
+
readVerificationLedgerAfter,
|
|
240
|
+
hasVerificationAfter,
|
|
241
|
+
getLastSuccessfulDispatchTimestamp,
|
|
242
|
+
getLastVerificationTimestamp,
|
|
243
|
+
readVisualLedgerAfter,
|
|
244
|
+
readVisualLedgerBetween,
|
|
245
|
+
hasVisualBetween,
|
|
246
|
+
hasVisualAfter,
|
|
247
|
+
};
|
package/lib/settings-gen.js
CHANGED
|
@@ -192,22 +192,9 @@ function writeWorkerSettings(terminalId, projectDir, scope, options = {}) {
|
|
|
192
192
|
const mergedAllow = [...new Set([...(existing.permissions?.allow || []), ...settings.permissions.allow])];
|
|
193
193
|
const mergedDeny = [...new Set([...(existing.permissions?.deny || []), ...settings.permissions.deny])];
|
|
194
194
|
|
|
195
|
-
// Build hooks for self-improvement loop (matches Claude Code's nested format)
|
|
196
|
-
const ninjaDir = path.resolve(__dirname, '..');
|
|
197
|
-
const hooks = {
|
|
198
|
-
PostToolUse: [{
|
|
199
|
-
matcher: '',
|
|
200
|
-
hooks: [{
|
|
201
|
-
type: 'command',
|
|
202
|
-
command: path.join(ninjaDir, '.claude/hooks/track-tool.sh'),
|
|
203
|
-
}],
|
|
204
|
-
}],
|
|
205
|
-
};
|
|
206
|
-
|
|
207
195
|
const merged = {
|
|
208
196
|
...existing,
|
|
209
197
|
permissions: { allow: mergedAllow, deny: mergedDeny },
|
|
210
|
-
hooks,
|
|
211
198
|
sandbox: settings.sandbox,
|
|
212
199
|
};
|
|
213
200
|
|
package/mcp-server.js
CHANGED
|
@@ -40,9 +40,9 @@ const {
|
|
|
40
40
|
} = require('./lib/runtime-session');
|
|
41
41
|
|
|
42
42
|
// ── Config ──────────────────────────────────────────────────
|
|
43
|
-
const PREFERRED_HTTP_PORT = parseInt(process.env.HTTP_PORT || '3300', 10);
|
|
43
|
+
const PREFERRED_HTTP_PORT = parseInt(process.env.HTTP_PORT || process.env.PORT || '3300', 10);
|
|
44
44
|
let HTTP_PORT = PREFERRED_HTTP_PORT;
|
|
45
|
-
const CLAUDE_CMD = process.env.CLAUDE_CMD || 'claude --
|
|
45
|
+
const CLAUDE_CMD = process.env.CLAUDE_CMD || process.env.CLAUDE_CHROME_CMD || 'claude --chrome --model claude-opus-4-5-20251101';
|
|
46
46
|
const SHELL = process.env.SHELL || '/bin/zsh';
|
|
47
47
|
const PROJECT_DIR = __dirname;
|
|
48
48
|
const INJECT_GUIDANCE = process.env.INJECT_GUIDANCE !== 'false';
|
|
@@ -151,6 +151,9 @@ function spawnTerminal(label, scope = [], cwd = null, tier = 'pro') {
|
|
|
151
151
|
env: {
|
|
152
152
|
...cleanEnv,
|
|
153
153
|
TERM: 'xterm-256color',
|
|
154
|
+
COLORTERM: 'truecolor',
|
|
155
|
+
FORCE_COLOR: '1',
|
|
156
|
+
CLICOLOR_FORCE: '1',
|
|
154
157
|
HOME: os.homedir(),
|
|
155
158
|
PATH: `${os.homedir()}/.local/bin:/opt/homebrew/bin:${process.env.PATH || ''}`,
|
|
156
159
|
SHELL_SESSIONS_DISABLE: '1',
|
|
@@ -940,37 +943,8 @@ async function main() {
|
|
|
940
943
|
console.error(`Ninja Terminals server already running on port ${HTTP_PORT}`);
|
|
941
944
|
console.error('MCP server starting in proxy mode (will use existing server)');
|
|
942
945
|
} else {
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
console.error(`Ninja Terminals preferred HTTP port ${PREFERRED_HTTP_PORT} unavailable; using ${HTTP_PORT}`);
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
// Standalone mode: start our own HTTP server
|
|
949
|
-
httpServer.listen(HTTP_PORT, () => {
|
|
950
|
-
const url = `http://localhost:${HTTP_PORT}`;
|
|
951
|
-
writeRuntimeSession({
|
|
952
|
-
port: HTTP_PORT,
|
|
953
|
-
url,
|
|
954
|
-
cwd: PROJECT_DIR,
|
|
955
|
-
terminals: parseInt(process.env.NINJA_TERMINAL_COUNT || '2', 10),
|
|
956
|
-
command: 'ninja-terminals-mcp',
|
|
957
|
-
});
|
|
958
|
-
ownsRuntimeSession = true;
|
|
959
|
-
console.error(`Ninja Terminals HTTP server running on ${url}`);
|
|
960
|
-
|
|
961
|
-
// Auto-spawn terminals based on tier (NINJA_TERMINAL_COUNT env var)
|
|
962
|
-
// Free = 2, Paid = 4
|
|
963
|
-
const terminalCount = parseInt(process.env.NINJA_TERMINAL_COUNT || '2', 10);
|
|
964
|
-
const labels = ['T1', 'T2', 'T3', 'T4', 'T5', 'T6', 'T7', 'T8'];
|
|
965
|
-
|
|
966
|
-
console.error(`Auto-spawning ${terminalCount} terminals...`);
|
|
967
|
-
for (let i = 0; i < terminalCount; i++) {
|
|
968
|
-
const label = labels[i] || `T${i + 1}`;
|
|
969
|
-
spawnTerminal(label, [], process.cwd(), 'pro');
|
|
970
|
-
console.error(` Spawned ${label}`);
|
|
971
|
-
}
|
|
972
|
-
console.error(`All ${terminalCount} terminals ready`);
|
|
973
|
-
});
|
|
946
|
+
console.error('No live Ninja runtime found. MCP is staying request-only until ninja-ensure starts the runtime.');
|
|
947
|
+
console.error('No HTTP server, no session file, no terminal spawn on Claude startup.');
|
|
974
948
|
}
|
|
975
949
|
|
|
976
950
|
// Start MCP server on stdio
|