OverBeyond/ClaudeCode
0
1import { Bot, InputFile } from "grammy";2import OpenAI from "openai";3import { exec } from "child_process";4import { promisify } from "util";5import fs from "fs";6import path from "path";7import archiver from "archiver";8 9const execAsync = promisify(exec);10 11const NVIDIA_API_KEY = process.env.NVIDIA_API_KEY;12const NVIDIA_BASE_URL = "https://integrate.api.nvidia.com/v1";13const TELEGRAM_BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN;14const OWNER_ID = process.env.OWNER_ID ? parseInt(process.env.OWNER_ID) : null;15 16const MODELS = [17 "z-ai/glm-5",18 "moonshotai/kimi-k2.5",19 "minimax/minimax-m2.1"20];21 22const openai = new OpenAI({23 apiKey: NVIDIA_API_KEY,24 baseURL: NVIDIA_BASE_URL25});26 27const bot = new Bot(TELEGRAM_BOT_TOKEN, {28 client: {29 apiRoot: process.env.TG_PROXY || "https://tg-proxyclaudecode.beyondoyj.workers.dev"30 }31});32console.log("Telegram proxy:", process.env.TG_PROXY || "https://tg-proxyclaudecode.beyondoyj.workers.dev");33 34// Per-user session state35const sessions = new Map();36 37function getSession(uid) {38 if (!sessions.has(uid)) {39 sessions.set(uid, {40 messages: [],41 awaitingApproval: false,42 pendingAction: null,43 cwd: `/app/projects/${uid}`44 });45 }46 return sessions.get(uid);47}48 49// ---- LLM with automatic fallback ----50async function callLLM(messages) {51 let lastErr;52 for (const model of MODELS) {53 try {54 const res = await openai.chat.completions.create({55 model,56 messages,57 temperature: 0.7,58 max_tokens: 409659 });60 return { content: res.choices[0].message.content, model };61 } catch (e) {62 lastErr = e;63 console.error(`[LLM] ${model} failed: ${e.message}`);64 }65 }66 throw new Error("All models failed: " + lastErr?.message);67}68 69// ---- Execute shell command safely ----70async function runCmd(cmd, cwd) {71 try {72 const { stdout, stderr } = await execAsync(cmd, {73 cwd,74 timeout: 180000,75 maxBuffer: 10 * 1024 * 102476 });77 return { ok: true, out: [stdout, stderr].filter(Boolean).join("\n").trim() || "(no output)" };78 } catch (e) {79 return { ok: false, out: e.stderr || e.message };80 }81}82 83// ---- Write file to project dir ----84function writeProjectFile(relPath, content, cwd) {85 const full = path.join(cwd, relPath);86 fs.mkdirSync(path.dirname(full), { recursive: true });87 fs.writeFileSync(full, content);88 return full;89}90 91// ---- Directory tree ----92function tree(dir, prefix = "") {93 let result = "";94 const entries = fs.readdirSync(dir, { withFileTypes: true })95 .sort((a, b) => (a.isDirectory() === b.isDirectory() ? a.name.localeCompare(b.name) : a.isDirectory() ? -1 : 1));96 entries.forEach((e, i) => {97 const last = i === entries.length - 1;98 result += `${prefix}${last ? "└── " : "├── "}${e.name}\n`;99 if (e.isDirectory()) result += tree(path.join(dir, e.name), prefix + (last ? " " : "│ "));100 });101 return result;102}103 104// ---- Zip a directory ----105function zipDir(src, outPath) {106 return new Promise((resolve, reject) => {107 const out = fs.createWriteStream(outPath);108 const arc = archiver("zip", { zlib: { level: 9 } });109 out.on("close", () => resolve(outPath));110 arc.on("error", reject);111 arc.pipe(out);112 arc.directory(src, false);113 arc.finalize();114 });115}116 117// ---- Parse LLM actions ----118function parseActions(text) {119 const actions = [];120 let approvalAction = null;121 let clean = text;122 123 // Extract approval blocks first124 const appRe = /\[NEEDS_APPROVAL\]\s*\[EXEC\]([\s\S]*?)\[\/EXEC\]\s*\[\/NEEDS_APPROVAL\]/g;125 let m;126 while ((m = appRe.exec(text)) !== null) {127 approvalAction = { type: "exec", cmd: m[1].trim() };128 clean = clean.replace(m[0], "");129 }130 131 // Extract regular EXEC blocks132 const execRe = /\[EXEC\]([\s\S]*?)\[\/EXEC\]/g;133 while ((m = execRe.exec(clean)) !== null) {134 actions.push({ type: "exec", cmd: m[1].trim() });135 clean = clean.replace(m[0], "");136 }137 138 // Extract FILE blocks139 const fileRe = /\[FILE:(.+?)\]([\s\S]*?)\[\/FILE\]/g;140 while ((m = fileRe.exec(clean)) !== null) {141 actions.push({ type: "file", path: m[1].trim(), content: m[2] });142 clean = clean.replace(m[0], "");143 }144 145 return { actions, approvalAction, text: clean.trim() };146}147 148// ---- Send long text in chunks ----149async function sendLong(ctx, text, parse_mode) {150 const MAX = 4000;151 const opts = parse_mode ? { parse_mode } : {};152 for (let i = 0; i < text.length; i += MAX) {153 await ctx.reply(text.slice(i, i + MAX), opts);154 }155}156 157// ---- SYSTEM PROMPT ----158function systemPrompt(cwd) {159 let files = "";160 try { files = tree(cwd); } catch {}161 return `You are CodeBot — an expert coding agent running in a Linux Docker container.162 163CAPABILITIES:164- Node.js 22, Python 3, ffmpeg, git, npm, pip165- Create websites (HTML/CSS/JS, React, Three.js, WebGL)166- 3D graphics, animations, motion effects167- Audio/video processing with ffmpeg168- Install any npm/pip packages169- Create any file type170 171OUTPUT FORMAT:172To execute a shell command:173[EXEC]174command here175[/EXEC]176 177To create/write a file:178[FILE:relative/path/to/file]179file content180[/FILE]181 182For dangerous commands (rm -rf, publish, etc.):183[NEEDS_APPROVAL]184[EXEC]185dangerous command186[/EXEC]187[/NEEDS_APPROVAL]188 189For explanations and chat, just respond as normal text.190 191WORKING DIRECTORY: ${cwd}192CURRENT FILES:193 ${files || "(empty)"}194 195RULES:196- Always create complete, working code — never use placeholders or TODOs197- For websites, create index.html with all CSS/JS inline or in separate files198- Use modern practices and clean code199- When creating 3D content, use Three.js via CDN200- When creating animations, use CSS animations or GSAP via CDN201- Keep responses focused and practical`;202}203 204// ---- MAIN MESSAGE HANDLER ----205bot.on("message:text", async (ctx) => {206 const uid = ctx.from.id;207 const text = ctx.message.text;208 const s = getSession(uid);209 210 // Ensure project dir exists211 fs.mkdirSync(s.cwd, { recursive: true });212 213 // --- Handle approval response ---214 if (s.awaitingApproval) {215 const lower = text.toLowerCase();216 if (lower === "approve" || lower === "yes" || lower === "y" || text === "✅") {217 s.awaitingApproval = false;218 const action = s.pendingAction;219 s.pendingAction = null;220 221 await ctx.reply("⏳ Executing...");222 const res = await runCmd(action.cmd, s.cwd);223 await sendLong(ctx, `${res.ok ? "✅" : "❌"} \`${action.cmd}\`\n${res.out}`, "Markdown");224 return;225 }226 if (lower === "reject" || lower === "no" || lower === "n" || text === "❌") {227 s.awaitingApproval = false;228 s.pendingAction = null;229 await ctx.reply("🚫 Action cancelled.");230 return;231 }232 await ctx.reply('Reply *approve* or *reject*.', { parse_mode: "Markdown" });233 return;234 }235 236 // --- Built-in commands ---237 if (text === "/start") {238 await ctx.reply(`🤖 *CodeBot Ready*239 240Models: GLM-5 → Kimi K2.5 → MiniMax-M2.1 (auto-fallback)241 242Send me any task:243• "Build a 3D website with spinning cube"244• "Create an animated landing page"245• "Convert this video to mp3"246 247Commands:248/list — show project files249/download — get project as zip250/clear — reset conversation251/run <cmd> — execute shell command252`, { parse_mode: "Markdown" });253 return;254 }255 256 if (text === "/list") {257 try {258 await ctx.reply(`📁 Project:\n\`\`\`\n${tree(s.cwd) || "(empty)"}\`\`\``, { parse_mode: "Markdown" });259 } catch (e) { await ctx.reply("Error: " + e.message); }260 return;261 }262 263 if (text === "/download") {264 try {265 const files = fs.readdirSync(s.cwd);266 if (!files.length) { await ctx.reply("No files yet. Build something first!"); return; }267 await ctx.reply("📦 Packaging...");268 const zipPath = `/app/projects/bot-${uid}-${Date.now()}.zip`;269 await zipDir(s.cwd, zipPath);270 await ctx.replyWithDocument(new InputFile(zipPath, "project.zip"));271 fs.unlinkSync(zipPath);272 } catch (e) { await ctx.reply("Error: " + e.message); }273 return;274 }275 276 if (text === "/clear") {277 s.messages = [];278 await ctx.reply("🗑 Conversation reset.");279 return;280 }281 282 if (text.startsWith("/run ")) {283 const cmd = text.slice(5);284 await ctx.reply("⏳ Running...");285 const res = await runCmd(cmd, s.cwd);286 await sendLong(ctx, `${res.ok ? "✅" : "❌"} \`${cmd}\`\n${res.out}`, "Markdown");287 return;288 }289 290 // --- Process with LLM ---291 await ctx.sendChatAction("typing");292 s.messages.push({ role: "user", content: text });293 294 try {295 const msgs = [296 { role: "system", content: systemPrompt(s.cwd) },297 ...s.messages.slice(-30)298 ];299 const { content, model } = await callLLM(msgs);300 s.messages.push({ role: "assistant", content });301 302 const { actions, approvalAction, text: replyText } = parseActions(content);303 304 // Send text response305 if (replyText) {306 await sendLong(ctx, `🤖 *${model}*\n\n${replyText}`, "Markdown");307 }308 309 // Execute non-approval actions immediately310 for (const action of actions) {311 if (action.type === "exec") {312 await ctx.sendChatAction("typing");313 const res = await runCmd(action.cmd, s.cwd);314 await sendLong(ctx, `${res.ok ? "✅" : "❌"} \`${action.cmd}\`\n${res.out}`, "Markdown");315 } else if (action.type === "file") {316 writeProjectFile(action.path, action.content, s.cwd);317 await ctx.reply(`📄 Created \`${action.path}\``, { parse_mode: "Markdown" });318 }319 }320 321 // Handle approval-required action322 if (approvalAction) {323 s.awaitingApproval = true;324 s.pendingAction = approvalAction;325 await ctx.reply(326 `⚠️ *Needs approval:*\n\`${approvalAction.cmd}\`\n\nReply *approve* or *reject*`,327 { parse_mode: "Markdown" }328 );329 }330 331 } catch (e) {332 await ctx.reply("❌ Error: " + e.message);333 // Remove failed user message from history334 s.messages.pop();335 }336});337 338// Start339console.log("CodeBot starting...");340console.log("Token prefix:", TELEGRAM_BOT_TOKEN.slice(0, 8) + "...");341 342bot.catch(e => console.error("Bot error:", e));343 344// Diagnostics: test raw connection to proxy345try {346 console.log("Step 1: Testing DNS and HTTPS to proxy...");347 const { execSync } = await import("child_process");348 const result = execSync(`curl -s -o /dev/null -w "HTTP %{http_code}" ${process.env.TG_PROXY}`, { timeout: 10000 });349 console.log("Proxy reachable:", result.toString().trim());350} catch (e) {351 console.error("Proxy UNREACHABLE:", e.message);352 console.error("HF Spaces may be blocking all outbound HTTPS");353 process.exit(1);354}355 356// Test actual Telegram API through proxy357try {358 console.log("Step 2: Testing Telegram getMe through proxy...");359 const proxyUrl = (process.env.TG_PROXY || "https://tg-proxyclaudecode.beyondoyj.workers.dev");360 const resp = await fetch(`${proxyUrl}/bot${TELEGRAM_BOT_TOKEN}/getMe`);361 const data = await resp.json();362 if (data.ok) {363 console.log("Telegram API works! Bot: @" + data.result.username);364 } else {365 console.error("Telegram API error:", JSON.stringify(data));366 process.exit(1);367 }368} catch (e) {369 console.error("Telegram API through proxy failed:", e.message);370 process.exit(1);371}372 373// Start polling374console.log("Step 3: Starting long-polling via grammy...");375bot.start({376 onStart: (info) => {377 console.log("Polling active! Bot: @" + info.username);378 }379}).catch(e => {380 console.error("Polling error:", e.message);381});