Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 66 additions & 29 deletions src/search_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
This is used when running `claude-search` from the command line.
"""

import sys
import argparse

# Handle both package and direct execution imports
try:
Expand All @@ -18,86 +18,123 @@
from extract_claude_logs import ClaudeConversationExtractor


def parse_args(argv=None):
"""Parse command-line arguments."""
parser = argparse.ArgumentParser(
prog="claude-search",
description="Search Claude Code conversation transcripts.",
)
parser.add_argument(
"words",
nargs="*",
default=[],
help="Search terms (joined with spaces)",
)
parser.add_argument(
"-n", "--no-interactive",
action="store_true",
default=False,
help="Print results and exit without interactive prompts",
)
parser.add_argument(
"-m", "--max-results",
type=int,
default=20,
help="Maximum number of results to return (default: 20)",
)

args = parser.parse_args(argv)
args.search_term = " ".join(args.words)
return args


def main():
"""Main entry point for CLI search."""
# Get search term from stdin or arguments
if len(sys.argv) > 1:
# Search term provided as argument
search_term = " ".join(sys.argv[1:])
else:
# Prompt for search term
args = parse_args()

search_term = args.search_term

# If no search term from args, prompt interactively
if not search_term:
if args.no_interactive:
print("❌ No search term provided")
return
try:
search_term = input("🔍 Enter search term: ").strip()
except (EOFError, KeyboardInterrupt):
print("\n👋 Search cancelled")
return

if not search_term:
print("❌ No search term provided")
return

print(f"\n🔍 Searching for: '{search_term}'")
print("=" * 60)

# Initialize searcher
searcher = ConversationSearcher()
smart_searcher = create_smart_searcher(searcher)

# Perform search
results = smart_searcher.search(search_term, max_results=20)
results = smart_searcher.search(search_term, max_results=args.max_results)

if results:
print(f"\n✅ Found {len(results)} results across conversations:\n")

# Group by file
by_file = {}
for result in results:
fname = result.file_path.name
if fname not in by_file:
by_file[fname] = []
by_file[fname].append(result)

# Display results
sessions = []
session_paths = []
extractor = ClaudeConversationExtractor()
all_sessions = extractor.find_sessions()

for i, (fname, file_results) in enumerate(by_file.items(), 1):
session_id = fname.replace('.jsonl', '')
sessions.append((fname, session_id))

# Find the actual file path
for session_path in all_sessions:
if session_path.name == fname:
session_paths.append(session_path)
break

print(f"{i}. Session {session_id[:8]}... ({len(file_results)} matches)")

# Show first match preview
first = file_results[0]
preview = first.matched_content[:150].replace('\n', ' ')
print(f" {first.speaker}: {preview}...")
print()


# In non-interactive mode, stop here
if args.no_interactive:
return

# Offer to view or extract conversations
if session_paths:
print("\n" + "=" * 60)
print("Options:")
print(" V. VIEW a conversation")
print(" E. EXTRACT all conversations")
print(" Q. QUIT")

try:
choice = input("\nYour choice: ").strip().upper()

if choice == 'V':
# View conversation
if len(session_paths) == 1:
# Only one result, view it directly
extractor.display_conversation(session_paths[0])

# After viewing, offer to extract
extract_choice = input("\n📤 Extract this conversation? (y/N): ").strip().lower()
if extract_choice == 'y':
Expand All @@ -110,12 +147,12 @@ def main():
print("\nSelect conversation to view:")
for i, (fname, sid) in enumerate(sessions, 1):
print(f" {i}. {sid[:8]}...")

try:
view_num = int(input("\nEnter number (1-{}): ".format(len(sessions))))
if 1 <= view_num <= len(session_paths):
extractor.display_conversation(session_paths[view_num - 1])

# After viewing, offer to extract
extract_choice = input("\n📤 Extract this conversation? (y/N): ").strip().lower()
if extract_choice == 'y':
Expand All @@ -125,7 +162,7 @@ def main():
print(f"✅ Saved: {output.name}")
except (ValueError, IndexError):
print("❌ Invalid selection")

elif choice == 'E':
# Extract all found conversations
for i, (session_path, (fname, sid)) in enumerate(zip(session_paths, sessions), 1):
Expand All @@ -134,10 +171,10 @@ def main():
if conversation:
output = extractor.save_as_markdown(conversation, sid)
print(f"✅ Saved: {output.name}")

elif choice == 'Q':
print("\n👋 Goodbye!")

except (EOFError, KeyboardInterrupt):
print("\n👋 Search cancelled")
else:
Expand All @@ -149,4 +186,4 @@ def main():


if __name__ == "__main__":
main()
main()
Loading