文件预览

patch_binary.py

查看 Clawd Modifier 技能包中的文件内容。

文件内容

scripts/patch_binary.py

#!/usr/bin/env python3
"""
Patch Clawd's appearance directly in the compiled Claude Code binary.

This script modifies specific byte sequences in the Bun-compiled binary
to change Clawd's ASCII art (e.g., adding arms, changing poses).

Usage:
    python patch_binary.py --variant excited
    python patch_binary.py --restore
    python patch_binary.py --list

WARNING: This modifies a compiled binary. Always backup first!
"""

import argparse
import shutil
import sys
from pathlib import Path
from datetime import datetime

# Default binary locations
DEFAULT_PATHS = [
    Path.home() / ".local/bin/claude",
    "/usr/local/bin/claude",
    "/opt/homebrew/bin/claude",
]

# Patch definitions
# Format: (search_bytes, replace_bytes, description)
# All strings are UTF-16LE encoded

PATCHES = {
    'excited': {
        'description': 'Clawd with arms up (excited pose) - LARGE startup Clawd',
        'patches': [
            # LARGE CLAWD (startup screen)
            # The body line: body"}," \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 ")
            # Change first \u2588 to \u2572 (left arm): 5c7532353838 -> 5c7532353732
            # Change last \u2588 to \u2571 (right arm): 5c7532353838 -> 5c7532353731
            {
                # Pattern: body"}," \u2588 (start of body line, first block)
                'search': b'body"}," \\u2588',
                'replace': b'body"}," \\u2572',  # ╲ left arm up
                'description': 'Large Clawd left arm up'
            },
            {
                # Pattern: \u2588 ") at end of body line (last block before space and close)
                'search': b'\\u2588 ")',
                'replace': b'\\u2571 ")',  # ╱ right arm up
                'description': 'Large Clawd right arm up'
            },
        ]
    },
    'excited-small': {
        'description': 'Clawd with arms up - SMALL prompt Clawd only',
        'patches': [
            # SMALL CLAWD (prompt icon) - UTF-16LE in constant pool
            {
                'search': b'\x20\x00\x90\x25',  # " ▐" in UTF-16LE
                'replace': b'\x72\x25\x90\x25', # "╲▐" in UTF-16LE
                'context': b'clawd_body',
                'max_distance': 50,
                'description': 'Small Clawd left arm up'
            },
        ]
    },
    'excited-both': {
        'description': 'Clawd with arms up - BOTH large and small',
        'patches': [
            # Large Clawd
            {
                'search': b'body"}," \\u2588',
                'replace': b'body"}," \\u2572',
                'description': 'Large Clawd left arm up'
            },
            {
                'search': b'\\u2588 ")',
                'replace': b'\\u2571 ")',
                'description': 'Large Clawd right arm up'
            },
            # Small Clawd
            {
                'search': b'\x20\x00\x90\x25',
                'replace': b'\x72\x25\x90\x25',
                'context': b'clawd_body',
                'max_distance': 50,
                'description': 'Small Clawd left arm up'
            },
        ]
    },
    'waving': {
        'description': 'Clawd waving (one arm up)',
        'patches': [
            {
                'search': b'\\u2588 ")',
                'replace': b'\\u2571 ")',
                'description': 'Large Clawd right arm waving'
            },
        ]
    },
    'original': {
        'description': 'Restore original Clawd',
        'patches': [
            # Restore large Clawd
            {
                'search': b'body"}," \\u2572',
                'replace': b'body"}," \\u2588',
                'description': 'Remove large Clawd left arm'
            },
            {
                'search': b'\\u2571 ")',
                'replace': b'\\u2588 ")',
                'description': 'Remove large Clawd right arm'
            },
            # Restore small Clawd
            {
                'search': b'\x72\x25\x90\x25',
                'replace': b'\x20\x00\x90\x25',
                'context': b'clawd_body',
                'max_distance': 50,
                'description': 'Remove small Clawd left arm'
            },
        ]
    }
}


def find_binary():
    """Find the Claude Code binary."""
    for path in DEFAULT_PATHS:
        if path.exists():
            return path
    return None


def backup_binary(binary_path: Path) -> Path:
    """Create a timestamped backup of the binary."""
    timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
    backup_path = binary_path.parent / f"{binary_path.name}.backup.{timestamp}"
    shutil.copy2(binary_path, backup_path)
    return backup_path


def find_context_matches(data: bytes, context: bytes, max_occurrences: int = 200) -> list:
    """Find all positions where context string appears."""
    positions = []
    start = 0
    while len(positions) < max_occurrences:
        pos = data.find(context, start)
        if pos == -1:
            break
        positions.append(pos)
        start = pos + 1
    return positions


def apply_patch(binary_path: Path, variant: str, dry_run: bool = False) -> dict:
    """Apply a patch variant to the binary."""
    if variant not in PATCHES:
        return {'success': False, 'error': f'Unknown variant: {variant}'}

    patch_def = PATCHES[variant]
    data = binary_path.read_bytes()

    total_patches = 0
    patch_details = []

    for patch in patch_def['patches']:
        if not patch.get('search') or not patch.get('replace'):
            continue

        search = patch['search']
        replace = patch['replace']
        context = patch.get('context', b'')
        max_dist = patch.get('max_distance', 100)

        if context:
            # Find patches only near context strings
            context_positions = find_context_matches(data, context)

            for ctx_pos in context_positions:
                # Search in the region after the context
                region_start = ctx_pos
                region_end = min(ctx_pos + max_dist + len(search), len(data))
                region = data[region_start:region_end]

                local_pos = region.find(search)
                if local_pos != -1:
                    abs_pos = region_start + local_pos
                    # Verify it's the right pattern
                    if data[abs_pos:abs_pos + len(search)] == search:
                        if not dry_run:
                            data = data[:abs_pos] + replace + data[abs_pos + len(search):]
                        total_patches += 1
                        patch_details.append({
                            'offset': abs_pos,
                            'description': patch.get('description', 'Unknown'),
                            'context_offset': ctx_pos
                        })
        else:
            # Global search and replace
            count = data.count(search)
            if count > 0:
                if not dry_run:
                    data = data.replace(search, replace)
                total_patches += count
                patch_details.append({
                    'count': count,
                    'description': patch.get('description', 'Unknown')
                })

    if total_patches == 0:
        return {
            'success': False,
            'error': 'No matching patterns found (already patched or different binary version?)'
        }

    if not dry_run:
        binary_path.write_bytes(data)

    return {
        'success': True,
        'patches_applied': total_patches,
        'details': patch_details,
        'dry_run': dry_run,
        'variant': variant,
        'description': patch_def['description']
    }


def main():
    parser = argparse.ArgumentParser(
        description="Patch Clawd's appearance in compiled Claude Code binary"
    )
    parser.add_argument('--variant', '-v', help='Variant to apply (excited, waving, original)')
    parser.add_argument('--binary', '-b', help='Path to Claude binary')
    parser.add_argument('--list', '-l', action='store_true', help='List available variants')
    parser.add_argument('--dry-run', '-n', action='store_true', help='Show what would be changed')
    parser.add_argument('--no-backup', action='store_true', help='Skip creating backup')
    parser.add_argument('--restore', '-r', action='store_true', help='Restore to original')

    args = parser.parse_args()

    if args.list:
        print("Available Clawd variants:")
        for name, info in PATCHES.items():
            patches_count = len([p for p in info.get('patches', []) if p.get('search')])
            print(f"  {name:15} {info['description']} ({patches_count} patches)")
        return

    if args.restore:
        args.variant = 'original'

    if not args.variant:
        parser.print_help()
        return

    # Find binary
    binary_path = Path(args.binary) if args.binary else find_binary()
    if not binary_path or not binary_path.exists():
        print(f"Error: Claude binary not found. Use --binary to specify path.", file=sys.stderr)
        sys.exit(1)

    print(f"Binary: {binary_path}")

    # Backup
    if not args.no_backup and not args.dry_run:
        backup = backup_binary(binary_path)
        print(f"Backup: {backup}")

    # Apply patch
    result = apply_patch(binary_path, args.variant, dry_run=args.dry_run)

    if result['success']:
        action = "Would apply" if args.dry_run else "Applied"
        print(f"\n{action} variant '{result['variant']}': {result['description']}")
        print(f"Total patches: {result['patches_applied']}")

        if result.get('details'):
            print("\nDetails:")
            for detail in result['details']:
                if 'offset' in detail:
                    print(f"  - {detail['description']} at offset {detail['offset']}")
                elif 'count' in detail:
                    print(f"  - {detail['description']}: {detail['count']} occurrences")

        if not args.dry_run:
            print("\nRestart Claude Code to see changes.")
    else:
        print(f"Error: {result['error']}", file=sys.stderr)
        sys.exit(1)


if __name__ == "__main__":
    main()