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 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. Run the startup check FIRST (see Startup Protocol below)
9
- 2. Read `ORCHESTRATOR-PROMPT.md`
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
@@ -8,8 +8,10 @@ Free & open. Donations welcome.
8
8
 
9
9
  ## Installation
10
10
 
11
+ Run the one-time setup command:
12
+
11
13
  ```bash
12
- npm install -g ninja-terminals
14
+ npx --yes ninja-terminals --setup
13
15
  ```
14
16
 
15
17
  ## Quick Start
package/cli.js CHANGED
@@ -30,7 +30,7 @@ USAGE
30
30
  npx ninja-terminals [options]
31
31
 
32
32
  OPTIONS
33
- --setup Configure MCP server + orchestrator prompt (run once)
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 .claude/settings.json (Claude Code format)
68
- const projectClaudeDir = path.join(process.cwd(), '.claude');
69
- const globalClaudeDir = path.join(os.homedir(), '.claude');
70
- const projectSettings = path.join(projectClaudeDir, 'settings.json');
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(settingsPath)) {
70
+ if (fs.existsSync(mcpPath)) {
89
71
  try {
90
- mcpConfig = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
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 ${settingsPath}, creating new config`);
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 (works in dev and installed mode)
109
- let npmRoot;
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(settingsPath, JSON.stringify(mcpConfig, null, 2) + '\n');
117
- console.log(`✅ Added ninja-terminals to ${settingsPath}`);
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
- // Use the real orchestrator prompt, not the pointer file
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(settingsPath, JSON.stringify(mcpConfig, null, 2) + '\n');
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
- Next: Restart Claude Code, then tell Claude:
186
- "use ninja terminals"
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', '4'), 10); // Full access
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
+ };
@@ -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 --dangerously-skip-permissions';
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
- HTTP_PORT = await findAvailablePort(PREFERRED_HTTP_PORT);
944
- if (HTTP_PORT !== PREFERRED_HTTP_PORT) {
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