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:
2026-05-20 10:11:36 -07:00
parent 377ab3a63f
commit 020ae0cc1c
4 changed files with 206 additions and 6 deletions

View File

@@ -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:

View File

@@ -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")

View 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,