ninja-terminals 2.3.1 → 2.3.3
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 +134 -28
- 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/agent-send.js
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const http = require('http');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
const {
|
|
9
|
+
SESSION_FILE,
|
|
10
|
+
LEDGER_FILE,
|
|
11
|
+
VERIFICATION_LEDGER_FILE,
|
|
12
|
+
readRuntimeSession,
|
|
13
|
+
healthCheckSession,
|
|
14
|
+
requestJson,
|
|
15
|
+
appendLedgerEntry,
|
|
16
|
+
readLedgerEntries,
|
|
17
|
+
appendVerificationEntry,
|
|
18
|
+
readVerificationEntries,
|
|
19
|
+
} = require('./lib/runtime-session');
|
|
20
|
+
|
|
21
|
+
const USAGE = `
|
|
22
|
+
Usage:
|
|
23
|
+
agent-send --status
|
|
24
|
+
agent-send --task-status [terminal-id]
|
|
25
|
+
agent-send --ledger [N]
|
|
26
|
+
agent-send --output <terminal-id> [--lines N]
|
|
27
|
+
agent-send --verifications [N]
|
|
28
|
+
agent-send <terminal-id> <message...>
|
|
29
|
+
|
|
30
|
+
Sends a message to a Ninja Terminal via PTY stdin.
|
|
31
|
+
|
|
32
|
+
Environment:
|
|
33
|
+
NINJA_HOST Server host (default: localhost)
|
|
34
|
+
NINJA_PORT Server port (overrides ${SESSION_FILE})
|
|
35
|
+
NINJA_AUTH_TOKEN Auth token (or use ${SESSION_FILE} / ~/.ninja/token)
|
|
36
|
+
|
|
37
|
+
Files:
|
|
38
|
+
${SESSION_FILE} Runtime session (port, host, token)
|
|
39
|
+
${LEDGER_FILE} Dispatch history (NDJSON)
|
|
40
|
+
${VERIFICATION_LEDGER_FILE} Output/status verification history (NDJSON)
|
|
41
|
+
|
|
42
|
+
Examples:
|
|
43
|
+
agent-send --status # Check process status (logged as verification)
|
|
44
|
+
agent-send --task-status # Check all terminals task status
|
|
45
|
+
agent-send --task-status 1 # Check terminal 1 task status
|
|
46
|
+
agent-send --ledger # Show last 20 dispatches
|
|
47
|
+
agent-send --output 1 # Read terminal 1 output (logged as verification)
|
|
48
|
+
agent-send --output 1 --lines 50 # Read last 50 lines
|
|
49
|
+
agent-send --verifications # Show verification ledger
|
|
50
|
+
agent-send 2 "Hello from agent"
|
|
51
|
+
`;
|
|
52
|
+
|
|
53
|
+
function getToken(session) {
|
|
54
|
+
// 1. Environment variable
|
|
55
|
+
if (process.env.NINJA_AUTH_TOKEN) {
|
|
56
|
+
return process.env.NINJA_AUTH_TOKEN;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 2. Runtime session file written by Ninja Terminals after browser auth
|
|
60
|
+
if (session && session.authToken) {
|
|
61
|
+
return session.authToken;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 3. Token file
|
|
65
|
+
const tokenPath = path.join(os.homedir(), '.ninja', 'token');
|
|
66
|
+
if (fs.existsSync(tokenPath)) {
|
|
67
|
+
return fs.readFileSync(tokenPath, 'utf8').trim();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function resolveTarget(session) {
|
|
74
|
+
const port = process.env.NINJA_PORT || session?.port;
|
|
75
|
+
const host = process.env.NINJA_HOST || session?.host || 'localhost';
|
|
76
|
+
|
|
77
|
+
if (!port) {
|
|
78
|
+
throw new Error(`No live Ninja Terminal session found.
|
|
79
|
+
|
|
80
|
+
Start Ninja Terminals first. Expected runtime file:
|
|
81
|
+
${SESSION_FILE}
|
|
82
|
+
|
|
83
|
+
Or set NINJA_PORT explicitly.`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { host, port: parseInt(port, 10) };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function printStatus(session, token) {
|
|
90
|
+
const target = resolveTarget(session);
|
|
91
|
+
const health = await healthCheckSession({ ...session, ...target });
|
|
92
|
+
if (!health.ok) {
|
|
93
|
+
throw new Error(`Ninja Terminal health check failed on ${target.host}:${target.port}: ${health.error}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
console.log(`Ninja Terminal: http://${target.host}:${target.port}`);
|
|
97
|
+
console.log(`Session file: ${SESSION_FILE}`);
|
|
98
|
+
console.log(`Health: ok (${health.response.terminals} terminals)`);
|
|
99
|
+
|
|
100
|
+
if (!token) {
|
|
101
|
+
console.log('Auth: missing token. Run: ninja-login');
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const res = await requestJson({
|
|
106
|
+
host: target.host,
|
|
107
|
+
port: target.port,
|
|
108
|
+
path: '/api/terminals',
|
|
109
|
+
token,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (res.statusCode === 200 && Array.isArray(res.body)) {
|
|
113
|
+
for (const t of res.body) {
|
|
114
|
+
console.log(`T${t.id} ${t.label || ''} ${t.status || 'unknown'} ${t.taskName ? `— ${t.taskName}` : ''}`.trim());
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
console.log(`Terminals: unavailable (HTTP ${res.statusCode})`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function main() {
|
|
122
|
+
const args = process.argv.slice(2);
|
|
123
|
+
const invokedAs = path.basename(process.argv[1] || '');
|
|
124
|
+
const session = readRuntimeSession();
|
|
125
|
+
const token = getToken(session);
|
|
126
|
+
|
|
127
|
+
if (invokedAs === 'ninja-status') {
|
|
128
|
+
await printStatus(session, token);
|
|
129
|
+
appendVerificationEntry({ type: 'status', command: 'ninja-status' });
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
134
|
+
console.log(USAGE);
|
|
135
|
+
process.exit(args.length === 0 ? 1 : 0);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (args[0] === '--status' || args[0] === 'status') {
|
|
139
|
+
await printStatus(session, token);
|
|
140
|
+
appendVerificationEntry({ type: 'status', command: 'agent-send --status' });
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (args[0] === '--ledger' || args[0] === 'ledger') {
|
|
145
|
+
const limit = parseInt(args[1], 10) || 20;
|
|
146
|
+
const entries = readLedgerEntries(limit);
|
|
147
|
+
if (entries.length === 0) {
|
|
148
|
+
console.log('No dispatch ledger entries found.');
|
|
149
|
+
console.log(`Ledger file: ${LEDGER_FILE}`);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
console.log(`Last ${entries.length} dispatch(es):\n`);
|
|
153
|
+
for (const e of entries) {
|
|
154
|
+
const status = e.success ? '✓' : '✗';
|
|
155
|
+
const preview = e.messagePreview || '';
|
|
156
|
+
console.log(`${status} ${e.timestamp} T${e.terminalId} ${e.command || 'dispatch'}`);
|
|
157
|
+
if (preview) console.log(` "${preview.slice(0, 80)}${preview.length > 80 ? '...' : ''}"`);
|
|
158
|
+
if (!e.success && e.error) console.log(` Error: ${e.error}`);
|
|
159
|
+
}
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (args[0] === '--output' || args[0] === 'output') {
|
|
164
|
+
const terminalId = args[1];
|
|
165
|
+
if (!terminalId) {
|
|
166
|
+
console.error('Error: --output requires terminal ID');
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
const linesIdx = args.indexOf('--lines');
|
|
170
|
+
const lines = linesIdx >= 0 ? parseInt(args[linesIdx + 1], 10) || 50 : 50;
|
|
171
|
+
|
|
172
|
+
const target = resolveTarget(session);
|
|
173
|
+
if (!token) {
|
|
174
|
+
console.error('Error: Auth token required for output read');
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const res = await requestJson({
|
|
179
|
+
host: target.host,
|
|
180
|
+
port: target.port,
|
|
181
|
+
path: `/api/terminals/${terminalId}/output?lines=${lines}`,
|
|
182
|
+
token,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
if (res.statusCode === 200 && res.body) {
|
|
186
|
+
const output = res.body.lines || res.body;
|
|
187
|
+
if (Array.isArray(output)) {
|
|
188
|
+
for (const line of output) {
|
|
189
|
+
console.log(line);
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
console.log(JSON.stringify(res.body, null, 2));
|
|
193
|
+
}
|
|
194
|
+
appendVerificationEntry({
|
|
195
|
+
type: 'output_read',
|
|
196
|
+
terminalId,
|
|
197
|
+
lines,
|
|
198
|
+
command: 'agent-send --output',
|
|
199
|
+
});
|
|
200
|
+
} else {
|
|
201
|
+
console.error(`Error: HTTP ${res.statusCode}`);
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (args[0] === '--verifications' || args[0] === 'verifications') {
|
|
208
|
+
const limit = parseInt(args[1], 10) || 20;
|
|
209
|
+
const entries = readVerificationEntries(limit);
|
|
210
|
+
if (entries.length === 0) {
|
|
211
|
+
console.log('No verification ledger entries found.');
|
|
212
|
+
console.log(`Ledger file: ${VERIFICATION_LEDGER_FILE}`);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
console.log(`Last ${entries.length} verification(s):\n`);
|
|
216
|
+
for (const e of entries) {
|
|
217
|
+
console.log(`${e.timestamp} ${e.type} ${e.terminalId ? `T${e.terminalId}` : ''} ${e.command || ''}`.trim());
|
|
218
|
+
if (e.statusMarker) console.log(` Marker: ${e.statusMarker}`);
|
|
219
|
+
}
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (args[0] === '--task-status' || args[0] === 'task-status') {
|
|
224
|
+
const terminalId = args[1];
|
|
225
|
+
const target = resolveTarget(session);
|
|
226
|
+
|
|
227
|
+
if (!token) {
|
|
228
|
+
console.error('Error: Auth token required for task-status');
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const apiPath = terminalId
|
|
233
|
+
? `/api/terminals/${terminalId}/task-status`
|
|
234
|
+
: '/api/terminals/task-status';
|
|
235
|
+
|
|
236
|
+
const res = await requestJson({
|
|
237
|
+
host: target.host,
|
|
238
|
+
port: target.port,
|
|
239
|
+
path: apiPath,
|
|
240
|
+
token,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
if (res.statusCode === 200 && res.body) {
|
|
244
|
+
const data = Array.isArray(res.body) ? res.body : [res.body];
|
|
245
|
+
|
|
246
|
+
// Task status icons
|
|
247
|
+
const icons = {
|
|
248
|
+
done: '\u2713', // ✓
|
|
249
|
+
blocked: '\u26A0', // ⚠
|
|
250
|
+
error: '\u2717', // ✗
|
|
251
|
+
running: '\u25CB', // ○
|
|
252
|
+
pending: '\u25CB', // ○
|
|
253
|
+
unknown: '?',
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
console.log('');
|
|
257
|
+
for (const t of data) {
|
|
258
|
+
const taskIcon = icons[t.taskStatus] || '?';
|
|
259
|
+
const processLabel = (t.processStatus || 'unknown').toUpperCase();
|
|
260
|
+
const taskLabel = (t.taskStatus || 'unknown').toUpperCase();
|
|
261
|
+
console.log(`T${t.id} ${t.label || ''}`);
|
|
262
|
+
console.log(` PROCESS: ${processLabel}`);
|
|
263
|
+
console.log(` TASK: ${taskIcon} ${taskLabel}${t.message ? ` — ${t.message}` : ''}`);
|
|
264
|
+
if (t.marker) console.log(` Marker: ${t.marker}`);
|
|
265
|
+
console.log('');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
appendVerificationEntry({
|
|
269
|
+
type: 'task_status',
|
|
270
|
+
terminalId: terminalId || 'all',
|
|
271
|
+
command: 'agent-send --task-status',
|
|
272
|
+
});
|
|
273
|
+
} else {
|
|
274
|
+
console.error(`Error: HTTP ${res.statusCode}`);
|
|
275
|
+
process.exit(1);
|
|
276
|
+
}
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (args.length < 2) {
|
|
281
|
+
console.error('Error: Missing arguments. Need <terminal-id> <message>');
|
|
282
|
+
console.log(USAGE);
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const terminalId = args[0];
|
|
287
|
+
const message = args.slice(1).join(' ');
|
|
288
|
+
|
|
289
|
+
if (!/^\d+$/.test(terminalId)) {
|
|
290
|
+
console.error(`Error: terminal-id must be numeric, got: ${terminalId}`);
|
|
291
|
+
process.exit(1);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (!token) {
|
|
295
|
+
appendLedgerEntry({
|
|
296
|
+
terminalId,
|
|
297
|
+
messagePreview: message.slice(0, 160),
|
|
298
|
+
command: invokedAs === 'ninja-dispatch' ? 'ninja-dispatch' : 'agent-send',
|
|
299
|
+
success: false,
|
|
300
|
+
error: 'No auth token found',
|
|
301
|
+
});
|
|
302
|
+
console.error(`Error: No auth token found.
|
|
303
|
+
|
|
304
|
+
Run: ninja-login
|
|
305
|
+
|
|
306
|
+
Or set NINJA_AUTH_TOKEN environment variable.
|
|
307
|
+
`);
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const { host, port } = resolveTarget(session);
|
|
312
|
+
const health = await healthCheckSession({ ...session, host, port });
|
|
313
|
+
if (!health.ok) {
|
|
314
|
+
appendLedgerEntry({
|
|
315
|
+
terminalId,
|
|
316
|
+
messagePreview: message.slice(0, 160),
|
|
317
|
+
host,
|
|
318
|
+
port,
|
|
319
|
+
command: invokedAs === 'ninja-dispatch' ? 'ninja-dispatch' : 'agent-send',
|
|
320
|
+
success: false,
|
|
321
|
+
error: `Health check failed: ${health.error}`,
|
|
322
|
+
});
|
|
323
|
+
throw new Error(`Ninja Terminal health check failed on ${host}:${port}: ${health.error}`);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Build JSON body - server handles delayed Enter for proper Claude Code submission
|
|
327
|
+
const body = JSON.stringify({ text: message });
|
|
328
|
+
|
|
329
|
+
const options = {
|
|
330
|
+
hostname: host,
|
|
331
|
+
port: parseInt(port, 10),
|
|
332
|
+
path: `/api/terminals/${terminalId}/input`,
|
|
333
|
+
method: 'POST',
|
|
334
|
+
headers: {
|
|
335
|
+
'Content-Type': 'application/json',
|
|
336
|
+
'Content-Length': Buffer.byteLength(body),
|
|
337
|
+
'Authorization': `Bearer ${token}`
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const ledgerBase = {
|
|
342
|
+
terminalId,
|
|
343
|
+
messagePreview: message.slice(0, 160),
|
|
344
|
+
host,
|
|
345
|
+
port,
|
|
346
|
+
url: `http://${host}:${port}`,
|
|
347
|
+
cwd: session?.cwd || null,
|
|
348
|
+
command: invokedAs === 'ninja-dispatch' ? 'ninja-dispatch' : 'agent-send',
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
const req = http.request(options, (res) => {
|
|
352
|
+
let data = '';
|
|
353
|
+
res.on('data', chunk => { data += chunk; });
|
|
354
|
+
res.on('end', () => {
|
|
355
|
+
if (res.statusCode === 200) {
|
|
356
|
+
try {
|
|
357
|
+
const result = JSON.parse(data);
|
|
358
|
+
if (result.ok) {
|
|
359
|
+
appendLedgerEntry({ ...ledgerBase, success: true });
|
|
360
|
+
console.log(`Sent to terminal ${terminalId}`);
|
|
361
|
+
if (result.guidanceInjected) {
|
|
362
|
+
console.log('(guidance was injected)');
|
|
363
|
+
}
|
|
364
|
+
} else {
|
|
365
|
+
appendLedgerEntry({ ...ledgerBase, success: false, error: `Server rejected: ${data}` });
|
|
366
|
+
console.error('Server returned:', data);
|
|
367
|
+
process.exit(1);
|
|
368
|
+
}
|
|
369
|
+
} catch {
|
|
370
|
+
appendLedgerEntry({ ...ledgerBase, success: true });
|
|
371
|
+
console.log('Response:', data);
|
|
372
|
+
}
|
|
373
|
+
} else {
|
|
374
|
+
appendLedgerEntry({ ...ledgerBase, success: false, error: `HTTP ${res.statusCode}: ${data}` });
|
|
375
|
+
console.error(`HTTP ${res.statusCode}: ${data}`);
|
|
376
|
+
process.exit(1);
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
req.on('error', (err) => {
|
|
382
|
+
appendLedgerEntry({ ...ledgerBase, success: false, error: err.message });
|
|
383
|
+
console.error(`Connection error: ${err.message}`);
|
|
384
|
+
console.error(`Is Ninja Terminal running on ${host}:${port}?`);
|
|
385
|
+
process.exit(1);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
req.write(body);
|
|
389
|
+
req.end();
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
main().catch((err) => {
|
|
393
|
+
console.error(`Error: ${err.message}`);
|
|
394
|
+
process.exit(1);
|
|
395
|
+
});
|
package/cli.js
CHANGED
|
@@ -54,24 +54,43 @@ if (hasFlag('--version') || hasFlag('-v')) {
|
|
|
54
54
|
|
|
55
55
|
// ── Setup command ───────────────────────────────────────────
|
|
56
56
|
if (hasFlag('--setup')) {
|
|
57
|
+
runSetup().then(() => process.exit(0)).catch(e => { console.error(e); process.exit(1); });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function runSetup() {
|
|
57
61
|
const fs = require('fs');
|
|
58
62
|
const path = require('path');
|
|
59
63
|
const os = require('os');
|
|
60
64
|
|
|
61
65
|
console.log('\n🥷 NINJA TERMINALS SETUP\n');
|
|
62
66
|
|
|
63
|
-
// 1. Find or create .
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
const
|
|
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
|
+
}
|
|
67
86
|
|
|
68
87
|
let mcpConfig = { mcpServers: {} };
|
|
69
|
-
if (fs.existsSync(
|
|
88
|
+
if (fs.existsSync(settingsPath)) {
|
|
70
89
|
try {
|
|
71
|
-
mcpConfig = JSON.parse(fs.readFileSync(
|
|
90
|
+
mcpConfig = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
72
91
|
if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
|
|
73
92
|
} catch (e) {
|
|
74
|
-
console.log(`⚠️ Could not parse ${
|
|
93
|
+
console.log(`⚠️ Could not parse ${settingsPath}, creating new config`);
|
|
75
94
|
}
|
|
76
95
|
}
|
|
77
96
|
|
|
@@ -85,11 +104,16 @@ if (hasFlag('--setup')) {
|
|
|
85
104
|
}
|
|
86
105
|
};
|
|
87
106
|
|
|
88
|
-
// Get npm root for copying orchestrator prompt
|
|
89
|
-
|
|
107
|
+
// Get npm root for copying orchestrator prompt (works in dev and installed mode)
|
|
108
|
+
let npmRoot;
|
|
109
|
+
try {
|
|
110
|
+
npmRoot = path.dirname(require.resolve('ninja-terminals/package.json'));
|
|
111
|
+
} catch {
|
|
112
|
+
npmRoot = __dirname; // Dev mode fallback
|
|
113
|
+
}
|
|
90
114
|
|
|
91
|
-
fs.writeFileSync(
|
|
92
|
-
console.log(`✅ Added ninja-terminals to ${
|
|
115
|
+
fs.writeFileSync(settingsPath, JSON.stringify(mcpConfig, null, 2) + '\n');
|
|
116
|
+
console.log(`✅ Added ninja-terminals to ${settingsPath}`);
|
|
93
117
|
|
|
94
118
|
// 3. Copy orchestrator prompt to CLAUDE.md
|
|
95
119
|
const claudeMd = path.join(process.cwd(), 'CLAUDE.md');
|
|
@@ -139,7 +163,7 @@ if (hasFlag('--setup')) {
|
|
|
139
163
|
}
|
|
140
164
|
|
|
141
165
|
// Save updated config with all MCPs
|
|
142
|
-
fs.writeFileSync(
|
|
166
|
+
fs.writeFileSync(settingsPath, JSON.stringify(mcpConfig, null, 2) + '\n');
|
|
143
167
|
|
|
144
168
|
// 5. Check for Claude in Chrome (optional but recommended)
|
|
145
169
|
const chromeExt = mcpConfig.mcpServers['claude-in-chrome'];
|
|
@@ -151,28 +175,108 @@ if (hasFlag('--setup')) {
|
|
|
151
175
|
|
|
152
176
|
console.log(`
|
|
153
177
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
178
|
+
✅ MCP configured!
|
|
179
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
180
|
+
`);
|
|
181
|
+
|
|
182
|
+
// Now prompt for login
|
|
183
|
+
const readline = require('readline');
|
|
184
|
+
const https = require('https');
|
|
185
|
+
const { execSync } = require('child_process');
|
|
186
|
+
const { writeAuthToken } = require('./lib/runtime-session');
|
|
187
|
+
|
|
188
|
+
const BACKEND_URL = process.env.NINJA_BACKEND_URL || 'https://emtchat-backend.onrender.com';
|
|
189
|
+
|
|
190
|
+
function prompt(question) {
|
|
191
|
+
return new Promise((resolve) => {
|
|
192
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
193
|
+
rl.question(question, (answer) => { rl.close(); resolve(answer); });
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function promptPassword(question) {
|
|
198
|
+
return new Promise((resolve) => {
|
|
199
|
+
try { execSync('stty -echo', { stdio: 'pipe' }); } catch {}
|
|
200
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
201
|
+
process.stdout.write(question);
|
|
202
|
+
rl.question('', (answer) => {
|
|
203
|
+
try { execSync('stty echo', { stdio: 'pipe' }); } catch {}
|
|
204
|
+
console.log();
|
|
205
|
+
rl.close();
|
|
206
|
+
resolve(answer);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function postJson(url, body) {
|
|
212
|
+
return new Promise((resolve, reject) => {
|
|
213
|
+
const parsed = new URL(url);
|
|
214
|
+
const payload = JSON.stringify(body);
|
|
215
|
+
const req = https.request({
|
|
216
|
+
hostname: parsed.hostname,
|
|
217
|
+
port: parsed.port || 443,
|
|
218
|
+
path: parsed.pathname,
|
|
219
|
+
method: 'POST',
|
|
220
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) },
|
|
221
|
+
}, (res) => {
|
|
222
|
+
let data = '';
|
|
223
|
+
res.on('data', chunk => { data += chunk; });
|
|
224
|
+
res.on('end', () => {
|
|
225
|
+
try { resolve({ status: res.statusCode, body: JSON.parse(data) }); }
|
|
226
|
+
catch { resolve({ status: res.statusCode, body: data }); }
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
req.on('error', reject);
|
|
230
|
+
req.write(payload);
|
|
231
|
+
req.end();
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function doLogin() {
|
|
236
|
+
console.log('Login to your Ninja Terminals account:\n');
|
|
237
|
+
const username = await prompt('Username or email: ');
|
|
238
|
+
const password = await promptPassword('Password: ');
|
|
239
|
+
|
|
240
|
+
if (!username || !password) {
|
|
241
|
+
console.log('\n⚠️ Skipped login. Run `npx ninja-login` later to authenticate.');
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
console.log('Authenticating...');
|
|
246
|
+
try {
|
|
247
|
+
const res = await postJson(`${BACKEND_URL}/api/auth/login`, { username, password });
|
|
248
|
+
if (res.status === 200 && res.body.token) {
|
|
249
|
+
writeAuthToken(res.body.token);
|
|
250
|
+
console.log(`\n✅ Logged in as ${username}`);
|
|
251
|
+
console.log(` Token saved to ~/.ninja/token`);
|
|
252
|
+
return true;
|
|
253
|
+
} else {
|
|
254
|
+
console.log(`\n❌ Login failed: ${res.body.message || res.body.error || 'Unknown error'}`);
|
|
255
|
+
console.log(' Run `npx ninja-login` to try again.');
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
} catch (err) {
|
|
259
|
+
console.log(`\n❌ Connection failed: ${err.message}`);
|
|
260
|
+
console.log(' Run `npx ninja-login` to try again.');
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
await doLogin();
|
|
266
|
+
|
|
267
|
+
console.log(`
|
|
268
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
154
269
|
✨ Setup complete!
|
|
155
270
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
• playwright - browser automation (screenshots, clicks, reading)
|
|
159
|
-
• fetch - API calls to /api/terminals
|
|
160
|
-
|
|
161
|
-
Next steps:
|
|
162
|
-
1. Restart Claude Code to load MCP servers
|
|
163
|
-
2. Run: npx ninja-terminals
|
|
164
|
-
3. Or use MCP tools directly in Claude Code
|
|
165
|
-
|
|
166
|
-
MCP tools available after restart:
|
|
167
|
-
mcp__ninja-terminals__spawn_terminal
|
|
168
|
-
mcp__ninja-terminals__send_input
|
|
169
|
-
mcp__ninja-terminals__list_terminals
|
|
170
|
-
... and 9 more
|
|
271
|
+
Next: Restart Claude Code, then tell Claude:
|
|
272
|
+
"use ninja terminals"
|
|
171
273
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
172
274
|
`);
|
|
173
|
-
process.exit(0);
|
|
174
275
|
}
|
|
175
276
|
|
|
277
|
+
// If setup mode was requested, we're done (setup function calls process.exit)
|
|
278
|
+
if (!hasFlag('--setup')) {
|
|
279
|
+
|
|
176
280
|
const port = parseInt(getArg('--port', '3300'), 10);
|
|
177
281
|
const terminals = parseInt(getArg('--terminals', '2'), 10); // Free tier default
|
|
178
282
|
const cwd = getArg('--cwd', process.cwd());
|
|
@@ -252,3 +356,5 @@ setTimeout(() => {
|
|
|
252
356
|
// ── Start the server ─────────────────────────────────────────
|
|
253
357
|
|
|
254
358
|
require('./server.js');
|
|
359
|
+
|
|
360
|
+
} // end if (!hasFlag('--setup'))
|