let-them-talk 4.3.0 → 5.2.0

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/cli.js CHANGED
@@ -1,589 +1,1089 @@
1
- #!/usr/bin/env node
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
- const os = require('os');
6
- const { execSync } = require('child_process');
7
-
8
- const command = process.argv[2];
9
-
10
- function printUsage() {
11
- console.log(`
12
- Let Them Talk — Agent Bridge v4.3.0
13
- MCP message broker for inter-agent communication
14
- Supports: Claude Code, Gemini CLI, Codex CLI, Ollama
15
-
16
- Usage:
17
- npx let-them-talk init Auto-detect CLI and configure MCP
18
- npx let-them-talk init --claude Configure for Claude Code
19
- npx let-them-talk init --gemini Configure for Gemini CLI
20
- npx let-them-talk init --codex Configure for Codex CLI
21
- npx let-them-talk init --all Configure for all supported CLIs
22
- npx let-them-talk init --ollama Setup Ollama agent bridge (local LLM)
23
- npx let-them-talk init --template T Initialize with a team template (pair, team, review, debate, ollama)
24
- npx let-them-talk templates List available agent templates
25
- npx let-them-talk dashboard Launch the web dashboard (http://localhost:3000)
26
- npx let-them-talk dashboard --lan Launch dashboard accessible on LAN (phone/tablet)
27
- npx let-them-talk reset Clear all conversation data
28
- npx let-them-talk msg <agent> <text> Send a message to an agent
29
- npx let-them-talk status Show active agents and message count
30
- npx let-them-talk help Show this help message
31
- `);
32
- }
33
-
34
- // Detect which CLIs are installed
35
- function detectCLIs() {
36
- const detected = [];
37
- const home = os.homedir();
38
-
39
- // Claude Code: ~/.claude/ directory exists
40
- if (fs.existsSync(path.join(home, '.claude'))) {
41
- detected.push('claude');
42
- }
43
-
44
- // Gemini CLI: ~/.gemini/ or GEMINI_API_KEY set
45
- if (fs.existsSync(path.join(home, '.gemini')) || process.env.GEMINI_API_KEY) {
46
- detected.push('gemini');
47
- }
48
-
49
- // Codex CLI: ~/.codex/ directory exists
50
- if (fs.existsSync(path.join(home, '.codex'))) {
51
- detected.push('codex');
52
- }
53
-
54
- return detected;
55
- }
56
-
57
- // Detect Ollama installation
58
- function detectOllama() {
59
- try {
60
- const version = execSync('ollama --version', { encoding: 'utf8', timeout: 5000 }).trim();
61
- return { installed: true, version };
62
- } catch {
63
- return { installed: false };
64
- }
65
- }
66
-
67
- // The data directory where all agents read/write — must be the same for server + dashboard
68
- function dataDir(cwd) {
69
- return path.join(cwd, '.agent-bridge').replace(/\\/g, '/');
70
- }
71
-
72
- // Configure for Claude Code (.mcp.json in project root)
73
- function setupClaude(serverPath, cwd) {
74
- const mcpConfigPath = path.join(cwd, '.mcp.json');
75
- let mcpConfig = { mcpServers: {} };
76
- if (fs.existsSync(mcpConfigPath)) {
77
- try {
78
- mcpConfig = JSON.parse(fs.readFileSync(mcpConfigPath, 'utf8'));
79
- if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
80
- } catch {
81
- // Backup corrupted file before overwriting
82
- const backup = mcpConfigPath + '.backup';
83
- fs.copyFileSync(mcpConfigPath, backup);
84
- console.log(' [warn] Existing .mcp.json was invalid — backed up to .mcp.json.backup');
85
- }
86
- }
87
-
88
- mcpConfig.mcpServers['agent-bridge'] = {
89
- command: 'node',
90
- args: [serverPath],
91
- timeout: 300,
92
- env: { AGENT_BRIDGE_DATA_DIR: dataDir(cwd) },
93
- };
94
-
95
- fs.writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2) + '\n');
96
- console.log(' [ok] Claude Code: .mcp.json updated');
97
- }
98
-
99
- // Configure for Gemini CLI (.gemini/settings.json or GEMINI.md with MCP config)
100
- function setupGemini(serverPath, cwd) {
101
- // Gemini CLI uses .gemini/settings.json for MCP configuration
102
- const geminiDir = path.join(cwd, '.gemini');
103
- const settingsPath = path.join(geminiDir, 'settings.json');
104
-
105
- if (!fs.existsSync(geminiDir)) {
106
- fs.mkdirSync(geminiDir, { recursive: true });
107
- }
108
-
109
- let settings = { mcpServers: {} };
110
- if (fs.existsSync(settingsPath)) {
111
- try {
112
- settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
113
- if (!settings.mcpServers) settings.mcpServers = {};
114
- } catch {
115
- const backup = settingsPath + '.backup';
116
- fs.copyFileSync(settingsPath, backup);
117
- console.log(' [warn] Existing settings.json was invalid — backed up to settings.json.backup');
118
- }
119
- }
120
-
121
- settings.mcpServers['agent-bridge'] = {
122
- command: 'node',
123
- args: [serverPath],
124
- timeout: 300,
125
- trust: true,
126
- env: { AGENT_BRIDGE_DATA_DIR: dataDir(cwd) },
127
- };
128
-
129
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
130
- console.log(' [ok] Gemini CLI: .gemini/settings.json updated');
131
- }
132
-
133
- // Configure for Codex CLI (uses .codex/config.toml)
134
- function setupCodex(serverPath, cwd) {
135
- const codexDir = path.join(cwd, '.codex');
136
- const configPath = path.join(codexDir, 'config.toml');
137
-
138
- if (!fs.existsSync(codexDir)) {
139
- fs.mkdirSync(codexDir, { recursive: true });
140
- }
141
-
142
- // Read existing config or start fresh
143
- let config = '';
144
- if (fs.existsSync(configPath)) {
145
- config = fs.readFileSync(configPath, 'utf8');
146
- }
147
-
148
- // Only add if not already present
149
- if (!config.includes('[mcp_servers.agent-bridge]')) {
150
- const tomlBlock = `
151
- [mcp_servers.agent-bridge]
152
- command = "node"
153
- args = [${JSON.stringify(serverPath)}]
154
- timeout = 300
155
-
156
- [mcp_servers.agent-bridge.env]
157
- AGENT_BRIDGE_DATA_DIR = ${JSON.stringify(dataDir(cwd))}
158
- `;
159
- config += tomlBlock;
160
- fs.writeFileSync(configPath, config);
161
- }
162
-
163
- console.log(' [ok] Codex CLI: .codex/config.toml updated');
164
- }
165
-
166
- // Setup Ollama agent bridge script
167
- function setupOllama(serverPath, cwd) {
168
- const dir = dataDir(cwd);
169
- const scriptPath = path.join(cwd, '.agent-bridge', 'ollama-agent.js');
170
-
171
- if (!fs.existsSync(path.join(cwd, '.agent-bridge'))) {
172
- fs.mkdirSync(path.join(cwd, '.agent-bridge'), { recursive: true });
173
- }
174
-
175
- const script = `#!/usr/bin/env node
176
- // ollama-agent.js - bridges Ollama to Let Them Talk
177
- // Usage: node .agent-bridge/ollama-agent.js [agent-name] [model]
178
- const fs = require('fs'), path = require('path'), http = require('http');
179
- const DATA_DIR = path.join(__dirname);
180
- const name = process.argv[2] || 'Ollama';
181
- const model = process.argv[3] || 'llama3';
182
- const OLLAMA_URL = process.env.OLLAMA_URL || 'http://localhost:11434';
183
-
184
- function readJson(f) { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch { return {}; } }
185
- function readJsonl(f) { if (!fs.existsSync(f)) return []; return fs.readFileSync(f, 'utf8').split('\\n').filter(l => l.trim()).map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean); }
186
-
187
- // Register agent
188
- function register() {
189
- const agentsFile = path.join(DATA_DIR, 'agents.json');
190
- const agents = readJson(agentsFile);
191
- agents[name] = { pid: process.pid, timestamp: new Date().toISOString(), last_activity: new Date().toISOString(), provider: 'Ollama (' + model + ')' };
192
- fs.writeFileSync(agentsFile, JSON.stringify(agents, null, 2));
193
- console.log('[' + name + '] Registered (PID ' + process.pid + ', model: ' + model + ')');
194
- }
195
-
196
- // Update heartbeat
197
- function heartbeat() {
198
- const agentsFile = path.join(DATA_DIR, 'agents.json');
199
- const agents = readJson(agentsFile);
200
- if (agents[name]) {
201
- agents[name].last_activity = new Date().toISOString();
202
- agents[name].pid = process.pid;
203
- fs.writeFileSync(agentsFile, JSON.stringify(agents, null, 2));
204
- }
205
- }
206
-
207
- // Call Ollama API
208
- function callOllama(prompt) {
209
- return new Promise(function(resolve, reject) {
210
- const url = new URL(OLLAMA_URL + '/api/chat');
211
- const body = JSON.stringify({ model: model, messages: [{ role: 'user', content: prompt }], stream: false });
212
- const req = http.request(url, { method: 'POST', headers: { 'Content-Type': 'application/json' } }, function(res) {
213
- let data = '';
214
- res.on('data', function(c) { data += c; });
215
- res.on('end', function() {
216
- try { const j = JSON.parse(data); resolve(j.message ? j.message.content : data); }
217
- catch { resolve(data); }
218
- });
219
- });
220
- req.on('error', reject);
221
- req.write(body);
222
- req.end();
223
- });
224
- }
225
-
226
- // Send a message
227
- function sendMessage(to, content) {
228
- const msgId = 'm' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
229
- const msg = { id: msgId, from: name, to: to, content: content, timestamp: new Date().toISOString() };
230
- fs.appendFileSync(path.join(DATA_DIR, 'messages.jsonl'), JSON.stringify(msg) + '\\n');
231
- fs.appendFileSync(path.join(DATA_DIR, 'history.jsonl'), JSON.stringify(msg) + '\\n');
232
- console.log('[' + name + '] -> ' + to + ': ' + content.substring(0, 80) + (content.length > 80 ? '...' : ''));
233
- }
234
-
235
- // Listen for messages
236
- let lastOffset = 0;
237
- function checkMessages() {
238
- const consumedFile = path.join(DATA_DIR, 'consumed-' + name + '.json');
239
- const consumed = readJson(consumedFile);
240
- lastOffset = consumed.offset || 0;
241
-
242
- const messages = readJsonl(path.join(DATA_DIR, 'messages.jsonl'));
243
- const newMsgs = messages.slice(lastOffset).filter(function(m) {
244
- return m.to === name || (m.to === 'all' && m.from !== name);
245
- });
246
-
247
- if (newMsgs.length > 0) {
248
- consumed.offset = messages.length;
249
- fs.writeFileSync(consumedFile, JSON.stringify(consumed));
250
- }
251
-
252
- return newMsgs;
253
- }
254
-
255
- async function processMessages() {
256
- const msgs = checkMessages();
257
- for (const m of msgs) {
258
- console.log('[' + name + '] <- ' + m.from + ': ' + m.content.substring(0, 80));
259
- try {
260
- const response = await callOllama(m.content);
261
- sendMessage(m.from, response);
262
- } catch (e) {
263
- sendMessage(m.from, 'Error calling Ollama: ' + e.message);
264
- }
265
- }
266
- }
267
-
268
- // Main loop
269
- register();
270
- const hb = setInterval(heartbeat, 10000);
271
- hb.unref();
272
- console.log('[' + name + '] Listening for messages... (Ctrl+C to stop)');
273
- setInterval(processMessages, 2000);
274
-
275
- // Cleanup on exit
276
- process.on('SIGINT', function() { console.log('\\n[' + name + '] Shutting down.'); process.exit(0); });
277
- `;
278
-
279
- fs.writeFileSync(scriptPath, script);
280
- console.log(' [ok] Ollama agent script created: .agent-bridge/ollama-agent.js');
281
- console.log('');
282
- console.log(' Launch an Ollama agent with:');
283
- console.log(' node .agent-bridge/ollama-agent.js <name> <model>');
284
- console.log('');
285
- console.log(' Examples:');
286
- console.log(' node .agent-bridge/ollama-agent.js Ollama llama3');
287
- console.log(' node .agent-bridge/ollama-agent.js Coder codellama');
288
- console.log(' node .agent-bridge/ollama-agent.js Writer mistral');
289
- }
290
-
291
- function init() {
292
- const cwd = process.cwd();
293
- const serverPath = path.join(__dirname, 'server.js').replace(/\\/g, '/');
294
- const gitignorePath = path.join(cwd, '.gitignore');
295
- const flag = process.argv[3];
296
-
297
- console.log('');
298
- console.log(' Let Them Talk — Initializing Agent Bridge');
299
- console.log(' ==========================================');
300
- console.log('');
301
-
302
- let targets = [];
303
-
304
- if (flag === '--claude') {
305
- targets = ['claude'];
306
- } else if (flag === '--gemini') {
307
- targets = ['gemini'];
308
- } else if (flag === '--codex') {
309
- targets = ['codex'];
310
- } else if (flag === '--all') {
311
- targets = ['claude', 'gemini', 'codex'];
312
- } else if (flag === '--ollama') {
313
- const ollama = detectOllama();
314
- if (!ollama.installed) {
315
- console.log(' Ollama not found. Install it from: https://ollama.com/download');
316
- console.log(' After installing, run: ollama pull llama3');
317
- console.log('');
318
- } else {
319
- console.log(' Ollama detected: ' + ollama.version);
320
- setupOllama(serverPath, cwd);
321
- }
322
- targets = detectCLIs();
323
- if (targets.length === 0) targets = ['claude'];
324
- } else {
325
- // Auto-detect
326
- targets = detectCLIs();
327
- if (targets.length === 0) {
328
- // Default to claude if nothing detected
329
- targets = ['claude'];
330
- console.log(' No CLI detected, defaulting to Claude Code config.');
331
- } else {
332
- console.log(` Detected CLI(s): ${targets.join(', ')}`);
333
- }
334
- }
335
-
336
- console.log('');
337
-
338
- for (const target of targets) {
339
- switch (target) {
340
- case 'claude': setupClaude(serverPath, cwd); break;
341
- case 'gemini': setupGemini(serverPath, cwd); break;
342
- case 'codex': setupCodex(serverPath, cwd); break;
343
- }
344
- }
345
-
346
- // Add .agent-bridge/ and MCP config files to .gitignore
347
- const gitignoreEntries = ['.agent-bridge/', '.mcp.json', '.codex/', '.gemini/'];
348
- if (fs.existsSync(gitignorePath)) {
349
- let content = fs.readFileSync(gitignorePath, 'utf8');
350
- const missing = gitignoreEntries.filter(e => !content.includes(e));
351
- if (missing.length) {
352
- content += '\n# Agent Bridge (auto-added by let-them-talk init)\n' + missing.join('\n') + '\n';
353
- fs.writeFileSync(gitignorePath, content);
354
- console.log(' [ok] Added to .gitignore: ' + missing.join(', '));
355
- } else {
356
- console.log(' [ok] .gitignore already configured');
357
- }
358
- } else {
359
- fs.writeFileSync(gitignorePath, '# Agent Bridge (auto-added by let-them-talk init)\n' + gitignoreEntries.join('\n') + '\n');
360
- console.log(' [ok] .gitignore created');
361
- }
362
-
363
- console.log('');
364
- console.log(' Agent Bridge is ready! Restart your CLI to pick up the MCP tools.');
365
- console.log('');
366
-
367
- // Show template if --template was provided
368
- var templateFlag = null;
369
- for (var i = 3; i < process.argv.length; i++) {
370
- if (process.argv[i] === '--template' && process.argv[i + 1]) {
371
- templateFlag = process.argv[i + 1];
372
- break;
373
- }
374
- }
375
-
376
- if (templateFlag) {
377
- showTemplate(templateFlag);
378
- } else {
379
- console.log(' Open two terminals and start a conversation between agents.');
380
- console.log(' Tip: Use "npx let-them-talk init --template pair" for ready-made prompts.');
381
- console.log('');
382
- console.log(' Optional: Run "npx let-them-talk dashboard" to monitor conversations.');
383
- console.log('');
384
- }
385
- }
386
-
387
- function reset() {
388
- const dataDir = process.env.AGENT_BRIDGE_DATA_DIR || path.join(process.cwd(), '.agent-bridge');
389
- // Also check legacy data/ dir
390
- const legacyDir = path.join(process.cwd(), 'data');
391
- const targetDir = fs.existsSync(dataDir) ? dataDir : fs.existsSync(legacyDir) ? legacyDir : dataDir;
392
-
393
- if (!fs.existsSync(targetDir)) {
394
- console.log(' No data directory found. Nothing to reset.');
395
- return;
396
- }
397
- fs.rmSync(targetDir, { recursive: true, force: true });
398
- fs.mkdirSync(targetDir, { recursive: true });
399
- console.log(` Cleared all data from ${targetDir}`);
400
- }
401
-
402
- function getTemplates() {
403
- const templatesDir = path.join(__dirname, 'templates');
404
- if (!fs.existsSync(templatesDir)) return [];
405
- return fs.readdirSync(templatesDir)
406
- .filter(f => f.endsWith('.json'))
407
- .map(f => {
408
- try { return JSON.parse(fs.readFileSync(path.join(templatesDir, f), 'utf8')); }
409
- catch { return null; }
410
- })
411
- .filter(Boolean);
412
- }
413
-
414
- function listTemplates() {
415
- const templates = getTemplates();
416
- console.log('');
417
- console.log(' Available Agent Templates');
418
- console.log(' ========================');
419
- console.log('');
420
- for (const t of templates) {
421
- const agentNames = t.agents.map(a => a.name).join(', ');
422
- console.log(' ' + t.name.padEnd(12) + ' ' + t.description);
423
- console.log(' ' + ''.padEnd(12) + ' Agents: ' + agentNames);
424
- console.log('');
425
- }
426
- console.log(' Usage: npx let-them-talk init --template <name>');
427
- console.log('');
428
- }
429
-
430
- function showTemplate(templateName) {
431
- const templates = getTemplates();
432
- const template = templates.find(t => t.name === templateName);
433
- if (!template) {
434
- console.error(' Unknown template: ' + templateName);
435
- console.error(' Available: ' + templates.map(t => t.name).join(', '));
436
- process.exit(1);
437
- }
438
-
439
- console.log('');
440
- console.log(' Template: ' + template.name);
441
- console.log(' ' + template.description);
442
- console.log('');
443
- console.log(' Copy these prompts into each terminal:');
444
- console.log(' ======================================');
445
-
446
- for (var i = 0; i < template.agents.length; i++) {
447
- var a = template.agents[i];
448
- console.log('');
449
- console.log(' --- Terminal ' + (i + 1) + ': ' + a.name + ' (' + a.role + ') ---');
450
- console.log('');
451
- console.log(' ' + a.prompt.replace(/\n/g, '\n '));
452
- console.log('');
453
- }
454
- }
455
-
456
- function dashboard() {
457
- if (process.argv.includes('--lan')) {
458
- process.env.AGENT_BRIDGE_LAN = 'true';
459
- }
460
- require('./dashboard.js');
461
- }
462
-
463
- function resolveDataDirCli() {
464
- return process.env.AGENT_BRIDGE_DATA_DIR || path.join(process.cwd(), '.agent-bridge');
465
- }
466
-
467
- function readJsonl(filePath) {
468
- if (!fs.existsSync(filePath)) return [];
469
- return fs.readFileSync(filePath, 'utf8')
470
- .split('\n')
471
- .filter(l => l.trim())
472
- .map(l => { try { return JSON.parse(l); } catch { return null; } })
473
- .filter(Boolean);
474
- }
475
-
476
- function readJson(filePath) {
477
- if (!fs.existsSync(filePath)) return {};
478
- try { return JSON.parse(fs.readFileSync(filePath, 'utf8')); } catch { return {}; }
479
- }
480
-
481
- function isPidAlive(pid) {
482
- if (!pid) return false;
483
- try { process.kill(pid, 0); return true; } catch { return false; }
484
- }
485
-
486
- function cliMsg() {
487
- const recipient = process.argv[3];
488
- const textParts = process.argv.slice(4);
489
- if (!recipient || !textParts.length) {
490
- console.error(' Usage: npx let-them-talk msg <agent> <text>');
491
- process.exit(1);
492
- }
493
- if (!/^[a-zA-Z0-9_-]{1,20}$/.test(recipient)) {
494
- console.error(' Agent name must be 1-20 alphanumeric characters (with _ or -).');
495
- process.exit(1);
496
- }
497
- const text = textParts.join(' ');
498
- const dir = resolveDataDirCli();
499
- if (!fs.existsSync(dir)) {
500
- console.error(' No .agent-bridge/ directory found. Run "npx let-them-talk init" first.');
501
- process.exit(1);
502
- }
503
-
504
- const msgId = 'm' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
505
- const msg = {
506
- id: msgId,
507
- from: 'CLI',
508
- to: recipient,
509
- content: text,
510
- timestamp: new Date().toISOString(),
511
- };
512
-
513
- const messagesFile = path.join(dir, 'messages.jsonl');
514
- const historyFile = path.join(dir, 'history.jsonl');
515
- fs.appendFileSync(messagesFile, JSON.stringify(msg) + '\n');
516
- fs.appendFileSync(historyFile, JSON.stringify(msg) + '\n');
517
-
518
- console.log(' Message sent to ' + recipient + ': ' + text);
519
- }
520
-
521
- function cliStatus() {
522
- const dir = resolveDataDirCli();
523
- if (!fs.existsSync(dir)) {
524
- console.error(' No .agent-bridge/ directory found. Run "npx let-them-talk init" first.');
525
- process.exit(1);
526
- }
527
-
528
- const agents = readJson(path.join(dir, 'agents.json'));
529
- const history = readJsonl(path.join(dir, 'history.jsonl'));
530
-
531
- console.log('');
532
- console.log(' Agent Bridge Status');
533
- console.log(' ===================');
534
- console.log(' Messages: ' + history.length);
535
- console.log('');
536
-
537
- const names = Object.keys(agents);
538
- if (!names.length) {
539
- console.log(' No agents registered.');
540
- } else {
541
- console.log(' Agents:');
542
- for (const name of names) {
543
- const info = agents[name];
544
- const alive = isPidAlive(info.pid);
545
- const status = alive ? '\x1b[32monline\x1b[0m' : '\x1b[31moffline\x1b[0m';
546
- const lastActivity = info.last_activity || info.timestamp || '';
547
- const msgCount = history.filter(m => m.from === name).length;
548
- console.log(' ' + name.padEnd(16) + ' ' + status + ' msgs: ' + msgCount + ' last: ' + (lastActivity ? new Date(lastActivity).toLocaleTimeString() : '-'));
549
- }
550
- }
551
- console.log('');
552
- }
553
-
554
- switch (command) {
555
- case 'init':
556
- init();
557
- break;
558
- case 'templates':
559
- listTemplates();
560
- break;
561
- case 'dashboard':
562
- dashboard();
563
- break;
564
- case 'reset':
565
- reset();
566
- break;
567
- case 'msg':
568
- case 'message':
569
- case 'send':
570
- cliMsg();
571
- break;
572
- case 'status':
573
- cliStatus();
574
- break;
575
- case 'plugin':
576
- case 'plugins':
577
- console.log(' Plugins have been removed in v3.4.3. CLI terminals have their own extension systems.');
578
- break;
579
- case 'help':
580
- case '--help':
581
- case '-h':
582
- case undefined:
583
- printUsage();
584
- break;
585
- default:
586
- console.error(` Unknown command: ${command}`);
587
- printUsage();
588
- process.exit(1);
589
- }
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const { execSync } = require('child_process');
7
+
8
+ const command = process.argv[2];
9
+
10
+ function printUsage() {
11
+ console.log(`
12
+ Let Them Talk — Agent Bridge v5.2.0
13
+ MCP message broker for inter-agent communication
14
+ Supports: Claude Code, Gemini CLI, Codex CLI, Ollama
15
+
16
+ Usage:
17
+ npx let-them-talk init Auto-detect CLI and configure MCP
18
+ npx let-them-talk init --claude Configure for Claude Code
19
+ npx let-them-talk init --gemini Configure for Gemini CLI
20
+ npx let-them-talk init --codex Configure for Codex CLI
21
+ npx let-them-talk init --all Configure for all supported CLIs
22
+ npx let-them-talk init --ollama Setup Ollama agent bridge (local LLM)
23
+ npx let-them-talk init --template T Initialize with a team template (pair, team, review, debate, ollama)
24
+ npx let-them-talk templates List available agent templates
25
+ npx let-them-talk dashboard Launch the web dashboard (http://localhost:3000)
26
+ npx let-them-talk dashboard --lan Launch dashboard accessible on LAN (phone/tablet)
27
+ npx let-them-talk reset Clear all conversation data
28
+ npx let-them-talk msg <agent> <text> Send a message to an agent
29
+ npx let-them-talk run "prompt" [--agents N] [--timeout M] Autonomous execution with N agents, auto-stop after M minutes
30
+ npx let-them-talk status Show active agents and message count
31
+ npx let-them-talk uninstall Remove agent-bridge from all CLI configs
32
+ npx let-them-talk help Show this help message
33
+
34
+ v5.0 True Autonomy Engine (61 tools):
35
+ New tools: get_work, verify_and_advance, start_plan, retry_with_improvement
36
+ Proactive work loop: get_work → do work → verify_and_advance → get_work
37
+ Parallel workflow steps with dependency graphs (depends_on)
38
+ Auto-retry with skill accumulation (3 attempts then team escalation)
39
+ Watchdog engine: idle nudge, stuck detection, auto-reassign
40
+ 100ms handoff cooldowns in autonomous mode
41
+ Plan dashboard: live progress, pause/stop/skip/reassign controls
42
+ `);
43
+ }
44
+
45
+ // Detect which CLIs are installed
46
+ function detectCLIs() {
47
+ const detected = [];
48
+ const home = os.homedir();
49
+
50
+ // Claude Code: ~/.claude/ directory exists
51
+ if (fs.existsSync(path.join(home, '.claude'))) {
52
+ detected.push('claude');
53
+ }
54
+
55
+ // Gemini CLI: ~/.gemini/ or GEMINI_API_KEY set
56
+ if (fs.existsSync(path.join(home, '.gemini')) || process.env.GEMINI_API_KEY) {
57
+ detected.push('gemini');
58
+ }
59
+
60
+ // Codex CLI: ~/.codex/ directory exists
61
+ if (fs.existsSync(path.join(home, '.codex'))) {
62
+ detected.push('codex');
63
+ }
64
+
65
+ return detected;
66
+ }
67
+
68
+ // Detect Ollama installation
69
+ function detectOllama() {
70
+ try {
71
+ const version = execSync('ollama --version', { encoding: 'utf8', timeout: 5000 }).trim();
72
+ return { installed: true, version };
73
+ } catch {
74
+ return { installed: false };
75
+ }
76
+ }
77
+
78
+ // The data directory where all agents read/write — must be the same for server + dashboard
79
+ function dataDir(cwd) {
80
+ return path.join(cwd, '.agent-bridge');
81
+ }
82
+
83
+ // Configure for Claude Code (.mcp.json in project root)
84
+ function setupClaude(serverPath, cwd) {
85
+ const mcpConfigPath = path.join(cwd, '.mcp.json');
86
+ let mcpConfig = { mcpServers: {} };
87
+ if (fs.existsSync(mcpConfigPath)) {
88
+ try {
89
+ mcpConfig = JSON.parse(fs.readFileSync(mcpConfigPath, 'utf8'));
90
+ if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
91
+ } catch {
92
+ // Backup corrupted file before overwriting
93
+ const backup = mcpConfigPath + '.backup';
94
+ fs.copyFileSync(mcpConfigPath, backup);
95
+ console.log(' [warn] Existing .mcp.json was invalid backed up to .mcp.json.backup');
96
+ }
97
+ }
98
+
99
+ mcpConfig.mcpServers['agent-bridge'] = {
100
+ command: 'node',
101
+ args: [serverPath],
102
+ timeout: 300,
103
+ };
104
+
105
+ fs.writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2) + '\n');
106
+ console.log(' [ok] Claude Code: .mcp.json updated');
107
+ }
108
+
109
+ // Configure for Gemini CLI (.gemini/settings.json or GEMINI.md with MCP config)
110
+ function setupGemini(serverPath, cwd) {
111
+ // Gemini CLI uses .gemini/settings.json for MCP configuration
112
+ const geminiDir = path.join(cwd, '.gemini');
113
+ const settingsPath = path.join(geminiDir, 'settings.json');
114
+
115
+ if (!fs.existsSync(geminiDir)) {
116
+ fs.mkdirSync(geminiDir, { recursive: true });
117
+ }
118
+
119
+ let settings = { mcpServers: {} };
120
+ if (fs.existsSync(settingsPath)) {
121
+ try {
122
+ settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
123
+ if (!settings.mcpServers) settings.mcpServers = {};
124
+ } catch {
125
+ const backup = settingsPath + '.backup';
126
+ fs.copyFileSync(settingsPath, backup);
127
+ console.log(' [warn] Existing settings.json was invalid — backed up to settings.json.backup');
128
+ }
129
+ }
130
+
131
+ settings.mcpServers['agent-bridge'] = {
132
+ command: 'node',
133
+ args: [serverPath],
134
+ timeout: 300,
135
+ trust: true,
136
+ };
137
+
138
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
139
+ console.log(' [ok] Gemini CLI: .gemini/settings.json updated');
140
+ }
141
+
142
+ // Configure for Codex CLI (uses .codex/config.toml)
143
+ function setupCodex(serverPath, cwd) {
144
+ const codexDir = path.join(cwd, '.codex');
145
+ const configPath = path.join(codexDir, 'config.toml');
146
+
147
+ if (!fs.existsSync(codexDir)) {
148
+ fs.mkdirSync(codexDir, { recursive: true });
149
+ }
150
+
151
+ // Read existing config or start fresh
152
+ let config = '';
153
+ if (fs.existsSync(configPath)) {
154
+ config = fs.readFileSync(configPath, 'utf8');
155
+ }
156
+
157
+ // Backup existing config before modifying
158
+ if (fs.existsSync(configPath)) {
159
+ fs.copyFileSync(configPath, configPath + '.backup');
160
+ }
161
+
162
+ // Only add if not already present
163
+ if (!config.includes('[mcp_servers.agent-bridge]')) {
164
+ const tomlBlock = `
165
+ [mcp_servers.agent-bridge]
166
+ command = "node"
167
+ args = [${JSON.stringify(serverPath)}]
168
+ timeout = 300
169
+ `;
170
+ config += tomlBlock;
171
+ fs.writeFileSync(configPath, config);
172
+ }
173
+
174
+ console.log(' [ok] Codex CLI: .codex/config.toml updated');
175
+ }
176
+
177
+ // Setup Ollama agent bridge script
178
+ function setupOllama(serverPath, cwd) {
179
+ const dir = dataDir(cwd);
180
+ const scriptPath = path.join(cwd, '.agent-bridge', 'ollama-agent.js');
181
+
182
+ if (!fs.existsSync(path.join(cwd, '.agent-bridge'))) {
183
+ fs.mkdirSync(path.join(cwd, '.agent-bridge'), { recursive: true });
184
+ }
185
+
186
+ const script = `#!/usr/bin/env node
187
+ // ollama-agent.js - bridges Ollama to Let Them Talk
188
+ // Usage: node .agent-bridge/ollama-agent.js [agent-name] [model]
189
+ const fs = require('fs'), path = require('path'), http = require('http');
190
+ const DATA_DIR = path.join(__dirname);
191
+ const name = process.argv[2] || 'Ollama';
192
+ if (!/^[a-zA-Z0-9_-]{1,20}$/.test(name)) throw new Error('Invalid agent name');
193
+ const model = process.argv[3] || 'llama3';
194
+ const OLLAMA_URL = process.env.OLLAMA_URL || 'http://localhost:11434';
195
+
196
+ function readJson(f) { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch { return {}; } }
197
+ function readJsonl(f) { if (!fs.existsSync(f)) return []; return fs.readFileSync(f, 'utf8').split(/\\r?\\n/).filter(l => l.trim()).map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean); }
198
+
199
+ // Register agent
200
+ function register() {
201
+ const agentsFile = path.join(DATA_DIR, 'agents.json');
202
+ const agents = readJson(agentsFile);
203
+ agents[name] = { pid: process.pid, timestamp: new Date().toISOString(), last_activity: new Date().toISOString(), provider: 'Ollama (' + model + ')' };
204
+ fs.writeFileSync(agentsFile, JSON.stringify(agents, null, 2));
205
+ console.log('[' + name + '] Registered (PID ' + process.pid + ', model: ' + model + ')');
206
+ }
207
+
208
+ // Update heartbeat
209
+ function heartbeat() {
210
+ const agentsFile = path.join(DATA_DIR, 'agents.json');
211
+ const agents = readJson(agentsFile);
212
+ if (agents[name]) {
213
+ agents[name].last_activity = new Date().toISOString();
214
+ agents[name].pid = process.pid;
215
+ fs.writeFileSync(agentsFile, JSON.stringify(agents, null, 2));
216
+ }
217
+ }
218
+
219
+ // Call Ollama API
220
+ function callOllama(prompt) {
221
+ return new Promise(function(resolve, reject) {
222
+ const url = new URL(OLLAMA_URL + '/api/chat');
223
+ const body = JSON.stringify({ model: model, messages: [{ role: 'user', content: prompt }], stream: false });
224
+ const req = http.request(url, { method: 'POST', headers: { 'Content-Type': 'application/json' } }, function(res) {
225
+ let data = '';
226
+ res.on('data', function(c) { data += c; });
227
+ res.on('end', function() {
228
+ try { const j = JSON.parse(data); resolve(j.message ? j.message.content : data); }
229
+ catch { resolve(data); }
230
+ });
231
+ });
232
+ req.on('error', reject);
233
+ req.write(body);
234
+ req.end();
235
+ });
236
+ }
237
+
238
+ // Send a message
239
+ function sendMessage(to, content) {
240
+ const msgId = 'm' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
241
+ const msg = { id: msgId, from: name, to: to, content: content, timestamp: new Date().toISOString() };
242
+ fs.appendFileSync(path.join(DATA_DIR, 'messages.jsonl'), JSON.stringify(msg) + '\\n');
243
+ fs.appendFileSync(path.join(DATA_DIR, 'history.jsonl'), JSON.stringify(msg) + '\\n');
244
+ console.log('[' + name + '] -> ' + to + ': ' + content.substring(0, 80) + (content.length > 80 ? '...' : ''));
245
+ }
246
+
247
+ // Listen for messages
248
+ let lastOffset = 0;
249
+ function checkMessages() {
250
+ const consumedFile = path.join(DATA_DIR, 'consumed-' + name + '.json');
251
+ const consumed = readJson(consumedFile);
252
+ lastOffset = consumed.offset || 0;
253
+
254
+ const messages = readJsonl(path.join(DATA_DIR, 'messages.jsonl'));
255
+ const newMsgs = messages.slice(lastOffset).filter(function(m) {
256
+ return m.to === name || (m.to === 'all' && m.from !== name);
257
+ });
258
+
259
+ if (newMsgs.length > 0) {
260
+ consumed.offset = messages.length;
261
+ fs.writeFileSync(consumedFile, JSON.stringify(consumed));
262
+ }
263
+
264
+ return newMsgs;
265
+ }
266
+
267
+ async function processMessages() {
268
+ const msgs = checkMessages();
269
+ for (const m of msgs) {
270
+ console.log('[' + name + '] <- ' + m.from + ': ' + m.content.substring(0, 80));
271
+ try {
272
+ const response = await callOllama(m.content);
273
+ sendMessage(m.from, response);
274
+ } catch (e) {
275
+ sendMessage(m.from, 'Error calling Ollama: ' + e.message);
276
+ }
277
+ }
278
+ }
279
+
280
+ // Main loop
281
+ register();
282
+ const hb = setInterval(heartbeat, 10000);
283
+ hb.unref();
284
+ console.log('[' + name + '] Listening for messages... (Ctrl+C to stop)');
285
+ setInterval(processMessages, 2000);
286
+
287
+ // Cleanup on exit
288
+ process.on('SIGINT', function() { console.log('\\n[' + name + '] Shutting down.'); process.exit(0); });
289
+ `;
290
+
291
+ const tmpPath = scriptPath + '.tmp.' + process.pid;
292
+ fs.writeFileSync(tmpPath, script);
293
+ fs.renameSync(tmpPath, scriptPath);
294
+ console.log(' [ok] Ollama agent script created: .agent-bridge/ollama-agent.js');
295
+ console.log('');
296
+ console.log(' Launch an Ollama agent with:');
297
+ console.log(' node .agent-bridge/ollama-agent.js <name> <model>');
298
+ console.log('');
299
+ console.log(' Examples:');
300
+ console.log(' node .agent-bridge/ollama-agent.js Ollama llama3');
301
+ console.log(' node .agent-bridge/ollama-agent.js Coder codellama');
302
+ console.log(' node .agent-bridge/ollama-agent.js Writer mistral');
303
+ }
304
+
305
+ function init() {
306
+ const cwd = process.cwd();
307
+ const serverPath = path.join(__dirname, 'server.js').replace(/\\/g, '/');
308
+ const gitignorePath = path.join(cwd, '.gitignore');
309
+ const flag = process.argv[3];
310
+
311
+ console.log('');
312
+ console.log(' Let Them Talk Initializing Agent Bridge');
313
+ console.log(' ==========================================');
314
+ console.log('');
315
+
316
+ let targets = [];
317
+
318
+ if (flag === '--claude') {
319
+ targets = ['claude'];
320
+ } else if (flag === '--gemini') {
321
+ targets = ['gemini'];
322
+ } else if (flag === '--codex') {
323
+ targets = ['codex'];
324
+ } else if (flag === '--all') {
325
+ targets = ['claude', 'gemini', 'codex'];
326
+ } else if (flag === '--ollama') {
327
+ const ollama = detectOllama();
328
+ if (!ollama.installed) {
329
+ console.log(' Ollama not found. Install it from: https://ollama.com/download');
330
+ console.log(' After installing, run: ollama pull llama3');
331
+ console.log('');
332
+ } else {
333
+ console.log(' Ollama detected: ' + ollama.version);
334
+ setupOllama(serverPath, cwd);
335
+ }
336
+ targets = detectCLIs();
337
+ if (targets.length === 0) targets = ['claude'];
338
+ } else {
339
+ // Auto-detect
340
+ targets = detectCLIs();
341
+ if (targets.length === 0) {
342
+ // Default to claude if nothing detected
343
+ targets = ['claude'];
344
+ console.log(' No CLI detected, defaulting to Claude Code config.');
345
+ } else {
346
+ console.log(` Detected CLI(s): ${targets.join(', ')}`);
347
+ }
348
+ }
349
+
350
+ console.log('');
351
+
352
+ for (const target of targets) {
353
+ switch (target) {
354
+ case 'claude': setupClaude(serverPath, cwd); break;
355
+ case 'gemini': setupGemini(serverPath, cwd); break;
356
+ case 'codex': setupCodex(serverPath, cwd); break;
357
+ }
358
+ }
359
+
360
+ // Add .agent-bridge/ and MCP config files to .gitignore
361
+ const gitignoreEntries = ['.agent-bridge/', '.mcp.json', '.codex/', '.gemini/'];
362
+ if (fs.existsSync(gitignorePath)) {
363
+ let content = fs.readFileSync(gitignorePath, 'utf8');
364
+ const missing = gitignoreEntries.filter(e => !content.includes(e));
365
+ if (missing.length) {
366
+ content += '\n# Agent Bridge (auto-added by let-them-talk init)\n' + missing.join('\n') + '\n';
367
+ fs.writeFileSync(gitignorePath, content);
368
+ console.log(' [ok] Added to .gitignore: ' + missing.join(', '));
369
+ } else {
370
+ console.log(' [ok] .gitignore already configured');
371
+ }
372
+ } else {
373
+ fs.writeFileSync(gitignorePath, '# Agent Bridge (auto-added by let-them-talk init)\n' + gitignoreEntries.join('\n') + '\n');
374
+ console.log(' [ok] .gitignore created');
375
+ }
376
+
377
+ console.log('');
378
+ console.log(' Agent Bridge is ready! Restart your CLI to pick up the MCP tools.');
379
+ console.log('');
380
+
381
+ // Show template if --template was provided
382
+ var templateFlag = null;
383
+ for (var i = 3; i < process.argv.length; i++) {
384
+ if (process.argv[i] === '--template' && process.argv[i + 1]) {
385
+ templateFlag = process.argv[i + 1];
386
+ break;
387
+ }
388
+ }
389
+
390
+ if (templateFlag) {
391
+ showTemplate(templateFlag);
392
+ } else {
393
+ console.log(' Open two terminals and start a conversation between agents.');
394
+ console.log(' Tip: Use "npx let-them-talk init --template pair" for ready-made prompts.');
395
+ console.log('');
396
+ console.log(' \x1b[1m Try autonomous mode:\x1b[0m');
397
+ console.log(' npx let-them-talk run "build a REST API" --agents 3');
398
+ console.log('');
399
+ console.log(' \x1b[1m Monitor:\x1b[0m');
400
+ console.log(' npx let-them-talk dashboard');
401
+ console.log(' npx let-them-talk status');
402
+ console.log(' npx let-them-talk doctor');
403
+ console.log('');
404
+ }
405
+ }
406
+
407
+ function reset() {
408
+ const targetDir = process.env.AGENT_BRIDGE_DATA_DIR || path.join(process.cwd(), '.agent-bridge');
409
+
410
+ if (!fs.existsSync(targetDir)) {
411
+ console.log(' No .agent-bridge/ directory found. Nothing to reset.');
412
+ return;
413
+ }
414
+
415
+ // Safety: count messages to show user what they're about to delete
416
+ const historyFile = path.join(targetDir, 'history.jsonl');
417
+ let msgCount = 0;
418
+ if (fs.existsSync(historyFile)) {
419
+ msgCount = fs.readFileSync(historyFile, 'utf8').split(/\r?\n/).filter(l => l.trim()).length;
420
+ }
421
+
422
+ // Require --force flag, otherwise warn and exit
423
+ if (!process.argv.includes('--force')) {
424
+ console.log('');
425
+ console.log(' ⚠ This will permanently delete all conversation data in:');
426
+ console.log(' ' + targetDir);
427
+ if (msgCount > 0) console.log(' (' + msgCount + ' messages in history)');
428
+ console.log('');
429
+ console.log(' To confirm, run: npx let-them-talk reset --force');
430
+ console.log('');
431
+ return;
432
+ }
433
+
434
+ // Auto-archive before deleting
435
+ const archiveDir = path.join(targetDir, '..', '.agent-bridge-archive');
436
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
437
+ const archivePath = path.join(archiveDir, timestamp);
438
+ try {
439
+ fs.mkdirSync(archivePath, { recursive: true });
440
+ const filesToArchive = ['history.jsonl', 'messages.jsonl', 'agents.json', 'decisions.json', 'tasks.json'];
441
+ let archived = 0;
442
+ for (const f of filesToArchive) {
443
+ const src = path.join(targetDir, f);
444
+ if (fs.existsSync(src)) {
445
+ fs.copyFileSync(src, path.join(archivePath, f));
446
+ archived++;
447
+ }
448
+ }
449
+ if (archived > 0) {
450
+ console.log(' [ok] Archived ' + archived + ' files to .agent-bridge-archive/' + timestamp + '/');
451
+ }
452
+ } catch (e) {
453
+ console.log(' [warn] Could not archive: ' + e.message + ' — proceeding with reset anyway.');
454
+ }
455
+
456
+ fs.rmSync(targetDir, { recursive: true, force: true });
457
+ fs.mkdirSync(targetDir, { recursive: true });
458
+ console.log(' Cleared all data from ' + targetDir);
459
+ }
460
+
461
+ function getTemplates() {
462
+ const templatesDir = path.join(__dirname, 'templates');
463
+ if (!fs.existsSync(templatesDir)) return [];
464
+ return fs.readdirSync(templatesDir)
465
+ .filter(f => f.endsWith('.json'))
466
+ .map(f => {
467
+ try { return JSON.parse(fs.readFileSync(path.join(templatesDir, f), 'utf8')); }
468
+ catch { return null; }
469
+ })
470
+ .filter(Boolean);
471
+ }
472
+
473
+ function listTemplates() {
474
+ const templates = getTemplates();
475
+ console.log('');
476
+ console.log(' Available Agent Templates');
477
+ console.log(' ========================');
478
+ console.log('');
479
+ for (const t of templates) {
480
+ const agentNames = t.agents.map(a => a.name).join(', ');
481
+ console.log(' ' + t.name.padEnd(12) + ' ' + t.description);
482
+ console.log(' ' + ''.padEnd(12) + ' Agents: ' + agentNames);
483
+ console.log('');
484
+ }
485
+ console.log(' Usage: npx let-them-talk init --template <name>');
486
+ console.log('');
487
+ }
488
+
489
+ function showTemplate(templateName) {
490
+ const templates = getTemplates();
491
+ const template = templates.find(t => t.name === templateName);
492
+ if (!template) {
493
+ console.error(' Unknown template: ' + templateName);
494
+ console.error(' Available: ' + templates.map(t => t.name).join(', '));
495
+ process.exit(1);
496
+ }
497
+
498
+ console.log('');
499
+ console.log(' Template: ' + template.name);
500
+ console.log(' ' + template.description);
501
+ console.log('');
502
+ console.log(' Copy these prompts into each terminal:');
503
+ console.log(' ======================================');
504
+
505
+ for (var i = 0; i < template.agents.length; i++) {
506
+ var a = template.agents[i];
507
+ console.log('');
508
+ console.log(' --- Terminal ' + (i + 1) + ': ' + a.name + ' (' + a.role + ') ---');
509
+ console.log('');
510
+ console.log(' ' + a.prompt.replace(/\n/g, '\n '));
511
+ console.log('');
512
+ }
513
+ }
514
+
515
+ function dashboard() {
516
+ if (process.argv.includes('--lan')) {
517
+ process.env.AGENT_BRIDGE_LAN = 'true';
518
+ }
519
+ require('./dashboard.js');
520
+ }
521
+
522
+ function resolveDataDirCli() {
523
+ return process.env.AGENT_BRIDGE_DATA_DIR || path.join(process.cwd(), '.agent-bridge');
524
+ }
525
+
526
+ function readJsonl(filePath) {
527
+ if (!fs.existsSync(filePath)) return [];
528
+ return fs.readFileSync(filePath, 'utf8')
529
+ .split(/\r?\n/)
530
+ .filter(l => l.trim())
531
+ .map(l => { try { return JSON.parse(l); } catch { return null; } })
532
+ .filter(Boolean);
533
+ }
534
+
535
+ function readJson(filePath) {
536
+ if (!fs.existsSync(filePath)) return {};
537
+ try { return JSON.parse(fs.readFileSync(filePath, 'utf8')); } catch { return {}; }
538
+ }
539
+
540
+ function isPidAlive(pid) {
541
+ if (!pid) return false;
542
+ try { process.kill(pid, 0); return true; } catch { return false; }
543
+ }
544
+
545
+ function cliMsg() {
546
+ const recipient = process.argv[3];
547
+ const textParts = process.argv.slice(4);
548
+ if (!recipient || !textParts.length) {
549
+ console.error(' Usage: npx let-them-talk msg <agent> <text>');
550
+ process.exit(1);
551
+ }
552
+ if (!/^[a-zA-Z0-9_-]{1,20}$/.test(recipient)) {
553
+ console.error(' Agent name must be 1-20 alphanumeric characters (with _ or -).');
554
+ process.exit(1);
555
+ }
556
+ const text = textParts.join(' ');
557
+ const dir = resolveDataDirCli();
558
+ if (!fs.existsSync(dir)) {
559
+ console.error(' No .agent-bridge/ directory found. Run "npx let-them-talk init" first.');
560
+ process.exit(1);
561
+ }
562
+
563
+ const msgId = 'm' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
564
+ const msg = {
565
+ id: msgId,
566
+ from: 'CLI',
567
+ to: recipient,
568
+ content: text,
569
+ timestamp: new Date().toISOString(),
570
+ };
571
+
572
+ const messagesFile = path.join(dir, 'messages.jsonl');
573
+ const historyFile = path.join(dir, 'history.jsonl');
574
+ fs.appendFileSync(messagesFile, JSON.stringify(msg) + '\n');
575
+ fs.appendFileSync(historyFile, JSON.stringify(msg) + '\n');
576
+
577
+ console.log(' Message sent to ' + recipient + ': ' + text);
578
+ }
579
+
580
+ function cliStatus() {
581
+ const dir = resolveDataDirCli();
582
+ if (!fs.existsSync(dir)) {
583
+ console.error(' No .agent-bridge/ directory found. Run "npx let-them-talk init" first.');
584
+ process.exit(1);
585
+ }
586
+
587
+ const agents = readJson(path.join(dir, 'agents.json'));
588
+ const history = readJsonl(path.join(dir, 'history.jsonl'));
589
+ const profiles = readJson(path.join(dir, 'profiles.json'));
590
+ const workflows = readJson(path.join(dir, 'workflows.json'));
591
+ const tasks = readJson(path.join(dir, 'tasks.json'));
592
+
593
+ // Merge heartbeat files for live activity data
594
+ try {
595
+ const files = fs.readdirSync(dir).filter(f => f.startsWith('heartbeat-') && f.endsWith('.json'));
596
+ for (const f of files) {
597
+ const name = f.slice(10, -5);
598
+ if (agents[name]) {
599
+ try {
600
+ const hb = JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8'));
601
+ if (hb.last_activity) agents[name].last_activity = hb.last_activity;
602
+ if (hb.pid) agents[name].pid = hb.pid;
603
+ } catch {}
604
+ }
605
+ }
606
+ } catch {}
607
+
608
+ const onlineCount = Object.values(agents).filter(a => isPidAlive(a.pid)).length;
609
+
610
+ console.log('');
611
+ console.log(' Let Them Talk — Status');
612
+ console.log(' =======================');
613
+ console.log(' Messages: ' + history.length + ' | Agents: ' + onlineCount + '/' + Object.keys(agents).length + ' online');
614
+ console.log('');
615
+
616
+ // Agents with roles
617
+ const names = Object.keys(agents);
618
+ if (!names.length) {
619
+ console.log(' No agents registered.');
620
+ } else {
621
+ console.log(' Agents:');
622
+ for (const name of names) {
623
+ const info = agents[name];
624
+ const alive = isPidAlive(info.pid);
625
+ const status = alive ? '\x1b[32monline\x1b[0m' : '\x1b[31moffline\x1b[0m';
626
+ const lastActivity = info.last_activity || info.timestamp || '';
627
+ const role = (profiles && profiles[name] && profiles[name].role) ? ' [' + profiles[name].role + ']' : '';
628
+ const msgCount = history.filter(m => m.from === name).length;
629
+ console.log(' ' + name.padEnd(16) + ' ' + status + role.padEnd(16) + ' msgs: ' + msgCount + ' last: ' + (lastActivity ? new Date(lastActivity).toLocaleTimeString() : '-'));
630
+ }
631
+ }
632
+
633
+ // Active workflows
634
+ const activeWfs = Array.isArray(workflows) ? workflows.filter(w => w.status === 'active') : [];
635
+ if (activeWfs.length > 0) {
636
+ console.log('');
637
+ console.log(' Workflows:');
638
+ for (const wf of activeWfs) {
639
+ const done = wf.steps.filter(s => s.status === 'done').length;
640
+ const total = wf.steps.length;
641
+ const pct = Math.round((done / total) * 100);
642
+ const mode = wf.autonomous ? ' (autonomous)' : '';
643
+ console.log(' ' + wf.name.padEnd(24) + ' ' + done + '/' + total + ' (' + pct + '%)' + mode);
644
+ }
645
+ }
646
+
647
+ // Active tasks
648
+ const activeTasks = Array.isArray(tasks) ? tasks.filter(t => t.status === 'in_progress') : [];
649
+ if (activeTasks.length > 0) {
650
+ console.log('');
651
+ console.log(' Tasks in progress:');
652
+ for (const t of activeTasks.slice(0, 5)) {
653
+ console.log(' ' + (t.title || 'Untitled').padEnd(30) + ' -> ' + (t.assignee || 'unassigned'));
654
+ }
655
+ if (activeTasks.length > 5) console.log(' ... and ' + (activeTasks.length - 5) + ' more');
656
+ }
657
+
658
+ console.log('');
659
+ }
660
+
661
+ // v5.0: One-command autonomous execution
662
+ function cliRun() {
663
+ const prompt = process.argv[3];
664
+ if (!prompt) {
665
+ console.error(' Usage: npx let-them-talk run "build a login system" [--agents N] [--timeout M]');
666
+ console.error(' Spawns N agent processes, auto-assigns roles, creates autonomous workflow, and starts execution.');
667
+ process.exit(1);
668
+ }
669
+
670
+ // Parse --agents flag (default: 3)
671
+ let agentCount = 3;
672
+ const agentsIdx = process.argv.indexOf('--agents');
673
+ if (agentsIdx !== -1 && process.argv[agentsIdx + 1]) {
674
+ agentCount = Math.max(2, Math.min(10, parseInt(process.argv[agentsIdx + 1]) || 3));
675
+ }
676
+
677
+ // Parse --timeout flag (default: no timeout, in minutes)
678
+ let timeoutMin = 0;
679
+ const timeoutIdx = process.argv.indexOf('--timeout');
680
+ if (timeoutIdx !== -1 && process.argv[timeoutIdx + 1]) {
681
+ timeoutMin = Math.max(1, parseInt(process.argv[timeoutIdx + 1]) || 0);
682
+ }
683
+
684
+ const cwd = process.cwd();
685
+ const dir = path.join(cwd, '.agent-bridge');
686
+ const serverPath = path.join(__dirname, 'server.js');
687
+
688
+ // Ensure data directory exists
689
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
690
+
691
+ // Set group conversation mode
692
+ const configPath = path.join(dir, 'config.json');
693
+ const config = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, 'utf8')) : {};
694
+ config.conversation_mode = 'group';
695
+ fs.writeFileSync(configPath, JSON.stringify(config));
696
+
697
+ // Agent names based on count
698
+ const AGENT_NAMES = ['Lead', 'Builder', 'Reviewer', 'Architect', 'Frontend', 'Backend', 'Tester', 'Designer', 'DevOps', 'Security'];
699
+ const names = AGENT_NAMES.slice(0, agentCount);
700
+
701
+ console.log('');
702
+ console.log(' Let Them Talk — Autonomous Run');
703
+ console.log(' ===============================');
704
+ console.log(' Prompt: ' + prompt);
705
+ console.log(' Agents: ' + agentCount + ' (' + names.join(', ') + ')');
706
+ console.log(' Mode: Autonomous (proactive work loop)');
707
+ console.log('');
708
+
709
+ const { spawn } = require('child_process');
710
+ const children = [];
711
+
712
+ // Spawn agent processes
713
+ for (let i = 0; i < agentCount; i++) {
714
+ const agentName = names[i];
715
+ console.log(' Spawning agent: ' + agentName + '...');
716
+
717
+ const child = spawn('node', [serverPath], {
718
+ env: {
719
+ ...process.env,
720
+ AGENT_BRIDGE_DATA_DIR: dir,
721
+ AGENT_BRIDGE_AUTO_REGISTER: agentName,
722
+ AGENT_BRIDGE_AUTO_PROMPT: i === 0 ? prompt : '', // only first agent gets the prompt
723
+ },
724
+ stdio: 'pipe',
725
+ cwd: cwd,
726
+ });
727
+
728
+ child.on('error', (err) => {
729
+ console.error(' [' + agentName + '] Error: ' + err.message);
730
+ });
731
+
732
+ child.on('exit', (code) => {
733
+ if (code !== 0 && code !== null) {
734
+ console.log(' \x1b[33m[' + agentName + '] Crashed (code ' + code + '). Auto-restarting...\x1b[0m');
735
+ const restart = spawn('node', [serverPath], {
736
+ env: { ...process.env, AGENT_BRIDGE_DATA_DIR: dir, AGENT_BRIDGE_AUTO_REGISTER: agentName },
737
+ stdio: 'pipe', cwd: cwd,
738
+ });
739
+ const entry = children.find(c => c.name === agentName);
740
+ if (entry) entry.process = restart;
741
+ } else {
742
+ console.log(' [' + agentName + '] Exited.');
743
+ }
744
+ });
745
+
746
+ children.push({ name: agentName, process: child });
747
+ }
748
+
749
+ console.log('');
750
+ console.log(' All ' + agentCount + ' agents spawned. They will auto-register and start working.');
751
+ console.log(' Open the dashboard to monitor: npx let-them-talk dashboard');
752
+ console.log('');
753
+ console.log(' Press Ctrl+C to stop all agents.');
754
+ console.log('');
755
+
756
+ // Inject the prompt as a dashboard message after agents register
757
+ setTimeout(() => {
758
+ try {
759
+ const messagesFile = path.join(dir, 'messages.jsonl');
760
+ const msg = {
761
+ id: 'run_' + Date.now().toString(36),
762
+ from: 'Dashboard',
763
+ to: '__group__',
764
+ content: prompt,
765
+ timestamp: new Date().toISOString(),
766
+ broadcast: true,
767
+ };
768
+ fs.appendFileSync(messagesFile, JSON.stringify(msg) + '\n');
769
+ const historyFile = path.join(dir, 'history.jsonl');
770
+ fs.appendFileSync(historyFile, JSON.stringify(msg) + '\n');
771
+ console.log(' Prompt injected to team. Agents will pick it up via get_work().');
772
+ } catch (e) {
773
+ console.error(' Failed to inject prompt: ' + e.message);
774
+ }
775
+ }, 3000); // 3s delay for agents to register
776
+
777
+ // Clean shutdown on Ctrl+C
778
+ process.on('SIGINT', () => {
779
+ console.log('\n Stopping all agents...');
780
+ for (const c of children) {
781
+ try { c.process.kill(); } catch {}
782
+ }
783
+ process.exit(0);
784
+ });
785
+
786
+ // Auto-stop after --timeout minutes
787
+ if (timeoutMin > 0) {
788
+ console.log(' Auto-stop in ' + timeoutMin + ' minute(s).');
789
+ setTimeout(() => {
790
+ console.log('\n Timeout reached (' + timeoutMin + 'min). Stopping all agents...');
791
+ for (const c of children) { try { c.process.kill(); } catch {} }
792
+ process.exit(0);
793
+ }, timeoutMin * 60000);
794
+ }
795
+
796
+ // Periodic progress updates every 30s
797
+ setInterval(() => {
798
+ try {
799
+ const agentsData = fs.existsSync(path.join(dir, 'agents.json')) ? JSON.parse(fs.readFileSync(path.join(dir, 'agents.json'), 'utf8')) : {};
800
+ const online = Object.values(agentsData).filter(a => {
801
+ try { process.kill(a.pid, 0); return true; } catch { return false; }
802
+ }).length;
803
+ const history = fs.existsSync(path.join(dir, 'history.jsonl')) ? fs.readFileSync(path.join(dir, 'history.jsonl'), 'utf8').trim().split(/\r?\n/).filter(l => l.trim()).length : 0;
804
+ const tasksData = fs.existsSync(path.join(dir, 'tasks.json')) ? JSON.parse(fs.readFileSync(path.join(dir, 'tasks.json'), 'utf8')) : [];
805
+ const done = Array.isArray(tasksData) ? tasksData.filter(t => t.status === 'done').length : 0;
806
+ const active = Array.isArray(tasksData) ? tasksData.filter(t => t.status === 'in_progress').length : 0;
807
+ console.log(` \x1b[90m[${new Date().toLocaleTimeString()}]\x1b[0m ${online} agents | ${history} msgs | ${done} done, ${active} active`);
808
+ } catch {}
809
+ }, 30000);
810
+ }
811
+
812
+ // v5.0: Diagnostic health check
813
+ function cliDoctor() {
814
+ console.log('');
815
+ console.log(' \x1b[1mLet Them Talk — Doctor\x1b[0m');
816
+ console.log(' ======================');
817
+ let issues = 0;
818
+
819
+ // Check data directory
820
+ const dir = path.join(process.cwd(), '.agent-bridge');
821
+ if (fs.existsSync(dir)) {
822
+ console.log(' \x1b[32m✓\x1b[0m .agent-bridge/ directory exists');
823
+ try { fs.accessSync(dir, fs.constants.W_OK); console.log(' \x1b[32m✓\x1b[0m .agent-bridge/ is writable'); }
824
+ catch { console.log(' \x1b[31m✗\x1b[0m .agent-bridge/ is NOT writable'); issues++; }
825
+ } else {
826
+ console.log(' \x1b[33m!\x1b[0m .agent-bridge/ not found. Run "npx let-them-talk init" first.');
827
+ issues++;
828
+ }
829
+
830
+ // Check server.js
831
+ const serverPath = path.join(__dirname, 'server.js');
832
+ if (fs.existsSync(serverPath)) {
833
+ console.log(' \x1b[32m✓\x1b[0m server.js found');
834
+ } else {
835
+ console.log(' \x1b[31m✗\x1b[0m server.js MISSING'); issues++;
836
+ }
837
+
838
+ // Check agents online
839
+ if (fs.existsSync(dir)) {
840
+ const agentsFile = path.join(dir, 'agents.json');
841
+ if (fs.existsSync(agentsFile)) {
842
+ const agents = readJson(agentsFile);
843
+ const online = Object.entries(agents).filter(([, a]) => isPidAlive(a.pid)).length;
844
+ const total = Object.keys(agents).length;
845
+ if (online > 0) {
846
+ console.log(' \x1b[32m✓\x1b[0m ' + online + '/' + total + ' agents online');
847
+ } else if (total > 0) {
848
+ console.log(' \x1b[33m!\x1b[0m ' + total + ' agents registered but none online');
849
+ } else {
850
+ console.log(' \x1b[33m!\x1b[0m No agents registered yet');
851
+ }
852
+ }
853
+
854
+ // Check config
855
+ const configFile = path.join(dir, 'config.json');
856
+ if (fs.existsSync(configFile)) {
857
+ const config = readJson(configFile);
858
+ console.log(' \x1b[32m✓\x1b[0m Conversation mode: ' + (config.conversation_mode || 'direct'));
859
+ }
860
+
861
+ // Check guide file
862
+ const guideFile = path.join(dir, 'guide.md');
863
+ if (fs.existsSync(guideFile)) {
864
+ console.log(' \x1b[32m✓\x1b[0m Custom guide.md found');
865
+ }
866
+ }
867
+
868
+ // Check Node version
869
+ const nodeVersion = process.version;
870
+ const major = parseInt(nodeVersion.slice(1));
871
+ if (major >= 18) {
872
+ console.log(' \x1b[32m✓\x1b[0m Node.js ' + nodeVersion + ' (OK)');
873
+ } else {
874
+ console.log(' \x1b[31m✗\x1b[0m Node.js ' + nodeVersion + ' — v18+ recommended'); issues++;
875
+ }
876
+
877
+ console.log('');
878
+ if (issues === 0) {
879
+ console.log(' \x1b[32mAll checks passed. System is healthy.\x1b[0m');
880
+ } else {
881
+ console.log(' \x1b[31m' + issues + ' issue(s) found. Fix them and run doctor again.\x1b[0m');
882
+ }
883
+ console.log('');
884
+ }
885
+
886
+ // Uninstall agent-bridge from all CLI configs
887
+ function uninstall() {
888
+ const cwd = process.cwd();
889
+ const home = os.homedir();
890
+ const removed = [];
891
+ const notFound = [];
892
+
893
+ console.log('');
894
+ console.log(' Let Them Talk — Uninstall');
895
+ console.log(' =========================');
896
+ console.log('');
897
+
898
+ // 1. Remove from Claude Code project config (.mcp.json in cwd)
899
+ const mcpLocalPath = path.join(cwd, '.mcp.json');
900
+ if (fs.existsSync(mcpLocalPath)) {
901
+ try {
902
+ const mcpConfig = JSON.parse(fs.readFileSync(mcpLocalPath, 'utf8'));
903
+ if (mcpConfig.mcpServers && mcpConfig.mcpServers['agent-bridge']) {
904
+ delete mcpConfig.mcpServers['agent-bridge'];
905
+ fs.writeFileSync(mcpLocalPath, JSON.stringify(mcpConfig, null, 2) + '\n');
906
+ removed.push('Claude Code (project): ' + mcpLocalPath);
907
+ } else {
908
+ notFound.push('Claude Code (project): no agent-bridge entry in .mcp.json');
909
+ }
910
+ } catch (e) {
911
+ console.log(' [warn] Could not parse ' + mcpLocalPath + ': ' + e.message);
912
+ }
913
+ } else {
914
+ notFound.push('Claude Code (project): .mcp.json not found');
915
+ }
916
+
917
+ // 2. Remove from Claude Code global config (~/.claude/mcp.json)
918
+ const mcpGlobalPath = path.join(home, '.claude', 'mcp.json');
919
+ if (fs.existsSync(mcpGlobalPath)) {
920
+ try {
921
+ const mcpConfig = JSON.parse(fs.readFileSync(mcpGlobalPath, 'utf8'));
922
+ if (mcpConfig.mcpServers && mcpConfig.mcpServers['agent-bridge']) {
923
+ delete mcpConfig.mcpServers['agent-bridge'];
924
+ fs.writeFileSync(mcpGlobalPath, JSON.stringify(mcpConfig, null, 2) + '\n');
925
+ removed.push('Claude Code (global): ' + mcpGlobalPath);
926
+ } else {
927
+ notFound.push('Claude Code (global): no agent-bridge entry');
928
+ }
929
+ } catch (e) {
930
+ console.log(' [warn] Could not parse ' + mcpGlobalPath + ': ' + e.message);
931
+ }
932
+ } else {
933
+ notFound.push('Claude Code (global): ~/.claude/mcp.json not found');
934
+ }
935
+
936
+ // 3. Remove from Gemini CLI config (~/.gemini/settings.json)
937
+ const geminiSettingsPath = path.join(home, '.gemini', 'settings.json');
938
+ if (fs.existsSync(geminiSettingsPath)) {
939
+ try {
940
+ const settings = JSON.parse(fs.readFileSync(geminiSettingsPath, 'utf8'));
941
+ if (settings.mcpServers && settings.mcpServers['agent-bridge']) {
942
+ delete settings.mcpServers['agent-bridge'];
943
+ fs.writeFileSync(geminiSettingsPath, JSON.stringify(settings, null, 2) + '\n');
944
+ removed.push('Gemini CLI: ' + geminiSettingsPath);
945
+ } else {
946
+ notFound.push('Gemini CLI: no agent-bridge entry');
947
+ }
948
+ } catch (e) {
949
+ console.log(' [warn] Could not parse ' + geminiSettingsPath + ': ' + e.message);
950
+ }
951
+ } else {
952
+ notFound.push('Gemini CLI: ~/.gemini/settings.json not found');
953
+ }
954
+
955
+ // 4. Remove from Gemini CLI project config (.gemini/settings.json in cwd)
956
+ const geminiLocalPath = path.join(cwd, '.gemini', 'settings.json');
957
+ if (fs.existsSync(geminiLocalPath)) {
958
+ try {
959
+ const settings = JSON.parse(fs.readFileSync(geminiLocalPath, 'utf8'));
960
+ if (settings.mcpServers && settings.mcpServers['agent-bridge']) {
961
+ delete settings.mcpServers['agent-bridge'];
962
+ fs.writeFileSync(geminiLocalPath, JSON.stringify(settings, null, 2) + '\n');
963
+ removed.push('Gemini CLI (project): ' + geminiLocalPath);
964
+ } else {
965
+ notFound.push('Gemini CLI (project): no agent-bridge entry');
966
+ }
967
+ } catch (e) {
968
+ console.log(' [warn] Could not parse ' + geminiLocalPath + ': ' + e.message);
969
+ }
970
+ }
971
+
972
+ // 5. Remove from Codex CLI config (~/.codex/config.toml)
973
+ const codexConfigPath = path.join(home, '.codex', 'config.toml');
974
+ if (fs.existsSync(codexConfigPath)) {
975
+ try {
976
+ let config = fs.readFileSync(codexConfigPath, 'utf8');
977
+ if (config.includes('[mcp_servers.agent-bridge]')) {
978
+ // Remove from [mcp_servers.agent-bridge] to the next [section] or end of file
979
+ // This covers both [mcp_servers.agent-bridge] and [mcp_servers.agent-bridge.env]
980
+ config = config.replace(/\n?\[mcp_servers\.agent-bridge[^\]]*\][^\[]*(?=\[|$)/g, '');
981
+ // Clean up multiple blank lines left behind
982
+ config = config.replace(/\n{3,}/g, '\n\n');
983
+ fs.writeFileSync(codexConfigPath, config);
984
+ removed.push('Codex CLI: ' + codexConfigPath);
985
+ } else {
986
+ notFound.push('Codex CLI: no agent-bridge section in config.toml');
987
+ }
988
+ } catch (e) {
989
+ console.log(' [warn] Could not process ' + codexConfigPath + ': ' + e.message);
990
+ }
991
+ } else {
992
+ notFound.push('Codex CLI: ~/.codex/config.toml not found');
993
+ }
994
+
995
+ // 6. Remove from Codex CLI project config (.codex/config.toml in cwd)
996
+ const codexLocalPath = path.join(cwd, '.codex', 'config.toml');
997
+ if (fs.existsSync(codexLocalPath)) {
998
+ try {
999
+ let config = fs.readFileSync(codexLocalPath, 'utf8');
1000
+ if (config.includes('[mcp_servers.agent-bridge]')) {
1001
+ config = config.replace(/\n?\[mcp_servers\.agent-bridge[^\]]*\][^\[]*(?=\[|$)/g, '');
1002
+ config = config.replace(/\n{3,}/g, '\n\n');
1003
+ fs.writeFileSync(codexLocalPath, config);
1004
+ removed.push('Codex CLI (project): ' + codexLocalPath);
1005
+ }
1006
+ } catch (e) {
1007
+ console.log(' [warn] Could not process ' + codexLocalPath + ': ' + e.message);
1008
+ }
1009
+ }
1010
+
1011
+ // Print summary
1012
+ if (removed.length > 0) {
1013
+ console.log(' Removed agent-bridge from:');
1014
+ for (const r of removed) {
1015
+ console.log(' [ok] ' + r);
1016
+ }
1017
+ } else {
1018
+ console.log(' No agent-bridge configurations found to remove.');
1019
+ }
1020
+
1021
+ if (notFound.length > 0) {
1022
+ console.log('');
1023
+ console.log(' Skipped (not found):');
1024
+ for (const n of notFound) {
1025
+ console.log(' [-] ' + n);
1026
+ }
1027
+ }
1028
+
1029
+ // 7. Check for data directory
1030
+ const dataPath = path.join(cwd, '.agent-bridge');
1031
+ if (fs.existsSync(dataPath)) {
1032
+ console.log('');
1033
+ console.log(' Found .agent-bridge/ directory with conversation data.');
1034
+ console.log(' To remove it, manually delete: ' + dataPath);
1035
+ }
1036
+
1037
+ console.log('');
1038
+ if (removed.length > 0) {
1039
+ console.log(' Restart your CLI terminals for changes to take effect.');
1040
+ }
1041
+ console.log('');
1042
+ }
1043
+
1044
+ switch (command) {
1045
+ case 'init':
1046
+ init();
1047
+ break;
1048
+ case 'templates':
1049
+ listTemplates();
1050
+ break;
1051
+ case 'dashboard':
1052
+ dashboard();
1053
+ break;
1054
+ case 'reset':
1055
+ reset();
1056
+ break;
1057
+ case 'doctor':
1058
+ cliDoctor();
1059
+ break;
1060
+ case 'msg':
1061
+ case 'message':
1062
+ case 'send':
1063
+ cliMsg();
1064
+ break;
1065
+ case 'status':
1066
+ cliStatus();
1067
+ break;
1068
+ case 'run':
1069
+ cliRun();
1070
+ break;
1071
+ case 'uninstall':
1072
+ case 'remove':
1073
+ uninstall();
1074
+ break;
1075
+ case 'plugin':
1076
+ case 'plugins':
1077
+ console.log(' Plugins have been removed in v3.4.3. CLI terminals have their own extension systems.');
1078
+ break;
1079
+ case 'help':
1080
+ case '--help':
1081
+ case '-h':
1082
+ case undefined:
1083
+ printUsage();
1084
+ break;
1085
+ default:
1086
+ console.error(` Unknown command: ${command}`);
1087
+ printUsage();
1088
+ process.exit(1);
1089
+ }