let-them-talk 3.4.4 → 3.5.1

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/dashboard.js CHANGED
@@ -138,8 +138,15 @@ function readJson(file) {
138
138
  try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return {}; }
139
139
  }
140
140
 
141
- function isPidAlive(pid) {
142
- try { process.kill(pid, 0); return true; } catch { return false; }
141
+ function isPidAlive(pid, lastActivity) {
142
+ try {
143
+ process.kill(pid, 0);
144
+ if (lastActivity) {
145
+ const stale = Date.now() - new Date(lastActivity).getTime();
146
+ if (stale > 30000) return false; // 30s = 3 missed heartbeats
147
+ }
148
+ return true;
149
+ } catch { return false; }
143
150
  }
144
151
 
145
152
  // --- Default avatar helpers ---
@@ -207,7 +214,7 @@ function apiAgents(query) {
207
214
  }
208
215
 
209
216
  for (const [name, info] of Object.entries(agents)) {
210
- const alive = isPidAlive(info.pid);
217
+ const alive = isPidAlive(info.pid, info.last_activity);
211
218
  const lastActivity = info.last_activity || info.timestamp;
212
219
  const idleSeconds = Math.floor((Date.now() - new Date(lastActivity).getTime()) / 1000);
213
220
  const profile = profiles[name] || {};
@@ -240,9 +247,9 @@ function apiStatus(query) {
240
247
  history.forEach(m => { if (m.thread_id) threads.add(m.thread_id); });
241
248
 
242
249
  const agentEntries = Object.entries(agents);
243
- const aliveCount = agentEntries.filter(([, a]) => isPidAlive(a.pid)).length;
250
+ const aliveCount = agentEntries.filter(([, a]) => isPidAlive(a.pid, a.last_activity)).length;
244
251
  const sleepingCount = agentEntries.filter(([, a]) => {
245
- if (!isPidAlive(a.pid)) return false;
252
+ if (!isPidAlive(a.pid, a.last_activity)) return false;
246
253
  const lastActivity = a.last_activity || a.timestamp;
247
254
  const idleSeconds = Math.floor((Date.now() - new Date(lastActivity).getTime()) / 1000);
248
255
  return idleSeconds > 60;
@@ -325,10 +332,252 @@ function apiStats(query) {
325
332
  };
326
333
  }
327
334
 
335
+ // --- v3.4: Notification Tracking ---
336
+ let notificationHistory = [];
337
+ let prevAgentState = {};
338
+
339
+ function generateNotifications(currentAgents) {
340
+ const crypto = require('crypto');
341
+ const now = new Date().toISOString();
342
+
343
+ for (const [name, agent] of Object.entries(currentAgents)) {
344
+ const prev = prevAgentState[name];
345
+ const isAlive = agent.pid ? isPidAlive(agent.pid, agent.last_activity) : false;
346
+ const isListening = !!agent.listening;
347
+
348
+ if (prev) {
349
+ if (!prev.alive && isAlive) {
350
+ notificationHistory.push({ id: crypto.randomBytes(8).toString('hex'), type: 'agent_online', agent: name, message: `${name} came online`, timestamp: now });
351
+ }
352
+ if (prev.alive && !isAlive) {
353
+ notificationHistory.push({ id: crypto.randomBytes(8).toString('hex'), type: 'agent_offline', agent: name, message: `${name} went offline`, timestamp: now });
354
+ }
355
+ if (!prev.listening && isListening) {
356
+ notificationHistory.push({ id: crypto.randomBytes(8).toString('hex'), type: 'agent_listening', agent: name, message: `${name} started listening`, timestamp: now });
357
+ }
358
+ if (prev.listening && !isListening) {
359
+ notificationHistory.push({ id: crypto.randomBytes(8).toString('hex'), type: 'agent_busy', agent: name, message: `${name} stopped listening`, timestamp: now });
360
+ }
361
+ } else if (isAlive) {
362
+ notificationHistory.push({ id: crypto.randomBytes(8).toString('hex'), type: 'agent_online', agent: name, message: `${name} came online`, timestamp: now });
363
+ }
364
+
365
+ prevAgentState[name] = { alive: isAlive, listening: isListening };
366
+ }
367
+
368
+ // Trim to max 50
369
+ if (notificationHistory.length > 50) {
370
+ notificationHistory = notificationHistory.slice(-50);
371
+ }
372
+ }
373
+
374
+ function apiNotifications() {
375
+ return notificationHistory;
376
+ }
377
+
378
+ // --- v3.4: Performance Scoring ---
379
+ function apiScores(query) {
380
+ const projectPath = query.get('project') || null;
381
+ const history = readJsonl(filePath('history.jsonl', projectPath));
382
+ const agents = readJson(filePath('agents.json', projectPath));
383
+
384
+ const perAgent = {};
385
+ const totalMessages = history.length;
386
+ const allAgentNames = new Set();
387
+
388
+ // Gather per-agent data
389
+ for (let i = 0; i < history.length; i++) {
390
+ const m = history[i];
391
+ const from = m.from || 'unknown';
392
+ allAgentNames.add(from);
393
+ if (m.to) allAgentNames.add(m.to);
394
+ if (!perAgent[from]) perAgent[from] = { messages: 0, responseTimes: [], peers: new Set() };
395
+ perAgent[from].messages++;
396
+ if (m.to) perAgent[from].peers.add(m.to);
397
+
398
+ if (m.reply_to) {
399
+ for (let j = i - 1; j >= Math.max(0, i - 50); j--) {
400
+ if (history[j].id === m.reply_to) {
401
+ const delta = new Date(m.timestamp).getTime() - new Date(history[j].timestamp).getTime();
402
+ if (delta > 0 && delta < 3600000) perAgent[from].responseTimes.push(delta / 1000);
403
+ break;
404
+ }
405
+ }
406
+ }
407
+ }
408
+
409
+ const totalAgents = allAgentNames.size;
410
+ const maxMessages = Math.max(1, ...Object.values(perAgent).map(d => d.messages));
411
+
412
+ const result = {};
413
+ const scores = [];
414
+
415
+ for (const [name, data] of Object.entries(perAgent)) {
416
+ const avgResponseSec = data.responseTimes.length
417
+ ? data.responseTimes.reduce((a, b) => a + b, 0) / data.responseTimes.length
418
+ : Infinity;
419
+
420
+ // Responsiveness (30 pts)
421
+ let responsiveness;
422
+ if (avgResponseSec < 10) responsiveness = 30;
423
+ else if (avgResponseSec < 30) responsiveness = 25;
424
+ else if (avgResponseSec < 60) responsiveness = 20;
425
+ else if (avgResponseSec < 120) responsiveness = 15;
426
+ else responsiveness = 10;
427
+
428
+ // Activity (30 pts) — linear scale relative to top agent
429
+ const activity = Math.round((data.messages / maxMessages) * 30);
430
+
431
+ // Reliability (20 pts) — uptime based on agent registration
432
+ let reliability = 10;
433
+ const agentInfo = agents[name];
434
+ if (agentInfo) {
435
+ const isAlive = agentInfo.pid ? isPidAlive(agentInfo.pid, agentInfo.last_activity) : false;
436
+ const registered = new Date(agentInfo.registered_at || agentInfo.last_activity).getTime();
437
+ const totalTime = Date.now() - registered;
438
+ if (totalTime > 0 && isAlive) {
439
+ const lastAct = new Date(agentInfo.last_activity).getTime();
440
+ const activeTime = lastAct - registered;
441
+ const uptime = Math.min(1, activeTime / totalTime);
442
+ if (uptime > 0.95) reliability = 20;
443
+ else if (uptime > 0.80) reliability = 15;
444
+ else if (uptime > 0.50) reliability = 10;
445
+ else reliability = 5;
446
+ } else if (!isAlive) {
447
+ reliability = 5;
448
+ }
449
+ }
450
+
451
+ // Collaboration (20 pts)
452
+ const collaboration = totalAgents > 1
453
+ ? Math.round((data.peers.size / (totalAgents - 1)) * 20)
454
+ : 20;
455
+
456
+ const score = responsiveness + activity + reliability + collaboration;
457
+ result[name] = { score, responsiveness, activity, reliability, collaboration };
458
+ scores.push({ name, score });
459
+ }
460
+
461
+ // Add ranks
462
+ scores.sort((a, b) => b.score - a.score);
463
+ scores.forEach((s, i) => { result[s.name].rank = i + 1; });
464
+
465
+ return { agents: result };
466
+ }
467
+
468
+ // --- v3.4: Cross-Project Search ---
469
+ function apiSearchAll(query) {
470
+ const q = (query.get('q') || '').toLowerCase();
471
+ const limit = Math.min(parseInt(query.get('limit') || '50', 10), 200);
472
+ if (!q) return { error: 'Missing "q" parameter' };
473
+
474
+ const projects = getProjects();
475
+ // Add default project
476
+ const allProjects = [{ name: path.basename(process.cwd()), path: null }];
477
+ for (const p of projects) allProjects.push(p);
478
+
479
+ const results = [];
480
+ let total = 0;
481
+
482
+ for (const proj of allProjects) {
483
+ const history = readJsonl(filePath('history.jsonl', proj.path));
484
+ const matches = [];
485
+ for (const m of history) {
486
+ if (matches.length >= limit) break;
487
+ const content = (m.content || '').toLowerCase();
488
+ const from = (m.from || '').toLowerCase();
489
+ const to = (m.to || '').toLowerCase();
490
+ if (content.includes(q) || from.includes(q) || to.includes(q)) {
491
+ matches.push({ id: m.id, from: m.from, to: m.to, content: m.content, timestamp: m.timestamp });
492
+ }
493
+ }
494
+ if (matches.length > 0) {
495
+ results.push({ project: proj.name, path: proj.path || process.cwd(), messages: matches });
496
+ total += matches.length;
497
+ }
498
+ }
499
+
500
+ return { results, total };
501
+ }
502
+
503
+ // --- v3.4: Replay Export ---
504
+ function apiExportReplay(query) {
505
+ const projectPath = query.get('project') || null;
506
+ const history = readJsonl(filePath('history.jsonl', projectPath));
507
+ const profiles = readJson(filePath('profiles.json', projectPath));
508
+
509
+ const colors = ['#58a6ff','#3fb950','#d29922','#bc8cff','#f778ba','#ff7b72','#79c0ff','#7ee787'];
510
+ const agentColors = {};
511
+ let colorIdx = 0;
512
+ for (const m of history) {
513
+ if (!agentColors[m.from]) agentColors[m.from] = colors[colorIdx++ % colors.length];
514
+ }
515
+
516
+ const messagesJson = JSON.stringify(history.map(m => ({
517
+ from: m.from, to: m.to, content: m.content, timestamp: m.timestamp, color: agentColors[m.from] || '#58a6ff'
518
+ })));
519
+
520
+ return `<!DOCTYPE html>
521
+ <html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
522
+ <title>Let Them Talk — Replay</title>
523
+ <style>
524
+ :root{--bg:#0d1117;--surface:#161b22;--surface-2:#21262d;--border:#30363d;--text:#e6edf3;--dim:#8b949e}
525
+ *{margin:0;padding:0;box-sizing:border-box}
526
+ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:var(--bg);color:var(--text);line-height:1.6}
527
+ .header{background:var(--surface);border-bottom:1px solid var(--border);padding:12px 20px;display:flex;align-items:center;justify-content:space-between}
528
+ .title{font-size:16px;font-weight:700;color:var(--text)}
529
+ .controls{display:flex;gap:8px;align-items:center}
530
+ .controls button{background:var(--surface-2);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:6px 14px;cursor:pointer;font-size:13px}
531
+ .controls button:hover{background:var(--border)}
532
+ .controls select{background:var(--surface-2);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:4px 8px;font-size:13px}
533
+ .messages{max-width:800px;margin:20px auto;padding:0 16px}
534
+ .msg{opacity:0;transform:translateY(8px);transition:opacity 0.3s,transform 0.3s;margin-bottom:12px;padding:10px 14px;background:var(--surface);border:1px solid var(--border);border-radius:8px}
535
+ .msg.visible{opacity:1;transform:translateY(0)}
536
+ .msg-header{display:flex;gap:8px;align-items:baseline;margin-bottom:4px;font-size:13px}
537
+ .msg-from{font-weight:700}
538
+ .msg-to{color:var(--dim)}
539
+ .msg-time{color:var(--dim);margin-left:auto;font-size:11px}
540
+ .msg-content{font-size:14px;white-space:pre-wrap;word-break:break-word}
541
+ .msg-content code{background:var(--surface-2);padding:1px 5px;border-radius:3px;font-size:0.9em}
542
+ .msg-content strong{font-weight:700}
543
+ .progress{font-size:12px;color:var(--dim)}
544
+ </style></head><body>
545
+ <div class="header">
546
+ <span class="title">Let Them Talk — Replay</span>
547
+ <div class="controls">
548
+ <button id="btn" onclick="toggle()">Pause</button>
549
+ <label><span style="color:var(--dim);font-size:12px">Speed:</span>
550
+ <select id="speed" onchange="setSpeed(this.value)">
551
+ <option value="2000">Slow</option><option value="1000" selected>Normal</option><option value="500">Fast</option><option value="200">Very Fast</option>
552
+ </select></label>
553
+ <span class="progress" id="progress">0 / 0</span>
554
+ </div></div>
555
+ <div class="messages" id="messages"></div>
556
+ <script>
557
+ var msgs=${messagesJson};
558
+ var idx=0,playing=true,timer=null,speed=1000;
559
+ function md(s){return s.replace(/\`\`\`[\\s\\S]*?\`\`\`/g,function(m){return '<pre><code>'+m.slice(3,-3).replace(/^\\w*\\n/,'')+'</code></pre>'}).replace(/\`([^\`]+)\`/g,'<code>$1</code>').replace(/\\*\\*([^*]+)\\*\\*/g,'<strong>$1</strong>').replace(/^### (.+)$/gm,'<h4 style="margin:8px 0 4px;font-size:14px">$1</h4>').replace(/^## (.+)$/gm,'<h3 style="margin:8px 0 4px;font-size:15px">$1</h3>').replace(/^# (.+)$/gm,'<h2 style="margin:8px 0 4px;font-size:16px">$1</h2>')}
560
+ function esc(s){return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')}
561
+ function showNext(){if(idx>=msgs.length){playing=false;document.getElementById('btn').textContent='Done';return}
562
+ var m=msgs[idx],el=document.createElement('div');el.className='msg';
563
+ var t=new Date(m.timestamp);var time=t.toLocaleTimeString();
564
+ el.innerHTML='<div class="msg-header"><span class="msg-from" style="color:'+m.color+'">'+esc(m.from)+'</span><span class="msg-to">→ '+esc(m.to||'all')+'</span><span class="msg-time">'+time+'</span></div><div class="msg-content">'+md(esc(m.content))+'</div>';
565
+ document.getElementById('messages').appendChild(el);
566
+ requestAnimationFrame(function(){el.classList.add('visible')});
567
+ el.scrollIntoView({behavior:'smooth',block:'end'});
568
+ idx++;document.getElementById('progress').textContent=idx+' / '+msgs.length;
569
+ if(playing)timer=setTimeout(showNext,speed)}
570
+ function toggle(){if(idx>=msgs.length){idx=0;document.getElementById('messages').innerHTML='';playing=true;document.getElementById('btn').textContent='Pause';showNext();return}
571
+ playing=!playing;document.getElementById('btn').textContent=playing?'Pause':'Play';if(playing)showNext();else clearTimeout(timer)}
572
+ function setSpeed(v){speed=parseInt(v)}
573
+ showNext();
574
+ </script></body></html>`;
575
+ }
576
+
328
577
  function apiReset(query) {
329
578
  const projectPath = query.get('project') || null;
330
579
  const dataDir = resolveDataDir(projectPath);
331
- const fixedFiles = ['messages.jsonl', 'history.jsonl', 'agents.json', 'acks.json', 'tasks.json', 'profiles.json', 'workflows.json', 'branches.json', 'read_receipts.json', 'permissions.json'];
580
+ const fixedFiles = ['messages.jsonl', 'history.jsonl', 'agents.json', 'acks.json', 'tasks.json', 'profiles.json', 'workflows.json', 'branches.json', 'read_receipts.json', 'permissions.json', 'config.json'];
332
581
  for (const f of fixedFiles) {
333
582
  const p = path.join(dataDir, f);
334
583
  if (fs.existsSync(p)) fs.unlinkSync(p);
@@ -1397,6 +1646,31 @@ const server = http.createServer(async (req, res) => {
1397
1646
  res.writeHead(result.error ? 400 : 200, { 'Content-Type': 'application/json' });
1398
1647
  res.end(JSON.stringify(result));
1399
1648
  }
1649
+ // --- v3.4: Notifications ---
1650
+ else if (url.pathname === '/api/notifications' && req.method === 'GET') {
1651
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1652
+ res.end(JSON.stringify(apiNotifications()));
1653
+ }
1654
+ // --- v3.4: Performance Scores ---
1655
+ else if (url.pathname === '/api/scores' && req.method === 'GET') {
1656
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1657
+ res.end(JSON.stringify(apiScores(url.searchParams)));
1658
+ }
1659
+ // --- v3.4: Cross-Project Search ---
1660
+ else if (url.pathname === '/api/search-all' && req.method === 'GET') {
1661
+ const result = apiSearchAll(url.searchParams);
1662
+ res.writeHead(result.error ? 400 : 200, { 'Content-Type': 'application/json' });
1663
+ res.end(JSON.stringify(result));
1664
+ }
1665
+ // --- v3.4: Replay Export ---
1666
+ else if (url.pathname === '/api/export-replay' && req.method === 'GET') {
1667
+ const html = apiExportReplay(url.searchParams);
1668
+ res.writeHead(200, {
1669
+ 'Content-Type': 'text/html; charset=utf-8',
1670
+ 'Content-Disposition': 'attachment; filename="replay-' + new Date().toISOString().slice(0, 10) + '.html"',
1671
+ });
1672
+ res.end(html);
1673
+ }
1400
1674
  // Server-Sent Events endpoint for real-time updates
1401
1675
  else if (url.pathname === '/api/events' && req.method === 'GET') {
1402
1676
  if (sseClients.size >= 100) {
@@ -1429,6 +1703,12 @@ const server = http.createServer(async (req, res) => {
1429
1703
  const sseClients = new Set();
1430
1704
 
1431
1705
  function sseNotifyAll() {
1706
+ // Generate notifications from agent state changes
1707
+ try {
1708
+ const agents = readJson(filePath('agents.json'));
1709
+ generateNotifications(agents);
1710
+ } catch {}
1711
+
1432
1712
  for (const res of sseClients) {
1433
1713
  try {
1434
1714
  res.write(`data: update\n\n`);
@@ -1472,7 +1752,7 @@ server.listen(PORT, LAN_MODE ? '0.0.0.0' : '127.0.0.1', () => {
1472
1752
  const dataDir = resolveDataDir();
1473
1753
  const lanIP = getLanIP();
1474
1754
  console.log('');
1475
- console.log(' Let Them Talk - Agent Bridge Dashboard v3.4.4');
1755
+ console.log(' Let Them Talk - Agent Bridge Dashboard v3.5.1');
1476
1756
  console.log(' ============================================');
1477
1757
  console.log(' Dashboard: http://localhost:' + PORT);
1478
1758
  if (LAN_MODE && lanIP) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "let-them-talk",
3
- "version": "3.4.4",
3
+ "version": "3.5.1",
4
4
  "description": "MCP message broker + web dashboard for inter-agent communication. Let AI CLI agents talk to each other.",
5
5
  "main": "server.js",
6
6
  "bin": {
@@ -44,7 +44,7 @@
44
44
  "type": "git",
45
45
  "url": "git+https://github.com/Dekelelz/let-them-talk.git"
46
46
  },
47
- "homepage": "https://github.com/Dekelelz/let-them-talk",
47
+ "homepage": "https://talk.unrealai.studio",
48
48
  "bugs": {
49
49
  "url": "https://github.com/Dekelelz/let-them-talk/issues"
50
50
  },