AtlatestRepositorycourier
1
# Courier3
Chat and notification channel server for Claude Code sessions, built with Sigil.5
## Architecture7
Courier is an MCP channel server that bridges chat messages (Telegram),8
desktop notifications, and relay connections (Unix sockets) into a running9
Claude Code session via the `claude/channel` protocol.11
Courier operates in three modes:12
- **Leader mode** (default): Telegram + relays + notifications13
- **Worker mode** (`--relay <name>`): relay-only, no Telegram14
- **Poller mode** (`--telegram-poller`): internal — the leader spawns15
itself in this mode as a supervised child that does the actual16
Telegram polling, so a wedged poll can never stall MCP servicing18
```19
courier/20
├── package.sgl # Package definition21
├── dev-redirects.sgl # Points deps at local sigil22
├── .env-courier # Telegram token + chat ID (gitignored)23
├── courier.yaml # Allowed senders (optional)24
├── src/courier/25
│ ├── main.sgl # Entry point: channel MCP server26
│ ├── config.sgl # Config loading (env + YAML)27
│ ├── telegram.sgl # send-message / send-media tools28
│ ├── poller.sgl # Telegram poller child + supervisor + watchdog29
│ ├── relay.sgl # Unix socket relay communication30
│ └── notify.sgl # Desktop notification tool (notify-send)31
└── test/32
└── test-config.sgl33
```35
## Configuration37
Secrets via environment (`.env-courier`):38
```39
COURIER_TELEGRAM_TOKEN=<bot-token-from-botfather>40
COURIER_TELEGRAM_CHAT_ID=<chat-id-for-replies>41
```43
Relay socket directory (optional, defaults to `~/.courier/relays/`):44
```45
COURIER_RELAY_DIR=/path/to/relay/sockets46
```48
Sender gating via `courier.yaml`:49
```yaml50
allowed-senders:51
- "123456789"52
```54
## Dependencies56
- **sigil-mcp** — MCP server framework (channel notifications)57
- **sigil-socket** — Unix domain socket support (relays)58
- **sigil-telegram** — Telegram Bot API polling and sending59
- **sigil-yaml** — Config file parsing60
- **sigil-log** — Structured logging62
## Building64
Always build within a Guix shell:66
```bash67
guix shell -m ../sigil/manifest.scm -- env CC=gcc \68
../sigil/build/release/bin/sigil build --redirects dev-redirects.sgl69
```71
## Testing73
```bash74
guix shell -m ../sigil/manifest.scm -- env CC=gcc \75
../sigil/build/release/bin/sigil -L build/dev/lib test --redirects dev-redirects.sgl76
```78
## Running80
### Leader mode (default)82
Courier is designed to be spawned by Claude Code as a channel MCP server:84
```bash85
claude --channels server:courier86
```88
Register it in `.mcp.json`:89
```json90
{91
"mcpServers": {92
"courier": {93
"command": "./build/dev/bin/courier",94
"env": {}95
}96
}97
}98
```100
### Worker mode102
Workers connect to a relay created by the leader:104
```bash105
courier --relay <name> --log logs/courier.log --log-level trace106
```108
No `.env-courier` needed — workers don't use Telegram.110
## Relays112
Relays are Unix domain sockets for bidirectional leader-worker messaging.113
See `DESIGN-relays.md` for the full design.115
**Leader tools**: `create-relay`, `close-relay`, `list-relays`117
**Socket path**: `~/.courier/relays/<name>.sock` (configurable via `COURIER_RELAY_DIR`)119
**Wire protocol**: Newline-delimited JSON:120
```json121
{"text":"message content","sender":"leader"}122
{"text":"message content","sender":"worker"}123
```125
**send-message routing**: The `to` parameter routes by type:126
- Relay name → sends via Unix socket127
- Chat ID → sends via Telegram128
- `"leader"` (worker mode) → sends to leader via relay130
## Key Implementation Notes132
- YAML dict keys from `yaml-decode` are **keywords** — use `keyword->string`133
- Struct constructors match the struct name (not `make-` prefixed)134
- The MCP server loop runs inside `with-async` so the poller supervisor135
and relay goroutines can interleave with stdin reads136
- `channel-notify!` from `(sigil mcp channel)` sends channel events137
- `channel-wait-initialized!` waits for MCP handshake before polling/reading138
- Telegram polling happens in a **separate child process** (see139
`poller.sgl`): the underlying TLS reads are blocking natives with no140
timeout, and a blackholed connection would otherwise freeze the141
single-threaded scheduler — the t-5ebb alive-but-wedged failure. The142
leader supervises the child over a stdout JSON event stream143
(hello/heartbeat/message), drains its stderr into the leader log, and144
a watchdog kills (SIGTERM + SIGKILL) and respawns it after 30s of145
silence; the child exits when its stdin reaches EOF (leader gone)146
- `make-line-reader` from `(sigil socket)` handles buffered line reading for relays147
- `tcp-accept` works for Unix domain sockets (generic `accept()` on any socket fd)