Skip to content

Commit b255dc1

Browse files
committed
unified python helpers and slash commands
1 parent efced40 commit b255dc1

12 files changed

Lines changed: 926 additions & 413 deletions

.gitignore

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,10 @@ pids
3030

3131
# Temporary files
3232
tmp/
33-
temp/
33+
temp/
34+
35+
# Python cache files
36+
__pycache__/
37+
*.py[cod]
38+
*$py.class
39+
*.so

commands/discord/discord_utils.py

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
#!/usr/bin/env python3
2+
3+
"""
4+
Shared utilities for Discord integration commands
5+
Provides common functions for JSON handling, path detection, validation, and formatting
6+
"""
7+
8+
import json
9+
import os
10+
import sys
11+
import re
12+
from pathlib import Path
13+
from typing import Dict, Any, Optional, List, Tuple
14+
15+
class DiscordUtils:
16+
"""Utility class for Discord integration operations"""
17+
18+
# Color codes for output formatting
19+
COLORS = {
20+
'SUCCESS': '✅',
21+
'ERROR': '❌',
22+
'INFO': 'ℹ️',
23+
'WARNING': '⚠️',
24+
'ACTIVE': '🟢',
25+
'INACTIVE': '🔴',
26+
'LOCAL': '📍',
27+
'GLOBAL': '🌐',
28+
'THREAD': '🧵',
29+
'CHANNEL': '📢',
30+
'AUTH': '🔐',
31+
'SETTINGS': '🔧',
32+
'HOOKS': '🪝'
33+
}
34+
35+
@staticmethod
36+
def get_installation_type() -> Tuple[str, str]:
37+
"""
38+
Determine installation type and return paths
39+
Returns: (installation_type, commands_base, hooks_base)
40+
"""
41+
local_commands = Path(".claude/commands/discord")
42+
local_hooks = Path(".claude/hooks")
43+
44+
if local_commands.exists() and local_hooks.exists():
45+
return "local", ".claude/commands/discord", ".claude/hooks"
46+
else:
47+
home_commands = Path.home() / ".claude/commands/discord"
48+
home_hooks = Path.home() / ".claude/hooks"
49+
return "global", str(home_commands), str(home_hooks)
50+
51+
@staticmethod
52+
def load_state(state_file: str = ".claude/discord-state.json") -> Dict[str, Any]:
53+
"""Load Discord state from JSON file"""
54+
try:
55+
with open(state_file, 'r', encoding='utf-8') as f:
56+
return json.load(f)
57+
except (FileNotFoundError, json.JSONDecodeError):
58+
return {}
59+
60+
@staticmethod
61+
def save_state(state: Dict[str, Any], state_file: str = ".claude/discord-state.json") -> bool:
62+
"""Save Discord state to JSON file"""
63+
try:
64+
# Ensure directory exists
65+
os.makedirs(os.path.dirname(state_file), exist_ok=True)
66+
67+
with open(state_file, 'w', encoding='utf-8') as f:
68+
json.dump(state, f, indent=2)
69+
return True
70+
except Exception as e:
71+
print(f"{DiscordUtils.COLORS['ERROR']} Failed to save state: {e}")
72+
return False
73+
74+
@staticmethod
75+
def validate_webhook_url(url: str) -> bool:
76+
"""Validate Discord webhook URL format"""
77+
if not url:
78+
return False
79+
80+
pattern = r'^https://discord\.com/api/webhooks/\d+/[a-zA-Z0-9_-]+$'
81+
return bool(re.match(pattern, url))
82+
83+
@staticmethod
84+
def mask_webhook_url(url: str) -> str:
85+
"""Mask webhook URL for display"""
86+
if not url:
87+
return "Not configured"
88+
89+
# Extract webhook ID and mask the token
90+
match = re.match(r'^(https://discord\.com/api/webhooks/\d+)/', url)
91+
if match:
92+
return f"{match.group(1)}/..."
93+
return "Invalid URL"
94+
95+
@staticmethod
96+
def get_project_name() -> str:
97+
"""Get current project name (directory name)"""
98+
return os.path.basename(os.getcwd())
99+
100+
@staticmethod
101+
def parse_arguments(args_string: str) -> List[str]:
102+
"""Parse space-separated arguments string"""
103+
if not args_string or not args_string.strip():
104+
return []
105+
return args_string.strip().split()
106+
107+
@staticmethod
108+
def print_header(title: str, char: str = "=") -> None:
109+
"""Print formatted header"""
110+
print(f"{DiscordUtils.COLORS['SETTINGS']} {title}")
111+
print(char * (len(title) + 2))
112+
113+
@staticmethod
114+
def print_status_line(label: str, value: str, emoji: str = "") -> None:
115+
"""Print formatted status line"""
116+
if emoji:
117+
print(f"{label}: {emoji} {value}")
118+
else:
119+
print(f"{label}: {value}")
120+
121+
@staticmethod
122+
def print_success(message: str) -> None:
123+
"""Print success message"""
124+
print(f"{DiscordUtils.COLORS['SUCCESS']} {message}")
125+
126+
@staticmethod
127+
def print_error(message: str) -> None:
128+
"""Print error message"""
129+
print(f"{DiscordUtils.COLORS['ERROR']} {message}")
130+
131+
@staticmethod
132+
def print_info(message: str) -> None:
133+
"""Print info message"""
134+
print(f"{DiscordUtils.COLORS['INFO']} {message}")
135+
136+
@staticmethod
137+
def print_warning(message: str) -> None:
138+
"""Print warning message"""
139+
print(f"{DiscordUtils.COLORS['WARNING']} {message}")
140+
141+
@staticmethod
142+
def create_state_config(webhook_url: str, project_name: str,
143+
auth_token: Optional[str] = None,
144+
thread_id: Optional[str] = None) -> Dict[str, Any]:
145+
"""Create initial state configuration"""
146+
config = {
147+
"active": False,
148+
"webhook_url": webhook_url,
149+
"project_name": project_name
150+
}
151+
152+
if auth_token:
153+
config["auth_token"] = auth_token
154+
155+
if thread_id:
156+
config["thread_id"] = thread_id
157+
158+
return config
159+
160+
@staticmethod
161+
def merge_hooks_config(settings_file: str = ".claude/settings.json") -> bool:
162+
"""Merge Discord hooks into settings.json"""
163+
try:
164+
# Get installation paths
165+
install_type, commands_base, hooks_base = DiscordUtils.get_installation_type()
166+
167+
# Discord hooks configuration
168+
discord_hooks = {
169+
"Stop": [
170+
{
171+
"matcher": "",
172+
"hooks": [
173+
{
174+
"type": "command",
175+
"command": f"{hooks_base}/stop-discord.py"
176+
}
177+
]
178+
}
179+
],
180+
"Notification": [
181+
{
182+
"matcher": "",
183+
"hooks": [
184+
{
185+
"type": "command",
186+
"command": f"{hooks_base}/notification-discord.py"
187+
}
188+
]
189+
}
190+
],
191+
"PostToolUse": [
192+
{
193+
"matcher": "",
194+
"hooks": [
195+
{
196+
"type": "command",
197+
"command": f"{hooks_base}/posttooluse-discord.py"
198+
}
199+
]
200+
}
201+
]
202+
}
203+
204+
# Load existing settings
205+
try:
206+
with open(settings_file, 'r', encoding='utf-8') as f:
207+
settings = json.load(f)
208+
except (FileNotFoundError, json.JSONDecodeError):
209+
settings = {}
210+
211+
# Ensure hooks section exists
212+
if 'hooks' not in settings:
213+
settings['hooks'] = {}
214+
215+
# Merge Discord hooks
216+
settings['hooks'].update(discord_hooks)
217+
218+
# Save back to file
219+
with open(settings_file, 'w', encoding='utf-8') as f:
220+
json.dump(settings, f, indent=2)
221+
222+
return True
223+
224+
except Exception as e:
225+
DiscordUtils.print_error(f"Failed to merge hooks: {e}")
226+
return False
227+
228+
@staticmethod
229+
def backup_settings(settings_file: str = ".claude/settings.json") -> bool:
230+
"""Create backup of settings file"""
231+
try:
232+
if os.path.exists(settings_file):
233+
import shutil
234+
backup_file = f"{settings_file}.backup"
235+
shutil.copy2(settings_file, backup_file)
236+
return True
237+
return False
238+
except Exception:
239+
return False
240+
241+
@staticmethod
242+
def check_state_exists(state_file: str = ".claude/discord-state.json") -> bool:
243+
"""Check if Discord state file exists"""
244+
return os.path.exists(state_file)
245+
246+
@staticmethod
247+
def get_available_commands() -> List[str]:
248+
"""Get list of available Discord commands"""
249+
return [
250+
"/user:discord:setup WEBHOOK_URL [AUTH_TOKEN] [THREAD_ID] - Setup Discord integration",
251+
"/user:discord:start [THREAD_ID] - Enable notifications",
252+
"/user:discord:stop - Disable notifications",
253+
"/user:discord:status - Check current status",
254+
"/user:discord:remove - Remove integration"
255+
]
256+
257+
@staticmethod
258+
def exit_with_error(message: str, code: int = 1) -> None:
259+
"""Print error and exit"""
260+
DiscordUtils.print_error(message)
261+
sys.exit(code)
262+
263+
@staticmethod
264+
def exit_with_success(message: str) -> None:
265+
"""Print success and exit"""
266+
DiscordUtils.print_success(message)
267+
sys.exit(0)

0 commit comments

Comments
 (0)