From c7173d708abe0823b2197c7ec3aa70b840ddacf6 Mon Sep 17 00:00:00 2001 From: Perry Greenwood Date: Sat, 17 Jan 2026 16:35:03 -0500 Subject: [PATCH] Add claude-slack-test command and standardize paths to ~/.claude/slack/ New Features: - Add claude-slack-test command for testing Slack connection - Add --local flag to test directory-level hook configuration - Add optional --description flag to claude-slack for meaningful thread descriptions Path Standardization: - Consolidate all paths to ~/.claude/slack/ directory structure: - ~/.claude/slack/logs/ for all log files - ~/.claude/slack/sockets/ for Unix sockets - ~/.claude/slack/registry.db for session database - Remove hardcoded /tmp/ paths that were inconsistent with config.py defaults - Update .env.example to show correct default paths (commented out) - Fix .env to not override defaults with /tmp/ paths Files Changed: - bin/claude-slack-test (new): Test Slack connection and directory setup - bin/claude-slack: Add --description and --help flags - bin/claude-slack-listener: Fix monitor log path - bin/claude-slack-monitor: Add env loading, fix log paths - bin/claude-slack-debug: Fix log/socket directory paths - core/slack_listener.py: Use get_socket_dir() for legacy socket - core/claude_wrapper_hybrid.py: Use LOG_DIR for buffer files - core/session_registry.py: Add description support in Slack threads - hooks/on_*.py: Add LOG_DIR, fix debug log paths - README.md, SECURITY.md: Update path references Co-Authored-By: Claude Opus 4.5 --- .env.example | 12 +- README.md | 6 +- SECURITY.md | 4 +- bin/claude-slack | 43 ++++- bin/claude-slack-debug | 4 +- bin/claude-slack-listener | 6 +- bin/claude-slack-monitor | 18 +- bin/claude-slack-test | 332 ++++++++++++++++++++++++++++++++++ core/claude_wrapper_hybrid.py | 20 +- core/session_registry.py | 48 +++-- core/slack_listener.py | 9 +- hooks/check_hook_status.sh | 2 +- hooks/on_notification.py | 10 +- hooks/on_pretooluse.py | 8 +- hooks/on_stop.py | 8 +- 15 files changed, 478 insertions(+), 52 deletions(-) create mode 100755 bin/claude-slack-test diff --git a/.env.example b/.env.example index 4de96c6..6a0fb0a 100644 --- a/.env.example +++ b/.env.example @@ -13,11 +13,13 @@ SLACK_APP_TOKEN=xapp-your-app-token-here # 3. Slack channel for notifications (optional, defaults to #claude-sessions) SLACK_CHANNEL=#your-channel-here -# Multi-Session Configuration (Phase 2.5) -# These have sensible defaults, usually don't need to change -SLACK_SOCKET_DIR=/tmp/claude_socks -REGISTRY_DB_PATH=/tmp/claude_sessions/registry.db -SLACK_LOG_DIR=/tmp +# Multi-Session Configuration +# These have sensible defaults in ~/.claude/slack/, usually don't need to change +# Uncomment and modify only if you need custom paths +# +# SLACK_SOCKET_DIR=${HOME}/.claude/slack/sockets +# REGISTRY_DB_PATH=${HOME}/.claude/slack/registry.db +# SLACK_LOG_DIR=${HOME}/.claude/slack/logs # Optional: VibeTunnel Integration (leave commented unless using VibeTunnel) # VIBE_TUNNEL_API_URL=https://your-vibetunnel-server.com diff --git a/README.md b/README.md index 003cb18..682b26e 100644 --- a/README.md +++ b/README.md @@ -175,13 +175,13 @@ If you still experience issues, ensure your Slack app has all the scopes and eve ```bash # Check listener logs -tail -f /tmp/slack_listener.log +tail -f ~/.claude/slack/logs/slack_listener.log # Check hook execution logs -tail -f /tmp/stop_hook_debug.log +tail -f ~/.claude/slack/logs/notification_hook_debug.log # Check session registry -sqlite3 /tmp/claude_sessions/registry.db "SELECT * FROM sessions;" +sqlite3 ~/.claude/slack/registry.db "SELECT * FROM sessions;" ``` ### Common Issues diff --git a/SECURITY.md b/SECURITY.md index 8fa9f30..ebdf66d 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -109,10 +109,10 @@ grep -E "xoxb-|xapp-" /tmp/*.log #### Active Session Monitoring ```bash # List active sessions -sqlite3 /tmp/claude_sessions/registry.db "SELECT * FROM sessions;" +sqlite3 ~/.claude/slack/registry.db "SELECT * FROM sessions;" # Check for stale sessions -sqlite3 /tmp/claude_sessions/registry.db "SELECT * FROM sessions WHERE updated_at < datetime('now', '-24 hours');" +sqlite3 ~/.claude/slack/registry.db "SELECT * FROM sessions WHERE updated_at < datetime('now', '-24 hours');" ``` ### Development Security diff --git a/bin/claude-slack b/bin/claude-slack index a708ff5..a6eaff9 100755 --- a/bin/claude-slack +++ b/bin/claude-slack @@ -23,6 +23,38 @@ YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color +# Parse arguments +SESSION_DESCRIPTION="" +CLAUDE_ARGS=() + +while [[ $# -gt 0 ]]; do + case $1 in + -d|--description) + SESSION_DESCRIPTION="$2" + shift 2 + ;; + -h|--help) + echo "Usage: claude-slack [OPTIONS] [CLAUDE_ARGS...]" + echo "" + echo "Launch Claude Code with Slack integration" + echo "" + echo "Options:" + echo " -d, --description TEXT Optional description for the Slack thread" + echo " -h, --help Show this help message" + echo "" + echo "Examples:" + echo " claude-slack" + echo " claude-slack -d \"Working on auth feature\"" + echo " claude-slack --description \"Bug fix for issue #123\"" + exit 0 + ;; + *) + CLAUDE_ARGS+=("$1") + shift + ;; + esac +done + # Step 1: Ensure listener is ready echo -e "${BLUE}Checking Slack integration...${NC}" if ! "${SCRIPT_DIR}/claude-slack-ensure"; then @@ -144,8 +176,15 @@ sleep 2 # Step 4: Launch Claude with the hybrid wrapper echo -e "${GREEN}Starting Claude with Slack integration...${NC}" +if [ -n "$SESSION_DESCRIPTION" ]; then + echo -e "${BLUE}Description: ${SESSION_DESCRIPTION}${NC}" +fi echo "" -# Pass all arguments through to the Python wrapper +# Pass description and Claude args to the Python wrapper cd "$PROJECT_DIR" -exec python3 "$CORE_DIR/claude_wrapper_hybrid.py" "$@" +if [ -n "$SESSION_DESCRIPTION" ]; then + exec python3 "$CORE_DIR/claude_wrapper_hybrid.py" --description "$SESSION_DESCRIPTION" "${CLAUDE_ARGS[@]}" +else + exec python3 "$CORE_DIR/claude_wrapper_hybrid.py" "${CLAUDE_ARGS[@]}" +fi diff --git a/bin/claude-slack-debug b/bin/claude-slack-debug index 94535cf..fbe9a5d 100755 --- a/bin/claude-slack-debug +++ b/bin/claude-slack-debug @@ -38,7 +38,7 @@ fi echo "" echo "4. Latest Wrapper Log" -LOG_DIR="${HOME}/.local/share/claude-code-integration/logs" +LOG_DIR="${SLACK_LOG_DIR:-$HOME/.claude/slack/logs}" LATEST_LOG=$(ls -t "$LOG_DIR"/wrapper_*.log 2>/dev/null | head -1) if [ -n "$LATEST_LOG" ]; then echo " Log: $LATEST_LOG" @@ -51,7 +51,7 @@ fi echo "" echo "5. Active Sessions" -SOCKET_DIR="${HOME}/.local/share/claude-code-integration/sockets" +SOCKET_DIR="${SLACK_SOCKET_DIR:-$HOME/.claude/slack/sockets}" if [ -d "$SOCKET_DIR" ]; then echo " Sockets in $SOCKET_DIR:" ls -lh "$SOCKET_DIR"/*.sock 2>/dev/null || echo " No active sessions" diff --git a/bin/claude-slack-listener b/bin/claude-slack-listener index 0358785..8b66787 100755 --- a/bin/claude-slack-listener +++ b/bin/claude-slack-listener @@ -111,13 +111,13 @@ echo -e "${BLUE}Starting slack_listener.py...${NC}" LISTENER_LOG="$SLACK_LOG_DIR/slack_listener.log" > "$LISTENER_LOG" 2>/dev/null || true -# Monitor expects logs at /tmp/slack_listener.log (for event starvation detection) -MONITOR_LOG="/tmp/slack_listener.log" +# Monitor log (same directory as other logs) +MONITOR_LOG="$SLACK_LOG_DIR/slack_listener_monitor.log" > "$MONITOR_LOG" 2>/dev/null || true # Start the listener in the background # NOTE: Removed 'nohup' - it was interfering with Socket Mode WebSocket connections -# Using 'tee' to write to both startup log and monitor log (monitor watches /tmp/slack_listener.log) +# Using 'tee' to write to both startup log and monitor log cd "$CORE_DIR" python3 -u slack_listener.py 2>&1 | tee "$MONITOR_LOG" > "$LISTENER_LOG" & NEW_PID=$! diff --git a/bin/claude-slack-monitor b/bin/claude-slack-monitor index 05f87b0..8c542c7 100755 --- a/bin/claude-slack-monitor +++ b/bin/claude-slack-monitor @@ -12,9 +12,21 @@ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" INTEGRATION_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" CORE_DIR="$INTEGRATION_DIR/core" -# Environment variables (use /tmp for compatibility with original) -LOG_FILE="/tmp/slack_listener.log" -MONITOR_LOG="/tmp/slack_monitor.log" +# Load environment variables from .env file +ENV_FILE="$INTEGRATION_DIR/.env" +if [ -f "$ENV_FILE" ]; then + set -a + source "$ENV_FILE" + set +a +fi + +# Log directory (defaults to ~/.claude/slack/logs) +SLACK_LOG_DIR=${SLACK_LOG_DIR:-"$HOME/.claude/slack/logs"} +mkdir -p "$SLACK_LOG_DIR" + +# Log files +LOG_FILE="$SLACK_LOG_DIR/slack_listener_monitor.log" +MONITOR_LOG="$SLACK_LOG_DIR/slack_monitor.log" CHECK_INTERVAL=180 # Check every 3 minutes EVENT_TIMEOUT=300 # Restart if no events for 5 minutes diff --git a/bin/claude-slack-test b/bin/claude-slack-test new file mode 100755 index 0000000..e404054 --- /dev/null +++ b/bin/claude-slack-test @@ -0,0 +1,332 @@ +#!/bin/bash + +# Claude-Slack Test Script +# Tests Slack connection by sending a test message to the configured channel +# With --local flag, tests the hooks configuration in the current directory + +set -e + +# Get the script directory and set paths +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +INTEGRATION_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +PROJECT_DIR="$(pwd)" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Parse arguments +LOCAL_TEST=false +while [[ $# -gt 0 ]]; do + case $1 in + -l|--local) + LOCAL_TEST=true + shift + ;; + -h|--help) + echo "Usage: claude-slack-test [OPTIONS]" + echo "" + echo "Test Slack connection and optionally project hooks" + echo "" + echo "Options:" + echo " -l, --local Test hooks configuration in current directory" + echo " -h, --help Show this help message" + echo "" + echo "Examples:" + echo " claude-slack-test # Test global Slack connection" + echo " claude-slack-test --local # Test current directory hooks setup" + exit 0 + ;; + *) + echo -e "${RED}Unknown option: $1${NC}" + echo "Use --help for usage information" + exit 1 + ;; + esac +done + +# Load environment variables from .env file +ENV_FILE="$INTEGRATION_DIR/.env" +if [ ! -f "$ENV_FILE" ]; then + echo -e "${RED}✗ Error: .env file not found at $ENV_FILE${NC}" + echo " Please create the .env file with your Slack tokens" + echo " Copy from .env.example and fill in your tokens" + exit 1 +fi + +set -a +source "$ENV_FILE" +set +a + +if [ "$LOCAL_TEST" = true ]; then + # ========================================= + # LOCAL/DIRECTORY TEST MODE + # ========================================= + echo "=======================================" + echo "Claude-Slack Directory Test" + echo "=======================================" + echo "" + echo -e "${BLUE}Testing: ${PROJECT_DIR}${NC}" + echo "" + + ISSUES=0 + + # Test 1: Check for .claude directory + echo -n "1. Checking .claude directory... " + if [ -d "$PROJECT_DIR/.claude" ]; then + echo -e "${GREEN}✓ Found${NC}" + else + echo -e "${RED}✗ Not found${NC}" + ISSUES=$((ISSUES + 1)) + fi + + # Test 2: Check for hooks directory + echo -n "2. Checking .claude/hooks directory... " + if [ -d "$PROJECT_DIR/.claude/hooks" ]; then + echo -e "${GREEN}✓ Found${NC}" + else + echo -e "${RED}✗ Not found${NC}" + ISSUES=$((ISSUES + 1)) + fi + + # Test 3: Check for required hook files + echo "3. Checking hook files..." + HOOKS=("on_stop.py" "on_notification.py" "on_pretooluse.py") + for hook in "${HOOKS[@]}"; do + echo -n " - $hook... " + if [ -f "$PROJECT_DIR/.claude/hooks/$hook" ]; then + # Check if executable + if [ -x "$PROJECT_DIR/.claude/hooks/$hook" ]; then + echo -e "${GREEN}✓ Found (executable)${NC}" + else + echo -e "${YELLOW}⚠ Found (not executable)${NC}" + fi + else + echo -e "${RED}✗ Not found${NC}" + ISSUES=$((ISSUES + 1)) + fi + done + + # Test 4: Check settings.local.json + echo -n "4. Checking settings.local.json... " + SETTINGS_FILE="$PROJECT_DIR/.claude/settings.local.json" + if [ -f "$SETTINGS_FILE" ]; then + echo -e "${GREEN}✓ Found${NC}" + + # Check if hooks are configured + echo -n " - Hooks configured... " + if grep -q '"hooks"' "$SETTINGS_FILE"; then + echo -e "${GREEN}✓ Yes${NC}" + else + echo -e "${RED}✗ No hooks section${NC}" + ISSUES=$((ISSUES + 1)) + fi + else + echo -e "${RED}✗ Not found${NC}" + ISSUES=$((ISSUES + 1)) + fi + + # Test 5: Check if listener is running + echo -n "5. Checking Slack listener... " + if pgrep -f "slack_listener.py" > /dev/null; then + PID=$(pgrep -f "slack_listener.py" | head -1) + echo -e "${GREEN}✓ Running (PID: $PID)${NC}" + else + echo -e "${RED}✗ Not running${NC}" + ISSUES=$((ISSUES + 1)) + fi + + # Test 6: Send a test message from this directory + echo "" + echo -n "6. Sending test message from this directory... " + + TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S') + PROJECT_NAME=$(basename "$PROJECT_DIR") + TEST_MESSAGE="🧪 *Directory Test: ${PROJECT_NAME}*\n\nThis is a test from \`claude-slack-test --local\`.\n\n• Directory: \`${PROJECT_DIR}\`\n• Time: ${TIMESTAMP}\n• Hooks installed: ✅" + + # Resolve channel + CHANNEL_ID="$SLACK_CHANNEL" + if [[ "$SLACK_CHANNEL" == \#* ]]; then + CHANNEL_NAME="${SLACK_CHANNEL:1}" + CHANNELS_RESPONSE=$(curl -s -X GET "https://slack.com/api/conversations.list?types=public_channel,private_channel&limit=200" \ + -H "Authorization: Bearer $SLACK_BOT_TOKEN") + CHANNEL_ID=$(echo "$CHANNELS_RESPONSE" | python3 -c " +import sys, json +try: + data = json.load(sys.stdin) + for ch in data.get('channels', []): + if ch.get('name') == '$CHANNEL_NAME': + print(ch.get('id')) + break +except: + pass +" 2>/dev/null) + fi + + if [ -n "$CHANNEL_ID" ]; then + SEND_RESPONSE=$(curl -s -X POST "https://slack.com/api/chat.postMessage" \ + -H "Authorization: Bearer $SLACK_BOT_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{ + \"channel\": \"$CHANNEL_ID\", + \"text\": \"$TEST_MESSAGE\", + \"mrkdwn\": true + }") + + SEND_OK=$(echo "$SEND_RESPONSE" | grep -o '"ok":[^,]*' | cut -d: -f2) + if [ "$SEND_OK" = "true" ]; then + echo -e "${GREEN}✓ Sent${NC}" + else + ERROR=$(echo "$SEND_RESPONSE" | grep -o '"error":"[^"]*"' | cut -d'"' -f4) + echo -e "${RED}✗ Failed ($ERROR)${NC}" + ISSUES=$((ISSUES + 1)) + fi + else + echo -e "${RED}✗ Channel not found${NC}" + ISSUES=$((ISSUES + 1)) + fi + + # Summary + echo "" + echo "=======================================" + if [ $ISSUES -eq 0 ]; then + echo -e "${GREEN}✅ DIRECTORY READY FOR SLACK${NC}" + echo "=======================================" + echo "" + echo "This directory is fully configured for Slack integration." + echo "Start a session with: claude-slack" + else + echo -e "${RED}✗ $ISSUES ISSUE(S) FOUND${NC}" + echo "=======================================" + echo "" + echo "To fix, run from this directory:" + echo " claude-slack" + echo "" + echo "This will install the required hooks and configuration." + fi + +else + # ========================================= + # GLOBAL CONNECTION TEST MODE + # ========================================= + echo "=======================================" + echo "Claude-Slack Connection Test" + echo "=======================================" + echo "" + + # Validate required variables + echo -n "1. Checking SLACK_BOT_TOKEN... " + if [ -z "$SLACK_BOT_TOKEN" ]; then + echo -e "${RED}✗ Not set${NC}" + exit 1 + fi + echo -e "${GREEN}✓ Set${NC}" + + echo -n "2. Checking SLACK_CHANNEL... " + if [ -z "$SLACK_CHANNEL" ]; then + echo -e "${RED}✗ Not set${NC}" + echo " Please set SLACK_CHANNEL in your .env file" + exit 1 + fi + echo -e "${GREEN}✓ $SLACK_CHANNEL${NC}" + + # Test 1: Verify API connection with auth.test + echo "" + echo -n "3. Testing Slack API connection... " + AUTH_RESPONSE=$(curl -s -X POST "https://slack.com/api/auth.test" \ + -H "Authorization: Bearer $SLACK_BOT_TOKEN" \ + -H "Content-Type: application/json") + + AUTH_OK=$(echo "$AUTH_RESPONSE" | grep -o '"ok":[^,]*' | cut -d: -f2) + if [ "$AUTH_OK" = "true" ]; then + BOT_NAME=$(echo "$AUTH_RESPONSE" | grep -o '"user":"[^"]*"' | cut -d'"' -f4) + TEAM_NAME=$(echo "$AUTH_RESPONSE" | grep -o '"team":"[^"]*"' | cut -d'"' -f4) + echo -e "${GREEN}✓ Connected${NC}" + echo " Bot: $BOT_NAME" + echo " Team: $TEAM_NAME" + else + echo -e "${RED}✗ Failed${NC}" + ERROR=$(echo "$AUTH_RESPONSE" | grep -o '"error":"[^"]*"' | cut -d'"' -f4) + echo " Error: $ERROR" + exit 1 + fi + + # Test 2: Send a test message + echo "" + echo -n "4. Sending test message to $SLACK_CHANNEL... " + + TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S') + HOSTNAME=$(hostname) + TEST_MESSAGE="🧪 *Claude-Slack Test Message*\n\nThis is a test message from \`claude-slack-test\`.\n\n• Time: $TIMESTAMP\n• Host: $HOSTNAME\n• Status: ✅ Connection working!" + + # Resolve channel name to ID if needed (channel names start with #) + CHANNEL_ID="$SLACK_CHANNEL" + if [[ "$SLACK_CHANNEL" == \#* ]]; then + # Remove the # prefix for the API + CHANNEL_NAME="${SLACK_CHANNEL:1}" + + # List channels to find the ID + CHANNELS_RESPONSE=$(curl -s -X GET "https://slack.com/api/conversations.list?types=public_channel,private_channel&limit=200" \ + -H "Authorization: Bearer $SLACK_BOT_TOKEN") + + CHANNEL_ID=$(echo "$CHANNELS_RESPONSE" | python3 -c " +import sys, json +try: + data = json.load(sys.stdin) + for ch in data.get('channels', []): + if ch.get('name') == '$CHANNEL_NAME': + print(ch.get('id')) + break +except: + pass +" 2>/dev/null) + + if [ -z "$CHANNEL_ID" ]; then + echo -e "${RED}✗ Channel not found${NC}" + echo " Could not find channel: $SLACK_CHANNEL" + echo " Make sure the bot is invited to the channel" + exit 1 + fi + fi + + # Send the message + SEND_RESPONSE=$(curl -s -X POST "https://slack.com/api/chat.postMessage" \ + -H "Authorization: Bearer $SLACK_BOT_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{ + \"channel\": \"$CHANNEL_ID\", + \"text\": \"$TEST_MESSAGE\", + \"mrkdwn\": true + }") + + SEND_OK=$(echo "$SEND_RESPONSE" | grep -o '"ok":[^,]*' | cut -d: -f2) + if [ "$SEND_OK" = "true" ]; then + echo -e "${GREEN}✓ Message sent!${NC}" + MESSAGE_TS=$(echo "$SEND_RESPONSE" | grep -o '"ts":"[^"]*"' | head -1 | cut -d'"' -f4) + echo " Message timestamp: $MESSAGE_TS" + else + echo -e "${RED}✗ Failed to send${NC}" + ERROR=$(echo "$SEND_RESPONSE" | grep -o '"error":"[^"]*"' | cut -d'"' -f4) + echo " Error: $ERROR" + + if [ "$ERROR" = "channel_not_found" ] || [ "$ERROR" = "not_in_channel" ]; then + echo "" + echo " Tip: The bot needs to be invited to the channel." + echo " In Slack, go to $SLACK_CHANNEL and type: /invite @$BOT_NAME" + fi + exit 1 + fi + + echo "" + echo "=======================================" + echo -e "${GREEN}✅ ALL TESTS PASSED${NC}" + echo "=======================================" + echo "" + echo "Your Slack connection is working correctly!" + echo "Check $SLACK_CHANNEL for the test message." + echo "" + echo "To test a specific directory, use: claude-slack-test --local" +fi diff --git a/core/claude_wrapper_hybrid.py b/core/claude_wrapper_hybrid.py index 30679a2..2537f82 100755 --- a/core/claude_wrapper_hybrid.py +++ b/core/claude_wrapper_hybrid.py @@ -374,7 +374,7 @@ def _send_command(self, command, data=None, timeout=8): debug_log(f"Registry communication error: {e}") return None - def register(self, project, terminal, socket_path): + def register(self, project, terminal, socket_path, description=None): """Register session with registry and create Slack thread""" data = { "session_id": self.session_id, @@ -382,6 +382,8 @@ def register(self, project, terminal, socket_path): "terminal": terminal, "socket_path": socket_path } + if description: + data["description"] = description response = self._send_command("REGISTER", data) @@ -399,10 +401,11 @@ def register(self, project, terminal, socket_path): class HybridPTYWrapper: """Hybrid PTY wrapper combining input control with hooks output""" - def __init__(self, session_id, project_dir, claude_args=None): + def __init__(self, session_id, project_dir, claude_args=None, description=None): self.session_id = session_id self.project_dir = project_dir self.claude_args = claude_args or [] + self.description = description # Optional description for Slack thread # Setup logging self.logger = setup_logging(session_id) @@ -411,6 +414,8 @@ def __init__(self, session_id, project_dir, claude_args=None): self.logger.info(f"Session ID: {session_id}") self.logger.info(f"Project directory: {project_dir}") self.logger.info(f"Claude args: {claude_args}") + if description: + self.logger.info(f"Description: {description}") self.logger.info(f"Python version: {sys.version}") self.logger.info(f"Working directory: {os.getcwd()}") @@ -443,7 +448,7 @@ def __init__(self, session_id, project_dir, claude_args=None): # Output buffer for capturing exact permission prompts (4KB ring buffer) # Increased from 1KB to 4KB to capture all 3 permission options self.output_buffer = deque(maxlen=4096) - self.buffer_file = f"/tmp/claude_output_{session_id}.txt" + self.buffer_file = os.path.join(LOG_DIR, f"claude_output_{session_id}.txt") self.buffer_lock = threading.Lock() self.logger.info(f"Output buffer initialized: {self.buffer_file}") @@ -571,7 +576,8 @@ def register_with_registry(self): success = self.registry.register( project=os.path.basename(self.project_dir), terminal=terminal, - socket_path=self.socket_path + socket_path=self.socket_path, + description=self.description ) if success: @@ -868,7 +874,7 @@ def update_buffer_file_path(self, claude_session_id): claude_session_id: Claude's full UUID session ID """ old_buffer_file = self.buffer_file - new_buffer_file = f"/tmp/claude_output_{claude_session_id}.txt" + new_buffer_file = os.path.join(LOG_DIR, f"claude_output_{claude_session_id}.txt") with self.buffer_lock: try: @@ -1152,6 +1158,7 @@ def main(): ) parser.add_argument("--session-id", help="Unique session ID (auto-generated if not provided)") + parser.add_argument("--description", "-d", help="Optional description for the Slack thread") parser.add_argument("--help", "-h", action="store_true", help="Show help message") # Parse known args, remaining go to Claude @@ -1172,7 +1179,8 @@ def main(): wrapper = HybridPTYWrapper( session_id=session_id, project_dir=project_dir, - claude_args=claude_args + claude_args=claude_args, + description=args.description ) # Run wrapper diff --git a/core/session_registry.py b/core/session_registry.py index 0d7e3b0..7284bd4 100755 --- a/core/session_registry.py +++ b/core/session_registry.py @@ -638,6 +638,9 @@ def _create_slack_thread(self, session_data: Dict[str, Any]) -> Dict[str, str]: if not self.slack_client: raise RuntimeError("Slack client not initialized") + # Get optional description + description = session_data.get('description') or session_data.get('user_label') + # Create simple parent message in channel (no status tracking) blocks = [ { @@ -646,25 +649,42 @@ def _create_slack_thread(self, session_data: Dict[str, Any]) -> Dict[str, str]: "type": "plain_text", "text": f"🚀 {session_data.get('project', 'Unknown')}" } - }, - { - "type": "section", - "fields": [ - { - "type": "mrkdwn", - "text": f"*Session:* `{session_data['session_id'][:12]}...`" - }, - { - "type": "mrkdwn", - "text": f"*Terminal:* {session_data.get('terminal', 'Unknown')}" - } - ] } ] + # Add description if provided + if description: + blocks.append({ + "type": "section", + "text": { + "type": "mrkdwn", + "text": f"_{description}_" + } + }) + + # Add session metadata + blocks.append({ + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": f"*Session:* `{session_data['session_id'][:12]}...`" + }, + { + "type": "mrkdwn", + "text": f"*Terminal:* {session_data.get('terminal', 'Unknown')}" + } + ] + }) + + # Build text fallback + text_fallback = f"New Session: {session_data.get('project', 'Unknown')}" + if description: + text_fallback += f" - {description}" + response = self.slack_client.chat_postMessage( channel=self.slack_channel, - text=f"New Session: {session_data.get('project', 'Unknown')}", + text=text_fallback, blocks=blocks ) diff --git a/core/slack_listener.py b/core/slack_listener.py index e110ca0..f2bdc1c 100755 --- a/core/slack_listener.py +++ b/core/slack_listener.py @@ -15,8 +15,8 @@ - Routes threaded messages to correct session socket - Supports multiple concurrent Claude sessions in different threads -Phase 2 Mode (legacy hard-coded socket): - - Sends to Unix socket at /tmp/claude_slack.sock +Phase 2 Mode (legacy socket): + - Sends to Unix socket at ~/.claude/slack/sockets/claude_slack.sock - Used for non-threaded messages as fallback Phase 1 Mode (file-based fallback): @@ -29,7 +29,7 @@ Environment Variables: SLACK_BOT_TOKEN - Bot User OAuth Token (required) SLACK_APP_TOKEN - App-Level Token for Socket Mode (required) - SLACK_SOCKET_PATH - Unix socket path (default: /tmp/claude_slack.sock) + SLACK_SOCKET_PATH - Unix socket path (default: ~/.claude/slack/sockets/claude_slack.sock) Registry Database: Location: ~/.claude/slack/registry.db (default, override via REGISTRY_DB_PATH) @@ -54,7 +54,8 @@ # Configuration - use centralized config for consistent paths PROJECT_DIR = Path(__file__).parent.parent RESPONSE_FILE = PROJECT_DIR / "slack_response.txt" -SOCKET_PATH = os.environ.get("SLACK_SOCKET_PATH", "/tmp/claude_slack.sock") +SOCKET_DIR = get_socket_dir() +SOCKET_PATH = os.environ.get("SLACK_SOCKET_PATH", os.path.join(SOCKET_DIR, "claude_slack.sock")) REGISTRY_DB_PATH = get_registry_db_path() # Uses ~/.claude/slack/registry.db by default # Initialize registry database - create directory and DB if needed diff --git a/hooks/check_hook_status.sh b/hooks/check_hook_status.sh index 285f61e..f16e6a3 100755 --- a/hooks/check_hook_status.sh +++ b/hooks/check_hook_status.sh @@ -36,7 +36,7 @@ fi echo echo "3. Debug log file:" -DEBUG_LOG="${STOP_HOOK_DEBUG_LOG:-/tmp/stop_hook_debug.log}" +DEBUG_LOG="${STOP_HOOK_DEBUG_LOG:-$HOME/.claude/slack/logs/stop_hook_debug.log}" if [ -f "$DEBUG_LOG" ]; then echo " ✓ Debug log exists" echo " File size: $(wc -c < "$DEBUG_LOG") bytes" diff --git a/hooks/on_notification.py b/hooks/on_notification.py index d73abdb..403d577 100755 --- a/hooks/on_notification.py +++ b/hooks/on_notification.py @@ -48,7 +48,7 @@ 5. Exit 0 (success or failure) Debug Logging: - - All execution logged to /tmp/notification_hook_debug.log + - All execution logged to ~/.claude/slack/logs/notification_hook_debug.log - Includes timestamps, session info, environment vars - Tracks hook lifecycle from entry to exit """ @@ -62,8 +62,12 @@ # Hook version (for auto-updates) HOOK_VERSION = "2.1.0" +# Log directory - use ~/.claude/slack/logs as default +LOG_DIR = os.environ.get("SLACK_LOG_DIR", os.path.expanduser("~/.claude/slack/logs")) +os.makedirs(LOG_DIR, exist_ok=True) + # Debug log file path -DEBUG_LOG = "/tmp/notification_hook_debug.log" +DEBUG_LOG = os.path.join(LOG_DIR, "notification_hook_debug.log") # Find claude-slack directory dynamically # Hooks are templates that get copied to project folders, but they need to find the @@ -862,7 +866,7 @@ def enhance_notification_message( # FIRST: Try to get exact permission text from output buffer exact_options_from_buffer = None - buffer_file = f"/tmp/claude_output_{session_id}.txt" + buffer_file = os.path.join(LOG_DIR, f"claude_output_{session_id}.txt") if os.path.exists(buffer_file): try: diff --git a/hooks/on_pretooluse.py b/hooks/on_pretooluse.py index 7bee491..789943e 100755 --- a/hooks/on_pretooluse.py +++ b/hooks/on_pretooluse.py @@ -48,7 +48,7 @@ 6. Exit 0 (success or failure) Debug Logging: - - All execution logged to /tmp/pretooluse_hook_debug.log + - All execution logged to ~/.claude/slack/logs/pretooluse_hook_debug.log """ import sys @@ -60,8 +60,12 @@ # Hook version for auto-update detection HOOK_VERSION = "1.1.0" +# Log directory - use ~/.claude/slack/logs as default +LOG_DIR = os.environ.get("SLACK_LOG_DIR", os.path.expanduser("~/.claude/slack/logs")) +os.makedirs(LOG_DIR, exist_ok=True) + # Debug log file path -DEBUG_LOG = "/tmp/pretooluse_hook_debug.log" +DEBUG_LOG = os.path.join(LOG_DIR, "pretooluse_hook_debug.log") # Find claude-slack directory dynamically def find_claude_slack_dir(): diff --git a/hooks/on_stop.py b/hooks/on_stop.py index a170ce1..14f592e 100755 --- a/hooks/on_stop.py +++ b/hooks/on_stop.py @@ -36,7 +36,7 @@ 5. Exit 0 (success or failure) Debug Logging: - - All execution logged to /tmp/stop_hook_debug.log + - All execution logged to ~/.claude/slack/logs/stop_hook_debug.log - Includes timestamps, session info, environment vars - Tracks hook lifecycle from entry to exit """ @@ -50,8 +50,12 @@ # Hook version for auto-update detection HOOK_VERSION = "1.1.0" +# Log directory - use ~/.claude/slack/logs as default +LOG_DIR = os.environ.get("SLACK_LOG_DIR", os.path.expanduser("~/.claude/slack/logs")) +os.makedirs(LOG_DIR, exist_ok=True) + # Debug log file path -DEBUG_LOG = "/tmp/stop_hook_debug.log" +DEBUG_LOG = os.path.join(LOG_DIR, "stop_hook_debug.log") # Find claude-slack directory dynamically # Hooks are templates that get copied to project folders, but they need to find the