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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,89 @@
# ubounty

Enable maintainers to clear their backlog with one command. Turn "I'll fix this someday" into "Done in 5 minutes."

## Installation

```bash
# Install from source
pip install -e .

# Or with pipx (recommended)
pipx install .
```

## Usage

### Browse Bounties

Discover open bounties from GitHub:

```bash
# List all bounties
ubounty browse

# Filter by programming language
ubounty browse --language python

# Filter by minimum amount
ubounty browse --min-amount 100

# Filter by difficulty
ubounty browse --difficulty easy

# Combine filters
ubounty browse --language javascript --min-amount 50 --max-amount 500

# Pagination
ubounty browse --page 2 --per-page 20
```

### Wallet Management

Connect your wallet to receive bounty payouts:

```bash
# Connect a wallet (interactive)
ubounty wallet connect

# Connect with flags
ubounty wallet connect --address 0x123... --type ethereum

# Show connected wallet
ubounty wallet show

# Disconnect wallet
ubounty wallet disconnect
```

## Features

### Browse Command
- 🔍 Search GitHub for bounty-labeled issues
- 🏷️ Filter by language, amount, and difficulty
- 📊 Clean table output with rich formatting
- 📄 Pagination support for large result sets

### Wallet Command
- 🔐 Secure local storage (~/.ubounty/wallet.json)
- ✅ Address format validation
- 🔗 Support for Ethereum, Solana, Bitcoin, and custom wallets
- ⚠️ Overwrite protection with confirmation prompts

## Development

```bash
# Clone the repo
git clone https://github.com/ubounty-app/ubounty.git
cd ubounty

# Install in development mode
pip install -e ".[dev]"

# Run the CLI
ubounty --help
```

## License

MIT
22 changes: 22 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[project]
name = "ubounty"
version = "0.1.0"
description = "Enable maintainers to clear their backlog with one command"
readme = "README.md"
license = {text = "MIT"}
requires-python = ">=3.8"
dependencies = [
"click>=8.0.0",
"requests>=2.28.0",
"rich>=13.0.0",
]

[project.scripts]
ubounty = "ubounty.cli:main"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["ubounty"]
3 changes: 3 additions & 0 deletions ubounty/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""Ubounty CLI - Enable maintainers to clear their backlog with one command."""

__version__ = "0.1.0"
185 changes: 185 additions & 0 deletions ubounty/browse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
"""Browse command for discovering bounties."""

from typing import Optional, List, Dict

import click
import requests
from rich.console import Console
from rich.table import Table
from rich.panel import Panel
from rich.text import Text

console = Console()

GITHUB_API_BASE = "https://api.github.com"


def search_bounties(
language: Optional[str] = None,
min_amount: Optional[int] = None,
max_amount: Optional[int] = None,
difficulty: Optional[str] = None,
page: int = 1,
per_page: int = 10,
) -> List[Dict]:
"""Search GitHub for bounty-labeled issues."""
# Build search query
query_parts = ["label:bounty", "state:open"]

if language:
query_parts.append(f"language:{language}")

query = " ".join(query_parts)

headers = {
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
}

params = {
"q": query,
"sort": "updated",
"order": "desc",
"per_page": per_page,
"page": page,
}

try:
response = requests.get(
f"{GITHUB_API_BASE}/search/issues",
headers=headers,
params=params,
timeout=10,
)
response.raise_for_status()
data = response.json()

items = data.get("items", [])

# Filter by amount if specified
filtered_items = []
for item in items:
amount = extract_amount(item)

if min_amount and amount and amount < min_amount:
continue
if max_amount and amount and amount > max_amount:
continue
if difficulty:
item_difficulty = extract_difficulty(item)
if item_difficulty and item_difficulty.lower() != difficulty.lower():
continue

item["_parsed_amount"] = amount
filtered_items.append(item)

return filtered_items

except requests.RequestException as e:
console.print(f"[red]Error fetching bounties: {e}[/red]")
return []


def extract_amount(issue: dict) -> Optional[int]:
"""Extract bounty amount from issue title or labels."""
# Check labels for amount patterns
for label in issue.get("labels", []):
name = label.get("name", "").lower()
# Match patterns like "$100", "$1000", "100 usdc", etc.
import re
match = re.search(r"\$?(\d+)\s*(usdc|usd|usdt)?", name, re.IGNORECASE)
if match:
return int(match.group(1))

# Check title
title = issue.get("title", "")
import re
match = re.search(r"\$(\d+)|(\d+)\s*(usdc|usd)", title, re.IGNORECASE)
if match:
return int(match.group(1) or match.group(2))

return None


def extract_difficulty(issue: dict) -> Optional[str]:
"""Extract difficulty from issue labels."""
for label in issue.get("labels", []):
name = label.get("name", "").lower()
if any(d in name for d in ["easy", "beginner", "good first"]):
return "easy"
if any(d in name for d in ["medium", "intermediate"]):
return "medium"
if any(d in name for d in ["hard", "advanced", "expert"]):
return "hard"
return None


def display_bounties(bounties: List[Dict], page: int, per_page: int) -> None:
"""Display bounties in a clean table format."""
if not bounties:
console.print(Panel("[yellow]No bounties found matching your criteria.[/yellow]"))
return

table = Table(
title=f"🎯 Open Bounties (Page {page})",
show_header=True,
header_style="bold magenta",
)

table.add_column("Amount", style="green", width=10)
table.add_column("Title", style="cyan", max_width=50)
table.add_column("Repo", style="blue", width=30)
table.add_column("Updated", style="dim", width=12)

for bounty in bounties:
amount = bounty.get("_parsed_amount")
amount_str = f"${amount}" if amount else "TBD"

title = bounty.get("title", "")[:47]
if len(bounty.get("title", "")) > 47:
title += "..."

repo_url = bounty.get("repository_url", "")
repo = repo_url.replace("https://api.github.com/repos/", "") if repo_url else "N/A"

updated = bounty.get("updated_at", "")[:10]

table.add_row(amount_str, title, repo, updated)

console.print(table)
console.print(f"\n[dim]Showing {len(bounties)} results. Use --page for more.[/dim]")


@click.command()
@click.option("--language", "-l", help="Filter by programming language (e.g., python, javascript)")
@click.option("--min-amount", "-min", type=int, help="Minimum bounty amount in USD")
@click.option("--max-amount", "-max", type=int, help="Maximum bounty amount in USD")
@click.option("--difficulty", "-d", type=click.Choice(["easy", "medium", "hard"], case_sensitive=False), help="Filter by difficulty")
@click.option("--page", "-p", default=1, type=int, help="Page number for pagination")
@click.option("--per-page", default=10, type=int, help="Results per page (max 30)")
def browse(language, min_amount, max_amount, difficulty, page, per_page):
"""Browse available bounties from GitHub.

Discover open bounties with filters for language, amount, and difficulty.

Examples:

ubounty browse # List all bounties
ubounty browse --language python # Python bounties only
ubounty browse --min-amount 100 # $100+ bounties
ubounty browse --difficulty easy # Easy bounties
"""
console.print("[bold]🔍 Searching for bounties...[/bold]\n")

per_page = min(per_page, 30) # GitHub API limit

bounties = search_bounties(
language=language,
min_amount=min_amount,
max_amount=max_amount,
difficulty=difficulty,
page=page,
per_page=per_page,
)

display_bounties(bounties, page, per_page)
21 changes: 21 additions & 0 deletions ubounty/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""Main CLI entry point for ubounty."""

import click

from ubounty.browse import browse
from ubounty.wallet import wallet


@click.group()
@click.version_option()
def main():
"""Ubounty CLI - Discover and claim bounties from your terminal."""
pass


main.add_command(browse)
main.add_command(wallet)


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