feat(telegram): v0.0.8 per-project bot configuration

Resolve state directory with precedence: TELEGRAM_STATE_DIR override >
project-local ($CLAUDE_PROJECT_DIR/.claude/channels/telegram, only when it
has a .env) > global (~/.claude/channels/telegram). Non-breaking — existing
setups have no project .env so keep global.

/telegram:configure gains --project <token> to opt a project in; server
auto-detects it on next start. Both skills describe the matching precedence.

Addresses anthropics/claude-code#37173.
This commit is contained in:
Claude 2026-08-21 18:42:23 +00:00
parent 340e33aef2
commit f1b74647fc
No known key found for this signature in database
4 changed files with 49 additions and 21 deletions

View File

@ -1,7 +1,7 @@
{
"name": "telegram",
"description": "Telegram channel for Claude Code \u2014 messaging bridge with built-in access control. Manage pairing, allowlists, and policy via /telegram:access.",
"version": "0.0.7",
"version": "0.0.8",
"keywords": [
"telegram",
"messaging",

View File

@ -24,8 +24,23 @@ import { homedir } from 'os'
import { execFileSync } from 'child_process'
import { join, extname, sep } from 'path'
const STATE_DIR = process.env.TELEGRAM_STATE_DIR
?? join(process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), '.claude'), 'channels', 'telegram')
// Precedence: explicit override > project-local (only when it has a configured
// bot) > global. A project opts in by placing a .env under its own
// .claude/channels/telegram/; sessions in projects without one keep using the
// shared global config. CLAUDE_PROJECT_DIR is set by the harness for every
// spawned MCP server. anthropics/claude-code#37173.
const STATE_DIR = (() => {
if (process.env.TELEGRAM_STATE_DIR) return process.env.TELEGRAM_STATE_DIR
const global = join(process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), '.claude'), 'channels', 'telegram')
if (process.env.CLAUDE_PROJECT_DIR) {
const local = join(process.env.CLAUDE_PROJECT_DIR, '.claude', 'channels', 'telegram')
try {
statSync(join(local, '.env'))
return local
} catch {}
}
return global
})()
const ACCESS_FILE = join(STATE_DIR, 'access.json')
const APPROVED_DIR = join(STATE_DIR, 'approved')
const ENV_FILE = join(STATE_DIR, '.env')
@ -1010,7 +1025,7 @@ void (async () => {
onStart: info => {
attempt = 0
botUsername = info.username
process.stderr.write(`telegram channel: polling as @${info.username}\n`)
process.stderr.write(`telegram channel: polling as @${info.username} (state: ${STATE_DIR})\n`)
void bot.api.setMyCommands(
[
{ command: 'start', description: 'Welcome and setup guide' },

View File

@ -22,15 +22,18 @@ downstream of untrusted input.
Manages access control for the Telegram channel. You never talk to Telegram —
you just edit JSON; the channel server re-reads it.
**Resolve the state directory first** (it may be overridden for multi-bot or
per-project setups):
**Resolve the state directory first.** The server picks the first of these
that applies — match its precedence exactly so your edits land where the
server reads:
```bash
echo "${TELEGRAM_STATE_DIR:-${CLAUDE_CONFIG_DIR:-$HOME/.claude}/channels/telegram}"
```
1. `$TELEGRAM_STATE_DIR` if set
2. `${CLAUDE_PROJECT_DIR}/.claude/channels/telegram`**only if** that dir
has a `.env` (a project-local bot is configured)
3. `${CLAUDE_CONFIG_DIR:-$HOME/.claude}/channels/telegram` (global default)
Use the printed path everywhere below in place of `<state-dir>`. The default
is `~/.claude/channels/telegram`.
Check with `echo $TELEGRAM_STATE_DIR` and
`ls ${CLAUDE_PROJECT_DIR}/.claude/channels/telegram/.env` as needed. Use the
resolved path everywhere below in place of `<state-dir>`.
Arguments passed: `$ARGUMENTS`

View File

@ -16,15 +16,22 @@ allowed-tools:
Writes the bot token to `<state-dir>/.env` and orients the user on access
policy. The server reads both files at boot.
**Resolve the state directory first** (it may be overridden for multi-bot or
per-project setups):
**Resolve the state directory first.** The server picks the first of these
that applies — match its precedence exactly so your writes land where the
server reads:
```bash
echo "${TELEGRAM_STATE_DIR:-${CLAUDE_CONFIG_DIR:-$HOME/.claude}/channels/telegram}"
```
1. `$TELEGRAM_STATE_DIR` if set
2. `${CLAUDE_PROJECT_DIR}/.claude/channels/telegram`**only if** that dir
has a `.env` (a project-local bot is configured)
3. `${CLAUDE_CONFIG_DIR:-$HOME/.claude}/channels/telegram` (global default)
Use the printed path everywhere below in place of `<state-dir>`. The default
is `~/.claude/channels/telegram`.
Check with `echo $TELEGRAM_STATE_DIR` and
`ls ${CLAUDE_PROJECT_DIR}/.claude/channels/telegram/.env` as needed. Use the
resolved path everywhere below in place of `<state-dir>`.
**Exception:** if `$ARGUMENTS` starts with `--project`, `<state-dir>` is
`${CLAUDE_PROJECT_DIR}/.claude/channels/telegram` regardless — you're creating
or updating the project-local config.
Arguments passed: `$ARGUMENTS`
@ -82,10 +89,13 @@ Drive the conversation this way:
Never frame `pairing` as the correct long-term choice. Don't skip the lockdown
offer.
### `<token>` — save it
### `<token>` or `--project <token>` — save it
1. Treat `$ARGUMENTS` as the token (trim whitespace). BotFather tokens look
like `123456789:AAH...` — numeric prefix, colon, long string.
1. Treat `$ARGUMENTS` as the token (strip a leading `--project` and trim
whitespace). BotFather tokens look like `123456789:AAH...` — numeric
prefix, colon, long string. With `--project`, `<state-dir>` is the
project-local path (see above) — this is how a project opts in to its own
bot; the server will pick it up automatically on next start.
2. `mkdir -p` the resolved `<state-dir>`.
3. Read existing `.env` if present; update/add the `TELEGRAM_BOT_TOKEN=` line,
preserve other keys. Write back, no quotes around the value.