| name | skill-zulip |
| description | Fetch a Zulip thread via API and write formatted JSON to a file. Invoke for /zulip command. |
| allowed-tools | Bash, AskUserQuestion |
Zulip Fetch Skill (Direct Execution)
Direct execution skill that fetches all messages from a Zulip chat thread and writes formatted JSON output (sender, date, content) to a target file. Reads credentials from ~/.zuliprc and parses the Zulip URL to extract channel and topic.
Execution
Step 1: Parse Arguments
Extract URL and optional output path from the arguments passed to this skill.
Run these bash commands to parse the arguments:
url=$(echo "$ARGUMENTS" | awk '{print $1}')
output_path=$(echo "$ARGUMENTS" | awk 'NF>=2{print $2}')
If $url is empty, print usage and stop:
if [ -z "$url" ]; then
echo "Error: Usage: /zulip <zulip-url> [output-path]"
echo "Example: /zulip https://leanprover.zulipchat.com/#narrow/channel/113489-new-members/topic/some.20topic"
exit 1
fi
Step 2: Validate Prerequisites
Check that the credentials file exists and all required tools are available:
zuliprc="$HOME/.zuliprc"
if [ ! -f "$zuliprc" ]; then
echo "Error: ~/.zuliprc not found. Create it with:"
echo " [api]"
echo " email=your@email.com"
echo " key=your_api_key"
echo " site=https://yourorg.zulipchat.com"
exit 1
fi
for tool in curl jq python3; do
if ! which "$tool" >/dev/null 2>&1; then
echo "Error: Required tool not found: $tool"
exit 1
fi
done
Step 3: Read Credentials
Parse ~/.zuliprc to extract the API email, key, and site URL:
email=$(grep -oP '(?<=email=)\S+' "$zuliprc")
api_key=$(grep -oP '(?<=key=)\S+' "$zuliprc")
site=$(grep -oP '(?<=site=)\S+' "$zuliprc" | sed 's|/$||')
if [ -z "$email" ] || [ -z "$api_key" ] || [ -z "$site" ]; then
echo "Error: ~/.zuliprc is missing required fields (email, key, site)"
exit 1
fi
Step 4: Parse Zulip URL
Extract the base URL, channel/stream segment, topic segment, and optional message anchor from the URL.
Note: Zulip uses #narrow/channel/ in newer versions and #narrow/stream/ in older ones — both must be handled.
base=$(echo "$url" | grep -oP 'https?://[^/#]+')
channel_raw=$(echo "$url" | grep -oP '(?<=#narrow/(?:channel|stream)/)[^/]+')
topic_raw=$(echo "$url" | grep -oP '(?<=/topic/)[^/]+')
near_id=$(echo "$url" | grep -oP '(?<=/near/)[0-9]+' || true)
if [ -z "$base" ] || [ -z "$channel_raw" ]; then
echo "Error: Could not parse Zulip URL. Expected format:"
echo " https://REALM.zulipchat.com/#narrow/channel/ID-stream-name/topic/topic.20text"
exit 1
fi
if [ -z "$topic_raw" ]; then
echo "Error: URL does not contain a topic segment. Provide a thread URL (not just a channel URL)."
exit 1
fi
Step 5: Decode URL Components
Convert the raw URL segments into a usable stream name and topic string.
Zulip uses dot-encoding in URL fragments: .20 means space, .2C means comma, etc. These are different from standard percent-encoding. The conversion is: .XX -> %XX, then standard URL-decode.
channel_slug=$(echo "$channel_raw" | sed 's/^[0-9]*-//')
stream_name=$(echo "$channel_slug" | tr '-' ' ')
topic=$(python3 -c "
import re, sys
from urllib.parse import unquote
raw = sys.argv[1]
pct = re.sub(r'\.([0-9A-Fa-f]{2})', lambda m: '%' + m.group(1), raw)
print(unquote(pct))
" "$topic_raw")
echo "Fetching thread: stream='$stream_name', topic='$topic'"
if [ -n "$near_id" ]; then
echo "Anchor: message ID $near_id"
fi
Step 6: Resolve Output Path
If no output path was provided in the arguments, prompt the user to choose where to send the output.
Use AskUserQuestion to present two choices:
- Option 1: Print JSON to the screen (stdout)
- Option 2: Write JSON to a file (user provides path)
Ask: "Where should the Zulip thread output go? Enter '1' to display inline, or '2' to write to a file."
If the user chooses option 2 (write to file), ask a follow-up question: "Enter output file path (e.g., /tmp/thread.json or ~/thread.json):"
Set output_path to the entered path, or set use_stdout=true if option 1 was chosen.
If output_path was already provided in the arguments (Step 1), skip this step entirely.
Step 7: Fetch Messages
Build the narrow parameter, URL-encode it, and call the Zulip REST API.
narrow=$(python3 -c "
import json, sys
print(json.dumps([
{'operator': 'stream', 'operand': sys.argv[1]},
{'operator': 'topic', 'operand': sys.argv[2]}
]))
" "$stream_name" "$topic")
narrow_encoded=$(python3 -c "
from urllib.parse import quote
import sys
print(quote(sys.argv[1]))
" "$narrow")
anchor="${near_id:-newest}"
echo "Calling Zulip API..."
response=$(curl -s --user "$email:$api_key" \
"${site}/api/v1/messages?anchor=${anchor}&num_before=5000&num_after=0&narrow=${narrow_encoded}")
api_result=$(echo "$response" | jq -r '.result' 2>/dev/null)
if [ "$api_result" != "success" ]; then
api_msg=$(echo "$response" | jq -r '.msg' 2>/dev/null || echo "Unknown error")
echo "Error: Zulip API returned error: $api_msg"
exit 1
fi
msg_count=$(echo "$response" | jq '.messages | length' 2>/dev/null || echo 0)
echo "Fetched $msg_count messages."
Step 8: Format and Write Output
Extract sender, date, and content from the API response and write to the destination.
Format each message as a JSON object with three fields: sender (full name), date (ISO 8601 date string from Unix timestamp), and content (raw HTML).
formatted=$(echo "$response" | jq '[.messages[] | {
sender: .sender_full_name,
date: (.timestamp | todate),
content: .content
}]')
If use_stdout is true, or if output_path is still empty after Step 6, print $formatted directly to stdout.
Otherwise write to file:
mkdir -p "$(dirname "$output_path")"
echo "$formatted" > "$output_path"
Step 9: Report Result
Print a completion summary. If output was written to a file:
echo ""
echo "Thread written to: $output_path"
echo "Messages: $msg_count"
echo "Stream: $stream_name"
echo "Topic: $topic"
If output was printed to stdout:
echo ""
echo "---"
echo "Stream: $stream_name | Topic: $topic | Messages: $msg_count"
Error Handling
- ~/.zuliprc missing: Print setup instructions, exit 1
- Required tool missing (curl/jq/python3): Print tool name, exit 1
- URL missing or malformed: Print usage example, exit 1
- No topic segment in URL: Print error explaining a thread URL is needed, exit 1
- API returns non-success: Print the API error message, exit 1
- Empty response: jq safely returns empty array
[] — this is valid output
- Output directory missing:
mkdir -p $(dirname "$output_path") before writing