molecule-core/scripts/claude-code-bridge.py
Hongming Wang 24fec62d7f initial commit — Molecule AI platform
Forked clean from public hackathon repo (Starfire-AgentTeam, BSL 1.1)
with full rebrand to Molecule AI under github.com/Molecule-AI/molecule-monorepo.

Brand: Starfire → Molecule AI.
Slug: starfire / agent-molecule → molecule.
Env vars: STARFIRE_* → MOLECULE_*.
Go module: github.com/agent-molecule/platform → github.com/Molecule-AI/molecule-monorepo/platform.
Python packages: starfire_plugin → molecule_plugin, starfire_agent → molecule_agent.
DB: agentmolecule → molecule.

History truncated; see public repo for prior commits and contributor
attribution. Verified green: go test -race ./... (platform), pytest
(workspace-template 1129 + sdk 132), vitest (canvas 352), build (mcp).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 11:55:37 -07:00

139 lines
4.8 KiB
Python

#!/usr/bin/env python3
"""External Workspace Bridge — plug any AI agent into Molecule AI via A2A.
Registers as an external workspace (no Docker container) and processes
incoming A2A messages using a configurable backend processor.
Usage:
# Claude Code backend (default)
python3 scripts/claude-code-bridge.py
# OpenAI API backend
python3 scripts/claude-code-bridge.py --processor openai --model gpt-4.1-mini
# Anthropic API backend
python3 scripts/claude-code-bridge.py --processor anthropic --model claude-sonnet-4-6
# Forward to any HTTP endpoint
python3 scripts/claude-code-bridge.py --processor http --url http://my-agent:8000/chat
# Echo (testing)
python3 scripts/claude-code-bridge.py --processor echo
# Management
python3 scripts/claude-code-bridge.py --inbox # Show messages
python3 scripts/claude-code-bridge.py --clear # Clear inbox
python3 scripts/claude-code-bridge.py --stop # Stop bridge
Environment variables:
PLATFORM_URL Platform API (default: http://localhost:8080)
BRIDGE_PORT Listen port (default: 9999)
BRIDGE_NAME Workspace name (default: Claude Code Advisor)
BRIDGE_PARENT_ID Parent workspace ID for hierarchy
OPENAI_API_KEY For --processor openai
ANTHROPIC_API_KEY For --processor anthropic
BRIDGE_FORWARD_URL For --processor http
"""
import argparse
import json
import logging
import os
import signal
import sys
from pathlib import Path
logging.basicConfig(level=logging.INFO, format="%(asctime)s [bridge] %(message)s")
logger = logging.getLogger("bridge")
BRIDGE_DIR = Path(__file__).parent.parent / ".claude-bridge"
INBOX = BRIDGE_DIR / "inbox.jsonl"
PID_FILE = BRIDGE_DIR / "bridge.pid"
BRIDGE_DIR.mkdir(exist_ok=True)
def show_inbox():
if not INBOX.exists():
print("No messages.")
return
unread = 0
for line in INBOX.read_text().splitlines():
try:
e = json.loads(line)
unread += 1
print(f" [{e.get('sender_name','?')}] {e.get('text','')[:120]}")
except json.JSONDecodeError:
continue
print(f"\n{unread} message(s)" if unread else "No messages.")
def main():
parser = argparse.ArgumentParser(description="External Workspace Bridge")
parser.add_argument("--processor", default="claude-code",
help="Backend processor: claude-code, openai, anthropic, http, echo")
parser.add_argument("--model", default="", help="Model name for the processor")
parser.add_argument("--url", default="", help="URL for http processor")
parser.add_argument("--name", default=os.environ.get("BRIDGE_NAME", "Claude Code Advisor"))
parser.add_argument("--role", default="CEO technical advisor — code review, architecture, debugging")
parser.add_argument("--port", type=int, default=int(os.environ.get("BRIDGE_PORT", "9999")))
parser.add_argument("--parent-id", default=os.environ.get("BRIDGE_PARENT_ID", ""))
parser.add_argument("--inbox", action="store_true", help="Show inbox")
parser.add_argument("--clear", action="store_true", help="Clear inbox")
parser.add_argument("--stop", action="store_true", help="Stop bridge")
args = parser.parse_args()
if args.inbox:
show_inbox()
return
if args.clear:
INBOX.unlink(missing_ok=True)
print("Inbox cleared")
return
if args.stop:
if PID_FILE.exists():
try:
os.kill(int(PID_FILE.read_text().strip()), signal.SIGTERM)
print("Bridge stopped")
except ProcessLookupError:
print("Bridge was not running")
PID_FILE.unlink(missing_ok=True)
return
# Import here to keep --inbox/--stop fast
from bridge.processor import create_processor
from bridge.platform import PlatformClient
from bridge.server import create_server
PID_FILE.write_text(str(os.getpid()))
# Create processor
kwargs = {}
if args.model:
kwargs["model"] = args.model
if args.url:
kwargs["url"] = args.url
processor = create_processor(args.processor, **kwargs)
logger.info(f"Processor: {args.processor} ({type(processor).__name__})")
# Register with platform
platform_url = os.environ.get("PLATFORM_URL", "http://localhost:8080")
client = PlatformClient(platform_url, args.port, BRIDGE_DIR)
ws_id = client.register(args.name, args.role, parent_id=args.parent_id)
client.start_heartbeat()
# Start A2A server
server = create_server(args.port, processor, INBOX, client.resolve_name)
logger.info(f"Listening on :{args.port} | Workspace: {ws_id}")
logger.info(f"Agents see '{args.name}' in list_peers → delegate_task to interact")
try:
server.serve_forever()
except KeyboardInterrupt:
server.shutdown()
PID_FILE.unlink(missing_ok=True)
if __name__ == "__main__":
main()