agent-bridge-mcp 1.0.0 → 1.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/README.md +77 -7
- package/package.json +1 -1
- package/server.mjs +282 -25
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# agent-bridge-mcp
|
|
2
2
|
|
|
3
|
-
MCP server that enables multiple Claude Code sessions to discover each other
|
|
3
|
+
MCP server that enables multiple Claude Code sessions to discover each other, exchange messages, and share working context through a shared filesystem bus.
|
|
4
4
|
|
|
5
|
-
Open multiple Claude Code tabs
|
|
5
|
+
Open multiple Claude Code tabs across different projects. Each tab can register a name, see other active tabs, send/receive messages, and share accumulated knowledge — no external services required. Context persists after sessions end.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -36,30 +36,37 @@ Add to your Claude Code MCP config:
|
|
|
36
36
|
}
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
**
|
|
39
|
+
**Per-project** (`.mcp.json` in project root) — with auto-registration:
|
|
40
40
|
|
|
41
41
|
```json
|
|
42
42
|
{
|
|
43
43
|
"mcpServers": {
|
|
44
44
|
"agent-bridge": {
|
|
45
45
|
"command": "npx",
|
|
46
|
-
"args": ["agent-bridge-mcp"]
|
|
46
|
+
"args": ["agent-bridge-mcp"],
|
|
47
|
+
"env": {
|
|
48
|
+
"AGENT_BRIDGE_NAME": "my-agent-name"
|
|
49
|
+
}
|
|
47
50
|
}
|
|
48
51
|
}
|
|
49
52
|
}
|
|
50
53
|
```
|
|
51
54
|
|
|
55
|
+
Setting `AGENT_BRIDGE_NAME` automatically registers the agent on startup — no need to call `register_agent` manually.
|
|
56
|
+
|
|
52
57
|
Then reload your editor window.
|
|
53
58
|
|
|
54
59
|
## Tools
|
|
55
60
|
|
|
56
61
|
| Tool | Description |
|
|
57
62
|
|------|-------------|
|
|
58
|
-
| `register_agent` | Name yourself (e.g., "frontend-dev", "reviewer") |
|
|
63
|
+
| `register_agent` | Name yourself (e.g., "frontend-dev", "reviewer"). Not needed if `AGENT_BRIDGE_NAME` is set. |
|
|
59
64
|
| `list_agents` | See all active agents across all tabs |
|
|
60
65
|
| `send_message` | Message a specific agent by name or ID |
|
|
61
66
|
| `read_messages` | Check for unread messages |
|
|
62
67
|
| `broadcast` | Message all active agents |
|
|
68
|
+
| `share_context` | Dump your accumulated working knowledge so other agents can read it. Persists after session ends. |
|
|
69
|
+
| `get_context` | Read shared context from another agent by name, or get all shared contexts. Works even if that agent is dead. |
|
|
63
70
|
|
|
64
71
|
## How It Works
|
|
65
72
|
|
|
@@ -67,9 +74,11 @@ Each Claude Code tab spawns its own MCP server process. Since they can't share m
|
|
|
67
74
|
|
|
68
75
|
- **Agents** register by writing a JSON file to a shared `agents/` directory
|
|
69
76
|
- **Messages** are individual JSON files in a `messages/` directory (atomic creation, no corruption)
|
|
77
|
+
- **Context** is stored per-agent in a `context/` directory, keyed by agent name (not session ID), so knowledge survives session restarts
|
|
70
78
|
- **Heartbeat** every 15s keeps agent registrations fresh
|
|
71
79
|
- **Dead agents** are auto-cleaned via PID liveness checks and heartbeat staleness
|
|
72
80
|
- **Messages expire** after 1 hour (configurable)
|
|
81
|
+
- **Context persists** indefinitely until overwritten by the same agent name
|
|
73
82
|
|
|
74
83
|
```
|
|
75
84
|
Data directory:
|
|
@@ -77,10 +86,14 @@ Data directory:
|
|
|
77
86
|
agent-a3f1c9.json # { id, name, project, pid, lastHeartbeat }
|
|
78
87
|
messages/
|
|
79
88
|
1740524430000-x7k2f9.json # { from, to, content, timestamp }
|
|
89
|
+
context/
|
|
90
|
+
nova-main.json # { agentName, agentId, project, updatedAt, content }
|
|
80
91
|
```
|
|
81
92
|
|
|
82
93
|
## Example Usage
|
|
83
94
|
|
|
95
|
+
### Messaging
|
|
96
|
+
|
|
84
97
|
**Tab 1:**
|
|
85
98
|
> Register as "frontend-dev"
|
|
86
99
|
|
|
@@ -92,16 +105,28 @@ Data directory:
|
|
|
92
105
|
|
|
93
106
|
**Tab 2:**
|
|
94
107
|
> Check messages
|
|
95
|
-
|
|
96
108
|
> Got message from frontend-dev: "I changed the auth response type, update your endpoint handlers"
|
|
97
109
|
|
|
110
|
+
### Context Sharing
|
|
111
|
+
|
|
112
|
+
**Project A (nova-main):**
|
|
113
|
+
> Share context: "Working on auth flow. Modified auth.ts and login.tsx. Using JWT with refresh tokens. API endpoint is /api/auth/login."
|
|
114
|
+
|
|
115
|
+
**Project B (emergence-main):**
|
|
116
|
+
> Get context from nova-main
|
|
117
|
+
|
|
118
|
+
> Returns: "Working on auth flow. Modified auth.ts and login.tsx. Using JWT with refresh tokens. API endpoint is /api/auth/login."
|
|
119
|
+
|
|
120
|
+
Context persists even after nova-main's session ends — emergence-main can read it anytime.
|
|
121
|
+
|
|
98
122
|
## Configuration
|
|
99
123
|
|
|
100
124
|
All settings are optional. Defaults work out of the box.
|
|
101
125
|
|
|
102
126
|
| Environment Variable | Default | Description |
|
|
103
127
|
|---------------------|---------|-------------|
|
|
104
|
-
| `
|
|
128
|
+
| `AGENT_BRIDGE_NAME` | *(none)* | Auto-register with this name on startup |
|
|
129
|
+
| `AGENT_BRIDGE_DATA_DIR` | Platform-specific (see below) | Directory for agent, message, and context files |
|
|
105
130
|
| `AGENT_BRIDGE_HEARTBEAT_MS` | `15000` | Heartbeat interval in milliseconds |
|
|
106
131
|
| `AGENT_BRIDGE_DEAD_MS` | `300000` | Time before stale agents are removed |
|
|
107
132
|
| `AGENT_BRIDGE_MESSAGE_TTL_MS` | `3600000` | Time before old messages are deleted |
|
|
@@ -121,6 +146,7 @@ Pass env vars through your MCP config:
|
|
|
121
146
|
"command": "npx",
|
|
122
147
|
"args": ["agent-bridge-mcp"],
|
|
123
148
|
"env": {
|
|
149
|
+
"AGENT_BRIDGE_NAME": "my-agent",
|
|
124
150
|
"AGENT_BRIDGE_MESSAGE_TTL_MS": "7200000"
|
|
125
151
|
}
|
|
126
152
|
}
|
|
@@ -128,6 +154,50 @@ Pass env vars through your MCP config:
|
|
|
128
154
|
}
|
|
129
155
|
```
|
|
130
156
|
|
|
157
|
+
## Automatic Context Sharing (Stop Hook)
|
|
158
|
+
|
|
159
|
+
The `--ingest` flag lets you auto-capture every conversation exchange into the context directory — no manual `share_context` calls needed.
|
|
160
|
+
|
|
161
|
+
**How it works:** Claude Code fires a [Stop hook](https://docs.anthropic.com/en/docs/claude-code/hooks) after every agent response. The hook pipes `session_id` and `last_assistant_message` via stdin. `agent-bridge-mcp --ingest` reads this, pairs it with the user prompt (saved by a `UserPromptSubmit` hook), strips system tags, and appends the exchange to the agent's context file.
|
|
162
|
+
|
|
163
|
+
**Setup:**
|
|
164
|
+
|
|
165
|
+
1. Add a `UserPromptSubmit` hook to save user prompts (required for `--ingest` to capture both sides):
|
|
166
|
+
|
|
167
|
+
Add to `~/.claude/settings.json`:
|
|
168
|
+
```json
|
|
169
|
+
{
|
|
170
|
+
"hooks": {
|
|
171
|
+
"UserPromptSubmit": [
|
|
172
|
+
{
|
|
173
|
+
"hooks": [
|
|
174
|
+
{
|
|
175
|
+
"type": "command",
|
|
176
|
+
"command": "npx agent-bridge-mcp --capture-prompt",
|
|
177
|
+
"timeout": 5
|
|
178
|
+
}
|
|
179
|
+
]
|
|
180
|
+
}
|
|
181
|
+
],
|
|
182
|
+
"Stop": [
|
|
183
|
+
{
|
|
184
|
+
"hooks": [
|
|
185
|
+
{
|
|
186
|
+
"type": "command",
|
|
187
|
+
"command": "npx agent-bridge-mcp --ingest",
|
|
188
|
+
"timeout": 10
|
|
189
|
+
}
|
|
190
|
+
]
|
|
191
|
+
}
|
|
192
|
+
]
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Set `AGENT_BRIDGE_NAME` in your project's `.mcp.json` so the ingest knows which agent name to write as.
|
|
198
|
+
|
|
199
|
+
Context is stored as the last 20 exchanges per agent, accessible via `get_context`.
|
|
200
|
+
|
|
131
201
|
## Requirements
|
|
132
202
|
|
|
133
203
|
- Node.js >= 18
|
package/package.json
CHANGED
package/server.mjs
CHANGED
|
@@ -34,6 +34,7 @@ function getDefaultDataDir() {
|
|
|
34
34
|
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
|
+
const CONTEXT_DIR = resolve(BRIDGE_DIR, 'context');
|
|
37
38
|
const HEARTBEAT_INTERVAL_MS = parseInt(process.env.AGENT_BRIDGE_HEARTBEAT_MS || '15000', 10);
|
|
38
39
|
const STALE_THRESHOLD_MS = HEARTBEAT_INTERVAL_MS * 3;
|
|
39
40
|
const DEAD_THRESHOLD_MS = parseInt(process.env.AGENT_BRIDGE_DEAD_MS || '300000', 10);
|
|
@@ -44,7 +45,7 @@ const MESSAGE_TTL_MS = parseInt(process.env.AGENT_BRIDGE_MESSAGE_TTL_MS || '3600
|
|
|
44
45
|
// ============================================================
|
|
45
46
|
|
|
46
47
|
const agentId = `agent-${randomBytes(3).toString('hex')}`;
|
|
47
|
-
let agentName = null;
|
|
48
|
+
let agentName = process.env.AGENT_BRIDGE_NAME || null;
|
|
48
49
|
const startedAt = new Date().toISOString();
|
|
49
50
|
let lastReadTimestamp = Date.now(); // only read messages after we start
|
|
50
51
|
let heartbeatTimer = null;
|
|
@@ -56,6 +57,7 @@ let heartbeatTimer = null;
|
|
|
56
57
|
function ensureDirs() {
|
|
57
58
|
mkdirSync(AGENTS_DIR, { recursive: true });
|
|
58
59
|
mkdirSync(MESSAGES_DIR, { recursive: true });
|
|
60
|
+
mkdirSync(CONTEXT_DIR, { recursive: true });
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
// ============================================================
|
|
@@ -380,6 +382,87 @@ async function handleBroadcast(args) {
|
|
|
380
382
|
};
|
|
381
383
|
}
|
|
382
384
|
|
|
385
|
+
// ============================================================
|
|
386
|
+
// Context Operations
|
|
387
|
+
// ============================================================
|
|
388
|
+
|
|
389
|
+
function writeContextFile(name, content) {
|
|
390
|
+
const safeName = name.replace(/[^a-zA-Z0-9_-]/g, '_').toLowerCase();
|
|
391
|
+
const data = JSON.stringify({
|
|
392
|
+
agentName: name,
|
|
393
|
+
agentId,
|
|
394
|
+
project: process.cwd().split(/[\\/]/).pop(),
|
|
395
|
+
updatedAt: new Date().toISOString(),
|
|
396
|
+
content
|
|
397
|
+
}, null, 2);
|
|
398
|
+
const targetPath = resolve(CONTEXT_DIR, `${safeName}.json`);
|
|
399
|
+
const tmpPath = targetPath + '.tmp';
|
|
400
|
+
try {
|
|
401
|
+
writeFileSync(tmpPath, data);
|
|
402
|
+
renameSync(tmpPath, targetPath);
|
|
403
|
+
} catch (err) {
|
|
404
|
+
try { writeFileSync(targetPath, data); } catch {}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function readAllContextFiles() {
|
|
409
|
+
let files;
|
|
410
|
+
try {
|
|
411
|
+
files = readdirSync(CONTEXT_DIR).filter(f => f.endsWith('.json'));
|
|
412
|
+
} catch {
|
|
413
|
+
return [];
|
|
414
|
+
}
|
|
415
|
+
const contexts = [];
|
|
416
|
+
for (const file of files) {
|
|
417
|
+
try {
|
|
418
|
+
const ctx = JSON.parse(readFileSync(resolve(CONTEXT_DIR, file), 'utf8'));
|
|
419
|
+
contexts.push(ctx);
|
|
420
|
+
} catch {
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return contexts;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async function handleShareContext(args) {
|
|
428
|
+
const { content } = args;
|
|
429
|
+
if (!content) return { error: 'Content is required — dump what you know' };
|
|
430
|
+
if (!agentName) return { error: 'Register with a name first (register_agent) before sharing context' };
|
|
431
|
+
|
|
432
|
+
writeContextFile(agentName, content);
|
|
433
|
+
|
|
434
|
+
return {
|
|
435
|
+
shared: true,
|
|
436
|
+
agentName,
|
|
437
|
+
project: process.cwd().split(/[\\/]/).pop(),
|
|
438
|
+
updatedAt: new Date().toISOString(),
|
|
439
|
+
contentLength: content.length
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
async function handleGetContext(args) {
|
|
444
|
+
const { from } = args;
|
|
445
|
+
|
|
446
|
+
if (from) {
|
|
447
|
+
// Get context from a specific agent
|
|
448
|
+
const safeName = from.replace(/[^a-zA-Z0-9_-]/g, '_').toLowerCase();
|
|
449
|
+
const filePath = resolve(CONTEXT_DIR, `${safeName}.json`);
|
|
450
|
+
try {
|
|
451
|
+
const ctx = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
452
|
+
return { found: true, context: ctx };
|
|
453
|
+
} catch {
|
|
454
|
+
return { found: false, error: `No shared context found for "${from}"` };
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Get all shared contexts
|
|
459
|
+
const contexts = readAllContextFiles();
|
|
460
|
+
return {
|
|
461
|
+
contexts,
|
|
462
|
+
count: contexts.length
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
383
466
|
// ============================================================
|
|
384
467
|
// Tool Definitions
|
|
385
468
|
// ============================================================
|
|
@@ -451,6 +534,33 @@ const TOOLS = [
|
|
|
451
534
|
},
|
|
452
535
|
required: ['content']
|
|
453
536
|
}
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
name: 'share_context',
|
|
540
|
+
description: 'Share your accumulated working knowledge so other agents (even in other projects) can read it. Write a comprehensive dump of everything you know — files read, decisions made, key facts, current state. Context persists after your session ends.',
|
|
541
|
+
inputSchema: {
|
|
542
|
+
type: 'object',
|
|
543
|
+
properties: {
|
|
544
|
+
content: {
|
|
545
|
+
type: 'string',
|
|
546
|
+
description: 'Freeform text dump of everything you know that would be useful to another agent picking up where you left off'
|
|
547
|
+
}
|
|
548
|
+
},
|
|
549
|
+
required: ['content']
|
|
550
|
+
}
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
name: 'get_context',
|
|
554
|
+
description: 'Read shared context from another agent. Works even if that agent is no longer active — context persists across sessions. Call with no arguments to see all available contexts.',
|
|
555
|
+
inputSchema: {
|
|
556
|
+
type: 'object',
|
|
557
|
+
properties: {
|
|
558
|
+
from: {
|
|
559
|
+
type: 'string',
|
|
560
|
+
description: 'Name of the agent whose context you want to read (e.g., "nova-main"). Omit to get all shared contexts.'
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
454
564
|
}
|
|
455
565
|
];
|
|
456
566
|
|
|
@@ -465,6 +575,8 @@ async function handleToolCall(name, args) {
|
|
|
465
575
|
case 'send_message': return handleSendMessage(args);
|
|
466
576
|
case 'read_messages': return handleReadMessages(args);
|
|
467
577
|
case 'broadcast': return handleBroadcast(args);
|
|
578
|
+
case 'share_context': return handleShareContext(args);
|
|
579
|
+
case 'get_context': return handleGetContext(args);
|
|
468
580
|
default: return { error: `Unknown tool: ${name}` };
|
|
469
581
|
}
|
|
470
582
|
}
|
|
@@ -474,7 +586,7 @@ async function handleToolCall(name, args) {
|
|
|
474
586
|
// ============================================================
|
|
475
587
|
|
|
476
588
|
const server = new Server(
|
|
477
|
-
{ name: 'agent-bridge', version: '1.
|
|
589
|
+
{ name: 'agent-bridge', version: '1.2.0' },
|
|
478
590
|
{ capabilities: { tools: {} } }
|
|
479
591
|
);
|
|
480
592
|
|
|
@@ -509,34 +621,179 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
509
621
|
});
|
|
510
622
|
|
|
511
623
|
// ============================================================
|
|
512
|
-
//
|
|
624
|
+
// CLI: --capture-prompt (UserPromptSubmit hook mode)
|
|
513
625
|
// ============================================================
|
|
626
|
+
// Saves user prompt to temp file for --ingest to pair with.
|
|
627
|
+
// Usage: node server.mjs --capture-prompt
|
|
628
|
+
|
|
629
|
+
async function runCapturePrompt() {
|
|
630
|
+
const chunks = [];
|
|
631
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
632
|
+
const raw = Buffer.concat(chunks).toString('utf8');
|
|
633
|
+
|
|
634
|
+
let data;
|
|
635
|
+
try { data = JSON.parse(raw); } catch { process.exit(0); }
|
|
636
|
+
|
|
637
|
+
const sessionId = data.session_id || '';
|
|
638
|
+
const prompt = data.prompt || '';
|
|
639
|
+
if (!sessionId || !prompt) process.exit(0);
|
|
640
|
+
|
|
641
|
+
// Strip system tags
|
|
642
|
+
const tagPatterns = [
|
|
643
|
+
/<system-reminder>[\s\S]*?<\/system-reminder>/g,
|
|
644
|
+
/<ide_opened_file>[\s\S]*?<\/ide_opened_file>/g,
|
|
645
|
+
/<ide_selection>[\s\S]*?<\/ide_selection>/g,
|
|
646
|
+
/<task-notification>[\s\S]*?<\/task-notification>/g,
|
|
647
|
+
];
|
|
648
|
+
let clean = prompt;
|
|
649
|
+
for (const pat of tagPatterns) clean = clean.replace(pat, '');
|
|
650
|
+
clean = clean.replace(/\n\s*\n/g, '\n').trim();
|
|
651
|
+
|
|
652
|
+
if (clean.length < 5) process.exit(0);
|
|
653
|
+
|
|
654
|
+
const tmp = tmpdir();
|
|
655
|
+
writeFileSync(resolve(tmp, `agent-bridge-prompt-${sessionId}`), clean, 'utf8');
|
|
656
|
+
process.exit(0);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// ============================================================
|
|
660
|
+
// CLI: --ingest (Stop hook mode)
|
|
661
|
+
// ============================================================
|
|
662
|
+
// Called by Claude Code Stop hook. Reads stdin JSON with
|
|
663
|
+
// session_id + last_assistant_message, writes to context dir.
|
|
664
|
+
// Usage: node server.mjs --ingest
|
|
514
665
|
|
|
515
|
-
async function
|
|
666
|
+
async function runIngest() {
|
|
516
667
|
ensureDirs();
|
|
517
|
-
cleanOldMessages();
|
|
518
|
-
writeAgentFile();
|
|
519
|
-
startHeartbeat();
|
|
520
668
|
|
|
521
|
-
//
|
|
522
|
-
const
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
669
|
+
// Read stdin
|
|
670
|
+
const chunks = [];
|
|
671
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
672
|
+
const raw = Buffer.concat(chunks).toString('utf8');
|
|
673
|
+
|
|
674
|
+
let data;
|
|
675
|
+
try {
|
|
676
|
+
data = JSON.parse(raw);
|
|
677
|
+
} catch {
|
|
678
|
+
process.exit(0);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const sessionId = data.session_id || '';
|
|
682
|
+
const assistantMsg = data.last_assistant_message || '';
|
|
683
|
+
if (!sessionId || !assistantMsg) process.exit(0);
|
|
684
|
+
|
|
685
|
+
// Read user prompt saved by --capture-prompt hook
|
|
686
|
+
const tmp = tmpdir();
|
|
687
|
+
const promptPath = resolve(tmp, `agent-bridge-prompt-${sessionId}`);
|
|
688
|
+
let userMsg = '';
|
|
689
|
+
try {
|
|
690
|
+
userMsg = readFileSync(promptPath, 'utf8');
|
|
691
|
+
} catch {}
|
|
692
|
+
|
|
693
|
+
// Strip system tags
|
|
694
|
+
const tagPatterns = [
|
|
695
|
+
/<system-reminder>[\s\S]*?<\/system-reminder>/g,
|
|
696
|
+
/<task-notification>[\s\S]*?<\/task-notification>/g,
|
|
697
|
+
/<ide_opened_file>[\s\S]*?<\/ide_opened_file>/g,
|
|
698
|
+
/<ide_selection>[\s\S]*?<\/ide_selection>/g,
|
|
699
|
+
];
|
|
700
|
+
let cleanAssistant = assistantMsg;
|
|
701
|
+
for (const pat of tagPatterns) cleanAssistant = cleanAssistant.replace(pat, '');
|
|
702
|
+
cleanAssistant = cleanAssistant.replace(/\n\s*\n/g, '\n').trim();
|
|
703
|
+
|
|
704
|
+
if (!cleanAssistant || cleanAssistant.length < 50) process.exit(0);
|
|
705
|
+
|
|
706
|
+
// Determine agent name from env or from active agent files
|
|
707
|
+
const name = process.env.AGENT_BRIDGE_NAME || findAgentNameForSession(sessionId);
|
|
708
|
+
if (!name) process.exit(0);
|
|
709
|
+
|
|
710
|
+
// Read existing context and append new exchange
|
|
711
|
+
const safeName = name.replace(/[^a-zA-Z0-9_-]/g, '_').toLowerCase();
|
|
712
|
+
const ctxPath = resolve(CONTEXT_DIR, `${safeName}.json`);
|
|
713
|
+
let existing = { exchanges: [] };
|
|
714
|
+
try {
|
|
715
|
+
const prev = JSON.parse(readFileSync(ctxPath, 'utf8'));
|
|
716
|
+
if (prev.exchanges) existing = prev;
|
|
717
|
+
else if (prev.content) existing = { exchanges: [{ role: 'context', content: prev.content }] };
|
|
718
|
+
} catch {}
|
|
719
|
+
|
|
720
|
+
// Keep last 20 exchanges to avoid unbounded growth
|
|
721
|
+
existing.exchanges.push({
|
|
722
|
+
timestamp: new Date().toISOString(),
|
|
723
|
+
user: userMsg.slice(0, 2000),
|
|
724
|
+
assistant: cleanAssistant.slice(0, 5000)
|
|
725
|
+
});
|
|
726
|
+
if (existing.exchanges.length > 20) {
|
|
727
|
+
existing.exchanges = existing.exchanges.slice(-20);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const ctxData = JSON.stringify({
|
|
731
|
+
agentName: name,
|
|
732
|
+
project: process.cwd().split(/[\\/]/).pop(),
|
|
733
|
+
updatedAt: new Date().toISOString(),
|
|
734
|
+
sessionId,
|
|
735
|
+
exchanges: existing.exchanges
|
|
736
|
+
}, null, 2);
|
|
529
737
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
738
|
+
const tmpPath = ctxPath + '.tmp';
|
|
739
|
+
try {
|
|
740
|
+
writeFileSync(tmpPath, ctxData);
|
|
741
|
+
renameSync(tmpPath, ctxPath);
|
|
742
|
+
} catch {
|
|
743
|
+
try { writeFileSync(ctxPath, ctxData); } catch {}
|
|
744
|
+
}
|
|
533
745
|
|
|
534
|
-
|
|
535
|
-
console.error(`[AgentBridge] Agent ID: ${agentId}`);
|
|
536
|
-
console.error(`[AgentBridge] Data dir: ${BRIDGE_DIR}`);
|
|
746
|
+
process.exit(0);
|
|
537
747
|
}
|
|
538
748
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
749
|
+
function findAgentNameForSession(sessionId) {
|
|
750
|
+
// Look through agent files to find one from this process's cwd
|
|
751
|
+
try {
|
|
752
|
+
const files = readdirSync(AGENTS_DIR).filter(f => f.endsWith('.json'));
|
|
753
|
+
const cwd = process.cwd();
|
|
754
|
+
for (const file of files) {
|
|
755
|
+
const agent = readAgentFile(resolve(AGENTS_DIR, file));
|
|
756
|
+
if (agent && agent.name && agent.cwd === cwd) return agent.name;
|
|
757
|
+
}
|
|
758
|
+
} catch {}
|
|
759
|
+
return null;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// ============================================================
|
|
763
|
+
// Main
|
|
764
|
+
// ============================================================
|
|
765
|
+
|
|
766
|
+
if (process.argv.includes('--capture-prompt')) {
|
|
767
|
+
runCapturePrompt().catch(() => process.exit(0));
|
|
768
|
+
} else if (process.argv.includes('--ingest')) {
|
|
769
|
+
runIngest().catch(() => process.exit(0));
|
|
770
|
+
} else {
|
|
771
|
+
async function main() {
|
|
772
|
+
ensureDirs();
|
|
773
|
+
cleanOldMessages();
|
|
774
|
+
writeAgentFile();
|
|
775
|
+
startHeartbeat();
|
|
776
|
+
|
|
777
|
+
// Cleanup on exit
|
|
778
|
+
const cleanup = () => {
|
|
779
|
+
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
780
|
+
removeAgentFile();
|
|
781
|
+
};
|
|
782
|
+
process.on('exit', cleanup);
|
|
783
|
+
process.on('SIGTERM', () => { cleanup(); process.exit(0); });
|
|
784
|
+
process.on('SIGINT', () => { cleanup(); process.exit(0); });
|
|
785
|
+
|
|
786
|
+
// Connect stdio transport
|
|
787
|
+
const transport = new StdioServerTransport();
|
|
788
|
+
await server.connect(transport);
|
|
789
|
+
|
|
790
|
+
console.error(`[AgentBridge] Server running`);
|
|
791
|
+
console.error(`[AgentBridge] Agent ID: ${agentId}`);
|
|
792
|
+
console.error(`[AgentBridge] Data dir: ${BRIDGE_DIR}`);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
main().catch(err => {
|
|
796
|
+
console.error(`[AgentBridge] Fatal: ${err.message}`);
|
|
797
|
+
process.exit(1);
|
|
798
|
+
});
|
|
799
|
+
}
|