From b20748834c1e848b0154611fa86372d49be32369 Mon Sep 17 00:00:00 2001 From: Arthur Chiu Date: Sun, 18 Jan 2026 09:04:34 -0800 Subject: [PATCH 1/7] refactor: remove bundled Python code Python code now lives in blockrun-llm PyPI package. --- bin/blockrun | 118 ------ install.sh | 108 ----- scripts/__init__.py | 3 - scripts/llm/__init__.py | 13 - scripts/llm/chat.py | 142 ------- scripts/llm/image.py | 108 ----- scripts/llm/router.py | 332 --------------- scripts/run.py | 802 ------------------------------------- scripts/utils/__init__.py | 5 - scripts/utils/branding.py | 318 --------------- scripts/utils/config.py | 147 ------- scripts/utils/spending.py | 151 ------- scripts/wallet/__init__.py | 10 - scripts/wallet/balance.py | 63 --- scripts/wallet/status.py | 50 --- 15 files changed, 2370 deletions(-) delete mode 100755 bin/blockrun delete mode 100755 install.sh delete mode 100644 scripts/__init__.py delete mode 100644 scripts/llm/__init__.py delete mode 100644 scripts/llm/chat.py delete mode 100644 scripts/llm/image.py delete mode 100644 scripts/llm/router.py delete mode 100644 scripts/run.py delete mode 100644 scripts/utils/__init__.py delete mode 100644 scripts/utils/branding.py delete mode 100644 scripts/utils/config.py delete mode 100644 scripts/utils/spending.py delete mode 100644 scripts/wallet/__init__.py delete mode 100644 scripts/wallet/balance.py delete mode 100644 scripts/wallet/status.py diff --git a/bin/blockrun b/bin/blockrun deleted file mode 100755 index 848f4da..0000000 --- a/bin/blockrun +++ /dev/null @@ -1,118 +0,0 @@ -#!/usr/bin/env python3 -"""BlockRun CLI - balance, generate, check commands.""" - -import sys - -def main(): - if len(sys.argv) < 2: - print("Usage: blockrun ") - print(" balance Show wallet address and USDC balance") - print(" generate Generate image with DALL-E") - print(" check @user Check X/Twitter account with Grok") - sys.exit(0) # Not an error, just showing help - - cmd = sys.argv[1].lower() - - if cmd == "balance": - cmd_balance() - elif cmd == "generate": - if len(sys.argv) < 3: - print("Usage: blockrun generate ") - sys.exit(1) - prompt = " ".join(sys.argv[2:]) - cmd_generate(prompt) - elif cmd == "check": - if len(sys.argv) < 3: - print("Usage: blockrun check @username") - sys.exit(1) - user = sys.argv[2] - cmd_check(user) - else: - print(f"Unknown command: {cmd}") - print("Commands: balance, generate, check") - sys.exit(1) - - -def cmd_balance(): - """Show wallet address and balance.""" - try: - from blockrun_llm import setup_agent_wallet - except ImportError: - print("Error: blockrun-llm not installed. Run: pip install blockrun-llm") - sys.exit(1) - - client = setup_agent_wallet(silent=True) - addr = client.get_wallet_address() - balance = client.get_balance() # SDK has built-in RPC fallback - - print(f"Wallet: {addr}") - print(f"Balance: ${balance:.2f} USDC (Base)") - print(f"View: https://basescan.org/address/{addr}") - - -def cmd_generate(prompt: str): - """Generate image with DALL-E.""" - try: - from blockrun_llm import ImageClient - except ImportError: - print("Error: blockrun-llm not installed. Run: pip install blockrun-llm") - sys.exit(1) - - print(f"Generating: {prompt}") - client = ImageClient() - - try: - result = client.generate(prompt) - url = result.data[0].url - print(f"Image: {url}") - - # Try to open in browser - try: - import webbrowser - webbrowser.open(url) - except: - pass - except Exception as e: - print(f"Error: {e}") - sys.exit(1) - - -def cmd_check(user: str): - """Check X/Twitter account with Grok.""" - try: - from blockrun_llm import setup_agent_wallet - except ImportError: - print("Error: blockrun-llm not installed. Run: pip install blockrun-llm") - sys.exit(1) - - # Clean up username - user = user.lstrip("@") - - print(f"Checking @{user} on X...") - client = setup_agent_wallet(silent=True) - - try: - response = client.chat( - "xai/grok-3", - f"Give a brief summary of @{user}'s recent X/Twitter activity, engagement, and what they're focused on.", - search_parameters={ - "mode": "on", - "sources": [{"type": "x", "included_x_handles": [user]}], - "max_search_results": 10, - "return_citations": True - } - ) - print() - print(response) - - # Show cost - spending = client.get_spending() - print() - print(f"Cost: ${spending['total_usd']:.2f}") - except Exception as e: - print(f"Error: {e}") - sys.exit(1) - - -if __name__ == "__main__": - main() diff --git a/install.sh b/install.sh deleted file mode 100755 index 18d7f06..0000000 --- a/install.sh +++ /dev/null @@ -1,108 +0,0 @@ -#!/bin/bash -# BlockRun Install Script -# One command to install BlockRun skill + SDK - -set -e - -echo "Installing BlockRun..." - -# Detect platform and set skills path -if [ -d "$HOME/.gemini/antigravity" ]; then - SKILLS_DIR="$HOME/.gemini/antigravity/skills/blockrun" - echo "Detected Antigravity (global)" -elif [ -d "$HOME/.claude" ]; then - SKILLS_DIR="$HOME/.claude/skills/blockrun" - echo "Detected Claude Code" -else - # Default to Claude Code - SKILLS_DIR="$HOME/.claude/skills/blockrun" - mkdir -p "$HOME/.claude/skills" - echo "Using Claude Code default" -fi - -# Clone or update skill -if [ ! -d "$SKILLS_DIR" ]; then - echo "Cloning skill..." - mkdir -p "$(dirname "$SKILLS_DIR")" - git clone --depth 1 --quiet https://github.com/BlockRunAI/blockrun-agent-wallet "$SKILLS_DIR" -else - echo "Updating skill..." - cd "$SKILLS_DIR" && git pull --ff-only --quiet -fi - -# Install SDK with fallbacks for different Python setups -echo "Installing Python SDK..." -if pip install --upgrade blockrun-llm >/dev/null 2>&1; then - : -elif pip install --user --upgrade blockrun-llm >/dev/null 2>&1; then - : -elif pip install --user --break-system-packages --upgrade blockrun-llm >/dev/null 2>&1; then - : -elif python3 -m pip install --upgrade blockrun-llm >/dev/null 2>&1; then - : -elif python3 -m pip install --user --upgrade blockrun-llm >/dev/null 2>&1; then - : -elif python3 -m pip install --user --break-system-packages --upgrade blockrun-llm >/dev/null 2>&1; then - : -else - echo "ERROR: Could not install blockrun-llm. Please install manually:" - echo " pip install blockrun-llm" - exit 1 -fi - -# Install CLI to ~/.local/bin -echo "Installing CLI..." -mkdir -p "$HOME/.local/bin" -cp "$SKILLS_DIR/bin/blockrun" "$HOME/.local/bin/blockrun" -chmod +x "$HOME/.local/bin/blockrun" - -# Check if ~/.local/bin is in PATH -if [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then - echo "Adding ~/.local/bin to PATH..." - # Add to shell config - if [ -f "$HOME/.zshrc" ]; then - echo 'export PATH="$HOME/.local/bin:$PATH"' >> "$HOME/.zshrc" - elif [ -f "$HOME/.bashrc" ]; then - echo 'export PATH="$HOME/.local/bin:$PATH"' >> "$HOME/.bashrc" - fi - # Also export for current session - export PATH="$HOME/.local/bin:$PATH" -fi - -# Verify installation and show status -echo "Verifying..." -python3 <<'PYEOF' -from blockrun_llm import setup_agent_wallet, save_wallet_qr - -client = setup_agent_wallet(silent=True) -addr = client.get_wallet_address() -balance = client.get_balance() # SDK has built-in RPC fallback - -# Save QR for later opening -save_wallet_qr(addr) - -print() -print('BlockRun installed!') -print(f'Wallet: {addr}') -print(f'Balance: ${balance:.2f} USDC') -print() -print('CLI commands:') -print(' blockrun balance - Check wallet balance') -print(' blockrun generate ... - Generate image with DALL-E') -print(' blockrun check @user - Check X/Twitter with Grok') -print() -print('Or just tell Claude: "generate an image of..." or "check @elonmusk on twitter"') -if balance == 0: - print() - print('Fund wallet: Send USDC on Base to the address above') -import sys -sys.stdout.flush() -PYEOF - -# Delay so user can read output before QR opens -sleep 3 - -# Open QR code AFTER all text is printed -if [ -f "$HOME/.blockrun/qr.png" ]; then - open "$HOME/.blockrun/qr.png" 2>/dev/null || xdg-open "$HOME/.blockrun/qr.png" 2>/dev/null || true -fi diff --git a/scripts/__init__.py b/scripts/__init__.py deleted file mode 100644 index 471373e..0000000 --- a/scripts/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -"""BlockRun Claude Code Wallet - Your AI Superpower.""" - -__version__ = "1.0.0" diff --git a/scripts/llm/__init__.py b/scripts/llm/__init__.py deleted file mode 100644 index 2408f3d..0000000 --- a/scripts/llm/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -"""BlockRun LLM integration modules.""" - -from .chat import chat, chat_completion -from .image import generate_image -from .router import smart_route, get_model_for_task - -__all__ = [ - "chat", - "chat_completion", - "generate_image", - "smart_route", - "get_model_for_task", -] diff --git a/scripts/llm/chat.py b/scripts/llm/chat.py deleted file mode 100644 index b0d466e..0000000 --- a/scripts/llm/chat.py +++ /dev/null @@ -1,142 +0,0 @@ -""" -BlockRun Chat Module - LLM chat completion wrapper. - -Provides high-level chat functions that wrap the blockrun_llm SDK -with additional features like smart routing and branded output. -""" - -from typing import Optional, List, Dict, Any - -try: - from blockrun_llm import LLMClient, ChatResponse, APIError, PaymentError - HAS_SDK = True -except ImportError: - HAS_SDK = False - -from .router import smart_route - - -def chat( - prompt: str, - *, - model: Optional[str] = None, - system: Optional[str] = None, - cheap: bool = False, - fast: bool = False, - max_tokens: int = 1024, - temperature: Optional[float] = None, - private_key: Optional[str] = None, -) -> str: - """ - Simple 1-line chat interface with smart routing. - - Args: - prompt: User message - model: Specific model ID (overrides smart routing) - system: Optional system prompt - cheap: Prefer cost-effective models - fast: Prefer low-latency models - max_tokens: Maximum tokens to generate - temperature: Sampling temperature - private_key: Override environment variable - - Returns: - Assistant's response text - - Raises: - ImportError: If blockrun_llm SDK not installed - PaymentError: If payment fails - APIError: If API request fails - """ - if not HAS_SDK: - raise ImportError( - "blockrun_llm SDK not installed. Install with: pip install blockrun-llm" - ) - - # Determine model via smart routing if not specified - selected_model = model or smart_route(prompt, cheap=cheap, fast=fast) - - # Create client and execute - client = LLMClient(private_key=private_key) if private_key else LLMClient() - - try: - response = client.chat( - model=selected_model, - prompt=prompt, - system=system, - max_tokens=max_tokens, - temperature=temperature, - ) - return response - finally: - client.close() - - -def chat_completion( - model: str, - messages: List[Dict[str, str]], - *, - max_tokens: int = 1024, - temperature: Optional[float] = None, - top_p: Optional[float] = None, - private_key: Optional[str] = None, -) -> "ChatResponse": - """ - Full chat completion interface (OpenAI-compatible). - - Args: - model: Model ID - messages: List of message dicts with 'role' and 'content' - max_tokens: Maximum tokens to generate - temperature: Sampling temperature - top_p: Nucleus sampling parameter - private_key: Override environment variable - - Returns: - ChatResponse object with choices and usage - - Raises: - ImportError: If blockrun_llm SDK not installed - PaymentError: If payment fails - APIError: If API request fails - """ - if not HAS_SDK: - raise ImportError( - "blockrun_llm SDK not installed. Install with: pip install blockrun-llm" - ) - - client = LLMClient(private_key=private_key) if private_key else LLMClient() - - try: - return client.chat_completion( - model=model, - messages=messages, - max_tokens=max_tokens, - temperature=temperature, - top_p=top_p, - ) - finally: - client.close() - - -def list_models(private_key: Optional[str] = None) -> List[Dict[str, Any]]: - """ - List available models with pricing. - - Args: - private_key: Override environment variable - - Returns: - List of model information dicts - """ - if not HAS_SDK: - raise ImportError( - "blockrun_llm SDK not installed. Install with: pip install blockrun-llm" - ) - - client = LLMClient(private_key=private_key) if private_key else LLMClient() - - try: - return client.list_models() - finally: - client.close() diff --git a/scripts/llm/image.py b/scripts/llm/image.py deleted file mode 100644 index 0298c25..0000000 --- a/scripts/llm/image.py +++ /dev/null @@ -1,108 +0,0 @@ -""" -BlockRun Image Module - Image generation wrapper. - -Provides high-level image generation functions that wrap the blockrun_llm SDK. -""" - -from typing import Optional - -try: - from blockrun_llm import ImageClient, ImageResponse, APIError, PaymentError - HAS_SDK = True -except ImportError: - HAS_SDK = False - - -# Available image models -IMAGE_MODELS = { - "nano-banana": "google/nano-banana", - "nano-banana-pro": "google/nano-banana-pro", - "dall-e-3": "openai/dall-e-3", - "gpt-image-1": "openai/gpt-image-1", -} - -DEFAULT_MODEL = "google/nano-banana" - - -def generate_image( - prompt: str, - *, - model: Optional[str] = None, - size: str = "1024x1024", - n: int = 1, - private_key: Optional[str] = None, -) -> "ImageResponse": - """ - Generate an image from a text prompt. - - Args: - prompt: Text description of the image to generate - model: Model ID (default: google/nano-banana) - size: Image size (default: 1024x1024) - n: Number of images to generate (default: 1) - private_key: Override environment variable - - Returns: - ImageResponse with generated image URLs/data - - Raises: - ImportError: If blockrun_llm SDK not installed - PaymentError: If payment fails - APIError: If API request fails - - Example: - result = generate_image("A sunset over mountains") - print(result.data[0].url) - """ - if not HAS_SDK: - raise ImportError( - "blockrun_llm SDK not installed. Install with: pip install blockrun-llm" - ) - - # Resolve model alias if provided - selected_model = IMAGE_MODELS.get(model, model) if model else DEFAULT_MODEL - - client = ImageClient(private_key=private_key) if private_key else ImageClient() - - try: - return client.generate( - prompt=prompt, - model=selected_model, - size=size, - n=n, - ) - finally: - client.close() - - -def get_image_url( - prompt: str, - *, - model: Optional[str] = None, - size: str = "1024x1024", - private_key: Optional[str] = None, -) -> str: - """ - Convenience function to get just the image URL. - - Args: - prompt: Text description of the image - model: Model ID - size: Image size - private_key: Override environment variable - - Returns: - URL or data URL of the generated image - """ - result = generate_image( - prompt=prompt, - model=model, - size=size, - n=1, - private_key=private_key, - ) - - if result.data and len(result.data) > 0: - return result.data[0].url - else: - raise ValueError("No image data returned from API") diff --git a/scripts/llm/router.py b/scripts/llm/router.py deleted file mode 100644 index 04640a8..0000000 --- a/scripts/llm/router.py +++ /dev/null @@ -1,332 +0,0 @@ -""" -BlockRun Smart Router - Intelligent model selection. - -Routes requests to the optimal LLM based on: -- Content analysis (keywords, task type) -- User preferences (cost, speed) -- Model capabilities (real-time data, reasoning, etc.) -""" - -from typing import Optional, Dict, List - - -# Model capabilities and routing rules -MODEL_CATALOG = { - # OpenAI GPT-5 family (latest) - "openai/gpt-5.2": { - "provider": "OpenAI", - "description": "Latest frontier model with 400K context and adaptive reasoning", - "strengths": ["general", "coding", "analysis", "second-opinion"], - "cost": "medium", - "speed": "fast", - }, - "openai/gpt-5.2-pro": { - "provider": "OpenAI", - "description": "Pro version with more compute for complex tasks", - "strengths": ["complex", "analysis", "research"], - "cost": "high", - "speed": "medium", - }, - "openai/gpt-5-mini": { - "provider": "OpenAI", - "description": "Cost-optimized reasoning model", - "strengths": ["general", "reasoning", "quick-tasks"], - "cost": "low", - "speed": "fast", - }, - "openai/gpt-5-nano": { - "provider": "OpenAI", - "description": "High-throughput for bulk tasks", - "strengths": ["quick-tasks", "bulk"], - "cost": "very-low", - "speed": "very-fast", - }, - - # OpenAI o-series (reasoning) - "openai/o4-mini": { - "provider": "OpenAI", - "description": "Latest efficient reasoning model", - "strengths": ["reasoning", "math", "logic"], - "cost": "medium", - "speed": "medium", - }, - "openai/o3": { - "provider": "OpenAI", - "description": "Advanced reasoning for complex problems", - "strengths": ["reasoning", "math", "logic", "complex"], - "cost": "high", - "speed": "slow", - }, - "openai/o3-mini": { - "provider": "OpenAI", - "description": "STEM-focused reasoning", - "strengths": ["reasoning", "math"], - "cost": "medium", - "speed": "medium", - }, - "openai/o1": { - "provider": "OpenAI", - "description": "Original advanced reasoning model", - "strengths": ["reasoning", "math", "logic", "complex"], - "cost": "high", - "speed": "slow", - }, - "openai/o1-mini": { - "provider": "OpenAI", - "description": "STEM-optimized reasoning", - "strengths": ["reasoning", "math"], - "cost": "medium", - "speed": "medium", - }, - - # OpenAI GPT-4 family (legacy but still available) - "openai/gpt-4.1": { - "provider": "OpenAI", - "strengths": ["general", "coding", "analysis"], - "cost": "medium", - "speed": "fast", - }, - "openai/gpt-4o": { - "provider": "OpenAI", - "strengths": ["general", "coding", "analysis"], - "cost": "low", - "speed": "fast", - }, - "openai/gpt-4o-mini": { - "provider": "OpenAI", - "strengths": ["general", "quick-tasks"], - "cost": "very-low", - "speed": "very-fast", - }, - - # Anthropic models - "anthropic/claude-opus-4": { - "provider": "Anthropic", - "description": "Most capable Claude model", - "strengths": ["coding", "analysis", "complex", "writing"], - "cost": "high", - "speed": "medium", - }, - "anthropic/claude-sonnet-4": { - "provider": "Anthropic", - "description": "Balanced performance and speed", - "strengths": ["coding", "analysis", "writing"], - "cost": "medium", - "speed": "fast", - }, - "anthropic/claude-haiku-4.5": { - "provider": "Anthropic", - "description": "Fast and efficient", - "strengths": ["quick-tasks", "summarization"], - "cost": "low", - "speed": "very-fast", - }, - - # Google models - "google/gemini-3-pro-preview": { - "provider": "Google", - "description": "Latest Gemini preview", - "strengths": ["long-context", "multimodal", "analysis"], - "cost": "medium", - "speed": "medium", - }, - "google/gemini-2.5-pro": { - "provider": "Google", - "strengths": ["long-context", "analysis", "research"], - "cost": "medium", - "speed": "medium", - }, - "google/gemini-2.5-flash": { - "provider": "Google", - "description": "Fast with long context support", - "strengths": ["long-context", "multimodal", "general"], - "cost": "low", - "speed": "fast", - }, - "google/gemini-2.5-flash-lite": { - "provider": "Google", - "strengths": ["quick-tasks", "budget"], - "cost": "very-low", - "speed": "very-fast", - }, - - # xAI models - "xai/grok-3": { - "provider": "xAI", - "description": "Real-time X/Twitter access with Live Search", - "strengths": ["real-time", "twitter", "news", "current-events"], - "cost": "medium", - "speed": "fast", - }, - "xai/grok-3-fast": { - "provider": "xAI", - "strengths": ["real-time", "quick-tasks"], - "cost": "low", - "speed": "very-fast", - }, - "xai/grok-3-mini": { - "provider": "xAI", - "strengths": ["real-time", "quick-tasks"], - "cost": "low", - "speed": "fast", - }, - - # DeepSeek models - "deepseek/deepseek-chat": { - "provider": "DeepSeek", - "description": "Most cost-effective general model", - "strengths": ["general", "coding", "budget"], - "cost": "very-low", - "speed": "fast", - }, - "deepseek/deepseek-reasoner": { - "provider": "DeepSeek", - "description": "Reasoning at budget prices", - "strengths": ["reasoning", "math", "budget"], - "cost": "low", - "speed": "medium", - }, -} - - -# Keyword to task type mapping -TASK_KEYWORDS = { - "real-time": ["twitter", "x.com", "trending", "news", "today", "current", "latest", "elon", "musk"], - "coding": ["code", "python", "javascript", "function", "debug", "error", "fix", "implement", "refactor"], - "reasoning": ["math", "proof", "logic", "solve", "calculate", "why", "explain step"], - "long-context": ["document", "summarize", "analyze file", "pdf", "long", "entire"], - "quick-tasks": ["simple", "quick", "short", "brief"], - "writing": ["write", "draft", "compose", "essay", "article", "blog"], -} - - -def detect_task_type(prompt: str) -> List[str]: - """ - Detect task types from prompt content. - - Args: - prompt: User's prompt text - - Returns: - List of detected task types - """ - prompt_lower = prompt.lower() - detected = [] - - for task_type, keywords in TASK_KEYWORDS.items(): - if any(keyword in prompt_lower for keyword in keywords): - detected.append(task_type) - - return detected if detected else ["general"] - - -def smart_route( - prompt: str, - *, - cheap: bool = False, - fast: bool = False, - task_hint: Optional[str] = None, -) -> str: - """ - Intelligently select the best model for a given prompt. - - Args: - prompt: User's prompt text - cheap: Prioritize cost-effective models - fast: Prioritize low-latency models - task_hint: Optional explicit task type hint - - Returns: - Model ID string (e.g., "openai/gpt-5.2") - """ - # Handle explicit preferences - if cheap: - return "deepseek/deepseek-chat" - - if fast: - return "openai/gpt-5-nano" - - # Detect task type - task_types = [task_hint] if task_hint else detect_task_type(prompt) - - # Route based on detected task - if "real-time" in task_types: - return "xai/grok-3" - - if "coding" in task_types: - return "anthropic/claude-sonnet-4" - - if "reasoning" in task_types: - return "openai/o4-mini" - - if "long-context" in task_types: - return "google/gemini-2.5-flash" - - if "quick-tasks" in task_types: - return "openai/gpt-5-mini" - - if "writing" in task_types: - return "anthropic/claude-sonnet-4" - - # Default: GPT-5.2 for general tasks (latest frontier model) - return "openai/gpt-5.2" - - -def get_model_for_task(task: str) -> str: - """ - Get recommended model for a specific task type. - - Args: - task: Task type (e.g., "coding", "reasoning", "real-time") - - Returns: - Model ID string - """ - task_to_model = { - "coding": "anthropic/claude-sonnet-4", - "reasoning": "openai/o4-mini", - "math": "openai/o4-mini", - "complex": "openai/o3", - "real-time": "xai/grok-3", - "twitter": "xai/grok-3", - "long-context": "google/gemini-2.5-flash", - "budget": "deepseek/deepseek-chat", - "cheap": "deepseek/deepseek-chat", - "fast": "openai/gpt-5-nano", - "general": "openai/gpt-5.2", - "second-opinion": "openai/gpt-5.2", - "writing": "anthropic/claude-sonnet-4", - "analysis": "openai/gpt-5.2", - } - - return task_to_model.get(task.lower(), "openai/gpt-5.2") - - -def get_model_info(model_id: str) -> Optional[Dict]: - """ - Get information about a specific model. - - Args: - model_id: Model ID string - - Returns: - Model info dict or None if not found - """ - return MODEL_CATALOG.get(model_id) - - -def list_models_by_strength(strength: str) -> List[str]: - """ - List models that have a specific strength. - - Args: - strength: Strength to filter by (e.g., "coding", "reasoning") - - Returns: - List of model IDs - """ - return [ - model_id - for model_id, info in MODEL_CATALOG.items() - if strength in info.get("strengths", []) - ] diff --git a/scripts/run.py b/scripts/run.py deleted file mode 100644 index 8205108..0000000 --- a/scripts/run.py +++ /dev/null @@ -1,802 +0,0 @@ -#!/usr/bin/env python3 -""" -BlockRun Claude Code Wallet - Unified CLI Entry Point - -Access unlimited LLM models and image generation through USDC micropayments. -Your private key never leaves your machine - only signatures are transmitted. - -Usage: - python run.py "Your prompt here" - python run.py "Prompt" --model openai/gpt-5.2 - python run.py "Description" --image - python run.py --balance - python run.py --models - -Environment: - BLOCKRUN_WALLET_KEY: Your Base chain wallet private key (required) - BLOCKRUN_API_URL: API endpoint (optional, default: https://blockrun.ai/api) -""" - -import argparse -import json -import os -import re -import sys -import urllib.request -import urllib.error -from typing import Optional - -# Plugin version (keep in sync with plugin.json) -__version__ = "1.0.0" -GITHUB_PLUGIN_URL = "https://raw.githubusercontent.com/BlockRunAI/blockrun-agent-wallet/main/plugin.json" - -# Add parent directory to path for imports -sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -try: - from scripts.utils.branding import branding - from scripts.utils.spending import SpendingTracker -except ImportError: - # Fallback if running directly - from utils.branding import branding - from utils.spending import SpendingTracker - -# Try to import blockrun_llm SDK -try: - from blockrun_llm import LLMClient, ImageClient, APIError, PaymentError - HAS_SDK = True -except ImportError: - HAS_SDK = False - - -def check_environment() -> bool: - """Check if wallet is available (session file or env var).""" - try: - from scripts.utils.config import get_private_key - except ImportError: - from utils.config import get_private_key - - key = get_private_key() - if not key: - branding.print_error( - "No wallet found", - help_link="https://blockrun.ai/docs/setup" - ) - print(" Wallet auto-creates on first use, or set manually:") - print(" export BLOCKRUN_WALLET_KEY=\"0x...\"") - print() - return False - return True - - -def is_realtime_query(prompt: str) -> bool: - """Check if prompt requires real-time data (Twitter/X).""" - prompt_lower = prompt.lower() - - # Direct keywords for real-time/social media queries - keywords = [ - "twitter", "x.com", "trending", "elon", "musk", - "breaking news", "latest posts", "live updates", - "what are people saying", "current events" - ] - if any(word in prompt_lower for word in keywords): - return True - - # Twitter handle pattern (@username but not email) - # Match @ followed by word chars, not preceded by word char (excludes email) - if re.search(r'(? str: - """ - Smart model routing based on prompt content and preferences. - - Args: - prompt: User's prompt text - cheap: Prefer cost-effective models - fast: Prefer low-latency models - - Returns: - Model ID string - """ - prompt_lower = prompt.lower() - - # PRIORITY 1: Real-time data requires Grok (even with --cheap) - # Grok is the only model with live X/Twitter access - if is_realtime_query(prompt): - return "xai/grok-3" - - # Warn if conflicting flags used - if cheap and fast: - branding.print_info("Note: --cheap and --fast both set; using --cheap") - - # Cost-optimized routing - if cheap: - return "deepseek/deepseek-chat" - - # Speed-optimized routing - if fast: - return "openai/gpt-5-mini" - - if any(word in prompt_lower for word in ["code", "python", "javascript", "function", "debug"]): - return "anthropic/claude-sonnet-4" - - if any(word in prompt_lower for word in ["math", "proof", "prove", "theorem", "logic", "reasoning", "solve", "calculate"]): - return "openai/o1-mini" - - if any(word in prompt_lower for word in ["long", "document", "summarize", "analyze file"]): - return "google/gemini-2.0-flash" - - # Default: GPT-5.2 for general tasks - return "openai/gpt-5.2" - - -def cmd_chat( - prompt: str, - model: Optional[str] = None, - system: Optional[str] = None, - cheap: bool = False, - fast: bool = False, - max_tokens: int = 1024, - temperature: Optional[float] = None, -): - """Execute chat command.""" - if not HAS_SDK: - branding.print_error( - "blockrun_llm SDK not installed", - help_link="https://github.com/blockrunai/blockrun-llm" - ) - print(" Install with: pip install blockrun-llm") - return 1 - - if not check_environment(): - return 1 - - # Validate temperature if provided - if temperature is not None and (temperature < 0.0 or temperature > 2.0): - branding.print_error("Temperature must be between 0.0 and 2.0") - return 1 - - # Determine model - selected_model = model or get_smart_model(prompt, cheap=cheap, fast=fast) - - # Check budget before making call - tracker = SpendingTracker() - within_budget, remaining = tracker.check_budget() - if not within_budget: - branding.print_budget_error( - spent=tracker.get_total(), - limit=tracker.get_limit(), - calls=tracker.get_calls() - ) - return 1 - - try: - client = LLMClient() - - # Print header - branding.print_header( - model=selected_model, - wallet=client.get_wallet_address(), - ) - - # Auto-enable search for Grok real-time queries (Twitter/X) - enable_search = is_realtime_query(prompt) and "grok" in selected_model.lower() - - # Execute chat - response = client.chat( - model=selected_model, - prompt=prompt, - system=system, - max_tokens=max_tokens, - temperature=temperature, - search=enable_search, - ) - - # Print response - branding.print_response(response) - - # Record spending - sdk_spending = client.get_spending() - call_cost = sdk_spending['total_usd'] - tracker.record(selected_model, call_cost) - - # Show spending with session totals - budget_limit = tracker.get_limit() - branding.print_footer( - actual_cost=f"{call_cost:.4f}", - session_total=tracker.get_total(), - session_calls=tracker.get_calls(), - budget_remaining=remaining - call_cost if budget_limit else None, - budget_limit=budget_limit, - ) - - client.close() - return 0 - - except PaymentError as e: - # Show funding instructions for insufficient balance - wallet = None - try: - from blockrun_llm import get_wallet_address, open_wallet_qr - wallet = get_wallet_address() - except Exception: - pass - - branding.print_error(f"Payment failed: {e}") - if wallet: - print(f"\n Your wallet: {wallet}") - print(f" Network: Base (USDC)") - print(f"\n To fund your wallet:") - print(f" 1. Send $1-5 USDC on Base to the address above") - print(f" 2. Or run this to get a QR code:") - print(f" python -c \"from blockrun_llm import open_wallet_qr, get_wallet_address; open_wallet_qr(get_wallet_address())\"") - print() - return 1 - except APIError as e: - error_str = str(e) - if "400" in error_str: - branding.print_error("Invalid request - model may not exist or parameters are wrong") - print("\n Run --models to see available models:") - print(" python scripts/run.py --models\n") - else: - branding.print_error(f"API error: {e}") - return 1 - except Exception as e: - branding.print_error(f"Unexpected error: {e}") - return 1 - - -def cmd_image( - prompt: str, - model: Optional[str] = None, - size: str = "1024x1024", -): - """Execute image generation command.""" - if not HAS_SDK: - branding.print_error( - "blockrun_llm SDK not installed", - help_link="https://github.com/blockrunai/blockrun-llm" - ) - print(" Install with: pip install blockrun-llm") - return 1 - - if not check_environment(): - return 1 - - selected_model = model or "google/nano-banana" - - # Check budget before making call - tracker = SpendingTracker() - within_budget, remaining = tracker.check_budget() - if not within_budget: - branding.print_budget_error( - spent=tracker.get_total(), - limit=tracker.get_limit(), - calls=tracker.get_calls() - ) - return 1 - - try: - client = ImageClient() - - # Print header - branding.print_header( - model=selected_model, - wallet=client.get_wallet_address(), - ) - - branding.print_info(f"Generating image: \"{prompt[:50]}...\"") - print() - - # Generate image - result = client.generate( - prompt=prompt, - model=selected_model, - size=size, - ) - - # Print result - if result.data and len(result.data) > 0: - image_url = result.data[0].url - branding.print_success("Image generated!") - - # Save image to file - import subprocess - from datetime import datetime - - # Create filename with timestamp - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - filename = f"blockrun_image_{timestamp}.png" - filepath = os.path.join(os.getcwd(), filename) - - try: - # Download and save - urllib.request.urlretrieve(image_url, filepath) - branding.print_success(f"Saved to: {filepath}") - - # Try to open with system viewer - if sys.platform == "darwin": # macOS - subprocess.run(["open", filepath], check=False) - elif sys.platform == "linux": - subprocess.run(["xdg-open", filepath], check=False) - elif sys.platform == "win32": - os.startfile(filepath) - - except Exception as e: - # Fallback to just showing URL - print(f"\n URL: {image_url}") - print(f" (Could not save locally: {e})\n") - else: - branding.print_error("No image data returned") - - # Record spending - sdk_spending = client.get_spending() - call_cost = sdk_spending['total_usd'] - tracker.record(selected_model, call_cost) - - # Show spending with session totals - budget_limit = tracker.get_limit() - branding.print_footer( - actual_cost=f"{call_cost:.4f}", - session_total=tracker.get_total(), - session_calls=tracker.get_calls(), - budget_remaining=remaining - call_cost if budget_limit else None, - budget_limit=budget_limit, - ) - - client.close() - return 0 - - except PaymentError as e: - # Show funding instructions for insufficient balance - wallet = None - try: - from blockrun_llm import get_wallet_address - wallet = get_wallet_address() - except Exception: - pass - - branding.print_error(f"Payment failed: {e}") - if wallet: - print(f"\n Your wallet: {wallet}") - print(f" Network: Base (USDC)") - print(f"\n To fund your wallet:") - print(f" 1. Send $1-5 USDC on Base to the address above") - print(f" 2. Or run this to get a QR code:") - print(f" python -c \"from blockrun_llm import open_wallet_qr, get_wallet_address; open_wallet_qr(get_wallet_address())\"") - print() - return 1 - except APIError as e: - error_str = str(e) - if "400" in error_str: - branding.print_error("Invalid request - check model and size parameters") - print("\n Note: Some models only support 1024x1024 size") - print(" Try without --size flag or use --size 1024x1024\n") - else: - branding.print_error(f"API error: {e}") - return 1 - except Exception as e: - branding.print_error(f"Unexpected error: {e}") - return 1 - - -def is_valid_wallet_address(address: str) -> bool: - """Validate Ethereum wallet address format.""" - if not address or not isinstance(address, str): - return False - if not address.startswith("0x"): - return False - if len(address) != 42: - return False - try: - int(address[2:], 16) - return True - except ValueError: - return False - - -def get_usdc_balance(wallet_address: str) -> Optional[float]: - """ - Get USDC balance for a wallet address on Base chain. - - Args: - wallet_address: Ethereum wallet address (0x...) - - Returns: - USDC balance as float, or None if query fails - """ - if not is_valid_wallet_address(wallet_address): - return None - - import httpx - - # USDC contract on Base - USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" - BASE_RPC = "https://mainnet.base.org" - - try: - # balanceOf(address) function selector: 0x70a08231 - data = { - "jsonrpc": "2.0", - "method": "eth_call", - "params": [{ - "to": USDC_ADDRESS, - "data": f"0x70a08231000000000000000000000000{wallet_address[2:]}" - }, "latest"], - "id": 1 - } - - with httpx.Client(timeout=10) as client: - response = client.post(BASE_RPC, json=data) - result = response.json().get("result", "0x0") - # USDC has 6 decimals - return int(result, 16) / 1e6 - - except Exception: - return None - - -def cmd_balance(): - """Show wallet balance.""" - if not HAS_SDK: - branding.print_error( - "blockrun_llm SDK not installed", - help_link="https://github.com/blockrunai/blockrun-llm" - ) - return 1 - - if not check_environment(): - return 1 - - try: - client = LLMClient() - wallet = client.get_wallet_address() - - # Get actual USDC balance from Base chain - balance = get_usdc_balance(wallet) - balance_str = f"{balance:.6f}" if balance is not None else "(unable to fetch)" - - branding.print_balance( - wallet=wallet, - balance=balance_str, - network="Base" - ) - - client.close() - return 0 - - except Exception as e: - branding.print_error(f"Error: {e}") - return 1 - - -def cmd_qr(): - """Show QR code for wallet funding on Base network.""" - if not HAS_SDK: - branding.print_error( - "blockrun_llm SDK not installed", - help_link="https://github.com/blockrunai/blockrun-llm" - ) - return 1 - - try: - from blockrun_llm import get_wallet_address, open_wallet_qr - wallet = get_wallet_address() - - print() - print(f" Wallet: {wallet}") - print(f" Network: Base (Chain ID: 8453)") - print(f" Currency: USDC") - print() - print(" Opening QR code in browser...") - print(" Scan with any wallet app to send USDC on Base.") - print() - - open_wallet_qr(wallet) - return 0 - - except Exception as e: - branding.print_error(f"Error: {e}") - return 1 - - -def cmd_models(): - """List available models (no wallet required).""" - try: - # Try to use SDK standalone functions first - from blockrun_llm import list_models, list_image_models - - llm_models = list_models() - image_models = list_image_models() - - if llm_models or image_models: - branding.print_models_list(llm_models, image_models) - else: - branding.print_info("No models returned. Check API connection.") - - return 0 - - except ImportError: - # Fallback: direct API call if SDK not updated - import httpx - - try: - with httpx.Client(timeout=30) as client: - response = client.get("https://blockrun.ai/api/v1/models") - if response.status_code == 200: - models = response.json().get("data", []) - branding.print_models_list(models, []) - return 0 - else: - branding.print_error(f"API error: {response.status_code}") - return 1 - except Exception as e: - branding.print_error(f"Could not fetch models: {e}") - return 1 - - except Exception as e: - branding.print_error(f"Error: {e}") - return 1 - - -def cmd_check_update(): - """Check for plugin updates from GitHub.""" - print(f"\n BlockRun Plugin v{__version__}") - print(" Checking for updates...\n") - - try: - req = urllib.request.Request( - GITHUB_PLUGIN_URL, - headers={"User-Agent": "BlockRun-Plugin"} - ) - with urllib.request.urlopen(req, timeout=10) as response: - remote_plugin = json.loads(response.read().decode()) - remote_version = remote_plugin.get("version", "unknown") - - if remote_version == __version__: - branding.print_success(f"You're up to date! (v{__version__})") - elif remote_version > __version__: - branding.print_info(f"Update available: v{__version__} → v{remote_version}") - print("\n To update, run:") - print(" /plugin update blockrun-agent-wallet\n") - else: - branding.print_info(f"Local: v{__version__}, Remote: v{remote_version}") - - return 0 - - except urllib.error.HTTPError as e: - if e.code == 404: - # Repo may be private or not yet public - branding.print_info(f"Current version: v{__version__}") - print("\n To update, run:") - print(" /plugin update blockrun-agent-wallet") - print("\n Or update the SDK:") - print(" pip install --upgrade blockrun-llm\n") - return 0 - branding.print_error(f"Could not check for updates: HTTP {e.code}") - return 1 - except urllib.error.URLError as e: - branding.print_error(f"Could not check for updates: {e.reason}") - return 1 - except json.JSONDecodeError: - branding.print_error("Invalid response from GitHub") - return 1 - except Exception as e: - branding.print_error(f"Error checking updates: {e}") - return 1 - - -def cmd_version(): - """Show current version.""" - print(f"BlockRun Plugin v{__version__}") - return 0 - - -def cmd_spending(): - """Show spending summary.""" - tracker = SpendingTracker() - branding.print_spending_summary(tracker.data) - return 0 - - -def cmd_set_budget(amount: float): - """Set daily budget limit.""" - tracker = SpendingTracker() - tracker.set_budget(amount) - branding.print_success(f"Budget set to ${amount:.2f}/day") - - # Show current status - spent = tracker.get_total() - remaining = amount - spent - if remaining > 0: - print(f" Current spending: ${spent:.4f}") - print(f" Remaining today: ${remaining:.4f}") - else: - print(f" Warning: Already spent ${spent:.4f} (over budget)") - print() - return 0 - - -def cmd_clear_budget(): - """Remove budget limit.""" - tracker = SpendingTracker() - tracker.clear_budget() - branding.print_success("Budget limit removed") - print(f" Session spending: ${tracker.get_total():.4f} ({tracker.get_calls()} calls)") - print() - return 0 - - -def main(): - """Main CLI entry point.""" - parser = argparse.ArgumentParser( - prog="blockrun-agent-wallet", - description="BlockRun Claude Code Wallet - Access unlimited LLMs via USDC micropayments", - epilog=""" -Examples: - %(prog)s "What is quantum computing?" - %(prog)s "Analyze this code" --model anthropic/claude-sonnet-4 - %(prog)s "A sunset over mountains" --image - %(prog)s --balance - %(prog)s --models - -More info: https://blockrun.ai - """, - formatter_class=argparse.RawDescriptionHelpFormatter, - ) - - # Positional argument: prompt - parser.add_argument( - "prompt", - nargs="?", - help="Prompt for chat or image generation", - ) - - # Mode flags - parser.add_argument( - "--image", "-i", - action="store_true", - help="Generate an image instead of chat", - ) - parser.add_argument( - "--balance", "-b", - action="store_true", - help="Show wallet balance", - ) - parser.add_argument( - "--qr", - action="store_true", - help="Show wallet funding QR code (Base network)", - ) - parser.add_argument( - "--models", "-m", - action="store_true", - help="List available models with pricing", - ) - parser.add_argument( - "--check-update", - action="store_true", - help="Check for plugin updates from GitHub", - ) - parser.add_argument( - "--version", "-v", - action="store_true", - help="Show plugin version", - ) - - # Budget options - parser.add_argument( - "--spending", - action="store_true", - help="Show spending summary for today", - ) - parser.add_argument( - "--set-budget", - type=float, - metavar="AMOUNT", - help="Set daily budget limit in USD (e.g., --set-budget 1.00)", - ) - parser.add_argument( - "--clear-budget", - action="store_true", - help="Remove daily budget limit", - ) - - # Chat options - parser.add_argument( - "--model", - help="Specific model ID (e.g., openai/gpt-5.2, xai/grok-3)", - ) - parser.add_argument( - "--system", "-s", - help="System prompt for chat", - ) - parser.add_argument( - "--cheap", - action="store_true", - help="Use most cost-effective model", - ) - parser.add_argument( - "--fast", - action="store_true", - help="Use fastest model", - ) - parser.add_argument( - "--max-tokens", - type=int, - default=1024, - help="Maximum tokens to generate (default: 1024)", - ) - parser.add_argument( - "--temperature", "-t", - type=float, - help="Sampling temperature (0.0-2.0)", - ) - - # Image options - parser.add_argument( - "--size", - default="1024x1024", - help="Image size (default: 1024x1024)", - ) - - # Parse arguments - args = parser.parse_args() - - # Handle commands - if args.version: - return cmd_version() - - if args.check_update: - return cmd_check_update() - - if args.balance: - return cmd_balance() - - if args.qr: - return cmd_qr() - - if args.models: - return cmd_models() - - if args.spending: - return cmd_spending() - - if args.set_budget is not None: - return cmd_set_budget(args.set_budget) - - if args.clear_budget: - return cmd_clear_budget() - - if not args.prompt: - parser.print_help() - return 1 - - if args.image: - return cmd_image( - prompt=args.prompt, - model=args.model, - size=args.size, - ) - - return cmd_chat( - prompt=args.prompt, - model=args.model, - system=args.system, - cheap=args.cheap, - fast=args.fast, - max_tokens=args.max_tokens, - temperature=args.temperature, - ) - - -if __name__ == "__main__": - try: - sys.exit(main()) - except KeyboardInterrupt: - print("\n Interrupted by user") - sys.exit(130) diff --git a/scripts/utils/__init__.py b/scripts/utils/__init__.py deleted file mode 100644 index d624758..0000000 --- a/scripts/utils/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""BlockRun Claude Code Wallet utilities.""" - -from .branding import BlockRunBranding, branding - -__all__ = ["BlockRunBranding", "branding"] diff --git a/scripts/utils/branding.py b/scripts/utils/branding.py deleted file mode 100644 index cce8601..0000000 --- a/scripts/utils/branding.py +++ /dev/null @@ -1,318 +0,0 @@ -""" -BlockRun Branding Utilities - Consistent CLI output formatting. - -Provides branded headers, footers, and response formatting for all BlockRun -CLI operations, ensuring a professional and recognizable user experience. -""" - -from typing import Optional -import sys - - -class BlockRunBranding: - """Unified branding output system for BlockRun Claude Code Wallet.""" - - # Compact ASCII logo for CLI - LOGO = """ - ____ _ _ ____ -| __ )| | ___ ___| | _| _ \\ _ _ _ __ -| _ \\| |/ _ \\ / __| |/ / |_) | | | | '_ \\ -| |_) | | (_) | (__| <| _ <| |_| | | | | -|____/|_|\\___/ \\___|_|\\_\\_| \\_\\\\__,_|_| |_| - CLAUDE CODE WALLET""" - - # Simple header for regular operations - HEADER_LINE = "=" * 60 - - # Brand colors (ANSI codes for terminal) - COLORS = { - "reset": "\033[0m", - "bold": "\033[1m", - "blue": "\033[94m", - "green": "\033[92m", - "yellow": "\033[93m", - "red": "\033[91m", - "cyan": "\033[96m", - "dim": "\033[2m", - } - - def __init__(self, use_color: bool = True, show_logo: bool = False): - """ - Initialize branding utilities. - - Args: - use_color: Whether to use ANSI color codes (default: True) - show_logo: Whether to show full logo (default: False for compact output) - """ - self.use_color = use_color and sys.stdout.isatty() - self.show_logo = show_logo - - def _c(self, color: str, text: str) -> str: - """Apply color if enabled.""" - if not self.use_color: - return text - return f"{self.COLORS.get(color, '')}{text}{self.COLORS['reset']}" - - def print_header( - self, - model: str, - wallet: Optional[str] = None, - balance: Optional[str] = None, - cost_estimate: Optional[str] = None, - ): - """ - Print branded header before operation. - - Args: - model: Model being used - wallet: Wallet address (truncated) - balance: Current USDC balance - cost_estimate: Estimated cost for this operation - """ - if self.show_logo: - print(self._c("cyan", self.LOGO)) - print() - - print(self._c("dim", self.HEADER_LINE)) - print(self._c("bold", " BLOCKRUN CLAUDE CODE WALLET")) - print(self._c("dim", self.HEADER_LINE)) - - # Model info - print(f" Model: {self._c('cyan', model)}", end="") - if cost_estimate: - print(f" | Est. Cost: {self._c('yellow', cost_estimate)}", end="") - print() - - # Wallet info - if wallet: - wallet_display = f"{wallet[:6]}...{wallet[-4:]}" if len(wallet) > 12 else wallet - print(f" Wallet: {self._c('dim', wallet_display)}", end="") - if balance: - print(f" | Balance: {self._c('green', balance)} USDC", end="") - print() - - print(self._c("dim", self.HEADER_LINE)) - print() - - def print_response(self, content: str): - """Print the main response content.""" - print(content) - - def print_model_attribution(self, model: str, description: str = None): - """ - Print model attribution after response. - - Args: - model: Model ID used (e.g., "openai/gpt-5.2") - description: Optional model description - """ - print() - print(self._c("dim", "-" * 60)) - - # Parse provider from model ID - provider = model.split("/")[0].upper() if "/" in model else "Unknown" - model_name = model.split("/")[1] if "/" in model else model - - # Format model name nicely (keep version numbers intact) - display_name = model_name.upper().replace("-", " ") - - print(f" {self._c('cyan', 'Reviewed by:')} {display_name} ({provider})") - - if description: - print(f" {self._c('dim', description)}") - - print(f" {self._c('dim', 'Accessed via: BlockRun x402 micropayments')}") - print(self._c("dim", "-" * 60)) - - def print_footer( - self, - actual_cost: Optional[str] = None, - new_balance: Optional[str] = None, - session_total: Optional[float] = None, - session_calls: Optional[int] = None, - budget_remaining: Optional[float] = None, - budget_limit: Optional[float] = None, - ): - """ - Print branded footer after operation. - - Args: - actual_cost: Actual cost of the operation - new_balance: New wallet balance after operation - session_total: Total spent this session - session_calls: Number of calls this session - budget_remaining: Remaining budget (None if no limit) - budget_limit: Budget limit (None if no limit) - """ - print() - print(self._c("dim", "-" * 60)) - - if actual_cost: - print(f" {self._c('green', '✓')} This call: ${actual_cost}") - - if session_total is not None: - calls_str = f" ({session_calls} calls)" if session_calls else "" - print(f" {self._c('green', '✓')} Session total: ${session_total:.4f}{calls_str}") - - if budget_remaining is not None and budget_limit is not None: - print(f" {self._c('green', '✓')} Budget remaining: ${budget_remaining:.4f} of ${budget_limit:.2f}") - - print(f" {self._c('dim', 'Powered by BlockRun • blockrun.ai')}") - - def print_error(self, message: str, help_link: Optional[str] = None): - """ - Print branded error message. - - Args: - message: Error message - help_link: Optional help URL - """ - print() - print(self._c("red", f" Error: {message}")) - if help_link: - print(f" Help: {self._c('cyan', help_link)}") - print() - - def print_success(self, message: str): - """Print branded success message.""" - print(self._c("green", f" ✓ {message}")) - - def print_info(self, message: str): - """Print branded info message.""" - print(self._c("cyan", f" ℹ {message}")) - - def print_balance(self, wallet: str, balance: str, network: str = "Base"): - """ - Print wallet balance in branded format. - - Args: - wallet: Full wallet address - balance: USDC balance - network: Network name (default: Base) - """ - print() - print(self._c("dim", self.HEADER_LINE)) - print(self._c("bold", " BLOCKRUN WALLET")) - print(self._c("dim", self.HEADER_LINE)) - print(f" Address: {self._c('cyan', wallet)}") - print(f" Network: {network}") - print(f" Balance: {self._c('green', balance)} USDC") - print(self._c("dim", self.HEADER_LINE)) - print() - - def print_models_list(self, models: list, image_models: list = None): - """ - Print available models in branded format with pricing. - - Args: - models: List of LLM model dicts with id, pricing info - image_models: Optional list of image model dicts - """ - print() - print(self._c("dim", self.HEADER_LINE)) - print(self._c("bold", " AVAILABLE MODELS")) - print(self._c("dim", self.HEADER_LINE)) - print() - print(f" {self._c('dim', 'Live data from:')} {self._c('cyan', 'https://blockrun.ai/api/pricing')}") - print() - - # LLM Models - if models: - print(self._c("bold", " Chat Models:")) - print() - for model in models: - model_id = model.get("id", "unknown") - # Handle different pricing formats from API - input_price = model.get("inputPrice") or model.get("pricing", {}).get("input") - output_price = model.get("outputPrice") or model.get("pricing", {}).get("output") - - print(f" {self._c('cyan', model_id)}") - if input_price is not None and output_price is not None: - print(f" ${input_price}/M in, ${output_price}/M out") - print() - - # Image Models - if image_models: - print(self._c("bold", " Image Models:")) - print() - for model in image_models: - model_id = model.get("id", "unknown") - price = model.get("pricePerImage") - - print(f" {self._c('cyan', model_id)}") - if price is not None: - print(f" ${price}/image") - print() - - print(self._c("dim", self.HEADER_LINE)) - print(f" {self._c('dim', 'Prices in USDC • Pay only for what you use')}") - print() - - - def print_budget_error(self, spent: float, limit: float, calls: int): - """ - Print budget limit reached error. - - Args: - spent: Amount spent - limit: Budget limit - calls: Number of calls made - """ - print() - print(self._c("dim", self.HEADER_LINE)) - print(self._c("yellow", " ⚠ BUDGET LIMIT REACHED")) - print(self._c("dim", self.HEADER_LINE)) - print(f" Spent: {self._c('red', f'${spent:.4f}')} across {calls} calls") - print(f" Limit: ${limit:.2f}/day") - print() - print(" Options:") - print(f" {self._c('cyan', 'python run.py --set-budget 2.00')} # Increase limit") - print(f" {self._c('cyan', 'python run.py --clear-budget')} # Remove limit") - print(f" {self._c('dim', '(Budget resets tomorrow)')}") - print() - - def print_spending_summary(self, data: dict): - """ - Print spending summary. - - Args: - data: Spending tracker data dict - """ - spending = data.get("spending", {}) - total = spending.get("total_usd", 0.0) - calls = spending.get("calls", 0) - limit = data.get("budget_limit") - history = data.get("history", []) - session_id = data.get("session_id", "today") - - print() - print(self._c("dim", self.HEADER_LINE)) - print(self._c("bold", " SPENDING SUMMARY")) - print(self._c("dim", self.HEADER_LINE)) - print(f" Date: {session_id}") - print(f" Spent: {self._c('cyan', f'${total:.4f}')} across {calls} calls") - - if limit is not None: - remaining = max(0, limit - total) - print(f" Budget: ${limit:.2f} ({self._c('green', f'${remaining:.4f}')} remaining)") - else: - print(f" Budget: {self._c('dim', 'No limit set')}") - - if history: - print() - print(self._c("bold", " Recent calls:")) - for entry in history[-10:]: - ts = entry.get("timestamp", "") - time_str = ts[11:16] if len(ts) >= 16 else ts # Extract HH:MM - model = entry.get("model", "unknown") - cost = entry.get("cost", 0) - # Truncate long model names to avoid misalignment - model_display = model[:32] + "..." if len(model) > 35 else model - print(f" {time_str} {model_display:<35} ${cost:.4f}") - - print(self._c("dim", self.HEADER_LINE)) - print() - - -# Singleton instance for easy import -branding = BlockRunBranding() diff --git a/scripts/utils/config.py b/scripts/utils/config.py deleted file mode 100644 index 5b1990e..0000000 --- a/scripts/utils/config.py +++ /dev/null @@ -1,147 +0,0 @@ -""" -BlockRun Configuration Module. - -Handles configuration management, environment variables, and presets. -""" - -import os -from typing import Optional, Dict, Any -from pathlib import Path - - -# Wallet storage location (agent's own wallet) -WALLET_DIR = Path.home() / ".blockrun" -WALLET_FILE = WALLET_DIR / ".session" - - -# Default configuration values -DEFAULTS = { - "api_url": "https://blockrun.ai/api", - "default_model": "openai/gpt-5.2", - "default_image_model": "google/nano-banana", - "max_tokens": 1024, - "timeout": 60.0, - "image_timeout": 120.0, -} - - -def load_wallet() -> Optional[str]: - """ - Load wallet private key from ~/.blockrun/.session file. - - AI agents should have their own wallets. This is the agent's wallet. - - Returns: - Private key string or None if not found - """ - # Check .session first (preferred) - if WALLET_FILE.exists(): - key = WALLET_FILE.read_text().strip() - if key: - return key - - # Check legacy wallet.key - legacy_file = WALLET_DIR / "wallet.key" - if legacy_file.exists(): - key = legacy_file.read_text().strip() - if key: - return key - - return None - - -def get_private_key() -> Optional[str]: - """ - Get private key - session file first (agent wallet), then env vars (user override). - - Priority: - 1. ~/.blockrun/.session - Agent's own wallet - 2. BLOCKRUN_WALLET_KEY env var - User override - 3. BASE_CHAIN_WALLET_KEY env var - Legacy fallback - - Returns: - Private key string or None - """ - # PRIORITY 1: Agent's own wallet (session file) - session_key = load_wallet() - if session_key: - return session_key - - # PRIORITY 2: User override via environment - return ( - os.environ.get("BLOCKRUN_WALLET_KEY") or - os.environ.get("BASE_CHAIN_WALLET_KEY") - ) - - -def get_config() -> Dict[str, Any]: - """ - Get current configuration from environment and defaults. - - Returns: - Configuration dictionary - """ - return { - "api_url": os.environ.get("BLOCKRUN_API_URL", DEFAULTS["api_url"]), - "wallet_key_set": bool(get_private_key()), - "default_model": os.environ.get("BLOCKRUN_DEFAULT_MODEL", DEFAULTS["default_model"]), - "default_image_model": os.environ.get("BLOCKRUN_IMAGE_MODEL", DEFAULTS["default_image_model"]), - "max_tokens": int(os.environ.get("BLOCKRUN_MAX_TOKENS", DEFAULTS["max_tokens"])), - "timeout": float(os.environ.get("BLOCKRUN_TIMEOUT", DEFAULTS["timeout"])), - } - - -def validate_config() -> Dict[str, Any]: - """ - Validate configuration and return status. - - Returns: - Dict with validation results: - { - "valid": bool, - "errors": list of error strings, - "warnings": list of warning strings, - } - """ - errors = [] - warnings = [] - - # Check for wallet key - if not get_private_key(): - errors.append("No wallet found (check ~/.blockrun/.session or BLOCKRUN_WALLET_KEY)") - - # Check API URL format - api_url = os.environ.get("BLOCKRUN_API_URL", DEFAULTS["api_url"]) - if not api_url.startswith(("http://", "https://")): - errors.append("Invalid BLOCKRUN_API_URL format") - - # Warnings for non-default settings - if os.environ.get("BLOCKRUN_API_URL"): - warnings.append("Using custom API URL (not default)") - - return { - "valid": len(errors) == 0, - "errors": errors, - "warnings": warnings, - } - - -def get_presets_dir() -> Path: - """Get path to presets directory.""" - return Path(__file__).parent.parent.parent / "configs" / "presets" - - -def list_presets() -> list: - """ - List available configuration presets. - - Returns: - List of preset names - """ - presets_dir = get_presets_dir() - if not presets_dir.exists(): - return [] - - return [ - f.stem for f in presets_dir.glob("*.json") - ] diff --git a/scripts/utils/spending.py b/scripts/utils/spending.py deleted file mode 100644 index c81b304..0000000 --- a/scripts/utils/spending.py +++ /dev/null @@ -1,151 +0,0 @@ -""" -BlockRun Spending Tracker. - -Persistent spending tracking with budget enforcement. -Stores spending data in ~/.blockrun/spending.json. -""" - -import json -import os -import tempfile -from datetime import datetime -from pathlib import Path -from typing import Optional, Tuple - - -class SpendingTracker: - """Persistent spending tracker with budget enforcement.""" - - MAX_HISTORY = 100 - - def __init__(self): - self.dir = Path.home() / ".blockrun" - self.file = self.dir / "spending.json" - self.data = self._load() - - def _today(self) -> str: - """Get today's date string.""" - return datetime.now().strftime("%Y-%m-%d") - - def _now(self) -> str: - """Get current timestamp.""" - return datetime.now().strftime("%Y-%m-%dT%H:%M:%S") - - def _new_session(self) -> dict: - """Create a fresh session.""" - return { - "session_id": self._today(), - "budget_limit": None, - "spending": { - "total_usd": 0.0, - "calls": 0 - }, - "history": [] - } - - def _load(self) -> dict: - """Load or create fresh session (resets daily).""" - # Ensure directory exists - self.dir.mkdir(parents=True, exist_ok=True) - - if self.file.exists(): - try: - data = json.loads(self.file.read_text()) - # Reset if new day - if data.get("session_id", "")[:10] != self._today(): - # Preserve budget limit across days - new_data = self._new_session() - new_data["budget_limit"] = data.get("budget_limit") - return new_data - return data - except (json.JSONDecodeError, KeyError): - # Corrupted file - start fresh - return self._new_session() - - return self._new_session() - - def _save(self): - """Atomic save to prevent corruption.""" - self.dir.mkdir(parents=True, exist_ok=True) - - # Write to temp file first - fd, temp_path = tempfile.mkstemp(dir=self.dir, suffix=".json") - try: - with os.fdopen(fd, 'w') as f: - json.dump(self.data, f, indent=2) - # Atomic rename - os.replace(temp_path, self.file) - except Exception: - # Clean up temp file on error - if os.path.exists(temp_path): - os.unlink(temp_path) - raise - - def record(self, model: str, cost: float): - """Record a call and save.""" - self.data["spending"]["total_usd"] += cost - self.data["spending"]["calls"] += 1 - - # Add to history - self.data["history"].append({ - "timestamp": self._now(), - "model": model, - "cost": cost - }) - - # Cap history size - if len(self.data["history"]) > self.MAX_HISTORY: - self.data["history"] = self.data["history"][-self.MAX_HISTORY:] - - self._save() - - def check_budget(self) -> Tuple[bool, float]: - """ - Check if within budget. - - Returns: - Tuple of (within_budget, remaining). - If no budget set, returns (True, float('inf')). - """ - limit = self.data.get("budget_limit") - if limit is None: - return True, float('inf') - - spent = self.data["spending"]["total_usd"] - remaining = limit - spent - - # Use tolerance for floating-point comparison (0.0001 = 0.01 cent) - EPSILON = 0.0001 - within_budget = remaining > -EPSILON - - return within_budget, max(0, remaining) - - def set_budget(self, amount: float): - """Set daily budget limit.""" - self.data["budget_limit"] = amount - self._save() - - def clear_budget(self): - """Remove budget limit.""" - self.data["budget_limit"] = None - self._save() - - def get_total(self) -> float: - """Get total spent this session.""" - return self.data["spending"]["total_usd"] - - def get_calls(self) -> int: - """Get number of calls this session.""" - return self.data["spending"]["calls"] - - def get_limit(self) -> Optional[float]: - """Get current budget limit (None if no limit).""" - return self.data.get("budget_limit") - - def get_history(self, limit: int = 10) -> list: - """Get recent call history.""" - return self.data["history"][-limit:] - - def get_session_id(self) -> str: - """Get current session ID (date).""" - return self.data.get("session_id", self._today()) diff --git a/scripts/wallet/__init__.py b/scripts/wallet/__init__.py deleted file mode 100644 index 4b8aaa2..0000000 --- a/scripts/wallet/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -"""BlockRun wallet management modules.""" - -from .balance import get_balance, get_wallet_address -from .status import get_wallet_status - -__all__ = [ - "get_balance", - "get_wallet_address", - "get_wallet_status", -] diff --git a/scripts/wallet/balance.py b/scripts/wallet/balance.py deleted file mode 100644 index 6772013..0000000 --- a/scripts/wallet/balance.py +++ /dev/null @@ -1,63 +0,0 @@ -""" -BlockRun Balance Module - Wallet balance queries. - -Query USDC balance on Base chain for BlockRun payments. -""" - -from typing import Optional - -try: - from blockrun_llm import LLMClient - HAS_SDK = True -except ImportError: - HAS_SDK = False - - -def get_wallet_address(private_key: Optional[str] = None) -> str: - """ - Get the wallet address from private key. - - Args: - private_key: Override environment variable - - Returns: - Wallet address string (0x...) - """ - if not HAS_SDK: - raise ImportError( - "blockrun_llm SDK not installed. Install with: pip install blockrun-llm" - ) - - client = LLMClient(private_key=private_key) if private_key else LLMClient() - try: - return client.get_wallet_address() - finally: - client.close() - - -def get_balance(private_key: Optional[str] = None) -> dict: - """ - Get wallet balance information. - - Note: Full balance query requires additional API endpoint. - Currently returns wallet address for manual balance check. - - Args: - private_key: Override environment variable - - Returns: - Dict with wallet info: - { - "address": "0x...", - "network": "Base", - "balance_url": "https://basescan.org/address/..." - } - """ - address = get_wallet_address(private_key) - - return { - "address": address, - "network": "Base (Mainnet)", - "balance_url": f"https://basescan.org/address/{address}", - "note": "Check balance at blockrun.ai or basescan.org", - } diff --git a/scripts/wallet/status.py b/scripts/wallet/status.py deleted file mode 100644 index 4e7e789..0000000 --- a/scripts/wallet/status.py +++ /dev/null @@ -1,50 +0,0 @@ -""" -BlockRun Wallet Status Module - Wallet health and status checks. -""" - -from typing import Optional, Dict, Any -from .balance import get_wallet_address - - -def get_wallet_status(private_key: Optional[str] = None) -> Dict[str, Any]: - """ - Get comprehensive wallet status. - - Args: - private_key: Override environment variable - - Returns: - Dict with wallet status information - """ - try: - address = get_wallet_address(private_key) - return { - "status": "connected", - "address": address, - "network": "Base (Mainnet)", - "chain_id": 8453, - "currency": "USDC", - "explorer_url": f"https://basescan.org/address/{address}", - } - except ValueError as e: - return { - "status": "not_configured", - "error": str(e), - "help": "Set BLOCKRUN_WALLET_KEY environment variable", - } - except Exception as e: - return { - "status": "error", - "error": str(e), - } - - -def validate_wallet_config() -> bool: - """ - Validate that wallet is properly configured. - - Returns: - True if wallet is configured, False otherwise - """ - status = get_wallet_status() - return status.get("status") == "connected" From 113fd266d0d7187f1c07ca9d5e87c13febcb7b49 Mon Sep 17 00:00:00 2001 From: Arthur Chiu Date: Sun, 18 Jan 2026 09:04:46 -0800 Subject: [PATCH 2/7] chore: remove working documents and legacy plugin.json Remove docs/plans/, docs/awesome-claude-skills-pr/, references/ --- .../PR_DESCRIPTION.md | 56 ---- .../README_ADDITION.md | 22 -- .../twitter-intel/SKILL.md | 308 ------------------ .../2026-01-13-budget-tracking-design.md | 166 ---------- plugin.json | 23 -- references/MODEL_GUIDE.md | 67 ---- references/PRICING.md | 61 ---- 7 files changed, 703 deletions(-) delete mode 100644 docs/awesome-claude-skills-pr/PR_DESCRIPTION.md delete mode 100644 docs/awesome-claude-skills-pr/README_ADDITION.md delete mode 100644 docs/awesome-claude-skills-pr/twitter-intel/SKILL.md delete mode 100644 docs/plans/2026-01-13-budget-tracking-design.md delete mode 100644 plugin.json delete mode 100644 references/MODEL_GUIDE.md delete mode 100644 references/PRICING.md diff --git a/docs/awesome-claude-skills-pr/PR_DESCRIPTION.md b/docs/awesome-claude-skills-pr/PR_DESCRIPTION.md deleted file mode 100644 index 3365a93..0000000 --- a/docs/awesome-claude-skills-pr/PR_DESCRIPTION.md +++ /dev/null @@ -1,56 +0,0 @@ -# Add Twitter Intel skill - -## Summary -Adds a new skill for real-time X/Twitter intelligence - analyzing accounts, tracking topics, and monitoring keywords with live data. - -## What problem it solves -Claude doesn't have access to real-time social media data. This skill enables: -- Account analysis (@username) -- Topic/hashtag tracking (#topic) -- Keyword/brand monitoring ("keyword") - -## Who uses this workflow -- Marketers monitoring brand mentions and competitor activity -- Developers tracking tech trends and community sentiment -- Researchers analyzing social conversations -- Sales teams researching prospects' social presence -- Product teams tracking launch sentiment - -## How it works -Uses [BlockRun](https://blockrun.ai) to access xAI's Grok with Live Search API. Pay-per-query ($0.25-0.50), no API keys or subscriptions needed. - -## Example usage - -``` -/twitter-intel @pmarca -``` - -Output: -``` -# Twitter Intel: @pmarca - -## Overview -- **Account**: @pmarca (Marc Andreessen) -- **Recent Activity**: Very active, 5-10 posts daily -- **Primary Topics**: AI, startups, tech policy, venture capital - -## Recent Highlights -1. **Thread on AI regulation** - 2.5K likes, 400 replies -2. **Startup advice post** - 1.8K likes - -## Key Insights -- Consistently bullish on AI despite regulatory concerns -- High influence on VC/startup community sentiment - -Query cost: $0.38 -``` - -## Testing -- [x] Tested in Claude Code -- [x] SDK installs correctly (`pip install blockrun-llm`) -- [x] Account analysis works -- [x] Topic tracking works -- [x] Keyword monitoring works - -## Category -Business & Marketing (or could fit Development for tech trend tracking) diff --git a/docs/awesome-claude-skills-pr/README_ADDITION.md b/docs/awesome-claude-skills-pr/README_ADDITION.md deleted file mode 100644 index cea7d8a..0000000 --- a/docs/awesome-claude-skills-pr/README_ADDITION.md +++ /dev/null @@ -1,22 +0,0 @@ -# README Addition for awesome-claude-skills - -Add this line to the **Business & Marketing** section (in alphabetical order): - -```markdown -- [Twitter Intel](./twitter-intel/) - Real-time X/Twitter intelligence - analyze accounts, track topics, and monitor keywords. Powered by BlockRun. -``` - -## Full context - -The line should be added in the Business & Marketing section, alphabetically placed after entries starting with "T" and before entries starting with "U" (if any). - -Example placement: -```markdown -### Business & Marketing - -... -- [Tailored Resume Generator](./tailored-resume-generator/) - ... -- [Twitter Intel](./twitter-intel/) - Real-time X/Twitter intelligence - analyze accounts, track topics, and monitor keywords. Powered by BlockRun. -- [Video Downloader](./video-downloader/) - ... -... -``` diff --git a/docs/awesome-claude-skills-pr/twitter-intel/SKILL.md b/docs/awesome-claude-skills-pr/twitter-intel/SKILL.md deleted file mode 100644 index 4f0c70f..0000000 --- a/docs/awesome-claude-skills-pr/twitter-intel/SKILL.md +++ /dev/null @@ -1,308 +0,0 @@ ---- -name: twitter-intel -description: Real-time X/Twitter intelligence - analyze accounts, track topics, and monitor keywords using live data. Use when you need current social media insights, competitor monitoring, or audience research. ---- - -# Twitter Intel - -Get real-time X/Twitter intelligence without API keys. Analyze accounts, track trending topics, and monitor keywords with live data from X. - -## When to Use This Skill - -- Analyzing a Twitter/X account's recent activity and engagement -- Tracking what people are saying about a topic or hashtag -- Monitoring brand mentions or competitor activity -- Researching audience sentiment and trends -- Getting real-time social data for market research -- Finding influencers or key voices on a topic - -## What This Skill Does - -1. **Account Analysis** (`@username`): Analyzes recent posts, engagement patterns, content style, and audience interactions -2. **Topic Tracking** (`#topic`): Monitors trending discussions, popular posts, and sentiment around hashtags -3. **Keyword Monitoring** (`"keyword"`): Tracks brand mentions, competitor activity, and industry discussions -4. **Engagement Insights**: Provides metrics on likes, replies, and viral potential - -## How to Use - -### Basic Usage - -``` -/twitter-intel @elonmusk -``` - -``` -/twitter-intel #AI -``` - -``` -/twitter-intel "artificial intelligence startups" -``` - -### Natural Language - -You can also use natural language: - -``` -What's @blockrunai posting about lately? -``` - -``` -What's trending about AI agents on X? -``` - -``` -Check Twitter for mentions of "Claude Code" -``` - -### Advanced Usage - -Combine multiple analyses: - -``` -/twitter-intel @competitor1 @competitor2 - compare their content strategies -``` - -``` -/twitter-intel #Web3 - focus on posts from the last 24 hours with high engagement -``` - -## Instructions - -When a user requests Twitter/X intelligence, follow these steps: - -### 1. Install Dependencies (First Time Only) - -If the BlockRun SDK is not installed, install it: - -```bash -pip install blockrun-llm -``` - -### 2. Initialize the Client - -```python -from blockrun_llm import setup_agent_wallet - -client = setup_agent_wallet() -``` - -If this is the first time, the client will display a QR code for funding the wallet. The user needs to add USDC on Base network ($1-5 is enough for many queries). - -### 3. Execute the Query - -**For Account Analysis (@username):** - -```python -response = client.chat( - "xai/grok-3", - f"Analyze @{username}'s recent X/Twitter activity. Include: recent posts, engagement patterns, content themes, posting frequency, and notable interactions.", - search_parameters={ - "mode": "on", - "sources": [ - { - "type": "x", - "included_x_handles": [username], - "post_favorite_count": 5 - } - ], - "max_search_results": 15, - "return_citations": True - } -) -``` - -**For Topic/Hashtag Tracking (#topic):** - -```python -response = client.chat( - "xai/grok-3", - f"What are people saying about #{topic} on X/Twitter right now? Include: trending discussions, popular posts, key voices, and overall sentiment.", - search_parameters={ - "mode": "on", - "sources": [{"type": "x", "post_favorite_count": 50}], - "max_search_results": 20, - "return_citations": True - } -) -``` - -**For Keyword Monitoring ("keyword"):** - -```python -response = client.chat( - "xai/grok-3", - f"Search X/Twitter for mentions of '{keyword}'. Include: recent discussions, sentiment, key influencers mentioning this, and notable posts.", - search_parameters={ - "mode": "on", - "sources": [{"type": "x", "post_favorite_count": 10}], - "max_search_results": 15, - "return_citations": True - } -) -``` - -### 4. Format the Output - -Present results in a clear, actionable format: - -```markdown -# Twitter Intel: @username - -## Overview -- **Account**: @username -- **Recent Activity**: [Summary of posting frequency] -- **Primary Topics**: [Main themes they discuss] - -## Recent Highlights -1. **[Post summary]** - [engagement metrics] - > Quote or key excerpt - -2. **[Post summary]** - [engagement metrics] - > Quote or key excerpt - -## Content Analysis -- **Tone**: [Professional/Casual/Technical/etc.] -- **Engagement Rate**: [High/Medium/Low based on follower count] -- **Best Performing Content**: [What type of posts get most engagement] - -## Key Insights -- [Insight 1] -- [Insight 2] -- [Insight 3] - -## Sources -[Links to referenced posts] -``` - -### 5. Report Costs - -After each query, show the cost: - -```python -spending = client.get_spending() -print(f"Query cost: ${spending['total_usd']:.4f}") -``` - -## Pricing - -- **Per source retrieved**: $0.025 -- **Typical query (10-20 sources)**: $0.25-0.50 -- **Account analysis**: ~$0.38 (15 sources) -- **Topic tracking**: ~$0.50 (20 sources) - -## Examples - -### Example 1: Account Analysis - -**User**: `/twitter-intel @pmarca` - -**Output**: -``` -# Twitter Intel: @pmarca - -## Overview -- **Account**: @pmarca (Marc Andreessen) -- **Recent Activity**: Very active, 5-10 posts daily -- **Primary Topics**: AI, startups, tech policy, venture capital - -## Recent Highlights -1. **Thread on AI regulation** - 2.5K likes, 400 replies - > "The AI moment is different because..." - -2. **Startup advice post** - 1.8K likes - > "The best founders I've met..." - -## Content Analysis -- **Tone**: Intellectual, contrarian, long-form threads -- **Engagement Rate**: Extremely high (10K+ avg likes) -- **Best Performing**: Controversial takes and founder advice - -## Key Insights -- Consistently bullish on AI despite regulatory concerns -- Engages heavily with tech policy debates -- High influence on VC/startup community sentiment - -Query cost: $0.38 -``` - -### Example 2: Topic Tracking - -**User**: `/twitter-intel #AIAgents` - -**Output**: -``` -# Twitter Intel: #AIAgents - -## Trending Now -- Discussions around autonomous coding assistants -- Debate on agent safety and sandboxing -- New tool launches getting attention - -## Top Posts (Last 24h) -1. @developer: "Just built an agent that..." - 500 likes -2. @researcher: "The problem with current agents..." - 320 likes - -## Sentiment Analysis -- **Overall**: Excited but cautious -- **Main concerns**: Safety, costs, reliability -- **Main enthusiasm**: Productivity gains, automation - -## Key Voices -- @karpathy - Technical deep dives -- @swyx - Developer tooling focus -- @anthropic - Safety-focused takes - -Query cost: $0.50 -``` - -### Example 3: Keyword Monitoring - -**User**: `/twitter-intel "Claude Code"` - -**Output**: -``` -# Twitter Intel: "Claude Code" - -## Mention Summary -- **Volume**: Moderate, growing steadily -- **Sentiment**: Very positive -- **Context**: Mostly developer reviews and tips - -## Notable Mentions -1. @dev_influencer: "Claude Code just saved me 3 hours..." - 200 likes -2. @techreview: "Comparing Cursor vs Claude Code..." - 150 likes - -## Common Themes -- Praise for code understanding -- Questions about pricing -- Comparisons to Cursor, Copilot - -## Recommendations -- Engage with comparison discussions -- Address pricing questions proactively -- Amplify positive developer testimonials - -Query cost: $0.38 -``` - -## Tips - -- **Reduce costs**: Use `max_search_results: 5` for quick checks -- **Increase depth**: Use `max_search_results: 30` for comprehensive analysis -- **Filter by engagement**: Increase `post_favorite_count` to focus on viral content -- **Date filtering**: Add `from_date` and `to_date` for time-specific analysis - -## Requirements - -- **BlockRun SDK**: `pip install blockrun-llm` -- **Wallet**: Auto-created on first use, fund with USDC on Base -- **Minimum balance**: $0.50 recommended for a few queries - -## Related Use Cases - -- Competitive intelligence gathering -- Influencer identification for marketing campaigns -- Real-time crisis monitoring -- Product launch sentiment tracking -- Industry trend analysis diff --git a/docs/plans/2026-01-13-budget-tracking-design.md b/docs/plans/2026-01-13-budget-tracking-design.md deleted file mode 100644 index d7bc422..0000000 --- a/docs/plans/2026-01-13-budget-tracking-design.md +++ /dev/null @@ -1,166 +0,0 @@ -# Budget Tracking Design - -**Date:** 2026-01-13 -**Status:** Approved -**Problem:** Spending resets on each session, no budget limits, poor visibility - -## Overview - -Add persistent spending tracking with budget enforcement at the plugin level. Users can set daily budget limits and see cumulative spending across sessions. - -## Data Model - -**File:** `~/.blockrun/spending.json` - -```json -{ - "session_id": "2026-01-13T17:30:00", - "budget_limit": 1.0, - "spending": { - "total_usd": 0.0234, - "calls": 12 - }, - "history": [ - {"timestamp": "2026-01-13T17:31:22", "model": "deepseek/deepseek-chat", "cost": 0.0001}, - {"timestamp": "2026-01-13T17:32:45", "model": "openai/gpt-4o", "cost": 0.0012} - ] -} -``` - -**Key decisions:** -- Session resets daily (new day = fresh budget) -- Budget limit is optional (`null` means no limit) -- History capped at 100 entries -- Atomic writes (temp file + rename) - -## New CLI Flags - -```bash -python run.py --set-budget 1.00 # Set $1 daily limit -python run.py --clear-budget # Remove limit -python run.py --spending # Show spending summary -``` - -## SpendingTracker Module - -**New file:** `scripts/utils/spending.py` - -```python -class SpendingTracker: - """Persistent spending tracker with budget enforcement.""" - - def __init__(self): - self.file = Path.home() / ".blockrun" / "spending.json" - self.data = self._load() - - def _load(self) -> dict: - """Load or create fresh session (resets daily).""" - - def record(self, model: str, cost: float): - """Record a call and save.""" - - def check_budget(self) -> tuple[bool, float]: - """Returns (within_budget, remaining).""" - - def set_budget(self, amount: float): - """Set daily budget limit.""" - - def clear_budget(self): - """Remove budget limit.""" - - def get_total(self) -> float: - """Get total spent this session.""" - - def get_limit(self) -> float | None: - """Get current budget limit.""" -``` - -## Integration with run.py - -```python -def cmd_chat(prompt, model, ...): - tracker = SpendingTracker() - - # CHECK BUDGET BEFORE CALL - within_budget, remaining = tracker.check_budget() - if not within_budget: - branding.print_error("Budget limit reached") - return 1 - - # MAKE THE CALL - response = client.chat(...) - - # RECORD SPENDING - sdk_spending = client.get_spending() - tracker.record(model, sdk_spending['total_usd']) - - # SHOW UPDATED FOOTER - branding.print_footer( - call_cost=sdk_spending['total_usd'], - session_total=tracker.get_total(), - budget_remaining=remaining - ) -``` - -## Updated Output Format - -**Normal footer:** -``` ------------------------------------------------------------- - This call: $0.0010 - Session total: $0.0234 (12 calls) - Budget remaining: $0.9766 of $1.00 - Powered by BlockRun -``` - -**Budget exhausted:** -``` -============================================================ - BUDGET LIMIT REACHED -============================================================ - Spent: $1.0012 across 47 calls - Limit: $1.00/day - - Options: - python run.py --set-budget 2.00 # Increase limit - python run.py --clear-budget # Remove limit - (Budget resets tomorrow) -``` - -**Spending summary (`--spending`):** -``` -============================================================ - SPENDING SUMMARY -============================================================ - Today: $0.0234 across 12 calls - Budget: $1.00 ($0.9766 remaining) - - Recent calls: - 17:31 deepseek/deepseek-chat $0.0001 - 17:32 openai/gpt-4o $0.0012 - 17:35 xai/grok-3 $0.0200 -``` - -## Files to Change - -| File | Action | Changes | -|------|--------|---------| -| `scripts/utils/spending.py` | Create | SpendingTracker class (~80 lines) | -| `scripts/run.py` | Modify | Budget check, record spending, new CLI flags | -| `scripts/utils/branding.py` | Modify | Update footer, add spending summary | - -## Edge Cases - -- **First run:** Creates `~/.blockrun/spending.json` automatically -- **No budget set:** Works as before (no limit enforced) -- **Corrupted file:** Resets to fresh session -- **Concurrent access:** Atomic writes prevent corruption -- **Midnight rollover:** New day starts fresh session - -## Testing Plan - -1. Test fresh install (no spending.json) -2. Test budget enforcement (set $0.01, make calls until blocked) -3. Test session reset (change system date or wait for midnight) -4. Test --spending output -5. Test --clear-budget removes limit diff --git a/plugin.json b/plugin.json deleted file mode 100644 index 831bfbe..0000000 --- a/plugin.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "blockrun", - "version": "1.0.0", - "description": "Give Claude a wallet to access capabilities it lacks - image generation, real-time X/Twitter data, and 30+ AI models via micropayments", - "author": { - "name": "BlockRun", - "email": "care@blockrun.ai" - }, - "homepage": "https://blockrun.ai", - "repository": "https://github.com/BlockRunAI/blockrun-agent-wallet", - "license": "MIT", - "keywords": [ - "blockrun", - "image-generation", - "dall-e", - "grok", - "gpt", - "deepseek", - "x402", - "micropayments", - "ai-routing" - ] -} diff --git a/references/MODEL_GUIDE.md b/references/MODEL_GUIDE.md deleted file mode 100644 index e84f4d2..0000000 --- a/references/MODEL_GUIDE.md +++ /dev/null @@ -1,67 +0,0 @@ -# BlockRun Model Guide - -## Live Model List - -**Always up-to-date at:** https://blockrun.ai/api/pricing - -View in terminal: -```bash -python run.py --models -``` - -Or fetch directly: -```bash -curl https://blockrun.ai/api/pricing | jq -``` - -## Smart Routing - -BlockRun automatically selects models based on your request: - -| Keywords in Prompt | Selected Model | -|--------------------|----------------| -| twitter, X, trending | xai/grok-3 | -| code, python, debug | anthropic/claude-sonnet-4 | -| math, proof, logic | openai/o1-mini | -| document, summarize | google/gemini-2.0-flash | - -Override with `--model` flag: -```bash -python run.py "prompt" --model openai/o1 -``` - -## Choosing the Right Model - -### For Coding -- anthropic/claude-sonnet-4 (best) -- openai/gpt-5.2 (alternative) -- deepseek/deepseek-chat (budget) - -### For Reasoning/Math -- openai/o1 (best) -- openai/o1-mini (balanced) -- deepseek/deepseek-r1 (budget) - -### For Real-Time X/Twitter Data -- xai/grok-3 (only option with live X access) - -### For Long Documents -- google/gemini-2.0-flash (1M+ context) - -### For Budget Operations -- deepseek/deepseek-chat (cheapest) - -## Image Generation - -View available image models: -```bash -curl https://blockrun.ai/api/v1/images/models | jq -``` - -Common options: -- google/nano-banana (artistic, fast) -- openai/dall-e-3 (photorealistic) - ---- - -*Model list and pricing from live API - always current.* diff --git a/references/PRICING.md b/references/PRICING.md deleted file mode 100644 index 694569f..0000000 --- a/references/PRICING.md +++ /dev/null @@ -1,61 +0,0 @@ -# BlockRun Pricing Guide - -## Live Pricing - -**Always up-to-date at:** https://blockrun.ai/api/pricing - -View in terminal: -```bash -python run.py --models -``` - -## How Pricing Works - -1. **No Subscriptions** - Pay per request only -2. **No API Keys** - Your wallet IS your API key -3. **Transparent** - Prices from API -4. **Instant** - Payments settle on Base chain - -## Cost Optimization Tips - -### Use `--cheap` Flag -```bash -python run.py "Simple task" --cheap -# Routes to DeepSeek (cheapest) -``` - -### Use `--fast` Flag -```bash -python run.py "Quick question" --fast -# Routes to GPT-5-mini (fastest) -``` - -### Choose Right Model for Task -- **Quick questions**: gpt-5-mini, claude-haiku -- **Bulk processing**: deepseek-chat -- **Quality matters**: gpt-5.2, claude-sonnet-4 - -## Funding Your Wallet - -1. Buy USDC on Coinbase -2. Send to your wallet address (shown on first run) -3. Start using models - -Check balance: -```bash -python run.py --balance -``` - -## What $1 USDC Gets You - -Approximate calls per $1: -- GPT-5: ~1,000 calls -- DeepSeek: ~10,000 calls -- Grok: ~500 calls -- DALL-E images: ~20 images - -**$1 is enough for weeks of normal use.** - ---- - -*Prices from live API - always current.* From 0cf518f04ea41e2f81186fd83344cfbd36880e2a Mon Sep 17 00:00:00 2001 From: Arthur Chiu Date: Sun, 18 Jan 2026 09:06:44 -0800 Subject: [PATCH 3/7] refactor: restructure plugin manifests for canonical format - Add .claude-plugin/plugin.json with full metadata - Update marketplace.json source to "./" (whole repo is plugin) --- .claude-plugin/marketplace.json | 5 +++-- .claude-plugin/plugin.json | 13 +++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 .claude-plugin/plugin.json diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index bb67e31..ac18a69 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -7,8 +7,9 @@ "plugins": [ { "name": "blockrun", - "source": "./skills/blockrun", - "description": "Give Claude Code a wallet to pay for GPT, Grok, DALL-E and 30+ AI models via USDC micropayments" + "description": "Give Claude a wallet for image generation, real-time X/Twitter, and 30+ AI models", + "version": "1.0.0", + "source": "./" } ] } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..8cf0c52 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,13 @@ +{ + "name": "blockrun", + "version": "1.0.0", + "description": "Give Claude a wallet to access capabilities it lacks - image generation, real-time X/Twitter data, and 30+ AI models via micropayments", + "author": { + "name": "BlockRun", + "email": "care@blockrun.ai" + }, + "homepage": "https://blockrun.ai", + "repository": "https://github.com/BlockRunAI/blockrun-agent-wallet", + "license": "MIT", + "keywords": ["blockrun", "image-generation", "dall-e", "grok", "x402", "micropayments"] +} From 1711585b6925c0365cb3fd2087ae9927caa02e9a Mon Sep 17 00:00:00 2001 From: Arthur Chiu Date: Sun, 18 Jan 2026 09:07:35 -0800 Subject: [PATCH 4/7] docs: move user guides to docs/ directory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - BULLETPROOF_QUICKSTART.md → docs/QUICKSTART.md - USDC_ON_BASE.md → docs/USDC_ON_BASE.md --- BULLETPROOF_QUICKSTART.md => docs/QUICKSTART.md | 0 USDC_ON_BASE.md => docs/USDC_ON_BASE.md | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename BULLETPROOF_QUICKSTART.md => docs/QUICKSTART.md (100%) rename USDC_ON_BASE.md => docs/USDC_ON_BASE.md (100%) diff --git a/BULLETPROOF_QUICKSTART.md b/docs/QUICKSTART.md similarity index 100% rename from BULLETPROOF_QUICKSTART.md rename to docs/QUICKSTART.md diff --git a/USDC_ON_BASE.md b/docs/USDC_ON_BASE.md similarity index 100% rename from USDC_ON_BASE.md rename to docs/USDC_ON_BASE.md From ab4d5f7cef85718a4a52c4fad3f65807534b9d9c Mon Sep 17 00:00:00 2001 From: Arthur Chiu Date: Sun, 18 Jan 2026 09:09:04 -0800 Subject: [PATCH 5/7] feat: add prerequisite to skills, remove redundant install.sh Skills now document pip install blockrun-llm as prerequisite --- skills/blockrun/SKILL.md | 2 ++ skills/twitter-intel/SKILL.md | 2 ++ skills/twitter-intel/install.sh | 22 ---------------------- 3 files changed, 4 insertions(+), 22 deletions(-) delete mode 100755 skills/twitter-intel/install.sh diff --git a/skills/blockrun/SKILL.md b/skills/blockrun/SKILL.md index 546153c..fcde4c8 100644 --- a/skills/blockrun/SKILL.md +++ b/skills/blockrun/SKILL.md @@ -7,6 +7,8 @@ allowed-tools: Read, Bash(python:*), Bash(python3:*), Bash(pip:*), Bash(source:* # BlockRun +**Prerequisite:** `pip install blockrun-llm` + **BlockRun works with Claude Code and Google Antigravity.** You can't generate images. You have no real-time X/Twitter access. BlockRun gives you these capabilities. diff --git a/skills/twitter-intel/SKILL.md b/skills/twitter-intel/SKILL.md index 4f0c70f..440801c 100644 --- a/skills/twitter-intel/SKILL.md +++ b/skills/twitter-intel/SKILL.md @@ -5,6 +5,8 @@ description: Real-time X/Twitter intelligence - analyze accounts, track topics, # Twitter Intel +**Prerequisite:** `pip install blockrun-llm` + Get real-time X/Twitter intelligence without API keys. Analyze accounts, track trending topics, and monitor keywords with live data from X. ## When to Use This Skill diff --git a/skills/twitter-intel/install.sh b/skills/twitter-intel/install.sh deleted file mode 100755 index ee00d31..0000000 --- a/skills/twitter-intel/install.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash -# Twitter Intel Skill - Dependency Installation -# Installs the BlockRun SDK for real-time X/Twitter data access - -set -e - -echo "Installing Twitter Intel dependencies..." - -# Install BlockRun SDK -pip install blockrun-llm - -echo "" -echo "Twitter Intel skill installed successfully!" -echo "" -echo "Usage:" -echo " /twitter-intel @username - Analyze an X/Twitter account" -echo " /twitter-intel #topic - Track a hashtag or topic" -echo " /twitter-intel \"keyword\" - Monitor keyword mentions" -echo "" -echo "First-time setup:" -echo " A wallet will be auto-created. Fund it with USDC on Base network." -echo " Recommended: \$1-5 for many queries (\$0.25-0.50 per query)" From 92294535fcc9895c90d1f3511c37f8cd396201b0 Mon Sep 17 00:00:00 2001 From: Arthur Chiu Date: Sun, 18 Jan 2026 09:10:56 -0800 Subject: [PATCH 6/7] docs: update installation instructions for plugin format Remove curl installer, focus on Claude Code plugin installation --- README.md | 26 ++++++-------------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 241af32..6a6baf1 100644 --- a/README.md +++ b/README.md @@ -86,40 +86,26 @@ Your agent has a USDC balance. When it needs a capability, it pays. You set the ## Install -### One Command +### Step 1: Install the Python SDK -```bash -curl -fsSL https://raw.githubusercontent.com/BlockRunAI/blockrun-agent-wallet/main/install.sh | bash -``` - -Auto-detects Claude Code or Antigravity and installs everything. - -### Manual Install - -**Step 1: Install the Python SDK** ```bash pip install blockrun-llm ``` -**Step 2: Install the skill for your platform** +### Step 2: Install the Claude Code plugin -**Claude Code (Option A - Plugin Marketplace):** +**Option A - Plugin Marketplace:** ``` /plugin marketplace add BlockRunAI/blockrun-agent-wallet /plugin install blockrun ``` -**Claude Code (Option B - Git Clone):** -```bash -git clone https://github.com/BlockRunAI/blockrun-agent-wallet ~/.claude/skills/blockrun -``` - -**Antigravity (global):** +**Option B - Git Clone:** ```bash -git clone https://github.com/BlockRunAI/blockrun-agent-wallet ~/.gemini/antigravity/skills/blockrun +git clone https://github.com/BlockRunAI/blockrun-agent-wallet ~/.claude/plugins/blockrun ``` -### Verify +### Step 3: Verify ```bash python3 -c "from blockrun_llm import status; status()" From 6b45cb3968b30835eaa466b07dbed8925e0242cd Mon Sep 17 00:00:00 2001 From: Arthur Chiu Date: Mon, 19 Jan 2026 15:35:38 -0800 Subject: [PATCH 7/7] restore keywords --- .claude-plugin/plugin.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 8cf0c52..aee0c30 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -9,5 +9,5 @@ "homepage": "https://blockrun.ai", "repository": "https://github.com/BlockRunAI/blockrun-agent-wallet", "license": "MIT", - "keywords": ["blockrun", "image-generation", "dall-e", "grok", "x402", "micropayments"] + "keywords": ["blockrun", "image-generation", "dall-e", "grok", "gpt", "deepseek", "x402", "micropayments", "ai-routing"] }