opencode-bridge 0.1.3__py3-none-any.whl
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.
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Install/uninstall opencode-bridge MCP server with Claude Code."""
|
|
3
|
+
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def install():
|
|
9
|
+
"""Register opencode-bridge as an MCP server with Claude Code."""
|
|
10
|
+
try:
|
|
11
|
+
result = subprocess.run(
|
|
12
|
+
["claude", "mcp", "add", "--transport", "stdio", "--scope", "user",
|
|
13
|
+
"opencode-bridge", "--", "opencode-bridge"],
|
|
14
|
+
capture_output=True,
|
|
15
|
+
text=True
|
|
16
|
+
)
|
|
17
|
+
if result.returncode == 0:
|
|
18
|
+
print("opencode-bridge registered with Claude Code")
|
|
19
|
+
print(result.stdout)
|
|
20
|
+
else:
|
|
21
|
+
if "already exists" in result.stderr.lower():
|
|
22
|
+
print("opencode-bridge already registered")
|
|
23
|
+
else:
|
|
24
|
+
print(f"Failed to register: {result.stderr}")
|
|
25
|
+
sys.exit(1)
|
|
26
|
+
except FileNotFoundError:
|
|
27
|
+
print("Claude Code CLI not found. Install from: https://claude.ai/download")
|
|
28
|
+
sys.exit(1)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def uninstall():
|
|
32
|
+
"""Remove opencode-bridge MCP server from Claude Code."""
|
|
33
|
+
try:
|
|
34
|
+
result = subprocess.run(
|
|
35
|
+
["claude", "mcp", "remove", "opencode-bridge"],
|
|
36
|
+
capture_output=True,
|
|
37
|
+
text=True
|
|
38
|
+
)
|
|
39
|
+
if result.returncode == 0:
|
|
40
|
+
print("opencode-bridge removed from Claude Code")
|
|
41
|
+
print(result.stdout)
|
|
42
|
+
else:
|
|
43
|
+
if "not found" in result.stderr.lower():
|
|
44
|
+
print("opencode-bridge not registered")
|
|
45
|
+
else:
|
|
46
|
+
print(f"Failed to remove: {result.stderr}")
|
|
47
|
+
sys.exit(1)
|
|
48
|
+
except FileNotFoundError:
|
|
49
|
+
print("Claude Code CLI not found")
|
|
50
|
+
sys.exit(1)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
if __name__ == "__main__":
|
|
54
|
+
if len(sys.argv) > 1 and sys.argv[1] == "uninstall":
|
|
55
|
+
uninstall()
|
|
56
|
+
else:
|
|
57
|
+
install()
|
|
@@ -0,0 +1,817 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
OpenCode Bridge - MCP server for continuous OpenCode sessions.
|
|
4
|
+
|
|
5
|
+
Features:
|
|
6
|
+
- Continuous discussion sessions with conversation history
|
|
7
|
+
- Access to all OpenCode models (GPT-5, Claude, Gemini, etc.)
|
|
8
|
+
- Agent support (plan, build, explore, general)
|
|
9
|
+
- Session continuation
|
|
10
|
+
- File attachment for code review
|
|
11
|
+
|
|
12
|
+
Configuration:
|
|
13
|
+
- OPENCODE_MODEL: Default model (e.g., openai/gpt-5.2-codex)
|
|
14
|
+
- OPENCODE_AGENT: Default agent (plan, build, explore, general)
|
|
15
|
+
- ~/.opencode-bridge/config.json: Persistent config
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
import json
|
|
20
|
+
import asyncio
|
|
21
|
+
import shutil
|
|
22
|
+
from datetime import datetime
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Optional
|
|
25
|
+
from dataclasses import dataclass, field, asdict
|
|
26
|
+
|
|
27
|
+
from mcp.server import Server, InitializationOptions
|
|
28
|
+
from mcp.server.stdio import stdio_server
|
|
29
|
+
from mcp.types import Tool, TextContent, ServerCapabilities, ToolsCapability
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Default configuration
|
|
33
|
+
DEFAULT_MODEL = "openai/gpt-5.2-codex"
|
|
34
|
+
DEFAULT_AGENT = "plan"
|
|
35
|
+
DEFAULT_VARIANT = "medium"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class Config:
|
|
40
|
+
model: str = DEFAULT_MODEL
|
|
41
|
+
agent: str = DEFAULT_AGENT
|
|
42
|
+
variant: str = DEFAULT_VARIANT
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def load(cls) -> "Config":
|
|
46
|
+
config = cls()
|
|
47
|
+
|
|
48
|
+
# Load from config file
|
|
49
|
+
config_path = Path.home() / ".opencode-bridge" / "config.json"
|
|
50
|
+
if config_path.exists():
|
|
51
|
+
try:
|
|
52
|
+
data = json.loads(config_path.read_text())
|
|
53
|
+
config.model = data.get("model", config.model)
|
|
54
|
+
config.agent = data.get("agent", config.agent)
|
|
55
|
+
config.variant = data.get("variant", config.variant)
|
|
56
|
+
except Exception:
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
# Environment variables override config file
|
|
60
|
+
config.model = os.environ.get("OPENCODE_MODEL", config.model)
|
|
61
|
+
config.agent = os.environ.get("OPENCODE_AGENT", config.agent)
|
|
62
|
+
config.variant = os.environ.get("OPENCODE_VARIANT") or config.variant
|
|
63
|
+
|
|
64
|
+
return config
|
|
65
|
+
|
|
66
|
+
def save(self):
|
|
67
|
+
config_dir = Path.home() / ".opencode-bridge"
|
|
68
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
config_path = config_dir / "config.json"
|
|
70
|
+
data = {"model": self.model, "agent": self.agent, "variant": self.variant}
|
|
71
|
+
config_path.write_text(json.dumps(data, indent=2))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def find_opencode() -> Optional[Path]:
|
|
75
|
+
"""Find opencode binary."""
|
|
76
|
+
# Check common locations
|
|
77
|
+
paths = [
|
|
78
|
+
Path.home() / ".opencode" / "bin" / "opencode",
|
|
79
|
+
Path("/usr/local/bin/opencode"),
|
|
80
|
+
Path("/usr/bin/opencode"),
|
|
81
|
+
]
|
|
82
|
+
for p in paths:
|
|
83
|
+
if p.exists():
|
|
84
|
+
return p
|
|
85
|
+
# Check PATH
|
|
86
|
+
which = shutil.which("opencode")
|
|
87
|
+
if which:
|
|
88
|
+
return Path(which)
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
OPENCODE_BIN = find_opencode()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass
|
|
96
|
+
class Message:
|
|
97
|
+
role: str
|
|
98
|
+
content: str
|
|
99
|
+
timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass
|
|
103
|
+
class Session:
|
|
104
|
+
id: str
|
|
105
|
+
model: str
|
|
106
|
+
agent: str
|
|
107
|
+
variant: str = DEFAULT_VARIANT
|
|
108
|
+
opencode_session_id: Optional[str] = None
|
|
109
|
+
messages: list[Message] = field(default_factory=list)
|
|
110
|
+
created: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
111
|
+
|
|
112
|
+
def add_message(self, role: str, content: str):
|
|
113
|
+
self.messages.append(Message(role=role, content=content))
|
|
114
|
+
|
|
115
|
+
def save(self, path: Path):
|
|
116
|
+
data = {
|
|
117
|
+
"id": self.id,
|
|
118
|
+
"model": self.model,
|
|
119
|
+
"agent": self.agent,
|
|
120
|
+
"variant": self.variant,
|
|
121
|
+
"opencode_session_id": self.opencode_session_id,
|
|
122
|
+
"created": self.created,
|
|
123
|
+
"messages": [asdict(m) for m in self.messages]
|
|
124
|
+
}
|
|
125
|
+
path.write_text(json.dumps(data, indent=2))
|
|
126
|
+
|
|
127
|
+
@classmethod
|
|
128
|
+
def load(cls, path: Path) -> "Session":
|
|
129
|
+
data = json.loads(path.read_text())
|
|
130
|
+
session = cls(
|
|
131
|
+
id=data["id"],
|
|
132
|
+
model=data["model"],
|
|
133
|
+
agent=data.get("agent", DEFAULT_AGENT),
|
|
134
|
+
variant=data.get("variant", DEFAULT_VARIANT),
|
|
135
|
+
opencode_session_id=data.get("opencode_session_id"),
|
|
136
|
+
created=data.get("created", datetime.now().isoformat())
|
|
137
|
+
)
|
|
138
|
+
for m in data.get("messages", []):
|
|
139
|
+
session.messages.append(Message(**m))
|
|
140
|
+
return session
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class OpenCodeBridge:
|
|
144
|
+
def __init__(self):
|
|
145
|
+
self.start_time = datetime.now()
|
|
146
|
+
self.config = Config.load()
|
|
147
|
+
self.sessions: dict[str, Session] = {}
|
|
148
|
+
self.active_session: Optional[str] = None
|
|
149
|
+
self.sessions_dir = Path.home() / ".opencode-bridge" / "sessions"
|
|
150
|
+
self.sessions_dir.mkdir(parents=True, exist_ok=True)
|
|
151
|
+
self.available_models: list[str] = []
|
|
152
|
+
self.available_agents: list[str] = []
|
|
153
|
+
self._load_sessions()
|
|
154
|
+
|
|
155
|
+
def _load_sessions(self):
|
|
156
|
+
for path in self.sessions_dir.glob("*.json"):
|
|
157
|
+
try:
|
|
158
|
+
session = Session.load(path)
|
|
159
|
+
self.sessions[session.id] = session
|
|
160
|
+
except Exception:
|
|
161
|
+
pass
|
|
162
|
+
|
|
163
|
+
async def _run_opencode(self, *args, timeout: int = 300) -> tuple[str, int]:
|
|
164
|
+
"""Run opencode CLI command and return output (async)."""
|
|
165
|
+
if not OPENCODE_BIN:
|
|
166
|
+
return "OpenCode not installed. Install from: https://opencode.ai", 1
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
proc = await asyncio.create_subprocess_exec(
|
|
170
|
+
str(OPENCODE_BIN), *args,
|
|
171
|
+
stdin=asyncio.subprocess.PIPE,
|
|
172
|
+
stdout=asyncio.subprocess.PIPE,
|
|
173
|
+
stderr=asyncio.subprocess.PIPE
|
|
174
|
+
)
|
|
175
|
+
stdout, stderr = await asyncio.wait_for(
|
|
176
|
+
proc.communicate(input=b''),
|
|
177
|
+
timeout=timeout
|
|
178
|
+
)
|
|
179
|
+
output = stdout.decode() or stderr.decode()
|
|
180
|
+
return output.strip(), proc.returncode or 0
|
|
181
|
+
except asyncio.TimeoutError:
|
|
182
|
+
proc.kill()
|
|
183
|
+
await proc.wait()
|
|
184
|
+
return "Command timed out", 1
|
|
185
|
+
except Exception as e:
|
|
186
|
+
return f"Error: {e}", 1
|
|
187
|
+
|
|
188
|
+
async def list_models(self, provider: Optional[str] = None) -> str:
|
|
189
|
+
"""List available models from OpenCode."""
|
|
190
|
+
args = ["models"]
|
|
191
|
+
if provider:
|
|
192
|
+
args.append(provider)
|
|
193
|
+
|
|
194
|
+
output, code = await self._run_opencode(*args)
|
|
195
|
+
if code != 0:
|
|
196
|
+
return f"Error listing models: {output}"
|
|
197
|
+
|
|
198
|
+
self.available_models = [line.strip() for line in output.split("\n") if line.strip()]
|
|
199
|
+
|
|
200
|
+
# Group by provider
|
|
201
|
+
providers: dict[str, list[str]] = {}
|
|
202
|
+
for model in self.available_models:
|
|
203
|
+
if "/" in model:
|
|
204
|
+
prov, name = model.split("/", 1)
|
|
205
|
+
else:
|
|
206
|
+
prov, name = "other", model
|
|
207
|
+
providers.setdefault(prov, []).append(name)
|
|
208
|
+
|
|
209
|
+
lines = ["Available models:"]
|
|
210
|
+
for prov in sorted(providers.keys()):
|
|
211
|
+
lines.append(f"\n**{prov}:**")
|
|
212
|
+
for name in sorted(providers[prov]):
|
|
213
|
+
full = f"{prov}/{name}"
|
|
214
|
+
lines.append(f" - {full}")
|
|
215
|
+
|
|
216
|
+
return "\n".join(lines)
|
|
217
|
+
|
|
218
|
+
async def list_agents(self) -> str:
|
|
219
|
+
"""List available agents from OpenCode."""
|
|
220
|
+
output, code = await self._run_opencode("agent", "list")
|
|
221
|
+
if code != 0:
|
|
222
|
+
return f"Error listing agents: {output}"
|
|
223
|
+
|
|
224
|
+
# Parse agent names from output
|
|
225
|
+
agents = []
|
|
226
|
+
for line in output.split("\n"):
|
|
227
|
+
line = line.strip()
|
|
228
|
+
if line and "(" in line:
|
|
229
|
+
name = line.split("(")[0].strip()
|
|
230
|
+
agents.append(name)
|
|
231
|
+
|
|
232
|
+
self.available_agents = agents
|
|
233
|
+
return "Available agents:\n" + "\n".join(f" - {a}" for a in agents)
|
|
234
|
+
|
|
235
|
+
async def start_session(
|
|
236
|
+
self,
|
|
237
|
+
session_id: str,
|
|
238
|
+
model: Optional[str] = None,
|
|
239
|
+
agent: Optional[str] = None,
|
|
240
|
+
variant: Optional[str] = None
|
|
241
|
+
) -> str:
|
|
242
|
+
# Use config defaults if not specified
|
|
243
|
+
model = model or self.config.model
|
|
244
|
+
agent = agent or self.config.agent
|
|
245
|
+
variant = variant or self.config.variant
|
|
246
|
+
|
|
247
|
+
session = Session(
|
|
248
|
+
id=session_id,
|
|
249
|
+
model=model,
|
|
250
|
+
agent=agent,
|
|
251
|
+
variant=variant
|
|
252
|
+
)
|
|
253
|
+
self.sessions[session_id] = session
|
|
254
|
+
self.active_session = session_id
|
|
255
|
+
session.save(self.sessions_dir / f"{session_id}.json")
|
|
256
|
+
|
|
257
|
+
result = f"Session '{session_id}' started\n Model: {model}\n Agent: {agent}"
|
|
258
|
+
if variant:
|
|
259
|
+
result += f"\n Variant: {variant}"
|
|
260
|
+
return result
|
|
261
|
+
|
|
262
|
+
def get_config(self) -> str:
|
|
263
|
+
"""Get current configuration."""
|
|
264
|
+
return f"""Current configuration:
|
|
265
|
+
Model: {self.config.model}
|
|
266
|
+
Agent: {self.config.agent}
|
|
267
|
+
Variant: {self.config.variant}
|
|
268
|
+
|
|
269
|
+
Set via:
|
|
270
|
+
- ~/.opencode-bridge/config.json
|
|
271
|
+
- OPENCODE_MODEL, OPENCODE_AGENT, OPENCODE_VARIANT env vars
|
|
272
|
+
- opencode_configure tool"""
|
|
273
|
+
|
|
274
|
+
def set_config(self, model: Optional[str] = None, agent: Optional[str] = None, variant: Optional[str] = None) -> str:
|
|
275
|
+
"""Update and persist configuration."""
|
|
276
|
+
changes = []
|
|
277
|
+
if model:
|
|
278
|
+
self.config.model = model
|
|
279
|
+
changes.append(f"model: {model}")
|
|
280
|
+
if agent:
|
|
281
|
+
self.config.agent = agent
|
|
282
|
+
changes.append(f"agent: {agent}")
|
|
283
|
+
if variant:
|
|
284
|
+
self.config.variant = variant
|
|
285
|
+
changes.append(f"variant: {variant}")
|
|
286
|
+
|
|
287
|
+
if changes:
|
|
288
|
+
self.config.save()
|
|
289
|
+
return "Configuration updated:\n " + "\n ".join(changes)
|
|
290
|
+
return "No changes made."
|
|
291
|
+
|
|
292
|
+
async def send_message(
|
|
293
|
+
self,
|
|
294
|
+
message: str,
|
|
295
|
+
session_id: Optional[str] = None,
|
|
296
|
+
files: Optional[list[str]] = None
|
|
297
|
+
) -> str:
|
|
298
|
+
sid = session_id or self.active_session
|
|
299
|
+
if not sid or sid not in self.sessions:
|
|
300
|
+
return "No active session. Use opencode_start first."
|
|
301
|
+
|
|
302
|
+
session = self.sessions[sid]
|
|
303
|
+
session.add_message("user", message)
|
|
304
|
+
|
|
305
|
+
# Build command - message must come right after "run" as positional arg
|
|
306
|
+
args = ["run", message]
|
|
307
|
+
args.extend(["--model", session.model])
|
|
308
|
+
args.extend(["--agent", session.agent])
|
|
309
|
+
|
|
310
|
+
# Add variant if specified
|
|
311
|
+
if session.variant:
|
|
312
|
+
args.extend(["--variant", session.variant])
|
|
313
|
+
|
|
314
|
+
# Continue session if we have an opencode session ID
|
|
315
|
+
if session.opencode_session_id:
|
|
316
|
+
args.extend(["--session", session.opencode_session_id])
|
|
317
|
+
|
|
318
|
+
# Attach files
|
|
319
|
+
if files:
|
|
320
|
+
for f in files:
|
|
321
|
+
args.extend(["--file", f])
|
|
322
|
+
|
|
323
|
+
# Use JSON format to get session ID
|
|
324
|
+
args.extend(["--format", "json"])
|
|
325
|
+
|
|
326
|
+
output, code = await self._run_opencode(*args)
|
|
327
|
+
|
|
328
|
+
if code != 0:
|
|
329
|
+
return f"Error: {output}"
|
|
330
|
+
|
|
331
|
+
# Parse JSON events for session ID and text
|
|
332
|
+
reply_parts = []
|
|
333
|
+
for line in output.split("\n"):
|
|
334
|
+
if not line:
|
|
335
|
+
continue
|
|
336
|
+
try:
|
|
337
|
+
event = json.loads(line)
|
|
338
|
+
if not session.opencode_session_id and "sessionID" in event:
|
|
339
|
+
session.opencode_session_id = event["sessionID"]
|
|
340
|
+
if event.get("type") == "text":
|
|
341
|
+
text = event.get("part", {}).get("text", "")
|
|
342
|
+
if text:
|
|
343
|
+
reply_parts.append(text)
|
|
344
|
+
except json.JSONDecodeError:
|
|
345
|
+
continue
|
|
346
|
+
|
|
347
|
+
reply = "".join(reply_parts)
|
|
348
|
+
if reply:
|
|
349
|
+
session.add_message("assistant", reply)
|
|
350
|
+
|
|
351
|
+
# Save if we got a reply or captured a new session ID
|
|
352
|
+
if reply or session.opencode_session_id:
|
|
353
|
+
session.save(self.sessions_dir / f"{sid}.json")
|
|
354
|
+
|
|
355
|
+
return reply or "No response received"
|
|
356
|
+
|
|
357
|
+
async def plan(
|
|
358
|
+
self,
|
|
359
|
+
task: str,
|
|
360
|
+
session_id: Optional[str] = None,
|
|
361
|
+
files: Optional[list[str]] = None
|
|
362
|
+
) -> str:
|
|
363
|
+
"""Start a planning discussion using the plan agent."""
|
|
364
|
+
sid = session_id or self.active_session
|
|
365
|
+
|
|
366
|
+
# If no active session, create one for planning
|
|
367
|
+
if not sid or sid not in self.sessions:
|
|
368
|
+
sid = f"plan-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
|
|
369
|
+
await self.start_session(sid, agent="plan")
|
|
370
|
+
|
|
371
|
+
# Switch to plan agent if not already
|
|
372
|
+
session = self.sessions[sid]
|
|
373
|
+
if session.agent != "plan":
|
|
374
|
+
session.agent = "plan"
|
|
375
|
+
session.save(self.sessions_dir / f"{sid}.json")
|
|
376
|
+
|
|
377
|
+
return await self.send_message(task, sid, files)
|
|
378
|
+
|
|
379
|
+
async def brainstorm(
|
|
380
|
+
self,
|
|
381
|
+
topic: str,
|
|
382
|
+
session_id: Optional[str] = None
|
|
383
|
+
) -> str:
|
|
384
|
+
"""Open-ended brainstorming discussion."""
|
|
385
|
+
sid = session_id or self.active_session
|
|
386
|
+
|
|
387
|
+
if not sid or sid not in self.sessions:
|
|
388
|
+
sid = f"brainstorm-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
|
|
389
|
+
await self.start_session(sid, agent="build")
|
|
390
|
+
|
|
391
|
+
prompt = f"""Let's brainstorm about: {topic}
|
|
392
|
+
|
|
393
|
+
Please provide:
|
|
394
|
+
1. Key considerations and trade-offs
|
|
395
|
+
2. Multiple approaches or solutions
|
|
396
|
+
3. Pros and cons of each approach
|
|
397
|
+
4. Your recommended approach and why"""
|
|
398
|
+
|
|
399
|
+
return await self.send_message(prompt, sid)
|
|
400
|
+
|
|
401
|
+
async def review_code(
|
|
402
|
+
self,
|
|
403
|
+
code_or_file: str,
|
|
404
|
+
focus: str = "correctness, efficiency, and potential bugs",
|
|
405
|
+
session_id: Optional[str] = None
|
|
406
|
+
) -> str:
|
|
407
|
+
"""Review code for issues and improvements."""
|
|
408
|
+
sid = session_id or self.active_session
|
|
409
|
+
|
|
410
|
+
if not sid or sid not in self.sessions:
|
|
411
|
+
sid = f"review-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
|
|
412
|
+
await self.start_session(sid, agent="build")
|
|
413
|
+
|
|
414
|
+
# Check if it's a file path
|
|
415
|
+
files = None
|
|
416
|
+
if Path(code_or_file).is_file():
|
|
417
|
+
files = [code_or_file]
|
|
418
|
+
prompt = f"Please review the attached code file, focusing on: {focus}"
|
|
419
|
+
else:
|
|
420
|
+
prompt = f"""Please review this code, focusing on: {focus}
|
|
421
|
+
|
|
422
|
+
```
|
|
423
|
+
{code_or_file}
|
|
424
|
+
```"""
|
|
425
|
+
|
|
426
|
+
return await self.send_message(prompt, sid, files)
|
|
427
|
+
|
|
428
|
+
def list_sessions(self) -> str:
|
|
429
|
+
if not self.sessions:
|
|
430
|
+
return "No sessions found."
|
|
431
|
+
|
|
432
|
+
lines = ["Sessions:"]
|
|
433
|
+
for sid, session in self.sessions.items():
|
|
434
|
+
active = " (active)" if sid == self.active_session else ""
|
|
435
|
+
msg_count = len(session.messages)
|
|
436
|
+
variant_str = f", variant={session.variant}" if session.variant else ""
|
|
437
|
+
lines.append(f" - {sid}: {session.model} [{session.agent}{variant_str}], {msg_count} messages{active}")
|
|
438
|
+
return "\n".join(lines)
|
|
439
|
+
|
|
440
|
+
def get_history(self, session_id: Optional[str] = None, last_n: int = 20) -> str:
|
|
441
|
+
sid = session_id or self.active_session
|
|
442
|
+
if not sid or sid not in self.sessions:
|
|
443
|
+
return "No active session."
|
|
444
|
+
|
|
445
|
+
session = self.sessions[sid]
|
|
446
|
+
variant_str = f", Variant: {session.variant}" if session.variant else ""
|
|
447
|
+
lines = [f"Session: {sid}", f"Model: {session.model}, Agent: {session.agent}{variant_str}", "---"]
|
|
448
|
+
|
|
449
|
+
for msg in session.messages[-last_n:]:
|
|
450
|
+
role = "You" if msg.role == "user" else "OpenCode"
|
|
451
|
+
lines.append(f"\n**{role}:**\n{msg.content}")
|
|
452
|
+
|
|
453
|
+
return "\n".join(lines)
|
|
454
|
+
|
|
455
|
+
def set_active(self, session_id: str) -> str:
|
|
456
|
+
if session_id not in self.sessions:
|
|
457
|
+
return f"Session '{session_id}' not found."
|
|
458
|
+
self.active_session = session_id
|
|
459
|
+
session = self.sessions[session_id]
|
|
460
|
+
variant_str = f", variant={session.variant}" if session.variant else ""
|
|
461
|
+
return f"Active session: '{session_id}' ({session.model}, {session.agent}{variant_str})"
|
|
462
|
+
|
|
463
|
+
def set_model(self, model: str, session_id: Optional[str] = None) -> str:
|
|
464
|
+
sid = session_id or self.active_session
|
|
465
|
+
if not sid or sid not in self.sessions:
|
|
466
|
+
return "No active session."
|
|
467
|
+
|
|
468
|
+
session = self.sessions[sid]
|
|
469
|
+
old_model = session.model
|
|
470
|
+
session.model = model
|
|
471
|
+
session.save(self.sessions_dir / f"{sid}.json")
|
|
472
|
+
|
|
473
|
+
return f"Model changed: {old_model} -> {model}"
|
|
474
|
+
|
|
475
|
+
def set_agent(self, agent: str, session_id: Optional[str] = None) -> str:
|
|
476
|
+
sid = session_id or self.active_session
|
|
477
|
+
if not sid or sid not in self.sessions:
|
|
478
|
+
return "No active session."
|
|
479
|
+
|
|
480
|
+
session = self.sessions[sid]
|
|
481
|
+
old_agent = session.agent
|
|
482
|
+
session.agent = agent
|
|
483
|
+
session.save(self.sessions_dir / f"{sid}.json")
|
|
484
|
+
|
|
485
|
+
return f"Agent changed: {old_agent} -> {agent}"
|
|
486
|
+
|
|
487
|
+
def set_variant(self, variant: Optional[str], session_id: Optional[str] = None) -> str:
|
|
488
|
+
sid = session_id or self.active_session
|
|
489
|
+
if not sid or sid not in self.sessions:
|
|
490
|
+
return "No active session."
|
|
491
|
+
|
|
492
|
+
session = self.sessions[sid]
|
|
493
|
+
old_variant = session.variant or "none"
|
|
494
|
+
session.variant = variant
|
|
495
|
+
session.save(self.sessions_dir / f"{sid}.json")
|
|
496
|
+
|
|
497
|
+
new_variant = variant or "none"
|
|
498
|
+
return f"Variant changed: {old_variant} -> {new_variant}"
|
|
499
|
+
|
|
500
|
+
def end_session(self, session_id: Optional[str] = None) -> str:
|
|
501
|
+
sid = session_id or self.active_session
|
|
502
|
+
if not sid or sid not in self.sessions:
|
|
503
|
+
return "No active session to end."
|
|
504
|
+
|
|
505
|
+
del self.sessions[sid]
|
|
506
|
+
session_path = self.sessions_dir / f"{sid}.json"
|
|
507
|
+
if session_path.exists():
|
|
508
|
+
session_path.unlink()
|
|
509
|
+
|
|
510
|
+
if self.active_session == sid:
|
|
511
|
+
self.active_session = None
|
|
512
|
+
|
|
513
|
+
return f"Session '{sid}' ended."
|
|
514
|
+
|
|
515
|
+
def health_check(self) -> dict:
|
|
516
|
+
"""Return server health status."""
|
|
517
|
+
uptime_seconds = int((datetime.now() - self.start_time).total_seconds())
|
|
518
|
+
return {
|
|
519
|
+
"status": "ok",
|
|
520
|
+
"sessions": len(self.sessions),
|
|
521
|
+
"uptime": uptime_seconds
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
# MCP Server setup
|
|
526
|
+
bridge = OpenCodeBridge()
|
|
527
|
+
server = Server("opencode-bridge")
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
@server.list_tools()
|
|
531
|
+
async def list_tools():
|
|
532
|
+
return [
|
|
533
|
+
Tool(
|
|
534
|
+
name="opencode_models",
|
|
535
|
+
description="List available models from OpenCode (GPT-5, Claude, Gemini, etc.)",
|
|
536
|
+
inputSchema={
|
|
537
|
+
"type": "object",
|
|
538
|
+
"properties": {
|
|
539
|
+
"provider": {
|
|
540
|
+
"type": "string",
|
|
541
|
+
"description": "Filter by provider (openai, github-copilot, anthropic)"
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
),
|
|
546
|
+
Tool(
|
|
547
|
+
name="opencode_agents",
|
|
548
|
+
description="List available agents (plan, build, explore, general)",
|
|
549
|
+
inputSchema={"type": "object", "properties": {}}
|
|
550
|
+
),
|
|
551
|
+
Tool(
|
|
552
|
+
name="opencode_start",
|
|
553
|
+
description="Start a new discussion session with OpenCode",
|
|
554
|
+
inputSchema={
|
|
555
|
+
"type": "object",
|
|
556
|
+
"properties": {
|
|
557
|
+
"session_id": {
|
|
558
|
+
"type": "string",
|
|
559
|
+
"description": "Unique identifier for this session"
|
|
560
|
+
},
|
|
561
|
+
"model": {
|
|
562
|
+
"type": "string",
|
|
563
|
+
"description": "Model to use (default: openai/gpt-5.2-codex)"
|
|
564
|
+
},
|
|
565
|
+
"agent": {
|
|
566
|
+
"type": "string",
|
|
567
|
+
"description": "Agent to use: plan, build, explore, general (default: plan)"
|
|
568
|
+
},
|
|
569
|
+
"variant": {
|
|
570
|
+
"type": "string",
|
|
571
|
+
"description": "Model variant for reasoning effort: minimal, low, medium, high, xhigh, max (default: medium)"
|
|
572
|
+
}
|
|
573
|
+
},
|
|
574
|
+
"required": ["session_id"]
|
|
575
|
+
}
|
|
576
|
+
),
|
|
577
|
+
Tool(
|
|
578
|
+
name="opencode_discuss",
|
|
579
|
+
description="Send a message to OpenCode. Use for code review, architecture, brainstorming.",
|
|
580
|
+
inputSchema={
|
|
581
|
+
"type": "object",
|
|
582
|
+
"properties": {
|
|
583
|
+
"message": {
|
|
584
|
+
"type": "string",
|
|
585
|
+
"description": "Your message or question"
|
|
586
|
+
},
|
|
587
|
+
"files": {
|
|
588
|
+
"type": "array",
|
|
589
|
+
"items": {"type": "string"},
|
|
590
|
+
"description": "File paths to attach for context"
|
|
591
|
+
}
|
|
592
|
+
},
|
|
593
|
+
"required": ["message"]
|
|
594
|
+
}
|
|
595
|
+
),
|
|
596
|
+
Tool(
|
|
597
|
+
name="opencode_plan",
|
|
598
|
+
description="Start a planning discussion with the plan agent",
|
|
599
|
+
inputSchema={
|
|
600
|
+
"type": "object",
|
|
601
|
+
"properties": {
|
|
602
|
+
"task": {
|
|
603
|
+
"type": "string",
|
|
604
|
+
"description": "What to plan"
|
|
605
|
+
},
|
|
606
|
+
"files": {
|
|
607
|
+
"type": "array",
|
|
608
|
+
"items": {"type": "string"},
|
|
609
|
+
"description": "Relevant file paths"
|
|
610
|
+
}
|
|
611
|
+
},
|
|
612
|
+
"required": ["task"]
|
|
613
|
+
}
|
|
614
|
+
),
|
|
615
|
+
Tool(
|
|
616
|
+
name="opencode_brainstorm",
|
|
617
|
+
description="Open-ended brainstorming on a topic",
|
|
618
|
+
inputSchema={
|
|
619
|
+
"type": "object",
|
|
620
|
+
"properties": {
|
|
621
|
+
"topic": {
|
|
622
|
+
"type": "string",
|
|
623
|
+
"description": "Topic to brainstorm about"
|
|
624
|
+
}
|
|
625
|
+
},
|
|
626
|
+
"required": ["topic"]
|
|
627
|
+
}
|
|
628
|
+
),
|
|
629
|
+
Tool(
|
|
630
|
+
name="opencode_review",
|
|
631
|
+
description="Review code for issues and improvements",
|
|
632
|
+
inputSchema={
|
|
633
|
+
"type": "object",
|
|
634
|
+
"properties": {
|
|
635
|
+
"code_or_file": {
|
|
636
|
+
"type": "string",
|
|
637
|
+
"description": "Code snippet or file path"
|
|
638
|
+
},
|
|
639
|
+
"focus": {
|
|
640
|
+
"type": "string",
|
|
641
|
+
"description": "What to focus on (default: correctness, efficiency, bugs)"
|
|
642
|
+
}
|
|
643
|
+
},
|
|
644
|
+
"required": ["code_or_file"]
|
|
645
|
+
}
|
|
646
|
+
),
|
|
647
|
+
Tool(
|
|
648
|
+
name="opencode_model",
|
|
649
|
+
description="Change the model for the current session",
|
|
650
|
+
inputSchema={
|
|
651
|
+
"type": "object",
|
|
652
|
+
"properties": {
|
|
653
|
+
"model": {"type": "string", "description": "New model"}
|
|
654
|
+
},
|
|
655
|
+
"required": ["model"]
|
|
656
|
+
}
|
|
657
|
+
),
|
|
658
|
+
Tool(
|
|
659
|
+
name="opencode_agent",
|
|
660
|
+
description="Change the agent for the current session",
|
|
661
|
+
inputSchema={
|
|
662
|
+
"type": "object",
|
|
663
|
+
"properties": {
|
|
664
|
+
"agent": {"type": "string", "description": "New agent (plan, build, explore, general)"}
|
|
665
|
+
},
|
|
666
|
+
"required": ["agent"]
|
|
667
|
+
}
|
|
668
|
+
),
|
|
669
|
+
Tool(
|
|
670
|
+
name="opencode_variant",
|
|
671
|
+
description="Change the model variant (reasoning effort) for the current session",
|
|
672
|
+
inputSchema={
|
|
673
|
+
"type": "object",
|
|
674
|
+
"properties": {
|
|
675
|
+
"variant": {"type": "string", "description": "New variant: minimal, low, medium, high, xhigh, max"}
|
|
676
|
+
},
|
|
677
|
+
"required": ["variant"]
|
|
678
|
+
}
|
|
679
|
+
),
|
|
680
|
+
Tool(
|
|
681
|
+
name="opencode_history",
|
|
682
|
+
description="Get conversation history",
|
|
683
|
+
inputSchema={
|
|
684
|
+
"type": "object",
|
|
685
|
+
"properties": {
|
|
686
|
+
"last_n": {"type": "integer", "description": "Number of messages (default: 20)"}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
),
|
|
690
|
+
Tool(
|
|
691
|
+
name="opencode_sessions",
|
|
692
|
+
description="List all sessions",
|
|
693
|
+
inputSchema={"type": "object", "properties": {}}
|
|
694
|
+
),
|
|
695
|
+
Tool(
|
|
696
|
+
name="opencode_switch",
|
|
697
|
+
description="Switch to a different session",
|
|
698
|
+
inputSchema={
|
|
699
|
+
"type": "object",
|
|
700
|
+
"properties": {
|
|
701
|
+
"session_id": {"type": "string", "description": "Session to switch to"}
|
|
702
|
+
},
|
|
703
|
+
"required": ["session_id"]
|
|
704
|
+
}
|
|
705
|
+
),
|
|
706
|
+
Tool(
|
|
707
|
+
name="opencode_end",
|
|
708
|
+
description="End the current session",
|
|
709
|
+
inputSchema={"type": "object", "properties": {}}
|
|
710
|
+
),
|
|
711
|
+
Tool(
|
|
712
|
+
name="opencode_config",
|
|
713
|
+
description="Get current configuration (default model, agent, variant)",
|
|
714
|
+
inputSchema={"type": "object", "properties": {}}
|
|
715
|
+
),
|
|
716
|
+
Tool(
|
|
717
|
+
name="opencode_configure",
|
|
718
|
+
description="Set default model, agent, and/or variant (persisted)",
|
|
719
|
+
inputSchema={
|
|
720
|
+
"type": "object",
|
|
721
|
+
"properties": {
|
|
722
|
+
"model": {"type": "string", "description": "Default model"},
|
|
723
|
+
"agent": {"type": "string", "description": "Default agent"},
|
|
724
|
+
"variant": {"type": "string", "description": "Default variant: minimal, low, medium, high, xhigh, max"}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
),
|
|
728
|
+
Tool(
|
|
729
|
+
name="opencode_health",
|
|
730
|
+
description="Health check: returns server status, session count, and uptime",
|
|
731
|
+
inputSchema={"type": "object", "properties": {}}
|
|
732
|
+
)
|
|
733
|
+
]
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
@server.call_tool()
|
|
737
|
+
async def call_tool(name: str, arguments: dict):
|
|
738
|
+
try:
|
|
739
|
+
if name == "opencode_models":
|
|
740
|
+
result = await bridge.list_models(arguments.get("provider"))
|
|
741
|
+
elif name == "opencode_agents":
|
|
742
|
+
result = await bridge.list_agents()
|
|
743
|
+
elif name == "opencode_start":
|
|
744
|
+
result = await bridge.start_session(
|
|
745
|
+
session_id=arguments["session_id"],
|
|
746
|
+
model=arguments.get("model"),
|
|
747
|
+
agent=arguments.get("agent"),
|
|
748
|
+
variant=arguments.get("variant")
|
|
749
|
+
)
|
|
750
|
+
elif name == "opencode_discuss":
|
|
751
|
+
result = await bridge.send_message(
|
|
752
|
+
message=arguments["message"],
|
|
753
|
+
files=arguments.get("files")
|
|
754
|
+
)
|
|
755
|
+
elif name == "opencode_plan":
|
|
756
|
+
result = await bridge.plan(
|
|
757
|
+
task=arguments["task"],
|
|
758
|
+
files=arguments.get("files")
|
|
759
|
+
)
|
|
760
|
+
elif name == "opencode_brainstorm":
|
|
761
|
+
result = await bridge.brainstorm(arguments["topic"])
|
|
762
|
+
elif name == "opencode_review":
|
|
763
|
+
result = await bridge.review_code(
|
|
764
|
+
code_or_file=arguments["code_or_file"],
|
|
765
|
+
focus=arguments.get("focus", "correctness, efficiency, and potential bugs")
|
|
766
|
+
)
|
|
767
|
+
elif name == "opencode_model":
|
|
768
|
+
result = bridge.set_model(arguments["model"])
|
|
769
|
+
elif name == "opencode_agent":
|
|
770
|
+
result = bridge.set_agent(arguments["agent"])
|
|
771
|
+
elif name == "opencode_variant":
|
|
772
|
+
result = bridge.set_variant(arguments["variant"])
|
|
773
|
+
elif name == "opencode_history":
|
|
774
|
+
result = bridge.get_history(last_n=arguments.get("last_n", 20))
|
|
775
|
+
elif name == "opencode_sessions":
|
|
776
|
+
result = bridge.list_sessions()
|
|
777
|
+
elif name == "opencode_switch":
|
|
778
|
+
result = bridge.set_active(arguments["session_id"])
|
|
779
|
+
elif name == "opencode_end":
|
|
780
|
+
result = bridge.end_session()
|
|
781
|
+
elif name == "opencode_config":
|
|
782
|
+
result = bridge.get_config()
|
|
783
|
+
elif name == "opencode_configure":
|
|
784
|
+
result = bridge.set_config(
|
|
785
|
+
model=arguments.get("model"),
|
|
786
|
+
agent=arguments.get("agent"),
|
|
787
|
+
variant=arguments.get("variant")
|
|
788
|
+
)
|
|
789
|
+
elif name == "opencode_health":
|
|
790
|
+
health = bridge.health_check()
|
|
791
|
+
result = f"Status: {health['status']}\nSessions: {health['sessions']}\nUptime: {health['uptime']}s"
|
|
792
|
+
else:
|
|
793
|
+
result = f"Unknown tool: {name}"
|
|
794
|
+
|
|
795
|
+
return [TextContent(type="text", text=result)]
|
|
796
|
+
|
|
797
|
+
except Exception as e:
|
|
798
|
+
return [TextContent(type="text", text=f"Error: {e}")]
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
def main():
|
|
802
|
+
import asyncio
|
|
803
|
+
|
|
804
|
+
async def run():
|
|
805
|
+
init_options = InitializationOptions(
|
|
806
|
+
server_name="opencode-bridge",
|
|
807
|
+
server_version="0.1.0",
|
|
808
|
+
capabilities=ServerCapabilities(tools=ToolsCapability())
|
|
809
|
+
)
|
|
810
|
+
async with stdio_server() as (read_stream, write_stream):
|
|
811
|
+
await server.run(read_stream, write_stream, init_options)
|
|
812
|
+
|
|
813
|
+
asyncio.run(run())
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
if __name__ == "__main__":
|
|
817
|
+
main()
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: opencode-bridge
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: MCP server for continuous OpenCode discussion sessions
|
|
5
|
+
Project-URL: Repository, https://github.com/genomewalker/opencode-bridge
|
|
6
|
+
Author: Antonio Fernandez-Guerra
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Keywords: claude,code-review,discussion,gpt,mcp,opencode
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Requires-Dist: mcp>=1.0.0
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
20
|
+
Requires-Dist: ruff>=0.4.0; extra == 'dev'
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# OpenCode Bridge
|
|
24
|
+
|
|
25
|
+
MCP server for continuous discussion sessions with OpenCode. Collaborate with GPT-5, Claude, Gemini, and other models through Claude Code.
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# 1. Install
|
|
31
|
+
uv pip install git+https://github.com/genomewalker/opencode-bridge.git
|
|
32
|
+
|
|
33
|
+
# 2. Register with Claude Code
|
|
34
|
+
opencode-bridge-install
|
|
35
|
+
|
|
36
|
+
# 3. Use in Claude Code
|
|
37
|
+
# The tools are now available - Claude will use them automatically
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Features
|
|
41
|
+
|
|
42
|
+
- **Continuous sessions**: Conversation history persists across messages
|
|
43
|
+
- **Multiple models**: Access all OpenCode models (GPT-5.x, Claude, Gemini, etc.)
|
|
44
|
+
- **Agent support**: plan, build, explore, general agents
|
|
45
|
+
- **Variant control**: Set reasoning effort (minimal → max)
|
|
46
|
+
- **File attachment**: Share code files for review
|
|
47
|
+
- **Session continuity**: Conversations continue across tool calls
|
|
48
|
+
|
|
49
|
+
## Installation
|
|
50
|
+
|
|
51
|
+
### With uv (recommended)
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
uv pip install git+https://github.com/genomewalker/opencode-bridge.git
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### With pip
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install git+https://github.com/genomewalker/opencode-bridge.git
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### From source
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
git clone https://github.com/genomewalker/opencode-bridge.git
|
|
67
|
+
cd opencode-bridge
|
|
68
|
+
pip install -e .
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Register with Claude Code
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
# Install (registers MCP server)
|
|
75
|
+
opencode-bridge-install
|
|
76
|
+
|
|
77
|
+
# Verify
|
|
78
|
+
claude mcp list
|
|
79
|
+
|
|
80
|
+
# Uninstall
|
|
81
|
+
opencode-bridge-uninstall
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Available Models
|
|
85
|
+
|
|
86
|
+
| Provider | Models |
|
|
87
|
+
|----------|--------|
|
|
88
|
+
| openai | gpt-5.2-codex, gpt-5.1-codex-max, gpt-5.1-codex-mini |
|
|
89
|
+
| github-copilot | claude-opus-4.5, claude-sonnet-4.5, gpt-5, gemini-2.5-pro |
|
|
90
|
+
| opencode | gpt-5-nano (free), glm-4.7-free, grok-code |
|
|
91
|
+
|
|
92
|
+
Run `opencode models` to see all available models.
|
|
93
|
+
|
|
94
|
+
## MCP Tools
|
|
95
|
+
|
|
96
|
+
| Tool | Description |
|
|
97
|
+
|------|-------------|
|
|
98
|
+
| `opencode_start` | Start a new session |
|
|
99
|
+
| `opencode_discuss` | Send a message |
|
|
100
|
+
| `opencode_plan` | Start planning discussion |
|
|
101
|
+
| `opencode_brainstorm` | Open-ended brainstorming |
|
|
102
|
+
| `opencode_review` | Review code |
|
|
103
|
+
| `opencode_models` | List available models |
|
|
104
|
+
| `opencode_agents` | List available agents |
|
|
105
|
+
| `opencode_model` | Change session model |
|
|
106
|
+
| `opencode_agent` | Change session agent |
|
|
107
|
+
| `opencode_variant` | Change reasoning effort |
|
|
108
|
+
| `opencode_config` | Show current configuration |
|
|
109
|
+
| `opencode_configure` | Set defaults (persisted) |
|
|
110
|
+
| `opencode_history` | Show conversation history |
|
|
111
|
+
| `opencode_sessions` | List all sessions |
|
|
112
|
+
| `opencode_switch` | Switch to another session |
|
|
113
|
+
| `opencode_end` | End current session |
|
|
114
|
+
| `opencode_health` | Server health check |
|
|
115
|
+
|
|
116
|
+
## Configuration
|
|
117
|
+
|
|
118
|
+
### Environment variables
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
export OPENCODE_MODEL="openai/gpt-5.2-codex"
|
|
122
|
+
export OPENCODE_AGENT="plan"
|
|
123
|
+
export OPENCODE_VARIANT="medium"
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Config file
|
|
127
|
+
|
|
128
|
+
`~/.opencode-bridge/config.json`:
|
|
129
|
+
```json
|
|
130
|
+
{
|
|
131
|
+
"model": "openai/gpt-5.2-codex",
|
|
132
|
+
"agent": "plan",
|
|
133
|
+
"variant": "medium"
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Variants (reasoning effort)
|
|
138
|
+
|
|
139
|
+
`minimal` → `low` → `medium` → `high` → `xhigh` → `max`
|
|
140
|
+
|
|
141
|
+
Higher variants use more reasoning tokens for complex tasks.
|
|
142
|
+
|
|
143
|
+
## Requirements
|
|
144
|
+
|
|
145
|
+
- Python 3.10+
|
|
146
|
+
- [OpenCode CLI](https://opencode.ai) installed
|
|
147
|
+
- Claude Code
|
|
148
|
+
|
|
149
|
+
## License
|
|
150
|
+
|
|
151
|
+
MIT
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
opencode_bridge/__init__.py,sha256=SkXVg907MuInd7UEYOjHjiiIIT46y4S2l20hE9cShKo,92
|
|
2
|
+
opencode_bridge/install.py,sha256=VOJNYUPxq88g0XizkHSQ9noM3Qcd3AfZxPUZInEKErk,1796
|
|
3
|
+
opencode_bridge/server.py,sha256=iQ2SHPy9rRp8K3hpRxOGgXcw7VdT9JX0amjUS87A6Fk,28475
|
|
4
|
+
opencode_bridge-0.1.3.dist-info/METADATA,sha256=Y6YMgJvGUT2k-Jr-wz1Npr_VUugcgkKy6gceHr8ND_0,3924
|
|
5
|
+
opencode_bridge-0.1.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
6
|
+
opencode_bridge-0.1.3.dist-info/entry_points.txt,sha256=8elAgeI-Sk7EPoV7kUr3CCgQyIAW2VfDj5ZXQ_9slCc,184
|
|
7
|
+
opencode_bridge-0.1.3.dist-info/RECORD,,
|