ninja-terminals 2.3.1 → 2.3.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 +81 -0
- package/ORCHESTRATOR-PROMPT.md +91 -19
- package/README.md +25 -2
- package/agent-send.js +395 -0
- package/cli.js +25 -10
- package/lib/nameGenerator.ts +101 -0
- package/lib/pre-dispatch.js +14 -4
- package/lib/runtime-session.js +337 -0
- package/lib/status-detect.js +68 -4
- package/mcp-server.js +267 -25
- package/ninja-claude-visual.js +13 -0
- package/ninja-codex-visual.js +258 -0
- package/ninja-codex.js +474 -0
- package/ninja-ensure.js +333 -0
- package/ninja-gate.js +340 -0
- package/ninja-login.js +171 -0
- package/ninja-logout.js +42 -0
- package/ninja-visual.js +125 -0
- package/ninja-whoami.js +29 -0
- package/package.json +26 -3
- package/prompts/orchestrator.md +3 -292
- package/public/app.js +197 -4
- package/public/log-viewer.html +463 -0
- package/public/style.css +64 -0
- package/server.js +335 -32
package/ninja-login.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const readline = require('readline');
|
|
5
|
+
const https = require('https');
|
|
6
|
+
const { execSync } = require('child_process');
|
|
7
|
+
const { writeAuthToken, readAuthToken, updateRuntimeSession, SESSION_DIR } = require('./lib/runtime-session');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
const BACKEND_URL = process.env.NINJA_BACKEND_URL || 'https://emtchat-backend.onrender.com';
|
|
12
|
+
const CREDENTIALS_FILE = path.join(SESSION_DIR, 'credentials.json');
|
|
13
|
+
|
|
14
|
+
function parseArgs() {
|
|
15
|
+
const args = process.argv.slice(2);
|
|
16
|
+
const result = { username: null, password: null };
|
|
17
|
+
|
|
18
|
+
for (let i = 0; i < args.length; i++) {
|
|
19
|
+
if ((args[i] === '--username' || args[i] === '--user' || args[i] === '-u') && args[i + 1]) {
|
|
20
|
+
result.username = args[++i];
|
|
21
|
+
} else if (args[i] === '--password' && args[i + 1]) {
|
|
22
|
+
result.password = args[++i];
|
|
23
|
+
} else if (args[i] === '-h' || args[i] === '--help') {
|
|
24
|
+
console.log(`
|
|
25
|
+
ninja-login — Authenticate with Ninja Terminals
|
|
26
|
+
|
|
27
|
+
Usage:
|
|
28
|
+
ninja-login Interactive login
|
|
29
|
+
ninja-login --username x --password y Non-interactive login
|
|
30
|
+
ninja-login -u x --password y Short form
|
|
31
|
+
|
|
32
|
+
Environment:
|
|
33
|
+
NINJA_BACKEND_URL Backend URL (default: https://emtchat-backend.onrender.com)
|
|
34
|
+
|
|
35
|
+
After login, token saved to ~/.ninja/token for CLI/MCP tools.
|
|
36
|
+
`);
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function prompt(question) {
|
|
44
|
+
return new Promise((resolve) => {
|
|
45
|
+
const rl = readline.createInterface({
|
|
46
|
+
input: process.stdin,
|
|
47
|
+
output: process.stdout,
|
|
48
|
+
});
|
|
49
|
+
rl.question(question, (answer) => {
|
|
50
|
+
rl.close();
|
|
51
|
+
resolve(answer);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function promptPassword(question) {
|
|
57
|
+
return new Promise((resolve) => {
|
|
58
|
+
// Disable terminal echo using stty (works on macOS/Linux)
|
|
59
|
+
try { execSync('stty -echo', { stdio: 'pipe' }); } catch {}
|
|
60
|
+
|
|
61
|
+
const rl = readline.createInterface({
|
|
62
|
+
input: process.stdin,
|
|
63
|
+
output: process.stdout,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
process.stdout.write(question);
|
|
67
|
+
rl.question('', (answer) => {
|
|
68
|
+
// Re-enable echo
|
|
69
|
+
try { execSync('stty echo', { stdio: 'pipe' }); } catch {}
|
|
70
|
+
console.log(); // newline
|
|
71
|
+
rl.close();
|
|
72
|
+
resolve(answer);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function postJson(url, body) {
|
|
78
|
+
return new Promise((resolve, reject) => {
|
|
79
|
+
const parsed = new URL(url);
|
|
80
|
+
const payload = JSON.stringify(body);
|
|
81
|
+
|
|
82
|
+
const req = https.request({
|
|
83
|
+
hostname: parsed.hostname,
|
|
84
|
+
port: parsed.port || 443,
|
|
85
|
+
path: parsed.pathname,
|
|
86
|
+
method: 'POST',
|
|
87
|
+
headers: {
|
|
88
|
+
'Content-Type': 'application/json',
|
|
89
|
+
'Content-Length': Buffer.byteLength(payload),
|
|
90
|
+
},
|
|
91
|
+
}, (res) => {
|
|
92
|
+
let data = '';
|
|
93
|
+
res.on('data', chunk => { data += chunk; });
|
|
94
|
+
res.on('end', () => {
|
|
95
|
+
try {
|
|
96
|
+
resolve({ status: res.statusCode, body: JSON.parse(data) });
|
|
97
|
+
} catch {
|
|
98
|
+
resolve({ status: res.statusCode, body: data });
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
req.on('error', reject);
|
|
104
|
+
req.write(payload);
|
|
105
|
+
req.end();
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function login(username, password) {
|
|
110
|
+
console.log('Authenticating...');
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const res = await postJson(`${BACKEND_URL}/api/auth/login`, { username, password });
|
|
114
|
+
|
|
115
|
+
if (res.status === 200 && res.body.token) {
|
|
116
|
+
writeAuthToken(res.body.token);
|
|
117
|
+
|
|
118
|
+
// Also update runtime session if it exists
|
|
119
|
+
updateRuntimeSession({ authToken: res.body.token, username });
|
|
120
|
+
|
|
121
|
+
// Save username for whoami (not password)
|
|
122
|
+
fs.mkdirSync(SESSION_DIR, { recursive: true, mode: 0o700 });
|
|
123
|
+
fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify({ username }, null, 2), { mode: 0o600 });
|
|
124
|
+
|
|
125
|
+
console.log(`✓ Logged in as ${username}`);
|
|
126
|
+
console.log(` Token saved to ~/.ninja/token`);
|
|
127
|
+
return true;
|
|
128
|
+
} else {
|
|
129
|
+
console.error(`✗ Login failed: ${res.body.error || res.body.message || 'Unknown error'}`);
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
} catch (err) {
|
|
133
|
+
console.error(`✗ Connection failed: ${err.message}`);
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function main() {
|
|
139
|
+
const args = parseArgs();
|
|
140
|
+
let { username, password } = args;
|
|
141
|
+
|
|
142
|
+
// Check if already logged in
|
|
143
|
+
const existingToken = readAuthToken();
|
|
144
|
+
if (existingToken && !username) {
|
|
145
|
+
try {
|
|
146
|
+
const creds = JSON.parse(fs.readFileSync(CREDENTIALS_FILE, 'utf8'));
|
|
147
|
+
console.log(`Already logged in as ${creds.username || creds.email}`);
|
|
148
|
+
console.log(`Run 'ninja-logout' to sign out, or continue with new credentials.`);
|
|
149
|
+
} catch {
|
|
150
|
+
console.log('Token exists but no stored user. Re-authenticating...');
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Interactive prompts if needed
|
|
155
|
+
if (!username) {
|
|
156
|
+
username = await prompt('Username: ');
|
|
157
|
+
}
|
|
158
|
+
if (!password) {
|
|
159
|
+
password = await promptPassword('Password: ');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!username || !password) {
|
|
163
|
+
console.error('Username and password required.');
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const success = await login(username, password);
|
|
168
|
+
process.exit(success ? 0 : 1);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
main();
|
package/ninja-logout.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { SESSION_DIR, TOKEN_FILE, readAuthToken, updateRuntimeSession } = require('./lib/runtime-session');
|
|
7
|
+
|
|
8
|
+
const CREDENTIALS_FILE = path.join(SESSION_DIR, 'credentials.json');
|
|
9
|
+
|
|
10
|
+
function main() {
|
|
11
|
+
const token = readAuthToken();
|
|
12
|
+
|
|
13
|
+
if (!token) {
|
|
14
|
+
console.log('Not logged in.');
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Clear token file
|
|
19
|
+
try {
|
|
20
|
+
fs.unlinkSync(TOKEN_FILE);
|
|
21
|
+
} catch {
|
|
22
|
+
// already gone
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Clear credentials
|
|
26
|
+
try {
|
|
27
|
+
fs.unlinkSync(CREDENTIALS_FILE);
|
|
28
|
+
} catch {
|
|
29
|
+
// already gone
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Clear authToken from session
|
|
33
|
+
try {
|
|
34
|
+
updateRuntimeSession({ authToken: null, email: null });
|
|
35
|
+
} catch {
|
|
36
|
+
// session may not exist
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
console.log('✓ Logged out. Token cleared.');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
main();
|
package/ninja-visual.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const {
|
|
5
|
+
VISUAL_LEDGER_FILE,
|
|
6
|
+
VALID_VISUAL_STAGES,
|
|
7
|
+
appendVisualEntry,
|
|
8
|
+
readVisualEntries,
|
|
9
|
+
clearVisualLedger,
|
|
10
|
+
} = require('./lib/runtime-session');
|
|
11
|
+
|
|
12
|
+
function printUsage() {
|
|
13
|
+
console.log(`Usage:
|
|
14
|
+
node ninja-visual.js --record <stage> --note "<what was seen>" [--terminal <id>]
|
|
15
|
+
node ninja-visual.js --ledger [N]
|
|
16
|
+
node ninja-visual.js --clear
|
|
17
|
+
|
|
18
|
+
Valid stages: ${VALID_VISUAL_STAGES.join(', ')}
|
|
19
|
+
|
|
20
|
+
Examples:
|
|
21
|
+
node ninja-visual.js --record pre-dispatch --note "Ninja tab visible, T1-T4 idle"
|
|
22
|
+
node ninja-visual.js --record post-output --terminal 1 --note "T1 STATUS: DONE visible"
|
|
23
|
+
node ninja-visual.js --ledger
|
|
24
|
+
node ninja-visual.js --ledger 10`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseArgs(args) {
|
|
28
|
+
const result = {};
|
|
29
|
+
let i = 0;
|
|
30
|
+
while (i < args.length) {
|
|
31
|
+
const arg = args[i];
|
|
32
|
+
if (arg === '--record' && args[i + 1]) {
|
|
33
|
+
result.command = 'record';
|
|
34
|
+
result.stage = args[i + 1];
|
|
35
|
+
i += 2;
|
|
36
|
+
} else if (arg === '--note' && args[i + 1]) {
|
|
37
|
+
result.note = args[i + 1];
|
|
38
|
+
i += 2;
|
|
39
|
+
} else if (arg === '--terminal' && args[i + 1]) {
|
|
40
|
+
result.terminalId = args[i + 1];
|
|
41
|
+
i += 2;
|
|
42
|
+
} else if (arg === '--ledger') {
|
|
43
|
+
result.command = 'ledger';
|
|
44
|
+
if (args[i + 1] && !args[i + 1].startsWith('--')) {
|
|
45
|
+
result.limit = parseInt(args[i + 1], 10);
|
|
46
|
+
i += 2;
|
|
47
|
+
} else {
|
|
48
|
+
i += 1;
|
|
49
|
+
}
|
|
50
|
+
} else if (arg === '--clear') {
|
|
51
|
+
result.command = 'clear';
|
|
52
|
+
i += 1;
|
|
53
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
54
|
+
result.command = 'help';
|
|
55
|
+
i += 1;
|
|
56
|
+
} else {
|
|
57
|
+
i += 1;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function main() {
|
|
64
|
+
const args = parseArgs(process.argv.slice(2));
|
|
65
|
+
|
|
66
|
+
if (args.command === 'help' || !args.command) {
|
|
67
|
+
printUsage();
|
|
68
|
+
process.exit(args.command === 'help' ? 0 : 1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (args.command === 'clear') {
|
|
72
|
+
clearVisualLedger();
|
|
73
|
+
console.log('Visual ledger cleared.');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (args.command === 'ledger') {
|
|
78
|
+
const limit = args.limit || 20;
|
|
79
|
+
const entries = readVisualEntries(limit);
|
|
80
|
+
if (entries.length === 0) {
|
|
81
|
+
console.log('No visual verification entries recorded.');
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
console.log(`Last ${Math.min(entries.length, limit)} visual verification(s):\n`);
|
|
85
|
+
for (const entry of entries) {
|
|
86
|
+
const ts = entry.timestamp.replace('T', ' ').slice(0, 19);
|
|
87
|
+
const term = entry.terminalId ? ` T${entry.terminalId}` : '';
|
|
88
|
+
console.log(`${ts} [${entry.stage}]${term}`);
|
|
89
|
+
console.log(` ${entry.note}`);
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (args.command === 'record') {
|
|
95
|
+
if (!args.stage) {
|
|
96
|
+
console.error('Error: --record requires a stage');
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
if (!VALID_VISUAL_STAGES.includes(args.stage)) {
|
|
100
|
+
console.error(`Error: Invalid stage "${args.stage}". Valid stages: ${VALID_VISUAL_STAGES.join(', ')}`);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
if (!args.note || args.note.trim() === '') {
|
|
104
|
+
console.error('Error: --note is required and must be non-empty');
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const entry = {
|
|
109
|
+
stage: args.stage,
|
|
110
|
+
note: args.note.trim(),
|
|
111
|
+
cwd: process.cwd(),
|
|
112
|
+
command: 'ninja-visual',
|
|
113
|
+
};
|
|
114
|
+
if (args.terminalId) {
|
|
115
|
+
entry.terminalId = args.terminalId;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const record = appendVisualEntry(entry);
|
|
119
|
+
console.log(`Visual verification recorded: [${args.stage}]${args.terminalId ? ' T' + args.terminalId : ''}`);
|
|
120
|
+
console.log(` ${args.note}`);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
main();
|
package/ninja-whoami.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { SESSION_DIR, readAuthToken } = require('./lib/runtime-session');
|
|
7
|
+
|
|
8
|
+
const CREDENTIALS_FILE = path.join(SESSION_DIR, 'credentials.json');
|
|
9
|
+
|
|
10
|
+
function main() {
|
|
11
|
+
const token = readAuthToken();
|
|
12
|
+
|
|
13
|
+
if (!token) {
|
|
14
|
+
console.log('Not logged in. Run: ninja-login');
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const creds = JSON.parse(fs.readFileSync(CREDENTIALS_FILE, 'utf8'));
|
|
20
|
+
console.log(`Logged in as: ${creds.username || creds.email}`);
|
|
21
|
+
console.log(`Token: ${token.slice(0, 20)}...`);
|
|
22
|
+
} catch {
|
|
23
|
+
console.log('Logged in (token present)');
|
|
24
|
+
console.log(`Token: ${token.slice(0, 20)}...`);
|
|
25
|
+
console.log('User unknown (logged in before ninja-login existed)');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
main();
|
package/package.json
CHANGED
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ninja-terminals",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.2",
|
|
4
4
|
"description": "MCP server for multi-terminal Claude Code orchestration with DAG task management, parallel execution, and self-improvement",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"ninja-terminals": "cli.js",
|
|
8
|
-
"ninja-terminals-mcp": "mcp-server.js"
|
|
8
|
+
"ninja-terminals-mcp": "mcp-server.js",
|
|
9
|
+
"agent-send": "agent-send.js",
|
|
10
|
+
"ninja-dispatch": "agent-send.js",
|
|
11
|
+
"ninja-status": "agent-send.js",
|
|
12
|
+
"ninja-ensure": "ninja-ensure.js",
|
|
13
|
+
"ninja-visual": "ninja-visual.js",
|
|
14
|
+
"ninja-gate": "ninja-gate.js",
|
|
15
|
+
"ninja-codex-visual": "ninja-codex-visual.js",
|
|
16
|
+
"ninja-claude-visual": "ninja-claude-visual.js",
|
|
17
|
+
"ninja-codex": "ninja-codex.js",
|
|
18
|
+
"ninja-login": "ninja-login.js",
|
|
19
|
+
"ninja-logout": "ninja-logout.js",
|
|
20
|
+
"ninja-whoami": "ninja-whoami.js"
|
|
9
21
|
},
|
|
10
22
|
"scripts": {
|
|
11
23
|
"start": "node server.js",
|
|
12
|
-
"mcp": "node mcp-server.js"
|
|
24
|
+
"mcp": "node mcp-server.js",
|
|
25
|
+
"install-hooks": "node scripts/install-ninja-hooks.js"
|
|
13
26
|
},
|
|
14
27
|
"files": [
|
|
15
28
|
"lib/",
|
|
@@ -25,6 +38,16 @@
|
|
|
25
38
|
"cli.js",
|
|
26
39
|
"server.js",
|
|
27
40
|
"mcp-server.js",
|
|
41
|
+
"agent-send.js",
|
|
42
|
+
"ninja-ensure.js",
|
|
43
|
+
"ninja-visual.js",
|
|
44
|
+
"ninja-gate.js",
|
|
45
|
+
"ninja-codex-visual.js",
|
|
46
|
+
"ninja-claude-visual.js",
|
|
47
|
+
"ninja-codex.js",
|
|
48
|
+
"ninja-login.js",
|
|
49
|
+
"ninja-logout.js",
|
|
50
|
+
"ninja-whoami.js",
|
|
28
51
|
"CLAUDE.md",
|
|
29
52
|
"ORCHESTRATOR-PROMPT.md"
|
|
30
53
|
],
|