let-them-talk 4.0.1 → 4.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/dashboard.js CHANGED
@@ -186,7 +186,22 @@ function apiHistory(query) {
186
186
  const histFile = branch && branch !== 'main'
187
187
  ? filePath(`branch-${branch}-history.jsonl`, projectPath)
188
188
  : filePath('history.jsonl', projectPath);
189
- const history = readJsonl(histFile);
189
+ let history = readJsonl(histFile);
190
+
191
+ // Merge channel-specific history files
192
+ const dataDir = resolveDataDir(projectPath);
193
+ try {
194
+ const files = fs.readdirSync(dataDir);
195
+ for (const f of files) {
196
+ if (f.startsWith('channel-') && f.endsWith('-history.jsonl') && f !== 'channel-general-history.jsonl') {
197
+ const channelHistory = readJsonl(path.join(dataDir, f));
198
+ history = history.concat(channelHistory);
199
+ }
200
+ }
201
+ } catch {}
202
+ // Sort merged messages by timestamp
203
+ history.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
204
+
190
205
  const acks = readJson(filePath('acks.json', projectPath));
191
206
  const limit = parseInt(query.get('limit') || '500', 10);
192
207
  const threadId = query.get('thread_id');
@@ -200,6 +215,29 @@ function apiHistory(query) {
200
215
  return messages;
201
216
  }
202
217
 
218
+ function apiChannels(query) {
219
+ const projectPath = query.get('project') || null;
220
+ const channelsFile = filePath('channels.json', projectPath);
221
+ const channels = readJson(channelsFile);
222
+ if (!channels) return { general: { description: 'General channel', members: ['*'], message_count: 0 } };
223
+ const dataDir = resolveDataDir(projectPath);
224
+ const result = {};
225
+ for (const [name, ch] of Object.entries(channels)) {
226
+ let msgCount = 0;
227
+ const msgFile = name === 'general'
228
+ ? filePath('history.jsonl', projectPath)
229
+ : path.join(dataDir, 'channel-' + name + '-history.jsonl');
230
+ try {
231
+ if (fs.existsSync(msgFile)) {
232
+ const content = fs.readFileSync(msgFile, 'utf8').trim();
233
+ if (content) msgCount = content.split('\n').length;
234
+ }
235
+ } catch {}
236
+ result[name] = { description: ch.description || '', members: ch.members, message_count: msgCount };
237
+ }
238
+ return result;
239
+ }
240
+
203
241
  function apiAgents(query) {
204
242
  const projectPath = query.get('project') || null;
205
243
  const agents = readJson(filePath('agents.json', projectPath));
@@ -236,6 +274,14 @@ function apiAgents(query) {
236
274
  bio: profile.bio || '',
237
275
  appearance: profile.appearance || {},
238
276
  };
277
+ // Include workspace status for agent intent board
278
+ try {
279
+ const wsPath = path.join(resolveDataDir(projectPath), 'workspaces', name + '.json');
280
+ if (fs.existsSync(wsPath)) {
281
+ const ws = JSON.parse(fs.readFileSync(wsPath, 'utf8'));
282
+ if (ws._status) result[name].current_status = ws._status;
283
+ }
284
+ } catch {}
239
285
  }
240
286
  return result;
241
287
  }
@@ -1388,15 +1434,44 @@ const server = http.createServer(async (req, res) => {
1388
1434
  if (libPath.includes('..') || libPath.includes('\\')) {
1389
1435
  res.writeHead(400); res.end('Bad path'); return;
1390
1436
  }
1391
- const filePath = path.join(__dirname, '..', 'node_modules', libPath);
1392
- if (fs.existsSync(filePath)) {
1437
+ // Search multiple node_modules locations (handles npx, local dev, monorepo, global)
1438
+ const searchPaths = [
1439
+ path.join(__dirname, 'node_modules', libPath), // inside package (nested deps)
1440
+ path.join(__dirname, '..', 'node_modules', libPath), // repo root (local dev)
1441
+ path.join(__dirname, '..', libPath), // npx sibling packages (three/ is next to let-them-talk/)
1442
+ ];
1443
+ // Also try require.resolve for robust npm path resolution (works with hoisted deps, npx cache, etc.)
1444
+ // Note: use require.resolve(pkg) not require.resolve(pkg/package.json) — modern packages
1445
+ // with "exports" fields block resolving package.json directly (ERR_PACKAGE_PATH_NOT_EXPORTED)
1446
+ try {
1447
+ const parts = libPath.split('/');
1448
+ const pkgName = parts[0];
1449
+ const subPath = parts.slice(1).join('/');
1450
+ // Try resolving the package's main entry, then navigate to subPath
1451
+ const resolved = require.resolve(pkgName);
1452
+ const pkgDir = path.dirname(resolved);
1453
+ // Walk up from the resolved entry to the package root (handle nested build/ dirs)
1454
+ let pkgRoot = pkgDir;
1455
+ while (pkgRoot !== path.dirname(pkgRoot)) {
1456
+ if (fs.existsSync(path.join(pkgRoot, 'package.json'))) {
1457
+ try {
1458
+ const pkg = JSON.parse(fs.readFileSync(path.join(pkgRoot, 'package.json'), 'utf8'));
1459
+ if (pkg.name === pkgName) break;
1460
+ } catch {}
1461
+ }
1462
+ pkgRoot = path.dirname(pkgRoot);
1463
+ }
1464
+ searchPaths.push(path.join(pkgRoot, subPath));
1465
+ } catch {}
1466
+ const filePath = searchPaths.find(p => fs.existsSync(p));
1467
+ if (filePath) {
1393
1468
  const ext = path.extname(filePath);
1394
1469
  const mimeTypes = { '.js': 'application/javascript', '.mjs': 'application/javascript', '.json': 'application/json', '.wasm': 'application/wasm' };
1395
1470
  const contentType = mimeTypes[ext] || 'application/octet-stream';
1396
1471
  res.writeHead(200, { 'Content-Type': contentType, 'Cache-Control': 'public, max-age=604800' });
1397
1472
  res.end(fs.readFileSync(filePath));
1398
1473
  } else {
1399
- res.writeHead(404); res.end('Not found');
1474
+ res.writeHead(404); res.end('Not found: ' + libPath);
1400
1475
  }
1401
1476
  return;
1402
1477
  }
@@ -1463,6 +1538,16 @@ const server = http.createServer(async (req, res) => {
1463
1538
  res.writeHead(200, { 'Content-Type': 'application/json' });
1464
1539
  res.end(JSON.stringify(apiAgents(url.searchParams)));
1465
1540
  }
1541
+ else if (url.pathname === '/api/channels' && req.method === 'GET') {
1542
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1543
+ res.end(JSON.stringify(apiChannels(url.searchParams)));
1544
+ }
1545
+ else if (url.pathname === '/api/decisions' && req.method === 'GET') {
1546
+ const projectPath = url.searchParams.get('project') || null;
1547
+ const decisions = readJson(filePath('decisions.json', projectPath));
1548
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1549
+ res.end(JSON.stringify(decisions || []));
1550
+ }
1466
1551
  else if (url.pathname === '/api/agents' && req.method === 'DELETE') {
1467
1552
  const body = await parseBody(req);
1468
1553
  if (!body.name) {
@@ -1543,6 +1628,80 @@ const server = http.createServer(async (req, res) => {
1543
1628
  res.writeHead(result.error ? 400 : 200, { 'Content-Type': 'application/json' });
1544
1629
  res.end(JSON.stringify(result));
1545
1630
  }
1631
+ else if (url.pathname === '/api/search' && req.method === 'GET') {
1632
+ const projectPath = url.searchParams.get('project') || null;
1633
+ const query = (url.searchParams.get('q') || '').trim();
1634
+ const from = url.searchParams.get('from') || null;
1635
+ const limit = Math.min(Math.max(1, parseInt(url.searchParams.get('limit') || '50', 10)), 100);
1636
+ if (query.length < 2) {
1637
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1638
+ res.end(JSON.stringify({ error: 'Query must be at least 2 characters' }));
1639
+ return;
1640
+ }
1641
+ // Search general history + all channel histories
1642
+ let allHistory = readJsonl(filePath('history.jsonl', projectPath));
1643
+ const dataDir = resolveDataDir(projectPath);
1644
+ try {
1645
+ const files = fs.readdirSync(dataDir);
1646
+ for (const f of files) {
1647
+ if (f.startsWith('channel-') && f.endsWith('-history.jsonl')) {
1648
+ allHistory = allHistory.concat(readJsonl(path.join(dataDir, f)));
1649
+ }
1650
+ }
1651
+ } catch {}
1652
+ const queryLower = query.toLowerCase();
1653
+ const results = [];
1654
+ for (let i = allHistory.length - 1; i >= 0 && results.length < limit; i--) {
1655
+ const m = allHistory[i];
1656
+ if (from && m.from !== from) continue;
1657
+ if (m.content && m.content.toLowerCase().includes(queryLower)) {
1658
+ results.push({
1659
+ id: m.id, from: m.from, to: m.to,
1660
+ preview: m.content.substring(0, 200),
1661
+ timestamp: m.timestamp,
1662
+ ...(m.channel && { channel: m.channel }),
1663
+ });
1664
+ }
1665
+ }
1666
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1667
+ res.end(JSON.stringify({ query, results_count: results.length, results }));
1668
+ }
1669
+ else if (url.pathname === '/api/export-json' && req.method === 'GET') {
1670
+ const projectPath = url.searchParams.get('project') || null;
1671
+ const history = apiHistory(url.searchParams);
1672
+ const agents = apiAgents(url.searchParams);
1673
+ const decisions = readJson(filePath('decisions.json', projectPath)) || [];
1674
+ const tasks = readJson(filePath('tasks.json', projectPath)) || [];
1675
+ const channels = apiChannels(url.searchParams);
1676
+ const pkg = readJson(path.join(__dirname, 'package.json')) || {};
1677
+ const result = {
1678
+ export_version: 1,
1679
+ exported_at: new Date().toISOString(),
1680
+ project: projectPath || process.cwd(),
1681
+ version: pkg.version || 'unknown',
1682
+ summary: {
1683
+ message_count: history.length,
1684
+ agent_count: Object.keys(agents).length,
1685
+ decision_count: decisions.length,
1686
+ task_count: tasks.length,
1687
+ channel_count: Object.keys(channels).length,
1688
+ time_range: history.length > 0 ? {
1689
+ start: history[0].timestamp,
1690
+ end: history[history.length - 1].timestamp,
1691
+ } : null,
1692
+ },
1693
+ agents,
1694
+ channels,
1695
+ decisions,
1696
+ tasks,
1697
+ messages: history,
1698
+ };
1699
+ res.writeHead(200, {
1700
+ 'Content-Type': 'application/json; charset=utf-8',
1701
+ 'Content-Disposition': 'attachment; filename="conversation-' + new Date().toISOString().slice(0, 10) + '-full.json"',
1702
+ });
1703
+ res.end(JSON.stringify(result, null, 2));
1704
+ }
1546
1705
  else if (url.pathname === '/api/export' && req.method === 'GET') {
1547
1706
  const html = apiExportHtml(url.searchParams);
1548
1707
  res.writeHead(200, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "let-them-talk",
3
- "version": "4.0.1",
3
+ "version": "4.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": {
@@ -53,6 +53,7 @@
53
53
  "author": "Dekelelz <contact@talk.unrealai.studio>",
54
54
  "license": "SEE LICENSE IN LICENSE",
55
55
  "dependencies": {
56
- "@modelcontextprotocol/sdk": "1.27.1"
56
+ "@modelcontextprotocol/sdk": "1.27.1",
57
+ "three": "0.170.0"
57
58
  }
58
59
  }