Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,20 @@ pipx install coding-bridge # recommended
pip install coding-bridge
```

On Windows, if `pipx` is not installed, use the Python launcher directly:

```powershell
py -m pip install --user --upgrade coding-bridge
```

If `pip` reports an old `0.1.0` editable install or `coding_bridge` cannot be
imported, remove the stale install before upgrading:

```powershell
py -m pip uninstall -y coding-bridge
py -m pip install --user --no-cache-dir "coding-bridge[wechat]"
```

For the ASCII-QR pairing helper, install the optional extra:

```bash
Expand Down Expand Up @@ -161,6 +175,18 @@ or a secrets-file path (`token_file`). Export it before starting:
export WECHAT_TOKEN_MY_WECHAT="…"
```

Windows PowerShell:

```powershell
$env:WECHAT_TOKEN_MY_WECHAT = "…"
```

Windows Command Prompt:

```bat
set "WECHAT_TOKEN_MY_WECHAT=…"
```

#### Provider sign-in

Each message runs a real **Claude Code** (or Codex) turn **on your machine**, so
Expand Down
7 changes: 6 additions & 1 deletion coding_bridge/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,9 @@
code execution stays local; the bridge only relays messages.
"""

__version__ = "0.1.0"
from importlib.metadata import PackageNotFoundError, version

try:
__version__ = version("coding-bridge")
except PackageNotFoundError:
__version__ = "0+unknown"
5 changes: 2 additions & 3 deletions coding_bridge/channels/wechat/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,8 @@
and hands anything left to the dispatcher — policy (trigger prefix,
allowlist, rate limit) lives one layer up in P7.
* ``POST /api/messages/send`` with ``Authorization: Bearer <TOKEN>`` — returns
``202 Accepted`` because the gateway queues a background UI task. P2 fires and
forgets; P7 will poll ``/api/messages/tasks/{id}`` for delivery
confirmation.
``202 Accepted`` because the gateway queues a background UI task. Delivery
diagnostics use ``GET /api/tasks/{id}``.

The adapter is import-safe without extras — it delegates to :mod:`websockets`
and :mod:`httpx`, both already required by CodingBridge core. The
Expand Down
2 changes: 1 addition & 1 deletion coding_bridge/channels/wechat/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ async def get_task_status(self, task_id: str) -> dict[str, Any]:
raise ValueError("task_id contains invalid characters")
# Belt-and-suspenders: `quote(safe="")` still produces a plain
# segment since the regex already rejects unsafe input.
path = f"/api/messages/tasks/{quote(task_id, safe='')}"
path = f"/api/tasks/{quote(task_id, safe='')}"
resp = await self._client.get(path)
resp.raise_for_status()
try:
Expand Down
4 changes: 2 additions & 2 deletions coding_bridge/channels_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ async def _doctor_one(inst: WeChatInstanceConfig) -> tuple[bool, str]:
"""Return (ok, message) for one instance. Never raises; never logs secrets.

Actually tests that the token is accepted by hitting an authenticated
endpoint (``GET /api/messages/tasks/<probe>``). The gateway's response:
endpoint (``GET /api/tasks/<probe>``). The gateway's response:

* 401 → token rejected → FAIL (loudly, since this is the whole point of doctor)
* 404 → token accepted but probe id unknown → PASS (this is expected)
Expand Down Expand Up @@ -193,7 +193,7 @@ async def _doctor_one(inst: WeChatInstanceConfig) -> tuple[bool, str]:
resp = await client._client.get("/health") # noqa: SLF001
if 200 <= resp.status_code < 300:
return False, (
f"reachable at /health but /api/messages/tasks "
f"reachable at /health but /api/tasks "
f"returned {code} (gateway may be misconfigured)"
)
except Exception: # noqa: BLE001
Expand Down
23 changes: 7 additions & 16 deletions tests/test_channels_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,7 @@ def test_refuses_to_overwrite(self, tmp_path: Path) -> None:
# Original file untouched
assert s.channels_config_path.read_text(encoding="utf-8") == "# already here"

@pytest.mark.skipif(
os.name == "nt", reason="POSIX permission bits don't apply on Windows"
)
@pytest.mark.skipif(os.name == "nt", reason="POSIX permission bits don't apply on Windows")
def test_posix_permissions_are_0600(self, tmp_path: Path) -> None:
import stat as _stat

Expand Down Expand Up @@ -134,7 +132,7 @@ def test_enabled_reachable_endpoint_marked_ok(

def patched_init(self, base_url, token, *, timeout=10.0, transport=None):
def handler(request: httpx.Request) -> httpx.Response:
assert request.url.path.startswith("/api/messages/tasks/")
assert request.url.path.startswith("/api/tasks/")
# Token was accepted; the probe task just doesn't exist.
return httpx.Response(404, json={"error": "task not found"})

Expand Down Expand Up @@ -171,6 +169,7 @@ def test_enabled_401_marked_failure(

def patched_init(self, base_url, token, *, timeout=10.0, transport=None):
def handler(request: httpx.Request) -> httpx.Response:
assert request.url.path.startswith("/api/tasks/")
return httpx.Response(401, json={"error": "unauthorized"})

original_init(
Expand Down Expand Up @@ -352,9 +351,7 @@ def script(sid):
_install_stub_factory(monkeypatch, script)
s = _settings(tmp_path)
rc, out, err = _capture(
lambda: channels_cli.cmd_channels_smoke(
s, provider="claude", prompt="x", timeout=5.0
)
lambda: channels_cli.cmd_channels_smoke(s, provider="claude", prompt="x", timeout=5.0)
)
assert rc == 1
assert "kaboom" in out
Expand All @@ -369,9 +366,7 @@ def script(sid):
_install_stub_factory(monkeypatch, script)
s = _settings(tmp_path)
rc, out, err = _capture(
lambda: channels_cli.cmd_channels_smoke(
s, provider="claude", prompt="x", timeout=5.0
)
lambda: channels_cli.cmd_channels_smoke(s, provider="claude", prompt="x", timeout=5.0)
)
assert rc == 1

Expand All @@ -387,9 +382,7 @@ def script(_sid):
_install_stub_factory(monkeypatch, script)
s = _settings(tmp_path)
rc, out, err = _capture(
lambda: channels_cli.cmd_channels_smoke(
s, provider="claude", prompt="x", timeout=0.2
)
lambda: channels_cli.cmd_channels_smoke(s, provider="claude", prompt="x", timeout=0.2)
)
assert rc == 1
assert "timed out" in out
Expand All @@ -399,9 +392,7 @@ def test_smoke_unknown_provider_exits_2(self, tmp_path: Path) -> None:
# --provider must fail loudly (rc=2), not silently fall back to Claude.
s = _settings(tmp_path)
rc, out, err = _capture(
lambda: channels_cli.cmd_channels_smoke(
s, provider="gpt4", prompt="x", timeout=5.0
)
lambda: channels_cli.cmd_channels_smoke(s, provider="gpt4", prompt="x", timeout=5.0)
)
assert rc == 2
assert "unknown provider" in err
Expand Down
7 changes: 7 additions & 0 deletions tests/test_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from importlib.metadata import version

import coding_bridge


def test_runtime_version_matches_distribution_metadata():
assert coding_bridge.__version__ == version("coding-bridge")
12 changes: 4 additions & 8 deletions tests/test_wechat_task_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
@pytest.mark.asyncio
async def test_get_task_status_returns_json_dict() -> None:
def handler(request: httpx.Request) -> httpx.Response:
assert request.url.path == "/api/messages/tasks/task-42"
assert request.url.path == "/api/tasks/task-42"
assert request.headers.get("authorization") == "Bearer tok"
return httpx.Response(200, json={"status": "delivered", "task_id": "task-42"})

Expand Down Expand Up @@ -46,9 +46,7 @@ def handler(request: httpx.Request) -> httpx.Response:
seen.append(str(request.url))
return httpx.Response(200, json={})

client = WeChatClient(
"http://wechat.local", "tok", transport=httpx.MockTransport(handler)
)
client = WeChatClient("http://wechat.local", "tok", transport=httpx.MockTransport(handler))
try:
for bad in [
"42?admin=1",
Expand All @@ -71,13 +69,11 @@ def handler(request: httpx.Request) -> httpx.Response:
async def test_get_task_status_accepts_safe_uuid_shaped_id() -> None:
def handler(request: httpx.Request) -> httpx.Response:
# Ensure the path was NOT tampered with
assert request.url.path == "/api/messages/tasks/abc-DEF_123"
assert request.url.path == "/api/tasks/abc-DEF_123"
assert request.url.query == b""
return httpx.Response(200, json={"status": "ok"})

client = WeChatClient(
"http://wechat.local", "tok", transport=httpx.MockTransport(handler)
)
client = WeChatClient("http://wechat.local", "tok", transport=httpx.MockTransport(handler))
try:
assert await client.get_task_status("abc-DEF_123") == {"status": "ok"}
finally:
Expand Down
Loading