Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:48:35 +08:00
commit 6f1ef3ef54
45 changed files with 15173 additions and 0 deletions

443
commands/triage.md Normal file
View File

@@ -0,0 +1,443 @@
---
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 <ticket-id>"
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'
```