{
  "markdown": "# Telegram Per-Project\n\nConnect a Telegram bot to Claude Code via an MCP channel plugin. Per-project mode gives each project its own bot, access policy, and message inbox. Includes a `claude-gram` watchdog that monitors the session, sends Telegram notifications on exit, and auto-retries.\n\n## Prerequisites\n\n- [Claude Code](https://claude.ai/code) installed and signed in (`claude` on your PATH).\n- [Bun](https://bun.sh) — the channel server runs on Bun. `install.sh` sets it up if missing.\n- `curl` — used by `claude-gram` for Telegram API calls (pre-installed on most systems).\n\n## Read this first: there are two plugins\n\nThis setup uses **two separate plugins**, and you need both. Confusing them is the #1 reason installs fail:\n\n| Plugin | What it gives you | How you install it | Required? |\n|---|---|---|---|\n| **`telegram@claude-plugins-official`** (the official channel plugin) | The **channel itself** — the MCP server that polls Telegram and pipes messages into your session. | `claude plugin install telegram@claude-plugins-official` | **Yes — without it `claude-gram` cannot connect.** |\n| **`telegram-per-project`** (this repo) | Per-project **skills** (`/telegram-per-project:access`, `/telegram-per-project:configure`) and hooks. | `--plugin-dir` pointing at the cloned repo | Yes, for per-project bot isolation. |\n\n`claude-gram` always launches with `--channels plugin:telegram@claude-plugins-official`. That flag resolves to the *official* plugin — so that plugin **must be installed first**, or you'll see `MCP servers ... plugin:telegram:telegram: failed`. The `--plugin-dir` flag then layers this repo's per-project skills on top. See [Troubleshooting](#troubleshooting) if the channel won't connect.\n\n> When this repo is eventually published to its own marketplace (see [`docs/claudePlugins.md`](./docs/claudePlugins.md)), the two collapse into one and only a single install will be needed. Until then, both are required.\n\n## Installation\n\n```bash\ngit clone https://github.com/trezero/telegram-per-project.git\ncd telegram-per-project\n./install.sh\n```\n\n`install.sh` does three things:\n\n1. Copies `claude-gram` to `~/.local/bin` (or `~/bin`) so it's on your PATH.\n2. Installs [Bun](https://bun.sh) if it isn't already present (the channel server runtime).\n3. **Installs the official channel plugin** `telegram@claude-plugins-official` (best-effort) — the step that makes `claude-gram` work.\n\nYou can pass a custom install directory: `./install.sh /usr/local/bin`.\n\nThe installed `claude-gram` copy is independent of the repo — re-run `./install.sh` to deploy updates.\n\n## Quick Setup\n\n> Default flow for a single-user DM bot. See [ACCESS.md](./ACCESS.md) for groups and multi-user setups.\n\n**0. Make sure the official channel plugin is installed.**\n\n`install.sh` attempts this automatically, but verify it — this is the step that, if skipped, breaks everything:\n\n```bash\nclaude plugin install telegram@claude-plugins-official\n```\n\n(Already-installed is fine; it's idempotent.) Confirm it shows up with `claude plugin list`.\n\n**1. Create a bot with BotFather.**\n\nOpen a chat with [@BotFather](https://t.me/BotFather) on Telegram and send `/newbot`. BotFather asks for two things:\n\n- **Name** — the display name shown in chat headers (anything, can contain spaces)\n- **Username** — a unique handle ending in `bot` (e.g. `my_assistant_bot`). This becomes your bot's link: `t.me/my_assistant_bot`.\n\nBotFather replies with a token that looks like `123456789:AAHfiqksKZ8...` — that's the whole token, copy it including the leading number and colon.\n\n**2. Launch with the per-project skills loaded.**\n\nFrom your project directory, pass `--plugin-dir` so this repo's per-project skills are active (otherwise you get the official global-only skills):\n\n```bash\ncd ~/projects/myproject\nclaude-gram --plugin-dir /path/to/telegram-per-project\n```\n\n`claude-gram` detects missing config and walks you through interactive setup: a project ID, bot-token validation against Telegram, and writing the config files. Add `-dsp` to also pass `--dangerously-skip-permissions`.\n\nLaunch manually without the watchdog (same flags):\n\n```bash\nclaude --channels plugin:telegram@claude-plugins-official --plugin-dir /path/to/telegram-per-project\n```\n\n**3. Pair.**\n\nDM your bot on Telegram — it replies with a 6-character pairing code. In your Claude Code session:\n\n```\n/telegram-per-project:access pair <code>\n```\n\nYour next DM reaches the assistant.\n\n> Unlike Discord, there's no server invite step — Telegram bots accept DMs immediately. Pairing handles the user-ID lookup so you never touch numeric IDs.\n\n**4. Lock it down.**\n\nPairing is for capturing IDs. Once you're in, switch to `allowlist` so strangers don't get pairing-code replies:\n\n```\n/telegram-per-project:access policy allowlist\n```\n\n## Troubleshooting\n\n**`MCP servers ... plugin:telegram:telegram: failed` / channel won't connect.**\nThe official channel plugin isn't installed. Run `claude plugin install telegram@claude-plugins-official`, confirm with `claude plugin list`, then restart the session. This is required — `claude-gram` gets the channel from that plugin via `--channels`.\n\n**The channel connects but `claude-gram`'s setup wrote to a project dir you can't reach.**\nYou launched without `--plugin-dir`, so the *official* skills (`/telegram:access`) are active instead of the per-project ones. Relaunch with `claude-gram --plugin-dir /path/to/telegram-per-project` and pair with `/telegram-per-project:access pair <code>`.\n\n**`claude-gram: no per-project Telegram config found` on every run.**\nExpected the first time in a new project — `claude-gram` will prompt you. If it keeps prompting, check that your project's `.claude/settings.local.json` has the `env.TELEGRAM_PROJECT_ID` / `env.TELEGRAM_STATE_DIR` keys the setup wrote.\n\n**`bun not found` inside the session but `bun` works in your shell.**\nRestart your shell so the bun PATH added by `install.sh` takes effect, or re-run `./install.sh`. The channel server (`server.ts`) is invoked as `bun server.ts`.\n\n## Plugin Structure\n\nThis plugin follows the [Claude Code plugin conventions](https://code.claude.com/docs/en/plugins-reference):\n\n```\ntelegram-per-project/\n├── .claude-plugin/\n│   └── plugin.json          # Manifest: channels, userConfig, mcpServers\n├── skills/\n│   ├── access/SKILL.md      # /telegram-per-project:access — pairing, allowlists, policies\n│   └── configure/SKILL.md   # /telegram-per-project:configure — token setup, status\n├── hooks/\n│   └── hooks.json           # SessionStart: install deps via ${CLAUDE_PLUGIN_DATA}\n├── .mcp.json                # MCP server config (telegram channel server)\n├── server.ts                # Telegram bot + MCP server (bun)\n├── package.json             # Dependencies: grammy, @modelcontextprotocol/sdk\n├── claude-gram              # Session launcher & watchdog script\n├── install.sh               # Installer (bun + claude-gram copy)\n└── uninstall.sh             # Cleanup script\n```\n\nKey conventions used:\n- **`channels`** in plugin.json — declares this as a channel plugin bound to the `telegram` MCP server\n- **`userConfig`** with `sensitive: true` — bot token prompted at enable time, stored in system keychain\n- **`${CLAUDE_PLUGIN_DATA}`** — persistent directory for `node_modules` that survives plugin updates\n- **SessionStart hook** — installs dependencies once, re-installs only when `package.json` changes\n\n### Bot token resolution\n\nThe server reads the bot token from the first available source:\n\n1. `CLAUDE_PLUGIN_OPTION_bot_token` — set by plugin `userConfig` (keychain-backed)\n2. `TELEGRAM_BOT_TOKEN` — set via `settings.local.json` env block or shell environment\n3. `.env` file in the state directory — legacy fallback for existing installations\n\n## `claude-gram` — Session Launcher & Watchdog\n\n`claude-gram` wraps Claude Code with process monitoring, Telegram notifications, and automatic retries. It's the recommended way to run long-lived sessions.\n\n```\nUsage: claude-gram [options] [project-dir]\n\nOptions:\n  -dsp              Add --dangerously-skip-permissions to the claude command\n  --plugin-dir DIR  Load a local plugin directory (skills/hooks override)\n  --retries N       Max consecutive restart attempts (default: 3, 0 = no restart)\n  --cooldown S      Seconds between restarts (default: 10)\n  -h, --help        Show usage\n\nArguments:\n  project-dir   Directory to run in (default: current directory)\n```\n\n### What it does\n\n1. **Resolves project context** — reads `TELEGRAM_STATE_DIR` / `TELEGRAM_PROJECT_ID` from `.claude/settings.local.json` to find the bot token and access config\n2. **Interactive setup** — if no Telegram config exists for the project, guides you through setup (project ID, bot token validation, config file creation)\n3. **Launches Claude Code** with `--channels plugin:telegram@claude-plugins-official`\n4. **Notifies on exit** — sends a Telegram message to all `allowFrom` users when the session ends\n5. **Retries with backoff** — waits `--cooldown` seconds and relaunches (up to `--retries` times)\n6. **Stability heuristic** — if a session ran for >5 minutes, the retry counter resets (distinguishes auth expiry from crash loops)\n7. **Clean shutdown** — `Ctrl+C` / `SIGTERM` forwards to Claude Code and suppresses the \"session died\" notification\n\n### Examples\n\n```bash\n# Basic usage — run in current project directory\nclaude-gram\n\n# With skip-permissions flag\nclaude-gram -dsp\n\n# Explicit project directory, no retries\nclaude-gram --retries 0 ~/projects/myproject\n\n# In tmux for persistence\ntmux new -s myproject \"claude-gram -dsp ~/projects/myproject\"\n```\n\n## Access control\n\nSee **[ACCESS.md](./ACCESS.md)** for DM policies, groups, mention detection, delivery config, skill commands, and the `access.json` schema.\n\nQuick reference: IDs are **numeric user IDs** (get yours from [@userinfobot](https://t.me/userinfobot)). Default policy is `pairing`. `ackReaction` only accepts Telegram's fixed emoji whitelist.\n\n## Per-project bots\n\nBy default, all Claude Code sessions share one bot token and one access config (the \"global\" setup described above). Per-project mode gives each project its own Telegram bot, access policy, and message inbox — so messages to `@alpha_dev_bot` only arrive in the Alpha project session, never in Beta's.\n\n### How it works\n\nThe MCP server resolves its state directory from environment variables in this order:\n\n1. `TELEGRAM_STATE_DIR` — full path override (takes precedence)\n2. `TELEGRAM_PROJECT_ID` — just the ID; the server builds `~/.claude/channels/telegram/projects/<id>/`\n3. Neither set — falls back to the global `~/.claude/channels/telegram/`\n\n| | Global (default) | Per-project |\n| --- | --- | --- |\n| State dir | `~/.claude/channels/telegram/` | `~/.claude/channels/telegram/projects/<id>/` |\n| Bot token | Shared across all sessions | One token per project |\n| Access policy | Shared `access.json` | Independent per-project `access.json` |\n| Inbox | Shared | Separate per project |\n\nEach project-scoped state directory contains the same files as the global one:\n\n```\n~/.claude/channels/telegram/projects/<id>/\n├── .env              # TELEGRAM_BOT_TOKEN for this project's bot\n├── access.json       # per-project access policy\n├── inbox/            # downloaded photos\n└── approved/         # pairing approval signals\n```\n\n### Setting the environment variables\n\nBoth variables are passed via the `env` block in `.claude/settings.local.json` in your project root. The `claude-gram` interactive setup and `/telegram-per-project:configure --project` command write this automatically, but the resulting structure is:\n\n```json\n{\n  \"env\": {\n    \"TELEGRAM_PROJECT_ID\": \"myproject\",\n    \"TELEGRAM_STATE_DIR\": \"/home/you/.claude/channels/telegram/projects/myproject\"\n  }\n}\n```\n\nClaude Code's `env` is a flat `Record<string, string>` — all values must be strings, no nesting. These environment variables are passed to all MCP server processes at startup.\n\n### Project ID rules\n\n- Allowed characters: letters, digits, hyphens, underscores (`[a-zA-Z0-9_-]`)\n- Maximum length: 64 characters\n- Used as a directory name — path separators (`/`, `\\`, `..`) are rejected\n- Validated both by the configure skill and by `server.ts` at startup (fail-fast on invalid IDs)\n\n### Setup walkthrough\n\nThe fastest path is to run `claude-gram` in each project directory — it handles everything interactively. For manual setup:\n\n**1. Create a bot per project** with [@BotFather](https://t.me/BotFather). Give each a descriptive username (e.g. `@myproject_dev_bot`).\n\n**2. Configure each project.** In the project's Claude Code session:\n\n```\n/telegram-per-project:configure --project myproject 123456789:AAH...\n```\n\nThis does four things:\n1. Creates `~/.claude/channels/telegram/projects/myproject/`\n2. Saves the bot token to `projects/myproject/.env` (mode `0600`)\n3. Writes `TELEGRAM_PROJECT_ID=myproject` to the project's `.claude/settings.local.json`\n4. Copies `allowFrom` from the global `access.json` as the initial per-project access list (if the global config exists)\n\n**3. Launch** with `claude-gram` or manually:\n\n```bash\n# Recommended — with watchdog and notifications:\ncd ~/projects/myproject && claude-gram\n\n# Or manually with --plugin-dir:\ncd ~/projects/myproject && claude --channels plugin:telegram@claude-plugins-official --plugin-dir /path/to/telegram-per-project\n```\n\n**4. Pair** by DMing the project's bot. If your user ID was already in the global allowlist, it carries over automatically and you can skip pairing. Otherwise, the bot replies with a pairing code — approve with `/telegram-per-project:access pair <code>` as usual.\n\n**5. Run multiple sessions** — each project directory launches its own bot:\n\n```bash\n# Terminal 1: Alpha project connects to @alpha_dev_bot\ncd ~/projects/alpha && claude-gram\n\n# Terminal 2: Beta project connects to @beta_dev_bot\ncd ~/projects/beta && claude-gram\n\n# Terminal 3: No project ID — connects to the global bot\ncd ~/projects/other && claude-gram\n```\n\nDM `@alpha_dev_bot` to reach Terminal 1, `@beta_dev_bot` to reach Terminal 2. Messages never cross between sessions.\n\n### Access inheritance\n\nWhen a per-project `access.json` is created for the first time (either by `claude-gram` setup, the configure skill, or the server's startup bootstrap), it inherits the `allowFrom` list from the global `~/.claude/channels/telegram/access.json`. After creation, the per-project config is fully independent — changes to the global config do not propagate, and vice versa.\n\n### Verifying your setup\n\nCheck which mode a project is using:\n\n```\n/telegram-per-project:configure\n```\n\nThis shows the active mode (global or per-project), the resolved state directory, token status, and access summary.\n\n### Removing per-project configuration\n\n```\n/telegram-per-project:configure clear\n```\n\nIn per-project mode, this removes the token from the per-project `.env` and removes `TELEGRAM_PROJECT_ID` from `.claude/settings.local.json`. After restarting, the session falls back to the global bot.\n\nSee [ACCESS.md](./ACCESS.md) for the full per-project access control reference.\n\n## Tools exposed to the assistant\n\n| Tool | Purpose |\n| --- | --- |\n| `reply` | Send to a chat. Takes `chat_id` + `text`, optionally `reply_to` (message ID) for native threading and `files` (absolute paths) for attachments. Images (`.jpg`/`.png`/`.gif`/`.webp`) send as photos with inline preview; other types send as documents. Max 50MB each. Auto-chunks text; files send as separate messages after the text. Returns the sent message ID(s). |\n| `react` | Add an emoji reaction to a message by ID. **Only Telegram's fixed whitelist** is accepted (👍 👎 ❤ 🔥 👀 etc). |\n| `edit_message` | Edit a message the bot previously sent. Useful for \"working…\" → result progress updates. Only works on the bot's own messages. |\n\nInbound messages trigger a typing indicator automatically — Telegram shows\n\"botname is typing…\" while the assistant works on a response.\n\n## Photos\n\nInbound photos are downloaded to `~/.claude/channels/telegram/inbox/` (or the project-scoped inbox in per-project mode) and the\nlocal path is included in the `<channel>` notification so the assistant can\n`Read` it. Telegram compresses photos — if you need the original file, send it\nas a document instead (long-press → Send as File).\n\n## No history or search\n\nTelegram's Bot API exposes **neither** message history nor search. The bot\nonly sees messages as they arrive — no `fetch_messages` tool exists. If the\nassistant needs earlier context, it will ask you to paste or summarize.\n\nThis also means there's no `download_attachment` tool for historical messages\n— photos are downloaded eagerly on arrival since there's no way to fetch them\nlater.\n\n## Uninstalling\n\n```bash\n# Remove claude-gram and bun symlinks, keep state\n./uninstall.sh\n\n# Also purge all state (tokens, access lists, inboxes)\n./uninstall.sh --purge\n\n# Also clean a specific project's settings.local.json\n./uninstall.sh --purge --project ~/projects/myproject\n```\n",
  "bytes": 16950,
  "sha": "03349d87149b9f9190695a6f81afbca6542f6fca69d8b49bf4004e2170fcf314",
  "repo_slug": "trezero/telegram-per-project",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/plg_trezero_telegram_per_project_telegram_pe_bf1c867b/readme"
}