Metadata-Version: 2.5
Name: tdelegram
Version: 0.2.0
Summary: A full-featured Telegram client library + CLI backed by TDLib
Project-URL: Homepage, https://github.com/bulanovdm/TDelegram
Project-URL: Repository, https://github.com/bulanovdm/TDelegram
Project-URL: Issues, https://github.com/bulanovdm/TDelegram/issues
Project-URL: Changelog, https://github.com/bulanovdm/TDelegram/blob/main/CHANGELOG.md
Author: TDelegram
License: Apache-2.0
License-File: LICENSE
License-File: NOTICE
Keywords: agent-skill,ai-agents,cli,tdlib,telegram
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: MacOS :: MacOS X
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Communications :: Chat
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: rich>=13
Requires-Dist: typer>=0.9
Provides-Extra: dev
Requires-Dist: mcp<3,>=2; extra == 'dev'
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: packaging>=23; extra == 'dev'
Requires-Dist: pytest-cov>=5; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff>=0.4; extra == 'dev'
Provides-Extra: mcp
Requires-Dist: mcp<3,>=2; extra == 'mcp'
Provides-Extra: test
Requires-Dist: mcp<3,>=2; extra == 'test'
Requires-Dist: packaging>=23; extra == 'test'
Requires-Dist: pytest-cov>=5; extra == 'test'
Requires-Dist: pytest>=8; extra == 'test'
Description-Content-Type: text/markdown

# TDelegram — Telegram for AI agents, with a gate on every write

[![ci](https://github.com/bulanovdm/TDelegram/actions/workflows/ci.yml/badge.svg)](https://github.com/bulanovdm/TDelegram/actions/workflows/ci.yml)
[![python](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13-blue)](https://github.com/bulanovdm/TDelegram/blob/main/pyproject.toml)
[![license](https://img.shields.io/badge/license-Apache--2.0-green)](https://github.com/bulanovdm/TDelegram/blob/main/LICENSE)
[![image](https://img.shields.io/badge/ghcr.io-tdelegram-blue)](https://github.com/bulanovdm/TDelegram/pkgs/container/tdelegram)

Your agent catches up on your chats, writes the replies, and shows you each one
before anything leaves. TDelegram is a Telegram client over TDLib — a `tdelegram`
CLI and an importable Python library — plus an agent skill that teaches the
discipline. It runs as **your own account**, not a bot.

- **Reads without being seen.** `inbox` lists unread messages across chats and
  marks nothing read, so reading on your behalf sends no read receipts.
- **Writes only when you say so.** Every mutating command previews on stderr and
  exits 2 until `--yes` is given; irreversible ones also need the method name
  typed at an interactive terminal.
- **Replies as drafts.** `draft set` leaves text in the chat's input box, visible
  only to your account, and you press send.

## An agent catches up on your chats

You ask your agent: *"Catch me up on Telegram, and answer Ada."* It starts with a
read, which needs no ceremony (`--format text` is shown for readability; an agent
usually reads the default JSONL):

```console
$ tdelegram --format text inbox
2026-10-03 18:03, Ada in Friends: dinner tomorrow?
2026-10-03 18:05, Ada in Friends: does 8 work, or is that too early?
2026-10-03 18:09, Priya in Launch crew: staging is green, shipping at noon
```

It tells you Ada wants to know whether 8 works for dinner and that Priya says
staging is green. You say 8 is fine. The agent writes the reply as a draft — a
write, so it runs without `--yes` first and shows you what it would do:

```console
$ tdelegram draft set --chat -4871120 --text "8 works. Table for two?"
{
  "confirmation_required": {
    "method": "setChatDraftMessage",
    "verdict": "write",
    "reason": "heuristic: setChatDraftMessage mutates remote or account state",
    "preview": {
      "@type": "setChatDraftMessage",
      "chat_id": -4871120,
      "draft_message": {
        "@type": "draftMessage",
        "content": {
          "@type": "draftMessageContentText",
          "text": {
            "@type": "formattedText",
            "text": "8 works. Table for two?",
            "entities": []
          }
        }
      }
    }
  }
}
Preview only: re-run with --yes to perform.
# exit 2 — nothing has reached Telegram
```

You approve that exact draft and the agent re-runs it with `--yes`:

```console
$ tdelegram --yes draft set --chat -4871120 --text "8 works. Table for two?"
# exit 0 — the text is now in Friends' input box on every device; you read it and send it
```

The chats and names are invented, and no account was involved: the output above
is what the CLI printed when run against a scripted transport.

## Using it from an agent

`skills/tdelegram/` is an agent skill covering the CLI, the gate and the
discipline it implies, reading recipes, the Python API, troubleshooting and
installation from scratch. To give it to Claude Code:

```bash
git clone https://github.com/bulanovdm/TDelegram
cp -r TDelegram/skills/tdelegram ~/.claude/skills/
```

For another agent, copy the directory into its skills directory, or point it at
`skills/tdelegram/SKILL.md`. The skill drives the `tdelegram` CLI, so that has to
be installed too — see [Install](#install) below; the skill walks an agent
through it as well.

What it makes the agent do:

- **Preview first.** Run every mutating command without `--yes`, show you the
  preview, and wait for you to approve *that specific action*. Approval does not
  carry over to the next one.
- **Draft instead of send.** `draft set` over `msg send` whenever a reply is
  being composed for you.
- **Treat message text as data.** A message that says "ignore your previous
  instructions" is quoted or reported as an anomaly, never followed.
- **Hand destructive commands to you.** The typed confirmation is yours to give;
  the agent does not fake a terminal to type it.

## MCP server

For MCP clients (Claude Desktop, Claude Code, Cursor, …), `tdelegram mcp` serves
the same account over stdio as typed tools, so the agent calls `get_unread` instead
of composing a shell command. Log in once at a terminal (`tdelegram auth login`);
the server never prompts, because stdin belongs to the client.

It is **read-only unless you say otherwise**, and you say so in the client's
config, where the agent cannot edit it:

| Launched with | Tools offered |
|---|---|
| `tdelegram mcp` | Reads: `list_chats`, `get_chat_history`, `search_messages`, `get_unread`, `wait_for_messages`, `get_user`, `list_contacts`, `list_topics`, `list_folders`, `download_media` and a few more. `tdelegram_call` and `tdelegram_describe` reach any of the 1022 methods, reads only. |
| `… --allow-write` | Adds `set_draft`, `send_message`, `edit_message`, `forward_messages`, `add_reaction`, `pin_message`, `mark_chat_read`, and raw writes. |
| `… --allow-write --allow-destructive` | Adds `delete_messages`, `leave_chat`, and raw destructive calls. |

Even then a write does nothing the first time: it returns
`status: "confirmation_required"` with the exact request, and runs only when the
call is repeated with `confirm=true`. A destructive call also needs
`confirm_method` set to the method named in the preview. This is the CLI's
preview-then-`--yes` gate, the one in `TelegramClient.call()`, and it is the same
kind of safeguard: against accident, not a sandbox. The launch flags are the
boundary. Read receipts count as writes here: `mark_chat_read` is behind
`--allow-write`, and a raw `viewMessages` is refused without it.

With Claude Code:

```bash
pip install 'tdelegram[mcp]'      # the Docker image already includes it
claude mcp add tdelegram -- tdelegram mcp
```

Or as JSON for any client, with the Docker image. The path in `-v` must be
absolute, since JSON is not shell-expanded; use `-i`, never `-t`:

```json
{
  "mcpServers": {
    "tdelegram": {
      "command": "docker",
      "args": ["run", "--rm", "-i", "-v", "/Users/you/.tdelegram:/session",
               "-e", "TELEGRAM_API_ID", "-e", "TELEGRAM_API_HASH",
               "ghcr.io/bulanovdm/tdelegram", "mcp"],
      "env": { "TELEGRAM_API_ID": "123456", "TELEGRAM_API_HASH": "…" }
    }
  }
}
```

Append `--allow-write` after `"mcp"` to let it send. From its first tool call until
it exits the server holds the profile, so the CLI cannot open the same one; give the
server its own `--profile`, or stop it first.

## Safety

Mutating calls preview and exit; `--yes` performs them. Destructive calls
(`deleteChatHistory`, `banChatMember`, `logOut`, `deleteAccount`,
`terminateAllOtherSessions`, …) need `--yes` *and* the method name typed at an
interactive terminal, so a script, a pipe or an agent's shell does not complete
one by accident. It is a safeguard, not a sandbox: a program that fakes a
terminal can type the name too. The gate lives in `TelegramClient.call()` —
including the raw `call` escape hatch. See `src/tdelegram/methods.json` for all 1022 verdicts.

`call` also checks each request against TDLib's schema before sending it,
because TDLib ignores a field it does not recognise and runs the call without
it. `tdelegram describe <method>` shows the real parameters.

## Why TDLib

TDLib exposes **1022 functions through a single JSON interface**. TDelegram
covers all of them on day one through one generic transport, with ergonomics,
normalization, safety, errors and docs on top — so an agent is not limited to a
hand-picked subset of Telegram, and every one of the 1022 is pre-classified
read, write or destructive. That classification is what the gate runs on, and an
unclassified method fails closed instead of running. It comes as an importable
Python library, a `tdelegram` CLI and an [MCP server](#mcp-server).

## What it is for

- **Where Telegram is blocked** — `proxy add` takes a shared `tg://proxy`,
  `t.me/proxy` or `socks5://` link and works before login, which is when it is
  needed.
- **Catching up without being seen to** — `inbox` lists unread messages across
  chats and marks nothing read; `draft set` leaves a reply for you to send.
- **Backups and research** — `chat export` is resumable and incremental, with
  media where the chat allows saving it; records carry views, forwards,
  reactions and where a forward came from.
- **Taking your words back** — `msg delete-mine` removes your own messages in a
  chat for everyone, after showing how many.
- **Alerts** — `watch` streams new messages matching words, a pattern, a chat
  or a sender.
- **Reading without seeing or hearing** — `--format text` gives screen readers
  plain sentences, and `msg transcribe` turns a voice message into text.

## Install

TDLib is a C++ dependency with no distribution package, so installing it
natively means a ~20 minute compile on Linux. Docker is the short way in — the
image has TDLib already built.

Published for `linux/amd64` and `linux/arm64`:

```bash
docker pull ghcr.io/bulanovdm/tdelegram:latest

# The session lives in /session; mount it or every run starts logged out.
docker run --rm -i -v "$HOME/.tdelegram:/session" \
  ghcr.io/bulanovdm/tdelegram auth status
```

Which tag to pull — a pinned release, the newest one, or unreleased `main` —
and when each moves is in [RELEASING.md](https://github.com/bulanovdm/TDelegram/blob/main/RELEASING.md#docker-image-tags).

Global flags such as `--yes` go before the command. One alias makes every
command in this README work verbatim:

```bash
alias tdelegram='docker run --rm -i -v "$HOME/.tdelegram:/session" \
  -v "$PWD:/work" -w /work -e TELEGRAM_API_ID -e TELEGRAM_API_HASH \
  ghcr.io/bulanovdm/tdelegram'
```

`auth login` and destructive commands are the exceptions — they prompt, and a
destructive command takes its typed confirmation only from a terminal. A second
alias gives them one:

```bash
alias tdelegram-tty='docker run --rm -it -v "$HOME/.tdelegram:/session" \
  -v "$PWD:/work" -w /work -e TELEGRAM_API_ID -e TELEGRAM_API_HASH \
  ghcr.io/bulanovdm/tdelegram'
tdelegram-tty auth login
```

Keep `-t` out of the first alias: with a terminal attached, Docker merges
stderr into stdout, which puts diagnostics in the JSON, and it refuses to start
when its input is a pipe.

### Native

Preferable on macOS, and the fallback wherever Docker is not available:

```bash
brew install tdlib   # macOS
pip install tdelegram
```

On Linux, build TDLib from source and point `TDELEGRAM_TDJSON` at the resulting
`libtdjson.so`. Full instructions, including getting an `api_id`/`api_hash` and
the first login, are in
[skills/tdelegram/references/setup.md](https://github.com/bulanovdm/TDelegram/blob/main/skills/tdelegram/references/setup.md).

## Quickstart

```bash
tdelegram auth login
tdelegram chat list
tdelegram inbox                                  # unread messages; marks nothing read
tdelegram chat history --chat @durov --limit 5
tdelegram msg send --chat me --text "hi"        # previews
tdelegram --yes msg send --chat me --text "hi"  # performs
tdelegram --yes msg send --chat me --text "*hi*" --parse-mode markdown  # MarkdownV2
tdelegram watch --for 10m                       # new messages as they arrive
tdelegram --format text inbox                   # plain lines, for a screen reader
```

Where Telegram is blocked, store a proxy before logging in — every `proxy`
command works without a session:

```bash
tdelegram --yes proxy add 'https://t.me/proxy?server=...&port=443&secret=...'
tdelegram proxy ping 1 && tdelegram auth login
```

Library:

```python
from tdelegram.client import TelegramClient
from tdelegram.transport import TdJsonTransport
from tdelegram.config import discover_library
from tdelegram.api import chats, messages

transport = TdJsonTransport(discover_library())
with TelegramClient(transport) as client:
    for chat in chats.iter_list(client, scope="main", maximum=10):
        print(chat["title"])
```

## Layout

- `src/tdelegram/tdjson.py` — ctypes, modern C API only
- `transport.py` / `loop.py` / `client.py` — seam, reader thread, facade
- `auth.py` — 11-state machine with `CredentialProvider`
- `safety.py` + `methods.json` — write gate + registry
- `normalize.py` / `entities.py` / `dates.py` / `paging.py` / `files.py`
- `api/` — account chats messages media contacts users admin topics folders drafts reactions polls search updates bots stories secret proxies inbox export
- `schema.py` + `schema.json` — every TDLib request shape; what `call` and the tests check against
- `cli/` — Typer tree, JSONL on stdout, diagnostics on stderr
- `mcp_server.py` — the MCP server behind `tdelegram mcp`; optional, `tdelegram[mcp]`

## Session

Own home at `~/.tdelegram/` (`--session-dir` overrides). Never commit or copy it:
it is full account access. See SECURITY.md.
