feat(bridge): 동적 채널 관리 - gravity-{프로젝트명} 채널 자동 생성/아카이브
This commit is contained in:
@@ -1,11 +1,14 @@
|
|||||||
# Discord Bot Token
|
# Discord Bot Token
|
||||||
DISCORD_TOKEN=your_discord_bot_token_here
|
DISCORD_TOKEN=your_discord_bot_token_here
|
||||||
|
|
||||||
# Discord Channel ID (메인 채널 — 세션 스레드가 여기에 생성됨)
|
# Discord Guild (서버) ID — 봇이 채널을 생성할 서버
|
||||||
DISCORD_CHANNEL_ID=
|
DISCORD_GUILD_ID=
|
||||||
|
|
||||||
# Antigravity Brain Path
|
# Antigravity Brain Path
|
||||||
BRAIN_PATH=C:\Users\Certes\.gemini\antigravity\brain
|
BRAIN_PATH=C:\Users\Certes\.gemini\antigravity\brain
|
||||||
|
|
||||||
|
# 세션 활성 판단: 마지막 파일 변경으로부터 이 시간(초) 이내면 활성
|
||||||
|
ACTIVE_TIMEOUT_SECONDS=300
|
||||||
|
|
||||||
# Watcher Settings
|
# Watcher Settings
|
||||||
DEBOUNCE_SECONDS=2
|
DEBOUNCE_SECONDS=2
|
||||||
|
|||||||
364
bot.py
364
bot.py
@@ -1,16 +1,22 @@
|
|||||||
"""Discord bot — relays Antigravity brain events to Discord channels.
|
"""Discord bot — relays Antigravity brain events to Discord channels.
|
||||||
|
|
||||||
Creates per-session text channels in Discord when new Antigravity sessions are
|
Dynamic channel management:
|
||||||
detected, relays task progress and artifact content as text messages and embeds.
|
- Scans brain/ for active sessions on startup
|
||||||
|
- Creates `gravity-{project_name}` channels per active session
|
||||||
|
- Archives channels when sessions become inactive
|
||||||
|
- Project name extracted from artifact content or short conversation ID
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
|
import time
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
from discord.ext import commands, tasks
|
||||||
|
|
||||||
from config import Config
|
from config import Config
|
||||||
from parser import (
|
from parser import (
|
||||||
@@ -23,6 +29,84 @@ from watcher import BrainEvent, EventType
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def detect_project_name(conv_dir: Path) -> str:
|
||||||
|
"""Extract a human-readable project name from conversation artifacts.
|
||||||
|
|
||||||
|
Strategy:
|
||||||
|
1. Check task.md first line for a title (# Title)
|
||||||
|
2. Check implementation_plan.md first line
|
||||||
|
3. Check any .metadata.json for summary keywords
|
||||||
|
4. Fallback to short conversation ID
|
||||||
|
"""
|
||||||
|
short_id = conv_dir.name[:8]
|
||||||
|
|
||||||
|
# Try task.md title
|
||||||
|
task_file = conv_dir / "task.md"
|
||||||
|
if task_file.exists():
|
||||||
|
try:
|
||||||
|
first_lines = task_file.read_text(encoding="utf-8").splitlines()[:5]
|
||||||
|
for line in first_lines:
|
||||||
|
match = re.match(r'^#\s+(.+?)[\s—\-]+', line)
|
||||||
|
if match:
|
||||||
|
name = match.group(1).strip()
|
||||||
|
# Sanitize for Discord channel name
|
||||||
|
name = re.sub(r'[^a-zA-Z0-9가-힣\s\-]', '', name)
|
||||||
|
name = re.sub(r'\s+', '-', name).lower()[:30]
|
||||||
|
if name:
|
||||||
|
return name
|
||||||
|
except (OSError, UnicodeDecodeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Try implementation_plan.md title
|
||||||
|
plan_file = conv_dir / "implementation_plan.md"
|
||||||
|
if plan_file.exists():
|
||||||
|
try:
|
||||||
|
first_lines = plan_file.read_text(encoding="utf-8").splitlines()[:5]
|
||||||
|
for line in first_lines:
|
||||||
|
match = re.match(r'^#\s+(.+?)[\s—\-]+', line)
|
||||||
|
if match:
|
||||||
|
name = match.group(1).strip()
|
||||||
|
name = re.sub(r'[^a-zA-Z0-9가-힣\s\-]', '', name)
|
||||||
|
name = re.sub(r'\s+', '-', name).lower()[:30]
|
||||||
|
if name:
|
||||||
|
return name
|
||||||
|
except (OSError, UnicodeDecodeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Try metadata summary
|
||||||
|
for meta_file in conv_dir.glob("*.metadata.json"):
|
||||||
|
try:
|
||||||
|
meta = json.loads(meta_file.read_text(encoding="utf-8"))
|
||||||
|
summary = meta.get("summary", "")
|
||||||
|
# Extract first meaningful noun/phrase
|
||||||
|
words = summary.split()[:3]
|
||||||
|
if words:
|
||||||
|
name = "-".join(words).lower()
|
||||||
|
name = re.sub(r'[^a-z0-9가-힣\-]', '', name)[:30]
|
||||||
|
if name:
|
||||||
|
return name
|
||||||
|
except (OSError, json.JSONDecodeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
return short_id
|
||||||
|
|
||||||
|
|
||||||
|
def is_session_active(conv_dir: Path) -> bool:
|
||||||
|
"""Check if a session is active based on file modification time."""
|
||||||
|
now = time.time()
|
||||||
|
threshold = Config.ACTIVE_TIMEOUT_SECONDS
|
||||||
|
|
||||||
|
for f in conv_dir.iterdir():
|
||||||
|
if f.is_file():
|
||||||
|
try:
|
||||||
|
mtime = f.stat().st_mtime
|
||||||
|
if now - mtime < threshold:
|
||||||
|
return True
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class GravityBot(commands.Bot):
|
class GravityBot(commands.Bot):
|
||||||
"""Discord bot for Antigravity session monitoring."""
|
"""Discord bot for Antigravity session monitoring."""
|
||||||
|
|
||||||
@@ -33,59 +117,119 @@ class GravityBot(commands.Bot):
|
|||||||
super().__init__(command_prefix="!", intents=intents)
|
super().__init__(command_prefix="!", intents=intents)
|
||||||
|
|
||||||
self.event_queue = event_queue
|
self.event_queue = event_queue
|
||||||
# conversation_id -> channel_id
|
# conversation_id -> channel object
|
||||||
self.session_channels: dict[str, int] = {}
|
self.session_channels: dict[str, discord.TextChannel] = {}
|
||||||
# conversation_id -> status message id (to edit in-place)
|
# conversation_id -> status message id (to edit in-place)
|
||||||
self.session_status_messages: dict[str, int] = {}
|
self.session_status_messages: dict[str, int] = {}
|
||||||
# Main channel for creating session threads
|
# conversation_id -> project name
|
||||||
self.main_channel: discord.TextChannel | None = None
|
self.session_names: dict[str, str] = {}
|
||||||
# Category for session channels
|
# Category for session channels
|
||||||
self.session_category: discord.CategoryChannel | None = None
|
self.session_category: discord.CategoryChannel | None = None
|
||||||
|
# Guild reference
|
||||||
|
self.guild: discord.Guild | None = None
|
||||||
|
|
||||||
async def setup_hook(self):
|
async def setup_hook(self):
|
||||||
"""Called after login, before processing events."""
|
"""Called after login, before processing events."""
|
||||||
self.loop.create_task(self._process_events())
|
self.loop.create_task(self._process_events())
|
||||||
|
self.session_cleanup_loop.start()
|
||||||
logger.info("Bot setup complete, event processor started")
|
logger.info("Bot setup complete, event processor started")
|
||||||
|
|
||||||
async def on_ready(self):
|
async def on_ready(self):
|
||||||
"""Called when bot is ready."""
|
"""Called when bot is ready."""
|
||||||
logger.info(f"Bot connected as {self.user} (ID: {self.user.id})")
|
logger.info(f"Bot connected as {self.user} (ID: {self.user.id})")
|
||||||
|
|
||||||
# Find or create main channel
|
# Get guild
|
||||||
if Config.DISCORD_CHANNEL_ID:
|
self.guild = self.get_guild(Config.DISCORD_GUILD_ID)
|
||||||
self.main_channel = self.get_channel(Config.DISCORD_CHANNEL_ID)
|
if not self.guild:
|
||||||
if self.main_channel:
|
logger.error(f"Guild {Config.DISCORD_GUILD_ID} not found!")
|
||||||
logger.info(f"Main channel: #{self.main_channel.name}")
|
return
|
||||||
|
|
||||||
# Find or create session category
|
# Find or create session category
|
||||||
guild = self.main_channel.guild
|
category_name = "Antigravity Sessions"
|
||||||
category_name = "Antigravity Sessions"
|
self.session_category = discord.utils.get(
|
||||||
self.session_category = discord.utils.get(
|
self.guild.categories, name=category_name
|
||||||
guild.categories, name=category_name
|
)
|
||||||
)
|
if not self.session_category:
|
||||||
if not self.session_category:
|
try:
|
||||||
try:
|
self.session_category = await self.guild.create_category(category_name)
|
||||||
self.session_category = await guild.create_category(category_name)
|
logger.info(f"Created category: {category_name}")
|
||||||
logger.info(f"Created category: {category_name}")
|
except discord.errors.Forbidden:
|
||||||
except discord.errors.Forbidden:
|
logger.error("No permission to create category!")
|
||||||
logger.warning("No permission to create category, using main channel")
|
return
|
||||||
|
|
||||||
# Send startup message
|
# Sync existing active sessions
|
||||||
embed = discord.Embed(
|
await self._sync_active_sessions()
|
||||||
title="🛰️ Gravity Control 연결됨",
|
|
||||||
description="Antigravity 세션 모니터링을 시작합니다.",
|
async def _sync_active_sessions(self):
|
||||||
color=discord.Color.green(),
|
"""Scan brain/ and create channels for currently active sessions."""
|
||||||
timestamp=datetime.now(timezone.utc),
|
brain_path = Config.BRAIN_PATH
|
||||||
)
|
if not brain_path.exists():
|
||||||
embed.add_field(
|
return
|
||||||
name="Brain Path",
|
|
||||||
value=f"`{Config.BRAIN_PATH}`",
|
active_count = 0
|
||||||
inline=False,
|
for entry in brain_path.iterdir():
|
||||||
)
|
if entry.is_dir() and self._is_conversation_id(entry.name):
|
||||||
await self.main_channel.send(embed=embed)
|
if is_session_active(entry):
|
||||||
|
project_name = detect_project_name(entry)
|
||||||
|
await self._ensure_channel(entry.name, project_name)
|
||||||
|
active_count += 1
|
||||||
|
|
||||||
|
logger.info(f"Synced {active_count} active sessions on startup")
|
||||||
|
|
||||||
|
def _is_conversation_id(self, name: str) -> bool:
|
||||||
|
"""Check if directory name looks like a UUID."""
|
||||||
|
parts = name.split("-")
|
||||||
|
return len(parts) == 5 and all(len(p) >= 4 for p in parts)
|
||||||
|
|
||||||
|
async def _ensure_channel(
|
||||||
|
self, conversation_id: str, project_name: str
|
||||||
|
) -> discord.TextChannel:
|
||||||
|
"""Get or create a Discord channel for a session."""
|
||||||
|
# Check if channel already exists
|
||||||
|
if conversation_id in self.session_channels:
|
||||||
|
return self.session_channels[conversation_id]
|
||||||
|
|
||||||
|
channel_name = f"{Config.CHANNEL_PREFIX}-{project_name}"
|
||||||
|
|
||||||
|
# Check if channel already exists in category (from previous run)
|
||||||
|
if self.session_category:
|
||||||
|
for ch in self.session_category.text_channels:
|
||||||
|
if ch.topic and conversation_id in ch.topic:
|
||||||
|
self.session_channels[conversation_id] = ch
|
||||||
|
self.session_names[conversation_id] = project_name
|
||||||
|
logger.info(f"Reconnected to existing channel #{ch.name}")
|
||||||
|
return ch
|
||||||
|
|
||||||
|
# Create new channel
|
||||||
|
try:
|
||||||
|
channel = await self.guild.create_text_channel(
|
||||||
|
name=channel_name,
|
||||||
|
category=self.session_category,
|
||||||
|
topic=f"Antigravity Session: {conversation_id}",
|
||||||
|
)
|
||||||
|
self.session_channels[conversation_id] = channel
|
||||||
|
self.session_names[conversation_id] = project_name
|
||||||
|
logger.info(f"Created channel #{channel_name}")
|
||||||
|
|
||||||
|
# Welcome embed
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=f"🚀 {project_name}",
|
||||||
|
description=(
|
||||||
|
f"Antigravity 세션 연결됨\n"
|
||||||
|
f"Session: `{conversation_id}`"
|
||||||
|
),
|
||||||
|
color=discord.Color.blue(),
|
||||||
|
timestamp=datetime.now(timezone.utc),
|
||||||
|
)
|
||||||
|
await channel.send(embed=embed)
|
||||||
|
return channel
|
||||||
|
|
||||||
|
except discord.errors.Forbidden:
|
||||||
|
logger.error(f"No permission to create channel: {channel_name}")
|
||||||
|
return None
|
||||||
|
|
||||||
async def _process_events(self):
|
async def _process_events(self):
|
||||||
"""Main event processing loop — consumes brain events."""
|
"""Main event processing loop."""
|
||||||
await self.wait_until_ready()
|
await self.wait_until_ready()
|
||||||
|
|
||||||
while not self.is_closed():
|
while not self.is_closed():
|
||||||
@@ -100,66 +244,23 @@ class GravityBot(commands.Bot):
|
|||||||
logger.error(f"Error processing event: {e}", exc_info=True)
|
logger.error(f"Error processing event: {e}", exc_info=True)
|
||||||
|
|
||||||
async def _handle_event(self, event: BrainEvent):
|
async def _handle_event(self, event: BrainEvent):
|
||||||
"""Route brain events to appropriate handlers."""
|
"""Route brain events to handlers."""
|
||||||
if event.event_type == EventType.SESSION_START:
|
if event.event_type == EventType.SESSION_START:
|
||||||
await self._handle_session_start(event)
|
conv_dir = Config.BRAIN_PATH / event.conversation_id
|
||||||
|
project_name = detect_project_name(conv_dir)
|
||||||
|
await self._ensure_channel(event.conversation_id, project_name)
|
||||||
|
|
||||||
elif event.event_type in (EventType.FILE_CREATED, EventType.FILE_CHANGED):
|
elif event.event_type in (EventType.FILE_CREATED, EventType.FILE_CHANGED):
|
||||||
await self._handle_file_event(event)
|
# Ensure channel exists
|
||||||
|
conv_dir = Config.BRAIN_PATH / event.conversation_id
|
||||||
|
project_name = detect_project_name(conv_dir)
|
||||||
|
channel = await self._ensure_channel(event.conversation_id, project_name)
|
||||||
|
|
||||||
async def _handle_session_start(self, event: BrainEvent):
|
if channel:
|
||||||
"""Create a new Discord channel for the session."""
|
if event.file_name == "task.md":
|
||||||
conv_id = event.conversation_id
|
await self._send_task_update(channel, event)
|
||||||
short_id = conv_id[:8]
|
else:
|
||||||
|
await self._send_artifact_content(channel, event)
|
||||||
if self.session_category:
|
|
||||||
guild = self.session_category.guild
|
|
||||||
channel_name = f"session-{short_id}"
|
|
||||||
|
|
||||||
try:
|
|
||||||
channel = await guild.create_text_channel(
|
|
||||||
name=channel_name,
|
|
||||||
category=self.session_category,
|
|
||||||
topic=f"Antigravity Session: {conv_id}",
|
|
||||||
)
|
|
||||||
self.session_channels[conv_id] = channel.id
|
|
||||||
logger.info(f"Created channel #{channel_name} for session {conv_id}")
|
|
||||||
|
|
||||||
# Send welcome embed
|
|
||||||
embed = discord.Embed(
|
|
||||||
title=f"🚀 새 Antigravity 세션",
|
|
||||||
description=f"Session ID: `{conv_id}`",
|
|
||||||
color=discord.Color.blue(),
|
|
||||||
timestamp=datetime.now(timezone.utc),
|
|
||||||
)
|
|
||||||
await channel.send(embed=embed)
|
|
||||||
|
|
||||||
# Notify main channel
|
|
||||||
if self.main_channel:
|
|
||||||
await self.main_channel.send(
|
|
||||||
f"📡 새 세션 시작: {channel.mention} (`{short_id}`)"
|
|
||||||
)
|
|
||||||
except discord.errors.Forbidden:
|
|
||||||
logger.warning(f"No permission to create channel for {conv_id}")
|
|
||||||
# Fall back to main channel
|
|
||||||
self.session_channels[conv_id] = Config.DISCORD_CHANNEL_ID
|
|
||||||
else:
|
|
||||||
# No category — use main channel
|
|
||||||
self.session_channels[conv_id] = Config.DISCORD_CHANNEL_ID
|
|
||||||
if self.main_channel:
|
|
||||||
await self.main_channel.send(
|
|
||||||
f"📡 새 세션 감지: `{short_id}`"
|
|
||||||
)
|
|
||||||
|
|
||||||
async def _handle_file_event(self, event: BrainEvent):
|
|
||||||
"""Handle file creation/modification events."""
|
|
||||||
channel = await self._get_session_channel(event.conversation_id)
|
|
||||||
if not channel:
|
|
||||||
return
|
|
||||||
|
|
||||||
if event.file_name == "task.md":
|
|
||||||
await self._send_task_update(channel, event)
|
|
||||||
else:
|
|
||||||
await self._send_artifact_content(channel, event)
|
|
||||||
|
|
||||||
async def _send_task_update(
|
async def _send_task_update(
|
||||||
self, channel: discord.TextChannel, event: BrainEvent
|
self, channel: discord.TextChannel, event: BrainEvent
|
||||||
@@ -175,9 +276,10 @@ class GravityBot(commands.Bot):
|
|||||||
else discord.Color.greyple(),
|
else discord.Color.greyple(),
|
||||||
timestamp=datetime.now(timezone.utc),
|
timestamp=datetime.now(timezone.utc),
|
||||||
)
|
)
|
||||||
embed.set_footer(text=f"Session: {event.conversation_id[:8]}")
|
short_id = event.conversation_id[:8]
|
||||||
|
embed.set_footer(text=f"Session: {short_id}")
|
||||||
|
|
||||||
# Edit existing status message or send new one
|
# Edit existing or send new
|
||||||
msg_id = self.session_status_messages.get(event.conversation_id)
|
msg_id = self.session_status_messages.get(event.conversation_id)
|
||||||
if msg_id:
|
if msg_id:
|
||||||
try:
|
try:
|
||||||
@@ -194,7 +296,6 @@ class GravityBot(commands.Bot):
|
|||||||
self, channel: discord.TextChannel, event: BrainEvent
|
self, channel: discord.TextChannel, event: BrainEvent
|
||||||
):
|
):
|
||||||
"""Send artifact file content as Discord text messages."""
|
"""Send artifact file content as Discord text messages."""
|
||||||
# File type label
|
|
||||||
labels = {
|
labels = {
|
||||||
"implementation_plan.md": "📐 구현 계획",
|
"implementation_plan.md": "📐 구현 계획",
|
||||||
"walkthrough.md": "📝 작업 결과 요약",
|
"walkthrough.md": "📝 작업 결과 요약",
|
||||||
@@ -202,62 +303,47 @@ class GravityBot(commands.Bot):
|
|||||||
label = labels.get(event.file_name, f"📄 {event.file_name}")
|
label = labels.get(event.file_name, f"📄 {event.file_name}")
|
||||||
event_label = "생성" if event.event_type == EventType.FILE_CREATED else "업데이트"
|
event_label = "생성" if event.event_type == EventType.FILE_CREATED else "업데이트"
|
||||||
|
|
||||||
# Header message
|
|
||||||
await channel.send(f"**{label} ({event_label}됨)**")
|
await channel.send(f"**{label} ({event_label}됨)**")
|
||||||
|
|
||||||
# Convert and send content
|
|
||||||
chunks = md_to_discord_text(event.content)
|
chunks = md_to_discord_text(event.content)
|
||||||
for chunk in chunks:
|
for chunk in chunks:
|
||||||
if chunk.strip():
|
if chunk.strip():
|
||||||
await channel.send(chunk)
|
await channel.send(chunk)
|
||||||
# Small delay to avoid rate limits
|
|
||||||
await asyncio.sleep(0.5)
|
await asyncio.sleep(0.5)
|
||||||
|
|
||||||
async def _get_session_channel(
|
@tasks.loop(minutes=5)
|
||||||
self, conversation_id: str
|
async def session_cleanup_loop(self):
|
||||||
) -> discord.TextChannel | None:
|
"""Periodically check for inactive sessions and archive their channels."""
|
||||||
"""Get the Discord channel for a session."""
|
if not self.guild:
|
||||||
channel_id = self.session_channels.get(conversation_id)
|
return
|
||||||
|
|
||||||
# If no channel mapped, check if this is a known session and create one
|
to_remove = []
|
||||||
if not channel_id:
|
for conv_id, channel in self.session_channels.items():
|
||||||
# Auto-create for sessions that started before the bot
|
conv_dir = Config.BRAIN_PATH / conv_id
|
||||||
await self._handle_session_start(BrainEvent(
|
if not conv_dir.exists() or not is_session_active(conv_dir):
|
||||||
event_type=EventType.SESSION_START,
|
to_remove.append(conv_id)
|
||||||
conversation_id=conversation_id,
|
|
||||||
))
|
for conv_id in to_remove:
|
||||||
channel_id = self.session_channels.get(conversation_id)
|
channel = self.session_channels.pop(conv_id, None)
|
||||||
|
self.session_status_messages.pop(conv_id, None)
|
||||||
|
name = self.session_names.pop(conv_id, conv_id[:8])
|
||||||
|
|
||||||
if channel_id:
|
|
||||||
channel = self.get_channel(channel_id)
|
|
||||||
if channel:
|
if channel:
|
||||||
return channel
|
|
||||||
|
|
||||||
return self.main_channel
|
|
||||||
|
|
||||||
async def cleanup_session(self, conversation_id: str):
|
|
||||||
"""Clean up when an Antigravity session ends."""
|
|
||||||
channel_id = self.session_channels.pop(conversation_id, None)
|
|
||||||
self.session_status_messages.pop(conversation_id, None)
|
|
||||||
|
|
||||||
if channel_id:
|
|
||||||
channel = self.get_channel(channel_id)
|
|
||||||
if channel and channel.id != Config.DISCORD_CHANNEL_ID:
|
|
||||||
try:
|
try:
|
||||||
# Send closing message before archiving
|
|
||||||
embed = discord.Embed(
|
embed = discord.Embed(
|
||||||
title="🔴 세션 종료",
|
title="🔴 세션 비활성",
|
||||||
description=f"Session `{conversation_id[:8]}` 이 종료되었습니다.",
|
description=f"`{name}` 세션이 비활성 상태입니다.",
|
||||||
color=discord.Color.red(),
|
color=discord.Color.red(),
|
||||||
timestamp=datetime.now(timezone.utc),
|
timestamp=datetime.now(timezone.utc),
|
||||||
)
|
)
|
||||||
await channel.send(embed=embed)
|
await channel.send(embed=embed)
|
||||||
|
await channel.edit(name=f"closed-{name[:20]}")
|
||||||
# Archive channel (move to bottom, read-only)
|
logger.info(f"Archived channel for {conv_id[:8]}")
|
||||||
await channel.edit(
|
|
||||||
name=f"closed-{conversation_id[:8]}",
|
|
||||||
sync_permissions=True,
|
|
||||||
)
|
|
||||||
logger.info(f"Archived channel for session {conversation_id[:8]}")
|
|
||||||
except discord.errors.Forbidden:
|
except discord.errors.Forbidden:
|
||||||
logger.warning(f"No permission to archive channel for {conversation_id}")
|
logger.warning(f"No permission to archive {conv_id[:8]}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to archive {conv_id[:8]}: {e}")
|
||||||
|
|
||||||
|
@session_cleanup_loop.before_loop
|
||||||
|
async def before_cleanup(self):
|
||||||
|
await self.wait_until_ready()
|
||||||
|
|||||||
12
config.py
12
config.py
@@ -13,7 +13,7 @@ class Config:
|
|||||||
|
|
||||||
# Discord
|
# Discord
|
||||||
DISCORD_TOKEN: str = os.getenv("DISCORD_TOKEN", "")
|
DISCORD_TOKEN: str = os.getenv("DISCORD_TOKEN", "")
|
||||||
DISCORD_CHANNEL_ID: int = int(os.getenv("DISCORD_CHANNEL_ID", "0"))
|
DISCORD_GUILD_ID: int = int(os.getenv("DISCORD_GUILD_ID") or "0")
|
||||||
|
|
||||||
# Antigravity Brain path
|
# Antigravity Brain path
|
||||||
BRAIN_PATH: Path = Path(os.getenv(
|
BRAIN_PATH: Path = Path(os.getenv(
|
||||||
@@ -21,6 +21,9 @@ class Config:
|
|||||||
os.path.expanduser("~/.gemini/antigravity/brain")
|
os.path.expanduser("~/.gemini/antigravity/brain")
|
||||||
))
|
))
|
||||||
|
|
||||||
|
# Session activity detection
|
||||||
|
ACTIVE_TIMEOUT_SECONDS: int = int(os.getenv("ACTIVE_TIMEOUT_SECONDS", "300"))
|
||||||
|
|
||||||
# Watcher settings
|
# Watcher settings
|
||||||
DEBOUNCE_SECONDS: float = float(os.getenv("DEBOUNCE_SECONDS", "2"))
|
DEBOUNCE_SECONDS: float = float(os.getenv("DEBOUNCE_SECONDS", "2"))
|
||||||
|
|
||||||
@@ -35,14 +38,17 @@ class Config:
|
|||||||
DISCORD_MSG_LIMIT: int = 2000
|
DISCORD_MSG_LIMIT: int = 2000
|
||||||
DISCORD_EMBED_DESC_LIMIT: int = 4096
|
DISCORD_EMBED_DESC_LIMIT: int = 4096
|
||||||
|
|
||||||
|
# Channel naming
|
||||||
|
CHANNEL_PREFIX: str = "gravity"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate(cls) -> list[str]:
|
def validate(cls) -> list[str]:
|
||||||
"""Return list of configuration errors."""
|
"""Return list of configuration errors."""
|
||||||
errors = []
|
errors = []
|
||||||
if not cls.DISCORD_TOKEN:
|
if not cls.DISCORD_TOKEN:
|
||||||
errors.append("DISCORD_TOKEN is not set")
|
errors.append("DISCORD_TOKEN is not set")
|
||||||
if not cls.DISCORD_CHANNEL_ID:
|
if not cls.DISCORD_GUILD_ID:
|
||||||
errors.append("DISCORD_CHANNEL_ID is not set")
|
errors.append("DISCORD_GUILD_ID is not set")
|
||||||
if not cls.BRAIN_PATH.exists():
|
if not cls.BRAIN_PATH.exists():
|
||||||
errors.append(f"BRAIN_PATH does not exist: {cls.BRAIN_PATH}")
|
errors.append(f"BRAIN_PATH does not exist: {cls.BRAIN_PATH}")
|
||||||
return errors
|
return errors
|
||||||
|
|||||||
Reference in New Issue
Block a user