Claude-to-IM: the bridge half of a Claude Code IM integration, and the half you still write
Host-agnostic bridge connecting Claude Code SDK to IM platforms (Telegram, Discord, Feishu)
At a glance
- What is it?
- A TypeScript library that handles message routing, streaming previews, chunking and per-platform rendering for Telegram, Discord and Feishu, while leaving persistence, the LLM stream and permission resolution to the host. The host side is roughly thirty store methods and an event protocol you have to match exactly.
- Who is it for?
- The bridge does the IM half of the work and deliberately refuses the other half. Message routing, streaming previews, chunking, retry, Markdown rendering and per-platform rate limits arrive ready to use; persistence, the LLM stream and permission resolution do not.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Activity is slowing. The repository last received commits 7 months ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on November 13, 2026, and from our analysis. They are not legal advice.
Star history
- op7418/Claude-to-IM473 ★ · -1
| Repository | 2026-09-22 | 2026-11-13 |
|---|---|---|
| op7418/Claude-to-IM | 474 | 473 |
-1 Stars in the last 52 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
Version 0.1.0, no GitHub releases, and a last push dated 2026-03-23
The package is named `claude-to-im` at version `0.1.0`, and the repository has no GitHub releases, so there is no tag to compare that version against. The last recorded push to the default branch is dated 2026-03-23, which leaves the tree some months behind the date you are reading it on.
Five runtime dependencies carry carets: `@anthropic-ai/claude-agent-sdk` at `^0.2.62`, `discord.js` at `^14.25.1`, `@larksuiteoapi/node-sdk` at `^1.59.0`, `markdown-it` at `^14.1.1` and `ws` at `^8.18.0`. The SDK range is the one to watch, because the bridge parses that event stream rather than a generic chat response. The runtime floor is Node 20 or newer, and the package is ESM only: it declares module type with no CommonJS entry. There is a Chinese README alongside the English one.
Four interfaces, and BridgeStore alone carries about thirty methods
Everything the bridge needs arrives through a single registration call:
import { initBridgeContext } from 'claude-to-im/context';
import type { BridgeStore, LLMProvider, PermissionGateway, LifecycleHooks } from 'claude-to-im/host';
const store: BridgeStore = { /* your persistence layer (~30 methods) */ };
const llm: LLMProvider = { /* wraps Claude Code SDK streamChat */ };
const permissions: PermissionGateway = { /* resolves pending tool permissions */ };
const lifecycle: LifecycleHooks = { /* optional start/stop callbacks */ };
initBridgeContext({ store, llm, permissions, lifecycle });The counts in that snippet are the honest part of the pitch. `BridgeStore` spans settings, sessions, messages, channel bindings, audit logs, dedup tracking, permission links and channel offsets, and is named as the largest integration surface. `LifecycleHooks` is the only optional one. Modules reach host services through `getBridgeContext()` rather than direct imports, which is what makes the library framework-neutral. Startup is `bridgeManager.start()`, followed by `bridgeManager.getStatus()`, which reports a running flag and a list of adapters carrying their channel type and running flag.
LLMProvider must emit the SDK event protocol, not chat completions
One interface carries a hard format requirement. `LLMProvider.streamChat()` returns a `ReadableStream<string>` of SSE-formatted strings, and those strings have to match the Claude Code SDK event protocol: text, tool_use, tool_result, permission_request, status and result events. A client that speaks the usual chat completion shape cannot be connected directly, because nothing here normalizes a different event vocabulary.
That constraint reaches into the rest of the design. Streaming previews work by editing an existing message in place, so partial output has to arrive as text events and then be throttled per platform. Inline buttons for tool approvals only make sense if a permission_request event arrives carrying something a host can resolve, which is the entire reason `PermissionGateway` exists as a separate interface. Below that, the Delivery Layer chunks messages at platform limits, retries with exponential backoff, falls back to HTML when Markdown parsing fails, and deduplicates messages it has already sent.
Three adapters, three transports, three renderers
Each adapter reaches its platform over a different mechanism. Telegram uses long polling, Discord uses the Gateway WebSocket, and Feishu uses its own WSClient. The `ws` package sits in runtime dependencies rather than dev dependencies, which is what the Gateway connection and the Feishu client are built on. No webhook URL appears anywhere in the settings tables, which lines up with the polling approach on the Telegram side.
Rendering is per platform rather than normalized to one format. Telegram receives HTML, Discord receives Discord-flavored Markdown, and Feishu receives rich text cards, all from the same Markdown input handled by `markdown-it`, with an HTML fallback when parsing fails. Session binding is the other per-platform invariant: each IM chat maps to a persistent conversation session carrying its own working directory and model settings, and the Channel Router component in the architecture diagram owns that mapping. It is the piece that keeps one chat's working directory out of another's session.
remote_bridge_enabled gates everything and new sessions start at $HOME
Three settings are required. `remote_bridge_enabled` is the master switch, `bridge_{adapter}_bot_token` holds the platform bot token, and `bridge_{adapter}_allowed_users` is a comma-separated list of user IDs authorized to use the bridge, where the adapter is replaced by `telegram`, `discord` or `feishu`. No default is stated for the master switch.
The optional settings split in two directions. `bridge_{adapter}_stream_enabled` defaults to true, while `bridge_{adapter}_enabled` and `bridge_auto_start` both default to false. `bridge_default_cwd` defaults to `$HOME`, so a session bound to a chat begins in the home directory unless the host overrides it, and `bridge_model` has no default at all because the host decides. On the security side, the allowed-users list is joined by input validation, a token bucket at 20 messages per minute per chat, and full audit logging.
The runnable example is a mock host with an echo LLM
The example that ships is deliberately not a Claude connection. It is a self-contained mock host with an in-memory store and an echo LLM:
npx tsx src/lib/bridge/examples/mock-host.tsThat proves wiring rather than model output, because the `LLMProvider` contract is satisfied by something that echoes back. The same target appears twice, once as the quick start step and once as the `example` script in package.json, so the npm script and the npx line address the same file. Installing from git behaves differently from installing from the registry, because a `prepare` script runs the build first, so `npm run build` and its `tsc -p tsconfig.build.json` execute before anything resolves. Tests are chained the same way: `npm test` runs `typecheck` and then `test:unit`, which is `node --test` over `src/__tests__/unit/bridge-*.test.ts` with tsx imported and a 15 second timeout per test.
The Session Locking section stops inside the method name
The limitations chapter ends where the interesting part begins. Its last heading is Session Locking, and the text underneath stops partway through naming the mechanism, at the two letters `ac`. What the lock protects, when it is taken, and what a second chat hitting the same session does are not written down in the README.
The limitations before it are stated in full, and they are constraints rather than warnings. This is a library, not an application, so there is no bundled database driver and every read and write goes through `BridgeStore`. There is no LLM client either. The stated benefit of the four interfaces is that the bridge holds no database driver, no LLM client and no framework, and the only surfaces the modules touch are those interfaces. The full specification is deferred to `docs/development.md` for the interfaces and to `src/lib/bridge/host.ts` for the store definition.
Export keys repeat the src/lib/bridge path and the package ships sources
The packaging is worth reading before you plan an import graph. `main` and `types` point at `dist/lib/bridge/context.js` and its declaration file, and the export map then repeats the source tree shape: keys like `./src/lib/bridge/adapters/*.js`, `./src/lib/bridge/markdown/*.js` and `./src/lib/bridge/security/*.js` are part of the public surface. Adapters, Markdown rendering and the security modules are therefore addressed by a path that mirrors the source directory instead of a package-shaped name, which matters for a bundler resolving exports maps.
The published file list is `dist` and `src/lib`, so both compiled output and the bridge sources are installed. That puts `src/lib/bridge/examples/mock-host.ts` inside the download, which is convenient for reading and means the mock host travels with every install. Two tsconfig files sit at the root, one for building and one for the editor. The test glob is narrow as well, limited to `src/__tests__/unit/` and to filenames beginning with `bridge-`.
Editorial conclusion
The bridge does the IM half of the work and deliberately refuses the other half. Message routing, streaming previews, chunking, retry, Markdown rendering and per-platform rate limits arrive ready to use; persistence, the LLM stream and permission resolution do not. Budget roughly thirty store methods and an SSE stream shaped like the Claude Code SDK protocol before starting, and read the session locking behavior in the source, since the README stops inside that section. If you would rather implement none of it, CodePilot is this codebase with the host interfaces and a UI already filled in. Then verify the SDK range against your Claude Code version: the last push to the default branch is dated 2026-03-23 and the package has no tagged release.
Frequently asked questions
What does claude-to-im give me out of the box?
The IM side only: message routing, streaming previews, permission approval flows, Markdown rendering, chunking, retry and rate limiting across Telegram, Discord and Feishu. Persistence, LLM calls and permission resolution all come from your host application through dependency injection interfaces.
Why does Claude-to-IM require four host interfaces?
`BridgeStore` covers persistence with about thirty methods, `LLMProvider` wraps the Claude Code SDK stream, `PermissionGateway` resolves pending tool permissions, and `LifecycleHooks` holds optional start and stop callbacks. They are registered together in one `initBridgeContext` call.
Can Claude-to-IM work with an ordinary chat completion API?
Not directly. `LLMProvider.streamChat()` must return SSE-formatted strings matching the Claude Code SDK event protocol, covering text, tool_use, tool_result, permission_request, status and result events. Without the SDK you need an adapter that rewrites your client's output into that shape.
Which settings does Claude-to-IM require before it starts?
`remote_bridge_enabled` as the master switch, `bridge_{adapter}_bot_token` for the platform bot token, and `bridge_{adapter}_allowed_users` as comma-separated authorized user IDs, with the adapter replaced by telegram, discord or feishu.
Does Claude-to-IM bundle a database driver or an LLM client?
No. There is no bundled database driver and no LLM client. All persistence goes through your `BridgeStore`, which has to cover sessions, messages, channel bindings, audit logs, dedup keys, permission links and channel offsets.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/op7418-claude-to-im)Star history chart for your README
[](https://hysenlabs.com/projects/op7418-claude-to-im)Was this useful?