let-them-talk 3.1.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024-2026 Dekelelz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dashboard.html CHANGED
@@ -2810,7 +2810,7 @@ function renderAgents(agents) {
2810
2810
  }
2811
2811
 
2812
2812
  var avatarHtml = info.avatar
2813
- ? '<img class="agent-avatar-img" src="' + info.avatar + '" alt="' + escapeHtml(name) + '" onerror="this.style.display=\'none\'">'
2813
+ ? '<img class="agent-avatar-img" src="' + escapeHtml(info.avatar) + '" alt="' + escapeHtml(name) + '" onerror="this.style.display=\'none\'">'
2814
2814
  : '<div class="agent-avatar" style="background:' + color + '">' + initial(name) + '</div>';
2815
2815
  var displayName = info.display_name || name;
2816
2816
  var roleHtml = info.role ? '<span class="role-badge">' + escapeHtml(info.role) + '</span>' : '';
@@ -3061,6 +3061,8 @@ function renderMessages(messages) {
3061
3061
  if (m.acked) badges += '<span class="badge badge-ack">ACK</span>';
3062
3062
  if (m.thread_id) badges += '<span class="badge badge-thread">Thread</span>';
3063
3063
 
3064
+ var msgAvatarHtml = getMsgAvatar(m.from, color);
3065
+
3064
3066
  if (isSystem) {
3065
3067
  html += '<div class="message system-msg' + newClass + '">' +
3066
3068
  '<div class="msg-body">' +
@@ -3068,8 +3070,6 @@ function renderMessages(messages) {
3068
3070
  '</div></div>';
3069
3071
  lastFrom = '';
3070
3072
  lastTo = '';
3071
- var msgAvatarHtml = getMsgAvatar(m.from, color);
3072
-
3073
3073
  } else if (m.type === 'handoff') {
3074
3074
  html += '<div class="message handoff-msg' + newClass + '">' +
3075
3075
  buildMsgActions(m.id) +
@@ -3755,7 +3755,7 @@ textarea.addEventListener('input', function() {
3755
3755
  function getMsgAvatar(name, color) {
3756
3756
  var agent = cachedAgents[name];
3757
3757
  if (agent && agent.avatar) {
3758
- return '<img class="msg-avatar-img" src="' + agent.avatar + '" alt="' + escapeHtml(name) + '" onerror="this.outerHTML=\'<div class=&quot;msg-avatar&quot; style=&quot;background:' + color + '&quot;>' + initial(name) + '</div>\'">';
3758
+ return '<img class="msg-avatar-img" src="' + escapeHtml(agent.avatar) + '" alt="' + escapeHtml(name) + '" onerror="this.outerHTML=\'<div class=&quot;msg-avatar&quot; style=&quot;background:' + color + '&quot;>' + initial(name) + '</div>\'">';
3759
3759
  }
3760
3760
  return '<div class="msg-avatar" style="background:' + color + '">' + initial(name) + '</div>';
3761
3761
  }
@@ -4205,7 +4205,13 @@ function addProject() {
4205
4205
  if (res.success) {
4206
4206
  input.value = '';
4207
4207
  input.classList.remove('visible');
4208
+ // Auto-switch to the newly added project
4209
+ activeProject = res.project.path;
4208
4210
  loadProjects();
4211
+ setTimeout(function() {
4212
+ document.getElementById('project-select').value = activeProject;
4213
+ switchProject();
4214
+ }, 200);
4209
4215
  } else {
4210
4216
  alert(res.error || 'Failed to add project');
4211
4217
  }
@@ -4317,7 +4323,7 @@ function updateTypingIndicator(agents) {
4317
4323
  var info = agents[keys[i]];
4318
4324
  // Agent is alive, not listening, and was recently active (processing a message)
4319
4325
  if (info.alive && !info.is_listening && info.status === 'active') {
4320
- typing.push(info.display_name || keys[i]);
4326
+ typing.push(escapeHtml(info.display_name || keys[i]));
4321
4327
  }
4322
4328
  }
4323
4329
  var bar = document.getElementById('typing-bar');
@@ -4599,6 +4605,8 @@ function renderPhoneModalContent() {
4599
4605
  }
4600
4606
 
4601
4607
  var url = 'http://' + lanState.lan_ip + ':' + lanState.port;
4608
+ // Include active project so the phone shows the same view
4609
+ if (activeProject) url += '?project=' + encodeURIComponent(activeProject);
4602
4610
  var qrUrl = 'https://api.qrserver.com/v1/create-qr-code/?size=180x180&color=58a6ff&bgcolor=0d1117&data=' + encodeURIComponent(url);
4603
4611
 
4604
4612
  el.innerHTML =
@@ -4614,6 +4622,7 @@ function renderPhoneModalContent() {
4614
4622
 
4615
4623
  function copyPhoneUrl() {
4616
4624
  var url = 'http://' + lanState.lan_ip + ':' + lanState.port;
4625
+ if (activeProject) url += '?project=' + encodeURIComponent(activeProject);
4617
4626
  navigator.clipboard.writeText(url).then(function() {
4618
4627
  var btn = document.querySelector('.phone-url-copy');
4619
4628
  btn.textContent = 'Copied!';
@@ -4720,7 +4729,7 @@ function doLaunch() {
4720
4729
  }).then(function(r) { return r.json(); }).then(function(data) {
4721
4730
  if (data.error) {
4722
4731
  resultEl.className = 'launch-result error';
4723
- resultEl.innerHTML = data.error;
4732
+ resultEl.innerHTML = escapeHtml(data.error);
4724
4733
  return;
4725
4734
  }
4726
4735
  resultEl.className = 'launch-result success';
@@ -4786,6 +4795,16 @@ function initSSE() {
4786
4795
  }
4787
4796
 
4788
4797
  // ==================== INIT ====================
4798
+
4799
+ // Auto-select project from URL param (for phone access links)
4800
+ (function() {
4801
+ var params = new URLSearchParams(window.location.search);
4802
+ var projectFromUrl = params.get('project');
4803
+ if (projectFromUrl) {
4804
+ activeProject = projectFromUrl;
4805
+ }
4806
+ })();
4807
+
4789
4808
  loadProjects();
4790
4809
  poll();
4791
4810
  initSSE();
package/dashboard.js CHANGED
@@ -3,19 +3,24 @@ const http = require('http');
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const os = require('os');
6
- const { exec } = require('child_process');
6
+ const { exec, spawn } = require('child_process');
7
7
 
8
8
  const PORT = parseInt(process.env.AGENT_BRIDGE_PORT || '3000', 10);
9
9
  let LAN_MODE = process.env.AGENT_BRIDGE_LAN === 'true';
10
10
 
11
11
  function getLanIP() {
12
12
  const interfaces = os.networkInterfaces();
13
+ let fallback = null;
13
14
  for (const name of Object.keys(interfaces)) {
14
15
  for (const iface of interfaces[name]) {
15
- if (iface.family === 'IPv4' && !iface.internal) return iface.address;
16
+ if (iface.family === 'IPv4' && !iface.internal) {
17
+ // Prefer real LAN IPs (192.168.x, 10.x, 172.16-31.x) over link-local (169.254.x)
18
+ if (!iface.address.startsWith('169.254.')) return iface.address;
19
+ if (!fallback) fallback = iface.address;
20
+ }
16
21
  }
17
22
  }
18
- return null;
23
+ return fallback;
19
24
  }
20
25
  const DEFAULT_DATA_DIR = process.env.AGENT_BRIDGE_DATA || path.join(process.cwd(), '.agent-bridge');
21
26
  const HTML_FILE = path.join(__dirname, 'dashboard.html');
@@ -134,6 +139,9 @@ function getDefaultAvatar(name) {
134
139
  function apiHistory(query) {
135
140
  const projectPath = query.get('project') || null;
136
141
  const branch = query.get('branch') || null;
142
+ if (branch && !/^[a-zA-Z0-9_-]{1,64}$/.test(branch)) {
143
+ return { error: 'Invalid branch name' };
144
+ }
137
145
  const histFile = branch && branch !== 'main'
138
146
  ? filePath(`branch-${branch}-history.jsonl`, projectPath)
139
147
  : filePath('history.jsonl', projectPath);
@@ -306,6 +314,14 @@ function apiAddProject(body) {
306
314
  const name = body.name || path.basename(absPath);
307
315
  if (projects.find(p => p.path === absPath)) return { error: 'Project already added' };
308
316
 
317
+ // Create .agent-bridge directory if it doesn't exist
318
+ const abDir = path.join(absPath, '.agent-bridge');
319
+ if (!fs.existsSync(abDir)) fs.mkdirSync(abDir, { recursive: true });
320
+
321
+ // Set up MCP config so agents can use it
322
+ const serverPath = path.join(__dirname, 'server.js').replace(/\\/g, '/');
323
+ ensureMCPConfig('claude', serverPath, absPath);
324
+
309
325
  projects.push({ name, path: absPath, added_at: new Date().toISOString() });
310
326
  saveProjects(projects);
311
327
  return { success: true, project: { name, path: absPath } };
@@ -602,8 +618,7 @@ function apiLaunchAgent(body) {
602
618
 
603
619
  // Try to launch terminal on Windows
604
620
  if (process.platform === 'win32') {
605
- const escapedDir = projectDir.replace(/"/g, '\\"');
606
- exec(`start cmd /k "cd /d "${escapedDir}" && ${cliCmd}"`, { cwd: projectDir });
621
+ spawn('cmd', ['/c', 'start', 'cmd', '/k', `cd /d "${projectDir}" && ${cliCmd}`], { cwd: projectDir, shell: false, detached: true, stdio: 'ignore' });
607
622
  return { success: true, launched: true, cli, project_dir: projectDir, prompt: launchPrompt };
608
623
  }
609
624
 
@@ -649,7 +664,7 @@ const server = http.createServer(async (req, res) => {
649
664
  const allowedOrigin = `http://localhost:${PORT}`;
650
665
  const reqOrigin = req.headers.origin;
651
666
  if (LAN_MODE && reqOrigin) {
652
- res.setHeader('Access-Control-Allow-Origin', reqOrigin);
667
+ res.setHeader('Access-Control-Allow-Origin', '*');
653
668
  } else if (reqOrigin === allowedOrigin || reqOrigin === `http://127.0.0.1:${PORT}`) {
654
669
  res.setHeader('Access-Control-Allow-Origin', reqOrigin);
655
670
  }
@@ -781,6 +796,11 @@ const server = http.createServer(async (req, res) => {
781
796
  else if (url.pathname === '/api/workspaces' && req.method === 'GET') {
782
797
  const projectPath = url.searchParams.get('project') || null;
783
798
  const agentParam = url.searchParams.get('agent');
799
+ if (agentParam && !/^[a-zA-Z0-9]{1,20}$/.test(agentParam)) {
800
+ res.writeHead(400, { 'Content-Type': 'application/json' });
801
+ res.end(JSON.stringify({ error: 'Invalid agent name' }));
802
+ return;
803
+ }
784
804
  const dataDir = resolveDataDir(projectPath);
785
805
  const wsDir = path.join(dataDir, 'workspaces');
786
806
  const result = {};
package/logo.png ADDED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "let-them-talk",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
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": {
@@ -9,14 +9,20 @@
9
9
  },
10
10
  "scripts": {
11
11
  "start": "node server.js",
12
- "dashboard": "node dashboard.js"
12
+ "dashboard": "node dashboard.js",
13
+ "test": "echo \"No tests yet\""
14
+ },
15
+ "engines": {
16
+ "node": ">=16.0.0"
13
17
  },
14
18
  "files": [
15
19
  "server.js",
16
20
  "dashboard.js",
17
21
  "dashboard.html",
18
22
  "cli.js",
19
- "templates/"
23
+ "templates/",
24
+ "logo.png",
25
+ "LICENSE"
20
26
  ],
21
27
  "keywords": [
22
28
  "mcp",
@@ -33,7 +39,7 @@
33
39
  ],
34
40
  "repository": {
35
41
  "type": "git",
36
- "url": "https://github.com/Dekelelz/let-them-talk.git"
42
+ "url": "git+https://github.com/Dekelelz/let-them-talk.git"
37
43
  },
38
44
  "homepage": "https://github.com/Dekelelz/let-them-talk",
39
45
  "bugs": {
@@ -42,6 +48,6 @@
42
48
  "author": "Dekelelz",
43
49
  "license": "MIT",
44
50
  "dependencies": {
45
- "@modelcontextprotocol/sdk": "^1.0.0"
51
+ "@modelcontextprotocol/sdk": "1.12.1"
46
52
  }
47
53
  }