Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
b5eb2a8
Merge pull request #6 from jeffusion/support-iterm2
bfly123 Dec 19, 2025
006f046
Merge pull request #4 from yongbo2046/fix/bridge-stability
bfly123 Dec 19, 2025
bd044a2
Revert "fix: resolve bridge process crash and message forwarding issues"
bfly123 Dec 19, 2025
d300c8b
fix: bridge stability (macOS/tmux)
bfly123 Dec 19, 2025
f8cf9cc
fix(windows): add Windows compatibility fixes
bfly123 Dec 19, 2025
c285652
feat(windows): add BackendEnv config and install confirmation
bfly123 Dec 19, 2025
270188f
fix: add session file permission check and friendly error messages
bfly123 Dec 19, 2025
cae1d33
docs: add WezTerm installation note for Windows users
bfly123 Dec 19, 2025
4324fe3
fix(wezterm): improve send_text reliability for enter key
bfly123 Dec 20, 2025
2e92e89
feat(commands): update cask-w/gask-w to use background mode
bfly123 Dec 20, 2025
e6037e7
feat(install): use marker block for CLAUDE.md config
bfly123 Dec 20, 2025
ccedf85
fix(uninstall): clean commands from all possible locations
bfly123 Dec 20, 2025
36b9f4b
refactor(i18n): convert project to English
bfly123 Dec 20, 2025
eed93b4
fix(gemini): verify actual session files before resume
bfly123 Dec 20, 2025
a7e482c
fix(install.ps1): replace emoji with ASCII for PowerShell compatibility
bfly123 Dec 20, 2025
f016050
feat(i18n): add multi-language support with CCB_LANG
bfly123 Dec 20, 2025
6417a52
fix(cask-w): add flush=True and early startup signal for background mode
bfly123 Dec 20, 2025
50faee5
feat(fast-path): add /cask-w and /gask-w to direct triggers
bfly123 Dec 20, 2025
b5e7674
perf(cask-w): optimize startup with lazy import and lazy init
bfly123 Dec 20, 2025
de73bef
perf(cask-w,gask-w): optimize startup with lazy import and lazy init
bfly123 Dec 20, 2025
a8db8d3
fix(cask-w,gask-w): stop after sending, wait for user input
bfly123 Dec 20, 2025
d9ab3ac
feat(cask-w,gask-w): non-blocking send with background reply wait
bfly123 Dec 20, 2025
5fa59ce
fix(cask-w,gask-w): use double fork to fully detach background process
bfly123 Dec 20, 2025
d77be60
refactor(cask-w,gask-w): pure sync mode with Claude Code background task
bfly123 Dec 20, 2025
0f9e1dd
fix(gask-w): align with cask-w implementation
bfly123 Dec 21, 2025
aec6827
fix(gemini_comm): prevent returning stale messages on JSON parse failure
bfly123 Dec 21, 2025
8350b67
fix(codex_comm): use binary mode for consistent byte offset handling
bfly123 Dec 21, 2025
6d9da2f
fix(install.ps1): add UTF-8 BOM and encoding setup for PS5.1 compatib…
bfly123 Dec 21, 2025
fab9fb3
release: bump version to 2.2
bfly123 Dec 21, 2025
24a6645
fix(ccb): ensure -r restore reads cwd/.codex-session with correct enc…
bfly123 Dec 21, 2025
758ceae
fix(install.ps1): create .cmd wrappers and fix shebang for Windows
bfly123 Dec 21, 2025
c35c096
fix(cask-w,gask-w): move setup_windows_encoding to module level
bfly123 Dec 21, 2025
76da62b
fix(ccb): normalize paths for cross-platform session restore
bfly123 Dec 21, 2025
e1bcc53
fix(ccb): add work_dir validation for codex_session_id to prevent cro…
bfly123 Dec 21, 2025
b7713a4
fix(ccb): improve Linux path matching to reduce false negatives
bfly123 Dec 21, 2025
201809c
fix(cpend): improve Windows compatibility and prevent cross-project r…
bfly123 Dec 21, 2025
4195c89
fix(comm): prevent missing replies during log file transitions
bfly123 Dec 21, 2025
81fb84e
feat(update): show version upgrade info with commit hash and date
bfly123 Dec 21, 2025
e2e67c4
fix(update): read version even without git repo
bfly123 Dec 21, 2025
36a6dae
feat(update): embed git commit and date in ccb during install
bfly123 Dec 21, 2025
e665126
fix(update): remove inline comments from GIT_COMMIT/GIT_DATE
bfly123 Dec 21, 2025
5454ddd
feat(install): get git info from GitHub API for tarball installs
bfly123 Dec 21, 2025
66dc7c3
fix(gask-w): return last gemini message instead of first
bfly123 Dec 21, 2025
00a43eb
fix(session): remove session_id_filter to fix session confusion
bfly123 Dec 21, 2025
39f037c
fix(skills): update cask-w/gask-w to auto-cat on bash-notification
bfly123 Dec 21, 2025
bdf7b1b
feat(ccb): add -v/--version command to show version and update status
bfly123 Dec 22, 2025
e1eebcf
fix(install): Bash 3.2 heredoc compatibility for macOS
bfly123 Dec 22, 2025
bfcb1ad
docs: add AGPL-3.0 license with bilingual explanation
bfly123 Dec 22, 2025
d213e10
fix(install): handle mktemp failure with set -u
bfly123 Dec 22, 2025
bc752f1
refactor(ccb): use claude --continue instead of --resume
bfly123 Dec 22, 2025
9ba7cc4
fix(ccb): check history before using claude --continue
bfly123 Dec 22, 2025
4894094
fix: Windows subprocess UTF-8 encoding in terminal backends
blackrion Dec 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ __pycache__/
.claude-session
.claude/
*.mp4
tmp/
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
- 该文件夹是 claude_code_bridge (ccb) 开发文件夹,要注意兼容性,同时修改代码注意同时修改install,安装使用install安装,完成后要git增加版本并推送
- This is the claude_code_bridge (ccb) development folder. Pay attention to compatibility. When modifying code, also update install scripts. Use install.sh/install.ps1 to install. After completion, git commit and push.
48 changes: 48 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# AGPL-3.0 - GNU Affero General Public License v3.0

## English

This software is licensed under the AGPL-3.0 license, which means:

- **Attribution Required**: You must give appropriate credit, provide a link to the license, and indicate if changes were made. When using Claude Code Bridge (CCB), please credit the original project.

- **Open Source Required**: If you modify this software and distribute it or run it as a service, you must release your source code under AGPL-3.0.

- **Network Use (Copyleft)**: If you run this software as a network service (e.g., SaaS), users interacting with it over the network must be able to receive the source code.

- **No Closed-Source Use**: You cannot use this software in proprietary/closed-source projects unless you open-source the entire project under AGPL-3.0.

**In short**: You can use Claude Code Bridge for free, but if you build upon it, your code must also be open-sourced under AGPL-3.0 with attribution to this project. Closed-source commercial use requires a separate license.

For commercial licensing inquiries (closed-source use), please contact the maintainer.

---

## 中文

本软件采用 AGPL-3.0 许可协议,这意味着:

- **署名要求**:您必须注明出处,提供许可协议链接,并说明是否进行了修改。使用 Claude Code Bridge (CCB) 时,请注明项目来源。

- **开源要求**:如果您修改此软件并将其分发或作为服务运行,则必须根据 AGPL-3.0 发布您的源代码。

- **网络使用(Copyleft)**:如果您将此软件作为网络服务(例如 SaaS)运行,则通过网络与其交互的用户必须能够接收源代码。

- **禁止闭源使用**:您不能在专有/闭源项目中使用此软件,除非您将整个项目根据 AGPL-3.0 开源。

**简单来说**:您可以免费使用 Claude Code Bridge,但如果您基于它进行开发,您的代码也必须根据 AGPL-3.0 开源,并注明本项目。闭源商业用途需要单独的许可证。

对于商业许可咨询(闭源使用),请联系维护者。

---

## Full License Text

GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007

Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>

Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.

For the complete license text, see: https://www.gnu.org/licenses/agpl-3.0.txt
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

**Windows | macOS | Linux — One Tool, All Platforms**

[![Version](https://img.shields.io/badge/version-2.1-orange.svg)]()
[![Version](https://img.shields.io/badge/version-2.2-orange.svg)]()
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
[![Platform](https://img.shields.io/badge/platform-Linux%20%7C%20macOS%20%7C%20Windows-lightgrey.svg)]()
Expand Down Expand Up @@ -184,6 +184,8 @@ ccb update # Update to latest version
- Python 3.10+
- tmux or WezTerm (at least one; WezTerm recommended)

> **⚠️ Windows Users:** Always install WezTerm using the **native Windows .exe installer** from [wezfurlong.org/wezterm](https://wezfurlong.org/wezterm/), even if you use WSL. Do NOT install WezTerm inside WSL. After installation, configure WezTerm to connect to WSL via `wsl.exe` as the default shell. This ensures proper split-pane functionality.

## Uninstall

```bash
Expand Down Expand Up @@ -370,8 +372,9 @@ ccb update # 更新到最新版本
## 依赖

- Python 3.10+
- tmux 或 WezTerm(至少安装一个),强烈推荐wezterm
- tmux 或 WezTerm(至少安装一个),强烈推荐 WezTerm

> **⚠️ Windows 用户注意:** 必须使用 **Windows 原生 .exe 安装包** 安装 WezTerm([下载地址](https://wezfurlong.org/wezterm/)),即使你使用 WSL 也是如此。**不要在 WSL 内部安装 WezTerm**。安装完成后,可在 WezTerm 设置中将默认 shell 配置为 `wsl.exe`,即可无缝接入 WSL 环境,同时保证分屏功能正常工作。

## 卸载

Expand Down
16 changes: 9 additions & 7 deletions bin/cask
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
cask - 将消息转发到 Codex 会话
cask - Forward message to Codex session
"""
from __future__ import annotations
import json
Expand All @@ -12,12 +12,14 @@ from typing import Optional, Tuple
script_dir = Path(__file__).resolve().parent
lib_dir = script_dir.parent / "lib"
sys.path.insert(0, str(lib_dir))
from compat import setup_windows_encoding
setup_windows_encoding()

from terminal import get_backend_for_session, get_pane_id_from_session


def _usage() -> None:
print("用法: cask <消息>", file=sys.stderr)
print("Usage: cask <message>", file=sys.stderr)


def _load_session() -> Optional[dict]:
Expand Down Expand Up @@ -46,10 +48,10 @@ def _load_session() -> Optional[dict]:
def _resolve_session() -> Tuple[dict, str]:
data = _load_session()
if not data:
raise RuntimeError("❌ 未找到 Codex 会话,请先运行 ccb up codex")
raise RuntimeError("❌ Codex session not found, please run ccb up codex first")
pane_id = get_pane_id_from_session(data)
if not pane_id:
raise RuntimeError("❌ 会话配置无效")
raise RuntimeError("❌ Session config invalid")
return data, pane_id


Expand All @@ -67,12 +69,12 @@ def main(argv: list[str]) -> int:
data, pane_id = _resolve_session()
backend = get_backend_for_session(data)
if not backend:
raise RuntimeError("❌ 无法初始化终端后端")
raise RuntimeError("❌ Cannot initialize terminal backend")
if not backend.is_alive(pane_id):
terminal = data.get("terminal", "tmux")
raise RuntimeError(f"❌ {terminal} 会话不存在: {pane_id}\n提示: 请确认 ccb 正在运行")
raise RuntimeError(f"❌ {terminal} session not found: {pane_id}\nHint: Please confirm ccb is running")
backend.send_text(pane_id, raw_command)
print(f"✅ 已发送到 Codex ({pane_id})")
print(f"✅ Sent to Codex ({pane_id})")
return 0
except Exception as exc:
print(exc, file=sys.stderr)
Expand Down
77 changes: 68 additions & 9 deletions bin/cask-w
Original file line number Diff line number Diff line change
@@ -1,35 +1,94 @@
#!/usr/bin/env python3
"""
cask-w - 同步发送消息到 Codex 并等待回复
cask-w - Send message to Codex and wait for reply (pure sync mode)
Designed to be run with Claude Code's run_in_background=true
"""
from __future__ import annotations
import os
import sys
from pathlib import Path
import json

script_dir = Path(__file__).resolve().parent
lib_dir = script_dir.parent / "lib"
sys.path.insert(0, str(lib_dir))

from codex_comm import CodexCommunicator
from compat import setup_windows_encoding
setup_windows_encoding()


def main(argv: list[str]) -> int:
if len(argv) <= 1:
print("用法: cask-w <消息>", file=sys.stderr)
print("Usage: cask-w <message>", file=sys.stderr)
return 1

message = " ".join(argv[1:]).strip()
if not message:
print("❌ 消息内容不能为空", file=sys.stderr)
print("❌ Message cannot be empty", file=sys.stderr)
return 1

from codex_comm import CodexCommunicator
from i18n import t

def save_pending_state(state: dict) -> None:
session_file = Path.cwd() / ".codex-session"
if not session_file.exists():
return
try:
with session_file.open("r", encoding="utf-8-sig") as handle:
data = json.load(handle)
data["pending_state"] = {
"log_path": str(state.get("log_path")) if state.get("log_path") else None,
"offset": int(state.get("offset", 0) or 0),
}
tmp_file = session_file.with_suffix(".tmp")
with tmp_file.open("w", encoding="utf-8") as handle:
json.dump(data, handle, ensure_ascii=False, indent=2)
os.replace(tmp_file, session_file)
except Exception:
return

try:
comm = CodexCommunicator()
reply = comm.ask_sync(message, timeout=0)
return 0 if reply else 1
comm = CodexCommunicator(lazy_init=True)

# Check session health
healthy, status = comm._check_session_health_impl(probe_terminal=False)
if not healthy:
print(f"❌ Session error: {status}", file=sys.stderr)
return 1

# Send message
print(f"🔔 {t('sending_to', provider='Codex')}", flush=True)
marker, state = comm._send_message(message)
comm._remember_codex_session(state.get("log_path") or comm.log_reader.current_log_path())

# Pure sync wait (default 1 hour, configurable via CCB_SYNC_TIMEOUT)
sync_timeout = float(os.environ.get("CCB_SYNC_TIMEOUT", "3600.0"))
message_reply, _ = comm.log_reader.wait_for_message(state, sync_timeout)

# Save to cache
cache_dir = Path.home() / ".cache" / "ccb"
cache_dir.mkdir(parents=True, exist_ok=True)
reply_file = cache_dir / "codex_last_reply.txt"

if message_reply:
print(f"🤖 {t('reply_from', provider='Codex')}")
print(message_reply)
reply_file.write_text(message_reply, encoding="utf-8")
else:
print(f"⏰ Timeout after {int(sync_timeout)}s")
save_pending_state(state)
return 0

except KeyboardInterrupt:
# Best-effort: preserve pending state so /cpend can fetch later.
try:
save_pending_state(locals().get("state", {}) if isinstance(locals().get("state"), dict) else {})
except Exception:
pass
print("❌ Interrupted", file=sys.stderr)
return 130
except Exception as exc:
print(exc, file=sys.stderr)
print(f"❌ {exc}", file=sys.stderr)
return 1


Expand Down
76 changes: 56 additions & 20 deletions bin/cpend
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
cpend - 查看 Codex 最新回复
cpend - View latest Codex reply
"""

import json
Expand All @@ -10,67 +10,103 @@ from pathlib import Path
script_dir = Path(__file__).resolve().parent
lib_dir = script_dir.parent / "lib"
sys.path.insert(0, str(lib_dir))
from compat import setup_windows_encoding
setup_windows_encoding()

from i18n import t
from session_utils import safe_write_session

try:
from codex_comm import CodexCommunicator
except ImportError as exc:
print(f"导入失败: {exc}")
print(f"Import failed: {exc}")
sys.exit(1)


def _load_cached_reply() -> str | None:
"""Load cached reply from background cask-w process"""
cache_file = Path.home() / ".cache" / "ccb" / "codex_last_reply.txt"
if not cache_file.exists():
return None
try:
content = cache_file.read_text(encoding="utf-8").strip()
if content:
cache_file.unlink() # Clear after reading
return content
except Exception:
pass
return None


def _load_pending_state() -> dict:
"""从 .codex-session 加载 cask-w 超时时保存的状态"""
"""Load pending state saved by cask-w timeout from .codex-session"""
session_file = Path.cwd() / ".codex-session"
if not session_file.exists():
return {}
try:
with session_file.open("r", encoding="utf-8") as f:
with session_file.open("r", encoding="utf-8-sig") as f:
data = json.load(f)
return data.get("pending_state", {})
pending = data.get("pending_state", {})
return pending if isinstance(pending, dict) else {}
except Exception:
return {}


def _clear_pending_state() -> None:
"""清除 pending_state"""
"""Clear pending_state"""
session_file = Path.cwd() / ".codex-session"
if not session_file.exists():
return
try:
with session_file.open("r", encoding="utf-8") as f:
with session_file.open("r", encoding="utf-8-sig") as f:
data = json.load(f)
if "pending_state" in data:
del data["pending_state"]
with session_file.open("w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
safe_write_session(session_file, json.dumps(data, ensure_ascii=False, indent=2))
except Exception:
pass


def main() -> int:
try:
# First check cached reply from background cask-w
cached = _load_cached_reply()
if cached:
print(f"🤖 Codex reply:")
print(cached)
return 0

comm = CodexCommunicator()

pending = _load_pending_state()
if pending and pending.get("log_path"):
state = {
"log_path": Path(pending["log_path"]),
"offset": pending.get("offset", 0),
}
message, _ = comm.log_reader.try_get_message(state)
if message:
_clear_pending_state()
print(message)
return 0
try:
log_path = Path(str(pending.get("log_path"))).expanduser()
except Exception:
log_path = None

# Avoid falling back to "global latest" if the pending log path is missing,
# otherwise cpend may display an unrelated reply from another project.
if log_path and log_path.exists():
try:
offset = int(pending.get("offset", 0) or 0)
except Exception:
offset = 0
state = {"log_path": str(log_path), "offset": offset}
message, _ = comm.log_reader.try_get_message(state)
if message:
_clear_pending_state()
print(message)
return 0

output = comm.consume_pending(display=False)
if output:
print(output)
else:
print('暂无 Codex 回复')
print(t('no_reply_available', provider='Codex'))
return 0
except Exception as exc:
print(f"❌ 执行失败: {exc}")
print(f"❌ {t('execution_failed', error=exc)}")
return 1


Expand Down
Loading