Spaces:
Running
Running
Switch from Claude Code/Codeman to native Avi Telegram Agent (@amangt_bot), OpenCode CLI, and DeepSeek Harness
Browse files- Dockerfile +8 -26
- config/Caddyfile.example +12 -5
- security/claude-wrapper.sh +0 -16
- security/patch-claude-telegram.py +0 -19
- services/25-codeman.sh +0 -36
- services/30-claude-telegram.sh +0 -52
- services/30-telegram-agent.sh +37 -0
- services/telegram_agent.py +170 -0
Dockerfile
CHANGED
|
@@ -75,7 +75,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
| 75 |
tmux \
|
| 76 |
locales \
|
| 77 |
&& rm -rf /var/lib/apt/lists/* && \
|
| 78 |
-
|
|
|
|
| 79 |
|
| 80 |
ENV LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8
|
| 81 |
|
|
@@ -146,7 +147,7 @@ ENV REA_APKTOOL_COMMAND="/usr/local/bin/apktool"
|
|
| 146 |
# ------------------------------------------
|
| 147 |
COPY --from=binwalk-builder /install/bin/binwalk /usr/local/bin/binwalk
|
| 148 |
RUN chmod +x /usr/local/bin/binwalk && \
|
| 149 |
-
pip3 install --no-cache-dir --break-system-packages unblob==26.6.4 pwntools
|
| 150 |
|
| 151 |
ENV REA_BINWALK_COMMAND="/usr/local/bin/binwalk"
|
| 152 |
ENV REA_UNBLOB_COMMAND="/usr/local/bin/unblob"
|
|
@@ -189,39 +190,20 @@ RUN curl -fsSL "https://github.com/oven-sh/bun/releases/latest/download/bun-linu
|
|
| 189 |
rm -rf /tmp/bun*
|
| 190 |
|
| 191 |
# ------------------------------------------
|
| 192 |
-
# 12.
|
| 193 |
# ------------------------------------------
|
| 194 |
COPY security/stub-playwright-index.js /tmp/stub-pw.js
|
| 195 |
COPY security/stub-playwright-index.mjs /tmp/stub-pw.mjs
|
| 196 |
-
RUN npm install -g supergateway@latest && \
|
| 197 |
-
|
| 198 |
-
npm install -g @anthropic-ai/claude-code && \
|
| 199 |
-
npm install -g aicodeman@latest && \
|
| 200 |
-
npm install -g opencode-ai && \
|
| 201 |
-
npm install -g @deepseek-ai/dsh pnpm yarn typescript && \
|
| 202 |
-
which supergateway codeman claude rea opencode && \
|
| 203 |
cp /tmp/stub-pw.js /usr/local/lib/node_modules/rea-agents/node_modules/playwright-core/index.js 2>/dev/null || true && \
|
| 204 |
cp /tmp/stub-pw.mjs /usr/local/lib/node_modules/rea-agents/node_modules/playwright-core/index.mjs 2>/dev/null || true && \
|
| 205 |
rm -f /tmp/stub-pw.* && \
|
| 206 |
for b in chromium chromium-browser google-chrome google-chrome-stable chrome firefox playwright playwright-core puppeteer chromedriver geckodriver msedge brave-browser webkit; do \
|
| 207 |
printf '#!/bin/sh\necho "ERROR: Browser engines and browser automation are strictly disabled by host security policy." >&2\nexit 126\n' > "/usr/local/bin/$b" && chmod 755 "/usr/local/bin/$b"; \
|
| 208 |
done && \
|
| 209 |
-
rm -rf /root/.npm
|
| 210 |
-
|
| 211 |
-
# ------------------------------------------
|
| 212 |
-
# 13. Claude Code Telegram Bot Bridge
|
| 213 |
-
# ------------------------------------------
|
| 214 |
-
COPY security/patch-claude-telegram.py /tmp/patch-claude-telegram.py
|
| 215 |
-
RUN git clone https://github.com/RichardAtCT/claude-code-telegram.git /opt/claude-code-telegram && \
|
| 216 |
-
python3 /tmp/patch-claude-telegram.py && \
|
| 217 |
-
python3 -m venv /opt/claude-code-telegram/venv && \
|
| 218 |
-
/opt/claude-code-telegram/venv/bin/pip install --no-cache-dir poetry && \
|
| 219 |
-
/opt/claude-code-telegram/venv/bin/pip install --no-cache-dir /opt/claude-code-telegram && \
|
| 220 |
-
rm -f /tmp/patch-claude-telegram.py
|
| 221 |
-
|
| 222 |
-
COPY security/claude-wrapper.sh /tmp/claude-wrapper.sh
|
| 223 |
-
RUN cp /tmp/claude-wrapper.sh /usr/local/bin/claude && chmod 755 /usr/local/bin/claude && rm -f /tmp/claude-wrapper.sh && \
|
| 224 |
-
/usr/local/bin/rea setup --client claude_code --client opencode --client vscode --yes || true
|
| 225 |
|
| 226 |
# ------------------------------------------
|
| 227 |
# 14. Application Setup & Runtime Runner
|
|
|
|
| 75 |
tmux \
|
| 76 |
locales \
|
| 77 |
&& rm -rf /var/lib/apt/lists/* && \
|
| 78 |
+
sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && \
|
| 79 |
+
/usr/sbin/locale-gen en_US.UTF-8
|
| 80 |
|
| 81 |
ENV LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8
|
| 82 |
|
|
|
|
| 147 |
# ------------------------------------------
|
| 148 |
COPY --from=binwalk-builder /install/bin/binwalk /usr/local/bin/binwalk
|
| 149 |
RUN chmod +x /usr/local/bin/binwalk && \
|
| 150 |
+
pip3 install --no-cache-dir --break-system-packages unblob==26.6.4 pwntools python-telegram-bot httpx
|
| 151 |
|
| 152 |
ENV REA_BINWALK_COMMAND="/usr/local/bin/binwalk"
|
| 153 |
ENV REA_UNBLOB_COMMAND="/usr/local/bin/unblob"
|
|
|
|
| 190 |
rm -rf /tmp/bun*
|
| 191 |
|
| 192 |
# ------------------------------------------
|
| 193 |
+
# 12. CLI Agents (OpenCode, DeepSeek Harness, REA, Supergateway) + Browser Hard-Block
|
| 194 |
# ------------------------------------------
|
| 195 |
COPY security/stub-playwright-index.js /tmp/stub-pw.js
|
| 196 |
COPY security/stub-playwright-index.mjs /tmp/stub-pw.mjs
|
| 197 |
+
RUN npm install -g supergateway@latest rea-agents@latest opencode-ai @deepseek-ai/dsh pnpm yarn typescript && \
|
| 198 |
+
which supergateway rea opencode && \
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
cp /tmp/stub-pw.js /usr/local/lib/node_modules/rea-agents/node_modules/playwright-core/index.js 2>/dev/null || true && \
|
| 200 |
cp /tmp/stub-pw.mjs /usr/local/lib/node_modules/rea-agents/node_modules/playwright-core/index.mjs 2>/dev/null || true && \
|
| 201 |
rm -f /tmp/stub-pw.* && \
|
| 202 |
for b in chromium chromium-browser google-chrome google-chrome-stable chrome firefox playwright playwright-core puppeteer chromedriver geckodriver msedge brave-browser webkit; do \
|
| 203 |
printf '#!/bin/sh\necho "ERROR: Browser engines and browser automation are strictly disabled by host security policy." >&2\nexit 126\n' > "/usr/local/bin/$b" && chmod 755 "/usr/local/bin/$b"; \
|
| 204 |
done && \
|
| 205 |
+
rm -rf /root/.npm && \
|
| 206 |
+
/usr/local/bin/rea setup --client opencode --client vscode --yes || true
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
|
| 208 |
# ------------------------------------------
|
| 209 |
# 14. Application Setup & Runtime Runner
|
config/Caddyfile.example
CHANGED
|
@@ -1,5 +1,14 @@
|
|
| 1 |
:7860 {
|
| 2 |
-
# 1. REA MCP Server (Streamable HTTP via supergateway)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
handle /mcp* {
|
| 4 |
reverse_proxy 127.0.0.1:8093 {
|
| 5 |
flush_interval -1
|
|
@@ -52,10 +61,8 @@
|
|
| 52 |
file_server
|
| 53 |
}
|
| 54 |
|
| 55 |
-
# 6.
|
| 56 |
handle {
|
| 57 |
-
|
| 58 |
-
header_up Host 127.0.0.1
|
| 59 |
-
}
|
| 60 |
}
|
| 61 |
}
|
|
|
|
| 1 |
:7860 {
|
| 2 |
+
# 1. REA MCP Server (Streamable HTTP via supergateway, gated with auth)
|
| 3 |
+
@mcp_unauth {
|
| 4 |
+
path /mcp*
|
| 5 |
+
not header Authorization "Bearer @man.g"
|
| 6 |
+
not header X-API-Key "@man.g"
|
| 7 |
+
}
|
| 8 |
+
respond @mcp_unauth "{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32000,\"message\":\"Unauthorized: valid Bearer token or X-API-Key required.\"},\"id\":null}" 401 {
|
| 9 |
+
close
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
handle /mcp* {
|
| 13 |
reverse_proxy 127.0.0.1:8093 {
|
| 14 |
flush_interval -1
|
|
|
|
| 61 |
file_server
|
| 62 |
}
|
| 63 |
|
| 64 |
+
# 6. Root URL redirects directly to VS Code (/code/)
|
| 65 |
handle {
|
| 66 |
+
redir / /code/ 302
|
|
|
|
|
|
|
| 67 |
}
|
| 68 |
}
|
security/claude-wrapper.sh
DELETED
|
@@ -1,16 +0,0 @@
|
|
| 1 |
-
#!/bin/bash
|
| 2 |
-
# Wrapper to allow Claude Code to run as root by stripping --dangerously-skip-permissions
|
| 3 |
-
args=("$@")
|
| 4 |
-
newargs=()
|
| 5 |
-
for arg in "${args[@]}"; do
|
| 6 |
-
if [ "$arg" != "--dangerously-skip-permissions" ] && [ "$arg" != "--allow-dangerously-skip-permissions" ]; then
|
| 7 |
-
newargs+=("$arg")
|
| 8 |
-
fi
|
| 9 |
-
done
|
| 10 |
-
|
| 11 |
-
REAL_CLAUDE="/usr/local/lib/node_modules/@anthropic-ai/claude-code/bin/claude.js"
|
| 12 |
-
if [ -f "$REAL_CLAUDE" ]; then
|
| 13 |
-
exec node "$REAL_CLAUDE" "${newargs[@]}"
|
| 14 |
-
else
|
| 15 |
-
exec npx -y @anthropic-ai/claude-code "${newargs[@]}"
|
| 16 |
-
fi
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
security/patch-claude-telegram.py
DELETED
|
@@ -1,19 +0,0 @@
|
|
| 1 |
-
import pathlib
|
| 2 |
-
|
| 3 |
-
p = pathlib.Path("/opt/claude-code-telegram/src/bot/core.py")
|
| 4 |
-
text = p.read_text(encoding="utf-8")
|
| 5 |
-
|
| 6 |
-
needle = "builder.get_updates_connection_pool_size(8)"
|
| 7 |
-
patch = """builder.get_updates_connection_pool_size(8)
|
| 8 |
-
|
| 9 |
-
apibase = os.environ.get("TELEGRAM_BOT_APIBASE")
|
| 10 |
-
if apibase:
|
| 11 |
-
builder.base_url(f"https://{apibase}/bot")
|
| 12 |
-
builder.base_file_url(f"https://{apibase}/file/bot")"""
|
| 13 |
-
|
| 14 |
-
if needle in text and "TELEGRAM_BOT_APIBASE" not in text:
|
| 15 |
-
text = text.replace(needle, patch, 1)
|
| 16 |
-
p.write_text(text, encoding="utf-8")
|
| 17 |
-
print("Patched TELEGRAM_BOT_APIBASE into claude-code-telegram core.py")
|
| 18 |
-
else:
|
| 19 |
-
print("Patch already applied or needle not found")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
services/25-codeman.sh
DELETED
|
@@ -1,36 +0,0 @@
|
|
| 1 |
-
#!/bin/bash
|
| 2 |
-
# 25-codeman.sh — Codeman AI Coding Mission Control on 127.0.0.1:3000
|
| 3 |
-
mkdir -p /data/script/codeman/data /data/workspace /tmp/logs /tmp/.codeman
|
| 4 |
-
LOG="/tmp/logs/codeman.log"
|
| 5 |
-
|
| 6 |
-
if [ -f /data/.env ]; then
|
| 7 |
-
set -a
|
| 8 |
-
. /data/.env
|
| 9 |
-
set +a
|
| 10 |
-
fi
|
| 11 |
-
|
| 12 |
-
export PATH="/usr/local/bin:/usr/bin:/bin:$PATH"
|
| 13 |
-
export TERM="xterm-256color"
|
| 14 |
-
export CODEMAN_PORT=3000
|
| 15 |
-
export CODEMAN_HOST="127.0.0.1"
|
| 16 |
-
export CODEMAN_PASSWORD="${AUTH_TOKEN:-}"
|
| 17 |
-
export CODEMAN_ALLOWED_HOSTS=".hf.space,amangs-cbot.hf.space,localhost,127.0.0.1"
|
| 18 |
-
export CODEMAN_CONFIG_DIR="/data/script/codeman"
|
| 19 |
-
export CODEMAN_DATA_DIR="/data/script/codeman/data"
|
| 20 |
-
export CODEMAN_CASES_PATH="/data/workspace"
|
| 21 |
-
export TMPDIR="/tmp"
|
| 22 |
-
|
| 23 |
-
# Route all CLI agents (Claude Code, OpenCode, Codex) to local opencode-router
|
| 24 |
-
export ANTHROPIC_BASE_URL="http://127.0.0.1:8787"
|
| 25 |
-
export OPENAI_BASE_URL="http://127.0.0.1:8787/v1"
|
| 26 |
-
export OPENCODE_BASE_URL="http://127.0.0.1:8787/v1"
|
| 27 |
-
export ANTHROPIC_API_KEY="${AUTH_TOKEN:-}"
|
| 28 |
-
export OPENAI_API_KEY="${AUTH_TOKEN:-}"
|
| 29 |
-
|
| 30 |
-
if pgrep -f "codeman.*web" >/dev/null 2>&1; then
|
| 31 |
-
echo "codeman already running"
|
| 32 |
-
exit 0
|
| 33 |
-
fi
|
| 34 |
-
|
| 35 |
-
cd /data/workspace || exit 1
|
| 36 |
-
exec codeman web --host 127.0.0.1 --port 3000 >> "$LOG" 2>&1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
services/30-claude-telegram.sh
DELETED
|
@@ -1,52 +0,0 @@
|
|
| 1 |
-
#!/bin/bash
|
| 2 |
-
# 30-claude-telegram.sh — Run Claude Code Telegram bot (@amangt_bot)
|
| 3 |
-
mkdir -p /tmp/logs /data/workspace /tmp/.cache
|
| 4 |
-
LOG="/tmp/logs/claude-telegram.log"
|
| 5 |
-
|
| 6 |
-
if [ -f /data/.env ]; then
|
| 7 |
-
set -a
|
| 8 |
-
. /data/.env
|
| 9 |
-
set +a
|
| 10 |
-
fi
|
| 11 |
-
|
| 12 |
-
TOKEN="${TELEGRAM_BOT_TOKEN:-}"
|
| 13 |
-
if [ -z "$TOKEN" ]; then
|
| 14 |
-
echo "TELEGRAM_BOT_TOKEN not set, skipping claude-code-telegram"
|
| 15 |
-
exit 0
|
| 16 |
-
fi
|
| 17 |
-
|
| 18 |
-
export TELEGRAM_BOT_TOKEN="$TOKEN"
|
| 19 |
-
export TELEGRAM_BOT_USERNAME="${TELEGRAM_BOT_USERNAME:-amangt_bot}"
|
| 20 |
-
export APPROVED_DIRECTORY="/data/workspace"
|
| 21 |
-
export ALLOWED_USERS="${TELEGRAM_USER:-973904286}"
|
| 22 |
-
export DATABASE_URL="sqlite+aiosqlite:////tmp/claude_bot.db"
|
| 23 |
-
export DISABLE_SECURITY_PATTERNS="true"
|
| 24 |
-
export DISABLE_TOOL_VALIDATION="true"
|
| 25 |
-
export INTERACTIVE_TOOL_APPROVAL="false"
|
| 26 |
-
export CLAUDE_MAX_TURNS="100"
|
| 27 |
-
|
| 28 |
-
# Caching & Temp redirected strictly to /tmp (never S3 /data)
|
| 29 |
-
export TMPDIR="/tmp"
|
| 30 |
-
export PIP_CACHE_DIR="/tmp/.cache/pip"
|
| 31 |
-
export npm_config_cache="/tmp/.cache/npm"
|
| 32 |
-
export GRADLE_USER_HOME="/tmp/.gradle"
|
| 33 |
-
export GOCACHE="/tmp/.cache/go-build"
|
| 34 |
-
export GOPATH="/tmp/go"
|
| 35 |
-
export XDG_CACHE_HOME="/tmp/.cache"
|
| 36 |
-
export HF_HOME="/tmp/.cache/huggingface"
|
| 37 |
-
|
| 38 |
-
# Point Claude Code to local opencode-router if ANTHROPIC_BASE_URL is unset
|
| 39 |
-
export ANTHROPIC_BASE_URL="${ANTHROPIC_BASE_URL:-http://127.0.0.1:8787}"
|
| 40 |
-
export ANTHROPIC_API_KEY="${ANTHROPIC_API_KEY:-${AUTH_TOKEN:-}}"
|
| 41 |
-
|
| 42 |
-
# Environment markers for Claude Code CLI inside Docker
|
| 43 |
-
export IS_SANDBOX="1"
|
| 44 |
-
export CI="1"
|
| 45 |
-
|
| 46 |
-
if pgrep -f "claude-telegram-bot" >/dev/null 2>&1; then
|
| 47 |
-
echo "claude-code-telegram already running"
|
| 48 |
-
exit 0
|
| 49 |
-
fi
|
| 50 |
-
|
| 51 |
-
cd /opt/claude-code-telegram || exit 1
|
| 52 |
-
exec /opt/claude-code-telegram/venv/bin/python3 -m src.main >> "$LOG" 2>&1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
services/30-telegram-agent.sh
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# 30-telegram-agent.sh — Avi Telegram AI Coding Agent (@amangt_bot)
|
| 3 |
+
mkdir -p /tmp/logs /data/workspace
|
| 4 |
+
LOG="/tmp/logs/telegram-agent.log"
|
| 5 |
+
|
| 6 |
+
if [ -f /tmp/.env ]; then
|
| 7 |
+
set -a
|
| 8 |
+
. /tmp/.env
|
| 9 |
+
set +a
|
| 10 |
+
fi
|
| 11 |
+
if [ -f /data/.env ]; then
|
| 12 |
+
set -a
|
| 13 |
+
. /data/.env
|
| 14 |
+
set +a
|
| 15 |
+
fi
|
| 16 |
+
|
| 17 |
+
TOKEN="${TELEGRAM_BOT_TOKEN:-}"
|
| 18 |
+
if [ -z "$TOKEN" ]; then
|
| 19 |
+
echo "TELEGRAM_BOT_TOKEN not set, skipping telegram agent" >> "$LOG"
|
| 20 |
+
exit 0
|
| 21 |
+
fi
|
| 22 |
+
|
| 23 |
+
export TELEGRAM_BOT_TOKEN="$TOKEN"
|
| 24 |
+
export TELEGRAM_BOT_APIBASE="${TELEGRAM_BOT_APIBASE:-tgp.ind.pp.ua}"
|
| 25 |
+
export ALLOWED_USERS="${TELEGRAM_USER:-973904286}"
|
| 26 |
+
export LLM_AMAN_BASE_URL="${LLM_AMAN_BASE_URL:-https://llm.aman.ltd/v1}"
|
| 27 |
+
export LLM_AMAN_API_KEY="${LLM_AMAN_API_KEY:-sk-mSm7P3YFDdeasXvlWgLKCGHsucUtAWJJBnLapDyRVUe6TV8w}"
|
| 28 |
+
export DEFAULT_MODEL="gemini-3.8-flash-high"
|
| 29 |
+
export WORKSPACE="/data/workspace"
|
| 30 |
+
|
| 31 |
+
if pgrep -f "services/telegram_agent.py" >/dev/null 2>&1; then
|
| 32 |
+
echo "telegram_agent already running"
|
| 33 |
+
exit 0
|
| 34 |
+
fi
|
| 35 |
+
|
| 36 |
+
cd /app || cd /data || exit 1
|
| 37 |
+
exec python3 /app/services/telegram_agent.py >> "$LOG" 2>&1
|
services/telegram_agent.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
import json
|
| 4 |
+
import asyncio
|
| 5 |
+
import subprocess
|
| 6 |
+
import httpx
|
| 7 |
+
from telegram import Update
|
| 8 |
+
from telegram.ext import Application, CommandHandler, MessageHandler, filters, ContextTypes
|
| 9 |
+
|
| 10 |
+
BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "")
|
| 11 |
+
API_BASE = os.environ.get("TELEGRAM_BOT_APIBASE", "api.telegram.org")
|
| 12 |
+
ALLOWED_USERS = [int(u.strip()) for u in os.environ.get("ALLOWED_USERS", "973904286").split(",") if u.strip().isdigit()]
|
| 13 |
+
LLM_BASE = os.environ.get("LLM_AMAN_BASE_URL", "https://llm.aman.ltd/v1")
|
| 14 |
+
LLM_KEY = os.environ.get("LLM_AMAN_API_KEY", "")
|
| 15 |
+
DEFAULT_MODEL = os.environ.get("DEFAULT_MODEL", "gemini-3.8-flash-high")
|
| 16 |
+
WORKSPACE = os.environ.get("WORKSPACE", "/data/workspace")
|
| 17 |
+
|
| 18 |
+
if not BOT_TOKEN:
|
| 19 |
+
print("TELEGRAM_BOT_TOKEN not configured. Exiting.")
|
| 20 |
+
sys.exit(0)
|
| 21 |
+
|
| 22 |
+
# In-memory per-user chat history
|
| 23 |
+
HISTORIES = {}
|
| 24 |
+
CURRENT_MODELS = {}
|
| 25 |
+
|
| 26 |
+
TOOLS = [
|
| 27 |
+
{
|
| 28 |
+
"type": "function",
|
| 29 |
+
"function": {
|
| 30 |
+
"name": "bash",
|
| 31 |
+
"description": "Execute a shell command in the persistent /data/workspace directory.",
|
| 32 |
+
"parameters": {
|
| 33 |
+
"type": "object",
|
| 34 |
+
"properties": {
|
| 35 |
+
"command": {"type": "string", "description": "The shell command to run"}
|
| 36 |
+
},
|
| 37 |
+
"required": ["command"]
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
}
|
| 41 |
+
]
|
| 42 |
+
|
| 43 |
+
def run_bash(cmd: str) -> str:
|
| 44 |
+
try:
|
| 45 |
+
res = subprocess.run(cmd, shell=True, cwd=WORKSPACE, capture_output=True, text=True, timeout=120)
|
| 46 |
+
out = (res.stdout + res.stderr).strip()
|
| 47 |
+
return out if out else "(command produced no output, exit 0)"
|
| 48 |
+
except subprocess.TimeoutExpired:
|
| 49 |
+
return "Error: Command timed out after 120 seconds."
|
| 50 |
+
except Exception as e:
|
| 51 |
+
return f"Error executing command: {e}"
|
| 52 |
+
|
| 53 |
+
async def call_llm(user_id: int, user_msg: str) -> str:
|
| 54 |
+
model = CURRENT_MODELS.get(user_id, DEFAULT_MODEL)
|
| 55 |
+
history = HISTORIES.setdefault(user_id, [
|
| 56 |
+
{"role": "system", "content": f"You are Avi, an expert software engineer and reverse-engineering assistant on a dedicated Linux development server. You have full terminal access via the bash tool. Working directory is {WORKSPACE}. Answer concisely and accurately. Use your tools to inspect, build, and debug."}
|
| 57 |
+
])
|
| 58 |
+
history.append({"role": "user", "content": user_msg})
|
| 59 |
+
|
| 60 |
+
# Cap history to last 16 messages
|
| 61 |
+
if len(history) > 17:
|
| 62 |
+
history = [history[0]] + history[-16:]
|
| 63 |
+
HISTORIES[user_id] = history
|
| 64 |
+
|
| 65 |
+
headers = {"Authorization": f"Bearer {LLM_KEY}", "Content-Type": "application/json"}
|
| 66 |
+
payload = {
|
| 67 |
+
"model": model,
|
| 68 |
+
"messages": history,
|
| 69 |
+
"tools": TOOLS,
|
| 70 |
+
"temperature": 0.2
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
async with httpx.AsyncClient(timeout=60.0) as client:
|
| 74 |
+
for _ in range(5): # Tool call loop up to 5 turns
|
| 75 |
+
try:
|
| 76 |
+
resp = await client.post(f"{LLM_BASE}/chat/completions", headers=headers, json=payload)
|
| 77 |
+
if resp.status_code != 200:
|
| 78 |
+
return f"API Error {resp.status_code}: {resp.text[:300]}"
|
| 79 |
+
data = resp.json()
|
| 80 |
+
msg = data["choices"][0]["message"]
|
| 81 |
+
except Exception as e:
|
| 82 |
+
return f"LLM error: {e}"
|
| 83 |
+
|
| 84 |
+
tool_calls = msg.get("tool_calls")
|
| 85 |
+
if not tool_calls:
|
| 86 |
+
reply = msg.get("content", "")
|
| 87 |
+
history.append({"role": "assistant", "content": reply})
|
| 88 |
+
return reply
|
| 89 |
+
|
| 90 |
+
# Handle tool calls
|
| 91 |
+
history.append(msg)
|
| 92 |
+
for tc in tool_calls:
|
| 93 |
+
fn_name = tc["function"]["name"]
|
| 94 |
+
try:
|
| 95 |
+
fn_args = json.loads(tc["function"]["arguments"])
|
| 96 |
+
except Exception:
|
| 97 |
+
fn_args = {}
|
| 98 |
+
if fn_name == "bash":
|
| 99 |
+
cmd = fn_args.get("command", "")
|
| 100 |
+
output = run_bash(cmd)
|
| 101 |
+
else:
|
| 102 |
+
output = f"Unknown tool {fn_name}"
|
| 103 |
+
|
| 104 |
+
history.append({
|
| 105 |
+
"role": "tool",
|
| 106 |
+
"tool_call_id": tc["id"],
|
| 107 |
+
"content": output[:8000]
|
| 108 |
+
})
|
| 109 |
+
|
| 110 |
+
return "Reached maximum tool execution turns."
|
| 111 |
+
|
| 112 |
+
async def handle_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
| 113 |
+
user_id = update.effective_user.id
|
| 114 |
+
if ALLOWED_USERS and user_id not in ALLOWED_USERS:
|
| 115 |
+
return
|
| 116 |
+
model = CURRENT_MODELS.get(user_id, DEFAULT_MODEL)
|
| 117 |
+
await update.message.reply_text(
|
| 118 |
+
f"Avi online.\nModel: {model} (via {LLM_BASE})\nWorkspace: {WORKSPACE}\nSend a message to start coding or running commands."
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
+
async def handle_model(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
| 122 |
+
user_id = update.effective_user.id
|
| 123 |
+
if ALLOWED_USERS and user_id not in ALLOWED_USERS:
|
| 124 |
+
return
|
| 125 |
+
args = context.args
|
| 126 |
+
if not args:
|
| 127 |
+
cur = CURRENT_MODELS.get(user_id, DEFAULT_MODEL)
|
| 128 |
+
await update.message.reply_text(f"Current model: {cur}\nUsage: /model <name>\nPopular: gemini-3.8-flash-high, deepseek-v4-flash, Codestral, Devstral 2, gpt-oss:120b")
|
| 129 |
+
return
|
| 130 |
+
new_model = args[0]
|
| 131 |
+
CURRENT_MODELS[user_id] = new_model
|
| 132 |
+
await update.message.reply_text(f"Model switched to: {new_model}")
|
| 133 |
+
|
| 134 |
+
async def handle_clear(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
| 135 |
+
user_id = update.effective_user.id
|
| 136 |
+
HISTORIES[user_id] = []
|
| 137 |
+
await update.message.reply_text("Conversation history cleared.")
|
| 138 |
+
|
| 139 |
+
async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
| 140 |
+
user_id = update.effective_user.id
|
| 141 |
+
if ALLOWED_USERS and user_id not in ALLOWED_USERS:
|
| 142 |
+
return
|
| 143 |
+
text = update.message.text
|
| 144 |
+
if not text:
|
| 145 |
+
return
|
| 146 |
+
await context.bot.send_chat_action(chat_id=update.effective_chat.id, action="typing")
|
| 147 |
+
reply = await call_llm(user_id, text)
|
| 148 |
+
# Split long replies to Telegram 4096 limit
|
| 149 |
+
for chunk in [reply[i:i+4000] for i in range(0, len(reply), 4000)]:
|
| 150 |
+
await update.message.reply_text(chunk)
|
| 151 |
+
|
| 152 |
+
def main():
|
| 153 |
+
builder = Application.builder()
|
| 154 |
+
builder.token(BOT_TOKEN)
|
| 155 |
+
if API_BASE and API_BASE != "api.telegram.org":
|
| 156 |
+
builder.base_url(f"https://{API_BASE}/bot")
|
| 157 |
+
builder.base_file_url(f"https://{API_BASE}/file/bot")
|
| 158 |
+
|
| 159 |
+
app = builder.build()
|
| 160 |
+
app.add_handler(CommandHandler("start", handle_start))
|
| 161 |
+
app.add_handler(CommandHandler("model", handle_model))
|
| 162 |
+
app.add_handler(CommandHandler("clear", handle_clear))
|
| 163 |
+
app.add_handler(CommandHandler("reset", handle_clear))
|
| 164 |
+
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message))
|
| 165 |
+
|
| 166 |
+
print(f"Starting Avi Telegram agent using {API_BASE}...")
|
| 167 |
+
app.run_polling()
|
| 168 |
+
|
| 169 |
+
if __name__ == "__main__":
|
| 170 |
+
main()
|