ayoub5550 commited on
Commit
df2e540
Β·
verified Β·
1 Parent(s): b711c8d

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +335 -0
app.py ADDED
@@ -0,0 +1,335 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Telegram bot wrapping the REAL Claude Code CLI.
4
+ - Streams output to Telegram in real-time by editing messages
5
+ - Merges stdout+stderr to capture everything
6
+ - Robust timeout handling
7
+ """
8
+
9
+ import os, asyncio, json, subprocess, logging, threading, time
10
+ from flask import Flask, jsonify
11
+ from telegram import Update, BotCommand, Bot
12
+ from telegram.ext import Application, CommandHandler, MessageHandler, filters
13
+ from telegram.constants import ChatAction
14
+ from telegram.request import HTTPXRequest
15
+ import httpx
16
+
17
+ # ──── Config ────
18
+ TELEGRAM_BOT_TOKEN = os.environ["TELEGRAM_BOT_TOKEN"]
19
+ ALLOWED_USERS = [u.strip() for u in os.environ.get("TELEGRAM_ALLOWED_USERS", "").split(",") if u.strip()]
20
+ PROXY_URL = os.environ.get("PROXY_URL", "")
21
+ PROXY_SECRET = os.environ.get("PROXY_SECRET", "")
22
+ TELEGRAM_BASE_URL = f"{PROXY_URL}/bot" if PROXY_URL else "https://api.telegram.org/bot"
23
+ MODEL = os.environ.get("MODEL", "z-ai/glm-5.1")
24
+ START_TIME = time.time()
25
+
26
+ # Claude Code environment
27
+ CLAUDE_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
+
40
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
41
+ log = logging.getLogger("claude-bot")
42
+
43
+ # ──── Flask health ────
44
+ flask_app = Flask(__name__)
45
+
46
+ @flask_app.route("/")
47
+ @flask_app.route("/health")
48
+ def 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 == 200
55
+ except:
56
+ proxy_ok = False
57
+ 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 ────
73
+ async 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 alive
101
+ now = time.time()
102
+ if now - last_typing > 3:
103
+ try:
104
+ await chat.send_action(ChatAction.TYPING)
105
+ except:
106
+ pass
107
+ last_typing = now
108
+
109
+ # Check if process died
110
+ if proc.returncode is not None:
111
+ break
112
+ continue
113
+
114
+ if not chunk:
115
+ break
116
+
117
+ text = chunk.decode("utf-8", errors="replace")
118
+ accumulated += text
119
+
120
+ # Stream to Telegram: edit message every 3 seconds
121
+ now = time.time()
122
+ if now - last_edit > 3 and accumulated.strip():
123
+ display = accumulated.strip()
124
+ # Take last 3900 chars to fit Telegram limit
125
+ 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 = now
132
+
133
+ except asyncio.CancelledError:
134
+ proc.kill()
135
+ raise
136
+ except Exception as e:
137
+ log.error(f"Stream read error: {e}")
138
+
139
+ # Wait for process to finish
140
+ try:
141
+ await asyncio.wait_for(proc.wait(), timeout=10)
142
+ except asyncio.TimeoutError:
143
+ proc.kill()
144
+
145
+ exit_code = proc.returncode
146
+ log.info(f"Claude Code exited with code {exit_code}, output length: {len(accumulated)}")
147
+
148
+ result = accumulated.strip()
149
+ if result:
150
+ return result
151
+ else:
152
+ return f"❌ No output (exit code: {exit_code})"
153
+
154
+
155
+ # ──── Telegram Handlers ────
156
+ async 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
+ return
161
+ 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
+
182
+ async def cmd_clear(update: Update, context):
183
+ uid = str(update.effective_user.id)
184
+ if ALLOWED_USERS and uid not in ALLOWED_USERS:
185
+ return
186
+ 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
+
191
+ async def cmd_files(update: Update, context):
192
+ uid = str(update.effective_user.id)
193
+ if ALLOWED_USERS and uid not in ALLOWED_USERS:
194
+ return
195
+ 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=10
198
+ )
199
+ files = r.stdout.strip() or "(empty)"
200
+ await update.message.reply_text(f"πŸ“ Workspace:\n```\n{files}\n```", parse_mode="Markdown")
201
+
202
+
203
+ async def cmd_bash(update: Update, context):
204
+ uid = str(update.effective_user.id)
205
+ if ALLOWED_USERS and uid not in ALLOWED_USERS:
206
+ return
207
+ 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
+ return
211
+ 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
+
221
+ async 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
+ return
226
+
227
+ text = update.message.text
228
+ if not text:
229
+ return
230
+
231
+ # Send initial streaming message
232
+ 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 timeout
239
+ )
240
+
241
+ # Send final result
242
+ 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 chunks
252
+ try:
253
+ await status_msg.delete()
254
+ except:
255
+ pass
256
+ 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
+
268
+ async 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 ────
284
+ def 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
+
289
+ async 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
+ pass
329
+
330
+ stop = asyncio.Event()
331
+ await stop.wait()
332
+
333
+
334
+ if __name__ == "__main__":
335
+ asyncio.run(main())