agent-bridge-mcp 1.1.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.
Files changed (3) hide show
  1. package/README.md +44 -0
  2. package/package.json +1 -1
  3. package/server.mjs +169 -24
package/README.md CHANGED
@@ -154,6 +154,50 @@ Pass env vars through your MCP config:
154
154
  }
155
155
  ```
156
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
+
157
201
  ## Requirements
158
202
 
159
203
  - Node.js >= 18
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-bridge-mcp",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "MCP server enabling communication between multiple Claude Code sessions via shared filesystem message bus",
5
5
  "type": "module",
6
6
  "main": "server.mjs",
package/server.mjs CHANGED
@@ -586,7 +586,7 @@ async function handleToolCall(name, args) {
586
586
  // ============================================================
587
587
 
588
588
  const server = new Server(
589
- { name: 'agent-bridge', version: '1.1.0' },
589
+ { name: 'agent-bridge', version: '1.2.0' },
590
590
  { capabilities: { tools: {} } }
591
591
  );
592
592
 
@@ -621,34 +621,179 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
621
621
  });
622
622
 
623
623
  // ============================================================
624
- // Main
624
+ // CLI: --capture-prompt (UserPromptSubmit hook mode)
625
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);
626
640
 
627
- async function main() {
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
665
+
666
+ async function runIngest() {
628
667
  ensureDirs();
629
- cleanOldMessages();
630
- writeAgentFile();
631
- startHeartbeat();
632
668
 
633
- // Cleanup on exit
634
- const cleanup = () => {
635
- if (heartbeatTimer) clearInterval(heartbeatTimer);
636
- removeAgentFile();
637
- };
638
- process.on('exit', cleanup);
639
- process.on('SIGTERM', () => { cleanup(); process.exit(0); });
640
- process.on('SIGINT', () => { cleanup(); process.exit(0); });
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);
641
737
 
642
- // Connect stdio transport
643
- const transport = new StdioServerTransport();
644
- await server.connect(transport);
738
+ const tmpPath = ctxPath + '.tmp';
739
+ try {
740
+ writeFileSync(tmpPath, ctxData);
741
+ renameSync(tmpPath, ctxPath);
742
+ } catch {
743
+ try { writeFileSync(ctxPath, ctxData); } catch {}
744
+ }
645
745
 
646
- console.error(`[AgentBridge] Server running`);
647
- console.error(`[AgentBridge] Agent ID: ${agentId}`);
648
- console.error(`[AgentBridge] Data dir: ${BRIDGE_DIR}`);
746
+ process.exit(0);
649
747
  }
650
748
 
651
- main().catch(err => {
652
- console.error(`[AgentBridge] Fatal: ${err.message}`);
653
- process.exit(1);
654
- });
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
+ }