|
#!/bin/bash |
|
|
|
# Claude Session Search Script |
|
# Searches through Claude session JSONL files for content matching a regex pattern |
|
|
|
# Color output |
|
RED='\033[0;31m' |
|
GREEN='\033[0;32m' |
|
YELLOW='\033[1;33m' |
|
BLUE='\033[0;34m' |
|
NC='\033[0m' # No Color |
|
|
|
# Default values |
|
CLAUDE_DIR="/home/vscode/.claude/projects" |
|
SEARCH_ALL_PROJECTS=false |
|
PROJECT_PATH="-workspaces-substrate" |
|
SHOW_CONTEXT=false |
|
CONTEXT_LINES=2 |
|
|
|
# Usage function |
|
usage() { |
|
echo "Usage: $0 [OPTIONS] PATTERN" |
|
echo "" |
|
echo "Search Claude session files for content matching a regex pattern" |
|
echo "" |
|
echo "OPTIONS:" |
|
echo " -a, --all Search all projects (default: current project only)" |
|
echo " -p, --project PATH Specify project path (default: -workspaces-substrate)" |
|
echo " -c, --context Show context lines around matches" |
|
echo " -n, --lines NUM Number of context lines (default: 2)" |
|
echo " -l, --list-only List only matching session files" |
|
echo " -h, --help Show this help message" |
|
echo "" |
|
echo "EXAMPLES:" |
|
echo " $0 'query.*system' # Search for 'query' followed by 'system'" |
|
echo " $0 -a 'import.*pandas' # Search all projects" |
|
echo " $0 -c -n 5 'error|exception' # Show 5 lines of context" |
|
echo " $0 -l 'TODO|FIXME' # List files with TODOs or FIXMEs" |
|
} |
|
|
|
# Parse arguments |
|
LIST_ONLY=false |
|
|
|
while [[ $# -gt 0 ]]; do |
|
case $1 in |
|
-a|--all) |
|
SEARCH_ALL_PROJECTS=true |
|
shift |
|
;; |
|
-p|--project) |
|
PROJECT_PATH="$2" |
|
shift 2 |
|
;; |
|
-c|--context) |
|
SHOW_CONTEXT=true |
|
shift |
|
;; |
|
-n|--lines) |
|
CONTEXT_LINES="$2" |
|
shift 2 |
|
;; |
|
-l|--list-only) |
|
LIST_ONLY=true |
|
shift |
|
;; |
|
-h|--help) |
|
usage |
|
exit 0 |
|
;; |
|
*) |
|
PATTERN="$1" |
|
shift |
|
;; |
|
esac |
|
done |
|
|
|
# Check if pattern is provided |
|
if [[ -z "$PATTERN" ]]; then |
|
echo -e "${RED}Error: No search pattern provided${NC}" |
|
usage |
|
exit 1 |
|
fi |
|
|
|
# Function to extract and format session info |
|
extract_session_info() { |
|
local file="$1" |
|
local session_id=$(basename "$file" .jsonl) |
|
local summary=$(head -1 "$file" 2>/dev/null | jq -r '.summary // "No summary"' 2>/dev/null || echo "No summary") |
|
local first_timestamp=$(grep -m1 '"timestamp"' "$file" | jq -r '.timestamp // "Unknown"' 2>/dev/null || echo "Unknown") |
|
|
|
# Extract first user message |
|
local first_user_msg="" |
|
while IFS= read -r line; do |
|
if echo "$line" | jq -e '.message.role == "user"' >/dev/null 2>&1; then |
|
first_user_msg=$(echo "$line" | jq -r '.message.content' 2>/dev/null | head -c 200) |
|
if [[ ${#first_user_msg} -eq 200 ]]; then |
|
first_user_msg="${first_user_msg}..." |
|
fi |
|
break |
|
fi |
|
done < "$file" |
|
|
|
echo -e "${BLUE}Session: ${YELLOW}$session_id${NC}" |
|
echo -e "${GREEN}Summary:${NC} $summary" |
|
echo -e "${GREEN}Started:${NC} $first_timestamp" |
|
if [[ -n "$first_user_msg" ]]; then |
|
echo -e "${GREEN}First message:${NC} $first_user_msg" |
|
fi |
|
} |
|
|
|
# Function to search in a single file |
|
search_file() { |
|
local file="$1" |
|
local matches=0 |
|
|
|
if [[ "$LIST_ONLY" == true ]]; then |
|
if grep -q "$PATTERN" "$file"; then |
|
extract_session_info "$file" |
|
echo -e "${GREEN}File:${NC} $file" |
|
echo "" |
|
return 0 |
|
fi |
|
return 1 |
|
fi |
|
|
|
# Search for user messages and assistant responses |
|
if [[ "$SHOW_CONTEXT" == true ]]; then |
|
# Extract matches with context |
|
local line_num=0 |
|
local found_match=false |
|
|
|
while IFS= read -r line; do |
|
((line_num++)) |
|
|
|
# Check if line contains message content |
|
if echo "$line" | jq -e '.message.content' >/dev/null 2>&1; then |
|
local content=$(echo "$line" | jq -r '.message.content' 2>/dev/null) |
|
local role=$(echo "$line" | jq -r '.message.role // .userType // "unknown"' 2>/dev/null) |
|
|
|
if echo "$content" | grep -E "$PATTERN" >/dev/null 2>&1; then |
|
if [[ "$found_match" == false ]]; then |
|
extract_session_info "$file" |
|
echo "" |
|
found_match=true |
|
fi |
|
|
|
echo -e "${GREEN}Line $line_num (${role}):${NC}" |
|
|
|
# Show context lines before |
|
if [[ $CONTEXT_LINES -gt 0 ]]; then |
|
local start=$((line_num - CONTEXT_LINES)) |
|
[[ $start -lt 1 ]] && start=1 |
|
sed -n "${start},$((line_num-1))p" "$file" | while IFS= read -r ctx_line; do |
|
local ctx_content=$(echo "$ctx_line" | jq -r '.message.content // .summary // "..."' 2>/dev/null | head -1) |
|
echo -e " ${BLUE}...${NC} ${ctx_content:0:80}" |
|
done |
|
fi |
|
|
|
# Highlight the match |
|
echo "$content" | grep -E --color=always "$PATTERN" |
|
|
|
# Show context lines after |
|
if [[ $CONTEXT_LINES -gt 0 ]]; then |
|
sed -n "$((line_num+1)),$((line_num+CONTEXT_LINES))p" "$file" | while IFS= read -r ctx_line; do |
|
local ctx_content=$(echo "$ctx_line" | jq -r '.message.content // .summary // "..."' 2>/dev/null | head -1) |
|
echo -e " ${BLUE}...${NC} ${ctx_content:0:80}" |
|
done |
|
fi |
|
|
|
echo "" |
|
((matches++)) |
|
fi |
|
fi |
|
done < "$file" |
|
|
|
else |
|
# Simple search without context |
|
local line_num=0 |
|
local found_match=false |
|
|
|
while IFS= read -r line; do |
|
((line_num++)) |
|
|
|
# Try to extract content from various fields |
|
local content="" |
|
local role="unknown" |
|
|
|
# Try message.content first |
|
if echo "$line" | jq -e '.message.content' >/dev/null 2>&1; then |
|
content=$(echo "$line" | jq -r '.message.content' 2>/dev/null) |
|
role=$(echo "$line" | jq -r '.message.role // .userType // "unknown"' 2>/dev/null) |
|
# Try message.content array (for structured messages) |
|
elif echo "$line" | jq -e '.message.content[0].text' >/dev/null 2>&1; then |
|
content=$(echo "$line" | jq -r '.message.content[0].text' 2>/dev/null) |
|
role=$(echo "$line" | jq -r '.message.role // .userType // "unknown"' 2>/dev/null) |
|
# Try summary field |
|
elif echo "$line" | jq -e '.summary' >/dev/null 2>&1; then |
|
content=$(echo "$line" | jq -r '.summary' 2>/dev/null) |
|
role="summary" |
|
fi |
|
|
|
if [[ -n "$content" ]] && echo "$content" | grep -E "$PATTERN" >/dev/null 2>&1; then |
|
if [[ "$found_match" == false ]]; then |
|
extract_session_info "$file" |
|
echo "" |
|
found_match=true |
|
fi |
|
|
|
echo -e "${GREEN}Line $line_num (${role}):${NC}" |
|
echo "$content" | grep -E --color=always "$PATTERN" |
|
echo "" |
|
((matches++)) |
|
fi |
|
done < "$file" |
|
fi |
|
|
|
return $matches |
|
} |
|
|
|
# Main search logic |
|
echo -e "${BLUE}Searching for pattern:${NC} $PATTERN" |
|
echo "" |
|
|
|
total_matches=0 |
|
total_files=0 |
|
|
|
if [[ "$SEARCH_ALL_PROJECTS" == true ]]; then |
|
# Search all projects |
|
for project_dir in "$CLAUDE_DIR"/*; do |
|
if [[ -d "$project_dir" ]]; then |
|
echo -e "${YELLOW}Project: $(basename "$project_dir")${NC}" |
|
echo "" |
|
|
|
for session_file in "$project_dir"/*.jsonl; do |
|
if [[ -f "$session_file" ]]; then |
|
if search_file "$session_file"; then |
|
((total_files++)) |
|
fi |
|
fi |
|
done |
|
fi |
|
done |
|
else |
|
# Search specific project |
|
project_dir="$CLAUDE_DIR/$PROJECT_PATH" |
|
|
|
if [[ ! -d "$project_dir" ]]; then |
|
echo -e "${RED}Error: Project directory not found: $project_dir${NC}" |
|
exit 1 |
|
fi |
|
|
|
for session_file in "$project_dir"/*.jsonl; do |
|
if [[ -f "$session_file" ]]; then |
|
if search_file "$session_file"; then |
|
((total_files++)) |
|
fi |
|
fi |
|
done |
|
fi |
|
|
|
echo -e "${BLUE}Search complete.${NC}" |
|
if [[ "$LIST_ONLY" == true ]]; then |
|
echo -e "${GREEN}Found matches in $total_files session(s)${NC}" |
|
else |
|
echo -e "${GREEN}Found matches in $total_files session(s)${NC}" |
|
fi |