let-them-talk 5.4.2 → 5.5.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.
Files changed (38) hide show
  1. package/README.md +3 -2
  2. package/USAGE.md +1 -1
  3. package/cli.js +1193 -1264
  4. package/conversation-templates/autonomous-feature.json +4 -4
  5. package/conversation-templates/code-review.json +3 -3
  6. package/conversation-templates/debug-squad.json +3 -3
  7. package/conversation-templates/feature-build.json +3 -3
  8. package/conversation-templates/research-write.json +3 -3
  9. package/dashboard.html +329 -177
  10. package/dashboard.js +3459 -3476
  11. package/package.json +114 -113
  12. package/scripts/check-dashboard-control-plane.js +7 -63
  13. package/server.js +33 -333
  14. package/templates/debate.json +2 -2
  15. package/templates/managed.json +4 -4
  16. package/templates/pair.json +2 -2
  17. package/templates/review.json +2 -2
  18. package/templates/team.json +3 -3
  19. package/vendor/highlight-github-dark.min.css +10 -0
  20. package/vendor/highlight.min.js +1232 -0
  21. package/vendor/katex-fonts/KaTeX_AMS-Regular.woff2 +0 -0
  22. package/vendor/katex-fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  23. package/vendor/katex-fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  24. package/vendor/katex-fonts/KaTeX_Main-Bold.woff2 +0 -0
  25. package/vendor/katex-fonts/KaTeX_Main-Italic.woff2 +0 -0
  26. package/vendor/katex-fonts/KaTeX_Main-Regular.woff2 +0 -0
  27. package/vendor/katex-fonts/KaTeX_Math-Italic.woff2 +0 -0
  28. package/vendor/katex-fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  29. package/vendor/katex-fonts/KaTeX_Script-Regular.woff2 +0 -0
  30. package/vendor/katex-fonts/KaTeX_Size1-Regular.woff2 +0 -0
  31. package/vendor/katex-fonts/KaTeX_Size2-Regular.woff2 +0 -0
  32. package/vendor/katex-fonts/KaTeX_Size3-Regular.woff2 +0 -0
  33. package/vendor/katex-fonts/KaTeX_Size4-Regular.woff2 +0 -0
  34. package/vendor/katex-fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  35. package/vendor/katex.min.css +1 -0
  36. package/vendor/katex.min.js +1 -0
  37. package/vendor/marked.min.js +6 -0
  38. package/vendor/mermaid.min.js +2314 -0
package/cli.js CHANGED
@@ -1,1264 +1,1193 @@
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
- const { resolveDataDir: resolveSharedDataDir } = require('./data-dir');
8
- const { createCanonicalState } = require('./state/canonical');
9
-
10
- function printUsage() {
11
- console.log(`
12
- Let Them Talk — Agent Bridge v5.4.2
13
- MCP message broker for inter-agent communication
14
- Supports: Claude Code, Gemini CLI, Codex CLI, Ollama
15
-
16
- Setup (one-time):
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 <name> Initialize and print an agent template
24
-
25
- After init, use the local launcher (no re-download):
26
- node .agent-bridge/launch.js Dashboard (http://localhost:3000)
27
- node .agent-bridge/launch.js --lan Dashboard on LAN (phone/tablet)
28
- node .agent-bridge/launch.js status Show active agents and message count
29
- node .agent-bridge/launch.js msg <agent> <text> Send a message to an agent
30
- node .agent-bridge/launch.js reset Clear all conversation data
31
- node .agent-bridge/launch.js migrate Backfill canonical event stream from legacy projections
32
- node .agent-bridge/launch.js migrate --dry-run Preview what migrate would do
33
-
34
- Or via npx (re-downloads each time):
35
- npx let-them-talk dashboard
36
- npx let-them-talk status
37
- npx let-them-talk templates List available agent templates
38
- npx let-them-talk uninstall Remove agent-bridge from all CLI configs
39
- npx let-them-talk help Show this help message
40
-
41
- v5.0 — True Autonomy Engine (61 tools):
42
- New tools: get_work, verify_and_advance, start_plan, retry_with_improvement
43
- Proactive work loop: get_work → do work → verify_and_advance → get_work
44
- Parallel workflow steps with dependency graphs (depends_on)
45
- Auto-retry with skill accumulation (3 attempts then team escalation)
46
- Watchdog engine: idle nudge, stuck detection, auto-reassign
47
- 100ms handoff cooldowns in autonomous mode
48
- Plan dashboard: live progress, pause/stop/skip/reassign controls
49
- `);
50
- }
51
-
52
- // Detect which CLIs are installed
53
- function detectCLIs() {
54
- const detected = [];
55
- const home = os.homedir();
56
-
57
- // Claude Code: ~/.claude/ directory exists
58
- if (fs.existsSync(path.join(home, '.claude'))) {
59
- detected.push('claude');
60
- }
61
-
62
- // Gemini CLI: ~/.gemini/ or GEMINI_API_KEY set
63
- if (fs.existsSync(path.join(home, '.gemini')) || process.env.GEMINI_API_KEY) {
64
- detected.push('gemini');
65
- }
66
-
67
- // Codex CLI: ~/.codex/ directory exists
68
- if (fs.existsSync(path.join(home, '.codex'))) {
69
- detected.push('codex');
70
- }
71
-
72
- return detected;
73
- }
74
-
75
- // Detect Ollama installation
76
- function detectOllama() {
77
- try {
78
- const version = execSync('ollama --version', { encoding: 'utf8', timeout: 5000 }).trim();
79
- return { installed: true, version };
80
- } catch {
81
- return { installed: false };
82
- }
83
- }
84
-
85
- // The data directory where all agents read/write — must be the same for server + dashboard
86
- function dataDir(cwd) {
87
- return resolveSharedDataDir({ cwd });
88
- }
89
-
90
- // Configure for Claude Code (.mcp.json in project root)
91
- function setupClaude(serverPath, cwd, log = console.log) {
92
- const mcpConfigPath = path.join(cwd, '.mcp.json');
93
- let mcpConfig = { mcpServers: {} };
94
- if (fs.existsSync(mcpConfigPath)) {
95
- try {
96
- mcpConfig = JSON.parse(fs.readFileSync(mcpConfigPath, 'utf8'));
97
- if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
98
- } catch {
99
- // Backup corrupted file before overwriting
100
- const backup = mcpConfigPath + '.backup';
101
- fs.copyFileSync(mcpConfigPath, backup);
102
- log(' [warn] Existing .mcp.json was invalid — backed up to .mcp.json.backup');
103
- }
104
- }
105
-
106
- mcpConfig.mcpServers['agent-bridge'] = {
107
- command: 'node',
108
- args: [serverPath],
109
- timeout: 300,
110
- };
111
-
112
- fs.writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2) + '\n');
113
- log(' [ok] Claude Code: .mcp.json updated');
114
- }
115
-
116
- // Configure for Gemini CLI (.gemini/settings.json or GEMINI.md with MCP config)
117
- function setupGemini(serverPath, cwd, log = console.log) {
118
- // Gemini CLI uses .gemini/settings.json for MCP configuration
119
- const geminiDir = path.join(cwd, '.gemini');
120
- const settingsPath = path.join(geminiDir, 'settings.json');
121
-
122
- if (!fs.existsSync(geminiDir)) {
123
- fs.mkdirSync(geminiDir, { recursive: true });
124
- }
125
-
126
- let settings = { mcpServers: {} };
127
- if (fs.existsSync(settingsPath)) {
128
- try {
129
- settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
130
- if (!settings.mcpServers) settings.mcpServers = {};
131
- } catch {
132
- const backup = settingsPath + '.backup';
133
- fs.copyFileSync(settingsPath, backup);
134
- log(' [warn] Existing settings.json was invalid — backed up to settings.json.backup');
135
- }
136
- }
137
-
138
- settings.mcpServers['agent-bridge'] = {
139
- command: 'node',
140
- args: [serverPath],
141
- timeout: 300,
142
- trust: true,
143
- };
144
-
145
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
146
- log(' [ok] Gemini CLI: .gemini/settings.json updated');
147
- }
148
-
149
- // Configure for Codex CLI (uses .codex/config.toml)
150
- function setupCodex(serverPath, cwd, log = console.log) {
151
- const codexDir = path.join(cwd, '.codex');
152
- const configPath = path.join(codexDir, 'config.toml');
153
-
154
- if (!fs.existsSync(codexDir)) {
155
- fs.mkdirSync(codexDir, { recursive: true });
156
- }
157
-
158
- // Read existing config or start fresh
159
- let config = '';
160
- if (fs.existsSync(configPath)) {
161
- config = fs.readFileSync(configPath, 'utf8');
162
- }
163
-
164
- // Backup existing config before modifying
165
- if (fs.existsSync(configPath)) {
166
- fs.copyFileSync(configPath, configPath + '.backup');
167
- }
168
-
169
- // Only add if not already present
170
- if (!config.includes('[mcp_servers.agent-bridge]')) {
171
- const tomlBlock = `
172
- [mcp_servers.agent-bridge]
173
- command = "node"
174
- args = [${JSON.stringify(serverPath)}]
175
- timeout = 300
176
- `;
177
- config += tomlBlock;
178
- fs.writeFileSync(configPath, config);
179
- }
180
-
181
- log(' [ok] Codex CLI: .codex/config.toml updated');
182
- }
183
-
184
- // Setup Ollama agent bridge script
185
- function setupOllama(serverPath, cwd, log = console.log) {
186
- const dir = dataDir(cwd);
187
- const scriptPath = path.join(dir, 'ollama-agent.js');
188
-
189
- if (!fs.existsSync(dir)) {
190
- fs.mkdirSync(dir, { recursive: true });
191
- }
192
-
193
- const script = `#!/usr/bin/env node
194
- // ollama-agent.js - bridges Ollama to Let Them Talk
195
- // Usage: node .agent-bridge/ollama-agent.js [agent-name] [model]
196
- const fs = require('fs'), path = require('path'), http = require('http');
197
- const DATA_DIR = path.join(__dirname);
198
- const name = process.argv[2] || 'Ollama';
199
- if (!/^[a-zA-Z0-9_-]{1,20}$/.test(name)) throw new Error('Invalid agent name');
200
- const model = process.argv[3] || 'llama3';
201
- const OLLAMA_URL = process.env.OLLAMA_URL || 'http://localhost:11434';
202
-
203
- function readJson(f) { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch { return {}; } }
204
- 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); }
205
-
206
- // Register agent
207
- function register() {
208
- const agentsFile = path.join(DATA_DIR, 'agents.json');
209
- const agents = readJson(agentsFile);
210
- agents[name] = { pid: process.pid, timestamp: new Date().toISOString(), last_activity: new Date().toISOString(), provider: 'Ollama (' + model + ')' };
211
- fs.writeFileSync(agentsFile, JSON.stringify(agents, null, 2));
212
- console.log('[' + name + '] Registered (PID ' + process.pid + ', model: ' + model + ')');
213
- }
214
-
215
- // Update heartbeat
216
- function heartbeat() {
217
- const agentsFile = path.join(DATA_DIR, 'agents.json');
218
- const agents = readJson(agentsFile);
219
- if (agents[name]) {
220
- agents[name].last_activity = new Date().toISOString();
221
- agents[name].pid = process.pid;
222
- fs.writeFileSync(agentsFile, JSON.stringify(agents, null, 2));
223
- }
224
- }
225
-
226
- // Call Ollama API
227
- function callOllama(prompt) {
228
- return new Promise(function(resolve, reject) {
229
- const url = new URL(OLLAMA_URL + '/api/chat');
230
- const body = JSON.stringify({ model: model, messages: [{ role: 'user', content: prompt }], stream: false });
231
- const req = http.request(url, { method: 'POST', headers: { 'Content-Type': 'application/json' } }, function(res) {
232
- let data = '';
233
- res.on('data', function(c) { data += c; });
234
- res.on('end', function() {
235
- try { const j = JSON.parse(data); resolve(j.message ? j.message.content : data); }
236
- catch { resolve(data); }
237
- });
238
- });
239
- req.on('error', reject);
240
- req.write(body);
241
- req.end();
242
- });
243
- }
244
-
245
- // Send a message
246
- function sendMessage(to, content) {
247
- const msgId = 'm' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
248
- const msg = { id: msgId, from: name, to: to, content: content, timestamp: new Date().toISOString() };
249
- fs.appendFileSync(path.join(DATA_DIR, 'messages.jsonl'), JSON.stringify(msg) + '\\n');
250
- fs.appendFileSync(path.join(DATA_DIR, 'history.jsonl'), JSON.stringify(msg) + '\\n');
251
- console.log('[' + name + '] -> ' + to + ': ' + content.substring(0, 80) + (content.length > 80 ? '...' : ''));
252
- }
253
-
254
- // Listen for messages
255
- let lastOffset = 0;
256
- function checkMessages() {
257
- const consumedFile = path.join(DATA_DIR, 'consumed-' + name + '.json');
258
- const consumed = readJson(consumedFile);
259
- lastOffset = consumed.offset || 0;
260
-
261
- const messages = readJsonl(path.join(DATA_DIR, 'messages.jsonl'));
262
- const newMsgs = messages.slice(lastOffset).filter(function(m) {
263
- return m.to === name || (m.to === 'all' && m.from !== name);
264
- });
265
-
266
- if (newMsgs.length > 0) {
267
- consumed.offset = messages.length;
268
- fs.writeFileSync(consumedFile, JSON.stringify(consumed));
269
- }
270
-
271
- return newMsgs;
272
- }
273
-
274
- async function processMessages() {
275
- const msgs = checkMessages();
276
- for (const m of msgs) {
277
- console.log('[' + name + '] <- ' + m.from + ': ' + m.content.substring(0, 80));
278
- try {
279
- const response = await callOllama(m.content);
280
- sendMessage(m.from, response);
281
- } catch (e) {
282
- sendMessage(m.from, 'Error calling Ollama: ' + e.message);
283
- }
284
- }
285
- }
286
-
287
- // Main loop
288
- register();
289
- const hb = setInterval(heartbeat, 10000);
290
- hb.unref();
291
- console.log('[' + name + '] Listening for messages... (Ctrl+C to stop)');
292
- setInterval(processMessages, 2000);
293
-
294
- // Cleanup on exit
295
- process.on('SIGINT', function() { console.log('\\n[' + name + '] Shutting down.'); process.exit(0); });
296
- `;
297
-
298
- const tmpPath = scriptPath + '.tmp.' + process.pid;
299
- fs.writeFileSync(tmpPath, script);
300
- fs.renameSync(tmpPath, scriptPath);
301
- log(' [ok] Ollama agent script created: .agent-bridge/ollama-agent.js');
302
- log('');
303
- log(' Launch an Ollama agent with:');
304
- log(' node .agent-bridge/ollama-agent.js <name> <model>');
305
- log('');
306
- log(' Examples:');
307
- log(' node .agent-bridge/ollama-agent.js Ollama llama3');
308
- log(' node .agent-bridge/ollama-agent.js Coder codellama');
309
- log(' node .agent-bridge/ollama-agent.js Writer mistral');
310
- }
311
-
312
- // Merge-safe AGENTS.md / CLAUDE.md updater. Writes a marker block with the
313
- // background-worker rules so any agent cd'd into this folder gets them every
314
- // session. Re-running replaces only the marked block, leaving user content alone.
315
- function setupAgentsMarkdown(cwd, log = console.log) {
316
- const BEGIN = '<!-- BEGIN let-them-talk (auto-managed — do not edit between markers) -->';
317
- const END = '<!-- END let-them-talk -->';
318
- const block = [
319
- BEGIN,
320
- '',
321
- '## Let Them Talk — Background-Worker Mode',
322
- '',
323
- 'This project uses the `agent-bridge` MCP server for multi-agent coordination.',
324
- 'When you run in this folder, you are a **background worker on a team**, not an',
325
- 'interactive chat assistant. Follow these rules strictly:',
326
- '',
327
- '1. **Your CLI terminal output is invisible** to the owner and to every other',
328
- ' agent. If you want anyone to see something, it MUST go through',
329
- ' `send_message(to="...", content="...")` or `broadcast(content="...")`.',
330
- '',
331
- '2. **No narration in terminal.** Do not "reply" to messages in your terminal',
332
- ' window. Do not summarize your progress in terminal. Do not print status',
333
- ' updates in terminal. Those are invisible. Talk like a human on a team chat',
334
- ' — announce starts, finishes, blockers, and questions via `send_message`.',
335
- '',
336
- '3. **Stay in the listen loop.** After every action, call `listen_group()` (or',
337
- ' `listen()` in direct mode). When it returns an empty batch, that is NORMAL',
338
- ' — call it again immediately. If it returns a tool error like',
339
- ' `"timed out awaiting tools/call"`, that is a Codex-level transport hiccup',
340
- ' — immediately call it again. Never stop looping, never treat an empty',
341
- ' return or tool error as "done".',
342
- '',
343
- '4. **Reply to Dashboard/Owner via `send_message(to="Dashboard")`.** The owner',
344
- ' reads replies in the dashboard Messages tab, not your terminal.',
345
- '',
346
- '5. **Do not answer on another agent\'s behalf.** If a message targets a',
347
- ' specific agent (`msg.to`), only that agent should reply.',
348
- '',
349
- '6. **Self-reliance.** When the Owner gives you a goal, break it down',
350
- ' yourself and work until done. NEVER stop to ask "should I do X?" or',
351
- ' "do you want me to Y?" for decisions the team can make. Decide,',
352
- ' `log_decision()` to record the choice, continue.',
353
- '',
354
- '7. **Team-first escalation.** Before DMing Owner with a question, try',
355
- ' these in order: (a) `kb_read()` — did the team already decide this?',
356
- ' (b) DM a teammate with the relevant skill (use `list_agents()`).',
357
- ' (c) `call_vote()` if the team genuinely disagrees. (d) `log_decision()`',
358
- ' to lock in your choice and move forward. Only escalate to Owner when',
359
- ' the overall goal is complete OR a true blocker only the Owner can',
360
- ' resolve (credentials, priorities, business rules).',
361
- '',
362
- '8. **Done-when-done.** "Done" means the Owner\'s original GOAL is',
363
- ' achieved, not the current step. After `verify_and_advance()`, call',
364
- ' `get_work()` again. If nothing is queued and the goal is not yet',
365
- ' done, synthesize new tasks with `create_task()` and keep going.',
366
- '',
367
- '9. The loop only ends when the goal is achieved with evidence OR the',
368
- ' Owner sends a message telling you to stop.',
369
- '',
370
- END,
371
- ].join('\n');
372
-
373
- const targets = [
374
- { file: 'AGENTS.md', label: 'Codex / oh-my-codex' },
375
- { file: 'CLAUDE.md', label: 'Claude Code' },
376
- ];
377
-
378
- for (const { file, label } of targets) {
379
- const fp = path.join(cwd, file);
380
- let existing = '';
381
- let existed = false;
382
- if (fs.existsSync(fp)) {
383
- existing = fs.readFileSync(fp, 'utf8');
384
- existed = true;
385
- }
386
-
387
- const markerRegex = new RegExp(
388
- BEGIN.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +
389
- '[\\s\\S]*?' +
390
- END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'),
391
- 'g'
392
- );
393
-
394
- let next;
395
- if (markerRegex.test(existing)) {
396
- // Replace only the managed block
397
- next = existing.replace(markerRegex, block);
398
- log(' [ok] ' + file + ': refreshed Let Them Talk block (' + label + ')');
399
- } else if (existed) {
400
- // Append below user content
401
- const separator = existing.endsWith('\n') ? '\n' : '\n\n';
402
- next = existing + separator + block + '\n';
403
- log(' [ok] ' + file + ': appended Let Them Talk block (' + label + ')');
404
- } else {
405
- // New file minimal content so only our block is present
406
- next = '# ' + path.basename(cwd) + ' — Agent Instructions\n\n' + block + '\n';
407
- log(' [ok] ' + file + ': created with Let Them Talk block (' + label + ')');
408
- }
409
- fs.writeFileSync(fp, next);
410
- }
411
- }
412
-
413
- function init(options) {
414
- const opts = options || {};
415
- const cwd = opts.cwd || process.cwd();
416
- const serverPath = path.join(__dirname, 'server.js').replace(/\\/g, '/');
417
- const gitignorePath = path.join(cwd, '.gitignore');
418
- const argv = Array.isArray(opts.argv) ? opts.argv : process.argv;
419
- const flag = opts.flag !== undefined ? opts.flag : argv[3];
420
- const log = typeof opts.log === 'function' ? opts.log : console.log;
421
-
422
- log('');
423
- log(' Let Them Talk Initializing Agent Bridge');
424
- log(' ==========================================');
425
- log('');
426
-
427
- let targets = [];
428
-
429
- if (flag === '--claude') {
430
- targets = ['claude'];
431
- } else if (flag === '--gemini') {
432
- targets = ['gemini'];
433
- } else if (flag === '--codex') {
434
- targets = ['codex'];
435
- } else if (flag === '--all') {
436
- targets = ['claude', 'gemini', 'codex'];
437
- } else if (flag === '--ollama') {
438
- const ollama = detectOllama();
439
- if (!ollama.installed) {
440
- log(' Ollama not found. Install it from: https://ollama.com/download');
441
- log(' After installing, run: ollama pull llama3');
442
- log('');
443
- } else {
444
- log(' Ollama detected: ' + ollama.version);
445
- setupOllama(serverPath, cwd, log);
446
- }
447
- targets = detectCLIs();
448
- if (targets.length === 0) targets = ['claude'];
449
- } else {
450
- // Auto-detect
451
- targets = detectCLIs();
452
- if (targets.length === 0) {
453
- // Default to claude if nothing detected
454
- targets = ['claude'];
455
- log(' No CLI detected, defaulting to Claude Code config.');
456
- } else {
457
- log(` Detected CLI(s): ${targets.join(', ')}`);
458
- }
459
- }
460
-
461
- log('');
462
-
463
- for (const target of targets) {
464
- switch (target) {
465
- case 'claude': setupClaude(serverPath, cwd, log); break;
466
- case 'gemini': setupGemini(serverPath, cwd, log); break;
467
- case 'codex': setupCodex(serverPath, cwd, log); break;
468
- }
469
- }
470
-
471
- // Persistent system-level directive for any agent that starts in this folder.
472
- // Codex (via oh-my-codex's developer_instructions) and Claude Code both read
473
- // AGENTS.md / CLAUDE.md automatically on startup. A marker block lets us merge
474
- // in/out cleanly without clobbering whatever else the user has written.
475
- setupAgentsMarkdown(cwd, log);
476
-
477
- // Add .agent-bridge/ and MCP config files to .gitignore
478
- const gitignoreEntries = ['.agent-bridge/', '.mcp.json', '.codex/', '.gemini/'];
479
- if (fs.existsSync(gitignorePath)) {
480
- let content = fs.readFileSync(gitignorePath, 'utf8');
481
- const missing = gitignoreEntries.filter(e => !content.includes(e));
482
- if (missing.length) {
483
- content += '\n# Agent Bridge (auto-added by let-them-talk init)\n' + missing.join('\n') + '\n';
484
- fs.writeFileSync(gitignorePath, content);
485
- log(' [ok] Added to .gitignore: ' + missing.join(', '));
486
- } else {
487
- log(' [ok] .gitignore already configured');
488
- }
489
- } else {
490
- fs.writeFileSync(gitignorePath, '# Agent Bridge (auto-added by let-them-talk init)\n' + gitignoreEntries.join('\n') + '\n');
491
- log(' [ok] .gitignore created');
492
- }
493
-
494
- // Save local launcher scripts so users never need to re-download
495
- const bridgeDir = dataDir(cwd);
496
- if (!fs.existsSync(bridgeDir)) {
497
- fs.mkdirSync(bridgeDir, { recursive: true });
498
- }
499
-
500
- const cliPath = path.join(__dirname, 'cli.js').replace(/\\/g, '/');
501
-
502
- // Dashboard launcher - run with: node .agent-bridge/launch.js
503
- const launcherScript = `#!/usr/bin/env node
504
- // Auto-generated by let-them-talk init - launch dashboard without re-downloading
505
- // Usage: node .agent-bridge/launch.js [--lan|dashboard|status|reset|msg]
506
-
507
- const firstArg = process.argv[2] || 'dashboard';
508
- const cliPath = ${JSON.stringify(cliPath)};
509
-
510
- try {
511
- require('fs').accessSync(cliPath);
512
- } catch {
513
- console.error(' Let Them Talk CLI not found at: ' + cliPath);
514
- console.error(' The npx cache may have been cleaned. Fix with either:');
515
- console.error(' npx let-them-talk init (re-creates launcher)');
516
- console.error(' npm i -g let-them-talk (permanent global install)');
517
- process.exit(1);
518
- }
519
-
520
- // Forward to cli.js with the command
521
- const forwardedArgs = firstArg === '--lan'
522
- ? ['dashboard', '--lan', ...process.argv.slice(3)]
523
- : [firstArg, ...process.argv.slice(3)];
524
- process.argv = [process.argv[0], cliPath, ...forwardedArgs];
525
- require(cliPath);
526
- `;
527
-
528
- fs.writeFileSync(path.join(bridgeDir, 'launch.js'), launcherScript);
529
- const launcherPath = path.join(bridgeDir, 'launch.js');
530
- log(' [ok] Local launcher saved to .agent-bridge/launch.js');
531
-
532
- log('');
533
- log(' Agent Bridge is ready! Restart your CLI to pick up the MCP tools.');
534
- log('');
535
-
536
- // Show template if --template was provided
537
- var templateFlag = null;
538
- for (var i = 3; i < argv.length; i++) {
539
- if (argv[i] === '--template' && argv[i + 1]) {
540
- templateFlag = argv[i + 1];
541
- break;
542
- }
543
- }
544
-
545
- if (templateFlag) {
546
- showTemplate(templateFlag);
547
- } else {
548
- log(' Open two terminals and start a conversation between agents.');
549
- log(' Tip: Use "npx let-them-talk init --template pair" for ready-made prompts.');
550
- log('');
551
- log(' \x1b[1m Monitor:\x1b[0m');
552
- log(' node .agent-bridge/launch.js (dashboard)');
553
- log(' node .agent-bridge/launch.js status (agent status)');
554
- log(' node .agent-bridge/launch.js reset (clear data)');
555
- log('');
556
- log(' Or use npx (re-downloads each time):');
557
- log(' npx let-them-talk dashboard');
558
- log('');
559
- }
560
-
561
- return {
562
- cwd,
563
- flag: flag || null,
564
- targets,
565
- bridgeDir,
566
- launcherPath,
567
- };
568
- }
569
-
570
- function reset() {
571
- const targetDir = resolveDataDirCli();
572
-
573
- if (!fs.existsSync(targetDir)) {
574
- console.log(' No .agent-bridge/ directory found. Nothing to reset.');
575
- return;
576
- }
577
-
578
- // Safety: count messages to show user what they're about to delete
579
- const historyFile = path.join(targetDir, 'history.jsonl');
580
- let msgCount = 0;
581
- if (fs.existsSync(historyFile)) {
582
- msgCount = fs.readFileSync(historyFile, 'utf8').split(/\r?\n/).filter(l => l.trim()).length;
583
- }
584
-
585
- // Require --force flag, otherwise warn and exit
586
- if (!process.argv.includes('--force')) {
587
- console.log('');
588
- console.log(' ⚠ This will permanently delete all conversation data in:');
589
- console.log(' ' + targetDir);
590
- if (msgCount > 0) console.log(' (' + msgCount + ' messages in history)');
591
- console.log('');
592
- console.log(' To confirm, run: npx let-them-talk reset --force');
593
- console.log('');
594
- return;
595
- }
596
-
597
- // Auto-archive before deleting
598
- const archiveDir = path.join(targetDir, '..', '.agent-bridge-archive');
599
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
600
- const archivePath = path.join(archiveDir, timestamp);
601
- try {
602
- const archiveResult = getCanonicalStateCli().archiveFiles({
603
- fileNames: ['history.jsonl', 'messages.jsonl', 'agents.json', 'decisions.json', 'tasks.json'],
604
- destinationDir: archivePath,
605
- });
606
- if (archiveResult.archived > 0) {
607
- console.log(' [ok] Archived ' + archiveResult.archived + ' files to .agent-bridge-archive/' + timestamp + '/');
608
- }
609
- } catch (e) {
610
- console.log(' [warn] Could not archive: ' + e.message + ' — proceeding with reset anyway.');
611
- }
612
-
613
- getCanonicalStateCli().resetRuntime({
614
- fixedFileNames: [
615
- 'messages.jsonl',
616
- 'history.jsonl',
617
- 'agents.json',
618
- 'acks.json',
619
- 'tasks.json',
620
- 'profiles.json',
621
- 'workflows.json',
622
- 'branches.json',
623
- 'read_receipts.json',
624
- 'permissions.json',
625
- 'config.json',
626
- 'decisions.json',
627
- ],
628
- });
629
- console.log(' Cleared all data from ' + targetDir);
630
- }
631
-
632
- function getTemplates() {
633
- var all = [];
634
-
635
- // 1. Built-in templates (shipped with the package)
636
- var builtinDir = path.join(__dirname, 'templates');
637
- if (fs.existsSync(builtinDir)) {
638
- fs.readdirSync(builtinDir).filter(f => f.endsWith('.json')).forEach(f => {
639
- try { var t = JSON.parse(fs.readFileSync(path.join(builtinDir, f), 'utf8')); t._source = 'built-in'; all.push(t); }
640
- catch {}
641
- });
642
- }
643
-
644
- // 2. Project-local templates: .agent-bridge/templates/ in current working directory
645
- var localDir = path.join(resolveDataDirCli(), 'templates');
646
- if (fs.existsSync(localDir)) {
647
- fs.readdirSync(localDir).filter(f => f.endsWith('.json')).forEach(f => {
648
- try {
649
- var t = JSON.parse(fs.readFileSync(path.join(localDir, f), 'utf8'));
650
- t._source = 'local';
651
- // Don't add duplicates (local overrides built-in with same name)
652
- var existing = all.findIndex(e => e.name === t.name);
653
- if (existing >= 0) all[existing] = t;
654
- else all.push(t);
655
- } catch {}
656
- });
657
- }
658
-
659
- // 3. User-global templates: ~/.let-them-talk/templates/
660
- var homeDir = process.env.HOME || process.env.USERPROFILE || '';
661
- var globalDir = path.join(homeDir, '.let-them-talk', 'templates');
662
- if (fs.existsSync(globalDir)) {
663
- fs.readdirSync(globalDir).filter(f => f.endsWith('.json')).forEach(f => {
664
- try {
665
- var t = JSON.parse(fs.readFileSync(path.join(globalDir, f), 'utf8'));
666
- t._source = 'global';
667
- var existing = all.findIndex(e => e.name === t.name);
668
- if (existing >= 0) all[existing] = t;
669
- else all.push(t);
670
- } catch {}
671
- });
672
- }
673
-
674
- return all;
675
- }
676
-
677
- function listTemplates() {
678
- const templates = getTemplates();
679
- console.log('');
680
- console.log(' Available Agent Templates');
681
- console.log(' ========================');
682
- console.log('');
683
- for (const t of templates) {
684
- const agentNames = t.agents.map(a => a.name).join(', ');
685
- const sourceTag = t._source === 'local' ? ' [local]' : t._source === 'global' ? ' [global]' : '';
686
- console.log(' ' + t.name.padEnd(12) + ' ' + t.description + sourceTag);
687
- console.log(' ' + ''.padEnd(12) + ' Agents: ' + agentNames);
688
- console.log('');
689
- }
690
- console.log(' Usage: npx let-them-talk init --template <name>');
691
- console.log(' Note: this command lists agent templates only. Conversation workflow templates ship separately in agent-bridge/conversation-templates/*.json.');
692
- console.log('');
693
- console.log(' Custom templates:');
694
- console.log(' Project-local: .agent-bridge/templates/*.json');
695
- console.log(' User-global: ~/.let-them-talk/templates/*.json');
696
- console.log('');
697
- }
698
-
699
- function showTemplate(templateName) {
700
- const templates = getTemplates();
701
- const template = templates.find(t => t.name === templateName);
702
- if (!template) {
703
- console.error(' Unknown template: ' + templateName);
704
- console.error(' Available: ' + templates.map(t => t.name).join(', '));
705
- process.exit(1);
706
- }
707
-
708
- console.log('');
709
- console.log(' Template: ' + template.name);
710
- console.log(' ' + template.description);
711
- console.log('');
712
- console.log(' Copy these agent prompts into each terminal:');
713
- console.log(' ======================================');
714
- console.log(' These prompts assume current onboarding: register, get_briefing(), then get_guide() when you need the current rules.');
715
-
716
- for (var i = 0; i < template.agents.length; i++) {
717
- var a = template.agents[i];
718
- console.log('');
719
- console.log(' --- Terminal ' + (i + 1) + ': ' + a.name + ' (' + a.role + ') ---');
720
- console.log('');
721
- console.log(' ' + a.prompt.replace(/\n/g, '\n '));
722
- console.log('');
723
- }
724
- }
725
-
726
- function dashboard() {
727
- if (process.argv.includes('--lan')) {
728
- process.env.AGENT_BRIDGE_LAN = 'true';
729
- }
730
- require('./dashboard.js');
731
- }
732
-
733
- function resolveDataDirCli() {
734
- return resolveSharedDataDir();
735
- }
736
-
737
- function getCanonicalStateCli() {
738
- return createCanonicalState({ dataDir: resolveDataDirCli(), processPid: process.pid });
739
- }
740
-
741
- function readJsonl(filePath) {
742
- if (!fs.existsSync(filePath)) return [];
743
- return fs.readFileSync(filePath, 'utf8')
744
- .split(/\r?\n/)
745
- .filter(l => l.trim())
746
- .map(l => { try { return JSON.parse(l); } catch { return null; } })
747
- .filter(Boolean);
748
- }
749
-
750
- function readJson(filePath) {
751
- if (!fs.existsSync(filePath)) return {};
752
- try { return JSON.parse(fs.readFileSync(filePath, 'utf8')); } catch { return {}; }
753
- }
754
-
755
- function isPidAlive(pid) {
756
- if (!pid) return false;
757
- try { process.kill(pid, 0); return true; } catch { return false; }
758
- }
759
-
760
- function cliMsg() {
761
- const recipient = process.argv[3];
762
- const textParts = process.argv.slice(4);
763
- if (!recipient || !textParts.length) {
764
- console.error(' Usage: npx let-them-talk msg <agent> <text>');
765
- process.exit(1);
766
- }
767
- if (!/^[a-zA-Z0-9_-]{1,20}$/.test(recipient)) {
768
- console.error(' Agent name must be 1-20 alphanumeric characters (with _ or -).');
769
- process.exit(1);
770
- }
771
- const text = textParts.join(' ');
772
- const dir = resolveDataDirCli();
773
- if (!fs.existsSync(dir)) {
774
- console.error(' No .agent-bridge/ directory found. Run "npx let-them-talk init" first.');
775
- process.exit(1);
776
- }
777
-
778
- const msgId = 'm' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
779
- const msg = {
780
- id: msgId,
781
- from: 'CLI',
782
- to: recipient,
783
- content: text,
784
- timestamp: new Date().toISOString(),
785
- };
786
-
787
- getCanonicalStateCli().appendMessage(msg);
788
-
789
- console.log(' Message sent to ' + recipient + ': ' + text);
790
- }
791
-
792
- function cliStatus() {
793
- const dir = resolveDataDirCli();
794
- if (!fs.existsSync(dir)) {
795
- console.error(' No .agent-bridge/ directory found. Run "npx let-them-talk init" first.');
796
- process.exit(1);
797
- }
798
-
799
- const agents = readJson(path.join(dir, 'agents.json'));
800
- const history = readJsonl(path.join(dir, 'history.jsonl'));
801
- const profiles = readJson(path.join(dir, 'profiles.json'));
802
- const workflows = readJson(path.join(dir, 'workflows.json'));
803
- const tasks = readJson(path.join(dir, 'tasks.json'));
804
-
805
- // Merge heartbeat files for live activity data
806
- try {
807
- const files = fs.readdirSync(dir).filter(f => f.startsWith('heartbeat-') && f.endsWith('.json'));
808
- for (const f of files) {
809
- const name = f.slice(10, -5);
810
- if (agents[name]) {
811
- try {
812
- const hb = JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8'));
813
- if (hb.last_activity) agents[name].last_activity = hb.last_activity;
814
- if (hb.pid) agents[name].pid = hb.pid;
815
- } catch {}
816
- }
817
- }
818
- } catch {}
819
-
820
- const onlineCount = Object.values(agents).filter(a => isPidAlive(a.pid)).length;
821
-
822
- console.log('');
823
- console.log(' Let Them Talk — Status');
824
- console.log(' =======================');
825
- console.log(' Messages: ' + history.length + ' | Agents: ' + onlineCount + '/' + Object.keys(agents).length + ' online');
826
- console.log('');
827
-
828
- // Agents with roles
829
- const names = Object.keys(agents);
830
- if (!names.length) {
831
- console.log(' No agents registered.');
832
- } else {
833
- console.log(' Agents:');
834
- for (const name of names) {
835
- const info = agents[name];
836
- const alive = isPidAlive(info.pid);
837
- const status = alive ? '\x1b[32monline\x1b[0m' : '\x1b[31moffline\x1b[0m';
838
- const lastActivity = info.last_activity || info.timestamp || '';
839
- const role = (profiles && profiles[name] && profiles[name].role) ? ' [' + profiles[name].role + ']' : '';
840
- const msgCount = history.filter(m => m.from === name).length;
841
- console.log(' ' + name.padEnd(16) + ' ' + status + role.padEnd(16) + ' msgs: ' + msgCount + ' last: ' + (lastActivity ? new Date(lastActivity).toLocaleTimeString() : '-'));
842
- }
843
- }
844
-
845
- // Active workflows
846
- const activeWfs = Array.isArray(workflows) ? workflows.filter(w => w.status === 'active') : [];
847
- if (activeWfs.length > 0) {
848
- console.log('');
849
- console.log(' Workflows:');
850
- for (const wf of activeWfs) {
851
- const done = wf.steps.filter(s => s.status === 'done').length;
852
- const total = wf.steps.length;
853
- const pct = Math.round((done / total) * 100);
854
- const mode = wf.autonomous ? ' (autonomous)' : '';
855
- console.log(' ' + wf.name.padEnd(24) + ' ' + done + '/' + total + ' (' + pct + '%)' + mode);
856
- }
857
- }
858
-
859
- // Active tasks
860
- const activeTasks = Array.isArray(tasks) ? tasks.filter(t => t.status === 'in_progress') : [];
861
- if (activeTasks.length > 0) {
862
- console.log('');
863
- console.log(' Tasks in progress:');
864
- for (const t of activeTasks.slice(0, 5)) {
865
- console.log(' ' + (t.title || 'Untitled').padEnd(30) + ' -> ' + (t.assignee || 'unassigned'));
866
- }
867
- if (activeTasks.length > 5) console.log(' ... and ' + (activeTasks.length - 5) + ' more');
868
- }
869
-
870
- console.log('');
871
- }
872
-
873
- function cliMigrate() {
874
- const args = process.argv.slice(3);
875
- const dryRun = args.includes('--dry-run') || args.includes('-n');
876
- const positional = args.filter((a) => !a.startsWith('-'));
877
- const projectArg = positional[0] || process.cwd();
878
- const { migrate } = require('./scripts/migrate-legacy-to-canonical');
879
- migrate(projectArg, { dryRun });
880
- }
881
-
882
- // v5.0: Diagnostic health check
883
- function cliDoctor() {
884
- console.log('');
885
- console.log(' \x1b[1mLet Them Talk — Doctor\x1b[0m');
886
- console.log(' ======================');
887
- let issues = 0;
888
-
889
- // Check data directory
890
- const dir = resolveDataDirCli();
891
- if (fs.existsSync(dir)) {
892
- console.log(' \x1b[32m✓\x1b[0m .agent-bridge/ directory exists');
893
- try { fs.accessSync(dir, fs.constants.W_OK); console.log(' \x1b[32m✓\x1b[0m .agent-bridge/ is writable'); }
894
- catch { console.log(' \x1b[31m✗\x1b[0m .agent-bridge/ is NOT writable'); issues++; }
895
- } else {
896
- console.log(' \x1b[33m!\x1b[0m .agent-bridge/ not found. Run "npx let-them-talk init" first.');
897
- issues++;
898
- }
899
-
900
- // Check server.js
901
- const serverPath = path.join(__dirname, 'server.js');
902
- if (fs.existsSync(serverPath)) {
903
- console.log(' \x1b[32m✓\x1b[0m server.js found');
904
- } else {
905
- console.log(' \x1b[31m✗\x1b[0m server.js MISSING'); issues++;
906
- }
907
-
908
- // Check agents online
909
- if (fs.existsSync(dir)) {
910
- const agentsFile = path.join(dir, 'agents.json');
911
- if (fs.existsSync(agentsFile)) {
912
- const agents = readJson(agentsFile);
913
- const online = Object.entries(agents).filter(([, a]) => isPidAlive(a.pid)).length;
914
- const total = Object.keys(agents).length;
915
- if (online > 0) {
916
- console.log(' \x1b[32m✓\x1b[0m ' + online + '/' + total + ' agents online');
917
- } else if (total > 0) {
918
- console.log(' \x1b[33m!\x1b[0m ' + total + ' agents registered but none online');
919
- } else {
920
- console.log(' \x1b[33m!\x1b[0m No agents registered yet');
921
- }
922
- }
923
-
924
- // Check config
925
- const configFile = path.join(dir, 'config.json');
926
- if (fs.existsSync(configFile)) {
927
- const config = readJson(configFile);
928
- console.log(' \x1b[32m✓\x1b[0m Conversation mode: ' + (config.conversation_mode || 'direct'));
929
- }
930
-
931
- // Check guide file
932
- const guideFile = path.join(dir, 'guide.md');
933
- if (fs.existsSync(guideFile)) {
934
- console.log(' \x1b[32m✓\x1b[0m Custom guide.md found');
935
- }
936
- }
937
-
938
- // Check Node version
939
- const nodeVersion = process.version;
940
- const major = parseInt(nodeVersion.slice(1));
941
- if (major >= 18) {
942
- console.log(' \x1b[32m✓\x1b[0m Node.js ' + nodeVersion + ' (OK)');
943
- } else {
944
- console.log(' \x1b[31m✗\x1b[0m Node.js ' + nodeVersion + ' — v18+ recommended'); issues++;
945
- }
946
-
947
- console.log('');
948
- if (issues === 0) {
949
- console.log(' \x1b[32mAll checks passed. System is healthy.\x1b[0m');
950
- } else {
951
- console.log(' \x1b[31m' + issues + ' issue(s) found. Fix them and run doctor again.\x1b[0m');
952
- }
953
- console.log('');
954
- }
955
-
956
- // Uninstall agent-bridge from all CLI configs
957
- function uninstall() {
958
- const cwd = process.cwd();
959
- const home = os.homedir();
960
- const removed = [];
961
- const notFound = [];
962
-
963
- console.log('');
964
- console.log(' Let Them Talk — Uninstall');
965
- console.log(' =========================');
966
- console.log('');
967
-
968
- // 1. Remove from Claude Code project config (.mcp.json in cwd)
969
- const mcpLocalPath = path.join(cwd, '.mcp.json');
970
- if (fs.existsSync(mcpLocalPath)) {
971
- try {
972
- const mcpConfig = JSON.parse(fs.readFileSync(mcpLocalPath, 'utf8'));
973
- if (mcpConfig.mcpServers && mcpConfig.mcpServers['agent-bridge']) {
974
- delete mcpConfig.mcpServers['agent-bridge'];
975
- fs.writeFileSync(mcpLocalPath, JSON.stringify(mcpConfig, null, 2) + '\n');
976
- removed.push('Claude Code (project): ' + mcpLocalPath);
977
- } else {
978
- notFound.push('Claude Code (project): no agent-bridge entry in .mcp.json');
979
- }
980
- } catch (e) {
981
- console.log(' [warn] Could not parse ' + mcpLocalPath + ': ' + e.message);
982
- }
983
- } else {
984
- notFound.push('Claude Code (project): .mcp.json not found');
985
- }
986
-
987
- // 2. Remove from Claude Code global config (~/.claude/mcp.json)
988
- const mcpGlobalPath = path.join(home, '.claude', 'mcp.json');
989
- if (fs.existsSync(mcpGlobalPath)) {
990
- try {
991
- const mcpConfig = JSON.parse(fs.readFileSync(mcpGlobalPath, 'utf8'));
992
- if (mcpConfig.mcpServers && mcpConfig.mcpServers['agent-bridge']) {
993
- delete mcpConfig.mcpServers['agent-bridge'];
994
- fs.writeFileSync(mcpGlobalPath, JSON.stringify(mcpConfig, null, 2) + '\n');
995
- removed.push('Claude Code (global): ' + mcpGlobalPath);
996
- } else {
997
- notFound.push('Claude Code (global): no agent-bridge entry');
998
- }
999
- } catch (e) {
1000
- console.log(' [warn] Could not parse ' + mcpGlobalPath + ': ' + e.message);
1001
- }
1002
- } else {
1003
- notFound.push('Claude Code (global): ~/.claude/mcp.json not found');
1004
- }
1005
-
1006
- // 3. Remove from Gemini CLI config (~/.gemini/settings.json)
1007
- const geminiSettingsPath = path.join(home, '.gemini', 'settings.json');
1008
- if (fs.existsSync(geminiSettingsPath)) {
1009
- try {
1010
- const settings = JSON.parse(fs.readFileSync(geminiSettingsPath, 'utf8'));
1011
- if (settings.mcpServers && settings.mcpServers['agent-bridge']) {
1012
- delete settings.mcpServers['agent-bridge'];
1013
- fs.writeFileSync(geminiSettingsPath, JSON.stringify(settings, null, 2) + '\n');
1014
- removed.push('Gemini CLI: ' + geminiSettingsPath);
1015
- } else {
1016
- notFound.push('Gemini CLI: no agent-bridge entry');
1017
- }
1018
- } catch (e) {
1019
- console.log(' [warn] Could not parse ' + geminiSettingsPath + ': ' + e.message);
1020
- }
1021
- } else {
1022
- notFound.push('Gemini CLI: ~/.gemini/settings.json not found');
1023
- }
1024
-
1025
- // 4. Remove from Gemini CLI project config (.gemini/settings.json in cwd)
1026
- const geminiLocalPath = path.join(cwd, '.gemini', 'settings.json');
1027
- if (fs.existsSync(geminiLocalPath)) {
1028
- try {
1029
- const settings = JSON.parse(fs.readFileSync(geminiLocalPath, 'utf8'));
1030
- if (settings.mcpServers && settings.mcpServers['agent-bridge']) {
1031
- delete settings.mcpServers['agent-bridge'];
1032
- fs.writeFileSync(geminiLocalPath, JSON.stringify(settings, null, 2) + '\n');
1033
- removed.push('Gemini CLI (project): ' + geminiLocalPath);
1034
- } else {
1035
- notFound.push('Gemini CLI (project): no agent-bridge entry');
1036
- }
1037
- } catch (e) {
1038
- console.log(' [warn] Could not parse ' + geminiLocalPath + ': ' + e.message);
1039
- }
1040
- }
1041
-
1042
- // 5. Remove from Codex CLI config (~/.codex/config.toml)
1043
- const codexConfigPath = path.join(home, '.codex', 'config.toml');
1044
- if (fs.existsSync(codexConfigPath)) {
1045
- try {
1046
- let config = fs.readFileSync(codexConfigPath, 'utf8');
1047
- if (config.includes('[mcp_servers.agent-bridge]')) {
1048
- // Remove from [mcp_servers.agent-bridge] to the next [section] or end of file
1049
- // This covers both [mcp_servers.agent-bridge] and [mcp_servers.agent-bridge.env]
1050
- config = config.replace(/\n?\[mcp_servers\.agent-bridge[^\]]*\][^\[]*(?=\[|$)/g, '');
1051
- // Clean up multiple blank lines left behind
1052
- config = config.replace(/\n{3,}/g, '\n\n');
1053
- fs.writeFileSync(codexConfigPath, config);
1054
- removed.push('Codex CLI: ' + codexConfigPath);
1055
- } else {
1056
- notFound.push('Codex CLI: no agent-bridge section in config.toml');
1057
- }
1058
- } catch (e) {
1059
- console.log(' [warn] Could not process ' + codexConfigPath + ': ' + e.message);
1060
- }
1061
- } else {
1062
- notFound.push('Codex CLI: ~/.codex/config.toml not found');
1063
- }
1064
-
1065
- // 6. Remove from Codex CLI project config (.codex/config.toml in cwd)
1066
- const codexLocalPath = path.join(cwd, '.codex', 'config.toml');
1067
- if (fs.existsSync(codexLocalPath)) {
1068
- try {
1069
- let config = fs.readFileSync(codexLocalPath, 'utf8');
1070
- if (config.includes('[mcp_servers.agent-bridge]')) {
1071
- config = config.replace(/\n?\[mcp_servers\.agent-bridge[^\]]*\][^\[]*(?=\[|$)/g, '');
1072
- config = config.replace(/\n{3,}/g, '\n\n');
1073
- fs.writeFileSync(codexLocalPath, config);
1074
- removed.push('Codex CLI (project): ' + codexLocalPath);
1075
- }
1076
- } catch (e) {
1077
- console.log(' [warn] Could not process ' + codexLocalPath + ': ' + e.message);
1078
- }
1079
- }
1080
-
1081
- // Print summary
1082
- if (removed.length > 0) {
1083
- console.log(' Removed agent-bridge from:');
1084
- for (const r of removed) {
1085
- console.log(' [ok] ' + r);
1086
- }
1087
- } else {
1088
- console.log(' No agent-bridge configurations found to remove.');
1089
- }
1090
-
1091
- if (notFound.length > 0) {
1092
- console.log('');
1093
- console.log(' Skipped (not found):');
1094
- for (const n of notFound) {
1095
- console.log(' [-] ' + n);
1096
- }
1097
- }
1098
-
1099
- // 7. Check for data directory
1100
- const dataPath = path.join(cwd, '.agent-bridge');
1101
- if (fs.existsSync(dataPath)) {
1102
- console.log('');
1103
- console.log(' Found .agent-bridge/ directory with conversation data.');
1104
- console.log(' To remove it, manually delete: ' + dataPath);
1105
- }
1106
-
1107
- console.log('');
1108
- if (removed.length > 0) {
1109
- console.log(' Restart your CLI terminals for changes to take effect.');
1110
- }
1111
- console.log('');
1112
- }
1113
-
1114
- // --- Assistant init ---
1115
- function assistantInit() {
1116
- const cwd = process.cwd();
1117
- const dataDir = path.join(cwd, '.agent-bridge');
1118
- const assistantDir = path.join(dataDir, 'assistant');
1119
-
1120
- console.log('');
1121
- console.log(' \x1b[1m\x1b[35mLet Them Talk — Assistant Setup\x1b[0m');
1122
- console.log(' ════════════════════════════════════');
1123
- console.log('');
1124
-
1125
- // Check if Let Them Talk is already initialized
1126
- if (!fs.existsSync(dataDir)) {
1127
- console.log(' \x1b[31m✗ Let Them Talk not found.\x1b[0m');
1128
- console.log(' Run \x1b[36mnpx let-them-talk init\x1b[0m first.');
1129
- console.log('');
1130
- return;
1131
- }
1132
-
1133
- // Create assistant directory with template files
1134
- if (!fs.existsSync(assistantDir)) {
1135
- fs.mkdirSync(assistantDir, { recursive: true });
1136
- }
1137
-
1138
- const templateDir = path.join(__dirname, 'assistant');
1139
- const files = ['Soul.md', 'Identity.md', 'Memory.md', 'Skills.md', 'Tools.md', 'SafetyRules.md'];
1140
- let created = 0;
1141
- let skipped = 0;
1142
-
1143
- for (const file of files) {
1144
- const dest = path.join(assistantDir, file);
1145
- if (fs.existsSync(dest)) {
1146
- console.log(' \x1b[33m⊘\x1b[0m ' + file + ' (already exists, skipped)');
1147
- skipped++;
1148
- continue;
1149
- }
1150
- const src = path.join(templateDir, file);
1151
- if (fs.existsSync(src)) {
1152
- fs.copyFileSync(src, dest);
1153
- } else {
1154
- // Fallback: create minimal template
1155
- const content = `# ${file.replace('.md', '')}\n\nEdit this file to customize your assistant.\n`;
1156
- fs.writeFileSync(dest, content);
1157
- }
1158
- console.log(' \x1b[32m✓\x1b[0m ' + file);
1159
- created++;
1160
- }
1161
-
1162
- console.log('');
1163
- if (created > 0) {
1164
- console.log(` Created ${created} file${created > 1 ? 's' : ''} in .agent-bridge/assistant/`);
1165
- }
1166
- if (skipped > 0) {
1167
- console.log(` Skipped ${skipped} existing file${skipped > 1 ? 's' : ''}`);
1168
- }
1169
-
1170
- console.log('');
1171
- console.log(' \x1b[1mNext steps:\x1b[0m');
1172
- console.log('');
1173
- console.log(' 1. Edit the personality files in \x1b[36m.agent-bridge/assistant/\x1b[0m');
1174
- console.log(' - Soul.md → personality and tone');
1175
- console.log(' - Identity.md → name and role');
1176
- console.log(' - Skills.md → what it can do');
1177
- console.log(' - Tools.md → allowed/blocked tools');
1178
- console.log(' - SafetyRules.md → safety guardrails');
1179
- console.log('');
1180
- console.log(' 2. Start the dashboard:');
1181
- console.log(' \x1b[36mnode .agent-bridge/launch.js\x1b[0m');
1182
- console.log('');
1183
- console.log(' 3. Open the Assistant tab in the dashboard:');
1184
- console.log(' \x1b[36mhttp://<your-ip>:3000\x1b[0m → click \x1b[35mAssistant\x1b[0m tab');
1185
- console.log('');
1186
- console.log(' 4. Open a terminal with Let Them Talk and register as "Assistant"');
1187
- console.log(' The agent should use the \x1b[36massistant()\x1b[0m tool to listen.');
1188
- console.log('');
1189
- }
1190
-
1191
- function runCli() {
1192
- const command = process.argv[2];
1193
-
1194
- switch (command) {
1195
- case 'init':
1196
- init();
1197
- break;
1198
- case 'assistant':
1199
- assistantInit();
1200
- break;
1201
- case 'templates':
1202
- listTemplates();
1203
- break;
1204
- case 'dashboard':
1205
- dashboard();
1206
- break;
1207
- case 'reset':
1208
- reset();
1209
- break;
1210
- case 'doctor':
1211
- cliDoctor();
1212
- break;
1213
- case 'migrate':
1214
- case 'migrate-legacy':
1215
- cliMigrate();
1216
- break;
1217
- case 'msg':
1218
- case 'message':
1219
- case 'send':
1220
- cliMsg();
1221
- break;
1222
- case 'status':
1223
- cliStatus();
1224
- break;
1225
- case 'uninstall':
1226
- case 'remove':
1227
- uninstall();
1228
- break;
1229
- case 'plugin':
1230
- case 'plugins':
1231
- console.log(' Plugins have been removed in v3.4.3. CLI terminals have their own extension systems.');
1232
- break;
1233
- case 'help':
1234
- case '--help':
1235
- case '-h':
1236
- case undefined:
1237
- printUsage();
1238
- break;
1239
- default:
1240
- console.error(` Unknown command: ${command}`);
1241
- printUsage();
1242
- process.exit(1);
1243
- }
1244
- }
1245
-
1246
- function shouldAutoRunCli() {
1247
- if (require.main === module) return true;
1248
- const argvPath = process.argv[1];
1249
- if (!argvPath) return false;
1250
- const normalizedArgvPath = path.resolve(argvPath).replace(/\\/g, '/');
1251
- const normalizedFilePath = path.resolve(__filename).replace(/\\/g, '/');
1252
- return process.platform === 'win32'
1253
- ? normalizedArgvPath.toLowerCase() === normalizedFilePath.toLowerCase()
1254
- : normalizedArgvPath === normalizedFilePath;
1255
- }
1256
-
1257
- module.exports = {
1258
- init,
1259
- runCli,
1260
- };
1261
-
1262
- if (shouldAutoRunCli()) {
1263
- runCli();
1264
- }
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
+ const { resolveDataDir: resolveSharedDataDir } = require('./data-dir');
8
+ const { createCanonicalState } = require('./state/canonical');
9
+
10
+ function printUsage() {
11
+ console.log(`
12
+ Let Them Talk — Agent Bridge v5.5.2
13
+ MCP message broker for inter-agent communication
14
+ Supports: Claude Code, Gemini CLI, Codex CLI, Ollama
15
+
16
+ Setup (one-time):
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 <name> Initialize and print an agent template
24
+
25
+ After init, use the local launcher (no re-download):
26
+ node .agent-bridge/launch.js Dashboard (http://localhost:3000)
27
+ node .agent-bridge/launch.js --lan Dashboard on LAN (phone/tablet)
28
+ node .agent-bridge/launch.js status Show active agents and message count
29
+ node .agent-bridge/launch.js msg <agent> <text> Send a message to an agent
30
+ node .agent-bridge/launch.js reset Clear all conversation data
31
+ node .agent-bridge/launch.js migrate Backfill canonical event stream from legacy projections
32
+ node .agent-bridge/launch.js migrate --dry-run Preview what migrate would do
33
+
34
+ Or via npx (re-downloads each time):
35
+ npx let-them-talk dashboard
36
+ npx let-them-talk status
37
+ npx let-them-talk templates List available agent templates
38
+ npx let-them-talk uninstall Remove agent-bridge from all CLI configs
39
+ npx let-them-talk help Show this help message
40
+
41
+ v5.0 — True Autonomy Engine (61 tools):
42
+ New tools: get_work, verify_and_advance, start_plan, retry_with_improvement
43
+ Proactive work loop: get_work → do work → verify_and_advance → get_work
44
+ Parallel workflow steps with dependency graphs (depends_on)
45
+ Auto-retry with skill accumulation (3 attempts then team escalation)
46
+ Watchdog engine: idle nudge, stuck detection, auto-reassign
47
+ 100ms handoff cooldowns in autonomous mode
48
+ Plan dashboard: live progress, pause/stop/skip/reassign controls
49
+ `);
50
+ }
51
+
52
+ // Detect which CLIs are installed
53
+ function detectCLIs() {
54
+ const detected = [];
55
+ const home = os.homedir();
56
+
57
+ // Claude Code: ~/.claude/ directory exists
58
+ if (fs.existsSync(path.join(home, '.claude'))) {
59
+ detected.push('claude');
60
+ }
61
+
62
+ // Gemini CLI: ~/.gemini/ or GEMINI_API_KEY set
63
+ if (fs.existsSync(path.join(home, '.gemini')) || process.env.GEMINI_API_KEY) {
64
+ detected.push('gemini');
65
+ }
66
+
67
+ // Codex CLI: ~/.codex/ directory exists
68
+ if (fs.existsSync(path.join(home, '.codex'))) {
69
+ detected.push('codex');
70
+ }
71
+
72
+ return detected;
73
+ }
74
+
75
+ // Detect Ollama installation
76
+ function detectOllama() {
77
+ try {
78
+ const version = execSync('ollama --version', { encoding: 'utf8', timeout: 5000 }).trim();
79
+ return { installed: true, version };
80
+ } catch {
81
+ return { installed: false };
82
+ }
83
+ }
84
+
85
+ // The data directory where all agents read/write — must be the same for server + dashboard
86
+ function dataDir(cwd) {
87
+ return resolveSharedDataDir({ cwd });
88
+ }
89
+
90
+ // Configure for Claude Code (.mcp.json in project root)
91
+ function setupClaude(serverPath, cwd, log = console.log) {
92
+ const mcpConfigPath = path.join(cwd, '.mcp.json');
93
+ let mcpConfig = { mcpServers: {} };
94
+ if (fs.existsSync(mcpConfigPath)) {
95
+ try {
96
+ mcpConfig = JSON.parse(fs.readFileSync(mcpConfigPath, 'utf8'));
97
+ if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
98
+ } catch {
99
+ // Backup corrupted file before overwriting
100
+ const backup = mcpConfigPath + '.backup';
101
+ fs.copyFileSync(mcpConfigPath, backup);
102
+ log(' [warn] Existing .mcp.json was invalid — backed up to .mcp.json.backup');
103
+ }
104
+ }
105
+
106
+ mcpConfig.mcpServers['agent-bridge'] = {
107
+ command: 'node',
108
+ args: [serverPath],
109
+ timeout: 300,
110
+ };
111
+
112
+ fs.writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2) + '\n');
113
+ log(' [ok] Claude Code: .mcp.json updated');
114
+ }
115
+
116
+ // Configure for Gemini CLI (.gemini/settings.json or GEMINI.md with MCP config)
117
+ function setupGemini(serverPath, cwd, log = console.log) {
118
+ // Gemini CLI uses .gemini/settings.json for MCP configuration
119
+ const geminiDir = path.join(cwd, '.gemini');
120
+ const settingsPath = path.join(geminiDir, 'settings.json');
121
+
122
+ if (!fs.existsSync(geminiDir)) {
123
+ fs.mkdirSync(geminiDir, { recursive: true });
124
+ }
125
+
126
+ let settings = { mcpServers: {} };
127
+ if (fs.existsSync(settingsPath)) {
128
+ try {
129
+ settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
130
+ if (!settings.mcpServers) settings.mcpServers = {};
131
+ } catch {
132
+ const backup = settingsPath + '.backup';
133
+ fs.copyFileSync(settingsPath, backup);
134
+ log(' [warn] Existing settings.json was invalid — backed up to settings.json.backup');
135
+ }
136
+ }
137
+
138
+ settings.mcpServers['agent-bridge'] = {
139
+ command: 'node',
140
+ args: [serverPath],
141
+ timeout: 300,
142
+ trust: true,
143
+ };
144
+
145
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
146
+ log(' [ok] Gemini CLI: .gemini/settings.json updated');
147
+ }
148
+
149
+ // Configure for Codex CLI (uses .codex/config.toml)
150
+ function setupCodex(serverPath, cwd, log = console.log) {
151
+ const codexDir = path.join(cwd, '.codex');
152
+ const configPath = path.join(codexDir, 'config.toml');
153
+
154
+ if (!fs.existsSync(codexDir)) {
155
+ fs.mkdirSync(codexDir, { recursive: true });
156
+ }
157
+
158
+ // Read existing config or start fresh
159
+ let config = '';
160
+ if (fs.existsSync(configPath)) {
161
+ config = fs.readFileSync(configPath, 'utf8');
162
+ }
163
+
164
+ // Backup existing config before modifying
165
+ if (fs.existsSync(configPath)) {
166
+ fs.copyFileSync(configPath, configPath + '.backup');
167
+ }
168
+
169
+ // Only add if not already present
170
+ if (!config.includes('[mcp_servers.agent-bridge]')) {
171
+ const tomlBlock = `
172
+ [mcp_servers.agent-bridge]
173
+ command = "node"
174
+ args = [${JSON.stringify(serverPath)}]
175
+ timeout = 300
176
+ `;
177
+ config += tomlBlock;
178
+ fs.writeFileSync(configPath, config);
179
+ }
180
+
181
+ log(' [ok] Codex CLI: .codex/config.toml updated');
182
+ }
183
+
184
+ // Setup Ollama agent bridge script
185
+ function setupOllama(serverPath, cwd, log = console.log) {
186
+ const dir = dataDir(cwd);
187
+ const scriptPath = path.join(dir, 'ollama-agent.js');
188
+
189
+ if (!fs.existsSync(dir)) {
190
+ fs.mkdirSync(dir, { recursive: true });
191
+ }
192
+
193
+ const script = `#!/usr/bin/env node
194
+ // ollama-agent.js - bridges Ollama to Let Them Talk
195
+ // Usage: node .agent-bridge/ollama-agent.js [agent-name] [model]
196
+ const fs = require('fs'), path = require('path'), http = require('http');
197
+ const DATA_DIR = path.join(__dirname);
198
+ const name = process.argv[2] || 'Ollama';
199
+ if (!/^[a-zA-Z0-9_-]{1,20}$/.test(name)) throw new Error('Invalid agent name');
200
+ const model = process.argv[3] || 'llama3';
201
+ const OLLAMA_URL = process.env.OLLAMA_URL || 'http://localhost:11434';
202
+
203
+ function readJson(f) { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch { return {}; } }
204
+ 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); }
205
+
206
+ // Register agent
207
+ function register() {
208
+ const agentsFile = path.join(DATA_DIR, 'agents.json');
209
+ const agents = readJson(agentsFile);
210
+ agents[name] = { pid: process.pid, timestamp: new Date().toISOString(), last_activity: new Date().toISOString(), provider: 'Ollama (' + model + ')' };
211
+ fs.writeFileSync(agentsFile, JSON.stringify(agents, null, 2));
212
+ console.log('[' + name + '] Registered (PID ' + process.pid + ', model: ' + model + ')');
213
+ }
214
+
215
+ // Update heartbeat
216
+ function heartbeat() {
217
+ const agentsFile = path.join(DATA_DIR, 'agents.json');
218
+ const agents = readJson(agentsFile);
219
+ if (agents[name]) {
220
+ agents[name].last_activity = new Date().toISOString();
221
+ agents[name].pid = process.pid;
222
+ fs.writeFileSync(agentsFile, JSON.stringify(agents, null, 2));
223
+ }
224
+ }
225
+
226
+ // Call Ollama API
227
+ function callOllama(prompt) {
228
+ return new Promise(function(resolve, reject) {
229
+ const url = new URL(OLLAMA_URL + '/api/chat');
230
+ const body = JSON.stringify({ model: model, messages: [{ role: 'user', content: prompt }], stream: false });
231
+ const req = http.request(url, { method: 'POST', headers: { 'Content-Type': 'application/json' } }, function(res) {
232
+ let data = '';
233
+ res.on('data', function(c) { data += c; });
234
+ res.on('end', function() {
235
+ try { const j = JSON.parse(data); resolve(j.message ? j.message.content : data); }
236
+ catch { resolve(data); }
237
+ });
238
+ });
239
+ req.on('error', reject);
240
+ req.write(body);
241
+ req.end();
242
+ });
243
+ }
244
+
245
+ // Send a message
246
+ function sendMessage(to, content) {
247
+ const msgId = 'm' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
248
+ const msg = { id: msgId, from: name, to: to, content: content, timestamp: new Date().toISOString() };
249
+ fs.appendFileSync(path.join(DATA_DIR, 'messages.jsonl'), JSON.stringify(msg) + '\\n');
250
+ fs.appendFileSync(path.join(DATA_DIR, 'history.jsonl'), JSON.stringify(msg) + '\\n');
251
+ console.log('[' + name + '] -> ' + to + ': ' + content.substring(0, 80) + (content.length > 80 ? '...' : ''));
252
+ }
253
+
254
+ // Listen for messages
255
+ let lastOffset = 0;
256
+ function checkMessages() {
257
+ const consumedFile = path.join(DATA_DIR, 'consumed-' + name + '.json');
258
+ const consumed = readJson(consumedFile);
259
+ lastOffset = consumed.offset || 0;
260
+
261
+ const messages = readJsonl(path.join(DATA_DIR, 'messages.jsonl'));
262
+ const newMsgs = messages.slice(lastOffset).filter(function(m) {
263
+ return m.to === name || (m.to === 'all' && m.from !== name);
264
+ });
265
+
266
+ if (newMsgs.length > 0) {
267
+ consumed.offset = messages.length;
268
+ fs.writeFileSync(consumedFile, JSON.stringify(consumed));
269
+ }
270
+
271
+ return newMsgs;
272
+ }
273
+
274
+ async function processMessages() {
275
+ const msgs = checkMessages();
276
+ for (const m of msgs) {
277
+ console.log('[' + name + '] <- ' + m.from + ': ' + m.content.substring(0, 80));
278
+ try {
279
+ const response = await callOllama(m.content);
280
+ sendMessage(m.from, response);
281
+ } catch (e) {
282
+ sendMessage(m.from, 'Error calling Ollama: ' + e.message);
283
+ }
284
+ }
285
+ }
286
+
287
+ // Main loop
288
+ register();
289
+ const hb = setInterval(heartbeat, 10000);
290
+ hb.unref();
291
+ console.log('[' + name + '] Listening for messages... (Ctrl+C to stop)');
292
+ setInterval(processMessages, 2000);
293
+
294
+ // Cleanup on exit
295
+ process.on('SIGINT', function() { console.log('\\n[' + name + '] Shutting down.'); process.exit(0); });
296
+ `;
297
+
298
+ const tmpPath = scriptPath + '.tmp.' + process.pid;
299
+ fs.writeFileSync(tmpPath, script);
300
+ fs.renameSync(tmpPath, scriptPath);
301
+ log(' [ok] Ollama agent script created: .agent-bridge/ollama-agent.js');
302
+ log('');
303
+ log(' Launch an Ollama agent with:');
304
+ log(' node .agent-bridge/ollama-agent.js <name> <model>');
305
+ log('');
306
+ log(' Examples:');
307
+ log(' node .agent-bridge/ollama-agent.js Ollama llama3');
308
+ log(' node .agent-bridge/ollama-agent.js Coder codellama');
309
+ log(' node .agent-bridge/ollama-agent.js Writer mistral');
310
+ }
311
+
312
+ // Merge-safe AGENTS.md / CLAUDE.md updater. Writes a marker block with the
313
+ // background-worker rules so any agent cd'd into this folder gets them every
314
+ // session. Re-running replaces only the marked block, leaving user content alone.
315
+ function setupAgentsMarkdown(cwd, log = console.log) {
316
+ const BEGIN = '<!-- BEGIN let-them-talk (auto-managed — do not edit between markers) -->';
317
+ const END = '<!-- END let-them-talk -->';
318
+ const block = [
319
+ BEGIN,
320
+ '',
321
+ '## Let Them Talk — Background-Worker Mode',
322
+ '',
323
+ 'This project uses the `agent-bridge` MCP server for multi-agent coordination.',
324
+ 'When you run in this folder, you are a **background worker on a team**, not an',
325
+ 'interactive chat assistant. Follow these rules strictly:',
326
+ '',
327
+ '1. **Your CLI terminal output is invisible** to the owner and to every other',
328
+ ' agent. If you want anyone to see something, it MUST go through',
329
+ ' `send_message(to="...", content="...")` or `broadcast(content="...")`.',
330
+ '',
331
+ '2. **No narration in terminal.** Do not "reply" to messages in your terminal',
332
+ ' window. Do not summarize your progress in terminal. Do not print status',
333
+ ' updates in terminal. Those are invisible. Talk like a human on a team chat',
334
+ ' — announce starts, finishes, blockers, and questions via `send_message`.',
335
+ '',
336
+ '3. **Stay in the listen loop.** After every action, call `listen_group()` (or',
337
+ ' `listen()` in direct mode). When it returns an empty batch, that is NORMAL',
338
+ ' — call it again immediately. If it returns a tool error like',
339
+ ' `"timed out awaiting tools/call"`, that is a Codex-level transport hiccup',
340
+ ' — immediately call it again. Never stop looping, never treat an empty',
341
+ ' return or tool error as "done".',
342
+ '',
343
+ '4. **Reply to Dashboard/Owner via `send_message(to="Dashboard")`.** The owner',
344
+ ' reads replies in the dashboard Messages tab, not your terminal.',
345
+ '',
346
+ '5. **Do not answer on another agent\'s behalf.** If a message targets a',
347
+ ' specific agent (`msg.to`), only that agent should reply.',
348
+ '',
349
+ '6. **Self-reliance.** When the Owner gives you a goal, break it down',
350
+ ' yourself and work until done. NEVER stop to ask "should I do X?" or',
351
+ ' "do you want me to Y?" for decisions the team can make. Decide,',
352
+ ' `log_decision()` to record the choice, continue.',
353
+ '',
354
+ '7. **Team-first escalation.** Before DMing Owner with a question, try',
355
+ ' these in order: (a) `kb_read()` — did the team already decide this?',
356
+ ' (b) DM a teammate with the relevant skill (use `list_agents()`).',
357
+ ' (c) `call_vote()` if the team genuinely disagrees. (d) `log_decision()`',
358
+ ' to lock in your choice and move forward. Only escalate to Owner when',
359
+ ' the overall goal is complete OR a true blocker only the Owner can',
360
+ ' resolve (credentials, priorities, business rules).',
361
+ '',
362
+ '8. **Done-when-done.** "Done" means the Owner\'s original GOAL is',
363
+ ' achieved, not the current step. After `verify_and_advance()`, call',
364
+ ' `get_work()` again. If nothing is queued and the goal is not yet',
365
+ ' done, synthesize new tasks with `create_task()` and keep going.',
366
+ '',
367
+ '9. **Write like you are publishing.** The Messages tab renders',
368
+ ' GFM markdown with tables, fenced code + syntax highlighting,',
369
+ ' Obsidian-style callouts, Mermaid diagrams, KaTeX math, and',
370
+ ' clickable images. Use tables for structured data, callouts for',
371
+ ' status (`> [!SUCCESS]`, `> [!WARNING]`, `> [!DANGER]`,',
372
+ ' `> [!SUMMARY]-` for collapsible long reports), ```mermaid for',
373
+ ' architecture/flow diagrams, and fenced code with language tags.',
374
+ ' A terse structured report beats a wall of text.',
375
+ '',
376
+ '10. The loop only ends when the goal is achieved with evidence OR the',
377
+ ' Owner sends a message telling you to stop.',
378
+ '',
379
+ END,
380
+ ].join('\n');
381
+
382
+ const targets = [
383
+ { file: 'AGENTS.md', label: 'Codex / oh-my-codex' },
384
+ { file: 'CLAUDE.md', label: 'Claude Code' },
385
+ ];
386
+
387
+ for (const { file, label } of targets) {
388
+ const fp = path.join(cwd, file);
389
+ let existing = '';
390
+ let existed = false;
391
+ if (fs.existsSync(fp)) {
392
+ existing = fs.readFileSync(fp, 'utf8');
393
+ existed = true;
394
+ }
395
+
396
+ const markerRegex = new RegExp(
397
+ BEGIN.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +
398
+ '[\\s\\S]*?' +
399
+ END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'),
400
+ 'g'
401
+ );
402
+
403
+ let next;
404
+ if (markerRegex.test(existing)) {
405
+ // Replace only the managed block
406
+ next = existing.replace(markerRegex, block);
407
+ log(' [ok] ' + file + ': refreshed Let Them Talk block (' + label + ')');
408
+ } else if (existed) {
409
+ // Append below user content
410
+ const separator = existing.endsWith('\n') ? '\n' : '\n\n';
411
+ next = existing + separator + block + '\n';
412
+ log(' [ok] ' + file + ': appended Let Them Talk block (' + label + ')');
413
+ } else {
414
+ // New file minimal content so only our block is present
415
+ next = '# ' + path.basename(cwd) + ' — Agent Instructions\n\n' + block + '\n';
416
+ log(' [ok] ' + file + ': created with Let Them Talk block (' + label + ')');
417
+ }
418
+ fs.writeFileSync(fp, next);
419
+ }
420
+ }
421
+
422
+ function init(options) {
423
+ const opts = options || {};
424
+ const cwd = opts.cwd || process.cwd();
425
+ const serverPath = path.join(__dirname, 'server.js').replace(/\\/g, '/');
426
+ const gitignorePath = path.join(cwd, '.gitignore');
427
+ const argv = Array.isArray(opts.argv) ? opts.argv : process.argv;
428
+ const flag = opts.flag !== undefined ? opts.flag : argv[3];
429
+ const log = typeof opts.log === 'function' ? opts.log : console.log;
430
+
431
+ log('');
432
+ log(' Let Them Talk — Initializing Agent Bridge');
433
+ log(' ==========================================');
434
+ log('');
435
+
436
+ let targets = [];
437
+
438
+ if (flag === '--claude') {
439
+ targets = ['claude'];
440
+ } else if (flag === '--gemini') {
441
+ targets = ['gemini'];
442
+ } else if (flag === '--codex') {
443
+ targets = ['codex'];
444
+ } else if (flag === '--all') {
445
+ targets = ['claude', 'gemini', 'codex'];
446
+ } else if (flag === '--ollama') {
447
+ const ollama = detectOllama();
448
+ if (!ollama.installed) {
449
+ log(' Ollama not found. Install it from: https://ollama.com/download');
450
+ log(' After installing, run: ollama pull llama3');
451
+ log('');
452
+ } else {
453
+ log(' Ollama detected: ' + ollama.version);
454
+ setupOllama(serverPath, cwd, log);
455
+ }
456
+ targets = detectCLIs();
457
+ if (targets.length === 0) targets = ['claude'];
458
+ } else {
459
+ // Auto-detect
460
+ targets = detectCLIs();
461
+ if (targets.length === 0) {
462
+ // Default to claude if nothing detected
463
+ targets = ['claude'];
464
+ log(' No CLI detected, defaulting to Claude Code config.');
465
+ } else {
466
+ log(` Detected CLI(s): ${targets.join(', ')}`);
467
+ }
468
+ }
469
+
470
+ log('');
471
+
472
+ for (const target of targets) {
473
+ switch (target) {
474
+ case 'claude': setupClaude(serverPath, cwd, log); break;
475
+ case 'gemini': setupGemini(serverPath, cwd, log); break;
476
+ case 'codex': setupCodex(serverPath, cwd, log); break;
477
+ }
478
+ }
479
+
480
+ // Persistent system-level directive for any agent that starts in this folder.
481
+ // Codex (via oh-my-codex's developer_instructions) and Claude Code both read
482
+ // AGENTS.md / CLAUDE.md automatically on startup. A marker block lets us merge
483
+ // in/out cleanly without clobbering whatever else the user has written.
484
+ setupAgentsMarkdown(cwd, log);
485
+
486
+ // Add .agent-bridge/ and MCP config files to .gitignore
487
+ const gitignoreEntries = ['.agent-bridge/', '.mcp.json', '.codex/', '.gemini/'];
488
+ if (fs.existsSync(gitignorePath)) {
489
+ let content = fs.readFileSync(gitignorePath, 'utf8');
490
+ const missing = gitignoreEntries.filter(e => !content.includes(e));
491
+ if (missing.length) {
492
+ content += '\n# Agent Bridge (auto-added by let-them-talk init)\n' + missing.join('\n') + '\n';
493
+ fs.writeFileSync(gitignorePath, content);
494
+ log(' [ok] Added to .gitignore: ' + missing.join(', '));
495
+ } else {
496
+ log(' [ok] .gitignore already configured');
497
+ }
498
+ } else {
499
+ fs.writeFileSync(gitignorePath, '# Agent Bridge (auto-added by let-them-talk init)\n' + gitignoreEntries.join('\n') + '\n');
500
+ log(' [ok] .gitignore created');
501
+ }
502
+
503
+ // Save local launcher scripts so users never need to re-download
504
+ const bridgeDir = dataDir(cwd);
505
+ if (!fs.existsSync(bridgeDir)) {
506
+ fs.mkdirSync(bridgeDir, { recursive: true });
507
+ }
508
+
509
+ const cliPath = path.join(__dirname, 'cli.js').replace(/\\/g, '/');
510
+
511
+ // Dashboard launcher - run with: node .agent-bridge/launch.js
512
+ const launcherScript = `#!/usr/bin/env node
513
+ // Auto-generated by let-them-talk init - launch dashboard without re-downloading
514
+ // Usage: node .agent-bridge/launch.js [--lan|dashboard|status|reset|msg]
515
+
516
+ const firstArg = process.argv[2] || 'dashboard';
517
+ const cliPath = ${JSON.stringify(cliPath)};
518
+
519
+ try {
520
+ require('fs').accessSync(cliPath);
521
+ } catch {
522
+ console.error(' Let Them Talk CLI not found at: ' + cliPath);
523
+ console.error(' The npx cache may have been cleaned. Fix with either:');
524
+ console.error(' npx let-them-talk init (re-creates launcher)');
525
+ console.error(' npm i -g let-them-talk (permanent global install)');
526
+ process.exit(1);
527
+ }
528
+
529
+ // Forward to cli.js with the command
530
+ const forwardedArgs = firstArg === '--lan'
531
+ ? ['dashboard', '--lan', ...process.argv.slice(3)]
532
+ : [firstArg, ...process.argv.slice(3)];
533
+ process.argv = [process.argv[0], cliPath, ...forwardedArgs];
534
+ require(cliPath);
535
+ `;
536
+
537
+ fs.writeFileSync(path.join(bridgeDir, 'launch.js'), launcherScript);
538
+ const launcherPath = path.join(bridgeDir, 'launch.js');
539
+ log(' [ok] Local launcher saved to .agent-bridge/launch.js');
540
+
541
+ log('');
542
+ log(' Agent Bridge is ready! Restart your CLI to pick up the MCP tools.');
543
+ log('');
544
+
545
+ // Show template if --template was provided
546
+ var templateFlag = null;
547
+ for (var i = 3; i < argv.length; i++) {
548
+ if (argv[i] === '--template' && argv[i + 1]) {
549
+ templateFlag = argv[i + 1];
550
+ break;
551
+ }
552
+ }
553
+
554
+ if (templateFlag) {
555
+ showTemplate(templateFlag);
556
+ } else {
557
+ log(' Open two terminals and start a conversation between agents.');
558
+ log(' Tip: Use "npx let-them-talk init --template pair" for ready-made prompts.');
559
+ log('');
560
+ log(' \x1b[1m Monitor:\x1b[0m');
561
+ log(' node .agent-bridge/launch.js (dashboard)');
562
+ log(' node .agent-bridge/launch.js status (agent status)');
563
+ log(' node .agent-bridge/launch.js reset (clear data)');
564
+ log('');
565
+ log(' Or use npx (re-downloads each time):');
566
+ log(' npx let-them-talk dashboard');
567
+ log('');
568
+ }
569
+
570
+ return {
571
+ cwd,
572
+ flag: flag || null,
573
+ targets,
574
+ bridgeDir,
575
+ launcherPath,
576
+ };
577
+ }
578
+
579
+ function reset() {
580
+ const targetDir = resolveDataDirCli();
581
+
582
+ if (!fs.existsSync(targetDir)) {
583
+ console.log(' No .agent-bridge/ directory found. Nothing to reset.');
584
+ return;
585
+ }
586
+
587
+ // Safety: count messages to show user what they're about to delete
588
+ const historyFile = path.join(targetDir, 'history.jsonl');
589
+ let msgCount = 0;
590
+ if (fs.existsSync(historyFile)) {
591
+ msgCount = fs.readFileSync(historyFile, 'utf8').split(/\r?\n/).filter(l => l.trim()).length;
592
+ }
593
+
594
+ // Require --force flag, otherwise warn and exit
595
+ if (!process.argv.includes('--force')) {
596
+ console.log('');
597
+ console.log(' ⚠ This will permanently delete all conversation data in:');
598
+ console.log(' ' + targetDir);
599
+ if (msgCount > 0) console.log(' (' + msgCount + ' messages in history)');
600
+ console.log('');
601
+ console.log(' To confirm, run: npx let-them-talk reset --force');
602
+ console.log('');
603
+ return;
604
+ }
605
+
606
+ // Auto-archive before deleting
607
+ const archiveDir = path.join(targetDir, '..', '.agent-bridge-archive');
608
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
609
+ const archivePath = path.join(archiveDir, timestamp);
610
+ try {
611
+ const archiveResult = getCanonicalStateCli().archiveFiles({
612
+ fileNames: ['history.jsonl', 'messages.jsonl', 'agents.json', 'decisions.json', 'tasks.json'],
613
+ destinationDir: archivePath,
614
+ });
615
+ if (archiveResult.archived > 0) {
616
+ console.log(' [ok] Archived ' + archiveResult.archived + ' files to .agent-bridge-archive/' + timestamp + '/');
617
+ }
618
+ } catch (e) {
619
+ console.log(' [warn] Could not archive: ' + e.message + ' — proceeding with reset anyway.');
620
+ }
621
+
622
+ getCanonicalStateCli().resetRuntime({
623
+ fixedFileNames: [
624
+ 'messages.jsonl',
625
+ 'history.jsonl',
626
+ 'agents.json',
627
+ 'acks.json',
628
+ 'tasks.json',
629
+ 'profiles.json',
630
+ 'workflows.json',
631
+ 'branches.json',
632
+ 'read_receipts.json',
633
+ 'permissions.json',
634
+ 'config.json',
635
+ 'decisions.json',
636
+ ],
637
+ });
638
+ console.log(' Cleared all data from ' + targetDir);
639
+ }
640
+
641
+ function getTemplates() {
642
+ var all = [];
643
+
644
+ // 1. Built-in templates (shipped with the package)
645
+ var builtinDir = path.join(__dirname, 'templates');
646
+ if (fs.existsSync(builtinDir)) {
647
+ fs.readdirSync(builtinDir).filter(f => f.endsWith('.json')).forEach(f => {
648
+ try { var t = JSON.parse(fs.readFileSync(path.join(builtinDir, f), 'utf8')); t._source = 'built-in'; all.push(t); }
649
+ catch {}
650
+ });
651
+ }
652
+
653
+ // 2. Project-local templates: .agent-bridge/templates/ in current working directory
654
+ var localDir = path.join(resolveDataDirCli(), 'templates');
655
+ if (fs.existsSync(localDir)) {
656
+ fs.readdirSync(localDir).filter(f => f.endsWith('.json')).forEach(f => {
657
+ try {
658
+ var t = JSON.parse(fs.readFileSync(path.join(localDir, f), 'utf8'));
659
+ t._source = 'local';
660
+ // Don't add duplicates (local overrides built-in with same name)
661
+ var existing = all.findIndex(e => e.name === t.name);
662
+ if (existing >= 0) all[existing] = t;
663
+ else all.push(t);
664
+ } catch {}
665
+ });
666
+ }
667
+
668
+ // 3. User-global templates: ~/.let-them-talk/templates/
669
+ var homeDir = process.env.HOME || process.env.USERPROFILE || '';
670
+ var globalDir = path.join(homeDir, '.let-them-talk', 'templates');
671
+ if (fs.existsSync(globalDir)) {
672
+ fs.readdirSync(globalDir).filter(f => f.endsWith('.json')).forEach(f => {
673
+ try {
674
+ var t = JSON.parse(fs.readFileSync(path.join(globalDir, f), 'utf8'));
675
+ t._source = 'global';
676
+ var existing = all.findIndex(e => e.name === t.name);
677
+ if (existing >= 0) all[existing] = t;
678
+ else all.push(t);
679
+ } catch {}
680
+ });
681
+ }
682
+
683
+ return all;
684
+ }
685
+
686
+ function listTemplates() {
687
+ const templates = getTemplates();
688
+ console.log('');
689
+ console.log(' Available Agent Templates');
690
+ console.log(' ========================');
691
+ console.log('');
692
+ for (const t of templates) {
693
+ const agentNames = t.agents.map(a => a.name).join(', ');
694
+ const sourceTag = t._source === 'local' ? ' [local]' : t._source === 'global' ? ' [global]' : '';
695
+ console.log(' ' + t.name.padEnd(12) + ' ' + t.description + sourceTag);
696
+ console.log(' ' + ''.padEnd(12) + ' Agents: ' + agentNames);
697
+ console.log('');
698
+ }
699
+ console.log(' Usage: npx let-them-talk init --template <name>');
700
+ console.log(' Note: this command lists agent templates only. Conversation workflow templates ship separately in agent-bridge/conversation-templates/*.json.');
701
+ console.log('');
702
+ console.log(' Custom templates:');
703
+ console.log(' Project-local: .agent-bridge/templates/*.json');
704
+ console.log(' User-global: ~/.let-them-talk/templates/*.json');
705
+ console.log('');
706
+ }
707
+
708
+ function showTemplate(templateName) {
709
+ const templates = getTemplates();
710
+ const template = templates.find(t => t.name === templateName);
711
+ if (!template) {
712
+ console.error(' Unknown template: ' + templateName);
713
+ console.error(' Available: ' + templates.map(t => t.name).join(', '));
714
+ process.exit(1);
715
+ }
716
+
717
+ console.log('');
718
+ console.log(' Template: ' + template.name);
719
+ console.log(' ' + template.description);
720
+ console.log('');
721
+ console.log(' Copy these agent prompts into each terminal:');
722
+ console.log(' ======================================');
723
+ console.log(' These prompts assume current onboarding: register, get_briefing(), then get_guide() when you need the current rules.');
724
+
725
+ for (var i = 0; i < template.agents.length; i++) {
726
+ var a = template.agents[i];
727
+ console.log('');
728
+ console.log(' --- Terminal ' + (i + 1) + ': ' + a.name + ' (' + a.role + ') ---');
729
+ console.log('');
730
+ console.log(' ' + a.prompt.replace(/\n/g, '\n '));
731
+ console.log('');
732
+ }
733
+ }
734
+
735
+ function dashboard() {
736
+ if (process.argv.includes('--lan')) {
737
+ process.env.AGENT_BRIDGE_LAN = 'true';
738
+ }
739
+ require('./dashboard.js');
740
+ }
741
+
742
+ function resolveDataDirCli() {
743
+ return resolveSharedDataDir();
744
+ }
745
+
746
+ function getCanonicalStateCli() {
747
+ return createCanonicalState({ dataDir: resolveDataDirCli(), processPid: process.pid });
748
+ }
749
+
750
+ function readJsonl(filePath) {
751
+ if (!fs.existsSync(filePath)) return [];
752
+ return fs.readFileSync(filePath, 'utf8')
753
+ .split(/\r?\n/)
754
+ .filter(l => l.trim())
755
+ .map(l => { try { return JSON.parse(l); } catch { return null; } })
756
+ .filter(Boolean);
757
+ }
758
+
759
+ function readJson(filePath) {
760
+ if (!fs.existsSync(filePath)) return {};
761
+ try { return JSON.parse(fs.readFileSync(filePath, 'utf8')); } catch { return {}; }
762
+ }
763
+
764
+ function isPidAlive(pid) {
765
+ if (!pid) return false;
766
+ try { process.kill(pid, 0); return true; } catch { return false; }
767
+ }
768
+
769
+ function cliMsg() {
770
+ const recipient = process.argv[3];
771
+ const textParts = process.argv.slice(4);
772
+ if (!recipient || !textParts.length) {
773
+ console.error(' Usage: npx let-them-talk msg <agent> <text>');
774
+ process.exit(1);
775
+ }
776
+ if (!/^[a-zA-Z0-9_-]{1,20}$/.test(recipient)) {
777
+ console.error(' Agent name must be 1-20 alphanumeric characters (with _ or -).');
778
+ process.exit(1);
779
+ }
780
+ const text = textParts.join(' ');
781
+ const dir = resolveDataDirCli();
782
+ if (!fs.existsSync(dir)) {
783
+ console.error(' No .agent-bridge/ directory found. Run "npx let-them-talk init" first.');
784
+ process.exit(1);
785
+ }
786
+
787
+ const msgId = 'm' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
788
+ const msg = {
789
+ id: msgId,
790
+ from: 'CLI',
791
+ to: recipient,
792
+ content: text,
793
+ timestamp: new Date().toISOString(),
794
+ };
795
+
796
+ getCanonicalStateCli().appendMessage(msg);
797
+
798
+ console.log(' Message sent to ' + recipient + ': ' + text);
799
+ }
800
+
801
+ function cliStatus() {
802
+ const dir = resolveDataDirCli();
803
+ if (!fs.existsSync(dir)) {
804
+ console.error(' No .agent-bridge/ directory found. Run "npx let-them-talk init" first.');
805
+ process.exit(1);
806
+ }
807
+
808
+ const agents = readJson(path.join(dir, 'agents.json'));
809
+ const history = readJsonl(path.join(dir, 'history.jsonl'));
810
+ const profiles = readJson(path.join(dir, 'profiles.json'));
811
+ const workflows = readJson(path.join(dir, 'workflows.json'));
812
+ const tasks = readJson(path.join(dir, 'tasks.json'));
813
+
814
+ // Merge heartbeat files for live activity data
815
+ try {
816
+ const files = fs.readdirSync(dir).filter(f => f.startsWith('heartbeat-') && f.endsWith('.json'));
817
+ for (const f of files) {
818
+ const name = f.slice(10, -5);
819
+ if (agents[name]) {
820
+ try {
821
+ const hb = JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8'));
822
+ if (hb.last_activity) agents[name].last_activity = hb.last_activity;
823
+ if (hb.pid) agents[name].pid = hb.pid;
824
+ } catch {}
825
+ }
826
+ }
827
+ } catch {}
828
+
829
+ const onlineCount = Object.values(agents).filter(a => isPidAlive(a.pid)).length;
830
+
831
+ console.log('');
832
+ console.log(' Let Them Talk — Status');
833
+ console.log(' =======================');
834
+ console.log(' Messages: ' + history.length + ' | Agents: ' + onlineCount + '/' + Object.keys(agents).length + ' online');
835
+ console.log('');
836
+
837
+ // Agents with roles
838
+ const names = Object.keys(agents);
839
+ if (!names.length) {
840
+ console.log(' No agents registered.');
841
+ } else {
842
+ console.log(' Agents:');
843
+ for (const name of names) {
844
+ const info = agents[name];
845
+ const alive = isPidAlive(info.pid);
846
+ const status = alive ? '\x1b[32monline\x1b[0m' : '\x1b[31moffline\x1b[0m';
847
+ const lastActivity = info.last_activity || info.timestamp || '';
848
+ const role = (profiles && profiles[name] && profiles[name].role) ? ' [' + profiles[name].role + ']' : '';
849
+ const msgCount = history.filter(m => m.from === name).length;
850
+ console.log(' ' + name.padEnd(16) + ' ' + status + role.padEnd(16) + ' msgs: ' + msgCount + ' last: ' + (lastActivity ? new Date(lastActivity).toLocaleTimeString() : '-'));
851
+ }
852
+ }
853
+
854
+ // Active workflows
855
+ const activeWfs = Array.isArray(workflows) ? workflows.filter(w => w.status === 'active') : [];
856
+ if (activeWfs.length > 0) {
857
+ console.log('');
858
+ console.log(' Workflows:');
859
+ for (const wf of activeWfs) {
860
+ const done = wf.steps.filter(s => s.status === 'done').length;
861
+ const total = wf.steps.length;
862
+ const pct = Math.round((done / total) * 100);
863
+ const mode = wf.autonomous ? ' (autonomous)' : '';
864
+ console.log(' ' + wf.name.padEnd(24) + ' ' + done + '/' + total + ' (' + pct + '%)' + mode);
865
+ }
866
+ }
867
+
868
+ // Active tasks
869
+ const activeTasks = Array.isArray(tasks) ? tasks.filter(t => t.status === 'in_progress') : [];
870
+ if (activeTasks.length > 0) {
871
+ console.log('');
872
+ console.log(' Tasks in progress:');
873
+ for (const t of activeTasks.slice(0, 5)) {
874
+ console.log(' ' + (t.title || 'Untitled').padEnd(30) + ' -> ' + (t.assignee || 'unassigned'));
875
+ }
876
+ if (activeTasks.length > 5) console.log(' ... and ' + (activeTasks.length - 5) + ' more');
877
+ }
878
+
879
+ console.log('');
880
+ }
881
+
882
+ function cliMigrate() {
883
+ const args = process.argv.slice(3);
884
+ const dryRun = args.includes('--dry-run') || args.includes('-n');
885
+ const positional = args.filter((a) => !a.startsWith('-'));
886
+ const projectArg = positional[0] || process.cwd();
887
+ const { migrate } = require('./scripts/migrate-legacy-to-canonical');
888
+ migrate(projectArg, { dryRun });
889
+ }
890
+
891
+ // v5.0: Diagnostic health check
892
+ function cliDoctor() {
893
+ console.log('');
894
+ console.log(' \x1b[1mLet Them Talk Doctor\x1b[0m');
895
+ console.log(' ======================');
896
+ let issues = 0;
897
+
898
+ // Check data directory
899
+ const dir = resolveDataDirCli();
900
+ if (fs.existsSync(dir)) {
901
+ console.log(' \x1b[32m✓\x1b[0m .agent-bridge/ directory exists');
902
+ try { fs.accessSync(dir, fs.constants.W_OK); console.log(' \x1b[32m✓\x1b[0m .agent-bridge/ is writable'); }
903
+ catch { console.log(' \x1b[31m✗\x1b[0m .agent-bridge/ is NOT writable'); issues++; }
904
+ } else {
905
+ console.log(' \x1b[33m!\x1b[0m .agent-bridge/ not found. Run "npx let-them-talk init" first.');
906
+ issues++;
907
+ }
908
+
909
+ // Check server.js
910
+ const serverPath = path.join(__dirname, 'server.js');
911
+ if (fs.existsSync(serverPath)) {
912
+ console.log(' \x1b[32m✓\x1b[0m server.js found');
913
+ } else {
914
+ console.log(' \x1b[31m✗\x1b[0m server.js MISSING'); issues++;
915
+ }
916
+
917
+ // Check agents online
918
+ if (fs.existsSync(dir)) {
919
+ const agentsFile = path.join(dir, 'agents.json');
920
+ if (fs.existsSync(agentsFile)) {
921
+ const agents = readJson(agentsFile);
922
+ const online = Object.entries(agents).filter(([, a]) => isPidAlive(a.pid)).length;
923
+ const total = Object.keys(agents).length;
924
+ if (online > 0) {
925
+ console.log(' \x1b[32m✓\x1b[0m ' + online + '/' + total + ' agents online');
926
+ } else if (total > 0) {
927
+ console.log(' \x1b[33m!\x1b[0m ' + total + ' agents registered but none online');
928
+ } else {
929
+ console.log(' \x1b[33m!\x1b[0m No agents registered yet');
930
+ }
931
+ }
932
+
933
+ // Check config
934
+ const configFile = path.join(dir, 'config.json');
935
+ if (fs.existsSync(configFile)) {
936
+ const config = readJson(configFile);
937
+ console.log(' \x1b[32m✓\x1b[0m Conversation mode: ' + (config.conversation_mode || 'direct'));
938
+ }
939
+
940
+ // Check guide file
941
+ const guideFile = path.join(dir, 'guide.md');
942
+ if (fs.existsSync(guideFile)) {
943
+ console.log(' \x1b[32m✓\x1b[0m Custom guide.md found');
944
+ }
945
+ }
946
+
947
+ // Check Node version
948
+ const nodeVersion = process.version;
949
+ const major = parseInt(nodeVersion.slice(1));
950
+ if (major >= 18) {
951
+ console.log(' \x1b[32m✓\x1b[0m Node.js ' + nodeVersion + ' (OK)');
952
+ } else {
953
+ console.log(' \x1b[31m✗\x1b[0m Node.js ' + nodeVersion + ' — v18+ recommended'); issues++;
954
+ }
955
+
956
+ console.log('');
957
+ if (issues === 0) {
958
+ console.log(' \x1b[32mAll checks passed. System is healthy.\x1b[0m');
959
+ } else {
960
+ console.log(' \x1b[31m' + issues + ' issue(s) found. Fix them and run doctor again.\x1b[0m');
961
+ }
962
+ console.log('');
963
+ }
964
+
965
+ // Uninstall agent-bridge from all CLI configs
966
+ function uninstall() {
967
+ const cwd = process.cwd();
968
+ const home = os.homedir();
969
+ const removed = [];
970
+ const notFound = [];
971
+
972
+ console.log('');
973
+ console.log(' Let Them Talk — Uninstall');
974
+ console.log(' =========================');
975
+ console.log('');
976
+
977
+ // 1. Remove from Claude Code project config (.mcp.json in cwd)
978
+ const mcpLocalPath = path.join(cwd, '.mcp.json');
979
+ if (fs.existsSync(mcpLocalPath)) {
980
+ try {
981
+ const mcpConfig = JSON.parse(fs.readFileSync(mcpLocalPath, 'utf8'));
982
+ if (mcpConfig.mcpServers && mcpConfig.mcpServers['agent-bridge']) {
983
+ delete mcpConfig.mcpServers['agent-bridge'];
984
+ fs.writeFileSync(mcpLocalPath, JSON.stringify(mcpConfig, null, 2) + '\n');
985
+ removed.push('Claude Code (project): ' + mcpLocalPath);
986
+ } else {
987
+ notFound.push('Claude Code (project): no agent-bridge entry in .mcp.json');
988
+ }
989
+ } catch (e) {
990
+ console.log(' [warn] Could not parse ' + mcpLocalPath + ': ' + e.message);
991
+ }
992
+ } else {
993
+ notFound.push('Claude Code (project): .mcp.json not found');
994
+ }
995
+
996
+ // 2. Remove from Claude Code global config (~/.claude/mcp.json)
997
+ const mcpGlobalPath = path.join(home, '.claude', 'mcp.json');
998
+ if (fs.existsSync(mcpGlobalPath)) {
999
+ try {
1000
+ const mcpConfig = JSON.parse(fs.readFileSync(mcpGlobalPath, 'utf8'));
1001
+ if (mcpConfig.mcpServers && mcpConfig.mcpServers['agent-bridge']) {
1002
+ delete mcpConfig.mcpServers['agent-bridge'];
1003
+ fs.writeFileSync(mcpGlobalPath, JSON.stringify(mcpConfig, null, 2) + '\n');
1004
+ removed.push('Claude Code (global): ' + mcpGlobalPath);
1005
+ } else {
1006
+ notFound.push('Claude Code (global): no agent-bridge entry');
1007
+ }
1008
+ } catch (e) {
1009
+ console.log(' [warn] Could not parse ' + mcpGlobalPath + ': ' + e.message);
1010
+ }
1011
+ } else {
1012
+ notFound.push('Claude Code (global): ~/.claude/mcp.json not found');
1013
+ }
1014
+
1015
+ // 3. Remove from Gemini CLI config (~/.gemini/settings.json)
1016
+ const geminiSettingsPath = path.join(home, '.gemini', 'settings.json');
1017
+ if (fs.existsSync(geminiSettingsPath)) {
1018
+ try {
1019
+ const settings = JSON.parse(fs.readFileSync(geminiSettingsPath, 'utf8'));
1020
+ if (settings.mcpServers && settings.mcpServers['agent-bridge']) {
1021
+ delete settings.mcpServers['agent-bridge'];
1022
+ fs.writeFileSync(geminiSettingsPath, JSON.stringify(settings, null, 2) + '\n');
1023
+ removed.push('Gemini CLI: ' + geminiSettingsPath);
1024
+ } else {
1025
+ notFound.push('Gemini CLI: no agent-bridge entry');
1026
+ }
1027
+ } catch (e) {
1028
+ console.log(' [warn] Could not parse ' + geminiSettingsPath + ': ' + e.message);
1029
+ }
1030
+ } else {
1031
+ notFound.push('Gemini CLI: ~/.gemini/settings.json not found');
1032
+ }
1033
+
1034
+ // 4. Remove from Gemini CLI project config (.gemini/settings.json in cwd)
1035
+ const geminiLocalPath = path.join(cwd, '.gemini', 'settings.json');
1036
+ if (fs.existsSync(geminiLocalPath)) {
1037
+ try {
1038
+ const settings = JSON.parse(fs.readFileSync(geminiLocalPath, 'utf8'));
1039
+ if (settings.mcpServers && settings.mcpServers['agent-bridge']) {
1040
+ delete settings.mcpServers['agent-bridge'];
1041
+ fs.writeFileSync(geminiLocalPath, JSON.stringify(settings, null, 2) + '\n');
1042
+ removed.push('Gemini CLI (project): ' + geminiLocalPath);
1043
+ } else {
1044
+ notFound.push('Gemini CLI (project): no agent-bridge entry');
1045
+ }
1046
+ } catch (e) {
1047
+ console.log(' [warn] Could not parse ' + geminiLocalPath + ': ' + e.message);
1048
+ }
1049
+ }
1050
+
1051
+ // 5. Remove from Codex CLI config (~/.codex/config.toml)
1052
+ const codexConfigPath = path.join(home, '.codex', 'config.toml');
1053
+ if (fs.existsSync(codexConfigPath)) {
1054
+ try {
1055
+ let config = fs.readFileSync(codexConfigPath, 'utf8');
1056
+ if (config.includes('[mcp_servers.agent-bridge]')) {
1057
+ // Remove from [mcp_servers.agent-bridge] to the next [section] or end of file
1058
+ // This covers both [mcp_servers.agent-bridge] and [mcp_servers.agent-bridge.env]
1059
+ config = config.replace(/\n?\[mcp_servers\.agent-bridge[^\]]*\][^\[]*(?=\[|$)/g, '');
1060
+ // Clean up multiple blank lines left behind
1061
+ config = config.replace(/\n{3,}/g, '\n\n');
1062
+ fs.writeFileSync(codexConfigPath, config);
1063
+ removed.push('Codex CLI: ' + codexConfigPath);
1064
+ } else {
1065
+ notFound.push('Codex CLI: no agent-bridge section in config.toml');
1066
+ }
1067
+ } catch (e) {
1068
+ console.log(' [warn] Could not process ' + codexConfigPath + ': ' + e.message);
1069
+ }
1070
+ } else {
1071
+ notFound.push('Codex CLI: ~/.codex/config.toml not found');
1072
+ }
1073
+
1074
+ // 6. Remove from Codex CLI project config (.codex/config.toml in cwd)
1075
+ const codexLocalPath = path.join(cwd, '.codex', 'config.toml');
1076
+ if (fs.existsSync(codexLocalPath)) {
1077
+ try {
1078
+ let config = fs.readFileSync(codexLocalPath, 'utf8');
1079
+ if (config.includes('[mcp_servers.agent-bridge]')) {
1080
+ config = config.replace(/\n?\[mcp_servers\.agent-bridge[^\]]*\][^\[]*(?=\[|$)/g, '');
1081
+ config = config.replace(/\n{3,}/g, '\n\n');
1082
+ fs.writeFileSync(codexLocalPath, config);
1083
+ removed.push('Codex CLI (project): ' + codexLocalPath);
1084
+ }
1085
+ } catch (e) {
1086
+ console.log(' [warn] Could not process ' + codexLocalPath + ': ' + e.message);
1087
+ }
1088
+ }
1089
+
1090
+ // Print summary
1091
+ if (removed.length > 0) {
1092
+ console.log(' Removed agent-bridge from:');
1093
+ for (const r of removed) {
1094
+ console.log(' [ok] ' + r);
1095
+ }
1096
+ } else {
1097
+ console.log(' No agent-bridge configurations found to remove.');
1098
+ }
1099
+
1100
+ if (notFound.length > 0) {
1101
+ console.log('');
1102
+ console.log(' Skipped (not found):');
1103
+ for (const n of notFound) {
1104
+ console.log(' [-] ' + n);
1105
+ }
1106
+ }
1107
+
1108
+ // 7. Check for data directory
1109
+ const dataPath = path.join(cwd, '.agent-bridge');
1110
+ if (fs.existsSync(dataPath)) {
1111
+ console.log('');
1112
+ console.log(' Found .agent-bridge/ directory with conversation data.');
1113
+ console.log(' To remove it, manually delete: ' + dataPath);
1114
+ }
1115
+
1116
+ console.log('');
1117
+ if (removed.length > 0) {
1118
+ console.log(' Restart your CLI terminals for changes to take effect.');
1119
+ }
1120
+ console.log('');
1121
+ }
1122
+
1123
+ function runCli() {
1124
+ const command = process.argv[2];
1125
+
1126
+ switch (command) {
1127
+ case 'init':
1128
+ init();
1129
+ break;
1130
+ case 'templates':
1131
+ listTemplates();
1132
+ break;
1133
+ case 'dashboard':
1134
+ dashboard();
1135
+ break;
1136
+ case 'reset':
1137
+ reset();
1138
+ break;
1139
+ case 'doctor':
1140
+ cliDoctor();
1141
+ break;
1142
+ case 'migrate':
1143
+ case 'migrate-legacy':
1144
+ cliMigrate();
1145
+ break;
1146
+ case 'msg':
1147
+ case 'message':
1148
+ case 'send':
1149
+ cliMsg();
1150
+ break;
1151
+ case 'status':
1152
+ cliStatus();
1153
+ break;
1154
+ case 'uninstall':
1155
+ case 'remove':
1156
+ uninstall();
1157
+ break;
1158
+ case 'plugin':
1159
+ case 'plugins':
1160
+ console.log(' Plugins have been removed in v3.4.3. CLI terminals have their own extension systems.');
1161
+ break;
1162
+ case 'help':
1163
+ case '--help':
1164
+ case '-h':
1165
+ case undefined:
1166
+ printUsage();
1167
+ break;
1168
+ default:
1169
+ console.error(` Unknown command: ${command}`);
1170
+ printUsage();
1171
+ process.exit(1);
1172
+ }
1173
+ }
1174
+
1175
+ function shouldAutoRunCli() {
1176
+ if (require.main === module) return true;
1177
+ const argvPath = process.argv[1];
1178
+ if (!argvPath) return false;
1179
+ const normalizedArgvPath = path.resolve(argvPath).replace(/\\/g, '/');
1180
+ const normalizedFilePath = path.resolve(__filename).replace(/\\/g, '/');
1181
+ return process.platform === 'win32'
1182
+ ? normalizedArgvPath.toLowerCase() === normalizedFilePath.toLowerCase()
1183
+ : normalizedArgvPath === normalizedFilePath;
1184
+ }
1185
+
1186
+ module.exports = {
1187
+ init,
1188
+ runCli,
1189
+ };
1190
+
1191
+ if (shouldAutoRunCli()) {
1192
+ runCli();
1193
+ }