Просмотр исходного кода

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.
Claude 3 недель назад
Родитель
Сommit
f1b74647fc

+ 1 - 1
external_plugins/telegram/.claude-plugin/plugin.json

@@ -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",

+ 18 - 3
external_plugins/telegram/server.ts

@@ -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' },

+ 12 - 9
external_plugins/telegram/skills/access/SKILL.md

@@ -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):
-
-```bash
-echo "${TELEGRAM_STATE_DIR:-${CLAUDE_CONFIG_DIR:-$HOME/.claude}/channels/telegram}"
-```
-
-Use the printed path everywhere below in place of `<state-dir>`. The default
-is `~/.claude/channels/telegram`.
+**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:
+
+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)
+
+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`
 

+ 20 - 10
external_plugins/telegram/skills/configure/SKILL.md

@@ -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.