npm package: @hyperdrive.bot/resilient-channel
Description: Transport-agnostic self-healing MCP channel middleware for Claude Code
Latest version: 0.2.2

# Resilient Channel

Transport-agnostic self-healing MCP channel middleware for [Claude Code](https://docs.anthropic.com/en/docs/claude-code).

Events buffer during session disconnects and replay automatically on reconnection. Works with Telegram, Slack, webhooks, or any custom transport.

## Why

Claude Code [channels](https://code.claude.com/docs/en/channels) are session-scoped — when a session ends, events are lost. Resilient Channel adds a persistence layer so nothing gets dropped:

- **Events buffer to disk** when Claude disconnects
- **Automatic replay** when a new session connects
- **Context summaries** survive across sessions via the `update_context` tool
- **Audit trail** logs every event per session

Inspired by [NVIDIA NemoClaw](https://github.com/NVIDIA/NemoClaw)'s state persistence patterns.

## Quick Start

### 1. Add to your `.mcp.json`

Pick a transport and add it to your project or user-level `.mcp.json`:

**Webhook** (simplest — test with curl):
```json
{
  "mcpServers": {
    "resilient-webhook": {
      "command": "bunx",
      "args": ["-y", "@hyperdrive.bot/resilient-channel/src/server.ts"]
    }
  }
}
```

**Telegram:**
```json
{
  "mcpServers": {
    "resilient-telegram": {
      "command": "bunx",
      "args": ["-y", "@hyperdrive.bot/resilient-channel/src/server-telegram.ts"]
    }
  }
}
```

**Slack:**
```json
{
  "mcpServers": {
    "resilient-slack": {
      "command": "bunx",
      "args": ["-y", "@hyperdrive.bot/resilient-channel/src/server-slack.ts"]
    }
  }
}
```

Or clone locally:
```bash
git clone https://github.com/hyperdrive-bot/claude-code-resilient-channel.git
cd claude-code-resilient-channel
bun install
```

Then use absolute paths in `.mcp.json`:
```json
{
  "mcpServers": {
    "resilient-telegram": {
      "command": "bun",
      "args": ["/path/to/claude-code-resilient-channel/src/server-telegram.ts"]
    }
  }
}
```

### 2. Launch Claude Code

```bash
# Set env vars for your transport, then:
claude --dangerously-load-development-channels server:resilient-webhook
```

### 3. Send a message

```bash
# Webhook
curl -X POST localhost:8788 -d "build failed on main"

# Telegram — DM your bot
# Slack — DM @your-bot or @mention it in a channel
```

## Transports

### Webhook (one-way)

HTTP POST listener on localhost. No auth, no external deps. Perfect for CI webhooks, monitoring alerts, or testing.

```bash
claude --dangerously-load-development-channels server:resilient-webhook
```

```bash
# Send events
curl -X POST localhost:8788 -d "deploy succeeded"
curl -X POST localhost:8788 -H "x-chat-id: ci" -d "tests passed (42/42)"
```

### Telegram (two-way)

Polls Telegram for messages. Supports text, voice notes (auto-transcribed via Whisper), photos, documents, and video (with key frame extraction + transcription).

```bash
TELEGRAM_BOT_TOKEN=<token> \
TELEGRAM_ALLOWED_IDS=<your-user-id> \
claude --dangerously-skip-permissions \
  --dangerously-load-development-channels server:resilient-telegram
```

**Setup:**
1. Create a bot via [@BotFather](https://t.me/BotFather)
2. Find your user ID via [@userinfobot](https://t.me/userinfobot)
3. Set `OPENAI_API_KEY` for voice/video transcription (optional)

**File support:**
| Type | Handling |
|------|----------|
| Text | Forwarded directly |
| Voice/Audio | Downloaded + transcribed via Whisper |
| Photo | Downloaded, passed as `file_path` (Claude sees it) |
| Video | Audio transcribed + up to 4 key frames extracted |
| Document | Downloaded, passed as `file_path` |

### Slack (two-way)

Uses Socket Mode — no public URL needed. Reacts with :eyes: to channel mentions, replies via DM.

```bash
SLACK_BOT_TOKEN=xoxb-... \
SLACK_APP_TOKEN=xapp-... \
SLACK_ALLOWED_IDS=U12345678 \
claude --dangerously-skip-permissions \
  --dangerously-load-development-channels server:resilient-slack
```

Also reads from `CLAUDE_CODE_SLACK_BOT_TOKEN`, `CLAUDE_CODE_SLACK_APP_TOKEN`, `CLAUDE_CODE_SLACK_USER_ID` env vars.

**Slack App requirements:**
- Socket Mode enabled
- App-Level Token with `connections:write`
- Event subscriptions: `message.im`, `message.channels`, `app_mention`
- Bot scopes: `chat:write`, `channels:history`, `im:history`, `app_mentions:read`

**Behavior:**
| Interaction | Response |
|-------------|----------|
| DM | Reply in same DM thread |
| @mention in channel | :eyes: react + reply via DM with thread link |
| Thread follow-up | Forwarded (no re-mention needed) |

## Architecture

```
External System → ChannelTransport → ResilientChannel → MCP stdio → Claude Code
                   (thin adapter)    (self-healing core)
```

### Self-Healing Mechanisms

| Mechanism | How |
|-----------|-----|
| **State persistence** | `~/.claude/channels/{name}/state.json` updated after every event |
| **Event buffering** | Events queue to disk when Claude disconnects |
| **Session replay** | Buffered events replayed with `is_replay="true"` on reconnect |
| **Instructions survival** | Injected into system prompt, survives context compaction |
| **Deduplication** | `externalId` tracking prevents double-processing |
| **Audit trail** | Per-session `events.jsonl` in `runs/{runId}/` |
| **Context handoff** | `update_context` tool persists summaries across sessions |

### State File

```json
{
  "runId": "ch-20260321-143022-a1b2c3d4",
  "sessionCount": 3,
  "lastEventId": "evt-42",
  "pendingEvents": [],
  "contextSummary": "User debugging CI pipeline failure",
  "status": "connected"
}
```

## Custom Transport

Implement the `ChannelTransport` interface:

```typescript
import type { ChannelTransport, InboundEvent } from './src/types.js'

class MyTransport implements ChannelTransport {
  name = 'my-transport'
  onEvent: (event: InboundEvent) => void = () => {}

  async start() { /* connect to your system */ }
  async stop() { /* disconnect */ }

  // Optional: two-way
  async send(destination: string, text: string) { /* send reply */ }
}
```

Then create an entry point:

```typescript
import { ResilientChannel } from './src/resilient-channel.js'
import { MyTransport } from './my-transport.js'

const channel = new ResilientChannel({
  name: 'my-channel',
  transport: new MyTransport(),
  twoWay: true,
  allowlist: new Set(['allowed-sender-id']),
})

channel.start()
```

Register in `.mcp.json`:

```json
{
  "mcpServers": {
    "my-channel": {
      "command": "bun",
      "args": ["./server-my-channel.ts"]
    }
  }
}
```

## Security

All transports enforce **deny-first** sender gating. No allowlist = no messages get through.

- Telegram: `TELEGRAM_ALLOWED_IDS` is mandatory
- Slack: `SLACK_ALLOWED_IDS` / `CLAUDE_CODE_SLACK_USER_ID` is mandatory
- Webhook: optional `x-sender-id` header + allowlist

## Requirements

- [Bun](https://bun.sh) runtime
- Claude Code v2.1.80+
- `OPENAI_API_KEY` for voice/video transcription (optional)
- `ffmpeg` for video key frame extraction (optional)

## License

MIT