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/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
|
@@ -60,18 +60,33 @@ if (hasFlag('--setup')) {
|
|
|
60
60
|
|
|
61
61
|
console.log('\n🥷 NINJA TERMINALS SETUP\n');
|
|
62
62
|
|
|
63
|
-
// 1. Find or create .
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
const
|
|
63
|
+
// 1. Find or create .claude/settings.json (Claude Code format)
|
|
64
|
+
const projectClaudeDir = path.join(process.cwd(), '.claude');
|
|
65
|
+
const globalClaudeDir = path.join(os.homedir(), '.claude');
|
|
66
|
+
const projectSettings = path.join(projectClaudeDir, 'settings.json');
|
|
67
|
+
const globalSettings = path.join(globalClaudeDir, 'settings.json');
|
|
68
|
+
|
|
69
|
+
// Prefer project-level if .claude dir exists, else use global
|
|
70
|
+
let settingsPath, claudeDir;
|
|
71
|
+
if (fs.existsSync(projectClaudeDir)) {
|
|
72
|
+
settingsPath = projectSettings;
|
|
73
|
+
claudeDir = projectClaudeDir;
|
|
74
|
+
} else {
|
|
75
|
+
settingsPath = globalSettings;
|
|
76
|
+
claudeDir = globalClaudeDir;
|
|
77
|
+
// Create global .claude dir if needed
|
|
78
|
+
if (!fs.existsSync(claudeDir)) {
|
|
79
|
+
fs.mkdirSync(claudeDir, { recursive: true });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
67
82
|
|
|
68
83
|
let mcpConfig = { mcpServers: {} };
|
|
69
|
-
if (fs.existsSync(
|
|
84
|
+
if (fs.existsSync(settingsPath)) {
|
|
70
85
|
try {
|
|
71
|
-
mcpConfig = JSON.parse(fs.readFileSync(
|
|
86
|
+
mcpConfig = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
72
87
|
if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
|
|
73
88
|
} catch (e) {
|
|
74
|
-
console.log(`⚠️ Could not parse ${
|
|
89
|
+
console.log(`⚠️ Could not parse ${settingsPath}, creating new config`);
|
|
75
90
|
}
|
|
76
91
|
}
|
|
77
92
|
|
|
@@ -88,8 +103,8 @@ if (hasFlag('--setup')) {
|
|
|
88
103
|
// Get npm root for copying orchestrator prompt
|
|
89
104
|
const npmRoot = path.dirname(require.resolve('ninja-terminals/package.json'));
|
|
90
105
|
|
|
91
|
-
fs.writeFileSync(
|
|
92
|
-
console.log(`✅ Added ninja-terminals to ${
|
|
106
|
+
fs.writeFileSync(settingsPath, JSON.stringify(mcpConfig, null, 2) + '\n');
|
|
107
|
+
console.log(`✅ Added ninja-terminals to ${settingsPath}`);
|
|
93
108
|
|
|
94
109
|
// 3. Copy orchestrator prompt to CLAUDE.md
|
|
95
110
|
const claudeMd = path.join(process.cwd(), 'CLAUDE.md');
|
|
@@ -139,7 +154,7 @@ if (hasFlag('--setup')) {
|
|
|
139
154
|
}
|
|
140
155
|
|
|
141
156
|
// Save updated config with all MCPs
|
|
142
|
-
fs.writeFileSync(
|
|
157
|
+
fs.writeFileSync(settingsPath, JSON.stringify(mcpConfig, null, 2) + '\n');
|
|
143
158
|
|
|
144
159
|
// 5. Check for Claude in Chrome (optional but recommended)
|
|
145
160
|
const chromeExt = mcpConfig.mcpServers['claude-in-chrome'];
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VeryRealNames - Absurd but plausible name generator
|
|
3
|
+
* Names sound like they COULD be real... but aren't
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const FIRST_NAMES = [
|
|
7
|
+
// Old-timey gems
|
|
8
|
+
'Hank', 'Gertrude', 'Chester', 'Mildred', 'Barnaby', 'Ethel', 'Cornelius', 'Bertha',
|
|
9
|
+
'Thaddeus', 'Gladys', 'Reginald', 'Edna', 'Archibald', 'Myrtle', 'Beauregard', 'Agatha',
|
|
10
|
+
'Wilbur', 'Prudence', 'Mortimer', 'Hortense', 'Bartholomew', 'Eunice', 'Horatio', 'Blanche',
|
|
11
|
+
'Cuthbert', 'Delphine', 'Engelbert', 'Geraldine', 'Humphrey', 'Imogene',
|
|
12
|
+
// Slightly unusual but real
|
|
13
|
+
'Grover', 'Cletus', 'Melvin', 'Dorcas', 'Floyd', 'Beulah', 'Lester', 'Opal',
|
|
14
|
+
'Merle', 'Velma', 'Elmer', 'Lurleen', 'Durwood', 'Wanda', 'Gus', 'Bernice',
|
|
15
|
+
'Norbert', 'Doreen', 'Seymour', 'Fern', 'Murray', 'Irma', 'Roscoe', 'Hazel',
|
|
16
|
+
// Fancy yet awkward
|
|
17
|
+
'Percival', 'Millicent', 'Fitzgerald', 'Clementine', 'Montgomery', 'Josephine',
|
|
18
|
+
'Wellington', 'Cordelia', 'Pemberton', 'Henrietta', 'Thurgood', 'Philomena',
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const LAST_NAMES = [
|
|
22
|
+
// Food-adjacent
|
|
23
|
+
'Pancely', 'Biscuitson', 'Crumbsworth', 'Gravyton', 'Stewbottom', 'Butterham',
|
|
24
|
+
'Cheesley', 'Meatworth', 'Croutonski', 'Souperton', 'Hamsworth', 'Loafman',
|
|
25
|
+
'Brisketson', 'Cabbagely', 'Noodleman', 'Porridge', 'Beefington', 'Dumpling',
|
|
26
|
+
// Animal-infused
|
|
27
|
+
'Wombleton', 'Badgerly', 'Gooseman', 'Ducksworth', 'Weaselton', 'Moosebury',
|
|
28
|
+
'Ferretson', 'Hedgewood', 'Squirrelman', 'Toadsworth', 'Pigglesworth', 'Newterson',
|
|
29
|
+
'Crabshaw', 'Lobsterman', 'Clamsworthy', 'Oysterton',
|
|
30
|
+
// Body part blends
|
|
31
|
+
'Finklebottom', 'Rumplesworth', 'Kneely', 'Elbowton', 'Thumbleton', 'Noseberg',
|
|
32
|
+
'Earlington', 'Toesworth', 'Chinley', 'Knuckleton', 'Shoulderby', 'Ankleman',
|
|
33
|
+
// Silly suffixes
|
|
34
|
+
'Wobbleford', 'Snickerton', 'Puddlesworth', 'Bumblesby', 'Giggleston', 'Fumbleby',
|
|
35
|
+
'Tumblewood', 'Mumbleston', 'Bumbleworth', 'Crumbleford', 'Stumblebrook',
|
|
36
|
+
// Object-based
|
|
37
|
+
'Lampsworth', 'Chairington', 'Tableson', 'Doorknobby', 'Carpetman', 'Curtainsby',
|
|
38
|
+
'Bucketworth', 'Brickleton', 'Plungerby', 'Sockington',
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
export interface GeneratedName {
|
|
42
|
+
firstName: string;
|
|
43
|
+
lastName: string;
|
|
44
|
+
fullName: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Generate a single absurd-but-plausible name
|
|
49
|
+
*/
|
|
50
|
+
export function generateName(): GeneratedName {
|
|
51
|
+
const firstName = FIRST_NAMES[Math.floor(Math.random() * FIRST_NAMES.length)];
|
|
52
|
+
const lastName = LAST_NAMES[Math.floor(Math.random() * LAST_NAMES.length)];
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
firstName,
|
|
56
|
+
lastName,
|
|
57
|
+
fullName: `${firstName} ${lastName}`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Generate multiple unique names
|
|
63
|
+
*/
|
|
64
|
+
export function generateNames(count: number): GeneratedName[] {
|
|
65
|
+
const names: GeneratedName[] = [];
|
|
66
|
+
const seen = new Set<string>();
|
|
67
|
+
|
|
68
|
+
const maxPossible = FIRST_NAMES.length * LAST_NAMES.length;
|
|
69
|
+
const targetCount = Math.min(count, maxPossible);
|
|
70
|
+
|
|
71
|
+
while (names.length < targetCount) {
|
|
72
|
+
const name = generateName();
|
|
73
|
+
if (!seen.has(name.fullName)) {
|
|
74
|
+
seen.add(name.fullName);
|
|
75
|
+
names.push(name);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return names;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get all available first names
|
|
84
|
+
*/
|
|
85
|
+
export function getFirstNames(): readonly string[] {
|
|
86
|
+
return FIRST_NAMES;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get all available last names
|
|
91
|
+
*/
|
|
92
|
+
export function getLastNames(): readonly string[] {
|
|
93
|
+
return LAST_NAMES;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get total possible unique combinations
|
|
98
|
+
*/
|
|
99
|
+
export function getTotalCombinations(): number {
|
|
100
|
+
return FIRST_NAMES.length * LAST_NAMES.length;
|
|
101
|
+
}
|
package/lib/pre-dispatch.js
CHANGED
|
@@ -41,11 +41,13 @@ async function loadToolRatings() {
|
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
43
|
* Generate tool guidance strings from ratings.
|
|
44
|
+
* Limited to top 5 warnings + top 5 recommendations to avoid flooding terminals.
|
|
44
45
|
* @param {Map<string, object>} ratings
|
|
45
46
|
* @returns {string[]}
|
|
46
47
|
*/
|
|
47
48
|
function generateToolGuidance(ratings) {
|
|
48
|
-
const
|
|
49
|
+
const warnings = [];
|
|
50
|
+
const recommendations = [];
|
|
49
51
|
|
|
50
52
|
for (const [tool, stats] of ratings) {
|
|
51
53
|
const { rating, composite, success_rate } = stats;
|
|
@@ -54,16 +56,24 @@ function generateToolGuidance(ratings) {
|
|
|
54
56
|
// Low-rated tool: suggest avoidance
|
|
55
57
|
const alt = TOOL_ALTERNATIVES[tool];
|
|
56
58
|
if (alt) {
|
|
57
|
-
|
|
59
|
+
warnings.push({ tool, msg: `Avoid ${tool} for ${alt.useCase}, prefer ${alt.preferred} (${rating} rating)`, composite });
|
|
58
60
|
} else {
|
|
59
|
-
|
|
61
|
+
warnings.push({ tool, msg: `Use ${tool} cautiously — ${rating} rating, success: ${(success_rate * 100).toFixed(0)}%`, composite });
|
|
60
62
|
}
|
|
61
63
|
} else if (rating === 'S' || rating === 'A') {
|
|
62
64
|
// High-rated tool: recommend preference
|
|
63
|
-
|
|
65
|
+
recommendations.push({ tool, msg: `Prefer ${tool} — ${rating} rating`, composite });
|
|
64
66
|
}
|
|
65
67
|
}
|
|
66
68
|
|
|
69
|
+
// Sort and limit: worst 5 warnings, best 5 recommendations
|
|
70
|
+
warnings.sort((a, b) => a.composite - b.composite);
|
|
71
|
+
recommendations.sort((a, b) => b.composite - a.composite);
|
|
72
|
+
|
|
73
|
+
const guidance = [];
|
|
74
|
+
for (const w of warnings.slice(0, 5)) guidance.push(w.msg);
|
|
75
|
+
for (const r of recommendations.slice(0, 5)) guidance.push(r.msg);
|
|
76
|
+
|
|
67
77
|
return guidance;
|
|
68
78
|
}
|
|
69
79
|
|