let-them-talk 5.4.1 → 5.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -2
- package/USAGE.md +1 -1
- package/cli.js +1184 -1245
- package/conversation-templates/autonomous-feature.json +4 -4
- package/conversation-templates/code-review.json +3 -3
- package/conversation-templates/debug-squad.json +3 -3
- package/conversation-templates/feature-build.json +3 -3
- package/conversation-templates/research-write.json +3 -3
- package/dashboard.html +0 -19
- package/dashboard.js +7 -50
- package/package.json +1 -1
- package/scripts/check-dashboard-control-plane.js +7 -63
- package/server.js +12 -250
- package/templates/debate.json +2 -2
- package/templates/managed.json +4 -4
- package/templates/pair.json +2 -2
- package/templates/review.json +2 -2
- package/templates/team.json +3 -3
package/cli.js
CHANGED
|
@@ -1,1245 +1,1184 @@
|
|
|
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.
|
|
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.
|
|
350
|
-
'',
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
targets =
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
console.log('');
|
|
575
|
-
return;
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
//
|
|
579
|
-
const
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
console.log('
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
'
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
'
|
|
608
|
-
|
|
609
|
-
})
|
|
610
|
-
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
console.log('
|
|
691
|
-
console.log('
|
|
692
|
-
console.log('');
|
|
693
|
-
console.log('
|
|
694
|
-
console.log('
|
|
695
|
-
console.log('
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
console.
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
.
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
function
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
const
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
const
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
// Check
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
const
|
|
940
|
-
const
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
console.log('');
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
//
|
|
969
|
-
const
|
|
970
|
-
if (fs.existsSync(
|
|
971
|
-
try {
|
|
972
|
-
const mcpConfig = JSON.parse(fs.readFileSync(
|
|
973
|
-
if (mcpConfig.mcpServers && mcpConfig.mcpServers['agent-bridge']) {
|
|
974
|
-
delete mcpConfig.mcpServers['agent-bridge'];
|
|
975
|
-
fs.writeFileSync(
|
|
976
|
-
removed.push('Claude Code (
|
|
977
|
-
} else {
|
|
978
|
-
notFound.push('Claude Code (
|
|
979
|
-
}
|
|
980
|
-
} catch (e) {
|
|
981
|
-
console.log(' [warn] Could not parse ' +
|
|
982
|
-
}
|
|
983
|
-
} else {
|
|
984
|
-
notFound.push('Claude Code (
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
//
|
|
988
|
-
const
|
|
989
|
-
if (fs.existsSync(
|
|
990
|
-
try {
|
|
991
|
-
const
|
|
992
|
-
if (
|
|
993
|
-
delete
|
|
994
|
-
fs.writeFileSync(
|
|
995
|
-
removed.push('
|
|
996
|
-
} else {
|
|
997
|
-
notFound.push('
|
|
998
|
-
}
|
|
999
|
-
} catch (e) {
|
|
1000
|
-
console.log(' [warn] Could not parse ' +
|
|
1001
|
-
}
|
|
1002
|
-
} else {
|
|
1003
|
-
notFound.push('
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
//
|
|
1007
|
-
const
|
|
1008
|
-
if (fs.existsSync(
|
|
1009
|
-
try {
|
|
1010
|
-
const settings = JSON.parse(fs.readFileSync(
|
|
1011
|
-
if (settings.mcpServers && settings.mcpServers['agent-bridge']) {
|
|
1012
|
-
delete settings.mcpServers['agent-bridge'];
|
|
1013
|
-
fs.writeFileSync(
|
|
1014
|
-
removed.push('Gemini CLI
|
|
1015
|
-
} else {
|
|
1016
|
-
notFound.push('Gemini CLI
|
|
1017
|
-
}
|
|
1018
|
-
} catch (e) {
|
|
1019
|
-
console.log(' [warn] Could not parse ' +
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
config = config.replace(/\n
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
if (
|
|
1083
|
-
console.log('');
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
console.log('
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
case 'dashboard':
|
|
1186
|
-
dashboard();
|
|
1187
|
-
break;
|
|
1188
|
-
case 'reset':
|
|
1189
|
-
reset();
|
|
1190
|
-
break;
|
|
1191
|
-
case 'doctor':
|
|
1192
|
-
cliDoctor();
|
|
1193
|
-
break;
|
|
1194
|
-
case 'migrate':
|
|
1195
|
-
case 'migrate-legacy':
|
|
1196
|
-
cliMigrate();
|
|
1197
|
-
break;
|
|
1198
|
-
case 'msg':
|
|
1199
|
-
case 'message':
|
|
1200
|
-
case 'send':
|
|
1201
|
-
cliMsg();
|
|
1202
|
-
break;
|
|
1203
|
-
case 'status':
|
|
1204
|
-
cliStatus();
|
|
1205
|
-
break;
|
|
1206
|
-
case 'uninstall':
|
|
1207
|
-
case 'remove':
|
|
1208
|
-
uninstall();
|
|
1209
|
-
break;
|
|
1210
|
-
case 'plugin':
|
|
1211
|
-
case 'plugins':
|
|
1212
|
-
console.log(' Plugins have been removed in v3.4.3. CLI terminals have their own extension systems.');
|
|
1213
|
-
break;
|
|
1214
|
-
case 'help':
|
|
1215
|
-
case '--help':
|
|
1216
|
-
case '-h':
|
|
1217
|
-
case undefined:
|
|
1218
|
-
printUsage();
|
|
1219
|
-
break;
|
|
1220
|
-
default:
|
|
1221
|
-
console.error(` Unknown command: ${command}`);
|
|
1222
|
-
printUsage();
|
|
1223
|
-
process.exit(1);
|
|
1224
|
-
}
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
function shouldAutoRunCli() {
|
|
1228
|
-
if (require.main === module) return true;
|
|
1229
|
-
const argvPath = process.argv[1];
|
|
1230
|
-
if (!argvPath) return false;
|
|
1231
|
-
const normalizedArgvPath = path.resolve(argvPath).replace(/\\/g, '/');
|
|
1232
|
-
const normalizedFilePath = path.resolve(__filename).replace(/\\/g, '/');
|
|
1233
|
-
return process.platform === 'win32'
|
|
1234
|
-
? normalizedArgvPath.toLowerCase() === normalizedFilePath.toLowerCase()
|
|
1235
|
-
: normalizedArgvPath === normalizedFilePath;
|
|
1236
|
-
}
|
|
1237
|
-
|
|
1238
|
-
module.exports = {
|
|
1239
|
-
init,
|
|
1240
|
-
runCli,
|
|
1241
|
-
};
|
|
1242
|
-
|
|
1243
|
-
if (shouldAutoRunCli()) {
|
|
1244
|
-
runCli();
|
|
1245
|
-
}
|
|
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.3
|
|
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
|
+
function runCli() {
|
|
1115
|
+
const command = process.argv[2];
|
|
1116
|
+
|
|
1117
|
+
switch (command) {
|
|
1118
|
+
case 'init':
|
|
1119
|
+
init();
|
|
1120
|
+
break;
|
|
1121
|
+
case 'templates':
|
|
1122
|
+
listTemplates();
|
|
1123
|
+
break;
|
|
1124
|
+
case 'dashboard':
|
|
1125
|
+
dashboard();
|
|
1126
|
+
break;
|
|
1127
|
+
case 'reset':
|
|
1128
|
+
reset();
|
|
1129
|
+
break;
|
|
1130
|
+
case 'doctor':
|
|
1131
|
+
cliDoctor();
|
|
1132
|
+
break;
|
|
1133
|
+
case 'migrate':
|
|
1134
|
+
case 'migrate-legacy':
|
|
1135
|
+
cliMigrate();
|
|
1136
|
+
break;
|
|
1137
|
+
case 'msg':
|
|
1138
|
+
case 'message':
|
|
1139
|
+
case 'send':
|
|
1140
|
+
cliMsg();
|
|
1141
|
+
break;
|
|
1142
|
+
case 'status':
|
|
1143
|
+
cliStatus();
|
|
1144
|
+
break;
|
|
1145
|
+
case 'uninstall':
|
|
1146
|
+
case 'remove':
|
|
1147
|
+
uninstall();
|
|
1148
|
+
break;
|
|
1149
|
+
case 'plugin':
|
|
1150
|
+
case 'plugins':
|
|
1151
|
+
console.log(' Plugins have been removed in v3.4.3. CLI terminals have their own extension systems.');
|
|
1152
|
+
break;
|
|
1153
|
+
case 'help':
|
|
1154
|
+
case '--help':
|
|
1155
|
+
case '-h':
|
|
1156
|
+
case undefined:
|
|
1157
|
+
printUsage();
|
|
1158
|
+
break;
|
|
1159
|
+
default:
|
|
1160
|
+
console.error(` Unknown command: ${command}`);
|
|
1161
|
+
printUsage();
|
|
1162
|
+
process.exit(1);
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
function shouldAutoRunCli() {
|
|
1167
|
+
if (require.main === module) return true;
|
|
1168
|
+
const argvPath = process.argv[1];
|
|
1169
|
+
if (!argvPath) return false;
|
|
1170
|
+
const normalizedArgvPath = path.resolve(argvPath).replace(/\\/g, '/');
|
|
1171
|
+
const normalizedFilePath = path.resolve(__filename).replace(/\\/g, '/');
|
|
1172
|
+
return process.platform === 'win32'
|
|
1173
|
+
? normalizedArgvPath.toLowerCase() === normalizedFilePath.toLowerCase()
|
|
1174
|
+
: normalizedArgvPath === normalizedFilePath;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
module.exports = {
|
|
1178
|
+
init,
|
|
1179
|
+
runCli,
|
|
1180
|
+
};
|
|
1181
|
+
|
|
1182
|
+
if (shouldAutoRunCli()) {
|
|
1183
|
+
runCli();
|
|
1184
|
+
}
|