let-them-talk 3.2.0 → 3.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli.js CHANGED
@@ -8,7 +8,7 @@ const command = process.argv[2];
8
8
 
9
9
  function printUsage() {
10
10
  console.log(`
11
- Let Them Talk — Agent Bridge v3.0.0
11
+ Let Them Talk — Agent Bridge v3.2.0
12
12
  MCP message broker for inter-agent communication
13
13
  Supports: Claude Code, Gemini CLI, Codex CLI
14
14
 
@@ -232,13 +232,9 @@ function reset() {
232
232
  console.log(' No data directory found. Nothing to reset.');
233
233
  return;
234
234
  }
235
- const files = fs.readdirSync(targetDir);
236
- let count = 0;
237
- for (const f of files) {
238
- fs.unlinkSync(path.join(targetDir, f));
239
- count++;
240
- }
241
- console.log(` Cleared ${count} file(s) from ${targetDir}`);
235
+ fs.rmSync(targetDir, { recursive: true, force: true });
236
+ fs.mkdirSync(targetDir, { recursive: true });
237
+ console.log(` Cleared all data from ${targetDir}`);
242
238
  }
243
239
 
244
240
  function getTemplates() {
package/dashboard.html CHANGED
@@ -2471,7 +2471,7 @@
2471
2471
  </div>
2472
2472
  </div>
2473
2473
  <div class="app-footer">
2474
- <span>Let Them Talk v3.0.0</span>
2474
+ <span>Let Them Talk v3.2.0</span>
2475
2475
  </div>
2476
2476
  <div class="profile-popup" id="profile-popup" onclick="event.stopPropagation()">
2477
2477
  <div class="profile-popup-header">
package/dashboard.js CHANGED
@@ -3,7 +3,7 @@ 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, spawn } = require('child_process');
6
+ const { 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';
@@ -262,7 +262,7 @@ function apiInjectMessage(body, query) {
262
262
  }
263
263
 
264
264
  if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true });
265
- const fromName = body.from || 'Dashboard';
265
+ const fromName = 'Dashboard';
266
266
  const now = new Date().toISOString();
267
267
 
268
268
  // Broadcast to all agents
@@ -310,6 +310,16 @@ function apiAddProject(body) {
310
310
  const absPath = path.resolve(body.path);
311
311
  if (!fs.existsSync(absPath)) return { error: `Path does not exist: ${absPath}` };
312
312
 
313
+ // Restrict to paths under cwd or paths that look like project directories
314
+ const cwd = path.resolve(process.cwd());
315
+ if (!absPath.startsWith(cwd + path.sep) && absPath !== cwd) {
316
+ const hasProject = fs.existsSync(path.join(absPath, 'package.json')) ||
317
+ fs.existsSync(path.join(absPath, '.git'));
318
+ if (!hasProject) {
319
+ return { error: 'Path must be a project directory (with package.json or .git)' };
320
+ }
321
+ }
322
+
313
323
  const projects = getProjects();
314
324
  const name = body.name || path.basename(absPath);
315
325
  if (projects.find(p => p.path === absPath)) return { error: 'Project already added' };
@@ -416,7 +426,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;backgrou
416
426
  <script>
417
427
  var COLORS=['#58a6ff','#3fb950','#d29922','#f85149','#bc8cff','#f778ba','#79c0ff','#7ee787','#e3b341','#ffa198'];
418
428
  var colorMap={},ci=0;
419
- var data=${JSON.stringify(history)};
429
+ var data=${JSON.stringify(history).replace(/<\//g, '<\\/')};
420
430
  function esc(t){var d=document.createElement('div');d.textContent=t;return d.innerHTML}
421
431
  function fmt(t){
422
432
  var h=esc(t);
@@ -603,6 +613,9 @@ function apiLaunchAgent(body) {
603
613
  if (!cli || !['claude', 'gemini', 'codex'].includes(cli)) {
604
614
  return { error: 'Invalid cli type. Must be: claude, gemini, or codex' };
605
615
  }
616
+ if (project_dir && !validateProjectPath(project_dir)) {
617
+ return { error: 'Project directory not registered. Add it via the dashboard first.' };
618
+ }
606
619
  const projectDir = project_dir || process.cwd();
607
620
  if (!fs.existsSync(projectDir)) {
608
621
  return { error: 'Project directory does not exist: ' + projectDir };
@@ -618,7 +631,7 @@ function apiLaunchAgent(body) {
618
631
 
619
632
  // Try to launch terminal on Windows
620
633
  if (process.platform === 'win32') {
621
- spawn('cmd', ['/c', 'start', 'cmd', '/k', `cd /d "${projectDir}" && ${cliCmd}`], { cwd: projectDir, shell: false, detached: true, stdio: 'ignore' });
634
+ spawn('cmd', ['/c', 'start', 'cmd', '/k', cliCmd], { cwd: projectDir, shell: false, detached: true, stdio: 'ignore' });
622
635
  return { success: true, launched: true, cli, project_dir: projectDir, prompt: launchPrompt };
623
636
  }
624
637
 
@@ -677,6 +690,20 @@ const server = http.createServer(async (req, res) => {
677
690
  return;
678
691
  }
679
692
 
693
+ // CSRF protection: validate origin on mutating requests
694
+ if (req.method === 'POST' || req.method === 'DELETE') {
695
+ const origin = req.headers.origin || '';
696
+ const referer = req.headers.referer || '';
697
+ const source = origin || referer;
698
+ const isLocal = !source || source.includes('localhost:' + PORT) || source.includes('127.0.0.1:' + PORT);
699
+ const isLan = LAN_MODE && getLanIP() && source.includes(getLanIP() + ':' + PORT);
700
+ if (!isLocal && !isLan) {
701
+ res.writeHead(403, { 'Content-Type': 'application/json' });
702
+ res.end(JSON.stringify({ error: 'Forbidden: invalid origin' }));
703
+ return;
704
+ }
705
+ }
706
+
680
707
  try {
681
708
  // Validate project parameter on all API endpoints
682
709
  const projectParam = url.searchParams.get('project');
@@ -953,6 +980,11 @@ const server = http.createServer(async (req, res) => {
953
980
  }
954
981
  // Server-Sent Events endpoint for real-time updates
955
982
  else if (url.pathname === '/api/events' && req.method === 'GET') {
983
+ if (sseClients.size >= 100) {
984
+ res.writeHead(503, { 'Content-Type': 'application/json' });
985
+ res.end(JSON.stringify({ error: 'Too many SSE connections' }));
986
+ return;
987
+ }
956
988
  res.writeHead(200, {
957
989
  'Content-Type': 'text/event-stream',
958
990
  'Cache-Control': 'no-cache',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "let-them-talk",
3
- "version": "3.2.0",
3
+ "version": "3.2.2",
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": {
@@ -48,6 +48,6 @@
48
48
  "author": "Dekelelz",
49
49
  "license": "MIT",
50
50
  "dependencies": {
51
- "@modelcontextprotocol/sdk": "1.12.1"
51
+ "@modelcontextprotocol/sdk": "1.27.1"
52
52
  }
53
53
  }
package/server.js CHANGED
@@ -436,7 +436,6 @@ function toolListAgents() {
436
436
  const idleSeconds = Math.floor((Date.now() - new Date(lastActivity).getTime()) / 1000);
437
437
  const profile = profiles[name] || {};
438
438
  result[name] = {
439
- pid: info.pid,
440
439
  alive,
441
440
  registered_at: info.timestamp,
442
441
  last_activity: lastActivity,
@@ -568,6 +567,7 @@ async function toolWaitForReply(timeoutSeconds = 300, from = null) {
568
567
  if (!registeredName) {
569
568
  return { error: 'You must call register() first' };
570
569
  }
570
+ timeoutSeconds = Math.min(Math.max(1, timeoutSeconds || 300), 3600);
571
571
 
572
572
  setListening(true);
573
573
 
@@ -646,6 +646,12 @@ function toolAckMessage(messageId) {
646
646
  return { error: 'You must call register() first' };
647
647
  }
648
648
 
649
+ const history = readJsonl(getHistoryFile(currentBranch));
650
+ const msg = history.find(m => m.id === messageId);
651
+ if (msg && msg.to !== registeredName) {
652
+ return { error: 'Can only acknowledge messages addressed to you' };
653
+ }
654
+
649
655
  const acks = getAcks();
650
656
  acks[messageId] = {
651
657
  acked_by: registeredName,
@@ -773,6 +779,7 @@ async function toolListenCodex(from = null) {
773
779
  }
774
780
 
775
781
  function toolGetHistory(limit = 50, thread_id = null) {
782
+ limit = Math.min(Math.max(1, limit || 50), 500);
776
783
  let history = readJsonl(getHistoryFile(currentBranch));
777
784
  if (thread_id) {
778
785
  history = history.filter(m => m.thread_id === thread_id || m.id === thread_id);
@@ -840,17 +847,16 @@ function toolShareFile(filePath, to = null, summary = null) {
840
847
  return { error: 'You must call register() first' };
841
848
  }
842
849
 
843
- // Resolve the file path — restrict to project directory
850
+ // Resolve the file path — restrict to project directory (follow symlinks)
844
851
  const resolved = path.resolve(filePath);
845
852
  const allowedRoot = path.resolve(process.cwd());
846
- if (!resolved.startsWith(allowedRoot + path.sep) && resolved !== allowedRoot) {
853
+ let realPath;
854
+ try { realPath = fs.realpathSync(resolved); } catch { return { error: 'File not found' }; }
855
+ if (!realPath.startsWith(allowedRoot + path.sep) && realPath !== allowedRoot) {
847
856
  return { error: 'File path must be within the project directory' };
848
857
  }
849
- if (!fs.existsSync(resolved)) {
850
- return { error: `File not found: ${path.basename(resolved)}` };
851
- }
852
858
 
853
- const stat = fs.statSync(resolved);
859
+ const stat = fs.statSync(realPath);
854
860
  if (stat.size > 100000) {
855
861
  return { error: `File too large (${Math.round(stat.size / 1024)}KB). Maximum 100KB for sharing.` };
856
862
  }
@@ -872,8 +878,8 @@ function toolShareFile(filePath, to = null, summary = null) {
872
878
  return { error: `Agent "${to}" is not registered` };
873
879
  }
874
880
 
875
- const fileContent = fs.readFileSync(resolved, 'utf8');
876
- const fileName = path.basename(resolved);
881
+ const fileContent = fs.readFileSync(realPath, 'utf8');
882
+ const fileName = path.basename(realPath);
877
883
 
878
884
  messageSeq++;
879
885
  const content = summary
@@ -1006,6 +1012,7 @@ function toolListTasks(status = null, assignee = null) {
1006
1012
  }
1007
1013
 
1008
1014
  function toolGetSummary(lastN = 20) {
1015
+ lastN = Math.min(Math.max(1, lastN || 20), 500);
1009
1016
  const history = readJsonl(getHistoryFile(currentBranch));
1010
1017
  if (history.length === 0) {
1011
1018
  return { summary: 'No messages in conversation yet.', message_count: 0 };
@@ -1137,8 +1144,8 @@ function toolWorkspaceWrite(key, content) {
1137
1144
  }
1138
1145
 
1139
1146
  function toolWorkspaceRead(key, agent) {
1147
+ if (!registeredName) return { error: 'You must call register() first' };
1140
1148
  const targetAgent = agent || registeredName;
1141
- if (!targetAgent) return { error: 'Specify agent or register first' };
1142
1149
 
1143
1150
  const ws = getWorkspace(targetAgent);
1144
1151
  if (key) {
@@ -1970,7 +1977,7 @@ async function main() {
1970
1977
  loadPlugins();
1971
1978
  const transport = new StdioServerTransport();
1972
1979
  await server.connect(transport);
1973
- console.error('Agent Bridge MCP server v3.0.0 running (' + (17 + loadedPlugins.length) + ' tools)');
1980
+ console.error('Agent Bridge MCP server v3.2.0 running (' + (27 + loadedPlugins.length) + ' tools)');
1974
1981
  }
1975
1982
 
1976
1983
  main().catch(console.error);