mirror of
https://github.com/zenlm/claude-collector.git
synced 2026-07-26 22:30:45 +00:00
Initial commit: Claude Collector v0.1.0
- Extract agent conversations - Auto-sanitize PII - Token counting - Training dataset export - Works with uvx/uv tool
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Hanzo AI
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,277 @@
|
||||
# 🤖 Claude Collector
|
||||
|
||||
**One command to extract all your Claude Code conversations for training datasets.**
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Install and run with uvx (recommended)
|
||||
|
||||
```bash
|
||||
uvx claude-collector
|
||||
```
|
||||
|
||||
That's it! The tool will:
|
||||
- ✅ Auto-find your Claude Code data (`~/.claude/projects`)
|
||||
- ✅ Extract all conversations
|
||||
- ✅ Sanitize PII (emails, API keys, paths)
|
||||
- ✅ Count total tokens
|
||||
- ✅ Save as training-ready JSONL
|
||||
|
||||
### Or install globally
|
||||
|
||||
```bash
|
||||
uv tool install claude-collector
|
||||
claude-collector
|
||||
```
|
||||
|
||||
## What It Does
|
||||
|
||||
Scans your Claude Code session files and:
|
||||
|
||||
1. **Finds** all conversation data in `~/.claude/projects`
|
||||
2. **Extracts** user/assistant message pairs
|
||||
3. **Sanitizes** sensitive information:
|
||||
- Emails → `[EMAIL]`
|
||||
- API keys → `[API_KEY]`
|
||||
- File paths → `/Users/[USER]/...`
|
||||
- IP addresses → `[IP]`
|
||||
- OAuth tokens → `[REDACTED]`
|
||||
4. **Counts** actual token usage
|
||||
5. **Saves** as clean JSONL dataset
|
||||
|
||||
## Example Output
|
||||
|
||||
```
|
||||
🤖 Claude Collector v0.1.0
|
||||
Extract & sanitize Claude Code conversations
|
||||
|
||||
✓ Found Claude data: /Users/z/.claude/projects
|
||||
|
||||
📂 Processing 1394 files...
|
||||
|
||||
╭─────────────────────┬──────────────╮
|
||||
│ Metric │ Value │
|
||||
├─────────────────────┼──────────────┤
|
||||
│ Files scanned │ 1,394 │
|
||||
│ Files with data │ 1,273 │
|
||||
│ Total messages │ 46,029 │
|
||||
│ Training examples │ 3,653 │
|
||||
│ Total tokens │ 4.04B │
|
||||
╰─────────────────────┴──────────────╯
|
||||
|
||||
✅ Dataset saved!
|
||||
File: claude_dataset_20251113.jsonl
|
||||
Size: 19.13 MB
|
||||
Examples: 3,653
|
||||
|
||||
🎉 Ready for training!
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
```bash
|
||||
# Dry run (see stats without saving)
|
||||
uvx claude-collector --dry-run
|
||||
|
||||
# Custom output location
|
||||
uvx claude-collector --output ~/my-dataset.jsonl
|
||||
|
||||
# Specify input directory
|
||||
uvx claude-collector --input ~/.config/claude/projects
|
||||
|
||||
# Filter by minimum tokens
|
||||
uvx claude-collector --min-tokens 1000
|
||||
|
||||
# Skip sanitization (NOT recommended for sharing!)
|
||||
uvx claude-collector --no-sanitize
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
### 1. Create Training Dataset
|
||||
|
||||
```bash
|
||||
uvx claude-collector --output training-data.jsonl
|
||||
```
|
||||
|
||||
### 2. Audit Your Usage
|
||||
|
||||
```bash
|
||||
uvx claude-collector --dry-run
|
||||
```
|
||||
|
||||
Shows total tokens without saving.
|
||||
|
||||
### 3. Collect from Multiple Machines
|
||||
|
||||
On each computer:
|
||||
|
||||
```bash
|
||||
# Machine 1
|
||||
uvx claude-collector --output machine1-data.jsonl
|
||||
|
||||
# Machine 2
|
||||
uvx claude-collector --output machine2-data.jsonl
|
||||
|
||||
# Combine later
|
||||
cat machine1-data.jsonl machine2-data.jsonl > combined-dataset.jsonl
|
||||
```
|
||||
|
||||
### 4. Add to Existing Dataset
|
||||
|
||||
```bash
|
||||
uvx claude-collector --output new-sessions.jsonl
|
||||
cat existing-dataset.jsonl new-sessions.jsonl > updated-dataset.jsonl
|
||||
```
|
||||
|
||||
## Output Format
|
||||
|
||||
Each line is a JSON object:
|
||||
|
||||
```json
|
||||
{
|
||||
"messages": [
|
||||
{"role": "user", "content": "How do I..."},
|
||||
{"role": "assistant", "content": "You can..."}
|
||||
],
|
||||
"metadata": {
|
||||
"timestamp": "2025-11-13T...",
|
||||
"tokens": {
|
||||
"input_tokens": 100,
|
||||
"output_tokens": 200,
|
||||
"cache_creation_input_tokens": 5000,
|
||||
"cache_read_input_tokens": 1000
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Perfect for:
|
||||
- Fine-tuning LLMs
|
||||
- Training coding assistants
|
||||
- Building instruction datasets
|
||||
- Analysis and research
|
||||
|
||||
## Finding Claude Data
|
||||
|
||||
### Default Locations
|
||||
|
||||
```bash
|
||||
~/.claude/projects/ # Primary
|
||||
~/.config/claude/projects/ # Alternative
|
||||
```
|
||||
|
||||
### Check All Users
|
||||
|
||||
```bash
|
||||
ls -la /Users/*/.claude/projects # macOS
|
||||
ls -la /home/*/.claude/projects # Linux
|
||||
```
|
||||
|
||||
### Find Anywhere
|
||||
|
||||
```bash
|
||||
find ~ -name "*.jsonl" -path "*/.claude/*" 2>/dev/null
|
||||
```
|
||||
|
||||
## Privacy & Security
|
||||
|
||||
⚠️ **Important**: Claude Code logs contain sensitive data!
|
||||
|
||||
**The tool sanitizes:**
|
||||
- ✅ Email addresses
|
||||
- ✅ API keys and tokens
|
||||
- ✅ File paths (username removed)
|
||||
- ✅ IP addresses
|
||||
- ✅ OAuth credentials
|
||||
- ✅ Passwords
|
||||
|
||||
**Still check before sharing:**
|
||||
- Project names (if sensitive)
|
||||
- Company-specific terminology
|
||||
- Proprietary code patterns
|
||||
|
||||
For maximum privacy, review the output file before uploading anywhere.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.8+
|
||||
- Claude Code installed (for data to exist)
|
||||
|
||||
## Installation Methods
|
||||
|
||||
### 1. uvx (easiest, no install)
|
||||
```bash
|
||||
uvx claude-collector
|
||||
```
|
||||
|
||||
### 2. uv tool (global install)
|
||||
```bash
|
||||
uv tool install claude-collector
|
||||
claude-collector
|
||||
```
|
||||
|
||||
### 3. pip
|
||||
```bash
|
||||
pip install claude-collector
|
||||
claude-collector
|
||||
```
|
||||
|
||||
### 4. From source
|
||||
```bash
|
||||
git clone https://github.com/hanzoai/claude-collector
|
||||
cd claude-collector
|
||||
uv pip install -e .
|
||||
claude-collector
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**"No Claude Code data found"**
|
||||
- Make sure Claude Code is installed
|
||||
- Check you've had at least one session
|
||||
- Try specifying path: `--input ~/.claude/projects`
|
||||
|
||||
**"Only found a few conversations"**
|
||||
- This is normal if you're new to Claude Code
|
||||
- Each session creates one file
|
||||
- More usage = more data
|
||||
|
||||
**"Tokens show 0"**
|
||||
- Some messages don't have usage tracking
|
||||
- This is normal for system messages
|
||||
- Real conversations will have token counts
|
||||
|
||||
## Advanced: Custom Processing
|
||||
|
||||
```python
|
||||
import json
|
||||
|
||||
# Read dataset
|
||||
with open('claude_dataset.jsonl', 'r') as f:
|
||||
for line in f:
|
||||
example = json.loads(line)
|
||||
|
||||
# Access messages
|
||||
user_msg = example['messages'][0]['content']
|
||||
assistant_msg = example['messages'][1]['content']
|
||||
|
||||
# Access metadata
|
||||
tokens = example['metadata']['tokens']
|
||||
timestamp = example['metadata']['timestamp']
|
||||
|
||||
# Your custom processing here
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT - Free to use for any purpose
|
||||
|
||||
## Credits
|
||||
|
||||
Built by [Hanzo AI](https://hanzo.ai) for the AI development community.
|
||||
|
||||
---
|
||||
|
||||
**Found a bug?** Open an issue: https://github.com/hanzoai/claude-collector/issues
|
||||
**Want to contribute?** PRs welcome!
|
||||
@@ -0,0 +1,2 @@
|
||||
"""Claude Collector - Extract Claude Code conversations for training datasets"""
|
||||
__version__ = "0.1.0"
|
||||
@@ -0,0 +1,257 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Claude Collector CLI
|
||||
Extract and sanitize Claude Code conversations
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
import click
|
||||
from rich.console import Console
|
||||
from rich.progress import track
|
||||
from rich.table import Table
|
||||
from rich import print as rprint
|
||||
|
||||
console = Console()
|
||||
|
||||
def sanitize_text(text):
|
||||
"""Remove PII from text"""
|
||||
if not isinstance(text, str):
|
||||
return str(text)
|
||||
|
||||
replacements = [
|
||||
(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b', '[EMAIL]'),
|
||||
(r'sk-[a-zA-Z0-9]{48}', '[API_KEY]'),
|
||||
(r'xox[baprs]-[a-zA-Z0-9-]+', '[SLACK_TOKEN]'),
|
||||
(r'/Users/[^/\s]+', '/Users/[USER]'),
|
||||
(r'/home/[^/\s]+', '/home/[USER]'),
|
||||
(r'C:\\Users\\[^\\]+', 'C:\\Users\\[USER]'),
|
||||
(r'\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b', '[IP]'),
|
||||
(r'oauth[_-]?token[_-]?secret?["\']?\s*[:=]\s*["\']?[a-f0-9:]+', 'oauth_token=[REDACTED]'),
|
||||
(r'(password|passwd|pwd)["\']?\s*[:=]\s*["\'][^"\']+["\']', r'\1=[REDACTED]'),
|
||||
]
|
||||
|
||||
for pattern, replacement in replacements:
|
||||
text = re.sub(pattern, replacement, text, flags=re.IGNORECASE)
|
||||
|
||||
return text
|
||||
|
||||
def extract_text_content(content):
|
||||
"""Extract text from content (string or array)"""
|
||||
if isinstance(content, str):
|
||||
return content
|
||||
if isinstance(content, list):
|
||||
texts = []
|
||||
for item in content:
|
||||
if isinstance(item, dict):
|
||||
if item.get('type') == 'text':
|
||||
texts.append(item.get('text', ''))
|
||||
elif 'text' in item:
|
||||
texts.append(item['text'])
|
||||
elif isinstance(item, str):
|
||||
texts.append(item)
|
||||
return '\n'.join(filter(None, texts))
|
||||
return str(content)
|
||||
|
||||
def process_jsonl_file(file_path):
|
||||
"""Extract conversations from a JSONL file"""
|
||||
messages = []
|
||||
total_tokens = 0
|
||||
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
for line in f:
|
||||
if not line.strip():
|
||||
continue
|
||||
|
||||
try:
|
||||
data = json.loads(line)
|
||||
|
||||
# Only process actual user/assistant messages
|
||||
if data.get('type') not in ['user', 'assistant']:
|
||||
continue
|
||||
|
||||
msg = data.get('message', {})
|
||||
if not msg:
|
||||
continue
|
||||
|
||||
# Extract content
|
||||
content = extract_text_content(msg.get('content', ''))
|
||||
if not content or len(content) < 10:
|
||||
continue
|
||||
|
||||
# Get usage data (in message.usage or top-level)
|
||||
usage = msg.get('usage') or data.get('usage') or {}
|
||||
|
||||
token_count = (
|
||||
usage.get('input_tokens', 0) +
|
||||
usage.get('output_tokens', 0) +
|
||||
usage.get('cache_creation_input_tokens', 0) +
|
||||
usage.get('cache_read_input_tokens', 0)
|
||||
)
|
||||
|
||||
total_tokens += token_count
|
||||
|
||||
messages.append({
|
||||
'role': data['type'],
|
||||
'content': sanitize_text(content),
|
||||
'timestamp': data.get('timestamp'),
|
||||
'tokens': token_count,
|
||||
'usage': usage
|
||||
})
|
||||
|
||||
except (json.JSONDecodeError, KeyError, TypeError):
|
||||
continue
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return messages, total_tokens
|
||||
|
||||
def find_claude_directories():
|
||||
"""Find all Claude Code data directories"""
|
||||
locations = []
|
||||
home = Path.home()
|
||||
|
||||
# Check common locations
|
||||
candidates = [
|
||||
home / '.claude' / 'projects',
|
||||
home / '.config' / 'claude' / 'projects',
|
||||
]
|
||||
|
||||
for loc in candidates:
|
||||
if loc.exists() and loc.is_dir():
|
||||
locations.append(loc)
|
||||
|
||||
return locations
|
||||
|
||||
@click.command()
|
||||
@click.option('--input', '-i', 'input_dir', type=click.Path(exists=True),
|
||||
help='Input directory (auto-detects ~/.claude/projects if not specified)')
|
||||
@click.option('--output', '-o', 'output_file', type=click.Path(),
|
||||
help='Output file (default: claude_dataset_YYYYMMDD.jsonl)')
|
||||
@click.option('--dry-run', is_flag=True, help='Show stats without saving')
|
||||
@click.option('--no-sanitize', is_flag=True, help='Skip PII sanitization (NOT RECOMMENDED)')
|
||||
@click.option('--min-tokens', type=int, default=0, help='Minimum tokens per conversation')
|
||||
def main(input_dir, output_file, dry_run, no_sanitize, min_tokens):
|
||||
"""
|
||||
🤖 Claude Collector - Extract Claude Code conversations
|
||||
|
||||
Extracts and sanitizes your Claude Code conversation history
|
||||
into a training dataset.
|
||||
|
||||
\b
|
||||
Quick start:
|
||||
uvx claude-collector
|
||||
|
||||
\b
|
||||
Custom output:
|
||||
uvx claude-collector --output my-dataset.jsonl
|
||||
|
||||
\b
|
||||
Check what you have:
|
||||
uvx claude-collector --dry-run
|
||||
"""
|
||||
|
||||
console.print("\n[bold cyan]🤖 Claude Collector v0.1.0[/bold cyan]")
|
||||
console.print("[dim]Extract & sanitize Claude Code conversations[/dim]\n")
|
||||
|
||||
# Find Claude directories
|
||||
if input_dir:
|
||||
dirs = [Path(input_dir)]
|
||||
else:
|
||||
dirs = find_claude_directories()
|
||||
|
||||
if not dirs:
|
||||
console.print("[red]❌ No Claude Code data found![/red]")
|
||||
console.print("\nChecked:")
|
||||
console.print(" • ~/.claude/projects")
|
||||
console.print(" • ~/.config/claude/projects")
|
||||
console.print("\nSpecify manually: --input /path/to/projects")
|
||||
return
|
||||
|
||||
console.print(f"[green]✓[/green] Found Claude data: {dirs[0]}\n")
|
||||
|
||||
# Find all JSONL files
|
||||
all_files = []
|
||||
for directory in dirs:
|
||||
all_files.extend(directory.rglob('*.jsonl'))
|
||||
|
||||
console.print(f"[cyan]📂 Processing {len(all_files)} files...[/cyan]\n")
|
||||
|
||||
# Process files
|
||||
all_messages = []
|
||||
grand_total_tokens = 0
|
||||
files_with_data = 0
|
||||
|
||||
for file_path in track(all_files, description="Extracting"):
|
||||
messages, tokens = process_jsonl_file(file_path)
|
||||
if messages:
|
||||
all_messages.extend(messages)
|
||||
grand_total_tokens += tokens
|
||||
files_with_data += 1
|
||||
|
||||
# Create training examples (user + assistant pairs)
|
||||
examples = []
|
||||
i = 0
|
||||
while i < len(all_messages) - 1:
|
||||
if all_messages[i]['role'] == 'user' and all_messages[i+1]['role'] == 'assistant':
|
||||
example_tokens = all_messages[i+1]['tokens']
|
||||
|
||||
if example_tokens >= min_tokens:
|
||||
examples.append({
|
||||
'messages': [
|
||||
{'role': 'user', 'content': all_messages[i]['content']},
|
||||
{'role': 'assistant', 'content': all_messages[i+1]['content']}
|
||||
],
|
||||
'metadata': {
|
||||
'timestamp': all_messages[i]['timestamp'],
|
||||
'tokens': all_messages[i+1]['usage']
|
||||
}
|
||||
})
|
||||
i += 2
|
||||
else:
|
||||
i += 1
|
||||
|
||||
# Display results
|
||||
table = Table(title="Extraction Results")
|
||||
table.add_column("Metric", style="cyan")
|
||||
table.add_column("Value", style="green")
|
||||
|
||||
table.add_row("Files scanned", f"{len(all_files):,}")
|
||||
table.add_row("Files with data", f"{files_with_data:,}")
|
||||
table.add_row("Total messages", f"{len(all_messages):,}")
|
||||
table.add_row("Training examples", f"{len(examples):,}")
|
||||
table.add_row("Total tokens", f"{grand_total_tokens:,} ({grand_total_tokens/1e9:.2f}B)")
|
||||
|
||||
console.print()
|
||||
console.print(table)
|
||||
console.print()
|
||||
|
||||
if dry_run:
|
||||
console.print("[yellow]🔍 DRY RUN - No files saved[/yellow]")
|
||||
return
|
||||
|
||||
# Save dataset
|
||||
if not output_file:
|
||||
output_file = f"claude_dataset_{datetime.now().strftime('%Y%m%d_%H%M')}.jsonl"
|
||||
|
||||
output_path = Path(output_file)
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with open(output_path, 'w') as f:
|
||||
for example in examples:
|
||||
f.write(json.dumps(example) + '\n')
|
||||
|
||||
size_mb = output_path.stat().st_size / 1024 / 1024
|
||||
|
||||
console.print(f"[green]✅ Dataset saved![/green]")
|
||||
console.print(f" File: [cyan]{output_path.name}[/cyan]")
|
||||
console.print(f" Size: [cyan]{size_mb:.2f} MB[/cyan]")
|
||||
console.print(f" Examples: [cyan]{len(examples):,}[/cyan]")
|
||||
console.print(f"\n[bold green]🎉 Ready for training![/bold green]\n")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,38 @@
|
||||
[project]
|
||||
name = "claude-collector"
|
||||
version = "0.1.0"
|
||||
description = "Extract and sanitize Claude Code conversation data for training datasets"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.8"
|
||||
license = {text = "MIT"}
|
||||
authors = [
|
||||
{name = "Hanzo AI", email = "hello@hanzo.ai"}
|
||||
]
|
||||
keywords = ["claude", "ai", "dataset", "training", "llm"]
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
]
|
||||
|
||||
dependencies = [
|
||||
"rich>=13.0.0",
|
||||
"click>=8.0.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
claude-collector = "claude_collector.cli:main"
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/hanzoai/claude-collector"
|
||||
Repository = "https://github.com/hanzoai/claude-collector"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
Reference in New Issue
Block a user