chore: auto-commit from dashboard

This commit is contained in:
MoltBot Service
2026-02-15 23:23:29 +00:00
parent 8ce7ea3bd6
commit fd9d962ad2
3 changed files with 408 additions and 26 deletions

View File

@@ -15,6 +15,7 @@ from src.claude_session import (
PROJECT_ROOT,
VALID_MODELS,
)
from src.fast_commands import dispatch as fast_dispatch
from src.router import route_message
logger = logging.getLogger("echo-core.discord")
@@ -114,26 +115,47 @@ def create_bot(config: Config) -> discord.Client:
"**Echo Commands**",
"`/ping` — Check bot latency",
"`/help` — Show this help message",
"`/setup` — Claim ownership of the bot (first run only)",
"`/channel add <alias>` — Register current channel (owner only)",
"`/channels` — List registered channels",
"`/admin add <user_id>` — Add an admin (owner only)",
"`/clear` — Clear the session for this channel",
"`/status` — Show session status for this channel",
"`/model` — View current model and available models",
"`/model <choice>` — Change model for this channel's session",
"`/logs [n]` — Show last N log lines (default 10)",
"`/restart` — Restart the bot process (owner only)",
"`/heartbeat` — Run heartbeat health checks",
"`/search <query>` — Search Echo's memory",
"`/status` — Show session status",
"`/model [choice]` — View/change AI model",
"",
"**Cron Jobs**",
"`/cron list` — List all scheduled jobs",
"`/cron run <name>` — Force-run a job now",
"`/cron add <name> <expr> [model]` — Create a scheduled job (admin)",
"`/cron remove <name>` — Remove a job (admin)",
"`/cron enable <name>` — Enable a job (admin)",
"`/cron disable <name>` — Disable a job (admin)",
"**Email**",
"`/email check` — Check unread emails",
"`/email send <to> <subject> <body>` — Send an email",
"`/email save` — Save unread emails to KB",
"",
"**Calendar**",
"`/calendar today` — Today + tomorrow events",
"`/calendar week` — This week's schedule",
"`/calendar busy` — Am I in a meeting?",
"",
"**Notes**",
"`/note <text>` — Quick note in daily file",
"`/jurnal <text>` — Journal entry in daily file",
"`/search <query>` — Search Echo's memory",
"`/kb [category]` — Recent KB notes",
"",
"**Reminders**",
"`/remind <time> <text> [date]` — Create reminder",
"",
"**Git**",
"`/commit [message]` — Commit all changes",
"`/push` — Push to remote",
"`/pull` — Pull with rebase",
"`/test [pattern]` — Run tests",
"",
"**Ops**",
"`/logs [n]` — Last N log lines",
"`/doctor` — System diagnostics",
"`/heartbeat` — Health checks",
"`/restart` — Restart bot (owner)",
"",
"**Admin**",
"`/setup` — Claim ownership",
"`/channel add <alias>` — Register channel",
"`/channels` — List channels",
"`/admin add <user_id>` — Add admin",
"`/cron list|run|add|remove|enable|disable` — Cron jobs",
]
await interaction.response.send_message(
"\n".join(lines), ephemeral=True
@@ -409,6 +431,158 @@ def create_bot(config: Config) -> discord.Client:
tree.add_command(cron_group)
# --- Email commands ---
email_group = app_commands.Group(
name="email", description="Email operations"
)
@email_group.command(name="check", description="Check unread emails")
async def email_check(interaction: discord.Interaction) -> None:
await interaction.response.defer()
result = await asyncio.to_thread(fast_dispatch, "email", [])
await interaction.followup.send(result)
@email_group.command(name="send", description="Send an email")
@app_commands.describe(
to="Recipient email address",
subject="Email subject",
body="Email body text",
)
async def email_send(
interaction: discord.Interaction, to: str, subject: str, body: str
) -> None:
await interaction.response.defer()
result = await asyncio.to_thread(
fast_dispatch, "email", ["send", f"{to} {subject} :: {body}"]
)
await interaction.followup.send(result)
@email_group.command(name="save", description="Save unread emails to knowledge base")
async def email_save(interaction: discord.Interaction) -> None:
await interaction.response.defer()
result = await asyncio.to_thread(fast_dispatch, "email", ["save"])
await interaction.followup.send(result)
tree.add_command(email_group)
# --- Calendar commands ---
calendar_group = app_commands.Group(
name="calendar", description="Calendar operations"
)
@calendar_group.command(name="today", description="Today and tomorrow events")
async def calendar_today(interaction: discord.Interaction) -> None:
await interaction.response.defer()
result = await asyncio.to_thread(fast_dispatch, "calendar", [])
await interaction.followup.send(result)
@calendar_group.command(name="week", description="This week's schedule")
async def calendar_week(interaction: discord.Interaction) -> None:
await interaction.response.defer()
result = await asyncio.to_thread(fast_dispatch, "calendar", ["week"])
await interaction.followup.send(result)
@calendar_group.command(name="busy", description="Am I in a meeting right now?")
async def calendar_busy(interaction: discord.Interaction) -> None:
await interaction.response.defer()
result = await asyncio.to_thread(fast_dispatch, "calendar", ["busy"])
await interaction.followup.send(result)
tree.add_command(calendar_group)
# --- Note & journal commands ---
@tree.command(name="note", description="Add a quick note to today's daily file")
@app_commands.describe(text="Note text")
async def note_cmd(interaction: discord.Interaction, text: str) -> None:
await interaction.response.defer(ephemeral=True)
result = await asyncio.to_thread(fast_dispatch, "note", text.split())
await interaction.followup.send(result, ephemeral=True)
@tree.command(name="jurnal", description="Add a journal entry to today's daily file")
@app_commands.describe(text="Journal entry text")
async def jurnal_cmd(interaction: discord.Interaction, text: str) -> None:
await interaction.response.defer(ephemeral=True)
result = await asyncio.to_thread(fast_dispatch, "jurnal", text.split())
await interaction.followup.send(result, ephemeral=True)
# --- KB command ---
@tree.command(name="kb", description="List recent knowledge base notes")
@app_commands.describe(category="Filter by category (optional)")
async def kb_cmd(
interaction: discord.Interaction, category: str | None = None
) -> None:
await interaction.response.defer()
args = [category] if category else []
result = await asyncio.to_thread(fast_dispatch, "kb", args)
await interaction.followup.send(result)
# --- Remind command ---
@tree.command(name="remind", description="Create a calendar reminder")
@app_commands.describe(
time="Time in HH:MM format",
text="Reminder text",
date="Date in YYYY-MM-DD format (default: today)",
)
async def remind_cmd(
interaction: discord.Interaction, time: str, text: str, date: str | None = None
) -> None:
await interaction.response.defer(ephemeral=True)
args = [date, time, text] if date else [time, text]
result = await asyncio.to_thread(fast_dispatch, "remind", args)
await interaction.followup.send(result, ephemeral=True)
# --- Git commands ---
@tree.command(name="commit", description="Git commit all changes")
@app_commands.describe(message="Commit message (auto-generated if empty)")
async def commit_cmd(
interaction: discord.Interaction, message: str | None = None
) -> None:
await interaction.response.defer()
args = message.split() if message else []
result = await asyncio.to_thread(fast_dispatch, "commit", args)
await interaction.followup.send(result)
@tree.command(name="push", description="Git push to remote")
async def push_cmd(interaction: discord.Interaction) -> None:
await interaction.response.defer()
result = await asyncio.to_thread(fast_dispatch, "push", [])
await interaction.followup.send(result)
@tree.command(name="pull", description="Git pull with rebase")
async def pull_cmd(interaction: discord.Interaction) -> None:
await interaction.response.defer()
result = await asyncio.to_thread(fast_dispatch, "pull", [])
await interaction.followup.send(result)
# --- Test command ---
@tree.command(name="test", description="Run pytest tests")
@app_commands.describe(pattern="Test filter pattern (optional)")
async def test_cmd(
interaction: discord.Interaction, pattern: str | None = None
) -> None:
await interaction.response.defer()
args = [pattern] if pattern else []
result = await asyncio.to_thread(fast_dispatch, "test", args)
# Truncate for Discord limit
if len(result) > 1900:
result = result[-1900:]
await interaction.followup.send(f"```\n{result}\n```")
# --- Doctor command ---
@tree.command(name="doctor", description="System diagnostics")
async def doctor_cmd(interaction: discord.Interaction) -> None:
await interaction.response.defer(ephemeral=True)
result = await asyncio.to_thread(fast_dispatch, "doctor", [])
await interaction.followup.send(result, ephemeral=True)
@tree.command(name="heartbeat", description="Run heartbeat health checks")
async def heartbeat_cmd(interaction: discord.Interaction) -> None:
from src.heartbeat import run_heartbeat

View File

@@ -3,7 +3,7 @@
import asyncio
import logging
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram import BotCommand, Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.constants import ChatAction, ChatType
from telegram.ext import (
Application,
@@ -21,6 +21,7 @@ from src.claude_session import (
set_session_model,
VALID_MODELS,
)
from src.fast_commands import dispatch as fast_dispatch
from src.router import route_message
logger = logging.getLogger("echo-core.telegram")
@@ -106,12 +107,40 @@ async def cmd_help(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Handle /help — list commands."""
lines = [
"*Echo Commands*",
"/start — Welcome message",
"/help — Show this help",
"/clear — Clear the session for this chat",
"/status — Show session status",
"/clear — Clear session",
"/status — Session status",
"/model — View/change AI model",
"/register <alias> — Register this chat (owner only)",
"",
"*Email*",
"/email — Check unread emails",
"/emailsend <to> <subject> :: <body>",
"/emailsave — Save emails to KB",
"",
"*Calendar*",
"/calendar — Today + tomorrow",
"/calendarweek — Week schedule",
"/calendarbusy — Am I busy?",
"",
"*Notes*",
"/note <text> — Quick note",
"/jurnal <text> — Journal entry",
"/search <query> — Memory search",
"/kb [category] — KB notes",
"",
"*Reminders*",
"/remind <HH:MM> <text>",
"",
"*Git*",
"/commit [msg] — Commit changes",
"/push — Push to remote",
"/pull — Pull with rebase",
"/test [pattern] — Run tests",
"",
"*Ops*",
"/logs [N] — Log lines",
"/doctor — Diagnostics",
"/heartbeat — Health checks",
]
await update.message.reply_text("\n".join(lines), parse_mode="Markdown")
@@ -270,6 +299,126 @@ async def cmd_register(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
)
# --- Fast command handlers ---
async def _fast_cmd(update: Update, name: str, args: list[str]) -> None:
"""Run a fast command and reply with the result."""
await update.message.chat.send_action(ChatAction.TYPING)
result = await asyncio.to_thread(fast_dispatch, name, args)
if result:
for chunk in split_message(result):
await update.message.reply_text(chunk)
else:
await update.message.reply_text(f"Unknown command: /{name}")
async def cmd_email(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""/email — check unread emails."""
await _fast_cmd(update, "email", list(context.args or []))
async def cmd_emailsend(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""/emailsend <to> <subject> :: <body>"""
await _fast_cmd(update, "email", ["send"] + list(context.args or []))
async def cmd_emailsave(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""/emailsave — save unread emails to KB."""
await _fast_cmd(update, "email", ["save"])
async def cmd_calendar(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""/calendar — today + tomorrow events."""
await _fast_cmd(update, "calendar", list(context.args or []))
async def cmd_calendarweek(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""/calendarweek — this week's schedule."""
await _fast_cmd(update, "calendar", ["week"])
async def cmd_calendarbusy(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""/calendarbusy — am I in a meeting?"""
await _fast_cmd(update, "calendar", ["busy"])
async def cmd_note(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""/note <text>"""
args = list(context.args or [])
if not args:
await update.message.reply_text("Usage: /note <text>")
return
await _fast_cmd(update, "note", args)
async def cmd_jurnal(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""/jurnal <text>"""
args = list(context.args or [])
if not args:
await update.message.reply_text("Usage: /jurnal <text>")
return
await _fast_cmd(update, "jurnal", args)
async def cmd_search(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""/search <query>"""
args = list(context.args or [])
if not args:
await update.message.reply_text("Usage: /search <query>")
return
await _fast_cmd(update, "search", args)
async def cmd_kb(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""/kb [category]"""
await _fast_cmd(update, "kb", list(context.args or []))
async def cmd_remind(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""/remind <HH:MM> <text> or /remind <YYYY-MM-DD> <HH:MM> <text>"""
args = list(context.args or [])
if len(args) < 2:
await update.message.reply_text("Usage: /remind <HH:MM> <text>")
return
await _fast_cmd(update, "remind", args)
async def cmd_commit(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""/commit [message]"""
await _fast_cmd(update, "commit", list(context.args or []))
async def cmd_push(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""/push"""
await _fast_cmd(update, "push", [])
async def cmd_pull(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""/pull"""
await _fast_cmd(update, "pull", [])
async def cmd_test(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""/test [pattern]"""
await _fast_cmd(update, "test", list(context.args or []))
async def cmd_logs(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""/logs [N]"""
await _fast_cmd(update, "logs", list(context.args or []))
async def cmd_doctor(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""/doctor"""
await _fast_cmd(update, "doctor", [])
async def cmd_heartbeat_tg(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""/heartbeat"""
await _fast_cmd(update, "heartbeat", [])
# --- Message handler ---
@@ -371,6 +520,7 @@ def create_telegram_bot(config: Config, token: str) -> Application:
app = Application.builder().token(token).build()
# Core commands
app.add_handler(CommandHandler("start", cmd_start))
app.add_handler(CommandHandler("help", cmd_help))
app.add_handler(CommandHandler("clear", cmd_clear))
@@ -378,8 +528,59 @@ def create_telegram_bot(config: Config, token: str) -> Application:
app.add_handler(CommandHandler("model", cmd_model))
app.add_handler(CommandHandler("register", cmd_register))
app.add_handler(CallbackQueryHandler(callback_model, pattern="^model:"))
# Fast commands
app.add_handler(CommandHandler("email", cmd_email))
app.add_handler(CommandHandler("emailsend", cmd_emailsend))
app.add_handler(CommandHandler("emailsave", cmd_emailsave))
app.add_handler(CommandHandler("calendar", cmd_calendar))
app.add_handler(CommandHandler("calendarweek", cmd_calendarweek))
app.add_handler(CommandHandler("calendarbusy", cmd_calendarbusy))
app.add_handler(CommandHandler("note", cmd_note))
app.add_handler(CommandHandler("jurnal", cmd_jurnal))
app.add_handler(CommandHandler("search", cmd_search))
app.add_handler(CommandHandler("kb", cmd_kb))
app.add_handler(CommandHandler("remind", cmd_remind))
app.add_handler(CommandHandler("commit", cmd_commit))
app.add_handler(CommandHandler("push", cmd_push))
app.add_handler(CommandHandler("pull", cmd_pull))
app.add_handler(CommandHandler("test", cmd_test))
app.add_handler(CommandHandler("logs", cmd_logs))
app.add_handler(CommandHandler("doctor", cmd_doctor))
app.add_handler(CommandHandler("heartbeat", cmd_heartbeat_tg))
# Text message handler (must be last)
app.add_handler(
MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message)
)
# Register bot menu commands on startup
async def post_init(application: Application) -> None:
await application.bot.set_my_commands([
BotCommand("help", "List commands"),
BotCommand("email", "Check unread emails"),
BotCommand("emailsend", "Send an email"),
BotCommand("emailsave", "Save emails to KB"),
BotCommand("calendar", "Today + tomorrow events"),
BotCommand("calendarweek", "Week schedule"),
BotCommand("calendarbusy", "Am I busy?"),
BotCommand("note", "Quick note"),
BotCommand("jurnal", "Journal entry"),
BotCommand("search", "Memory search"),
BotCommand("kb", "KB notes"),
BotCommand("remind", "Create reminder"),
BotCommand("commit", "Git commit"),
BotCommand("push", "Git push"),
BotCommand("pull", "Git pull"),
BotCommand("test", "Run tests"),
BotCommand("clear", "Clear session"),
BotCommand("status", "Session status"),
BotCommand("model", "View/change model"),
BotCommand("logs", "Show log lines"),
BotCommand("doctor", "Diagnostics"),
BotCommand("heartbeat", "Health checks"),
])
app.post_init = post_init
return app

View File

@@ -137,9 +137,16 @@ def _email_check() -> str:
if count == 0:
return "Inbox curat."
emails = data.get("emails", [])
subjects = [e.get("subject", "?") for e in emails[:5]]
subject_list = ", ".join(subjects)
return f"{count} necitite: {subject_list}"
parts = [f"{count} necitite:\n"]
for e in emails[:5]:
sender = e.get("from", "?")
subject = e.get("subject", "?")
body = e.get("body_preview", "").strip()
parts.append(f"**{subject}**\nDe la: {sender}")
if body:
parts.append(body[:500])
parts.append("")
return "\n".join(parts).strip()
except json.JSONDecodeError:
return proc.stdout.strip()[:300] if proc.stdout else "Email check: no output."
except Exception as e: