feat: Discord bot — per-session rules, user identity, and DISCORD_CLAUDE.md
- Add DISCORD_CLAUDE.md as the Discord bot's dedicated system prompt, replacing the main CLAUDE.md for bot sessions. Covers: no-interactive rules, Discord user authorization, vault/remediation guidance, /save after every task, and formatting rules for Discord. - config.py: add discord_system_prompt field (default: projects/discord-bot/ DISCORD_CLAUDE.md, overridable via env var). - client.py: _load_system_prompt() now loads discord_system_prompt path with fallback to CLAUDE.md if file is missing. - message_handler.py: inject [DISCORD_CONTEXT] header into every agent message containing Discord username, display name, user ID, channel, and guild so the agent always knows who is asking. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -20,8 +20,14 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _load_system_prompt() -> str:
|
||||
claude_md = settings.claudetools_root / ".claude" / "CLAUDE.md"
|
||||
return claude_md.read_text(encoding="utf-8")
|
||||
prompt_path = settings.claudetools_root / settings.discord_system_prompt
|
||||
if prompt_path.exists():
|
||||
return prompt_path.read_text(encoding="utf-8")
|
||||
logger.warning(
|
||||
"[WARNING] Discord system prompt not found at %s — falling back to CLAUDE.md",
|
||||
prompt_path,
|
||||
)
|
||||
return (settings.claudetools_root / ".claude" / "CLAUDE.md").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
class ThreadAgent:
|
||||
|
||||
@@ -22,6 +22,14 @@ class Settings(BaseSettings):
|
||||
description="Path to ClaudeTools repository (agent cwd)",
|
||||
)
|
||||
|
||||
# Path to Discord-specific system prompt, relative to claudetools_root.
|
||||
# Edit projects/discord-bot/DISCORD_CLAUDE.md to change bot behavior without
|
||||
# touching the main CLAUDE.md. Changes take effect on next bot restart.
|
||||
discord_system_prompt: Path = Field(
|
||||
default=Path("projects/discord-bot/DISCORD_CLAUDE.md"),
|
||||
description="System prompt for Discord bot sessions (relative to claudetools_root)",
|
||||
)
|
||||
|
||||
log_level: str = Field(default="INFO", description="Logging level")
|
||||
log_file: Optional[Path] = Field(default=Path("logs/bot.log"), description="Log file")
|
||||
|
||||
|
||||
@@ -29,21 +29,37 @@ class MessageHandler:
|
||||
if message.author == self.bot.user:
|
||||
return
|
||||
|
||||
content = message.content
|
||||
# Strip the bot mention to get the raw user text.
|
||||
user_text = message.content
|
||||
for mention in message.mentions:
|
||||
if mention == self.bot.user:
|
||||
content = content.replace(f"<@{mention.id}>", "").replace(
|
||||
user_text = user_text.replace(f"<@{mention.id}>", "").replace(
|
||||
f"<@!{mention.id}>", ""
|
||||
).strip()
|
||||
|
||||
if not content and not message.attachments:
|
||||
if not user_text and not message.attachments:
|
||||
await message.reply("Hey! How can I help?")
|
||||
return
|
||||
|
||||
# Build caller-identity header so the agent always knows who is asking.
|
||||
author = message.author
|
||||
display = getattr(author, "display_name", author.name)
|
||||
guild_name = message.guild.name if message.guild else "DM"
|
||||
channel_name = getattr(message.channel, "name", "unknown")
|
||||
discord_ctx = (
|
||||
"[DISCORD_CONTEXT]\n"
|
||||
f"User: @{author.name}"
|
||||
+ (f" (display: {display})" if display != author.name else "")
|
||||
+ f" | ID: {author.id}\n"
|
||||
f"Channel: #{channel_name} | Guild: {guild_name}\n"
|
||||
"[/DISCORD_CONTEXT]\n\n"
|
||||
)
|
||||
content = (discord_ctx + user_text).strip()
|
||||
|
||||
if isinstance(message.channel, discord.Thread):
|
||||
thread = message.channel
|
||||
else:
|
||||
name = self._thread_name(content) if content else "Attachment"
|
||||
name = self._thread_name(user_text) if user_text else "Attachment"
|
||||
thread = await message.create_thread(
|
||||
name=name,
|
||||
auto_archive_duration=1440,
|
||||
|
||||
Reference in New Issue
Block a user