31 Commits

Author SHA1 Message Date
Boris Cherny
c4ce608cc2
telegram: don't kill a running session's poller when a second session starts
The startup takeover guard SIGTERMed any live bot.pid holder, so starting a
second Claude Code session stole the Telegram polling slot from the session
already using it — and when that second session exited, no poller remained
at all and the bot went permanently silent (anthropics/claude-code#81571).

Replace the kill-on-startup pattern with cooperative slot ownership:
- slot free (no pid file, holder dead, or pid recycled to an unrelated
  process — identity-checked via /proc cmdline or ps args) → claim and poll
- live healthy holder → standby: outbound tools stay fully usable, and a
  watcher claims the slot the moment the holder exits, so the channel hands
  off to the last session standing instead of dying
- nothing is ever killed; genuinely orphaned pollers are already reaped by
  the stdin orphan watchdog, and the pid file is removed by its owner on
  shutdown

Adds a regression test (bun test external_plugins/telegram/test) that spawns
two real servers against an isolated state dir: the second must not kill the
first, the slot must hand off when the holder exits, and the pid file must
be cleaned up by the last server out. Fails on 0.0.7, passes on 0.0.8.

Fixes anthropics/claude-code#81571

No-Verification-Needed: fix is in the plugins repo; verified by its own regression test plus a two-session CLI e2e
2026-08-24 18:06:02 -07:00
Noah Zweben
1e96c6b0a0
fix(telegram): v0.0.7 reliability rollup — state-dir, PID guard, ppid watchdog, install stdout (#1424)
* fix(telegram): honor TELEGRAM_STATE_DIR/CLAUDE_CONFIG_DIR in skills and server

The server already reads TELEGRAM_STATE_DIR for multi-bot setups, but the
/telegram:access and /telegram:configure skills hardcoded
~/.claude/channels/telegram/ in 11 places. So with a custom state dir the
skill writes access.json to the default location while the server reads
from the override — pairing and allowlist edits silently don't take effect.

Skills now resolve the state dir via shell expansion (TELEGRAM_STATE_DIR →
CLAUDE_CONFIG_DIR/channels/telegram → ~/.claude/channels/telegram) before
any read/write. Server gets the same CLAUDE_CONFIG_DIR fallback. Also adds
Bash(echo)/Bash(chmod) to configure skill's allowed-tools (chmod was already
documented but not allowlisted).

* fix(telegram): verify stale PID is a server.ts process before SIGTERM

PID files race with OS PID recycling. The lockfile from #1349 stored only a
PID; after enough churn that PID can be reassigned to anything — including
the new launch's own bun-run wrapper. SIGTERMing the wrapper closes our
stdin and triggers immediate self-shutdown ('replacing stale poller' then
'shutting down' within seconds — matches #1459 item 3).

Now check 'ps -p <pid> -o args=' contains 'server.ts' before killing.
execFileSync (no shell); whole block already try/catch so Windows/ps-missing
falls through to just overwriting the lockfile.

* fix(telegram): drop ppid watchdog check; redirect bun install stdout to stderr

Two v0.0.5/0.0.6 regressions causing the plugin to fail at startup:

1. The orphan watchdog's process.ppid !== bootPpid check false-fires when the
   bun-run/shell wrapper exits or execs during normal startup and we get
   reparented to init — plugin self-terminates ~5s after launch. Stdin-close
   alone is the correct signal: the kernel closes the MCP pipe on any CLI
   death regardless of intermediate wrappers, so the ppid check was both
   unnecessary and harmful. (#1467; also the actual cause of #1459 item 3
   and likely #1425.)

2. 'bun install --no-summary' in the start script writes to stdout, which is
   the MCP JSON-RPC transport. The harness sees non-JSON bytes during the
   handshake and drops the connection ('Failed to connect'). Redirect install
   output to stderr. (#1470; also explains #1425 on Windows.)

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Bryan Thompson <238056179+bryan-anthropic@users.noreply.github.com>
2026-08-12 20:03:09 -05:00
Noah Zweben
5a71459c03
telegram: gate /start, /help, /status behind dmPolicy (#894)
The bot command handlers bypassed access control — they responded to
any DM user regardless of dmPolicy, leaking bot presence and
contradicting ACCESS.md's "Drop silently. No reply." contract for
allowlist mode.

Add dmCommandGate() that applies the same disabled/allowlist checks
as gate() without the pairing side effects, and route all three
handlers through it. Also prune expired pending codes before /status
iterates them.

Fixes #854

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 12:02:34 -07:00
Noah Zweben
7e401edac7
fix(telegram): retry polling on all transient errors, not just 409 (#1397)
A single ETIMEDOUT/ECONNRESET/DNS failure during long-polling rejected
bot.start(); the catch block returned and polling stopped permanently.
The MCP server process stayed alive (stdin keeps it running), so outbound
reply/react tools kept working — but the bot was deaf to inbound messages
until a full restart. Users see 'typing...' then nothing, indistinguishable
from the harness-side gate bug.

Now all errors retry with the same capped backoff (max 15s). attempt resets
to 0 in onStart so backoff doesn't accumulate across a long-running session.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-14 12:47:13 -07:00
Noah Zweben
58578a456a
fix(telegram): prevent zombie pollers from blocking new sessions with 409 Conflict (#1349)
* fix(telegram): prevent zombie pollers from blocking new sessions

The MCP server runs as a grandchild of the CLI (via `bun run start` →
shell → `bun server.ts`). When the CLI is killed uncleanly (SIGKILL,
crash, terminal close), the grandchild survives as an orphan and keeps
long-polling getUpdates indefinitely. Telegram allows only one consumer
per token, so every subsequent session sees 409 Conflict and the
existing retry loop spins forever.

Three layered mitigations:

- PID lockfile (STATE_DIR/bot.pid): on startup, SIGTERM any stale holder
  before claiming the slot, so a fresh session always wins.
- Orphan watchdog: every 5s check for parent reparenting (POSIX ppid
  change) or a dead stdin pipe, and self-terminate. Covers cases where
  the existing stdin end/close events never fire through the wrapper.
- 409 retry cap: give up after 8 attempts (~28s) instead of looping
  forever, and bail immediately if shutdown has begun.

Also adds a SIGHUP handler and removes the pidfile on clean shutdown
(only if still owned by this process).

* chore(telegram): bump version to 0.0.5

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-10 12:01:01 -07:00
Noah Zweben
4b1e2a28ce
feat(telegram,discord): compact permission messages with expandable details (#952)
* feat(telegram,discord): compact permission messages with expandable details

Replace verbose permission request messages with a compact format showing
only the tool name. Adds a "See more" button that expands inline to show
tool_name, description, and pretty-printed input_preview JSON. Yes/No
buttons replace Allow/Deny. Bump plugin versions to 0.0.4.

* revert: restore Allow/Deny button labels
2026-03-23 22:53:47 -07:00
Daisy S. Hollman
b3a0714d7f
feat(telegram,discord): inline buttons for permission approval (#945)
Replace "Reply 'yes abcde' to allow" text instruction with native
inline buttons (Telegram InlineKeyboard, Discord ButtonBuilder).
One tap to approve/deny instead of typing a 5-char ID.

- Telegram: callback_query handler with allowFrom gate, edits message
  to show outcome and remove buttons after decision
- Discord: interactionCreate handler with allowFrom gate, updates
  interaction with outcome and clears components
- Text-reply path (PERMISSION_REPLY_RE) kept as fallback
- Bump both plugins to v0.0.3

🏠 Remote-Dev: homespace
2026-03-23 22:19:51 -07:00
Daisy Hollman
daa84c99c8
feat(telegram,discord): permission-relay capability + bidirectional handlers
Complete the plugin side of anthropics/claude-cli-internal#23061 (permission
prompts over channels).

Capability: both servers now declare
  experimental["claude/channel/permission"]
which tells CC they can relay permission requests. This capability asserts the
server authenticates the replier — gate()/access.allowFrom filters
non-allowlisted senders before handleInbound runs.

Outbound (CC → user): setNotificationHandler for
  notifications/claude/channel/permission_request
formats the tool name, description, and input preview into a human-readable
message and sends it to every allowlisted DM. Groups are excluded — the
security thread resolution was "single-user mode for official plugins."

Inbound (user → CC): PERMISSION_REPLY_RE intercept in handleInbound catches
"yes xxxxx" / "no xxxxx" replies, emits the structured
  notifications/claude/channel/permission
event with {request_id, behavior}, reacts with checkmark/cross, and returns
without relaying the text to Claude as a chat message.

The regex is inlined from channelPermissions.ts (no cross-repo dep). IDs are
lowercased at the plugin boundary per the case-insensitive spec.

Version bumped 0.0.1 → 0.0.2 so the plugin reconciler picks up the change.

🏠 Remote-Dev: homespace
2026-03-23 08:59:02 +00:00
Kenneth Lien
51bd7bd5f2
Merge remote-tracking branch 'origin/kenneth/telegram-all-file-types' into kenneth/channels-rollup 2026-03-20 13:13:58 -07:00
Kenneth Lien
71b102d75d
Merge remote-tracking branch 'origin/kenneth/telegram-bot-commands-795' into kenneth/channels-rollup
# Conflicts:
#	external_plugins/telegram/server.ts
2026-03-20 13:13:58 -07:00
Kenneth Lien
556b21af96
Merge remote-tracking branch 'origin/kenneth/telegram-bot-commands' into kenneth/channels-rollup 2026-03-20 13:13:08 -07:00
Kenneth Lien
aa4f7c4fb0
Merge remote-tracking branch 'origin/kenneth/discord-edit-notif-guidance' into kenneth/channels-rollup 2026-03-20 13:13:08 -07:00
Kenneth Lien
24a170a704
Merge remote-tracking branch 'origin/kenneth/channels-state-dir' into kenneth/channels-rollup 2026-03-20 13:13:07 -07:00
Kenneth Lien
f3fc62a8e7
Merge remote-tracking branch 'origin/kenneth/telegram-409' into kenneth/channels-rollup
# Conflicts:
#	external_plugins/telegram/server.ts
2026-03-20 13:13:07 -07:00
Kenneth Lien
757480dd76
Merge remote-tracking branch 'origin/kenneth/telegram-shutdown' into kenneth/channels-rollup 2026-03-20 13:12:58 -07:00
Kenneth Lien
1636fedbd4
Sanitize user-controlled filenames and download path components
- safeName() strips <>[]\r\n; from file_name/title before they hit the
  <channel> notification — delimiter chars would let an uploader break
  out of the tag or forge meta entries
- download_attachment strips ext/uniqueId to alphanumeric before join()
  — defense-in-depth against path traversal (file_unique_id is
  Telegram-controlled so this is belt-and-braces)
2026-03-20 11:56:57 -07:00
Kenneth Lien
ea382ec6a4
Tighten /start and /help copy
Less chatty, more precise. Explicitly mentions the /telegram:access
skill and the 6-char code format.
2026-03-20 11:55:56 -07:00
Kenneth Lien
9a101ba34c
Restrict bot commands to DMs (security)
- /status in a group would leak the sender's pending pairing code to
  other group members, who could then pair as that user
- Commands in non-allowlisted groups confirm bot presence and enable spam
- /start now acknowledges dmPolicy === 'disabled' instead of lying
- setMyCommands scoped to private chats so the / menu only shows in DMs
2026-03-20 11:54:48 -07:00
Kenneth Lien
a9bc23da6f
telegram: handle all inbound file types + download_attachment tool 2026-03-20 11:51:42 -07:00
Kenneth Lien
521f858e11
telegram: add /start /help /status bot commands 2026-03-20 11:47:39 -07:00
Kenneth Lien
a7cb39c269
telegram: add MarkdownV2 parse_mode to reply/edit_message 2026-03-20 11:45:46 -07:00
Kenneth Lien
5c58308be4
discord/telegram: guide assistant to send new reply on completion
Message edits don't trigger push notifications on the user's device.
Update system instructions and edit_message tool description to steer
the assistant toward edit-for-progress + new-reply-on-completion.

Fixes #786
2026-03-20 11:27:09 -07:00
Kenneth Lien
3d8042f259
Silently return when bot.stop() aborts the setup phase
If bot.stop() is called while bot.start() is still in setup (deleteWebhook/
getMe), grammy rejects with 'Aborted delay'. Expected, not an error.
2026-03-20 11:07:05 -07:00
Kenneth Lien
14927ff475
telegram/discord: make state dir configurable via env var
Hardcoded ~/.claude/channels/<name>/ meant only one bot per machine.
Respect TELEGRAM_STATE_DIR / DISCORD_STATE_DIR so users can run
multiple bots with separate tokens and allowlists.

Also fixed README path ('in your project' -> '~/...') to match the code.

Fixes #792
2026-03-20 10:56:57 -07:00
Kenneth Lien
1daff5f224
telegram: retry on 409 Conflict instead of crashing
During /mcp reload or when a zombie from a previous session still holds
the polling slot, the new process gets 409 Conflict on its first
getUpdates and dies immediately. Retry with backoff until the slot
frees — typically within a second or two.

Also handles the two-sessions case: the second Claude Code instance
keeps retrying (with a clear message about what's happening) and takes
over when the first one exits.

Fixes #804 #794, partial #788 (issue 4)
2026-03-20 10:55:27 -07:00
Kenneth Lien
2aa90a8387
telegram: exit when Claude Code closes the connection
When the MCP stdio transport closes, the bot kept polling Telegram as
a zombie process — holding the token and causing 409 Conflict for the
next session.

- Listen for stdin end/close and SIGTERM/SIGINT -> bot.stop() + exit
- Force-exit after 2s if bot.stop() stalls on the long-poll timeout
- unref the approval-check interval so it doesn't keep us alive

Fixes #793, partial #788 (issue 3)
2026-03-20 10:54:33 -07:00
Kenneth Lien
9f2a4feab9
telegram: add error handlers to stop silent polling death
The bot would silently stop delivering messages after the first error:
grammy's default handler calls bot.stop() on any middleware throw, and
void bot.start() / void mcp.notification() swallow rejections with no log.

- bot.catch(): log and keep polling on handler errors
- bot.start().catch(): log when polling dies (bad token, 409, network)
- mcp.notification().catch(): log when inbound delivery to Claude fails
- process-level unhandledRejection/uncaughtException as a safety net

Fixes #756 #759 #761 #777 #809, partial #788
2026-03-20 10:53:36 -07:00
Kenneth Lien
8140fbad22
Lock telegram/discord .env files to owner (chmod 600)
The bot token is a credential. Tighten perms on load so hand-written
or pre-existing .env files get locked down, and update the configure
skill to chmod after writing. No-op on Windows.
2026-03-20 10:37:13 -07:00
Kenneth Lien
7994c270e5
Revert "Remove telegram, discord, and fakechat plugins (#741)" (#753)
This reverts commit d53f6ca4cdb000c671dfdc1c181b021e97c25505.
2026-03-19 13:59:14 -07:00
Noah Zweben
d53f6ca4cd
Remove telegram, discord, and fakechat plugins (#741)
Remove the three chat bridge plugins from external_plugins/ and their
corresponding entries in marketplace.json.

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-18 21:31:12 -07:00
Kenneth Lien
1b33c1d9f9
Add telegram channel plugin (#735)
Telegram messaging bridge for Claude Code. Runs a local MCP server that
connects to the Telegram Bot API via a user-created bot token.

Built-in access control: inbound messages are gated by an allowlist
(default: pairing mode), outbound sends are scoped to the same allowlist.
The /telegram:access skill manages pairing, allowlists, and policy.

Ships full source — server.ts runs locally via bun, started by the
.mcp.json command. First external_plugins entry to bundle source rather
than point at a hosted MCP endpoint.
2026-03-18 23:46:59 +00:00