Team Ai
Apppublic

ayoub5550/ClaudeCode

sourceHugging Faceupdated 5mo agoView on Hugging Face
0likes
app.py336 linesDownload Raw Back to root
1#!/usr/bin/env python32"""3Telegram bot wrapping the REAL Claude Code CLI.4- Streams output to Telegram in real-time by editing messages5- Merges stdout+stderr to capture everything6- Robust timeout handling7"""8 9import os, asyncio, json, subprocess, logging, threading, time10from flask import Flask, jsonify11from telegram import Update, BotCommand, Bot12from telegram.ext import Application, CommandHandler, MessageHandler, filters13from telegram.constants import ChatAction14from telegram.request import HTTPXRequest15import httpx16 17# ──── Config ────18TELEGRAM_BOT_TOKEN = os.environ["TELEGRAM_BOT_TOKEN"]19ALLOWED_USERS = [u.strip() for u in os.environ.get("TELEGRAM_ALLOWED_USERS", "").split(",") if u.strip()]20PROXY_URL = os.environ.get("PROXY_URL", "")21PROXY_SECRET = os.environ.get("PROXY_SECRET", "")22TELEGRAM_BASE_URL = f"{PROXY_URL}/bot" if PROXY_URL else "https://api.telegram.org/bot"23MODEL = os.environ.get("MODEL", "z-ai/glm-5.1")24START_TIME = time.time()25 26# Claude Code environment27CLAUDE_ENV = {28    **os.environ,29    "ANTHROPIC_BASE_URL": "http://localhost:4000",30    "ANTHROPIC_API_KEY": "sk-ant-proxy-key-for-local-translation",31    "ANTHROPIC_MODEL": "claude-sonnet-4-20250514",32    "DISABLE_PROMPT_CACHING": "true",33    "CLAUDE_CODE_SKIP_OOBE": "1",34    "HOME": "/home/user",35    "PATH": os.environ.get("PATH", "/usr/local/bin:/usr/bin:/bin"),36    "TERM": "xterm-256color",37    "LANG": "en_US.UTF-8",38}39 40logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")41log = logging.getLogger("claude-bot")42 43# ──── Flask health ────44flask_app = Flask(__name__)45 46@flask_app.route("/")47@flask_app.route("/health")48def health():49    uptime = int(time.time() - START_TIME)50    m, s = divmod(uptime, 60)51    h, m = divmod(m, 60)52    try:53        r = httpx.get("http://localhost:4000/health", timeout=5)54        proxy_ok = r.status_code == 20055    except:56        proxy_ok = False57    try:58        v = subprocess.run(["claude", "--version"], capture_output=True, text=True, timeout=10, env=CLAUDE_ENV)59        claude_version = v.stdout.strip() or v.stderr.strip() or "?"60    except:61        claude_version = "?"62    return jsonify({63        "status": "ok",64        "engine": "Claude Code CLI (real)",65        "claude_version": claude_version,66        "model_backend": MODEL,67        "proxy": "ok" if proxy_ok else "down",68        "uptime": f"{h}h{m}m{s}s",69    })70 71 72# ──── Claude Code runner with streaming ────73async def run_claude_streaming(prompt: str, status_msg, chat) -> str:74    """Run claude -p and stream output to Telegram by editing the status message."""75    cmd = [76        "claude", "-p", prompt,77        "--dangerously-skip-permissions",78        "--output-format", "text",79    ]80 81    log.info(f"Running: claude -p '{prompt[:80]}...'")82 83    proc = await asyncio.create_subprocess_exec(84        *cmd,85        stdout=asyncio.subprocess.PIPE,86        stderr=asyncio.subprocess.STDOUT,  # Merge stderr → stdout (avoids deadlocks)87        cwd="/home/user/workspace",88        env=CLAUDE_ENV,89    )90 91    accumulated = ""92    last_edit = time.time()93    last_typing = time.time()94 95    try:96        while True:97            try:98                chunk = await asyncio.wait_for(proc.stdout.read(1024), timeout=4)99            except asyncio.TimeoutError:100                # Keep typing indicator alive101                now = time.time()102                if now - last_typing > 3:103                    try:104                        await chat.send_action(ChatAction.TYPING)105                    except:106                        pass107                    last_typing = now108 109                # Check if process died110                if proc.returncode is not None:111                    break112                continue113 114            if not chunk:115                break116 117            text = chunk.decode("utf-8", errors="replace")118            accumulated += text119 120            # Stream to Telegram: edit message every 3 seconds121            now = time.time()122            if now - last_edit > 3 and accumulated.strip():123                display = accumulated.strip()124                # Take last 3900 chars to fit Telegram limit125                if len(display) > 3900:126                    display = "...\n" + display[-3900:]127                try:128                    await status_msg.edit_text(display[:4096])129                except Exception as e:130                    log.debug(f"Edit failed: {e}")131                last_edit = now132 133    except asyncio.CancelledError:134        proc.kill()135        raise136    except Exception as e:137        log.error(f"Stream read error: {e}")138 139    # Wait for process to finish140    try:141        await asyncio.wait_for(proc.wait(), timeout=10)142    except asyncio.TimeoutError:143        proc.kill()144 145    exit_code = proc.returncode146    log.info(f"Claude Code exited with code {exit_code}, output length: {len(accumulated)}")147 148    result = accumulated.strip()149    if result:150        return result151    else:152        return f"❌ No output (exit code: {exit_code})"153 154 155# ──── Telegram Handlers ────156async def cmd_start(update: Update, context):157    uid = str(update.effective_user.id)158    if ALLOWED_USERS and uid not in ALLOWED_USERS:159        await update.message.reply_text("⛔")160        return161    try:162        v = subprocess.run(["claude", "--version"], capture_output=True, text=True, timeout=10, env=CLAUDE_ENV)163        version = v.stdout.strip() or "?"164    except:165        version = "?"166    await update.message.reply_text(167        "🤖 *Claude Code* — Real CLI Agent\n\n"168        f"📦 Version: `{version}`\n"169        f"🧠 Backend: `{MODEL}`\n"170        "💻 Workspace: `/home/user/workspace`\n"171        "🔧 Permissions: Full (bash, read, write, edit)\n\n"172        "*Commands:*\n"173        "/start — New session\n"174        "/clear — Clear workspace & session\n"175        "/files — List workspace files\n"176        "/bash `cmd` — Quick bash command\n\n"177        "Send any coding task! 🚀",178        parse_mode="Markdown",179    )180 181 182async def cmd_clear(update: Update, context):183    uid = str(update.effective_user.id)184    if ALLOWED_USERS and uid not in ALLOWED_USERS:185        return186    subprocess.run(["bash", "-c", "rm -rf /home/user/workspace/* /home/user/.claude/projects/*"],187                    capture_output=True, cwd="/home/user")188    await update.message.reply_text("🧹 Workspace and session cleared.")189 190 191async def cmd_files(update: Update, context):192    uid = str(update.effective_user.id)193    if ALLOWED_USERS and uid not in ALLOWED_USERS:194        return195    r = subprocess.run(196        ["bash", "-c", "find /home/user/workspace -type f 2>/dev/null | head -50 || echo '(empty)'"],197        capture_output=True, text=True, timeout=10198    )199    files = r.stdout.strip() or "(empty)"200    await update.message.reply_text(f"📁 Workspace:\n```\n{files}\n```", parse_mode="Markdown")201 202 203async def cmd_bash(update: Update, context):204    uid = str(update.effective_user.id)205    if ALLOWED_USERS and uid not in ALLOWED_USERS:206        return207    cmd = update.message.text.replace("/bash", "", 1).strip()208    if not cmd:209        await update.message.reply_text("Usage: `/bash ls -la`", parse_mode="Markdown")210        return211    await update.message.chat.send_action(ChatAction.TYPING)212    try:213        r = subprocess.run(["bash", "-c", cmd], capture_output=True, text=True,214                           timeout=60, cwd="/home/user/workspace")215        output = (r.stdout + r.stderr).strip() or "(no output)"216        await send_long(update, f"```\n{output[:4000]}\n```")217    except subprocess.TimeoutExpired:218        await update.message.reply_text("⏰ Timed out (60s).")219 220 221async def handle_message(update: Update, context):222    uid = str(update.effective_user.id)223    if ALLOWED_USERS and uid not in ALLOWED_USERS:224        await update.message.reply_text("⛔")225        return226 227    text = update.message.text228    if not text:229        return230 231    # Send initial streaming message232    status_msg = await update.message.reply_text("⏳ Claude Code is working...")233 234    try:235        # Run with streaming (edits the status_msg in real-time)236        result = await asyncio.wait_for(237            run_claude_streaming(text, status_msg, update.message.chat),238            timeout=330,  # 5.5 min total timeout239        )240 241        # Send final result242        if len(result) <= 4096:243            try:244                await status_msg.edit_text(result, parse_mode="Markdown")245            except:246                try:247                    await status_msg.edit_text(result)248                except:249                    await send_long(update, result)250        else:251            # Delete status msg and send in chunks252            try:253                await status_msg.delete()254            except:255                pass256            await send_long(update, result)257 258    except asyncio.TimeoutError:259        await status_msg.edit_text("⏰ Timed out after 5 minutes.")260    except Exception as e:261        log.exception("Error in handle_message")262        try:263            await status_msg.edit_text(f"❌ Error: {e}")264        except:265            await update.message.reply_text(f"❌ Error: {e}")266 267 268async def send_long(update: Update, text: str):269    """Send long text in chunks."""270    if not text:271        text = "✅ Done."272    chunks = [text[i:i + 4096] for i in range(0, len(text), 4096)]273    for chunk in chunks[:15]:274        try:275            await update.message.reply_text(chunk, parse_mode="Markdown")276        except:277            try:278                await update.message.reply_text(chunk)279            except Exception as e:280                log.error(f"Send failed: {e}")281 282 283# ──── Main ────284def run_flask():285    port = int(os.environ.get("FLASK_PORT", "7860"))286    flask_app.run(host="0.0.0.0", port=port, use_reloader=False)287 288 289async def main():290    log.info("Starting Claude Code Telegram Bot")291 292    flask_thread = threading.Thread(target=run_flask, daemon=True)293    flask_thread.start()294 295    request = HTTPXRequest(296        connection_pool_size=8,297        connect_timeout=30.0,298        read_timeout=60.0,299        write_timeout=30.0,300    )301    bot = Bot(302        token=TELEGRAM_BOT_TOKEN,303        base_url=TELEGRAM_BASE_URL,304        request=request,305    )306    application = Application.builder().bot(bot).build()307 308    application.add_handler(CommandHandler("start", cmd_start))309    application.add_handler(CommandHandler("clear", cmd_clear))310    application.add_handler(CommandHandler("files", cmd_files))311    application.add_handler(CommandHandler("bash", cmd_bash))312    application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message))313 314    await application.initialize()315    await application.start()316    await application.updater.start_polling(drop_pending_updates=True)317 318    log.info("✅ Bot started — polling Telegram")319 320    try:321        await bot.set_my_commands([322            BotCommand("start", "New session"),323            BotCommand("clear", "Clear workspace"),324            BotCommand("files", "List files"),325            BotCommand("bash", "Run bash command"),326        ])327    except:328        pass329 330    stop = asyncio.Event()331    await stop.wait()332 333 334if __name__ == "__main__":335    asyncio.run(main())336