agent-bridge-mcp 1.2.0 → 1.4.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/README.md +97 -0
- package/package.json +1 -1
- package/server.mjs +372 -23
package/README.md
CHANGED
|
@@ -88,6 +88,8 @@ Data directory:
|
|
|
88
88
|
1740524430000-x7k2f9.json # { from, to, content, timestamp }
|
|
89
89
|
context/
|
|
90
90
|
nova-main.json # { agentName, agentId, project, updatedAt, content }
|
|
91
|
+
cursors/
|
|
92
|
+
nova-main.cursor # timestamp of last delivered message
|
|
91
93
|
```
|
|
92
94
|
|
|
93
95
|
## Example Usage
|
|
@@ -198,6 +200,101 @@ Set `AGENT_BRIDGE_NAME` in your project's `.mcp.json` so the ingest knows which
|
|
|
198
200
|
|
|
199
201
|
Context is stored as the last 20 exchanges per agent, accessible via `get_context`.
|
|
200
202
|
|
|
203
|
+
## Automatic Message Delivery (UserPromptSubmit Hook)
|
|
204
|
+
|
|
205
|
+
The `--check-messages` flag automatically delivers pending messages to an agent before every prompt — no manual `read_messages` calls needed.
|
|
206
|
+
|
|
207
|
+
**How it works:** Every time you send a prompt to any tab, the hook checks for new messages addressed to that agent. If there are any, they get injected into the conversation as context so Claude sees them immediately. A cursor file tracks what's already been delivered so messages are never repeated.
|
|
208
|
+
|
|
209
|
+
**Setup:**
|
|
210
|
+
|
|
211
|
+
Add to the `UserPromptSubmit` hooks in `~/.claude/settings.json`:
|
|
212
|
+
```json
|
|
213
|
+
{
|
|
214
|
+
"type": "command",
|
|
215
|
+
"command": "npx agent-bridge-mcp --check-messages",
|
|
216
|
+
"timeout": 5
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
The agent name is resolved automatically from the project's `.mcp.json` (reads `AGENT_BRIDGE_NAME` from the agent-bridge MCP config).
|
|
221
|
+
|
|
222
|
+
## Session Bootstrap (SessionStart Hook)
|
|
223
|
+
|
|
224
|
+
The `--bootstrap-context` flag loads shared context from all other agents when a new session starts — so new tabs immediately know what everyone else knows.
|
|
225
|
+
|
|
226
|
+
**How it works:** When a Claude Code session starts or resumes, the hook reads all shared context files and lists active agents, then injects everything into the conversation. The agent starts with full awareness of what other agents have been working on.
|
|
227
|
+
|
|
228
|
+
**Setup:**
|
|
229
|
+
|
|
230
|
+
Add a `SessionStart` hook to `~/.claude/settings.json`:
|
|
231
|
+
```json
|
|
232
|
+
{
|
|
233
|
+
"hooks": {
|
|
234
|
+
"SessionStart": [
|
|
235
|
+
{
|
|
236
|
+
"hooks": [
|
|
237
|
+
{
|
|
238
|
+
"type": "command",
|
|
239
|
+
"command": "npx agent-bridge-mcp --bootstrap-context",
|
|
240
|
+
"timeout": 10
|
|
241
|
+
}
|
|
242
|
+
]
|
|
243
|
+
}
|
|
244
|
+
]
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## Full Hook Setup
|
|
250
|
+
|
|
251
|
+
Here's the complete `~/.claude/settings.json` hooks configuration with all agent-bridge features enabled:
|
|
252
|
+
|
|
253
|
+
```json
|
|
254
|
+
{
|
|
255
|
+
"hooks": {
|
|
256
|
+
"SessionStart": [
|
|
257
|
+
{
|
|
258
|
+
"hooks": [
|
|
259
|
+
{
|
|
260
|
+
"type": "command",
|
|
261
|
+
"command": "npx agent-bridge-mcp --bootstrap-context",
|
|
262
|
+
"timeout": 10
|
|
263
|
+
}
|
|
264
|
+
]
|
|
265
|
+
}
|
|
266
|
+
],
|
|
267
|
+
"UserPromptSubmit": [
|
|
268
|
+
{
|
|
269
|
+
"hooks": [
|
|
270
|
+
{
|
|
271
|
+
"type": "command",
|
|
272
|
+
"command": "npx agent-bridge-mcp --capture-prompt",
|
|
273
|
+
"timeout": 5
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
"type": "command",
|
|
277
|
+
"command": "npx agent-bridge-mcp --check-messages",
|
|
278
|
+
"timeout": 5
|
|
279
|
+
}
|
|
280
|
+
]
|
|
281
|
+
}
|
|
282
|
+
],
|
|
283
|
+
"Stop": [
|
|
284
|
+
{
|
|
285
|
+
"hooks": [
|
|
286
|
+
{
|
|
287
|
+
"type": "command",
|
|
288
|
+
"command": "npx agent-bridge-mcp --ingest",
|
|
289
|
+
"timeout": 10
|
|
290
|
+
}
|
|
291
|
+
]
|
|
292
|
+
}
|
|
293
|
+
]
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
201
298
|
## Requirements
|
|
202
299
|
|
|
203
300
|
- Node.js >= 18
|
package/package.json
CHANGED
package/server.mjs
CHANGED
|
@@ -35,6 +35,7 @@ const BRIDGE_DIR = process.env.AGENT_BRIDGE_DATA_DIR || getDefaultDataDir();
|
|
|
35
35
|
const AGENTS_DIR = resolve(BRIDGE_DIR, 'agents');
|
|
36
36
|
const MESSAGES_DIR = resolve(BRIDGE_DIR, 'messages');
|
|
37
37
|
const CONTEXT_DIR = resolve(BRIDGE_DIR, 'context');
|
|
38
|
+
const CURSORS_DIR = resolve(BRIDGE_DIR, 'cursors');
|
|
38
39
|
const HEARTBEAT_INTERVAL_MS = parseInt(process.env.AGENT_BRIDGE_HEARTBEAT_MS || '15000', 10);
|
|
39
40
|
const STALE_THRESHOLD_MS = HEARTBEAT_INTERVAL_MS * 3;
|
|
40
41
|
const DEAD_THRESHOLD_MS = parseInt(process.env.AGENT_BRIDGE_DEAD_MS || '300000', 10);
|
|
@@ -46,6 +47,7 @@ const MESSAGE_TTL_MS = parseInt(process.env.AGENT_BRIDGE_MESSAGE_TTL_MS || '3600
|
|
|
46
47
|
|
|
47
48
|
const agentId = `agent-${randomBytes(3).toString('hex')}`;
|
|
48
49
|
let agentName = process.env.AGENT_BRIDGE_NAME || null;
|
|
50
|
+
let agentBaseName = process.env.AGENT_BRIDGE_NAME || null; // Original name before auto-suffix
|
|
49
51
|
const startedAt = new Date().toISOString();
|
|
50
52
|
let lastReadTimestamp = Date.now(); // only read messages after we start
|
|
51
53
|
let heartbeatTimer = null;
|
|
@@ -58,6 +60,7 @@ function ensureDirs() {
|
|
|
58
60
|
mkdirSync(AGENTS_DIR, { recursive: true });
|
|
59
61
|
mkdirSync(MESSAGES_DIR, { recursive: true });
|
|
60
62
|
mkdirSync(CONTEXT_DIR, { recursive: true });
|
|
63
|
+
mkdirSync(CURSORS_DIR, { recursive: true });
|
|
61
64
|
}
|
|
62
65
|
|
|
63
66
|
// ============================================================
|
|
@@ -81,6 +84,7 @@ function getAgentData() {
|
|
|
81
84
|
return {
|
|
82
85
|
id: agentId,
|
|
83
86
|
name: agentName,
|
|
87
|
+
baseName: agentBaseName || agentName,
|
|
84
88
|
project: process.cwd().split(/[\\/]/).pop(),
|
|
85
89
|
cwd: process.cwd(),
|
|
86
90
|
pid: process.pid,
|
|
@@ -281,17 +285,47 @@ function resolveAgent(nameOrId) {
|
|
|
281
285
|
const byId = agents.find(a => a.id === nameOrId);
|
|
282
286
|
if (byId) return byId;
|
|
283
287
|
|
|
284
|
-
// Exact name match (case-insensitive)
|
|
285
|
-
const
|
|
286
|
-
|
|
288
|
+
// Exact name match (case-insensitive) — pick most recently active if multiple
|
|
289
|
+
const nameL = nameOrId.toLowerCase();
|
|
290
|
+
const byName = agents
|
|
291
|
+
.filter(a => a.name && a.name.toLowerCase() === nameL)
|
|
292
|
+
.sort((a, b) => new Date(b.lastHeartbeat) - new Date(a.lastHeartbeat));
|
|
293
|
+
if (byName.length > 0) return byName[0];
|
|
294
|
+
|
|
295
|
+
// Also match by baseName for auto-suffixed agents
|
|
296
|
+
const byBase = agents
|
|
297
|
+
.filter(a => a.baseName && a.baseName.toLowerCase() === nameL)
|
|
298
|
+
.sort((a, b) => new Date(b.lastHeartbeat) - new Date(a.lastHeartbeat));
|
|
299
|
+
if (byBase.length > 0) return byBase[0];
|
|
287
300
|
|
|
288
301
|
// Partial name match
|
|
289
|
-
const partial = agents.filter(a => a.name && a.name.toLowerCase().includes(
|
|
302
|
+
const partial = agents.filter(a => a.name && a.name.toLowerCase().includes(nameL));
|
|
290
303
|
if (partial.length === 1) return partial[0];
|
|
291
304
|
|
|
292
305
|
return null;
|
|
293
306
|
}
|
|
294
307
|
|
|
308
|
+
// Resolve ALL agents matching a name (for multi-session messaging)
|
|
309
|
+
function resolveAllAgents(nameOrId) {
|
|
310
|
+
const agents = getAllAgents().filter(a => !a.isMe && a.status === 'active');
|
|
311
|
+
const nameL = nameOrId.toLowerCase();
|
|
312
|
+
|
|
313
|
+
// Exact ID match — single result
|
|
314
|
+
const byId = agents.find(a => a.id === nameOrId);
|
|
315
|
+
if (byId) return [byId];
|
|
316
|
+
|
|
317
|
+
// Match by name or baseName
|
|
318
|
+
const matches = agents.filter(a =>
|
|
319
|
+
(a.name && a.name.toLowerCase() === nameL) ||
|
|
320
|
+
(a.baseName && a.baseName.toLowerCase() === nameL)
|
|
321
|
+
);
|
|
322
|
+
if (matches.length > 0) return matches;
|
|
323
|
+
|
|
324
|
+
// Partial name match
|
|
325
|
+
const partial = agents.filter(a => a.name && a.name.toLowerCase().includes(nameL));
|
|
326
|
+
return partial;
|
|
327
|
+
}
|
|
328
|
+
|
|
295
329
|
// ============================================================
|
|
296
330
|
// Tool Handlers
|
|
297
331
|
// ============================================================
|
|
@@ -300,26 +334,32 @@ async function handleRegister(args) {
|
|
|
300
334
|
const name = args.name?.trim();
|
|
301
335
|
if (!name) return { error: 'Name is required' };
|
|
302
336
|
|
|
303
|
-
//
|
|
337
|
+
// Allow duplicate base names — auto-suffix if taken
|
|
304
338
|
const agents = getAllAgents().filter(a => !a.isMe && a.status === 'active');
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
339
|
+
let finalName = name;
|
|
340
|
+
const nameLower = name.toLowerCase();
|
|
341
|
+
const conflicts = agents.filter(a => a.name && a.name.toLowerCase() === nameLower);
|
|
342
|
+
if (conflicts.length > 0) {
|
|
343
|
+
// Find next available suffix
|
|
344
|
+
const allNames = agents.map(a => a.name?.toLowerCase()).filter(Boolean);
|
|
345
|
+
let suffix = 2;
|
|
346
|
+
while (allNames.includes(`${nameLower}-${suffix}`)) suffix++;
|
|
347
|
+
finalName = `${name}-${suffix}`;
|
|
311
348
|
}
|
|
312
349
|
|
|
313
|
-
agentName =
|
|
350
|
+
agentName = finalName;
|
|
351
|
+
agentBaseName = name; // Track original base name for context grouping
|
|
314
352
|
writeAgentFile();
|
|
315
353
|
|
|
316
354
|
return {
|
|
317
355
|
registered: true,
|
|
318
356
|
id: agentId,
|
|
319
357
|
name: agentName,
|
|
358
|
+
baseName: name,
|
|
320
359
|
project: process.cwd().split(/[\\/]/).pop(),
|
|
321
360
|
cwd: process.cwd(),
|
|
322
|
-
pid: process.pid
|
|
361
|
+
pid: process.pid,
|
|
362
|
+
...(finalName !== name ? { note: `Name "${name}" was taken, registered as "${finalName}"` } : {})
|
|
323
363
|
};
|
|
324
364
|
}
|
|
325
365
|
|
|
@@ -386,16 +426,18 @@ async function handleBroadcast(args) {
|
|
|
386
426
|
// Context Operations
|
|
387
427
|
// ============================================================
|
|
388
428
|
|
|
389
|
-
function writeContextFile(name, content) {
|
|
429
|
+
function writeContextFile(name, content, uniqueId) {
|
|
390
430
|
const safeName = name.replace(/[^a-zA-Z0-9_-]/g, '_').toLowerCase();
|
|
431
|
+
const suffix = uniqueId || agentId;
|
|
391
432
|
const data = JSON.stringify({
|
|
392
433
|
agentName: name,
|
|
393
|
-
agentId,
|
|
434
|
+
agentId: uniqueId || agentId,
|
|
435
|
+
baseName: name,
|
|
394
436
|
project: process.cwd().split(/[\\/]/).pop(),
|
|
395
437
|
updatedAt: new Date().toISOString(),
|
|
396
438
|
content
|
|
397
439
|
}, null, 2);
|
|
398
|
-
const targetPath = resolve(CONTEXT_DIR, `${safeName}.json`);
|
|
440
|
+
const targetPath = resolve(CONTEXT_DIR, `${safeName}--${suffix}.json`);
|
|
399
441
|
const tmpPath = targetPath + '.tmp';
|
|
400
442
|
try {
|
|
401
443
|
writeFileSync(tmpPath, data);
|
|
@@ -444,15 +486,34 @@ async function handleGetContext(args) {
|
|
|
444
486
|
const { from } = args;
|
|
445
487
|
|
|
446
488
|
if (from) {
|
|
447
|
-
// Get
|
|
489
|
+
// Get ALL contexts matching this base name (handles multiple sessions)
|
|
448
490
|
const safeName = from.replace(/[^a-zA-Z0-9_-]/g, '_').toLowerCase();
|
|
449
|
-
|
|
491
|
+
let files;
|
|
450
492
|
try {
|
|
451
|
-
|
|
452
|
-
return { found: true, context: ctx };
|
|
493
|
+
files = readdirSync(CONTEXT_DIR).filter(f => f.endsWith('.json'));
|
|
453
494
|
} catch {
|
|
454
495
|
return { found: false, error: `No shared context found for "${from}"` };
|
|
455
496
|
}
|
|
497
|
+
|
|
498
|
+
const matches = [];
|
|
499
|
+
for (const file of files) {
|
|
500
|
+
// Match: exact old-style "{name}.json" OR new-style "{name}--{id}.json"
|
|
501
|
+
const basePart = file.replace(/\.json$/, '').split('--')[0];
|
|
502
|
+
if (basePart === safeName) {
|
|
503
|
+
try {
|
|
504
|
+
const ctx = JSON.parse(readFileSync(resolve(CONTEXT_DIR, file), 'utf8'));
|
|
505
|
+
matches.push(ctx);
|
|
506
|
+
} catch { continue; }
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (matches.length === 0) {
|
|
511
|
+
return { found: false, error: `No shared context found for "${from}"` };
|
|
512
|
+
}
|
|
513
|
+
if (matches.length === 1) {
|
|
514
|
+
return { found: true, context: matches[0] };
|
|
515
|
+
}
|
|
516
|
+
return { found: true, contexts: matches, count: matches.length };
|
|
456
517
|
}
|
|
457
518
|
|
|
458
519
|
// Get all shared contexts
|
|
@@ -463,6 +524,108 @@ async function handleGetContext(args) {
|
|
|
463
524
|
};
|
|
464
525
|
}
|
|
465
526
|
|
|
527
|
+
// ============================================================
|
|
528
|
+
// Hook Helpers (used by --check-messages and --bootstrap-context)
|
|
529
|
+
// ============================================================
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Figure out the agent name for this project.
|
|
533
|
+
* Checks: 1) AGENT_BRIDGE_NAME env var, 2) .mcp.json in the cwd, 3) agent files matching cwd
|
|
534
|
+
*/
|
|
535
|
+
function resolveAgentName(cwd) {
|
|
536
|
+
if (process.env.AGENT_BRIDGE_NAME) return process.env.AGENT_BRIDGE_NAME;
|
|
537
|
+
|
|
538
|
+
// Read .mcp.json from the project directory
|
|
539
|
+
if (cwd) {
|
|
540
|
+
const mcpPath = resolve(cwd, '.mcp.json');
|
|
541
|
+
try {
|
|
542
|
+
const mcp = JSON.parse(readFileSync(mcpPath, 'utf8'));
|
|
543
|
+
const bridge = mcp.mcpServers?.['agent-bridge'];
|
|
544
|
+
if (bridge?.env?.AGENT_BRIDGE_NAME) return bridge.env.AGENT_BRIDGE_NAME;
|
|
545
|
+
} catch {}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Fallback: scan agent files for matching cwd
|
|
549
|
+
try {
|
|
550
|
+
const files = readdirSync(AGENTS_DIR).filter(f => f.endsWith('.json'));
|
|
551
|
+
for (const file of files) {
|
|
552
|
+
const agent = readAgentFile(resolve(AGENTS_DIR, file));
|
|
553
|
+
if (agent?.name && agent.cwd === cwd) return agent.name;
|
|
554
|
+
}
|
|
555
|
+
} catch {}
|
|
556
|
+
|
|
557
|
+
return null;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Read messages for a specific agent, starting from a cursor timestamp.
|
|
562
|
+
* Returns { messages: [...], newCursor: number }
|
|
563
|
+
*/
|
|
564
|
+
function readMessagesForAgent(name, cursorTimestamp) {
|
|
565
|
+
let files;
|
|
566
|
+
try {
|
|
567
|
+
files = readdirSync(MESSAGES_DIR).filter(f => f.endsWith('.json')).sort();
|
|
568
|
+
} catch {
|
|
569
|
+
return { messages: [], newCursor: cursorTimestamp };
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const pending = [];
|
|
573
|
+
let newCursor = cursorTimestamp;
|
|
574
|
+
const nameLower = name.toLowerCase();
|
|
575
|
+
|
|
576
|
+
for (const file of files) {
|
|
577
|
+
const timestamp = parseInt(file.split('-')[0], 10);
|
|
578
|
+
if (isNaN(timestamp) || timestamp <= cursorTimestamp) continue;
|
|
579
|
+
|
|
580
|
+
const filePath = resolve(MESSAGES_DIR, file);
|
|
581
|
+
let msg;
|
|
582
|
+
try { msg = JSON.parse(readFileSync(filePath, 'utf8')); } catch { continue; }
|
|
583
|
+
|
|
584
|
+
// Skip messages FROM this agent
|
|
585
|
+
if (msg.fromName?.toLowerCase() === nameLower) continue;
|
|
586
|
+
|
|
587
|
+
// Check if addressed to this agent or is a broadcast
|
|
588
|
+
const isForMe = msg.toName?.toLowerCase() === nameLower || msg.to?.toLowerCase() === nameLower;
|
|
589
|
+
const isBroadcast = msg.broadcast && msg.to === '*';
|
|
590
|
+
|
|
591
|
+
if (isForMe || isBroadcast) {
|
|
592
|
+
pending.push(msg);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
if (timestamp > newCursor) newCursor = timestamp;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return { messages: pending, newCursor };
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Read the cursor file for an agent. Returns the timestamp of the last processed message.
|
|
603
|
+
*/
|
|
604
|
+
function readCursor(name) {
|
|
605
|
+
const safeName = name.replace(/[^a-zA-Z0-9_-]/g, '_').toLowerCase();
|
|
606
|
+
const cursorPath = resolve(CURSORS_DIR, `${safeName}.cursor`);
|
|
607
|
+
try {
|
|
608
|
+
return parseInt(readFileSync(cursorPath, 'utf8').trim(), 10) || 0;
|
|
609
|
+
} catch {
|
|
610
|
+
return 0;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Write the cursor file for an agent.
|
|
616
|
+
*/
|
|
617
|
+
function writeCursor(name, timestamp) {
|
|
618
|
+
const safeName = name.replace(/[^a-zA-Z0-9_-]/g, '_').toLowerCase();
|
|
619
|
+
const cursorPath = resolve(CURSORS_DIR, `${safeName}.cursor`);
|
|
620
|
+
const tmpPath = cursorPath + '.tmp';
|
|
621
|
+
try {
|
|
622
|
+
writeFileSync(tmpPath, String(timestamp));
|
|
623
|
+
renameSync(tmpPath, cursorPath);
|
|
624
|
+
} catch {
|
|
625
|
+
try { writeFileSync(cursorPath, String(timestamp)); } catch {}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
466
629
|
// ============================================================
|
|
467
630
|
// Tool Definitions
|
|
468
631
|
// ============================================================
|
|
@@ -586,7 +749,7 @@ async function handleToolCall(name, args) {
|
|
|
586
749
|
// ============================================================
|
|
587
750
|
|
|
588
751
|
const server = new Server(
|
|
589
|
-
{ name: 'agent-bridge', version: '1.
|
|
752
|
+
{ name: 'agent-bridge', version: '1.4.0' },
|
|
590
753
|
{ capabilities: { tools: {} } }
|
|
591
754
|
);
|
|
592
755
|
|
|
@@ -708,14 +871,22 @@ async function runIngest() {
|
|
|
708
871
|
if (!name) process.exit(0);
|
|
709
872
|
|
|
710
873
|
// Read existing context and append new exchange
|
|
874
|
+
// Use session-specific filename so multiple sessions don't overwrite each other
|
|
711
875
|
const safeName = name.replace(/[^a-zA-Z0-9_-]/g, '_').toLowerCase();
|
|
712
|
-
const ctxPath = resolve(CONTEXT_DIR, `${safeName}.json`);
|
|
876
|
+
const ctxPath = resolve(CONTEXT_DIR, `${safeName}--${sessionId}.json`);
|
|
877
|
+
|
|
878
|
+
// Also check for legacy file (no session suffix) and migrate it
|
|
879
|
+
const legacyPath = resolve(CONTEXT_DIR, `${safeName}.json`);
|
|
880
|
+
|
|
713
881
|
let existing = { exchanges: [] };
|
|
714
882
|
try {
|
|
715
883
|
const prev = JSON.parse(readFileSync(ctxPath, 'utf8'));
|
|
716
884
|
if (prev.exchanges) existing = prev;
|
|
717
885
|
else if (prev.content) existing = { exchanges: [{ role: 'context', content: prev.content }] };
|
|
718
|
-
} catch {
|
|
886
|
+
} catch {
|
|
887
|
+
// If no session-specific file exists, DON'T inherit from legacy file
|
|
888
|
+
// (it belongs to a different session)
|
|
889
|
+
}
|
|
719
890
|
|
|
720
891
|
// Keep last 20 exchanges to avoid unbounded growth
|
|
721
892
|
existing.exchanges.push({
|
|
@@ -729,6 +900,7 @@ async function runIngest() {
|
|
|
729
900
|
|
|
730
901
|
const ctxData = JSON.stringify({
|
|
731
902
|
agentName: name,
|
|
903
|
+
baseName: name,
|
|
732
904
|
project: process.cwd().split(/[\\/]/).pop(),
|
|
733
905
|
updatedAt: new Date().toISOString(),
|
|
734
906
|
sessionId,
|
|
@@ -759,6 +931,167 @@ function findAgentNameForSession(sessionId) {
|
|
|
759
931
|
return null;
|
|
760
932
|
}
|
|
761
933
|
|
|
934
|
+
// ============================================================
|
|
935
|
+
// CLI: --check-messages (UserPromptSubmit hook mode)
|
|
936
|
+
// ============================================================
|
|
937
|
+
// Runs on every prompt. Checks for new messages addressed to this agent
|
|
938
|
+
// and outputs them as additionalContext so Claude sees them automatically.
|
|
939
|
+
// Usage: node server.mjs --check-messages
|
|
940
|
+
|
|
941
|
+
async function runCheckMessages() {
|
|
942
|
+
ensureDirs();
|
|
943
|
+
|
|
944
|
+
// Read stdin for hook context
|
|
945
|
+
const chunks = [];
|
|
946
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
947
|
+
const raw = Buffer.concat(chunks).toString('utf8');
|
|
948
|
+
|
|
949
|
+
let data;
|
|
950
|
+
try { data = JSON.parse(raw); } catch { process.exit(0); }
|
|
951
|
+
|
|
952
|
+
const cwd = data.cwd || process.cwd();
|
|
953
|
+
const name = resolveAgentName(cwd);
|
|
954
|
+
if (!name) process.exit(0);
|
|
955
|
+
|
|
956
|
+
// Read cursor and scan for new messages
|
|
957
|
+
const cursor = readCursor(name);
|
|
958
|
+
const { messages, newCursor } = readMessagesForAgent(name, cursor);
|
|
959
|
+
|
|
960
|
+
// Update cursor even if no messages (advances past scanned files)
|
|
961
|
+
if (newCursor > cursor) {
|
|
962
|
+
writeCursor(name, newCursor);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
if (messages.length === 0) {
|
|
966
|
+
process.exit(0); // No output = no context injection
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// Format messages for Claude
|
|
970
|
+
const formatted = messages.map(m => {
|
|
971
|
+
const from = m.fromName || m.from;
|
|
972
|
+
const age = Math.round((Date.now() - new Date(m.timestamp).getTime()) / 1000);
|
|
973
|
+
const ageStr = age < 60 ? `${age}s ago` : `${Math.round(age / 60)}min ago`;
|
|
974
|
+
return `[MESSAGE FROM ${from}]: ${m.content} (${ageStr})`;
|
|
975
|
+
}).join('\n\n');
|
|
976
|
+
|
|
977
|
+
const output = {
|
|
978
|
+
hookSpecificOutput: {
|
|
979
|
+
hookEventName: data.hook_event_name || 'UserPromptSubmit',
|
|
980
|
+
additionalContext: `--- INCOMING AGENT MESSAGES (${messages.length}) ---\n${formatted}\n--- END AGENT MESSAGES ---\nYou received ${messages.length} message(s) from other agents. Read and respond to them. Use send_message to reply if needed.`
|
|
981
|
+
}
|
|
982
|
+
};
|
|
983
|
+
|
|
984
|
+
process.stdout.write(JSON.stringify(output));
|
|
985
|
+
process.exit(0);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// ============================================================
|
|
989
|
+
// CLI: --bootstrap-context (SessionStart hook mode)
|
|
990
|
+
// ============================================================
|
|
991
|
+
// Runs once when a new session starts. Reads all shared contexts
|
|
992
|
+
// from other agents and any pending messages, outputs them so
|
|
993
|
+
// Claude starts the session knowing what everyone else knows.
|
|
994
|
+
// Usage: node server.mjs --bootstrap-context
|
|
995
|
+
|
|
996
|
+
async function runBootstrapContext() {
|
|
997
|
+
ensureDirs();
|
|
998
|
+
|
|
999
|
+
// Read stdin for hook context
|
|
1000
|
+
const chunks = [];
|
|
1001
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
1002
|
+
const raw = Buffer.concat(chunks).toString('utf8');
|
|
1003
|
+
|
|
1004
|
+
let data;
|
|
1005
|
+
try { data = JSON.parse(raw); } catch { process.exit(0); }
|
|
1006
|
+
|
|
1007
|
+
const cwd = data.cwd || process.cwd();
|
|
1008
|
+
const myName = resolveAgentName(cwd);
|
|
1009
|
+
if (!myName) process.exit(0);
|
|
1010
|
+
|
|
1011
|
+
const myNameLower = myName.toLowerCase();
|
|
1012
|
+
|
|
1013
|
+
// Get active agents (excluding self)
|
|
1014
|
+
let activeAgents = [];
|
|
1015
|
+
try {
|
|
1016
|
+
const agentFiles = readdirSync(AGENTS_DIR).filter(f => f.endsWith('.json'));
|
|
1017
|
+
for (const file of agentFiles) {
|
|
1018
|
+
try {
|
|
1019
|
+
const agent = JSON.parse(readFileSync(resolve(AGENTS_DIR, file), 'utf8'));
|
|
1020
|
+
if (agent.name && agent.name.toLowerCase() !== myNameLower && isProcessAlive(agent.pid)) {
|
|
1021
|
+
activeAgents.push(agent);
|
|
1022
|
+
}
|
|
1023
|
+
} catch {}
|
|
1024
|
+
}
|
|
1025
|
+
} catch {}
|
|
1026
|
+
|
|
1027
|
+
// Get all shared contexts (excluding own session only, NOT all same-name sessions)
|
|
1028
|
+
// Use sessionId to filter — other sessions with the same name should still be visible
|
|
1029
|
+
const mySessionId = data.session_id || '';
|
|
1030
|
+
const contexts = readAllContextFiles().filter(c => {
|
|
1031
|
+
// Exclude our own session's context, but keep other sessions even if same name
|
|
1032
|
+
if (c.sessionId && mySessionId && c.sessionId === mySessionId) return false;
|
|
1033
|
+
// Fallback: if no sessionId, exclude exact agentId match
|
|
1034
|
+
if (c.agentId && c.agentId === myName) return false;
|
|
1035
|
+
return true;
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
// Check for pending messages too
|
|
1039
|
+
const cursor = readCursor(myName);
|
|
1040
|
+
const { messages, newCursor } = readMessagesForAgent(myName, cursor);
|
|
1041
|
+
if (newCursor > cursor) {
|
|
1042
|
+
writeCursor(myName, newCursor);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// Nothing to bootstrap
|
|
1046
|
+
if (activeAgents.length === 0 && contexts.length === 0 && messages.length === 0) {
|
|
1047
|
+
process.exit(0);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// Build the bootstrap output
|
|
1051
|
+
const parts = [];
|
|
1052
|
+
|
|
1053
|
+
if (activeAgents.length > 0) {
|
|
1054
|
+
parts.push(`ACTIVE AGENTS: ${activeAgents.map(a => `${a.name} (${a.project})`).join(', ')}`);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
for (const ctx of contexts) {
|
|
1058
|
+
const ageMin = Math.round((Date.now() - new Date(ctx.updatedAt).getTime()) / 1000 / 60);
|
|
1059
|
+
let summary = '';
|
|
1060
|
+
if (ctx.content) {
|
|
1061
|
+
summary = ctx.content.slice(0, 3000);
|
|
1062
|
+
} else if (ctx.exchanges && ctx.exchanges.length > 0) {
|
|
1063
|
+
// Show last 3 exchanges as summary
|
|
1064
|
+
const recent = ctx.exchanges.slice(-3);
|
|
1065
|
+
summary = recent.map(e =>
|
|
1066
|
+
`User: ${(e.user || '').slice(0, 200)}\nAssistant: ${(e.assistant || '').slice(0, 500)}`
|
|
1067
|
+
).join('\n---\n');
|
|
1068
|
+
}
|
|
1069
|
+
if (summary) {
|
|
1070
|
+
parts.push(`--- CONTEXT FROM ${ctx.agentName} (${ctx.project || 'unknown'}, updated ${ageMin}min ago) ---\n${summary}`);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
if (messages.length > 0) {
|
|
1075
|
+
const formatted = messages.map(m => {
|
|
1076
|
+
const from = m.fromName || m.from;
|
|
1077
|
+
const age = Math.round((Date.now() - new Date(m.timestamp).getTime()) / 1000);
|
|
1078
|
+
const ageStr = age < 60 ? `${age}s ago` : `${Math.round(age / 60)}min ago`;
|
|
1079
|
+
return `[MESSAGE FROM ${from}]: ${m.content} (${ageStr})`;
|
|
1080
|
+
}).join('\n\n');
|
|
1081
|
+
parts.push(`--- PENDING MESSAGES (${messages.length}) ---\n${formatted}`);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
const output = {
|
|
1085
|
+
hookSpecificOutput: {
|
|
1086
|
+
hookEventName: data.hook_event_name || 'SessionStart',
|
|
1087
|
+
additionalContext: `--- AGENT BRIDGE BOOTSTRAP ---\nYou are "${myName}". The agent-bridge connects you with other Claude Code sessions. Messages from other agents are delivered automatically before each prompt.\n\n${parts.join('\n\n')}\n--- END BOOTSTRAP ---`
|
|
1088
|
+
}
|
|
1089
|
+
};
|
|
1090
|
+
|
|
1091
|
+
process.stdout.write(JSON.stringify(output));
|
|
1092
|
+
process.exit(0);
|
|
1093
|
+
}
|
|
1094
|
+
|
|
762
1095
|
// ============================================================
|
|
763
1096
|
// Main
|
|
764
1097
|
// ============================================================
|
|
@@ -767,10 +1100,26 @@ if (process.argv.includes('--capture-prompt')) {
|
|
|
767
1100
|
runCapturePrompt().catch(() => process.exit(0));
|
|
768
1101
|
} else if (process.argv.includes('--ingest')) {
|
|
769
1102
|
runIngest().catch(() => process.exit(0));
|
|
1103
|
+
} else if (process.argv.includes('--check-messages')) {
|
|
1104
|
+
runCheckMessages().catch(() => process.exit(0));
|
|
1105
|
+
} else if (process.argv.includes('--bootstrap-context')) {
|
|
1106
|
+
runBootstrapContext().catch(() => process.exit(0));
|
|
770
1107
|
} else {
|
|
771
1108
|
async function main() {
|
|
772
1109
|
ensureDirs();
|
|
773
1110
|
cleanOldMessages();
|
|
1111
|
+
|
|
1112
|
+
// Auto-suffix name if env var conflicts with an existing active agent
|
|
1113
|
+
if (agentName) {
|
|
1114
|
+
const existing = getAllAgents().filter(a => a.status === 'active' && a.name?.toLowerCase() === agentName.toLowerCase());
|
|
1115
|
+
if (existing.length > 0) {
|
|
1116
|
+
const allNames = getAllAgents().map(a => a.name?.toLowerCase()).filter(Boolean);
|
|
1117
|
+
let suffix = 2;
|
|
1118
|
+
while (allNames.includes(`${agentName.toLowerCase()}-${suffix}`)) suffix++;
|
|
1119
|
+
agentName = `${agentBaseName}-${suffix}`;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
774
1123
|
writeAgentFile();
|
|
775
1124
|
startHeartbeat();
|
|
776
1125
|
|