let-them-talk 3.4.2 → 3.4.4
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/CHANGELOG.md +19 -0
- package/LICENSE +1 -1
- package/README.md +7 -38
- package/cli.js +13 -127
- package/dashboard.html +2 -105
- package/dashboard.js +3 -32
- package/package.json +1 -1
- package/server.js +5 -108
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.4.4] - 2026-03-15
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- Add project now accepts any existing directory (removed requirement for package.json or .git)
|
|
7
|
+
- Init safely backs up corrupted .mcp.json and settings.json before overwriting
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- Removed plugin references from website and docs
|
|
11
|
+
- Website updated with security features (LAN auth token, CSRF, CSP)
|
|
12
|
+
|
|
13
|
+
## [3.4.3] - 2026-03-15
|
|
14
|
+
|
|
15
|
+
### Removed — Plugin System
|
|
16
|
+
- Removed the entire plugin system (`vm.runInNewContext` sandbox, plugin CLI commands, dashboard plugin UI)
|
|
17
|
+
- **Why:** Plugins were an unnecessary attack surface. Node.js `vm` is not a security sandbox — plugins could escape and execute arbitrary OS commands. CLI terminals (Claude Code, Gemini, Codex) have their own extension systems, making our plugins redundant.
|
|
18
|
+
- `npx let-them-talk plugin` now shows a deprecation notice
|
|
19
|
+
- MCP tools reduced from 27 + plugins to 27 (all core tools remain)
|
|
20
|
+
- ~200 lines of code removed from server.js, cli.js, dashboard.js, dashboard.html
|
|
21
|
+
|
|
3
22
|
## [3.4.2] - 2026-03-15
|
|
4
23
|
|
|
5
24
|
### Security — CSRF Protection
|
package/LICENSE
CHANGED
|
@@ -6,7 +6,7 @@ License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved.
|
|
|
6
6
|
Parameters
|
|
7
7
|
|
|
8
8
|
Licensor: Dekelelz
|
|
9
|
-
Licensed Work: Let Them Talk v3.4.
|
|
9
|
+
Licensed Work: Let Them Talk v3.4.4
|
|
10
10
|
The Licensed Work is (c) 2024-2026 Dekelelz.
|
|
11
11
|
Additional Use Grant: You may make use of the Licensed Work, provided that
|
|
12
12
|
you may not use the Licensed Work for a Commercial
|
package/README.md
CHANGED
|
@@ -72,7 +72,7 @@ Run `npx let-them-talk init --all` to configure all three at once.
|
|
|
72
72
|
| | |
|
|
73
73
|
+----------- .agent-bridge/ directory ----------+
|
|
74
74
|
messages · agents · tasks
|
|
75
|
-
profiles · workflows ·
|
|
75
|
+
profiles · workflows · permissions
|
|
76
76
|
|
|
|
77
77
|
v
|
|
78
78
|
Web Dashboard :3000
|
|
@@ -84,7 +84,7 @@ Each terminal spawns its own MCP server process. All processes share a `.agent-b
|
|
|
84
84
|
|
|
85
85
|
## Highlights
|
|
86
86
|
|
|
87
|
-
- **27 MCP tools** — messaging, tasks, workflows, profiles, workspaces, branching
|
|
87
|
+
- **27 MCP tools** — messaging, tasks, workflows, profiles, workspaces, branching
|
|
88
88
|
- **Premium dashboard** — glassmorphism UI, Inter font, gradient accents, SSE real-time (~200ms)
|
|
89
89
|
- **Stats & analytics** — per-agent message counts, response times, hourly activity charts, velocity
|
|
90
90
|
- **Conversation templates** — 4 built-in multi-agent workflows (Code Review, Debug Squad, Feature Dev, Research & Write)
|
|
@@ -96,7 +96,7 @@ Each terminal spawns its own MCP server process. All processes share a `.agent-b
|
|
|
96
96
|
- **Compact view** — dense message toggle for power users, persists to localStorage
|
|
97
97
|
- **Multi-format export** — HTML, Markdown, and JSON export
|
|
98
98
|
- **CLI tools** — send messages and check status directly from the command line
|
|
99
|
-
- **
|
|
99
|
+
- **Secure by default** — CSRF protection, LAN auth tokens, Content Security Policy, agent permissions
|
|
100
100
|
- **Zero config** — one `npx` command, auto-detects your CLI, works immediately
|
|
101
101
|
|
|
102
102
|
## Agent Templates
|
|
@@ -147,7 +147,7 @@ Launch with `npx let-them-talk dashboard` — opens at `http://localhost:3000`.
|
|
|
147
147
|
- Browser notifications and sound alerts
|
|
148
148
|
- LAN mode for phone access
|
|
149
149
|
|
|
150
|
-
## MCP Tools (27
|
|
150
|
+
## MCP Tools (27)
|
|
151
151
|
|
|
152
152
|
<details>
|
|
153
153
|
<summary><strong>Messaging (13 tools)</strong></summary>
|
|
@@ -207,38 +207,6 @@ Launch with `npx let-them-talk dashboard` — opens at `http://localhost:3000`.
|
|
|
207
207
|
|
|
208
208
|
</details>
|
|
209
209
|
|
|
210
|
-
## Plugins
|
|
211
|
-
|
|
212
|
-
Extend Let Them Talk with custom tools. Drop a `.js` file in `.agent-bridge/plugins/`.
|
|
213
|
-
|
|
214
|
-
```javascript
|
|
215
|
-
module.exports = {
|
|
216
|
-
name: 'my-tool',
|
|
217
|
-
description: 'What this tool does',
|
|
218
|
-
inputSchema: {
|
|
219
|
-
type: 'object',
|
|
220
|
-
properties: {
|
|
221
|
-
query: { type: 'string', description: 'Input text' }
|
|
222
|
-
},
|
|
223
|
-
required: ['query']
|
|
224
|
-
},
|
|
225
|
-
handler(args, ctx) {
|
|
226
|
-
// ctx: sendMessage, getAgents, getHistory, readFile, registeredName, dataDir
|
|
227
|
-
return { result: 'done', query: args.query };
|
|
228
|
-
}
|
|
229
|
-
};
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
```bash
|
|
233
|
-
npx let-them-talk plugin add my-tool.js # install
|
|
234
|
-
npx let-them-talk plugin list # list installed
|
|
235
|
-
npx let-them-talk plugin remove my-tool # remove
|
|
236
|
-
npx let-them-talk plugin enable my-tool # enable
|
|
237
|
-
npx let-them-talk plugin disable my-tool # disable
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
Plugins run sandboxed with a 30-second timeout. Manage via CLI or dashboard.
|
|
241
|
-
|
|
242
210
|
## CLI Reference
|
|
243
211
|
|
|
244
212
|
```bash
|
|
@@ -248,7 +216,8 @@ npx let-them-talk init --template <name> # use a team template
|
|
|
248
216
|
npx let-them-talk templates # list templates
|
|
249
217
|
npx let-them-talk dashboard # launch web dashboard
|
|
250
218
|
npx let-them-talk reset # clear conversation data
|
|
251
|
-
npx let-them-talk
|
|
219
|
+
npx let-them-talk msg <agent> <text> # send a message from CLI
|
|
220
|
+
npx let-them-talk status # show active agents
|
|
252
221
|
npx let-them-talk help # show help
|
|
253
222
|
```
|
|
254
223
|
|
|
@@ -268,7 +237,7 @@ Let Them Talk is a **local message broker**. It passes text messages between CLI
|
|
|
268
237
|
|
|
269
238
|
**Does not:** access the internet, store API keys, run cloud services, or grant new filesystem access.
|
|
270
239
|
|
|
271
|
-
**Built-in protections:** CORS restriction, XSS prevention, path traversal protection, symlink validation, origin enforcement, SSE connection limits, input validation, message size limits (1MB),
|
|
240
|
+
**Built-in protections:** CSRF custom header, LAN auth tokens, Content Security Policy, CORS restriction, XSS prevention, path traversal protection, symlink validation, origin enforcement, SSE connection limits, input validation, message size limits (1MB), agent permissions.
|
|
272
241
|
|
|
273
242
|
**LAN mode:** Optional phone access exposes the dashboard to your local WiFi only. Requires explicit activation.
|
|
274
243
|
|
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.4.
|
|
11
|
+
Let Them Talk — Agent Bridge v3.4.4
|
|
12
12
|
MCP message broker for inter-agent communication
|
|
13
13
|
Supports: Claude Code, Gemini CLI, Codex CLI
|
|
14
14
|
|
|
@@ -23,11 +23,6 @@ function printUsage() {
|
|
|
23
23
|
npx let-them-talk dashboard Launch the web dashboard (http://localhost:3000)
|
|
24
24
|
npx let-them-talk dashboard --lan Launch dashboard accessible on LAN (phone/tablet)
|
|
25
25
|
npx let-them-talk reset Clear all conversation data
|
|
26
|
-
npx let-them-talk plugin list List installed plugins
|
|
27
|
-
npx let-them-talk plugin add <file> Install a plugin from a .js file
|
|
28
|
-
npx let-them-talk plugin remove <n> Remove a plugin by name
|
|
29
|
-
npx let-them-talk plugin enable <n> Enable a plugin
|
|
30
|
-
npx let-them-talk plugin disable <n> Disable a plugin
|
|
31
26
|
npx let-them-talk msg <agent> <text> Send a message to an agent
|
|
32
27
|
npx let-them-talk status Show active agents and message count
|
|
33
28
|
npx let-them-talk help Show this help message
|
|
@@ -70,7 +65,12 @@ function setupClaude(serverPath, cwd) {
|
|
|
70
65
|
try {
|
|
71
66
|
mcpConfig = JSON.parse(fs.readFileSync(mcpConfigPath, 'utf8'));
|
|
72
67
|
if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
|
|
73
|
-
} catch {
|
|
68
|
+
} catch {
|
|
69
|
+
// Backup corrupted file before overwriting
|
|
70
|
+
const backup = mcpConfigPath + '.backup';
|
|
71
|
+
fs.copyFileSync(mcpConfigPath, backup);
|
|
72
|
+
console.log(' [warn] Existing .mcp.json was invalid — backed up to .mcp.json.backup');
|
|
73
|
+
}
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
mcpConfig.mcpServers['agent-bridge'] = {
|
|
@@ -98,7 +98,11 @@ function setupGemini(serverPath, cwd) {
|
|
|
98
98
|
try {
|
|
99
99
|
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
100
100
|
if (!settings.mcpServers) settings.mcpServers = {};
|
|
101
|
-
} catch {
|
|
101
|
+
} catch {
|
|
102
|
+
const backup = settingsPath + '.backup';
|
|
103
|
+
fs.copyFileSync(settingsPath, backup);
|
|
104
|
+
console.log(' [warn] Existing settings.json was invalid — backed up to settings.json.backup');
|
|
105
|
+
}
|
|
102
106
|
}
|
|
103
107
|
|
|
104
108
|
settings.mcpServers['agent-bridge'] = {
|
|
@@ -293,124 +297,6 @@ function showTemplate(templateName) {
|
|
|
293
297
|
}
|
|
294
298
|
}
|
|
295
299
|
|
|
296
|
-
function pluginCmd() {
|
|
297
|
-
const subCmd = process.argv[3];
|
|
298
|
-
const dataDir = process.env.AGENT_BRIDGE_DATA_DIR || path.join(process.cwd(), '.agent-bridge');
|
|
299
|
-
const pluginsDir = path.join(dataDir, 'plugins');
|
|
300
|
-
const pluginsFile = path.join(dataDir, 'plugins.json');
|
|
301
|
-
|
|
302
|
-
function getRegistry() {
|
|
303
|
-
if (!fs.existsSync(pluginsFile)) return [];
|
|
304
|
-
try { return JSON.parse(fs.readFileSync(pluginsFile, 'utf8')); } catch { return []; }
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
function saveRegistry(reg) {
|
|
308
|
-
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true });
|
|
309
|
-
fs.writeFileSync(pluginsFile, JSON.stringify(reg, null, 2));
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
switch (subCmd) {
|
|
313
|
-
case 'list': {
|
|
314
|
-
const plugins = getRegistry();
|
|
315
|
-
if (!plugins.length) {
|
|
316
|
-
console.log(' No plugins installed.');
|
|
317
|
-
console.log(' Install with: npx let-them-talk plugin add <file.js>');
|
|
318
|
-
return;
|
|
319
|
-
}
|
|
320
|
-
console.log('');
|
|
321
|
-
console.log(' Installed Plugins');
|
|
322
|
-
console.log(' =================');
|
|
323
|
-
for (const p of plugins) {
|
|
324
|
-
const status = p.enabled !== false ? 'enabled' : 'disabled';
|
|
325
|
-
console.log(' ' + p.name.padEnd(20) + ' ' + status.padEnd(10) + ' ' + (p.description || ''));
|
|
326
|
-
}
|
|
327
|
-
console.log('');
|
|
328
|
-
break;
|
|
329
|
-
}
|
|
330
|
-
case 'add': {
|
|
331
|
-
const filePath = process.argv[4];
|
|
332
|
-
if (!filePath) { console.error(' Usage: npx let-them-talk plugin add <file.js>'); process.exit(1); }
|
|
333
|
-
const absPath = path.resolve(filePath);
|
|
334
|
-
if (!fs.existsSync(absPath)) { console.error(' File not found: ' + absPath); process.exit(1); }
|
|
335
|
-
|
|
336
|
-
// Validate plugin structure without executing it (no require — prevents RCE on install)
|
|
337
|
-
try {
|
|
338
|
-
const src = fs.readFileSync(absPath, 'utf8');
|
|
339
|
-
if (!src.includes('module.exports') || !src.includes('name') || !src.includes('handler')) {
|
|
340
|
-
console.error(' Plugin must export name, description, and handler (module.exports = { name, handler })');
|
|
341
|
-
process.exit(1);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// Extract plugin name from source using regex (no eval)
|
|
345
|
-
const nameMatch = src.match(/name\s*:\s*['"]([^'"]+)['"]/);
|
|
346
|
-
const descMatch = src.match(/description\s*:\s*['"]([^'"]+)['"]/);
|
|
347
|
-
const pluginName = nameMatch ? nameMatch[1] : path.basename(absPath, '.js');
|
|
348
|
-
const pluginDesc = descMatch ? descMatch[1] : '';
|
|
349
|
-
|
|
350
|
-
if (!fs.existsSync(pluginsDir)) fs.mkdirSync(pluginsDir, { recursive: true });
|
|
351
|
-
const destFile = path.join(pluginsDir, path.basename(absPath));
|
|
352
|
-
fs.copyFileSync(absPath, destFile);
|
|
353
|
-
|
|
354
|
-
const reg = getRegistry();
|
|
355
|
-
if (!reg.find(p => p.name === pluginName)) {
|
|
356
|
-
reg.push({ name: pluginName, description: pluginDesc, file: path.basename(absPath), enabled: true, added_at: new Date().toISOString() });
|
|
357
|
-
saveRegistry(reg);
|
|
358
|
-
}
|
|
359
|
-
console.log(' Plugin "' + pluginName + '" installed successfully.');
|
|
360
|
-
console.log(' Restart CLI to load the new tool (runs sandboxed).');
|
|
361
|
-
} catch (e) {
|
|
362
|
-
console.error(' Failed to install plugin: ' + e.message);
|
|
363
|
-
process.exit(1);
|
|
364
|
-
}
|
|
365
|
-
break;
|
|
366
|
-
}
|
|
367
|
-
case 'remove': {
|
|
368
|
-
const name = process.argv[4];
|
|
369
|
-
if (!name) { console.error(' Usage: npx let-them-talk plugin remove <name>'); process.exit(1); }
|
|
370
|
-
const reg = getRegistry();
|
|
371
|
-
const plugin = reg.find(p => p.name === name);
|
|
372
|
-
if (!plugin) { console.error(' Plugin not found: ' + name); process.exit(1); }
|
|
373
|
-
const newReg = reg.filter(p => p.name !== name);
|
|
374
|
-
saveRegistry(newReg);
|
|
375
|
-
if (plugin.file) {
|
|
376
|
-
const pluginFile = path.resolve(pluginsDir, plugin.file);
|
|
377
|
-
// Prevent path traversal — only delete files inside pluginsDir
|
|
378
|
-
if (pluginFile.startsWith(path.resolve(pluginsDir) + path.sep) && fs.existsSync(pluginFile)) {
|
|
379
|
-
fs.unlinkSync(pluginFile);
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
console.log(' Plugin "' + name + '" removed.');
|
|
383
|
-
break;
|
|
384
|
-
}
|
|
385
|
-
case 'enable': {
|
|
386
|
-
const name = process.argv[4];
|
|
387
|
-
if (!name) { console.error(' Usage: npx let-them-talk plugin enable <name>'); process.exit(1); }
|
|
388
|
-
const reg = getRegistry();
|
|
389
|
-
const plugin = reg.find(p => p.name === name);
|
|
390
|
-
if (!plugin) { console.error(' Plugin not found: ' + name); process.exit(1); }
|
|
391
|
-
plugin.enabled = true;
|
|
392
|
-
saveRegistry(reg);
|
|
393
|
-
console.log(' Plugin "' + name + '" enabled.');
|
|
394
|
-
break;
|
|
395
|
-
}
|
|
396
|
-
case 'disable': {
|
|
397
|
-
const name = process.argv[4];
|
|
398
|
-
if (!name) { console.error(' Usage: npx let-them-talk plugin disable <name>'); process.exit(1); }
|
|
399
|
-
const reg = getRegistry();
|
|
400
|
-
const plugin = reg.find(p => p.name === name);
|
|
401
|
-
if (!plugin) { console.error(' Plugin not found: ' + name); process.exit(1); }
|
|
402
|
-
plugin.enabled = false;
|
|
403
|
-
saveRegistry(reg);
|
|
404
|
-
console.log(' Plugin "' + name + '" disabled.');
|
|
405
|
-
break;
|
|
406
|
-
}
|
|
407
|
-
default:
|
|
408
|
-
console.error(' Unknown plugin command: ' + (subCmd || ''));
|
|
409
|
-
console.error(' Available: list, add, remove, enable, disable');
|
|
410
|
-
process.exit(1);
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
300
|
function dashboard() {
|
|
415
301
|
if (process.argv.includes('--lan')) {
|
|
416
302
|
process.env.AGENT_BRIDGE_LAN = 'true';
|
|
@@ -532,7 +418,7 @@ switch (command) {
|
|
|
532
418
|
break;
|
|
533
419
|
case 'plugin':
|
|
534
420
|
case 'plugins':
|
|
535
|
-
|
|
421
|
+
console.log(' Plugins have been removed in v3.4.3. CLI terminals have their own extension systems.');
|
|
536
422
|
break;
|
|
537
423
|
case 'help':
|
|
538
424
|
case '--help':
|
package/dashboard.html
CHANGED
|
@@ -2572,65 +2572,7 @@
|
|
|
2572
2572
|
}
|
|
2573
2573
|
|
|
2574
2574
|
/* ===== v3.0: PLUGINS SECTION ===== */
|
|
2575
|
-
.
|
|
2576
|
-
background: var(--surface-2);
|
|
2577
|
-
border: 1px solid var(--border);
|
|
2578
|
-
border-radius: 6px;
|
|
2579
|
-
padding: 8px 10px;
|
|
2580
|
-
margin-bottom: 4px;
|
|
2581
|
-
display: flex;
|
|
2582
|
-
align-items: center;
|
|
2583
|
-
justify-content: space-between;
|
|
2584
|
-
}
|
|
2585
|
-
|
|
2586
|
-
.plugin-info {
|
|
2587
|
-
flex: 1;
|
|
2588
|
-
min-width: 0;
|
|
2589
|
-
}
|
|
2590
|
-
|
|
2591
|
-
.plugin-name {
|
|
2592
|
-
font-size: 12px;
|
|
2593
|
-
font-weight: 600;
|
|
2594
|
-
}
|
|
2595
|
-
|
|
2596
|
-
.plugin-desc {
|
|
2597
|
-
font-size: 10px;
|
|
2598
|
-
color: var(--text-muted);
|
|
2599
|
-
}
|
|
2600
|
-
|
|
2601
|
-
.plugin-toggle {
|
|
2602
|
-
background: var(--surface-3);
|
|
2603
|
-
border: 1px solid var(--border);
|
|
2604
|
-
border-radius: 10px;
|
|
2605
|
-
width: 36px;
|
|
2606
|
-
height: 20px;
|
|
2607
|
-
cursor: pointer;
|
|
2608
|
-
position: relative;
|
|
2609
|
-
transition: all 0.2s;
|
|
2610
|
-
flex-shrink: 0;
|
|
2611
|
-
}
|
|
2612
|
-
|
|
2613
|
-
.plugin-toggle.on {
|
|
2614
|
-
background: var(--green-dim);
|
|
2615
|
-
border-color: var(--green);
|
|
2616
|
-
}
|
|
2617
|
-
|
|
2618
|
-
.plugin-toggle::after {
|
|
2619
|
-
content: '';
|
|
2620
|
-
position: absolute;
|
|
2621
|
-
width: 14px;
|
|
2622
|
-
height: 14px;
|
|
2623
|
-
border-radius: 50%;
|
|
2624
|
-
background: var(--text-dim);
|
|
2625
|
-
top: 2px;
|
|
2626
|
-
left: 2px;
|
|
2627
|
-
transition: all 0.2s;
|
|
2628
|
-
}
|
|
2629
|
-
|
|
2630
|
-
.plugin-toggle.on::after {
|
|
2631
|
-
left: 18px;
|
|
2632
|
-
background: var(--green);
|
|
2633
|
-
}
|
|
2575
|
+
/* Plugins removed in v3.4.3 */
|
|
2634
2576
|
|
|
2635
2577
|
/* ===== v3.0: AVATAR PICKER ===== */
|
|
2636
2578
|
.avatar-option {
|
|
@@ -2758,12 +2700,6 @@
|
|
|
2758
2700
|
<div id="activity-heatmap"></div>
|
|
2759
2701
|
</div>
|
|
2760
2702
|
|
|
2761
|
-
<!-- Plugins Section -->
|
|
2762
|
-
<div class="sidebar-section">
|
|
2763
|
-
<div class="sidebar-title"><span>Plugins</span></div>
|
|
2764
|
-
<div id="plugins-list"></div>
|
|
2765
|
-
</div>
|
|
2766
|
-
|
|
2767
2703
|
<!-- Bookmarks Section -->
|
|
2768
2704
|
<div class="sidebar-section">
|
|
2769
2705
|
<div class="sidebar-title">
|
|
@@ -2842,7 +2778,7 @@
|
|
|
2842
2778
|
</div>
|
|
2843
2779
|
</div>
|
|
2844
2780
|
<div class="app-footer">
|
|
2845
|
-
<span>Let Them Talk v3.4.
|
|
2781
|
+
<span>Let Them Talk v3.4.4</span>
|
|
2846
2782
|
</div>
|
|
2847
2783
|
<div class="profile-popup" id="profile-popup" onclick="event.stopPropagation()">
|
|
2848
2784
|
<div class="profile-popup-header">
|
|
@@ -4750,44 +4686,6 @@ function switchBranch(name) {
|
|
|
4750
4686
|
poll();
|
|
4751
4687
|
}
|
|
4752
4688
|
|
|
4753
|
-
// ==================== v3.0: PLUGINS ====================
|
|
4754
|
-
|
|
4755
|
-
function fetchPlugins() {
|
|
4756
|
-
var pq = projectParam();
|
|
4757
|
-
lttFetch('/api/plugins' + pq).then(function(r) { return r.json(); }).then(function(data) {
|
|
4758
|
-
renderPlugins(Array.isArray(data) ? data : []);
|
|
4759
|
-
}).catch(function() {});
|
|
4760
|
-
}
|
|
4761
|
-
|
|
4762
|
-
function renderPlugins(plugins) {
|
|
4763
|
-
var el = document.getElementById('plugins-list');
|
|
4764
|
-
if (!plugins.length) {
|
|
4765
|
-
el.innerHTML = '<div style="color:var(--text-muted);font-size:12px;padding:4px;">No plugins installed</div>';
|
|
4766
|
-
return;
|
|
4767
|
-
}
|
|
4768
|
-
var html = '';
|
|
4769
|
-
for (var i = 0; i < plugins.length; i++) {
|
|
4770
|
-
var p = plugins[i];
|
|
4771
|
-
var onClass = p.enabled !== false ? ' on' : '';
|
|
4772
|
-
html += '<div class="plugin-card">' +
|
|
4773
|
-
'<div class="plugin-info">' +
|
|
4774
|
-
'<div class="plugin-name">' + escapeHtml(p.name) + '</div>' +
|
|
4775
|
-
'<div class="plugin-desc">' + escapeHtml(p.description || '') + '</div>' +
|
|
4776
|
-
'</div>' +
|
|
4777
|
-
'<div class="plugin-toggle' + onClass + '" onclick="togglePlugin(\'' + escapeHtml(p.name) + '\')"></div>' +
|
|
4778
|
-
'</div>';
|
|
4779
|
-
}
|
|
4780
|
-
el.innerHTML = html;
|
|
4781
|
-
}
|
|
4782
|
-
|
|
4783
|
-
function togglePlugin(name) {
|
|
4784
|
-
lttFetch('/api/plugins' + projectParam(), {
|
|
4785
|
-
method: 'POST',
|
|
4786
|
-
headers: { 'Content-Type': 'application/json' },
|
|
4787
|
-
body: JSON.stringify({ action: 'toggle', name: name })
|
|
4788
|
-
}).then(function() { fetchPlugins(); }).catch(function() {});
|
|
4789
|
-
}
|
|
4790
|
-
|
|
4791
4689
|
// ==================== POLLING ====================
|
|
4792
4690
|
|
|
4793
4691
|
function poll() {
|
|
@@ -4839,7 +4737,6 @@ function poll() {
|
|
|
4839
4737
|
renderBookmarksSidebar();
|
|
4840
4738
|
fetchActivity();
|
|
4841
4739
|
fetchBranches();
|
|
4842
|
-
fetchPlugins();
|
|
4843
4740
|
updateTypingIndicator(cachedAgents);
|
|
4844
4741
|
if (activeView === 'tasks') fetchTasks();
|
|
4845
4742
|
if (activeView === 'workspaces') fetchWorkspaces();
|
package/dashboard.js
CHANGED
|
@@ -328,7 +328,7 @@ function apiStats(query) {
|
|
|
328
328
|
function apiReset(query) {
|
|
329
329
|
const projectPath = query.get('project') || null;
|
|
330
330
|
const dataDir = resolveDataDir(projectPath);
|
|
331
|
-
const fixedFiles = ['messages.jsonl', 'history.jsonl', 'agents.json', 'acks.json', 'tasks.json', 'profiles.json', 'workflows.json', 'branches.json', '
|
|
331
|
+
const fixedFiles = ['messages.jsonl', 'history.jsonl', 'agents.json', 'acks.json', 'tasks.json', 'profiles.json', 'workflows.json', 'branches.json', 'read_receipts.json', 'permissions.json'];
|
|
332
332
|
for (const f of fixedFiles) {
|
|
333
333
|
const p = path.join(dataDir, f);
|
|
334
334
|
if (fs.existsSync(p)) fs.unlinkSync(p);
|
|
@@ -412,15 +412,7 @@ function apiAddProject(body) {
|
|
|
412
412
|
const absPath = path.resolve(body.path);
|
|
413
413
|
if (!fs.existsSync(absPath)) return { error: `Path does not exist: ${absPath}` };
|
|
414
414
|
|
|
415
|
-
//
|
|
416
|
-
const cwd = path.resolve(process.cwd());
|
|
417
|
-
if (!absPath.startsWith(cwd + path.sep) && absPath !== cwd) {
|
|
418
|
-
const hasProject = fs.existsSync(path.join(absPath, 'package.json')) ||
|
|
419
|
-
fs.existsSync(path.join(absPath, '.git'));
|
|
420
|
-
if (!hasProject) {
|
|
421
|
-
return { error: 'Path must be a project directory (with package.json or .git)' };
|
|
422
|
-
}
|
|
423
|
-
}
|
|
415
|
+
// Any existing directory can be added as a project — user explicitly chose it
|
|
424
416
|
|
|
425
417
|
const projects = getProjects();
|
|
426
418
|
const name = body.name || path.basename(absPath);
|
|
@@ -1300,27 +1292,6 @@ const server = http.createServer(async (req, res) => {
|
|
|
1300
1292
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1301
1293
|
res.end(JSON.stringify(branches));
|
|
1302
1294
|
}
|
|
1303
|
-
else if (url.pathname === '/api/plugins' && req.method === 'GET') {
|
|
1304
|
-
const projectPath = url.searchParams.get('project') || null;
|
|
1305
|
-
const pluginsFile = filePath('plugins.json', projectPath);
|
|
1306
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1307
|
-
res.end(JSON.stringify(fs.existsSync(pluginsFile) ? JSON.parse(fs.readFileSync(pluginsFile, 'utf8')) : []));
|
|
1308
|
-
}
|
|
1309
|
-
else if (url.pathname === '/api/plugins' && req.method === 'POST') {
|
|
1310
|
-
const body = await parseBody(req);
|
|
1311
|
-
const projectPath = url.searchParams.get('project') || null;
|
|
1312
|
-
const pluginsFile = filePath('plugins.json', projectPath);
|
|
1313
|
-
let plugins = [];
|
|
1314
|
-
if (fs.existsSync(pluginsFile)) try { plugins = JSON.parse(fs.readFileSync(pluginsFile, 'utf8')); } catch {}
|
|
1315
|
-
if (body.action === 'toggle' && body.name) {
|
|
1316
|
-
const p = plugins.find(x => x.name === body.name);
|
|
1317
|
-
if (!p) { res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Plugin not found' })); return; }
|
|
1318
|
-
p.enabled = !p.enabled;
|
|
1319
|
-
fs.writeFileSync(pluginsFile, JSON.stringify(plugins, null, 2));
|
|
1320
|
-
}
|
|
1321
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1322
|
-
res.end(JSON.stringify({ success: true }));
|
|
1323
|
-
}
|
|
1324
1295
|
else if (url.pathname === '/api/projects' && req.method === 'DELETE') {
|
|
1325
1296
|
const body = await parseBody(req);
|
|
1326
1297
|
const result = apiRemoveProject(body);
|
|
@@ -1501,7 +1472,7 @@ server.listen(PORT, LAN_MODE ? '0.0.0.0' : '127.0.0.1', () => {
|
|
|
1501
1472
|
const dataDir = resolveDataDir();
|
|
1502
1473
|
const lanIP = getLanIP();
|
|
1503
1474
|
console.log('');
|
|
1504
|
-
console.log(' Let Them Talk - Agent Bridge Dashboard v3.4.
|
|
1475
|
+
console.log(' Let Them Talk - Agent Bridge Dashboard v3.4.4');
|
|
1505
1476
|
console.log(' ============================================');
|
|
1506
1477
|
console.log(' Dashboard: http://localhost:' + PORT);
|
|
1507
1478
|
if (LAN_MODE && lanIP) {
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -18,8 +18,7 @@ const PROFILES_FILE = path.join(DATA_DIR, 'profiles.json');
|
|
|
18
18
|
const WORKFLOWS_FILE = path.join(DATA_DIR, 'workflows.json');
|
|
19
19
|
const WORKSPACES_DIR = path.join(DATA_DIR, 'workspaces');
|
|
20
20
|
const BRANCHES_FILE = path.join(DATA_DIR, 'branches.json');
|
|
21
|
-
|
|
22
|
-
const PLUGINS_DIR = path.join(DATA_DIR, 'plugins');
|
|
21
|
+
// Plugins removed in v3.4.3 — unnecessary attack surface, CLIs have their own extension systems
|
|
23
22
|
|
|
24
23
|
// In-memory state for this process
|
|
25
24
|
let registeredName = null;
|
|
@@ -418,17 +417,6 @@ function getHistoryFile(branch) {
|
|
|
418
417
|
return path.join(DATA_DIR, `branch-${sanitizeName(branch)}-history.jsonl`);
|
|
419
418
|
}
|
|
420
419
|
|
|
421
|
-
// --- Plugin helpers ---
|
|
422
|
-
|
|
423
|
-
function getPluginRegistry() {
|
|
424
|
-
if (!fs.existsSync(PLUGINS_FILE)) return [];
|
|
425
|
-
try { return JSON.parse(fs.readFileSync(PLUGINS_FILE, 'utf8')); } catch { return []; }
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
function savePluginRegistry(plugins) {
|
|
429
|
-
fs.writeFileSync(PLUGINS_FILE, JSON.stringify(plugins, null, 2));
|
|
430
|
-
}
|
|
431
|
-
|
|
432
420
|
// --- Tool implementations ---
|
|
433
421
|
|
|
434
422
|
function toolRegister(name, provider = null) {
|
|
@@ -1181,8 +1169,8 @@ function toolReset() {
|
|
|
1181
1169
|
}
|
|
1182
1170
|
}
|
|
1183
1171
|
}
|
|
1184
|
-
// Remove profiles, workflows, branches,
|
|
1185
|
-
for (const f of [PROFILES_FILE, WORKFLOWS_FILE, BRANCHES_FILE,
|
|
1172
|
+
// Remove profiles, workflows, branches, permissions, read receipts
|
|
1173
|
+
for (const f of [PROFILES_FILE, WORKFLOWS_FILE, BRANCHES_FILE, PERMISSIONS_FILE, READ_RECEIPTS_FILE]) {
|
|
1186
1174
|
if (fs.existsSync(f)) fs.unlinkSync(f);
|
|
1187
1175
|
}
|
|
1188
1176
|
// Remove workspaces dir
|
|
@@ -1492,16 +1480,11 @@ function toolListBranches() {
|
|
|
1492
1480
|
// --- MCP Server setup ---
|
|
1493
1481
|
|
|
1494
1482
|
const server = new Server(
|
|
1495
|
-
{ name: 'agent-bridge', version: '3.
|
|
1483
|
+
{ name: 'agent-bridge', version: '3.4.4' },
|
|
1496
1484
|
{ capabilities: { tools: {} } }
|
|
1497
1485
|
);
|
|
1498
1486
|
|
|
1499
1487
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
1500
|
-
const pluginTools = loadedPlugins.map(p => ({
|
|
1501
|
-
name: 'plugin_' + p.name,
|
|
1502
|
-
description: '[Plugin] ' + p.description,
|
|
1503
|
-
inputSchema: p.inputSchema,
|
|
1504
|
-
}));
|
|
1505
1488
|
return {
|
|
1506
1489
|
tools: [
|
|
1507
1490
|
{
|
|
@@ -1875,7 +1858,6 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1875
1858
|
properties: {},
|
|
1876
1859
|
},
|
|
1877
1860
|
},
|
|
1878
|
-
...pluginTools,
|
|
1879
1861
|
],
|
|
1880
1862
|
};
|
|
1881
1863
|
});
|
|
@@ -1969,12 +1951,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1969
1951
|
result = toolListBranches();
|
|
1970
1952
|
break;
|
|
1971
1953
|
default:
|
|
1972
|
-
// Check if it's a plugin tool
|
|
1973
|
-
if (name.startsWith('plugin_')) {
|
|
1974
|
-
const pluginName = name.substring(7);
|
|
1975
|
-
result = await executePlugin(pluginName, args);
|
|
1976
|
-
break;
|
|
1977
|
-
}
|
|
1978
1954
|
return {
|
|
1979
1955
|
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
|
|
1980
1956
|
isError: true,
|
|
@@ -2014,90 +1990,11 @@ process.on('exit', () => {
|
|
|
2014
1990
|
process.on('SIGTERM', () => process.exit(0));
|
|
2015
1991
|
process.on('SIGINT', () => process.exit(0));
|
|
2016
1992
|
|
|
2017
|
-
// --- Phase 5: Plugin system ---
|
|
2018
|
-
|
|
2019
|
-
let loadedPlugins = []; // { name, description, inputSchema, handler }
|
|
2020
|
-
|
|
2021
|
-
function loadPlugins() {
|
|
2022
|
-
loadedPlugins = [];
|
|
2023
|
-
if (!fs.existsSync(PLUGINS_DIR)) return;
|
|
2024
|
-
const registry = getPluginRegistry();
|
|
2025
|
-
const enabledNames = new Set(registry.filter(p => p.enabled !== false).map(p => p.name));
|
|
2026
|
-
|
|
2027
|
-
try {
|
|
2028
|
-
const vm = require('vm');
|
|
2029
|
-
const files = fs.readdirSync(PLUGINS_DIR).filter(f => f.endsWith('.js'));
|
|
2030
|
-
for (const file of files) {
|
|
2031
|
-
try {
|
|
2032
|
-
const pluginPath = path.join(PLUGINS_DIR, file);
|
|
2033
|
-
const code = fs.readFileSync(pluginPath, 'utf8');
|
|
2034
|
-
// Run plugin in a sandboxed VM context — no require, no process, no child_process
|
|
2035
|
-
const sandbox = { module: { exports: {} }, exports: {}, console: { log: () => {}, error: () => {}, warn: () => {} } };
|
|
2036
|
-
vm.runInNewContext(code, sandbox, { filename: file, timeout: 5000 });
|
|
2037
|
-
const plugin = sandbox.module.exports;
|
|
2038
|
-
if (!plugin.name || !plugin.description || !plugin.handler) {
|
|
2039
|
-
console.error(`Plugin ${file}: missing name, description, or handler`);
|
|
2040
|
-
continue;
|
|
2041
|
-
}
|
|
2042
|
-
if (!enabledNames.has(plugin.name) && enabledNames.size > 0) continue;
|
|
2043
|
-
loadedPlugins.push({
|
|
2044
|
-
name: plugin.name,
|
|
2045
|
-
description: plugin.description,
|
|
2046
|
-
inputSchema: plugin.inputSchema || { type: 'object', properties: {} },
|
|
2047
|
-
handler: plugin.handler,
|
|
2048
|
-
});
|
|
2049
|
-
console.error(`Plugin loaded: ${plugin.name} (sandboxed)`);
|
|
2050
|
-
} catch (e) {
|
|
2051
|
-
console.error(`Plugin ${file} failed to load: ${e.message}`);
|
|
2052
|
-
}
|
|
2053
|
-
}
|
|
2054
|
-
} catch {}
|
|
2055
|
-
}
|
|
2056
|
-
|
|
2057
|
-
function executePlugin(pluginName, args) {
|
|
2058
|
-
const plugin = loadedPlugins.find(p => p.name === pluginName);
|
|
2059
|
-
if (!plugin) return { error: `Plugin "${pluginName}" not found` };
|
|
2060
|
-
|
|
2061
|
-
const context = {
|
|
2062
|
-
registeredName,
|
|
2063
|
-
sendMessage: (to, content) => toolSendMessage(content, to),
|
|
2064
|
-
getAgents: () => toolListAgents().agents,
|
|
2065
|
-
getHistory: (limit) => toolGetHistory(limit),
|
|
2066
|
-
readFile: (filePath) => {
|
|
2067
|
-
const resolved = path.resolve(filePath);
|
|
2068
|
-
const allowedRoot = path.resolve(process.cwd());
|
|
2069
|
-
let realPath;
|
|
2070
|
-
try { realPath = fs.realpathSync(resolved); } catch { throw new Error('File not found'); }
|
|
2071
|
-
if (!realPath.startsWith(allowedRoot + path.sep) && realPath !== allowedRoot) {
|
|
2072
|
-
throw new Error('File path must be within the project directory');
|
|
2073
|
-
}
|
|
2074
|
-
return fs.readFileSync(realPath, 'utf8');
|
|
2075
|
-
},
|
|
2076
|
-
};
|
|
2077
|
-
|
|
2078
|
-
return new Promise((resolve) => {
|
|
2079
|
-
const timeout = setTimeout(() => resolve({ error: 'Plugin execution timed out (30s)' }), 30000);
|
|
2080
|
-
try {
|
|
2081
|
-
const result = plugin.handler(args, context);
|
|
2082
|
-
if (result && typeof result.then === 'function') {
|
|
2083
|
-
result.then(r => { clearTimeout(timeout); resolve(r); }).catch(e => { clearTimeout(timeout); resolve({ error: e.message }); });
|
|
2084
|
-
} else {
|
|
2085
|
-
clearTimeout(timeout);
|
|
2086
|
-
resolve(result);
|
|
2087
|
-
}
|
|
2088
|
-
} catch (e) {
|
|
2089
|
-
clearTimeout(timeout);
|
|
2090
|
-
resolve({ error: e.message });
|
|
2091
|
-
}
|
|
2092
|
-
});
|
|
2093
|
-
}
|
|
2094
|
-
|
|
2095
1993
|
async function main() {
|
|
2096
1994
|
ensureDataDir();
|
|
2097
|
-
loadPlugins();
|
|
2098
1995
|
const transport = new StdioServerTransport();
|
|
2099
1996
|
await server.connect(transport);
|
|
2100
|
-
console.error('Agent Bridge MCP server v3.4.
|
|
1997
|
+
console.error('Agent Bridge MCP server v3.4.4 running (27 tools)');
|
|
2101
1998
|
}
|
|
2102
1999
|
|
|
2103
2000
|
main().catch(console.error);
|