Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
a4991b0
feat: add multi-session router with Telegram topic support
Filyus Apr 12, 2026
d39ff65
feat: track Claude sessionId to rebind topics on --resume and /resume
Filyus Apr 20, 2026
3130b1c
refactor: extract bot-token discovery to shared `token` module
Filyus Apr 20, 2026
f8b8daa
feat: add `fs_perms` helpers for owner-only router state
Filyus Apr 20, 2026
10db5b1
feat: parse supergroup_id as i64 with flexible deserialization
Filyus Apr 20, 2026
1b28ab1
feat: fall back to env/.env for router bot_token
Filyus Apr 20, 2026
33bc082
feat: fail loudly when router prerequisites are missing
Filyus Apr 20, 2026
771b972
feat: enforce owner-only perms on router state (Unix)
Filyus Apr 20, 2026
f120cfc
feat: warn on world-readable router config.json
Filyus Apr 20, 2026
cc5180e
docs(router): explicit allowlist invariant in handle_update
Filyus Apr 20, 2026
bc037b3
ci: run tests on ubuntu and macos matrix
Filyus Apr 20, 2026
9f920e2
test(router): reconcile + lock contention coverage
Filyus Apr 20, 2026
c0a1538
chore: cargo fmt + fix unnecessary_unwrap in reconcile_session
Filyus Apr 20, 2026
6cca081
fix(router): reject invalid unix pids
Filyus Apr 25, 2026
7387c6e
fix(session): support macos parent lookup
Filyus Apr 25, 2026
ef4cb01
fix(security): secure rewritten registrations
Filyus Apr 25, 2026
53f5048
fix(config): warn on positive supergroup id
Filyus Apr 25, 2026
6edb0a5
fix: use libproc for macos parent process lookup
Filyus Apr 25, 2026
c7976a1
fix: use ps for macos parent lookup
Filyus Apr 25, 2026
20566c0
test: cover unix parent process lookup
Filyus Apr 25, 2026
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
28 changes: 26 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,28 @@ jobs:
- name: cargo test
run: cargo test --all-features

test:
name: Test (${{ matrix.os }})
needs: check
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable

- name: Cache cargo registry + build
uses: Swatinem/rust-cache@v2
with:
key: test-${{ matrix.os }}

- name: cargo test
run: cargo test --all-targets --all-features

build:
name: Build (${{ matrix.target }})
needs: check
Expand Down Expand Up @@ -87,19 +109,21 @@ jobs:
if: matrix.cross
run: cross build --release --target ${{ matrix.target }}

- name: Package binary (unix)
- name: Package binaries (unix)
if: runner.os != 'Windows'
run: |
mkdir -p dist
cp target/${{ matrix.target }}/release/hdcd-telegram dist/
cp target/${{ matrix.target }}/release/hdcd-router dist/
cd dist && tar czf ../hdcd-telegram-${{ matrix.artifact }}.tar.gz *

- name: Package binary (windows)
- name: Package binaries (windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
New-Item -ItemType Directory -Force -Path dist
Copy-Item target/${{ matrix.target }}/release/hdcd-telegram.exe dist/
Copy-Item target/${{ matrix.target }}/release/hdcd-router.exe dist/
Compress-Archive -Path dist/* -DestinationPath hdcd-telegram-${{ matrix.artifact }}.zip

- name: Upload artifact
Expand Down
28 changes: 26 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ jobs:
run: |
mkdir -p dist
cp target/${{ matrix.target }}/release/hdcd-telegram dist/
cp target/${{ matrix.target }}/release/hdcd-router dist/
cd dist && tar czf ../hdcd-telegram-${{ github.ref_name }}-${{ matrix.artifact }}.tar.gz *

- name: Package (windows)
Expand All @@ -78,6 +79,7 @@ jobs:
run: |
New-Item -ItemType Directory -Force -Path dist
Copy-Item target/${{ matrix.target }}/release/hdcd-telegram.exe dist/
Copy-Item target/${{ matrix.target }}/release/hdcd-router.exe dist/
Compress-Archive -Path dist/* -DestinationPath hdcd-telegram-${{ github.ref_name }}-${{ matrix.artifact }}.zip

- name: Generate SHA256 checksum (unix)
Expand Down Expand Up @@ -155,12 +157,34 @@ jobs:
xattr -d com.apple.quarantine ./hdcd-telegram
```

## Quick start
## Included binaries

Each archive contains two binaries:
- **hdcd-telegram** — MCP server (1:1 standalone mode, or `--router` for multi-session)
- **hdcd-router** — multi-session router that creates Telegram forum topics per session

## Quick start (standalone)

```bash
tar xzf hdcd-telegram-${{ github.ref_name }}-<platform>.tar.gz
# Add to .mcp.json, then:
claude --dangerously-load-development-channels server:telegram
```

See [README](https://github.com/gohyperdev/hdcd-telegram#quick-start) for full setup.
## Quick start (router)

```bash
# 1. Configure router
mkdir -p ~/.claude/channels/telegram-router
cat > ~/.claude/channels/telegram-router/config.json << 'EOF'
{"bot_token":"YOUR_TOKEN","supergroup_id":"YOUR_GROUP_ID","allowed_users":["YOUR_USER_ID"]}
EOF

# 2. Start router (once)
./hdcd-router

# 3. Launch Claude Code sessions with --router flag
claude --dangerously-load-development-channels server:telegram
```

See [README](https://github.com/gohyperdev/hdcd-telegram#router-mode-multi-session) for full setup.
34 changes: 28 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,21 @@ tokio-util = "0.7"
regex-lite = "0.1"
hex = "0.4"
dirs = "6"
fs4 = "0.13"

[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.61", features = ["Win32_System_Threading", "Win32_Foundation", "Win32_System_Diagnostics_ToolHelp"] }

[target.'cfg(unix)'.dependencies]
libc = "0.2"

[[bin]]
name = "hdcd-telegram"
path = "src/main.rs"

[[bin]]
name = "hdcd-router"
path = "src/router/main.rs"

[dev-dependencies]
tokio = { version = "1", features = ["test-util"] }
Expand Down
129 changes: 128 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,131 @@ Done. Your next DM reaches Claude.

No ports opened. No webhooks. Everything runs locally over stdio + outbound HTTPS to `api.telegram.org`.

## Router mode (multi-session)

The default standalone mode supports one Claude Code session per bot token. If you run multiple sessions simultaneously (parallel agents, CI workers, different projects), each would need its own bot -- and only one can poll at a time (Telegram returns 409 Conflict otherwise).

**Router mode** solves this with a two-binary architecture:

```
┌─────────────┐ ┌────────────────┐ ┌──────────────────┐ ┌────────────┐
│ Telegram │────▶│ hdcd-router │────▶│ hdcd-telegram │────▶│ Claude Code│
│ (forum │◀────│ (single poll) │◀────│ (--router mode) │◀────│ (session) │
│ topics) │ └────────────────┘ └──────────────────┘ └────────────┘
└─────────────┘ one process one per session one per session
```

- **hdcd-router** holds the single Telegram polling connection, creates a forum topic per session, and routes messages via filesystem IPC (JSONL mailbox files)
- **hdcd-telegram --router** runs in router mode -- no direct Telegram polling, reads from inbox, writes to outbox
- Each session gets its own forum topic in a Telegram supergroup
- Topics start with a placeholder title (project folder + short session ID, e.g. `hdcd-telegram #a1b2c3`) and Claude renames them via the `set_topic_title` MCP tool once the conversation topic becomes clear

### Router setup

#### 1. Create a Telegram supergroup with topics

1. Create a new group in Telegram (any name, e.g. "Claude Sessions")
2. Open group settings > **Group Type** > set to **Public** or **Private** (this converts it to a supergroup)
3. Go to settings > **Topics** > toggle **ON** (this option only appears after the group is a supergroup)
4. Add your bot to the group
5. Promote the bot to admin with **Manage Topics** enabled (required to create, close, and reopen forum topics). Other admin permissions are optional.

> **Tip:** If you don't see the Topics toggle, make sure you completed step 2 first — Topics are only available in supergroups, not regular groups.

#### 2. Get your supergroup ID and user ID

Send any message in the group, then query the bot API:

```bash
curl -s "https://api.telegram.org/bot<YOUR_TOKEN>/getUpdates" | jq '.result[-1].message'
```

From the response:
- **supergroup ID**: `.chat.id` — negative, starts with `-100` (e.g. `-1001234567890`)
- **your user ID**: `.from.id` — positive number (e.g. `123456789`)

#### 3. Configure the router

```bash
mkdir -p ~/.claude/channels/telegram-router
cat > ~/.claude/channels/telegram-router/config.json << 'EOF'
{
"bot_token": "YOUR_BOT_TOKEN",
"supergroup_id": "-100XXXXXXXXXX",
"allowed_users": ["YOUR_TELEGRAM_USER_ID"]
}
EOF
```

Optional config fields:

| Field | Default | Description |
|---|---|---|
| `close_topic_on_disconnect` | `true` | Close forum topic when session disconnects |
| `outbox_poll_interval_ms` | `200` | How often to check outbox files |
| `health_check_interval_s` | `30` | How often to check if session PIDs are alive |
| `auto_shutdown_delay_s` | `60` | Shut down router after this many seconds with no active sessions (0 = stay running) |

#### 4. Launch Claude Code sessions

The router starts automatically when needed. `hdcd-telegram --router` checks `router.lock` on startup -- if the router isn't running, it spawns `hdcd-router` as a background process (both binaries must be in the same directory). The router shuts down automatically after 60 seconds with no active sessions.

To start the router manually instead: `./hdcd-router`

Each session uses `hdcd-telegram` in router mode. Add to `.mcp.json`:

```json
{
"mcpServers": {
"telegram": {
"command": "/path/to/hdcd-telegram",
"args": ["--router"]
}
}
}
```

Then launch as usual:

```bash
claude --dangerously-load-development-channels server:telegram
```

Each session automatically registers with the router, gets a forum topic, and starts receiving messages.

#### Router commands

Send these in the General topic of your supergroup:

| Command | Description |
|---|---|
| `/status` | List active sessions with PIDs, topic IDs, and working directories |
| `/kill <session-id>` | Close a session's forum topic |
| `/help` | Show available commands |

### How router mode works

```
~/.claude/channels/telegram-router/
config.json ← router config
sessions.json ← persistent session registry
router.lock ← heartbeat file (PID + timestamp)
register/ ← session registration files
<session-id>.json
inbox/ ← Telegram → session (router writes, MCP reads)
<session-id>.jsonl
outbox/ ← session → Telegram (MCP writes, router reads)
<session-id>.jsonl
```

1. `hdcd-telegram --router` writes a registration file to `register/`
2. `hdcd-router` detects it, creates a forum topic, updates `sessions.json`
3. Telegram messages in the topic are written to `inbox/<session-id>.jsonl`
4. MCP server reads inbox, converts to `notifications/claude/channel`
5. Claude's replies (via `reply` tool) are written to `outbox/<session-id>.jsonl`
6. Router reads outbox, sends to the correct forum topic
7. On disconnect (stdin EOF or dead PID), topic is closed

## Troubleshooting

### Authentication: channels require claude.ai OAuth
Expand Down Expand Up @@ -288,6 +413,7 @@ If whisper or ffmpeg are not installed, voice messages are forwarded as `"(voice
| `WHISPER_MODEL` | `small` | Whisper model size (`tiny`, `base`, `small`, `medium`, `large`) |
| `WHISPER_LANGUAGE` | auto-detect | Language hint (`Polish`, `English`, etc.) |
| `HDCD_ECHO_TRANSCRIPT` | `true` | Send transcript back for user confirmation before delivering to Claude |
| `ROUTER_STATE_DIR` | `~/.claude/channels/telegram-router` | Router state directory (config.json, sessions, mailbox) |
| `RUST_LOG` | `hdcd_telegram=info` | Log level filter ([`tracing-subscriber`](https://docs.rs/tracing-subscriber) format) |

## Running alongside the official Telegram plugin
Expand Down Expand Up @@ -318,11 +444,12 @@ If only `TELEGRAM_BOT_TOKEN` is set, hdcd-telegram uses it as before — fully b
## Features

- **All 8 message types**: text, photo, document, voice, audio, video, video note, sticker
- **4 MCP tools**: `reply` (with chunking, threading, file attachments, MarkdownV2), `react`, `edit_message`, `download_attachment`
- **5 MCP tools**: `reply` (with chunking, threading, file attachments, MarkdownV2), `react`, `edit_message`, `download_attachment`, `set_topic_title` (router mode — lets Claude rename its forum topic when the conversation shifts)
- **Access control**: pairing flow (6-hex code), allowlist, group policies with @mention gating
- **Permission relay**: inline keyboard for remote tool-use approval/denial (`claude/channel/permission`)
- **Voice transcription** (optional): automatic speech-to-text via [whisper](https://github.com/openai/whisper) with echo-back confirmation flow
- **Bot commands**: `/start`, `/help`, `/status`
- **Router mode**: multi-session support via `hdcd-router` + forum topics (one topic per session, `/status`, `/kill` commands)
- **409 Conflict retry** with exponential backoff
- **Clean shutdown** on stdin EOF (no zombie polling)

Expand Down
Loading
Loading