claude-telegram-bot 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- claude_telegram_bot-0.1.0/.claude/settings.local.json +7 -0
- claude_telegram_bot-0.1.0/.env.example +7 -0
- claude_telegram_bot-0.1.0/.gitignore +9 -0
- claude_telegram_bot-0.1.0/PKG-INFO +75 -0
- claude_telegram_bot-0.1.0/README.md +52 -0
- claude_telegram_bot-0.1.0/config.example.toml +16 -0
- claude_telegram_bot-0.1.0/pyproject.toml +44 -0
- claude_telegram_bot-0.1.0/src/claude_telegram_bot/__init__.py +0 -0
- claude_telegram_bot-0.1.0/src/claude_telegram_bot/__main__.py +3 -0
- claude_telegram_bot-0.1.0/src/claude_telegram_bot/auth.py +49 -0
- claude_telegram_bot-0.1.0/src/claude_telegram_bot/bot.py +425 -0
- claude_telegram_bot-0.1.0/src/claude_telegram_bot/claude_runner.py +100 -0
- claude_telegram_bot-0.1.0/src/claude_telegram_bot/config.py +188 -0
- claude_telegram_bot-0.1.0/src/claude_telegram_bot/project_manager.py +82 -0
- claude_telegram_bot-0.1.0/src/claude_telegram_bot/session_manager.py +180 -0
- claude_telegram_bot-0.1.0/uv.lock +228 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: claude-telegram-bot
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Telegram bot for interacting with Claude Code CLI
|
|
5
|
+
Project-URL: Homepage, https://github.com/littzhch/claude-telegram-bot
|
|
6
|
+
Project-URL: Repository, https://github.com/littzhch/claude-telegram-bot
|
|
7
|
+
Author-email: littzhch <2371050115@qq.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: ai,anthropic,bot,claude,telegram
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Requires-Dist: python-dotenv>=1.0
|
|
20
|
+
Requires-Dist: python-telegram-bot>=21.0
|
|
21
|
+
Requires-Dist: tomli>=2.0; python_version < '3.11'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# Claude Telegram Bot
|
|
25
|
+
|
|
26
|
+
Telegram bot for interacting with Claude Code CLI
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install claude-telegram-bot
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Configuration
|
|
35
|
+
|
|
36
|
+
Configuration priority: CLI > ENV > config file > defaults
|
|
37
|
+
|
|
38
|
+
### Config File
|
|
39
|
+
|
|
40
|
+
Create `~/.config/claude-tg-bot.toml`:
|
|
41
|
+
|
|
42
|
+
```toml
|
|
43
|
+
[telegram]
|
|
44
|
+
bot_token = "your-telegram-bot-token"
|
|
45
|
+
admin_user_id = 123456789
|
|
46
|
+
allowed_user_ids = [123456789]
|
|
47
|
+
|
|
48
|
+
[claude]
|
|
49
|
+
path = "/home/zhangchi/.local/bin/claude"
|
|
50
|
+
timeout = 120
|
|
51
|
+
|
|
52
|
+
[data]
|
|
53
|
+
dir = "~/.cache/claude-tg-bot"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Environment Variables
|
|
57
|
+
|
|
58
|
+
- `BOT_TOKEN`: Telegram bot token
|
|
59
|
+
- `ADMIN_USER_ID`: Admin user ID
|
|
60
|
+
- `ALLOWED_USER_IDS`: Comma-separated user IDs
|
|
61
|
+
- `CLAUDE_PATH`: Path to Claude CLI
|
|
62
|
+
- `CLAUDE_TIMEOUT`: Command timeout in seconds
|
|
63
|
+
- `DATA_DIR`: Data directory
|
|
64
|
+
|
|
65
|
+
### CLI Arguments
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
claude-tg-bot --help
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Usage
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
claude-tg-bot --bot-token YOUR_TOKEN
|
|
75
|
+
```
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Claude Telegram Bot
|
|
2
|
+
|
|
3
|
+
Telegram bot for interacting with Claude Code CLI
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install claude-telegram-bot
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Configuration
|
|
12
|
+
|
|
13
|
+
Configuration priority: CLI > ENV > config file > defaults
|
|
14
|
+
|
|
15
|
+
### Config File
|
|
16
|
+
|
|
17
|
+
Create `~/.config/claude-tg-bot.toml`:
|
|
18
|
+
|
|
19
|
+
```toml
|
|
20
|
+
[telegram]
|
|
21
|
+
bot_token = "your-telegram-bot-token"
|
|
22
|
+
admin_user_id = 123456789
|
|
23
|
+
allowed_user_ids = [123456789]
|
|
24
|
+
|
|
25
|
+
[claude]
|
|
26
|
+
path = "/home/zhangchi/.local/bin/claude"
|
|
27
|
+
timeout = 120
|
|
28
|
+
|
|
29
|
+
[data]
|
|
30
|
+
dir = "~/.cache/claude-tg-bot"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Environment Variables
|
|
34
|
+
|
|
35
|
+
- `BOT_TOKEN`: Telegram bot token
|
|
36
|
+
- `ADMIN_USER_ID`: Admin user ID
|
|
37
|
+
- `ALLOWED_USER_IDS`: Comma-separated user IDs
|
|
38
|
+
- `CLAUDE_PATH`: Path to Claude CLI
|
|
39
|
+
- `CLAUDE_TIMEOUT`: Command timeout in seconds
|
|
40
|
+
- `DATA_DIR`: Data directory
|
|
41
|
+
|
|
42
|
+
### CLI Arguments
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
claude-tg-bot --help
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
claude-tg-bot --bot-token YOUR_TOKEN
|
|
52
|
+
```
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Claude Telegram Bot Configuration
|
|
2
|
+
# Save this to ~/.config/claude-tg-bot.toml
|
|
3
|
+
|
|
4
|
+
[telegram]
|
|
5
|
+
bot_token = "your-telegram-bot-token"
|
|
6
|
+
admin_user_id = 123456789
|
|
7
|
+
allowed_user_ids = [123456789, 987654321]
|
|
8
|
+
|
|
9
|
+
[claude]
|
|
10
|
+
path = "/home/zhangchi/.local/bin/claude"
|
|
11
|
+
timeout = 120
|
|
12
|
+
max_history_messages = 40
|
|
13
|
+
|
|
14
|
+
[data]
|
|
15
|
+
# Directory for storing bot data (default: ~/.cache/claude-tg-bot/)
|
|
16
|
+
# dir = "/custom/path/to/data"
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "claude-telegram-bot"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Telegram bot for interacting with Claude Code CLI"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.9"
|
|
7
|
+
license = {text = "MIT"}
|
|
8
|
+
authors = [
|
|
9
|
+
{name = "littzhch", email = "2371050115@qq.com"}
|
|
10
|
+
]
|
|
11
|
+
keywords = ["telegram", "bot", "claude", "ai", "anthropic"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 3 - Alpha",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.9",
|
|
18
|
+
"Programming Language :: Python :: 3.10",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
dependencies = [
|
|
24
|
+
"python-telegram-bot>=21.0",
|
|
25
|
+
"python-dotenv>=1.0",
|
|
26
|
+
"tomli>=2.0; python_version < '3.11'",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.scripts]
|
|
30
|
+
claude-tg-bot = "claude_telegram_bot.bot:main"
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/littzhch/claude-telegram-bot"
|
|
34
|
+
Repository = "https://github.com/littzhch/claude-telegram-bot"
|
|
35
|
+
|
|
36
|
+
[tool.uv]
|
|
37
|
+
package = true
|
|
38
|
+
|
|
39
|
+
[build-system]
|
|
40
|
+
requires = ["hatchling"]
|
|
41
|
+
build-backend = "hatchling.build"
|
|
42
|
+
|
|
43
|
+
[tool.hatch.build.targets.wheel]
|
|
44
|
+
packages = ["src/claude_telegram_bot"]
|
|
File without changes
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from functools import wraps
|
|
3
|
+
from telegram import Update
|
|
4
|
+
from telegram.ext import ContextTypes
|
|
5
|
+
|
|
6
|
+
from claude_telegram_bot import config
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _load_allowed_ids() -> set[int]:
|
|
12
|
+
"""Reload allowed user IDs from config (supports runtime admin changes)."""
|
|
13
|
+
return set(config.ALLOWED_USER_IDS)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def is_allowed(user_id: int) -> bool:
|
|
17
|
+
allowed = _load_allowed_ids()
|
|
18
|
+
return user_id in allowed
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def is_admin(user_id: int) -> bool:
|
|
22
|
+
return user_id == config.ADMIN_USER_ID
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def require_auth(func):
|
|
26
|
+
"""Decorator: reject users not in the allowed list."""
|
|
27
|
+
@wraps(func)
|
|
28
|
+
async def wrapper(update: Update, context: ContextTypes.DEFAULT_TYPE, *args, **kwargs):
|
|
29
|
+
user_id = update.effective_user.id
|
|
30
|
+
if not is_allowed(user_id):
|
|
31
|
+
logger.warning("Unauthorized access from user %s", user_id)
|
|
32
|
+
if update.message:
|
|
33
|
+
await update.message.reply_text("You are not authorized to use this bot.")
|
|
34
|
+
return
|
|
35
|
+
return await func(update, context, *args, **kwargs)
|
|
36
|
+
return wrapper
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def require_admin(func):
|
|
40
|
+
"""Decorator: reject non-admin users."""
|
|
41
|
+
@wraps(func)
|
|
42
|
+
async def wrapper(update: Update, context: ContextTypes.DEFAULT_TYPE, *args, **kwargs):
|
|
43
|
+
user_id = update.effective_user.id
|
|
44
|
+
if not is_admin(user_id):
|
|
45
|
+
if update.message:
|
|
46
|
+
await update.message.reply_text("Admin only.")
|
|
47
|
+
return
|
|
48
|
+
return await func(update, context, *args, **kwargs)
|
|
49
|
+
return wrapper
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Telegram Bot for Claude Code CLI."""
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import tempfile
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from telegram import Update, BotCommand
|
|
10
|
+
from telegram.ext import (
|
|
11
|
+
Application,
|
|
12
|
+
CommandHandler,
|
|
13
|
+
MessageHandler,
|
|
14
|
+
filters,
|
|
15
|
+
ContextTypes,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from claude_telegram_bot import config
|
|
19
|
+
from claude_telegram_bot.auth import require_auth, is_admin
|
|
20
|
+
from claude_telegram_bot.claude_runner import run_claude
|
|
21
|
+
from claude_telegram_bot.session_manager import SessionManager, init_db as init_session_db
|
|
22
|
+
from claude_telegram_bot.project_manager import (
|
|
23
|
+
init_db as init_project_db,
|
|
24
|
+
add_project,
|
|
25
|
+
remove_project,
|
|
26
|
+
list_projects,
|
|
27
|
+
get_project_path,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
logging.basicConfig(
|
|
31
|
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
32
|
+
level=logging.INFO,
|
|
33
|
+
)
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
# In-memory session managers per user
|
|
37
|
+
_user_sessions: dict[int, SessionManager] = {}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _get_session(user_id: int) -> SessionManager:
|
|
41
|
+
if user_id not in _user_sessions:
|
|
42
|
+
_user_sessions[user_id] = SessionManager(user_id)
|
|
43
|
+
return _user_sessions[user_id]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ── Commands ──
|
|
47
|
+
|
|
48
|
+
@require_auth
|
|
49
|
+
async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
50
|
+
user = update.effective_user
|
|
51
|
+
await update.message.reply_text(
|
|
52
|
+
f"Hello {user.first_name}! I'm your Claude Code bot.\n\n"
|
|
53
|
+
"Send me any message and I'll run it through Claude Code CLI.\n\n"
|
|
54
|
+
"Key commands:\n"
|
|
55
|
+
"/new - Start a new conversation\n"
|
|
56
|
+
"/sessions - List all sessions\n"
|
|
57
|
+
"/history - View conversation history\n"
|
|
58
|
+
"/projects - List configured projects\n"
|
|
59
|
+
"/add_project <name> <path> - Add a project\n"
|
|
60
|
+
"/use <project> - Switch to a project\n"
|
|
61
|
+
"/status - Current status\n"
|
|
62
|
+
"/help - Show help"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@require_auth
|
|
67
|
+
async def cmd_help(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
68
|
+
text = (
|
|
69
|
+
"*Claude Code Telegram Bot*\n\n"
|
|
70
|
+
"*Conversation*\n"
|
|
71
|
+
"/new - Start new conversation\n"
|
|
72
|
+
"/sessions - List all sessions\n"
|
|
73
|
+
"/switch <id> - Switch session\n"
|
|
74
|
+
"/history - Show recent history\n\n"
|
|
75
|
+
"*Projects*\n"
|
|
76
|
+
"/projects - List projects\n"
|
|
77
|
+
"/add_project <name> <path> - Add project\n"
|
|
78
|
+
"/remove_project <name> - Remove project\n"
|
|
79
|
+
"/use <name> - Switch active project\n\n"
|
|
80
|
+
"*Status*\n"
|
|
81
|
+
"/status - Current status\n"
|
|
82
|
+
"/help - This message\n\n"
|
|
83
|
+
"*Files*\n"
|
|
84
|
+
"Send a document to attach it to the conversation.\n"
|
|
85
|
+
"The file path will be included in the prompt to Claude."
|
|
86
|
+
)
|
|
87
|
+
if is_admin(update.effective_user.id):
|
|
88
|
+
text += (
|
|
89
|
+
"\n\n*Admin*\n"
|
|
90
|
+
"/admin add <user_id> - Add user\n"
|
|
91
|
+
"/admin remove <user_id> - Remove user\n"
|
|
92
|
+
"/admin list - List allowed users"
|
|
93
|
+
)
|
|
94
|
+
await update.message.reply_text(text, parse_mode="Markdown")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@require_auth
|
|
98
|
+
async def cmd_new(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
99
|
+
sm = _get_session(update.effective_user.id)
|
|
100
|
+
sm.new_session()
|
|
101
|
+
await update.message.reply_text("New conversation started.")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@require_auth
|
|
105
|
+
async def cmd_sessions(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
106
|
+
sm = _get_session(update.effective_user.id)
|
|
107
|
+
sessions = sm.list_sessions()
|
|
108
|
+
if not sessions:
|
|
109
|
+
await update.message.reply_text("No sessions yet. Send a message to start one.")
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
lines = []
|
|
113
|
+
active_id = sm.active_session_id
|
|
114
|
+
for s in sessions:
|
|
115
|
+
marker = " *" if s["id"] == active_id else ""
|
|
116
|
+
lines.append(f"{s['id']}. {s['name']} (updated: {s['updated_at'][:16]}){marker}")
|
|
117
|
+
await update.message.reply_text(
|
|
118
|
+
"Sessions (active marked with *):\n" + "\n".join(lines)
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@require_auth
|
|
123
|
+
async def cmd_switch(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
124
|
+
if not context.args:
|
|
125
|
+
await update.message.reply_text("Usage: /switch <session_id>")
|
|
126
|
+
return
|
|
127
|
+
try:
|
|
128
|
+
sid = int(context.args[0])
|
|
129
|
+
except ValueError:
|
|
130
|
+
await update.message.reply_text("Invalid session ID.")
|
|
131
|
+
return
|
|
132
|
+
sm = _get_session(update.effective_user.id)
|
|
133
|
+
if sm.switch_session(sid):
|
|
134
|
+
await update.message.reply_text(f"Switched to session {sid}.")
|
|
135
|
+
else:
|
|
136
|
+
await update.message.reply_text(f"Session {sid} not found.")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@require_auth
|
|
140
|
+
async def cmd_history(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
141
|
+
sm = _get_session(update.effective_user.id)
|
|
142
|
+
history = sm.get_history(limit=20)
|
|
143
|
+
if not history:
|
|
144
|
+
await update.message.reply_text("No history in current session.")
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
lines = []
|
|
148
|
+
for msg in history:
|
|
149
|
+
role = "You" if msg["role"] == "user" else "Claude"
|
|
150
|
+
content = msg["content"][:200]
|
|
151
|
+
if len(msg["content"]) > 200:
|
|
152
|
+
content += "..."
|
|
153
|
+
lines.append(f"*{role}:* {content}")
|
|
154
|
+
|
|
155
|
+
text = "\n\n".join(lines)
|
|
156
|
+
# Telegram message limit
|
|
157
|
+
if len(text) > 4000:
|
|
158
|
+
text = text[:4000] + "\n... (truncated)"
|
|
159
|
+
await update.message.reply_text(text, parse_mode="Markdown")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@require_auth
|
|
163
|
+
async def cmd_projects(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
164
|
+
user_id = update.effective_user.id
|
|
165
|
+
projects = list_projects(user_id)
|
|
166
|
+
if not projects:
|
|
167
|
+
await update.message.reply_text(
|
|
168
|
+
"No projects configured. Use /add_project <name> <path> to add one."
|
|
169
|
+
)
|
|
170
|
+
return
|
|
171
|
+
|
|
172
|
+
sm = _get_session(user_id)
|
|
173
|
+
active_cwd = sm.get_active_cwd()
|
|
174
|
+
lines = []
|
|
175
|
+
for p in projects:
|
|
176
|
+
active = " *" if p["path"] == active_cwd else ""
|
|
177
|
+
lines.append(f"• {p['name']}: {p['path']}{active}")
|
|
178
|
+
await update.message.reply_text(
|
|
179
|
+
"Projects (active marked with *):\n" + "\n".join(lines)
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@require_auth
|
|
184
|
+
async def cmd_add_project(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
185
|
+
if len(context.args) < 2:
|
|
186
|
+
await update.message.reply_text("Usage: /add_project <name> <path>")
|
|
187
|
+
return
|
|
188
|
+
name = context.args[0]
|
|
189
|
+
path = " ".join(context.args[1:])
|
|
190
|
+
user_id = update.effective_user.id
|
|
191
|
+
ok, msg = add_project(user_id, name, path)
|
|
192
|
+
await update.message.reply_text(msg)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@require_auth
|
|
196
|
+
async def cmd_remove_project(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
197
|
+
if not context.args:
|
|
198
|
+
await update.message.reply_text("Usage: /remove_project <name>")
|
|
199
|
+
return
|
|
200
|
+
name = context.args[0]
|
|
201
|
+
user_id = update.effective_user.id
|
|
202
|
+
ok, msg = remove_project(user_id, name)
|
|
203
|
+
await update.message.reply_text(msg)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@require_auth
|
|
207
|
+
async def cmd_use(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
208
|
+
if not context.args:
|
|
209
|
+
await update.message.reply_text("Usage: /use <project_name>")
|
|
210
|
+
return
|
|
211
|
+
name = context.args[0]
|
|
212
|
+
user_id = update.effective_user.id
|
|
213
|
+
path = get_project_path(user_id, name)
|
|
214
|
+
if path is None:
|
|
215
|
+
await update.message.reply_text(f"Project '{name}' not found. Use /projects to list.")
|
|
216
|
+
return
|
|
217
|
+
sm = _get_session(user_id)
|
|
218
|
+
sm.set_cwd(path)
|
|
219
|
+
await update.message.reply_text(f"Switched to project '{name}' ({path}).")
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@require_auth
|
|
223
|
+
async def cmd_status(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
224
|
+
user_id = update.effective_user.id
|
|
225
|
+
sm = _get_session(user_id)
|
|
226
|
+
active_cwd = sm.get_active_cwd() or "(default: home)"
|
|
227
|
+
sessions = sm.list_sessions()
|
|
228
|
+
active_sid = sm.active_session_id
|
|
229
|
+
projects = list_projects(user_id)
|
|
230
|
+
|
|
231
|
+
text = (
|
|
232
|
+
f"*Status*\n"
|
|
233
|
+
f"User ID: {user_id}\n"
|
|
234
|
+
f"Admin: {'Yes' if is_admin(user_id) else 'No'}\n"
|
|
235
|
+
f"Active session: {active_sid or 'None'}\n"
|
|
236
|
+
f"Working directory: {active_cwd}\n"
|
|
237
|
+
f"Sessions: {len(sessions)}\n"
|
|
238
|
+
f"Projects: {len(projects)}"
|
|
239
|
+
)
|
|
240
|
+
await update.message.reply_text(text, parse_mode="Markdown")
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# ── Admin ──
|
|
244
|
+
|
|
245
|
+
@require_auth
|
|
246
|
+
async def cmd_admin(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
247
|
+
user_id = update.effective_user.id
|
|
248
|
+
if not is_admin(user_id):
|
|
249
|
+
await update.message.reply_text("Admin only.")
|
|
250
|
+
return
|
|
251
|
+
|
|
252
|
+
if not context.args:
|
|
253
|
+
await update.message.reply_text(
|
|
254
|
+
"Usage:\n/admin add <user_id>\n/admin remove <user_id>\n/admin list"
|
|
255
|
+
)
|
|
256
|
+
return
|
|
257
|
+
|
|
258
|
+
action = context.args[0].lower()
|
|
259
|
+
|
|
260
|
+
if action == "list":
|
|
261
|
+
ids = ", ".join(str(uid) for uid in config.ALLOWED_USER_IDS)
|
|
262
|
+
await update.message.reply_text(f"Allowed user IDs:\n{ids}")
|
|
263
|
+
return
|
|
264
|
+
|
|
265
|
+
if action == "add" and len(context.args) >= 2:
|
|
266
|
+
try:
|
|
267
|
+
new_id = int(context.args[1])
|
|
268
|
+
except ValueError:
|
|
269
|
+
await update.message.reply_text("Invalid user ID.")
|
|
270
|
+
return
|
|
271
|
+
if new_id not in config.ALLOWED_USER_IDS:
|
|
272
|
+
config.ALLOWED_USER_IDS.append(new_id)
|
|
273
|
+
await update.message.reply_text(f"Added user {new_id}.")
|
|
274
|
+
return
|
|
275
|
+
|
|
276
|
+
if action == "remove" and len(context.args) >= 2:
|
|
277
|
+
try:
|
|
278
|
+
rem_id = int(context.args[1])
|
|
279
|
+
except ValueError:
|
|
280
|
+
await update.message.reply_text("Invalid user ID.")
|
|
281
|
+
return
|
|
282
|
+
if rem_id in config.ALLOWED_USER_IDS:
|
|
283
|
+
config.ALLOWED_USER_IDS.remove(rem_id)
|
|
284
|
+
await update.message.reply_text(f"Removed user {rem_id}.")
|
|
285
|
+
return
|
|
286
|
+
|
|
287
|
+
await update.message.reply_text("Unknown admin command.")
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
# ── File upload ──
|
|
291
|
+
|
|
292
|
+
@require_auth
|
|
293
|
+
async def handle_document(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
294
|
+
"""Handle uploaded documents - download and save to session's temp dir."""
|
|
295
|
+
doc = update.message.document
|
|
296
|
+
if not doc:
|
|
297
|
+
return
|
|
298
|
+
|
|
299
|
+
user_id = update.effective_user.id
|
|
300
|
+
sm = _get_session(user_id)
|
|
301
|
+
cwd = sm.get_active_cwd() or str(Path.home())
|
|
302
|
+
|
|
303
|
+
file = await context.bot.get_file(doc.file_id)
|
|
304
|
+
dest = os.path.join(cwd, doc.file_name or "uploaded_file")
|
|
305
|
+
await file.download_to_drive(dest)
|
|
306
|
+
|
|
307
|
+
sm.add_message("user", f"[Uploaded file: {dest}]")
|
|
308
|
+
await update.message.reply_text(f"File saved to: {dest}")
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
# ── Message handler ──
|
|
312
|
+
|
|
313
|
+
@require_auth
|
|
314
|
+
async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
315
|
+
"""Handle regular text messages - forward to Claude Code CLI."""
|
|
316
|
+
text = update.message.text
|
|
317
|
+
if not text:
|
|
318
|
+
return
|
|
319
|
+
|
|
320
|
+
user_id = update.effective_user.id
|
|
321
|
+
sm = _get_session(user_id)
|
|
322
|
+
|
|
323
|
+
# Ensure we have a session
|
|
324
|
+
if sm.active_session_id is None:
|
|
325
|
+
sm.new_session()
|
|
326
|
+
|
|
327
|
+
cwd = sm.get_active_cwd() or str(Path.home())
|
|
328
|
+
|
|
329
|
+
# Build prompt with history
|
|
330
|
+
prompt = sm.build_prompt(text)
|
|
331
|
+
|
|
332
|
+
# Save user message
|
|
333
|
+
sm.add_message("user", text)
|
|
334
|
+
|
|
335
|
+
# Send "thinking" indicator
|
|
336
|
+
thinking_msg = await update.message.reply_text("Claude is thinking...")
|
|
337
|
+
|
|
338
|
+
try:
|
|
339
|
+
output = await run_claude(prompt, cwd=cwd, continue_session=False)
|
|
340
|
+
except Exception as e:
|
|
341
|
+
output = f"Error: {e}"
|
|
342
|
+
|
|
343
|
+
# Save assistant response
|
|
344
|
+
sm.add_message("assistant", output)
|
|
345
|
+
|
|
346
|
+
# Send response (split if too long)
|
|
347
|
+
await thinking_msg.delete()
|
|
348
|
+
if len(output) <= 4000:
|
|
349
|
+
await update.message.reply_text(output)
|
|
350
|
+
else:
|
|
351
|
+
# Split into chunks
|
|
352
|
+
chunks = []
|
|
353
|
+
while output:
|
|
354
|
+
chunk = output[:4000]
|
|
355
|
+
# Try to split at a newline boundary
|
|
356
|
+
last_nl = chunk.rfind("\n")
|
|
357
|
+
if last_nl > 2000:
|
|
358
|
+
chunk = output[:last_nl]
|
|
359
|
+
output = output[last_nl + 1:]
|
|
360
|
+
else:
|
|
361
|
+
output = output[4000:]
|
|
362
|
+
chunks.append(chunk)
|
|
363
|
+
for chunk in chunks:
|
|
364
|
+
await update.message.reply_text(chunk)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
# ── Main ──
|
|
368
|
+
|
|
369
|
+
async def post_init(app: Application):
|
|
370
|
+
"""Set bot commands for the Telegram UI."""
|
|
371
|
+
commands = [
|
|
372
|
+
BotCommand("start", "Start using the bot"),
|
|
373
|
+
BotCommand("help", "Show help"),
|
|
374
|
+
BotCommand("new", "New conversation"),
|
|
375
|
+
BotCommand("sessions", "List sessions"),
|
|
376
|
+
BotCommand("switch", "Switch session"),
|
|
377
|
+
BotCommand("history", "Show history"),
|
|
378
|
+
BotCommand("projects", "List projects"),
|
|
379
|
+
BotCommand("add_project", "Add project"),
|
|
380
|
+
BotCommand("remove_project", "Remove project"),
|
|
381
|
+
BotCommand("use", "Switch project"),
|
|
382
|
+
BotCommand("status", "Current status"),
|
|
383
|
+
]
|
|
384
|
+
await app.bot.set_my_commands(commands)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def main():
|
|
388
|
+
if not config.BOT_TOKEN:
|
|
389
|
+
print("Error: BOT_TOKEN not set. Copy .env.example to .env and configure it.")
|
|
390
|
+
return
|
|
391
|
+
if not config.ALLOWED_USER_IDS:
|
|
392
|
+
print("Warning: ALLOWED_USER_IDS is empty. Only admin can use the bot.")
|
|
393
|
+
|
|
394
|
+
# Init databases
|
|
395
|
+
init_session_db()
|
|
396
|
+
init_project_db()
|
|
397
|
+
|
|
398
|
+
app = Application.builder().token(config.BOT_TOKEN).post_init(post_init).build()
|
|
399
|
+
|
|
400
|
+
# Register handlers
|
|
401
|
+
app.add_handler(CommandHandler("start", cmd_start))
|
|
402
|
+
app.add_handler(CommandHandler("help", cmd_help))
|
|
403
|
+
app.add_handler(CommandHandler("new", cmd_new))
|
|
404
|
+
app.add_handler(CommandHandler("sessions", cmd_sessions))
|
|
405
|
+
app.add_handler(CommandHandler("switch", cmd_switch))
|
|
406
|
+
app.add_handler(CommandHandler("history", cmd_history))
|
|
407
|
+
app.add_handler(CommandHandler("projects", cmd_projects))
|
|
408
|
+
app.add_handler(CommandHandler("add_project", cmd_add_project))
|
|
409
|
+
app.add_handler(CommandHandler("remove_project", cmd_remove_project))
|
|
410
|
+
app.add_handler(CommandHandler("use", cmd_use))
|
|
411
|
+
app.add_handler(CommandHandler("status", cmd_status))
|
|
412
|
+
app.add_handler(CommandHandler("admin", cmd_admin))
|
|
413
|
+
|
|
414
|
+
# File uploads
|
|
415
|
+
app.add_handler(MessageHandler(filters.Document.ALL, handle_document))
|
|
416
|
+
|
|
417
|
+
# Text messages
|
|
418
|
+
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message))
|
|
419
|
+
|
|
420
|
+
logger.info("Starting bot...")
|
|
421
|
+
app.run_polling(allowed_updates=Update.ALL_TYPES)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
if __name__ == "__main__":
|
|
425
|
+
main()
|