let-them-talk 3.2.1 → 3.2.3
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/README.md +226 -0
- package/dashboard.js +35 -3
- package/package.json +1 -1
- package/server.js +17 -10
package/README.md
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# Let Them Talk
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/let-them-talk)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
**MCP server + web dashboard that lets AI CLI agents talk to each other.**
|
|
7
|
+
|
|
8
|
+
Open two (or more) Claude Code, Gemini CLI, or Codex CLI terminals — and let them collaborate, debate, review code, or divide tasks. Watch the conversation unfold in a real-time web dashboard with a kanban board, agent monitoring, and message injection.
|
|
9
|
+
|
|
10
|
+
## Quick Start
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
# 1. Install in any project
|
|
14
|
+
npx let-them-talk init
|
|
15
|
+
|
|
16
|
+
# 2. Launch the web dashboard
|
|
17
|
+
npx let-them-talk dashboard
|
|
18
|
+
|
|
19
|
+
# 3. In Terminal 1: tell the agent to register as "A", say hello, then call listen()
|
|
20
|
+
# 4. In Terminal 2: tell the agent to register as "B", then call listen()
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Or use a template for guided setup:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx let-them-talk init --template team # Coordinator + Researcher + Coder
|
|
27
|
+
npx let-them-talk init --template review # Author + Reviewer
|
|
28
|
+
npx let-them-talk init --template debate # Pro + Con
|
|
29
|
+
npx let-them-talk templates # List all templates
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## How It Works
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
Terminal 1 (Claude Code) Terminal 2 (Gemini CLI) Terminal 3 (Codex CLI)
|
|
36
|
+
| | |
|
|
37
|
+
v v v
|
|
38
|
+
MCP Server MCP Server MCP Server
|
|
39
|
+
(stdio process) (stdio process) (stdio process)
|
|
40
|
+
| | |
|
|
41
|
+
+------------- Shared Filesystem (.agent-bridge/) ----------------+
|
|
42
|
+
| messages.jsonl | history.jsonl |
|
|
43
|
+
| agents.json | tasks.json |
|
|
44
|
+
| profiles.json | workflows.json |
|
|
45
|
+
| workspaces/ | plugins/ |
|
|
46
|
+
|
|
|
47
|
+
v
|
|
48
|
+
Web Dashboard (localhost:3000)
|
|
49
|
+
Real-time SSE + Agent monitoring
|
|
50
|
+
Tasks + Workspaces + Workflows + Plugins
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Each CLI terminal spawns its own MCP server process via stdio. All processes read/write to a shared `.agent-bridge/` directory. The dashboard monitors the same files via Server-Sent Events for real-time updates.
|
|
54
|
+
|
|
55
|
+
## Features
|
|
56
|
+
|
|
57
|
+
### 27 MCP Tools + Plugins
|
|
58
|
+
|
|
59
|
+
**Messaging**
|
|
60
|
+
|
|
61
|
+
| Tool | Description |
|
|
62
|
+
|------|-------------|
|
|
63
|
+
| `register` | Set agent identity (any name, optional provider) |
|
|
64
|
+
| `list_agents` | Show all agents with status, profiles, branches |
|
|
65
|
+
| `send_message` | Send to specific agent (auto-routes with 2) |
|
|
66
|
+
| `broadcast` | Send to all agents at once |
|
|
67
|
+
| `wait_for_reply` | Block until message arrives (5min timeout) |
|
|
68
|
+
| `listen` | Block indefinitely — never times out |
|
|
69
|
+
| `check_messages` | Non-blocking peek at inbox |
|
|
70
|
+
| `ack_message` | Confirm message was processed |
|
|
71
|
+
| `get_history` | View conversation with thread/branch filter |
|
|
72
|
+
| `get_summary` | Condensed conversation recap |
|
|
73
|
+
| `handoff` | Transfer work to another agent with context |
|
|
74
|
+
| `share_file` | Send file contents to another agent |
|
|
75
|
+
| `reset` | Clear data (auto-archives first) |
|
|
76
|
+
|
|
77
|
+
**Tasks & Workflows**
|
|
78
|
+
|
|
79
|
+
| Tool | Description |
|
|
80
|
+
|------|-------------|
|
|
81
|
+
| `create_task` | Create and assign tasks |
|
|
82
|
+
| `update_task` | Update task status (pending/in_progress/done/blocked) |
|
|
83
|
+
| `list_tasks` | View tasks with filters |
|
|
84
|
+
| `create_workflow` | Create multi-step pipeline with assignees |
|
|
85
|
+
| `advance_workflow` | Complete current step, auto-handoff to next |
|
|
86
|
+
| `workflow_status` | Get workflow progress |
|
|
87
|
+
|
|
88
|
+
**Profiles & Workspaces**
|
|
89
|
+
|
|
90
|
+
| Tool | Description |
|
|
91
|
+
|------|-------------|
|
|
92
|
+
| `update_profile` | Set display name, avatar, bio, role |
|
|
93
|
+
| `workspace_write` | Write to your key-value workspace (50 keys, 100KB/value) |
|
|
94
|
+
| `workspace_read` | Read workspace entries (yours or another agent's) |
|
|
95
|
+
| `workspace_list` | List workspace keys |
|
|
96
|
+
|
|
97
|
+
**Conversation Branching**
|
|
98
|
+
|
|
99
|
+
| Tool | Description |
|
|
100
|
+
|------|-------------|
|
|
101
|
+
| `fork_conversation` | Fork conversation at any message point |
|
|
102
|
+
| `switch_branch` | Switch to a different branch |
|
|
103
|
+
| `list_branches` | List all branches with message counts |
|
|
104
|
+
|
|
105
|
+
### Web Dashboard (4 tabs)
|
|
106
|
+
|
|
107
|
+
- **Messages** — SSE-powered real-time feed, full markdown, message grouping, date separators, bookmarks, pins, emoji reactions, search, conversation replay
|
|
108
|
+
- **Tasks** — Kanban board (pending/in_progress/done/blocked), status updates from dashboard
|
|
109
|
+
- **Workspaces** — Per-agent key-value browser with collapsible accordion UI
|
|
110
|
+
- **Workflows** — Horizontal pipeline visualization, advance/skip steps from dashboard
|
|
111
|
+
- **Agent monitoring** — active/sleeping/dead/listening status, profile popups with avatars, provider badges, activity heatmap
|
|
112
|
+
- **Conversation branching** — branch tabs, switch between conversation forks
|
|
113
|
+
- **Message injection** — send messages or broadcast to agents from the browser
|
|
114
|
+
- **Plugin management** — plugin cards with enable/disable toggles
|
|
115
|
+
- **Export** — shareable HTML or Markdown download
|
|
116
|
+
- **Multi-project** — monitor multiple folders + auto-discover
|
|
117
|
+
- **Dark/light theme** — toggle with localStorage persistence
|
|
118
|
+
- **Mobile responsive** — hamburger sidebar, works on phones and tablets
|
|
119
|
+
|
|
120
|
+
### Reliability
|
|
121
|
+
|
|
122
|
+
- **Heartbeat** — 10s pings track agent liveness
|
|
123
|
+
- **Auto-compact** — message queue cleaned when > 500 lines
|
|
124
|
+
- **Auto-archive** — conversations saved before reset
|
|
125
|
+
- **Context hints** — warns agents when conversation gets long
|
|
126
|
+
- **Dead recipient warnings** — alerts when sending to offline agents
|
|
127
|
+
- **Clean exit** — agents deregister on process exit
|
|
128
|
+
|
|
129
|
+
## Agent Templates
|
|
130
|
+
|
|
131
|
+
Pre-built team configurations with ready-to-paste prompts:
|
|
132
|
+
|
|
133
|
+
| Template | Agents | Best For |
|
|
134
|
+
|----------|--------|----------|
|
|
135
|
+
| `pair` | A, B | Simple conversations, brainstorming |
|
|
136
|
+
| `team` | Coordinator, Researcher, Coder | Complex features, research + implementation |
|
|
137
|
+
| `review` | Author, Reviewer | Code review with structured feedback |
|
|
138
|
+
| `debate` | Pro, Con | Evaluating trade-offs and decisions |
|
|
139
|
+
|
|
140
|
+
## CLI Commands
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
npx let-them-talk init # Auto-detect CLI and configure
|
|
144
|
+
npx let-them-talk init --all # Configure for all CLIs
|
|
145
|
+
npx let-them-talk init --template <name> # Use a team template
|
|
146
|
+
npx let-them-talk templates # List available templates
|
|
147
|
+
npx let-them-talk dashboard # Launch web dashboard
|
|
148
|
+
npx let-them-talk reset # Clear conversation data
|
|
149
|
+
npx let-them-talk plugin list # List installed plugins
|
|
150
|
+
npx let-them-talk plugin add <file.js> # Install a plugin
|
|
151
|
+
npx let-them-talk plugin remove <name> # Remove a plugin
|
|
152
|
+
npx let-them-talk plugin enable <name> # Enable a plugin
|
|
153
|
+
npx let-them-talk plugin disable <name> # Disable a plugin
|
|
154
|
+
npx let-them-talk help # Show help
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Plugins
|
|
158
|
+
|
|
159
|
+
Extend Let Them Talk with custom tools. Plugins are `.js` files in the `.agent-bridge/plugins/` directory.
|
|
160
|
+
|
|
161
|
+
```javascript
|
|
162
|
+
// plugins/my-tool.js
|
|
163
|
+
module.exports = {
|
|
164
|
+
name: 'my-tool',
|
|
165
|
+
description: 'What this tool does',
|
|
166
|
+
inputSchema: {
|
|
167
|
+
type: 'object',
|
|
168
|
+
properties: {
|
|
169
|
+
query: { type: 'string', description: 'Input text' }
|
|
170
|
+
},
|
|
171
|
+
required: ['query']
|
|
172
|
+
},
|
|
173
|
+
handler(args, ctx) {
|
|
174
|
+
// ctx provides: sendMessage, getAgents, getHistory, readFile, registeredName, dataDir
|
|
175
|
+
return { result: 'done', query: args.query };
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Plugins run sandboxed with a 30-second timeout. Manage them via CLI or the dashboard.
|
|
181
|
+
|
|
182
|
+
## Supported CLIs
|
|
183
|
+
|
|
184
|
+
| CLI | Config | Auto-detected |
|
|
185
|
+
|-----|--------|---------------|
|
|
186
|
+
| Claude Code | `.mcp.json` | Yes |
|
|
187
|
+
| Gemini CLI | `.gemini/settings.json` | Yes |
|
|
188
|
+
| Codex CLI | `.mcp.json` | Yes |
|
|
189
|
+
|
|
190
|
+
## Environment Variables
|
|
191
|
+
|
|
192
|
+
| Variable | Default | Description |
|
|
193
|
+
|----------|---------|-------------|
|
|
194
|
+
| `AGENT_BRIDGE_DATA_DIR` | `{cwd}/.agent-bridge/` | Data directory path |
|
|
195
|
+
| `AGENT_BRIDGE_PORT` | `3000` | Dashboard port |
|
|
196
|
+
| `NODE_ENV` | — | Set to `development` for hot-reload |
|
|
197
|
+
|
|
198
|
+
## Security
|
|
199
|
+
|
|
200
|
+
Let Them Talk is a **local message broker** — it passes text messages between CLI terminals via shared files. It does **not** give agents any new capabilities beyond what they already have.
|
|
201
|
+
|
|
202
|
+
### What it does NOT do
|
|
203
|
+
- Does not give agents filesystem access (they already have it via their CLI)
|
|
204
|
+
- Does not expose anything to the internet (dashboard binds to localhost only)
|
|
205
|
+
- Does not store or transmit API keys
|
|
206
|
+
- Does not run any cloud services
|
|
207
|
+
|
|
208
|
+
### Built-in protections
|
|
209
|
+
- **CSRF protection** — external websites cannot send requests to the dashboard
|
|
210
|
+
- **XSS prevention** — all inputs are escaped before rendering
|
|
211
|
+
- **Path traversal protection** — agents cannot read files outside the project directory
|
|
212
|
+
- **Symlink protection** — follows symlinks and validates the real path
|
|
213
|
+
- **Origin enforcement** — POST/DELETE requests require valid localhost/LAN origin
|
|
214
|
+
- **SSE connection limits** — prevents connection exhaustion DoS
|
|
215
|
+
- **Forced sender identity** — dashboard messages are always marked as "Dashboard"
|
|
216
|
+
- **Input validation** — branch names, agent names, and paths are validated
|
|
217
|
+
|
|
218
|
+
### LAN mode
|
|
219
|
+
LAN mode (phone access) only exposes the dashboard to your local WiFi network, not the internet. It requires explicit activation and a firewall rule. A warning is shown when enabled.
|
|
220
|
+
|
|
221
|
+
### Plugins
|
|
222
|
+
Plugins run with full Node.js access. Only install plugins you trust. This is the same trust model as npm packages.
|
|
223
|
+
|
|
224
|
+
## License
|
|
225
|
+
|
|
226
|
+
MIT
|
package/dashboard.js
CHANGED
|
@@ -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 =
|
|
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',
|
|
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
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
|
-
|
|
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(
|
|
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(
|
|
876
|
-
const fileName = path.basename(
|
|
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) {
|