let-them-talk 4.0.2 → 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,11 +1434,35 @@ 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
- // Search multiple node_modules locations (handles npx, local dev, monorepo)
1437
+ // Search multiple node_modules locations (handles npx, local dev, monorepo, global)
1392
1438
  const searchPaths = [
1393
- path.join(__dirname, 'node_modules', libPath), // inside agent-bridge (npx installs deps here)
1439
+ path.join(__dirname, 'node_modules', libPath), // inside package (nested deps)
1394
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/)
1395
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 {}
1396
1466
  const filePath = searchPaths.find(p => fs.existsSync(p));
1397
1467
  if (filePath) {
1398
1468
  const ext = path.extname(filePath);
@@ -1468,6 +1538,16 @@ const server = http.createServer(async (req, res) => {
1468
1538
  res.writeHead(200, { 'Content-Type': 'application/json' });
1469
1539
  res.end(JSON.stringify(apiAgents(url.searchParams)));
1470
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
+ }
1471
1551
  else if (url.pathname === '/api/agents' && req.method === 'DELETE') {
1472
1552
  const body = await parseBody(req);
1473
1553
  if (!body.name) {
@@ -1548,6 +1628,80 @@ const server = http.createServer(async (req, res) => {
1548
1628
  res.writeHead(result.error ? 400 : 200, { 'Content-Type': 'application/json' });
1549
1629
  res.end(JSON.stringify(result));
1550
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
+ }
1551
1705
  else if (url.pathname === '/api/export' && req.method === 'GET') {
1552
1706
  const html = apiExportHtml(url.searchParams);
1553
1707
  res.writeHead(200, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "let-them-talk",
3
- "version": "4.0.2",
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": {