--- allowed-tools: Bash(*), SlashCommand description: Triage FreshService ticket and create GitHub issue argument-hint: [ticket-id] model: claude-sonnet-4-5 extended-thinking: true --- # FreshService Ticket Triage You are a support engineer who triages bug reports from FreshService and creates well-structured GitHub issues. You extract all relevant information from FreshService tickets and automatically create comprehensive issues for development teams. **Ticket ID:** $ARGUMENTS ## Workflow ### Phase 1: Configuration Validation & Ticket Fetch Validate credentials, fetch the ticket, and format the data: ```bash # Validate and sanitize ticket ID TICKET_ID="$ARGUMENTS" TICKET_ID="${TICKET_ID//[^0-9]/}" # Remove all non-numeric characters if [ -z "$TICKET_ID" ] || ! [[ "$TICKET_ID" =~ ^[0-9]+$ ]]; then echo "❌ Invalid ticket ID" echo "Usage: /triage " exit 1 fi # Check for environment configuration if [ -f ~/.claude/freshservice.env ]; then echo "✓ Loading FreshService configuration..." source ~/.claude/freshservice.env else echo "❌ FreshService configuration not found!" echo "" echo "Please create ~/.claude/freshservice.env with:" echo "" echo "FRESHSERVICE_API_KEY=your_api_key_here" echo "FRESHSERVICE_DOMAIN=your_domain" echo "" echo "Example:" echo "FRESHSERVICE_API_KEY=abcdef123456" echo "FRESHSERVICE_DOMAIN=psd401" echo "" exit 1 fi # Validate required variables if [ -z "$FRESHSERVICE_API_KEY" ] || [ -z "$FRESHSERVICE_DOMAIN" ]; then echo "❌ Missing required environment variables!" echo "Required: FRESHSERVICE_API_KEY, FRESHSERVICE_DOMAIN" exit 1 fi # Validate domain format (alphanumeric and hyphens only, prevents SSRF) if ! [[ "$FRESHSERVICE_DOMAIN" =~ ^[a-zA-Z0-9-]+$ ]]; then echo "❌ Invalid FRESHSERVICE_DOMAIN format" echo "Domain must contain only alphanumeric characters and hyphens" echo "Example: 'psd401' (not 'psd401.freshservice.com')" exit 1 fi # Validate API key format (basic sanity check) if [ ${#FRESHSERVICE_API_KEY} -lt 20 ]; then echo "⚠️ Warning: API key appears too short. Please verify your configuration." fi echo "✓ Configuration validated" echo "Domain: $FRESHSERVICE_DOMAIN" echo "" # API configuration API_BASE_URL="https://${FRESHSERVICE_DOMAIN}.freshservice.com/api/v2" TICKET_ENDPOINT="${API_BASE_URL}/tickets/${TICKET_ID}" # Temporary files for API responses TICKET_JSON="/tmp/fs-ticket-${TICKET_ID}.json" CONVERSATIONS_JSON="/tmp/fs-conversations-${TICKET_ID}.json" # Cleanup function cleanup() { rm -f "$TICKET_JSON" "$CONVERSATIONS_JSON" } trap cleanup EXIT echo "=== Fetching FreshService Ticket #${TICKET_ID} ===" echo "" # Function to make API request with retry logic api_request() { local url="$1" local output_file="$2" local max_retries=3 local retry_delay=2 local attempt=1 while [ $attempt -le $max_retries ]; do # Make request and capture HTTP status code http_code=$(curl -s -w "%{http_code}" -u "${FRESHSERVICE_API_KEY}:X" \ -H "Content-Type: application/json" \ -X GET "$url" \ -o "$output_file" \ --max-time 30) # Check for rate limiting (HTTP 429) if [ "$http_code" = "429" ]; then echo "❌ Error: Rate limit exceeded. Please wait before retrying." echo "FreshService API has rate limits (typically 1000 requests/hour)." return 1 fi # Success (HTTP 200) if [ "$http_code" = "200" ]; then return 0 fi # Unauthorized (HTTP 401) if [ "$http_code" = "401" ]; then echo "❌ Error: Authentication failed. Please check your API key." return 1 fi # Not found (HTTP 404) if [ "$http_code" = "404" ]; then echo "❌ Error: Ticket not found. Please verify the ticket ID." return 1 fi # Retry on server errors (5xx) if [ $attempt -lt $max_retries ]; then echo "⚠️ Warning: API request failed with HTTP $http_code (attempt $attempt/$max_retries), retrying in ${retry_delay}s..." sleep $retry_delay retry_delay=$((retry_delay * 2)) # Exponential backoff fi attempt=$((attempt + 1)) done echo "❌ Error: API request failed after $max_retries attempts (last HTTP code: $http_code)" return 1 } # Fetch ticket with embedded fields echo "Fetching ticket #${TICKET_ID}..." if ! api_request "${TICKET_ENDPOINT}?include=requester,stats" "$TICKET_JSON"; then echo "" echo "❌ Failed to retrieve ticket from FreshService" echo "Please verify:" echo " - Ticket ID $TICKET_ID exists" echo " - API key is valid" echo " - Domain is correct ($FRESHSERVICE_DOMAIN)" exit 1 fi # Fetch conversations (comments) echo "Fetching ticket conversations..." if ! api_request "${TICKET_ENDPOINT}/conversations" "$CONVERSATIONS_JSON"; then echo "⚠️ Warning: Failed to fetch conversations, continuing without them..." echo '{"conversations":[]}' > "$CONVERSATIONS_JSON" fi echo "✓ Ticket retrieved successfully" echo "" # Check if jq is available for JSON parsing if ! command -v jq &> /dev/null; then echo "⚠️ Warning: jq not found, using basic parsing" echo "Install jq for full functionality: brew install jq (macOS) or apt-get install jq (Linux)" JQ_AVAILABLE=false else JQ_AVAILABLE=true fi # Extract ticket fields if [ "$JQ_AVAILABLE" = true ]; then SUBJECT=$(jq -r '.ticket.subject // "No subject"' "$TICKET_JSON") DESCRIPTION=$(jq -r '.ticket.description_text // .ticket.description // "No description"' "$TICKET_JSON") PRIORITY=$(jq -r '.ticket.priority // 0' "$TICKET_JSON") STATUS=$(jq -r '.ticket.status // 0' "$TICKET_JSON") CREATED_AT=$(jq -r '.ticket.created_at // "Unknown"' "$TICKET_JSON") REQUESTER_NAME=$(jq -r '.ticket.requester.name // "Unknown"' "$TICKET_JSON" 2>/dev/null || echo "Unknown") CATEGORY=$(jq -r '.ticket.category // "Uncategorized"' "$TICKET_JSON") URGENCY=$(jq -r '.ticket.urgency // 0' "$TICKET_JSON") # Extract custom fields if present CUSTOM_FIELDS=$(jq -r '.ticket.custom_fields // {}' "$TICKET_JSON") # Extract attachments if present ATTACHMENTS=$(jq -r '.ticket.attachments // []' "$TICKET_JSON") HAS_ATTACHMENTS=$(echo "$ATTACHMENTS" | jq '. | length > 0') # Extract conversations CONVERSATIONS=$(jq -r '.conversations // []' "$CONVERSATIONS_JSON") CONVERSATION_COUNT=$(echo "$CONVERSATIONS" | jq '. | length') else # Fallback to basic grep/sed parsing SUBJECT=$(grep -o '"subject":"[^"]*"' "$TICKET_JSON" | head -1 | sed 's/"subject":"//;s/"$//' || echo "No subject") DESCRIPTION=$(grep -o '"description_text":"[^"]*"' "$TICKET_JSON" | head -1 | sed 's/"description_text":"//;s/"$//' || echo "No description") PRIORITY="0" STATUS="0" CREATED_AT="Unknown" REQUESTER_NAME="Unknown" CATEGORY="Unknown" URGENCY="0" HAS_ATTACHMENTS="false" CONVERSATION_COUNT="0" fi # Map priority codes to human-readable strings case "$PRIORITY" in 1) PRIORITY_STR="Low" ;; 2) PRIORITY_STR="Medium" ;; 3) PRIORITY_STR="High" ;; 4) PRIORITY_STR="Urgent" ;; *) PRIORITY_STR="Unknown" ;; esac # Map urgency codes case "$URGENCY" in 1) URGENCY_STR="Low" ;; 2) URGENCY_STR="Medium" ;; 3) URGENCY_STR="High" ;; *) URGENCY_STR="Unknown" ;; esac # Map status codes case "$STATUS" in 2) STATUS_STR="Open" ;; 3) STATUS_STR="Pending" ;; 4) STATUS_STR="Resolved" ;; 5) STATUS_STR="Closed" ;; *) STATUS_STR="Unknown" ;; esac # Format the issue description ISSUE_DESCRIPTION="Bug report from FreshService Ticket #${TICKET_ID} ## Summary ${SUBJECT} ## Description ${DESCRIPTION} ## Ticket Information - **FreshService Ticket**: #${TICKET_ID} - **Status**: ${STATUS_STR} - **Priority**: ${PRIORITY_STR} - **Urgency**: ${URGENCY_STR} - **Category**: ${CATEGORY} - **Created**: ${CREATED_AT} ## Reporter Information - **Name**: ${REQUESTER_NAME} - **Contact**: Available in FreshService ticket #${TICKET_ID} ## Steps to Reproduce (Please extract from description or conversations if available) ## Expected Behavior (To be determined from ticket context) ## Actual Behavior (Described in ticket) ## Additional Context" # Add attachments section if present if [ "$HAS_ATTACHMENTS" = "true" ] && [ "$JQ_AVAILABLE" = true ]; then ISSUE_DESCRIPTION="${ISSUE_DESCRIPTION} ### Attachments" ATTACHMENTS_LIST=$(echo "$ATTACHMENTS" | jq -r '.[] | "- \(.name) (\(.size) bytes)"') ISSUE_DESCRIPTION="${ISSUE_DESCRIPTION} ${ATTACHMENTS_LIST}" fi # Add conversation history if present (sanitize HTML/script tags) if [ "$CONVERSATION_COUNT" -gt 0 ] && [ "$JQ_AVAILABLE" = true ]; then ISSUE_DESCRIPTION="${ISSUE_DESCRIPTION} ### Conversation History " CONVERSATION_TEXT=$(echo "$CONVERSATIONS" | jq -r '.[] | "**\(.user_id // "User")** (\(.created_at)):\n" + ((.body_text // .body) | gsub("[<>]"; "")) + "\n"') ISSUE_DESCRIPTION="${ISSUE_DESCRIPTION}${CONVERSATION_TEXT}" fi # Add custom fields if present and not empty if [ "$JQ_AVAILABLE" = true ]; then CUSTOM_FIELDS_COUNT=$(echo "$CUSTOM_FIELDS" | jq '. | length') if [ "$CUSTOM_FIELDS_COUNT" -gt 0 ]; then ISSUE_DESCRIPTION="${ISSUE_DESCRIPTION} ### Custom Fields \`\`\`json # Custom fields from FreshService - review before using $(echo "$CUSTOM_FIELDS" | jq '.') \`\`\`" fi fi # Add link to original ticket ISSUE_DESCRIPTION="${ISSUE_DESCRIPTION} --- *Imported from FreshService: https://${FRESHSERVICE_DOMAIN}.freshservice.com/a/tickets/${TICKET_ID}*" echo "=== Ticket Information ===" echo "" echo "Subject: $SUBJECT" echo "Priority: $PRIORITY_STR" echo "Status: $STATUS_STR" echo "" ``` ### Phase 2: Create GitHub Issue Now invoke the `/issue` command with the extracted information: **IMPORTANT**: Use the SlashCommand tool to invoke `/psd-claude-coding-system:issue` with the ticket description. Pass the `$ISSUE_DESCRIPTION` variable that contains the formatted bug report from FreshService. **After the issue is created, capture the issue number/URL for the FreshService reply.** ### Phase 3: Update FreshService Ticket After successfully creating the GitHub issue, add a reply to the FreshService ticket and update its status: ```bash echo "" echo "=== Updating FreshService Ticket ===" echo "" # Add a reply to the ticket with the GitHub issue link echo "Adding reply to ticket..." REPLY_BODY="Thank you for submitting this issue. We have received your ticket and created a GitHub issue to track this problem. We will let you know when the issue has been resolved." REPLY_RESPONSE=$(curl -s -w "\n%{http_code}" -u "${FRESHSERVICE_API_KEY}:X" \ -H "Content-Type: application/json" \ -X POST "${API_BASE_URL}/tickets/${TICKET_ID}/conversations" \ -d "{\"body\": \"${REPLY_BODY}\"}") # Extract HTTP code from response REPLY_HTTP_CODE=$(echo "$REPLY_RESPONSE" | tail -n1) REPLY_JSON=$(echo "$REPLY_RESPONSE" | head -n-1) if [ "$REPLY_HTTP_CODE" = "201" ]; then echo "✓ Reply added to ticket" else echo "⚠️ Warning: Failed to add reply (HTTP $REPLY_HTTP_CODE)" echo " FreshService ticket NOT updated" fi # Update ticket status to "In Progress" (status code 2) echo "Updating ticket status to In Progress..." STATUS_RESPONSE=$(curl -s -w "\n%{http_code}" -u "${FRESHSERVICE_API_KEY}:X" \ -H "Content-Type: application/json" \ -X PUT "${API_BASE_URL}/tickets/${TICKET_ID}" \ -d '{"status": 2}') # Extract HTTP code from response STATUS_HTTP_CODE=$(echo "$STATUS_RESPONSE" | tail -n1) if [ "$STATUS_HTTP_CODE" = "200" ]; then echo "✓ Ticket status updated to In Progress" else echo "⚠️ Warning: Failed to update status (HTTP $STATUS_HTTP_CODE)" echo " FreshService ticket status NOT updated" fi echo "" ``` ### Phase 4: Confirmation After the issue is created, provide a summary: ```bash echo "" echo "✅ Triage completed successfully!" echo "" echo "Summary:" echo " - FreshService Ticket: #$TICKET_ID" echo " - Subject: $SUBJECT" echo " - Priority: $PRIORITY_STR" echo " - Status: Updated to In Progress" echo " - Reply: Sent to requester" echo "" echo "Next steps:" echo " - Review the created GitHub issue" echo " - Use /work [issue-number] to begin implementation" echo " - When resolved, update FreshService ticket manually" ``` ## Error Handling Handle common error scenarios: 1. **Missing Configuration**: Guide user to create `~/.claude/freshservice.env` 2. **Invalid Ticket ID**: Validate numeric format 3. **API Failures**: Provide clear error messages with troubleshooting steps 4. **Network Issues**: Suggest checking connectivity and credentials ## Security Notes - API key is stored in `~/.claude/freshservice.env` (user-level, not in repository) - All API communications use HTTPS - Input validation prevents injection attacks - Credentials are never logged or displayed - Sensitive data (emails) not included in public GitHub issues ## Example Usage ```bash # Triage a FreshService ticket /triage 12345 # This will: # 1. Fetch ticket #12345 from FreshService # 2. Extract all relevant information # 3. Format as a bug report # 4. Create a GitHub issue automatically # 5. Return the new issue URL ``` ## Troubleshooting **Configuration Issues:** ```bash # Check if config file exists ls -la ~/.claude/freshservice.env # View configuration (without exposing API key) grep FRESHSERVICE_DOMAIN ~/.claude/freshservice.env ``` **API Issues:** ```bash # Test API connectivity manually curl -u YOUR_API_KEY:X -X GET 'https://YOUR_DOMAIN.freshservice.com/api/v2/tickets/TICKET_ID' ```