CoolFace
Apppublic

OverBeyond/ClaudeCode

sourceHugging Faceupdated 6mo agoView on Hugging Face
0likes
bot.js381 linesDownload Raw Back to root
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});